多平台发布编排(跨平台状态机与质量门禁)
🚧 骨架状态(2026-10-01):本文已写入三条经过实战验证的门禁规则 (回读校验、批次串行、品类匹配),但平台适配层与完整实现尚未补齐。 见文末「待补」。
这个技能解决什么
多平台分发真正难的不是「调发一篇文章」,而是重复、遗漏、与「以为成功其实没成」:
| 症状 | 真实原因 |
|---|---|
| 脚本报「成功 37 条」,远端一条没变 | 判成功只看「按钮点中了」,没有回读 |
| 重跑一遍,37 条全部重发 | 没先 sync 既有状态,幂等判据缺失 |
| 跑 19 分钟一条没删 | 目标列表分页渲染,只处理了首屏 |
| 内容被判违规、笔记被下架 | 发布前没有平台合规预检 |
这四类都不是平台 API 的问题,是编排层缺门禁。本技能只解决这四类。
核心模型:落盘清单状态机
pending → building → draft_saved → published
↑ │ │
└─────────┴────────────┘(失败回到 pending,attempt+1)
failed(重试耗尽 / 需人工介入)
| 状态 | 含义 | 是否算完成 |
|---|---|---|
pending |
排队中 | ❌ |
building |
正在写 | ❌ 崩在这里必须能续跑 |
draft_saved |
远端已确认存草稿 | ✅ |
published |
正式发表 | ✅ |
failed |
失败 | ❌ 需 retry |
⚠ 关键约束
building不算完成 —— 进程崩在building时,重跑必须能接着走, 否则用户永远看不到那条。设计成幂等重入而不是从头再来。- 写盘用
tmp+os.replace原子替换 —— 状态文件被截断等于丢记录, 用户会对着一个「少一条」的清单干瞪眼。 - 清单必须入库(不能只放
out/) —— 远端只存在草稿箱的平台上, 本地清单是唯一事实来源。丢了只能人肉翻四个平台草稿箱重建。 ⚠ 用.gitignore排除机器产物时,务必确认清单文件没被一起排除。
幂等:判据是「清单 OR 平台状态文件」
已完成 ⟺ 清单里状态已完成 OR 平台侧状态文件记录已完成
两者都查,任一为已完成就跳过
⚠ 首次使用必须先 sync,把既有记录灌进清单。否则清单是空的,
37 条会被当成全新内容全部重发一遍。
反向也要做:平台状态文件里有、清单里没有的(比如清单被覆盖过), 要回写进清单 —— 否则会重复发。
门禁一:⚠ 点按钮型操作必须回读校验
这是本技能最重要的一条。
判「保存成功」不能只看「保存按钮点中了」。
实战(2026-09-30):某平台的
publish_one点完「保存为草稿」就return True,日志打印「成功」。实测远端正文一字未变 —— 整轮 37 条全部「成功」,但用户看到的还是旧排版。 根因:编辑器内容没提交到内部文档状态,点按钮只是触发了保存动作, 而保存的是「上一次的状态」。
必须做的
点保存
↓
回读远端(列表接口 / 重新打开草稿 / API 拉详情)
↓
比对关键内容(正文纯文本 / summary 前缀 / mtime)
↓
一致 → 才写 draft_saved
不一致 → 判失败,可重试
校验用纯文本比,别比字节 —— 本地与远端的 HTML 结构往往不同
(远程编辑器会重排标签、补 、删空属性),字节比对永远不一致。
⚠ 别拿 warn 当失败结论:刷新类操作常报「超时 / 连接断开」但实际成功。 必须回读核实,而不是看日志级别。
⚠ 回读要挑对通道。小红书草稿是浏览器本地的,列表 UI 只有标题+时间,
拿它回读等于只验了标题 —— 正文和图片数量根本没验。要验就得进 IndexedDB
(见 cn-social-platform-adapters)。
⚠ 回读的匹配键要用稳定标识,别用标题。标题会随数据修正而变, 按标题反查会「一个都匹配不上」,而匹配失败常被当成「无需更新」—— 这会伪造出一份「全部正常」的结论。用篇号/固定 id 这类不会变的标识。
门禁二:⚠ 批处理不要接 | tail -N
批处理脚本输出是块缓冲的,| tail -N 会一直攒着不发,
表现为「跑19 分钟一条输出都没有」,用户以为卡死。
❌ python publish.py --all | tail -20 # 全程零输出
✅ python -u publish.py --all # -u 行缓冲,逐条可见
✅ python -u publish.py --all | tee run.log # 既实时可见又留档
配套要求:脚本自己要 flush=True 逐条打印 + 打印单条/累计耗时。
这个坑踩过 3 次 —— 用户看不到进度就会打断你。
门禁三:⚠ 发布前做平台合规预检
不同平台的禁售类目差别极大,同一段内容 A 平台过、B 平台直接下架。
发布前必须过一遍目标平台的类目红线,见 cn-social-platform-adapters
的「合规预检」章节。
通用红线(跨平台都适用的)
| 红线 | 典型触发词 |
|---|---|
| 引流 / 导流 | 微信号、QQ、加群、私信、评论区引导、联系方式 |
| 灰产工具 | 破解、刷量、批量注册、接码、代实名、虚拟号 |
| 规避平台规则 | 「绕过风控」「免实名」「不需验证」 |
| 非正规服务 | 账号租售、接码平台、外挂、脚本批量 |
⚠ 红线是「语义级」的,不是关键词级。正文里写「面向 XX 群体提供服务」 被判成导流,即使没有任何联系方式 —— 因为它暗示了「你可以来找我」。 改文案要改整段的意图表达,不是替换个别词。
⭐ 覆盖层:只救一个平台时,绝不改共用数据源
实战(2026-10-01):一篇案例被小红书判违规,但同一份数据还要发 公众号/头条/B站。直接改源数据会牵连另外三个平台 —— 公众号那35 篇 得重建 HTML 并重跑 refresh(每条 4–5 分钟,全量要几小时)。
正确做法:加平台专属覆盖层。
data/<platform>_note_overrides.json ← 覆盖表(只存差异,不是全量拷贝)
↓ build_<platform>() 里读,优先级高于规则生成
out/<platform>/<case>/note.json+卡片
覆盖范围要包含所有会被渲染出去的文字:标题、正文、标签、 以及图片卡片上的每一个字段。
⚠ 只改正文是错的 —— 卡片是把同一批文案印到图片上, 不覆盖卡片等于把违规文案重新印了一遍。图片违规和文本违规是分开判的。
判「这个平台真的干净了」的测试:把最终产物(标题+正文+卡片文字) 拼起来跑禁售词表,要求零命中。卡片文字要单独抽出来参与检查 —— 只查正文会漏掉图片上的雷。
⚠ 台账/队列里禁止硬编码案例 id
skip = set(published) | set(drafted) | {"某个案例"} 这种写法看起来无害,
实际后果是那个案例永久无法再被队列选中,只能手动 --case 发。
临时跳过应该是「临时改代码 + 记得改回来」,而不是留一个永久陷阱。
测试要钉死这条不变量:待发判定只允许看那两个台账。
门禁四:⚠ 内容品类必须与骨架匹配
实战(2026-10-01):用户要发一篇开源工具推广,被塞进了 「海外小生意案例」的骨架。用户原话:「1,2,3,4 这几个都是为了拆解海外 文章所需要的,但这个是 free llm 的推广和使用,不需要这些」。
那 1/2/3/4 是案例骨架的栏目:钱从哪来/ 它到底做到多大 / 能搬走的部分 / 一个人能不能做。对一篇推广工具的文章,它们全是错的:
| 案例栏目 | 用在工具推广上会怎样 |
|---|---|
| 钱从哪来 | 工具不收费 → 逼你编一段「其实它不赚钱」 |
| 它到底做到多大 | 读者去找收入/客户数 → 找不到,文章就软了 |
| 能搬走的部分 | 读者不是来抄生意��,是来用这个工具的 |
| 一个人能不能做 | 商业可行性评分,与推广无关 |
根因不是文案写不好,是品类不匹配。 改措辞救不回来 —— 「它不赚钱」这句话不管怎么润色都还是「钱从哪来」这一栏。
正确做法:加 content_kind 让渲染层分流
data/<id>.json里声明 content_kind: "tool"
↓
build_sections() 按 kind 走不同骨架
"case" → 它是干什么的 / 钱从哪来 / 做到多大 / 为什么能成 / 能搬走什么
"tool" → 它是什么 / 怎么用 / 实测结果 / 适合谁 / 它不做什么 / 判断
配套原则:
- 按字段缺省实现,不改既有行为 —— 新分支的入口条件是
content_kind == "tool",既有 39 条不填该字段,渲染结果逐字节不变 (用git diff --stat -- out/验证,为空才算合格)。 - 品类专属字段要用新名字 —— 工具类用
how_to_use/who_is_it_for/limits,不要复用how_it_makes_money/playbook。 复用等于把新内容塞进旧栏目,骨架分流就白做了。 - ⚠ 栏目身份也要换 —— 「拆解海外 · 第 N 篇」是栏目名,
工具推广挂在它下面会让读者以为这是第 42 个小生意案例。
栏目标签(
series_tag)、页脚署名(series_footer)、 栏目简介(footer_text)都要能覆盖。 - ⚠ 封面版式文案同样是骨架的一部分 —— 默认封面是「金额 + 产品名」, 工具类会渲染成「84 个模型里 11 / 探测脚本名」(像在报营收, 而且句子被截断、语义不完整)。要能整体指定两行。
检测方法:发布前扫栏目残留
CASE_ONLY = ["钱从哪来", "它到底做到多大", "能搬走的部分",
"一个人能不能做", "能不能搬回国内"]
SERIES = ["拆解海外", "万物解释者", "CASE STUDY", "案例库"]
# 对四个平台的**正文层产物**逐个扫,命中即失败
⚠ 要扫「所有会被渲染出去的文字」,包括封面/卡片上的字。
CASE STUDY · 海外小生意 这类版式文案常被硬编码在模板里,
按字段取的覆盖表碰不到它 —— 得单独给版式字段开覆盖口。
图片违规与文本违规是分开判的,只改正文等于把违规文案重新印一遍。
批量必须串行
⚠ 并发抢同一个浏览器/CDP 会批量报错(ConnectionAborted 10053 /
TimeoutError),甚至挤掉已经打开的标签页。实测批量发版串行 70s/条,
并发反而全部失败。
✅ 单线程串行,逐条处理
❌ 线程池 / asyncio 并发调同一CDP
单条也要有界:失败重试有上限,超了就归failed 等人工,
不要无限重试卡住整批。
对账:先做,别先动手
发现「数量对不上」时,先对账再操作。按标题反查、把差异分成 5 类:
| 类别 | 含义 | 处置 |
|---|---|---|
| 远端有 / 台账无 | 之前发了没记 | 回写台账,别重发 |
| 台账有 / 远端无 | 记了但没发 | 才需要补发 |
| 两边都有 | 正常 | 跳过 |
| 远端重复 | 发了多次 | 按标题去重,保留信息更全的 |
| 孤儿草稿 | 远端有、内容不在本次范围 | 人工确认 |
实战(2026-10-01):对账发现「台账多 2 条」,看着像要补发, 细查发现是
purge删掉的记录没从台账移除 —— 若是直接补发就会重复发布。 ⚠mark_drafted类台账只增不减,对账时必须反向清理 (正解 = 台账 ∩ 远端,只留远端确实存在的)。
调查结论先摆出来,不要替用户做决定
平台接口能力(能不能 API 直发、有没有删除接口)必须先核实再动手, 不要凭印象回答。核实途径:官方开放平台文档页 → 实际草稿列表回读 → 网络资料交叉验证。
实战:某平台文档站已把内容发布 API 重定向到另一套开放平台, 网页搜索出来的多篇「如何用 API 发文」文章是 AI 生成的幻觉 (接口路径、示例代码全为空)。不能作为技术依据。
待补(等批量操作结束后补齐)
- 清单文件的具体 schema 与读写实现
-
sync/queue/run/status/retry/verify六个命令 - 与
cn-social-platform-adapters的适配器接口定义 - 断点续传与并发安全的实测验证
- 配套脚本(清单管理 + 对账)
- 门禁三的合规预检从「警告」升级成「硬阻断」的开关设计
(当前实现只打印警告仍继续渲染,需支持
--strict直接拒绝出图) - 门禁四的品类残留扫描做成脚本(现在是一次性python -c,
应固化成
check_content_kind.py --id X --kind tool) -
content_kind支持更多形态(目前只有case默认 /tool两种)
关联
cn-social-platform-adapters—— 各平台具体接口、认证门槛、 编辑器陷阱、合规红线明细(待建)web-console-cdp-upload—— 通用 CDP 能力(复用登录态、给 file input 塞文件、 富文本编辑器 CSS 实测)
