@pyrokine/mcp-chrome 2.3.2 → 2.4.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 +40 -13
- package/README_zh.md +30 -11
- package/dist/anti-detection/behavior.d.ts.map +1 -1
- package/dist/anti-detection/behavior.js.map +1 -1
- package/dist/anti-detection/injection.js.map +1 -1
- package/dist/cdp/client.d.ts.map +1 -1
- package/dist/cdp/client.js.map +1 -1
- package/dist/cdp/launcher.d.ts.map +1 -1
- package/dist/cdp/launcher.js.map +1 -1
- package/dist/core/auto-wait.d.ts.map +1 -1
- package/dist/core/auto-wait.js.map +1 -1
- package/dist/core/browser-driver.d.ts +6 -1
- package/dist/core/browser-driver.d.ts.map +1 -1
- package/dist/core/browser-driver.js.map +1 -1
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js.map +1 -1
- package/dist/core/extension-errors.d.ts.map +1 -1
- package/dist/core/locator.d.ts.map +1 -1
- package/dist/core/locator.js.map +1 -1
- package/dist/core/retry.js.map +1 -1
- package/dist/core/session.d.ts +2 -2
- package/dist/core/session.d.ts.map +1 -1
- package/dist/core/session.js +1 -1
- package/dist/core/session.js.map +1 -1
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/core/unified-session.d.ts +17 -2
- package/dist/core/unified-session.d.ts.map +1 -1
- package/dist/core/unified-session.js +44 -6
- package/dist/core/unified-session.js.map +1 -1
- package/dist/extension/bridge.d.ts +8 -2
- package/dist/extension/bridge.d.ts.map +1 -1
- package/dist/extension/bridge.js +5 -1
- package/dist/extension/bridge.js.map +1 -1
- package/dist/extension/http-server.d.ts +17 -0
- package/dist/extension/http-server.d.ts.map +1 -1
- package/dist/extension/http-server.js +63 -8
- package/dist/extension/http-server.js.map +1 -1
- package/dist/extension/index.d.ts +1 -0
- package/dist/extension/index.d.ts.map +1 -1
- package/dist/extension/index.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/tools/browse.d.ts.map +1 -1
- package/dist/tools/browse.js +13 -37
- package/dist/tools/browse.js.map +1 -1
- package/dist/tools/cookies.js.map +1 -1
- package/dist/tools/diagnostics.d.ts +31 -0
- package/dist/tools/diagnostics.d.ts.map +1 -0
- package/dist/tools/diagnostics.js +93 -0
- package/dist/tools/diagnostics.js.map +1 -0
- package/dist/tools/evaluate.d.ts +15 -0
- package/dist/tools/evaluate.d.ts.map +1 -1
- package/dist/tools/evaluate.js +136 -84
- package/dist/tools/evaluate.js.map +1 -1
- package/dist/tools/extract.d.ts.map +1 -1
- package/dist/tools/extract.js +36 -17
- package/dist/tools/extract.js.map +1 -1
- package/dist/tools/input.d.ts +6 -0
- package/dist/tools/input.d.ts.map +1 -1
- package/dist/tools/input.js +394 -248
- package/dist/tools/input.js.map +1 -1
- package/dist/tools/logs.d.ts +17 -0
- package/dist/tools/logs.d.ts.map +1 -1
- package/dist/tools/logs.js +41 -10
- package/dist/tools/logs.js.map +1 -1
- package/dist/tools/network-sanitizer.d.ts +15 -0
- package/dist/tools/network-sanitizer.d.ts.map +1 -0
- package/dist/tools/network-sanitizer.js +61 -0
- package/dist/tools/network-sanitizer.js.map +1 -0
- package/dist/tools/post-condition.d.ts +18 -1
- package/dist/tools/post-condition.d.ts.map +1 -1
- package/dist/tools/post-condition.js +52 -94
- package/dist/tools/post-condition.js.map +1 -1
- package/dist/tools/target-diagnostics.d.ts +31 -0
- package/dist/tools/target-diagnostics.d.ts.map +1 -0
- package/dist/tools/target-diagnostics.js +91 -0
- package/dist/tools/target-diagnostics.js.map +1 -0
- package/dist/tools/wait.d.ts.map +1 -1
- package/dist/tools/wait.js +183 -209
- package/dist/tools/wait.js.map +1 -1
- package/extension/dist/assets/content.ts-Cj_iiU2s.js +25 -0
- package/extension/dist/assets/{index.ts-CJXGbOcp.js → index.ts-GG6dPUPX.js} +569 -347
- package/extension/dist/manifest.json +3 -3
- package/extension/dist/service-worker-loader.js +1 -1
- package/package.json +14 -10
- package/extension/dist/assets/content.ts-pEASbjEL.js +0 -6
|
@@ -15,6 +15,272 @@ function isExpectedOperationError(err) {
|
|
|
15
15
|
return err instanceof ExpectedOperationError;
|
|
16
16
|
}
|
|
17
17
|
//#endregion
|
|
18
|
+
//#region src/background/tab-state.ts
|
|
19
|
+
/**
|
|
20
|
+
* Managed tab/window 状态与 storage 恢复。
|
|
21
|
+
*/
|
|
22
|
+
var STORAGE_KEY = "mcp_managed_tab_ids";
|
|
23
|
+
var mcpTabGroupId = null;
|
|
24
|
+
var mcpWindowId = null;
|
|
25
|
+
var managedTabIds = /* @__PURE__ */ new Set();
|
|
26
|
+
function getMcpTabGroupId() {
|
|
27
|
+
return mcpTabGroupId;
|
|
28
|
+
}
|
|
29
|
+
function getMcpWindowId() {
|
|
30
|
+
return mcpWindowId;
|
|
31
|
+
}
|
|
32
|
+
function setMcpTabGroupId(groupId) {
|
|
33
|
+
mcpTabGroupId = groupId;
|
|
34
|
+
}
|
|
35
|
+
function setMcpWindowId(windowId) {
|
|
36
|
+
mcpWindowId = windowId;
|
|
37
|
+
}
|
|
38
|
+
async function markManagedTab(tabId) {
|
|
39
|
+
managedTabIds.add(tabId);
|
|
40
|
+
await chrome.storage.local.set({ [STORAGE_KEY]: [...managedTabIds] });
|
|
41
|
+
}
|
|
42
|
+
async function unmarkManagedTab(tabId) {
|
|
43
|
+
managedTabIds.delete(tabId);
|
|
44
|
+
await chrome.storage.local.set({ [STORAGE_KEY]: [...managedTabIds] });
|
|
45
|
+
}
|
|
46
|
+
function isManagedTab(tab, context) {
|
|
47
|
+
if (tab.id !== void 0 && managedTabIds.has(tab.id)) return true;
|
|
48
|
+
return context.mcpTabGroupId !== null && tab.groupId === context.mcpTabGroupId;
|
|
49
|
+
}
|
|
50
|
+
/** Extension 启动时调用,从 storage 恢复 managed tab 集合 */
|
|
51
|
+
async function restoreManagedTabs() {
|
|
52
|
+
const result = await chrome.storage.local.get(STORAGE_KEY);
|
|
53
|
+
const stored = Array.isArray(result[STORAGE_KEY]) ? result[STORAGE_KEY] : [];
|
|
54
|
+
for (const id of stored) managedTabIds.add(id);
|
|
55
|
+
const tabs = await chrome.tabs.query({});
|
|
56
|
+
const existingIds = new Set(tabs.map((t) => t.id).filter((id) => id !== void 0));
|
|
57
|
+
for (const id of managedTabIds) if (!existingIds.has(id)) managedTabIds.delete(id);
|
|
58
|
+
await chrome.storage.local.set({ [STORAGE_KEY]: [...managedTabIds] });
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region src/background/action-utils.ts
|
|
62
|
+
function isRestrictedUrl(url) {
|
|
63
|
+
if (!url) return true;
|
|
64
|
+
return url === "about:blank" || url.startsWith("about:") || url.startsWith("chrome://") || url.startsWith("chrome-extension://");
|
|
65
|
+
}
|
|
66
|
+
async function assertScriptable(tabId) {
|
|
67
|
+
const tab = await chrome.tabs.get(tabId);
|
|
68
|
+
if (isRestrictedUrl(tab.url)) throw new ExpectedOperationError(`Cannot execute on restricted URL "${tab.url}". Navigate to an http/https page first.`);
|
|
69
|
+
}
|
|
70
|
+
async function getTargetTabId(tabId) {
|
|
71
|
+
if (tabId !== void 0 && tabId !== null) {
|
|
72
|
+
try {
|
|
73
|
+
await chrome.tabs.get(tabId);
|
|
74
|
+
} catch {
|
|
75
|
+
throw new ExpectedOperationError(`Tab ${tabId} 不存在(可能已被关闭)`);
|
|
76
|
+
}
|
|
77
|
+
return tabId;
|
|
78
|
+
}
|
|
79
|
+
const [activeTab] = await chrome.tabs.query({
|
|
80
|
+
active: true,
|
|
81
|
+
currentWindow: true
|
|
82
|
+
});
|
|
83
|
+
if (!activeTab?.id) throw new ExpectedOperationError("No active tab found");
|
|
84
|
+
return activeTab.id;
|
|
85
|
+
}
|
|
86
|
+
function tabToInfo(tab, context) {
|
|
87
|
+
return {
|
|
88
|
+
id: tab.id,
|
|
89
|
+
url: tab.url || "",
|
|
90
|
+
title: tab.title || "",
|
|
91
|
+
active: tab.active,
|
|
92
|
+
windowId: tab.windowId,
|
|
93
|
+
index: tab.index,
|
|
94
|
+
groupId: tab.groupId,
|
|
95
|
+
pinned: tab.pinned,
|
|
96
|
+
incognito: tab.incognito,
|
|
97
|
+
managed: isManagedTab(tab, context),
|
|
98
|
+
status: tab.status || "unknown"
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
async function assertManagedTab(tabId, context, operation) {
|
|
102
|
+
let tab;
|
|
103
|
+
try {
|
|
104
|
+
tab = await chrome.tabs.get(tabId);
|
|
105
|
+
} catch {
|
|
106
|
+
throw new ExpectedOperationError(`Tab ${tabId} 不存在(可能已被关闭)`);
|
|
107
|
+
}
|
|
108
|
+
if (!isManagedTab(tab, context)) throw new ExpectedOperationError(`${operation} 拒绝操作非托管 tab, ${JSON.stringify({
|
|
109
|
+
tabId,
|
|
110
|
+
managed: false,
|
|
111
|
+
groupId: tab.groupId,
|
|
112
|
+
mcpTabGroupId: context.mcpTabGroupId,
|
|
113
|
+
windowId: tab.windowId,
|
|
114
|
+
mcpWindowId: context.mcpWindowId
|
|
115
|
+
})}, 请使用 browse(action="list") 选择 managed=true 的受控 tab, 或用 manage(action="newPage") 创建受控页面`);
|
|
116
|
+
return tab;
|
|
117
|
+
}
|
|
118
|
+
var FRAME_PROBE_TIMEOUT_MS = 500;
|
|
119
|
+
var pendingDomFrameProbes = /* @__PURE__ */ new Map();
|
|
120
|
+
function recordDomFrameProbe(message, sender) {
|
|
121
|
+
const pending = pendingDomFrameProbes.get(message.token);
|
|
122
|
+
if (!pending || sender.tab?.id !== pending.tabId || sender.frameId === void 0 || sender.frameId === 0 || !Number.isInteger(message.index) || message.index < 0) return { accepted: false };
|
|
123
|
+
const frameIds = pending.frameIdsByIndex.get(message.index) ?? [];
|
|
124
|
+
if (!frameIds.includes(sender.frameId)) {
|
|
125
|
+
frameIds.push(sender.frameId);
|
|
126
|
+
pending.frameIdsByIndex.set(message.index, frameIds);
|
|
127
|
+
}
|
|
128
|
+
return { accepted: true };
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* 将父文档里的 DOM iframe 与 Chrome frameId 建立可验证映射
|
|
132
|
+
*
|
|
133
|
+
* webNavigation 可能包含页面 DOM 不可见的其他 Extension frame,因此不能按两个列表的顺序配对
|
|
134
|
+
* 这里向每个 DOM iframe 的 contentWindow 发送一次随机 probe,再从对应 content script 的 sender 读取实际 frameId
|
|
135
|
+
*/
|
|
136
|
+
async function getDomFrameSnapshot(tabId, parentFrameId, selection) {
|
|
137
|
+
const token = crypto.randomUUID();
|
|
138
|
+
const pendingProbe = {
|
|
139
|
+
tabId,
|
|
140
|
+
frameIdsByIndex: /* @__PURE__ */ new Map()
|
|
141
|
+
};
|
|
142
|
+
pendingDomFrameProbes.set(token, pendingProbe);
|
|
143
|
+
try {
|
|
144
|
+
const parentResult = (await chrome.scripting.executeScript({
|
|
145
|
+
target: {
|
|
146
|
+
tabId,
|
|
147
|
+
frameIds: [parentFrameId]
|
|
148
|
+
},
|
|
149
|
+
func: async (probeToken, requestedSelection, timeoutMs) => {
|
|
150
|
+
const iframes = Array.from(document.querySelectorAll("iframe, frame"));
|
|
151
|
+
let selectedIndex = null;
|
|
152
|
+
let selectionStatus = "not-requested";
|
|
153
|
+
let selectionError;
|
|
154
|
+
if (typeof requestedSelection === "number") if (requestedSelection < 0 || requestedSelection >= iframes.length) selectionStatus = "out-of-range";
|
|
155
|
+
else {
|
|
156
|
+
selectedIndex = requestedSelection;
|
|
157
|
+
selectionStatus = "found";
|
|
158
|
+
}
|
|
159
|
+
else if (typeof requestedSelection === "string") try {
|
|
160
|
+
const selected = document.querySelector(requestedSelection);
|
|
161
|
+
if (!selected) selectionStatus = "not-found";
|
|
162
|
+
else if (!selected.matches("iframe, frame")) selectionStatus = "not-frame";
|
|
163
|
+
else {
|
|
164
|
+
selectedIndex = iframes.indexOf(selected);
|
|
165
|
+
selectionStatus = selectedIndex >= 0 ? "found" : "not-found";
|
|
166
|
+
}
|
|
167
|
+
} catch (error) {
|
|
168
|
+
selectionStatus = "invalid-selector";
|
|
169
|
+
selectionError = (error instanceof Error ? error.message : String(error)).slice(0, 300);
|
|
170
|
+
}
|
|
171
|
+
const acknowledged = await Promise.all(iframes.map((frame, index) => new Promise((resolve) => {
|
|
172
|
+
const child = frame.contentWindow;
|
|
173
|
+
if (!child) {
|
|
174
|
+
resolve(false);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
let settled = false;
|
|
178
|
+
const finish = (value) => {
|
|
179
|
+
if (settled) return;
|
|
180
|
+
settled = true;
|
|
181
|
+
window.clearTimeout(timer);
|
|
182
|
+
window.removeEventListener("message", onMessage);
|
|
183
|
+
resolve(value);
|
|
184
|
+
};
|
|
185
|
+
const onMessage = (event) => {
|
|
186
|
+
const data = event.data;
|
|
187
|
+
if (event.source === child && data?.type === "mcp-frame-probe-ack" && data.token === probeToken && data.index === index) finish(true);
|
|
188
|
+
};
|
|
189
|
+
const timer = window.setTimeout(() => finish(false), timeoutMs);
|
|
190
|
+
window.addEventListener("message", onMessage);
|
|
191
|
+
child.postMessage({
|
|
192
|
+
type: "mcp-frame-probe",
|
|
193
|
+
token: probeToken,
|
|
194
|
+
index
|
|
195
|
+
}, "*");
|
|
196
|
+
})));
|
|
197
|
+
return {
|
|
198
|
+
parent: {
|
|
199
|
+
title: document.title || null,
|
|
200
|
+
rect: {
|
|
201
|
+
x: 0,
|
|
202
|
+
y: 0,
|
|
203
|
+
width: window.innerWidth,
|
|
204
|
+
height: window.innerHeight
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
frames: iframes.map((frame, index) => {
|
|
208
|
+
const rect = frame.getBoundingClientRect();
|
|
209
|
+
return {
|
|
210
|
+
index,
|
|
211
|
+
acknowledged: acknowledged[index],
|
|
212
|
+
url: frame.src || "",
|
|
213
|
+
title: frame.title || null,
|
|
214
|
+
name: frame.name || null,
|
|
215
|
+
selector: frame.id ? `#${CSS.escape(frame.id)}` : `${frame.tagName.toLowerCase()}:nth-of-type(${index + 1})`,
|
|
216
|
+
rect: {
|
|
217
|
+
x: rect.x,
|
|
218
|
+
y: rect.y,
|
|
219
|
+
width: rect.width,
|
|
220
|
+
height: rect.height
|
|
221
|
+
},
|
|
222
|
+
contentOffset: {
|
|
223
|
+
x: rect.x + frame.clientLeft,
|
|
224
|
+
y: rect.y + frame.clientTop
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
}),
|
|
228
|
+
selectedIndex,
|
|
229
|
+
selectionStatus,
|
|
230
|
+
selectionError
|
|
231
|
+
};
|
|
232
|
+
},
|
|
233
|
+
args: [
|
|
234
|
+
token,
|
|
235
|
+
selection ?? null,
|
|
236
|
+
FRAME_PROBE_TIMEOUT_MS
|
|
237
|
+
]
|
|
238
|
+
}))[0]?.result;
|
|
239
|
+
if (!parentResult) throw new ExpectedOperationError(`无法读取 parent frame ${parentFrameId} 的 iframe DOM`);
|
|
240
|
+
return {
|
|
241
|
+
parent: parentResult.parent,
|
|
242
|
+
frames: parentResult.frames.map(({ acknowledged, ...frame }) => {
|
|
243
|
+
const candidateFrameIds = acknowledged ? pendingProbe.frameIdsByIndex.get(frame.index) ?? [] : [];
|
|
244
|
+
return {
|
|
245
|
+
...frame,
|
|
246
|
+
frameId: candidateFrameIds.length === 1 ? candidateFrameIds[0] : null,
|
|
247
|
+
candidateFrameIds: candidateFrameIds.slice(0, 10)
|
|
248
|
+
};
|
|
249
|
+
}),
|
|
250
|
+
selectedIndex: parentResult.selectedIndex,
|
|
251
|
+
selectionStatus: parentResult.selectionStatus,
|
|
252
|
+
...parentResult.selectionError ? { selectionError: parentResult.selectionError } : {}
|
|
253
|
+
};
|
|
254
|
+
} finally {
|
|
255
|
+
pendingDomFrameProbes.delete(token);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* 递归计算 iframe 在页面坐标系中的偏移量
|
|
260
|
+
*
|
|
261
|
+
* 对于嵌套 iframe,递归累加各层偏移,clientLeft/clientTop 补偿 iframe border
|
|
262
|
+
*/
|
|
263
|
+
async function getFrameOffset(tabId, frameId) {
|
|
264
|
+
const targetFrame = (await chrome.webNavigation.getAllFrames({ tabId }))?.find((frame) => frame.frameId === frameId);
|
|
265
|
+
if (!targetFrame || targetFrame.parentFrameId === -1) return null;
|
|
266
|
+
if (isRestrictedUrl((await chrome.tabs.get(tabId)).url)) return null;
|
|
267
|
+
let parentOffset = {
|
|
268
|
+
x: 0,
|
|
269
|
+
y: 0
|
|
270
|
+
};
|
|
271
|
+
if (targetFrame.parentFrameId !== 0) {
|
|
272
|
+
const resolvedParentOffset = await getFrameOffset(tabId, targetFrame.parentFrameId);
|
|
273
|
+
if (!resolvedParentOffset) return null;
|
|
274
|
+
parentOffset = resolvedParentOffset;
|
|
275
|
+
}
|
|
276
|
+
const mappedFrame = (await getDomFrameSnapshot(tabId, targetFrame.parentFrameId)).frames.find((frame) => frame.frameId === frameId);
|
|
277
|
+
if (!mappedFrame) return null;
|
|
278
|
+
return {
|
|
279
|
+
x: parentOffset.x + mappedFrame.contentOffset.x,
|
|
280
|
+
y: parentOffset.y + mappedFrame.contentOffset.y
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
//#endregion
|
|
18
284
|
//#region node_modules/zod/v4/core/core.js
|
|
19
285
|
var _a$1;
|
|
20
286
|
function $constructor(name, initializer, params) {
|
|
@@ -136,7 +402,10 @@ function assignProp(target, prop, value) {
|
|
|
136
402
|
}
|
|
137
403
|
function mergeDefs(...defs) {
|
|
138
404
|
const mergedDescriptors = {};
|
|
139
|
-
for (const def of defs)
|
|
405
|
+
for (const def of defs) {
|
|
406
|
+
const descriptors = Object.getOwnPropertyDescriptors(def);
|
|
407
|
+
Object.assign(mergedDescriptors, descriptors);
|
|
408
|
+
}
|
|
140
409
|
return Object.defineProperties({}, mergedDescriptors);
|
|
141
410
|
}
|
|
142
411
|
function esc(str) {
|
|
@@ -458,7 +727,7 @@ var _parse = (_Err) => (schema, value, _ctx, _params) => {
|
|
|
458
727
|
}, ctx);
|
|
459
728
|
if (result instanceof Promise) throw new $ZodAsyncError();
|
|
460
729
|
if (result.issues.length) {
|
|
461
|
-
const e = new (_params?.Err ?? _Err)(result.issues.map((iss) => finalizeIssue(iss, ctx, config())));
|
|
730
|
+
const e = new ((_params?.Err) ?? _Err)(result.issues.map((iss) => finalizeIssue(iss, ctx, config())));
|
|
462
731
|
captureStackTrace(e, _params?.callee);
|
|
463
732
|
throw e;
|
|
464
733
|
}
|
|
@@ -475,7 +744,7 @@ var _parseAsync = (_Err) => async (schema, value, _ctx, params) => {
|
|
|
475
744
|
}, ctx);
|
|
476
745
|
if (result instanceof Promise) result = await result;
|
|
477
746
|
if (result.issues.length) {
|
|
478
|
-
const e = new (params?.Err ?? _Err)(result.issues.map((iss) => finalizeIssue(iss, ctx, config())));
|
|
747
|
+
const e = new ((params?.Err) ?? _Err)(result.issues.map((iss) => finalizeIssue(iss, ctx, config())));
|
|
479
748
|
captureStackTrace(e, params?.callee);
|
|
480
749
|
throw e;
|
|
481
750
|
}
|
|
@@ -1567,13 +1836,13 @@ var $ZodObject = /*@__PURE__*/ $constructor("$ZodObject", (inst, def) => {
|
|
|
1567
1836
|
}
|
|
1568
1837
|
return propValues;
|
|
1569
1838
|
});
|
|
1570
|
-
const isObject$
|
|
1839
|
+
const isObject$2 = isObject;
|
|
1571
1840
|
const catchall = def.catchall;
|
|
1572
1841
|
let value;
|
|
1573
1842
|
inst._zod.parse = (payload, ctx) => {
|
|
1574
1843
|
value ?? (value = _normalized.value);
|
|
1575
1844
|
const input = payload.value;
|
|
1576
|
-
if (!isObject$
|
|
1845
|
+
if (!isObject$2(input)) {
|
|
1577
1846
|
payload.issues.push({
|
|
1578
1847
|
expected: "object",
|
|
1579
1848
|
code: "invalid_type",
|
|
@@ -1696,7 +1965,7 @@ var $ZodObjectJIT = /*@__PURE__*/ $constructor("$ZodObjectJIT", (inst, def) => {
|
|
|
1696
1965
|
return (payload, ctx) => fn(shape, payload, ctx);
|
|
1697
1966
|
};
|
|
1698
1967
|
let fastpass;
|
|
1699
|
-
const isObject$
|
|
1968
|
+
const isObject$1 = isObject;
|
|
1700
1969
|
const jit = !globalConfig.jitless;
|
|
1701
1970
|
const fastEnabled = jit && allowsEval.value;
|
|
1702
1971
|
const catchall = def.catchall;
|
|
@@ -1704,7 +1973,7 @@ var $ZodObjectJIT = /*@__PURE__*/ $constructor("$ZodObjectJIT", (inst, def) => {
|
|
|
1704
1973
|
inst._zod.parse = (payload, ctx) => {
|
|
1705
1974
|
value ?? (value = _normalized.value);
|
|
1706
1975
|
const input = payload.value;
|
|
1707
|
-
if (!isObject$
|
|
1976
|
+
if (!isObject$1(input)) {
|
|
1708
1977
|
payload.issues.push({
|
|
1709
1978
|
expected: "object",
|
|
1710
1979
|
code: "invalid_type",
|
|
@@ -2032,7 +2301,7 @@ var $ZodOptional = /*@__PURE__*/ $constructor("$ZodOptional", (inst, def) => {
|
|
|
2032
2301
|
inst._zod.optin = "optional";
|
|
2033
2302
|
inst._zod.optout = "optional";
|
|
2034
2303
|
defineLazy(inst._zod, "values", () => {
|
|
2035
|
-
return def.innerType._zod.values ? new Set([...def.innerType._zod.values, void 0]) : void 0;
|
|
2304
|
+
return def.innerType._zod.values ? /* @__PURE__ */ new Set([...def.innerType._zod.values, void 0]) : void 0;
|
|
2036
2305
|
});
|
|
2037
2306
|
defineLazy(inst._zod, "pattern", () => {
|
|
2038
2307
|
const pattern = def.innerType._zod.pattern;
|
|
@@ -2066,7 +2335,7 @@ var $ZodNullable = /*@__PURE__*/ $constructor("$ZodNullable", (inst, def) => {
|
|
|
2066
2335
|
return pattern ? new RegExp(`^(${cleanRegex(pattern.source)}|null)$`) : void 0;
|
|
2067
2336
|
});
|
|
2068
2337
|
defineLazy(inst._zod, "values", () => {
|
|
2069
|
-
return def.innerType._zod.values ? new Set([...def.innerType._zod.values, null]) : void 0;
|
|
2338
|
+
return def.innerType._zod.values ? /* @__PURE__ */ new Set([...def.innerType._zod.values, null]) : void 0;
|
|
2070
2339
|
});
|
|
2071
2340
|
inst._zod.parse = (payload, ctx) => {
|
|
2072
2341
|
if (payload.value === null) return payload;
|
|
@@ -4564,172 +4833,10 @@ var EvaluateInFrameSchema = object({
|
|
|
4564
4833
|
tabId: tabIdOpt,
|
|
4565
4834
|
frameId: number().int().nonnegative(),
|
|
4566
4835
|
expression: string().min(1),
|
|
4567
|
-
timeout: number().nonnegative().optional()
|
|
4836
|
+
timeout: number().nonnegative().optional(),
|
|
4837
|
+
staleContextRetry: _enum(["never", "readOnly"]).default("never")
|
|
4568
4838
|
});
|
|
4569
4839
|
//#endregion
|
|
4570
|
-
//#region src/background/tab-state.ts
|
|
4571
|
-
/**
|
|
4572
|
-
* Managed tab/window 状态与 storage 恢复。
|
|
4573
|
-
*/
|
|
4574
|
-
var STORAGE_KEY = "mcp_managed_tab_ids";
|
|
4575
|
-
var mcpTabGroupId = null;
|
|
4576
|
-
var mcpWindowId = null;
|
|
4577
|
-
var managedTabIds = /* @__PURE__ */ new Set();
|
|
4578
|
-
function getMcpTabGroupId() {
|
|
4579
|
-
return mcpTabGroupId;
|
|
4580
|
-
}
|
|
4581
|
-
function getMcpWindowId() {
|
|
4582
|
-
return mcpWindowId;
|
|
4583
|
-
}
|
|
4584
|
-
function setMcpTabGroupId(groupId) {
|
|
4585
|
-
mcpTabGroupId = groupId;
|
|
4586
|
-
}
|
|
4587
|
-
function setMcpWindowId(windowId) {
|
|
4588
|
-
mcpWindowId = windowId;
|
|
4589
|
-
}
|
|
4590
|
-
async function markManagedTab(tabId) {
|
|
4591
|
-
managedTabIds.add(tabId);
|
|
4592
|
-
await chrome.storage.local.set({ [STORAGE_KEY]: [...managedTabIds] });
|
|
4593
|
-
}
|
|
4594
|
-
async function unmarkManagedTab(tabId) {
|
|
4595
|
-
managedTabIds.delete(tabId);
|
|
4596
|
-
await chrome.storage.local.set({ [STORAGE_KEY]: [...managedTabIds] });
|
|
4597
|
-
}
|
|
4598
|
-
function isManagedTab(tab, context) {
|
|
4599
|
-
if (tab.id !== void 0 && managedTabIds.has(tab.id)) return true;
|
|
4600
|
-
return context.mcpTabGroupId !== null && tab.groupId === context.mcpTabGroupId;
|
|
4601
|
-
}
|
|
4602
|
-
/** Extension 启动时调用,从 storage 恢复 managed tab 集合 */
|
|
4603
|
-
async function restoreManagedTabs() {
|
|
4604
|
-
const result = await chrome.storage.local.get(STORAGE_KEY);
|
|
4605
|
-
const stored = Array.isArray(result[STORAGE_KEY]) ? result[STORAGE_KEY] : [];
|
|
4606
|
-
for (const id of stored) managedTabIds.add(id);
|
|
4607
|
-
const tabs = await chrome.tabs.query({});
|
|
4608
|
-
const existingIds = new Set(tabs.map((t) => t.id).filter((id) => id !== void 0));
|
|
4609
|
-
for (const id of managedTabIds) if (!existingIds.has(id)) managedTabIds.delete(id);
|
|
4610
|
-
await chrome.storage.local.set({ [STORAGE_KEY]: [...managedTabIds] });
|
|
4611
|
-
}
|
|
4612
|
-
//#endregion
|
|
4613
|
-
//#region src/background/action-utils.ts
|
|
4614
|
-
/**
|
|
4615
|
-
* Action 处理器共享工具函数
|
|
4616
|
-
*
|
|
4617
|
-
* 纯工具函数,不依赖任何实例状态,可被多个 handler 模块直接 import 使用
|
|
4618
|
-
*/
|
|
4619
|
-
function isRestrictedUrl(url) {
|
|
4620
|
-
if (!url) return true;
|
|
4621
|
-
return url === "about:blank" || url.startsWith("about:") || url.startsWith("chrome://") || url.startsWith("chrome-extension://");
|
|
4622
|
-
}
|
|
4623
|
-
async function assertScriptable(tabId) {
|
|
4624
|
-
const tab = await chrome.tabs.get(tabId);
|
|
4625
|
-
if (isRestrictedUrl(tab.url)) throw new ExpectedOperationError(`Cannot execute on restricted URL "${tab.url}". Navigate to an http/https page first.`);
|
|
4626
|
-
}
|
|
4627
|
-
async function getTargetTabId(tabId) {
|
|
4628
|
-
if (tabId !== void 0 && tabId !== null) {
|
|
4629
|
-
try {
|
|
4630
|
-
await chrome.tabs.get(tabId);
|
|
4631
|
-
} catch {
|
|
4632
|
-
throw new ExpectedOperationError(`Tab ${tabId} 不存在(可能已被关闭)`);
|
|
4633
|
-
}
|
|
4634
|
-
return tabId;
|
|
4635
|
-
}
|
|
4636
|
-
const [activeTab] = await chrome.tabs.query({
|
|
4637
|
-
active: true,
|
|
4638
|
-
currentWindow: true
|
|
4639
|
-
});
|
|
4640
|
-
if (!activeTab?.id) throw new ExpectedOperationError("No active tab found");
|
|
4641
|
-
return activeTab.id;
|
|
4642
|
-
}
|
|
4643
|
-
function tabToInfo(tab, context) {
|
|
4644
|
-
return {
|
|
4645
|
-
id: tab.id,
|
|
4646
|
-
url: tab.url || "",
|
|
4647
|
-
title: tab.title || "",
|
|
4648
|
-
active: tab.active,
|
|
4649
|
-
windowId: tab.windowId,
|
|
4650
|
-
index: tab.index,
|
|
4651
|
-
groupId: tab.groupId,
|
|
4652
|
-
pinned: tab.pinned,
|
|
4653
|
-
incognito: tab.incognito,
|
|
4654
|
-
managed: isManagedTab(tab, context),
|
|
4655
|
-
status: tab.status || "unknown"
|
|
4656
|
-
};
|
|
4657
|
-
}
|
|
4658
|
-
async function assertManagedTab(tabId, context, operation) {
|
|
4659
|
-
let tab;
|
|
4660
|
-
try {
|
|
4661
|
-
tab = await chrome.tabs.get(tabId);
|
|
4662
|
-
} catch {
|
|
4663
|
-
throw new ExpectedOperationError(`Tab ${tabId} 不存在(可能已被关闭)`);
|
|
4664
|
-
}
|
|
4665
|
-
if (!isManagedTab(tab, context)) throw new ExpectedOperationError(`${operation} 拒绝操作非托管 tab, ${JSON.stringify({
|
|
4666
|
-
tabId,
|
|
4667
|
-
managed: false,
|
|
4668
|
-
groupId: tab.groupId,
|
|
4669
|
-
mcpTabGroupId: context.mcpTabGroupId,
|
|
4670
|
-
windowId: tab.windowId,
|
|
4671
|
-
mcpWindowId: context.mcpWindowId
|
|
4672
|
-
})}, 请使用 browse(action="list") 选择 managed=true 的受控 tab, 或用 manage(action="newPage") 创建受控页面`);
|
|
4673
|
-
return tab;
|
|
4674
|
-
}
|
|
4675
|
-
/**
|
|
4676
|
-
* 递归计算 iframe 在页面坐标系中的偏移量
|
|
4677
|
-
*
|
|
4678
|
-
* 对于嵌套 iframe,递归累加各层偏移,
|
|
4679
|
-
* 当多个 iframe 共享同一 URL 时,优先按同 URL 的出现顺序匹配,
|
|
4680
|
-
* clientLeft/clientTop 补偿 iframe 的 border,确保坐标指向内容区域起点
|
|
4681
|
-
*/
|
|
4682
|
-
async function getFrameOffset(tabId, frameId) {
|
|
4683
|
-
const allFrames = await chrome.webNavigation.getAllFrames({ tabId });
|
|
4684
|
-
if (!allFrames) return null;
|
|
4685
|
-
const targetFrame = allFrames.find((f) => f.frameId === frameId);
|
|
4686
|
-
if (!targetFrame || targetFrame.parentFrameId === -1) return null;
|
|
4687
|
-
if (isRestrictedUrl((await chrome.tabs.get(tabId)).url)) return null;
|
|
4688
|
-
let parentOffset = {
|
|
4689
|
-
x: 0,
|
|
4690
|
-
y: 0
|
|
4691
|
-
};
|
|
4692
|
-
if (targetFrame.parentFrameId !== 0) {
|
|
4693
|
-
const po = await getFrameOffset(tabId, targetFrame.parentFrameId);
|
|
4694
|
-
if (po) parentOffset = po;
|
|
4695
|
-
}
|
|
4696
|
-
const siblingFrames = allFrames.filter((f) => f.parentFrameId === targetFrame.parentFrameId && f.frameId !== targetFrame.parentFrameId);
|
|
4697
|
-
const frameIndex = siblingFrames.findIndex((f) => f.frameId === frameId);
|
|
4698
|
-
const sameUrlIndex = siblingFrames.filter((f) => f.url === targetFrame.url).findIndex((f) => f.frameId === frameId);
|
|
4699
|
-
const localOffset = (await chrome.scripting.executeScript({
|
|
4700
|
-
target: {
|
|
4701
|
-
tabId,
|
|
4702
|
-
frameIds: [targetFrame.parentFrameId]
|
|
4703
|
-
},
|
|
4704
|
-
world: "MAIN",
|
|
4705
|
-
func: (frameUrl, frameIndex, sameUrlIndex) => {
|
|
4706
|
-
const iframes = Array.from(document.querySelectorAll("iframe, frame"));
|
|
4707
|
-
const urlMatches = frameUrl ? iframes.filter((iframe) => iframe.src === frameUrl) : [];
|
|
4708
|
-
let target;
|
|
4709
|
-
if (urlMatches.length === 1) target = urlMatches[0];
|
|
4710
|
-
else if (sameUrlIndex >= 0 && sameUrlIndex < urlMatches.length) target = urlMatches[sameUrlIndex];
|
|
4711
|
-
else if (frameIndex >= 0 && frameIndex < iframes.length) target = iframes[frameIndex];
|
|
4712
|
-
else if (urlMatches.length > 0) target = urlMatches[0];
|
|
4713
|
-
if (!target) return null;
|
|
4714
|
-
const rect = target.getBoundingClientRect();
|
|
4715
|
-
return {
|
|
4716
|
-
x: rect.x + target.clientLeft,
|
|
4717
|
-
y: rect.y + target.clientTop
|
|
4718
|
-
};
|
|
4719
|
-
},
|
|
4720
|
-
args: [
|
|
4721
|
-
targetFrame.url,
|
|
4722
|
-
frameIndex,
|
|
4723
|
-
sameUrlIndex
|
|
4724
|
-
]
|
|
4725
|
-
}))[0]?.result;
|
|
4726
|
-
if (!localOffset) return null;
|
|
4727
|
-
return {
|
|
4728
|
-
x: parentOffset.x + localOffset.x,
|
|
4729
|
-
y: parentOffset.y + localOffset.y
|
|
4730
|
-
};
|
|
4731
|
-
}
|
|
4732
|
-
//#endregion
|
|
4733
4840
|
//#region src/background/debugger-manager.ts
|
|
4734
4841
|
var DebuggerBlockedError = class extends ExpectedOperationError {
|
|
4735
4842
|
code = "DEBUGGER_BLOCKED";
|
|
@@ -4778,6 +4885,13 @@ var DebuggerManager = class {
|
|
|
4778
4885
|
getExecutionContexts(tabId) {
|
|
4779
4886
|
return this.executionContexts.get(tabId) ?? [];
|
|
4780
4887
|
}
|
|
4888
|
+
invalidateExecutionContext(tabId, contextId) {
|
|
4889
|
+
const contexts = this.executionContexts.get(tabId);
|
|
4890
|
+
if (!contexts) return;
|
|
4891
|
+
const next = contexts.filter((context) => context.id !== contextId);
|
|
4892
|
+
if (next.length > 0) this.executionContexts.set(tabId, next);
|
|
4893
|
+
else this.executionContexts.delete(tabId);
|
|
4894
|
+
}
|
|
4781
4895
|
setupListeners() {
|
|
4782
4896
|
if (this.listenersBound) return;
|
|
4783
4897
|
this.listenersBound = true;
|
|
@@ -5548,7 +5662,8 @@ async function simulateKeyboardType(text, delay) {
|
|
|
5548
5662
|
success: false,
|
|
5549
5663
|
error: `Active element is not inputable: ${activeElement.tagName}`
|
|
5550
5664
|
};
|
|
5551
|
-
const
|
|
5665
|
+
const proto = Object.getPrototypeOf(activeElement);
|
|
5666
|
+
const valueSetter = Object.getOwnPropertyDescriptor(proto, "value") ?? Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value") ?? Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, "value");
|
|
5552
5667
|
const setNativeValue = (val) => {
|
|
5553
5668
|
if (valueSetter?.set) valueSetter.set.call(activeElement, val);
|
|
5554
5669
|
else if ("value" in activeElement) activeElement.value = val;
|
|
@@ -5666,7 +5781,8 @@ function injectStealthScripts() {
|
|
|
5666
5781
|
configurable: true
|
|
5667
5782
|
});
|
|
5668
5783
|
const originalChrome = window.chrome;
|
|
5669
|
-
|
|
5784
|
+
const chromeDescriptor = Object.getOwnPropertyDescriptor(window, "chrome");
|
|
5785
|
+
if (originalChrome && chromeDescriptor?.configurable) Object.defineProperty(window, "chrome", {
|
|
5670
5786
|
get: () => {
|
|
5671
5787
|
const chrome = { ...originalChrome };
|
|
5672
5788
|
delete chrome.runtime;
|
|
@@ -6017,7 +6133,8 @@ function dispatchInputToElement(refId, val) {
|
|
|
6017
6133
|
error: `Element ${refId} not found`
|
|
6018
6134
|
};
|
|
6019
6135
|
el.focus();
|
|
6020
|
-
const
|
|
6136
|
+
const proto = Object.getPrototypeOf(el);
|
|
6137
|
+
const setter = Object.getOwnPropertyDescriptor(proto, "value") ?? Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value") ?? Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, "value");
|
|
6021
6138
|
if (setter?.set) setter.set.call(el, val);
|
|
6022
6139
|
else el.value = val;
|
|
6023
6140
|
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
@@ -6497,53 +6614,28 @@ var FrameResolver = class {
|
|
|
6497
6614
|
async resolveFrame(params, context) {
|
|
6498
6615
|
const p = ResolveFrameSchema.parse(params);
|
|
6499
6616
|
const tabId = await this.getManagedScriptableTabId(p.tabId, context, "resolve_frame");
|
|
6500
|
-
const
|
|
6501
|
-
if (
|
|
6502
|
-
|
|
6503
|
-
|
|
6504
|
-
|
|
6505
|
-
const info = (await chrome.scripting.executeScript({
|
|
6506
|
-
target: {
|
|
6507
|
-
tabId,
|
|
6508
|
-
frameIds: [0]
|
|
6509
|
-
},
|
|
6510
|
-
world: "MAIN",
|
|
6511
|
-
func: (frame) => {
|
|
6512
|
-
const iframes = Array.from(document.querySelectorAll("iframe, frame"));
|
|
6513
|
-
let target;
|
|
6514
|
-
let index = -1;
|
|
6515
|
-
if (typeof frame === "number") {
|
|
6516
|
-
if (frame < 0 || frame >= iframes.length) return null;
|
|
6517
|
-
target = iframes[frame];
|
|
6518
|
-
index = frame;
|
|
6519
|
-
} else {
|
|
6520
|
-
target = document.querySelector(frame);
|
|
6521
|
-
if (target) index = iframes.indexOf(target);
|
|
6522
|
-
}
|
|
6523
|
-
if (!target) return null;
|
|
6524
|
-
let absoluteSrc = target.src || "";
|
|
6525
|
-
if (absoluteSrc && !absoluteSrc.startsWith("http") && !absoluteSrc.startsWith("about:")) try {
|
|
6526
|
-
absoluteSrc = new URL(absoluteSrc, location.href).href;
|
|
6527
|
-
} catch {}
|
|
6528
|
-
return {
|
|
6529
|
-
src: absoluteSrc,
|
|
6530
|
-
index
|
|
6531
|
-
};
|
|
6532
|
-
},
|
|
6533
|
-
args: [p.frame]
|
|
6534
|
-
}))[0]?.result;
|
|
6535
|
-
if (!info) throw new ExpectedOperationError(`iframe not found: ${typeof p.frame === "number" ? `index ${p.frame}` : `selector "${p.frame}"`}`);
|
|
6536
|
-
let matchedFrameId;
|
|
6537
|
-
if (info.src) {
|
|
6538
|
-
const urlMatches = allChildFrames.filter((f) => f.url === info.src);
|
|
6539
|
-
if (urlMatches.length === 1) matchedFrameId = urlMatches[0].frameId;
|
|
6617
|
+
const snapshot = await getDomFrameSnapshot(tabId, 0, p.frame);
|
|
6618
|
+
if (snapshot.selectionStatus !== "found" || snapshot.selectedIndex === null) {
|
|
6619
|
+
const desc = typeof p.frame === "number" ? `index ${p.frame}` : `selector "${p.frame}"`;
|
|
6620
|
+
const detail = snapshot.selectionError ? `: ${snapshot.selectionError}` : "";
|
|
6621
|
+
throw new ExpectedOperationError(`iframe not found: ${desc} (${snapshot.selectionStatus})${detail}`);
|
|
6540
6622
|
}
|
|
6541
|
-
|
|
6542
|
-
if (
|
|
6543
|
-
|
|
6623
|
+
const selected = snapshot.frames.find((frame) => frame.index === snapshot.selectedIndex);
|
|
6624
|
+
if (!selected || selected.frameId === null) throw new ExpectedOperationError(JSON.stringify({ error: {
|
|
6625
|
+
code: "FRAME_IDENTITY_UNAVAILABLE",
|
|
6626
|
+
message: "Cannot map the selected DOM iframe to one Chrome frameId",
|
|
6627
|
+
suggestion: "请等待 iframe 完成加载后重试;工具不会按 webNavigation 列表顺序选择其他 frame",
|
|
6628
|
+
context: {
|
|
6629
|
+
tabId,
|
|
6630
|
+
selection: p.frame,
|
|
6631
|
+
domIndex: snapshot.selectedIndex,
|
|
6632
|
+
candidateFrameIds: selected?.candidateFrameIds ?? [],
|
|
6633
|
+
domFrameCount: snapshot.frames.length
|
|
6634
|
+
}
|
|
6635
|
+
} }));
|
|
6544
6636
|
return {
|
|
6545
|
-
frameId:
|
|
6546
|
-
offset
|
|
6637
|
+
frameId: selected.frameId,
|
|
6638
|
+
offset: selected.contentOffset
|
|
6547
6639
|
};
|
|
6548
6640
|
}
|
|
6549
6641
|
async getAllFrames(params, context) {
|
|
@@ -6551,80 +6643,20 @@ var FrameResolver = class {
|
|
|
6551
6643
|
const tabId = await this.getManagedScriptableTabId(p.tabId, context, "get_all_frames");
|
|
6552
6644
|
const frames = await chrome.webNavigation.getAllFrames({ tabId });
|
|
6553
6645
|
if (!frames) throw new Error("Failed to get frames");
|
|
6554
|
-
|
|
6555
|
-
|
|
6556
|
-
rect: {
|
|
6557
|
-
x: 0,
|
|
6558
|
-
y: 0,
|
|
6559
|
-
width: 0,
|
|
6560
|
-
height: 0
|
|
6561
|
-
}
|
|
6562
|
-
};
|
|
6563
|
-
let domFrames = [];
|
|
6564
|
-
try {
|
|
6565
|
-
const pageFrames = (await chrome.scripting.executeScript({
|
|
6566
|
-
target: {
|
|
6567
|
-
tabId,
|
|
6568
|
-
frameIds: [0]
|
|
6569
|
-
},
|
|
6570
|
-
world: "MAIN",
|
|
6571
|
-
func: () => ({
|
|
6572
|
-
main: {
|
|
6573
|
-
title: document.title || null,
|
|
6574
|
-
rect: {
|
|
6575
|
-
x: 0,
|
|
6576
|
-
y: 0,
|
|
6577
|
-
width: window.innerWidth,
|
|
6578
|
-
height: window.innerHeight
|
|
6579
|
-
}
|
|
6580
|
-
},
|
|
6581
|
-
frames: Array.from(document.querySelectorAll("iframe, frame")).map((frame, index) => {
|
|
6582
|
-
const el = frame;
|
|
6583
|
-
const rect = el.getBoundingClientRect();
|
|
6584
|
-
const selector = el.id ? `#${CSS.escape(el.id)}` : `${el.tagName.toLowerCase()}:nth-of-type(${index + 1})`;
|
|
6585
|
-
return {
|
|
6586
|
-
index,
|
|
6587
|
-
url: el.src || "",
|
|
6588
|
-
title: el.title || null,
|
|
6589
|
-
name: el.name || null,
|
|
6590
|
-
selector,
|
|
6591
|
-
rect: {
|
|
6592
|
-
x: rect.x,
|
|
6593
|
-
y: rect.y,
|
|
6594
|
-
width: rect.width,
|
|
6595
|
-
height: rect.height
|
|
6596
|
-
}
|
|
6597
|
-
};
|
|
6598
|
-
})
|
|
6599
|
-
})
|
|
6600
|
-
}))[0]?.result;
|
|
6601
|
-
mainFrameInfo = pageFrames?.main ?? mainFrameInfo;
|
|
6602
|
-
domFrames = pageFrames?.frames ?? [];
|
|
6603
|
-
} catch {
|
|
6604
|
-
domFrames = [];
|
|
6605
|
-
}
|
|
6606
|
-
const directChildFrames = frames.filter((frame) => frame.parentFrameId === 0 && frame.frameId !== 0);
|
|
6607
|
-
const getDomFrame = (frame) => {
|
|
6608
|
-
if (frame.parentFrameId !== 0) return;
|
|
6609
|
-
const directIndex = directChildFrames.findIndex((candidate) => candidate.frameId === frame.frameId);
|
|
6610
|
-
const byIndex = directIndex >= 0 ? domFrames[directIndex] : void 0;
|
|
6611
|
-
if (byIndex) return byIndex;
|
|
6612
|
-
const navigationMatches = directChildFrames.filter((candidate) => candidate.url === frame.url);
|
|
6613
|
-
const domMatches = domFrames.filter((candidate) => candidate.url === frame.url);
|
|
6614
|
-
return navigationMatches.length === 1 && domMatches.length === 1 ? domMatches[0] : void 0;
|
|
6615
|
-
};
|
|
6646
|
+
const snapshot = await getDomFrameSnapshot(tabId, 0);
|
|
6647
|
+
const domFramesByFrameId = new Map(snapshot.frames.filter((frame) => frame.frameId !== null).map((frame) => [frame.frameId, frame]));
|
|
6616
6648
|
return { frames: frames.map((frame, index) => {
|
|
6617
6649
|
const isMainFrame = frame.frameId === 0;
|
|
6618
|
-
const dom = isMainFrame ? void 0 :
|
|
6650
|
+
const dom = isMainFrame ? void 0 : domFramesByFrameId.get(frame.frameId);
|
|
6619
6651
|
return {
|
|
6620
6652
|
index,
|
|
6621
6653
|
frameId: frame.frameId,
|
|
6622
6654
|
parentFrameId: isMainFrame ? null : frame.parentFrameId,
|
|
6623
6655
|
url: frame.url,
|
|
6624
|
-
title: isMainFrame ?
|
|
6656
|
+
title: isMainFrame ? snapshot.parent.title : dom?.title ?? null,
|
|
6625
6657
|
name: isMainFrame ? null : dom?.name ?? null,
|
|
6626
6658
|
selector: isMainFrame ? null : dom?.selector ?? null,
|
|
6627
|
-
rect: isMainFrame ?
|
|
6659
|
+
rect: isMainFrame ? snapshot.parent.rect : dom?.rect ?? null
|
|
6628
6660
|
};
|
|
6629
6661
|
}) };
|
|
6630
6662
|
}
|
|
@@ -6636,40 +6668,154 @@ var FrameResolver = class {
|
|
|
6636
6668
|
*/
|
|
6637
6669
|
async evaluateInFrame(params, context) {
|
|
6638
6670
|
const p = EvaluateInFrameSchema.parse(params);
|
|
6639
|
-
const
|
|
6640
|
-
const awaitPromise = params?.awaitPromise;
|
|
6671
|
+
const options = params;
|
|
6641
6672
|
const tabId = await this.getManagedScriptableTabId(p.tabId, context, "evaluate_in_frame");
|
|
6642
6673
|
await this.debuggerManager.ensureAttached(tabId);
|
|
6643
|
-
const
|
|
6644
|
-
|
|
6645
|
-
|
|
6646
|
-
|
|
6647
|
-
const
|
|
6648
|
-
|
|
6649
|
-
|
|
6650
|
-
|
|
6651
|
-
|
|
6652
|
-
|
|
6653
|
-
|
|
6654
|
-
|
|
6655
|
-
|
|
6656
|
-
|
|
6657
|
-
|
|
6658
|
-
|
|
6674
|
+
const startedAt = Date.now();
|
|
6675
|
+
const originalFrame = await this.getExtensionFrameIdentity(tabId, p.frameId);
|
|
6676
|
+
let retryAttempted = false;
|
|
6677
|
+
let retryReason;
|
|
6678
|
+
const maxAttempts = p.staleContextRetry === "readOnly" ? 2 : 1;
|
|
6679
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
6680
|
+
const remaining = p.timeout === void 0 ? void 0 : p.timeout - (Date.now() - startedAt);
|
|
6681
|
+
if (remaining !== void 0 && remaining <= 0) throw this.frameEvaluationError("FRAME_EVALUATION_TIMEOUT", "Frame evaluation timed out before resolution", {
|
|
6682
|
+
tabId,
|
|
6683
|
+
extensionFrameId: p.frameId,
|
|
6684
|
+
timeout: p.timeout,
|
|
6685
|
+
retryAttempted,
|
|
6686
|
+
retryReason
|
|
6687
|
+
});
|
|
6688
|
+
const identity = await this.getExtensionFrameIdentity(tabId, p.frameId);
|
|
6689
|
+
if (identity.parentFrameId !== originalFrame.parentFrameId || identity.frameId !== originalFrame.frameId) throw this.frameEvaluationError("FRAME_IDENTITY_CHANGED", "Frame identity changed before stale-context retry; refusing to guess target", {
|
|
6690
|
+
tabId,
|
|
6691
|
+
retryAttempted,
|
|
6692
|
+
retryReason,
|
|
6693
|
+
originalFrame: this.frameIdentitySummary(originalFrame),
|
|
6694
|
+
currentFrame: this.frameIdentitySummary(identity)
|
|
6695
|
+
});
|
|
6696
|
+
const resolved = await this.resolveCdpFrame(tabId, identity.url);
|
|
6697
|
+
let contexts = this.debuggerManager.getExecutionContexts(tabId);
|
|
6698
|
+
if (contexts.length === 0 || attempt > 0) await chrome.debugger.sendCommand({ tabId }, "Runtime.enable");
|
|
6699
|
+
let targetCtx = contexts.find((item) => item.frameId === resolved.cdpFrameId && item.isDefault);
|
|
6700
|
+
const deadline = p.timeout === void 0 ? Date.now() + 2e3 : startedAt + p.timeout;
|
|
6701
|
+
while (!targetCtx && Date.now() < deadline) {
|
|
6702
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(100, Math.max(1, deadline - Date.now()))));
|
|
6659
6703
|
contexts = this.debuggerManager.getExecutionContexts(tabId);
|
|
6660
|
-
targetCtx = contexts.find((
|
|
6661
|
-
|
|
6704
|
+
targetCtx = contexts.find((item) => item.frameId === resolved.cdpFrameId && item.isDefault);
|
|
6705
|
+
}
|
|
6706
|
+
if (!targetCtx) throw this.frameEvaluationError("FRAME_CONTEXT_UNAVAILABLE", "No execution context for frame", {
|
|
6707
|
+
tabId,
|
|
6708
|
+
...this.frameIdentitySummary(identity),
|
|
6709
|
+
cdpFrameId: resolved.cdpFrameId,
|
|
6710
|
+
contextCount: contexts.length,
|
|
6711
|
+
retryAttempted,
|
|
6712
|
+
retryReason
|
|
6713
|
+
});
|
|
6714
|
+
const evaluationTimeout = p.timeout === void 0 ? void 0 : p.timeout - (Date.now() - startedAt);
|
|
6715
|
+
if (evaluationTimeout !== void 0 && evaluationTimeout <= 0) throw this.frameEvaluationError("FRAME_EVALUATION_TIMEOUT", "Frame evaluation timed out before Runtime.evaluate", {
|
|
6716
|
+
tabId,
|
|
6717
|
+
...this.frameIdentitySummary(identity),
|
|
6718
|
+
cdpFrameId: resolved.cdpFrameId,
|
|
6719
|
+
executionContextId: targetCtx.id,
|
|
6720
|
+
timeout: p.timeout,
|
|
6721
|
+
retryAttempted,
|
|
6722
|
+
retryReason
|
|
6723
|
+
});
|
|
6724
|
+
const evalParams = {
|
|
6725
|
+
contextId: targetCtx.id,
|
|
6726
|
+
expression: p.expression,
|
|
6727
|
+
returnByValue: options.returnByValue ?? true,
|
|
6728
|
+
awaitPromise: options.awaitPromise ?? true,
|
|
6729
|
+
...evaluationTimeout !== void 0 ? { timeout: evaluationTimeout } : {}
|
|
6730
|
+
};
|
|
6731
|
+
try {
|
|
6732
|
+
return {
|
|
6733
|
+
...await chrome.debugger.sendCommand({ tabId }, "Runtime.evaluate", evalParams),
|
|
6734
|
+
retryAttempted,
|
|
6735
|
+
retryReason,
|
|
6736
|
+
staleContextRetry: p.staleContextRetry,
|
|
6737
|
+
frameContext: {
|
|
6738
|
+
tabId,
|
|
6739
|
+
extensionFrameId: identity.frameId,
|
|
6740
|
+
parentFrameId: identity.parentFrameId,
|
|
6741
|
+
url: identity.url.slice(0, 500),
|
|
6742
|
+
originalUrl: originalFrame.url.slice(0, 500),
|
|
6743
|
+
urlChanged: identity.url !== originalFrame.url,
|
|
6744
|
+
cdpFrameId: resolved.cdpFrameId,
|
|
6745
|
+
executionContextId: targetCtx.id
|
|
6746
|
+
}
|
|
6747
|
+
};
|
|
6748
|
+
} catch (error) {
|
|
6749
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
6750
|
+
const staleContext = /context.*(?:destroyed|not found|invalid)|Cannot find context|Execution context/i.test(message);
|
|
6751
|
+
if (attempt > 0) throw this.frameEvaluationError("FRAME_EVALUATION_FAILED", message.slice(0, 500), {
|
|
6752
|
+
tabId,
|
|
6753
|
+
...this.frameIdentitySummary(identity),
|
|
6754
|
+
cdpFrameId: resolved.cdpFrameId,
|
|
6755
|
+
executionContextId: targetCtx.id,
|
|
6756
|
+
retryAttempted,
|
|
6757
|
+
retryReason
|
|
6758
|
+
});
|
|
6759
|
+
if (!staleContext) throw error;
|
|
6760
|
+
if (p.staleContextRetry !== "readOnly") throw this.frameEvaluationError("FRAME_STALE_CONTEXT", "Execution context became stale; script was not replayed because staleContextRetry is never", {
|
|
6761
|
+
tabId,
|
|
6762
|
+
...this.frameIdentitySummary(identity),
|
|
6763
|
+
cdpFrameId: resolved.cdpFrameId,
|
|
6764
|
+
executionContextId: targetCtx.id,
|
|
6765
|
+
retryAttempted: false,
|
|
6766
|
+
staleContextRetry: p.staleContextRetry
|
|
6767
|
+
});
|
|
6768
|
+
retryAttempted = true;
|
|
6769
|
+
retryReason = message.slice(0, 500);
|
|
6770
|
+
this.debuggerManager.invalidateExecutionContext(tabId, targetCtx.id);
|
|
6771
|
+
if (p.timeout !== void 0 && Date.now() - startedAt >= p.timeout) throw this.frameEvaluationError("FRAME_EVALUATION_TIMEOUT", message.slice(0, 500), {
|
|
6772
|
+
tabId,
|
|
6773
|
+
...this.frameIdentitySummary(identity),
|
|
6774
|
+
cdpFrameId: resolved.cdpFrameId,
|
|
6775
|
+
executionContextId: targetCtx.id,
|
|
6776
|
+
timeout: p.timeout,
|
|
6777
|
+
retryAttempted,
|
|
6778
|
+
retryReason
|
|
6779
|
+
});
|
|
6662
6780
|
}
|
|
6663
6781
|
}
|
|
6664
|
-
|
|
6665
|
-
|
|
6666
|
-
|
|
6667
|
-
|
|
6668
|
-
|
|
6669
|
-
|
|
6782
|
+
throw new Error("Stale execution context retry exhausted");
|
|
6783
|
+
}
|
|
6784
|
+
frameIdentitySummary(frame) {
|
|
6785
|
+
return {
|
|
6786
|
+
extensionFrameId: frame.frameId,
|
|
6787
|
+
parentFrameId: frame.parentFrameId,
|
|
6788
|
+
url: frame.url.slice(0, 500)
|
|
6670
6789
|
};
|
|
6671
|
-
|
|
6672
|
-
|
|
6790
|
+
}
|
|
6791
|
+
frameEvaluationError(code, message, context) {
|
|
6792
|
+
return new ExpectedOperationError(JSON.stringify({ error: {
|
|
6793
|
+
code,
|
|
6794
|
+
message,
|
|
6795
|
+
suggestion: "请重新读取 frame 列表和页面状态;若 frame 候选不唯一,请改用唯一 URL 或 selector",
|
|
6796
|
+
context
|
|
6797
|
+
} }));
|
|
6798
|
+
}
|
|
6799
|
+
async getExtensionFrameIdentity(tabId, frameId) {
|
|
6800
|
+
const frame = (await chrome.webNavigation.getAllFrames({ tabId }))?.find((candidate) => candidate.frameId === frameId);
|
|
6801
|
+
if (!frame) throw new ExpectedOperationError(`Frame ${frameId} not found`);
|
|
6802
|
+
return frame;
|
|
6803
|
+
}
|
|
6804
|
+
async resolveCdpFrame(tabId, targetUrl) {
|
|
6805
|
+
const treeResult = await chrome.debugger.sendCommand({ tabId }, "Page.getFrameTree");
|
|
6806
|
+
const candidates = this.findCdpFrameIds(treeResult.frameTree, targetUrl);
|
|
6807
|
+
if (candidates.length === 0) throw this.frameEvaluationError("FRAME_CDP_UNAVAILABLE", "Cannot resolve CDP frame for URL", {
|
|
6808
|
+
tabId,
|
|
6809
|
+
url: targetUrl.slice(0, 500),
|
|
6810
|
+
candidateCount: 0
|
|
6811
|
+
});
|
|
6812
|
+
if (candidates.length > 1) throw this.frameEvaluationError("FRAME_AMBIGUOUS", "Multiple CDP frames match the same URL; refusing to guess target", {
|
|
6813
|
+
tabId,
|
|
6814
|
+
url: targetUrl.slice(0, 500),
|
|
6815
|
+
candidateCount: candidates.length,
|
|
6816
|
+
candidates: candidates.slice(0, 10)
|
|
6817
|
+
});
|
|
6818
|
+
return { cdpFrameId: candidates[0] };
|
|
6673
6819
|
}
|
|
6674
6820
|
async getManagedScriptableTabId(tabId, context, operation) {
|
|
6675
6821
|
const resolvedTabId = await getTargetTabId(tabId);
|
|
@@ -8143,6 +8289,25 @@ function randomHex(bytes = 16) {
|
|
|
8143
8289
|
crypto.getRandomValues(data);
|
|
8144
8290
|
return bytesToHex(data);
|
|
8145
8291
|
}
|
|
8292
|
+
var backgroundBundleHashPromise;
|
|
8293
|
+
async function loadBackgroundBundleHash() {
|
|
8294
|
+
try {
|
|
8295
|
+
const response = await fetch(chrome.runtime.getURL("service-worker-loader.js"), { cache: "no-store" });
|
|
8296
|
+
if (!response.ok) {
|
|
8297
|
+
console.warn(`[HTTP] Cannot read Extension service worker loader: HTTP ${response.status}`);
|
|
8298
|
+
return;
|
|
8299
|
+
}
|
|
8300
|
+
const digest = await crypto.subtle.digest("SHA-256", await response.arrayBuffer());
|
|
8301
|
+
return `sha256:${bytesToHex(new Uint8Array(digest))}`;
|
|
8302
|
+
} catch (error) {
|
|
8303
|
+
console.warn("[HTTP] Cannot compute Extension background bundle hash:", error instanceof Error ? error.message : String(error));
|
|
8304
|
+
return;
|
|
8305
|
+
}
|
|
8306
|
+
}
|
|
8307
|
+
function getBackgroundBundleHash() {
|
|
8308
|
+
backgroundBundleHashPromise ??= loadBackgroundBundleHash();
|
|
8309
|
+
return backgroundBundleHashPromise;
|
|
8310
|
+
}
|
|
8146
8311
|
async function hmacHex(token, payload) {
|
|
8147
8312
|
const encoder = new TextEncoder();
|
|
8148
8313
|
const key = await crypto.subtle.importKey("raw", encoder.encode(token), {
|
|
@@ -8164,6 +8329,7 @@ var HttpClient = class {
|
|
|
8164
8329
|
pairingToken = null;
|
|
8165
8330
|
pairingTokenLoaded = false;
|
|
8166
8331
|
allowInsecureNoToken = null;
|
|
8332
|
+
lastServerFailureSummary = "";
|
|
8167
8333
|
onMessage(handler) {
|
|
8168
8334
|
this.messageHandler = handler;
|
|
8169
8335
|
}
|
|
@@ -8212,7 +8378,11 @@ var HttpClient = class {
|
|
|
8212
8378
|
};
|
|
8213
8379
|
}
|
|
8214
8380
|
const newPorts = healthyPorts.filter((p) => !this.connections.has(p));
|
|
8215
|
-
|
|
8381
|
+
const results = await Promise.allSettled(newPorts.map((port) => this.connectToPort(port)));
|
|
8382
|
+
const successCount = results.filter((r) => r.status === "fulfilled" && r.value).length;
|
|
8383
|
+
const failedPorts = newPorts.filter((_, index) => results[index].status !== "fulfilled" || !results[index].value);
|
|
8384
|
+
if (failedPorts.length > 0) console.debug(`[HTTP] ${failedPorts.length} candidate WebSocket connection(s) rejected before open: ${failedPorts.slice(0, 10).join(", ")}${failedPorts.length > 10 ? ", ..." : ""}`);
|
|
8385
|
+
if (successCount > 0) this.reconnectAttempts = 0;
|
|
8216
8386
|
const ports = this.getConnectedPorts();
|
|
8217
8387
|
if (ports.length === 0 && this.shouldReconnect) this.scheduleReconnect();
|
|
8218
8388
|
return {
|
|
@@ -8277,18 +8447,27 @@ var HttpClient = class {
|
|
|
8277
8447
|
const auth = this.verifiedAuth.get(port);
|
|
8278
8448
|
const url = `ws://127.0.0.1:${port}/${auth ? `?clientNonce=${encodeURIComponent(auth.clientNonce)}&serverNonce=${encodeURIComponent(auth.serverNonce)}&clientProof=${encodeURIComponent(auth.clientProof)}` : ""}`;
|
|
8279
8449
|
const ws = new WebSocket(url);
|
|
8450
|
+
let opened = false;
|
|
8280
8451
|
let resolved = false;
|
|
8452
|
+
const rejectCandidate = () => {
|
|
8453
|
+
if (!resolved) {
|
|
8454
|
+
resolved = true;
|
|
8455
|
+
resolve(false);
|
|
8456
|
+
}
|
|
8457
|
+
};
|
|
8281
8458
|
const connectionTimeout = setTimeout(() => {
|
|
8282
8459
|
if (ws.readyState !== WebSocket.OPEN) {
|
|
8460
|
+
rejectCandidate();
|
|
8283
8461
|
ws.close();
|
|
8284
|
-
if (!resolved) {
|
|
8285
|
-
resolved = true;
|
|
8286
|
-
resolve(false);
|
|
8287
|
-
}
|
|
8288
8462
|
}
|
|
8289
8463
|
}, 5e3);
|
|
8290
8464
|
ws.onopen = () => {
|
|
8291
8465
|
clearTimeout(connectionTimeout);
|
|
8466
|
+
if (resolved) {
|
|
8467
|
+
ws.close();
|
|
8468
|
+
return;
|
|
8469
|
+
}
|
|
8470
|
+
opened = true;
|
|
8292
8471
|
resolved = true;
|
|
8293
8472
|
const conn = {
|
|
8294
8473
|
ws,
|
|
@@ -8296,10 +8475,14 @@ var HttpClient = class {
|
|
|
8296
8475
|
};
|
|
8297
8476
|
this.connections.set(port, conn);
|
|
8298
8477
|
this.resetHeartbeat(port);
|
|
8299
|
-
|
|
8300
|
-
|
|
8301
|
-
|
|
8302
|
-
|
|
8478
|
+
getBackgroundBundleHash().then((backgroundBundleHash) => {
|
|
8479
|
+
if (ws.readyState !== WebSocket.OPEN) return;
|
|
8480
|
+
ws.send(JSON.stringify({
|
|
8481
|
+
type: "hello",
|
|
8482
|
+
version: chrome.runtime.getManifest().version,
|
|
8483
|
+
...backgroundBundleHash ? { backgroundBundleHash } : {}
|
|
8484
|
+
}));
|
|
8485
|
+
});
|
|
8303
8486
|
console.log(`[HTTP] Connected to MCP Server at port ${port} (total: ${this.connections.size})`);
|
|
8304
8487
|
this.statusHandler?.("connected", this.connections.size);
|
|
8305
8488
|
resolve(true);
|
|
@@ -8308,17 +8491,22 @@ var HttpClient = class {
|
|
|
8308
8491
|
this.handleMessage(event.data, port);
|
|
8309
8492
|
};
|
|
8310
8493
|
ws.onclose = () => {
|
|
8494
|
+
clearTimeout(connectionTimeout);
|
|
8495
|
+
if (!opened) {
|
|
8496
|
+
rejectCandidate();
|
|
8497
|
+
return;
|
|
8498
|
+
}
|
|
8311
8499
|
console.log(`[HTTP] WebSocket closed for port ${port}`);
|
|
8312
8500
|
this.removeConnection(port);
|
|
8313
8501
|
};
|
|
8314
8502
|
ws.onerror = (error) => {
|
|
8315
8503
|
clearTimeout(connectionTimeout);
|
|
8316
|
-
|
|
8317
|
-
|
|
8318
|
-
|
|
8319
|
-
|
|
8320
|
-
resolve(false);
|
|
8504
|
+
if (opened) {
|
|
8505
|
+
console.error(`[HTTP] Established WebSocket error for port ${port}:`, error);
|
|
8506
|
+
this.removeConnection(port);
|
|
8507
|
+
return;
|
|
8321
8508
|
}
|
|
8509
|
+
rejectCandidate();
|
|
8322
8510
|
};
|
|
8323
8511
|
} catch {
|
|
8324
8512
|
resolve(false);
|
|
@@ -8345,41 +8533,74 @@ var HttpClient = class {
|
|
|
8345
8533
|
* 完整扫描整段范围,保证多 CC 并存时新启动的 server 能被发现
|
|
8346
8534
|
*/
|
|
8347
8535
|
async discoverServers() {
|
|
8348
|
-
const healthyPorts = [];
|
|
8349
8536
|
const checks = [];
|
|
8350
|
-
for (let port = PORT_RANGE_START; port <= PORT_RANGE_END; port++) checks.push(this.checkPort(port).then((
|
|
8351
|
-
|
|
8352
|
-
|
|
8353
|
-
|
|
8537
|
+
for (let port = PORT_RANGE_START; port <= PORT_RANGE_END; port++) checks.push(this.checkPort(port).then((result) => ({
|
|
8538
|
+
port,
|
|
8539
|
+
result
|
|
8540
|
+
})));
|
|
8541
|
+
const results = await Promise.all(checks);
|
|
8542
|
+
const healthyPorts = results.filter(({ result }) => result.healthy).map(({ port }) => port);
|
|
8543
|
+
const selectedFailures = results.filter(({ result }) => result.failureKind);
|
|
8544
|
+
const failureSummary = selectedFailures.slice(0, 10).map(({ port, result }) => `${port} (${result.failureKind}: ${result.reason})`).join(", ");
|
|
8545
|
+
if (failureSummary && failureSummary !== this.lastServerFailureSummary) console.warn(`[HTTP] ${selectedFailures.length} MCP Server candidate(s) rejected after health identification: ${failureSummary}${selectedFailures.length > 10 ? ", ..." : ""}`);
|
|
8546
|
+
this.lastServerFailureSummary = failureSummary;
|
|
8354
8547
|
if (healthyPorts.length > 0) console.log(`[HTTP] Found ${healthyPorts.length} MCP Server(s) at ports: ${healthyPorts.sort((a, b) => a - b).join(", ")}`);
|
|
8355
8548
|
else console.log("[HTTP] No MCP Server found in port range");
|
|
8356
8549
|
return healthyPorts.sort((a, b) => a - b);
|
|
8357
8550
|
}
|
|
8358
8551
|
async checkPort(port) {
|
|
8552
|
+
const controller = new AbortController();
|
|
8553
|
+
const timeoutId = setTimeout(() => controller.abort(), HEALTH_CHECK_TIMEOUT);
|
|
8359
8554
|
try {
|
|
8360
8555
|
const token = await this.getPairingToken();
|
|
8361
8556
|
const clientNonce = randomHex();
|
|
8362
|
-
const controller = new AbortController();
|
|
8363
|
-
const timeoutId = setTimeout(() => controller.abort(), HEALTH_CHECK_TIMEOUT);
|
|
8364
8557
|
const response = await fetch(`http://127.0.0.1:${port}/api/health?clientNonce=${clientNonce}`, { signal: controller.signal });
|
|
8365
|
-
|
|
8366
|
-
if (!response.ok) return false;
|
|
8558
|
+
if (!response.ok) return { healthy: false };
|
|
8367
8559
|
const health = await response.json().catch(() => null);
|
|
8368
|
-
if (health
|
|
8369
|
-
|
|
8370
|
-
if (
|
|
8560
|
+
if (!health || typeof health.authRequired !== "boolean") return { healthy: false };
|
|
8561
|
+
if (health.authRequired) {
|
|
8562
|
+
if (!token) return {
|
|
8563
|
+
healthy: false,
|
|
8564
|
+
failureKind: "authentication",
|
|
8565
|
+
reason: "pairing token missing"
|
|
8566
|
+
};
|
|
8567
|
+
if (!health.auth?.serverNonce || !health.auth.serverProof) return {
|
|
8568
|
+
healthy: false,
|
|
8569
|
+
failureKind: "protocol",
|
|
8570
|
+
reason: "authentication challenge incomplete"
|
|
8571
|
+
};
|
|
8572
|
+
if (await hmacHex(token, `server:${clientNonce}:${health.auth.serverNonce}`) !== health.auth.serverProof) return {
|
|
8573
|
+
healthy: false,
|
|
8574
|
+
failureKind: "authentication",
|
|
8575
|
+
reason: "server proof mismatch"
|
|
8576
|
+
};
|
|
8371
8577
|
this.verifiedAuth.set(port, {
|
|
8372
8578
|
clientNonce,
|
|
8373
8579
|
serverNonce: health.auth.serverNonce,
|
|
8374
8580
|
clientProof: await hmacHex(token, `client:${clientNonce}:${health.auth.serverNonce}`)
|
|
8375
8581
|
});
|
|
8376
|
-
return true;
|
|
8582
|
+
return { healthy: true };
|
|
8377
8583
|
}
|
|
8378
|
-
if (token
|
|
8584
|
+
if (token) return {
|
|
8585
|
+
healthy: false,
|
|
8586
|
+
failureKind: "authentication",
|
|
8587
|
+
reason: "client token configured for no-token server"
|
|
8588
|
+
};
|
|
8589
|
+
if (!await this.getAllowInsecureNoToken()) return {
|
|
8590
|
+
healthy: false,
|
|
8591
|
+
failureKind: "authentication",
|
|
8592
|
+
reason: "local no-token policy disabled"
|
|
8593
|
+
};
|
|
8379
8594
|
this.verifiedAuth.delete(port);
|
|
8380
|
-
return health
|
|
8595
|
+
return health.status === "ok" ? { healthy: true } : {
|
|
8596
|
+
healthy: false,
|
|
8597
|
+
failureKind: "protocol",
|
|
8598
|
+
reason: "health status is not ok"
|
|
8599
|
+
};
|
|
8381
8600
|
} catch {
|
|
8382
|
-
return false;
|
|
8601
|
+
return { healthy: false };
|
|
8602
|
+
} finally {
|
|
8603
|
+
clearTimeout(timeoutId);
|
|
8383
8604
|
}
|
|
8384
8605
|
}
|
|
8385
8606
|
handleMessage(data, port) {
|
|
@@ -8466,11 +8687,11 @@ httpClient.onStatusChange((status, count) => {
|
|
|
8466
8687
|
updateBadge(status, count);
|
|
8467
8688
|
broadcastStatus(status, count);
|
|
8468
8689
|
});
|
|
8469
|
-
chrome.runtime.onMessage.addListener((message,
|
|
8470
|
-
handleInternalMessage(message).then(sendResponse);
|
|
8690
|
+
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
8691
|
+
handleInternalMessage(message, sender).then(sendResponse);
|
|
8471
8692
|
return true;
|
|
8472
8693
|
});
|
|
8473
|
-
async function handleInternalMessage(message) {
|
|
8694
|
+
async function handleInternalMessage(message, sender) {
|
|
8474
8695
|
switch (message.type) {
|
|
8475
8696
|
case "CONNECT": return httpClient.connect();
|
|
8476
8697
|
case "DISCONNECT":
|
|
@@ -8488,6 +8709,7 @@ async function handleInternalMessage(message) {
|
|
|
8488
8709
|
pairingTokenConfigured: await httpClient.hasPairingToken(),
|
|
8489
8710
|
allowInsecureNoToken: await httpClient.isAllowInsecureNoToken()
|
|
8490
8711
|
};
|
|
8712
|
+
case "MCP_FRAME_PROBE": return recordDomFrameProbe(message, sender);
|
|
8491
8713
|
default: return { error: "Unknown message type" };
|
|
8492
8714
|
}
|
|
8493
8715
|
}
|