weflow-cli 1.8.2 → 1.10.0

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.
Files changed (185) hide show
  1. package/ARCHITECTURE.md +62 -26
  2. package/CHANGELOG.md +1290 -0
  3. package/OPERATIONS.md +144 -11
  4. package/README.en.md +20 -4
  5. package/README.md +30 -5
  6. package/bin/weflow-cli-electron.cjs +2 -0
  7. package/bin/weflow-cli.ts +996 -118
  8. package/dist/bin/weflow-cli.js +1038 -146
  9. package/dist/bin/weflow-cli.js.map +1 -1
  10. package/dist/mcp-server/http.d.ts +14 -0
  11. package/dist/mcp-server/http.d.ts.map +1 -0
  12. package/dist/mcp-server/http.js +84 -0
  13. package/dist/mcp-server/http.js.map +1 -0
  14. package/dist/mcp-server/httpConfig.d.ts +50 -0
  15. package/dist/mcp-server/httpConfig.d.ts.map +1 -0
  16. package/dist/mcp-server/httpConfig.js +104 -0
  17. package/dist/mcp-server/httpConfig.js.map +1 -0
  18. package/dist/mcp-server/index.d.ts +13 -1
  19. package/dist/mcp-server/index.d.ts.map +1 -1
  20. package/dist/mcp-server/index.js +58 -374
  21. package/dist/mcp-server/index.js.map +1 -1
  22. package/dist/src/core/dbPathService.d.ts +12 -0
  23. package/dist/src/core/dbPathService.d.ts.map +1 -1
  24. package/dist/src/core/dbPathService.js +20 -3
  25. package/dist/src/core/dbPathService.js.map +1 -1
  26. package/dist/src/panel/server.d.ts.map +1 -1
  27. package/dist/src/panel/server.js +61 -1
  28. package/dist/src/panel/server.js.map +1 -1
  29. package/dist/src/services/assistantEval.d.ts +69 -0
  30. package/dist/src/services/assistantEval.d.ts.map +1 -1
  31. package/dist/src/services/assistantEval.js +270 -8
  32. package/dist/src/services/assistantEval.js.map +1 -1
  33. package/dist/src/services/assistantScenes.d.ts +81 -0
  34. package/dist/src/services/assistantScenes.d.ts.map +1 -0
  35. package/dist/src/services/assistantScenes.js +322 -0
  36. package/dist/src/services/assistantScenes.js.map +1 -0
  37. package/dist/src/services/assistantService.d.ts +33 -4
  38. package/dist/src/services/assistantService.d.ts.map +1 -1
  39. package/dist/src/services/assistantService.js +102 -6
  40. package/dist/src/services/assistantService.js.map +1 -1
  41. package/dist/src/services/assistantSkills.d.ts +94 -0
  42. package/dist/src/services/assistantSkills.d.ts.map +1 -0
  43. package/dist/src/services/assistantSkills.js +330 -0
  44. package/dist/src/services/assistantSkills.js.map +1 -0
  45. package/dist/src/services/assistantTools.d.ts +34 -1
  46. package/dist/src/services/assistantTools.d.ts.map +1 -1
  47. package/dist/src/services/assistantTools.js +931 -88
  48. package/dist/src/services/assistantTools.js.map +1 -1
  49. package/dist/src/services/assistantTrace.d.ts +4 -0
  50. package/dist/src/services/assistantTrace.d.ts.map +1 -1
  51. package/dist/src/services/assistantTrace.js +9 -0
  52. package/dist/src/services/assistantTrace.js.map +1 -1
  53. package/dist/src/services/configService.d.ts +9 -0
  54. package/dist/src/services/configService.d.ts.map +1 -1
  55. package/dist/src/services/configService.js +5 -2
  56. package/dist/src/services/configService.js.map +1 -1
  57. package/dist/src/services/messageQuery.d.ts +18 -0
  58. package/dist/src/services/messageQuery.d.ts.map +1 -1
  59. package/dist/src/services/messageQuery.js +29 -0
  60. package/dist/src/services/messageQuery.js.map +1 -1
  61. package/dist/src/services/outboundSend.d.ts +53 -0
  62. package/dist/src/services/outboundSend.d.ts.map +1 -0
  63. package/dist/src/services/outboundSend.js +90 -0
  64. package/dist/src/services/outboundSend.js.map +1 -0
  65. package/dist/src/services/pythonBridge.d.ts.map +1 -1
  66. package/dist/src/services/pythonBridge.js +1 -0
  67. package/dist/src/services/pythonBridge.js.map +1 -1
  68. package/dist/src/services/wechatClient.d.ts +125 -0
  69. package/dist/src/services/wechatClient.d.ts.map +1 -0
  70. package/dist/src/services/wechatClient.js +344 -0
  71. package/dist/src/services/wechatClient.js.map +1 -0
  72. package/dist/src/services/wechatMedia.d.ts +44 -0
  73. package/dist/src/services/wechatMedia.d.ts.map +1 -0
  74. package/dist/src/services/wechatMedia.js +155 -0
  75. package/dist/src/services/wechatMedia.js.map +1 -0
  76. package/dist/src/services/wechatMessageService.d.ts +11 -0
  77. package/dist/src/services/wechatMessageService.d.ts.map +1 -1
  78. package/dist/src/services/wechatMessageService.js +55 -11
  79. package/dist/src/services/wechatMessageService.js.map +1 -1
  80. package/dist/src/types.d.ts +4 -0
  81. package/dist/src/types.d.ts.map +1 -1
  82. package/dist/src/utils/python.js +2 -2
  83. package/dist/src/utils/python.js.map +1 -1
  84. package/docs/BRANCHES.md +37 -0
  85. package/docs/CONTACT_DB_SCHEMA.md +1085 -0
  86. package/docs/DECISIONS.md +1624 -0
  87. package/docs/EXTENDING.md +98 -10
  88. package/docs/FUNCTIONAL-IMPROVEMENT-SPEC.md +6 -0
  89. package/docs/HEALTH-CHECK.md +12 -7
  90. package/docs/JEV/345/272/224/347/224/250/346/226/275/345/267/245/346/226/207/346/241/243.md +2 -2
  91. package/docs/MCP.md +58 -4
  92. package/docs/PROJECT_STATE.md +180 -11
  93. package/docs/RELEASING.md +115 -1
  94. package/docs/images/weflow-architecture-gpt-image-2.png +0 -0
  95. package/logo.png +0 -0
  96. package/mcp-server/http.ts +108 -0
  97. package/mcp-server/httpConfig.ts +126 -0
  98. package/mcp-server/index.ts +359 -681
  99. package/package.json +5 -3
  100. package/resources/js/graph3d/NOTICE.txt +35 -0
  101. package/resources/js/graph3d/d3-binarytree.min.js +2 -0
  102. package/resources/js/graph3d/d3-dispatch.min.js +2 -0
  103. package/resources/js/graph3d/d3-force-3d.min.js +2 -0
  104. package/resources/js/graph3d/d3-octree.min.js +2 -0
  105. package/resources/js/graph3d/d3-quadtree.min.js +2 -0
  106. package/resources/js/graph3d/d3-timer.min.js +2 -0
  107. package/resources/js/graph3d/layout.mjs +63 -0
  108. package/resources/js/graph3d/three.min.js +6 -0
  109. package/resources/js/graph3d/viewer2d.js +448 -0
  110. package/resources/panel/ball-position.cjs +48 -2
  111. package/resources/panel/index.html +18 -4
  112. package/resources/panel/main.cjs +137 -5
  113. package/resources/panel/mascot-base.png +0 -0
  114. package/resources/panel/mascot-blink-1.png +0 -0
  115. package/resources/panel/mascot-blink-2.png +0 -0
  116. package/resources/panel/mascot-iris.png +0 -0
  117. package/resources/panel/mascot-lift-down-1.png +0 -0
  118. package/resources/panel/mascot-lift-down-2.png +0 -0
  119. package/resources/panel/mascot-lift-down-3.png +0 -0
  120. package/resources/panel/mascot-lift-down-4.png +0 -0
  121. package/resources/panel/mascot-lift-down-5.png +0 -0
  122. package/resources/panel/mascot-lift-down-6.png +0 -0
  123. package/resources/panel/mascot-lift-up-1.png +0 -0
  124. package/resources/panel/mascot-lift-up-2.png +0 -0
  125. package/resources/panel/mascot-lift-up-3.png +0 -0
  126. package/resources/panel/mascot-lift-up-4.png +0 -0
  127. package/resources/panel/mascot-lift-up-5.png +0 -0
  128. package/resources/panel/mascot-lift-up-6.png +0 -0
  129. package/resources/panel/mascot-peek.png +0 -0
  130. package/resources/panel/mascot-tickle-1.png +0 -0
  131. package/resources/panel/mascot-tickle-2.png +0 -0
  132. package/resources/panel/mascot-tickle-3.png +0 -0
  133. package/resources/panel/mascot-tickle-4.png +0 -0
  134. package/resources/panel/panel.css +205 -59
  135. package/resources/panel/preload.cjs +26 -0
  136. package/resources/panel/renderer.js +646 -17
  137. package/scripts/_utils.py +1203 -931
  138. package/scripts/backfill_articles.py +202 -21
  139. package/scripts/biz_daily.py +26 -34
  140. package/scripts/bonds.py +331 -0
  141. package/scripts/chat_notes.py +561 -470
  142. package/scripts/chat_stats.py +8 -7
  143. package/scripts/check_readme_commands.py +426 -0
  144. package/scripts/compile_wiki.py +187 -59
  145. package/scripts/contact_schema.py +339 -0
  146. package/scripts/data/wechat_emoji.json +225 -0
  147. package/scripts/export_chat_html.py +3 -1
  148. package/scripts/fav_notes.py +4 -1
  149. package/scripts/graph_2d.py +151 -0
  150. package/scripts/graph_3d.py +646 -0
  151. package/scripts/health_check.py +163 -26
  152. package/scripts/mcp_bridge.py +6 -7
  153. package/scripts/nt_decrypt.py +3 -3
  154. package/scripts/panel_frames.py +683 -0
  155. package/scripts/pipeline.py +21 -10
  156. package/scripts/quality_eval.py +5 -0
  157. package/scripts/rag_chat.py +33 -16
  158. package/scripts/reply_debt.py +26 -6
  159. package/scripts/semantic_search.py +130 -22
  160. package/scripts/vault_rag.py +42 -16
  161. package/scripts/vault_search.py +44 -8
  162. package/scripts/wechat_emoticon.py +6 -0
  163. package/scripts/wechat_voice.py +9 -0
  164. package/scripts/wiki_lint.py +124 -24
  165. package/src/core/dbPathService.ts +22 -4
  166. package/src/panel/server.ts +58 -1
  167. package/src/services/assistantEval.ts +323 -12
  168. package/src/services/assistantScenes.ts +342 -0
  169. package/src/services/assistantService.ts +107 -6
  170. package/src/services/assistantSkills.ts +364 -0
  171. package/src/services/assistantTools.ts +2452 -1640
  172. package/src/services/assistantTrace.ts +9 -0
  173. package/src/services/configService.ts +415 -403
  174. package/src/services/messageQuery.ts +29 -0
  175. package/src/services/outboundSend.ts +126 -0
  176. package/src/services/pythonBridge.ts +1 -0
  177. package/src/services/wechatClient.ts +380 -0
  178. package/src/services/wechatMedia.ts +166 -0
  179. package/src/services/wechatMessageService.ts +62 -11
  180. package/src/types.ts +4 -0
  181. package/src/utils/python.ts +2 -2
  182. package/dist/src/utils/wechatEmoji.d.ts +0 -5
  183. package/dist/src/utils/wechatEmoji.d.ts.map +0 -1
  184. package/dist/src/utils/wechatEmoji.js +0 -35
  185. package/dist/src/utils/wechatEmoji.js.map +0 -1
