weflow-cli 1.7.0 → 1.8.1
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 +2108 -2
- package/OPERATIONS.md +810 -1
- package/README.en.md +96 -10
- package/README.md +34 -9
- package/bin/weflow-cli.ts +937 -28
- package/dist/bin/weflow-cli.js +955 -28
- 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 +25 -6
- 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 +599 -75
- package/dist/src/services/assistantService.js.map +1 -1
- package/dist/src/services/assistantTools.d.ts +217 -1
- package/dist/src/services/assistantTools.d.ts.map +1 -1
- package/dist/src/services/assistantTools.js +1116 -59
- 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/services/wereadService.d.ts +33 -6
- package/dist/src/services/wereadService.d.ts.map +1 -1
- package/dist/src/services/wereadService.js +27 -2
- package/dist/src/services/wereadService.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/BRANCHES.md +13 -4
- 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 +210 -9
- 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 +483 -0
- package/resources/panel/mascot-happy.png +0 -0
- package/resources/panel/mascot-sorry.png +0 -0
- package/resources/panel/mascot-thinking.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 +415 -0
- package/resources/panel/preload.cjs +79 -0
- package/resources/panel/quick-menu.cjs +197 -0
- package/resources/panel/renderer.js +470 -0
- package/resources/panel/tray-menu.cjs +40 -0
- package/resources/panel/tray.png +0 -0
- package/scripts/_utils.py +425 -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 +1060 -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 +429 -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 +25 -6
- package/src/services/assistantRouter.ts +191 -0
- package/src/services/assistantService.ts +681 -74
- package/src/services/assistantTools.ts +1149 -58
- 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/services/wereadService.ts +43 -7
- package/src/utils/atomicWrite.ts +56 -0
- package/src/utils/dateRange.ts +46 -0
- package/src/utils/text.ts +20 -0
package/OPERATIONS.md
CHANGED
|
@@ -132,12 +132,426 @@ weflow-cli sync verify <会话> # 重读记录范围并与当
|
|
|
132
132
|
| --- | --- |
|
|
133
133
|
| `complete` | 扫到的分片全部打开、打开的都读成功 |
|
|
134
134
|
| `unverified` | 有分片**打不开**——读到的部分可信,但那几个分片里有没有本会话消息**无法判断** |
|
|
135
|
-
| `partial` |
|
|
135
|
+
| `partial` | 已打开的分片没能按请求读完、或还有更多未读。**不算成功**,退出码 1,且不推进"上次成功时间" |
|
|
136
136
|
|
|
137
137
|
看到 `unverified` 时,`sync status` 的 `warnings` 会点名是哪个分片。这通常意味着该分片的密钥没解开——先排查 `init` 是否覆盖了全部分片。
|
|
138
138
|
|
|
139
|
+
`partial` 的 `warnings` 有三种,都意味着**那个分片的这段范围没读到**,对应处理方式不同:
|
|
140
|
+
|
|
141
|
+
| 警告 | 含义 | 怎么办 |
|
|
142
|
+
| --- | --- | --- |
|
|
143
|
+
| `shard-read-failed:<分片>` | 打开成功但读到一半出错 | 数据库可能损坏或被占用;重试,仍失败则视为该分片不可信 |
|
|
144
|
+
| `shard-not-read:<分片>:SCHEMA_MISMATCH` | 表里的列没有一个能认出来 | 微信表结构变了,需要更新读库代码;先别把这次同步当完整 |
|
|
145
|
+
| `shard-not-read:<分片>:WINDOW_UNAVAILABLE` | 表里没有 `create_time`,时间窗无法表达 | 同上;此时**不会**返回未筛选的行来充数 |
|
|
146
|
+
|
|
147
|
+
另有一类**不算** partial 的警告:`shard-columns-missing:<分片>:<列名>`——行都读回来了,只是少了某个字段。`coverage` 仍是 `complete`,因为要问的"有没有读全"答案是"读全了"。
|
|
148
|
+
|
|
139
149
|
**没有"稳定游标"**:`sync` 提供的是重叠窗口 + 本地去重,不是可直接续传的游标(D-027 要求所有后端都支持后才能宣称)。边界消息被重复读取是**预期行为**,会被去重吸收。
|
|
140
150
|
|
|
151
|
+
### 谁在等我回话
|
|
152
|
+
|
|
153
|
+
```powershell
|
|
154
|
+
weflow-cli awaiting --dry-run # 预览:会判哪些会话、要发多少字符(不出境)
|
|
155
|
+
weflow-cli awaiting --yes # 真跑:最近 14 天有动静的会话
|
|
156
|
+
weflow-cli awaiting --days 75 --limit 25 --min-prob 0.45 --yes
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**它会读取聊天正文并发送到决策模型**,所以照 `search` 的规矩来:`--dry-run` 只读本地、
|
|
160
|
+
零出境;`--yes` 才真跑;都没给时会让你确认。它不写任何本地数据。
|
|
161
|
+
|
|
162
|
+
每个会话一次决策调用(一次请求里同时问:是否停在我该回的位置、多急、有没有没兑现的
|
|
163
|
+
承诺、涉不涉及钱、属于哪类),所以每条判断都知道它属于哪个人。25 个会话约 5 秒。
|
|
164
|
+
`waiting` 概率低于 `--min-prob` 的不算欠账;判定为客服/推销/通知类的不计入(但会报数量,
|
|
165
|
+
免得看起来像没扫到)。
|
|
166
|
+
|
|
167
|
+
- **每行都会打印它的证据有多薄**:`证据:对方末条 N 字 · 对方实质发言 M 条`。
|
|
168
|
+
少于 5 个字会直接标注「证据很薄,这个分数不可当结论」——一个从两个字的末条得出的
|
|
169
|
+
0.69 和一个从整段说明得出的 0.69 不是一回事。
|
|
170
|
+
- 判定"对方在等我"但对话最后一条其实是我发的,会被自检抓出来单列。
|
|
171
|
+
- `--html <路径>` 另外写一份单页 HTML:**自包含、不含任何聊天内容**(只有概率、天数、
|
|
172
|
+
类别与证据量),所以可以直接发给别人看。页脚写明它不是什么:没有金标准校准过、
|
|
173
|
+
**概率在阈值附近会抖**(同一批数据两次跑会有出入,别细究 0.45 与 0.55 的区别)、
|
|
174
|
+
以及证据薄的那几条不足为凭。
|
|
175
|
+
- **没有金标准校准过**,当提示看,不当事实用。图片/语音等非文本消息以类型标签进入判断,
|
|
176
|
+
不会被当成空内容。
|
|
177
|
+
|
|
178
|
+
### 帮我起草回复
|
|
179
|
+
|
|
180
|
+
```powershell
|
|
181
|
+
weflow-cli draft 老王 --dry-run # 预览:会发多少字符给哪两个模型(不出境、不花钱)
|
|
182
|
+
weflow-cli draft 老王 --yes # 真跑:给 3 条候选,按"最适合当下一条"排序
|
|
183
|
+
weflow-cli draft 老王 --count 5 --yes # 最多 5 条
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**它只产出文字,不会替你发送**——没有回填输入框、没有模拟按键、没有抢焦点。
|
|
187
|
+
回不回、怎么回由你决定(本机面板里助手的每条回答下面有复制按钮,微信里那侧没有)。
|
|
188
|
+
|
|
189
|
+
跑的时候先**判断**再**起草**:判断那一步问七件事(对方想干什么、要什么、该用哪种动作、
|
|
190
|
+
该不该给实质内容、风险有多高、涉不涉及钱、有没有没兑现的承诺),结论会写进起草的提示词里,
|
|
191
|
+
并把你**自己以前说话的样子**当作语气样本。判断和排序各一次决策调用,起草一次生成调用,
|
|
192
|
+
所以是三次。
|
|
193
|
+
|
|
194
|
+
- **涉钱、或风险 ≥ 7 时不给草稿**,只说明风险在哪、该先确认什么。这不是失败,是这条
|
|
195
|
+
最该给的回答。风险的 0-9 是**拍的阈值**,用来解释,不单独用来决定。
|
|
196
|
+
- **判断那一步调不通时(Jev 免费期已过 / key 失效 / 网络断)会自动降级**:只用 DeepSeek 给出候选。
|
|
197
|
+
但**那道闸门就没有了**——它本来就是靠判断结果才存在的。所以降级时输出里会**先写一行**
|
|
198
|
+
「这次没有判断…闸门没生效」,候选照给。看到这行时请自己看一眼再决定发不发。
|
|
199
|
+
(三个 key 里现在**只有 `deepseekApiKey` 是必须的**。)
|
|
200
|
+
- 承诺未兑现**不会**被拒("我明天一定发你"正是该帮写的时候),改在提示词里约束不让它
|
|
201
|
+
替你许新的承诺;想让它也硬拒,加 `--gate-commitment`。
|
|
202
|
+
- **没有金标准校准过**:判断准不准只有"和你自己读一遍对比",不是校准过的结论。
|
|
203
|
+
- 判断用的是英文判据,而 `awaiting` 那两问是中文——同一个模型上的两套口径,**没做过 A/B**。
|
|
204
|
+
- 群聊不适用。
|
|
205
|
+
|
|
206
|
+
微信里让助手起草(工具名 `draft_reply`)走的是同一条实现,但**先把正文遮罩再交出去**,
|
|
207
|
+
且隐私模式为 `strict` 时直接拒绝:起草必须把正文发给远端模型,遮成"内容 N 字"之后再起草
|
|
208
|
+
只会得到一段看起来正常的空话。
|
|
209
|
+
|
|
210
|
+
### 右键快速回复(面板里)
|
|
211
|
+
|
|
212
|
+
在面板窗口里**点右键**,弹出一个菜单:名单里那几个人,加上最后一项「关闭悬浮球」。
|
|
213
|
+
点一个人,助手就给那个人起草候选。名单是一个配置键:
|
|
214
|
+
|
|
215
|
+
```powershell
|
|
216
|
+
weflow-cli config set quickReplyContacts "咸鱼梦想家,彪弟,平凡的世界" # 逗号分隔;改完**重启助手**
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
- 名单里写**联系人名**(就是你平时叫他的那个,和 `weflow-cli messages` 一样解析)。**改完要重启助手**(`assistant stop` 再 `assistant start`)——配置是守护进程启动时读进内存的,面板最多再等 30 秒(它每 30 秒轮询一次状态)。
|
|
220
|
+
重名会得到"匹配多个会话"这种可读的失败——**不猜**。
|
|
221
|
+
- 候选回在**对话气泡里**(就是平时问答的那个位置,最近 30 条对话是它的依据)。
|
|
222
|
+
- 菜单底部写着代价:**起草会把这段对话发给两个云端模型**(只产出文本,不会替你发送)。
|
|
223
|
+
点一下就是一次出境,所以那句话必须在菜单里看得见,而不是点完才知道。
|
|
224
|
+
- 弹的是**原生菜单**,画在窗口外面:所以球形态下窗口**不会变大、球也不会动**(上一版是「先把窗口展开再弹」,用户看到的就是「点右键把第二大脑窗口弹出来了」——那不是快速功能该有的样子)。
|
|
225
|
+
- 没配名单时菜单不弹空框,直接写着 `config set quickReplyContacts` 该跑的那条命令。
|
|
226
|
+
- 菜单**最后一项**是「**关闭悬浮球(托盘图标能再打开)**」:点它只是把球**收起来**(`win.hide()`),
|
|
227
|
+
助手照常在后台跑。叫回来有三条路:点托盘图标、托盘菜单的「显示 / 收起」、`Ctrl+Shift+W`。
|
|
228
|
+
之所以在标签里写出"托盘图标能再打开":球是 `setSkipTaskbar(true)`,藏起来之后任务栏里没有它,
|
|
229
|
+
不写清楚就像"点一下它永远消失了"。真要退出用**托盘菜单**那两项(「退出面板」/「退出并停止助手」)——
|
|
230
|
+
"收起来"和"退出"是两件事,不放在同一个菜单里。
|
|
231
|
+
这一项在**名单为空时也在**,否则没配过名单的人连球都收不起来。
|
|
232
|
+
- 浏览器降级那条路**不接管右键**(Edge/Chrome 原生菜单里的「复制」要留给用户),那边也就没有快速回复——要起草就在对话里直接问一句。
|
|
233
|
+
|
|
234
|
+
### 本机判断层
|
|
235
|
+
|
|
236
|
+
把"判断"从"生成"里拆出来,做成一条命令行原语:**一个 state、一批类型化问题、一次调用**,
|
|
237
|
+
返回带概率的类型化答案,外加这次调用的 token 数与花费。它**不读任何本地数据**——
|
|
238
|
+
state 是什么完全由你给。
|
|
239
|
+
|
|
240
|
+
```powershell
|
|
241
|
+
# 从文件(CLI):
|
|
242
|
+
weflow-cli decide --request req.json --dry-run # 只校验并回显形状,不出境、不需要 key
|
|
243
|
+
weflow-cli decide --request req.json --yes
|
|
244
|
+
|
|
245
|
+
# 从 stdin(直接调脚本,管道里更好用):
|
|
246
|
+
echo '{"state":"...","questions":{"相关":{"type":"noul","instructions":"..."}}}' | python scripts/decide.py
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
请求格式:
|
|
250
|
+
|
|
251
|
+
```json
|
|
252
|
+
{"state": "字符串 / JSON 对象 / 数组都行",
|
|
253
|
+
"questions": {
|
|
254
|
+
"要退款": {"type": "noul", "instructions": "对方明确要求退款吗?"},
|
|
255
|
+
"紧急度": {"type": "score", "instructions": "多急?", "criteria": ["不急", "一般", "紧急"]},
|
|
256
|
+
"部门": {"type": "choice", "instructions": "转给谁?",
|
|
257
|
+
"criteria": {"billing": "账单", "tech": "报错"}}
|
|
258
|
+
}}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
**`score` 的 criteria 是零索引的有序数组**——位置即分值,`criteria[0]` 是 0 分。
|
|
262
|
+
只有一档时会被本机拒掉,因为那样 score 恒等于 0,问不出东西。
|
|
263
|
+
|
|
264
|
+
批量模式把最常用的那层展开做掉了——一个 glob × 一批问题,自动变成 N×M 个问题:
|
|
265
|
+
|
|
266
|
+
```powershell
|
|
267
|
+
weflow-cli decide --over "scripts/*.py" --ask "会写本地文件吗" --ask "会出网吗" --dry-run
|
|
268
|
+
weflow-cli decide --over "scripts/*.py" --ask "会写本地文件吗" --ask "会出网吗" --yes
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
- `--max-chars`(默认 1500)决定每个文件喂多少,**它决定上限**:同一批问题,只给每个文件
|
|
272
|
+
前 14 行时与基准一致 77%,给前 1500 字符时 89%。返回里会回显这个值。
|
|
273
|
+
- 答案的键是 `f<序号>|q<序号>`,序号到文件/问题的映射在返回的 `batch.files` / `batch.asks` 里,
|
|
274
|
+
不用去解析问题名。
|
|
275
|
+
- `--request` 那条路**不读任何本地文件**,`--over` 会读——`capabilities` 里是分开声明的。
|
|
276
|
+
|
|
277
|
+
什么时候值得用它:**一批**判断(比如给 200 个条目各打几个标签),一次请求约 1 秒,
|
|
278
|
+
问题数几乎不影响成本。**一次性的单个判断不值得**——调用方自己的模型就够了,
|
|
279
|
+
多这一跳只是绕路。
|
|
280
|
+
|
|
281
|
+
它对**不生成文本**这一点是有意的:答案不会以散文形式回来,所以放在控制流里是安全的。
|
|
282
|
+
这一版**没有**把它暴露成 MCP 工具——那会让远端 MCP 客户端能驱动本机往第三方发文,
|
|
283
|
+
属于要单独决策的出境面。
|
|
284
|
+
|
|
285
|
+
### 校准:这些概率到底准不准
|
|
286
|
+
|
|
287
|
+
前面所有概率的依据都是"与另一个不完美的基准比"。要换成真的答案,只有一条路:人工标注
|
|
288
|
+
一批,然后算校准。这个脚本把那条路压到十分钟:
|
|
289
|
+
|
|
290
|
+
```powershell
|
|
291
|
+
python scripts/quality_eval.py sample --n 50 # 抽样并现场打分(约 $0.005)
|
|
292
|
+
# 打开 ~/.weflow-cli/labels/labels-<时间戳>.json,把每条的 label 填成:
|
|
293
|
+
# {"topic": "AI|学术|新闻|文学|投资|政治", "relevance": "低|中|高", "include": true/false}
|
|
294
|
+
# 没把握的留 null —— 它算"未标注",不算"标错"
|
|
295
|
+
python scripts/quality_eval.py score ~/.weflow-cli/labels/labels-<时间戳>.json
|
|
296
|
+
python scripts/quality_eval.py consistency ~/.weflow-cli/labels/labels-<时间戳>.json
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
**清单是盲的**:`sample` 打印给你的那张表**不印 Jev 的答案**(只有日期与历史标注)。
|
|
300
|
+
先看见模型说了什么,人就会往它靠,测出来的"一致率"会偏高——而偏高的量无法估计。
|
|
301
|
+
`topicConfidence`/`relevanceScore`/`includeScore` 仍然存在 JSON 里供算账,标注时别翻它。
|
|
302
|
+
|
|
303
|
+
`score` 会给出四件事:
|
|
304
|
+
|
|
305
|
+
1. **主题判断的准确率**,同时给出**历史存档标签**的准确率作对照——存档标签一直被当成
|
|
306
|
+
基准,但它本身就是要被检验的那个东西。
|
|
307
|
+
2. **相关度的准确率 + 平均差几档**。只比三个字会漏掉"整体偏高一点点"这种系统性错位:
|
|
308
|
+
两篇都判成「中」、一个偏 +0.4 一个偏 −0.4,准确率是 100%,而平均差 0.4 说明分数没
|
|
309
|
+
对齐档位——准确率看不见这件事。
|
|
310
|
+
3. **校准表**:每个概率区间里,人工说"该收"的比例。**斜着往上走说明分数有分辨力;
|
|
311
|
+
一条平线说明它只是在乱猜**——这是"概率有没有意义"唯一能回答的方式。
|
|
312
|
+
4. **两处阈值扫描**:收录分的切点(现在 `0.50`)与相关度的两条线(现在 `0.5 / 1.5`)。
|
|
313
|
+
切点是当时拍的——`jev_client` 里那句注释自己写着"暂定"。最优点离它们很远的话,
|
|
314
|
+
该改的是常数,不是模型。
|
|
315
|
+
|
|
316
|
+
`consistency` 是**不需要人工标签**的那一半:同一篇文章有没有被答成"高相关 / 没内容"
|
|
317
|
+
或"低相关 / 很有内容"。这是 `jev-chat-jarvis` 的题目书里那条硬要求("题目之间不许互相
|
|
318
|
+
矛盾")——他们靠标注集去压,而**矛盾是自证的**,所以它现在就能给你一个数。矛盾率高
|
|
319
|
+
说明题目的措辞或切点有问题,不是模型有问题。
|
|
320
|
+
|
|
321
|
+
**打了的分会被缓存**(`~/.weflow-cli/labels/.jev-scores.json`,按文章路径)。这不是为了省时间,
|
|
322
|
+
是为了**让 `--seed` 真的能复现样本**:分数由模型现场给出,同一篇两次可能 0.49 / 0.52 地跨过一
|
|
323
|
+
档边界,于是分档抽样抽出来的又是另一批(实测同一条命令连跑两次,50 条里 7 个位置不同)。
|
|
324
|
+
模型换了或判据改了要加 `--refresh` 重新打分。
|
|
325
|
+
|
|
326
|
+
**样本是现场用生产路径重新打分的**,不是读存档值:磁盘上只有 4 篇带 `relevanceScore`、
|
|
327
|
+
**一篇都没有 `includeScore`**(那 4 篇是 2026-09-05 开发期 `--date` 重跑的残留),而且
|
|
328
|
+
存档的 `topic` 正是要被检验的对象。它只**读** `output/`,不重跑、不写回。
|
|
329
|
+
|
|
330
|
+
`--pool`(默认 150)是**先打分的候选池**,最终样本再从池里**按概率档位**分层取。这一步
|
|
331
|
+
是必须的:只按主题抽样得到的 50 篇里,45 篇的收录分都在 0.2 以下,**你标 50 条买到的
|
|
332
|
+
信息量等于标 4 条**。分层后 50 篇里有 22 篇落在真正影响阈值的区间。代价是样本不再是
|
|
333
|
+
自然分布——所以档位在池里的**自然占比会单独报出来**(`prevalence`),它回答的是另一个
|
|
334
|
+
问题:"这个阈值每天会多收或少收多少篇"。实测(160 篇均衡候选):86.1% 低于 0.2,
|
|
335
|
+
**8.9% ≥ 0.5**。
|
|
336
|
+
|
|
337
|
+
**耗时**:单次判断约 1 秒,且与 state 长短无关(实测 200 字与 4000 字都是 ~1.1s);
|
|
338
|
+
但这个数字是**空载**下的。6 并发跑 158 篇时实测平均 **5.7 秒/篇**(有 10 秒的离群),
|
|
339
|
+
所以估算大批量时按"秒/篇"算,别按"一秒"。
|
|
340
|
+
|
|
341
|
+
标注文件落在 `~/.weflow-cli/labels/`,里面是文章标题与你的判断,不进仓库。
|
|
342
|
+
|
|
343
|
+
### 判据改动的对照实验(不需要标注)
|
|
344
|
+
|
|
345
|
+
想动判据措辞(比如把它们改成英文——`jev-chat-jarvis` 说 Jev 主训练语言是英文)之前,
|
|
346
|
+
先量一下**改动会改变什么**:
|
|
347
|
+
|
|
348
|
+
```powershell
|
|
349
|
+
python scripts/jev_probe.py --criteria-ab --limit 24 --yes
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
它把同一批文章问两遍(现行判据 vs 英文具体情景变体),报主题一致率、相关度原始分的
|
|
353
|
+
平均差、以及分歧落在哪几篇。**它回答不了"改好没有"**——那需要上面的标注集。
|
|
354
|
+
本机实测(24 + 12 篇):主题一致率 79% / 67%,相关度平均差 0.17 / 0.24,分歧集中在
|
|
355
|
+
**AI↔学术**(一篇讲方法的论文)与**文学↔新闻**这两个边界上。
|
|
356
|
+
|
|
357
|
+
### 语义检索要用的 embedding key
|
|
358
|
+
|
|
359
|
+
语义检索和 RAG 问答需要阿里云百炼(DashScope)的 key:
|
|
360
|
+
|
|
361
|
+
```powershell
|
|
362
|
+
weflow-cli config set dashscopeApiKey "sk-..."
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
它和别的密钥一样**机器绑定加密保存**。不设的话 `search`/`chat` 会直接说缺 key,
|
|
366
|
+
而不会拿一个空 key 去请求。`--api-key` 参数或 `DASHSCOPE_API_KEY` 环境变量仍然优先,
|
|
367
|
+
临时用一次时更方便。
|
|
368
|
+
|
|
369
|
+
顺带一提,`favPassphrase`(解锁收藏与 biz 库的口令)现在也能这样设置了——
|
|
370
|
+
|
|
371
|
+
```powershell
|
|
372
|
+
weflow-cli config set favPassphrase "..."
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
在此之前它是个"一等机密却设不了"的项:在加密名单里、被日报与欠账雷达读,
|
|
376
|
+
却不在 `config set` 的允许名单里。
|
|
377
|
+
|
|
378
|
+
### 索引窗口:知识库记多久
|
|
379
|
+
|
|
380
|
+
建索引时要先决定**收多长时间的数据**,这决定了"知识库记得住多少":
|
|
381
|
+
|
|
382
|
+
```powershell
|
|
383
|
+
weflow-cli search-index --dry-run # 先看窗口与代价(不读库、不联网、不写文件)
|
|
384
|
+
weflow-cli search-index --yes # 默认:聊天最近 90 天 + 日报最近 30 个日期目录
|
|
385
|
+
weflow-cli search-index --days 3650 --article-days 365 --yes # 要"整段历史"就显式给大值
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
- 默认值与从前**完全一致**(90 / 30)——加参数不是改代价,只是把原来写死、改不了的数字
|
|
389
|
+
变成改得了的。想建"整份聊天记录"的知识库,就显式给一个大窗口。
|
|
390
|
+
- **天数越多,发给向量服务的文本越多**,这是出境成本,所以确认语里会写明这次用的窗口。
|
|
391
|
+
- 上限 36500 天(≈100 年);`0` 会被拒——0 天等于建出一个**空**索引,而它看起来像"建好了"。
|
|
392
|
+
|
|
393
|
+
### 知识库框架:一图看懂
|
|
394
|
+
|
|
395
|
+
```
|
|
396
|
+
素材 卡片(每篇/每会话一次模型调用) 概念页(本地聚合+生成) 用
|
|
397
|
+
─────────────────────────────────────────────────────────────────────────────────────────
|
|
398
|
+
文章笔记(Vault 34,092 篇)──article-notes──┐
|
|
399
|
+
├─→ output/*-notes/ ──wiki compile──→ Wiki/Concepts/*.md(文章线)
|
|
400
|
+
你的聊天(最近 N 天) ──chat-notes────┘ │ + Wiki/00-Overview.md
|
|
401
|
+
│ └─(聊天线要加 --output)──→ Chat/Concepts/*.md
|
|
402
|
+
│ + Chat/00-Overview.md
|
|
403
|
+
(卡里带 [[概念]] — 说明,这才是聚合的原料) │
|
|
404
|
+
↓
|
|
405
|
+
vault search / 助手的 search_knowledge
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
四条命令,各管一段(**故意不做成一条大命令**:中间两步都要出境,合成一条会把"这一步要发什么出去"
|
|
409
|
+
藏起来):
|
|
410
|
+
|
|
411
|
+
| 命令 | 出境 | 说什么 |
|
|
412
|
+
| --- | --- | --- |
|
|
413
|
+
| `weflow-cli article-notes --limit N --yes` | 文章发给 DeepSeek(每篇一次) | 默认**增量**:已有卡的不重做(`--refresh` 强制)。**按时间跑用 `--since`/`--until`**("把 9 月的跑一遍"是人的说法,`--limit` 表达不了:用 limit 凑月份,要么漏要么带上隔壁月) |
|
|
414
|
+
| `weflow-cli chat-notes --days N --yes` | 对话发给 DeepSeek(每会话一次) | 卡是滚动快照,重跑即覆盖 |
|
|
415
|
+
| `weflow-cli fav-notes --limit N --yes` | 收藏的正文发给 DeepSeek(每条一次) | 正文优先用收藏自带的;文章类走日报那条线的抓取(**带缓存**,日报抓过就零网络);`--dry-run` 只报"几条要抓",自己不联网 |
|
|
416
|
+
| `weflow-cli user-notes --limit N --yes` | **你自己写的**笔记发给 DeepSeek(每篇一次) | 你的笔记**只读、绝不修改**;卡上标 `summary_by: model`(那是模型的理解,不是你的原话);**你笔记里的 `[[链接]]` 优先** |
|
|
417
|
+
| `weflow-cli wiki compile --source … --min-refs 2 --yes` | 概念发给 DeepSeek(每个概念一次) | `--min-refs 2` 滤掉只被一篇提到的一次性实体 |
|
|
418
|
+
| `weflow-cli wiki compile --refresh-sources --yes` | **不出境** | 只把**已有页**的「来源」段补齐成当前全部来源(本地重写)。**拉完新文章要跑这一条**,否则新文章提到老概念时,那一页不会多出这条边 |
|
|
419
|
+
| `weflow-cli wiki compile --merge-duplicates --dry-run` | **不出境** | 把「规范化后同名」的多张页合并成一张(`GPT 5.6`/`GPT-5.6`/`GPT5.6`)。留来源最多的那张,其余名字进 `aliases`——**指向旧名字的链接一条都不断**,不必改别的文件。**它会删页**,先跑 `--dry-run` 看名单 |
|
|
420
|
+
| `weflow-cli wiki compile --pages-from-cards --cards <线> --dry-run` | **不出境** | 给还没有页的概念建页:定义复用卡片里那句原话、相关概念取共现。**必须指定 `--cards`**(一条线一条线建,否则聊天线会被建进文章线) |
|
|
421
|
+
| `weflow-cli wiki lint` | **不出境** | 断链 / 孤儿 / 空页 / 同名 / 近似重名 / 退化字段 |
|
|
422
|
+
|
|
423
|
+
四条卡片来源(文章 / 聊天 / 收藏 / 你自己的笔记)产出的东西是**同一种形状**(带 frontmatter +
|
|
424
|
+
`## AI 摘要` + `[[概念]] — 说明`),所以 `wiki compile` 一视同仁——再加一条来源时改的是产卡那一步,
|
|
425
|
+
聚合那一步不用动(体检也一样:卡片目录用 `output/*-notes` 通配找,不写死目录名)。
|
|
426
|
+
|
|
427
|
+
### 从聊天长出知识页(文章线之外的那一半)
|
|
428
|
+
|
|
429
|
+
知识库有两条来源:**公众号文章**(`output/biz-daily` → `wiki compile`)和**你自己的微信对话**。
|
|
430
|
+
后者分两步走,第二步是现成的:
|
|
431
|
+
|
|
432
|
+
```powershell
|
|
433
|
+
weflow-cli chat-notes --dry-run # 先看代价:几个会话、多少字符会发给生成模型(只读本地)
|
|
434
|
+
weflow-cli chat-notes --days 30 --yes # 每个会话一张知识卡 → output/chat-notes/
|
|
435
|
+
weflow-cli wiki compile --source ./output/chat-notes --output ./output/wechat-vault/Chat/Concepts --yes
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
- **第一步会出境**:每个会话的对话会发给生成模型(一次调用一个会话)。所以有 `--dry-run`
|
|
439
|
+
先报数、有 `--yes` 才真跑——与 `draft` 是同一条纪律。产出的卡**只写到本地**,
|
|
440
|
+
不会发送任何消息。
|
|
441
|
+
- **第二步不需要联网**(概念页是本地聚合 + 生成模型,走的是 `wiki compile` 原有的确认流程)。
|
|
442
|
+
- **`--output` 要指到 `Chat/Concepts`**:文章线与聊天线在库里是**两个目录**(`Wiki/` 与 `Chat/`,
|
|
443
|
+
同一个 Obsidian 库)。`wiki compile` 一次只产一条线的页,默认写文章线——用默认值跑聊天线,
|
|
444
|
+
两边的页会混进同一个目录,而图谱里就再也分不出"我读到的"和"我们聊过的"。
|
|
445
|
+
- 三种形状自然落下来:**按会话的知识卡**就是第一步入库的那些卡;**主题汇总**与
|
|
446
|
+
**人物/时间线**是第二步把卡里的 `[[话题]]`/`[[人名]]` 聚出来的概念页。
|
|
447
|
+
- **文章线要先用一道提炼**(2026-09-26 实测后补上):
|
|
448
|
+
```powershell
|
|
449
|
+
weflow-cli article-notes --dry-run # 先看代价(只读本地)
|
|
450
|
+
weflow-cli article-notes --limit 30 --yes # 每篇一次调用,**只问概念**(摘要照抄原笔记)
|
|
451
|
+
weflow-cli wiki compile --source ./output/article-notes --yes
|
|
452
|
+
```
|
|
453
|
+
实测:30 篇 → 110 个概念 → 生成 10 个概念页(`output/wechat-vault/Wiki/Concepts/`,
|
|
454
|
+
另加一张索引 `Wiki/00-Overview.md`)。生成的概念是「标签配料表一致性」「椰子水全覆盖风险排查」
|
|
455
|
+
这种具体的,不是主题词。
|
|
456
|
+
- **为什么必须有这道提炼**(历史)::`wiki compile` 要的是「概念形状」的链接
|
|
457
|
+
(`[[概念]] — 关于它说了什么`),而 Vault 里那 1633 篇文章笔记里**一条都没有**——正文里的
|
|
458
|
+
`[[…]]` 只有两类:与自己主题同名的那条(`> - **主题**: [[AI]]`),以及 `## 🔗 关联网络` 里的路径互链。
|
|
459
|
+
这两类**都不是概念**(拿它们当概念跑过:引用榜首是「新闻 752 / 政治 388 / 学术 341」加两条 `.md` 路径),
|
|
460
|
+
所以聚合器现在会把它们滤掉,结果是文章线暂时聚不出东西。要有产出,得再加一道「从文章提炼概念」的
|
|
461
|
+
步骤(对话线那道就是 `chat_notes`)。
|
|
462
|
+
- 太薄的会话(少于 3 条消息或 60 字)**不产卡**,并在输出里列出来——几句话的对话写成"知识卡"
|
|
463
|
+
只会是"只说了两句话",还白占一次调用、往概念页里灌一条没有信息量的来源。
|
|
464
|
+
- 卡是**滚动覆盖**的:同一个会话永远只对应一个文件,重跑就重写(记录 `updated` 与窗口),
|
|
465
|
+
所以不会越跑越多;概念页**是累积的**——**卡是快照,概念是账本**。要看更长的时间跨度,
|
|
466
|
+
就用更大的 `--days` 重跑。
|
|
467
|
+
- **但"累积"要靠 `wiki compile --refresh-sources` 这一步,它不是自动的。** 实测:`wiki compile`
|
|
468
|
+
只为**还没有页**的概念建页,已有页一个字节都不动——所以新文章再讲到 `DeepSeek`,那一页的来源
|
|
469
|
+
也不会多一条,图谱只长新节点。`--refresh-sources` 就是补那些边:扫所有 `output/*-notes`、
|
|
470
|
+
把每张页的来源段重写成当前的全部来源。它**本地重写、不调模型**,幂等可以反复跑。
|
|
471
|
+
实测一次补上了文章线 324 张页 / 3,164 条边、聊天线 34 张 / 148 条。
|
|
472
|
+
两个细节值得知道:**页里那些当前卡片已找不到出处的旧来源行会原样留着**(只报数不删——那篇文章
|
|
473
|
+
可能真的讲过这个概念,只是卡片的当前版本没提),所以页里的来源数**可能多于**索引里的引用数
|
|
474
|
+
(索引反映"卡片现在怎么说",页是"历史上所有引用");**frontmatter 的 `sources:` 故意不跟着改**,
|
|
475
|
+
它要的是卡片名(给 `来源/*` 标签用),而正文要的是 Vault 里能解析的名字,两者本来就是两套。
|
|
476
|
+
|
|
477
|
+
#### 一边自己写、一边让 AI 读(闭环)
|
|
478
|
+
|
|
479
|
+
```powershell
|
|
480
|
+
# 1) 你在 Obsidian 里写:000_Inbox / 003_Ideas / 004_Permanent / 005_Reference … 或库根目录直接新建
|
|
481
|
+
# 2) 让 AI 读你写的,长出概念
|
|
482
|
+
weflow-cli user-notes --dry-run # 先看几篇、多少字会出境(空笔记会被列出来)
|
|
483
|
+
weflow-cli user-notes --yes
|
|
484
|
+
weflow-cli wiki compile --source ./output/user-notes --min-refs 1 --yes
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
- **你的笔记只读**:这条线一个字都不会改你的笔记,产出另存在 `output/user-notes/`;
|
|
488
|
+
- **卡上的摘要是模型的理解**(`summary_by: model`,正文里也写着"不是你的原话")——
|
|
489
|
+
一段模型的理解被当成"我自己写的",是这条线最该防的事;
|
|
490
|
+
- **你笔记里自己写的 `[[链接]]` 优先**:你自己起的概念名排前面,模型只补你没提到的;
|
|
491
|
+
- **闭环怎么闭**:写 → AI 长出概念页 → 你若手改概念页,**下次 compile 会覆盖** ✗
|
|
492
|
+
所以值得留的版本请挪到 `004_Permanent/`(人写层)——下次 `user-notes` 会把它当**来源**读回去 ✓
|
|
493
|
+
- 生成的概念页带 `来源/我的笔记` 标签:`tag:#来源/我的笔记` 一眼分出"我想到的"和"我读到的"。
|
|
494
|
+
|
|
495
|
+
### 在 Obsidian 里怎么用这些页
|
|
496
|
+
|
|
497
|
+
- **按类型筛**:每个概念页带嵌套标签 `知识/概念`,搜索 `tag:#知识/概念` 就是全部知识页
|
|
498
|
+
(嵌套标签的写法借自一个现成的考公知识库);
|
|
499
|
+
- **看图谱要带筛选**:库里上万条材料,直接开全局图谱会卡。只想看知识页,把图谱左上角筛选框填
|
|
500
|
+
`path:"Wiki/Concepts"` 或聊天线的 `path:"Chat/Concepts"`(两个一起:`path:"Wiki/Concepts" OR path:"Chat/Concepts"`);更顺手的是**局部图谱**(打开任意一页 → 右上角更多 →「局部图谱」),秒开;
|
|
501
|
+
- **别直接改这些笔记**:概念页与 `00-Overview.md` 都是生成的,重跑 `wiki compile` 会覆盖——
|
|
502
|
+
要改就改上游的材料(卡片)再重跑。唯一的例外是 `--refresh-sources`:它只**追加**来源行、
|
|
503
|
+
不动别的,也不动 frontmatter;
|
|
504
|
+
- **它们是线索不是定论**:每页 frontmatter 里 `verified: false`,表示**未经人工核验**。
|
|
505
|
+
|
|
506
|
+
## 知识库体检(本地,不联网)
|
|
507
|
+
|
|
508
|
+
```powershell
|
|
509
|
+
weflow-cli wiki lint # 断链 / 孤儿 / 空页 / 同名 / 近似重名 / 退化字段
|
|
510
|
+
weflow-cli wiki lint --json # 机器可读
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
- **断链**:`## 来源` 里指向的卡片没了(卡片删了或改名)——这是**真坏了**;
|
|
514
|
+
- **还没建页的相关概念**:`## 相关概念` 里模型提出、但当前 `--limit` 还没轮到建页的——
|
|
515
|
+
这是**扩张候选,不是错误**(实测首次跑:38 个候选、0 个断链)。两类分开报,是为了让你
|
|
516
|
+
还能信任这份报告;混在一起报成"40 条死链",下次就没人看了;
|
|
517
|
+
- **孤儿**:连卡片都没提到过它(卡片链接也算入链,否则每张页都会被误报);
|
|
518
|
+
- **空页**:没有定义或要点——多半是那次模型返回不完整,重跑 `wiki compile` 就好;
|
|
519
|
+
- **同名 / 大小写同名**:同一个名字有多张页,或名字只差大小写——Windows 上后者是同一个文件;
|
|
520
|
+
- **退化字段**:整列几乎同一个值(实测 `topic: AI` 占 18,474/22,374 页)——那个字段等于没携带
|
|
521
|
+
信息,**别照着信**;
|
|
522
|
+
- **同一个节点的不同写法**(2026-09-28 起):规范化之后同名,也就是只差空格/连字符/大小写/
|
|
523
|
+
英文复数尾。判据与 `wiki compile --merge-duplicates` 是**同一份实现**(`_utils.normalize_concept_name`),
|
|
524
|
+
所以这份名单是**可以直接执行的**。22,374 张页时实测 314 组;同日按这份名单合并掉 **341 张页**
|
|
525
|
+
(文章线 338、聊天线 3),现在剩 **0 组**。合并后 `wiki lint` 的断链仍是 0——`aliases` 把指向
|
|
526
|
+
旧名字的链接全接住了。**按目录跑**(两个目录是两个知识库),跨线同名由下一节报;
|
|
527
|
+
- **一个名字是另一个加了限定**(同日新增):实测 378 组,**这条是线索不是结论**——人工抽样大约
|
|
528
|
+
一半是真重复(`Agent Harness` / `Agent Harness研讨会`),另一半是"主概念 vs 它的子话题"
|
|
529
|
+
(`国家自然科学基金` / `国家自然科学基金申请书`),那些**不该合并**。判据里专门挡了"枢纽"
|
|
530
|
+
(短名字已被 ≥5 个别名包含,如 `agent` 被 731 个名字包含),但挡不干净,删哪张由人定。
|
|
531
|
+
|
|
532
|
+
这两档取代了旧的"标题高度相似"判据:那条(公共子串 ≥5 字且占较短标题 ≥40%)在 5,062 张页
|
|
533
|
+
时合理,到 22,374 张页时报出 **424,697 组**、完全失去分辨力。换判据的实测记录在
|
|
534
|
+
`near_duplicate_titles` 的 docstring 里(包括被否掉的另外两个方向)。
|
|
535
|
+
|
|
536
|
+
**两条线都是默认增量**:`article-notes` 只处理还没卡的(原笔记改了才重做,`--refresh` 强制重做),
|
|
537
|
+
`chat-notes` 的卡是滚动快照(本来就该覆盖)。所以"补新的"不会把旧的再问一遍。
|
|
538
|
+
|
|
539
|
+
### 检索与重排
|
|
540
|
+
|
|
541
|
+
```powershell
|
|
542
|
+
weflow-cli search "PM2.5 对植物用水效率的影响"
|
|
543
|
+
weflow-cli search "..." --no-rerank # 只按向量/关键词相似度,不调决策模型
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
检索分两段:第一段算相似度取回 20 条候选,第二段用**一次**决策请求给每条候选问一个
|
|
547
|
+
"是否真的回答了这个问题",按概率重排后返回 `--top-k` 条(默认 10)。第二段约 1 秒,
|
|
548
|
+
与候选数基本无关。
|
|
549
|
+
|
|
550
|
+
- 返回里 `score` 仍是相似度(含义没变),重排的分数在 `rerankScore`。
|
|
551
|
+
- 没配 `typesafeApiKey`、连接失败或服务过载(529)时,**原序返回并打一行 WARN**,
|
|
552
|
+
检索本身不会因此失败。瞬时错误会自动重试最多 2 次。
|
|
553
|
+
- 想确认重排有没有帮上忙:同一个查询跑一次带重排、一次 `--no-rerank` 对比顺序即可。
|
|
554
|
+
|
|
141
555
|
### 语音消息转文字
|
|
142
556
|
|
|
143
557
|
HTML 里放不出语音:微信语音是 **SILK v3** 格式,浏览器不支持,ffmpeg 也没有 SILK 解码器(只有 AMR)。所以语音转文字是让语音消息在导出里能携带信息的唯一办法。
|
|
@@ -209,6 +623,11 @@ python scripts/wechat_voice.py --db "<message_0.db>" --key <key> --salt <salt> `
|
|
|
209
623
|
weflow-cli daily
|
|
210
624
|
```
|
|
211
625
|
|
|
626
|
+
报告末尾会附一段「我拿不准的」:收录分卡在阈值上的条目(分收了与没收两组)、以及主题
|
|
627
|
+
置信度偏低、可能被分错栏的条目。这一段在本地算出来,**不额外调用模型**。如果这一天的文章
|
|
628
|
+
生成于概率字段引入之前,它会直接说明"这份报告无法告诉你它哪里不确定"——那一段缺席
|
|
629
|
+
不等于"哪里都很确定"。
|
|
630
|
+
|
|
212
631
|
关闭本次运行的全部 AI:
|
|
213
632
|
|
|
214
633
|
```powershell
|
|
@@ -249,6 +668,88 @@ weflow-cli config show
|
|
|
249
668
|
weflow-cli config set dailySourceCategories '{"公众号A":"新闻","公众号B":"政治"}'
|
|
250
669
|
```
|
|
251
670
|
|
|
671
|
+
**配了类别的来源会失去标签,这是设计。** 一旦某个来源有了类别,日报对它只发
|
|
672
|
+
"生成摘要"的提示词,并明确不要模型输出主题/标签/相关度/概念——主题用你配的那个,
|
|
673
|
+
标签直接取 `[类别]`。所以标签只有**没配类别**的来源才会有真内容。实测:11 天
|
|
674
|
+
1584 篇里 1396 篇的标签恰好等于主题、182 篇为空、只有 6 篇是真标签,而那 6 篇正是
|
|
675
|
+
唯一没配类别的语料。
|
|
676
|
+
|
|
677
|
+
代价要自己权衡:想让某个来源有真标签,就得把它的类别留空,代价是它的主题改由
|
|
678
|
+
模型判断而不是你说了算。类别值必须是 `AI/学术/新闻/文学/投资/政治` 之一——写错
|
|
679
|
+
一个词,那篇文章会被兜底类接住,运行时也会打一行 WARN 告诉你有多少篇落了兜底。
|
|
680
|
+
|
|
681
|
+
### 文章分类用谁判断
|
|
682
|
+
|
|
683
|
+
主题与相关度默认由 TypeSafe 的 Jev 决策模型判断(`choice` 选主题、`score` 打相关度,
|
|
684
|
+
返回概率而不是一段要解析的文字)。配了 key 就用,没配就沿用原来的 LLM 解析路径:
|
|
685
|
+
|
|
686
|
+
```powershell
|
|
687
|
+
weflow-cli config set typesafeApiKey "..." # 机器绑定加密保存,和 deepseekApiKey 一样
|
|
688
|
+
weflow-cli config set typesafeApiKey "" # 清空即回到 LLM 解析路径
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
### 不想看某一类:排除主题
|
|
692
|
+
|
|
693
|
+
```powershell
|
|
694
|
+
weflow-cli config set dailyExcludeTopics "新闻,投资,学术" # 留空即不排除
|
|
695
|
+
weflow-cli config set dailyExcludeTopics ""
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
**这是展示层开关,不是抓取层的**:正文照常抓取、照常归档,只是不出现在日报报告和
|
|
699
|
+
日报页里。所以改主意不用重抓——改配置重新生成一遍就行。
|
|
700
|
+
|
|
701
|
+
为什么不做成"拉之前就判类型、不想要的不拉":实测过,拉之前只有来源+标题+平台摘要,
|
|
702
|
+
按它判类型和你配的来源类别一致率只有 60%(217 篇里 48 篇误伤),而且**排除不可逆**——
|
|
703
|
+
没抓就没归档,而公众号页隔几周再看已不是同一篇。宁可多看一眼,不可丢掉一篇。
|
|
704
|
+
|
|
705
|
+
几条边界,写下来免得踩:
|
|
706
|
+
|
|
707
|
+
- 写错主题名会被忽略,但会**打一行 WARN**(静默忽略会让人以为过滤生效了);
|
|
708
|
+
- **焦点主题(AI)不能排除**:排掉它不会得到空报告,而会得到一份会走错方向的报告——
|
|
709
|
+
没有文章时它报的是"未找到文章,请先运行 biz_daily.py"。同样 WARN 后忽略;
|
|
710
|
+
- 排除和 `--include-all`、收录分是**正交**的:任何参数组合都不会把排掉的类带回来;
|
|
711
|
+
报告末尾"我拿不准的"那三份清单也一起过滤,不会一边说不要新闻、一边在下面列新闻。
|
|
712
|
+
|
|
713
|
+
单个脚本上想临时看一次(不动配置):
|
|
714
|
+
|
|
715
|
+
```powershell
|
|
716
|
+
python scripts/generate_ai_report.py --date 2026-09-05 --exclude-topics "新闻,投资"
|
|
717
|
+
python scripts/generate_html.py --date 2026-09-05 --exclude-topics "新闻,投资"
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
### 来源级先验:哪个号一贯发哪一类
|
|
721
|
+
|
|
722
|
+
日报跑完会在本地累加一张表(**仓库外**,`~/.weflow-cli/source_topics.json`):每个公众号
|
|
723
|
+
被判断过多少篇、各属于哪一类。跑完会打两行,其中一行点名"已经稳到能判、且正好是你不
|
|
724
|
+
想看的那些类"的来源:
|
|
725
|
+
|
|
726
|
+
```
|
|
727
|
+
来源先验: 今天记了 187 篇,表里共 42 个来源(~/.weflow-cli/source_topics.json)
|
|
728
|
+
来源先验: 这些来源已经稳到能判,且正落在你配的排除主题里(只报数,不跳过):
|
|
729
|
+
某某号 → 新闻(24 篇里 23 篇 = 96%)
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
**它只报数,不跳过任何东西**:跳过是不可逆的,而这张表还在长。等某个来源攒够样本
|
|
733
|
+
(默认 8 篇、单一类占比 ≥80%)再由你决定要不要拿它做拉取前的筛选。计数只增不减——
|
|
734
|
+
它是你事后判断"这号稳不稳"的唯一依据。
|
|
735
|
+
|
|
736
|
+
- 相关度会写进 frontmatter 的 `relevance`(仍是「高/中/低」三个字),并额外写入
|
|
737
|
+
`relevanceScore`(原始分值)与 `topicConfidence`(主题的置信度)。那三档的切点是
|
|
738
|
+
暂定的,原始分留着,将来重新校准时不用重跑历史日报。
|
|
739
|
+
- 同一批问题里还问了一个**「该不该收进今天的日报」**(`includeScore`,0~1)。
|
|
740
|
+
日报的收录门用它,而不是拿相关度顶替——相关度答的是"对读者的实用价值",
|
|
741
|
+
答不了"今天该不该收它"。切点默认 `0.5`,同样暂定;`generate_ai_report.py`
|
|
742
|
+
里那个 `INCLUDE_THRESHOLD` 改起来不用重跑历史数据。想一次收全部:
|
|
743
|
+
`python scripts/generate_ai_report.py --date <日期> --include-all`。
|
|
744
|
+
- `includeScore` 出现之前写下的老文章没有这个字段,会退回旧的
|
|
745
|
+
「相关度 = 高才收」规则,所以**重新生成旧日期的报告不会突然换一批文章**。
|
|
746
|
+
- 单篇分类失败(网络、鉴权、超时)只影响那一篇,会打印一行 WARN 并退回 LLM 解析路径,
|
|
747
|
+
不会让整天的日报中断。
|
|
748
|
+
- 分类要把文章标题与正文发往 `api.typesafe.ai`——和生成摘要发给 DeepSeek 是同一类动作,
|
|
749
|
+
想完全不出网就用 `weflow-cli daily --no-ai`。
|
|
750
|
+
- 排查用 `python scripts/biz_daily.py --date <日期> --classifier llm`(强制老路径)对比。
|
|
751
|
+
- 只要判断、不要生成(连 DeepSeek key 都不需要):`weflow-cli daily --no-summary`。摘要/标签/简报一律不生成,md 里不会有 `## AI 摘要` 段;主题与相关度仍由 Jev 判断。流水线里下游步骤(行动建议/概念编译/AI 报告)仍会用 LLM,要全关再加 `--skip-classify --skip-wiki --skip-ai-report`。
|
|
752
|
+
|
|
252
753
|
启动指定日期阅读器:
|
|
253
754
|
|
|
254
755
|
```powershell
|
|
@@ -263,6 +764,40 @@ weflow-cli daily-server --date YYYY-MM-DD --open
|
|
|
263
764
|
weflow-cli daily-stats --days 30 --limit 30
|
|
264
765
|
```
|
|
265
766
|
|
|
767
|
+
### 让它每天自己跑(日报不会自己发生)
|
|
768
|
+
|
|
769
|
+
**没有任何地方给日报排期,它就不会跑**——这是"日报怎么好几天没更新"最常见的答案。
|
|
770
|
+
先确认流水线本身是好的(只读、不写盘、不调 AI):
|
|
771
|
+
|
|
772
|
+
```powershell
|
|
773
|
+
weflow-cli daily --dry-run --json # 看它读到多少篇、准备处理哪一天
|
|
774
|
+
```
|
|
775
|
+
|
|
776
|
+
正常会打印当天扫到的文章数与一个 `{"success":true,"dryRun":true,...}`。读到几百篇就说明
|
|
777
|
+
读取没问题,缺的只是一个触发。交给计划任务(与「WeFlow Health Check」同一套做法,
|
|
778
|
+
不依赖任何编辑器或会话开着):
|
|
779
|
+
|
|
780
|
+
```powershell
|
|
781
|
+
schtasks /create /tn "WeFlow Daily" ^
|
|
782
|
+
/tr "weflow-cli daily --yes" ^
|
|
783
|
+
/sc daily /st 08:20
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
- 查看:`Get-ScheduledTask -TaskName 'WeFlow Daily'`
|
|
787
|
+
- 手动跑一次:`Start-ScheduledTask -TaskName 'WeFlow Daily'`
|
|
788
|
+
- 上次结果:`schtasks /query /tn "WeFlow Daily" /fo LIST /v`
|
|
789
|
+
|
|
790
|
+
几点要事先知道:
|
|
791
|
+
|
|
792
|
+
- **`--yes` 不能省**:机器模式下没有它就会停在确认门上(这是有意的——日报会调 AI、会写文件)。
|
|
793
|
+
- **会花钱**:默认跑 AI(摘要/标签/简报)。想让它不出网、不花钱就用
|
|
794
|
+
`weflow-cli daily --yes --no-ai`;只要主题与相关度、不要摘要可用 `--no-summary`。
|
|
795
|
+
- **没跑成留下的空目录不等于跑过了**:一个只有目录、没有 `README.md` / `.articles.json` /
|
|
796
|
+
`index.html` 的日期**不算完成**,再跑一次不带日期的 `daily` 会先把它补上(D-011)。
|
|
797
|
+
所以看到 `output/biz-daily/2026-09-22/` 这种空目录时,处理办法就是再跑一次,
|
|
798
|
+
不需要手工删。
|
|
799
|
+
- **别把触发条件设成"上次成功才跑"**:那样第一次失败之后就再也不会被触发了。
|
|
800
|
+
|
|
266
801
|
## 6. AI 和助手
|
|
267
802
|
|
|
268
803
|
日报关闭 AI 不会自动关闭其他命令的 AI。报告、RAG、助手和证据线索分析分别按命令参数和配置决定是否调用模型。云端分析前应确认输入范围、供应商和隐私设置;优先使用本地模型处理聊天正文。
|
|
@@ -276,8 +811,282 @@ weflow-cli assistant start
|
|
|
276
811
|
weflow-cli assistant status
|
|
277
812
|
```
|
|
278
813
|
|
|
814
|
+
### 本机面板:不扫码也能跟助手说话
|
|
815
|
+
|
|
816
|
+
```powershell
|
|
817
|
+
weflow-cli panel # 打开面板窗口(没在跑就先起守护进程)
|
|
818
|
+
weflow-cli panel --status # 只看状态,不开窗口
|
|
819
|
+
weflow-cli panel --ask "我最近在关注什么" --yes # 在命令行里问一句
|
|
820
|
+
```
|
|
821
|
+
|
|
822
|
+
面板与微信里问的是**同一个大脑**:同一份记忆、同一条每日配额、同一条串行队列。窗口只是客户端,
|
|
823
|
+
消息走守护进程开在 `127.0.0.1:8766` 上的入口。**没有登录微信也能用**——消息通道是可选的
|
|
824
|
+
(`assistant start` 在没有 token 时会以"本机入口模式"启动,日志里会写明)。
|
|
825
|
+
|
|
826
|
+
判据看 `assistant status`:它把"配了 token 吗"(`messageChannelLoggedIn`)和"通道真的接上了吗"
|
|
827
|
+
(`channelActive` / `mode`)分开报,另外给出本机入口端口与**记忆桶**。
|
|
828
|
+
|
|
829
|
+
**token 过期这件事现在会说出来。** 服务端用 `errcode: -14` 表示这个 token 已失效,而**重试不可能成功**;
|
|
830
|
+
在这之前本仓查的是别的码,于是它把"过期"当成普通错误、每 5 秒重试一次、永远重试下去——用户那边
|
|
831
|
+
看不出任何异常(`channelActive` 当时只表示"配了 token")。现在:
|
|
832
|
+
|
|
833
|
+
- 日志里会出现**一行明确的**话(只喊一次,不刷屏):`微信通道的 token 已失效……重新登录:weflow-cli login-wechat`;
|
|
834
|
+
- 面板与本机状态里的 `channelActive` 会**如实变 false**(也就是显示"仅本机入口"),而不是继续报"微信 + 本机";
|
|
835
|
+
- 处理办法就是去重新登录,**然后重启助手**(运行中的守护进程手里还是那个旧 token):
|
|
836
|
+
`weflow-cli login-wechat` → `weflow-cli assistant stop` → `assistant start`。
|
|
837
|
+
在此之前**本机面板那条入口照常可用**,助手不是整个不能用。
|
|
838
|
+
- 其余失败(网络抖动等)仍然重试,但改成 1s→30s **指数退避**:原来固定 5 秒一次,长时间断网会把
|
|
839
|
+
日志刷成噪音,而噪音等于没说。通道恢复后计数归零。
|
|
840
|
+
|
|
841
|
+
**记忆桶决定是不是"共用一个大脑"**:事实是按会话 id 分存的,所以面板用的是哪个 id 就等于
|
|
842
|
+
它跟谁共享记忆。白名单里恰好一个人时自动用那个 id;零个或两个以上**不会猜**——它会用独立的
|
|
843
|
+
`panel` 桶并在界面上写明"与微信那边是分开的"。要指定就用:
|
|
844
|
+
|
|
845
|
+
```powershell
|
|
846
|
+
weflow-cli config set assistantPanelUser "o9cq80...@im.wechat" # 改完重启助手
|
|
847
|
+
```
|
|
848
|
+
|
|
849
|
+
**悬浮球 vs 浏览器小窗**:真·悬浮球(无边框、置顶、托盘、`Ctrl+Shift+W`)需要 Electron:
|
|
850
|
+
|
|
851
|
+
```powershell
|
|
852
|
+
npm i -g electron
|
|
853
|
+
```
|
|
854
|
+
|
|
855
|
+
没装 Electron 时会降级用 Edge/Chrome 的 `--app` 打开同一个界面——那是一个没有地址栏的小窗,
|
|
856
|
+
**但给不了悬浮球**,命令输出里会这么说。它用的是独立 profile 目录
|
|
857
|
+
`~/.weflow-cli/panel-browser-profile`(不蹭你日常那个浏览器),删掉它即可清掉那个窗口的状态。
|
|
858
|
+
|
|
859
|
+
**球怎么用**:**点一下**在它旁边展开对话气泡(点球本身或气泡标题栏上的"收起"都能收回去,
|
|
860
|
+
球也可以再点一下——它是个开关)。**球全程不动**,它不消失也不淡出,气泡从它另一侧长出来。
|
|
861
|
+
气泡默认开在球的**左边**(球默认在屏幕右下角,所以往左上长不遮屏);球被拖到屏幕左缘时,
|
|
862
|
+
左边不够地方,气泡会自己翻到右边;上方不够就改成与球顶对齐。**拖动球**换位置(4 像素内算点击、
|
|
863
|
+
超过才算拖),位置记在 `~/.weflow-cli/panel_position.json`,下次打开还在那儿;显示器换了或
|
|
864
|
+
球被拖到屏幕外时会回到右下角。展开后气泡这一整块**不置顶**——去点别的窗口时会到后面去,
|
|
865
|
+
从任务栏点回来即可。
|
|
866
|
+
|
|
867
|
+
**球有表情**(都是 96px 的图,素材在 `resources/panel/`):**按住**它变成眯眼笑的脸,**松手立刻**
|
|
868
|
+
变回来(没有保留期);助手**忙**的时候、**离线**的时候、**配额用完**的时候各有一张脸,空闲时
|
|
869
|
+
不画光晕。这些是"真状态",不是动画效果:状态脸由 `renderer.js` 的 `setBallFace()` 统一设置——
|
|
870
|
+
加新状态时改那一个入口,别在别处各画一份。
|
|
871
|
+
|
|
872
|
+
**关掉窗口 ≠ 停止助手**:窗口没了,守护进程还在跑(微信那边可能还在用)。要停:
|
|
873
|
+
|
|
874
|
+
```powershell
|
|
875
|
+
weflow-cli assistant stop
|
|
876
|
+
```
|
|
877
|
+
|
|
878
|
+
托盘的"退出并停止助手"是同一个动作。
|
|
879
|
+
|
|
880
|
+
排障:
|
|
881
|
+
|
|
882
|
+
| 现象 | 原因与处理 |
|
|
883
|
+
| --- | --- |
|
|
884
|
+
| `panel --status` 报 `PANEL_NOT_RUNNING` | 助手没在跑,或端点文件是崩溃残留(读取端会探活,进程没了就当没有):先 `assistant start` |
|
|
885
|
+
| 日志里 `本机入口启动失败: 端口 8766 已被占用` | 别的程序占了 8766。找出它并停掉,或先停掉重复启动的助手 |
|
|
886
|
+
| 面板显示"连不上本机入口" | 守护进程刚被停掉,或那个端口上的不是助手(端点带 `service` 字段做身份校验) |
|
|
887
|
+
| 面板里"凭据失效了" | 助手重启过,token 每轮都换(这是设计)。重新 `weflow-cli panel` 打开 |
|
|
888
|
+
|
|
889
|
+
手工验证端点(想确认它真的只绑回环、真的认 token 时):
|
|
890
|
+
|
|
891
|
+
```powershell
|
|
892
|
+
# 端口与 token 在 ~/.weflow-cli/assistant_endpoint.json 里;token 不要贴到别处
|
|
893
|
+
curl.exe -i http://127.0.0.1:8766/api/status # 401(没 token)
|
|
894
|
+
curl.exe -i -H "Authorization: Bearer <token>" http://127.0.0.1:8766/api/status # 200
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
**中文别用 Git Bash 的 `curl -d '中文'`**:那条路径会把正文按控制台代码页编出去,
|
|
898
|
+
助手收到的是乱码(实测过)。要手工发就写成文件用 `--data-binary @文件`,
|
|
899
|
+
或者直接用 `panel --ask`。
|
|
900
|
+
|
|
901
|
+
### 助手能读到多少:隐私三档,以及本地引擎这个例外
|
|
902
|
+
|
|
903
|
+
| `assistantPrivacy` | 工具拿到的聊天正文 | 出境 |
|
|
904
|
+
| --- | --- | --- |
|
|
905
|
+
| `strict`(默认) | 换成 `[内容N字已按严格模式屏蔽]` | 不出 |
|
|
906
|
+
| `balanced` | **原文**,但电话/证件/邮箱/密钥/链接被打成 `[电话]` 这类占位 | 出(到当前模型) |
|
|
907
|
+
| `open` | 原文,什么都不打 | 出 |
|
|
908
|
+
|
|
909
|
+
**换本地引擎时严格模式的屏蔽会自动让路**:`aiEngine=ollama` 或 `lmstudio` 时数据不出机器,
|
|
910
|
+
`maskMessageBodyText` 直接返回原文,不需要动 `assistantPrivacy`。所以"我想让它读得到聊天内容,
|
|
911
|
+
但又不想把内容发出去"的正解是**换引擎**,不是降档——降档等于把原文交给第三方。
|
|
912
|
+
|
|
913
|
+
```powershell
|
|
914
|
+
weflow-cli config set assistantPrivacy balanced # 降档(正文会出境,PII 打码)
|
|
915
|
+
weflow-cli config set aiEngine ollama # 或换本地引擎(内容不出机器)
|
|
916
|
+
```
|
|
917
|
+
|
|
918
|
+
在微信里发「**隐私**」可以随时问出当前档位、工具实际拿到的是原文还是被屏蔽、以及该执行哪条命令
|
|
919
|
+
(它只报不改——**不让一条微信消息能改隐私档位**,那等于把隐私开关搬进对话里)。
|
|
920
|
+
|
|
921
|
+
**改完必须重启助手**:配置是**启动时**读进进程内存的(`configService.get` 不回读磁盘),
|
|
922
|
+
所以对一个正在跑的助手,外部 `config set` 不生效——`assistant stop` 再 `assistant start`。
|
|
923
|
+
|
|
279
924
|
助手默认拒绝所有发送者,需明确设置 `assistantWhitelist`。群聊还需要群白名单、成员白名单和 @ 门槛;项目不通过客户端自动化或非官方协议拉群。
|
|
280
925
|
|
|
926
|
+
**第一次启用**:扫码一次,剩下的它会告诉你。
|
|
927
|
+
|
|
928
|
+
1. `weflow-cli login-wechat` —— 扫码登录消息通道(这一步只能人来做:iLink 只有扫码这一条官方登录路);
|
|
929
|
+
2. `weflow-cli assistant start`;
|
|
930
|
+
3. 从你的微信给机器人发一条消息。**助手会拒绝它**(白名单为空 = 拒绝所有人),但日志里会出现一行
|
|
931
|
+
`[首次配置]`,带着**你自己的发送者 ID** 和该执行的那条 `config set assistantWhitelist` ——
|
|
932
|
+
照抄执行即可。之后它就开始回话了。
|
|
933
|
+
|
|
934
|
+
为什么不让登录自己把白名单写好:登录响应给的是 `ilink_user_id`,而白名单要的是入站消息里的
|
|
935
|
+
`from_user_id`(文档里写成 `@im.wechat ID`),**这两者是不是同一个值这个仓库里没有任何东西验证过**。
|
|
936
|
+
猜错的后果是"白名单非空、看着配好了、却仍然拒你",而且提示也不会再出现(白名单已经不空了)——
|
|
937
|
+
所以这里宁可多一次复制粘贴。真值在第 3 步那条消息里,它自己会来。
|
|
938
|
+
|
|
939
|
+
想让助手先"只看不答"式地跑一段(记下它本来会怎么走,行为不改):`config set assistantFastRoute log`。
|
|
940
|
+
|
|
941
|
+
**启动前先确认两件事**,否则 `assistant start` 会如实地告诉你它起不来:
|
|
942
|
+
|
|
943
|
+
1. **消息通道要已登录**(`weflow-cli login-wechat`)。没登录时子进程会退出,启动命令会把
|
|
944
|
+
退出码和日志尾部原样报出来:`子进程启动后立即退出 (code 1);日志尾部: Error: 未登录消息通道…`。
|
|
945
|
+
这句话出现在终端里就说明机制是对的,**问题在通道,不在守护进程**。
|
|
946
|
+
2. **改了源码要先 `npm run build`**。守护进程优先运行 `dist/bin/weflow-cli.js`,只要这个文件
|
|
947
|
+
存在就不会用 `bin/weflow-cli.ts`;不重建,守护进程跑的还是旧的编译产物。
|
|
948
|
+
|
|
949
|
+
助手还有一个**默认关闭**的单轮快路径(D-035):先用本机判断层问一次"这条要不要查本机数据、
|
|
950
|
+
查哪一项",把那个工具先跑掉,于是模型第一轮就看得到结果(两次往返变一次)。
|
|
951
|
+
|
|
952
|
+
```powershell
|
|
953
|
+
weflow-cli config set assistantFastRoute log # 灰度:只记"本来会走哪条",行为一个字不改
|
|
954
|
+
weflow-cli config set assistantFastRoute on # 打开
|
|
955
|
+
weflow-cli config set assistantFastRoute off # 关(默认)
|
|
956
|
+
```
|
|
957
|
+
|
|
958
|
+
还有一个**工具使用守卫**:如果路由判断"这条消息要查本机数据",而这一轮**一次工具都没调**,它会把矛盾顶回去一次(提示模型"你手上没有工具结果"),让模型重答。审计里对应 `TOOL_GUARD_PUSHBACK`。
|
|
959
|
+
注意它与 `log` 的关系:`log` 的承诺是"**路由**不改变行为",守卫不跟着一起关——它是安全行为,而观测期正是最该有它的时候(`off` 完全不介入,那时不问路由,也就没有触发的信号)。
|
|
960
|
+
|
|
961
|
+
灰度期看两个地方:`assistant log` 里的 `[快路径/只记] 路由到 X(...)`,以及
|
|
962
|
+
`~/.weflow-cli/assistant_audit.log` 里的 `FASTROUTE_WOULD` / `FASTROUTE_SKIP`(回退时那行会写
|
|
963
|
+
明原因:不需要查本机数据、置信度不足、能力名不认识…)。只有参数是固定集合的工具会被路由;
|
|
964
|
+
像"总结一下我和某某的聊天"这种要点名某个人的,一律回退到原来的循环——这是刻意的,硬凑参数
|
|
965
|
+
会让模型拿着不相关的结果自信作答。
|
|
966
|
+
|
|
967
|
+
启动失败时**不会**留下 pid 文件 —— 写 pid 就等于对外宣称它在运行。排查用
|
|
968
|
+
`weflow-cli assistant status`(不碰数据库)与 `weflow-cli assistant log`。
|
|
969
|
+
|
|
970
|
+
**没配 key 的工具不会摆给模型**:语义检索要 `dashscopeApiKey`、微信读书要 `wereadApiKey`,
|
|
971
|
+
缺了就不出现在工具表里——摆一个跑不了的工具,模型会去试、拿一个错回来(实测出现过答复里带着
|
|
972
|
+
"两个检索工具都跑不通")。这只挡"跑不了",不挡"暂时没数据":后者工具自己会说该运行什么。
|
|
973
|
+
|
|
974
|
+
**副作用要知道**:工具被藏起来之后,模型**看不见它存在**,所以问"你在读什么书"时它只会答
|
|
975
|
+
"我没有这个工具"。2026-09-28 实测就是这么一轮对话——用户以为功能没做,其实做了、只是没配 key。
|
|
976
|
+
所以缺 key 时它给的那句"没有"要按"没配",不是"没有这功能"来读。
|
|
977
|
+
|
|
978
|
+
### 微信读书的 key 从哪来
|
|
979
|
+
|
|
980
|
+
登录 **https://weread.qq.com/r/weread-skills**,点「快速配置」,页面上会给出你的 `api-key`
|
|
981
|
+
(形如 `wrk-…`)。它调的是官方网关 `https://i.weread.qq.com/api/agent/gateway`。
|
|
982
|
+
|
|
983
|
+
拿到之后两种存法**都认**:
|
|
984
|
+
|
|
985
|
+
```powershell
|
|
986
|
+
weflow-cli config set-env wereadApiKey WEREAD_API_KEY # 从环境变量读,秘密不进命令参数
|
|
987
|
+
weflow-cli config set wereadApiKey wrk-xxxx # 或者直接给值
|
|
988
|
+
```
|
|
989
|
+
|
|
990
|
+
- 本机的配置项是**密文落盘**(`lock:` 开头),与其它密钥一样;
|
|
991
|
+
- 环境变量 `WEREAD_API_KEY` **优先于**配置项——官方文档建议不要把 key 直接交给 AI;
|
|
992
|
+
- 两条路都认是有原因的:助手的 `get_weread` 判据读**配置项**,而 `weflow-cli weread …` 早先
|
|
993
|
+
只读**环境变量**。2026-09-28 之前只认一个,用户按 CLI 的提示配好配置项,助手能用了、
|
|
994
|
+
CLI 却回"未设置"。
|
|
995
|
+
|
|
996
|
+
**改了配置要重启守护进程**:`configService` 在构造时读一次盘,跑着的助手不会自己发现新 key。
|
|
997
|
+
|
|
998
|
+
```powershell
|
|
999
|
+
weflow-cli assistant stop
|
|
1000
|
+
weflow-cli assistant start
|
|
1001
|
+
```
|
|
1002
|
+
|
|
1003
|
+
### 看助手"刚才怎么想的"(决策轨迹)
|
|
1004
|
+
|
|
1005
|
+
助手对外只吐一句答复,中间的判断过程默认看不见。要看得两条路:
|
|
1006
|
+
|
|
1007
|
+
```powershell
|
|
1008
|
+
weflow-cli assistant trace -n 3 # 本机看最近三轮
|
|
1009
|
+
weflow-cli assistant trace --json # 结构化的,给脚本用
|
|
1010
|
+
```
|
|
1011
|
+
|
|
1012
|
+
微信里发 **`轨迹`** 则显示**上一轮**的步骤(一条消息能装下的那种短版本)。
|
|
1013
|
+
|
|
1014
|
+
一轮长这样:
|
|
1015
|
+
|
|
1016
|
+
```
|
|
1017
|
+
2026-09-23 12:23:49 1775ms 往返 2 次 · 工具 1 次 · 结束于「answered」
|
|
1018
|
+
· 判断层: 判断层认为不需要工具 (none)
|
|
1019
|
+
· 工具 who_owes_reply(days=14) → 93 字节
|
|
1020
|
+
· 模型往返: 第 2 轮往返后给出答复
|
|
1021
|
+
· 推理: 无(当前模型不返回 reasoning_content)
|
|
1022
|
+
```
|
|
1023
|
+
|
|
1024
|
+
逐项含义:
|
|
1025
|
+
|
|
1026
|
+
- **判断层**:快路径那一问的结论(只在 `assistantFastRoute` 开着时才有;`log` 模式会标"只记,未派发")
|
|
1027
|
+
- **工具(name=args) → N 字节**:调了哪个、**带了什么参数**、结果多大。后面标「(无内容)」表示这次没给出东西("没找到"与"失败了"都算——判据是返回值以 `(` 起头,见 D-043)
|
|
1028
|
+
- **往返 N 次**:模型来回几次。一次问句用掉 5 次以上值得看一眼
|
|
1029
|
+
- **结束于**:`answered` / `rounds-exhausted`(撞上 6 轮上限)/ `llm-error` / `builtin`(内置指令)
|
|
1030
|
+
- **推理**:模型**真返回**的思维链。默认的 `deepseek-chat` 不返回,所以这里是"无"——**空就是空**。
|
|
1031
|
+
换成会返回 `reasoning_content` 的模型(如 `deepseek-reasoner`)之后这里有内容:
|
|
1032
|
+
`weflow-cli config set aiModel deepseek-reasoner`(注意这类模型更慢也更贵)
|
|
1033
|
+
|
|
1034
|
+
**与审计的分工**:审计(`assistant_audit.log`)只记事件与字节量、**绝不记内容**,是出境记录;
|
|
1035
|
+
轨迹(`assistant_trace.jsonl`)记的是"怎么走的",**包含工具参数摘要**,所以它是本地排查用的。
|
|
1036
|
+
参数摘要过一遍脱敏、每个值截到 40 字;微信里那条 `轨迹` 不打印发送者 ID。
|
|
1037
|
+
|
|
1038
|
+
轨迹文件上限 512KB,超了裁到最近 200 轮;它是排查用的,不做统计聚合。
|
|
1039
|
+
|
|
1040
|
+
### 助手的行为评测(要联网、要花钱,按需跑)
|
|
1041
|
+
|
|
1042
|
+
助手此前没有任何质量反馈:单元测试全是注入假模型的(证明代码路径没坏,证明不了"模型拿着
|
|
1043
|
+
这 16 个工具会怎么用"),唯一的其它办法是去微信里聊两句、靠运气发现问题。
|
|
1044
|
+
|
|
1045
|
+
```powershell
|
|
1046
|
+
npm run eval:assistant # 18 条用例,跑真实模型(含一条多轮)
|
|
1047
|
+
npm run eval:assistant -- --only=no-tool # 只跑一条
|
|
1048
|
+
npm run eval:assistant -- --json # 附机器可读报告
|
|
1049
|
+
```
|
|
1050
|
+
|
|
1051
|
+
它断言的是**底线有没有被越过**,不是"答得多好":
|
|
1052
|
+
|
|
1053
|
+
- 该调的工具调了吗(问"谁在等我回话"有没有真的去查)、**不该调的呢**(翻译一句话时别乱伸手)
|
|
1054
|
+
- 工具调用次数有没有失控(实测见过一次问句用了 5 次调用)
|
|
1055
|
+
- **工具挂了会不会被说成"没有数据"** —— 这条最有价值:脚本报退出码 2 时,助手会引用报错
|
|
1056
|
+
原文、指出可能是密钥没解锁、再给一条替代路径,而不是回一句"没人在等你"
|
|
1057
|
+
- 不存在的联系人会不会被编出内容
|
|
1058
|
+
- 记忆是**按结果**判的(那件事实进没进长期记忆),不是按"有没有调 save_memory"
|
|
1059
|
+
|
|
1060
|
+
三条边界,用之前要知道:
|
|
1061
|
+
|
|
1062
|
+
1. **期望值是我写的,不是人工标注的金标准**。所以它测底线,不测水平;引用它的结论要连着
|
|
1063
|
+
这句话一起引用。
|
|
1064
|
+
2. **首轮全绿不算数**:用例与行为是同一个作者写的,容易只覆盖自己想得到的路径。它的价值
|
|
1065
|
+
在**改动之后再跑**——比如动了系统提示、工具描述、记忆注入,看有没有哪条从通过变失败。
|
|
1066
|
+
3. **它有假失败的风险,而且这是写这套东西最难的部分。** 另有一条结构性对策:
|
|
1067
|
+
**底线是硬的,期待是软的**——判据只有一句:**这个数字/期待跟着模型走法或措辞浮动吗?**
|
|
1068
|
+
浮动的一律进软通道(`toolBudget`、`answerShouldMatch`),只打一句 ⚠、永不判失败。
|
|
1069
|
+
`maxTools` 只拦"真的失控"(同一句话的调用次数实测能差三倍),唯一的硬例外是 `no-tool`
|
|
1070
|
+
的 0:不需要工具时调了工具,是实打实的缺陷。 加用例时踩过的四个坑,一个比一个阴:
|
|
1071
|
+
- **别枚举模型的措辞**:我用"查不到"去卡模型说的"查不了",一条答得很好的用例连着两次报失败。
|
|
1072
|
+
- **匹配词干,不匹配整词**:我写"抓不了",模型说"抓不下来"——词表再长也追不上措辞。
|
|
1073
|
+
- **别写禁用词**:模型说"这是本地环境的问题,**不是**没人在等你",而我的禁用词
|
|
1074
|
+
`/没人在等/` 把它判成了"没人在等"——正则分不出断言与否认。
|
|
1075
|
+
- **上调上限前先问"这是乱试吗"**:一条要解析歧义名字、撞上"匹配多个"、再找文档的用例,
|
|
1076
|
+
4 次调用是合理探查,而原来随手写的上限 3 把正确答案判成了失败。
|
|
1077
|
+
- **空夹具会招模型重试**:桩返回空结果时,它会换着关键词反复问(实测 7 次,每次参数都
|
|
1078
|
+
不同,所以"重复调用去重"拦不住)。那是夹具的产物、不是缺陷——给桩一个真实命中,
|
|
1079
|
+
或者把次数放进软预算。
|
|
1080
|
+
- **禁用词会打在真话上**(同一个坑踩了两次):模型回答"查不到某人"时会顺带说"你最近的
|
|
1081
|
+
会话里只有甲乙(甲说项目文档改好了)"——那是如实说明**确实有谁**,而禁用词 `文档` 把它
|
|
1082
|
+
判成了编造。**正面断言(有没有说出"没找到")足够,反面断言只会误伤。**
|
|
1083
|
+
**判定逻辑本身有离线测试**(`test/assistant-eval.test.ts`),所以"一条用例过不过"这件事
|
|
1084
|
+
是被测过的;但"这条断言值不值得写"只能靠人看。
|
|
1085
|
+
|
|
1086
|
+
跑的时候数据全在**临时家目录**里,真实的审计与记忆不受影响(更要紧的是:边写同一个记忆文件
|
|
1087
|
+
会把守护进程刚写的状态覆盖掉)。用的 key 是你配置里那一个(`lock:` 密文,只在本机解),
|
|
1088
|
+
临时配置里只放这一个字段。
|
|
1089
|
+
|
|
281
1090
|
## 7. 常见问题
|
|
282
1091
|
|
|
283
1092
|
| 现象 | 处理 |
|