

















@@ -50,12 +50,14 @@ registerGetReplyRuntimeOverrides(mocks);
5050let getReplyFromConfig: typeof import("./get-reply.js").getReplyFromConfig;
5151let resolveDefaultModelMock: typeof import("./directive-handling.defaults.js").resolveDefaultModel;
5252let runPreparedReplyMock: typeof import("./get-reply-run.js").runPreparedReply;
53+let stageSandboxMediaMock: typeof import("./stage-sandbox-media.runtime.js").stageSandboxMedia;
53545455async function loadGetReplyRuntimeForTest() {
5556({ getReplyFromConfig } = await loadGetReplyModuleForTest({ cacheKey: import.meta.url }));
5657({ resolveDefaultModel: resolveDefaultModelMock } =
5758await import("./directive-handling.defaults.js"));
5859({ runPreparedReply: runPreparedReplyMock } = await import("./get-reply-run.js"));
60+({ stageSandboxMedia: stageSandboxMediaMock } = await import("./stage-sandbox-media.runtime.js"));
5961}
60626163function emptyAliasIndex() {
@@ -101,6 +103,7 @@ describe("getReplyFromConfig message hooks", () => {
101103mocks.initSessionState.mockReset();
102104vi.mocked(resolveDefaultModelMock).mockReset();
103105vi.mocked(runPreparedReplyMock).mockReset();
106+vi.mocked(stageSandboxMediaMock).mockReset();
104107vi.mocked(logVerbose).mockReset();
105108106109mocks.applyMediaUnderstanding.mockImplementation(async (...args: unknown[]) => {
@@ -141,6 +144,7 @@ describe("getReplyFromConfig message hooks", () => {
141144aliasIndex: emptyAliasIndex(),
142145});
143146vi.mocked(runPreparedReplyMock).mockResolvedValue({ text: "ok" });
147+vi.mocked(stageSandboxMediaMock).mockResolvedValue({ staged: new Map() });
144148mocks.initSessionState.mockResolvedValue(
145149createGetReplySessionState({
146150sessionKey: "agent:main:telegram:-100123",
@@ -281,6 +285,70 @@ describe("getReplyFromConfig message hooks", () => {
281285text: "a tiny dot image",
282286}),
283287]);
288+expect(stageSandboxMediaMock).not.toHaveBeenCalled();
289+});
290+291+it("stages remote iMessage media before media understanding", async () => {
292+const order: string[] = [];
293+const remotePath = "/Users/demo/Library/Messages/Attachments/ab/cd/photo.jpg";
294+const stagedPath = "/tmp/openclaw-remote-cache/photo.jpg";
295+vi.mocked(stageSandboxMediaMock).mockImplementationOnce(async (params) => {
296+order.push("stage");
297+params.ctx.MediaPath = stagedPath;
298+params.ctx.MediaPaths = [stagedPath];
299+params.ctx.MediaUrl = stagedPath;
300+params.ctx.MediaUrls = [stagedPath];
301+params.sessionCtx.MediaPath = stagedPath;
302+params.sessionCtx.MediaPaths = [stagedPath];
303+params.sessionCtx.MediaUrl = stagedPath;
304+params.sessionCtx.MediaUrls = [stagedPath];
305+return { staged: new Map([[remotePath, stagedPath]]) };
306+});
307+mocks.applyMediaUnderstanding.mockImplementationOnce(async (...args: unknown[]) => {
308+order.push("understand");
309+const { ctx } = args[0] as { ctx: MsgContext };
310+expect(ctx.MediaPath).toBe(stagedPath);
311+expect(ctx.MediaPaths).toEqual([stagedPath]);
312+expect(ctx.MediaUrl).toBe(stagedPath);
313+expect(ctx.MediaUrls).toEqual([stagedPath]);
314+expect(ctx.MediaStaged).toBe(true);
315+});
316+317+await getReplyFromConfig(
318+buildCtx({
319+Provider: "imessage",
320+Surface: "imessage",
321+OriginatingChannel: "imessage",
322+OriginatingTo: "imessage:chat:abc",
323+ChatType: "direct",
324+Body: "please describe this",
325+BodyForAgent: "please describe this",
326+RawBody: "please describe this",
327+CommandBody: "please describe this",
328+BodyForCommands: "please describe this",
329+SessionKey: "agent:main:imessage:direct:user",
330+From: "imessage:user",
331+To: "imessage:chat:abc",
332+MediaPath: remotePath,
333+MediaPaths: [remotePath],
334+MediaUrl: remotePath,
335+MediaUrls: [remotePath],
336+MediaType: "image/jpeg",
337+MediaTypes: ["image/jpeg"],
338+MediaRemoteHost: "user@gateway-host",
339+}),
340+undefined,
341+withFastReplyConfig({}),
342+);
343+344+expect(order).toEqual(["stage", "understand"]);
345+expect(stageSandboxMediaMock).toHaveBeenCalledTimes(1);
346+expect(stageSandboxMediaMock).toHaveBeenCalledWith(
347+expect.objectContaining({
348+sessionKey: "agent:main:imessage:direct:user",
349+workspaceDir: "/tmp/workspace",
350+}),
351+);
284352});
285353286354it("emits only preprocessed when no transcript is produced", async () => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。