chrome-ai-bridge 2.3.10 → 2.5.1
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 +159 -36
- package/build/extension/manifest.json +2 -2
- package/build/extension/relay-server.ts +55 -7
- 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 +47 -7
- 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 +55 -6
- package/build/src/fast-cdp/fast-chat.js +137 -98
- package/build/src/fast-cdp/network-interceptor.js +96 -26
- 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 +200 -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
|
|
@@ -170,6 +170,8 @@ class RelayConnection {
|
|
|
170
170
|
}
|
|
171
171
|
if (message.method === 'reloadExtension') {
|
|
172
172
|
logInfo('reload', 'reloadExtension command received');
|
|
173
|
+
// Set flag so the reloaded service worker skips cooldown
|
|
174
|
+
chrome.storage.local.set({ _reloadTriggered: Date.now() }).catch(() => {});
|
|
173
175
|
// Delay reload to allow response to be sent first
|
|
174
176
|
setTimeout(() => {
|
|
175
177
|
logInfo('reload', 'Calling chrome.runtime.reload()');
|
|
@@ -267,7 +269,7 @@ class TabShareExtension {
|
|
|
267
269
|
case 'connectToRelay':
|
|
268
270
|
this._connectToRelay(
|
|
269
271
|
sender.tab?.id,
|
|
270
|
-
message.
|
|
272
|
+
message.relayUrl,
|
|
271
273
|
message.sessionId,
|
|
272
274
|
).then(
|
|
273
275
|
() => sendResponse({success: true}),
|
|
@@ -290,7 +292,7 @@ class TabShareExtension {
|
|
|
290
292
|
sender.tab?.id,
|
|
291
293
|
message.tabId || sender.tab?.id,
|
|
292
294
|
message.windowId || sender.tab?.windowId,
|
|
293
|
-
message.
|
|
295
|
+
message.relayUrl,
|
|
294
296
|
message.tabUrl,
|
|
295
297
|
message.newTab,
|
|
296
298
|
message.sessionId,
|
|
@@ -329,8 +331,8 @@ class TabShareExtension {
|
|
|
329
331
|
return `selector:${selectorTabId}`;
|
|
330
332
|
}
|
|
331
333
|
|
|
332
|
-
async _connectToRelay(selectorTabId,
|
|
333
|
-
if (!
|
|
334
|
+
async _connectToRelay(selectorTabId, relayUrl, sessionId) {
|
|
335
|
+
if (!relayUrl) {
|
|
334
336
|
logError('relay', 'Missing relay URL');
|
|
335
337
|
throw new Error('Missing relay URL');
|
|
336
338
|
}
|
|
@@ -342,10 +344,10 @@ class TabShareExtension {
|
|
|
342
344
|
this._pendingTabSelection.delete(pendingKey);
|
|
343
345
|
ensureKeepAliveAlarm('replace-stale-pending');
|
|
344
346
|
}
|
|
345
|
-
logInfo('relay', 'Connecting to relay', {
|
|
347
|
+
logInfo('relay', 'Connecting to relay', {relayUrl, selectorTabId, sessionId, pendingKey});
|
|
346
348
|
|
|
347
349
|
const openSocket = async attempt => {
|
|
348
|
-
const socket = new WebSocket(
|
|
350
|
+
const socket = new WebSocket(relayUrl);
|
|
349
351
|
await new Promise((resolve, reject) => {
|
|
350
352
|
let settled = false;
|
|
351
353
|
const finish = (handler) => {
|
|
@@ -391,7 +393,7 @@ class TabShareExtension {
|
|
|
391
393
|
let lastError;
|
|
392
394
|
const maxAttempts = 5;
|
|
393
395
|
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
394
|
-
logDebug('relay', `WebSocket attempt ${attempt + 1}/${maxAttempts}`, {
|
|
396
|
+
logDebug('relay', `WebSocket attempt ${attempt + 1}/${maxAttempts}`, {relayUrl});
|
|
395
397
|
try {
|
|
396
398
|
socket = await openSocket(attempt);
|
|
397
399
|
logInfo('relay', 'WebSocket connected', {attempt: attempt + 1});
|
|
@@ -426,7 +428,7 @@ class TabShareExtension {
|
|
|
426
428
|
selectorTabId,
|
|
427
429
|
tabId,
|
|
428
430
|
windowId,
|
|
429
|
-
|
|
431
|
+
relayUrl,
|
|
430
432
|
tabUrl,
|
|
431
433
|
newTab,
|
|
432
434
|
sessionId,
|
|
@@ -480,14 +482,14 @@ class TabShareExtension {
|
|
|
480
482
|
|
|
481
483
|
const pending = this._pendingTabSelection.get(pendingKey);
|
|
482
484
|
if (!pending) {
|
|
483
|
-
logDebug('connect', 'No pending connection, creating relay', {selectorTabId, sessionId,
|
|
485
|
+
logDebug('connect', 'No pending connection, creating relay', {selectorTabId, sessionId, relayUrl});
|
|
484
486
|
// If no pending connection, create one now.
|
|
485
|
-
await this._connectToRelay(selectorTabId,
|
|
487
|
+
await this._connectToRelay(selectorTabId, relayUrl, sessionId);
|
|
486
488
|
}
|
|
487
489
|
const newPending = this._pendingTabSelection.get(pendingKey);
|
|
488
490
|
if (!newPending) {
|
|
489
|
-
logError('connect', 'No active
|
|
490
|
-
throw new Error('No active
|
|
491
|
+
logError('connect', 'No active relay connection');
|
|
492
|
+
throw new Error('No active relay connection');
|
|
491
493
|
}
|
|
492
494
|
|
|
493
495
|
if (
|
|
@@ -519,7 +521,7 @@ class TabShareExtension {
|
|
|
519
521
|
this._tabSessionOwners.set(tabId, sessionId || `selector:${selectorTabId}`);
|
|
520
522
|
logInfo('connect', 'Tab connected successfully', {tabId, windowId, sessionId});
|
|
521
523
|
ensureKeepAliveAlarm('tab-connected');
|
|
522
|
-
//
|
|
524
|
+
// バッジのみ設定(フォーカスはサーバー側が必要に応じて制御)
|
|
523
525
|
await this._setConnectedTab(tabId, true);
|
|
524
526
|
}
|
|
525
527
|
|
|
@@ -599,6 +601,9 @@ class TabShareExtension {
|
|
|
599
601
|
async _getDebugLogs(filter, limit) {
|
|
600
602
|
const result = await chrome.storage.local.get('logs');
|
|
601
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);
|
|
602
607
|
const normalized = rawLogs.map(logEntry => ({
|
|
603
608
|
ts: logEntry.timestamp || logEntry.ts || new Date().toISOString(),
|
|
604
609
|
category: logEntry.category || 'unknown',
|
|
@@ -626,6 +631,25 @@ class TabShareExtension {
|
|
|
626
631
|
activeConnections: Array.from(this._activeConnections.keys()),
|
|
627
632
|
pendingTabSelection: Array.from(this._pendingTabSelection.keys()),
|
|
628
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
|
+
},
|
|
629
653
|
},
|
|
630
654
|
};
|
|
631
655
|
}
|
|
@@ -715,7 +739,7 @@ class TabShareExtension {
|
|
|
715
739
|
|
|
716
740
|
const tabShareExtension = new TabShareExtension();
|
|
717
741
|
|
|
718
|
-
const DISCOVERY_ALARM = '
|
|
742
|
+
const DISCOVERY_ALARM = 'cab-relay-discovery';
|
|
719
743
|
const KEEPALIVE_ALARM = 'keepAlive';
|
|
720
744
|
const KEEPALIVE_PERIOD_MINUTES = 0.5;
|
|
721
745
|
const DISCOVERY_PORTS = [38765, 38766, 38767, 38768, 38769, 38770, 38771, 38772, 38773, 38774, 38775];
|
|
@@ -731,7 +755,7 @@ const DISCOVERY_INTERVAL_MS = {
|
|
|
731
755
|
};
|
|
732
756
|
const FAST_TO_NORMAL_EMPTY_STREAK = 5;
|
|
733
757
|
const NORMAL_TO_IDLE_EMPTY_STREAK = 20;
|
|
734
|
-
const ACTIVE_TO_IDLE_EMPTY_STREAK =
|
|
758
|
+
const ACTIVE_TO_IDLE_EMPTY_STREAK = 10;
|
|
735
759
|
let lastSuccessfulPort = null;
|
|
736
760
|
const lastRelayByPort = new Map();
|
|
737
761
|
|
|
@@ -743,10 +767,35 @@ let isDiscoveryRunning = false;
|
|
|
743
767
|
let discoveryMode = DISCOVERY_MODE.FAST;
|
|
744
768
|
let emptyDiscoveryStreak = 0;
|
|
745
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;
|
|
746
780
|
|
|
747
781
|
// リロード時クールダウン: 5秒間は「新しいrelay」検出をスキップ
|
|
782
|
+
// ただし reloadExtension コマンド経由のリロード時はスキップしない
|
|
748
783
|
const extensionStartTime = Date.now();
|
|
749
784
|
const COOLDOWN_MS = 5000;
|
|
785
|
+
let cooldownDisabled = false;
|
|
786
|
+
|
|
787
|
+
// Check if this is a reload triggered by reloadExtension command
|
|
788
|
+
chrome.storage.local.get('_reloadTriggered').then(result => {
|
|
789
|
+
if (result._reloadTriggered) {
|
|
790
|
+
const age = Date.now() - result._reloadTriggered;
|
|
791
|
+
if (age < 10000) { // Within 10 seconds of reload trigger
|
|
792
|
+
cooldownDisabled = true;
|
|
793
|
+
logInfo('discovery', 'Cooldown disabled (reloadExtension triggered)', {age});
|
|
794
|
+
}
|
|
795
|
+
// Clear the flag
|
|
796
|
+
chrome.storage.local.remove('_reloadTriggered').catch(() => {});
|
|
797
|
+
}
|
|
798
|
+
}).catch(() => {});
|
|
750
799
|
|
|
751
800
|
// ユーザー操作によるDiscoveryかどうかのフラグ
|
|
752
801
|
// Chrome起動時やService Worker再起動時はfalse、アイコンクリック時のみtrue
|
|
@@ -838,7 +887,7 @@ function buildConnectUrl(
|
|
|
838
887
|
sessionId,
|
|
839
888
|
allowTabTakeover = false,
|
|
840
889
|
) {
|
|
841
|
-
const params = new URLSearchParams({
|
|
890
|
+
const params = new URLSearchParams({relayUrl: wsUrl});
|
|
842
891
|
if (tabUrl) params.set('tabUrl', tabUrl);
|
|
843
892
|
if (newTab) params.set('newTab', 'true');
|
|
844
893
|
if (autoMode) params.set('auto', 'true');
|
|
@@ -898,16 +947,34 @@ async function ensureConnectUiTab(
|
|
|
898
947
|
async function fetchRelayInfo(port, timeoutMs = 800) {
|
|
899
948
|
const discoveryUrl = `http://127.0.0.1:${port}/relay-info`;
|
|
900
949
|
let timer = null;
|
|
950
|
+
lastRelayProbeAt = Date.now();
|
|
951
|
+
lastRelayProbePort = port;
|
|
901
952
|
try {
|
|
902
953
|
const controller = new AbortController();
|
|
903
954
|
timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
904
955
|
const res = await fetch(discoveryUrl, {signal: controller.signal});
|
|
905
|
-
if (!res.ok)
|
|
956
|
+
if (!res.ok) {
|
|
957
|
+
lastRelayProbeStatus = `http-${res.status}`;
|
|
958
|
+
lastRelayProbeError = null;
|
|
959
|
+
return null;
|
|
960
|
+
}
|
|
906
961
|
const data = await res.json();
|
|
907
|
-
if (!data?.wsUrl)
|
|
962
|
+
if (!data?.wsUrl) {
|
|
963
|
+
lastRelayProbeStatus = 'invalid-payload';
|
|
964
|
+
lastRelayProbeError = null;
|
|
965
|
+
return null;
|
|
966
|
+
}
|
|
967
|
+
lastRelayProbeStatus = 'ok';
|
|
968
|
+
lastRelayProbeError = null;
|
|
908
969
|
lastSuccessfulPort = port;
|
|
909
970
|
return data;
|
|
910
|
-
} 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;
|
|
911
978
|
return null;
|
|
912
979
|
} finally {
|
|
913
980
|
if (timer) {
|
|
@@ -937,15 +1004,15 @@ async function autoConnectRelay(best) {
|
|
|
937
1004
|
|
|
938
1005
|
// tabUrl があれば、connect.html を開かずに直接接続
|
|
939
1006
|
// preferredTabId があれば優先的に使用
|
|
1007
|
+
const requestedNewTab = Boolean(best?.data?.newTab);
|
|
940
1008
|
let targetTabId;
|
|
941
1009
|
try {
|
|
942
1010
|
// autoConnectRelay経由の場合はフォーカスしない(active: false)
|
|
943
|
-
//
|
|
944
|
-
// newTab: false に固定 - 自動接続では既存タブを優先してタブスパムを防止
|
|
1011
|
+
// newTab: relay の要求を尊重する(サーバーが newTab: true を指定した場合は新規タブ作成を許可)
|
|
945
1012
|
targetTabId = await tabShareExtension._resolveTabId(
|
|
946
1013
|
tabUrl,
|
|
947
1014
|
preferredTabId,
|
|
948
|
-
|
|
1015
|
+
requestedNewTab,
|
|
949
1016
|
false, // active: false - 自動接続時はタブをフォーカスしない
|
|
950
1017
|
);
|
|
951
1018
|
} catch (error) {
|
|
@@ -957,8 +1024,23 @@ async function autoConnectRelay(best) {
|
|
|
957
1024
|
return false;
|
|
958
1025
|
}
|
|
959
1026
|
if (tabShareExtension._activeConnections?.has(targetTabId)) {
|
|
960
|
-
|
|
961
|
-
|
|
1027
|
+
const existingSessionId = tabShareExtension._tabSessionOwners?.get(targetTabId);
|
|
1028
|
+
const newSessionId = best?.data?.sessionId;
|
|
1029
|
+
if (existingSessionId && newSessionId && existingSessionId !== newSessionId) {
|
|
1030
|
+
// Different session wants the same tab — replace the old connection
|
|
1031
|
+
logInfo('auto-connect', 'Replacing stale connection with newer session', {
|
|
1032
|
+
targetTabId, oldSession: existingSessionId, newSession: newSessionId,
|
|
1033
|
+
});
|
|
1034
|
+
const oldConn = tabShareExtension._activeConnections.get(targetTabId);
|
|
1035
|
+
if (oldConn) {
|
|
1036
|
+
oldConn.close('Replaced by newer session');
|
|
1037
|
+
tabShareExtension._activeConnections.delete(targetTabId);
|
|
1038
|
+
tabShareExtension._tabSessionOwners.delete(targetTabId);
|
|
1039
|
+
}
|
|
1040
|
+
} else {
|
|
1041
|
+
logInfo('auto-connect', 'Tab already connected', {targetTabId});
|
|
1042
|
+
return true; // 同じセッションで接続済み
|
|
1043
|
+
}
|
|
962
1044
|
}
|
|
963
1045
|
|
|
964
1046
|
const targetTab = await chrome.tabs.get(targetTabId).catch(() => null);
|
|
@@ -1008,9 +1090,10 @@ async function autoOpenConnectUi() {
|
|
|
1008
1090
|
failureCount: 0,
|
|
1009
1091
|
};
|
|
1010
1092
|
|
|
1011
|
-
//
|
|
1093
|
+
// リロード直後はタブを開かない(既存サーバーとの再接続を防ぐ)
|
|
1094
|
+
// ただし reloadExtension コマンド経由の場合はスキップしない
|
|
1012
1095
|
const elapsed = Date.now() - extensionStartTime;
|
|
1013
|
-
if (elapsed < COOLDOWN_MS) {
|
|
1096
|
+
if (elapsed < COOLDOWN_MS && !cooldownDisabled) {
|
|
1014
1097
|
logDebug('discovery', `Cooldown active (${elapsed}ms < ${COOLDOWN_MS}ms), skipping`);
|
|
1015
1098
|
result.skippedCooldown = true;
|
|
1016
1099
|
return result;
|
|
@@ -1038,7 +1121,7 @@ async function autoOpenConnectUi() {
|
|
|
1038
1121
|
continue;
|
|
1039
1122
|
}
|
|
1040
1123
|
|
|
1041
|
-
logInfo('discovery', 'New relay detected', {port, tabUrl: data.tabUrl, wsUrl: data.wsUrl});
|
|
1124
|
+
logInfo('discovery', 'New relay detected', {port, tabUrl: data.tabUrl, wsUrl: data.wsUrl, startedAt});
|
|
1042
1125
|
lastRelayByPort.set(port, {
|
|
1043
1126
|
wsUrl: data.wsUrl,
|
|
1044
1127
|
startedAt,
|
|
@@ -1047,14 +1130,34 @@ async function autoOpenConnectUi() {
|
|
|
1047
1130
|
newRelays.push({port, data});
|
|
1048
1131
|
}
|
|
1049
1132
|
|
|
1050
|
-
|
|
1133
|
+
// Deduplicate: when multiple relays serve the same tabUrl, prefer the newest (by startedAt)
|
|
1134
|
+
// This prevents stale servers from stealing connections from active ones
|
|
1135
|
+
const bestByTabUrl = new Map();
|
|
1136
|
+
for (const relay of newRelays) {
|
|
1137
|
+
const tabUrl = relay.data?.tabUrl || '';
|
|
1138
|
+
const existing = bestByTabUrl.get(tabUrl);
|
|
1139
|
+
const relayStartedAt = relay.data?.startedAt || 0;
|
|
1140
|
+
if (!existing || relayStartedAt > (existing.data?.startedAt || 0)) {
|
|
1141
|
+
bestByTabUrl.set(tabUrl, relay);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
const dedupedRelays = [...bestByTabUrl.values()];
|
|
1145
|
+
if (dedupedRelays.length < newRelays.length) {
|
|
1146
|
+
logInfo('discovery', 'Deduped relays by tabUrl (preferring newest)', {
|
|
1147
|
+
before: newRelays.length,
|
|
1148
|
+
after: dedupedRelays.length,
|
|
1149
|
+
dropped: newRelays.filter(r => !dedupedRelays.includes(r)).map(r => ({port: r.port, tabUrl: r.data?.tabUrl, startedAt: r.data?.startedAt})),
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1051
1152
|
|
|
1052
|
-
|
|
1053
|
-
|
|
1153
|
+
result.newRelayCount = dedupedRelays.length;
|
|
1154
|
+
|
|
1155
|
+
if (dedupedRelays.length > 0) {
|
|
1156
|
+
logInfo('discovery', `Processing ${dedupedRelays.length} new relay(s)`);
|
|
1054
1157
|
}
|
|
1055
1158
|
|
|
1056
1159
|
// 全ての新しい relay を処理(並列ではなく順次)
|
|
1057
|
-
for (const relay of
|
|
1160
|
+
for (const relay of dedupedRelays) {
|
|
1058
1161
|
logInfo('discovery', 'Processing relay', {port: relay.port, tabUrl: relay.data.tabUrl});
|
|
1059
1162
|
debugLog('Processing new relay:', relay.port, relay.data.tabUrl);
|
|
1060
1163
|
let ok = false;
|
|
@@ -1104,9 +1207,9 @@ async function autoOpenConnectUi() {
|
|
|
1104
1207
|
return result;
|
|
1105
1208
|
}
|
|
1106
1209
|
|
|
1107
|
-
// Discovery is now passive - only triggered by
|
|
1210
|
+
// Discovery is now passive - only triggered by Chrome AI Bridge requests
|
|
1108
1211
|
// The extension no longer auto-opens tabs on install/startup
|
|
1109
|
-
//
|
|
1212
|
+
// サーバーからの明示的な接続要求時のみ動作する
|
|
1110
1213
|
|
|
1111
1214
|
// Clear any existing discovery alarms from previous sessions
|
|
1112
1215
|
// This prevents leftover alarms from auto-opening tabs
|
|
@@ -1170,21 +1273,38 @@ function scheduleDiscoveryTick(delayMs) {
|
|
|
1170
1273
|
}
|
|
1171
1274
|
|
|
1172
1275
|
isDiscoveryRunning = true;
|
|
1276
|
+
lastDiscoveryTickStartedAt = Date.now();
|
|
1277
|
+
lastDiscoveryError = null;
|
|
1278
|
+
lastDiscoverySummary = null;
|
|
1173
1279
|
try {
|
|
1174
1280
|
const result = await autoOpenConnectUi();
|
|
1281
|
+
lastDiscoverySummary = {
|
|
1282
|
+
...result,
|
|
1283
|
+
mode: discoveryMode,
|
|
1284
|
+
intervalMs: getDiscoveryIntervalMs(),
|
|
1285
|
+
};
|
|
1175
1286
|
updateDiscoveryMode(result);
|
|
1176
1287
|
} catch (error) {
|
|
1288
|
+
lastDiscoveryError =
|
|
1289
|
+
error && typeof error === 'object' && 'message' in error
|
|
1290
|
+
? String(error.message)
|
|
1291
|
+
: String(error);
|
|
1177
1292
|
logWarn('discovery', 'Discovery cycle failed', {
|
|
1178
|
-
error:
|
|
1293
|
+
error: lastDiscoveryError,
|
|
1179
1294
|
});
|
|
1180
1295
|
emptyDiscoveryStreak = 0;
|
|
1181
1296
|
setDiscoveryMode(DISCOVERY_MODE.FAST, 'cycle-error');
|
|
1182
1297
|
} finally {
|
|
1298
|
+
lastDiscoveryTickFinishedAt = Date.now();
|
|
1299
|
+
lastDiscoveryTickDurationMs =
|
|
1300
|
+
lastDiscoveryTickFinishedAt - lastDiscoveryTickStartedAt;
|
|
1183
1301
|
isDiscoveryRunning = false;
|
|
1184
|
-
ensureKeepAliveAlarm('discovery-cycle');
|
|
1185
1302
|
}
|
|
1186
1303
|
|
|
1187
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');
|
|
1188
1308
|
}, Math.max(0, delayMs));
|
|
1189
1309
|
}
|
|
1190
1310
|
|
|
@@ -1217,6 +1337,7 @@ function kickDiscovery(reason) {
|
|
|
1217
1337
|
|
|
1218
1338
|
chrome.alarms.onAlarm.addListener((alarm) => {
|
|
1219
1339
|
if (alarm.name === KEEPALIVE_ALARM) {
|
|
1340
|
+
lastKeepAliveAlarmAt = Date.now();
|
|
1220
1341
|
const {activeCount, pendingCount} = getConnectionCounts();
|
|
1221
1342
|
if (activeCount > 0 || pendingCount > 0) {
|
|
1222
1343
|
logDebug('keepalive', 'Alarm triggered', {activeCount, pendingCount});
|
|
@@ -1235,7 +1356,7 @@ chrome.alarms.onAlarm.addListener((alarm) => {
|
|
|
1235
1356
|
});
|
|
1236
1357
|
|
|
1237
1358
|
// Note: We no longer register an onAlarm listener for DISCOVERY_ALARM
|
|
1238
|
-
// The scheduleDiscovery function is only called on explicit
|
|
1359
|
+
// The scheduleDiscovery function is only called on explicit server requests
|
|
1239
1360
|
|
|
1240
1361
|
// Discovery auto-starts on Chrome startup
|
|
1241
1362
|
// connect.html only opens when user clicks the extension icon
|
|
@@ -1259,5 +1380,7 @@ chrome.runtime.onStartup.addListener(() => {
|
|
|
1259
1380
|
scheduleDiscovery();
|
|
1260
1381
|
});
|
|
1261
1382
|
scheduleDiscovery(); // Start immediately
|
|
1383
|
+
// Ensure keepAlive is active from the start to prevent SW termination during discovery
|
|
1384
|
+
ensureKeepAliveAlarm('startup');
|
|
1262
1385
|
|
|
1263
1386
|
logInfo('background', 'Extension loaded (discovery active)');
|
|
@@ -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": {
|