@szc-ft/mcp-szcd-client 0.35.0 → 0.38.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/agents/build.js +59 -1
- package/agents/platforms.json +7 -0
- package/agents/szcd-component-expert.qoder.md +2 -0
- package/opencode-extension/opencode.json +9 -0
- package/opencode-extension/skills/local-browser-test/SKILL.md +91 -0
- package/opencode-extension/skills/local-browser-test/config.example.yaml +51 -0
- package/opencode-extension/skills/local-browser-test/lib/api-call-executor.js +251 -0
- package/opencode-extension/skills/local-browser-test/lib/browser-engine.js +95 -21
- package/opencode-extension/skills/local-browser-test/lib/config-loader.js +104 -0
- package/opencode-extension/skills/local-browser-test/lib/console-archiver.js +100 -0
- package/opencode-extension/skills/local-browser-test/lib/global-setup.js +72 -0
- package/opencode-extension/skills/local-browser-test/lib/har-collector.js +124 -0
- package/opencode-extension/skills/local-browser-test/lib/html-reporter.js +248 -0
- package/opencode-extension/skills/local-browser-test/lib/projects.js +46 -0
- package/opencode-extension/skills/local-browser-test/lib/raw-script-helpers.js +93 -0
- package/opencode-extension/skills/local-browser-test/lib/redact.js +94 -0
- package/opencode-extension/skills/local-browser-test/lib/report-retain.js +84 -0
- package/opencode-extension/skills/local-browser-test/lib/retry-policy.js +160 -0
- package/opencode-extension/skills/local-browser-test/lib/storage-state.js +111 -0
- package/opencode-extension/skills/local-browser-test/lib/tag-runner.js +103 -0
- package/opencode-extension/skills/local-browser-test/lib/test-plan.js +2 -0
- package/opencode-extension/skills/local-browser-test/lib/trace-collector.js +94 -0
- package/opencode-extension/skills/local-browser-test/lib/trend-aggregator.js +97 -0
- package/opencode-extension/skills/local-browser-test/lib/workspace-init.js +178 -0
- package/opencode-extension/skills/local-browser-test/local-browser-executor.js +226 -2
- package/opencode-extension/skills/local-browser-test/tests/integration/pr-01.test.js +254 -0
- package/opencode-extension/skills/local-browser-test/tests/integration/pr-02.test.js +263 -0
- package/opencode-extension/skills/local-browser-test/tests/integration/pr-03.test.js +308 -0
- package/opencode-extension/skills/local-browser-test/tests/integration/pr-04.test.js +168 -0
- package/opencode-extension/skills/local-browser-test/tests/integration/pr-05.test.js +183 -0
- package/opencode-extension/skills/local-browser-test/tests/integration/pr-06.test.js +252 -0
- package/opencode-extension/skills/local-browser-test/tests/integration/pr-07.test.js +168 -0
- package/opencode-extension/skills/local-browser-test/tests/integration/pr-08.test.js +293 -0
- package/opencode-extension/skills/shadcn-design-to-code/SKILL.md +352 -0
- package/opencode-extension/skills/shadcn-design-to-code/examples/1.1-overview-page.tsx +382 -0
- package/opencode-extension/skills/shadcn-design-to-code/references/install-pitfalls.md +526 -0
- package/opencode-extension/skills/shadcn-design-to-code/references/layouts.md +551 -0
- package/opencode-extension/skills/shadcn-design-to-code/references/sketch-mapping.md +247 -0
- package/opencode-extension/skills/shadcn-design-to-code/references/tokens.md +534 -0
- package/opencode-extension/skills/szcd-upload-zip/SKILL.md +45 -0
- package/package.json +70 -70
- package/qwen-extension/qwen-extension.json +10 -1
- package/qwen-extension/skills/local-browser-test/SKILL.md +91 -0
- package/qwen-extension/skills/local-browser-test/config.example.yaml +51 -0
- package/qwen-extension/skills/local-browser-test/lib/api-call-executor.js +251 -0
- package/qwen-extension/skills/local-browser-test/lib/browser-engine.js +95 -21
- package/qwen-extension/skills/local-browser-test/lib/config-loader.js +104 -0
- package/qwen-extension/skills/local-browser-test/lib/console-archiver.js +100 -0
- package/qwen-extension/skills/local-browser-test/lib/global-setup.js +72 -0
- package/qwen-extension/skills/local-browser-test/lib/har-collector.js +124 -0
- package/qwen-extension/skills/local-browser-test/lib/html-reporter.js +248 -0
- package/qwen-extension/skills/local-browser-test/lib/projects.js +46 -0
- package/qwen-extension/skills/local-browser-test/lib/raw-script-helpers.js +93 -0
- package/qwen-extension/skills/local-browser-test/lib/redact.js +94 -0
- package/qwen-extension/skills/local-browser-test/lib/report-retain.js +84 -0
- package/qwen-extension/skills/local-browser-test/lib/retry-policy.js +160 -0
- package/qwen-extension/skills/local-browser-test/lib/storage-state.js +111 -0
- package/qwen-extension/skills/local-browser-test/lib/tag-runner.js +103 -0
- package/qwen-extension/skills/local-browser-test/lib/test-plan.js +2 -0
- package/qwen-extension/skills/local-browser-test/lib/trace-collector.js +94 -0
- package/qwen-extension/skills/local-browser-test/lib/trend-aggregator.js +97 -0
- package/qwen-extension/skills/local-browser-test/lib/workspace-init.js +178 -0
- package/qwen-extension/skills/local-browser-test/local-browser-executor.js +226 -2
- package/qwen-extension/skills/local-browser-test/tests/integration/pr-01.test.js +254 -0
- package/qwen-extension/skills/local-browser-test/tests/integration/pr-02.test.js +263 -0
- package/qwen-extension/skills/local-browser-test/tests/integration/pr-03.test.js +308 -0
- package/qwen-extension/skills/local-browser-test/tests/integration/pr-04.test.js +168 -0
- package/qwen-extension/skills/local-browser-test/tests/integration/pr-05.test.js +183 -0
- package/qwen-extension/skills/local-browser-test/tests/integration/pr-06.test.js +252 -0
- package/qwen-extension/skills/local-browser-test/tests/integration/pr-07.test.js +168 -0
- package/qwen-extension/skills/local-browser-test/tests/integration/pr-08.test.js +293 -0
- package/qwen-extension/skills/shadcn-design-to-code/SKILL.md +352 -0
- package/qwen-extension/skills/shadcn-design-to-code/examples/1.1-overview-page.tsx +382 -0
- package/qwen-extension/skills/shadcn-design-to-code/references/install-pitfalls.md +526 -0
- package/qwen-extension/skills/shadcn-design-to-code/references/layouts.md +551 -0
- package/qwen-extension/skills/shadcn-design-to-code/references/sketch-mapping.md +247 -0
- package/qwen-extension/skills/shadcn-design-to-code/references/tokens.md +534 -0
- package/qwen-extension/skills/szcd-upload-zip/SKILL.md +45 -0
- package/scripts/lib/common.js +7 -0
- package/standard-skill/local-browser-test/SKILL.md +91 -0
- package/standard-skill/local-browser-test/config.example.yaml +51 -0
- package/standard-skill/local-browser-test/lib/api-call-executor.js +251 -0
- package/standard-skill/local-browser-test/lib/browser-engine.js +95 -21
- package/standard-skill/local-browser-test/lib/config-loader.js +104 -0
- package/standard-skill/local-browser-test/lib/console-archiver.js +100 -0
- package/standard-skill/local-browser-test/lib/global-setup.js +72 -0
- package/standard-skill/local-browser-test/lib/har-collector.js +124 -0
- package/standard-skill/local-browser-test/lib/html-reporter.js +248 -0
- package/standard-skill/local-browser-test/lib/projects.js +46 -0
- package/standard-skill/local-browser-test/lib/raw-script-helpers.js +93 -0
- package/standard-skill/local-browser-test/lib/redact.js +94 -0
- package/standard-skill/local-browser-test/lib/report-retain.js +84 -0
- package/standard-skill/local-browser-test/lib/retry-policy.js +160 -0
- package/standard-skill/local-browser-test/lib/storage-state.js +111 -0
- package/standard-skill/local-browser-test/lib/tag-runner.js +103 -0
- package/standard-skill/local-browser-test/lib/test-plan.js +2 -0
- package/standard-skill/local-browser-test/lib/trace-collector.js +94 -0
- package/standard-skill/local-browser-test/lib/trend-aggregator.js +97 -0
- package/standard-skill/local-browser-test/lib/workspace-init.js +178 -0
- package/standard-skill/local-browser-test/local-browser-executor.js +226 -2
- package/standard-skill/local-browser-test/tests/integration/pr-01.test.js +254 -0
- package/standard-skill/local-browser-test/tests/integration/pr-02.test.js +263 -0
- package/standard-skill/local-browser-test/tests/integration/pr-03.test.js +308 -0
- package/standard-skill/local-browser-test/tests/integration/pr-04.test.js +168 -0
- package/standard-skill/local-browser-test/tests/integration/pr-05.test.js +183 -0
- package/standard-skill/local-browser-test/tests/integration/pr-06.test.js +252 -0
- package/standard-skill/local-browser-test/tests/integration/pr-07.test.js +168 -0
- package/standard-skill/local-browser-test/tests/integration/pr-08.test.js +293 -0
- package/standard-skill/shadcn-design-to-code/SKILL.md +352 -0
- package/standard-skill/shadcn-design-to-code/examples/1.1-overview-page.tsx +382 -0
- package/standard-skill/shadcn-design-to-code/references/install-pitfalls.md +526 -0
- package/standard-skill/shadcn-design-to-code/references/layouts.md +551 -0
- package/standard-skill/shadcn-design-to-code/references/sketch-mapping.md +247 -0
- package/standard-skill/shadcn-design-to-code/references/tokens.md +534 -0
- package/standard-skill/szcd-upload-zip/SKILL.md +45 -0
- package/opencode-extension/skills/local-browser-test/local-browser-executor.cjs +0 -742
- package/qwen-extension/skills/local-browser-test/local-browser-executor.cjs +0 -742
|
@@ -1,742 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* local-browser-executor.cjs - 本地浏览器自动化执行器
|
|
4
|
-
*
|
|
5
|
-
* 功能:
|
|
6
|
-
* --action diagnose 全面诊断(JS错误、网络请求、API检查、资源加载)
|
|
7
|
-
* --action list-pages 列出所有标签页
|
|
8
|
-
* --action screenshot 截图(支持 --full-page 滚动截图)
|
|
9
|
-
* --action api-check 检查 API 请求状态
|
|
10
|
-
* --action snapshot 结构化 DOM 快照(类似 browser-use take_snapshot)
|
|
11
|
-
* --action interact 智能交互(支持 text=/role= 选择器,类似 Playwright)
|
|
12
|
-
* --action perf 性能指标采集(FCP/LCP/CLS/TBT)
|
|
13
|
-
* --action watch 持续监控模式(定时截图+错误检测)
|
|
14
|
-
* --action plan 执行测试计划 JSON
|
|
15
|
-
*
|
|
16
|
-
* 连接方式:
|
|
17
|
-
* --cdp-url http://localhost:9222 (默认 9222)
|
|
18
|
-
*
|
|
19
|
-
* 通用参数:
|
|
20
|
-
* --url <url> 目标页面 URL
|
|
21
|
-
* --url-contains <s> 按关键词查找已打开的页面
|
|
22
|
-
* --wait <ms> 额外等待时间(默认 5000)
|
|
23
|
-
* --output <path> 结果输出文件
|
|
24
|
-
* --output-dir <dir> 截图/产物输出目录
|
|
25
|
-
*
|
|
26
|
-
* snapshot 参数:
|
|
27
|
-
* --selector <css> 快照根元素(默认 body)
|
|
28
|
-
* --depth <n> DOM 深度(默认 5)
|
|
29
|
-
*
|
|
30
|
-
* interact 参数:
|
|
31
|
-
* --click <selector> 点击元素(支持 text=、role=、CSS)
|
|
32
|
-
* --type <text> 输入文本(配合 --click 定位输入框)
|
|
33
|
-
* --hover <selector> 悬停元素
|
|
34
|
-
* --scroll <direction> 滚动(up/down/left/right,配合 --scroll-amount)
|
|
35
|
-
*
|
|
36
|
-
* watch 参数:
|
|
37
|
-
* --interval <ms> 监控间隔(默认 5000)
|
|
38
|
-
* --max-rounds <n> 最大轮数(默认 60)
|
|
39
|
-
*/
|
|
40
|
-
|
|
41
|
-
// 共享缓存自愈:6/17 反馈中 NODE_PATH 未注入会导致 puppeteer-core MODULE_NOT_FOUND。
|
|
42
|
-
// 把 ~/.szcd-mcp/deps/node_modules 注入 module 解析路径,再 require puppeteer-core。
|
|
43
|
-
(function ensureSharedDeps() {
|
|
44
|
-
try {
|
|
45
|
-
const os = require('os');
|
|
46
|
-
const Module = require('module');
|
|
47
|
-
const sharedDeps = require('path').join(os.homedir(), '.szcd-mcp', 'deps', 'node_modules');
|
|
48
|
-
if (require('fs').existsSync(sharedDeps) && !Module.globalPaths.includes(sharedDeps)) {
|
|
49
|
-
Module.globalPaths.push(sharedDeps);
|
|
50
|
-
}
|
|
51
|
-
} catch {
|
|
52
|
-
// ignore — 没有共享缓存时退回项目本地 node_modules
|
|
53
|
-
}
|
|
54
|
-
})();
|
|
55
|
-
|
|
56
|
-
const puppeteer = require('puppeteer-core');
|
|
57
|
-
const fs = require('fs');
|
|
58
|
-
const path = require('path');
|
|
59
|
-
const http = require('http');
|
|
60
|
-
|
|
61
|
-
// ===== 参数解析 =====
|
|
62
|
-
const args = process.argv.slice(2);
|
|
63
|
-
function getArg(name, defaultVal) {
|
|
64
|
-
const idx = args.indexOf(`--${name}`);
|
|
65
|
-
if (idx === -1) return defaultVal;
|
|
66
|
-
return args[idx + 1] || defaultVal;
|
|
67
|
-
}
|
|
68
|
-
function hasFlag(name) {
|
|
69
|
-
return args.includes(`--${name}`);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const ACTION = getArg('action', 'diagnose');
|
|
73
|
-
const CDP_URL = getArg('cdp-url', 'http://localhost:9222');
|
|
74
|
-
const TARGET_URL = getArg('url', null);
|
|
75
|
-
const URL_CONTAINS = getArg('url-contains', null);
|
|
76
|
-
const WAIT_MS = parseInt(getArg('wait', '5000'), 10);
|
|
77
|
-
const OUTPUT = getArg('output', null);
|
|
78
|
-
const OUTPUT_DIR = getArg('output-dir', '/tmp/browser-test');
|
|
79
|
-
const PLAN_FILE = getArg('plan-file', null);
|
|
80
|
-
|
|
81
|
-
// ===== 工具函数 =====
|
|
82
|
-
function log(msg) { console.log(msg); }
|
|
83
|
-
function ensureDir(dir) { fs.mkdirSync(dir, { recursive: true }); }
|
|
84
|
-
|
|
85
|
-
function checkCDP(url) {
|
|
86
|
-
return new Promise((resolve) => {
|
|
87
|
-
const req = http.get(url + '/json/version', { timeout: 2000 }, (res) => {
|
|
88
|
-
let data = '';
|
|
89
|
-
res.on('data', c => data += c);
|
|
90
|
-
res.on('end', () => {
|
|
91
|
-
try { resolve(JSON.parse(data)); } catch { resolve(null); }
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
req.on('error', () => resolve(null));
|
|
95
|
-
req.on('timeout', () => { req.destroy(); resolve(null); });
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
async function findPage(browser) {
|
|
100
|
-
let pages;
|
|
101
|
-
try {
|
|
102
|
-
pages = await Promise.race([
|
|
103
|
-
browser.pages(),
|
|
104
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error('pages() timeout')), 8000)),
|
|
105
|
-
]);
|
|
106
|
-
} catch {
|
|
107
|
-
// browser.pages() 超时,尝试用 targets() 找到目标再 .page()
|
|
108
|
-
try {
|
|
109
|
-
const targets = browser.targets();
|
|
110
|
-
let matchTarget = null;
|
|
111
|
-
if (URL_CONTAINS) {
|
|
112
|
-
matchTarget = targets.find(t => t.type() === 'page' && t.url().includes(URL_CONTAINS));
|
|
113
|
-
}
|
|
114
|
-
if (TARGET_URL && !matchTarget) {
|
|
115
|
-
matchTarget = targets.find(t => t.type() === 'page' && t.url().includes(TARGET_URL));
|
|
116
|
-
}
|
|
117
|
-
if (!matchTarget) {
|
|
118
|
-
matchTarget = targets.find(t => t.type() === 'page' && !t.url().startsWith('about:') && !t.url().startsWith('devtools:'));
|
|
119
|
-
}
|
|
120
|
-
if (matchTarget) {
|
|
121
|
-
const page = await matchTarget.page();
|
|
122
|
-
if (page) return page;
|
|
123
|
-
}
|
|
124
|
-
} catch (e) {
|
|
125
|
-
log(`[WARN] targets() 回退失败: ${e.message}`);
|
|
126
|
-
}
|
|
127
|
-
// 最终回退
|
|
128
|
-
if (TARGET_URL) {
|
|
129
|
-
const page = await browser.newPage();
|
|
130
|
-
await page.goto(TARGET_URL, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
|
131
|
-
return page;
|
|
132
|
-
}
|
|
133
|
-
return await browser.newPage();
|
|
134
|
-
}
|
|
135
|
-
if (TARGET_URL) {
|
|
136
|
-
for (const p of pages) {
|
|
137
|
-
if (p.url().includes(TARGET_URL) || p.url() === TARGET_URL) return p;
|
|
138
|
-
}
|
|
139
|
-
// 导航到目标 URL
|
|
140
|
-
const p = pages[0] || await browser.newPage();
|
|
141
|
-
await p.goto(TARGET_URL, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
|
142
|
-
return p;
|
|
143
|
-
}
|
|
144
|
-
if (URL_CONTAINS) {
|
|
145
|
-
for (const p of pages) {
|
|
146
|
-
if (p.url().includes(URL_CONTAINS)) return p;
|
|
147
|
-
}
|
|
148
|
-
log(`[WARN] 未找到包含 "${URL_CONTAINS}" 的页面,使用第一个页面`);
|
|
149
|
-
}
|
|
150
|
-
// 找到非 about:blank 的页面
|
|
151
|
-
for (const p of pages) {
|
|
152
|
-
if (!p.url().startsWith('about:') && !p.url().startsWith('devtools:')) return p;
|
|
153
|
-
}
|
|
154
|
-
return pages[0];
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// ===== Action: list-pages =====
|
|
158
|
-
async function listPages(browser) {
|
|
159
|
-
log('[INFO] 获取标签页列表...');
|
|
160
|
-
// browser.pages() 在大量标签页时可能卡住,用 CDP 直接获取
|
|
161
|
-
let pages;
|
|
162
|
-
try {
|
|
163
|
-
pages = await Promise.race([
|
|
164
|
-
browser.pages(),
|
|
165
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error('pages() timeout')), 5000)),
|
|
166
|
-
]);
|
|
167
|
-
} catch {
|
|
168
|
-
// 回退:通过 CDP Target API 获取
|
|
169
|
-
const client = await browser.target().createCDPSession();
|
|
170
|
-
const { targetInfos } = await client.send('Target.getTargets');
|
|
171
|
-
log(`[INFO] CDP 回退: 共 ${targetInfos.filter(t => t.type === 'page').length} 个标签页`);
|
|
172
|
-
for (const t of targetInfos.filter(t => t.type === 'page')) {
|
|
173
|
-
log(` ${t.url}`);
|
|
174
|
-
if (t.title) log(` title: ${t.title}`);
|
|
175
|
-
}
|
|
176
|
-
try { await client.detach(); } catch {}
|
|
177
|
-
return { total: targetInfos.filter(t => t.type === 'page').length };
|
|
178
|
-
}
|
|
179
|
-
log(`[INFO] 共 ${pages.length} 个标签页`);
|
|
180
|
-
const result = [];
|
|
181
|
-
for (let i = 0; i < pages.length; i++) {
|
|
182
|
-
const p = pages[i];
|
|
183
|
-
let title = '';
|
|
184
|
-
try { title = await p.title(); } catch {}
|
|
185
|
-
result.push({ index: i, url: p.url(), title });
|
|
186
|
-
log(`[${i}] ${p.url()}`);
|
|
187
|
-
if (title) log(` title: ${title}`);
|
|
188
|
-
}
|
|
189
|
-
return { pages: result, total: result.length };
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// ===== Action: diagnose =====
|
|
193
|
-
async function diagnose(browser) {
|
|
194
|
-
ensureDir(OUTPUT_DIR);
|
|
195
|
-
const page = await findPage(browser);
|
|
196
|
-
log(`[PAGE] ${page.url()}`);
|
|
197
|
-
|
|
198
|
-
// 收集数据
|
|
199
|
-
const pageErrors = [];
|
|
200
|
-
const consoleMessages = [];
|
|
201
|
-
const failedRequests = [];
|
|
202
|
-
const apiRequests = [];
|
|
203
|
-
const resourceRequests = [];
|
|
204
|
-
|
|
205
|
-
page.on('pageerror', err => {
|
|
206
|
-
pageErrors.push({ message: err.message, stack: err.stack?.split('\n').slice(0, 5).join('\n') });
|
|
207
|
-
});
|
|
208
|
-
page.on('console', msg => {
|
|
209
|
-
const type = msg.type();
|
|
210
|
-
const text = msg.text().substring(0, 500);
|
|
211
|
-
consoleMessages.push({ type, text });
|
|
212
|
-
});
|
|
213
|
-
page.on('response', resp => {
|
|
214
|
-
const url = resp.url();
|
|
215
|
-
const status = resp.status();
|
|
216
|
-
const type = resp.request().resourceType();
|
|
217
|
-
if (type === 'xhr' || type === 'fetch') {
|
|
218
|
-
apiRequests.push({ status, url, method: resp.request().method() });
|
|
219
|
-
}
|
|
220
|
-
if (type === 'stylesheet' || type === 'script') {
|
|
221
|
-
resourceRequests.push({ status, url, type });
|
|
222
|
-
}
|
|
223
|
-
if (status >= 400) {
|
|
224
|
-
failedRequests.push({ status, url, type });
|
|
225
|
-
}
|
|
226
|
-
});
|
|
227
|
-
page.on('requestfailed', req => {
|
|
228
|
-
failedRequests.push({ status: 'ABORTED', url: req.url(), error: req.failure()?.errorText });
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
// 刷新页面收集
|
|
232
|
-
log('[INFO] 刷新页面收集诊断数据...');
|
|
233
|
-
try {
|
|
234
|
-
await page.reload({ waitUntil: 'load', timeout: 30000 });
|
|
235
|
-
} catch (e) {
|
|
236
|
-
log(`[WARN] 刷新超时: ${e.message.substring(0, 100)}`);
|
|
237
|
-
}
|
|
238
|
-
await new Promise(r => setTimeout(r, WAIT_MS));
|
|
239
|
-
|
|
240
|
-
// 截图
|
|
241
|
-
const screenshotPath = path.join(OUTPUT_DIR, 'diagnose-screenshot.png');
|
|
242
|
-
await page.screenshot({ path: screenshotPath, fullPage: false });
|
|
243
|
-
log(`[OK] 截图: ${screenshotPath}`);
|
|
244
|
-
|
|
245
|
-
// 检查 iframe
|
|
246
|
-
const frames = page.frames();
|
|
247
|
-
const iframeInfo = [];
|
|
248
|
-
for (const frame of frames) {
|
|
249
|
-
const url = frame.url();
|
|
250
|
-
if (url === page.url() || url === 'about:blank') continue;
|
|
251
|
-
let bodyText = '';
|
|
252
|
-
let rootContent = '';
|
|
253
|
-
try {
|
|
254
|
-
const info = await frame.evaluate(() => ({
|
|
255
|
-
bodyText: document.body?.innerText?.substring(0, 200) || '',
|
|
256
|
-
rootHTML: document.getElementById('root')?.innerHTML?.substring(0, 200) || 'no root',
|
|
257
|
-
hasReactRoot: !document.getElementById('root')?.innerHTML?.includes('loading-spinner'),
|
|
258
|
-
antdElements: document.querySelectorAll('[class*="ant-"]').length,
|
|
259
|
-
}));
|
|
260
|
-
bodyText = info.bodyText;
|
|
261
|
-
rootContent = info.rootHTML;
|
|
262
|
-
iframeInfo.push({ url, ...info });
|
|
263
|
-
} catch (e) {
|
|
264
|
-
iframeInfo.push({ url, error: e.message.substring(0, 100) });
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// 输出结果
|
|
269
|
-
const errors = consoleMessages.filter(m => m.type === 'error');
|
|
270
|
-
const warnings = consoleMessages.filter(m => m.type === 'warning');
|
|
271
|
-
|
|
272
|
-
const result = {
|
|
273
|
-
url: page.url(),
|
|
274
|
-
timestamp: new Date().toISOString(),
|
|
275
|
-
screenshot: screenshotPath,
|
|
276
|
-
pageErrors,
|
|
277
|
-
consoleErrors: errors,
|
|
278
|
-
consoleWarnings: warnings,
|
|
279
|
-
consoleTotal: consoleMessages.length,
|
|
280
|
-
failedRequests,
|
|
281
|
-
apiRequests,
|
|
282
|
-
resourceStatus: resourceRequests.map(r => ({
|
|
283
|
-
status: r.status, type: r.type, name: r.url.split('/').pop()
|
|
284
|
-
})),
|
|
285
|
-
iframes: iframeInfo,
|
|
286
|
-
summary: {
|
|
287
|
-
jsErrors: pageErrors.length,
|
|
288
|
-
consoleErrors: consoleMessages.filter(m => m.type === 'error').length,
|
|
289
|
-
failedNetwork: failedRequests.length,
|
|
290
|
-
totalAPI: apiRequests.length,
|
|
291
|
-
failedAPI: apiRequests.filter(r => r.status >= 400).length,
|
|
292
|
-
totalResources: resourceRequests.length,
|
|
293
|
-
failedResources: resourceRequests.filter(r => r.status >= 400).length,
|
|
294
|
-
}
|
|
295
|
-
};
|
|
296
|
-
|
|
297
|
-
// 打印摘要
|
|
298
|
-
log('\n========== 诊断结果 ==========');
|
|
299
|
-
log(`JS 异常: ${result.summary.jsErrors}`);
|
|
300
|
-
pageErrors.forEach(e => log(` [PAGEERROR] ${e.message}`));
|
|
301
|
-
log(`Console 错误: ${errors.length}`);
|
|
302
|
-
errors.forEach(e => log(` [ERROR] ${e.text}`));
|
|
303
|
-
log(`Console 警告: ${warnings.length}`);
|
|
304
|
-
warnings.forEach(e => log(` [WARN] ${e.text}`));
|
|
305
|
-
log(`Console 消息总数: ${consoleMessages.length}`);
|
|
306
|
-
log(`失败网络请求: ${result.summary.failedNetwork}`);
|
|
307
|
-
failedRequests.forEach(r => log(` [${r.status}] ${r.url}`));
|
|
308
|
-
log(`API 请求: ${result.summary.totalAPI} (失败: ${result.summary.failedAPI})`);
|
|
309
|
-
apiRequests.forEach(r => {
|
|
310
|
-
const marker = r.status >= 400 ? '❌' : '✅';
|
|
311
|
-
log(` ${marker} [${r.status}] ${r.method} ${r.url}`);
|
|
312
|
-
});
|
|
313
|
-
log(`CSS/JS 资源: ${result.summary.totalResources} (失败: ${result.summary.failedResources})`);
|
|
314
|
-
resourceRequests.filter(r => r.status >= 400).forEach(r => log(` ❌ [${r.status}] ${r.url}`));
|
|
315
|
-
if (iframeInfo.length > 0) {
|
|
316
|
-
log(`\niframe 信息:`);
|
|
317
|
-
iframeInfo.forEach(f => log(` ${f.url} → ${f.error || (f.hasReactRoot ? 'React已挂载' : '未挂载')}`));
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// 保存结果
|
|
321
|
-
if (OUTPUT) {
|
|
322
|
-
fs.writeFileSync(OUTPUT, JSON.stringify(result, null, 2));
|
|
323
|
-
log(`\n[OK] 结果已保存: ${OUTPUT}`);
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
return result;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// ===== Action: screenshot =====
|
|
330
|
-
async function screenshot(browser) {
|
|
331
|
-
ensureDir(OUTPUT_DIR);
|
|
332
|
-
const page = await findPage(browser);
|
|
333
|
-
const filename = getArg('filename', 'screenshot.png');
|
|
334
|
-
const fullPath = path.join(OUTPUT_DIR, filename);
|
|
335
|
-
const fullPage = hasFlag('full-page');
|
|
336
|
-
await page.screenshot({ path: fullPath, fullPage });
|
|
337
|
-
log(`[OK] 截图已保存: ${fullPath}`);
|
|
338
|
-
return { filepath: fullPath };
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// ===== Action: api-check =====
|
|
342
|
-
async function apiCheck(browser) {
|
|
343
|
-
const page = await findPage(browser);
|
|
344
|
-
log(`[PAGE] ${page.url()}`);
|
|
345
|
-
|
|
346
|
-
const requests = [];
|
|
347
|
-
const failed = [];
|
|
348
|
-
|
|
349
|
-
page.on('response', resp => {
|
|
350
|
-
const type = resp.request().resourceType();
|
|
351
|
-
if (type === 'xhr' || type === 'fetch') {
|
|
352
|
-
requests.push({
|
|
353
|
-
status: resp.status(),
|
|
354
|
-
url: resp.url(),
|
|
355
|
-
method: resp.request().method(),
|
|
356
|
-
frameUrl: resp.request().frame()?.url?.() || '',
|
|
357
|
-
});
|
|
358
|
-
}
|
|
359
|
-
});
|
|
360
|
-
page.on('requestfailed', req => {
|
|
361
|
-
failed.push({ url: req.url(), error: req.failure()?.errorText });
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
log('[INFO] 刷新页面收集 API 请求...');
|
|
365
|
-
try { await page.reload({ waitUntil: 'load', timeout: 30000 }); } catch {}
|
|
366
|
-
await new Promise(r => setTimeout(r, WAIT_MS));
|
|
367
|
-
|
|
368
|
-
log('\n========== API 请求 ==========');
|
|
369
|
-
requests.forEach(r => {
|
|
370
|
-
const marker = r.status >= 400 ? '❌' : '✅';
|
|
371
|
-
const isIframe = r.frameUrl && r.frameUrl !== page.url() ? ' [iframe]' : '';
|
|
372
|
-
log(`${marker} [${r.status}] ${r.method} ${r.url}${isIframe}`);
|
|
373
|
-
});
|
|
374
|
-
if (failed.length > 0) {
|
|
375
|
-
log('\n========== 失败请求 ==========');
|
|
376
|
-
failed.forEach(r => log(`[FAILED] ${r.url} → ${r.error}`));
|
|
377
|
-
}
|
|
378
|
-
log(`\n总计: ${requests.length} 请求, 失败: ${requests.filter(r => r.status >= 400).length + failed.length}`);
|
|
379
|
-
|
|
380
|
-
return { requests, failed };
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// ===== Action: plan =====
|
|
384
|
-
async function executePlan(browser) {
|
|
385
|
-
const planPath = PLAN_FILE;
|
|
386
|
-
if (!planPath) { log('[ERROR] --plan-file 未指定'); return; }
|
|
387
|
-
const plan = JSON.parse(fs.readFileSync(planPath, 'utf-8'));
|
|
388
|
-
ensureDir(plan.options?.outputDir || OUTPUT_DIR);
|
|
389
|
-
|
|
390
|
-
log(`[PLAN] ${plan.description || plan.planId}`);
|
|
391
|
-
log(`[STEPS] ${plan.steps.length} 个步骤`);
|
|
392
|
-
|
|
393
|
-
const results = [];
|
|
394
|
-
let currentPage = await findPage(browser);
|
|
395
|
-
|
|
396
|
-
for (let i = 0; i < plan.steps.length; i++) {
|
|
397
|
-
const step = plan.steps[i];
|
|
398
|
-
const start = Date.now();
|
|
399
|
-
try {
|
|
400
|
-
let result = {};
|
|
401
|
-
switch (step.type) {
|
|
402
|
-
case 'navigate':
|
|
403
|
-
await currentPage.goto(step.url, { waitUntil: step.waitUntil || 'domcontentloaded', timeout: 30000 });
|
|
404
|
-
result = { url: currentPage.url() };
|
|
405
|
-
break;
|
|
406
|
-
case 'screenshot':
|
|
407
|
-
const fp = path.join(plan.options?.outputDir || OUTPUT_DIR, step.filename || `step-${i}.png`);
|
|
408
|
-
await currentPage.screenshot({ path: fp, fullPage: step.fullPage });
|
|
409
|
-
result = { filepath: fp };
|
|
410
|
-
break;
|
|
411
|
-
case 'waitFor':
|
|
412
|
-
if (step.selector) await currentPage.waitForSelector(step.selector, { timeout: step.timeout || 10000 });
|
|
413
|
-
result = { matched: true };
|
|
414
|
-
break;
|
|
415
|
-
case 'evaluate':
|
|
416
|
-
const evalResult = await currentPage.evaluate(step.expression);
|
|
417
|
-
result = { result: evalResult };
|
|
418
|
-
break;
|
|
419
|
-
case 'click':
|
|
420
|
-
await currentPage.click(step.selector);
|
|
421
|
-
result = { clicked: true };
|
|
422
|
-
break;
|
|
423
|
-
case 'checkElement':
|
|
424
|
-
const count = await currentPage.$$eval(step.selector, els => els.length).catch(() => 0);
|
|
425
|
-
const passed = step.expect?.minCount ? count >= step.expect.minCount : count > 0;
|
|
426
|
-
result = { passed, elementCount: count };
|
|
427
|
-
break;
|
|
428
|
-
default:
|
|
429
|
-
result = { skipped: true, reason: `unknown step type: ${step.type}` };
|
|
430
|
-
}
|
|
431
|
-
const duration = Date.now() - start;
|
|
432
|
-
results.push({ index: i, step: step.type, status: 'PASS', duration, ...result });
|
|
433
|
-
log(` [${i + 1}/${plan.steps.length}] ✅ ${step.type} (${duration}ms)`);
|
|
434
|
-
} catch (e) {
|
|
435
|
-
const duration = Date.now() - start;
|
|
436
|
-
results.push({ index: i, step: step.type, status: 'FAIL', duration, error: e.message });
|
|
437
|
-
log(` [${i + 1}/${plan.steps.length}] ❌ ${step.type} (${duration}ms) → ${e.message.substring(0, 100)}`);
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
const summary = {
|
|
442
|
-
total: results.length,
|
|
443
|
-
passed: results.filter(r => r.status === 'PASS').length,
|
|
444
|
-
failed: results.filter(r => r.status === 'FAIL').length,
|
|
445
|
-
};
|
|
446
|
-
log(`\n[RESULT] ${summary.passed}/${summary.total} 通过`);
|
|
447
|
-
|
|
448
|
-
if (OUTPUT) {
|
|
449
|
-
fs.writeFileSync(OUTPUT, JSON.stringify({ steps: results, summary }, null, 2));
|
|
450
|
-
}
|
|
451
|
-
return { steps: results, summary };
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
// ===== 智能选择器解析(融合 Playwright 风格) =====
|
|
455
|
-
async function smartResolve(pageOrFrame, selector) {
|
|
456
|
-
if (!selector) return null;
|
|
457
|
-
if (selector.startsWith('text=')) {
|
|
458
|
-
const text = selector.slice(5).replace(/^["']|["']$/g, '');
|
|
459
|
-
return pageOrFrame.evaluateHandle(t => {
|
|
460
|
-
const all = document.querySelectorAll('a, button, [role="button"], input, [class*="btn"], span, div, p');
|
|
461
|
-
return Array.from(all).find(el => el.textContent?.includes(t) && el.offsetHeight > 0) || null;
|
|
462
|
-
}, text);
|
|
463
|
-
}
|
|
464
|
-
if (selector.startsWith('role=')) {
|
|
465
|
-
return pageOrFrame.$(`[role="${selector.slice(5)}"]`);
|
|
466
|
-
}
|
|
467
|
-
if (selector.startsWith('~')) {
|
|
468
|
-
return pageOrFrame.$(`[class*="${selector.slice(1).replace(/^\./, '')}"]`);
|
|
469
|
-
}
|
|
470
|
-
return pageOrFrame.$(selector);
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
// ===== Action: snapshot (结构化 DOM 快照) =====
|
|
474
|
-
async function snapshot(browser) {
|
|
475
|
-
const page = await findPage(browser);
|
|
476
|
-
const selector = getArg('selector', 'body');
|
|
477
|
-
const maxDepth = parseInt(getArg('depth', '5'), 10);
|
|
478
|
-
log(`[PAGE] ${page.url()}`);
|
|
479
|
-
|
|
480
|
-
const tree = await page.evaluate((rootSel, depth) => {
|
|
481
|
-
function buildTree(el, d) {
|
|
482
|
-
if (d > depth || !el) return null;
|
|
483
|
-
const tag = el.tagName?.toLowerCase();
|
|
484
|
-
if (!tag || ['script','style','svg','path'].includes(tag)) return null;
|
|
485
|
-
const attrs = {};
|
|
486
|
-
for (const a of (el.attributes || [])) {
|
|
487
|
-
if (['id','class','href','src','role','aria-label','data-testid'].includes(a.name)) {
|
|
488
|
-
attrs[a.name] = a.value.substring(0, 100);
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
// 直接文本内容
|
|
492
|
-
let text = '';
|
|
493
|
-
for (const node of el.childNodes) {
|
|
494
|
-
if (node.nodeType === 3) text += node.textContent.trim();
|
|
495
|
-
}
|
|
496
|
-
// 子元素
|
|
497
|
-
const children = [];
|
|
498
|
-
for (const child of el.children) {
|
|
499
|
-
const childTree = buildTree(child, d + 1);
|
|
500
|
-
if (childTree) children.push(childTree);
|
|
501
|
-
}
|
|
502
|
-
// 可交互性
|
|
503
|
-
const interactive = ['a','button','input','select','textarea'].includes(tag) ||
|
|
504
|
-
el.getAttribute('role') === 'button' || el.onclick || el.style.cursor === 'pointer';
|
|
505
|
-
// 可见性
|
|
506
|
-
const rect = el.getBoundingClientRect();
|
|
507
|
-
const visible = rect.width > 0 && rect.height > 0;
|
|
508
|
-
return { tag, attrs, text: text.substring(0, 80), children, interactive, visible, depth: d };
|
|
509
|
-
}
|
|
510
|
-
const root = document.querySelector(rootSel) || document.body;
|
|
511
|
-
return buildTree(root, 0);
|
|
512
|
-
}, selector, maxDepth);
|
|
513
|
-
|
|
514
|
-
// 打印树形结构
|
|
515
|
-
function printTree(node, indent = '') {
|
|
516
|
-
if (!node) return;
|
|
517
|
-
const cls = node.attrs.class ? `.${node.attrs.class.split(' ')[0].substring(0, 30)}` : '';
|
|
518
|
-
const id = node.attrs.id ? `#${node.attrs.id}` : '';
|
|
519
|
-
const role = node.attrs.role ? `[role=${node.attrs.role}]` : '';
|
|
520
|
-
const interactive = node.interactive ? ' \u{1F517}' : '';
|
|
521
|
-
const vis = node.visible ? '' : ' [hidden]';
|
|
522
|
-
const txt = node.text ? ` "${node.text.substring(0, 40)}"` : '';
|
|
523
|
-
log(`${indent}<${node.tag}${id}${cls}${role}>${txt}${interactive}${vis}`);
|
|
524
|
-
for (const child of (node.children || [])) {
|
|
525
|
-
printTree(child, indent + ' ');
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
log('\n========== DOM 快照 ==========');
|
|
529
|
-
printTree(tree);
|
|
530
|
-
|
|
531
|
-
if (OUTPUT) {
|
|
532
|
-
fs.writeFileSync(OUTPUT, JSON.stringify(tree, null, 2));
|
|
533
|
-
log(`\n[OK] 结果已保存: ${OUTPUT}`);
|
|
534
|
-
}
|
|
535
|
-
return tree;
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
// ===== Action: interact (智能交互) =====
|
|
539
|
-
async function interact(browser) {
|
|
540
|
-
const page = await findPage(browser);
|
|
541
|
-
log(`[PAGE] ${page.url()}`);
|
|
542
|
-
|
|
543
|
-
const clickSel = getArg('click', null);
|
|
544
|
-
const typeText = getArg('type', null);
|
|
545
|
-
const hoverSel = getArg('hover', null);
|
|
546
|
-
const scrollDir = getArg('scroll', null);
|
|
547
|
-
const scrollAmt = parseInt(getArg('scroll-amount', '300'), 10);
|
|
548
|
-
|
|
549
|
-
if (clickSel) {
|
|
550
|
-
const el = await smartResolve(page, clickSel);
|
|
551
|
-
if (el) {
|
|
552
|
-
await el.click();
|
|
553
|
-
log(`[OK] 点击: ${clickSel}`);
|
|
554
|
-
await new Promise(r => setTimeout(r, 1000));
|
|
555
|
-
} else {
|
|
556
|
-
log(`[FAIL] 未找到元素: ${clickSel}`);
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
if (typeText) {
|
|
560
|
-
const target = clickSel ? await smartResolve(page, clickSel) : await page.$('input:focus, textarea:focus');
|
|
561
|
-
if (target) {
|
|
562
|
-
await target.type(typeText, { delay: 50 });
|
|
563
|
-
log(`[OK] 输入: "${typeText}"`);
|
|
564
|
-
} else {
|
|
565
|
-
log(`[FAIL] 未找到输入目标`);
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
if (hoverSel) {
|
|
569
|
-
const el = await smartResolve(page, hoverSel);
|
|
570
|
-
if (el) {
|
|
571
|
-
await el.hover();
|
|
572
|
-
log(`[OK] 悬停: ${hoverSel}`);
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
if (scrollDir) {
|
|
576
|
-
const dx = scrollDir === 'left' ? -scrollAmt : scrollDir === 'right' ? scrollAmt : 0;
|
|
577
|
-
const dy = scrollDir === 'up' ? -scrollAmt : scrollDir === 'down' ? scrollAmt : 0;
|
|
578
|
-
await page.evaluate(({ dx, dy }) => window.scrollBy(dx, dy), { dx, dy });
|
|
579
|
-
log(`[OK] 滚动: ${scrollDir} ${scrollAmt}px`);
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
// 交互后截图
|
|
583
|
-
if (hasFlag('screenshot-after')) {
|
|
584
|
-
ensureDir(OUTPUT_DIR);
|
|
585
|
-
const fp = path.join(OUTPUT_DIR, 'after-interact.png');
|
|
586
|
-
await page.screenshot({ path: fp });
|
|
587
|
-
log(`[OK] 截图: ${fp}`);
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
// ===== Action: perf (性能指标) =====
|
|
592
|
-
async function perf(browser) {
|
|
593
|
-
const page = await findPage(browser);
|
|
594
|
-
log(`[PAGE] ${page.url()}`);
|
|
595
|
-
|
|
596
|
-
// 收集性能指标
|
|
597
|
-
const perfMetrics = await page.evaluate(() => {
|
|
598
|
-
const perf = performance.getEntriesByType('navigation')[0];
|
|
599
|
-
const paint = performance.getEntriesByType('paint');
|
|
600
|
-
const fcp = paint.find(p => p.name === 'first-contentful-paint');
|
|
601
|
-
// LCP 通过 PerformanceObserver 获取
|
|
602
|
-
const resources = performance.getEntriesByType('resource');
|
|
603
|
-
return {
|
|
604
|
-
// 导航时间
|
|
605
|
-
dns: perf?.domainEnd - perf?.domainStart || 0,
|
|
606
|
-
tcp: perf?.connectEnd - perf?.connectStart || 0,
|
|
607
|
-
ttfb: perf?.responseStart - perf?.requestStart || 0,
|
|
608
|
-
domReady: perf?.domContentLoadedEventEnd - perf?.startTime || 0,
|
|
609
|
-
loadComplete: perf?.loadEventEnd - perf?.startTime || 0,
|
|
610
|
-
// 绘制
|
|
611
|
-
fcp: fcp?.startTime || null,
|
|
612
|
-
// 资源统计
|
|
613
|
-
totalResources: resources.length,
|
|
614
|
-
totalTransferSize: resources.reduce((sum, r) => sum + (r.transferSize || 0), 0),
|
|
615
|
-
slowResources: resources.filter(r => r.duration > 1000).map(r => ({
|
|
616
|
-
name: r.name.split('/').pop(), duration: Math.round(r.duration), size: r.transferSize
|
|
617
|
-
})),
|
|
618
|
-
};
|
|
619
|
-
});
|
|
620
|
-
|
|
621
|
-
// Web Vitals (通过 CDP)
|
|
622
|
-
let webVitals = {};
|
|
623
|
-
try {
|
|
624
|
-
const client = await page.target().createCDPSession();
|
|
625
|
-
await client.send('Performance.enable');
|
|
626
|
-
const metrics = await client.send('Performance.getMetrics');
|
|
627
|
-
const m = {};
|
|
628
|
-
metrics.metrics.forEach(item => { m[item.name] = item.value; });
|
|
629
|
-
webVitals = {
|
|
630
|
-
layoutCount: m.LayoutCount || 0,
|
|
631
|
-
recalcCount: m.RecalcLayoutCount || 0,
|
|
632
|
-
jsHeapUsed: Math.round((m.JSHeapUsedSize || 0) / 1024 / 1024) + 'MB',
|
|
633
|
-
jsHeapTotal: Math.round((m.JSHeapTotalSize || 0) / 1024 / 1024) + 'MB',
|
|
634
|
-
taskDuration: (m.TaskDuration || 0).toFixed(2) + 's',
|
|
635
|
-
};
|
|
636
|
-
await client.send('Performance.disable');
|
|
637
|
-
client.detach();
|
|
638
|
-
} catch {}
|
|
639
|
-
|
|
640
|
-
log('\n========== 性能指标 ==========');
|
|
641
|
-
log(`DNS: ${Math.round(perfMetrics.dns)}ms`);
|
|
642
|
-
log(`TCP: ${Math.round(perfMetrics.tcp)}ms`);
|
|
643
|
-
log(`TTFB: ${Math.round(perfMetrics.ttfb)}ms`);
|
|
644
|
-
log(`DOM Ready: ${Math.round(perfMetrics.domReady)}ms`);
|
|
645
|
-
log(`Load Complete: ${Math.round(perfMetrics.loadComplete)}ms`);
|
|
646
|
-
log(`FCP: ${perfMetrics.fcp ? Math.round(perfMetrics.fcp) + 'ms' : 'N/A'}`);
|
|
647
|
-
log(`\n资源总数: ${perfMetrics.totalResources}`);
|
|
648
|
-
log(`传输总量: ${Math.round(perfMetrics.totalTransferSize / 1024)}KB`);
|
|
649
|
-
if (perfMetrics.slowResources.length > 0) {
|
|
650
|
-
log(`\n慢资源 (>1s):`);
|
|
651
|
-
perfMetrics.slowResources.forEach(r => log(` ${r.duration}ms ${r.name} (${Math.round((r.size||0)/1024)}KB)`));
|
|
652
|
-
}
|
|
653
|
-
if (Object.keys(webVitals).length > 0) {
|
|
654
|
-
log('\n========== Chrome 内部指标 ==========');
|
|
655
|
-
Object.entries(webVitals).forEach(([k, v]) => log(` ${k}: ${v}`));
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
return { perfMetrics, webVitals };
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
// ===== Action: watch (持续监控) =====
|
|
662
|
-
async function watch(browser) {
|
|
663
|
-
const page = await findPage(browser);
|
|
664
|
-
const interval = parseInt(getArg('interval', '5000'), 10);
|
|
665
|
-
const maxRounds = parseInt(getArg('max-rounds', '60'), 10);
|
|
666
|
-
ensureDir(OUTPUT_DIR);
|
|
667
|
-
log(`[PAGE] ${page.url()}`);
|
|
668
|
-
log(`[WATCH] 间隔=${interval}ms, 最大轮数=${maxRounds}`);
|
|
669
|
-
|
|
670
|
-
const errors = [];
|
|
671
|
-
page.on('pageerror', err => errors.push({ time: new Date().toISOString(), error: err.message }));
|
|
672
|
-
page.on('console', msg => {
|
|
673
|
-
if (msg.type() === 'error') errors.push({ time: new Date().toISOString(), type: 'console', text: msg.text() });
|
|
674
|
-
});
|
|
675
|
-
|
|
676
|
-
for (let i = 1; i <= maxRounds; i++) {
|
|
677
|
-
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
678
|
-
const fp = path.join(OUTPUT_DIR, `watch-${String(i).padStart(3, '0')}-${ts}.png`);
|
|
679
|
-
await page.screenshot({ path: fp });
|
|
680
|
-
const errCount = errors.length;
|
|
681
|
-
log(`[${i}/${maxRounds}] 截图 | 累计错误: ${errCount}`);
|
|
682
|
-
if (errCount > 0 && i === 1) {
|
|
683
|
-
errors.forEach(e => log(` [ERROR] ${e.error || e.text}`));
|
|
684
|
-
}
|
|
685
|
-
await new Promise(r => setTimeout(r, interval));
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
log(`\n========== 监控结束 ==========`);
|
|
689
|
-
log(`总轮数: ${maxRounds}, 累计错误: ${errors.length}`);
|
|
690
|
-
errors.forEach(e => log(` [${e.time}] ${e.error || e.text}`));
|
|
691
|
-
return { rounds: maxRounds, errors };
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
// ===== 主入口 =====
|
|
695
|
-
(async () => {
|
|
696
|
-
// 检测 CDP
|
|
697
|
-
const cdpInfo = await checkCDP(CDP_URL);
|
|
698
|
-
if (!cdpInfo) {
|
|
699
|
-
log(`[ERROR] 无法连接到 CDP: ${CDP_URL}`);
|
|
700
|
-
log('请确保浏览器已启动调试端口:');
|
|
701
|
-
log(' Windows Edge: "C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe" --remote-debugging-port=9222');
|
|
702
|
-
log(' Windows Chrome: "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe" --remote-debugging-port=9222');
|
|
703
|
-
process.exit(1);
|
|
704
|
-
}
|
|
705
|
-
log(`[CDP] ${cdpInfo.Browser || 'connected'} (${CDP_URL})`);
|
|
706
|
-
|
|
707
|
-
// 获取 WebSocket URL
|
|
708
|
-
let wsUrl;
|
|
709
|
-
try {
|
|
710
|
-
const versionData = await new Promise((resolve, reject) => {
|
|
711
|
-
const req = http.get(CDP_URL + '/json/version', { timeout: 3000 }, res => {
|
|
712
|
-
let d = ''; res.on('data', c => d += c); res.on('end', () => resolve(JSON.parse(d)));
|
|
713
|
-
});
|
|
714
|
-
req.on('error', reject);
|
|
715
|
-
});
|
|
716
|
-
wsUrl = versionData.webSocketDebuggerUrl;
|
|
717
|
-
} catch {}
|
|
718
|
-
|
|
719
|
-
const browser = await Promise.race([
|
|
720
|
-
puppeteer.connect({ browserWSEndpoint: wsUrl || CDP_URL, defaultViewport: null }),
|
|
721
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error('CDP WebSocket 连接超时 (10s)')), 10000)),
|
|
722
|
-
]);
|
|
723
|
-
log('[OK] 浏览器已连接');
|
|
724
|
-
|
|
725
|
-
let result;
|
|
726
|
-
switch (ACTION) {
|
|
727
|
-
case 'list-pages': result = await listPages(browser); break;
|
|
728
|
-
case 'diagnose': result = await diagnose(browser); break;
|
|
729
|
-
case 'screenshot': result = await screenshot(browser); break;
|
|
730
|
-
case 'api-check': result = await apiCheck(browser); break;
|
|
731
|
-
case 'snapshot': result = await snapshot(browser); break;
|
|
732
|
-
case 'interact': result = await interact(browser); break;
|
|
733
|
-
case 'perf': result = await perf(browser); break;
|
|
734
|
-
case 'watch': result = await watch(browser); break;
|
|
735
|
-
case 'plan': result = await executePlan(browser); break;
|
|
736
|
-
default: log(`[ERROR] 未知 action: ${ACTION}`);
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
browser.disconnect();
|
|
740
|
-
log('\n[DONE]');
|
|
741
|
-
process.exit(0);
|
|
742
|
-
})();
|