mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-03 03:46:21 +00:00
* refactor: share talk event metric extraction * refactor: reuse shared coercion helpers * refactor: reuse shared primitive guards * refactor: reuse shared record guard * refactor: reuse shared primitive helpers * refactor: reuse shared string guards * refactor: reuse shared non-empty string guard * refactor: share plugin primitive coercion helpers * refactor: reuse plugin coercion helpers * refactor: reuse plugin coercion helpers in more plugins * refactor: reuse channel coercion helpers * refactor: reuse monitor coercion helpers * refactor: reuse provider coercion helpers * refactor: reuse core coercion helpers * refactor: reuse runtime coercion helpers * refactor: reuse helper coercion in codex paths * refactor: reuse helper coercion in runtime paths * refactor: reuse codex app-server coercion helpers * refactor: reuse codex record helpers * refactor: reuse migration and qa record helpers * refactor: reuse feishu and core helper guards * refactor: reuse browser and policy coercion helpers * refactor: reuse memory wiki record helper * refactor: share boolean coercion helpers * refactor: reuse finite number coercion * refactor: reuse trimmed string list helpers * refactor: reuse string list normalization * refactor: reuse remaining string list helpers * refactor: reuse string entry normalizer * refactor: share sorted string helpers * refactor: share string list normalization * test: preserve command registry browser imports * refactor: reuse trimmed list helpers * refactor: reuse string dedupe helpers * refactor: reuse local dedupe helpers * refactor: reuse more string dedupe helpers * refactor: reuse command string dedupe helpers * refactor: dedupe memory path lists with helper * refactor: expose string dedupe helpers to plugins * refactor: reuse core string dedupe helpers * refactor: reuse shared unique value helpers * refactor: reuse unique helpers in agent utilities * refactor: reuse unique helpers in config plumbing * refactor: reuse unique helpers in extensions * refactor: reuse unique helpers in core utilities * refactor: reuse unique helpers in qa plugins * refactor: reuse unique helpers in memory plugins * refactor: reuse unique helpers in channel plugins * refactor: reuse unique helpers in core tails * refactor: reuse unique helper in comfy workflow * refactor: reuse unique helpers in test utilities * refactor: expose unique value helper to plugins * refactor: reuse unique helpers for numeric lists * refactor: replace index dedupe filters * refactor: reuse string entry normalization * refactor: reuse string normalization in plugin helpers * refactor: reuse string normalization in extension helpers * refactor: reuse string normalization in channel parsers * refactor: reuse string normalization in memory search * refactor: reuse string normalization in provider parsers * refactor: reuse string normalization in qa helpers * refactor: reuse string normalization in infra parsers * refactor: reuse string normalization in messaging parsers * refactor: reuse string normalization in core parsers * refactor: reuse string normalization in extension parsers * refactor: reuse string normalization in remaining parsers * refactor: reuse string normalization in final parser spots * refactor: reuse string normalization in qa media helpers * refactor: reuse normalization in provider and media lists * refactor: reuse normalization for remaining set filters * refactor: reuse normalization in policy allowlists * refactor: reuse normalization in session and owner lists * refactor: centralize primitive string lists * refactor: reuse lowercase entry helpers * refactor: reuse sorted string helpers * refactor: reuse unique trimmed helpers * refactor: reuse string normalization helpers * refactor: reuse catalog string helpers * refactor: reuse remaining string helpers * refactor: simplify remaining list normalization * refactor: reuse codex auth order normalization * chore: refresh plugin sdk api baseline * fix: make shared string sorting deterministic * chore: refresh plugin sdk api baseline * fix: align host env security ordering
203 lines
6.1 KiB
TypeScript
203 lines
6.1 KiB
TypeScript
import {
|
|
attachChannelToResult,
|
|
createAttachedChannelResultAdapter,
|
|
} from "openclaw/plugin-sdk/channel-send-result";
|
|
import { resolveOutboundSendDep } from "openclaw/plugin-sdk/outbound-send-deps";
|
|
import {
|
|
resolvePayloadMediaUrls,
|
|
resolveTextChunksWithFallback,
|
|
sendPayloadMediaSequence,
|
|
} from "openclaw/plugin-sdk/reply-payload";
|
|
import {
|
|
chunkTextForOutbound,
|
|
normalizeStringEntries,
|
|
type ChannelOutboundAdapter,
|
|
} from "../runtime-api.js";
|
|
import { createMSTeamsPollStoreFs } from "./polls.js";
|
|
import { buildMSTeamsPresentationCard, MSTEAMS_PRESENTATION_CAPABILITIES } from "./presentation.js";
|
|
import { sendAdaptiveCardMSTeams, sendMessageMSTeams, sendPollMSTeams } from "./send.js";
|
|
|
|
function asObjectRecord(value: unknown): Record<string, unknown> | undefined {
|
|
return value && typeof value === "object" && !Array.isArray(value)
|
|
? (value as Record<string, unknown>)
|
|
: undefined;
|
|
}
|
|
|
|
const MSTEAMS_TEXT_CHUNK_LIMIT = 4000;
|
|
|
|
export const msteamsOutbound: ChannelOutboundAdapter = {
|
|
deliveryMode: "direct",
|
|
chunker: chunkTextForOutbound,
|
|
chunkerMode: "markdown",
|
|
textChunkLimit: MSTEAMS_TEXT_CHUNK_LIMIT,
|
|
pollMaxOptions: 12,
|
|
deliveryCapabilities: {
|
|
durableFinal: {
|
|
text: true,
|
|
media: true,
|
|
payload: true,
|
|
messageSendingHooks: true,
|
|
},
|
|
},
|
|
presentationCapabilities: MSTEAMS_PRESENTATION_CAPABILITIES,
|
|
renderPresentation: ({ payload, presentation }) => {
|
|
if (payload.mediaUrl || payload.mediaUrls?.length) {
|
|
return null;
|
|
}
|
|
const card = buildMSTeamsPresentationCard({
|
|
presentation,
|
|
text: payload.text,
|
|
});
|
|
const msteamsData = asObjectRecord(payload.channelData?.msteams) ?? {};
|
|
return {
|
|
...payload,
|
|
channelData: {
|
|
...payload.channelData,
|
|
msteams: {
|
|
...msteamsData,
|
|
presentationCard: card,
|
|
},
|
|
},
|
|
};
|
|
},
|
|
sendPayload: async ({
|
|
cfg,
|
|
to,
|
|
text,
|
|
mediaUrl,
|
|
mediaLocalRoots,
|
|
mediaReadFile,
|
|
payload,
|
|
deps,
|
|
}) => {
|
|
const msteamsData = asObjectRecord(payload.channelData?.msteams);
|
|
const presentationCard = msteamsData?.presentationCard;
|
|
if (
|
|
presentationCard &&
|
|
typeof presentationCard === "object" &&
|
|
!Array.isArray(presentationCard)
|
|
) {
|
|
const result = await sendAdaptiveCardMSTeams({
|
|
cfg,
|
|
to,
|
|
card: presentationCard as Record<string, unknown>,
|
|
});
|
|
return attachChannelToResult("msteams", result);
|
|
}
|
|
const mediaUrls = normalizeStringEntries(
|
|
resolvePayloadMediaUrls({
|
|
...payload,
|
|
mediaUrl: payload.mediaUrl ?? mediaUrl,
|
|
}),
|
|
);
|
|
if (mediaUrls.length > 0) {
|
|
type SendFn = (
|
|
to: string,
|
|
text: string,
|
|
opts?: {
|
|
mediaUrl?: string;
|
|
mediaLocalRoots?: readonly string[];
|
|
mediaReadFile?: (filePath: string) => Promise<Buffer>;
|
|
},
|
|
) => Promise<{ messageId: string; conversationId: string }>;
|
|
const send =
|
|
resolveOutboundSendDep<SendFn>(deps, "msteams") ??
|
|
((to, text, opts) =>
|
|
sendMessageMSTeams({
|
|
cfg,
|
|
to,
|
|
text,
|
|
mediaUrl: opts?.mediaUrl,
|
|
mediaLocalRoots: opts?.mediaLocalRoots,
|
|
mediaReadFile: opts?.mediaReadFile,
|
|
}));
|
|
const result = await sendPayloadMediaSequence({
|
|
text,
|
|
mediaUrls,
|
|
send: async ({ text, mediaUrl }) =>
|
|
await send(to, text, { mediaUrl, mediaLocalRoots, mediaReadFile }),
|
|
});
|
|
if (result) {
|
|
return attachChannelToResult("msteams", result);
|
|
}
|
|
}
|
|
if (text.trim()) {
|
|
type SendFn = (
|
|
to: string,
|
|
text: string,
|
|
) => Promise<{ messageId: string; conversationId: string }>;
|
|
const send =
|
|
resolveOutboundSendDep<SendFn>(deps, "msteams") ??
|
|
((to, text) => sendMessageMSTeams({ cfg, to, text }));
|
|
const chunks = resolveTextChunksWithFallback(
|
|
text,
|
|
chunkTextForOutbound(text, MSTEAMS_TEXT_CHUNK_LIMIT),
|
|
);
|
|
let result: Awaited<ReturnType<SendFn>>;
|
|
for (const chunk of chunks) {
|
|
result = await send(to, chunk);
|
|
}
|
|
return attachChannelToResult("msteams", result!);
|
|
}
|
|
throw new Error("MS Teams payload send requires text, media, or a presentation card.");
|
|
},
|
|
...createAttachedChannelResultAdapter({
|
|
channel: "msteams",
|
|
sendText: async ({ cfg, to, text, deps }) => {
|
|
type SendFn = (
|
|
to: string,
|
|
text: string,
|
|
) => Promise<{ messageId: string; conversationId: string }>;
|
|
const send =
|
|
resolveOutboundSendDep<SendFn>(deps, "msteams") ??
|
|
((to, text) => sendMessageMSTeams({ cfg, to, text }));
|
|
return await send(to, text);
|
|
},
|
|
sendMedia: async ({ cfg, to, text, mediaUrl, mediaLocalRoots, mediaReadFile, deps }) => {
|
|
type SendFn = (
|
|
to: string,
|
|
text: string,
|
|
opts?: {
|
|
mediaUrl?: string;
|
|
mediaLocalRoots?: readonly string[];
|
|
mediaReadFile?: (filePath: string) => Promise<Buffer>;
|
|
},
|
|
) => Promise<{ messageId: string; conversationId: string }>;
|
|
const send =
|
|
resolveOutboundSendDep<SendFn>(deps, "msteams") ??
|
|
((to, text, opts) =>
|
|
sendMessageMSTeams({
|
|
cfg,
|
|
to,
|
|
text,
|
|
mediaUrl: opts?.mediaUrl,
|
|
mediaLocalRoots: opts?.mediaLocalRoots,
|
|
mediaReadFile: opts?.mediaReadFile,
|
|
}));
|
|
return await send(to, text, { mediaUrl, mediaLocalRoots, mediaReadFile });
|
|
},
|
|
sendPoll: async ({ cfg, to, poll }) => {
|
|
const maxSelections = poll.maxSelections ?? 1;
|
|
const result = await sendPollMSTeams({
|
|
cfg,
|
|
to,
|
|
question: poll.question,
|
|
options: poll.options,
|
|
maxSelections,
|
|
});
|
|
const pollStore = createMSTeamsPollStoreFs();
|
|
await pollStore.createPoll({
|
|
id: result.pollId,
|
|
question: poll.question,
|
|
options: poll.options,
|
|
maxSelections,
|
|
createdAt: new Date().toISOString(),
|
|
conversationId: result.conversationId,
|
|
messageId: result.messageId,
|
|
votes: {},
|
|
});
|
|
return result;
|
|
},
|
|
}),
|
|
};
|