weflow-cli 1.5.0 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (332) hide show
  1. package/ARCHITECTURE.md +105 -0
  2. package/CHANGELOG.md +121 -0
  3. package/CONTRIBUTING.md +45 -0
  4. package/LICENSE +21 -21
  5. package/OPERATIONS.md +272 -0
  6. package/README.en.md +281 -0
  7. package/README.md +334 -149
  8. package/SECURITY.md +47 -0
  9. package/bin/weflow-cli.ts +4050 -581
  10. package/dist/bin/weflow-cli.js +4310 -588
  11. package/dist/bin/weflow-cli.js.map +1 -1
  12. package/dist/mcp-server/index.js +178 -236
  13. package/dist/mcp-server/index.js.map +1 -1
  14. package/dist/src/core/dbPathService.d.ts +15 -1
  15. package/dist/src/core/dbPathService.d.ts.map +1 -1
  16. package/dist/src/core/dbPathService.js +177 -19
  17. package/dist/src/core/dbPathService.js.map +1 -1
  18. package/dist/src/core/keyService.d.ts +8 -0
  19. package/dist/src/core/keyService.d.ts.map +1 -1
  20. package/dist/src/core/keyService.js +127 -22
  21. package/dist/src/core/keyService.js.map +1 -1
  22. package/dist/src/core/ntCore.d.ts +59 -1
  23. package/dist/src/core/ntCore.d.ts.map +1 -1
  24. package/dist/src/core/ntCore.js +201 -65
  25. package/dist/src/core/ntCore.js.map +1 -1
  26. package/dist/src/core/sqlcipherCore.d.ts.map +1 -1
  27. package/dist/src/core/sqlcipherCore.js +18 -1
  28. package/dist/src/core/sqlcipherCore.js.map +1 -1
  29. package/dist/src/core/wcdbCore.d.ts.map +1 -1
  30. package/dist/src/core/wcdbCore.js +3 -6
  31. package/dist/src/core/wcdbCore.js.map +1 -1
  32. package/dist/src/services/assistantDaemon.d.ts +19 -0
  33. package/dist/src/services/assistantDaemon.d.ts.map +1 -0
  34. package/dist/src/services/assistantDaemon.js +117 -0
  35. package/dist/src/services/assistantDaemon.js.map +1 -0
  36. package/dist/src/services/assistantMemory.d.ts +36 -0
  37. package/dist/src/services/assistantMemory.d.ts.map +1 -0
  38. package/dist/src/services/assistantMemory.js +182 -0
  39. package/dist/src/services/assistantMemory.js.map +1 -0
  40. package/dist/src/services/assistantPrivacy.d.ts +25 -0
  41. package/dist/src/services/assistantPrivacy.d.ts.map +1 -0
  42. package/dist/src/services/assistantPrivacy.js +77 -0
  43. package/dist/src/services/assistantPrivacy.js.map +1 -0
  44. package/dist/src/services/assistantRouting.d.ts +18 -0
  45. package/dist/src/services/assistantRouting.d.ts.map +1 -0
  46. package/dist/src/services/assistantRouting.js +68 -0
  47. package/dist/src/services/assistantRouting.js.map +1 -0
  48. package/dist/src/services/assistantService.d.ts +37 -0
  49. package/dist/src/services/assistantService.d.ts.map +1 -0
  50. package/dist/src/services/assistantService.js +284 -0
  51. package/dist/src/services/assistantService.js.map +1 -0
  52. package/dist/src/services/assistantTools.d.ts +24 -0
  53. package/dist/src/services/assistantTools.d.ts.map +1 -0
  54. package/dist/src/services/assistantTools.js +582 -0
  55. package/dist/src/services/assistantTools.js.map +1 -0
  56. package/dist/src/services/chatService.d.ts +44 -0
  57. package/dist/src/services/chatService.d.ts.map +1 -1
  58. package/dist/src/services/chatService.js +105 -4
  59. package/dist/src/services/chatService.js.map +1 -1
  60. package/dist/src/services/configService.d.ts +55 -0
  61. package/dist/src/services/configService.d.ts.map +1 -1
  62. package/dist/src/services/configService.js +147 -6
  63. package/dist/src/services/configService.js.map +1 -1
  64. package/dist/src/services/evidenceService.d.ts +30 -0
  65. package/dist/src/services/evidenceService.d.ts.map +1 -0
  66. package/dist/src/services/evidenceService.js +98 -0
  67. package/dist/src/services/evidenceService.js.map +1 -0
  68. package/dist/src/services/exportService.d.ts +13 -20
  69. package/dist/src/services/exportService.d.ts.map +1 -1
  70. package/dist/src/services/exportService.js +113 -47
  71. package/dist/src/services/exportService.js.map +1 -1
  72. package/dist/src/services/initKeyService.d.ts +19 -0
  73. package/dist/src/services/initKeyService.d.ts.map +1 -0
  74. package/dist/src/services/initKeyService.js +80 -0
  75. package/dist/src/services/initKeyService.js.map +1 -0
  76. package/dist/src/services/messageContract.d.ts +23 -0
  77. package/dist/src/services/messageContract.d.ts.map +1 -0
  78. package/dist/src/services/messageContract.js +49 -0
  79. package/dist/src/services/messageContract.js.map +1 -0
  80. package/dist/src/services/messageQuery.d.ts +7 -0
  81. package/dist/src/services/messageQuery.d.ts.map +1 -0
  82. package/dist/src/services/messageQuery.js +30 -0
  83. package/dist/src/services/messageQuery.js.map +1 -0
  84. package/dist/src/services/wechat-formatter.js +14 -14
  85. package/dist/src/services/wechatMessageService.d.ts.map +1 -1
  86. package/dist/src/services/wechatMessageService.js +31 -3
  87. package/dist/src/services/wechatMessageService.js.map +1 -1
  88. package/dist/src/services/whitelistService.d.ts +52 -4
  89. package/dist/src/services/whitelistService.d.ts.map +1 -1
  90. package/dist/src/services/whitelistService.js +183 -17
  91. package/dist/src/services/whitelistService.js.map +1 -1
  92. package/dist/src/types.d.ts +20 -0
  93. package/dist/src/types.d.ts.map +1 -1
  94. package/dist/src/utils/dateRange.d.ts +15 -0
  95. package/dist/src/utils/dateRange.d.ts.map +1 -0
  96. package/dist/src/utils/dateRange.js +48 -0
  97. package/dist/src/utils/dateRange.js.map +1 -0
  98. package/dist/src/utils/mcpSecurity.d.ts +5 -0
  99. package/dist/src/utils/mcpSecurity.d.ts.map +1 -0
  100. package/dist/src/utils/mcpSecurity.js +39 -0
  101. package/dist/src/utils/mcpSecurity.js.map +1 -0
  102. package/dist/src/utils/packageRoot.d.ts +2 -0
  103. package/dist/src/utils/packageRoot.d.ts.map +1 -0
  104. package/dist/src/utils/packageRoot.js +16 -0
  105. package/dist/src/utils/packageRoot.js.map +1 -0
  106. package/dist/src/utils/python.d.ts +2 -0
  107. package/dist/src/utils/python.d.ts.map +1 -0
  108. package/dist/src/utils/python.js +72 -0
  109. package/dist/src/utils/python.js.map +1 -0
  110. package/dist/src/utils/pythonProcessEnv.d.ts +3 -0
  111. package/dist/src/utils/pythonProcessEnv.d.ts.map +1 -0
  112. package/dist/src/utils/pythonProcessEnv.js +46 -0
  113. package/dist/src/utils/pythonProcessEnv.js.map +1 -0
  114. package/dist/src/utils/talkerUtils.d.ts +4 -1
  115. package/dist/src/utils/talkerUtils.d.ts.map +1 -1
  116. package/dist/src/utils/talkerUtils.js +9 -9
  117. package/dist/src/utils/talkerUtils.js.map +1 -1
  118. package/dist/src/utils/wechatEmoji.d.ts +5 -0
  119. package/dist/src/utils/wechatEmoji.d.ts.map +1 -0
  120. package/dist/src/utils/wechatEmoji.js +35 -0
  121. package/dist/src/utils/wechatEmoji.js.map +1 -0
  122. package/docs/AI_INTERFACE.md +189 -0
  123. package/docs/BRANCHES.md +87 -0
  124. package/docs/DATA_CONTRACT.md +74 -0
  125. package/docs/DECISIONS.md +270 -0
  126. package/docs/DEEPSEEK_V4_FLASH/346/226/275/345/267/245/346/226/207/346/241/243.md +234 -0
  127. package/docs/EVIDENCE_GUIDE.md +96 -0
  128. package/docs/MCP.md +95 -0
  129. package/docs/PARTNERS.md +41 -0
  130. package/docs/PROJECT_STATE.md +96 -0
  131. package/docs/RELEASING.md +124 -0
  132. package/docs/ROADMAP.md +191 -0
  133. package/docs/SETUP.md +103 -0
  134. package/docs/images/weflow-architecture-gpt-image-2.png +0 -0
  135. package/docs/images/weflow-architecture.png +0 -0
  136. package/docs/images/weflow-architecture.svg +65 -0
  137. package/mcp-server/index.ts +169 -269
  138. package/package.json +81 -72
  139. package/requirements-3x.txt +4 -0
  140. package/requirements-voice.txt +15 -0
  141. package/requirements.txt +9 -0
  142. package/resources/js/marked.min.js +69 -0
  143. package/resources/wechat-emoji/666.png +0 -0
  144. package/resources/wechat-emoji/Awesome.png +0 -0
  145. package/resources/wechat-emoji/Concerned.png +0 -0
  146. package/resources/wechat-emoji/Cry.png +0 -0
  147. package/resources/wechat-emoji/Emm.png +0 -0
  148. package/resources/wechat-emoji/Facepalm.png +0 -0
  149. package/resources/wechat-emoji/Grin.png +0 -0
  150. package/resources/wechat-emoji/OK.png +0 -0
  151. package/resources/wechat-emoji/Respect.png +0 -0
  152. package/resources/wechat-emoji/Sick.png +0 -0
  153. package/resources/wechat-emoji/Sleep.png +0 -0
  154. package/resources/wechat-emoji/Smile.png +0 -0
  155. package/resources/wechat-emoji//344/272/262/344/272/262.png +0 -0
  156. package/resources/wechat-emoji//344/276/277/344/276/277.png +0 -0
  157. package/resources/wechat-emoji//345/201/267/347/254/221.png +0 -0
  158. package/resources/wechat-emoji//345/202/262/346/205/242.png +0 -0
  159. package/resources/wechat-emoji//345/206/215/350/247/201.png +0 -0
  160. package/resources/wechat-emoji//345/207/213/350/260/242.png +0 -0
  161. package/resources/wechat-emoji//345/212/240/346/262/271.png +0 -0
  162. package/resources/wechat-emoji//345/213/276/345/274/225.png +0 -0
  163. package/resources/wechat-emoji//345/217/221/345/221/206.png +0 -0
  164. package/resources/wechat-emoji//345/217/221/346/200/222.png +0 -0
  165. package/resources/wechat-emoji//345/217/221/346/212/226.png +0 -0
  166. package/resources/wechat-emoji//345/217/257/346/200/234.png +0 -0
  167. package/resources/wechat-emoji//345/217/263/345/223/274/345/223/274.png +0 -0
  168. package/resources/wechat-emoji//345/217/271/346/260/224.png +0 -0
  169. package/resources/wechat-emoji//345/220/203/347/223/234.png +0 -0
  170. package/resources/wechat-emoji//345/220/210/345/215/201.png +0 -0
  171. package/resources/wechat-emoji//345/220/220.png +0 -0
  172. package/resources/wechat-emoji//345/221/262/347/211/231.png +0 -0
  173. package/resources/wechat-emoji//345/222/222/351/252/202.png +0 -0
  174. package/resources/wechat-emoji//345/222/226/345/225/241.png +0 -0
  175. package/resources/wechat-emoji//345/223/207.png +0 -0
  176. package/resources/wechat-emoji//345/225/244/351/205/222.png +0 -0
  177. package/resources/wechat-emoji//345/230/230.png +0 -0
  178. package/resources/wechat-emoji//345/230/264/345/224/207.png +0 -0
  179. package/resources/wechat-emoji//345/230/277/345/223/210.png +0 -0
  180. package/resources/wechat-emoji//345/233/247.png +0 -0
  181. package/resources/wechat-emoji//345/233/260.png +0 -0
  182. package/resources/wechat-emoji//345/235/217/347/254/221.png +0 -0
  183. package/resources/wechat-emoji//345/244/247/345/223/255.png +0 -0
  184. package/resources/wechat-emoji//345/244/251/345/225/212.png +0 -0
  185. package/resources/wechat-emoji//345/244/252/351/230/263.png +0 -0
  186. package/resources/wechat-emoji//345/244/261/346/234/233.png +0 -0
  187. package/resources/wechat-emoji//345/245/270/347/254/221.png +0 -0
  188. package/resources/wechat-emoji//345/245/275/347/232/204.png +0 -0
  189. package/resources/wechat-emoji//345/247/224/345/261/210.png +0 -0
  190. package/resources/wechat-emoji//345/256/263/347/276/236.png +0 -0
  191. package/resources/wechat-emoji//345/260/264/345/260/254.png +0 -0
  192. package/resources/wechat-emoji//345/272/206/347/245/235.png +0 -0
  193. package/resources/wechat-emoji//345/274/261.png +0 -0
  194. package/resources/wechat-emoji//345/274/272.png +0 -0
  195. package/resources/wechat-emoji//345/276/227/346/204/217.png +0 -0
  196. package/resources/wechat-emoji//345/276/256/347/254/221.png +0 -0
  197. package/resources/wechat-emoji//345/277/203/347/242/216.png +0 -0
  198. package/resources/wechat-emoji//345/277/253/345/223/255/344/272/206.png +0 -0
  199. package/resources/wechat-emoji//346/201/220/346/203/247.png +0 -0
  200. package/resources/wechat-emoji//346/202/240/351/227/262.png +0 -0
  201. package/resources/wechat-emoji//346/203/212/346/201/220.png +0 -0
  202. package/resources/wechat-emoji//346/203/212/350/256/266.png +0 -0
  203. package/resources/wechat-emoji//346/204/211/345/277/253.png +0 -0
  204. package/resources/wechat-emoji//346/206/250/347/254/221.png +0 -0
  205. package/resources/wechat-emoji//346/211/223/350/204/270.png +0 -0
  206. package/resources/wechat-emoji//346/212/223/347/213/202.png +0 -0
  207. package/resources/wechat-emoji//346/212/240/351/274/273.png +0 -0
  208. package/resources/wechat-emoji//346/212/261/346/213/263.png +0 -0
  209. package/resources/wechat-emoji//346/213/245/346/212/261.png +0 -0
  210. package/resources/wechat-emoji//346/213/263/345/244/264.png +0 -0
  211. package/resources/wechat-emoji//346/215/202/350/204/270.png +0 -0
  212. package/resources/wechat-emoji//346/217/241/346/211/213.png +0 -0
  213. package/resources/wechat-emoji//346/222/207/345/230/264.png +0 -0
  214. package/resources/wechat-emoji//346/223/246/346/261/227.png +0 -0
  215. package/resources/wechat-emoji//346/225/262/346/211/223.png +0 -0
  216. package/resources/wechat-emoji//346/227/240/350/257/255.png +0 -0
  217. package/resources/wechat-emoji//346/227/272/346/237/264.png +0 -0
  218. package/resources/wechat-emoji//346/231/225.png +0 -0
  219. package/resources/wechat-emoji//346/234/210/344/272/256.png +0 -0
  220. package/resources/wechat-emoji//346/234/272/346/231/272.png +0 -0
  221. package/resources/wechat-emoji//346/261/227.png +0 -0
  222. package/resources/wechat-emoji//346/265/201/346/263/252.png +0 -0
  223. package/resources/wechat-emoji//347/202/270/345/274/271.png +0 -0
  224. package/resources/wechat-emoji//347/203/237/350/212/261.png +0 -0
  225. package/resources/wechat-emoji//347/210/206/347/253/271.png +0 -0
  226. package/resources/wechat-emoji//347/210/261/345/277/203.png +0 -0
  227. package/resources/wechat-emoji//347/214/252/345/244/264.png +0 -0
  228. package/resources/wechat-emoji//347/216/253/347/221/260.png +0 -0
  229. package/resources/wechat-emoji//347/224/237/347/227/205.png +0 -0
  230. package/resources/wechat-emoji//347/226/221/351/227/256.png +0 -0
  231. package/resources/wechat-emoji//347/231/274.png +0 -0
  232. package/resources/wechat-emoji//347/231/275/347/234/274.png +0 -0
  233. package/resources/wechat-emoji//347/232/261/347/234/211.png +0 -0
  234. package/resources/wechat-emoji//347/235/241.png +0 -0
  235. package/resources/wechat-emoji//347/240/264/346/266/225/344/270/272/347/254/221.png +0 -0
  236. package/resources/wechat-emoji//347/244/274/347/211/251.png +0 -0
  237. package/resources/wechat-emoji//347/244/276/344/274/232/347/244/276/344/274/232.png +0 -0
  238. package/resources/wechat-emoji//347/246/217.png +0 -0
  239. package/resources/wechat-emoji//347/254/221/350/204/270.png +0 -0
  240. package/resources/wechat-emoji//347/272/242/345/214/205.png +0 -0
  241. package/resources/wechat-emoji//347/277/273/347/231/275/347/234/274.png +0 -0
  242. package/resources/wechat-emoji//350/200/266.png +0 -0
  243. package/resources/wechat-emoji//350/203/234/345/210/251.png +0 -0
  244. package/resources/wechat-emoji//350/204/270/347/272/242.png +0 -0
  245. package/resources/wechat-emoji//350/211/262.png +0 -0
  246. package/resources/wechat-emoji//350/213/246/346/266/251.png +0 -0
  247. package/resources/wechat-emoji//350/217/234/345/210/200.png +0 -0
  248. package/resources/wechat-emoji//350/233/213/347/263/225.png +0 -0
  249. package/resources/wechat-emoji//350/241/260.png +0 -0
  250. package/resources/wechat-emoji//350/243/202/345/274/200.png +0 -0
  251. package/resources/wechat-emoji//350/256/251/346/210/221/347/234/213/347/234/213.png +0 -0
  252. package/resources/wechat-emoji//350/260/203/347/232/256.png +0 -0
  253. package/resources/wechat-emoji//350/267/263/350/267/263.png +0 -0
  254. package/resources/wechat-emoji//350/275/254/345/234/210.png +0 -0
  255. package/resources/wechat-emoji//351/204/231/350/247/206.png +0 -0
  256. package/resources/wechat-emoji//351/227/255/345/230/264.png +0 -0
  257. package/resources/wechat-emoji//351/230/264/351/231/251.png +0 -0
  258. package/resources/wechat-emoji//351/232/276/350/277/207.png +0 -0
  259. package/resources/wechat-emoji//351/252/267/351/253/205.png +0 -0
  260. package/resources/wechat-emoji//351/274/223/346/216/214.png +0 -0
  261. package/scripts/_batch_link_sources.py +73 -73
  262. package/scripts/_utils.py +29 -0
  263. package/scripts/annual_report.py +796 -796
  264. package/scripts/auto_tag.py +128 -128
  265. package/scripts/biz_daily.py +210 -53
  266. package/scripts/chat_report.py +10 -0
  267. package/scripts/chat_stats.py +702 -702
  268. package/scripts/classify_daily.py +17 -11
  269. package/scripts/create_reading_notes.py +370 -370
  270. package/scripts/daily_stats.py +132 -0
  271. package/scripts/enrich_backlinks.py +163 -163
  272. package/scripts/export_chat_html.py +1704 -107
  273. package/scripts/extract_todos.py +445 -404
  274. package/scripts/fav_server.py +119 -12
  275. package/scripts/fix_topics.py +111 -111
  276. package/scripts/generate_ai_report.py +1 -1
  277. package/scripts/generate_html.py +597 -277
  278. package/scripts/inspection_records.py +354 -0
  279. package/scripts/mcp_bridge.py +398 -398
  280. package/scripts/nt_decrypt.py +702 -48
  281. package/scripts/pipeline.py +27 -19
  282. package/scripts/promote_all.py +312 -310
  283. package/scripts/promote_ideas.py +299 -297
  284. package/scripts/rag_chat.py +194 -191
  285. package/scripts/semantic_search.py +435 -431
  286. package/scripts/sync_fav.py +201 -146
  287. package/scripts/sync_weread.py +192 -192
  288. package/scripts/vault_rag.py +140 -139
  289. package/scripts/vault_search.py +141 -141
  290. package/scripts/wechat_emoji.py +132 -0
  291. package/scripts/wechat_emoticon.py +433 -0
  292. package/scripts/wechat_image.py +258 -0
  293. package/scripts/wechat_voice.py +327 -0
  294. package/src/core/dbPathService.ts +163 -18
  295. package/src/core/keyService.ts +137 -25
  296. package/src/core/ntCore.ts +215 -62
  297. package/src/core/sqlcipherCore.ts +17 -1
  298. package/src/core/wcdbCore.ts +3 -7
  299. package/src/services/assistantDaemon.ts +111 -0
  300. package/src/services/assistantMemory.ts +193 -0
  301. package/src/services/assistantPrivacy.ts +87 -0
  302. package/src/services/assistantRouting.ts +92 -0
  303. package/src/services/assistantService.ts +295 -0
  304. package/src/services/assistantTools.ts +549 -0
  305. package/src/services/chatService.ts +133 -4
  306. package/src/services/configService.ts +194 -6
  307. package/src/services/evidenceService.ts +129 -0
  308. package/src/services/exportService.ts +113 -48
  309. package/src/services/initKeyService.ts +83 -0
  310. package/src/services/messageContract.ts +77 -0
  311. package/src/services/messageQuery.ts +38 -0
  312. package/src/services/wechat-formatter.ts +348 -348
  313. package/src/services/wechatMessageService.ts +25 -3
  314. package/src/services/wereadService.ts +308 -308
  315. package/src/services/whitelistService.ts +209 -22
  316. package/src/types.ts +20 -0
  317. package/src/utils/dateRange.ts +58 -0
  318. package/src/utils/mcpSecurity.ts +39 -0
  319. package/src/utils/packageRoot.ts +14 -0
  320. package/src/utils/python.ts +73 -0
  321. package/src/utils/pythonProcessEnv.ts +45 -0
  322. package/src/utils/talkerUtils.ts +14 -2
  323. package/dist/src/utils/errors.d.ts +0 -25
  324. package/dist/src/utils/errors.d.ts.map +0 -1
  325. package/dist/src/utils/errors.js +0 -43
  326. package/dist/src/utils/errors.js.map +0 -1
  327. package/dist/src/utils/pythonRunner.d.ts +0 -27
  328. package/dist/src/utils/pythonRunner.d.ts.map +0 -1
  329. package/dist/src/utils/pythonRunner.js +0 -62
  330. package/dist/src/utils/pythonRunner.js.map +0 -1
  331. package/src/utils/errors.ts +0 -42
  332. package/src/utils/pythonRunner.ts +0 -81
