yuanflow-cli 0.1.28 → 0.1.29

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "yuanflow-cli",
3
- "version": "0.1.28",
4
- "description": "YuanFlow 自媒体 API CLI 与 Skill 安装器。",
3
+ "version": "0.1.29",
4
+ "description": "YuanFlow 自媒体 API CLI 与 Skill 安装器。",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "private": false,
@@ -71,5 +71,3 @@
71
71
  "douyin"
72
72
  ]
73
73
  }
74
-
75
-
@@ -48,6 +48,33 @@ Agent 在排版前应做结构化增强,但不能改变事实:
48
48
 
49
49
  当前 Skill 内置 `scripts/wechat_format.py`,用于把 Markdown 转成微信可粘贴的内联样式 HTML。
50
50
 
51
+ ### 主题选择强制闸门
52
+
53
+ 当用户要求进入排版、预览、导出 HTML 或全流程时,Agent 必须先进入 `theme_select` 主题选择态。
54
+
55
+ 除非用户在本轮已明确指定某个主题 ID,否则不得直接执行 `wechat_format.py --theme ... --output ...`。正确顺序是:
56
+
57
+ 1. 文章正文确认或生成后,先写入 Markdown 源文件。
58
+ 2. 执行主题画廊命令,基于正文生成 20 个核心主题预览:
59
+
60
+ ```bash
61
+ python scripts/wechat_format.py --input article.md --theme-gallery --gallery-limit 20 --json
62
+ ```
63
+
64
+ 3. 使用 `content_workbench_update` 写入右侧 `内容创作` 工作台:
65
+ - `status`: `collecting`
66
+ - `activeStep`: `theme_select`
67
+ - `steps`: 必须包含 `主题选择`
68
+ - `views`: 至少包含脚本返回的 `option_grid` 主题卡片;可以同时附带 `editor_panel` 显示 Markdown 摘要。
69
+ 4. 明确告诉用户:请在右侧工作台选择一个主题,然后发送输入框上方出现的待执行任务。
70
+ 5. 只有拿到用户选择的主题后,才执行:
71
+
72
+ ```bash
73
+ python scripts/wechat_format.py --input article.md --theme <selected_theme> --output dist/wechat.html --preview dist/preview.html --json
74
+ ```
75
+
76
+ 主题选择不是装饰步骤,是排版导出前的执行闸门。用户说“完整执行”也不能跳过,除非用户同时明确指定主题。
77
+
51
78
  执行方式示例:
52
79
 
