Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions openclaw/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,35 @@ describe("Hent-ai service adapter configuration", () => {
expect(readFileSync(mediaUrl!)).toEqual(Buffer.from([1, 2, 3]));
});

it.each(["headers", "body"])("bounds image hydration while waiting for %s", async (stage) => {
vi.useFakeTimers();
let imageSignal: AbortSignal | null | undefined;
vi.stubGlobal("fetch", vi.fn(async (url: string | URL, options: RequestInit) => {
if (url.toString().endsWith("/verdict")) return okJson({ verdict: { media: { url: "/static/emotion.png" } } });
imageSignal = options.signal;
const stalled = () => new Promise((_resolve, reject) => {
options.signal?.addEventListener("abort", () => reject(new DOMException("aborted", "AbortError")), { once: true });
});
return stage === "headers" ? stalled() : { ok: true, headers: new Headers(), arrayBuffer: stalled };
}));
const { events, logger } = setup({ hentAiService: { url: "https://hent.test", token: "secret", timeoutMs: 10 } });
const payload = { text: "original reply", to: "channel:1" };
const pending = events.get("reply_payload_sending")!({ kind: "final", payload }, { messageId: "m" });
await vi.advanceTimersByTimeAsync(20);
await expect(pending).resolves.toBeUndefined();
expect(payload.text).toBe("original reply");
expect(imageSignal?.aborted).toBe(true);
expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining("timed out"));
});

it("keeps an existing single attachment when adding emotion media", async () => {
vi.stubGlobal("fetch", vi.fn(async () => okJson({ verdict: { media: { url: "https://cdn.test/emotion.png" } } })));
const { events } = setup();
expect(await events.get("reply_payload_sending")!({ kind: "final", payload: { text: "report", to: "channel:1", mediaUrl: "/tmp/report.pdf" } })).toMatchObject({
payload: { mediaUrls: ["/tmp/report.pdf", "https://cdn.test/emotion.png"] },
});
});

it.each([
["null", null],
["malformed", { verdict: { media: { caption: "missing url" } } }],
Expand Down
16 changes: 12 additions & 4 deletions openclaw/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,14 +305,16 @@ async function saveServiceMediaBuffer(buffer: Buffer, contentType: string): Prom
return path;
}

async function hydrateLocalServiceMedia(media: OpenClawStage1Media, baseUrl: URL, fetchImpl: FetchLike): Promise<OpenClawStage1Media> {
async function hydrateLocalServiceMedia(media: OpenClawStage1Media, baseUrl: URL, fetchImpl: FetchLike, signal: AbortSignal): Promise<OpenClawStage1Media> {
const mediaUrl = new URL(media.mediaUrl, baseUrl);
if (mediaUrl.origin !== baseUrl.origin) return media;

const response = await fetchImpl(mediaUrl, { method: "GET" });
signal.throwIfAborted();
const response = await fetchImpl(mediaUrl, { method: "GET", signal });
if (!response.ok) throw new Error(`media fetch returned HTTP ${response.status}`);
const contentType = response.headers.get("content-type") ?? media.contentType ?? "image/png";
const buffer = Buffer.from(await response.arrayBuffer());
signal.throwIfAborted();
return { ...media, mediaUrl: await saveServiceMediaBuffer(buffer, contentType), contentType };
}

Expand Down Expand Up @@ -380,7 +382,7 @@ async function callHentAiService(params: {
loggerWarn(params.logger, `hent-ai adapter: service media missing or malformed; skipping media${detail}`);
return { media: null, diagnostics: diagnostic("service media missing or malformed") };
}
const hydratedMedia = await hydrateLocalServiceMedia(media, params.baseUrl, fetchImpl);
const hydratedMedia = await hydrateLocalServiceMedia(media, params.baseUrl, fetchImpl, controller.signal);

loggerInfo(params.logger, `hent-ai adapter: service returned media endpoint=${params.endpoint}`);
const diagnostics = Array.isArray(root?.diagnostics)
Expand Down Expand Up @@ -496,10 +498,16 @@ export function extractEmbeddedResponseAffect(text: string): {
}

function applyMediaToPayload(payload: Record<string, unknown>, media: OpenClawStage1Media): Record<string, unknown> {
const urls = [...new Set([
...(typeof payload.mediaUrl === "string" ? [payload.mediaUrl] : []),
...(Array.isArray(payload.mediaUrls) ? payload.mediaUrls.filter((url): url is string => typeof url === "string") : []),
media.mediaUrl,
...(media.mediaUrls ?? []),
])];
return {
...payload,
mediaUrl: media.mediaUrl,
...(media.mediaUrls ? { mediaUrls: media.mediaUrls } : {}),
...(urls.length > 1 || media.mediaUrls ? { mediaUrls: urls } : {}),
...(media.sensitiveMedia !== undefined ? { sensitiveMedia: media.sensitiveMedia } : {}),
...(media.channelData ? { channelData: { ...(asRecord(payload.channelData) ?? {}), ...media.channelData } } : {}),
};
Expand Down
Loading