From 9f9978635c844fb78ea6ba81c5ce74a1f5cdbac3 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 15 Feb 2026 13:30:37 +0000 Subject: [PATCH] refactor(gateway): share rpc attachment normalization --- src/gateway/server-methods/agent.ts | 20 ++---------- .../attachment-normalize.test.ts | 19 +++++++++++ .../server-methods/attachment-normalize.ts | 32 +++++++++++++++++++ src/gateway/server-methods/chat.ts | 20 ++---------- 4 files changed, 55 insertions(+), 36 deletions(-) create mode 100644 src/gateway/server-methods/attachment-normalize.test.ts create mode 100644 src/gateway/server-methods/attachment-normalize.ts diff --git a/src/gateway/server-methods/agent.ts b/src/gateway/server-methods/agent.ts index fed4d190f..5d4c8773f 100644 --- a/src/gateway/server-methods/agent.ts +++ b/src/gateway/server-methods/agent.ts @@ -48,6 +48,7 @@ import { import { formatForLog } from "../ws-log.js"; import { waitForAgentJob } from "./agent-job.js"; import { injectTimestamp, timestampOptsFromConfig } from "./agent-timestamp.js"; +import { normalizeRpcAttachmentsToChatAttachments } from "./attachment-normalize.js"; import { sessionsHandlers } from "./sessions.js"; const RESET_COMMAND_RE = /^\/(new|reset)(?:\s+([\s\S]*))?$/i; @@ -213,24 +214,7 @@ export const agentHandlers: GatewayRequestHandlers = { }); return; } - const normalizedAttachments = - request.attachments - ?.map((a) => ({ - type: typeof a?.type === "string" ? a.type : undefined, - mimeType: typeof a?.mimeType === "string" ? a.mimeType : undefined, - fileName: typeof a?.fileName === "string" ? a.fileName : undefined, - content: - typeof a?.content === "string" - ? a.content - : ArrayBuffer.isView(a?.content) - ? Buffer.from( - a.content.buffer, - a.content.byteOffset, - a.content.byteLength, - ).toString("base64") - : undefined, - })) - .filter((a) => a.content) ?? []; + const normalizedAttachments = normalizeRpcAttachmentsToChatAttachments(request.attachments); let message = request.message.trim(); let images: Array<{ type: "image"; data: string; mimeType: string }> = []; diff --git a/src/gateway/server-methods/attachment-normalize.test.ts b/src/gateway/server-methods/attachment-normalize.test.ts new file mode 100644 index 000000000..159bae804 --- /dev/null +++ b/src/gateway/server-methods/attachment-normalize.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from "vitest"; +import { normalizeRpcAttachmentsToChatAttachments } from "./attachment-normalize.js"; + +describe("normalizeRpcAttachmentsToChatAttachments", () => { + it("passes through string content", () => { + const res = normalizeRpcAttachmentsToChatAttachments([ + { type: "file", mimeType: "image/png", fileName: "a.png", content: "Zm9v" }, + ]); + expect(res).toEqual([ + { type: "file", mimeType: "image/png", fileName: "a.png", content: "Zm9v" }, + ]); + }); + + it("converts Uint8Array content to base64", () => { + const bytes = new TextEncoder().encode("foo"); + const res = normalizeRpcAttachmentsToChatAttachments([{ content: bytes }]); + expect(res[0]?.content).toBe("Zm9v"); + }); +}); diff --git a/src/gateway/server-methods/attachment-normalize.ts b/src/gateway/server-methods/attachment-normalize.ts new file mode 100644 index 000000000..b8eb00926 --- /dev/null +++ b/src/gateway/server-methods/attachment-normalize.ts @@ -0,0 +1,32 @@ +import type { ChatAttachment } from "../chat-attachments.js"; + +export type RpcAttachmentInput = { + type?: unknown; + mimeType?: unknown; + fileName?: unknown; + content?: unknown; +}; + +export function normalizeRpcAttachmentsToChatAttachments( + attachments: RpcAttachmentInput[] | undefined, +): ChatAttachment[] { + return ( + attachments + ?.map((a) => ({ + type: typeof a?.type === "string" ? a.type : undefined, + mimeType: typeof a?.mimeType === "string" ? a.mimeType : undefined, + fileName: typeof a?.fileName === "string" ? a.fileName : undefined, + content: + typeof a?.content === "string" + ? a.content + : ArrayBuffer.isView(a?.content) + ? Buffer.from(a.content.buffer, a.content.byteOffset, a.content.byteLength).toString( + "base64", + ) + : a?.content instanceof ArrayBuffer + ? Buffer.from(a.content).toString("base64") + : undefined, + })) + .filter((a) => a.content) ?? [] + ); +} diff --git a/src/gateway/server-methods/chat.ts b/src/gateway/server-methods/chat.ts index d82511386..52225dc0b 100644 --- a/src/gateway/server-methods/chat.ts +++ b/src/gateway/server-methods/chat.ts @@ -39,6 +39,7 @@ import { } from "../session-utils.js"; import { formatForLog } from "../ws-log.js"; import { injectTimestamp, timestampOptsFromConfig } from "./agent-timestamp.js"; +import { normalizeRpcAttachmentsToChatAttachments } from "./attachment-normalize.js"; type TranscriptAppendResult = { ok: boolean; @@ -385,24 +386,7 @@ export const chatHandlers: GatewayRequestHandlers = { } const inboundMessage = sanitizedMessageResult.message; const stopCommand = isChatStopCommandText(inboundMessage); - const normalizedAttachments = - p.attachments - ?.map((a) => ({ - type: typeof a?.type === "string" ? a.type : undefined, - mimeType: typeof a?.mimeType === "string" ? a.mimeType : undefined, - fileName: typeof a?.fileName === "string" ? a.fileName : undefined, - content: - typeof a?.content === "string" - ? a.content - : ArrayBuffer.isView(a?.content) - ? Buffer.from( - a.content.buffer, - a.content.byteOffset, - a.content.byteLength, - ).toString("base64") - : undefined, - })) - .filter((a) => a.content) ?? []; + const normalizedAttachments = normalizeRpcAttachmentsToChatAttachments(p.attachments); const rawMessage = inboundMessage.trim(); if (!rawMessage && normalizedAttachments.length === 0) { respond(