بنقرة واحدة
feishu-doc-to-wechat-draft
从飞书/Feishu 文档直接生成微信公众号草稿内容(支持预览、默认样式、dry-run 发布),基于现有 wechat-draft-publisher 流程的独立封装。
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
القائمة
从飞书/Feishu 文档直接生成微信公众号草稿内容(支持预览、默认样式、dry-run 发布),基于现有 wechat-draft-publisher 流程的独立封装。
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
استنادا إلى تصنيف SOC المهني
Use when generating PPT-style image slides, poetic presentation covers, quiet paper-texture visual pages, report pages, invitations, social cards, or slide-image sets with GPT-Image-2 via image_generate.
Use when generating high-readability hand-drawn knowledge diagrams, architecture diagrams, workflow maps, or consulting-style visual explanations with GPT-Image-2 via image_generate.
可独立运行的 GPT-Image 增强版 EPUB2Podcast:在本地把 EPUB 转成双人中文音频、GPT-Image/Smart Slide 视觉页、最终 MP4,并生成 YouTube 发布素材。
可独立运行的 standalone 版 EPUB2Podcast:用户只需下载当前项目本身,即可在本地把 EPUB 转成 Smart Slide + 双人中文音频 + 最终 MP4 视频播客。
【Ark Agent Plan 专用版本】EPUB 转双人中文播客视频流水线:使用火山引擎 TTS(与 Seedream/Seedance 共享技术栈),Smart Slide + 双人音频 + 最终 MP4 视频,无需额外 Google/OpenRouter API Key。
【Ark Agent Plan 专用版本】Manim 数学/算法讲解视频完整流水线,使用火山引擎 TTS 中文旁白(与 Seedream/Seedance 共享认证)。Plan → TTS → Code → Render → Stitch → Deliver. 适用于:Manim 动画 + 中文配音、音画同步讲解视频、3Blue1Brown 风格教学视频。
| names | ["feishu-doc-to-wechat-draft"] |
| name | feishu-doc-to-wechat-draft |
| aliases | ["飞书文档到微信草稿箱","飞书文档微信草稿箱","lark-wechat-draft"] |
| description | 从飞书/Feishu 文档直接生成微信公众号草稿内容(支持预览、默认样式、dry-run 发布),基于现有 wechat-draft-publisher 流程的独立封装。 |
| version | 0.1.0 |
| author | Draco |
| license | MIT |
| metadata | {"hermes":{"tags":["feishu","lark","wechat","draft","markdown","publisher","preview"]}} |
这是一个独立可运行的 skill 封装:把飞书文档内容抓取为 Markdown,按公众号样式渲染,并支持
render-preview-feishu-doc-default)publish-feishu-doc-default)说明:这是在保留旧
wechat-official-account-draft-publisher能力的前提下,新增的“从飞书文档入库”方向,不会覆盖旧版本。
feishu-doc-to-wechat-draft/
├── scripts/
│ ├── run.py # 本地 CLI 入口
│ └── wechat_draft_publisher/ # 独立迁移后的核心逻辑(load/render/pipeline/cli)
├── examples/
│ └── default-publish-style.yaml
├── tests/
│ └── test_integration_example_doc.py
├── requirements.txt
└── .env.example
cd ~/.hermes/skills/productivity/feishu-doc-to-wechat-draft
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
当前渲染链路除 PyYAML、markdown-it-py 外,还需要 Pygments。
原因:代码块现在采用服务端语法高亮,而不是只靠简单的纯文本换行包装。这样发布到微信公众号草稿箱后,代码块才能更接近 Doocs 的真实实现效果。
示例文档:
https://g1mu6da08l.feishu.cn/docx/H0TZdmw3GoW2JSxGBFacw2jdnV8?from=from_copylink
python3 scripts/run.py render-preview-feishu-doc-default \
--doc "https://g1mu6da08l.feishu.cn/docx/H0TZdmw3GoW2JSxGBFacw2jdnV8?from=from_copylink" \
--output /tmp/example-feishu-to-wechat-preview.html
python3 scripts/run.py publish-feishu-doc-default \
--doc "https://g1mu6da08l.feishu.cn/docx/H0TZdmw3GoW2JSxGBFacw2jdnV8?from=from_copylink" \
--thumb-media-id DRY_RUN_MEDIA_ID \
--dry-run
返回 JSON 中会包含 payload,可直接用于确认是否满足微信要求(标题、摘要、封面 ID、正文 HTML)。
--thumb-media-id这次真实联调再次确认:
publish-feishu-doc-default --dry-run如果不传:
--thumb-media-id会直接报:
ValueError: thumb_media_id is required in dry-run mode
所以 dry-run 的最小正确写法应固定为:
python3 scripts/run.py publish-feishu-doc-default \
--doc "<飞书文档URL>" \
--author "DracoVibeCoding" \
--thumb-media-id DRY_RUN_MEDIA_ID \
--dry-run
也就是说:
thumb_media_id 占位即可用户明确要求公众号草稿内容默认全左对齐。当前 doocs/grace 发布链路应满足:
justify=falsetext-align: justifytext-align: center注意:此前当前文档并不是两端对齐;doocs 默认 justify=false。真正容易造成“看起来没左齐”的残留是标题默认样式里的 display: table; margin: ... auto 和图注/星号分割线的居中样式。
1.飞书/Lark 文档导出的 Markdown 可能把每个有序列表项都写成 1.;如果列表项中间夹了截图、引用块,Markdown 渲染会把它们拆成多个单项列表,推到微信公众号后台后序号看起来全是 1.。
当前链路已做两层修复:
normalize_lark_markdown() 会把跨图片/引用块的连续步骤重编号,例如 1,2,3...15。<ol start="N"> 的起始序号,并把它转成微信更稳的显式 <span class="md-ordered-index">N.</span>,避免依赖微信后台的 <ol> 默认样式。验收时固定检查:
md-ordered-index 不应全是 1.。lark-image:// 残留应为 0。这次真实联调踩到一个很隐蔽的坑:
~/.cache/wechat-draft-publisher/access_token.json)表面现象会非常迷惑:
draft/add 看起来返回成功media_iddraft/get 这个 media_id,会得到 invalid media_id稳妥做法:
appid 分开,例如:
~/.cache/wechat-draft-publisher/access_token_<APPID>.jsondraft/batchget 是否真能列出草稿draft/get 是否能读取刚返回的 media_id这次继续联调时又踩到一个新的真实坑:
appid/appsecret 没错GET /cgi-bin/token 也能返回 access_tokenmaterial/add_material、draft/add 等接口时,微信仍可能返回:40001 invalid credential, access_token is invalid or not latest
could get access_token by getStableAccessToken
这类报错在当前公众号环境里,不能只理解成“密钥错了”,更常见的根因是:
POST /cgi-bin/stable_tokenforce_refresh=False 仍会拿到一个不是 latest 的 token稳妥策略:
POST https://api.weixin.qq.com/cgi-bin/stable_token
Content-Type: application/json
{
"grant_type": "client_credential",
"appid": "...",
"secret": "...",
"force_refresh": true
}
如果报错里明确出现:
invalid credentialnot latestcould get access_token by getStableAccessToken那就不要继续重试旧 token;直接切到:
stable_tokenforce_refresh=true对于“上传封面 → 发草稿”的一次性发布链路,宁可每次正式发布时都重新取一次最新 stable token,也不要过度相信本地缓存。
修完后立即做最小验证:
get_access_token() 是否成功upload_cover_image(...) 是否成功如果现有代码仍是:
GET /cgi-bin/token则建议改成:
POST /cgi-bin/stable_tokenforce_refresh=true这样能显著减少“本地看着拿到 token 了,但微信后续接口仍判你不是 latest”的问题。
publish-default 时,frontmatter 不是可选项这次继续联调又踩到一个很容易忽略的坑:
publish-default --input /path/to/article.md 直接发布本地 Markdown 时# 从飞书文档一键发布到微信公众号ValueError: title is required
根因是当前 load_article(...) 的取值规则是:
title / author / digest / cover_image / source_url# 一级标题 自动回填 title也就是说,下面这种文件不能直接用于 publish-default:
# 文章标题
正文……
稳妥做法是至少补上:
---
title: 文章标题
author: DracoVibeCoding
---
然后再接正文 Markdown。
如果只是临时重发一篇本地稿,可以先在 /tmp 里生成一个带 frontmatter 的发布副本,再执行:
python3 scripts/run.py publish-default \
--input /tmp/article_publishable.md \
--thumb-media-id "已有封面素材ID"
media_id这次排查也再次证明:
draft_media_id更稳的收尾动作应是:
draft_media_idaccess_token 调 draft/batchgetmedia_idtitleupdate_time也就是把“微信接口说成功”升级成“当前公众号后台可列出这条草稿”。
尤其当用户明确反馈“后台没看到新推送”时,优先排查:
thumb_media_idmedia_id,但当前号的 draft/batchget 根本列不出来需要正式发布到微信草稿箱时,请在环境变量中提供:
thumb_media_id 时,先本地做一个临时封面再发这次把新的飞书文档直推公众号草稿箱时,真实踩到的第一道坎仍然是:
ValueError: cover_image or thumb_media_id is required for publish
也就是说:
publish-feishu-doc-default 真发草稿时--thumb-media-id--cover-image如果手头没有现成封面素材,最省事的办法是:
python3 scripts/run.py render-preview-feishu-doc-default \
--doc "<飞书文档URL>" \
--author "DracoVibeCoding" \
--output /tmp/feishu_wechat_preview.html
ffmpeg 直接生成一张临时封面 PNG这台机器上没有 PIL,但有 ffmpeg,下面这类命令可直接出图:
ffmpeg -y \
-f lavfi -i color=c='#FA5151':s=900x383:d=1 \
-vf "drawbox=x=28:y=28:w=844:h=327:color=white@0.10:t=fill,\
drawtext=fontfile=/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc:text='文章标题':fontcolor=white:fontsize=46:x=(w-text_w)/2:y=130,\
drawtext=fontfile=/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc:text='DracoVibeCoding':fontcolor=white@0.92:fontsize=24:x=(w-text_w)/2:y=220" \
-frames:v 1 /tmp/wechat_cover.png
python3 scripts/run.py publish-feishu-doc-default \
--doc "<飞书文档URL>" \
--author "DracoVibeCoding" \
--cover-image /tmp/wechat_cover.png
适用场景:
ffmpegdraft/get 回查这次成功发布后,又额外做了一步验证:
stable_token 重新取最新 tokencgi-bin/draft/gettitleauthorthumb_media_idmedia_id这样才能确认:
这次真实联调又踩到一个非常容易误判的问题:
draft_media_id更稳的做法是:每次说“已经发到草稿箱”之前,都立即用微信 draft/batchget 回查一次,确认当前公众号后台真的能列出目标标题。
推荐最小验证链路:
draft_media_idstable_tokenPOST https://api.weixin.qq.com/cgi-bin/draft/batchget?access_token=ACCESS_TOKEN
Content-Type: application/json
{
"offset": 0,
"count": 10,
"no_content": 1
}
item[].content.news_item[] 里同时核对:
titleauthorthumb_media_idmedia_id如果用户反馈“草稿箱里没看到新稿”,优先不要争论;直接重新做这一步回查。
publish-default 发布本地 Markdown 时,缺 frontmatter 会直接报 title is required这次还有一个非常具体、很容易再次遇到的坑:
publish-default --input article.md那么 load_article() 读不到:
titleauthordigestcover_image此时发布链路会在校验阶段直接报:
ValueError: title is required
稳妥做法:
---
title: 从飞书文档一键发布到微信公众号
author: DracoVibeCoding
---
python3 scripts/run.py publish-default \
--input /tmp/article_publishable.md \
--thumb-media-id YOUR_THUMB_MEDIA_ID
thumb_media_iddraft/batchget 回查需要正式发布到微信草稿箱时,请在环境变量中提供:
如果用户反馈“公众号草稿箱里的 code block 看起来不对”,优先检查下面两件事,而不是先盲目改 CSS:
有没有对 <code> 内容做二次 HTML 转义
& / < / > 再整体替换一遍,就会把 " 变成 &quot;。有没有真正做服务端语法高亮
<span style="display:block">...,外壳看起来像代码块,但底层并不接近 Doocs。<code> 里取出已 escape 的内容Pygments)生成带 inline style 的 token HTML实践上,这类问题的根因通常是渲染链路错误,不是单纯“主题颜色不一致”。
<pre>这次真实发布后又踩到一个非常关键的兼容性坑:
<pre> 挪到内部 <code>,表面上仍然可能看起来“能滚”不要把外层改成:
<pre style="overflow: hidden; ...">
...
<code style="overflow-x: auto; white-space: pre; min-width: max-content; ...">
这个结构在普通浏览器里可能还能工作,但在微信文章环境里不够稳。
应优先保持:
<pre style="overflow-x: auto; overflow-y: hidden; -webkit-overflow-scrolling: touch; ...">
...
<code style="white-space: pre; min-width: max-content; ...">
也就是:
<pre> 负责横向滚动<code> 只负责承载内容与内边距,不要再承担主要滚动职责如果你还要保留 Doocs-like 的 Mac 顶部样式(红黄绿 dots / 顶部 header 感):
pre 内部的独立块放在前面pre 改成 overflow: hiddencodeoverflow-x: auto; overflow-y: hidden;-webkit-overflow-scrolling: touch;code 保留:
white-space: premin-width: max-contentword-break: normaloverflow-wrap: normal至少补一条针对公众号横滑的测试:
<pre> 上仍有 overflow-x: auto-webkit-overflow-scrolling: touchoverflow: hidden 这种会吞掉横滑的结构一句话记忆:在微信公众号里,code block 的横向滚动容器应优先是 <pre>,不是内部 <code>。视觉细节可以继续优化,但不要为 Mac 顶部样式牺牲 pre 的横滑能力。
需要正式发布到微信草稿箱时,请在环境变量中提供:
export WECHAT_APP_ID="<AppID>"
export WECHAT_APP_SECRET="<AppSecret>"
再执行:
python3 scripts/run.py publish-feishu-doc-default \
--doc "https://g1mu6da08l.feishu.cn/docx/H0TZdmw3GoW2JSxGBFacw2jdnV8?from=from_copylink"
source .venv/bin/activate
pytest -q tests/test_integration_example_doc.py
成功后可复用示例输出,确认新 skill 封装链路可用。
这次真实联调又踩到一个很蠢、但非常容易再次出现的坑:
~/.hermes/skills/productivity/feishu-doc-to-wechat-draft/scripts/run.py/home/ubuntu/projects/draco-skills-collection/feishu-doc-to-wechat-draft/如果两边代码漂移,就会出现非常迷惑的现象:
运行时副本里仍是旧逻辑,例如:
display: -webkit-boxwhite-space: nowrapwhite-space: pre + min-width: max-content + pre 负责横向滚动<br/> 保留换行的修复而 standalone 项目里已经是修复后的版本。
不要只靠“记得同步文件”。更稳的是:
例如 scripts/run.py 应优先类似这样做:
ROOT = Path(__file__).resolve().parent
CANONICAL_ROOT = Path("/home/ubuntu/projects/draco-skills-collection/feishu-doc-to-wechat-draft/scripts")
IMPORT_ROOT = CANONICAL_ROOT if CANONICAL_ROOT.exists() else ROOT
sys.path.insert(0, str(IMPORT_ROOT))
当用户反馈“这个 bug 明明修过,为什么又回来了”时,优先不要先怀疑微信;先查:
run.pyrenderer.py 路径是哪一个white-space: nowrapdisplay: -webkit-boxwhite-space: premin-width: max-contentoverflow-x: auto; overflow-y: hidden-webkit-overflow-scrolling: touch一句话记忆:如果 skill 既有运行时副本,又有 standalone canonical 项目,就不要让运行入口默认吃本地副本;否则回归只是时间问题。
这次真实回归排查踩到了一个很隐蔽但很致命的坑:
feishu-doc-to-wechat-draft:
~/.hermes/skills/productivity/feishu-doc-to-wechat-draft//home/ubuntu/projects/draco-skills-collection/feishu-doc-to-wechat-draft/~/.hermes/skills/.../scripts/run.pyrenderer.py 仍然是旧版,于是把 white-space: nowrap / display: -webkit-box 这些老逻辑又带回去了,导致:
不是微信随机抽风,也不是测试全白费。
是因为:
这类问题一旦出现,单看 HTML/CSS 症状会很像“神秘回归”,但真正根因是 执行入口与源码来源不一致。
如果这台机器上同时存在:
那么在重新发布公众号草稿箱前,必须先确认以下三件事:
例如本技能实际发布命令是:
python3 ~/.hermes/skills/productivity/feishu-doc-to-wechat-draft/scripts/run.py ...
那就说明真正生效的是:
~/.hermes/skills/productivity/feishu-doc-to-wechat-draft/scripts/wechat_draft_publisher/*.py而不是 /home/ubuntu/projects/... 那份。
对于这次 code block 回归,至少应对拍:
diff -u \
~/.hermes/skills/productivity/feishu-doc-to-wechat-draft/scripts/wechat_draft_publisher/renderer.py \
/home/ubuntu/projects/draco-skills-collection/feishu-doc-to-wechat-draft/scripts/wechat_draft_publisher/renderer.py
重点看是否还残留这些旧逻辑:
display: -webkit-boxwhite-space: nowrapmin-width: max-contentoverflow-x: auto; overflow-y: hidden-webkit-overflow-scrolling: touch不要只测 standalone 项目。
应直接用运行时 skill 重新生成 preview / payload,并检查是否满足:
white-space: nowrapdisplay: -webkit-boxwhite-space: premin-width: max-contentoverflow-x: auto; overflow-y: hidden-webkit-overflow-scrolling: touch如果用户反馈:
优先不要先怪微信。
先检查:
renderer.py 是否还是旧副本white-space: nowrap以后凡是这类“standalone + 运行时副本共存”的技能,公众号真实发布前建议固定做:
~/.hermes/skills/...draft/get 回查最终草稿里的 HTML 关键片段一句话记忆:在这台机器上,发布是否成功,不只取决于你“修过哪份代码”,更取决于“命令实际执行的是哪份代码”。
飞书文档里经常出现一种视觉上很像有序列表、但抓回来的 Markdown 实际还只是普通段落的结构,例如:
简单说,它解决三个核心问题:
**1. 图片自动处理**
说明正文……
**2. 格式完整保留**
说明正文……
**3. 排版风格统一**
说明正文……
如果直接交给 Markdown 渲染器,或者只做“标题+正文硬拼成同一行”的粗暴重写,都会出问题。常见症状包括:
1 / 2 / 3 不再保留原本的段落层次更稳的做法,是在 normalize_lark_markdown(...) 阶段先做结构正规化:
**1. 标题**也就是优先转成:
1. **图片自动处理**
说明正文……
2. **格式完整保留**
说明正文……
3. **排版风格统一**
说明正文……
而不是:
1. **图片自动处理** 说明正文……
2. **格式完整保留** 说明正文……
3. **排版风格统一** 说明正文……
这样 MarkdownIt 会自然产出“一个 <li> 内含多个 <p>”的结构,后续 HTML 渲染才能既保留有序列表语义,又保留段落边界。
即使 normalize 已经产出了正确的多段 list item,渲染层仍然可能把它破坏掉。
这次真实踩到的坑是:
_rewrite_ordered_lists() 如果把整个 <li>...</li> 内容塞回一个 inline <span class="md-ordered-text">...</span><p> 会被再次 inline 化稳妥做法:
<section class="md-ordered-text">...</section>)item.strip() 后塞进一个 <span> 就是安全的至少补两类测试:
normalize_lark_markdown(...) 回归测试
**1. 标题** + 下一行正文 + **2. 标题** + 下一行正文1. **标题**\n\n 正文renderer 回归测试
这次实际补上的测试方向包括:
tests/test_lark_markdown_normalization.pytests/test_ordered_list_start_and_code_theme.py这次继续联调又发现一个很容易遗漏的结构坑:
飞书原始 Markdown 里像下面这种内容:
**2. 微信公众号凭证**
登录微信公众平台,在「开发」-「基本配置」里获取:
- AppID
- AppSecret(只显示一次,记得保存)
同时把你的服务器 IP 添加到「IP 白名单」,否则调用接口会报错。
如果 normalize 只把 **2. 微信公众号凭证** 变成 ordered item 标题,却没有把:
一起缩进并吸附进同一个 list item,就会在公众号里出现:
更稳的正规化目标应是:
2. **微信公众号凭证**
登录微信公众平台,在「开发」-「基本配置」里获取:
- AppID
- AppSecret(只显示一次,记得保存)
同时把你的服务器 IP 添加到「IP 白名单」,否则调用接口会报错。
strong-numbered block 的更稳处理规则在 _convert_strong_numbered_blocks(...) 里,建议采用下面的策略:
**2. 标题** 这类独占一行的强编号标题2. **标题****3. 标题** 这类强编号标题**方法一:...**)# / ## / <hr>)- ... 保留为 item 内部 bullet list这意味着:
2. 微信公众号凭证,后面的说明 + bullets + IP 白名单说明,都应继续属于同一个 ordered item3. 封面图的 media_id,遇到 **方法一:...** 时必须停止吸收,让方法标题和后续步骤重新成为 item 外的独立块除了“标题 + 下一行正文”的测试,还应覆盖:
编号项内部 bullets 不掉出 item
**2. 微信公众号凭证** + 段落 + bullet 列表 + 补充说明2. item 内编号项遇到独立 strong 小标题时及时断开
**3. 封面图的 media_id** 后面跟 **方法一:...**方法一 不会被吞进 3. item 的正文段里最终 HTML 中 2/3 两个编号项连续且不重复错号
2. 微信公众号凭证 与 3. 封面图的 media_id 都在同一 ordered list 体系内2. 项内部包含 bullets 与补充说明3. 项之后的方法标题和方法步骤是编号项外的独立块飞书抓回来的文本“看起来有结构”,不代表 Markdown 语义已经完整;而且就算 normalize 修对了,renderer 也可能把结构再次压扁。发布到公众号前,必须同时检查:语义结构有没有补对,渲染后处理有没有把它破坏掉。对于编号项,既要避免把后续内容吞过头,也要避免只收标题不收其内部 bullets / 补充说明。
<ul><li>这次真实发布又踩到一个 renderer 层面的坑:
- Hermes:
- 模型:...- 宿主:...<ul> / <li>根因不是 normalize,而是 renderer 旧逻辑对无序列表做了这种事:
<ul ...>(...)</ul><li ...>(...)</li><ul> 时会在第一个 </li> / </ul> 处把层级拆坏稳妥做法:
ul.md-ul 做递归重写<p> 包整个 item这次实际修复采用的是:
xml.etree.ElementTree 解析当前 HTML 片段ul.md-ulmd-bullet-item 改成 block 级 <section>md-bullet-text 允许继续包住下一层 md-list md-list-unordered建议补的回归测试至少包括:
md-list md-list-unordered<ul class="md-ul"><li class="md-li">一句话记忆:无序列表一旦有嵌套,就必须按树处理;用正则拆 <ul>/<li> 迟早会炸。