@lark-apaas/coding-miaoda-sandbox-skills 0.1.0-dev.b2f659e → 0.1.0-dev.c8cc2b5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/miaoda/charts-skill/SKILL.md +1 -1
  2. package/miaoda/creative-to-fullstack/SKILL.md +105 -100
  3. package/miaoda/creative-to-fullstack/references/artifact-signals.md +2 -1
  4. package/miaoda/creative-to-fullstack/references/ui-to-function.md +5 -69
  5. package/miaoda/feishu/SKILL.md +4 -4
  6. package/miaoda/forms-skill/SKILL.md +9 -0
  7. package/miaoda/lark-apps-db/SKILL.md +3 -1
  8. package/miaoda/lark-apps-db/references/full-reference.md +13 -4
  9. package/miaoda/lark-apps-ops/SKILL.md +3 -2
  10. package/miaoda/lark-apps-ops/references/lark-apps-mcp.md +26 -0
  11. package/miaoda/lark-design-prototype/DESIGN.md +603 -0
  12. package/miaoda/lark-design-prototype/SKILL.md +85 -0
  13. package/miaoda/lark-design-prototype/references/assets/card-illustration-library.md +113 -0
  14. package/miaoda/lark-design-prototype/references/case-matching.md +53 -0
  15. package/miaoda/lark-design-prototype/references/cases/conversational-ai-home.md +27 -0
  16. package/miaoda/lark-design-prototype/references/cases/data-table.md +30 -0
  17. package/miaoda/lark-design-prototype/references/cases/official-home.md +26 -0
  18. package/miaoda/lark-design-prototype/references/cases/workspace-home.md +34 -0
  19. package/miaoda/lark-design-prototype/references/color-roles.md +163 -0
  20. package/miaoda/lark-design-prototype/references/component-selection.md +134 -0
  21. package/miaoda/lark-design-prototype/references/design-quality-checklist.md +156 -0
  22. package/miaoda/lark-design-prototype/references/form-shell-patterns.md +77 -0
  23. package/miaoda/lark-design-prototype/references/icon-semantics.md +237 -0
  24. package/miaoda/lark-design-prototype/references/layout-interaction.md +164 -0
  25. package/miaoda/lark-design-prototype/references/page-contract.md +281 -0
  26. package/miaoda/lark-design-prototype/references/product-patterns.md +93 -0
  27. package/miaoda/lark-design-prototype/references/prompt-expansion.md +107 -0
  28. package/miaoda/lark-design-prototype/references/restoration-traps.md +113 -0
  29. package/miaoda/lark-design-prototype/references/token-semantics.md +112 -0
  30. package/miaoda/lark-design-prototype/references/visual-brief.md +125 -0
  31. package/miaoda/lark-design-prototype/references/visual-style-prompts.md +61 -0
  32. package/miaoda/lark-design-prototype/scripts/icon-query.mjs +272 -0
  33. package/miaoda/lark-design-prototype/scripts/token-query.mjs +76 -0
  34. package/miaoda/lark-design-prototype/scripts/verify-static-html.mjs +117 -0
  35. package/miaoda/testing-guide/SKILL.md +106 -12
  36. package/miaoda-modern/charts-skill/SKILL.md +1 -1
  37. package/miaoda-modern/forms-skill/SKILL.md +33 -3
  38. package/miaoda-modern/lark-apps-ops/SKILL.md +3 -2
  39. package/miaoda-modern/lark-apps-ops/references/lark-apps-mcp.md +26 -0
  40. package/miaoda-modern/lark-design-prototype/DESIGN.md +603 -0
  41. package/miaoda-modern/lark-design-prototype/SKILL.md +85 -0
  42. package/miaoda-modern/lark-design-prototype/references/assets/card-illustration-library.md +113 -0
  43. package/miaoda-modern/lark-design-prototype/references/case-matching.md +53 -0
  44. package/miaoda-modern/lark-design-prototype/references/cases/conversational-ai-home.md +27 -0
  45. package/miaoda-modern/lark-design-prototype/references/cases/data-table.md +30 -0
  46. package/miaoda-modern/lark-design-prototype/references/cases/official-home.md +26 -0
  47. package/miaoda-modern/lark-design-prototype/references/cases/workspace-home.md +34 -0
  48. package/miaoda-modern/lark-design-prototype/references/color-roles.md +163 -0
  49. package/miaoda-modern/lark-design-prototype/references/component-selection.md +134 -0
  50. package/miaoda-modern/lark-design-prototype/references/design-quality-checklist.md +156 -0
  51. package/miaoda-modern/lark-design-prototype/references/form-shell-patterns.md +77 -0
  52. package/miaoda-modern/lark-design-prototype/references/icon-semantics.md +237 -0
  53. package/miaoda-modern/lark-design-prototype/references/layout-interaction.md +164 -0
  54. package/miaoda-modern/lark-design-prototype/references/page-contract.md +281 -0
  55. package/miaoda-modern/lark-design-prototype/references/product-patterns.md +93 -0
  56. package/miaoda-modern/lark-design-prototype/references/prompt-expansion.md +107 -0
  57. package/miaoda-modern/lark-design-prototype/references/restoration-traps.md +113 -0
  58. package/miaoda-modern/lark-design-prototype/references/token-semantics.md +112 -0
  59. package/miaoda-modern/lark-design-prototype/references/visual-brief.md +125 -0
  60. package/miaoda-modern/lark-design-prototype/references/visual-style-prompts.md +61 -0
  61. package/miaoda-modern/lark-design-prototype/scripts/icon-query.mjs +272 -0
  62. package/miaoda-modern/lark-design-prototype/scripts/token-query.mjs +76 -0
  63. package/miaoda-modern/lark-design-prototype/scripts/verify-static-html.mjs +117 -0
  64. package/miaoda-modern/reviewer-usage/SKILL.md +2 -0
  65. package/package.json +1 -1
  66. package/shared/attachment/SKILL.md +5 -1
  67. package/miaoda-modern/testing-guide/SKILL.md +0 -218
