11// Gateway assistant identity resolver.
22// Combines UI, agent config, and workspace identity files for Control UI display.
3+ import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce" ;
4+ import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice" ;
35import { resolveAgentWorkspaceDir , resolveDefaultAgentId } from "../agents/agent-scope.js" ;
46import { resolveAgentIdentity } from "../agents/identity.js" ;
57import { loadAgentIdentity } from "../commands/agents.config.js" ;
68import type { OpenClawConfig } from "../config/types.openclaw.js" ;
79import { normalizeAgentId } from "../routing/session-key.js" ;
8- import { coerceIdentityValue } from "../shared/assistant-identity-values.js" ;
910import {
1011 isAvatarHttpUrl ,
1112 isAvatarImageDataUrl ,
1213 looksLikeAvatarPath ,
1314} from "../shared/avatar-policy.js" ;
1415
15- const MAX_ASSISTANT_NAME = 50 ;
16- // Image-bearing avatars (data: URLs, paths) need to round-trip through
17- // coerceIdentityValue without truncation. Sized to match
18- // MAX_LOCAL_USER_IMAGE_AVATAR / AVATAR_MAX_BYTES expansion.
19- const MAX_ASSISTANT_AVATAR = 2_000_000 ;
20- const MAX_ASSISTANT_EMOJI = 16 ;
16+ const ASSISTANT_IDENTITY_LIMITS = {
17+ name : 50 ,
18+ // Image-bearing avatars must round-trip without truncation. This matches
19+ // MAX_LOCAL_USER_IMAGE_AVATAR / AVATAR_MAX_BYTES expansion.
20+ avatar : 2_000_000 ,
21+ emoji : 16 ,
22+ } as const ;
23+ type AssistantIdentityField = keyof typeof ASSISTANT_IDENTITY_LIMITS ;
2124
2225export const DEFAULT_ASSISTANT_IDENTITY : AssistantIdentity = {
2326 agentId : "main" ,
@@ -32,26 +35,31 @@ type AssistantIdentity = {
3235 emoji ?: string ;
3336} ;
3437
38+ function normalizeIdentityValue (
39+ field : AssistantIdentityField ,
40+ value : string | undefined ,
41+ ) : string | undefined {
42+ const trimmed = normalizeOptionalString ( value ) ;
43+ return trimmed ? truncateUtf16Safe ( trimmed , ASSISTANT_IDENTITY_LIMITS [ field ] ) : undefined ;
44+ }
45+
3546function isAvatarUrl ( value : string ) : boolean {
3647 return isAvatarHttpUrl ( value ) || isAvatarImageDataUrl ( value ) ;
3748}
3849
50+ // Candidates are already trimmed and field-bounded by normalizeIdentityValue.
3951function normalizeAvatarValue ( value : string | undefined ) : string | undefined {
4052 if ( ! value ) {
4153 return undefined ;
4254 }
43- const trimmed = value . trim ( ) ;
44- if ( ! trimmed ) {
45- return undefined ;
46- }
47- if ( isAvatarUrl ( trimmed ) ) {
48- return trimmed ;
55+ if ( isAvatarUrl ( value ) ) {
56+ return value ;
4957 }
50- if ( looksLikeAvatarPath ( trimmed ) ) {
51- return trimmed ;
58+ if ( looksLikeAvatarPath ( value ) ) {
59+ return value ;
5260 }
53- if ( ! / \s / . test ( trimmed ) && trimmed . length <= 4 ) {
54- return trimmed ;
61+ if ( ! / \s / . test ( value ) && value . length <= 4 ) {
62+ return value ;
5563 }
5664 return undefined ;
5765}
@@ -60,27 +68,20 @@ function normalizeEmojiValue(value: string | undefined): string | undefined {
6068 if ( ! value ) {
6169 return undefined ;
6270 }
63- const trimmed = value . trim ( ) ;
64- if ( ! trimmed ) {
65- return undefined ;
66- }
67- if ( trimmed . length > MAX_ASSISTANT_EMOJI ) {
68- return undefined ;
69- }
7071 let hasNonAscii = false ;
71- for ( let i = 0 ; i < trimmed . length ; i += 1 ) {
72- if ( trimmed . charCodeAt ( i ) > 127 ) {
72+ for ( let i = 0 ; i < value . length ; i += 1 ) {
73+ if ( value . charCodeAt ( i ) > 127 ) {
7374 hasNonAscii = true ;
7475 break ;
7576 }
7677 }
7778 if ( ! hasNonAscii ) {
7879 return undefined ;
7980 }
80- if ( isAvatarUrl ( trimmed ) || looksLikeAvatarPath ( trimmed ) ) {
81+ if ( isAvatarUrl ( value ) || looksLikeAvatarPath ( value ) ) {
8182 return undefined ;
8283 }
83- return trimmed ;
84+ return value ;
8485}
8586
8687/** Resolve the display name/avatar/emoji for an agent-facing assistant identity. */
@@ -97,19 +98,19 @@ export function resolveAssistantIdentity(params: {
9798 const agentIdentity = resolveAgentIdentity ( params . cfg , agentId ) ;
9899 const fileIdentity = workspaceDir ? loadAgentIdentity ( workspaceDir ) : null ;
99100
100- const uiName = coerceIdentityValue ( configAssistant ?. name , MAX_ASSISTANT_NAME ) ;
101- const agentName = coerceIdentityValue ( agentIdentity ?. name , MAX_ASSISTANT_NAME ) ;
102- const fileName = coerceIdentityValue ( fileIdentity ?. name , MAX_ASSISTANT_NAME ) ;
101+ const uiName = normalizeIdentityValue ( "name" , configAssistant ?. name ) ;
102+ const agentName = normalizeIdentityValue ( "name" , agentIdentity ?. name ) ;
103+ const fileName = normalizeIdentityValue ( "name" , fileIdentity ?. name ) ;
103104 const name =
104105 ( isDefaultAgent ? ( uiName ?? agentName ?? fileName ) : ( agentName ?? fileName ?? uiName ) ) ??
105106 DEFAULT_ASSISTANT_IDENTITY . name ;
106107
107- const uiAvatar = coerceIdentityValue ( configAssistant ?. avatar , MAX_ASSISTANT_AVATAR ) ;
108+ const uiAvatar = normalizeIdentityValue ( "avatar" , configAssistant ?. avatar ) ;
108109 const agentAvatarCandidates = [
109- coerceIdentityValue ( agentIdentity ?. avatar , MAX_ASSISTANT_AVATAR ) ,
110- coerceIdentityValue ( agentIdentity ?. emoji , MAX_ASSISTANT_AVATAR ) ,
111- coerceIdentityValue ( fileIdentity ?. avatar , MAX_ASSISTANT_AVATAR ) ,
112- coerceIdentityValue ( fileIdentity ?. emoji , MAX_ASSISTANT_AVATAR ) ,
110+ normalizeIdentityValue ( "avatar" , agentIdentity ?. avatar ) ,
111+ normalizeIdentityValue ( "avatar" , agentIdentity ?. emoji ) ,
112+ normalizeIdentityValue ( "avatar" , fileIdentity ?. avatar ) ,
113+ normalizeIdentityValue ( "avatar" , fileIdentity ?. emoji ) ,
113114 ] ;
114115 const avatarCandidates = isDefaultAgent
115116 ? [ uiAvatar , ...agentAvatarCandidates ]
@@ -119,10 +120,10 @@ export function resolveAssistantIdentity(params: {
119120 DEFAULT_ASSISTANT_IDENTITY . avatar ;
120121
121122 const emojiCandidates = [
122- coerceIdentityValue ( agentIdentity ?. emoji , MAX_ASSISTANT_EMOJI ) ,
123- coerceIdentityValue ( fileIdentity ?. emoji , MAX_ASSISTANT_EMOJI ) ,
124- coerceIdentityValue ( agentIdentity ?. avatar , MAX_ASSISTANT_EMOJI ) ,
125- coerceIdentityValue ( fileIdentity ?. avatar , MAX_ASSISTANT_EMOJI ) ,
123+ normalizeIdentityValue ( "emoji" , agentIdentity ?. emoji ) ,
124+ normalizeIdentityValue ( "emoji" , fileIdentity ?. emoji ) ,
125+ normalizeIdentityValue ( "emoji" , agentIdentity ?. avatar ) ,
126+ normalizeIdentityValue ( "emoji" , fileIdentity ?. avatar ) ,
126127 ] ;
127128 const emoji = emojiCandidates . map ( ( candidate ) => normalizeEmojiValue ( candidate ) ) . find ( Boolean ) ;
128129
0 commit comments