md-pushplus
把指定的 Markdown 文件渲染成带样式的 HTML,再通过 PushPlus 推送到微信、邮箱等渠道。
目标
用户已经有(或刚生成)一份 Markdown,想把它「变好看 + 送到手机/邮箱」。本 skill 负责后半段——渲染与发送,是一个确定性的收尾动作,报告内容本身由用户或上游 skill 提供。
何时用 / 不用
- 用:用户给出一个
.md文件路径(或上一步刚写出的报告文件),要求推送、发送、push、通知到微信/邮箱,并提到 pushplus,或没指定渠道但想"发到手机"。 - 不用:用户要的是生成报告内容(那是上游 skill 的事);或明确要走邮件 SMTP、Server酱、钉钉、飞书等非 PushPlus 渠道(本 skill 只接 PushPlus)。
默认行为(不要问,直接这么做)
- 推送格式默认 HTML:PushPlus 的
template固定html,正文一律先渲染成 HTML 再发,不发裸 Markdown、不发纯文本。 - 渲染默认用共享主题模板:走
shared/html_report的 Markdown 引擎与主题 CSS(默认default主题,即 AlphaVault 站点风格),跟 a-stock-* / usmarket / ch-news-reporter 出的网页版报告同一套观感。除非用户点名要别的主题或要极简体积,否则不要切换。
用户没提要求时,这两条就是默认值,python scripts/md_to_pushplus.py 报告.md 一条命令即可,无需额外参数。
前置:token
PushPlus 的 token 从环境变量 PUSHPLUS_TOKEN 读取(在 pushplus.plus 登录后于「一对一推送」页面获取)。也可用 --token 显式传入。若两者都没有,发送会失败并提示——这时向用户要 token,不要瞎编。
工作流程
- 确认输入:拿到要推送的 Markdown 文件路径。如果是上一步刚生成的报告,直接用那个路径。
- 想好标题:标题是用户在微信/邮件里第一眼看到的东西。
- 默认会取 Markdown 里的第一个
# 一级标题;没有就用文件名。 - 如果默认标题不够清楚(比如就是个日期),主动替用户拟一个有信息量的标题(如「6/24 宏观日报:美债走高、黄金回落」),用
--title传入。这是模型该做的判断,别留给脚本。
- 默认会取 Markdown 里的第一个
- 先 dry-run 自检(推荐):加
--dry-run --save-html /tmp/preview.html先渲染不发送,脚本会打印推送包的字符数与 UTF-8 字节数,并把体积拆成css + body + 外壳三块。看字节数那个:PushPlus 文档没写清 4 万的上限是字符还是字节,而一个中文字符占 3 字节,所以脚本按字节这一侧保守判定——一份 1.4 万字符的中文日报就已经接近上限了。顶到上限看下面「常见失败」。预览文件按手机视口打开看最准——推送主要在微信里读。 - 发送:去掉
--dry-run正式推送。脚本返回 PushPlus 的 JSON,code==200即成功。 - 如实回报:把发送结果(成功 / 失败原因)告诉用户,并说明用的是哪套渲染(主题名或 inline 降级)。失败时按「常见失败」排查,别假装成功。
命令
# 最简:默认 html 模板 + 共享 default 主题,标题自动从 # 标题或文件名推断
python scripts/md_to_pushplus.py 报告.md
# 显式标题(推荐——一个好标题比正文更影响打开率)
python scripts/md_to_pushplus.py 报告.md --title "6/24 宏观日报:美债走高、黄金回落"
# 换共享主题:claude(暖色衬线)/ print(黑白衬线,适合转 PDF)
python scripts/md_to_pushplus.py 报告.md --theme claude
# 先自检不发送,并保存一份 HTML 预览(存的就是要推的那份,所见即所推)
python scripts/md_to_pushplus.py 报告.md --dry-run --save-html /tmp/preview.html
# 降级成纯内联样式(体积小、兼容老邮件客户端)
python scripts/md_to_pushplus.py 报告.md --renderer inline
# 群组推送(一对多)/ 指定渠道(邮件)
python scripts/md_to_pushplus.py 报告.md --topic 群组code
python scripts/md_to_pushplus.py 报告.md --channel mail
参数:--token(默认读 $PUSHPLUS_TOKEN)、--template(PushPlus 模板,默认 html)、--renderer(theme 默认 / inline 降级)、--theme(default 默认 / claude / print)、--topic(群组 code,一对多)、--channel(wechat|mail|webhook|cp|sms)、--save-html、--dry-run。
渲染说明
默认路径(--renderer theme):用 shared/html_report 的 Markdown 引擎和主题 CSS,但推出去的是一个自包含的 HTML 片段,不是整页文档——一个带作用域的 <div id="pp-doc">,里面是内联的主题 CSS 加报告正文。
为什么是片段不是整页,三条都是在真实推送页面(pushplus.plus/shortMessage/…)上实测出来的:
- PushPlus 把 content 以 innerHTML 注入自己的详情页,页面里的
<script>一律不执行。所以 shared 那套装饰脚本(表格数字红绿、h2 轮色、隐藏独立---)全都失效,正文里会裸露---。现在这些装饰走shared/html_report/static_decorations.py的构建期投影,静态写进 HTML,不再依赖 JS——规则和浏览器版同源,别在本 skill 里另抄一份。 - 整页的主题 CSS 会漫出去改掉 PushPlus 自己的页面:
*、html、body那几条规则实测把宿主的字体、底色、间距一起换了。现在所有选择器都加了#pp-doc前缀,:root/html/body收敛到容器本身,宿主一个属性都不受影响。 <!doctype>/<head>/<title>/<meta>会被当正文解析成垃圾节点,而.page的calc(100vw - 40px)算的是视口宽不是容器宽,在窄容器里会溢出。片段没有这些标签,宽度一律按 100% 走。
另外两处是专门为手机读做的(推送基本都在微信里看):
- 主题给桌面报告的表格设了
min-width:520px+ 单元格nowrap,结果 375px 的手机上连两三列的小表都被迫横滑。窄屏下这两条被放开,同时给单元格加word-break:keep-all——只放开nowrap的话中文短词会被逐字拆成一列一个字(实测同一张表从 468px 高涨到 891px)。现在窄表能收进屏幕,宽表保持一行一条、由.table-wrap横滑兜底。 - CSS 按片段里实际出现的 class 摇树——图表、时间轴、hero 卡、折叠更新那些规则在纯 Markdown 推送里用不到,直接不进包。主题 CSS 从约 1.3 万字符降到 5–6 千。
--save-html 存的预览文件比推送包多一层 doctype + viewport 外壳,正文与样式逐字节相同,就是为了能在本地按手机视口看到微信里的样子。
降级路径(--renderer inline):脚本内置的纯标准库渲染,样式全部内联到元素 style="" 上。什么时候用它——用户点名要、或目标是对 <style> 支持差的老邮件客户端(如 Outlook 桌面版,不认 CSS 变量)。注意它不是"更省字符"的选项:inline 给每个元素都挂 style,正文越长越亏,实测 md 超过约 10KB 后整包就比主题版更大了。共享包导入不到时脚本也会自动落到这条路,并在 stderr 说明原因;这种情况要如实告诉用户,别当成主题版推送成功。
输入若是 Obsidian / Jekyll 笔记,开头的 YAML frontmatter(--- ... --- 元数据块)会被自动剥离,不会渲染进正文;若正文没有 # 一级标题,会用 frontmatter 里的 title: 兜底作推送标题。
改动这条渲染路径后跑一遍 scripts/test_push_render.py(静态装饰、CSS 作用域化、摇树、片段装配的断言都在里面)。
常见失败
no token:没设PUSHPLUS_TOKEN也没传--token→ 向用户要 token。- PushPlus 返回
code != 200:常见是 token 失效、当天免费额度用尽、或content超长。把msg原文转告用户。 - 内容超长(约 4 万,按 UTF-8 字节算):脚本会先 WARNING。别指望换
--renderer inline能救——主题版的 CSS 只占 5–6 千字符,长报告的体积几乎全在正文,inline 反而更大。正路是把长报告拆成几条分别推,或先精简正文。 shared/html_report unavailable:共享包没同步进来(scripts/_shared/html_report缺失,且不在仓库开发目录下)。跑一次python scripts/skill_sync.py补齐;在此之前脚本会用 inline 兜底,推送不会中断。
边界
只做渲染 + PushPlus 发送,不生成报告内容,不接入 PushPlus 以外的推送服务。报告内容的对错由上游负责,本 skill 不审校正文。