fix(onboard): give actionable managed gateway recovery (#124783)

* fix(onboard): keep managed gateway recovery on service path

Amp-Thread-ID: https://ampcode.com/threads/T-01a00ae0-190d-718b-8a76-b75f3e8d1fae

* fix(onboard): distinguish install and restart recovery

Amp-Thread-ID: https://ampcode.com/threads/T-01a00ae0-190d-718b-8a76-b75f3e8d1fae

* fix(onboard): localize gateway recovery guidance

Amp-Thread-ID: https://ampcode.com/threads/T-01a00ae0-190d-718b-8a76-b75f3e8d1fae

* style(onboard): format gateway recovery test

Amp-Thread-ID: https://ampcode.com/threads/T-01a00ae0-190d-718b-8a76-b75f3e8d1fae

* chore(ci): refresh service recovery validation

---------

Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Peter Steinberger
2026-08-16 22:55:25 -07:00
committed by GitHub
parent 515fe51d8c
commit 54cbce25bd
5 changed files with 103 additions and 8 deletions
+4
View File
@@ -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.",
+4
View File
@@ -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}” 尚未配置凭据,聊天将失败,直到添加认证。",
+4
View File
@@ -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}」尚未設定憑證,聊天將失敗,直到新增認證。",
+42 -1
View File
@@ -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 () => {
+49 -7
View File
@@ -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) {