yuanflow-cli 0.1.27 → 0.1.28
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 +1 -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/SKILL.md +106 -0
- 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 +255 -0
- 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/themes/bauhaus.json +352 -0
- 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/themes/bold-blue.json +338 -0
- 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/themes/bold-green.json +338 -0
- 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/themes/bold-navy.json +338 -0
- 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/themes/bytedance.json +346 -0
- 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/themes/chinese.json +340 -0
- 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/themes/coffee-house.json +335 -0
- 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/themes/elegant-blue.json +336 -0
- 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/themes/elegant-green.json +336 -0
- 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/themes/elegant-navy.json +336 -0
- 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/themes/focus-blue.json +336 -0
- 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/themes/focus-gold.json +336 -0
- 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/themes/focus-red.json +336 -0
- 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/themes/fresh-card.json +299 -0
- 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/themes/github.json +336 -0
- 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/themes/ink.json +329 -0
- 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/themes/lavender-dream.json +334 -0
- 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/themes/magazine.json +333 -0
- 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/themes/midnight.json +341 -0
- 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/themes/minimal-blue.json +320 -0
- 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/themes/minimal-gold.json +320 -0
- 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/themes/minimal-gray.json +320 -0
- 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/themes/minimal-navy.json +320 -0
- 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/themes/minimal-red.json +320 -0
- 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/themes/mint-fresh.json +338 -0
- 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/themes/newspaper.json +341 -0
- 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/themes/ocean-card.json +299 -0
- 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/themes/sports.json +341 -0
- 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/themes/sspai.json +337 -0
- 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/themes/sunset-amber.json +336 -0
- 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/themes/terracotta.json +335 -0
- 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/themes/warm-card.json +299 -0
- 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/themes/wechat-native.json +336 -0
package/package.json
CHANGED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: 公众号生成与发布
|
|
3
|
+
description: 当用户需要公众号文章创作、文章改写、Markdown 排版、主题预览、HTML 导出或推送到微信公众号草稿箱时使用。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 公众号生成与发布
|
|
7
|
+
|
|
8
|
+
本 Skill 用于把公众号文章从“需求确认、内容生成、排版预览、导出 HTML、推送草稿箱”串成一个可控流程。它可以一次性完成全流程,也可以只做其中一段,例如只写文章、只生成封面建议、只做排版预览或只推送草稿箱。
|
|
9
|
+
|
|
10
|
+
## 分类
|
|
11
|
+
|
|
12
|
+
自媒体技能区
|
|
13
|
+
|
|
14
|
+
## 开始前必须确认
|
|
15
|
+
|
|
16
|
+
1. 用户是在已有文章上处理,还是完全重新创作。
|
|
17
|
+
2. 用户本轮要做完整流程,还是只做写作、排版、封面建议、导出或草稿箱推送中的一部分。
|
|
18
|
+
3. 排版结果使用 Markdown 还是 HTML。
|
|
19
|
+
- 如果选择 HTML,会把 Markdown 转成微信公众号编辑器可粘贴的内联样式 HTML。
|
|
20
|
+
4. 是否需要推送到微信公众号草稿箱。草稿箱只负责生成草稿,正式发布必须由用户自行在公众号后台确认。
|
|
21
|
+
|
|
22
|
+
## 内容创作流程
|
|
23
|
+
|
|
24
|
+
### 已有文章处理
|
|
25
|
+
|
|
26
|
+
1. 读取用户提供的原文、素材或 Markdown。
|
|
27
|
+
2. 判断是否需要改写、润色、补结构、提炼标题、生成摘要或只做排版。
|
|
28
|
+
3. 保留用户原文中的事实、数字、引用和语气约束,不擅自新增未证实的信息。
|
|
29
|
+
|
|
30
|
+
### 完全重新创作
|
|
31
|
+
|
|
32
|
+
1. 先明确主题、读者、账号定位、文章目的和期望风格。
|
|
33
|
+
2. 使用 `自媒体知识库` 查询公众号创作相关知识点,根据主题构造 `domain` 和 `content_goal`。
|
|
34
|
+
3. 结合知识库结果输出文章结构、标题候选和正文。
|
|
35
|
+
4. 用户确认正文后再进入排版、导出或草稿箱流程。
|
|
36
|
+
|
|
37
|
+
## AI 内容增强
|
|
38
|
+
|
|
39
|
+
Agent 在排版前应做结构化增强,但不能改变事实:
|
|
40
|
+
|
|
41
|
+
- 连续对话内容转成 `dialogue` 容器。
|
|
42
|
+
- 连续图片转成 `gallery` 容器。
|
|
43
|
+
- 金句、关键引用、核心观点转成 `callout` 容器。
|
|
44
|
+
- 步骤、时间线、对比、清单转成对应结构容器。
|
|
45
|
+
- 外部链接转脚注,避免微信编辑器粘贴后结构混乱。
|
|
46
|
+
|
|
47
|
+
## 确定性排版脚本
|
|
48
|
+
|
|
49
|
+
当前 Skill 内置 `scripts/wechat_format.py`,用于把 Markdown 转成微信可粘贴的内联样式 HTML。
|
|
50
|
+
|
|
51
|
+
执行方式示例:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
python scripts/wechat_format.py --input article.md --theme newspaper --output dist/wechat.html --json
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
常用参数:
|
|
58
|
+
|
|
59
|
+
- `--input`:Markdown 文件路径。
|
|
60
|
+
- `--theme`:主题名,默认 `wechat-native`。主题来自 `themes/*.json`。
|
|
61
|
+
- `--output`:输出 HTML 文件路径。
|
|
62
|
+
- `--json`:同时输出结构化执行结果,便于 Agent 写入右侧工作台。
|
|
63
|
+
|
|
64
|
+
## 工作台展示
|
|
65
|
+
|
|
66
|
+
当右侧工作台存在 `内容创作` Tab 时,应使用 `content_workbench_update` 写入工作台,而不是写入数据面板。
|
|
67
|
+
|
|
68
|
+
推荐写入结构:
|
|
69
|
+
|
|
70
|
+
- `appId`: `wechat_official_account`
|
|
71
|
+
- `title`: `公众号生成与发布`
|
|
72
|
+
- `status`: `started` / `collecting` / `completed` / `failed`
|
|
73
|
+
- `views`: 可组合使用 `stepper`、`option_grid`、`preview_frame`、`editor_panel`、`asset_gallery`、`action_bar`、`status_timeline`、`result_card`
|
|
74
|
+
|
|
75
|
+
典型展示:
|
|
76
|
+
|
|
77
|
+
1. `stepper`:需求确认、内容创作、主题选择、预览、导出、草稿箱。
|
|
78
|
+
2. `option_grid`:主题卡片和分类筛选结果。
|
|
79
|
+
3. `preview_frame`:手机样式 HTML 预览。
|
|
80
|
+
4. `editor_panel`:Markdown 或 HTML 源码。
|
|
81
|
+
5. `action_bar`:导出 HTML、推送草稿箱、继续改写等动作。
|
|
82
|
+
|
|
83
|
+
## 公众号凭证
|
|
84
|
+
|
|
85
|
+
如果用户首次要求推送草稿箱,需要配置微信公众号官方接口凭证:
|
|
86
|
+
|
|
87
|
+
- 官方入口:https://developers.weixin.qq.com/platform
|
|
88
|
+
- 必需信息:AppID、AppSecret
|
|
89
|
+
- 凭证由 YuanFlow Runtime 保存到用户数据目录的本地 secrets/config 中,Skill 不自行写入凭证文件,也不把密钥打印到对话里。
|
|
90
|
+
|
|
91
|
+
如果当前环境没有可用的受控凭证保存工具,应提示用户先在程序设置或 Runtime 受控入口配置凭证,再继续草稿箱流程。
|
|
92
|
+
|
|
93
|
+
## 封面
|
|
94
|
+
|
|
95
|
+
封面不使用第三方生成接口。需要封面时,使用 YuanFlow 内置生图技能生成。
|
|
96
|
+
|
|
97
|
+
建议比例:
|
|
98
|
+
|
|
99
|
+
- 横向封面:2.35:1
|
|
100
|
+
- 方图辅助:1:1
|
|
101
|
+
|
|
102
|
+
## 输出要求
|
|
103
|
+
|
|
104
|
+
- 默认先给用户可确认的阶段性结果,不要一次性跳过用户选择。
|
|
105
|
+
- HTML 结果要适合复制到微信公众号编辑器。
|
|
106
|
+
- 草稿箱流程完成后只说明“已生成草稿箱草稿”,不要声称已发布。
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import html
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
12
|
+
THEMES_DIR = ROOT / "themes"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _style(style_map: dict[str, Any]) -> str:
|
|
16
|
+
parts: list[str] = []
|
|
17
|
+
for key, value in style_map.items():
|
|
18
|
+
css_key = key.replace("_", "-")
|
|
19
|
+
parts.append(f"{css_key}:{value}")
|
|
20
|
+
return ";".join(parts)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _load_theme(theme_name: str) -> dict[str, Any]:
|
|
24
|
+
safe_name = re.sub(r"[^a-zA-Z0-9_-]", "", theme_name or "wechat-native")
|
|
25
|
+
path = THEMES_DIR / f"{safe_name}.json"
|
|
26
|
+
if not path.exists():
|
|
27
|
+
path = THEMES_DIR / "wechat-native.json"
|
|
28
|
+
if not path.exists():
|
|
29
|
+
return {"name": "YuanFlow", "styles": {}}
|
|
30
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _paragraph(text: str, styles: dict[str, Any]) -> str:
|
|
34
|
+
return f'<p style="{_style(styles.get("p", {}))}">{html.escape(text)}</p>'
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _render_list(items: list[str], ordered: bool, styles: dict[str, Any]) -> str:
|
|
38
|
+
tag = "ol" if ordered else "ul"
|
|
39
|
+
li_style = _style(styles.get("li", styles.get("p", {})))
|
|
40
|
+
rendered = "".join(f'<li style="{li_style}">{html.escape(item)}</li>' for item in items)
|
|
41
|
+
return f'<{tag} style="{_style(styles.get(tag, {}))}">{rendered}</{tag}>'
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _render_image(line: str, styles: dict[str, Any]) -> str | None:
|
|
45
|
+
match = re.match(r"!\[([^\]]*)\]\(([^)]+)\)", line.strip())
|
|
46
|
+
if not match:
|
|
47
|
+
return None
|
|
48
|
+
alt, src = match.groups()
|
|
49
|
+
img_style = _style(
|
|
50
|
+
styles.get(
|
|
51
|
+
"img",
|
|
52
|
+
{
|
|
53
|
+
"max_width": "100%",
|
|
54
|
+
"border_radius": "8px",
|
|
55
|
+
"display": "block",
|
|
56
|
+
"margin": "16px auto",
|
|
57
|
+
},
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
return f'<img src="{html.escape(src)}" alt="{html.escape(alt)}" style="{img_style}" />'
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _flush_paragraph(buffer: list[str], output: list[str], styles: dict[str, Any]) -> None:
|
|
64
|
+
if not buffer:
|
|
65
|
+
return
|
|
66
|
+
output.append(_paragraph(" ".join(buffer).strip(), styles))
|
|
67
|
+
buffer.clear()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def markdown_to_wechat_html(markdown: str, theme: dict[str, Any]) -> str:
|
|
71
|
+
styles = theme.get("styles") if isinstance(theme.get("styles"), dict) else {}
|
|
72
|
+
wrapper_style = _style(
|
|
73
|
+
styles.get(
|
|
74
|
+
"wrapper",
|
|
75
|
+
{
|
|
76
|
+
"background_color": "#ffffff",
|
|
77
|
+
"padding": "16px",
|
|
78
|
+
"font_family": "-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif",
|
|
79
|
+
},
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
output: list[str] = [f'<section data-yuanflow="wechat-article" style="{wrapper_style}">']
|
|
83
|
+
lines = markdown.splitlines()
|
|
84
|
+
if lines and lines[0].strip() == "---":
|
|
85
|
+
for index, line in enumerate(lines[1:], start=1):
|
|
86
|
+
if line.strip() == "---":
|
|
87
|
+
lines = lines[index + 1 :]
|
|
88
|
+
break
|
|
89
|
+
paragraph_buffer: list[str] = []
|
|
90
|
+
unordered: list[str] = []
|
|
91
|
+
ordered: list[str] = []
|
|
92
|
+
in_code = False
|
|
93
|
+
code_lines: list[str] = []
|
|
94
|
+
|
|
95
|
+
def flush_lists() -> None:
|
|
96
|
+
if unordered:
|
|
97
|
+
output.append(_render_list(unordered, False, styles))
|
|
98
|
+
unordered.clear()
|
|
99
|
+
if ordered:
|
|
100
|
+
output.append(_render_list(ordered, True, styles))
|
|
101
|
+
ordered.clear()
|
|
102
|
+
|
|
103
|
+
for raw_line in lines:
|
|
104
|
+
line = raw_line.rstrip()
|
|
105
|
+
stripped = line.strip()
|
|
106
|
+
|
|
107
|
+
if stripped.startswith("```"):
|
|
108
|
+
_flush_paragraph(paragraph_buffer, output, styles)
|
|
109
|
+
flush_lists()
|
|
110
|
+
if in_code:
|
|
111
|
+
code_style = _style(styles.get("code_block", styles.get("pre", {})))
|
|
112
|
+
output.append(
|
|
113
|
+
f'<pre style="{code_style}"><code>{html.escape(chr(10).join(code_lines))}</code></pre>'
|
|
114
|
+
)
|
|
115
|
+
code_lines.clear()
|
|
116
|
+
in_code = False
|
|
117
|
+
else:
|
|
118
|
+
in_code = True
|
|
119
|
+
continue
|
|
120
|
+
|
|
121
|
+
if in_code:
|
|
122
|
+
code_lines.append(line)
|
|
123
|
+
continue
|
|
124
|
+
|
|
125
|
+
if not stripped:
|
|
126
|
+
_flush_paragraph(paragraph_buffer, output, styles)
|
|
127
|
+
flush_lists()
|
|
128
|
+
continue
|
|
129
|
+
|
|
130
|
+
image_html = _render_image(stripped, styles)
|
|
131
|
+
if image_html:
|
|
132
|
+
_flush_paragraph(paragraph_buffer, output, styles)
|
|
133
|
+
flush_lists()
|
|
134
|
+
output.append(image_html)
|
|
135
|
+
continue
|
|
136
|
+
|
|
137
|
+
heading = re.match(r"^(#{1,3})\s+(.+)$", stripped)
|
|
138
|
+
if heading:
|
|
139
|
+
_flush_paragraph(paragraph_buffer, output, styles)
|
|
140
|
+
flush_lists()
|
|
141
|
+
level = len(heading.group(1))
|
|
142
|
+
tag = f"h{level}"
|
|
143
|
+
text = html.escape(heading.group(2).strip())
|
|
144
|
+
output.append(f'<{tag} style="{_style(styles.get(tag, {}))}">{text}</{tag}>')
|
|
145
|
+
continue
|
|
146
|
+
|
|
147
|
+
quote = re.match(r"^>\s?(.+)$", stripped)
|
|
148
|
+
if quote:
|
|
149
|
+
_flush_paragraph(paragraph_buffer, output, styles)
|
|
150
|
+
flush_lists()
|
|
151
|
+
output.append(
|
|
152
|
+
f'<blockquote style="{_style(styles.get("blockquote", {}))}">{html.escape(quote.group(1))}</blockquote>'
|
|
153
|
+
)
|
|
154
|
+
continue
|
|
155
|
+
|
|
156
|
+
ordered_match = re.match(r"^\d+\.\s+(.+)$", stripped)
|
|
157
|
+
if ordered_match:
|
|
158
|
+
_flush_paragraph(paragraph_buffer, output, styles)
|
|
159
|
+
if unordered:
|
|
160
|
+
output.append(_render_list(unordered, False, styles))
|
|
161
|
+
unordered.clear()
|
|
162
|
+
ordered.append(ordered_match.group(1))
|
|
163
|
+
continue
|
|
164
|
+
|
|
165
|
+
unordered_match = re.match(r"^[-*]\s+(.+)$", stripped)
|
|
166
|
+
if unordered_match:
|
|
167
|
+
_flush_paragraph(paragraph_buffer, output, styles)
|
|
168
|
+
if ordered:
|
|
169
|
+
output.append(_render_list(ordered, True, styles))
|
|
170
|
+
ordered.clear()
|
|
171
|
+
unordered.append(unordered_match.group(1))
|
|
172
|
+
continue
|
|
173
|
+
|
|
174
|
+
flush_lists()
|
|
175
|
+
paragraph_buffer.append(stripped)
|
|
176
|
+
|
|
177
|
+
_flush_paragraph(paragraph_buffer, output, styles)
|
|
178
|
+
flush_lists()
|
|
179
|
+
output.append("</section>")
|
|
180
|
+
return "\n".join(output)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def build_preview(html_body: str, title: str) -> str:
|
|
184
|
+
escaped_title = html.escape(title or "公众号预览")
|
|
185
|
+
return f"""<!doctype html>
|
|
186
|
+
<html lang="zh-CN">
|
|
187
|
+
<head>
|
|
188
|
+
<meta charset="utf-8" />
|
|
189
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
190
|
+
<title>{escaped_title}</title>
|
|
191
|
+
<style>
|
|
192
|
+
body {{ margin: 0; background: #f4f6f8; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }}
|
|
193
|
+
.phone {{ width: min(430px, 100%); min-height: 100vh; margin: 0 auto; background: #fff; box-shadow: 0 12px 32px rgba(15, 23, 42, .12); }}
|
|
194
|
+
.bar {{ padding: 14px 18px; border-bottom: 1px solid #eef2f7; font-weight: 700; }}
|
|
195
|
+
.content {{ padding: 14px 18px 28px; }}
|
|
196
|
+
</style>
|
|
197
|
+
</head>
|
|
198
|
+
<body>
|
|
199
|
+
<main class="phone">
|
|
200
|
+
<div class="bar">{escaped_title}</div>
|
|
201
|
+
<div class="content">{html_body}</div>
|
|
202
|
+
</main>
|
|
203
|
+
</body>
|
|
204
|
+
</html>"""
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def main() -> int:
|
|
208
|
+
parser = argparse.ArgumentParser(description="YuanFlow 微信公众号 Markdown 排版工具")
|
|
209
|
+
parser.add_argument("--input", required=True, help="Markdown 输入文件")
|
|
210
|
+
parser.add_argument("--theme", default="wechat-native", help="主题名称")
|
|
211
|
+
parser.add_argument("--output", required=True, help="HTML 输出文件")
|
|
212
|
+
parser.add_argument("--title", default="公众号生成与发布", help="预览标题")
|
|
213
|
+
parser.add_argument("--preview", default="", help="可选预览 HTML 输出文件")
|
|
214
|
+
parser.add_argument("--json", action="store_true", help="输出 JSON 执行结果")
|
|
215
|
+
args = parser.parse_args()
|
|
216
|
+
|
|
217
|
+
input_path = Path(args.input).resolve()
|
|
218
|
+
output_path = Path(args.output).resolve()
|
|
219
|
+
markdown = input_path.read_text(encoding="utf-8")
|
|
220
|
+
theme = _load_theme(args.theme)
|
|
221
|
+
html_body = markdown_to_wechat_html(markdown, theme)
|
|
222
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
223
|
+
output_path.write_text(html_body, encoding="utf-8")
|
|
224
|
+
|
|
225
|
+
preview_path = Path(args.preview).resolve() if args.preview else output_path.with_name("preview.html")
|
|
226
|
+
preview_path.write_text(build_preview(html_body, args.title), encoding="utf-8")
|
|
227
|
+
|
|
228
|
+
result = {
|
|
229
|
+
"ok": True,
|
|
230
|
+
"theme": args.theme,
|
|
231
|
+
"themeName": theme.get("name") or args.theme,
|
|
232
|
+
"output": str(output_path),
|
|
233
|
+
"preview": str(preview_path),
|
|
234
|
+
"views": [
|
|
235
|
+
{
|
|
236
|
+
"id": "preview",
|
|
237
|
+
"type": "preview_frame",
|
|
238
|
+
"title": "公众号手机预览",
|
|
239
|
+
"html": preview_path.read_text(encoding="utf-8"),
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
"id": "html",
|
|
243
|
+
"type": "editor_panel",
|
|
244
|
+
"title": "微信内联 HTML",
|
|
245
|
+
"content": html_body,
|
|
246
|
+
},
|
|
247
|
+
],
|
|
248
|
+
}
|
|
249
|
+
if args.json:
|
|
250
|
+
print(json.dumps(result, ensure_ascii=False))
|
|
251
|
+
return 0
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
if __name__ == "__main__":
|
|
255
|
+
raise SystemExit(main())
|