package/docs/RELEASING.md CHANGED
@@ -32,8 +32,25 @@ npm 包与 GitHub 分开发布,中间隔多久都不奇怪——`1.5.0` 与主
32
32
  ```powershell
33
33
  npm run build
34
34
  npm test
35
+ py scripts/check_readme_commands.py # 文档/脚本调用/capabilities 里写的命令必须真的存在
35
36
  ```
36
37
 
38
+ 最后那条盯的是**用户先发现的那一类漂移**:`1.5.0` 与主线拉开 128 个提交时,用户照着 README 敲
39
+ `init --path` 得到 `unknown option`。它逐级问 CLI 的 `--help`(不复刻命令表),扫**三个手写的命令面**:
40
+ **用户文档**的围栏代码块(两份 README + 仓库根那几份 + `docs/*.md`,排除按性质记录过去的
41
+ `CHANGELOG.md` 与 `docs/DECISIONS.md` —— 它们会成段引用当时的命令;同一批代码块里
42
+ `node …/cli.cjs <命令>`、`npx tsx bin/weflow-cli.ts <命令>` 也算这一面 —— 那是同一套命令换个写法)、
43
+ 同一批代码块里的
44
+ `python|py scripts/x.py --flag` 调用(**不跑脚本**,按 AST 读它们的 `add_argument` 声明表 —— 仓库对
45
+ Python 脚本的既定纪律就是不 import 也不运行),以及 `capabilities --json` 里手写的 `cli:` **与 `preview:`**
46
+ 串(`preview:` 是两阶段约定的第一步 —— 用户与 agent 都按它来,所以它和 `cli:` 一样会被手写漂移;
47
+ 那个输出的 MCP 那半是从真实注册表派生的,不用管。串里有这个输出自己的记法:`<a|b>` 是"或"、
48
+ 括号外的 `|` 是"两种写法"、`…` 之后截断 —— 判据会按这个归一化后再验)。`--self-test` 是十三条正对照;
49
+ 文档命令那一面还有反空扫(缺省语料一定有命令,扫到 0 条就不报通过)。CI 的 `node` job 每次推送都会跑这两步。
50
+
51
+ > 2026-10-11 它就是这么抓到东西的:`OPERATIONS.md` 教用户敲 `search --no-rerank`,而 CLI 从来没有
52
+ > 这个旗标(Python 侧一直有)。只扫两份 README 时它一条也扫不到 —— 所以缺省集合就是"用户文档"。
53
+
37
54
  ## 4. 打包检查(**不能跳过**)
