mirror of https://github.com/openclaw/openclaw.git
refactor(media): centralize outbound access plumbing
This commit is contained in:
parent
c416527df6
commit
43ef8a5a86
|
|
@ -25,6 +25,7 @@ export async function handleDiscordMessageAction(
|
|||
| "accountId"
|
||||
| "requesterSenderId"
|
||||
| "toolContext"
|
||||
| "mediaAccess"
|
||||
| "mediaLocalRoots"
|
||||
| "mediaReadFile"
|
||||
>,
|
||||
|
|
@ -32,6 +33,7 @@ export async function handleDiscordMessageAction(
|
|||
const { action, params, cfg } = ctx;
|
||||
const accountId = ctx.accountId ?? readStringParam(params, "accountId");
|
||||
const actionOptions = {
|
||||
mediaAccess: ctx.mediaAccess,
|
||||
mediaLocalRoots: ctx.mediaLocalRoots,
|
||||
mediaReadFile: ctx.mediaReadFile,
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -172,6 +172,7 @@ export const discordOutbound: ChannelOutboundAdapter = {
|
|||
if (isFirst) {
|
||||
return await sendDiscordComponentMessage(target, componentSpec, {
|
||||
mediaUrl,
|
||||
mediaAccess: ctx.mediaAccess,
|
||||
mediaLocalRoots: ctx.mediaLocalRoots,
|
||||
mediaReadFile: ctx.mediaReadFile,
|
||||
replyTo: ctx.replyToId ?? undefined,
|
||||
|
|
@ -183,6 +184,7 @@ export const discordOutbound: ChannelOutboundAdapter = {
|
|||
return await send(target, text, {
|
||||
verbose: false,
|
||||
mediaUrl,
|
||||
mediaAccess: ctx.mediaAccess,
|
||||
mediaLocalRoots: ctx.mediaLocalRoots,
|
||||
mediaReadFile: ctx.mediaReadFile,
|
||||
replyTo: ctx.replyToId ?? undefined,
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ export {
|
|||
createAccountListHelpers,
|
||||
} from "openclaw/plugin-sdk/account-helpers";
|
||||
export { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
||||
export { loadOutboundMediaFromUrl } from "openclaw/plugin-sdk/discord";
|
||||
export { resolveAccountEntry } from "openclaw/plugin-sdk/routing";
|
||||
export type {
|
||||
ChannelMessageActionAdapter,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import {
|
|||
import { ChannelType, Routes } from "discord-api-types/v10";
|
||||
import { recordChannelActivity } from "openclaw/plugin-sdk/channel-runtime";
|
||||
import { loadConfig, type OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { loadWebMedia } from "openclaw/plugin-sdk/web-media";
|
||||
import { resolveDiscordAccount } from "./accounts.js";
|
||||
import { registerDiscordComponentEntries } from "./components-registry.js";
|
||||
import {
|
||||
|
|
@ -17,6 +16,7 @@ import {
|
|||
type DiscordComponentBuildResult,
|
||||
type DiscordComponentMessageSpec,
|
||||
} from "./components.js";
|
||||
import { loadOutboundMediaFromUrl } from "./runtime-api.js";
|
||||
import {
|
||||
buildDiscordSendError,
|
||||
createDiscordClient,
|
||||
|
|
@ -51,6 +51,10 @@ type DiscordComponentSendOpts = {
|
|||
sessionKey?: string;
|
||||
agentId?: string;
|
||||
mediaUrl?: string;
|
||||
mediaAccess?: {
|
||||
localRoots?: readonly string[];
|
||||
readFile?: (filePath: string) => Promise<Buffer>;
|
||||
};
|
||||
mediaLocalRoots?: readonly string[];
|
||||
mediaReadFile?: (filePath: string) => Promise<Buffer>;
|
||||
filename?: string;
|
||||
|
|
@ -99,10 +103,10 @@ async function buildDiscordComponentPayload(params: {
|
|||
const expectedAttachmentName = uniqueAttachmentNames[0];
|
||||
let files: MessagePayloadFile[] | undefined;
|
||||
if (params.opts.mediaUrl) {
|
||||
const media = await loadWebMedia(params.opts.mediaUrl, {
|
||||
localRoots: params.opts.mediaLocalRoots,
|
||||
readFile: params.opts.mediaReadFile,
|
||||
hostReadCapability: Boolean(params.opts.mediaReadFile),
|
||||
const media = await loadOutboundMediaFromUrl(params.opts.mediaUrl, {
|
||||
mediaAccess: params.opts.mediaAccess,
|
||||
mediaLocalRoots: params.opts.mediaLocalRoots,
|
||||
mediaReadFile: params.opts.mediaReadFile,
|
||||
});
|
||||
const filenameOverride = params.opts.filename?.trim();
|
||||
const fileName = filenameOverride || media.fileName || "upload";
|
||||
|
|
|
|||
|
|
@ -49,6 +49,10 @@ type DiscordSendOpts = {
|
|||
accountId?: string;
|
||||
mediaUrl?: string;
|
||||
filename?: string;
|
||||
mediaAccess?: {
|
||||
localRoots?: readonly string[];
|
||||
readFile?: (filePath: string) => Promise<Buffer>;
|
||||
};
|
||||
mediaLocalRoots?: readonly string[];
|
||||
mediaReadFile?: (filePath: string) => Promise<Buffer>;
|
||||
verbose?: boolean;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
createActionGate,
|
||||
extractToolSend,
|
||||
jsonResult,
|
||||
loadOutboundMediaFromUrl,
|
||||
readNumberParam,
|
||||
readReactionParams,
|
||||
readStringParam,
|
||||
|
|
@ -53,6 +54,10 @@ function resolveAppUserNames(account: { config: { botUser?: string | null } }) {
|
|||
async function loadGoogleChatActionMedia(params: {
|
||||
mediaUrl: string;
|
||||
maxBytes: number;
|
||||
mediaAccess?: {
|
||||
localRoots?: readonly string[];
|
||||
readFile?: (filePath: string) => Promise<Buffer>;
|
||||
};
|
||||
mediaLocalRoots?: readonly string[];
|
||||
mediaReadFile?: (filePath: string) => Promise<Buffer>;
|
||||
}) {
|
||||
|
|
@ -62,11 +67,11 @@ async function loadGoogleChatActionMedia(params: {
|
|||
url: params.mediaUrl,
|
||||
maxBytes: params.maxBytes,
|
||||
})
|
||||
: await runtime.media.loadWebMedia(params.mediaUrl, {
|
||||
: await loadOutboundMediaFromUrl(params.mediaUrl, {
|
||||
maxBytes: params.maxBytes,
|
||||
localRoots: params.mediaLocalRoots?.length ? params.mediaLocalRoots : undefined,
|
||||
readFile: params.mediaReadFile,
|
||||
hostReadCapability: Boolean(params.mediaReadFile),
|
||||
mediaAccess: params.mediaAccess,
|
||||
mediaLocalRoots: params.mediaLocalRoots,
|
||||
mediaReadFile: params.mediaReadFile,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -88,7 +93,15 @@ export const googlechatMessageActions: ChannelMessageActionAdapter = {
|
|||
extractToolSend: ({ args }) => {
|
||||
return extractToolSend(args, "sendMessage");
|
||||
},
|
||||
handleAction: async ({ action, params, cfg, accountId, mediaLocalRoots, mediaReadFile }) => {
|
||||
handleAction: async ({
|
||||
action,
|
||||
params,
|
||||
cfg,
|
||||
accountId,
|
||||
mediaAccess,
|
||||
mediaLocalRoots,
|
||||
mediaReadFile,
|
||||
}) => {
|
||||
const account = resolveGoogleChatAccount({
|
||||
cfg: cfg,
|
||||
accountId,
|
||||
|
|
@ -120,6 +133,7 @@ export const googlechatMessageActions: ChannelMessageActionAdapter = {
|
|||
const loaded = await loadGoogleChatActionMedia({
|
||||
mediaUrl,
|
||||
maxBytes,
|
||||
mediaAccess,
|
||||
mediaLocalRoots,
|
||||
mediaReadFile,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -28,10 +28,10 @@ import {
|
|||
DEFAULT_ACCOUNT_ID,
|
||||
createAccountStatusSink,
|
||||
getChatChannelMeta,
|
||||
loadOutboundMediaFromUrl,
|
||||
missingTargetError,
|
||||
PAIRING_APPROVED_MESSAGE,
|
||||
fetchRemoteMedia,
|
||||
loadWebMedia,
|
||||
resolveChannelMediaMaxBytes,
|
||||
runPassiveAccountLifecycle,
|
||||
type ChannelMessageActionAdapter,
|
||||
|
|
@ -401,6 +401,7 @@ export const googlechatPlugin = createChatChannelPlugin({
|
|||
to,
|
||||
text,
|
||||
mediaUrl,
|
||||
mediaAccess,
|
||||
mediaLocalRoots,
|
||||
mediaReadFile,
|
||||
accountId,
|
||||
|
|
@ -433,11 +434,11 @@ export const googlechatPlugin = createChatChannelPlugin({
|
|||
url: mediaUrl,
|
||||
maxBytes: effectiveMaxBytes,
|
||||
})
|
||||
: await loadWebMedia(mediaUrl, {
|
||||
: await loadOutboundMediaFromUrl(mediaUrl, {
|
||||
maxBytes: effectiveMaxBytes,
|
||||
localRoots: mediaLocalRoots?.length ? mediaLocalRoots : undefined,
|
||||
readFile: mediaReadFile,
|
||||
hostReadCapability: Boolean(mediaReadFile),
|
||||
mediaAccess,
|
||||
mediaLocalRoots,
|
||||
mediaReadFile,
|
||||
});
|
||||
const { sendGoogleChatMessage, uploadGoogleChatAttachment } =
|
||||
await loadGoogleChatChannelRuntime();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
import type { MarkdownTableMode, PollInput } from "../runtime-api.js";
|
||||
import {
|
||||
loadOutboundMediaFromUrl,
|
||||
type MarkdownTableMode,
|
||||
type PollInput,
|
||||
} from "../runtime-api.js";
|
||||
import { getMatrixRuntime } from "../runtime.js";
|
||||
import type { CoreConfig } from "../types.js";
|
||||
import { buildPollStartContent, M_POLL_START } from "./poll-types.js";
|
||||
|
|
@ -163,11 +167,11 @@ export async function sendMessageMatrix(
|
|||
let lastMessageId = "";
|
||||
if (opts.mediaUrl) {
|
||||
const maxBytes = resolveMediaMaxBytes(opts.accountId, cfg);
|
||||
const media = await getCore().media.loadWebMedia(opts.mediaUrl, {
|
||||
const media = await loadOutboundMediaFromUrl(opts.mediaUrl, {
|
||||
maxBytes,
|
||||
localRoots: opts.mediaLocalRoots,
|
||||
readFile: opts.mediaReadFile,
|
||||
hostReadCapability: Boolean(opts.mediaReadFile),
|
||||
mediaAccess: opts.mediaAccess,
|
||||
mediaLocalRoots: opts.mediaLocalRoots,
|
||||
mediaReadFile: opts.mediaReadFile,
|
||||
});
|
||||
const uploaded = await uploadMediaMaybeEncrypted(client, roomId, media.buffer, {
|
||||
contentType: media.contentType,
|
||||
|
|
|
|||
|
|
@ -88,6 +88,10 @@ export type MatrixSendOpts = {
|
|||
client?: import("../sdk.js").MatrixClient;
|
||||
cfg?: CoreConfig;
|
||||
mediaUrl?: string;
|
||||
mediaAccess?: {
|
||||
localRoots?: readonly string[];
|
||||
readFile?: (filePath: string) => Promise<Buffer>;
|
||||
};
|
||||
mediaLocalRoots?: readonly string[];
|
||||
mediaReadFile?: (filePath: string) => Promise<Buffer>;
|
||||
accountId?: string;
|
||||
|
|
|
|||
|
|
@ -13,6 +13,10 @@ export type SignalSendOpts = {
|
|||
account?: string;
|
||||
accountId?: string;
|
||||
mediaUrl?: string;
|
||||
mediaAccess?: {
|
||||
localRoots?: readonly string[];
|
||||
readFile?: (filePath: string) => Promise<Buffer>;
|
||||
};
|
||||
mediaLocalRoots?: readonly string[];
|
||||
mediaReadFile?: (filePath: string) => Promise<Buffer>;
|
||||
maxBytes?: number;
|
||||
|
|
@ -129,6 +133,7 @@ export async function sendMessageSignal(
|
|||
let attachments: string[] | undefined;
|
||||
if (opts.mediaUrl?.trim()) {
|
||||
const resolved = await resolveOutboundAttachmentFromUrl(opts.mediaUrl.trim(), maxBytes, {
|
||||
mediaAccess: opts.mediaAccess,
|
||||
localRoots: opts.mediaLocalRoots,
|
||||
readFile: opts.mediaReadFile,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -159,6 +159,10 @@ export async function sendSlackMessage(
|
|||
content: string,
|
||||
opts: SlackActionClientOpts & {
|
||||
mediaUrl?: string;
|
||||
mediaAccess?: {
|
||||
localRoots?: readonly string[];
|
||||
readFile?: (filePath: string) => Promise<Buffer>;
|
||||
};
|
||||
mediaLocalRoots?: readonly string[];
|
||||
mediaReadFile?: (filePath: string) => Promise<Buffer>;
|
||||
threadTs?: string;
|
||||
|
|
@ -171,6 +175,7 @@ export async function sendSlackMessage(
|
|||
accountId: opts.accountId,
|
||||
token: opts.token,
|
||||
mediaUrl: opts.mediaUrl,
|
||||
mediaAccess: opts.mediaAccess,
|
||||
mediaLocalRoots: opts.mediaLocalRoots,
|
||||
mediaReadFile: opts.mediaReadFile,
|
||||
client: opts.client,
|
||||
|
|
|
|||
|
|
@ -82,6 +82,10 @@ async function sendSlackOutboundMessage(params: {
|
|||
to: string;
|
||||
text: string;
|
||||
mediaUrl?: string;
|
||||
mediaAccess?: {
|
||||
localRoots?: readonly string[];
|
||||
readFile?: (filePath: string) => Promise<Buffer>;
|
||||
};
|
||||
mediaLocalRoots?: readonly string[];
|
||||
mediaReadFile?: (filePath: string) => Promise<Buffer>;
|
||||
blocks?: NonNullable<Parameters<typeof sendMessageSlack>[2]>["blocks"];
|
||||
|
|
@ -118,6 +122,7 @@ async function sendSlackOutboundMessage(params: {
|
|||
...(params.mediaUrl
|
||||
? {
|
||||
mediaUrl: params.mediaUrl,
|
||||
mediaAccess: params.mediaAccess,
|
||||
mediaLocalRoots: params.mediaLocalRoots,
|
||||
mediaReadFile: params.mediaReadFile,
|
||||
}
|
||||
|
|
@ -188,6 +193,7 @@ export const slackOutbound: ChannelOutboundAdapter = {
|
|||
to: ctx.to,
|
||||
text,
|
||||
mediaUrl,
|
||||
mediaAccess: ctx.mediaAccess,
|
||||
mediaLocalRoots: ctx.mediaLocalRoots,
|
||||
mediaReadFile: ctx.mediaReadFile,
|
||||
accountId: ctx.accountId,
|
||||
|
|
@ -201,6 +207,7 @@ export const slackOutbound: ChannelOutboundAdapter = {
|
|||
cfg: ctx.cfg,
|
||||
to: ctx.to,
|
||||
text: payload.text ?? "",
|
||||
mediaAccess: ctx.mediaAccess,
|
||||
mediaLocalRoots: ctx.mediaLocalRoots,
|
||||
mediaReadFile: ctx.mediaReadFile,
|
||||
blocks,
|
||||
|
|
@ -231,6 +238,7 @@ export const slackOutbound: ChannelOutboundAdapter = {
|
|||
to,
|
||||
text,
|
||||
mediaUrl,
|
||||
mediaAccess,
|
||||
mediaLocalRoots,
|
||||
mediaReadFile,
|
||||
accountId,
|
||||
|
|
@ -244,6 +252,7 @@ export const slackOutbound: ChannelOutboundAdapter = {
|
|||
to,
|
||||
text,
|
||||
mediaUrl,
|
||||
mediaAccess,
|
||||
mediaLocalRoots,
|
||||
mediaReadFile,
|
||||
accountId,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ export {
|
|||
resolveConfiguredFromRequiredCredentialStatuses,
|
||||
} from "openclaw/plugin-sdk/channel-status";
|
||||
export { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id";
|
||||
export { loadOutboundMediaFromUrl } from "openclaw/plugin-sdk/slack";
|
||||
export { looksLikeSlackTargetId, normalizeSlackMessagingTarget } from "./targets.js";
|
||||
export type { ChannelPlugin, OpenClawConfig, SlackAccountConfig } from "openclaw/plugin-sdk/slack";
|
||||
export {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import {
|
|||
import { resolveTextChunksWithFallback } from "openclaw/plugin-sdk/reply-payload";
|
||||
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
|
||||
import { loadWebMedia } from "openclaw/plugin-sdk/web-media";
|
||||
import type { SlackTokenSource } from "./accounts.js";
|
||||
import { resolveSlackAccount } from "./accounts.js";
|
||||
import { buildSlackBlocksFallbackText } from "./blocks-fallback.js";
|
||||
|
|
@ -19,6 +18,7 @@ import { validateSlackBlocksArray } from "./blocks-input.js";
|
|||
import { createSlackWebClient } from "./client.js";
|
||||
import { markdownToSlackMrkdwnChunks } from "./format.js";
|
||||
import { SLACK_TEXT_LIMIT } from "./limits.js";
|
||||
import { loadOutboundMediaFromUrl } from "./runtime-api.js";
|
||||
import { parseSlackTarget } from "./targets.js";
|
||||
import { resolveSlackBotToken } from "./token.js";
|
||||
const SLACK_UPLOAD_SSRF_POLICY = {
|
||||
|
|
@ -49,6 +49,10 @@ type SlackSendOpts = {
|
|||
token?: string;
|
||||
accountId?: string;
|
||||
mediaUrl?: string;
|
||||
mediaAccess?: {
|
||||
localRoots?: readonly string[];
|
||||
readFile?: (filePath: string) => Promise<Buffer>;
|
||||
};
|
||||
uploadFileName?: string;
|
||||
uploadTitle?: string;
|
||||
mediaLocalRoots?: readonly string[];
|
||||
|
|
@ -231,6 +235,10 @@ async function uploadSlackFile(params: {
|
|||
client: WebClient;
|
||||
channelId: string;
|
||||
mediaUrl: string;
|
||||
mediaAccess?: {
|
||||
localRoots?: readonly string[];
|
||||
readFile?: (filePath: string) => Promise<Buffer>;
|
||||
};
|
||||
uploadFileName?: string;
|
||||
uploadTitle?: string;
|
||||
mediaLocalRoots?: readonly string[];
|
||||
|
|
@ -239,11 +247,11 @@ async function uploadSlackFile(params: {
|
|||
threadTs?: string;
|
||||
maxBytes?: number;
|
||||
}): Promise<string> {
|
||||
const { buffer, contentType, fileName } = await loadWebMedia(params.mediaUrl, {
|
||||
const { buffer, contentType, fileName } = await loadOutboundMediaFromUrl(params.mediaUrl, {
|
||||
maxBytes: params.maxBytes,
|
||||
localRoots: params.mediaLocalRoots,
|
||||
readFile: params.mediaReadFile,
|
||||
hostReadCapability: Boolean(params.mediaReadFile),
|
||||
mediaAccess: params.mediaAccess,
|
||||
mediaLocalRoots: params.mediaLocalRoots,
|
||||
mediaReadFile: params.mediaReadFile,
|
||||
});
|
||||
const uploadFileName = params.uploadFileName ?? fileName ?? "upload";
|
||||
const uploadTitle = params.uploadTitle ?? uploadFileName;
|
||||
|
|
@ -373,6 +381,7 @@ export async function sendMessageSlack(
|
|||
client,
|
||||
channelId,
|
||||
mediaUrl: opts.mediaUrl,
|
||||
mediaAccess: opts.mediaAccess,
|
||||
uploadFileName: opts.uploadFileName,
|
||||
uploadTitle: opts.uploadTitle,
|
||||
mediaLocalRoots: opts.mediaLocalRoots,
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ export {
|
|||
type GroupPolicy,
|
||||
type WhatsAppAccountConfig,
|
||||
} from "openclaw/plugin-sdk/whatsapp-shared";
|
||||
export { loadOutboundMediaFromUrl } from "openclaw/plugin-sdk/whatsapp";
|
||||
export {
|
||||
isWhatsAppGroupJid,
|
||||
isWhatsAppUserTarget,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { markdownToWhatsApp } from "openclaw/plugin-sdk/text-runtime";
|
|||
import { toWhatsappJid } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { resolveWhatsAppAccount, resolveWhatsAppMediaMaxBytes } from "./accounts.js";
|
||||
import { type ActiveWebSendOptions, requireActiveWebListener } from "./active-listener.js";
|
||||
import { loadWebMedia } from "./media.js";
|
||||
import { loadOutboundMediaFromUrl } from "./runtime-api.js";
|
||||
|
||||
const outboundLog = createSubsystemLogger("gateway/channels/whatsapp").child("outbound");
|
||||
|
||||
|
|
@ -21,6 +21,10 @@ export async function sendMessageWhatsApp(
|
|||
verbose: boolean;
|
||||
cfg?: OpenClawConfig;
|
||||
mediaUrl?: string;
|
||||
mediaAccess?: {
|
||||
localRoots?: readonly string[];
|
||||
readFile?: (filePath: string) => Promise<Buffer>;
|
||||
};
|
||||
mediaLocalRoots?: readonly string[];
|
||||
mediaReadFile?: (filePath: string) => Promise<Buffer>;
|
||||
gifPlayback?: boolean;
|
||||
|
|
@ -61,11 +65,11 @@ export async function sendMessageWhatsApp(
|
|||
let mediaType: string | undefined;
|
||||
let documentFileName: string | undefined;
|
||||
if (options.mediaUrl) {
|
||||
const media = await loadWebMedia(options.mediaUrl, {
|
||||
const media = await loadOutboundMediaFromUrl(options.mediaUrl, {
|
||||
maxBytes: resolveWhatsAppMediaMaxBytes(account),
|
||||
localRoots: options.mediaLocalRoots,
|
||||
readFile: options.mediaReadFile,
|
||||
hostReadCapability: Boolean(options.mediaReadFile),
|
||||
mediaAccess: options.mediaAccess,
|
||||
mediaLocalRoots: options.mediaLocalRoots,
|
||||
mediaReadFile: options.mediaReadFile,
|
||||
});
|
||||
const caption = text || undefined;
|
||||
mediaBuffer = media.buffer;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
import { chunkText } from "../../../auto-reply/chunk.js";
|
||||
import type { OpenClawConfig } from "../../../config/config.js";
|
||||
import type { OutboundSendDeps } from "../../../infra/outbound/deliver.js";
|
||||
import type { OutboundMediaAccess } from "../../../media/load-options.js";
|
||||
import { resolveChannelMediaMaxBytes } from "../media-limits.js";
|
||||
import type { ChannelOutboundAdapter } from "../types.js";
|
||||
|
||||
|
|
@ -16,6 +17,7 @@ type DirectSendOptions = {
|
|||
accountId?: string | null;
|
||||
replyToId?: string | null;
|
||||
mediaUrl?: string;
|
||||
mediaAccess?: OutboundMediaAccess;
|
||||
mediaLocalRoots?: readonly string[];
|
||||
mediaReadFile?: (filePath: string) => Promise<Buffer>;
|
||||
maxBytes?: number;
|
||||
|
|
@ -80,8 +82,7 @@ export function createDirectTextMediaOutbound<
|
|||
deps?: OutboundSendDeps;
|
||||
replyToId?: string | null;
|
||||
mediaUrl?: string;
|
||||
mediaLocalRoots?: readonly string[];
|
||||
mediaReadFile?: (filePath: string) => Promise<Buffer>;
|
||||
mediaAccess?: OutboundMediaAccess;
|
||||
buildOptions: (params: DirectSendOptions) => TOpts;
|
||||
}) => {
|
||||
const send = params.resolveSender(sendParams.deps);
|
||||
|
|
@ -95,8 +96,9 @@ export function createDirectTextMediaOutbound<
|
|||
sendParams.buildOptions({
|
||||
cfg: sendParams.cfg,
|
||||
mediaUrl: sendParams.mediaUrl,
|
||||
mediaLocalRoots: sendParams.mediaLocalRoots,
|
||||
mediaReadFile: sendParams.mediaReadFile,
|
||||
mediaAccess: sendParams.mediaAccess,
|
||||
mediaLocalRoots: sendParams.mediaAccess?.localRoots,
|
||||
mediaReadFile: sendParams.mediaAccess?.readFile,
|
||||
accountId: sendParams.accountId,
|
||||
replyToId: sendParams.replyToId,
|
||||
maxBytes,
|
||||
|
|
@ -128,6 +130,7 @@ export function createDirectTextMediaOutbound<
|
|||
to,
|
||||
text,
|
||||
mediaUrl,
|
||||
mediaAccess,
|
||||
mediaLocalRoots,
|
||||
mediaReadFile,
|
||||
accountId,
|
||||
|
|
@ -139,8 +142,14 @@ export function createDirectTextMediaOutbound<
|
|||
to,
|
||||
text,
|
||||
mediaUrl,
|
||||
mediaLocalRoots,
|
||||
mediaReadFile,
|
||||
mediaAccess:
|
||||
mediaAccess ??
|
||||
(mediaLocalRoots || mediaReadFile
|
||||
? {
|
||||
...(mediaLocalRoots?.length ? { localRoots: mediaLocalRoots } : {}),
|
||||
...(mediaReadFile ? { readFile: mediaReadFile } : {}),
|
||||
}
|
||||
: undefined),
|
||||
accountId,
|
||||
deps,
|
||||
replyToId,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import type {
|
|||
PluginApprovalRequest,
|
||||
PluginApprovalResolved,
|
||||
} from "../../infra/plugin-approvals.js";
|
||||
import type { OutboundMediaAccess } from "../../media/load-options.js";
|
||||
import type { PluginRuntime } from "../../plugins/runtime/types.js";
|
||||
import type { RuntimeEnv } from "../../runtime.js";
|
||||
import type { ConfigWriteTarget } from "./config-writes.js";
|
||||
|
|
@ -137,6 +138,7 @@ export type ChannelOutboundContext = {
|
|||
text: string;
|
||||
mediaUrl?: string;
|
||||
audioAsVoice?: boolean;
|
||||
mediaAccess?: OutboundMediaAccess;
|
||||
mediaLocalRoots?: readonly string[];
|
||||
mediaReadFile?: (filePath: string) => Promise<Buffer>;
|
||||
gifPlayback?: boolean;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import type { TSchema } from "@sinclair/typebox";
|
|||
import type { MsgContext } from "../../auto-reply/templating.js";
|
||||
import type { ReplyPayload } from "../../auto-reply/types.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import type { OutboundMediaAccess } from "../../media/load-options.js";
|
||||
import type { PollInput } from "../../polls.js";
|
||||
import type { GatewayClientMode, GatewayClientName } from "../../utils/message-channel.js";
|
||||
import type { ChatType } from "../chat-type.js";
|
||||
|
|
@ -494,6 +495,7 @@ export type ChannelMessageActionContext = {
|
|||
action: ChannelMessageActionName;
|
||||
cfg: OpenClawConfig;
|
||||
params: Record<string, unknown>;
|
||||
mediaAccess?: OutboundMediaAccess;
|
||||
mediaLocalRoots?: readonly string[];
|
||||
mediaReadFile?: (filePath: string) => Promise<Buffer>;
|
||||
accountId?: string | null;
|
||||
|
|
|
|||
|
|
@ -29,6 +29,10 @@ type WhatsAppSendMessage = (
|
|||
verbose: boolean;
|
||||
cfg?: OpenClawConfig;
|
||||
mediaUrl?: string;
|
||||
mediaAccess?: {
|
||||
localRoots?: readonly string[];
|
||||
readFile?: (filePath: string) => Promise<Buffer>;
|
||||
};
|
||||
mediaLocalRoots?: readonly string[];
|
||||
mediaReadFile?: (filePath: string) => Promise<Buffer>;
|
||||
gifPlayback?: boolean;
|
||||
|
|
@ -99,6 +103,7 @@ export function createWhatsAppOutboundBase({
|
|||
to,
|
||||
text,
|
||||
mediaUrl,
|
||||
mediaAccess,
|
||||
mediaLocalRoots,
|
||||
mediaReadFile,
|
||||
accountId,
|
||||
|
|
@ -111,6 +116,7 @@ export function createWhatsAppOutboundBase({
|
|||
verbose: false,
|
||||
cfg,
|
||||
mediaUrl,
|
||||
mediaAccess,
|
||||
mediaLocalRoots,
|
||||
mediaReadFile,
|
||||
accountId: accountId ?? undefined,
|
||||
|
|
|
|||
|
|
@ -7,11 +7,7 @@ import type { CronJob } from "../../cron/types.js";
|
|||
import { requestHeartbeatNow } from "../../infra/heartbeat-wake.js";
|
||||
import { enqueueSystemEvent } from "../../infra/system-events.js";
|
||||
import type { createSubsystemLogger } from "../../logging/subsystem.js";
|
||||
import {
|
||||
normalizeHookDispatchSessionKey,
|
||||
type HookAgentDispatchPayload,
|
||||
type HooksConfigResolved,
|
||||
} from "../hooks.js";
|
||||
import { type HookAgentDispatchPayload, type HooksConfigResolved } from "../hooks.js";
|
||||
import { createHooksRequestHandler, type HookClientIpConfig } from "../server-http.js";
|
||||
|
||||
type SubsystemLogger = ReturnType<typeof createSubsystemLogger>;
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ function createRoute(params: {
|
|||
}
|
||||
|
||||
function createMockLogger(): SubsystemLogger {
|
||||
const child = vi.fn<(name: string) => SubsystemLogger>();
|
||||
const logger = {
|
||||
subsystem: "test/plugins-http-runtime-scopes",
|
||||
isEnabled: () => true,
|
||||
|
|
@ -38,10 +39,10 @@ function createMockLogger(): SubsystemLogger {
|
|||
error: vi.fn(),
|
||||
fatal: vi.fn(),
|
||||
raw: vi.fn(),
|
||||
child: vi.fn(),
|
||||
} satisfies Omit<SubsystemLogger, "child"> & { child: ReturnType<typeof vi.fn> };
|
||||
logger.child.mockImplementation(() => logger);
|
||||
return logger;
|
||||
child,
|
||||
} satisfies SubsystemLogger;
|
||||
child.mockImplementation(() => logger);
|
||||
return logger as SubsystemLogger;
|
||||
}
|
||||
|
||||
function assertWriteHelperAllowed() {
|
||||
|
|
|
|||
|
|
@ -29,8 +29,8 @@ import {
|
|||
} from "../../hooks/message-hook-mappers.js";
|
||||
import { hasReplyPayloadContent } from "../../interactive/payload.js";
|
||||
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
||||
import { getAgentScopedMediaLocalRootsForSources } from "../../media/local-roots.js";
|
||||
import { createAgentScopedHostMediaReadFile } from "../../media/read-capability.js";
|
||||
import type { OutboundMediaAccess } from "../../media/load-options.js";
|
||||
import { resolveAgentScopedOutboundMediaAccess } from "../../media/read-capability.js";
|
||||
import { getGlobalHookRunner } from "../../plugins/hook-runner-global.js";
|
||||
import { throwIfAborted } from "./abort.js";
|
||||
import { resolveOutboundChannelPlugin } from "./channel-resolution.js";
|
||||
|
|
@ -130,8 +130,7 @@ type ChannelHandlerParams = {
|
|||
gifPlayback?: boolean;
|
||||
forceDocument?: boolean;
|
||||
silent?: boolean;
|
||||
mediaLocalRoots?: readonly string[];
|
||||
mediaReadFile?: (filePath: string) => Promise<Buffer>;
|
||||
mediaAccess?: OutboundMediaAccess;
|
||||
gatewayClientScopes?: readonly string[];
|
||||
};
|
||||
|
||||
|
|
@ -252,8 +251,9 @@ function createChannelOutboundContextBase(
|
|||
forceDocument: params.forceDocument,
|
||||
deps: params.deps,
|
||||
silent: params.silent,
|
||||
mediaLocalRoots: params.mediaLocalRoots,
|
||||
mediaReadFile: params.mediaReadFile,
|
||||
mediaAccess: params.mediaAccess,
|
||||
mediaLocalRoots: params.mediaAccess?.localRoots,
|
||||
mediaReadFile: params.mediaAccess?.readFile,
|
||||
gatewayClientScopes: params.gatewayClientScopes,
|
||||
};
|
||||
}
|
||||
|
|
@ -564,15 +564,11 @@ async function deliverOutboundPayloadsCore(
|
|||
const accountId = params.accountId;
|
||||
const deps = params.deps;
|
||||
const abortSignal = params.abortSignal;
|
||||
const mediaLocalRoots = getAgentScopedMediaLocalRootsForSources({
|
||||
const mediaAccess = resolveAgentScopedOutboundMediaAccess({
|
||||
cfg,
|
||||
agentId: params.session?.agentId ?? params.mirror?.agentId,
|
||||
mediaSources: collectPayloadMediaSources(payloads),
|
||||
});
|
||||
const mediaReadFile = createAgentScopedHostMediaReadFile({
|
||||
cfg,
|
||||
agentId: params.session?.agentId ?? params.mirror?.agentId,
|
||||
});
|
||||
const results: OutboundDeliveryResult[] = [];
|
||||
const handler = await createChannelHandler({
|
||||
cfg,
|
||||
|
|
@ -586,8 +582,7 @@ async function deliverOutboundPayloadsCore(
|
|||
gifPlayback: params.gifPlayback,
|
||||
forceDocument: params.forceDocument,
|
||||
silent: params.silent,
|
||||
mediaLocalRoots,
|
||||
mediaReadFile,
|
||||
mediaAccess,
|
||||
gatewayClientScopes: params.gatewayClientScopes,
|
||||
});
|
||||
const configuredTextLimit = handler.chunker
|
||||
|
|
|
|||
|
|
@ -4,6 +4,12 @@ import type { ChannelId, ChannelMessageActionName } from "../../channels/plugins
|
|||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { createRootScopedReadFile } from "../../infra/fs-safe.js";
|
||||
import { basenameFromMediaSource } from "../../infra/local-file-access.js";
|
||||
import {
|
||||
buildOutboundMediaLoadOptions,
|
||||
resolveOutboundMediaAccess,
|
||||
type OutboundMediaAccess,
|
||||
type OutboundMediaReadFile,
|
||||
} from "../../media/load-options.js";
|
||||
import { extensionForMime } from "../../media/mime.js";
|
||||
import { loadWebMedia } from "../../media/web-media.js";
|
||||
import { readBooleanParam as readBooleanParamShared } from "../../plugin-sdk/boolean-param.js";
|
||||
|
|
@ -101,14 +107,14 @@ export type AttachmentMediaPolicy =
|
|||
}
|
||||
| {
|
||||
mode: "host";
|
||||
localRoots?: readonly string[];
|
||||
readFile?: (filePath: string) => Promise<Buffer>;
|
||||
mediaAccess?: OutboundMediaAccess;
|
||||
};
|
||||
|
||||
export function resolveAttachmentMediaPolicy(params: {
|
||||
sandboxRoot?: string;
|
||||
mediaAccess?: OutboundMediaAccess;
|
||||
mediaLocalRoots?: readonly string[];
|
||||
mediaReadFile?: (filePath: string) => Promise<Buffer>;
|
||||
mediaReadFile?: OutboundMediaReadFile;
|
||||
}): AttachmentMediaPolicy {
|
||||
const sandboxRoot = params.sandboxRoot?.trim();
|
||||
if (sandboxRoot) {
|
||||
|
|
@ -119,8 +125,11 @@ export function resolveAttachmentMediaPolicy(params: {
|
|||
}
|
||||
return {
|
||||
mode: "host",
|
||||
localRoots: params.mediaLocalRoots,
|
||||
readFile: params.mediaReadFile,
|
||||
mediaAccess: resolveOutboundMediaAccess({
|
||||
mediaAccess: params.mediaAccess,
|
||||
mediaLocalRoots: params.mediaLocalRoots,
|
||||
mediaReadFile: params.mediaReadFile,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -136,7 +145,7 @@ function buildAttachmentMediaLoadOptions(params: {
|
|||
| {
|
||||
maxBytes?: number;
|
||||
localRoots?: readonly string[] | "any";
|
||||
readFile?: (filePath: string) => Promise<Buffer>;
|
||||
readFile?: OutboundMediaReadFile;
|
||||
hostReadCapability?: boolean;
|
||||
} {
|
||||
if (params.policy.mode === "sandbox") {
|
||||
|
|
@ -149,16 +158,10 @@ function buildAttachmentMediaLoadOptions(params: {
|
|||
readFile: readSandboxFile,
|
||||
};
|
||||
}
|
||||
return {
|
||||
return buildOutboundMediaLoadOptions({
|
||||
maxBytes: params.maxBytes,
|
||||
...(params.policy.readFile
|
||||
? {
|
||||
localRoots: "any" as const,
|
||||
readFile: params.policy.readFile,
|
||||
hostReadCapability: true,
|
||||
}
|
||||
: { localRoots: params.policy.localRoots }),
|
||||
};
|
||||
mediaAccess: params.policy.mediaAccess,
|
||||
});
|
||||
}
|
||||
|
||||
async function hydrateAttachmentPayload(params: {
|
||||
|
|
|
|||
|
|
@ -15,11 +15,9 @@ import type {
|
|||
} from "../../channels/plugins/types.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { hasInteractiveReplyBlocks, hasReplyPayloadContent } from "../../interactive/payload.js";
|
||||
import {
|
||||
getAgentScopedMediaLocalRoots,
|
||||
getAgentScopedMediaLocalRootsForSources,
|
||||
} from "../../media/local-roots.js";
|
||||
import { createAgentScopedHostMediaReadFile } from "../../media/read-capability.js";
|
||||
import type { OutboundMediaAccess } from "../../media/load-options.js";
|
||||
import { getAgentScopedMediaLocalRoots } from "../../media/local-roots.js";
|
||||
import { resolveAgentScopedOutboundMediaAccess } from "../../media/read-capability.js";
|
||||
import { hasPollCreationParams } from "../../poll-params.js";
|
||||
import { resolvePollMaxSelections } from "../../polls.js";
|
||||
import { buildChannelAccountBindings } from "../../routing/bindings.js";
|
||||
|
|
@ -273,8 +271,7 @@ type ResolvedActionContext = {
|
|||
cfg: OpenClawConfig;
|
||||
params: Record<string, unknown>;
|
||||
channel: ChannelId;
|
||||
mediaLocalRoots: readonly string[];
|
||||
mediaReadFile?: (filePath: string) => Promise<Buffer>;
|
||||
mediaAccess: OutboundMediaAccess;
|
||||
accountId?: string | null;
|
||||
dryRun: boolean;
|
||||
gateway?: MessageActionRunnerGateway;
|
||||
|
|
@ -520,7 +517,7 @@ async function handleSendAction(ctx: ResolvedActionContext): Promise<MessageActi
|
|||
channel,
|
||||
params,
|
||||
agentId,
|
||||
mediaReadFile: ctx.mediaReadFile,
|
||||
mediaAccess: ctx.mediaAccess,
|
||||
accountId: accountId ?? undefined,
|
||||
gateway,
|
||||
toolContext: input.toolContext,
|
||||
|
|
@ -644,7 +641,7 @@ async function handlePluginAction(ctx: ResolvedActionContext): Promise<MessageAc
|
|||
cfg,
|
||||
params,
|
||||
channel,
|
||||
mediaLocalRoots,
|
||||
mediaAccess,
|
||||
accountId,
|
||||
dryRun,
|
||||
gateway,
|
||||
|
|
@ -675,8 +672,9 @@ async function handlePluginAction(ctx: ResolvedActionContext): Promise<MessageAc
|
|||
action,
|
||||
cfg,
|
||||
params,
|
||||
mediaLocalRoots,
|
||||
mediaReadFile: ctx.mediaReadFile,
|
||||
mediaAccess,
|
||||
mediaLocalRoots: mediaAccess.localRoots,
|
||||
mediaReadFile: mediaAccess.readFile,
|
||||
accountId: accountId ?? undefined,
|
||||
requesterSenderId: input.requesterSenderId ?? undefined,
|
||||
sessionKey: input.sessionKey,
|
||||
|
|
@ -748,19 +746,14 @@ export async function runMessageAction(
|
|||
mediaPolicy: normalizationPolicy,
|
||||
});
|
||||
|
||||
const mediaLocalRoots = getAgentScopedMediaLocalRootsForSources({
|
||||
const mediaAccess = resolveAgentScopedOutboundMediaAccess({
|
||||
cfg,
|
||||
agentId: resolvedAgentId,
|
||||
mediaSources: collectActionMediaSourceHints(params),
|
||||
});
|
||||
const mediaReadFile = createAgentScopedHostMediaReadFile({
|
||||
cfg,
|
||||
agentId: resolvedAgentId,
|
||||
});
|
||||
const mediaPolicy = resolveAttachmentMediaPolicy({
|
||||
sandboxRoot: input.sandboxRoot,
|
||||
mediaLocalRoots,
|
||||
mediaReadFile,
|
||||
mediaAccess,
|
||||
});
|
||||
|
||||
await hydrateAttachmentParamsForAction({
|
||||
|
|
@ -800,8 +793,7 @@ export async function runMessageAction(
|
|||
cfg,
|
||||
params,
|
||||
channel,
|
||||
mediaLocalRoots,
|
||||
mediaReadFile,
|
||||
mediaAccess,
|
||||
accountId,
|
||||
dryRun,
|
||||
gateway,
|
||||
|
|
@ -817,8 +809,7 @@ export async function runMessageAction(
|
|||
cfg,
|
||||
params,
|
||||
channel,
|
||||
mediaLocalRoots,
|
||||
mediaReadFile,
|
||||
mediaAccess,
|
||||
accountId,
|
||||
dryRun,
|
||||
gateway,
|
||||
|
|
@ -831,8 +822,7 @@ export async function runMessageAction(
|
|||
cfg,
|
||||
params,
|
||||
channel,
|
||||
mediaLocalRoots,
|
||||
mediaReadFile,
|
||||
mediaAccess,
|
||||
accountId,
|
||||
dryRun,
|
||||
gateway,
|
||||
|
|
|
|||
|
|
@ -1,15 +1,52 @@
|
|||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../../utils/message-channel.js";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
getDefaultMediaLocalRoots: vi.fn(() => []),
|
||||
dispatchChannelMessageAction: vi.fn(),
|
||||
sendMessage: vi.fn(),
|
||||
sendPoll: vi.fn(),
|
||||
getAgentScopedMediaLocalRootsForSources: vi.fn(() => ["/tmp/agent-roots"]),
|
||||
createAgentScopedHostMediaReadFile: vi.fn(() => async () => Buffer.from("capability")),
|
||||
appendAssistantMessageToSessionTranscript: vi.fn(async () => ({ ok: true, sessionFile: "x" })),
|
||||
}));
|
||||
const getDefaultMediaLocalRootsMock = vi.hoisted(() => vi.fn(() => []));
|
||||
const dispatchChannelMessageActionMock = vi.hoisted(() => vi.fn());
|
||||
const sendMessageMock = vi.hoisted(() => vi.fn());
|
||||
const sendPollMock = vi.hoisted(() => vi.fn());
|
||||
const getAgentScopedMediaLocalRootsForSourcesMock = vi.hoisted(() =>
|
||||
vi.fn<(params: { cfg: unknown; agentId?: string; mediaSources?: readonly string[] }) => string[]>(
|
||||
() => ["/tmp/agent-roots"],
|
||||
),
|
||||
);
|
||||
const createAgentScopedHostMediaReadFileMock = vi.hoisted(() =>
|
||||
vi.fn<(params: { cfg: unknown; agentId?: string }) => (filePath: string) => Promise<Buffer>>(
|
||||
() => async () => Buffer.from("capability"),
|
||||
),
|
||||
);
|
||||
const resolveAgentScopedOutboundMediaAccessMock = vi.hoisted(() =>
|
||||
vi.fn<
|
||||
(params: { cfg: unknown; agentId?: string; mediaSources?: readonly string[] }) => {
|
||||
localRoots: string[];
|
||||
readFile: (filePath: string) => Promise<Buffer>;
|
||||
}
|
||||
>((params) => ({
|
||||
localRoots: getAgentScopedMediaLocalRootsForSourcesMock({
|
||||
cfg: params.cfg,
|
||||
agentId: params.agentId,
|
||||
mediaSources: params.mediaSources ?? [],
|
||||
}),
|
||||
readFile: createAgentScopedHostMediaReadFileMock({
|
||||
cfg: params.cfg,
|
||||
agentId: params.agentId,
|
||||
}),
|
||||
})),
|
||||
);
|
||||
const appendAssistantMessageToSessionTranscriptMock = vi.hoisted(() =>
|
||||
vi.fn(async () => ({ ok: true, sessionFile: "x" })),
|
||||
);
|
||||
|
||||
const mocks = {
|
||||
getDefaultMediaLocalRoots: getDefaultMediaLocalRootsMock,
|
||||
dispatchChannelMessageAction: dispatchChannelMessageActionMock,
|
||||
sendMessage: sendMessageMock,
|
||||
sendPoll: sendPollMock,
|
||||
getAgentScopedMediaLocalRootsForSources: getAgentScopedMediaLocalRootsForSourcesMock,
|
||||
createAgentScopedHostMediaReadFile: createAgentScopedHostMediaReadFileMock,
|
||||
resolveAgentScopedOutboundMediaAccess: resolveAgentScopedOutboundMediaAccessMock,
|
||||
appendAssistantMessageToSessionTranscript: appendAssistantMessageToSessionTranscriptMock,
|
||||
};
|
||||
|
||||
vi.mock("../../channels/plugins/message-action-dispatch.js", () => ({
|
||||
dispatchChannelMessageAction: mocks.dispatchChannelMessageAction,
|
||||
|
|
@ -22,6 +59,7 @@ vi.mock("./message.js", () => ({
|
|||
|
||||
vi.mock("../../media/read-capability.js", () => ({
|
||||
createAgentScopedHostMediaReadFile: mocks.createAgentScopedHostMediaReadFile,
|
||||
resolveAgentScopedOutboundMediaAccess: mocks.resolveAgentScopedOutboundMediaAccess,
|
||||
}));
|
||||
|
||||
vi.mock("../../media/local-roots.js", async (importOriginal) => {
|
||||
|
|
@ -105,6 +143,7 @@ describe("executeSendAction", () => {
|
|||
mocks.getDefaultMediaLocalRoots.mockClear();
|
||||
mocks.getAgentScopedMediaLocalRootsForSources.mockClear();
|
||||
mocks.createAgentScopedHostMediaReadFile.mockClear();
|
||||
mocks.resolveAgentScopedOutboundMediaAccess.mockClear();
|
||||
mocks.appendAssistantMessageToSessionTranscript.mockClear();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ import { dispatchChannelMessageAction } from "../../channels/plugins/message-act
|
|||
import type { ChannelId, ChannelThreadingToolContext } from "../../channels/plugins/types.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { appendAssistantMessageToSessionTranscript } from "../../config/sessions.js";
|
||||
import { getAgentScopedMediaLocalRootsForSources } from "../../media/local-roots.js";
|
||||
import { createAgentScopedHostMediaReadFile } from "../../media/read-capability.js";
|
||||
import type { OutboundMediaAccess, OutboundMediaReadFile } from "../../media/load-options.js";
|
||||
import { resolveAgentScopedOutboundMediaAccess } from "../../media/read-capability.js";
|
||||
import type { GatewayClientMode, GatewayClientName } from "../../utils/message-channel.js";
|
||||
import { throwIfAborted } from "./abort.js";
|
||||
import type { OutboundSendDeps } from "./deliver.js";
|
||||
|
|
@ -28,7 +28,8 @@ export type OutboundSendContext = {
|
|||
params: Record<string, unknown>;
|
||||
/** Active agent id for per-agent outbound media root scoping. */
|
||||
agentId?: string;
|
||||
mediaReadFile?: (filePath: string) => Promise<Buffer>;
|
||||
mediaAccess?: OutboundMediaAccess;
|
||||
mediaReadFile?: OutboundMediaReadFile;
|
||||
accountId?: string | null;
|
||||
gateway?: OutboundGatewayContext;
|
||||
toolContext?: ChannelThreadingToolContext;
|
||||
|
|
@ -64,24 +65,21 @@ async function tryHandleWithPluginAction(params: {
|
|||
if (params.ctx.dryRun) {
|
||||
return null;
|
||||
}
|
||||
const mediaLocalRoots = getAgentScopedMediaLocalRootsForSources({
|
||||
const mediaAccess = resolveAgentScopedOutboundMediaAccess({
|
||||
cfg: params.ctx.cfg,
|
||||
agentId: params.ctx.agentId ?? params.ctx.mirror?.agentId,
|
||||
mediaSources: collectActionMediaSources(params.ctx.params),
|
||||
mediaAccess: params.ctx.mediaAccess,
|
||||
mediaReadFile: params.ctx.mediaReadFile,
|
||||
});
|
||||
const mediaReadFile =
|
||||
params.ctx.mediaReadFile ??
|
||||
createAgentScopedHostMediaReadFile({
|
||||
cfg: params.ctx.cfg,
|
||||
agentId: params.ctx.agentId ?? params.ctx.mirror?.agentId,
|
||||
});
|
||||
const handled = await dispatchChannelMessageAction({
|
||||
channel: params.ctx.channel,
|
||||
action: params.action,
|
||||
cfg: params.ctx.cfg,
|
||||
params: params.ctx.params,
|
||||
mediaLocalRoots,
|
||||
mediaReadFile,
|
||||
mediaAccess,
|
||||
mediaLocalRoots: mediaAccess.localRoots,
|
||||
mediaReadFile: mediaAccess.readFile,
|
||||
accountId: params.ctx.accountId ?? undefined,
|
||||
gateway: params.ctx.gateway,
|
||||
toolContext: params.ctx.toolContext,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,15 @@
|
|||
export type OutboundMediaReadFile = (filePath: string) => Promise<Buffer>;
|
||||
|
||||
export type OutboundMediaAccess = {
|
||||
localRoots?: readonly string[];
|
||||
readFile?: OutboundMediaReadFile;
|
||||
};
|
||||
|
||||
export type OutboundMediaLoadParams = {
|
||||
maxBytes?: number;
|
||||
mediaAccess?: OutboundMediaAccess;
|
||||
mediaLocalRoots?: readonly string[];
|
||||
mediaReadFile?: (filePath: string) => Promise<Buffer>;
|
||||
mediaReadFile?: OutboundMediaReadFile;
|
||||
optimizeImages?: boolean;
|
||||
};
|
||||
|
||||
|
|
@ -19,19 +27,40 @@ export function resolveOutboundMediaLocalRoots(
|
|||
return mediaLocalRoots && mediaLocalRoots.length > 0 ? mediaLocalRoots : undefined;
|
||||
}
|
||||
|
||||
export function resolveOutboundMediaAccess(
|
||||
params: {
|
||||
mediaAccess?: OutboundMediaAccess;
|
||||
mediaLocalRoots?: readonly string[];
|
||||
mediaReadFile?: OutboundMediaReadFile;
|
||||
} = {},
|
||||
): OutboundMediaAccess | undefined {
|
||||
const localRoots = resolveOutboundMediaLocalRoots(
|
||||
params.mediaAccess?.localRoots ?? params.mediaLocalRoots,
|
||||
);
|
||||
const readFile = params.mediaAccess?.readFile ?? params.mediaReadFile;
|
||||
if (!localRoots && !readFile) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
...(localRoots ? { localRoots } : {}),
|
||||
...(readFile ? { readFile } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildOutboundMediaLoadOptions(
|
||||
params: OutboundMediaLoadParams = {},
|
||||
): OutboundMediaLoadOptions {
|
||||
if (params.mediaReadFile) {
|
||||
const mediaAccess = resolveOutboundMediaAccess(params);
|
||||
if (mediaAccess?.readFile) {
|
||||
return {
|
||||
...(params.maxBytes !== undefined ? { maxBytes: params.maxBytes } : {}),
|
||||
localRoots: "any",
|
||||
readFile: params.mediaReadFile,
|
||||
readFile: mediaAccess.readFile,
|
||||
hostReadCapability: true,
|
||||
...(params.optimizeImages !== undefined ? { optimizeImages: params.optimizeImages } : {}),
|
||||
};
|
||||
}
|
||||
const localRoots = resolveOutboundMediaLocalRoots(params.mediaLocalRoots);
|
||||
const localRoots = mediaAccess?.localRoots;
|
||||
return {
|
||||
...(params.maxBytes !== undefined ? { maxBytes: params.maxBytes } : {}),
|
||||
...(localRoots ? { localRoots } : {}),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { buildOutboundMediaLoadOptions } from "./load-options.js";
|
||||
import { buildOutboundMediaLoadOptions, type OutboundMediaAccess } from "./load-options.js";
|
||||
import { saveMediaBuffer } from "./store.js";
|
||||
import { loadWebMedia } from "./web-media.js";
|
||||
|
||||
|
|
@ -6,6 +6,7 @@ export async function resolveOutboundAttachmentFromUrl(
|
|||
mediaUrl: string,
|
||||
maxBytes: number,
|
||||
options?: {
|
||||
mediaAccess?: OutboundMediaAccess;
|
||||
localRoots?: readonly string[];
|
||||
readFile?: (filePath: string) => Promise<Buffer>;
|
||||
},
|
||||
|
|
@ -14,6 +15,7 @@ export async function resolveOutboundAttachmentFromUrl(
|
|||
mediaUrl,
|
||||
buildOutboundMediaLoadOptions({
|
||||
maxBytes,
|
||||
mediaAccess: options?.mediaAccess,
|
||||
mediaLocalRoots: options?.localRoots,
|
||||
mediaReadFile: options?.readFile,
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -4,12 +4,14 @@ import { resolveEffectiveToolFsRootExpansionAllowed } from "../agents/tool-fs-po
|
|||
import { resolveWorkspaceRoot } from "../agents/workspace-dir.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { readLocalFileSafely } from "../infra/fs-safe.js";
|
||||
import type { OutboundMediaAccess, OutboundMediaReadFile } from "./load-options.js";
|
||||
import { getAgentScopedMediaLocalRootsForSources } from "./local-roots.js";
|
||||
|
||||
export function createAgentScopedHostMediaReadFile(params: {
|
||||
cfg: OpenClawConfig;
|
||||
agentId?: string;
|
||||
workspaceDir?: string;
|
||||
}): ((filePath: string) => Promise<Buffer>) | undefined {
|
||||
}): OutboundMediaReadFile | undefined {
|
||||
if (
|
||||
!resolveEffectiveToolFsRootExpansionAllowed({
|
||||
cfg: params.cfg,
|
||||
|
|
@ -27,3 +29,32 @@ export function createAgentScopedHostMediaReadFile(params: {
|
|||
return (await readLocalFileSafely({ filePath: resolvedPath })).buffer;
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveAgentScopedOutboundMediaAccess(params: {
|
||||
cfg: OpenClawConfig;
|
||||
agentId?: string;
|
||||
mediaSources?: readonly string[];
|
||||
workspaceDir?: string;
|
||||
mediaAccess?: OutboundMediaAccess;
|
||||
mediaReadFile?: OutboundMediaReadFile;
|
||||
}): OutboundMediaAccess {
|
||||
const localRoots =
|
||||
params.mediaAccess?.localRoots ??
|
||||
getAgentScopedMediaLocalRootsForSources({
|
||||
cfg: params.cfg,
|
||||
agentId: params.agentId,
|
||||
mediaSources: params.mediaSources,
|
||||
});
|
||||
const readFile =
|
||||
params.mediaAccess?.readFile ??
|
||||
params.mediaReadFile ??
|
||||
createAgentScopedHostMediaReadFile({
|
||||
cfg: params.cfg,
|
||||
agentId: params.agentId,
|
||||
workspaceDir: params.workspaceDir,
|
||||
});
|
||||
return {
|
||||
...(localRoots?.length ? { localRoots } : {}),
|
||||
...(readFile ? { readFile } : {}),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { OutboundMediaAccess } from "../media/load-options.js";
|
||||
import { attachChannelToResult } from "./channel-send-result.js";
|
||||
import type { DiscordSendResult } from "./discord.js";
|
||||
|
||||
|
|
@ -9,6 +10,7 @@ type DiscordSendOptionInput = {
|
|||
|
||||
type DiscordSendMediaOptionInput = DiscordSendOptionInput & {
|
||||
mediaUrl?: string;
|
||||
mediaAccess?: OutboundMediaAccess;
|
||||
mediaLocalRoots?: readonly string[];
|
||||
mediaReadFile?: (filePath: string) => Promise<Buffer>;
|
||||
};
|
||||
|
|
@ -28,6 +30,7 @@ export function buildDiscordSendMediaOptions(input: DiscordSendMediaOptionInput)
|
|||
return {
|
||||
...buildDiscordSendOptions(input),
|
||||
mediaUrl: input.mediaUrl,
|
||||
mediaAccess: input.mediaAccess,
|
||||
mediaLocalRoots: input.mediaLocalRoots,
|
||||
mediaReadFile: input.mediaReadFile,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ export {
|
|||
resolveDiscordAccount,
|
||||
resolveDefaultDiscordAccountId,
|
||||
} from "./discord-surface.js";
|
||||
export { loadOutboundMediaFromUrl } from "./outbound-media.js";
|
||||
export { inspectDiscordAccount } from "./discord-surface.js";
|
||||
export {
|
||||
looksLikeDiscordTargetId,
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ export { createAccountStatusSink, runPassiveAccountLifecycle } from "./channel-l
|
|||
export { formatPairingApproveHint } from "../channels/plugins/helpers.js";
|
||||
export { fetchRemoteMedia } from "../media/fetch.js";
|
||||
export { resolveChannelMediaMaxBytes } from "../channels/plugins/media-limits.js";
|
||||
export { loadOutboundMediaFromUrl } from "./outbound-media.js";
|
||||
export { loadWebMedia } from "./web-media.js";
|
||||
export { chunkTextForOutbound } from "./text-chunking.js";
|
||||
export {
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ export {
|
|||
} from "./matrix-thread-bindings.js";
|
||||
export { createTypingCallbacks } from "../channels/typing.js";
|
||||
export { createChannelReplyPipeline } from "./channel-reply-pipeline.js";
|
||||
export { loadOutboundMediaFromUrl } from "./outbound-media.js";
|
||||
export type { OpenClawConfig } from "../config/config.js";
|
||||
export {
|
||||
GROUP_POLICY_BLOCKED_LABEL,
|
||||
|
|
|
|||
|
|
@ -1,20 +1,22 @@
|
|||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const loadWebMediaMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("./web-media.js", () => ({
|
||||
loadWebMedia: loadWebMediaMock,
|
||||
}));
|
||||
|
||||
type OutboundMediaModule = typeof import("./outbound-media.js");
|
||||
|
||||
let loadOutboundMediaFromUrl: OutboundMediaModule["loadOutboundMediaFromUrl"];
|
||||
|
||||
describe("loadOutboundMediaFromUrl", () => {
|
||||
beforeAll(async () => {
|
||||
const webMedia = await import("./web-media.js");
|
||||
vi.spyOn(webMedia, "loadWebMedia").mockImplementation(loadWebMediaMock);
|
||||
({ loadOutboundMediaFromUrl } = await import("./outbound-media.js"));
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
loadWebMediaMock.mockReset();
|
||||
});
|
||||
|
|
@ -46,10 +48,7 @@ describe("loadOutboundMediaFromUrl", () => {
|
|||
|
||||
await loadOutboundMediaFromUrl("https://example.com/image.png");
|
||||
|
||||
expect(loadWebMediaMock).toHaveBeenCalledWith("https://example.com/image.png", {
|
||||
maxBytes: undefined,
|
||||
localRoots: undefined,
|
||||
});
|
||||
expect(loadWebMediaMock).toHaveBeenCalledWith("https://example.com/image.png", {});
|
||||
});
|
||||
|
||||
it("prefers host read capability over local roots when provided", async () => {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { buildOutboundMediaLoadOptions } from "../media/load-options.js";
|
||||
import { buildOutboundMediaLoadOptions, type OutboundMediaAccess } from "../media/load-options.js";
|
||||
import { loadWebMedia } from "./web-media.js";
|
||||
|
||||
export type OutboundMediaLoadOptions = {
|
||||
maxBytes?: number;
|
||||
mediaAccess?: OutboundMediaAccess;
|
||||
mediaLocalRoots?: readonly string[];
|
||||
mediaReadFile?: (filePath: string) => Promise<Buffer>;
|
||||
};
|
||||
|
|
@ -16,6 +17,7 @@ export async function loadOutboundMediaFromUrl(
|
|||
mediaUrl,
|
||||
buildOutboundMediaLoadOptions({
|
||||
maxBytes: options.maxBytes,
|
||||
mediaAccess: options.mediaAccess,
|
||||
mediaLocalRoots: options.mediaLocalRoots,
|
||||
mediaReadFile: options.mediaReadFile,
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ export {
|
|||
resolveDefaultSlackAccountId,
|
||||
resolveSlackReplyToMode,
|
||||
} from "./slack-surface.js";
|
||||
export { loadOutboundMediaFromUrl } from "./outbound-media.js";
|
||||
export { isSlackInteractiveRepliesEnabled } from "./slack-surface.js";
|
||||
export { inspectSlackAccount } from "./slack-surface.js";
|
||||
export { parseSlackTarget, resolveSlackChannelId } from "./slack-targets.js";
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ export {
|
|||
resolveAllowlistProviderRuntimeGroupPolicy,
|
||||
resolveDefaultGroupPolicy,
|
||||
} from "../config/runtime-group-policy.js";
|
||||
export { loadOutboundMediaFromUrl } from "./outbound-media.js";
|
||||
export {
|
||||
resolveWhatsAppGroupRequireMention,
|
||||
resolveWhatsAppGroupToolPolicy,
|
||||
|
|
|
|||
Loading…
Reference in New Issue