fix(deploy): 失败通知不再把所有失败都说成「Gateway 不可达」

# 误导的来源

`reportFailure` 只在 `result.sent` 上分叉,而 sent 为假同时覆盖「fetch 抛」「HTTP 4xx/5xx」,
于是两种情况都打印同一句话。

# 实测代价

切换插件时 dsh 进了崩溃循环(我自己的部署缺陷所致),通知脚本连打六条
「Gateway 不可达」并写进 spool —— 而网关**一直在正常服务**(NRestarts=0),
日志里真实响应是 **HTTP 403**:

    POST http://127.0.0.1:8180/api/v1/mail/send ... - 403 245B

403 的原因是同一会话连续中继邮件撞上防「两个 Agent 互相唤醒」的跳数上限
(`maxRelayHops=5`):告警邮件的会话别名由主题派生,崩溃循环下六封落进同一条会话。

一句话把人送去查网络,而问题在策略层 —— **故障通知本身给出误导性诊断,
是「静默失败」的另一种形态**。

# 修法

新增 `describeSendError`:`Gateway HTTP <status>: <body>` 归类为「网关可达,但返回
HTTP xxx(附响应体)」;只有 fetch 本身失败 / AbortError 才说「不可达」/「超时」。

双向验证(真实走代码路径,都不实际发信):
  网关指向关闭端口     → 「网关不可达:fetch failed」
  真网关 + 坏密钥      → 「网关可达,但返回 HTTP 401:{"error":"密钥无效"}」
This commit is contained in:
2026-09-12 11:52:52 +08:00
parent cbe5ef5cec
commit c9209e4adc

View File

@ -213,6 +213,26 @@ export async function flushSpool(env = process.env) {
return { total: names.length, sent, failed }; return { total: names.length, sent, failed };
} }
/**
* 把发送失败翻译成一句能定位原因的话。
*
* `postPayload` 对非 2xx 抛的是 `Gateway HTTP <status>: <body>` —— 里面已经带着
* 状态码与响应体,直接打出来就够定位了。只有 fetch 本身抛DNS/连接/超时)才真的是
* 「不可达」,此时按名字区分开。
*/
function describeSendError(error) {
const message = String(error?.message || error || '');
const httpMatch = message.match(/^Gateway HTTP (\d{3}): ([\s\S]*)$/);
if (httpMatch) {
return `网关可达,但返回 HTTP ${httpMatch[1]}${httpMatch[2].slice(0, 300)}`;
}
if (error?.name === 'AbortError') return '请求超时(网关未在限定时间内响应)';
if (/fetch failed|ECONNREFUSED|ENOTFOUND|EHOSTUNREACH/i.test(message)) {
return `网关不可达:${message}`;
}
return message || '未知错误';
}
async function main(argv = process.argv.slice(2), env = process.env) { async function main(argv = process.argv.slice(2), env = process.env) {
const serviceName = argValue(argv, '--service'); const serviceName = argValue(argv, '--service');
if (argv.includes('--flush')) { if (argv.includes('--flush')) {
@ -234,9 +254,25 @@ async function main(argv = process.argv.slice(2), env = process.env) {
env, env,
serviceName, serviceName,
}); });
console.error(result.sent if (result.sent) {
? `[agentmail-failure-notify] ${serviceName} 异常已上报` console.error(`[agentmail-failure-notify] ${serviceName} 异常已上报`);
: `[agentmail-failure-notify] Gateway 不可达,报告已暂存 ${result.path}`); return;
}
/*
* 区分「连不上」与「连上了但被拒」。
*
* 原先两种情况都打印「Gateway 不可达」。实测代价:切换插件时 dsh 进了崩溃循环,
* 通知脚本连打六条「Gateway 不可达」并写进 spool —— 而网关**一直在正常服务**
* NRestarts=0真实响应是 **HTTP 403**(同一会话连续中继邮件撞上防互相唤醒
* 的跳数上限,`maxRelayHops=5`)。
*
* 那句话会把人送去查网络,而问题在策略层。故障通知本身给出误导性诊断,
* 是「静默失败」的另一种形态。
*/
console.error(
`[agentmail-failure-notify] ${serviceName} 异常上报失败,报告已暂存 ${result.path}`
);
console.error(`[agentmail-failure-notify] 原因:${describeSendError(result.error)}`);
} }
const isCLI = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url); const isCLI = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);