From 9872c551c297223a8846beaadc870044b326e3e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=A7=E7=A6=B9?= Date: Thu, 12 Mar 2026 19:20:02 +0800 Subject: [PATCH] fix(ui): restore control-ui query token imports --- docs/web/control-ui.md | 2 +- ui/src/ui/app-settings.test.ts | 90 ++++++++++++++++++++++++++++ ui/src/ui/app-settings.ts | 2 +- ui/src/ui/navigation.browser.test.ts | 26 +++++++- 4 files changed, 116 insertions(+), 4 deletions(-) diff --git a/docs/web/control-ui.md b/docs/web/control-ui.md index 59e9c0c226b..d363c92e31b 100644 --- a/docs/web/control-ui.md +++ b/docs/web/control-ui.md @@ -242,7 +242,7 @@ http://localhost:5173/?gatewayUrl=wss://:18789#token= void) | null; logsPollInterval: number | null; debugPollInterval: number | null; + pendingGatewayUrl?: string | null; + pendingGatewayToken?: string | null; }; function createStorageMock(): Storage { @@ -80,6 +82,49 @@ function createStorageMock(): Storage { }; } +function setTestWindowUrl(urlString: string) { + const current = new URL(urlString); + const history = { + replaceState: vi.fn((_state: unknown, _title: string, nextUrl: string | URL) => { + const next = new URL(String(nextUrl), current.toString()); + current.href = next.toString(); + current.protocol = next.protocol; + current.host = next.host; + current.pathname = next.pathname; + current.search = next.search; + current.hash = next.hash; + }), + }; + const locationLike = { + get href() { + return current.toString(); + }, + get protocol() { + return current.protocol; + }, + get host() { + return current.host; + }, + get pathname() { + return current.pathname; + }, + get search() { + return current.search; + }, + get hash() { + return current.hash; + }, + }; + vi.stubGlobal("window", { + location: locationLike, + history, + setInterval, + clearInterval, + } as unknown as Window & typeof globalThis); + vi.stubGlobal("location", locationLike as Location); + return { history, location: locationLike }; +} + const createHost = (tab: Tab): SettingsHost => ({ settings: { gatewayUrl: "", @@ -233,3 +278,48 @@ describe("setTabFromRoute", () => { expect(root.style.colorScheme).toBe("light"); }); }); + +describe("applySettingsFromUrl", () => { + let appSettings: AppSettingsModule; + + beforeEach(() => { + vi.resetModules(); + vi.stubGlobal("localStorage", createStorageMock()); + vi.stubGlobal("sessionStorage", createStorageMock()); + vi.stubGlobal("navigator", { language: "en-US" } as Navigator); + setTestWindowUrl("https://control.example/ui/overview"); + }); + + afterEach(() => { + vi.restoreAllMocks(); + vi.unstubAllGlobals(); + }); + + it("hydrates query token params and strips them from the URL", async () => { + appSettings ??= await import("./app-settings.ts"); + setTestWindowUrl("https://control.example/ui/overview?token=abc123"); + const host = createHost("overview"); + host.settings.gatewayUrl = "wss://control.example/openclaw"; + + appSettings.applySettingsFromUrl(host); + + expect(host.settings.token).toBe("abc123"); + expect(window.location.search).toBe(""); + }); + + it("keeps query token params pending when a gatewayUrl confirmation is required", async () => { + appSettings ??= await import("./app-settings.ts"); + setTestWindowUrl( + "https://control.example/ui/overview?gatewayUrl=wss://other-gateway.example/openclaw&token=abc123", + ); + const host = createHost("overview"); + host.settings.gatewayUrl = "wss://control.example/openclaw"; + + appSettings.applySettingsFromUrl(host); + + expect(host.settings.token).toBe(""); + expect(host.pendingGatewayUrl).toBe("wss://other-gateway.example/openclaw"); + expect(host.pendingGatewayToken).toBe("abc123"); + expect(window.location.search).toBe(""); + }); +}); diff --git a/ui/src/ui/app-settings.ts b/ui/src/ui/app-settings.ts index f7ed785b69a..546b567b55e 100644 --- a/ui/src/ui/app-settings.ts +++ b/ui/src/ui/app-settings.ts @@ -107,7 +107,7 @@ export function applySettingsFromUrl(host: SettingsHost) { const gatewayUrlRaw = params.get("gatewayUrl") ?? hashParams.get("gatewayUrl"); const nextGatewayUrl = gatewayUrlRaw?.trim() ?? ""; const gatewayUrlChanged = Boolean(nextGatewayUrl && nextGatewayUrl !== host.settings.gatewayUrl); - const tokenRaw = hashParams.get("token"); + const tokenRaw = hashParams.get("token") ?? params.get("token"); const passwordRaw = params.get("password") ?? hashParams.get("password"); const sessionRaw = params.get("session") ?? hashParams.get("session"); let shouldCleanUrl = false; diff --git a/ui/src/ui/navigation.browser.test.ts b/ui/src/ui/navigation.browser.test.ts index d9b5f3c7182..ae8e13a561c 100644 --- a/ui/src/ui/navigation.browser.test.ts +++ b/ui/src/ui/navigation.browser.test.ts @@ -146,11 +146,11 @@ describe("control UI routing", () => { expect(container.scrollTop).toBe(maxScroll); }); - it("strips query token params without importing them", async () => { + it("hydrates token from query params and strips them", async () => { const app = mountApp("/ui/overview?token=abc123"); await app.updateComplete; - expect(app.settings.token).toBe(""); + expect(app.settings.token).toBe("abc123"); expect(JSON.parse(localStorage.getItem("openclaw.control.settings.v1") ?? "{}").token).toBe( undefined, ); @@ -236,6 +236,28 @@ describe("control UI routing", () => { expect(window.location.hash).toBe(""); }); + it("keeps a query token pending until the gateway URL change is confirmed", async () => { + const app = mountApp( + "/ui/overview?gatewayUrl=wss://other-gateway.example/openclaw&token=abc123", + ); + await app.updateComplete; + + expect(app.settings.gatewayUrl).not.toBe("wss://other-gateway.example/openclaw"); + expect(app.settings.token).toBe(""); + + const confirmButton = Array.from(app.querySelectorAll("button")).find( + (button) => button.textContent?.trim() === "Confirm", + ); + expect(confirmButton).not.toBeUndefined(); + confirmButton?.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true })); + await app.updateComplete; + + expect(app.settings.gatewayUrl).toBe("wss://other-gateway.example/openclaw"); + expect(app.settings.token).toBe("abc123"); + expect(window.location.search).toBe(""); + expect(window.location.hash).toBe(""); + }); + it("restores the token after a same-tab refresh", async () => { const first = mountApp("/ui/overview#token=abc123"); await first.updateComplete;