chrome-ai-bridge 2.0.4 → 2.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/src/fast-cdp/fast-chat.js +408 -68
- package/package.json +1 -1
|
@@ -39,6 +39,37 @@ async function isConnectionHealthy(client, kind) {
|
|
|
39
39
|
return false;
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* メッセージカウントが安定するまで待機
|
|
44
|
+
* ページ読み込み完了を確認するため、カウントが2回連続で同じ値になるまで待機
|
|
45
|
+
* @param client CDPクライアント
|
|
46
|
+
* @param countExpr カウントを取得するJavaScript式
|
|
47
|
+
* @param maxWaitMs 最大待機時間(デフォルト3000ms)
|
|
48
|
+
* @param pollIntervalMs ポーリング間隔(デフォルト300ms)
|
|
49
|
+
* @returns 安定したカウント値
|
|
50
|
+
*/
|
|
51
|
+
async function waitForStableCount(client, countExpr, maxWaitMs = 3000, pollIntervalMs = 300) {
|
|
52
|
+
const startTime = Date.now();
|
|
53
|
+
let lastCount = -1;
|
|
54
|
+
let stableCount = 0;
|
|
55
|
+
while (Date.now() - startTime < maxWaitMs) {
|
|
56
|
+
const currentCount = await client.evaluate(countExpr);
|
|
57
|
+
if (currentCount === lastCount) {
|
|
58
|
+
stableCount++;
|
|
59
|
+
if (stableCount >= 2) {
|
|
60
|
+
// 2回連続で同じ値なら安定したとみなす
|
|
61
|
+
return currentCount;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
stableCount = 0;
|
|
66
|
+
lastCount = currentCount;
|
|
67
|
+
}
|
|
68
|
+
await new Promise(r => setTimeout(r, pollIntervalMs));
|
|
69
|
+
}
|
|
70
|
+
// タイムアウト時は最後のカウントを返す
|
|
71
|
+
return lastCount >= 0 ? lastCount : 0;
|
|
72
|
+
}
|
|
42
73
|
function getProjectName() {
|
|
43
74
|
return path.basename(process.cwd()) || 'default';
|
|
44
75
|
}
|
|
@@ -48,6 +79,51 @@ function getSessionPath() {
|
|
|
48
79
|
function getHistoryPath() {
|
|
49
80
|
return path.join(process.cwd(), '.local', 'chrome-ai-bridge', 'history.jsonl');
|
|
50
81
|
}
|
|
82
|
+
function getLocalTimestamp() {
|
|
83
|
+
const now = new Date();
|
|
84
|
+
const y = now.getFullYear();
|
|
85
|
+
const m = String(now.getMonth() + 1).padStart(2, '0');
|
|
86
|
+
const d = String(now.getDate()).padStart(2, '0');
|
|
87
|
+
const h = String(now.getHours()).padStart(2, '0');
|
|
88
|
+
const min = String(now.getMinutes()).padStart(2, '0');
|
|
89
|
+
const s = String(now.getSeconds()).padStart(2, '0');
|
|
90
|
+
return `${y}-${m}-${d} ${h}:${min}:${s}`;
|
|
91
|
+
}
|
|
92
|
+
async function rotateHistoryIfNeeded() {
|
|
93
|
+
const historyPath = getHistoryPath();
|
|
94
|
+
try {
|
|
95
|
+
const content = await fs.readFile(historyPath, 'utf-8');
|
|
96
|
+
const lines = content.trim().split('\n').filter(Boolean);
|
|
97
|
+
// 1000件以下なら何もしない
|
|
98
|
+
if (lines.length <= 1000)
|
|
99
|
+
return;
|
|
100
|
+
const now = Date.now();
|
|
101
|
+
const thirtyDaysAgo = now - 30 * 24 * 60 * 60 * 1000;
|
|
102
|
+
// 30日以上古いエントリを除外
|
|
103
|
+
const filtered = lines.filter(line => {
|
|
104
|
+
try {
|
|
105
|
+
const entry = JSON.parse(line);
|
|
106
|
+
// ローカル時刻形式 "2026-02-01 00:36:02" または ISO形式 "2026-01-31T15:36:02.273Z" 両対応
|
|
107
|
+
const ts = new Date(entry.ts).getTime();
|
|
108
|
+
return ts > thirtyDaysAgo; // 30日以内は保持
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return true; // パース失敗は保持
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
// 削除対象があれば書き換え
|
|
115
|
+
if (filtered.length < lines.length) {
|
|
116
|
+
await fs.writeFile(historyPath, filtered.join('\n') + '\n', 'utf-8');
|
|
117
|
+
console.error(`[history] Rotated: ${lines.length} -> ${filtered.length} entries`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
// ファイルがない場合は無視
|
|
122
|
+
if (err.code !== 'ENOENT') {
|
|
123
|
+
console.error('[history] Rotation error:', err);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
51
127
|
async function loadSessions() {
|
|
52
128
|
try {
|
|
53
129
|
const data = await fs.readFile(getSessionPath(), 'utf-8');
|
|
@@ -82,13 +158,15 @@ async function saveSession(kind, url, tabId) {
|
|
|
82
158
|
async function appendHistory(entry) {
|
|
83
159
|
const project = getProjectName();
|
|
84
160
|
const payload = {
|
|
85
|
-
ts:
|
|
161
|
+
ts: getLocalTimestamp(),
|
|
86
162
|
project,
|
|
87
163
|
...entry,
|
|
88
164
|
};
|
|
89
165
|
const targetPath = getHistoryPath();
|
|
90
166
|
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
91
167
|
await fs.appendFile(targetPath, `${JSON.stringify(payload)}\n`, 'utf-8');
|
|
168
|
+
// ローテーション実行(非同期、エラーは無視)
|
|
169
|
+
rotateHistoryIfNeeded().catch(() => { });
|
|
92
170
|
}
|
|
93
171
|
async function saveDebug(kind, payload) {
|
|
94
172
|
const targetDir = path.join(process.cwd(), '.local', 'chrome-ai-bridge', 'debug');
|
|
@@ -312,9 +390,13 @@ async function askChatGPTFastInternal(question) {
|
|
|
312
390
|
)`, 30000);
|
|
313
391
|
timings.waitInputMs = nowMs() - tWaitInput;
|
|
314
392
|
logInfo('chatgpt', 'Input field found', { waitInputMs: timings.waitInputMs });
|
|
315
|
-
//
|
|
316
|
-
|
|
317
|
-
|
|
393
|
+
// 初期メッセージカウントを取得(ページ読み込み完了を待ってから)
|
|
394
|
+
// ページが完全に読み込まれるまでカウントが安定しないため、安定するまで待機
|
|
395
|
+
const userCountExpr = `document.querySelectorAll('[data-message-author-role="user"]').length`;
|
|
396
|
+
const assistantCountExpr = `document.querySelectorAll('[data-message-author-role="assistant"]').length`;
|
|
397
|
+
const initialUserCount = await waitForStableCount(client, userCountExpr);
|
|
398
|
+
const initialAssistantCount = await waitForStableCount(client, assistantCountExpr);
|
|
399
|
+
console.error(`[ChatGPT] Initial counts (stable): user=${initialUserCount}, assistant=${initialAssistantCount}`);
|
|
318
400
|
// createConnection で正しいURL (https://chatgpt.com/) に接続済み
|
|
319
401
|
const sanitized = JSON.stringify(question);
|
|
320
402
|
const tInput = nowMs();
|
|
@@ -792,6 +874,7 @@ async function askChatGPTFastInternal(question) {
|
|
|
792
874
|
const startWait = Date.now();
|
|
793
875
|
let lastLoggedState = '';
|
|
794
876
|
let sawStopButton = false; // 生成中状態を検出したかどうか
|
|
877
|
+
let streamingText = ''; // ストリーミング中に取得したテキスト(完了後に折りたたまれる対策)
|
|
795
878
|
while (Date.now() - startWait < maxWaitMs) {
|
|
796
879
|
const state = await client.evaluate(`
|
|
797
880
|
(() => {
|
|
@@ -865,17 +948,38 @@ async function askChatGPTFastInternal(question) {
|
|
|
865
948
|
lastLoggedState = currentState;
|
|
866
949
|
}
|
|
867
950
|
// 応答完了条件(シンプル版):
|
|
868
|
-
// 停止ボタンを一度でも見た後に消えた AND 入力欄が空 AND
|
|
869
|
-
//
|
|
870
|
-
if (sawStopButton && !state.hasStopButton && !state.inputBoxHasText && state.assistantMsgCount >
|
|
871
|
-
console.error(`[ChatGPT] Response complete - stop button disappeared, input empty,
|
|
951
|
+
// 停止ボタンを一度でも見た後に消えた AND 入力欄が空 AND 新しいアシスタントメッセージが増えた
|
|
952
|
+
// initialAssistantCountとの比較で、既存の回答を新しい回答と誤判断しない
|
|
953
|
+
if (sawStopButton && !state.hasStopButton && !state.inputBoxHasText && state.assistantMsgCount > initialAssistantCount) {
|
|
954
|
+
console.error(`[ChatGPT] Response complete - stop button disappeared, input empty, assistant count increased (${initialAssistantCount} -> ${state.assistantMsgCount})`);
|
|
955
|
+
// ChatGPT 5.2 Thinking: 完了直後にストリーミング中のテキストをキャプチャ
|
|
956
|
+
// (完了後は折りたたまれてしまうため、この時点で取得)
|
|
957
|
+
streamingText = await client.evaluate(`
|
|
958
|
+
(() => {
|
|
959
|
+
const msgs = document.querySelectorAll('[data-message-author-role="assistant"]');
|
|
960
|
+
if (msgs.length === 0) return '';
|
|
961
|
+
const last = msgs[msgs.length - 1];
|
|
962
|
+
// .markdown, .result-thinking, または直接テキストを試す
|
|
963
|
+
const md = last.querySelector('.markdown');
|
|
964
|
+
if (md) {
|
|
965
|
+
const t = (md.innerText || md.textContent || '').trim();
|
|
966
|
+
if (t.length > 0) return t;
|
|
967
|
+
}
|
|
968
|
+
const rt = last.querySelector('.result-thinking');
|
|
969
|
+
if (rt) {
|
|
970
|
+
const t = (rt.innerText || rt.textContent || '').trim();
|
|
971
|
+
if (t.length > 0) return t;
|
|
972
|
+
}
|
|
973
|
+
return (last.innerText || last.textContent || '').trim();
|
|
974
|
+
})()
|
|
975
|
+
`);
|
|
872
976
|
break;
|
|
873
977
|
}
|
|
874
|
-
// フォールバック: 5秒以上待って、stop
|
|
978
|
+
// フォールバック: 5秒以上待って、stopボタンなし、入力欄空、新しいアシスタントメッセージが増えた
|
|
875
979
|
// (stopボタンを見逃した場合の救済)
|
|
876
980
|
const elapsed = Date.now() - startWait;
|
|
877
|
-
if (elapsed > 5000 && !state.hasStopButton && !state.inputBoxHasText && state.assistantMsgCount >
|
|
878
|
-
console.error(`[ChatGPT] Response complete - fallback after 5s (no stop button, input empty,
|
|
981
|
+
if (elapsed > 5000 && !state.hasStopButton && !state.inputBoxHasText && state.assistantMsgCount > initialAssistantCount) {
|
|
982
|
+
console.error(`[ChatGPT] Response complete - fallback after 5s (no stop button, input empty, assistant count increased ${initialAssistantCount} -> ${state.assistantMsgCount})`);
|
|
879
983
|
break;
|
|
880
984
|
}
|
|
881
985
|
await new Promise(r => setTimeout(r, pollIntervalMs));
|
|
@@ -913,21 +1017,165 @@ async function askChatGPTFastInternal(question) {
|
|
|
913
1017
|
console.error(`[ChatGPT] Timeout - final state: ${JSON.stringify(finalState)}`);
|
|
914
1018
|
throw new Error(`Timed out waiting for ChatGPT response (8min). Final state: ${JSON.stringify(finalState)}`);
|
|
915
1019
|
}
|
|
916
|
-
//
|
|
917
|
-
|
|
1020
|
+
// ChatGPT 5.2 Thinking モデル対応:
|
|
1021
|
+
// 回答が「思考」として折りたたまれている場合は展開してからテキストを取得
|
|
1022
|
+
// 「思考の拡張」ボタンをページ全体から探してクリック
|
|
1023
|
+
const clickedExpand = await client.evaluate(`
|
|
918
1024
|
(() => {
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
const
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
1025
|
+
// ページ全体から「思考の拡張」「Expand thinking」ボタンを探す
|
|
1026
|
+
const allButtons = document.querySelectorAll('button');
|
|
1027
|
+
for (const btn of allButtons) {
|
|
1028
|
+
const text = (btn.innerText || '').toLowerCase();
|
|
1029
|
+
if (text.includes('思考') || text.includes('thinking') || text.includes('expand')) {
|
|
1030
|
+
if (btn.getAttribute('aria-expanded') === 'false') {
|
|
1031
|
+
btn.click();
|
|
1032
|
+
return true;
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
return false;
|
|
928
1037
|
})()
|
|
929
1038
|
`);
|
|
930
|
-
|
|
1039
|
+
if (clickedExpand) {
|
|
1040
|
+
// 展開アニメーションとコンテンツロードを待つ
|
|
1041
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
1042
|
+
console.error('[ChatGPT] Expanded thinking content');
|
|
1043
|
+
}
|
|
1044
|
+
// 最後のアシスタントメッセージを直接取得
|
|
1045
|
+
// ChatGPT 5.2 Thinking: .result-thinking または .markdown 内のテキスト
|
|
1046
|
+
// リトライロジック: CDPがReactレンダリング完了前に実行される問題に対応
|
|
1047
|
+
let answer = '';
|
|
1048
|
+
const extractMaxRetries = 5;
|
|
1049
|
+
const extractRetryIntervalMs = 500;
|
|
1050
|
+
for (let retry = 0; retry < extractMaxRetries; retry++) {
|
|
1051
|
+
answer = await client.evaluate(`
|
|
1052
|
+
(() => {
|
|
1053
|
+
const articles = document.querySelectorAll('article');
|
|
1054
|
+
let lastAssistantArticle = null;
|
|
1055
|
+
|
|
1056
|
+
// 新UI: article内のh6/h5/[role="heading"]に"ChatGPT"を含むものを探す
|
|
1057
|
+
for (const article of articles) {
|
|
1058
|
+
const heading = article.querySelector('h6, h5, [role="heading"]');
|
|
1059
|
+
if (heading && (heading.textContent || '').includes('ChatGPT')) {
|
|
1060
|
+
lastAssistantArticle = article;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// フォールバック: 旧セレクター
|
|
1065
|
+
if (!lastAssistantArticle) {
|
|
1066
|
+
const old = document.querySelectorAll('[data-message-author-role="assistant"]');
|
|
1067
|
+
if (old.length > 0) lastAssistantArticle = old[old.length - 1];
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
if (!lastAssistantArticle) return '';
|
|
1071
|
+
|
|
1072
|
+
// ChatGPT 5.2 Thinking: 展開された思考コンテンツを取得
|
|
1073
|
+
// .result-thinking.markdown 内のテキストを優先
|
|
1074
|
+
const thinking = lastAssistantArticle.querySelector('.result-thinking.markdown, .result-thinking');
|
|
1075
|
+
if (thinking) {
|
|
1076
|
+
const text = (thinking.innerText || thinking.textContent || '').trim();
|
|
1077
|
+
if (text.length > 0) return text;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// 通常のmarkdownコンテンツ
|
|
1081
|
+
const markdown = lastAssistantArticle.querySelector('.markdown');
|
|
1082
|
+
if (markdown) {
|
|
1083
|
+
const text = (markdown.innerText || markdown.textContent || '').trim();
|
|
1084
|
+
if (text.length > 0) return text;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// Thinkingモード対応: button以外のコンテンツコンテナからテキストを抽出
|
|
1088
|
+
// DOM構造: article > div > div (generic) > button/"思考時間" + div (generic) > p/回答
|
|
1089
|
+
const contentDivs = lastAssistantArticle.querySelectorAll(':scope > div > div');
|
|
1090
|
+
for (const div of contentDivs) {
|
|
1091
|
+
// buttonはスキップ(「思考中」「今すぐ回答」「思考時間: Xs」)
|
|
1092
|
+
if (div.tagName === 'BUTTON') continue;
|
|
1093
|
+
|
|
1094
|
+
// div内のparagraphを探す
|
|
1095
|
+
const paragraphs = div.querySelectorAll('p');
|
|
1096
|
+
if (paragraphs.length > 0) {
|
|
1097
|
+
const text = Array.from(paragraphs)
|
|
1098
|
+
.map(p => (p.innerText || p.textContent || '').trim())
|
|
1099
|
+
.filter(t => t.length > 0)
|
|
1100
|
+
.join('\\n\\n');
|
|
1101
|
+
if (text.length > 0) return text;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// テキスト抽出: p要素を結合(button内のpは除外)
|
|
1106
|
+
const paragraphs = lastAssistantArticle.querySelectorAll('p');
|
|
1107
|
+
if (paragraphs.length > 0) {
|
|
1108
|
+
const text = Array.from(paragraphs)
|
|
1109
|
+
.filter(p => !p.closest('button')) // button内のpは除外
|
|
1110
|
+
.map(p => (p.innerText || p.textContent || '').trim())
|
|
1111
|
+
.filter(t => t.length > 0)
|
|
1112
|
+
.join('\\n\\n');
|
|
1113
|
+
if (text.length > 0) return text;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// フォールバック: article全体のテキスト(ヘッダー・ボタンテキスト除去)
|
|
1117
|
+
const fullText = (lastAssistantArticle.innerText || lastAssistantArticle.textContent || '').trim();
|
|
1118
|
+
// ボタンテキストパターンを除去
|
|
1119
|
+
const cleaned = fullText
|
|
1120
|
+
.replace(/^ChatGPT:\\s*/i, '')
|
|
1121
|
+
.split('\\n')
|
|
1122
|
+
.filter(line => {
|
|
1123
|
+
const trimmed = line.trim();
|
|
1124
|
+
// Thinking関連のボタンテキストを除外
|
|
1125
|
+
if (/^思考時間:\\s*\\d+s?$/.test(trimmed)) return false;
|
|
1126
|
+
if (trimmed === '思考中') return false;
|
|
1127
|
+
if (trimmed === '今すぐ回答') return false;
|
|
1128
|
+
if (trimmed === 'Skip thinking') return false;
|
|
1129
|
+
return true;
|
|
1130
|
+
})
|
|
1131
|
+
.join('\\n')
|
|
1132
|
+
.trim();
|
|
1133
|
+
return cleaned;
|
|
1134
|
+
})()
|
|
1135
|
+
`);
|
|
1136
|
+
// 有効なテキストが取得できたら終了
|
|
1137
|
+
if (answer && answer.length > 0 && !answer.startsWith('ChatGPT:')) {
|
|
1138
|
+
if (retry > 0) {
|
|
1139
|
+
console.error(`[ChatGPT] Got response on retry ${retry}`);
|
|
1140
|
+
}
|
|
1141
|
+
break;
|
|
1142
|
+
}
|
|
1143
|
+
// リトライ待機
|
|
1144
|
+
if (retry < extractMaxRetries - 1) {
|
|
1145
|
+
console.error(`[ChatGPT] Response empty, retrying (${retry + 1}/${extractMaxRetries})...`);
|
|
1146
|
+
await new Promise(r => setTimeout(r, extractRetryIntervalMs));
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
// リトライでも取得できない場合、body.innerTextから抽出(最終フォールバック)
|
|
1150
|
+
if (!answer || answer.length === 0) {
|
|
1151
|
+
console.error('[ChatGPT] Trying body.innerText fallback...');
|
|
1152
|
+
answer = await client.evaluate(`
|
|
1153
|
+
(() => {
|
|
1154
|
+
const bodyText = document.body.innerText || '';
|
|
1155
|
+
// 最後の"ChatGPT:"以降のテキストを抽出
|
|
1156
|
+
const parts = bodyText.split('ChatGPT:');
|
|
1157
|
+
if (parts.length < 2) return '';
|
|
1158
|
+
const lastPart = parts[parts.length - 1];
|
|
1159
|
+
// 終端マーカーまでを取得
|
|
1160
|
+
const endMarkers = ['あなた:', 'You:', '思考の拡張', 'cookie', 'ChatGPT は間違えることがあります'];
|
|
1161
|
+
let endIndex = lastPart.length;
|
|
1162
|
+
for (const marker of endMarkers) {
|
|
1163
|
+
const idx = lastPart.indexOf(marker);
|
|
1164
|
+
if (idx > 0 && idx < endIndex) endIndex = idx;
|
|
1165
|
+
}
|
|
1166
|
+
return lastPart.slice(0, endIndex).trim();
|
|
1167
|
+
})()
|
|
1168
|
+
`);
|
|
1169
|
+
if (answer && answer.length > 0) {
|
|
1170
|
+
console.error(`[ChatGPT] Got response from body.innerText fallback`);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
// streamingTextが有効な場合はそれを優先(ChatGPT 5.2 Thinking対応)
|
|
1174
|
+
// DOMから取得したテキストが空または見出しのみ("ChatGPT:"など)の場合はstreamingTextを使用
|
|
1175
|
+
const finalAnswer = (answer && answer.length > 20 && !answer.startsWith('ChatGPT:'))
|
|
1176
|
+
? answer
|
|
1177
|
+
: (streamingText || answer);
|
|
1178
|
+
console.error(`[ChatGPT] Response extracted: ${finalAnswer.slice(0, 100)}...`);
|
|
931
1179
|
const finalUrl = await client.evaluate('location.href');
|
|
932
1180
|
if (finalUrl && finalUrl.includes('chatgpt.com')) {
|
|
933
1181
|
await saveSession('chatgpt', finalUrl);
|
|
@@ -937,7 +1185,7 @@ async function askChatGPTFastInternal(question) {
|
|
|
937
1185
|
await appendHistory({
|
|
938
1186
|
provider: 'chatgpt',
|
|
939
1187
|
question,
|
|
940
|
-
answer,
|
|
1188
|
+
answer: finalAnswer,
|
|
941
1189
|
url: finalUrl || undefined,
|
|
942
1190
|
timings,
|
|
943
1191
|
});
|
|
@@ -950,7 +1198,7 @@ async function askChatGPTFastInternal(question) {
|
|
|
950
1198
|
waitResponseMs: timings.waitResponseMs ?? 0,
|
|
951
1199
|
totalMs: timings.totalMs ?? 0,
|
|
952
1200
|
};
|
|
953
|
-
return { answer, timings: fullTimings };
|
|
1201
|
+
return { answer: finalAnswer, timings: fullTimings };
|
|
954
1202
|
}
|
|
955
1203
|
/**
|
|
956
1204
|
* ChatGPTに質問して回答を取得(後方互換用)
|
|
@@ -1024,8 +1272,7 @@ async function askGeminiFastInternal(question) {
|
|
|
1024
1272
|
collectDeep(selectors);
|
|
1025
1273
|
return results.length;
|
|
1026
1274
|
})()`;
|
|
1027
|
-
const
|
|
1028
|
-
const initialModelResponseCount = await client.evaluate(`
|
|
1275
|
+
const geminiModelResponseCountExpr = `
|
|
1029
1276
|
(() => {
|
|
1030
1277
|
const collectDeep = (selectorList) => {
|
|
1031
1278
|
const results = [];
|
|
@@ -1052,8 +1299,12 @@ async function askGeminiFastInternal(question) {
|
|
|
1052
1299
|
};
|
|
1053
1300
|
return collectDeep(['model-response', '.model-response', '[data-test-id*="response"]']).length;
|
|
1054
1301
|
})()
|
|
1055
|
-
|
|
1056
|
-
|
|
1302
|
+
`;
|
|
1303
|
+
// ページ読み込み完了を待ってから初期カウントを取得
|
|
1304
|
+
// カウントが安定するまでポーリング(2回連続で同じ値になるまで)
|
|
1305
|
+
const initialGeminiUserCount = await waitForStableCount(client, geminiUserCountExpr);
|
|
1306
|
+
const initialModelResponseCount = await waitForStableCount(client, geminiModelResponseCountExpr);
|
|
1307
|
+
console.error(`[Gemini] Initial counts (stable): user=${initialGeminiUserCount}, modelResponse=${initialModelResponseCount}`);
|
|
1057
1308
|
const sanitized = JSON.stringify(question);
|
|
1058
1309
|
const tInput = nowMs();
|
|
1059
1310
|
// Phase 1: 最初の入力試行
|
|
@@ -1429,12 +1680,31 @@ async function askGeminiFastInternal(question) {
|
|
|
1429
1680
|
.filter(el => !isDisabled(el));
|
|
1430
1681
|
|
|
1431
1682
|
// 「停止」ボタンがあるかチェック(応答生成中)
|
|
1432
|
-
const hasStopButton =
|
|
1433
|
-
|
|
1434
|
-
const
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1683
|
+
const hasStopButton = (() => {
|
|
1684
|
+
// 方法1: aria-labelベースの検索(最も信頼性が高い)
|
|
1685
|
+
const stopByLabel = buttons.some(b => {
|
|
1686
|
+
const label = (b.getAttribute('aria-label') || '').trim();
|
|
1687
|
+
return label.includes('回答を停止') || label.includes('Stop generating') ||
|
|
1688
|
+
label.includes('Stop streaming') || label === 'Stop';
|
|
1689
|
+
});
|
|
1690
|
+
if (stopByLabel) return true;
|
|
1691
|
+
|
|
1692
|
+
// 方法2: mat-icon要素での検出(Gemini用)
|
|
1693
|
+
const stopIcon = document.querySelector('mat-icon[data-mat-icon-name="stop"]');
|
|
1694
|
+
if (stopIcon) {
|
|
1695
|
+
const btn = stopIcon.closest('button');
|
|
1696
|
+
if (btn && isVisible(btn)) return true;
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
// 方法3: img[alt="stop"] での検出(ChatGPT用)
|
|
1700
|
+
const stopImg = document.querySelector('img[alt="stop"]');
|
|
1701
|
+
if (stopImg) {
|
|
1702
|
+
const btn = stopImg.closest('button');
|
|
1703
|
+
if (btn && isVisible(btn)) return true;
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
return false;
|
|
1707
|
+
})();
|
|
1438
1708
|
|
|
1439
1709
|
// 応答生成中の場合、送信ボタンはdisabled扱い
|
|
1440
1710
|
if (hasStopButton) {
|
|
@@ -1678,23 +1948,50 @@ async function askGeminiFastInternal(question) {
|
|
|
1678
1948
|
|
|
1679
1949
|
const buttons = collectDeep(['button', '[role="button"]']).filter(isVisible);
|
|
1680
1950
|
|
|
1681
|
-
//
|
|
1682
|
-
const hasStopButton =
|
|
1683
|
-
|
|
1684
|
-
const
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1951
|
+
// 停止ボタン検出(応答生成中かどうか)
|
|
1952
|
+
const hasStopButton = (() => {
|
|
1953
|
+
// 方法1: aria-labelベースの検索(最も信頼性が高い)
|
|
1954
|
+
const stopByLabel = buttons.some(b => {
|
|
1955
|
+
const label = (b.getAttribute('aria-label') || '').trim();
|
|
1956
|
+
return label.includes('回答を停止') || label.includes('Stop generating') ||
|
|
1957
|
+
label.includes('Stop streaming') || label === 'Stop';
|
|
1958
|
+
});
|
|
1959
|
+
if (stopByLabel) return true;
|
|
1960
|
+
|
|
1961
|
+
// 方法2: mat-icon要素での検出(Gemini用)
|
|
1962
|
+
const stopIcon = document.querySelector('mat-icon[data-mat-icon-name="stop"]');
|
|
1963
|
+
if (stopIcon) {
|
|
1964
|
+
const btn = stopIcon.closest('button');
|
|
1965
|
+
if (btn && isVisible(btn)) return true;
|
|
1966
|
+
}
|
|
1688
1967
|
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1968
|
+
// 方法3: img[alt="stop"] での検出(ChatGPT用)
|
|
1969
|
+
const stopImg = document.querySelector('img[alt="stop"]');
|
|
1970
|
+
if (stopImg) {
|
|
1971
|
+
const btn = stopImg.closest('button');
|
|
1972
|
+
if (btn && isVisible(btn)) return true;
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
return false;
|
|
1976
|
+
})();
|
|
1977
|
+
|
|
1978
|
+
// マイクボタン検出(言語非依存: img[alt="mic"]を使用)
|
|
1979
|
+
const micButton = (() => {
|
|
1980
|
+
// img[alt="mic"] を含むボタンを探す(アイコン名は言語非依存)
|
|
1981
|
+
const micImg = document.querySelector('img[alt="mic"]');
|
|
1982
|
+
if (micImg) {
|
|
1983
|
+
const btn = micImg.closest('button');
|
|
1984
|
+
if (btn && isVisible(btn)) return btn;
|
|
1985
|
+
}
|
|
1986
|
+
// フォールバック: aria-labelベースの検索
|
|
1987
|
+
return buttons.find(b => {
|
|
1988
|
+
const label = (b.getAttribute('aria-label') || '').toLowerCase();
|
|
1989
|
+
return label.includes('マイク') ||
|
|
1990
|
+
label.includes('mic') ||
|
|
1991
|
+
label.includes('microphone') ||
|
|
1992
|
+
label.includes('voice');
|
|
1993
|
+
});
|
|
1994
|
+
})();
|
|
1698
1995
|
|
|
1699
1996
|
// 送信ボタン検出
|
|
1700
1997
|
const sendBtn = buttons.find(b =>
|
|
@@ -1706,6 +2003,13 @@ async function askGeminiFastInternal(question) {
|
|
|
1706
2003
|
b.querySelector('[data-icon="send"]')
|
|
1707
2004
|
);
|
|
1708
2005
|
|
|
2006
|
+
// フィードバックボタン検出(言語非依存: thumb_up/thumb_downアイコン)
|
|
2007
|
+
const hasFeedbackButtons = !!(
|
|
2008
|
+
document.querySelector('img[alt="thumb_up"], img[alt="thumb_down"]') ||
|
|
2009
|
+
document.querySelector('button[aria-label*="良い回答"], button[aria-label*="悪い回答"]') ||
|
|
2010
|
+
document.querySelector('button[aria-label*="Good"], button[aria-label*="Bad"]')
|
|
2011
|
+
);
|
|
2012
|
+
|
|
1709
2013
|
// モデルレスポンス収集(Shadow DOM対応)
|
|
1710
2014
|
const allResponses = collectDeep(['model-response', '[data-test-id*="response"]', '.response', '.model-response']);
|
|
1711
2015
|
const lastResponse = allResponses[allResponses.length - 1];
|
|
@@ -1728,6 +2032,7 @@ async function askGeminiFastInternal(question) {
|
|
|
1728
2032
|
return {
|
|
1729
2033
|
hasStopButton,
|
|
1730
2034
|
hasMicButton: Boolean(micButton && isVisible(micButton)),
|
|
2035
|
+
hasFeedbackButtons,
|
|
1731
2036
|
sendButtonEnabled: Boolean(sendBtn && !isDisabled(sendBtn)),
|
|
1732
2037
|
modelResponseCount: allResponses.length,
|
|
1733
2038
|
lastResponseTextLength,
|
|
@@ -1751,28 +2056,33 @@ async function askGeminiFastInternal(question) {
|
|
|
1751
2056
|
const currentState = JSON.stringify(state);
|
|
1752
2057
|
if (currentState !== lastLoggedState) {
|
|
1753
2058
|
const elapsed = Math.round((Date.now() - startWait) / 1000);
|
|
1754
|
-
console.error(`[Gemini] State @${elapsed}s: stop=${state.hasStopButton}, mic=${state.hasMicButton}, send=${state.sendButtonEnabled}, responses=${state.modelResponseCount}, textLen=${state.lastResponseTextLength}, inputEmpty=${state.inputBoxEmpty}, sawStop=${sawStopButton}, textStable=${textStableCount}`);
|
|
2059
|
+
console.error(`[Gemini] State @${elapsed}s: stop=${state.hasStopButton}, mic=${state.hasMicButton}, feedback=${state.hasFeedbackButtons}, send=${state.sendButtonEnabled}, responses=${state.modelResponseCount}, textLen=${state.lastResponseTextLength}, inputEmpty=${state.inputBoxEmpty}, sawStop=${sawStopButton}, textStable=${textStableCount}`);
|
|
1755
2060
|
lastLoggedState = currentState;
|
|
1756
2061
|
}
|
|
1757
|
-
// 応答完了条件
|
|
1758
|
-
if (sawStopButton && !state.hasStopButton && state.
|
|
1759
|
-
console.error(
|
|
2062
|
+
// 応答完了条件0: 停止ボタンを見た後に消えた AND フィードバックボタン表示 AND 新しい回答が増えた
|
|
2063
|
+
if (sawStopButton && !state.hasStopButton && state.hasFeedbackButtons && state.modelResponseCount > initialModelResponseCount) {
|
|
2064
|
+
console.error(`[Gemini] Response complete - stop button disappeared, feedback buttons visible, response count increased (${initialModelResponseCount} -> ${state.modelResponseCount})`);
|
|
2065
|
+
break;
|
|
2066
|
+
}
|
|
2067
|
+
// 応答完了条件1: 停止ボタンを見た後に消えた AND マイクボタン表示 AND 新しい回答が増えた
|
|
2068
|
+
if (sawStopButton && !state.hasStopButton && state.hasMicButton && state.modelResponseCount > initialModelResponseCount) {
|
|
2069
|
+
console.error(`[Gemini] Response complete - stop button disappeared, mic button visible, response count increased (${initialModelResponseCount} -> ${state.modelResponseCount})`);
|
|
1760
2070
|
break;
|
|
1761
2071
|
}
|
|
1762
|
-
// 応答完了条件2: 停止ボタンを見た後に消えた AND 送信ボタン有効 AND 入力欄空
|
|
1763
|
-
if (sawStopButton && !state.hasStopButton && state.sendButtonEnabled && state.inputBoxEmpty) {
|
|
1764
|
-
console.error(
|
|
2072
|
+
// 応答完了条件2: 停止ボタンを見た後に消えた AND 送信ボタン有効 AND 入力欄空 AND 新しい回答が増えた
|
|
2073
|
+
if (sawStopButton && !state.hasStopButton && state.sendButtonEnabled && state.inputBoxEmpty && state.modelResponseCount > initialModelResponseCount) {
|
|
2074
|
+
console.error(`[Gemini] Response complete - stop button disappeared, send button enabled, input empty, response count increased (${initialModelResponseCount} -> ${state.modelResponseCount})`);
|
|
1765
2075
|
break;
|
|
1766
2076
|
}
|
|
1767
|
-
// 応答完了条件3: テキスト長が5秒間安定 AND
|
|
1768
|
-
if (textStableCount >= 5 && state.modelResponseCount >
|
|
1769
|
-
console.error(`[Gemini] Response complete - text stable for ${textStableCount}s, ${state.modelResponseCount}
|
|
2077
|
+
// 応答完了条件3: テキスト長が5秒間安定 AND 新しいレスポンスが増えた
|
|
2078
|
+
if (textStableCount >= 5 && state.modelResponseCount > initialModelResponseCount && !state.hasStopButton) {
|
|
2079
|
+
console.error(`[Gemini] Response complete - text stable for ${textStableCount}s, response count increased (${initialModelResponseCount} -> ${state.modelResponseCount})`);
|
|
1770
2080
|
break;
|
|
1771
2081
|
}
|
|
1772
|
-
// フォールバック: 10秒以上経過 + 停止ボタンを見ていない +
|
|
2082
|
+
// フォールバック: 10秒以上経過 + 停止ボタンを見ていない + 新しいレスポンスが増えた + 停止ボタンなし
|
|
1773
2083
|
const elapsed = Date.now() - startWait;
|
|
1774
|
-
if (elapsed > 10000 && !sawStopButton && state.modelResponseCount >
|
|
1775
|
-
console.error(`[Gemini] Response complete - fallback after 10s (no stop button seen, ${state.modelResponseCount}
|
|
2084
|
+
if (elapsed > 10000 && !sawStopButton && state.modelResponseCount > initialModelResponseCount && !state.hasStopButton && (state.hasMicButton || state.inputBoxEmpty)) {
|
|
2085
|
+
console.error(`[Gemini] Response complete - fallback after 10s (no stop button seen, response count increased ${initialModelResponseCount} -> ${state.modelResponseCount}, mic=${state.hasMicButton})`);
|
|
1776
2086
|
break;
|
|
1777
2087
|
}
|
|
1778
2088
|
await new Promise(r => setTimeout(r, pollIntervalMs));
|
|
@@ -1810,9 +2120,40 @@ async function askGeminiFastInternal(question) {
|
|
|
1810
2120
|
console.error(`[Gemini] Timeout - final state: ${JSON.stringify(finalState)}`);
|
|
1811
2121
|
throw new Error(`Timed out waiting for Gemini response (8min). sawStopButton=${sawStopButton}, textStableCount=${textStableCount}. Final state: ${JSON.stringify(finalState)}`);
|
|
1812
2122
|
}
|
|
1813
|
-
//
|
|
2123
|
+
// 最後のレスポンスを取得(フィードバックボタン基準 + フォールバック)
|
|
1814
2124
|
const rawText = await client.evaluate(`
|
|
1815
2125
|
(() => {
|
|
2126
|
+
// 方法1: フィードバックボタン(thumb_up)を基準に応答を探す(言語非依存・最も確実)
|
|
2127
|
+
const thumbUpImg = document.querySelector('img[alt="thumb_up"]');
|
|
2128
|
+
if (thumbUpImg) {
|
|
2129
|
+
// ボタンの親コンテナを遡る
|
|
2130
|
+
let container = thumbUpImg.closest('button')?.parentElement;
|
|
2131
|
+
if (container) {
|
|
2132
|
+
// さらに親を遡って応答テキストを含む要素を探す
|
|
2133
|
+
const parent = container.parentElement;
|
|
2134
|
+
if (parent) {
|
|
2135
|
+
// paragraph, heading, list などのテキスト要素を収集
|
|
2136
|
+
const textElements = parent.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, pre, code');
|
|
2137
|
+
const texts = Array.from(textElements)
|
|
2138
|
+
.map(el => (el.innerText || el.textContent || '').trim())
|
|
2139
|
+
.filter(t => t.length > 0);
|
|
2140
|
+
|
|
2141
|
+
if (texts.length > 0) {
|
|
2142
|
+
return texts.join('\\n\\n');
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
// フォールバック: 親要素全体からテキスト取得(ボタンを除外)
|
|
2146
|
+
const clone = parent.cloneNode(true);
|
|
2147
|
+
clone.querySelectorAll('button, img').forEach(el => el.remove());
|
|
2148
|
+
const text = (clone.innerText || clone.textContent || '').trim();
|
|
2149
|
+
if (text.length > 0) {
|
|
2150
|
+
return text;
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
// 方法2: 従来のセレクターベース(Shadow DOM対応)
|
|
1816
2157
|
const collectDeep = (selectorList) => {
|
|
1817
2158
|
const results = [];
|
|
1818
2159
|
const seen = new Set();
|
|
@@ -1837,16 +2178,15 @@ async function askGeminiFastInternal(question) {
|
|
|
1837
2178
|
return results;
|
|
1838
2179
|
};
|
|
1839
2180
|
|
|
1840
|
-
// Shadow DOM内のmodel-responseを収集
|
|
1841
2181
|
const allResponses = collectDeep(['model-response', '[data-test-id*="response"]', '.response', '.model-response']);
|
|
1842
2182
|
|
|
1843
2183
|
if (allResponses.length === 0) {
|
|
1844
|
-
//
|
|
2184
|
+
// 方法3: aria-live="polite"
|
|
1845
2185
|
const live = document.querySelector('[aria-live="polite"]');
|
|
1846
2186
|
return live ? (live.innerText || live.textContent || '').trim() : '';
|
|
1847
2187
|
}
|
|
1848
2188
|
|
|
1849
|
-
//
|
|
2189
|
+
// 最後のレスポンス要素を直接取得
|
|
1850
2190
|
const lastMsg = allResponses[allResponses.length - 1];
|
|
1851
2191
|
|
|
1852
2192
|
// マークダウンコンテンツを優先、なければ要素全体
|
package/package.json
CHANGED