@pyrokine/mcp-chrome 2.4.0 → 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 +29 -13
- package/README_zh.md +22 -14
- 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/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/unified-session.d.ts +10 -2
- package/dist/core/unified-session.d.ts.map +1 -1
- package/dist/core/unified-session.js +30 -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/tools/browse.d.ts.map +1 -1
- package/dist/tools/browse.js +1 -0
- package/dist/tools/browse.js.map +1 -1
- package/dist/tools/diagnostics.d.ts.map +1 -1
- package/dist/tools/diagnostics.js +5 -4
- package/dist/tools/diagnostics.js.map +1 -1
- package/dist/tools/evaluate.d.ts +8 -0
- package/dist/tools/evaluate.d.ts.map +1 -1
- package/dist/tools/evaluate.js +33 -7
- package/dist/tools/evaluate.js.map +1 -1
- package/dist/tools/extract.d.ts.map +1 -1
- package/dist/tools/extract.js +10 -9
- package/dist/tools/extract.js.map +1 -1
- package/dist/tools/input.d.ts +4 -0
- package/dist/tools/input.d.ts.map +1 -1
- package/dist/tools/input.js +103 -75
- package/dist/tools/input.js.map +1 -1
- package/dist/tools/logs.d.ts +7 -4
- package/dist/tools/logs.d.ts.map +1 -1
- package/dist/tools/logs.js +9 -6
- 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.map +1 -1
- package/dist/tools/post-condition.js +5 -1
- package/dist/tools/post-condition.js.map +1 -1
- package/dist/tools/target-diagnostics.d.ts.map +1 -1
- package/dist/tools/target-diagnostics.js +7 -1
- package/dist/tools/target-diagnostics.js.map +1 -1
- package/dist/tools/wait.js +1 -1
- 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-BMmrcSQ6.js → index.ts-GG6dPUPX.js} +335 -282
- package/extension/dist/manifest.json +3 -3
- package/extension/dist/service-worker-loader.js +1 -1
- package/package.json +1 -1
- 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) {
|
|
@@ -4567,172 +4833,10 @@ var EvaluateInFrameSchema = object({
|
|
|
4567
4833
|
tabId: tabIdOpt,
|
|
4568
4834
|
frameId: number().int().nonnegative(),
|
|
4569
4835
|
expression: string().min(1),
|
|
4570
|
-
timeout: number().nonnegative().optional()
|
|
4836
|
+
timeout: number().nonnegative().optional(),
|
|
4837
|
+
staleContextRetry: _enum(["never", "readOnly"]).default("never")
|
|
4571
4838
|
});
|
|
4572
4839
|
//#endregion
|
|
4573
|
-
//#region src/background/tab-state.ts
|
|
4574
|
-
/**
|
|
4575
|
-
* Managed tab/window 状态与 storage 恢复。
|
|
4576
|
-
*/
|
|
4577
|
-
var STORAGE_KEY = "mcp_managed_tab_ids";
|
|
4578
|
-
var mcpTabGroupId = null;
|
|
4579
|
-
var mcpWindowId = null;
|
|
4580
|
-
var managedTabIds = /* @__PURE__ */ new Set();
|
|
4581
|
-
function getMcpTabGroupId() {
|
|
4582
|
-
return mcpTabGroupId;
|
|
4583
|
-
}
|
|
4584
|
-
function getMcpWindowId() {
|
|
4585
|
-
return mcpWindowId;
|
|
4586
|
-
}
|
|
4587
|
-
function setMcpTabGroupId(groupId) {
|
|
4588
|
-
mcpTabGroupId = groupId;
|
|
4589
|
-
}
|
|
4590
|
-
function setMcpWindowId(windowId) {
|
|
4591
|
-
mcpWindowId = windowId;
|
|
4592
|
-
}
|
|
4593
|
-
async function markManagedTab(tabId) {
|
|
4594
|
-
managedTabIds.add(tabId);
|
|
4595
|
-
await chrome.storage.local.set({ [STORAGE_KEY]: [...managedTabIds] });
|
|
4596
|
-
}
|
|
4597
|
-
async function unmarkManagedTab(tabId) {
|
|
4598
|
-
managedTabIds.delete(tabId);
|
|
4599
|
-
await chrome.storage.local.set({ [STORAGE_KEY]: [...managedTabIds] });
|
|
4600
|
-
}
|
|
4601
|
-
function isManagedTab(tab, context) {
|
|
4602
|
-
if (tab.id !== void 0 && managedTabIds.has(tab.id)) return true;
|
|
4603
|
-
return context.mcpTabGroupId !== null && tab.groupId === context.mcpTabGroupId;
|
|
4604
|
-
}
|
|
4605
|
-
/** Extension 启动时调用,从 storage 恢复 managed tab 集合 */
|
|
4606
|
-
async function restoreManagedTabs() {
|
|
4607
|
-
const result = await chrome.storage.local.get(STORAGE_KEY);
|
|
4608
|
-
const stored = Array.isArray(result[STORAGE_KEY]) ? result[STORAGE_KEY] : [];
|
|
4609
|
-
for (const id of stored) managedTabIds.add(id);
|
|
4610
|
-
const tabs = await chrome.tabs.query({});
|
|
4611
|
-
const existingIds = new Set(tabs.map((t) => t.id).filter((id) => id !== void 0));
|
|
4612
|
-
for (const id of managedTabIds) if (!existingIds.has(id)) managedTabIds.delete(id);
|
|
4613
|
-
await chrome.storage.local.set({ [STORAGE_KEY]: [...managedTabIds] });
|
|
4614
|
-
}
|
|
4615
|
-
//#endregion
|
|
4616
|
-
//#region src/background/action-utils.ts
|
|
4617
|
-
/**
|
|
4618
|
-
* Action 处理器共享工具函数
|
|
4619
|
-
*
|
|
4620
|
-
* 纯工具函数,不依赖任何实例状态,可被多个 handler 模块直接 import 使用
|
|
4621
|
-
*/
|
|
4622
|
-
function isRestrictedUrl(url) {
|
|
4623
|
-
if (!url) return true;
|
|
4624
|
-
return url === "about:blank" || url.startsWith("about:") || url.startsWith("chrome://") || url.startsWith("chrome-extension://");
|
|
4625
|
-
}
|
|
4626
|
-
async function assertScriptable(tabId) {
|
|
4627
|
-
const tab = await chrome.tabs.get(tabId);
|
|
4628
|
-
if (isRestrictedUrl(tab.url)) throw new ExpectedOperationError(`Cannot execute on restricted URL "${tab.url}". Navigate to an http/https page first.`);
|
|
4629
|
-
}
|
|
4630
|
-
async function getTargetTabId(tabId) {
|
|
4631
|
-
if (tabId !== void 0 && tabId !== null) {
|
|
4632
|
-
try {
|
|
4633
|
-
await chrome.tabs.get(tabId);
|
|
4634
|
-
} catch {
|
|
4635
|
-
throw new ExpectedOperationError(`Tab ${tabId} 不存在(可能已被关闭)`);
|
|
4636
|
-
}
|
|
4637
|
-
return tabId;
|
|
4638
|
-
}
|
|
4639
|
-
const [activeTab] = await chrome.tabs.query({
|
|
4640
|
-
active: true,
|
|
4641
|
-
currentWindow: true
|
|
4642
|
-
});
|
|
4643
|
-
if (!activeTab?.id) throw new ExpectedOperationError("No active tab found");
|
|
4644
|
-
return activeTab.id;
|
|
4645
|
-
}
|
|
4646
|
-
function tabToInfo(tab, context) {
|
|
4647
|
-
return {
|
|
4648
|
-
id: tab.id,
|
|
4649
|
-
url: tab.url || "",
|
|
4650
|
-
title: tab.title || "",
|
|
4651
|
-
active: tab.active,
|
|
4652
|
-
windowId: tab.windowId,
|
|
4653
|
-
index: tab.index,
|
|
4654
|
-
groupId: tab.groupId,
|
|
4655
|
-
pinned: tab.pinned,
|
|
4656
|
-
incognito: tab.incognito,
|
|
4657
|
-
managed: isManagedTab(tab, context),
|
|
4658
|
-
status: tab.status || "unknown"
|
|
4659
|
-
};
|
|
4660
|
-
}
|
|
4661
|
-
async function assertManagedTab(tabId, context, operation) {
|
|
4662
|
-
let tab;
|
|
4663
|
-
try {
|
|
4664
|
-
tab = await chrome.tabs.get(tabId);
|
|
4665
|
-
} catch {
|
|
4666
|
-
throw new ExpectedOperationError(`Tab ${tabId} 不存在(可能已被关闭)`);
|
|
4667
|
-
}
|
|
4668
|
-
if (!isManagedTab(tab, context)) throw new ExpectedOperationError(`${operation} 拒绝操作非托管 tab, ${JSON.stringify({
|
|
4669
|
-
tabId,
|
|
4670
|
-
managed: false,
|
|
4671
|
-
groupId: tab.groupId,
|
|
4672
|
-
mcpTabGroupId: context.mcpTabGroupId,
|
|
4673
|
-
windowId: tab.windowId,
|
|
4674
|
-
mcpWindowId: context.mcpWindowId
|
|
4675
|
-
})}, 请使用 browse(action="list") 选择 managed=true 的受控 tab, 或用 manage(action="newPage") 创建受控页面`);
|
|
4676
|
-
return tab;
|
|
4677
|
-
}
|
|
4678
|
-
/**
|
|
4679
|
-
* 递归计算 iframe 在页面坐标系中的偏移量
|
|
4680
|
-
*
|
|
4681
|
-
* 对于嵌套 iframe,递归累加各层偏移,
|
|
4682
|
-
* 当多个 iframe 共享同一 URL 时,优先按同 URL 的出现顺序匹配,
|
|
4683
|
-
* clientLeft/clientTop 补偿 iframe 的 border,确保坐标指向内容区域起点
|
|
4684
|
-
*/
|
|
4685
|
-
async function getFrameOffset(tabId, frameId) {
|
|
4686
|
-
const allFrames = await chrome.webNavigation.getAllFrames({ tabId });
|
|
4687
|
-
if (!allFrames) return null;
|
|
4688
|
-
const targetFrame = allFrames.find((f) => f.frameId === frameId);
|
|
4689
|
-
if (!targetFrame || targetFrame.parentFrameId === -1) return null;
|
|
4690
|
-
if (isRestrictedUrl((await chrome.tabs.get(tabId)).url)) return null;
|
|
4691
|
-
let parentOffset = {
|
|
4692
|
-
x: 0,
|
|
4693
|
-
y: 0
|
|
4694
|
-
};
|
|
4695
|
-
if (targetFrame.parentFrameId !== 0) {
|
|
4696
|
-
const po = await getFrameOffset(tabId, targetFrame.parentFrameId);
|
|
4697
|
-
if (po) parentOffset = po;
|
|
4698
|
-
}
|
|
4699
|
-
const siblingFrames = allFrames.filter((f) => f.parentFrameId === targetFrame.parentFrameId && f.frameId !== targetFrame.parentFrameId);
|
|
4700
|
-
const frameIndex = siblingFrames.findIndex((f) => f.frameId === frameId);
|
|
4701
|
-
const sameUrlIndex = siblingFrames.filter((f) => f.url === targetFrame.url).findIndex((f) => f.frameId === frameId);
|
|
4702
|
-
const localOffset = (await chrome.scripting.executeScript({
|
|
4703
|
-
target: {
|
|
4704
|
-
tabId,
|
|
4705
|
-
frameIds: [targetFrame.parentFrameId]
|
|
4706
|
-
},
|
|
4707
|
-
world: "MAIN",
|
|
4708
|
-
func: (frameUrl, frameIndex, sameUrlIndex) => {
|
|
4709
|
-
const iframes = Array.from(document.querySelectorAll("iframe, frame"));
|
|
4710
|
-
const urlMatches = frameUrl ? iframes.filter((iframe) => iframe.src === frameUrl) : [];
|
|
4711
|
-
let target;
|
|
4712
|
-
if (urlMatches.length === 1) target = urlMatches[0];
|
|
4713
|
-
else if (sameUrlIndex >= 0 && sameUrlIndex < urlMatches.length) target = urlMatches[sameUrlIndex];
|
|
4714
|
-
else if (frameIndex >= 0 && frameIndex < iframes.length) target = iframes[frameIndex];
|
|
4715
|
-
else if (urlMatches.length > 0) target = urlMatches[0];
|
|
4716
|
-
if (!target) return null;
|
|
4717
|
-
const rect = target.getBoundingClientRect();
|
|
4718
|
-
return {
|
|
4719
|
-
x: rect.x + target.clientLeft,
|
|
4720
|
-
y: rect.y + target.clientTop
|
|
4721
|
-
};
|
|
4722
|
-
},
|
|
4723
|
-
args: [
|
|
4724
|
-
targetFrame.url,
|
|
4725
|
-
frameIndex,
|
|
4726
|
-
sameUrlIndex
|
|
4727
|
-
]
|
|
4728
|
-
}))[0]?.result;
|
|
4729
|
-
if (!localOffset) return null;
|
|
4730
|
-
return {
|
|
4731
|
-
x: parentOffset.x + localOffset.x,
|
|
4732
|
-
y: parentOffset.y + localOffset.y
|
|
4733
|
-
};
|
|
4734
|
-
}
|
|
4735
|
-
//#endregion
|
|
4736
4840
|
//#region src/background/debugger-manager.ts
|
|
4737
4841
|
var DebuggerBlockedError = class extends ExpectedOperationError {
|
|
4738
4842
|
code = "DEBUGGER_BLOCKED";
|
|
@@ -6510,53 +6614,28 @@ var FrameResolver = class {
|
|
|
6510
6614
|
async resolveFrame(params, context) {
|
|
6511
6615
|
const p = ResolveFrameSchema.parse(params);
|
|
6512
6616
|
const tabId = await this.getManagedScriptableTabId(p.tabId, context, "resolve_frame");
|
|
6513
|
-
const
|
|
6514
|
-
if (
|
|
6515
|
-
|
|
6516
|
-
|
|
6517
|
-
|
|
6518
|
-
const info = (await chrome.scripting.executeScript({
|
|
6519
|
-
target: {
|
|
6520
|
-
tabId,
|
|
6521
|
-
frameIds: [0]
|
|
6522
|
-
},
|
|
6523
|
-
world: "MAIN",
|
|
6524
|
-
func: (frame) => {
|
|
6525
|
-
const iframes = Array.from(document.querySelectorAll("iframe, frame"));
|
|
6526
|
-
let target;
|
|
6527
|
-
let index = -1;
|
|
6528
|
-
if (typeof frame === "number") {
|
|
6529
|
-
if (frame < 0 || frame >= iframes.length) return null;
|
|
6530
|
-
target = iframes[frame];
|
|
6531
|
-
index = frame;
|
|
6532
|
-
} else {
|
|
6533
|
-
target = document.querySelector(frame);
|
|
6534
|
-
if (target) index = iframes.indexOf(target);
|
|
6535
|
-
}
|
|
6536
|
-
if (!target) return null;
|
|
6537
|
-
let absoluteSrc = target.src || "";
|
|
6538
|
-
if (absoluteSrc && !absoluteSrc.startsWith("http") && !absoluteSrc.startsWith("about:")) try {
|
|
6539
|
-
absoluteSrc = new URL(absoluteSrc, location.href).href;
|
|
6540
|
-
} catch {}
|
|
6541
|
-
return {
|
|
6542
|
-
src: absoluteSrc,
|
|
6543
|
-
index
|
|
6544
|
-
};
|
|
6545
|
-
},
|
|
6546
|
-
args: [p.frame]
|
|
6547
|
-
}))[0]?.result;
|
|
6548
|
-
if (!info) throw new ExpectedOperationError(`iframe not found: ${typeof p.frame === "number" ? `index ${p.frame}` : `selector "${p.frame}"`}`);
|
|
6549
|
-
let matchedFrameId;
|
|
6550
|
-
if (info.src) {
|
|
6551
|
-
const urlMatches = allChildFrames.filter((f) => f.url === info.src);
|
|
6552
|
-
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}`);
|
|
6553
6622
|
}
|
|
6554
|
-
|
|
6555
|
-
if (
|
|
6556
|
-
|
|
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
|
+
} }));
|
|
6557
6636
|
return {
|
|
6558
|
-
frameId:
|
|
6559
|
-
offset
|
|
6637
|
+
frameId: selected.frameId,
|
|
6638
|
+
offset: selected.contentOffset
|
|
6560
6639
|
};
|
|
6561
6640
|
}
|
|
6562
6641
|
async getAllFrames(params, context) {
|
|
@@ -6564,80 +6643,20 @@ var FrameResolver = class {
|
|
|
6564
6643
|
const tabId = await this.getManagedScriptableTabId(p.tabId, context, "get_all_frames");
|
|
6565
6644
|
const frames = await chrome.webNavigation.getAllFrames({ tabId });
|
|
6566
6645
|
if (!frames) throw new Error("Failed to get frames");
|
|
6567
|
-
|
|
6568
|
-
|
|
6569
|
-
rect: {
|
|
6570
|
-
x: 0,
|
|
6571
|
-
y: 0,
|
|
6572
|
-
width: 0,
|
|
6573
|
-
height: 0
|
|
6574
|
-
}
|
|
6575
|
-
};
|
|
6576
|
-
let domFrames = [];
|
|
6577
|
-
try {
|
|
6578
|
-
const pageFrames = (await chrome.scripting.executeScript({
|
|
6579
|
-
target: {
|
|
6580
|
-
tabId,
|
|
6581
|
-
frameIds: [0]
|
|
6582
|
-
},
|
|
6583
|
-
world: "MAIN",
|
|
6584
|
-
func: () => ({
|
|
6585
|
-
main: {
|
|
6586
|
-
title: document.title || null,
|
|
6587
|
-
rect: {
|
|
6588
|
-
x: 0,
|
|
6589
|
-
y: 0,
|
|
6590
|
-
width: window.innerWidth,
|
|
6591
|
-
height: window.innerHeight
|
|
6592
|
-
}
|
|
6593
|
-
},
|
|
6594
|
-
frames: Array.from(document.querySelectorAll("iframe, frame")).map((frame, index) => {
|
|
6595
|
-
const el = frame;
|
|
6596
|
-
const rect = el.getBoundingClientRect();
|
|
6597
|
-
const selector = el.id ? `#${CSS.escape(el.id)}` : `${el.tagName.toLowerCase()}:nth-of-type(${index + 1})`;
|
|
6598
|
-
return {
|
|
6599
|
-
index,
|
|
6600
|
-
url: el.src || "",
|
|
6601
|
-
title: el.title || null,
|
|
6602
|
-
name: el.name || null,
|
|
6603
|
-
selector,
|
|
6604
|
-
rect: {
|
|
6605
|
-
x: rect.x,
|
|
6606
|
-
y: rect.y,
|
|
6607
|
-
width: rect.width,
|
|
6608
|
-
height: rect.height
|
|
6609
|
-
}
|
|
6610
|
-
};
|
|
6611
|
-
})
|
|
6612
|
-
})
|
|
6613
|
-
}))[0]?.result;
|
|
6614
|
-
mainFrameInfo = pageFrames?.main ?? mainFrameInfo;
|
|
6615
|
-
domFrames = pageFrames?.frames ?? [];
|
|
6616
|
-
} catch {
|
|
6617
|
-
domFrames = [];
|
|
6618
|
-
}
|
|
6619
|
-
const directChildFrames = frames.filter((frame) => frame.parentFrameId === 0 && frame.frameId !== 0);
|
|
6620
|
-
const getDomFrame = (frame) => {
|
|
6621
|
-
if (frame.parentFrameId !== 0) return;
|
|
6622
|
-
const directIndex = directChildFrames.findIndex((candidate) => candidate.frameId === frame.frameId);
|
|
6623
|
-
const byIndex = directIndex >= 0 ? domFrames[directIndex] : void 0;
|
|
6624
|
-
if (byIndex) return byIndex;
|
|
6625
|
-
const navigationMatches = directChildFrames.filter((candidate) => candidate.url === frame.url);
|
|
6626
|
-
const domMatches = domFrames.filter((candidate) => candidate.url === frame.url);
|
|
6627
|
-
return navigationMatches.length === 1 && domMatches.length === 1 ? domMatches[0] : void 0;
|
|
6628
|
-
};
|
|
6646
|
+
const snapshot = await getDomFrameSnapshot(tabId, 0);
|
|
6647
|
+
const domFramesByFrameId = new Map(snapshot.frames.filter((frame) => frame.frameId !== null).map((frame) => [frame.frameId, frame]));
|
|
6629
6648
|
return { frames: frames.map((frame, index) => {
|
|
6630
6649
|
const isMainFrame = frame.frameId === 0;
|
|
6631
|
-
const dom = isMainFrame ? void 0 :
|
|
6650
|
+
const dom = isMainFrame ? void 0 : domFramesByFrameId.get(frame.frameId);
|
|
6632
6651
|
return {
|
|
6633
6652
|
index,
|
|
6634
6653
|
frameId: frame.frameId,
|
|
6635
6654
|
parentFrameId: isMainFrame ? null : frame.parentFrameId,
|
|
6636
6655
|
url: frame.url,
|
|
6637
|
-
title: isMainFrame ?
|
|
6656
|
+
title: isMainFrame ? snapshot.parent.title : dom?.title ?? null,
|
|
6638
6657
|
name: isMainFrame ? null : dom?.name ?? null,
|
|
6639
6658
|
selector: isMainFrame ? null : dom?.selector ?? null,
|
|
6640
|
-
rect: isMainFrame ?
|
|
6659
|
+
rect: isMainFrame ? snapshot.parent.rect : dom?.rect ?? null
|
|
6641
6660
|
};
|
|
6642
6661
|
}) };
|
|
6643
6662
|
}
|
|
@@ -6656,7 +6675,8 @@ var FrameResolver = class {
|
|
|
6656
6675
|
const originalFrame = await this.getExtensionFrameIdentity(tabId, p.frameId);
|
|
6657
6676
|
let retryAttempted = false;
|
|
6658
6677
|
let retryReason;
|
|
6659
|
-
|
|
6678
|
+
const maxAttempts = p.staleContextRetry === "readOnly" ? 2 : 1;
|
|
6679
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
6660
6680
|
const remaining = p.timeout === void 0 ? void 0 : p.timeout - (Date.now() - startedAt);
|
|
6661
6681
|
if (remaining !== void 0 && remaining <= 0) throw this.frameEvaluationError("FRAME_EVALUATION_TIMEOUT", "Frame evaluation timed out before resolution", {
|
|
6662
6682
|
tabId,
|
|
@@ -6713,6 +6733,7 @@ var FrameResolver = class {
|
|
|
6713
6733
|
...await chrome.debugger.sendCommand({ tabId }, "Runtime.evaluate", evalParams),
|
|
6714
6734
|
retryAttempted,
|
|
6715
6735
|
retryReason,
|
|
6736
|
+
staleContextRetry: p.staleContextRetry,
|
|
6716
6737
|
frameContext: {
|
|
6717
6738
|
tabId,
|
|
6718
6739
|
extensionFrameId: identity.frameId,
|
|
@@ -6736,6 +6757,14 @@ var FrameResolver = class {
|
|
|
6736
6757
|
retryReason
|
|
6737
6758
|
});
|
|
6738
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
|
+
});
|
|
6739
6768
|
retryAttempted = true;
|
|
6740
6769
|
retryReason = message.slice(0, 500);
|
|
6741
6770
|
this.debuggerManager.invalidateExecutionContext(tabId, targetCtx.id);
|
|
@@ -8260,6 +8289,25 @@ function randomHex(bytes = 16) {
|
|
|
8260
8289
|
crypto.getRandomValues(data);
|
|
8261
8290
|
return bytesToHex(data);
|
|
8262
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
|
+
}
|
|
8263
8311
|
async function hmacHex(token, payload) {
|
|
8264
8312
|
const encoder = new TextEncoder();
|
|
8265
8313
|
const key = await crypto.subtle.importKey("raw", encoder.encode(token), {
|
|
@@ -8427,10 +8475,14 @@ var HttpClient = class {
|
|
|
8427
8475
|
};
|
|
8428
8476
|
this.connections.set(port, conn);
|
|
8429
8477
|
this.resetHeartbeat(port);
|
|
8430
|
-
|
|
8431
|
-
|
|
8432
|
-
|
|
8433
|
-
|
|
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
|
+
});
|
|
8434
8486
|
console.log(`[HTTP] Connected to MCP Server at port ${port} (total: ${this.connections.size})`);
|
|
8435
8487
|
this.statusHandler?.("connected", this.connections.size);
|
|
8436
8488
|
resolve(true);
|
|
@@ -8635,11 +8687,11 @@ httpClient.onStatusChange((status, count) => {
|
|
|
8635
8687
|
updateBadge(status, count);
|
|
8636
8688
|
broadcastStatus(status, count);
|
|
8637
8689
|
});
|
|
8638
|
-
chrome.runtime.onMessage.addListener((message,
|
|
8639
|
-
handleInternalMessage(message).then(sendResponse);
|
|
8690
|
+
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
8691
|
+
handleInternalMessage(message, sender).then(sendResponse);
|
|
8640
8692
|
return true;
|
|
8641
8693
|
});
|
|
8642
|
-
async function handleInternalMessage(message) {
|
|
8694
|
+
async function handleInternalMessage(message, sender) {
|
|
8643
8695
|
switch (message.type) {
|
|
8644
8696
|
case "CONNECT": return httpClient.connect();
|
|
8645
8697
|
case "DISCONNECT":
|
|
@@ -8657,6 +8709,7 @@ async function handleInternalMessage(message) {
|
|
|
8657
8709
|
pairingTokenConfigured: await httpClient.hasPairingToken(),
|
|
8658
8710
|
allowInsecureNoToken: await httpClient.isAllowInsecureNoToken()
|
|
8659
8711
|
};
|
|
8712
|
+
case "MCP_FRAME_PROBE": return recordDomFrameProbe(message, sender);
|
|
8660
8713
|
default: return { error: "Unknown message type" };
|
|
8661
8714
|
}
|
|
8662
8715
|
}
|