weflow-cli 1.5.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (331) hide show
  1. package/ARCHITECTURE.md +105 -0
  2. package/CHANGELOG.md +110 -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 +333 -149
  8. package/SECURITY.md +47 -0
  9. package/bin/weflow-cli.ts +4032 -583
  10. package/dist/bin/weflow-cli.js +4296 -593
  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 +188 -64
  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/ROADMAP.md +191 -0
  132. package/docs/SETUP.md +103 -0
  133. package/docs/images/weflow-architecture-gpt-image-2.png +0 -0
  134. package/docs/images/weflow-architecture.png +0 -0
  135. package/docs/images/weflow-architecture.svg +65 -0
  136. package/mcp-server/index.ts +169 -269
  137. package/package.json +81 -72
  138. package/requirements-3x.txt +4 -0
  139. package/requirements-voice.txt +15 -0
  140. package/requirements.txt +9 -0
  141. package/resources/js/marked.min.js +69 -0
  142. package/resources/wechat-emoji/666.png +0 -0
  143. package/resources/wechat-emoji/Awesome.png +0 -0
  144. package/resources/wechat-emoji/Concerned.png +0 -0
  145. package/resources/wechat-emoji/Cry.png +0 -0
  146. package/resources/wechat-emoji/Emm.png +0 -0
  147. package/resources/wechat-emoji/Facepalm.png +0 -0
  148. package/resources/wechat-emoji/Grin.png +0 -0
  149. package/resources/wechat-emoji/OK.png +0 -0
  150. package/resources/wechat-emoji/Respect.png +0 -0
  151. package/resources/wechat-emoji/Sick.png +0 -0
  152. package/resources/wechat-emoji/Sleep.png +0 -0
  153. package/resources/wechat-emoji/Smile.png +0 -0
  154. package/resources/wechat-emoji//344/272/262/344/272/262.png +0 -0
  155. package/resources/wechat-emoji//344/276/277/344/276/277.png +0 -0
  156. package/resources/wechat-emoji//345/201/267/347/254/221.png +0 -0
  157. package/resources/wechat-emoji//345/202/262/346/205/242.png +0 -0
  158. package/resources/wechat-emoji//345/206/215/350/247/201.png +0 -0
  159. package/resources/wechat-emoji//345/207/213/350/260/242.png +0 -0
  160. package/resources/wechat-emoji//345/212/240/346/262/271.png +0 -0
  161. package/resources/wechat-emoji//345/213/276/345/274/225.png +0 -0
  162. package/resources/wechat-emoji//345/217/221/345/221/206.png +0 -0
  163. package/resources/wechat-emoji//345/217/221/346/200/222.png +0 -0
  164. package/resources/wechat-emoji//345/217/221/346/212/226.png +0 -0
  165. package/resources/wechat-emoji//345/217/257/346/200/234.png +0 -0
  166. package/resources/wechat-emoji//345/217/263/345/223/274/345/223/274.png +0 -0
  167. package/resources/wechat-emoji//345/217/271/346/260/224.png +0 -0
  168. package/resources/wechat-emoji//345/220/203/347/223/234.png +0 -0
  169. package/resources/wechat-emoji//345/220/210/345/215/201.png +0 -0
  170. package/resources/wechat-emoji//345/220/220.png +0 -0
  171. package/resources/wechat-emoji//345/221/262/347/211/231.png +0 -0
  172. package/resources/wechat-emoji//345/222/222/351/252/202.png +0 -0
  173. package/resources/wechat-emoji//345/222/226/345/225/241.png +0 -0
  174. package/resources/wechat-emoji//345/223/207.png +0 -0
  175. package/resources/wechat-emoji//345/225/244/351/205/222.png +0 -0
  176. package/resources/wechat-emoji//345/230/230.png +0 -0
  177. package/resources/wechat-emoji//345/230/264/345/224/207.png +0 -0
  178. package/resources/wechat-emoji//345/230/277/345/223/210.png +0 -0
  179. package/resources/wechat-emoji//345/233/247.png +0 -0
  180. package/resources/wechat-emoji//345/233/260.png +0 -0
  181. package/resources/wechat-emoji//345/235/217/347/254/221.png +0 -0
  182. package/resources/wechat-emoji//345/244/247/345/223/255.png +0 -0
  183. package/resources/wechat-emoji//345/244/251/345/225/212.png +0 -0
  184. package/resources/wechat-emoji//345/244/252/351/230/263.png +0 -0
  185. package/resources/wechat-emoji//345/244/261/346/234/233.png +0 -0
  186. package/resources/wechat-emoji//345/245/270/347/254/221.png +0 -0
  187. package/resources/wechat-emoji//345/245/275/347/232/204.png +0 -0
  188. package/resources/wechat-emoji//345/247/224/345/261/210.png +0 -0
  189. package/resources/wechat-emoji//345/256/263/347/276/236.png +0 -0
  190. package/resources/wechat-emoji//345/260/264/345/260/254.png +0 -0
  191. package/resources/wechat-emoji//345/272/206/347/245/235.png +0 -0
  192. package/resources/wechat-emoji//345/274/261.png +0 -0
  193. package/resources/wechat-emoji//345/274/272.png +0 -0
  194. package/resources/wechat-emoji//345/276/227/346/204/217.png +0 -0
  195. package/resources/wechat-emoji//345/276/256/347/254/221.png +0 -0
  196. package/resources/wechat-emoji//345/277/203/347/242/216.png +0 -0
  197. package/resources/wechat-emoji//345/277/253/345/223/255/344/272/206.png +0 -0
  198. package/resources/wechat-emoji//346/201/220/346/203/247.png +0 -0
  199. package/resources/wechat-emoji//346/202/240/351/227/262.png +0 -0
  200. package/resources/wechat-emoji//346/203/212/346/201/220.png +0 -0
  201. package/resources/wechat-emoji//346/203/212/350/256/266.png +0 -0
  202. package/resources/wechat-emoji//346/204/211/345/277/253.png +0 -0
  203. package/resources/wechat-emoji//346/206/250/347/254/221.png +0 -0
  204. package/resources/wechat-emoji//346/211/223/350/204/270.png +0 -0
  205. package/resources/wechat-emoji//346/212/223/347/213/202.png +0 -0
  206. package/resources/wechat-emoji//346/212/240/351/274/273.png +0 -0
  207. package/resources/wechat-emoji//346/212/261/346/213/263.png +0 -0
  208. package/resources/wechat-emoji//346/213/245/346/212/261.png +0 -0
  209. package/resources/wechat-emoji//346/213/263/345/244/264.png +0 -0
  210. package/resources/wechat-emoji//346/215/202/350/204/270.png +0 -0
  211. package/resources/wechat-emoji//346/217/241/346/211/213.png +0 -0
  212. package/resources/wechat-emoji//346/222/207/345/230/264.png +0 -0
  213. package/resources/wechat-emoji//346/223/246/346/261/227.png +0 -0
  214. package/resources/wechat-emoji//346/225/262/346/211/223.png +0 -0
  215. package/resources/wechat-emoji//346/227/240/350/257/255.png +0 -0
  216. package/resources/wechat-emoji//346/227/272/346/237/264.png +0 -0
  217. package/resources/wechat-emoji//346/231/225.png +0 -0
  218. package/resources/wechat-emoji//346/234/210/344/272/256.png +0 -0
  219. package/resources/wechat-emoji//346/234/272/346/231/272.png +0 -0
  220. package/resources/wechat-emoji//346/261/227.png +0 -0
  221. package/resources/wechat-emoji//346/265/201/346/263/252.png +0 -0
  222. package/resources/wechat-emoji//347/202/270/345/274/271.png +0 -0
  223. package/resources/wechat-emoji//347/203/237/350/212/261.png +0 -0
  224. package/resources/wechat-emoji//347/210/206/347/253/271.png +0 -0
  225. package/resources/wechat-emoji//347/210/261/345/277/203.png +0 -0
  226. package/resources/wechat-emoji//347/214/252/345/244/264.png +0 -0
  227. package/resources/wechat-emoji//347/216/253/347/221/260.png +0 -0
  228. package/resources/wechat-emoji//347/224/237/347/227/205.png +0 -0
  229. package/resources/wechat-emoji//347/226/221/351/227/256.png +0 -0
  230. package/resources/wechat-emoji//347/231/274.png +0 -0
  231. package/resources/wechat-emoji//347/231/275/347/234/274.png +0 -0
  232. package/resources/wechat-emoji//347/232/261/347/234/211.png +0 -0
  233. package/resources/wechat-emoji//347/235/241.png +0 -0
  234. package/resources/wechat-emoji//347/240/264/346/266/225/344/270/272/347/254/221.png +0 -0
  235. package/resources/wechat-emoji//347/244/274/347/211/251.png +0 -0
  236. package/resources/wechat-emoji//347/244/276/344/274/232/347/244/276/344/274/232.png +0 -0
  237. package/resources/wechat-emoji//347/246/217.png +0 -0
  238. package/resources/wechat-emoji//347/254/221/350/204/270.png +0 -0
  239. package/resources/wechat-emoji//347/272/242/345/214/205.png +0 -0
  240. package/resources/wechat-emoji//347/277/273/347/231/275/347/234/274.png +0 -0
  241. package/resources/wechat-emoji//350/200/266.png +0 -0
  242. package/resources/wechat-emoji//350/203/234/345/210/251.png +0 -0
  243. package/resources/wechat-emoji//350/204/270/347/272/242.png +0 -0
  244. package/resources/wechat-emoji//350/211/262.png +0 -0
  245. package/resources/wechat-emoji//350/213/246/346/266/251.png +0 -0
  246. package/resources/wechat-emoji//350/217/234/345/210/200.png +0 -0
  247. package/resources/wechat-emoji//350/233/213/347/263/225.png +0 -0
  248. package/resources/wechat-emoji//350/241/260.png +0 -0
  249. package/resources/wechat-emoji//350/243/202/345/274/200.png +0 -0
  250. package/resources/wechat-emoji//350/256/251/346/210/221/347/234/213/347/234/213.png +0 -0
  251. package/resources/wechat-emoji//350/260/203/347/232/256.png +0 -0
  252. package/resources/wechat-emoji//350/267/263/350/267/263.png +0 -0
  253. package/resources/wechat-emoji//350/275/254/345/234/210.png +0 -0
  254. package/resources/wechat-emoji//351/204/231/350/247/206.png +0 -0
  255. package/resources/wechat-emoji//351/227/255/345/230/264.png +0 -0
  256. package/resources/wechat-emoji//351/230/264/351/231/251.png +0 -0
  257. package/resources/wechat-emoji//351/232/276/350/277/207.png +0 -0
  258. package/resources/wechat-emoji//351/252/267/351/253/205.png +0 -0
  259. package/resources/wechat-emoji//351/274/223/346/216/214.png +0 -0
  260. package/scripts/_batch_link_sources.py +73 -73
  261. package/scripts/_utils.py +29 -0
  262. package/scripts/annual_report.py +796 -796
  263. package/scripts/auto_tag.py +128 -128
  264. package/scripts/biz_daily.py +210 -53
  265. package/scripts/chat_report.py +10 -0
  266. package/scripts/chat_stats.py +702 -702
  267. package/scripts/classify_daily.py +17 -11
  268. package/scripts/create_reading_notes.py +370 -370
  269. package/scripts/daily_stats.py +132 -0
  270. package/scripts/enrich_backlinks.py +163 -163
  271. package/scripts/export_chat_html.py +1704 -107
  272. package/scripts/extract_todos.py +445 -404
  273. package/scripts/fav_server.py +119 -12
  274. package/scripts/fix_topics.py +111 -111
  275. package/scripts/generate_ai_report.py +1 -1
  276. package/scripts/generate_html.py +597 -277
  277. package/scripts/inspection_records.py +354 -0
  278. package/scripts/mcp_bridge.py +398 -398
  279. package/scripts/nt_decrypt.py +693 -48
  280. package/scripts/pipeline.py +27 -19
  281. package/scripts/promote_all.py +312 -310
  282. package/scripts/promote_ideas.py +299 -297
  283. package/scripts/rag_chat.py +194 -191
  284. package/scripts/semantic_search.py +435 -431
  285. package/scripts/sync_fav.py +201 -146
  286. package/scripts/sync_weread.py +192 -192
  287. package/scripts/vault_rag.py +140 -139
  288. package/scripts/vault_search.py +141 -141
  289. package/scripts/wechat_emoji.py +132 -0
  290. package/scripts/wechat_emoticon.py +433 -0
  291. package/scripts/wechat_image.py +258 -0
  292. package/scripts/wechat_voice.py +327 -0
  293. package/src/core/dbPathService.ts +163 -18
  294. package/src/core/keyService.ts +137 -25
  295. package/src/core/ntCore.ts +202 -61
  296. package/src/core/sqlcipherCore.ts +17 -1
  297. package/src/core/wcdbCore.ts +3 -7
  298. package/src/services/assistantDaemon.ts +111 -0
  299. package/src/services/assistantMemory.ts +193 -0
  300. package/src/services/assistantPrivacy.ts +87 -0
  301. package/src/services/assistantRouting.ts +92 -0
  302. package/src/services/assistantService.ts +295 -0
  303. package/src/services/assistantTools.ts +549 -0
  304. package/src/services/chatService.ts +133 -4
  305. package/src/services/configService.ts +194 -6
  306. package/src/services/evidenceService.ts +129 -0
  307. package/src/services/exportService.ts +113 -48
  308. package/src/services/initKeyService.ts +83 -0
  309. package/src/services/messageContract.ts +77 -0
  310. package/src/services/messageQuery.ts +38 -0
  311. package/src/services/wechat-formatter.ts +348 -348
  312. package/src/services/wechatMessageService.ts +25 -3
  313. package/src/services/wereadService.ts +308 -308
  314. package/src/services/whitelistService.ts +209 -22
  315. package/src/types.ts +20 -0
  316. package/src/utils/dateRange.ts +58 -0
  317. package/src/utils/mcpSecurity.ts +39 -0
  318. package/src/utils/packageRoot.ts +14 -0
  319. package/src/utils/python.ts +73 -0
  320. package/src/utils/pythonProcessEnv.ts +45 -0
  321. package/src/utils/talkerUtils.ts +14 -2
  322. package/dist/src/utils/errors.d.ts +0 -25
  323. package/dist/src/utils/errors.d.ts.map +0 -1
  324. package/dist/src/utils/errors.js +0 -43
  325. package/dist/src/utils/errors.js.map +0 -1
  326. package/dist/src/utils/pythonRunner.d.ts +0 -27
  327. package/dist/src/utils/pythonRunner.d.ts.map +0 -1
  328. package/dist/src/utils/pythonRunner.js +0 -62
  329. package/dist/src/utils/pythonRunner.js.map +0 -1
  330. package/src/utils/errors.ts +0 -42
  331. package/src/utils/pythonRunner.ts +0 -81
