@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.
Files changed (86) hide show
  1. package/README.md +40 -13
  2. package/README_zh.md +30 -11
  3. package/dist/anti-detection/behavior.d.ts.map +1 -1
  4. package/dist/anti-detection/behavior.js.map +1 -1
  5. package/dist/anti-detection/injection.js.map +1 -1
  6. package/dist/cdp/client.d.ts.map +1 -1
  7. package/dist/cdp/client.js.map +1 -1
  8. package/dist/cdp/launcher.d.ts.map +1 -1
  9. package/dist/cdp/launcher.js.map +1 -1
  10. package/dist/core/auto-wait.d.ts.map +1 -1
  11. package/dist/core/auto-wait.js.map +1 -1
  12. package/dist/core/browser-driver.d.ts +6 -1
  13. package/dist/core/browser-driver.d.ts.map +1 -1
  14. package/dist/core/browser-driver.js.map +1 -1
  15. package/dist/core/errors.d.ts.map +1 -1
  16. package/dist/core/errors.js.map +1 -1
  17. package/dist/core/extension-errors.d.ts.map +1 -1
  18. package/dist/core/locator.d.ts.map +1 -1
  19. package/dist/core/locator.js.map +1 -1
  20. package/dist/core/retry.js.map +1 -1
  21. package/dist/core/session.d.ts +2 -2
  22. package/dist/core/session.d.ts.map +1 -1
  23. package/dist/core/session.js +1 -1
  24. package/dist/core/session.js.map +1 -1
  25. package/dist/core/types.d.ts.map +1 -1
  26. package/dist/core/types.js.map +1 -1
  27. package/dist/core/unified-session.d.ts +17 -2
  28. package/dist/core/unified-session.d.ts.map +1 -1
  29. package/dist/core/unified-session.js +44 -6
  30. package/dist/core/unified-session.js.map +1 -1
  31. package/dist/extension/bridge.d.ts +8 -2
  32. package/dist/extension/bridge.d.ts.map +1 -1
  33. package/dist/extension/bridge.js +5 -1
  34. package/dist/extension/bridge.js.map +1 -1
  35. package/dist/extension/http-server.d.ts +17 -0
  36. package/dist/extension/http-server.d.ts.map +1 -1
  37. package/dist/extension/http-server.js +63 -8
  38. package/dist/extension/http-server.js.map +1 -1
  39. package/dist/extension/index.d.ts +1 -0
  40. package/dist/extension/index.d.ts.map +1 -1
  41. package/dist/extension/index.js.map +1 -1
  42. package/dist/index.js.map +1 -1
  43. package/dist/tools/browse.d.ts.map +1 -1
  44. package/dist/tools/browse.js +13 -37
  45. package/dist/tools/browse.js.map +1 -1
  46. package/dist/tools/cookies.js.map +1 -1
  47. package/dist/tools/diagnostics.d.ts +31 -0
  48. package/dist/tools/diagnostics.d.ts.map +1 -0
  49. package/dist/tools/diagnostics.js +93 -0
  50. package/dist/tools/diagnostics.js.map +1 -0
  51. package/dist/tools/evaluate.d.ts +15 -0
  52. package/dist/tools/evaluate.d.ts.map +1 -1
  53. package/dist/tools/evaluate.js +136 -84
  54. package/dist/tools/evaluate.js.map +1 -1
  55. package/dist/tools/extract.d.ts.map +1 -1
  56. package/dist/tools/extract.js +36 -17
  57. package/dist/tools/extract.js.map +1 -1
  58. package/dist/tools/input.d.ts +6 -0
  59. package/dist/tools/input.d.ts.map +1 -1
  60. package/dist/tools/input.js +394 -248
  61. package/dist/tools/input.js.map +1 -1
  62. package/dist/tools/logs.d.ts +17 -0
  63. package/dist/tools/logs.d.ts.map +1 -1
  64. package/dist/tools/logs.js +41 -10
  65. package/dist/tools/logs.js.map +1 -1
  66. package/dist/tools/network-sanitizer.d.ts +15 -0
  67. package/dist/tools/network-sanitizer.d.ts.map +1 -0
  68. package/dist/tools/network-sanitizer.js +61 -0
  69. package/dist/tools/network-sanitizer.js.map +1 -0
  70. package/dist/tools/post-condition.d.ts +18 -1
  71. package/dist/tools/post-condition.d.ts.map +1 -1
  72. package/dist/tools/post-condition.js +52 -94
  73. package/dist/tools/post-condition.js.map +1 -1
  74. package/dist/tools/target-diagnostics.d.ts +31 -0
  75. package/dist/tools/target-diagnostics.d.ts.map +1 -0
  76. package/dist/tools/target-diagnostics.js +91 -0
  77. package/dist/tools/target-diagnostics.js.map +1 -0
  78. package/dist/tools/wait.d.ts.map +1 -1
  79. package/dist/tools/wait.js +183 -209
  80. package/dist/tools/wait.js.map +1 -1
  81. package/extension/dist/assets/content.ts-Cj_iiU2s.js +25 -0
  82. package/extension/dist/assets/{index.ts-CJXGbOcp.js → index.ts-GG6dPUPX.js} +569 -347
  83. package/extension/dist/manifest.json +3 -3
  84. package/extension/dist/service-worker-loader.js +1 -1
  85. package/package.json +14 -10
  86. 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) Object.assign(mergedDescriptors, Object.getOwnPropertyDescriptors(def));
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$1 = 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$1(input)) {
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$2 = 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$2(input)) {
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 valueSetter = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(activeElement), "value") ?? Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value") ?? Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, "value");
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
- if (originalChrome && Object.getOwnPropertyDescriptor(window, "chrome")?.configurable) Object.defineProperty(window, "chrome", {
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 setter = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(el), "value") ?? Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value") ?? Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, "value");
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 allFrames = await chrome.webNavigation.getAllFrames({ tabId });
6501
- if (!allFrames) throw new Error("Failed to get frames");
6502
- const allChildFrames = allFrames.filter((f) => f.frameId !== 0);
6503
- if (allChildFrames.length === 0) throw new Error("No iframes found in page");
6504
- const directChildFrames = allFrames.filter((f) => f.parentFrameId === 0 && f.frameId !== 0);
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
- if (matchedFrameId === void 0 && info.index >= 0 && info.index < directChildFrames.length) matchedFrameId = directChildFrames[info.index].frameId;
6542
- if (matchedFrameId === void 0) throw new Error(`Cannot resolve iframe to frameId. src: "${info.src}", directChildren: ${directChildFrames.length}, allFrames: ${allChildFrames.length}`);
6543
- const offset = await getFrameOffset(tabId, matchedFrameId);
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: matchedFrameId,
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
- let mainFrameInfo = {
6555
- title: null,
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 : getDomFrame(frame);
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 ? mainFrameInfo.title : dom?.title ?? null,
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 ? mainFrameInfo.rect : dom?.rect ?? null
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 returnByValue = params?.returnByValue;
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 extFrames = await chrome.webNavigation.getAllFrames({ tabId });
6644
- if (!extFrames) throw new Error("Failed to enumerate frames");
6645
- const targetFrame = extFrames.find((f) => f.frameId === p.frameId);
6646
- if (!targetFrame) throw new ExpectedOperationError(`Frame ${p.frameId} not found`);
6647
- const treeResult = await chrome.debugger.sendCommand({ tabId }, "Page.getFrameTree");
6648
- const cdpFrameIds = this.findCdpFrameIds(treeResult.frameTree, targetFrame.url);
6649
- if (cdpFrameIds.length === 0) throw new Error(`Cannot resolve CDP frame for URL: ${targetFrame.url}`);
6650
- if (cdpFrameIds.length > 1) throw new ExpectedOperationError(`Multiple CDP frames (${cdpFrameIds.length}) match URL "${targetFrame.url}". Cannot uniquely identify target iframe for precise evaluate. Use stealth mode or ensure iframes have distinct URLs.`);
6651
- const cdpFrameId = cdpFrameIds[0];
6652
- let contexts = this.debuggerManager.getExecutionContexts(tabId);
6653
- if (contexts.length === 0) await chrome.debugger.sendCommand({ tabId }, "Runtime.enable");
6654
- let targetCtx = contexts.find((c) => c.frameId === cdpFrameId && c.isDefault);
6655
- if (!targetCtx) {
6656
- const maxAttempts = Math.ceil((p.timeout ?? 2e3) / 100);
6657
- for (let i = 0; i < maxAttempts; i++) {
6658
- await new Promise((resolve) => setTimeout(resolve, 100));
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((c) => c.frameId === cdpFrameId && c.isDefault);
6661
- if (targetCtx) break;
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
- if (!targetCtx) throw new Error(`No execution context for frame (CDP: ${cdpFrameId}, contexts: ${contexts.length})`);
6665
- const evalParams = {
6666
- contextId: targetCtx.id,
6667
- expression: p.expression,
6668
- returnByValue: returnByValue ?? true,
6669
- awaitPromise: awaitPromise ?? true
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
- if (p.timeout !== void 0) evalParams.timeout = p.timeout;
6672
- return await chrome.debugger.sendCommand({ tabId }, "Runtime.evaluate", evalParams);
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
- if ((await Promise.allSettled(newPorts.map((port) => this.connectToPort(port)))).filter((r) => r.status === "fulfilled" && r.value).length > 0) this.reconnectAttempts = 0;
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
- ws.send(JSON.stringify({
8300
- type: "hello",
8301
- version: chrome.runtime.getManifest().version
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
- console.error(`[HTTP] WebSocket error for port ${port}:`, error);
8317
- if (resolved) this.removeConnection(port);
8318
- else {
8319
- resolved = true;
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((ok) => {
8351
- if (ok) healthyPorts.push(port);
8352
- }));
8353
- await Promise.all(checks);
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
- clearTimeout(timeoutId);
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?.authRequired) {
8369
- if (!token || !health.auth?.serverNonce || !health.auth.serverProof) return false;
8370
- if (await hmacHex(token, `server:${clientNonce}:${health.auth.serverNonce}`) !== health.auth.serverProof) return false;
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 || !await this.getAllowInsecureNoToken()) return false;
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?.status === "ok";
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, _sender, sendResponse) => {
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
  }