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
@@ -27,7 +27,7 @@ from pathlib import Path
27
27
 
28
28
  # 公共工具
29
29
  sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
30
- from _utils import call_deepseek, load_config, decrypt_lock, write_with_frontmatter, format_wikilinks
30
+ from _utils import call_deepseek, load_config, decrypt_lock, get_api_key, write_with_frontmatter, format_wikilinks
31
31
 
32
32
  try:
33
33
  from sqlcipher3 import dbapi2 as sqlcipher
@@ -56,7 +56,31 @@ DEEPSEEK_TIMEOUT = 60
56
56
  FETCH_DELAY_MIN = 8 # 最小抓取间隔 (秒)
57
57
  FETCH_DELAY_MAX = 12 # 最大抓取间隔 (秒)
58
58
 
59
- TOPICS = ['AI', '学术', '新闻', '文学', '投资']
59
+ TOPICS = ['AI', '学术', '新闻', '文学', '投资', '政治']
60
+
61
+
62
+ def load_source_categories(config: dict) -> dict[str, str]:
63
+ """Load the explicit official-account category map from CLI config."""
64
+ raw = config.get('dailySourceCategories', '')
65
+ if not raw:
66
+ return {}
67
+ try:
68
+ data = json.loads(raw)
69
+ except (TypeError, json.JSONDecodeError):
70
+ print('[WARN] dailySourceCategories 不是有效 JSON,忽略来源类别配置')
71
+ return {}
72
+ if not isinstance(data, dict):
73
+ return {}
74
+ return {
75
+ str(name).strip().casefold(): str(category).strip()
76
+ for name, category in data.items()
77
+ if str(name).strip() and str(category).strip() in TOPICS
78
+ }
79
+
80
+
81
+ def get_source_category(categories: dict[str, str], username: str, display_name: str) -> str:
82
+ """Match a category by official-account ID first, then display name."""
83
+ return categories.get(username.casefold()) or categories.get(display_name.casefold()) or ''
60
84
  def _guess_topic(article: dict) -> str:
61
85
  """Keyword-based topic guess when AI classification fails."""
62
86
  title = article.get('title', '')
@@ -90,36 +114,40 @@ def _guess_topic(article: dict) -> str:
90
114
  return '新闻'
91
115
 
92
116
 
93
- TOPIC_PROMPT = f"""对文章分类、摘要、打标签,并评估与读者的相关度。
117
+ TOPIC_PROMPT = f"""对文章分类、深度摘要、打标签,并评估与读者的相关度。
94
118
 
95
119
  【读者定位】环境科学研究生,研究方向是计算机与环境的交叉领域(环境模型、大气污染模拟、遥感反演、环境大数据分析、LCA等),关注AI工具如何提升科研效率。
96
120
 
97
- 【主题】必须且只能是:{' / '.join(TOPICS)} 中的一个词,不要写其他任何文字。
121
+ 【主题】必须且只能是:{' / '.join(TOPICS)} 中的一个词。
98
122
 
99
123
  判断规则:
100
- - AI:涉及AI大模型/Agent/编程/开源/科技产品/工具教程 → 归AI
101
- - 投资:涉及股票基金/融资/经济分析/商业市场 → 归投资
102
- - 新闻:时事政策/社会热点/娱乐圈/招聘促销/会议通知 → 归新闻
103
- - 文学:散文小说/美食旅游/生活随笔/历史文化 → 归文学
104
- - 学术:严格科研论文/学术期刊/实验室研究/学位论文 → 归学术(公众号文章极少属此类)
124
+ - AIAI大模型/Agent/编程/开源/科技产品/工具教程
125
+ - 投资:股票基金/融资/经济分析/商业市场
126
+ - 新闻:时事政策/社会热点/娱乐/招聘促销/会议通知
127
+ - 文学:散文小说/美食旅游/生活随笔/历史文化
128
+ - 学术:科研论文/期刊文章/实验室成果/学术会议/高校研究(环境/气候/地理/海洋/生态/化学/材料/生物医学等)
105
129
 
106
- 【相关度】判断这篇文章对上述读者的实用价值:
130
+ 【相关度】对上述读者的实用价值:
107
131
  - 高:可直接用于科研(新工具/新方法/数据源/代码库)
