weflow-cli 1.16.0 → 1.17.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 (97) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +16 -4
  3. package/bin/weflow-cli.ts +348 -6
  4. package/dist/bin/weflow-cli.js +381 -8
  5. package/dist/bin/weflow-cli.js.map +1 -1
  6. package/dist/src/core/ntCore.d.ts +47 -0
  7. package/dist/src/core/ntCore.d.ts.map +1 -1
  8. package/dist/src/core/ntCore.js +24 -0
  9. package/dist/src/core/ntCore.js.map +1 -1
  10. package/dist/src/services/assistantService.d.ts +8 -1
  11. package/dist/src/services/assistantService.d.ts.map +1 -1
  12. package/dist/src/services/assistantService.js +47 -8
  13. package/dist/src/services/assistantService.js.map +1 -1
  14. package/dist/src/services/assistantTools.d.ts +8 -0
  15. package/dist/src/services/assistantTools.d.ts.map +1 -1
  16. package/dist/src/services/assistantTools.js +67 -11
  17. package/dist/src/services/assistantTools.js.map +1 -1
  18. package/dist/src/services/configService.d.ts.map +1 -1
  19. package/dist/src/services/configService.js +3 -2
  20. package/dist/src/services/configService.js.map +1 -1
  21. package/dist/src/services/cursorProbe.d.ts +44 -0
  22. package/dist/src/services/cursorProbe.d.ts.map +1 -0
  23. package/dist/src/services/cursorProbe.js +183 -0
  24. package/dist/src/services/cursorProbe.js.map +1 -0
  25. package/dist/src/services/qchatImport.d.ts +89 -0
  26. package/dist/src/services/qchatImport.d.ts.map +1 -0
  27. package/dist/src/services/qchatImport.js +273 -0
  28. package/dist/src/services/qchatImport.js.map +1 -0
  29. package/dist/src/services/setupVenv.d.ts +43 -0
  30. package/dist/src/services/setupVenv.d.ts.map +1 -0
  31. package/dist/src/services/setupVenv.js +198 -0
  32. package/dist/src/services/setupVenv.js.map +1 -0
  33. package/dist/src/utils/python.d.ts +10 -0
  34. package/dist/src/utils/python.d.ts.map +1 -1
  35. package/dist/src/utils/python.js +27 -4
  36. package/dist/src/utils/python.js.map +1 -1
  37. package/docs/DATA_CONTRACT.md +25 -0
  38. package/docs/DECISIONS.md +92 -30
  39. package/docs/FUNCTIONAL-IMPROVEMENT-SPEC.md +365 -365
  40. package/docs/HEALTH-CHECK.md +104 -104
  41. package/docs/JEV/345/272/224/347/224/250/346/226/275/345/267/245/346/226/207/346/241/243.md +310 -310
  42. package/docs/NEXT_ACTIONS.md +236 -236
  43. package/docs/NPM-PUBLISH.md +130 -130
  44. package/docs/POSITIONING.md +68 -68
  45. package/docs/PROJECT_STATE.md +6 -3
  46. package/docs/REPLAY.md +169 -0
  47. package/docs/SYNC_CONTRACT.md +197 -180
  48. package/package.json +89 -88
  49. package/requirements-3x.txt +4 -4
  50. package/requirements-voice.txt +15 -15
  51. package/resources/js/graph3d/NOTICE.txt +35 -35
  52. package/resources/js/graph3d/d3-binarytree.min.js +2 -2
  53. package/resources/js/graph3d/d3-dispatch.min.js +2 -2
  54. package/resources/js/graph3d/d3-force-3d.min.js +2 -2
  55. package/resources/js/graph3d/d3-octree.min.js +2 -2
  56. package/resources/js/graph3d/d3-quadtree.min.js +2 -2
  57. package/resources/js/graph3d/d3-timer.min.js +2 -2
  58. package/resources/js/graph3d/three.min.js +6 -6
  59. package/resources/js/graph3d/viewer2d.js +550 -448
  60. package/resources/panel/quick-menu.cjs +197 -197
  61. package/scripts/_utils.py +1338 -1338
  62. package/scripts/article_notes.py +423 -423
  63. package/scripts/chat_notes.py +86 -3
  64. package/scripts/check_readme_commands.py +427 -426
  65. package/scripts/contact_schema.py +339 -339
  66. package/scripts/create_reading_notes.py +11 -11
  67. package/scripts/daily_task.ps1 +213 -213
  68. package/scripts/fav_notes.py +321 -321
  69. package/scripts/graph_2d.py +156 -151
  70. package/scripts/graph_3d.py +672 -646
  71. package/scripts/health_check.py +473 -473
  72. package/scripts/imported_chats.py +68 -0
  73. package/scripts/install_tasks.ps1 +105 -105
  74. package/scripts/leads.py +179 -179
  75. package/scripts/network.py +206 -206
  76. package/scripts/nt_decrypt.py +135 -1
  77. package/scripts/on_this_day.py +178 -178
  78. package/scripts/opinion_timeline.py +138 -138
  79. package/scripts/promises.py +185 -185
  80. package/scripts/replay.py +803 -0
  81. package/scripts/run_hidden.vbs +25 -0
  82. package/scripts/semantic_search.py +73 -5
  83. package/scripts/user_notes.py +315 -315
  84. package/scripts/voice_transcribe.py +139 -139
  85. package/scripts/watch_issues.py +212 -212
  86. package/scripts/watch_mail.py +131 -131
  87. package/src/core/ntCore.ts +59 -0
  88. package/src/services/assistantService.ts +48 -8
  89. package/src/services/assistantTools.ts +73 -10
  90. package/src/services/configService.ts +3 -2
  91. package/src/services/cursorProbe.ts +222 -0
  92. package/src/services/qchatImport.ts +327 -0
  93. package/src/services/setupVenv.ts +238 -0
  94. package/src/services/syncService.ts +363 -363
  95. package/src/services/syncState.ts +275 -275
  96. package/src/services/wechatMedia.ts +166 -166
  97. package/src/utils/python.ts +33 -4
