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
@@ -1,398 +1,398 @@
1
- #!/usr/bin/env python3
2
- """
3
- MCP Bridge — 为 MCP Server 提供微信数据库查询能力。
4
-
5
- 用法(由 MCP Server 通过 child_process 调用):
6
- python scripts/mcp_bridge.py list_sessions [--limit 30]
7
- python scripts/mcp_bridge.py search_messages --keyword "遥感" --talker "张三" --days 30 --limit 20
8
- python scripts/mcp_bridge.py get_todos [--days 30]
9
- python scripts/mcp_bridge.py get_action_suggestions [--date 2026-05-15]
10
- python scripts/mcp_bridge.py get_chat_stats [--period week]
11
-
12
- 输出: JSON 格式
13
- """
14
-
15
- import sys, os, json, hashlib, argparse
16
- from datetime import datetime, timezone, timedelta
17
- from pathlib import Path
18
-
19
- try:
20
- from sqlcipher3 import dbapi2 as sqlcipher
21
- except ImportError:
22
- print(json.dumps({"error": "请安装: pip install sqlcipher3"}))
23
- sys.exit(1)
24
-
25
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
26
- from _utils import load_config, decrypt_lock, parse_frontmatter
27
-
28
- OUTPUT_ROOT = 'output'
29
- TZ = timezone(timedelta(hours=8))
30
-
31
-
32
- def json_output(data):
33
- """Output as JSON."""
34
- print(json.dumps(data, ensure_ascii=False, indent=2))
35
-
36
-
37
- # ====== Database ======
38
-
39
- def open_db(db_path, key_hex, salt_hex):
40
- raw_key = f"x'{key_hex}{salt_hex}'"
41
- conn = sqlcipher.connect(db_path)
42
- c = conn.cursor()
43
- c.execute(f'PRAGMA key = "{raw_key}";')
44
- c.execute("SELECT count(*) FROM sqlite_master")
45
- return conn
46
-
47
-
48
- def get_name_map(contact_db, contact_key, contact_salt):
49
- name_map = {}
50
- if not contact_db or not contact_key or not os.path.exists(contact_db):
51
- return name_map
52
- try:
53
- conn = open_db(contact_db, contact_key, contact_salt)
54
- c = conn.cursor()
55
- c.execute("SELECT username, COALESCE(NULLIF(remark,''), NULLIF(nick_name,''), username) FROM contact")
56
- for r in c.fetchall():
57
- name_map[r[0]] = r[1]
58
- conn.close()
59
- except:
60
- pass
61
- return name_map
62
-
63
-
64
- def get_biz_keys(config):
65
- nt_db = config.get('ntDbPath', '')
66
- msg_dir = os.path.dirname(nt_db.replace('\\', '/'))
67
- wxid_dir = os.path.dirname(os.path.dirname(msg_dir))
68
- contact_db = os.path.join(wxid_dir, 'db_storage', 'contact', 'contact.db')
69
- contact_key_enc = config.get('contactKey', '')
70
- contact_salt = config.get('contactSalt', '')
71
- contact_key = decrypt_lock(contact_key_enc) if contact_key_enc else ''
72
- biz_db = os.path.join(msg_dir, 'biz_message_0.db')
73
- biz_key_enc = config.get('bizKey', '')
74
- biz_salt = config.get('bizSalt', '')
75
- if biz_key_enc and biz_salt:
76
- biz_key = decrypt_lock(biz_key_enc)
77
- else:
78
- return {'error': '缺少 biz_message_0.db 密钥,请运行 python scripts/nt_decrypt.py scan --json'}
79
- return {
80
- 'biz_db': biz_db, 'biz_key': biz_key, 'biz_salt': biz_salt,
81
- 'contact_db': contact_db, 'contact_key': contact_key, 'contact_salt': contact_salt,
82
- }
83
-
84
-
85
- def load_dbs():
86
- """Load all databases and return connections + metadata."""
87
- config = load_config()
88
- nt_db = config.get('ntDbPath', '')
89
- if not nt_db:
90
- return None, None, None, None, {"error": "未初始化,请先运行 weflow-cli init"}
91
-
92
- nt_key = decrypt_lock(config.get('ntKey', ''))
93
- nt_salt = config.get('ntSalt', '')
94
- keys = get_biz_keys(config)
95
- name_map = get_name_map(keys['contact_db'], keys['contact_key'], keys['contact_salt'])
96
-
97
- try:
98
- conn = open_db(nt_db, nt_key, nt_salt)
99
- except Exception as e:
100
- return None, None, None, None, {"error": f"数据库连接失败: {e}"}
101
-
102
- return conn, keys, name_map, nt_db, None
103
-
104
-
105
- # ====== Commands ======
106
-
107
- def cmd_list_sessions(args):
108
- """List recent chat sessions."""
109
- conn, keys, name_map, nt_db, err = load_dbs()
110
- if err:
111
- return err
112
-
113
- c = conn.cursor()
114
- limit = args.limit or 30
115
- now = datetime.now(TZ)
116
- start_ts = int((now - timedelta(days=90)).timestamp())
117
- end_ts = int(now.timestamp())
118
-
119
- try:
120
- c.execute("SELECT user_name FROM Name2Id WHERE is_session = 1")
121
- sessions = [r[0] for r in c.fetchall()]
122
- except:
123
- c.execute("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'Msg_%'")
124
- sessions = [r[0] for r in c.fetchall()]
125
-
126
- results = []
127
- for talker in sessions:
128
- tbl = f"Msg_{hashlib.md5(talker.encode()).hexdigest()}"
129
- try:
130
- c.execute(f'SELECT COUNT(*), MAX(create_time) FROM "{tbl}" WHERE create_time >= ? AND create_time < ?', (start_ts, end_ts))
131
- count, last_ts = c.fetchone()
132
- if count > 0:
133
- name = name_map.get(talker, talker)
134
- last_time = datetime.fromtimestamp(last_ts, tz=TZ).strftime('%m-%d %H:%M')
135
- results.append({
136
- "name": name,
137
- "wxid": talker,
138
- "count": count,
139
- "last_active": last_time,
140
- })
141
- except:
142
- pass
143
-
144
- conn.close()
145
- results.sort(key=lambda x: x['count'], reverse=True)
146
- return {"sessions": results[:limit], "total": len(results)}
147
-
148
-
149
- def cmd_search_messages(args):
150
- """Search messages by keyword."""
151
- conn, keys, name_map, nt_db, err = load_dbs()
152
- if err:
153
- return err
154
-
155
- keyword = args.keyword or ''
156
- talker_query = args.talker or ''
157
- days = args.days or 30
158
- limit = args.limit or 20
159
-
160
- now = datetime.now(TZ)
161
- start_ts = int((now - timedelta(days=days)).timestamp())
162
- end_ts = int(now.timestamp())
163
-
164
- c = conn.cursor()
165
- try:
166
- c.execute("SELECT user_name FROM Name2Id WHERE is_session = 1")
167
- sessions = [r[0] for r in c.fetchall()]
168
- except:
169
- c.execute("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'Msg_%'")
170
- sessions = [r[0] for r in c.fetchall()]
171
-
172
- # Filter by talker if specified
173
- if talker_query:
174
- filtered = []
175
- for s in sessions:
176
- name = name_map.get(s, s)
177
- if talker_query in name or talker_query in s:
178
- filtered.append(s)
179
- sessions = filtered
180
- if not sessions:
181
- conn.close()
182
- return {"results": [], "message": f"未找到联系人: {talker_query}"}
183
-
184
- results = []
185
- for talker in sessions[:50]: # Limit scan scope
186
- tbl = f"Msg_{hashlib.md5(talker.encode()).hexdigest()}"
187
- try:
188
- c.execute(f'SELECT COUNT(*) FROM sqlite_master WHERE name="{tbl}"')
189
- if c.fetchone()[0] == 0:
190
- continue
191
-
192
- c.execute(f'''
193
- SELECT create_time, real_sender_id, message_content
194
- FROM "{tbl}"
195
- WHERE create_time >= ? AND create_time < ?
196
- ORDER BY create_time DESC
197
- ''', (start_ts, end_ts))
198
-
199
- for ts, sender, content in c.fetchall():
200
- if not content or not isinstance(content, str):
201
- continue
202
- if keyword and keyword.lower() not in content.lower():
203
- continue
204
- sender_name = name_map.get(sender, sender) if sender else name_map.get(talker, talker)
205
- dt = datetime.fromtimestamp(ts, tz=TZ)
206
- results.append({
207
- "talker": name_map.get(talker, talker),
208
- "sender": sender_name,
209
- "time": dt.strftime('%Y-%m-%d %H:%M'),
210
- "content": content[:500],
211
- })
212
- if len(results) >= limit:
213
- break
214
- except:
215
- pass
216
- if len(results) >= limit:
217
- break
218
-
219
- conn.close()
220
- results.sort(key=lambda x: x['time'], reverse=True)
221
- return {"results": results, "total": len(results), "keyword": keyword, "days": days}
222
-
223
-
224
- def cmd_get_todos(args):
225
- """Get existing todo files."""
226
- days = args.days or 30
227
- todo_dir = Path(OUTPUT_ROOT)
228
- results = []
229
-
230
- # Find all 待办_*.md files
231
- for f in sorted(todo_dir.glob('待办_*.md'), reverse=True):
232
- content = f.read_text(encoding='utf-8')
233
- # Extract todo items
234
- items = []
235
- for line in content.split('\n'):
236
- line = line.strip()
237
- if line.startswith('- [ ]') or line.startswith('- [x]'):
238
- items.append(line)
239
- if items:
240
- results.append({
241
- "file": f.name,
242
- "items": items,
243
- "count": len(items),
244
- })
245
-
246
- if not results:
247
- return {"todos": [], "message": "暂无待办事项文件,请先运行 extract_todos.py"}
248
-
249
- return {"todos": results}
250
-
251
-
252
- def cmd_get_action_suggestions(args):
253
- """Get action suggestion files."""
254
- date = args.date or ''
255
- daily_dir = Path(OUTPUT_ROOT) / 'biz-daily'
256
-
257
- if not daily_dir.exists():
258
- return {"suggestions": [], "message": "日报目录不存在"}
259
-
260
- if date:
261
- # Specific date
262
- action_file = daily_dir / date / '行动建议.md'
263
- if action_file.exists():
264
- content = action_file.read_text(encoding='utf-8')
265
- return {"date": date, "content": content}
266
- return {"suggestions": [], "message": f"未找到 {date} 的行动建议"}
267
- else:
268
- # Latest
269
- dates = sorted([d.name for d in daily_dir.iterdir() if d.is_dir()], reverse=True)
270
- for d in dates[:7]: # Check last 7 days
271
- action_file = daily_dir / d / '行动建议.md'
272
- if action_file.exists():
273
- content = action_file.read_text(encoding='utf-8')
274
- return {"date": d, "content": content}
275
- return {"suggestions": [], "message": "最近 7 天无行动建议"}
276
-
277
-
278
- def cmd_get_chat_stats(args):
279
- """Get chat statistics."""
280
- conn, keys, name_map, nt_db, err = load_dbs()
281
- if err:
282
- return err
283
-
284
- period = args.period or 'week'
285
- now = datetime.now(TZ)
286
-
287
- if period == 'week':
288
- days_since_monday = now.weekday()
289
- start_date = (now - timedelta(days=days_since_monday)).replace(hour=0, minute=0, second=0, microsecond=0)
290
- else:
291
- start_date = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
292
-
293
- start_ts = int(start_date.timestamp())
294
- end_ts = int(now.timestamp())
295
-
296
- c = conn.cursor()
297
- try:
298
- c.execute("SELECT user_name FROM Name2Id WHERE is_session = 1")
299
- sessions = [r[0] for r in c.fetchall()]
300
- except:
301
- c.execute("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'Msg_%'")
302
- sessions = [r[0] for r in c.fetchall()]
303
-
304
- total_sent = 0
305
- total_recv = 0
306
- talker_counts = {}
307
- hour_dist = [0] * 24
308
-
309
- for talker in sessions:
310
- tbl = f"Msg_{hashlib.md5(talker.encode()).hexdigest()}"
311
- try:
312
- c.execute(f'SELECT COUNT(*) FROM sqlite_master WHERE name="{tbl}"')
313
- if c.fetchone()[0] == 0:
314
- continue
315
- c.execute(f'SELECT create_time FROM "{tbl}" WHERE create_time >= ? AND create_time < ?', (start_ts, end_ts))
316
- count = 0
317
- for (ts,) in c.fetchall():
318
- count += 1
319
- dt = datetime.fromtimestamp(ts, tz=TZ)
320
- hour_dist[dt.hour] += 1
321
- if count > 0:
322
- name = name_map.get(talker, talker)
323
- talker_counts[name] = talker_counts.get(name, 0) + count
324
- total_recv += count # Approximate
325
- except:
326
- pass
327
-
328
- conn.close()
329
-
330
- # Top talkers
331
- top_talkers = sorted(talker_counts.items(), key=lambda x: x[1], reverse=True)[:10]
332
-
333
- # Most active hour
334
- most_active_hour = max(range(24), key=lambda h: hour_dist[h])
335
-
336
- return {
337
- "period": period,
338
- "start_date": start_date.strftime('%Y-%m-%d'),
339
- "end_date": now.strftime('%Y-%m-%d'),
340
- "total_messages": sum(talker_counts.values()),
341
- "active_contacts": len(talker_counts),
342
- "top_talkers": [{"name": n, "count": c} for n, c in top_talkers],
343
- "most_active_hour": f"{most_active_hour:02d}:00",
344
- "hour_distribution": {f"{h:02d}:00": hour_dist[h] for h in range(24)},
345
- }
346
-
347
-
348
- # ====== Main ======
349
-
350
- def main():
351
- sys.stdout.reconfigure(encoding='utf-8', errors='replace')
352
- parser = argparse.ArgumentParser()
353
- subparsers = parser.add_subparsers(dest='command')
354
-
355
- # list_sessions
356
- p = subparsers.add_parser('list_sessions')
357
- p.add_argument('--limit', type=int, default=30)
358
-
359
- # search_messages
360
- p = subparsers.add_parser('search_messages')
361
- p.add_argument('--keyword', default='')
362
- p.add_argument('--talker', default='')
363
- p.add_argument('--days', type=int, default=30)
364
- p.add_argument('--limit', type=int, default=20)
365
-
366
- # get_todos
367
- p = subparsers.add_parser('get_todos')
368
- p.add_argument('--days', type=int, default=30)
369
-
370
- # get_action_suggestions
371
- p = subparsers.add_parser('get_action_suggestions')
372
- p.add_argument('--date', default='')
373
-
374
- # get_chat_stats
375
- p = subparsers.add_parser('get_chat_stats')
376
- p.add_argument('--period', choices=['week', 'month'], default='week')
377
-
378
- args = parser.parse_args()
379
-
380
- handlers = {
381
- 'list_sessions': cmd_list_sessions,
382
- 'search_messages': cmd_search_messages,
383
- 'get_todos': cmd_get_todos,
384
- 'get_action_suggestions': cmd_get_action_suggestions,
385
- 'get_chat_stats': cmd_get_chat_stats,
386
- }
387
-
388
- handler = handlers.get(args.command)
389
- if not handler:
390
- json_output({"error": f"未知命令: {args.command}", "available": list(handlers.keys())})
391
- sys.exit(1)
392
-
393
- result = handler(args)
394
- json_output(result)
395
-
396
-
397
- if __name__ == '__main__':
398
- main()
1
+ #!/usr/bin/env python3
2
+ """
3
+ MCP Bridge — 为 MCP Server 提供微信数据库查询能力。
4
+
5
+ 用法(由 MCP Server 通过 child_process 调用):
6
+ python scripts/mcp_bridge.py list_sessions [--limit 30]
7
+ python scripts/mcp_bridge.py search_messages --keyword "遥感" --talker "张三" --days 30 --limit 20
8
+ python scripts/mcp_bridge.py get_todos [--days 30]
9
+ python scripts/mcp_bridge.py get_action_suggestions [--date 2026-05-15]
10
+ python scripts/mcp_bridge.py get_chat_stats [--period week]
11
+
12
+ 输出: JSON 格式
13
+ """
14
+
15
+ import sys, os, json, hashlib, argparse
16
+ from datetime import datetime, timezone, timedelta
17
+ from pathlib import Path
18
+
19
+ try:
20
+ from sqlcipher3 import dbapi2 as sqlcipher
21
+ except ImportError:
22
+ print(json.dumps({"error": "请安装: pip install sqlcipher3"}))
23
+ sys.exit(1)
24
+
25
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
26
+ from _utils import load_config, decrypt_lock, parse_frontmatter
27
+
28
+ OUTPUT_ROOT = 'output'
29
+ TZ = timezone(timedelta(hours=8))
30
+
31
+
32
+ def json_output(data):
33
+ """Output as JSON."""
34
+ print(json.dumps(data, ensure_ascii=False, indent=2))
35
+
36
+
37
+ # ====== Database ======
38
+
39
+ def open_db(db_path, key_hex, salt_hex):
40
+ raw_key = f"x'{key_hex}{salt_hex}'"
41
+ conn = sqlcipher.connect(db_path)
42
+ c = conn.cursor()
43
+ c.execute(f'PRAGMA key = "{raw_key}";')
44
+ c.execute("SELECT count(*) FROM sqlite_master")
45
+ return conn
46
+
47
+
48
+ def get_name_map(contact_db, contact_key, contact_salt):
49
+ name_map = {}
50
+ if not contact_db or not contact_key or not os.path.exists(contact_db):
51
+ return name_map
52
+ try:
53
+ conn = open_db(contact_db, contact_key, contact_salt)
54
+ c = conn.cursor()
55
+ c.execute("SELECT username, COALESCE(NULLIF(remark,''), NULLIF(nick_name,''), username) FROM contact")
56
+ for r in c.fetchall():
57
+ name_map[r[0]] = r[1]
58
+ conn.close()
59
+ except:
60
+ pass
61
+ return name_map
62
+
63
+
64
+ def get_biz_keys(config):
65
+ nt_db = config.get('ntDbPath', '')
66
+ msg_dir = os.path.dirname(nt_db.replace('\\', '/'))
67
+ wxid_dir = os.path.dirname(os.path.dirname(msg_dir))
68
+ contact_db = os.path.join(wxid_dir, 'db_storage', 'contact', 'contact.db')
69
+ contact_key_enc = config.get('contactKey', '')
70
+ contact_salt = config.get('contactSalt', '')
71
+ contact_key = decrypt_lock(contact_key_enc) if contact_key_enc else ''
72
+ biz_db = os.path.join(msg_dir, 'biz_message_0.db')
73
+ biz_key_enc = config.get('bizKey', '')
74
+ biz_salt = config.get('bizSalt', '')
75
+ if biz_key_enc and biz_salt:
76
+ biz_key = decrypt_lock(biz_key_enc)
77
+ else:
78
+ return {'error': '缺少 biz_message_0.db 密钥,请运行 python scripts/nt_decrypt.py scan --json'}
79
+ return {
80
+ 'biz_db': biz_db, 'biz_key': biz_key, 'biz_salt': biz_salt,
81
+ 'contact_db': contact_db, 'contact_key': contact_key, 'contact_salt': contact_salt,
82
+ }
83
+
84
+
85
+ def load_dbs():
86
+ """Load all databases and return connections + metadata."""
87
+ config = load_config()
88
+ nt_db = config.get('ntDbPath', '')
89
+ if not nt_db:
90
+ return None, None, None, None, {"error": "未初始化,请先运行 weflow-cli init"}
91
+
92
+ nt_key = decrypt_lock(config.get('ntKey', ''))
93
+ nt_salt = config.get('ntSalt', '')
94
+ keys = get_biz_keys(config)
95
+ name_map = get_name_map(keys['contact_db'], keys['contact_key'], keys['contact_salt'])
96
+
97
+ try:
98
+ conn = open_db(nt_db, nt_key, nt_salt)
99
+ except Exception as e:
100
+ return None, None, None, None, {"error": f"数据库连接失败: {e}"}
101
+
102
+ return conn, keys, name_map, nt_db, None
103
+
104
+
105
+ # ====== Commands ======
106
+
107
+ def cmd_list_sessions(args):
108
+ """List recent chat sessions."""
109
+ conn, keys, name_map, nt_db, err = load_dbs()
110
+ if err:
111
+ return err
112
+
113
+ c = conn.cursor()
114
+ limit = args.limit or 30
115
+ now = datetime.now(TZ)
116
+ start_ts = int((now - timedelta(days=90)).timestamp())
117
+ end_ts = int(now.timestamp())
118
+
119
+ try:
120
+ c.execute("SELECT user_name FROM Name2Id WHERE is_session = 1")
121
+ sessions = [r[0] for r in c.fetchall()]
122
+ except:
123
+ c.execute("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'Msg_%'")
124
+ sessions = [r[0] for r in c.fetchall()]
125
+
126
+ results = []
127
+ for talker in sessions:
128
+ tbl = f"Msg_{hashlib.md5(talker.encode()).hexdigest()}"
129
+ try:
130
+ c.execute(f'SELECT COUNT(*), MAX(create_time) FROM "{tbl}" WHERE create_time >= ? AND create_time < ?', (start_ts, end_ts))
131
+ count, last_ts = c.fetchone()
132
+ if count > 0:
133
+ name = name_map.get(talker, talker)
134
+ last_time = datetime.fromtimestamp(last_ts, tz=TZ).strftime('%m-%d %H:%M')
135
+ results.append({
136
+ "name": name,
137
+ "wxid": talker,
138
+ "count": count,
139
+ "last_active": last_time,
140
+ })
141
+ except:
142
+ pass
143
+
144
+ conn.close()
145
+ results.sort(key=lambda x: x['count'], reverse=True)
146
+ return {"sessions": results[:limit], "total": len(results)}
147
+
148
+
149
+ def cmd_search_messages(args):
150
+ """Search messages by keyword."""
151
+ conn, keys, name_map, nt_db, err = load_dbs()
152
+ if err:
153
+ return err
154
+
155
+ keyword = args.keyword or ''
156
+ talker_query = args.talker or ''
157
+ days = args.days or 30
158
+ limit = args.limit or 20
159
+
160
+ now = datetime.now(TZ)
161
+ start_ts = int((now - timedelta(days=days)).timestamp())
162
+ end_ts = int(now.timestamp())
163
+
164
+ c = conn.cursor()
165
+ try:
166
+ c.execute("SELECT user_name FROM Name2Id WHERE is_session = 1")
167
+ sessions = [r[0] for r in c.fetchall()]
168
+ except:
169
+ c.execute("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'Msg_%'")
170
+ sessions = [r[0] for r in c.fetchall()]
171
+
172
+ # Filter by talker if specified
173
+ if talker_query:
174
+ filtered = []
175
+ for s in sessions:
176
+ name = name_map.get(s, s)
177
+ if talker_query in name or talker_query in s:
178
+ filtered.append(s)
179
+ sessions = filtered
180
+ if not sessions:
181
+ conn.close()
182
+ return {"results": [], "message": f"未找到联系人: {talker_query}"}
183
+
184
+ results = []
185
+ for talker in sessions[:50]: # Limit scan scope
186
+ tbl = f"Msg_{hashlib.md5(talker.encode()).hexdigest()}"
187
+ try:
188
+ c.execute(f'SELECT COUNT(*) FROM sqlite_master WHERE name="{tbl}"')
189
+ if c.fetchone()[0] == 0:
190
+ continue
191
+
192
+ c.execute(f'''
193
+ SELECT create_time, real_sender_id, message_content
194
+ FROM "{tbl}"
195
+ WHERE create_time >= ? AND create_time < ?
196
+ ORDER BY create_time DESC
197
+ ''', (start_ts, end_ts))
198
+
199
+ for ts, sender, content in c.fetchall():
200
+ if not content or not isinstance(content, str):
201
+ continue
202
+ if keyword and keyword.lower() not in content.lower():
203
+ continue
204
+ sender_name = name_map.get(sender, sender) if sender else name_map.get(talker, talker)
205
+ dt = datetime.fromtimestamp(ts, tz=TZ)
206
+ results.append({
207
+ "talker": name_map.get(talker, talker),
208
+ "sender": sender_name,
209
+ "time": dt.strftime('%Y-%m-%d %H:%M'),
210
+ "content": content[:500],
211
+ })
212
+ if len(results) >= limit:
213
+ break
214
+ except:
215
+ pass
216
+ if len(results) >= limit:
217
+ break
218
+
219
+ conn.close()
220
+ results.sort(key=lambda x: x['time'], reverse=True)
221
+ return {"results": results, "total": len(results), "keyword": keyword, "days": days}
222
+
223
+
224
+ def cmd_get_todos(args):
225
+ """Get existing todo files."""
226
+ days = args.days or 30
227
+ todo_dir = Path(OUTPUT_ROOT)
228
+ results = []
229
+
230
+ # Find all 待办_*.md files
231
+ for f in sorted(todo_dir.glob('待办_*.md'), reverse=True):
232
+ content = f.read_text(encoding='utf-8')
233
+ # Extract todo items
234
+ items = []
235
+ for line in content.split('\n'):
236
+ line = line.strip()
237
+ if line.startswith('- [ ]') or line.startswith('- [x]'):
238
+ items.append(line)
239
+ if items:
240
+ results.append({
241
+ "file": f.name,
242
+ "items": items,
243
+ "count": len(items),
244
+ })
245
+
246
+ if not results:
247
+ return {"todos": [], "message": "暂无待办事项文件,请先运行 extract_todos.py"}
248
+
249
+ return {"todos": results}
250
+
251
+
252
+ def cmd_get_action_suggestions(args):
253
+ """Get action suggestion files."""
254
+ date = args.date or ''
255
+ daily_dir = Path(OUTPUT_ROOT) / 'biz-daily'
256
+
257
+ if not daily_dir.exists():
258
+ return {"suggestions": [], "message": "日报目录不存在"}
259
+
260
+ if date:
261
+ # Specific date
262
+ action_file = daily_dir / date / '行动建议.md'
263
+ if action_file.exists():
264
+ content = action_file.read_text(encoding='utf-8')
265
+ return {"date": date, "content": content}
266
+ return {"suggestions": [], "message": f"未找到 {date} 的行动建议"}
267
+ else:
268
+ # Latest
269
+ dates = sorted([d.name for d in daily_dir.iterdir() if d.is_dir()], reverse=True)
270
+ for d in dates[:7]: # Check last 7 days
271
+ action_file = daily_dir / d / '行动建议.md'
272
+ if action_file.exists():
273
+ content = action_file.read_text(encoding='utf-8')
274
+ return {"date": d, "content": content}
275
+ return {"suggestions": [], "message": "最近 7 天无行动建议"}
276
+
277
+
278
+ def cmd_get_chat_stats(args):
279
+ """Get chat statistics."""
280
+ conn, keys, name_map, nt_db, err = load_dbs()
281
+ if err:
282
+ return err
283
+
284
+ period = args.period or 'week'
285
+ now = datetime.now(TZ)
286
+
287
+ if period == 'week':
288
+ days_since_monday = now.weekday()
289
+ start_date = (now - timedelta(days=days_since_monday)).replace(hour=0, minute=0, second=0, microsecond=0)
290
+ else:
291
+ start_date = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
292
+
293
+ start_ts = int(start_date.timestamp())
294
+ end_ts = int(now.timestamp())
295
+
296
+ c = conn.cursor()
297
+ try:
298
+ c.execute("SELECT user_name FROM Name2Id WHERE is_session = 1")
299
+ sessions = [r[0] for r in c.fetchall()]
300
+ except:
301
+ c.execute("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'Msg_%'")
302
+ sessions = [r[0] for r in c.fetchall()]
303
+
304
+ total_sent = 0
305
+ total_recv = 0
306
+ talker_counts = {}
307
+ hour_dist = [0] * 24
308
+
309
+ for talker in sessions:
310
+ tbl = f"Msg_{hashlib.md5(talker.encode()).hexdigest()}"
311
+ try:
312
+ c.execute(f'SELECT COUNT(*) FROM sqlite_master WHERE name="{tbl}"')
313
+ if c.fetchone()[0] == 0:
314
+ continue
315
+ c.execute(f'SELECT create_time FROM "{tbl}" WHERE create_time >= ? AND create_time < ?', (start_ts, end_ts))
316
+ count = 0
317
+ for (ts,) in c.fetchall():
318
+ count += 1
319
+ dt = datetime.fromtimestamp(ts, tz=TZ)
320
+ hour_dist[dt.hour] += 1
321
+ if count > 0:
322
+ name = name_map.get(talker, talker)
323
+ talker_counts[name] = talker_counts.get(name, 0) + count
324
+ total_recv += count # Approximate
325
+ except:
326
+ pass
327
+
328
+ conn.close()
329
+
330
+ # Top talkers
331
+ top_talkers = sorted(talker_counts.items(), key=lambda x: x[1], reverse=True)[:10]
332
+
333
+ # Most active hour
334
+ most_active_hour = max(range(24), key=lambda h: hour_dist[h])
335
+
336
+ return {
337
+ "period": period,
338
+ "start_date": start_date.strftime('%Y-%m-%d'),
339
+ "end_date": now.strftime('%Y-%m-%d'),
340
+ "total_messages": sum(talker_counts.values()),
341
+ "active_contacts": len(talker_counts),
342
+ "top_talkers": [{"name": n, "count": c} for n, c in top_talkers],
343
+ "most_active_hour": f"{most_active_hour:02d}:00",
344
+ "hour_distribution": {f"{h:02d}:00": hour_dist[h] for h in range(24)},
345
+ }
346
+
347
+
348
+ # ====== Main ======
349
+
350
+ def main():
351
+ sys.stdout.reconfigure(encoding='utf-8', errors='replace')
352
+ parser = argparse.ArgumentParser()
353
+ subparsers = parser.add_subparsers(dest='command')
354
+
355
+ # list_sessions
356
+ p = subparsers.add_parser('list_sessions')
357
+ p.add_argument('--limit', type=int, default=30)
358
+
359
+ # search_messages
360
+ p = subparsers.add_parser('search_messages')
361
+ p.add_argument('--keyword', default='')
362
+ p.add_argument('--talker', default='')
363
+ p.add_argument('--days', type=int, default=30)
364
+ p.add_argument('--limit', type=int, default=20)
365
+
366
+ # get_todos
367
+ p = subparsers.add_parser('get_todos')
368
+ p.add_argument('--days', type=int, default=30)
369
+
370
+ # get_action_suggestions
371
+ p = subparsers.add_parser('get_action_suggestions')
372
+ p.add_argument('--date', default='')
373
+
374
+ # get_chat_stats
375
+ p = subparsers.add_parser('get_chat_stats')
376
+ p.add_argument('--period', choices=['week', 'month'], default='week')
377
+
378
+ args = parser.parse_args()
379
+
380
+ handlers = {
381
+ 'list_sessions': cmd_list_sessions,
382
+ 'search_messages': cmd_search_messages,
383
+ 'get_todos': cmd_get_todos,
384
+ 'get_action_suggestions': cmd_get_action_suggestions,
385
+ 'get_chat_stats': cmd_get_chat_stats,
386
+ }
387
+
388
+ handler = handlers.get(args.command)
389
+ if not handler:
390
+ json_output({"error": f"未知命令: {args.command}", "available": list(handlers.keys())})
391
+ sys.exit(1)
392
+
393
+ result = handler(args)
394
+ json_output(result)
395
+
396
+
397
+ if __name__ == '__main__':
398
+ main()