Spracherkennung für: .ts vermutete Sprache: Unknown {[0] [0] [0]} [Methode: Schwerpunktbildung, einfache Gewichte, sechs Dimensionen]
import {
type ChannelOutboundAdapter,
createAttachedChannelResultAdapter,
} from "openclaw/plugin-sdk/channel-send-result";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import {
resolveOutboundSendDep,
type OutboundIdentity,
} from "openclaw/plugin-sdk/outbound-runtime";
import {
normalizeOptionalString,
normalizeOptionalStringifiedId,
} from "openclaw/plugin-sdk/text-runtime";
import { chunkDiscordTextWithMode } from "./chunk.js";
import { withDiscordDeliveryRetry } from "./delivery-retry.js";
import { isLikelyDiscordVideoMedia } from "./media-detection.js";
import type { ThreadBindingRecord } from "./monitor/thread-bindings.js";
import { normalizeDiscordOutboundTarget } from "./normalize.js";
import { normalizeDiscordApprovalPayload } from "./outbound-approval.js";
import { buildDiscordPresentationPayload } from "./outbound-components.js";
import { sendDiscordOutboundPayload } from "./outbound-payload.js";
import {
loadDiscordSendRuntime,
resolveDiscordFormattingOptions,
resolveDiscordOutboundTarget,
type DiscordSendFn,
type DiscordVoiceSendFn,
} from "./outbound-send-context.js";
export const DISCORD_TEXT_CHUNK_LIMIT = 2000;
type DiscordThreadBindingsModule = typeof import("./monitor/thread-bindings.js");
let discordThreadBindingsPromise: Promise<DiscordThreadBindingsModule> | undefined;
function loadDiscordThreadBindings(): Promise<DiscordThreadBindingsModule> {
discordThreadBindingsPromise ??= import("./monitor/thread-bindings.js");
return discordThreadBindingsPromise;
}
function resolveDiscordWebhookIdentity(params: {
identity?: OutboundIdentity;
binding: ThreadBindingRecord;
}): { username?: string; avatarUrl?: string } {
const usernameRaw = normalizeOptionalString(params.identity?.name);
const fallbackUsername = normalizeOptionalString(params.binding.label) ?? params.binding.agentId;
const username = (usernameRaw || fallbackUsername || "").slice(0, 80) || undefined;
const avatarUrl = normalizeOptionalString(params.identity?.avatarUrl);
return { username, avatarUrl };
}
async function maybeSendDiscordWebhookText(params: {
cfg: OpenClawConfig;
text: string;
threadId?: string | number | null;
accountId?: string | null;
identity?: OutboundIdentity;
replyToId?: string | null;
}): Promise<{ messageId: string; channelId: string } | null> {
if (params.threadId == null) {
return null;
}
const threadId = normalizeOptionalStringifiedId(params.threadId) ?? "";
if (!threadId) {
return null;
}
const { getThreadBindingManager } = await loadDiscordThreadBindings();
const manager = getThreadBindingManager(params.accountId ?? undefined);
if (!manager) {
return null;
}
const binding = manager.getByThreadId(threadId);
if (!binding?.webhookId || !binding?.webhookToken) {
return null;
}
const persona = resolveDiscordWebhookIdentity({
identity: params.identity,
binding,
});
const { sendWebhookMessageDiscord } = await loadDiscordSendRuntime();
const result = await sendWebhookMessageDiscord(params.text, {
webhookId: binding.webhookId,
webhookToken: binding.webhookToken,
accountId: binding.accountId,
threadId: binding.threadId,
cfg: params.cfg,
replyTo: params.replyToId ?? undefined,
username: persona.username,
avatarUrl: persona.avatarUrl,
});
return result;
}
export const discordOutbound: ChannelOutboundAdapter = {
deliveryMode: "direct",
chunker: (text, limit, ctx) =>
chunkDiscordTextWithMode(text, {
maxChars: limit,
maxLines: ctx?.formatting?.maxLinesPerMessage,
}),
textChunkLimit: DISCORD_TEXT_CHUNK_LIMIT,
pollMaxOptions: 10,
normalizePayload: ({ payload }) => normalizeDiscordApprovalPayload(payload),
presentationCapabilities: {
supported: true,
buttons: true,
selects: true,
context: true,
divider: true,
},
renderPresentation: async ({ payload, presentation }) => {
return await buildDiscordPresentationPayload({
payload,
presentation,
});
},
resolveTarget: ({ to }) => normalizeDiscordOutboundTarget(to),
sendPayload: async (ctx) =>
await sendDiscordOutboundPayload({
ctx,
fallbackAdapter: discordOutbound,
}),
...createAttachedChannelResultAdapter({
channel: "discord",
sendText: async ({
cfg,
to,
text,
accountId,
deps,
replyToId,
threadId,
identity,
silent,
formatting,
}) => {
if (!silent) {
const webhookResult = await maybeSendDiscordWebhookText({
cfg,
text,
threadId,
accountId,
identity,
replyToId,
}).catch(() => null);
if (webhookResult) {
return webhookResult;
}
}
const send =
resolveOutboundSendDep<DiscordSendFn>(deps, "discord") ??
(await loadDiscordSendRuntime()).sendMessageDiscord;
return await withDiscordDeliveryRetry({
cfg,
accountId,
fn: async () =>
await send(resolveDiscordOutboundTarget({ to, threadId }), text, {
verbose: false,
replyTo: replyToId ?? undefined,
accountId: accountId ?? undefined,
silent: silent ?? undefined,
cfg,
...resolveDiscordFormattingOptions({ formatting }),
}),
});
},
sendMedia: async ({
cfg,
to,
text,
mediaUrl,
audioAsVoice,
mediaAccess,
mediaLocalRoots,
mediaReadFile,
accountId,
deps,
replyToId,
threadId,
silent,
formatting,
}) => {
const send =
resolveOutboundSendDep<DiscordSendFn>(deps, "discord") ??
(await loadDiscordSendRuntime()).sendMessageDiscord;
const target = resolveDiscordOutboundTarget({ to, threadId });
const formattingOptions = resolveDiscordFormattingOptions({ formatting });
if (audioAsVoice && mediaUrl) {
const sendVoice =
resolveOutboundSendDep<DiscordVoiceSendFn>(deps, "discordVoice") ??
(await loadDiscordSendRuntime()).sendVoiceMessageDiscord;
return await withDiscordDeliveryRetry({
cfg,
accountId,
fn: async () =>
await sendVoice(target, mediaUrl, {
cfg,
replyTo: replyToId ?? undefined,
accountId: accountId ?? undefined,
silent: silent ?? undefined,
}),
});
}
if (text.trim() && mediaUrl && isLikelyDiscordVideoMedia(mediaUrl)) {
await withDiscordDeliveryRetry({
cfg,
accountId,
fn: async () =>
await send(target, text, {
verbose: false,
replyTo: replyToId ?? undefined,
accountId: accountId ?? undefined,
silent: silent ?? undefined,
cfg,
...formattingOptions,
}),
});
return await withDiscordDeliveryRetry({
cfg,
accountId,
fn: async () =>
await send(target, "", {
verbose: false,
mediaUrl,
mediaAccess,
mediaLocalRoots,
mediaReadFile,
accountId: accountId ?? undefined,
silent: silent ?? undefined,
cfg,
...formattingOptions,
}),
});
}
return await withDiscordDeliveryRetry({
cfg,
accountId,
fn: async () =>
await send(target, text, {
verbose: false,
mediaUrl,
mediaAccess,
mediaLocalRoots,
mediaReadFile,
replyTo: replyToId ?? undefined,
accountId: accountId ?? undefined,
silent: silent ?? undefined,
cfg,
...formattingOptions,
}),
});
},
sendPoll: async ({ cfg, to, poll, accountId, threadId, silent }) =>
await withDiscordDeliveryRetry({
cfg,
accountId,
fn: async () =>
await (
await loadDiscordSendRuntime()
).sendPollDiscord(resolveDiscordOutboundTarget({ to, threadId }), poll, {
accountId: accountId ?? undefined,
silent: silent ?? undefined,
cfg,
}),
}),
}),
afterDeliverPayload: async ({ target }) => {
const threadId = normalizeOptionalStringifiedId(target.threadId);
if (!threadId) {
return;
}
const { getThreadBindingManager } = await loadDiscordThreadBindings();
const manager = getThreadBindingManager(target.accountId ?? undefined);
if (!manager?.getByThreadId(threadId)) {
return;
}
manager.touchThread({ threadId });
},
};
¤ Dauer der Verarbeitung: 0.20 Sekunden
(vorverarbeitet am 2026-04-27)
¤
*© Formatika GbR, Deutschland
|
|