cardbuddy 0.14.2 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/dist/cli.js +0 -0
  2. package/dist/preview-editor/assets/{index-BN79pTkQ.js → index-CIIfMlwI.js} +43 -40
  3. package/dist/preview-editor/assets/index-DQn0zQBP.css +1 -0
  4. package/dist/preview-editor/index.html +2 -2
  5. package/dist/standalone.js +3 -0
  6. package/dist/vendored/mcp/client.mjs +8 -1
  7. package/dist/vendored/server/core/ai-chat.js +1521 -124
  8. package/dist/vendored/server/core/browser.js +12 -3
  9. package/dist/vendored/server/core/cardFiles.js +3 -0
  10. package/dist/vendored/server/core/cardHistory.js +1 -0
  11. package/dist/vendored/server/core/cardImportMeta.js +7 -6
  12. package/dist/vendored/server/core/cardOpenFolder.js +1 -0
  13. package/dist/vendored/server/core/cardSave.js +1 -0
  14. package/dist/vendored/server/core/cardSync.js +3 -0
  15. package/dist/vendored/server/core/codeBridge.js +5 -1
  16. package/dist/vendored/server/core/logStream.js +1 -0
  17. package/dist/vendored/server/core/sliceStatic.js +24 -4
  18. package/dist/vendored/server/core/sliceUpload.js +1 -0
  19. package/dist/vendored/server/core/turnJournal.js +6 -0
  20. package/dist/vendored/skills/card-skill-trainer/SKILL.md +196 -195
  21. package/dist/vendored/skills/dsl-card-ai/SKILL.md +1 -1
  22. package/dist/vendored/skills/dsl-card-generator/SKILL.md +142 -380
  23. package/dist/vendored/skills/dsl-card-generator/references/generation_pitfalls.md +65 -18
  24. package/dist/vendored/skills/dsl-card-generator/references/ir-format.md +116 -2
  25. package/dist/vendored/skills/dsl-card-generator/references/iteration_protocol.md +58 -0
  26. package/dist/vendored/skills/dsl-card-generator/references/previewer_contract.md +1 -1
  27. package/dist/vendored/skills/dsl-card-generator/references/quickref.md +15 -7
  28. package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/title-underline-overlay.json +59 -11
  29. package/dist/vendored/skills/dsl-card-generator/references/recipes.md +14 -0
  30. package/dist/vendored/skills/dsl-card-generator/references/rules.md +23 -4
  31. package/dist/vendored/skills/dsl-card-generator/references/scripts_reference.md +51 -0
  32. package/dist/vendored/skills/dsl-card-generator/references/semantic_adjudication.md +23 -0
  33. package/dist/vendored/skills/dsl-card-generator/references/slices_convention.md +4 -3
  34. package/dist/vendored/skills/dsl-card-generator/references/spec_to_schema_patterns.md +32 -0
  35. package/dist/vendored/skills/dsl-card-generator/references/ui_fidelity_rules.md +13 -1
  36. package/dist/vendored/skills/dsl-card-generator/scripts/autofix_schema.py +45 -2
  37. package/dist/vendored/skills/dsl-card-generator/scripts/check_fidelity.mjs +116 -10
  38. package/dist/vendored/skills/dsl-card-generator/scripts/check_magic_values.mjs +6 -1
  39. package/dist/vendored/skills/dsl-card-generator/scripts/crop_slices.mjs +92 -6
  40. package/dist/vendored/skills/dsl-card-generator/scripts/expand_ir.mjs +227 -23
  41. package/dist/vendored/skills/dsl-card-generator/scripts/gate.mjs +144 -0
  42. package/dist/vendored/skills/dsl-card-generator/scripts/generate_cards.mjs +12 -8
  43. package/dist/vendored/skills/dsl-card-generator/scripts/ir_merge.mjs +288 -0
  44. package/dist/vendored/skills/dsl-card-generator/scripts/ir_restore.mjs +175 -0
  45. package/dist/vendored/skills/dsl-card-generator/scripts/iterate.mjs +644 -42
  46. package/dist/vendored/skills/dsl-card-generator/scripts/lib/geometry_audit.mjs +131 -0
  47. package/dist/vendored/skills/dsl-card-generator/scripts/lib/layout_rules.mjs +160 -0
  48. package/dist/vendored/skills/dsl-card-generator/scripts/lib/slicePath.mjs +17 -5
  49. package/dist/vendored/skills/dsl-card-generator/scripts/lib/slice_dedupe.mjs +102 -0
  50. package/dist/vendored/skills/dsl-card-generator/scripts/lib/v3_assemble.mjs +5443 -0
  51. package/dist/vendored/skills/dsl-card-generator/scripts/lib/v3_extract.mjs +4108 -0
  52. package/dist/vendored/skills/dsl-card-generator/scripts/lib/v3_shared.mjs +1116 -0
  53. package/dist/vendored/skills/dsl-card-generator/scripts/lint_card.mjs +214 -4
  54. package/dist/vendored/skills/dsl-card-generator/scripts/preflight.mjs +48 -2
  55. package/dist/vendored/skills/dsl-card-generator/scripts/render_card.mjs +91 -43
  56. package/dist/vendored/skills/dsl-card-generator/scripts/route_input.mjs +4 -3
  57. package/dist/vendored/skills/dsl-card-generator/scripts/spec_audit.mjs +150 -38
  58. package/dist/vendored/skills/dsl-card-generator/scripts/spec_to_schema_v3.mjs +43 -10499
  59. package/dist/vendored/skills/dsl-card-generator/scripts/v3_regression.mjs +186 -0
  60. package/dist/vendored/skills/sketch-spec-to-card/SKILL.md +18 -12
  61. package/dist/vendored/skills/sketch-spec-to-card/references/spec-schema.md +1 -1
  62. package/dist/vendored/skills/sketch-spec-to-card/scripts/build_prompt.py +47 -1
  63. package/dist/vendored/skills/sketch-spec-to-card/scripts/parse_meaxure.py +460 -49
  64. package/dist/vendored/skills/sketch-spec-to-card/scripts/png_utils.py +51 -0
  65. package/dist/vendored/skills/sketch-spec-to-card/scripts/test_text_only_artboard.py +157 -0
  66. package/dist/vendored/skills/sketch-spec-to-card/scripts/vision.py +24 -0
  67. package/package.json +1 -1
  68. package/dist/preview-editor/assets/index-cbTCHp8J.css +0 -1