38
55
 
39
56
  ```powershell
@@ -57,6 +74,46 @@ grep -E "^output/|^models/|node_modules|\.env|\.db$|\.dat$|secret|credential" /t
57
74
  grep -E "新文件名" /tmp/list.txt
58
75
  ```
59
76
 
77
+ 这后半句("**该有的在不在**")现在有自动判据:`test/pack-contents.test.ts` 跑一次
78
+ `npm pack --dry-run --json`,断言 `scripts/` 下每个跟踪文件(`.ts` 开发入口除外)与两份 README 引用的
79
+ 本地图片都在包里。它防的故障 2026-10-11 在**真实发布物**上量到过:registry 上 `weflow-cli@1.9.0`
80
+ 的 501 个文件里同时缺 `scripts/data/wechat_emoji.json` 与 `logo.png` —— 前者让 npm 用户导出的
81
+ 聊天记录里一批表情码原样显示成 `[名字]`,后者让包页面挂着一张裂图。这条测试跟着 `npm test` 跑,
82
+ CI 的 `node` job 也在跑;上面那条人工命令留着,用来核对本次**新加**的文件名。
83
+
84
+ 想验到"**装出来真的能跑**"(最强的一道,代价是多装一次依赖):把包打进临时目录、再装进一个空
85
+ prefix,然后从**装好的那一份**里问版本。它盯的是白名单之外的另一半 —— 清单对、运行时不成立的情况。
86
+
87
+ ```powershell
88
+ npm pack --pack-destination $env:TEMP
89
+ mkdir $env:TEMP\wf-smoke ; cd $env:TEMP\wf-smoke # 空目录
90
+ npm install --no-save --no-audit --no-fund "$env:TEMP\weflow-cli-<版本>.tgz"
91
+ node node_modules\weflow-cli\cli.cjs --version
92
+ node node_modules\weflow-cli\cli.cjs --help
93
+ ```
94
+
95
+ 2026-10-11 拿 1.10.0 跑过一遍:`ALL_NAMES = 222`(不是只有原图的 118 —— 那正是上一版缺
96
+ `scripts/data/wechat_emoji.json` 时的值)、`[乒乓]`/`[冷汗]`/`[开红包]` 都渲染成对应表情、
97
+ `--version` 答 `1.10.0`。
98
+
99
+ **两个数字口径,免得下一次又花半小时**(2026-10-11 实测):`npm publish` 的 notice 报
100
+ `package size: 12.5 MB` 用的是**十进制 MB**,而 `npm pack --json` 的 `size` 是**字节**(你按习惯除以
101
+ `1MB` 得到 11.88 是 MiB)—— **同一份 tarball**:12,460,202 字节 = 12.46 MB = 11.88 MiB。
102
+ `unpacked size` 同理(publish 27.9 MB = pack 26.62 MiB,解压实测 27,915,402 字节)。不是
103
+ "publish 比 pack 大",是两套单位。
104
+
105
+ **发布时那两条 `npm warn publish … errors corrected` 是修饰性的,不是故障。**
106
+ `bin[weflow-cli]` 那句「cli.cjs was invalid and removed」措辞很吓人,实际只是把 `./cli.cjs` 规范成
107
+ `cli.cjs`;`repository.url` 被改写成 `git+https://…`。**实测证据**:registry 上 `weflow-cli@1.9.0`
108
+ 的元数据里 `bin = { 'weflow-cli': 'cli.cjs' }` 完好无损(`npm view weflow-cli@1.9.0 bin repository.url`)
109
+ ⇒ `npm i -g` 装出来的命令入口一直都在,**别据此以为发布物少了 bin**。
110
+ (那两条规范化 2026-10-11 已经直接落到仓库里了:`package.json` 的 `bin` 与 `repository.url` 现在就是
111
+ 规范形式,发布时不再报这两条 warning,仓库也与 registry 上的一致。)
112
+
113
+ > ⚠️ **`npm pkg fix` 没有 `--dry-run`**:`npm pkg fix --dry-run` 会把 `--dry-run` 当成不认识的参数**忽略掉**,
114
+ > 然后**直接改你的 `package.json`**(2026-10-11 就是这么被改的 —— 我原以为只是"看看会改什么")。
115
+ > 想先看结果就用 `git diff package.json` 兜底,别指望它自己停手。
116
+
60
117
  ## 5. 发布
