@lark-apaas/coding-miaoda-sandbox-skills 0.1.0-dev.6f4e4bc → 0.1.0-dev.72ac425

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 (71) hide show
  1. package/miaoda/ai-data-processing/SKILL.md +4 -1
  2. package/miaoda/charts-skill/SKILL.md +1 -1
  3. package/miaoda/creative-to-fullstack/SKILL.md +38 -226
  4. package/miaoda/feishu/SKILL.md +4 -4
  5. package/miaoda/forms-skill/SKILL.md +9 -0
  6. package/miaoda/lark-apps-db/SKILL.md +6 -4
  7. package/miaoda/lark-apps-db/references/full-reference.md +13 -4
  8. package/miaoda/lark-apps-ops/SKILL.md +2 -1
  9. package/miaoda/lark-apps-ops/references/lark-apps-export.md +46 -0
  10. package/miaoda/lark-design-prototype/DESIGN.md +603 -0
  11. package/miaoda/lark-design-prototype/SKILL.md +85 -0
  12. package/miaoda/lark-design-prototype/references/assets/card-illustration-library.md +113 -0
  13. package/miaoda/lark-design-prototype/references/case-matching.md +53 -0
  14. package/miaoda/lark-design-prototype/references/cases/conversational-ai-home.md +27 -0
  15. package/miaoda/lark-design-prototype/references/cases/data-table.md +30 -0
  16. package/miaoda/lark-design-prototype/references/cases/official-home.md +26 -0
  17. package/miaoda/lark-design-prototype/references/cases/workspace-home.md +34 -0
  18. package/miaoda/lark-design-prototype/references/color-roles.md +163 -0
  19. package/miaoda/lark-design-prototype/references/component-selection.md +134 -0
  20. package/miaoda/lark-design-prototype/references/design-quality-checklist.md +156 -0
  21. package/miaoda/lark-design-prototype/references/form-shell-patterns.md +77 -0
  22. package/miaoda/lark-design-prototype/references/icon-semantics.md +237 -0
  23. package/miaoda/lark-design-prototype/references/layout-interaction.md +164 -0
  24. package/miaoda/lark-design-prototype/references/page-contract.md +281 -0
  25. package/miaoda/lark-design-prototype/references/product-patterns.md +93 -0
  26. package/miaoda/lark-design-prototype/references/prompt-expansion.md +107 -0
  27. package/miaoda/lark-design-prototype/references/restoration-traps.md +113 -0
  28. package/miaoda/lark-design-prototype/references/token-semantics.md +112 -0
  29. package/miaoda/lark-design-prototype/references/visual-brief.md +125 -0
  30. package/miaoda/lark-design-prototype/references/visual-style-prompts.md +61 -0
  31. package/miaoda/lark-design-prototype/scripts/icon-query.mjs +272 -0
  32. package/miaoda/lark-design-prototype/scripts/token-query.mjs +76 -0
  33. package/miaoda/lark-design-prototype/scripts/verify-static-html.mjs +117 -0
  34. package/miaoda/performance-review/SKILL.md +3 -3
  35. package/miaoda/semantic-search/SKILL.md +6 -3
  36. package/miaoda/testing-guide/SKILL.md +116 -12
  37. package/miaoda-modern/charts-skill/SKILL.md +1 -1
  38. package/miaoda-modern/forms-skill/SKILL.md +33 -3
  39. package/miaoda-modern/lark-apps-ops/SKILL.md +2 -1
  40. package/miaoda-modern/lark-apps-ops/references/lark-apps-export.md +46 -0
  41. package/miaoda-modern/lark-design-prototype/DESIGN.md +603 -0
  42. package/miaoda-modern/lark-design-prototype/SKILL.md +85 -0
  43. package/miaoda-modern/lark-design-prototype/references/assets/card-illustration-library.md +113 -0
  44. package/miaoda-modern/lark-design-prototype/references/case-matching.md +53 -0
  45. package/miaoda-modern/lark-design-prototype/references/cases/conversational-ai-home.md +27 -0
  46. package/miaoda-modern/lark-design-prototype/references/cases/data-table.md +30 -0
  47. package/miaoda-modern/lark-design-prototype/references/cases/official-home.md +26 -0
  48. package/miaoda-modern/lark-design-prototype/references/cases/workspace-home.md +34 -0
  49. package/miaoda-modern/lark-design-prototype/references/color-roles.md +163 -0
  50. package/miaoda-modern/lark-design-prototype/references/component-selection.md +134 -0
  51. package/miaoda-modern/lark-design-prototype/references/design-quality-checklist.md +156 -0
  52. package/miaoda-modern/lark-design-prototype/references/form-shell-patterns.md +77 -0
  53. package/miaoda-modern/lark-design-prototype/references/icon-semantics.md +237 -0
  54. package/miaoda-modern/lark-design-prototype/references/layout-interaction.md +164 -0
  55. package/miaoda-modern/lark-design-prototype/references/page-contract.md +281 -0
  56. package/miaoda-modern/lark-design-prototype/references/product-patterns.md +93 -0
  57. package/miaoda-modern/lark-design-prototype/references/prompt-expansion.md +107 -0
  58. package/miaoda-modern/lark-design-prototype/references/restoration-traps.md +113 -0
  59. package/miaoda-modern/lark-design-prototype/references/token-semantics.md +112 -0
  60. package/miaoda-modern/lark-design-prototype/references/visual-brief.md +125 -0
  61. package/miaoda-modern/lark-design-prototype/references/visual-style-prompts.md +61 -0
  62. package/miaoda-modern/lark-design-prototype/scripts/icon-query.mjs +272 -0
  63. package/miaoda-modern/lark-design-prototype/scripts/token-query.mjs +76 -0
  64. package/miaoda-modern/lark-design-prototype/scripts/verify-static-html.mjs +117 -0
  65. package/miaoda-modern/performance-review/SKILL.md +3 -3
  66. package/miaoda-modern/reviewer-usage/SKILL.md +2 -0
  67. package/package.json +1 -1
  68. package/shared/attachment/SKILL.md +5 -1
  69. package/miaoda/creative-to-fullstack/references/artifact-signals.md +0 -46
  70. package/miaoda/creative-to-fullstack/references/ui-to-function.md +0 -134
  71. 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
