refactor: centralize control ui avatar helpers
This commit is contained in:
@@ -10,7 +10,7 @@ import type { ClawdbotConfig } from "../config/config.js";
|
||||
import { CONFIG_PATH_CLAWDBOT } from "../config/config.js";
|
||||
import { resolveSessionTranscriptsDirForAgent } from "../config/sessions.js";
|
||||
import { callGateway } from "../gateway/call.js";
|
||||
import { normalizeControlUiBasePath } from "../gateway/control-ui.js";
|
||||
import { normalizeControlUiBasePath } from "../gateway/control-ui-shared.js";
|
||||
import { isSafeExecutableValue } from "../infra/exec-safety.js";
|
||||
import { pickPrimaryTailnetIPv4 } from "../infra/tailnet.js";
|
||||
import { isWSL } from "../infra/wsl.js";
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { GatewayService } from "../daemon/service.js";
|
||||
import { resolveGatewayService } from "../daemon/service.js";
|
||||
import { resolveNodeService } from "../daemon/node-service.js";
|
||||
import { buildGatewayConnectionDetails, callGateway } from "../gateway/call.js";
|
||||
import { normalizeControlUiBasePath } from "../gateway/control-ui.js";
|
||||
import { normalizeControlUiBasePath } from "../gateway/control-ui-shared.js";
|
||||
import { probeGateway } from "../gateway/probe.js";
|
||||
import { collectChannelStatusIssues } from "../infra/channels-status-issues.js";
|
||||
import { resolveClawdbotPackageRoot } from "../infra/clawdbot-root.js";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { withProgress } from "../cli/progress.js";
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import { buildGatewayConnectionDetails, callGateway } from "../gateway/call.js";
|
||||
import { normalizeControlUiBasePath } from "../gateway/control-ui.js";
|
||||
import { normalizeControlUiBasePath } from "../gateway/control-ui-shared.js";
|
||||
import { probeGateway } from "../gateway/probe.js";
|
||||
import { collectChannelStatusIssues } from "../infra/channels-status-issues.js";
|
||||
import { resolveOsSummary } from "../infra/os-summary.js";
|
||||
|
||||
49
src/gateway/control-ui-shared.ts
Normal file
49
src/gateway/control-ui-shared.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
const CONTROL_UI_AVATAR_PREFIX = "/avatar";
|
||||
|
||||
export function normalizeControlUiBasePath(basePath?: string): string {
|
||||
if (!basePath) return "";
|
||||
let normalized = basePath.trim();
|
||||
if (!normalized) return "";
|
||||
if (!normalized.startsWith("/")) normalized = `/${normalized}`;
|
||||
if (normalized === "/") return "";
|
||||
if (normalized.endsWith("/")) normalized = normalized.slice(0, -1);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
export function buildControlUiAvatarUrl(basePath: string, agentId: string): string {
|
||||
return basePath
|
||||
? `${basePath}${CONTROL_UI_AVATAR_PREFIX}/${agentId}`
|
||||
: `${CONTROL_UI_AVATAR_PREFIX}/${agentId}`;
|
||||
}
|
||||
|
||||
function looksLikeLocalAvatarPath(value: string): boolean {
|
||||
if (/[\\/]/.test(value)) return true;
|
||||
return /\.(png|jpe?g|gif|webp|svg|ico)$/i.test(value);
|
||||
}
|
||||
|
||||
export function resolveAssistantAvatarUrl(params: {
|
||||
avatar?: string | null;
|
||||
agentId?: string | null;
|
||||
basePath?: string;
|
||||
}): string | undefined {
|
||||
const avatar = params.avatar?.trim();
|
||||
if (!avatar) return undefined;
|
||||
if (/^https?:\/\//i.test(avatar) || /^data:image\//i.test(avatar)) return avatar;
|
||||
|
||||
const basePath = normalizeControlUiBasePath(params.basePath);
|
||||
const baseAvatarPrefix = basePath
|
||||
? `${basePath}${CONTROL_UI_AVATAR_PREFIX}/`
|
||||
: `${CONTROL_UI_AVATAR_PREFIX}/`;
|
||||
if (basePath && avatar.startsWith(`${CONTROL_UI_AVATAR_PREFIX}/`)) {
|
||||
return `${basePath}${avatar}`;
|
||||
}
|
||||
if (avatar.startsWith(baseAvatarPrefix)) return avatar;
|
||||
|
||||
if (!params.agentId) return avatar;
|
||||
if (looksLikeLocalAvatarPath(avatar)) {
|
||||
return buildControlUiAvatarUrl(basePath, params.agentId);
|
||||
}
|
||||
return avatar;
|
||||
}
|
||||
|
||||
export { CONTROL_UI_AVATAR_PREFIX };
|
||||
@@ -1,8 +1,26 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { resolveAssistantAvatarUrl } from "./control-ui.js";
|
||||
import {
|
||||
buildControlUiAvatarUrl,
|
||||
normalizeControlUiBasePath,
|
||||
resolveAssistantAvatarUrl,
|
||||
} from "./control-ui-shared.js";
|
||||
|
||||
describe("resolveAssistantAvatarUrl", () => {
|
||||
it("normalizes base paths", () => {
|
||||
expect(normalizeControlUiBasePath()).toBe("");
|
||||
expect(normalizeControlUiBasePath("")).toBe("");
|
||||
expect(normalizeControlUiBasePath(" ")).toBe("");
|
||||
expect(normalizeControlUiBasePath("/")).toBe("");
|
||||
expect(normalizeControlUiBasePath("ui")).toBe("/ui");
|
||||
expect(normalizeControlUiBasePath("/ui/")).toBe("/ui");
|
||||
});
|
||||
|
||||
it("builds avatar URLs", () => {
|
||||
expect(buildControlUiAvatarUrl("", "main")).toBe("/avatar/main");
|
||||
expect(buildControlUiAvatarUrl("/ui", "main")).toBe("/ui/avatar/main");
|
||||
});
|
||||
|
||||
it("keeps remote and data URLs", () => {
|
||||
expect(
|
||||
resolveAssistantAvatarUrl({
|
||||
@@ -54,6 +72,15 @@ describe("resolveAssistantAvatarUrl", () => {
|
||||
).toBe("/ui/avatar/main");
|
||||
});
|
||||
|
||||
it("leaves local paths untouched when agentId is missing", () => {
|
||||
expect(
|
||||
resolveAssistantAvatarUrl({
|
||||
avatar: "avatars/me.png",
|
||||
basePath: "/ui",
|
||||
}),
|
||||
).toBe("avatars/me.png");
|
||||
});
|
||||
|
||||
it("keeps short text avatars", () => {
|
||||
expect(
|
||||
resolveAssistantAvatarUrl({
|
||||
|
||||
@@ -5,9 +5,14 @@ import { fileURLToPath } from "node:url";
|
||||
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import { DEFAULT_ASSISTANT_IDENTITY, resolveAssistantIdentity } from "./assistant-identity.js";
|
||||
import {
|
||||
buildControlUiAvatarUrl,
|
||||
CONTROL_UI_AVATAR_PREFIX,
|
||||
normalizeControlUiBasePath,
|
||||
resolveAssistantAvatarUrl,
|
||||
} from "./control-ui-shared.js";
|
||||
|
||||
const ROOT_PREFIX = "/";
|
||||
const AVATAR_PREFIX = "/avatar";
|
||||
|
||||
export type ControlUiRequestOptions = {
|
||||
basePath?: string;
|
||||
@@ -15,16 +20,6 @@ export type ControlUiRequestOptions = {
|
||||
agentId?: string;
|
||||
};
|
||||
|
||||
export function normalizeControlUiBasePath(basePath?: string): string {
|
||||
if (!basePath) return "";
|
||||
let normalized = basePath.trim();
|
||||
if (!normalized) return "";
|
||||
if (!normalized.startsWith("/")) normalized = `/${normalized}`;
|
||||
if (normalized === "/") return "";
|
||||
if (normalized.endsWith("/")) normalized = normalized.slice(0, -1);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function resolveControlUiRoot(): string | null {
|
||||
const here = path.dirname(fileURLToPath(import.meta.url));
|
||||
const execDir = (() => {
|
||||
@@ -98,10 +93,6 @@ function sendJson(res: ServerResponse, status: number, body: unknown) {
|
||||
res.end(JSON.stringify(body));
|
||||
}
|
||||
|
||||
export function buildAvatarUrl(basePath: string, agentId: string): string {
|
||||
return basePath ? `${basePath}${AVATAR_PREFIX}/${agentId}` : `${AVATAR_PREFIX}/${agentId}`;
|
||||
}
|
||||
|
||||
function isValidAgentId(agentId: string): boolean {
|
||||
return /^[a-z0-9][a-z0-9_-]{0,63}$/i.test(agentId);
|
||||
}
|
||||
@@ -118,7 +109,9 @@ export function handleControlUiAvatarRequest(
|
||||
const url = new URL(urlRaw, "http://localhost");
|
||||
const basePath = normalizeControlUiBasePath(opts.basePath);
|
||||
const pathname = url.pathname;
|
||||
const pathWithBase = basePath ? `${basePath}${AVATAR_PREFIX}/` : `${AVATAR_PREFIX}/`;
|
||||
const pathWithBase = basePath
|
||||
? `${basePath}${CONTROL_UI_AVATAR_PREFIX}/`
|
||||
: `${CONTROL_UI_AVATAR_PREFIX}/`;
|
||||
if (!pathname.startsWith(pathWithBase)) return false;
|
||||
|
||||
const agentIdParts = pathname.slice(pathWithBase.length).split("/").filter(Boolean);
|
||||
@@ -132,7 +125,7 @@ export function handleControlUiAvatarRequest(
|
||||
const resolved = opts.resolveAvatar(agentId);
|
||||
const avatarUrl =
|
||||
resolved.kind === "local"
|
||||
? buildAvatarUrl(basePath, agentId)
|
||||
? buildControlUiAvatarUrl(basePath, agentId)
|
||||
: resolved.kind === "remote" || resolved.kind === "data"
|
||||
? resolved.url
|
||||
: null;
|
||||
@@ -206,34 +199,6 @@ interface ServeIndexHtmlOpts {
|
||||
agentId?: string;
|
||||
}
|
||||
|
||||
function looksLikeLocalAvatarPath(value: string): boolean {
|
||||
if (/[\\/]/.test(value)) return true;
|
||||
return /\.(png|jpe?g|gif|webp|svg|ico)$/i.test(value);
|
||||
}
|
||||
|
||||
export function resolveAssistantAvatarUrl(params: {
|
||||
avatar?: string | null;
|
||||
agentId?: string | null;
|
||||
basePath?: string;
|
||||
}): string | undefined {
|
||||
const avatar = params.avatar?.trim();
|
||||
if (!avatar) return undefined;
|
||||
if (/^https?:\/\//i.test(avatar) || /^data:image\//i.test(avatar)) return avatar;
|
||||
|
||||
const basePath = normalizeControlUiBasePath(params.basePath);
|
||||
const baseAvatarPrefix = basePath ? `${basePath}${AVATAR_PREFIX}/` : `${AVATAR_PREFIX}/`;
|
||||
if (basePath && avatar.startsWith(`${AVATAR_PREFIX}/`)) {
|
||||
return `${basePath}${avatar}`;
|
||||
}
|
||||
if (avatar.startsWith(baseAvatarPrefix)) return avatar;
|
||||
|
||||
if (!params.agentId) return avatar;
|
||||
if (looksLikeLocalAvatarPath(avatar)) {
|
||||
return buildAvatarUrl(basePath, params.agentId);
|
||||
}
|
||||
return avatar;
|
||||
}
|
||||
|
||||
function serveIndexHtml(res: ServerResponse, indexPath: string, opts: ServeIndexHtmlOpts) {
|
||||
const { basePath, config, agentId } = opts;
|
||||
const identity = config
|
||||
|
||||
@@ -38,7 +38,7 @@ import {
|
||||
import { loadSessionEntry } from "../session-utils.js";
|
||||
import { formatForLog } from "../ws-log.js";
|
||||
import { resolveAssistantIdentity } from "../assistant-identity.js";
|
||||
import { resolveAssistantAvatarUrl } from "../control-ui.js";
|
||||
import { resolveAssistantAvatarUrl } from "../control-ui-shared.js";
|
||||
import { waitForAgentJob } from "./agent-job.js";
|
||||
import type { GatewayRequestHandlers } from "./types.js";
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
type ResolvedGatewayAuth,
|
||||
resolveGatewayAuth,
|
||||
} from "./auth.js";
|
||||
import { normalizeControlUiBasePath } from "./control-ui.js";
|
||||
import { normalizeControlUiBasePath } from "./control-ui-shared.js";
|
||||
import { resolveHooksConfig } from "./hooks.js";
|
||||
import { isLoopbackHost, resolveGatewayBindHost } from "./net.js";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user