61
118
 
62
119
  需要 npm 凭据。`ribiaosu` 账号开了 2FA,所以两条路:
@@ -68,7 +125,7 @@ grep -E "新文件名" /tmp/list.txt
68
125
  | 现象 | 原因 |
69
126
  | --- | --- |
70
127
  | `403 ... two-factor authentication ... required` | token 没勾 Bypass 2FA,或没用 OTP |
71
- | `ENEEAUTH` | 没登录 / 没配 token |
128
+ | `ENEEDAUTH` | 没登录 / 没配 token |
72
129
 
73
130
  **验证 token 是否可用**(`bypass_2fa` 必须是 `true`):
74
131
 
@@ -111,6 +168,22 @@ npm publish # 再发一次
111
168
 
112
169
  报 `409 Conflict - Cannot publish over previously staged version` → **已经发布成功**。
113
170
 
171
+ 但 409 只说明**版本槽位已被占**,不等于 registry 已经能查到它。发布后 npm 要先扫描再让版本可读,
172
+ **官方不承诺上限**:常见 5 分钟,实测出现过 14、16、25、55 分钟。这个窗口里的表现是:
173
+
174
+ - `npm view weflow-cli version` 还是旧版本号
175
+ - tarball 直链仍然 404
176
+ - 再发一次仍然是 409 —— 所以 409 **区分不了**「还在传播」和「已经好了」
177
+
178
+ 窗口期内只做一件事:等 + 轮询,**不要改版本号重发**。想确认不是卡在人工审批:
179
+
180
+ ```powershell
181
+ npx --yes npm@11.17.0 stage list weflow-cli --json # 返回 [] 即无待审批版本,属传播中
182
+ ```
183
+
184
+ (`npm stage` 需要 npm ≥ 11.15;本机装的是 11.6.2,直接跑会报 `Unknown command: "stage"`,
185
+ 所以用 npx 临时拉一个高版本。`npm@11.17.0` 是在本机 Node 24.11.1 上验证过能用的。)
186
+
114
187
  ## 8. 兜底:告诉用户怎么绕开镜像
