mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-22 06:32:00 +00:00
fix(avatar): check ui.assistant.avatar in resolveAvatarSource
resolveAvatarSource in identity-avatar.ts only checked agents.list[].identity.avatar and IDENTITY.md, ignoring the ui.assistant.avatar config key written by the CLI config set command and the Appearance UI. This meant that `openclaw config set ui.assistant.avatar path/to/img.png` had no effect on the HTTP avatar endpoint (/avatar/:id), causing the avatar to not display in the control UI even when the backend served the correct image from IDENTITY.md. The fix mirrors the priority order already used by resolveAssistantIdentity in gateway/assistant-identity.ts: 1. ui.assistant.avatar (highest) 2. agents.list[].identity.avatar 3. IDENTITY.md avatar (lowest) Tests added for all three priority cases.
This commit is contained in:
@@ -15,9 +15,10 @@ async function expectLocalAvatarPath(
|
||||
cfg: OpenClawConfig,
|
||||
workspace: string,
|
||||
expectedRelativePath: string,
|
||||
opts?: Parameters<typeof resolveAgentAvatar>[2],
|
||||
) {
|
||||
const workspaceReal = await fs.realpath(workspace);
|
||||
const resolved = resolveAgentAvatar(cfg, "main");
|
||||
const resolved = resolveAgentAvatar(cfg, "main", opts);
|
||||
expect(resolved.kind).toBe("local");
|
||||
if (resolved.kind === "local") {
|
||||
const resolvedReal = await fs.realpath(resolved.filePath);
|
||||
@@ -164,4 +165,72 @@ describe("resolveAgentAvatar", () => {
|
||||
const data = resolveAgentAvatar(cfg, "data");
|
||||
expect(data.kind).toBe("data");
|
||||
});
|
||||
|
||||
it("resolves local avatar from ui.assistant.avatar when no agents.list identity is set", async () => {
|
||||
const root = await createTempAvatarRoot();
|
||||
const workspace = path.join(root, "work");
|
||||
const avatarPath = path.join(workspace, "ui-avatar.png");
|
||||
await writeFile(avatarPath);
|
||||
|
||||
const cfg: OpenClawConfig = {
|
||||
ui: { assistant: { avatar: "ui-avatar.png" } },
|
||||
agents: { list: [{ id: "main", workspace }] },
|
||||
};
|
||||
|
||||
await expectLocalAvatarPath(cfg, workspace, "ui-avatar.png", { includeUiOverride: true });
|
||||
});
|
||||
|
||||
it("ui.assistant.avatar ignored without includeUiOverride (outbound callers)", async () => {
|
||||
const root = await createTempAvatarRoot();
|
||||
const workspace = path.join(root, "work");
|
||||
const uiAvatarPath = path.join(workspace, "ui-avatar.png");
|
||||
const cfgAvatarPath = path.join(workspace, "cfg-avatar.png");
|
||||
await writeFile(uiAvatarPath);
|
||||
await writeFile(cfgAvatarPath);
|
||||
|
||||
const cfg: OpenClawConfig = {
|
||||
ui: { assistant: { avatar: "ui-avatar.png" } },
|
||||
agents: { list: [{ id: "main", workspace, identity: { avatar: "cfg-avatar.png" } }] },
|
||||
};
|
||||
|
||||
// Without the opt-in, outbound callers get the per-agent identity avatar, not the UI override.
|
||||
await expectLocalAvatarPath(cfg, workspace, "cfg-avatar.png");
|
||||
});
|
||||
|
||||
it("ui.assistant.avatar takes priority over agents.list identity.avatar with includeUiOverride", async () => {
|
||||
const root = await createTempAvatarRoot();
|
||||
const workspace = path.join(root, "work");
|
||||
const uiAvatarPath = path.join(workspace, "ui-avatar.png");
|
||||
const cfgAvatarPath = path.join(workspace, "cfg-avatar.png");
|
||||
await writeFile(uiAvatarPath);
|
||||
await writeFile(cfgAvatarPath);
|
||||
|
||||
const cfg: OpenClawConfig = {
|
||||
ui: { assistant: { avatar: "ui-avatar.png" } },
|
||||
agents: { list: [{ id: "main", workspace, identity: { avatar: "cfg-avatar.png" } }] },
|
||||
};
|
||||
|
||||
await expectLocalAvatarPath(cfg, workspace, "ui-avatar.png", { includeUiOverride: true });
|
||||
});
|
||||
|
||||
it("ui.assistant.avatar takes priority over IDENTITY.md avatar with includeUiOverride", async () => {
|
||||
const root = await createTempAvatarRoot();
|
||||
const workspace = path.join(root, "work");
|
||||
const uiAvatarPath = path.join(workspace, "ui-avatar.png");
|
||||
const identityAvatarPath = path.join(workspace, "identity-avatar.png");
|
||||
await writeFile(uiAvatarPath);
|
||||
await writeFile(identityAvatarPath);
|
||||
await fs.writeFile(
|
||||
path.join(workspace, "IDENTITY.md"),
|
||||
"- Avatar: identity-avatar.png\n",
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const cfg: OpenClawConfig = {
|
||||
ui: { assistant: { avatar: "ui-avatar.png" } },
|
||||
agents: { list: [{ id: "main", workspace }] },
|
||||
};
|
||||
|
||||
await expectLocalAvatarPath(cfg, workspace, "ui-avatar.png", { includeUiOverride: true });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,7 +24,17 @@ function normalizeAvatarValue(value: string | undefined | null): string | null {
|
||||
return trimmed ? trimmed : null;
|
||||
}
|
||||
|
||||
function resolveAvatarSource(cfg: OpenClawConfig, agentId: string): string | null {
|
||||
function resolveAvatarSource(
|
||||
cfg: OpenClawConfig,
|
||||
agentId: string,
|
||||
opts?: { includeUiOverride?: boolean },
|
||||
): string | null {
|
||||
if (opts?.includeUiOverride) {
|
||||
const fromUiConfig = normalizeAvatarValue(cfg.ui?.assistant?.avatar);
|
||||
if (fromUiConfig) {
|
||||
return fromUiConfig;
|
||||
}
|
||||
}
|
||||
const fromConfig = normalizeAvatarValue(resolveAgentIdentity(cfg, agentId)?.avatar);
|
||||
if (fromConfig) {
|
||||
return fromConfig;
|
||||
@@ -73,8 +83,12 @@ function resolveLocalAvatarPath(params: {
|
||||
return { ok: true, filePath: realPath };
|
||||
}
|
||||
|
||||
export function resolveAgentAvatar(cfg: OpenClawConfig, agentId: string): AgentAvatarResolution {
|
||||
const source = resolveAvatarSource(cfg, agentId);
|
||||
export function resolveAgentAvatar(
|
||||
cfg: OpenClawConfig,
|
||||
agentId: string,
|
||||
opts?: { includeUiOverride?: boolean },
|
||||
): AgentAvatarResolution {
|
||||
const source = resolveAvatarSource(cfg, agentId, opts);
|
||||
if (!source) {
|
||||
return { kind: "none", reason: "missing" };
|
||||
}
|
||||
|
||||
@@ -946,7 +946,8 @@ export function createGatewayHttpServer(opts: {
|
||||
run: () =>
|
||||
handleControlUiAvatarRequest(req, res, {
|
||||
basePath: controlUiBasePath,
|
||||
resolveAvatar: (agentId) => resolveAgentAvatar(configSnapshot, agentId),
|
||||
resolveAvatar: (agentId) =>
|
||||
resolveAgentAvatar(configSnapshot, agentId, { includeUiOverride: true }),
|
||||
}),
|
||||
});
|
||||
requestStages.push({
|
||||
|
||||
Reference in New Issue
Block a user