@@ -11,11 +11,20 @@ import ctypes
11
11
  from ctypes import wintypes, c_void_p, c_size_t, create_string_buffer, byref, sizeof
12
12
  from pathlib import Path
13
13
 
14
- try:
15
- from sqlcipher3 import dbapi2 as sqlcipher
16
- except ImportError:
17
- print(json.dumps({"error": "需要 sqlcipher3: pip install sqlcipher3"}))
18
- sys.exit(1)
14
+ sqlcipher = None
15
+
16
+
17
+ def require_sqlcipher():
18
+ """Load SQLCipher only for database operations, not path discovery."""
19
+ global sqlcipher
20
+ if sqlcipher is not None:
21
+ return sqlcipher
22
+ try:
23
+ from sqlcipher3 import dbapi2 as sqlcipher_module
24
+ except ImportError as error:
25
+ raise RuntimeError("需要 sqlcipher3: pip install sqlcipher3") from error
26
+ sqlcipher = sqlcipher_module
27
+ return sqlcipher
19
28
 
20
29
  # ========== Memory Scanner ==========
21
30
  PROCESS_VM_READ = 0x0010
@@ -36,17 +45,24 @@ class MEMORY_BASIC_INFORMATION(ctypes.Structure):
36
45
  ('Type', wintypes.DWORD),
