@yuhan1124/draw-prompt 0.4.8 → 0.4.10
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 +3 -1
- package/SKILL.md +1 -1
- package/package.json +1 -1
- package/scripts/prompt_cli.py +394 -9
package/README.md
CHANGED
|
@@ -70,7 +70,9 @@ npx --yes --registry=https://registry.npmjs.org/ @yuhan1124/draw-prompt@latest i
|
|
|
70
70
|
`install-skill` 默认复制 npm 包内的运行文件,避免软链到 npx 缓存导致后续路径失效。它只复制
|
|
71
71
|
`SKILL.md`、CLI、references 和必要元数据;开发仓库里的 `tests/`、`tmp/`、`golden-cases.jsonl`、
|
|
72
72
|
`visual-cases.jsonl` 不会进入 npm 包或安装目录。安装后可直接跑 `doctor`,它会检查
|
|
73
|
-
|
|
73
|
+
包文件、版本一致性、核心单图转化、真实长输入 `compose` 链路,以及
|
|
74
|
+
`variants` / `series` / `adapt` 等主工作流 smoke。
|
|
75
|
+
`brand` / `character` / `data-viz` / `edit` / `rewrite` 这类主入口也在 `doctor` 中做命令级 smoke。
|
|
74
76
|
|
|
75
77
|
开发者本地调试也可以软链 repo:
|
|
76
78
|
|
package/SKILL.md
CHANGED
package/package.json
CHANGED
package/scripts/prompt_cli.py
CHANGED
|
@@ -45,8 +45,10 @@ DRAW_PROMPT_HOME 覆盖),不进 git repo。
|
|
|
45
45
|
from __future__ import annotations
|
|
46
46
|
|
|
47
47
|
import argparse
|
|
48
|
+
import contextlib
|
|
48
49
|
import csv
|
|
49
50
|
import hashlib
|
|
51
|
+
import io
|
|
50
52
|
import json
|
|
51
53
|
import os
|
|
52
54
|
import re
|
|
@@ -136,7 +138,7 @@ def ensure_home() -> None:
|
|
|
136
138
|
|
|
137
139
|
|
|
138
140
|
SCHEMA_VERSION = 1
|
|
139
|
-
COMPILER_VERSION = "0.4.
|
|
141
|
+
COMPILER_VERSION = "0.4.10"
|
|
140
142
|
|
|
141
143
|
|
|
142
144
|
PACKAGED_SKILL_FILES = [
|
|
@@ -234,6 +236,66 @@ DOCTOR_COMPOSE_CASES = [
|
|
|
234
236
|
}
|
|
235
237
|
]
|
|
236
238
|
|
|
239
|
+
DOCTOR_WORKFLOW_CASES = [
|
|
240
|
+
{
|
|
241
|
+
"id": "variants-style-envelope",
|
|
242
|
+
"kind": "variants",
|
|
243
|
+
"request": "小红书方图,主题 AI 生图 Prompt 三步法,标题 生图 Prompt 三步法",
|
|
244
|
+
"style_presets": ["premium", "flat-vector"],
|
|
245
|
+
"expect_count": 2,
|
|
246
|
+
"expect_required_text": ["AI 生图 Prompt 三步法", "生图 Prompt 三步法"],
|
|
247
|
+
"forbid_palette": ["小红书方图"],
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
"id": "series-consistent-posters",
|
|
251
|
+
"kind": "series",
|
|
252
|
+
"briefs": ["茶饮品牌春季主视觉,突出冷泡茶瓶", "同一品牌社媒方图,展示三种口味"],
|
|
253
|
+
"shared_style": "single coherent series style, same camera language, same palette discipline, same visual density",
|
|
254
|
+
"style_preset": "premium",
|
|
255
|
+
"expect_count": 2,
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
"id": "adapt-product-brand-and-selling-points",
|
|
259
|
+
"kind": "adapt",
|
|
260
|
+
"request": "电商产品详情首屏,品牌 ThermoFlow,展示实时测温、24小时保温、316不锈钢、Type-C 充电",
|
|
261
|
+
"style_preset": "premium-packshot",
|
|
262
|
+
"aspects": ["1:1", "3:4", "16:9"],
|
|
263
|
+
"expect_required_text": ["ThermoFlow", "实时测温", "24小时保温", "316不锈钢", "Type-C 充电"],
|
|
264
|
+
},
|
|
265
|
+
]
|
|
266
|
+
|
|
267
|
+
DOCTOR_COMMAND_CASES = [
|
|
268
|
+
{
|
|
269
|
+
"id": "edit-preserve-change-contract",
|
|
270
|
+
"kind": "edit",
|
|
271
|
+
"expect_prompt": ["Preserve exactly: cup silhouette.", "Change only: background only."],
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
"id": "brand-compile",
|
|
275
|
+
"kind": "brand",
|
|
276
|
+
"expect_required_text": ["山川茶事"],
|
|
277
|
+
"expect_prompt": ["Brand system for invented brand"],
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
"id": "character-reference-and-scene",
|
|
281
|
+
"kind": "character",
|
|
282
|
+
"expect_prompt": ["Mira", "same identity anchors"],
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
"id": "data-viz-csv",
|
|
286
|
+
"kind": "data-viz",
|
|
287
|
+
"expect_title": "转化率趋势",
|
|
288
|
+
"expect_columns": ["week", "conversion"],
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
"id": "rewrite-risky-ip",
|
|
292
|
+
"kind": "rewrite",
|
|
293
|
+
"expect_safety_rewrite": True,
|
|
294
|
+
"forbid_prompt": ["迪士尼"],
|
|
295
|
+
"expect_prompt": ["original"],
|
|
296
|
+
},
|
|
297
|
+
]
|
|
298
|
+
|
|
237
299
|
|
|
238
300
|
def package_root() -> Path:
|
|
239
301
|
return Path(__file__).resolve().parent.parent
|
|
@@ -2491,12 +2553,69 @@ def infer_aspect(request: str, asset_type: str, override: str | None, profile: d
|
|
|
2491
2553
|
return "3:4"
|
|
2492
2554
|
if any(k in lower for k in ["方图", "方形", "square"]):
|
|
2493
2555
|
return "1:1"
|
|
2556
|
+
if asset_type == "ui":
|
|
2557
|
+
desktop_terms = [
|
|
2558
|
+
"saas",
|
|
2559
|
+
"后台",
|
|
2560
|
+
"控制台",
|
|
2561
|
+
"dashboard",
|
|
2562
|
+
"仪表盘",
|
|
2563
|
+
"看板",
|
|
2564
|
+
"网页",
|
|
2565
|
+
"web",
|
|
2566
|
+
"desktop",
|
|
2567
|
+
"左侧导航",
|
|
2568
|
+
"顶部状态栏",
|
|
2569
|
+
"右侧",
|
|
2570
|
+
]
|
|
2571
|
+
mobile_terms = ["手机", "mobile", "app 首页", "小程序", "9:16"]
|
|
2572
|
+
if any(term in lower for term in desktop_terms) and not any(term in lower for term in mobile_terms):
|
|
2573
|
+
return "16:9"
|
|
2494
2574
|
prof = str(profile.get("default_aspect") or "").strip()
|
|
2495
2575
|
if prof and prof != "未设置":
|
|
2496
2576
|
return prof
|
|
2497
2577
|
return str(ASSET_ROUTES[asset_type]["aspect"])
|
|
2498
2578
|
|
|
2499
2579
|
|
|
2580
|
+
NON_DISPLAY_TEXTS = {
|
|
2581
|
+
"顶部导航",
|
|
2582
|
+
"底部导航",
|
|
2583
|
+
"左侧导航",
|
|
2584
|
+
"右侧导航",
|
|
2585
|
+
"导航栏",
|
|
2586
|
+
"顶部栏",
|
|
2587
|
+
"底部栏",
|
|
2588
|
+
"状态栏",
|
|
2589
|
+
"顶部状态栏",
|
|
2590
|
+
"底部状态栏",
|
|
2591
|
+
"箭头关系",
|
|
2592
|
+
"箭头",
|
|
2593
|
+
"关系",
|
|
2594
|
+
"标签",
|
|
2595
|
+
"卖点标签",
|
|
2596
|
+
"购买转化氛围",
|
|
2597
|
+
"但不要过度促销",
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2600
|
+
|
|
2601
|
+
def is_non_display_text(text: str) -> bool:
|
|
2602
|
+
value = text.strip(" \t\n\r,,、。;;::.!?!?")
|
|
2603
|
+
lower = value.lower()
|
|
2604
|
+
if value in NON_DISPLAY_TEXTS:
|
|
2605
|
+
return True
|
|
2606
|
+
if lower.startswith(("不要", "不需要", "不能", "不得", "但不要", "avoid ", "do not ")):
|
|
2607
|
+
return True
|
|
2608
|
+
if re.fullmatch(r"\d+\s*%\s*(?:height|width|高|宽|高度|宽度)", lower):
|
|
2609
|
+
return True
|
|
2610
|
+
if re.fullmatch(r"\d+\s*(?:px|pt|rem|em)\s*(?:height|width|高|宽|高度|宽度)?", lower):
|
|
2611
|
+
return True
|
|
2612
|
+
if re.fullmatch(r"\d+\s*[:x×]\s*\d+", lower):
|
|
2613
|
+
return True
|
|
2614
|
+
if value.endswith("氛围"):
|
|
2615
|
+
return True
|
|
2616
|
+
return False
|
|
2617
|
+
|
|
2618
|
+
|
|
2500
2619
|
def infer_size(aspect: str, override: str | None, asset_type: str) -> str:
|
|
2501
2620
|
if override:
|
|
2502
2621
|
return override
|
|
@@ -2536,7 +2655,7 @@ def extract_required_texts(request: str, explicit_texts: list[str]) -> list[str]
|
|
|
2536
2655
|
return ""
|
|
2537
2656
|
if re.fullmatch(r"[A-Za-z]{1,2}", text) and text.upper() not in {"AI", "UI"}:
|
|
2538
2657
|
return ""
|
|
2539
|
-
if text
|
|
2658
|
+
if is_non_display_text(text):
|
|
2540
2659
|
return ""
|
|
2541
2660
|
return text.strip(" \t\n\r,,、。;;::.!?!?")
|
|
2542
2661
|
|
|
@@ -2574,6 +2693,7 @@ def extract_required_texts(request: str, explicit_texts: list[str]) -> list[str]
|
|
|
2574
2693
|
r"(?:时间|地点)\s+([^,,、。;;\n]{2,40})",
|
|
2575
2694
|
r"(?:核心卡片|主要按钮|主按钮|按钮)\s*(?:写上|写|显示|为|是|叫|[::])\s*([^,,。;;\n]{2,30})",
|
|
2576
2695
|
r"(?:需要)?包含\s*([A-Za-z][A-Za-z0-9_-]{2,30})\s*字样",
|
|
2696
|
+
r"(?:品牌名|品牌|brand)\s*(?:写上|写|显示|为|是|叫|[::])?\s*([A-Za-z][A-Za-z0-9_-]{2,30})\b",
|
|
2577
2697
|
r"(?:名为|叫做|名称是|名字叫)\s*([A-Za-z][A-Za-z0-9_-]{2,30})\b",
|
|
2578
2698
|
]
|
|
2579
2699
|
for pat in labeled_single_patterns:
|
|
@@ -2642,7 +2762,7 @@ def merge_texts(primary: list[str], extra: list[str]) -> list[str]:
|
|
|
2642
2762
|
continue
|
|
2643
2763
|
if re.fullmatch(r"[A-Za-z]{1,2}", text) and text.upper() not in {"AI", "UI"}:
|
|
2644
2764
|
continue
|
|
2645
|
-
if text
|
|
2765
|
+
if is_non_display_text(text):
|
|
2646
2766
|
continue
|
|
2647
2767
|
key = re.sub(r"\s+", "", text)
|
|
2648
2768
|
if text and key not in seen:
|
|
@@ -2664,6 +2784,10 @@ def extract_structural_labels(request: str, asset_type: str) -> list[str]:
|
|
|
2664
2784
|
rf"(?:模块|节点|栏目|部分|层|入口|能力|场景|列表|指标卡|卡片|步骤|分支|数据|指标|卖点|亮点|功能点|特性|路径)\s*(?:包括|包含|有|为)[::\s]*([^。;;\n]{{2,180}})",
|
|
2665
2785
|
rf"(?:模块|节点|栏目|部分|层|入口|能力|场景|列表|指标卡|卡片|步骤|分支|数据|指标|卖点|亮点|功能点|特性|路径)[^。;;\n::]{{0,16}}[::]([^。;;\n]{{2,180}})",
|
|
2666
2786
|
]
|
|
2787
|
+
if asset_type in {"diagram", "infographic", "slide", "ui"}:
|
|
2788
|
+
patterns.append(
|
|
2789
|
+
r"(?:图中|图里|画面中|架构图中|系统图中|流程图中)\s*(?:需要)?(?:要有|应有|需有|有)[::\s]*([^。;;\n]{2,180})"
|
|
2790
|
+
)
|
|
2667
2791
|
for pattern in patterns:
|
|
2668
2792
|
for match in re.finditer(pattern, request, flags=re.IGNORECASE):
|
|
2669
2793
|
value = re.split(stop_words, match.group(1), maxsplit=1, flags=re.IGNORECASE)[0]
|
|
@@ -2683,7 +2807,7 @@ def extract_structural_labels(request: str, asset_type: str) -> list[str]:
|
|
|
2683
2807
|
continue
|
|
2684
2808
|
if re.search(r"箭头.*(?:关系|展示|输入|输出)|(?:关系|展示|输入|输出).*箭头|展示输入输出关系", part):
|
|
2685
2809
|
continue
|
|
2686
|
-
if part in {"顶部", "底部", "左侧", "右侧", "上方", "下方", "中间"
|
|
2810
|
+
if part in {"顶部", "底部", "左侧", "右侧", "上方", "下方", "中间"} or is_non_display_text(part):
|
|
2687
2811
|
continue
|
|
2688
2812
|
if re.fullmatch(r"\d+(?:\s*:\s*\d+)?", part):
|
|
2689
2813
|
continue
|
|
@@ -2721,8 +2845,9 @@ def infer_style_anchors(request: str, override: str | None, profile: dict, prese
|
|
|
2721
2845
|
|
|
2722
2846
|
|
|
2723
2847
|
def infer_palette_constraints(request: str) -> list[str]:
|
|
2724
|
-
|
|
2725
|
-
|
|
2848
|
+
cjk_colors = "红橙黄绿青蓝紫粉黑白灰金银棕"
|
|
2849
|
+
false_color_tokens = ["小红书", "红书", "白皮书"]
|
|
2850
|
+
direct_color_terms = [
|
|
2726
2851
|
"深色", "浅色", "渐变", "配色", "主色", "点缀", "背景",
|
|
2727
2852
|
"red", "orange", "yellow", "green", "cyan", "blue", "purple", "pink", "black", "white",
|
|
2728
2853
|
"gray", "grey", "gold", "silver", "brown", "gradient", "palette", "color", "colour",
|
|
@@ -2735,7 +2860,11 @@ def infer_palette_constraints(request: str) -> list[str]:
|
|
|
2735
2860
|
if not fragment:
|
|
2736
2861
|
continue
|
|
2737
2862
|
lower = fragment.lower()
|
|
2738
|
-
if
|
|
2863
|
+
if any(token in fragment for token in false_color_tokens):
|
|
2864
|
+
continue
|
|
2865
|
+
has_direct_color = any(term in lower for term in direct_color_terms)
|
|
2866
|
+
has_cjk_color = re.search(rf"(?:[{cjk_colors}](?:色|系|调|主色|点缀|背景|渐变|不要太俗|高级)|(?:深|浅|亮|暗|磨砂|纯|主|背景|点缀|配色)[^,,。;;\n]{{0,12}}[{cjk_colors}]|黑白)", fragment)
|
|
2867
|
+
if not (has_direct_color or has_cjk_color):
|
|
2739
2868
|
continue
|
|
2740
2869
|
if len(fragment) > 72:
|
|
2741
2870
|
fragment = fragment[:72].rstrip()
|
|
@@ -3163,12 +3292,16 @@ def build_spec(args: argparse.Namespace) -> dict:
|
|
|
3163
3292
|
|
|
3164
3293
|
|
|
3165
3294
|
def exact_text_block(texts: list[str]) -> str:
|
|
3295
|
+
non_display_rule = (
|
|
3296
|
+
"Do not render layout measurements, area percentages, placement notes, placeholder source notes, "
|
|
3297
|
+
"or prompt instruction phrases as visible in-image text unless they are explicitly listed above as exact strings."
|
|
3298
|
+
)
|
|
3166
3299
|
if not texts:
|
|
3167
|
-
return "No required in-image text unless explicitly useful; avoid decorative fake text."
|
|
3300
|
+
return f"No required in-image text unless explicitly useful; avoid decorative fake text. {non_display_rule}"
|
|
3168
3301
|
quoted = " / ".join(f'"{t}"' for t in texts)
|
|
3169
3302
|
return (
|
|
3170
3303
|
"The image must accurately display these exact strings, unchanged and large enough to read: "
|
|
3171
|
-
f"{quoted}. Use crisp, legible typography; keep Chinese text exactly as written."
|
|
3304
|
+
f"{quoted}. Use crisp, legible typography; keep Chinese text exactly as written. {non_display_rule}"
|
|
3172
3305
|
)
|
|
3173
3306
|
|
|
3174
3307
|
|
|
@@ -3525,6 +3658,8 @@ def request_mentions_any(request: str, variants: list[str]) -> bool:
|
|
|
3525
3658
|
def intent_group_allowed_by_context(group: str, request: str, spec: dict) -> bool:
|
|
3526
3659
|
lower = request.lower()
|
|
3527
3660
|
asset_type = str(spec.get("asset_type") or "")
|
|
3661
|
+
if group == "background_scene":
|
|
3662
|
+
return any(k in lower for k in ["场景", "背景", "环境", "scene", "background", "environment", "beside", "inside", "outside"])
|
|
3528
3663
|
if group == "cards" and asset_type == "ui":
|
|
3529
3664
|
return any(k in lower for k in ["首页", "展示", "列表", "作品", "项目", "内容", "recent", "gallery", "feed"])
|
|
3530
3665
|
if group == "modules" and asset_type in {"diagram", "infographic", "slide"}:
|
|
@@ -5916,6 +6051,246 @@ def doctor_check_compose_case(case: dict) -> dict:
|
|
|
5916
6051
|
}
|
|
5917
6052
|
|
|
5918
6053
|
|
|
6054
|
+
def doctor_check_workflow_case(case: dict) -> dict:
|
|
6055
|
+
findings: list[dict] = []
|
|
6056
|
+
kind = case.get("kind")
|
|
6057
|
+
compiled_items: list[dict] = []
|
|
6058
|
+
if kind == "variants":
|
|
6059
|
+
request = str(case.get("request") or "")
|
|
6060
|
+
for preset in case.get("style_presets") or []:
|
|
6061
|
+
compiled_items.append(
|
|
6062
|
+
compile_visual_case(
|
|
6063
|
+
{"request": request, "style_preset": preset, "target": "raw", "tags": "doctor,variants"},
|
|
6064
|
+
target="raw",
|
|
6065
|
+
include_handoff=False,
|
|
6066
|
+
)
|
|
6067
|
+
)
|
|
6068
|
+
elif kind == "series":
|
|
6069
|
+
shared_style = str(case.get("shared_style") or "")
|
|
6070
|
+
for brief in case.get("briefs") or []:
|
|
6071
|
+
compiled_items.append(
|
|
6072
|
+
compile_visual_case(
|
|
6073
|
+
{"request": str(brief), "style": shared_style, "style_preset": case.get("style_preset"), "target": "raw", "tags": "doctor,series"},
|
|
6074
|
+
target="raw",
|
|
6075
|
+
include_handoff=False,
|
|
6076
|
+
)
|
|
6077
|
+
)
|
|
6078
|
+
elif kind == "adapt":
|
|
6079
|
+
request = str(case.get("request") or "")
|
|
6080
|
+
for aspect in case.get("aspects") or []:
|
|
6081
|
+
compiled_items.append(
|
|
6082
|
+
compile_visual_case(
|
|
6083
|
+
{"request": request, "aspect": aspect, "style_preset": case.get("style_preset"), "target": "raw", "tags": "doctor,adapt"},
|
|
6084
|
+
target="raw",
|
|
6085
|
+
include_handoff=False,
|
|
6086
|
+
)
|
|
6087
|
+
)
|
|
6088
|
+
else:
|
|
6089
|
+
doctor_add(findings, "error", "workflow.kind", f"未知工作流类型:{kind}")
|
|
6090
|
+
|
|
6091
|
+
for idx, item in enumerate(compiled_items, start=1):
|
|
6092
|
+
for lint in item["lint"]:
|
|
6093
|
+
findings.append({"severity": lint["severity"], "rule": f"workflow.{idx}.lint.{lint['rule']}", "message": lint["message"]})
|
|
6094
|
+
for intent in item["intent_check"]:
|
|
6095
|
+
findings.append({"severity": intent["severity"], "rule": f"workflow.{idx}.intent.{intent['rule']}", "message": intent["message"]})
|
|
6096
|
+
|
|
6097
|
+
if case.get("expect_count") and len(compiled_items) != int(case["expect_count"]):
|
|
6098
|
+
doctor_add(findings, "error", "workflow.count", f"期望 {case['expect_count']} 个输出,实际 {len(compiled_items)}")
|
|
6099
|
+
expected_texts = case.get("expect_required_text") or []
|
|
6100
|
+
for idx, item in enumerate(compiled_items, start=1):
|
|
6101
|
+
labels = set(item["spec"].get("required_text") or [])
|
|
6102
|
+
prompt = item["prompt"]
|
|
6103
|
+
for text_item in expected_texts:
|
|
6104
|
+
if text_item not in labels:
|
|
6105
|
+
doctor_add(findings, "error", "workflow.required_text_missing", f"第 {idx} 个输出缺少必显文字:{text_item}")
|
|
6106
|
+
elif f'"{text_item}"' not in prompt:
|
|
6107
|
+
doctor_add(findings, "error", "workflow.required_text_not_quoted", f"第 {idx} 个输出 Prompt 未逐字引用:{text_item}")
|
|
6108
|
+
forbidden_palette = case.get("forbid_palette") or []
|
|
6109
|
+
for idx, item in enumerate(compiled_items, start=1):
|
|
6110
|
+
palette = " ".join(str(part) for part in item["spec"].get("palette") or [])
|
|
6111
|
+
for forbidden in forbidden_palette:
|
|
6112
|
+
if forbidden in palette:
|
|
6113
|
+
doctor_add(findings, "error", "workflow.forbidden_palette", f"第 {idx} 个输出把非颜色词当成配色约束:{forbidden}")
|
|
6114
|
+
return {
|
|
6115
|
+
"name": f"workflow-{case['id']}",
|
|
6116
|
+
"pass": not has_lint_error(findings),
|
|
6117
|
+
"kind": kind,
|
|
6118
|
+
"count": len(compiled_items),
|
|
6119
|
+
"asset_types": [item["spec"].get("asset_type") for item in compiled_items],
|
|
6120
|
+
"prompt_digests": [item["prompt_digest"] for item in compiled_items],
|
|
6121
|
+
"required_text": [item["spec"].get("required_text") or [] for item in compiled_items],
|
|
6122
|
+
"findings": findings,
|
|
6123
|
+
}
|
|
6124
|
+
|
|
6125
|
+
|
|
6126
|
+
def doctor_run_json_command(func, args: argparse.Namespace) -> tuple[int, dict, str]:
|
|
6127
|
+
stdout = io.StringIO()
|
|
6128
|
+
with contextlib.redirect_stdout(stdout):
|
|
6129
|
+
code = func(args)
|
|
6130
|
+
output = stdout.getvalue()
|
|
6131
|
+
data = json.loads(output)
|
|
6132
|
+
return code, data, output
|
|
6133
|
+
|
|
6134
|
+
|
|
6135
|
+
def doctor_collect_quality(findings: list[dict], scope: str, payload: dict) -> None:
|
|
6136
|
+
for item in payload.get("lint") or []:
|
|
6137
|
+
findings.append({"severity": item["severity"], "rule": f"{scope}.lint.{item['rule']}", "message": item["message"]})
|
|
6138
|
+
for item in payload.get("intent_check") or []:
|
|
6139
|
+
findings.append({"severity": item["severity"], "rule": f"{scope}.intent.{item['rule']}", "message": item["message"]})
|
|
6140
|
+
|
|
6141
|
+
|
|
6142
|
+
def doctor_check_command_case(case: dict) -> dict:
|
|
6143
|
+
findings: list[dict] = []
|
|
6144
|
+
kind = case.get("kind")
|
|
6145
|
+
data: dict = {}
|
|
6146
|
+
prompt_blobs: list[str] = []
|
|
6147
|
+
cleanup_paths: list[Path] = []
|
|
6148
|
+
try:
|
|
6149
|
+
if kind == "edit":
|
|
6150
|
+
code, data, _ = doctor_run_json_command(
|
|
6151
|
+
cmd_edit,
|
|
6152
|
+
argparse.Namespace(
|
|
6153
|
+
goal="把杯子背景换成夏季冷泡茶吧台",
|
|
6154
|
+
reference=["product:/tmp/cup.png"],
|
|
6155
|
+
preserve=["cup silhouette"],
|
|
6156
|
+
change=["background only"],
|
|
6157
|
+
text=[],
|
|
6158
|
+
aspect="3:4",
|
|
6159
|
+
asset_type="product",
|
|
6160
|
+
quality="high",
|
|
6161
|
+
style=None,
|
|
6162
|
+
target="raw",
|
|
6163
|
+
out=None,
|
|
6164
|
+
json=True,
|
|
6165
|
+
),
|
|
6166
|
+
)
|
|
6167
|
+
if code != 0:
|
|
6168
|
+
doctor_add(findings, "error", "command.exit", f"edit 返回非零状态:{code}")
|
|
6169
|
+
doctor_collect_quality(findings, "edit", data)
|
|
6170
|
+
prompt_blobs.append(str(data.get("prompt") or ""))
|
|
6171
|
+
elif kind == "brand":
|
|
6172
|
+
code, data, _ = doctor_run_json_command(
|
|
6173
|
+
cmd_brand,
|
|
6174
|
+
argparse.Namespace(
|
|
6175
|
+
name="山川茶事",
|
|
6176
|
+
industry="茶饮",
|
|
6177
|
+
palette="moss green,ivory",
|
|
6178
|
+
avoid=None,
|
|
6179
|
+
values=None,
|
|
6180
|
+
style=None,
|
|
6181
|
+
request="新品冷泡茶品牌主视觉",
|
|
6182
|
+
asset_type="poster",
|
|
6183
|
+
style_preset=None,
|
|
6184
|
+
text=["山川茶事"],
|
|
6185
|
+
strict_text=False,
|
|
6186
|
+
target="raw",
|
|
6187
|
+
json=True,
|
|
6188
|
+
),
|
|
6189
|
+
)
|
|
6190
|
+
if code != 0:
|
|
6191
|
+
doctor_add(findings, "error", "command.exit", f"brand 返回非零状态:{code}")
|
|
6192
|
+
compiled = data.get("compiled") or {}
|
|
6193
|
+
doctor_collect_quality(findings, "brand.compiled", compiled)
|
|
6194
|
+
prompt_blobs.extend([str(data.get("brand_prompt_block") or ""), str(compiled.get("prompt") or "")])
|
|
6195
|
+
labels = set((compiled.get("spec") or {}).get("required_text") or [])
|
|
6196
|
+
for text_item in case.get("expect_required_text") or []:
|
|
6197
|
+
if text_item not in labels:
|
|
6198
|
+
doctor_add(findings, "error", "command.required_text_missing", f"brand 缺少必显文字:{text_item}")
|
|
6199
|
+
elif kind == "character":
|
|
6200
|
+
code, data, _ = doctor_run_json_command(
|
|
6201
|
+
cmd_character,
|
|
6202
|
+
argparse.Namespace(
|
|
6203
|
+
name="Mira",
|
|
6204
|
+
description="short silver hair, amber eyes, explorer coat",
|
|
6205
|
+
scene=["standing beside a glowing archive machine"],
|
|
6206
|
+
style=None,
|
|
6207
|
+
outfit=None,
|
|
6208
|
+
palette=None,
|
|
6209
|
+
style_preset=None,
|
|
6210
|
+
target="raw",
|
|
6211
|
+
json=True,
|
|
6212
|
+
),
|
|
6213
|
+
)
|
|
6214
|
+
if code != 0:
|
|
6215
|
+
doctor_add(findings, "error", "command.exit", f"character 返回非零状态:{code}")
|
|
6216
|
+
reference = data.get("reference_sheet") or {}
|
|
6217
|
+
doctor_collect_quality(findings, "character.reference", reference)
|
|
6218
|
+
prompt_blobs.append(str(reference.get("prompt") or ""))
|
|
6219
|
+
for idx, scene in enumerate(data.get("scene_prompts") or [], start=1):
|
|
6220
|
+
doctor_collect_quality(findings, f"character.scene.{idx}", scene)
|
|
6221
|
+
prompt_blobs.append(str(scene.get("prompt") or ""))
|
|
6222
|
+
elif kind == "data-viz":
|
|
6223
|
+
with NamedTemporaryFile("w", suffix=".csv", encoding="utf-8", delete=False) as tmp:
|
|
6224
|
+
tmp.write("week,conversion\nW1,0.12\nW2,0.18\n")
|
|
6225
|
+
tmp_path = Path(tmp.name)
|
|
6226
|
+
cleanup_paths.append(tmp_path)
|
|
6227
|
+
code, data, _ = doctor_run_json_command(
|
|
6228
|
+
cmd_data_viz,
|
|
6229
|
+
argparse.Namespace(
|
|
6230
|
+
file=str(tmp_path),
|
|
6231
|
+
request="展示转化率趋势",
|
|
6232
|
+
title="转化率趋势",
|
|
6233
|
+
chart_type=None,
|
|
6234
|
+
strict_text=False,
|
|
6235
|
+
json=True,
|
|
6236
|
+
),
|
|
6237
|
+
)
|
|
6238
|
+
if code != 0:
|
|
6239
|
+
doctor_add(findings, "error", "command.exit", f"data-viz 返回非零状态:{code}")
|
|
6240
|
+
doctor_collect_quality(findings, "data-viz", data)
|
|
6241
|
+
prompt_blobs.append(str(data.get("prompt") or ""))
|
|
6242
|
+
chart_spec = data.get("chart_data_spec") or {}
|
|
6243
|
+
if case.get("expect_title") and chart_spec.get("title") != case["expect_title"]:
|
|
6244
|
+
doctor_add(findings, "error", "command.title", f"data-viz 标题错误:{chart_spec.get('title')}")
|
|
6245
|
+
for column in case.get("expect_columns") or []:
|
|
6246
|
+
if column not in (chart_spec.get("columns") or []):
|
|
6247
|
+
doctor_add(findings, "error", "command.column_missing", f"data-viz 缺少列:{column}")
|
|
6248
|
+
elif kind == "rewrite":
|
|
6249
|
+
code, data, _ = doctor_run_json_command(
|
|
6250
|
+
cmd_rewrite,
|
|
6251
|
+
argparse.Namespace(
|
|
6252
|
+
prompt_text=["做一个迪士尼风格的角色海报,好看高级"],
|
|
6253
|
+
file=None,
|
|
6254
|
+
asset_type="poster",
|
|
6255
|
+
style=None,
|
|
6256
|
+
style_preset=None,
|
|
6257
|
+
strict_text=False,
|
|
6258
|
+
text=[],
|
|
6259
|
+
target="raw",
|
|
6260
|
+
json=True,
|
|
6261
|
+
),
|
|
6262
|
+
)
|
|
6263
|
+
if code != 0:
|
|
6264
|
+
doctor_add(findings, "error", "command.exit", f"rewrite 返回非零状态:{code}")
|
|
6265
|
+
doctor_collect_quality(findings, "rewrite", data)
|
|
6266
|
+
prompt_blobs.append(str(data.get("prompt") or ""))
|
|
6267
|
+
safety = (data.get("spec") or {}).get("safety_rewrite") or []
|
|
6268
|
+
if case.get("expect_safety_rewrite") and not safety:
|
|
6269
|
+
doctor_add(findings, "error", "command.safety_rewrite", "rewrite 未触发风险引用改写")
|
|
6270
|
+
else:
|
|
6271
|
+
doctor_add(findings, "error", "command.kind", f"未知命令类型:{kind}")
|
|
6272
|
+
finally:
|
|
6273
|
+
for path in cleanup_paths:
|
|
6274
|
+
try:
|
|
6275
|
+
path.unlink()
|
|
6276
|
+
except OSError:
|
|
6277
|
+
pass
|
|
6278
|
+
|
|
6279
|
+
joined_prompt = "\n".join(prompt_blobs)
|
|
6280
|
+
for phrase in case.get("expect_prompt") or []:
|
|
6281
|
+
if phrase not in joined_prompt:
|
|
6282
|
+
doctor_add(findings, "error", "command.prompt_missing", f"{kind} 输出缺少片段:{phrase}")
|
|
6283
|
+
for phrase in case.get("forbid_prompt") or []:
|
|
6284
|
+
if phrase in joined_prompt:
|
|
6285
|
+
doctor_add(findings, "error", "command.forbidden_prompt", f"{kind} 输出出现不应出现的片段:{phrase}")
|
|
6286
|
+
return {
|
|
6287
|
+
"name": f"command-{case['id']}",
|
|
6288
|
+
"pass": not has_lint_error(findings),
|
|
6289
|
+
"kind": kind,
|
|
6290
|
+
"findings": findings,
|
|
6291
|
+
}
|
|
6292
|
+
|
|
6293
|
+
|
|
5919
6294
|
def cmd_doctor(args: argparse.Namespace) -> int:
|
|
5920
6295
|
root = package_root()
|
|
5921
6296
|
checks = [doctor_check_package(root)]
|
|
@@ -5929,6 +6304,16 @@ def cmd_doctor(args: argparse.Namespace) -> int:
|
|
|
5929
6304
|
checks.append(doctor_check_compose_case(case))
|
|
5930
6305
|
except Exception as exc:
|
|
5931
6306
|
checks.append({"name": f"compose-{case.get('id', 'case')}", "pass": False, "findings": [{"severity": "error", "rule": "compose.exception", "message": str(exc)}]})
|
|
6307
|
+
for case in DOCTOR_WORKFLOW_CASES:
|
|
6308
|
+
try:
|
|
6309
|
+
checks.append(doctor_check_workflow_case(case))
|
|
6310
|
+
except Exception as exc:
|
|
6311
|
+
checks.append({"name": f"workflow-{case.get('id', 'case')}", "pass": False, "findings": [{"severity": "error", "rule": "workflow.exception", "message": str(exc)}]})
|
|
6312
|
+
for case in DOCTOR_COMMAND_CASES:
|
|
6313
|
+
try:
|
|
6314
|
+
checks.append(doctor_check_command_case(case))
|
|
6315
|
+
except Exception as exc:
|
|
6316
|
+
checks.append({"name": f"command-{case.get('id', 'case')}", "pass": False, "findings": [{"severity": "error", "rule": "command.exception", "message": str(exc)}]})
|
|
5932
6317
|
passed = all(item["pass"] for item in checks)
|
|
5933
6318
|
summary = {"pass": passed, "version": COMPILER_VERSION, "checks": len(checks), "failed": len([item for item in checks if not item["pass"]])}
|
|
5934
6319
|
if args.json:
|