115
188
 
116
189
  ```powershell
@@ -121,6 +194,30 @@ npm view weflow-cli version --registry=https://registry.npmjs.org # 用官方
121
194
  两个源结果不一致 → 是镜像同步问题,不是包的问题:
122
195
  `npm install -g weflow-cli@<版本> --registry=https://registry.npmjs.org` 立刻可用。
123
196
 
197
+ ## 建 GitHub Release 时的两个坑(2026-10-02 补 1.8.0/1.8.1/1.8.2 时实测)
198
+
199
+ - **正文上限 125,000 字符。** 1.8.0 那段 CHANGELOG 是 140,154 字符,直接贴会被 API 拒:
200
+ `HTTP 422 ... body is too long (maximum is 125000 characters)`。处理方式是**按 `### ` 小节边界
201
+ 机械截断**(放到装不下为止),末尾补一句"完整条目在 `CHANGELOG.md`"。**不要凭"哪几段更重要"去挑**:
202
+ 那既不可复现,也不是发布这件事该做的判断。
203
+ - **补历史的 Release 要显式 `--latest=false`。** `gh release create` 默认会把新页面标成 Latest ——
204
+ 给 1.8.x 补账时会把 1.9.0 从 Latest 上挤下去。补完用 `gh release list` 确认 Latest 还是最新的那个。
205
+ - **发布前先确认工作区干净**(`git status` 为空,HEAD 就是你打算打 tag 的那次提交)。
206
+ 理由不是形式主义,是实测出来的:2026-10-02 给 1.8.0/1.8.1/1.8.2 补 tag 时做了逐文件溯源
207
+ (拿 registry 上的 tarball 与 `git archive <tag>` 比,行尾差异不算),结果是 1.8.1 与 1.8.2 的
208
+ 发布源码与 tag 树**一致**,而 **1.8.0 的包里有两个文档文件在 git 里找不到对应状态**:
209
+ `docs/DECISIONS.md` 与更晚的一次提交一致,`README.en.md` 则**与任何提交都不一致**(比 tag 那份
210
+ 长约 7,000 字符)。代码部分没问题(逐字节相同),但**那次发布出去的文档再也回不来** ——
211
+ 这正是"不把 npm 发布包当作 GitHub 源码的实时镜像;先确认版本和提交"这条规矩要防的事。
212
+ - **1.9.0 的出处核过了(2026-10-11,用上面同一套办法)**:把 registry 上的 tarball 与
213
+ `git archive v1.9.0` 逐文件比(行尾归一后比内容):两边都有的 **289 个文件全部逐字节一致、0 处不同**;
214
+ 包里多出的 212 个是 `dist/`(构建产物、不在 git 里);tag 里有而包里没有的 124 个是白名单外的
215
+ 东西(`.github/`、`AGENTS.md`、`package-lock.json`、`scripts/*.ts` 等)。
216
+ ⚠️ 其中 **`logo.png` 与 `scripts/data/wechat_emoji.json` 两个文件当时确实没进包** —— 那正是
217
+ 1.10.0 修掉的缺口(同一件事,两条独立的路都指到它)。
218
+ **结论:1.8.0 那个"发出去的文档再也回不来"没有在 1.9.0 重演** ✅
219
+
220
+
124
221
  ## 发布后
125
222
 
126
223
  - 回到相关 issue 回复「已发布 + 升级方式」
@@ -162,6 +259,23 @@ npm view weflow-cli version --registry=https://registry.npmjs.org # 用官方
162
259
  - 手动跑一次:`py scripts/watch_issues.py`
163
260
  - 查看/改频率:`Get-ScheduledTask -TaskName 'WeFlow Issue Watch'`
164
261
 
