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
@@ -0,0 +1,549 @@
1
+ /**
2
+ * Agent 工具注册表 (OpenAI function-calling 格式, DeepSeek 兼容)。
3
+ * 所有工具在本机执行; 结果经 PrivacyGate 脱敏后才进入 LLM 上下文。
4
+ */
5
+ import { chatService } from './chatService.js'
6
+ import type { AssistantMemory } from './assistantMemory.js'
7
+ import { privacyGate } from './assistantPrivacy.js'
8
+ import { existsSync, readFileSync, readdirSync } from 'fs'
9
+ import { join } from 'path'
10
+ import { execFile } from 'child_process'
11
+ import { promisify } from 'util'
12
+ import { createPythonProcessEnv, safeSubprocessError } from '../utils/pythonProcessEnv.js'
13
+ import { resolvePackageRoot } from '../utils/packageRoot.js'
14
+
15
+ const execFileAsync = promisify(execFile)
16
+ const PKG_ROOT = resolvePackageRoot(import.meta.url)
17
+ const BIZ_DAILY_DIR = join(PKG_ROOT, 'output', 'biz-daily')
18
+ const VAULT_WIKI_DIR = join(PKG_ROOT, 'output', 'wechat-vault', 'Wiki', 'Concepts')
19
+
20
+ export interface ToolDef {
21
+ type: 'function'
22
+ function: {
23
+ name: string
24
+ description: string
25
+ parameters: Record<string, unknown>
26
+ }
27
+ }
28
+
29
+ export interface ToolContext {
30
+ userId: string
31
+ memory: AssistantMemory
32
+ }
33
+
34
+ function fmtTime(ts: number): string {
35
+ const n = Number(ts)
36
+ const d = n > 1e12 ? new Date(n) : new Date(n * 1000)
37
+ return `${d.getMonth() + 1}/${d.getDate()} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
38
+ }
39
+
40
+ /** SSRF 防护: 仅 http(s), 拒绝内网/环回地址 */
41
+ function isSafeUrl(raw: string): boolean {
42
+ try {
43
+ const u = new URL(raw)
44
+ if (u.protocol !== 'http:' && u.protocol !== 'https:') return false
45
+ const h = u.hostname
46
+ if (h === 'localhost' || h === '[::1]') return false
47
+ if (/^(127\.|10\.|192\.168\.|169\.254\.|0\.)/.test(h)) return false
48
+ if (/^172\.(1[6-9]|2\d|3[01])\./.test(h)) return false
49
+ return true
50
+ } catch { return false }
51
+ }
52
+
53
+ /** 标签剥离 → 纯文本 */
54
+ function stripTags(seg: string): string {
55
+ return seg.replace(/<script[\s\S]*?<\/script>/gi, ' ')
56
+ .replace(/<style[\s\S]*?<\/style>/gi, ' ')
57
+ .replace(/<[^>]+>/g, '\n')
58
+ .replace(/&nbsp;/g, ' ').replace(/&amp;/g, '&').replace(/&lt;/g, '<')
59
+ .replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&#39;/g, "'")
60
+ .split('\n').map(s => s.trim()).filter(Boolean).join('\n')
61
+ }
62
+
63
+ /** WAF 挑战页兜底: 正文藏在 cgiDataNew.content_noencode (JS 转义字符串) */
64
+ function extractFromChallengePage(html: string): string | null {
65
+ const start = html.indexOf("content_noencode: '")
66
+ if (start < 0) return null
67
+ let i = start + "content_noencode: '".length
68
+ let out = ''
69
+ while (i < html.length && out.length < 1_000_000) {
70
+ const ch = html[i]
71
+ if (ch === "'") break
72
+ if (ch === '\\' && i + 1 < html.length) {
73
+ const n = html[i + 1]
74
+ if (n === 'x' && i + 3 < html.length) {
75
+ out += String.fromCharCode(parseInt(html.slice(i + 2, i + 4), 16))
76
+ i += 4
77
+ continue
78
+ }
79
+ if (n === 'n') { out += '\n'; i += 2; continue }
80
+ if (n === 'r') { out += '\r'; i += 2; continue }
81
+ if (n === 't') { out += '\t'; i += 2; continue }
82
+ if (n === '\\' || n === "'" || n === '"') { out += n; i += 2; continue }
83
+ }
84
+ out += ch
85
+ i++
86
+ }
87
+ return out.length > 100 ? out : null
88
+ }
89
+
90
+ /** 从 HTML 提取正文文本 (微信公众号 js_content 优先, 配平 div 边界; 兜底 body) */
91
+ function extractText(html: string): string {
92
+ let seg = html
93
+ const jc = html.indexOf('id="js_content"')
94
+ if (jc >= 0) {
95
+ // 配平 <div/</div> 找 js_content 完整区块; depth 从 1 起算 (自身开标签位于 jc 之前)
96
+ let depth = 1, end = -1
97
+ const tag = /<\/?div\b/g
98
+ tag.lastIndex = jc
99
+ let m: RegExpExecArray | null
100
+ while ((m = tag.exec(html))) {
101
+ if (m[0] === '</div') { depth--; if (depth === 0) { end = m.index; break } }
102
+ else depth++
103
+ }
104
+ seg = end > jc ? html.slice(jc, end) : html.slice(jc, jc + 100_000)
105
+ } else {
106
+ const body = html.indexOf('<body')
107
+ if (body >= 0) seg = html.slice(body)
108
+ }
109
+ return stripTags(seg)
110
+ }
111
+
112
+ /** 确保数据库已连接 (connect 幂等, 已连接时直接返回) */
113
+ async function ensureDb(): Promise<void> {
114
+ await chatService.connect()
115
+ }
116
+
117
+ interface TalkerCandidate {
118
+ username: string
119
+ displayName?: string | null
120
+ }
121
+
122
+ class ToolInputError extends Error {}
123
+
124
+ export function boundedToolInteger(value: unknown, fallback: number, maximum: number, field = 'limit'): number {
125
+ if (value === undefined || value === null || value === '') return fallback
126
+ const parsed = Number(value)
127
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > maximum) {
128
+ throw new ToolInputError(`${field} 必须是 1-${maximum} 的整数`)
129
+ }
130
+ return parsed
131
+ }
132
+
133
+ export function resolveUniqueTalker(name: string, sessions: TalkerCandidate[]): string {
134
+ const query = name.trim()
135
+ if (!query) throw new ToolInputError('contact 不能为空')
136
+
137
+ const unique = (matches: TalkerCandidate[]) => [...new Set(matches.map(item => item.username))]
138
+ const usernameMatches = unique(sessions.filter(item => item.username === query))
139
+ if (usernameMatches.length === 1) return usernameMatches[0]
140
+
141
+ const exactNameMatches = unique(sessions.filter(item => item.displayName === query))
142
+ if (exactNameMatches.length === 1) return exactNameMatches[0]
143
+ if (exactNameMatches.length > 1) {
144
+ throw new ToolInputError(`会话名称“${query}”对应多个会话,请改用会话 ID`)
145
+ }
146
+
147
+ const partialMatches = unique(sessions.filter(item => item.displayName?.includes(query)))
148
+ if (partialMatches.length === 1) return partialMatches[0]
149
+ if (partialMatches.length > 1) {
150
+ throw new ToolInputError(`会话名称“${query}”匹配多个会话,请提供更完整名称或会话 ID`)
151
+ }
152
+ return query
153
+ }
154
+
155
+ /** 显示名 → username 解析 (会话表里两者都有) */
156
+ async function resolveTalker(name: string): Promise<string> {
157
+ const sessions = await chatService.listSessions(undefined, 300)
158
+ return resolveUniqueTalker(name, sessions)
159
+ }
160
+
161
+ export const TOOL_DEFS: ToolDef[] = [
162
+ {
163
+ type: 'function',
164
+ function: {
165
+ name: 'list_sessions',
166
+ description: '列出用户最近的微信聊天会话(显示名+最后一条消息摘要)。用于了解用户最近在和谁聊天。',
167
+ parameters: {
168
+ type: 'object',
169
+ properties: {
170
+ limit: { type: 'number', description: '返回条数, 默认15' },
171
+ },
172
+ },
173
+ },
174
+ },
175
+ {
176
+ type: 'function',
177
+ function: {
178
+ name: 'get_messages',
179
+ description: '读取用户与某位联系人的最近聊天记录。联系人名用会话里出现的显示名。',
180
+ parameters: {
181
+ type: 'object',
182
+ properties: {
183
+ contact: { type: 'string', description: '联系人显示名或备注名' },
184
+ limit: { type: 'number', description: '消息条数, 默认20' },
185
+ },
186
+ required: ['contact'],
187
+ },
188
+ },
189
+ },
190
+ {
191
+ type: 'function',
192
+ function: {
193
+ name: 'search_favorites',
194
+ description: '搜索用户的微信收藏(主要是收藏的公众号文章)。',
195
+ parameters: {
196
+ type: 'object',
197
+ properties: {
198
+ keyword: { type: 'string', description: '搜索关键词' },
199
+ limit: { type: 'number', description: '返回条数, 默认8' },
200
+ },
201
+ required: ['keyword'],
202
+ },
203
+ },
204
+ },
205
+ {
206
+ type: 'function',
207
+ function: {
208
+ name: 'read_favorite',
209
+ description: '读取一篇收藏文章的正文内容。先用 search_favorites 或按关键词找到文章标题, 再用标题关键词调本工具抓取正文。适合「这篇文章讲了啥」「总结第一篇」类问题。',
210
+ parameters: {
211
+ type: 'object',
212
+ properties: {
213
+ keyword: { type: 'string', description: '文章标题中的关键词' },
214
+ max_chars: { type: 'number', description: '返回正文字数上限, 默认3000' },
215
+ },
216
+ required: ['keyword'],
217
+ },
218
+ },
219
+ },
220
+ {
221
+ type: 'function',
222
+ function: {
223
+ name: 'get_daily_report',
224
+ description: '获取公众号日报: 某天推送了哪些文章(标题/来源/分类/AI摘要)。适合「今天/某天公众号推了什么」「最近有哪些AI文章」类问题。',
225
+ parameters: {
226
+ type: 'object',
227
+ properties: {
228
+ date: { type: 'string', description: '日期 YYYY-MM-DD, 默认最新一期' },
229
+ topic: { type: 'string', description: '分类过滤: AI | 学术 | 新闻 | 文学 | 投资' },
230
+ keyword: { type: 'string', description: '标题/摘要关键词过滤' },
231
+ limit: { type: 'number', description: '返回条数, 默认15' },
232
+ },
233
+ },
234
+ },
235
+ },
236
+ {
237
+ type: 'function',
238
+ function: {
239
+ name: 'get_sns',
240
+ description: '查看用户朋友圈: 最新动态时间线或统计概览(发帖数/好友数)。适合「朋友圈最近发了啥」「谁常发朋友圈」类问题。',
241
+ parameters: {
242
+ type: 'object',
243
+ properties: {
244
+ mode: { type: 'string', description: 'timeline (最新动态) 或 stats (统计), 默认 timeline' },
245
+ limit: { type: 'number', description: 'timeline 模式条数, 默认10' },
246
+ },
247
+ },
248
+ },
249
+ },
250
+ {
251
+ type: 'function',
252
+ function: {
253
+ name: 'get_weread',
254
+ description: '查看用户微信读书数据: 书架(shelf)、笔记(notebooks)、搜索书(search)。适合「我在读什么书」「某本书的笔记」类问题。',
255
+ parameters: {
256
+ type: 'object',
257
+ properties: {
258
+ mode: { type: 'string', description: 'shelf | notebooks | search, 默认 shelf' },
259
+ keyword: { type: 'string', description: 'search 模式的书名关键词' },
260
+ },
261
+ },
262
+ },
263
+ },
264
+ {
265
+ type: 'function',
266
+ function: {
267
+ name: 'get_todos',
268
+ description: '查看用户从聊天记录提取的待办任务清单(含优先级和截止时间)。适合「我最近有什么待办」「有什么紧急的事」类问题。',
269
+ parameters: {
270
+ type: 'object',
271
+ properties: {
272
+ status: { type: 'string', description: 'pending | done, 默认 pending' },
273
+ },
274
+ },
275
+ },
276
+ },
277
+ {
278
+ type: 'function',
279
+ function: {
280
+ name: 'search_knowledge',
281
+ description: '搜索用户的本地知识库(从公众号文章沉淀的 Wiki 概念页与学习日报)。适合查概念解释、找之前整理过的知识。',
282
+ parameters: {
283
+ type: 'object',
284
+ properties: {
285
+ keyword: { type: 'string', description: '概念名或关键词, 如 RAG、Agent' },
286
+ },
287
+ required: ['keyword'],
288
+ },
289
+ },
290
+ },
291
+ {
292
+ type: 'function',
293
+ function: {
294
+ name: 'get_stats',
295
+ description: '获取用户本地微信数据统计(会话数/收藏总数等)。',
296
+ parameters: { type: 'object', properties: {} },
297
+ },
298
+ },
299
+ {
300
+ type: 'function',
301
+ function: {
302
+ name: 'search_memory',
303
+ description: '搜索助手关于用户的长期记忆(此前对话中提取的持久事实)。',
304
+ parameters: {
305
+ type: 'object',
306
+ properties: {
307
+ keyword: { type: 'string', description: '搜索关键词' },
308
+ },
309
+ required: ['keyword'],
310
+ },
311
+ },
312
+ },
313
+ {
314
+ type: 'function',
315
+ function: {
316
+ name: 'save_memory',
317
+ description: '把关于用户的重要持久事实存入长期记忆(如偏好、项目背景、重要关系)。',
318
+ parameters: {
319
+ type: 'object',
320
+ properties: {
321
+ content: { type: 'string', description: '事实内容, 40字以内' },
322
+ },
323
+ required: ['content'],
324
+ },
325
+ },
326
+ },
327
+ ]
328
+
329
+ export const MCP_READ_ONLY_TOOL_DEFS = TOOL_DEFS.filter(tool => tool.function.name !== 'save_memory')
330
+
331
+ export async function executeTool(name: string, args: Record<string, any>, ctx: ToolContext): Promise<string> {
332
+ try {
333
+ switch (name) {
334
+ case 'list_sessions': {
335
+ await ensureDb()
336
+ const limit = boundedToolInteger(args.limit, 15, 30)
337
+ const sessions = await chatService.listSessions(undefined, limit)
338
+ if (!sessions.length) return '(未查到会话, 数据库可能未连接)'
339
+ return sessions.map(s =>
340
+ `· ${s.displayName || s.username}: ${(s.summary || '').replace(/\n/g, ' ').slice(0, 30)}`).join('\n')
341
+ }
342
+ case 'get_messages': {
343
+ const contact = String(args.contact || '')
344
+ if (!contact) return '(缺少 contact 参数)'
345
+ const limit = boundedToolInteger(args.limit, 20, 50)
346
+ const talker = await resolveTalker(contact)
347
+ const msgs = await chatService.getMessages(talker, limit)
348
+ if (!msgs.length) return `(没找到「${contact}」的消息)`
349
+ return msgs.map(m => {
350
+ const body = privacyGate.maskMessageBody((m.content || m.parsedContent || '').replace(/\n/g, ' ').slice(0, 80))
351
+ return `[${fmtTime(m.createTime)}] ${m.isSend ? '用户' : (m.senderUsername || '对方')}: ${body}`
352
+ }).join('\n')
353
+ }
354
+ case 'search_favorites': {
355
+ await ensureDb()
356
+ const keyword = String(args.keyword || '')
357
+ if (!keyword) return '(缺少 keyword 参数)'
358
+ const limit = boundedToolInteger(args.limit, 8, 15)
359
+ const r = await chatService.getFavorites({ keyword, limit })
360
+ if (!r.success || !r.favorites?.length) {
361
+ return r.error ? `(查询失败: ${r.error})` : `(收藏中未搜到「${keyword}」)`
362
+ }
363
+ return `共${r.total}条, 前${r.favorites.length}条:\n` +
364
+ r.favorites.map(f => {
365
+ const desc = (f.desc || '').replace(/\s+/g, ' ').slice(0, 60)
366
+ return `· ${f.title || '(无标题)'} (${f.source_name || '未知来源'})${desc ? ` — ${desc}` : ''}`
367
+ }).join('\n')
368
+ }
369
+ case 'read_favorite': {
370
+ await ensureDb()
371
+ const keyword = String(args.keyword || '')
372
+ if (!keyword) return '(缺少 keyword 参数)'
373
+ const maxChars = boundedToolInteger(args.max_chars, 3000, 6000, 'max_chars')
374
+ const r = await chatService.getFavorites({ keyword, limit: 5 })
375
+ if (!r.success || !r.favorites?.length) {
376
+ return `(收藏中未找到「${keyword}」)`
377
+ }
378
+ const art = r.favorites.find(f => f.link) || r.favorites[0]
379
+ if (!art.link) {
380
+ const txt = (art.desc || art.content || '').trim()
381
+ return txt ? `「${art.title || '无标题'}」内容:\n${txt.slice(0, maxChars)}` : `(「${art.title}」没有可读的链接和内容)`
382
+ }
383
+ if (!isSafeUrl(art.link)) return `(链接不安全, 拒绝抓取: ${art.link.slice(0, 60)})`
384
+ // 微信内置浏览器 UA + Referer 绕 WAF (与 biz_daily.py 同策略)
385
+ let html = ''
386
+ let status = 0
387
+ for (let attempt = 0; attempt < 2; attempt++) {
388
+ try {
389
+ const res = await fetch(art.link, {
390
+ headers: {
391
+ 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.38(0x18002633) NetType/WIFI Language/zh_CN',
392
+ 'Referer': 'https://mp.weixin.qq.com/',
393
+ 'Accept': 'text/html,application/xhtml+xml',
394
+ 'Accept-Language': 'zh-CN,zh;q=0.9',
395
+ },
396
+ signal: AbortSignal.timeout(20_000),
397
+ })
398
+ status = res.status
399
+ html = (await res.text()).slice(0, 500_000)
400
+ // 正文验证: 必须含 js_content (否则是验证页, 重试)
401
+ if (res.ok && html.includes('js_content')) break
402
+ } catch { /* 超时/网络错误 → 重试 */ }
403
+ }
404
+ if (!html) return `(抓取失败 HTTP ${status})`
405
+ // 路径1: 正常页面 js_content; 路径2: WAF 挑战页 content_noencode 兜底
406
+ let text = html.includes('js_content') ? extractText(html) : ''
407
+ if (!text || (text.match(/[\u4e00-\u9fff]/g) || []).length < 50) {
408
+ const decoded = extractFromChallengePage(html)
409
+ if (decoded) text = stripTags(decoded)
410
+ }
411
+ text = text.slice(0, maxChars)
412
+ if (html.includes('已被发布者删除')) return `(「${art.title}」已被发布者删除)`
413
+ if (!text.trim() || (text.match(/[\u4e00-\u9fff]/g) || []).length < 20) {
414
+ return `(「${art.title}」正文提取失败, 可能是纯图片文章或已被删除)`
415
+ }
416
+ return `「${art.title || '无标题'}」(${art.source_name || '未知来源'}) 正文:\n${text}`
417
+ }
418
+ case 'get_daily_report': {
419
+ const dates = existsSync(BIZ_DAILY_DIR)
420
+ ? readdirSync(BIZ_DAILY_DIR).filter(d => /^\d{4}-\d{2}-\d{2}$/.test(d)).sort().reverse()
421
+ : []
422
+ if (!dates.length) return '(还没有日报数据, 先运行 biz_daily 生成)'
423
+ const date = String(args.date || dates[0])
424
+ const articlesFile = join(BIZ_DAILY_DIR, date, '.articles.json')
425
+ if (!existsSync(articlesFile)) return `(${date} 没有日报。可用日期: ${dates.slice(0, 5).join(', ')})`
426
+ const raw = JSON.parse(readFileSync(articlesFile, 'utf8'))
427
+ const all = Array.isArray(raw) ? raw : (raw.articles || Object.values(raw))
428
+ let list = all as any[]
429
+ if (args.topic) list = list.filter(a => a.topic === args.topic)
430
+ if (args.keyword) {
431
+ const kw = String(args.keyword).toLowerCase()
432
+ list = list.filter(a =>
433
+ (a.title || '').toLowerCase().includes(kw) || (a.summary || '').toLowerCase().includes(kw))
434
+ }
435
+ const limit = boundedToolInteger(args.limit, 15, 30)
436
+ if (!list.length) return `(${date} 日报共 ${all.length} 篇, 过滤后无匹配)`
437
+ const byTopic: Record<string, number> = {}
438
+ for (const a of all) byTopic[a.topic] = (byTopic[a.topic] || 0) + 1
439
+ const topicStat = Object.entries(byTopic).map(([t, n]) => `${t}${n}篇`).join(' ')
440
+ return `${date} 日报共 ${all.length} 篇 (${topicStat}), 匹配 ${list.length} 篇:\n` +
441
+ list.slice(0, limit).map(a => {
442
+ const summary = (a.summary || '').replace(/\s+/g, ' ').slice(0, 80)
443
+ return `· [${a.topic}] ${a.title} (${a.source})${summary ? `\n ${summary}` : ''}`
444
+ }).join('\n')
445
+ }
446
+ case 'get_sns': {
447
+ await ensureDb()
448
+ if (String(args.mode || 'timeline') === 'stats') {
449
+ const r = await chatService.getSnsExportStats()
450
+ if (!r.success) return `(朋友圈统计失败: ${r.error})`
451
+ const d = r.data!
452
+ return `朋友圈统计: 总动态 ${d.totalPosts} 条, 发布过动态的好友 ${d.totalFriends} 人${d.myPosts != null ? `, 用户自己发过 ${d.myPosts} 条` : ''}`
453
+ }
454
+ const limit = boundedToolInteger(args.limit, 10, 20)
455
+ const r = await chatService.getSnsTimeline({ limit })
456
+ if (!r.success || !r.timeline?.length) return r.error ? `(朋友圈查询失败: ${r.error})` : '(朋友圈暂无缓存数据)'
457
+ return `最新 ${r.timeline.length} 条朋友圈:\n` + r.timeline.slice(0, limit).map((p: any) => {
458
+ const content = (p.content || '').replace(/\s+/g, ' ').slice(0, 100)
459
+ return `· [${fmtTime(p.create_time)}] ${p.nickname || p.username}: ${content}${p.media_count ? ` [${p.media_count}图]` : ''}`
460
+ }).join('\n')
461
+ }
462
+ case 'get_weread': {
463
+ const { wereadService } = await import('./wereadService.js')
464
+ const mode = String(args.mode || 'shelf')
465
+ const apiKey = (await import('./configService.js')).configService.get('wereadApiKey')
466
+ if (!apiKey) return '(微信读书未配置: config set wereadApiKey <key>)'
467
+ try {
468
+ if (mode === 'notebooks') {
469
+ const r = await wereadService.notebooks(20)
470
+ if (!r.ok || !r.data?.books?.length) return `(暂无读书笔记: ${r.error || ''})`
471
+ return `有笔记的书 ${r.data.books.length} 本:\n` +
472
+ r.data.books.slice(0, 15).map((b: any) => `· ${b.title} (${b.author}) — ${b.noteCount || 0} 条笔记`).join('\n')
473
+ }
474
+ if (mode === 'search') {
475
+ const kw = String(args.keyword || '')
476
+ if (!kw) return '(search 模式需要 keyword)'
477
+ const r = await wereadService.search(kw, 8)
478
+ if (!r.ok || !r.data?.books?.length) return `(没搜到「${kw}」: ${r.error || ''})`
479
+ return `搜索「${kw}」结果:\n` +
480
+ r.data.books.map((b: any) => `· ${b.title} (${b.author})`).join('\n')
481
+ }
482
+ const r = await wereadService.shelf()
483
+ if (!r.ok || !r.data?.books?.length) return `(书架为空或读取失败: ${r.error || ''})`
484
+ const books = r.data.books
485
+ const reading = books.filter((b: any) => b.progress && b.progress > 0 && b.progress < 100)
486
+ return `书架共 ${books.length} 本, 在读 ${reading.length} 本:\n` +
487
+ (reading.length ? reading.slice(0, 10).map((b: any) =>
488
+ `· ${b.title} (${b.author}) — 已读 ${b.progress || 0}%`).join('\n') : books.slice(0, 10).map((b: any) => `· ${b.title}`).join('\n'))
489
+ } catch {
490
+ return '(微信读书接口失败,请检查网络和本地配置)'
491
+ }
492
+ }
493
+ case 'get_todos': {
494
+ const status = String(args.status || 'pending')
495
+ try {
496
+ const { getPythonCommand } = await import('../utils/python.js')
497
+ const py = await getPythonCommand()
498
+ const { stdout } = await execFileAsync(py, [join(PKG_ROOT, 'scripts', 'extract_todos.py'), 'list', '--status', status, '--json'], {
499
+ timeout: 30_000,
500
+ env: createPythonProcessEnv(),
501
+ })
502
+ const todos = JSON.parse(stdout.trim())
503
+ if (!Array.isArray(todos) || !todos.length) return `(没有${status === 'done' ? '已完成' : '待办'}任务)`
504
+ return `${status === 'done' ? '已完成' : '待办'} ${todos.length} 项:\n` +
505
+ todos.slice(0, 15).map((t: any) =>
506
+ `· [${t.urgency || '中'}] ${t.task || t.content || t.text || t.title}${t.deadline && t.deadline !== '未提及' ? ` (截止 ${t.deadline})` : ''}`).join('\n')
507
+ } catch (error) {
508
+ return `(${safeSubprocessError(error, '待办查询失败')})`
509
+ }
510
+ }
511
+ case 'search_knowledge': {
512
+ const kw = String(args.keyword || '')
513
+ if (!kw) return '(缺少 keyword 参数)'
514
+ if (!existsSync(VAULT_WIKI_DIR)) return '(知识库尚未生成, 先运行 weflow-cli wiki compile)'
515
+ const files = readdirSync(VAULT_WIKI_DIR).filter(f => f.endsWith('.md'))
516
+ const hits = files.filter(f => f.replace('.md', '').toLowerCase().includes(kw.toLowerCase()))
517
+ if (!hits.length) {
518
+ const contentHits = files.filter(f => readFileSync(join(VAULT_WIKI_DIR, f), 'utf8').toLowerCase().includes(kw.toLowerCase())).slice(0, 5)
519
+ if (!contentHits.length) return `(知识库未收录「${kw}」, 共 ${files.length} 个概念页)`
520
+ return `正文提及「${kw}」的概念页:\n` + contentHits.map(f => `· ${f.replace('.md', '')}`).join('\n')
521
+ }
522
+ const page = readFileSync(join(VAULT_WIKI_DIR, hits[0]), 'utf8')
523
+ return `「${hits[0].replace('.md', '')}」概念页:\n${page.slice(0, 2000)}`
524
+ }
525
+ case 'get_stats': {
526
+ await ensureDb()
527
+ const sessions = await chatService.listSessions(undefined, 1000)
528
+ const fav = await chatService.getFavorites({ limit: 1 })
529
+ return `会话数: ${sessions.length}\n收藏总数: ${fav.success ? fav.total : '未知'}`
530
+ }
531
+ case 'search_memory': {
532
+ const hits = ctx.memory.searchFacts(ctx.userId, String(args.keyword || ''))
533
+ if (!hits.length) return '(长期记忆中无相关内容)'
534
+ return hits.map(f => `· ${f.content}`).join('\n')
535
+ }
536
+ case 'save_memory': {
537
+ const content = String(args.content || '').trim().slice(0, 60)
538
+ if (!content) return '(内容为空, 未保存)'
539
+ const added = ctx.memory.addFact(ctx.userId, content)
540
+ return added ? '(已存入长期记忆)' : '(与已有记忆重复, 未保存)'
541
+ }
542
+ default:
543
+ return `(未知工具: ${name})`
544
+ }
545
+ } catch (error) {
546
+ if (error instanceof ToolInputError) return `(参数错误: ${error.message})`
547
+ return '(工具执行失败,请检查本地配置或运行状态)'
548
+ }
549
+ }