@@ -1,404 +1,445 @@
1
- #!/usr/bin/env python3
2
- """
3
- 微信对话待办提取 & 任务追踪 — AI 识别承诺 + JSON 持久化。
4
-
5
- 用法:
6
- python scripts/extract_todos.py extract [--days 7]
7
- python scripts/extract_todos.py list [--status pending|done] [--urgency high|mid|low]
8
- python scripts/extract_todos.py done <id>
9
- python scripts/extract_todos.py undone <id>
10
- python scripts/extract_todos.py rm <id>
11
- python scripts/extract_todos.py remind
12
-
13
- 数据: ~/.weflow-cli/todos.json
14
- """
15
-
16
- import sys, os, json, hashlib, argparse
17
- from datetime import datetime, timezone, timedelta
18
- from pathlib import Path
19
-
20
- try:
21
- from sqlcipher3 import dbapi2 as sqlcipher
22
- except ImportError:
23
- print(json.dumps({"error": "请安装: pip install sqlcipher3"}))
24
- sys.exit(1)
25
-
26
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
27
- from _utils import load_config, decrypt_lock, call_deepseek
28
-
29
- TODOS_FILE = os.path.join(os.path.expanduser('~'), '.weflow-cli', 'todos.json')
30
- TZ = timezone(timedelta(hours=8))
31
-
32
- TODO_EXTRACT_PROMPT = """你是一个任务提取助手。分析微信聊天记录,提取其中出现的承诺、待办、约定事项。
33
-
34
- 要求:
35
- 1. 提取任何人的承诺或待办(如"我明天做""我处理""尽快发"等)
36
- 2. 每项返回 JSON: {{"task": "事项描述", "urgency": "高/中/低", "deadline": "截止描述或未提及"}}
37
- 3. 不要提取闲聊、问候等非任务内容
38
- 4. 如果没有待办,返回空数组 []
39
-
40
- 聊天记录:
41
- {messages}
42
-
43
- 请只返回 JSON 数组,不要其他内容。"""
44
-
45
-
46
- # ====== Database ======
47
-
48
- def open_db(db_path, key_hex, salt_hex):
49
- raw_key = f"x'{key_hex}{salt_hex}'"
50
- conn = sqlcipher.connect(db_path)
51
- c = conn.cursor()
52
- c.execute(f'PRAGMA key = "{raw_key}";')
53
- c.execute("SELECT count(*) FROM sqlite_master")
54
- return conn
55
-
56
-
57
- def get_name_map(contact_db, contact_key, contact_salt):
58
- name_map = {}
59
- if not contact_db or not contact_key or not os.path.exists(contact_db):
60
- return name_map
61
- try:
62
- conn = open_db(contact_db, contact_key, contact_salt)
63
- c = conn.cursor()
64
- c.execute("SELECT username, COALESCE(NULLIF(remark,''), NULLIF(nick_name,''), username) FROM contact")
65
- for r in c.fetchall():
66
- name_map[r[0]] = r[1]
67
- conn.close()
68
- except:
69
- pass
70
- return name_map
71
-
72
-
73
- def collect_recent_messages(conn, start_ts, end_ts, name_map, limit_per_session=50):
74
- """收集近期待办相关消息:扫描所有会话,过滤含承诺/任务关键词的消息。"""
75
- c = conn.cursor()
76
- try:
77
- c.execute("SELECT user_name FROM Name2Id WHERE is_session = 1")
78
- sessions = [r[0] for r in c.fetchall()]
79
- except:
80
- c.execute("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'Msg_%'")
81
- sessions = [r[0].replace('Msg_', '') for r in c.fetchall()]
82
-
83
- # 关键词预筛选:承诺、任务、约定类词汇
84
- keywords = ['明天', '下次', '等会', '一会', '尽快', '稍后', '待会', '改天',
85
- '我弄', '我做', '我来', '我写', '我发', '我处理', '我整', '我搞',
86
- '我看看', '我去', '我找', '我帮忙', '我帮', '好的我', '行我',
87
- '没问题', 'ok', 'OK', '可以我', '收到我']
88
-
89
- all_msgs = []
90
- seen = set()
91
-
92
- for talker in sessions[:80]:
93
- tbl = f"Msg_{hashlib.md5(talker.encode()).hexdigest()}"
94
- try:
95
- c.execute(f'SELECT COUNT(*) FROM sqlite_master WHERE name="{tbl}"')
96
- if c.fetchone()[0] == 0:
97
- continue
98
-
99
- c.execute(f'''
100
- SELECT create_time, real_sender_id, message_content
101
- FROM "{tbl}"
102
- WHERE create_time >= ? AND create_time < ?
103
- AND message_content IS NOT NULL
104
- ORDER BY create_time DESC
105
- LIMIT ?
106
- ''', (start_ts, end_ts, limit_per_session))
107
-
108
- for ts, sender_id, content in c.fetchall():
109
- if not content or not isinstance(content, str) or len(content) < 5:
110
- continue
111
- # 关键词过滤
112
- content_lower = content.lower()
113
- if not any(kw.lower() in content_lower for kw in keywords):
114
- continue
115
- # 去重
116
- msg_hash = hashlib.md5(f"{talker}:{ts}:{content[:50]}".encode()).hexdigest()
117
- if msg_hash in seen:
118
- continue
119
- seen.add(msg_hash)
120
-
121
- # 获取发送者名称
122
- try:
123
- c2 = conn.cursor()
124
- c2.execute("SELECT user_name FROM Name2Id WHERE rowid = ?", (sender_id,))
125
- row = c2.fetchone()
126
- sender_raw = row[0] if row else str(sender_id)
127
- except:
128
- sender_raw = str(sender_id)
129
- sender_name = name_map.get(sender_raw, sender_raw)
130
- talker_name = name_map.get(talker, talker)
131
-
132
- dt = datetime.fromtimestamp(ts, tz=TZ)
133
- all_msgs.append({
134
- 'time': dt.strftime('%m-%d %H:%M'),
135
- 'talker': talker_name,
136
- 'sender': sender_name,
137
- 'content': content[:300],
138
- })
139
- except:
140
- pass
141
-
142
- all_msgs.sort(key=lambda x: x['time'])
143
- return all_msgs
144
-
145
-
146
- # ====== Todo CRUD ======
147
-
148
- def load_todos() -> list[dict]:
149
- if os.path.exists(TODOS_FILE):
150
- try:
151
- return json.loads(open(TODOS_FILE, encoding='utf-8').read())
152
- except:
153
- return []
154
- return []
155
-
156
-
157
- def save_todos(todos: list[dict]):
158
- os.makedirs(os.path.dirname(TODOS_FILE), exist_ok=True)
159
- with open(TODOS_FILE, 'w', encoding='utf-8') as f:
160
- json.dump(todos, f, ensure_ascii=False, indent=2)
161
-
162
-
163
- def extract_todos(api_key: str, days: int = 7):
164
- """扫描最近的聊天记录,AI 提取待办。"""
165
- config = load_config()
166
- nt_db = config.get('ntDbPath', '')
167
- if not nt_db:
168
- return {"error": "未初始化,请先运行 weflow-cli init"}
169
-
170
- nt_key = decrypt_lock(config.get('ntKey', ''))
171
- nt_salt = config.get('ntSalt', '')
172
- contact_key = decrypt_lock(config.get('contactKey', ''))
173
- contact_salt = config.get('contactSalt', '')
174
- msg_dir = os.path.dirname(nt_db.replace('\\', '/'))
175
- wxid_dir = os.path.dirname(os.path.dirname(msg_dir))
176
- contact_db = os.path.join(wxid_dir, 'db_storage', 'contact', 'contact.db')
177
-
178
- name_map = get_name_map(contact_db, contact_key, contact_salt)
179
- now = datetime.now(TZ)
180
- start_ts = int((now - timedelta(days=days)).timestamp())
181
- end_ts = int(now.timestamp())
182
-
183
- conn = open_db(nt_db, nt_key, nt_salt)
184
- messages = collect_recent_messages(conn, start_ts, end_ts, name_map)
185
- conn.close()
186
-
187
- if not messages:
188
- return {"new": 0, "total": len(load_todos()), "message": "近 {days} 天没有发现相关消息"}
189
-
190
- # 构建 prompt
191
- msg_text = '\n'.join(
192
- f"[{m['time']}] {m['talker']} - {m['sender']}: {m['content']}"
193
- for m in messages[-150:] # 最多 150 条
194
- )
195
- prompt = TODO_EXTRACT_PROMPT.format(messages=msg_text)
196
-
197
- # AI 提取
198
- try:
199
- response = call_deepseek(prompt, api_key, max_tokens=2000, timeout=90)
200
- # 清理可能的 markdown 代码块
201
- response = response.strip()
202
- if response.startswith('```'):
203
- response = response.split('\n', 1)[1]
204
- if response.endswith('```'):
205
- response = response[:-3]
206
- todos_raw = json.loads(response)
207
- except Exception as e:
208
- return {"error": f"AI 提取失败: {e}"}
209
-
210
- if not isinstance(todos_raw, list):
211
- return {"error": "AI 返回格式错误"}
212
-
213
- # 合并到现有待办(去重:相同 task 文本)
214
- existing = load_todos()
215
- existing_tasks = {t['task'] for t in existing}
216
- new_count = 0
217
-
218
- for item in todos_raw:
219
- if not isinstance(item, dict) or not item.get('task'):
220
- continue
221
- if item['task'] in existing_tasks:
222
- continue
223
- todo = {
224
- 'id': f"todo_{now.strftime('%Y%m%d_%H%M%S')}_{new_count + 1}",
225
- 'task': item['task'],
226
- 'urgency': item.get('urgency', '中'),
227
- 'deadline': item.get('deadline', '未提及'),
228
- 'status': 'pending',
229
- 'created_at': now.strftime('%Y-%m-%d %H:%M'),
230
- 'context': '',
231
- }
232
- existing.append(todo)
233
- new_count += 1
234
-
235
- save_todos(existing)
236
- return {"new": new_count, "total": len(existing)}
237
-
238
-
239
- def list_todos(status: str = None, urgency: str = None):
240
- todos = load_todos()
241
- if status:
242
- todos = [t for t in todos if t['status'] == status]
243
- if urgency:
244
- todos = [t for t in todos if t['urgency'] == urgency]
245
-
246
- # 排序:pending 在前,高紧急度在前
247
- order = {'pending': 0, 'done': 1}
248
- urg_order = {'高': 0, '中': 1, '低': 2}
249
- todos.sort(key=lambda t: (order.get(t['status'], 2), urg_order.get(t['urgency'], 3)))
250
-
251
- return todos
252
-
253
-
254
- def toggle_todo(todo_id: str, status: str):
255
- todos = load_todos()
256
- for t in todos:
257
- if t['id'] == todo_id or t['id'].startswith(todo_id):
258
- t['status'] = status
259
- save_todos(todos)
260
- return t
261
- return None
262
-
263
-
264
- def delete_todo(todo_id: str):
265
- todos = load_todos()
266
- for i, t in enumerate(todos):
267
- if t['id'] == todo_id or t['id'].startswith(todo_id):
268
- deleted = todos.pop(i)
269
- save_todos(todos)
270
- return deleted
271
- return None
272
-
273
-
274
- def remind():
275
- """显示需要关注的待办。"""
276
- todos = load_todos()
277
- pending = [t for t in todos if t['status'] == 'pending']
278
- high = [t for t in pending if t['urgency'] == '高']
279
- mid = [t for t in pending if t['urgency'] == '中']
280
- low = [t for t in pending if t['urgency'] == '低']
281
-
282
- return {
283
- 'total_pending': len(pending),
284
- 'high': high,
285
- 'mid': mid,
286
- 'low': low,
287
- }
288
-
289
-
290
- # ====== Main ======
291
-
292
- def main():
293
- sys.stdout.reconfigure(encoding='utf-8', errors='replace')
294
- parser = argparse.ArgumentParser(description='待办提取与任务追踪')
295
- subparsers = parser.add_subparsers(dest='command')
296
-
297
- # extract
298
- p = subparsers.add_parser('extract', help='从聊天记录中 AI 提取待办')
299
- p.add_argument('--days', type=int, default=7, help='扫描最近多少天(默认 7)')
300
- p.add_argument('--api-key', help='DeepSeek API key(优先从 config 读取)')
301
-
302
- # list
303
- p = subparsers.add_parser('list', help='列出待办')
304
- p.add_argument('--status', choices=['pending', 'done'], help='按状态筛选')
305
- p.add_argument('--urgency', choices=['高', '中', '低'], help='按紧急度筛选')
306
- p.add_argument('--json', action='store_true', help='JSON 输出')
307
-
308
- # done
309
- p = subparsers.add_parser('done', help='标记待办为已完成')
310
- p.add_argument('id', help='待办 ID(或前缀)')
311
-
312
- # undone
313
- p = subparsers.add_parser('undone', help='取消已完成标记')
314
- p.add_argument('id', help='待办 ID(或前缀)')
315
-
316
- # rm
317
- p = subparsers.add_parser('rm', help='删除待办')
318
- p.add_argument('id', help='待办 ID(或前缀)')
319
-
320
- # remind
321
- subparsers.add_parser('remind', help='查看待办提醒')
322
-
323
- args = parser.parse_args()
324
-
325
- if not args.command:
326
- parser.print_help()
327
- sys.exit(1)
328
-
329
- config = load_config()
330
- api_key = getattr(args, 'api_key', None) or os.environ.get('DEEPSEEK_API_KEY', '') or config.get('deepseekApiKey', '')
331
-
332
- if args.command == 'extract':
333
- if not api_key:
334
- print('[ERROR] 缺少 DeepSeek API key')
335
- sys.exit(1)
336
- print(f'扫描最近 {args.days} 天的聊天记录...')
337
- result = extract_todos(api_key, days=args.days)
338
- print(json.dumps(result, ensure_ascii=False, indent=2))
339
-
340
- elif args.command == 'list':
341
- result = list_todos(args.status, args.urgency)
342
- if getattr(args, 'json', False):
343
- print(json.dumps(result, ensure_ascii=False, indent=2))
344
- elif not result:
345
- print('✅ 暂无待办')
346
- else:
347
- status_labels = {'pending': '⬜', 'done': '✅'}
348
- urg_labels = {'高': '🔴', '中': '🟡', '低': '🟢'}
349
- for t in result:
350
- s = status_labels.get(t['status'], '❓')
351
- u = urg_labels.get(t['urgency'], '⚪')
352
- print(f" {s} {u} [{t['id']}] {t['task']}")
353
- if t.get('deadline') and t['deadline'] != '未提及':
354
- print(f" ⏰ {t['deadline']}")
355
- print(f"\n共 {len(result)} 项")
356
-
357
- elif args.command == 'done':
358
- t = toggle_todo(args.id, 'done')
359
- if t:
360
- print(f' 已标记完成: {t["task"]}')
361
- else:
362
- print(f'未找到待办: {args.id}')
363
-
364
- elif args.command == 'undone':
365
- t = toggle_todo(args.id, 'pending')
366
- if t:
367
- print(f'🔄 已恢复: {t["task"]}')
368
- else:
369
- print(f'未找到待办: {args.id}')
370
-
371
- elif args.command == 'rm':
372
- t = delete_todo(args.id)
373
- if t:
374
- print(f'🗑️ 已删除: {t["task"]}')
375
- else:
376
- print(f'未找到待办: {args.id}')
377
-
378
- elif args.command == 'remind':
379
- result = remind()
380
- print(f'\n📋 待办提醒 — {datetime.now(TZ).strftime("%Y-%m-%d %H:%M")}')
381
- print(f' 共 {result["total_pending"]} 项未完成\n')
382
- if result['high']:
383
- print('🔴 高紧急度:')
384
- for t in result['high']:
385
- print(f' - {t["task"]}')
386
- if t['deadline'] != '未提及':
387
- print(f' ⏰ {t["deadline"]}')
388
- print()
389
- if result['mid']:
390
- print('🟡 中紧急度:')
391
- for t in result['mid']:
392
- print(f' - {t["task"]}')
393
- print()
394
- if result['low']:
395
- print('🟢 低紧急度:')
396
- for t in result['low']:
397
- print(f' - {t["task"]}')
398
- print()
399
- if result['total_pending'] == 0:
400
- print('✅ 暂无待办,干得好!')
401
-
402
-
403
- if __name__ == '__main__':
404
- main()
1
+ #!/usr/bin/env python3
2
+ """
3
+ 微信对话待办提取 & 任务追踪 — AI 识别承诺 + JSON 持久化。
4
+
5
+ 用法:
6
+ python scripts/extract_todos.py extract [--days 7]
7
+ python scripts/extract_todos.py list [--status pending|done] [--urgency high|mid|low]
8
+ python scripts/extract_todos.py done <id>
9
+ python scripts/extract_todos.py undone <id>
10
+ python scripts/extract_todos.py rm <id>
11
+ python scripts/extract_todos.py remind
12
+
13
+ 数据: ~/.weflow-cli/todos.json
14
+ """
15
+
16
+ import sys, os, json, hashlib, argparse
17
+ from datetime import datetime, timezone, timedelta
18
+ from pathlib import Path
19
+
20
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
21
+ from _utils import load_config, decrypt_lock, call_deepseek
22
+
23
+ TODOS_FILE = os.path.join(os.path.expanduser('~'), '.weflow-cli', 'todos.json')
24
+ TZ = timezone(timedelta(hours=8))
25
+
26
+ TODO_EXTRACT_PROMPT = """你是一个任务提取助手。分析微信聊天记录,提取其中出现的承诺、待办、约定事项。
27
+
28
+ 要求:
29
+ 1. 提取任何人的承诺或待办(如"我明天做""我处理""尽快发"等)
30
+ 2. 每项返回 JSON: {{"task": "事项描述", "urgency": "高/中/低", "deadline": "截止描述或未提及"}}
31
+ 3. 不要提取闲聊、问候等非任务内容
32
+ 4. 如果没有待办,返回空数组 []
33
+
34
+ 聊天记录:
35
+ {messages}
36
+
37
+ 请只返回 JSON 数组,不要其他内容。"""
38
+
39
+
40
+ # ====== Database ======
41
+
42
+ def open_db(db_path, key_hex, salt_hex):
43
+ try:
44
+ from sqlcipher3 import dbapi2 as sqlcipher
45
+ except ImportError as exc:
46
+ raise RuntimeError('请安装: pip install sqlcipher3') from exc
47
+ raw_key = f"x'{key_hex}{salt_hex}'"
48
+ conn = sqlcipher.connect(db_path)
49
+ c = conn.cursor()
50
+ c.execute(f'PRAGMA key = "{raw_key}";')
51
+ c.execute("SELECT count(*) FROM sqlite_master")
52
+ return conn
53
+
54
+
55
+ def get_name_map(contact_db, contact_key, contact_salt):
56
+ name_map = {}
57
+ if not contact_db or not contact_key or not os.path.exists(contact_db):
58
+ return name_map
59
+ try:
60
+ conn = open_db(contact_db, contact_key, contact_salt)
61
+ c = conn.cursor()
62
+ c.execute("SELECT username, COALESCE(NULLIF(remark,''), NULLIF(nick_name,''), username) FROM contact")
63
+ for r in c.fetchall():
64
+ name_map[r[0]] = r[1]
65
+ conn.close()
66
+ except:
67
+ pass
68
+ return name_map
69
+
70
+
71
+ def collect_recent_messages(conn, start_ts, end_ts, name_map, limit_per_session=50):
72
+ """收集近期待办相关消息:扫描所有会话,过滤含承诺/任务关键词的消息。"""
73
+ c = conn.cursor()
74
+ try:
75
+ c.execute("SELECT user_name FROM Name2Id WHERE is_session = 1")
76
+ sessions = [r[0] for r in c.fetchall()]
77
+ except:
78
+ c.execute("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'Msg_%'")
79
+ sessions = [r[0].replace('Msg_', '') for r in c.fetchall()]
80
+
81
+ # 关键词预筛选:承诺、任务、约定类词汇
82
+ keywords = ['明天', '下次', '等会', '一会', '尽快', '稍后', '待会', '改天',
83
+ '我弄', '我做', '我来', '我写', '我发', '我处理', '我整', '我搞',
84
+ '我看看', '我去', '我找', '我帮忙', '我帮', '好的我', '行我',
85
+ '没问题', 'ok', 'OK', '可以我', '收到我']
86
+
87
+ all_msgs = []
88
+ seen = set()
89
+
90
+ for talker in sessions[:80]:
91
+ tbl = f"Msg_{hashlib.md5(talker.encode()).hexdigest()}"
92
+ try:
93
+ c.execute(f'SELECT COUNT(*) FROM sqlite_master WHERE name="{tbl}"')
94
+ if c.fetchone()[0] == 0:
95
+ continue
96
+
97
+ c.execute(f'''
98
+ SELECT create_time, real_sender_id, message_content
99
+ FROM "{tbl}"
100
+ WHERE create_time >= ? AND create_time < ?
101
+ AND message_content IS NOT NULL
102
+ ORDER BY create_time DESC
103
+ LIMIT ?
104
+ ''', (start_ts, end_ts, limit_per_session))
105
+
106
+ for ts, sender_id, content in c.fetchall():
107
+ if not content or not isinstance(content, str) or len(content) < 5:
108
+ continue
109
+ # 关键词过滤
110
+ content_lower = content.lower()
111
+ if not any(kw.lower() in content_lower for kw in keywords):
112
+ continue
113
+ # 去重
114
+ msg_hash = hashlib.md5(f"{talker}:{ts}:{content[:50]}".encode()).hexdigest()
115
+ if msg_hash in seen:
116
+ continue
117
+ seen.add(msg_hash)
118
+
119
+ # 获取发送者名称
120
+ try:
121
+ c2 = conn.cursor()
122
+ c2.execute("SELECT user_name FROM Name2Id WHERE rowid = ?", (sender_id,))
123
+ row = c2.fetchone()
124
+ sender_raw = row[0] if row else str(sender_id)
125
+ except:
126
+ sender_raw = str(sender_id)
127
+ sender_name = name_map.get(sender_raw, sender_raw)
128
+ talker_name = name_map.get(talker, talker)
129
+
130
+ dt = datetime.fromtimestamp(ts, tz=TZ)
131
+ all_msgs.append({
132
+ 'time': dt.strftime('%m-%d %H:%M'),
133
+ 'talker': talker_name,
134
+ 'sender': sender_name,
135
+ 'content': content[:300],
136
+ })
137
+ except:
138
+ pass
139
+
140
+ all_msgs.sort(key=lambda x: x['time'])
141
+ return all_msgs
142
+
143
+
144
+ # ====== Todo CRUD ======
145
+
146
+ def load_todos() -> list[dict]:
147
+ if os.path.exists(TODOS_FILE):
148
+ try:
149
+ return json.loads(open(TODOS_FILE, encoding='utf-8').read())
150
+ except:
151
+ return []
152
+ return []
153
+
154
+
155
+ def save_todos(todos: list[dict]):
156
+ os.makedirs(os.path.dirname(TODOS_FILE), exist_ok=True)
157
+ with open(TODOS_FILE, 'w', encoding='utf-8') as f:
158
+ json.dump(todos, f, ensure_ascii=False, indent=2)
159
+
160
+
161
+ def extract_todos(api_key: str, days: int = 7):
162
+ """扫描最近的聊天记录,AI 提取待办。"""
163
+ config = load_config()
164
+ nt_db = config.get('ntDbPath', '')
165
+ if not nt_db:
166
+ return {"error": "未初始化,请先运行 weflow-cli init"}
167
+
168
+ nt_key = decrypt_lock(config.get('ntKey', ''))
169
+ nt_salt = config.get('ntSalt', '')
170
+ contact_key = decrypt_lock(config.get('contactKey', ''))
171
+ contact_salt = config.get('contactSalt', '')
172
+ msg_dir = os.path.dirname(nt_db.replace('\\', '/'))
173
+ wxid_dir = os.path.dirname(os.path.dirname(msg_dir))
174
+ contact_db = os.path.join(wxid_dir, 'db_storage', 'contact', 'contact.db')
175
+
176
+ name_map = get_name_map(contact_db, contact_key, contact_salt)
177
+ now = datetime.now(TZ)
178
+ start_ts = int((now - timedelta(days=days)).timestamp())
179
+ end_ts = int(now.timestamp())
180
+
181
+ conn = open_db(nt_db, nt_key, nt_salt)
182
+ messages = collect_recent_messages(conn, start_ts, end_ts, name_map)
183
+ conn.close()
184
+
185
+ if not messages:
186
+ return {"new": 0, "total": len(load_todos()), "message": "近 {days} 天没有发现相关消息"}
187
+
188
+ # 构建 prompt
189
+ msg_text = '\n'.join(
190
+ f"[{m['time']}] {m['talker']} - {m['sender']}: {m['content']}"
191
+ for m in messages[-150:] # 最多 150 条
192
+ )
193
+ prompt = TODO_EXTRACT_PROMPT.format(messages=msg_text)
194
+
195
+ # AI 提取
196
+ try:
197
+ response = call_deepseek(prompt, api_key, max_tokens=2000, timeout=90)
198
+ # 清理可能的 markdown 代码块
199
+ response = response.strip()
200
+ if response.startswith('```'):
201
+ response = response.split('\n', 1)[1]
202
+ if response.endswith('```'):
203
+ response = response[:-3]
204
+ todos_raw = json.loads(response)
205
+ except Exception as e:
206
+ return {"error": f"AI 提取失败: {e}"}
207
+
208
+ if not isinstance(todos_raw, list):
209
+ return {"error": "AI 返回格式错误"}
210
+
211
+ # 合并到现有待办(去重:相同 task 文本)
212
+ existing = load_todos()
213
+ existing_tasks = {t['task'] for t in existing}
214
+ new_count = 0
215
+
216
+ for item in todos_raw:
217
+ if not isinstance(item, dict) or not item.get('task'):
218
+ continue
219
+ if item['task'] in existing_tasks:
220
+ continue
221
+ todo = {
222
+ 'id': f"todo_{now.strftime('%Y%m%d_%H%M%S')}_{new_count + 1}",
223
+ 'task': item['task'],
224
+ 'urgency': item.get('urgency', '中'),
225
+ 'deadline': item.get('deadline', '未提及'),
226
+ 'status': 'pending',
227
+ 'created_at': now.strftime('%Y-%m-%d %H:%M'),
228
+ 'context': '',
229
+ }
230
+ existing.append(todo)
231
+ new_count += 1
232
+
233
+ save_todos(existing)
234
+ return {"new": new_count, "total": len(existing)}
235
+
236
+
237
+ def list_todos(status: str = None, urgency: str = None):
238
+ todos = load_todos()
239
+ if status:
240
+ todos = [t for t in todos if t['status'] == status]
241
+ if urgency:
242
+ todos = [t for t in todos if t['urgency'] == urgency]
243
+
244
+ # 排序:pending 在前,高紧急度在前
245
+ order = {'pending': 0, 'done': 1}
246
+ urg_order = {'高': 0, '中': 1, '低': 2}
247
+ todos.sort(key=lambda t: (order.get(t['status'], 2), urg_order.get(t['urgency'], 3)))
248
+
249
+ return todos
250
+
251
+
252
+ def find_todo(todo_id: str):
253
+ for todo in load_todos():
254
+ if todo['id'] == todo_id or todo['id'].startswith(todo_id):
255
+ return todo
256
+ return None
257
+
258
+
259
+ def toggle_todo(todo_id: str, status: str):
260
+ todos = load_todos()
261
+ for t in todos:
262
+ if t['id'] == todo_id or t['id'].startswith(todo_id):
263
+ t['status'] = status
264
+ save_todos(todos)
265
+ return t
266
+ return None
267
+
268
+
269
+ def delete_todo(todo_id: str):
270
+ todos = load_todos()
271
+ for i, t in enumerate(todos):
272
+ if t['id'] == todo_id or t['id'].startswith(todo_id):
273
+ deleted = todos.pop(i)
274
+ save_todos(todos)
275
+ return deleted
276
+ return None
277
+
278
+
279
+ def remind():
280
+ """显示需要关注的待办。"""
281
+ todos = load_todos()
282
+ pending = [t for t in todos if t['status'] == 'pending']
283
+ high = [t for t in pending if t['urgency'] == '高']
284
+ mid = [t for t in pending if t['urgency'] == '中']
285
+ low = [t for t in pending if t['urgency'] == '低']
286
+
287
+ return {
288
+ 'total_pending': len(pending),
289
+ 'high': high,
290
+ 'mid': mid,
291
+ 'low': low,
292
+ }
293
+
294
+
295
+ # ====== Main ======
296
+
297
+ def main():
298
+ sys.stdout.reconfigure(encoding='utf-8', errors='replace')
299
+ parser = argparse.ArgumentParser(description='待办提取与任务追踪')
300
+ subparsers = parser.add_subparsers(dest='command')
301
+
302
+ # extract
303
+ p = subparsers.add_parser('extract', help='从聊天记录中 AI 提取待办')
304
+ p.add_argument('--days', type=int, default=7, help='扫描最近多少天(默认 7)')
305
+ p.add_argument('--api-key', help='DeepSeek API key(优先从 config 读取)')
306
+
307
+ # list
308
+ p = subparsers.add_parser('list', help='列出待办')
309
+ p.add_argument('--status', choices=['pending', 'done'], help='按状态筛选')
310
+ p.add_argument('--urgency', choices=['高', '中', '低'], help='按紧急度筛选')
311
+ p.add_argument('--json', action='store_true', help='JSON 输出')
312
+
313
+ # done
314
+ p = subparsers.add_parser('done', help='标记待办为已完成')
315
+ p.add_argument('id', help='待办 ID(或前缀)')
316
+ p.add_argument('--json', action='store_true', help='JSON 输出')
317
+
318
+ # undone
319
+ p = subparsers.add_parser('undone', help='取消已完成标记')
320
+ p.add_argument('id', help='待办 ID(或前缀)')
321
+ p.add_argument('--json', action='store_true', help='JSON 输出')
322
+
323
+ # rm
324
+ p = subparsers.add_parser('rm', help='删除待办')
325
+ p.add_argument('id', help='待办 ID(或前缀)')
326
+ p.add_argument('--json', action='store_true', help='JSON 输出')
327
+
328
+ p = subparsers.add_parser('preview', help='预览待办写操作,不修改数据')
329
+ p.add_argument('id', help='待办 ID(或前缀)')
330
+ p.add_argument('--action', required=True, choices=['done', 'undone', 'remove'])
331
+
332
+ # remind
333
+ p = subparsers.add_parser('remind', help='查看待办提醒')
334
+ p.add_argument('--json', action='store_true', help='JSON 输出')
335
+
336
+ args = parser.parse_args()
337
+
338
+ if not args.command:
339
+ parser.print_help()
340
+ sys.exit(1)
341
+
342
+ if args.command == 'extract':
343
+ config = load_config()
344
+ api_key = getattr(args, 'api_key', None) or os.environ.get('DEEPSEEK_API_KEY', '') or config.get('deepseekApiKey', '')
345
+ if not api_key:
346
+ print('[ERROR] 缺少 DeepSeek API key')
347
+ sys.exit(1)
348
+ print(f'扫描最近 {args.days} 天的聊天记录...')
349
+ result = extract_todos(api_key, days=args.days)
350
+ print(json.dumps(result, ensure_ascii=False, indent=2))
351
+
352
+ elif args.command == 'list':
353
+ result = list_todos(args.status, args.urgency)
354
+ if getattr(args, 'json', False):
355
+ print(json.dumps(result, ensure_ascii=False, indent=2))
356
+ elif not result:
357
+ print('✅ 暂无待办')
358
+ else:
359
+ status_labels = {'pending': '⬜', 'done': '✅'}
360
+ urg_labels = {'高': '🔴', '中': '🟡', '低': '🟢'}
361
+ for t in result:
362
+ s = status_labels.get(t['status'], '❓')
363
+ u = urg_labels.get(t['urgency'], '⚪')
364
+ print(f" {s} {u} [{t['id']}] {t['task']}")
365
+ if t.get('deadline') and t['deadline'] != '未提及':
366
+ print(f" ⏰ {t['deadline']}")
367
+ print(f"\n共 {len(result)} ")
368
+
369
+ elif args.command == 'done':
370
+ t = toggle_todo(args.id, 'done')
371
+ if args.json:
372
+ print(json.dumps({'success': bool(t), 'action': 'done', 'todo': t}, ensure_ascii=False, indent=2))
373
+ if not t:
374
+ sys.exit(1)
375
+ return
376
+ if t:
377
+ print(f'✅ 已标记完成: {t["task"]}')
378
+ else:
379
+ print(f'未找到待办: {args.id}')
380
+
381
+ elif args.command == 'undone':
382
+ t = toggle_todo(args.id, 'pending')
383
+ if args.json:
384
+ print(json.dumps({'success': bool(t), 'action': 'undone', 'todo': t}, ensure_ascii=False, indent=2))
385
+ if not t:
386
+ sys.exit(1)
387
+ return
388
+ if t:
389
+ print(f'🔄 已恢复: {t["task"]}')
390
+ else:
391
+ print(f'未找到待办: {args.id}')
392
+
393
+ elif args.command == 'rm':
394
+ t = delete_todo(args.id)
395
+ if args.json:
396
+ print(json.dumps({'success': bool(t), 'action': 'remove', 'todo': t}, ensure_ascii=False, indent=2))
397
+ if not t:
398
+ sys.exit(1)
399
+ return
400
+ if t:
401
+ print(f'🗑️ 已删除: {t["task"]}')
402
+ else:
403
+ print(f'未找到待办: {args.id}')
404
+
405
+ elif args.command == 'preview':
406
+ todo = find_todo(args.id)
407
+ print(json.dumps({
408
+ 'success': bool(todo),
409
+ 'dryRun': True,
410
+ 'action': args.action,
411
+ 'todo': todo,
412
+ }, ensure_ascii=False, indent=2))
413
+ if not todo:
414
+ sys.exit(1)
415
+
416
+ elif args.command == 'remind':
417
+ result = remind()
418
+ if args.json:
419
+ print(json.dumps(result, ensure_ascii=False, indent=2))
420
+ return
421
+ print(f'\n📋 待办提醒 — {datetime.now(TZ).strftime("%Y-%m-%d %H:%M")}')
422
+ print(f' 共 {result["total_pending"]} 项未完成\n')
423
+ if result['high']:
424
+ print('🔴 高紧急度:')
425
+ for t in result['high']:
426
+ print(f' - {t["task"]}')
427
+ if t['deadline'] != '未提及':
428
+ print(f' ⏰ {t["deadline"]}')
429
+ print()
430
+ if result['mid']:
431
+ print('🟡 中紧急度:')
432
+ for t in result['mid']:
433
+ print(f' - {t["task"]}')
434
+ print()
435
+ if result['low']:
436
+ print('🟢 低紧急度:')
437
+ for t in result['low']:
438
+ print(f' - {t["task"]}')
439
+ print()
440
+ if result['total_pending'] == 0:
441
+ print('✅ 暂无待办,干得好!')
442
+
443
+
444
+ if __name__ == '__main__':
445
+ main()