@yuhan1124/draw-prompt 0.4.6 → 0.4.7

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 CHANGED
@@ -35,6 +35,7 @@ Prompt 的默认结构是三段:`User visual brief` 原始视觉需求、`Styl
35
35
 
36
36
  ```bash
37
37
  npx --yes --registry=https://registry.npmjs.org/ @yuhan1124/draw-prompt@latest status
38
+ npx --yes --registry=https://registry.npmjs.org/ @yuhan1124/draw-prompt@latest doctor
38
39
  npx --yes --registry=https://registry.npmjs.org/ @yuhan1124/draw-prompt@latest convert "茶饮新品海报,写冷泡系列"
39
40
  npx --yes --registry=https://registry.npmjs.org/ @yuhan1124/draw-prompt@latest install-skill --target codex
40
41
  ```
@@ -116,6 +117,7 @@ Harness,作为下一次转化的辅助信号。
116
117
 
117
118
  ```
118
119
  convert "自然语言画图需求" [--style-preset premium] [--strict-text] [--out p] # 默认 single-pass:需求 → Prompt / handoff
120
+ doctor # 安装后运行时自检:包文件、版本、核心场景转化
119
121
  compose "长输入/文档内容" --max-images 6 # 长输入 → 多图视觉计划
120
122
  variants "自然语言画图需求" --style-presets all # 同一输入 → 多风格 Prompt 组
121
123
  styles --json # 查看内置风格预设
package/SKILL.md CHANGED
@@ -8,7 +8,7 @@ description: >-
8
8
  画图的指令"、"优化我的出图 prompt"、"按我的风格生成 prompt",或在用 GPT Image 2 /
9
9
  gpt-image-2 出图前需要一段精准提示词时,使用本 skill。
10
10
  metadata:
11
- version: 0.4.6
11
+ version: 0.4.7
12
12
  openclaw:
13
13
  anyBins: ["uv", "python3"]
14
14
  ---
@@ -61,6 +61,7 @@ prompt」「给 Codex 出图的指令」「优化这条出图 prompt」等意图
61
61
 
62
62
  2. **优先自动转化,并选对入口**。能直接交付时,按场景运行对应命令:
63
63
  - 安装到 agent skill 目录:`prompt_cli.py install-skill --target codex|claude [--force]`
64
+ - 安装后自检:`prompt_cli.py doctor`
64
65
  - 单图:`prompt_cli.py convert "<自然语言画图需求>" [--style-preset premium] [--strict-text] [--out <path>] [--record-pending]`
65
66
  - 长输入整理成多张图:`prompt_cli.py compose "<长输入>" --max-images 6 [--style-preset corporate] [--strict-text]`
66
67
  - 同一输入多风格探索:`prompt_cli.py variants "<自然语言画图需求>" --style-presets all`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuhan1124/draw-prompt",
3
- "version": "0.4.6",
3
+ "version": "0.4.7",
4
4
  "description": "Convert natural-language image requests into high-quality gpt-image-2 prompts and Codex handoff blocks.",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -136,7 +136,7 @@ def ensure_home() -> None:
136
136
 
137
137
 
138
138
  SCHEMA_VERSION = 1
139
- COMPILER_VERSION = "0.4.6"
139
+ COMPILER_VERSION = "0.4.7"
140
140
 
141
141
 
