From c062d1cc8b4f1b6302892eec9ac142e674e6e96f Mon Sep 17 00:00:00 2001 From: jkooe <62494645+jkooe@users.noreply.github.com> Date: Tue, 27 Oct 2026 13:06:51 +0800 Subject: [PATCH] =?UTF-8?q?fix(agent):=20ETIMEDOUT=20=E7=BB=99=E5=87=BA?= =?UTF-8?q?=E3=80=8C=E9=85=8D=E4=BB=A3=E7=90=86=E3=80=8D=E6=8F=90=E7=A4=BA?= =?UTF-8?q?=20=E2=80=94=E2=80=94=20=E5=A2=83=E5=86=85=E7=9B=B4=E8=BF=9E?= =?UTF-8?q?=E6=9C=80=E9=AB=98=E9=A2=91=E5=A4=B1=E8=B4=A5=E5=BD=A2=E6=80=81?= =?UTF-8?q?=E6=97=A0=E6=8C=87=E5=BC=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 真机(Windows 192.168.0.160)验证自更新出网时发现: `connect ETIMEDOUT 20.205.243.166:443` 落到 describeNetError 的兜底分支, 只回一串 IP:端口超时 —— 而这正是境内直连 GitHub 最常见的形态,用户唯一 有效动作就是配代理,却完全没被告知。 修法:新增 ETIMEDOUT / EHOSTUNREACH / ENETUNREACH 分支,点明 「设置 NA_PROXY=<代理地址> 或配置系统代理后重试」。与 ECONNRESET 的 「代理或线路问题」保持区分(两类原因不同,单测钉死互不串味)。 真机复验:重新部署后同一命令已输出新提示。 --- apps/agent/src/capabilities/selfupdate.ts | 6 ++++++ tests/unit/selfupdate-backup-cleanup.test.mjs | 20 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/apps/agent/src/capabilities/selfupdate.ts b/apps/agent/src/capabilities/selfupdate.ts index 4162504..8ce3fc9 100644 --- a/apps/agent/src/capabilities/selfupdate.ts +++ b/apps/agent/src/capabilities/selfupdate.ts @@ -102,6 +102,12 @@ export function describeNetError(err: unknown, timeoutMs: number): string { if (code === 'ENOTFOUND' || code === 'EAI_AGAIN' || code === 'ECONNREFUSED') { return `${msg}(域名解析失败或目标拒绝连接;若配了代理,请核对代理地址与端口)`; } + // 2026-10-27 真机补:境内直连 GitHub 的**最常见**失败形态就是 TCP 层 ETIMEDOUT + // (不是我们自己的 timeoutMs,也不是 ECONNRESET)。原先它落到下面兜底分支, + // 用户只看到一串 IP:443 超时,不知道该干什么 —— 而这时唯一有效的动作就是配代理。 + if (code === 'ETIMEDOUT' || code === 'EHOSTUNREACH' || code === 'ENETUNREACH') { + return `${msg}(连接超时/网络不可达;若目标在境外或需要代理,请设置 NA_PROXY=<代理地址> 或配置系统代理后重试)`; + } return msg; } diff --git a/tests/unit/selfupdate-backup-cleanup.test.mjs b/tests/unit/selfupdate-backup-cleanup.test.mjs index a421b58..be6fe28 100644 --- a/tests/unit/selfupdate-backup-cleanup.test.mjs +++ b/tests/unit/selfupdate-backup-cleanup.test.mjs @@ -85,3 +85,23 @@ test('错误分类:DNS/拒连单独一类,普通错误原样透出', () => { const plain = new Error('boom'); assert.equal(describeNetError(plain, 1000), 'boom', '无法归类的错误原样透出,不编造原因'); }); + +test('describeNetError:TCP 层 ETIMEDOUT 必须给出「配代理」的可执行提示(2026-10-27 真机)', () => { + // 真机现场:境内 Windows 直连 github.com:443 → connect ETIMEDOUT 20.205.243.166:443。 + // 这条最常见,原先落到兜底分支只回一串超时,用户不知道唯一有效动作是配代理。 + const etimedout = Object.assign(new Error('connect ETIMEDOUT 20.205.243.166:443'), { code: 'ETIMEDOUT' }); + const msg = describeNetError(etimedout, 20_000); + assert.match(msg, /连接超时\/网络不可达/); + assert.match(msg, /NA_PROXY/, '必须点明 NA_PROXY 这个具体开关'); + assert.match(msg, /系统代理/); + + // 同族错误走同一提示 + for (const code of ['EHOSTUNREACH', 'ENETUNREACH']) { + assert.match(describeNetError(Object.assign(new Error('x'), { code }), 1000), /NA_PROXY/, code); + } + + // 不得与 ECONNRESET 的「代理或线路问题」混为一谈(两类原因不同) + const reset = Object.assign(new Error('read ECONNRESET'), { code: 'ECONNRESET' }); + assert.match(describeNetError(reset, 1000), /代理或线路/); + assert.doesNotMatch(describeNetError(reset, 1000), /NA_PROXY/); +});