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
@@ -6,12 +6,31 @@
6
6
  python scripts/generate_html.py --date 2026-05-18
7
7
  python scripts/generate_html.py --date 2026-05-18 --output custom.html
8
8
  """
9
- import sys, os, json, re
9
+ import sys, os, json, re, shutil
10
10
  from pathlib import Path
11
11
  from datetime import datetime
12
12
 
13
13
  SCRIPTS_DIR = os.path.dirname(os.path.abspath(__file__))
14
- SOURCE_ROOT = os.path.join(os.path.dirname(SCRIPTS_DIR), 'output', 'biz-daily')
14
+ PROJECT_ROOT = os.path.dirname(SCRIPTS_DIR)
15
+ SOURCE_ROOT = os.path.join(PROJECT_ROOT, 'output', 'biz-daily')
16
+
17
+
18
+ def _inline_marked_js(html_path: str):
19
+ """Replace CDN marked.js with local inline copy to eliminate network dependency."""
20
+ marked_js_path = os.path.join(PROJECT_ROOT, 'resources', 'js', 'marked.min.js')
21
+ if not os.path.exists(marked_js_path):
22
+ print(f' [WARN] marked.js not found: {marked_js_path}')
23
+ return
24
+ with open(marked_js_path, 'r', encoding='utf-8') as f:
25
+ marked_js = f.read()
26
+ with open(html_path, 'r', encoding='utf-8') as f:
27
+ html = f.read()
28
+ cdn_tag = '<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>'
29
+ inline_tag = '<script>' + marked_js + '</script>'
30
+ if cdn_tag in html:
31
+ html = html.replace(cdn_tag, inline_tag, 1)
32
+ with open(html_path, 'w', encoding='utf-8') as f:
33
+ f.write(html)
15
34
 
16
35
  TOPIC_LABELS = {
17
36
  'AI': ('AI', '#8b5cf6', '🤖'),
@@ -19,16 +38,17 @@ TOPIC_LABELS = {
19
38
  '新闻': ('新闻', '#f59e0b', '📰'),
20
39
  '文学': ('文学', '#ec4899', '📝'),
21
40
  '投资': ('投资', '#10b981', '💰'),
41
+ '政治': ('政治', '#0f766e', '🏛️'),
22
42
  }
23
43
 
24
- TOPIC_ORDER = ['AI', '学术', '新闻', '文学', '投资']
44
+ TOPIC_ORDER = ['AI', '学术', '新闻', '文学', '投资', '政治']
25
45
 
26
46
 
27
47
  def parse_frontmatter(text: str) -> dict:
28
48
  """解析 YAML frontmatter(简易版,兼容现有格式)。"""
29
49
  if not text.startswith('---'):
30
50
  return {}
31
- end = text.find('---', 3)
51
+ end = text.find('\n---', 3)
32
52
  if end == -1:
33
53
  return {}
34
54
  fm_text = text[3:end].strip()
@@ -48,8 +68,8 @@ def parse_frontmatter(text: str) -> dict:
48
68
  def get_body_preview(text: str, max_chars=200) -> str:
49
69
  """提取正文预览(跳过 frontmatter 和标题行)。"""
50
70
  if text.startswith('---'):
51
- end = text.find('---', 3)
52
- text = text[end + 3:] if end != -1 else text
71
+ end = text.find('\n---', 3)
72
+ text = text[end + 4:] if end != -1 else text
53
73
  lines = text.strip().split('\n')
54
74
  body_lines = []
55
75
  for line in lines:
@@ -106,6 +126,209 @@ def escape_html(text: str) -> str:
106
126
  return text.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('"', '&quot;')
107
127
 
108
128
 
129
+ def simple_markdown_to_html(md_text: str) -> str:
130
+ """轻量 Markdown 转 HTML:支持标题、加粗、列表、引用、复选框、链接、分隔线。"""
131
+ import re
132
+ lines = md_text.split('\n')
133
+ html_parts = []
134
+ in_list = False
135
+ in_ul = False
136
+ in_blockquote = False
137
+
138
+ def close_lists():
139
+ nonlocal in_list, in_ul
140
+ if in_list:
141
+ html_parts.append('</ol>')
142
+ in_list = False
143
+ if in_ul:
144
+ html_parts.append('</ul>')
145
+ in_ul = False
146
+
147
+ def close_blockquote():
148
+ nonlocal in_blockquote
149
+ if in_blockquote:
150
+ html_parts.append('</blockquote>')
151
+ in_blockquote = False
152
+
153
+ for line in lines:
154
+ stripped = line.strip()
155
+
156
+ # 空行
157
+ if not stripped:
158
+ close_lists()
159
+ close_blockquote()
160
+ continue
161
+
162
+ # 分隔线
163
+ if re.match(r'^---+$', stripped):
164
+ close_lists()
165
+ close_blockquote()
166
+ html_parts.append('<hr>')
167
+ continue
168
+
169
+ # 标题
170
+ h_match = re.match(r'^(#{1,4})\s+(.+)$', stripped)
171
+ if h_match:
172
+ close_lists()
173
+ close_blockquote()
174
+ level = len(h_match.group(1))
175
+ text = h_match.group(2)
176
+ text = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', text)
177
+ html_parts.append(f'<h{level}>{text}</h{level}>')
178
+ continue
179
+
180
+ # 引用
181
+ if stripped.startswith('> '):
182
+ close_lists()
183
+ if not in_blockquote:
184
+ html_parts.append('<blockquote>')
185
+ in_blockquote = True
186
+ text = stripped[2:]
187
+ text = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', text)
188
+ text = re.sub(r'\[(.+?)\]\((.+?)\)', r'<a href="\2">\1</a>', text)
189
+ html_parts.append(f'<p>{text}</p>')
190
+ continue
191
+ else:
192
+ close_blockquote()
193
+
194
+ # 有序列表
195
+ ol_match = re.match(r'^(\d+)\.\s+(.+)$', stripped)
196
+ if ol_match:
197
+ close_blockquote()
198
+ if in_ul:
199
+ html_parts.append('</ul>')
200
+ in_ul = False
201
+ if not in_list:
202
+ html_parts.append('<ol>')
203
+ in_list = True
204
+ text = ol_match.group(2)
205
+ text = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', text)
206
+ text = re.sub(r'\[(.+?)\]\((.+?)\)', r'<a href="\2">\1</a>', text)
207
+ # 复选框
208
+ text = re.sub(r'^\[ \]\s*', '<input type="checkbox" disabled> ', text)
209
+ text = re.sub(r'^\[x\]\s*', '<input type="checkbox" checked disabled> ', text, flags=re.IGNORECASE)
210
+ html_parts.append(f'<li>{text}</li>')
211
+ continue
212
+
213
+ # 无序列表
214
+ ul_match = re.match(r'^[-*•]\s+(.+)$', stripped)
215
+ if ul_match:
216
+ close_blockquote()
217
+ if in_list:
218
+ html_parts.append('</ol>')
219
+ in_list = False
220
+ if not in_ul:
221
+ html_parts.append('<ul>')
222
+ in_ul = True
223
+ text = ul_match.group(1)
224
+ text = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', text)
225
+ text = re.sub(r'\[(.+?)\]\((.+?)\)', r'<a href="\2">\1</a>', text)
226
+ text = re.sub(r'^\[ \]\s*', '<input type="checkbox" disabled> ', text)
227
+ text = re.sub(r'^\[x\]\s*', '<input type="checkbox" checked disabled> ', text, flags=re.IGNORECASE)
228
+ html_parts.append(f'<li>{text}</li>')
229
+ continue
230
+
231
+ # 普通段落
232
+ close_lists()
233
+ close_blockquote()
234
+ text = stripped
235
+ text = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', text)
236
+ text = re.sub(r'\[(.+?)\]\((.+?)\)', r'<a href="\2">\1</a>', text)
237
+ html_parts.append(f'<p>{text}</p>')
238
+
239
+ close_lists()
240
+ close_blockquote()
241
+ return '\n'.join(html_parts)
242
+
243
+
244
+ def generate_action_html(date_str: str, md_path: str, out_path: str):
245
+ """生成行动建议 HTML 页面,风格与 index.html 一致。"""
246
+ with open(md_path, 'r', encoding='utf-8') as f:
247
+ md_content = f.read()
248
+ content_html = simple_markdown_to_html(md_content)
249
+
250
+ html = f'''<!DOCTYPE html>
251
+ <html lang="zh-CN">
252
+ <head>
253
+ <meta charset="UTF-8">
254
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
255
+ <title>行动建议 — {date_str}</title>
256
+ <style>
257
+ * {{ margin: 0; padding: 0; box-sizing: border-box; }}
258
+ body {{
259
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;
260
+ background: var(--bg); color: var(--text); line-height: 1.75;
261
+ min-height: 100vh; -webkit-font-smoothing: antialiased;
262
+ }}
263
+ .page {{ max-width: 780px; margin: 0 auto; padding: 32px 24px 60px; }}
264
+
265
+ /* ---- Header ---- */
266
+ .header {{ text-align: center; margin-bottom: 36px; padding-top: 12px; position: relative; }}
267
+ .header::after {{
268
+ content: ''; position: absolute; bottom: -12px; left: 50%; transform: translateX(-50%);
269
+ width: 50px; height: 2px; background: linear-gradient(90deg, #8b6914, #c8963e); border-radius: 2px;
270
+ }}
271
+ .header h1 {{
272
+ font-size: 30px; font-weight: 800; color: var(--text-title);
273
+ display: inline-flex; align-items: center; gap: 10px;
274
+ }}
275
+ .back-link {{
276
+ display: inline-block; margin-top: 16px;
277
+ font-size: 18px; color: var(--text-muted); text-decoration: none;
278
+ padding: 7px 18px; border-radius: 20px;
279
+ background: var(--bg-soft); border: 1px solid #d4c5a060;
280
+ transition: all .25s;
281
+ }}
282
+ .back-link:hover {{ color: var(--accent); border-color: #c8963e50; background: var(--bg-soft); }}
283
+
284
+ /* ---- Content ---- */
285
+ .content {{
286
+ background: var(--bg-card); border-radius: 14px; padding: 32px 36px;
287
+ border: 1px solid #d4c5a0; margin-top: 8px;
288
+ }}
289
+ .content h1 {{ font-size: 26px; font-weight: 700; color: var(--text-title); margin: 28px 0 14px; padding-bottom: 8px; border-bottom: 1px solid #d4c5a0; }}
290
+ .content h1:first-child {{ margin-top: 0; }}
291
+ .content h2 {{ font-size: 23px; font-weight: 700; color: #5a4a2a; margin: 24px 0 12px; }}
292
+ .content h3 {{ font-size: 21px; font-weight: 600; color: #6b5535; margin: 20px 0 10px; }}
293
+ .content h4 {{ font-size: 19px; font-weight: 600; color: #7a6545; margin: 16px 0 8px; }}
294
+ .content p {{ margin: 10px 0; font-size: 19px; color: #4a4035; }}
295
+ .content strong {{ color: var(--text-title); font-weight: 700; }}
296
+ .content a {{ color: var(--accent); text-decoration: none; border-bottom: 1px solid #c8963e40; transition: all .2s; }}
297
+ .content a:hover {{ border-bottom-color: #c8963e; }}
298
+ .content ul, .content ol {{ margin: 12px 0; padding-left: 28px; }}
299
+ .content li {{ margin: 8px 0; font-size: 19px; color: #4a4035; }}
300
+ .content li input[type="checkbox"] {{ margin-right: 8px; accent-color: var(--accent); }}
301
+ .content blockquote {{
302
+ border-left: 3px solid #c8963e; padding: 10px 18px; margin: 16px 0;
303
+ background: var(--bg-soft); border-radius: 0 10px 10px 0; color: #6b5535;
304
+ }}
305
+ .content blockquote p {{ margin: 4px 0; color: #6b5535; }}
306
+ .content hr {{ border: none; border-top: 1px solid #d4c5a0; margin: 28px 0; }}
307
+ .content code {{ background: #f0e8d5; padding: 2px 7px; border-radius: 5px; font-size: 18px; color: var(--accent); }}
308
+
309
+ @media (max-width: 640px) {{
310
+ .page {{ padding: 20px 14px 40px; }}
311
+ .content {{ padding: 20px 18px; border-radius: 10px; }}
312
+ .header h1 {{ font-size: 26px; }}
313
+ }}
314
+ </style>
315
+ </head>
316
+ <body>
317
+ <div class="page">
318
+ <div class="header">
319
+ <h1>📋 行动建议 — {date_str}</h1>
320
+ <div><a href="./index.html" class="back-link">← 返回日报</a></div>
321
+ </div>
322
+ <div class="content">
323
+ {content_html}
324
+ </div>
325
+ </div>
326
+ </body>
327
+ </html>'''
328
+ with open(out_path, 'w', encoding='utf-8') as f:
329
+ f.write(html)
330
+
331
+
109
332
  def generate_article_viewer(out_path: str):
110
333
  """生成 article.html — Markdown 渲染阅读器。"""
111
334
  html = r'''<!DOCTYPE html>
@@ -147,100 +370,95 @@ function md5(string) {
147
370
  * { margin: 0; padding: 0; box-sizing: border-box; }
148
371
  body {
149
372
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;
150
- background: #0f172a;
151
- color: #e2e8f0;
373
+ background: var(--bg);
374
+ color: var(--text);
152
375
  min-height: 100vh;
153
376
  line-height: 1.8;
377
+ -webkit-font-smoothing: antialiased;
154
378
  }
155
- .container { max-width: 960px; margin: 0 auto; padding: 24px; }
379
+ .container { max-width: 1440px; margin: 0 auto; padding: 24px 32px; display: flex; gap: 28px; }
380
+ .article-col { flex: 1; min-width: 0; }
156
381
  .top-bar {
157
382
  display: flex; align-items: center; gap: 12px; padding: 12px 0;
158
- margin-bottom: 24px; border-bottom: 1px solid #1e293b;
159
- position: sticky; top: 0; background: #0f172a; z-index: 10; flex-wrap: wrap;
383
+ margin-bottom: 24px; border-bottom: 1px solid #d4c5a0;
384
+ position: sticky; top: 0; background: var(--bg); z-index: 10; flex-wrap: wrap;
160
385
  }
161
386
  .btn-back {
162
- color: #94a3b8; text-decoration: none; font-size: 14px;
163
- padding: 6px 14px; border: 1px solid #334155; border-radius: 8px;
164
- transition: all .2s; white-space: nowrap;
387
+ color: var(--text-muted); text-decoration: none; font-size: 18px;
388
+ padding: 6px 14px; border: 1px solid #d4c5a0; border-radius: 8px;
389
+ transition: all .2s; white-space: nowrap; background: var(--bg-soft);
165
390
  }
166
- .btn-back:hover { color: #e2e8f0; border-color: #475569; }
167
- .article-source { color: #818cf8; font-size: 13px; font-weight: 600; }
168
- .article-meta { color: #64748b; font-size: 12px; margin-left: auto; }
169
- .mode-badge { font-size: 11px; padding: 2px 10px; border-radius: 10px; font-weight: 600; }
170
- .mode-badge.html { background: #10b98120; color: #34d399; border: 1px solid #10b98140; }
171
- .view-switch { display: inline-flex; background: #1e293b; border-radius: 8px; overflow: hidden; border: 1px solid #334155; }
172
- .view-switch a { padding: 4px 12px; font-size: 12px; color: #64748b; text-decoration: none; transition: all .2s; }
173
- .view-switch a.active { background: #334155; color: #e2e8f0; }
174
- .view-switch a:hover:not(.active) { color: #94a3b8; }
175
- .article-header { margin-bottom: 32px; padding-bottom: 20px; border-bottom: 1px solid #1e293b; }
176
- .article-header h1 { font-size: 26px; font-weight: 700; line-height: 1.4; margin-bottom: 12px; color: #f1f5f9; }
177
- .article-header .meta-row { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; font-size: 13px; color: #64748b; }
178
- .article-header .source-tag { background: #312e81; color: #a5b4fc; padding: 2px 10px; border-radius: 6px; font-size: 12px; }
391
+ .btn-back:hover { color: var(--text-title); border-color: #c8963e; background: var(--bg-card); }
392
+ .article-source { color: var(--accent); font-size: 17px; font-weight: 600; margin-left: auto; background: #c8963e10; padding: 2px 10px; border-radius: 6px; }
393
+ .article-header { margin-bottom: 32px; padding-bottom: 20px; border-bottom: 1px solid #d4c5a0; }
394
+ .article-header h1 { font-size: 30px; font-weight: 700; line-height: 1.4; margin-bottom: 12px; color: var(--text-title); }
395
+ .article-header .meta-row { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; font-size: 17px; color: #96876a; }
396
+ .article-header .source-tag { background: #c8963e15; color: var(--accent); padding: 2px 10px; border-radius: 6px; font-size: 16px; }
179
397
  .tags-row { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 10px; }
180
- .tag { font-size: 11px; padding: 2px 8px; background: #334155; border-radius: 6px; color: #94a3b8; }
181
- .article-body { font-size: 16px; color: #cbd5e1; }
182
- .article-body h2 { font-size: 22px; font-weight: 700; margin: 32px 0 16px; color: #f1f5f9; border-bottom: 1px solid #1e293b; padding-bottom: 8px; }
183
- .article-body h3 { font-size: 18px; font-weight: 600; margin: 24px 0 12px; color: #e2e8f0; }
184
- .article-body h4 { font-size: 16px; font-weight: 600; margin: 20px 0 8px; color: #cbd5e1; }
398
+ .tag { font-size: 15px; padding: 2px 8px; background: #f0e8d5; border-radius: 6px; color: var(--text-muted); }
399
+ .article-body { font-size: 20px; color: #4a4035; }
400
+ .article-body h2 { font-size: 26px; font-weight: 700; margin: 32px 0 16px; color: var(--text-title); border-bottom: 1px solid #d4c5a0; padding-bottom: 8px; }
401
+ .article-body h3 { font-size: 22px; font-weight: 600; margin: 24px 0 12px; color: #5a4a2a; }
402
+ .article-body h4 { font-size: 20px; font-weight: 600; margin: 20px 0 8px; color: #6b5535; }
185
403
  .article-body p { margin: 12px 0; }
186
- .article-body strong { color: #f1f5f9; font-weight: 700; }
187
- .article-body a { color: #818cf8; text-decoration: none; }
188
- .article-body a:hover { text-decoration: underline; }
189
- .article-body img { max-width: 100%; height: auto; border-radius: 8px; margin: 16px 0; border: 1px solid #334155; }
190
- .article-body blockquote { border-left: 3px solid #818cf8; padding: 8px 16px; margin: 16px 0; background: #1a1040; border-radius: 0 8px 8px 0; color: #a5b4fc; }
404
+ .article-body strong { color: var(--text-title); font-weight: 700; }
405
+ .article-body a { color: var(--accent); text-decoration: none; border-bottom: 1px solid #c8963e40; }
406
+ .article-body a:hover { border-bottom-color: #c8963e; }
407
+ .article-body img { max-width: 100%; height: auto; border-radius: 8px; margin: 16px 0; border: 1px solid #e0d5be; }
408
+ .article-body blockquote { border-left: 3px solid #c8963e; padding: 8px 16px; margin: 16px 0; background: var(--bg-soft); border-radius: 0 8px 8px 0; color: #6b5535; }
191
409
  .article-body blockquote p { margin: 4px 0; }
192
- .article-body code { background: #1e293b; padding: 2px 6px; border-radius: 4px; font-size: 14px; color: #fbbf24; }
193
- .article-body pre { background: #0c1222; border: 1px solid #1e293b; border-radius: 10px; padding: 16px; overflow-x: auto; margin: 16px 0; font-size: 13px; line-height: 1.7; }
194
- .article-body pre code { background: none; padding: 0; color: #e2e8f0; }
410
+ .article-body code { background: #f0e8d5; padding: 2px 6px; border-radius: 4px; font-size: 18px; color: var(--accent); }
411
+ .article-body pre { background: var(--bg-soft); border: 1px solid #d4c5a0; border-radius: 10px; padding: 16px; overflow-x: auto; margin: 16px 0; font-size: 17px; line-height: 1.7; }
412
+ .article-body pre code { background: none; padding: 0; color: #4a4035; }
195
413
  .article-body ul, .article-body ol { margin: 12px 0; padding-left: 24px; }
196
- .article-body li { margin: 6px 0; color: #cbd5e1; }
197
- .article-body hr { border: none; border-top: 1px solid #1e293b; margin: 24px 0; }
198
- .article-body table { width: 100%; border-collapse: collapse; margin: 16px 0; font-size: 14px; }
199
- .article-body th { background: #1e293b; padding: 10px 14px; text-align: left; color: #e2e8f0; font-weight: 600; border-bottom: 2px solid #334155; }
200
- .article-body td { padding: 8px 14px; border-bottom: 1px solid #1e293b; color: #94a3b8; }
201
- .loading { text-align: center; padding: 80px 20px; color: #64748b; font-size: 16px; }
202
- .error-box { background: #dc262610; border: 1px solid #dc262630; border-radius: 12px; padding: 20px; text-align: center; color: #f87171; margin: 40px 0; }
203
- @media (max-width: 640px) {
204
- .container { padding: 12px; }
205
- .article-header h1 { font-size: 20px; }
206
- .article-body { font-size: 15px; }
414
+ .article-body li { margin: 6px 0; color: #4a4035; }
415
+ .article-body hr { border: none; border-top: 1px solid #d4c5a0; margin: 24px 0; }
416
+ .article-body table { width: 100%; border-collapse: collapse; margin: 16px 0; font-size: 18px; }
417
+ .article-body th { background: #f0e8d5; padding: 10px 14px; text-align: left; color: var(--text-title); font-weight: 600; border-bottom: 2px solid #d4c5a0; }
418
+ .article-body td { padding: 8px 14px; border-bottom: 1px solid #e0d5be; color: #5c4a32; }
419
+ .loading { text-align: center; padding: 80px 20px; color: #96876a; font-size: 20px; }
420
+ .error-box { background: #dc5b5b10; border: 1px solid #dc5b5b30; border-radius: 12px; padding: 20px; text-align: center; color: #b04444; margin: 40px 0; }
421
+ @media (max-width: 768px) {
422
+ .container { padding: 16px 14px; flex-direction: column; }
423
+ .article-header h1 { font-size: 24px; }
424
+ .article-body { font-size: 19px; }
207
425
  }
208
426
  /* ---- Highlight & Notes ---- */
209
- .article-body mark.hl { background: #fef08a; color: #1e1b4b; padding: 1px 0; border-radius: 2px; cursor: pointer; transition: all .2s; }
427
+ .article-body mark.hl { background: #fef08a; color: var(--text-title); padding: 1px 0; border-radius: 2px; cursor: pointer; transition: all .2s; }
210
428
  .article-body mark.hl:hover { background: #fde047; }
211
429
  .article-body mark.hl.active { background: #fbbf24; box-shadow: 0 0 0 2px #fbbf2440; }
212
- .selection-popup { position: absolute; z-index: 1000; background: #1e293b; border: 1px solid #334155; border-radius: 10px; padding: 6px 10px; display: none; box-shadow: 0 8px 24px #00000040; }
213
- .selection-popup button { background: #818cf8; color: #fff; border: none; padding: 6px 14px; border-radius: 7px; cursor: pointer; font-size: 13px; font-weight: 600; white-space: nowrap; }
214
- .selection-popup button:hover { background: #6366f1; }
215
- .note-modal-overlay { position: fixed; inset: 0; background: #00000060; z-index: 2000; display: flex; align-items: center; justify-content: center; }
216
- .note-modal { background: #1e293b; border: 1px solid #334155; border-radius: 14px; padding: 24px; width: 90%; max-width: 480px; box-shadow: 0 16px 48px #00000060; }
217
- .note-modal h3 { color: #e2e8f0; font-size: 16px; margin-bottom: 12px; }
218
- .note-modal .hl-text { background: #1a1040; border: 1px solid #312e81; border-radius: 8px; padding: 10px 14px; color: #a5b4fc; font-size: 13px; margin-bottom: 14px; max-height: 100px; overflow-y: auto; line-height: 1.6; }
219
- .note-modal textarea { width: 100%; height: 80px; background: #0f172a; border: 1px solid #334155; border-radius: 8px; color: #e2e8f0; padding: 10px 14px; font-size: 13px; resize: vertical; font-family: inherit; outline: none; }
220
- .note-modal textarea:focus { border-color: #818cf8; }
430
+ .selection-popup { position: absolute; z-index: 1000; background: var(--bg-card); border: 1px solid #c8963e; border-radius: 10px; padding: 6px 10px; display: none; box-shadow: 0 8px 24px #3c3a3820; }
431
+ .selection-popup button { background: #8b6914; color: #fff; border: none; padding: 6px 14px; border-radius: 7px; cursor: pointer; font-size: 17px; font-weight: 600; white-space: nowrap; }
432
+ .selection-popup button:hover { background: #6b4c1e; }
433
+ .note-modal-overlay { position: fixed; inset: 0; background: #3c3a3840; z-index: 2000; display: flex; align-items: center; justify-content: center; }
434
+ .note-modal { background: var(--bg-card); border: 1px solid #d4c5a0; border-radius: 14px; padding: 24px; width: 90%; max-width: 480px; box-shadow: 0 16px 48px #3c3a3830; }
435
+ .note-modal h3 { color: var(--text-title); font-size: 20px; margin-bottom: 12px; }
436
+ .note-modal .hl-text { background: var(--bg-soft); border: 1px solid #d4c5a0; border-radius: 8px; padding: 10px 14px; color: #6b5535; font-size: 17px; margin-bottom: 14px; max-height: 100px; overflow-y: auto; line-height: 1.6; }
437
+ .note-modal textarea { width: 100%; height: 80px; background: var(--bg-soft); border: 1px solid #d4c5a0; border-radius: 8px; color: var(--text); padding: 10px 14px; font-size: 17px; resize: vertical; font-family: inherit; outline: none; }
438
+ .note-modal textarea:focus { border-color: #c8963e; }
221
439
  .note-modal .btn-row { display: flex; gap: 8px; justify-content: flex-end; margin-top: 14px; }
222
- .note-modal .btn-row button { padding: 7px 18px; border-radius: 8px; font-size: 13px; font-weight: 600; cursor: pointer; border: none; }
223
- .note-modal .btn-save { background: #818cf8; color: #fff; }
224
- .note-modal .btn-save:hover { background: #6366f1; }
225
- .note-modal .btn-cancel { background: #334155; color: #94a3b8; }
226
- .note-modal .btn-cancel:hover { background: #475569; }
227
- .notes-sidebar { position: fixed; right: 0; top: 0; width: 340px; height: 100vh; background: #0f172a; border-left: 1px solid #1e293b; z-index: 500; overflow-y: auto; transform: translateX(100%); transition: transform .3s ease; }
440
+ .note-modal .btn-row button { padding: 7px 18px; border-radius: 8px; font-size: 17px; font-weight: 600; cursor: pointer; border: none; }
441
+ .note-modal .btn-save { background: #8b6914; color: #fff; }
442
+ .note-modal .btn-save:hover { background: #6b4c1e; }
443
+ .note-modal .btn-cancel { background: #f0e8d5; color: var(--text-muted); }
444
+ .note-modal .btn-cancel:hover { background: #e8dcc0; }
445
+ .notes-sidebar { position: fixed; right: 0; top: 0; width: 340px; height: 100vh; background: var(--bg-card); border-left: 1px solid #d4c5a0; z-index: 500; overflow-y: auto; transform: translateX(100%); transition: transform .3s ease; }
228
446
  .notes-sidebar.open { transform: translateX(0); }
229
- .notes-sidebar-header { padding: 16px 20px; border-bottom: 1px solid #1e293b; display: flex; align-items: center; justify-content: space-between; position: sticky; top: 0; background: #0f172a; z-index: 2; }
230
- .notes-sidebar-header h3 { color: #e2e8f0; font-size: 15px; font-weight: 700; }
231
- .notes-sidebar-header button { background: none; border: none; color: #64748b; font-size: 18px; cursor: pointer; }
232
- .notes-sidebar-header button:hover { color: #e2e8f0; }
233
- .note-item { padding: 14px 20px; border-bottom: 1px solid #1e293b; }
234
- .note-item .hl-text { color: #fef08a; font-size: 13px; line-height: 1.6; margin-bottom: 8px; border-left: 3px solid #fbbf24; padding-left: 10px; }
235
- .note-item .hl-note { color: #cbd5e1; font-size: 13px; line-height: 1.6; margin-bottom: 6px; }
236
- .note-item .hl-meta { color: #475569; font-size: 11px; display: flex; align-items: center; gap: 10px; }
237
- .note-item .hl-meta button { background: none; border: none; color: #dc5b5b; font-size: 11px; cursor: pointer; }
238
- .note-item .hl-meta button:hover { color: #f87171; }
239
- .notes-sidebar .empty { text-align: center; padding: 60px 20px; color: #475569; font-size: 14px; line-height: 2; }
240
- .btn-notes-toggle { position: fixed; right: 16px; bottom: 24px; z-index: 501; background: #818cf8; color: #fff; border: none; width: 44px; height: 44px; border-radius: 12px; font-size: 18px; cursor: pointer; box-shadow: 0 4px 16px #818cf840; transition: all .2s; display: flex; align-items: center; justify-content: center; }
241
- .btn-notes-toggle:hover { background: #6366f1; transform: scale(1.05); }
242
- .btn-notes-toggle .badge { position: absolute; top: -4px; right: -4px; background: #ef4444; color: #fff; font-size: 10px; width: 18px; height: 18px; border-radius: 9px; display: flex; align-items: center; justify-content: center; font-weight: 700; }
243
- @media (max-width: 640px) { .notes-sidebar { width: 100%; } }
447
+ .notes-sidebar-header { padding: 16px 20px; border-bottom: 1px solid #d4c5a0; display: flex; align-items: center; justify-content: space-between; position: sticky; top: 0; background: var(--bg-card); z-index: 2; }
448
+ .notes-sidebar-header h3 { color: var(--text-title); font-size: 19px; font-weight: 700; }
449
+ .notes-sidebar-header button { background: none; border: none; color: #96876a; font-size: 22px; cursor: pointer; }
450
+ .notes-sidebar-header button:hover { color: var(--text-title); }
451
+ .note-item { padding: 14px 20px; border-bottom: 1px solid #e0d5be; }
452
+ .note-item .hl-text { color: var(--accent); font-size: 17px; line-height: 1.6; margin-bottom: 8px; border-left: 3px solid #c8963e; padding-left: 10px; }
453
+ .note-item .hl-note { color: #4a4035; font-size: 17px; line-height: 1.6; margin-bottom: 6px; }
454
+ .note-item .hl-meta { color: #b8a080; font-size: 15px; display: flex; align-items: center; gap: 10px; }
455
+ .note-item .hl-meta button { background: none; border: none; color: #dc5b5b; font-size: 15px; cursor: pointer; }
456
+ .note-item .hl-meta button:hover { color: #b04444; }
457
+ .notes-sidebar .empty { text-align: center; padding: 60px 20px; color: #b8a080; font-size: 18px; line-height: 2; }
458
+ .btn-notes-toggle { position: fixed; right: 16px; bottom: 24px; z-index: 501; background: #8b6914; color: #fff; border: none; width: 44px; height: 44px; border-radius: 12px; font-size: 22px; cursor: pointer; box-shadow: 0 4px 16px #8b691430; transition: all .2s; display: flex; align-items: center; justify-content: center; }
459
+ .btn-notes-toggle:hover { background: #6b4c1e; transform: scale(1.05); }
460
+ .btn-notes-toggle .badge { position: absolute; top: -4px; right: -4px; background: #ef4444; color: #fff; font-size: 14px; width: 18px; height: 18px; border-radius: 9px; display: flex; align-items: center; justify-content: center; font-weight: 700; }
461
+ @media (max-width: 768px) { .notes-sidebar { width: 100%; } }
244
462
  </style>
245
463
  </head>
246
464
  <body>
@@ -317,14 +535,8 @@ body {
317
535
  const tagsHtml = tags.length ? '<div class="tags-row">' + tags.map(t => '<span class="tag">' + t + '</span>').join('') + '</div>' : '';
318
536
  document.getElementById('app').innerHTML =
319
537
  '<div class="top-bar">' +
320
- '<a href="./" class="btn-back">\u2190 \u8fd4\u56de\u5217\u8868</a>' +
538
+ '<a href="./index.html" class="btn-back">\u2190 \u8fd4\u56de\u65e5\u62a5</a>' +
321
539
  '<span class="article-source">' + (source || '') + '</span>' +
322
- '<span class="article-meta">\u9605\u8bfb\u6a21\u5f0f</span>' +
323
- '<span class="mode-badge html">\ud83c\udfa8 HTML</span>' +
324
- '<div class="view-switch">' +
325
- '<a href="' + file + '">\ud83d\udcdd MD</a>' +
326
- '<a href="article.html?file=' + encodeURIComponent(file) + '" class="active">\ud83c\udfa8 HTML</a>' +
327
- '</div>' +
328
540
  '</div>' +
329
541
  '<div class="article-header">' +
330
542
  '<h1>' + title + '</h1>' +
@@ -332,7 +544,7 @@ body {
332
544
  tagsHtml +
333
545
  '</div>' +
334
546
  '<div class="article-body">' + htmlContent + '</div>' +
335
- '<div style="text-align:center;padding:40px;color:#475569;font-size:12px;">\u2014 END \u2014</div>' +
547
+ '<div style="text-align:center;padding:40px;color:#475569;font-size: 16px;">\u2014 END \u2014</div>' +
336
548
  '<div class="notes-sidebar" id="notes-sidebar">' +
337
549
  '<div class="notes-sidebar-header"><h3>\ud83d\udcdd \u7b14\u8bb0</h3><button onclick="toggleNotes()">\u2715</button></div>' +
338
550
  '<div id="notes-list"><div class="empty">\u9009\u4e2d\u6587\u5b57\u540e\u70b9\u51fb\u201c\u6807\u8bb0\u7b14\u8bb0\u201d\u5373\u53ef\u6dfb\u52a0</div></div>' +
@@ -340,7 +552,7 @@ body {
340
552
  '<button class="btn-notes-toggle" id="btn-notes-toggle" onclick="toggleNotes()" title="\u7b14\u8bb0">\ud83d\udcdd<span class="badge" id="notes-badge" style="display:none">0</span></button>';
341
553
  } catch(e) {
342
554
  document.getElementById('app').innerHTML =
343
- '<div class="error-box">\u274c \u52a0\u8f7d\u5931\u8d25: ' + e.message + '<br><br><a href="./" class="btn-back">\u2190 \u8fd4\u56de\u5217\u8868</a></div>';
555
+ '<div class="error-box">\u274c \u52a0\u8f7d\u5931\u8d25: ' + e.message + '<br><br><a href="./index.html" class="btn-back">\u2190 \u8fd4\u56de\u65e5\u62a5</a></div>';
344
556
  }
345
557
  })();
346
558
 
@@ -608,7 +820,7 @@ def generate_html(date_str: str, topics: dict, action_suggestions_exist: bool, b
608
820
  relevance_badge = '<span class="relevance mid">中相关</span>'
609
821
 
610
822
  cards.append(f'''
611
- <article class="card" data-id="{escape_html(art['rel_path'])}" data-topic="{topic}">
823
+ <article class="card" data-id="{escape_html(art['rel_path'])}" data-topic="{topic}" onclick="if(!event.target.closest('button,a')){{var a=this.querySelector('a[href]');if(a)a.click()}}" style="cursor:pointer">
612
824
  <div class="card-header">
613
825
  <div class="card-title-row">
614
826
  <span class="unread-dot" id="dot-{escape_html(art['rel_path']).replace('/', '_').replace('.', '_')}"></span>
@@ -629,13 +841,13 @@ def generate_html(date_str: str, topics: dict, action_suggestions_exist: bool, b
629
841
  <div class="card-actions">
630
842
  <a href="{escape_html(art['rel_path'])}"
631
843
  class="btn-read" onclick="markRead('{escape_html(art['rel_path'])}')">
632
- 📖 阅读
844
+ 📖 阅读全文 <span class="arrow">→</span>
633
845
  </a>
634
- <button class="btn-toggle" onclick="toggleRead('{escape_html(art['rel_path'])}')">
635
- <span class="toggle-label">✓ 标记已读</span>
846
+ <button class="btn-toggle" onclick="event.stopPropagation();toggleRead('{escape_html(art['rel_path'])}')">
847
+ <span class="toggle-label"></span>
636
848
  </button>
637
- <button class="btn-fav" onclick="toggleFav('{escape_html(art['rel_path'])}')" title="收藏">
638
-
849
+ <button class="btn-fav" onclick="event.stopPropagation();toggleFav('{escape_html(art['rel_path'])}')" title="收藏">
850
+ <span class="fav-label"></span>
639
851
  </button>
640
852
  </div>
641
853
  </article>''')
@@ -654,7 +866,7 @@ def generate_html(date_str: str, topics: dict, action_suggestions_exist: bool, b
654
866
  action_link = ''
655
867
  if action_suggestions_exist:
656
868
  action_link = '''
657
- <a href="./行动建议.md" target="_blank" class="action-link">📋 行动建议</a>
869
+ <a href="./行动建议.html" class="action-link">📋 行动建议</a>
658
870
  '''
659
871
 
660
872
  briefing_html = ''
@@ -673,19 +885,49 @@ def generate_html(date_str: str, topics: dict, action_suggestions_exist: bool, b
673
885
  <title>公众号日报 — {date_str}</title>
674
886
  <style>
675
887
  * {{ margin: 0; padding: 0; box-sizing: border-box; }}
888
+ :root {{
889
+ --bg: #f5f0e8;
890
+ --bg-card: #fefcf8;
891
+ --bg-soft: #faf6ef;
892
+ --bg-hover: #fffdf7;
893
+ --border: #e0d5be;
894
+ --border-soft: #d4c5a060;
895
+ --accent: #8b6914;
896
+ --accent-light: #c8963e;
897
+ --accent-glow: #c8963e15;
898
+ --text: #3c3a38;
899
+ --text-title: #2a2018;
900
+ --text-muted: #96876a;
901
+ --text-faint: #b8a080;
902
+ --radius: 14px;
903
+ --radius-sm: 10px;
904
+ --shadow-sm: 0 1px 3px #00000008;
905
+ --shadow-md: 0 4px 16px #3c3a380a;
906
+ --shadow-lg: 0 8px 28px #3c3a3810;
907
+ --transition: .25s cubic-bezier(.4,0,.2,1);
908
+ }}
909
+
910
+ [data-theme="dark"] {{
911
+ --bg: #1a1816; --bg-card: #252220; --bg-soft: #2a2624;
912
+ --bg-hover: #2f2b28; --border: #3d3835; --border-soft: #332e2b;
913
+ --accent: #c8963e; --accent-light: #d4a853; --accent-glow: #c8963e20;
914
+ --text: #d4cfc8; --text-title: #e8e0d4; --text-muted: #968b7a;
915
+ --text-faint: #6b6358;
916
+ }}
676
917
  body {{
677
918
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;
678
- background: #f5f0e8;
679
- color: #3c3a38;
919
+ background: var(--bg);
920
+ background-image: radial-gradient(circle at 20% 0%, #faf6ef 0%, transparent 50%), radial-gradient(circle at 80% 100%, #f0e8d530 0%, transparent 50%);
921
+ color: var(--text);
680
922
  min-height: 100vh;
681
923
  -webkit-font-smoothing: antialiased;
682
924
  }}
683
- .container {{ max-width: 1400px; margin: 0 auto; padding: 28px 32px; }}
925
+ .container {{ max-width: 1300px; margin: 0 auto; padding: 28px 28px 48px; }}
684
926
 
685
927
  /* ---- Header ---- */
686
928
  .header {{
687
929
  text-align: center;
688
- padding: 44px 20px 28px;
930
+ padding: 40px 20px 36px;
689
931
  margin-bottom: 24px;
690
932
  position: relative;
691
933
  }}
@@ -693,205 +935,258 @@ body {{
693
935
  content: '';
694
936
  position: absolute;
695
937
  bottom: 0; left: 50%; transform: translateX(-50%);
696
- width: 60px; height: 3px;
697
- background: linear-gradient(90deg, #8b6914, #c8963e, #d4a853);
938
+ width: 56px; height: 3px;
939
+ background: linear-gradient(90deg, var(--accent), var(--accent-light), #d4a853);
698
940
  border-radius: 2px;
699
941
  }}
700
942
  .header h1 {{
701
- font-size: 28px; font-weight: 800; letter-spacing: -.3px;
702
- color: #4a3728;
943
+ font-size: 32px; font-weight: 800; letter-spacing: -.5px;
944
+ color: var(--text-title);
945
+ background: linear-gradient(135deg, #4a3728, #6b5535);
946
+ -webkit-background-clip: text; -webkit-text-fill-color: transparent;
947
+ background-clip: text;
703
948
  }}
704
949
  .header .meta {{
705
- margin-top: 8px; color: #8b7355; font-size: 14px;
706
- display: flex; align-items: center; justify-content: center; gap: 14px; flex-wrap: wrap;
950
+ margin-top: 14px; color: var(--text-muted); font-size: 18px;
951
+ display: flex; align-items: center; justify-content: center; gap: 16px; flex-wrap: wrap;
707
952
  }}
708
953
  .action-link {{
709
- color: #8b6914; text-decoration: none; font-weight: 600;
710
- padding: 5px 14px; border: 1px solid #c8963e40; border-radius: 20px;
711
- font-size: 13px; transition: all .25s; background: #faf6ef;
954
+ color: var(--accent); text-decoration: none; font-weight: 600;
955
+ padding: 7px 18px; border: 1px solid #c8963e40; border-radius: 22px;
956
+ font-size: 17px; transition: all var(--transition); background: var(--bg-soft);
957
+ display: inline-flex; align-items: center; gap: 5px;
958
+ box-shadow: var(--shadow-sm);
712
959
  }}
713
- .action-link:hover {{ background: #c8963e15; border-color: #c8963e70; }}
960
+ .action-link:hover {{ background: var(--accent-glow); border-color: var(--accent-light); transform: translateY(-2px); box-shadow: var(--shadow-md); }}
714
961
 
715
- /* ---- Stats Bar ---- */
716
- .stats-bar {{
717
- display: flex; align-items: center; justify-content: center; gap: 10px;
718
- flex-wrap: wrap; margin-bottom: 22px; padding: 10px 22px;
719
- background: #faf6ef; border: 1px solid #d4c5a060; border-radius: 12px;
720
- font-size: 13px; color: #8b7355;
962
+ /* ---- Search ---- */
963
+ .search-box {{
964
+ width: 100%; max-width: 560px; margin: 0 auto 20px; display: block;
965
+ padding: 14px 22px; border-radius: var(--radius); border: 1px solid var(--border);
966
+ background: var(--bg-card); color: var(--text); font-size: 18px; outline: none;
967
+ transition: all var(--transition); font-family: inherit;
968
+ box-shadow: var(--shadow-sm);
721
969
  }}
722
- .stats-bar strong {{ color: #3c3a38; font-weight: 700; }}
723
- .stats-group {{
724
- display: flex; align-items: center; gap: 8px;
970
+ .search-box:focus {{ border-color: var(--accent-light); box-shadow: 0 0 0 4px var(--accent-glow), var(--shadow-md); }}
971
+ .search-box::placeholder {{ color: var(--text-faint); }}
972
+
973
+ /* ---- Toolbar: Stats + Tabs ---- */
974
+ .toolbar {{
975
+ display: flex; align-items: center; gap: 12px;
976
+ flex-wrap: wrap; margin-bottom: 28px;
977
+ }}
978
+ .stats-bar {{
979
+ display: flex; align-items: center; gap: 10px;
980
+ flex-wrap: wrap; padding: 8px 16px;
981
+ background: var(--bg-soft); border: 1px solid var(--border-soft); border-radius: var(--radius-sm);
982
+ font-size: 17px; color: var(--text-muted);
983
+ box-shadow: var(--shadow-sm);
725
984
  }}
985
+ .stats-bar strong {{ color: var(--text); font-weight: 700; }}
986
+ .stats-group {{ display: flex; align-items: center; gap: 8px; }}
987
+ .stats-divider {{ width: 1px; height: 18px; background: #d4c5a080; }}
726
988
  .btn-clear {{
727
- background: #f0e8d5; color: #5c4a32; border: 1px solid #d4c5a0;
728
- padding: 5px 14px; border-radius: 8px; cursor: pointer;
729
- font-size: 12px; font-weight: 500; transition: all .2s;
989
+ background: #f0e8d5; color: #5c4a32; border: 1px solid var(--border);
990
+ padding: 5px 13px; border-radius: 8px; cursor: pointer;
991
+ font-size: 16px; font-weight: 500; transition: all var(--transition);
992
+ white-space: nowrap;
730
993
  }}
731
- .btn-clear:hover {{ background: #e8dcc0; border-color: #c8963e; color: #3c3a38; }}
994
+ .btn-clear:hover {{ background: #e8dcc0; border-color: var(--accent-light); color: var(--text); transform: translateY(-1px); }}
732
995
  .btn-clear.active-mode {{
733
- background: #8b6914 !important; border-color: #8b6914 !important; color: #fff !important;
996
+ background: var(--accent) !important; border-color: var(--accent) !important; color: #fff !important;
734
997
  }}
735
998
 
736
999
  /* ---- Tabs ---- */
737
1000
  .tabs {{
738
- display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 24px;
739
- position: sticky; top: 12px; z-index: 10; padding: 6px;
740
- background: #faf6eff2; backdrop-filter: blur(12px);
741
- border: 1px solid #d4c5a060; border-radius: 12px;
1001
+ display: flex; gap: 4px; flex-wrap: wrap; flex: 1; min-width: 280px;
1002
+ padding: 5px;
1003
+ background: var(--bg-soft); border: 1px solid var(--border-soft); border-radius: var(--radius-sm);
1004
+ box-shadow: var(--shadow-sm);
742
1005
  }}
743
1006
  .tab-btn {{
744
- background: transparent; color: #8b7355; border: none;
745
- padding: 9px 18px; border-radius: 10px; cursor: pointer;
746
- font-size: 14px; font-weight: 500; transition: all .2s;
1007
+ background: transparent; color: var(--text-muted); border: none;
1008
+ padding: 8px 16px; border-radius: 9px; cursor: pointer;
1009
+ font-size: 18px; font-weight: 500; transition: all var(--transition);
1010
+ white-space: nowrap;
747
1011
  }}
748
- .tab-btn:hover {{ color: #3c3a38; background: #f0e8d5; }}
1012
+ .tab-btn:hover {{ color: var(--text); background: #f0e8d5; }}
749
1013
  .tab-btn.active {{
750
- color: #2a2018; background: #dcc89a;
751
- box-shadow: 0 1px 4px #00000018, inset 0 1px 0 #fff8;
1014
+ color: var(--text-title); background: #dcc89a;
1015
+ box-shadow: 0 2px 8px #00000015, inset 0 1px 0 #fff8;
752
1016
  font-weight: 700;
753
- border: 1px solid #c8963e30;
754
1017
  }}
755
1018
  .tab-btn .count {{
756
- background: #d4c5a040; padding: 2px 8px; border-radius: 8px;
757
- font-size: 12px; margin-left: 5px; font-weight: 600;
1019
+ background: #d4c5a040; padding: 1px 7px; border-radius: 7px;
1020
+ font-size: 15px; margin-left: 4px; font-weight: 600;
758
1021
  }}
759
- .tab-btn.active .count {{ background: #c8963e20; color: #8b6914; }}
1022
+ .tab-btn.active .count {{ background: var(--accent-glow); color: var(--accent); }}
760
1023
 
761
1024
  /* ---- Topic Sections ---- */
762
1025
  .topic-section {{ display: none; }}
763
- .topic-section.active {{ display: block; }}
1026
+ .topic-section.active {{ display: block; animation: fadeIn .35s cubic-bezier(.4,0,.2,1); }}
1027
+ @keyframes fadeIn {{ from {{ opacity: 0; transform: translateY(8px); }} to {{ opacity: 1; transform: translateY(0); }} }}
764
1028
  .topic-heading {{
765
- font-size: 18px; font-weight: 700; margin-bottom: 16px;
766
- display: flex; align-items: center; gap: 10px; color: #4a3728;
1029
+ font-size: 22px; font-weight: 700; margin-bottom: 16px; margin-top: 4px;
1030
+ display: flex; align-items: center; gap: 10px; color: var(--text-title);
767
1031
  }}
768
1032
  .topic-heading::after {{
769
- content: ''; flex: 1; height: 1px; background: #d4c5a0; margin-left: 8px;
1033
+ content: ''; flex: 1; height: 1px; background: linear-gradient(90deg, #d4c5a0, transparent); margin-left: 4px;
770
1034
  }}
771
- .topic-heading .count {{ font-size: 13px; color: #8b7355; font-weight: 400; }}
1035
+ .topic-heading .count {{ font-size: 16px; color: var(--text-muted); font-weight: 400; background: #f0e8d5; padding: 2px 10px; border-radius: 10px; }}
772
1036
 
773
1037
  /* ---- Cards ---- */
774
1038
  .cards-grid {{ display: flex; flex-direction: column; gap: 10px; }}
775
1039
  .card {{
776
- background: #fefcf8; border-radius: 12px; padding: 20px 24px;
777
- border: 1px solid #d4c5a0; transition: all .25s ease;
1040
+ background: var(--bg-card); border-radius: var(--radius); padding: 20px 24px;
1041
+ border: 1px solid var(--border); transition: all var(--transition);
778
1042
  position: relative; overflow: hidden;
779
1043
  }}
780
1044
  .card::before {{
781
1045
  content: ''; position: absolute; top: 0; left: 0; width: 3px; height: 100%;
782
- background: transparent; transition: background .3s; border-radius: 12px 0 0 12px;
1046
+ background: transparent; transition: background .3s; border-radius: var(--radius) 0 0 var(--radius);
783
1047
  }}
784
1048
  .card:hover {{
785
- border-color: #c8963e50; background: #fffdf7;
786
- transform: translateY(-1px); box-shadow: 0 3px 16px #3c3a3810;
1049
+ border-color: #c8963e60; background: var(--bg-hover);
1050
+ transform: translateY(-2px); box-shadow: var(--shadow-lg);
787
1051
  }}
788
- .card:hover::before {{ background: #c8963e; }}
1052
+ .card:hover::before {{ background: var(--accent-light); }}
789
1053
  .card.read {{
790
- opacity: 0.55; background: #f5f0e8; border-color: #d4c5a030;
1054
+ opacity: 0.48; background: #f8f3ea; border-color: #d4c5a040;
791
1055
  }}
792
- .card.read:hover {{ opacity: 0.7; border-color: #d4c5a060; transform: none; box-shadow: none; }}
1056
+
1057
+
793
1058
  .card.read:hover::before {{ background: #b8a080; }}
794
- .card.faved {{ border-color: #c8963e40; background: #fffcf5; }}
1059
+ .card.faved {{ border-color: #c8963e50; background: #fffcf3; box-shadow: 0 2px 12px #c8963e0a; }}
795
1060
 
796
- .card-header {{ margin-bottom: 8px; }}
1061
+ .card-header {{ margin-bottom: 6px; }}
797
1062
  .card-title-row {{ display: flex; align-items: flex-start; gap: 10px; }}
798
1063
  .unread-dot {{
799
- width: 9px; height: 9px; border-radius: 50%;
800
- background: #c8963e; flex-shrink: 0; margin-top: 5px; transition: all .3s;
801
- box-shadow: 0 0 6px #c8963e40;
1064
+ width: 8px; height: 8px; border-radius: 50%;
1065
+ background: var(--accent-light); flex-shrink: 0; margin-top: 7px; transition: all .3s;
1066
+ box-shadow: 0 0 0 3px var(--accent-glow);
1067
+ animation: pulse 2s ease-in-out infinite;
802
1068
  }}
803
- .card.read .unread-dot {{ background: #b8a080; box-shadow: none; }}
1069
+ @keyframes pulse {{ 0%, 100% {{ box-shadow: 0 0 0 3px var(--accent-glow); }} 50% {{ box-shadow: 0 0 0 5px #c8963e10; }} }}
1070
+ .card.read .unread-dot {{ background: #c8b898; box-shadow: none; animation: none; }}
804
1071
  .card-title {{
805
- font-size: 18px; font-weight: 700; flex: 1; line-height: 1.4; letter-spacing: -.3px;
1072
+ font-size: 20px; font-weight: 700; flex: 1; line-height: 1.5; letter-spacing: -.2px;
806
1073
  }}
807
- .card-title a {{ color: #2a2018; text-decoration: none; transition: color .2s; }}
808
- .card-title a:hover {{ color: #8b6914; text-decoration: underline; text-underline-offset: 3px; }}
809
- .card.read .card-title a {{ color: #8b7355; }}
1074
+ .card-title a {{ color: var(--text-title); text-decoration: none; transition: color .2s; }}
1075
+ .card-title a:hover {{ color: var(--accent); }}
1076
+ .card.read .card-title a {{ color: var(--text-muted); }}
810
1077
 
811
- .card-meta {{ display: flex; align-items: center; gap: 8px; margin-top: 6px; font-size: 13px; color: #8b7355; font-weight: 500; }}
1078
+ .card-meta {{ display: flex; align-items: center; gap: 8px; margin-top: 5px; font-size: 16px; color: var(--text-muted); font-weight: 500; }}
812
1079
  .card-meta .source {{
813
- color: #6b4c1e; font-weight: 600;
1080
+ color: var(--accent); font-weight: 600;
1081
+ background: var(--accent-glow); padding: 1px 8px; border-radius: 4px;
814
1082
  }}
815
1083
 
816
1084
  .relevance {{
817
- font-size: 10px; padding: 3px 10px; border-radius: 12px; font-weight: 700;
818
- flex-shrink: 0; letter-spacing: .3px;
1085
+ font-size: 14px; padding: 2px 8px; border-radius: 10px; font-weight: 700;
1086
+ flex-shrink: 0; letter-spacing: .3px; text-transform: uppercase;
819
1087
  }}
820
- .relevance.high {{ background: #dc5b5b15; color: #b04444; border: 1px solid #dc5b5b30; }}
821
- .relevance.mid {{ background: #c8963e15; color: #8b6914; border: 1px solid #c8963e40; }}
1088
+ .relevance.high {{ background: #dc5b5b12; color: #b04444; border: 1px solid #dc5b5b25; }}
1089
+ .relevance.mid {{ background: #c8963e12; color: var(--accent); border: 1px solid #c8963e30; }}
822
1090
 
823
- .card-tags {{ display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 8px; }}
1091
+ .card-tags {{ display: flex; gap: 5px; flex-wrap: wrap; margin: 8px 0 6px; }}
824
1092
  .tag {{
825
- font-size: 11px; padding: 3px 10px; background: #f0e8d5; border-radius: 6px;
826
- color: #8b7355; font-weight: 500; letter-spacing: .2px;
1093
+ font-size: 15px; padding: 2px 9px; background: #f0e8d5; border-radius: 5px;
1094
+ color: var(--text-muted); font-weight: 500; letter-spacing: .1px;
1095
+ transition: all .15s;
827
1096
  }}
1097
+ .tag:hover {{ background: #e8dcc0; color: var(--text); }}
828
1098
  .tag-more {{
829
- background: #ede0c8; color: #96876a; font-weight: 600; cursor: default;
1099
+ background: #e8dcc0; color: var(--text-muted); font-weight: 600; cursor: default;
830
1100
  }}
831
1101
 
832
1102
  .card-preview {{
833
- font-size: 13px; color: #a0987a; line-height: 1.65; margin-bottom: 12px;
1103
+ font-size: 17px; color: #a0987a; line-height: 1.7; margin-bottom: 12px;
834
1104
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
835
1105
  }}
836
1106
 
837
1107
  /* ---- Card Actions ---- */
838
- .card-actions {{ display: flex; gap: 8px; align-items: center; justify-content: flex-end; }}
1108
+ .card-actions {{ display: flex; gap: 10px; align-items: center; justify-content: flex-start; flex-wrap: wrap; }}
839
1109
  .btn-read {{
840
- font-size: 13px; color: #fff; text-decoration: none;
841
- padding: 6px 18px; border: none; border-radius: 7px;
842
- transition: all .2s; font-weight: 600;
843
- background: linear-gradient(135deg, #8b6914, #a07828);
844
- box-shadow: 0 1px 3px #8b691420;
1110
+ font-size: 16px; color: #fff; text-decoration: none;
1111
+ padding: 8px 20px; border: none; border-radius: 8px;
1112
+ transition: all var(--transition); font-weight: 600;
1113
+ background: linear-gradient(135deg, var(--accent), #a07828);
1114
+ box-shadow: 0 2px 8px #8b691420;
845
1115
  }}
846
- .btn-read:hover {{ background: linear-gradient(135deg, #6b4c1e, #8b6914); box-shadow: 0 2px 8px #8b691430; color: #fff; }}
1116
+ .btn-read:hover {{ background: linear-gradient(135deg, #6b4c1e, var(--accent)); box-shadow: 0 4px 14px #8b691430; color: #fff; transform: translateY(-1px); }}
1117
+ .btn-read .arrow {{ display: inline-block; transition: transform .2s; }}
1118
+ .btn-read:hover .arrow {{ transform: translateX(3px); }}
847
1119
  .btn-toggle {{
848
- font-size: 12px; color: #8b7355; background: none; border: 1px solid #d4c5a0;
849
- padding: 5px 14px; border-radius: 7px; cursor: pointer;
850
- transition: all .2s; font-weight: 500;
1120
+ font-size: 16px; color: var(--text-muted); background: var(--bg-card); border: 1px solid var(--border);
1121
+ padding: 8px 20px; border-radius: 8px; cursor: pointer;
1122
+ transition: all var(--transition); font-weight: 600; white-space: nowrap;
851
1123
  }}
852
- .btn-toggle:hover {{ color: #3c3a38; border-color: #b8a080; }}
853
- .card.read .btn-toggle {{ color: #6b8f5e; border-color: #6b8f5e30; }}
854
- .card.read .toggle-label::before {{ content: '↩ 标记未读'; }}
1124
+ .btn-toggle:hover {{ color: #5c4a32; border-color: #c8b898; background: var(--bg-soft); transform: translateY(-1px); }}
1125
+ .card.read .btn-toggle {{ color: #6b8f5e; border-color: #6b8f5e40; }}
1126
+ .card.read .btn-toggle:hover {{ background: #6b8f5e08; border-color: #6b8f5e60; }}
1127
+ .card.read .toggle-label::before {{ content: '↩ 取消已读'; }}
855
1128
  .toggle-label::before {{ content: '✓ 标记已读'; }}
856
1129
 
857
1130
  /* ---- Favorite ---- */
858
1131
  .btn-fav {{
859
- font-size: 14px; background: none; border: 1px solid #d4c5a0;
860
- padding: 4px 10px; border-radius: 7px; cursor: pointer;
861
- transition: all .2s; color: #8b7355; line-height: 1;
1132
+ font-size: 16px; background: var(--bg-card); border: 1px solid var(--border);
1133
+ padding: 8px 20px; border-radius: 8px; cursor: pointer;
1134
+ transition: all var(--transition); color: var(--text-muted); font-weight: 600; white-space: nowrap;
862
1135
  }}
863
- .btn-fav:hover {{ color: #c8963e; border-color: #c8963e60; }}
864
- .btn-fav.faved {{ color: #c8963e; border-color: #c8963e50; background: #c8963e10; }}
1136
+ .btn-fav:hover {{ color: var(--accent-light); border-color: #c8963e60; background: var(--accent-glow); transform: translateY(-1px); }}
1137
+ .btn-fav.faved {{ color: var(--accent-light); border-color: #c8963e50; background: #c8963e10; }}
1138
+ .fav-label::before {{ content: '☆ 收藏'; }}
1139
+ .btn-fav.faved .fav-label::before {{ content: '★ 已收藏'; }}
865
1140
  .fav-empty {{
866
- text-align: center; padding: 60px 20px; color: #96876a;
867
- font-size: 15px; line-height: 2;
1141
+ text-align: center; padding: 64px 20px; color: var(--text-faint);
1142
+ font-size: 18px; line-height: 2.2;
1143
+ background: var(--bg-soft); border-radius: var(--radius); border: 1px dashed #d4c5a0;
868
1144
  }}
869
1145
 
870
- /* ---- Search ---- */
871
- .search-box {{
872
- width: 100%; max-width: 480px; margin: 0 auto 20px; display: block;
873
- padding: 11px 18px; border-radius: 12px; border: 1px solid #d4c5a0;
874
- background: #fdfaf5; color: #3c3a38; font-size: 14px; outline: none;
875
- transition: all .25s; font-family: inherit;
1146
+ /* ---- Briefing ---- */
1147
+ .briefing {{
1148
+ background: linear-gradient(135deg, var(--bg-soft), var(--bg-card));
1149
+ border: 1px solid #d4c5a0; border-radius: var(--radius);
1150
+ padding: 20px 24px; margin-bottom: 24px;
1151
+ border-left: 4px solid var(--accent-light);
1152
+ box-shadow: var(--shadow-sm);
1153
+ }}
1154
+ .briefing-title {{
1155
+ font-size: 18px; font-weight: 700; color: var(--accent); margin-bottom: 8px;
1156
+ display: flex; align-items: center; gap: 6px;
1157
+ }}
1158
+ .briefing-text {{
1159
+ font-size: 18px; color: #5c4a32; line-height: 1.8;
876
1160
  }}
877
- .search-box:focus {{ border-color: #c8963e; box-shadow: 0 0 0 3px #c8963e15; }}
878
- .search-box::placeholder {{ color: #b8a080; }}
879
1161
 
880
1162
  /* ---- Footer ---- */
881
1163
  .footer {{
882
- text-align: center; padding: 32px 20px; color: #b8a080;
883
- font-size: 12px; margin-top: 24px; border-top: 1px solid #d4c5a050;
1164
+ text-align: center; padding: 36px 20px 8px; color: var(--text-faint);
1165
+ font-size: 16px; margin-top: 32px;
884
1166
  }}
885
1167
 
1168
+ /* ---- Scrollbar ---- */
1169
+ ::-webkit-scrollbar {{ width: 8px; }}
1170
+ ::-webkit-scrollbar-track {{ background: var(--bg); }}
1171
+ ::-webkit-scrollbar-thumb {{ background: #d4c5a0; border-radius: 4px; }}
1172
+ ::-webkit-scrollbar-thumb:hover {{ background: var(--accent-light); }}
1173
+
886
1174
  /* ---- Responsive ---- */
887
- @media (max-width: 640px) {{
888
- .container {{ padding: 14px; }}
889
- .header {{ padding: 28px 14px 22px; }}
890
- .header h1 {{ font-size: 22px; }}
891
- .card {{ padding: 14px 16px; }}
1175
+ @media (max-width: 768px) {{
1176
+ .container {{ padding: 16px 14px 32px; }}
1177
+ .header {{ padding: 28px 14px 24px; margin-bottom: 16px; }}
1178
+ .header h1 {{ font-size: 26px; }}
1179
+ .card {{ padding: 14px 16px; border-radius: var(--radius-sm); }}
1180
+ .card-title {{ font-size: 19px; }}
1181
+ .tabs {{ min-width: 100%; }}
1182
+ .stats-bar {{ width: 100%; justify-content: center; }}
1183
+ .card-actions {{ flex-direction: column; align-items: stretch; gap: 8px; }}
1184
+ .btn-read, .btn-toggle, .btn-fav {{ text-align: center; }}
1185
+ }}
1186
+ @media (max-width: 480px) {{
892
1187
  .tabs {{ gap: 2px; padding: 4px; }}
893
- .tab-btn {{ padding: 7px 11px; font-size: 13px; }}
894
- .card-title {{ font-size: 14px; }}
1188
+ .tab-btn {{ padding: 7px 10px; font-size: 17px; }}
1189
+ .briefing {{ padding: 14px 16px; }}
895
1190
  }}
896
1191
  </style>
897
1192
  </head>
@@ -899,40 +1194,42 @@ body {{
899
1194
  <div class="container">
900
1195
 
901
1196
  <div class="header">
902
- <h1>📋 公众号日报 {date_str}</h1>
1197
+ <button id="theme-toggle" title="切换暗色" style="position:absolute;top:0;right:0;background:var(--bg-card);border:1px solid var(--border);border-radius:8px;padding:6px 10px;cursor:pointer;font-size: 20px;box-shadow:var(--shadow-sm);transition:all var(--transition);z-index:5;">🌙</button>
1198
+ <h1>公众号日报 — {date_str}</h1>
903
1199
  <div class="meta">
904
- <span>共 <strong>{total}</strong> 篇文章 {', '.join(f'{TOPIC_LABELS[t][2]} {len(topics[t])}' for t in TOPIC_ORDER if t in topics)}</span>
1200
+ <span>共 <strong>{total}</strong> 篇文章 · {', '.join(f'{TOPIC_LABELS[t][2]} {len(topics[t])}' for t in TOPIC_ORDER if t in topics)}</span>
905
1201
  {action_link}
906
- <span style="font-size:12px;color:#b8a080;display:flex;align-items:center;gap:6px">
907
- <button class="btn-clear" id="btn-mode-md" onclick="setViewMode('md')" style="font-size:11px;padding:3px 10px">📝</button>
908
- <button class="btn-clear" id="btn-mode-html" onclick="setViewMode('html')" style="font-size:11px;padding:3px 10px">🎨</button>
909
- <button class="btn-clear" onclick="exportFav()" id="btn-export-fav" style="font-size:11px;padding:3px 10px">📥</button>
910
- </span>
911
1202
  </div>
912
1203
  </div>
913
1204
 
914
- <div class="stats-bar">
915
- <span class="stats-group">
916
- <span>📊 <strong id="unread-count">-</strong> 篇未读 / {total} 篇</span>
917
- <button class="btn-clear" id="btn-filter-unread" onclick="toggleUnreadFilter()" style="font-weight:600">👁 仅看未读</button>
918
- </span>
919
- <span class="stats-group">
920
- <button class="btn-clear" onclick="markAllRead()">✅ 全部标为已读</button>
921
- <button class="btn-clear" onclick="clearAll()">🔄 重置</button>
922
- </span>
923
- <span class="stats-group">
924
- <span id="sync-indicator" style="display:none;font-size:12px;color:#6b8f5e;font-weight:600">🟢 已同步</span>
925
- </span>
1205
+ {briefing_html}
926
1206
 
927
1207
  <input type="text" class="search-box" placeholder="🔍 搜索文章标题、来源、标签..." oninput="doSearch(this.value)">
928
1208
 
929
- <nav class="tabs">
1209
+ <div class="toolbar">
1210
+ <div class="stats-bar">
1211
+ <span class="stats-group">
1212
+ <span>📊 <strong id="unread-count">-</strong>/{total} 未读</span>
1213
+ </span>
1214
+ <span class="stats-divider"></span>
1215
+ <span class="stats-group">
1216
+ <button class="btn-clear" id="btn-filter-unread" onclick="toggleUnreadFilter()">👁 仅未读</button>
1217
+ <button class="btn-clear" onclick="markAllRead()">✅ 全部已读</button>
1218
+ <button class="btn-clear" onclick="clearAll()">🔄 重置</button>
1219
+ </span>
1220
+ <span class="stats-group" style="margin-left:auto">
1221
+ <button class="btn-clear" onclick="exportFav()" id="btn-export-fav" title="导出收藏">⭐ 收藏</button>
1222
+ <span id="sync-indicator" style="display:none;font-size: 16px;color:#6b8f5e;font-weight:600">🟢 已同步</span>
1223
+ </span>
1224
+ </div>
1225
+ <nav class="tabs">
930
1226
  {''.join(nav_items)}
931
- <button class="tab-btn" data-topic="收藏"
932
- style="--accent: #fbbf24" onclick="switchTab('收藏')">
933
- ⭐ 收藏 <span class="count" id="fav-count">0</span>
934
- </button>
935
- </nav>
1227
+ <button class="tab-btn" data-topic="收藏"
1228
+ style="--accent: #fbbf24" onclick="switchTab('收藏')">
1229
+ ⭐ 收藏 <span class="count" id="fav-count">0</span>
1230
+ </button>
1231
+ </nav>
1232
+ </div>
936
1233
 
937
1234
  <section class="topic-section" id="section-收藏" data-topic="收藏">
938
1235
  <h2 class="topic-heading" style="--accent: #fbbf24">
@@ -1023,7 +1320,7 @@ function switchTab(topic) {{
1023
1320
  const btn = document.querySelector(`.tab-btn[data-topic="${{topic}}"]`);
1024
1321
  if (btn) btn.classList.add('active');
1025
1322
  sessionStorage.setItem('weflow_tab_{date_str}', topic);
1026
- window.location.hash = topic;
1323
+ try {{ history.replaceState(null, '', '#' + topic); }} catch(e) {{}}
1027
1324
  }}
1028
1325
  function doSearch(query) {{
1029
1326
  const q = query.toLowerCase().trim();
@@ -1103,11 +1400,9 @@ function applyFavState() {{
1103
1400
  const id = card.getAttribute('data-id');
1104
1401
  if (favs.includes(id)) {{
1105
1402
  btn.classList.add('faved');
1106
- btn.innerHTML = '★';
1107
1403
  card.classList.add('faved');
1108
1404
  }} else {{
1109
1405
  btn.classList.remove('faved');
1110
- btn.innerHTML = '☆';
1111
1406
  card.classList.remove('faved');
1112
1407
  }}
1113
1408
  }});
@@ -1135,14 +1430,6 @@ function renderFavSection() {{
1135
1430
  container.innerHTML = html || '<div class="fav-empty">⭐ 暂无收藏文章</div>';
1136
1431
  }}
1137
1432
 
1138
- const VIEW_MODE_KEY = 'weflow_vmode_{date_str}';
1139
- function getViewMode() {{
1140
- return localStorage.getItem(VIEW_MODE_KEY) || 'html';
1141
- }}
1142
- function setViewMode(mode) {{
1143
- localStorage.setItem(VIEW_MODE_KEY, mode);
1144
- applyViewMode();
1145
- }}
1146
1433
  const UNREAD_KEY = 'weflow_unread_{date_str}';
1147
1434
  let unreadFilterOn = sessionStorage.getItem(UNREAD_KEY) === '1';
1148
1435
  function toggleUnreadFilter() {{
@@ -1152,10 +1439,10 @@ function toggleUnreadFilter() {{
1152
1439
  if (btn) {{
1153
1440
  if (unreadFilterOn) {{
1154
1441
  btn.classList.add('active-mode');
1155
- btn.textContent = '👁 仅看未读 ✓';
1442
+ btn.textContent = '👁 仅未读 ✓';
1156
1443
  }} else {{
1157
1444
  btn.classList.remove('active-mode');
1158
- btn.textContent = '👁 仅看未读';
1445
+ btn.textContent = '👁 仅未读';
1159
1446
  }}
1160
1447
  }}
1161
1448
  applyUnreadFilter();
@@ -1168,29 +1455,13 @@ function applyUnreadFilter() {{
1168
1455
  }}
1169
1456
 
1170
1457
  function applyViewMode() {{
1171
- const mode = getViewMode();
1172
- const btnMd = document.getElementById('btn-mode-md');
1173
- const btnHtml = document.getElementById('btn-mode-html');
1174
- if (btnMd && btnHtml) {{
1175
- btnMd.classList.toggle('active-mode', mode === 'md');
1176
- btnHtml.classList.toggle('active-mode', mode === 'html');
1177
- }}
1178
- // Update all article title links and read buttons
1458
+ // 默认 HTML 渲染模式,所有链接指向 article.html
1179
1459
  document.querySelectorAll('.card-title a, .btn-read').forEach(a => {{
1180
1460
  const card = a.closest('.card');
1181
1461
  if (!card) return;
1182
1462
  const id = card.getAttribute('data-id');
1183
1463
  if (!id) return;
1184
- if (mode === 'html') {{
1185
- const newHref = 'article.html?file=' + encodeURIComponent(id) + '&date={date_str}';
1186
- if (a.getAttribute('data-md-href') === null) {{
1187
- a.setAttribute('data-md-href', a.getAttribute('href') || '');
1188
- }}
1189
- a.setAttribute('href', newHref);
1190
- }} else {{
1191
- const mdHref = a.getAttribute('data-md-href');
1192
- if (mdHref) a.setAttribute('href', mdHref);
1193
- }}
1464
+ a.setAttribute('href', 'article.html?file=' + encodeURIComponent(id) + '&date={date_str}');
1194
1465
  }});
1195
1466
  }}
1196
1467
 
@@ -1254,7 +1525,7 @@ function exportFav() {{
1254
1525
  for (const [k,v] of Object.entries(serverState)) {{
1255
1526
  if (!local[k]) local[k] = true; // 只补充,不删除
1256
1527
  }}
1257
- // 本地���也同步到服务端
1528
+ // 本地已读也同步到服务端
1258
1529
  for (const [k,v] of Object.entries(local)) {{
1259
1530
  if (!(k in serverState)) {{
1260
1531
  fetch('/api/read/toggle', {{method:'POST',headers:{{'Content-Type':'application/json'}},body:JSON.stringify({{id:k}})}}).catch(()=>{{}});
@@ -1272,7 +1543,48 @@ function exportFav() {{
1272
1543
  const savedTab = sessionStorage.getItem('weflow_tab_{date_str}');
1273
1544
  const hash = window.location.hash.slice(1);
1274
1545
  switchTab(hash || savedTab || 'AI');
1546
+ initDarkMode(); initKeyboard();
1275
1547
  }})();
1548
+
1549
+ // ==== Dark Mode ====
1550
+ function initDarkMode() {{
1551
+ const toggle = document.getElementById('theme-toggle');
1552
+ if (!toggle) return;
1553
+ toggle.addEventListener('click', () => {{
1554
+ const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
1555
+ document.documentElement.setAttribute('data-theme', isDark ? '' : 'dark');
1556
+ toggle.textContent = isDark ? '\U0001f319' : '\u2600\ufe0f';
1557
+ localStorage.setItem('theme', isDark ? 'light' : 'dark');
1558
+ }});
1559
+ const saved = localStorage.getItem('theme');
1560
+ if (saved === 'dark') {{
1561
+ document.documentElement.setAttribute('data-theme', 'dark');
1562
+ toggle.textContent = '\u2600\ufe0f';
1563
+ }}
1564
+ }}
1565
+
1566
+ // ==== Keyboard Shortcuts ====
1567
+ function initKeyboard() {{
1568
+ const cards = document.querySelectorAll('.card');
1569
+ let focused = -1;
1570
+ document.addEventListener('keydown', (e) => {{
1571
+ if (e.target.closest('input, textarea, select, [contenteditable]')) return;
1572
+ const cds = document.querySelectorAll('.card');
1573
+ if (e.key === 'j' || e.key === 'J') {{ focused = Math.min(focused + 1, cds.length - 1); focusCard(cds); }}
1574
+ else if (e.key === 'k' || e.key === 'K') {{ focused = Math.max(focused - 1, 0); focusCard(cds); }}
1575
+ else if (e.key === 'Enter' && focused >= 0) {{ cds[focused]?.querySelector('a[href]')?.click(); }}
1576
+ else if (e.key === 'Escape') {{ focused = -1; focusCard(cds); document.getElementById('search-input')?.focus(); }}
1577
+ else if (e.key === 'f' || e.key === 'F') {{ const id = cds[focused]?.getAttribute('data-id'); if (id) toggleFav(id); }}
1578
+ }});
1579
+ function focusCard(cds) {{
1580
+ cds.forEach((c, i) => {{
1581
+ c.style.outline = i === focused ? '2px solid var(--accent)' : '';
1582
+ c.style.transform = i === focused ? 'scale(1.01)' : '';
1583
+ c.style.boxShadow = i === focused ? '0 4px 20px var(--accent-glow)' : '';
1584
+ if (i === focused) c.scrollIntoView({{block:'center',behavior:'smooth'}});
1585
+ }});
1586
+ }}
1587
+ }}
1276
1588
  </script>
1277
1589
  </body>
1278
1590
  </html>'''
@@ -1339,15 +1651,21 @@ def main():
1339
1651
  with open(out_path, 'w', encoding='utf-8') as f:
1340
1652
  f.write(html)
1341
1653
 
1342
- # 同时生成 article.html(Markdown 渲染阅读器)
1654
+ # 生成行动建议 HTML
1655
+ action_md_path = os.path.join(date_dir, '行动建议.md')
1656
+ action_html_path = os.path.join(date_dir, '行动建议.html')
1657
+ if os.path.exists(action_md_path):
1658
+ generate_action_html(date_str, action_md_path, action_html_path)
1659
+ print(f'✓ 行动建议 HTML 生成完成: {action_html_path}')
1660
+
1661
+ # 生成 article.html — 从永久模板复制
1343
1662
  article_html_path = os.path.join(date_dir, 'article.html')
1344
- if not os.path.exists(article_html_path):
1345
- import shutil
1346
- template = os.path.join(SOURCE_ROOT, '2026-05-19', 'article.html')
1347
- if os.path.exists(template):
1348
- shutil.copy2(template, article_html_path)
1349
- else:
1350
- generate_article_viewer(article_html_path)
1663
+ template = os.path.join(SOURCE_ROOT, '.template', 'article.html')
1664
+ if os.path.exists(template):
1665
+ shutil.copy2(template, article_html_path)
1666
+ else:
1667
+ generate_article_viewer(article_html_path)
1668
+ _inline_marked_js(article_html_path)
1351
1669
 
1352
1670
  # 注入本地图片映射,优先用本地图片避免代理延迟
1353
1671
  image_map_path = os.path.join(date_dir, '.image_map.json')
@@ -1357,7 +1675,9 @@ def main():
1357
1675
  if img_map and os.path.exists(article_html_path):
1358
1676
  with open(article_html_path, 'r', encoding='utf-8') as f:
1359
1677
  article_html = f.read()
1360
- img_map_js = '<script>window._IMG_MAP=' + json.dumps(img_map, ensure_ascii=False) + ';</script>'
1678
+ # Escape < in JSON to prevent XSS via </script> injection
1679
+ img_map_json = json.dumps(img_map, ensure_ascii=False).replace('<', '\\u003c')
1680
+ img_map_js = '<script>window._IMG_MAP=' + img_map_json + ';</script>'
1361
1681
  # 注入到 </head> 之前
1362
1682
  article_html = article_html.replace('</head>', img_map_js + '\n</head>', 1)
1363
1683
  with open(article_html_path, 'w', encoding='utf-8') as f: