weflow-cli 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +149 -0
- package/bin/weflow-cli-electron.cjs +59 -0
- package/bin/weflow-cli.ts +2311 -0
- package/cli.cjs +3 -0
- package/dist/bin/weflow-cli.d.ts +3 -0
- package/dist/bin/weflow-cli.d.ts.map +1 -0
- package/dist/bin/weflow-cli.js +2270 -0
- package/dist/bin/weflow-cli.js.map +1 -0
- package/dist/mcp-server/index.d.ts +3 -0
- package/dist/mcp-server/index.d.ts.map +1 -0
- package/dist/mcp-server/index.js +690 -0
- package/dist/mcp-server/index.js.map +1 -0
- package/dist/src/core/dbPathService.d.ts +40 -0
- package/dist/src/core/dbPathService.d.ts.map +1 -0
- package/dist/src/core/dbPathService.js +324 -0
- package/dist/src/core/dbPathService.js.map +1 -0
- package/dist/src/core/keyService.d.ts +59 -0
- package/dist/src/core/keyService.d.ts.map +1 -0
- package/dist/src/core/keyService.js +368 -0
- package/dist/src/core/keyService.js.map +1 -0
- package/dist/src/core/ntCore.d.ts +62 -0
- package/dist/src/core/ntCore.d.ts.map +1 -0
- package/dist/src/core/ntCore.js +191 -0
- package/dist/src/core/ntCore.js.map +1 -0
- package/dist/src/core/sqlcipherCore.d.ts +119 -0
- package/dist/src/core/sqlcipherCore.d.ts.map +1 -0
- package/dist/src/core/sqlcipherCore.js +693 -0
- package/dist/src/core/sqlcipherCore.js.map +1 -0
- package/dist/src/core/wcdbCore.d.ts +738 -0
- package/dist/src/core/wcdbCore.d.ts.map +1 -0
- package/dist/src/core/wcdbCore.js +4545 -0
- package/dist/src/core/wcdbCore.js.map +1 -0
- package/dist/src/core/wechatClient.d.ts +23 -0
- package/dist/src/core/wechatClient.d.ts.map +1 -0
- package/dist/src/core/wechatClient.js +166 -0
- package/dist/src/core/wechatClient.js.map +1 -0
- package/dist/src/services/chatService.d.ts +25 -0
- package/dist/src/services/chatService.d.ts.map +1 -0
- package/dist/src/services/chatService.js +291 -0
- package/dist/src/services/chatService.js.map +1 -0
- package/dist/src/services/configService.d.ts +43 -0
- package/dist/src/services/configService.d.ts.map +1 -0
- package/dist/src/services/configService.js +156 -0
- package/dist/src/services/configService.js.map +1 -0
- package/dist/src/services/exportService.d.ts +30 -0
- package/dist/src/services/exportService.d.ts.map +1 -0
- package/dist/src/services/exportService.js +273 -0
- package/dist/src/services/exportService.js.map +1 -0
- package/dist/src/services/wechat-formatter.d.ts +69 -0
- package/dist/src/services/wechat-formatter.d.ts.map +1 -0
- package/dist/src/services/wechat-formatter.js +248 -0
- package/dist/src/services/wechat-formatter.js.map +1 -0
- package/dist/src/services/wechatMessageService.d.ts +28 -0
- package/dist/src/services/wechatMessageService.d.ts.map +1 -0
- package/dist/src/services/wechatMessageService.js +341 -0
- package/dist/src/services/wechatMessageService.js.map +1 -0
- package/dist/src/services/wereadService.d.ts +206 -0
- package/dist/src/services/wereadService.d.ts.map +1 -0
- package/dist/src/services/wereadService.js +145 -0
- package/dist/src/services/wereadService.js.map +1 -0
- package/dist/src/services/whitelistService.d.ts +20 -0
- package/dist/src/services/whitelistService.d.ts.map +1 -0
- package/dist/src/services/whitelistService.js +60 -0
- package/dist/src/services/whitelistService.js.map +1 -0
- package/dist/src/types.d.ts +140 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/utils/errors.d.ts +25 -0
- package/dist/src/utils/errors.d.ts.map +1 -0
- package/dist/src/utils/errors.js +43 -0
- package/dist/src/utils/errors.js.map +1 -0
- package/dist/src/utils/pathUtils.d.ts +5 -0
- package/dist/src/utils/pathUtils.d.ts.map +1 -0
- package/dist/src/utils/pathUtils.js +16 -0
- package/dist/src/utils/pathUtils.js.map +1 -0
- package/dist/src/utils/pythonRunner.d.ts +27 -0
- package/dist/src/utils/pythonRunner.d.ts.map +1 -0
- package/dist/src/utils/pythonRunner.js +62 -0
- package/dist/src/utils/pythonRunner.js.map +1 -0
- package/dist/src/utils/talkerUtils.d.ts +14 -0
- package/dist/src/utils/talkerUtils.d.ts.map +1 -0
- package/dist/src/utils/talkerUtils.js +73 -0
- package/dist/src/utils/talkerUtils.js.map +1 -0
- package/mcp-server/index.ts +757 -0
- package/package.json +72 -0
- package/resources/key/win32/x64/wx_key.dll +0 -0
- package/resources/wcdb/win32/x64/SDL2.dll +0 -0
- package/resources/wcdb/win32/x64/WCDB.dll +0 -0
- package/resources/wcdb/win32/x64/msvcp140.dll +0 -0
- package/resources/wcdb/win32/x64/msvcp140_1.dll +0 -0
- package/resources/wcdb/win32/x64/vcruntime140.dll +0 -0
- package/resources/wcdb/win32/x64/vcruntime140_1.dll +0 -0
- package/resources/wcdb/win32/x64/wcdb_api.dll +0 -0
- package/scripts/_batch_link_sources.py +73 -0
- package/scripts/_utils.py +531 -0
- package/scripts/annual_report.py +796 -0
- package/scripts/auto_tag.py +128 -0
- package/scripts/biz_daily.py +896 -0
- package/scripts/chat_report.py +397 -0
- package/scripts/chat_stats.py +702 -0
- package/scripts/classify_daily.py +411 -0
- package/scripts/compile_wiki.py +258 -0
- package/scripts/create_reading_notes.py +370 -0
- package/scripts/enrich_backlinks.py +163 -0
- package/scripts/export_chat_html.py +766 -0
- package/scripts/extract_3x_key.py +86 -0
- package/scripts/extract_todos.py +404 -0
- package/scripts/fav_server.py +392 -0
- package/scripts/fix_topics.py +111 -0
- package/scripts/generate_ai_report.py +367 -0
- package/scripts/generate_html.py +1372 -0
- package/scripts/generate_review.py +170 -0
- package/scripts/mcp_bridge.py +398 -0
- package/scripts/nt_decrypt.py +562 -0
- package/scripts/pipeline.py +145 -0
- package/scripts/promote_all.py +310 -0
- package/scripts/promote_ideas.py +297 -0
- package/scripts/rag_chat.py +191 -0
- package/scripts/scan_decrypt_4x.py +329 -0
- package/scripts/semantic_search.py +431 -0
- package/scripts/sync_fav.py +146 -0
- package/scripts/sync_weread.py +192 -0
- package/scripts/vault_rag.py +139 -0
- package/scripts/vault_search.py +141 -0
- package/src/core/dbPathService.ts +348 -0
- package/src/core/keyService.ts +409 -0
- package/src/core/ntCore.ts +224 -0
- package/src/core/sqlcipherCore.ts +764 -0
- package/src/core/wcdbCore.ts +4568 -0
- package/src/core/wechatClient.ts +204 -0
- package/src/lz4.d.ts +10 -0
- package/src/services/chatService.ts +296 -0
- package/src/services/configService.ts +197 -0
- package/src/services/exportService.ts +305 -0
- package/src/services/wechat-formatter.ts +348 -0
- package/src/services/wechatMessageService.ts +383 -0
- package/src/services/wereadService.ts +308 -0
- package/src/services/whitelistService.ts +72 -0
- package/src/types.ts +141 -0
- package/src/utils/errors.ts +42 -0
- package/src/utils/pathUtils.ts +16 -0
- package/src/utils/pythonRunner.ts +81 -0
- package/src/utils/talkerUtils.ts +84 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
AI 阅读日报 — 信息密集但精炼版本。
|
|
4
|
+
|
|
5
|
+
核心目标:没时间看公众号时,用这份报告快速扫一眼即可。
|
|
6
|
+
- AI 一次调用搞定:总评 + 趋势 + 逐篇摘要 + 行动指南
|
|
7
|
+
- 按主题分组,AI 主题最详细
|
|
8
|
+
- 默认优先走本地推理(Ollama/LM Studio/Claude Code 本地服务)
|
|
9
|
+
|
|
10
|
+
用法:
|
|
11
|
+
python scripts/generate_ai_report.py # 今天(默认走本地推理)
|
|
12
|
+
python scripts/generate_ai_report.py --date 2026-06-09
|
|
13
|
+
python scripts/generate_ai_report.py --range 7 # 最近 7 天
|
|
14
|
+
python scripts/generate_ai_report.py --engine local|deepseek|claude|ollama
|
|
15
|
+
python scripts/generate_ai_report.py --dry-run # 仅文章清单,不调用 AI
|
|
16
|
+
|
|
17
|
+
输出:
|
|
18
|
+
output/ai-reports/ai-report-YYYY-MM-DD.md
|
|
19
|
+
"""
|
|
20
|
+
import sys, os, json, re, time, argparse
|
|
21
|
+
from datetime import datetime, timezone, timedelta, date
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from collections import Counter
|
|
24
|
+
|
|
25
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
26
|
+
from _utils import create_engine, parse_frontmatter
|
|
27
|
+
|
|
28
|
+
SCRIPTS_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
29
|
+
ROOT_DIR = os.path.dirname(SCRIPTS_DIR)
|
|
30
|
+
SOURCE_ROOT = os.path.join(ROOT_DIR, 'output', 'biz-daily')
|
|
31
|
+
OUTPUT_DIR = os.path.join(ROOT_DIR, 'output', 'ai-reports')
|
|
32
|
+
|
|
33
|
+
TZ = timezone(timedelta(hours=8))
|
|
34
|
+
TOPICS = ['AI', '学术', '新闻', '文学', '投资']
|
|
35
|
+
FOCUS_TOPIC = 'AI'
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ======================================================================
|
|
39
|
+
# PROMPT(信息密集精炼版,一次调用搞定)
|
|
40
|
+
# ======================================================================
|
|
41
|
+
|
|
42
|
+
REPORT_PROMPT = """你是一位为忙碌研究生写作的「深度阅读策展人」。你的任务是用**信息密集但精炼**的方式,把今天的公众号文章整理成一份有深度、有观点的日报。读者读完能清晰了解「今天发生了什么、核心观点是什么、对我有什么用」。
|
|
43
|
+
|
|
44
|
+
【读者】环境科学研究生,关注 AI 前沿 + 环境科学交叉方向(大气模拟、遥感反演、环境大数据等)。
|
|
45
|
+
|
|
46
|
+
【文章列表】
|
|
47
|
+
{articles_block}
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## 写作要求(核心)
|
|
52
|
+
|
|
53
|
+
**精炼不是简短,是信息密度高。** 每句话都要有信息量:
|
|
54
|
+
- **覆盖要全面**:尽量覆盖所有文章,至少 20 篇以上要被提及或概括
|
|
55
|
+
- **重点要突出**:对最值得关注的 10-15 篇文章展开讲清楚「是什么、为什么重要、怎么用」
|
|
56
|
+
- **其余也要有存在感**:剩下的文章用主题分类 + 一句话概括,让读者知道今天还有什么
|
|
57
|
+
- 用**段落叙述**串联信息,而不是 bullet list 罗列标题
|
|
58
|
+
- 每个观点回答**「所以呢?」**——对读者有什么启发
|
|
59
|
+
|
|
60
|
+
## 结构(Markdown,遵守标题层级)
|
|
61
|
+
|
|
62
|
+
### 今日焦点(3-5 段叙述)
|
|
63
|
+
用叙事段落讲清最重要的事。每段聚焦一个主题,把 2-4 篇相关文章串成故事线。用 **《标题》**(*来源*) 格式。深入解释核心观点、数据和意义。
|
|
64
|
+
|
|
65
|
+
### AI 工具与趋势(2-3 段)
|
|
66
|
+
今天 AI 领域的新动态。对有价值的工具/项目,讲清楚:它是什么、能做什么、和环境科学研究有什么关系、怎么入手。
|
|
67
|
+
|
|
68
|
+
### 学术前沿(2-3 段)
|
|
69
|
+
与环境科学相关的研究进展。每篇讲清:研究了什么问题、用什么方法、得出什么结论、有什么启发。
|
|
70
|
+
|
|
71
|
+
### 其他值得关注
|
|
72
|
+
用表格或分组列出今天其他值得知道的文章,每条至少写清楚核心观点。不要只写标题。
|
|
73
|
+
|
|
74
|
+
### 值得做的事(具体、可执行)
|
|
75
|
+
5-8 个具体行动。不要泛泛而谈,要说「打开 XX 试 YY」或「搜索 XX 论文看第 N 节」。
|
|
76
|
+
|
|
77
|
+
## 写作规则
|
|
78
|
+
1. 中文输出,语气专业但不僵硬
|
|
79
|
+
2. 不引入文章列表中未出现的信息
|
|
80
|
+
3. 段落为主,可适当用表格对比
|
|
81
|
+
4. Markdown,**粗体**强调关键,*斜体*标注来源
|
|
82
|
+
5. 全文 3000-5000 字,确保信息量和深度
|
|
83
|
+
6. 每个章节都要有实质内容,不要用「今天内容较少,跳过」敷衍
|
|
84
|
+
|
|
85
|
+
只返回 Markdown 正文。"""
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# ======================================================================
|
|
89
|
+
# DATA LOADING
|
|
90
|
+
# ======================================================================
|
|
91
|
+
|
|
92
|
+
def load_articles_from_date(date_str: str) -> list[dict]:
|
|
93
|
+
json_path = Path(SOURCE_ROOT) / date_str / '.articles.json'
|
|
94
|
+
if json_path.exists():
|
|
95
|
+
try:
|
|
96
|
+
with open(json_path, 'r', encoding='utf-8') as f:
|
|
97
|
+
data = json.load(f)
|
|
98
|
+
arts = data.get('articles', data) if isinstance(data, dict) else data
|
|
99
|
+
for a in arts:
|
|
100
|
+
a.setdefault('date', date_str)
|
|
101
|
+
return arts
|
|
102
|
+
except Exception:
|
|
103
|
+
pass
|
|
104
|
+
# 回退:扫描 MD 文件
|
|
105
|
+
date_dir = Path(SOURCE_ROOT) / date_str
|
|
106
|
+
if not date_dir.exists():
|
|
107
|
+
return []
|
|
108
|
+
articles = []
|
|
109
|
+
for topic in TOPICS:
|
|
110
|
+
topic_dir = date_dir / topic
|
|
111
|
+
if not topic_dir.exists():
|
|
112
|
+
continue
|
|
113
|
+
for md_file in sorted(topic_dir.glob('*.md')):
|
|
114
|
+
try:
|
|
115
|
+
content = md_file.read_text(encoding='utf-8')
|
|
116
|
+
except Exception:
|
|
117
|
+
continue
|
|
118
|
+
fm, body = parse_frontmatter(content)
|
|
119
|
+
if not fm:
|
|
120
|
+
continue
|
|
121
|
+
relevance = fm.get('relevance', '中')
|
|
122
|
+
# 非 AI 主题只收录相关度"高"的文章
|
|
123
|
+
if topic != FOCUS_TOPIC and relevance != '高':
|
|
124
|
+
continue
|
|
125
|
+
# AI 主题读取完整正文(最多 5000 字),其他主题读取摘要
|
|
126
|
+
if topic == FOCUS_TOPIC:
|
|
127
|
+
# 提取正文部分供 AI 精读
|
|
128
|
+
body_sections = body.split('## 正文')
|
|
129
|
+
if len(body_sections) > 1:
|
|
130
|
+
full_text = body_sections[1].strip() # 不限制字数,完整正文
|
|
131
|
+
else:
|
|
132
|
+
m = re.search(r'## (?:AI 摘要|深度解析)\s*\n\s*(.+?)(?=\n##|\Z)', body, re.DOTALL)
|
|
133
|
+
full_text = m.group(1).strip()[:2000] if m else body[:2000].strip()
|
|
134
|
+
summary = full_text
|
|
135
|
+
else:
|
|
136
|
+
m = re.search(r'## (?:AI 摘要|深度解析)\s*\n\s*(.+?)(?=\n##|\Z)', body, re.DOTALL)
|
|
137
|
+
summary = m.group(1).strip()[:800] if m else body[:500].strip()
|
|
138
|
+
articles.append({
|
|
139
|
+
'date': date_str,
|
|
140
|
+
'title': fm.get('title', md_file.stem).strip('"\''),
|
|
141
|
+
'source': fm.get('source', '').strip('"\''),
|
|
142
|
+
'topic': topic,
|
|
143
|
+
'relevance': relevance,
|
|
144
|
+
'tags': fm.get('tags', []),
|
|
145
|
+
'url': fm.get('url', '').strip('"\''),
|
|
146
|
+
'summary': summary,
|
|
147
|
+
})
|
|
148
|
+
return articles
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def load_articles_range(from_date: date, to_date: date) -> list[dict]:
|
|
152
|
+
all_articles = []
|
|
153
|
+
cur = from_date
|
|
154
|
+
while cur <= to_date:
|
|
155
|
+
all_articles.extend(load_articles_from_date(cur.strftime('%Y-%m-%d')))
|
|
156
|
+
cur += timedelta(days=1)
|
|
157
|
+
return all_articles
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# ======================================================================
|
|
161
|
+
# PROMPT 准备
|
|
162
|
+
# ======================================================================
|
|
163
|
+
|
|
164
|
+
def build_articles_block(articles: list[dict]) -> str:
|
|
165
|
+
lines = []
|
|
166
|
+
ordered = [FOCUS_TOPIC] + [t for t in TOPICS if t != FOCUS_TOPIC]
|
|
167
|
+
remaining = list(articles)
|
|
168
|
+
for topic in ordered:
|
|
169
|
+
group = [a for a in remaining if a.get('topic') == topic]
|
|
170
|
+
if not group:
|
|
171
|
+
continue
|
|
172
|
+
lines.append(f'【主题:{topic}】共 {len(group)} 篇')
|
|
173
|
+
for i, a in enumerate(group):
|
|
174
|
+
tags = a.get('tags') or []
|
|
175
|
+
tags_str = '、'.join(tags[:5]) if isinstance(tags, list) else str(tags)[:50]
|
|
176
|
+
summary = (a.get('summary') or '(无摘要)').strip()
|
|
177
|
+
if topic == FOCUS_TOPIC:
|
|
178
|
+
# AI 文章:给完整的正文内容,不截断
|
|
179
|
+
lines.append(
|
|
180
|
+
f' [{i+1}] 《{a.get("title","")}》'
|
|
181
|
+
f'({a.get("source","")},相关度:{a.get("relevance","中")})'
|
|
182
|
+
)
|
|
183
|
+
if tags_str:
|
|
184
|
+
lines.append(f' 标签:{tags_str}')
|
|
185
|
+
lines.append(f' 正文内容:\n{summary}')
|
|
186
|
+
lines.append('')
|
|
187
|
+
else:
|
|
188
|
+
# 非 AI 文章:摘要截断到 300 字
|
|
189
|
+
summary = re.sub(r'\n+', ' / ', summary)[:300]
|
|
190
|
+
lines.append(
|
|
191
|
+
f' [{i+1}] 《{a.get("title","")}》'
|
|
192
|
+
f'({a.get("source","")},相关度:{a.get("relevance","中")})'
|
|
193
|
+
)
|
|
194
|
+
if tags_str:
|
|
195
|
+
lines.append(f' 标签:{tags_str}')
|
|
196
|
+
lines.append(f' 摘要:{summary}')
|
|
197
|
+
lines.append('')
|
|
198
|
+
extra = [a for a in remaining if a.get('topic') not in ordered]
|
|
199
|
+
if extra:
|
|
200
|
+
lines.append(f'【主题:其他】共 {len(extra)} 篇')
|
|
201
|
+
for a in extra:
|
|
202
|
+
summary = re.sub(r'\n+', ' / ', a.get('summary') or '')[:120]
|
|
203
|
+
lines.append(f' • 《{a.get("title","")}》({a.get("source","")})')
|
|
204
|
+
lines.append(f' 摘要:{summary}')
|
|
205
|
+
return '\n'.join(lines)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
# ======================================================================
|
|
209
|
+
# REPORT 生成
|
|
210
|
+
# ======================================================================
|
|
211
|
+
|
|
212
|
+
def generate_report(articles: list[dict], engine, date_label: str,
|
|
213
|
+
dry_run: bool = False) -> str:
|
|
214
|
+
topic_counts = Counter(a.get('topic', '其他') for a in articles)
|
|
215
|
+
source_counts = Counter(a.get('source', '') for a in articles)
|
|
216
|
+
top_sources = '、'.join(f'{s}({c})' for s, c in source_counts.most_common(5))
|
|
217
|
+
ai_count = sum(1 for a in articles if a.get('topic') == FOCUS_TOPIC)
|
|
218
|
+
other_count = len(articles) - ai_count
|
|
219
|
+
|
|
220
|
+
header_lines = [
|
|
221
|
+
f'# 🤖 AI 阅读日报 — {date_label}',
|
|
222
|
+
'',
|
|
223
|
+
f'> 📖 共 **{len(articles)}** 篇 · AI **{ai_count}** · 其他 **{other_count}** ',
|
|
224
|
+
f'> 📰 来源:{top_sources} ',
|
|
225
|
+
f'> 🎯 主题:{" / ".join(f"{t}{c}" for t, c in topic_counts.most_common())}',
|
|
226
|
+
'',
|
|
227
|
+
'---',
|
|
228
|
+
'',
|
|
229
|
+
]
|
|
230
|
+
|
|
231
|
+
if not articles:
|
|
232
|
+
return '\n'.join(header_lines) + '> 暂无文章数据。请先运行 `biz_daily.py` 抓取公众号内容。\n'
|
|
233
|
+
|
|
234
|
+
# dry-run:直接输出统计骨架
|
|
235
|
+
if dry_run or engine is None:
|
|
236
|
+
body_lines = ['## 📋 文章清单(dry-run,未调用 AI 提炼)', '']
|
|
237
|
+
ordered = [FOCUS_TOPIC] + [t for t in TOPICS if t != FOCUS_TOPIC]
|
|
238
|
+
for topic in ordered:
|
|
239
|
+
group = [a for a in articles if a.get('topic') == topic]
|
|
240
|
+
if not group:
|
|
241
|
+
continue
|
|
242
|
+
body_lines.append(f'### {topic} ({len(group)} 篇)')
|
|
243
|
+
body_lines.append('')
|
|
244
|
+
for a in group:
|
|
245
|
+
s = re.sub(r'\n+', ' / ', a.get('summary') or '(无摘要)')[:160]
|
|
246
|
+
body_lines.append(
|
|
247
|
+
f'- **《{a.get("title", "")}》** — *{a.get("source", "")}* '
|
|
248
|
+
f'({a.get("date", "")})'
|
|
249
|
+
)
|
|
250
|
+
body_lines.append(f' - {s}')
|
|
251
|
+
body_lines.append('')
|
|
252
|
+
ai_body = '\n'.join(body_lines)
|
|
253
|
+
else:
|
|
254
|
+
articles_block = build_articles_block(articles)
|
|
255
|
+
print(f' 🤖 AI 提炼中({len(articles)} 篇文章,本地/云端推理中)...')
|
|
256
|
+
try:
|
|
257
|
+
ai_body = engine.chat(
|
|
258
|
+
REPORT_PROMPT.format(articles_block=articles_block),
|
|
259
|
+
max_tokens=8192,
|
|
260
|
+
)
|
|
261
|
+
except Exception as e:
|
|
262
|
+
ai_body = f'> ⚠️ AI 调用失败:{e}\n\n已输出文章骨架,请检查模型连接或使用 --dry-run。\n'
|
|
263
|
+
|
|
264
|
+
footer_lines = [
|
|
265
|
+
'',
|
|
266
|
+
'---',
|
|
267
|
+
'',
|
|
268
|
+
f'*由 weflow-cli · generate_ai_report 自动生成 · '
|
|
269
|
+
f'{datetime.now(TZ).strftime("%Y-%m-%d %H:%M")}*',
|
|
270
|
+
'',
|
|
271
|
+
]
|
|
272
|
+
|
|
273
|
+
return '\n'.join(header_lines) + ai_body.strip() + '\n' + '\n'.join(footer_lines)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
# ======================================================================
|
|
277
|
+
# MAIN
|
|
278
|
+
# ======================================================================
|
|
279
|
+
|
|
280
|
+
def main():
|
|
281
|
+
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
|
282
|
+
|
|
283
|
+
parser = argparse.ArgumentParser(description='AI 阅读日报(信息密集精炼版)')
|
|
284
|
+
parser.add_argument('--api-key', help='API key(local/ollama 不需要)')
|
|
285
|
+
parser.add_argument('--engine', default='local',
|
|
286
|
+
help='AI 引擎: local(自动检测·默认) / deepseek / claude / ollama')
|
|
287
|
+
parser.add_argument('--date', help='单日期 YYYY-MM-DD,默认今天')
|
|
288
|
+
parser.add_argument('--range', type=int, help='最近 N 天合并报告,如 --range 7')
|
|
289
|
+
parser.add_argument('--from', dest='from_date', help='起始日期 YYYY-MM-DD')
|
|
290
|
+
parser.add_argument('--to', dest='to_date', help='结束日期 YYYY-MM-DD,默认今天')
|
|
291
|
+
parser.add_argument('--output', help='输出文件路径(默认 output/ai-reports/)')
|
|
292
|
+
parser.add_argument('--dry-run', action='store_true', help='不调用 AI,仅输出统计骨架')
|
|
293
|
+
args = parser.parse_args()
|
|
294
|
+
|
|
295
|
+
# 日期解析
|
|
296
|
+
today = datetime.now(TZ).date()
|
|
297
|
+
if args.date:
|
|
298
|
+
d = datetime.strptime(args.date, '%Y-%m-%d').date()
|
|
299
|
+
from_date, to_date = d, d
|
|
300
|
+
elif args.range:
|
|
301
|
+
to_date = today
|
|
302
|
+
from_date = to_date - timedelta(days=args.range - 1)
|
|
303
|
+
elif args.from_date:
|
|
304
|
+
from_date = datetime.strptime(args.from_date, '%Y-%m-%d').date()
|
|
305
|
+
to_date = datetime.strptime(args.to_date, '%Y-%m-%d').date() if args.to_date else today
|
|
306
|
+
else:
|
|
307
|
+
from_date = to_date = today
|
|
308
|
+
|
|
309
|
+
date_label = from_date.strftime('%Y-%m-%d') if from_date == to_date else \
|
|
310
|
+
f'{from_date.strftime("%Y-%m-%d")} ~ {to_date.strftime("%Y-%m-%d")}'
|
|
311
|
+
|
|
312
|
+
print(f'=== AI 阅读日报 ===')
|
|
313
|
+
print(f'📅 范围: {date_label}')
|
|
314
|
+
print(f'🔧 引擎: {args.engine}' + (' (dry-run)' if args.dry_run else ''))
|
|
315
|
+
|
|
316
|
+
# 加载文章
|
|
317
|
+
articles = load_articles_range(from_date, to_date)
|
|
318
|
+
if not articles:
|
|
319
|
+
print(f'\n❌ 未找到 {date_label} 的文章。请先运行:')
|
|
320
|
+
print(f' python scripts/biz_daily.py')
|
|
321
|
+
sys.exit(1)
|
|
322
|
+
|
|
323
|
+
ai_count = sum(1 for a in articles if a.get('topic') == FOCUS_TOPIC)
|
|
324
|
+
topic_counts = Counter(a.get('topic', '其他') for a in articles)
|
|
325
|
+
print(f'📖 文章: {len(articles)} 篇(AI {ai_count} · 其他 {len(articles) - ai_count})')
|
|
326
|
+
print(f'🎯 主题: {dict(topic_counts)}')
|
|
327
|
+
print(f'📰 来源: {len(set(a.get("source","") for a in articles))} 个公众号')
|
|
328
|
+
|
|
329
|
+
# 输出路径
|
|
330
|
+
Path(OUTPUT_DIR).mkdir(parents=True, exist_ok=True)
|
|
331
|
+
if args.output:
|
|
332
|
+
out_path = Path(args.output)
|
|
333
|
+
if not out_path.is_absolute():
|
|
334
|
+
out_path = Path(ROOT_DIR) / out_path
|
|
335
|
+
else:
|
|
336
|
+
out_name = f'ai-report-{from_date.strftime("%Y-%m-%d")}.md' if from_date == to_date else \
|
|
337
|
+
f'ai-report-{from_date.strftime("%Y%m%d")}-{to_date.strftime("%Y%m%d")}.md'
|
|
338
|
+
out_path = Path(OUTPUT_DIR) / out_name
|
|
339
|
+
|
|
340
|
+
# AI 引擎初始化
|
|
341
|
+
engine = None
|
|
342
|
+
if not args.dry_run:
|
|
343
|
+
api_key = args.api_key or ''
|
|
344
|
+
if args.engine not in ('local', 'ollama') and not api_key:
|
|
345
|
+
api_key = os.environ.get('DEEPSEEK_API_KEY', '')
|
|
346
|
+
try:
|
|
347
|
+
engine = create_engine(args.engine, api_key)
|
|
348
|
+
except (ValueError, RuntimeError) as e:
|
|
349
|
+
print(f'\n[ERROR] {e}')
|
|
350
|
+
print(' 或者使用 --dry-run 直接输出文章清单。')
|
|
351
|
+
sys.exit(1)
|
|
352
|
+
|
|
353
|
+
# 生成报告
|
|
354
|
+
t0 = time.time()
|
|
355
|
+
report = generate_report(articles, engine, date_label, dry_run=args.dry_run)
|
|
356
|
+
elapsed = time.time() - t0
|
|
357
|
+
|
|
358
|
+
with open(out_path, 'w', encoding='utf-8') as f:
|
|
359
|
+
f.write(report)
|
|
360
|
+
size_kb = len(report.encode('utf-8')) / 1024
|
|
361
|
+
|
|
362
|
+
print(f'\n✅ 报告已生成: {out_path}')
|
|
363
|
+
print(f' 大小: {size_kb:.0f} KB · 耗时: {elapsed:.0f}s')
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
if __name__ == '__main__':
|
|
367
|
+
main()
|