mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-06 23:01:08 +00:00
fix(avatar): check ui.assistant.avatar in resolveAvatarSource (#60778)
Merged via squash.
Prepared head SHA: df8d953a14
Co-authored-by: hannasdev <4538260+hannasdev@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
This commit is contained in:
@@ -110,6 +110,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/exec: restore `host=node` routing for node-pinned and `host=auto` sessions, while still blocking sandboxed `auto` sessions from jumping to gateway. (#60788) Thanks @openperf.
|
||||
- Agents/compaction: keep assistant tool calls and displaced tool results in the same compaction chunk so strict summarization providers stop rejecting orphaned tool pairs. (#58849) Thanks @openperf.
|
||||
- Outbound/sanitizer: strip leaked `<tool_call>`, `<function_calls>`, and model special tokens from shared user-visible assistant text, including truncated tool-call streams, so internal scaffolding no longer bleeds into replies across surfaces. (#60619) Thanks @oliviareid-svg.
|
||||
- Control UI/avatar: honor `ui.assistant.avatar` when serving `/avatar/:agentId` so Appearance UI avatar paths stop falling back to initials placeholders. (#60778) Thanks @hannasdev.
|
||||
|
||||
## 2026.4.2
|
||||
|
||||
|
||||
@@ -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