cardbuddy 0.13.3 → 0.14.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.
- package/README.md +7 -3
- package/dist/cli.js +4 -4
- package/dist/config.js +7 -7
- package/dist/init.js +15 -4
- package/dist/portGuard.js +2 -2
- package/dist/preview-editor/assets/index-DnXqKrCc.js +307 -0
- package/dist/preview-editor/assets/index-cbTCHp8J.css +1 -0
- package/dist/preview-editor/index.html +2 -2
- package/dist/standalone.js +14 -6
- package/dist/vendored/card-editor/api.js +6 -6
- package/dist/vendored/mcp/client.mjs +53 -3
- package/dist/vendored/mcp/tools.mjs +17 -1
- package/dist/vendored/server/core/ai-chat.js +1443 -271
- package/dist/vendored/server/core/cardFiles.js +271 -39
- package/dist/vendored/server/core/cardImportMeta.js +16 -0
- package/dist/vendored/server/core/chatHistory.js +122 -0
- package/dist/vendored/server/core/index.js +10 -3
- package/dist/vendored/server/core/turnJournal.js +386 -0
- package/dist/vendored/skills/card-skill-trainer/SKILL.md +399 -222
- package/dist/vendored/skills/card-skill-trainer/scripts/build_dataset.py +103 -19
- package/dist/vendored/skills/card-skill-trainer/scripts/diagnose_case.mjs +378 -200
- package/dist/vendored/skills/card-skill-trainer/scripts/lib/runs.mjs +147 -0
- package/dist/vendored/skills/card-skill-trainer/scripts/lib/scoring.mjs +297 -4
- package/dist/vendored/skills/card-skill-trainer/scripts/plan_next_round.mjs +484 -0
- package/dist/vendored/skills/card-skill-trainer/scripts/round_diff.mjs +77 -96
- package/dist/vendored/skills/card-skill-trainer/scripts/run_round.mjs +237 -62
- package/dist/vendored/skills/card-skill-trainer/scripts/score_round.mjs +74 -35
- package/dist/vendored/skills/dsl-card-ai/SKILL.md +15 -2
- package/dist/vendored/skills/dsl-card-generator/SKILL.md +5 -3
- package/dist/vendored/skills/dsl-card-generator/references/generation_pitfalls.md +120 -0
- package/dist/vendored/skills/dsl-card-generator/references/previewer_contract.md +10 -0
- package/dist/vendored/skills/dsl-card-generator/references/quickref.md +15 -1
- package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/canvas-radar.json +2 -2
- package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/scatter-pill-band.json +10 -0
- package/dist/vendored/skills/dsl-card-generator/references/recipes.md +43 -1
- package/dist/vendored/skills/dsl-card-generator/references/rules.md +33 -6
- package/dist/vendored/skills/dsl-card-generator/references/slices_convention.md +12 -5
- package/dist/vendored/skills/dsl-card-generator/scripts/add_component.mjs +5 -10
- package/dist/vendored/skills/dsl-card-generator/scripts/crop_slices.mjs +47 -10
- package/dist/vendored/skills/dsl-card-generator/scripts/expand_ir.mjs +12 -4
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/design_sample.mjs +388 -328
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/slice_pixels.mjs +109 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/render_card.mjs +45 -7
- package/dist/vendored/skills/dsl-card-generator/scripts/route_input.mjs +7 -3
- package/dist/vendored/skills/dsl-card-generator/scripts/spec_audit.mjs +110 -23
- package/dist/vendored/skills/dsl-card-generator/scripts/spec_to_schema_v3.mjs +2954 -127
- package/dist/vendored/skills/dsl-card-generator/scripts/spec_tree.mjs +128 -3
- package/dist/vendored/skills/dsl-card-generator/scripts/validate_schema.py +28 -0
- package/dist/vendored/skills/finmall-card-sync/SKILL.md +1 -1
- package/dist/vendored/skills/finmall-session-navigator/SKILL.md +1 -1
- package/dist/vendored/skills/finmall-session-navigator/scripts/session-manager.mjs +1 -1
- package/dist/vendored/skills/shared/finmall-config.mjs +4 -4
- package/dist/vendored/skills/sketch-spec-to-card/SKILL.md +2 -2
- package/dist/vendored/skills/sketch-spec-to-card/scripts/parse_meaxure.py +29 -2
- package/dist/vendored/tools/finmall-env.mjs +6 -6
- package/package.json +1 -1
- package/dist/preview-editor/assets/index-CQScMhyg.css +0 -1
- package/dist/preview-editor/assets/index-pW1J4vP3.js +0 -276
- package/dist/vendored/skills/card-skill-trainer/baselines/v3-r038n.json +0 -43
- package/dist/vendored/skills/card-skill-trainer/baselines/v3-r039n.json +0 -47
- package/dist/vendored/skills/card-skill-trainer/baselines/v3-r050n.json +0 -48
|
@@ -32,7 +32,7 @@ import fs from 'node:fs'
|
|
|
32
32
|
import path from 'node:path'
|
|
33
33
|
|
|
34
34
|
function parseArgs(argv) {
|
|
35
|
-
const out = { spec: '', card: 1, maxDepth: 0, compact: false, json: false, denoise: true }
|
|
35
|
+
const out = { spec: '', card: 1, maxDepth: 0, compact: false, json: false, denoise: true, listCards: '', uiDir: '' }
|
|
36
36
|
for (let i = 0; i < argv.length; i++) {
|
|
37
37
|
const a = argv[i]
|
|
38
38
|
if (a === '--spec') out.spec = argv[++i] || ''
|
|
@@ -41,6 +41,7 @@ function parseArgs(argv) {
|
|
|
41
41
|
else if (a === '--compact') out.compact = true
|
|
42
42
|
else if (a === '--no-denoise') out.denoise = false
|
|
43
43
|
else if (a === '--json') out.json = true
|
|
44
|
+
else if (a === '--list-cards') { out.listCards = 'on'; out.uiDir = argv[++i] || '' }
|
|
44
45
|
else if (a === '-h' || a === '--help') {
|
|
45
46
|
console.log(`Usage: node spec_tree.mjs --spec <spec.json> [--card 1] [--max-depth 0] [--compact] [--no-denoise] [--json]
|
|
46
47
|
--spec spec.json 路径
|
|
@@ -48,17 +49,109 @@ function parseArgs(argv) {
|
|
|
48
49
|
--max-depth 最大显示深度(0=不限)
|
|
49
50
|
--compact 极简模式(仅类型+名称,不显示样式)
|
|
50
51
|
--no-denoise 关闭去噪(默认开:剥离 #tmp- 前缀、无语义编组名→_group、折叠单子节点组、合并连续相同兄弟)
|
|
51
|
-
--json 输出几何重建树 JSON
|
|
52
|
+
--json 输出几何重建树 JSON
|
|
53
|
+
--list-cards <UI目录> 扫描 UI 产物目录,列出每个画板下的卡片清单(序号 画板.卡片)`)
|
|
52
54
|
process.exit(0)
|
|
53
55
|
}
|
|
54
56
|
}
|
|
55
|
-
if (!out.spec) {
|
|
57
|
+
if (!out.spec && !out.listCards) {
|
|
56
58
|
console.error('缺少 --spec 参数')
|
|
57
59
|
process.exit(1)
|
|
58
60
|
}
|
|
59
61
|
return out
|
|
60
62
|
}
|
|
61
63
|
|
|
64
|
+
/* ── 列卡片清单:扫描 UI 产物目录,按「画板序号.卡片序号」展开 ────── */
|
|
65
|
+
// 一个画板(spec.json)可能含多张卡片(cards[])。传统清单只列画板序号,
|
|
66
|
+
// 多卡画板缺子选择项导致用户无法指定具体哪张卡。本函数把每个画板按其内
|
|
67
|
+
// 卡片数展开为「画板序号 . 卡片序号」(如画板有 2 张卡则输出 1.1 和 1.2),
|
|
68
|
+
// 同一画板共享一张预览图(artboard.png),单卡画板退化为「画板序号 . 1」。
|
|
69
|
+
function listCardEntries(uiDir) {
|
|
70
|
+
const dir = path.resolve(uiDir)
|
|
71
|
+
if (!fs.existsSync(dir)) {
|
|
72
|
+
console.error(`UI 目录不存在: ${dir}`)
|
|
73
|
+
process.exit(1)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 当前目录自身可能就是单画板产物(spec.json + card-N.png 直接在 uiDir 下)
|
|
77
|
+
const ownSpec = path.join(dir, 'spec.json')
|
|
78
|
+
// 收集所有含 spec.json 的画板目录(平铺 UI/<画板>/ 或分片 UI/<稿>/<画板>/)
|
|
79
|
+
const boardDirs = []
|
|
80
|
+
if (fs.existsSync(ownSpec)) {
|
|
81
|
+
boardDirs.push({ dir: dir, name: path.basename(dir) })
|
|
82
|
+
}
|
|
83
|
+
const topEntries = fs.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory())
|
|
84
|
+
for (const te of topEntries) {
|
|
85
|
+
const subDir = path.join(dir, te.name)
|
|
86
|
+
const ownSpec = path.join(subDir, 'spec.json')
|
|
87
|
+
if (fs.existsSync(ownSpec)) {
|
|
88
|
+
boardDirs.push({ dir: subDir, name: te.name })
|
|
89
|
+
} else {
|
|
90
|
+
// 分片模式:UI/<稿名>/<画板>/
|
|
91
|
+
const inner = fs.readdirSync(subDir, { withFileTypes: true }).filter((e) => e.isDirectory())
|
|
92
|
+
for (const ie of inner) {
|
|
93
|
+
const innerSpec = path.join(subDir, ie.name, 'spec.json')
|
|
94
|
+
if (fs.existsSync(innerSpec)) boardDirs.push({ dir: path.join(subDir, ie.name), name: te.name + '/' + ie.name })
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 尝试从 manifest.json 取画板序号(order 字段,1 起);无 manifest 则按目录顺序
|
|
100
|
+
let canvasOrder = null
|
|
101
|
+
const manifestPath = path.join(dir, 'manifest.json')
|
|
102
|
+
if (fs.existsSync(manifestPath)) {
|
|
103
|
+
try {
|
|
104
|
+
const mf = JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
|
|
105
|
+
if (Array.isArray(mf.canvases)) canvasOrder = mf.canvases
|
|
106
|
+
} catch (e) { /* manifest 损坏,退化为目录序 */ }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const entries = []
|
|
110
|
+
let boardSeq = 0
|
|
111
|
+
for (const bd of boardDirs) {
|
|
112
|
+
boardSeq += 1
|
|
113
|
+
// 如果 manifest 有 order,优先用 order;否则用递增序
|
|
114
|
+
let order = boardSeq
|
|
115
|
+
if (canvasOrder) {
|
|
116
|
+
// 全局 manifest 的 order 是与导出画板顺序对齐的(可能从 1 也可能从 27 起)
|
|
117
|
+
const match = canvasOrder.find((c) => c.name === bd.name || bd.name.endsWith('/' + c.name))
|
|
118
|
+
if (match) order = match.order || boardSeq
|
|
119
|
+
} else {
|
|
120
|
+
// 无全局 manifest 时用连续递增(1, 2, 3...),避免跳号
|
|
121
|
+
order = boardSeq
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let spec
|
|
125
|
+
try { spec = JSON.parse(fs.readFileSync(path.join(bd.dir, 'spec.json'), 'utf8')) } catch (e) { continue }
|
|
126
|
+
const cards = Array.isArray(spec.cards) ? spec.cards : []
|
|
127
|
+
const cardCount = cards.length || 1
|
|
128
|
+
|
|
129
|
+
// 预览图:同一画板共享一张 artboard.png(或 card-1.png 作降级)
|
|
130
|
+
let preview = path.join(bd.dir, 'artboard.png')
|
|
131
|
+
if (!fs.existsSync(preview)) {
|
|
132
|
+
preview = path.join(bd.dir, 'card-1.png')
|
|
133
|
+
if (!fs.existsSync(preview)) preview = null
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
for (let ci = 0; ci < cardCount; ci += 1) {
|
|
137
|
+
const cardImg = path.join(bd.dir, `card-${ci + 1}.png`)
|
|
138
|
+
entries.push({
|
|
139
|
+
seq: `${order}.${ci + 1}`,
|
|
140
|
+
boardName: spec.canvas || bd.name,
|
|
141
|
+
boardDir: bd.dir,
|
|
142
|
+
boardSeq: order,
|
|
143
|
+
cardIndex: ci + 1,
|
|
144
|
+
cardCount,
|
|
145
|
+
cardImg: fs.existsSync(cardImg) ? cardImg : null,
|
|
146
|
+
preview,
|
|
147
|
+
cardRect: cards[ci] ? cards[ci].rect : null,
|
|
148
|
+
trainingEligible: cards[ci] ? cards[ci].trainingEligible : undefined,
|
|
149
|
+
artboardType: spec.artboardType || undefined,
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return entries
|
|
154
|
+
}
|
|
62
155
|
/* ── 几何重建:从扁平 elements 用矩形包含关系建树 ────────────── */
|
|
63
156
|
// 父 = 最小面积、且完全包含子 rect 的元素(严格父:面积必须更大)。
|
|
64
157
|
function reconstructFromElements(elements) {
|
|
@@ -263,6 +356,38 @@ function renderTree(nodes, depth, maxDepth, compact, prefix, isLast, lines) {
|
|
|
263
356
|
/* ── 主流程 ─────────────────────────────────────────────────── */
|
|
264
357
|
function main() {
|
|
265
358
|
const args = parseArgs(process.argv.slice(2))
|
|
359
|
+
|
|
360
|
+
// ── --list-cards 模式:扫描 UI 产物目录,按「画板序号.卡片序号」列出全量清单 ──
|
|
361
|
+
if (args.listCards === 'on') {
|
|
362
|
+
if (!args.uiDir) {
|
|
363
|
+
console.error('--list-cards 需要指定 UI 产物目录(如 UI/<稿名> 或 UI/)')
|
|
364
|
+
process.exit(1)
|
|
365
|
+
}
|
|
366
|
+
const entries = listCardEntries(args.uiDir)
|
|
367
|
+
if (!entries.length) {
|
|
368
|
+
console.error('该目录下未找到任何含 spec.json 的画板')
|
|
369
|
+
process.exit(1)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// 统计画板数与卡片数
|
|
373
|
+
const boardSeqs = [...new Set(entries.map((e) => e.boardSeq))]
|
|
374
|
+
console.log(`# 画板 ${boardSeqs.length} 个 → 卡片 ${entries.length} 张(序号 画板.卡片)`)
|
|
375
|
+
console.log('')
|
|
376
|
+
|
|
377
|
+
entries.forEach((e) => {
|
|
378
|
+
const multiTag = e.cardCount > 1 ? ` (${e.cardCount} 张卡)` : ''
|
|
379
|
+
const rectTag = e.cardRect ? ` [${e.cardRect.width || 0}x${e.cardRect.height || 0}]` : ''
|
|
380
|
+
const typeTag = e.artboardType ? ` type=${e.artboardType}` : ''
|
|
381
|
+
console.log(`${e.seq}. ${e.boardName}${typeTag}${multiTag}${rectTag}`)
|
|
382
|
+
if (e.cardImg) console.log(` 图: ${e.cardImg}`)
|
|
383
|
+
else if (e.preview) console.log(` 图: ${e.preview} (共享画板预览图,无单独 card-${e.cardIndex}.png)`)
|
|
384
|
+
else console.log(` 图: (预览图缺失)`)
|
|
385
|
+
console.log(` spec: ${path.join(e.boardDir, 'spec.json')} --card ${e.cardIndex}`)
|
|
386
|
+
})
|
|
387
|
+
console.log('')
|
|
388
|
+
console.log('# 全部')
|
|
389
|
+
return
|
|
390
|
+
}
|
|
266
391
|
const specPath = path.resolve(args.spec)
|
|
267
392
|
if (!fs.existsSync(specPath)) {
|
|
268
393
|
console.error(`spec.json 不存在: ${specPath}`)
|
|
@@ -206,6 +206,27 @@ def load_meta():
|
|
|
206
206
|
# ---------------------------------------------------------------------------
|
|
207
207
|
# Helpers
|
|
208
208
|
# ---------------------------------------------------------------------------
|
|
209
|
+
def cid_tag(cid):
|
|
210
|
+
"""组件定位标记:消息末尾的 " #<componentId>",前端渲染成可点链接,点一下就在画布 /
|
|
211
|
+
结构树里选中该节点(校验结论直接变成「可操作」,不用人再去树里翻)。
|
|
212
|
+
|
|
213
|
+
为什么不用 depth[i]:check_components 递归时按「单个子节点」下钻
|
|
214
|
+
(check_components([child], depth + 1)),嵌套层的 i 恒为 0,光看 "depth 2[0]"
|
|
215
|
+
根本不知道是哪个节点;componentId 全卡唯一,是唯一稳定的定位键。缺失时不打标记,
|
|
216
|
+
前端退回纯文本,不影响校验结论本身。
|
|
217
|
+
"""
|
|
218
|
+
return f" #{cid}" if cid else ""
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def tag_messages(messages, start, tag):
|
|
222
|
+
"""给 messages[start:] 新产生的消息统一补定位标记(幂等:已带同一标记的不重复追加)。"""
|
|
223
|
+
if not tag:
|
|
224
|
+
return
|
|
225
|
+
for k in range(start, len(messages)):
|
|
226
|
+
if not messages[k].endswith(tag):
|
|
227
|
+
messages[k] = f"{messages[k]}{tag}"
|
|
228
|
+
|
|
229
|
+
|
|
209
230
|
def check_value_from(item, owner, errors, warnings):
|
|
210
231
|
"""itemValueFrom 缺失/空串 → 运行时静默丢弃该属性(致命),升级为 ERROR。
|
|
211
232
|
|
|
@@ -372,6 +393,8 @@ def check_components(components, errors, warnings, depth=0, style_ids=None, seen
|
|
|
372
393
|
errors.append(f"Component at depth {depth}[{i}] is not an object")
|
|
373
394
|
continue
|
|
374
395
|
|
|
396
|
+
# 本组件自身产生的消息(含各 helper 内部追加的)统一挂定位标记:见 cid_tag()
|
|
397
|
+
err_mark, warn_mark = len(errors), len(warnings)
|
|
375
398
|
cn = comp.get("className", "")
|
|
376
399
|
if not cn:
|
|
377
400
|
errors.append(f"Component at depth {depth}[{i}] missing 'className'")
|
|
@@ -407,6 +430,11 @@ def check_components(components, errors, warnings, depth=0, style_ids=None, seen
|
|
|
407
430
|
errors.append(f"Property '{pk}' in '{cn}' at depth {depth}[{i}] has itemValueFrom="
|
|
408
431
|
f"'{prop.get('itemValueFrom')}' but no itemDefaultValue")
|
|
409
432
|
|
|
433
|
+
# 到这里为止都是「本组件自身」的问题,统一挂上定位标记;子组件的消息由下面递归时
|
|
434
|
+
# 各自挂自己的标记,不能被父级污染(否则点一下会选到父节点)
|
|
435
|
+
tag_messages(errors, err_mark, cid_tag(cid))
|
|
436
|
+
tag_messages(warnings, warn_mark, cid_tag(cid))
|
|
437
|
+
|
|
410
438
|
# Slots / children
|
|
411
439
|
if cn in CONTAINER_TYPES or cn == "Select":
|
|
412
440
|
slots = comp.get("slots", [])
|
|
@@ -10,7 +10,7 @@ description: 通过 API 把 FinMall UAT 卡片列表项纯接口同步进本工
|
|
|
10
10
|
## 前置条件
|
|
11
11
|
|
|
12
12
|
1. 先使用 `finmall-session-navigator` 打开并鉴权卡片列表页。
|
|
13
|
-
2. 保持已启动的 Chrome 以远程调试模式运行(端口由 `
|
|
13
|
+
2. 保持已启动的 Chrome 以远程调试模式运行(端口由 `CARDBUDDY_CDP_PORT` 配置,默认 `9222`)。
|
|
14
14
|
3. 确保 `skills/finmall-session-navigator/scripts/node_modules/playwright` 存在;本 skill 复用该依赖。
|
|
15
15
|
|
|
16
16
|
## 环境配置
|
|
@@ -23,7 +23,7 @@ description: 使用 Playwright 独立启动 Chrome,自动登录 FinMall UAT
|
|
|
23
23
|
| `FINMALL_BASE_URL` | 服务域名 | `https://uat-pkg.finmall.com` |
|
|
24
24
|
| `FINMALL_APP_ID` | 应用 ID | `100042` |
|
|
25
25
|
| `FINMALL_WORKSPACE_ID` | 工作空间 ID | `dev` |
|
|
26
|
-
| `
|
|
26
|
+
| `CARDBUDDY_CDP_PORT` | Chrome 调试端口 | `9222` |
|
|
27
27
|
|
|
28
28
|
切换环境(如生产/测试)只需修改 `.env.cb.local`,无需改动代码。
|
|
29
29
|
|
|
@@ -16,7 +16,7 @@ const LOCAL_DIR = path.resolve(SCRIPT_DIR, "..", ".local");
|
|
|
16
16
|
// 避免两个 Chrome 实例争用同一 user-data-dir 的锁。
|
|
17
17
|
const SYNC_BROWSER_DATA_DIR = path.join(LOCAL_DIR, "sync-browser-data");
|
|
18
18
|
// 后台会话专用调试端口,与手动导航器端口隔离(手动可见、自动无界面互不干扰)。
|
|
19
|
-
// 取值走统一配置链:process.env
|
|
19
|
+
// 取值走统一配置链:process.env CARDBUDDY_SYNC_CDP_PORT > .env.cb(.local) > 内置默认。
|
|
20
20
|
const SYNC_PORT = finmallConfig.syncCdpPort;
|
|
21
21
|
|
|
22
22
|
// 排除 localhost 代理:本机若设了 HTTP_PROXY,Node 侧 connectOverCDP 走代理会
|
|
@@ -60,8 +60,8 @@ const defaults = {
|
|
|
60
60
|
FINMALL_BASE_URL: 'https://uat-pkg.finmall.com',
|
|
61
61
|
FINMALL_APP_ID: '100042',
|
|
62
62
|
FINMALL_WORKSPACE_ID: 'dev',
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
CARDBUDDY_CDP_PORT: '9222',
|
|
64
|
+
CARDBUDDY_SYNC_CDP_PORT: '9333',
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
const envFile = parseEnvFile(path.join(PROJECT_ROOT, '.env.cb'))
|
|
@@ -77,8 +77,8 @@ for (const key of Object.keys(defaults)) {
|
|
|
77
77
|
const baseUrl = merged.FINMALL_BASE_URL.replace(/\/+$/, '')
|
|
78
78
|
const appId = merged.FINMALL_APP_ID
|
|
79
79
|
const workspaceId = merged.FINMALL_WORKSPACE_ID
|
|
80
|
-
const cdpPort = Number(merged.
|
|
81
|
-
const syncCdpPort = Number(merged.
|
|
80
|
+
const cdpPort = Number(merged.CARDBUDDY_CDP_PORT) || 9222
|
|
81
|
+
const syncCdpPort = Number(merged.CARDBUDDY_SYNC_CDP_PORT) || 9333
|
|
82
82
|
|
|
83
83
|
export const finmallConfig = {
|
|
84
84
|
/** 服务域名(无尾斜杠) */
|
|
@@ -14,7 +14,7 @@ description: 读取 Sketch MeaXure 标注导出(index.html + preview/ + assets
|
|
|
14
14
|
输入是**多画板**设计稿且用户**没有在同一条消息里指定画板**时,**第一件事是选画板,不是跑管线**:
|
|
15
15
|
|
|
16
16
|
1. `python3 scripts/parse_meaxure.py --src <导出目录> --list` 机械取全量画板;
|
|
17
|
-
2. 按 **`1..n` 编号列出(序号 1 起,每行 `序号. 画板名 — 类型 / 状态`)+ 末尾一项 `全部`**;**每个画板行下面紧跟它自己那张预览图的绝对路径**(`图: <绝对路径>`;`parse_meaxure.py --list` 与门禁 exit-4 输出已机械逐行配图,取值优先 `UI/<稿>/<画板>/artboard.png` → 导出 `preview/<slug>.png`),**缺图该行显式写 `图:
|
|
17
|
+
2. 按 **`1..n` 编号列出(序号 1 起,每行 `序号. 画板名 — 类型 / 状态`)+ 末尾一项 `全部`**;**每个画板行下面紧跟它自己那张预览图的绝对路径**(`图: <绝对路径>`;`parse_meaxure.py --list` 与门禁 exit-4 输出已机械逐行配图,取值优先 `UI/<稿>/<画板>/artboard.png` → 导出 `preview/<slug>.png`),**缺图该行显式写 `图: (预览图缺失)`**——**禁止把所有预览图统一堆在清单末尾**(部分缺图时序号与图会错位);**直接输出清单**——编辑器助手没有弹选项工具(不存在 `request_user_input`,不要去调用或寻找),一律「列出清单 + 一句提问后停下等用户回序号」;
|
|
18
18
|
3. 一句纯文本提问(「请回复要处理的画板序号,或回复 `全部`」)后**停止输出、结束本轮等回复**;
|
|
19
19
|
4. 拿到回复才继续:序号 k → `--indices <k-1>`;`全部` → `--scope-all`(UI 产物全量跑;产卡时视为显式批量授权)。
|
|
20
20
|
|
|
@@ -74,7 +74,7 @@ UI/
|
|
|
74
74
|
## 2. 工作流
|
|
75
75
|
1. **解析**:`scripts/parse_meaxure.py` 从 `index.html` 提取 `let data={...}` JSON(括号配平后 `json.loads`),遍历全部画板,视觉驱动检测卡片区域 → 裁切聚焦图 → 序列化要素,生成 `manifest.json` + 各画板 `spec.json`/`card-*.png`/`slices/`;同时检测 JSAPI 交互控件(密码/验证码/toast/二次确认)与 Flex 布局。系统 chrome 由 `vision.py` 几何阈值排除(见 §4)。
|
|
76
76
|
2. **选画板(硬门禁,先于解析/生成)**:先 `parse_meaxure.py --src <导出目录> --list` **机械列出全量画板**,按 **`1..n` 编号(1 起)+ 末尾 `全部`** 交给用户,**停止输出等回复**后才跑管线。**「UI 产物允许全量生成」= 用户可以回答 `全部`(此时加 `--scope-all` 整份导出全跑,素材可复用),不是 Agent 可以默认全量**——用户消息里没指定画板时,**包括「生成产物 / 前处理」在内的任何请求都必须先过本门禁**;**禁止**自行挑画板、**禁止**替用户选「全部」、**禁止**批量产卡、**禁止**「先建一张看看」。下游卡片生成仍以用户指定的单个画板 / 单个 UI 产物为单位(铁律见 `dsl-card-generator/references/rules.md §十三`,机械卡点见门禁 0)。
|
|
77
|
-
- **选择交互(固定流程)**:① 把全量画板按 **`1..n` 编号列出**(序号 **1 起**,每行 `序号. 画板名 — 类型 / 状态`,**行下紧跟该画板自己的预览图绝对路径,缺图标缺失,禁止堆末尾**)+ 末尾追加一项 **`全部`**(清单是完整选项池,不得只列节选)。②
|
|
77
|
+
- **选择交互(固定流程)**:① 把全量画板按 **`1..n` 编号列出**(序号 **1 起**,每行 `序号. 画板名 — 类型 / 状态`,**行下紧跟该画板自己的预览图绝对路径,缺图标缺失,禁止堆末尾**)+ 末尾追加一项 **`全部`**(清单是完整选项池,不得只列节选)。② **仅当工具清单里确实列有原生弹窗工具时才用**:选项放最具代表性的 2 个画板 + `全部`(每题上限 3 项,客户端自动追加 Other),问题文案写明「其余画板在 Other 里填清单序号或画板名」;返回空答案不反复追问,降级到 ③。**编辑器助手没有这个工具**(不存在 `request_user_input`),不要去调用或寻找,直接走 ③。③ **默认路径**→ 一句纯文本提问(如「请回复要处理的画板序号,或回复 `全部`」)后**停止输出、结束本轮等用户回序号**,不得在同一轮顺手产卡。④ 回 `全部` = 整份导出全量跑 UI 产物(**允许**);回单个序号 = 只跑该画板,并作为下游产卡的目标画板。转给用户时把每行图绝对路径**逐条内联成 Markdown 图片**看图选(与序号一一对应);系统级选择弹窗不可实现(Codex 无原生弹窗控件),③ 即等价物。
|
|
78
78
|
- **清单来源与序号对齐**:已前处理的稿读 `UI/<稿名>/manifest.json` 的 `canvases[]`(`order` 1 起 / `name` / `artboardType` / `status`)与 `clusters.json` 的 `cards[]`(`cardKey` / `pageId` / `stateNames` / `recommendedBase`);`--list` 与门禁清单**展示统一 1 起**,用户回的序号 k 传 `--indices` 时 **-1(0 起)**。`overlay_dialog` 画板标注「不产卡」(补到父卡事件)。同卡多状态时选择单位是 **cardKey**,未指定状态用 `recommendedBase` 并告知。
|
|
79
79
|
- `--only` 名称子串 / `--indices` 序号 / `--sample N` 是**用户指定后的范围裁剪**;`--sample` 抽样只是取代表性素材,**不等于用户已选定要产卡的画板**。
|
|
80
80
|
3. **生成文档**:`scripts/build_prompt.py`(或 `--build` 一步式)由 `spec.json` 生成 `prompt.md` + `spec.md`(含列表检测、渐变/字体/flex 样式、JSAPI 交互)。
|
|
@@ -32,6 +32,7 @@ parse_meaxure.py — 解析 Sketch MeaXure 标注导出,视觉驱动生成「
|
|
|
32
32
|
python parse_meaxure.py --build # 一步式(含 build_prompt)
|
|
33
33
|
"""
|
|
34
34
|
import argparse
|
|
35
|
+
import hashlib
|
|
35
36
|
import json
|
|
36
37
|
import math
|
|
37
38
|
import os
|
|
@@ -1245,6 +1246,17 @@ def mark_missing_slice_elements(spec, preview_path, asset_index, asset_root=None
|
|
|
1245
1246
|
defects.append(defect)
|
|
1246
1247
|
return defect
|
|
1247
1248
|
|
|
1249
|
+
# 裁切产物按内容 sha256 去重(同一图标在多个位置/多次缺失时被反复裁 → 字节级重复副本)。
|
|
1250
|
+
# 索引含 crop_dir 里既有的裁切图(跨轮次复用);命中即复用既有文件名,不新写一份。
|
|
1251
|
+
crop_sha_index = {}
|
|
1252
|
+
if crop_dir:
|
|
1253
|
+
crop_dir.mkdir(parents=True, exist_ok=True)
|
|
1254
|
+
for existing in crop_dir.glob("*.png"):
|
|
1255
|
+
try:
|
|
1256
|
+
crop_sha_index[hashlib.sha256(existing.read_bytes()).hexdigest()] = existing.name
|
|
1257
|
+
except OSError:
|
|
1258
|
+
continue
|
|
1259
|
+
|
|
1248
1260
|
def crop_from_artboard(element, defect):
|
|
1249
1261
|
rect = element.get("rect") or {}
|
|
1250
1262
|
width = float(rect.get("width", 0))
|
|
@@ -1259,16 +1271,31 @@ def mark_missing_slice_elements(spec, preview_path, asset_index, asset_root=None
|
|
|
1259
1271
|
round(width),
|
|
1260
1272
|
round(height),
|
|
1261
1273
|
)
|
|
1274
|
+
part = crop_dir / (filename + ".part")
|
|
1262
1275
|
try:
|
|
1263
1276
|
png_utils.crop_png(
|
|
1264
1277
|
preview_path,
|
|
1265
1278
|
(float(rect.get("x", 0)), float(rect.get("y", 0)), width, height),
|
|
1266
|
-
|
|
1279
|
+
part,
|
|
1267
1280
|
scale,
|
|
1268
1281
|
)
|
|
1269
|
-
|
|
1282
|
+
data = part.read_bytes()
|
|
1283
|
+
sha = hashlib.sha256(data).hexdigest()
|
|
1284
|
+
twin = crop_sha_index.get(sha)
|
|
1285
|
+
if twin:
|
|
1286
|
+
part.unlink(missing_ok=True)
|
|
1287
|
+
filename = twin
|
|
1288
|
+
defect["resolution"] = "artboard-crop-reused"
|
|
1289
|
+
else:
|
|
1290
|
+
os.replace(part, crop_dir / filename)
|
|
1291
|
+
crop_sha_index[sha] = filename
|
|
1292
|
+
defect["resolution"] = "artboard-crop"
|
|
1270
1293
|
defect["cropSlice"] = filename
|
|
1271
1294
|
except (OSError, ValueError):
|
|
1295
|
+
try:
|
|
1296
|
+
part.unlink(missing_ok=True)
|
|
1297
|
+
except OSError:
|
|
1298
|
+
pass
|
|
1272
1299
|
return
|
|
1273
1300
|
|
|
1274
1301
|
for element in spec.get("elements") or []:
|
|
@@ -32,7 +32,7 @@ function parseEnvFile(file) {
|
|
|
32
32
|
* @param {string} [root] 项目根(含 .env.cb);默认 process.cwd()
|
|
33
33
|
* @returns {{
|
|
34
34
|
* FINMALL_BASE_URL:string, FINMALL_APP_ID:string, FINMALL_WORKSPACE_ID:string,
|
|
35
|
-
*
|
|
35
|
+
* CARDBUDDY_CDP_PORT:number, CARDBUDDY_DEV_PORT:number, FINMALL_EDITOR_PATH:string
|
|
36
36
|
* }}
|
|
37
37
|
*/
|
|
38
38
|
export function loadFinmallEnv(root = process.cwd()) {
|
|
@@ -48,9 +48,9 @@ export function loadFinmallEnv(root = process.cwd()) {
|
|
|
48
48
|
/** 工作空间标识 */
|
|
49
49
|
FINMALL_WORKSPACE_ID: env.FINMALL_WORKSPACE_ID || 'dev',
|
|
50
50
|
/** Chrome 远程调试端口(connectOverCDP 用) */
|
|
51
|
-
|
|
51
|
+
CARDBUDDY_CDP_PORT: Number(env.CARDBUDDY_CDP_PORT || 9222),
|
|
52
52
|
/** 本地 dev server 监听端口(vite / 截图 / MCP / 各类工具默认连此端口) */
|
|
53
|
-
|
|
53
|
+
CARDBUDDY_DEV_PORT: Number(env.CARDBUDDY_DEV_PORT || 5180),
|
|
54
54
|
/** 远端编辑器 Web 访问别名(站点下挂载路径,部署到别的目录时改这里) */
|
|
55
55
|
FINMALL_EDITOR_PATH: (env.FINMALL_EDITOR_PATH || '/ubomp-h5-sub').replace(/\/+$/, ''),
|
|
56
56
|
}
|
|
@@ -62,8 +62,8 @@ const d = loadFinmallEnv(process.cwd())
|
|
|
62
62
|
export const FINMALL_BASE_URL = d.FINMALL_BASE_URL
|
|
63
63
|
export const FINMALL_APP_ID = d.FINMALL_APP_ID
|
|
64
64
|
export const FINMALL_WORKSPACE_ID = d.FINMALL_WORKSPACE_ID
|
|
65
|
-
export const
|
|
66
|
-
export const
|
|
65
|
+
export const CARDBUDDY_CDP_PORT = d.CARDBUDDY_CDP_PORT
|
|
66
|
+
export const CARDBUDDY_DEV_PORT = d.CARDBUDDY_DEV_PORT
|
|
67
67
|
export const FINMALL_EDITOR_PATH = d.FINMALL_EDITOR_PATH
|
|
68
68
|
|
|
69
69
|
/**
|
|
@@ -73,7 +73,7 @@ export const FINMALL_EDITOR_PATH = d.FINMALL_EDITOR_PATH
|
|
|
73
73
|
* @returns {string}
|
|
74
74
|
*/
|
|
75
75
|
export function devApiBase(host = '127.0.0.1') {
|
|
76
|
-
return `http://${host}:${
|
|
76
|
+
return `http://${host}:${CARDBUDDY_DEV_PORT}`
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cardbuddy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.1",
|
|
4
4
|
"description": "Local dev service for the CardBuddy card editor — decouples card CRUD (sync/save/validate/render-status/screenshot) from the static public editor, so the editor can be deployed to any static host while each developer runs a local service against their own card source.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|