dsh-tiddlywiki 0.22.10 → 0.23.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -1
- package/docs/plans/2026-09-17-wechat-publish-design.md +208 -0
- package/docs/seed-initialization.md +5 -1
- package/docs/wechat-publish-setup.md +387 -0
- package/lib/client.js +6 -6
- package/lib/index.js +228 -8
- package/lib/index.js.map +1 -1
- package/package.json +3 -2
- package/src/client/settings-page.ts +23 -2
- package/src/host/config.ts +14 -0
- package/src/host/prompt.ts +33 -4
- package/src/host/seed-publish-spec.ts +146 -0
- package/src/host/seed-wechat-docs.ts +70 -0
- package/src/host/seeds.ts +45 -0
- package/src/index.ts +35 -2
- package/tools/wechat/backfill-publish-state.mjs +165 -0
- package/tools/wechat/create-article.js +114 -0
- package/tools/wechat/install-wechat-adapters.mjs +132 -0
- package/tools/wechat/publish-note.js +277 -0
- package/tools/wechat/seed-publish-spec-now.mts +77 -0
- package/tools/wechat/wechat-html.js +190 -0
- package/tools/wechat/weixin-flow.js +430 -0
package/README.md
CHANGED
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
| 🧯 **路由不会拖垮进程** | 所有路由经 `guardHandler` 包装:任何 rejection(含代理里 try 之外的 `new URL()`)都变成 413/500 响应,而不是宿主未处理的 promise rejection(那会**直接结束 dsh web 进程**并挂死请求);剪藏桥 listen 后保留常驻 `error` 监听(v0.19.3) |
|
|
38
38
|
| 📊 **回复流卡片** | 工具结果显示原生 TW 卡片(**按笔记自己的内容类型渲染**:Markdown 笔记就是 Markdown,v0.18.0),检索/最近列表带**命中处摘要**(v0.22.8);`[标题](/dsh-tiddlywiki/tw/#标题)` 点击直达 TW 面板 |
|
|
39
39
|
| 📤 **发送给 Agent** | TW 笔记工具栏一键把当前笔记注入所选 dsh 会话(可选工作模式/权限/附加说明);成功/失败会弹出提示(v0.20.0 修复:此前提示把自由文本当 tiddler 标题传给 TW notifier,全部静默) |
|
|
40
|
+
| 📮 **发布到微信公众号**(**可选,默认关**) | 把笔记一键发到公众号**草稿箱**(可选点发表):TW 渲染 → 补内联样式 → 浏览器自动化复用你已登录的后台会话。**绕开官方 API 权限封锁**(2025-07 起个人主体账号的发布接口被回收),个人号可用;发表需管理员扫一次码。**需额外安装**(opencli + 浏览器扩展),插件不替你装;**关闭时完全不打扰**(不注入提示词、不写文档)。**带发布元数据**(`pub-state`/`pub-platform`/`pub-wechat-*` + `no-publish` 标签)避免重发或误发。见 [docs/wechat-publish-setup.md](docs/wechat-publish-setup.md) |
|
|
40
41
|
| 🧭 **内嵌编辑器** | 中央列内嵌完整 TW 5 编辑器(同源代理,Tailscale/内网/域名/HTTPS 均可) |
|
|
41
42
|
| 🗂️ **右侧边栏 Tab** | DSH 新右侧栏(rightbar):首页「TiddlyWiki 知识库」入口一键打开,与聊天并排;链接点击可直达(v0.16.21) |
|
|
42
43
|
| 🧪 **审计守门** | 第四轮审计(v0.20.0)把 CI 与 `npm run verify:*` 合成一份清单,并补上 `verify-constants`(filter 长度预算)、`/render` 403、auth 打码、notify 与草稿避让回归 |
|
|
@@ -153,6 +154,41 @@ dsh plugin --profile web add link:/path/to/dsh-tiddlywiki
|
|
|
153
154
|
|
|
154
155
|
`wikiRoot` / `wiki` 现在只是**默认值**——运行中的实际位置以指针文件优先。这也是为什么它能不改 cordis、不重装就切。
|
|
155
156
|
|
|
157
|
+
### 📮 发布到微信公众号(**可选功能,默认关闭**,2026-09-17)
|
|
158
|
+
|
|
159
|
+
> **这是可选功能**:需要额外安装 opencli + Browser Bridge 浏览器扩展,**插件不替你装**。
|
|
160
|
+
> 开关 `wechat.enabled` **默认 `false`**——关闭时不注入任何发布相关提示词、也不往 wiki 写
|
|
161
|
+
> 「发布元数据规范」。开启:DSH 设置 →「TiddlyWiki 知识库」→「可选功能:微信公众号发布」。
|
|
162
|
+
> 完整安装步骤与排错见 [docs/wechat-publish-setup.md](docs/wechat-publish-setup.md)。
|
|
163
|
+
|
|
164
|
+
把 wiki 里的任意笔记**一键发到微信公众号草稿箱**(可选直接发表)。整套能力放在 `tools/wechat/`,**不依赖公众号服务端 API**——因为 2025-07 起官方已回收个人主体账号的「发布能力」接口权限;本方案改用**浏览器自动化复用你已登录的后台会话**,所以个人号也能用。
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
# ① 先按 docs/wechat-publish-setup.md 装好 opencli + 浏览器扩展,并登录公众号
|
|
168
|
+
# ② 一次性:装 adapter 到本机 opencli(幂等,会自检扩展/登录状态)
|
|
169
|
+
node tools/wechat/install-wechat-adapters.mjs
|
|
170
|
+
|
|
171
|
+
# ③ 发布(注意必须带 --trace retain-on-failure,原因见下)
|
|
172
|
+
opencli weixin publish-note "笔记标题" --trace retain-on-failure -f json
|
|
173
|
+
opencli weixin publish-note "笔记标题" --cover ./cover.png -f json # 带封面
|
|
174
|
+
opencli weixin publish-note "笔记标题" --preview ./out -f json # 先导出排版预览
|
|
175
|
+
opencli weixin publish-note "笔记标题" --publish -f json # 直接发表(需管理员扫码)
|
|
176
|
+
|
|
177
|
+
# ④ 可选:回填存量「已发布」状态(默认 dry-run)
|
|
178
|
+
node tools/wechat/backfill-publish-state.mjs # 看
|
|
179
|
+
node tools/wechat/backfill-publish-state.mjs --write # 写
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**流程**:笔记标题 → DSH 的 `/render`(TW 自己渲染成语义 HTML,含代码高亮)→ `wechat-html.js` 补**内联样式**(微信会剥 `<style>` 和 class,只认内联)→ opencli 驱动后台填表/写正文/传图/设封面/存草稿 →(可选)点发表。
|
|
183
|
+
|
|
184
|
+
**三个要点**:
|
|
185
|
+
|
|
186
|
+
1. **必须带 `--trace retain-on-failure`**——不带会对 `mp.weixin.qq.com` 稳定报 `Navigation rejected`(实测 trace 开 5/5 成功、关 8/8 失败;这是 opencli 1.8.7 的 bug,`--site-session ephemeral` 等绕法均无效)。
|
|
187
|
+
2. **发表必须管理员扫码**——后台点「发表」后微信要求管理员微信扫码确认,无法自动化。「一键」的真实含义是「脚本做到填表/排版/上传,你只需扫一次码」。默认走**发表**(不推送粉丝、不占群发额度),群发请自行在后台操作。
|
|
188
|
+
3. **图片上传用 DataTransfer 注入**,不用 `page.setFileInput`——后者依赖 CDP `Page.fileChooserOpened`,本机扩展版本组合下稳定失败;改用页面上下文直接塞 `input.files`,实测图片真进 `mmbiz.qpic.cn`。代价是单图 **8MB** 上限(字节要以 base64 穿过 evaluate)。
|
|
189
|
+
|
|
190
|
+
**换机器还原**见 [docs/wechat-publish-setup.md](docs/wechat-publish-setup.md)(含 opencli / Browser Bridge 扩展安装、扫码登录、排错表)。设计依据与全部实测细节见 [docs/plans/2026-09-17-wechat-publish-design.md](docs/plans/2026-09-17-wechat-publish-design.md)。
|
|
191
|
+
|
|
156
192
|
### 🧩 初始化(一次性预置 seed):哪些「必备」,哪些「可有可无」
|
|
157
193
|
|
|
158
194
|
seed 是把「wiki 里预置内容」随插件分发的机制:**ONE-SHOT(只写缺失)+ 安全跳过(同名 tiddler 已存在绝不覆盖你的数据)**,需要时可「重新初始化」恢复、可「反初始化」移除。详细见 [docs/seed-initialization.md](docs/seed-initialization.md)。
|
|
@@ -353,6 +389,20 @@ lib/ # 预构建产物(发布含 lib/**,提交入库;
|
|
|
353
389
|
|
|
354
390
|
> 最近几个主要版本的一句话记录(完整变更见 [Releases](https://github.com/bbqisbbq/dsh-tiddlywiki/releases) / git log)。
|
|
355
391
|
|
|
392
|
+
- **v0.23.1**(2026-09-17):**新增 `wechat-setup` seed——「微信公众号发布指南」也随插件分发**(补齐 v0.23.0 的缺口:当时只有「发布元数据规范」进了 seed,387 行的安装/换机还原指南只存在于仓库与 npm 包的 `docs/` 里,wiki 里那篇「换机还原清单」是手写指针笔记)。与 `publish-spec` 完全同模式:起步层(`startup: true`)+ `gate: (ctx) => ctx.wechat === true`——**只在设置页开启「微信公众号发布」时**启动写入(笔记「微信公众号发布指南」,`dsh-docs` 标签自动进首页插件文档栏),不开该功能的用户 wiki 不出现、设置页手动「初始化」不受 gate 约束。**内容单一来源**:新 gen 脚本 `scripts/gen-seed-wechat-docs.mjs` 从 `docs/wechat-publish-setup.md` 生成 `src/host/seed-wechat-docs.ts`(照 send-to-agent 的 gen 流水线,勿手改常量),新守门 `scripts/verify-wechat-docs-seed.mjs`(进 `verify:unit`)断言 seed 常量与文档**逐字节一致** + 注册表 gate 接线(2 个 gated seed = gate 谓词恰好 2 处)。顺带:`verify-package-contents.mjs` 的 `npm pack` 从管道捕获改为**文件重定向 + 临时缓存目录**(DSH 沙箱禁止命名管道 stdio → 原 `exec` 实现 EPERM;npm 默认缓存在沙箱外也要绕开),本地终于能跑这条守门。selftest / verify-seed-error-policy 的 gate 断言改为**从注册表派生** gated 清单(新增 gated seed 不再漏改硬编码);设置页与 config 注释同步「开启后写两篇文档」;docs/seed-initialization.md 补齐 publish-spec / wechat-setup 两行表格。
|
|
393
|
+
|
|
394
|
+
- **v0.23.0**(2026-09-17):**新增「发布到微信公众号」+ 发布元数据**(`tools/wechat/`,**可选功能,默认关闭,需额外安装**,不动 host 代码)。把 wiki 笔记一键发到公众号**草稿箱**,可选点发表。
|
|
395
|
+
|
|
396
|
+
**⚠️ 可选功能,默认关**:真正干活的是仓库 `tools/wechat/` 下的 opencli adapter + Browser Bridge 浏览器扩展——**插件本体不含它,也不会替你装**。开关 `wechat.enabled` 默认 `false`:**关闭时**不注入任何发布相关提示词、也不往 wiki 写「发布元数据规范」文档;**打开后**仅多这两项,不会安装任何东西、不启用任何后台服务。不装/不开它,插件其他功能完全不受影响。开启方式:设置 →「TiddlyWiki 知识库」→「可选功能:微信公众号发布」→ 勾选 → 保存配置。完整安装步骤(opencli、浏览器扩展、扫码登录)与排错见 [docs/wechat-publish-setup.md](docs/wechat-publish-setup.md)。
|
|
397
|
+
|
|
398
|
+
**为什么不用官方 API**:2025-07 起官方回收了「发布能力」接口对个人主体/未认证账号的调用权限(`freepublish/submit` 不可用、`draft/add` 常回 48001);即便可用还要配 API IP 白名单、封面永久素材、正文图片必须走 `media/uploadimg`。**本方案改走浏览器自动化**,复用你**已登录**的公众号后台会话(opencli + Browser Bridge 扩展),个人号可用、零凭据落盘。数据流:笔记标题 → DSH `POST /render`(TW 自己渲染成语义 HTML,**代码高亮白蹭**)→ `wechat-html.js` 补**内联样式**(实测微信会剥 `<style>` 并删 class,**只认内联 style**;而 TW 输出零内联样式,所以必须有这一步)→ `weixin-flow.js` 驱动后台填表/写正文/传图/设封面/存草稿。
|
|
399
|
+
|
|
400
|
+
**发布元数据**(本轮追加,解决「哪些发过 / 哪些不能发」):每篇笔记用自定义字段记录对外发布状态——`pub-state`(`draft`/`published`/`excluded`)、`pub-platform`、`pub-wechat-at` / `pub-wechat-title` / `pub-wechat-url`(**按平台分字段**,将来 `pub-zhihu-*` 直接平铺)、`pub-note`;标签 `no-publish` 作为给人看的镜像。**规范全文放进 seed**(「发布元数据规范」,`dsh-docs` 标签),因为注入提示词 slim 形态实测只剩几十字符余量,只在其中放**一句指针**;该 seed 带 `gate`——**只有开启可选功能才会写**。⚠️ 三个名字**已被占用**故特意避开:`publish`/`publishyear` 是 Obsidian 导入书籍的「出版社/出版年」,`发布记录` 是本插件自己的版本说明。存量来源可辨的公众号文章(`source-path` 含「公众号」,本 wiki 实测 18 篇)用 `tools/wechat/backfill-publish-state.mjs` 回填(**默认 dry-run**;先 GET 完整 tiddler 再只添加字段整体写回;已有 `pub-state` 跳过;正文为空跳过;发表时间无法考证就留空)。发布前检查**只告警不阻断**(读 `pub-state`/`no-publish` 命中就打印警告并继续,`--force` 仅改措辞),回执里附**建议回写值**供 agent 回写。
|
|
401
|
+
|
|
402
|
+
**四个实测踩坑**:① **必须带 `--trace retain-on-failure`**——不带就对 `mp.weixin.qq.com` 稳定报 `Navigation rejected`(trace 开 5/5 成功、关 8/8 失败;`--site-session ephemeral` / `--keep-tab false` / 前后台窗口 / 重置标签页 / 重启 daemon 全部无效,是 opencli 1.8.7 的 bug);② **轮询里绝不能读 `document.body.innerText`**——微信编辑器 DOM 极大,读一次强制整页 layout,**实测单次 ≈17 秒**,8 次轮询把命令拖过 210s 超时而**草稿其实已保存成功**(假阴性);改成只查少量 toast 节点后 `saveDraft` 从 **137 秒降到 3.9 秒**;③ **图片上传必须用 DataTransfer 注入**,不能用 `page.setFileInput`(后者依赖 CDP `Page.fileChooserOpened`,本机扩展版本组合下稳定失败;DataTransfer 在页面上下文直接塞 `input.files`,实测图片真进 `mmbiz` CDN,代价是单图 8MB 上限);④ **正文必须用 `execCommand('insertHTML')`**,`insertText` 会把 HTML 当字面文本(opencli 内置 `weixin create-draft` 就是这样,实测 Markdown 符号原样进库)。**发表需管理员扫码**——微信的账号安全机制,无法自动化,「一键」的真实含义是「脚本做完排版/上传/填表,你只扫一次码」;默认走**发表**(不推送粉丝、不占群发额度)而非群发。
|
|
403
|
+
|
|
404
|
+
命令:`opencli weixin publish-note "标题" [--cover x.png] [--preview out] [--publish] [--force]`(另有 `create-article` 从本地 HTML 文件建草稿)。**换机器还原**:`node tools/wechat/install-wechat-adapters.mjs` 一条命令装好 adapter 并自检扩展/登录状态;完整步骤与排错见 `docs/wechat-publish-setup.md`。守门:`verify-package-contents.mjs` +5 断言(发布包必须含 publish-note / install / backfill / seed-now 与 setup 文档)、`package.json` 的 `files` 白名单补 `tools`,新增 **`scripts/verify-wechat-adapters.mjs`**(进 `verify:unit`,10 条源码级断言:文件齐全、**轮询不得出现 `body.innerText`**、**不得用 `setFileInput`**、装饰器注入内联样式、发布前检查存在且**只告警不抛错**、返回行 key 与 `columns` 一一对应、install 清单与磁盘一致、三个坑的注释在位;**已反向验证**:塞回 `body.innerText` 即红)。调研与本机实测全过程沉淀在 wiki 笔记「公众号发布插件调研」。
|
|
405
|
+
|
|
356
406
|
- **v0.22.10**(2026-09-17):**修「写入丢失 `created`/`modified`,新笔记在按修改时间排序的页面上沉到最后一名」**(用户实测报障:日记在「主题页·日志」排 175/175,看着像没被收录)。根因:插件把这两个字段列进 `CLEAN_SKIP_FIELDS`(假设「TW 服务端会补」),但 TW 服务端的 PUT 路由**从不补**时间戳——`getCreationFields()`/`getModificationFields()` 只在 TW 自己的浏览器 UI 里调用,于是插件写的条目落盘只有 tags/title/type;而 TW 的 `sortTiddlers` 对缺失字段取 `fields[sortField] || ""`,`!sort[modified]` 降序时空串直接沉底。**修复**:`buildWriteTiddler()` 统一负责写这两个字段,语义与 TW 编辑器一致——新建 = 都取当前时刻,覆盖 = 保留原 `created`、只刷新 `modified`(基底无 `created` 的迁移存量则补当前时刻);格式用新增 `formatTiddlerDate()`(TW 的 17 位紧凑 UTC,与 `$tw.utils.stringifyDate()` 逐字节一致,别用 ISO)。两者继续留在 `RESERVED_TIDDLER_FIELDS`(`fields` 不得覆盖),只是不再被 CLEAN_SKIP 丢弃。`TiddlyWebClient.put()` 另有 `ensureTiddlerTimestamps()` 兜底(**只补缺、绝不改写**已有值),覆盖剪藏桥 / seed / 配置 tiddler 等手拼 PUT 的路径。顺带统一:`rename`(含引用改写)与回收站恢复改走共享写策略(原先手拼 body 会把旧 `modified` 原样带过去);回收站快照与恢复保留原时间戳(恢复 = 恢复原状,`trash-at` 单独记删除时刻);`/edit` 生成的 TW 草稿携带原笔记的 `created`——TW 保存草稿时草稿字段会覆盖新生成的时间戳,草稿不带它就会把笔记的创建时刻重置成「刚才」。守门:`verify-write-policy` +7 条纯函数(格式往返 / 三种语义 / `$:/` / `fields` 不可覆盖 / 兜底只补不改写),`verify-tools` +5 条 E2E(含用 TW 真实 `!sort[modified]` 过滤器验证排序),selftest 增配置 `created` 保留断言。**存量数据不自动回填**:线上 wiki 1161 个 `.tid` 中 948 个缺 `modified`(多为历史迁移脚本所致),它们仍会在 `sort[modified]` 页面沉底,可用 wiki 根目录的 `backfill-timestamps.mjs` 一次性补齐(默认 dry-run,按 **git 历史**取首次出现/最后变更时间,`--write` 才落盘)。
|
|
357
407
|
|
|
358
408
|
- **v0.22.9**(2026-09-16):**修「TiddlyWiki 编辑者署名被静默清空」**(用户实测报障:控制面板 →「信息」→「基础」里的「编辑者署名」填完保存不了、刷新就没了)。根因是 DSH 启动 TW 子进程时(匿名 loopback 分支)**没传 `anon-username`**:`get-status.js:21` 回落到 `""`,而 TiddlyWeb adaptor 判定登录用的是 `isLoggedIn = json.username !== "GUEST"`(`tiddlywebadaptor.js:93`)——`"" !== "GUEST"` 为真,于是**匿名读者被当成已登录用户**;`syncer.js:281-283` 随即在**每次页面加载**时把 `$:/status/UserName` 覆写成那个空串。名字在当次会话里看着还在,一刷新就没了;而一旦 `$:/config/SyncFilter` 放行了 `UserName`(本仓库 wiki 的现状,有一篇专门的踩坑笔记记录),这个空串还会**同步落盘并进 git**,从「内存里丢了」升级成**真实、可追溯的数据丢失**。现在匿名分支显式传 `anon-username=GUEST`(`wiki.ts` 的 `ANON_USERNAME` 常量):`isLoggedIn` 变 false → syncer **根本不再写** `UserName` → 用户填的名字稳定保留。⚠️ `GUEST` 是**承重的魔法值**,不是随便取的名字:TW 判的就是 `!== "GUEST"` 这个哨兵,填任何人名(含你自己的名字)都仍然算「已登录」,只会把署名覆盖成人名而不是空串。**为什么拖到现在**:`src/host/wiki.ts` 从 v0.16 起就写着一句注释「Passing anon-username/readers/writers here was verified to 401 every request」——它把**两个独立参数**混为一谈,等于给后来每个维护者发了张「此路不通」的牌子。实测(TW 5.4.1)只有 `readers`/`writers` 会关掉匿名访问(`server.js:63-66` 里显式的 `readers` 会让 `(anon)` 不再是 principal,于是 `isAuthorized()` 为 false,`requestHandler` 用当时为 `undefined` 的 `authenticatedUsername` 拼出 `'undefined' is not authorized` 的 401);`anon-username` **单独**传完全安全(`/status` 200、GET 200、PUT 204)。那句注释已改正并写明真实机制。**副作用(外观级)**:`$:/status/IsLoggedIn` 恒为 `no`,TW 的 SyncerDropdown 会多出一个用不上的 Login 按钮;`wiki.js:1690` 的 `generateDraftTitle` 会从 `Draft/Title` 形态切到 `Draft/Attribution` 形态(内核既有行为,与「快速笔记」的 `/edit` 草稿路由无关——后者按 `draft.of` 字段匹配,不依赖标题字面量)。**影响面**:默认 SyncFilter 的用户**不会**丢盘(`$:/status/` 被默认过滤器整个排除,空串走不到落盘),实际受数据丢失影响的只有**已经放行 `UserName` 的人**——所以这一版主要是消除一颗静默的数据丢失地雷 + 拔掉那句误导注释。守门:新增 `scripts/verify-anon-username.mjs`(进 `verify:e2e`,真实 TW 子进程)——`ANON_USERNAME` 必须字面量 `GUEST`、`/status` 必须回该哨兵、落盘署名不得被覆盖、匿名模式仍可正常写条目、auth 模式不受影响且对匿名请求仍 401,外加源码级断言(旧误导注释必须已删、鉴权分支不得顺手传 `anon-username`);**已反向验证**:把 `anon-username` 那行去掉即红 3 条。
|
|
@@ -440,4 +490,4 @@ npm publish # 版本号在 package.json;文件白名单见 files 字段
|
|
|
440
490
|
- **GitHub**:https://github.com/bbqisbbq/dsh-tiddlywiki
|
|
441
491
|
- **npm**:`dsh-tiddlywiki`(https://www.npmjs.com/package/dsh-tiddlywiki)
|
|
442
492
|
- 说明笔记、首页、示例文档、样式等预置内容由 seed 机制写入 wiki(见 [🧩 初始化](#-初始化一次性预置-seed哪些必备哪些可有可无))
|
|
443
|
-
- MIT;Node ≥ 22;GitHub topics:`dsh` `dsh-plugin` `tiddlywiki` `knowledge-base` `note-taking` `git-sync` `agent-tools` 等
|
|
493
|
+
- MIT;Node ≥ 22;GitHub topics:`dsh` `dsh-plugin` `tiddlywiki` `knowledge-base` `note-taking` `git-sync` `agent-tools` 等
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# 设计:TiddlyWiki 一键发布到微信公众号
|
|
2
|
+
|
|
3
|
+
- 日期:2026-09-17
|
|
4
|
+
- 状态:待评审
|
|
5
|
+
- 来源:会话调研(dsh-tiddlywiki 工作区)
|
|
6
|
+
- 用户选定:**唤醒 agent** + **草稿 + 自动点发表(完整发布)**
|
|
7
|
+
|
|
8
|
+
## 1. 问题与约束(调研结论)
|
|
9
|
+
|
|
10
|
+
### 1.1 为什么不能走官方 API
|
|
11
|
+
|
|
12
|
+
公众号发布的服务端链路是 `access_token → draft/add → freepublish/submit`。但:
|
|
13
|
+
|
|
14
|
+
- **2025 年 7 月起,官方回收「发布能力」接口对个人主体、企业未认证、不支持认证账号的调用权限**([发布能力文档](https://developers.weixin.qq.com/doc/subscription/guide/product/publish.html) 原文注)。个人主体无法做微信认证,故 `freepublish/submit` 不可用。
|
|
15
|
+
- 草稿箱 `draft/add` 文档未列该限制,但 48001/`api unauthorized` 是常见返回,不能假定可用。
|
|
16
|
+
- 即便可用,还叠加:**必须把本机公网出口 IP 加入 API IP 白名单**(否则 61004/40164)、封面 `thumb_media_id` 必须是**永久素材**、正文图片必须是 `media/uploadimg` 产出的 mmbiz 地址(外链被过滤)。
|
|
17
|
+
|
|
18
|
+
**结论:纯 API 路线对个人号不可行。**
|
|
19
|
+
|
|
20
|
+
### 1.2 为什么浏览器路线可行
|
|
21
|
+
|
|
22
|
+
后台网页端(`mp.weixin.qq.com`)的**发表/群发从未受 API 回收影响**——这是所有个人号日常发文的方式。因此「驱动后台网页」= 完整发布能力。
|
|
23
|
+
|
|
24
|
+
### 1.3 硬约束:发表需要管理员扫码
|
|
25
|
+
|
|
26
|
+
多份实操文档一致表明:后台点「发表」后**需要公众号管理员微信扫码确认**([135编辑器流程](https://www.135editor.com/books/chapter/1/797.html):「点【发布】,扫码验证身份后文章即发布成功」)。
|
|
27
|
+
|
|
28
|
+
**这是本设计的中心约束**:不存在「完全无人值守的一键发布」。
|
|
29
|
+
「一键」的真实含义是——**agent 完成素材、排版、上传、填表、进草稿箱,直到人工只需扫一次码**。
|
|
30
|
+
|
|
31
|
+
另需区分两个操作(易混):
|
|
32
|
+
|
|
33
|
+
| 操作 | 是否推送粉丝 | 是否占群发额度 | 扫码 |
|
|
34
|
+
|---|---|---|---|
|
|
35
|
+
| **发表** | 否(仅生成永久链接) | 否,不限次数 | 是 |
|
|
36
|
+
| **群发** | 是 | 个人订阅号 1 天 1 次 | 是 |
|
|
37
|
+
|
|
38
|
+
本设计默认走**发表**(不打扰粉丝、不吃每日额度),群发作为显式可选。
|
|
39
|
+
|
|
40
|
+
## 2. 现状与可复用资产(已实测)
|
|
41
|
+
|
|
42
|
+
- 本机 `opencli` 已升级 **1.7.4 → 1.8.7**,`D:\npm-global` 下有 `puppeteer-core@24.38.0`,Chrome 位于 `C:/Program Files/Google/Chrome/Application/chrome.exe`。
|
|
43
|
+
- **opencli 内置 `weixin` 适配器**(`clis/weixin/`):
|
|
44
|
+
|
|
45
|
+
| 命令 | 作用 | access |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| `weixin create-draft` | 建图文草稿 | write |
|
|
48
|
+
| `weixin drafts` | 列草稿箱 | read |
|
|
49
|
+
| `weixin download` / `search` | 下文章 / 搜狗微信搜索 | read |
|
|
50
|
+
|
|
51
|
+
`create-draft` 走 `Strategy.COOKIE` + `mp.weixin.qq.com` 登录态:打开后台 → 从 URL 取 `token=(\d+)` → 进 `appmsg_edit_v2` → 填 `textarea#title`/`input#author`/`textarea#js_description` → 图片经 CDP `setFileInput` 上传 → 点「保存为草稿」。
|
|
52
|
+
- **三个缺口**(本次要补):
|
|
53
|
+
1. **无「发表」命令** —— 发布段需自建。
|
|
54
|
+
2. **正文写的是纯文本**(`execCommand('insertText')`),HTML/排版会被当字面文字。
|
|
55
|
+
3. 长正文走命令行位置参数,Windows 下有转义/长度风险。
|
|
56
|
+
|
|
57
|
+
### 阻塞前提
|
|
58
|
+
|
|
59
|
+
`opencli doctor` 实测:daemon 正常(19825),但 **`[MISSING] Extension: not connected`**,Chrome 未运行。**Browser Bridge 扩展不在 npm 包里**,须从 [Chrome Web Store](https://chromewebstore.google.com/detail/opencli/ildkmabpimmkaediidaifkhjpohdnifk) 安装,或 GitHub Releases 下载 zip 后 `chrome://extensions` → 开发者模式 → 加载已解压的扩展程序。装好且 Chrome 登录 `mp.weixin.qq.com` 后方可用。
|
|
60
|
+
|
|
61
|
+
## 3. 架构
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
TW 笔记(Markdown,含附件图)
|
|
65
|
+
│ ① 工具栏按钮「发布到公众号」
|
|
66
|
+
▼
|
|
67
|
+
发送给 Agent(复用既有链路:pick 会话 / 新建会话 → POST /agent/send)
|
|
68
|
+
│ ② 消息体 = 发布任务指令 + 笔记正文 + 指向发布 SOP
|
|
69
|
+
▼
|
|
70
|
+
Agent 会话
|
|
71
|
+
│ ③ 读 [[公众号发布流程]] SOP,排版 + 选配图 + 校验
|
|
72
|
+
▼
|
|
73
|
+
执行 opencli(复用 Chrome 登录态,无需二次扫码)
|
|
74
|
+
│ ④ weixin create-draft(或自建富文本版)
|
|
75
|
+
▼
|
|
76
|
+
草稿箱 mp.weixin.qq.com
|
|
77
|
+
│ ⑤ 自建 weixin/publish-draft:找到草稿 → 点「发表」
|
|
78
|
+
▼
|
|
79
|
+
⚠️ 管理员扫码确认(人工,预期内)→ 发布成功 → agent 回报链接
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### 组件
|
|
83
|
+
|
|
84
|
+
| # | 组件 | 位置 | 说明 |
|
|
85
|
+
|---|---|---|---|
|
|
86
|
+
| 1 | 「发布到公众号」按钮 | `scripts/bundle/send-to-agent/button-publish.tid`(新) | toolbar 按钮,`message="dsh-publish-to-wechat"` |
|
|
87
|
+
| 2 | 事件处理 | `scripts/bundle/send-to-agent/startup.js` | 新增 `dsh-publish-to-wechat` 监听,构造发布任务消息,复用现有 picker |
|
|
88
|
+
| 3 | 发布 SOP | `src/host/seed-wechat-publish.ts`(新 seed,`dsh-docs` 标签) | 排版规范 + opencli 命令 + 扫码处理 + 失败回退;agent 读它执行 |
|
|
89
|
+
| 4 | 发布 adapter | `~/.opencli/clis/weixin/publish-draft.js`(用户机) | 草稿箱 → 发表 → 扫码等待 → 验证;**沉淀型资产**,不进本仓库 |
|
|
90
|
+
| 5 | 富文本正文(可选) | 自建 `weixin/create-article.js` | 用 `insertHTML` 写微信内联样式 HTML,替代纯文本 |
|
|
91
|
+
|
|
92
|
+
### 为什么按钮走「唤醒 agent」而不是主机直调 opencli
|
|
93
|
+
|
|
94
|
+
- 与你既有「发送给 Agent」链路同构,**复用 picker、会话创建、消息注入**全部现成代码。
|
|
95
|
+
- agent 能做确定性代码做不了的事:按公众号风格改写、挑配图、检查敏感词、按需调整排版。
|
|
96
|
+
- 代价:每次发布消耗一次会话(可接受,发布本就是低频重活)。
|
|
97
|
+
|
|
98
|
+
## 4. 富文本转换规范(首版即做,已确认)
|
|
99
|
+
|
|
100
|
+
### 4.0 已实测验证的结论(2026-09-17,链路已跑通)
|
|
101
|
+
|
|
102
|
+
自建 adapter `~/.opencli/clis/weixin/create-article.js` 已实跑成功,后台 `list_ex` 核对:
|
|
103
|
+
|
|
104
|
+
| 实现 | 封面 | digest |
|
|
105
|
+
|---|---|---|
|
|
106
|
+
| 内置 `weixin create-draft` | 无 | `# 一级标题这是一段**加粗**与*斜体*测试。` ← Markdown 原样(纯文本) |
|
|
107
|
+
| 自建 `create-article` | `mmbiz.qlogo.cn/...` | `富文本排版验证这是一段带内联样式的正文测试。…` ← **HTML 已解析** |
|
|
108
|
+
|
|
109
|
+
**三个关键技术事实**(推翻/修正了本节早先的假设):
|
|
110
|
+
|
|
111
|
+
1. **正文写入用 `execCommand('insertHTML')`**——编辑器是 **ProseMirror**(非 UEditor iframe),实测内联样式完整保留。
|
|
112
|
+
2. **图片上传必须用 DataTransfer 注入,不能用 `page.setFileInput`**——后者依赖 CDP `Page.fileChooserOpened`,本机(CLI 1.8.7 + 扩展 v1.0.24 + Edge)稳定失败([issue #1582](https://github.com/jackwener/OpenCLI/issues/1582) 佐证版本不匹配问题)。DataTransfer 在页面上下文直接塞 `input.files` 并派发 `change`,实测图片真进 `mmbiz.qpic.cn`。代价:字节以 base64 过 evaluate,**限 8MB**。
|
|
113
|
+
3. **所有 weixin 命令必须带 `--trace retain-on-failure`**——否则稳定报 `Navigation rejected`(trace 开 5/5 成功,关 8/8 失败)。
|
|
114
|
+
|
|
115
|
+
其余踩坑:图片下拉必须**按文案**点「本地上传」(`items[0]` 无效);正文是最后一个 contenteditable;回读校验须**忽略所有空白**(innerText 会插空白/转 nbsp,否则假阴性);保存按钮兜底文案**不含「发表」**以防误点。
|
|
116
|
+
|
|
117
|
+
### 4.1 微信编辑器的硬限制
|
|
118
|
+
|
|
119
|
+
| 限制 | 后果 | 对策 |
|
|
120
|
+
|---|---|---|
|
|
121
|
+
| `<style>` 标签被移除 | 外链/内嵌 CSS 全失效 | **所有样式必须内联** `style="..."` |
|
|
122
|
+
| `class` 属性被删除 | 类名选择器无用 | 不用 class,只用内联 style |
|
|
123
|
+
| 自定义字体不可引入 | `@font-face` 无效 | 只用系统字体(苹方/黑体等) |
|
|
124
|
+
| 复杂 `<table>` 样式受限 | 表格排版易崩 | 简单表格;必要时 `div`→`table` 包裹 |
|
|
125
|
+
| `<script>` 被剥离 | 不能靠 JS 修饰 | 纯静态 HTML |
|
|
126
|
+
| 代码块换行易被吞 | 代码糊成一行 | 预处理 `white-space: pre-wrap` + 处理 nbsp |
|
|
127
|
+
| 表格内字体不继承 | 表格字号与正文不一致 | 每个单元格**显式声明** font-size |
|
|
128
|
+
|
|
129
|
+
### 4.2 已确认可用的内联属性
|
|
130
|
+
|
|
131
|
+
`font-size` / `color` / `line-height` / `letter-spacing` / `margin` / `padding` / `text-align` / `background-color` / `border-radius` / `box-shadow` / `max-width`。
|
|
132
|
+
|
|
133
|
+
推荐基准:正文 `font-size:16px; line-height:1.75; letter-spacing:0.5px; margin:0 0 1em`。
|
|
134
|
+
|
|
135
|
+
### 4.3 参考实现(不要照抄,取规则)
|
|
136
|
+
|
|
137
|
+
| 项目 | 可借鉴之处 |
|
|
138
|
+
|---|---|
|
|
139
|
+
| [vigorX777/wechat-article-formatter](https://github.com/vigorX777/wechat-article-formatter) | **CSS 兼容性引擎**(div→table、white-space 预处理、字体显式声明);图片用 `WECHATIMGPH_N` 占位符 + manifest 映射;保存草稿的 **verdict 校验**(titleOk/contentOk/imageCountOk/saveToastOk/blockingDialog);保存按钮 fallback **特意不匹配「发表/发布」**避免误点 |
|
|
140
|
+
| [iamzifei/wechat-article-formatter-skill](https://github.com/iamzifei/wechat-article-formatter-skill) | 内联 CSS、免外部依赖的极简形态 |
|
|
141
|
+
| mdnice / TypeZen | 通用 Markdown→公众号内联样式思路 |
|
|
142
|
+
|
|
143
|
+
⚠️ vigorX777 项目标注为 **private skill「仅供个人使用」**,故只借鉴其**兼容性规则与校验思路**,不复制代码。
|
|
144
|
+
|
|
145
|
+
### 4.4 转换流水线
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
Markdown(笔记正文)
|
|
149
|
+
→ markdown-it 解析(复用 tiddlywiki/markdown 插件已带的 markdown-it.min.js)
|
|
150
|
+
→ 逐元素注入内联样式(主题表驱动)
|
|
151
|
+
→ CSS 兼容性后处理(div→table / 代码块 white-space / 表格字体)
|
|
152
|
+
→ 图片:本地附件 → 占位符 → 发布时 CDP 逐个替换上传
|
|
153
|
+
→ 输出 HTML 片段 → insertHTML 写入编辑器
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### 4.5 发布 SOP 要点(组件 3 的内容大纲)
|
|
157
|
+
|
|
158
|
+
1. **标题 ≤64 字**(后台限制)、作者 ≤8 字、摘要 ≤120 字。
|
|
159
|
+
2. **封面**:`--cover-image <本地路径>`,adapter 会自动先传正文再「从正文选择」设为封面。建议 2.35:1。
|
|
160
|
+
4. **图片**:本机路径传入,由 CDP 真上传到微信 CDN;外链图会被过滤。
|
|
161
|
+
5. **命令**:
|
|
162
|
+
```bash
|
|
163
|
+
opencli weixin create-draft "<正文>" --title "..." --author "..." --cover-image "..." --summary "..." -f json
|
|
164
|
+
opencli weixin drafts -f json # 确认草稿已建
|
|
165
|
+
```
|
|
166
|
+
6. **富文本正文**:内置 `create-draft` 只写纯文本;富文本走自建 adapter 的 `insertHTML`,或先复制 HTML 到剪贴板再粘贴。
|
|
167
|
+
7. **发表**:调自建 `weixin publish-draft`;检测到扫码页时**暂停并提示用户扫码**,轮询结果。
|
|
168
|
+
8. **成功校验(fail-closed)**:不得只看「没报错」就宣告成功——须回读校验标题已持久化、正文非空、图片数量正确、无阻塞弹窗、检测到保存/发表成功信号。参考 vigorX777 的 verdict 清单。
|
|
169
|
+
9. **失败回退**:若发表失败或账号受限,草稿仍在草稿箱 → 提示用户手动进入后台点发表(不丢工作成果)。
|
|
170
|
+
|
|
171
|
+
## 5. 风险与边界
|
|
172
|
+
|
|
173
|
+
| 风险 | 应对 |
|
|
174
|
+
|---|---|
|
|
175
|
+
| **发表需扫码**(不可避免) | 设计为「预期内的人工确认点」,不是失败;用 `--keep-tab`/`manual` 模式把标签页留下 |
|
|
176
|
+
| 后台 DOM 改版 | adapter 用多选择器降级 + typed error;失败落 trace + 截图 |
|
|
177
|
+
| 风控/封号 | 不做规避检测的行为伪装;低频使用;默认走「发表」而非「群发」;不碰验证码 |
|
|
178
|
+
| 每日额度 | 默认「发表」不占群发额度;群发需显式确认 |
|
|
179
|
+
| 扩展未装 | 前置自检:`opencli doctor` 不通则明确提示装扩展,不静默失败 |
|
|
180
|
+
| 长文本命令行传参 | 改为读文件传正文(避免 Windows 转义/长度限制) |
|
|
181
|
+
| 账号隐私 | 凭据是浏览器 cookie,**不落盘、不进 git**;不引入 AppSecret |
|
|
182
|
+
|
|
183
|
+
## 6. 实施步骤
|
|
184
|
+
|
|
185
|
+
1. **前置打通**(用户手动,一次性):装 Browser Bridge 扩展 → 启动 Chrome → 登录 `mp.weixin.qq.com` → `opencli doctor` 全绿。
|
|
186
|
+
2. **验证内置能力**:用一篇测试笔记跑 `weixin create-draft`,确认草稿真的进了草稿箱。
|
|
187
|
+
3. **自建发表 adapter**:`~/.opencli/clis/weixin/publish-draft.js`,用 `opencli browser verify` 验证。
|
|
188
|
+
4. **写 SOP seed**:`seed-wechat-publish.ts` + 注册进 `SEED_DEFS`(可选层)。
|
|
189
|
+
5. **加按钮**:bundle 加 `button-publish.tid` + startup 事件,bump `SEND_TO_AGENT_BUNDLE_VERSION`(0.3.5 → 0.4.0),走 §4 流水线(build → gen → verify)。
|
|
190
|
+
6. **端到端**:TW 笔记 → 按钮 → agent → 草稿箱 → 扫码发表。
|
|
191
|
+
7. **收尾**:README/AGENTS.md 同步;bump 版本;wiki 记录。
|
|
192
|
+
|
|
193
|
+
## 7. 尚待确认
|
|
194
|
+
|
|
195
|
+
- **发表(点「发表」→ 管理员扫码)尚未实跑**。adapter 里 `--publish` 分支已写好(含扫码轮询、超时、成功检测),但会在你的公众号上真发一篇文章,需你确认后再验。
|
|
196
|
+
- 组件 5(富文本正文 adapter)**已完成并验证**(`create-article.js`)。
|
|
197
|
+
- 组件 3(SOP seed)、组件 1/2(按钮 + 事件)尚未开工。
|
|
198
|
+
- 是否需要「群发」支持,还是只做「发表」。
|
|
199
|
+
- SOP 放 wiki seed(随插件走)已定;是否另外生成 DSH 本地 skill。
|
|
200
|
+
- 测试期间在草稿箱留下 4 条测试草稿(`DSH链路测试`/`DSH富文本测试`/`DSH富文本adapter测试`/`DSH图文封面测试v2`),需要清理。
|
|
201
|
+
|
|
202
|
+
## 8. 参考
|
|
203
|
+
|
|
204
|
+
- [发布能力(2025-07 权限回收)](https://developers.weixin.qq.com/doc/subscription/guide/product/publish.html)
|
|
205
|
+
- [新增草稿 draft/add](https://developers.weixin.qq.com/doc/subscription/api/draftbox/draftmanage/api_draft_add.html)
|
|
206
|
+
- [服务端 API 调用说明(IP 白名单)](https://developers.weixin.qq.com/doc/subscription/guide/dev/api/)
|
|
207
|
+
- [135编辑器:草稿箱发表流程(含扫码)](https://www.135editor.com/books/chapter/1/797.html)
|
|
208
|
+
- [opencli](https://github.com/jackwener/opencli)
|
|
@@ -58,13 +58,17 @@ interface SeedDef {
|
|
|
58
58
|
| `ui-styles` | `seed-ui-styles.ts` → `seedUiStyles` / `unseedUiStyles` | 5 张通用样式表(只带功能 tag `$:/tags/Stylesheet`,剥离 wiki 本地标签与个人数据):编辑器美化、标题与按钮区分开、侧边栏窄屏自动隐藏、批注弹窗、menubar 顶栏加高 | `$:/plugins/dsh-tiddlywiki/seed-ui-styles` | 可选 |
|
|
59
59
|
| `menubar-theme` | `seed-menubar-theme.ts` → `seedMenubarTheme` / `unseedMenubarTheme` | `$:/plugins/dsh-tiddlywiki/menubar-theme`(tag `$:/tags/Stylesheet`)——覆盖 tiddlywiki/menubar 顶栏:把 `<<colour menubar-background>>` 的「默认色映射蓝色」改为跟随活动 palette 的 `background`/`foreground`,随 DSH 主题切换(`$:/palette` 翻转)自动换色 | `$:/plugins/dsh-tiddlywiki/seed-menubar-theme` | 可选 |
|
|
60
60
|
| `clip-bridge` | `seed-clip-bridge.ts` → `seedClipBridge` / `unseedClipBridge` | 「本地剪藏桥 + 书签小工具」使用说明(Markdown 文档,带书签代码 / 启用步骤 / 安全说明,tag `dsh-docs`)——真功能在 `clip-bridge.ts` 运行时代码里 | `$:/plugins/dsh-tiddlywiki/seed-clip-bridge` | 可选 |
|
|
61
|
+
| `publish-spec` | `seed-publish-spec.ts` → `seedPublishSpec` / `unseedPublishSpec` | 「发布元数据规范」(Markdown,tag `dsh-docs`)——pub-state / pub-* 字段与 no-publish 标签约定,供微信发布流程判断「发过没有 / 能不能发」 | `$:/dsh-tiddlywiki/publish-spec-seeded` | **起步(gated)** |
|
|
62
|
+
| `wechat-setup` | `seed-wechat-docs.ts` → `seedWechatDocs` / `unseedWechatDocs` | 「微信公众号发布指南」(Markdown,tag `dsh-docs`)——**逐字节等于 `docs/wechat-publish-setup.md`**(由 `scripts/gen-seed-wechat-docs.mjs` 生成,勿手改常量):opencli / 扩展 / 登录 / adapter 安装与换机还原、三个坑、排错表 | `$:/plugins/dsh-tiddlywiki/seed-wechat-docs` | **起步(gated)** |
|
|
61
63
|
| `tw-web-host` | `seeds.ts` 内联 | `$:/config/tiddlyweb/host` → `/dsh-tiddlywiki/tw/` | 无 marker(ensure 型,见 §4) | **核心** |
|
|
62
64
|
|
|
63
65
|
> ℹ️ `home-index` 的 seed 版首页是**通用版**:生成脚本**默认**剥离作者 wiki 里的个人元素(主题页 tabs、书籍书架入口等,仅 `--keep-private` 才原样嵌入),并内置「📚 插件文档」tabs 栏(`[tag[dsh-docs]!is[system]]`,默认展开插件说明)。作者自己的 wiki 首页不受影响(seed 是 ONE-SHOT,不会覆盖)。
|
|
64
66
|
>
|
|
65
67
|
> ℹ️ **v0.22.0 起 marker 记内容哈希**:marker tiddler(`$:/plugins/dsh-tiddlywiki/seed-*`)的正文从一行 `seeded-once` 升级为 JSON `{ version, hashes: { <标题>: <sha256 前 16 位> }, at }`——哈希记录的是**我们写下的内置正文**,据此可区分「内置内容更新了」与「用户自己改过」(见 §3.1)。旧 marker 仍可读,按文本比对,并在下一次重新初始化时升级。
|
|
66
68
|
>
|
|
67
|
-
> ℹ️ **v0.
|
|
69
|
+
> ℹ️ **v0.23.0 起 gated seed**:`publish-spec` / `wechat-setup`(v0.23.1)虽是起步层(`startup: true`),但带 `gate: (ctx) => ctx.wechat === true`——**只在设置页开启「微信公众号发布」(`wechat.enabled`)时**才参与启动写入;不开该功能的用户 wiki 里不会出现发布相关文档(提示词里也没有指针,死链风险只存在于开启侧)。`gate` 只作用于启动路径 `runAllSeeds`;设置页手动「初始化」走 `runSeedById` 不受它约束(显式请求)。`checkAllSeeds` 照常报告两项「缺失」,属预期。
|
|
70
|
+
>
|
|
71
|
+
> ℹ️ **v0.22.0 起 `doc-note` 正文是生成的**:工具清单来自 `tiddlywikiToolSummary()`(`docNoteText(tools)`),不再手抄「N 个 agent 工具」。无注册表的 headless 调用会退化成一句指针,绝不写出过期数量。`wechat-setup` 的正文同理由 `docs/wechat-publish-setup.md` 生成(`scripts/gen-seed-wechat-docs.mjs`),`scripts/verify-wechat-docs-seed.mjs` 守逐字节一致。
|
|
68
72
|
|
|
69
73
|
### 统一入口(`src/index.ts` 导出)
|
|
70
74
|
|