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
@@ -9,14 +9,48 @@
9
9
  打开 http://localhost:8765 即可使用,点击 ☆ 收藏按钮实时写入磁盘。
10
10
  """
11
11
 
12
- import sys, os, json
12
+ import sys, os, json, hashlib, shutil, ipaddress, socket
13
13
  from pathlib import Path
14
14
  from http.server import HTTPServer, SimpleHTTPRequestHandler
15
+ from socketserver import ThreadingMixIn
16
+ from urllib.parse import unquote, urlparse
15
17
  import urllib.request
16
18
 
17
19
  SCRIPTS_DIR = os.path.dirname(os.path.abspath(__file__))
18
20
  PROJECT_ROOT = os.path.dirname(SCRIPTS_DIR)
19
21
  SOURCE_ROOT = os.path.join(PROJECT_ROOT, 'output', 'biz-daily')
22
+ MAX_BODY_BYTES = 1 * 1024 * 1024
23
+
24
+
25
+ def is_safe_remote_url(raw_url):
26
+ """Allow public HTTP(S) image hosts while rejecting SSRF targets."""
27
+ try:
28
+ parsed = urlparse(raw_url)
29
+ if parsed.scheme not in ('http', 'https') or not parsed.hostname or parsed.username or parsed.password:
30
+ return False
31
+ if parsed.port and not 1 <= parsed.port <= 65535:
32
+ return False
33
+ addresses = socket.getaddrinfo(parsed.hostname, parsed.port or 443, type=socket.SOCK_STREAM)
34
+ for address in addresses:
35
+ ip = ipaddress.ip_address(address[4][0])
36
+ if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved or ip.is_multicast or ip.is_unspecified:
37
+ return False
38
+ return True
39
+ except (ValueError, socket.gaierror, OSError):
40
+ return False
41
+
42
+
43
+ class NoRedirectHandler(urllib.request.HTTPRedirectHandler):
44
+ def redirect_request(self, req, fp, code, msg, headers, newurl):
45
+ return None
46
+
47
+
48
+ REMOTE_OPENER = urllib.request.build_opener(NoRedirectHandler)
49
+
50
+
51
+ class ThreadingHTTPServer(ThreadingMixIn, HTTPServer):
52
+ """多线程 HTTP 服务器,避免图片代理阻塞其他请求。"""
53
+ daemon_threads = True # 线程随主进程退出
20
54
 
21
55
 
22
56
  class FavHandler(SimpleHTTPRequestHandler):
@@ -42,7 +76,12 @@ class FavHandler(SimpleHTTPRequestHandler):
42
76
 
43
77
  def do_GET(self):
44
78
  try:
45
- if self.path.startswith('/api/fav/list'):
79
+ if not self._is_local_origin():
80
+ self.send_error(403, 'Cross-origin request denied')
81
+ return
82
+ if self.path == '/api/status':
83
+ self._send_json({'ok': True, 'service': 'weflow-daily-reader', 'date': self.date_str})
84
+ elif self.path.startswith('/api/fav/list'):
46
85
  self._handle_fav_list()
47
86
  elif self.path == '/api/read/list':
48
87
  self._handle_read_list()
@@ -60,6 +99,13 @@ class FavHandler(SimpleHTTPRequestHandler):
60
99
 
61
100
  def do_POST(self):
62
101
  try:
102
+ content_length = int(self.headers.get('Content-Length', 0))
103
+ if content_length < 0 or content_length > MAX_BODY_BYTES:
104
+ self.send_error(413, 'Request body too large')
105
+ return
106
+ if not self._is_local_origin():
107
+ self.send_error(403, 'Cross-origin request denied')
108
+ return
63
109
  if self.path == '/api/fav/toggle':
64
110
  self._handle_fav_toggle()
65
111
  elif self.path == '/api/read/toggle':
@@ -86,31 +132,73 @@ class FavHandler(SimpleHTTPRequestHandler):
86
132
  pass
87
133
 
88
134
  def _handle_proxy(self):
89
- """代理微信CDN图片,绕过防盗链Referer检查。"""
90
- from urllib.parse import unquote
135
+ """代理微信CDN图片,绕过防盗链Referer检查(带本地缓存)。"""
91
136
  qs = self.path.split('?', 1)[1] if '?' in self.path else ''
92
137
  url = ''
93
138
  for p in qs.split('&'):
94
139
  if p.startswith('url='):
95
140
  url = unquote(p[4:])
96
141
  break
97
- if not url or not url.startswith(('http://', 'https://')):
142
+ if not url or not is_safe_remote_url(url):
98
143
  self.send_error(400, 'Missing url')
99
144
  return
145
+
146
+ # 本地缓存:URL 的 MD5 作为文件名
147
+ cache_dir = os.path.join(self.date_dir, '.img_cache')
148
+ os.makedirs(cache_dir, exist_ok=True)
149
+ cache_key = hashlib.md5(url.encode()).hexdigest()
150
+ cache_file = os.path.join(cache_dir, cache_key)
151
+
152
+ # 尝试从缓存读取
153
+ if os.path.isfile(cache_file):
154
+ try:
155
+ with open(cache_file, 'rb') as f:
156
+ data = f.read()
157
+ ext = os.path.splitext(urlparse(url).path)[1].lower()
158
+ mime_map = {'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
159
+ '.gif': 'image/gif', '.webp': 'image/webp', '.svg': 'image/svg+xml'}
160
+ content_type = mime_map.get(ext, 'image/jpeg')
161
+ self.send_response(200)
162
+ self.send_header('Content-Type', content_type)
163
+ self.send_header('Content-Length', len(data))
164
+ self.send_header('Cache-Control', 'public, max-age=86400')
165
+ self.send_header('X-Cache', 'HIT')
166
+ self.end_headers()
167
+ self.wfile.write(data)
168
+ return
169
+ except:
170
+ pass # cache miss, re-fetch
171
+
100
172
  try:
101
173
  req = urllib.request.Request(url, headers={
102
174
  'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
103
175
  'Referer': 'https://mp.weixin.qq.com/',
104
176
  'Accept': 'image/webp,image/apng,image/*,*/*;q=0.8',
105
177
  })
106
- with urllib.request.urlopen(req, timeout=15) as resp:
178
+ with REMOTE_OPENER.open(req, timeout=8) as resp:
179
+ if resp.headers.get('Content-Length') and int(resp.headers['Content-Length']) > 10 * 1024 * 1024:
180
+ self.send_error(413, 'Image too large')
181
+ return
107
182
  data = resp.read(10 * 1024 * 1024) # max 10MB
108
183
  content_type = resp.headers.get('Content-Type', 'image/jpeg')
184
+
185
+ # 写入缓存
186
+ try:
187
+ tmp = cache_file + '.tmp'
188
+ with open(tmp, 'wb') as f:
189
+ f.write(data)
190
+ shutil.move(tmp, cache_file)
191
+ except:
192
+ pass
193
+
109
194
  self.send_response(200)
110
195
  self.send_header('Content-Type', content_type)
111
196
  self.send_header('Content-Length', len(data))
112
197
  self.send_header('Cache-Control', 'public, max-age=86400')
113
- self.send_header('Access-Control-Allow-Origin', '*')
198
+ self.send_header('X-Cache', 'MISS')
199
+ self.send_header('Cache-Control', 'public, max-age=86400')
200
+ if self._is_local_origin():
201
+ self.send_header('Access-Control-Allow-Origin', self.headers.get('Origin', f'http://localhost:{self.server.server_port}'))
114
202
  self.end_headers()
115
203
  self.wfile.write(data)
116
204
  except Exception as e:
@@ -167,8 +255,13 @@ class FavHandler(SimpleHTTPRequestHandler):
167
255
 
168
256
  desired_names = set()
169
257
  for rel_path in favs:
170
- src = Path(self.date_dir) / rel_path
171
- if src.exists():
258
+ src = (Path(self.date_dir) / str(rel_path)).resolve()
259
+ date_root = Path(self.date_dir).resolve()
260
+ try:
261
+ src.relative_to(date_root)
262
+ except ValueError:
263
+ continue
264
+ if src.is_file():
172
265
  desired_names.add(src.name)
173
266
  link = fav_dir / src.name
174
267
  if not link.exists():
@@ -337,13 +430,27 @@ class FavHandler(SimpleHTTPRequestHandler):
337
430
  self.send_response(200)
338
431
  self.send_header('Content-Type', 'application/json; charset=utf-8')
339
432
  self.send_header('Content-Length', len(body))
340
- self.send_header('Access-Control-Allow-Origin', '*')
433
+ if self._is_local_origin():
434
+ self.send_header('Access-Control-Allow-Origin', self.headers.get('Origin', f'http://localhost:{self.server.server_port}'))
341
435
  self.end_headers()
342
436
  self.wfile.write(body)
343
437
 
438
+ def _is_local_origin(self):
439
+ origin = self.headers.get('Origin')
440
+ if not origin:
441
+ return True
442
+ expected = {
443
+ f'http://localhost:{self.server.server_port}',
444
+ f'http://127.0.0.1:{self.server.server_port}',
445
+ }
446
+ return origin in expected
447
+
344
448
  def do_OPTIONS(self):
449
+ if not self._is_local_origin():
450
+ self.send_error(403, 'Cross-origin request denied')
451
+ return
345
452
  self.send_response(200)
346
- self.send_header('Access-Control-Allow-Origin', '*')
453
+ self.send_header('Access-Control-Allow-Origin', self.headers.get('Origin', f'http://localhost:{self.server.server_port}'))
347
454
  self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
348
455
  self.send_header('Access-Control-Allow-Headers', 'Content-Type')
349
456
  self.end_headers()
@@ -374,7 +481,7 @@ def main():
374
481
  # 切换到日报目录以提供静态文件服务
375
482
  os.chdir(date_dir)
376
483
 
377
- server = HTTPServer(('0.0.0.0', args.port), FavHandler)
484
+ server = ThreadingHTTPServer(('127.0.0.1', args.port), FavHandler)
378
485
  print(f'⭐ 收藏服务器已启动')
379
486
  print(f' 打开: http://localhost:{args.port}')
380
487
  print(f' 日期: {args.date}')
@@ -1,111 +1,111 @@
1
- #!/usr/bin/env python3
2
- """修复已生成文章的主题分类(关键词 fallback)。用法: python scripts/fix_topics.py 2026-05-19"""
3
- import sys, os, re
4
- from pathlib import Path
5
-
6
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
7
- from biz_daily import _guess_topic
8
-
9
- TOPICS = ['AI', '学术', '新闻', '文学', '投资']
10
-
11
- def fix_date(date_str: str):
12
- sys.stdout.reconfigure(encoding='utf-8', errors='replace')
13
- base = Path('output/biz-daily') / date_str
14
- if not base.is_dir():
15
- print(f'[ERROR] 目录不存在: {base}')
16
- sys.exit(1)
17
-
18
- fixed = 0
19
- for topic in TOPICS:
20
- topic_dir = base / topic
21
- if not topic_dir.is_dir():
22
- continue
23
- for md_file in topic_dir.glob('*.md'):
24
- try:
25
- content = md_file.read_text(encoding='utf-8')
26
- except:
27
- continue
28
-
29
- # Extract current topic from frontmatter
30
- m = re.search(r'^topic:\s*(.+)', content, re.MULTILINE)
31
- old_topic = m.group(1).strip() if m else ''
32
-
33
- # Extract title
34
- title = md_file.stem
35
- m2 = re.search(r'^title:\s*"?(.+?)"?\s*$', content, re.MULTILINE)
36
- if m2:
37
- title = m2.group(1).strip().strip('"').strip("'")
38
-
39
- # Guess topic
40
- article = {'title': title, 'account_name': ''}
41
- m3 = re.search(r'^source:\s*"?(.+?)"?\s*$', content, re.MULTILINE)
42
- if m3:
43
- article['account_name'] = m3.group(1).strip().strip('"').strip("'")
44
-
45
- guessed = _guess_topic(article)
46
-
47
- if guessed != old_topic:
48
- # Update frontmatter
49
- new_content = re.sub(
50
- r'^topic:\s*.+', f'topic: {guessed}',
51
- content, flags=re.MULTILINE
52
- )
53
- # Update topic folder if needed - move file
54
- if guessed != topic:
55
- new_dir = base / guessed
56
- new_dir.mkdir(exist_ok=True)
57
- new_path = new_dir / md_file.name
58
- md_file.rename(new_path)
59
- # Also update the file at new location
60
- new_path.write_text(new_content, encoding='utf-8')
61
- print(f' [FIX] {title[:50]} -> {guessed} (was {topic})')
62
- else:
63
- md_file.write_text(new_content, encoding='utf-8')
64
- fixed += 1
65
-
66
- print(f'\n✓ 修复 {fixed} 篇,未变动 {sum(1 for _ in base.rglob("*.md") if _.name != "README.md") - fixed} 篇')
67
-
68
- # Rebuild README
69
- from biz_daily import sanitize_filename
70
- all_articles = []
71
- for topic in TOPICS:
72
- topic_dir = base / topic
73
- if not topic_dir.is_dir():
74
- continue
75
- for md_file in sorted(topic_dir.glob('*.md')):
76
- content = md_file.read_text(encoding='utf-8')
77
- title = md_file.stem
78
- source = ''
79
- article_time = ''
80
- for line in content.split('\n'):
81
- if line.startswith('title:'):
82
- title = line.split(':', 1)[1].strip().strip('"').strip("'")
83
- elif line.startswith('source:'):
84
- source = line.split(':', 1)[1].strip().strip('"').strip("'")
85
- mm = re.search(r'时间:\d{4}-\d{2}-\d{2} (\d{2}:\d{2})', content)
86
- if mm: article_time = mm.group(1)
87
- all_articles.append({'title': title, 'source': source, 'time': article_time, 'topic': topic, 'file': md_file.name})
88
-
89
- index_lines = [f'# 公众号日报 — {date_str}', '', f'共 {len(all_articles)} 篇推送,按主题分类', '']
90
- for topic in TOPICS:
91
- group = [a for a in all_articles if a['topic'] == topic]
92
- if not group: continue
93
- index_lines.append(f'## {topic} ({len(group)}篇)')
94
- index_lines.append('')
95
- index_lines.append('| # | 时间 | 公众号 | 标题 |')
96
- index_lines.append('|---|------|--------|------|')
97
- for j, a in enumerate(group):
98
- index_lines.append(f'| {j+1} | {a["time"]} | {a["source"]} | [{a["title"]}](./{topic}/{a["file"]}) |')
99
- index_lines.append('')
100
-
101
- with open(base / 'README.md', 'w', encoding='utf-8') as f:
102
- f.write('\n'.join(index_lines))
103
-
104
- print(f' README 已重建')
105
-
106
-
107
- if __name__ == '__main__':
108
- if len(sys.argv) < 2:
109
- print('用法: python scripts/fix_topics.py 2026-05-19')
110
- sys.exit(1)
111
- fix_date(sys.argv[1])
1
+ #!/usr/bin/env python3
2
+ """修复已生成文章的主题分类(关键词 fallback)。用法: python scripts/fix_topics.py 2026-05-19"""
3
+ import sys, os, re
4
+ from pathlib import Path
5
+
6
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
7
+ from biz_daily import _guess_topic
8
+
9
+ TOPICS = ['AI', '学术', '新闻', '文学', '投资', '政治']
10
+
11
+ def fix_date(date_str: str):
12
+ sys.stdout.reconfigure(encoding='utf-8', errors='replace')
13
+ base = Path('output/biz-daily') / date_str
14
+ if not base.is_dir():
15
+ print(f'[ERROR] 目录不存在: {base}')
16
+ sys.exit(1)
17
+
18
+ fixed = 0
19
+ for topic in TOPICS:
20
+ topic_dir = base / topic
21
+ if not topic_dir.is_dir():
22
+ continue
23
+ for md_file in topic_dir.glob('*.md'):
24
+ try:
25
+ content = md_file.read_text(encoding='utf-8')
26
+ except:
27
+ continue
28
+
29
+ # Extract current topic from frontmatter
30
+ m = re.search(r'^topic:\s*(.+)', content, re.MULTILINE)
31
+ old_topic = m.group(1).strip() if m else ''
32
+
33
+ # Extract title
34
+ title = md_file.stem
35
+ m2 = re.search(r'^title:\s*"?(.+?)"?\s*$', content, re.MULTILINE)
36
+ if m2:
37
+ title = m2.group(1).strip().strip('"').strip("'")
38
+
39
+ # Guess topic
40
+ article = {'title': title, 'account_name': ''}
41
+ m3 = re.search(r'^source:\s*"?(.+?)"?\s*$', content, re.MULTILINE)
42
+ if m3:
43
+ article['account_name'] = m3.group(1).strip().strip('"').strip("'")
44
+
45
+ guessed = _guess_topic(article)
46
+
47
+ if guessed != old_topic:
48
+ # Update frontmatter
49
+ new_content = re.sub(
50
+ r'^topic:\s*.+', f'topic: {guessed}',
51
+ content, flags=re.MULTILINE
52
+ )
53
+ # Update topic folder if needed - move file
54
+ if guessed != topic:
55
+ new_dir = base / guessed
56
+ new_dir.mkdir(exist_ok=True)
57
+ new_path = new_dir / md_file.name
58
+ md_file.rename(new_path)
59
+ # Also update the file at new location
60
+ new_path.write_text(new_content, encoding='utf-8')
61
+ print(f' [FIX] {title[:50]} -> {guessed} (was {topic})')
62
+ else:
63
+ md_file.write_text(new_content, encoding='utf-8')
64
+ fixed += 1
65
+
66
+ print(f'\n✓ 修复 {fixed} 篇,未变动 {sum(1 for _ in base.rglob("*.md") if _.name != "README.md") - fixed} 篇')
67
+
68
+ # Rebuild README
69
+ from biz_daily import sanitize_filename
70
+ all_articles = []
71
+ for topic in TOPICS:
72
+ topic_dir = base / topic
73
+ if not topic_dir.is_dir():
74
+ continue
75
+ for md_file in sorted(topic_dir.glob('*.md')):
76
+ content = md_file.read_text(encoding='utf-8')
77
+ title = md_file.stem
78
+ source = ''
79
+ article_time = ''
80
+ for line in content.split('\n'):
81
+ if line.startswith('title:'):
82
+ title = line.split(':', 1)[1].strip().strip('"').strip("'")
83
+ elif line.startswith('source:'):
84
+ source = line.split(':', 1)[1].strip().strip('"').strip("'")
85
+ mm = re.search(r'时间:\d{4}-\d{2}-\d{2} (\d{2}:\d{2})', content)
86
+ if mm: article_time = mm.group(1)
87
+ all_articles.append({'title': title, 'source': source, 'time': article_time, 'topic': topic, 'file': md_file.name})
88
+
89
+ index_lines = [f'# 公众号日报 — {date_str}', '', f'共 {len(all_articles)} 篇推送,按主题分类', '']
90
+ for topic in TOPICS:
91
+ group = [a for a in all_articles if a['topic'] == topic]
92
+ if not group: continue
93
+ index_lines.append(f'## {topic} ({len(group)}篇)')
94
+ index_lines.append('')
95
+ index_lines.append('| # | 时间 | 公众号 | 标题 |')
96
+ index_lines.append('|---|------|--------|------|')
97
+ for j, a in enumerate(group):
98
+ index_lines.append(f'| {j+1} | {a["time"]} | {a["source"]} | [{a["title"]}](./{topic}/{a["file"]}) |')
99
+ index_lines.append('')
100
+
101
+ with open(base / 'README.md', 'w', encoding='utf-8') as f:
102
+ f.write('\n'.join(index_lines))
103
+
104
+ print(f' README 已重建')
105
+
106
+
107
+ if __name__ == '__main__':
108
+ if len(sys.argv) < 2:
109
+ print('用法: python scripts/fix_topics.py 2026-05-19')
110
+ sys.exit(1)
111
+ fix_date(sys.argv[1])
@@ -31,7 +31,7 @@ SOURCE_ROOT = os.path.join(ROOT_DIR, 'output', 'biz-daily')
31
31
  OUTPUT_DIR = os.path.join(ROOT_DIR, 'output', 'ai-reports')
32
32
 
33
33
  TZ = timezone(timedelta(hours=8))
34
- TOPICS = ['AI', '学术', '新闻', '文学', '投资']
34
+ TOPICS = ['AI', '学术', '新闻', '文学', '投资', '政治']
35
35
  FOCUS_TOPIC = 'AI'
36
36
 
37
37