cardbuddy 0.14.3 → 0.16.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 (108) hide show
  1. package/dist/cli.js +0 -0
  2. package/dist/preview-editor/assets/index-BkbBxDTT.css +1 -0
  3. package/dist/preview-editor/assets/index-Bro07DTa.js +313 -0
  4. package/dist/preview-editor/index.html +2 -2
  5. package/dist/standalone.js +3 -0
  6. package/dist/vendored/card-editor/runtime/adapter.js +2 -2
  7. package/dist/vendored/mcp/client.mjs +9 -2
  8. package/dist/vendored/server/core/ai-chat.js +1702 -134
  9. package/dist/vendored/server/core/browser.js +12 -3
  10. package/dist/vendored/server/core/cardFiles.js +3 -0
  11. package/dist/vendored/server/core/cardHistory.js +1 -0
  12. package/dist/vendored/server/core/cardImportMeta.js +7 -6
  13. package/dist/vendored/server/core/cardOpenFolder.js +1 -0
  14. package/dist/vendored/server/core/cardSave.js +1 -0
  15. package/dist/vendored/server/core/cardSync.js +3 -0
  16. package/dist/vendored/server/core/cardValidate.js +16 -5
  17. package/dist/vendored/server/core/codeBridge.js +5 -1
  18. package/dist/vendored/server/core/logStream.js +1 -0
  19. package/dist/vendored/server/core/sliceStatic.js +24 -4
  20. package/dist/vendored/server/core/sliceUpload.js +1 -0
  21. package/dist/vendored/server/core/turnJournal.js +6 -0
  22. package/dist/vendored/skills/card-skill-trainer/SKILL.md +257 -201
  23. package/dist/vendored/skills/card-skill-trainer/scripts/diagnose_case.mjs +14 -82
  24. package/dist/vendored/skills/card-skill-trainer/scripts/lib/diagnosis.mjs +207 -0
  25. package/dist/vendored/skills/card-skill-trainer/scripts/lib/layout-correction-sim.mjs +62 -0
  26. package/dist/vendored/skills/card-skill-trainer/scripts/lib/paradigm-router.mjs +76 -0
  27. package/dist/vendored/skills/card-skill-trainer/scripts/lib/pixel-worker-runner.mjs +31 -0
  28. package/dist/vendored/skills/card-skill-trainer/scripts/lib/pixel-worker.mjs +138 -0
  29. package/dist/vendored/skills/card-skill-trainer/scripts/lib/scoring.mjs +24 -20
  30. package/dist/vendored/skills/card-skill-trainer/scripts/lib/telemetry.mjs +46 -0
  31. package/dist/vendored/skills/card-skill-trainer/scripts/plan_next_round.mjs +4 -1
  32. package/dist/vendored/skills/card-skill-trainer/scripts/run_round.mjs +279 -90
  33. package/dist/vendored/skills/card-skill-trainer/scripts/score_round.mjs +74 -2
  34. package/dist/vendored/skills/card-skill-trainer/scripts/sink_rules.mjs +127 -0
  35. package/dist/vendored/skills/dsl-card-ai/SKILL.md +1 -1
  36. package/dist/vendored/skills/dsl-card-generator/SKILL.md +208 -380
  37. package/dist/vendored/skills/dsl-card-generator/references/dsl_structure.md +1 -15
  38. package/dist/vendored/skills/dsl-card-generator/references/expression_engine.md +11 -20
  39. package/dist/vendored/skills/dsl-card-generator/references/generation_pitfalls.md +233 -61
  40. package/dist/vendored/skills/dsl-card-generator/references/ir-format.md +202 -4
  41. package/dist/vendored/skills/dsl-card-generator/references/iteration_protocol.md +87 -0
  42. package/dist/vendored/skills/dsl-card-generator/references/previewer_contract.md +1 -1
  43. package/dist/vendored/skills/dsl-card-generator/references/quickref.md +42 -33
  44. package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/title-underline-overlay.json +59 -11
  45. package/dist/vendored/skills/dsl-card-generator/references/recipes.md +44 -8
  46. package/dist/vendored/skills/dsl-card-generator/references/rules.md +26 -6
  47. package/dist/vendored/skills/dsl-card-generator/references/scripts_reference.md +54 -0
  48. package/dist/vendored/skills/dsl-card-generator/references/semantic_adjudication.md +23 -0
  49. package/dist/vendored/skills/dsl-card-generator/references/slices_convention.md +6 -6
  50. package/dist/vendored/skills/dsl-card-generator/references/spec_to_schema_patterns.md +33 -4
  51. package/dist/vendored/skills/dsl-card-generator/references/ui_fidelity_rules.md +13 -1
  52. package/dist/vendored/skills/dsl-card-generator/scripts/add_component.mjs +5 -14
  53. package/dist/vendored/skills/dsl-card-generator/scripts/autofix_schema.py +133 -2
  54. package/dist/vendored/skills/dsl-card-generator/scripts/check_fidelity.mjs +226 -19
  55. package/dist/vendored/skills/dsl-card-generator/scripts/check_magic_values.mjs +6 -1
  56. package/dist/vendored/skills/dsl-card-generator/scripts/check_topology.py +0 -35
  57. package/dist/vendored/skills/dsl-card-generator/scripts/crop_slices.mjs +92 -6
  58. package/dist/vendored/skills/dsl-card-generator/scripts/expand_ir.mjs +277 -27
  59. package/dist/vendored/skills/dsl-card-generator/scripts/gate.mjs +147 -0
  60. package/dist/vendored/skills/dsl-card-generator/scripts/generate_cards.mjs +12 -8
  61. package/dist/vendored/skills/dsl-card-generator/scripts/ir_merge.mjs +288 -0
  62. package/dist/vendored/skills/dsl-card-generator/scripts/ir_restore.mjs +175 -0
  63. package/dist/vendored/skills/dsl-card-generator/scripts/iterate.mjs +1015 -54
  64. package/dist/vendored/skills/dsl-card-generator/scripts/lib/component_contract.mjs +315 -0
  65. package/dist/vendored/skills/dsl-card-generator/scripts/lib/geometry_audit.mjs +131 -0
  66. package/dist/vendored/skills/dsl-card-generator/scripts/lib/layout-tokens.mjs +1 -1
  67. package/dist/vendored/skills/dsl-card-generator/scripts/lib/layout_rules.mjs +160 -0
  68. package/dist/vendored/skills/dsl-card-generator/scripts/lib/paths.mjs +23 -0
  69. package/dist/vendored/skills/dsl-card-generator/scripts/lib/paths.py +0 -37
  70. package/dist/vendored/skills/dsl-card-generator/scripts/lib/python.mjs +99 -0
  71. package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/adaptive-width.mjs +105 -0
  72. package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/bindings.mjs +260 -0
  73. package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/expr.mjs +52 -0
  74. package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/image.mjs +96 -0
  75. package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/registry.mjs +166 -0
  76. package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/structure.mjs +127 -0
  77. package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/walk.mjs +61 -0
  78. package/dist/vendored/skills/dsl-card-generator/scripts/lib/scope_alias.mjs +125 -0
  79. package/dist/vendored/skills/dsl-card-generator/scripts/lib/slicePath.mjs +17 -5
  80. package/dist/vendored/skills/dsl-card-generator/scripts/lib/slice_dedupe.mjs +102 -0
  81. package/dist/vendored/skills/dsl-card-generator/scripts/lib/v3_assemble.mjs +5727 -0
  82. package/dist/vendored/skills/dsl-card-generator/scripts/lib/v3_extract.mjs +4214 -0
  83. package/dist/vendored/skills/dsl-card-generator/scripts/lib/v3_shared.mjs +1110 -0
  84. package/dist/vendored/skills/dsl-card-generator/scripts/lint_card.mjs +322 -71
  85. package/dist/vendored/skills/dsl-card-generator/scripts/preflight.mjs +87 -43
  86. package/dist/vendored/skills/dsl-card-generator/scripts/readiness.mjs +634 -0
  87. package/dist/vendored/skills/dsl-card-generator/scripts/register_card_template.mjs +5 -8
  88. package/dist/vendored/skills/dsl-card-generator/scripts/render_card.mjs +91 -43
  89. package/dist/vendored/skills/dsl-card-generator/scripts/route_input.mjs +7 -11
  90. package/dist/vendored/skills/dsl-card-generator/scripts/spec_audit.mjs +272 -46
  91. package/dist/vendored/skills/dsl-card-generator/scripts/spec_to_schema_v3.mjs +43 -10499
  92. package/dist/vendored/skills/dsl-card-generator/scripts/v3_regression.mjs +188 -0
  93. package/dist/vendored/skills/dsl-card-generator/scripts/validate_schema.py +23 -15
  94. package/dist/vendored/skills/shared/exit-codes.md +79 -0
  95. package/dist/vendored/skills/shared/spec-contract.md +118 -0
  96. package/dist/vendored/skills/sketch-spec-to-card/SKILL.md +43 -20
  97. package/dist/vendored/skills/sketch-spec-to-card/references/spec-schema.md +4 -4
  98. package/dist/vendored/skills/sketch-spec-to-card/scripts/build_prompt.py +47 -9
  99. package/dist/vendored/skills/sketch-spec-to-card/scripts/cluster_states.py +14 -25
  100. package/dist/vendored/skills/sketch-spec-to-card/scripts/parse_meaxure.py +546 -91
  101. package/dist/vendored/skills/sketch-spec-to-card/scripts/png_utils.py +51 -5
  102. package/dist/vendored/skills/sketch-spec-to-card/scripts/test_text_only_artboard.py +217 -0
  103. package/dist/vendored/skills/sketch-spec-to-card/scripts/vision.py +64 -34
  104. package/package.json +1 -1
  105. package/dist/preview-editor/assets/index-Cm0USikc.js +0 -307
  106. package/dist/preview-editor/assets/index-cbTCHp8J.css +0 -1
  107. package/dist/vendored/skills/card-skill-trainer/scripts/test_missing_slice.py +0 -122
  108. package/dist/vendored/skills/dsl-card-generator/scripts/analyze_component_usage.mjs +0 -153
@@ -14,11 +14,11 @@ 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`),**缺图该行显式写 `图: (预览图缺失)`**——**禁止把所有预览图统一堆在清单末尾**(部分缺图时序号与图会错位);**直接输出清单**——编辑器助手没有弹选项工具(不存在 `request_user_input`,不要去调用或寻找),一律「列出清单 + 一句提问后停下等用户回序号」;
17
+ 2. 按 **`1..n` 编号列出(序号 1 起,每行 `序号. 画板名 — 类型 / 状态`)+ 末尾一项 `全部`**;**每个画板行下面紧跟它自己那张预览图的绝对路径**(`图: <绝对路径>`;`parse_meaxure.py --list` 与门禁 exit-4 输出已机械逐行配图,取值优先 `UI/<稿>/<画板>/artboard.png` → 导出 `preview/<slug>.png`),**缺图该行显式写 `图: (预览图缺失)`**——**禁止把所有预览图统一堆在清单末尾**(部分缺图时序号与图会错位);脚本给的路径**一字不改照抄完整绝对路径**(**禁止用 `…` / `...` 缩写前缀、禁止包进反引号或代码块**——缩写的路径渲染端取不到图,用户那几行只剩一串文字);`text_only` 纯文本板**不进可建卡池**,脚本把它们列在清单末尾的「已过滤」段(只给画板名,不给图,序号仍按全量编);**直接输出清单**——编辑器助手没有弹选项工具(不存在 `request_user_input`,不要去调用或寻找),一律「列出清单 + 一句提问后停下等用户回序号」;
18
18
  3. 一句纯文本提问(「请回复要处理的画板序号,或回复 `全部`」)后**停止输出、结束本轮等回复**;
19
- 4. 拿到回复才继续:序号 k → `--indices <k-1>`;`全部` → `--scope-all`(UI 产物全量跑;产卡时视为显式批量授权)。
19
+ 4. 拿到回复才继续:**照抄清单行尾的 `--pick`**(板号 1 起、卡号 1 起:用户说「8.1」就 `--pick 8.1`;多张逗号分隔 `--pick 8.1,9.2`;同板连卡 `--pick 8.1~8.2`;连板 `--pick 7~9`)。等价的 0 起老写法是 `--indices <k-1>`,**不要去换算它**——实测换算错一位,建了错的画板后重跑一整次(34s + 一步 LLM)。`全部` → `--scope-all`(UI 产物全量跑;产卡时视为显式批量授权)。
20
20
 
21
- **「生成产物 / 前处理 / 跑管线」这类请求同样适用本门禁**——「UI 产物允许全量生成」的含义是**用户可以回答 `全部`**,**不是** Agent 可以默认全量。脚本层有机械卡点:`parse_meaxure.py` 不带 `--only/--indices/--sample/--scope-all` 的生成运行会打印 1..n+全部 清单并 **exit 4**(无人值守训练 `card-skill-trainer` 属显式批量例外,直接带 `--scope-all`)。详见 `dsl-card-generator/references/rules.md §十三` 与本文件 §2 步骤 2。
21
+ **「生成产物 / 前处理 / 跑管线」这类请求同样适用本门禁**——「UI 产物允许全量生成」的含义是**用户可以回答 `全部`**,**不是** Agent 可以默认全量。脚本层有机械卡点:`parse_meaxure.py` 不带 `--pick/--only/--indices/--sample/--scope-all` 的生成运行会打印 1..n+全部 清单并 **exit 4**(退出码语义 `kind=needs-user`,即「**等用户选**」而非脚本报错,见 `skills/shared/exit-codes.md`;无人值守训练 `card-skill-trainer` 属显式批量例外,直接带 `--scope-all`)。详见 `dsl-card-generator/references/rules.md §十三` 与本文件 §2 步骤 2。
22
22
 
23
23
  ## -1. 视觉优先原则(最高优先级)
24
24
 
@@ -50,8 +50,10 @@ Sketch MeaXure 导出的目录,结构形如:
50
50
  ```
51
51
  UI/
52
52
  manifest.json # 全部画板清单:order / name / status(generated|skipped) / artboardType
53
- # / trainingEligible(普通卡=true,子卡/弹框=false)
54
- <画布名>/ # 如「01转账_未登录」,目录名直接用画板原始中文名;若 parse 时传 --draft <稿名>,则整体落在 UI/<draft>/ 下(按稿分片)
53
+ # / trainingEligible(普通卡=true,子卡/弹框/纯文本板=false)
54
+ # / name 已唯一化(同名板追加 -2/-3,见下方「同名画板消歧」)
55
+ # / nameOriginal 仅消歧板有(Sketch 里的原始名)
56
+ <画布名>/ # 如「01转账_未登录」,目录名 = 画板名(消歧后);若 parse 时传 --draft <稿名>,则整体落在 UI/<draft>/ 下(按稿分片)
55
57
  artboard.png # 整图截图(来自 preview/,已降采样至最长边≤640,仅作整体参考;卡片细节以 card-N.png 为准)
56
58
  card-N.png # 聚焦裁切图(每张卡片局部,@2x,RGBA;圆角/不规则外区域透明,非原始整稿)
57
59
  spec.json # 视觉要素标注:cards/elements/styleSummary/artboardType/jsapiInteractions + cards[].segments(几何视觉分段)
@@ -71,12 +73,16 @@ UI/
71
73
  > 子卡独立成卡:subcard 视作独立 cardKey,仅通过 `parentKey` 与父卡建立交互关系。
72
74
  > **按稿分片(item 19,2026-08-31)**:`parse_meaxure.py --draft <稿名>` 时画板落在 `UI/<draft>/<画板>/`、manifest 在 `UI/<draft>/manifest.json`,聚类的 `cards/`+`clusters.json` 也落在各草稿内(不再有全局 `UI/cards`)。`cluster_states.py` 给每张卡产出英文 `pageId`(稳定序号 `card-N`,因无拼音库走序号方案)替代中文 cardKey 当下游 id。
73
75
 
