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,297 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
知识升级管道 — 002_Literature → 003_Ideas → 004_Permanent → 008_MOC
|
|
4
|
+
|
|
5
|
+
用法:
|
|
6
|
+
python scripts/promote_ideas.py
|
|
7
|
+
python scripts/promote_ideas.py --api-key <key>
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import sys, os, re, json
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from collections import Counter, defaultdict
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
|
|
15
|
+
SCRIPTS_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
16
|
+
PROJECT_ROOT = os.path.dirname(SCRIPTS_DIR)
|
|
17
|
+
DEFAULT_VAULT = os.path.join(PROJECT_ROOT, 'output', 'wechat-vault')
|
|
18
|
+
|
|
19
|
+
sys.path.insert(0, SCRIPTS_DIR)
|
|
20
|
+
from _utils import call_deepseek
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def collect_metadata(vault_path: str) -> list[dict]:
|
|
24
|
+
"""收集所有 002_Literature 笔记的元数据。"""
|
|
25
|
+
lit_dir = Path(vault_path) / '002_Literature' / 'WeChat'
|
|
26
|
+
articles = []
|
|
27
|
+
for date_dir in sorted(lit_dir.iterdir()):
|
|
28
|
+
if not date_dir.is_dir():
|
|
29
|
+
continue
|
|
30
|
+
for note_file in date_dir.glob('*.md'):
|
|
31
|
+
try:
|
|
32
|
+
content = note_file.read_text(encoding='utf-8')
|
|
33
|
+
except Exception:
|
|
34
|
+
continue
|
|
35
|
+
# 提取 frontmatter
|
|
36
|
+
fm = {}
|
|
37
|
+
if content.startswith('---'):
|
|
38
|
+
end = content.find('---', 3)
|
|
39
|
+
if end != -1:
|
|
40
|
+
for line in content[3:end].split('\n'):
|
|
41
|
+
if ':' in line:
|
|
42
|
+
k, _, v = line.partition(':')
|
|
43
|
+
k = k.strip()
|
|
44
|
+
v = v.strip().strip('"').strip("'")
|
|
45
|
+
fm[k] = v
|
|
46
|
+
|
|
47
|
+
# 提取概念
|
|
48
|
+
concept_match = re.findall(r'## 关联网络.*?##', content, re.DOTALL)
|
|
49
|
+
concepts = []
|
|
50
|
+
if concept_match:
|
|
51
|
+
concepts = re.findall(r'\[\[([^\]]+)\]\]', concept_match[0])
|
|
52
|
+
|
|
53
|
+
# 提取标签
|
|
54
|
+
tags = []
|
|
55
|
+
tag_match = re.search(r'tags:\s*\[(.+?)\]', content)
|
|
56
|
+
if tag_match:
|
|
57
|
+
tags = [t.strip() for t in tag_match.group(1).split(',')]
|
|
58
|
+
|
|
59
|
+
articles.append({
|
|
60
|
+
'title': fm.get('title', note_file.stem),
|
|
61
|
+
'source': fm.get('source', ''),
|
|
62
|
+
'date': date_dir.name,
|
|
63
|
+
'topic': fm.get('topic', ''),
|
|
64
|
+
'hasTopic': fm.get('hasTopic', ''),
|
|
65
|
+
'concepts': concepts,
|
|
66
|
+
'tags': tags,
|
|
67
|
+
'path': note_file,
|
|
68
|
+
})
|
|
69
|
+
return articles
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def generate_ideas(articles: list[dict], api_key: str, vault_path: str) -> int:
|
|
73
|
+
"""AI 分析文章,提取跨领域研究想法 → 003_Ideas。"""
|
|
74
|
+
# 按主题分组,提取代表性样本
|
|
75
|
+
by_topic = defaultdict(list)
|
|
76
|
+
for a in articles:
|
|
77
|
+
topic = a['hasTopic'].replace('[[', '').replace(']]', '') or a.get('topic', '其他')
|
|
78
|
+
by_topic[topic].append(a)
|
|
79
|
+
|
|
80
|
+
ideas_dir = Path(vault_path) / '003_Ideas'
|
|
81
|
+
ideas_dir.mkdir(parents=True, exist_ok=True)
|
|
82
|
+
created = 0
|
|
83
|
+
|
|
84
|
+
for topic, arts in by_topic.items():
|
|
85
|
+
if len(arts) < 5:
|
|
86
|
+
continue
|
|
87
|
+
# 取代表性文章(高概念数的)
|
|
88
|
+
samples = sorted(arts, key=lambda a: len(a.get('concepts', [])), reverse=True)[:10]
|
|
89
|
+
titles = '\n'.join(f'- {a["title"][:60]} (来源: {a["source"]})' for a in samples[:8])
|
|
90
|
+
|
|
91
|
+
# 收集热门概念
|
|
92
|
+
all_concepts = []
|
|
93
|
+
for a in arts:
|
|
94
|
+
all_concepts.extend(a.get('concepts', []))
|
|
95
|
+
top_concepts = [c for c, _ in Counter(all_concepts).most_common(8) if len(c) > 2]
|
|
96
|
+
|
|
97
|
+
concepts_str = ', '.join(top_concepts[:8]) if top_concepts else '无'
|
|
98
|
+
|
|
99
|
+
prompt = f'''你是一个研究助理。分析以下 {topic} 主题的文章列表,从中提炼 1-2 个研究想法。
|
|
100
|
+
|
|
101
|
+
文章列表(共 {len(arts)} 篇,样本 {len(samples)} 篇):
|
|
102
|
+
{titles}
|
|
103
|
+
|
|
104
|
+
热门概念:{concepts_str}
|
|
105
|
+
|
|
106
|
+
请为每个想法生成以下格式(200字内/想法):
|
|
107
|
+
|
|
108
|
+
## 想法1: [标题]
|
|
109
|
+
- **背景**: 一句话描述
|
|
110
|
+
- **核心思路**: 2-3句
|
|
111
|
+
- **相关文章**: 从样本列表中引用2-3篇
|
|
112
|
+
- **可行性**: 高/中/低
|
|
113
|
+
|
|
114
|
+
只返回想法,不要其他内容。'''
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
result = call_deepseek(prompt, api_key, max_tokens=500)
|
|
118
|
+
except Exception as e:
|
|
119
|
+
print(f' [WARN] {topic} AI 失败: {e}')
|
|
120
|
+
continue
|
|
121
|
+
|
|
122
|
+
idea_name = f'idea-{topic}-研究想法.md'
|
|
123
|
+
idea_path = ideas_dir / idea_name
|
|
124
|
+
if idea_path.exists():
|
|
125
|
+
continue
|
|
126
|
+
|
|
127
|
+
idea_content = f'''---
|
|
128
|
+
title: "{topic} 研究想法"
|
|
129
|
+
created: {datetime.now().strftime('%Y-%m-%d')}
|
|
130
|
+
type: idea
|
|
131
|
+
hasTopic: [[{topic}]]
|
|
132
|
+
tags: [idea, {topic.lower()}, research]
|
|
133
|
+
inspired_by: [{len(arts)} articles]
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
# {topic} 研究想法
|
|
137
|
+
|
|
138
|
+
> 基于 {len(arts)} 篇 {topic} 文章自动提炼
|
|
139
|
+
|
|
140
|
+
{result}
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## 相关文章
|
|
145
|
+
|
|
146
|
+
'''
|
|
147
|
+
for a in arts[:15]:
|
|
148
|
+
idea_content += f'- [[{a["date"]}/{a["path"].name}|{a["title"][:50]}]]\n'
|
|
149
|
+
|
|
150
|
+
idea_content += f'''
|
|
151
|
+
|
|
152
|
+
## 热门概念
|
|
153
|
+
|
|
154
|
+
'''
|
|
155
|
+
for c in top_concepts[:10]:
|
|
156
|
+
idea_content += f'- [[{c}]]\n'
|
|
157
|
+
|
|
158
|
+
idea_path.write_text(idea_content, encoding='utf-8')
|
|
159
|
+
created += 1
|
|
160
|
+
print(f' [{topic}] → {idea_name}')
|
|
161
|
+
|
|
162
|
+
return created
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def generate_moc(articles: list[dict], vault_path: str) -> int:
|
|
166
|
+
"""为每个主题生成 Map of Content → 008_MOC。"""
|
|
167
|
+
by_topic = defaultdict(list)
|
|
168
|
+
for a in articles:
|
|
169
|
+
topic = a['hasTopic'].replace('[[', '').replace(']]', '') or a.get('topic', '其他')
|
|
170
|
+
by_topic[topic].append(a)
|
|
171
|
+
|
|
172
|
+
moc_dir = Path(vault_path) / '008_MOC'
|
|
173
|
+
moc_dir.mkdir(parents=True, exist_ok=True)
|
|
174
|
+
created = 0
|
|
175
|
+
|
|
176
|
+
for topic, arts in by_topic.items():
|
|
177
|
+
if len(arts) < 5:
|
|
178
|
+
continue
|
|
179
|
+
|
|
180
|
+
# 按日期分组
|
|
181
|
+
by_date = defaultdict(list)
|
|
182
|
+
for a in arts:
|
|
183
|
+
by_date[a['date']].append(a)
|
|
184
|
+
|
|
185
|
+
# 收集所有概念
|
|
186
|
+
all_concepts = []
|
|
187
|
+
for a in arts:
|
|
188
|
+
all_concepts.extend(a.get('concepts', []))
|
|
189
|
+
top_concepts = Counter(all_concepts).most_common(15)
|
|
190
|
+
|
|
191
|
+
moc_path = moc_dir / f'MOC-{topic}.md'
|
|
192
|
+
content = f'''---
|
|
193
|
+
title: "{topic} — 主题导航"
|
|
194
|
+
created: {datetime.now().strftime('%Y-%m-%d')}
|
|
195
|
+
type: moc
|
|
196
|
+
hasTopic: [[{topic}]]
|
|
197
|
+
tags: [moc, {topic.lower()}]
|
|
198
|
+
article_count: {len(arts)}
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
# {topic} — 主题导航地图
|
|
202
|
+
|
|
203
|
+
> {len(arts)} 篇文章 · 覆盖 {len(by_date)} 天
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## 📅 按日期
|
|
208
|
+
|
|
209
|
+
'''
|
|
210
|
+
for date_str in sorted(by_date.keys(), reverse=True):
|
|
211
|
+
day_arts = by_date[date_str]
|
|
212
|
+
content += f'\n### {date_str} ({len(day_arts)} 篇)\n'
|
|
213
|
+
for a in day_arts[:8]:
|
|
214
|
+
content += f'- [[{a["date"]}/{a["path"].name}|{a["title"][:60]}]] — {a["source"]}\n'
|
|
215
|
+
if len(day_arts) > 8:
|
|
216
|
+
content += f'- ... 等 {len(day_arts)} 篇\n'
|
|
217
|
+
|
|
218
|
+
content += f'''
|
|
219
|
+
|
|
220
|
+
## 🔥 热门概念
|
|
221
|
+
|
|
222
|
+
'''
|
|
223
|
+
for c, count in top_concepts:
|
|
224
|
+
content += f'- [[{c}]] ({count} 次)\n'
|
|
225
|
+
|
|
226
|
+
content += f'''
|
|
227
|
+
|
|
228
|
+
## 🔗 相关 MOC
|
|
229
|
+
|
|
230
|
+
'''
|
|
231
|
+
for other in by_topic.keys():
|
|
232
|
+
if other != topic:
|
|
233
|
+
content += f'- [[MOC-{other}]]\n'
|
|
234
|
+
|
|
235
|
+
content += f'''
|
|
236
|
+
|
|
237
|
+
## 📊 统计
|
|
238
|
+
|
|
239
|
+
```dataview
|
|
240
|
+
TABLE date, source, rating
|
|
241
|
+
FROM "002_Literature"
|
|
242
|
+
WHERE contains(hasTopic, "{topic}")
|
|
243
|
+
SORT date DESC
|
|
244
|
+
```
|
|
245
|
+
'''
|
|
246
|
+
|
|
247
|
+
moc_path.write_text(content, encoding='utf-8')
|
|
248
|
+
created += 1
|
|
249
|
+
print(f' [{topic}] → MOC-{topic}.md ({len(arts)} articles)')
|
|
250
|
+
|
|
251
|
+
return created
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def main():
|
|
255
|
+
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
|
256
|
+
import argparse
|
|
257
|
+
parser = argparse.ArgumentParser(description='知识升级管道')
|
|
258
|
+
parser.add_argument('--vault', default=DEFAULT_VAULT, help='Vault 路径')
|
|
259
|
+
parser.add_argument('--api-key', default='', help='DeepSeek API key')
|
|
260
|
+
parser.add_argument('--skip-ai', action='store_true', help='跳过 AI 部分(仅生成 MOC)')
|
|
261
|
+
args = parser.parse_args()
|
|
262
|
+
|
|
263
|
+
api_key = args.api_key or os.environ.get('DEEPSEEK_API_KEY', '')
|
|
264
|
+
|
|
265
|
+
print('📊 收集文章元数据...')
|
|
266
|
+
articles = collect_metadata(args.vault)
|
|
267
|
+
print(f' 共 {len(articles)} 篇文章')
|
|
268
|
+
|
|
269
|
+
# 统计
|
|
270
|
+
by_topic = defaultdict(int)
|
|
271
|
+
for a in articles:
|
|
272
|
+
topic = a['hasTopic'].replace('[[', '').replace(']]', '') or a.get('topic', '其他')
|
|
273
|
+
by_topic[topic] += 1
|
|
274
|
+
print(f' 主题分布: {dict(by_topic)}')
|
|
275
|
+
|
|
276
|
+
# Step 1: MOC(不需要 AI)
|
|
277
|
+
print('\n🗺️ 生成 008_MOC 主题导航...')
|
|
278
|
+
moc_count = generate_moc(articles, args.vault)
|
|
279
|
+
|
|
280
|
+
# Step 2: Ideas(需要 AI)
|
|
281
|
+
if not args.skip_ai and api_key:
|
|
282
|
+
print('\n💡 生成 003_Ideas 研究想法...')
|
|
283
|
+
idea_count = generate_ideas(articles, api_key, args.vault)
|
|
284
|
+
print(f'\n✓ 完成: {moc_count} MOC + {idea_count} Ideas')
|
|
285
|
+
else:
|
|
286
|
+
idea_count = 0
|
|
287
|
+
print(f'\n✓ 完成: {moc_count} MOC (跳过 AI)')
|
|
288
|
+
|
|
289
|
+
if not api_key and not args.skip_ai:
|
|
290
|
+
print(' 提示: 使用 --api-key 或设置 DEEPSEEK_API_KEY 以启用 AI 想法生成')
|
|
291
|
+
|
|
292
|
+
total = moc_count + idea_count
|
|
293
|
+
print(f' 共生成 {total} 个文件')
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
if __name__ == '__main__':
|
|
297
|
+
main()
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
RAG 智能聊天助手 — 基于语义搜索 + AI 引擎的对话式知识检索。
|
|
4
|
+
|
|
5
|
+
用法:
|
|
6
|
+
# 单次提问
|
|
7
|
+
python scripts/rag_chat.py "上周和张三聊了什么"
|
|
8
|
+
python scripts/rag_chat.py "帮我总结最近 AI 动态" --top-k 8
|
|
9
|
+
python scripts/rag_chat.py "WRF 相关的讨论" --talker "项目群"
|
|
10
|
+
|
|
11
|
+
# 交互模式(多轮对话)
|
|
12
|
+
python scripts/rag_chat.py --interactive
|
|
13
|
+
|
|
14
|
+
配置:
|
|
15
|
+
API key 从 ~/.weflow-cli/config.json 读取(deepseekApiKey / dashscopeApiKey)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import sys, os, json
|
|
19
|
+
|
|
20
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
21
|
+
from _utils import load_config, create_engine
|
|
22
|
+
from semantic_search import search as semantic_search, build_index, INDEX_DIR, VECTORS_FILE
|
|
23
|
+
|
|
24
|
+
PROMPT_TEMPLATE = """你是一个个人知识助手,可以访问用户的微信聊天记录和公众号文章。
|
|
25
|
+
|
|
26
|
+
基于以下检索到的相关信息回答问题。如果信息不足以回答,请明确说明,不要编造。
|
|
27
|
+
|
|
28
|
+
## 相关信息
|
|
29
|
+
|
|
30
|
+
{context}
|
|
31
|
+
|
|
32
|
+
## 用户问题
|
|
33
|
+
|
|
34
|
+
{question}
|
|
35
|
+
|
|
36
|
+
请用中文简洁回答,在关键信息处引用来源编号(如 [1]、[2])。"""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def format_context(results: list[dict]) -> str:
|
|
40
|
+
"""将搜索结果格式化为 RAG context。"""
|
|
41
|
+
chunks = []
|
|
42
|
+
for i, item in enumerate(results, 1):
|
|
43
|
+
if item.get('type') == 'article':
|
|
44
|
+
chunks.append(
|
|
45
|
+
f"[{i}] 📄 公众号文章 | {item.get('date', '?')} | {item.get('topic', '')}\n"
|
|
46
|
+
f"标题: {item.get('title', '')}\n"
|
|
47
|
+
f"内容: {item.get('text', '')[:500]}"
|
|
48
|
+
)
|
|
49
|
+
elif item.get('type') == 'chat':
|
|
50
|
+
chunks.append(
|
|
51
|
+
f"[{i}] 💬 {item.get('talker', '?')} | {item.get('time', '?')}\n"
|
|
52
|
+
f"内容: {item.get('text', '')[:500]}"
|
|
53
|
+
)
|
|
54
|
+
else:
|
|
55
|
+
chunks.append(
|
|
56
|
+
f"[{i}] 内容: {item.get('text', str(item)[:500])}"
|
|
57
|
+
)
|
|
58
|
+
return '\n\n'.join(chunks) if chunks else '(未找到相关信息)'
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def build_prompt(question: str, results: list[dict], history: list[dict] = None) -> str:
|
|
62
|
+
"""构建 RAG prompt。"""
|
|
63
|
+
context = format_context(results)
|
|
64
|
+
|
|
65
|
+
if history and len(history) > 0:
|
|
66
|
+
history_text = '\n'.join(
|
|
67
|
+
f"用户: {h['question']}\n助手: {h['answer']}"
|
|
68
|
+
for h in history[-6:] # 最近 3 轮
|
|
69
|
+
)
|
|
70
|
+
context = f"## 对话历史\n\n{history_text}\n\n## 当前检索结果\n\n{context}"
|
|
71
|
+
|
|
72
|
+
return PROMPT_TEMPLATE.format(context=context, question=question)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def query_rag(question: str, embed_key: str, chat_key: str, top_k: int = 10,
|
|
76
|
+
talker: str = None, history: list[dict] = None) -> dict:
|
|
77
|
+
"""执行一次 RAG 查询。"""
|
|
78
|
+
# 1. 语义检索
|
|
79
|
+
results = semantic_search(question, embed_key, top_k=top_k)
|
|
80
|
+
|
|
81
|
+
# 2. 过滤 talker(如果指定)
|
|
82
|
+
if talker:
|
|
83
|
+
results = [r for r in results if talker.lower() in (r.get('talker', '') + r.get('title', '')).lower()]
|
|
84
|
+
if not results:
|
|
85
|
+
return {
|
|
86
|
+
'answer': f'未找到与 "{talker}" 相关的对话或文章。',
|
|
87
|
+
'sources': [],
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# 3. 构建 prompt
|
|
91
|
+
prompt = build_prompt(question, results, history)
|
|
92
|
+
|
|
93
|
+
# 4. 调用 AI
|
|
94
|
+
config = load_config()
|
|
95
|
+
engine_type = config.get('aiEngine', 'deepseek')
|
|
96
|
+
engine = create_engine(engine_type, chat_key)
|
|
97
|
+
answer = engine.chat(prompt, max_tokens=1500)
|
|
98
|
+
|
|
99
|
+
# 5. 格式化来源
|
|
100
|
+
sources = []
|
|
101
|
+
for i, item in enumerate(results[:5], 1):
|
|
102
|
+
if item.get('type') == 'chat':
|
|
103
|
+
sources.append(f"[{i}] {item.get('talker', '?')} ({item.get('time', '?')})")
|
|
104
|
+
else:
|
|
105
|
+
sources.append(f"[{i}] {item.get('title', item.get('id', '?'))[:60]}")
|
|
106
|
+
|
|
107
|
+
return {'answer': answer, 'sources': sources}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def interactive_mode(embed_key: str, chat_key: str):
|
|
111
|
+
"""交互式多轮对话。"""
|
|
112
|
+
config = load_config()
|
|
113
|
+
engine_type = config.get('aiEngine', 'deepseek')
|
|
114
|
+
|
|
115
|
+
print(f'\n🤖 RAG 智能助手 ({engine_type})')
|
|
116
|
+
print(' 输入问题开始对话,输入 /help 查看帮助,输入 /exit 退出\n')
|
|
117
|
+
|
|
118
|
+
history: list[dict] = []
|
|
119
|
+
|
|
120
|
+
while True:
|
|
121
|
+
try:
|
|
122
|
+
question = input('🔍 你: ').strip()
|
|
123
|
+
except (EOFError, KeyboardInterrupt):
|
|
124
|
+
print('\n👋 再见!')
|
|
125
|
+
break
|
|
126
|
+
|
|
127
|
+
if not question:
|
|
128
|
+
continue
|
|
129
|
+
|
|
130
|
+
if question == '/exit':
|
|
131
|
+
print('👋 再见!')
|
|
132
|
+
break
|
|
133
|
+
if question == '/help':
|
|
134
|
+
print('命令: /exit 退出, /clear 清除历史, /rebuild 重建索引\n')
|
|
135
|
+
continue
|
|
136
|
+
if question == '/clear':
|
|
137
|
+
history = []
|
|
138
|
+
print('✓ 对话历史已清除\n')
|
|
139
|
+
continue
|
|
140
|
+
if question == '/rebuild':
|
|
141
|
+
print('重建索引中...')
|
|
142
|
+
result = build_index(embed_key, full=False)
|
|
143
|
+
print(f'✓ {result}\n')
|
|
144
|
+
continue
|
|
145
|
+
|
|
146
|
+
result = query_rag(question, embed_key, chat_key, top_k=10, history=history)
|
|
147
|
+
history.append({'question': question, 'answer': result['answer']})
|
|
148
|
+
|
|
149
|
+
print(f'\n🤖 助手:\n{result["answer"]}')
|
|
150
|
+
if result.get('sources'):
|
|
151
|
+
print(f"\n📎 来源: {' | '.join(result['sources'])}")
|
|
152
|
+
print()
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def main():
|
|
156
|
+
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
|
157
|
+
import argparse
|
|
158
|
+
parser = argparse.ArgumentParser(description='RAG 智能聊天助手')
|
|
159
|
+
parser.add_argument('question', nargs='?', help='要问的问题')
|
|
160
|
+
parser.add_argument('--interactive', '-i', action='store_true', help='交互模式')
|
|
161
|
+
parser.add_argument('--top-k', type=int, default=10, help='检索数量(默认 10)')
|
|
162
|
+
parser.add_argument('--talker', help='限定联系人/群聊')
|
|
163
|
+
parser.add_argument('--api-key', help='DeepSeek API key(优先从 config 读取)')
|
|
164
|
+
parser.add_argument('--json', action='store_true', help='JSON 输出')
|
|
165
|
+
args = parser.parse_args()
|
|
166
|
+
|
|
167
|
+
config = load_config()
|
|
168
|
+
embed_key = os.environ.get('DASHSCOPE_API_KEY', '') or config.get('dashscopeApiKey', '')
|
|
169
|
+
chat_key = args.api_key or os.environ.get('DEEPSEEK_API_KEY', '') or config.get('deepseekApiKey', '')
|
|
170
|
+
|
|
171
|
+
if not embed_key:
|
|
172
|
+
print('[ERROR] 缺少 Embedding API key。请在 ~/.weflow-cli/config.json 中设置 dashscopeApiKey')
|
|
173
|
+
sys.exit(1)
|
|
174
|
+
if not chat_key:
|
|
175
|
+
print('[ERROR] 缺少 AI API key。请在 ~/.weflow-cli/config.json 中设置 deepseekApiKey')
|
|
176
|
+
sys.exit(1)
|
|
177
|
+
|
|
178
|
+
if args.interactive or not args.question:
|
|
179
|
+
interactive_mode(embed_key, chat_key)
|
|
180
|
+
else:
|
|
181
|
+
result = query_rag(args.question, embed_key, chat_key, top_k=args.top_k, talker=args.talker)
|
|
182
|
+
if args.json:
|
|
183
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
184
|
+
else:
|
|
185
|
+
print(f'\n{result["answer"]}')
|
|
186
|
+
if result.get('sources'):
|
|
187
|
+
print(f"\n📎 来源: {' | '.join(result['sources'])}")
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
if __name__ == '__main__':
|
|
191
|
+
main()
|