@szc-ft/mcp-szcd-client 0.26.0 → 0.27.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,7 +12,7 @@ compatibility:
12
12
  ## 触发条件
13
13
 
14
14
  当用户说出以下关键词时启动浏览器测试:
15
- - "测试一下"、"帮我验证"、"浏览器测试"
15
+ - "测试一下"、"帮我验证"、"浏览器测试"、"接管浏览器"
16
16
  - "对比设计稿"、"还原度"、"看看效果"
17
17
  - "功能测试"、"能不能正常用"
18
18
  - "检查页面"、"页面渲染"
@@ -159,32 +159,89 @@ wujie 微前端子应用渲染在 blob URL iframe 中,**坐标点击无法到
159
159
  - `~#myId` → 自动转为 `[id*="myId"]`
160
160
  - 普通选择器(如 `.my-class`)保持精确匹配
161
161
 
162
- ### puppeteer-core v22 注意事项
162
+ ## 实战经验补充
163
+
164
+ ### wujie 微前端诊断要点
165
+
166
+ 1. **子应用渲染在 blob URL iframe 中**,puppeteer 的 `page.frames()` 可以访问
167
+ 2. **iframe 内的 console 监听**:不要用 `frame.page().on()`(puppeteer-core 无此方法),改用 `page.on('console')` + `msg.frame().url()` 判断来源
168
+ 3. **生产环境 console 被屏蔽**:代码中 `console.log = () => {}` 导致 0 条消息。需提前注入 `localStorage.setItem('showConsoleLog', 'true')`
169
+ 4. **pageerror 事件是关键**:未捕获的 JS 异常不会出现在 console 中,必须监听 `page.on('pageerror')`
170
+ 5. **API 404/403 检查**:刷新页面后监听 `page.on('response')`,过滤 `resourceType === 'xhr'` 检查后端接口状态
171
+
172
+ ### 常见诊断流程
173
+
174
+ ```
175
+ 1. --action list-pages 确认目标页面存在
176
+ 2. --action diagnose 全面诊断(JS错误 + 网络 + 资源 + iframe)
177
+ 3. --action api-check 专门检查 API 请求(含 iframe 内的请求)
178
+ 4. 根据结果定位问题
179
+ ```
180
+
181
+ ### puppeteer-core 注意事项
182
+ - **连接时必须设 `defaultViewport: null`**,否则浏览器窗口会被强制缩放到指定尺寸:
183
+ ```js
184
+ // 正确:保持浏览器原始窗口大小
185
+ puppeteer.connect({ browserURL: 'http://localhost:9222', defaultViewport: null })
186
+ // 错误:会把浏览器视口改成 1440x900
187
+ puppeteer.connect({ browserURL: '...', defaultViewport: { width: 1440, height: 900 } })
188
+ ```
163
189
  - `page.$x()` 已移除,用 `evaluate` + `querySelectorAll` 替代
164
190
  - SVG 元素的 `className` 是 `SVGAnimatedString` 对象而非字符串,需先检查 `typeof`
165
191
  - 所有 `evaluate` 调用内部使用 `safeEval` 包装,自动处理 context destroyed 错误
166
192
 
167
193
  ## 执行命令
168
194
 
195
+ ### 快速诊断(最常用)
196
+ ```bash
197
+ NODE_PATH=~/.szcd-mcp/deps/node_modules node {skill_path}/local-browser-executor.cjs \
198
+ --action diagnose \
199
+ --url-contains "platform.aicityos.com" \
200
+ --wait 15000 \
201
+ --output-dir /tmp/browser-test-diagnose
202
+ ```
203
+
204
+ ### 列出所有标签页
205
+ ```bash
206
+ NODE_PATH=~/.szcd-mcp/deps/node_modules node {skill_path}/local-browser-executor.cjs \
207
+ --action list-pages
208
+ ```
209
+
210
+ ### 检查 API 请求状态
211
+ ```bash
212
+ NODE_PATH=~/.szcd-mcp/deps/node_modules node {skill_path}/local-browser-executor.cjs \
213
+ --action api-check \
214
+ --url-contains "platform.aicityos.com" \
215
+ --wait 15000
216
+ ```
217
+
218
+ ### 截图
169
219
  ```bash
170
- node {client_install_path}/local-browser-executor.js \
171
- --plan '<测试计划JSON>' \
172
- --output /tmp/browser-test-result.json \
173
- --base-url http://localhost:8088
220
+ NODE_PATH=~/.szcd-mcp/deps/node_modules node {skill_path}/local-browser-executor.cjs \
221
+ --action screenshot \
222
+ --url-contains "platform.aicityos.com" \
223
+ --filename current-page.png \
224
+ --output-dir /tmp/browser-test
174
225
  ```
