@pyrokine/mcp-chrome 2.0.2 → 2.2.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.
- package/README.md +86 -35
- package/README_zh.md +570 -0
- package/dist/cdp/launcher.d.ts.map +1 -1
- package/dist/cdp/launcher.js +41 -19
- package/dist/cdp/launcher.js.map +1 -1
- package/dist/core/browser-driver.d.ts +118 -1
- package/dist/core/browser-driver.d.ts.map +1 -1
- package/dist/core/browser-driver.js +11 -0
- package/dist/core/browser-driver.js.map +1 -1
- package/dist/core/error-sanitizer.js +2 -2
- package/dist/core/error-sanitizer.js.map +1 -1
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +17 -4
- package/dist/core/errors.js.map +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/locator.d.ts.map +1 -1
- package/dist/core/locator.js +4 -3
- package/dist/core/locator.js.map +1 -1
- package/dist/core/session.d.ts +5 -2
- package/dist/core/session.d.ts.map +1 -1
- package/dist/core/session.js +107 -4
- package/dist/core/session.js.map +1 -1
- package/dist/core/types.d.ts +24 -1
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +77 -4
- package/dist/core/types.js.map +1 -1
- package/dist/core/unified-session.d.ts +38 -9
- package/dist/core/unified-session.d.ts.map +1 -1
- package/dist/core/unified-session.js +291 -18
- package/dist/core/unified-session.js.map +1 -1
- package/dist/core/utils.d.ts +1 -0
- package/dist/core/utils.d.ts.map +1 -1
- package/dist/core/utils.js +35 -7
- package/dist/core/utils.js.map +1 -1
- package/dist/extension/bridge.d.ts +37 -22
- package/dist/extension/bridge.d.ts.map +1 -1
- package/dist/extension/bridge.js +223 -63
- package/dist/extension/bridge.js.map +1 -1
- package/dist/extension/http-server.d.ts +6 -0
- package/dist/extension/http-server.d.ts.map +1 -1
- package/dist/extension/http-server.js +125 -14
- package/dist/extension/http-server.js.map +1 -1
- package/dist/tools/browse.d.ts.map +1 -1
- package/dist/tools/browse.js +153 -54
- package/dist/tools/browse.js.map +1 -1
- package/dist/tools/cookies.d.ts.map +1 -1
- package/dist/tools/cookies.js +2 -4
- package/dist/tools/cookies.js.map +1 -1
- package/dist/tools/evaluate.d.ts.map +1 -1
- package/dist/tools/evaluate.js +37 -10
- package/dist/tools/evaluate.js.map +1 -1
- package/dist/tools/extract.d.ts +1 -0
- package/dist/tools/extract.d.ts.map +1 -1
- package/dist/tools/extract.js +466 -227
- package/dist/tools/extract.js.map +1 -1
- package/dist/tools/input.d.ts.map +1 -1
- package/dist/tools/input.js +781 -444
- package/dist/tools/input.js.map +1 -1
- package/dist/tools/logs.d.ts.map +1 -1
- package/dist/tools/logs.js +75 -23
- package/dist/tools/logs.js.map +1 -1
- package/dist/tools/manage.d.ts +0 -3
- package/dist/tools/manage.d.ts.map +1 -1
- package/dist/tools/manage.js +339 -285
- package/dist/tools/manage.js.map +1 -1
- package/dist/tools/png.d.ts +16 -0
- package/dist/tools/png.d.ts.map +1 -0
- package/dist/tools/png.js +197 -0
- package/dist/tools/png.js.map +1 -0
- package/dist/tools/schema.d.ts +14 -112
- package/dist/tools/schema.d.ts.map +1 -1
- package/dist/tools/schema.js +21 -12
- package/dist/tools/schema.js.map +1 -1
- package/dist/tools/wait.d.ts.map +1 -1
- package/dist/tools/wait.js +73 -13
- package/dist/tools/wait.js.map +1 -1
- package/package.json +16 -13
package/dist/tools/extract.js
CHANGED
|
@@ -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 {
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
}
|