37
46
  ]
38
47
 
39
- kernel32 = ctypes.windll.kernel32
40
- ReadProcessMemory = kernel32.ReadProcessMemory
41
- ReadProcessMemory.argtypes = [wintypes.HANDLE, wintypes.LPCVOID, wintypes.LPVOID, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t)]
42
- ReadProcessMemory.restype = wintypes.BOOL
43
- VirtualQueryEx = kernel32.VirtualQueryEx
44
- VirtualQueryEx.argtypes = [wintypes.HANDLE, wintypes.LPCVOID, ctypes.c_void_p, ctypes.c_size_t]
45
- VirtualQueryEx.restype = ctypes.c_size_t
48
+ IS_WINDOWS = os.name == 'nt'
49
+
50
+ if IS_WINDOWS:
51
+ kernel32 = ctypes.windll.kernel32
52
+ ReadProcessMemory = kernel32.ReadProcessMemory
53
+ ReadProcessMemory.argtypes = [wintypes.HANDLE, wintypes.LPCVOID, wintypes.LPVOID, ctypes.c_size_t, ctypes.POINTER(c_size_t)]
54
+ ReadProcessMemory.restype = wintypes.BOOL
55
+ VirtualQueryEx = kernel32.VirtualQueryEx
56
+ VirtualQueryEx.argtypes = [wintypes.HANDLE, wintypes.LPCVOID, ctypes.c_void_p, ctypes.c_size_t]
57
+ VirtualQueryEx.restype = ctypes.c_size_t
58
+ else:
59
+ kernel32 = None
46
60
 
47
61
 
48
62
  def find_weixin_pid():
49
- """Find Weixin.exe process ID."""
63
+ """Find WeChat process ID (Windows: pymem; Linux: /proc scan)."""
64
+ if not IS_WINDOWS:
65
+ return find_weixin_pid_linux()
50
66
  try:
51
67
  import pymem, pymem.process
52
68
  for proc in pymem.process.list_processes():
@@ -63,11 +79,67 @@ def find_weixin_pid():
63
79
  return None
64
80
 
65
81
 
82
+ _LINUX_WECHAT_COMMS = {'wechat', 'wechatappex', 'weixin'}
83
+ _LINUX_EXE_PREFIX_DENY = ('python', 'bash', 'sh', 'zsh', 'node', 'perl', 'ruby', 'electron')
84
+
85
+
86
+ def _is_linux_wechat_process(pid):
87
+ if pid == os.getpid():
88
+ return False
89
+ try:
90
+ with open(f'/proc/{pid}/comm') as f:
91
+ comm = f.read().strip().lower()
92
+ if comm in _LINUX_WECHAT_COMMS:
93
+ return True
94
+ try:
95
+ exe = os.path.realpath(os.readlink(f'/proc/{pid}/exe'))
96
+ except OSError:
97
+ return False
98
+ name = os.path.basename(exe).lower()
99
+ if any(name.startswith(p) for p in _LINUX_EXE_PREFIX_DENY):
100
+ return False
101
+ return 'wechat' in name or 'weixin' in name
102
+ except (PermissionError, FileNotFoundError, ProcessLookupError):
103
+ return False
104
+
105
+
106
+ def find_weixin_pid_linux():
107
+ """Find Linux WeChat main process (largest RSS among candidates)."""
108
+ best = None
109
+ best_rss = -1
110
+ try:
111
+ pids = os.listdir('/proc')
112
+ except OSError:
113
+ return None
114
+ for pid_str in pids:
115
+ if not pid_str.isdigit():
116
+ continue
117
+ pid = int(pid_str)
118
+ if not _is_linux_wechat_process(pid):
119
+ continue
120
+ try:
121
+ with open(f'/proc/{pid}/statm') as f:
122
+ rss_kb = int(f.read().split()[1]) * 4
123
+ except (OSError, IndexError, ValueError):
124
+ rss_kb = 0
125
+ if rss_kb > best_rss:
126
+ best_rss = rss_kb
127
+ best = pid
128
+ return best
129
+
130
+
66
131
  def scan_memory_keys(pid):
67
- """Scan process memory for x'<64hex_key><32hex_salt>' patterns."""
132
+ """Scan process memory for x'<64hex_key><32hex_salt>' patterns.
133
+
134
+ Returns (keys, error): keys is a list of {"key","salt"} dicts,
135
+ error is None on success or 'permission' / 'gone' / 'not_windows'.
136
+ """
137
+ if not IS_WINDOWS:
138
+ return scan_memory_keys_linux(pid)
139
+
68
140
  hProcess = kernel32.OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, False, pid)
69
141
  if not hProcess:
70
- return []
142
+ return [], None
71
143
 
72
144
  pattern = re.compile(rb"x'([0-9a-fA-F]{64})([0-9a-fA-F]{32})'")
73
145
  keys_found = []
@@ -114,17 +186,128 @@ def scan_memory_keys(pid):
114
186
  seen.add(pair)
115
187
  unique_keys.append({"key": k, "salt": s})
116
188
 
117
- return unique_keys
189
+ return unique_keys, None
190
+
191
+
192
+ _LINUX_SKIP_MAPPINGS = {'[vdso]', '[vsyscall]', '[vvar]'}
193
+ _LINUX_SKIP_PREFIXES = ('/usr/lib/', '/lib/', '/usr/share/')
194
+
195
+
196
+ def scan_memory_keys_linux(pid):
197
+ """Scan /proc/<pid>/maps + /proc/<pid>/mem for the key pattern.
198
+
199
+ Requires root or CAP_SYS_PTRACE (or the target being a descendant
200
+ of this process when yama ptrace_scope=1).
201
+ """
202
+ regions = []
203
+ try:
204
+ with open(f'/proc/{pid}/maps') as f:
205
+ for line in f:
206
+ parts = line.split()
207
+ if len(parts) < 2 or 'r' not in parts[1]:
208
+ continue
209
+ if len(parts) >= 6:
210
+ name = parts[5]
211
+ if name in _LINUX_SKIP_MAPPINGS:
212
+ continue
213
+ name_lower = name.lower()
214
+ if name.startswith(_LINUX_SKIP_PREFIXES) and \
215
+ 'wcdb' not in name_lower and 'wechat' not in name_lower and 'weixin' not in name_lower:
216
+ continue
217
+ try:
218
+ start_s, end_s = parts[0].split('-')
219
+ start = int(start_s, 16)
220
+ size = int(end_s, 16) - start
221
+ except ValueError:
222
+ continue
223
+ if 0 < size < 500 * 1024 * 1024:
224
+ regions.append((start, size))
225
+ except PermissionError:
226
+ return [], 'permission'
227
+ except (FileNotFoundError, ProcessLookupError):
228
+ return [], 'gone'
229
+
230
+ pattern = re.compile(rb"x'([0-9a-fA-F]{64})([0-9a-fA-F]{32})'")
231
+ keys_found = []
232
+ try:
233
+ with open(f'/proc/{pid}/mem', 'rb') as mem:
234
+ for base, size in regions:
235
+ try:
236
+ mem.seek(base)
237
+ data = mem.read(size)
238
+ except (OSError, ValueError):
239
+ continue
240
+ for m in pattern.finditer(data):
241
+ keys_found.append((m.group(1).decode(), m.group(2).decode()))
242
+ except PermissionError:
243
+ return [], 'permission'
244
+ except (FileNotFoundError, ProcessLookupError):
245
+ return [], 'gone'
246
+
247
+ seen = set()
248
+ unique_keys = []
249
+ for k, s in keys_found:
250
+ pair = (k, s)
251
+ if pair not in seen:
252
+ seen.add(pair)
253
+ unique_keys.append({"key": k, "salt": s})
254
+
255
+ return unique_keys, None
118
256
 
