From b5ada806dddc485e508bbde3b96191e2fe4cd67a Mon Sep 17 00:00:00 2001 From: haoxingjun Date: Fri, 22 May 2026 10:54:38 +0800 Subject: [PATCH] fix: hydrate current turn image attachments --- src/auto-reply/reply/current-turn-images.ts | 29 +++++++++++++++++-- .../reply/get-reply-run.media-only.test.ts | 4 +-- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/auto-reply/reply/current-turn-images.ts b/src/auto-reply/reply/current-turn-images.ts index 5ace9b6ae74..fbb551010cb 100644 --- a/src/auto-reply/reply/current-turn-images.ts +++ b/src/auto-reply/reply/current-turn-images.ts @@ -2,6 +2,7 @@ import type { ImageContent } from "@earendil-works/pi-ai"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { logVerbose } from "../../globals.js"; import { formatErrorMessage } from "../../infra/errors.js"; +import { mimeTypeFromFilePath } from "../../media/mime.js"; import type { PromptImageOrderEntry } from "../../media/prompt-image-order.js"; import { normalizeOptionalString } from "../../shared/string-coerce.js"; import type { MsgContext } from "../templating.js"; @@ -13,6 +14,30 @@ type CurrentImageAttachment = { mediaType: string; }; +function isGenericMediaType(mediaType: string | undefined): boolean { + if (!mediaType) { + return true; + } + const normalized = mediaType.split(";")[0]?.trim().toLowerCase(); + return normalized === "application/octet-stream" || normalized === "binary/octet-stream"; +} + +function resolveCurrentImageMediaType(pathValue: unknown, mediaType?: unknown): string | undefined { + const mediaPath = normalizeOptionalString(pathValue); + if (!mediaPath) { + return undefined; + } + const normalizedMediaType = normalizeOptionalString(mediaType); + if (normalizedMediaType?.startsWith("image/")) { + return normalizedMediaType; + } + if (!isGenericMediaType(normalizedMediaType)) { + return undefined; + } + const inferredType = mimeTypeFromFilePath(mediaPath); + return inferredType?.startsWith("image/") ? inferredType : undefined; +} + function collectCurrentImageAttachments(ctx: MsgContext): CurrentImageAttachment[] { const pathsFromArray = Array.isArray(ctx.MediaPaths) ? ctx.MediaPaths : undefined; const paths = @@ -31,8 +56,8 @@ function collectCurrentImageAttachments(ctx: MsgContext): CurrentImageAttachment const attachments: CurrentImageAttachment[] = []; for (const [index, pathValue] of paths.entries()) { const mediaPath = normalizeOptionalString(pathValue); - const mediaType = normalizeOptionalString(types?.[index] ?? ctx.MediaType); - if (mediaPath && mediaType?.startsWith("image/")) { + const mediaType = resolveCurrentImageMediaType(pathValue, types?.[index] ?? ctx.MediaType); + if (mediaPath && mediaType) { attachments.push({ index, path: mediaPath, mediaType }); } } diff --git a/src/auto-reply/reply/get-reply-run.media-only.test.ts b/src/auto-reply/reply/get-reply-run.media-only.test.ts index 06d2f530e61..f666dcd8d32 100644 --- a/src/auto-reply/reply/get-reply-run.media-only.test.ts +++ b/src/auto-reply/reply/get-reply-run.media-only.test.ts @@ -878,7 +878,7 @@ describe("runPreparedReply media-only handling", () => { expect(call?.followupRun.prompt).toContain("[User sent media without caption]"); }); - it("hydrates current MediaPaths into queued followup images", async () => { + it("hydrates current image MediaPaths by extension when MediaTypes are missing", async () => { const tmpDir = await mkdtemp(path.join(os.tmpdir(), "openclaw-followup-image-")); cleanupPaths.push(tmpDir); const imagePath = path.join(tmpDir, "inbound.png"); @@ -897,7 +897,6 @@ describe("runPreparedReply media-only handling", () => { RawBody: "describe this", CommandBody: "describe this", MediaPaths: [imagePath], - MediaTypes: ["image/png"], MediaWorkspaceDir: tmpDir, OriginatingChannel: "discord", OriginatingTo: "C123", @@ -911,7 +910,6 @@ describe("runPreparedReply media-only handling", () => { OriginatingTo: "C123", ChatType: "group", MediaPaths: [imagePath], - MediaTypes: ["image/png"], MediaWorkspaceDir: tmpDir, }, }),