108
- - 中:有启发性,需转化后使用(思路/趋势/跨领域技术)
109
- - 低:信息性阅读,无直接行动价值(纯新闻/娱乐/文学)
132
+ - 中:有启发性(思路/趋势/跨领域技术)
133
+ - 低:信息性阅读(纯新闻/娱乐/文学)
134
+
135
+ **分类关键**:
136
+ - 【主题】只写一个词(AI/学术/新闻/文学/投资),不要写其他文字
137
+ - 【相关度】只写一个词(高/中/低)
138
+ - 科研论文、期刊文章优先归学术;AI技术/工具/产品归AI
110
139
 
111
- **关键**:
112
- - 【主题】这一行只写一个词:AI 或 学术 或 新闻 或 文学 或 投资
113
- - 【相关度】只写:高 / 中 / 低
114
- - 科技报道、开发者工具、AI产品即使提到Nature/Science也归AI或新闻,不归学术
115
- - 不确定时选最可能的非学术分类
140
+ **摘要要求**:
141
+ - 【摘要】写一段完整的深度摘要(150-300字),不要只写一两句
142
+ - 格式:【核心观点】一句话概括中心思想。【关键细节】列出3-5个具体要点(工具/方法/数据/结论/人物/事件等),每个要点一句话
143
+ - 摘要不需要包含分类/相关度/标签信息,那些由上面的字段处理
116
144
 
117
145
  返回格式(严格):
118
146
  【主题】AI
119
147
  【相关度】高
120
148
  【标签】tag1, tag2, tag3
121
- 【摘要】2-4句中文总结
122
- 【概念】概念名|说明, 概念名|说明"""
149
+ 【摘要】【核心观点】一句话。【关键细节】1. 要点一;2. 要点二;3. 要点三
150
+ 【概念】概念名|一句话说明, 概念名|一句话说明"""
123
151
 
124
152
  # ====== Helpers ======
125
153
 
@@ -141,16 +169,35 @@ def get_db_keys(config):
141
169
  contact_salt = config.get('contactSalt', '')
142
170
  contact_key = decrypt_lock(contact_key_enc) if contact_key_enc else ''
143
171
 
144
- # biz_message_0.db (separate key from message/contact db)
172
+ # biz_message_0.db (每库独立密钥)
145
173
  biz_db = os.path.join(msg_dir, 'biz_message_0.db')
146
- # Try config first, then fallback to known key from scan
147
174
  biz_key_enc = config.get('bizKey', '')
148
175
  biz_salt = config.get('bizSalt', '')
176
+ pass_enc = config.get('favPassphrase', '')
177
+ passphrase = decrypt_lock(pass_enc) if pass_enc else ''
178
+ if passphrase and os.path.exists(biz_db):
179
+ with open(biz_db, 'rb') as fh:
180
+ biz_salt = fh.read(16).hex()
181
+ biz_key_enc = ''
149
182
  if biz_key_enc and biz_salt:
150
183
  biz_key = decrypt_lock(biz_key_enc)
151
184
  else:
152
- print('[ERROR] 缺少 biz_message_0.db 密钥,请运行: python scripts/nt_decrypt.py scan --json')
153
- sys.exit(1)
185
+ # 从全库共用 passphrase 派生 (微信 4.x: PBKDF2-HMAC-SHA512, 256000 轮, 32 字节)
186
+ pass_enc = config.get('favPassphrase', '')
187
+ passphrase = decrypt_lock(pass_enc) if pass_enc else ''
188
+ biz_key = ''
189
+ if passphrase and os.path.exists(biz_db):
190
+ with open(biz_db, 'rb') as fh:
191
+ biz_salt = fh.read(16).hex()
192
+ biz_key = hashlib.pbkdf2_hmac(
193
+ 'sha512', bytes.fromhex(passphrase),
194
+ bytes.fromhex(biz_salt), 256000, dklen=32
195
+ ).hex()
196
+ if not biz_key:
197
+ raise SystemExit(
198
+ '缺少公众号数据库密钥: 请运行 weflow-cli fav set-key --passphrase <64位hex> '
199
+ '配置全库 passphrase (自动派生各库密钥), 或 weflow-cli config set bizKey <raw key>'
200
+ )
154
201
 
