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