142
142
  PACKAGED_SKILL_FILES = [
@@ -154,6 +154,63 @@ PACKAGED_SKILL_FILES = [
154
154
  "references/harness.md",
155
155
  ]
156
156
 
157
+ DOCTOR_CASES = [
158
+ {
159
+ "id": "social-plain-label",
160
+ "request": "小红书封面,主题 30分钟搭好AI知识库,副标题 从资料整理到智能问答,不要真人,1:1",
161
+ "asset_type": "poster",
162
+ "style_preset": "creator-economy",
163
+ "expect_asset_type": "poster",
164
+ "expect_template_id": "poster_social_cover",
165
+ "expect_aspect": "1:1",
166
+ "expect_required_text": ["30分钟搭好AI知识库", "从资料整理到智能问答"],
167
+ "forbid_prompt": ["price/offer block"],
168
+ },
169
+ {
170
+ "id": "saas-dashboard",
171
+ "request": "SaaS 看板,需要包含通过率、失败数、平均评分、待修复,16:9",
172
+ "asset_type": "ui",
173
+ "style_preset": "clean-ui",
174
+ "expect_asset_type": "ui",
175
+ "expect_template_id": "ui_dashboard",
176
+ "expect_aspect": "16:9",
177
+ "expect_required_text": ["通过率", "失败数", "平均评分", "待修复"],
178
+ "forbid_prompt": ["chart panel", "data table"],
179
+ },
180
+ {
181
+ "id": "event-poster",
182
+ "request": "设计一张 AI 产品发布会海报,4:5,标题 Prompt Craft 2026,副标题是 Build visual ideas faster,时间 2026.08.18,地点 Shanghai。",
183
+ "asset_type": "poster",
184
+ "style_preset": "event-poster",
185
+ "expect_asset_type": "poster",
186
+ "expect_template_id": "poster_event",
187
+ "expect_aspect": "4:5",
188
+ "expect_required_text": ["Prompt Craft 2026", "Build visual ideas faster", "2026.08.18", "Shanghai"],
189
+ "forbid_prompt": ["price/offer block"],
190
+ },
191
+ {
192
+ "id": "architecture-diagram",
193
+ "request": "draw-prompt 产品架构图,16:9。需要展示用户输入、场景识别、风格预设、Prompt 编译器、意图保持检查、质量门、Image 2 出图、样本反馈学习。",
194
+ "asset_type": "diagram",
195
+ "style_preset": "systems-map",
196
+ "expect_asset_type": "diagram",
197
+ "expect_template_id": "diagram_system",
198
+ "expect_aspect": "16:9",
199
+ "expect_required_text": ["用户输入", "场景识别", "Prompt 编译器", "质量门", "Image 2 出图", "样本反馈学习"],
200
+ "forbid_prompt": ["academic system-architecture figure"],
201
+ },
202
+ {
203
+ "id": "architecture-with-app-web",
204
+ "request": "画一张 16:9 中文技术架构图:AI 客服工单自动化系统。结构从左到右:用户入口(App、Web、企业微信)进入消息网关;再到意图识别、知识库检索、工单创建、人工坐席协同;底部有监控与质检层,包含响应时长、解决率、满意度、风险拦截;右侧输出自动回复、工单流转、运营报表。要求企业级、清晰、不要像宣传海报,中文标签要尽量清楚。",
205
+ "style_preset": "technical-blueprint",
206
+ "expect_asset_type": "diagram",
207
+ "expect_template_id": "diagram_system",
208
+ "expect_aspect": "16:9",
209
+ "expect_required_text": ["用户入口", "App", "Web", "企业微信", "消息网关", "意图识别", "知识库检索", "工单创建", "人工坐席协同", "响应时长", "解决率", "满意度", "风险拦截", "自动回复", "工单流转", "运营报表"],
210
+ "forbid_prompt": ["UI mockup", "dashboard workspace"],
211
+ },
212
+ ]
213
+
157
214
 
158
215
  def package_root() -> Path:
159
216
  return Path(__file__).resolve().parent.parent
@@ -2378,9 +2435,9 @@ def route_asset_type(request: str, override: str | None = None) -> str:
2378
2435
  lower = request.lower()
2379
2436
  explicit_routes = [
2380
2437
  ("slide", ["powerpoint", "slide", "presentation", "ppt", "幻灯片", "演示文稿", "汇报单页"]),
2381
- ("ui", ["ui", "界面", "app", "dashboard", "仪表盘", "看板", "saas", "后台", "控制台", "网页", "mockup"]),
2382
2438
  ("diagram", ["架构图", "系统图", "流程架构", "architecture diagram", "system diagram"]),
2383
2439
  ("infographic", ["信息图", "infographic", "图解", "流程图", "时间线"]),
2440
+ ("ui", ["ui", "界面", "app", "dashboard", "仪表盘", "看板", "saas", "后台", "控制台", "网页", "mockup"]),
2384
2441
  ("poster", ["海报", "poster", "banner", "主视觉", "kv", "封面"]),
2385
2442
  ("logo", ["logo", "品牌标识", "字标", "visual identity"]),
2386
2443
  ]
@@ -2452,7 +2509,7 @@ def extract_required_texts(request: str, explicit_texts: list[str]) -> list[str]
2452
2509
  text = re.sub(r"(?:要)?(?:渲染)?(?:清晰|清楚|可读|明显)$", "", text)
2453
2510
  text = re.sub(r"(?:这些)?元素$", "", text)
2454
2511
  text = re.sub(r"(?:[一二三四五六七八九十\d]+个)?步骤$", "", text)
2455
- if re.search(r"箭头.*关系|关系.*箭头|展示输入输出关系", text):
2512
+ if re.search(r"箭头.*(?:关系|展示|输入|输出)|(?:关系|展示|输入|输出).*箭头|展示输入输出关系", text):
2456
2513
  return ""
2457
2514
  if re.fullmatch(r"[A-Za-z]{1,2}", text) and text.upper() not in {"AI", "UI"}:
2458
2515
  return ""
@@ -2558,7 +2615,7 @@ def merge_texts(primary: list[str], extra: list[str]) -> list[str]:
2558
2615
  text = re.sub(r"(?:这些)?元素$", "", text)
2559
2616
  text = re.sub(r"(?:[一二三四五六七八九十\d]+个)?步骤$", "", text)
2560
2617
  text = text.strip(" \t\n\r,,、。;;::.!?!?")
2561
- if re.search(r"箭头.*关系|关系.*箭头|展示输入输出关系", text):
2618
+ if re.search(r"箭头.*(?:关系|展示|输入|输出)|(?:关系|展示|输入|输出).*箭头|展示输入输出关系", text):
2562
2619
  continue
2563
2620
  if re.fullmatch(r"[A-Za-z]{1,2}", text) and text.upper() not in {"AI", "UI"}:
2564
2621
  continue
@@ -2572,29 +2629,39 @@ def merge_texts(primary: list[str], extra: list[str]) -> list[str]:
2572
2629
 
2573
2630
 
2574
2631
  def extract_structural_labels(request: str, asset_type: str) -> list[str]:
2575
- if asset_type not in {"diagram", "infographic", "slide", "ui", "poster"}:
2632
+ if asset_type not in {"diagram", "infographic", "slide", "ui", "poster", "product"}:
2576
2633
  return []
2577
2634
  candidates: list[tuple[int, str]] = []
2578
- list_intro = r"(?:需要)?(?:展示|呈现|列出|包含|包括|含有|分为|覆盖)"
2635
+ list_intro = r"(?:需要)?(?:展示|呈现|列出|包含|包括|含有|分为|覆盖|输出)"
2579
2636
  stop_words = r"(?:\b16\s*:\s*9\b|\b9\s*:\s*16\b|\b3\s*:\s*4\b|\b1\s*:\s*1\b|适合|用于|画幅|aspect|高质量|高清|clean|corporate)"
2580
2637
  patterns = [
2581
- rf"{list_intro}\s*(?:这些|以下|对应的)?(?:模块|部分|层|栏目|节点|入口|能力|场景|列表|指标卡|卡片|步骤|分支)?[::\s]*([^。;;\n]{{2,180}})",
2582
- rf"(?:模块|节点|栏目|部分|层|入口|能力|场景|列表|指标卡|卡片|步骤|分支)\s*(?:包括|包含|有|为)[::\s]*([^。;;\n]{{2,180}})",
2583
- rf"(?:模块|节点|栏目|部分|层|入口|能力|场景|列表|指标卡|卡片|步骤|分支)[^。;;\n::]{{0,16}}[::]([^。;;\n]{{2,180}})",
2638
+ rf"(?:结构(?:从左到右|从右到左|从上到下|从下到上)?|链路|流程)\s*[::]\s*([^。\n]{{2,240}})",
2639
+ rf"(?:数据|指标|核心数据|关键指标|卖点|亮点|功能点|特性|活动路径|路径)\s*(?:包括|包含|有|为)?[::\s]*([^。;;\n]{{2,180}})",
2640
+ rf"{list_intro}\s*(?:这些|以下|对应的)?(?:模块|部分|层|栏目|节点|入口|能力|场景|列表|指标卡|卡片|步骤|分支|数据|指标|卖点|亮点|功能点|特性|路径)?[::\s]*([^。;;\n]{{2,180}})",
2641
+ rf"(?:模块|节点|栏目|部分|层|入口|能力|场景|列表|指标卡|卡片|步骤|分支|数据|指标|卖点|亮点|功能点|特性|路径)\s*(?:包括|包含|有|为)[::\s]*([^。;;\n]{{2,180}})",
2642
+ rf"(?:模块|节点|栏目|部分|层|入口|能力|场景|列表|指标卡|卡片|步骤|分支|数据|指标|卖点|亮点|功能点|特性|路径)[^。;;\n::]{{0,16}}[::]([^。;;\n]{{2,180}})",
2584
2643
  ]
2585
2644
  for pattern in patterns:
2586
2645
  for match in re.finditer(pattern, request, flags=re.IGNORECASE):
2587
2646
  value = re.split(stop_words, match.group(1), maxsplit=1, flags=re.IGNORECASE)[0]
2647
+ value = re.sub(r"[()()]", "、", value)
2648
+ value = re.sub(r"(?:进入|再到|到|输出|->|→)", "、", value)
2588
2649
  for part_match in re.finditer(r"[^、,,;;/|]+", value):
2589
2650
  part = part_match.group(0).strip(" \t\n\r,,、::")
2590
2651
  part = re.sub(r"^(?:[^::]{0,12}(?:指标卡|卡片|列表|标题|模块|节点|步骤|栏目|分支))[::]", "", part)
2591
- part = re.sub(r"^(?:和|与|及|以及|and)\s*", "", part, flags=re.IGNORECASE).strip()
2652
+ part = re.sub(r"^(?:和|与|及|以及|and|再|则)\s*", "", part, flags=re.IGNORECASE).strip()
2653
+ part = re.sub(r"^(?:底部有|顶部有|左侧有|右侧有|上方有|下方有)\s*", "", part)
2654
+ part = re.sub(r"^(?:包含|包括|含有)\s*", "", part)
2592
2655
  part = re.sub(r"\s*(?:和|与|及|以及|and)$", "", part, flags=re.IGNORECASE).strip()
2593
2656
  part = re.sub(r"(?:这些)?元素$", "", part)
2594
2657
  part = re.sub(r"(?:[一二三四五六七八九十\d]+个)?步骤$", "", part)
2595
2658
  part = part.strip(" \t\n\r,,、::")
2596
2659
  if not part:
2597
2660
  continue
2661
+ if re.search(r"箭头.*(?:关系|展示|输入|输出)|(?:关系|展示|输入|输出).*箭头|展示输入输出关系", part):
2662
+ continue
2663
+ if part in {"顶部", "底部", "左侧", "右侧", "上方", "下方", "中间", "顶部导航", "底部导航", "左侧导航", "右侧导航", "导航栏", "顶部栏", "状态栏", "箭头关系", "箭头", "关系"}:
2664
+ continue
2598
2665
  if re.fullmatch(r"\d+(?:\s*:\s*\d+)?", part):
2599
2666
  continue
2600
2667
  if len(part) > 36:
@@ -2630,6 +2697,43 @@ def infer_style_anchors(request: str, override: str | None, profile: dict, prese
2630
2697
  return anchors[:4]
2631
2698
 
2632
2699
 
2700
+ def infer_palette_constraints(request: str) -> list[str]:
2701
+ color_terms = [
2702
+ "红", "橙", "黄", "绿", "青", "蓝", "紫", "粉", "黑", "白", "灰", "金", "银", "棕",
2703
+ "深色", "浅色", "渐变", "配色", "主色", "点缀", "背景",
2704
+ "red", "orange", "yellow", "green", "cyan", "blue", "purple", "pink", "black", "white",
2705
+ "gray", "grey", "gold", "silver", "brown", "gradient", "palette", "color", "colour",
2706
+ ]
2707
+ constraints: list[str] = []
2708
+ seen: set[str] = set()
2709
+ for sentence in re.split(r"[。;;\n]", request):
2710
+ for fragment in re.split(r"[,,]", sentence):
2711
+ fragment = fragment.strip(" \t\r\n。.;;,,")
2712
+ if not fragment:
2713
+ continue
2714
+ lower = fragment.lower()
2715
+ if not any(term in lower for term in color_terms):
2716
+ continue
2717
+ if len(fragment) > 72:
2718
+ fragment = fragment[:72].rstrip()
2719
+ if "红" in fragment and any(marker in fragment for marker in ["不要太俗", "别太俗", "不能太俗", "不俗", "高级"]):
2720
+ normalized = "use a restrained, mature red campaign accent palette; keep red visibly present without garish saturated red"
2721
+ if normalized not in seen:
2722
+ seen.add(normalized)
2723
+ constraints.append(normalized)
2724
+ if len(constraints) >= 3:
2725
+ return constraints
2726
+ continue
2727
+ key = re.sub(r"\s+", "", fragment)
2728
+ if key in seen:
2729
+ continue
2730
+ seen.add(key)
2731
+ constraints.append(f"preserve requested color direction: {fragment}")
2732
+ if len(constraints) >= 3:
2733
+ return constraints
2734
+ return constraints
2735
+
2736
+
2633
2737
  def infer_negative(asset_type: str, texts: list[str], profile: dict, request: str = "", template_id: str = "") -> list[str]:
2634
2738
  negative = ["avoid vague generic AI gloss", "avoid clutter", "avoid adding content not requested by the user"]
2635
2739
  if texts:
@@ -3023,7 +3127,7 @@ def build_spec(args: argparse.Namespace) -> dict:
3023
3127
  "style_anchors": infer_style_anchors(safe_request, getattr(args, "style", None), profile, style_preset),
3024
3128
  "materials": split_csv(getattr(args, "materials", None)) or ["tactile, specific visible materials chosen for the subject"],
3025
3129
  "lighting": getattr(args, "lighting", None) or "controlled, readable light with clear subject hierarchy",
3026
- "palette": split_csv(getattr(args, "palette", None)) or ["restrained palette matched to the asset type"],
3130
+ "palette": split_csv(getattr(args, "palette", None)) or infer_palette_constraints(visual_request) or ["restrained palette matched to the asset type"],
3027
3131
  "negative": negative,
3028
3132
  "must_include": infer_must_include(asset_type, template_id, texts, bool(getattr(args, "strict_text", False))),
3029
3133
  "must_avoid": negative,
@@ -3157,7 +3261,7 @@ def render_prompt(spec: dict) -> str:
3157
3261
  if asset_type == "infographic":
3158
3262
  return "\n".join(
3159
3263
  [
3160
- f"Create a {aspect} educational infographic from the user visual brief below.",
3264
+ f"Create a {aspect} information graphic from the user visual brief below.",
3161
3265
  brief_block,
3162
3266
  style_quality_block(spec, label="infographic", extra=["Build a clear information hierarchy from the user's brief and preserve any named regions, section counts, and reading order."]),
3163
3267
  f"Composition support: {spec.get('template_label', 'infographic')}; layout guidance: {layout}.",
@@ -5607,6 +5711,113 @@ def npm_registry_status() -> str:
5607
5711
  return f"{registry}(建议 npx 加 --registry=https://registry.npmjs.org/)"
5608
5712
 
5609
5713
 
5714
+ def read_skill_version(path: Path) -> str:
5715
+ if not path.exists():
5716
+ return ""
5717
+ for line in path.read_text(encoding="utf-8").splitlines():
5718
+ match = re.match(r"\s*version:\s*([^\s]+)", line)
5719
+ if match:
5720
+ return match.group(1)
5721
+ return ""
5722
+
5723
+
5724
+ def doctor_add(findings: list[dict], severity: str, rule: str, message: str) -> None:
5725
+ findings.append({"severity": severity, "rule": rule, "message": message})
5726
+
5727
+
5728
+ def doctor_check_package(root: Path) -> dict:
5729
+ findings: list[dict] = []
5730
+ package_path = root / "package.json"
5731
+ package_version = ""
5732
+ if package_path.exists():
5733
+ try:
5734
+ package_version = str(json.loads(package_path.read_text(encoding="utf-8")).get("version") or "")
5735
+ except Exception as exc:
5736
+ doctor_add(findings, "error", "package.invalid_json", f"package.json 读取失败:{exc}")
5737
+ else:
5738
+ doctor_add(findings, "error", "package.missing", "缺少 package.json")
5739
+ skill_version = read_skill_version(root / "SKILL.md")
5740
+ if not skill_version:
5741
+ doctor_add(findings, "error", "skill.version_missing", "SKILL.md 缺少 metadata.version")
5742
+ if package_version and skill_version and package_version != skill_version:
5743
+ doctor_add(findings, "error", "version.package_skill_mismatch", f"package.json={package_version} SKILL.md={skill_version}")
5744
+ if package_version and package_version != COMPILER_VERSION:
5745
+ doctor_add(findings, "error", "version.compiler_mismatch", f"package.json={package_version} compiler={COMPILER_VERSION}")
5746
+ missing = [rel for rel in PACKAGED_SKILL_FILES if not (root / rel).exists()]
5747
+ for rel in missing:
5748
+ doctor_add(findings, "error", "package.file_missing", f"缺少运行文件:{rel}")
5749
+ style_count = len(available_style_presets(include_auto=False))
5750
+ if style_count < 400:
5751
+ doctor_add(findings, "error", "styles.too_few", f"内置风格数量过少:{style_count}")
5752
+ return {
5753
+ "name": "package",
5754
+ "pass": not has_lint_error(findings),
5755
+ "version": package_version or COMPILER_VERSION,
5756
+ "style_count": style_count,
5757
+ "missing_files": missing,
5758
+ "findings": findings,
5759
+ }
5760
+
5761
+
5762
+ def doctor_check_case(case: dict) -> dict:
5763
+ findings: list[dict] = []
5764
+ result = convert_for_benchmark(case)
5765
+ spec = result["spec"]
5766
+ prompt = result["prompt"]
5767
+ for item in result["lint"]:
5768
+ findings.append({"severity": item["severity"], "rule": f"lint.{item['rule']}", "message": item["message"]})
5769
+ for item in result["intent_check"]:
5770
+ findings.append({"severity": item["severity"], "rule": f"intent.{item['rule']}", "message": item["message"]})
5771
+ if case.get("expect_asset_type") and spec.get("asset_type") != case["expect_asset_type"]:
5772
+ doctor_add(findings, "error", "case.asset_type", f"期望 asset_type={case['expect_asset_type']},实际={spec.get('asset_type')}")
5773
+ if case.get("expect_template_id") and spec.get("template_id") != case["expect_template_id"]:
5774
+ doctor_add(findings, "error", "case.template_id", f"期望 template_id={case['expect_template_id']},实际={spec.get('template_id')}")
5775
+ if case.get("expect_aspect") and spec.get("aspect") != case["expect_aspect"]:
5776
+ doctor_add(findings, "error", "case.aspect", f"期望 aspect={case['expect_aspect']},实际={spec.get('aspect')}")
5777
+ labels = set(spec.get("required_text") or [])
5778
+ for text in case.get("expect_required_text") or []:
5779
+ if text not in labels:
5780
+ doctor_add(findings, "error", "case.required_text_missing", f"缺少必显文字:{text}")
5781
+ elif f'"{text}"' not in prompt:
5782
+ doctor_add(findings, "error", "case.required_text_not_quoted", f"Prompt 未逐字引用:{text}")
5783
+ prompt_lower = prompt.lower()
5784
+ for phrase in case.get("forbid_prompt") or []:
5785
+ if phrase.lower() in prompt_lower:
5786
+ doctor_add(findings, "error", "case.forbidden_prompt", f"Prompt 出现不应出现的片段:{phrase}")
5787
+ return {
5788
+ "name": case["id"],
5789
+ "pass": not has_lint_error(findings),
5790
+ "asset_type": spec.get("asset_type"),
5791
+ "template_id": spec.get("template_id"),
5792
+ "aspect": spec.get("aspect"),
5793
+ "prompt_digest": result["prompt_digest"],
5794
+ "required_text": spec.get("required_text") or [],
5795
+ "findings": findings,
5796
+ }
5797
+
5798
+
5799
+ def cmd_doctor(args: argparse.Namespace) -> int:
5800
+ root = package_root()
5801
+ checks = [doctor_check_package(root)]
5802
+ for case in DOCTOR_CASES:
5803
+ try:
5804
+ checks.append(doctor_check_case(case))
5805
+ except Exception as exc:
5806
+ checks.append({"name": case.get("id", "case"), "pass": False, "findings": [{"severity": "error", "rule": "case.exception", "message": str(exc)}]})
5807
+ passed = all(item["pass"] for item in checks)
5808
+ summary = {"pass": passed, "version": COMPILER_VERSION, "checks": len(checks), "failed": len([item for item in checks if not item["pass"]])}
5809
+ if args.json:
5810
+ print(json.dumps({"summary": summary, "checks": checks}, ensure_ascii=False, indent=2))
5811
+ else:
5812
+ print(f"doctor: version={COMPILER_VERSION} checks={summary['checks']} pass={passed}")
5813
+ for item in checks:
5814
+ status = "PASS" if item["pass"] else "FAIL"
5815
+ print(f"- {item['name']}: {status}")
5816
+ for finding in item.get("findings", []):
5817
+ print(f" [{finding['severity']}] {finding['rule']}: {finding['message']}")
5818
+ return 0 if passed else 1
5819
+
5820
+
5610
5821
  def cmd_status(args: argparse.Namespace) -> int:
5611
5822
  print("draw-prompt 环境检查")
5612
5823
  print(f" 数据目录 : {data_home()} ({'存在' if data_home().exists() else '未创建'})")
@@ -5628,7 +5839,7 @@ def cmd_status(args: argparse.Namespace) -> int:
5628
5839
  print(f" codex CLI : {which('codex') or '未找到'}")
5629
5840
  plugin = Path.home() / ".claude" / "plugins" / "cache" / "codex-image-in-cc"
5630
5841
  print(f" codex-image: {'已安装' if plugin.exists() else '未安装(可 /codex-image:generate 出图)'}")
5631
- print(" 核心转化命令: convert / compose / variants / series / edit / brand / character / data-viz / rewrite / adapt")
5842
+ print(" 核心转化命令: doctor / convert / compose / variants / series / edit / brand / character / data-viz / rewrite / adapt")
5632
5843
  print(" 稳定性命令 : overlay / visual-check / edit-check / visual-regress / lint / intent-check / benchmark / revise / styles")
5633
5844
  return 0
5634
5845
 
@@ -5674,6 +5885,10 @@ def build_parser() -> argparse.ArgumentParser:
5674
5885
  pis.add_argument("--json", action="store_true")
5675
5886
  pis.set_defaults(func=cmd_install_skill)
5676
5887
 
5888
+ pdoc = sub.add_parser("doctor", help="安装后运行时自检:包文件、版本和核心场景转化")
5889
+ pdoc.add_argument("--json", action="store_true")
5890
+ pdoc.set_defaults(func=cmd_doctor)
5891
+
5677
5892
  pc = sub.add_parser("convert", help="自然语言画图需求 -> 高质量生图 Prompt / handoff")
5678
5893
  pc.add_argument("request_text", nargs="+", help="自然语言画图需求")
5679
5894
  pc.add_argument("--asset-type", choices=sorted(ASSET_ROUTES.keys()), help="覆盖自动识别的资产类型")