175
226
 
176
- 也支持从文件读取计划:
227
+ ### 执行测试计划 JSON
177
228
  ```bash
178
- node {client_install_path}/local-browser-executor.js \
229
+ NODE_PATH=~/.szcd-mcp/deps/node_modules node {skill_path}/local-browser-executor.cjs \
230
+ --action plan \
179
231
  --plan-file /tmp/test-plan.json \
180
- --output /tmp/browser-test-result.json \
181
- --base-url http://localhost:8088
232
+ --output /tmp/browser-test-result.json
182
233
  ```
183
234
 
184
235
  ### 可选参数
185
236
 
186
- - `--cdp-url http://localhost:9223`:指定非默认的 CDP 端口
187
- - `--page-url http://xxx/page`:connect 模式下指定目标页面
237
+ - `--cdp-url http://localhost:9222`:CDP 地址(默认 9222)
238
+ - `--url <url>`:目标页面 URL(不存在则导航)
239
+ - `--url-contains <keyword>`:按关键词查找已打开的页面
240
+ - `--wait <ms>`:额外等待时间(默认 5000ms,微前端建议 15000)
241
+ - `--output <path>`:JSON 结果输出路径
242
+ - `--output-dir <dir>`:截图/产物目录
243
+ - `--filename <name>`:截图文件名
244
+ - `--full-page`:全页截图
188
245
 
189
246
  ## AI 语义断言(aiAssert)
190
247
 
@@ -216,7 +273,7 @@ node {client_install_path}/local-browser-executor.js \
216
273
  - 后续所有项目共享复用,无需重复安装
217
274
  - 如果自动安装失败,引导用户手动执行:
218
275
  ```bash
219
- cd ~/.szcd-mcp/deps && npm install puppeteer-core pixelmatch pngjs sharp --registry=https://npmmirror.com
276
+ cd ~/.szcd-mcp/deps && npm install puppeteer-core pixelmatch pngjs sharp --registry=https://registry.npmmirror.com
220
277
  ```
221
278
 
222
279
  ## 结果解读