53
80
  ```bash
@@ -60,6 +87,9 @@ python scripts/wechat_format.py --input article.md --theme newspaper --output di
60
87
  - `--theme`:主题名,默认 `wechat-native`。主题来自 `themes/*.json`。
61
88
  - `--output`:输出 HTML 文件路径。
62
89
  - `--json`:同时输出结构化执行结果,便于 Agent 写入右侧工作台。
90
+ - `--list-themes`:输出所有主题元数据。
91
+ - `--theme-gallery`:基于文章生成可写入工作台 `option_grid` 的主题画廊 JSON。
92
+ - `--gallery-limit`:主题画廊数量,默认 20。
63
93
 
64
94
  ## 工作台展示
65
95
 
@@ -75,7 +105,7 @@ python scripts/wechat_format.py --input article.md --theme newspaper --output di
75
105
  典型展示:
76
106
 
77
107
  1. `stepper`:需求确认、内容创作、主题选择、预览、导出、草稿箱。
78
- 2. `option_grid`:主题卡片和分类筛选结果。
108
+ 2. `option_grid`:主题卡片和文章预览结果。主题卡片必须带 `intent: select_wechat_theme`、`payload.theme` 和 `instruction`,让用户点击后形成待执行任务。
79
109
  3. `preview_frame`:手机样式 HTML 预览。
80
110
  4. `editor_panel`:Markdown 或 HTML 源码。
81
111
  5. `action_bar`:导出 HTML、推送草稿箱、继续改写等动作。
@@ -10,6 +10,28 @@ from typing import Any
10
10
 
11
11
  ROOT = Path(__file__).resolve().parents[1]
12
12
  THEMES_DIR = ROOT / "themes"
13
+ CORE_THEME_IDS = [
14
+ "wechat-native",
15
+ "newspaper",
16
+ "magazine",
17
+ "sspai",
18
+ "github",
19
+ "chinese",
20
+ "ink",
21
+ "bauhaus",
22
+ "bytedance",
23
+ "sports",
24
+ "midnight",
25
+ "coffee-house",
26
+ "terracotta",
27
+ "mint-fresh",
28
+ "lavender-dream",
29
+ "fresh-card",
30
+ "ocean-card",
31
+ "warm-card",
32
+ "focus-gold",
33
+ "sunset-amber",
34
+ ]
13
35
 
14
36
 
15
37
  def _style(style_map: dict[str, Any]) -> str:
@@ -30,6 +52,94 @@ def _load_theme(theme_name: str) -> dict[str, Any]:
30
52
  return json.loads(path.read_text(encoding="utf-8"))
31
53
 
32
54
 
55
+ def _theme_id_from_path(path: Path) -> str:
56
+ return path.stem
57
+
58
+
59
+ def _load_theme_by_id(theme_id: str) -> dict[str, Any]:
60
+ path = THEMES_DIR / f"{theme_id}.json"
61
+ if not path.exists():
62
+ return {"name": theme_id, "description": "", "styles": {}, "colors": {}}
63
+ return json.loads(path.read_text(encoding="utf-8"))
64
+
65
+
66
+ def list_themes(*, core_only: bool = False, limit: int | None = None) -> list[dict[str, Any]]:
67
+ if core_only:
68
+ theme_ids = [theme_id for theme_id in CORE_THEME_IDS if (THEMES_DIR / f"{theme_id}.json").exists()]
69
+ else:
70
+ theme_ids = sorted(_theme_id_from_path(path) for path in THEMES_DIR.glob("*.json"))
71
+ if limit and limit > 0:
72
+ theme_ids = theme_ids[:limit]
73
+
74
+ items: list[dict[str, Any]] = []
75
+ for theme_id in theme_ids:
76
+ theme = _load_theme_by_id(theme_id)
77
+ colors = theme.get("colors") if isinstance(theme.get("colors"), dict) else {}
78
+ items.append(
79
+ {
80
+ "id": theme_id,
81
+ "name": theme.get("name") or theme_id,
82
+ "description": theme.get("description") or "",
83
+ "accent": colors.get("accent") or colors.get("primary") or "#1d4ed8",
84
+ "background": colors.get("background") or "#ffffff",
85
+ }
86
+ )
87
+ return items
88
+
89
+
90
+ def _gallery_markdown_sample(markdown: str) -> str:
91
+ content = markdown.strip()
92
+ if len(content) <= 1200:
93
+ return content
94
+ return f"{content[:1200].rstrip()}\n\n> 预览仅截取正文开头,完整排版会使用全文。"
95
+
96
+
97
+ def build_theme_gallery(markdown: str, title: str, *, limit: int = 20) -> dict[str, Any]:
98
+ sample = _gallery_markdown_sample(markdown)
99
+ themes = []
100
+ for theme_meta in list_themes(core_only=True, limit=limit):
101
+ theme_id = theme_meta["id"]
102
+ theme = _load_theme_by_id(theme_id)
103
+ preview_body = markdown_to_wechat_html(sample, theme)
104
+ preview_html = build_preview(preview_body, f"{theme_meta['name']} · 主题预览")
105
+ themes.append(
106
+ {
107
+ "id": theme_id,
108
+ "label": theme_meta["name"],
109
+ "title": theme_meta["name"],
110
+ "summary": theme_meta["description"],
111
+ "description": theme_meta["description"],
112
+ "accent": theme_meta["accent"],
113
+ "background": theme_meta["background"],
114
+ "previewHtml": preview_html,
115
+ "intent": "select_wechat_theme",
116
+ "payload": {
117
+ "theme": theme_id,
118
+ "themeName": theme_meta["name"],
119
+ "title": title,
120
+ },
121
+ "instruction": (
122
+ f"选择公众号主题「{theme_meta['name']}」(theme={theme_id}),"
123
+ "继续执行排版并导出 HTML。"
124
+ ),
125
+ }
126
+ )
127
+ return {
128
+ "ok": True,
129
+ "title": title,
130
+ "themes": themes,
131
+ "views": [
132
+ {
133
+ "id": "wechat-theme-gallery",
134
+ "type": "option_grid",
135
+ "title": "选择公众号排版主题",
136
+ "summary": "点击一个主题卡片后,发送输入框上方的待执行任务,Agent 才能继续排版导出。",
137
+ "items": themes,
138
+ }
139
+ ],
140
+ }
141
+
142
+
33
143
  def _paragraph(text: str, styles: dict[str, Any]) -> str:
34
144
  return f'<p style="{_style(styles.get("p", {}))}">{html.escape(text)}</p>'
35
145
 
@@ -206,17 +316,43 @@ def build_preview(html_body: str, title: str) -> str:
206
316
 
207
317
  def main() -> int:
208
318
  parser = argparse.ArgumentParser(description="YuanFlow 微信公众号 Markdown 排版工具")
209
- parser.add_argument("--input", required=True, help="Markdown 输入文件")
319
+ parser.add_argument("--input", default="", help="Markdown 输入文件")
210
320
  parser.add_argument("--theme", default="wechat-native", help="主题名称")
211
- parser.add_argument("--output", required=True, help="HTML 输出文件")
321
+ parser.add_argument("--output", default="", help="HTML 输出文件")
212
322
  parser.add_argument("--title", default="公众号生成与发布", help="预览标题")
213
323
  parser.add_argument("--preview", default="", help="可选预览 HTML 输出文件")
324
+ parser.add_argument("--list-themes", action="store_true", help="输出可用主题列表")
325
+ parser.add_argument("--theme-gallery", action="store_true", help="基于文章生成主题选择画廊 JSON")
326
+ parser.add_argument("--gallery-limit", type=int, default=20, help="主题画廊数量,默认 20")
214
327
  parser.add_argument("--json", action="store_true", help="输出 JSON 执行结果")
215
328
  args = parser.parse_args()
216
329
 
330
+ if args.list_themes:
331
+ payload = {"ok": True, "themes": list_themes()}
332
+ print(json.dumps(payload, ensure_ascii=False))
333
+ return 0
334
+
335
+ if not args.input:
336
+ raise SystemExit("--input is required unless --list-themes is used")
337
+
217
338
  input_path = Path(args.input).resolve()
218
- output_path = Path(args.output).resolve()
219
339
  markdown = input_path.read_text(encoding="utf-8")
340
+
341
+ if args.theme_gallery:
342
+ payload = build_theme_gallery(markdown, args.title, limit=args.gallery_limit)
343
+ if args.output:
344
+ output_path = Path(args.output).resolve()
345
+ output_path.parent.mkdir(parents=True, exist_ok=True)
346
+ output_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
347
+ payload["output"] = str(output_path)
348
+ if args.json or not args.output:
349
+ print(json.dumps(payload, ensure_ascii=False))
350
+ return 0
351
+
352
+ if not args.output:
353
+ raise SystemExit("--output is required for HTML export")
354
+
355
+ output_path = Path(args.output).resolve()
220
356
  theme = _load_theme(args.theme)
221
357
  html_body = markdown_to_wechat_html(markdown, theme)
222
358
  output_path.parent.mkdir(parents=True, exist_ok=True)