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.
@@ -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', '50000'));
65
- const RESPONSE_WAIT_MAX_MS = Number(process.env.CAI_RESPONSE_WAIT_MAX_MS || '40000');
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
- while (Date.now() - startWait < maxWaitMs) {
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
- if (sawStopButton && !state.hasStopButton && !state.inputBoxHasText &&
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
- if (Date.now() - startWait >= maxWaitMs) {
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
- console.error(`[ChatGPT] Timeout - final state: ${JSON.stringify(finalState)}`);
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 (${maxWaitMs}ms). Final state: ${JSON.stringify(finalState)}`);
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
- while (Date.now() - startWait < maxWaitMs) {
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
- if (Date.now() - startWait >= maxWaitMs) {
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 (${maxWaitMs}ms). sawStopButton=${sawStopButton}, textStableCount=${textStableCount}. Final state: ${JSON.stringify(finalState)}`);
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 ?? 120000;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chrome-ai-bridge",
3
- "version": "2.5.1",
3
+ "version": "2.5.3",
4
4
  "description": "CLI tool for querying ChatGPT and Gemini via Chrome extension. No Puppeteer required.",
5
5
  "type": "module",
6
6
  "bin": {
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
- CAB_DIR="$(cd "$(dirname "$0")" && pwd)"
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