+ }
@@ -59,11 +59,11 @@ const userMap = new Map(users.map(u => [u.id, u]));
59
59
  - N+1 queries: per-item DB calls inside loops — use batch or JOIN
60
60
  - List endpoints without pagination — will break as data grows
61
61
  - OFFSET pagination on large datasets — use cursor pagination (WHERE id > cursor)
62
- - Multiple independent DB queries running serially — use Promise.all
62
+ - Multiple independent DB queries running serially — use Promise.all. The platform connection pool caps real concurrency at **7**; beyond that queries queue. It only pays off for IO-bound queries — CPU-heavy ones (large aggregations, big `generate_series`) still take the same total time, so for those the only fix is fewer/cheaper queries
63
63
  - External API calls without timeout
64
- - WHERE/ORDER BY columns missing indexes — **cross-check with schema definition: for every ORDER BY or WHERE column in service queries, verify a matching index exists in schema.ts**
64
+ - WHERE/ORDER BY columns missing indexes — **cross-check against the live DDL: for every ORDER BY or WHERE column in service queries, verify a matching index exists via `npx -y @lark-apaas/miaoda-data-cli db schema <table>`. Do NOT use `schema.ts` for this — it is a generated mirror that silently drops partial-index `WHERE` clauses and CHECK constraints, so an existing partial index reads as missing (and a partial unique index reads as full-table unique)**
65
65
  - Aggregation done in JS instead of SQL layer — fetch all rows then compute in app is a common anti-pattern
66
- - **Sorting fields used in list/ranking endpoints must have database indexes — read schema.ts and verify**
66
+ - **Sorting fields used in list/ranking endpoints must have database indexes — verify with `db schema <table>` (the live DDL), not `schema.ts`**
67
67
  - **Date-range loop expansion in JS (CRITICAL)**: code shaped like `for (let d = new Date(start); d <= end; d.setDate(d.getDate()+1)) for (item of items) ...` is O(N×days). For 1000 records over 90 days = 90,000 ops per request. Must be pushed to SQL using `generate_series(start, end, '1 day') + unnest(array_field) + GROUP BY ... HAVING COUNT(*) > N` — let the database do the date×row cross product
