@szc-ft/mcp-szcd-client 0.27.1 → 0.27.3
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/commands/szcd-mcp-browser-test.md +1 -1
- package/lib/browser-engine.js +831 -5
- package/lib/shared-deps.js +80 -40
- package/lib/visual-compare.js +3 -3
- package/local-browser-executor.js +321 -7
- package/opencode-extension/commands/szcd-mcp-browser-test.md +1 -1
- package/opencode-extension/skills/local-browser-test/SKILL.md +199 -15
- package/opencode-extension/skills/local-browser-test/local-browser-executor-old.cjs +395 -0
- package/opencode-extension/skills/local-browser-test/local-browser-executor.cjs +348 -16
- package/package.json +1 -1
- package/qwen-extension/commands/szcd-mcp-browser-test.md +1 -1
- package/qwen-extension/qwen-extension.json +1 -1
- package/qwen-extension/skills/local-browser-test/SKILL.md +199 -15
- package/qwen-extension/skills/local-browser-test/local-browser-executor-old.cjs +395 -0
- package/qwen-extension/skills/local-browser-test/local-browser-executor.cjs +348 -16
- package/standard-skill/local-browser-test/SKILL.md +199 -15
- package/standard-skill/local-browser-test/local-browser-executor.cjs +348 -16
|
@@ -1,24 +1,41 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* local-browser-executor.
|
|
3
|
+
* local-browser-executor.cjs - 本地浏览器自动化执行器
|
|
4
4
|
*
|
|
5
5
|
* 功能:
|
|
6
|
-
* --action diagnose
|
|
7
|
-
* --action list-pages
|
|
8
|
-
* --action screenshot
|
|
9
|
-
* --action api-check
|
|
10
|
-
* --action
|
|
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
|
|
11
15
|
*
|
|
12
16
|
* 连接方式:
|
|
13
|
-
* --cdp-url http://localhost:9222 (
|
|
14
|
-
* --auto-detect 自动检测 Chrome/Edge 进程
|
|
17
|
+
* --cdp-url http://localhost:9222 (默认 9222)
|
|
15
18
|
*
|
|
16
19
|
* 通用参数:
|
|
17
|
-
* --url <url>
|
|
18
|
-
* --url-contains <s>
|
|
19
|
-
* --wait <ms>
|
|
20
|
-
* --output <path>
|
|
21
|
-
* --output-dir <dir>
|
|
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)
|
|
22
39
|
*/
|
|
23
40
|
|
|
24
41
|
const puppeteer = require('puppeteer-core');
|
|
@@ -65,7 +82,41 @@ function checkCDP(url) {
|
|
|
65
82
|
}
|
|
66
83
|
|
|
67
84
|
async function findPage(browser) {
|
|
68
|
-
|
|
85
|
+
let pages;
|
|
86
|
+
try {
|
|
87
|
+
pages = await Promise.race([
|
|
88
|
+
browser.pages(),
|
|
89
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('pages() timeout')), 8000)),
|
|
90
|
+
]);
|
|
91
|
+
} catch {
|
|
92
|
+
// browser.pages() 超时,尝试用 targets() 找到目标再 .page()
|
|
93
|
+
try {
|
|
94
|
+
const targets = browser.targets();
|
|
95
|
+
let matchTarget = null;
|
|
96
|
+
if (URL_CONTAINS) {
|
|
97
|
+
matchTarget = targets.find(t => t.type() === 'page' && t.url().includes(URL_CONTAINS));
|
|
98
|
+
}
|
|
99
|
+
if (TARGET_URL && !matchTarget) {
|
|
100
|
+
matchTarget = targets.find(t => t.type() === 'page' && t.url().includes(TARGET_URL));
|
|
101
|
+
}
|
|
102
|
+
if (!matchTarget) {
|
|
103
|
+
matchTarget = targets.find(t => t.type() === 'page' && !t.url().startsWith('about:') && !t.url().startsWith('devtools:'));
|
|
104
|
+
}
|
|
105
|
+
if (matchTarget) {
|
|
106
|
+
const page = await matchTarget.page();
|
|
107
|
+
if (page) return page;
|
|
108
|
+
}
|
|
109
|
+
} catch (e) {
|
|
110
|
+
log(`[WARN] targets() 回退失败: ${e.message}`);
|
|
111
|
+
}
|
|
112
|
+
// 最终回退
|
|
113
|
+
if (TARGET_URL) {
|
|
114
|
+
const page = await browser.newPage();
|
|
115
|
+
await page.goto(TARGET_URL, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
|
116
|
+
return page;
|
|
117
|
+
}
|
|
118
|
+
return await browser.newPage();
|
|
119
|
+
}
|
|
69
120
|
if (TARGET_URL) {
|
|
70
121
|
for (const p of pages) {
|
|
71
122
|
if (p.url().includes(TARGET_URL) || p.url() === TARGET_URL) return p;
|
|
@@ -90,7 +141,27 @@ async function findPage(browser) {
|
|
|
90
141
|
|
|
91
142
|
// ===== Action: list-pages =====
|
|
92
143
|
async function listPages(browser) {
|
|
93
|
-
|
|
144
|
+
log('[INFO] 获取标签页列表...');
|
|
145
|
+
// browser.pages() 在大量标签页时可能卡住,用 CDP 直接获取
|
|
146
|
+
let pages;
|
|
147
|
+
try {
|
|
148
|
+
pages = await Promise.race([
|
|
149
|
+
browser.pages(),
|
|
150
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('pages() timeout')), 5000)),
|
|
151
|
+
]);
|
|
152
|
+
} catch {
|
|
153
|
+
// 回退:通过 CDP Target API 获取
|
|
154
|
+
const client = await browser.target().createCDPSession();
|
|
155
|
+
const { targetInfos } = await client.send('Target.getTargets');
|
|
156
|
+
log(`[INFO] CDP 回退: 共 ${targetInfos.filter(t => t.type === 'page').length} 个标签页`);
|
|
157
|
+
for (const t of targetInfos.filter(t => t.type === 'page')) {
|
|
158
|
+
log(` ${t.url}`);
|
|
159
|
+
if (t.title) log(` title: ${t.title}`);
|
|
160
|
+
}
|
|
161
|
+
try { await client.detach(); } catch {}
|
|
162
|
+
return { total: targetInfos.filter(t => t.type === 'page').length };
|
|
163
|
+
}
|
|
164
|
+
log(`[INFO] 共 ${pages.length} 个标签页`);
|
|
94
165
|
const result = [];
|
|
95
166
|
for (let i = 0; i < pages.length; i++) {
|
|
96
167
|
const p = pages[i];
|
|
@@ -365,6 +436,246 @@ async function executePlan(browser) {
|
|
|
365
436
|
return { steps: results, summary };
|
|
366
437
|
}
|
|
367
438
|
|
|
439
|
+
// ===== 智能选择器解析(融合 Playwright 风格) =====
|
|
440
|
+
async function smartResolve(pageOrFrame, selector) {
|
|
441
|
+
if (!selector) return null;
|
|
442
|
+
if (selector.startsWith('text=')) {
|
|
443
|
+
const text = selector.slice(5).replace(/^["']|["']$/g, '');
|
|
444
|
+
return pageOrFrame.evaluateHandle(t => {
|
|
445
|
+
const all = document.querySelectorAll('a, button, [role="button"], input, [class*="btn"], span, div, p');
|
|
446
|
+
return Array.from(all).find(el => el.textContent?.includes(t) && el.offsetHeight > 0) || null;
|
|
447
|
+
}, text);
|
|
448
|
+
}
|
|
449
|
+
if (selector.startsWith('role=')) {
|
|
450
|
+
return pageOrFrame.$(`[role="${selector.slice(5)}"]`);
|
|
451
|
+
}
|
|
452
|
+
if (selector.startsWith('~')) {
|
|
453
|
+
return pageOrFrame.$(`[class*="${selector.slice(1).replace(/^\./, '')}"]`);
|
|
454
|
+
}
|
|
455
|
+
return pageOrFrame.$(selector);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// ===== Action: snapshot (结构化 DOM 快照) =====
|
|
459
|
+
async function snapshot(browser) {
|
|
460
|
+
const page = await findPage(browser);
|
|
461
|
+
const selector = getArg('selector', 'body');
|
|
462
|
+
const maxDepth = parseInt(getArg('depth', '5'), 10);
|
|
463
|
+
log(`[PAGE] ${page.url()}`);
|
|
464
|
+
|
|
465
|
+
const tree = await page.evaluate((rootSel, depth) => {
|
|
466
|
+
function buildTree(el, d) {
|
|
467
|
+
if (d > depth || !el) return null;
|
|
468
|
+
const tag = el.tagName?.toLowerCase();
|
|
469
|
+
if (!tag || ['script','style','svg','path'].includes(tag)) return null;
|
|
470
|
+
const attrs = {};
|
|
471
|
+
for (const a of (el.attributes || [])) {
|
|
472
|
+
if (['id','class','href','src','role','aria-label','data-testid'].includes(a.name)) {
|
|
473
|
+
attrs[a.name] = a.value.substring(0, 100);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
// 直接文本内容
|
|
477
|
+
let text = '';
|
|
478
|
+
for (const node of el.childNodes) {
|
|
479
|
+
if (node.nodeType === 3) text += node.textContent.trim();
|
|
480
|
+
}
|
|
481
|
+
// 子元素
|
|
482
|
+
const children = [];
|
|
483
|
+
for (const child of el.children) {
|
|
484
|
+
const childTree = buildTree(child, d + 1);
|
|
485
|
+
if (childTree) children.push(childTree);
|
|
486
|
+
}
|
|
487
|
+
// 可交互性
|
|
488
|
+
const interactive = ['a','button','input','select','textarea'].includes(tag) ||
|
|
489
|
+
el.getAttribute('role') === 'button' || el.onclick || el.style.cursor === 'pointer';
|
|
490
|
+
// 可见性
|
|
491
|
+
const rect = el.getBoundingClientRect();
|
|
492
|
+
const visible = rect.width > 0 && rect.height > 0;
|
|
493
|
+
return { tag, attrs, text: text.substring(0, 80), children, interactive, visible, depth: d };
|
|
494
|
+
}
|
|
495
|
+
const root = document.querySelector(rootSel) || document.body;
|
|
496
|
+
return buildTree(root, 0);
|
|
497
|
+
}, selector, maxDepth);
|
|
498
|
+
|
|
499
|
+
// 打印树形结构
|
|
500
|
+
function printTree(node, indent = '') {
|
|
501
|
+
if (!node) return;
|
|
502
|
+
const cls = node.attrs.class ? `.${node.attrs.class.split(' ')[0].substring(0, 30)}` : '';
|
|
503
|
+
const id = node.attrs.id ? `#${node.attrs.id}` : '';
|
|
504
|
+
const role = node.attrs.role ? `[role=${node.attrs.role}]` : '';
|
|
505
|
+
const interactive = node.interactive ? ' \u{1F517}' : '';
|
|
506
|
+
const vis = node.visible ? '' : ' [hidden]';
|
|
507
|
+
const txt = node.text ? ` "${node.text.substring(0, 40)}"` : '';
|
|
508
|
+
log(`${indent}<${node.tag}${id}${cls}${role}>${txt}${interactive}${vis}`);
|
|
509
|
+
for (const child of (node.children || [])) {
|
|
510
|
+
printTree(child, indent + ' ');
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
log('\n========== DOM 快照 ==========');
|
|
514
|
+
printTree(tree);
|
|
515
|
+
|
|
516
|
+
if (OUTPUT) {
|
|
517
|
+
fs.writeFileSync(OUTPUT, JSON.stringify(tree, null, 2));
|
|
518
|
+
log(`\n[OK] 结果已保存: ${OUTPUT}`);
|
|
519
|
+
}
|
|
520
|
+
return tree;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// ===== Action: interact (智能交互) =====
|
|
524
|
+
async function interact(browser) {
|
|
525
|
+
const page = await findPage(browser);
|
|
526
|
+
log(`[PAGE] ${page.url()}`);
|
|
527
|
+
|
|
528
|
+
const clickSel = getArg('click', null);
|
|
529
|
+
const typeText = getArg('type', null);
|
|
530
|
+
const hoverSel = getArg('hover', null);
|
|
531
|
+
const scrollDir = getArg('scroll', null);
|
|
532
|
+
const scrollAmt = parseInt(getArg('scroll-amount', '300'), 10);
|
|
533
|
+
|
|
534
|
+
if (clickSel) {
|
|
535
|
+
const el = await smartResolve(page, clickSel);
|
|
536
|
+
if (el) {
|
|
537
|
+
await el.click();
|
|
538
|
+
log(`[OK] 点击: ${clickSel}`);
|
|
539
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
540
|
+
} else {
|
|
541
|
+
log(`[FAIL] 未找到元素: ${clickSel}`);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
if (typeText) {
|
|
545
|
+
const target = clickSel ? await smartResolve(page, clickSel) : await page.$('input:focus, textarea:focus');
|
|
546
|
+
if (target) {
|
|
547
|
+
await target.type(typeText, { delay: 50 });
|
|
548
|
+
log(`[OK] 输入: "${typeText}"`);
|
|
549
|
+
} else {
|
|
550
|
+
log(`[FAIL] 未找到输入目标`);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
if (hoverSel) {
|
|
554
|
+
const el = await smartResolve(page, hoverSel);
|
|
555
|
+
if (el) {
|
|
556
|
+
await el.hover();
|
|
557
|
+
log(`[OK] 悬停: ${hoverSel}`);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
if (scrollDir) {
|
|
561
|
+
const dx = scrollDir === 'left' ? -scrollAmt : scrollDir === 'right' ? scrollAmt : 0;
|
|
562
|
+
const dy = scrollDir === 'up' ? -scrollAmt : scrollDir === 'down' ? scrollAmt : 0;
|
|
563
|
+
await page.evaluate(({ dx, dy }) => window.scrollBy(dx, dy), { dx, dy });
|
|
564
|
+
log(`[OK] 滚动: ${scrollDir} ${scrollAmt}px`);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// 交互后截图
|
|
568
|
+
if (hasFlag('screenshot-after')) {
|
|
569
|
+
ensureDir(OUTPUT_DIR);
|
|
570
|
+
const fp = path.join(OUTPUT_DIR, 'after-interact.png');
|
|
571
|
+
await page.screenshot({ path: fp });
|
|
572
|
+
log(`[OK] 截图: ${fp}`);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// ===== Action: perf (性能指标) =====
|
|
577
|
+
async function perf(browser) {
|
|
578
|
+
const page = await findPage(browser);
|
|
579
|
+
log(`[PAGE] ${page.url()}`);
|
|
580
|
+
|
|
581
|
+
// 收集性能指标
|
|
582
|
+
const perfMetrics = await page.evaluate(() => {
|
|
583
|
+
const perf = performance.getEntriesByType('navigation')[0];
|
|
584
|
+
const paint = performance.getEntriesByType('paint');
|
|
585
|
+
const fcp = paint.find(p => p.name === 'first-contentful-paint');
|
|
586
|
+
// LCP 通过 PerformanceObserver 获取
|
|
587
|
+
const resources = performance.getEntriesByType('resource');
|
|
588
|
+
return {
|
|
589
|
+
// 导航时间
|
|
590
|
+
dns: perf?.domainEnd - perf?.domainStart || 0,
|
|
591
|
+
tcp: perf?.connectEnd - perf?.connectStart || 0,
|
|
592
|
+
ttfb: perf?.responseStart - perf?.requestStart || 0,
|
|
593
|
+
domReady: perf?.domContentLoadedEventEnd - perf?.startTime || 0,
|
|
594
|
+
loadComplete: perf?.loadEventEnd - perf?.startTime || 0,
|
|
595
|
+
// 绘制
|
|
596
|
+
fcp: fcp?.startTime || null,
|
|
597
|
+
// 资源统计
|
|
598
|
+
totalResources: resources.length,
|
|
599
|
+
totalTransferSize: resources.reduce((sum, r) => sum + (r.transferSize || 0), 0),
|
|
600
|
+
slowResources: resources.filter(r => r.duration > 1000).map(r => ({
|
|
601
|
+
name: r.name.split('/').pop(), duration: Math.round(r.duration), size: r.transferSize
|
|
602
|
+
})),
|
|
603
|
+
};
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
// Web Vitals (通过 CDP)
|
|
607
|
+
let webVitals = {};
|
|
608
|
+
try {
|
|
609
|
+
const client = await page.target().createCDPSession();
|
|
610
|
+
await client.send('Performance.enable');
|
|
611
|
+
const metrics = await client.send('Performance.getMetrics');
|
|
612
|
+
const m = {};
|
|
613
|
+
metrics.metrics.forEach(item => { m[item.name] = item.value; });
|
|
614
|
+
webVitals = {
|
|
615
|
+
layoutCount: m.LayoutCount || 0,
|
|
616
|
+
recalcCount: m.RecalcLayoutCount || 0,
|
|
617
|
+
jsHeapUsed: Math.round((m.JSHeapUsedSize || 0) / 1024 / 1024) + 'MB',
|
|
618
|
+
jsHeapTotal: Math.round((m.JSHeapTotalSize || 0) / 1024 / 1024) + 'MB',
|
|
619
|
+
taskDuration: (m.TaskDuration || 0).toFixed(2) + 's',
|
|
620
|
+
};
|
|
621
|
+
await client.send('Performance.disable');
|
|
622
|
+
client.detach();
|
|
623
|
+
} catch {}
|
|
624
|
+
|
|
625
|
+
log('\n========== 性能指标 ==========');
|
|
626
|
+
log(`DNS: ${Math.round(perfMetrics.dns)}ms`);
|
|
627
|
+
log(`TCP: ${Math.round(perfMetrics.tcp)}ms`);
|
|
628
|
+
log(`TTFB: ${Math.round(perfMetrics.ttfb)}ms`);
|
|
629
|
+
log(`DOM Ready: ${Math.round(perfMetrics.domReady)}ms`);
|
|
630
|
+
log(`Load Complete: ${Math.round(perfMetrics.loadComplete)}ms`);
|
|
631
|
+
log(`FCP: ${perfMetrics.fcp ? Math.round(perfMetrics.fcp) + 'ms' : 'N/A'}`);
|
|
632
|
+
log(`\n资源总数: ${perfMetrics.totalResources}`);
|
|
633
|
+
log(`传输总量: ${Math.round(perfMetrics.totalTransferSize / 1024)}KB`);
|
|
634
|
+
if (perfMetrics.slowResources.length > 0) {
|
|
635
|
+
log(`\n慢资源 (>1s):`);
|
|
636
|
+
perfMetrics.slowResources.forEach(r => log(` ${r.duration}ms ${r.name} (${Math.round((r.size||0)/1024)}KB)`));
|
|
637
|
+
}
|
|
638
|
+
if (Object.keys(webVitals).length > 0) {
|
|
639
|
+
log('\n========== Chrome 内部指标 ==========');
|
|
640
|
+
Object.entries(webVitals).forEach(([k, v]) => log(` ${k}: ${v}`));
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
return { perfMetrics, webVitals };
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// ===== Action: watch (持续监控) =====
|
|
647
|
+
async function watch(browser) {
|
|
648
|
+
const page = await findPage(browser);
|
|
649
|
+
const interval = parseInt(getArg('interval', '5000'), 10);
|
|
650
|
+
const maxRounds = parseInt(getArg('max-rounds', '60'), 10);
|
|
651
|
+
ensureDir(OUTPUT_DIR);
|
|
652
|
+
log(`[PAGE] ${page.url()}`);
|
|
653
|
+
log(`[WATCH] 间隔=${interval}ms, 最大轮数=${maxRounds}`);
|
|
654
|
+
|
|
655
|
+
const errors = [];
|
|
656
|
+
page.on('pageerror', err => errors.push({ time: new Date().toISOString(), error: err.message }));
|
|
657
|
+
page.on('console', msg => {
|
|
658
|
+
if (msg.type() === 'error') errors.push({ time: new Date().toISOString(), type: 'console', text: msg.text() });
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
for (let i = 1; i <= maxRounds; i++) {
|
|
662
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
663
|
+
const fp = path.join(OUTPUT_DIR, `watch-${String(i).padStart(3, '0')}-${ts}.png`);
|
|
664
|
+
await page.screenshot({ path: fp });
|
|
665
|
+
const errCount = errors.length;
|
|
666
|
+
log(`[${i}/${maxRounds}] 截图 | 累计错误: ${errCount}`);
|
|
667
|
+
if (errCount > 0 && i === 1) {
|
|
668
|
+
errors.forEach(e => log(` [ERROR] ${e.error || e.text}`));
|
|
669
|
+
}
|
|
670
|
+
await new Promise(r => setTimeout(r, interval));
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
log(`\n========== 监控结束 ==========`);
|
|
674
|
+
log(`总轮数: ${maxRounds}, 累计错误: ${errors.length}`);
|
|
675
|
+
errors.forEach(e => log(` [${e.time}] ${e.error || e.text}`));
|
|
676
|
+
return { rounds: maxRounds, errors };
|
|
677
|
+
}
|
|
678
|
+
|
|
368
679
|
// ===== 主入口 =====
|
|
369
680
|
(async () => {
|
|
370
681
|
// 检测 CDP
|
|
@@ -378,7 +689,23 @@ async function executePlan(browser) {
|
|
|
378
689
|
}
|
|
379
690
|
log(`[CDP] ${cdpInfo.Browser || 'connected'} (${CDP_URL})`);
|
|
380
691
|
|
|
381
|
-
|
|
692
|
+
// 获取 WebSocket URL
|
|
693
|
+
let wsUrl;
|
|
694
|
+
try {
|
|
695
|
+
const versionData = await new Promise((resolve, reject) => {
|
|
696
|
+
const req = http.get(CDP_URL + '/json/version', { timeout: 3000 }, res => {
|
|
697
|
+
let d = ''; res.on('data', c => d += c); res.on('end', () => resolve(JSON.parse(d)));
|
|
698
|
+
});
|
|
699
|
+
req.on('error', reject);
|
|
700
|
+
});
|
|
701
|
+
wsUrl = versionData.webSocketDebuggerUrl;
|
|
702
|
+
} catch {}
|
|
703
|
+
|
|
704
|
+
const browser = await Promise.race([
|
|
705
|
+
puppeteer.connect({ browserWSEndpoint: wsUrl || CDP_URL, defaultViewport: null }),
|
|
706
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('CDP WebSocket 连接超时 (10s)')), 10000)),
|
|
707
|
+
]);
|
|
708
|
+
log('[OK] 浏览器已连接');
|
|
382
709
|
|
|
383
710
|
let result;
|
|
384
711
|
switch (ACTION) {
|
|
@@ -386,10 +713,15 @@ async function executePlan(browser) {
|
|
|
386
713
|
case 'diagnose': result = await diagnose(browser); break;
|
|
387
714
|
case 'screenshot': result = await screenshot(browser); break;
|
|
388
715
|
case 'api-check': result = await apiCheck(browser); break;
|
|
716
|
+
case 'snapshot': result = await snapshot(browser); break;
|
|
717
|
+
case 'interact': result = await interact(browser); break;
|
|
718
|
+
case 'perf': result = await perf(browser); break;
|
|
719
|
+
case 'watch': result = await watch(browser); break;
|
|
389
720
|
case 'plan': result = await executePlan(browser); break;
|
|
390
721
|
default: log(`[ERROR] 未知 action: ${ACTION}`);
|
|
391
722
|
}
|
|
392
723
|
|
|
393
724
|
browser.disconnect();
|
|
394
725
|
log('\n[DONE]');
|
|
726
|
+
process.exit(0);
|
|
395
727
|
})();
|
package/package.json
CHANGED
|
@@ -53,5 +53,5 @@ argument-hint: <目标页面URL> [可选:设计稿图片路径]
|
|
|
53
53
|
|
|
54
54
|
**注意事项**:
|
|
55
55
|
- 首次使用需确保 Chrome 以调试模式启动:`google-chrome --remote-debugging-port=9222`
|
|
56
|
-
-
|
|
56
|
+
- 如执行器报错依赖缺失,优先按提示安装核心依赖;仅设计稿像素对比需要额外安装 `pixelmatch pngjs sharp`
|
|
57
57
|
- connect 模式下天然继承用户浏览器的登录态和微前端环境
|