@pyrokine/mcp-chrome 2.0.2 → 2.1.0

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 (75) hide show
  1. package/README.md +77 -35
  2. package/dist/core/browser-driver.d.ts +118 -1
  3. package/dist/core/browser-driver.d.ts.map +1 -1
  4. package/dist/core/browser-driver.js +11 -0
  5. package/dist/core/browser-driver.js.map +1 -1
  6. package/dist/core/error-sanitizer.js +2 -2
  7. package/dist/core/error-sanitizer.js.map +1 -1
  8. package/dist/core/errors.d.ts.map +1 -1
  9. package/dist/core/errors.js +17 -4
  10. package/dist/core/errors.js.map +1 -1
  11. package/dist/core/index.d.ts +1 -1
  12. package/dist/core/index.d.ts.map +1 -1
  13. package/dist/core/index.js +1 -1
  14. package/dist/core/index.js.map +1 -1
  15. package/dist/core/locator.d.ts.map +1 -1
  16. package/dist/core/locator.js +4 -3
  17. package/dist/core/locator.js.map +1 -1
  18. package/dist/core/session.d.ts +5 -2
  19. package/dist/core/session.d.ts.map +1 -1
  20. package/dist/core/session.js +107 -4
  21. package/dist/core/session.js.map +1 -1
  22. package/dist/core/types.d.ts +17 -1
  23. package/dist/core/types.d.ts.map +1 -1
  24. package/dist/core/types.js +57 -4
  25. package/dist/core/types.js.map +1 -1
  26. package/dist/core/unified-session.d.ts +29 -9
  27. package/dist/core/unified-session.d.ts.map +1 -1
  28. package/dist/core/unified-session.js +226 -18
  29. package/dist/core/unified-session.js.map +1 -1
  30. package/dist/core/utils.d.ts +1 -0
  31. package/dist/core/utils.d.ts.map +1 -1
  32. package/dist/core/utils.js +17 -2
  33. package/dist/core/utils.js.map +1 -1
  34. package/dist/extension/bridge.d.ts +37 -22
  35. package/dist/extension/bridge.d.ts.map +1 -1
  36. package/dist/extension/bridge.js +223 -63
  37. package/dist/extension/bridge.js.map +1 -1
  38. package/dist/extension/http-server.d.ts.map +1 -1
  39. package/dist/extension/http-server.js +39 -10
  40. package/dist/extension/http-server.js.map +1 -1
  41. package/dist/tools/browse.d.ts.map +1 -1
  42. package/dist/tools/browse.js +153 -54
  43. package/dist/tools/browse.js.map +1 -1
  44. package/dist/tools/cookies.d.ts.map +1 -1
  45. package/dist/tools/cookies.js +2 -4
  46. package/dist/tools/cookies.js.map +1 -1
  47. package/dist/tools/evaluate.d.ts.map +1 -1
  48. package/dist/tools/evaluate.js +37 -10
  49. package/dist/tools/evaluate.js.map +1 -1
  50. package/dist/tools/extract.d.ts +1 -0
  51. package/dist/tools/extract.d.ts.map +1 -1
  52. package/dist/tools/extract.js +466 -227
  53. package/dist/tools/extract.js.map +1 -1
  54. package/dist/tools/input.d.ts.map +1 -1
  55. package/dist/tools/input.js +781 -444
  56. package/dist/tools/input.js.map +1 -1
  57. package/dist/tools/logs.d.ts.map +1 -1
  58. package/dist/tools/logs.js +75 -23
  59. package/dist/tools/logs.js.map +1 -1
  60. package/dist/tools/manage.d.ts +0 -3
  61. package/dist/tools/manage.d.ts.map +1 -1
  62. package/dist/tools/manage.js +339 -285
  63. package/dist/tools/manage.js.map +1 -1
  64. package/dist/tools/png.d.ts +16 -0
  65. package/dist/tools/png.d.ts.map +1 -0
  66. package/dist/tools/png.js +197 -0
  67. package/dist/tools/png.js.map +1 -0
  68. package/dist/tools/schema.d.ts +14 -112
  69. package/dist/tools/schema.d.ts.map +1 -1
  70. package/dist/tools/schema.js +21 -12
  71. package/dist/tools/schema.js.map +1 -1
  72. package/dist/tools/wait.d.ts.map +1 -1
  73. package/dist/tools/wait.js +73 -13
  74. package/dist/tools/wait.js.map +1 -1
  75. package/package.json +11 -11
@@ -4,15 +4,17 @@
4
4
  * 提取页面内容:
5
5
  * - text: 文本内容
6
6
  * - html: HTML 源码(可选附带图片元信息或图片数据)
7
+ * - frameHtml: iframe HTML 源码(Extension 模式,支持跨域 iframe)
7
8
  * - attribute: 元素属性
8
9
  * - screenshot: 截图
9
10
  * - state: 页面状态(精简的可交互元素列表)
10
11
  * - metadata: 页面元信息(title/og/jsonLd 等)
11
12
  */
