weflow-cli 1.5.0 → 1.6.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.
Files changed (332) hide show
  1. package/ARCHITECTURE.md +105 -0
  2. package/CHANGELOG.md +121 -0
  3. package/CONTRIBUTING.md +45 -0
  4. package/LICENSE +21 -21
  5. package/OPERATIONS.md +272 -0
  6. package/README.en.md +281 -0
  7. package/README.md +334 -149
  8. package/SECURITY.md +47 -0
  9. package/bin/weflow-cli.ts +4050 -581
  10. package/dist/bin/weflow-cli.js +4310 -588
  11. package/dist/bin/weflow-cli.js.map +1 -1
  12. package/dist/mcp-server/index.js +178 -236
  13. package/dist/mcp-server/index.js.map +1 -1
  14. package/dist/src/core/dbPathService.d.ts +15 -1
  15. package/dist/src/core/dbPathService.d.ts.map +1 -1
  16. package/dist/src/core/dbPathService.js +177 -19
  17. package/dist/src/core/dbPathService.js.map +1 -1
  18. package/dist/src/core/keyService.d.ts +8 -0
  19. package/dist/src/core/keyService.d.ts.map +1 -1
  20. package/dist/src/core/keyService.js +127 -22
  21. package/dist/src/core/keyService.js.map +1 -1
  22. package/dist/src/core/ntCore.d.ts +59 -1
  23. package/dist/src/core/ntCore.d.ts.map +1 -1
  24. package/dist/src/core/ntCore.js +201 -65
  25. package/dist/src/core/ntCore.js.map +1 -1
  26. package/dist/src/core/sqlcipherCore.d.ts.map +1 -1
  27. package/dist/src/core/sqlcipherCore.js +18 -1
  28. package/dist/src/core/sqlcipherCore.js.map +1 -1
  29. package/dist/src/core/wcdbCore.d.ts.map +1 -1
  30. package/dist/src/core/wcdbCore.js +3 -6
  31. package/dist/src/core/wcdbCore.js.map +1 -1
  32. package/dist/src/services/assistantDaemon.d.ts +19 -0
  33. package/dist/src/services/assistantDaemon.d.ts.map +1 -0
  34. package/dist/src/services/assistantDaemon.js +117 -0
  35. package/dist/src/services/assistantDaemon.js.map +1 -0
  36. package/dist/src/services/assistantMemory.d.ts +36 -0
  37. package/dist/src/services/assistantMemory.d.ts.map +1 -0
  38. package/dist/src/services/assistantMemory.js +182 -0
  39. package/dist/src/services/assistantMemory.js.map +1 -0
  40. package/dist/src/services/assistantPrivacy.d.ts +25 -0
  41. package/dist/src/services/assistantPrivacy.d.ts.map +1 -0
  42. package/dist/src/services/assistantPrivacy.js +77 -0
  43. package/dist/src/services/assistantPrivacy.js.map +1 -0
  44. package/dist/src/services/assistantRouting.d.ts +18 -0
  45. package/dist/src/services/assistantRouting.d.ts.map +1 -0
  46. package/dist/src/services/assistantRouting.js +68 -0
  47. package/dist/src/services/assistantRouting.js.map +1 -0
  48. package/dist/src/services/assistantService.d.ts +37 -0
  49. package/dist/src/services/assistantService.d.ts.map +1 -0
  50. package/dist/src/services/assistantService.js +284 -0
  51. package/dist/src/services/assistantService.js.map +1 -0
  52. package/dist/src/services/assistantTools.d.ts +24 -0
  53. package/dist/src/services/assistantTools.d.ts.map +1 -0
  54. package/dist/src/services/assistantTools.js +582 -0
  55. package/dist/src/services/assistantTools.js.map +1 -0
  56. package/dist/src/services/chatService.d.ts +44 -0
  57. package/dist/src/services/chatService.d.ts.map +1 -1
  58. package/dist/src/services/chatService.js +105 -4
  59. package/dist/src/services/chatService.js.map +1 -1
  60. package/dist/src/services/configService.d.ts +55 -0
  61. package/dist/src/services/configService.d.ts.map +1 -1
  62. package/dist/src/services/configService.js +147 -6
  63. package/dist/src/services/configService.js.map +1 -1
  64. package/dist/src/services/evidenceService.d.ts +30 -0
  65. package/dist/src/services/evidenceService.d.ts.map +1 -0
  66. package/dist/src/services/evidenceService.js +98 -0
  67. package/dist/src/services/evidenceService.js.map +1 -0
  68. package/dist/src/services/exportService.d.ts +13 -20
  69. package/dist/src/services/exportService.d.ts.map +1 -1
  70. package/dist/src/services/exportService.js +113 -47
  71. package/dist/src/services/exportService.js.map +1 -1
  72. package/dist/src/services/initKeyService.d.ts +19 -0
  73. package/dist/src/services/initKeyService.d.ts.map +1 -0
  74. package/dist/src/services/initKeyService.js +80 -0
  75. package/dist/src/services/initKeyService.js.map +1 -0
  76. package/dist/src/services/messageContract.d.ts +23 -0
  77. package/dist/src/services/messageContract.d.ts.map +1 -0
  78. package/dist/src/services/messageContract.js +49 -0
  79. package/dist/src/services/messageContract.js.map +1 -0
  80. package/dist/src/services/messageQuery.d.ts +7 -0
  81. package/dist/src/services/messageQuery.d.ts.map +1 -0
  82. package/dist/src/services/messageQuery.js +30 -0
  83. package/dist/src/services/messageQuery.js.map +1 -0
  84. package/dist/src/services/wechat-formatter.js +14 -14
  85. package/dist/src/services/wechatMessageService.d.ts.map +1 -1
  86. package/dist/src/services/wechatMessageService.js +31 -3
  87. package/dist/src/services/wechatMessageService.js.map +1 -1
  88. package/dist/src/services/whitelistService.d.ts +52 -4
  89. package/dist/src/services/whitelistService.d.ts.map +1 -1
  90. package/dist/src/services/whitelistService.js +183 -17
  91. package/dist/src/services/whitelistService.js.map +1 -1
  92. package/dist/src/types.d.ts +20 -0
  93. package/dist/src/types.d.ts.map +1 -1
  94. package/dist/src/utils/dateRange.d.ts +15 -0
  95. package/dist/src/utils/dateRange.d.ts.map +1 -0
  96. package/dist/src/utils/dateRange.js +48 -0
  97. package/dist/src/utils/dateRange.js.map +1 -0
  98. package/dist/src/utils/mcpSecurity.d.ts +5 -0
  99. package/dist/src/utils/mcpSecurity.d.ts.map +1 -0
  100. package/dist/src/utils/mcpSecurity.js +39 -0
  101. package/dist/src/utils/mcpSecurity.js.map +1 -0
  102. package/dist/src/utils/packageRoot.d.ts +2 -0
  103. package/dist/src/utils/packageRoot.d.ts.map +1 -0
  104. package/dist/src/utils/packageRoot.js +16 -0
  105. package/dist/src/utils/packageRoot.js.map +1 -0
  106. package/dist/src/utils/python.d.ts +2 -0
  107. package/dist/src/utils/python.d.ts.map +1 -0
  108. package/dist/src/utils/python.js +72 -0
  109. package/dist/src/utils/python.js.map +1 -0
  110. package/dist/src/utils/pythonProcessEnv.d.ts +3 -0
  111. package/dist/src/utils/pythonProcessEnv.d.ts.map +1 -0
  112. package/dist/src/utils/pythonProcessEnv.js +46 -0
  113. package/dist/src/utils/pythonProcessEnv.js.map +1 -0
  114. package/dist/src/utils/talkerUtils.d.ts +4 -1
  115. package/dist/src/utils/talkerUtils.d.ts.map +1 -1
  116. package/dist/src/utils/talkerUtils.js +9 -9
  117. package/dist/src/utils/talkerUtils.js.map +1 -1
  118. package/dist/src/utils/wechatEmoji.d.ts +5 -0
  119. package/dist/src/utils/wechatEmoji.d.ts.map +1 -0
  120. package/dist/src/utils/wechatEmoji.js +35 -0
  121. package/dist/src/utils/wechatEmoji.js.map +1 -0
  122. package/docs/AI_INTERFACE.md +189 -0
  123. package/docs/BRANCHES.md +87 -0
  124. package/docs/DATA_CONTRACT.md +74 -0
  125. package/docs/DECISIONS.md +270 -0
  126. package/docs/DEEPSEEK_V4_FLASH/346/226/275/345/267/245/346/226/207/346/241/243.md +234 -0
  127. package/docs/EVIDENCE_GUIDE.md +96 -0
  128. package/docs/MCP.md +95 -0
  129. package/docs/PARTNERS.md +41 -0
  130. package/docs/PROJECT_STATE.md +96 -0
  131. package/docs/RELEASING.md +124 -0
  132. package/docs/ROADMAP.md +191 -0
  133. package/docs/SETUP.md +103 -0
  134. package/docs/images/weflow-architecture-gpt-image-2.png +0 -0
  135. package/docs/images/weflow-architecture.png +0 -0
  136. package/docs/images/weflow-architecture.svg +65 -0
  137. package/mcp-server/index.ts +169 -269
  138. package/package.json +81 -72
  139. package/requirements-3x.txt +4 -0
  140. package/requirements-voice.txt +15 -0
  141. package/requirements.txt +9 -0
  142. package/resources/js/marked.min.js +69 -0
  143. package/resources/wechat-emoji/666.png +0 -0
  144. package/resources/wechat-emoji/Awesome.png +0 -0
  145. package/resources/wechat-emoji/Concerned.png +0 -0
  146. package/resources/wechat-emoji/Cry.png +0 -0
  147. package/resources/wechat-emoji/Emm.png +0 -0
  148. package/resources/wechat-emoji/Facepalm.png +0 -0
  149. package/resources/wechat-emoji/Grin.png +0 -0
  150. package/resources/wechat-emoji/OK.png +0 -0
  151. package/resources/wechat-emoji/Respect.png +0 -0
  152. package/resources/wechat-emoji/Sick.png +0 -0
  153. package/resources/wechat-emoji/Sleep.png +0 -0
  154. package/resources/wechat-emoji/Smile.png +0 -0
  155. package/resources/wechat-emoji//344/272/262/344/272/262.png +0 -0
  156. package/resources/wechat-emoji//344/276/277/344/276/277.png +0 -0
  157. package/resources/wechat-emoji//345/201/267/347/254/221.png +0 -0
  158. package/resources/wechat-emoji//345/202/262/346/205/242.png +0 -0
  159. package/resources/wechat-emoji//345/206/215/350/247/201.png +0 -0
  160. package/resources/wechat-emoji//345/207/213/350/260/242.png +0 -0
  161. package/resources/wechat-emoji//345/212/240/346/262/271.png +0 -0
  162. package/resources/wechat-emoji//345/213/276/345/274/225.png +0 -0
  163. package/resources/wechat-emoji//345/217/221/345/221/206.png +0 -0
  164. package/resources/wechat-emoji//345/217/221/346/200/222.png +0 -0
  165. package/resources/wechat-emoji//345/217/221/346/212/226.png +0 -0
  166. package/resources/wechat-emoji//345/217/257/346/200/234.png +0 -0
  167. package/resources/wechat-emoji//345/217/263/345/223/274/345/223/274.png +0 -0
  168. package/resources/wechat-emoji//345/217/271/346/260/224.png +0 -0
  169. package/resources/wechat-emoji//345/220/203/347/223/234.png +0 -0
  170. package/resources/wechat-emoji//345/220/210/345/215/201.png +0 -0
  171. package/resources/wechat-emoji//345/220/220.png +0 -0
  172. package/resources/wechat-emoji//345/221/262/347/211/231.png +0 -0
  173. package/resources/wechat-emoji//345/222/222/351/252/202.png +0 -0
  174. package/resources/wechat-emoji//345/222/226/345/225/241.png +0 -0
  175. package/resources/wechat-emoji//345/223/207.png +0 -0
  176. package/resources/wechat-emoji//345/225/244/351/205/222.png +0 -0
  177. package/resources/wechat-emoji//345/230/230.png +0 -0
  178. package/resources/wechat-emoji//345/230/264/345/224/207.png +0 -0
  179. package/resources/wechat-emoji//345/230/277/345/223/210.png +0 -0
  180. package/resources/wechat-emoji//345/233/247.png +0 -0
  181. package/resources/wechat-emoji//345/233/260.png +0 -0
  182. package/resources/wechat-emoji//345/235/217/347/254/221.png +0 -0
  183. package/resources/wechat-emoji//345/244/247/345/223/255.png +0 -0
  184. package/resources/wechat-emoji//345/244/251/345/225/212.png +0 -0
  185. package/resources/wechat-emoji//345/244/252/351/230/263.png +0 -0
  186. package/resources/wechat-emoji//345/244/261/346/234/233.png +0 -0
  187. package/resources/wechat-emoji//345/245/270/347/254/221.png +0 -0
  188. package/resources/wechat-emoji//345/245/275/347/232/204.png +0 -0
  189. package/resources/wechat-emoji//345/247/224/345/261/210.png +0 -0
  190. package/resources/wechat-emoji//345/256/263/347/276/236.png +0 -0
  191. package/resources/wechat-emoji//345/260/264/345/260/254.png +0 -0
  192. package/resources/wechat-emoji//345/272/206/347/245/235.png +0 -0
  193. package/resources/wechat-emoji//345/274/261.png +0 -0
  194. package/resources/wechat-emoji//345/274/272.png +0 -0
  195. package/resources/wechat-emoji//345/276/227/346/204/217.png +0 -0
  196. package/resources/wechat-emoji//345/276/256/347/254/221.png +0 -0
  197. package/resources/wechat-emoji//345/277/203/347/242/216.png +0 -0
  198. package/resources/wechat-emoji//345/277/253/345/223/255/344/272/206.png +0 -0
  199. package/resources/wechat-emoji//346/201/220/346/203/247.png +0 -0
  200. package/resources/wechat-emoji//346/202/240/351/227/262.png +0 -0
  201. package/resources/wechat-emoji//346/203/212/346/201/220.png +0 -0
  202. package/resources/wechat-emoji//346/203/212/350/256/266.png +0 -0
  203. package/resources/wechat-emoji//346/204/211/345/277/253.png +0 -0
  204. package/resources/wechat-emoji//346/206/250/347/254/221.png +0 -0
  205. package/resources/wechat-emoji//346/211/223/350/204/270.png +0 -0
  206. package/resources/wechat-emoji//346/212/223/347/213/202.png +0 -0
  207. package/resources/wechat-emoji//346/212/240/351/274/273.png +0 -0
  208. package/resources/wechat-emoji//346/212/261/346/213/263.png +0 -0
  209. package/resources/wechat-emoji//346/213/245/346/212/261.png +0 -0
  210. package/resources/wechat-emoji//346/213/263/345/244/264.png +0 -0
  211. package/resources/wechat-emoji//346/215/202/350/204/270.png +0 -0
  212. package/resources/wechat-emoji//346/217/241/346/211/213.png +0 -0
  213. package/resources/wechat-emoji//346/222/207/345/230/264.png +0 -0
  214. package/resources/wechat-emoji//346/223/246/346/261/227.png +0 -0
  215. package/resources/wechat-emoji//346/225/262/346/211/223.png +0 -0
  216. package/resources/wechat-emoji//346/227/240/350/257/255.png +0 -0
  217. package/resources/wechat-emoji//346/227/272/346/237/264.png +0 -0
  218. package/resources/wechat-emoji//346/231/225.png +0 -0
  219. package/resources/wechat-emoji//346/234/210/344/272/256.png +0 -0
  220. package/resources/wechat-emoji//346/234/272/346/231/272.png +0 -0
  221. package/resources/wechat-emoji//346/261/227.png +0 -0
  222. package/resources/wechat-emoji//346/265/201/346/263/252.png +0 -0
  223. package/resources/wechat-emoji//347/202/270/345/274/271.png +0 -0
  224. package/resources/wechat-emoji//347/203/237/350/212/261.png +0 -0
  225. package/resources/wechat-emoji//347/210/206/347/253/271.png +0 -0
  226. package/resources/wechat-emoji//347/210/261/345/277/203.png +0 -0
  227. package/resources/wechat-emoji//347/214/252/345/244/264.png +0 -0
  228. package/resources/wechat-emoji//347/216/253/347/221/260.png +0 -0
  229. package/resources/wechat-emoji//347/224/237/347/227/205.png +0 -0
  230. package/resources/wechat-emoji//347/226/221/351/227/256.png +0 -0
  231. package/resources/wechat-emoji//347/231/274.png +0 -0
  232. package/resources/wechat-emoji//347/231/275/347/234/274.png +0 -0
  233. package/resources/wechat-emoji//347/232/261/347/234/211.png +0 -0
  234. package/resources/wechat-emoji//347/235/241.png +0 -0
  235. package/resources/wechat-emoji//347/240/264/346/266/225/344/270/272/347/254/221.png +0 -0
  236. package/resources/wechat-emoji//347/244/274/347/211/251.png +0 -0
  237. package/resources/wechat-emoji//347/244/276/344/274/232/347/244/276/344/274/232.png +0 -0
  238. package/resources/wechat-emoji//347/246/217.png +0 -0
  239. package/resources/wechat-emoji//347/254/221/350/204/270.png +0 -0
  240. package/resources/wechat-emoji//347/272/242/345/214/205.png +0 -0
  241. package/resources/wechat-emoji//347/277/273/347/231/275/347/234/274.png +0 -0
  242. package/resources/wechat-emoji//350/200/266.png +0 -0
  243. package/resources/wechat-emoji//350/203/234/345/210/251.png +0 -0
  244. package/resources/wechat-emoji//350/204/270/347/272/242.png +0 -0
  245. package/resources/wechat-emoji//350/211/262.png +0 -0
  246. package/resources/wechat-emoji//350/213/246/346/266/251.png +0 -0
  247. package/resources/wechat-emoji//350/217/234/345/210/200.png +0 -0
  248. package/resources/wechat-emoji//350/233/213/347/263/225.png +0 -0
  249. package/resources/wechat-emoji//350/241/260.png +0 -0
  250. package/resources/wechat-emoji//350/243/202/345/274/200.png +0 -0
  251. package/resources/wechat-emoji//350/256/251/346/210/221/347/234/213/347/234/213.png +0 -0
  252. package/resources/wechat-emoji//350/260/203/347/232/256.png +0 -0
  253. package/resources/wechat-emoji//350/267/263/350/267/263.png +0 -0
  254. package/resources/wechat-emoji//350/275/254/345/234/210.png +0 -0
  255. package/resources/wechat-emoji//351/204/231/350/247/206.png +0 -0
  256. package/resources/wechat-emoji//351/227/255/345/230/264.png +0 -0
  257. package/resources/wechat-emoji//351/230/264/351/231/251.png +0 -0
  258. package/resources/wechat-emoji//351/232/276/350/277/207.png +0 -0
  259. package/resources/wechat-emoji//351/252/267/351/253/205.png +0 -0
  260. package/resources/wechat-emoji//351/274/223/346/216/214.png +0 -0
  261. package/scripts/_batch_link_sources.py +73 -73
  262. package/scripts/_utils.py +29 -0
  263. package/scripts/annual_report.py +796 -796
  264. package/scripts/auto_tag.py +128 -128
  265. package/scripts/biz_daily.py +210 -53
  266. package/scripts/chat_report.py +10 -0
  267. package/scripts/chat_stats.py +702 -702
  268. package/scripts/classify_daily.py +17 -11
  269. package/scripts/create_reading_notes.py +370 -370
  270. package/scripts/daily_stats.py +132 -0
  271. package/scripts/enrich_backlinks.py +163 -163
  272. package/scripts/export_chat_html.py +1704 -107
  273. package/scripts/extract_todos.py +445 -404
  274. package/scripts/fav_server.py +119 -12
  275. package/scripts/fix_topics.py +111 -111
  276. package/scripts/generate_ai_report.py +1 -1
  277. package/scripts/generate_html.py +597 -277
  278. package/scripts/inspection_records.py +354 -0
  279. package/scripts/mcp_bridge.py +398 -398
  280. package/scripts/nt_decrypt.py +702 -48
  281. package/scripts/pipeline.py +27 -19
  282. package/scripts/promote_all.py +312 -310
  283. package/scripts/promote_ideas.py +299 -297
  284. package/scripts/rag_chat.py +194 -191
  285. package/scripts/semantic_search.py +435 -431
  286. package/scripts/sync_fav.py +201 -146
  287. package/scripts/sync_weread.py +192 -192
  288. package/scripts/vault_rag.py +140 -139
  289. package/scripts/vault_search.py +141 -141
  290. package/scripts/wechat_emoji.py +132 -0
  291. package/scripts/wechat_emoticon.py +433 -0
  292. package/scripts/wechat_image.py +258 -0
  293. package/scripts/wechat_voice.py +327 -0
  294. package/src/core/dbPathService.ts +163 -18
  295. package/src/core/keyService.ts +137 -25
  296. package/src/core/ntCore.ts +215 -62
  297. package/src/core/sqlcipherCore.ts +17 -1
  298. package/src/core/wcdbCore.ts +3 -7
  299. package/src/services/assistantDaemon.ts +111 -0
  300. package/src/services/assistantMemory.ts +193 -0
  301. package/src/services/assistantPrivacy.ts +87 -0
  302. package/src/services/assistantRouting.ts +92 -0
  303. package/src/services/assistantService.ts +295 -0
  304. package/src/services/assistantTools.ts +549 -0
  305. package/src/services/chatService.ts +133 -4
  306. package/src/services/configService.ts +194 -6
  307. package/src/services/evidenceService.ts +129 -0
  308. package/src/services/exportService.ts +113 -48
  309. package/src/services/initKeyService.ts +83 -0
  310. package/src/services/messageContract.ts +77 -0
  311. package/src/services/messageQuery.ts +38 -0
  312. package/src/services/wechat-formatter.ts +348 -348
  313. package/src/services/wechatMessageService.ts +25 -3
  314. package/src/services/wereadService.ts +308 -308
  315. package/src/services/whitelistService.ts +209 -22
  316. package/src/types.ts +20 -0
  317. package/src/utils/dateRange.ts +58 -0
  318. package/src/utils/mcpSecurity.ts +39 -0
  319. package/src/utils/packageRoot.ts +14 -0
  320. package/src/utils/python.ts +73 -0
  321. package/src/utils/pythonProcessEnv.ts +45 -0
  322. package/src/utils/talkerUtils.ts +14 -2
  323. package/dist/src/utils/errors.d.ts +0 -25
  324. package/dist/src/utils/errors.d.ts.map +0 -1
  325. package/dist/src/utils/errors.js +0 -43
  326. package/dist/src/utils/errors.js.map +0 -1
  327. package/dist/src/utils/pythonRunner.d.ts +0 -27
  328. package/dist/src/utils/pythonRunner.d.ts.map +0 -1
  329. package/dist/src/utils/pythonRunner.js +0 -62
  330. package/dist/src/utils/pythonRunner.js.map +0 -1
  331. package/src/utils/errors.ts +0 -42
  332. package/src/utils/pythonRunner.ts +0 -81
