mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 09:20:43 +00:00
feat(qa-channel): forward inbound media attachments
This commit is contained in:
@@ -6,7 +6,9 @@ import { createQaBusState, startQaBusServer } from "../../qa-lab/api.js";
|
||||
import { qaChannelPlugin } from "../api.js";
|
||||
import { setQaChannelRuntime } from "../api.js";
|
||||
|
||||
function createMockQaRuntime(): PluginRuntime {
|
||||
function createMockQaRuntime(params?: {
|
||||
onDispatch?: (ctx: Record<string, unknown>) => void;
|
||||
}): PluginRuntime {
|
||||
const sessionUpdatedAt = new Map<string, number>();
|
||||
return {
|
||||
channel: {
|
||||
@@ -57,6 +59,7 @@ function createMockQaRuntime(): PluginRuntime {
|
||||
ctx: { BodyForAgent?: string; Body?: string };
|
||||
dispatcherOptions: { deliver: (payload: { text: string }) => Promise<void> };
|
||||
}) {
|
||||
params?.onDispatch?.(ctx as Record<string, unknown>);
|
||||
await dispatcherOptions.deliver({
|
||||
text: `qa-echo: ${ctx.BodyForAgent ?? ctx.Body ?? ""}`,
|
||||
});
|
||||
@@ -116,6 +119,76 @@ describe("qa-channel plugin", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("stages inbound image attachments into agent media payload", { timeout: 20_000 }, async () => {
|
||||
const state = createQaBusState();
|
||||
const bus = await startQaBusServer({ state });
|
||||
let dispatchedCtx: Record<string, unknown> | null = null;
|
||||
setQaChannelRuntime(
|
||||
createMockQaRuntime({
|
||||
onDispatch: (ctx) => {
|
||||
dispatchedCtx = ctx;
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const cfg = {
|
||||
channels: {
|
||||
"qa-channel": {
|
||||
baseUrl: bus.baseUrl,
|
||||
botUserId: "openclaw",
|
||||
botDisplayName: "OpenClaw QA",
|
||||
allowFrom: ["*"],
|
||||
},
|
||||
},
|
||||
};
|
||||
const account = qaChannelPlugin.config.resolveAccount(cfg, "default");
|
||||
const abort = new AbortController();
|
||||
const startAccount = qaChannelPlugin.gateway?.startAccount;
|
||||
expect(startAccount).toBeDefined();
|
||||
const task = startAccount!(
|
||||
createStartAccountContext({
|
||||
account,
|
||||
cfg,
|
||||
abortSignal: abort.signal,
|
||||
}),
|
||||
);
|
||||
|
||||
try {
|
||||
state.addInboundMessage({
|
||||
conversation: { id: "alice", kind: "direct" },
|
||||
senderId: "alice",
|
||||
senderName: "Alice",
|
||||
text: "describe this image",
|
||||
attachments: [
|
||||
{
|
||||
id: "image-1",
|
||||
kind: "image",
|
||||
mimeType: "image/png",
|
||||
fileName: "red-top-blue-bottom.png",
|
||||
contentBase64:
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAFElEQVR4nGP4z8Dwn4GBgYGJAQoAHxcCAr7cGDwAAAAASUVORK5CYII=",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await state.waitFor({
|
||||
kind: "message-text",
|
||||
textIncludes: "qa-echo: describe this image",
|
||||
direction: "outbound",
|
||||
timeoutMs: 15_000,
|
||||
});
|
||||
|
||||
expect(dispatchedCtx?.MediaPath).toEqual(expect.stringContaining("red-top-blue-bottom"));
|
||||
expect(dispatchedCtx?.MediaType).toBe("image/png");
|
||||
expect(dispatchedCtx?.MediaPaths).toEqual([dispatchedCtx?.MediaPath]);
|
||||
expect(dispatchedCtx?.MediaTypes).toEqual(["image/png"]);
|
||||
} finally {
|
||||
abort.abort();
|
||||
await task;
|
||||
await bus.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("exposes thread and message actions against the qa bus", async () => {
|
||||
const state = createQaBusState();
|
||||
const bus = await startQaBusServer({ state });
|
||||
|
||||
@@ -1,9 +1,48 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { dispatchInboundReplyWithBase } from "openclaw/plugin-sdk/inbound-reply-dispatch";
|
||||
import {
|
||||
buildAgentMediaPayload,
|
||||
saveMediaBuffer,
|
||||
saveMediaSource,
|
||||
} from "openclaw/plugin-sdk/media-runtime";
|
||||
import { buildQaTarget, sendQaBusMessage, type QaBusMessage } from "./bus-client.js";
|
||||
import { getQaChannelRuntime } from "./runtime.js";
|
||||
import type { CoreConfig, ResolvedQaChannelAccount } from "./types.js";
|
||||
|
||||
async function resolveQaInboundMediaPayload(attachments: QaBusMessage["attachments"]) {
|
||||
if (!Array.isArray(attachments) || attachments.length === 0) {
|
||||
return {};
|
||||
}
|
||||
const mediaList: Array<{ path: string; contentType?: string | null }> = [];
|
||||
for (const attachment of attachments) {
|
||||
if (!attachment?.mimeType) {
|
||||
continue;
|
||||
}
|
||||
if (typeof attachment.contentBase64 === "string" && attachment.contentBase64.trim()) {
|
||||
const saved = await saveMediaBuffer(
|
||||
Buffer.from(attachment.contentBase64, "base64"),
|
||||
attachment.mimeType,
|
||||
"inbound",
|
||||
undefined,
|
||||
attachment.fileName,
|
||||
);
|
||||
mediaList.push({
|
||||
path: saved.path,
|
||||
contentType: saved.contentType,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (typeof attachment.url === "string" && attachment.url.trim()) {
|
||||
const saved = await saveMediaSource(attachment.url, undefined, "inbound");
|
||||
mediaList.push({
|
||||
path: saved.path,
|
||||
contentType: saved.contentType,
|
||||
});
|
||||
}
|
||||
}
|
||||
return mediaList.length > 0 ? buildAgentMediaPayload(mediaList) : {};
|
||||
}
|
||||
|
||||
export async function handleQaInbound(params: {
|
||||
channelId: string;
|
||||
channelLabel: string;
|
||||
@@ -42,6 +81,7 @@ export async function handleQaInbound(params: {
|
||||
envelope: runtime.channel.reply.resolveEnvelopeFormatOptions(params.config as OpenClawConfig),
|
||||
body: inbound.text,
|
||||
});
|
||||
const mediaPayload = await resolveQaInboundMediaPayload(inbound.attachments);
|
||||
|
||||
const ctxPayload = runtime.channel.reply.finalizeInboundContext({
|
||||
Body: body,
|
||||
@@ -81,6 +121,7 @@ export async function handleQaInbound(params: {
|
||||
OriginatingChannel: params.channelId,
|
||||
OriginatingTo: target,
|
||||
CommandAuthorized: true,
|
||||
...mediaPayload,
|
||||
});
|
||||
|
||||
await dispatchInboundReplyWithBase({
|
||||
|
||||
Reference in New Issue
Block a user