119
257
 
120
258
  # ========== NT Database Discovery ==========
121
259
 
122
- def find_nt_databases():
260
+ def _is_nt_account_dir(path):
261
+ return os.path.isdir(os.path.join(path, 'db_storage')) or \
262
+ os.path.isdir(os.path.join(path, 'Msg'))
263
+
264
+
265
+ def _normalize_nt_root(root):
266
+ if not root:
267
+ return None
268
+ path = os.path.abspath(os.path.expandvars(os.path.expanduser(root)))
269
+ if os.path.isfile(path):
270
+ path = os.path.dirname(path)
271
+ if not os.path.isdir(path):
272
+ return None
273
+
274
+ try:
275
+ for entry in os.listdir(path):
276
+ candidate = os.path.join(path, entry)
277
+ if os.path.isdir(candidate) and _is_nt_account_dir(candidate):
278
+ return path
279
+ except OSError:
280
+ return None
281
+
282
+ for _ in range(6):
283
+ if _is_nt_account_dir(path):
284
+ return os.path.dirname(path)
285
+ parent = os.path.dirname(path)
286
+ if parent == path:
287
+ break
288
+ path = parent
289
+
290
+ return None
291
+
292
+
293
+ def find_nt_databases(root=None):
123
294
  """Find all NT-format databases under xwechat_files (message + contact)."""
124
- candidates = [
125
- os.path.expandvars(r'%USERPROFILE%\xwechat_files'),
126
- os.path.expandvars(r'%USERPROFILE%\Documents\xwechat_files'),
127
- ]
295
+ if root:
296
+ normalized_root = _normalize_nt_root(root)
297
+ candidates = [normalized_root] if normalized_root else []
298
+ elif IS_WINDOWS:
299
+ candidates = [
300
+ os.path.expandvars(r'%USERPROFILE%\xwechat_files'),
301
+ os.path.expandvars(r'%USERPROFILE%\Documents\xwechat_files'),
302
+ ]
303
+ else:
304
+ home = os.path.expanduser('~')
305
+ candidates = [
306
+ os.path.join(home, '.local', 'share', 'xwechat_files'),
307
+ os.path.join(home, 'xwechat_files'),
308
+ os.path.join(home, 'Documents', 'xwechat_files'),
309
+ os.path.join(home, '文档', 'xwechat_files'),
310
+ ]
128
311
  xwechat = None
129
312
  for c in candidates:
130
313
  if os.path.isdir(c):
@@ -186,6 +369,22 @@ def find_nt_databases():
186
369
  except:
187
370
  pass
188
371
 
372
+ # Scan favorites (收藏) database
373
+ fav_db = os.path.join(xwechat, wxid_dir, 'db_storage', 'favorite', 'favorite.db')
374
+ if os.path.isfile(fav_db):
375
+ try:
376
+ with open(fav_db, 'rb') as fh:
377
+ salt = fh.read(16)
378
+ databases.append({
379
+ "path": fav_db,
380
+ "name": "favorite/favorite.db",
381
+ "salt": salt.hex(),
382
+ "size": os.path.getsize(fav_db),
383
+ "wxid": wxid_dir,
384
+ })
385
+ except:
386
+ pass
387
+
189
388
  return databases
190
389
 
191
390
 
@@ -215,7 +414,7 @@ def load_contact_names(contact_db_path, contact_key_hex, contact_salt_hex):
215
414
 
216
415
  try:
217
416
  raw_key = f"x'{contact_key_hex}{contact_salt_hex}'"
218
- conn = sqlcipher.connect(contact_db_path)
417
+ conn = require_sqlcipher().connect(contact_db_path)
219
418
  c = conn.cursor()
220
419
  c.execute(f'PRAGMA key = "{raw_key}";')
221
420
 
@@ -263,12 +462,138 @@ def match_keys_to_databases(keys, databases):
263
462
  def connect_nt_db(db_path, key_hex, salt_hex):
264
463
  """Connect to an NT database using sqlcipher3."""
265
464
  raw_key = f"x'{key_hex}{salt_hex}'"
266
- conn = sqlcipher.connect(db_path)
465
+ conn = require_sqlcipher().connect(db_path)
267
466
  c = conn.cursor()
268
467
  c.execute(f'PRAGMA key = "{raw_key}";')
269
468
  return conn, c
270
469
 
271
470
 
