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,11 +1,12 @@
1
- import { join, dirname } from 'path'
2
- import { fileURLToPath } from 'url'
1
+ import { join } from 'path'
3
2
  import { existsSync } from 'fs'
4
3
  import { WcdbCore } from '../core/wcdbCore.js'
5
4
  import { SqlcipherCore } from '../core/sqlcipherCore.js'
6
5
  import { NtCore } from '../core/ntCore.js'
7
6
  import { configService } from './configService.js'
8
7
  import type { ChatSession, Message, Contact, DataVersion } from '../types.js'
8
+ import { collectMessagesInRange } from './messageQuery.js'
9
+ import { resolvePackageRoot } from '../utils/packageRoot.js'
9
10
 
10
11
  // 创建单例
11
12
  const wcdbCore = new WcdbCore()
@@ -57,8 +58,10 @@ export class ChatService {
57
58
  const dbPath = configService.get('dbPath')
58
59
  const wxid = configService.get('wxid')
59
60
  const decryptKey = configService.get('decryptKey')
61
+ const ntConfigured = !!(configService.get('ntDbPath') && configService.get('ntKey'))
60
62
 
61
- if (!dbPath || !decryptKey) {
63
+ // NT 配置完整时无需 decryptKey(非 Windows 平台密钥自动提取不可用,仅有 NT 密钥)
64
+ if (!dbPath || (!decryptKey && !ntConfigured)) {
62
65
  return { success: false, error: '4.x 配置不完整,请运行 weflow-cli init' }
63
66
  }
64
67
 
@@ -112,7 +115,7 @@ export class ChatService {
112
115
 
113
116
  // 方案 4: 尝试 WCDB API 连接 (需要 db_storage/session.db 结构,仅 3.x 兼容)
114
117
  // Use package root instead of cwd for global install support
115
- const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..')
118
+ const pkgRoot = resolvePackageRoot(import.meta.url)
116
119
  const resourcesPath = join(pkgRoot, 'resources')
117
120
  wcdbCore.setPaths(resourcesPath, '')
118
121
 
@@ -236,6 +239,22 @@ export class ChatService {
236
239
  return []
237
240
  }
238
241
 
242
+ async getMessagesInRange(
243
+ talker: string,
244
+ limit = 0,
245
+ from?: number,
246
+ to?: number,
247
+ ): Promise<Message[]> {
248
+ if (from === undefined && to === undefined) {
249
+ return this.getMessages(talker, limit)
250
+ }
251
+ return collectMessagesInRange(
252
+ (pageLimit, offset) => this.getMessages(talker, pageLimit, offset),
253
+ limit,
254
+ { from, to },
255
+ )
256
+ }
257
+
239
258
  async listContacts(keyword?: string, limit = 200): Promise<Contact[]> {
240
259
  const conn = await this.connect()
241
260
  if (!conn.success) return []
@@ -291,6 +310,116 @@ export class ChatService {
291
310
  this.activeVersion = null
292
311
  this.useRawSqlite4x = false
293
312
  }
313
+
314
+ // ====== SNS (朋友圈本地缓存查询) ======
315
+ // 4.x 支持两条路径: WCDB API 或 NT (Python sqlcipher3)
316
+
317
+ /** 当前连接是否支持朋友圈查询 */
318
+ isSnsSupported(): boolean {
319
+ if (!this.connected || this.activeVersion !== '4.x') return false
320
+ // 路径 1: WCDB API (传统 4.x)
321
+ if (!this.useRawSqlite4x && !this.ntCore) return true
322
+ // 路径 2: NT + sns config 已配置
323
+ if (this.ntCore) {
324
+ const snsPath = configService.get('snsDbPath')
325
+ const snsKey = configService.get('snsKey')
326
+ const snsSalt = configService.get('snsSalt')
327
+ return !!(snsPath && snsKey && snsSalt)
328
+ }
329
+ return false
330
+ }
331
+
332
+ async getSnsTimeline(opts: {
333
+ limit?: number
334
+ offset?: number
335
+ usernames?: string[]
336
+ keyword?: string
337
+ startTime?: number
338
+ endTime?: number
339
+ } = {}): Promise<{ success: boolean; timeline?: any[]; error?: string }> {
340
+ if (!this.isSnsSupported()) {
341
+ return { success: false, error: '当前数据通道不支持朋友圈查询 (需 4.x WCDB 或 NT+sns 配置)' }
342
+ }
343
+ // NT 路径
344
+ if (this.ntCore) {
345
+ const snsPath = configService.get('snsDbPath')
346
+ const snsKey = configService.get('snsKey')
347
+ const snsSalt = configService.get('snsSalt')
348
+ return this.ntCore.getSnsTimeline(snsPath, snsKey, snsSalt, opts)
349
+ }
350
+ // WCDB 路径
351
+ return wcdbCore.getSnsTimeline(
352
+ opts.limit ?? 20,
353
+ opts.offset ?? 0,
354
+ opts.usernames,
355
+ opts.keyword,
356
+ opts.startTime,
357
+ opts.endTime,
358
+ )
359
+ }
360
+
361
+ async getSnsUsernames(): Promise<{ success: boolean; usernames?: string[]; error?: string }> {
362
+ if (!this.isSnsSupported()) {
363
+ return { success: false, error: '当前数据通道不支持朋友圈查询 (需 4.x WCDB 或 NT+sns 配置)' }
364
+ }
365
+ if (this.ntCore) {
366
+ const snsPath = configService.get('snsDbPath')
367
+ const snsKey = configService.get('snsKey')
368
+ const snsSalt = configService.get('snsSalt')
369
+ return this.ntCore.getSnsUsernames(snsPath, snsKey, snsSalt)
370
+ }
371
+ return wcdbCore.getSnsUsernames()
372
+ }
373
+
374
+ async getSnsExportStats(myWxid?: string): Promise<{ success: boolean; data?: { totalPosts: number; totalFriends: number; myPosts: number | null }; error?: string }> {
375
+ if (!this.isSnsSupported()) {
376
+ return { success: false, error: '当前数据通道不支持朋友圈查询 (需 4.x WCDB 或 NT+sns 配置)' }
377
+ }
378
+ if (this.ntCore) {
379
+ const snsPath = configService.get('snsDbPath')
380
+ const snsKey = configService.get('snsKey')
381
+ const snsSalt = configService.get('snsSalt')
382
+ return this.ntCore.getSnsExportStats(snsPath, snsKey, snsSalt, myWxid)
383
+ }
384
+ return wcdbCore.getSnsExportStats(myWxid)
385
+ }
386
+
387
+ // ====== 收藏 (favorite.db, 仅 NT 路径) ======
388
+
389
+ /** 当前连接是否支持收藏查询 */
390
+ isFavSupported(): boolean {
391
+ if (!this.connected || this.activeVersion !== '4.x' || !this.ntCore) return false
392
+ const favPath = configService.get('favDbPath')
393
+ const favKey = configService.get('favKey')
394
+ const favPass = configService.get('favPassphrase')
395
+ return !!(favPath && (favKey || favPass))
396
+ }
397
+
398
+ /** 解析收藏密钥: 优先 favKey, 其次从 favPassphrase + 库 salt 派生 */
399
+ private resolveFavKey(): string | null {
400
+ const favKey = configService.get('favKey')
401
+ if (favKey) return favKey
402
+ const favPass = configService.get('favPassphrase')
403
+ const favPath = configService.get('favDbPath')
404
+ if (!favPass || !favPath) return null
405
+ const salt = NtCore.readDbSalt(favPath)
406
+ if (!salt) return null
407
+ const derived = NtCore.deriveRawKey(favPass, salt)
408
+ configService.set('favKey', derived)
409
+ return derived
410
+ }
411
+
412
+ async getFavorites(opts: {
413
+ limit?: number; offset?: number; keyword?: string; favType?: number;
414
+ } = {}): Promise<{ success: boolean; favorites?: any[]; total?: number; error?: string }> {
415
+ if (!this.isFavSupported()) {
416
+ return { success: false, error: '当前数据通道不支持收藏查询 (需 4.x NT 连接 + fav 配置)' }
417
+ }
418
+ const favPath = configService.get('favDbPath')
419
+ const favKey = this.resolveFavKey()
420
+ if (!favKey) return { success: false, error: '收藏密钥解析失败 (favKey/favPassphrase 配置无效)' }
421
+ return this.ntCore!.getFavorites(favPath, favKey, opts)
422
+ }
294
423
  }
295
424
 
296
425
  export const chatService = new ChatService()
@@ -1,5 +1,5 @@
1
1
  import { join } from 'path'
2
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs'
2
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from 'fs'
3
3
  import { homedir, hostname, userInfo } from 'os'
4
4
  import crypto from 'crypto'
5
5
  import { expandHomePath } from '../utils/pathUtils.js'
@@ -25,8 +25,16 @@ interface CliConfig {
25
25
  wechatOcAccountId: string
26
26
  wechatOcBaseUrl: string
27
27
  wechatOcSyncBuf: string
28
+ /** 持久化的 context_token: wxid -> token (加密 JSON 字符串) */
29
+ wechatOcContextTokens: string
28
30
  // 白名单
29
31
  whitelist: string[]
32
+ // 黑名单 (绝对禁止收发)
33
+ blacklist: string[]
34
+ /** 白名单带昵称: {wxid, displayName, addedAt}[] */
35
+ whitelistEntries: ListEntry[]
36
+ /** 黑名单带昵称: {wxid, displayName, addedAt, reason?}[] */
37
+ blacklistEntries: ListEntry[]
30
38
  // Vault & Pipeline
31
39
  vaultRepo: string
32
40
  aiEngine: string
@@ -34,19 +42,58 @@ interface CliConfig {
34
42
  wereadApiKey: string
35
43
  // AI 引擎 API Key
36
44
  deepseekApiKey: string
45
+ // 朋友圈 (SNS) 数据库
46
+ snsDbPath: string
47
+ snsKey: string
48
+ snsSalt: string
49
+ // 收藏 (favorite.db)
50
+ favDbPath: string
51
+ favKey: string
52
+ emoticonSeed: string
53
+ favPassphrase: string
54
+ // 第二大脑助手
55
+ /** 隐私模式: open | balanced | strict,默认 strict */
56
+ assistantPrivacy: string
57
+ /** 本地推理引擎模型名 (ollama/lmstudio) */
58
+ localModel: string
59
+ /** 自定义 AI 端点 (OpenAI 兼容中转站); 设置后云端引擎走此地址 */
60
+ aiBaseUrl: string
61
+ /** 自定义端点使用的模型名 */
62
+ aiModel: string
63
+ /** 助手白名单 (@im.wechat ID, 逗号/空格分隔); 空 = 拒绝所有人 */
64
+ assistantWhitelist: string
65
+ /** 官方通道明确提供群 ID 时才使用;空 = 拒绝所有群 */
66
+ assistantGroupWhitelist: string
67
+ /** 群内是否必须明确 @ 机器人;默认开启 */
68
+ assistantGroupRequireMention: string
69
+ dailySources: string
70
+ /** 公众号类别映射,JSON 对象:公众号名称或 gh_ ID -> 类别 */
71
+ dailySourceCategories: string
72
+ dailyAiEnabled: string
73
+ }
74
+
75
+ export interface ListEntry {
76
+ wxid: string
77
+ displayName?: string
78
+ addedAt?: number
79
+ reason?: string
37
80
  }
38
81
 
39
82
  /** 需要加密存储的字段 */
40
83
  const ENCRYPTED_KEYS: ReadonlySet<keyof CliConfig> = new Set([
41
84
  'decryptKey', 'decryptKey3x', 'ntKey', 'contactKey',
42
- 'wechatOcToken', 'wereadApiKey', 'deepseekApiKey',
85
+ 'wechatOcToken', 'wereadApiKey', 'deepseekApiKey', 'snsKey',
86
+ 'favKey', 'favPassphrase',
43
87
  ])
44
88
 
45
89
  const CONFIG_DIR = join(homedir(), '.weflow-cli')
46
90
  const CONFIG_FILE = join(CONFIG_DIR, 'config.json')
47
91
 
48
92
  export class ConfigService {
49
- private config: CliConfig = { dbPath: '', wxid: '', decryptKey: '', decryptKey3x: '', dataVersion: '', dbPath3x: '', ntDbPath: '', ntKey: '', ntSalt: '', contactDbPath: '', contactKey: '', contactSalt: '', wechatOcToken: '', wechatOcAccountId: '', wechatOcBaseUrl: '', wechatOcSyncBuf: '', whitelist: [], vaultRepo: '', aiEngine: 'deepseek', wereadApiKey: '', deepseekApiKey: '' }
93
+ private config: CliConfig = { dbPath: '', wxid: '', decryptKey: '', decryptKey3x: '', dataVersion: '', dbPath3x: '', ntDbPath: '', ntKey: '', ntSalt: '', contactDbPath: '', contactKey: '', contactSalt: '', wechatOcToken: '', wechatOcAccountId: '', wechatOcBaseUrl: '', wechatOcSyncBuf: '', wechatOcContextTokens: '', whitelist: [], blacklist: [], whitelistEntries: [], blacklistEntries: [], vaultRepo: '', aiEngine: 'deepseek', wereadApiKey: '', deepseekApiKey: '', snsDbPath: '', snsKey: '', snsSalt: '', favDbPath: '', favKey: '', favPassphrase: '', emoticonSeed: '', assistantPrivacy: 'strict', localModel: '', aiBaseUrl: '', aiModel: '', assistantWhitelist: '', assistantGroupWhitelist: '', assistantGroupRequireMention: 'true', dailySources: '', dailySourceCategories: '', dailyAiEnabled: 'true' }
94
+
95
+ /** 本进程修改过、待回写的字段 (多进程并发写保护) */
96
+ private dirty = new Set<keyof CliConfig>()
50
97
 
51
98
  constructor() {
52
99
  this.load()
@@ -56,6 +103,15 @@ export class ConfigService {
56
103
  try {
57
104
  if (existsSync(CONFIG_FILE)) {
58
105
  const data = JSON.parse(readFileSync(CONFIG_FILE, 'utf8'))
106
+ const whitelist = Array.isArray(data.whitelist) ? data.whitelist : []
107
+ const blacklist = Array.isArray(data.blacklist) ? data.blacklist : []
108
+ // entries 优先; 缺失时从旧 string[] 迁移
109
+ const whitelistEntries: ListEntry[] = Array.isArray(data.whitelistEntries)
110
+ ? data.whitelistEntries
111
+ : whitelist.map((wxid: string) => ({ wxid }))
112
+ const blacklistEntries: ListEntry[] = Array.isArray(data.blacklistEntries)
113
+ ? data.blacklistEntries
114
+ : blacklist.map((wxid: string) => ({ wxid }))
59
115
  this.config = {
60
116
  dbPath: data.dbPath || '',
61
117
  wxid: data.wxid || '',
@@ -73,11 +129,32 @@ export class ConfigService {
73
129
  wechatOcAccountId: data.wechatOcAccountId || '',
74
130
  wechatOcBaseUrl: data.wechatOcBaseUrl || '',
75
131
  wechatOcSyncBuf: data.wechatOcSyncBuf || '',
76
- whitelist: Array.isArray(data.whitelist) ? data.whitelist : [],
132
+ wechatOcContextTokens: data.wechatOcContextTokens || '',
133
+ whitelist,
134
+ blacklist,
135
+ whitelistEntries,
136
+ blacklistEntries,
77
137
  vaultRepo: data.vaultRepo || '',
78
138
  aiEngine: data.aiEngine || 'deepseek',
79
139
  wereadApiKey: data.wereadApiKey || '',
80
140
  deepseekApiKey: data.deepseekApiKey || '',
141
+ snsDbPath: data.snsDbPath || '',
142
+ snsKey: data.snsKey || '',
143
+ snsSalt: data.snsSalt || '',
144
+ favDbPath: data.favDbPath || '',
145
+ favKey: data.favKey || '',
146
+ favPassphrase: data.favPassphrase || '',
147
+ emoticonSeed: data.emoticonSeed || '',
148
+ assistantPrivacy: data.assistantPrivacy || 'strict',
149
+ localModel: data.localModel || '',
150
+ aiBaseUrl: data.aiBaseUrl || '',
151
+ aiModel: data.aiModel || '',
152
+ assistantWhitelist: data.assistantWhitelist || '',
153
+ assistantGroupWhitelist: data.assistantGroupWhitelist || '',
154
+ assistantGroupRequireMention: data.assistantGroupRequireMention === 'false' ? 'false' : 'true',
155
+ dailySources: data.dailySources || '',
156
+ dailySourceCategories: data.dailySourceCategories || '',
157
+ dailyAiEnabled: data.dailyAiEnabled === 'false' ? 'false' : 'true',
81
158
  }
82
159
  }
83
160
  } catch (e) {
@@ -91,7 +168,24 @@ export class ConfigService {
91
168
  if (!existsSync(CONFIG_DIR)) {
92
169
  mkdirSync(CONFIG_DIR, { recursive: true })
93
170
  }
94
- writeFileSync(CONFIG_FILE, JSON.stringify(this.config, null, 2), 'utf8')
171
+ // 多进程安全 (守护进程与 CLI 并发): 只回写本进程修改过的字段,
172
+ // 其余字段以磁盘最新值保留, 避免旧内存快照覆盖他人写入 (幽灵回写)。
173
+ let merged: Record<string, unknown> = { ...this.config }
174
+ try {
175
+ if (existsSync(CONFIG_FILE) && this.dirty.size > 0) {
176
+ const onDisk: Record<string, unknown> = JSON.parse(readFileSync(CONFIG_FILE, 'utf8'))
177
+ merged = { ...onDisk, ...this.config }
178
+ for (const key of Object.keys(onDisk)) {
179
+ if (this.dirty.has(key as keyof CliConfig)) continue
180
+ merged[key] = onDisk[key]
181
+ }
182
+ }
183
+ } catch { /* 磁盘副本不可读时退回整份写入 */ }
184
+ // 原子写入: 先写临时文件再改名, 防止进程中途被杀导致配置损坏
185
+ const tmpFile = CONFIG_FILE + '.tmp'
186
+ writeFileSync(tmpFile, JSON.stringify(merged, null, 2), 'utf8')
187
+ renameSync(tmpFile, CONFIG_FILE)
188
+ this.dirty.clear()
95
189
  } catch (e) {
96
190
  console.error('保存配置失败:', e)
97
191
  }
@@ -116,6 +210,7 @@ export class ConfigService {
116
210
  } else {
117
211
  this.config[key] = value
118
212
  }
213
+ this.dirty.add(key)
119
214
  this.save()
120
215
  }
121
216
 
@@ -133,6 +228,9 @@ export class ConfigService {
133
228
  contactDbPath: this.config.contactDbPath,
134
229
  contactKey: this.get('contactKey'),
135
230
  contactSalt: this.config.contactSalt,
231
+ snsDbPath: this.config.snsDbPath,
232
+ snsKey: this.get('snsKey'),
233
+ snsSalt: this.config.snsSalt,
136
234
  }
137
235
  }
138
236
 
@@ -142,9 +240,76 @@ export class ConfigService {
142
240
 
143
241
  setWhitelist(list: string[]): void {
144
242
  this.config.whitelist = list
243
+ this.dirty.add('whitelist')
244
+ this.save()
245
+ }
246
+
247
+ getWhitelistEntries(): ListEntry[] {
248
+ return this.config.whitelistEntries || []
249
+ }
250
+
251
+ setWhitelistEntries(entries: ListEntry[]): void {
252
+ this.config.whitelistEntries = entries
253
+ // 同步 string[] 视图, 供旧代码使用
254
+ this.config.whitelist = entries.map(e => e.wxid)
255
+ this.dirty.add('whitelistEntries')
256
+ this.dirty.add('whitelist')
145
257
  this.save()
146
258
  }
147
259
 
260
+ getBlacklist(): string[] {
261
+ return this.config.blacklist || []
262
+ }
263
+
264
+ setBlacklist(list: string[]): void {
265
+ this.config.blacklist = list
266
+ this.dirty.add('blacklist')
267
+ this.save()
268
+ }
269
+
270
+ getBlacklistEntries(): ListEntry[] {
271
+ return this.config.blacklistEntries || []
272
+ }
273
+
274
+ setBlacklistEntries(entries: ListEntry[]): void {
275
+ this.config.blacklistEntries = entries
276
+ this.config.blacklist = entries.map(e => e.wxid)
277
+ this.dirty.add('blacklistEntries')
278
+ this.dirty.add('blacklist')
279
+ this.save()
280
+ }
281
+
282
+ /** 读取持久化的 context_token: wxid -> token */
283
+ getContextTokens(): Record<string, string> {
284
+ const raw = this.config.wechatOcContextTokens || ''
285
+ if (!raw) return {}
286
+ const json = raw.startsWith(LOCK_PREFIX) ? this.lockDecrypt(raw) : raw
287
+ if (!json) return {}
288
+ try {
289
+ const obj = JSON.parse(json)
290
+ return (obj && typeof obj === 'object') ? obj as Record<string, string> : {}
291
+ } catch {
292
+ return {}
293
+ }
294
+ }
295
+
296
+ /** 持久化 context_token 集合 (加密存储) */
297
+ setContextTokens(tokens: Record<string, string>): void {
298
+ const json = JSON.stringify(tokens || {})
299
+ this.config.wechatOcContextTokens = json ? this.lockEncrypt(json) : ''
300
+ this.dirty.add('wechatOcContextTokens')
301
+ this.save()
302
+ }
303
+
304
+ /** 增量更新单个 wxid 的 context_token */
305
+ upsertContextToken(wxid: string, token: string): void {
306
+ if (!wxid || !token) return
307
+ const tokens = this.getContextTokens()
308
+ if (tokens[wxid] === token) return
309
+ tokens[wxid] = token
310
+ this.setContextTokens(tokens)
311
+ }
312
+
148
313
  isConfigured(): boolean {
149
314
  const has4x = !!(this.config.dbPath && this.config.decryptKey)
150
315
  const has3x = !!(this.config.dbPath3x && this.config.decryptKey3x)
@@ -152,8 +317,31 @@ export class ConfigService {
152
317
  return has4x || has3x || hasNt
153
318
  }
154
319
 
320
+ clearDatabaseKeys(): void {
321
+ this.clearDatabaseKeysInMemory(true)
322
+ }
323
+
324
+ maskDatabaseKeysForTesting(): void {
325
+ this.clearDatabaseKeysInMemory(false)
326
+ }
327
+
328
+ private clearDatabaseKeysInMemory(persist: boolean): void {
329
+ const keyFields: Array<keyof CliConfig> = [
330
+ 'decryptKey', 'decryptKey3x', 'ntKey', 'ntSalt',
331
+ 'contactKey', 'contactSalt', 'snsKey', 'snsSalt',
332
+ 'favKey', 'favPassphrase',
333
+ ]
334
+ for (const key of keyFields) {
335
+ this.config[key] = '' as never
336
+ if (persist) this.dirty.add(key)
337
+ }
338
+ if (persist) this.save()
339
+ }
340
+
155
341
  clear(): void {
156
- this.config = { dbPath: '', wxid: '', decryptKey: '', decryptKey3x: '', dataVersion: '', dbPath3x: '', ntDbPath: '', ntKey: '', ntSalt: '', contactDbPath: '', contactKey: '', contactSalt: '', wechatOcToken: '', wechatOcAccountId: '', wechatOcBaseUrl: '', wechatOcSyncBuf: '', whitelist: [], vaultRepo: '', aiEngine: 'deepseek', wereadApiKey: '', deepseekApiKey: '' }
342
+ this.config = { dbPath: '', wxid: '', decryptKey: '', decryptKey3x: '', dataVersion: '', dbPath3x: '', ntDbPath: '', ntKey: '', ntSalt: '', contactDbPath: '', contactKey: '', contactSalt: '', wechatOcToken: '', wechatOcAccountId: '', wechatOcBaseUrl: '', wechatOcSyncBuf: '', wechatOcContextTokens: '', whitelist: [], blacklist: [], whitelistEntries: [], blacklistEntries: [], vaultRepo: '', aiEngine: 'deepseek', wereadApiKey: '', deepseekApiKey: '', snsDbPath: '', snsKey: '', snsSalt: '', favDbPath: '', favKey: '', favPassphrase: '', emoticonSeed: '', assistantPrivacy: 'strict', localModel: '', aiBaseUrl: '', aiModel: '', assistantWhitelist: '', assistantGroupWhitelist: '', assistantGroupRequireMention: 'true', dailySources: '', dailySourceCategories: '', dailyAiEnabled: 'true' }
343
+ // clear 意图是全量重置: 所有字段标记为脏, 覆盖磁盘上的全部旧值
344
+ this.dirty = new Set(Object.keys(this.config) as (keyof CliConfig)[])
157
345
  this.save()
158
346
  }
159
347
 
@@ -0,0 +1,129 @@
1
+ import { createHash } from 'crypto'
2
+ import { mkdirSync, writeFileSync } from 'fs'
3
+ import { join } from 'path'
4
+ import type { Message } from '../types.js'
5
+ import { redactText, type PrivacyMode } from './assistantPrivacy.js'
6
+
7
+ export interface EvidenceManifest {
8
+ schemaVersion: '1.0'
9
+ generatedAt: string
10
+ source: 'local-wechat'
11
+ authorization: 'user-authorized-local-data'
12
+ talker: string
13
+ caseNote: string
14
+ messageCount: number
15
+ firstMessageAt: string | null
16
+ lastMessageAt: string | null
17
+ messageIds: number[]
18
+ messagesSha256: string
19
+ notice: string
20
+ }
21
+
22
+ export interface EvidencePackageResult {
23
+ path: string
24
+ manifest: EvidenceManifest
25
+ }
26
+
27
+ export interface EvidenceReviewInput {
28
+ transcript: string
29
+ redactions: number
30
+ localInference: boolean
31
+ }
32
+
33
+ const LEGAL_NOTICE = '本包用于本地证据整理,不代表任何法院或机构必然采信;请保留原设备和原始数据,并根据案件情况咨询专业律师。'
34
+
35
+ function sha256(value: string): string {
36
+ return createHash('sha256').update(value, 'utf8').digest('hex')
37
+ }
38
+
39
+ function isoTime(timestamp: number | undefined): string | null {
40
+ if (!timestamp || !Number.isFinite(timestamp)) return null
41
+ return new Date(timestamp * 1000).toISOString()
42
+ }
43
+
44
+ function safePackageName(talker: string): string {
45
+ const name = talker.replace(/[^a-zA-Z0-9._-]+/g, '_').replace(/^\.+/, '')
46
+ return (name || 'conversation').slice(0, 80)
47
+ }
48
+
49
+ export function buildEvidenceManifest(talker: string, messages: Message[], caseNote = '', generatedAt = new Date().toISOString()): EvidenceManifest {
50
+ const serialized = JSON.stringify(messages, null, 2)
51
+ const timestamps = messages.map(message => message.createTime).filter(Number.isFinite)
52
+ return {
53
+ schemaVersion: '1.0',
54
+ generatedAt,
55
+ source: 'local-wechat',
56
+ authorization: 'user-authorized-local-data',
57
+ talker,
58
+ caseNote: caseNote.trim().slice(0, 500),
59
+ messageCount: messages.length,
60
+ firstMessageAt: timestamps.length ? isoTime(Math.min(...timestamps)) : null,
61
+ lastMessageAt: timestamps.length ? isoTime(Math.max(...timestamps)) : null,
62
+ messageIds: messages.map(message => message.localId).filter(Number.isFinite),
63
+ messagesSha256: sha256(serialized),
64
+ notice: LEGAL_NOTICE,
65
+ }
66
+ }
67
+
68
+ export function writeEvidencePackage(outputRoot: string, talker: string, messages: Message[], caseNote = ''): EvidencePackageResult {
69
+ const packagePath = join(outputRoot, `${safePackageName(talker)}-${Date.now()}`)
70
+ mkdirSync(packagePath, { recursive: true })
71
+ const messagesText = JSON.stringify(messages, null, 2)
72
+ const manifest = buildEvidenceManifest(talker, messages, caseNote)
73
+ writeFileSync(join(packagePath, 'messages.json'), messagesText, { encoding: 'utf8', flag: 'wx' })
74
+ writeFileSync(join(packagePath, 'manifest.json'), JSON.stringify(manifest, null, 2), { encoding: 'utf8', flag: 'wx' })
75
+ writeFileSync(join(packagePath, 'README.md'), [
76
+ '# 本地聊天证据整理包',
77
+ '',
78
+ `- 消息数量:${manifest.messageCount}`,
79
+ `- 生成时间:${manifest.generatedAt}`,
80
+ `- 原始消息 SHA-256:\`${manifest.messagesSha256}\``,
81
+ '',
82
+ '## 使用边界',
83
+ '',
84
+ LEGAL_NOTICE,
85
+ '',
86
+ '请勿修改 `messages.json`。如需脱敏、摘要或分析,请复制后处理,并保留处理前后的文件哈希和操作记录。',
87
+ '',
88
+ '## 建议补充',
89
+ '',
90
+ '- 保留原设备、原始数据库和官方导出记录。',
91
+ '- 记录导出人、导出时间、工具版本和数据范围。',
92
+ '- 保存完整上下文,不要只截取对自己有利的片段。',
93
+ '- 将本包交给律师或有权处理案件的机构复核。',
94
+ '',
95
+ ].join('\n'), { encoding: 'utf8', flag: 'wx' })
96
+ return { path: packagePath, manifest }
97
+ }
98
+
99
+ export const evidenceLegalNotice = LEGAL_NOTICE
100
+
101
+ export function buildEvidenceReviewInput(talker: string, messages: Message[], mode: PrivacyMode, localInference: boolean): EvidenceReviewInput {
102
+ let redactions = 0
103
+ const safeMetadata = (value: string, fallback: string): string => {
104
+ if (localInference) return value || fallback
105
+ if (mode === 'strict') return fallback
106
+ const result = redactText(value || fallback, mode, false)
107
+ redactions += result.redactions
108
+ return result.safe
109
+ }
110
+ const safeTalker = safeMetadata(talker, '[会话已按严格隐私模式屏蔽]')
111
+ const lines = messages.map(message => {
112
+ const rawBody = message.content || message.parsedContent || message.rawContent || ''
113
+ const body = mode === 'strict' && !localInference
114
+ ? `[聊天正文已按严格隐私模式屏蔽,共${rawBody.length}字]`
115
+ : (() => {
116
+ const result = redactText(rawBody, mode, localInference)
117
+ redactions += result.redactions
118
+ return result.safe
119
+ })()
120
+ const time = isoTime(message.createTime) || '时间未知'
121
+ const sender = safeMetadata(message.senderUsername || '', '[发送方已按严格隐私模式屏蔽]')
122
+ return `[消息ID:${message.localId}] [${time}] [发送方:${sender}] ${body}`
123
+ })
124
+ return {
125
+ transcript: `会话:${safeTalker}\n消息数:${messages.length}\n\n${lines.join('\n')}`,
126
+ redactions,
127
+ localInference,
128
+ }
129
+ }