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,192 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
微信读书同步到 Vault — 将书架、笔记、划线写入 Obsidian。
|
|
4
|
+
|
|
5
|
+
用法:
|
|
6
|
+
python scripts/sync_weread.py # 同步所有
|
|
7
|
+
python scripts/sync_weread.py --type notes # 仅笔记
|
|
8
|
+
python scripts/sync_weread.py --type shelf # 仅书架概览
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import sys, os, json, urllib.request
|
|
12
|
+
from pathlib import Path
|
|
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
|
+
GATEWAY = 'https://i.weread.qq.com/api/agent/gateway'
|
|
19
|
+
SKILL_VERSION = '1.0.3'
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def weread_call(api_name: str, api_key: str, **params) -> dict:
|
|
23
|
+
"""调用微信读书 API。"""
|
|
24
|
+
body = json.dumps({'api_name': api_name, 'skill_version': SKILL_VERSION, **params}).encode('utf-8')
|
|
25
|
+
req = urllib.request.Request(GATEWAY, data=body, headers={
|
|
26
|
+
'Authorization': f'Bearer {api_key}',
|
|
27
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
28
|
+
})
|
|
29
|
+
try:
|
|
30
|
+
resp = urllib.request.urlopen(req, timeout=15)
|
|
31
|
+
return json.loads(resp.read().decode('utf-8'))
|
|
32
|
+
except Exception as e:
|
|
33
|
+
print(f' [WARN] API 调用失败: {e}')
|
|
34
|
+
return {}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def sync_shelf(api_key, vault_dir):
|
|
38
|
+
"""同步书架概览到 Vault。"""
|
|
39
|
+
data = weread_call('/shelf/sync', api_key)
|
|
40
|
+
books = data.get('books', [])
|
|
41
|
+
if not books:
|
|
42
|
+
print(' 书架为空')
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
shelf_dir = Path(vault_dir) / 'Sources' / 'WeRead'
|
|
46
|
+
shelf_dir.mkdir(parents=True, exist_ok=True)
|
|
47
|
+
|
|
48
|
+
# 生成书架索引
|
|
49
|
+
lines = [
|
|
50
|
+
'---',
|
|
51
|
+
'type: weread-shelf',
|
|
52
|
+
f'updated: {datetime.now().strftime("%Y-%m-%d %H:%M")}',
|
|
53
|
+
'---',
|
|
54
|
+
'',
|
|
55
|
+
'# 微信读书书架',
|
|
56
|
+
'',
|
|
57
|
+
f'共 {len(books)} 本书',
|
|
58
|
+
'',
|
|
59
|
+
]
|
|
60
|
+
for b in sorted(books, key=lambda x: x.get('readUpdateTime', 0), reverse=True):
|
|
61
|
+
done = '✓' if b.get('finishReading') == 1 else ''
|
|
62
|
+
lines.append(f'- {done} **{b.get("title", "")}** — {b.get("author", "")}')
|
|
63
|
+
|
|
64
|
+
(shelf_dir / 'README.md').write_text('\n'.join(lines), encoding='utf-8')
|
|
65
|
+
print(f' 书架索引: {shelf_dir / "README.md"} ({len(books)} 本)')
|
|
66
|
+
|
|
67
|
+
# 为每本在读的书创建笔记页
|
|
68
|
+
reading_dir = Path(vault_dir) / 'Notes' / 'Reading'
|
|
69
|
+
reading_dir.mkdir(parents=True, exist_ok=True)
|
|
70
|
+
created = 0
|
|
71
|
+
for b in books:
|
|
72
|
+
if b.get('finishReading') == 1:
|
|
73
|
+
continue # 跳过已读完的
|
|
74
|
+
safe_name = b.get('title', 'unknown').replace('/', '_').replace('\\', '_')
|
|
75
|
+
for ch in '<>:"/\\|?*':
|
|
76
|
+
safe_name = safe_name.replace(ch, '')
|
|
77
|
+
safe_name = safe_name[:40].rstrip('. ')
|
|
78
|
+
note_path = reading_dir / f'Weread-{safe_name}.md'
|
|
79
|
+
if note_path.exists():
|
|
80
|
+
continue
|
|
81
|
+
note_path.write_text(f'''---
|
|
82
|
+
title: "{b.get('title', '')}"
|
|
83
|
+
author: "{b.get('author', '')}"
|
|
84
|
+
bookId: "{b.get('bookId', '')}"
|
|
85
|
+
type: weread-reading
|
|
86
|
+
tags: [weread, {b.get('category', '').lower()}]
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
# {b.get('title', '')}
|
|
90
|
+
|
|
91
|
+
> 作者: {b.get('author', '')}
|
|
92
|
+
|
|
93
|
+
## 阅读笔记
|
|
94
|
+
|
|
95
|
+
## 划线摘录
|
|
96
|
+
|
|
97
|
+
## 读后感
|
|
98
|
+
|
|
99
|
+
''', encoding='utf-8')
|
|
100
|
+
created += 1
|
|
101
|
+
if created:
|
|
102
|
+
print(f' 阅读笔记: {created} 本新书')
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def sync_notes(api_key, vault_dir):
|
|
106
|
+
"""同步笔记划线到 Vault。"""
|
|
107
|
+
data = weread_call('/user/notebooks', api_key, count=50)
|
|
108
|
+
books = data.get('books', [])
|
|
109
|
+
if not books:
|
|
110
|
+
print(' 无法获取笔记列表')
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
highlights_dir = Path(vault_dir) / 'Notes' / 'WereadHighlights'
|
|
114
|
+
highlights_dir.mkdir(parents=True, exist_ok=True)
|
|
115
|
+
synced = 0
|
|
116
|
+
|
|
117
|
+
for nb in books[:10]: # 最多处理 10 本有笔记的书
|
|
118
|
+
book = nb.get('book', {})
|
|
119
|
+
book_id = book.get('bookId', '')
|
|
120
|
+
title = book.get('title', '')
|
|
121
|
+
if not book_id:
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
marks = weread_call('/book/bookmarklist', api_key, bookId=book_id, count=50)
|
|
125
|
+
updated = marks.get('updated', [])
|
|
126
|
+
if not updated:
|
|
127
|
+
continue
|
|
128
|
+
|
|
129
|
+
safe_name = title.replace('/', '_')
|
|
130
|
+
for ch in '<>:"/\\|?*':
|
|
131
|
+
safe_name = safe_name.replace(ch, '')
|
|
132
|
+
safe_name = safe_name[:40].rstrip('. ')
|
|
133
|
+
note_path = highlights_dir / f'{safe_name}-划线.md'
|
|
134
|
+
|
|
135
|
+
lines = [
|
|
136
|
+
'---',
|
|
137
|
+
f'title: "{title}"',
|
|
138
|
+
f'bookId: "{book_id}"',
|
|
139
|
+
'type: weread-highlights',
|
|
140
|
+
'tags: [weread, highlights]',
|
|
141
|
+
'---',
|
|
142
|
+
'',
|
|
143
|
+
f'# {title} — 划线笔记',
|
|
144
|
+
'',
|
|
145
|
+
]
|
|
146
|
+
for m in updated:
|
|
147
|
+
text = m.get('markText', '')
|
|
148
|
+
note = m.get('content', '')
|
|
149
|
+
chapter = m.get('chapterTitle', '')
|
|
150
|
+
if text:
|
|
151
|
+
lines.append(f'> {text}')
|
|
152
|
+
if chapter:
|
|
153
|
+
lines.append(f' — {chapter}')
|
|
154
|
+
if note:
|
|
155
|
+
lines.append(f' 💭 {note}')
|
|
156
|
+
lines.append('')
|
|
157
|
+
|
|
158
|
+
note_path.write_text('\n'.join(lines), encoding='utf-8')
|
|
159
|
+
synced += 1
|
|
160
|
+
|
|
161
|
+
print(f' 划线笔记: {synced} 本书')
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def main():
|
|
165
|
+
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
|
166
|
+
import argparse
|
|
167
|
+
parser = argparse.ArgumentParser(description='微信读书 → Vault 同步')
|
|
168
|
+
parser.add_argument('--type', default='all', choices=['all', 'shelf', 'notes'])
|
|
169
|
+
parser.add_argument('--vault', default=DEFAULT_VAULT, help='Vault 路径')
|
|
170
|
+
args = parser.parse_args()
|
|
171
|
+
|
|
172
|
+
api_key = os.environ.get('WEREAD_API_KEY', '')
|
|
173
|
+
if not api_key:
|
|
174
|
+
print('[ERROR] 未设置 WEREAD_API_KEY')
|
|
175
|
+
sys.exit(1)
|
|
176
|
+
|
|
177
|
+
vault_dir = Path(args.vault)
|
|
178
|
+
(vault_dir / 'Sources' / 'WeRead').mkdir(parents=True, exist_ok=True)
|
|
179
|
+
|
|
180
|
+
if args.type in ('all', 'shelf'):
|
|
181
|
+
print('📚 同步书架...')
|
|
182
|
+
sync_shelf(api_key, vault_dir)
|
|
183
|
+
|
|
184
|
+
if args.type in ('all', 'notes'):
|
|
185
|
+
print('📝 同步笔记...')
|
|
186
|
+
sync_notes(api_key, vault_dir)
|
|
187
|
+
|
|
188
|
+
print('✓ WeRead 同步完成')
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
if __name__ == '__main__':
|
|
192
|
+
main()
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Vault RAG 对话 — 基于 Vault 知识库(文章+概念+笔记)的问答。
|
|
4
|
+
|
|
5
|
+
用法:
|
|
6
|
+
python scripts/vault_rag.py "深度学习在遥感中的应用"
|
|
7
|
+
python scripts/vault_rag.py "最近读了什么文章" --json
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import sys, os, json, argparse, re
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
SCRIPTS_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
14
|
+
PROJECT_ROOT = os.path.dirname(SCRIPTS_DIR)
|
|
15
|
+
DEFAULT_VAULT = os.path.join(PROJECT_ROOT, 'output', 'wechat-vault')
|
|
16
|
+
DEFAULT_BIZ = os.path.join(PROJECT_ROOT, 'output', 'biz-daily')
|
|
17
|
+
|
|
18
|
+
sys.path.insert(0, SCRIPTS_DIR)
|
|
19
|
+
from _utils import call_deepseek
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def collect_context(vault: str, biz_daily: str, question: str, top_k: int) -> list[dict]:
|
|
23
|
+
"""从 Vault + biz-daily 检索相关上下文。"""
|
|
24
|
+
results = []
|
|
25
|
+
search_terms = question.lower().split()
|
|
26
|
+
|
|
27
|
+
# 1. 概念页(权重最高)
|
|
28
|
+
concepts_dir = Path(vault) / 'Wiki' / 'Concepts'
|
|
29
|
+
if concepts_dir.is_dir():
|
|
30
|
+
for md in concepts_dir.glob('*.md'):
|
|
31
|
+
try:
|
|
32
|
+
text = md.read_text(encoding='utf-8')[:2000]
|
|
33
|
+
except Exception:
|
|
34
|
+
continue
|
|
35
|
+
score = sum(text.lower().count(t) for t in search_terms)
|
|
36
|
+
if score > 0:
|
|
37
|
+
results.append({'source': 'concept', 'title': md.stem, 'content': text, 'score': score})
|
|
38
|
+
|
|
39
|
+
# 2. 阅读笔记
|
|
40
|
+
notes_dir = Path(vault) / 'Notes'
|
|
41
|
+
if notes_dir.is_dir():
|
|
42
|
+
for md in notes_dir.rglob('*.md'):
|
|
43
|
+
try:
|
|
44
|
+
text = md.read_text(encoding='utf-8')[:1500]
|
|
45
|
+
except Exception:
|
|
46
|
+
continue
|
|
47
|
+
score = sum(text.lower().count(t) for t in search_terms)
|
|
48
|
+
# 标题匹配加分
|
|
49
|
+
if any(t in md.stem.lower() for t in search_terms):
|
|
50
|
+
score += 15
|
|
51
|
+
if score > 0:
|
|
52
|
+
results.append({'source': 'note', 'title': md.stem[:60], 'content': text, 'score': score})
|
|
53
|
+
|
|
54
|
+
# 3. 最新文章
|
|
55
|
+
biz_path = Path(biz_daily)
|
|
56
|
+
for date_dir in sorted(biz_path.glob('20*'), reverse=True)[:7]: # 最近 7 天
|
|
57
|
+
for md in date_dir.rglob('*.md'):
|
|
58
|
+
if md.name == 'README.md' or 'README' in md.name:
|
|
59
|
+
continue
|
|
60
|
+
try:
|
|
61
|
+
text = md.read_text(encoding='utf-8')[:1000]
|
|
62
|
+
except Exception:
|
|
63
|
+
continue
|
|
64
|
+
score = sum(text.lower().count(t) for t in search_terms)
|
|
65
|
+
if score > 0:
|
|
66
|
+
results.append({'source': 'article', 'title': md.stem[:60], 'content': text, 'score': score})
|
|
67
|
+
|
|
68
|
+
results.sort(key=lambda x: x['score'], reverse=True)
|
|
69
|
+
return results[:top_k]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def build_prompt(question: str, context: list[dict]) -> str:
|
|
73
|
+
sources_text = ''
|
|
74
|
+
for i, ctx in enumerate(context):
|
|
75
|
+
label = {'concept': '概念', 'note': '笔记', 'article': '文章'}.get(ctx['source'], ctx['source'])
|
|
76
|
+
sources_text += f'\n[{i+1}] ({label}) {ctx["title"]}\n{ctx["content"][:600]}\n'
|
|
77
|
+
|
|
78
|
+
return f'''你是个人知识助手,基于用户笔记库回答问题。
|
|
79
|
+
|
|
80
|
+
知识库内容:
|
|
81
|
+
{sources_text}
|
|
82
|
+
|
|
83
|
+
用户问题:{question}
|
|
84
|
+
|
|
85
|
+
请综合知识库内容回答。如果知识库中没有相关信息,如实说明。'''
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def main():
|
|
89
|
+
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
|
90
|
+
parser = argparse.ArgumentParser(description='Vault RAG 对话')
|
|
91
|
+
parser.add_argument('question', nargs='?', default='', help='问题')
|
|
92
|
+
parser.add_argument('--top-k', default='8', help='检索条数')
|
|
93
|
+
parser.add_argument('--vault', default=DEFAULT_VAULT, help='Vault 路径')
|
|
94
|
+
parser.add_argument('--biz-daily', default=DEFAULT_BIZ, help='biz-daily 路径')
|
|
95
|
+
parser.add_argument('--api-key', default='', help='DeepSeek API key')
|
|
96
|
+
parser.add_argument('--json', action='store_true', help='JSON 输出')
|
|
97
|
+
args = parser.parse_args()
|
|
98
|
+
|
|
99
|
+
if not args.question:
|
|
100
|
+
print('请提供问题')
|
|
101
|
+
sys.exit(1)
|
|
102
|
+
|
|
103
|
+
api_key = args.api_key or os.environ.get('DEEPSEEK_API_KEY', '')
|
|
104
|
+
if not api_key:
|
|
105
|
+
print('[ERROR] 缺少 API Key')
|
|
106
|
+
sys.exit(1)
|
|
107
|
+
|
|
108
|
+
top_k = int(args.top_k)
|
|
109
|
+
context = collect_context(args.vault, args.biz_daily, args.question, top_k)
|
|
110
|
+
|
|
111
|
+
if args.json:
|
|
112
|
+
output = {'question': args.question, 'sources': [{'source': c['source'], 'title': c['title'], 'score': c['score']} for c in context]}
|
|
113
|
+
if context:
|
|
114
|
+
prompt = build_prompt(args.question, context)
|
|
115
|
+
output['answer'] = call_deepseek(prompt, api_key, max_tokens=800)
|
|
116
|
+
else:
|
|
117
|
+
output['answer'] = '知识库中没有相关信息'
|
|
118
|
+
print(json.dumps(output, ensure_ascii=False, indent=2))
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
print(f'🔍 "{args.question}"\n')
|
|
122
|
+
if not context:
|
|
123
|
+
print('未找到相关内容')
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
print(f'检索到 {len(context)} 条相关内容:\n')
|
|
127
|
+
for i, c in enumerate(context):
|
|
128
|
+
icon = {'concept': '🧠', 'note': '📝', 'article': '📄'}.get(c['source'], '📎')
|
|
129
|
+
print(f' {i+1}. {icon} [{c["source"]}] {c["title"]}')
|
|
130
|
+
|
|
131
|
+
prompt = build_prompt(args.question, context)
|
|
132
|
+
print(f'\n{"="*50}')
|
|
133
|
+
answer = call_deepseek(prompt, api_key, max_tokens=800)
|
|
134
|
+
print(answer)
|
|
135
|
+
print(f'{"="*50}')
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
if __name__ == '__main__':
|
|
139
|
+
main()
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Vault 全局搜索 — 关键词 + AI 排序,覆盖文章/概念/笔记。
|
|
4
|
+
|
|
5
|
+
用法:
|
|
6
|
+
python scripts/vault_search.py "遥感反演" --top-k 10
|
|
7
|
+
python scripts/vault_search.py "空气污染" --type article --days 30
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import sys, os, json, argparse, re
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from datetime import datetime, timedelta
|
|
13
|
+
|
|
14
|
+
SCRIPTS_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
15
|
+
PROJECT_ROOT = os.path.dirname(SCRIPTS_DIR)
|
|
16
|
+
DEFAULT_VAULT = os.path.join(PROJECT_ROOT, 'output', 'wechat-vault')
|
|
17
|
+
DEFAULT_BIZ = os.path.join(PROJECT_ROOT, 'output', 'biz-daily')
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def search_text(query: str, files: list[tuple], top_k: int) -> list[dict]:
|
|
21
|
+
"""关键词搜索 + 简单排序。"""
|
|
22
|
+
terms = query.lower().split()
|
|
23
|
+
scored = []
|
|
24
|
+
for filepath, category, meta in files:
|
|
25
|
+
try:
|
|
26
|
+
text = filepath.read_text(encoding='utf-8')[:2000]
|
|
27
|
+
except Exception:
|
|
28
|
+
continue
|
|
29
|
+
text_lower = text.lower()
|
|
30
|
+
# 计分:标题匹配 > 关键词频次
|
|
31
|
+
score = 0
|
|
32
|
+
title = meta.get('title', '')
|
|
33
|
+
if any(t in title.lower() for t in terms):
|
|
34
|
+
score += 10
|
|
35
|
+
for t in terms:
|
|
36
|
+
score += text_lower.count(t)
|
|
37
|
+
if score > 0:
|
|
38
|
+
scored.append({'file': str(filepath), 'category': category, 'title': title,
|
|
39
|
+
'score': score, 'snippet': text[:300]})
|
|
40
|
+
scored.sort(key=lambda x: x['score'], reverse=True)
|
|
41
|
+
return scored[:top_k]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def collect_files(vault: str, biz_daily: str, search_type: str, days: int) -> list[tuple]:
|
|
45
|
+
"""收集可搜索文件。"""
|
|
46
|
+
files = []
|
|
47
|
+
cutoff = datetime.now() - timedelta(days=days)
|
|
48
|
+
|
|
49
|
+
def parse_fm(text):
|
|
50
|
+
if not text.startswith('---'):
|
|
51
|
+
return {}
|
|
52
|
+
end = text.find('---', 3)
|
|
53
|
+
if end == -1:
|
|
54
|
+
return {}
|
|
55
|
+
fm = {}
|
|
56
|
+
for line in text[3:end].strip().split('\n'):
|
|
57
|
+
if ':' in line:
|
|
58
|
+
k, _, v = line.partition(':')
|
|
59
|
+
fm[k.strip()] = v.strip().strip('"').strip("'")
|
|
60
|
+
return fm
|
|
61
|
+
|
|
62
|
+
if search_type in ('all', 'article'):
|
|
63
|
+
# biz-daily 文章
|
|
64
|
+
biz_dir = Path(biz_daily)
|
|
65
|
+
for date_dir in sorted(biz_dir.glob('20*'), reverse=True):
|
|
66
|
+
try:
|
|
67
|
+
dir_date = datetime.strptime(date_dir.name, '%Y-%m-%d')
|
|
68
|
+
if dir_date < cutoff:
|
|
69
|
+
continue
|
|
70
|
+
except ValueError:
|
|
71
|
+
continue
|
|
72
|
+
for md in date_dir.rglob('*.md'):
|
|
73
|
+
if md.name == 'README.md':
|
|
74
|
+
continue
|
|
75
|
+
try:
|
|
76
|
+
fm = parse_fm(md.read_text(encoding='utf-8'))
|
|
77
|
+
except Exception:
|
|
78
|
+
fm = {}
|
|
79
|
+
files.append((md, 'article', fm))
|
|
80
|
+
|
|
81
|
+
if search_type in ('all', 'concept'):
|
|
82
|
+
vault_path = Path(vault)
|
|
83
|
+
concepts_dir = vault_path / 'Wiki' / 'Concepts'
|
|
84
|
+
if concepts_dir.is_dir():
|
|
85
|
+
for md in concepts_dir.glob('*.md'):
|
|
86
|
+
try:
|
|
87
|
+
fm = parse_fm(md.read_text(encoding='utf-8'))
|
|
88
|
+
except Exception:
|
|
89
|
+
fm = {}
|
|
90
|
+
files.append((md, 'concept', fm))
|
|
91
|
+
|
|
92
|
+
if search_type in ('all', 'note'):
|
|
93
|
+
vault_path = Path(vault)
|
|
94
|
+
notes_dir = vault_path / 'Notes'
|
|
95
|
+
if notes_dir.is_dir():
|
|
96
|
+
for md in notes_dir.rglob('*.md'):
|
|
97
|
+
try:
|
|
98
|
+
fm = parse_fm(md.read_text(encoding='utf-8'))
|
|
99
|
+
except Exception:
|
|
100
|
+
fm = {}
|
|
101
|
+
files.append((md, 'note', fm))
|
|
102
|
+
|
|
103
|
+
return files
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def main():
|
|
107
|
+
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
|
108
|
+
parser = argparse.ArgumentParser(description='Vault 全局搜索')
|
|
109
|
+
parser.add_argument('query', nargs='?', default='', help='搜索关键词')
|
|
110
|
+
parser.add_argument('--top-k', default='10', help='返回数量')
|
|
111
|
+
parser.add_argument('--type', default='all', choices=['all', 'article', 'concept', 'note'])
|
|
112
|
+
parser.add_argument('--days', type=int, default=90, help='搜索天数范围')
|
|
113
|
+
parser.add_argument('--vault', default=DEFAULT_VAULT, help='Vault 路径')
|
|
114
|
+
parser.add_argument('--biz-daily', default=DEFAULT_BIZ, help='biz-daily 路径')
|
|
115
|
+
parser.add_argument('--json', action='store_true', help='JSON 输出')
|
|
116
|
+
args = parser.parse_args()
|
|
117
|
+
|
|
118
|
+
if not args.query:
|
|
119
|
+
print('请提供搜索关键词')
|
|
120
|
+
sys.exit(1)
|
|
121
|
+
|
|
122
|
+
files = collect_files(args.vault, args.biz_daily, args.type, args.days)
|
|
123
|
+
results = search_text(args.query, files, int(args.top_k))
|
|
124
|
+
|
|
125
|
+
if args.json:
|
|
126
|
+
print(json.dumps(results, ensure_ascii=False, indent=2))
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
print(f'🔍 搜索 "{args.query}" 找到 {len(results)} 条 (类型: {args.type}, {args.days}天内)\n')
|
|
130
|
+
for i, r in enumerate(results):
|
|
131
|
+
icon = {'article': '📄', 'concept': '🧠', 'note': '📝'}.get(r['category'], '📎')
|
|
132
|
+
num = str(i + 1).rjust(2)
|
|
133
|
+
print(f'{num}. {icon} {r["title"] or r["file"].split("/")[-1]}')
|
|
134
|
+
print(f' {r["category"]} | 相关度: {r["score"]}')
|
|
135
|
+
snippet = re.sub(r'\s+', ' ', r['snippet'][:120])
|
|
136
|
+
print(f' {snippet}')
|
|
137
|
+
print()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
if __name__ == '__main__':
|
|
141
|
+
main()
|