76
+ > **纯文本板过滤(用户 2026-09-20)**:整板只有对话/问答/提示文字、查不到任何可建卡结构的画板,判 `artboardType=text_only`(`status=skipped`、`trainingEligible=false`)——**没有生成卡片的必要**,不进选板清单的可建卡池。这类画板**不产 `card-N.png`(连以前的整屏兜底裁切也不产,否则 `spec_tree.mjs` 会拿它当预览降级)**,`prompt.md` 只写「本画板不要生成卡片」+ 判定依据,不写建卡要素。规则与回归见 §5 / §6。
77
+
78
+ > **同名画板消歧(2026-09-24)**:Sketch 允许两个画板**完全同名**(实测「地址管理」稿 25 板里两个「11…_失败_再次修改」,375×1232/129 层 vs 375×1252/130 层,是两个不同设计)。目录名直接用画板名时,后跑的会**整板覆盖**先跑的——`artboard.png` / `card-N.png` / `spec.json` / `slices/` 静默丢失,且 manifest 两条 entry 指向同一目录;下游按名定位(`generate_cards.mjs`:`uiDir/<name>/spec.json`)也会读错同一份。故 `manifest.canvases[].name` 对**第 2 个及以后**的同名板追加 `-2` / `-3` 后缀(`nameOriginal` 留 Sketch 原始名便于回溯),**目录名、选板清单、下游定位键三者一律用唯一名**——下游零改动即正确。分类判定(`classify_artboard`)与预览图兜底探测仍用原始名,后缀不参与语义。回归见 `test_text_only_artboard.py`「同名画板消歧」。
79
+
74
80
  ## 2. 工作流
75
81
  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
82
  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 起**,每行 `序号. 画板名 — 类型 / 状态`,**行下紧跟该画板自己的预览图绝对路径,缺图标缺失,禁止堆末尾**)+ 末尾追加一项 **`全部`**(清单是完整选项池,不得只列节选)。② **仅当工具清单里确实列有原生弹窗工具时才用**:选项放最具代表性的 2 个画板 + `全部`(每题上限 3 项,客户端自动追加 Other),问题文案写明「其余画板在 Other 里填清单序号或画板名」;返回空答案不反复追问,降级到 ③。**编辑器助手没有这个工具**(不存在 `request_user_input`),不要去调用或寻找,直接走 ③。③ **默认路径**→ 一句纯文本提问(如「请回复要处理的画板序号,或回复 `全部`」)后**停止输出、结束本轮等用户回序号**,不得在同一轮顺手产卡。④ 回 `全部` = 整份导出全量跑 UI 产物(**允许**);回单个序号 = 只跑该画板,并作为下游产卡的目标画板。转给用户时把每行图绝对路径**逐条内联成 Markdown 图片**看图选(与序号一一对应);系统级选择弹窗不可实现(Codex 无原生弹窗控件),③ 即等价物。
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
- - `--only` 名称子串 / `--indices` 序号 / `--sample N` 是**用户指定后的范围裁剪**;`--sample` 抽样只是取代表性素材,**不等于用户已选定要产卡的画板**。
83
+ - **选择交互(固定流程)**:① 把全量画板按 **`1..n` 编号列出**(序号 **1 起**,每行 `序号. 画板名 — 类型 / 状态`,**行下紧跟该画板自己的预览图绝对路径,缺图标缺失,禁止堆末尾**)+ 末尾追加一项 **`全部`**(清单是完整选项池,不得只列节选;`text_only` 纯文本板已过滤,单独列在末尾「已过滤」段)。② **仅当工具清单里确实列有原生弹窗工具时才用**:选项放最具代表性的 2 个画板 + `全部`(每题上限 3 项,客户端自动追加 Other),问题文案写明「其余画板在 Other 里填清单序号或画板名」;返回空答案不反复追问,降级到 ③。**编辑器助手没有这个工具**(不存在 `request_user_input`),不要去调用或寻找,直接走 ③。③ **默认路径**→ 一句纯文本提问(如「请回复要处理的画板序号,或回复 `全部`」)后**停止输出、结束本轮等用户回序号**,不得在同一轮顺手产卡。④ 回 `全部` = 整份导出全量跑 UI 产物(**允许**);回单个序号 = 只跑该画板,并作为下游产卡的目标画板。转给用户时把每行图绝对路径**逐条内联成 Markdown 图片**看图选(与序号一一对应);系统级选择弹窗不可实现(Codex 无原生弹窗控件),③ 即等价物。**清单分两段**:先列**可建卡选项池**(带序号 + 各自预览图),末尾附一段**已过滤的纯文本板**(只列画板名,不配图)——序号仍按全量 `1..n` 编,用户若回了纯文本板的序号,脚本只打 `[WARN] 纯文本画板…不会出卡`,不产卡。
84
+ - **清单来源与序号对齐**:已前处理的稿读 `UI/<稿名>/manifest.json` 的 `canvases[]`(`order` 1 起 / `name` / `artboardType` / `status`)与 `clusters.json` 的 `cards[]`(`cardKey` / `pageId` / `stateNames` / `recommendedBase`);`--list` 与门禁清单**展示统一 1 起**、行尾自带可复制的 `--pick`(1 起、免换算,照抄即可),用户回的序号 k 传 `--indices` 时才是 **-1(0 起)**。`overlay_dialog` 画板标注「不产卡」(补到父卡事件)。同卡多状态时选择单位是 **cardKey**,未指定状态用 `recommendedBase` 并告知。
85
+ - `--pick` 板号(1 起,行尾提示即它)/ `--only` 名称子串 / `--indices` 序号(0 起)/ `--sample N` 是**用户指定后的范围裁剪**;`--sample` 抽样只是取代表性素材,**不等于用户已选定要产卡的画板**。
80
86
  3. **生成文档**:`scripts/build_prompt.py`(或 `--build` 一步式)由 `spec.json` 生成 `prompt.md` + `spec.md`(含列表检测、渐变/字体/flex 样式、JSAPI 交互)。
81
87
  4. **聚类聚合**:`scripts/cluster_states.py` 按卡片维度聚类画板 → `cards/<cardKey>/` 多状态产物 + `clusters.json` + `interactions/index.yaml`(弹框/子卡父子 + 维度切换 + JSAPI 控件)。
82
88
  5. **交付**:把某卡片的 `cards/<cardKey>/`(含多状态聚焦截图 + card.json)交给 `dsl-card-generator` 即可生成该卡片 schema。
@@ -87,7 +93,7 @@ UI/
87
93
  python skills/sketch-spec-to-card/scripts/parse_meaxure.py --list
88
94
 
89
95
  # ⚠️ 多画板选择门禁:不带范围参数的生成运行会打印 1..n + 全部 清单并 exit 4(不生成任何东西);
90
- # 用户回复序号 k → 加 --indices <k-1>;回复「全部」→ 加 --scope-all(无人值守训练可直接加,或 SKETCH_SPEC_SCOPE_ALL=1)
96
+ # 用户回复「8.1」→ 照抄清单行尾的 --pick 8.1;回复「全部」→ 加 --scope-all(无人值守训练可直接加,或 SKETCH_SPEC_SCOPE_ALL=1)
91
97
  python skills/sketch-spec-to-card/scripts/parse_meaxure.py --src <导出目录> --draft <稿名> --build --scope-all
92
98
 
93
99
  # 抽样 5 个代表性画布(按业务流去重 + 状态关键字打分,保证覆盖不同主屏)
@@ -108,9 +114,6 @@ python skills/sketch-spec-to-card/scripts/parse_meaxure.py --src <导出目录>
108
114
  # 一步式:解析并直接生成 prompt.md/spec.md(省一次 build_prompt 命令,提速)
109
115
  python skills/sketch-spec-to-card/scripts/parse_meaxure.py --src <导出目录> --sample 5 --build
110
116
 
111
- # 新业务域微调意图推导:传 {关键字: 意图模板} JSON(覆盖/追加默认规则)
112
- python skills/sketch-spec-to-card/scripts/parse_meaxure.py --src <导出目录> --sample 5 --intent-map intent-map.json
113
-
114
117
  # 生成 prompt.md / spec.md(处理 manifest 中全部 generated 画板;也可由 --build 自动触发)
115
118
  python skills/sketch-spec-to-card/scripts/build_prompt.py
116
119
  python skills/sketch-spec-to-card/scripts/build_prompt.py --only "未登录"
@@ -136,17 +139,19 @@ python skills/sketch-spec-to-card/scripts/cluster_states.py --out <UI目录> --a
136
139
  - `prompt.md` 已是可直接消费的 prompt:引用 `artboard.png` 与 `slices/`,给出卡片意图与关键标注,并指示对方运行 `skills/dsl-card-generator/scripts/validate_schema.py` 自查。**§6「人力卡拓扑建议」**段落基于 `skills/dsl-card-generator/references/handcrafted/patterns.json`(52 张人力卡的统计规则库)生成,含 Root 直系骨架推荐 / slot 约定提醒 / 节点 style itemKey 必备度 / 列表场景提醒 / 文本绑定范式 / 相似人力卡参考链接 / 变量命名推荐集 / 生成后校验闭环共 8 个子段。规则库缺失时静默降级(不阻断 prompt 生成),仅占位说明「规则库未加载」。**⚠️ 跨 skill 依赖**:`build_prompt.py::_load_handcrafted()` 经相对路径 `../dsl-card-generator/references/handcrafted/` 读下游 skill 的产物;跨仓移动 `dsl-card-generator` 时需同步更新该路径(或把 handcrafted/ 提升为共享资源)。详见 `skills/dsl-card-generator/references/handcrafted/README.md`。