@@ -0,0 +1,395 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * local-browser-executor.js - 本地浏览器自动化执行器
4
+ *
5
+ * 功能:
6
+ * --action diagnose 全面诊断(JS错误、网络请求、API检查、资源加载)
7
+ * --action list-pages 列出所有标签页
8
+ * --action screenshot 截图
9
+ * --action api-check 检查 API 请求状态
10
+ * --action plan 执行测试计划 JSON
11
+ *
12
+ * 连接方式:
13
+ * --cdp-url http://localhost:9222 (默认自动检测 9222)
14
+ * --auto-detect 自动检测 Chrome/Edge 进程
15
+ *
16
+ * 通用参数:
17
+ * --url <url> 目标页面 URL
18
+ * --url-contains <s> 按关键词查找已打开的页面
19
+ * --wait <ms> 额外等待时间(默认 5000)
20
+ * --output <path> 结果输出文件
21
+ * --output-dir <dir> 截图/产物输出目录
22
+ */
23
+
24
+ const puppeteer = require('puppeteer-core');
25
+ const fs = require('fs');
26
+ const path = require('path');
27
+ const http = require('http');
28
+
29
+ // ===== 参数解析 =====
30
+ const args = process.argv.slice(2);
31
+ function getArg(name, defaultVal) {
32
+ const idx = args.indexOf(`--${name}`);
33
+ if (idx === -1) return defaultVal;
34
+ return args[idx + 1] || defaultVal;
35
+ }
36
+ function hasFlag(name) {
37
+ return args.includes(`--${name}`);
38
+ }
39
+
40
+ const ACTION = getArg('action', 'diagnose');
41
+ const CDP_URL = getArg('cdp-url', 'http://localhost:9222');
42
+ const TARGET_URL = getArg('url', null);
43
+ const URL_CONTAINS = getArg('url-contains', null);
44
+ const WAIT_MS = parseInt(getArg('wait', '5000'), 10);
45
+ const OUTPUT = getArg('output', null);
46
+ const OUTPUT_DIR = getArg('output-dir', '/tmp/browser-test');
47
+ const PLAN_FILE = getArg('plan-file', null);
48
+
49
+ // ===== 工具函数 =====
50
+ function log(msg) { console.log(msg); }
51
+ function ensureDir(dir) { fs.mkdirSync(dir, { recursive: true }); }
52
+
53
+ function checkCDP(url) {
54
+ return new Promise((resolve) => {
55
+ const req = http.get(url + '/json/version', { timeout: 2000 }, (res) => {
56
+ let data = '';
57
+ res.on('data', c => data += c);
58
+ res.on('end', () => {
59
+ try { resolve(JSON.parse(data)); } catch { resolve(null); }
60
+ });
61
+ });
62
+ req.on('error', () => resolve(null));
63
+ req.on('timeout', () => { req.destroy(); resolve(null); });
64
+ });
65
+ }
66
+
67
+ async function findPage(browser) {
68
+ const pages = await browser.pages();
69
+ if (TARGET_URL) {
70
+ for (const p of pages) {
71
+ if (p.url().includes(TARGET_URL) || p.url() === TARGET_URL) return p;
72
+ }
73
+ // 导航到目标 URL
74
+ const p = pages[0] || await browser.newPage();
75
+ await p.goto(TARGET_URL, { waitUntil: 'domcontentloaded', timeout: 30000 });
76
+ return p;
77
+ }
78
+ if (URL_CONTAINS) {
79
+ for (const p of pages) {
80
+ if (p.url().includes(URL_CONTAINS)) return p;
81
+ }
82
+ log(`[WARN] 未找到包含 "${URL_CONTAINS}" 的页面,使用第一个页面`);
83
+ }
84
+ // 找到非 about:blank 的页面
85
+ for (const p of pages) {
86
+ if (!p.url().startsWith('about:') && !p.url().startsWith('devtools:')) return p;
87
+ }
88
+ return pages[0];
89
+ }
90
+
91
+ // ===== Action: list-pages =====
92
+ async function listPages(browser) {
93
+ const pages = await browser.pages();
94
+ const result = [];
95
+ for (let i = 0; i < pages.length; i++) {
96
+ const p = pages[i];
97
+ let title = '';
98
+ try { title = await p.title(); } catch {}
99
+ result.push({ index: i, url: p.url(), title });
100
+ log(`[${i}] ${p.url()}`);
101
+ if (title) log(` title: ${title}`);
102
+ }
103
+ return { pages: result, total: result.length };
104
+ }
105
+
106
+ // ===== Action: diagnose =====
107
+ async function diagnose(browser) {
108
+ ensureDir(OUTPUT_DIR);
109
+ const page = await findPage(browser);
110
+ log(`[PAGE] ${page.url()}`);
111
+
112
+ // 收集数据
113
+ const pageErrors = [];
114
+ const consoleMessages = [];
115
+ const failedRequests = [];
116
+ const apiRequests = [];
117
+ const resourceRequests = [];
118
+
119
+ page.on('pageerror', err => {
120
+ pageErrors.push({ message: err.message, stack: err.stack?.split('\n').slice(0, 5).join('\n') });
121
+ });
122
+ page.on('console', msg => {
123
+ const type = msg.type();
124
+ const text = msg.text().substring(0, 500);
125
+ consoleMessages.push({ type, text });
126
+ });
127
+ page.on('response', resp => {
128
+ const url = resp.url();
129
+ const status = resp.status();
130
+ const type = resp.request().resourceType();
131
+ if (type === 'xhr' || type === 'fetch') {
132
+ apiRequests.push({ status, url, method: resp.request().method() });
133
+ }
134
+ if (type === 'stylesheet' || type === 'script') {
135
+ resourceRequests.push({ status, url, type });
136
+ }
137
+ if (status >= 400) {
138
+ failedRequests.push({ status, url, type });
139
+ }
140
+ });
141
+ page.on('requestfailed', req => {
142
+ failedRequests.push({ status: 'ABORTED', url: req.url(), error: req.failure()?.errorText });
143
+ });
144
+
145
+ // 刷新页面收集
146
+ log('[INFO] 刷新页面收集诊断数据...');
147
+ try {
148
+ await page.reload({ waitUntil: 'load', timeout: 30000 });
149
+ } catch (e) {
150
+ log(`[WARN] 刷新超时: ${e.message.substring(0, 100)}`);
151
+ }
152
+ await new Promise(r => setTimeout(r, WAIT_MS));
153
+
154
+ // 截图
155
+ const screenshotPath = path.join(OUTPUT_DIR, 'diagnose-screenshot.png');
156
+ await page.screenshot({ path: screenshotPath, fullPage: false });
157
+ log(`[OK] 截图: ${screenshotPath}`);
158
+
159
+ // 检查 iframe
160
+ const frames = page.frames();
161
+ const iframeInfo = [];
162
+ for (const frame of frames) {
163
+ const url = frame.url();
164
+ if (url === page.url() || url === 'about:blank') continue;
165
+ let bodyText = '';
166
+ let rootContent = '';
167
+ try {
168
+ const info = await frame.evaluate(() => ({
169
+ bodyText: document.body?.innerText?.substring(0, 200) || '',
170
+ rootHTML: document.getElementById('root')?.innerHTML?.substring(0, 200) || 'no root',
171
+ hasReactRoot: !document.getElementById('root')?.innerHTML?.includes('loading-spinner'),
172
+ antdElements: document.querySelectorAll('[class*="ant-"]').length,
173
+ }));
174
+ bodyText = info.bodyText;
175
+ rootContent = info.rootHTML;
176
+ iframeInfo.push({ url, ...info });
177
+ } catch (e) {
178
+ iframeInfo.push({ url, error: e.message.substring(0, 100) });
179
+ }
180
+ }
181
+
182
+ // 输出结果
183
+ const errors = consoleMessages.filter(m => m.type === 'error');
184
+ const warnings = consoleMessages.filter(m => m.type === 'warning');
185
+
186
+ const result = {
187
+ url: page.url(),
188
+ timestamp: new Date().toISOString(),
189
+ screenshot: screenshotPath,
190
+ pageErrors,
191
+ consoleErrors: errors,
192
+ consoleWarnings: warnings,
193
+ consoleTotal: consoleMessages.length,
194
+ failedRequests,
195
+ apiRequests,
196
+ resourceStatus: resourceRequests.map(r => ({
197
+ status: r.status, type: r.type, name: r.url.split('/').pop()
198
+ })),
199
+ iframes: iframeInfo,
200
+ summary: {
201
+ jsErrors: pageErrors.length,
202
+ consoleErrors: consoleMessages.filter(m => m.type === 'error').length,
203
+ failedNetwork: failedRequests.length,
204
+ totalAPI: apiRequests.length,
205
+ failedAPI: apiRequests.filter(r => r.status >= 400).length,
206
+ totalResources: resourceRequests.length,
207
+ failedResources: resourceRequests.filter(r => r.status >= 400).length,
208
+ }
209
+ };
210
+
211
+ // 打印摘要
212
+ log('\n========== 诊断结果 ==========');
213
+ log(`JS 异常: ${result.summary.jsErrors}`);
214
+ pageErrors.forEach(e => log(` [PAGEERROR] ${e.message}`));
215
+ log(`Console 错误: ${errors.length}`);
216
+ errors.forEach(e => log(` [ERROR] ${e.text}`));
217
+ log(`Console 警告: ${warnings.length}`);
218
+ warnings.forEach(e => log(` [WARN] ${e.text}`));
219
+ log(`Console 消息总数: ${consoleMessages.length}`);
220
+ log(`失败网络请求: ${result.summary.failedNetwork}`);
221
+ failedRequests.forEach(r => log(` [${r.status}] ${r.url}`));
222
+ log(`API 请求: ${result.summary.totalAPI} (失败: ${result.summary.failedAPI})`);
223
+ apiRequests.forEach(r => {
224
+ const marker = r.status >= 400 ? '❌' : '✅';
225
+ log(` ${marker} [${r.status}] ${r.method} ${r.url}`);
226
+ });
227
+ log(`CSS/JS 资源: ${result.summary.totalResources} (失败: ${result.summary.failedResources})`);
228
+ resourceRequests.filter(r => r.status >= 400).forEach(r => log(` ❌ [${r.status}] ${r.url}`));
229
+ if (iframeInfo.length > 0) {
230
+ log(`\niframe 信息:`);
231
+ iframeInfo.forEach(f => log(` ${f.url} → ${f.error || (f.hasReactRoot ? 'React已挂载' : '未挂载')}`));
232
+ }
233
+
234
+ // 保存结果
235
+ if (OUTPUT) {
236
+ fs.writeFileSync(OUTPUT, JSON.stringify(result, null, 2));
237
+ log(`\n[OK] 结果已保存: ${OUTPUT}`);
238
+ }
239
+
240
+ return result;
241
+ }
242
+
243
+ // ===== Action: screenshot =====
244
+ async function screenshot(browser) {
245
+ ensureDir(OUTPUT_DIR);
246
+ const page = await findPage(browser);
247
+ const filename = getArg('filename', 'screenshot.png');
248
+ const fullPath = path.join(OUTPUT_DIR, filename);
249
+ const fullPage = hasFlag('full-page');
250
+ await page.screenshot({ path: fullPath, fullPage });
251
+ log(`[OK] 截图已保存: ${fullPath}`);
252
+ return { filepath: fullPath };
253
+ }
254
+
255
+ // ===== Action: api-check =====
256
+ async function apiCheck(browser) {
257
+ const page = await findPage(browser);
258
+ log(`[PAGE] ${page.url()}`);
259
+
260
+ const requests = [];
261
+ const failed = [];
262
+
263
+ page.on('response', resp => {
264
+ const type = resp.request().resourceType();
265
+ if (type === 'xhr' || type === 'fetch') {
266
+ requests.push({
267
+ status: resp.status(),
268
+ url: resp.url(),
269
+ method: resp.request().method(),
270
+ frameUrl: resp.request().frame()?.url?.() || '',
271
+ });
272
+ }
273
+ });
274
+ page.on('requestfailed', req => {
275
+ failed.push({ url: req.url(), error: req.failure()?.errorText });
276
+ });
277
+
278
+ log('[INFO] 刷新页面收集 API 请求...');
279
+ try { await page.reload({ waitUntil: 'load', timeout: 30000 }); } catch {}
280
+ await new Promise(r => setTimeout(r, WAIT_MS));
281
+
282
+ log('\n========== API 请求 ==========');
283
+ requests.forEach(r => {
284
+ const marker = r.status >= 400 ? '❌' : '✅';
285
+ const isIframe = r.frameUrl && r.frameUrl !== page.url() ? ' [iframe]' : '';
286
+ log(`${marker} [${r.status}] ${r.method} ${r.url}${isIframe}`);
287
+ });
288
+ if (failed.length > 0) {
289
+ log('\n========== 失败请求 ==========');
290
+ failed.forEach(r => log(`[FAILED] ${r.url} → ${r.error}`));
291
+ }
292
+ log(`\n总计: ${requests.length} 请求, 失败: ${requests.filter(r => r.status >= 400).length + failed.length}`);
293
+
294
+ return { requests, failed };
295
+ }
296
+
297
+ // ===== Action: plan =====
298
+ async function executePlan(browser) {
299
+ const planPath = PLAN_FILE;
300
+ if (!planPath) { log('[ERROR] --plan-file 未指定'); return; }
301
+ const plan = JSON.parse(fs.readFileSync(planPath, 'utf-8'));
302
+ ensureDir(plan.options?.outputDir || OUTPUT_DIR);
303
+
304
+ log(`[PLAN] ${plan.description || plan.planId}`);
305
+ log(`[STEPS] ${plan.steps.length} 个步骤`);
306
+
307
+ const results = [];
308
+ let currentPage = await findPage(browser);
309
+
310
+ for (let i = 0; i < plan.steps.length; i++) {
311
+ const step = plan.steps[i];
312
+ const start = Date.now();
313
+ try {
314
+ let result = {};
315
+ switch (step.type) {
316
+ case 'navigate':
317
+ await currentPage.goto(step.url, { waitUntil: step.waitUntil || 'domcontentloaded', timeout: 30000 });
318
+ result = { url: currentPage.url() };
319
+ break;
320
+ case 'screenshot':
321
+ const fp = path.join(plan.options?.outputDir || OUTPUT_DIR, step.filename || `step-${i}.png`);
322
+ await currentPage.screenshot({ path: fp, fullPage: step.fullPage });
323
+ result = { filepath: fp };
324
+ break;
325
+ case 'waitFor':
326
+ if (step.selector) await currentPage.waitForSelector(step.selector, { timeout: step.timeout || 10000 });
327
+ result = { matched: true };
328
+ break;
329
+ case 'evaluate':
330
+ const evalResult = await currentPage.evaluate(step.expression);
331
+ result = { result: evalResult };
332
+ break;
333
+ case 'click':
334
+ await currentPage.click(step.selector);
335
+ result = { clicked: true };
336
+ break;
337
+ case 'checkElement':
338
+ const count = await currentPage.$$eval(step.selector, els => els.length).catch(() => 0);
339
+ const passed = step.expect?.minCount ? count >= step.expect.minCount : count > 0;
340
+ result = { passed, elementCount: count };
341
+ break;
342
+ default:
343
+ result = { skipped: true, reason: `unknown step type: ${step.type}` };
344
+ }
345
+ const duration = Date.now() - start;
346
+ results.push({ index: i, step: step.type, status: 'PASS', duration, ...result });
347
+ log(` [${i + 1}/${plan.steps.length}] ✅ ${step.type} (${duration}ms)`);
348
+ } catch (e) {
349
+ const duration = Date.now() - start;
350
+ results.push({ index: i, step: step.type, status: 'FAIL', duration, error: e.message });
351
+ log(` [${i + 1}/${plan.steps.length}] ❌ ${step.type} (${duration}ms) → ${e.message.substring(0, 100)}`);
352
+ }
353
+ }
354
+
355
+ const summary = {
356
+ total: results.length,
357
+ passed: results.filter(r => r.status === 'PASS').length,
358
+ failed: results.filter(r => r.status === 'FAIL').length,
359
+ };
360
+ log(`\n[RESULT] ${summary.passed}/${summary.total} 通过`);
361
+
362
+ if (OUTPUT) {
363
+ fs.writeFileSync(OUTPUT, JSON.stringify({ steps: results, summary }, null, 2));
364
+ }
365
+ return { steps: results, summary };
366
+ }
367
+
368
+ // ===== 主入口 =====
369
+ (async () => {
370
+ // 检测 CDP
371
+ const cdpInfo = await checkCDP(CDP_URL);
372
+ if (!cdpInfo) {
373
+ log(`[ERROR] 无法连接到 CDP: ${CDP_URL}`);
374
+ log('请确保浏览器已启动调试端口:');
375
+ log(' Windows Edge: "C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe" --remote-debugging-port=9222');
376
+ log(' Windows Chrome: "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe" --remote-debugging-port=9222');
377
+ process.exit(1);
378
+ }
379
+ log(`[CDP] ${cdpInfo.Browser || 'connected'} (${CDP_URL})`);
380
+
381
+ const browser = await puppeteer.connect({ browserURL: CDP_URL, defaultViewport: null });
382
+
383
+ let result;
384
+ switch (ACTION) {
385
+ case 'list-pages': result = await listPages(browser); break;
386
+ case 'diagnose': result = await diagnose(browser); break;
387
+ case 'screenshot': result = await screenshot(browser); break;
388
+ case 'api-check': result = await apiCheck(browser); break;
389
+ case 'plan': result = await executePlan(browser); break;
390
+ default: log(`[ERROR] 未知 action: ${ACTION}`);
391
+ }
392
+
393
+ browser.disconnect();
394
+ log('\n[DONE]');
395
+ })();
@@ -61,7 +61,7 @@ compatibility:
61
61
  AI 助手在处理页面生成需求时,必须按以下流程使用工具:
62
62
 
63
63
  ### 步骤1:架构认知 + 需求理解
64
- - 调用 `get_architecture_overview(detail="summary")` 获取 7 层架构和模板组合模式
64
+ - 调用 `get_architecture_overview(detail="full")` 获取 7 层架构、模板组合模式、全量组件设计元信息(componentSummary)+ styleMapping + assetExtraction,用于后续步骤的组件推理与样式注入
65
65
  - 分析用户需求的页面布局类型、查询字段、操作按钮、表格列等
66
66
 
67
67
  ### 步骤2:组件发现(优先语义搜索)
@@ -93,17 +93,13 @@ AI 助手在处理页面生成需求时,必须按以下流程使用工具:
93
93
  → get_component_full_profile(批量) → 编码
94
94
  ```
95
95
 
96
- **Sketch 结构 → 组件映射规则**:
97
- | Sketch 特征 | 推断结果 | 依据 |
98
- |---|---|---|
99
- | 画板名称(如 "4.1-编目审核-待办") | `pageName` | 直接使用 |
100
- | 左侧窄区域 + 树形图层 | `LeftTree` 组件 | templatePatterns.TreeQueryTable |
101
- | 顶部输入框/下拉框/日期选择器 | `Query` 组件 | 搜索区域特征 |
102
- | 中间表头 + 数据行图层 | `TableOrList` 组件 | 表格区域特征 |
103
- | 弹窗/抽屉图层 | `ModelOrDrawer` 组件 | 弹窗交互特征 |
104
- | 页面标题 + 返回箭头 | `TitleAndBack` 组件 | 标题栏特征 |
105
- | 左右分区布局 | `TemplateMode(templateTpye="LeftRight")` | 区域分布 |
106
- | 上下分区布局 | `TemplateMode(templateTpye="TopBottom")` | 区域分布 |
96
+ **Sketch 结构 → 组件映射依据**:直接查阅步骤 1 `get_architecture_overview(detail="full")` 返回的 `componentSummary` 全量元信息,按以下顺序反查组件:
97
+ 1. **大区切分** `componentSummary.other` `layoutRole === '页面定位'`(如 TemplateMode),结合 `variantProps.templateType` 和 `templatePatterns` 选模板
98
+ 2. **区域容器** → `layoutRole === '区域容器'` 的组件(LeftTree / Query / TableOrList / ModelOrDrawer / TitleAndBack 等),按 `useCases` 匹配 Sketch 区域语义
99
+ 3. **图表** → `componentSummary.echarts[*].sketchSignals` 反查 `chartType`
100
+ 4. **变体** `variantProps` 配合 `useCases` 区分子类型
101
+
102
+ `componentSummary` 由组件库源码动态扫描生成,是唯一可信源;不要依赖任何硬编码 Sketch→组件对照表。
107
103
 
108
104
  **提示**:
109
105
  - 大型 .sketch 文件用 `getPageStructure(maxDepth=1-2)` 即可获取布局概况,避免深层递归超时
@@ -204,7 +200,10 @@ sketch-mcp-server 是社区版 MCP Server(npm: `sketch-mcp-server`),通过
204
200
  #### 4. get_architecture_overview
205
201
  **功能**:查看 7 层架构(AntD→Cover→Wrapper→ProPackage→复合组件→模板)、层级关系和推荐使用顺序。含 LLM 映射修正提示。
206
202
  **参数**:
207
- - `detail` (enum, optional, default: "summary"): summary=层级概览+模板模式+LLM提示;full=含所有层组件列表详情
203
+ - `detail` (enum, optional, default: "summary"):
204
+ - `summary`:层级概览 + 模板模式 + LLM 提示(轻量,~2K tokens)
205
+ - `full`:在 summary 基础上附加 `componentSummary`(全量组件清单 + 设计元信息:layoutRole/variantProps/styleInjection/useCases,other/echarts 层另含 compositionSlots/requiredHooks/chartType/sketchSignals)+ `styleMapping`(节点样式映射 + 注入机制 + 选择器表)+ `assetExtraction`(位图/图标/图表色提取规则)。设计稿 → 代码场景必选 full
206
+ - `decisions`:含架构决策记录
208
207
 
209
208
  ### Props 深度解析工具
210
209