@@ -14,7 +14,16 @@ import { devApiBase } from '../../tools/finmall-env.mjs'
14
14
  * 的 ElementHandle,不存在时为 null)。
15
15
  */
16
16
 
17
- const BASE_URL = process.env.CARD_SCREENSHOT_BASE || devApiBase()
17
+ /**
18
+ * 截图要打开的「编辑器页面」基址。
19
+ *
20
+ * 惰性读取(不是模块级常量):preview 模式下编辑器由 cardbuddy 自己托管在同一端口,
21
+ * 启动期才知道端口号;模块级常量会在 import 时就把基址钉死在默认的 vite 端口上。
22
+ * 实测(2026-09-21 demo 建卡回合):`card_screenshot` 两次返回
23
+ * `page.goto: net::ERR_CONNECTION_REFUSED at http://127.0.0.1:5180` —— 服务跑在 8787、
24
+ * 截图却去敲 5180,模型拿到一条纯噪音(还以为是卡片的问题)。
25
+ */
26
+ const baseUrl = () => process.env.CARD_SCREENSHOT_BASE || devApiBase()
18
27
  const IDLE_CLOSE_MS = 5 * 60 * 1000
19
28
  const MAX_QUEUED = 10
20
29
  const NAV_TIMEOUT_MS = 30000
