yuanflow-cli 0.1.33 → 0.1.35
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 +30 -10
- 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/references/wechat-official-api.md +16 -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_draft.py +73 -23
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@ description: 当用户需要公众号文章创作、文章改写、Markdown 排
|
|
|
5
5
|
|
|
6
6
|
# 公众号生成与发布
|
|
7
7
|
|
|
8
|
-
本 Skill 用于把公众号文章从“需求确认、内容生成、排版预览、导出 HTML
|
|
8
|
+
本 Skill 用于把公众号文章从“需求确认、内容生成、排版预览、导出 HTML、新增或更新草稿箱草稿”串成一个可控流程。它可以一次性完成全流程,也可以只做其中一段,例如只写文章、只生成封面建议、只做排版预览、只新增草稿或更新已有草稿。
|
|
9
9
|
|
|
10
10
|
## 分类
|
|
11
11
|
|
|
@@ -22,7 +22,7 @@ description: 当用户需要公众号文章创作、文章改写、Markdown 排
|
|
|
22
22
|
- 用户没有要求正文插图时:不要擅自生成插图;只处理文字正文和可选封面。
|
|
23
23
|
5. 排版结果使用 Markdown 还是 HTML。
|
|
24
24
|
- 如果选择 HTML,会把 Markdown 转成微信公众号编辑器可粘贴的内联样式 HTML。
|
|
25
|
-
6.
|
|
25
|
+
6. 是否需要写入微信公众号草稿箱,并确认是新增草稿还是更新已有草稿。更新已有草稿时必须先拿到目标草稿的 `media_id`,多图文还要确认更新第几篇,第一篇 `index=0`。草稿箱只负责生成或更新草稿,正式发布必须由用户自行在公众号后台确认。
|
|
26
26
|
|
|
27
27
|
## 内容创作流程
|
|
28
28
|
|
|
@@ -63,6 +63,8 @@ Agent 在排版前应做结构化增强,但不能改变事实:
|
|
|
63
63
|
- 用户要求生成插图:调用 `生图技能` 生成图片,使用工具返回的本地缓存路径插入 Markdown。
|
|
64
64
|
- 用户只要求封面:只生成或使用封面,不在正文里额外插图。
|
|
65
65
|
|
|
66
|
+
使用生图技能生成公众号封面或正文插图时,不能只凭当前文章主题临时想象一个空泛提示词。必须先查看 `生图技能` 的提示词参考目录,优先根据参考文件的标题、目录或小节名判断有没有适合当前用户需求或间接相近方向的内容,例如知识卡、信息图、海报封面、科技商业插画、人物/场景图、产品图或编辑部视觉;发现相近方向后再进入对应小节阅读参考。不要求每次完整查看所有提示词,也不要机械逐篇浏览。读取后只借鉴其中的构图、镜头、版式、材质、灯光、信息层级、文字约束和负面提示写法,再结合当前公众号主题重写成新的高质量中文提示词;不要原样复制案例,不要带入无关品牌、人物、Logo、水印或英文占位字。若参考库没有完全匹配的方向,也要选择最接近的视觉类型作为结构参考,再生成更具体的画面规划。
|
|
67
|
+
|
|
66
68
|
生成公众号正文插图时必须遵守中文默认规则:
|
|
67
69
|
|
|
68
70
|
- 中文公众号文章默认生成中文语境图片。生图提示词必须写明:画面中如出现文字、标签、标题、图表标注,全部使用清晰简体中文。
|
|
@@ -70,11 +72,13 @@ Agent 在排版前应做结构化增强,但不能改变事实:
|
|
|
70
72
|
- 如果文章段落确实需要图中文字,只允许使用与文章一致的简体中文短句,并明确列出具体中文文字。
|
|
71
73
|
- 除非用户明确要求英文或双语,不要在正文插图、封面图或预览图里生成英文。
|
|
72
74
|
|
|
73
|
-
|
|
75
|
+
写入草稿箱时使用 `scripts/wechat_draft.py`。
|
|
76
|
+
|
|
77
|
+
新增草稿示例:
|
|
74
78
|
|
|
75
79
|
```bash
|
|
76
80
|
python scripts/wechat_draft.py \
|
|
77
|
-
--credentials-file "D
|
|
81
|
+
--credentials-file "D:\path\to\wechat-credentials.txt" \
|
|
78
82
|
--input article.md \
|
|
79
83
|
--theme newspaper \
|
|
80
84
|
--cover-image cover.png \
|
|
@@ -83,22 +87,36 @@ python scripts/wechat_draft.py \
|
|
|
83
87
|
--json
|
|
84
88
|
```
|
|
85
89
|
|
|
90
|
+
更新已有草稿示例:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
python scripts/wechat_draft.py \
|
|
94
|
+
--credentials-file "D:\path\to\wechat-credentials.txt" \
|
|
95
|
+
--input article.md \
|
|
96
|
+
--theme newspaper \
|
|
97
|
+
--draft-media-id MEDIA_ID \
|
|
98
|
+
--draft-index 0 \
|
|
99
|
+
--output-dir dist \
|
|
100
|
+
--update-draft \
|
|
101
|
+
--json
|
|
102
|
+
```
|
|
103
|
+
|
|
86
104
|
执行顺序:
|
|
87
105
|
|
|
88
106
|
1. 读取或生成 Markdown 正文,确保正文里的本地图片路径真实存在。
|
|
89
107
|
2. 先用 `wechat_format.py` 完成主题排版和本地预览。
|
|
90
|
-
3.
|
|
108
|
+
3. 如果要新增或更新草稿箱,再用 `wechat_draft.py`:
|
|
91
109
|
- 获取 `access_token`。
|
|
92
110
|
- 上传正文图片到 `/cgi-bin/media/uploadimg`,替换正文图片地址。
|
|
93
|
-
-
|
|
94
|
-
-
|
|
95
|
-
4. 返回草稿 `media_id
|
|
111
|
+
- 新增草稿时必须上传或提供封面图 `thumb_media_id`,再调用 `/cgi-bin/draft/add` 创建草稿箱草稿。
|
|
112
|
+
- 更新已有草稿时必须提供 `--draft-media-id` 和 `--draft-index`,调用 `/cgi-bin/draft/update` 更新指定图文。更新接口不是局部 patch,需要按接口要求重新提交该篇文章的完整 `articles` 对象;如果只改正文且不换封面,可以不传封面。
|
|
113
|
+
4. 返回草稿 `media_id` 或更新结果、本地 `draft_payload.json`、`wechat.html`、`preview.html` 和图片上传映射。
|
|
96
114
|
|
|
97
115
|
接口细节需要确认时,读取 `references/wechat-official-api.md`,不要凭记忆改接口路径。
|
|
98
116
|
|
|
99
117
|
安全要求:
|
|
100
118
|
|
|
101
|
-
-
|
|
119
|
+
- 真实新增草稿必须显式传入 `--push-draft`;真实更新草稿必须显式传入 `--update-draft`、`--draft-media-id` 和 `--draft-index`。否则只生成本地 HTML 和草稿 payload。
|
|
102
120
|
- 不要在对话、工作台或日志里输出 AppSecret、access_token。
|
|
103
121
|
- 如果微信接口返回 IP 白名单、权限不足、未认证、额度不足等错误,要把错误原因原样归类说明,不要假装成功。
|
|
104
122
|
|
|
@@ -167,7 +185,7 @@ python scripts/wechat_format.py --input article.md --theme newspaper --output di
|
|
|
167
185
|
2. `option_grid`:主题卡片和文章预览结果。主题卡片必须带 `intent: select_wechat_theme`、`payload.theme` 和 `instruction`,让用户点击后形成待执行任务。
|
|
168
186
|
3. `preview_frame`:手机样式 HTML 预览。
|
|
169
187
|
4. `editor_panel`:Markdown 或 HTML 源码。
|
|
170
|
-
5. `action_bar`:导出 HTML
|
|
188
|
+
5. `action_bar`:导出 HTML、新增草稿、更新已有草稿、继续改写等动作。
|
|
171
189
|
|
|
172
190
|
## 公众号凭证
|
|
173
191
|
|
|
@@ -185,6 +203,8 @@ python scripts/wechat_format.py --input article.md --theme newspaper --output di
|
|
|
185
203
|
|
|
186
204
|
封面不使用第三方生成接口。需要封面时,使用 YuanFlow 内置生图技能生成。
|
|
187
205
|
|
|
206
|
+
生成封面前同样必须先查看 `生图技能/references/image-prompt-reference.md` 的标题、目录或小节名,选择与文章定位最接近的封面、海报、知识卡、商业科技视觉或信息图方向作为参考;只需进入相近方向阅读,不需要完整扫完所有提示词。封面提示词要把文章主题、目标读者、视觉主元素、构图层级、比例、中文文字规则和负面约束写清楚,避免只写“生成一张某主题封面图”这类空泛需求。
|
|
207
|
+
|
|
188
208
|
建议比例:
|
|
189
209
|
|
|
190
210
|
- 横向封面:2.35:1
|
|
@@ -61,8 +61,24 @@ JSON:{"articles":[...]}
|
|
|
61
61
|
- `need_open_comment`
|
|
62
62
|
- `only_fans_can_comment`
|
|
63
63
|
|
|
64
|
+
5. 更新草稿
|
|
65
|
+
|
|
66
|
+
```text
|
|
67
|
+
POST https://api.weixin.qq.com/cgi-bin/draft/update?access_token=ACCESS_TOKEN
|
|
68
|
+
JSON:{"media_id":"MEDIA_ID","index":0,"articles":{...}}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
核心字段:
|
|
72
|
+
|
|
73
|
+
- `media_id`:要修改的图文消息草稿 ID
|
|
74
|
+
- `index`:要更新的文章位置,第一篇为 `0`
|
|
75
|
+
- `articles`:新的图文信息对象,不是数组
|
|
76
|
+
|
|
77
|
+
更新草稿不是局部 patch。调用时应按官方字段重新提交指定 `index` 这一篇文章的完整内容对象。正文内图片仍必须先走 `/cgi-bin/media/uploadimg`,封面如需更新则传 `thumb_media_id`;如果只更新正文且不更换封面,可以不传新的封面。
|
|
78
|
+
|
|
64
79
|
## 执行约束
|
|
65
80
|
|
|
66
81
|
- 真实创建草稿必须显式使用 `--push-draft`。
|
|
82
|
+
- 真实更新草稿必须显式使用 `--update-draft`,并提供 `--draft-media-id` 和 `--draft-index`。
|
|
67
83
|
- 不要输出 AppSecret 或 access_token。
|
|
68
84
|
- 微信返回错误时保留 `errcode/errmsg`,并把错误归因到白名单、凭证、素材、草稿权限或额度。
|
|
@@ -18,6 +18,7 @@ WECHAT_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token"
|
|
|
18
18
|
WECHAT_UPLOAD_ARTICLE_IMAGE_URL = "https://api.weixin.qq.com/cgi-bin/media/uploadimg"
|
|
19
19
|
WECHAT_ADD_MATERIAL_URL = "https://api.weixin.qq.com/cgi-bin/material/add_material"
|
|
20
20
|
WECHAT_ADD_DRAFT_URL = "https://api.weixin.qq.com/cgi-bin/draft/add"
|
|
21
|
+
WECHAT_UPDATE_DRAFT_URL = "https://api.weixin.qq.com/cgi-bin/draft/update"
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
class WechatApiError(RuntimeError):
|
|
@@ -171,29 +172,31 @@ def _truncate_utf8(value: str, max_bytes: int) -> str:
|
|
|
171
172
|
return encoded[:max_bytes].decode("utf-8", errors="ignore").rstrip()
|
|
172
173
|
|
|
173
174
|
|
|
174
|
-
def
|
|
175
|
+
def build_draft_article(
|
|
175
176
|
*,
|
|
176
177
|
title: str,
|
|
177
178
|
author: str,
|
|
178
179
|
digest: str,
|
|
179
180
|
content: str,
|
|
180
|
-
thumb_media_id: str,
|
|
181
|
+
thumb_media_id: str = "",
|
|
181
182
|
content_source_url: str = "",
|
|
182
183
|
) -> dict[str, Any]:
|
|
183
|
-
|
|
184
|
-
"
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
"thumb_media_id": thumb_media_id,
|
|
192
|
-
"need_open_comment": 0,
|
|
193
|
-
"only_fans_can_comment": 0,
|
|
194
|
-
}
|
|
195
|
-
]
|
|
184
|
+
article = {
|
|
185
|
+
"title": _truncate_utf8(title, 64),
|
|
186
|
+
"author": author,
|
|
187
|
+
"digest": _truncate_utf8(digest, 54),
|
|
188
|
+
"content": content,
|
|
189
|
+
"content_source_url": content_source_url,
|
|
190
|
+
"need_open_comment": 0,
|
|
191
|
+
"only_fans_can_comment": 0,
|
|
196
192
|
}
|
|
193
|
+
if thumb_media_id:
|
|
194
|
+
article["thumb_media_id"] = thumb_media_id
|
|
195
|
+
return article
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def build_draft_payload(*, article: dict[str, Any]) -> dict[str, Any]:
|
|
199
|
+
return {"articles": [article]}
|
|
197
200
|
|
|
198
201
|
|
|
199
202
|
def add_draft(access_token: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
@@ -207,7 +210,36 @@ def add_draft(access_token: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
|
207
210
|
)
|
|
208
211
|
|
|
209
212
|
|
|
213
|
+
def update_draft(
|
|
214
|
+
access_token: str,
|
|
215
|
+
*,
|
|
216
|
+
media_id: str,
|
|
217
|
+
index: int,
|
|
218
|
+
article: dict[str, Any],
|
|
219
|
+
) -> dict[str, Any]:
|
|
220
|
+
if not media_id.strip():
|
|
221
|
+
raise WechatApiError("draft-media-id-missing", "更新草稿必须提供 --draft-media-id。")
|
|
222
|
+
body = json.dumps(
|
|
223
|
+
{
|
|
224
|
+
"media_id": media_id.strip(),
|
|
225
|
+
"index": index,
|
|
226
|
+
"articles": article,
|
|
227
|
+
},
|
|
228
|
+
ensure_ascii=False,
|
|
229
|
+
).encode("utf-8")
|
|
230
|
+
return _request_json(
|
|
231
|
+
"POST",
|
|
232
|
+
WECHAT_UPDATE_DRAFT_URL,
|
|
233
|
+
params={"access_token": access_token},
|
|
234
|
+
data=body,
|
|
235
|
+
headers={"Content-Type": "application/json; charset=utf-8"},
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
210
239
|
def run(args: argparse.Namespace) -> dict[str, Any]:
|
|
240
|
+
if args.push_draft and args.update_draft:
|
|
241
|
+
raise WechatApiError("draft-operation-conflict", "--push-draft 和 --update-draft 不能同时使用。")
|
|
242
|
+
|
|
211
243
|
input_path = Path(args.input).resolve()
|
|
212
244
|
output_dir = Path(args.output_dir).resolve()
|
|
213
245
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -222,14 +254,15 @@ def run(args: argparse.Namespace) -> dict[str, Any]:
|
|
|
222
254
|
access_token_status = "skipped"
|
|
223
255
|
|
|
224
256
|
credentials = {"app_id": "", "app_secret": ""}
|
|
225
|
-
|
|
257
|
+
will_write_draft = bool(args.push_draft or args.update_draft)
|
|
258
|
+
if will_write_draft or args.credentials_file or args.app_id or args.app_secret:
|
|
226
259
|
credentials = _load_credentials(
|
|
227
260
|
Path(args.credentials_file).resolve() if args.credentials_file else None,
|
|
228
261
|
args.app_id,
|
|
229
262
|
args.app_secret,
|
|
230
263
|
)
|
|
231
264
|
|
|
232
|
-
if
|
|
265
|
+
if will_write_draft:
|
|
233
266
|
access_token = get_access_token(credentials["app_id"], credentials["app_secret"])
|
|
234
267
|
access_token_status = "ok"
|
|
235
268
|
markdown, image_uploads = upload_and_replace_markdown_images(markdown, input_path.parent, access_token)
|
|
@@ -245,10 +278,8 @@ def run(args: argparse.Namespace) -> dict[str, Any]:
|
|
|
245
278
|
uploaded_markdown_path.write_text(markdown, encoding="utf-8")
|
|
246
279
|
|
|
247
280
|
cover_media_id = args.thumb_media_id.strip()
|
|
248
|
-
if
|
|
281
|
+
if will_write_draft and not cover_media_id and args.cover_image:
|
|
249
282
|
cover_image = Path(args.cover_image).resolve() if args.cover_image else None
|
|
250
|
-
if not cover_image:
|
|
251
|
-
raise WechatApiError("cover-image-required", "推送草稿箱必须提供 --cover-image 或 --thumb-media-id。")
|
|
252
283
|
if not cover_image.exists() or not cover_image.is_file():
|
|
253
284
|
raise WechatApiError("cover-image-not-found", f"封面图片不存在:{cover_image}")
|
|
254
285
|
try:
|
|
@@ -261,8 +292,11 @@ def run(args: argparse.Namespace) -> dict[str, Any]:
|
|
|
261
292
|
cover_upload = {"source": str(cover_image), "media_id": cover_media_id}
|
|
262
293
|
|
|
263
294
|
draft_payload: dict[str, Any] | None = None
|
|
264
|
-
|
|
265
|
-
|
|
295
|
+
draft_article: dict[str, Any] | None = None
|
|
296
|
+
if args.push_draft and not cover_media_id:
|
|
297
|
+
raise WechatApiError("cover-image-required", "新增草稿必须提供 --cover-image 或 --thumb-media-id。")
|
|
298
|
+
if cover_media_id or args.update_draft:
|
|
299
|
+
draft_article = build_draft_article(
|
|
266
300
|
title=title,
|
|
267
301
|
author=author,
|
|
268
302
|
digest=args.digest.strip() or _plain_text_digest(markdown),
|
|
@@ -270,6 +304,9 @@ def run(args: argparse.Namespace) -> dict[str, Any]:
|
|
|
270
304
|
content_source_url=args.content_source_url.strip(),
|
|
271
305
|
thumb_media_id=cover_media_id,
|
|
272
306
|
)
|
|
307
|
+
draft_payload = build_draft_payload(
|
|
308
|
+
article=draft_article,
|
|
309
|
+
)
|
|
273
310
|
draft_payload_path = output_dir / "draft_payload.json"
|
|
274
311
|
draft_payload_path.write_text(json.dumps(draft_payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
275
312
|
else:
|
|
@@ -279,10 +316,20 @@ def run(args: argparse.Namespace) -> dict[str, Any]:
|
|
|
279
316
|
if not draft_payload:
|
|
280
317
|
raise WechatApiError("draft-payload-missing", "草稿 payload 未生成。")
|
|
281
318
|
draft_result = add_draft(access_token, draft_payload)
|
|
319
|
+
elif args.update_draft:
|
|
320
|
+
if not draft_article:
|
|
321
|
+
raise WechatApiError("draft-payload-missing", "草稿更新 payload 未生成。")
|
|
322
|
+
draft_result = update_draft(
|
|
323
|
+
access_token,
|
|
324
|
+
media_id=args.draft_media_id,
|
|
325
|
+
index=args.draft_index,
|
|
326
|
+
article=draft_article,
|
|
327
|
+
)
|
|
282
328
|
|
|
283
329
|
result = {
|
|
284
330
|
"ok": True,
|
|
285
|
-
"pushed":
|
|
331
|
+
"pushed": will_write_draft,
|
|
332
|
+
"operation": "update" if args.update_draft else ("add" if args.push_draft else "preview"),
|
|
286
333
|
"title": title,
|
|
287
334
|
"theme": args.theme,
|
|
288
335
|
"account": {
|
|
@@ -323,6 +370,9 @@ def main() -> int:
|
|
|
323
370
|
parser.add_argument("--app-id", default="", help="微信公众号 AppID")
|
|
324
371
|
parser.add_argument("--app-secret", default="", help="微信公众号 AppSecret")
|
|
325
372
|
parser.add_argument("--push-draft", action="store_true", help="显式创建草稿箱草稿;不传则只生成本地文件和 payload")
|
|
373
|
+
parser.add_argument("--update-draft", action="store_true", help="显式更新已有草稿;需同时提供 --draft-media-id")
|
|
374
|
+
parser.add_argument("--draft-media-id", default="", help="要更新的已有草稿 media_id")
|
|
375
|
+
parser.add_argument("--draft-index", type=int, default=0, help="要更新的图文位置,第一篇为 0")
|
|
326
376
|
parser.add_argument("--json", action="store_true", help="输出 JSON")
|
|
327
377
|
args = parser.parse_args()
|
|
328
378
|
|