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.
Files changed (145) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +149 -0
  3. package/bin/weflow-cli-electron.cjs +59 -0
  4. package/bin/weflow-cli.ts +2311 -0
  5. package/cli.cjs +3 -0
  6. package/dist/bin/weflow-cli.d.ts +3 -0
  7. package/dist/bin/weflow-cli.d.ts.map +1 -0
  8. package/dist/bin/weflow-cli.js +2270 -0
  9. package/dist/bin/weflow-cli.js.map +1 -0
  10. package/dist/mcp-server/index.d.ts +3 -0
  11. package/dist/mcp-server/index.d.ts.map +1 -0
  12. package/dist/mcp-server/index.js +690 -0
  13. package/dist/mcp-server/index.js.map +1 -0
  14. package/dist/src/core/dbPathService.d.ts +40 -0
  15. package/dist/src/core/dbPathService.d.ts.map +1 -0
  16. package/dist/src/core/dbPathService.js +324 -0
  17. package/dist/src/core/dbPathService.js.map +1 -0
  18. package/dist/src/core/keyService.d.ts +59 -0
  19. package/dist/src/core/keyService.d.ts.map +1 -0
  20. package/dist/src/core/keyService.js +368 -0
  21. package/dist/src/core/keyService.js.map +1 -0
  22. package/dist/src/core/ntCore.d.ts +62 -0
  23. package/dist/src/core/ntCore.d.ts.map +1 -0
  24. package/dist/src/core/ntCore.js +191 -0
  25. package/dist/src/core/ntCore.js.map +1 -0
  26. package/dist/src/core/sqlcipherCore.d.ts +119 -0
  27. package/dist/src/core/sqlcipherCore.d.ts.map +1 -0
  28. package/dist/src/core/sqlcipherCore.js +693 -0
  29. package/dist/src/core/sqlcipherCore.js.map +1 -0
  30. package/dist/src/core/wcdbCore.d.ts +738 -0
  31. package/dist/src/core/wcdbCore.d.ts.map +1 -0
  32. package/dist/src/core/wcdbCore.js +4545 -0
  33. package/dist/src/core/wcdbCore.js.map +1 -0
  34. package/dist/src/core/wechatClient.d.ts +23 -0
  35. package/dist/src/core/wechatClient.d.ts.map +1 -0
  36. package/dist/src/core/wechatClient.js +166 -0
  37. package/dist/src/core/wechatClient.js.map +1 -0
  38. package/dist/src/services/chatService.d.ts +25 -0
  39. package/dist/src/services/chatService.d.ts.map +1 -0
  40. package/dist/src/services/chatService.js +291 -0
  41. package/dist/src/services/chatService.js.map +1 -0
  42. package/dist/src/services/configService.d.ts +43 -0
  43. package/dist/src/services/configService.d.ts.map +1 -0
  44. package/dist/src/services/configService.js +156 -0
  45. package/dist/src/services/configService.js.map +1 -0
  46. package/dist/src/services/exportService.d.ts +30 -0
  47. package/dist/src/services/exportService.d.ts.map +1 -0
  48. package/dist/src/services/exportService.js +273 -0
  49. package/dist/src/services/exportService.js.map +1 -0
  50. package/dist/src/services/wechat-formatter.d.ts +69 -0
  51. package/dist/src/services/wechat-formatter.d.ts.map +1 -0
  52. package/dist/src/services/wechat-formatter.js +248 -0
  53. package/dist/src/services/wechat-formatter.js.map +1 -0
  54. package/dist/src/services/wechatMessageService.d.ts +28 -0
  55. package/dist/src/services/wechatMessageService.d.ts.map +1 -0
  56. package/dist/src/services/wechatMessageService.js +341 -0
  57. package/dist/src/services/wechatMessageService.js.map +1 -0
  58. package/dist/src/services/wereadService.d.ts +206 -0
  59. package/dist/src/services/wereadService.d.ts.map +1 -0
  60. package/dist/src/services/wereadService.js +145 -0
  61. package/dist/src/services/wereadService.js.map +1 -0
  62. package/dist/src/services/whitelistService.d.ts +20 -0
  63. package/dist/src/services/whitelistService.d.ts.map +1 -0
  64. package/dist/src/services/whitelistService.js +60 -0
  65. package/dist/src/services/whitelistService.js.map +1 -0
  66. package/dist/src/types.d.ts +140 -0
  67. package/dist/src/types.d.ts.map +1 -0
  68. package/dist/src/types.js +2 -0
  69. package/dist/src/types.js.map +1 -0
  70. package/dist/src/utils/errors.d.ts +25 -0
  71. package/dist/src/utils/errors.d.ts.map +1 -0
  72. package/dist/src/utils/errors.js +43 -0
  73. package/dist/src/utils/errors.js.map +1 -0
  74. package/dist/src/utils/pathUtils.d.ts +5 -0
  75. package/dist/src/utils/pathUtils.d.ts.map +1 -0
  76. package/dist/src/utils/pathUtils.js +16 -0
  77. package/dist/src/utils/pathUtils.js.map +1 -0
  78. package/dist/src/utils/pythonRunner.d.ts +27 -0
  79. package/dist/src/utils/pythonRunner.d.ts.map +1 -0
  80. package/dist/src/utils/pythonRunner.js +62 -0
  81. package/dist/src/utils/pythonRunner.js.map +1 -0
  82. package/dist/src/utils/talkerUtils.d.ts +14 -0
  83. package/dist/src/utils/talkerUtils.d.ts.map +1 -0
  84. package/dist/src/utils/talkerUtils.js +73 -0
  85. package/dist/src/utils/talkerUtils.js.map +1 -0
  86. package/mcp-server/index.ts +757 -0
  87. package/package.json +72 -0
  88. package/resources/key/win32/x64/wx_key.dll +0 -0
  89. package/resources/wcdb/win32/x64/SDL2.dll +0 -0
  90. package/resources/wcdb/win32/x64/WCDB.dll +0 -0
  91. package/resources/wcdb/win32/x64/msvcp140.dll +0 -0
  92. package/resources/wcdb/win32/x64/msvcp140_1.dll +0 -0
  93. package/resources/wcdb/win32/x64/vcruntime140.dll +0 -0
  94. package/resources/wcdb/win32/x64/vcruntime140_1.dll +0 -0
  95. package/resources/wcdb/win32/x64/wcdb_api.dll +0 -0
  96. package/scripts/_batch_link_sources.py +73 -0
  97. package/scripts/_utils.py +531 -0
  98. package/scripts/annual_report.py +796 -0
  99. package/scripts/auto_tag.py +128 -0
  100. package/scripts/biz_daily.py +896 -0
  101. package/scripts/chat_report.py +397 -0
  102. package/scripts/chat_stats.py +702 -0
  103. package/scripts/classify_daily.py +411 -0
  104. package/scripts/compile_wiki.py +258 -0
  105. package/scripts/create_reading_notes.py +370 -0
  106. package/scripts/enrich_backlinks.py +163 -0
  107. package/scripts/export_chat_html.py +766 -0
  108. package/scripts/extract_3x_key.py +86 -0
  109. package/scripts/extract_todos.py +404 -0
  110. package/scripts/fav_server.py +392 -0
  111. package/scripts/fix_topics.py +111 -0
  112. package/scripts/generate_ai_report.py +367 -0
  113. package/scripts/generate_html.py +1372 -0
  114. package/scripts/generate_review.py +170 -0
  115. package/scripts/mcp_bridge.py +398 -0
  116. package/scripts/nt_decrypt.py +562 -0
  117. package/scripts/pipeline.py +145 -0
  118. package/scripts/promote_all.py +310 -0
  119. package/scripts/promote_ideas.py +297 -0
  120. package/scripts/rag_chat.py +191 -0
  121. package/scripts/scan_decrypt_4x.py +329 -0
  122. package/scripts/semantic_search.py +431 -0
  123. package/scripts/sync_fav.py +146 -0
  124. package/scripts/sync_weread.py +192 -0
  125. package/scripts/vault_rag.py +139 -0
  126. package/scripts/vault_search.py +141 -0
  127. package/src/core/dbPathService.ts +348 -0
  128. package/src/core/keyService.ts +409 -0
  129. package/src/core/ntCore.ts +224 -0
  130. package/src/core/sqlcipherCore.ts +764 -0
  131. package/src/core/wcdbCore.ts +4568 -0
  132. package/src/core/wechatClient.ts +204 -0
  133. package/src/lz4.d.ts +10 -0
  134. package/src/services/chatService.ts +296 -0
  135. package/src/services/configService.ts +197 -0
  136. package/src/services/exportService.ts +305 -0
  137. package/src/services/wechat-formatter.ts +348 -0
  138. package/src/services/wechatMessageService.ts +383 -0
  139. package/src/services/wereadService.ts +308 -0
  140. package/src/services/whitelistService.ts +72 -0
  141. package/src/types.ts +141 -0
  142. package/src/utils/errors.ts +42 -0
  143. package/src/utils/pathUtils.ts +16 -0
  144. package/src/utils/pythonRunner.ts +81 -0
  145. 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()