137
140
  - 当用户说「用某画板生成卡片」时,把 `UI/<画布名>/` 交给 dsl-card-generator(读 `artboard.png` + `prompt.md`),由其产出 `schema.json` + `data.json`。
138
141
 
139
- ## 4.5 画板分类:overlay_dialog(alertDialog)/ subcard(alertCard)/ card
140
- 设计稿里除了普通卡片主体,还常出现**弹框 / 确认框 / 消息提示**与**带标题+关闭按钮的弹出子卡**。这两类不按普通卡片还原,需识别后分流(用户 2026-08-13 补充):
142
+ ## 4.5 画板分类:overlay_dialog(alertDialog)/ subcard(alertCard)/ card / text_only(不产卡)
143
+ 设计稿里除了普通卡片主体,还常出现**弹框 / 确认框 / 消息提示**、**带标题+关闭按钮的弹出子卡**,以及**整板只有对话/问答/提示文字的纯文本页**。这三类都不按普通卡片还原(第三类干脆不产卡),需识别后分流(用户 2026-08-13 补充,`text_only` 2026-09-20 补充):
141
144
 
142
145
  - **overlay_dialog(→ 系统 `alertDialog` JSAPI)**:弹框 / 确认框 / 消息提示 / toast 类组件。**不要求严格还原外观**,实现方式是「在触发它的父卡片对应交互中调用原生 `alertDialog` JSAPI」,**不生成独立卡片**。`prompt.md` 改为输出「交互提示文档」(含 `alertDialog` 调用范式 + 从画板提取的 title/message/按钮 + 父卡归属提示 + 参考存量卡)。
143
146
  - **subcard(→ `alertCard` 父子卡片)**:带标题栏 + 关闭按钮(×)的模态子卡,是一种「子卡」实现。按普通卡片**正常生成一张独立 Dialog 卡**,并补 `alertCard` 打开调用 + `closeAlertCard` 关闭调用(参考存量 `monthlyBill→monthlyBillDialog`、`creditCardBill→billListDialog`)。`prompt.md` 在常规建卡指令后追加「§6 弹窗形态」说明。
144
147
  - **card(普通卡片主体)**:严格还原设计稿。
148
+ - **text_only(纯文本板,→ 不产卡)**:整板只有对话气泡/问答/提示文字,检测不到任何站得住的卡片区域 → `classify_artboard` 返回 `text_only`,manifest 标 `status=skipped` + `trainingEligible=false`,不裁 `card-N.png`(含兜底图)、不进产卡池。判据见 §5「纯文本画板过滤」。
145
149
 
146
150
  **识别规则(`parse_meaxure.py → classify_artboard`,保守、可解释)**:
147
151
  1. 名称含「子卡/子页面/子卡片/浮层卡/弹层卡/弹窗卡/…」→ **subcard**(名称优先,直接判父子卡)。
148
152
  2. 名称含「弹框/弹窗/对话框/确认框/提示框/消息提示/toast/二次确认/…」→ **overlay_dialog**。
149
- 3. 名称无弹层字样时,按**结构信号**兜底:画板含「近全屏半透明遮罩(scrim) + 关闭按钮(×) + 标题」→ subcard;仅含 scrim → overlay_dialog。**无 scrim 一律判 card**(避免把卡片内「可关闭的提示条」误判为弹框/子卡)。
153
+ 3. 名称无弹层字样时,按**结构信号**兜底:画板含「近全屏半透明遮罩(scrim) + 关闭按钮(×) + 标题」→ subcard;仅含 scrim → overlay_dialog。
154
+ 4. 无 scrim / 无底部抽屉时**先判纯文本板**:`cards` 为空 + 候选拒绝理由全属「本来就不是卡」+ 板上无卡片级大盘切图 → `text_only`;其余才判 card(避免把卡片内「可关闭的提示条」误判为弹框/子卡,也避免把纯文本对话页硬塞进产卡池)。
150
155
 
151
156
  #### 卡片聚焦验收门(Agent 多模态复核)
152
157
 
@@ -193,22 +198,26 @@ python skills/sketch-spec-to-card/scripts/cluster_states.py --out <UI目录> --a
193
198
  - **切图自包含 + 同名取最高倍率**。每个画板 `slices/` 仅含该画板引用的切图(按画板内 `slice` 层名匹配 `assets/` 文件名,剥 `@Nx` 与 `_icon` 后缀、取路径末段)。**同名多尺寸去重**:同一图标若存在 `@3x`/`@2x`/`@1x`/原始 多版本,只保留最高倍率(`@3x`>`@2x`>`@1x`>无后缀),每图标仅 1 个文件。候选排序必须先交叉验证目录语义段,再按位图格式、最高倍率和路径简洁度择优;`8、icon` 与 `8.icon` 这类目录分隔差异不能阻断匹配。零匹配时回退拷贝整个 `assets/` 池,保证不缺素材。**保留导出原名**(作为来源证据,不在本 skill 改名):语义化重命名与**跨画板内容去重**(同内容只存一份、按 sha256 判定)在下游落 `src/slices/` 时做,见 `dsl-card-generator/references/slices_convention.md §1.1 / §3.1`(lint `r59` / `r60` 兜底)。
194
199
  - **标注提取**。text 层含 `content/color(hex)/fontSize/fontFace/textAlign/lineHeight`;其余层取 `rect/fills/borders/opacity`。group 层递归展平,保留 `depth` 与 `parent`。
195
200
  - **双主题 / 预览区独立性**:本 skill 不动编辑器主题;切图按原样拷贝,颜色以标注 hex 为准。
196
- - **文本气泡误识别剔除**(`parse_meaxure.py::_is_pure_text_card`):`detect_cards` 会把 AI 应答文本气泡(纯文本气泡 / 大段应答多行文本)误识别为卡片。判定规则(纯结构/几何,不读名称/文案):①存在「大层」(rect 宽≥80%cw 且高≥50%ch 的元素 = 卡面/蒙版/top-bg 等背景装饰);②剥去所有大层与组(包装)后,剩余「真实内容」= slice + 小 symbol(图标) + 带填充小 shape(图标/按钮) + text;③若无 slice/图标/带填充小形状 且 text 段数≤2 → 文本气泡,剔除。剔除后删除对应 `card-N.png` 并对保留卡片重新 1..N 编号。无大层 → 保守保留(透明卡不杀)。真卡(空态/列表/结果卡,含图标或多段 label-value 文本)不受影响。
201
+ - **文本气泡误识别剔除**(2026-09-20 起并入「拒绝理由体系」,旧函数已删):`detect_cards` 偶把 AI 应答文本气泡识别为卡片。现由 `vision.detect_cards` 对每个候选区域逐条给出**拒绝理由**——文本气泡类 = `conversation_bubble_region`(单气泡)/ `stacked_conversation_bubbles_region`(堆叠气泡)/ `prose_paragraph_region`(段落)/ `empty_region`(空区域),见 `vision.py` 顺序否决链;画板级再由 `parse_meaxure.is_text_only_artboard` 按「所有拒绝理由都落在纯文本类集合」收口为 `text_only`(下一条)。剔除后删除对应 `card-N.png` 并对保留卡片重新 1..N 编号。**注意:旧实现 `_is_pure_text_card` / `_filter_pure_text_cards` 已于 2026-09-20 重构中删除**(被上述理由体系取代),任何仍引用它们的文档段落均为过期信息。
202
+ - **纯文本画板过滤**(`parse_meaxure.py::is_text_only_artboard`,用户 2026-09-20):整板就是对话/问答/提示文字的画板**没有生成卡片的必要**,不该继续占产卡池与训练集。判据(`classify_artboard` 在视觉弹层判定之后、名称兜底之前调用;`focus_artboard` 复用同一入口 `detect_artboard_cards`,两处判定不漂移):①`cards` 非空 → 不是纯文本;②`focusSkipReason` 属 overlay/底部抽屉 → 归弹层类,不走这条;③板上有**卡片级大盘切图**(中段、宽≥55%W、高≥12%H 的 slice,即卡片被整体导成一张图,`vision.has_card_like_visual`)→ 保留;④其余凡「至少有一个候选被拒」且**所有拒绝理由都落在纯文本类集合**(`text_only_or_low_structure_region` / `prose_paragraph_region` / `conversation_bubble_region` / `stacked_conversation_bubbles_region` / `conversation_inputDock_region` / `conversation_keyboard_region` / `empty_region`)→ `text_only`。**白名单式判定**是关键:理由只要沾上 `single_button_or_bar_region`(读起来像卡、规则没认的窄卡)或 `repeated_control_bar_region` 就一律保留——汇率牌价这类宽扁卡靠这条保住;0 候选的普通文字页同样保守保留。落地:`status=skipped` + `trainingEligible=false` + 不产 `card-N.png`(连兜底图也不产)+ `prompt.md` 只写「不要建卡」的判定依据;`--list` / 门禁清单把它归到「已过滤」段。
197
203
  - 详细 spec.json 字段见 `references/spec-schema.md`。
