chrome-ai-bridge 2.5.1 → 2.5.3
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 +72 -20
- package/build/src/main.js +1 -1
- package/package.json +1 -1
- package/scripts/cab +3 -1
|
@@ -61,8 +61,8 @@ function envWithFallback(newName, oldName, defaultVal) {
|
|
|
61
61
|
}
|
|
62
62
|
const CONNECT_REUSE_TIMEOUT_MS = Number(envWithFallback('CAI_CONNECT_REUSE_TIMEOUT_MS', 'MCP_CONNECT_REUSE_TIMEOUT_MS', '12000'));
|
|
63
63
|
const CONNECT_NEWTAB_TIMEOUT_MS = Number(envWithFallback('CAI_CONNECT_NEWTAB_TIMEOUT_MS', 'MCP_CONNECT_NEWTAB_TIMEOUT_MS', '20000'));
|
|
64
|
-
const TOOL_BUDGET_MS = Number(envWithFallback('CAI_TOOL_BUDGET_MS', 'CAI_MCP_TOOL_BUDGET_MS', '
|
|
65
|
-
const RESPONSE_WAIT_MAX_MS = Number(process.env.CAI_RESPONSE_WAIT_MAX_MS || '
|
|
64
|
+
const TOOL_BUDGET_MS = Number(envWithFallback('CAI_TOOL_BUDGET_MS', 'CAI_MCP_TOOL_BUDGET_MS', '300000'));
|
|
65
|
+
const RESPONSE_WAIT_MAX_MS = Number(process.env.CAI_RESPONSE_WAIT_MAX_MS || '300000');
|
|
66
66
|
const BUDGET_RESERVE_MS = Number(envWithFallback('CAI_BUDGET_RESERVE_MS', 'CAI_MCP_BUDGET_RESERVE_MS', '3000'));
|
|
67
67
|
function getRemainingBudgetMs(startMs, overrideBudgetMs) {
|
|
68
68
|
return (overrideBudgetMs ?? TOOL_BUDGET_MS) - (nowMs() - startMs) - BUDGET_RESERVE_MS;
|
|
@@ -1037,21 +1037,33 @@ async function askChatGPTFastInternal(question, debug, budgetMs) {
|
|
|
1037
1037
|
// 60秒 caller deadline を超えないよう、残り予算内で待機する。
|
|
1038
1038
|
const maxWaitMs = getResponseWaitBudgetMs(t0, RESPONSE_WAIT_MAX_MS, 'chatgpt-response', budgetMs);
|
|
1039
1039
|
const pollIntervalMs = 1000;
|
|
1040
|
+
const IDLE_TIMEOUT_MS = 60000; // ストップボタン消失後、60秒間無活動でタイムアウト
|
|
1040
1041
|
const startWait = Date.now();
|
|
1042
|
+
let lastActivityAt = Date.now(); // 最後にストップボタンorテキスト成長を検出した時刻
|
|
1041
1043
|
let lastLoggedState = '';
|
|
1042
1044
|
let sawStopButton = false; // 生成中状態を検出したかどうか
|
|
1043
1045
|
let streamingText = ''; // ストリーミング中に取得したテキスト(完了後に折りたたまれる対策)
|
|
1044
1046
|
let textStableCount = 0; // テキスト長が安定した回数(2-poll confirmation)
|
|
1045
1047
|
let lastTextLength = -1; // 前回のテキスト長
|
|
1046
|
-
|
|
1048
|
+
let stopButtonGoneCount = 0; // ストップボタンが連続不在のポール数(Thinkingフェーズ切替誤判定防止)
|
|
1049
|
+
let textGrowingCount = 0; // テキストが成長中のポール数(成長中はフォールバック抑止)
|
|
1050
|
+
// ストップボタンが見えている間は maxWaitMs を無視し、IDLE_TIMEOUT のみで判定する。
|
|
1051
|
+
// sawStopButton=false の初期フェーズでは maxWaitMs も併用。
|
|
1052
|
+
while (Date.now() - lastActivityAt < IDLE_TIMEOUT_MS &&
|
|
1053
|
+
(sawStopButton || Date.now() - startWait < maxWaitMs)) {
|
|
1047
1054
|
const state = await client.evaluate(`
|
|
1048
1055
|
(() => {
|
|
1049
1056
|
${DOM_UTILS_CODE}
|
|
1050
1057
|
|
|
1051
1058
|
// 停止ボタン検出(フォールバックセレクター付き)
|
|
1052
1059
|
const stopBtn = document.querySelector('button[data-testid="stop-button"]') ||
|
|
1060
|
+
document.querySelector('button[aria-label="ストリーミングの停止"]') ||
|
|
1061
|
+
document.querySelector('button[aria-label="Stop streaming"]') ||
|
|
1053
1062
|
document.querySelector('button[aria-label*="停止"]') ||
|
|
1054
|
-
document.querySelector('button[aria-label*="Stop"]')
|
|
1063
|
+
document.querySelector('button[aria-label*="Stop"]') ||
|
|
1064
|
+
[...document.querySelectorAll('button')].find(b =>
|
|
1065
|
+
b.querySelector('rect') && (b.textContent || '').trim() === ''
|
|
1066
|
+
);
|
|
1055
1067
|
const buttons = __collectDeep(['button', '[role="button"]']).nodes;
|
|
1056
1068
|
// 送信ボタン検出(フォールバックセレクター付き)
|
|
1057
1069
|
// 注意: 応答完了後は音声ボタンに置き換わり、送信ボタンがDOMから消える
|
|
@@ -1089,7 +1101,7 @@ async function askChatGPTFastInternal(question, debug, budgetMs) {
|
|
|
1089
1101
|
// 「今すぐ回答」「Skip thinking」ボタンがある場合はThinking進行中
|
|
1090
1102
|
const hasSkipThinkingButton = bodyText.includes('今すぐ回答') ||
|
|
1091
1103
|
bodyText.includes('Skip thinking');
|
|
1092
|
-
const isStillGenerating = (hasGeneratingText && !hasThinkingComplete) || hasSkipThinkingButton;
|
|
1104
|
+
const isStillGenerating = Boolean(stopBtn) || (hasGeneratingText && !hasThinkingComplete) || hasSkipThinkingButton;
|
|
1093
1105
|
|
|
1094
1106
|
// 最後のアシスタントメッセージに実際のテキストがあるかチェック
|
|
1095
1107
|
// 旧UIセレクター + 新UI(article)の両方を試す
|
|
@@ -1302,14 +1314,16 @@ async function askChatGPTFastInternal(question, debug, budgetMs) {
|
|
|
1302
1314
|
})()
|
|
1303
1315
|
`);
|
|
1304
1316
|
// stopボタンを検出したらフラグを立てる(生成が始まった証拠)
|
|
1317
|
+
// ストップボタンが見えている間はアクティブとみなし、タイムアウトを延長し続ける
|
|
1305
1318
|
if (state.hasStopButton) {
|
|
1306
1319
|
sawStopButton = true;
|
|
1320
|
+
lastActivityAt = Date.now();
|
|
1307
1321
|
}
|
|
1308
1322
|
// 状態が変化した場合のみログ出力
|
|
1309
1323
|
const currentState = JSON.stringify(state);
|
|
1310
1324
|
if (currentState !== lastLoggedState) {
|
|
1311
1325
|
const elapsed = Math.round((Date.now() - startWait) / 1000);
|
|
1312
|
-
console.error(`[ChatGPT] State @${elapsed}s: stop=${state.hasStopButton}, send=${state.sendButtonFound}(disabled=${state.sendButtonDisabled}), assistant=${state.assistantMsgCount}, inputHasText=${state.inputBoxHasText}, sawStop=${sawStopButton}, generating=${state.isStillGenerating}, skipThink=${state.hasSkipThinkingButton}, hasText=${state.hasResponseText}`);
|
|
1326
|
+
console.error(`[ChatGPT] State @${elapsed}s: stop=${state.hasStopButton}, send=${state.sendButtonFound}(disabled=${state.sendButtonDisabled}), assistant=${state.assistantMsgCount}, inputHasText=${state.inputBoxHasText}, sawStop=${sawStopButton}, generating=${state.isStillGenerating}, skipThink=${state.hasSkipThinkingButton}, hasText=${state.hasResponseText}, textGrow=${textGrowingCount}`);
|
|
1313
1327
|
lastLoggedState = currentState;
|
|
1314
1328
|
}
|
|
1315
1329
|
// 応答完了条件(Thinkingモード対応版):
|
|
@@ -1321,16 +1335,31 @@ async function askChatGPTFastInternal(question, debug, budgetMs) {
|
|
|
1321
1335
|
const currentTextLen = state.debug_lastAssistantInnerTextLen;
|
|
1322
1336
|
if (currentTextLen === lastTextLength && currentTextLen > 0) {
|
|
1323
1337
|
textStableCount++;
|
|
1338
|
+
textGrowingCount = 0;
|
|
1339
|
+
}
|
|
1340
|
+
else if (currentTextLen > lastTextLength) {
|
|
1341
|
+
textStableCount = 0;
|
|
1342
|
+
textGrowingCount++;
|
|
1343
|
+
lastTextLength = currentTextLen;
|
|
1344
|
+
lastActivityAt = Date.now();
|
|
1324
1345
|
}
|
|
1325
1346
|
else {
|
|
1326
1347
|
textStableCount = 0;
|
|
1348
|
+
textGrowingCount = 0;
|
|
1327
1349
|
lastTextLength = currentTextLen;
|
|
1328
1350
|
}
|
|
1329
|
-
|
|
1351
|
+
// ストップボタン不在の連続カウント(Thinkingフェーズ切替時の一瞬消失を除外)
|
|
1352
|
+
if (sawStopButton && !state.hasStopButton) {
|
|
1353
|
+
stopButtonGoneCount++;
|
|
1354
|
+
}
|
|
1355
|
+
else {
|
|
1356
|
+
stopButtonGoneCount = 0;
|
|
1357
|
+
}
|
|
1358
|
+
if (sawStopButton && stopButtonGoneCount >= 3 && !state.inputBoxHasText &&
|
|
1330
1359
|
state.assistantMsgCount > initialAssistantCount) {
|
|
1331
1360
|
// 2-poll confirmation: テキスト長が2回連続安定してから完了とする
|
|
1332
1361
|
if (textStableCount >= 2) {
|
|
1333
|
-
console.error(`[ChatGPT] Response complete - stop gone, text stable for ${textStableCount} polls (len=${currentTextLen})`);
|
|
1362
|
+
console.error(`[ChatGPT] Response complete - stop gone for ${stopButtonGoneCount} polls, text stable for ${textStableCount} polls (len=${currentTextLen})`);
|
|
1334
1363
|
streamingText = await client.evaluate(`
|
|
1335
1364
|
(() => {
|
|
1336
1365
|
const msgs = document.querySelectorAll('[data-message-author-role="assistant"]');
|
|
@@ -1352,7 +1381,7 @@ async function askChatGPTFastInternal(question, debug, budgetMs) {
|
|
|
1352
1381
|
break;
|
|
1353
1382
|
}
|
|
1354
1383
|
// まだ安定していない — 次のポールまで待機
|
|
1355
|
-
console.error(`[ChatGPT] Stop button gone but text not stable yet (len=${currentTextLen}, stableCount=${textStableCount})`);
|
|
1384
|
+
console.error(`[ChatGPT] Stop button gone (${stopButtonGoneCount} polls) but text not stable yet (len=${currentTextLen}, stableCount=${textStableCount})`);
|
|
1356
1385
|
await new Promise(r => setTimeout(r, pollIntervalMs));
|
|
1357
1386
|
continue;
|
|
1358
1387
|
}
|
|
@@ -1360,7 +1389,8 @@ async function askChatGPTFastInternal(question, debug, budgetMs) {
|
|
|
1360
1389
|
// (stopボタンを見逃した場合の救済)
|
|
1361
1390
|
const elapsed = Date.now() - startWait;
|
|
1362
1391
|
if (elapsed > 5000 && !state.hasStopButton && !state.inputBoxHasText &&
|
|
1363
|
-
state.assistantMsgCount > initialAssistantCount && !state.isStillGenerating
|
|
1392
|
+
state.assistantMsgCount > initialAssistantCount && !state.isStillGenerating &&
|
|
1393
|
+
textGrowingCount === 0) {
|
|
1364
1394
|
if (textStableCount >= 2) {
|
|
1365
1395
|
console.error(`[ChatGPT] Response complete - fallback after 5s, text stable (len=${currentTextLen}, stableCount=${textStableCount})`);
|
|
1366
1396
|
break;
|
|
@@ -1379,8 +1409,10 @@ async function askChatGPTFastInternal(question, debug, budgetMs) {
|
|
|
1379
1409
|
}
|
|
1380
1410
|
await new Promise(r => setTimeout(r, pollIntervalMs));
|
|
1381
1411
|
}
|
|
1382
|
-
//
|
|
1383
|
-
|
|
1412
|
+
// タイムアウトチェック(IDLE_TIMEOUT で抜けた場合も含む)
|
|
1413
|
+
const loopElapsed = Date.now() - startWait;
|
|
1414
|
+
const loopIdle = Date.now() - lastActivityAt;
|
|
1415
|
+
if (loopIdle >= IDLE_TIMEOUT_MS || (!sawStopButton && loopElapsed >= maxWaitMs)) {
|
|
1384
1416
|
const finalState = await client.evaluate(`
|
|
1385
1417
|
(() => {
|
|
1386
1418
|
// フォールバックセレクター付きの検出
|
|
@@ -1409,9 +1441,14 @@ async function askChatGPTFastInternal(question, debug, budgetMs) {
|
|
|
1409
1441
|
};
|
|
1410
1442
|
})()
|
|
1411
1443
|
`);
|
|
1412
|
-
|
|
1444
|
+
const elapsedMs = Date.now() - startWait;
|
|
1445
|
+
const idleMs = Date.now() - lastActivityAt;
|
|
1446
|
+
const reason = idleMs >= IDLE_TIMEOUT_MS
|
|
1447
|
+
? `idle for ${Math.round(idleMs / 1000)}s (no stop button or text growth)`
|
|
1448
|
+
: `absolute ceiling ${maxWaitMs}ms reached`;
|
|
1449
|
+
console.error(`[ChatGPT] Timeout - ${reason}, elapsed=${Math.round(elapsedMs / 1000)}s, final state: ${JSON.stringify(finalState)}`);
|
|
1413
1450
|
await resetConnection('chatgpt');
|
|
1414
|
-
throw new Error(`Timed out waiting for ChatGPT response (${
|
|
1451
|
+
throw new Error(`Timed out waiting for ChatGPT response (${Math.round(elapsedMs / 1000)}s, ${reason}). Final state: ${JSON.stringify(finalState)}`);
|
|
1415
1452
|
}
|
|
1416
1453
|
// ChatGPT 5.2 Thinking モデル対応:
|
|
1417
1454
|
// 回答が「思考」として折りたたまれている場合は展開してからテキストを取得
|
|
@@ -2714,12 +2751,16 @@ async function askGeminiFastInternal(question, debug, budgetMs) {
|
|
|
2714
2751
|
// ChatGPT側と同様のポーリングループで応答完了を検出
|
|
2715
2752
|
const maxWaitMs = getResponseWaitBudgetMs(t0, RESPONSE_WAIT_MAX_MS, 'gemini-response', budgetMs);
|
|
2716
2753
|
const pollIntervalMs = 1000;
|
|
2754
|
+
const IDLE_TIMEOUT_MS = 60000; // ストップボタン消失後、60秒間無活動でタイムアウト
|
|
2717
2755
|
const startWait = Date.now();
|
|
2756
|
+
let lastActivityAt = Date.now(); // 最後にストップボタンorテキスト成長を検出した時刻
|
|
2718
2757
|
let lastLoggedState = '';
|
|
2719
2758
|
let sawStopButton = false; // 停止ボタンを見たかどうか(生成が始まった証拠)
|
|
2720
2759
|
let lastTextLength = 0;
|
|
2721
2760
|
let textStableCount = 0; // テキスト長が変わらなかった回数
|
|
2722
|
-
|
|
2761
|
+
// ストップボタンが見えている間は maxWaitMs を無視し、IDLE_TIMEOUT のみで判定する。
|
|
2762
|
+
while (Date.now() - lastActivityAt < IDLE_TIMEOUT_MS &&
|
|
2763
|
+
(sawStopButton || Date.now() - startWait < maxWaitMs)) {
|
|
2723
2764
|
const state = await client.evaluate(`
|
|
2724
2765
|
(() => {
|
|
2725
2766
|
${DOM_UTILS_CODE}
|
|
@@ -2821,14 +2862,20 @@ async function askGeminiFastInternal(question, debug, budgetMs) {
|
|
|
2821
2862
|
};
|
|
2822
2863
|
})()
|
|
2823
2864
|
`);
|
|
2824
|
-
//
|
|
2865
|
+
// 停止ボタンを検出したらフラグを立て、アクティビティを更新
|
|
2825
2866
|
if (state.hasStopButton) {
|
|
2826
2867
|
sawStopButton = true;
|
|
2868
|
+
lastActivityAt = Date.now();
|
|
2827
2869
|
}
|
|
2828
2870
|
// テキスト長安定化検出
|
|
2829
2871
|
if (state.lastResponseTextLength === lastTextLength && state.lastResponseTextLength > 0) {
|
|
2830
2872
|
textStableCount++;
|
|
2831
2873
|
}
|
|
2874
|
+
else if (state.lastResponseTextLength > lastTextLength) {
|
|
2875
|
+
textStableCount = 0;
|
|
2876
|
+
lastTextLength = state.lastResponseTextLength;
|
|
2877
|
+
lastActivityAt = Date.now(); // テキスト成長もアクティビティとみなす
|
|
2878
|
+
}
|
|
2832
2879
|
else {
|
|
2833
2880
|
textStableCount = 0;
|
|
2834
2881
|
lastTextLength = state.lastResponseTextLength;
|
|
@@ -2878,8 +2925,13 @@ async function askGeminiFastInternal(question, debug, budgetMs) {
|
|
|
2878
2925
|
}
|
|
2879
2926
|
await new Promise(r => setTimeout(r, pollIntervalMs));
|
|
2880
2927
|
}
|
|
2881
|
-
//
|
|
2882
|
-
|
|
2928
|
+
// タイムアウトチェック(IDLE_TIMEOUT で抜けた場合も含む)
|
|
2929
|
+
const geminiLoopElapsed = Date.now() - startWait;
|
|
2930
|
+
const geminiLoopIdle = Date.now() - lastActivityAt;
|
|
2931
|
+
if (geminiLoopIdle >= IDLE_TIMEOUT_MS || (!sawStopButton && geminiLoopElapsed >= maxWaitMs)) {
|
|
2932
|
+
const reason = geminiLoopIdle >= IDLE_TIMEOUT_MS
|
|
2933
|
+
? `idle for ${Math.round(geminiLoopIdle / 1000)}s (no stop button or text growth)`
|
|
2934
|
+
: `absolute ceiling ${maxWaitMs}ms reached (stop button never seen)`;
|
|
2883
2935
|
const finalState = await client.evaluate(`
|
|
2884
2936
|
(() => {
|
|
2885
2937
|
const textIncludes = (needle) => document.body && document.body.innerText && document.body.innerText.includes(needle);
|
|
@@ -2908,9 +2960,9 @@ async function askGeminiFastInternal(question, debug, budgetMs) {
|
|
|
2908
2960
|
};
|
|
2909
2961
|
})()
|
|
2910
2962
|
`);
|
|
2911
|
-
console.error(`[Gemini] Timeout - final state: ${JSON.stringify(finalState)}`);
|
|
2963
|
+
console.error(`[Gemini] Timeout - ${reason}, elapsed=${Math.round(geminiLoopElapsed / 1000)}s, final state: ${JSON.stringify(finalState)}`);
|
|
2912
2964
|
await resetConnection('gemini');
|
|
2913
|
-
throw new Error(`Timed out waiting for Gemini response (${
|
|
2965
|
+
throw new Error(`Timed out waiting for Gemini response (${Math.round(geminiLoopElapsed / 1000)}s, ${reason}). sawStopButton=${sawStopButton}, textStableCount=${textStableCount}. Final state: ${JSON.stringify(finalState)}`);
|
|
2914
2966
|
}
|
|
2915
2967
|
// 重要: タブをフォアグラウンドに持ってくる(バックグラウンドタブ対策)
|
|
2916
2968
|
// GeminiもChatGPTと同様、バックグラウンドタブではDOMの状態が正しく取得できない
|
package/build/src/main.js
CHANGED
|
@@ -202,7 +202,7 @@ const httpServer = http.createServer(async (req, res) => {
|
|
|
202
202
|
return;
|
|
203
203
|
}
|
|
204
204
|
const { target, question, debug: debugFlag, budgetMs: requestBudgetMs } = parsed;
|
|
205
|
-
const effectiveBudgetMs = requestBudgetMs ??
|
|
205
|
+
const effectiveBudgetMs = requestBudgetMs ?? 300000;
|
|
206
206
|
if (!target || !question) {
|
|
207
207
|
res.writeHead(400, { 'Content-Type': 'application/json' }).end(JSON.stringify({ success: false, error: 'Missing required fields: target, question' }));
|
|
208
208
|
return;
|
package/package.json
CHANGED
package/scripts/cab
CHANGED
|
@@ -11,7 +11,9 @@ set -euo pipefail
|
|
|
11
11
|
CAB_PORT="${CAI_IPC_PORT:-9321}"
|
|
12
12
|
CAB_HOST="127.0.0.1"
|
|
13
13
|
CAB_BASE="http://${CAB_HOST}:${CAB_PORT}"
|
|
14
|
-
|
|
14
|
+
# Resolve symlinks to get the real script path (macOS compatible)
|
|
15
|
+
_CAB_SELF="$(realpath "$0" 2>/dev/null || python3 -c "import os,sys; print(os.path.realpath(sys.argv[1]))" "$0")"
|
|
16
|
+
CAB_DIR="$(cd "$(dirname "$_CAB_SELF")" && pwd)"
|
|
15
17
|
CAB_LOG_DIR="${HOME}/.cache/chrome-ai-bridge"
|
|
16
18
|
CAB_LOG="${CAB_LOG_DIR}/cab-daemon.log"
|
|
17
19
|
CAB_STARTUP_TIMEOUT=30
|