Compare commits
3 Commits
main
...
fix/avatar
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ddfa51ba31 | ||
|
|
467d32ae45 | ||
|
|
e54b9ac743 |
@ -7,6 +7,7 @@ Docs: https://docs.clawd.bot
|
|||||||
### Fixes
|
### Fixes
|
||||||
- BlueBubbles: stop typing indicator on idle/no-reply. (#1439) Thanks @Nicell.
|
- BlueBubbles: stop typing indicator on idle/no-reply. (#1439) Thanks @Nicell.
|
||||||
- Auto-reply: only report a model switch when session state is available. (#1465) Thanks @robbyczgw-cla.
|
- Auto-reply: only report a model switch when session state is available. (#1465) Thanks @robbyczgw-cla.
|
||||||
|
- Control UI: resolve local avatar URLs with basePath across injection + identity RPC. (#1457) Thanks @dlauer.
|
||||||
|
|
||||||
## 2026.1.21-2
|
## 2026.1.21-2
|
||||||
|
|
||||||
|
|||||||
66
src/gateway/control-ui.test.ts
Normal file
66
src/gateway/control-ui.test.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { resolveAssistantAvatarUrl } from "./control-ui.js";
|
||||||
|
|
||||||
|
describe("resolveAssistantAvatarUrl", () => {
|
||||||
|
it("keeps remote and data URLs", () => {
|
||||||
|
expect(
|
||||||
|
resolveAssistantAvatarUrl({
|
||||||
|
avatar: "https://example.com/avatar.png",
|
||||||
|
agentId: "main",
|
||||||
|
basePath: "/ui",
|
||||||
|
}),
|
||||||
|
).toBe("https://example.com/avatar.png");
|
||||||
|
expect(
|
||||||
|
resolveAssistantAvatarUrl({
|
||||||
|
avatar: "data:image/png;base64,abc",
|
||||||
|
agentId: "main",
|
||||||
|
basePath: "/ui",
|
||||||
|
}),
|
||||||
|
).toBe("data:image/png;base64,abc");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prefixes basePath for /avatar endpoints", () => {
|
||||||
|
expect(
|
||||||
|
resolveAssistantAvatarUrl({
|
||||||
|
avatar: "/avatar/main",
|
||||||
|
agentId: "main",
|
||||||
|
basePath: "/ui",
|
||||||
|
}),
|
||||||
|
).toBe("/ui/avatar/main");
|
||||||
|
expect(
|
||||||
|
resolveAssistantAvatarUrl({
|
||||||
|
avatar: "/ui/avatar/main",
|
||||||
|
agentId: "main",
|
||||||
|
basePath: "/ui",
|
||||||
|
}),
|
||||||
|
).toBe("/ui/avatar/main");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps local avatar paths to the avatar endpoint", () => {
|
||||||
|
expect(
|
||||||
|
resolveAssistantAvatarUrl({
|
||||||
|
avatar: "avatars/me.png",
|
||||||
|
agentId: "main",
|
||||||
|
basePath: "/ui",
|
||||||
|
}),
|
||||||
|
).toBe("/ui/avatar/main");
|
||||||
|
expect(
|
||||||
|
resolveAssistantAvatarUrl({
|
||||||
|
avatar: "avatars/profile",
|
||||||
|
agentId: "main",
|
||||||
|
basePath: "/ui",
|
||||||
|
}),
|
||||||
|
).toBe("/ui/avatar/main");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps short text avatars", () => {
|
||||||
|
expect(
|
||||||
|
resolveAssistantAvatarUrl({
|
||||||
|
avatar: "PS",
|
||||||
|
agentId: "main",
|
||||||
|
basePath: "/ui",
|
||||||
|
}),
|
||||||
|
).toBe("PS");
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -98,7 +98,7 @@ function sendJson(res: ServerResponse, status: number, body: unknown) {
|
|||||||
res.end(JSON.stringify(body));
|
res.end(JSON.stringify(body));
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildAvatarUrl(basePath: string, agentId: string): string {
|
export function buildAvatarUrl(basePath: string, agentId: string): string {
|
||||||
return basePath ? `${basePath}${AVATAR_PREFIX}/${agentId}` : `${AVATAR_PREFIX}/${agentId}`;
|
return basePath ? `${basePath}${AVATAR_PREFIX}/${agentId}` : `${AVATAR_PREFIX}/${agentId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -206,11 +206,49 @@ interface ServeIndexHtmlOpts {
|
|||||||
agentId?: string;
|
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) {
|
function serveIndexHtml(res: ServerResponse, indexPath: string, opts: ServeIndexHtmlOpts) {
|
||||||
const { basePath, config, agentId } = opts;
|
const { basePath, config, agentId } = opts;
|
||||||
const identity = config
|
const identity = config
|
||||||
? resolveAssistantIdentity({ cfg: config, agentId })
|
? resolveAssistantIdentity({ cfg: config, agentId })
|
||||||
: DEFAULT_ASSISTANT_IDENTITY;
|
: DEFAULT_ASSISTANT_IDENTITY;
|
||||||
|
const resolvedAgentId =
|
||||||
|
typeof (identity as { agentId?: string }).agentId === "string"
|
||||||
|
? (identity as { agentId?: string }).agentId
|
||||||
|
: agentId;
|
||||||
|
const avatarValue =
|
||||||
|
resolveAssistantAvatarUrl({
|
||||||
|
avatar: identity.avatar,
|
||||||
|
agentId: resolvedAgentId,
|
||||||
|
basePath,
|
||||||
|
}) ?? identity.avatar;
|
||||||
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
||||||
res.setHeader("Cache-Control", "no-cache");
|
res.setHeader("Cache-Control", "no-cache");
|
||||||
const raw = fs.readFileSync(indexPath, "utf8");
|
const raw = fs.readFileSync(indexPath, "utf8");
|
||||||
@ -218,7 +256,7 @@ function serveIndexHtml(res: ServerResponse, indexPath: string, opts: ServeIndex
|
|||||||
injectControlUiConfig(raw, {
|
injectControlUiConfig(raw, {
|
||||||
basePath,
|
basePath,
|
||||||
assistantName: identity.name,
|
assistantName: identity.name,
|
||||||
assistantAvatar: identity.avatar,
|
assistantAvatar: avatarValue,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,6 +38,7 @@ import {
|
|||||||
import { loadSessionEntry } from "../session-utils.js";
|
import { loadSessionEntry } from "../session-utils.js";
|
||||||
import { formatForLog } from "../ws-log.js";
|
import { formatForLog } from "../ws-log.js";
|
||||||
import { resolveAssistantIdentity } from "../assistant-identity.js";
|
import { resolveAssistantIdentity } from "../assistant-identity.js";
|
||||||
|
import { resolveAssistantAvatarUrl } from "../control-ui.js";
|
||||||
import { waitForAgentJob } from "./agent-job.js";
|
import { waitForAgentJob } from "./agent-job.js";
|
||||||
import type { GatewayRequestHandlers } from "./types.js";
|
import type { GatewayRequestHandlers } from "./types.js";
|
||||||
|
|
||||||
@ -407,7 +408,13 @@ export const agentHandlers: GatewayRequestHandlers = {
|
|||||||
}
|
}
|
||||||
const cfg = loadConfig();
|
const cfg = loadConfig();
|
||||||
const identity = resolveAssistantIdentity({ cfg, agentId });
|
const identity = resolveAssistantIdentity({ cfg, agentId });
|
||||||
respond(true, identity, undefined);
|
const avatarValue =
|
||||||
|
resolveAssistantAvatarUrl({
|
||||||
|
avatar: identity.avatar,
|
||||||
|
agentId: identity.agentId,
|
||||||
|
basePath: cfg.gateway?.controlUi?.basePath,
|
||||||
|
}) ?? identity.avatar;
|
||||||
|
respond(true, { ...identity, avatar: avatarValue }, undefined);
|
||||||
},
|
},
|
||||||
"agent.wait": async ({ params, respond }) => {
|
"agent.wait": async ({ params, respond }) => {
|
||||||
if (!validateAgentWaitParams(params)) {
|
if (!validateAgentWaitParams(params)) {
|
||||||
|
|||||||
@ -158,7 +158,8 @@ function renderAvatar(
|
|||||||
function isAvatarUrl(value: string): boolean {
|
function isAvatarUrl(value: string): boolean {
|
||||||
return (
|
return (
|
||||||
/^https?:\/\//i.test(value) ||
|
/^https?:\/\//i.test(value) ||
|
||||||
/^data:image\//i.test(value)
|
/^data:image\//i.test(value) ||
|
||||||
|
/^\//.test(value) // Relative paths from avatar endpoint
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user