12
- import { mkdir, writeFile } from 'fs/promises';
13
+ import { readFile } from 'fs/promises';
13
14
  import { basename, extname, join } from 'path';
14
15
  import { z } from 'zod';
15
- import { CWD_PATH_PREFIX, TMP_PATH_PREFIX, ensureParentDir, formatErrorResponse, formatResponse, getSession, getUnifiedSession, resolveScopedOutputPath, } from '../core/index.js';
16
+ import { CWD_PATH_PREFIX, formatErrorResponse, formatResponse, getSession, getUnifiedSession, resolveScopedOutputPath, TMP_PATH_PREFIX, writePrivateFile, } from '../core/index.js';
17
+ import { comparePngImages, MAX_COMPARE_PIXELS, MAX_COMPARE_PNG_BYTES, readPngHeader } from './png.js';
16
18
  import { targetToFindParams, targetZodSchema } from './schema.js';
17
19
  /** 无 output 时附录返回的最大图片数 */
18
20
  const MAX_APPENDIX_IMAGES = 20;
@@ -20,7 +22,9 @@ const MAX_APPENDIX_IMAGES = 20;
20
22
  * extract 参数 schema
21
23
  */
22
24
  const extractSchema = z.object({
23
- type: z.enum(['text', 'html', 'attribute', 'screenshot', 'state', 'metadata']).describe('提取类型'),
25
+ type: z
26
+ .enum(['text', 'html', 'frameHtml', 'attribute', 'screenshot', 'state', 'metadata', 'diagnosticBundle'])
27
+ .describe('提取类型'),
24
28
  target: targetZodSchema
25
29
  .optional()
26
30
  .describe('目标元素(attribute 必填;text/html 可选,省略则提取整个页面;screenshot 可选用于元素截图;state 可选(仅 Extension)用于返回目标子树;metadata 不需要)'),
@@ -44,6 +48,17 @@ const extractSchema = z.object({
44
48
  .max(100)
45
49
  .optional()
46
50
  .describe('截图质量(screenshot,仅 jpeg/webp 有效),0-100,推荐 80'),
51
+ clip: z
52
+ .object({
53
+ x: z.number(),
54
+ y: z.number(),
55
+ width: z.number().positive(),
56
+ height: z.number().positive(),
57
+ })
58
+ .optional()
59
+ .describe('坐标区域截图(screenshot),单位为 CSS 像素'),
60
+ compareWith: z.string().optional().describe('截图对比基准 PNG 路径,遵循 output 相同的 tmp:/cwd: 路径规则'),
61
+ diffOutput: z.string().optional().describe('截图对比差异图输出路径,遵循 output 相同的 tmp:/cwd: 路径规则'),
47
62
  output: z
48
63
  .string()
49
64
  .optional()
@@ -63,14 +78,15 @@ const extractSchema = z.object({
63
78
  .optional()
64
79
  .describe('iframe 定位(可选,仅 Extension 模式),CSS 选择器(如 "iframe#main")或索引(如 0),不指定则在主框架操作'),
65
80
  });
66
- /**
67
- * extract 工具处理器
68
- */
69
81
  async function handleExtract(args) {
70
82
  try {
71
83
  const unifiedSession = getUnifiedSession();
72
84
  const useExtension = unifiedSession.isExtensionConnected();
73
85
  const session = getSession();
86
+ const frameHtmlError = validateFrameHtmlArgs(args, useExtension);
87
+ if (frameHtmlError) {
88
+ return frameHtmlError;
89
+ }
74
90
  // 多 tab 并行:临时切换到指定 tab
75
91
  return await unifiedSession.withTabId(args.tabId, async () => {
76
92
  return await unifiedSession.withFrame(args.frame, async () => {
@@ -78,219 +94,7 @@ async function handleExtract(args) {
78
94
  if (useExtension && args.target && args.timeout !== undefined) {
79
95
  await waitForTargetExtension(unifiedSession, args.target, args.timeout);
80
96
  }
81
- switch (args.type) {
82
- case 'text': {
83
- const text = useExtension
84
- ? await extractTextExtension(unifiedSession, args.target)
85
- : await extractText(session, args.target, args.timeout);
86
- if (args.output) {
87
- const outputPath = await writeOutputFile(args.output, text, 'utf-8');
88
- return formatResponse({
89
- success: true,
90
- type: 'text',
91
- output: outputPath,
92
- size: text.length,
93
- });
94
- }
95
- return formatResponse({
96
- success: true,
97
- type: 'text',
98
- content: text,
99
- });
100
- }
101
- case 'html': {
102
- // 带图片提取的增强路径
103
- if (args.images) {
104
- return await handleHtmlWithImages(unifiedSession, session, useExtension, args);
105
- }
106
- // 原有路径:纯 HTML
107
- const html = useExtension
108
- ? await extractHtmlExtension(unifiedSession, args.target)
109
- : await extractHTML(session, args.target, args.timeout);
110
- if (args.output) {
111
- const outputPath = await writeOutputFile(args.output, html, 'utf-8');
112
- return formatResponse({
113
- success: true,
114
- type: 'html',
115
- output: outputPath,
116
- size: html.length,
117
- });
118
- }
119
- return formatResponse({
120
- success: true,
121
- type: 'html',
122
- content: html,
123
- });
124
- }
125
- case 'attribute': {
126
- if (!args.target) {
127
- return formatErrorResponse(new Error('attribute 提取需要 target 参数'));
128
- }
129
- if (!args.attribute) {
130
- return formatErrorResponse(new Error('attribute 提取需要 attribute 参数'));
131
- }
132
- let value;
133
- if (useExtension) {
134
- value = await extractAttributeExtension(unifiedSession, args.target, args.attribute);
135
- }
136
- else {
137
- value = await extractAttribute(session, args.target, args.attribute, args.timeout);
138
- }
139
- return formatResponse({
140
- success: true,
141
- type: 'attribute',
142
- attribute: args.attribute,
143
- value,
144
- });
145
- }
146
- case 'screenshot': {
147
- // 有 target 时获取元素区域用于裁剪(支持所有 target 类型)
148
- let clip;
149
- if (args.target) {
150
- const { selector, text, xpath, nth: nthParam, } = targetToFindParams(args.target);
151
- const nth = nthParam ?? 0;
152
- // unified.find 内部根据 Extension 连接状态自动路由到 Extension/CDP 路径,
153
- // 返回视口绝对坐标(含 iframe 坐标修正)
154
- const found = await unifiedSession.find(selector, text, xpath);
155
- if (found.length > nth) {
156
- const rect = found[nth].rect;
157
- if (rect.width > 0 && rect.height > 0) {
158
- clip = rect;
159
- }
160
- }
161
- }
162
- const base64 = await unifiedSession.screenshot({
163
- fullPage: clip ? false : (args.fullPage ?? false),
164
- scale: args.scale,
165
- format: args.format,
166
- quality: args.quality,
167
- clip,
168
- });
169
- if (args.output) {
170
- // 写入文件
171
- const outputPath = await writeOutputFile(args.output, Buffer.from(base64, 'base64'));
172
- return formatResponse({
173
- success: true,
174
- type: 'screenshot',
175
- output: outputPath,
176
- });
177
- }
178
- // 返回 base64 图片
179
- return {
180
- content: [
181
- {
182
- type: 'image',
183
- data: base64,
184
- mimeType: `image/${args.format === 'jpeg' ? 'jpeg' : (args.format ?? 'png')}`,
185
- },
186
- ],
187
- };
188
- }
189
- case 'state': {
190
- // mode=domsnapshot:用 CDP DOMSnapshot.captureSnapshot 取全量快照(仅 CDP 模式)
191
- if (args.mode === 'domsnapshot') {
192
- if (useExtension) {
193
- return {
194
- content: [
195
- {
196
- type: 'text',
197
- text: JSON.stringify({
198
- error: {
199
- code: 'INVALID_ARGUMENT',
200
- message: 'mode=domsnapshot 仅 CDP 模式支持,Extension 模式请用默认 accessibility',
201
- },
202
- }),
203
- },
204
- ],
205
- isError: true,
206
- };
207
- }
208
- const snapshot = await unifiedSession.sendCdpCommand('DOMSnapshot.captureSnapshot', {
209
- computedStyles: ['display', 'visibility', 'opacity'],
210
- includePaintOrder: false,
211
- includeDOMRects: true,
212
- });
213
- if (args.output) {
214
- const outputPath = await writeOutputFile(args.output, JSON.stringify(snapshot, null, 2), 'utf-8');
215
- return formatResponse({
216
- success: true,
217
- type: 'state',
218
- mode: 'domsnapshot',
219
- output: outputPath,
220
- });
221
- }
222
- return formatResponse({
223
- success: true,
224
- type: 'state',
225
- mode: 'domsnapshot',
226
- snapshot,
227
- });
228
- }
229
- // 默认:accessibility 树(原行为)
230
- // 有 target 时获取子树的无障碍状态
231
- let refId;
232
- if (args.target && useExtension) {
233
- const { selector, text, xpath, nth: nthParam, } = targetToFindParams(args.target);
234
- const nth = nthParam ?? 0;
235
- const elements = await unifiedSession.find(selector, text, xpath);
236
- if (elements.length > 0 && nth < elements.length) {
237
- refId = elements[nth].refId;
238
- }
239
- }
240
- const readPageOptions = {};
241
- if (refId) {
242
- readPageOptions.refId = refId;
243
- }
244
- if (args.depth !== undefined) {
245
- readPageOptions.depth = args.depth;
246
- }
247
- const state = await unifiedSession.readPage(Object.keys(readPageOptions).length > 0 ? readPageOptions : undefined);
248
- if (args.output) {
249
- const outputPath = await writeOutputFile(args.output, JSON.stringify(state, null, 2), 'utf-8');
250
- return formatResponse({
251
- success: true,
252
- type: 'state',
253
- output: outputPath,
254
- });
255
- }
256
- return formatResponse({
257
- success: true,
258
- type: 'state',
259
- state,
260
- });
261
- }
262
- case 'metadata': {
263
- const metadata = await unifiedSession.getMetadata();
264
- if (args.output) {
265
- const outputPath = await writeOutputFile(args.output, JSON.stringify(metadata, null, 2), 'utf-8');
266
- return formatResponse({
267
- success: true,
268
- type: 'metadata',
269
- output: outputPath,
270
- });
271
- }
272
- return formatResponse({
273
- success: true,
274
- type: 'metadata',
275
- ...metadata,
276
- });
277
- }
278
- default:
279
- return {
280
- content: [
281
- {
282
- type: 'text',
283
- text: JSON.stringify({
284
- error: {
285
- code: 'INVALID_ARGUMENT',
286
- message: `未知提取类型: ${args.type}`,
287
- },
288
- }),
289
- },
290
- ],
291
- isError: true,
292
- };
293
- }
97
+ return handleExtractInFrame({ unifiedSession, session, useExtension }, args);
294
98
  }); // withFrame
295
99
  }); // withTabId
296
100
  }
@@ -298,14 +102,446 @@ async function handleExtract(args) {
298
102
  return formatErrorResponse(error);
299
103
  }
300
104
  }
105
+ function validateFrameHtmlArgs(args, useExtension) {
106
+ if (args.type !== 'frameHtml') {
107
+ return undefined;
108
+ }
109
+ if (!useExtension) {
110
+ return {
111
+ content: [
112
+ {
113
+ type: 'text',
114
+ text: JSON.stringify({
115
+ error: {
116
+ code: 'UNSUPPORTED_MODE',
117
+ message: 'frameHtml 需要 Extension 模式',
118
+ suggestion: '请连接 MCP Chrome Extension,并通过 frame 参数指定 iframe selector 或 index',
119
+ },
120
+ }),
121
+ },
122
+ ],
123
+ isError: true,
124
+ };
125
+ }
126
+ if (args.frame === undefined) {
127
+ return formatErrorResponse(new Error('frameHtml 提取需要 frame 参数,例如 frame="iframe#main" 或 frame=0'));
128
+ }
129
+ return undefined;
130
+ }
131
+ async function handleExtractInFrame(context, args) {
132
+ switch (args.type) {
133
+ case 'text':
134
+ return handleTextExtract(context, args);
135
+ case 'html':
136
+ return handleHtmlExtract(context, args);
137
+ case 'frameHtml':
138
+ return handleFrameHtmlExtract(context, args);
139
+ case 'attribute':
140
+ return handleAttributeExtract(context, args);
141
+ case 'screenshot':
142
+ return handleScreenshotExtract(context, args);
143
+ case 'state':
144
+ return handleStateExtract(context, args);
145
+ case 'diagnosticBundle':
146
+ return handleDiagnosticBundleExtract(context, args);
147
+ case 'metadata':
148
+ return handleMetadataExtract(context, args);
149
+ default:
150
+ return invalidExtractTypeResponse(args);
151
+ }
152
+ }
153
+ async function handleTextExtract({ unifiedSession, session, useExtension }, args) {
154
+ const text = useExtension
155
+ ? await extractTextExtension(unifiedSession, args.target)
156
+ : await extractText(session, args.target, args.timeout);
157
+ if (args.output) {
158
+ const outputPath = await writeOutputFile(args.output, text, 'utf-8');
159
+ return formatResponse({ success: true, type: 'text', output: outputPath, size: text.length });
160
+ }
161
+ return formatResponse({ success: true, type: 'text', content: text });
162
+ }
163
+ async function handleHtmlExtract(context, args) {
164
+ const { unifiedSession, session, useExtension } = context;
165
+ if (args.images) {
166
+ return handleHtmlWithImages(unifiedSession, session, useExtension, args);
167
+ }
168
+ const html = useExtension
169
+ ? await extractHtmlExtension(unifiedSession, args.target)
170
+ : await extractHTML(session, args.target, args.timeout);
171
+ if (args.output) {
172
+ const outputPath = await writeOutputFile(args.output, html, 'utf-8');
173
+ return formatResponse({ success: true, type: 'html', output: outputPath, size: html.length });
174
+ }
175
+ return formatResponse({ success: true, type: 'html', content: html });
176
+ }
177
+ async function handleFrameHtmlExtract({ unifiedSession }, args) {
178
+ const html = await extractHtmlExtension(unifiedSession, args.target);
179
+ if (args.output) {
180
+ const outputPath = await writeOutputFile(args.output, html, 'utf-8');
181
+ return formatResponse({
182
+ success: true,
183
+ type: 'frameHtml',
184
+ frame: args.frame,
185
+ output: outputPath,
186
+ size: html.length,
187
+ });
188
+ }
189
+ return formatResponse({ success: true, type: 'frameHtml', frame: args.frame, content: html });
190
+ }
191
+ async function handleAttributeExtract({ unifiedSession, session, useExtension }, args) {
192
+ if (!args.target) {
193
+ return formatErrorResponse(new Error('attribute 提取需要 target 参数'));
194
+ }
195
+ if (!args.attribute) {
196
+ return formatErrorResponse(new Error('attribute 提取需要 attribute 参数'));
197
+ }
198
+ const value = useExtension
199
+ ? await extractAttributeExtension(unifiedSession, args.target, args.attribute)
200
+ : await extractAttribute(session, args.target, args.attribute, args.timeout);
201
+ return formatResponse({ success: true, type: 'attribute', attribute: args.attribute, value });
202
+ }
203
+ async function handleScreenshotExtract({ unifiedSession, useExtension }, args) {
204
+ let clip;
205
+ if (args.target) {
206
+ const { selector, text, xpath, nth: nthParam } = targetToFindParams(args.target);
207
+ const nth = nthParam ?? 0;
208
+ const found = await unifiedSession.find(selector, text, xpath);
209
+ if (found.length > nth) {
210
+ const rect = found[nth].rect;
211
+ if (rect.width > 0 && rect.height > 0) {
212
+ const scrollOffset = args.frame === undefined ? await getPageScrollOffset(unifiedSession, args.timeout) : { x: 0, y: 0 };
213
+ clip = {
214
+ x: rect.x + scrollOffset.x,
215
+ y: rect.y + scrollOffset.y,
216
+ width: rect.width,
217
+ height: rect.height,
218
+ };
219
+ }
220
+ }
221
+ }
222
+ clip = args.clip ?? clip;
223
+ const fullPage = clip ? false : (args.fullPage ?? false);
224
+ const scale = args.scale ?? 1;
225
+ const screenshot = await unifiedSession.screenshot({
226
+ fullPage,
227
+ scale: args.scale,
228
+ format: args.format,
229
+ quality: args.quality,
230
+ clip,
231
+ });
232
+ const screenshotBuffer = Buffer.from(screenshot.data, 'base64');
233
+ const fallbackDimensions = await getScreenshotFallbackDimensions(unifiedSession, clip, fullPage, scale, args.timeout);
234
+ const dimensions = readImageDimensions(screenshotBuffer, screenshot.format, fallbackDimensions);
235
+ const metadata = {
236
+ format: screenshot.format,
237
+ width: dimensions?.width,
238
+ height: dimensions?.height,
239
+ dimensionSource: dimensions?.source,
240
+ scale,
241
+ fullPage,
242
+ clip,
243
+ size: screenshotBuffer.length,
244
+ byteSize: screenshotBuffer.length,
245
+ capabilities: screenshotCapabilities(useExtension),
246
+ };
247
+ if (args.compareWith) {
248
+ if (screenshot.format !== 'png') {
249
+ throw new Error('screenshot compare 仅支持 png 格式');
250
+ }
251
+ const baselinePath = await resolveScopedOutputPath(args.compareWith, 'mcp-chrome');
252
+ const baseline = await readFile(baselinePath.absolutePath);
253
+ const comparison = comparePngImages(baseline, screenshotBuffer);
254
+ metadata.comparison = {
255
+ pixelDiffRatio: comparison.pixelDiffRatio,
256
+ differentPixels: comparison.differentPixels,
257
+ totalPixels: comparison.totalPixels,
258
+ width: comparison.width,
259
+ height: comparison.height,
260
+ };
261
+ if (args.diffOutput) {
262
+ const diffPath = await writeOutputFile(args.diffOutput, comparison.diffPng);
263
+ metadata.comparison = { ...metadata.comparison, diffOutput: diffPath };
264
+ }
265
+ }
266
+ if (args.output) {
267
+ const outputPath = await writeOutputFile(args.output, screenshotBuffer);
268
+ return formatResponse({ success: true, type: 'screenshot', output: outputPath, metadata });
269
+ }
270
+ return {
271
+ content: [
272
+ { type: 'text', text: JSON.stringify({ success: true, type: 'screenshot', metadata }, null, 2) },
273
+ {
274
+ type: 'image',
275
+ data: screenshot.data,
276
+ mimeType: `image/${screenshot.format === 'jpeg' ? 'jpeg' : screenshot.format}`,
277
+ },
278
+ ],
279
+ };
280
+ }
281
+ async function handleStateExtract({ unifiedSession, useExtension }, args) {
282
+ if (args.mode === 'domsnapshot') {
283
+ return handleDomSnapshotState(unifiedSession, useExtension, args);
284
+ }
285
+ let refId;
286
+ if (args.target && useExtension) {
287
+ const { selector, text, xpath, nth: nthParam } = targetToFindParams(args.target);
288
+ const nth = nthParam ?? 0;
289
+ const elements = await unifiedSession.find(selector, text, xpath);
290
+ if (elements.length > 0 && nth < elements.length) {
291
+ refId = elements[nth].refId;
292
+ }
293
+ }
294
+ const readPageOptions = {};
295
+ if (refId) {
296
+ readPageOptions.refId = refId;
297
+ }
298
+ if (args.depth !== undefined) {
299
+ readPageOptions.depth = args.depth;
300
+ }
301
+ const state = await unifiedSession.readPage(Object.keys(readPageOptions).length > 0 ? readPageOptions : undefined);
302
+ if (args.output) {
303
+ const outputPath = await writeOutputFile(args.output, JSON.stringify(state, null, 2), 'utf-8');
304
+ return formatResponse({ success: true, type: 'state', output: outputPath });
305
+ }
306
+ return formatResponse({ success: true, type: 'state', state });
307
+ }
308
+ async function handleDomSnapshotState(unifiedSession, useExtension, args) {
309
+ if (useExtension) {
310
+ return {
311
+ content: [
312
+ {
313
+ type: 'text',
314
+ text: JSON.stringify({
315
+ error: {
316
+ code: 'INVALID_ARGUMENT',
317
+ message: 'mode=domsnapshot 仅 CDP 模式支持,Extension 模式请用默认 accessibility',
318
+ },
319
+ }),
320
+ },
321
+ ],
322
+ isError: true,
323
+ };
324
+ }
325
+ const snapshot = await unifiedSession.sendCdpCommand('DOMSnapshot.captureSnapshot', {
326
+ computedStyles: ['display', 'visibility', 'opacity'],
327
+ includePaintOrder: false,
328
+ includeDOMRects: true,
329
+ });
330
+ if (args.output) {
331
+ const outputPath = await writeOutputFile(args.output, JSON.stringify(snapshot, null, 2), 'utf-8');
332
+ return formatResponse({ success: true, type: 'state', mode: 'domsnapshot', output: outputPath });
333
+ }
334
+ return formatResponse({ success: true, type: 'state', mode: 'domsnapshot', snapshot });
335
+ }
336
+ async function handleDiagnosticBundleExtract({ unifiedSession, useExtension }, args) {
337
+ const state = unifiedSession.getState();
338
+ const metadata = await unifiedSession.getMetadata();
339
+ const frames = useExtension ? await unifiedSession.getFrames() : { frames: [] };
340
+ await unifiedSession.enableConsole();
341
+ await unifiedSession.enableNetwork();
342
+ const consoleLogs = await unifiedSession.getConsoleLogs();
343
+ const networkRequests = await unifiedSession.getNetworkRequests();
344
+ const bundle = {
345
+ schema: 'mcp-chrome.diagnosticBundle.v1',
346
+ url: state?.url,
347
+ title: state?.title,
348
+ managed: state?.managed ?? false,
349
+ mode: unifiedSession.getMode(),
350
+ metadata,
351
+ frames,
352
+ consoleSummary: {
353
+ total: consoleLogs.length,
354
+ recentErrors: consoleLogs.filter((item) => ['error', 'warning', 'warn'].includes(item.level)).slice(-20),
355
+ },
356
+ networkSummary: {
357
+ total: networkRequests.length,
358
+ recentFailures: networkRequests
359
+ .filter((item) => item.errorText || (item.status !== undefined && item.status >= 400))
360
+ .slice(-20),
361
+ },
362
+ capabilities: {
363
+ screenshot: true,
364
+ hiddenTabScreenshot: useExtension,
365
+ frames: useExtension,
366
+ },
367
+ };
368
+ const summary = {
369
+ url: bundle.url,
370
+ title: bundle.title,
371
+ managed: bundle.managed,
372
+ mode: bundle.mode,
373
+ frameCount: Array.isArray(frames.frames) ? frames.frames.length : 0,
374
+ consoleErrors: bundle.consoleSummary.recentErrors.length,
375
+ networkFailures: bundle.networkSummary.recentFailures.length,
376
+ };
377
+ if (args.output) {
378
+ const outputPath = await writeOutputFile(args.output, JSON.stringify(bundle, null, 2), 'utf-8');
379
+ return formatResponse({ success: true, type: 'diagnosticBundle', output: outputPath, summary });
380
+ }
381
+ return formatResponse({
382
+ success: true,
383
+ type: 'diagnosticBundle',
384
+ summary: { ...summary, capabilities: bundle.capabilities },
385
+ frames: bundle.frames,
386
+ console: bundle.consoleSummary.recentErrors,
387
+ failedRequests: bundle.networkSummary.recentFailures,
388
+ });
389
+ }
390
+ async function handleMetadataExtract({ unifiedSession, useExtension }, args) {
391
+ const metadata = await unifiedSession.getMetadata();
392
+ const frames = useExtension ? await unifiedSession.getFrames() : { frames: [] };
393
+ if (args.output) {
394
+ const outputPath = await writeOutputFile(args.output, JSON.stringify({ ...metadata, ...frames }, null, 2), 'utf-8');
395
+ return formatResponse({ success: true, type: 'metadata', output: outputPath });
396
+ }
397
+ return formatResponse({ success: true, type: 'metadata', ...metadata, ...frames });
398
+ }
399
+ function invalidExtractTypeResponse(args) {
400
+ return {
401
+ content: [
402
+ {
403
+ type: 'text',
404
+ text: JSON.stringify({
405
+ error: { code: 'INVALID_ARGUMENT', message: `未知提取类型: ${args.type}` },
406
+ }),
407
+ },
408
+ ],
409
+ isError: true,
410
+ };
411
+ }
301
412
  // ==================== HTML + 图片提取 ====================
302
413
  /** 写入文件前自动创建父目录,并收敛到受控范围 */
303
414
  async function writeOutputFile(path, data, encoding) {
304
415
  const resolvedPath = await resolveScopedOutputPath(path, 'mcp-chrome');
305
- await ensureParentDir(resolvedPath.absolutePath);
306
- await writeFile(resolvedPath.absolutePath, data, encoding);
416
+ await writePrivateFile(resolvedPath.absolutePath, data, encoding);
307
417
  return resolvedPath.absolutePath;
308
418
  }
419
+ async function getPageScrollOffset(unifiedSession, timeout) {
420
+ return unifiedSession.evaluate('(() => ({ x: window.scrollX || 0, y: window.scrollY || 0 }))()', undefined, timeout);
421
+ }
422
+ async function getScreenshotFallbackDimensions(unifiedSession, clip, fullPage, scale, timeout) {
423
+ if (clip) {
424
+ return {
425
+ width: Math.round(clip.width * scale),
426
+ height: Math.round(clip.height * scale),
427
+ source: 'captureArea',
428
+ };
429
+ }
430
+ const size = await unifiedSession.evaluate(fullPage
431
+ ? '(() => ({ width: document.documentElement.scrollWidth, height: document.documentElement.scrollHeight }))()'
432
+ : '(() => ({ width: window.innerWidth, height: window.innerHeight }))()', undefined, timeout);
433
+ return {
434
+ width: Math.round(size.width * scale),
435
+ height: Math.round(size.height * scale),
436
+ source: 'captureArea',
437
+ };
438
+ }
439
+ function screenshotCapabilities(useExtension) {
440
+ return {
441
+ formats: ['png', 'jpeg', 'webp'],
442
+ clip: true,
443
+ fullPage: true,
444
+ scale: true,
445
+ hiddenTabScreenshot: useExtension,
446
+ compare: {
447
+ formats: ['png'],
448
+ maxBytes: MAX_COMPARE_PNG_BYTES,
449
+ maxPixels: MAX_COMPARE_PIXELS,
450
+ },
451
+ };
452
+ }
453
+ function readImageDimensions(buffer, format, fallback) {
454
+ try {
455
+ if (format === 'png') {
456
+ return { ...readPngHeader(buffer), source: 'encoded' };
457
+ }
458
+ if (format === 'jpeg') {
459
+ const size = readJpegDimensions(buffer);
460
+ return size ? { ...size, source: 'encoded' } : fallback;
461
+ }
462
+ if (format === 'webp') {
463
+ const size = readWebpDimensions(buffer);
464
+ return size ? { ...size, source: 'encoded' } : fallback;
465
+ }
466
+ }
467
+ catch {
468
+ return fallback;
469
+ }
470
+ return fallback;
471
+ }
472
+ function readJpegDimensions(buffer) {
473
+ if (buffer.length < 4 || buffer[0] !== 0xff || buffer[1] !== 0xd8) {
474
+ return undefined;
475
+ }
476
+ let offset = 2;
477
+ while (offset + 9 < buffer.length) {
478
+ if (buffer[offset] !== 0xff) {
479
+ offset++;
480
+ continue;
481
+ }
482
+ while (buffer[offset] === 0xff) {
483
+ offset++;
484
+ }
485
+ const marker = buffer[offset++];
486
+ if (marker === 0xd9 || marker === 0xda) {
487
+ return undefined;
488
+ }
489
+ const segmentLength = buffer.readUInt16BE(offset);
490
+ if (segmentLength < 2 || offset + segmentLength > buffer.length) {
491
+ return undefined;
492
+ }
493
+ if ((marker >= 0xc0 && marker <= 0xc3) ||
494
+ (marker >= 0xc5 && marker <= 0xc7) ||
495
+ (marker >= 0xc9 && marker <= 0xcb) ||
496
+ (marker >= 0xcd && marker <= 0xcf)) {
497
+ return {
498
+ height: buffer.readUInt16BE(offset + 3),
499
+ width: buffer.readUInt16BE(offset + 5),
500
+ };
501
+ }
502
+ offset += segmentLength;
503
+ }
504
+ return undefined;
505
+ }
506
+ function readWebpDimensions(buffer) {
507
+ if (buffer.length < 30 || buffer.toString('ascii', 0, 4) !== 'RIFF' || buffer.toString('ascii', 8, 12) !== 'WEBP') {
508
+ return undefined;
509
+ }
510
+ let offset = 12;
511
+ while (offset + 8 <= buffer.length) {
512
+ const chunkType = buffer.toString('ascii', offset, offset + 4);
513
+ const chunkSize = buffer.readUInt32LE(offset + 4);
514
+ const data = offset + 8;
515
+ if (data + chunkSize > buffer.length) {
516
+ return undefined;
517
+ }
518
+ if (chunkType === 'VP8X' && chunkSize >= 10) {
519
+ return {
520
+ width: 1 + buffer.readUIntLE(data + 4, 3),
521
+ height: 1 + buffer.readUIntLE(data + 7, 3),
522
+ };
523
+ }
524
+ if (chunkType === 'VP8L' && chunkSize >= 5 && buffer[data] === 0x2f) {
525
+ const bits = buffer.readUInt32LE(data + 1);
526
+ return {
527
+ width: (bits & 0x3fff) + 1,
528
+ height: ((bits >> 14) & 0x3fff) + 1,
529
+ };
530
+ }
531
+ if (chunkType === 'VP8 ' &&
532
+ chunkSize >= 10 &&
533
+ buffer[data + 3] === 0x9d &&
534
+ buffer[data + 4] === 0x01 &&
535
+ buffer[data + 5] === 0x2a) {
536
+ return {
537
+ width: buffer.readUInt16LE(data + 6) & 0x3fff,
538
+ height: buffer.readUInt16LE(data + 8) & 0x3fff,
539
+ };
540
+ }
541
+ offset += 8 + chunkSize + (chunkSize % 2);
542
+ }
543
+ return undefined;
544
+ }
309
545
  /**
310
546
  * 处理 html + images 提取
311
547
  */
@@ -496,9 +732,8 @@ async function fetchImageData(unifiedSession, images, limit) {
496
732
  */
497
733
  async function writeImageDirectory(outputDir, html, images, imageDataList) {
498
734
  const imagesDir = join(outputDir, 'images');
499
- await mkdir(imagesDir, { recursive: true });
500
735
  // 写入 HTML
501
- await writeFile(join(outputDir, 'content.html'), html, 'utf-8');
736
+ await writePrivateFile(join(outputDir, 'content.html'), html, 'utf-8');
502
737
  // 写入图片文件 + 构建索引(相同 src 去重)
503
738
  const indexEntries = [];
504
739
  const writtenFiles = new Map(); // src → file path
@@ -518,7 +753,7 @@ async function writeImageDirectory(outputDir, html, images, imageDataList) {
518
753
  const safeName = sanitizeFilename(src);
519
754
  const filename = `${i}-${safeName}${ext}`;
520
755
  file = `images/${filename}`;
521
- await writeFile(join(imagesDir, filename), Buffer.from(data.base64, 'base64'));
756
+ await writePrivateFile(join(imagesDir, filename), Buffer.from(data.base64, 'base64'));
522
757
  writtenFiles.set(src, file);
523
758
  }
524
759
  }
@@ -532,7 +767,7 @@ async function writeImageDirectory(outputDir, html, images, imageDataList) {
532
767
  });
533
768
  }
534
769
  // 写入索引
535
- await writeFile(join(outputDir, 'index.json'), JSON.stringify({
770
+ await writePrivateFile(join(outputDir, 'index.json'), JSON.stringify({
536
771
  html: 'content.html',
537
772
  images: indexEntries,
538
773
  }, null, 2), 'utf-8');
@@ -818,8 +1053,9 @@ async function extractAttributeExtension(unifiedSession, target, attribute) {
818
1053
  if (text) {
819
1054
  // selector + text 组合:find 已实现 AND 过滤
820
1055
  const elements = await unifiedSession.find(selector, text, undefined);
821
- if (nth >= elements.length)
1056
+ if (nth >= elements.length) {
822
1057
  return null;
1058
+ }
823
1059
  return unifiedSession.getAttribute(undefined, elements[nth].refId, attribute);
824
1060
  }
825
1061
  if (nth > 0) {
@@ -850,6 +1086,9 @@ async function extractComputedStyleExtension(unifiedSession, selector, text, xpa
850
1086
  * 用于实现 extract 的 timeout 参数语义
851
1087
  */
852
1088
  async function waitForTargetExtension(unifiedSession, target, timeout) {
1089
+ if ('x' in target && 'y' in target) {
1090
+ return;
1091
+ }
853
1092
  const startTime = Date.now();
854
1093
  const retryDelay = 100;
855
1094
  const { selector, text, xpath, nth: nthParam } = targetToFindParams(target);
@@ -891,7 +1130,7 @@ async function waitForTargetExtension(unifiedSession, target, timeout) {
891
1130
  */
892
1131
  export function registerExtractTool(server) {
893
1132
  server.registerTool('extract', {
894
- description: `提取页面内容:文本、HTML(可附带图片)、属性、截图、状态、页面元信息`,
1133
+ description: `提取页面内容:文本、HTML(可附带图片)、iframe HTML、属性、截图、状态、页面元信息`,
895
1134
  inputSchema: extractSchema,
896
1135
  }, (args) => handleExtract(args));
897
1136
  }