262
+ **两个任务怎么建**(这一节此前只写"交给计划任务",**命令本身没记下来过** —— 而 `scripts/health_check.py`
263
+ 正是按这两个**名字**判红的,所以缺的从来不是判断,是这三行):
264
+
265
+ ```powershell
266
+ schtasks /create /tn "WeFlow Issue Watch" /sc minute /mo 30 ^
267
+ /tr "py \"<仓库路径>\scripts\watch_issues.py\""
268
+ schtasks /create /tn "WeFlow Mail Watch" /sc minute /mo 30 ^
269
+ /tr "py \"<仓库路径>\scripts\watch_mail.py\""
270
+ ```
271
+
272
+ - 查看:`Get-ScheduledTask -TaskName 'WeFlow *'` / 撤销:`schtasks /delete /tn "<任务名>" /f`
273
+ - **PATH 不用操心**(计划任务的 PATH 与交互式会话不同,这是这类任务常见的翻车点):`watch_issues.py`
274
+ 自己带兜底 —— `GH_CANDIDATES = ['gh', r'C:\Program Files\GitHub CLI\gh.exe']`;`py` 是系统启动器
275
+ (`C:\Windows\py.exe`)。两者都不依赖任务继承的 PATH。
276
+ - `watch_mail.py` 做的是**另一件事**:把白名单来信追加进本地队列 `~/.weflow-mail-queue.jsonl`
277
+ (不读别人的信、不发信;真正按指令做事要等一个会话在跑)。
278
+
165
279
  ### 发布后自检
166
280
 
