From a020247627fd7172a39b6b612013b5a3d3c2c08a Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Wed, 14 Oct 2026 11:36:15 +0800 Subject: [PATCH] =?UTF-8?q?docs(channels):=20=E8=AE=B0=E5=BD=95=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E4=BA=8B=E6=95=85=E5=85=A8=E9=93=BE=EF=BC=88=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E8=A6=86=E7=9B=96=E7=94=9F=E4=BA=A7=E8=B4=A6=E5=8F=B7?= =?UTF-8?q?=E6=B1=A0=EF=BC=89+=20=E4=BF=AE=E5=A4=8D=E4=B8=AD=E7=9A=84?= =?UTF-8?q?=E8=87=AA=E8=B8=A9=E5=9D=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 含根因实测输出、为什么 DisableStatePersistence 不够(调度器 goroutine 比 t.Setenv 生命周期长)、两层修法、判据与变异验证。 另记一条我自己的操作事故:恢复账号时把保险库里的**密文**当明文喂给 account/add,而 vault 读取时会解密 → 凭据被二次加密(1971→2965 字符), 该账号推理开始返回空。教训:凭据只能由登录/续期路径写入。 --- docs/channels-parity.md | 70 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/docs/channels-parity.md b/docs/channels-parity.md index f44fe73..9b265c9 100644 --- a/docs/channels-parity.md +++ b/docs/channels-parity.md @@ -372,3 +372,73 @@ flow 类型,不是"登录成功"。 **修正判断**:B 类不是"登录闭环的拦路虎"。zcode 的登录(`/api/v1/oauth/cli/init` → poll)不需要验证码;captcha 只在**自动领取**时出现。所以 B 类的真实价值是 "让领取全自动",而不是"让平台可用"。 + +--- + +# 真实数据事故:测试覆盖了生产账号池(2026-10-14) + +## 症状 + +用户报「完全不可用」。逐平台 `account/list` 后发现:**13 个账号里只有 1 个有凭据**, +其余是占位壳子;再查发现**账号池被清空了**(`pool` 全为空 `{}`), +而保险库 `.secrets/channels.json` 里的凭据**还在**。 +症状正是「账号凭空消失」。 + +## 根因(实测输出,非推断) + +channels 插件用 `storeDir()` 决定存储位置,它有多级兜底: + +``` +env LLMSPROXY_PLUGIN_DIR → settings.store_dir → 读 /etc/llmsproxy/config.yaml 的 plugin_dir → 硬编码 +``` + +测试台没设 `LLMSPROXY_PLUGIN_DIR`,**后两级直接命中生产**: + +``` +测试插件目录 = /tmp/TestXXX/002/plugins +storeDir 结果 = /etc/llmsproxy/plugins ← 探针实测输出 +``` + +于是测试加载真 `channels.lua` 时**读的是生产存储、写的也是生产存储**。 +一次全量 `go test ./internal/lua` 就把 +`.channels-store.json` 从 1730 覆盖成 1986 字节、账号池清空(inode 都换了)。 + +只有 `internal/lua` 复现,`internal/core` 不复现 —— 因为 core 的测试台 +(`kernelTestCore`)设了 `LLMSPROXY_PLUGIN_DIR`。**这是隔离做得不一致**的代价。 + +同类前科:曾把 26 个测试账号写进生产 store。两次同一类错误。 + +## 为什么 `DisableStatePersistence` 不够 + +第一版修复给 `runtimePlugins` 关了持久化,**仍然复现**:插件声明了 `schedule` +时内核会自动起调度器(`plugins.go: p.Runtime = ps.startRuntime(...)`), +真 channels 的 `refresh_models` 会在 5s 后(first_due_now)跑一轮并 +`logEvent → markDirty`。那是**后台 goroutine,比 `t.Setenv` 的生命周期长** —— +测试结束后它仍可能落盘,此时环境变量已还原。 + +## 修法(两层,第二层才是根治) + +1. **内核把插件的真实目录交给插件**(ground truth,不再靠猜): + 新增 `__llmsproxy_plugin_dir` 全局(`registerPluginDirBridge`), + `storeDir()` 优先用它。**即使没有环境变量也不会落到生产路径** —— + 实测:撤掉 env 后 `storeDir` 仍落在临时目录。 +2. 测试台补隔离:`runtimePlugins` 设 env + `DisableStatePersistence()`。 + +## 判据 + 变异验证 + +`internal/lua/store_dir_isolation_test.go`: +`storeDir` 绝不能是 `/etc/llmsproxy` 前缀、必须落在临时目录、env 必须真的被设。 +**变异验证**:把隔离设成生产路径 → 判据变红并打印「storeDir 指向了**生产**目录」; +恢复 → 变绿。 + +全量回归:9 个包全绿,且 `go test ./...` 后生产存储**未被改动**(此前必被改)。 + +## ⚠️ 修复过程中我自己踩的坑 + +恢复账号时我把**保险库文件里的密文**当成明文,喂给 `account/add` 的 +`credential` —— 保险库 `loadSecretsLocked` **会在读取时解密**,于是那份凭据被 +**二次加密**(1971 → 2965 字符),该账号的推理开始返回空 +(`codebuddy_*` 源失效,而非池路径 `channels:buddy` 正常)。 + +教训:**凭据只能由登录/续期路径写入,绝不要手工把密文塞回 API**。 +要复用既有凭据,应走「让插件自己读保险库」的路径,或者重新登录一次。