openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
517 lines (516 loc) • 18.8 kB
JavaScript
import { C as resolveExpiresAtMsFromDurationMs, S as resolveDateTimestampMs, m as isFutureDateTimestampMs, o as asDateTimestampMs } from "./number-coercion-CJQ8TR--.js";
import { t as resolveGlobalMap } from "./global-singleton-PwlQSEal.js";
import { _ as uniqueStrings } from "./string-normalization-WNUDCpXX.js";
import "./number-runtime-DBLVDypr.js";
import "./string-coerce-runtime-CEGJWkQ_.js";
import "./routing-CQ63ICPX.js";
import "./secret-input-DVCzFGxN.js";
import { n as recordChannelActivity } from "./channel-activity-4piA219h.js";
import { t as requireRuntimeConfig } from "./plugin-config-runtime-CpE6DYgJ.js";
import { n as loadOutboundMediaFromUrl } from "./outbound-media-MHBXtJsJ.js";
import "./channel-plugin-common-DTFXpVi6.js";
import "./channel-status-DxlXr0Xh.js";
import "./channel-actions-BOfvC-Gl.js";
import { s as resolveDiscordAccount } from "./accounts-BI_6uJlY.js";
import "./config-api-CO6ylNeg.js";
import "./channel-api-_pvHxxap.js";
import "./outbound-session-route-BzeFQp5N.js";
import { $ as createChannelMessage, C as serializePayload, It as ChannelType, it as editChannelMessage } from "./discord-DdqXEGEm.js";
import { F as createDiscordClient, P as parseAndResolveChannelRecipient, b as stripUndefinedFields, h as SUPPRESS_NOTIFICATIONS_FLAG, l as resolveChannelId, m as toDiscordFileBlob, t as buildDiscordSendError, u as resolveDiscordChannelType } from "./send.shared-SdWkx4sF.js";
import { n as createDiscordSendResult } from "./send.receipt-CUG417Vx.js";
import { t as sendMessageDiscord } from "./send.outbound-Df86oBHP.js";
import { a as buildDiscordComponentMessageFlags, c as resolveDiscordComponentAttachmentName, i as buildDiscordComponentMessage } from "./components-CDHzCfc9.js";
import { n as getOptionalDiscordRuntime } from "./runtime-n11XewvP.js";
//#region extensions/discord/src/components-registry.ts
const DEFAULT_COMPONENT_TTL_MS = 1800 * 1e3;
const PERSISTENT_COMPONENT_NAMESPACE = "discord.components";
const PERSISTENT_MODAL_NAMESPACE = "discord.modals";
const PERSISTENT_COMPONENT_MAX_ENTRIES = 500;
const PERSISTENT_MODAL_MAX_ENTRIES = 500;
const DISCORD_COMPONENT_ENTRIES_KEY = Symbol.for("openclaw.discord.componentEntries");
const DISCORD_MODAL_ENTRIES_KEY = Symbol.for("openclaw.discord.modalEntries");
let componentEntries;
let modalEntries;
let persistentComponentStore;
let persistentModalStore;
let persistentRegistryDisabled = false;
function getComponentEntries() {
componentEntries ??= resolveGlobalMap(DISCORD_COMPONENT_ENTRIES_KEY);
return componentEntries;
}
function getModalEntries() {
modalEntries ??= resolveGlobalMap(DISCORD_MODAL_ENTRIES_KEY);
return modalEntries;
}
function reportPersistentComponentRegistryError(error) {
try {
getOptionalDiscordRuntime()?.logging.getChildLogger({
plugin: "discord",
feature: "component-registry-state"
}).warn("Discord persistent component registry state failed", formatRegistryError(error));
} catch {}
}
function formatRegistryError(error) {
if (!(error instanceof Error)) return { error: formatRegistryErrorValue(error) };
const details = {
error: String(error),
errorName: error.name,
errorMessage: error.message
};
if (error.stack) details.errorStack = error.stack;
const cause = error.cause;
if (cause instanceof Error) {
details.errorCause = String(cause);
details.errorCauseName = cause.name;
details.errorCauseMessage = cause.message;
if (cause.stack) details.errorCauseStack = cause.stack;
} else if (cause !== void 0) details.errorCause = formatRegistryErrorValue(cause);
return details;
}
function formatRegistryErrorValue(value) {
if (typeof value === "string") return value;
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint" || typeof value === "symbol") return String(value);
if (value === null) return "null";
try {
return JSON.stringify(value) ?? Object.prototype.toString.call(value);
} catch {
return Object.prototype.toString.call(value);
}
}
function disablePersistentComponentRegistry(error) {
persistentRegistryDisabled = true;
persistentComponentStore = void 0;
persistentModalStore = void 0;
reportPersistentComponentRegistryError(error);
}
function getPersistentComponentStore() {
if (persistentRegistryDisabled) return;
if (persistentComponentStore) return persistentComponentStore;
const runtime = getOptionalDiscordRuntime();
if (!runtime) return;
try {
persistentComponentStore = runtime.state.openKeyedStore({
namespace: PERSISTENT_COMPONENT_NAMESPACE,
maxEntries: PERSISTENT_COMPONENT_MAX_ENTRIES,
defaultTtlMs: DEFAULT_COMPONENT_TTL_MS
});
return persistentComponentStore;
} catch (error) {
disablePersistentComponentRegistry(error);
return;
}
}
function getPersistentModalStore() {
if (persistentRegistryDisabled) return;
if (persistentModalStore) return persistentModalStore;
const runtime = getOptionalDiscordRuntime();
if (!runtime) return;
try {
persistentModalStore = runtime.state.openKeyedStore({
namespace: PERSISTENT_MODAL_NAMESPACE,
maxEntries: PERSISTENT_MODAL_MAX_ENTRIES,
defaultTtlMs: DEFAULT_COMPONENT_TTL_MS
});
return persistentModalStore;
} catch (error) {
disablePersistentComponentRegistry(error);
return;
}
}
function isExpired(entry, now) {
return entry.expiresAt !== void 0 && !isFutureDateTimestampMs(entry.expiresAt, { nowMs: now });
}
function normalizeEntryTimestamps(entry, now, ttlMs) {
const createdAt = resolveDateTimestampMs(entry.createdAt, now);
const expiresAt = asDateTimestampMs(entry.expiresAt) ?? resolveExpiresAtMsFromDurationMs(ttlMs, { nowMs: createdAt }) ?? 0;
return {
...entry,
createdAt,
expiresAt
};
}
function pruneUndefinedRegistryValues(value) {
if (Array.isArray(value)) return value.filter((entry) => entry !== void 0).map((entry) => pruneUndefinedRegistryValues(entry));
if (!value || typeof value !== "object") return value;
const result = {};
for (const [key, entry] of Object.entries(value)) {
if (entry === void 0) continue;
result[key] = pruneUndefinedRegistryValues(entry);
}
return result;
}
function registerEntries(entries, store, params) {
const normalizedEntries = [];
for (const entry of entries) {
const normalized = normalizeEntryTimestamps({
...entry,
messageId: params.messageId ?? entry.messageId
}, params.now, params.ttlMs);
store.set(entry.id, normalized);
normalizedEntries.push(normalized);
}
return normalizedEntries;
}
function resolveEntry(store, params) {
const entry = store.get(params.id);
if (!entry) return null;
if (isExpired(entry, Date.now())) {
store.delete(params.id);
return null;
}
if (params.consume !== false) store.delete(params.id);
return entry;
}
function readPersistedRegistryEntry(persisted) {
if (persisted?.version !== 1 || typeof persisted.entry?.id !== "string") return null;
return persisted.entry;
}
function registerPersistentRegistryEntries(params) {
if (params.entries.length === 0) return;
const store = params.openStore();
if (!store) return;
for (const entry of params.entries) {
const persistedEntry = pruneUndefinedRegistryValues(entry);
store.register(entry.id, {
version: 1,
entry: persistedEntry
}, { ttlMs: params.ttlMs }).catch(disablePersistentComponentRegistry);
}
}
function registerPersistentEntries(params) {
registerPersistentRegistryEntries({
entries: params.entries,
ttlMs: params.ttlMs,
openStore: getPersistentComponentStore
});
registerPersistentRegistryEntries({
entries: params.modals,
ttlMs: params.ttlMs,
openStore: getPersistentModalStore
});
}
function deletePersistentEntry(params) {
const store = params.openStore();
if (!store) return;
store.delete(params.id).catch(disablePersistentComponentRegistry);
}
function resolveComponentConsumptionIds(entry) {
if (!entry.consumptionGroupId) return [entry.id];
const ids = entry.consumptionGroupEntryIds?.filter((id) => typeof id === "string" && id) ?? [];
return ids.length > 0 ? uniqueStrings(ids) : [entry.id];
}
function deleteComponentConsumptionGroup(entry) {
const store = getComponentEntries();
for (const id of resolveComponentConsumptionIds(entry)) store.delete(id);
}
function deletePersistentComponentConsumptionGroup(entry) {
const store = getPersistentComponentStore();
if (!store) return;
for (const id of resolveComponentConsumptionIds(entry)) store.delete(id).catch(disablePersistentComponentRegistry);
}
async function resolvePersistentRegistryEntry(params) {
const store = params.openStore();
if (!store) return null;
try {
return readPersistedRegistryEntry(params.consume === false ? await store.lookup(params.id) : await store.consume(params.id));
} catch (error) {
disablePersistentComponentRegistry(error);
return null;
}
}
function registerDiscordComponentEntries(params) {
const now = Date.now();
const ttlMs = params.ttlMs ?? DEFAULT_COMPONENT_TTL_MS;
registerPersistentEntries({
entries: registerEntries(params.entries, getComponentEntries(), {
now,
ttlMs,
messageId: params.messageId
}),
modals: registerEntries(params.modals, getModalEntries(), {
now,
ttlMs,
messageId: params.messageId
}),
ttlMs
});
}
function resolveDiscordComponentEntry(params) {
const entry = resolveEntry(getComponentEntries(), params);
if (entry && params.consume !== false) deleteComponentConsumptionGroup(entry);
return entry;
}
async function resolveDiscordComponentEntryWithPersistence(params) {
const inMemory = resolveDiscordComponentEntry(params);
if (inMemory) {
if (params.consume !== false) deletePersistentComponentConsumptionGroup(inMemory);
return inMemory;
}
const persisted = await resolvePersistentRegistryEntry({
...params,
openStore: getPersistentComponentStore
});
if (persisted && params.consume !== false) deletePersistentComponentConsumptionGroup(persisted);
return persisted;
}
function resolveDiscordModalEntry(params) {
return resolveEntry(getModalEntries(), params);
}
async function resolveDiscordModalEntryWithPersistence(params) {
const inMemory = resolveDiscordModalEntry(params);
if (inMemory) {
if (params.consume !== false) deletePersistentEntry({
...params,
openStore: getPersistentModalStore
});
return inMemory;
}
return await resolvePersistentRegistryEntry({
...params,
openStore: getPersistentModalStore
});
}
//#endregion
//#region extensions/discord/src/send.components.ts
const DISCORD_FORUM_LIKE_TYPES = new Set([ChannelType.GuildForum, ChannelType.GuildMedia]);
function extractComponentAttachmentNames(spec) {
const names = [];
for (const block of spec.blocks ?? []) if (block.type === "file") names.push(resolveDiscordComponentAttachmentName(block.file));
return names;
}
function hasComponentAttachmentBlock(spec) {
return (spec.blocks ?? []).some((block) => block.type === "file");
}
function withImplicitComponentAttachmentBlock(spec, attachmentName) {
if (!attachmentName || hasComponentAttachmentBlock(spec)) return spec;
return {
...spec,
blocks: [...spec.blocks ?? [], {
type: "file",
file: `attachment://${attachmentName}`
}]
};
}
function hasClassicOnlyBlocks(spec) {
return (spec.blocks ?? []).every((block) => block.type === "text" || block.type === "file");
}
function hasUnsupportedClassicFeatures(spec) {
return Boolean(spec.modal || spec.container);
}
function hasAtMostOneNonSpoilerFile(spec) {
let fileBlockCount = 0;
for (const block of spec.blocks ?? []) {
if (block.type !== "file") continue;
fileBlockCount += 1;
if (block.spoiler) return false;
}
return fileBlockCount <= 1;
}
/**
* Keep the downgrade rules explicit because this path is only safe when the
* spec means exactly what a plain Discord message can represent.
*/
function getClassicDiscordMessageDecision(spec) {
if (hasUnsupportedClassicFeatures(spec)) return {
mode: "components",
reason: "unsupported-feature"
};
if (!hasClassicOnlyBlocks(spec)) return {
mode: "components",
reason: "unsupported-block"
};
if (!hasAtMostOneNonSpoilerFile(spec)) return {
mode: "components",
reason: "multiple-or-spoiler-files"
};
return {
mode: "classic",
reason: "plain-text-single-file"
};
}
function collapseClassicComponentText(spec) {
const parts = [];
const addPart = (value) => {
if (typeof value !== "string") return;
const trimmed = value.trim();
if (!trimmed || parts.includes(trimmed)) return;
parts.push(trimmed);
};
addPart(spec.text);
for (const block of spec.blocks ?? []) if (block.type === "text") addPart(block.text);
return parts.join("\n\n");
}
function registerBuiltDiscordComponentMessage(params) {
registerDiscordComponentEntries({
entries: params.buildResult.entries,
modals: params.buildResult.modals,
messageId: params.messageId,
ttlMs: params.ttlMs
});
}
function resolveDiscordComponentRegistryTtlMs(accountConfig) {
const ttlMs = accountConfig?.agentComponents?.ttlMs;
return typeof ttlMs === "number" && Number.isFinite(ttlMs) && ttlMs > 0 ? Math.floor(ttlMs) : void 0;
}
async function buildDiscordComponentPayload(params) {
const messageReference = params.opts.replyTo ? {
message_id: params.opts.replyTo,
fail_if_not_exists: false
} : void 0;
let spec = params.spec;
let resolvedFileName;
let files;
if (params.opts.mediaUrl) {
const media = await loadOutboundMediaFromUrl(params.opts.mediaUrl, {
mediaAccess: params.opts.mediaAccess,
mediaLocalRoots: params.opts.mediaLocalRoots,
mediaReadFile: params.opts.mediaReadFile
});
resolvedFileName = params.opts.filename?.trim() || media.fileName || "upload";
spec = withImplicitComponentAttachmentBlock(spec, resolvedFileName);
files = [{
data: toDiscordFileBlob(media.buffer),
name: resolvedFileName
}];
}
const uniqueAttachmentNames = uniqueStrings(extractComponentAttachmentNames(spec));
if (uniqueAttachmentNames.length > 1) throw new Error("Discord component attachments currently support a single file. Use media-gallery for multiple files.");
const expectedAttachmentName = uniqueAttachmentNames[0];
if (expectedAttachmentName && resolvedFileName && expectedAttachmentName !== resolvedFileName) throw new Error(`Component file block expects attachment "${expectedAttachmentName}", but the uploaded file is "${resolvedFileName}". Update components.blocks[].file or provide a matching filename.`);
if (!params.opts.mediaUrl && expectedAttachmentName) throw new Error("Discord component file blocks require a media attachment (media/path/filePath).");
const buildResult = buildDiscordComponentMessage({
spec,
sessionKey: params.opts.sessionKey,
agentId: params.opts.agentId,
accountId: params.accountId
});
const flags = buildDiscordComponentMessageFlags(buildResult.components);
const finalFlags = params.opts.silent ? (flags ?? 0) | SUPPRESS_NOTIFICATIONS_FLAG : flags ?? void 0;
return {
body: stripUndefinedFields({
...serializePayload({
components: buildResult.components,
...finalFlags ? { flags: finalFlags } : {},
...files ? { files } : {}
}),
...messageReference ? { message_reference: messageReference } : {}
}),
buildResult
};
}
async function sendDiscordComponentMessage(to, spec, opts) {
const classicDecision = getClassicDiscordMessageDecision(spec);
if (opts.mediaUrl && classicDecision.mode === "classic") return await sendMessageDiscord(to, collapseClassicComponentText(spec), {
cfg: opts.cfg,
accountId: opts.accountId,
token: opts.token,
rest: opts.rest,
mediaUrl: opts.mediaUrl,
filename: opts.filename,
mediaLocalRoots: opts.mediaLocalRoots,
mediaReadFile: opts.mediaReadFile,
mediaAccess: opts.mediaAccess,
replyTo: opts.replyTo,
silent: opts.silent,
textLimit: opts.textLimit,
maxLinesPerMessage: opts.maxLinesPerMessage,
tableMode: opts.tableMode,
chunkMode: opts.chunkMode,
...opts.suppressEmbeds === void 0 ? {} : { suppressEmbeds: opts.suppressEmbeds }
});
const cfg = requireRuntimeConfig(opts.cfg, "Discord component send");
const accountInfo = resolveDiscordAccount({
cfg,
accountId: opts.accountId
});
const { token, rest, request } = createDiscordClient({
...opts,
cfg
});
const { channelId } = await resolveChannelId(rest, await parseAndResolveChannelRecipient(to, cfg, opts.accountId), request);
const channelType = await resolveDiscordChannelType(rest, channelId);
if (channelType && DISCORD_FORUM_LIKE_TYPES.has(channelType)) throw new Error("Discord components are not supported in forum-style channels");
const { body, buildResult } = await buildDiscordComponentPayload({
spec,
opts,
accountId: accountInfo.accountId
});
let result;
try {
result = await request(() => createChannelMessage(rest, channelId, { body }), "components");
} catch (err) {
throw await buildDiscordSendError(err, {
channelId,
cfg,
rest,
token,
hasMedia: Boolean(opts.mediaUrl)
});
}
registerBuiltDiscordComponentMessage({
buildResult,
messageId: result.id,
ttlMs: resolveDiscordComponentRegistryTtlMs(accountInfo.config)
});
recordChannelActivity({
channel: "discord",
accountId: accountInfo.accountId,
direction: "outbound"
});
return createDiscordSendResult({
result,
fallbackChannelId: channelId,
kind: "card",
...opts.replyTo ? { replyToId: opts.replyTo } : {}
});
}
async function editDiscordComponentMessage(to, messageId, spec, opts) {
const cfg = requireRuntimeConfig(opts.cfg, "Discord component edit");
const accountInfo = resolveDiscordAccount({
cfg,
accountId: opts.accountId
});
const { token, rest, request } = createDiscordClient({
...opts,
cfg
});
const { channelId } = await resolveChannelId(rest, await parseAndResolveChannelRecipient(to, cfg, opts.accountId), request);
const { body, buildResult } = await buildDiscordComponentPayload({
spec,
opts,
accountId: accountInfo.accountId
});
let result;
try {
result = await request(() => editChannelMessage(rest, channelId, messageId, { body }), "components");
} catch (err) {
throw await buildDiscordSendError(err, {
channelId,
cfg,
rest,
token,
hasMedia: Boolean(opts.mediaUrl)
});
}
registerBuiltDiscordComponentMessage({
buildResult,
messageId: result.id ?? messageId,
ttlMs: resolveDiscordComponentRegistryTtlMs(accountInfo.config)
});
recordChannelActivity({
channel: "discord",
accountId: accountInfo.accountId,
direction: "outbound"
});
return createDiscordSendResult({
result: {
id: result.id ?? messageId,
channel_id: result.channel_id
},
fallbackChannelId: channelId,
kind: "card",
...opts.replyTo ? { replyToId: opts.replyTo } : {}
});
}
//#endregion
export { resolveDiscordModalEntryWithPersistence as a, resolveDiscordComponentEntryWithPersistence as i, registerBuiltDiscordComponentMessage as n, sendDiscordComponentMessage as r, editDiscordComponentMessage as t };