「OpenAI GPT に相談」の版間の差分
ナビゲーションに移動
検索に移動
タグ: モバイル編集 モバイルウェブ編集 |
編集の要約なし |
||
| 10行目: | 10行目: | ||
* 応答内容は履歴として保持し、次回のリクエストに利用する | * 応答内容は履歴として保持し、次回のリクエストに利用する | ||
後述のソースコードは、従来版スクリプトエンジンで動作するものとV8エンジンで動作するものの2種類を掲載しています。 | |||
V8エンジン版の追加仕様: | |||
* ストリーミングモードでリアルタイムに1文字ずつMeryに入力されていくため、さながらChatGPT本家のような雰囲気を楽しむことができる | |||
* Shiftキー長押しで、テキスト生成を中断できる | |||
== 注意事項 (必ず読んでからご利用ください) == | == 注意事項 (必ず読んでからご利用ください) == | ||
| 54行目: | 59行目: | ||
const HISTORY_SIZE = 4; | const HISTORY_SIZE = 4; | ||
// | // 1回の入力での文字数 | ||
const INPUT_DELAY = | const INPUT_LENGTH = 3; | ||
// 生成AI応答の入力毎の遅延時間 (ミリ秒) | |||
const INPUT_DELAY = 20; | |||
// コンテキストメニュー番号 | // コンテキストメニュー番号 | ||
| 70行目: | 78行目: | ||
const doc = document; | const doc = document; | ||
const sel = document.selection; | const sel = document.selection; | ||
// 生成リクエスト中止フラグ | |||
let abortRequestFlg = false | |||
// 生成リクエスト完了フラグ; | |||
let requestCompletedFlg = false; | |||
main(); | main(); | ||
| 83行目: | 96行目: | ||
const promptText = sel.Text.trim(); | const promptText = sel.Text.trim(); | ||
const promptLength = sel.TextLength; | const promptLength = sel.TextLength; | ||
// コンテキストメニューの作成 | while (true) { | ||
// コンテキストメニューの作成 | |||
const menu = createContextMenu(selectionIsEmpty); | |||
const menuTrack = menu.Track(0); | |||
// メニュー選択がキャンセルされた場合は終了 | |||
if (menuTrack === 0) return; | |||
// 会話履歴のリセット | |||
if (menuTrack === 2) { | |||
resetTag(); | |||
continue; | |||
} | |||
break; | |||
} | } | ||
| 125行目: | 141行目: | ||
// GPT API に問い合わせ | // GPT API に問い合わせ | ||
sel.Text = "\n---\n\n" | sel.Text = "\n---\n\n" | ||
await callGPTAPI(apiKey, promptText); | await Promise.all([ | ||
callGPTAPI(apiKey, promptText), | |||
abortRequest(), | |||
]); | |||
sel.Text = "\n\n---\n\n\n" | sel.Text = "\n\n---\n\n\n" | ||
} catch (e) { | } catch (e) { | ||
console.log("エラーが発生しました: " + e.message + "\n" + e.stack); | console.log("エラーが発生しました: " + e.message + "\n" + e.stack); | ||
sel.Text = " | sel.Text = "<処理中にエラー発生>" | ||
} finally { | } finally { | ||
// 処理終了待機を解除 | // 処理終了待機を解除 | ||
| 151行目: | 169行目: | ||
} | } | ||
return menu; | return menu; | ||
} | |||
// Shift キーが長押しされた場合はリクエスト中止フラグを立てる | |||
async function abortRequest() { | |||
const VIRTUAL_KEY_CODE_SHIFT = 0x10; | |||
let pressCount = 0; | |||
while (!requestCompletedFlg) { | |||
await new Promise(resolve => setTimeout(resolve, 300)); | |||
if (shell.GetKeyState(VIRTUAL_KEY_CODE_SHIFT) < 0) { | |||
pressCount++; | |||
if (pressCount > 2) { | |||
abortRequestFlg = true; | |||
return; | |||
} | |||
} | |||
} | |||
} | } | ||
async function callGPTAPI(apiKey, promptText) { | async function callGPTAPI(apiKey, promptText) { | ||
const controller = new AbortController(); | |||
const signal = controller.signal; | |||
try { | try { | ||
// fetch で OpenAI API にリクエスト | // fetch で OpenAI API にリクエスト | ||
// stream: true, stream_options: { "include_usage": true } で利用トークン数を取得 | // stream: true, stream_options: { "include_usage": true } で利用トークン数を取得 | ||
const response = await fetch('https://api.openai.com/v1/chat/completions', { | const response = await fetch('https://api.openai.com/v1/chat/completions', { | ||
signal: signal, | |||
method: 'POST', | method: 'POST', | ||
headers: { | headers: { | ||
| 178行目: | 215行目: | ||
let stopFlg = false; | let stopFlg = false; | ||
while (true) { | while (true) { | ||
// リクエスト中止フラグが立っている場合は中断 | |||
if (abortRequestFlg) { | |||
controller.abort(); | |||
sel.Text = "<処理を中止>" | |||
return; | |||
} | |||
const { done, value } = await reader.read(); | const { done, value } = await reader.read(); | ||
if (done) { | if (done) { | ||
| 190行目: | 234行目: | ||
const lines = text.split(/\n+/); | const lines = text.split(/\n+/); | ||
let usageText = ""; | |||
let inputText = ""; | |||
for (const line of lines) { | for (const line of lines) { | ||
const json_text = line.replace(/^data:\s*/, ''); | const json_text = line.replace(/^data:\s*/, ''); | ||
| 206行目: | 252行目: | ||
if (stopFlg) { | if (stopFlg) { | ||
const usage = data.usage; | const usage = data.usage; | ||
usageText = "利用トークン数: プロンプト=" + usage.prompt_tokens | |||
+ ", 応答=" + usage.completion_tokens | + ", 応答=" + usage.completion_tokens | ||
+ ", 合計=" + usage.total_tokens; | + ", 合計=" + usage.total_tokens; | ||
break; | |||
} | } | ||
| 218行目: | 263行目: | ||
// メッセージの終了理由が "stop" の場合は次のループでトークン数の取得を行う | // メッセージの終了理由が "stop" の場合は次のループでトークン数の取得を行う | ||
stopFlg = true; | stopFlg = true; | ||
} | continue; | ||
} | |||
// 応答内容をテキスト配列に追加 | |||
const content = choice.delta.content; | |||
if (content) { | |||
inputText += content; | |||
} | } | ||
} | } | ||
// 上記までで数文字まとまって取得されるので、まとめて入力 | |||
sel.Text = inputText; | |||
// トークン数文字列がある場合は入力 | |||
if (usageText) { | |||
sel.Text = `\n\n(${usageText})`; | |||
} | |||
// 会話履歴用に応答内容を追加 | |||
responseMessage += inputText; | |||
// 入力を少しゆっくりにする | |||
await new Promise(resolve => setTimeout(resolve, INPUT_DELAY)); | |||
} | } | ||
| 236行目: | 289行目: | ||
} | } | ||
} catch (e) { | } catch (e) { | ||
controller.abort(); | |||
throw e; | throw e; | ||
} finally { | |||
requestCompletedFlg = true; | |||
} | } | ||
} | } | ||
2024年12月21日 (土) 18:09時点における版
概要
OpenAIの生成AIである「GPT」を利用するためのMeryマクロです。
このマクロは、以下の流れで動作します:
- ユーザーがエディタ内でテキストを選択する (未選択の場合、アクティブ行が対象になる)
- 選択したテキストをプロンプトとしてOpenAI APIに送信する
- APIからの応答を取得し、エディタに表示する
- 応答内容は履歴として保持し、次回のリクエストに利用する
後述のソースコードは、従来版スクリプトエンジンで動作するものとV8エンジンで動作するものの2種類を掲載しています。
V8エンジン版の追加仕様:
- ストリーミングモードでリアルタイムに1文字ずつMeryに入力されていくため、さながらChatGPT本家のような雰囲気を楽しむことができる
- Shiftキー長押しで、テキスト生成を中断できる
注意事項 (必ず読んでからご利用ください)
- 本マクロではOpenAI APIを利用します。API利用に伴う料金発生やOpenAI社の情報取り扱い等によるユーザー不利益が発生しても、作者は一切の責任を負いません。
- OpenAIでの支払いはチャージ制なので、あらかじめチャージした金額内での利用になります。オートチャージの設定もありますが、慣れていないうちはオートチャージOFFでの利用を推奨します。また、誤って高額の課金にならないように上限を設けることもできますので、併せて上限設定の利用を強くおすすめします。
- 2024/12/18 時点、本マクロでは「gpt-4o-mini」モデルを利用しており、費用は従量課金ですが「$0.150/100万入力トークン、 $0.600/100万出力トークン」と低価格です。目安として、日本語1文字 1~2 トークン程で計上されますが、よほど大量の文字の入出力を繰り返さない限りは月間数ドルで利用できると思われます。
- OpenAI APIでは受け取ったリクエストを学習に使わないと宣言していますが、くれぐれも機密情報やプライバシーに関わる情報を指定しないように注意してください。
- OpenAI APIは、 OpenAI利用規約 に則ってご利用ください。
利用前提
- Mery Ver 3.7.8 以上
- OpenAI Platform で、OpenAI APIキーを取得していること
- OpenAI APIを利用できるだけの課金残高があること
利用方法
- 環境変数の設定
- コマンド プロンプトで以下のコマンドを実行し、環境変数を設定してください。
setx OPENAI_API_KEY "***************************************"
- 設定後、Mery または Windows の再起動が必要な場合があります。
- マクロの実行
- プロンプトにしたい文字列を選択してマクロを実行すると、選択したテキストがプロンプトとしてOpenAI APIに送信され、応答がエディタに表示されます。
- 会話の履歴数を調整
- 会話の履歴は設定した個数分が保持され、文脈として利用されます。適宜、ソースコード内の「HISTORY_SIZE」の数を調整してください。
- ※数を増やすほど会話内容を保持して文脈を理解するようになりますが、メモリ使用量が増えたり動作が重くなる可能性があります。
- ※開いている文書ごとに履歴を保持します。文脈をリセットしたいときは、新しい文書を開いて会話をやり直してください。
ソースコード (V8エンジン版)
#language = "v8"
#title = "GPT に相談 (v8版)"
BeginUndoGroup();
// 会話の履歴を保持する数 (質問と回答で1セット)
// 数を増やすと、会話の履歴が増えますが、Meryのメモリ使用量が増えたり、
// 動作が重くなる可能性があるため、適宜調整してください。
// document.Tag に保持されるため、開いているファイルを閉じると履歴は消えます。
const HISTORY_SIZE = 4;
// 1回の入力での文字数
const INPUT_LENGTH = 3;
// 生成AI応答の入力毎の遅延時間 (ミリ秒)
const INPUT_DELAY = 20;
// コンテキストメニュー番号
const EXECUTE_AI = 1;
const RESET_HISTORY = 2;
const ConsoleClass = function () {
this.log = function (str) {
outputBar.Writeln(str)
}
}
const console = new ConsoleClass();
const doc = document;
const sel = document.selection;
// 生成リクエスト中止フラグ
let abortRequestFlg = false
// 生成リクエスト完了フラグ;
let requestCompletedFlg = false;
main();
async function main() {
const selectionIsEmpty = sel.IsEmpty
// プロンプトの取得
if (selectionIsEmpty) {
sel.StartOfLine(false, mePosLogical);
sel.EndOfLine(true, mePosLogical);
}
const promptText = sel.Text.trim();
const promptLength = sel.TextLength;
while (true) {
// コンテキストメニューの作成
const menu = createContextMenu(selectionIsEmpty);
const menuTrack = menu.Track(0);
// メニュー選択がキャンセルされた場合は終了
if (menuTrack === 0) return;
// 会話履歴のリセット
if (menuTrack === 2) {
resetTag();
continue;
}
break;
}
// プロンプトが空の場合はエラー
if (promptLength === 0) {
alert("プロンプトを入力してください。");
return;
}
// 文字数が多すぎる場合はエラー
if (promptLength > 10000) {
alert("プロンプトの文字数が多すぎます。10000 文字以下にしてください。");
return;
}
// 読み取り専用の場合はエラー
if (doc.ReadOnly) {
alert("ファイルが読み取り専用のため実行できません。");
return;
}
try {
// 処理終了を待機
shell.KeepRunning = true;
// カーソル位置移動
sel.SetActivePoint(mePosLogical, sel.GetBottomPointX(mePosLogical), sel.GetBottomPointY(mePosLogical));
sel.EndOfLine(false, mePosLogical);
sel.NewLine();
// APIキーの取得
const apiKey = shell.getEnv("OPENAI_API_KEY");
// GPT API に問い合わせ
sel.Text = "\n---\n\n"
await Promise.all([
callGPTAPI(apiKey, promptText),
abortRequest(),
]);
sel.Text = "\n\n---\n\n\n"
} catch (e) {
console.log("エラーが発生しました: " + e.message + "\n" + e.stack);
sel.Text = "<処理中にエラー発生>"
} finally {
// 処理終了待機を解除
shell.KeepRunning = false;
}
}
function createContextMenu(selectionIsEmpty) {
const menu = CreatePopupMenu();
menu.Add("※注意※ プライバシー・機密情報を送信しないこと", 0);
menu.Add("", 0, meMenuSeparator);
if (selectionIsEmpty) {
menu.Add("アクティブ行の内容で相談 (&Q)", EXECUTE_AI);
} else {
menu.Add("選択範囲の内容で相談 (&Q)", EXECUTE_AI);
}
if (document.Tag.exists("gpt-text")) {
const turn = JSON.parse(document.Tag["gpt-text"]).length / 2;
menu.Add("会話履歴をリセット (現在の会話数: " + turn + ") (&R)", RESET_HISTORY);
}
return menu;
}
// Shift キーが長押しされた場合はリクエスト中止フラグを立てる
async function abortRequest() {
const VIRTUAL_KEY_CODE_SHIFT = 0x10;
let pressCount = 0;
while (!requestCompletedFlg) {
await new Promise(resolve => setTimeout(resolve, 300));
if (shell.GetKeyState(VIRTUAL_KEY_CODE_SHIFT) < 0) {
pressCount++;
if (pressCount > 2) {
abortRequestFlg = true;
return;
}
}
}
}
async function callGPTAPI(apiKey, promptText) {
const controller = new AbortController();
const signal = controller.signal;
try {
// fetch で OpenAI API にリクエスト
// stream: true, stream_options: { "include_usage": true } で利用トークン数を取得
const response = await fetch('https://api.openai.com/v1/chat/completions', {
signal: signal,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + apiKey,
},
body: JSON.stringify({
model: 'gpt-4o-mini',
messages: createGPTRequest(promptText),
stream: true,
stream_options: { "include_usage": true },
temperature: 0.7,
max_tokens: 4000,
})
});
let responseMessage = "";
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
let stopFlg = false;
while (true) {
// リクエスト中止フラグが立っている場合は中断
if (abortRequestFlg) {
controller.abort();
sel.Text = "<処理を中止>"
return;
}
const { done, value } = await reader.read();
if (done) {
break;
}
// 受信したバイナリデータ (value) をテキストに変換する
const text = decoder.decode(value);
// テキストには複数のメッセージが含まれている可能性があるため、
// 改行 (LF) で分離して、1 行ずつ処理する
const lines = text.split(/\n+/);
let usageText = "";
let inputText = "";
for (const line of lines) {
const json_text = line.replace(/^data:\s*/, '');
// データがない場合はスキップ
if (!json_text) {
continue;
}
// データが終了した場合は終了
if (json_text === '[DONE]') {
break;
}
const data = JSON.parse(json_text);
// 終了時、利用トークン数を表示
if (stopFlg) {
const usage = data.usage;
usageText = "利用トークン数: プロンプト=" + usage.prompt_tokens
+ ", 応答=" + usage.completion_tokens
+ ", 合計=" + usage.total_tokens;
break;
}
const choice = data.choices[0];
if (choice.finish_reason === "stop") {
// メッセージの終了理由が "stop" の場合は次のループでトークン数の取得を行う
stopFlg = true;
continue;
}
// 応答内容をテキスト配列に追加
const content = choice.delta.content;
if (content) {
inputText += content;
}
}
// 上記までで数文字まとまって取得されるので、まとめて入力
sel.Text = inputText;
// トークン数文字列がある場合は入力
if (usageText) {
sel.Text = `\n\n(${usageText})`;
}
// 会話履歴用に応答内容を追加
responseMessage += inputText;
// 入力を少しゆっくりにする
await new Promise(resolve => setTimeout(resolve, INPUT_DELAY));
}
// 結果を会話履歴に追加
if (HISTORY_SIZE > 0) {
appendReturnTextToTag(promptText, responseMessage);
}
} catch (e) {
controller.abort();
throw e;
} finally {
requestCompletedFlg = true;
}
}
// document.Tag の内容を加味してGPTのリクエストを作成
function createGPTRequest(promptText) {
const tag = document.Tag;
// 以前の会話が存在しない場合
if (!tag.exists("gpt-text")) {
return [{
"role": "user",
"content": promptText
}];
}
const gptText = JSON.parse(tag["gpt-text"]);
gptText.push({
"role": "user",
"content": promptText
});
return gptText;
}
// document.Tag にGPTの応答を追加
function appendReturnTextToTag(userText, modelText) {
const tag = document.Tag;
if (!tag.exists("gpt-text")) {
tag["gpt-text"] = JSON.stringify([
{
"role": "user",
"content": userText
},
{
"role": "assistant",
"content": modelText
}
]);
} else {
const gptText = JSON.parse(tag["gpt-text"]);
gptText.push({
"role": "user",
"content": userText
});
gptText.push({
"role": "assistant",
"content": modelText
});
// 会話セット (user, model 1往復) が特定セット数を超えた場合、最初のセットを削除
while (gptText.length > HISTORY_SIZE * 2) {
gptText.shift();
}
tag["gpt-text"] = JSON.stringify(gptText);
}
}
function resetTag() {
if (document.Tag.exists("gpt-text")) {
document.Tag.remove("gpt-text");
}
}
ソースコード (従来エンジン版)
#title = "GPT に相談"
BeginUndoGroup();
// 会話の履歴を保持する数 (質問と回答で1セット)
// 数を増やすと、会話の履歴が増えますが、Meryのメモリ使用量が増えたり、
// 動作が重くなる可能性があるため、適宜調整してください。
// document.Tag に保持されるため、開いているファイルを閉じると履歴は消えます。
var HISTORY_SIZE = 4;
// コンテキストメニュー番号
var EXECUTE_AI = 1;
var RESET_HISTORY = 2;
var ConsoleClass = function () {
this.log = function (str) {
outputBar.Writeln(str)
}
}
var console = new ConsoleClass();
var doc = document;
var sel = document.selection;
main();
function main() {
var selectionIsEmpty = sel.IsEmpty
var beforeScrollY = ScrollY;
// プロンプトの取得
var promptText = "";
var promptLength = 0;
if (selectionIsEmpty) {
sel.StartOfLine(false, mePosLogical);
sel.EndOfLine(true, mePosLogical);
}
promptText = sel.Text.trim();
promptLength = sel.TextLength;
// コンテキストメニューの作成
var menu = createContextMenu(selectionIsEmpty);
var menuTrack = menu.Track(0);
// メニュー選択がキャンセルされた場合は終了
if (menuTrack === 0) return;
// 会話履歴のリセット
if (menuTrack === 2) {
resetTag();
return;
}
// プロンプトが空の場合はエラー
if (promptLength === 0) {
alert("プロンプトを入力してください。");
return;
}
// 文字数が多すぎる場合はエラー
if (promptLength > 10000) {
alert("プロンプトの文字数が多すぎます。10000 文字以下にしてください。");
return;
}
// 読み取り専用の場合はエラー
if (doc.ReadOnly) {
alert("ファイルが読み取り専用のため実行できません。");
return;
}
// カーソル位置移動
sel.SetActivePoint(mePosLogical, sel.GetBottomPointX(mePosLogical), sel.GetBottomPointY(mePosLogical));
sel.EndOfLine(false, mePosLogical);
sel.NewLine();
var activeY = sel.GetActivePointY(mePosLogical);
// GPT API に問い合わせ
doc.ReadOnly = true;
var ret = callGPTAPI(promptText);
doc.ReadOnly = false;
// 結果を表示
sel.Text = "\n---\n\n" + ret[0] + "\n\n" + ret[1] + "\n\n---\n";
// 位置調整
sel.SetActivePoint(
mePosLogical,
1,
activeY,
false
)
if (beforeScrollY + 2 < ScrollY) {
ScrollY = ScrollY - 1;
}
}
function createContextMenu(selectionIsEmpty) {
var menu = CreatePopupMenu();
menu.Add("※注意※ プライバシー・機密情報を送信しないこと", 0);
menu.Add("", 0, meMenuSeparator);
if (selectionIsEmpty) {
menu.Add("アクティブ行の内容で相談 (&Q)", EXECUTE_AI);
} else {
menu.Add("選択範囲の内容で相談 (&Q)", EXECUTE_AI);
}
if (document.Tag.exists("gpt-text")) {
var turn = JSON.parse(document.Tag("gpt-text")).length / 2;
menu.Add("会話履歴をリセット (現在の会話数: " + turn + ") (&R)", RESET_HISTORY);
}
return menu;
}
function callGPTAPI(promptText) {
try {
// APIキーの取得
var gptApiKey = shell.getEnv("OPENAI_API_KEY");
var http = new ActiveXObject("Msxml2.ServerXMLHTTP");
var url = 'https://api.openai.com/v1/chat/completions';
http.open('POST', url, true);
http.setRequestHeader('Content-Type', 'application/json');
http.setRequestHeader('Authorization', 'Bearer ' + gptApiKey);
http.send('{'
+ '"model": "gpt-4o-mini",' // モデルの指定
+ '"temperature": 0.7,' // 生成の多様性 (0.0 ~ 1.0)
+ '"max_tokens": 4000,' // 生成トークン数の上限
+ '"messages":' + createGPTRequest(promptText)
+ '}');
// 応答待機
var timeout = 60;
var retrySec = 0;
var startX = sel.GetActivePointX(mePosLogical);
var prevPos = sel.GetActivePos();
while (http.readyState != 4) {
if (retrySec > timeout) {
break;
}
http.waitForResponse(1);
retrySec += 1;
inputProgressChar(prevPos);
prevPos = sel.GetActivePos();
}
// 進捗表示文字を削除
sel.SetActivePoint(
mePosLogical,
startX,
sel.GetActivePointY(mePosLogical),
true
);
sel.Delete();
var responseMessage = "";
var token = "";
// 応答ステータスチェック
if (http.readyState == 4 && http.status == 200) {
// JSONをパースして必要なテキストを抽出
var response = JSON.parse(http.responseText);
if (response.choices && response.choices.length > 0) {
// メッセージの内容を取得
response.choices.forEach(function (choice) {
if (choice.message && choice.message.content) {
responseMessage += choice.message.content;
}
});
// トークン数を取得
var usage = response.usage;
token += "(使用トークン数: "
+ "プロンプト=" + usage.prompt_tokens
+ ", 応答=" + usage.completion_tokens
+ ", 合計=" + usage.total_tokens
+ ")";
}
} else {
var responseMessage = "エラーが発生しました: " + http.status + " " + http.statusText
http.abort();
return responseMessage;
}
// 結果を会話履歴に追加
if (HISTORY_SIZE > 0) {
appendReturnTextToTag(promptText, responseMessage);
}
// 整形しつつ返す
return [responseMessage.trim(), token];
} catch (e) {
console.log(e.stack);
return ["エラーが発生しました: " + e.message, ""];
}
}
// document.Tag の内容を加味してGPTのリクエストを作成
function createGPTRequest(promptText) {
var tag = document.Tag;
// 以前の会話が存在しない場合
if (!tag.exists("gpt-text")) {
return JSON.stringify([{ "role": "user", "content": promptText }]);
}
var gptText = JSON.parse(tag("gpt-text"));
gptText.push({
"role": "user",
"content": promptText
});
return JSON.stringify(gptText);
}
// document.Tag にGPTの応答を追加
function appendReturnTextToTag(userText, modelText) {
var tag = document.Tag;
if (!tag.exists("gpt-text")) {
tag("gpt-text") = JSON.stringify([
{
"role": "user",
"content": userText
},
{
"role": "assistant",
"content": modelText
}
]);
} else {
var gptText = JSON.parse(tag("gpt-text"));
gptText.push({
"role": "user",
"content": userText
});
gptText.push({
"role": "assistant",
"content": modelText
});
// 会話セット (user, model 1往復) が特定セット数を超えた場合、最初のセットを削除
while (gptText.length > HISTORY_SIZE * 2) {
gptText.shift();
}
tag("gpt-text") = JSON.stringify(gptText);
}
}
function resetTag() {
if (document.Tag.exists("gpt-text")) {
document.Tag.remove("gpt-text");
}
}
function inputProgressChar(pos) {
doc.ReadOnly = false;
sel.SetActivePos(pos);
sel.Text = ".";
doc.ReadOnly = true;
}
更新履歴
- 2024-12-18 初版
スポンサーリンク