198
204
 
199
205
  ## 6. 已沉淀问题 / 优化(维护记录)
206
+ - **同名画板整板覆盖(2026-09-24)**:重跑「地址管理」稿时发现 25 个画板只落 24 个目录 —— 源导出里 `11信用卡地址管理_…_失败_再次修改` 出现**两次**(idx=10 为 375×1232/129 层,idx=11 为 375×1252/130 层,`imagePath` 分别是 `…-2-11…png` 与 `…-1-11…png`,是两个不同设计而非重复导出)。`cdir = base / name` 直接用画板名 → 后跑的整板覆盖先跑的(`artboard.png` / `card-N.png` / `spec.json` / `slices/` 静默丢失),manifest 两条 entry 还都指向同一目录;下游 `generate_cards.mjs`(`uiDir/<name>/spec.json`)与选板清单同样按 name 走,两行同名用户也无法指定。修复:新增 `unique_canvas_names`,第 2 个及以后追加 `-2` / `-3` 并**同步写入 `name` 与 `slug`**(`nameOriginal` 留原始名)——下游只认 name,故零改动即正确;清单展示同用唯一名,保证「用户看到的行 = 落盘目录 = 下游定位键」三者一致。`classify_artboard` 与预览图兜底 glob 仍传原始名(后缀不参与语义判定)。**已知残留**:同名板且 `imagePath` 也缺失时,兜底 glob 仍可能取到同一张预览图(双缺失场景,已在代码注释标注)。回归:自检新增「同名画板消歧」断言(`["A_首页","A_首页","B_备用","A_首页"]` → `["A_首页","A_首页-2","B_备用","A_首页-3"]`)+ 真实稿 25 板 → 25 目录、两板元素数 129/130 各自独立。
207
+ - **`subcard` 产卡(2026-09-24)**:`bottom_sheet` 此前只写 `focusSkipReason=bottom_sheet_geometry` 而**不产 `cards`**,于是 `cluster_states` 承认它是独立逻辑卡(`pageId=card-N`、`isDialog=true`)而 spec 层给不出任何卡级数据 —— 下游只能读整图自定边界。现由 `detect_artboard_cards` 的 sheet 分支把面板几何(`vision._bottom_sheet_rect` 早已算出 rect + 顶圆角)落成 1 张卡:`cards[0]` 含 `rect` / `mask.radius` / 卡内 `elements` / `segments` / `detectedList`,并实产 `card-1.png`。**`focusSkipReason` 保持不变** —— `classify_artboard`(`:1667`)靠它判 `artboardType=subcard`,改它会连带打翻 `is_text_only_artboard`(`:1643`) / `board_verdict` / trainer 的 `is_popup_case`(`build_dataset.py:188`);该字段语义是「跳过自动候选检测」,不是「0 卡」。元素归属抽 `vision.elements_in_rect(elements, rect, min_cover)` 作**唯一口径**:普通卡走默认(中心在内 or 覆盖 >30%,与 `detect_cards` 原实现等价),抽屉卡走 `min_cover=0.7`(面板上沿会切过相邻气泡与其文字层 —— 其大头刚过沿就会被默认口径收进来;真实稿 04/05/06 实测阈值 0.65~0.90 元素集合完全一致,不敏感)。回归:自检 6 例(含 `subcard` → `cards=1`)+ 抽屉元素归属 + `focus_artboard` 端到端(card-1.png / mask.radius 实产)+ 真实稿三张实证元素归属 0 异常;trainer 不受影响(`POPUP_TYPES` 已整体跳过 subcard)。**注意全量统计会变**:原先 `subcard=82` 全部是 0 卡,重跑后每张 +1 卡。
208
+ - **纯文本画板过滤(2026-09-20)**:用户反馈「纯文本对话的设计稿,里面都是文本,没有任何生成卡片的必要」。新增 `vision.has_card_like_visual`(卡片级大盘切图保险)+ `parse_meaxure.is_text_only_artboard`(纯文本拒绝理由白名单收口)+ `detect_artboard_cards` 统一检出入口(`focus_artboard` 与清单 `board_verdict` 同源,判定不漂移),并停掉纯文本板的兜底 `card-1.png`、给这类板单独产「不要建卡」prompt。**全量回归(24 稿 675 画板)**:`card=530 / subcard=82 / overlay_dialog=43 / text_only=20`;被过滤的 20 张全是各稿「无权限 / 知识问答 / 无返现记录 / 额度不够 / 交易超时」这类纯文本对话页;人工复核房贷 14 张、汇率 02、理财 06/25、我的账户 01 均为真卡或「对话 + 真卡」,零误伤。单测 `scripts/test_text_only_artboard.py`(6 例:纯文本对话页→text_only、对话+内嵌矢量卡→card、对话+卡片切图→card、底部抽屉→subcard、遮罩弹框→overlay_dialog、0 候选普通文字页→card)。
200
209
  - **`_is_repeated_control_bar` 误杀业务卡(2026-09-06)**:01基金_基金推荐 主卡(标题 + 3 编号选项 + 底部 3 列收益子卡)被控件条启发式误判——底部 3 个同构子卡恰好满足「3+ 横向等距重复块」全部数值条件(宽比 0.28/高比 0.32/等间距),导致主卡 0 检出、下游被迫手工补框。根因:规则只识别「横排重复」的几何形态,未区分「整卡=控件条」与「业务卡底部恰好有同构子卡行」。修复:加入**支配性守卫**——簇的纵向并集须覆盖卡高 ≥50% 才判控件条(真评分/开关组整卡只有一条横排);收益卡行只占卡高 32% → 不再误杀。回归:24 画板零崩溃,存量 UI 无其他画板曾被该规则拒绝(零回归面),主卡恢复 1 卡检出且自动裁切与人工标注框逐字节一致。
201
210
 
202
211
  > 详细回归与优化记录见 `references/regression-notes.md`。本 skill 经过「收支分析 → 信用卡查询与还款 / 转账」两稿回归,已从「单一稿写死」重构为「通用可配置」:
203
212
 
204
- - **意图推导不再写死业务域**:原 `derive_intent` 硬编码「收支分析」各分支,对其它稿会产出张冠李戴的意图(如转账未登录 → "引导登录后查看收支分析")。现改为「通用状态规则 + 可选 `--intent-map` 覆盖」,`spec.json.intent` 对任何稿都给出合理中性意图。
213
+ - **意图推导(机制已整体移除,勿复用)**:`derive_intent` / `spec.json.intent` / `--intent-map` 均已删除(曾硬编码「收支分析」分支,跨稿张冠李戴);意图现由下游 AI 读图 + 语义裁决主判(`dsl-card-generator/references/semantic_adjudication.md`)。
205
214
  - **关键标注自动排除系统 chrome**:原 `prompt.md` 把状态栏 `9:41`、对话标题、AI 输入框、`欢迎向米宝提问` 等系统 chrome 文案一并 dump 进「关键文案」,与 §0 边界自相矛盾,既增噪又拖慢生成。现由 `vision.py` 几何阈值在聚焦裁切时排除顶/底系统区与全屏背景(设计稿文本多为示意,名称/文本不参与保留/剔除判定),`build_prompt.py` 仅聚焦卡片主体区域,切图也区分主体/系统(见 §4 与 `references/boundary.json`)。