@@ -79,7 +88,7 @@ function serialize(task) {
79
88
  */
80
89
  async function openCardScreen(context, id) {
81
90
  const page = await context.newPage()
82
- const target = `${BASE_URL}/?card=${encodeURIComponent(id)}&chrome=off`
91
+ const target = `${baseUrl()}/?card=${encodeURIComponent(id)}&chrome=off`
83
92
  await page.goto(target, { waitUntil: 'domcontentloaded', timeout: NAV_TIMEOUT_MS })
84
93
  // 等卡片首帧挂载(App.vue 置 data-card-ready=1)。用 waitForFunction 精确判断属性出现,
85
94
  // 而非等待「元素可见」(避免卡片有但属性未置位时误判)。
@@ -97,7 +106,7 @@ async function openCardScreen(context, id) {
97
106
  }
98
107
 
99
108
  export {
100
- BASE_URL,
109
+ baseUrl,
101
110
  IDLE_CLOSE_MS,
102
111
  MAX_QUEUED,
103
112
  NAV_TIMEOUT_MS,
@@ -303,6 +303,7 @@ export function createCardFilesMiddleware({ root, notify = null }) {
303
303
  // 不生成封面 —— 空白卡在 /__card-image 直接 404,网格回退统一占位图。
304
304
  if (pathname === '/__cards' && method === 'POST') {
305
305
  let body = ''
306
+ req.setEncoding('utf8')
306
307
  req.on('data', (c) => {
307
308
  body += c
308
309
  if (body.length > 64 * 1024) req.destroy()
@@ -414,6 +415,7 @@ export function createCardFilesMiddleware({ root, notify = null }) {
414
415
  }
415
416
  if (method === 'POST') {
416
417
  let body = ''
418
+ req.setEncoding('utf8')
417
419
  req.on('data', (c) => {
418
420
  body += c
419
421
  if (body.length > 16 * 1024 * 1024) req.destroy()
@@ -471,6 +473,7 @@ export function createCardFilesMiddleware({ root, notify = null }) {
471
473
  // 重命名卡片(目录改名 + 描述写回):PATCH /__card/<oldId>,body { id?, desc? }
472
474
  if (method === 'PATCH') {
473
475
  let body = ''
476
+ req.setEncoding('utf8')
474
477
  req.on('data', (c) => {
475
478
  body += c
476
479
  if (body.length > 64 * 1024) req.destroy()
@@ -117,6 +117,7 @@ export function createHistoryMiddleware({ root }) {
117
117
  // POST:把「上一版本 schema」落盘为一张新快照
118
118
  if (method === 'POST') {
119
119
  let body = ''
120
+ req.setEncoding('utf8')
120
121
  req.on('data', (c) => {
121
122
  body += c
122
123
  if (body.length > 32 * 1024 * 1024) req.destroy()
@@ -129,7 +129,11 @@ export function recordExport(dir, verified) {
129
129
 
130
130
  /**
131
131
  * dev server 中间件:GET /__import-meta/:id 暴露导入来源元数据(供 UI「来源」tab / AI skill 读取)。
132
- * 纯只读、no-store;卡片无侧车时 404(区分「本地新建卡」与「已同步卡」)。
132
+ * 纯只读、no-store。
133
+ *
134
+ * 无侧车(本地新建卡)用 **200 + imported:false** 表达,不用 404:
135
+ * 旧版回 404,于是每打开一张本地卡 DevTools 就多一条红色 404 —— 而「这张卡没从远端同步过」
136
+ * 是常态答案、不是故障。红色报错会误导用户,也会被图片/网络审计工具当成异常报出来。
133
137
  *
134
138
  * @param {{ root: string }} opts
135
139
  * @returns {(req, res, next) => void}
@@ -145,11 +149,8 @@ export function createImportMetaMiddleware({ root }) {
145
149
  const meta = readImportMeta(dir)
146
150
  res.setHeader('Cache-Control', 'no-store')
147
151
  res.setHeader('Content-Type', 'application/json')
148
- if (!meta) {
149
- res.statusCode = 404
150
- return res.end(JSON.stringify({ error: 'import-meta not found', id, hint: '该卡尚未经同步远程导入,无导入来源元数据' }))
151
- }
152
152
  res.statusCode = 200
153
- return res.end(JSON.stringify({ id, meta }))
153
+ // imported 布尔让调用方一眼判断,不必靠状态码区分「本地新建卡」与「已同步卡」
154
+ return res.end(JSON.stringify(meta ? { id, imported: true, meta } : { id, imported: false, meta: null }))
154
155
  }
155
156
  }
@@ -93,6 +93,7 @@ export function createOpenFolderMiddleware({ root }) {
93
93
  }
94
94
 
95
95
  let body = ''
96
+ req.setEncoding('utf8')
96
97
  req.on('data', (c) => {
97
98
  body += c
98
99
  if (body.length > 1 * 1024 * 1024) req.destroy()
@@ -25,6 +25,7 @@ export function createSaveMiddleware({ root }) {
25
25
  const method = (req.method || 'GET').toUpperCase()
26
26
  if (pathname !== '/__save' || method !== 'POST') return next()
27
27
  let body = ''
28
+ req.setEncoding('utf8')
28
29
  req.on('data', (c) => {
29
30
  body += c
30
31
  if (body.length > 32 * 1024 * 1024) req.destroy()
@@ -24,6 +24,7 @@ export function createSyncMiddleware({ root }) {
24
24
  if ((req.method || 'GET').toUpperCase() !== 'POST') return next()
25
25
 
26
26
  let body = ''
27
+ req.setEncoding('utf8')
27
28
  req.on('data', (c) => {
28
29
  body += c
29
30
  if (body.length > 1 * 1024 * 1024) req.destroy()
@@ -119,6 +120,7 @@ export function createPreviewMiddleware({ root }) {
119
120
  if ((req.method || 'GET').toUpperCase() !== 'POST') return next()
120
121
 
121
122
  let body = ''
123
+ req.setEncoding('utf8')
122
124
  req.on('data', (c) => {
123
125
  body += c
124
126
  if (body.length > 4 * 1024 * 1024) req.destroy()
@@ -195,6 +197,7 @@ export function createLoginCredentialsMiddleware({ root }) {
195
197
  if ((req.method || 'GET').toUpperCase() !== 'POST') return next()
196
198
 
197
199
  let body = ''
200
+ req.setEncoding('utf8')
198
201
  req.on('data', (c) => {
199
202
  body += c
200
203
  if (body.length > 64 * 1024) req.destroy()
@@ -73,7 +73,10 @@ export function resolveEditorSpec(file, env = process.env, platform = process.pl
73
73
 
74
74
  /** VSCode 未安装 / CLI 缺失时的协议兜底(安装器默认注册 vscode:// 处理器) */
75
75
  export function resolveProtocolSpec(file, env = process.env, platform = process.platform) {
76
- const url = 'vscode://file/' + String(file).split(path.sep).join('/')
76
+ // 反斜杠一律转正斜杠:协议 URL 只认 `/`(`C:\t\a.js` 直接拼进 URL 会被 VSCode 当非法路径)。
77
+ // 按**目标平台**的形态转,而不是宿主 `path.sep` —— 后者在 macOS / Linux 上是 `/`,
78
+ // win32 分支里那条 `C:\…` 路径永远不会被转,测试与「跨平台拉起」都会静默失效。
79
+ const url = 'vscode://file/' + String(file).replace(/\\/g, '/')
77
80
  if (platform === 'darwin') return { cmd: 'open', args: [url], shell: false }
78
81
  if (platform === 'win32') {
79
82
  return { command: `start "" "${url}"`, shell: true }
@@ -291,6 +294,7 @@ export function createCodeBridgeMiddleware({ notify = null, watchFile = null } =
291
294
  const readBody = (req, limit = MAX_BODY) =>
292
295
  new Promise((resolve, reject) => {
293
296
  let body = ''
297
+ req.setEncoding('utf8')
294
298
  req.on('data', (c) => {
295
299
  body += c
296
300
  if (body.length > limit) {
@@ -226,6 +226,7 @@ export function createLogMiddleware() {
226
226
  const readBody = (req, limit = MAX_BODY) =>
227
227
  new Promise((resolve, reject) => {
228
228
  let body = ''
229
+ req.setEncoding('utf8')
229
230
  req.on('data', (c) => {
230
231
  body += c
231
232
  if (body.length > limit) {
@@ -10,6 +10,12 @@ import path from 'node:path'
10
10
  * - Vite(5180):编辑器预览同源访问 /slices/*;
11
11
  * - cardbuddy(8787):本地开发服务若直接托管预览页,也能取到切图。
12
12
  * 仅服务本地生成产物,不做任何远端回源(远端切图走 assetProxy 的 /minio 等前缀)。
13
+ *
14
+ * **未命中一律 404,禁止 next() 落 SPA 兜底**(2026-09-21 事故):
15
+ * 此前未命中走 `next()`,SPA 兜底对任意路径都返回 `200 + text/html`,于是「引用路径写错 /
16
+ * 切图名含 `#` 被浏览器截断」表现为 —— 图片空白、网络面板满屏 200、控制台零报错,
17
+ * 30 分钟无人定位。返回 404 后同一故障会立刻在 devtools 里显示为红色 404,
18
+ * 这是「坏图必须吵」的最小代价。
13
19
  */
14
20
 
15
21
  const MIME = {
@@ -27,15 +33,29 @@ const MIME = {
27
33
  */
28
34
  export function createSliceStaticMiddleware({ root }) {
29
35
  const slicesRoot = path.join(root, 'src', 'slices')
36
+ const notFound = (res, reason) => {
37
+ res.statusCode = 404
38
+ res.setHeader('Content-Type', 'text/plain; charset=utf-8')
39
+ res.setHeader('Cache-Control', 'no-store')
40
+ res.end(`slice not found: ${reason}`)
41
+ }
30
42
  return (req, res, next) => {
31
43
  const url = (req.url || '').split('?')[0]
32
44
  if (!url.startsWith('/slices/')) return next()
33
- const rel = decodeURIComponent(url.slice('/slices/'.length))
45
+ const raw = url.slice('/slices/'.length)
46
+ let rel
47
+ try {
48
+ rel = decodeURIComponent(raw)
49
+ } catch {
50
+ return notFound(res, `${raw}(percent 编码非法,多半是文件名里带了 % 之类的保留字符)`)
51
+ }
34
52
  // 防目录穿越:禁止 .. 且解析后必须仍在 src/slices 内
35
- if (rel.includes('..')) return next()
53
+ if (!rel || rel.includes('..')) return notFound(res, `${raw}(非法路径)`)
36
54
  const file = path.join(slicesRoot, rel)
37
- if (!file.startsWith(slicesRoot + path.sep) && file !== slicesRoot) return next()
38
- if (!fs.existsSync(file) || !fs.statSync(file).isFile()) return next()
55
+ if (!file.startsWith(slicesRoot + path.sep) && file !== slicesRoot) return notFound(res, `${raw}(越出切图目录)`)
56
+ if (!fs.existsSync(file) || !fs.statSync(file).isFile()) {
57
+ return notFound(res, `${raw}(文件不存在于 src/slices;若 schema 里引用的是这个路径,检查文件名是否含 # ? % & + 或空格)`)
58
+ }
39
59
  const ext = path.extname(file).toLowerCase()
40
60
  res.setHeader('Content-Type', MIME[ext] || 'application/octet-stream')
41
61
  res.setHeader('Cache-Control', 'public, max-age=0')
@@ -26,6 +26,7 @@ export function createSliceUploadMiddleware({ root }) {
26
26
  if ((req.method || 'GET').toUpperCase() !== 'POST') return next()
27
27
 
28
28
  let body = ''
29
+ req.setEncoding('utf8')
29
30
  req.on('data', (c) => {
30
31
  body += c
31
32
  if (body.length > 64 * 1024) req.destroy()
@@ -218,6 +218,8 @@ export class TurnJournal {
218
218
  id: `srv-${this.turnId}`,
219
219
  role: 'assistant',
220
220
  content: '',
221
+ // 过程性正文的切点:最后一次工具调用之前的正文字数(前端据此折叠,见 runtime/ai-narration.js)
222
+ narrationChars: 0,
221
223
  reasoning: '',
222
224
  reasoningMs: 0,
223
225
  toolCalls: [],
@@ -252,6 +254,9 @@ export class TurnJournal {
252
254
  msg.content += l.text || ''
253
255
  break
254
256
  case 'tool-call-start': {
257
+ // 工具调用之前累积的正文 = 过程性正文(模型把像素比对 / 逐行对齐这类推理写进正文时,
258
+ // 这段能占几万字)。取最后一次工具调用的切点:它之后的才是给用户的收尾结论。
259
+ if (msg.content) msg.narrationChars = msg.content.length
255
260
  const row = rowOf(l.toolCallId, l.toolName)
256
261
  row.toolName = l.toolName || row.toolName
257
262
  row.startedAt = row.startedAt || l.ts
@@ -259,6 +264,7 @@ export class TurnJournal {
259
264
  break
260
265
  }
261
266
  case 'tool-call': {
267
+ if (msg.content) msg.narrationChars = msg.content.length
262
268
  const row = rowOf(l.toolCallId, l.toolName)
263
269
  row.toolName = l.toolName || row.toolName
264
270
  row.args = l.args || {}