@@ -1,310 +1,310 @@
1
- # Jev 在 WeFlow CLI 的应用 · 施工文档
2
-
3
- > 用途:交给代码 Agent(或开发者)接手 WeFlow CLI 中 Jev(TypeSafe 决策模型)相关的后续工作。
4
- > 项目:WeFlow CLI 1.7.0
5
- > 本仓库可能存在尚未提交的维护者改动。**施工前必须先检查工作区,不得覆盖或重置已有修改。**
6
-
7
- Jev 已经接入并落地在六个地方。这份文档回答三个问题:**它是什么(契约)**、**现在怎么用的(现状)**、**还欠什么(待办)**。目标是接手人只读这一份就能判断该动哪里、不该动哪里。
8
-
9
- ---
10
-
11
- ## 1. 施工目标
12
-
13
- 按优先级:
14
-
15
- 1. **把质量闭环做完(P0,唯一真正欠着的)**。所有概率目前都没有金标准——现有的一切依据都是"与另一个不完美的基准比"。`scripts/quality_eval.py` 已经把这件事压到"照着编号填 50 行",但**这一步需要人读文章**,见 §7 的红线。
16
- 2. **按定标结果校准阈值(P0 的第二半)**。相关度切点 0.5 / 1.5、收录切点 `INCLUDE_THRESHOLD = 0.5` 全部是**暂定值**,没有任何金标准支撑。定标完成后如果最优切点离现用值很远,该改的是常数,不是模型。
17
- 3. **在新增判断点时正确使用 Jev(P1)**。契约、失败取向、边界见 §3 与 §6。
18
- 4. **不要重做已经定了的事**。有几件看起来像"改进"的事已经被量过并明确否掉或推迟,见 §5。
19
-
20
- 长期路线见 [ROADMAP.md](ROADMAP.md);安全与架构取舍见 [DECISIONS.md](DECISIONS.md)(Jev 相关决策编号见 §4 末尾)。**本文件是施工执行书。**
21
-
22
- ---
23
-
24
- ## 2. 开工前检查
25
-
26
- 在任何编辑前执行并记录结果:
27
-
28
- ```powershell
29
- git status --short
30
- git log --oneline -12
31
- npm run build
32
- npm test
33
- python -m unittest discover -s test -p '*_test.py' -q
34
- ```
35
-
36
- 如果已有测试失败,先记录失败测试和原因,**不能把既有失败归到本次施工,也不能为了变绿而删除或放宽测试**。尤其不要放宽 `test/jev_client_test.py` 里的契约断言——那是刻意拧紧的。
37
-
38
- 重点阅读:
39
-
40
- - `AGENTS.md`:仓库级约束、敏感数据与占位符要求。
41
- - `docs/PROJECT_STATE.md`:当前真实能力、限制与验证状态。
42
- - `docs/DECISIONS.md`:Jev 相关的**定过的事和不做的事**(编号见 §4)。
43
- - `scripts/jev_client.py`:契约的唯一实现,注释里写明了每一条为什么。
44
- - `scripts/quality_eval.py`:本轮的施工入口。
45
- - `SECURITY.md`:AI 出境、密钥与本地服务边界。
46
-
47
- ---
48
-
49
- ## 3. Jev 的契约(施工前必须掌握)
50
-
51
- ### 3.1 它是什么
52
-
53
- TypeSafe 的 Jev(System One)是一个**只做判断、不做生成**的模型:输入一段 state 和一批**类型化问题**,返回**带概率的答案**。它不吐散文,所以没有"用正则从自然语言里抠回结构"这一层——那正是它被引入的原因。
54
-
55
- 对比旧路径(`docs/DECISIONS.md` 有测量):日报原来把"主题 + 相关度"塞进同一个散文 prompt 让 LLM 顺手答,再用两条正则抠回来。结果在 2201 篇存量文章里,`relevance` 有 **2199 篇**都是默认值「中」,`tags` 全是 `[主题名]`。
56
-
57
- **Jev 只替代"判断",不替代"生成"。** 摘要、标签、概念仍然是 LLM 生成的——Jev 根本不会生成。
58
-
59
- ### 3.2 接口
60
-
61
- 唯一实现:`scripts/jev_client.py`(别处不要另写一份客户端)。
62
-
63
- ```
64
- POST https://api.typesafe.ai/v1/systemone Authorization: Bearer <key>
65
- request {model, state, questions}
66
- response {model, answers, usage}
67
- ```
68
-
69
- - `state`:字符串 / JSON 对象 / 数组,**共享给该请求里所有问题**。
70
- - `questions`:`{名字: {type, instructions, criteria}}`,一次请求可以问很多个。
71
- - 契约来源不是转述:`GET https://api.typesafe.ai/openapi.json` 加上实测。
72
- - 别名只有两个:`jev-latest`(默认)、`jev-preview`。
73
- - key 的取值顺序:显式传入 → `TYPESAFE_API_KEY` → 配置(走 `_utils.decrypt_lock` 自动解密)。配置命令:`weflow-cli config set typesafeApiKey "..."`。key 不落盘、不打印。
74
-
75
- ### 3.3 三种问题类型
76
-
77
- | type | `criteria` 的形状 | 答案字段 | 备注 |
78
- | --- | --- | --- | --- |
79
- | `choice` | **对象** `{选项名: 何时选它}`(判据文本会被模型读到) | `choice` / `confidence` / `probabilities` | 选项集合就是 criteria 本身——**少一个选项,模型就永远选不到它** |
80
- | `score` | **有序数组**,**位置即分值,从 0 开始**(零索引) | `score` / `legend`(+ `confidence`) | `score` 落在 `[0, len-1]` 区间 |
81
- | `noul` | 不需要(可选传 `{true, false}`) | `noul`(一个浮点) | **不返回 `confidence`** |
82
-
83
- 零索引是硬语义:`scripts/jev_client.py` 的 `RELEVANCE_LEVELS` 与 `_utils.RELEVANCE_NAMES` **按下标一一对应,两者都不能重排**(有测试钉住)。
84
-
85
- ### 3.4 两个容易漏掉的契约细节
86
-
87
- - **`instructions` 可以是结构化对象**(`{"goal": ..., "rules": [...]}`),不只是字符串。已对着线上 API 验过。
88
- - **每个响应都带 `model` 字段,是实际服务的版本**(实测 `jev-1.13.0`),不是你请求的别名(`jev-latest`)。别名会漂而结果看起来都正常,所以它被记进 `usage['model']`、`JevClient.last_model`,以及日报的 `.articles.json` 的 `decisionModel` 键。
89
-
90
- ### 3.5 四条硬校验(`jev_client.py:_check_choice`)
91
-
92
- 每个 `choice` 答案在使用前都要过四关,不过就抛 `JevError`:
93
-
94
- 1. `probabilities` 的**键集等于 criteria 的键集**;
95
- 2. 所有取值在 `[0, 1]`;
96
- 3. 概率和 ≈ 1(容差 0.02);
97
- 4. `choice` **就是概率的 argmax**。
98
-
99
- 第 4 条最要紧:**契约漂移的失效方式不是报错,而是"选错了但看起来正常"**。这个仓库踩过一个同型的坑——把 `noul`(浮点)当布尔读,每个问题都读成"否",而输出还把责任推给了模型。
100
-
101
- ### 3.6 成本模型(决定了该怎么用它)
102
-
103
- - **多问几个问题的边际成本≈0**:实测 2 个问题 0.84s / 2083 输入 token,12 个问题 0.91s / 2238。**`state` 才是开销大头。**
104
- - 结论:**Jev 的使用单位是「一个 state → 一批判断」,不是一个问题。** 与其为每个条目单独调一次,不如把 N 个条目装进同一份 state 一次问完(`decide --over` 就是干这个的)。
105
- - 输入 token 单价按 `INPUT_USD_PER_TOKEN`(`0.042 / 1M`)折算,输出在推广期内不计费。客户端只拿得到 token 数、**拿不到账单金额**,所以成本始终是估算。
106
- - 实测数量级:40 篇约 $0.0037、60 篇约 $0.0057,整库 2201 篇约 $0.21。
107
-
108
- ### 3.7 时效(会过期,别照着旧文抄)
109
-
110
- **决策模型的免费期到 2026-09-25 结束**,之后输入 token 开始计费(推广期内输出免费)。凡是重测代价高的事情,要么赶在免费期内做完,要么先假定重跑的钱。这一条同时写在 `docs/PROJECT_STATE.md`。
111
-
112
- ---
113
-
114
- ## 4. 已落地的六个应用
115
-
116
- 每个触点都写清了:入口在哪、问什么、失败怎么退、踩过什么坑。
117
-
118
- ### 4.1 日报分类 —— 主题 / 相关度 / 收录
119
-
120
- 文章的主题(6 类 choice)、相关度(3 档 score)、以及"该不该收录"(noul)由 Jev 判断;**摘要、标签、概念仍然由 LLM 生成**。
121
-
122
- - 调用点:`scripts/biz_daily.py:810-821`(`create_client(config=config) if args.classifier in ('auto','jev')`,逐篇走 `_classify_with_jev` `213-226` → `decide_article`)。
123
- - `--classifier {auto,llm,jev}` 定义在 `biz_daily.py:632-634`,**默认 `auto`** = 有 key 就用 Jev,否则走老路。这是**一条命令回滚**,也让任何人能把同一天两种路径各跑一遍。`jev` 但无 key 时打 WARN 并退回 LLM 路径(`815-816`)。
124
- - **`--no-ai`(或 `dailyAiEnabled=false`)时整块 Jev 代码都不执行**——总闸在 `biz_daily.py:807`。
125
- - **LLM 的 prompt 是故意不动的**:它仍然被要求输出 `【主题】`/`【相关度】`,而这两个值现在被忽略。这样回退路径与旧行为**逐字节一致**,代价是每篇浪费约 20 个输出 token。瘦身是后续项,不是本次。
126
- - 相关度映射用 `int(score + 0.5)` 取最近档,**不是 `round()`**——Python 的 round 是银行家舍入,`round(0.5)` 得 0、`round(1.5)` 得 2,边界会跳档。
127
- - 原始分一律落盘(`relevanceScore` / `includeScore` / `topicConfidence`,在 `_serializable_article` `biz_daily.py:142-165`,**非 None 才写**、`round(float,3)`),所以改阈值不用重跑语料。**`decisionModel` 是 `.articles.json` 的顶层键**(`biz_daily.py:977`);md frontmatter 侧在 `1020-1025`。
128
- - **收录门 `INCLUDE_THRESHOLD = 0.5` 在 `scripts/generate_ai_report.py:95`**,唯一使用者是谓词 `admits()`(`205-227`),**两条装载路径都调它**(`.articles.json` `241` / md 兜底 `278`);焦点主题永远收(`218`)。
129
- - **`generate_ai_report.py` 自己完全不调 Jev**(该文件里 `create_client` 出现 0 次),它只读已落盘的概率。所以"改收录口径"和"改判断口径"是**两个不同的文件**。
130
-
131
- ### 4.2 `decide` —— 本机判断层
132
-
133
- `python scripts/decide.py`:一个 state + 一批类型化问题、一次调用,返回类型化答案 + 概率 + 这次的花费。
134
-
135
- - 三种输入:`--request <file>`(或 stdin)、批量模式 `--over <glob> --ask <text>`、`--dry-run` 只校验不调用。
136
- - 请求形状**本地先校验**(`validate_request`):形状不对就在本机报错,不换一个来自服务端的 422——那种错误只说得出"哪个字段不合法",说不出"你本来想干什么"。
137
- - 批量模式的问题名是 `f<序号>|q<序号>`,**不是把文件名编进去**(文件名里有 `|`、空格、中文标点);序号→文件的映射在返回的 `batch.files` / `batch.asks` 里。
138
- - `--max-chars`(默认 1500)决定每个文件喂多少,**并且回显**——因为它决定上限:同一批问题,只喂前 14 行与基准一致率 77.3%,喂前 1500 字符则 88.6%。**证据量决定上限。**
139
- - **刻意不暴露成 MCP 工具**。`bin/weflow-cli.ts:332-343` 的 `capabilities` 声明原文:`readsLocalData: { request: false, over: true }`、`invokesAI: true`、`sendsCallerProvidedState: true`、`confirmationRequired: true`、**`mcpExposed: false`**、`whyNotMcp: 'an MCP client must not be able to drive local outbound calls'`。命令注册在 `4974-5067`;`--json` 而没有 `--yes` 会返回 `CONFIRMATION_REQUIRED`(`5028-5036`);子进程超时 120s(`5051-5054`)。**MCP 工具清单里确认没有 decide**(`5812-5828`)。
140
- - 声明粒度必须与行为粒度一致:`--over` **会读本地文件**,所以 `readsLocalData` 按两条输入路径**分开**声明,不能一刀切成 `false`。
141
-
142
- ### 4.3 `quality_eval` —— 定标(本轮入口)
143
-
144
- ```powershell
145
- python scripts/quality_eval.py sample --n 50 # 抽样 + 现场用生产路径打分
146
- # 人工填 ~/.weflow-cli/labels/<时间戳>.json 里每条的 label
147
- python scripts/quality_eval.py score <该文件> # 准确率 + 校准曲线 + 阈值扫描
148
- ```
149
-
150
- - **必须现场打分,不能读存档**:磁盘上没有任何一篇文章带 `includeScore`(现存那批生成于该字段引入之前),而且存档的 topic 本身就是要被检验的东西。样本写进 `~/.weflow-cli/labels/`,**不进仓库**(里面是文章标题和人的判断)。
151
- - 取样是**按概率档位分层**的。只按主题取样会退化成"标了 49 条、买到的信息量等于标了 4 条"(这是脚本 docstring 里的原始测量,与当前那份 50 篇样本不是同一批):49 篇里 45 篇的收录分都在 0.2 以下,边界上只有 4 篇。分层取样后样本**不再是自然分布**,所以档位在全池里的占比单独报(`prevalence`)——它回答的是另一个问题:"阈值一动,每天会多收或少收多少篇"。
152
- - `label` 里没把握的留 `null`,算"未标注"而不是"标错"。
153
- - 固定 seed,同一条命令两次跑抽到同一批——否则"我标了一半"之后重跑就全错位。
154
- - `score` 打印三样:主题准确率(**Jev 与历史存档标签并列**)、**校准表**(每个概率桶里人工说"该收"的比例——斜着往上走才有分辨力,一条平线就是乱猜)、**阈值扫描**(最优点在哪,离 0.50 很远就该改常数)。
155
-
156
- ### 4.4 `reply_debt` —— 谁在等我回话
157
-
158
- 按会话逐个判,一个会话一份 state、一次请求问 **5** 个问题,所以**每条判断都知道它属于哪个会话**。
159
-
160
- > ⚠️ 模块 docstring(`reply_debt.py:9-12`)写的是"6 个问题",**代码是 5 个**(`build_questions` `155-174`),测试也钉的是 5 个(`test/reply_debt_test.py:142-147`)。这是 docstring 的笔误,不是被删掉了一个——`reply_debt.py` 最初那一版同样是 5 个。**不要为了对齐 docstring 去加第六个问题。**
161
-
162
- 五个问题是:`waiting`(noul,对方是否在等我回应)、`urgency`(score,4 档)、`commitment`(noul,我承诺过而没下文的)、`money`(noul,硬承诺)、`kind`(choice,`KINDS` `54-61`)。
163
-
164
- 这个方法与 `extract_todos.py` 不是同一件事:那个把 4000 条消息一次交给模型,问的是"有没有承诺被提到","是谁在等"在那种输出里根本没有立足之地。
165
-
166
- - `--min-prob` 可以把概率当阈值用;**输出是提示,不是事实**,这点写在脚本 docstring 里,接手人不要把它升级成结论。
167
- - 不落盘任何数据文件:只有 `--html` 时才写一个文件(`399-410`),`--json` 走 stdout。
168
- - `--html <path>` 输出一个**不含聊天内容**的自包含单页,所以可分享(渲染 `render_html` `89-152`,docstring `90-93` 明写这是刻意约束;页脚 `148-149` 自述"本页不含任何聊天内容")。只透传固定字段(name/days/kind/waiting/urgencyScore/证据字数),有测试钉住:往行数据里塞消息正文,页面里不许出现(`test/reply_debt_test.py:185`)。
169
- - 阈值默认:`--days` 14、`--limit` 40、`--min-prob` 0.5;"拿不太准"的下限 0.3 是**硬编码**、不是参数(`397`)。
170
-
171
- ### 4.5 `route_cards` —— 查询词选择(**排序不是它做的**)
172
-
173
- 把"搜我的聊天"变成"先选会话、再精确检索"。**分工是实测出来的,不是设计出来的**:
174
-
175
- - 最初想让 Jev 排序会话 → **实测不成立**:用 noul 问"这个会话更值得打开吗",候选全落 0.50~0.51(领券群与会务群同档);换成 score 问"有多相关",20 个会话全给 1.74~1.76,**与命中数毫无关系**。同一批数据里纯本地按命中数排序却很准。
176
- - 原因:模型看不到会话内容,卡片上只有"类型/条数/时间/名称/命中数",它无从判断,只能给中庸分。
177
- - **所以排序由本地命中数决定,Jev 只做一件它确实做得到的事:从候选词里挑出真正的查询词**(`pick_terms` `256-273`,每个候选词一个 `noul` 问题 `t0`/`t1`/…,阈值 `NOUL_THRESHOLD = 0.5` 在 `48`)。第一版把 n-gram 碎片一起递过去,它稳定地留下「会议」、否掉其余七个——这是在**有限的选项里做选择**,正是它的本职。
178
- - **不要把排序"优化"回去。** 这一段是可复用的教训:先证明它排得了,再让它排。无 key 时 `SystemExit('没有 TypeSafe key;改用 --keyword 手动给查询词')`(`359-360`);Jev 全否时用 `kept or terms` 兜底(`376`);缺字段单独告警"契约不符,不要当成模型判为否"(`363-367`)。
179
- - **这条实测结论目前只写在注释和文档里**(`route_cards.py:5-14` 记了 0.50~0.51 与 1.74~1.76 两组数字、`344`、D-036),**没有断言测试**——也就是说没有东西会拦住下一个人把它改回去。要加固的话,加一条"排序不得依赖 Jev 分数"的测试是合理的施工项。
180
-
181
- ### 4.6 `semantic_search` —— rerank
182
-
183
- 语义搜索是两段式:先按向量相似度取候选,再用决策模型对**前 20 条**候选做 rerank(`RERANK_CANDIDATES = 20` `semantic_search.py:392`,摘录 `RERANK_EXCERPT_CHARS = 300` `393`)。
184
-
185
- - 喂什么:state 是 `问题:…\n\n候选:…`,每行 `【候选i】标题|来源` + 该条前 300 字符(`423-429`)。**正文来源可能是聊天消息**。
186
- - 问什么:每个候选一个 `noul`「是否直接回答了上面的问题,或含有回答它所需的关键信息?仅话题相近不算。」(`396-408`),**外加一个 `best` 单选做自检**(`433-438`)。自检与逐条判断不一致时**只 WARN,不重排**(`461-465`)。
187
- - 怎么重排:`sort(key=lambda t: (t[0] is None, -(t[0] or 0.0), t[1]))`(`460`)——**没拿到分的按原序垫到最后,不当 0 分**。新分写进 `rerankScore`(仅非 None 时),原始 `score` 保留。
188
- - 回退全是 fail-soft(这是设计,活体跑出过 `HTTP 529`):没有客户端或候选 <2 → 原样返回(`420-421`);调用异常 → WARN + 原顺序(`442-444`);全部返回 None → WARN + 原顺序(`455-457`)。
189
- - **它唯一的真实风险是候选编号错位**:模型拿 `c5` 的内容回答 `c3` 时,最该被选中的那条会**无声下沉**,没有任何报错。`docs/PROJECT_STATE.md:83` 记了这一条。
190
- - ✅ **`--no-rerank` 现在两边都有**(2026-10-11 接上的通道):CLI 的 `search` 把它转发给脚本,`--dry-run --json` 的预览里也带 `noRerank`。此前它只在 Python 侧 —— 而 `docs/OPERATIONS.md` 已经在教用户敲 `weflow-cli search "..." --no-rerank`,用户会拿到 `unknown option`。**是文档命令面那道检查发现的**(`scripts/check_readme_commands.py` 扫文档代码块):它按"今天真实的 CLI"校验,于是把这条缺口指了出来 —— 而只扫两份 README 时它扫不到。新增 `test/search-cli.test.ts` 钉住这类(声明的旗标必须被分类、被读到、该转发的真的转发)。
191
-
192
- ### 4.7 通用原则(六个触点共同的形状)
193
-
194
- - **客户端 fail-loud,调用方 fail-soft**:`jev_client` 一律抛 `JevError`(带 HTTP 状态与 400 字符截断的响应体,**从不含 key**);调用方逐条决定退到哪条老路(打印 WARN 并回退)。一篇文章判不出来,不该让一整天的日报崩掉;但客户端**不许把失败伪装成一个看起来正常的答案**。
195
- - **瞬时故障要重试**:429/5xx/529 重试(次数与退避刻意短,最坏约 2.4s)。服务 2026-09-15 才发布,被打满时返回 `529 system_overloaded` 是常态;调用方是逐篇分类的,长退避只会把一整天的日报拖成几十分钟——而那正是它该退回老路的时候。
196
- - **并发刻意保持 6**:服务还新,提高扇出买到的是重试而不是吞吐。
197
-
198
- **Jev 相关的决策编号**(改任何 Jev 行为前先读这几条——里面记着"为什么不那样做"):
199
-
200
- | 编号 | 位置 | 内容 |
201
- | --- | --- | --- |
202
- | D-031 | `DECISIONS.md:351` | 判断交给决策模型,生成留给 LLM(含 `--classifier` 回滚、60 篇对照**不是**准确率) |
203
- | D-032 | `:506` | 决策模型做 rerank:一次请求、取 top 20 |
204
- | D-033 | `:549` | 给人看的机器判断都要带证据(`reply_debt` 的五问 + 两条通用规则) |
205
- | D-034 | `:646` | 把决策模型作为本地原语暴露,但**不**经 MCP |
206
- | D-035 | `:697` | 助手的单轮快路:**已发、默认关**(`assistantFastRoute` = `off`|`log`|`on`;2026-09-22 起 Status: Active) |
207
- | D-036 | `:759` | 检索用决策模型选查询词,**不**用它排会话 |
208
-
209
- ---
210
-
211
- ## 5. 待办清单
212
-
213
- ### P0 — 定标与校准(本轮的主要工作)
214
-
215
- 1. 用 `quality_eval.py sample` 生成待标注样本。**磁盘上已经有一份现成的**:`~/.weflow-cli/labels/labels-20260921-203232.json`,**50 篇,`include` 与 `topic` 两栏都还是空的(已标注 0 条)**。先用它,不必重跑抽样。
216
- 2. **由人**读完这 50 篇并填 `{"topic": ..., "include": true|false}`。**这一条代码 Agent 不能代做**,见 §7。
217
- 3. 跑 `quality_eval.py score`,按结果判断:
218
- - **校准表**是否斜着往上走(是 → 概率有信息量;平线 → 它只是在乱猜,这本身是一个必须如实报告的结论);
219
- - **阈值扫描**的最优点离现用的 0.50 有多远(远 → 改常数,不改模型);
220
- - 主题准确率里 **Jev 与历史存档标签并列**的那两行——存档标签从来不是金标准,这次才第一次有对照。
221
- 4. 若结论支持改阈值:改 `INCLUDE_THRESHOLD` 与相关度切点,并**在 `DECISIONS.md` 追加一条**记录改的理由与新值;`PROJECT_STATE.md` 里"暂定"的措辞同步更新。
222
-
223
- ### P1 — 待决的收尾项
224
-
225
- - **LLM prompt 瘦身**:把日报 prompt 里现在已经没人读的 `【主题】`/`【相关度】` 要求去掉,省下每篇约 20 个输出 token。`DECISIONS.md` 明确把它记为"后续项"而不是本次——**要做就把回退路径的变化一起测了**,别让它悄悄变成"新的、更差的老路"。
226
- - `relevance` 的兜底值:未分类文章目前记为「中」,而"中"读起来是个正面评价。这是**待决策**,不是 bug(已具名 `DEFAULT_RELEVANCE`)。
227
- - 语料层面的缺口(`relevance` 全库 2199/2201 都是「中」这类)已经量完并记录,**不要在没有新证据时重开**。
228
-
229
- ### 明确不做
230
-
231
- - **D-035(助手单轮快路)**:设计已记录、代码未写。它**只能靠活着的微信通道端到端验证**,普通环境复现不了,而失效方式是"模型拿着不匹配的工具结果自信地答圆"——比慢更糟。落地条件写在 D-035 里,**不要在没有那些条件时实现它**。
232
- - **用 Jev 排序会话**(`route_cards` 已量过,见 §4.5)。
233
- - **把 `decide` 暴露成 MCP 工具**。
234
-
235
- ### 交接时容易踩的四处不一致(都已核实,属于事实)
236
-
237
- 1. `reply_debt.py` 的 docstring 说"6 个问题",**代码与测试都是 5 个**(§4.4)。不要为了对齐 docstring 去加第六问。
238
- 2. **`--html`(`reply_debt`)与 `--no-rerank`(`semantic_search`)只在 Python 侧存在**,CLI 没给通道(`awaiting` 参数表 `bin/weflow-cli.ts:5075-5080`、`search` 组参 `4719`)。写脚本或文档时别假设 CLI 能传这两个参数。
239
- 3. **`docs/SETUP.md` 缺 `config set typesafeApiKey` 的命令原文**——实际在 `README.md:189-190` 与 `OPERATIONS.md:437-438`(后者含 `config set typesafeApiKey ""`,清空即回退到 LLM 解析路径)。用户是按 SETUP 走的,这一条补在 SETUP 里更合适。
240
- 4. `route_cards` 的"不让 Jev 排序"只有注释与文档,**没有断言测试**(§4.5)。
241
-
242
- ---
243
-
244
- ## 6. 不可违反的边界
245
-
246
- - **新增数据出境必须写进决策记录。** Jev 的 state 会发到 `api.typesafe.ai`——文章标题与正文现在也在出境范围内。任何**新增**一类内容出境(新的字段、新的脚本、新的判断点)都要在 `DECISIONS.md` 留痕,并确认它符合 `SECURITY.md` 的"显式 AI opt-in / 只上传你选择的内容"。
247
- - **key 不落盘、不打印、不进命令行、不进日志。** 走 `_utils.decrypt_lock` / `config set`。错误信息只带 HTTP 状态与截断响应体。
248
- - **不要把 Jev 的判断表述成事实。** 所有输出都是"带概率的提示";用户可见文本里不许写成结论、认定或保证。法律相关只能整理线索。
249
- - **客户端 fail-loud、调用方 fail-soft**:不要为了"稳"而让客户端把一个失败包装成正常答案(§4.7)。
250
- - **判据表只有一份,且不可重排。** `TOPICS` / `TOPIC_CRITERIA` / `RELEVANCE_NAMES` 都在 `_utils`,提示词路径与决策路径读同一张表;相关度档位与 `RELEVANCE_LEVELS` 按下标对应。**回退时不能换标准。**
251
- - **不要把"答案的味道"放进 state。** `decide_article` 只放标题和正文,不放日期、来源,也不放 frontmatter 里的 `topic`/`relevance`——那些是被判断的对象本身。
252
- - **不要新增一条只有一条路径生效的输入。** 这个仓库反复证明"只在一条路径上生效"是最常见的缺陷形状(json/md 两个装载器、两份分片发现、argv 与环境变量、序列化与批量分支)。新增输入路径**必须配一条测试**。
253
- - 敏感数据红线照 `AGENTS.md`:数据库、导出、wxid、key、token、路径、日志、聊天内容一律不进代码/文档/夹具/提交/公开输出;示例用 `联系人A`、`示例群`、`YOUR_API_KEY`、`YYYY-MM-DD`。
254
-
255
- ---
256
-
257
- ## 7. 红线:哪些事代码 Agent 不许做
258
-
259
- 1. **不许自己编人工标签。** `quality_eval.py` 的 `label` 字段是**人读完文章后的判断**,是这一整轮唯一能提供金标准的东西。Agent 自己填(无论用哪个模型)会让校准曲线退化成"两个模型互相比",比现在的状态更糟——因为它看起来有了金标准。**做不了就如实报告"需要人工标注",并停在那里。**
260
- 2. **不许在没有金标准时把校准结论写成结论。** 标注少于 20 条时脚本自己会打印"这些数字还不稳";报告结论时要连这句话一起带出来。
261
- 3. **不许为了让指标好看而放宽断言或测试**(尤其 `_check_choice` 的四条)。
262
- 4. **不许把"概率看起来合理"当成"验证过"。** 校准表是平线时,正确的报告是"没有分辨力",不是换一个更漂亮的阈值。
263
-
264
- ---
265
-
266
- ## 8. 必须补齐的测试
267
-
268
- - **契约**:`_check_choice` 的四条断言各有反例(键集不符、越界、和不为 1、`choice` 不是 argmax)——**这四条都必须有用例**,且必须能在离线状态下跑。
269
- - **序列化路径**:`parse_frontmatter` 返回的是**字符串**标量,所以 md 兜底路径上 `includeScore` 会是 `'0.8'` 这样的字符串。只认 `int/float` 的实现会让新字段在那条路径被**静默忽略**——必须有测试钉住两条装载路径(`.articles.json` 与 md 兜底)行为一致。
270
- - **回退**:无 key、`JevError`、超时、`529` 各一条,验证日报不崩且降级路径可观察。
271
- - **批量模式**:`--over` 的展开顺序稳定、问题名↔文件映射正确、`--max-chars` 真的生效并回显。(历史教训:批量模式最初没写测试就加了,一跑就崩——删除补丁在 `main()` 里留下死代码,批量分支落进去引用未定义变量。)
272
- - **隐私**:`reply_debt` 的 `--html` 页面**不含**任何消息正文(往行数据里塞正文,页面里不许出现)。
273
- - **全部测试必须离线**,不得调用付费 API。(现有测试确实全部离线:触碰 Jev 的一律 `patch.object(jev.urllib.request,'urlopen')` 或传桩客户端,没有任何测试真的付钱调 `api.typesafe.ai`。)
274
-
275
- **当前完全没有测试的输入路径**(都是"真调用付费服务"或 CLI 接线那一层,现有测试只覆盖纯函数)——这些是施工时可以补的地方:
276
-
277
- - `quality_eval.py:169-175` `cmd_sample` 里真正 `create_client()` 并打分的那条路;
278
- - `semantic_search.search()` 的重排接线(`498`/`507` 的 `create_client()` 与 pool 扩张 `491`/`504`);
279
- - `route_cards.py` 的 `main()` 全链路(`307-406`:`--yes` 门、`kept or terms` 回退、命中计数、取消息);
280
- - `reply_debt.py` 的 `main()` 端到端(`274-467`:参数默认值、`--min-prob` 过滤、`--html` 落文件、退出码);
281
- - `biz_daily.py` 的 `--classifier` 参数接线与 `_apply_decision`(`271-286`)——**全仓库没有任何测试引用 `_apply_decision`**。
282
-
283
- ---
284
-
285
- ## 9. 验收标准
286
-
287
- 只有全部满足才能报告完成:
288
-
289
- - 改动前后工作区差异清晰,**未覆盖维护者已有修改**。
290
- - `npm run build` 通过;`npm test` 通过;Python 相关专项测试通过;`git diff --check` 通过。
291
- - 定标部分:如实报告已标注条数,以及"够不够形成结论"(<20 条时必须说明不稳)。
292
- - 任何阈值改动都有 `DECISIONS.md` 条目与理由;`PROJECT_STATE.md` 的措辞同步。
293
- - 检查差异中没有真实隐私数据、密钥、令牌、账号标识、数据库、日志或导出文件。
294
- - **无法验证的事,明确报告"无法验证"及阻断原因**,不得把推测写成完成。宁可难看的占位,也不要通顺但可能错的结论。
295
-
296
- ---
297
-
298
- ## 10. 提交规则
299
-
300
- 施工 Agent 默认只修改代码、测试和必要文档,**不自动提交或推送**。提交前输出:
301
-
302
- 1. 改动文件清单;2. 每个改动的目的;3. 测试命令和结果;4. 仍存在的限制;5. 敏感信息检查结果。
303
-
304
- 提交信息简短、准确、说明用户可见结果,例如:
305
-
306
- ```text
307
- fix: apply the calibrated inclusion threshold
308
- ```
309
-
310
- 不得在提交信息、Issue、PR 或公开文档中写入真实本地数据、文章标题或调试输出。
1
+ # Jev 在 WeFlow CLI 的应用 · 施工文档
2
+
3
+ > 用途:交给代码 Agent(或开发者)接手 WeFlow CLI 中 Jev(TypeSafe 决策模型)相关的后续工作。
4
+ > 项目:WeFlow CLI 1.7.0
5
+ > 本仓库可能存在尚未提交的维护者改动。**施工前必须先检查工作区,不得覆盖或重置已有修改。**
6
+
7
+ Jev 已经接入并落地在六个地方。这份文档回答三个问题:**它是什么(契约)**、**现在怎么用的(现状)**、**还欠什么(待办)**。目标是接手人只读这一份就能判断该动哪里、不该动哪里。
8
+
9
+ ---
10
+
11
+ ## 1. 施工目标
12
+
13
+ 按优先级:
14
+
15
+ 1. **把质量闭环做完(P0,唯一真正欠着的)**。所有概率目前都没有金标准——现有的一切依据都是"与另一个不完美的基准比"。`scripts/quality_eval.py` 已经把这件事压到"照着编号填 50 行",但**这一步需要人读文章**,见 §7 的红线。
16
+ 2. **按定标结果校准阈值(P0 的第二半)**。相关度切点 0.5 / 1.5、收录切点 `INCLUDE_THRESHOLD = 0.5` 全部是**暂定值**,没有任何金标准支撑。定标完成后如果最优切点离现用值很远,该改的是常数,不是模型。
17
+ 3. **在新增判断点时正确使用 Jev(P1)**。契约、失败取向、边界见 §3 与 §6。
18
+ 4. **不要重做已经定了的事**。有几件看起来像"改进"的事已经被量过并明确否掉或推迟,见 §5。
19
+
20
+ 长期路线见 [ROADMAP.md](ROADMAP.md);安全与架构取舍见 [DECISIONS.md](DECISIONS.md)(Jev 相关决策编号见 §4 末尾)。**本文件是施工执行书。**
21
+
22
+ ---
23
+
24
+ ## 2. 开工前检查
25
+
26
+ 在任何编辑前执行并记录结果:
27
+
28
+ ```powershell
29
+ git status --short
30
+ git log --oneline -12
31
+ npm run build
32
+ npm test
33
+ python -m unittest discover -s test -p '*_test.py' -q
34
+ ```
35
+
36
+ 如果已有测试失败,先记录失败测试和原因,**不能把既有失败归到本次施工,也不能为了变绿而删除或放宽测试**。尤其不要放宽 `test/jev_client_test.py` 里的契约断言——那是刻意拧紧的。
37
+
38
+ 重点阅读:
39
+
40
+ - `AGENTS.md`:仓库级约束、敏感数据与占位符要求。
41
+ - `docs/PROJECT_STATE.md`:当前真实能力、限制与验证状态。
42
+ - `docs/DECISIONS.md`:Jev 相关的**定过的事和不做的事**(编号见 §4)。
43
+ - `scripts/jev_client.py`:契约的唯一实现,注释里写明了每一条为什么。
44
+ - `scripts/quality_eval.py`:本轮的施工入口。
45
+ - `SECURITY.md`:AI 出境、密钥与本地服务边界。
46
+
47
+ ---
48
+
49
+ ## 3. Jev 的契约(施工前必须掌握)
50
+
51
+ ### 3.1 它是什么
52
+
53
+ TypeSafe 的 Jev(System One)是一个**只做判断、不做生成**的模型:输入一段 state 和一批**类型化问题**,返回**带概率的答案**。它不吐散文,所以没有"用正则从自然语言里抠回结构"这一层——那正是它被引入的原因。
54
+
55
+ 对比旧路径(`docs/DECISIONS.md` 有测量):日报原来把"主题 + 相关度"塞进同一个散文 prompt 让 LLM 顺手答,再用两条正则抠回来。结果在 2201 篇存量文章里,`relevance` 有 **2199 篇**都是默认值「中」,`tags` 全是 `[主题名]`。
56
+
57
+ **Jev 只替代"判断",不替代"生成"。** 摘要、标签、概念仍然是 LLM 生成的——Jev 根本不会生成。
58
+
59
+ ### 3.2 接口
60
+
61
+ 唯一实现:`scripts/jev_client.py`(别处不要另写一份客户端)。
62
+
63
+ ```
64
+ POST https://api.typesafe.ai/v1/systemone Authorization: Bearer <key>
65
+ request {model, state, questions}
66
+ response {model, answers, usage}
67
+ ```
68
+
69
+ - `state`:字符串 / JSON 对象 / 数组,**共享给该请求里所有问题**。
70
+ - `questions`:`{名字: {type, instructions, criteria}}`,一次请求可以问很多个。
71
+ - 契约来源不是转述:`GET https://api.typesafe.ai/openapi.json` 加上实测。
72
+ - 别名只有两个:`jev-latest`(默认)、`jev-preview`。
73
+ - key 的取值顺序:显式传入 → `TYPESAFE_API_KEY` → 配置(走 `_utils.decrypt_lock` 自动解密)。配置命令:`weflow-cli config set typesafeApiKey "..."`。key 不落盘、不打印。
74
+
75
+ ### 3.3 三种问题类型
76
+
77
+ | type | `criteria` 的形状 | 答案字段 | 备注 |
78
+ | --- | --- | --- | --- |
79
+ | `choice` | **对象** `{选项名: 何时选它}`(判据文本会被模型读到) | `choice` / `confidence` / `probabilities` | 选项集合就是 criteria 本身——**少一个选项,模型就永远选不到它** |
80
+ | `score` | **有序数组**,**位置即分值,从 0 开始**(零索引) | `score` / `legend`(+ `confidence`) | `score` 落在 `[0, len-1]` 区间 |
81
+ | `noul` | 不需要(可选传 `{true, false}`) | `noul`(一个浮点) | **不返回 `confidence`** |
82
+
83
+ 零索引是硬语义:`scripts/jev_client.py` 的 `RELEVANCE_LEVELS` 与 `_utils.RELEVANCE_NAMES` **按下标一一对应,两者都不能重排**(有测试钉住)。
84
+
85
+ ### 3.4 两个容易漏掉的契约细节
86
+
87
+ - **`instructions` 可以是结构化对象**(`{"goal": ..., "rules": [...]}`),不只是字符串。已对着线上 API 验过。
88
+ - **每个响应都带 `model` 字段,是实际服务的版本**(实测 `jev-1.13.0`),不是你请求的别名(`jev-latest`)。别名会漂而结果看起来都正常,所以它被记进 `usage['model']`、`JevClient.last_model`,以及日报的 `.articles.json` 的 `decisionModel` 键。
89
+
90
+ ### 3.5 四条硬校验(`jev_client.py:_check_choice`)
91
+
92
+ 每个 `choice` 答案在使用前都要过四关,不过就抛 `JevError`:
93
+
94
+ 1. `probabilities` 的**键集等于 criteria 的键集**;
95
+ 2. 所有取值在 `[0, 1]`;
96
+ 3. 概率和 ≈ 1(容差 0.02);
97
+ 4. `choice` **就是概率的 argmax**。
98
+
99
+ 第 4 条最要紧:**契约漂移的失效方式不是报错,而是"选错了但看起来正常"**。这个仓库踩过一个同型的坑——把 `noul`(浮点)当布尔读,每个问题都读成"否",而输出还把责任推给了模型。
100
+
101
+ ### 3.6 成本模型(决定了该怎么用它)
102
+
103
+ - **多问几个问题的边际成本≈0**:实测 2 个问题 0.84s / 2083 输入 token,12 个问题 0.91s / 2238。**`state` 才是开销大头。**
104
+ - 结论:**Jev 的使用单位是「一个 state → 一批判断」,不是一个问题。** 与其为每个条目单独调一次,不如把 N 个条目装进同一份 state 一次问完(`decide --over` 就是干这个的)。
105
+ - 输入 token 单价按 `INPUT_USD_PER_TOKEN`(`0.042 / 1M`)折算,输出在推广期内不计费。客户端只拿得到 token 数、**拿不到账单金额**,所以成本始终是估算。
106
+ - 实测数量级:40 篇约 $0.0037、60 篇约 $0.0057,整库 2201 篇约 $0.21。
107
+
108
+ ### 3.7 时效(会过期,别照着旧文抄)
109
+
110
+ **决策模型的免费期到 2026-09-25 结束**,之后输入 token 开始计费(推广期内输出免费)。凡是重测代价高的事情,要么赶在免费期内做完,要么先假定重跑的钱。这一条同时写在 `docs/PROJECT_STATE.md`。
111
+
112
+ ---
113
+
114
+ ## 4. 已落地的六个应用
115
+
116
+ 每个触点都写清了:入口在哪、问什么、失败怎么退、踩过什么坑。
117
+
118
+ ### 4.1 日报分类 —— 主题 / 相关度 / 收录
119
+
120
+ 文章的主题(6 类 choice)、相关度(3 档 score)、以及"该不该收录"(noul)由 Jev 判断;**摘要、标签、概念仍然由 LLM 生成**。
121
+
122
+ - 调用点:`scripts/biz_daily.py:810-821`(`create_client(config=config) if args.classifier in ('auto','jev')`,逐篇走 `_classify_with_jev` `213-226` → `decide_article`)。
123
+ - `--classifier {auto,llm,jev}` 定义在 `biz_daily.py:632-634`,**默认 `auto`** = 有 key 就用 Jev,否则走老路。这是**一条命令回滚**,也让任何人能把同一天两种路径各跑一遍。`jev` 但无 key 时打 WARN 并退回 LLM 路径(`815-816`)。
124
+ - **`--no-ai`(或 `dailyAiEnabled=false`)时整块 Jev 代码都不执行**——总闸在 `biz_daily.py:807`。
125
+ - **LLM 的 prompt 是故意不动的**:它仍然被要求输出 `【主题】`/`【相关度】`,而这两个值现在被忽略。这样回退路径与旧行为**逐字节一致**,代价是每篇浪费约 20 个输出 token。瘦身是后续项,不是本次。
126
+ - 相关度映射用 `int(score + 0.5)` 取最近档,**不是 `round()`**——Python 的 round 是银行家舍入,`round(0.5)` 得 0、`round(1.5)` 得 2,边界会跳档。
127
+ - 原始分一律落盘(`relevanceScore` / `includeScore` / `topicConfidence`,在 `_serializable_article` `biz_daily.py:142-165`,**非 None 才写**、`round(float,3)`),所以改阈值不用重跑语料。**`decisionModel` 是 `.articles.json` 的顶层键**(`biz_daily.py:977`);md frontmatter 侧在 `1020-1025`。
128
+ - **收录门 `INCLUDE_THRESHOLD = 0.5` 在 `scripts/generate_ai_report.py:95`**,唯一使用者是谓词 `admits()`(`205-227`),**两条装载路径都调它**(`.articles.json` `241` / md 兜底 `278`);焦点主题永远收(`218`)。
129
+ - **`generate_ai_report.py` 自己完全不调 Jev**(该文件里 `create_client` 出现 0 次),它只读已落盘的概率。所以"改收录口径"和"改判断口径"是**两个不同的文件**。
130
+
131
+ ### 4.2 `decide` —— 本机判断层
132
+
133
+ `python scripts/decide.py`:一个 state + 一批类型化问题、一次调用,返回类型化答案 + 概率 + 这次的花费。
134
+
135
+ - 三种输入:`--request <file>`(或 stdin)、批量模式 `--over <glob> --ask <text>`、`--dry-run` 只校验不调用。
136
+ - 请求形状**本地先校验**(`validate_request`):形状不对就在本机报错,不换一个来自服务端的 422——那种错误只说得出"哪个字段不合法",说不出"你本来想干什么"。
137
+ - 批量模式的问题名是 `f<序号>|q<序号>`,**不是把文件名编进去**(文件名里有 `|`、空格、中文标点);序号→文件的映射在返回的 `batch.files` / `batch.asks` 里。
138
+ - `--max-chars`(默认 1500)决定每个文件喂多少,**并且回显**——因为它决定上限:同一批问题,只喂前 14 行与基准一致率 77.3%,喂前 1500 字符则 88.6%。**证据量决定上限。**
139
+ - **刻意不暴露成 MCP 工具**。`bin/weflow-cli.ts:332-343` 的 `capabilities` 声明原文:`readsLocalData: { request: false, over: true }`、`invokesAI: true`、`sendsCallerProvidedState: true`、`confirmationRequired: true`、**`mcpExposed: false`**、`whyNotMcp: 'an MCP client must not be able to drive local outbound calls'`。命令注册在 `4974-5067`;`--json` 而没有 `--yes` 会返回 `CONFIRMATION_REQUIRED`(`5028-5036`);子进程超时 120s(`5051-5054`)。**MCP 工具清单里确认没有 decide**(`5812-5828`)。
140
+ - 声明粒度必须与行为粒度一致:`--over` **会读本地文件**,所以 `readsLocalData` 按两条输入路径**分开**声明,不能一刀切成 `false`。
141
+
142
+ ### 4.3 `quality_eval` —— 定标(本轮入口)
143
+
144
+ ```powershell
145
+ python scripts/quality_eval.py sample --n 50 # 抽样 + 现场用生产路径打分
146
+ # 人工填 ~/.weflow-cli/labels/<时间戳>.json 里每条的 label
147
+ python scripts/quality_eval.py score <该文件> # 准确率 + 校准曲线 + 阈值扫描
148
+ ```
149
+
150
+ - **必须现场打分,不能读存档**:磁盘上没有任何一篇文章带 `includeScore`(现存那批生成于该字段引入之前),而且存档的 topic 本身就是要被检验的东西。样本写进 `~/.weflow-cli/labels/`,**不进仓库**(里面是文章标题和人的判断)。
151
+ - 取样是**按概率档位分层**的。只按主题取样会退化成"标了 49 条、买到的信息量等于标了 4 条"(这是脚本 docstring 里的原始测量,与当前那份 50 篇样本不是同一批):49 篇里 45 篇的收录分都在 0.2 以下,边界上只有 4 篇。分层取样后样本**不再是自然分布**,所以档位在全池里的占比单独报(`prevalence`)——它回答的是另一个问题:"阈值一动,每天会多收或少收多少篇"。
152
+ - `label` 里没把握的留 `null`,算"未标注"而不是"标错"。
153
+ - 固定 seed,同一条命令两次跑抽到同一批——否则"我标了一半"之后重跑就全错位。
154
+ - `score` 打印三样:主题准确率(**Jev 与历史存档标签并列**)、**校准表**(每个概率桶里人工说"该收"的比例——斜着往上走才有分辨力,一条平线就是乱猜)、**阈值扫描**(最优点在哪,离 0.50 很远就该改常数)。
155
+
156
+ ### 4.4 `reply_debt` —— 谁在等我回话
157
+
158
+ 按会话逐个判,一个会话一份 state、一次请求问 **5** 个问题,所以**每条判断都知道它属于哪个会话**。
159
+
160
+ > ⚠️ 模块 docstring(`reply_debt.py:9-12`)写的是"6 个问题",**代码是 5 个**(`build_questions` `155-174`),测试也钉的是 5 个(`test/reply_debt_test.py:142-147`)。这是 docstring 的笔误,不是被删掉了一个——`reply_debt.py` 最初那一版同样是 5 个。**不要为了对齐 docstring 去加第六个问题。**
161
+
162
+ 五个问题是:`waiting`(noul,对方是否在等我回应)、`urgency`(score,4 档)、`commitment`(noul,我承诺过而没下文的)、`money`(noul,硬承诺)、`kind`(choice,`KINDS` `54-61`)。
163
+
164
+ 这个方法与 `extract_todos.py` 不是同一件事:那个把 4000 条消息一次交给模型,问的是"有没有承诺被提到","是谁在等"在那种输出里根本没有立足之地。
165
+
166
+ - `--min-prob` 可以把概率当阈值用;**输出是提示,不是事实**,这点写在脚本 docstring 里,接手人不要把它升级成结论。
167
+ - 不落盘任何数据文件:只有 `--html` 时才写一个文件(`399-410`),`--json` 走 stdout。
168
+ - `--html <path>` 输出一个**不含聊天内容**的自包含单页,所以可分享(渲染 `render_html` `89-152`,docstring `90-93` 明写这是刻意约束;页脚 `148-149` 自述"本页不含任何聊天内容")。只透传固定字段(name/days/kind/waiting/urgencyScore/证据字数),有测试钉住:往行数据里塞消息正文,页面里不许出现(`test/reply_debt_test.py:185`)。
169
+ - 阈值默认:`--days` 14、`--limit` 40、`--min-prob` 0.5;"拿不太准"的下限 0.3 是**硬编码**、不是参数(`397`)。
170
+
171
+ ### 4.5 `route_cards` —— 查询词选择(**排序不是它做的**)
172
+
173
+ 把"搜我的聊天"变成"先选会话、再精确检索"。**分工是实测出来的,不是设计出来的**:
174
+
175
+ - 最初想让 Jev 排序会话 → **实测不成立**:用 noul 问"这个会话更值得打开吗",候选全落 0.50~0.51(领券群与会务群同档);换成 score 问"有多相关",20 个会话全给 1.74~1.76,**与命中数毫无关系**。同一批数据里纯本地按命中数排序却很准。
176
+ - 原因:模型看不到会话内容,卡片上只有"类型/条数/时间/名称/命中数",它无从判断,只能给中庸分。
177
+ - **所以排序由本地命中数决定,Jev 只做一件它确实做得到的事:从候选词里挑出真正的查询词**(`pick_terms` `256-273`,每个候选词一个 `noul` 问题 `t0`/`t1`/…,阈值 `NOUL_THRESHOLD = 0.5` 在 `48`)。第一版把 n-gram 碎片一起递过去,它稳定地留下「会议」、否掉其余七个——这是在**有限的选项里做选择**,正是它的本职。
178
+ - **不要把排序"优化"回去。** 这一段是可复用的教训:先证明它排得了,再让它排。无 key 时 `SystemExit('没有 TypeSafe key;改用 --keyword 手动给查询词')`(`359-360`);Jev 全否时用 `kept or terms` 兜底(`376`);缺字段单独告警"契约不符,不要当成模型判为否"(`363-367`)。
179
+ - **这条实测结论目前只写在注释和文档里**(`route_cards.py:5-14` 记了 0.50~0.51 与 1.74~1.76 两组数字、`344`、D-036),**没有断言测试**——也就是说没有东西会拦住下一个人把它改回去。要加固的话,加一条"排序不得依赖 Jev 分数"的测试是合理的施工项。
180
+
181
+ ### 4.6 `semantic_search` —— rerank
182
+
183
+ 语义搜索是两段式:先按向量相似度取候选,再用决策模型对**前 20 条**候选做 rerank(`RERANK_CANDIDATES = 20` `semantic_search.py:392`,摘录 `RERANK_EXCERPT_CHARS = 300` `393`)。
184
+
185
+ - 喂什么:state 是 `问题:…\n\n候选:…`,每行 `【候选i】标题|来源` + 该条前 300 字符(`423-429`)。**正文来源可能是聊天消息**。
186
+ - 问什么:每个候选一个 `noul`「是否直接回答了上面的问题,或含有回答它所需的关键信息?仅话题相近不算。」(`396-408`),**外加一个 `best` 单选做自检**(`433-438`)。自检与逐条判断不一致时**只 WARN,不重排**(`461-465`)。
187
+ - 怎么重排:`sort(key=lambda t: (t[0] is None, -(t[0] or 0.0), t[1]))`(`460`)——**没拿到分的按原序垫到最后,不当 0 分**。新分写进 `rerankScore`(仅非 None 时),原始 `score` 保留。
188
+ - 回退全是 fail-soft(这是设计,活体跑出过 `HTTP 529`):没有客户端或候选 <2 → 原样返回(`420-421`);调用异常 → WARN + 原顺序(`442-444`);全部返回 None → WARN + 原顺序(`455-457`)。
189
+ - **它唯一的真实风险是候选编号错位**:模型拿 `c5` 的内容回答 `c3` 时,最该被选中的那条会**无声下沉**,没有任何报错。`docs/PROJECT_STATE.md:83` 记了这一条。
190
+ - ✅ **`--no-rerank` 现在两边都有**(2026-10-11 接上的通道):CLI 的 `search` 把它转发给脚本,`--dry-run --json` 的预览里也带 `noRerank`。此前它只在 Python 侧 —— 而 `docs/OPERATIONS.md` 已经在教用户敲 `weflow-cli search "..." --no-rerank`,用户会拿到 `unknown option`。**是文档命令面那道检查发现的**(`scripts/check_readme_commands.py` 扫文档代码块):它按"今天真实的 CLI"校验,于是把这条缺口指了出来 —— 而只扫两份 README 时它扫不到。新增 `test/search-cli.test.ts` 钉住这类(声明的旗标必须被分类、被读到、该转发的真的转发)。
191
+
192
+ ### 4.7 通用原则(六个触点共同的形状)
193
+
194
+ - **客户端 fail-loud,调用方 fail-soft**:`jev_client` 一律抛 `JevError`(带 HTTP 状态与 400 字符截断的响应体,**从不含 key**);调用方逐条决定退到哪条老路(打印 WARN 并回退)。一篇文章判不出来,不该让一整天的日报崩掉;但客户端**不许把失败伪装成一个看起来正常的答案**。
195
+ - **瞬时故障要重试**:429/5xx/529 重试(次数与退避刻意短,最坏约 2.4s)。服务 2026-09-15 才发布,被打满时返回 `529 system_overloaded` 是常态;调用方是逐篇分类的,长退避只会把一整天的日报拖成几十分钟——而那正是它该退回老路的时候。
196
+ - **并发刻意保持 6**:服务还新,提高扇出买到的是重试而不是吞吐。
197
+
198
+ **Jev 相关的决策编号**(改任何 Jev 行为前先读这几条——里面记着"为什么不那样做"):
199
+
200
+ | 编号 | 位置 | 内容 |
201
+ | --- | --- | --- |
202
+ | D-031 | `DECISIONS.md:351` | 判断交给决策模型,生成留给 LLM(含 `--classifier` 回滚、60 篇对照**不是**准确率) |
203
+ | D-032 | `:506` | 决策模型做 rerank:一次请求、取 top 20 |
204
+ | D-033 | `:549` | 给人看的机器判断都要带证据(`reply_debt` 的五问 + 两条通用规则) |
205
+ | D-034 | `:646` | 把决策模型作为本地原语暴露,但**不**经 MCP |
206
+ | D-035 | `:697` | 助手的单轮快路:**已发、默认关**(`assistantFastRoute` = `off`|`log`|`on`;2026-09-22 起 Status: Active) |
207
+ | D-036 | `:759` | 检索用决策模型选查询词,**不**用它排会话 |
208
+
209
+ ---
210
+
211
+ ## 5. 待办清单
212
+
213
+ ### P0 — 定标与校准(本轮的主要工作)
214
+
215
+ 1. 用 `quality_eval.py sample` 生成待标注样本。**磁盘上已经有一份现成的**:`~/.weflow-cli/labels/labels-20260921-203232.json`,**50 篇,`include` 与 `topic` 两栏都还是空的(已标注 0 条)**。先用它,不必重跑抽样。
216
+ 2. **由人**读完这 50 篇并填 `{"topic": ..., "include": true|false}`。**这一条代码 Agent 不能代做**,见 §7。
217
+ 3. 跑 `quality_eval.py score`,按结果判断:
218
+ - **校准表**是否斜着往上走(是 → 概率有信息量;平线 → 它只是在乱猜,这本身是一个必须如实报告的结论);
219
+ - **阈值扫描**的最优点离现用的 0.50 有多远(远 → 改常数,不改模型);
220
+ - 主题准确率里 **Jev 与历史存档标签并列**的那两行——存档标签从来不是金标准,这次才第一次有对照。
221
+ 4. 若结论支持改阈值:改 `INCLUDE_THRESHOLD` 与相关度切点,并**在 `DECISIONS.md` 追加一条**记录改的理由与新值;`PROJECT_STATE.md` 里"暂定"的措辞同步更新。
222
+
223
+ ### P1 — 待决的收尾项
224
+
225
+ - **LLM prompt 瘦身**:把日报 prompt 里现在已经没人读的 `【主题】`/`【相关度】` 要求去掉,省下每篇约 20 个输出 token。`DECISIONS.md` 明确把它记为"后续项"而不是本次——**要做就把回退路径的变化一起测了**,别让它悄悄变成"新的、更差的老路"。
226
+ - `relevance` 的兜底值:未分类文章目前记为「中」,而"中"读起来是个正面评价。这是**待决策**,不是 bug(已具名 `DEFAULT_RELEVANCE`)。
227
+ - 语料层面的缺口(`relevance` 全库 2199/2201 都是「中」这类)已经量完并记录,**不要在没有新证据时重开**。
228
+
229
+ ### 明确不做
230
+
231
+ - **D-035(助手单轮快路)**:设计已记录、代码未写。它**只能靠活着的微信通道端到端验证**,普通环境复现不了,而失效方式是"模型拿着不匹配的工具结果自信地答圆"——比慢更糟。落地条件写在 D-035 里,**不要在没有那些条件时实现它**。
232
+ - **用 Jev 排序会话**(`route_cards` 已量过,见 §4.5)。
233
+ - **把 `decide` 暴露成 MCP 工具**。
234
+
235
+ ### 交接时容易踩的四处不一致(都已核实,属于事实)
236
+
237
+ 1. `reply_debt.py` 的 docstring 说"6 个问题",**代码与测试都是 5 个**(§4.4)。不要为了对齐 docstring 去加第六问。
238
+ 2. **`--html`(`reply_debt`)与 `--no-rerank`(`semantic_search`)只在 Python 侧存在**,CLI 没给通道(`awaiting` 参数表 `bin/weflow-cli.ts:5075-5080`、`search` 组参 `4719`)。写脚本或文档时别假设 CLI 能传这两个参数。
239
+ 3. **`docs/SETUP.md` 缺 `config set typesafeApiKey` 的命令原文**——实际在 `README.md:189-190` 与 `OPERATIONS.md:437-438`(后者含 `config set typesafeApiKey ""`,清空即回退到 LLM 解析路径)。用户是按 SETUP 走的,这一条补在 SETUP 里更合适。
240
+ 4. `route_cards` 的"不让 Jev 排序"只有注释与文档,**没有断言测试**(§4.5)。
241
+
242
+ ---
243
+
244
+ ## 6. 不可违反的边界
245
+
246
+ - **新增数据出境必须写进决策记录。** Jev 的 state 会发到 `api.typesafe.ai`——文章标题与正文现在也在出境范围内。任何**新增**一类内容出境(新的字段、新的脚本、新的判断点)都要在 `DECISIONS.md` 留痕,并确认它符合 `SECURITY.md` 的"显式 AI opt-in / 只上传你选择的内容"。
247
+ - **key 不落盘、不打印、不进命令行、不进日志。** 走 `_utils.decrypt_lock` / `config set`。错误信息只带 HTTP 状态与截断响应体。
248
+ - **不要把 Jev 的判断表述成事实。** 所有输出都是"带概率的提示";用户可见文本里不许写成结论、认定或保证。法律相关只能整理线索。
249
+ - **客户端 fail-loud、调用方 fail-soft**:不要为了"稳"而让客户端把一个失败包装成正常答案(§4.7)。
250
+ - **判据表只有一份,且不可重排。** `TOPICS` / `TOPIC_CRITERIA` / `RELEVANCE_NAMES` 都在 `_utils`,提示词路径与决策路径读同一张表;相关度档位与 `RELEVANCE_LEVELS` 按下标对应。**回退时不能换标准。**
251
+ - **不要把"答案的味道"放进 state。** `decide_article` 只放标题和正文,不放日期、来源,也不放 frontmatter 里的 `topic`/`relevance`——那些是被判断的对象本身。
252
+ - **不要新增一条只有一条路径生效的输入。** 这个仓库反复证明"只在一条路径上生效"是最常见的缺陷形状(json/md 两个装载器、两份分片发现、argv 与环境变量、序列化与批量分支)。新增输入路径**必须配一条测试**。
253
+ - 敏感数据红线照 `AGENTS.md`:数据库、导出、wxid、key、token、路径、日志、聊天内容一律不进代码/文档/夹具/提交/公开输出;示例用 `联系人A`、`示例群`、`YOUR_API_KEY`、`YYYY-MM-DD`。
254
+
255
+ ---
256
+
257
+ ## 7. 红线:哪些事代码 Agent 不许做
258
+
259
+ 1. **不许自己编人工标签。** `quality_eval.py` 的 `label` 字段是**人读完文章后的判断**,是这一整轮唯一能提供金标准的东西。Agent 自己填(无论用哪个模型)会让校准曲线退化成"两个模型互相比",比现在的状态更糟——因为它看起来有了金标准。**做不了就如实报告"需要人工标注",并停在那里。**
260
+ 2. **不许在没有金标准时把校准结论写成结论。** 标注少于 20 条时脚本自己会打印"这些数字还不稳";报告结论时要连这句话一起带出来。
261
+ 3. **不许为了让指标好看而放宽断言或测试**(尤其 `_check_choice` 的四条)。
262
+ 4. **不许把"概率看起来合理"当成"验证过"。** 校准表是平线时,正确的报告是"没有分辨力",不是换一个更漂亮的阈值。
263
+
264
+ ---
265
+
266
+ ## 8. 必须补齐的测试
267
+
268
+ - **契约**:`_check_choice` 的四条断言各有反例(键集不符、越界、和不为 1、`choice` 不是 argmax)——**这四条都必须有用例**,且必须能在离线状态下跑。
269
+ - **序列化路径**:`parse_frontmatter` 返回的是**字符串**标量,所以 md 兜底路径上 `includeScore` 会是 `'0.8'` 这样的字符串。只认 `int/float` 的实现会让新字段在那条路径被**静默忽略**——必须有测试钉住两条装载路径(`.articles.json` 与 md 兜底)行为一致。
270
+ - **回退**:无 key、`JevError`、超时、`529` 各一条,验证日报不崩且降级路径可观察。
271
+ - **批量模式**:`--over` 的展开顺序稳定、问题名↔文件映射正确、`--max-chars` 真的生效并回显。(历史教训:批量模式最初没写测试就加了,一跑就崩——删除补丁在 `main()` 里留下死代码,批量分支落进去引用未定义变量。)
272
+ - **隐私**:`reply_debt` 的 `--html` 页面**不含**任何消息正文(往行数据里塞正文,页面里不许出现)。
273
+ - **全部测试必须离线**,不得调用付费 API。(现有测试确实全部离线:触碰 Jev 的一律 `patch.object(jev.urllib.request,'urlopen')` 或传桩客户端,没有任何测试真的付钱调 `api.typesafe.ai`。)
274
+
275
+ **当前完全没有测试的输入路径**(都是"真调用付费服务"或 CLI 接线那一层,现有测试只覆盖纯函数)——这些是施工时可以补的地方:
276
+
277
+ - `quality_eval.py:169-175` `cmd_sample` 里真正 `create_client()` 并打分的那条路;
278
+ - `semantic_search.search()` 的重排接线(`498`/`507` 的 `create_client()` 与 pool 扩张 `491`/`504`);
279
+ - `route_cards.py` 的 `main()` 全链路(`307-406`:`--yes` 门、`kept or terms` 回退、命中计数、取消息);
280
+ - `reply_debt.py` 的 `main()` 端到端(`274-467`:参数默认值、`--min-prob` 过滤、`--html` 落文件、退出码);
281
+ - `biz_daily.py` 的 `--classifier` 参数接线与 `_apply_decision`(`271-286`)——**全仓库没有任何测试引用 `_apply_decision`**。
282
+
283
+ ---
284
+
285
+ ## 9. 验收标准
286
+
287
+ 只有全部满足才能报告完成:
288
+
289
+ - 改动前后工作区差异清晰,**未覆盖维护者已有修改**。
290
+ - `npm run build` 通过;`npm test` 通过;Python 相关专项测试通过;`git diff --check` 通过。
291
+ - 定标部分:如实报告已标注条数,以及"够不够形成结论"(<20 条时必须说明不稳)。
292
+ - 任何阈值改动都有 `DECISIONS.md` 条目与理由;`PROJECT_STATE.md` 的措辞同步。
293
+ - 检查差异中没有真实隐私数据、密钥、令牌、账号标识、数据库、日志或导出文件。
294
+ - **无法验证的事,明确报告"无法验证"及阻断原因**,不得把推测写成完成。宁可难看的占位,也不要通顺但可能错的结论。
295
+
296
+ ---
297
+
298
+ ## 10. 提交规则
299
+
300
+ 施工 Agent 默认只修改代码、测试和必要文档,**不自动提交或推送**。提交前输出:
301
+
302
+ 1. 改动文件清单;2. 每个改动的目的;3. 测试命令和结果;4. 仍存在的限制;5. 敏感信息检查结果。
303
+
304
+ 提交信息简短、准确、说明用户可见结果,例如:
305
+
306
+ ```text
307
+ fix: apply the calibrated inclusion threshold
308
+ ```
309
+
310
+ 不得在提交信息、Issue、PR 或公开文档中写入真实本地数据、文章标题或调试输出。