易海推送技能包(封面 + 推草稿箱)
> 读者是模型,不是人。 按 §0 → §9 顺序执行,不要跳步。
> 适用账号:易海工程AI(订阅号,AppID 见环境变量 WECHAT_APPID)。不要用于其他矩阵号。
> 本包只管交付链路最后一段:接收成品 HTML → 生成封面 → 推草稿箱 → 验证。
> 不写稿、不做正文排版、不套品牌头尾。 上游是「易海排版技能包」,它交出来的就是成品。
§0 你的第一个动作:先问封面风格(禁止跳过)
封面风格与正文风格是两回事,必须单独问一次。 用户没选就问,不要拿上次的封面接着用。
| 封面风格 | 长什么样 | 素材从哪来 | 适用题材 | 用哪个脚本 |
|---|---|---|---|---|
| ① 真人风 | 人物为主视觉(半身/特写),压暗或加渐变蒙版后压白色大字标题 | 你自己生成人物图;用户主动给了照片则优先用用户的 | 名人稿、人物故事、创始人叙事、访谈 | make_cover_huangrenxun.py |
| ② 大字报风 | 强色底 + 超大加粗文字占满画面,几乎无图 | 纯代码绘制,无需素材 | 观点文、金句、政策解读、悬念标题 | make_covers_tiangong.py(换 --main) |
| ③ 实景风 | 工地/产品/设备/办公场景为底,叠半透明色块压字 | 你自己生成场景图;用户主动给了实拍图则优先用 | 项目纪实、产品落地、案例复盘 | make_cover_jihaoxing.py |
| ④ 品牌渐变风 | 蓝色对角渐变 + 品牌名/主标题居中 | 纯代码绘制,无需素材 | 产品介绍、能力平台、系列文章 | make_covers_tiangong.py |
| ⑤ 真实照片满铺 | 真实照片满铺 + 底部黑渐变承白字,一次出方形/微信首图/横版三张 | 你自己生成照片;用户主动给了照片则优先用 | 用户明确说「用实际的方形照片」「照片更有吸引力」时 | make_cover_photo.py |
★ 核心原则(用户明确纠正过):
①②③⑤ 四种需要图像的封面,全部由你自己生成(用图像生成工具),不要一上来就向用户要图、更不要因为没有素材就拒绝或降级。
只有当用户主动说"用我这张照片/这张实拍图"时,才用用户提供的图(此时用图生图方式处理)。
禁止:向用户索要素材当作必填前置条件;也禁止用与内容无关的网图/图库图凑数。
0.1 提问话术(可直接复制发给用户)
封面用哪种?(和正文排版风格分开选)
① 真人风 —— 人物为主视觉 + 大字标题(我会生成人物图)★人物/名人稿推荐
② 大字报 —— 纯色底 + 超大文字(最快,不需要素材)
③ 实景风 —— 场景/产品为主视觉 + 文字(我会生成场景图)
④ 品牌渐变 —— 蓝色渐变 + 品牌名(天工词元系列默认款)
⑤ 真实照片满铺 —— 真照片铺满,一次出方形/微信首图/横版(我会生成照片)
▸ 也可以直接发一张你想要的封面参考图,我照着做;
或者直接给我一张照片/实拍图,我用你的图来做。
▸ 你不需要提前准备素材——①②③⑤ 的底图我来生成(单张约 5–10 credits,会先告知)。
0.2 明确禁止
- ❌ 未问封面风格就套用模板 / 上次封面 / 自己挑一个
- ❌ 把「要用户提供照片」当成前置条件(缺图就自己生成,这是默认动作)
- ❌ 用与文章内容无关的网图、图库图凑数
- ❌ 在公众号后台点「发布」——这一步永远留给人工
- ❌ 批量删草稿、按标题模糊删草稿
§1 全程固定参数与本包资产
1.1 固定参数(照抄,不要改)
| 参数 | 值 |
|---|---|
| 公众号名称 | 易海工程AI(订阅号) |
| AppID | 环境变量 WECHAT_APPID(不在本包内写出) |
| AppSecret | 环境变量 WECHAT_SECRET(不在本包内写出) |
author 字段 |
必须显式传 易海工程AI(wechat_publisher 默认值是别人的号) |
need_open_comment |
1(开启留言,账号要求,不要覆盖为 0) |
only_fans_can_comment |
0(所有人可评论) |
| 标题长度 | ≤ 64 字(超了报 45003) |
| digest 摘要 | 50–60 字 |
| 封面尺寸 | 900 × 500(微信首图展示位实为 2.35:1 = 900×383,见 §5) |
> 🔐 凭据从环境变量取,本包不含任何明文 AppID / AppSecret。
> bash
> export WECHAT_APPID=wx**************** # Windows: set WECHAT_APPID=wx****************
> export WECHAT_SECRET=****************
>
> 真实值请向账号管理员索取,不要写进本包任何文件。脚本缺环境变量时会直接退出并提示。
1.2 本包自带的东西
易海推送技能包/
├── SKILL.md ← 本文件(唯一入口)
├── assets/
│ ├── 品牌头尾/ ⚠️ 兜底副本(权威在「易海排版技能包」,正常流程不用)
│ │ ├── _ref_head_only.html 品牌头部块(顶部 banner GIF + 装饰卡)
│ │ ├── _ref_tail_only.html 品牌尾部块(蓝线+署名+三篇历史文章链接+加星标 GIF)
│ │ ├── 开头.gif 头图 GIF(正文里用的是线上 URL,此份为备份)
│ │ └── 尾标.gif 尾图 GIF(同上)
│ └── 封面参考图/
│ ├── ref_cover_guide.jpg 品牌渐变的采样参考(900×383,取自线上草稿封面)
│ ├── ref_cover_guide1.png 备选参考
│ ├── ref_cover_guide3.png 备选参考
│ ├── ref_cover_guide4.png 备选参考
│ └── ref_photo_default.png ⑤ 真实照片满铺的默认底图(示例,可 --src 覆盖)
└── scripts/
├── wechat_publisher.py 微信 API 封装(token/建草稿/删草稿/传图/封面)
├── test_draft_flow.py 链路自检(建→查→删一条测试草稿)
├── fix_rewrap.py 套品牌头尾(⚠️ 兜底用;正常由排版包负责)
├── validate_gzh_html.py 合规校验(ERROR/WARNING 计数)
├── _chk.py 版式核查(左右边界/大块底色/字号分布)
├── wrap_preview.py 生成浏览器预览页
├── make_covers_tiangong.py ② 大字报 / ④ 品牌渐变(蓝渐变+居中大字)
├── make_cover_huangrenxun.py ① 真人风(--photo 传人物图;不传则渐变底)
├── make_cover_jihaoxing.py ③ 实景/样机风(--ref 传场景图)
├── make_cover_photo.py ⑤ 真实照片满铺(一次出方形/微信首图/横版三张)
├── upload_images_batch.py 正文配图批量换微信图床
├── verify_draft_images.py 核对线上草稿与本地正文是否一致
├── build_img_body.py 配图按章节嵌进正文(含图裸正文)
├── make_img_preview.py 本地自包含预览页
└── .wechat_cache/ token / 封面缓存(见 §8)
> 另有 assets/preview-template.html(预览页外壳)与本节同级的 5 个脚本,
> 是 wrap_preview.py / _chk.py / validate_gzh_html.py 的运行时依赖,不要删。
> 排版包内另有一份独立副本,两包各自自足、互不依赖。
1.3 执行前准备
# SK = 本 SKILL.md 所在目录。两个安装位置,用你实际加载的那个:
# 工作区镜像(唯一可信源):D:/中建三局/公众号易海工程/易海推送技能包
# 安装目录(供 agent 调用):C:/Users/mi/.workbuddy/skills/易海推送技能包
SK="<本 SKILL.md 所在目录>"
PY="C:/Users/mi/.workbuddy/binaries/python/envs/default/Scripts/python.exe"
> 下文所有命令都用这两个变量。用绝对路径,不要依赖当前目录。
§2 通道打通(首次 / 换机器 / 报 40164 时做)
2.1 ★ IP 白名单(最容易卡住,必须带用户做完)
为什么必须做:微信 API 只接受来自白名单 IP 的调用。
你要做的四步:
- 帮用户查出 IP——让用户在运行脚本的那台机器执行:
bash
curl ifconfig.me
⚠️ 填的是运行脚本那台机器的公网出口 IP,不是 192.168.x.x 内网地址(填内网无效)。
- 告诉用户去哪里配(原样念给用户):
1. 用「管理员微信」扫码登录 mp.weixin.qq.com
2. 左侧菜单「设置与开发」→「基本配置」
3. 找到「公众号开发信息」→「IP 白名单」→ 点「修改」
4. 一行填一个 IP,保存(需要管理员扫码确认)
-
配完立刻跑 §2.3 自检,用结果判定,不要只听用户说"配好了"。
-
失败时读报错:报
40164时错误信息里直接带着当前 IP,把它填进白名单即可。
四个坑(要主动提醒用户):
| 坑 | 说明 |
|---|---|
| 出口 IP 会变 | 家庭宽带/公司网络多为动态 IP,重启路由后可能变;报 40164 先怀疑它。本机最近一次为 113.56.181.173 |
| 换机器要重加 | 换一台电脑跑就要加那台的 IP;云服务器填服务器公网 IP |
| 不要填内网地址 | 192.168.x.x 之类无效 |
| 白名单有数量上限 | 不要一次堆一堆无关 IP |
2.2 凭据注意事项
凭据走环境变量 WECHAT_APPID / WECHAT_SECRET(见 §1.1),本包不存明文。
一旦被重置,旧值立即失效;若突然报 40125,第一件事就是确认它有没有被重置。
复制粘贴时带上换行或不可见字符也会导致 40125,粘完核对长度。
2.3 链路自检(每次推送前必须跑通)
export WECHAT_APPID=wx****************
export WECHAT_SECRET=****************
$PY "$SK/scripts/test_draft_flow.py"
也可显式传参:--appid <APPID> --secret <SECRET>(同样不要把真实值写进任何文件)。
它依次执行:清缓存 → 取 token → 建测试草稿 → 查询确认 → 删除 → 复查。
判定标准:5 步全部显示 [通过] 才算通。 任一失败 → 查 §8 错误码表,修好再重跑。
> 自检会建一条测试草稿再删掉,属正常动作,不要跳过。
§3 接收上游成品(只验收,不重做)
品牌头尾、署名、三篇历史链接是「固定格式」,由「易海排版技能包」负责套好。
本包接收的是成品 HTML,正常情况下不要再套一次、不要改任何一个字。
3.1 成品验收清单(吃到文件先跑这三条)
# ① <section> 开合平衡
grep -o "<section" <文件>.html | wc -l ; grep -o "</section>" <文件>.html | wc -l # 两者差值应为 0
# ② 作者签名在(重写正文最容易弄丢)
grep -c "我是工程AI团队,AI建筑领域的创业者。" <文件>.html # 应为 1
# ③ 头尾齐全:头部 banner GIF + 尾部蓝线/署名/三链接/星标 GIF
grep -c "data-imgfileid" <文件>.html # 头部 1 张 + 尾部星标 1 张,≥ 2
grep -c "mid=2247488216" <文件>.html # 更多AI场景
grep -c "mid=2247488428" <文件>.html # 全民营销
grep -c "mid=2247490062" <文件>.html # 工程AI事业团队产品与服务手册
grep -o "编辑 |.\{0,6\} \?.\{0,6\}校对 |.\{0,6\} \?.\{0,6\}审核丨.\{0,6\}" <文件>.html # 三行署名
署名格式铁律:编辑、校对与竖线「|」之间有一个半角空格;审核用全角「丨」,两侧无空格。
线上渲染即如此——竖线前的空隙是真实空格,竖线后没有空格;审核行两侧的大空隙只是「丨」字自带边距,
不要"修正"它。
3.2 头尾三条历史链接(验收时核对,不要改动、不要增删)
| 链接文字 | href |
|---|---|
| 更多AI场景 | …mid=2247488216… |
| 全民营销 | …mid=2247488428… |
| 工程AI事业团队产品与服务手册 | …mid=2247490062&sn=c660b0938467ec14145fe3093f857c06… |
三链接以线上草稿逐字核对过。将来要换,统一在排版包的尾块文件里改,本包不碰。
3.3 验收不过怎么办
| 情况 | 处置 |
|---|---|
| 头尾缺失 / 被套了两层 / 署名错 | 退回排版包重套。请用户重新跑一次排版包(fix_rewrap.py --credits "三人名"),本包不代做 |
<section> 不平衡,差值 -6 |
本地文件还套着旧版头尾,新尾块剥不掉。以线上最新草稿为准,不要强改本地快照;需重推就从草稿箱拉最新正文再套 |
| 是完整 HTML 文档 | 带 <!DOCTYPE>/<html> 外壳,是预览页不是推文产物,不要推它 |
| 全部合格 | 进入 §4 交付前校验 → §5 封面 → §6 推送 |
> 兜底(仅当用户明确要求"就在这推,别来回切包"时):本包 scripts/fix_rewrap.py 与
> assets/品牌头尾/ 保留了一份可用副本,可临时执行:
> bash
> $PY "$SK/scripts/fix_rewrap.py" --dir <文章目录> \
> --head "$SK/assets/品牌头尾/_ref_head_only.html" \
> --tail "$SK/assets/品牌头尾/_ref_tail_only.html" \
> --credits "雨函,Jerry,阿良"
>
> 输出 [OK] … diff=0 才算通过(--credits 与 --keep-credits 都不传 → exit 2,人名必须每次确认)。
>
> ⚠️ 头尾资产的唯一权威副本在「易海排版技能包」。本包这份只是兜底,改动一律以排版包为准;
> 若两边不一致,以排版包 assets/品牌头尾/ 覆盖本包。
§4 交付前校验
# ① 合规校验(硬门禁:ERROR=0 且 半角标点 WARNING=0)
$PY "$SK/scripts/validate_gzh_html.py" <套好头尾的文件.html>
# ② 边界/底色/字号实测(Playwright 390 视口 + 左右 20px 模拟微信容器)
$PY "$SK/scripts/_chk.py" <套好头尾的文件.html>
# 期望:左边界集合=[20] 右边界集合=[370]
# ③ 生成人工预览页(浏览器可「复制到公众号」)
$PY "$SK/scripts/wrap_preview.py" <套好头尾的文件.html>
判定标准:ERROR = 0 且 半角标点 WARNING = 0。 未清零不得推送。
校验器内置两条排版底线门禁:
- ❌ ERROR:全局容器两种版式都没声明(既无 margin:0 -16px,也无 padding:0 0 Npx)→ 页面会两侧留白
(2026-10-16 由"必须负 margin"放宽为"二选一":套头尾稿件用后者才对齐)
- ⚠️ WARNING:左右 padding > 12px(疑似留白);字号 < 15px 或 > 24px;正文缺 16px 主字号
4.1 _chk.py 期望输出
左边界集合=[20] 右边界集合=[370] ← 对齐通过
--- 大块有底色元素(>60x16)---
rgb(255, 255, 255) ×N ← 只有纯白
H2 ... fs=17px / P ... fs=16px / P ... fs=15px
> L:20 R:370 之外若出现 L:28 R:29(1×36px)或 L:20 R:28(8×8px)属品牌头装饰资产,正常。
> 出现任何 >60px 宽 的彩色底 → 说明头部装饰卡又被拆开未闭合,退回排版包排查
> (见「易海排版技能包」§4.6 头尾资产的已知病理)。
4.2 ★ 头尾资产的已知病理(2026-10-16 修复,勿回退|权威在排版包)
> 本包只保留此条目供验收时判断异常;实际修复动作在「易海排版技能包」。
> assets/品牌头尾/_ref_head_only.html 曾有三处缺陷,同时造成"整篇蓝底 + 两边不齐":
| # | 病理 | 修复 |
|---|---|---|
| 1 | 淡蓝装饰卡 background-color:rgba(95,156,239,0.06);padding:7px;width:100% 的 <section> 未闭合,把整篇正文包了进去 → 实测浅蓝区 430×3680(整篇高) |
改为 background-color:rgb(255,255,255);padding:0px |
| 2 | 同层另一块 flex:0 0 auto;padding:0 10px; → 头图比正文缩进 17px |
改为 flex:0 0 auto;padding:0px |
| 3 | 正文根容器 margin:0 -16px → 比头尾各宽 16px |
正文改 padding:0 0 56px(零负 margin) |
> .orig1016 备份存于 D:/中建三局/公众号易海工程/_技能拆分备份_20261019/
> —— 那份 .orig 只是留档对照,已被修复的版本才是正确版本,不要拿它覆盖在用的头尾。
> 头尾资产任何改动都要同步两包 + 两处安装位置:先在「易海排版技能包」改,
> 再 cp 覆盖本包兜底副本,并同步工作区镜像 + 安装目录。
4.3 预览页的两个坑(实测)
| 现象 | 原因 | 解法 |
|---|---|---|
| 预览页里品牌头/尾的 GIF 是空白 | 品牌头尾的 <img> 是微信懒加载写法,只有 data-src 没有 src,file:// 下不会触发加载 |
生成预览时把 data-src 同步补一份 src |
| 内联后 GIF 仍 broken | base64 内联时 MIME 写死成 image/png,GIF 解码失败 |
MIME 按扩展名判(.gif→image/gif、.jpg→image/jpeg) |
| 远程图全部空白 | 外链在 file:// / 预览面板下不加载 |
预览用的图先下载到本地再 base64 内联,做成自包含单文件 |
> 自包含预览页 ≈ wrap_preview.py + 「本地图 base64 内联」+ 「data-src 补 src」。
> 生成后用 Playwright 断言 loaded == total && broken == 0 才算过。
>
> ⚠️ 资产自检:预览页外壳依赖本包 assets/preview-template.html。
> 该文件曾在同步中丢失导致 FileNotFoundError。跑之前先确认存在;
> 缺失时从任意 xxx_预览.html 反推重建:取第一个 <section 之前为前缀、
> 最后一个 </section> 之后为后缀,<title> 换成 {{TITLE}} · 公众号排版预览,
> 正文位置换成 <!--GZH_CONTENT-->。
>
> 另:wrap_preview.py 只在纯正文片段上工作,产出页含 <script>,
> 不要拿预览页去跑 validate_gzh_html.py(会误报)。校验一律对 _预览.html 之外的原文件跑。
> 排版包内也有一份同样职责的 wrap_preview.py,两处模板各存一份,互不依赖。
§5 封面生成
5.0 ★ 规格与铁律
| 项 | 要求 |
|---|---|
| 规格 | 900 × 500 px,命名 01_cover_xxx.png、02_cover_xxx.png … |
| 主标题字号 | 建议 84–112px(真人/实景风可适当小,文字别压住人脸) |
| 副标题字号 | 30–38px |
| 文字对比度 | 白字必须压在暗部或加蒙版,禁止浅底浅字 |
| 文字安全区 | 四边留 ≥ 40px,不贴边 |
| 主体避让 | 人脸/关键信息不要被文字压住 |
| 通用实现 | Pillow 中若闭包绑定了外层 draw,img.copy() 后文字会画到副本外面(丢失且不报错)→ 每张图内部必须重新 ImageDraw.Draw(canvas) |
| 字体 | 中文用系统黑体(Windows C:/Windows/Fonts/msyhbd.ttc 微软雅黑粗体) |
| 量宽 | 文字必须先量宽再居中(draw.textlength),不要凭字数估算 |
| 导出 | 统一 resize((900, 500)),避免不同底图尺寸不一致 |
| ⛔ 文字 | 绝对不要把文字交给图像生成模型写(会写错字/乱码),标题文字一律由 Pillow 后压 |
| 底图 | 默认自己生成(①②③⑤);只有用户主动给图时才用用户的。不要向用户索要素材,也不要用无关网图凑数 |
5.1 生成底图的注意事项
| 项 | 要求 |
|---|---|
| 生成尺寸 | 用横版比例生成(如 1536x1024),再裁切缩放 |
| 画面预留 | prompt 里明确要求"画面左侧/下部留出大面积纯色或暗部空间用于压字",避免文字压主体 |
| 真人肖像 | 生成公众人物肖像时,优先用"剪影/背影/侧脸/虚化"等不依赖面部还原的构图,或明确按"形象示意图"处理,避免肖像权与还原度争议;用户提供真实照片时不受此限 |
| 成本告知 | 图像生成会消耗额外额度(单张约 5–10 credits),调用前先告知用户 |
| 文字 | 不要让模型在图里写字,标题一律 Pillow 后压 |
5.2 模板 A · ② 大字报 / ④ 品牌渐变(make_covers_tiangong.py)
# 批量(生成脚本内 SUBTITLES 列表里的三张)
$PY "$SK/scripts/make_covers_tiangong.py" --out <输出目录>
# 只生成单张(非天工词元题材必须换 --main 与 --prefix)
$PY "$SK/scripts/make_covers_tiangong.py" \
--tag 01 --main "文章主标题" --prefix <题材标识> --subtitle "本篇主旨" --out <输出目录>
| 参数 | 说明 |
|---|---|
--ref |
采样渐变的参考图,默认 assets/封面参考图/ref_cover_guide.jpg |
--out |
输出目录 |
--tag + --subtitle |
只出单张时用(--tag 是文件名前缀,如 05) |
--main |
主标题,默认「天工词元」;非天工词元题材必须换 |
--prefix |
输出文件名前缀,默认 tiangong → 01_cover_tiangong.png |
- 背景:从参考图采样对角渐变(左上深蓝 → 右下亮蓝),右下角加柔光
- 主标题 112px 白色加粗居中;副标题 34px 白色居中
- 只支持两行结构(主标题 + 副标题),没有第三行标签;需要三行请用模板 D
5.3 模板 B · ③ 实景/样机风(make_cover_jihaoxing.py)
# 底图来源二选一:
# a) 自己生成场景图 → 先用图像生成工具出图,再用 --ref 传入
# b) 用户主动给了实拍图 → 直接用用户的图
$PY "$SK/scripts/make_cover_jihaoxing.py" --ref <场景图或手机样机图> --out <输出目录>
- 深蓝横向渐变打底 + 对角光带 + 星点;右侧样机/实拍主体高铺满、左缘 alpha 渐变融合
- 主体图叠一层半透明色块压字,保证白字对比度
- 底图默认自己生成:用图像生成工具按文章题材出场景图(如"建筑工地塔吊黄昏""手机办公场景"),
存入本地后--ref传入;用户主动提供了实拍图则优先用用户的图 - 必须显式传
--ref:脚本不内置样机图,找不到会直接退出(有意设计,避免误用旧图) - 适用:单品推介、项目纪实、案例复盘
5.4 模板 C · ① 真人风(make_cover_huangrenxun.py)
# 底图来源二选一:
# a) 自己生成人物图 → 先用图像生成工具出图,再用 --photo 传入
# b) 用户主动给了人物照片 → 直接用用户的照片
$PY "$SK/scripts/make_cover_huangrenxun.py" \
--photo <人物图> --main "主标题" --sub "副标题" --tag "标签行" --out <输出目录/文件名.png>
| 参数 | 说明 |
|---|---|
--photo |
人物照片路径;不传则用蓝渐变底(无人物,退化成渐变款) |
--main / --sub / --tag |
主标题 / 副标题 / 标签行 |
--out |
输出路径(可含文件名,如 <目录>/真人.png) |
- 底图默认自己生成(人物半身/特写、构图留白、压暗处理),脚本再做裁切 → 渐变蒙版 → 左下角压白色大字标题
- 用户主动提供照片时,用图生图保特征处理(不重画成另一个人)
- 裁切规则:人物居中/偏右,文字区留在画面下部或左侧留白处,禁止文字压脸
- 如底图背景杂乱:先整体加一层
rgba(0,0,0,0.35)蒙版再压字,不要直接白字压亮背景 - 适用:名人稿、人物故事、创始人叙事、访谈
5.5 模板 D · ⑤ 真实照片满铺(make_cover_photo.py)
用户明确说「封面改成实际的方形/照片,更有吸引力」时,用这一套——不要再做
「左侧补纸面 + 右侧羽化贴图」那种拼贴,那会出来合成海报感,不是"实际照片"。
$PY "$SK/scripts/make_cover_photo.py" \
--src <底图> \
--main1 "主标题第1行" --main2 "主标题第2行" \
--sub "副标题" --tag1 "标签行①" --tag2 "标签行②" \
--out-dir <输出目录>
| 参数 | 说明 |
|---|---|
--src |
底图路径,默认 assets/封面参考图/ref_photo_default.png(内置示例) |
--main1 / --main2 |
主标题两行(单行标题把 --main2 传空串) |
--sub |
副标题 |
--tag1 |
标签行①(方形版与微信首图共用) |
--tag2 |
标签行②(仅横版用) |
--out-dir |
输出目录(不传会写进脚本目录,务必传) |
--cy-wx / --cy-sq / --cy-wd |
各尺寸裁切中心 y(0~1),决定取哪一带 |
一次出三张:
| 输出 | 规格 | 用途 |
|---|---|---|
01_cover_square_900.png |
900×900 方形 | 分享卡/Feed,字必须落居中安全区 y 258~641 |
01_cover_wechat_900x383.png |
2.35:1 微信首图 | ★ 上传这张最稳,微信不裁 |
01_cover_wide_900x500.png |
900×500 横版 | 备用,字落 2.35:1 可见区 y 58~441 |
_preview_square_wechat_cut.png |
方形按 2.35:1 居中裁切模拟 | 自检用,看方形被裁后文字是否还在 |
★★ 血泪教训:微信封面实际展示比例是 2.35:1,方形会被居中裁切
症状:上传 900×900 方形封面,线上预览/首图里副标与标签整个消失、主标被切一半。
根因:微信订阅号封面展示位是 2.35:1(900×383),上传非该比例的图会居中裁切。
结论与做法:
1. 优先上传 900×383(01_cover_wechat_900x383.png)——比例原生,微信不动刀
2. 若必须给方形(分享卡),方形内所有文字压在中段 y 258~641,这样被裁也完整
3. 原图比例不足 2.35:1 时只能裁上下,--cy-wx 决定取哪一带:先扫行剖面
(std 越大细节越足、亮% 越低越不透白)再定,别默认居中
★ 遮罩必调参数:bottom_scrim(canvas, top_y, alpha_max, gamma, floor)
- 遮罩偏浅会导致白字对比度不足。自检标准:压字区亮度 mean < 120、亮像素(>170) 占比 < 15%
- 首次用 top=28%, alpha=0.90 出图,压字区亮度 138.7 → 白字发灰
- 改 top=16%, alpha=0.95, floor=0.58, gamma=0.70 → 亮度降到 61.0,白字才立住
自检(脚本自带输出):每条文字量宽 vs 可用宽度(可用 = 图宽 - 104)、压字区亮度/亮像素占比、
以及方形版按 2.35:1 裁切模拟后的"文字占用 y 范围是否落在安全区"。三项都过才导出。
§6 推送草稿箱
6.1 推送代码模板(复制即用)
import sys, os
sys.path.insert(0, "<SK>/scripts")
import wechat_publisher
from wechat_publisher import WeChatPublisher
APPID = os.environ["WECHAT_APPID"] # 凭据走环境变量,不写明文
APPSECRET = os.environ["WECHAT_SECRET"]
wp = WeChatPublisher(APPID, APPSECRET)
wp.get_token()
# ⚠️ 关键:每次 create_draft 前必须清封面缓存,否则多篇全用第一张封面
if wechat_publisher.THUMB_FILE.exists():
wechat_publisher.THUMB_FILE.unlink()
html = open("<套好头尾的正文.html>", encoding="utf-8").read() # 从文件读,不要拼字符串
media_id = wp.create_draft(
title="文章标题(≤64 字)",
content=html, # 纯 <section> 片段
digest="摘要,50-60 字",
author="易海工程AI", # 必须显式传
thumb_path="<封面.png>", # 900×500 或 900×383
)
print(media_id)
6.2 create_draft 参数说明
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
title |
str | 必填 | ≤ 64 字,超出报 45003 |
content |
str | 必填 | 纯 <section> 片段 |
author |
str | 老李读书笔记 |
必须覆盖为 易海工程AI |
digest |
str | "" |
建议 50–60 字 |
thumb_path |
str | None |
封面路径;不传则自动生成纯色图 |
source_url |
str | "" |
原文链接,一般留空 |
show_cover |
int | 1 |
封面是否显示在正文前 |
need_open_comment |
int | 1 |
开启留言(账号要求,不要覆盖为 0) |
only_fans_can_comment |
int | 0 |
0=所有人可评论 |
6.3 推送前 checklist(逐项确认)
□ 封面风格已询问并选定(①真人/②大字报/③实景/④品牌渐变/⑤真实照片满铺)
□ 链路自检 5 步全 [通过]
□ **上游成品已验收**(§3.1):<section> diff=0 | 头尾齐全 | 署名三人名正确 | 三链接在
□ 签名「我是工程AI团队,AI建筑领域的创业者。」grep -c == 1
□ validate_gzh_html.py:ERROR=0 且半角标点 WARNING=0
□ _chk.py:边界 L=20 / R=370
□ 封面已生成,且风格与用户选定一致
□ 已删除 scripts/.wechat_cache/thumb_media_id.txt
□ author="易海工程AI"、need_open_comment=1
□ 文章用词已核对:正文无官网明文链接(用户要求双路径时除外)、无假按钮、无「点击进入天工先问」图
6.4 推送后必须验证
items = wp.list_drafts(0, 20) + wp.list_drafts(20, 20) # 分页!
> 返回结构:每个元素是 {media_id, content:{news_item:[{title, author, digest, thumb_media_id}]}}。
> 标题/作者/封面要从 news_item[0] 里取,不要在 content 层找(取不到会误报"未找到")。
> ⚠️ 编码坑(核对线上正文时必踩):draft/get 返回的 content / title 中文是
> latin-1 包装的 UTF-8,直接拿中文字符串做 in 判断会全部假阴性,
> 误判成"线上内容没改"。核对前先还原:
>
> python
> def fix(s):
> try:
> return s.encode("latin-1").decode("utf-8")
> except Exception:
> return s
>
>
> draft/batchget(list_drafts)返回的标题不受影响,只有 draft/get 需要还原。
> ⚠️ list_drafts 单页上限 20 条。只调 list_drafts(0,20) 会误判总数,统计必须分页累加。
核对三件事:标题正确 / 作者是易海工程AI / 封面是本篇封面。
★ draft/get 报 40007 不等于推送失败(2026-10-17 踩到)
事后回查草稿箱时,若拿旧 media_id 调 draft/get 返回
40007 invalid media_id,不要直接判定"草稿没推上去/推丢了"。先做两步取证:
# 1) 全量分页列草稿,看总数与最新条目
items, off = [], 0
while True:
b = wp.list_drafts(off, 20)
if not b: break
items += b; off += 20
print(len(items))
rows = sorted(((it.get("update_time") or 0,
((it.get("content") or {}).get("news_item") or [{}])[0].get("title", ""),
it.get("media_id")) for it in items), reverse=True)
for r in rows[:8]: print(r)
# 2) 按标题判断该篇是否还在草稿箱
# 不在 → 大概率已被用户手动发布(订阅号 freepublish 接口未授权,48001,查不到已发布列表)
判定规则:草稿箱里找不到该标题 → 该篇已不在草稿箱(通常为用户已发布),
不是推送失败;此时应如实告知用户「草稿已不在草稿箱,疑为已发布,请在后台确认」,
不要再盲推一次(会重复进草稿箱)。
> 另:freepublish/batchget 对未开通发布能力的订阅号返回 48001 api unauthorized,
> 不要依赖它判断是否已发布;用工作日志里的 media_id + 草稿箱分页列表做交叉核对即可。
★ 封面必须额外验证「是否被微信裁切」(2026-10-17 事故后必做)
只看「封面是不是本篇那张」不够——微信会把非 2.35:1 的封面居中裁切,
上线后发现图对但文字被裁掉了。所以还要做尺寸 + 剖面比对:
import io, json, re, requests
import numpy as np
from PIL import Image
r = requests.post(f"https://api.weixin.qq.com/cgi-bin/draft/get?access_token={tok}",
data=json.dumps({"media_id": MID}).encode(),
headers={"Content-Type": "application/json"})
ni = json.loads(r.content.decode("latin-1").encode("latin-1").decode("utf-8"))["news_item"][0]
url = ni["thumb_url"] # 下载线上封面
# (用 curl -L 落盘为 _online_thumb.jpg)
o = Image.open("_online_thumb.jpg").convert("L").resize((900, 383))
l = Image.open("01_cover_wechat_900x383.png").convert("L")
r_ = np.corrcoef(o.mean(axis=1), l.mean(axis=1))[0, 1]
print("线上尺寸 =", Image.open("_online_thumb.jpg").size, "| 行剖面 r =", round(r_, 4))
# 判定:尺寸 ≈ 上传尺寸 且 r > 0.99 → 未被裁切,封面可用
# r 明显偏低 → 微信裁切了,改用 900x383 版重推
> 判定标准:线上尺寸 ≈ 上传尺寸 且行剖面 r > 0.99 → 封面完整。
> r 偏低说明被裁,换 01_cover_wechat_900x383.png 重推。
6.5 正文配图上传
url = wp.upload_image("<本地图片.png>") # 返回 url 写进 <img data-src="...">
> ⚠️ 外链图(mmbiz.qpic.cn 等)微信不显示。正文里要放图,必须先把本地图
> upload_image 换到素材库、拿到 URL 再替换 data-src,否则线上是空白。
> media/uploadimg 返回的是图文内图床 URL(可直接嵌 <img>),与封面用的
> media/upload(拿 thumb_media_id)不是同一个接口,别混。
批量换图的标准做法(可断点续传,重推不必重传):
# 1) 扫正文里所有本地图 data-src → 去重
# 2) 逐张 wp.upload_image(),把 {本地相对路径: 微信URL} 落盘到 img_url_map.json
# 3) 用映射一次性替换 data-src 与 src;替换后断言 本地图残留 == 0
> 映射文件落盘是关键:中途失败可续跑;重推时自动跳过已上传的(微信图床 URL 长期有效)。
> 推完必须核对:线上图片数 == 本地图片数,且线上 data-src 全部是 mmbiz.qpic.cn。
> 现成脚本:scripts/upload_images_batch.py + scripts/verify_draft_images.py。
正文配图写法(保持统一极简不破版):
<section style="margin:0 0 8px 0;text-align:center;">
<img src="…" data-src="…" data-type="png" data-w="1080" data-ratio="0.7"
style="width:100%;height:auto;display:block;" type="block" />
</section>
<p style="…;font-size:15px;…;text-align:center;"><span leaf="">图注</span></p>
> 图片独立 section、宽 100%,与正文共用同一条左右边界;图注 15px #55606B;
> 不新增任何色块。套完头尾用 _chk.py 复核 IMG 也在 L=20/R=370。
6.6 替换已有草稿(严格流程)
1. list_drafts 分页找到目标 → 记录 media_id(并核对标题确为目标)
2. delete_draft(该 media_id) ← 只删这一个
3. 推送新版
4. list_drafts 重新验证
铁律:只删除明确指定的 media_id,绝不批量删、绝不按标题模糊删。
草稿箱历史草稿很多,删了无法恢复。不碰任何不是本次创建的草稿。
§7 三条铁律
- 只推草稿箱,不发布。 点「发布」永远由人工在后台完成。
- 删除操作只针对明确指定的
media_id,绝不批量删、绝不按标题模糊删。 - 不碰任何不是本次创建的草稿;草稿箱历史草稿很多,删了无法恢复。
> 账号边界:本工作区只服务「易海工程AI」。不得改动其他矩阵号(「篮球后视镜」「动一下」
> 「今日天气真的好」等)的任何草稿。遇冲突先取证核对,无改动则沿用原状继续推进。
§8 错误码速查与缓存
| 错误码 | 含义 | 处置 |
|---|---|---|
40125 |
AppSecret 无效或被重置 | 检查多余字符/换行;确认没被重置 |
40164 |
服务器 IP 不在白名单 | 报错信息中会给出当前 IP → 引导用户加入白名单(§2.1) |
40013 |
AppID 错误 | 核对 AppID |
45003 |
标题超过 64 字 | 缩短标题(超 64 字会报这个) |
40007 |
invalid media_id | ≠ 推送失败,见 §6.4 |
48001 |
api unauthorized | freepublish/batchget 对未开通发布能力的订阅号会返回,不要依赖它 |
45004 / 45009 |
接口调用限额 | 等待后重试,避免高频调用 |
| 封面串号 | 多篇共用同一张封面 | 每次推送前删 .wechat_cache/thumb_media_id.txt |
缓存目录:scripts/.wechat_cache/(跟随脚本所在目录)。
token.json 更换公众号/重置凭据后删;thumb_media_id.txt 每篇推送前都删。
> 🔐 token.json 里只存 appid 指纹(appid_fp = sha256 前 16 位)和 access_token,不存 appid 明文——
> 保证「三包内无明文凭据」的扫描能过。appid 指纹不匹配即视为无缓存,会重新取 token。
§9 收尾:向用户汇报的格式
✅ 已推送到草稿箱(未发布,请在后台确认后手动发布)
- 标题:<标题>
- 封面风格:<①真人 / ②大字报 / ③实景 / ④品牌渐变 / ⑤真实照片满铺>
- 上游成品:头尾已由排版包套好 ✅(署名:编辑 |x / 校对 |x / 审核丨x,已验收)
- 封面:<封面文件名>(已用行剖面 r=… 验证未被裁切)
- media_id:<media_id>
- 草稿箱当前总条数:<分页统计结果>
- 预览页:<xxx_预览.html 路径>
请在公众号后台 → 草稿箱 中预览并发布。
附录 · 执行总流程(速查)
上游:「易海排版技能包」产出成品 HTML(正文 + 品牌头尾 + 署名 + 三链接,diff=0)
↓
§0 问用户封面风格(①真人/②大字报/③实景/④品牌渐变/⑤真实照片满铺)——必须问,不许默认
↓
§2 链路自检 5 步全 [通过]($SK/scripts/test_draft_flow.py)
↓ 失败 → §2.1 IP 白名单 / §8 错误码
§3 验收上游成品(§3.1 三条 grep)——不合格退回排版包,本包不代套头尾
↓
§4 validate_gzh_html.py(ERROR=0、半角 WARNING=0)+ _chk.py(L=20/R=370)+ 生成预览页
↓
§5 按用户选定的封面风格生成封面
↓ ①②③⑤ 底图自己生成(用图像生成工具,先告知额度消耗);用户主动给了图才用用户的
↓ 上传微信首图优先用 900×383(微信不裁)
§6 清 thumb 缓存 → create_draft(author=易海工程AI、need_open_comment=1)
↓ 分页验证标题/作者/封面 + 行剖面验证封面未被裁
§9 向用户汇报,提示去后台手动发布