@yuhan1124/draw-prompt 0.4.1 → 0.4.3

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
@@ -36,6 +36,7 @@ Prompt 的默认结构是三段:`User visual brief` 原始视觉需求、`Styl
36
36
  ```bash
37
37
  npx @yuhan1124/draw-prompt status
38
38
  npx @yuhan1124/draw-prompt convert "茶饮新品海报,写冷泡系列"
39
+ npx @yuhan1124/draw-prompt install-skill --target codex
39
40
  ```
40
41
 
41
42
  `npx` 包内置同一份 Python CLI 和 references。执行时优先使用 `uv run` 自动处理
@@ -44,11 +45,29 @@ PEP723 依赖;没有 `uv` 时退回 `python3`。`overlay`、`visual-check`、`
44
45
 
45
46
  `visual-regress` / `benchmark` 用到的回归集只保留在开发仓库本地,不随 npm 包发布。
46
47
 
47
- ## 安装(独立 repo + 软链)
48
+ ## 安装 skill
49
+
50
+ 普通用户优先从公网 npm 安装到 agent 的 skills 目录:
51
+
52
+ ```bash
53
+ # 安装到 Codex:~/.codex/skills/draw-prompt
54
+ npx @yuhan1124/draw-prompt install-skill --target codex
55
+
56
+ # 安装到 Claude Code:~/.claude/skills/draw-prompt
57
+ npx @yuhan1124/draw-prompt install-skill --target claude
58
+
59
+ # 同时安装两边;更新已有安装时加 --force
60
+ npx @yuhan1124/draw-prompt install-skill --target both --force
61
+ ```
62
+
63
+ `install-skill` 默认复制 npm 包内的运行文件,避免软链到 npx 缓存导致后续路径失效。它只复制
64
+ `SKILL.md`、CLI、references 和必要元数据;开发仓库里的 `tests/`、`tmp/`、`golden-cases.jsonl`、
65
+ `visual-cases.jsonl` 不会进入 npm 包或安装目录。
66
+
67
+ 开发者本地调试也可以软链 repo:
48
68
 
49
69
  ```bash
50
- # 1) 克隆/进入本 repo(这里假设在 ~/Desktop/draw-prompt)
51
- # 2) 软链到 Claude Code 的 skills 目录,让它被即时发现
70
+ # 克隆/进入本 repo(这里假设在 ~/Desktop/draw-prompt)
52
71
  ln -s ~/Desktop/draw-prompt ~/.claude/skills/draw-prompt
53
72
  ```
54
73
 
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.1
11
+ version: 0.4.3
12
12
  openclaw:
13
13
  anyBins: ["uv", "python3"]
14
14
  ---
@@ -60,6 +60,7 @@ prompt」「给 Codex 出图的指令」「优化这条出图 prompt」等意图
60
60
  还是"想先聊方向"。解析时先锁定用户显式目标和约束,后续所有增强都不能越过这些约束。
61
61
 
62
62
  2. **优先自动转化,并选对入口**。能直接交付时,按场景运行对应命令:
63
+ - 安装到 agent skill 目录:`prompt_cli.py install-skill --target codex|claude [--force]`
63
64
  - 单图:`prompt_cli.py convert "<自然语言画图需求>" [--style-preset premium] [--strict-text] [--out <path>] [--record-pending]`
64
65
  - 长输入整理成多张图:`prompt_cli.py compose "<长输入>" --max-images 6 [--style-preset corporate] [--strict-text]`
