mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-07 22:09:57 +00:00
Gateway: fix control-ui version version-reporting consistency
This commit is contained in:
@@ -22,6 +22,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Skills/native command deduplication: centralize skill command dedupe by canonical `skillName` in `listSkillCommandsForAgents` so duplicate suffixed variants (for example `_2`) are no longer surfaced across interfaces outside Discord. (#27521) thanks @shivama205.
|
||||
- Agents/xAI tool-call argument decoding: decode HTML-entity encoded xAI/Grok tool-call argument values (`&`, `"`, `<`, `>`, numeric entities) before tool execution so commands with shell operators and quotes no longer fail with parse errors. (#35276) Thanks @Sid-Qin.
|
||||
- Agents/thinking-tag promotion hardening: guard `promoteThinkingTagsToBlocks` against malformed assistant content entries (`null`/`undefined`) before `block.type` reads so malformed provider payloads no longer crash session processing while preserving pass-through behavior. (#35143) thanks @Sid-Qin.
|
||||
- Gateway/Control UI version reporting: align runtime and browser client version metadata to avoid `dev` placeholders, wait for bootstrap version before first UI websocket connect, and only forward bootstrap `serverVersion` to same-origin gateway targets to prevent cross-target version leakage. (from #35230, #30928, #33928) Thanks @Sid-Qin, @joelnishanth, and @MoerAI.
|
||||
- Feishu/streaming card delivery synthesis: unify snapshot and delta streaming merge semantics, apply overlap-aware final merge, suppress duplicate final text delivery (including text+media final packets), prefer topic-thread `message.reply` routing when a reply target exists, and tune card print cadence to avoid duplicate incremental rendering. (from #33245, #32896, #33840) Thanks @rexl2018, @kcinzgg, and @aerelune.
|
||||
- Security/dependency audit: patch transitive Hono vulnerabilities by pinning `hono` to `4.12.5` and `@hono/node-server` to `1.19.10` in production resolution paths. Thanks @shakkernerd.
|
||||
- Security/dependency audit: bump `tar` to `7.5.10` (from `7.5.9`) to address the high-severity hardlink path traversal advisory (`GHSA-qffp-2rhf-9h96`). Thanks @shakkernerd.
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
} from "../infra/device-identity.js";
|
||||
import { clearDevicePairing } from "../infra/device-pairing.js";
|
||||
import { normalizeFingerprint } from "../infra/tls/fingerprint.js";
|
||||
import { VERSION } from "../version.js";
|
||||
import { rawDataToString } from "../infra/ws.js";
|
||||
import { logDebug, logError } from "../logger.js";
|
||||
import {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { resolveControlUiRootSync } from "../infra/control-ui-assets.js";
|
||||
import { isWithinDir } from "../infra/path-safety.js";
|
||||
import { openVerifiedFileSync } from "../infra/safe-open-sync.js";
|
||||
import { AVATAR_MAX_BYTES } from "../shared/avatar-policy.js";
|
||||
import { resolveRuntimeServiceVersion } from "../version.js";
|
||||
import { DEFAULT_ASSISTANT_IDENTITY, resolveAssistantIdentity } from "./assistant-identity.js";
|
||||
import {
|
||||
CONTROL_UI_BOOTSTRAP_CONFIG_PATH,
|
||||
@@ -25,7 +26,6 @@ import {
|
||||
normalizeControlUiBasePath,
|
||||
resolveAssistantAvatarUrl,
|
||||
} from "./control-ui-shared.js";
|
||||
import { resolveRuntimeServiceVersion } from "../version.js";
|
||||
|
||||
const ROOT_PREFIX = "/";
|
||||
const CONTROL_UI_ASSETS_MISSING_MESSAGE =
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { GATEWAY_EVENT_UPDATE_AVAILABLE } from "../../../src/gateway/events.js";
|
||||
import { connectGateway } from "./app-gateway.ts";
|
||||
import { connectGateway, resolveControlUiClientVersion } from "./app-gateway.ts";
|
||||
|
||||
type GatewayClientMock = {
|
||||
start: ReturnType<typeof vi.fn>;
|
||||
stop: ReturnType<typeof vi.fn>;
|
||||
options: { clientVersion?: string };
|
||||
emitClose: (info: {
|
||||
code: number;
|
||||
reason?: string;
|
||||
@@ -34,6 +35,7 @@ vi.mock("./gateway.ts", () => {
|
||||
|
||||
constructor(
|
||||
private opts: {
|
||||
clientVersion?: string;
|
||||
onClose?: (info: {
|
||||
code: number;
|
||||
reason: string;
|
||||
@@ -46,6 +48,7 @@ vi.mock("./gateway.ts", () => {
|
||||
gatewayClientInstances.push({
|
||||
start: this.start,
|
||||
stop: this.stop,
|
||||
options: { clientVersion: this.opts.clientVersion },
|
||||
emitClose: (info) => {
|
||||
this.opts.onClose?.({
|
||||
code: info.code,
|
||||
@@ -100,6 +103,7 @@ function createHost() {
|
||||
assistantName: "OpenClaw",
|
||||
assistantAvatar: null,
|
||||
assistantAgentId: null,
|
||||
serverVersion: null,
|
||||
sessionKey: "main",
|
||||
chatRunId: null,
|
||||
refreshSessionsAfterChat: new Set<string>(),
|
||||
@@ -227,3 +231,25 @@ describe("connectGateway", () => {
|
||||
expect(host.lastErrorCode).toBe("AUTH_TOKEN_MISMATCH");
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveControlUiClientVersion", () => {
|
||||
it("returns serverVersion for same-origin websocket targets", () => {
|
||||
expect(
|
||||
resolveControlUiClientVersion({
|
||||
gatewayUrl: "ws://localhost:8787",
|
||||
serverVersion: "2026.3.3",
|
||||
pageUrl: "http://localhost:8787/openclaw/",
|
||||
}),
|
||||
).toBe("2026.3.3");
|
||||
});
|
||||
|
||||
it("omits serverVersion for cross-origin targets", () => {
|
||||
expect(
|
||||
resolveControlUiClientVersion({
|
||||
gatewayUrl: "wss://gateway.example.com",
|
||||
serverVersion: "2026.3.3",
|
||||
pageUrl: "https://control.example.com/openclaw/",
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -85,6 +85,33 @@ type SessionDefaultsSnapshot = {
|
||||
scope?: string;
|
||||
};
|
||||
|
||||
export function resolveControlUiClientVersion(params: {
|
||||
gatewayUrl: string;
|
||||
serverVersion: string | null;
|
||||
pageUrl?: string;
|
||||
}): string | undefined {
|
||||
const serverVersion = params.serverVersion?.trim();
|
||||
if (!serverVersion) {
|
||||
return undefined;
|
||||
}
|
||||
const pageUrl =
|
||||
params.pageUrl ?? (typeof window === "undefined" ? undefined : window.location.href);
|
||||
if (!pageUrl) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
const page = new URL(pageUrl);
|
||||
const gateway = new URL(params.gatewayUrl, page);
|
||||
const expectedWsProtocol = page.protocol === "https:" ? "wss:" : "ws:";
|
||||
if (gateway.protocol !== expectedWsProtocol || gateway.host !== page.host) {
|
||||
return undefined;
|
||||
}
|
||||
return serverVersion;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeSessionKeyForDefaults(
|
||||
value: string | undefined,
|
||||
defaults: SessionDefaultsSnapshot,
|
||||
@@ -146,12 +173,16 @@ export function connectGateway(host: GatewayHost) {
|
||||
host.execApprovalError = null;
|
||||
|
||||
const previousClient = host.client;
|
||||
const clientVersion = resolveControlUiClientVersion({
|
||||
gatewayUrl: host.settings.gatewayUrl,
|
||||
serverVersion: host.serverVersion,
|
||||
});
|
||||
const client = new GatewayBrowserClient({
|
||||
url: host.settings.gatewayUrl,
|
||||
token: host.settings.token.trim() ? host.settings.token : undefined,
|
||||
password: host.password.trim() ? host.password : undefined,
|
||||
clientName: "openclaw-control-ui",
|
||||
clientVersion: host.serverVersion ?? undefined,
|
||||
clientVersion,
|
||||
mode: "webchat",
|
||||
instanceId: host.clientInstanceId,
|
||||
onHello: (hello) => {
|
||||
|
||||
82
ui/src/ui/app-lifecycle-connect.node.test.ts
Normal file
82
ui/src/ui/app-lifecycle-connect.node.test.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
const connectGatewayMock = vi.fn();
|
||||
const loadBootstrapMock = vi.fn();
|
||||
|
||||
vi.mock("./app-gateway.ts", () => ({
|
||||
connectGateway: connectGatewayMock,
|
||||
}));
|
||||
|
||||
vi.mock("./controllers/control-ui-bootstrap.ts", () => ({
|
||||
loadControlUiBootstrapConfig: loadBootstrapMock,
|
||||
}));
|
||||
|
||||
vi.mock("./app-settings.ts", () => ({
|
||||
applySettingsFromUrl: vi.fn(),
|
||||
attachThemeListener: vi.fn(),
|
||||
detachThemeListener: vi.fn(),
|
||||
inferBasePath: vi.fn(() => "/"),
|
||||
syncTabWithLocation: vi.fn(),
|
||||
syncThemeWithSettings: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("./app-polling.ts", () => ({
|
||||
startLogsPolling: vi.fn(),
|
||||
startNodesPolling: vi.fn(),
|
||||
stopLogsPolling: vi.fn(),
|
||||
stopNodesPolling: vi.fn(),
|
||||
startDebugPolling: vi.fn(),
|
||||
stopDebugPolling: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("./app-scroll.ts", () => ({
|
||||
observeTopbar: vi.fn(),
|
||||
scheduleChatScroll: vi.fn(),
|
||||
scheduleLogsScroll: vi.fn(),
|
||||
}));
|
||||
|
||||
import { handleConnected } from "./app-lifecycle.ts";
|
||||
|
||||
function createHost() {
|
||||
return {
|
||||
basePath: "",
|
||||
client: null,
|
||||
connected: false,
|
||||
tab: "chat",
|
||||
assistantName: "OpenClaw",
|
||||
assistantAvatar: null,
|
||||
assistantAgentId: null,
|
||||
serverVersion: null,
|
||||
chatHasAutoScrolled: false,
|
||||
chatManualRefreshInFlight: false,
|
||||
chatLoading: false,
|
||||
chatMessages: [],
|
||||
chatToolMessages: [],
|
||||
chatStream: "",
|
||||
logsAutoFollow: false,
|
||||
logsAtBottom: true,
|
||||
logsEntries: [],
|
||||
popStateHandler: vi.fn(),
|
||||
topbarObserver: null,
|
||||
};
|
||||
}
|
||||
|
||||
describe("handleConnected", () => {
|
||||
it("waits for bootstrap load before first gateway connect", async () => {
|
||||
let resolveBootstrap!: () => void;
|
||||
loadBootstrapMock.mockReturnValueOnce(
|
||||
new Promise<void>((resolve) => {
|
||||
resolveBootstrap = resolve;
|
||||
}),
|
||||
);
|
||||
connectGatewayMock.mockReset();
|
||||
const host = createHost();
|
||||
|
||||
handleConnected(host as never);
|
||||
expect(connectGatewayMock).not.toHaveBeenCalled();
|
||||
|
||||
resolveBootstrap();
|
||||
await Promise.resolve();
|
||||
expect(connectGatewayMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -43,13 +43,15 @@ type LifecycleHost = {
|
||||
|
||||
export function handleConnected(host: LifecycleHost) {
|
||||
host.basePath = inferBasePath();
|
||||
void loadControlUiBootstrapConfig(host);
|
||||
const bootstrapReady = loadControlUiBootstrapConfig(host);
|
||||
applySettingsFromUrl(host as unknown as Parameters<typeof applySettingsFromUrl>[0]);
|
||||
syncTabWithLocation(host as unknown as Parameters<typeof syncTabWithLocation>[0], true);
|
||||
syncThemeWithSettings(host as unknown as Parameters<typeof syncThemeWithSettings>[0]);
|
||||
attachThemeListener(host as unknown as Parameters<typeof attachThemeListener>[0]);
|
||||
window.addEventListener("popstate", host.popStateHandler);
|
||||
connectGateway(host as unknown as Parameters<typeof connectGateway>[0]);
|
||||
void bootstrapReady.finally(() =>
|
||||
connectGateway(host as unknown as Parameters<typeof connectGateway>[0]),
|
||||
);
|
||||
startNodesPolling(host as unknown as Parameters<typeof startNodesPolling>[0]);
|
||||
if (host.tab === "logs") {
|
||||
startLogsPolling(host as unknown as Parameters<typeof startLogsPolling>[0]);
|
||||
|
||||
Reference in New Issue
Block a user