@@ -11,11 +11,35 @@ import {
11
11
  CallToolRequestSchema,
12
12
  ListToolsRequestSchema,
13
13
  } from '@modelcontextprotocol/sdk/types.js'
14
- import { readFileSync, readdirSync, existsSync, statSync, writeFileSync, mkdirSync } from 'fs'
15
- import { join, resolve } from 'path'
16
- import { deflateSync } from 'zlib'
14
+ import { readFileSync, readdirSync, existsSync, statSync } from 'fs'
15
+ import { join } from 'path'
16
+ import { isAllowedWeChatArticleUrl, safeChildPath, safeDate } from '../src/utils/mcpSecurity.js'
17
+ import { resolvePackageRoot } from '../src/utils/packageRoot.js'
18
+ import { chatService } from '../src/services/chatService.js'
19
+ import { createWeFlowEnvelope } from '../src/services/messageContract.js'
17
20
 
18
21
  // ---- 微信公众号文章抓取 ----
22
+ async function readResponseTextLimited(response: Response, maximumBytes: number): Promise<string> {
23
+ const contentLength = Number(response.headers.get('content-length') || 0)
24
+ if (contentLength > maximumBytes) throw new Error('RESPONSE_TOO_LARGE')
25
+ if (!response.body) return ''
26
+
27
+ const reader = response.body.getReader()
28
+ const chunks: Uint8Array[] = []
29
+ let size = 0
30
+ while (true) {
31
+ const { done, value } = await reader.read()
32
+ if (done) break
33
+ size += value.byteLength
34
+ if (size > maximumBytes) {
35
+ await reader.cancel()
36
+ throw new Error('RESPONSE_TOO_LARGE')
37
+ }
38
+ chunks.push(value)
39
+ }
40
+ return Buffer.concat(chunks).toString('utf8')
41
+ }
42
+
19
43
  async function fetchWeChatArticle(url: string): Promise<{
20
44
  title: string
21
45
  author: string
@@ -23,16 +47,28 @@ async function fetchWeChatArticle(url: string): Promise<{
23
47
  content: string
24
48
  images: string[]
25
49
  }> {
26
- const resp = await fetch(url, {
27
- headers: {
28
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
29
- 'Accept': 'text/html,application/xhtml+xml',
30
- 'Accept-Language': 'zh-CN,zh;q=0.9',
31
- },
32
- redirect: 'follow',
33
- })
50
+ let currentUrl = url
51
+ let resp: Response | null = null
52
+ for (let redirectCount = 0; redirectCount <= 3; redirectCount++) {
53
+ if (!isAllowedWeChatArticleUrl(currentUrl)) throw new Error('URL_NOT_ALLOWED')
54
+ resp = await fetch(currentUrl, {
55
+ headers: {
56
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
57
+ 'Accept': 'text/html,application/xhtml+xml',
58
+ 'Accept-Language': 'zh-CN,zh;q=0.9',
59
+ },
60
+ redirect: 'manual',
61
+ signal: AbortSignal.timeout(20_000),
62
+ })
63
+ if (resp.status < 300 || resp.status >= 400) break
64
+ const location = resp.headers.get('location')
65
+ if (!location) throw new Error('REDIRECT_WITHOUT_LOCATION')
66
+ currentUrl = new URL(location, currentUrl).toString()
67
+ resp = null
68
+ }
69
+ if (!resp) throw new Error('TOO_MANY_REDIRECTS')
34
70
  if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${resp.statusText}`)
35
- const html = await resp.text()
71
+ const html = await readResponseTextLimited(resp, 5 * 1024 * 1024)
36
72
 
37
73
  // 提取 meta 信息
38
74
  const title = html.match(/<meta[^>]*property="og:title"[^>]*content="([^"]+)"/i)?.[1]
@@ -110,103 +146,75 @@ function htmlToMarkdown(html: string): string {
110
146
  return md
111
147
  }
112
148
  import { formatWeChatArticle, listThemes } from '../src/services/wechat-formatter.js'
149
+ import { boundedToolInteger, MCP_READ_ONLY_TOOL_DEFS, executeTool } from '../src/services/assistantTools.js'
150
+ import { AssistantMemory } from '../src/services/assistantMemory.js'
151
+
152
+ // ---- 本地微信数据工具 (复用 assistant 工具层, 微信 bot / MCP server 同源) ----
153
+ // MCP 无微信用户身份, 记忆固定挂 'mcp' 用户下; 与 assistant 守护进程共用同一持久化文件
154
+ const mcpMemory = new AssistantMemory()
155
+ const MCP_TOOL_CTX = { userId: 'mcp', memory: mcpMemory }
156
+
157
+ /** assistant 工具 (OpenAI function 格式) → MCP 工具格式; get_stats 并入现有 wechat.get_stats */
158
+ const ASSISTANT_TOOLS = MCP_READ_ONLY_TOOL_DEFS
159
+ .filter(t => t.function.name !== 'get_stats')
160
+ .map(t => ({
161
+ name: `wechat.${t.function.name}`,
162
+ description: t.function.description,
163
+ inputSchema: t.function.parameters,
164
+ }))
113
165
 
114
- // ---- 微信公众号 API 辅助 ----
115
- const WECHAT_APP_ID = process.env.WECHAT_APPID || ''
116
- const WECHAT_APP_SECRET = process.env.WECHAT_APPSECRET || ''
117
-
118
- interface TokenCache {
119
- token: string
120
- expiresAt: number
121
- }
122
- let _tokenCache: TokenCache | null = null
123
-
124
- async function getWeChatToken(): Promise<string> {
125
- if (_tokenCache && Date.now() < _tokenCache.expiresAt - 60000) {
126
- return _tokenCache.token
127
- }
128
- if (!WECHAT_APP_ID || !WECHAT_APP_SECRET) {
129
- throw new Error('请设置环境变量 WECHAT_APPID 和 WECHAT_APPSECRET')
130
- }
131
- const resp = await fetch(
132
- `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${WECHAT_APP_ID}&secret=${WECHAT_APP_SECRET}`
133
- )
134
- const data = await resp.json() as any
135
- if (data.errcode) {
136
- throw new Error(`微信 Token 获取失败: ${data.errmsg} (${data.errcode})`)
137
- }
138
- _tokenCache = {
139
- token: data.access_token,
140
- expiresAt: Date.now() + (data.expires_in - 60) * 1000,
166
+ const PKG_ROOT = resolvePackageRoot(import.meta.url)
167
+ const BIZ_DAILY = join(PKG_ROOT, 'output', 'biz-daily')
168
+ const VAULT_WIKI = join(PKG_ROOT, 'output', 'wechat-vault', 'Wiki', 'Concepts')
169
+ const VAULT_INDEX = join(PKG_ROOT, 'output', 'wechat-vault', 'Wiki', '00-Overview.md')
170
+ const REVIEWS = join(PKG_ROOT, 'output', 'reviews', 'Daily')
171
+ const MCP_MESSAGE_LIMIT_DEFAULT = 100
172
+ const MCP_MESSAGE_LIMIT_MAX = 1000
173
+
174
+ function parseMessageDate(value: unknown, label: string): number | undefined {
175
+ if (value === undefined || value === null || value === '') return undefined
176
+ const date = safeDate(value)
177
+ if (!date) throw new Error(`${label} 必须是有效的 YYYY-MM-DD 日期`)
178
+ const [year, month, day] = date.split('-').map(Number)
179
+ const start = new Date(year, month - 1, day)
180
+ if (start.getFullYear() !== year || start.getMonth() !== month - 1 || start.getDate() !== day) {
181
+ throw new Error(`${label} 必须是有效的日历日期`)
141
182
  }
142
- return _tokenCache.token
183
+ return Math.floor(start.getTime() / 1000)
143
184
  }
144
185
 
145
- async function uploadWeChatImage(token: string, filePath: string): Promise<string> {
146
- const buf = readFileSync(filePath)
147
- const boundary = `----WeFlow${Date.now()}`
148
- const filename = filePath.replace(/^.*[\\/]/, '')
149
- const ext = filename.split('.').pop()?.toLowerCase() || 'png'
150
- const mimeType = ext === 'jpg' || ext === 'jpeg' ? 'image/jpeg' : 'image/png'
151
-
152
- const header = [
153
- `--${boundary}`,
154
- `Content-Disposition: form-data; name="media"; filename="${filename}"`,
155
- `Content-Type: ${mimeType}`,
156
- '', '',
157
- ].join('\r\n')
158
- const trailer = `\r\n--${boundary}--\r\n`
159
-
160
- const headerBytes = new TextEncoder().encode(header)
161
- const trailerBytes = new TextEncoder().encode(trailer)
162
- const body = new Uint8Array(headerBytes.length + buf.length + trailerBytes.length)
163
- body.set(headerBytes, 0)
164
- body.set(buf, headerBytes.length)
165
- body.set(trailerBytes, headerBytes.length + buf.length)
166
-
167
- const resp = await fetch(
168
- `https://api.weixin.qq.com/cgi-bin/material/add_material?access_token=${token}&type=image`,
169
- {
170
- method: 'POST',
171
- headers: { 'Content-Type': `multipart/form-data; boundary=${boundary}` },
172
- body,
173
- }
174
- )
175
- const data = await resp.json() as any
176
- if (data.errcode) {
177
- throw new Error(`上传封面图失败: ${data.errmsg} (${data.errcode})`)
178
- }
179
- return data.media_id
186
+ async function resolveMcpTalker(input: unknown): Promise<string> {
187
+ const contact = String(input || '').trim()
188
+ if (!contact) throw new Error('contact 不能为空')
189
+ if (contact.startsWith('wxid_') || contact.includes('@chatroom') || contact.includes('@openim')) return contact
190
+
191
+ const sessions = await chatService.listSessions(contact, 20)
192
+ const exact = sessions.filter(session => session.username === contact || session.displayName === contact)
193
+ if (exact.length === 1) return exact[0].username
194
+ if (exact.length > 1 || sessions.length > 1) throw new Error('contact 匹配到多个会话,请使用会话 ID')
195
+ if (sessions.length === 1) return sessions[0].username
196
+ throw new Error('未找到对应会话,请使用会话 ID 或准确的会话名称')
180
197
  }
181
198
 
182
- async function createWeChatDraft(token: string, articles: Array<{
183
- title: string
184
- author?: string
185
- digest?: string
186
- content: string
187
- thumb_media_id?: string
188
- }>): Promise<string> {
189
- const resp = await fetch(
190
- `https://api.weixin.qq.com/cgi-bin/draft/add?access_token=${token}`,
191
- {
192
- method: 'POST',
193
- headers: { 'Content-Type': 'application/json' },
194
- body: JSON.stringify({ articles }),
195
- }
196
- )
197
- const data = await resp.json() as any
198
- if (data.errcode) {
199
- throw new Error(`创建草稿失败: ${data.errmsg} (${data.errcode})`)
199
+ async function exportMessagesForMcp(args: Record<string, any>): Promise<string> {
200
+ const limit = args.limit === undefined ? MCP_MESSAGE_LIMIT_DEFAULT : Number(args.limit)
201
+ if (!Number.isInteger(limit) || limit < 1 || limit > MCP_MESSAGE_LIMIT_MAX) {
202
+ throw new Error(`limit 必须是 1-${MCP_MESSAGE_LIMIT_MAX} 的整数`)
200
203
  }
201
- return data.media_id
204
+ const from = parseMessageDate(args.from, 'from')
205
+ const toStart = parseMessageDate(args.to, 'to')
206
+ const to = toStart === undefined ? undefined : toStart + 24 * 60 * 60 - 1
207
+ if (from !== undefined && to !== undefined && from > to) throw new Error('from 不能晚于 to')
208
+
209
+ const talker = await resolveMcpTalker(args.contact)
210
+ const messages = await chatService.getMessagesInRange(talker, limit, from, to)
211
+ return JSON.stringify(createWeFlowEnvelope(messages, new Date().toISOString(), {
212
+ requestedFrom: from,
213
+ requestedTo: to,
214
+ requestedLimit: limit,
215
+ }), null, 2)
202
216
  }
203
217
 
204
- const PKG_ROOT = resolve(join(import.meta.dirname || '.', '..'))
205
- const BIZ_DAILY = join(PKG_ROOT, 'output', 'biz-daily')
206
- const VAULT_WIKI = join(PKG_ROOT, 'output', 'wechat-vault', 'Wiki', 'Concepts')
207
- const VAULT_INDEX = join(PKG_ROOT, 'output', 'wechat-vault', 'Wiki', '00-Overview.md')
208
- const REVIEWS = join(PKG_ROOT, 'output', 'reviews', 'Daily')
209
-
210
218
  function parseFrontmatter(text: string): Record<string, any> {
211
219
  if (!text.startsWith('---')) return {}
212
220
  const end = text.indexOf('---', 3)
@@ -268,7 +276,7 @@ function searchArticles(args: Record<string, any>): string {
268
276
  if (args.date) {
269
277
  articles = articles.filter(a => a.dateDir === args.date)
270
278
  }
271
- const limit = Number(args.limit) || 20
279
+ const limit = boundedToolInteger(args.limit, 20, 100)
272
280
  const top = articles.slice(0, limit)
273
281
  if (!top.length) return '未找到匹配文章'
274
282
  return top.map(a =>
@@ -276,66 +284,6 @@ function searchArticles(args: Record<string, any>): string {
276
284
  ).join('\n\n')
277
285
  }
278
286
 
279
- /** 自动生成封面图(纯色主题渐变 PNG,无外部依赖) */
280
- function generateCoverImage(title: string, theme: string): string {
281
- const colors: Record<string, [number, number, number]> = {
282
- warm: [230, 126, 34],
283
- default: [52, 152, 219],
284
- minimal: [51, 51, 51],
285
- green: [39, 174, 96],
286
- }
287
- const [r, g, b] = colors[theme] || colors.default
288
- const w = 900, h = 500
289
-
290
- function crc32(buf: Buffer): number {
291
- let c = 0xffffffff
292
- for (let i = 0; i < buf.length; i++) {
293
- c ^= buf[i]
294
- for (let j = 0; j < 8; j++) c = (c >>> 1) ^ (c & 1 ? 0xedb88320 : 0)
295
- }
296
- return (c ^ 0xffffffff) >>> 0
297
- }
298
- function chunk(type: string, data: Buffer): Buffer {
299
- const head = Buffer.alloc(8)
300
- head.writeUInt32BE(data.length, 0)
301
- head.write(type, 4)
302
- const crcBuf = Buffer.alloc(4)
303
- crcBuf.writeUInt32BE(crc32(Buffer.concat([head.slice(4, 8), data])), 0)
304
- return Buffer.concat([head, data, crcBuf])
305
- }
306
-
307
- const signature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])
308
- const ihdr = Buffer.alloc(13)
309
- ihdr.writeUInt32BE(w, 0); ihdr.writeUInt32BE(h, 4)
310
- ihdr[8] = 8; ihdr[9] = 2
311
-
312
- const raw = Buffer.alloc(h * (1 + w * 3))
313
- for (let y = 0; y < h; y++) {
314
- const offset = y * (1 + w * 3)
315
- raw[offset] = 0
316
- for (let x = 0; x < w; x++) {
317
- const px = offset + 1 + x * 3
318
- const darken = 0.9 + 0.1 * (y / h)
319
- raw[px] = Math.floor(r * darken)
320
- raw[px + 1] = Math.floor(g * darken)
321
- raw[px + 2] = Math.floor(b * darken)
322
- }
323
- }
324
-
325
- const png = Buffer.concat([
326
- signature,
327
- chunk('IHDR', ihdr),
328
- chunk('IDAT', deflateSync(raw)),
329
- chunk('IEND', Buffer.alloc(0)),
330
- ])
331
-
332
- const tmpDir = join(PKG_ROOT, 'output', '.tmp')
333
- if (!existsSync(tmpDir)) mkdirSync(tmpDir, { recursive: true })
334
- const path = join(tmpDir, `cover-${Date.now()}.png`)
335
- writeFileSync(path, png)
336
- return path
337
- }
338
-
339
287
  async function main() {
340
288
  const server = new Server(
341
289
  { name: 'weflow-mcp', version: '1.0.0' },
@@ -395,7 +343,7 @@ async function main() {
395
343
  },
396
344
  {
397
345
  name: 'wechat.get_stats',
398
- description: '知识库统计概览',
346
+ description: '统计概览: 知识库文章/概念页 + 本地微信数据(会话数/收藏总数)',
399
347
  inputSchema: { type: 'object', properties: {} },
400
348
  },
401
349
  {
@@ -410,22 +358,6 @@ async function main() {
410
358
  required: ['content'],
411
359
  },
412
360
  },
413
- {
414
- name: 'wechat.publish_article',
415
- description: '排版并发布 Markdown 文章到微信公众号草稿箱。需要设置环境变量 WECHAT_APPID 和 WECHAT_APPSECRET',
416
- inputSchema: {
417
- type: 'object',
418
- properties: {
419
- title: { type: 'string', description: '文章标题(≤64字符)' },
420
- content: { type: 'string', description: 'Markdown 格式的文章内容' },
421
- author: { type: 'string', description: '作者名(≤8字符),默认 "AI Assistant"' },
422
- theme: { type: 'string', description: '排版主题: default | warm | minimal | green,默认 default' },
423
- cover_image: { type: 'string', description: '封面图本地路径(可选)' },
424
- preview_only: { type: 'boolean', description: '仅排版预览不发布草稿,默认 false' },
425
- },
426
- required: ['title', 'content'],
427
- },
428
- },
429
361
  {
430
362
  name: 'wechat.list_themes',
431
363
  description: '列出微信公众号排版可用的所有主题',
@@ -454,6 +386,22 @@ async function main() {
454
386
  required: ['url'],
455
387
  },
456
388
  },
389
+ {
390
+ name: 'wechat.export_messages',
391
+ description: '以 weflow-message/v1 JSON 返回指定会话的本地消息。只读,不返回数据库路径、密钥或配置。',
392
+ inputSchema: {
393
+ type: 'object',
394
+ properties: {
395
+ contact: { type: 'string', description: '会话 ID,或唯一匹配的会话名称' },
396
+ limit: { type: 'number', description: '返回上限,默认 100,最大 1000' },
397
+ from: { type: 'string', description: '起始日期 YYYY-MM-DD(含当天)' },
398
+ to: { type: 'string', description: '结束日期 YYYY-MM-DD(含当天)' },
399
+ },
400
+ required: ['contact'],
401
+ },
402
+ },
403
+ // ---- 本地微信数据 (与 weflow-cli assistant / 微信 bot 共享工具层) ----
404
+ ...ASSISTANT_TOOLS,
457
405
  ],
458
406
  }))
459
407
 
@@ -467,9 +415,10 @@ async function main() {
467
415
 
468
416
  case 'wechat.get_daily': {
469
417
  const dates = existsSync(BIZ_DAILY) ? readdirSync(BIZ_DAILY).sort().reverse() : []
470
- const date = String(args.date || dates[0] || '')
471
- const readme = join(BIZ_DAILY, date, 'README.md')
472
- if (!existsSync(readme)) return { content: [{ type: 'text', text: `未找到 ${date} 的日报` }] }
418
+ if (args.date && !safeDate(args.date)) return { content: [{ type: 'text', text: '日期无效,请使用 YYYY-MM-DD' }] }
419
+ const date = safeDate(args.date) || dates.find(d => /^\d{4}-\d{2}-\d{2}$/.test(d)) || ''
420
+ const readme = safeChildPath(BIZ_DAILY, join(date, 'README.md'))
421
+ if (!readme || !existsSync(readme)) return { content: [{ type: 'text', text: `未找到 ${date} 的日报` }] }
473
422
  return { content: [{ type: 'text', text: readFileSync(readme, 'utf-8') }] }
474
423
  }
475
424
 
@@ -480,9 +429,12 @@ async function main() {
480
429
 
481
430
  case 'wechat.get_concept': {
482
431
  const name = String(args.name)
432
+ if (!name || name.includes('/') || name.includes('\\') || name.includes('..')) {
433
+ return { content: [{ type: 'text', text: '概念名无效' }] }
434
+ }
483
435
  for (const ext of ['', '.md']) {
484
- const path = join(VAULT_WIKI, name + ext)
485
- if (existsSync(path)) {
436
+ const path = safeChildPath(VAULT_WIKI, name + ext)
437
+ if (path && existsSync(path)) {
486
438
  return { content: [{ type: 'text', text: readFileSync(path, 'utf-8') }] }
487
439
  }
488
440
  }
@@ -490,7 +442,8 @@ async function main() {
490
442
  if (existsSync(VAULT_WIKI)) {
491
443
  for (const f of readdirSync(VAULT_WIKI)) {
492
444
  if (f.includes(name)) {
493
- return { content: [{ type: 'text', text: readFileSync(join(VAULT_WIKI, f), 'utf-8') }] }
445
+ const path = safeChildPath(VAULT_WIKI, f)
446
+ if (path) return { content: [{ type: 'text', text: readFileSync(path, 'utf-8') }] }
494
447
  }
495
448
  }
496
449
  }
@@ -502,7 +455,7 @@ async function main() {
502
455
  const date = String(args.date || '')
503
456
  let path: string
504
457
  if (date) {
505
- path = join(REVIEWS, `Daily-${date}.md`)
458
+ path = safeDate(date) ? join(REVIEWS, `Daily-${date}.md`) : ''
506
459
  } else {
507
460
  path = files.length ? join(REVIEWS, files[0]) : ''
508
461
  }
@@ -540,78 +493,6 @@ async function main() {
540
493
  }
541
494
  }
542
495
 
543
- case 'wechat.publish_article': {
544
- const title = String(args.title || '').slice(0, 64)
545
- const content = String(args.content || '')
546
- const author = String(args.author || 'AI Assistant').slice(0, 8)
547
- const theme = String(args.theme || 'default')
548
- const coverImage = args.cover_image ? String(args.cover_image) : ''
549
- const previewOnly = args.preview_only === true || args.preview_only === 'true'
550
-
551
- if (!title || !content) {
552
- return { content: [{ type: 'text', text: '错误: title 和 content 不能为空' }] }
553
- }
554
-
555
- // 1. 排版
556
- const html = formatWeChatArticle(content, { theme: theme as any })
557
- const wordCount = html.replace(/<[^>]*>/g, '').replace(/\s+/g, '').length
558
-
559
- if (previewOnly) {
560
- return {
561
- content: [{
562
- type: 'text',
563
- text: [
564
- `[仅预览] 标题: ${title}`,
565
- `作者: ${author} | 主题: ${theme} | 字数: ${wordCount}`,
566
- `---`,
567
- html,
568
- ].join('\n')
569
- }]
570
- }
571
- }
572
-
573
- // 2. 发布到草稿箱
574
- try {
575
- const token = await getWeChatToken()
576
-
577
- // 上传封面图(如果提供,否则自动生成)
578
- let thumbMediaId = ''
579
- if (coverImage && existsSync(coverImage)) {
580
- thumbMediaId = await uploadWeChatImage(token, coverImage)
581
- } else {
582
- // 自动生成封面图
583
- const autoCover = generateCoverImage(title, theme)
584
- thumbMediaId = await uploadWeChatImage(token, autoCover)
585
- }
586
-
587
- // 创建草稿
588
- const mediaId = await createWeChatDraft(token, [{
589
- title,
590
- author,
591
- digest: html.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').slice(0, 120),
592
- content: html,
593
- thumb_media_id: thumbMediaId || undefined,
594
- }])
595
-
596
- return {
597
- content: [{
598
- type: 'text',
599
- text: [
600
- `✅ 已发布到公众号草稿箱`,
601
- `media_id: ${mediaId}`,
602
- `标题: ${title}`,
603
- `作者: ${author} | 主题: ${theme} | 字数: ${wordCount}`,
604
- `封面图: ${thumbMediaId ? '已上传' : '未提供(使用默认)'}`,
605
- '',
606
- `请登录 mp.weixin.qq.com 草稿箱查看和正式发布。`,
607
- ].join('\n')
608
- }]
609
- }
610
- } catch (e: any) {
611
- return { content: [{ type: 'text', text: `发布失败: ${e.message}\n\n——以下为排版预览——\n\n${html}` }] }
612
- }
613
- }
614
-
615
496
  case 'wechat.get_stats': {
616
497
  const articles = scanArticles()
617
498
  const dates = [...new Set(articles.map(a => a.dateDir))].sort()
@@ -626,13 +507,21 @@ async function main() {
626
507
  const m = idx.match(/共 (\d+) 个概念/)
627
508
  if (m) stats += `- 概念页: ${m[1]} 个\n`
628
509
  }
510
+ // 追加本地微信数据统计 (会话/收藏; 数据库未配置时返回提示)
511
+ const chatStats = await executeTool('get_stats', {}, MCP_TOOL_CTX)
512
+ stats += `\n## 本地微信数据\n\n${chatStats}\n`
629
513
  return { content: [{ type: 'text', text: stats }] }
630
514
  }
631
515
 
632
516
  case 'wechat.search_public': {
633
517
  const keyword = String(args.keyword || '').trim()
634
518
  if (!keyword) return { content: [{ type: 'text', text: '错误: keyword 不能为空' }] }
635
- const limit = Number(args.limit) || 10
519
+ let limit = 10
520
+ try {
521
+ limit = boundedToolInteger(args.limit, 10, 20)
522
+ } catch {
523
+ return { content: [{ type: 'text', text: '错误: limit 必须是 1-20 的整数' }] }
524
+ }
636
525
 
637
526
  try {
638
527
  // 使用搜狗微信搜索
@@ -643,11 +532,12 @@ async function main() {
643
532
  'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
644
533
  'Referer': 'https://weixin.sogou.com/',
645
534
  'Accept': 'text/html',
646
- 'Accept-Language': 'zh-CN,zh;q=0.9',
535
+ 'Accept-Language': 'zh-CN,zh;q=0.9',
647
536
  },
537
+ signal: AbortSignal.timeout(20_000),
648
538
  }
649
539
  )
650
- const html = await resp.text()
540
+ const html = await readResponseTextLimited(resp, 2 * 1024 * 1024)
651
541
 
652
542
  // 从搜狗结果中提取标题、公众号名、摘要
653
543
  // 搜狗将结果放在 li 标签中,每篇文章包含标题链接 + 摘要
@@ -711,16 +601,16 @@ async function main() {
711
601
  ].join('\n')
712
602
  }]
713
603
  }
714
- } catch (e: any) {
715
- return { content: [{ type: 'text', text: `搜索失败: ${e.message}` }] }
604
+ } catch {
605
+ return { content: [{ type: 'text', text: '搜索失败,请检查网络连接后重试' }] }
716
606
  }
717
607
  }
718
608
 
719
609
  case 'wechat.fetch_article': {
720
610
  const url = String(args.url || '').trim()
721
611
  if (!url) return { content: [{ type: 'text', text: '错误: url 不能为空' }] }
722
- if (!url.includes('mp.weixin.qq.com')) {
723
- return { content: [{ type: 'text', text: '错误: 仅支持微信公众号文章链接 (mp.weixin.qq.com)' }] }
612
+ if (!isAllowedWeChatArticleUrl(url)) {
613
+ return { content: [{ type: 'text', text: '错误: 仅支持 HTTPS 微信公众号文章链接 (mp.weixin.qq.com)' }] }
724
614
  }
725
615
 
726
616
  try {
@@ -737,16 +627,26 @@ async function main() {
737
627
  ].join('\n')
738
628
  }]
739
629
  }
740
- } catch (e: any) {
741
- return { content: [{ type: 'text', text: `抓取失败: ${e.message}\n\n提示: 部分文章需要微信客户端环境才能访问,可尝试在微信中打开后复制链接。` }] }
630
+ } catch {
631
+ return { content: [{ type: 'text', text: '抓取失败,请检查链接和网络连接。部分文章需要微信客户端环境才能访问。' }] }
742
632
  }
743
633
  }
744
634
 
745
- default:
635
+ case 'wechat.export_messages': {
636
+ return { content: [{ type: 'text', text: await exportMessagesForMcp(args) }] }
637
+ }
638
+
639
+ default: {
640
+ // assistant 工具层统一分发 (wechat.list_sessions / wechat.get_messages / ...)
641
+ if (name.startsWith('wechat.')) {
642
+ const text = await executeTool(name.slice('wechat.'.length), args, MCP_TOOL_CTX)
643
+ return { content: [{ type: 'text', text }] }
644
+ }
746
645
  return { content: [{ type: 'text', text: `未知工具: ${name}` }] }
646
+ }
747
647
  }
748
- } catch (e: any) {
749
- return { content: [{ type: 'text', text: `错误: ${e.message}` }] }
648
+ } catch {
649
+ return { content: [{ type: 'text', text: '工具执行失败,请检查输入、本地配置或运行状态' }] }
750
650
  }
751
651
  })
752
652