167
281
  ```powershell
package/logo.png ADDED
Binary file
@@ -0,0 +1,108 @@
1
+ /**
2
+ * MCP 的 **HTTP 传输**(Streamable HTTP)。stdio 那条路一个字没动,这是**另一条**路。
3
+ *
4
+ * 为什么要它:stdio 只能被"能起进程"的客户端用(Claude Code、IDE 插件)。HTTP 那条路在
5
+ * 生态里已是常态——chatlog 就是拿 Streamable HTTP / SSE 把同一批查询能力摊出来的,于是浏览器型
6
+ * 客户端、常驻服务、多个客户端同时连都能用。工具集**完全一样**(同一张 `TOOL_DEFS` 派生表),
7
+ * 变的只是搬运动作的方式。
8
+ *
9
+ * 三道边界(判定在 `httpConfig.ts`,纯函数、有测试):
10
+ * 1. **默认不开**:不给 `--http` 就是 stdio,行为与以前逐字相同。
11
+ * 2. **必须带令牌**,且**只绑回环**(远程走 SSH 隧道,见 docs/MCP.md)。
12
+ * 3. **打开 DNS 重绑定防护**:SDK 的默认是 `false`,而本机 HTTP 服务最现实的攻击面就是
13
+ * "你正在浏览的某个网页往 127.0.0.1 发请求"。打开后 Host 头必须对得上,带 Origin 的
14
+ * 浏览器请求也必须在白名单里。
15
+ *
16
+ * 采用**无状态**模式(每个请求一套 server+transport):这一层只有只读查询,没有要跨请求维持的
17
+ * 会话,无状态就没有"会话之间串了"这类问题。
18
+ */
19
+ import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http'
20
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'
21
+ import type { Server as McpServer } from '@modelcontextprotocol/sdk/server/index.js'
22
+ import {
23
+ allowedHostsFor,
24
+ allowedOriginsFor,
25
+ isAuthorized,
26
+ type HttpOptions,
27
+ } from './httpConfig.js'
28
+
29
+ export const MCP_HTTP_PATH = '/mcp'
30
+
31
+ export interface RunningHttpServer {
32
+ /** 实际绑定的地址(端口传 0 时是内核挑的,所以以这里为准) */
33
+ url: string
34
+ port: number
35
+ close: () => Promise<void>
36
+ }
37
+
38
+ function json(res: ServerResponse, code: number, body: unknown): void {
39
+ res.writeHead(code, { 'content-type': 'application/json' })
40
+ res.end(JSON.stringify(body))
41
+ }
42
+
43
+ /**
44
+ * 起 HTTP 传输。`buildServer` 每次调用都必须返回一个**新的** Server(无状态模式每个请求一套)。
45
+ */
46
+ export async function startHttpServer(
47
+ opts: HttpOptions,
48
+ buildServer: () => McpServer,
49
+ log: (line: string) => void = () => {},
50
+ ): Promise<RunningHttpServer> {
51
+ let allowedHosts: string[] = []
52
+ let allowedOrigins: string[] = []
53
+
54
+ const handler = async (req: IncomingMessage, res: ServerResponse): Promise<void> => {
55
+ const url = req.url ?? '/'
56
+ if (url !== MCP_HTTP_PATH && !url.startsWith(MCP_HTTP_PATH + '?')) {
57
+ json(res, 404, { error: 'not found', hint: `MCP 端点在 ${MCP_HTTP_PATH}` })
58
+ return
59
+ }
60
+ // 令牌先判、再谈别的:没令牌的请求连"MCP 认不认识这个方法"都不该知道。
61
+ if (!isAuthorized(req.headers.authorization, opts.token)) {
62
+ json(res, 401, { jsonrpc: '2.0', error: { code: -32001, message: '未授权:缺少或不对的 Bearer 令牌' }, id: null })
63
+ return
64
+ }
65
+
66
+ const transport = new StreamableHTTPServerTransport({
67
+ sessionIdGenerator: undefined, // 无状态
68
+ enableJsonResponse: true, // 不强制 SSE:普通客户端拿 JSON 就好
69
+ enableDnsRebindingProtection: true, // SDK 默认 false,必须显式开
70
+ allowedHosts,
71
+ allowedOrigins,
72
+ })
73
+ const server = buildServer()
74
+ res.on('close', () => { void transport.close(); void server.close().catch(() => {}) })
75
+ try {
76
+ await server.connect(transport)
77
+ await transport.handleRequest(req, res)
78
+ } catch (error: any) {
79
+ log(`[mcp-http] 请求处理失败: ${error?.message ?? error}`)
80
+ if (!res.headersSent) {
81
+ json(res, 500, { jsonrpc: '2.0', error: { code: -32603, message: '内部错误' }, id: null })
82
+ }
83
+ }
84
+ }
85
+
86
+ const httpServer: Server = createServer((req, res) => { void handler(req, res) })
87
+
88
+ await new Promise<void>((resolve, reject) => {
89
+ httpServer.once('error', reject)
90
+ httpServer.listen(opts.port, opts.host, () => {
91
+ httpServer.removeListener('error', reject)
92
+ resolve()
93
+ })
94
+ })
95
+
96
+ const address = httpServer.address()
97
+ const port = typeof address === 'object' && address ? address.port : opts.port
98
+ // **绑定之后**才算白名单:端口传 0 时真实端口此刻才知道,而白名单必须用真实端口。
99
+ allowedHosts = allowedHostsFor(opts.host, port)
100
+ allowedOrigins = allowedOriginsFor(opts.host, port)
101
+
102
+ const shownHost = opts.host === '::1' ? '[::1]' : opts.host
103
+ return {
104
+ url: `http://${shownHost}:${port}${MCP_HTTP_PATH}`,
105
+ port,
106
+ close: () => new Promise<void>((resolve) => { httpServer.close(() => resolve()) }),
107
+ }
108
+ }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * HTTP 传输的**判定**——纯函数,不碰 socket、不连 MCP,所以能离线测。
3
+ *
4
+ * 为什么值得单独一个文件:这里每一条规则决定的都是"要不要把本机的私密数据摊到一个网络
5
+ * 端口上"。服务器本体要起 socket、要装 SDK,测起来重;把判定抽出来,规则就能一条一条钉住
6
+ * (`test/mcp-http.test.ts`)。
7
+ *
8
+ * 边界(写死在这里,不靠调用方自觉):
9
+ * - **默认不开**:不给 `--http` 也不给 `WEFLOW_MCP_HTTP` 就还是 stdio,行为与以前逐字相同。
10
+ * - **必须带令牌**:没给就**拒绝启动**,不给默认值 —— 一个悄悄生成的默认口令比没有更坏,
11
+ * 因为它看起来是"配好了"。
12
+ * - **只绑回环**:远程访问自己走 SSH 隧道或反向代理。把私密聊天记录摊到局域网上,
13
+ * 不该由这个文件顺手打开。
14
+ */
15
+
16
+ import { createHash, timingSafeEqual } from 'node:crypto'
17
+
18
+ /** 默认端口。5030 是 chatlog 的,避开它免得两个服务打架。 */
19
+ export const DEFAULT_HTTP_PORT = 8790
20
+
21
+ /**
22
+ * 令牌长度下限。128 位随机(32 个 hex 字符)是常态;24 个字符足够排除"随手敲一个",
23
+ * 又不至于逼人非得用某个特定长度。
24
+ */
25
+ export const MIN_TOKEN_CHARS = 24
26
+
27
+ /** 允许绑定的地址。**只回环**:`0.0.0.0` 不在此列,而且不是"忘了加"。 */
28
+ export const LOOPBACK_HOSTS = ['127.0.0.1', 'localhost', '::1']
29
+
30
+ export interface HttpOptions {
31
+ host: string
32
+ port: number
33
+ token: string
34
+ }
35
+
36
+ /** 这一轮是不是要走 HTTP。`--http`、`--http=8790`、`WEFLOW_MCP_HTTP=1` 都算。 */
37
+ export function httpModeRequested(env: Record<string, string | undefined>, argv: string[]): boolean {
38
+ if (argv.some((a) => a === '--http' || a.startsWith('--http='))) return true
39
+ const value = String(env.WEFLOW_MCP_HTTP ?? '').trim()
40
+ return value !== '' && value !== '0' && value.toLowerCase() !== 'false'
41
+ }
42
+
43
+ function flagValue(argv: string[], name: string): string | undefined {
44
+ const hit = argv.find((a) => a.startsWith(`${name}=`))
45
+ return hit ? hit.slice(name.length + 1) : undefined
46
+ }
47
+
48
+ /**
49
+ * 解出 HTTP 模式的参数。**不走 HTTP 就返回 `null`**;要走但配置不合法就**抛错**(fail closed,
50
+ * 绝不"降级成不安全的默认值"继续跑)。
51
+ *
52
+ * 端口允许 `0`:那是让内核挑一个空闲端口,测试要用;正式跑用默认端口。
53
+ */
54
+ export function resolveHttpOptions(
55
+ env: Record<string, string | undefined>,
56
+ argv: string[],
57
+ ): HttpOptions | null {
58
+ if (!httpModeRequested(env, argv)) return null
59
+
60
+ const token = String(flagValue(argv, '--token') ?? env.WEFLOW_MCP_TOKEN ?? '').trim()
61
+ if (!token) {
62
+ throw new Error(
63
+ 'HTTP 模式必须给令牌:`--token=<随机串>` 或环境变量 WEFLOW_MCP_TOKEN。\n'
64
+ + '没有默认值是有意的 —— 一个自动生成、只在日志里露一面的口令,看起来像"配好了"。\n'
65
+ + '生成一个:node -e "console.log(require(\'crypto\').randomBytes(32).toString(\'hex\'))"',
66
+ )
67
+ }
68
+ if (token.length < MIN_TOKEN_CHARS) {
69
+ throw new Error(`令牌太短(${token.length} < ${MIN_TOKEN_CHARS} 个字符)。用随机串,别用可猜的短语。`)
70
+ }
71
+
72
+ const host = String(flagValue(argv, '--host') ?? '127.0.0.1').trim()
73
+ if (!LOOPBACK_HOSTS.includes(host)) {
74
+ throw new Error(
75
+ `只允许绑回环地址(${LOOPBACK_HOSTS.join(' / ')}),收到 ${host}。\n`
76
+ + '要从别的机器连,用 SSH 隧道(ssh -L 8790:127.0.0.1:8790 <host>)或自己的反向代理 —— '
77
+ + '把聊天记录摊到局域网上不该由这个命令顺手打开。',
78
+ )
79
+ }
80
+
81
+ const raw = flagValue(argv, '--http') ?? flagValue(argv, '--port') ?? env.WEFLOW_MCP_HTTP_PORT ?? String(DEFAULT_HTTP_PORT)
82
+ const rawText = String(raw).trim()
83
+ // 先按"纯数字"卡一道:`Number('') === 0`,不卡的话空值会静默变成一个端口。
84
+ if (!/^\d{1,5}$/.test(rawText)) {
85
+ throw new Error(`端口要 0-65535 之间的整数(0 = 让系统挑一个空闲端口),收到 ${JSON.stringify(raw)}`)
86
+ }
87
+ const port = Number(rawText)
88
+ if (port > 65535) {
89
+ throw new Error(`端口要 0-65535 之间的整数,收到 ${JSON.stringify(raw)}`)
90
+ }
91
+
92
+ return { host, port, token }
93
+ }
94
+
95
+ /**
96
+ * `Authorization` 头认不认。
97
+ *
98
+ * 比的是**摘要**而不是原文:`timingSafeEqual` 要求等长,直接比长度会漏出"令牌有多长",
99
+ * 于是"不同长度直接返回 false"就成了一个可测的短路径。两边先过一遍 SHA-256,长度就固定了。
100
+ * 方案名按 RFC 7235 大小写不敏感。
101
+ */
102
+ export function isAuthorized(header: string | undefined, token: string): boolean {
103
+ const raw = String(header ?? '')
104
+ const space = raw.indexOf(' ')
105
+ if (space < 0) return false
106
+ if (raw.slice(0, space).toLowerCase() !== 'bearer') return false
107
+ const presented = raw.slice(space + 1).trim()
108
+ if (!presented) return false
109
+
110
+ const digest = (value: string) => createHash('sha256').update(value, 'utf8').digest()
111
+ return timingSafeEqual(digest(presented), digest(token))
112
+ }
113
+
114
+ /** Host 头允许的取值(DNS 重绑定防护用)。带端口,因为 SDK 比的是**原始 Host 头**。 */
115
+ export function allowedHostsFor(host: string, port: number): string[] {
116
+ const forms = new Set<string>()
117
+ for (const h of host === 'localhost' ? ['127.0.0.1', 'localhost'] : [host]) {
118
+ forms.add(h === '::1' ? `[::1]:${port}` : `${h}:${port}`)
119
+ }
120
+ return [...forms]
121
+ }
122
+
123
+ /** 允许的 Origin(浏览器来的请求会带;命令行客户端通常不带,不带就放行)。 */
124
+ export function allowedOriginsFor(host: string, port: number): string[] {
125
+ return allowedHostsFor(host, port).map((h) => `http://${h}`)
126
+ }