@@ -0,0 +1,272 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs/promises';
3
+ import { createHash } from 'node:crypto';
4
+ import path from 'node:path';
5
+
6
+ const SVG_BASE = 'https://cdn-tos-cn.bytedance.net/obj/archi/ee/es-design-base/svgs';
7
+ const REMOTE_CATALOG =
8
+ 'https://lf3-static.bytednsdoc.com/obj/eden-cn/pshi/lark-design-prototype/1.0.4/db71a6cd-33f97519-0d446cd5/icons.catalog.json';
9
+ const REMOTE_CATALOG_SHA256 = 'db71a6cd6ac90c77629e8ce0fc0905ca1488a6601800ff7533385490b10eac9d';
10
+
11
+ function usage() {
12
+ return `Usage:
13
+ node icon-query.mjs [--catalog <remote|catalog.json|url>] --query "<keywords>" [--family outlined] [--style v2-outlined|source-outlined|source-filled|source-colorful] [--shape normal|round] [--prefer-v2] [--require-v2] [--limit 5]
14
+
15
+ Returns a compact JSON array with name, hash, family, version, shape, score, description, and final svgUrl.
16
+ Never print the raw icon catalog during page generation.`;
17
+ }
18
+
19
+ function argValue(name, fallback = '') {
20
+ const index = process.argv.indexOf(name);
21
+ return index === -1 ? fallback : process.argv[index + 1] || fallback;
22
+ }
23
+
24
+ function argFlag(name) {
25
+ return process.argv.includes(name);
26
+ }
27
+
28
+ function tokenize(text) {
29
+ const rawTokens = String(text || '')
30
+ .toLowerCase()
31
+ .split(/[\s,,、::;;/|()()\[\]{}'"`<>.!??。_-]+/)
32
+ .map((item) => item.trim())
33
+ .filter(Boolean);
34
+ const aliases = {
35
+ 搜索: ['search'],
36
+ 查找: ['search'],
37
+ 通知: ['notification', 'bell'],
38
+ 提醒: ['reminder', 'bell'],
39
+ 主页: ['home'],
40
+ 首页: ['home'],
41
+ 模板: ['template'],
42
+ 文档: ['doc', 'document', 'file'],
43
+ document: ['doc'],
44
+ 文件: ['file'],
45
+ 文件夹: ['folder'],
46
+ 表格: ['sheet', 'table', 'bitable'],
47
+ spreadsheet: ['sheet'],
48
+ table: ['sheet'],
49
+ 图表: ['chart'],
50
+ 看板: ['dashboard', 'chart'],
51
+ 帮助: ['help', 'faq'],
52
+ 指南: ['guide', 'info'],
53
+ 飞书: ['feishu', 'lark', 'logo', 'logotype'],
54
+ 品牌: ['brand', 'logo', 'logotype'],
55
+ 标识: ['logo', 'logotype'],
56
+ logo: ['brand', 'logotype'],
57
+ };
58
+ const expanded = new Set(rawTokens);
59
+ for (const token of rawTokens) {
60
+ for (const alias of aliases[token] || []) {
61
+ expanded.add(alias);
62
+ }
63
+ }
64
+ return [...expanded];
65
+ }
66
+
67
+ async function readJsonFile(filePath) {
68
+ const text = await fs.readFile(filePath, 'utf8');
69
+ return JSON.parse(text);
70
+ }
71
+
72
+ async function readCatalog(source) {
73
+ const resolved = !source || source === 'remote' ? REMOTE_CATALOG : source;
74
+ if (/^https?:\/\//.test(resolved)) {
75
+ const response = await fetch(resolved);
76
+ if (!response.ok) {
77
+ throw new Error(`Catalog request failed with HTTP ${response.status}`);
78
+ }
79
+ const text = await response.text();
80
+ if (resolved === REMOTE_CATALOG) {
81
+ const actualHash = createHash('sha256').update(text).digest('hex');
82
+ if (actualHash !== REMOTE_CATALOG_SHA256) {
83
+ throw new Error(
84
+ `Catalog integrity check failed: expected ${REMOTE_CATALOG_SHA256}, got ${actualHash}`,
85
+ );
86
+ }
87
+ }
88
+ return { catalog: JSON.parse(text), catalogSource: resolved };
89
+ }
90
+ const filePath = path.resolve(resolved);
91
+ return { catalog: await readJsonFile(filePath), catalogSource: filePath };
92
+ }
93
+
94
+ function detectVersion(name) {
95
+ return /(?:^|[-_])v2(?:[-_]|$)/i.test(name) ? 'v2' : 'non-v2';
96
+ }
97
+
98
+ function detectShape(icon) {
99
+ const name = icon.name.toLowerCase();
100
+ if (name.includes('file-round-') || name.includes('wiki-round-')) return 'round';
101
+ if (icon.family === 'colorful' && /(?:icon_|wiki-)?file[-_]/.test(name)) return 'normal';
102
+ return 'none';
103
+ }
104
+
105
+ function detectStyleType(icon) {
106
+ const version = detectVersion(icon.name);
107
+ const shape = detectShape(icon);
108
+ if (icon.family === 'outlined') {
109
+ return version === 'v2' ? 'v2-outlined' : 'non-v2-outlined';
110
+ }
111
+ if (icon.family === 'filled') {
112
+ return 'source-filled';
113
+ }
114
+ if (icon.family === 'colorful' && /(?:feishu|lark).*logo|logotype|logo/i.test(icon.name)) {
115
+ return 'brand-colorful';
116
+ }
117
+ if (icon.family === 'colorful' && version === 'v2' && shape === 'round') {
118
+ return 'file-v2-colorful-round';
119
+ }
120
+ if (icon.family === 'colorful' && version === 'v2' && shape === 'normal') {
121
+ return 'file-v2-colorful-normal';
122
+ }
123
+ return icon.family;
124
+ }
125
+
126
+ function normalizeOptions() {
127
+ const style = argValue('--style');
128
+ const options = {
129
+ family: argValue('--family', 'outlined'),
130
+ style,
131
+ shape: argValue('--shape'),
132
+ preferV2: argFlag('--prefer-v2'),
133
+ requireV2: argFlag('--require-v2'),
134
+ requireNonV2: argFlag('--require-non-v2'),
135
+ include: argValue('--name-include'),
136
+ exclude: argValue('--name-exclude'),
137
+ };
138
+
139
+ if (style === 'v2-outlined') {
140
+ options.family = 'outlined';
141
+ options.requireV2 = true;
142
+ } else if (style === 'non-v2-outlined') {
143
+ options.family = 'outlined';
144
+ options.requireNonV2 = true;
145
+ } else if (style === 'file-v2-colorful-normal') {
146
+ options.family = 'colorful';
147
+ options.requireV2 = true;
148
+ options.shape = 'normal';
149
+ options.include ||= 'file';
150
+ } else if (style === 'file-v2-colorful-round') {
151
+ options.family = 'colorful';
152
+ options.requireV2 = true;
153
+ options.shape = 'round';
154
+ options.include ||= 'file-round';
155
+ } else if (style === 'source-outlined') {
156
+ options.family = 'outlined';
157
+ options.preferV2 = true;
158
+ } else if (style === 'source-filled') {
159
+ options.family = 'filled';
160
+ } else if (style === 'brand-colorful' || style === 'source-colorful') {
161
+ options.family = 'colorful';
162
+ if (style === 'source-colorful') {
163
+ options.preferV2 = true;
164
+ }
165
+ }
166
+ return options;
167
+ }
168
+
169
+ function flattenCatalog(catalog, options) {
170
+ const rows = [];
171
+ for (const category of catalog) {
172
+ for (const [family, icons] of Object.entries(category.icons || {})) {
173
+ if (options.family && family !== options.family) {
174
+ continue;
175
+ }
176
+ for (const icon of icons || []) {
177
+ const row = {
178
+ category: category.id || '',
179
+ family,
180
+ name: icon.name,
181
+ hash: icon.hash,
182
+ darkHash: icon.darkHash,
183
+ description: icon.description || '',
184
+ };
185
+ row.version = detectVersion(row.name);
186
+ row.shape = detectShape(row);
187
+ row.styleType = detectStyleType(row);
188
+ rows.push(row);
189
+ }
190
+ }
191
+ }
192
+ return rows;
193
+ }
194
+
195
+ function passesFilters(icon, options) {
196
+ const name = icon.name.toLowerCase();
197
+ if (options.requireV2 && icon.version !== 'v2') return false;
198
+ if (options.requireNonV2 && icon.version !== 'non-v2') return false;
199
+ if (options.shape && icon.shape !== options.shape) return false;
200
+ if (options.include && !name.includes(options.include.toLowerCase())) return false;
201
+ if (options.exclude && name.includes(options.exclude.toLowerCase())) return false;
202
+ const sourceStyles = new Set(['source-outlined', 'source-filled', 'source-colorful']);
203
+ if (options.style && !sourceStyles.has(options.style) && icon.styleType !== options.style)
204
+ return false;
205
+ return true;
206
+ }
207
+
208
+ function scoreIcon(icon, queryTokens, options) {
209
+ const name = icon.name.toLowerCase();
210
+ const description = icon.description.toLowerCase();
211
+ let score = 0;
212
+ for (const token of queryTokens) {
213
+ if (name.includes(token)) score += 5;
214
+ if (description.includes(token)) score += 3;
215
+ if (name === `icon_${token}_outlined`) score += 8;
216
+ if (name === `icon_${token}-v2_outlined`) score += 8;
217
+ if (name === `icon_${token}_filled`) score += 8;
218
+ if (name === `icon_file-${token}-v2_colorful`) score += 8;
219
+ }
220
+ if (score === 0) return 0;
221
+ if (icon.family === 'outlined') score += 1;
222
+ if ((options.preferV2 || options.style?.includes('v2')) && icon.version === 'v2') score += 4;
223
+ if (
224
+ options.style === 'brand-colorful' &&
225
+ /feishu|lark|logotype|logo/i.test(`${name} ${description}`)
226
+ )
227
+ score += 10;
228
+
229
+ const objectTokens = ['doc', 'document', 'file', 'card', 'sheet', 'bitable', 'ai'];
230
+ const queryHasObject = objectTokens.some((token) => queryTokens.includes(token));
231
+ if (
232
+ !queryHasObject &&
233
+ objectTokens.some((token) => name.includes(`_${token}`) || name.includes(`-${token}`))
234
+ ) {
235
+ score -= 3;
236
+ }
237
+ return score;
238
+ }
239
+
240
+ const catalogSource = argValue('--catalog', 'remote');
241
+ const query = argValue('--query');
242
+ const limit = Math.max(1, Number.parseInt(argValue('--limit', '5'), 10) || 5);
243
+
244
+ if (!query) {
245
+ console.error(usage());
246
+ process.exit(2);
247
+ }
248
+
249
+ const options = normalizeOptions();
250
+ const queryTokens = tokenize(query);
251
+ const { catalog, catalogSource: usedCatalogSource } = await readCatalog(catalogSource);
252
+ const matches = flattenCatalog(catalog, options)
253
+ .filter((icon) => passesFilters(icon, options))
254
+ .map((icon) => ({ ...icon, score: scoreIcon(icon, queryTokens, options) }))
255
+ .filter((icon) => icon.score > 0)
256
+ .sort((a, b) => b.score - a.score || a.name.localeCompare(b.name))
257
+ .slice(0, limit)
258
+ .map((icon) => ({
259
+ name: icon.name,
260
+ hash: icon.hash,
261
+ darkHash: icon.darkHash || undefined,
262
+ family: icon.family,
263
+ version: icon.version,
264
+ shape: icon.shape,
265
+ styleType: icon.styleType,
266
+ score: icon.score,
267
+ description: icon.description.slice(0, 180),
268
+ svgUrl: `${SVG_BASE}/${icon.name}.${icon.hash}.svg`,
269
+ catalogSource: usedCatalogSource,
270
+ }));
271
+
272
+ console.log(JSON.stringify(matches, null, 2));
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+ import { createHash } from 'node:crypto';
3
+
4
+ const SOURCES = {
5
+ semantic: {
6
+ url: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/pshi/lark-design-prototype/1.0.4/db71a6cd-33f97519-0d446cd5/semantic.json',
7
+ sha256: '33f975193bcd0b5b12df9003e6f8586bf4662336e0bd5cf73ea1f362cfa4eb50',
8
+ },
9
+ keys: {
10
+ url: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/pshi/lark-design-prototype/1.0.4/db71a6cd-33f97519-0d446cd5/keys.json',
11
+ sha256: '0d446cd57b0beaaaa9ba485009b31a73be77d5f87a8481ec032c54f33b3bb471',
12
+ },
13
+ };
14
+
15
+ function usage() {
16
+ return `Usage:
17
+ node token-query.mjs --query "<token name or intent>" [--source semantic|keys] [--limit 5]
18
+
19
+ Fetches the versioned public token resource, verifies SHA-256, and returns compact matches.`;
20
+ }
21
+
22
+ function argValue(name, fallback = '') {
23
+ const index = process.argv.indexOf(name);
24
+ return index === -1 ? fallback : process.argv[index + 1] || fallback;
25
+ }
26
+
27
+ function tokenize(value) {
28
+ return String(value || '')
29
+ .toLowerCase()
30
+ .split(/[\s,,、::;;/|()()\[\]{}'"`<>.!??。_-]+/)
31
+ .map((item) => item.trim())
32
+ .filter(Boolean);
33
+ }
34
+
35
+ function scoreEntry([key, value], query, queryTokens) {
36
+ const normalizedKey = key.toLowerCase();
37
+ const searchText = `${key} ${value?.$description || ''} ${value?.$extensions?.id || ''} ${
38
+ value?.$extensions?.name || ''
39
+ }`.toLowerCase();
40
+ let score = normalizedKey === query ? 100 : normalizedKey.includes(query) ? 40 : 0;
41
+ for (const token of queryTokens) {
42
+ if (normalizedKey.includes(token)) score += 10;
43
+ if (searchText.includes(token)) score += 3;
44
+ }
45
+ return score;
46
+ }
47
+
48
+ const sourceName = argValue('--source', 'semantic');
49
+ const source = SOURCES[sourceName];
50
+ const query = argValue('--query').trim().toLowerCase();
51
+ const limit = Math.max(1, Number.parseInt(argValue('--limit', '5'), 10) || 5);
52
+
53
+ if (!source || !query) {
54
+ console.error(usage());
55
+ process.exit(2);
56
+ }
57
+
58
+ const response = await fetch(source.url);
59
+ if (!response.ok) {
60
+ throw new Error(`Token request failed with HTTP ${response.status}`);
61
+ }
62
+ const text = await response.text();
63
+ const actualHash = createHash('sha256').update(text).digest('hex');
64
+ if (actualHash !== source.sha256) {
65
+ throw new Error(`Token integrity check failed: expected ${source.sha256}, got ${actualHash}`);
66
+ }
67
+
68
+ const queryTokens = tokenize(query);
69
+ const matches = Object.entries(JSON.parse(text))
70
+ .map((entry) => ({ entry, score: scoreEntry(entry, query, queryTokens) }))
71
+ .filter(({ score }) => score > 0)
72
+ .sort((a, b) => b.score - a.score || a.entry[0].localeCompare(b.entry[0]))
73
+ .slice(0, limit)
74
+ .map(({ entry: [key, value] }) => ({ key, ...value, source: source.url }));
75
+
76
+ console.log(JSON.stringify(matches, null, 2));
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { pathToFileURL } from 'node:url';
5
+
6
+ function usage() {
7
+ return `Usage:
8
+ node verify-static-html.mjs <file.html> [--sidebar]
9
+
10
+ Runs token-light pre-browser checks for Lark-style static HTML deliverables.
11
+ Use --sidebar only when the page contains a navigation sidebar. Shadow declarations are reported for contextual review.`;
12
+ }
13
+
14
+ const htmlPath = process.argv[2];
15
+ if (!htmlPath) {
16
+ console.error(usage());
17
+ process.exit(2);
18
+ }
19
+
20
+ const absoluteHtmlPath = path.resolve(htmlPath);
21
+ const html = await fs.readFile(absoluteHtmlPath, 'utf8');
22
+
23
+ const hardFailures = [];
24
+ const warnings = [];
25
+ const passed = [];
26
+
27
+ function pass(name, ok, failMessage, warn = false) {
28
+ if (ok) {
29
+ passed.push(name);
30
+ } else if (warn) {
31
+ warnings.push(failMessage);
32
+ } else {
33
+ hardFailures.push(failMessage);
34
+ }
35
+ }
36
+
37
+ pass(
38
+ 'no gradients',
39
+ !/(linear-gradient|radial-gradient|conic-gradient)/i.test(html),
40
+ 'Page UI contains gradients.',
41
+ );
42
+ const shadows = [...html.matchAll(/box-shadow\s*:\s*([^;}"'<>]+)/gi)]
43
+ .map((match) => match[1].trim())
44
+ .filter((value) => !/^none(?:\s*!important)?$/i.test(value));
45
+ pass(
46
+ 'no shadows requiring contextual review',
47
+ shadows.length === 0,
48
+ 'Shadow declarations found; verify they belong to permitted floating layers, not ordinary cards.',
49
+ true,
50
+ );
51
+ if (process.argv.includes('--sidebar')) {
52
+ pass(
53
+ 'fixed sidebar background',
54
+ html.includes('#f9f9f9'),
55
+ 'Sidebar background #f9f9f9 is missing.',
56
+ );
57
+ pass(
58
+ 'fixed selected sidebar state',
59
+ html.includes('#1f23290d'),
60
+ 'Selected sidebar state #1f23290d is missing.',
61
+ );
62
+ }
63
+ pass('has table', /<table[\s>]/i.test(html), 'No table found.', true);
64
+ pass('has search input', /type=["']search["']/i.test(html), 'No search input found.', true);
65
+ pass(
66
+ 'has local responsive CSS',
67
+ /@media\s*\(/i.test(html),
68
+ 'No responsive media query found.',
69
+ true,
70
+ );
71
+
72
+ const imageRefs = [...html.matchAll(/<img\b[^>]*\bsrc=["']([^"']+)["']/gi)].map(
73
+ (match) => match[1],
74
+ );
75
+ const localImageRefs = imageRefs.filter(
76
+ (src) => !/^(https?:)?\/\//.test(src) && !src.startsWith('data:'),
77
+ );
78
+ const missingImages = [];
79
+ for (const src of localImageRefs) {
80
+ try {
81
+ await fs.access(new URL(src, pathToFileURL(absoluteHtmlPath)));
82
+ } catch {
83
+ missingImages.push(src);
84
+ }
85
+ }
86
+
87
+ pass(
88
+ 'all local images exist',
89
+ missingImages.length === 0,
90
+ `Missing local image assets: ${missingImages.join(', ')}`,
91
+ );
92
+
93
+ const tableHeaders = [...html.matchAll(/<th\b[^>]*>([\s\S]*?)<\/th>/gi)]
94
+ .map((match) => match[1].replace(/<[^>]+>/g, '').trim())
95
+ .filter(Boolean);
96
+
97
+ console.log(
98
+ JSON.stringify(
99
+ {
100
+ file: absoluteHtmlPath,
101
+ passed,
102
+ warnings,
103
+ hardFailures,
104
+ stats: {
105
+ images: imageRefs.length,
106
+ localImages: localImageRefs.length,
107
+ tableHeaders,
108
+ },
109
+ },
110
+ null,
111
+ 2,
112
+ ),
113
+ );
114
+
115
+ if (hardFailures.length > 0) {
116
+ process.exit(1);
117
+ }
@@ -8,6 +8,59 @@ gate-tools:
8
8
  when:
9
9
  subagent_type: E2E
10
10
  ---
11
+ {% if visualCheckEnabled %}
12
+ # 检查与验证:工具选择指南
13
+
14
+ 静态页面检查和功能验收使用不同工具。向用户只说「页面检查」或「用浏览器操作验证」,不要解释内部工具或 agent 名称。
15
+
16
+ ## 决策规则
17
+
18
+ ```
19
+ 用户想确认应用是否正常
20
+ ├─ 明确要求验证后端接口 / API / 请求响应 → api_request
21
+ ├─ 只检查静态视觉结果(白屏、布局、样式、文案、图片)→ visual_check
22
+ └─ 交互 / 业务流程 / 数据 / 网络 → Task(subagent_type="E2E")
23
+ ```
24
+
25
+ - **静态视觉检查**:调用 `visual_check`,一条路由一次调用;互不依赖的路由可同时发起。它只检查指定路由的最终静态画面,不用来点击、填写、提交或验证接口。
26
+ - **功能验收**:交互 / 业务流程 / 数据 / 网络都派 `Task(subagent_type="E2E")`。E2E 一律按标准语义执行,调用时不要额外指定运行方式。
27
+ - **接口验收**:只在用户明确要检查接口、API、请求或响应时调用 `api_request`;完成浏览器验收后不追加接口调用。
28
+ - **E2E 不可用时(Legacy)**:Task 的 `subagent_type` 没有 E2E 选项时,交互、业务、数据和网络验收改用 `api_request`、读代码和读运行时日志;不要尝试派 E2E。静态视觉仍使用 `visual_check`。
29
+
30
+ ## 调用方式
31
+
32
+ ```js
33
+ visual_check({
34
+ relative_path: "<路由相对路径>",
35
+ })
36
+ ```
37
+
38
+ `relative_path` 只填一个路由。一条路由检查完成后再根据结果决定是否修复;多个互不依赖的路由可以分别同时调用。
39
+
40
+ ```js
41
+ Task({
42
+ subagent_type: "E2E",
43
+ {% if e2eSemanticPlan %}
44
+ agent_options: { test_plan_file_path: ".spark/e2e-test/standard/<语义名>.json" },
45
+ {% endif %}
46
+ description: "验收<功能名称>",
47
+ prompt: "执行<具体交互或业务闭环>,断言<可观察终态>。",
48
+ })
49
+ ```
50
+
51
+ {% if e2eSemanticPlan %}
52
+ ### 验收计划文件
53
+
54
+ 先读可能覆盖当前需求的 `.spark/e2e-test/standard/<语义名>.json`;有变化则更新同一文件,换功能或页面则新建。计划只包含交互、业务、数据和网络的可观察终态;`prompt` 只说明本轮执行范围和 Case 编号。
55
+ {% endif %}
56
+
57
+ ## 验收要求
58
+
59
+ - 视觉检查写清可观察结果,例如「首页标题完整可见、卡片两列排列、主图不是裂图」,不要只写「看起来正常」。
60
+ - 功能验收必须走到终态:提交后记录出现、跳转后内容正确、删除后记录消失或 AI 输出出现;不要停在弹窗、按钮或表单字段出现。
61
+ - 写操作和资源加载要核对网络结果;非 2xx、空结果、错误兜底或白屏都应报失败。
62
+ - 不测试登录、认证、CAPTCHA,或会造成真实外部副作用的行为。
63
+ {% else %}
11
64
 
12
65
  # 检查与验证:工具选择指南
13
66
 
@@ -29,32 +82,40 @@ gate-tools:
29
82
  ├─ Task 工具定义的 subagent_type 选项里没有 E2E(E2E 被关闭/不可用,非"本轮没派过")→ api_request(接口级验收)+ 读代码/读日志兜底,不要尝试派 E2E
30
83
  ├─ 明确要求验证「后端接口」「API 返回值」「请求响应」 → api_request
31
84
  └─ 其他所有情况 → Task(subagent_type="E2E")(打开浏览器,以用户视角操作应用)
32
- ├─ 只看视觉(白屏/布局/样式/文案)→ agent_options.mode: "lite"
33
- └─ 涉及交互/业务流程/数据 → 不传或 agent_options.mode: "standard"
85
+ ├─ 基础视觉验证(白屏/布局/样式/文案)→ agent_options.mode: "lite"(只截图看视觉)
86
+ └─ 用户明确要求验证交互/业务流程/数据(点击/填写/提交/走一遍流程)→ agent_options.mode: "standard"
34
87
  ```
35
88
 
36
89
  - **E2E 不可用时改走 api_request(最高优先,先于下面所有规则判断)**:当 Task 工具定义的 subagent_type 选项里没有 E2E(用户关闭了浏览器验收 / 环境不支持;判断依据是工具定义中是否含 E2E 选项,不是"本轮有没有派过 E2E")时,下面"模糊需求一律派遣 E2E"不再适用——改用 `api_request` 做接口级验收,配合读代码 / 读运行时日志确认核心链路,**不要尝试派 E2E、也不要空等**;此时验收范围收敛到接口与数据层可判定结果,视觉类目标顺延到非核心降级范围
37
90
  - **模糊需求一律派遣 E2E**(仅当 E2E 可用):未明确提到「接口」「API」「后端」时必须派遣 E2E,不要主动选择 `api_request`
91
+ - **基础视觉验证走 lite,standard 需用户明确要求功能测试**:用户没有主动要求跑具体功能(只说"看看有没有问题"这类模糊需求也算没主动要求)时,显式传 `agent_options.mode: "lite"`;只有用户明确要验证交互 / 业务流程 / 数据(点击、填写、提交、走一遍流程等)时才显式传 `agent_options.mode: "standard"`
38
92
  - **`api_request` 仅限显式请求**:仅当明确提到接口 / API / 后端 / 请求 / 响应、且意图是验证接口逻辑而非页面功能时才使用
39
93
  - **E2E 结束后禁止追加 `api_request`**:浏览器操作的结果即为最终结果,**严禁**再自动补充验证
40
94
 
95
+ ### 低投入产出验收的停止边界
96
+
97
+ - 仅按“能否在 2 分钟、2 次观察内稳定看到结果”决定是否验收;短时且终态可观察的任务照常验收。
98
+ - 完整体验依赖真实设备、语音/视频生成等长媒体处理、第三方实时通道或后台长异步终态且无法稳定观察时,标注未验收边界。
99
+ - 有可观察部分时只选一种现有验证方式;达到时间或次数上限即停,标为部分验收/未验收,不新增验收专用工具,不跨轮等待不可控终态。
100
+
41
101
  ### E2E 的 mode 选择
42
102
 
43
- 通过 `agent_options.mode` 指定,**可选参数**,不传或非 `lite` 一律按 `standard` 处理:
103
+ 通过 `agent_options.mode` 指定,**必填参数**,且只允许 `lite` 或 `standard`;缺失或非法时 E2E 会拒绝执行:
44
104
 
45
- | mode | 适用场景 | 行为 | 总超时 | 录屏 |
46
- |------|---------|------|-------|------|
47
- | `standard`(缺省) | 涉及业务流程、数据流、交互后状态变化 | 完整交互(点击/输入/滚动/提交)+ Network 验证 + 录制 | 5 分钟 | ✅ |
48
- | `lite` | 只看视觉渲染(CSS/布局/文案/图标)或快速冒烟 | 仅 `open` / `goto` / `wait` / `snapshot` / `screenshot`,**禁止** `click` / `fill` / `type` / `scroll` / `select` / `press` / `drag` / `hover` | 2 分钟 | ❌ |
105
+ | mode | 适用场景 | 行为 | 录屏 |
106
+ |------|---------|------|------|
107
+ | `lite` | 基础视觉验证(白屏/布局/样式/文案)或快速冒烟 | 仅 `open` / `goto` / `wait` / `snapshot` / `screenshot`,**禁止** `click` / `fill` / `type` / `scroll` / `select` / `press` / `drag` / `hover` | ❌ |
108
+ | `standard` | **仅当用户明确要求**验证业务流程、数据流、交互后状态变化 | 完整交互(点击/输入/滚动/提交)+ Network 验证 + 录制 | ✅ |
49
109
 
50
- **仅改 CSS / 文案 / 图标 / 布局**(无事件处理、无状态、无数据获取)应直接选 `lite`——更快且 token 消耗显著低,不要默认走 standard 浪费配额;但 `lite` 不能触发 popup / toast / 提交后状态切换等需要操作的现象。testRequirements 里出现"点击"、"填写"、"提交"等动词,**禁止** `lite`,必须 `standard`。
110
+ **每次派发都必须显式传 mode**。基础视觉验证传 `lite`;`lite` 不能触发 popup / toast / 提交后状态切换等需要操作的现象。**用户明确要求跑功能**(testRequirements 里出现"点击"、"填写"、"提交"等动词,或用户直接要求验证某功能)时,传 `standard`。
51
111
 
52
112
  ## 工具选择速查表
53
113
 
54
114
  | 用户表达 | 选择 | 原因 |
55
115
  | --- | --- | --- |
56
- | "检查一下页面" / "看看有没有问题" / "走一遍流程" / "哪里出问题了" / "试试能不能用" / "XX 不好使" | E2E(standard) | 打开浏览器以用户视角操作 / 复现 |
57
- | "刚改了 CSS,看看样式对不对" | E2E(lite) | 仅视觉巡检,省时省 token |
116
+ | "检查一下页面" / "看看有没有问题" / "哪里出问题了" / "试试能不能用" / "XX 不好使" | E2E(显式 `mode: "lite"`) | 未主动要求跑功能,执行基础视觉验证 |
117
+ | "刚改了 CSS,看看样式对不对" | E2E(lite) | 仅视觉巡检 |
118
+ | "点提交试试能不能成功" / "帮我把下单流程走一遍" / "走一遍流程" / "验证一下这个功能" | E2E(standard) | 用户明确要求功能 / 交互验证 |
58
119
  | "测试一下这个接口的返回值对不对" / "调一下后端 API 看看响应" | `api_request` | 明确指定接口验证 |
59
120
 
60
121
  ## 派遣 E2E 时的 prompt / 用例规范
@@ -63,6 +124,25 @@ gate-tools:
63
124
 
64
125
  > **核心原则:agent 只「机械执行 + 报判断」,不做推理。** 判定标准全写死在断言里——别让它自己推断"该测什么"或定义"什么算正常 / 好看"。它的活只有:执行动作 → 看现象 → 报通过 / 不通过。
65
126
 
127
+ {% if e2eSemanticPlan %}
128
+ ### 验收计划文件:先写文件,再派发(首次与后续同一套)
129
+
130
+ 每次派发都必须带 `agent_options.test_plan_file_path`,不传不执行。**两种 mode 各一份卷**,目录必须与派发 mode 一致(串卷不执行):standard 卷写 `.spark/e2e-test/standard/<语义名>.json`(含点击 / 输入 / 提交),lite 卷写 `.spark/e2e-test/lite/<语义名>.json`(只放看得见的检查点)。
131
+
132
+ 1. **先看已有的**:`glob .spark/e2e-test/<mode>/*.json` → **读**可能覆盖本需求的那份(没读过不要传)
133
+ 2. **改还是新建**:同需求用例有变化 → 编辑那份;换了功能 / 页面 → 新建一份
134
+ 3. **派发**:`prompt` 只说跑哪些(`跑 Case 1、2` / `复测 p0 和 p1` / `测试全部用例`),**不点名默认只跑 p0**;一轮点名 ≤5 条
135
+
136
+ 格式(一条用例一行,优先级小写,每卷 ≤10 条;一轮只跑其中 ≤5 条):
137
+
138
+ ```json
139
+ {"cases":[{"id":1,"priority":"p0","description":"打开首页,预期工单列表渲染出至少 1 行"}]}
140
+ ```
141
+
142
+ - **用例正文只写在计划文件里**:本模式下 `prompt` 不写 `## 测试用例`(下面示例里的用例正文改为写进卷),只留应用结构与本次范围——两处都写会让计划外用例一条不跑、报告却全绿
143
+ - **复测只点名、不删用例**:「只传 failed + 未完成的 Case」指在 `prompt` 里点名,不是从卷里删掉已通过的用例
144
+ {% endif %}
145
+
66
146
  ### 调用结构
67
147
 
68
148
  ```js
@@ -109,7 +189,8 @@ task({
109
189
  - **跑到终态**:提交后出现在列表 / 跳转后内容正确 / 删除后从列表消失
110
190
  - **AI 功能看到输出**:翻译 / 摘要 / 生成等核心卖点,须断言到 AI 产出的内容,不停在"AI 弹窗打开"
111
191
  - **主链路 + AI 都覆盖**,不因"非主链路"遗漏
112
- - **Case ≤ 10**:过多会 token 膨胀、超时
192
+ - **一次最多 5 条 Case**:多出来的分下一轮跑,不塞进同一轮
193
+ - **单条 Case ≤ 8 步**:动作步 + 断言步合计,超了就拆 Case 或删断言
113
194
  - **不碰登录 / 认证**:应用无登录逻辑,禁止登录 / 注册 / OAuth / CAPTCHA 相关 Case
114
195
 
115
196
  | ❌ 停在中间状态 | ✅ 跑到最终结果 |
@@ -155,6 +236,16 @@ E2E agent 只在离散时刻抓快照 / 截图,**看不到瞬时过程、不
155
236
 
156
237
  断言对象若是瞬时中间态 / toast 本身 / 动效本身 / 需伪造网络的分支——改写成稳定终态断言,或直接删掉。
157
238
 
239
+ ### 高成本控件(最耗步数,按下表写)
240
+
241
+ | 控件 | 写法 |
242
+ |------|------|
243
+ | 日期 / 时间选择器 | 用默认值,不指定具体日期 |
244
+ | 下拉 / 搜索选择框(antd / MUI / Radix) | 选第一项,不搜关键词筛选;多级联动只验第一级 |
245
+ | 长表单 | 只写必填项 |
246
+
247
+ 必须验这些控件本身时,单独占一条 Case,本轮总数相应减少。
248
+
158
249
  ### 建议覆盖维度
159
250
 
160
251
  覆盖维度(按实际功能裁剪):
@@ -188,7 +279,7 @@ E2E 派遣是**昂贵操作**(每次消耗大量 token + 时间)。E2E agent
188
279
 
189
280
  **白屏 / 整页打不开 → 复测前先重启 devServer**:白屏多为 **devServer 编译挂死 / HMR 卡住**,非代码 bug,直接再派只会重复白屏、白吃一轮配额。顺序:**① 重启 devServer → ② 查编译输出 / runtime-log 确认无报错(不派 E2E)→ ③ 再正常复测**;真要肉眼确认用 `lite` 截图,别用 standard 探活。
190
281
 
191
- **测试范围**:首轮(spec 验收)总 Case **≤ 5**(单次派发上限 ≤10 见 Case 复杂度约束),一行一个 Case,必须含主链路流程测试。禁止测试有真实副作用的功能(发送飞书消息、飞书建群)。
282
+ **测试范围**:每次派发 **≤ 5 条 Case**(首轮 spec 验收同此上限),一行一个 Case,必须含主链路流程测试。禁止测试有真实副作用的功能(发送飞书消息、飞书建群)。
192
283
 
193
284
  **停止派遣 E2E 的触发条件**(满足任一即停止):
194
285
 
@@ -215,6 +306,9 @@ E2E 派遣是**昂贵操作**(每次消耗大量 token + 时间)。E2E agent
215
306
  | E2E 完成后又调 `api_request` 补充验证 | 浏览器操作结果即为最终结果,不追加 |
216
307
  | 用户说"看看有没有 bug"就同时调两个 | 只派遣 E2E |
217
308
  | 用户说"检查接口"时派遣 E2E | 明确提到接口时用 `api_request` |
309
+ | 派发 E2E 时不传 `mode` | 每次都显式传 `lite` 或 `standard`;缺失或非法时不会执行 |
310
+ | 用户没主动要求跑功能,却传 `standard` | 显式传 `lite`;仅用户明确要求交互 / 功能验证才传 `standard` |
218
311
  | 仅改 CSS / 文案,仍走 `standard` | 优先用 `lite`,速度快、token 消耗低 |
219
312
  | 达到停止条件(≥3 轮 / 同一错误未改代码连续失败)仍继续派 E2E | 停止派遣,转为根因分析(读代码 / 查日志) |
220
313
  | 复测时全量重测 / 白屏直接再派一轮 | 只传 failed + 未完成的 Case;白屏先重启 devServer 再复测 |
314
+ {% endif -%}
@@ -5,7 +5,7 @@ description: "shadcn/ui + ReactECharts 图表开发规范。Use when creating or
5
5
 
6
6
  ## L0 基础配置
7
7
 
8
- - **库**: `import ReactECharts from 'echarts-for-react'` (v5.6.0)
8
+ - **库**: `import ReactECharts from 'echarts-for-react'` (echarts v6.1.x)
9
9
  - **主题**: `theme="ud"`
10
10
  - **颜色**: 🚨 只能用 hex(如 `#1890ff`),禁止 hsl/rgb
11
11
  - **高度**: ≥300px (`className='h-[300px]'`)