471
+ def get_fav_schema(db_path, key_hex):
472
+ """Dump favorite.db schema + sample rows (key verification / exploration)."""
473
+ try:
474
+ with open(db_path, 'rb') as fh:
475
+ salt = fh.read(16).hex()
476
+ conn = require_sqlcipher().connect(db_path)
477
+ conn.execute(f'PRAGMA key = "x\'{key_hex}{salt}\'";')
478
+ tables = conn.execute(
479
+ "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
480
+ ).fetchall()
481
+ result = {"success": True, "tables": [t[0] for t in tables]}
482
+ if not tables:
483
+ result["error"] = "数据库为空或密钥错误"
484
+ result["success"] = False
485
+ conn.close()
486
+ return result
487
+ except Exception as e:
488
+ return {"success": False, "error": str(e).split('\n')[0][:200]}
489
+
490
+
491
+ FAV_TYPE_NAMES = {
492
+ 1: 'text', # 文字
493
+ 2: 'image', # 图片
494
+ 4: 'video', # 视频
495
+ 5: 'article', # 公众号文章/网页链接
496
+ 14: 'chatrecord', # 聊天记录
497
+ }
498
+
499
+
500
+ def parse_fav_content(content):
501
+ """Extract display fields (title/link/desc/source/cover) from favitem XML."""
502
+ out = {}
503
+ if not content:
504
+ return out
505
+ import xml.etree.ElementTree as ET
506
+ try:
507
+ root = ET.fromstring(content)
508
+ except ET.ParseError:
509
+ return out
510
+
511
+ def text(path):
512
+ el = root.find(path)
513
+ if el is not None and el.text and el.text.strip():
514
+ return el.text.strip()
515
+ return None
516
+
517
+ title = (text('weburlitem/pagetitle')
518
+ or text('datalist/dataitem/datatitle')
519
+ or text('title')
520
+ or text('desc'))
521
+ if title:
522
+ out['title'] = title
523
+ link = (text('weburlitem/clean_url')
524
+ or text('source/link')
525
+ or text('datalist/dataitem/stream_weburl'))
526
+ if link:
527
+ out['link'] = link
528
+ desc = text('weburlitem/pagedesc') or text('datalist/dataitem/datadesc')
529
+ if desc and desc != out.get('title'):
530
+ out['desc'] = desc
531
+ src = text('weburlitem/appmsgshareitem/srcdisplayname')
532
+ if src:
533
+ out['source_name'] = src
534
+ cover = (text('weburlitem/pagethumb_url')
535
+ or text('datalist/dataitem/dataext')
536
+ or text('datalist/dataitem/cdn_thumburl'))
537
+ if cover:
538
+ out['cover'] = cover
539
+ fmt = text('datalist/dataitem/datafmt')
540
+ if fmt:
541
+ out['format'] = fmt
542
+ return out
543
+
544
+
545
+ def get_favorites(db_path, key_hex, limit=100, offset=0, keyword=None, fav_type=None):
546
+ """List favorite items from favorite.db with parsed content."""
547
+ try:
548
+ with open(db_path, 'rb') as fh:
549
+ salt = fh.read(16).hex()
550
+ conn = require_sqlcipher().connect(db_path)
551
+ conn.execute(f'PRAGMA key = "x\'{key_hex}{salt}\'";')
552
+ c = conn.cursor()
553
+
554
+ tables = [t[0] for t in c.execute(
555
+ "SELECT name FROM sqlite_master WHERE type='table'").fetchall()]
556
+ if 'fav_db_item' not in tables:
557
+ conn.close()
558
+ return {"error": f"未找到 fav_db_item 表,现有表: {tables[:20]}"}
559
+
560
+ total = c.execute('SELECT count(*) FROM fav_db_item').fetchone()[0]
561
+
562
+ sql = ('SELECT local_id, server_id, type, update_time, fromusr, realchatname, content '
563
+ 'FROM fav_db_item WHERE 1=1')
564
+ params = []
565
+ if fav_type is not None:
566
+ sql += ' AND type = ?'
567
+ params.append(fav_type)
568
+ if keyword:
569
+ sql += ' AND content LIKE ?'
570
+ params.append(f'%{keyword}%')
571
+ sql += ' ORDER BY update_time DESC LIMIT ? OFFSET ?'
572
+ params.extend([limit, offset])
573
+ rows = c.execute(sql, params).fetchall()
574
+ conn.close()
575
+
576
+ items = []
577
+ for local_id, server_id, ftype, update_time, fromusr, realchatname, content in rows:
578
+ ct = content if isinstance(content, str) else (
579
+ content.decode('utf-8', errors='replace') if content else '')
580
+ item = {
581
+ 'local_id': local_id,
582
+ 'server_id': server_id,
583
+ 'type': ftype,
584
+ 'type_name': FAV_TYPE_NAMES.get(ftype, 'type_%s' % ftype),
585
+ 'update_time': update_time,
586
+ 'from_user': fromusr,
587
+ 'chat_name': realchatname or None,
588
+ }
589
+ item.update(parse_fav_content(ct))
590
+ items.append(item)
591
+ return {"favorites": items, "total": total, "count": len(items),
592
+ "limit": limit, "offset": offset}
593
+ except Exception as e:
594
+ return {"error": str(e).split('\n')[0][:200]}
595
+
596
+
272
597
  def get_sessions(conn):
273
598
  """Get chat sessions from NT database (Name2Id table)."""
274
599
  c = conn.cursor()
@@ -439,6 +764,223 @@ def get_contacts(conn, limit=200):
439
764
  return {"error": str(e)}
440
765
 
441
766
 
