SSkill 广场
Skill 广场/ 文档/ wp-publish

wp-publish

WordPress 文章发布

下载技能包
精选 自研 文档 v1.0.0
15 次下载热度 24更新 2026-09-08 5 个文件 18.4 KB MIT 许可
描述

Publish an article (Markdown or DingTalk doc) to a WordPress blog via the REST API + Application Passwords, including downloading source images, re-hosting them in the WP media library, converting Markdown to HTML, and creating a draft post. Also supports an optional block-level redaction of the source DingTalk doc before publishing. Use when the user asks to upload/publish/发布 an article or 文章 to their WordPress blog (e.g. yushuoxin.top), migrate a DingTalk doc to WordPress, re-host doc images onto a blog, or redact/脱敏 a DingTalk doc block by block without overwriting manual edits.

SKILL.md 正文

WordPress 文章发布 (wp-publish)

把一篇文章(本地 Markdown 或钉钉文档)发布到个人 WordPress 博客。核心难点已在本 Skill 固化:内网域名拦截绕行图片重托管朴素固定链接的 REST 路由嵌套代码围栏的 Markdown 转换

前置:读取配置

敏感凭证不写在本文件里,统一放在项目根的 0-wp.env(见文末格式)。使用前先读取其中的 WP_SITE_URL / WP_USERNAME / WP_APP_PASSWORD

鉴权用 Application Password(后台 用户→个人资料→应用程序密码 生成),Basic Auth:btoa("用户名:应用密码"),密码含空格也照原样带上。

关键环境事实(决定方案)

  1. 朴素固定链接:站点若 /wp-json/ 报 404,说明固定链接是"朴素"模式,REST 必须走 /?rest_route=/wp/v2/... 形式。用 GET /?rest_route=/ 返回 JSON 即可确认。
  2. 本机可能无法直连站点:若命令行 curl 被企业网络策略/代理拦截(常见于 307 跳转到管控页),则所有对站点的 API 调用都走 browser-use 的页面内 fetch(),而不是 curl。

完整流程

复制此清单跟踪进度:

- [ ] 1. 读取 0-wp.env 凭证
- [ ] 2. 取文章内容(钉钉 dws doc read / 本地 md)
- [ ] 3. 下载文章内所有图片到 /tmp/wp_imgs/
- [ ] 4. 浏览器放行域名 + 验证 REST 鉴权 (users/me)
- [ ] 5. 逐张上传图片到媒体库,按顺序收集 source_url
- [ ] 6. 构建 HTML:替换图片地址 + Markdown→HTML
- [ ] 7. 创建 draft 文章 + 设置分类
- [ ] 8. 校验草稿 + 清理临时媒体/本地文件

1-3. 取内容与下载图片(命令行,可直连钉钉/OSS)

  • 钉钉文档:在 ~/.qoder/skills/dws 目录 dws doc read --node <id> --format json,取 .markdown
  • 用正则 ![[^]]*](([^)]+)) 抽出图片 URL(注意去掉 markdown 空标题产生的尾部 " "url.split(' ')[0])。
  • 文档出现顺序下载为 img_01.png … img_NN.png(顺序即后续定位依据)。钉钉签名图 URL 会过期,尽快下载。

4. 放行域名 + 验证鉴权(browser-use)

  1. navigate_page${WP_SITE_URL}/?rest_route=/。若被企业网络策略拦到放行页,先在浏览器里完成放行,回到本步重试。
  2. navigate_page${WP_SITE_URL}/(HTML 页,便于注入 DOM)。
  3. evaluate_script 验证鉴权:
   async () => {
     const auth = 'Basic ' + btoa(USER + ':' + PW);
     const r = await fetch('/?rest_route=/wp/v2/users/me&context=edit', { headers: { Authorization: auth } });
     return { status: r.status, body: (await r.text()).slice(0,200) };
   }

期望 200 且含 "roles":["administrator"]

5. 上传图片(browser-use,见 reference.md 详解)

思路:页面内注入一个 <input type=file id=qup>,用 upload_file 把本地图片塞进去,再 evaluate_scriptfiles[0]fetch() POST 到 /?rest_route=/wp/v2/media

  • 注入后先精简页面document.body.innerHTML='' 只留 #qup),让后续 take_snapshot 极小、省 token。
  • 严格配对:每次 upload_file必须紧跟一次上传 fetch(),否则下一次 upload_file 会覆盖文件导致漏传。
  • 每张记录 source_url(存到 window.__up 便于最后汇总)。上传返回大图可能带 -scaled 后缀,以返回的 source_url 为准。

6. 构建 HTML(命令行)

钉钉文档一律走 JSONML,不要用 markdown。scripts/jsonml_to_html.py(无第三方依赖):

# 先抽块结构(code 是显式节点,自带 syntax 与完整 code 文本)
dws doc read --node "<URL>" --content-format jsonml --output doc.jsonml.json --format json
python3 scripts/jsonml_to_html.py doc.jsonml.json wp_urls.txt out.html

为何必须用 JSONMLdws doc read 默认导出的 markdown 里,代码块用三反引号表示。 钉钉常见写法是把一整段 AI 输出放进 `markdown 块,内部又含 `bash # 标题—— 三反引号无法表达嵌套,任何解析器到这里都只能猜,极易把代码块内的标题渲染成文章标题。 JSONML 里 code 是显式节点,边界不存在歧义,还能拿到语言标注。 实测对比(post 2437):JSONML 取得 21 标题 / 7 代码块 / 8 表格;markdown 路径跑出 111 标题 / 53 代码块 / 30 表格。

