mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
fix: resolve Control UI public assets from base path
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="manifest" href="manifest.webmanifest" />
|
||||
<link rel="manifest" href="/manifest.webmanifest" />
|
||||
<script>
|
||||
(function () {
|
||||
var THEMES = { claw: 1, knot: 1, dash: 1 };
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import "./styles.css";
|
||||
import "./ui/app.ts";
|
||||
import { inferControlUiPublicAssetPath } from "./ui/public-assets.ts";
|
||||
|
||||
type ViteImportMeta = ImportMeta & {
|
||||
readonly env?: {
|
||||
@@ -11,8 +12,10 @@ declare const OPENCLAW_CONTROL_UI_BUILD_ID: string | undefined;
|
||||
|
||||
const isProd = (import.meta as ViteImportMeta).env?.PROD === true;
|
||||
|
||||
syncDocumentPublicAssetLinks();
|
||||
|
||||
if (isProd && "serviceWorker" in navigator) {
|
||||
const swUrl = new URL("./sw.js", window.location.href);
|
||||
const swUrl = new URL(inferControlUiPublicAssetPath("sw.js"), window.location.origin);
|
||||
swUrl.searchParams.set("v", OPENCLAW_CONTROL_UI_BUILD_ID || "dev");
|
||||
void navigator.serviceWorker.register(swUrl, { updateViaCache: "none" });
|
||||
} else if (!isProd && "serviceWorker" in navigator) {
|
||||
@@ -23,3 +26,21 @@ if (isProd && "serviceWorker" in navigator) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function syncDocumentPublicAssetLinks() {
|
||||
setDocumentLinkHref('link[rel="icon"][type="image/svg+xml"]', "favicon.svg");
|
||||
setDocumentLinkHref('link[rel="icon"][type="image/png"]', "favicon-32.png");
|
||||
setDocumentLinkHref('link[rel="apple-touch-icon"]', "apple-touch-icon.png");
|
||||
setDocumentLinkHref('link[rel="manifest"]', "manifest.webmanifest");
|
||||
}
|
||||
|
||||
function setDocumentLinkHref(
|
||||
selector: string,
|
||||
asset: Parameters<typeof inferControlUiPublicAssetPath>[0],
|
||||
) {
|
||||
const link = document.querySelector<HTMLLinkElement>(selector);
|
||||
if (!link) {
|
||||
return;
|
||||
}
|
||||
link.href = inferControlUiPublicAssetPath(asset);
|
||||
}
|
||||
|
||||
37
ui/src/ui/public-assets.test.ts
Normal file
37
ui/src/ui/public-assets.test.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { controlUiPublicAssetPath, inferControlUiPublicAssetPath } from "./public-assets.ts";
|
||||
|
||||
describe("controlUiPublicAssetPath", () => {
|
||||
it("resolves root-mounted public assets from the URL root", () => {
|
||||
expect(controlUiPublicAssetPath("favicon.svg", "")).toBe("/favicon.svg");
|
||||
expect(controlUiPublicAssetPath("manifest.webmanifest", null)).toBe("/manifest.webmanifest");
|
||||
});
|
||||
|
||||
it("resolves base-mounted public assets under the configured base path", () => {
|
||||
expect(controlUiPublicAssetPath("favicon.svg", "/ui")).toBe("/ui/favicon.svg");
|
||||
expect(controlUiPublicAssetPath("sw.js", "/apps/openclaw/")).toBe("/apps/openclaw/sw.js");
|
||||
});
|
||||
});
|
||||
|
||||
describe("inferControlUiPublicAssetPath", () => {
|
||||
it("uses the root for known nested routes without a configured base path", () => {
|
||||
expect(
|
||||
inferControlUiPublicAssetPath("manifest.webmanifest", { pathname: "/skills/workshop" }),
|
||||
).toBe("/manifest.webmanifest");
|
||||
});
|
||||
|
||||
it("infers base-mounted assets from nested routes", () => {
|
||||
expect(inferControlUiPublicAssetPath("sw.js", { pathname: "/openclaw/skills/workshop" })).toBe(
|
||||
"/openclaw/sw.js",
|
||||
);
|
||||
});
|
||||
|
||||
it("prefers an explicit base path over pathname inference", () => {
|
||||
expect(
|
||||
inferControlUiPublicAssetPath("apple-touch-icon.png", {
|
||||
basePath: "/control/",
|
||||
pathname: "/skills/workshop",
|
||||
}),
|
||||
).toBe("/control/apple-touch-icon.png");
|
||||
});
|
||||
});
|
||||
52
ui/src/ui/public-assets.ts
Normal file
52
ui/src/ui/public-assets.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { inferBasePathFromPathname, normalizeBasePath } from "./navigation.ts";
|
||||
|
||||
export type ControlUiPublicAsset =
|
||||
| "apple-touch-icon.png"
|
||||
| "favicon-32.png"
|
||||
| "favicon.ico"
|
||||
| "favicon.svg"
|
||||
| "manifest.webmanifest"
|
||||
| "sw.js";
|
||||
|
||||
type WindowWithControlUiBasePath = Window &
|
||||
typeof globalThis & {
|
||||
__OPENCLAW_CONTROL_UI_BASE_PATH__?: string;
|
||||
};
|
||||
|
||||
export function controlUiPublicAssetPath(
|
||||
asset: ControlUiPublicAsset,
|
||||
basePath: string | null | undefined,
|
||||
): string {
|
||||
const base = normalizeBasePath(basePath ?? "");
|
||||
return base ? `${base}/${asset}` : `/${asset}`;
|
||||
}
|
||||
|
||||
export function inferControlUiPublicAssetPath(
|
||||
asset: ControlUiPublicAsset,
|
||||
params?: {
|
||||
basePath?: string | null;
|
||||
pathname?: string;
|
||||
},
|
||||
): string {
|
||||
const configured = params?.basePath ?? readConfiguredBasePath();
|
||||
const inferredBasePath =
|
||||
configured != null
|
||||
? configured
|
||||
: inferBasePathFromPathname(params?.pathname ?? currentPathname());
|
||||
return controlUiPublicAssetPath(asset, inferredBasePath);
|
||||
}
|
||||
|
||||
function readConfiguredBasePath(): string | null {
|
||||
if (typeof window === "undefined") {
|
||||
return null;
|
||||
}
|
||||
const value = (window as WindowWithControlUiBasePath).__OPENCLAW_CONTROL_UI_BASE_PATH__;
|
||||
return typeof value === "string" ? value : null;
|
||||
}
|
||||
|
||||
function currentPathname(): string {
|
||||
if (typeof window === "undefined") {
|
||||
return "/";
|
||||
}
|
||||
return window.location.pathname;
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
} from "../../../../src/agents/tool-policy-shared.js";
|
||||
import { DEFAULT_ASSISTANT_AVATAR } from "../assistant-identity.ts";
|
||||
import { buildQualifiedChatModelValue } from "../chat-model-ref.ts";
|
||||
import { controlUiPublicAssetPath } from "../public-assets.ts";
|
||||
import { normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "../string-coerce.ts";
|
||||
import type {
|
||||
AgentIdentityResult,
|
||||
@@ -244,13 +245,11 @@ export function resolveChatAvatarRenderUrl(
|
||||
}
|
||||
|
||||
export function agentLogoUrl(basePath: string): string {
|
||||
const base = normalizeOptionalString(basePath)?.replace(/\/$/, "") ?? "";
|
||||
return base ? `${base}/favicon.svg` : "/favicon.svg";
|
||||
return controlUiPublicAssetPath("favicon.svg", basePath);
|
||||
}
|
||||
|
||||
export function assistantAvatarFallbackUrl(basePath: string): string {
|
||||
const base = normalizeOptionalString(basePath)?.replace(/\/$/, "") ?? "";
|
||||
return base ? `${base}/apple-touch-icon.png` : "/apple-touch-icon.png";
|
||||
return controlUiPublicAssetPath("apple-touch-icon.png", basePath);
|
||||
}
|
||||
|
||||
function isAvatarUrl(value: string): boolean {
|
||||
|
||||
Reference in New Issue
Block a user