weflow-cli 1.7.0 → 1.8.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/ARCHITECTURE.md +3 -2
- package/CHANGELOG.md +1949 -1
- package/OPERATIONS.md +774 -1
- package/README.en.md +93 -7
- package/README.md +31 -6
- package/bin/weflow-cli.ts +934 -27
- package/dist/bin/weflow-cli.js +952 -27
- package/dist/bin/weflow-cli.js.map +1 -1
- package/dist/mcp-server/index.js +39 -13
- package/dist/mcp-server/index.js.map +1 -1
- package/dist/src/core/appMsgFormat.d.ts +37 -0
- package/dist/src/core/appMsgFormat.d.ts.map +1 -0
- package/dist/src/core/appMsgFormat.js +106 -0
- package/dist/src/core/appMsgFormat.js.map +1 -0
- package/dist/src/core/ntCore.d.ts +19 -2
- package/dist/src/core/ntCore.d.ts.map +1 -1
- package/dist/src/core/ntCore.js +11 -1
- package/dist/src/core/ntCore.js.map +1 -1
- package/dist/src/core/sqlcipherCore.d.ts +0 -12
- package/dist/src/core/sqlcipherCore.d.ts.map +1 -1
- package/dist/src/core/sqlcipherCore.js +5 -64
- package/dist/src/core/sqlcipherCore.js.map +1 -1
- package/dist/src/core/wcdbCore.d.ts +1 -1
- package/dist/src/core/wcdbCore.d.ts.map +1 -1
- package/dist/src/core/wcdbCore.js +5 -1
- package/dist/src/core/wcdbCore.js.map +1 -1
- package/dist/src/panel/client.d.ts +25 -0
- package/dist/src/panel/client.d.ts.map +1 -0
- package/dist/src/panel/client.js +34 -0
- package/dist/src/panel/client.js.map +1 -0
- package/dist/src/panel/endpoint.d.ts +45 -0
- package/dist/src/panel/endpoint.d.ts.map +1 -0
- package/dist/src/panel/endpoint.js +132 -0
- package/dist/src/panel/endpoint.js.map +1 -0
- package/dist/src/panel/launch.d.ts +50 -0
- package/dist/src/panel/launch.d.ts.map +1 -0
- package/dist/src/panel/launch.js +74 -0
- package/dist/src/panel/launch.js.map +1 -0
- package/dist/src/panel/server.d.ts +23 -0
- package/dist/src/panel/server.d.ts.map +1 -0
- package/dist/src/panel/server.js +413 -0
- package/dist/src/panel/server.js.map +1 -0
- package/dist/src/panel/userId.d.ts +37 -0
- package/dist/src/panel/userId.d.ts.map +1 -0
- package/dist/src/panel/userId.js +56 -0
- package/dist/src/panel/userId.js.map +1 -0
- package/dist/src/services/assistantDaemon.d.ts +20 -3
- package/dist/src/services/assistantDaemon.d.ts.map +1 -1
- package/dist/src/services/assistantDaemon.js +46 -9
- package/dist/src/services/assistantDaemon.js.map +1 -1
- package/dist/src/services/assistantEval.d.ts +141 -0
- package/dist/src/services/assistantEval.d.ts.map +1 -0
- package/dist/src/services/assistantEval.js +497 -0
- package/dist/src/services/assistantEval.js.map +1 -0
- package/dist/src/services/assistantMemory.d.ts +106 -4
- package/dist/src/services/assistantMemory.d.ts.map +1 -1
- package/dist/src/services/assistantMemory.js +396 -61
- package/dist/src/services/assistantMemory.js.map +1 -1
- package/dist/src/services/assistantPrivacy.d.ts +15 -2
- package/dist/src/services/assistantPrivacy.d.ts.map +1 -1
- package/dist/src/services/assistantPrivacy.js +18 -4
- package/dist/src/services/assistantPrivacy.js.map +1 -1
- package/dist/src/services/assistantRouter.d.ts +41 -0
- package/dist/src/services/assistantRouter.d.ts.map +1 -0
- package/dist/src/services/assistantRouter.js +151 -0
- package/dist/src/services/assistantRouter.js.map +1 -0
- package/dist/src/services/assistantService.d.ts +209 -3
- package/dist/src/services/assistantService.d.ts.map +1 -1
- package/dist/src/services/assistantService.js +590 -75
- package/dist/src/services/assistantService.js.map +1 -1
- package/dist/src/services/assistantTools.d.ts +205 -1
- package/dist/src/services/assistantTools.d.ts.map +1 -1
- package/dist/src/services/assistantTools.js +1089 -58
- package/dist/src/services/assistantTools.js.map +1 -1
- package/dist/src/services/assistantTrace.d.ts +47 -0
- package/dist/src/services/assistantTrace.d.ts.map +1 -0
- package/dist/src/services/assistantTrace.js +150 -0
- package/dist/src/services/assistantTrace.js.map +1 -0
- package/dist/src/services/chatService.d.ts +1 -1
- package/dist/src/services/chatService.d.ts.map +1 -1
- package/dist/src/services/chatService.js +5 -2
- package/dist/src/services/chatService.js.map +1 -1
- package/dist/src/services/configService.d.ts +9 -0
- package/dist/src/services/configService.d.ts.map +1 -1
- package/dist/src/services/configService.js +15 -8
- package/dist/src/services/configService.js.map +1 -1
- package/dist/src/services/pythonBridge.d.ts +41 -0
- package/dist/src/services/pythonBridge.d.ts.map +1 -0
- package/dist/src/services/pythonBridge.js +119 -0
- package/dist/src/services/pythonBridge.js.map +1 -0
- package/dist/src/services/syncService.d.ts +8 -2
- package/dist/src/services/syncService.d.ts.map +1 -1
- package/dist/src/services/syncService.js +26 -7
- package/dist/src/services/syncService.js.map +1 -1
- package/dist/src/services/wechatMessageService.d.ts +25 -0
- package/dist/src/services/wechatMessageService.d.ts.map +1 -1
- package/dist/src/services/wechatMessageService.js +69 -7
- package/dist/src/services/wechatMessageService.js.map +1 -1
- package/dist/src/utils/atomicWrite.d.ts +4 -0
- package/dist/src/utils/atomicWrite.d.ts.map +1 -0
- package/dist/src/utils/atomicWrite.js +60 -0
- package/dist/src/utils/atomicWrite.js.map +1 -0
- package/dist/src/utils/dateRange.d.ts +15 -0
- package/dist/src/utils/dateRange.d.ts.map +1 -1
- package/dist/src/utils/dateRange.js +47 -0
- package/dist/src/utils/dateRange.js.map +1 -1
- package/dist/src/utils/text.d.ts +18 -0
- package/dist/src/utils/text.d.ts.map +1 -0
- package/dist/src/utils/text.js +20 -0
- package/dist/src/utils/text.js.map +1 -0
- package/docs/AI_INTERFACE.md +2 -0
- package/docs/DECISIONS.md +1424 -0
- package/docs/EXTENDING.md +187 -0
- package/docs/JEV/345/272/224/347/224/250/346/226/275/345/267/245/346/226/207/346/241/243.md +310 -0
- package/docs/MCP.md +23 -4
- package/docs/PROJECT_STATE.md +299 -98
- package/docs/SETUP.md +22 -0
- package/docs/SYNC_CONTRACT.md +31 -3
- package/mcp-server/index.ts +681 -657
- package/package.json +84 -83
- package/resources/panel/ball-position.cjs +153 -0
- package/resources/panel/index.html +46 -0
- package/resources/panel/main.cjs +447 -0
- package/resources/panel/mascot-focus.png +0 -0
- package/resources/panel/mascot-happy.png +0 -0
- package/resources/panel/mascot-sorry.png +0 -0
- package/resources/panel/mascot-tired.png +0 -0
- package/resources/panel/mascot.png +0 -0
- package/resources/panel/package.json +7 -0
- package/resources/panel/panel.css +407 -0
- package/resources/panel/preload.cjs +72 -0
- package/resources/panel/quick-menu.cjs +197 -0
- package/resources/panel/renderer.js +441 -0
- package/resources/panel/tray-menu.cjs +40 -0
- package/resources/panel/tray.png +0 -0
- package/scripts/_utils.py +403 -54
- package/scripts/annual_report.py +4 -2
- package/scripts/article_notes.py +423 -0
- package/scripts/backfill_articles.py +438 -0
- package/scripts/biz_daily.py +590 -105
- package/scripts/chat_notes.py +470 -0
- package/scripts/classify_daily.py +10 -18
- package/scripts/compile_wiki.py +1059 -46
- package/scripts/create_reading_notes.py +199 -14
- package/scripts/decide.py +253 -0
- package/scripts/draft_reply.py +671 -0
- package/scripts/export_chat_html.py +35 -33
- package/scripts/extract_todos.py +31 -5
- package/scripts/fav_notes.py +318 -0
- package/scripts/fix_topics.py +2 -1
- package/scripts/generate_ai_report.py +214 -9
- package/scripts/generate_html.py +18 -3
- package/scripts/generate_review.py +2 -2
- package/scripts/jev_client.py +294 -0
- package/scripts/jev_probe.py +440 -0
- package/scripts/nt_common.py +82 -0
- package/scripts/nt_decrypt.py +1755 -1460
- package/scripts/pipeline.py +27 -2
- package/scripts/promote_ideas.py +19 -4
- package/scripts/quality_eval.py +707 -0
- package/scripts/rag_chat.py +4 -4
- package/scripts/read_image.py +187 -0
- package/scripts/reply_debt.py +488 -0
- package/scripts/route_cards.py +453 -0
- package/scripts/semantic_search.py +208 -49
- package/scripts/user_notes.py +315 -0
- package/scripts/vault_rag.py +13 -7
- package/scripts/vault_search.py +21 -4
- package/scripts/wiki_lint.py +397 -0
- package/src/core/appMsgFormat.ts +117 -0
- package/src/core/ntCore.ts +27 -2
- package/src/core/sqlcipherCore.ts +5 -61
- package/src/core/wcdbCore.ts +6 -2
- package/src/panel/client.ts +49 -0
- package/src/panel/endpoint.ts +144 -0
- package/src/panel/launch.ts +101 -0
- package/src/panel/server.ts +424 -0
- package/src/panel/userId.ts +72 -0
- package/src/services/assistantDaemon.ts +57 -9
- package/src/services/assistantEval.ts +611 -0
- package/src/services/assistantMemory.ts +413 -57
- package/src/services/assistantPrivacy.ts +18 -4
- package/src/services/assistantRouter.ts +191 -0
- package/src/services/assistantService.ts +671 -74
- package/src/services/assistantTools.ts +1118 -57
- package/src/services/assistantTrace.ts +175 -0
- package/src/services/chatService.ts +6 -2
- package/src/services/configService.ts +403 -385
- package/src/services/pythonBridge.ts +139 -0
- package/src/services/syncService.ts +37 -9
- package/src/services/wechatMessageService.ts +73 -7
- package/src/utils/atomicWrite.ts +56 -0
- package/src/utils/dateRange.ts +46 -0
- package/src/utils/text.ts +20 -0
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
# 构建索引(首次或增量)
|
|
7
7
|
python scripts/semantic_search.py build
|
|
8
8
|
|
|
9
|
+
# 窗口可调(默认聊天 90 天 / 日报 30 天;要"整段历史"就给大值)
|
|
10
|
+
python scripts/semantic_search.py build --days 3650 --article-days 365
|
|
11
|
+
|
|
9
12
|
# 搜索(有索引用向量,否则关键词 fallback)
|
|
10
13
|
python scripts/semantic_search.py search "有人推荐过遥感的工具吗" --top-k 10
|
|
11
14
|
|
|
@@ -15,21 +18,39 @@
|
|
|
15
18
|
输出: JSON 格式
|
|
16
19
|
"""
|
|
17
20
|
|
|
18
|
-
import sys, os, json, hashlib, argparse
|
|
21
|
+
import sys, os, json, hashlib, re, argparse
|
|
19
22
|
from datetime import datetime, timezone, timedelta
|
|
20
23
|
from pathlib import Path
|
|
21
24
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
# 重依赖一律懒加载(照 nt_decrypt.require_sqlcipher 的做法):
|
|
26
|
+
# 关键词检索与重排不需要它们,缺了也不该让整份模块 import 就 sys.exit——
|
|
27
|
+
# 那样连不碰向量库的代码路径都用不了,CI 里也没法测任何东西。
|
|
28
|
+
np = None
|
|
29
|
+
sqlcipher = None
|
|
27
30
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
|
|
32
|
+
def require_numpy():
|
|
33
|
+
global np
|
|
34
|
+
if np is not None:
|
|
35
|
+
return np
|
|
36
|
+
try:
|
|
37
|
+
import numpy as _np
|
|
38
|
+
except ImportError:
|
|
39
|
+
raise RuntimeError('需要 numpy: pip install numpy')
|
|
40
|
+
np = _np
|
|
41
|
+
return np
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def require_sqlcipher():
|
|
45
|
+
global sqlcipher
|
|
46
|
+
if sqlcipher is not None:
|
|
47
|
+
return sqlcipher
|
|
48
|
+
try:
|
|
49
|
+
from sqlcipher3 import dbapi2 as _sqlcipher
|
|
50
|
+
except ImportError:
|
|
51
|
+
raise RuntimeError('需要 sqlcipher3: pip install sqlcipher3')
|
|
52
|
+
sqlcipher = _sqlcipher
|
|
53
|
+
return sqlcipher
|
|
33
54
|
|
|
34
55
|
try:
|
|
35
56
|
import urllib.request
|
|
@@ -38,7 +59,8 @@ except:
|
|
|
38
59
|
pass
|
|
39
60
|
|
|
40
61
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
41
|
-
from _utils import load_config, decrypt_lock
|
|
62
|
+
from _utils import load_config, decrypt_lock, get_dashscope_key
|
|
63
|
+
from jev_client import create_client
|
|
42
64
|
|
|
43
65
|
OUTPUT_ROOT = 'output'
|
|
44
66
|
INDEX_DIR = Path(OUTPUT_ROOT) / '.semantic_index'
|
|
@@ -99,6 +121,7 @@ def get_embeddings(texts: list[str], api_key: str) -> list[list[float]]:
|
|
|
99
121
|
# ====== Data Collection ======
|
|
100
122
|
|
|
101
123
|
def open_db(db_path, key_hex, salt_hex):
|
|
124
|
+
sqlcipher = require_sqlcipher()
|
|
102
125
|
raw_key = f"x'{key_hex}{salt_hex}'"
|
|
103
126
|
conn = sqlcipher.connect(db_path)
|
|
104
127
|
c = conn.cursor()
|
|
@@ -171,20 +194,27 @@ def collect_chat_messages(conn, name_map, days=90):
|
|
|
171
194
|
return items
|
|
172
195
|
|
|
173
196
|
|
|
174
|
-
def collect_articles():
|
|
175
|
-
"""Collect articles from biz-daily.
|
|
197
|
+
def collect_articles(article_days=30):
|
|
198
|
+
"""Collect articles from biz-daily.
|
|
199
|
+
|
|
200
|
+
`article_days` = 取最近多少个**日期目录**(原来写死 30)。可调是必要的:
|
|
201
|
+
这个窗口决定了"知识库里有多少文章",而它原先只写在常量里,用户看不到也改不了。
|
|
202
|
+
"""
|
|
176
203
|
items = []
|
|
177
204
|
daily_dir = Path(OUTPUT_ROOT) / 'biz-daily'
|
|
178
205
|
if not daily_dir.exists():
|
|
179
206
|
return items
|
|
180
207
|
|
|
181
|
-
for date_dir in sorted(daily_dir.iterdir(), reverse=True)[:
|
|
208
|
+
for date_dir in sorted(daily_dir.iterdir(), reverse=True)[:article_days]:
|
|
182
209
|
if not date_dir.is_dir():
|
|
183
210
|
continue
|
|
184
211
|
for topic_dir in date_dir.iterdir():
|
|
185
212
|
if not topic_dir.is_dir():
|
|
186
213
|
continue
|
|
187
|
-
|
|
214
|
+
# 这里原先是 topic_dir.glob(' marriage*.md')——前导空格加 marriage 前缀,
|
|
215
|
+
# 在真实的「公众号-标题.md」命名下一个都匹配不到。后果是 collect_articles()
|
|
216
|
+
# 永远返回空表,**语义索引里从来没有任何文章**,只有聊天记录,而且不报错。
|
|
217
|
+
for f in topic_dir.glob('*.md'):
|
|
188
218
|
if f.name == 'README.md':
|
|
189
219
|
continue
|
|
190
220
|
try:
|
|
@@ -217,9 +247,17 @@ def collect_articles():
|
|
|
217
247
|
|
|
218
248
|
# ====== Index Operations ======
|
|
219
249
|
|
|
220
|
-
def build_index(api_key: str, full: bool = False):
|
|
221
|
-
"""Build or update the semantic index.
|
|
250
|
+
def build_index(api_key: str, full: bool = False, days: int = 90, article_days: int = 30):
|
|
251
|
+
"""Build or update the semantic index.
|
|
252
|
+
|
|
253
|
+
`days` / `article_days` 决定这个索引**记多久**:它们原先写死在两处调用里
|
|
254
|
+
(聊天 90 天、日报 30 天),用户既看不见也改不了——而"个人知识库只有三个月记忆"
|
|
255
|
+
正是那个写死的数字造成的。现在由 CLI 显式传进来(见 `search-index --days / --article-days`),
|
|
256
|
+
默认值保持不变,**不悄悄改变代价**。
|
|
257
|
+
"""
|
|
258
|
+
np = require_numpy()
|
|
222
259
|
INDEX_DIR.mkdir(parents=True, exist_ok=True)
|
|
260
|
+
print(f"窗口: 聊天最近 {days} 天 / 日报最近 {article_days} 天", file=sys.stderr)
|
|
223
261
|
|
|
224
262
|
# Load existing index
|
|
225
263
|
existing_ids = set()
|
|
@@ -253,13 +291,13 @@ def build_index(api_key: str, full: bool = False):
|
|
|
253
291
|
items = []
|
|
254
292
|
try:
|
|
255
293
|
conn = open_db(nt_db, nt_key, nt_salt)
|
|
256
|
-
chat_items = collect_chat_messages(conn, name_map, days=
|
|
294
|
+
chat_items = collect_chat_messages(conn, name_map, days=days)
|
|
257
295
|
items.extend(chat_items)
|
|
258
296
|
conn.close()
|
|
259
297
|
except Exception as e:
|
|
260
298
|
print(f"[WARN] 聊天消息收集失败: {e}", file=sys.stderr)
|
|
261
299
|
|
|
262
|
-
article_items = collect_articles()
|
|
300
|
+
article_items = collect_articles(article_days=article_days)
|
|
263
301
|
items.extend(article_items)
|
|
264
302
|
|
|
265
303
|
# Filter new items
|
|
@@ -267,7 +305,8 @@ def build_index(api_key: str, full: bool = False):
|
|
|
267
305
|
print(f"总数据: {len(items)} 条, 新数据: {len(new_items)} 条", file=sys.stderr)
|
|
268
306
|
|
|
269
307
|
if not new_items:
|
|
270
|
-
return {"status": "up_to_date", "total": len(items)
|
|
308
|
+
return {"status": "up_to_date", "total": len(items),
|
|
309
|
+
"chatDays": days, "articleDays": article_days}
|
|
271
310
|
|
|
272
311
|
# Generate embeddings
|
|
273
312
|
print("生成 embeddings...", file=sys.stderr)
|
|
@@ -275,7 +314,8 @@ def build_index(api_key: str, full: bool = False):
|
|
|
275
314
|
embeddings = get_embeddings(texts, api_key)
|
|
276
315
|
|
|
277
316
|
if not embeddings or all(e == [0.0] * EMBEDDING_DIM for e in embeddings):
|
|
278
|
-
return {"error": "Embedding 生成失败,请检查 API key"
|
|
317
|
+
return {"error": "Embedding 生成失败,请检查 API key",
|
|
318
|
+
"chatDays": days, "articleDays": article_days}
|
|
279
319
|
|
|
280
320
|
# Load existing vectors
|
|
281
321
|
if VECTORS_FILE.exists() and not full:
|
|
@@ -303,23 +343,38 @@ def build_index(api_key: str, full: bool = False):
|
|
|
303
343
|
"status": "success",
|
|
304
344
|
"total": len(meta),
|
|
305
345
|
"new": len(new_items),
|
|
346
|
+
"chatDays": days,
|
|
347
|
+
"articleDays": article_days,
|
|
306
348
|
}
|
|
307
349
|
|
|
308
350
|
|
|
309
|
-
def keyword_search(query: str, top_k: int = 10):
|
|
310
|
-
"""Simple keyword fallback: scan articles + messages for keyword matches.
|
|
351
|
+
def keyword_search(query: str, top_k: int = 10, root=None):
|
|
352
|
+
"""Simple keyword fallback: scan articles + messages for keyword matches.
|
|
353
|
+
|
|
354
|
+
`root` 只为可测而参数化(默认就是日报目录)。它原先硬编码成相对路径,
|
|
355
|
+
于是这条路径只能靠 chdir 才能测,也就一直没被测过——而它正是检索不可用时
|
|
356
|
+
真正跑着的那条(本机索引从未构建过)。
|
|
357
|
+
"""
|
|
311
358
|
results = []
|
|
312
359
|
keywords = query.lower().split()
|
|
313
360
|
|
|
314
361
|
# Scan biz-daily articles
|
|
315
|
-
biz_dir = Path('output/biz-daily')
|
|
362
|
+
biz_dir = Path(root or 'output/biz-daily')
|
|
316
363
|
if biz_dir.exists():
|
|
317
364
|
for md_file in sorted(biz_dir.rglob('*.md'), reverse=True):
|
|
318
|
-
|
|
319
|
-
|
|
365
|
+
# **只收真文章。** 日报目录里还躺着 <天>/README.md、<天>/行动建议.md
|
|
366
|
+
# 这类**生成产物**,它们会被当成检索结果返回(实测搜到过
|
|
367
|
+
# 「行动建议 — 2026-08-30」)。而它们**两种深度都有**:既在日期目录下、
|
|
368
|
+
# 也在主题目录下(实测各 4 个),所以按路径判断是不够的——我第一版就是这么
|
|
369
|
+
# 写的,然后拿一个"看不见产物"的 top_k 去验证,误以为修好了。
|
|
370
|
+
#
|
|
371
|
+
# 判据用内容:真文章的 frontmatter 里一定有 url。
|
|
372
|
+
# `quality_eval.read_article` 用的是同一条。
|
|
320
373
|
try:
|
|
321
374
|
content = md_file.read_text(encoding='utf-8')[:5000]
|
|
322
|
-
except:
|
|
375
|
+
except OSError:
|
|
376
|
+
continue
|
|
377
|
+
if not re.search(r'^url:\s*\S', content[:800], re.M):
|
|
323
378
|
continue
|
|
324
379
|
score = sum(content.lower().count(kw) for kw in keywords)
|
|
325
380
|
if score > 0:
|
|
@@ -350,11 +405,99 @@ def keyword_search(query: str, top_k: int = 10):
|
|
|
350
405
|
return results[:top_k]
|
|
351
406
|
|
|
352
407
|
|
|
353
|
-
|
|
408
|
+
# 送进重排的候选数。检索是粗筛("语义相近"),重排是细判("真的回答了问题吗"),
|
|
409
|
+
# 所以池子要比 top_k 大,才有可换的空间。
|
|
410
|
+
RERANK_CANDIDATES = 20
|
|
411
|
+
RERANK_EXCERPT_CHARS = 300
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def build_rerank_questions(count):
|
|
415
|
+
"""**一次请求问 N 个候选**——这是 Jev 最适合"重排"的地方。
|
|
416
|
+
|
|
417
|
+
实测 2 个问题 0.84s、12 个问题 0.91s(`state` 才是开销大头),所以给 20 条候选
|
|
418
|
+
各问一个是 0.9 秒量级,不是 20 次往返。问题编号与候选编号必须严格对应:
|
|
419
|
+
错位了不会报错,只会把最相关的排到最后——所以每条候选在 state 里都带【候选k】标签。
|
|
420
|
+
"""
|
|
421
|
+
return {
|
|
422
|
+
'c%d' % i: {'type': 'noul',
|
|
423
|
+
'instructions': '【候选%d】是否直接回答了上面的问题,'
|
|
424
|
+
'或含有回答它所需的关键信息?仅话题相近不算。' % i}
|
|
425
|
+
for i in range(count)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def rerank(query, results, client=None, pool_size=RERANK_CANDIDATES):
|
|
430
|
+
"""用**一次**决策请求给检索结果重排。
|
|
431
|
+
|
|
432
|
+
没有客户端、候选太少、或调用失败 → 原样返回(也就是这个函数还不存在时的行为)。
|
|
433
|
+
这是刻意的:重排是加分项,不该让检索本身失败。
|
|
434
|
+
|
|
435
|
+
`score` 保持原义(余弦相似度或关键词命中数),新的分数放在 `rerankScore` 里,
|
|
436
|
+
与 `relevanceScore`/`includeScore` 同一套只增字段的做法——下游可能在展示 score。
|
|
437
|
+
"""
|
|
438
|
+
if client is None or len(results) < 2:
|
|
439
|
+
return results
|
|
440
|
+
|
|
441
|
+
pool = results[:pool_size]
|
|
442
|
+
lines = []
|
|
443
|
+
for i, item in enumerate(pool):
|
|
444
|
+
excerpt = (item.get('text') or '').strip().replace(chr(10), ' ')
|
|
445
|
+
lines.append('【候选%d】%s|%s' % (i, item.get('title') or item.get('source') or '',
|
|
446
|
+
excerpt[:RERANK_EXCERPT_CHARS]))
|
|
447
|
+
state = '问题:%s\n\n候选:\n%s' % (query, chr(10).join(lines))
|
|
448
|
+
questions = build_rerank_questions(len(pool))
|
|
449
|
+
# 同问两次的一致性检查:哪一条最直接地回答了问题。逐条打分与这个单选对不上,
|
|
450
|
+
# 通常意味着编号被模型搞混了——那正是重排最危险、也最不容易察觉的失效方式。
|
|
451
|
+
questions['best'] = {
|
|
452
|
+
'type': 'choice',
|
|
453
|
+
'instructions': '哪一条候选最直接地回答了上面的问题?',
|
|
454
|
+
'criteria': {'候选%d' % i: (item.get('title') or None)
|
|
455
|
+
for i, item in enumerate(pool)},
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
try:
|
|
459
|
+
answers, _usage = client.decide(state, questions)
|
|
460
|
+
except Exception as error:
|
|
461
|
+
print(' [WARN] 重排失败(%s),保持原检索顺序:%s' % (type(error).__name__, error))
|
|
462
|
+
return results
|
|
463
|
+
|
|
464
|
+
scored = []
|
|
465
|
+
for i, item in enumerate(pool):
|
|
466
|
+
value = (answers.get('c%d' % i) or {}).get('noul')
|
|
467
|
+
try:
|
|
468
|
+
value = float(value)
|
|
469
|
+
except (TypeError, ValueError):
|
|
470
|
+
value = None
|
|
471
|
+
scored.append((value, i, item))
|
|
472
|
+
|
|
473
|
+
if all(value is None for value, _i, _item in scored):
|
|
474
|
+
print(' [WARN] 重排没有返回任何可用分数,保持原检索顺序')
|
|
475
|
+
return results
|
|
476
|
+
|
|
477
|
+
# 没拿到分数的按原序垫在最后,而不是当成 0 分——"未知"和"不相关"不是一回事。
|
|
478
|
+
scored.sort(key=lambda triple: (triple[0] is None, -(triple[0] or 0.0), triple[1]))
|
|
479
|
+
best = (answers.get('best') or {}).get('choice')
|
|
480
|
+
top = scored[0]
|
|
481
|
+
if best is not None and best != '候选%d' % top[1]:
|
|
482
|
+
print(' [WARN] 重排自检不一致:逐条打分最高的是候选%d,单选却答 %s'
|
|
483
|
+
% (top[1], best))
|
|
484
|
+
|
|
485
|
+
ordered = []
|
|
486
|
+
for value, i, item in scored:
|
|
487
|
+
entry = dict(item)
|
|
488
|
+
if value is not None:
|
|
489
|
+
entry['rerankScore'] = round(value, 3)
|
|
490
|
+
ordered.append(entry)
|
|
491
|
+
ordered.extend(results[pool_size:])
|
|
492
|
+
return ordered
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def search(query: str, api_key: str, top_k: int = 10, rerank_results: bool = True):
|
|
354
496
|
"""Semantic search with keyword fallback."""
|
|
355
497
|
# Try embedding-based search first
|
|
356
498
|
if VECTORS_FILE.exists() and META_FILE.exists():
|
|
357
499
|
try:
|
|
500
|
+
np = require_numpy()
|
|
358
501
|
vectors = np.load(VECTORS_FILE)
|
|
359
502
|
meta = json.loads(META_FILE.read_text(encoding='utf-8'))
|
|
360
503
|
query_embeddings = get_embeddings([query], api_key)
|
|
@@ -362,34 +505,40 @@ def search(query: str, api_key: str, top_k: int = 10):
|
|
|
362
505
|
query_vec = np.array(query_embeddings[0], dtype=np.float32)
|
|
363
506
|
query_vec = query_vec / np.linalg.norm(query_vec)
|
|
364
507
|
similarities = np.dot(vectors, query_vec)
|
|
365
|
-
|
|
508
|
+
# 先取一个比重排池更大的候选集,重排才有可以换的空间。
|
|
509
|
+
pool_size = max(top_k, RERANK_CANDIDATES) if rerank_results else top_k
|
|
510
|
+
top_indices = np.argsort(similarities)[::-1][:pool_size]
|
|
366
511
|
results = []
|
|
367
512
|
for idx in top_indices:
|
|
368
513
|
item = meta[idx]
|
|
369
514
|
results.append({**item, 'score': float(similarities[idx])})
|
|
370
|
-
|
|
515
|
+
if rerank_results:
|
|
516
|
+
results = rerank(query, results, create_client())
|
|
517
|
+
return results[:top_k]
|
|
371
518
|
except:
|
|
372
519
|
pass
|
|
373
520
|
|
|
374
521
|
# Fallback: keyword search
|
|
375
|
-
|
|
522
|
+
results = keyword_search(query, max(top_k, RERANK_CANDIDATES) if rerank_results else top_k)
|
|
523
|
+
if rerank_results:
|
|
524
|
+
# 关键词打分比余弦更粗,重排在这里的收益反而更大。
|
|
525
|
+
results = rerank(query, results, create_client())
|
|
526
|
+
return results[:top_k]
|
|
376
527
|
|
|
377
|
-
results = []
|
|
378
|
-
for idx in top_indices:
|
|
379
|
-
item = meta[idx]
|
|
380
|
-
results.append({
|
|
381
|
-
**item,
|
|
382
|
-
"score": float(similarities[idx]),
|
|
383
|
-
})
|
|
384
528
|
|
|
385
|
-
|
|
386
|
-
"query": query,
|
|
387
|
-
"total": len(meta),
|
|
388
|
-
"results": results,
|
|
389
|
-
}
|
|
529
|
+
# ====== Main ======
|
|
390
530
|
|
|
531
|
+
def add_window_args(parser):
|
|
532
|
+
"""索引窗口(两个子命令共用)。
|
|
533
|
+
|
|
534
|
+
默认值与原先把死的数字**完全一致**(聊天 90 / 日报 30)——加参数不是改代价,
|
|
535
|
+
只是把原来改不了的数字变成改得了的。要建"整段历史"的知识库就显式给一个大值。
|
|
536
|
+
"""
|
|
537
|
+
parser.add_argument('--days', type=int, default=90,
|
|
538
|
+
help='聊天记录收最近多少天(默认 90)')
|
|
539
|
+
parser.add_argument('--article-days', type=int, default=30,
|
|
540
|
+
help='日报文章收最近多少个日期目录(默认 30)')
|
|
391
541
|
|
|
392
|
-
# ====== Main ======
|
|
393
542
|
|
|
394
543
|
def main():
|
|
395
544
|
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
|
@@ -400,31 +549,41 @@ def main():
|
|
|
400
549
|
p = subparsers.add_parser('build')
|
|
401
550
|
p.add_argument('--api-key', help='DeepSeek API key(优先从 config 读取)')
|
|
402
551
|
p.add_argument('--full', action='store_true', help='全量重建')
|
|
552
|
+
add_window_args(p)
|
|
403
553
|
|
|
404
554
|
# update
|
|
405
555
|
p = subparsers.add_parser('update')
|
|
406
556
|
p.add_argument('--api-key', help='DeepSeek API key(优先从 config 读取)')
|
|
557
|
+
add_window_args(p)
|
|
407
558
|
|
|
408
559
|
# search
|
|
409
560
|
p = subparsers.add_parser('search')
|
|
410
561
|
p.add_argument('query', nargs='?')
|
|
411
562
|
p.add_argument('--api-key', help='DeepSeek API key(向量搜索时需要,关键词 fallback 不需要)')
|
|
412
563
|
p.add_argument('--top-k', type=int, default=10)
|
|
564
|
+
p.add_argument('--no-rerank', action='store_true',
|
|
565
|
+
help='不调用决策模型重排,只按向量/关键词相似度返回(回退到引入重排之前)')
|
|
413
566
|
|
|
414
567
|
args = parser.parse_args()
|
|
568
|
+
# 手动跑时的第二道闸门(CLI 那条路已经按 1-36500 校验过):0 天等于建一个空库,
|
|
569
|
+
# 那结果看起来"建好了",实际什么都没有——这种"看起来成功"的失败要挡住
|
|
570
|
+
if getattr(args, 'days', 90) < 1 or getattr(args, 'article_days', 30) < 1:
|
|
571
|
+
json_output({'error': '--days 与 --article-days 都要 ≥ 1(0 天等于索引一个空库)'})
|
|
572
|
+
return
|
|
415
573
|
config = load_config()
|
|
416
|
-
api_key = args.api_key or os.environ.get('DASHSCOPE_API_KEY', '') or config
|
|
574
|
+
api_key = args.api_key or os.environ.get('DASHSCOPE_API_KEY', '') or get_dashscope_key(config)
|
|
417
575
|
|
|
418
576
|
if args.command == 'build':
|
|
419
|
-
result = build_index(api_key, full=args.full)
|
|
577
|
+
result = build_index(api_key, full=args.full, days=args.days, article_days=args.article_days)
|
|
420
578
|
elif args.command == 'update':
|
|
421
|
-
result = build_index(api_key, full=False)
|
|
579
|
+
result = build_index(api_key, full=False, days=args.days, article_days=args.article_days)
|
|
422
580
|
elif args.command == 'search':
|
|
423
581
|
query = args.query or os.environ.get('WEFLOW_SEARCH_QUERY', '')
|
|
424
582
|
if not query:
|
|
425
583
|
result = {"error": "missing search query"}
|
|
426
584
|
else:
|
|
427
|
-
result = search(query, api_key, top_k=args.top_k
|
|
585
|
+
result = search(query, api_key, top_k=args.top_k,
|
|
586
|
+
rerank_results=not args.no_rerank)
|
|
428
587
|
else:
|
|
429
588
|
result = {"error": f"未知命令: {args.command}"}
|
|
430
589
|
|