Files
Moltbot/src/infra/outbound/message.ts
nyanjou 77df8b1104 feat(discord): add silent message support (SUPPRESS_NOTIFICATIONS flag)
- Add silent option to message tool for Discord
- Passes SUPPRESS_NOTIFICATIONS flag (4096) to Discord API
- Threads silent param through entire outbound chain:
  - message-action-runner.ts
  - outbound-send-service.ts
  - message.ts
  - deliver.ts
  - discord outbound adapter
  - send.outbound.ts
  - send.shared.ts

Usage: message tool with silent=true suppresses push/desktop notifications
2026-02-13 12:44:14 -06:00

303 lines
8.3 KiB
TypeScript

import type { OpenClawConfig } from "../../config/config.js";
import type { PollInput } from "../../polls.js";
import { getChannelPlugin, normalizeChannelId } from "../../channels/plugins/index.js";
import { loadConfig } from "../../config/config.js";
import { callGateway, randomIdempotencyKey } from "../../gateway/call.js";
import { normalizePollInput } from "../../polls.js";
import {
GATEWAY_CLIENT_MODES,
GATEWAY_CLIENT_NAMES,
type GatewayClientMode,
type GatewayClientName,
} from "../../utils/message-channel.js";
import { resolveMessageChannelSelection } from "./channel-selection.js";
import {
deliverOutboundPayloads,
type OutboundDeliveryResult,
type OutboundSendDeps,
} from "./deliver.js";
import { normalizeReplyPayloadsForDelivery } from "./payloads.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;
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?: {
sessionKey: string;
agentId?: string;
text?: string;
mediaUrls?: string[];
};
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;
durationHours?: number;
channel?: string;
dryRun?: boolean;
cfg?: OpenClawConfig;
gateway?: MessageGatewayOptions;
idempotencyKey?: string;
};
export type MessagePollResult = {
channel: string;
to: string;
question: string;
options: string[];
maxSelections: number;
durationHours: number | null;
via: "gateway";
result?: {
messageId: string;
toJid?: string;
channelId?: string;
conversationId?: string;
pollId?: string;
};
dryRun?: boolean;
};
function resolveGatewayOptions(opts?: MessageGatewayOptions) {
return {
url: opts?.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,
};
}
export async function sendMessage(params: MessageSendParams): Promise<MessageSendResult> {
const cfg = params.cfg ?? loadConfig();
const channel = params.channel?.trim()
? normalizeChannelId(params.channel)
: (await resolveMessageChannelSelection({ cfg })).channel;
if (!channel) {
throw new Error(`Unknown channel: ${params.channel}`);
}
const plugin = getChannelPlugin(channel);
if (!plugin) {
throw new Error(`Unknown channel: ${channel}`);
}
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 results = await deliverOutboundPayloads({
cfg,
channel: outboundChannel,
to: resolvedTarget.to,
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,
}
: undefined,
});
return {
channel,
to: params.to,
via: "direct",
mediaUrl: primaryMediaUrl,
mediaUrls: mirrorMediaUrls.length ? mirrorMediaUrls : undefined,
result: results.at(-1),
};
}
const gateway = resolveGatewayOptions(params.gateway);
const result = await callGateway<{ messageId: string }>({
url: gateway.url,
token: gateway.token,
method: "send",
params: {
to: params.to,
message: params.content,
mediaUrl: params.mediaUrl,
mediaUrls: mirrorMediaUrls.length ? mirrorMediaUrls : params.mediaUrls,
gifPlayback: params.gifPlayback,
accountId: params.accountId,
channel,
sessionKey: params.mirror?.sessionKey,
idempotencyKey: params.idempotencyKey ?? randomIdempotencyKey(),
},
timeoutMs: gateway.timeoutMs,
clientName: gateway.clientName,
clientDisplayName: gateway.clientDisplayName,
mode: gateway.mode,
});
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 = params.channel?.trim()
? normalizeChannelId(params.channel)
: (await resolveMessageChannelSelection({ cfg })).channel;
if (!channel) {
throw new Error(`Unknown channel: ${params.channel}`);
}
const pollInput: PollInput = {
question: params.question,
options: params.options,
maxSelections: params.maxSelections,
durationHours: params.durationHours,
};
const plugin = getChannelPlugin(channel);
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 {
channel,
to: params.to,
question: normalized.question,
options: normalized.options,
maxSelections: normalized.maxSelections,
durationHours: normalized.durationHours ?? null,
via: "gateway",
dryRun: true,
};
}
const gateway = resolveGatewayOptions(params.gateway);
const result = await callGateway<{
messageId: string;
toJid?: string;
channelId?: string;
conversationId?: string;
pollId?: string;
}>({
url: gateway.url,
token: gateway.token,
method: "poll",
params: {
to: params.to,
question: normalized.question,
options: normalized.options,
maxSelections: normalized.maxSelections,
durationHours: normalized.durationHours,
channel,
idempotencyKey: params.idempotencyKey ?? randomIdempotencyKey(),
},
timeoutMs: gateway.timeoutMs,
clientName: gateway.clientName,
clientDisplayName: gateway.clientDisplayName,
mode: gateway.mode,
});
return {
channel,
to: params.to,
question: normalized.question,
options: normalized.options,
maxSelections: normalized.maxSelections,
durationHours: normalized.durationHours ?? null,
via: "gateway",
result,
};
}