767
+ # ========== SNS (朋友圈) Queries ==========
768
+
769
+ def parse_sns_content(content_str):
770
+ """Parse SNS content XML/Protobuf text to extract title, description, media etc."""
771
+ result = {
772
+ 'content': '',
773
+ 'create_time': 0,
774
+ 'username': '',
775
+ 'object_id': '',
776
+ 'media_count': 0,
777
+ }
778
+ if not content_str:
779
+ return result
780
+
781
+ import re
782
+
783
+ # Extract createTime
784
+ m = re.search(r'<createTime>(\d+)</createTime>', content_str)
785
+ if m:
786
+ result['create_time'] = int(m.group(1))
787
+
788
+ # Extract username
789
+ m = re.search(r'<username>([^<]+)</username>', content_str)
790
+ if m:
791
+ result['username'] = m.group(1)
792
+
793
+ # Extract id
794
+ m = re.search(r'<id>(\d+)</id>', content_str)
795
+ if m:
796
+ result['object_id'] = m.group(1)
797
+
798
+ # Extract contentDesc (main text)
799
+ m = re.search(r'<contentDesc>([^<]*)</contentDesc>', content_str)
800
+ if m:
801
+ result['content'] = m.group(1)
802
+
803
+ # Extract contentDesc CDATA
804
+ m = re.search(r'<contentDesc>\s*<!\[CDATA\[(.*?)\]\]>\s*</contentDesc>', content_str, re.DOTALL)
805
+ if m:
806
+ result['content'] = m.group(1).strip()
807
+
808
+ # Extract title if present
809
+ m = re.search(r'<title>([^<]*)</title>', content_str)
810
+ if m:
811
+ title = m.group(1)
812
+ if title and not result['content']:
813
+ result['content'] = title
814
+ elif title:
815
+ result['content'] = title + '\n' + result['content']
816
+
817
+ # Count media (ContentObject tags)
818
+ result['media_count'] = len(re.findall(r'<ContentObject[ >]', content_str))
819
+
820
+ return result
821
+
822
+
823
+ def get_sns_timeline(cursor, limit=20, offset=0, usernames=None, keyword=None,
824
+ start_time=None, end_time=None):
825
+ """Query SNS timeline posts from SnsTimeLine table."""
826
+ # Check if SnsTimeLine exists, fall back to SnsTopItem_1
827
+ tables = [r[0] for r in cursor.execute(
828
+ "SELECT name FROM sqlite_master WHERE type='table';").fetchall()]
829
+
830
+ table = 'SnsTimeLine' if 'SnsTimeLine' in tables else None
831
+ if not table:
832
+ table = 'SnsTopItem_1' if 'SnsTopItem_1' in tables else None
833
+ if not table:
834
+ return {'success': False, 'error': 'No SNS table found'}
835
+
836
+ conditions = []
837
+ params = []
838
+
839
+ if table == 'SnsTimeLine':
840
+ if usernames:
841
+ placeholders = ','.join(['?' for _ in usernames])
842
+ conditions.append(f'user_name IN ({placeholders})')
843
+ params.extend(usernames)
844
+ if keyword:
845
+ conditions.append('content LIKE ?')
846
+ params.append(f'%{keyword}%')
847
+ if start_time:
848
+ conditions.append("CAST(substr(content, instr(content, '<createTime>') + 12, 10) AS INTEGER) >= ?")
849
+ params.append(start_time)
850
+ if end_time:
851
+ conditions.append("CAST(substr(content, instr(content, '<createTime>') + 12, 10) AS INTEGER) <= ?")
852
+ params.append(end_time)
853
+ elif table == 'SnsTopItem_1':
854
+ uname_col = 'username' if 'username' in [r[1] for r in cursor.execute(f'PRAGMA table_info([{table}]);').fetchall()] else 'user_name'
855
+ if usernames:
856
+ placeholders = ','.join(['?' for _ in usernames])
857
+ conditions.append(f'{uname_col} IN ({placeholders})')
858
+ params.extend(usernames)
859
+ if keyword:
860
+ conditions.append('summary LIKE ?')
861
+ params.append(f'%{keyword}%')
862
+ if start_time:
863
+ conditions.append('create_time >= ?')
864
+ params.append(start_time)
865
+ if end_time:
866
+ conditions.append('create_time <= ?')
867
+ params.append(end_time)
868
+
869
+ where = ' AND '.join(conditions) if conditions else '1=1'
870
+ order = 'tid DESC' if table == 'SnsTimeLine' else 'create_time DESC'
871
+
872
+ try:
873
+ cols = [r[1] for r in cursor.execute(f'PRAGMA table_info([{table}]);').fetchall()]
874
+ # Only select known text columns to avoid binary decode errors
875
+ text_cols = [c for c in cols if c in ('tid', 'user_name', 'content', 'username', 'summary',
876
+ 'create_time', 'last_read_time', 'is_read',
877
+ 'from_username', 'from_nickname', 'to_username',
878
+ 'to_nickname', 'comment_id', 'feed_id',
879
+ 'createTime', 'userName')]
880
+ if not text_cols:
881
+ text_cols = ['*']
882
+ select_str = ', '.join(text_cols)
883
+ rows = cursor.execute(
884
+ f'SELECT {select_str} FROM [{table}] WHERE {where} ORDER BY {order} LIMIT ? OFFSET ?',
885
+ params + [limit, offset]
886
+ ).fetchall()
887
+
888
+ timeline = []
889
+
890
+ for row in rows:
891
+ item = dict(zip(text_cols, row))
892
+
893
+ if table == 'SnsTimeLine':
894
+ # Parse XML content
895
+ content_str = item.get('content', '') or ''
896
+ parsed = parse_sns_content(content_str)
897
+ create_time = parsed['create_time'] or 0
898
+ username = parsed['username'] or item.get('user_name', '')
899
+ text = parsed['content'] or ''
900
+ media_count = parsed['media_count']
901
+ else:
902
+ create_time = item.get('create_time', 0) or 0
903
+ username = item.get('username', '') or item.get('user_name', '')
904
+ text = item.get('summary', '') or ''
905
+ media_count = 0
906
+
907
+ timeline.append({
908
+ 'create_time': create_time,
909
+ 'username': username,
910
+ 'content': text,
911
+ 'media_count': media_count,
912
+ 'table': table,
913
+ })
914
+
915
+ return {'success': True, 'timeline': timeline, 'table': table}
916
+ except Exception as e:
917
+ return {'success': False, 'error': str(e)}
918
+
919
+
920
+ def get_sns_usernames(cursor):
921
+ """Get unique usernames from SnsTopItem_1."""
922
+ tables = [r[0] for r in cursor.execute(
923
+ "SELECT name FROM sqlite_master WHERE type='table';").fetchall()]
924
+
925
+ # Prefer SnsTopItem_1 for user listing (larger)
926
+ table = 'SnsTopItem_1' if 'SnsTopItem_1' in tables else ('SnsTimeLine' if 'SnsTimeLine' in tables else None)
927
+ if not table:
928
+ return {'success': False, 'error': 'No SNS table found'}
929
+
930
+ # Discover username column
931
+ cols = [r[1] for r in cursor.execute(f'PRAGMA table_info([{table}]);').fetchall()]
932
+ uname_col = 'username' if 'username' in cols else 'user_name'
933
+
934
+ try:
935
+ rows = cursor.execute(
936
+ f'SELECT [{uname_col}], COUNT(*) as cnt FROM [{table}] GROUP BY [{uname_col}] ORDER BY cnt DESC'
937
+ ).fetchall()
938
+ usernames = [r[0] for r in rows if r[0]]
939
+ counts = {r[0]: r[1] for r in rows if r[0]}
940
+ return {'success': True, 'usernames': usernames, 'counts': counts}
941
+ except Exception as e:
942
+ return {'success': False, 'error': str(e)}
943
+
944
+
945
+ def get_sns_stats(cursor, my_wxid=None):
946
+ """Get SNS statistics from SnsTopItem_1."""
947
+ tables = [r[0] for r in cursor.execute(
948
+ "SELECT name FROM sqlite_master WHERE type='table';").fetchall()]
949
+
950
+ table = 'SnsTopItem_1' if 'SnsTopItem_1' in tables else ('SnsTimeLine' if 'SnsTimeLine' in tables else None)
951
+ if not table:
952
+ return {'success': False, 'error': 'No SNS table found'}
953
+
954
+ try:
955
+ total = cursor.execute(f'SELECT COUNT(*) FROM [{table}]').fetchone()[0]
956
+
957
+ cols = [r[1] for r in cursor.execute(f'PRAGMA table_info([{table}]);').fetchall()]
958
+ uname_col = 'username' if 'username' in cols else 'user_name'
959
+
960
+ total_friends = 0
961
+ if uname_col:
962
+ total_friends = cursor.execute(
963
+ f'SELECT COUNT(DISTINCT [{uname_col}]) FROM [{table}] WHERE [{uname_col}] IS NOT NULL'
964
+ ).fetchone()[0]
965
+
966
+ my_posts = None
967
+ if my_wxid and uname_col:
968
+ my_posts = cursor.execute(
969
+ f'SELECT COUNT(*) FROM [{table}] WHERE [{uname_col}] = ?',
970
+ (my_wxid,)).fetchone()[0]
971
+
972
+ return {
973
+ 'success': True,
974
+ 'data': {
975
+ 'totalPosts': total,
976
+ 'totalFriends': total_friends,
977
+ 'myPosts': my_posts,
978
+ }
979
+ }
980
+ except Exception as e:
981
+ return {'success': False, 'error': str(e)}
982
+
983
+
442
984
  # ========== Main CLI ==========
443
985
 
444
986
  def main():
@@ -449,57 +991,112 @@ def main():
449
991
  # scan command
450
992
  scan_parser = sub.add_parser('scan', help='Scan memory for keys and match NT databases')
451
993
  scan_parser.add_argument('--json', action='store_true', help='Output as JSON')
994
+ scan_parser.add_argument('--root', default=os.environ.get('WEFLOW_SCAN_ROOT'), help='xwechat_files root directory override')
452
995
 
453
996
  # sessions command
454
997
  sessions_parser = sub.add_parser('sessions', help='List chat sessions')
455
- sessions_parser.add_argument('--db', required=True, help='Path to NT database')
456
- sessions_parser.add_argument('--key', required=True, help='Key hex (64 chars)')
457
- sessions_parser.add_argument('--salt', required=True, help='Salt hex (32 chars)')
458
- sessions_parser.add_argument('--keyword', help='Filter by keyword')
459
- sessions_parser.add_argument('--contact-db', help='Path to contact.db for display names')
460
- sessions_parser.add_argument('--contact-key', help='Contact DB key hex (64 chars)')
461
- sessions_parser.add_argument('--contact-salt', help='Contact DB salt hex (32 chars)')
998
+ sessions_parser.add_argument('--db', default=os.environ.get('WEFLOW_DB_PATH'), required=not os.environ.get('WEFLOW_DB_PATH'), help='Path to NT database')
999
+ sessions_parser.add_argument('--key', default=os.environ.get('WEFLOW_NT_KEY'), required=not os.environ.get('WEFLOW_NT_KEY'), help='Key hex (64 chars)')
1000
+ sessions_parser.add_argument('--salt', default=os.environ.get('WEFLOW_NT_SALT'), required=not os.environ.get('WEFLOW_NT_SALT'), help='Salt hex (32 chars)')
1001
+ sessions_parser.add_argument('--keyword', default=os.environ.get('WEFLOW_QUERY_KEYWORD'), help='Filter by keyword')
1002
+ sessions_parser.add_argument('--contact-db', default=os.environ.get('WEFLOW_CONTACT_DB'), help='Path to contact.db for display names')
1003
+ sessions_parser.add_argument('--contact-key', default=os.environ.get('WEFLOW_CONTACT_KEY'), help='Contact DB key hex (64 chars)')
1004
+ sessions_parser.add_argument('--contact-salt', default=os.environ.get('WEFLOW_CONTACT_SALT'), help='Contact DB salt hex (32 chars)')
462
1005
 
