



























@@ -0,0 +1,212 @@
1+import fs from "node:fs/promises";
2+import os from "node:os";
3+import path from "node:path";
4+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5+import type { OpenClawConfig } from "../../config/types.openclaw.js";
6+import { getAgentScopedMediaLocalRoots } from "../../media/local-roots.js";
7+import { createManagedOutgoingImageBlocks } from "../managed-image-attachments.js";
8+import { normalizeWebchatReplyMediaPathsForDisplay } from "./chat-reply-media.js";
9+10+const PNG_BYTES = Buffer.from(
11+"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII=",
12+"base64",
13+);
14+15+describe("normalizeWebchatReplyMediaPathsForDisplay", () => {
16+let rootDir = "";
17+18+beforeEach(async () => {
19+rootDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-webchat-reply-media-"));
20+vi.stubEnv("OPENCLAW_STATE_DIR", path.join(rootDir, "state"));
21+});
22+23+afterEach(async () => {
24+vi.unstubAllEnvs();
25+await fs.rm(rootDir, { recursive: true, force: true });
26+rootDir = "";
27+});
28+29+function createConfig(params: {
30+agentDir: string;
31+workspaceDir: string;
32+allowRead: boolean;
33+}): OpenClawConfig {
34+return {
35+tools: params.allowRead ? { allow: ["read"] } : { fs: { workspaceOnly: true } },
36+agents: {
37+list: [
38+{
39+id: "main",
40+agentDir: params.agentDir,
41+workspace: params.workspaceDir,
42+},
43+],
44+},
45+};
46+}
47+48+async function createCodexHomeImage(params: { agentDir: string }): Promise<string> {
49+const imagePath = path.join(params.agentDir, "codex-home", "outputs", "chart.png");
50+await fs.mkdir(path.dirname(imagePath), { recursive: true });
51+await fs.writeFile(imagePath, PNG_BYTES);
52+return imagePath;
53+}
54+55+it("stages Codex-home image paths before Gateway managed-image display", async () => {
56+const stateDir = process.env.OPENCLAW_STATE_DIR ?? "";
57+const agentDir = path.join(stateDir, "agents", "main", "agent");
58+const workspaceDir = path.join(stateDir, "workspace");
59+const sourcePath = await createCodexHomeImage({ agentDir });
60+const cfg = createConfig({ agentDir, workspaceDir, allowRead: true });
61+62+const [payload] = await normalizeWebchatReplyMediaPathsForDisplay({
63+ cfg,
64+sessionKey: "agent:main:webchat:direct:user",
65+agentId: "main",
66+payloads: [{ mediaUrls: [sourcePath] }],
67+});
68+69+const normalizedPath = payload?.mediaUrls?.[0];
70+expect(normalizedPath).toBeTruthy();
71+expect(normalizedPath).not.toBe(sourcePath);
72+expect(normalizedPath?.startsWith(path.join(stateDir, "media"))).toBe(true);
73+const blocks = await createManagedOutgoingImageBlocks({
74+sessionKey: "agent:main:webchat:direct:user",
75+mediaUrls: payload?.mediaUrls ?? [],
76+localRoots: getAgentScopedMediaLocalRoots(cfg, "main"),
77+});
78+79+expect(blocks).toHaveLength(1);
80+expect((blocks[0] as { type?: string }).type).toBe("image");
81+});
82+83+it("does not expose Codex-home media when host read policy is not enabled", async () => {
84+const stateDir = process.env.OPENCLAW_STATE_DIR ?? "";
85+const agentDir = path.join(stateDir, "agents", "main", "agent");
86+const workspaceDir = path.join(stateDir, "workspace");
87+const sourcePath = await createCodexHomeImage({ agentDir });
88+const cfg = createConfig({ agentDir, workspaceDir, allowRead: false });
89+90+const [payload] = await normalizeWebchatReplyMediaPathsForDisplay({
91+ cfg,
92+sessionKey: "agent:main:webchat:direct:user",
93+agentId: "main",
94+payloads: [{ mediaUrls: [sourcePath] }],
95+});
96+97+expect(payload?.mediaUrl).toBeUndefined();
98+expect(payload?.mediaUrls).toBeUndefined();
99+expect(payload?.text).toBeTruthy();
100+});
101+102+it("does not stage sensitive media before display suppression", async () => {
103+const stateDir = process.env.OPENCLAW_STATE_DIR ?? "";
104+const agentDir = path.join(stateDir, "agents", "main", "agent");
105+const workspaceDir = path.join(stateDir, "workspace");
106+const sourcePath = await createCodexHomeImage({ agentDir });
107+const cfg = createConfig({ agentDir, workspaceDir, allowRead: true });
108+109+const [payload] = await normalizeWebchatReplyMediaPathsForDisplay({
110+ cfg,
111+sessionKey: "agent:main:webchat:direct:user",
112+agentId: "main",
113+payloads: [{ mediaUrls: [sourcePath], sensitiveMedia: true }],
114+});
115+116+expect(payload?.mediaUrl).toBeUndefined();
117+expect(payload?.mediaUrls).toEqual([sourcePath]);
118+await expect(fs.stat(path.join(stateDir, "media", "outbound"))).rejects.toThrow();
119+});
120+121+it("preserves inline data image replies for WebChat rendering", async () => {
122+const stateDir = process.env.OPENCLAW_STATE_DIR ?? "";
123+const agentDir = path.join(stateDir, "agents", "main", "agent");
124+const workspaceDir = path.join(stateDir, "workspace");
125+const dataUrl = `data:image/png;base64,${PNG_BYTES.toString("base64")}`;
126+const cfg = createConfig({ agentDir, workspaceDir, allowRead: true });
127+128+const [payload] = await normalizeWebchatReplyMediaPathsForDisplay({
129+ cfg,
130+sessionKey: "agent:main:webchat:direct:user",
131+agentId: "main",
132+payloads: [{ mediaUrls: [dataUrl] }],
133+});
134+135+expect(payload?.mediaUrl).toBeUndefined();
136+expect(payload?.mediaUrls).toEqual([dataUrl]);
137+await expect(fs.stat(path.join(stateDir, "media", "outbound"))).rejects.toThrow();
138+});
139+140+it("preserves local audio paths for WebChat audio embedding", async () => {
141+const stateDir = process.env.OPENCLAW_STATE_DIR ?? "";
142+const agentDir = path.join(stateDir, "agents", "main", "agent");
143+const workspaceDir = path.join(stateDir, "workspace");
144+const audioPath = path.join(workspaceDir, "voice.mp3");
145+await fs.mkdir(path.dirname(audioPath), { recursive: true });
146+await fs.writeFile(audioPath, Buffer.from([0xff, 0xfb, 0x90, 0x00]));
147+const cfg = createConfig({ agentDir, workspaceDir, allowRead: false });
148+149+const [payload] = await normalizeWebchatReplyMediaPathsForDisplay({
150+ cfg,
151+sessionKey: "agent:main:webchat:direct:user",
152+agentId: "main",
153+payloads: [{ mediaUrls: [audioPath], trustedLocalMedia: true, audioAsVoice: true }],
154+});
155+156+expect(payload?.mediaUrl).toBeUndefined();
157+expect(payload?.mediaUrls).toEqual([audioPath]);
158+expect(payload?.trustedLocalMedia).toBe(true);
159+expect(payload?.audioAsVoice).toBe(true);
160+await expect(fs.stat(path.join(stateDir, "media", "outbound"))).rejects.toThrow();
161+});
162+163+it("preserves data images while staging mixed local image replies", async () => {
164+const stateDir = process.env.OPENCLAW_STATE_DIR ?? "";
165+const agentDir = path.join(stateDir, "agents", "main", "agent");
166+const workspaceDir = path.join(stateDir, "workspace");
167+const sourcePath = await createCodexHomeImage({ agentDir });
168+const dataUrl = `data:image/png;base64,${PNG_BYTES.toString("base64")}`;
169+const cfg = createConfig({ agentDir, workspaceDir, allowRead: true });
170+171+const [payload] = await normalizeWebchatReplyMediaPathsForDisplay({
172+ cfg,
173+sessionKey: "agent:main:webchat:direct:user",
174+agentId: "main",
175+payloads: [{ mediaUrls: [dataUrl, sourcePath] }],
176+});
177+178+const normalizedLocalPath = payload?.mediaUrls?.[1];
179+expect(payload?.mediaUrls?.[0]).toBe(dataUrl);
180+expect(normalizedLocalPath).toBeTruthy();
181+expect(normalizedLocalPath).not.toBe(sourcePath);
182+expect(normalizedLocalPath?.startsWith(path.join(stateDir, "media"))).toBe(true);
183+const blocks = await createManagedOutgoingImageBlocks({
184+sessionKey: "agent:main:webchat:direct:user",
185+mediaUrls: payload?.mediaUrls ?? [],
186+localRoots: getAgentScopedMediaLocalRoots(cfg, "main"),
187+});
188+189+expect(blocks).toHaveLength(2);
190+});
191+192+it("does not add a failure warning when a mixed inline image survives", async () => {
193+const stateDir = process.env.OPENCLAW_STATE_DIR ?? "";
194+const agentDir = path.join(stateDir, "agents", "main", "agent");
195+const workspaceDir = path.join(stateDir, "workspace");
196+const sourcePath = await createCodexHomeImage({ agentDir });
197+const dataUrl = `data:image/png;base64,${PNG_BYTES.toString("base64")}`;
198+const cfg = createConfig({ agentDir, workspaceDir, allowRead: false });
199+200+const [payload] = await normalizeWebchatReplyMediaPathsForDisplay({
201+ cfg,
202+sessionKey: "agent:main:webchat:direct:user",
203+agentId: "main",
204+payloads: [{ mediaUrls: [sourcePath, dataUrl] }],
205+});
206+207+expect(payload?.text).toBeUndefined();
208+expect(payload?.mediaUrl).toBe(dataUrl);
209+expect(payload?.mediaUrls).toEqual([dataUrl]);
210+await expect(fs.stat(path.join(stateDir, "media", "outbound"))).rejects.toThrow();
211+});
212+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。