155
202
  return {
156
203
  'biz_db': biz_db, 'biz_key': biz_key, 'biz_salt': biz_salt,
@@ -317,6 +364,8 @@ def download_images_to_local(markdown: str, images_dir: Path) -> tuple[str, dict
317
364
  def extract_article_info(content_bytes: bytes) -> dict:
318
365
  """Extract title, digest, URL, full text from protobuf message_content."""
319
366
  import zstandard as zstd
367
+ if isinstance(content_bytes, str):
368
+ content_bytes = content_bytes.encode('utf-8', errors='ignore')
320
369
  dctx = zstd.ZstdDecompressor()
321
370
  try:
322
371
  text = dctx.decompress(content_bytes).decode('utf-8', errors='ignore')
@@ -384,6 +433,19 @@ def sanitize_filename(name: str) -> str:
384
433
  return re.sub(r'[\\/:*?"<>|]', '_', name)[:80]
385
434
 
386
435
 
436
+ def parse_source_filters(values) -> list[str]:
437
+ """Normalize repeated or comma-separated official-account filters."""
438
+ filters = []
439
+ seen = set()
440
+ for value in values or []:
441
+ for item in re.split(r'[,;\n]+', str(value or '')):
442
+ normalized = item.strip().strip('"\'')
443
+ if normalized and normalized.casefold() not in seen:
444
+ seen.add(normalized.casefold())
445
+ filters.append(normalized)
446
+ return filters
447
+
448
+
387
449
  # ====== Main ======
388
450
 
389
451
  def main():
@@ -394,7 +456,11 @@ def main():
394
456
  parser.add_argument('--date', help='日期 YYYY-MM-DD, 默认今天')
395
457
  parser.add_argument('--dry-run', action='store_true', help='仅预览,不抓取')
396
458
  parser.add_argument('--limit', type=int, default=0, help='最多处理篇数 (0=不限制)')
397
- parser.add_argument('--api-key', help='DeepSeek API key (或设环境变量 DEEPSEEK_API_KEY)')
459
+ parser.add_argument('--api-key', help='AI API key (或设环境变量 DEEPSEEK_API_KEY)')
460
+ parser.add_argument('--engine', default='deepseek', help='AI 引擎: local/deepseek/claude/ollama')
461
+ parser.add_argument('--no-ai', action='store_true', help='关闭摘要、分类和日报简报的 AI 调用')
462
+ parser.add_argument('--source', action='append', default=[], metavar='NAME',
463
+ help='仅处理指定公众号,可重复或用逗号分隔;默认读取配置 dailySources')
398
464
  args = parser.parse_args()
399
465
 
400
466
  # Date
@@ -409,16 +475,27 @@ def main():
409
475
 
410
476
  # Load config & DB keys
411
477
  config = load_config()
478
+ if str(config.get('dailyAiEnabled', 'true')).strip().lower() == 'false':
479
+ args.no_ai = True
412
480
  keys = get_db_keys(config)
413
-
414
- # API key
415
- api_key = args.api_key or os.environ.get('DEEPSEEK_API_KEY', '') or config.get('deepseekApiKey', '')
416
- if not args.dry_run and not api_key:
417
- print('[ERROR] 缺少 DeepSeek API key。请通过 --api-key、环境变量 DEEPSEEK_API_KEY 或 ~/.weflow-cli/config.json 中的 deepseekApiKey 提供')
481
+ source_filters = parse_source_filters(args.source or [config.get('dailySources', '')])
482
+ source_categories = load_source_categories(config)
483
+
484
+ # API key only required for cloud engines
485
+ engine = args.engine or 'deepseek'
486
+ api_key = args.api_key or os.environ.get('DEEPSEEK_API_KEY', '') or get_api_key(config)
487
+ if not args.dry_run and engine in ('deepseek', 'claude') and not api_key:
488
+ print(f'[ERROR] --engine {engine} 需要 API key。请通过 --api-key、环境变量或配置文件提供')
418
489
  sys.exit(1)
490
+ # Auto-detect local engine if no api_key and engine is deepseek
491
+ if not api_key and engine == 'deepseek':
492
+ engine = 'local'
493
+ print(f' 无 API key,切换到本地引擎')
419
494
 
420
495
  print(f'=== 公众号日报 {date_str} ===')
421
496
  print(f'Biz DB: {keys["biz_db"]}')
497
+ if source_filters:
498
+ print(f'公众号筛选: {", ".join(source_filters)}')
422
499
 
423
500
  # Connect biz db
424
501
  raw_key = f"x'{keys['biz_key']}{keys['biz_salt']}'"
@@ -443,6 +520,16 @@ def main():
443
520
  # Find today's articles
444
521
  c.execute("SELECT user_name FROM Name2Id WHERE user_name LIKE 'gh_%'")
445
522
  biz_users = [row[0] for row in c.fetchall()]
523
+ if source_filters:
524
+ filter_keys = {item.casefold() for item in source_filters}
525
+ biz_users = [
526
+ user for user in biz_users
527
+ if user.casefold() in filter_keys
528
+ or str(name_map.get(user, user)).casefold() in filter_keys
529
+ ]
530
+ print(f'匹配公众号: {len(biz_users)} 个')
531
+ if not biz_users:
532
+ print('[WARN] 没有匹配的公众号;请检查昵称或 gh_ ID')
446
533
 
447
534
  articles = []
448
535
  for user in biz_users:
@@ -465,6 +552,9 @@ def main():
465
552
  articles.append({
466
553
  'account': user,
467
554
  'account_name': name_map.get(user, user),
555
+ 'source_category': get_source_category(
556
+ source_categories, user, name_map.get(user, user)
557
+ ) if source_filters else '',
468
558
  'title': info['title'],
469
559
  'digest': info['digest'],
470
560
  'url': info['url'],
@@ -536,26 +626,49 @@ def main():
536
626
  elif a.get('local_text'):
537
627
  print(f' 无URL, 使用本地缓存')
538
628
 
539
- # ====== Phase 2: DeepSeek summary + topic classification ======
540
- if api_key:
541
- print(f'\n=== Phase 2: AI 摘要 + 主题分类 ===\n')
629
+ # ====== Phase 2: AI summary + topic classification ======
630
+ if not args.no_ai and (api_key or engine != 'deepseek'):
631
+ print(f'\n=== Phase 2: AI 摘要 + 主题分类 (engine={engine}) ===\n')
632
+ from _utils import call_ai
542
633
  for i, a in enumerate(articles):
543
634
  t, n, ti = a['time'], a['account_name'], a['title']
544
635
  content = a.get('fetched_md') or a.get('local_text', '')
545
636
  if content and len(content.strip()) > 50:
546
637
  try:
547
- prompt = TOPIC_PROMPT + f'\n\n标题:{a["title"]}\n来源:{a["account_name"]}\n\n内容:\n{content[:4000]}'
548
- response = call_deepseek(prompt, api_key, max_tokens=600)
638
+ category_hint = a.get('source_category', '')
639
+ if category_hint:
640
+ summary_prompt = f'''请只为下面这篇公众号文章生成一段 50-300 字的中文摘要。
641
+ 来源类别已经确定为「{category_hint}」,不要重新判断或改写文章分类,不要输出主题、标签、相关度或概念字段。
642
+
643
+ 标题:{a["title"]}
644
+ 来源:{a["account_name"]}
645
+
646
+ 正文:
647
+ {content[:4000]}'''
648
+ response = call_ai(summary_prompt, engine, api_key, max_tokens=1000)
649
+ a['topic'] = category_hint
650
+ a['tags'] = [category_hint]
651
+ a['summary'] = response.strip()[:1000]
652
+ a['relevance'] = '中'
653
+ a['concepts'] = []
654
+ print(f'[{i+1}/{len(articles)}] [{t}] {n} - [{category_hint}] 固定来源类别,仅生成摘要')
655
+ time.sleep(0.3)
656
+ continue
657
+ prompt = TOPIC_PROMPT + f'\n\n标题:{a["title"]}\n来源:{a["account_name"]}'
658
+ prompt += f'\n\n内容:\n{content[:4000]}'
659
+ response = call_ai(prompt, engine, api_key, max_tokens=2000)
549
660
 
550
661
  # Parse response: 【主题】xxx 【相关度】xxx 【标签】xxx 【摘要】xxx 【概念】xxx
551
662
  topic_match = re.search(r'【主题】\s*(.+)', response)
552
663
  relevance_match = re.search(r'【相关度】\s*(.+)', response)
553
664
  tags_match = re.search(r'【标签】\s*(.+)', response)
554
- # Stop summary at next 【tag or end
555
- summary_match = re.search(r'【摘要】\s*(.+?)(?=\n【|$)', response, re.DOTALL)
665
+ # Capture summary - stop at next top-level 【tag (主题/相关度/标签/概念) or end
666
+ summary_match = re.search(r'【摘要】\s*(.+?)(?=\n【(?:主题|相关度|标签|概念)】|$)', response, re.DOTALL)
556
667
  concepts_match = re.search(r'【概念】\s*(.+)', response, re.DOTALL)
557
668
 
558
- if topic_match:
669
+ if category_hint:
670
+ a['topic'] = category_hint
671
+ elif topic_match:
559
672
  raw_topic = topic_match.group(1).strip()
560
673
  if raw_topic in TOPICS:
561
674
  a['topic'] = raw_topic
@@ -589,7 +702,9 @@ def main():
589
702
  a['relevance'] = '中'
590
703
 
591
704
  # Parse tags: comma-separated, clean up
592
- if tags_match:
705
+ if category_hint:
706
+ a['tags'] = [category_hint]
707
+ elif tags_match:
593
708
  a['tags'] = [t.strip() for t in tags_match.group(1).split(',') if t.strip()]
594
709
  else:
595
710
  a['tags'] = [a['topic']]
@@ -617,16 +732,16 @@ def main():
617
732
  time.sleep(0.3)
618
733
  except Exception as e:
619
734
  a['summary'] = a.get('digest', '') or content[:300]
620
- a['topic'] = '学术'
621
- a['tags'] = ['学术']
735
+ a['topic'] = a.get('source_category') or '学术'
736
+ a['tags'] = [a['topic']]
622
737
  a['concepts'] = []
623
738
  print(f'[{i+1}/{len(articles)}] [{t}] {n} - ERR: {e}')
624
739
  elif content:
625
740
  a['summary'] = content[:400]
626
- a['topic'] = '学术'
741
+ a['topic'] = a.get('source_category') or '学术'
627
742
  else:
628
743
  a['summary'] = a.get('digest', '(无内容)')
629
- a['topic'] = '学术'
744
+ a['topic'] = a.get('source_category') or '学术'
630
745
 
631
746
  # Print topic distribution
632
747
  from collections import Counter
@@ -787,7 +902,7 @@ def main():
787
902
 
788
903
  # Generate AI briefing from all article files on disk
789
904
  briefing = ''
790
- if api_key and all_articles:
905
+ if all_articles:
791
906
  topic_counts = {}
792
907
  for a in all_articles:
793
908
  t = a['topic']
@@ -821,13 +936,13 @@ def main():
821
936
  break
822
937
  if summary:
823
938
  highlights.append(f"[{topic}] {title} | {summary}")
824
- except:
939
+ except Exception:
825
940
  pass
826
941
  if len(highlights) >= 25:
827
942
  break
828
943
 
829
944
  highlight_text = '\n'.join(highlights[:25])
830
- if highlight_text:
945
+ if highlight_text and not args.no_ai:
831
946
  try:
832
947
  briefing_prompt = f"""你是公众号日报助手。基于今天 {len(all_articles)} 篇文章({topic_summary}),生成一段200字以内的今日简报。
833
948
 
@@ -840,13 +955,37 @@ def main():
840
955
  {highlight_text}
841
956
 
842
957
  请直接输出简报内容(不要标题)。"""
843
- briefing = call_deepseek(briefing_prompt, api_key, max_tokens=500, timeout=90).strip()
958
+ # Use call_ai to support local/deepseek/claude engines
959
+ from _utils import call_ai
960
+ engine_type = 'deepseek' if api_key else 'local'
961
+ briefing = call_ai(briefing_prompt, engine_type, api_key, max_tokens=500).strip()
844
962
  if briefing:
845
963
  print(f'\n ✓ 简报生成完成')
846
964
  else:
847
- print(f'\n [WARN] 简报 API 返回空,跳过')
965
+ print(f'\n [WARN] 简报 API 返回空,使用摘要拼接')
966
+ briefing = ''
848
967
  except Exception as e:
849
- print(f'\n [WARN] 简报生成失败: {e}')
968
+ print(f'\n [WARN] 简报生成失败: {e},使用摘要拼接')
969
+ briefing = ''
970
+
971
+ # Fallback: build briefing from highlights if AI failed
972
+ if not briefing and highlights:
973
+ topic_groups = {}
974
+ for h in highlights[:15]:
975
+ parts = h.split(' | ', 1)
976
+ title_part = parts[0].replace('[', '').replace(']', ' - ')
977
+ summary_part = parts[1] if len(parts) > 1 else ''
978
+ topic = h.split(']')[0].replace('[', '')
979
+ if topic not in topic_groups:
980
+ topic_groups[topic] = []
981
+ if len(topic_groups[topic]) < 3:
982
+ topic_groups[topic].append(f"{title_part.split(' - ', 1)[-1]}:{summary_part[:60]}")
983
+ parts = []
984
+ for topic in TOPICS:
985
+ if topic in topic_groups:
986
+ items = topic_groups[topic]
987
+ parts.append(f"{topic}:{';'.join(items)}")
988
+ briefing = '今日共收录' + str(len(all_articles)) + '篇文章。' + '。'.join(parts) if parts else f'今日共收录{len(all_articles)}篇文章,涵盖{topic_summary}。'
850
989
 
851
990
  index_lines = [
852
991
  f'# 公众号日报 — {date_str}',
@@ -882,12 +1021,30 @@ def main():
882
1021
  if count:
883
1022
  print(f' {t}/: {count} 篇')
884
1023
 
885
- # Save run state for incremental dedup
1024
+ # Save run state for incremental dedup (atomically)
1025
+ # Only mark articles that were actually written to disk
1026
+ written_urls = set()
1027
+ for topic in TOPICS:
1028
+ topic_dir = out_dir / topic
1029
+ if not topic_dir.is_dir():
1030
+ continue
1031
+ for md_file in topic_dir.glob('*.md'):
1032
+ try:
1033
+ content = md_file.read_text(encoding='utf-8')
1034
+ for line in content.split('\n'):
1035
+ if line.strip().startswith('url:'):
1036
+ url = line.split(':', 1)[1].strip().strip('"').strip("'")
1037
+ if url:
1038
+ written_urls.add(url)
1039
+ break
1040
+ except Exception:
1041
+ pass
886
1042
  for a in topic_groups.values():
887
- for art in a:
888
- fp = hashlib.sha256(art.get('url', '').encode()).hexdigest()[:16]
889
- if art.get('url'):
890
- processed[fp] = art['title'][:50]
1043
+ if isinstance(a, list):
1044
+ for art in a:
1045
+ if isinstance(art, dict) and art.get('url') and art['url'] in written_urls:
1046
+ fp = hashlib.sha256(art['url'].encode()).hexdigest()[:16]
1047
+ processed[fp] = art.get('title', '')[:50]
891
1048
  with open(state_file, 'w', encoding='utf-8') as f:
892
1049
  json.dump(processed, f, ensure_ascii=False, indent=2)
893
1050
 
@@ -152,6 +152,16 @@ def main():
152
152
  p.add_argument('--output', '-o', default=OUTPUT_ROOT)
153
153
  args = p.parse_args()
154
154
 
155
+ env_talkers = os.environ.get('WEFLOW_REPORT_TALKERS', '')
156
+ if env_talkers and not args.talker:
157
+ try:
158
+ parsed_talkers = json.loads(env_talkers)
159
+ if isinstance(parsed_talkers, list):
160
+ args.talker = [str(item) for item in parsed_talkers if str(item).strip()]
161
+ except (TypeError, ValueError, json.JSONDecodeError):
162
+ print('[ERROR] invalid report talker selection')
163
+ sys.exit(1)
164
+
155
165
  # Time range
156
166
  tz = timezone(timedelta(hours=8))
157
167
  if args.month: