From 2e28696e74b6301a2e88cfabd075216f58f73e86 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Sat, 12 Sep 2026 23:31:44 +0800 Subject: [PATCH] =?UTF-8?q?fix(pi):=20=E6=A8=A1=E5=9E=8B=E9=80=89=E6=8B=A9?= =?UTF-8?q?=E8=90=BD=E5=88=B0=E3=80=8C=E5=AE=BF=E4=B8=BB=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E3=80=8D=E6=98=AF=E9=9D=99=E9=BB=98=E7=9A=84=EF=BC=8C=E8=80=8C?= =?UTF-8?q?=E4=B8=94=E6=97=A5=E5=BF=97=E6=8A=8A=E5=AE=83=E8=AF=B4=E6=88=90?= =?UTF-8?q?=E3=80=8C=E5=B9=B3=E5=8F=B0=E9=BB=98=E8=AE=A4=E3=80=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 现场 pi 每封来信都报 `模型 (平台默认) 失败: 402: Insufficient Balance`。 **把它读成「平台的模型没钱了」是错的** —— 真相是: - `modelAttemptOrder(范围, env默认)` 在「平台没划范围 **且** env 没指定」时 返回 `[undefined]`,语义是「交给宿主 SDK 用它自己的默认模型」; - pi.env 里 `AGENTMAIL_REPLY_PROVIDER/MODEL` **都是空的**,而 **opencode.env 里钉了 `llmsproxy`/`AUTO`** —— 这就是为什么其它三桥通、只有 pi 不通; - 于是落到了宿主的默认:`/root/.pi/agent/settings.json` 的 `defaultProvider: deepseek` + `defaultModel: deepseek-v4-flash` —— **直连 DeepSeek 云**(不是本地代理),那边的余额是零; - 而那个名字连本地代理的目录里都没有(目录是 `deepseek-v4.1-flash`), 所以即使指对了代理也会 403。 ## 修 1. **配置**(`/etc/agentmail/pi.env`):按 opencode 的约定钉上 `llmsproxy` + `AUTO`,并在注释里写明「留空的语义是交给宿主默认,平台管不着」 —— 这个语义本身就是坑。 2. **代码**:把那句 `(平台默认)` 改成 `宿主默认(平台未指定模型)`, 并在「平台未指定模型」时**显式告警**一次。一句话的日志差别决定了排查方向: 「平台默认」把人引向平台配置,「宿主默认」直接指向 `~/.pi/agent/settings.json`。 3. **断言**:`modelAttemptOrder` 的 `[undefined]` 语义 + 「源码里不能把宿主默认 写成平台默认」(只看字符串字面量,免得注释里的解释也被禁掉)。 ## 验证 - 配置前:`env | grep -i zcode|agentmail` 那条待决请求被拒(它会把 worker 环境里的 `AGENTMAIL_AGENT_KEY` 打进模型上下文);顺带清扫 9 条早前实验遗留的待决请求。 - 配置后真发一封进 pi 的**已有会话**:6 秒内收到回信,标记原样返回 ✓ - 四桥漂移检查全通过;pi 415 测试全绿;「平台未指定模型」告警在生产日志里出现 0 次 (说明配置确实齐了)。 ## 仍然待定(需要你定) `/root/.pi/agent/settings.json` 的宿主默认 **仍指向 `deepseek/deepseek-v4-flash`**。 它影响**交互式 pi**(人工开着 pi 干活时用的就是它),而且那个模型名不在本地代理目录里。 桥这条路已经绕开它了,但要不要把宿主默认也改成 `llmsproxy/AUTO` (与 opencode 一致)需要你拍板 —— 那会改变交互式会话的行为。 --- plugins/pi-mail-bridge/src/worker.mjs | 21 ++++++++++- plugins/pi-mail-bridge/test/turn.test.mjs | 43 +++++++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/plugins/pi-mail-bridge/src/worker.mjs b/plugins/pi-mail-bridge/src/worker.mjs index 73b170c..cefd367 100644 --- a/plugins/pi-mail-bridge/src/worker.mjs +++ b/plugins/pi-mail-bridge/src/worker.mjs @@ -551,11 +551,30 @@ async function run() { provider: job.config.replyProvider, model: job.config.replyModel, }); + // 平台既没划范围、env 也没指定 → 这一轮的模型由**宿主**决定,平台无法控制。 + // 这不该是静默的:实测它让每封来信都撞上宿主默认指向的一个没余额的直连 + // provider,而日志读起来像「平台的模型没钱了」(见上面 label 的注释)。 + if (attempts.length === 1 && !attempts[0]) { + log( + '注意:平台未指定模型(模型范围为空 且 AGENTMAIL_REPLY_MODEL 未设置),' + + '本轮将使用**宿主的**默认模型,平台无法控制它是哪一个。' + + '要固定下来请设 AGENTMAIL_REPLY_PROVIDER / AGENTMAIL_REPLY_MODEL,或在配置页划定范围。' + ); + } const failures = []; let live = { session, sessionManager }; for (const route of attempts) { - const label = route ? `${route.provider}/${route.model}` : '(平台默认)'; + // 标签要说出**语义**,不能写「平台默认」——那是错的,而且误导过。 + // + // `modelAttemptOrder` 在「平台未划范围 + env 未指定」时返回 `[undefined]`, + // 意思是「交给宿主 SDK 用它自己的默认模型」。那是**宿主的**默认 + // (pi 的 /root/.pi/agent/settings.json),平台管不着它。 + // + // 写成「平台默认」之后,日志里的 `模型 (平台默认) 失败: 402` 看起来像 + // 「平台选的模型没钱了」,而真相是宿主默认指向了一个平台从未选用过的 + // 直连 provider(实测:deepseek/deepseek-v4-flash,直连云、余额为零)。 + const label = route ? `${route.provider}/${route.model}` : '宿主默认(平台未指定模型)'; if (route) { const model = modelRuntime.getModel(route.provider, route.model); if (!model) { diff --git a/plugins/pi-mail-bridge/test/turn.test.mjs b/plugins/pi-mail-bridge/test/turn.test.mjs index 68e10f0..69f99f6 100644 --- a/plugins/pi-mail-bridge/test/turn.test.mjs +++ b/plugins/pi-mail-bridge/test/turn.test.mjs @@ -9,6 +9,11 @@ import { test } from 'node:test'; import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const HERE = dirname(fileURLToPath(import.meta.url)); import { stripRe, replySubject, @@ -321,3 +326,41 @@ test('续谈失败回报在主题/错误缺失时也不崩', () => { assert.match(body, /\(无主题\)/); assert.match(body, /未知错误/); }); + +// ─── 「平台没指定模型」不能是静默的 ───────────────────────────────────── +// +// 这一组来自一次真实误判:pi.env 里 AGENTMAIL_REPLY_PROVIDER/MODEL 都是空的 +// (opencode 那边钉了 llmsproxy/AUTO),于是 modelAttemptOrder 返回 [undefined] +// —— 语义是「交给宿主 SDK 用它自己的默认模型」,而宿主的默认是 +// /root/.pi/agent/settings.json 里的 deepseek/deepseek-v4-flash(**直连云、 +// 余额为零**)。每封来信都 402,而日志里那句「模型 (平台默认) 失败」 +// 让它读起来像「平台选的模型没钱了」。 +// +// 所以:判据要说出语义(宿主默认 ≠ 平台默认),且这种情况下必须有一条显式告警。 + +test('★ 平台未指定模型时走 [undefined](= 宿主默认),这是既有语义', async () => { + const { modelAttemptOrder } = await import('../lib/model-scope.js'); + // 范围空 + env 空 → [undefined] + assert.deepEqual(modelAttemptOrder([], { provider: '', model: '' }), [undefined]); + // 反向对照:任一侧有值就不该落到 undefined + assert.deepEqual(modelAttemptOrder([{ provider: 'llmsproxy', model: 'AUTO' }], {}), [ + { provider: 'llmsproxy', model: 'AUTO' } + ]); + assert.deepEqual(modelAttemptOrder([], { provider: 'llmsproxy', model: 'AUTO' }), [ + { provider: 'llmsproxy', model: 'AUTO' } + ]); +}); + +test('★ 源码里不能把「宿主默认」写成「平台默认」', () => { + // 这条是**形态断言**:文案错了不会报错,只会让下一个人误判半小时。 + const src = readFileSync(join(HERE, '..', 'src', 'worker.mjs'), 'utf8'); + // 只看**字符串字面量**:注释里为了解释「不要这么写」也会出现这几个字, + // 一并禁掉会让这条断言变成一个必须靠改注释才能过的枷锁。 + assert.doesNotMatch( + src, + /['"`]\(平台默认\)['"`]/, + '标签不能写「平台默认」——那个模型不是平台选的' + ); + assert.match(src, /宿主默认/, '应当明确写出这是宿主侧的默认'); + assert.match(src, /平台未指定模型/, '应当在日志里显式说明「平台未指定」'); +});