| import type { OpenClawConfig } from "../../config/config.js"; |
| import { loadConfig } from "../../config/config.js"; |
| import { callGatewayLeastPrivilege, randomIdempotencyKey } from "../../gateway/call.js"; |
| import type { PollInput } from "../../polls.js"; |
| import { normalizePollInput } from "../../polls.js"; |
| import { |
| GATEWAY_CLIENT_MODES, |
| GATEWAY_CLIENT_NAMES, |
| type GatewayClientMode, |
| type GatewayClientName, |
| } from "../../utils/message-channel.js"; |
| import { resolveOutboundChannelPlugin } from "./channel-resolution.js"; |
| import { resolveMessageChannelSelection } from "./channel-selection.js"; |
| import { |
| deliverOutboundPayloads, |
| type OutboundDeliveryResult, |
| type OutboundSendDeps, |
| } from "./deliver.js"; |
| import type { OutboundMirror } from "./mirror.js"; |
| import { normalizeReplyPayloadsForDelivery } from "./payloads.js"; |
| import { buildOutboundSessionContext } from "./session-context.js"; |
| import { resolveOutboundTarget } from "./targets.js"; |
|
|
| export type MessageGatewayOptions = { |
| url?: string; |
| token?: string; |
| timeoutMs?: number; |
| clientName?: GatewayClientName; |
| clientDisplayName?: string; |
| mode?: GatewayClientMode; |
| }; |
|
|
| type MessageSendParams = { |
| to: string; |
| content: string; |
| |
| agentId?: string; |
| channel?: string; |
| mediaUrl?: string; |
| mediaUrls?: string[]; |
| gifPlayback?: boolean; |
| accountId?: string; |
| replyToId?: string; |
| threadId?: string | number; |
| dryRun?: boolean; |
| bestEffort?: boolean; |
| deps?: OutboundSendDeps; |
| cfg?: OpenClawConfig; |
| gateway?: MessageGatewayOptions; |
| idempotencyKey?: string; |
| mirror?: OutboundMirror; |
| abortSignal?: AbortSignal; |
| silent?: boolean; |
| }; |
|
|
| export type MessageSendResult = { |
| channel: string; |
| to: string; |
| via: "direct" | "gateway"; |
| mediaUrl: string | null; |
| mediaUrls?: string[]; |
| result?: OutboundDeliveryResult | { messageId: string }; |
| dryRun?: boolean; |
| }; |
|
|
| type MessagePollParams = { |
| to: string; |
| question: string; |
| options: string[]; |
| maxSelections?: number; |
| durationSeconds?: number; |
| durationHours?: number; |
| channel?: string; |
| accountId?: string; |
| threadId?: string; |
| silent?: boolean; |
| isAnonymous?: boolean; |
| dryRun?: boolean; |
| cfg?: OpenClawConfig; |
| gateway?: MessageGatewayOptions; |
| idempotencyKey?: string; |
| }; |
|
|
| export type MessagePollResult = { |
| channel: string; |
| to: string; |
| question: string; |
| options: string[]; |
| maxSelections: number; |
| durationSeconds: number | null; |
| durationHours: number | null; |
| via: "gateway"; |
| result?: { |
| messageId: string; |
| toJid?: string; |
| channelId?: string; |
| conversationId?: string; |
| pollId?: string; |
| }; |
| dryRun?: boolean; |
| }; |
|
|
| function buildMessagePollResult(params: { |
| channel: string; |
| to: string; |
| normalized: { |
| question: string; |
| options: string[]; |
| maxSelections: number; |
| durationSeconds?: number | null; |
| durationHours?: number | null; |
| }; |
| result?: MessagePollResult["result"]; |
| dryRun?: boolean; |
| }): MessagePollResult { |
| return { |
| channel: params.channel, |
| to: params.to, |
| question: params.normalized.question, |
| options: params.normalized.options, |
| maxSelections: params.normalized.maxSelections, |
| durationSeconds: params.normalized.durationSeconds ?? null, |
| durationHours: params.normalized.durationHours ?? null, |
| via: "gateway", |
| ...(params.dryRun ? { dryRun: true } : { result: params.result }), |
| }; |
| } |
|
|
| async function resolveRequiredChannel(params: { |
| cfg: OpenClawConfig; |
| channel?: string; |
| }): Promise<string> { |
| return ( |
| await resolveMessageChannelSelection({ |
| cfg: params.cfg, |
| channel: params.channel, |
| }) |
| ).channel; |
| } |
|
|
| function resolveRequiredPlugin(channel: string, cfg: OpenClawConfig) { |
| const plugin = resolveOutboundChannelPlugin({ channel, cfg }); |
| if (!plugin) { |
| throw new Error(`Unknown channel: ${channel}`); |
| } |
| return plugin; |
| } |
|
|
| function resolveGatewayOptions(opts?: MessageGatewayOptions) { |
| |
| |
| const url = |
| opts?.mode === GATEWAY_CLIENT_MODES.BACKEND || |
| opts?.clientName === GATEWAY_CLIENT_NAMES.GATEWAY_CLIENT |
| ? undefined |
| : opts?.url; |
| return { |
| url, |
| token: opts?.token, |
| timeoutMs: |
| typeof opts?.timeoutMs === "number" && Number.isFinite(opts.timeoutMs) |
| ? Math.max(1, Math.floor(opts.timeoutMs)) |
| : 10_000, |
| clientName: opts?.clientName ?? GATEWAY_CLIENT_NAMES.CLI, |
| clientDisplayName: opts?.clientDisplayName, |
| mode: opts?.mode ?? GATEWAY_CLIENT_MODES.CLI, |
| }; |
| } |
|
|
| async function callMessageGateway<T>(params: { |
| gateway?: MessageGatewayOptions; |
| method: string; |
| params: Record<string, unknown>; |
| }): Promise<T> { |
| const gateway = resolveGatewayOptions(params.gateway); |
| return await callGatewayLeastPrivilege<T>({ |
| url: gateway.url, |
| token: gateway.token, |
| method: params.method, |
| params: params.params, |
| timeoutMs: gateway.timeoutMs, |
| clientName: gateway.clientName, |
| clientDisplayName: gateway.clientDisplayName, |
| mode: gateway.mode, |
| }); |
| } |
|
|
| export async function sendMessage(params: MessageSendParams): Promise<MessageSendResult> { |
| const cfg = params.cfg ?? loadConfig(); |
| const channel = await resolveRequiredChannel({ cfg, channel: params.channel }); |
| const plugin = resolveRequiredPlugin(channel, cfg); |
| const deliveryMode = plugin.outbound?.deliveryMode ?? "direct"; |
| const normalizedPayloads = normalizeReplyPayloadsForDelivery([ |
| { |
| text: params.content, |
| mediaUrl: params.mediaUrl, |
| mediaUrls: params.mediaUrls, |
| }, |
| ]); |
| const mirrorText = normalizedPayloads |
| .map((payload) => payload.text) |
| .filter(Boolean) |
| .join("\n"); |
| const mirrorMediaUrls = normalizedPayloads.flatMap( |
| (payload) => payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []), |
| ); |
| const primaryMediaUrl = mirrorMediaUrls[0] ?? params.mediaUrl ?? null; |
|
|
| if (params.dryRun) { |
| return { |
| channel, |
| to: params.to, |
| via: deliveryMode === "gateway" ? "gateway" : "direct", |
| mediaUrl: primaryMediaUrl, |
| mediaUrls: mirrorMediaUrls.length ? mirrorMediaUrls : undefined, |
| dryRun: true, |
| }; |
| } |
|
|
| if (deliveryMode !== "gateway") { |
| const outboundChannel = channel; |
| const resolvedTarget = resolveOutboundTarget({ |
| channel: outboundChannel, |
| to: params.to, |
| cfg, |
| accountId: params.accountId, |
| mode: "explicit", |
| }); |
| if (!resolvedTarget.ok) { |
| throw resolvedTarget.error; |
| } |
|
|
| const outboundSession = buildOutboundSessionContext({ |
| cfg, |
| agentId: params.agentId, |
| sessionKey: params.mirror?.sessionKey, |
| }); |
| const results = await deliverOutboundPayloads({ |
| cfg, |
| channel: outboundChannel, |
| to: resolvedTarget.to, |
| session: outboundSession, |
| accountId: params.accountId, |
| payloads: normalizedPayloads, |
| replyToId: params.replyToId, |
| threadId: params.threadId, |
| gifPlayback: params.gifPlayback, |
| deps: params.deps, |
| bestEffort: params.bestEffort, |
| abortSignal: params.abortSignal, |
| silent: params.silent, |
| mirror: params.mirror |
| ? { |
| ...params.mirror, |
| text: mirrorText || params.content, |
| mediaUrls: mirrorMediaUrls.length ? mirrorMediaUrls : undefined, |
| idempotencyKey: params.mirror.idempotencyKey ?? params.idempotencyKey, |
| } |
| : undefined, |
| }); |
|
|
| return { |
| channel, |
| to: params.to, |
| via: "direct", |
| mediaUrl: primaryMediaUrl, |
| mediaUrls: mirrorMediaUrls.length ? mirrorMediaUrls : undefined, |
| result: results.at(-1), |
| }; |
| } |
|
|
| const result = await callMessageGateway<{ messageId: string }>({ |
| gateway: params.gateway, |
| method: "send", |
| params: { |
| to: params.to, |
| message: params.content, |
| mediaUrl: params.mediaUrl, |
| mediaUrls: mirrorMediaUrls.length ? mirrorMediaUrls : params.mediaUrls, |
| gifPlayback: params.gifPlayback, |
| accountId: params.accountId, |
| agentId: params.agentId, |
| channel, |
| sessionKey: params.mirror?.sessionKey, |
| idempotencyKey: params.idempotencyKey ?? randomIdempotencyKey(), |
| }, |
| }); |
|
|
| return { |
| channel, |
| to: params.to, |
| via: "gateway", |
| mediaUrl: primaryMediaUrl, |
| mediaUrls: mirrorMediaUrls.length ? mirrorMediaUrls : undefined, |
| result, |
| }; |
| } |
|
|
| export async function sendPoll(params: MessagePollParams): Promise<MessagePollResult> { |
| const cfg = params.cfg ?? loadConfig(); |
| const channel = await resolveRequiredChannel({ cfg, channel: params.channel }); |
|
|
| const pollInput: PollInput = { |
| question: params.question, |
| options: params.options, |
| maxSelections: params.maxSelections, |
| durationSeconds: params.durationSeconds, |
| durationHours: params.durationHours, |
| }; |
| const plugin = resolveRequiredPlugin(channel, cfg); |
| const outbound = plugin?.outbound; |
| if (!outbound?.sendPoll) { |
| throw new Error(`Unsupported poll channel: ${channel}`); |
| } |
| const normalized = outbound.pollMaxOptions |
| ? normalizePollInput(pollInput, { maxOptions: outbound.pollMaxOptions }) |
| : normalizePollInput(pollInput); |
|
|
| if (params.dryRun) { |
| return buildMessagePollResult({ |
| channel, |
| to: params.to, |
| normalized, |
| dryRun: true, |
| }); |
| } |
|
|
| const result = await callMessageGateway<{ |
| messageId: string; |
| toJid?: string; |
| channelId?: string; |
| conversationId?: string; |
| pollId?: string; |
| }>({ |
| gateway: params.gateway, |
| method: "poll", |
| params: { |
| to: params.to, |
| question: normalized.question, |
| options: normalized.options, |
| maxSelections: normalized.maxSelections, |
| durationSeconds: normalized.durationSeconds, |
| durationHours: normalized.durationHours, |
| threadId: params.threadId, |
| silent: params.silent, |
| isAnonymous: params.isAnonymous, |
| channel, |
| accountId: params.accountId, |
| idempotencyKey: params.idempotencyKey ?? randomIdempotencyKey(), |
| }, |
| }); |
|
|
| return buildMessagePollResult({ |
| channel, |
| to: params.to, |
| normalized, |
| result, |
| }); |
| } |
|
|