diff --git a/src/wizard/i18n/locales/en.ts b/src/wizard/i18n/locales/en.ts index c629a2d850cb..166ac0753f4e 100644 --- a/src/wizard/i18n/locales/en.ts +++ b/src/wizard/i18n/locales/en.ts @@ -1146,6 +1146,10 @@ export const en = { healthCheckHelp: "Health check help", installGateway: "Install Gateway service (recommended)", laterTitle: "Later", + managedGatewaySetupFailed: + "The managed {service} setup failed: {error}\nInspect service state and logs: {statusCommand}\nRetry the managed service installation: {recoveryCommand}", + managedGatewayUnreachable: + "The managed {service} did not become reachable after setup.\nInspect service state and logs: {statusCommand}\nRetry the managed service: {recoveryCommand}", managedWebSearchSkipped: "Managed web search provider was skipped.", noBackgroundGatewayExpected: "Setup was run without Gateway service install, so no background gateway is expected.", diff --git a/src/wizard/i18n/locales/zh-CN.ts b/src/wizard/i18n/locales/zh-CN.ts index 9f0381f7ffb5..d94297e01849 100644 --- a/src/wizard/i18n/locales/zh-CN.ts +++ b/src/wizard/i18n/locales/zh-CN.ts @@ -1108,6 +1108,10 @@ export const zh_CN = { healthCheckHelp: "健康检查帮助", installGateway: "安装 Gateway 服务(推荐)", laterTitle: "稍后", + managedGatewaySetupFailed: + "托管的 {service} 设置失败:{error}\n检查服务状态和日志:{statusCommand}\n重试托管服务安装:{recoveryCommand}", + managedGatewayUnreachable: + "托管的 {service} 在设置后仍无法访问。\n检查服务状态和日志:{statusCommand}\n重试托管服务:{recoveryCommand}", managedWebSearchSkipped: "已跳过托管 web search provider。", noBackgroundGatewayExpected: "本次设置未安装 Gateway 服务,因此不会有后台 Gateway。", noModelAuth: "提供商 “{provider}” 尚未配置凭据,聊天将失败,直到添加认证。", diff --git a/src/wizard/i18n/locales/zh-TW.ts b/src/wizard/i18n/locales/zh-TW.ts index 2d7a5daf4e44..9f5f293820d1 100644 --- a/src/wizard/i18n/locales/zh-TW.ts +++ b/src/wizard/i18n/locales/zh-TW.ts @@ -1109,6 +1109,10 @@ export const zh_TW = { healthCheckHelp: "健康檢查說明", installGateway: "安裝 Gateway 服務(建議)", laterTitle: "稍後", + managedGatewaySetupFailed: + "受管理的 {service} 設定失敗:{error}\n檢查服務狀態和日誌:{statusCommand}\n重試受管理服務安裝:{recoveryCommand}", + managedGatewayUnreachable: + "受管理的 {service} 在設定後仍無法存取。\n檢查服務狀態和日誌:{statusCommand}\n重試受管理服務:{recoveryCommand}", managedWebSearchSkipped: "已略過託管 web search provider。", noBackgroundGatewayExpected: "本次設定未安裝 Gateway 服務,因此不會有背景 Gateway。", noModelAuth: "提供商「{provider}」尚未設定憑證,聊天將失敗,直到新增認證。", diff --git a/src/wizard/setup.finalize.test.ts b/src/wizard/setup.finalize.test.ts index 029dacde4188..02c18a7c5976 100644 --- a/src/wizard/setup.finalize.test.ts +++ b/src/wizard/setup.finalize.test.ts @@ -231,6 +231,7 @@ vi.mock("../daemon/service.js", () => ({ issues.map((issue) => issue.message).join("; "), startGatewayService, resolveGatewayService: vi.fn(() => ({ + label: "Mock Platform Service", isLoaded: gatewayServiceIsLoaded, restart: gatewayServiceRestart, uninstall: gatewayServiceUninstall, @@ -1297,8 +1298,48 @@ describe("finalizeSetupWizard", () => { expectNoteContains(prompter, "service install exploded", "Gateway"); expectNoteContains(prompter, "Gateway: not detected (service install exploded)", "Control UI"); expect(prompter.outro).toHaveBeenCalledWith( - "Gateway not detected yet. Start now: openclaw gateway run", + expect.stringContaining("managed Mock Platform Service setup failed"), ); + expectNoteContains(prompter, "openclaw gateway status --deep", "Gateway"); + expectNoteContains(prompter, "openclaw gateway install --force", "Gateway"); + expectNoteNotContains(prompter, "openclaw gateway run"); + expectNoteNotContains(prompter, "openclaw gateway restart"); + }); + + it.each([ + ["readiness timeout", "gateway readiness timed out"], + ["service crash", "gateway closed (1006 abnormal closure)"], + ["occupied port", "listen EADDRINUSE: address already in use 127.0.0.1:18789"], + ])("keeps managed %s recovery on the canonical service path", async (_name, detail) => { + waitForGatewayReachable.mockResolvedValue({ ok: false, detail }); + probeGatewayReachable.mockResolvedValue({ ok: false, detail }); + const prompter = createLaterPrompter(); + const args = createAdvancedFinalizeArgs({ installDaemon: true, prompter }); + + await finalizeSetupWizard({ ...args, opts: { ...args.opts, skipHealth: false } }); + + expectNoteContains(prompter, "managed Mock Platform Service", "Gateway"); + expectNoteContains(prompter, "openclaw gateway status --deep", "Gateway"); + expectNoteContains(prompter, "openclaw gateway restart", "Gateway"); + expectNoteNotContains(prompter, "openclaw gateway run"); + expectNoteNotContains(prompter, "openclaw onboard --install-daemon"); + expectNoteNotContains(prompter, "openclaw gateway install --force"); + }); + + it("localizes managed service recovery at the finalize boundary", async () => { + await withEnvAsync({ OPENCLAW_LOCALE: "zh-CN" }, async () => { + waitForGatewayReachable.mockResolvedValue({ ok: false, detail: "readiness timed out" }); + probeGatewayReachable.mockResolvedValue({ ok: false, detail: "readiness timed out" }); + const prompter = createLaterPrompter(); + const args = createAdvancedFinalizeArgs({ installDaemon: true, prompter }); + + await finalizeSetupWizard({ ...args, opts: { ...args.opts, skipHealth: false } }); + + expectNoteContains(prompter, "托管的 Mock Platform Service 在设置后仍无法访问", "Gateway"); + expectNoteContains(prompter, "检查服务状态和日志", "Gateway"); + expectNoteContains(prompter, "openclaw gateway restart", "Gateway"); + expectNoteNotContains(prompter, "openclaw gateway run"); + }); }); it("returns an authoritative failed outcome when gateway installation fails", async () => { diff --git a/src/wizard/setup.finalize.ts b/src/wizard/setup.finalize.ts index 1f059d516364..7ea0676b8878 100644 --- a/src/wizard/setup.finalize.ts +++ b/src/wizard/setup.finalize.ts @@ -163,19 +163,47 @@ export type GatewayServiceSetupOutcome = | { status: "skipped"; reason: "explicit" | "systemd-unavailable" | "external" } | { status: "failed"; error: string }; -function buildGatewayRecoveryProjection(gateway: GatewayServiceSetupOutcome): { +function buildGatewayRecoveryProjection(params: { + gateway: GatewayServiceSetupOutcome; + reachable: boolean; + serviceLabel?: string; +}): { detail: string; summary: string; } { + const { gateway } = params; + const notDetected = t("wizard.finalize.gatewayNotDetected"); + if (params.reachable) { + return { detail: t("wizard.finalize.gatewayReachable"), summary: t("wizard.guided.complete") }; + } + if (gateway.status === "ready") { + const service = params.serviceLabel ?? t("wizard.finalize.gatewayService"); + const detail = t("wizard.finalize.managedGatewayUnreachable", { + service, + statusCommand: formatCliCommand("openclaw gateway status --deep"), + recoveryCommand: formatCliCommand("openclaw gateway restart"), + }); + return { detail, summary: `${notDetected} ${detail.replaceAll("\n", " ")}` }; + } + if (gateway.status === "failed") { + const service = params.serviceLabel ?? t("wizard.finalize.gatewayService"); + const detail = t("wizard.finalize.managedGatewaySetupFailed", { + service, + error: gateway.error, + statusCommand: formatCliCommand("openclaw gateway status --deep"), + recoveryCommand: formatCliCommand("openclaw gateway install --force"), + }); + return { detail, summary: `${notDetected} ${detail.replaceAll("\n", " ")}` }; + } + const startGuidance = - gateway.status === "skipped" && gateway.reason === "external" + gateway.reason === "external" ? formatExternalSupervisorActionRequired("start the gateway") : t("wizard.finalize.startGatewayNow", { command: formatCliCommand("openclaw gateway run"), }); - const notDetected = t("wizard.finalize.gatewayNotDetected"); const summary = [notDetected, startGuidance].join(" "); - if (gateway.status === "skipped" && gateway.reason === "external") { + if (gateway.reason === "external") { return { detail: [notDetected, startGuidance].join("\n"), summary }; } return { @@ -492,7 +520,6 @@ export async function finalizeSetupWizard( prompter, runtime, }); - const gatewayRecovery = buildGatewayRecoveryProjection(gateway); if (gateway.status === "failed") { gatewayProbe = { ok: false, detail: gateway.error }; } @@ -599,8 +626,19 @@ export async function finalizeSetupWizard( ].join("\n"), t("wizard.finalize.healthCheckHelp"), ); + await prompter.note( + buildGatewayRecoveryProjection({ + gateway, + reachable: false, + serviceLabel: resolveGatewayService().label, + }).detail, + "Gateway", + ); } else { - await prompter.note(gatewayRecovery.detail, "Gateway"); + await prompter.note( + buildGatewayRecoveryProjection({ gateway, reachable: false }).detail, + "Gateway", + ); } } @@ -966,7 +1004,11 @@ export async function finalizeSetupWizard( }), ].join(" ") : t("wizard.guided.complete") - : gatewayRecovery.summary, + : buildGatewayRecoveryProjection({ + gateway, + reachable: false, + serviceLabel: gateway.status === "skipped" ? undefined : resolveGatewayService().label, + }).summary, ); if (shouldLaunchTui) {