@yuhan1124/draw-prompt 0.4.6 → 0.4.8
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 +4 -1
- package/SKILL.md +2 -1
- package/package.json +1 -1
- package/scripts/prompt_cli.py +355 -15
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
|
```
|
|
@@ -68,7 +69,8 @@ npx --yes --registry=https://registry.npmjs.org/ @yuhan1124/draw-prompt@latest i
|
|
|
68
69
|
|
|
69
70
|
`install-skill` 默认复制 npm 包内的运行文件,避免软链到 npx 缓存导致后续路径失效。它只复制
|
|
70
71
|
`SKILL.md`、CLI、references 和必要元数据;开发仓库里的 `tests/`、`tmp/`、`golden-cases.jsonl`、
|
|
71
|
-
`visual-cases.jsonl` 不会进入 npm
|
|
72
|
+
`visual-cases.jsonl` 不会进入 npm 包或安装目录。安装后可直接跑 `doctor`,它会检查
|
|
73
|
+
包文件、版本一致性、核心单图转化和一条真实长输入 `compose` 链路。
|
|
72
74
|
|
|
73
75
|
开发者本地调试也可以软链 repo:
|
|
74
76
|
|
|
@@ -116,6 +118,7 @@ Harness,作为下一次转化的辅助信号。
|
|
|
116
118
|
|
|
117
119
|
```
|
|
118
120
|
convert "自然语言画图需求" [--style-preset premium] [--strict-text] [--out p] # 默认 single-pass:需求 → Prompt / handoff
|
|
121
|
+
doctor # 安装后运行时自检:包文件、版本、核心场景转化
|
|
119
122
|
compose "长输入/文档内容" --max-images 6 # 长输入 → 多图视觉计划
|
|
120
123
|
variants "自然语言画图需求" --style-presets all # 同一输入 → 多风格 Prompt 组
|
|
121
124
|
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.
|
|
11
|
+
version: 0.4.8
|
|
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
package/scripts/prompt_cli.py
CHANGED
|
@@ -136,7 +136,7 @@ def ensure_home() -> None:
|
|
|
136
136
|
|
|
137
137
|
|
|
138
138
|
SCHEMA_VERSION = 1
|
|
139
|
-
COMPILER_VERSION = "0.4.
|
|
139
|
+
COMPILER_VERSION = "0.4.8"
|
|
140
140
|
|
|
141
141
|
|
|
142
142
|
PACKAGED_SKILL_FILES = [
|
|
@@ -154,6 +154,86 @@ 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
|
+
|
|
214
|
+
DOCTOR_COMPOSE_CASES = [
|
|
215
|
+
{
|
|
216
|
+
"id": "long-input-product-plan",
|
|
217
|
+
"request": (
|
|
218
|
+
"请把下面这段产品方案整理成 4 张图用于路演:我们要做一个 AI 生图 Prompt 转化 skill。"
|
|
219
|
+
"目标用户是经常把长文档、产品需求、运营数据、架构说明变成图片的研发和运营同学。"
|
|
220
|
+
"当前痛点是直接把长输入丢给模型容易跑偏,常见问题包括风格不稳定、中文标签遗漏、架构图被画成 UI、产品图漏卖点、活动战报颜色跑偏。"
|
|
221
|
+
"方案包括:场景识别、风格预设、意图保持检查、质量门、真实 Image 2 验证、样本反馈学习。"
|
|
222
|
+
"上线标准是安装后 doctor 通过、常见场景开箱高质量、NPM 公网安装可用。"
|
|
223
|
+
),
|
|
224
|
+
"max_images": 4,
|
|
225
|
+
"style_preset": "corporate",
|
|
226
|
+
"expect_count": 4,
|
|
227
|
+
"expect_no_asset_type": "illustration",
|
|
228
|
+
"expect_required_text_by_index": {
|
|
229
|
+
0: ["目标用户"],
|
|
230
|
+
1: ["当前痛点"],
|
|
231
|
+
2: ["方案", "场景识别", "风格预设", "质量门"],
|
|
232
|
+
3: ["上线标准", "NPM 公网安装可用"],
|
|
233
|
+
},
|
|
234
|
+
}
|
|
235
|
+
]
|
|
236
|
+
|
|
157
237
|
|
|
158
238
|
def package_root() -> Path:
|
|
159
239
|
return Path(__file__).resolve().parent.parent
|
|
@@ -2378,9 +2458,9 @@ def route_asset_type(request: str, override: str | None = None) -> str:
|
|
|
2378
2458
|
lower = request.lower()
|
|
2379
2459
|
explicit_routes = [
|
|
2380
2460
|
("slide", ["powerpoint", "slide", "presentation", "ppt", "幻灯片", "演示文稿", "汇报单页"]),
|
|
2381
|
-
("ui", ["ui", "界面", "app", "dashboard", "仪表盘", "看板", "saas", "后台", "控制台", "网页", "mockup"]),
|
|
2382
2461
|
("diagram", ["架构图", "系统图", "流程架构", "architecture diagram", "system diagram"]),
|
|
2383
2462
|
("infographic", ["信息图", "infographic", "图解", "流程图", "时间线"]),
|
|
2463
|
+
("ui", ["ui", "界面", "app", "dashboard", "仪表盘", "看板", "saas", "后台", "控制台", "网页", "mockup"]),
|
|
2384
2464
|
("poster", ["海报", "poster", "banner", "主视觉", "kv", "封面"]),
|
|
2385
2465
|
("logo", ["logo", "品牌标识", "字标", "visual identity"]),
|
|
2386
2466
|
]
|
|
@@ -2452,7 +2532,7 @@ def extract_required_texts(request: str, explicit_texts: list[str]) -> list[str]
|
|
|
2452
2532
|
text = re.sub(r"(?:要)?(?:渲染)?(?:清晰|清楚|可读|明显)$", "", text)
|
|
2453
2533
|
text = re.sub(r"(?:这些)?元素$", "", text)
|
|
2454
2534
|
text = re.sub(r"(?:[一二三四五六七八九十\d]+个)?步骤$", "", text)
|
|
2455
|
-
if re.search(r"
|
|
2535
|
+
if re.search(r"箭头.*(?:关系|展示|输入|输出)|(?:关系|展示|输入|输出).*箭头|展示输入输出关系", text):
|
|
2456
2536
|
return ""
|
|
2457
2537
|
if re.fullmatch(r"[A-Za-z]{1,2}", text) and text.upper() not in {"AI", "UI"}:
|
|
2458
2538
|
return ""
|
|
@@ -2558,7 +2638,7 @@ def merge_texts(primary: list[str], extra: list[str]) -> list[str]:
|
|
|
2558
2638
|
text = re.sub(r"(?:这些)?元素$", "", text)
|
|
2559
2639
|
text = re.sub(r"(?:[一二三四五六七八九十\d]+个)?步骤$", "", text)
|
|
2560
2640
|
text = text.strip(" \t\n\r,,、。;;::.!?!?")
|
|
2561
|
-
if re.search(r"
|
|
2641
|
+
if re.search(r"箭头.*(?:关系|展示|输入|输出)|(?:关系|展示|输入|输出).*箭头|展示输入输出关系", text):
|
|
2562
2642
|
continue
|
|
2563
2643
|
if re.fullmatch(r"[A-Za-z]{1,2}", text) and text.upper() not in {"AI", "UI"}:
|
|
2564
2644
|
continue
|
|
@@ -2572,29 +2652,39 @@ def merge_texts(primary: list[str], extra: list[str]) -> list[str]:
|
|
|
2572
2652
|
|
|
2573
2653
|
|
|
2574
2654
|
def extract_structural_labels(request: str, asset_type: str) -> list[str]:
|
|
2575
|
-
if asset_type not in {"diagram", "infographic", "slide", "ui", "poster"}:
|
|
2655
|
+
if asset_type not in {"diagram", "infographic", "slide", "ui", "poster", "product"}:
|
|
2576
2656
|
return []
|
|
2577
2657
|
candidates: list[tuple[int, str]] = []
|
|
2578
|
-
list_intro = r"(?:需要)?(
|
|
2658
|
+
list_intro = r"(?:需要)?(?:展示|呈现|列出|包含|包括|含有|分为|覆盖|输出)"
|
|
2579
2659
|
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
2660
|
patterns = [
|
|
2581
|
-
rf"
|
|
2582
|
-
rf"(
|
|
2583
|
-
rf"(
|
|
2661
|
+
rf"(?:结构(?:从左到右|从右到左|从上到下|从下到上)?|链路|流程)\s*[::]\s*([^。\n]{{2,240}})",
|
|
2662
|
+
rf"(?:数据|指标|核心数据|关键指标|卖点|亮点|功能点|特性|活动路径|路径)\s*(?:包括|包含|有|为)?[::\s]*([^。;;\n]{{2,180}})",
|
|
2663
|
+
rf"{list_intro}\s*(?:这些|以下|对应的)?(?:模块|部分|层|栏目|节点|入口|能力|场景|列表|指标卡|卡片|步骤|分支|数据|指标|卖点|亮点|功能点|特性|路径)?[::\s]*([^。;;\n]{{2,180}})",
|
|
2664
|
+
rf"(?:模块|节点|栏目|部分|层|入口|能力|场景|列表|指标卡|卡片|步骤|分支|数据|指标|卖点|亮点|功能点|特性|路径)\s*(?:包括|包含|有|为)[::\s]*([^。;;\n]{{2,180}})",
|
|
2665
|
+
rf"(?:模块|节点|栏目|部分|层|入口|能力|场景|列表|指标卡|卡片|步骤|分支|数据|指标|卖点|亮点|功能点|特性|路径)[^。;;\n::]{{0,16}}[::]([^。;;\n]{{2,180}})",
|
|
2584
2666
|
]
|
|
2585
2667
|
for pattern in patterns:
|
|
2586
2668
|
for match in re.finditer(pattern, request, flags=re.IGNORECASE):
|
|
2587
2669
|
value = re.split(stop_words, match.group(1), maxsplit=1, flags=re.IGNORECASE)[0]
|
|
2670
|
+
value = re.sub(r"[()()]", "、", value)
|
|
2671
|
+
value = re.sub(r"(?:进入|再到|到|输出|->|→)", "、", value)
|
|
2588
2672
|
for part_match in re.finditer(r"[^、,,;;/|]+", value):
|
|
2589
2673
|
part = part_match.group(0).strip(" \t\n\r,,、::")
|
|
2590
2674
|
part = re.sub(r"^(?:[^::]{0,12}(?:指标卡|卡片|列表|标题|模块|节点|步骤|栏目|分支))[::]", "", part)
|
|
2591
|
-
part = re.sub(r"^(?:和|与|及|以及|and)\s*", "", part, flags=re.IGNORECASE).strip()
|
|
2675
|
+
part = re.sub(r"^(?:和|与|及|以及|and|再|则)\s*", "", part, flags=re.IGNORECASE).strip()
|
|
2676
|
+
part = re.sub(r"^(?:底部有|顶部有|左侧有|右侧有|上方有|下方有)\s*", "", part)
|
|
2677
|
+
part = re.sub(r"^(?:包含|包括|含有)\s*", "", part)
|
|
2592
2678
|
part = re.sub(r"\s*(?:和|与|及|以及|and)$", "", part, flags=re.IGNORECASE).strip()
|
|
2593
2679
|
part = re.sub(r"(?:这些)?元素$", "", part)
|
|
2594
2680
|
part = re.sub(r"(?:[一二三四五六七八九十\d]+个)?步骤$", "", part)
|
|
2595
2681
|
part = part.strip(" \t\n\r,,、::")
|
|
2596
2682
|
if not part:
|
|
2597
2683
|
continue
|
|
2684
|
+
if re.search(r"箭头.*(?:关系|展示|输入|输出)|(?:关系|展示|输入|输出).*箭头|展示输入输出关系", part):
|
|
2685
|
+
continue
|
|
2686
|
+
if part in {"顶部", "底部", "左侧", "右侧", "上方", "下方", "中间", "顶部导航", "底部导航", "左侧导航", "右侧导航", "导航栏", "顶部栏", "状态栏", "箭头关系", "箭头", "关系"}:
|
|
2687
|
+
continue
|
|
2598
2688
|
if re.fullmatch(r"\d+(?:\s*:\s*\d+)?", part):
|
|
2599
2689
|
continue
|
|
2600
2690
|
if len(part) > 36:
|
|
@@ -2630,6 +2720,43 @@ def infer_style_anchors(request: str, override: str | None, profile: dict, prese
|
|
|
2630
2720
|
return anchors[:4]
|
|
2631
2721
|
|
|
2632
2722
|
|
|
2723
|
+
def infer_palette_constraints(request: str) -> list[str]:
|
|
2724
|
+
color_terms = [
|
|
2725
|
+
"红", "橙", "黄", "绿", "青", "蓝", "紫", "粉", "黑", "白", "灰", "金", "银", "棕",
|
|
2726
|
+
"深色", "浅色", "渐变", "配色", "主色", "点缀", "背景",
|
|
2727
|
+
"red", "orange", "yellow", "green", "cyan", "blue", "purple", "pink", "black", "white",
|
|
2728
|
+
"gray", "grey", "gold", "silver", "brown", "gradient", "palette", "color", "colour",
|
|
2729
|
+
]
|
|
2730
|
+
constraints: list[str] = []
|
|
2731
|
+
seen: set[str] = set()
|
|
2732
|
+
for sentence in re.split(r"[。;;\n]", request):
|
|
2733
|
+
for fragment in re.split(r"[,,]", sentence):
|
|
2734
|
+
fragment = fragment.strip(" \t\r\n。.;;,,")
|
|
2735
|
+
if not fragment:
|
|
2736
|
+
continue
|
|
2737
|
+
lower = fragment.lower()
|
|
2738
|
+
if not any(term in lower for term in color_terms):
|
|
2739
|
+
continue
|
|
2740
|
+
if len(fragment) > 72:
|
|
2741
|
+
fragment = fragment[:72].rstrip()
|
|
2742
|
+
if "红" in fragment and any(marker in fragment for marker in ["不要太俗", "别太俗", "不能太俗", "不俗", "高级"]):
|
|
2743
|
+
normalized = "use a restrained, mature red campaign accent palette; keep red visibly present without garish saturated red"
|
|
2744
|
+
if normalized not in seen:
|
|
2745
|
+
seen.add(normalized)
|
|
2746
|
+
constraints.append(normalized)
|
|
2747
|
+
if len(constraints) >= 3:
|
|
2748
|
+
return constraints
|
|
2749
|
+
continue
|
|
2750
|
+
key = re.sub(r"\s+", "", fragment)
|
|
2751
|
+
if key in seen:
|
|
2752
|
+
continue
|
|
2753
|
+
seen.add(key)
|
|
2754
|
+
constraints.append(f"preserve requested color direction: {fragment}")
|
|
2755
|
+
if len(constraints) >= 3:
|
|
2756
|
+
return constraints
|
|
2757
|
+
return constraints
|
|
2758
|
+
|
|
2759
|
+
|
|
2633
2760
|
def infer_negative(asset_type: str, texts: list[str], profile: dict, request: str = "", template_id: str = "") -> list[str]:
|
|
2634
2761
|
negative = ["avoid vague generic AI gloss", "avoid clutter", "avoid adding content not requested by the user"]
|
|
2635
2762
|
if texts:
|
|
@@ -3023,7 +3150,7 @@ def build_spec(args: argparse.Namespace) -> dict:
|
|
|
3023
3150
|
"style_anchors": infer_style_anchors(safe_request, getattr(args, "style", None), profile, style_preset),
|
|
3024
3151
|
"materials": split_csv(getattr(args, "materials", None)) or ["tactile, specific visible materials chosen for the subject"],
|
|
3025
3152
|
"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"],
|
|
3153
|
+
"palette": split_csv(getattr(args, "palette", None)) or infer_palette_constraints(visual_request) or ["restrained palette matched to the asset type"],
|
|
3027
3154
|
"negative": negative,
|
|
3028
3155
|
"must_include": infer_must_include(asset_type, template_id, texts, bool(getattr(args, "strict_text", False))),
|
|
3029
3156
|
"must_avoid": negative,
|
|
@@ -3157,7 +3284,7 @@ def render_prompt(spec: dict) -> str:
|
|
|
3157
3284
|
if asset_type == "infographic":
|
|
3158
3285
|
return "\n".join(
|
|
3159
3286
|
[
|
|
3160
|
-
f"Create a {aspect}
|
|
3287
|
+
f"Create a {aspect} information graphic from the user visual brief below.",
|
|
3161
3288
|
brief_block,
|
|
3162
3289
|
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
3290
|
f"Composition support: {spec.get('template_label', 'infographic')}; layout guidance: {layout}.",
|
|
@@ -4783,6 +4910,30 @@ def split_document_sections(text: str, max_images: int) -> list[str]:
|
|
|
4783
4910
|
if buf:
|
|
4784
4911
|
chunks.append(" ".join(buf).strip())
|
|
4785
4912
|
|
|
4913
|
+
if len(chunks) <= 1:
|
|
4914
|
+
semantic_patterns = [
|
|
4915
|
+
r"(目标用户(?:是|:|:)[^。;;\n]+)",
|
|
4916
|
+
r"(用户场景(?:是|:|:)[^。;;\n]+)",
|
|
4917
|
+
r"(当前痛点(?:是|:|:)[^。;;\n]+)",
|
|
4918
|
+
r"(主要问题(?:是|:|:)[^。;;\n]+)",
|
|
4919
|
+
r"(方案(?:包括|是|:|:)[^。;;\n]+)",
|
|
4920
|
+
r"(核心能力(?:包括|是|:|:)[^。;;\n]+)",
|
|
4921
|
+
r"(上线标准(?:是|:|:)[^。;;\n]+)",
|
|
4922
|
+
r"(验收标准(?:是|:|:)[^。;;\n]+)",
|
|
4923
|
+
]
|
|
4924
|
+
semantic_matches: list[tuple[int, str]] = []
|
|
4925
|
+
seen_spans: set[tuple[int, int]] = set()
|
|
4926
|
+
for pattern in semantic_patterns:
|
|
4927
|
+
for match in re.finditer(pattern, text):
|
|
4928
|
+
span = match.span(1)
|
|
4929
|
+
if span in seen_spans:
|
|
4930
|
+
continue
|
|
4931
|
+
seen_spans.add(span)
|
|
4932
|
+
semantic_matches.append((span[0], match.group(1).strip()))
|
|
4933
|
+
semantic_chunks = [chunk for _, chunk in sorted(semantic_matches, key=lambda item: item[0])]
|
|
4934
|
+
if len(semantic_chunks) >= 2:
|
|
4935
|
+
chunks = semantic_chunks
|
|
4936
|
+
|
|
4786
4937
|
if len(chunks) <= 1:
|
|
4787
4938
|
sentences = [s.strip() for s in re.split(r"(?<=[。!?!?;;])\s*", text) if s.strip()]
|
|
4788
4939
|
chunks = []
|
|
@@ -4804,6 +4955,8 @@ def split_document_sections(text: str, max_images: int) -> list[str]:
|
|
|
4804
4955
|
|
|
4805
4956
|
def choose_compose_asset(chunk: str, index: int) -> str:
|
|
4806
4957
|
lower = chunk.lower()
|
|
4958
|
+
if re.search(r"^(?:目标用户|用户场景|当前痛点|主要问题|上线标准|验收标准)", chunk):
|
|
4959
|
+
return "infographic"
|
|
4807
4960
|
if any(k in lower for k in ["架构", "系统", "模块", "链路", "rag", "llm", "retriever", "pipeline"]):
|
|
4808
4961
|
return "diagram"
|
|
4809
4962
|
if any(k in lower for k in ["数据", "指标", "报表", "趋势", "占比", "转化率", "漏斗", "图表"]):
|
|
@@ -4818,7 +4971,9 @@ def choose_compose_asset(chunk: str, index: int) -> str:
|
|
|
4818
4971
|
return "character"
|
|
4819
4972
|
if index == 0 and any(k in lower for k in ["标题", "主题", "发布", "活动", "封面", "总结"]):
|
|
4820
4973
|
return "poster"
|
|
4821
|
-
|
|
4974
|
+
if any(k in lower for k in ["插画", "场景插图", "故事", "氛围图", "场景图", "scene illustration"]):
|
|
4975
|
+
return "illustration"
|
|
4976
|
+
return "infographic"
|
|
4822
4977
|
|
|
4823
4978
|
|
|
4824
4979
|
def infer_compose_style(text: str) -> str:
|
|
@@ -4842,6 +4997,13 @@ def extract_visual_labels(chunk: str, asset_type: str, limit: int = 5) -> list[s
|
|
|
4842
4997
|
|
|
4843
4998
|
for match in re.findall(r'"([^"\n]{1,28})"|“([^”\n]{1,28})”|「([^」\n]{1,28})」', chunk):
|
|
4844
4999
|
add(next((m for m in match if m), ""))
|
|
5000
|
+
for match in re.finditer(r"(目标用户|用户场景|当前痛点|主要问题|方案|核心能力|上线标准|验收标准)\s*(?:是|包括|包含|:|:)\s*([^。;;\n]{2,140})", chunk):
|
|
5001
|
+
add(match.group(1))
|
|
5002
|
+
value = match.group(2)
|
|
5003
|
+
for part in re.split(r"[、,,/|]", value):
|
|
5004
|
+
part = re.sub(r"^(?:和|与|及|以及|and)\s*", "", part.strip(), flags=re.IGNORECASE)
|
|
5005
|
+
if 1 < len(part) <= 18:
|
|
5006
|
+
add(part)
|
|
4845
5007
|
for match in re.findall(r"\b[A-Z][A-Za-z0-9_-]{1,20}\b", chunk):
|
|
4846
5008
|
add(match)
|
|
4847
5009
|
for match in re.findall(r"(?:标题|主题|模块|步骤|节点|页面)[::\s]*([^,。;;\n]{2,24})", chunk):
|
|
@@ -4860,7 +5022,7 @@ def compose_purpose(asset_type: str, index: int) -> str:
|
|
|
4860
5022
|
"ui": "界面概念图",
|
|
4861
5023
|
"product": "产品视觉图",
|
|
4862
5024
|
"character": "角色设定图",
|
|
4863
|
-
"illustration": "
|
|
5025
|
+
"illustration": "场景插画",
|
|
4864
5026
|
}
|
|
4865
5027
|
return f"{index}. {purpose_map.get(asset_type, '配图')}"
|
|
4866
5028
|
|
|
@@ -5607,6 +5769,180 @@ def npm_registry_status() -> str:
|
|
|
5607
5769
|
return f"{registry}(建议 npx 加 --registry=https://registry.npmjs.org/)"
|
|
5608
5770
|
|
|
5609
5771
|
|
|
5772
|
+
def read_skill_version(path: Path) -> str:
|
|
5773
|
+
if not path.exists():
|
|
5774
|
+
return ""
|
|
5775
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
5776
|
+
match = re.match(r"\s*version:\s*([^\s]+)", line)
|
|
5777
|
+
if match:
|
|
5778
|
+
return match.group(1)
|
|
5779
|
+
return ""
|
|
5780
|
+
|
|
5781
|
+
|
|
5782
|
+
def doctor_add(findings: list[dict], severity: str, rule: str, message: str) -> None:
|
|
5783
|
+
findings.append({"severity": severity, "rule": rule, "message": message})
|
|
5784
|
+
|
|
5785
|
+
|
|
5786
|
+
def doctor_check_package(root: Path) -> dict:
|
|
5787
|
+
findings: list[dict] = []
|
|
5788
|
+
package_path = root / "package.json"
|
|
5789
|
+
package_version = ""
|
|
5790
|
+
if package_path.exists():
|
|
5791
|
+
try:
|
|
5792
|
+
package_version = str(json.loads(package_path.read_text(encoding="utf-8")).get("version") or "")
|
|
5793
|
+
except Exception as exc:
|
|
5794
|
+
doctor_add(findings, "error", "package.invalid_json", f"package.json 读取失败:{exc}")
|
|
5795
|
+
else:
|
|
5796
|
+
doctor_add(findings, "error", "package.missing", "缺少 package.json")
|
|
5797
|
+
skill_version = read_skill_version(root / "SKILL.md")
|
|
5798
|
+
if not skill_version:
|
|
5799
|
+
doctor_add(findings, "error", "skill.version_missing", "SKILL.md 缺少 metadata.version")
|
|
5800
|
+
if package_version and skill_version and package_version != skill_version:
|
|
5801
|
+
doctor_add(findings, "error", "version.package_skill_mismatch", f"package.json={package_version} SKILL.md={skill_version}")
|
|
5802
|
+
if package_version and package_version != COMPILER_VERSION:
|
|
5803
|
+
doctor_add(findings, "error", "version.compiler_mismatch", f"package.json={package_version} compiler={COMPILER_VERSION}")
|
|
5804
|
+
missing = [rel for rel in PACKAGED_SKILL_FILES if not (root / rel).exists()]
|
|
5805
|
+
for rel in missing:
|
|
5806
|
+
doctor_add(findings, "error", "package.file_missing", f"缺少运行文件:{rel}")
|
|
5807
|
+
style_count = len(available_style_presets(include_auto=False))
|
|
5808
|
+
if style_count < 400:
|
|
5809
|
+
doctor_add(findings, "error", "styles.too_few", f"内置风格数量过少:{style_count}")
|
|
5810
|
+
return {
|
|
5811
|
+
"name": "package",
|
|
5812
|
+
"pass": not has_lint_error(findings),
|
|
5813
|
+
"version": package_version or COMPILER_VERSION,
|
|
5814
|
+
"style_count": style_count,
|
|
5815
|
+
"missing_files": missing,
|
|
5816
|
+
"findings": findings,
|
|
5817
|
+
}
|
|
5818
|
+
|
|
5819
|
+
|
|
5820
|
+
def doctor_check_case(case: dict) -> dict:
|
|
5821
|
+
findings: list[dict] = []
|
|
5822
|
+
result = convert_for_benchmark(case)
|
|
5823
|
+
spec = result["spec"]
|
|
5824
|
+
prompt = result["prompt"]
|
|
5825
|
+
for item in result["lint"]:
|
|
5826
|
+
findings.append({"severity": item["severity"], "rule": f"lint.{item['rule']}", "message": item["message"]})
|
|
5827
|
+
for item in result["intent_check"]:
|
|
5828
|
+
findings.append({"severity": item["severity"], "rule": f"intent.{item['rule']}", "message": item["message"]})
|
|
5829
|
+
if case.get("expect_asset_type") and spec.get("asset_type") != case["expect_asset_type"]:
|
|
5830
|
+
doctor_add(findings, "error", "case.asset_type", f"期望 asset_type={case['expect_asset_type']},实际={spec.get('asset_type')}")
|
|
5831
|
+
if case.get("expect_template_id") and spec.get("template_id") != case["expect_template_id"]:
|
|
5832
|
+
doctor_add(findings, "error", "case.template_id", f"期望 template_id={case['expect_template_id']},实际={spec.get('template_id')}")
|
|
5833
|
+
if case.get("expect_aspect") and spec.get("aspect") != case["expect_aspect"]:
|
|
5834
|
+
doctor_add(findings, "error", "case.aspect", f"期望 aspect={case['expect_aspect']},实际={spec.get('aspect')}")
|
|
5835
|
+
labels = set(spec.get("required_text") or [])
|
|
5836
|
+
for text in case.get("expect_required_text") or []:
|
|
5837
|
+
if text not in labels:
|
|
5838
|
+
doctor_add(findings, "error", "case.required_text_missing", f"缺少必显文字:{text}")
|
|
5839
|
+
elif f'"{text}"' not in prompt:
|
|
5840
|
+
doctor_add(findings, "error", "case.required_text_not_quoted", f"Prompt 未逐字引用:{text}")
|
|
5841
|
+
prompt_lower = prompt.lower()
|
|
5842
|
+
for phrase in case.get("forbid_prompt") or []:
|
|
5843
|
+
if phrase.lower() in prompt_lower:
|
|
5844
|
+
doctor_add(findings, "error", "case.forbidden_prompt", f"Prompt 出现不应出现的片段:{phrase}")
|
|
5845
|
+
return {
|
|
5846
|
+
"name": case["id"],
|
|
5847
|
+
"pass": not has_lint_error(findings),
|
|
5848
|
+
"asset_type": spec.get("asset_type"),
|
|
5849
|
+
"template_id": spec.get("template_id"),
|
|
5850
|
+
"aspect": spec.get("aspect"),
|
|
5851
|
+
"prompt_digest": result["prompt_digest"],
|
|
5852
|
+
"required_text": spec.get("required_text") or [],
|
|
5853
|
+
"findings": findings,
|
|
5854
|
+
}
|
|
5855
|
+
|
|
5856
|
+
|
|
5857
|
+
def doctor_check_compose_case(case: dict) -> dict:
|
|
5858
|
+
findings: list[dict] = []
|
|
5859
|
+
text = str(case.get("request") or "")
|
|
5860
|
+
max_images = int(case.get("max_images") or 4)
|
|
5861
|
+
chunks = split_document_sections(text, max_images)
|
|
5862
|
+
shared_style = infer_compose_style(text)
|
|
5863
|
+
visual_plan: list[dict] = []
|
|
5864
|
+
for idx, chunk in enumerate(chunks, start=1):
|
|
5865
|
+
asset_type = choose_compose_asset(chunk, idx - 1)
|
|
5866
|
+
labels = extract_visual_labels(chunk, asset_type)
|
|
5867
|
+
purpose = compose_purpose(asset_type, idx)
|
|
5868
|
+
compiled = compile_visual_case(
|
|
5869
|
+
{
|
|
5870
|
+
"id": f"compose-{idx:02d}",
|
|
5871
|
+
"request": f"{purpose}。根据这段内容生成对应画面:{chunk}",
|
|
5872
|
+
"asset_type": asset_type,
|
|
5873
|
+
"style": shared_style,
|
|
5874
|
+
"style_preset": case.get("style_preset"),
|
|
5875
|
+
"text": labels if asset_type in {"diagram", "infographic", "ui"} else [],
|
|
5876
|
+
"target": "raw",
|
|
5877
|
+
"tags": "compose,long-input",
|
|
5878
|
+
},
|
|
5879
|
+
target="raw",
|
|
5880
|
+
include_handoff=False,
|
|
5881
|
+
)
|
|
5882
|
+
visual_plan.append({"chunk": chunk, "compiled": compiled})
|
|
5883
|
+
for item in compiled["lint"]:
|
|
5884
|
+
findings.append({"severity": item["severity"], "rule": f"compose.{idx}.lint.{item['rule']}", "message": item["message"]})
|
|
5885
|
+
for item in compiled["intent_check"]:
|
|
5886
|
+
findings.append({"severity": item["severity"], "rule": f"compose.{idx}.intent.{item['rule']}", "message": item["message"]})
|
|
5887
|
+
|
|
5888
|
+
if case.get("expect_count") and len(visual_plan) != int(case["expect_count"]):
|
|
5889
|
+
doctor_add(findings, "error", "compose.count", f"期望 {case['expect_count']} 张图,实际 {len(visual_plan)}")
|
|
5890
|
+
forbidden_asset = case.get("expect_no_asset_type")
|
|
5891
|
+
if forbidden_asset:
|
|
5892
|
+
for idx, item in enumerate(visual_plan, start=1):
|
|
5893
|
+
asset_type = item["compiled"]["spec"].get("asset_type")
|
|
5894
|
+
if asset_type == forbidden_asset:
|
|
5895
|
+
doctor_add(findings, "error", "compose.asset_type", f"第 {idx} 张图不应路由为 {forbidden_asset}")
|
|
5896
|
+
for raw_index, expected_texts in (case.get("expect_required_text_by_index") or {}).items():
|
|
5897
|
+
index = int(raw_index)
|
|
5898
|
+
if index >= len(visual_plan):
|
|
5899
|
+
doctor_add(findings, "error", "compose.index_missing", f"缺少第 {index + 1} 张图")
|
|
5900
|
+
continue
|
|
5901
|
+
labels = set(visual_plan[index]["compiled"]["spec"].get("required_text") or [])
|
|
5902
|
+
prompt = visual_plan[index]["compiled"]["prompt"]
|
|
5903
|
+
for text_item in expected_texts:
|
|
5904
|
+
if text_item not in labels:
|
|
5905
|
+
doctor_add(findings, "error", "compose.required_text_missing", f"第 {index + 1} 张图缺少必显文字:{text_item}")
|
|
5906
|
+
elif f'"{text_item}"' not in prompt:
|
|
5907
|
+
doctor_add(findings, "error", "compose.required_text_not_quoted", f"第 {index + 1} 张图 Prompt 未逐字引用:{text_item}")
|
|
5908
|
+
return {
|
|
5909
|
+
"name": f"compose-{case['id']}",
|
|
5910
|
+
"pass": not has_lint_error(findings),
|
|
5911
|
+
"count": len(visual_plan),
|
|
5912
|
+
"asset_types": [item["compiled"]["spec"].get("asset_type") for item in visual_plan],
|
|
5913
|
+
"prompt_digests": [item["compiled"]["prompt_digest"] for item in visual_plan],
|
|
5914
|
+
"required_text": [item["compiled"]["spec"].get("required_text") or [] for item in visual_plan],
|
|
5915
|
+
"findings": findings,
|
|
5916
|
+
}
|
|
5917
|
+
|
|
5918
|
+
|
|
5919
|
+
def cmd_doctor(args: argparse.Namespace) -> int:
|
|
5920
|
+
root = package_root()
|
|
5921
|
+
checks = [doctor_check_package(root)]
|
|
5922
|
+
for case in DOCTOR_CASES:
|
|
5923
|
+
try:
|
|
5924
|
+
checks.append(doctor_check_case(case))
|
|
5925
|
+
except Exception as exc:
|
|
5926
|
+
checks.append({"name": case.get("id", "case"), "pass": False, "findings": [{"severity": "error", "rule": "case.exception", "message": str(exc)}]})
|
|
5927
|
+
for case in DOCTOR_COMPOSE_CASES:
|
|
5928
|
+
try:
|
|
5929
|
+
checks.append(doctor_check_compose_case(case))
|
|
5930
|
+
except Exception as exc:
|
|
5931
|
+
checks.append({"name": f"compose-{case.get('id', 'case')}", "pass": False, "findings": [{"severity": "error", "rule": "compose.exception", "message": str(exc)}]})
|
|
5932
|
+
passed = all(item["pass"] for item in checks)
|
|
5933
|
+
summary = {"pass": passed, "version": COMPILER_VERSION, "checks": len(checks), "failed": len([item for item in checks if not item["pass"]])}
|
|
5934
|
+
if args.json:
|
|
5935
|
+
print(json.dumps({"summary": summary, "checks": checks}, ensure_ascii=False, indent=2))
|
|
5936
|
+
else:
|
|
5937
|
+
print(f"doctor: version={COMPILER_VERSION} checks={summary['checks']} pass={passed}")
|
|
5938
|
+
for item in checks:
|
|
5939
|
+
status = "PASS" if item["pass"] else "FAIL"
|
|
5940
|
+
print(f"- {item['name']}: {status}")
|
|
5941
|
+
for finding in item.get("findings", []):
|
|
5942
|
+
print(f" [{finding['severity']}] {finding['rule']}: {finding['message']}")
|
|
5943
|
+
return 0 if passed else 1
|
|
5944
|
+
|
|
5945
|
+
|
|
5610
5946
|
def cmd_status(args: argparse.Namespace) -> int:
|
|
5611
5947
|
print("draw-prompt 环境检查")
|
|
5612
5948
|
print(f" 数据目录 : {data_home()} ({'存在' if data_home().exists() else '未创建'})")
|
|
@@ -5628,7 +5964,7 @@ def cmd_status(args: argparse.Namespace) -> int:
|
|
|
5628
5964
|
print(f" codex CLI : {which('codex') or '未找到'}")
|
|
5629
5965
|
plugin = Path.home() / ".claude" / "plugins" / "cache" / "codex-image-in-cc"
|
|
5630
5966
|
print(f" codex-image: {'已安装' if plugin.exists() else '未安装(可 /codex-image:generate 出图)'}")
|
|
5631
|
-
print(" 核心转化命令: convert / compose / variants / series / edit / brand / character / data-viz / rewrite / adapt")
|
|
5967
|
+
print(" 核心转化命令: doctor / convert / compose / variants / series / edit / brand / character / data-viz / rewrite / adapt")
|
|
5632
5968
|
print(" 稳定性命令 : overlay / visual-check / edit-check / visual-regress / lint / intent-check / benchmark / revise / styles")
|
|
5633
5969
|
return 0
|
|
5634
5970
|
|
|
@@ -5674,6 +6010,10 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
5674
6010
|
pis.add_argument("--json", action="store_true")
|
|
5675
6011
|
pis.set_defaults(func=cmd_install_skill)
|
|
5676
6012
|
|
|
6013
|
+
pdoc = sub.add_parser("doctor", help="安装后运行时自检:包文件、版本和核心场景转化")
|
|
6014
|
+
pdoc.add_argument("--json", action="store_true")
|
|
6015
|
+
pdoc.set_defaults(func=cmd_doctor)
|
|
6016
|
+
|
|
5677
6017
|
pc = sub.add_parser("convert", help="自然语言画图需求 -> 高质量生图 Prompt / handoff")
|
|
5678
6018
|
pc.add_argument("request_text", nargs="+", help="自然语言画图需求")
|
|
5679
6019
|
pc.add_argument("--asset-type", choices=sorted(ASSET_ROUTES.keys()), help="覆盖自动识别的资产类型")
|