463
1006
  # messages command
464
1007
  msg_parser = sub.add_parser('messages', help='Get messages')
465
- msg_parser.add_argument('--db', required=True, help='Path to NT database')
466
- msg_parser.add_argument('--key', required=True, help='Key hex (64 chars)')
467
- msg_parser.add_argument('--salt', required=True, help='Salt hex (32 chars)')
468
- msg_parser.add_argument('--talker', required=True, help='Talker username')
1008
+ msg_parser.add_argument('--db', default=os.environ.get('WEFLOW_DB_PATH'), required=not os.environ.get('WEFLOW_DB_PATH'), help='Path to NT database')
1009
+ msg_parser.add_argument('--key', default=os.environ.get('WEFLOW_NT_KEY'), required=not os.environ.get('WEFLOW_NT_KEY'), help='Key hex (64 chars)')
1010
+ msg_parser.add_argument('--salt', default=os.environ.get('WEFLOW_NT_SALT'), required=not os.environ.get('WEFLOW_NT_SALT'), help='Salt hex (32 chars)')
1011
+ msg_parser.add_argument('--talker', default=os.environ.get('WEFLOW_TALKER'), required=not os.environ.get('WEFLOW_TALKER'), help='Talker username')
469
1012
  msg_parser.add_argument('--limit', type=int, default=100)
470
1013
  msg_parser.add_argument('--offset', type=int, default=0)
471
- msg_parser.add_argument('--contact-db', help='Path to contact.db for sender names')
472
- msg_parser.add_argument('--contact-key', help='Contact DB key hex (64 chars)')
473
- msg_parser.add_argument('--contact-salt', help='Contact DB salt hex (32 chars)')
474
- msg_parser.add_argument('--own-wxid', help='Account owner wxid (for self-message detection)')
1014
+ msg_parser.add_argument('--contact-db', default=os.environ.get('WEFLOW_CONTACT_DB'), help='Path to contact.db for sender names')
1015
+ msg_parser.add_argument('--contact-key', default=os.environ.get('WEFLOW_CONTACT_KEY'), help='Contact DB key hex (64 chars)')
1016
+ msg_parser.add_argument('--contact-salt', default=os.environ.get('WEFLOW_CONTACT_SALT'), help='Contact DB salt hex (32 chars)')
1017
+ msg_parser.add_argument('--own-wxid', default=os.environ.get('WEFLOW_OWN_WXID'), help='Account owner wxid (for self-message detection)')
475
1018
 
476
1019
  # contacts command
477
1020
  contacts_parser = sub.add_parser('contacts', help='List contacts')
478
- contacts_parser.add_argument('--db', required=True, help='Path to NT database')
479
- contacts_parser.add_argument('--key', required=True, help='Key hex (64 chars)')
480
- contacts_parser.add_argument('--salt', required=True, help='Salt hex (32 chars)')
481
- contacts_parser.add_argument('--keyword', help='Filter by keyword')
1021
+ contacts_parser.add_argument('--db', default=os.environ.get('WEFLOW_DB_PATH'), required=not os.environ.get('WEFLOW_DB_PATH'), help='Path to NT database')
1022
+ contacts_parser.add_argument('--key', default=os.environ.get('WEFLOW_NT_KEY'), required=not os.environ.get('WEFLOW_NT_KEY'), help='Key hex (64 chars)')
1023
+ contacts_parser.add_argument('--salt', default=os.environ.get('WEFLOW_NT_SALT'), required=not os.environ.get('WEFLOW_NT_SALT'), help='Salt hex (32 chars)')
1024
+ contacts_parser.add_argument('--keyword', default=os.environ.get('WEFLOW_QUERY_KEYWORD'), help='Filter by keyword')
482
1025
  contacts_parser.add_argument('--limit', type=int, default=200)
483
- contacts_parser.add_argument('--contact-db', help='Path to contact.db for display names')
484
- contacts_parser.add_argument('--contact-key', help='Contact DB key hex (64 chars)')
485
- contacts_parser.add_argument('--contact-salt', help='Contact DB salt hex (32 chars)')
1026
+ contacts_parser.add_argument('--contact-db', default=os.environ.get('WEFLOW_CONTACT_DB'), help='Path to contact.db for display names')
1027
+ contacts_parser.add_argument('--contact-key', default=os.environ.get('WEFLOW_CONTACT_KEY'), help='Contact DB key hex (64 chars)')
1028
+ contacts_parser.add_argument('--contact-salt', default=os.environ.get('WEFLOW_CONTACT_SALT'), help='Contact DB salt hex (32 chars)')
1029
+
1030
+ # sns-timeline command
1031
+ sns_tl_parser = sub.add_parser('sns-timeline', help='Get SNS/Moments timeline')
1032
+ sns_tl_parser.add_argument('--db', default=os.environ.get('WEFLOW_DB_PATH'), required=not os.environ.get('WEFLOW_DB_PATH'), help='Path to sns.db')
1033
+ sns_tl_parser.add_argument('--key', default=os.environ.get('WEFLOW_NT_KEY'), required=not os.environ.get('WEFLOW_NT_KEY'), help='Key hex (64 chars)')
1034
+ sns_tl_parser.add_argument('--salt', default=os.environ.get('WEFLOW_NT_SALT'), required=not os.environ.get('WEFLOW_NT_SALT'), help='Salt hex (32 chars)')
1035
+ sns_tl_parser.add_argument('--limit', type=int, default=20)
1036
+ sns_tl_parser.add_argument('--offset', type=int, default=0)
1037
+ sns_tl_parser.add_argument('--usernames', default=os.environ.get('WEFLOW_QUERY_USERNAMES'), help='JSON array of usernames to filter')
1038
+ sns_tl_parser.add_argument('--keyword', default=os.environ.get('WEFLOW_QUERY_KEYWORD'), help='Search keyword')
1039
+ sns_tl_parser.add_argument('--start-time', type=int, help='Start timestamp')
1040
+ sns_tl_parser.add_argument('--end-time', type=int, help='End timestamp')
1041
+
1042
+ # sns-usernames command
1043
+ sns_un_parser = sub.add_parser('sns-usernames', help='List usernames with SNS posts')
1044
+ sns_un_parser.add_argument('--db', default=os.environ.get('WEFLOW_DB_PATH'), required=not os.environ.get('WEFLOW_DB_PATH'), help='Path to sns.db')
1045
+ sns_un_parser.add_argument('--key', default=os.environ.get('WEFLOW_NT_KEY'), required=not os.environ.get('WEFLOW_NT_KEY'), help='Key hex (64 chars)')
1046
+ sns_un_parser.add_argument('--salt', default=os.environ.get('WEFLOW_NT_SALT'), required=not os.environ.get('WEFLOW_NT_SALT'), help='Salt hex (32 chars)')
1047
+
1048
+ # sns-stats command
1049
+ sns_stats_parser = sub.add_parser('sns-stats', help='SNS statistics')
1050
+ sns_stats_parser.add_argument('--db', default=os.environ.get('WEFLOW_DB_PATH'), required=not os.environ.get('WEFLOW_DB_PATH'), help='Path to sns.db')
1051
+ sns_stats_parser.add_argument('--key', default=os.environ.get('WEFLOW_NT_KEY'), required=not os.environ.get('WEFLOW_NT_KEY'), help='Key hex (64 chars)')
1052
+ sns_stats_parser.add_argument('--salt', default=os.environ.get('WEFLOW_NT_SALT'), required=not os.environ.get('WEFLOW_NT_SALT'), help='Salt hex (32 chars)')
1053
+ sns_stats_parser.add_argument('--my-wxid', default=os.environ.get('WEFLOW_OWN_WXID'), help='Account owner wxid for my-posts count')
1054
+
1055
+ # fav-schema command
1056
+ fav_schema_parser = sub.add_parser('fav-schema', help='Dump favorite.db schema (key verification)')
1057
+ fav_schema_parser.add_argument('--db', default=os.environ.get('WEFLOW_DB_PATH'), required=not os.environ.get('WEFLOW_DB_PATH'), help='Path to favorite.db')
1058
+ fav_schema_parser.add_argument('--key', default=os.environ.get('WEFLOW_NT_KEY'), required=not os.environ.get('WEFLOW_NT_KEY'), help='Key hex (64 chars)')
1059
+
1060
+ verify_parser = sub.add_parser('verify', help='Verify a key+salt can open a database')
1061
+ verify_parser.add_argument('--db', default=os.environ.get('WEFLOW_DB_PATH'), required=not os.environ.get('WEFLOW_DB_PATH'), help='Path to NT database')
1062
+ verify_parser.add_argument('--key', default=os.environ.get('WEFLOW_NT_KEY'), required=not os.environ.get('WEFLOW_NT_KEY'), help='Key hex (64 chars)')
1063
+ verify_parser.add_argument('--salt', default=os.environ.get('WEFLOW_NT_SALT'), required=not os.environ.get('WEFLOW_NT_SALT'), help='Salt hex (32 chars)')
1064
+
1065
+ # fav-list command
1066
+ fav_list_parser = sub.add_parser('fav-list', help='List favorite items')
1067
+ fav_list_parser.add_argument('--db', default=os.environ.get('WEFLOW_DB_PATH'), required=not os.environ.get('WEFLOW_DB_PATH'), help='Path to favorite.db')
1068
+ fav_list_parser.add_argument('--key', default=os.environ.get('WEFLOW_NT_KEY'), required=not os.environ.get('WEFLOW_NT_KEY'), help='Key hex (64 chars)')
1069
+ fav_list_parser.add_argument('--limit', type=int, default=100)
1070
+ fav_list_parser.add_argument('--offset', type=int, default=0)
1071
+ fav_list_parser.add_argument('--keyword', default=os.environ.get('WEFLOW_QUERY_KEYWORD'), help='Search keyword')
1072
+ fav_list_parser.add_argument('--type', type=int, dest='fav_type',
1073
+ help='Filter by type: 1=text 2=image 4=video 5=article 14=chatrecord')
486
1074
 