65
66
  - 同一输入多风格探索:`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.1",
3
+ "version": "0.4.3",
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": {
@@ -14,6 +14,7 @@ codex exec)。本 CLI 的 `handoff` 子命令负责把 prompt 包装成一段
14
14
  粘贴/转交给 Codex 的现成指令,但不会自己去执行它。
15
15
 
16
16
  CLI 只干确定性的脏活:
17
+ install-skill 从 npx/npm 包安装到 Codex/Claude skills 目录
17
18
  convert 自然语言画图需求 -> Prompt / handoff
18
19
  compose 长输入/文档 -> 多张配套图的视觉计划 + Prompt
19
20
  variants 同一需求 -> 多风格 Prompt 组
@@ -49,6 +50,7 @@ import hashlib
49
50
  import json
50
51
  import os
51
52
  import re
53
+ import shutil
52
54
  import sys
53
55
  from datetime import datetime, timezone
54
56
  from pathlib import Path
@@ -133,7 +135,27 @@ def ensure_home() -> None:
133
135
 
134
136
 
135
137
  SCHEMA_VERSION = 1
136
- COMPILER_VERSION = "0.4.1"
138
+ COMPILER_VERSION = "0.4.3"
139
+
140
+
141
+ PACKAGED_SKILL_FILES = [
142
+ "SKILL.md",
143
+ "README.md",
144
+ "LICENSE",
145
+ "package.json",
146
+ "agents/openai.yaml",
147
+ "bin/draw-prompt.js",
148
+ "scripts/prompt_cli.py",
149
+ "references/codex-handoff.md",
150
+ "references/compile.md",
151
+ "references/conversion-skill-plan.md",
152
+ "references/gallery.md",
153
+ "references/harness.md",
154
+ ]
155
+
156
+
157
+ def package_root() -> Path:
158
+ return Path(__file__).resolve().parent.parent
137
159
 
138
160
 
139
161
  # --------------------------------------------------------------------------- #
@@ -2076,6 +2098,7 @@ DEFAULT_VARIANT_PRESETS = ["corporate", "premium", "minimal", "flat-vector", "ph
2076
2098
 
2077
2099
  ASPECT_SIZE = {
2078
2100
  "3:4": "portrait",
2101
+ "4:5": "portrait",
2079
2102
  "4:3": "landscape",
2080
2103
  "16:9": "landscape",
2081
2104
  "9:16": "portrait",
@@ -2095,7 +2118,7 @@ TEMPLATE_DEFS = {
2095
2118
  "asset_type": "poster",
2096
2119
  "label": "中文促销海报",
2097
2120
  "layout": "large headline zone, hero product zone, price/offer block, quiet footer",
2098
- "keywords": ["促销", "新品", "价格", "优惠", "茶饮", "冷泡", "海报"],
2121
+ "keywords": ["促销", "新品", "价格", "优惠", "茶饮", "冷泡"],
2099
2122
  },
2100
2123
  "poster_brand_kv": {
2101
2124
  "asset_type": "poster",
@@ -2107,7 +2130,7 @@ TEMPLATE_DEFS = {
2107
2130
  "asset_type": "poster",
2108
2131
  "label": "活动海报",
2109
2132
  "layout": "title, date/venue, speaker or theme block, organizer footer",
2110
- "keywords": ["活动", "会议", "展览", "event", "workshop", "讲座"],
2133
+ "keywords": ["活动", "会议", "发布会", "展览", "event", "workshop", "讲座"],
2111
2134
  },
2112
2135
  "poster_info_dense": {
2113
2136
  "asset_type": "poster",
@@ -2421,11 +2444,17 @@ def extract_required_texts(request: str, explicit_texts: list[str]) -> list[str]
2421
2444
  text = text.strip(" \t\n\r,,、。;;::.!?!?")
2422
2445
  text = re.sub(r"^(?:[^::]{0,12}(?:指标卡|卡片|列表|标题|模块|节点|步骤|栏目|分支))[::]", "", text)
2423
2446
  text = re.sub(r"^(?:顶部|底部|中间|左侧|右侧|上方|下方|首页|页面)?(?:主标题|副标题|标题)\s+", "", text)
2447
+ text = re.sub(r"^(?:问候语|核心卡片|主要按钮|主按钮|按钮)\s+", "", text)
2448
+ text = re.sub(r"^(?:是|为|叫)\s+", "", text)
2424
2449
  text = re.sub(r"^(?:一个|一枚|一项)?(?:明显的|醒目的|主要的|primary\s+)?(.{1,16})按钮$", r"\1", text, flags=re.IGNORECASE)
2425
2450
  text = re.sub(r"(?:网格|列表|区域|模块)$", "", text)
2426
2451
  text = re.sub(r"(?:要)?(?:渲染)?(?:清晰|清楚|可读|明显)$", "", text)
2427
2452
  text = re.sub(r"(?:这些)?元素$", "", text)
2428
2453
  text = re.sub(r"(?:[一二三四五六七八九十\d]+个)?步骤$", "", text)
2454
+ if re.search(r"箭头.*关系|关系.*箭头|展示输入输出关系", text):
2455
+ return ""
2456
+ if re.fullmatch(r"[A-Za-z]{1,2}", text) and text.upper() not in {"AI", "UI"}:
2457
+ return ""
2429
2458
  if text in {"顶部导航", "底部导航", "左侧导航", "右侧导航", "导航栏", "顶部栏", "状态栏", "箭头关系", "箭头", "关系"}:
2430
2459
  return ""
2431
2460
  return text.strip(" \t\n\r,,、。;;::.!?!?")
@@ -2458,6 +2487,31 @@ def extract_required_texts(request: str, explicit_texts: list[str]) -> list[str]
2458
2487
  for pat in patterns:
2459
2488
  for match in re.finditer(pat, request):
2460
2489
  add_candidate(match.start(1), match.group(1))
2490
+ labeled_single_patterns = [
2491
+ r"(?:主标题|标题|副标题|主题|时间|地点)\s*(?:写上|写|显示|为|是|叫|[::])\s*([^,,、。;;\n]{2,40})",
2492
+ r"(?:主标题|标题|副标题|主题|时间|地点)\s+([^,,、。;;\n]{2,40})",
2493
+ r"(?:时间|地点)\s+([^,,、。;;\n]{2,40})",
2494
+ r"(?:核心卡片|主要按钮|主按钮|按钮)\s*(?:写上|写|显示|为|是|叫|[::])\s*([^,,。;;\n]{2,30})",
2495
+ r"(?:需要)?包含\s*([A-Za-z][A-Za-z0-9_-]{2,30})\s*字样",
2496
+ r"(?:名为|叫做|名称是|名字叫)\s*([A-Za-z][A-Za-z0-9_-]{2,30})\b",
2497
+ ]
2498
+ for pat in labeled_single_patterns:
2499
+ for match in re.finditer(pat, request, flags=re.IGNORECASE):
2500
+ add_candidate(match.start(1), match.group(1))
2501
+ for match in re.finditer(
2502
+ r"问候语\s*(?:写上|写|显示|为|是|叫|[::])?\s*(.+?)(?=,(?:核心|下方|上方|主要|页面|风格)|[。;;\n]|$)",
2503
+ request,
2504
+ flags=re.IGNORECASE,
2505
+ ):
2506
+ add_candidate(match.start(1), match.group(1))
2507
+ for match in re.finditer(
2508
+ r"(?:需要)?(?:出现|展示|包含|包括)?(?:文案|文字)\s*(?:写上|写|显示|为|是|[::])\s*([^。;;\n]{2,100})",
2509
+ request,
2510
+ flags=re.IGNORECASE,
2511
+ ):
2512
+ value = match.group(1)
2513
+ for part_match in re.finditer(r"[^、;;/|]+", value):
2514
+ add_candidate(match.start(1) + part_match.start(), part_match.group(0))
2461
2515
  title_patterns = [
2462
2516
  r"\btitle\s*[::]\s*([^,,。.;;\n]{1,40})",
2463
2517
  r"(?:Chinese\s+title|title\s+in\s+Chinese)\s*[::]\s*(.+?)(?=\s+(?:Bullet\s+points?|Icon|Column\s+\d+|Bottom\s+area|High\s+contrast)\b|[,,。.;;\n]|$)",
@@ -2475,8 +2529,8 @@ def extract_required_texts(request: str, explicit_texts: list[str]) -> list[str]
2475
2529
  value = match.group(1)
2476
2530
  for part_match in re.finditer(r"[^,,、;;]+", value):
2477
2531
  add_candidate(match.start(1) + part_match.start(), part_match.group(0))
2478
- text_hint = r"(?:写上|写|显示|文案|标题|品牌名|wordmark)"
2479
- for match in re.finditer(rf"{text_hint}[::\s]*(?:写上|写|显示|为|叫)?[::\s]*([^,,、。;;,.]{{2,24}})", request, flags=re.IGNORECASE):
2532
+ text_hint = r"(?:写上|写|显示|品牌名|wordmark|(?<!副)标题)"
2533
+ for match in re.finditer(rf"{text_hint}[::\s]*(?:写上|写|显示|为|是|叫)?[::\s]*([^,,、。;;,.]{{2,30}})", request, flags=re.IGNORECASE):
2480
2534
  add_candidate(match.start(1), match.group(1))
2481
2535
  for match in re.finditer(r"(?:文字|文案)\s*(?:写上|写|显示|为|是|[::])\s*([^,,、。;;,.]{2,24})", request, flags=re.IGNORECASE):
2482
2536
  add_candidate(match.start(1), match.group(1))
@@ -2495,12 +2549,18 @@ def merge_texts(primary: list[str], extra: list[str]) -> list[str]:
2495
2549
  text = item.strip(" \t\n\r,,、。;;::.!?!?")
2496
2550
  text = re.sub(r"^(?:[^::]{0,12}(?:指标卡|卡片|列表|标题|模块|节点|步骤|栏目|分支))[::]", "", text)
2497
2551
  text = re.sub(r"^(?:顶部|底部|中间|左侧|右侧|上方|下方|首页|页面)?(?:主标题|副标题|标题)\s+", "", text)
2552
+ text = re.sub(r"^(?:问候语|核心卡片|主要按钮|主按钮|按钮)\s+", "", text)
2553
+ text = re.sub(r"^(?:是|为|叫)\s+", "", text)
2498
2554
  text = re.sub(r"^(?:一个|一枚|一项)?(?:明显的|醒目的|主要的|primary\s+)?(.{1,16})按钮$", r"\1", text, flags=re.IGNORECASE)
2499
2555
  text = re.sub(r"(?:网格|列表|区域|模块)$", "", text)
2500
2556
  text = re.sub(r"(?:要)?(?:渲染)?(?:清晰|清楚|可读|明显)$", "", text)
2501
2557
  text = re.sub(r"(?:这些)?元素$", "", text)
2502
2558
  text = re.sub(r"(?:[一二三四五六七八九十\d]+个)?步骤$", "", text)
2503
2559
  text = text.strip(" \t\n\r,,、。;;::.!?!?")
2560
+ if re.search(r"箭头.*关系|关系.*箭头|展示输入输出关系", text):
2561
+ continue
2562
+ if re.fullmatch(r"[A-Za-z]{1,2}", text) and text.upper() not in {"AI", "UI"}:
2563
+ continue
2504
2564
  if text in {"顶部导航", "底部导航", "左侧导航", "右侧导航", "导航栏", "顶部栏", "状态栏", "箭头关系", "箭头", "关系"}:
2505
2565
  continue
2506
2566
  key = re.sub(r"\s+", "", text)
@@ -2622,6 +2682,8 @@ def infer_template_id(request: str, asset_type: str, override: str | None = None
2622
2682
  if asset_type == "poster":
2623
2683
  if any(keyword_in_text(kw, lower) for kw in ["小红书", "社媒", "方图", "封面", "cover", "social"]):
2624
2684
  return "poster_social_cover"
2685
+ if any(keyword_in_text(kw, lower) for kw in ["活动", "会议", "发布会", "展览", "event", "workshop", "讲座"]):
2686
+ return "poster_event"
2625
2687
  if any(keyword_in_text(kw, lower) for kw in ["品牌", "主视觉", "kv", "brand key visual"]):
2626
2688
  return "poster_brand_kv"
2627
2689
  if any(keyword_in_text(kw, lower) for kw in ["信息", "清单", "流程", "说明"]):
@@ -3220,7 +3282,7 @@ def lint_prompt(prompt: str, asset_type: str | None, quality: str | None, requir
3220
3282
  lower = compact.lower()
3221
3283
  if len(compact) < 80:
3222
3284
  add("error", "prompt.too_short", "Prompt 太短,缺少可执行的视觉约束。")
3223
- if not re.search(r"\b(3:4|4:3|16:9|9:16|1:1|portrait|landscape|square)\b", lower):
3285
+ if not re.search(r"(?<!\d)(?:3:4|4:5|4:3|16:9|9:16|1:1)(?!\d)|\b(?:portrait|landscape|square)\b", lower):
3224
3286
  add("error", "prompt.missing_aspect", "Prompt 缺少画幅/宽高比/制品类型开场。")
3225
3287
  if not re.search(r"\b(avoid|no |without|不要|避免)\b", lower):
3226
3288
  add("warning", "prompt.missing_negative", "Prompt 缺少针对常见失败模式的否定项。")
@@ -5425,6 +5487,86 @@ def cmd_adapt(args: argparse.Namespace) -> int:
5425
5487
  return 1 if any(has_lint_error(item["lint"]) for item in variants) else 0
5426
5488
 
5427
5489
 
5490
+ def default_skill_parent(target: str) -> Path:
5491
+ if target == "claude":
5492
+ return Path.home() / ".claude" / "skills"
5493
+ return Path.home() / ".codex" / "skills"
5494
+
5495
+
5496
+ def is_draw_prompt_install(path: Path) -> bool:
5497
+ skill = path / "SKILL.md"
5498
+ if not skill.exists():
5499
+ return False
5500
+ try:
5501
+ head = skill.read_text(encoding="utf-8")[:500]
5502
+ except OSError:
5503
+ return False
5504
+ return "name: draw-prompt" in head
5505
+
5506
+
5507
+ def copy_packaged_skill(src_root: Path, dst: Path) -> list[str]:
5508
+ copied = []
5509
+ for rel in PACKAGED_SKILL_FILES:
5510
+ src = src_root / rel
5511
+ if not src.exists():
5512
+ raise FileNotFoundError(f"缺少发布文件:{src}")
5513
+ target = dst / rel
5514
+ target.parent.mkdir(parents=True, exist_ok=True)
5515
+ shutil.copy2(src, target)
5516
+ copied.append(rel)
5517
+ return copied
5518
+
5519
+
5520
+ def install_skill_to_path(dst: Path, mode: str, force: bool) -> dict:
5521
+ src_root = package_root()
5522
+ dst = dst.expanduser()
5523
+ if dst.exists() or dst.is_symlink():
5524
+ if not force:
5525
+ if is_draw_prompt_install(dst):
5526
+ return {
5527
+ "path": str(dst),
5528
+ "status": "exists",
5529
+ "message": "draw-prompt skill 已存在;如需更新请加 --force。",
5530
+ "mode": mode,
5531
+ }
5532
+ raise FileExistsError(f"目标路径已存在且不像 draw-prompt skill:{dst}。如确认覆盖,请加 --force。")
5533
+ if dst.is_symlink() or dst.is_file():
5534
+ dst.unlink()
5535
+ else:
5536
+ shutil.rmtree(dst)
5537
+ dst.parent.mkdir(parents=True, exist_ok=True)
5538
+ if mode == "symlink":
5539
+ dst.symlink_to(src_root, target_is_directory=True)
5540
+ copied = []
5541
+ else:
5542
+ dst.mkdir(parents=True, exist_ok=True)
5543
+ copied = copy_packaged_skill(src_root, dst)
5544
+ return {"path": str(dst), "status": "installed", "mode": mode, "files": copied, "source": str(src_root)}
5545
+
5546
+
5547
+ def cmd_install_skill(args: argparse.Namespace) -> int:
5548
+ targets = ["codex", "claude"] if args.target == "both" else [args.target]
5549
+ if args.path and len(targets) > 1:
5550
+ print("--path 只能和单个 --target 一起使用,不能用于 --target both。", file=sys.stderr)
5551
+ return 2
5552
+ results = []
5553
+ try:
5554
+ for target in targets:
5555
+ dst = Path(args.path).expanduser() if args.path else default_skill_parent(target) / "draw-prompt"
5556
+ results.append({"target": target, **install_skill_to_path(dst, args.mode, args.force)})
5557
+ except Exception as exc:
5558
+ print(f"install-skill 失败:{exc}", file=sys.stderr)
5559
+ return 2
5560
+ if args.json:
5561
+ print(json.dumps({"results": results}, ensure_ascii=False, indent=2))
5562
+ else:
5563
+ for item in results:
5564
+ print(f"{item['target']}: {item['status']} {item['path']} ({item['mode']})")
5565
+ if item.get("message"):
5566
+ print(f" {item['message']}")
5567
+ return 0
5568
+
5569
+
5428
5570
  # --------------------------------------------------------------------------- #
5429
5571
  # status
5430
5572
  # --------------------------------------------------------------------------- #
@@ -5440,6 +5582,10 @@ def cmd_status(args: argparse.Namespace) -> int:
5440
5582
  print(f" 自带范例库 : {own} ({'可用' if own.exists() else '缺失!'})")
5441
5583
  gi = Path.home() / ".claude" / "skills" / "gpt-image" / "references" / "gallery.md"
5442
5584
  print(f" 可选扩展库 : gpt-image ({'可用' if gi.exists() else '未装(不影响)'})")
5585
+ codex_skill = Path.home() / ".codex" / "skills" / "draw-prompt"
5586
+ claude_skill = Path.home() / ".claude" / "skills" / "draw-prompt"
5587
+ print(f" Codex skill: {codex_skill} ({'已安装' if is_draw_prompt_install(codex_skill) else '未安装,可运行 install-skill --target codex'})")
5588
+ print(f" Claude skill: {claude_skill} ({'已安装' if is_draw_prompt_install(claude_skill) else '未安装,可运行 install-skill --target claude'})")
5443
5589
  print(" ── 下游出图通道(本 skill 不主动调用,仅提示可用性)──")
5444
5590
  print(f" codex CLI : {which('codex') or '未找到'}")
5445
5591
  plugin = Path.home() / ".claude" / "plugins" / "cache" / "codex-image-in-cc"
@@ -5482,6 +5628,14 @@ def build_parser() -> argparse.ArgumentParser:
5482
5628
  p = argparse.ArgumentParser(prog="prompt_cli.py", description="draw-prompt:自然语言画图需求 -> 生图 Prompt / handoff(不主动出图)")
5483
5629
  sub = p.add_subparsers(dest="cmd", required=True)
5484
5630
 
5631
+ pis = sub.add_parser("install-skill", help="把当前包安装到 Codex/Claude skills 目录")
5632
+ pis.add_argument("--target", choices=["codex", "claude", "both"], default="codex", help="默认安装到 ~/.codex/skills/draw-prompt")
5633
+ pis.add_argument("--path", help="覆盖安装目标路径;只能和单个 target 一起用")
5634
+ pis.add_argument("--mode", choices=["copy", "symlink"], default="copy", help="默认复制发布文件,避免 npx 缓存路径失效")
5635
+ pis.add_argument("--force", action="store_true", help="覆盖已有 draw-prompt skill")
5636
+ pis.add_argument("--json", action="store_true")
5637
+ pis.set_defaults(func=cmd_install_skill)
5638
+
5485
5639
  pc = sub.add_parser("convert", help="自然语言画图需求 -> 高质量生图 Prompt / handoff")
5486
5640
  pc.add_argument("request_text", nargs="+", help="自然语言画图需求")
5487
5641
  pc.add_argument("--asset-type", choices=sorted(ASSET_ROUTES.keys()), help="覆盖自动识别的资产类型")