206
215
  - **抽样更具代表性**:原 `PRIORITY_KEYWORDS` 全是收支分析词,对新稿退化为取前 N。现关键字泛化为通用业务状态,并新增「业务流去重」——优先每个主屏(画板名首个 `_` 前段)各取一个代表,保证覆盖不同流程。
207
216
  - **一步式提速**:新增 `--build`,解析后直接生成 `prompt.md/spec.md`,省去单独跑 `build_prompt.py` 的一次命令/上下文切换。
208
217
  - **配置外置(去金融写死)**:参考存量卡(`alertDialog`/`alertCard`)外置为 `references/card_refs.json`,`cluster_states.py` 默认读取,`--card-refs`/`--auto-refs` 覆盖,`--auto-refs` 可从 `src/cards` 动态探测存量卡用法。
209
218
  - **维度判定去业务词(2026-08-14 强化)**:维度检测改为 100% 纯结构(指纹 Jaccard 相似度 + qualifier 语法角色),彻底删除 `KNOWN_ENUMS`/`PAIR_PATTERNS`/`FORM_PATTERNS`/`STATUS_PATTERNS`/`SCENARIO_VERBS`/`EMPTY_ERROR_MARKS`/`TRANSITION_HINT` 等金融业务词集与 `enum_map.json`,移除 `--enum-map` 参数。过渡态判定改纯结构(qualifier 命中其他 cardKey)。`classify_artboard` 改视觉信号优先(scrim/bottom-sheet)、名称仅兜底,并从 `SUBCARD_NAME_PATTERNS` 删除「选择账号/选择银行卡」业务实体名。换业务域零调整即可生效。
210
219
  - **已知限制**:AI 应答卡常被建模在 `用户问-单行备份` 组内,纯用户问话泡(非备份)才判为 chrome;若某稿把卡片主体放在非 `-备份` 的 `用户问` 组,主体文案可能被误标——此时以 `artboard.png` 截图为准(generator 多模态读图不会丢)。已在 §0 与 `regression-notes.md` 标注。
211
- - **文本气泡误识别剔除(2026-08-14)**:信用卡稿回归发现 `detect_cards` 把 AI 应答文本气泡(含纯文本气泡「请选择要查询...」与大段应答气泡「好的,为您查询信用卡(3568)账单...」)误识别为卡片。新增 `_is_pure_text_card`(见 §5):剥去大背景层与组后若仅剩 ≤2 段文本且无图标/切图 → 剔除并重编号。v1 用「无填充色」失败(气泡白底 `#FFFFFF` 与真卡卡面同色),v2 改为「剥去最大填充背景后剩仅文本」只覆盖纯文本气泡,v3 终版改为「剥去所有大层(含 top-bg/蒙版装饰) 后剩仅少量文本」同时覆盖纯文本 + 大段应答气泡。信用卡稿 41 画板剔除 9 张应答气泡,真卡(空态/列表/结果卡)全保留,逻辑卡仍 6 张。
220
+ - **文本气泡误识别剔除(2026-08-14 提出,2026-09-20 重构收口)**:曾以 `_is_pure_text_card` 启发式剔除误识别的应答气泡(信用卡稿 41 画板剔 9 张,真卡零误伤);该函数已删,判定并入 §5「拒绝理由体系 + `is_text_only_artboard`」,现行机制以 §5 为准。
212
221
  - **业务硬编码彻查(2026-08-15)**:按 `禁止业务性硬编码` 铁律做全量 grep 审计(`转账|还款|收款|银行卡|借记卡|信用卡|转出|余额|账单|payee|outAccount|cardNo|balance`)。结论:**本 skill 合规,无业务性硬编码进入活跃逻辑**——业务词仅出现在文档/注释/可覆盖配置三类:
213
222
  - `parse_meaxure.py` 弹层识别纯几何+视觉信号(scrim/bottom-sheet),名称仅兜底且 `DIALOG/SUBCARD_NAME_PATTERNS` 仅含通用 UI 容器类型词(弹框/弹窗/子卡/浮层卡/弹层卡…),并已删除「选择账号/银行卡选择」等业务实体名。
214
223
  - `cluster_states.py` 维度/状态识别 100% 纯结构(指纹 Jaccard 相似度 + qualifier 语法角色),无任何业务词集。
@@ -261,7 +270,7 @@ python skills/sketch-spec-to-card/scripts/cluster_states.py --out <UI目录> --a
261
270
  - 逐格比色 ≥0.9 即形状+颜色双重一致,直接采信(浅色描边的前景掩码不可靠,IoU 低不应否决);
262
271
  - IoU≥0.5 时以前景色度复核:抗锯齿只把描边色向白色混合,色度近似不变(色度差 ≤25 且亮度差 ≤100 采信;真实内容差色度通常 >150)。
263
272
  - 内容真实不一致(如标注"未选中"但像素是黄色选中态)仍判 `slice-visual-mismatch` 并保留画板裁切。
264
- 5. **manifest 部分运行继承**:`--only/--indices/--sample` 局部重跑时,未选中画板的 `status`/`trainingEligible`/`artboardType`/`preview` 从旧 manifest 继承,禁止默认清零——否则数据集构建会因资格字段塌缩而丢案例。
273
+ 5. **manifest 部分运行继承**:`--pick/--only/--indices/--sample` 局部重跑时,未选中画板的 `status`/`trainingEligible`/`artboardType`/`preview` 从旧 manifest 继承,禁止默认清零——否则数据集构建会因资格字段塌缩而丢案例。
265
274
 
266
275
  ## r146 视觉判定三修复:弹层 scrim 契约 / 键盘控件去重 / 气泡对纯度(2026-09-06)
267
276
 
@@ -302,7 +311,7 @@ python skills/sketch-spec-to-card/scripts/cluster_states.py --out <UI目录> --a
302
311
 
303
312
  - **弹窗/对话框**(alertDialog):覆盖在卡片上的浮层,有半透明遮罩 + 居中弹窗
304
313
  - **底部弹出抽屉**(bottom-sheet):从底部滑出的交互层
305
- - **聊天气泡**:AI 应答的纯文本气泡(已有 `_is_pure_text_card` 识别,但多模态验证可进一步确认)
314
+ - **聊天气泡**:AI 应答的纯文本气泡(已由 `detect_cards` 的 `conversation_bubble_region` / `stacked_conversation_bubbles_region` 拒绝理由 + `is_text_only_artboard` 收口,但多模态验证可进一步确认)
306
315
  - **纯按钮区域**:单独的一个按钮不应作为独立卡片
307
316
 
308
317
  判定信号(不看业务词,纯结构/视觉):半透明遮罩层存在、底部圆角向上无延伸、无卡片外壳 shape、面积占比 <30% 画板。
@@ -327,3 +336,17 @@ python skills/sketch-spec-to-card/scripts/cluster_states.py --out <UI目录> --a
327
336
  - **矢量 logo 组按裁切处理**:设计稿里品牌 logo / 图标常是「纯矢量编组」——MeaXure 不会给它导出切图,标注里只有一个 `group` 节点。判据(`parse_meaxure.py::is_vector_logo_group` + group 分支):紧凑媒体尺寸(宽高各 12–220pt 且面积 ≤ 卡面积 20%)、无同名导出切图、矩形内**无文本叠加**、组内含 ≥1 个 shape 子件 → 记 `annotationDefect(missing-slice)` 并从原画板按标注矩形裁切成 `cropSlice`,下游 `spec_to_schema_v3.mjs` 把 `annotationDefect.cropSlice` 提权进 slices 池由 Image 承载。不裁切就整块丢失(组件分 + 像素分双掉)。
328
337
  - **自动命名图层只认精确同名匹配**:`编组 N` / `组 N` / `矩形 N` / `椭圆 N` / `直线 N` / `多边形 N` / `Group N` / `Rectangle N` / `Shape|Path|Oval|Line|Star|Polygon N` 这类 Sketch 自动命名(`AUTO_LAYER_NAME_RE`)在切图池里高度重名,`collect_slice_files` 的「去尾数字宽松匹配」会把 `编组 2` 错配到 `编组 4.png`(拿到别人的像素)。规则:图层名命中该正则时**跳过宽松匹配**,只用精确同名文件;宁可判为缺素材走裁切,也不要静默换图。
