


























@@ -794,28 +794,45 @@ function buildChatSendTranscriptMessage(params: {
794794};
795795}
796796797-// Stages non-image offloads into the agent sandbox synchronously so chat.send
797+function stripTrailingOffloadedMediaMarkers(message: string, refs: OffloadedRef[]): string {
798+if (refs.length === 0) {
799+return message;
800+}
801+const removableRefs = new Set(refs.map((ref) => ref.mediaRef));
802+const lines = message.split(/\r?\n/);
803+while (lines.length > 0) {
804+const last = lines[lines.length - 1]?.trim() ?? "";
805+const match = /^\[media attached:\s*(media:\/\/inbound\/[^\]\s]+)\]$/.exec(last);
806+if (!match?.[1] || !removableRefs.delete(match[1])) {
807+break;
808+}
809+lines.pop();
810+}
811+return lines.join("\n").trimEnd();
812+}
813+814+// Stages media-path offloads into the agent sandbox synchronously so chat.send
798815// can surface 5xx before respond(). Throws MediaOffloadError on any staging
799816// failure (ENOSPC / EPERM / partial-stage) so the outer chat.send handler can
800817// map it to UNAVAILABLE (5xx); plain Error would be misclassified as 4xx. All
801818// offloaded refs are cleaned up from the media store before rethrow.
802819// Callers MUST set ctx.MediaStaged=true when this runs so the dispatch
803820// pipeline skips its own stageSandboxMedia pass.
804821//
805-// Returned paths are ABSOLUTE (pointing into the sandbox workspace when sandbox
806-// is enabled, or the media-store origin when it is not). applyMediaUnderstanding
807-// runs before any further staging in get-reply.ts and uses
808-// `path.isAbsolute(raw) ? raw : path.resolve(raw)` against the gateway CWD, so
809-// any relative path here would make media-understanding target the wrong host
810-// path and silently skip file analysis.
811-async function prestageNonImageOffloads(params: {
822+// Returned paths are absolute media-store paths when no sandbox is active, or
823+// sandbox-relative paths plus `workspaceDir` when sandboxing is active. Host-side
824+// media-understanding uses MediaWorkspaceDir to resolve those relative paths.
825+async function prestageMediaPathOffloads(params: {
812826offloadedRefs: OffloadedRef[];
827+includeImageRefs?: boolean;
813828cfg: OpenClawConfig;
814829sessionKey: string;
815830agentId: string;
816831}): Promise<{ paths: string[]; types: string[]; workspaceDir?: string }> {
817-const nonImage = params.offloadedRefs.filter((ref) => !ref.mimeType.startsWith("image/"));
818-if (nonImage.length === 0) {
832+const mediaPathRefs = params.offloadedRefs.filter(
833+(ref) => params.includeImageRefs || !ref.mimeType.startsWith("image/"),
834+);
835+if (mediaPathRefs.length === 0) {
819836return { paths: [], types: [] };
820837}
821838@@ -828,33 +845,33 @@ async function prestageNonImageOffloads(params: {
828845});
829846if (!sandbox) {
830847return {
831-paths: nonImage.map((ref) => ref.path),
832-types: nonImage.map((ref) => ref.mimeType),
848+paths: mediaPathRefs.map((ref) => ref.path),
849+types: mediaPathRefs.map((ref) => ref.mimeType),
833850};
834851}
835852836853// stageSandboxMedia caps each file at STAGED_MEDIA_MAX_BYTES (=
837854// MEDIA_MAX_BYTES, 5MB) and silently skips oversized files. The parse cap
838855// (resolveChatAttachmentMaxBytes, default 20MB) is higher, so a sandboxed
839-// session receiving a non-image file between the two caps would otherwise
856+// session receiving a file between the two caps would otherwise
840857// pass parse, fail staging, and surface as a retryable 5xx even though
841858// retry cannot succeed. Reject here as a client-side 4xx instead.
842-const oversizedForSandbox = nonImage.filter((ref) => ref.sizeBytes > MEDIA_MAX_BYTES);
859+const oversizedForSandbox = mediaPathRefs.filter((ref) => ref.sizeBytes > MEDIA_MAX_BYTES);
843860if (oversizedForSandbox.length > 0) {
844861const details = oversizedForSandbox
845862.map((ref) => `${ref.label} (${ref.sizeBytes} bytes)`)
846863.join(", ");
847864throw new UnsupportedAttachmentError(
848865"non-image-too-large-for-sandbox",
849-`non-image attachments exceed sandbox staging limit (${MEDIA_MAX_BYTES} bytes): ${details}`,
866+`attachments exceed sandbox staging limit (${MEDIA_MAX_BYTES} bytes): ${details}`,
850867);
851868}
852869853870const stagingCtx: MsgContext = {
854-MediaPath: nonImage[0].path,
855-MediaPaths: nonImage.map((ref) => ref.path),
856-MediaType: nonImage[0].mimeType,
857-MediaTypes: nonImage.map((ref) => ref.mimeType),
871+MediaPath: mediaPathRefs[0].path,
872+MediaPaths: mediaPathRefs.map((ref) => ref.path),
873+MediaType: mediaPathRefs[0].mimeType,
874+MediaTypes: mediaPathRefs.map((ref) => ref.mimeType),
858875};
859876const stageResult = await stageSandboxMedia({
860877ctx: stagingCtx,
@@ -871,14 +888,14 @@ async function prestageNonImageOffloads(params: {
871888// (STAGED_MEDIA_MAX_BYTES = 5MB); check the returned `staged` map so any
872889// missing source becomes a 5xx MediaOffloadError the client can retry.
873890const stagedSources = stageResult.staged;
874-const missing = nonImage.filter((ref) => !stagedSources.has(ref.path));
891+const missing = mediaPathRefs.filter((ref) => !stagedSources.has(ref.path));
875892if (missing.length > 0) {
876893throw new Error(
877-`non-image attachment staging incomplete: ${stagedSources.size}/${nonImage.length} paths staged into sandbox workspace (missing: ${missing.map((ref) => ref.path).join(", ")})`,
894+`attachment staging incomplete: ${stagedSources.size}/${mediaPathRefs.length} paths staged into sandbox workspace (missing: ${missing.map((ref) => ref.path).join(", ")})`,
878895);
879896}
880897const stagedPaths = stagingCtx.MediaPaths ?? [];
881-const stagedTypes = stagingCtx.MediaTypes ?? nonImage.map((ref) => ref.mimeType);
898+const stagedTypes = stagingCtx.MediaTypes ?? mediaPathRefs.map((ref) => ref.mimeType);
882899883900// Keep stagedPaths sandbox-relative (e.g. `media/inbound/foo.pdf`) so the
884901// agent inside the container can read them. Host-side media-understanding
@@ -897,7 +914,7 @@ async function prestageNonImageOffloads(params: {
897914throw err;
898915}
899916throw new MediaOffloadError(
900-`[Gateway Error] Failed to stage non-image attachments into agent workspace: ${formatErrorMessage(err)}`,
917+`[Gateway Error] Failed to stage attachments into agent workspace: ${formatErrorMessage(err)}`,
901918{ cause: err },
902919);
903920}
@@ -1896,9 +1913,9 @@ export const chatHandlers: GatewayRequestHandlers = {
18961913let parsedImages: ChatImageContent[] = [];
18971914let imageOrder: PromptImageOrderEntry[] = [];
18981915let offloadedRefs: OffloadedRef[] = [];
1899-let nonImageMediaPaths: string[] = [];
1900-let nonImageMediaTypes: string[] = [];
1901-let nonImageMediaWorkspaceDir: string | undefined;
1916+let mediaPathOffloadPaths: string[] = [];
1917+let mediaPathOffloadTypes: string[] = [];
1918+let mediaPathOffloadWorkspaceDir: string | undefined;
19021919const timeoutMs = resolveAgentTimeoutMs({
19031920 cfg,
19041921overrideMs: p.timeoutMs,
@@ -1971,25 +1988,35 @@ export const chatHandlers: GatewayRequestHandlers = {
19711988supportsSessionModelImages ||
19721989explicitOriginTargetsAcpSession(explicitOriginResult.value) ||
19731990explicitOriginTargetsPlugin;
1991+const routeImageOffloadsAsMediaPaths = !supportsImages;
19741992try {
19751993const parsed = await parseMessageWithAttachments(inboundMessage, normalizedAttachments, {
19761994maxBytes: resolveChatAttachmentMaxBytes(cfg),
19771995log: context.logGateway,
19781996 supportsImages,
1979-// chat.send routes non-image offloadedRefs into ctx.MediaPaths below
1997+// chat.send routes selected offloadedRefs into ctx.MediaPaths below
19801998// so the auto-reply stage pipeline can surface them to the agent.
19811999acceptNonImage: true,
19822000});
1983-parsedMessage = parsed.message;
2001+parsedMessage = stripTrailingOffloadedMediaMarkers(
2002+parsed.message,
2003+routeImageOffloadsAsMediaPaths
2004+ ? parsed.offloadedRefs.filter((ref) => ref.mimeType.startsWith("image/"))
2005+ : [],
2006+);
19842007parsedImages = parsed.images;
1985-imageOrder = parsed.imageOrder;
2008+imageOrder = routeImageOffloadsAsMediaPaths ? [] : parsed.imageOrder;
19862009offloadedRefs = parsed.offloadedRefs;
19872010({
1988-paths: nonImageMediaPaths,
1989-types: nonImageMediaTypes,
1990-workspaceDir: nonImageMediaWorkspaceDir,
1991-} = await prestageNonImageOffloads({
2011+paths: mediaPathOffloadPaths,
2012+types: mediaPathOffloadTypes,
2013+workspaceDir: mediaPathOffloadWorkspaceDir,
2014+} = await prestageMediaPathOffloads({
19922015 offloadedRefs,
2016+// Text-only image offloads need ctx.MediaPaths so media-understanding
2017+// can describe them via agents.defaults.imageModel. Vision-capable
2018+// image offloads stay as prompt refs for native image loading.
2019+includeImageRefs: routeImageOffloadsAsMediaPaths,
19932020 cfg,
19942021 sessionKey,
19952022 agentId,
@@ -2100,17 +2127,17 @@ export const chatHandlers: GatewayRequestHandlers = {
21002127GatewayClientScopes: client?.connect?.scopes ?? [],
21012128 ...pluginBoundMediaFields,
21022129};
2103-if (nonImageMediaPaths.length > 0) {
2104-// Inject non-image offloads via the same MsgContext fields the channel
2130+if (mediaPathOffloadPaths.length > 0) {
2131+// Inject offloads via the same MsgContext fields the channel
21052132// path uses so buildInboundMediaNote renders a real `[media attached:
21062133// <workspace-relative-path>]` line into the agent prompt. Marker
21072134// blocks the dispatch pipeline from re-running stageSandboxMedia; see
2108-// prestageNonImageOffloads.
2109-ctx.MediaPath = nonImageMediaPaths[0];
2110-ctx.MediaPaths = nonImageMediaPaths;
2111-ctx.MediaType = nonImageMediaTypes[0];
2112-ctx.MediaTypes = nonImageMediaTypes;
2113-ctx.MediaWorkspaceDir = nonImageMediaWorkspaceDir;
2135+// prestageMediaPathOffloads.
2136+ctx.MediaPath = mediaPathOffloadPaths[0];
2137+ctx.MediaPaths = mediaPathOffloadPaths;
2138+ctx.MediaType = mediaPathOffloadTypes[0];
2139+ctx.MediaTypes = mediaPathOffloadTypes;
2140+ctx.MediaWorkspaceDir = mediaPathOffloadWorkspaceDir;
21142141ctx.MediaStaged = true;
21152142}
21162143此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。