487
1075
  args = parser.parse_args()
488
1076
 
489
1077
  if args.command == 'scan':
490
1078
  pid = find_weixin_pid()
491
1079
  if not pid:
492
- print(json.dumps({"error": "Weixin.exe 未运行"}))
1080
+ if IS_WINDOWS:
1081
+ print(json.dumps({"error": "Weixin.exe 未运行"}))
1082
+ else:
1083
+ print(json.dumps({"error": "未检测到 Linux 微信进程,请确认微信已启动并登录"}))
493
1084
  return
494
1085
 
495
1086
  if not args.json:
496
1087
  print(f"扫描进程 PID {pid}...")
497
1088
 
498
- keys = scan_memory_keys(pid)
1089
+ keys, scan_err = scan_memory_keys(pid)
1090
+ if scan_err == 'permission':
1091
+ print(json.dumps({"error": "PERMISSION_DENIED: 读取微信进程内存需要 root 或 CAP_SYS_PTRACE 权限。可使用 sudo 运行,或执行: sudo setcap cap_sys_ptrace=ep $(which python3)"}))
1092
+ return
1093
+ if scan_err == 'gone':
1094
+ print(json.dumps({"error": "微信进程已退出,请重试"}))
1095
+ return
499
1096
  if not args.json:
500
1097
  print(f"找到 {len(keys)} 个密钥")
501
1098
 
502
- databases = find_nt_databases()
1099
+ databases = find_nt_databases(getattr(args, 'root', None))
503
1100
  if not args.json:
504
1101
  print(f"找到 {len(databases)} 个 NT 数据库")
505
1102
 
@@ -510,6 +1107,20 @@ def main():
510
1107
  print(f" {db['name']} ({db['size']/1024/1024:.1f}MB) key={db['key'][:16]}... salt={db['salt'][:16]}...")
511
1108
  else:
512
1109
  print(json.dumps({"keys": keys, "databases": databases, "matched": matched}))
1110
+ return
1111
+
1112
+ if args.command == 'verify':
1113
+ # sqlcipher 打开+读 sqlite_master 才触发解密, 错误密钥在此报 "file is not a database"
1114
+ try:
1115
+ conn, _ = connect_nt_db(args.db, args.key, args.salt)
1116
+ n = conn.execute(
1117
+ "SELECT count(*) FROM sqlite_master WHERE type='table'"
1118
+ ).fetchone()[0]
1119
+ conn.close()
1120
+ print(json.dumps({"success": n > 0, "tables": n}, ensure_ascii=True))
1121
+ except Exception as e:
1122
+ print(json.dumps({"success": False, "error": str(e).split('\n')[0][:200]}, ensure_ascii=True))
1123
+ return
513
1124
 
514
1125
  # Build contact name map once if contact db provided
515
1126
  contact_name_map = {}
@@ -554,6 +1165,40 @@ def main():
554
1165
  print(json.dumps(result, ensure_ascii=True))
555
1166
  conn.close()
556
1167
 
1168
+ elif args.command == 'sns-timeline':
1169
+ conn, _ = connect_nt_db(args.db, args.key, args.salt)
1170
+ usernames = None
1171
+ if args.usernames:
1172
+ try:
1173
+ usernames = json.loads(args.usernames)
1174
+ except: pass
1175
+ result = get_sns_timeline(conn.cursor(), args.limit, args.offset,
1176
+ usernames, args.keyword,
1177
+ args.start_time, args.end_time)
1178
+ print(json.dumps(result, ensure_ascii=True, default=str))
1179
+ conn.close()
1180
+
1181
+ elif args.command == 'sns-usernames':
1182
+ conn, _ = connect_nt_db(args.db, args.key, args.salt)
1183
+ result = get_sns_usernames(conn.cursor())
1184
+ print(json.dumps(result, ensure_ascii=True, default=str))
1185
+ conn.close()
1186
+
1187
+ elif args.command == 'sns-stats':
1188
+ conn, _ = connect_nt_db(args.db, args.key, args.salt)
1189
+ result = get_sns_stats(conn.cursor(), args.my_wxid)
1190
+ print(json.dumps(result, ensure_ascii=True, default=str))
1191
+ conn.close()
1192
+
1193
+ elif args.command == 'fav-schema':
1194
+ result = get_fav_schema(args.db, args.key)
1195
+ print(json.dumps(result, ensure_ascii=True))
1196
+
1197
+ elif args.command == 'fav-list':
1198
+ result = get_favorites(args.db, args.key, args.limit, args.offset,
1199
+ args.keyword, getattr(args, 'fav_type', None))
1200
+ print(json.dumps(result, ensure_ascii=True, default=str))
1201
+
557
1202
  else:
558
1203
  parser.print_help()
559
1204