校验:脚本会打印 headings/pre/tables/imgs/lists/callouts 计数,逐项对照 JSONML 中同名节点数应完全相等; 图片数与 URL 数不一致会告警。建议再做一次文本完整性对比(去空白后源文本字符数 == HTML 文本字符数)。

退路:本地 Markdown 文件

源头就是 .md 文件(没有块结构可抽)时才用 scripts/build_post.py(依赖 pip3 install markdown --break-system-packages):

python3 scripts/build_post.py <doc.md> <wp_urls.txt> <out.html>

它做四件事:① 剥离首个 H1 作为标题(并把 + 还原为 +);② 按顺序把第 N 张图片 URL 替换为 wp_urls 第 N 行③ 转 HTMLextensions=['tables','fenced_code','sane_lists']);④ 标题泄漏校验

嵌套围栏陷阱(已修,务必看):钉钉文档常把一整段 AI 输出放进 `markdown 代码块,内部又含 `bash 。 三反引号无法表达嵌套(CommonMark 下外层会在第一个 `` 处提前结束),所以脚本默认把外层围栏升级为 4 个反引号, 让整段(含内部 ``bash # 标题)完整保留在一个代码块里。

早期版本的做法是反的——直接删掉外层围栏(--unwrap-outer-md)让内部表格能渲染,后果是代码块里的 #/## 变成了文章一级/二级标题, 且旧的围栏配对逻辑(带语言=开、裸 ` =闭)遇到无语言代码块会整体错位,导致 <pre> 与标题交替错乱。 旧行为已降为可选开关,除非文档里那段 `markdown 本来就是想当正文渲染的,否则不要用

标题泄漏校验:转完会对比「HTML 里的 <h*> 数」与「原文围栏外的标题数」,前者更多就报错退出(exit 2)并列出可疑标题。 确认无误时才加 --allow-heading-leak 放行。发布前看一眼这行统计headings=实际/预期

校验:<img>/wp-content/uploads 计数应等于图片数,alidocs 计数应为 0,headings= 不应超出预期值。

7. 创建草稿(browser-use)

大段 HTML 不要直接塞进 evaluate_script(易踩转义)。用中转文件:把 HTML 存成 .txt(WP 允许 text/plain),用图片相同方式上传到媒体库拿到 URL,页面内 fetch() 取回文本作为正文,再 POST 创建文章:

const body = { title, content, status: 'draft', categories: [CAT_ID], comment_status: 'closed' };
await fetch('/?rest_route=/wp/v2/posts', { method:'POST',
  headers:{ Authorization: auth, 'Content-Type':'application/json' }, body: JSON.stringify(body) });

分类 ID 先 GET /?rest_route=/wp/v2/categories&per_page=100 查询。(yushuoxin.top 现有:技术博客=8、学习笔记=2、生活随记=18、未分类=1。)

8. 校验与清理

  • GET /?rest_route=/wp/v2/posts/<id>&context=edit 确认 status=draftcontent.raw<img>/<table> 数量正确、alidocs 为 0。
  • 删除中转 .txt 媒体:DELETE 常被 405 拦,用 POST + 头 X-HTTP-Method-Override: DELETE/?rest_route=/wp/v2/media/<id>&force=true
  • 删除本地 /tmp/wp_imgs、临时脚本产物。
  • 给用户后台编辑链接:${WP_SITE_URL}/wp-admin/post.php?post=<id>&action=edit(用户已登录后台,可预览后手动点「发布」)。

可选:发布前给钉钉原文脱敏(块级编辑)

若源文是钉钉文档、且线上版本已被用户手动插入截图/改动,不能整体覆盖(会丢改动)。用块级精准脱敏只改命中敏感信息的块。适用场景:账号 ID、AK/SK、余额/授信额度、真实消费金额、安全组 ID、手机号等。

scripts/redact_doc.py(需 dws 可执行,建议 cd ~/.qoder/skills/dws):

# 1) 先试运行,只看命中哪些块,不改动
python3 scripts/redact_doc.py --node <文档节点ID> --rules rules.json --dry-run
# 2) 确认无误后正式脱敏
python3 scripts/redact_doc.py --node <文档节点ID> --rules rules.json

rules.json 是「正则→替换」数组(按序应用,replace 支持 1 反向引用):

[
  {"pattern": "(余额|授信额度)[:: ]*¥[\d,\.]+", "replace": "\1:¥**,***.**"},
  {"pattern": "¥\s*[\d,]+\.\d{2}", "replace": "¥**,***.**"},
  {"pattern": "\b\d{16}\b", "replace": "<ACCOUNT_ID>"},
  {"pattern": "LTAI[A-Za-z0-9]+", "replace": "<YOUR_ACCESS_KEY_ID>"}
]

关键坑(务必知道):文本/代码内容的真正源头在子节点 leaf["span",{"data-type":"leaf"},"文字"]),不是元素属性。代码块虽有 code 属性,但 --fix-jsonml 会从 leaf 重新生成 code——只改属性无效。脚本已对 leaf 和 code 属性同时改写。更多见 reference.md

脱敏后务必回读校验dws doc read --node <id> --format json,确认真实数值已变为掩码、且截图/编号未受影响。

安全须知

  • 永远从 0-wp.env 读凭证,不要把应用密码写进文章、日志、Skill 或提交物
  • 发布前确认文章已脱敏(账号 ID、余额、密钥等)。可用上面的块级脱敏处理钉钉原文,本地 Markdown 直接改文件即可。

更多细节

  • 逐图上传的 browser-use 调用范式、常见报错处理见 reference.md