weflow-cli 1.11.0 → 1.12.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/CHANGELOG.md +200 -0
- package/OPERATIONS.md +120 -1
- package/bin/weflow-cli.ts +53 -3
- package/dist/bin/weflow-cli.js +53 -3
- package/dist/bin/weflow-cli.js.map +1 -1
- package/dist/src/services/assistantEval.d.ts.map +1 -1
- package/dist/src/services/assistantEval.js +3 -1
- package/dist/src/services/assistantEval.js.map +1 -1
- package/dist/src/services/assistantTools.d.ts.map +1 -1
- package/dist/src/services/assistantTools.js +25 -7
- package/dist/src/services/assistantTools.js.map +1 -1
- package/dist/src/services/exportService.d.ts +7 -0
- package/dist/src/services/exportService.d.ts.map +1 -1
- package/dist/src/services/exportService.js +1 -1
- package/dist/src/services/exportService.js.map +1 -1
- package/docs/DECISIONS.md +315 -0
- package/docs/MCP.md +1 -1
- package/docs/NEXT_ACTIONS.md +230 -0
- package/docs/PROJECT_STATE.md +91 -10
- package/docs/RELEASING.md +47 -0
- package/package.json +1 -1
- package/scripts/backfill_articles.py +64 -14
- package/scripts/export_chat_html.py +181 -42
- package/scripts/recall_eval.py +247 -0
- package/scripts/semantic_search.py +659 -46
- package/src/services/assistantEval.ts +3 -1
- package/src/services/assistantTools.ts +2471 -2452
- package/src/services/exportService.ts +14 -2
package/docs/DECISIONS.md
CHANGED
|
@@ -1406,6 +1406,315 @@ which is what the gate is for.
|
|
|
1406
1406
|
semantics are not reliable here), as is any automatic labelling: there is no gold standard for "is this
|
|
1407
1407
|
draft right", so the feature records what it judged and says so rather than claiming calibration.
|
|
1408
1408
|
|
|
1409
|
+
## D-117: MMR 做了,但**默认关** —— 它压掉的"重复"里有相当一部分是有用的
|
|
1410
|
+
|
|
1411
|
+
**Status:** Active(2026-10-16 实测;同批 40 题看排序,另 10 题 × 5 条走判官看准确率)
|
|
1412
|
+
|
|
1413
|
+
**Decision.** 加 `--mmr`(Maximal Marginal Relevance,**λ=0.7**,照 WeKnora 的取值)与配套的多样性指标,
|
|
1414
|
+
但**默认关**。理由是量出来的,而且方向与预期相反。
|
|
1415
|
+
|
|
1416
|
+
**Reason: 在这份语料上,"多样性"与"有用"不是一回事。**
|
|
1417
|
+
- 排序侧(同批 40 题、池 100):多样性确实改善 —— 最终 top-10 两两余弦均值 **0.627 → 0.559**;
|
|
1418
|
+
recall@10 不变(0.95),hit@1 0.72 → 0.75(噪声量级),**recall@5 0.93 → 0.90**(一格一个用例)。
|
|
1419
|
+
- **准确率侧(判官,10 题 × top-5 = 50 条):掉了。** 严格相关 **40% → 30%**,宽松(含 partial)**68% → 64%**。
|
|
1420
|
+
- 为什么:这份语料里的"重复"常常是**多个公众号转载同一件事**(多源确认,不是噪声)—— MMR 压掉的正是
|
|
1421
|
+
这些条目,而判官把它们算作相关。所以它治不了"top-5 里邻居太多"这个抱怨:那个问题的另一面是
|
|
1422
|
+
"同一件事被几个号发过",而**那一半已经由"同文本折叠"(D-114)吃掉了**(折叠是免费的、不损失信息)。
|
|
1423
|
+
再往前压就得开始删有用的东西 —— 实测正是如此。
|
|
1424
|
+
|
|
1425
|
+
**Consequences.**
|
|
1426
|
+
- `--mmr` 与 `--no-dedup` 是两个层次:折叠同文本(默认开)、压近似(默认关)。
|
|
1427
|
+
- λ 不是这里的问题:λ 调大就退化成纯相关性(等于不 MMR),调小只会删得更多。**方向不对,不是权重不对**,
|
|
1428
|
+
所以没有"再调一调"的余地 —— 要它有用,得先有"同一件事的不同角度值得区分"的证据,而这份语料里没有。
|
|
1429
|
+
- 判官口径本身有边界:它逐条独立判、看不到重复,所以"多源确认"在它眼里和"一条新信息"等价。
|
|
1430
|
+
这个偏差对 MMR 不利,但也**不改变结论的方向**(宽松口径同样掉)。
|
|
1431
|
+
|
|
1432
|
+
## D-116: `--fetch-missing` 按 url 补正文,复用日报的缓存与判据
|
|
1433
|
+
|
|
1434
|
+
**Status:** Active(2026-10-16 实测:`XGBoost-SHAP 陕西 PM2.5` 的第一条 201 字 → **965 字**)
|
|
1435
|
+
|
|
1436
|
+
**Decision.** `--with-text` 之外再加 `--fetch-missing [N]`(默认关,`const=3` = 不写字数时最多抓 3 条):
|
|
1437
|
+
**本地正文短于 300 字、且有 `url`** 的那些,按 url 抓一次;抓到的写回 `fullText` 并标 `fetchedFromUrl`。
|
|
1438
|
+
抓取**复用日报那套** `biz_daily.fetch_article_cached`(同一份缓存 `output/.cache/fetch/<md5(url)>.md`、
|
|
1439
|
+
只缓存成功、"先看缓存再上网"),不另写一份。抓失败就保留本地那份,并把条数报出来。
|
|
1440
|
+
|
|
1441
|
+
**Reason: 薄的正文不该逼用户自己放弃,但也不该默认去打扰上游。** 实测那条的日报 md 只有 201 字
|
|
1442
|
+
(好文回顾类文章 md 本身就短),抓回来是完整摘要与结论(2013—2022 年陕西 PM2.5 年均浓度从 56.02 µg/m³ 降下来、
|
|
1443
|
+
主控因子按高程/相对湿度/温度/人口密度排序)。三条边界是**故意的**:
|
|
1444
|
+
- **默认关**:检索不该默认产生对上游的请求(`docs/NEXT_ACTIONS.md` 点过名的规矩);
|
|
1445
|
+
- **有上限**:一次最多抓 3 条,避免"一次检索打十个请求"。与日报的 8–12 秒批量节流不同 ——
|
|
1446
|
+
那是几百篇的礼貌,这是几条的按需,**这个差异是刻意的**,不是漏了节流;
|
|
1447
|
+
- **只抓薄的**:本地已经 3,020 字的那条在这趟里没被碰(实测验过)。
|
|
1448
|
+
|
|
1449
|
+
**Consequences.**
|
|
1450
|
+
- 同一份缓存被日报/回填/检索共用:检索抓过的,日报重跑时直接命中;反过来也一样。
|
|
1451
|
+
- 与日报一样,抓来的正文只有"当时那一版"——源站改稿不会反映到缓存里(要新就删那条缓存重抓)。
|
|
1452
|
+
- 顺手把"预览时标签不可点/微信扫一扫/关注该公众号"这类尾巴补进了整行套话清单:抓回来的那篇结尾挂着它们。
|
|
1453
|
+
清单只删**整行**,不动正文句子。
|
|
1454
|
+
|
|
1455
|
+
## D-115: `search --with-text` 返回**正文**,而不是"用来匹配的那段摘录"
|
|
1456
|
+
|
|
1457
|
+
**Status:** Active(2026-10-16 用户指出:只返回标题/分数/摘录"没什么具体意思",要具体到某篇的内容)
|
|
1458
|
+
|
|
1459
|
+
**Decision.** `search` 加 `--with-text [N]`(N = 每条最多多少字,默认 4000,`0` = 不开)。打开后每条结果多三个字段:
|
|
1460
|
+
`fullText`(**能读的正文**)、`sourceFile`(本地日报 md 的路径)、`url`(原文链接)。正文有两处来源,取**更长**的那份:
|
|
1461
|
+
- 日报 md:`output/biz-daily/<id 里那段相对路径>`,正文用同一套 `extract_article_body` 取;
|
|
1462
|
+
- **正文缓存**:`output/.cache/fetch/<md5(url)>.md` —— 日报/回填抓来的完整正文,通常比 md 更全。
|
|
1463
|
+
聊天记录没有这两层:**索引里那段本来就是消息全文**。
|
|
1464
|
+
|
|
1465
|
+
**默认不开的理由是上下文不是小气**:助手、MCP、RAG 走的是同一个 `search()`,10 条 × 4000 字会把这些通路
|
|
1466
|
+
一次撑进 2 万字;要看正文就显式开(`--top-k 3 --with-text` 是最常用的组合)。
|
|
1467
|
+
|
|
1468
|
+
**读不出来要说出来。** `load_record_text` 对读不到的文件返回 `None`(不是空串),`attach_full_text` 把条数
|
|
1469
|
+
数出来;`sourceFile` 仍然给出路径 —— 用户能自己去开那个文件,而不是以为"这篇没有正文"。
|
|
1470
|
+
|
|
1471
|
+
**`readable_body` 与重排那份摘录的区别(别混)**:`cleanse_passage` 是喂模型的(压成一整行、截 300 字);
|
|
1472
|
+
`readable_body` 是给人看的(去加粗标记、去图片语法、把链接留成文字、删"点击蓝字/在小说阅读器读本章"这类
|
|
1473
|
+
**整行**套话),但**保留段落**。原始文件路径在 `sourceFile` 里,想核对"有没有被我改过"就打开它对比。
|
|
1474
|
+
|
|
1475
|
+
**实测(`用卫星遥感估算 PM2.5` 的第一条)**:日报 md 11.7 KB → 正文 1,393 汉字、正文缓存 1,462 汉字;
|
|
1476
|
+
整理后 `fullText` 1,723 字,含标题、作者期刊、引用格式、三条亮点、导读、**研究结论**(三重交叉验证
|
|
1477
|
+
R² 0.87/0.86/0.83、AOD 特征重要性 40.63%)与相关文章列表。去图片语法前那一版是 3,860 字,其中约 2,100 字
|
|
1478
|
+
是 `` 拼成的一整行 —— 所以必须去。
|
|
1479
|
+
|
|
1480
|
+
## D-114: 结果侧折叠同文本转载(默认开),而索引侧**不**去重
|
|
1481
|
+
|
|
1482
|
+
**Status:** Active(2026-10-16 实测:20 条问句的 top-5 里 7% 是同文本转载,6/20 条问句的 top-5 出现过重复)
|
|
1483
|
+
|
|
1484
|
+
**Decision.** 取出结果时按**正文文本**折叠:同一段文本只留排名最高的那条,其余按条数记进
|
|
1485
|
+
`duplicateCount`(用户该知道"同一篇被几个号发过",而不是以为丢了什么);`--no-dedup` 关掉。空文本的
|
|
1486
|
+
记录各算各的(空串之间不是"同一段")。
|
|
1487
|
+
|
|
1488
|
+
**Reason: 转载占的是名额,不是信息;但索引侧不能去重。** 实测两次都指向同一件事:
|
|
1489
|
+
- 2026-10-15 那次量到索引里 15,995 条只有 15,324 段不同文本(671 条重复);
|
|
1490
|
+
- 2026-10-16 在新默认(重排 + 池 100)下重测:100 个 top-5 名额里有 **7 个**是重复文本,**6/20 条问句**
|
|
1491
|
+
的 top-5 出现过重复(例如"环境监测新规"那句,五部门通稿被 3 个号发、推进会也被 3 个号发 → 一次检索
|
|
1492
|
+
里 4 个名额是副本)。
|
|
1493
|
+
而**不在建索引时去重**的理由是具体的:`--from <公众号>`(D-109 之后那条属性筛)靠的是 `id` 里的文件名,
|
|
1494
|
+
把被折叠的账号从索引里删掉,按号查就会缺记录。所以:**索引留着全部,折叠只发生在展示**。
|
|
1495
|
+
所以这一条**不改变 recall 指标**(ground truth 是内容,副本折叠不影响首条的 rank),只让"实际看到的东西"
|
|
1496
|
+
变多。这也是为什么它是纯展示层改动、不需要重建索引。
|
|
1497
|
+
|
|
1498
|
+
**Consequences.**
|
|
1499
|
+
- 每条结果多一个 `duplicateCount` 字段(只增字段,不改既有字段语义);CLI JSON 里也带 `dedup` 开关状态。
|
|
1500
|
+
- 与 `--dedup`(评测台的那个)**不是一回事**:评测台的 `--dedup` 是**排名前**折叠,用来量"去重后的名次";
|
|
1501
|
+
生产这一条是**取出结果时**折叠。两者顺序不同、目的不同,别混。
|
|
1502
|
+
- 想回到"原样返回所有副本":`--no-dedup`。
|
|
1503
|
+
|
|
1504
|
+
## D-113: 真正的杠杆是"重排能看到多少候选" —— 池子 20 → 100,并更正 D-112 那处倒退
|
|
1505
|
+
|
|
1506
|
+
**Status:** Active(2026-10-16 实测;同一批 40 题、同 seed,只改重排候选池)
|
|
1507
|
+
|
|
1508
|
+
**Decision.** 重排候选池(`RERANK_CANDIDATES`)默认从 **20 提到 100**,并把它做成可调:Python 侧
|
|
1509
|
+
`--rerank-pool`、CLI 侧同名旗标(**没传就不转发**,默认值只在 Python 侧定义一处)。曲线:
|
|
1510
|
+
|
|
1511
|
+
| 池子 | recall@10 | recall@5 | hit@1 | MRR@10 | nDCG@10 | 碎片/文章 recall@10(最弱那格) |
|
|
1512
|
+
| --- | --- | --- | --- | --- | --- | --- |
|
|
1513
|
+
| 20(旧默认) | 0.82 | 0.82 | 0.65 | 0.72 | 0.75 | 0.70 |
|
|
1514
|
+
| 50 | 0.88 | 0.88 | 0.68 | 0.75 | 0.78 | 0.80 |
|
|
1515
|
+
| **100(新默认)** | **0.95** | **0.93** | **0.72** | **0.80** | **0.83** | **1.00** |
|
|
1516
|
+
| 200 | 0.95 | 0.93 | 0.75 | 0.82 | 0.85 | 1.00 |
|
|
1517
|
+
|
|
1518
|
+
**Reason: 重排只能重排它拿到的那些。** 之前的提升假象是"融合 + 重排",而这两个都只在**召回交上来的候选**
|
|
1519
|
+
里做文章:池子 20 意味着正确答案只要在融合排名 21 名开外就永远进不来。把池子放到 100 之后,recall@10
|
|
1520
|
+
就到 **0.95**、MRR **0.80** —— 对照公开的生产参考阈值(Recall@10 ≥ 0.90、MRR ≥ 0.75),这一项**越过了**。
|
|
1521
|
+
100 → 200 只剩噪声级增益(hit@1 +0.03、@10 不动),所以取 100,并且**不许**拿这一批题继续往上调。
|
|
1522
|
+
|
|
1523
|
+
**代价**:每次重排的提示从 ~2.5k tokens 涨到 ~20k(deepseek 系约 ¥0.01/次,几秒延迟)。
|
|
1524
|
+
嫌贵就把池子调小(`--rerank-pool 20`),这比 `--no-rerank` 温和。
|
|
1525
|
+
|
|
1526
|
+
**更正 D-112 的一处记录。** D-112 记了"标题/文章那格 recall@5 从 1.00 掉到 0.90,一处倒退"。重跑同一批题后
|
|
1527
|
+
那一格是 **1.00**(hit@1 也有 0.80/0.90 两次数值不同)——**那是噪声**:重排温度虽为 0,模型仍不完全确定,
|
|
1528
|
+
而每格只有 10 条题,一条就是 0.10。结论不变("不能据此说重排有害"),但当时那句话说重了:**该格没有倒退**。
|
|
1529
|
+
教训写在这里:n=10/格的分格指标不要用来判单点得失,要看整体与最弱格的方向。
|
|
1530
|
+
|
|
1531
|
+
**踩到的坑(值得记)**:CLI 侧先用 `parseCliInteger(opts.rerankPool, ...)` 把没传的选项也解析了一遍 ——
|
|
1532
|
+
commander 对未提供的选项给 `undefined`,于是**用户不传这个旗标就会被自己的校验判成非法参数**
|
|
1533
|
+
(`rerank-pool 必须是 5-500 的整数`,搜索直接不工作)。改成"没传就不转发"。这也说明:**默认值只该有一处**。
|
|
1534
|
+
|
|
1535
|
+
## D-112: 把"召回 + 重排"的第二段点亮 —— 重排改用本机可用的模型,并给出降级状态
|
|
1536
|
+
|
|
1537
|
+
**Status:** Active(2026-10-15 实测:同一批题加上重排,hit@1 0.55→0.65、nDCG@10 0.63→0.74)
|
|
1538
|
+
|
|
1539
|
+
**Decision.** 重排这一段**恢复工作**,但换一个真正能连上的提供方:先按原契约(Jev/Typesafe)试,
|
|
1540
|
+
失败或不可用则退到**配置里的对话模型**(OpenAI 兼容,本机是 deepseek 系);两条都失败就保持原序,
|
|
1541
|
+
并把 `RERANK_STATE`(`provider` / `degraded` / `error` / `scored`)**带进 JSON 输出**。同时借 WeKnora
|
|
1542
|
+
的做法,在送进模型前做**段落清洗**(去 `**加粗**`、公众号套话、署名行、图片/链接语法)——
|
|
1543
|
+
候选摘录本来只有 300 字,不清洗会有一大半注意力花在噪声上。
|
|
1544
|
+
|
|
1545
|
+
**Reason: 主流管线是两段,而我们的第二段一直是黑的。** 本机 Typesafe 返 `HTTP 451 not available in your
|
|
1546
|
+
region`,于是"重排"只在 stderr 打一行 WARN(CLI 还不转发子进程 stderr)——查不到、也没生效。参考口径:
|
|
1547
|
+
- MS MARCO nDCG@10:BM25 **0.184** → 稠密 **0.396** → 稠密+重排 **0.452**;
|
|
1548
|
+
- 一个公开的 retrieve&rerank 例子(NanoNFCorpus):混合 RRF nDCG@10 **32.95** → 混合 RRF+重排 **36.16**,
|
|
1549
|
+
MRR 50.19 → 55.77;
|
|
1550
|
+
- 中文域:bge-reranker-large 把 Top-10 准确率从 80.7% 提到 **91.2%**;
|
|
1551
|
+
- 生产参考阈值(腾讯云):HitRate@3 ≥ 90%、Recall@10 ≥ 0.90、MRR ≥ 0.75、nDCG@5 ≥ 0.80。
|
|
1552
|
+
- 反面也记一笔:有文献报告重排器**会倒退**(jina-reranker-v2-base 在 BM25 形状的候选池上 −7.5pp),
|
|
1553
|
+
所以"加了重排"不是无条件更好,必须用同一批题量。
|
|
1554
|
+
|
|
1555
|
+
同一批题(40 条:碎片/标题各 20,seed 固定)实测:
|
|
1556
|
+
|
|
1557
|
+
| 组 | 指标 | 不重排 | **加重排** |
|
|
1558
|
+
| --- | --- | --- | --- |
|
|
1559
|
+
| 全部 | hit@1 | 0.55 | **0.65** |
|
|
1560
|
+
| 全部 | recall@5 | 0.65 | **0.80** |
|
|
1561
|
+
| 全部 | MRR@10 | 0.59 | **0.72** |
|
|
1562
|
+
| 全部 | nDCG@10 | 0.63 | **0.74** |
|
|
1563
|
+
| 碎片/文章(最弱那格) | recall@5 | 0.30 | **0.70** |
|
|
1564
|
+
| 碎片/文章 | MRR@10 | 0.32 | **0.53** |
|
|
1565
|
+
| 标题/聊天 | hit@1 | 0.60 | **0.80** |
|
|
1566
|
+
| 标题/文章 | recall@5 | 1.00 | 0.90 ⚠️ |
|
|
1567
|
+
|
|
1568
|
+
**一处倒退如实记下**:标题/文章那格 recall@5 从 1.00 掉到 0.90 —— 一个用例被重排挤出了前五(候选池 20,
|
|
1569
|
+
重排认为别的更相关)。n=10 时一个用例就是 0.10,**不能据此说重排有害**,但也不能装作没发生:
|
|
1570
|
+
如果后续在这格上重复出现,对策是"扩大候选池"或"保住召回侧的第 1 名",**这两条都还没做**。
|
|
1571
|
+
|
|
1572
|
+
**Consequences.**
|
|
1573
|
+
- **每次检索现在会多花一次模型调用**(约 20 条候选 × 300 字 ≈ 2–3k tokens,deepseek 系约 ¥0.004/次)
|
|
1574
|
+
和几秒延迟;`search`、助手的 `search_semantic`、MCP 的 `search_knowledge`、`chat` 走的是同一个
|
|
1575
|
+
`search()`,所以它们**一起变了**。要省就 `--no-rerank`。
|
|
1576
|
+
- 重排仍然"不该让检索失败":没有 key、网络断、模型返回的编号不齐(这是重排最危险的失效方式)都
|
|
1577
|
+
**保持原序**并标 `degraded`;区别是现在这件事**看得见**了。
|
|
1578
|
+
- 借 WeKnora 的两条还没做:**重排后的 MMR 去冗余**(它用 λ=0.7)与**候选不足时迭代扩池**。
|
|
1579
|
+
MMR 与"单答案召回"指标是相冲的(它故意压掉相似项),要量的话得用"去重后的名次"或人工判据。
|
|
1580
|
+
|
|
1581
|
+
## D-111: 召回有了评测台,门控后的关键词融合因此翻成默认
|
|
1582
|
+
|
|
1583
|
+
**Status:** Active(判据是 2026-10-15 用新评测台跑的三组 A/B;本条同时更正 D-110 的结论)
|
|
1584
|
+
|
|
1585
|
+
**Decision.** 两件事一起定:
|
|
1586
|
+
1. **留一个能复跑的评测台** `scripts/recall_eval.py`。指标全部用业界标准口径,**不自创**:Recall@1/5/10、
|
|
1587
|
+
MRR@10、nDCG@10、Hit@1(中文规范《人工智能 检索增强生成系统测评规范》用的是"命中率"这个词),
|
|
1588
|
+
外加**覆盖(coverage)**——这是这类语料特有的那一项:答案在库里根本不存在时任何排序都救不了,
|
|
1589
|
+
把它算进召回率是在诬赖排序,所以每题先判"库里有没有"。标准答案来自两处:从记录**自己正文中段**
|
|
1590
|
+
取的 12 字干净片段(`frag`,唯一性已校验)与标题/开头 30 字(`title`),都在运行时生成、**不把语料
|
|
1591
|
+
写进仓库**;手写问句走 `--queries-file`(仓库外的 TSV:问题+锚串)。
|
|
1592
|
+
对标过的同类做法:BEIR/MTEB(nDCG@10 为主指标)、C-MTEB(35 个中文数据集)、MS MARCO(MRR@10)、
|
|
1593
|
+
RAGAS(生成侧);最接近的线下实现是 PsyRAG-Lab(中文,BM25 vs dense vs RRF vs cross-encoder,
|
|
1594
|
+
报 Hit@K/Recall@K/MRR/nDCG/MAP)——它也是**先比再定**而不是假定 RRF 有用。没有现成基准能覆盖
|
|
1595
|
+
"个人微信语料 + 文章/聊天双线 + 多号转载"这三件事,所以语料层我们自建,指标层不另立。
|
|
1596
|
+
2. **关键词侧加门控后,把它翻成默认开**(`--no-hybrid` 关)。门控两条,缺一不可:只让**命中稀有词**
|
|
1597
|
+
(`df/N ≤ 5%`)的文档投票;并且它们的 BM25 分数要**接近最高分**(≥ 0.5×)。问句里一个稀有词都
|
|
1598
|
+
没有("怎么用/有没有"这类)⇒ 关键词侧**整个弃权**。
|
|
1599
|
+
|
|
1600
|
+
**Reason: 让关键词侧起作用的是门控,不是融合本身。** 同一批题(frag 40 / title 40 / 手写问句 12):
|
|
1601
|
+
|
|
1602
|
+
| 排名方式 | 碎片 recall@10 | 碎片 recall@5 | 碎片 hit@1 | 标题 hit@1 | 手写问句 |
|
|
1603
|
+
| --- | --- | --- | --- | --- | --- |
|
|
1604
|
+
| 只用向量 | 0.91 | 0.89 | 0.60 | 0.83 | 0.75 |
|
|
1605
|
+
| 无门控 RRF(D-110 的版本) | 0.91 | 0.89 | 0.60 | 0.83 | 0.75 |
|
|
1606
|
+
| **门控 RRF(0.05 / 0.5,现行默认)** | **1.00** | **0.97** | **0.80** | **0.86** | 0.75 |
|
|
1607
|
+
|
|
1608
|
+
无门控版在**同一批题上是零增益**——与纯向量一模一样;这从另一面解释了 D-110 为什么量出"有害":
|
|
1609
|
+
问题的根源是虚词投票,不是融合这个动作。门控把它修好之后,三个台子**都不掉**,最弱那格(碎片召回,
|
|
1610
|
+
"文里那句话在哪篇")从 0.91/0.60 提到 1.00/0.80。阈值敏感性也量了:宽松 (0.10/0.3) 略弱、紧的
|
|
1611
|
+
(0.02/0.7) 略好,**平台是平的** ⇒ 这个数字不承重,取中间值,并且**不许**再拿这一批题去微调它
|
|
1612
|
+
(避免把偶然结果固化)。
|
|
1613
|
+
|
|
1614
|
+
**Consequences.**
|
|
1615
|
+
- `search` 的默认排序与 1.12.0 之前不同:`score`(余弦)**不再单调**,顺序来自融合名次。想回到纯向量:
|
|
1616
|
+
`--no-hybrid`。助手 / MCP / RAG 走的是同一个 `search()`,所以它们也一起变了。
|
|
1617
|
+
- 评测台要花钱(每题一次 embedding,十几 tokens)且**问句出境**,所以它和 `search-index` 一样要求
|
|
1618
|
+
`--yes`;它**不写任何文件**,报告只打到 stdout(JSON 走 `--json`)。
|
|
1619
|
+
- `--dedup`(同文本合并再排名)实测只值 +0.03 左右的 recall@10:多号转载确实吃名额(实测 50 条 top-5
|
|
1620
|
+
里有 4 条是同文本),但不是主因。它的正确位置是**建索引时**(省向量、省名额),不是每次检索时做。
|
|
1621
|
+
- 这一条**不影响** D-110 记的"评测要把'没命中'与'库里没有'分开"那条纪律——评测台的 `coverage` 字段
|
|
1622
|
+
就是把它落到代码里的样子;`HYSPLIT` 那类问句(库里 0 条记录)因此不会被算成排序失败。
|
|
1623
|
+
|
|
1624
|
+
## D-110: 混合检索做成了开关而不是默认 —— 量出来它只在一种问句上赢
|
|
1625
|
+
|
|
1626
|
+
**Status:** Active(判据来自 2026-10-15 的两组 A/B:同一份索引、同一批查询,只换排名方式)
|
|
1627
|
+
|
|
1628
|
+
**Decision.** 保留"向量名次 + 本地 BM25 名次用 RRF 合成"这条路,但**默认关**,由 `search --hybrid` 打开
|
|
1629
|
+
(CLI 与 Python 两侧同名同义,默认值两边一致)。关键词那一侧完全本地:中文按**字符二元组**切词(没有分词器,
|
|
1630
|
+
也就不需要词表,代价是虚词也成词——那交给 BM25 的 idf 自己去压),一遍扫过所有记录算分,**不建额外索引文件**,
|
|
1631
|
+
所以不存在"关键词索引与正文不同步"这种失效。实测每次 31 ms(中位,16k 条)。
|
|
1632
|
+
|
|
1633
|
+
**Reason: 默认值要挑"多数问句更好"的那个,而融合只在少数问句上更好。**
|
|
1634
|
+
- 逐字碎片查询(16 条,从记录正文中段取 12 字原话):BM25 单独 **16/16**、向量单独 9/16、RRF 12/16。
|
|
1635
|
+
关键词侧近乎满分,融合反被向量侧拖低。
|
|
1636
|
+
- 真实问句(5 条):RRF 在 **1 条**上明显更好 ——「卫星遥感反演气溶胶光学厚度」把目标(基于遥感的 PM2.5 反演)
|
|
1637
|
+
从第 3 提到第 2;在另外 4 条上**都不优于**向量单独,其中 2 条更差 ——「有没有讲遥感或卫星数据用在空气质量
|
|
1638
|
+
监测上的文章」的正确首条被挤出前五,「后向轨迹 HYSPLIT 怎么用」被"怎么用"这类虚词带偏。
|
|
1639
|
+
- 机理清楚:RRF 给两侧**同等票权**,而中文问句里的虚词会让 BM25 侧变成纯噪声。
|
|
1640
|
+
|
|
1641
|
+
**Consequences.**
|
|
1642
|
+
- 开 `--hybrid` 后输出的 `score`(余弦)**可能不再单调** —— 顺序来自 RRF,而 `score` 仍按原义报(没有改它的含义,
|
|
1643
|
+
改的是顺序)。看排序别只看这一列。
|
|
1644
|
+
- 想在真实问句上默认开,先解决虚词投票(按 idf 设阈、只让稀有词投票),并用**同一套 A/B** 复核;
|
|
1645
|
+
不许拿这 5 条查询调阈值 —— 那正是这个仓库点过名的"把偶然结果固化"。
|
|
1646
|
+
- 这条路不花钱、不出境:关键词侧全离线,查询嵌入那一次调用与原来完全相同。
|
|
1647
|
+
|
|
1648
|
+
## D-109: 语义检索的 embedding 走阿里云,出境的是每条素材的截断片段
|
|
1649
|
+
|
|
1650
|
+
**Status:** Active(做法与成本来自 2026-10-15 的真机实测:按默认窗口建了一份 16,027 条的索引,并跑了真实检索)
|
|
1651
|
+
|
|
1652
|
+
**Decision.** 语义检索(`weflow-cli search` / `chat`、助手的 `search_semantic`)的向量由阿里云百炼
|
|
1653
|
+
(DashScope)的 `text-embedding-v4` 算,1024 维,密钥走 `dashscopeApiKey`(机器绑定加密保存)。**出境的是每条
|
|
1654
|
+
素材的一小段纯文本**:文章 = 标题 + 正文前 500 字(`semantic_search.py` 的 `collect_articles`),聊天 = 消息内容
|
|
1655
|
+
前 500 字(同文件的 `collect_chat_messages`)。**不出的**:数据库与密钥、图片与文件本体、文件路径、好友名/群名,
|
|
1656
|
+
以及任何一条没被截进那 500 字的正文。索引与元数据都留在本地(`output/.semantic_index/{vectors.npy,meta.json}`),
|
|
1657
|
+
查询时才把**这一次的问句**发出去一次。
|
|
1658
|
+
|
|
1659
|
+
**Reason: 本地没有能用的中文向量,而它必须是可选且看得见的。** 默认不建索引——没配 key 时
|
|
1660
|
+
`search`/`chat` 直接说缺 key,不拿空 key 去请求。写死 500 字是因为"全文出境"是另一档暴露面与另一档账单:
|
|
1661
|
+
实测这份索引累计送了 **587 万字 ≈ 391 万 tokens ≈ ¥2.0**(估算按 1.5 字/token、¥0.514/百万 token),
|
|
1662
|
+
而单次查询只送 12 tokens ≈ **¥0.00001**——真正贵的是建索引那一次,它只在窗口变化时才需要重跑。
|
|
1663
|
+
|
|
1664
|
+
**代价要一起写下来 —— 而且 2026-10-15 当天把主因查清了。** 当时"截断"只是可疑方向;同一天实测最近 30 个日期目录的
|
|
1665
|
+
10,759 篇日报 md,**含 `## 正文` 的有 0 篇**(真实结构是 `frontmatter → # 标题 → > 来源 → --- → ## AI 摘要 → --- →
|
|
1666
|
+
重复标题 → 正文`),而取正文的判据只有一句 `startswith('## 正文')`,于是每一篇都退回 `content[:1000]`:送去 embedding
|
|
1667
|
+
的是 YAML 头(抽样 300 篇:100% 含 `title:`,汉字占比中位 **0.16**)。所以"文章线只能靠标题排序"的原因是**正文压根没进索引**,
|
|
1668
|
+
不是截断。已改成按真实结构取正文(同一批 300 篇:汉字占比中位 0.16 → **0.80**,299/300 篇改变),并配了一条
|
|
1669
|
+
**会因"退回老写法"而变红**的测试(`test/semantic_article_body_test.py`,夹具照真实结构写、要断言的那句话放在第 1000 字之后)。
|
|
1670
|
+
|
|
1671
|
+
还有两件**没做**,一起记在这里:
|
|
1672
|
+
- **要 `--full` 重建索引才生效**:增量建索引只嵌"新 id",提取器改了而老记录不动 —— 也就是说"修好了"和"索引里还是旧数据"
|
|
1673
|
+
可以同时成立。这是本轮实测最容易误判的一处。
|
|
1674
|
+
- **素材本身的厚度**:日报 md 中位 912 字(汉字 259),而正文缓存是 2,063、Vault 笔记是 422。要不要给文章线换更厚的素材
|
|
1675
|
+
是个独立取舍(更厚 = 更多出境与成本),本轮没动。
|
|
1676
|
+
同一批实测还留下一个读数:**原始余弦分跨问句不可比**(同一个库上,一个问句的 0.58 不必比另一个问句的 0.50 更"像")。
|
|
1677
|
+
|
|
1678
|
+
**Consequences.**
|
|
1679
|
+
- 索引是**派生物**:删掉它只影响耗时与再次出境的成本,不影响任何原文。
|
|
1680
|
+
- 重排是**第二段出境**(候选片段发给决策模型)。本机实测 Typesafe 返回 **HTTP 451 "not available in your
|
|
1681
|
+
region"**,所以重排在这台机器上一直不生效、等同 `--no-rerank`。这是**已知边界**,不是"决定不重排"。
|
|
1682
|
+
- 想完全不出境只有不用语义检索:`vault search` 是纯本地关键词检索,不联网、不调模型。
|
|
1683
|
+
- 上面所有金额都是**估算**(token 按 1.5 字/token、单价取百炼公开价),不是账单读数。
|
|
1684
|
+
|
|
1685
|
+
## D-108: 回填与日报共用一份正文缓存,但两条流水线仍然是两条
|
|
1686
|
+
|
|
1687
|
+
**Status:** Active(判据来自 2026-10-14 的真机实测:一天的回填把缓存从 57 个文件灌到 202 个)
|
|
1688
|
+
|
|
1689
|
+
**Decision.** 历史回填 `backfill_articles.py` 的正文抓取改为调用**日报自己那个** `biz_daily.fetch_article_cached`:
|
|
1690
|
+
成功结果写进 `output/.cache/fetch/<md5(url)>.md`(与日报同一份、仍是"只缓存非空成功"),命中缓存时不联网;
|
|
1691
|
+
两边都有 `--no-fetch-cache`,**同名同义**(跳过"读",成功的仍然写回,所以它同时是"刷新旧缓存")。两条流水线
|
|
1692
|
+
**本身不合并**:输出形态不同(纯文本 vs HTML+图片)、抓取策略不同(回填并发且不节流,日报串行且刻意 8–12 秒
|
|
1693
|
+
节流以少打扰上游)——共用缓存 ≠ 共用流程。节流的 `sleep` 在日报的**调用循环**里、不在抓取函数里,所以命中
|
|
1694
|
+
缓存的篇既不联网也不睡。
|
|
1695
|
+
|
|
1696
|
+
**Reason: 让"补历史"与"正式产出"少重抓一遍。** 实测 2026-10-13 那天:回填灌了 **145 条缓存**
|
|
1697
|
+
(57 → 202 个文件,与"尝试 152、成功 145"逐条对上),而这 145 篇正是回填认为"正文太短、必须联网取"的那批。
|
|
1698
|
+
|
|
1699
|
+
**但接力是部分的,这一点别读多。** 日报的 Phase 1 对**每篇有 url 的文章**都抓(它要完整正文来写摘要),
|
|
1700
|
+
而回填只抓"本地正文短于 100 字"的那些、其余的连请求都不发。实测同一天:485 篇里**只有 145 篇在缓存里,
|
|
1701
|
+
日报重跑仍需联网 340 篇**(按 8–12 秒节流约 55 分钟)。所以要"把这一天正式产出"仍然要付大部分抓取成本,
|
|
1702
|
+
省下的是那 145 篇。反向那一半是成立的:日报跑过的天,回填重跑时不再打扰上游。
|
|
1703
|
+
|
|
1704
|
+
想让它真的全命中,得让回填**也**去取那些本不需要取的正文——那是**增加对上游的请求**,而
|
|
1705
|
+
`docs/NEXT_ACTIONS.md` 明确说过不要"通过改变默认行为造成额外请求",所以默认不这么做;要做就做一个显式开关
|
|
1706
|
+
(例如 `--prefetch-all`),由使用者决定。**这项没做**,留在这里当已知边界。
|
|
1707
|
+
|
|
1708
|
+
**Consequences.**
|
|
1709
|
+
- 缓存只代表"曾经成功抓到过",**不代表正文是最新的**:源站改稿不会反映到缓存里。要刷新只能显式
|
|
1710
|
+
`--no-fetch-cache`;默认行为不为"可能过期"付费(这是 `docs/NEXT_ACTIONS.md` 点名的取舍)。
|
|
1711
|
+
- `output/.cache/fetch/` 从此是两条线共用的状态。删掉它只影响耗时(各自退回"要重新抓"),不影响产出内容。
|
|
1712
|
+
- **抓取失败与"正文本来就短"从此分开记**:`.articles.json` 多一个可选键 `fetchFailed`(该轮抓取失败的篇数),
|
|
1713
|
+
`day_done` 据此判那天**没做完** ⇒ 下次重跑只重试失败的篇,成功的那批走缓存。它与另两个标记
|
|
1714
|
+
(`topicFilter`、`truncated`)有一点不同:**不是 sticky 的**——一次零失败的全量跑说明(含上轮失败的)都补齐了,
|
|
1715
|
+
标记就该消失,否则那天会被永远重做。两种情况的区别是有代价的:把它们混成一句"跳过 N 篇(正文太短)"时,
|
|
1716
|
+
一次网络抖动会被写成"这几篇本来就没有正文",而那些篇再也不会被重试(同仓库量过:84 次失败里 28 次重试可得)。
|
|
1717
|
+
|
|
1409
1718
|
## D-107: 报"客户端运行时版本",判据是活进程,不是 exe 里的字面量
|
|
1410
1719
|
|
|
1411
1720
|
**Status:** Active(做法来自一份**实测的启动器逆向报告**——报告在本仓库外,含真实的开发者用户名与
|
|
@@ -2818,6 +3127,12 @@ infer completeness from the presence of `.articles.json`. An unknown `--topic` i
|
|
|
2818
3127
|
opened, since a typo would otherwise select 0 articles and report "nothing to do" - indistinguishable from a day that
|
|
2819
3128
|
genuinely had none.
|
|
2820
3129
|
|
|
3130
|
+
**A third marker joined them on 2026-10-14**: `fetchFailed` records a body fetch that failed *in that run*, so one
|
|
3131
|
+
network hiccup is no longer written down as "skipped N (body too short)" with the day still counted as done - which
|
|
3132
|
+
is how those articles used to disappear for good. Unlike `topicFilter` and `truncated` it is **not** sticky: a full
|
|
3133
|
+
run with zero failures proves the earlier ones were recovered, so the marker clears (and the cache makes that retry
|
|
3134
|
+
cheap). See D-108.
|
|
3135
|
+
|
|
2821
3136
|
## D-059: The ball's eyes follow the mouse - and that makes the cursor position a data flow, so it is written down
|
|
2822
3137
|
|
|
2823
3138
|
**Status:** Active
|
package/docs/MCP.md
CHANGED
|
@@ -121,7 +121,7 @@ client-local config that is not in version control.
|
|
|
121
121
|
| `wechat.search_knowledge` | Fuzzy-search concept pages in the knowledge base. Searches **one of the two lines by default** (the article one); pass `line: "chat"` or `"all"` for the conversation line. | Vault Wiki output |
|
|
122
122
|
| `wechat.search_memory` | Search long-term assistant memory. | `~/.weflow-cli/assistant_memory.json` |
|
|
123
123
|
| `wechat.search_chats` | Find which conversation discussed something (literal words). | Decrypted WeChat database, **words leave the machine - `confirm: true` required** |
|
|
124
|
-
| `wechat.search_semantic` | Find chat messages by meaning, not literal words. Defaults to the article line; pass `line: "chat"` or `"all"`. | `dashscopeApiKey` config, network - **query and hits leave the machine, `confirm: true` required** |
|
|
124
|
+
| `wechat.search_semantic` | Find chat messages by meaning, not literal words. Defaults to the article line; pass `line: "chat"` or `"all"`. Pass `with_text: true` when the **content** is wanted rather than titles: each hit then carries up to ~1500 characters of the local article body (from the daily markdown, or the shared fetch cache) instead of an 80-character snippet. | `dashscopeApiKey` config, network - **query and hits leave the machine, `confirm: true` required** |
|
|
125
125
|
| `wechat.who_owes_reply` | Rank the conversations waiting on a reply. | Decrypted WeChat database, **text leaves the machine - `confirm: true` required** |
|
|
126
126
|
| `wechat.draft_reply` | Draft candidate replies. | Decrypted WeChat database, **text leaves the machine - `confirm: true` required** |
|
|
127
127
|
| `wechat.export_chat` | Export one conversation to HTML, txt, json or excel. | Decrypted WeChat database, **writes under `output/exports/`** |
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# WeFlow CLI 下一阶段行动建议
|
|
2
|
+
|
|
3
|
+
> 编写日期:2026-10-14
|
|
4
|
+
> 依据:本地仓库 `package.json` 的 1.11.1 版本、截至本次梳理的提交与工作区状态。
|
|
5
|
+
> 用途:把近期改进方向拆成可以独立评审、实现和验收的任务。项目现状以 [PROJECT_STATE.md](PROJECT_STATE.md)、技术取舍以 [DECISIONS.md](DECISIONS.md) 为准。
|
|
6
|
+
|
|
7
|
+
## 建议结论
|
|
8
|
+
|
|
9
|
+
下一阶段应先完成三个“收口”任务:
|
|
10
|
+
|
|
11
|
+
1. 让历史文章回填与日报共用缓存的行为可控、可验证,尤其补上强制重新抓取的入口。
|
|
12
|
+
2. 统一不同导出格式的媒体状态语义,并补上导出失败和跨环境的验证证据。
|
|
13
|
+
3. 分批补齐 CLI 在 `--json` 模式下的运行时错误结构,保持机器调用结果稳定。
|
|
14
|
+
|
|
15
|
+
这三项都能建立在现有实现上,不需要引入新的数据处理架构。同步游标、MCP 扩面、资料包和更复杂的 AI 功能可在上述边界稳定后,根据实际集成需求推进。
|
|
16
|
+
|
|
17
|
+
## 当前基线
|
|
18
|
+
|
|
19
|
+
- 本地代码版本为 `1.11.1`。最近一批提交完成了 Markdown 连图、文档本体导出和文档导出的多项边界修复。
|
|
20
|
+
- 本次读取时,本地 `master` 比 `origin/master` 多 11 个提交;工作区中 `scripts/backfill_articles.py` 和 `test/backfill_articles_test.py` 有未提交改动。它们正在让历史回填复用日报正文缓存,本文件将这部分视为进行中的工作,不把它描述为已发布功能。
|
|
21
|
+
- HTML 导出已有媒体覆盖报告;Markdown 和 JSON/TXT/Excel 的媒体导出复用同一份清单。文件本体导出目前只有本机实测记录,Issue #11 尚待报告者确认及其他环境验证。
|
|
22
|
+
- `sync run/status/verify` 和本地同步状态契约已经存在,但稳定游标尚未作为对外能力承诺。
|
|
23
|
+
- 项目已记录扩展边界:代码扩展在仓库内完成;外部程序通过 CLI、JSON 或 MCP 接入。近期不建议增加进程内插件加载器。
|
|
24
|
+
|
|
25
|
+
## P0:完成正在进行的缓存衔接
|
|
26
|
+
|
|
27
|
+
**已完成(2026-10-14)**,验收的六类行为各有测试(命中不联网 / 未命中去抓 / 成功写缓存 / 失败不写 /
|
|
28
|
+
强制重抓刷新旧缓存 / 重抓失败不动旧缓存 / 缓存不可写不影响这一篇 / 并发下标不错位),关键两条做过变异检查。
|
|
29
|
+
落地的几处与本节设想不同的地方:回填与日报**共用同一条缓存**(`fetch_article_cached`,成功才写);
|
|
30
|
+
`--no-fetch-cache` 两边同名同义;`--refresh` 的帮助写明它只管"做哪些天"。另外**多做了一个标记**:
|
|
31
|
+
抓取失败与"正文本来就短"从此分开记(`.articles.json` 的 `fetchFailed`,那天判为没做完、重跑只重试失败的篇)——
|
|
32
|
+
它直接对应本节验收里"失败后重跑可继续补齐,不会将部分日期或部分文章误标为完整"那一条。
|
|
33
|
+
判据与代价写在 D-108;顺带量到一件事:**接力是部分的**——日报对每篇有 url 的都抓正文,回填只抓"正文过短"的,
|
|
34
|
+
2026-10-13 那天 485 篇里只有 145 篇进了缓存(详见 D-108,未做显式预取开关)。
|
|
35
|
+
|
|
36
|
+
### 目标
|
|
37
|
+
|
|
38
|
+
使回填和日报共享正文缓存后,用户仍能明确选择复用缓存或重新抓取;重跑行为有测试保护,不会因旧缓存而悄悄使用过期正文。
|
|
39
|
+
|
|
40
|
+
### 建议工作
|
|
41
|
+
|
|
42
|
+
- 为历史回填增加显式的缓存控制选项,例如允许用户绕过已有缓存重新抓取;参数名称和交互方式应与日报已有的 `--no-fetch-cache` 保持一致或清楚说明差别。
|
|
43
|
+
- 明确 `--refresh` 的语义:它目前用于重做已回填日期,不应让用户误以为它必定重新请求文章正文。
|
|
44
|
+
- 覆盖缓存命中、缓存未命中、抓取成功、抓取失败、强制重抓及并发抓取的行为。
|
|
45
|
+
- 继续只缓存非空成功正文;缓存不可写时回填仍应按现有约定完成,不因缓存辅助功能导致整批失败。
|
|
46
|
+
- 确认文档中所述的缓存目录、请求节流和“日报生成可复用历史正文”与代码一致。
|
|
47
|
+
|
|
48
|
+
### 验收标准
|
|
49
|
+
|
|
50
|
+
- 默认回填会复用有效缓存,缓存命中时不访问网络。
|
|
51
|
+
- 用户显式选择重抓时,即使已有缓存也会访问正文来源,并以成功结果更新缓存。
|
|
52
|
+
- 抓取失败不会覆盖已有的有效缓存;没有旧缓存时,失败也不会生成空缓存文件。
|
|
53
|
+
- 失败后重跑可继续补齐,不会将部分日期或部分文章误标为完整。
|
|
54
|
+
- `test/backfill_articles_test.py` 和日报缓存相关测试覆盖以上行为;Python 脚本的参数帮助与实际行为一致。
|
|
55
|
+
|
|
56
|
+
### 需要注意
|
|
57
|
+
|
|
58
|
+
缓存只代表“曾成功抓到的正文”,不代表正文永远是最新版本。刷新入口应明确,而不应通过改变默认行为造成额外请求。
|
|
59
|
+
|
|
60
|
+
## P1:统一导出媒体状态与验证口径
|
|
61
|
+
|
|
62
|
+
**基本完成(2026-10-14)**,**只差本节最后那条"第二台设备/另一个账号的验收记录"**(依赖外部环境,做不了)。
|
|
63
|
+
已落地的:统一词汇(报告答"源数据里能不能定位"→ `found`/`missing`;清单与 CLI JSON 答"有没有落进导出目录"
|
|
64
|
+
→ `written`/`copied`/`unavailable`,此后没有哪个词承担两种意思);`manifest.documents` 的 `exported` 改名为
|
|
65
|
+
`written`(此前与报告的 `exported` 同形不同义,拷贝失败时会让"已导出"变成假话);三种格式(html / md /
|
|
66
|
+
`--media`)的机器可读摘要收成**一个函数** `media_json_summary`,因此不会再各说各话,且报告文件名三处都有
|
|
67
|
+
(此前只有 HTML 那条给);回归测试覆盖"拷贝失败"(报告说 `found`、清单说 `unavailable`)、"重复运行"
|
|
68
|
+
(`written` 与 `copied` 分开,第二遍不误报 0)、"局部范围不吃掉宽范围的清单"(合并式写入),并断言摘要
|
|
69
|
+
**不含本机绝对路径**。仍未做:`ranges`(窄/宽)之外的状态读取兼容性只按现有 v1/v2 读取路径验过。
|
|
70
|
+
|
|
71
|
+
### 目标
|
|
72
|
+
|
|
73
|
+
让 HTML 报告、Markdown 清单及其他格式引用的媒体状态表达同一事实;用户可以区分“本地未找到”“已找到但未成功写出”和“已写入导出目录”。
|
|
74
|
+
|
|
75
|
+
### 建议工作
|
|
76
|
+
|
|
77
|
+
- 盘点 `weflow-media-report/v1` 与 `weflow-media-manifest/v2` 中的状态、计数和含义,尤其核对文档文件的 `exported`、`found` 与实际写入结果。
|
|
78
|
+
- 选定统一语义:媒体是否可在源数据中定位、是否成功写入资产目录、是否仅在 HTML 内嵌,分别用清楚且不互相覆盖的字段表达。
|
|
79
|
+
- 保持已有 manifest 的读取兼容;如需更改 schema,采用增量字段或明确的版本升级策略,不悄悄改变旧字段含义。
|
|
80
|
+
- 为文件复制失败、重复导出、已有资产、窄日期范围覆盖已有宽范围清单等场景添加回归检查。
|
|
81
|
+
- 在不输出完整本地路径、聊天正文或密钥的前提下,让 JSON 操作结果能给出各状态计数及报告文件名。
|
|
82
|
+
- 补充第二台设备或另一个账号的文档导出验证记录,以区分匹配逻辑遗漏和源设备没有相应文件这两种情况;继续等待 Issue #11 报告者反馈。
|
|
83
|
+
|
|
84
|
+
### 验收标准
|
|
85
|
+
|
|
86
|
+
- 所有格式对同一条消息给出的状态含义一致;复制失败不会仍显示“已导出”。
|
|
87
|
+
- 重复运行不会把“本次新写入数”误报为“最终可用数”。
|
|
88
|
+
- 局部范围导出不会删除其他范围已记录的媒体条目。
|
|
89
|
+
- 自动化测试覆盖故障写入和重复运行;至少一份真实环境验收记录注明平台、客户端版本、成功数量和缺失数量,不包含真实聊天内容或个人路径。
|
|
90
|
+
|
|
91
|
+
### 可选扩展:离线资料包
|
|
92
|
+
|
|
93
|
+
统一清单后,可将现有消息导出、媒体资产、版本信息和覆盖摘要封装为可迁移目录或压缩包。第一版应只包含用户选定范围的数据,提供清单校验与离线打开说明;默认不加密格式变更、不上传、不自动同步。该任务应以现有导出能力为基础,避免再实现一套媒体匹配逻辑。
|
|
94
|
+
|
|
95
|
+
## P1:分批完善 `--json` 运行时错误
|
|
96
|
+
|
|
97
|
+
### 目标
|
|
98
|
+
|
|
99
|
+
机器调用时,无论成功还是失败,都能得到稳定的 JSON 结果和可判断的错误码;人类交互模式保持现有行为。
|
|
100
|
+
|
|
101
|
+
### 建议工作
|
|
102
|
+
|
|
103
|
+
- 先按命令整理运行时失败场景:未初始化、找不到客户端、数据库不可读、权限不足、外部服务失败、输出文件不可写及用户取消。
|
|
104
|
+
- 优先选取无真实账号、密钥和网络依赖的失败场景,补自动化回归;需要本机资源的场景记录为手工验收,不把开发机状态写入 CI 测试。
|
|
105
|
+
- 对每个 `--json` 失败分支确认 stdout 只有一个机器可读 JSON 对象;日志和诊断文字进入 stderr,退出码与 `success` 状态一致。
|
|
106
|
+
- 复用已有错误码,不为同类问题建立多个同义码;错误消息应可读,但不含数据库路径、密钥、账号标识和正文。
|
|
107
|
+
- 逐项处理,不先用全局拦截器改造整个 Commander 出口;`--help`、`--version` 和提示信息必须保持兼容。
|
|
108
|
+
|
|
109
|
+
### 验收标准
|
|
110
|
+
|
|
111
|
+
- 被纳入范围的命令在成功、预览和失败三种路径下都有明确 JSON 结构。
|
|
112
|
+
- JSON 模式 stdout 不混入进度、彩色文本或普通错误行。
|
|
113
|
+
- 每个修复场景至少有一个能够触发该失败的测试;测试不依赖真实本地数据库或个人配置。
|
|
114
|
+
- README、操作手册和能力发现输出中的命令说明仍通过仓库的命令面检查。
|
|
115
|
+
|
|
116
|
+
## P2:提高搜索结果的可解释性
|
|
117
|
+
|
|
118
|
+
**实测补充(2026-10-15)**:这台机器上按默认窗口建了索引(16,027 条)并跑了真实检索,本节要的几件事因此有了具体读数:
|
|
119
|
+
|
|
120
|
+
- 重排在这台机器上是**一直不生效**的(Typesafe 返 `HTTP 451: not available in your region`)。这正是本节"执行元信息"
|
|
121
|
+
要解决的问题本身:现在 JSON 里**看不出**降级(没有 `reranked` / `degraded` 字段),终端上也看不见——CLI 不转发
|
|
122
|
+
子进程的 stderr,那行 WARN 只有直接跑 Python 时才有,于是"没报错"会被读成"重排过了"。
|
|
123
|
+
- **引用定位已经有了**:`id` 是 `article:<相对路径>` 或聊天消息的 `id` + `time`,能回到本地原记录,且不含绝对路径
|
|
124
|
+
(与本节"不要把完整绝对路径变成默认机器接口"一致)。
|
|
125
|
+
- 另量到两件本节没写、但同属可解释性的事:**一次检索约 20 秒**,几乎全花在把 62 MB 的 `vectors.npy` 读进内存
|
|
126
|
+
(没有 mmap);以及**同一篇文章会因为几个公众号都转载而在结果里重复出现**。两项都还没做。
|
|
127
|
+
- **找到一个真洞,已修**:那 10,759 篇日报 md 里含 `## 正文` 的有 **0 篇** —— 取正文的判据从来没匹配上,于是送进
|
|
128
|
+
embedding 的全是 YAML 头(抽样 300 篇:汉字占比中位 0.16)。"文章线只能靠标题排序"的原因在这里,不是截断。
|
|
129
|
+
已按真实结构改(0.16 → 0.80),并留了一条会因"退回老写法"而变红的测试;**索引要 `--full` 重建才生效**
|
|
130
|
+
(增量只嵌新 id —— 这是本轮最容易误判成"已经修好了"的一处)。素材厚度另算:日报 md 只有 259 个汉字,
|
|
131
|
+
正文缓存 2,063、Vault 笔记 422,换不换是个独立取舍。
|
|
132
|
+
- **建索引这条路也修了两处**(同一天实测踩出来的):`--full` 原来只给子进程 5 分钟超时,而全量重建要 20 分钟以上
|
|
133
|
+
—— 那个开关在真实索引上**从来没跑完过**;而且原先是"攒到最后才写",超时/被杀等于把已经花掉的钱和时间一起丢。
|
|
134
|
+
现在每 100 批落一次盘(先向量后 meta、各走临时文件 + replace),被杀只留一个**有效**的半成品,重跑
|
|
135
|
+
`search-index`(不加 `--full`)从断点续;失败批次(实测有 `IncompleteRead`)不再以全零向量入库(那会让那些 id
|
|
136
|
+
永久占位、再也不会被重嵌),改为在 `embedFailed` 里报数。顺带补了"meta 与向量行数不一致时按少的截断"——
|
|
137
|
+
不补的话越界会被 `search()` 的 `except` 吞成"静默退回关键词",一次真检索看起来像没命中。
|
|
138
|
+
|
|
139
|
+
- 本节第 3 条那个**合成评估集已经有了**(2026-10-15):`scripts/recall_eval.py` 在运行时从记录自己的文本造题(碎片 / 标题两式,唯一性校验),指标走标准口径 + 覆盖判据;手写问句走仓库外的 TSV。但它**还不覆盖引用定位**(答案有没有落到具体段落),那一条仍缺。
|
|
140
|
+
|
|
141
|
+
### 目标
|
|
142
|
+
|
|
143
|
+
当语义搜索的重排服务不可用或被降级时,调用者能看出本次结果采用了哪条路径;检索结果能定位回原始消息或文章。
|
|
144
|
+
|
|
145
|
+
### 建议工作
|
|
146
|
+
|
|
147
|
+
- 为搜索结果的机器输出增加简洁的执行元信息,例如向量检索是否执行、重排是否执行、是否降级及稳定错误原因。
|
|
148
|
+
- 保持现有结果字段兼容;执行元信息作为可选字段增加,避免将服务暂时不可用伪装成成功重排。
|
|
149
|
+
- 建立不含真实聊天内容的合成评估集,分别检查关键词命中、语义召回、重排和引用定位。
|
|
150
|
+
- 在评估集证明收益前,不改变默认重排策略,也不为了得到一个完整索引而默认增加云端调用和费用。
|
|
151
|
+
- 当返回引用时,提供足以回到本地原记录的稳定标识和时间信息;不要把完整绝对路径变成默认机器接口。
|
|
152
|
+
|
|
153
|
+
### 验收标准
|
|
154
|
+
|
|
155
|
+
- 重排成功与降级时的 JSON 可以区分;终端输出也会说明降级原因。
|
|
156
|
+
- 同一合成查询能验证命中、排序和引用对象,不依赖个人语料。
|
|
157
|
+
- 额外云端发送仍需遵循现有预览、确认和隐私边界。
|
|
158
|
+
|
|
159
|
+
## P2:按真实消费者需求扩展只读 MCP
|
|
160
|
+
|
|
161
|
+
### 目标
|
|
162
|
+
|
|
163
|
+
让外部本地 Agent 能检查同步覆盖和任务结果,而无需解析 CLI 文本或直接读取状态文件。
|
|
164
|
+
|
|
165
|
+
### 建议工作
|
|
166
|
+
|
|
167
|
+
- 先确认至少一个真实下游需要此能力,再将 `sync status`、任务结果或媒体覆盖摘要整理为只读 MCP 资源/工具。
|
|
168
|
+
- MCP 与 CLI 共用现有服务层和校验,不另写一套同步状态解析器。
|
|
169
|
+
- 为每个请求限制会话范围、时间范围、返回数量及字段;返回中不含密钥、绝对路径和聊天正文,除非具体工具明确提供且受相应确认协议保护。
|
|
170
|
+
- 对不完整状态保留 `complete`、`unverified`、`partial` 的区别,不能将“未验证”显示成“完整”。
|
|
171
|
+
- 增量游标只有在各相关后端都能验证正确性后,才作为稳定接口开放。当前可继续使用重叠时间窗与本地去重。
|
|
172
|
+
|
|
173
|
+
### 验收标准
|
|
174
|
+
|
|
175
|
+
- MCP 与 CLI 对同一同步状态返回一致的覆盖信息。
|
|
176
|
+
- 请求上限、未知会话、部分完成、未知 schema 和损坏状态文件均有明确结果。
|
|
177
|
+
- 使用合成状态文件完成集成测试;默认能力边界与 `capabilities --json` 一致。
|
|
178
|
+
|
|
179
|
+
## P3:再评估的平台与 AI 扩展
|
|
180
|
+
|
|
181
|
+
这些方向需要先有明确用户需求或验证证据,暂不作为近期主线:
|
|
182
|
+
|
|
183
|
+
- **本地模型:** 先通过能力检测和小规模体验验证基础检索/摘要质量,再考虑统一模型执行层;云端仍是显式配置和显式选择。
|
|
184
|
+
- **日报阅读状态:** 如果用户确实需要跨日报恢复进度,再增加本地阅读标记、收藏和笔记关联;先定义可迁移的数据格式和备份方式。
|
|
185
|
+
- **客户端兼容矩阵:** 将真实反馈沉淀为版本、平台、数据库类型和媒体覆盖的脱敏验收记录,优先修复高频兼容问题。
|
|
186
|
+
- **公开开发接口:** 只有当外部项目确实需要在同一进程内复用能力时,再讨论稳定公共 API。现阶段 CLI/JSON/MCP 已是明确集成面,插件加载器会扩大代码权限范围。
|
|
187
|
+
|
|
188
|
+
## 明确不作为近期目标
|
|
189
|
+
|
|
190
|
+
- 未验证的个人微信界面自动化、个人账号自动发信、拉群或群管理。
|
|
191
|
+
- 默认云端同步、默认上传聊天数据或无人确认的云端分析。
|
|
192
|
+
- 为了“插件生态”直接开放任意进程内代码加载。
|
|
193
|
+
- 在各后端未提供可靠依据时承诺稳定消息游标或无遗漏同步。
|
|
194
|
+
- 对找不到的图片、文档、语音或视频做猜测性匹配。
|
|
195
|
+
|
|
196
|
+
## 建议的拆分顺序
|
|
197
|
+
|
|
198
|
+
| 顺序 | 可独立评审的工作项 | 主要依赖 | 完成信号 |
|
|
199
|
+
| --- | --- | --- | --- |
|
|
200
|
+
| 1 | 历史回填缓存语义与强制重抓选项 | 当前未提交的缓存改动 | 缓存命中、失败、刷新及日报复用均有测试 |
|
|
201
|
+
| 2 | 媒体状态语义统一与复制失败回归 | 现有 manifest 与媒体报告 | 同一文件在报告、清单和导出结果中的状态一致 |
|
|
202
|
+
| 3 | 第二环境的文件导出验收 | 可获得的第二账号/设备与授权 | 有可复核的脱敏计数和缺失分类 |
|
|
203
|
+
| 4 | JSON 运行时错误分批审计 | 错误码和各命令现有分支 | 每批命令的失败 stdout、stderr、退出码均有测试 |
|
|
204
|
+
| 5 | 搜索降级元信息和引用 | 现有搜索结果结构 | 调用者能识别重排未执行并定位来源 |
|
|
205
|
+
| 6 | 只读 MCP 同步状态扩展 | 已存在的 sync 状态服务;明确下游需求 | CLI/MCP 一致性与权限测试通过 |
|
|
206
|
+
| 7 | 资料包、阅读状态或本地模型试点 | 前序任务的真实使用反馈 | 有可量化需求、预算、边界和回滚方案 |
|
|
207
|
+
|
|
208
|
+
前三项是建议的下一轮范围。第 3 项依赖外部环境,若暂时没有第二设备,可先完成前两项与 JSON 错误审计,不应将“没有第二台设备”误报成代码验证通过。
|
|
209
|
+
|
|
210
|
+
## 每项任务的交付清单
|
|
211
|
+
|
|
212
|
+
每个实现任务在合并前应留下:
|
|
213
|
+
|
|
214
|
+
- 一个明确的问题描述和用户可见行为变化;
|
|
215
|
+
- 数据范围、网络请求、写入位置及权限边界;
|
|
216
|
+
- 对应的回归测试或明确标记的人工验收记录;
|
|
217
|
+
- 失败、部分完成、重试和重复运行的行为说明;
|
|
218
|
+
- 同步更新必要的 `PROJECT_STATE.md`、`DECISIONS.md`、用户文档及发布说明;
|
|
219
|
+
- 可通过恢复旧版本或重跑任务完成的回滚路径。
|
|
220
|
+
|
|
221
|
+
## 相关资料
|
|
222
|
+
|
|
223
|
+
- [整体路线图](ROADMAP.md)
|
|
224
|
+
- [项目当前状态](PROJECT_STATE.md)
|
|
225
|
+
- [功能增强方案](FUNCTIONAL-IMPROVEMENT-SPEC.md)
|
|
226
|
+
- [同步与任务状态契约](SYNC_CONTRACT.md)
|
|
227
|
+
- [消息数据契约](DATA_CONTRACT.md)
|
|
228
|
+
- [AI 接口与确认协议](AI_INTERFACE.md)
|
|
229
|
+
- [扩展检查清单](EXTENDING.md)
|
|
230
|
+
- [安全说明](../SECURITY.md)
|