68
68
  - **Multiple endpoints sharing the same base data should be merged**: e.g. Dashboard frontend calling 4 separate stats endpoints, each internally re-querying the full teacher table to build a lookup map. Merge into a single overview endpoint that runs the 4 stats in parallel internally and shares one base-data query.
69
69
  - **Stats / Dashboard endpoints must have a default time window**: any query that scans the entire fact table for aggregation (`select ... from coursePlan` with no `where`) will degrade as data grows. Add a default `where startDate >= now() - interval '90 day'` (or business-appropriate window) so it can hit `idx_*_start_date`. Without a time window, even with indexes the planner may still seq-scan. If "all history" is required, use a materialized view or pre-aggregated table refreshed on schedule, never a live full-table scan.
@@ -57,6 +57,8 @@ SELECT rds_ai.ai_embed('你好')::vector;
57
57
  -- → vector(1024)
58
58
  ```
59
59
 
60
+ ⚠️ **模型可用性由租户管理后台管控。** embedding 仅此一个模型、无备选,管理员将其下架或停用后 `rds_ai.ai_embed` 即不可用,调用报 `model_unavailable`(处置见「关键注意事项」)。
61
+
60
62
  ## 实现步骤
61
63
 
62
64
  以下 SQL 使用 `products` / `embedding` 作为示例。Agent 生成实际方案时,必须替换成用户应用里的真实表名、字段名、主键、返回列和索引名。
@@ -281,14 +283,15 @@ LIMIT 10;
281
283
 
282
284
  ## 关键注意事项
283
285
 
284
- ### ⚠️ ai_embed 报错优先看状态码:422 (额度不足) / 429 (限流)
286
+ ### ⚠️ ai_embed 报错优先看状态码:422 (额度不足) / 429 (限流) / 500 (无可用模型)
285
287
 
286
- `rds_ai.ai_embed` 抛错时,PG 错误信息里通常会带 HTTP 状态码或 `quota_exceeded` / `rate_limit_exceeded` 关键字。识别后给用户的提示要分开:
288
+ `rds_ai.ai_embed` 抛错时,PG 错误信息里通常会带 HTTP 状态码或 `quota_exceeded` / `rate_limit_exceeded` / `model_unavailable` 关键字。识别后给用户的提示要分开:
287
289
 
288
290
  - **422 / quota_exceeded** — 当前应用的 AI 调用额度已用完。提示用户:「AI 调用额度已耗尽,请联系应用 Owner 在控制台续费或升级套餐后再试」。**不要重试**,重试只会继续报错。
289
291
  - **429 / rate_limit_exceeded** — 调用过于密集触发限流。提示用户:「AI 调用过于频繁触发了限流,稍后再试,或调小回填批次 / 降低并发即可恢复」。**可重试**,建议指数退避。
292
+ - **500 / model_unavailable** — 租户预置的 embedding 模型已被管理员下架或停用,且无备选模型。提示用户:「当前租户无可用 AI 模型,请联系租户管理员恢复后再试」。**不要重试**,模型恢复后重跑存量回填即可。
290
293
 
291
- trigger 已经用 `EXCEPTION WHEN OTHERS THEN NEW.embedding := NULL` 兜住单条失败,业务写入不会回滚;后续走存量回填补齐。批量回填(步骤 7)建议捕获错误码:遇到 422 立刻停止整批并提示续费,遇到 429 退避后续跑。
294
+ trigger 已经用 `EXCEPTION WHEN OTHERS THEN NEW.embedding := NULL` 兜住单条失败,业务写入不会回滚;后续走存量回填补齐。批量回填(步骤 7)建议捕获错误码:遇到 422 立刻停止整批并提示续费,遇到 429 退避后续跑,遇到 500 停整批并提示联系租户管理员。
292
295
 
293
296
  ### ⚠️ 同步 trigger 调 rds_ai.ai_embed 必须 BEGIN/EXCEPTION 兜底
294
297