openclaw/src/gateway/assistant-identity.ts

119 lines
3.7 KiB
TypeScript

import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
import { resolveAgentIdentity } from "../agents/identity.js";
import { loadAgentIdentity } from "../commands/agents.config.js";
import type { OpenClawConfig } from "../config/config.js";
import { normalizeAgentId } from "../routing/session-key.js";
import { coerceIdentityValue } from "../shared/assistant-identity-values.js";
import {
isAvatarHttpUrl,
isAvatarImageDataUrl,
looksLikeAvatarPath,
} from "../shared/avatar-policy.js";
const MAX_ASSISTANT_NAME = 50;
const MAX_ASSISTANT_AVATAR = 200;
const MAX_ASSISTANT_EMOJI = 16;
export const DEFAULT_ASSISTANT_IDENTITY: AssistantIdentity = {
agentId: "main",
name: "Assistant",
avatar: "A",
};
export type AssistantIdentity = {
agentId: string;
name: string;
avatar: string;
emoji?: string;
};
function isAvatarUrl(value: string): boolean {
return isAvatarHttpUrl(value) || isAvatarImageDataUrl(value);
}
function normalizeAvatarValue(value: string | undefined): string | undefined {
if (!value) {
return undefined;
}
const trimmed = value.trim();
if (!trimmed) {
return undefined;
}
if (isAvatarUrl(trimmed)) {
return trimmed;
}
if (looksLikeAvatarPath(trimmed)) {
return trimmed;
}
if (!/\s/.test(trimmed) && trimmed.length <= 4) {
return trimmed;
}
return undefined;
}
function normalizeEmojiValue(value: string | undefined): string | undefined {
if (!value) {
return undefined;
}
const trimmed = value.trim();
if (!trimmed) {
return undefined;
}
if (trimmed.length > MAX_ASSISTANT_EMOJI) {
return undefined;
}
let hasNonAscii = false;
for (let i = 0; i < trimmed.length; i += 1) {
if (trimmed.charCodeAt(i) > 127) {
hasNonAscii = true;
break;
}
}
if (!hasNonAscii) {
return undefined;
}
if (isAvatarUrl(trimmed) || looksLikeAvatarPath(trimmed)) {
return undefined;
}
return trimmed;
}
export function resolveAssistantIdentity(params: {
cfg: OpenClawConfig;
agentId?: string | null;
workspaceDir?: string | null;
}): AssistantIdentity {
const agentId = normalizeAgentId(params.agentId ?? resolveDefaultAgentId(params.cfg));
const workspaceDir = params.workspaceDir ?? resolveAgentWorkspaceDir(params.cfg, agentId);
const configAssistant = params.cfg.ui?.assistant;
const agentIdentity = resolveAgentIdentity(params.cfg, agentId);
const fileIdentity = workspaceDir ? loadAgentIdentity(workspaceDir) : null;
const name =
coerceIdentityValue(configAssistant?.name, MAX_ASSISTANT_NAME) ??
coerceIdentityValue(agentIdentity?.name, MAX_ASSISTANT_NAME) ??
coerceIdentityValue(fileIdentity?.name, MAX_ASSISTANT_NAME) ??
DEFAULT_ASSISTANT_IDENTITY.name;
const avatarCandidates = [
coerceIdentityValue(configAssistant?.avatar, MAX_ASSISTANT_AVATAR),
coerceIdentityValue(agentIdentity?.avatar, MAX_ASSISTANT_AVATAR),
coerceIdentityValue(agentIdentity?.emoji, MAX_ASSISTANT_AVATAR),
coerceIdentityValue(fileIdentity?.avatar, MAX_ASSISTANT_AVATAR),
coerceIdentityValue(fileIdentity?.emoji, MAX_ASSISTANT_AVATAR),
];
const avatar =
avatarCandidates.map((candidate) => normalizeAvatarValue(candidate)).find(Boolean) ??
DEFAULT_ASSISTANT_IDENTITY.avatar;
const emojiCandidates = [
coerceIdentityValue(agentIdentity?.emoji, MAX_ASSISTANT_EMOJI),
coerceIdentityValue(fileIdentity?.emoji, MAX_ASSISTANT_EMOJI),
coerceIdentityValue(agentIdentity?.avatar, MAX_ASSISTANT_EMOJI),
coerceIdentityValue(fileIdentity?.avatar, MAX_ASSISTANT_EMOJI),
];
const emoji = emojiCandidates.map((candidate) => normalizeEmojiValue(candidate)).find(Boolean);
return { agentId, name, avatar, emoji };
}