



























@@ -3,8 +3,10 @@ import path from "node:path";
33import type { OpenClawConfig } from "../config/types.openclaw.js";
44import {
55AVATAR_MAX_BYTES,
6+hasAvatarUriScheme,
67isAvatarDataUrl,
78isAvatarHttpUrl,
9+isWindowsAbsolutePath,
810isPathWithinRoot,
911isSupportedLocalAvatarExtension,
1012} from "../shared/avatar-policy.js";
@@ -15,10 +17,18 @@ import { loadAgentIdentityFromWorkspace } from "./identity-file.js";
1517import { resolveAgentIdentity } from "./identity.js";
16181719export type AgentAvatarResolution =
18-| { kind: "none"; reason: string }
19-| { kind: "local"; filePath: string }
20-| { kind: "remote"; url: string }
21-| { kind: "data"; url: string };
20+| { kind: "none"; reason: string; source?: string }
21+| { kind: "local"; filePath: string; source: string }
22+| { kind: "remote"; url: string; source: string }
23+| { kind: "data"; url: string; source: string };
24+25+type AgentAvatarPublicSourceInput = {
26+kind: AgentAvatarResolution["kind"];
27+source?: string | null;
28+};
29+30+const PUBLIC_AVATAR_SOURCE_MAX_CHARS = 256;
31+const PUBLIC_DATA_AVATAR_HEADER_MAX_CHARS = 64;
22322333function resolveAvatarSource(
2434cfg: OpenClawConfig,
@@ -80,6 +90,42 @@ function resolveLocalAvatarPath(params: {
8090return { ok: true, filePath: realPath };
8191}
829293+function isSafeRelativeAvatarSource(source: string): boolean {
94+if (
95+source.length > PUBLIC_AVATAR_SOURCE_MAX_CHARS ||
96+source.startsWith("~") ||
97+path.isAbsolute(source) ||
98+isWindowsAbsolutePath(source) ||
99+(hasAvatarUriScheme(source) && !isWindowsAbsolutePath(source)) ||
100+source.includes("\0")
101+) {
102+return false;
103+}
104+const parts = source.replace(/\\/g, "/").split("/");
105+return parts.every((part) => part !== "..");
106+}
107+108+export function resolvePublicAgentAvatarSource(
109+resolved: AgentAvatarPublicSourceInput,
110+): string | undefined {
111+const source = normalizeOptionalString(resolved.source) ?? null;
112+if (!source) {
113+return undefined;
114+}
115+if (isAvatarDataUrl(source)) {
116+const commaIndex = source.indexOf(",");
117+const header =
118+commaIndex > 0
119+ ? source.slice(0, Math.min(commaIndex, PUBLIC_DATA_AVATAR_HEADER_MAX_CHARS))
120+ : source.slice(0, PUBLIC_DATA_AVATAR_HEADER_MAX_CHARS);
121+return `${header},...`;
122+}
123+if (isAvatarHttpUrl(source)) {
124+return "remote URL";
125+}
126+return isSafeRelativeAvatarSource(source) ? source : undefined;
127+}
128+83129export function resolveAgentAvatar(
84130cfg: OpenClawConfig,
85131agentId: string,
@@ -90,15 +136,15 @@ export function resolveAgentAvatar(
90136return { kind: "none", reason: "missing" };
91137}
92138if (isAvatarHttpUrl(source)) {
93-return { kind: "remote", url: source };
139+return { kind: "remote", url: source, source };
94140}
95141if (isAvatarDataUrl(source)) {
96-return { kind: "data", url: source };
142+return { kind: "data", url: source, source };
97143}
98144const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
99145const resolved = resolveLocalAvatarPath({ raw: source, workspaceDir });
100146if (!resolved.ok) {
101-return { kind: "none", reason: resolved.reason };
147+return { kind: "none", reason: resolved.reason, source };
102148}
103-return { kind: "local", filePath: resolved.filePath };
149+return { kind: "local", filePath: resolved.filePath, source };
104150}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。