329
338
  - 两条都在 `spec.json` 留证据(`annotationDefects[].resolution=artboard-crop` / 缺素材记录),便于训练侧归因是上游解析还是下游生成。
339
+
340
+ ---
341
+ ### popup/subcard artboards are not ordinary whole-card training(r002 训练回灌)
342
+ > lesson: `LESSON-20260824-003` status: `promoted`
343
+
344
+ sketch-spec-to-card 在 manifest 中输出 trainingEligible=false;普通卡训练集只消费普通卡,弹框/子卡走专项交互训练。
345
+
346
+
347
+ ---
348
+ ### card focus needs visual, annotation and agent semantic cross(r039n 训练回灌)
349
+ > lesson: `LESSON-20260826-047` status: `promoted`
350
+
351
+ 卡片候选由截图视觉、标注几何和 Agent 语义三方交叉验证;完整业务容器才可接受,对话气泡、单按钮、弹窗、toast 和底部抽屉必须拒绝或跳过。规则只使用通用结构信号,禁止业务词硬编码。
352
+
@@ -13,7 +13,7 @@
13
13
  | `elements` | array | 全画板展平后的图层列表(含系统区/背景),见下「elements[]」 |
14
14
  | `styleSummary` | object | 聚合样式摘要(见下) |
15
15
  | `slices` | array<string> | 本画板 `slices/` 下切图相对路径(as posix),由 `collect_slice_names` + `copy_slices` 落地 |
16
- | `artboardType` | string | 画板类型:`card`(普通卡片主体,严格还原)/ `overlay_dialog`(弹框·确认框·消息提示·toast → `alertDialog` JSAPI,不建卡)/ `subcard`(带标题+关闭按钮的模态子卡 → `alertCard` 父子卡)。由 `classify_artboard` 判定 |
16
+ | `artboardType` | string | 画板类型:`card`(普通卡片主体,严格还原)/ `overlay_dialog`(弹框·确认框·消息提示·toast → `alertDialog` JSAPI,不建卡)/ `subcard`(带标题+关闭按钮的模态子卡 → `alertCard` 父子卡)/ `text_only`(纯文本对话/问答/提示板:板上查不到任何可建卡结构 → 不产卡、不进产卡池,见 SKILL.md §5)。由 `classify_artboard` 判定 |
17
17
  | `interaction` | string | 非空时说明该画板的交互归宿(如 `alertDialog 系统弹框/确认框/消息提示`、`alertCard 子卡:…`);card 为空串 |
18
18
  | `jsapiInteractions` | array<object> | JSAPI 交互控件检测(密码/验证码/数字键盘/toast/二次确认),见下 |
19
19
  | `skippedReason` | string | 仅当 `cards=[]`(视觉检测无独立卡片)时存在,说明跳过原因 |
@@ -24,7 +24,7 @@
24
24
  由 `vision.detect_cards()` 视觉检测得到的 1..N 张卡片区域。每项字段:
25
25
  | 字段 | 类型 | 说明 |
26
26
  |------|------|------|
27
- | `index` | int | 卡片序号(1 起)。剔除纯文本误识别后会重新编号(见 `_filter_pure_text_cards`) |
27
+ | `index` | int | 卡片序号(1 起)。剔除纯文本误识别后重新编号(由 `detect_cards` 拒绝理由体系 + `is_text_only_artboard` 收口) |
28
28
  | `rect` | {x,y,width,height} | 相对画板的卡片区域几何(设计坐标) |
29
29
  | `image` | string | 该卡片聚焦裁切图文件名(如 `card-1.png`,@2x);裁切失败可能缺失 |
30
30
  | `cardFill` | string\|null | 卡片容器自身纯色填充(`#RRGGBB`),供下游生成卡片 Root 背景;**不是画布背景**。背景非均匀/非固定时本字段仍来自卡片容器 fill,绝不从截图像素反推 |
@@ -32,7 +32,7 @@
32
32
  | `elements` | array | 归属该卡片的图层(同 elements[] 结构) |
33
33
  | `flexLayout` | object\|undefined | 容器 flex 布局推断(仅当子元素 ≥2):`{direction, justifyContent, alignItems, childCount}` |
34
34
 
35
- > **纯文本气泡误识别剔除**:`detect_cards` 偶把 AI 应答文本气泡识别为卡片。`_is_pure_text_card` 判定(纯结构/几何):剥去所有大层(宽≥80%cw 且高≥50%ch)与 group 包装后,若剩余仅 text 段且 ≤2 段、无 slice/带填充小shape/小symbol → 视为文本气泡剔除并删除 PNG、重新 1..N 编号。真卡(空态/列表/结果卡,含图标或多段 label-value 文本)不受影响。
35
+ > **纯文本气泡误识别剔除**(2026-09-20 起并入「拒绝理由体系」,旧函数已删):`detect_cards` 偶把 AI 应答文本气泡识别为卡片。现由 `vision.detect_cards` 逐候选给出拒绝理由(文本气泡类 = `conversation_bubble_region` / `stacked_conversation_bubbles_region` / `prose_paragraph_region` / `empty_region`),画板级再由 `parse_meaxure.is_text_only_artboard` 按「所有拒绝理由都落在纯文本类集合」收口为 `text_only`。剔除后删除对应 PNG 并重新 1..N 编号。真卡(空态/列表/结果卡,含图标或多段 label-value 文本)不受影响。**旧实现 `_is_pure_text_card` / `_filter_pure_text_cards` 已删除**,引用它们的文档即过期信息。
36
36
 
37
37
  ## elements[]
38
38
  全画板展平图层(`vision._flatten_raw` + `serialize_element`),含系统区/背景(不在 `cards[]` 内的元素)。每项字段:
