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 +2 -4
- package/skills/yuanflow-skill//345/205/254/344/274/227/345/217/267/347/224/237/346/210/220/344/270/216/345/217/221/345/270/203/SKILL.md +31 -1
- package/skills/yuanflow-skill//345/205/254/344/274/227/345/217/267/347/224/237/346/210/220/344/270/216/345/217/221/345/270/203/scripts/wechat_format.py +139 -3
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yuanflow-cli",
|
|
3
|
-
"version": "0.1.
|
|
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",
|
|
319
|
+
parser.add_argument("--input", default="", help="Markdown 输入文件")
|
|
210
320
|
parser.add_argument("--theme", default="wechat-native", help="主题名称")
|
|
211
|
-
parser.add_argument("--output",
|
|
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)
|