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, /平台未指定模型/, '应当在日志里显式说明「平台未指定」'); +});