@@ -99,5 +99,5 @@ MeaXure 的 `color` 对象含 `rgb/hsl/color-hex/argb-hex/rgba-hex/css-rgba/css-
99
99
 
100
100
  ## 历史变更(与本文档相关)
101
101
  - **2026-08-14 重构**:删除 `layers[]` / `chrome` 字段 / `summary.chromeCount` / `summary.cardBodyCount` / `CHROME_NAME_PATTERNS` / `CHROME_CONTAINER_PATTERNS` 名称标记机制;系统 chrome 改由 `vision.py` 几何阈值在卡片检测阶段直接剔除(不出现在 `cards[]` 内)。新增 `elements[]` 平铺结构、`cards[]` 区域结构、`styleSummary` 聚合样式。
102
- - **意图推导下线**:旧版 `derive_intent` / `DEFAULT_INTENT_RULES` / `spec.json.intent` 字段已删除;意图推导改由 `build_prompt.py` 在生成 prompt 时基于画板名 + 可选 `--intent-map` 在 prompt 文本层产出,不进 spec.json 结构。
102
+ - **意图推导下线**:旧版 `derive_intent` / `DEFAULT_INTENT_RULES` / `spec.json.intent` 字段已删除;现由 `build_prompt._detect_spec_intents` 从 spec 结构信号识别布局意图(`single_column_flow` / `list_with_sections` / `amount_header` / `form_rows` 等)并写入 prompt 文本层,不进 spec.json 结构。**无 `--intent-map` 覆盖入口** —— 该参数从未在 `build_prompt.py` / `parse_meaxure.py` 的 argparse 中实现过(argparse 实为 `--out / --only / --all`)。
103
103
  - **2026-08-15**:补齐文档与代码同步(前版本描述了已不存在的 chrome 标记字段)。
@@ -420,14 +420,6 @@ def build_handcrafted_hints(spec):
420
420
 
421
421
 
422
422
  # ---------- 列表检测(视觉:卡片内重复行结构) ----------
423
- def _rect_center_in(a, b):
424
- """a 的中心点是否落在 b 矩形内。"""
425
- ax, ay, aw, ah = a
426
- bx, by, bw, bh = b
427
- cx, cy = ax + aw / 2.0, ay + ah / 2.0
428
- return bx <= cx <= bx + bw and by <= cy <= by + bh
429
-
430
-
431
423
  def _axis_overlap_ratio(a, b, pos_i, main_i):
432
424
  """a 与 b 在主轴投影上的重叠占较短主尺寸的比例(0..1)。"""
433
425
  a0, a1 = a[pos_i], a[pos_i] + a[main_i]
@@ -574,6 +566,10 @@ def detect_list_in_card(card, min_items=2):
574
566
 
575
567
  def build_prompt_md(spec, cdir_name):
576
568
  name = spec["canvas"]
569
+ # 纯文本画板(对话 / 问答 / 提示):没有卡可建。产物只留判定依据,并在开头写死「不要建卡」,
570
+ # 免得下游读到 prompt.md 就顺手生成一张空壳卡(真实案例见 SKILL.md §5 纯文本画板过滤)。
571
+ if spec.get("artboardType") == "text_only":
572
+ return build_text_only_md(spec, cdir_name)
577
573
  w, h = spec["size"]["width"], spec["size"]["height"]
578
574
  st = spec.get("styleSummary", {})
579
575
  lines = []
@@ -722,11 +718,50 @@ def build_prompt_md(spec, cdir_name):
722
718
  return "\n".join(lines)
723
719
 
724
720
 
721
+ def build_text_only_md(spec, cdir_name):
722
+ """纯文本画板(artboardType=text_only)的 prompt.md。
723
+
724
+ 这种画板只有对话/问答/提示文字,板上没有任何卡片级结构。产物唯一目的是留判定依据 +
725
+ 明确写死「不要建卡」,所以不列聚焦图、不列切图、不给布局建议。
726
+ """
727
+ name = spec["canvas"]
728
+ w, h = spec["size"]["width"], spec["size"]["height"]
729
+ st = spec.get("styleSummary", {})
730
+ lines = []
731
+ lines.append(f"# 不建卡画板(纯文本):{name}\n")
732
+ lines.append("> **本画板不要生成卡片。** 视觉检测未在板上找到任何卡片级结构,只有对话/问答/提示类文字")
733
+ lines.append("> (`artboardType=text_only`):该画板已被过滤出选板清单的可建卡池,`trainingEligible=false`、`status=skipped`。")
734
+ lines.append("> 确需为它产卡时:换结构完整的画板,或先在 Sketch 标注里补出卡片区域再重跑 `parse_meaxure.py`。\n")
735
+ lines.append("## 判定依据\n")
736
+ lines.append(f"- 画板:{w} × {h}")
737
+ lines.append(f"- 类型:{spec.get('artboardType')}({spec.get('interaction', '')})")
738
+ lines.append(f"- 卡片数:0 / 视觉元素 {len(spec.get('elements', []))}")
739
+ if spec.get("skippedReason"):
740
+ lines.append(f"- 跳过原因:{spec['skippedReason']}")
741
+ rejected = spec.get("rejectedCards") or []
742
+ if rejected:
743
+ counter = Counter(r.get("reason") for r in rejected)
744
+ lines.append(f"- 候选区域被否 {len(rejected)} 个:"
745
+ + "、".join(f"`{r}`×{n}" for r, n in counter.most_common()))
746
+ lines.append("\n## 参考图(人工确认用,非建卡输入)\n")
747
+ lines.append(f"- `{cdir_name}/artboard.png` 整图")
748
+ lines.append("\n## 风格摘要(仅当人工判断需要)\n")
749
+ colors = st.get("colors", [])[:6]
750
+ if colors:
751
+ lines.append("- 主色:" + " ".join(f"`{c}`" for c in colors))
752
+ for f in st.get("fonts", [])[:4]:
753
+ lines.append(f"- 字体:{f.get('size')}px/{f.get('fontFace')} w{f.get('fontWeight')} align={f.get('textAlign')}")
754
+ lines.append("")
755
+ return "\n".join(lines)
756
+
757
+
725
758
  def build_spec_md(spec):
726
759
  name = spec["canvas"]
727
760
  lines = [f"# 标注摘要:{name}\n"]
728
761
  lines.append(f"- 尺寸:{spec['size']['width']} × {spec['size']['height']}")
729
762
  lines.append(f"- 类型:{spec.get('artboardType')} {spec.get('interaction','')}")
763
+ if spec.get("skippedReason"):
764
+ lines.append(f"- 跳过:{spec['skippedReason']}")
730
765
  lines.append(f"- 卡片数:{len(spec.get('cards', []))} / 视觉元素 {len(spec.get('elements', []))}\n")
731
766
  st = spec.get("styleSummary", {})
732
767
  lines.append("## 主色")
@@ -769,7 +804,10 @@ def run(out, only=None, all_flag=False):
769
804
  if all_flag:
770
805
  targets = [c["name"] for c in canvases]
771
806
  else:
772
- targets = [c["name"] for c in canvases if c.get("status") == "generated"]
807
+ # text_only(纯文本板)虽然 status=skipped,也要重写 prompt.md:它得写死「不要建卡」,
808
+ # 否则残留一份旧版「建卡要素」prompt,人工翻目录时会被误导。
809
+ targets = [c["name"] for c in canvases
810
+ if c.get("status") == "generated" or c.get("artboardType") == "text_only"]
773
811
  if only:
774
812
  kws = [k.strip() for k in only.split(",") if k.strip()]
775
813
  targets = [n for n in targets if any(k in n for k in kws)]
@@ -60,10 +60,22 @@ _AUTH_HINT = re.compile(r"(登录|授权|登入)", re.I)
60
60
 
61
61
 
62
62
  def _resolve_src_cards(explicit):
63
+ """定位 --auto-refs 的扫描目录(宿主工程根/src/cards)。
64
+
65
+ 锚点契约与下游 `dsl-card-generator/scripts/lib/paths.py` 一致:
66
+ 显式 --src-cards > 环境变量 DSL_CARD_PROJECT_ROOT > 按脚本位置回溯。
67
+
68
+ 回溯必须退**三级**:本文件位于 `<repo>/skills/sketch-spec-to-card/scripts/`。
69
+ 历史 bug:只退两级 → 得到 `<repo>/skills`,拼出 `skills/src/cards` 恒不存在
70
+ → 返回 None → `load_card_refs` 静默回退写死的金融卡参考
71
+ (`card_refs.BUILTIN_CARD_REFS`)—— 整条「跨业务域自动探测参考卡」从未生效。
72
+ """
63
73
  if explicit:
64
74
  return explicit
65
- repo_root = os.path.dirname(os.path.dirname(SCRIPT_DIR))
66
- cand = os.path.join(repo_root, "src", "cards")
75
+ root = os.environ.get("DSL_CARD_PROJECT_ROOT")
76
+ if not root:
77
+ root = os.path.dirname(os.path.dirname(os.path.dirname(SCRIPT_DIR)))
78
+ cand = os.path.join(root, "src", "cards")
67
79
  return cand if os.path.isdir(cand) else None
68
80
 
69
81
 
@@ -162,13 +174,6 @@ def diff_fingerprint(base, other):
162
174
  # ----------------------------------------------------------------------------
163
175
  # 4. 维度提取(tab / conditional / form / sequential)—— 纯结构,无业务词
164
176
  # ----------------------------------------------------------------------------
165
- def _fp_sim(a, b):
166
- """两状态指纹平均相似度(文本 Jaccard + 块 Jaccard)/2。"""
167
- jt = jaccard(set(a["fp"]["texts"]), set(b["fp"]["texts"]))
168
- jb = jaccard(set(a["fp"]["blocks"]), set(b["fp"]["blocks"]))
169
- return (jt + jb) / 2
170
-
171
-
172
177
  def is_empty_or_error(qs, fp=None):
173
178
  """空/错误态判定:qualifier 含否定语法前缀 或 指纹内容极稀疏。"""
174
179
  joined = "_".join(qs)
@@ -345,22 +350,6 @@ def detect_forms(cluster_states):
345
350
  return out
346
351
 
347
352
 
348
- # 兼容旧调用名(build_cluster 仍引用)
349
- def detect_enums(cluster_states):
350
- return [d for d in detect_positional_tabs(cluster_states) if d["type"] == "tab"]
351
-
352
-
353
- def detect_pairs(cluster_states):
354
- return [d for d in detect_positional_tabs(cluster_states) if d["type"] == "conditional"]
355
-
356
-
357
- def detect_sequential(cluster_states):
358
- seqs = [d for d in detect_positional_tabs(cluster_states) if d["type"] == "sequential"]
359
- # 保持旧输出结构(phases + 单变量)
360
- return [{"type": "sequential", "phases": s["values"], "boundVar": s["boundVar"],
361
- "note": s.get("note", "")} for s in seqs]
362
-
363
-
364
353
  # ----------------------------------------------------------------------------
365
354
  # 5. 主流程:聚类 + 产物聚合
366
355
  # ----------------------------------------------------------------------------