chrome-ai-bridge 2.4.0 → 2.5.2
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/README.md +28 -40
- package/build/extension/README.md +10 -10
- package/build/extension/background.mjs +94 -26
- package/build/extension/manifest.json +2 -2
- package/build/extension/relay-server.ts +16 -2
- package/build/extension/ui/connect.html +1 -1
- package/build/extension/ui/connect.js +46 -10
- package/build/src/cli.js +6 -1
- package/build/src/config.js +2 -4
- package/build/src/extension/relay-server.js +20 -2
- package/build/src/fast-cdp/agent-context.js +2 -2
- package/build/src/fast-cdp/{mcp-logger.js → debug-logger.js} +11 -11
- package/build/src/fast-cdp/extension-raw.js +51 -5
- package/build/src/fast-cdp/fast-chat.js +166 -101
- package/build/src/logger.js +3 -3
- package/build/src/main.js +104 -568
- package/build/src/plugin-api.js +1 -1
- package/build/src/runtime-scope.js +1 -1
- package/build/src/tools/ai-helpers.js +72 -17
- package/build/src/tools/chatgpt-gemini-web.js +1 -1
- package/build/src/tools/chatgpt-web.js +7 -7
- package/build/src/tools/gemini-web.js +10 -22
- package/build/src/tools/optional-tools.js +8 -5
- package/package.json +17 -18
- package/scripts/cab +202 -0
- package/scripts/cli.mjs +1 -1
- package/build/src/McpResponse.js +0 -60
- package/build/src/stdio-http-proxy.js +0 -157
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[](https://npmjs.org/package/chrome-ai-bridge)
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
|
|
6
|
-
>
|
|
6
|
+
> **Requires Chrome Extension** — This CLI tool controls ChatGPT/Gemini tabs via a browser extension.
|
|
7
7
|
|
|
8
8
|
Let your AI assistant (Claude Code, Cursor, etc.) consult ChatGPT and Gemini for second opinions.
|
|
9
9
|
|
|
@@ -12,20 +12,20 @@ Let your AI assistant (Claude Code, Cursor, etc.) consult ChatGPT and Gemini for
|
|
|
12
12
|
## How it works
|
|
13
13
|
|
|
14
14
|
```
|
|
15
|
-
Your AI Assistant →
|
|
15
|
+
Your AI Assistant → cab CLI / daemon → Chrome Extension → ChatGPT/Gemini tabs
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
1. **Chrome Extension** (you install) bridges
|
|
19
|
-
2.
|
|
18
|
+
1. **Chrome Extension** (you install) bridges the daemon and browser
|
|
19
|
+
2. **`cab` CLI / daemon** (npm package) receives requests from your AI assistant via REST API
|
|
20
20
|
3. **Extension** controls ChatGPT/Gemini tabs via CDP (Chrome DevTools Protocol)
|
|
21
21
|
|
|
22
|
-
**Why an extension?** ChatGPT and Gemini don't have public APIs. The extension automates the web UI while you stay logged in.
|
|
22
|
+
**Why an extension?** ChatGPT and Gemini don't have free public APIs. The extension automates the web UI while you stay logged in.
|
|
23
23
|
|
|
24
24
|
---
|
|
25
25
|
|
|
26
26
|
## What is this?
|
|
27
27
|
|
|
28
|
-
chrome-ai-bridge is a
|
|
28
|
+
chrome-ai-bridge is a CLI tool and daemon that gives AI assistants the ability to:
|
|
29
29
|
|
|
30
30
|
- **Consult other AIs**: Ask ChatGPT and Gemini questions via browser
|
|
31
31
|
- **Get multiple perspectives**: Query both AIs in parallel for second opinions
|
|
@@ -71,7 +71,7 @@ Network extraction is the primary path. DOM extraction remains as an automatic f
|
|
|
71
71
|
|
|
72
72
|
## Quick Start
|
|
73
73
|
|
|
74
|
-
>
|
|
74
|
+
> **Both steps are required** — The extension and daemon work together.
|
|
75
75
|
|
|
76
76
|
### Step 1: Install Chrome Extension
|
|
77
77
|
|
|
@@ -95,30 +95,23 @@ Then load the extension in Chrome:
|
|
|
95
95
|
|
|
96
96
|
You should see "Chrome AI Bridge" appear in your extensions list.
|
|
97
97
|
|
|
98
|
-
### Step 2:
|
|
98
|
+
### Step 2: Install the `cab` CLI
|
|
99
99
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
```json
|
|
103
|
-
{
|
|
104
|
-
"mcpServers": {
|
|
105
|
-
"chrome-ai-bridge": {
|
|
106
|
-
"command": "npx",
|
|
107
|
-
"args": ["chrome-ai-bridge@latest"]
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
100
|
+
```bash
|
|
101
|
+
npm install -g chrome-ai-bridge
|
|
111
102
|
```
|
|
112
103
|
|
|
113
104
|
### Step 3: Connect the Extension
|
|
114
105
|
|
|
115
106
|
1. Open ChatGPT (https://chatgpt.com) or Gemini (https://gemini.google.com) in Chrome
|
|
116
107
|
2. Log in to both services
|
|
117
|
-
3. The extension will automatically connect when the
|
|
108
|
+
3. The extension will automatically connect when the daemon starts
|
|
118
109
|
|
|
119
110
|
### Step 4: Verify it works
|
|
120
111
|
|
|
121
|
-
|
|
112
|
+
```bash
|
|
113
|
+
cab ask chatgpt "How do I implement OAuth in Node.js?"
|
|
114
|
+
```
|
|
122
115
|
|
|
123
116
|
---
|
|
124
117
|
|
|
@@ -187,7 +180,7 @@ User: "Ask ChatGPT specifically about this"
|
|
|
187
180
|
|
|
188
181
|
| Variable | Description |
|
|
189
182
|
|----------|-------------|
|
|
190
|
-
| `
|
|
183
|
+
| `CAI_DISABLE_WEB_LLM` | Set `true` to disable ChatGPT/Gemini tools |
|
|
191
184
|
|
|
192
185
|
---
|
|
193
186
|
|
|
@@ -201,17 +194,10 @@ cd chrome-ai-bridge
|
|
|
201
194
|
npm install && npm run build
|
|
202
195
|
```
|
|
203
196
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
"mcpServers": {
|
|
209
|
-
"chrome-ai-bridge": {
|
|
210
|
-
"command": "node",
|
|
211
|
-
"args": ["/path/to/chrome-ai-bridge/scripts/cli.mjs"]
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
}
|
|
197
|
+
For local development, run the daemon directly:
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
node /path/to/chrome-ai-bridge/scripts/cli.mjs
|
|
215
201
|
```
|
|
216
202
|
|
|
217
203
|
### Commands
|
|
@@ -230,7 +216,7 @@ chrome-ai-bridge/
|
|
|
230
216
|
├── src/
|
|
231
217
|
│ ├── fast-cdp/ # CDP client and AI chat logic
|
|
232
218
|
│ ├── extension/ # Chrome extension source
|
|
233
|
-
│ ├── main.ts #
|
|
219
|
+
│ ├── main.ts # Daemon entry point
|
|
234
220
|
│ └── index.ts # Main exports
|
|
235
221
|
├── scripts/
|
|
236
222
|
│ └── cli.mjs # CLI entry point
|
|
@@ -261,7 +247,7 @@ npm run cdp:gemini
|
|
|
261
247
|
| Guide | Description |
|
|
262
248
|
|-------|-------------|
|
|
263
249
|
| [Technical Spec](docs/SPEC.md) | Detailed architecture and implementation |
|
|
264
|
-
| [Setup Guide](docs/user/setup.md) | Detailed
|
|
250
|
+
| [Setup Guide](docs/user/setup.md) | Detailed setup and configuration |
|
|
265
251
|
| [Troubleshooting](docs/user/troubleshooting.md) | Problem solving |
|
|
266
252
|
| [CI Policy](docs/ci-policy.md) | Required checks and browser E2E lane policy |
|
|
267
253
|
| [Technical Spec - Architecture](docs/SPEC.md#1-architecture-overview) | Extension architecture |
|
|
@@ -276,10 +262,12 @@ npm run cdp:gemini
|
|
|
276
262
|
2. Verify ChatGPT/Gemini tabs are open and logged in
|
|
277
263
|
3. Check the extension popup for connection status
|
|
278
264
|
|
|
279
|
-
###
|
|
265
|
+
### Daemon not responding
|
|
280
266
|
|
|
281
267
|
```bash
|
|
282
|
-
|
|
268
|
+
cab status
|
|
269
|
+
# or restart:
|
|
270
|
+
cab daemon restart
|
|
283
271
|
```
|
|
284
272
|
|
|
285
273
|
### ChatGPT/Gemini not responding
|
|
@@ -295,9 +283,9 @@ npx clear-npx-cache && npx chrome-ai-bridge@latest
|
|
|
295
283
|
## Architecture (v2.0.0)
|
|
296
284
|
|
|
297
285
|
```
|
|
298
|
-
┌─────────────────┐
|
|
299
|
-
│ Claude Code │ ◀──────────────────▶│
|
|
300
|
-
│ (
|
|
286
|
+
┌─────────────────┐ REST API ┌──────────────────┐
|
|
287
|
+
│ Claude Code │ ◀──────────────────▶│ Chrome AI Bridge│
|
|
288
|
+
│ (cab CLI) │ │ (Node.js) │
|
|
301
289
|
└─────────────────┘ └────────┬─────────┘
|
|
302
290
|
│
|
|
303
291
|
▼
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
# chrome-ai-bridge Extension
|
|
2
2
|
|
|
3
|
-
このChrome拡張機能は、chrome-ai-bridge
|
|
3
|
+
このChrome拡張機能は、chrome-ai-bridge サーバーとChromeブラウザのタブを接続します。
|
|
4
4
|
|
|
5
5
|
## アーキテクチャ
|
|
6
6
|
|
|
7
7
|
```
|
|
8
|
-
chrome-ai-bridge
|
|
8
|
+
chrome-ai-bridge サーバー (プロセス1)
|
|
9
9
|
↓ WebSocket
|
|
10
10
|
Extension (TabShareExtension)
|
|
11
11
|
↓ chrome.debugger API
|
|
12
12
|
Chrome Tab #101 (ChatGPT)
|
|
13
13
|
|
|
14
|
-
chrome-ai-bridge
|
|
14
|
+
chrome-ai-bridge サーバー (プロセス2)
|
|
15
15
|
↓ WebSocket
|
|
16
16
|
Extension (TabShareExtension)
|
|
17
17
|
↓ chrome.debugger API
|
|
@@ -66,10 +66,10 @@ npm run build
|
|
|
66
66
|
}
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
-
###
|
|
69
|
+
### サーバー起動
|
|
70
70
|
|
|
71
71
|
1. Claude Codeを起動
|
|
72
|
-
2.
|
|
72
|
+
2. サーバーが自動的に起動し、WebSocket Relayサーバーが立ち上がります
|
|
73
73
|
3. ログに以下のようなメッセージが表示されます:
|
|
74
74
|
|
|
75
75
|
```
|
|
@@ -112,7 +112,7 @@ chrome-extension://[EXTENSION_ID]/ui/connect.html?mcpRelayUrl=ws://127.0.0.1:123
|
|
|
112
112
|
|
|
113
113
|
### 接続確認
|
|
114
114
|
|
|
115
|
-
|
|
115
|
+
サーバーのログに以下のメッセージが表示されれば成功:
|
|
116
116
|
|
|
117
117
|
```
|
|
118
118
|
[Extension Bridge] Extension connected to tab 101
|
|
@@ -128,8 +128,8 @@ src/extension/
|
|
|
128
128
|
├── ui/
|
|
129
129
|
│ ├── connect.html # タブ選択UI
|
|
130
130
|
│ └── connect.js # UIロジック
|
|
131
|
-
├── relay-server.ts # WebSocketサーバー (
|
|
132
|
-
└── extension-transport.ts # Puppeteer Transport実装 (
|
|
131
|
+
├── relay-server.ts # WebSocketサーバー (サーバー側)
|
|
132
|
+
└── extension-transport.ts # Puppeteer Transport実装 (サーバー側)
|
|
133
133
|
```
|
|
134
134
|
|
|
135
135
|
## トラブルシューティング
|
|
@@ -144,10 +144,10 @@ src/extension/
|
|
|
144
144
|
|
|
145
145
|
### Invalid token
|
|
146
146
|
|
|
147
|
-
**原因**:
|
|
147
|
+
**原因**: サーバーのトークンが一致しない
|
|
148
148
|
|
|
149
149
|
**解決策**:
|
|
150
|
-
1.
|
|
150
|
+
1. サーバーのログからトークンを確認
|
|
151
151
|
2. URLパラメータに正しいトークンを含める
|
|
152
152
|
|
|
153
153
|
### Tab not found
|
|
@@ -269,7 +269,7 @@ class TabShareExtension {
|
|
|
269
269
|
case 'connectToRelay':
|
|
270
270
|
this._connectToRelay(
|
|
271
271
|
sender.tab?.id,
|
|
272
|
-
message.
|
|
272
|
+
message.relayUrl,
|
|
273
273
|
message.sessionId,
|
|
274
274
|
).then(
|
|
275
275
|
() => sendResponse({success: true}),
|
|
@@ -292,7 +292,7 @@ class TabShareExtension {
|
|
|
292
292
|
sender.tab?.id,
|
|
293
293
|
message.tabId || sender.tab?.id,
|
|
294
294
|
message.windowId || sender.tab?.windowId,
|
|
295
|
-
message.
|
|
295
|
+
message.relayUrl,
|
|
296
296
|
message.tabUrl,
|
|
297
297
|
message.newTab,
|
|
298
298
|
message.sessionId,
|
|
@@ -331,8 +331,8 @@ class TabShareExtension {
|
|
|
331
331
|
return `selector:${selectorTabId}`;
|
|
332
332
|
}
|
|
333
333
|
|
|
334
|
-
async _connectToRelay(selectorTabId,
|
|
335
|
-
if (!
|
|
334
|
+
async _connectToRelay(selectorTabId, relayUrl, sessionId) {
|
|
335
|
+
if (!relayUrl) {
|
|
336
336
|
logError('relay', 'Missing relay URL');
|
|
337
337
|
throw new Error('Missing relay URL');
|
|
338
338
|
}
|
|
@@ -344,10 +344,10 @@ class TabShareExtension {
|
|
|
344
344
|
this._pendingTabSelection.delete(pendingKey);
|
|
345
345
|
ensureKeepAliveAlarm('replace-stale-pending');
|
|
346
346
|
}
|
|
347
|
-
logInfo('relay', 'Connecting to relay', {
|
|
347
|
+
logInfo('relay', 'Connecting to relay', {relayUrl, selectorTabId, sessionId, pendingKey});
|
|
348
348
|
|
|
349
349
|
const openSocket = async attempt => {
|
|
350
|
-
const socket = new WebSocket(
|
|
350
|
+
const socket = new WebSocket(relayUrl);
|
|
351
351
|
await new Promise((resolve, reject) => {
|
|
352
352
|
let settled = false;
|
|
353
353
|
const finish = (handler) => {
|
|
@@ -393,7 +393,7 @@ class TabShareExtension {
|
|
|
393
393
|
let lastError;
|
|
394
394
|
const maxAttempts = 5;
|
|
395
395
|
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
396
|
-
logDebug('relay', `WebSocket attempt ${attempt + 1}/${maxAttempts}`, {
|
|
396
|
+
logDebug('relay', `WebSocket attempt ${attempt + 1}/${maxAttempts}`, {relayUrl});
|
|
397
397
|
try {
|
|
398
398
|
socket = await openSocket(attempt);
|
|
399
399
|
logInfo('relay', 'WebSocket connected', {attempt: attempt + 1});
|
|
@@ -428,7 +428,7 @@ class TabShareExtension {
|
|
|
428
428
|
selectorTabId,
|
|
429
429
|
tabId,
|
|
430
430
|
windowId,
|
|
431
|
-
|
|
431
|
+
relayUrl,
|
|
432
432
|
tabUrl,
|
|
433
433
|
newTab,
|
|
434
434
|
sessionId,
|
|
@@ -482,14 +482,14 @@ class TabShareExtension {
|
|
|
482
482
|
|
|
483
483
|
const pending = this._pendingTabSelection.get(pendingKey);
|
|
484
484
|
if (!pending) {
|
|
485
|
-
logDebug('connect', 'No pending connection, creating relay', {selectorTabId, sessionId,
|
|
485
|
+
logDebug('connect', 'No pending connection, creating relay', {selectorTabId, sessionId, relayUrl});
|
|
486
486
|
// If no pending connection, create one now.
|
|
487
|
-
await this._connectToRelay(selectorTabId,
|
|
487
|
+
await this._connectToRelay(selectorTabId, relayUrl, sessionId);
|
|
488
488
|
}
|
|
489
489
|
const newPending = this._pendingTabSelection.get(pendingKey);
|
|
490
490
|
if (!newPending) {
|
|
491
|
-
logError('connect', 'No active
|
|
492
|
-
throw new Error('No active
|
|
491
|
+
logError('connect', 'No active relay connection');
|
|
492
|
+
throw new Error('No active relay connection');
|
|
493
493
|
}
|
|
494
494
|
|
|
495
495
|
if (
|
|
@@ -521,7 +521,7 @@ class TabShareExtension {
|
|
|
521
521
|
this._tabSessionOwners.set(tabId, sessionId || `selector:${selectorTabId}`);
|
|
522
522
|
logInfo('connect', 'Tab connected successfully', {tabId, windowId, sessionId});
|
|
523
523
|
ensureKeepAliveAlarm('tab-connected');
|
|
524
|
-
//
|
|
524
|
+
// バッジのみ設定(フォーカスはサーバー側が必要に応じて制御)
|
|
525
525
|
await this._setConnectedTab(tabId, true);
|
|
526
526
|
}
|
|
527
527
|
|
|
@@ -601,6 +601,9 @@ class TabShareExtension {
|
|
|
601
601
|
async _getDebugLogs(filter, limit) {
|
|
602
602
|
const result = await chrome.storage.local.get('logs');
|
|
603
603
|
const rawLogs = Array.isArray(result.logs) ? result.logs : [];
|
|
604
|
+
const toIso = value => (typeof value === 'number' && value > 0
|
|
605
|
+
? new Date(value).toISOString()
|
|
606
|
+
: null);
|
|
604
607
|
const normalized = rawLogs.map(logEntry => ({
|
|
605
608
|
ts: logEntry.timestamp || logEntry.ts || new Date().toISOString(),
|
|
606
609
|
category: logEntry.category || 'unknown',
|
|
@@ -628,6 +631,25 @@ class TabShareExtension {
|
|
|
628
631
|
activeConnections: Array.from(this._activeConnections.keys()),
|
|
629
632
|
pendingTabSelection: Array.from(this._pendingTabSelection.keys()),
|
|
630
633
|
tabSessionOwners: Object.fromEntries(this._tabSessionOwners.entries()),
|
|
634
|
+
discovery: {
|
|
635
|
+
mode: discoveryMode,
|
|
636
|
+
intervalMs: getDiscoveryIntervalMs(),
|
|
637
|
+
isRunning: isDiscoveryRunning,
|
|
638
|
+
hasScheduledTick: discoveryIntervalId !== null,
|
|
639
|
+
emptyDiscoveryStreak,
|
|
640
|
+
lastSuccessfulPort,
|
|
641
|
+
lastTickStartedAt: toIso(lastDiscoveryTickStartedAt),
|
|
642
|
+
lastTickFinishedAt: toIso(lastDiscoveryTickFinishedAt),
|
|
643
|
+
lastTickDurationMs: lastDiscoveryTickDurationMs,
|
|
644
|
+
lastSummary: lastDiscoverySummary,
|
|
645
|
+
lastError: lastDiscoveryError,
|
|
646
|
+
lastRelayProbeAt: toIso(lastRelayProbeAt),
|
|
647
|
+
lastRelayProbePort,
|
|
648
|
+
lastRelayProbeStatus,
|
|
649
|
+
lastRelayProbeError,
|
|
650
|
+
keepAliveActive,
|
|
651
|
+
lastKeepAliveAlarmAt: toIso(lastKeepAliveAlarmAt),
|
|
652
|
+
},
|
|
631
653
|
},
|
|
632
654
|
};
|
|
633
655
|
}
|
|
@@ -717,7 +739,7 @@ class TabShareExtension {
|
|
|
717
739
|
|
|
718
740
|
const tabShareExtension = new TabShareExtension();
|
|
719
741
|
|
|
720
|
-
const DISCOVERY_ALARM = '
|
|
742
|
+
const DISCOVERY_ALARM = 'cab-relay-discovery';
|
|
721
743
|
const KEEPALIVE_ALARM = 'keepAlive';
|
|
722
744
|
const KEEPALIVE_PERIOD_MINUTES = 0.5;
|
|
723
745
|
const DISCOVERY_PORTS = [38765, 38766, 38767, 38768, 38769, 38770, 38771, 38772, 38773, 38774, 38775];
|
|
@@ -745,6 +767,16 @@ let isDiscoveryRunning = false;
|
|
|
745
767
|
let discoveryMode = DISCOVERY_MODE.FAST;
|
|
746
768
|
let emptyDiscoveryStreak = 0;
|
|
747
769
|
let keepAliveActive = false;
|
|
770
|
+
let lastDiscoveryTickStartedAt = 0;
|
|
771
|
+
let lastDiscoveryTickFinishedAt = 0;
|
|
772
|
+
let lastDiscoveryTickDurationMs = null;
|
|
773
|
+
let lastDiscoverySummary = null;
|
|
774
|
+
let lastDiscoveryError = null;
|
|
775
|
+
let lastRelayProbeAt = 0;
|
|
776
|
+
let lastRelayProbePort = null;
|
|
777
|
+
let lastRelayProbeStatus = null;
|
|
778
|
+
let lastRelayProbeError = null;
|
|
779
|
+
let lastKeepAliveAlarmAt = 0;
|
|
748
780
|
|
|
749
781
|
// リロード時クールダウン: 5秒間は「新しいrelay」検出をスキップ
|
|
750
782
|
// ただし reloadExtension コマンド経由のリロード時はスキップしない
|
|
@@ -855,7 +887,7 @@ function buildConnectUrl(
|
|
|
855
887
|
sessionId,
|
|
856
888
|
allowTabTakeover = false,
|
|
857
889
|
) {
|
|
858
|
-
const params = new URLSearchParams({
|
|
890
|
+
const params = new URLSearchParams({relayUrl: wsUrl});
|
|
859
891
|
if (tabUrl) params.set('tabUrl', tabUrl);
|
|
860
892
|
if (newTab) params.set('newTab', 'true');
|
|
861
893
|
if (autoMode) params.set('auto', 'true');
|
|
@@ -915,16 +947,34 @@ async function ensureConnectUiTab(
|
|
|
915
947
|
async function fetchRelayInfo(port, timeoutMs = 800) {
|
|
916
948
|
const discoveryUrl = `http://127.0.0.1:${port}/relay-info`;
|
|
917
949
|
let timer = null;
|
|
950
|
+
lastRelayProbeAt = Date.now();
|
|
951
|
+
lastRelayProbePort = port;
|
|
918
952
|
try {
|
|
919
953
|
const controller = new AbortController();
|
|
920
954
|
timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
921
955
|
const res = await fetch(discoveryUrl, {signal: controller.signal});
|
|
922
|
-
if (!res.ok)
|
|
956
|
+
if (!res.ok) {
|
|
957
|
+
lastRelayProbeStatus = `http-${res.status}`;
|
|
958
|
+
lastRelayProbeError = null;
|
|
959
|
+
return null;
|
|
960
|
+
}
|
|
923
961
|
const data = await res.json();
|
|
924
|
-
if (!data?.wsUrl)
|
|
962
|
+
if (!data?.wsUrl) {
|
|
963
|
+
lastRelayProbeStatus = 'invalid-payload';
|
|
964
|
+
lastRelayProbeError = null;
|
|
965
|
+
return null;
|
|
966
|
+
}
|
|
967
|
+
lastRelayProbeStatus = 'ok';
|
|
968
|
+
lastRelayProbeError = null;
|
|
925
969
|
lastSuccessfulPort = port;
|
|
926
970
|
return data;
|
|
927
|
-
} catch {
|
|
971
|
+
} catch (error) {
|
|
972
|
+
const message =
|
|
973
|
+
error && typeof error === 'object' && 'message' in error
|
|
974
|
+
? String(error.message)
|
|
975
|
+
: String(error);
|
|
976
|
+
lastRelayProbeStatus = 'fetch-error';
|
|
977
|
+
lastRelayProbeError = message;
|
|
928
978
|
return null;
|
|
929
979
|
} finally {
|
|
930
980
|
if (timer) {
|
|
@@ -958,7 +1008,7 @@ async function autoConnectRelay(best) {
|
|
|
958
1008
|
let targetTabId;
|
|
959
1009
|
try {
|
|
960
1010
|
// autoConnectRelay経由の場合はフォーカスしない(active: false)
|
|
961
|
-
// newTab: relay
|
|
1011
|
+
// newTab: relay の要求を尊重する(サーバーが newTab: true を指定した場合は新規タブ作成を許可)
|
|
962
1012
|
targetTabId = await tabShareExtension._resolveTabId(
|
|
963
1013
|
tabUrl,
|
|
964
1014
|
preferredTabId,
|
|
@@ -1040,7 +1090,7 @@ async function autoOpenConnectUi() {
|
|
|
1040
1090
|
failureCount: 0,
|
|
1041
1091
|
};
|
|
1042
1092
|
|
|
1043
|
-
//
|
|
1093
|
+
// リロード直後はタブを開かない(既存サーバーとの再接続を防ぐ)
|
|
1044
1094
|
// ただし reloadExtension コマンド経由の場合はスキップしない
|
|
1045
1095
|
const elapsed = Date.now() - extensionStartTime;
|
|
1046
1096
|
if (elapsed < COOLDOWN_MS && !cooldownDisabled) {
|
|
@@ -1081,7 +1131,7 @@ async function autoOpenConnectUi() {
|
|
|
1081
1131
|
}
|
|
1082
1132
|
|
|
1083
1133
|
// Deduplicate: when multiple relays serve the same tabUrl, prefer the newest (by startedAt)
|
|
1084
|
-
// This prevents stale
|
|
1134
|
+
// This prevents stale servers from stealing connections from active ones
|
|
1085
1135
|
const bestByTabUrl = new Map();
|
|
1086
1136
|
for (const relay of newRelays) {
|
|
1087
1137
|
const tabUrl = relay.data?.tabUrl || '';
|
|
@@ -1157,9 +1207,9 @@ async function autoOpenConnectUi() {
|
|
|
1157
1207
|
return result;
|
|
1158
1208
|
}
|
|
1159
1209
|
|
|
1160
|
-
// Discovery is now passive - only triggered by
|
|
1210
|
+
// Discovery is now passive - only triggered by Chrome AI Bridge requests
|
|
1161
1211
|
// The extension no longer auto-opens tabs on install/startup
|
|
1162
|
-
//
|
|
1212
|
+
// サーバーからの明示的な接続要求時のみ動作する
|
|
1163
1213
|
|
|
1164
1214
|
// Clear any existing discovery alarms from previous sessions
|
|
1165
1215
|
// This prevents leftover alarms from auto-opening tabs
|
|
@@ -1223,21 +1273,38 @@ function scheduleDiscoveryTick(delayMs) {
|
|
|
1223
1273
|
}
|
|
1224
1274
|
|
|
1225
1275
|
isDiscoveryRunning = true;
|
|
1276
|
+
lastDiscoveryTickStartedAt = Date.now();
|
|
1277
|
+
lastDiscoveryError = null;
|
|
1278
|
+
lastDiscoverySummary = null;
|
|
1226
1279
|
try {
|
|
1227
1280
|
const result = await autoOpenConnectUi();
|
|
1281
|
+
lastDiscoverySummary = {
|
|
1282
|
+
...result,
|
|
1283
|
+
mode: discoveryMode,
|
|
1284
|
+
intervalMs: getDiscoveryIntervalMs(),
|
|
1285
|
+
};
|
|
1228
1286
|
updateDiscoveryMode(result);
|
|
1229
1287
|
} catch (error) {
|
|
1288
|
+
lastDiscoveryError =
|
|
1289
|
+
error && typeof error === 'object' && 'message' in error
|
|
1290
|
+
? String(error.message)
|
|
1291
|
+
: String(error);
|
|
1230
1292
|
logWarn('discovery', 'Discovery cycle failed', {
|
|
1231
|
-
error:
|
|
1293
|
+
error: lastDiscoveryError,
|
|
1232
1294
|
});
|
|
1233
1295
|
emptyDiscoveryStreak = 0;
|
|
1234
1296
|
setDiscoveryMode(DISCOVERY_MODE.FAST, 'cycle-error');
|
|
1235
1297
|
} finally {
|
|
1298
|
+
lastDiscoveryTickFinishedAt = Date.now();
|
|
1299
|
+
lastDiscoveryTickDurationMs =
|
|
1300
|
+
lastDiscoveryTickFinishedAt - lastDiscoveryTickStartedAt;
|
|
1236
1301
|
isDiscoveryRunning = false;
|
|
1237
|
-
ensureKeepAliveAlarm('discovery-cycle');
|
|
1238
1302
|
}
|
|
1239
1303
|
|
|
1240
1304
|
scheduleDiscoveryTick(getDiscoveryIntervalMs());
|
|
1305
|
+
// Must run AFTER scheduleDiscoveryTick so discoveryIntervalId is set,
|
|
1306
|
+
// otherwise shouldKeepAlive() returns false and clears the alarm.
|
|
1307
|
+
ensureKeepAliveAlarm('discovery-cycle');
|
|
1241
1308
|
}, Math.max(0, delayMs));
|
|
1242
1309
|
}
|
|
1243
1310
|
|
|
@@ -1270,6 +1337,7 @@ function kickDiscovery(reason) {
|
|
|
1270
1337
|
|
|
1271
1338
|
chrome.alarms.onAlarm.addListener((alarm) => {
|
|
1272
1339
|
if (alarm.name === KEEPALIVE_ALARM) {
|
|
1340
|
+
lastKeepAliveAlarmAt = Date.now();
|
|
1273
1341
|
const {activeCount, pendingCount} = getConnectionCounts();
|
|
1274
1342
|
if (activeCount > 0 || pendingCount > 0) {
|
|
1275
1343
|
logDebug('keepalive', 'Alarm triggered', {activeCount, pendingCount});
|
|
@@ -1288,7 +1356,7 @@ chrome.alarms.onAlarm.addListener((alarm) => {
|
|
|
1288
1356
|
});
|
|
1289
1357
|
|
|
1290
1358
|
// Note: We no longer register an onAlarm listener for DISCOVERY_ALARM
|
|
1291
|
-
// The scheduleDiscovery function is only called on explicit
|
|
1359
|
+
// The scheduleDiscovery function is only called on explicit server requests
|
|
1292
1360
|
|
|
1293
1361
|
// Discovery auto-starts on Chrome startup
|
|
1294
1362
|
// connect.html only opens when user clicks the extension icon
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "chrome-ai-bridge Extension",
|
|
4
|
-
"version": "2.0.
|
|
4
|
+
"version": "2.0.30",
|
|
5
5
|
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqDLiSB+b/gbnQ4zWRP65jnd27KmzpjJyR1JAQIjCD/dNORzTgk6+G0TjYEDZLIDceKHzGJudqnwq4q9g3T1eJ0SECZNXnaoE00WkgXfAUSQn6cmmXR3aQGFky/zbCmxnkRa0vYupxszlhw0yrlSZrIJd/weWF75Byh0zJfZ84kqDDhaj7TlB5laHICnoSLmPTif4mQcUW9oOKmAJPriPw4CWATKZsrQ4X46djxefSmfbqYfb9rttAqJVst40gO0Gsl6GOGxMHMds5Cl9GELc0dI3Gpobw07hQldZb8TeyilI/SnOaeS3HPtrp+KyEgRu8SgRdlrvuq6DeEZsP+kK7wIDAQAB",
|
|
6
|
-
"description": "Bridge between Chrome tabs and
|
|
6
|
+
"description": "Bridge between Chrome tabs and Chrome AI Bridge",
|
|
7
7
|
"permissions": ["debugger", "activeTab", "tabs", "storage", "alarms"],
|
|
8
8
|
"host_permissions": ["<all_urls>"],
|
|
9
9
|
"background": {
|
|
@@ -466,9 +466,23 @@ export class RelayServer extends EventEmitter {
|
|
|
466
466
|
this.stopDiscoveryServer();
|
|
467
467
|
|
|
468
468
|
if (this.wss) {
|
|
469
|
+
// Capture ref before nulling so isReady() returns false immediately
|
|
470
|
+
const wss = this.wss;
|
|
471
|
+
this.wss = null;
|
|
472
|
+
|
|
469
473
|
return new Promise((resolve) => {
|
|
470
|
-
|
|
471
|
-
|
|
474
|
+
const timeout = setTimeout(() => {
|
|
475
|
+
debugLog('[RelayServer] stop() timed out after 5s — force-terminating remaining clients');
|
|
476
|
+
for (const client of wss.clients) {
|
|
477
|
+
try { client.terminate(); } catch { /* ignore */ }
|
|
478
|
+
}
|
|
479
|
+
try { wss.close(); } catch { /* ignore */ }
|
|
480
|
+
debugLog('[RelayServer] Server stopped (forced)');
|
|
481
|
+
resolve();
|
|
482
|
+
}, 5000);
|
|
483
|
+
|
|
484
|
+
wss.close(() => {
|
|
485
|
+
clearTimeout(timeout);
|
|
472
486
|
debugLog('[RelayServer] Server stopped');
|
|
473
487
|
resolve();
|
|
474
488
|
});
|
|
@@ -382,7 +382,7 @@
|
|
|
382
382
|
</div>
|
|
383
383
|
|
|
384
384
|
<div id="tab-selection" class="hidden">
|
|
385
|
-
<div class="section-label">Select page to expose to
|
|
385
|
+
<div class="section-label">Select page to expose to Chrome AI Bridge:</div>
|
|
386
386
|
<div class="tabs-list" id="tabs-list"></div>
|
|
387
387
|
</div>
|
|
388
388
|
|