cardbuddy 0.15.0 → 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.
- package/dist/preview-editor/assets/index-BkbBxDTT.css +1 -0
- package/dist/preview-editor/assets/{index-CIIfMlwI.js → index-Bro07DTa.js} +91 -88
- package/dist/preview-editor/index.html +2 -2
- package/dist/vendored/card-editor/runtime/adapter.js +2 -2
- package/dist/vendored/mcp/client.mjs +1 -1
- package/dist/vendored/server/core/ai-chat.js +251 -80
- package/dist/vendored/server/core/cardValidate.js +16 -5
- package/dist/vendored/skills/card-skill-trainer/SKILL.md +61 -6
- package/dist/vendored/skills/card-skill-trainer/scripts/diagnose_case.mjs +14 -82
- package/dist/vendored/skills/card-skill-trainer/scripts/lib/diagnosis.mjs +207 -0
- package/dist/vendored/skills/card-skill-trainer/scripts/lib/layout-correction-sim.mjs +62 -0
- package/dist/vendored/skills/card-skill-trainer/scripts/lib/paradigm-router.mjs +76 -0
- package/dist/vendored/skills/card-skill-trainer/scripts/lib/pixel-worker-runner.mjs +31 -0
- package/dist/vendored/skills/card-skill-trainer/scripts/lib/pixel-worker.mjs +138 -0
- package/dist/vendored/skills/card-skill-trainer/scripts/lib/scoring.mjs +24 -20
- package/dist/vendored/skills/card-skill-trainer/scripts/lib/telemetry.mjs +46 -0
- package/dist/vendored/skills/card-skill-trainer/scripts/plan_next_round.mjs +4 -1
- package/dist/vendored/skills/card-skill-trainer/scripts/run_round.mjs +279 -90
- package/dist/vendored/skills/card-skill-trainer/scripts/score_round.mjs +74 -2
- package/dist/vendored/skills/card-skill-trainer/scripts/sink_rules.mjs +127 -0
- package/dist/vendored/skills/dsl-card-generator/SKILL.md +93 -27
- package/dist/vendored/skills/dsl-card-generator/references/dsl_structure.md +1 -15
- package/dist/vendored/skills/dsl-card-generator/references/expression_engine.md +11 -20
- package/dist/vendored/skills/dsl-card-generator/references/generation_pitfalls.md +169 -44
- package/dist/vendored/skills/dsl-card-generator/references/ir-format.md +87 -3
- package/dist/vendored/skills/dsl-card-generator/references/iteration_protocol.md +35 -6
- package/dist/vendored/skills/dsl-card-generator/references/quickref.md +28 -27
- package/dist/vendored/skills/dsl-card-generator/references/recipes.md +30 -8
- package/dist/vendored/skills/dsl-card-generator/references/rules.md +3 -2
- package/dist/vendored/skills/dsl-card-generator/references/scripts_reference.md +5 -2
- package/dist/vendored/skills/dsl-card-generator/references/slices_convention.md +2 -3
- package/dist/vendored/skills/dsl-card-generator/references/spec_to_schema_patterns.md +1 -4
- package/dist/vendored/skills/dsl-card-generator/scripts/add_component.mjs +5 -14
- package/dist/vendored/skills/dsl-card-generator/scripts/autofix_schema.py +88 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/check_fidelity.mjs +143 -42
- package/dist/vendored/skills/dsl-card-generator/scripts/check_topology.py +0 -35
- package/dist/vendored/skills/dsl-card-generator/scripts/expand_ir.mjs +99 -53
- package/dist/vendored/skills/dsl-card-generator/scripts/gate.mjs +5 -2
- package/dist/vendored/skills/dsl-card-generator/scripts/iterate.mjs +388 -29
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/component_contract.mjs +315 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/layout-tokens.mjs +1 -1
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/paths.mjs +23 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/paths.py +0 -37
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/python.mjs +99 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/adaptive-width.mjs +105 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/bindings.mjs +260 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/expr.mjs +52 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/image.mjs +96 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/registry.mjs +166 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/structure.mjs +127 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/walk.mjs +61 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/scope_alias.mjs +125 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/v3_assemble.mjs +325 -41
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/v3_extract.mjs +112 -6
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/v3_shared.mjs +0 -6
- package/dist/vendored/skills/dsl-card-generator/scripts/lint_card.mjs +119 -78
- package/dist/vendored/skills/dsl-card-generator/scripts/preflight.mjs +48 -50
- package/dist/vendored/skills/dsl-card-generator/scripts/readiness.mjs +634 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/register_card_template.mjs +5 -8
- package/dist/vendored/skills/dsl-card-generator/scripts/route_input.mjs +5 -10
- package/dist/vendored/skills/dsl-card-generator/scripts/spec_audit.mjs +122 -8
- package/dist/vendored/skills/dsl-card-generator/scripts/v3_regression.mjs +3 -1
- package/dist/vendored/skills/dsl-card-generator/scripts/validate_schema.py +23 -15
- package/dist/vendored/skills/shared/exit-codes.md +79 -0
- package/dist/vendored/skills/shared/spec-contract.md +118 -0
- package/dist/vendored/skills/sketch-spec-to-card/SKILL.md +26 -9
- package/dist/vendored/skills/sketch-spec-to-card/references/spec-schema.md +3 -3
- package/dist/vendored/skills/sketch-spec-to-card/scripts/build_prompt.py +0 -8
- package/dist/vendored/skills/sketch-spec-to-card/scripts/cluster_states.py +14 -25
- package/dist/vendored/skills/sketch-spec-to-card/scripts/parse_meaxure.py +91 -47
- package/dist/vendored/skills/sketch-spec-to-card/scripts/png_utils.py +0 -5
- package/dist/vendored/skills/sketch-spec-to-card/scripts/test_text_only_artboard.py +75 -15
- package/dist/vendored/skills/sketch-spec-to-card/scripts/vision.py +40 -34
- package/package.json +1 -1
- package/dist/preview-editor/assets/index-DQn0zQBP.css +0 -1
- package/dist/vendored/skills/card-skill-trainer/scripts/test_missing_slice.py +0 -122
- package/dist/vendored/skills/dsl-card-generator/scripts/analyze_component_usage.mjs +0 -153
|
@@ -18,7 +18,7 @@ description: 读取 Sketch MeaXure 标注导出(index.html + preview/ + assets
|
|
|
18
18
|
3. 一句纯文本提问(「请回复要处理的画板序号,或回复 `全部`」)后**停止输出、结束本轮等回复**;
|
|
19
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` 不带 `--pick/--only/--indices/--sample/--scope-all` 的生成运行会打印 1..n+全部 清单并 **exit 4
|
|
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
|
|
|
@@ -51,7 +51,9 @@ Sketch MeaXure 导出的目录,结构形如:
|
|
|
51
51
|
UI/
|
|
52
52
|
manifest.json # 全部画板清单:order / name / status(generated|skipped) / artboardType
|
|
53
53
|
# / trainingEligible(普通卡=true,子卡/弹框/纯文本板=false)
|
|
54
|
-
|
|
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(几何视觉分段)
|
|
@@ -73,6 +75,8 @@ UI/
|
|
|
73
75
|
|
|
74
76
|
> **纯文本板过滤(用户 2026-09-20)**:整板只有对话/问答/提示文字、查不到任何可建卡结构的画板,判 `artboardType=text_only`(`status=skipped`、`trainingEligible=false`)——**没有生成卡片的必要**,不进选板清单的可建卡池。这类画板**不产 `card-N.png`(连以前的整屏兜底裁切也不产,否则 `spec_tree.mjs` 会拿它当预览降级)**,`prompt.md` 只写「本画板不要生成卡片」+ 判定依据,不写建卡要素。规则与回归见 §5 / §6。
|
|
75
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
|
+
|
|
76
80
|
## 2. 工作流
|
|
77
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)。
|
|
78
82
|
2. **选画板(硬门禁,先于解析/生成)**:先 `parse_meaxure.py --src <导出目录> --list` **机械列出全量画板**,按 **`1..n` 编号(1 起)+ 末尾 `全部`** 交给用户,**停止输出等回复**后才跑管线。**「UI 产物允许全量生成」= 用户可以回答 `全部`(此时加 `--scope-all` 整份导出全跑,素材可复用),不是 Agent 可以默认全量**——用户消息里没指定画板时,**包括「生成产物 / 前处理」在内的任何请求都必须先过本门禁**;**禁止**自行挑画板、**禁止**替用户选「全部」、**禁止**批量产卡、**禁止**「先建一张看看」。下游卡片生成仍以用户指定的单个画板 / 单个 UI 产物为单位(铁律见 `dsl-card-generator/references/rules.md §十三`,机械卡点见门禁 0)。
|
|
@@ -110,9 +114,6 @@ python skills/sketch-spec-to-card/scripts/parse_meaxure.py --src <导出目录>
|
|
|
110
114
|
# 一步式:解析并直接生成 prompt.md/spec.md(省一次 build_prompt 命令,提速)
|
|
111
115
|
python skills/sketch-spec-to-card/scripts/parse_meaxure.py --src <导出目录> --sample 5 --build
|
|
112
116
|
|
|
113
|
-
# 新业务域微调意图推导:传 {关键字: 意图模板} JSON(覆盖/追加默认规则)
|
|
114
|
-
python skills/sketch-spec-to-card/scripts/parse_meaxure.py --src <导出目录> --sample 5 --intent-map intent-map.json
|
|
115
|
-
|
|
116
117
|
# 生成 prompt.md / spec.md(处理 manifest 中全部 generated 画板;也可由 --build 自动触发)
|
|
117
118
|
python skills/sketch-spec-to-card/scripts/build_prompt.py
|
|
118
119
|
python skills/sketch-spec-to-card/scripts/build_prompt.py --only "未登录"
|
|
@@ -197,24 +198,26 @@ python skills/sketch-spec-to-card/scripts/cluster_states.py --out <UI目录> --a
|
|
|
197
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` 兜底)。
|
|
198
199
|
- **标注提取**。text 层含 `content/color(hex)/fontSize/fontFace/textAlign/lineHeight`;其余层取 `rect/fills/borders/opacity`。group 层递归展平,保留 `depth` 与 `parent`。
|
|
199
200
|
- **双主题 / 预览区独立性**:本 skill 不动编辑器主题;切图按原样拷贝,颜色以标注 hex 为准。
|
|
200
|
-
-
|
|
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 重构中删除**(被上述理由体系取代),任何仍引用它们的文档段落均为过期信息。
|
|
201
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` / 门禁清单把它归到「已过滤」段。
|
|
202
203
|
- 详细 spec.json 字段见 `references/spec-schema.md`。
|
|
203
204
|
|
|
204
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 卡。
|
|
205
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)。
|
|
206
209
|
- **`_is_repeated_control_bar` 误杀业务卡(2026-09-06)**:01基金_基金推荐 主卡(标题 + 3 编号选项 + 底部 3 列收益子卡)被控件条启发式误判——底部 3 个同构子卡恰好满足「3+ 横向等距重复块」全部数值条件(宽比 0.28/高比 0.32/等间距),导致主卡 0 检出、下游被迫手工补框。根因:规则只识别「横排重复」的几何形态,未区分「整卡=控件条」与「业务卡底部恰好有同构子卡行」。修复:加入**支配性守卫**——簇的纵向并集须覆盖卡高 ≥50% 才判控件条(真评分/开关组整卡只有一条横排);收益卡行只占卡高 32% → 不再误杀。回归:24 画板零崩溃,存量 UI 无其他画板曾被该规则拒绝(零回归面),主卡恢复 1 卡检出且自动裁切与人工标注框逐字节一致。
|
|
207
210
|
|
|
208
211
|
> 详细回归与优化记录见 `references/regression-notes.md`。本 skill 经过「收支分析 → 信用卡查询与还款 / 转账」两稿回归,已从「单一稿写死」重构为「通用可配置」:
|
|
209
212
|
|
|
210
|
-
-
|
|
213
|
+
- **意图推导(机制已整体移除,勿复用)**:`derive_intent` / `spec.json.intent` / `--intent-map` 均已删除(曾硬编码「收支分析」分支,跨稿张冠李戴);意图现由下游 AI 读图 + 语义裁决主判(`dsl-card-generator/references/semantic_adjudication.md`)。
|
|
211
214
|
- **关键标注自动排除系统 chrome**:原 `prompt.md` 把状态栏 `9:41`、对话标题、AI 输入框、`欢迎向米宝提问` 等系统 chrome 文案一并 dump 进「关键文案」,与 §0 边界自相矛盾,既增噪又拖慢生成。现由 `vision.py` 几何阈值在聚焦裁切时排除顶/底系统区与全屏背景(设计稿文本多为示意,名称/文本不参与保留/剔除判定),`build_prompt.py` 仅聚焦卡片主体区域,切图也区分主体/系统(见 §4 与 `references/boundary.json`)。
|
|
212
215
|
- **抽样更具代表性**:原 `PRIORITY_KEYWORDS` 全是收支分析词,对新稿退化为取前 N。现关键字泛化为通用业务状态,并新增「业务流去重」——优先每个主屏(画板名首个 `_` 前段)各取一个代表,保证覆盖不同流程。
|
|
213
216
|
- **一步式提速**:新增 `--build`,解析后直接生成 `prompt.md/spec.md`,省去单独跑 `build_prompt.py` 的一次命令/上下文切换。
|
|
214
217
|
- **配置外置(去金融写死)**:参考存量卡(`alertDialog`/`alertCard`)外置为 `references/card_refs.json`,`cluster_states.py` 默认读取,`--card-refs`/`--auto-refs` 覆盖,`--auto-refs` 可从 `src/cards` 动态探测存量卡用法。
|
|
215
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` 删除「选择账号/选择银行卡」业务实体名。换业务域零调整即可生效。
|
|
216
219
|
- **已知限制**:AI 应答卡常被建模在 `用户问-单行备份` 组内,纯用户问话泡(非备份)才判为 chrome;若某稿把卡片主体放在非 `-备份` 的 `用户问` 组,主体文案可能被误标——此时以 `artboard.png` 截图为准(generator 多模态读图不会丢)。已在 §0 与 `regression-notes.md` 标注。
|
|
217
|
-
- **文本气泡误识别剔除(2026-08-14
|
|
220
|
+
- **文本气泡误识别剔除(2026-08-14 提出,2026-09-20 重构收口)**:曾以 `_is_pure_text_card` 启发式剔除误识别的应答气泡(信用卡稿 41 画板剔 9 张,真卡零误伤);该函数已删,判定并入 §5「拒绝理由体系 + `is_text_only_artboard`」,现行机制以 §5 为准。
|
|
218
221
|
- **业务硬编码彻查(2026-08-15)**:按 `禁止业务性硬编码` 铁律做全量 grep 审计(`转账|还款|收款|银行卡|借记卡|信用卡|转出|余额|账单|payee|outAccount|cardNo|balance`)。结论:**本 skill 合规,无业务性硬编码进入活跃逻辑**——业务词仅出现在文档/注释/可覆盖配置三类:
|
|
219
222
|
- `parse_meaxure.py` 弹层识别纯几何+视觉信号(scrim/bottom-sheet),名称仅兜底且 `DIALOG/SUBCARD_NAME_PATTERNS` 仅含通用 UI 容器类型词(弹框/弹窗/子卡/浮层卡/弹层卡…),并已删除「选择账号/银行卡选择」等业务实体名。
|
|
220
223
|
- `cluster_states.py` 维度/状态识别 100% 纯结构(指纹 Jaccard 相似度 + qualifier 语法角色),无任何业务词集。
|
|
@@ -308,7 +311,7 @@ python skills/sketch-spec-to-card/scripts/cluster_states.py --out <UI目录> --a
|
|
|
308
311
|
|
|
309
312
|
- **弹窗/对话框**(alertDialog):覆盖在卡片上的浮层,有半透明遮罩 + 居中弹窗
|
|
310
313
|
- **底部弹出抽屉**(bottom-sheet):从底部滑出的交互层
|
|
311
|
-
- **聊天气泡**:AI
|
|
314
|
+
- **聊天气泡**:AI 应答的纯文本气泡(已由 `detect_cards` 的 `conversation_bubble_region` / `stacked_conversation_bubbles_region` 拒绝理由 + `is_text_only_artboard` 收口,但多模态验证可进一步确认)
|
|
312
315
|
- **纯按钮区域**:单独的一个按钮不应作为独立卡片
|
|
313
316
|
|
|
314
317
|
判定信号(不看业务词,纯结构/视觉):半透明遮罩层存在、底部圆角向上无延伸、无卡片外壳 shape、面积占比 <30% 画板。
|
|
@@ -333,3 +336,17 @@ python skills/sketch-spec-to-card/scripts/cluster_states.py --out <UI目录> --a
|
|
|
333
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 承载。不裁切就整块丢失(组件分 + 像素分双掉)。
|
|
334
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`(拿到别人的像素)。规则:图层名命中该正则时**跳过宽松匹配**,只用精确同名文件;宁可判为缺素材走裁切,也不要静默换图。
|
|
335
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
|
+
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
由 `vision.detect_cards()` 视觉检测得到的 1..N 张卡片区域。每项字段:
|
|
25
25
|
| 字段 | 类型 | 说明 |
|
|
26
26
|
|------|------|------|
|
|
27
|
-
| `index` | int | 卡片序号(1
|
|
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
|
-
>
|
|
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`
|
|
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]
|
|
@@ -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
|
-
|
|
66
|
-
|
|
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
|
# ----------------------------------------------------------------------------
|
|
@@ -85,17 +85,6 @@ def hex_of(color_obj):
|
|
|
85
85
|
return None
|
|
86
86
|
|
|
87
87
|
|
|
88
|
-
def rgba_of(color_obj):
|
|
89
|
-
"""MeaXure color -> 'rgba(r,g,b,a)'(含透明度)。"""
|
|
90
|
-
if not isinstance(color_obj, dict):
|
|
91
|
-
return None
|
|
92
|
-
css = color_obj.get("css-rgba") or color_obj.get("css-hsla")
|
|
93
|
-
if css:
|
|
94
|
-
return css
|
|
95
|
-
h = hex_of(color_obj)
|
|
96
|
-
return h
|
|
97
|
-
|
|
98
|
-
|
|
99
88
|
def extract_data(index_html: Path) -> dict:
|
|
100
89
|
html = index_html.read_text(encoding="utf-8", errors="ignore")
|
|
101
90
|
marker = "let data = "
|
|
@@ -303,7 +292,8 @@ def _segment_hint(e, card_w, card_rect=None):
|
|
|
303
292
|
|
|
304
293
|
|
|
305
294
|
def detect_segments(card_els, card_rect):
|
|
306
|
-
"""按 y 几何聚类视觉段。返回 [{y, height,
|
|
295
|
+
"""按 y 几何聚类视觉段。返回 [{y, height, hint, items}](y/height 为卡内相对坐标;
|
|
296
|
+
段间距可由相邻 y/height 推导,不再冗余写 gap_top)。
|
|
307
297
|
规则:
|
|
308
298
|
1. 排除卡外装饰(全屏背景/蒙版:元素在卡 y 轴交集 < 元素高 50%);
|
|
309
299
|
2. decor-bg(装饰背景层)元素**强制独立成段**(背景层是独立容器,其下内容必须切段);
|
|
@@ -353,26 +343,18 @@ def detect_segments(card_els, card_rect):
|
|
|
353
343
|
for i, s in enumerate(segs):
|
|
354
344
|
bottom = max((it["rect"]["y"] + (it["rect"]["height"] or 0)) for it in s["items"])
|
|
355
345
|
top = s["y"]
|
|
356
|
-
|
|
346
|
+
# gap_top 已删(2026-09-24):纯冗余推导值——首段等于 y,后续段可由相邻 y/height 推出
|
|
347
|
+
out.append({
|
|
357
348
|
"y": round(top - cy, 2),
|
|
358
349
|
"height": round(bottom - top, 2),
|
|
359
350
|
"hint": s["hint"],
|
|
360
351
|
"items": len(s["items"]),
|
|
361
|
-
}
|
|
362
|
-
if i == 0:
|
|
363
|
-
seg["gap_top"] = round(top - cy, 2)
|
|
364
|
-
else:
|
|
365
|
-
# 修正坐标混用 bug:统一用卡内相对坐标计算 gap_top
|
|
366
|
-
prev_bottom_rel = out[i - 1]["y"] + out[i - 1]["height"]
|
|
367
|
-
seg["gap_top"] = round(seg["y"] - prev_bottom_rel, 2)
|
|
368
|
-
out.append(seg)
|
|
352
|
+
})
|
|
369
353
|
# 后处理:消除段重叠(前一段底部超出后一段顶部时,截断前一段高度)
|
|
370
354
|
for i in range(1, len(out)):
|
|
371
355
|
overlap = (out[i - 1]["y"] + out[i - 1]["height"]) - out[i]["y"]
|
|
372
356
|
if overlap > 0:
|
|
373
357
|
out[i - 1]["height"] = round(out[i - 1]["height"] - overlap, 2)
|
|
374
|
-
# 重算 gap_top(截断后应 ≥ 0)
|
|
375
|
-
out[i]["gap_top"] = round(out[i]["y"] - (out[i - 1]["y"] + out[i - 1]["height"]), 2)
|
|
376
358
|
return out
|
|
377
359
|
|
|
378
360
|
|
|
@@ -526,6 +508,11 @@ def detect_artboard_cards(ab_raw):
|
|
|
526
508
|
|
|
527
509
|
这是**唯一**的卡片检出入口:focus_artboard(真跑,随后按卡片裁图)与门禁清单的
|
|
528
510
|
board_verdict(只想知道「这个画板有没有卡可建」)都走它,两处判定不会漂移。
|
|
511
|
+
|
|
512
|
+
底部抽屉(bottom_sheet ⇒ artboardType=subcard)走**几何直取**而非候选检测:
|
|
513
|
+
抽屉面板矩形由 `vision._bottom_sheet_rect` 精确给出,落成 1 张卡。所以
|
|
514
|
+
`focusSkipReason=bottom_sheet_geometry` 与 `cards` 非空**同时成立** —— 该字段的
|
|
515
|
+
语义是「跳过了自动候选检测」,不是「0 卡」。
|
|
529
516
|
"""
|
|
530
517
|
W, H = ab_raw.get("width", 0), ab_raw.get("height", 0)
|
|
531
518
|
raw_layers = ab_raw.get("layers") or []
|
|
@@ -538,6 +525,23 @@ def detect_artboard_cards(ab_raw):
|
|
|
538
525
|
focus_skip_reason = (
|
|
539
526
|
"overlay_dialog_geometry" if overlay else "bottom_sheet_geometry"
|
|
540
527
|
)
|
|
528
|
+
# 底部抽屉(alertCard 子卡)产 1 张卡:面板几何 `vision._bottom_sheet_rect`
|
|
529
|
+
# 早已算出精确 rect + 顶圆角,此前却只写 focusSkipReason 不产 cards —— 结果是
|
|
530
|
+
# cluster_states 承认它是独立逻辑卡(pageId=card-N、isDialog=true),而 spec 层
|
|
531
|
+
# 给不出任何卡级数据,下游只能读整图自己猜边界。
|
|
532
|
+
# ⚠️ focusSkipReason 必须保持 bottom_sheet_geometry:`classify_artboard`(:1667)
|
|
533
|
+
# 靠它判 artboardType=subcard,改它会连带打翻 `is_text_only_artboard`(:1643)、
|
|
534
|
+
# `board_verdict` 与 trainer 的 `is_popup_case`(build_dataset.py:188)。
|
|
535
|
+
if bottom_sheet and not overlay:
|
|
536
|
+
sx, sy, sw, sh, s_radii = bottom_sheet
|
|
537
|
+
# min_cover=0.7:抽屉上沿会切过相邻元素(气泡、气泡内文字层的大头刚过沿
|
|
538
|
+
# 就会被默认口径收进来),用覆盖率门槛滤掉;跨沿的抽屉标题(覆盖≈98%)保留。
|
|
539
|
+
sheet_els = vision.elements_in_rect(flattened, (sx, sy, sw, sh), min_cover=0.7)
|
|
540
|
+
cards.append((
|
|
541
|
+
sx, sy, sw, sh, sheet_els,
|
|
542
|
+
[((sx, sy, sw, sh), list(s_radii or []))],
|
|
543
|
+
vision.card_focus_metrics(sheet_els, (sx, sy, sw, sh), W, H),
|
|
544
|
+
))
|
|
541
545
|
else:
|
|
542
546
|
for x, y, w, h, els, shapes in vision.detect_cards(raw_layers, W, H, conversation_flow):
|
|
543
547
|
eligible, reason, focus_metrics = vision.classify_card_focus(
|
|
@@ -560,10 +564,14 @@ def detect_artboard_cards(ab_raw):
|
|
|
560
564
|
}
|
|
561
565
|
|
|
562
566
|
|
|
563
|
-
def focus_artboard(ab_raw, preview_path, cdir):
|
|
567
|
+
def focus_artboard(ab_raw, preview_path, cdir, canvas_name=None):
|
|
564
568
|
"""对一个画板:检测独立卡片区域 → 结构过滤 → 逐一裁切 → 序列化标注。
|
|
565
569
|
|
|
566
570
|
返回 spec dict(含卡片区域、聚焦截图、样式汇总)。
|
|
571
|
+
|
|
572
|
+
`canvas_name`:写进 `spec["canvas"]` 的画板标识。调用方传**唯一化后**的名字
|
|
573
|
+
(同名画板已追加 -2/-3);缺省回落 Sketch 原始名。下游 `spec_tree.mjs:140`
|
|
574
|
+
用 `spec.canvas || 目录名` 当展示名,两板同名时不传就会在清单里撞成两行一模一样的字。
|
|
567
575
|
"""
|
|
568
576
|
found = detect_artboard_cards(ab_raw)
|
|
569
577
|
W, H = found["W"], found["H"]
|
|
@@ -671,8 +679,7 @@ def focus_artboard(ab_raw, preview_path, cdir):
|
|
|
671
679
|
focus_skip_reason = "conversation_flow_geometry"
|
|
672
680
|
|
|
673
681
|
spec = {
|
|
674
|
-
"canvas": ab_raw.get("name", ""),
|
|
675
|
-
"schemaVersion": "1.0",
|
|
682
|
+
"canvas": canvas_name or ab_raw.get("name", ""),
|
|
676
683
|
"size": {"width": W, "height": H},
|
|
677
684
|
"cards": card_regions,
|
|
678
685
|
"rejectedCards": rejected_regions,
|
|
@@ -1152,17 +1159,6 @@ def _foreground_mean_color(image_path, rect=None, scale=1.0):
|
|
|
1152
1159
|
return (red / count, green / count, blue / count), count
|
|
1153
1160
|
|
|
1154
1161
|
|
|
1155
|
-
def foreground_color_distance(asset_path, preview_path, rect, scale):
|
|
1156
|
-
"""资产与设计裁切的前景均值色距离;任一侧无前景时返回 None。"""
|
|
1157
|
-
asset_mean, asset_count = _foreground_mean_color(asset_path)
|
|
1158
|
-
design_mean, design_count = _foreground_mean_color(preview_path, rect, scale)
|
|
1159
|
-
if not asset_mean or not design_mean:
|
|
1160
|
-
return None
|
|
1161
|
-
return math.sqrt(sum(
|
|
1162
|
-
(left - right) ** 2 for left, right in zip(asset_mean, design_mean)
|
|
1163
|
-
))
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
1162
|
def foreground_chroma_distance(asset_path, preview_path, rect, scale):
|
|
1167
1163
|
"""前景均值色的色度距离(r-g / b-g 两分量)。
|
|
1168
1164
|
|
|
@@ -1540,6 +1536,26 @@ def parse_pick(spec, total):
|
|
|
1540
1536
|
return idxs, cards
|
|
1541
1537
|
|
|
1542
1538
|
|
|
1539
|
+
def unique_canvas_names(names):
|
|
1540
|
+
"""同名画板消歧:Sketch 允许两个画板**完全同名**(实测「地址管理」稿 25 板里有两个
|
|
1541
|
+
「11…_失败_再次修改」,一个 375×1232/129 层、一个 375×1252/130 层,是两个不同设计)。
|
|
1542
|
+
|
|
1543
|
+
目录名直接用 name 时,后跑的那个会覆盖先跑的(整板产物——artboard.png / card-N.png /
|
|
1544
|
+
spec.json / slices/ ——静默丢失,且 manifest 里两条 entry 指向同一目录);下游也是按
|
|
1545
|
+
name 拼路径定位(`generate_cards.mjs`:`uiDir/<name>/spec.json`),两行同名时用户
|
|
1546
|
+
在选板清单里同样无法区分。
|
|
1547
|
+
|
|
1548
|
+
故第 2 个及以后的同名画板追加 `-2` / `-3` 后缀,**name 与 slug 同步唯一化**——
|
|
1549
|
+
下游零改动即正确(它们只认 name)。
|
|
1550
|
+
"""
|
|
1551
|
+
seen = {}
|
|
1552
|
+
out = []
|
|
1553
|
+
for nm in names:
|
|
1554
|
+
seen[nm] = seen.get(nm, 0) + 1
|
|
1555
|
+
out.append(nm if seen[nm] == 1 else f"{nm}-{seen[nm]}")
|
|
1556
|
+
return out
|
|
1557
|
+
|
|
1558
|
+
|
|
1543
1559
|
def select_canvases(all_names, args):
|
|
1544
1560
|
n = len(all_names)
|
|
1545
1561
|
if args.list:
|
|
@@ -2035,10 +2051,13 @@ def main():
|
|
|
2035
2051
|
print(f"[INFO] 解析到 {total} 个画布(来源:{src.name})")
|
|
2036
2052
|
|
|
2037
2053
|
names = [ab.get("name", f"artboard-{i}") for i, ab in enumerate(artboards)]
|
|
2054
|
+
# 同名画板唯一化:name 同时是目录名、manifest 标识与下游定位键,必须唯一(详见
|
|
2055
|
+
# unique_canvas_names)。清单展示也用唯一名——用户看到的行与落盘目录、下游定位键三者一致。
|
|
2056
|
+
dir_names = unique_canvas_names(names)
|
|
2038
2057
|
if args.list:
|
|
2039
2058
|
# 展示统一 1 起(与多画板门禁清单同口径);--indices 仍为 0 起,用户序号 k → k-1
|
|
2040
2059
|
# 每个画板行下面紧跟它自己那张预览图(一一对应;缺图显式标缺失),禁止把图堆在末尾
|
|
2041
|
-
print_board_pool(
|
|
2060
|
+
print_board_pool(dir_names, artboards, src, base, " 全部. 全部")
|
|
2042
2061
|
return
|
|
2043
2062
|
|
|
2044
2063
|
# ── 多画板选择门禁(rules.md §十三 / SKILL.md §2 步骤 2,2026-09-17 机械兜底)──
|
|
@@ -2052,7 +2071,7 @@ def main():
|
|
|
2052
2071
|
print("[GATE] 按铁律必须先把下面的清单(1..n + 全部)交给用户并**停止输出等回复**,"
|
|
2053
2072
|
"不得自行跑全量(「生成产物 / 前处理」请求同样适用):")
|
|
2054
2073
|
print("[GATE] 清单已按体检结果分段:纯文本画板(对话/问答/提示)无卡可建,已过滤出选项池。")
|
|
2055
|
-
print_board_pool(
|
|
2074
|
+
print_board_pool(dir_names, artboards, src, base,
|
|
2056
2075
|
" 全部. 全部(整份导出全量跑 UI 产物;产卡时视为显式批量授权"
|
|
2057
2076
|
"——纯文本画板只出参考图,不出卡)")
|
|
2058
2077
|
print("[GATE] 拿到回复后重跑:**直接照抄清单行尾的 --pick**(板号 1 起、卡号 1 起,"
|
|
@@ -2061,17 +2080,17 @@ def main():
|
|
|
2061
2080
|
"无人值守批量训练可直接 --scope-all 或 SKETCH_SPEC_SCOPE_ALL=1。")
|
|
2062
2081
|
sys.exit(4)
|
|
2063
2082
|
|
|
2064
|
-
sel = select_canvases(
|
|
2083
|
+
sel = select_canvases(dir_names, args)
|
|
2065
2084
|
if not sel:
|
|
2066
2085
|
print("[WARN] 没有匹配到任何画布。", file=sys.stderr)
|
|
2067
2086
|
sys.exit(0)
|
|
2068
2087
|
# 显式点名纯文本画板时不拦(用户可能只想拿参考图),但必须说清楚不会出卡
|
|
2069
2088
|
for i in sel:
|
|
2070
|
-
v = board_verdict(
|
|
2089
|
+
v = board_verdict(dir_names[i], artboards[i] if i < len(artboards) else None)
|
|
2071
2090
|
if v["skip"]:
|
|
2072
|
-
print(f"[WARN] {
|
|
2091
|
+
print(f"[WARN] {dir_names[i]}:纯文本画板(对话/问答/提示),无卡可建——"
|
|
2073
2092
|
f"本次只会产出整图参考与判定依据,不会出卡。", file=sys.stderr)
|
|
2074
|
-
print(f"[INFO] 本次生成 {len(sel)} 个画布:{', '.join(
|
|
2093
|
+
print(f"[INFO] 本次生成 {len(sel)} 个画布:{', '.join(dir_names[i] for i in sel)}")
|
|
2075
2094
|
if args.pick:
|
|
2076
2095
|
_, picked_cards = parse_pick(args.pick, total)
|
|
2077
2096
|
if picked_cards:
|
|
@@ -2110,7 +2129,8 @@ def main():
|
|
|
2110
2129
|
previous_entries = {}
|
|
2111
2130
|
|
|
2112
2131
|
for i, ab in enumerate(artboards):
|
|
2113
|
-
name =
|
|
2132
|
+
name = dir_names[i] # 目录名 / manifest 标识 / 下游定位键(同名已唯一化)
|
|
2133
|
+
orig_name = names[i] # Sketch 原始名:只用于分类兜底与预览图兜底探测
|
|
2114
2134
|
status = "generated" if i in sel else "pending"
|
|
2115
2135
|
entry = {
|
|
2116
2136
|
"order": i + 1,
|
|
@@ -2119,6 +2139,9 @@ def main():
|
|
|
2119
2139
|
"status": status,
|
|
2120
2140
|
"trainingEligible": False,
|
|
2121
2141
|
}
|
|
2142
|
+
if name != orig_name:
|
|
2143
|
+
# 同名画板被消歧(name 追加了 -2/-3):留下原始名便于人回溯这板在 Sketch 里叫什么
|
|
2144
|
+
entry["nameOriginal"] = orig_name
|
|
2122
2145
|
if i not in sel and name in previous_entries:
|
|
2123
2146
|
for inherited in ("status", "trainingEligible", "artboardType", "preview"):
|
|
2124
2147
|
if inherited in previous_entries[name]:
|
|
@@ -2134,7 +2157,11 @@ def main():
|
|
|
2134
2157
|
preview_rel = unquote(ab.get("imagePath", "") or "")
|
|
2135
2158
|
src_png = (src / preview_rel) if preview_rel else None
|
|
2136
2159
|
if not (src_png and src_png.is_file()):
|
|
2137
|
-
|
|
2160
|
+
# 注意:只对 orig_name 兜底——唯一化后缀(-2)在导出目录里不存在,
|
|
2161
|
+
# 拿 name 去 glob 必然空手。
|
|
2162
|
+
# 已知局限:同名画板且 imagePath 也缺失时,两板 tail 相同、glob 命中多张
|
|
2163
|
+
# 且顺序不定,可能取到同一张图。这是「双缺失」场景,属兜底兜不住,不在此展开。
|
|
2164
|
+
cand = [c for c in src.glob(f"preview/*{orig_name.split('_', 1)[-1]}*.png") if c.is_file()]
|
|
2138
2165
|
src_png = cand[0] if cand else None
|
|
2139
2166
|
if src_png:
|
|
2140
2167
|
shutil.copy2(src_png, cdir / "artboard.png")
|
|
@@ -2154,10 +2181,12 @@ def main():
|
|
|
2154
2181
|
print(f" [WARN] {name}: 找不到整图 {preview_rel}", file=sys.stderr)
|
|
2155
2182
|
|
|
2156
2183
|
# 视觉聚焦:卡片区域检测 + 裁切
|
|
2157
|
-
spec
|
|
2184
|
+
# 传唯一名:spec.canvas 是下游展示名(spec_tree.mjs:140)与 prompt.md 标题
|
|
2185
|
+
spec = focus_artboard(ab, src_png, cdir, canvas_name=name)
|
|
2158
2186
|
|
|
2159
2187
|
# 交互分类(视觉信号优先,名称兜底 — 详见 classify_artboard)
|
|
2160
|
-
|
|
2188
|
+
# 传 orig_name:名称模式是子串匹配,唯一化后缀(-2)不该参与语义判定
|
|
2189
|
+
at, interaction = classify_artboard(orig_name, spec, ab.get("layers") or [], ab.get("height", 812))
|
|
2161
2190
|
spec["artboardType"] = at
|
|
2162
2191
|
spec["interaction"] = interaction
|
|
2163
2192
|
# JSAPI 交互控件检测(密码/验证码/数字键盘/toast/二次确认)
|
|
@@ -2181,6 +2210,21 @@ def main():
|
|
|
2181
2210
|
)
|
|
2182
2211
|
spec["slices"] = slices_rel
|
|
2183
2212
|
|
|
2213
|
+
# 切图索引(C5):slices/ 是嵌套目录(如 8.icon/通用/警示-#999@3x.png),没有索引时
|
|
2214
|
+
# 模型只能 fs_list 逐层摸索真名(gen-1 实测 6 次 fs_list + 2 次 fs_copy 手工搬运)。
|
|
2215
|
+
# 有了索引,第一次写 IR 就能对上真名(引用名按 R31 消毒,expand_ir 递归命中)。
|
|
2216
|
+
try:
|
|
2217
|
+
_slice_root = cdir / "slices"
|
|
2218
|
+
_slice_idx = [
|
|
2219
|
+
{"file": str(p.relative_to(_slice_root)).replace(os.sep, "/"), "bytes": p.stat().st_size}
|
|
2220
|
+
for p in sorted(_slice_root.rglob("*"))
|
|
2221
|
+
if p.is_file() and p.name != "index.json" and p.suffix.lower() in {".png", ".jpg", ".jpeg", ".webp", ".svg"}
|
|
2222
|
+
]
|
|
2223
|
+
(_slice_root / "index.json").write_text(
|
|
2224
|
+
json.dumps(_slice_idx, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
2225
|
+
except Exception as e: # noqa
|
|
2226
|
+
print(f" [WARN] slices/index.json 写入失败(不阻塞要素包):{e}", file=sys.stderr)
|
|
2227
|
+
|
|
2184
2228
|
# 画板是否无卡(0 卡 → 标记跳过,保留整图供参考)
|
|
2185
2229
|
# 仅当既无独立卡片、又元素极少(<3)才视为真正空画板;
|
|
2186
2230
|
# 元素丰富但无卡的情况可能是底部弹窗/全屏覆盖层,保留 status 供下游决策。
|
|
@@ -302,11 +302,6 @@ def crop_png_masked(path, box, out_path, scale=1.0, shapes=None):
|
|
|
302
302
|
write_png(out_path, cw, ch, 4, out_rows)
|
|
303
303
|
|
|
304
304
|
|
|
305
|
-
def _paeth(a, b, c):
|
|
306
|
-
pa, pb, pc = abs(b - c), abs(a - c), abs(a + b - 2 * c)
|
|
307
|
-
return a if (pa <= pb and pa <= pc) else (b if pb <= pc else c)
|
|
308
|
-
|
|
309
|
-
|
|
310
305
|
def write_png(path, width, height, channels, rows):
|
|
311
306
|
"""写 8-bit 非交织 PNG(filter 全 0,简单可靠)。"""
|
|
312
307
|
stride = width * channels
|