import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/config-runtime"; import { convertMarkdownTables } from "openclaw/plugin-sdk/text-runtime"; import { loadOutboundMediaFromUrl, type OpenClawConfig } from "../runtime-api.js"; import { createMSTeamsConversationStoreFs } from "./conversation-store-fs.js"; import {
classifyMSTeamsSendError,
formatMSTeamsSendErrorHint,
formatUnknownError,
} from "./errors.js"; import { prepareFileConsentActivityFs, requiresFileConsent } from "./file-consent-helpers.js"; import { buildTeamsFileInfoCard } from "./graph-chat.js"; import {
getDriveItemProperties,
uploadAndShareOneDrive,
uploadAndShareSharePoint,
} from "./graph-upload.js"; import { extractFilename, extractMessageId } from "./media-helpers.js"; import { buildConversationReference, sendMSTeamsMessages } from "./messenger.js"; import { setPendingUploadActivityIdFs } from "./pending-uploads-fs.js"; import { setPendingUploadActivityId } from "./pending-uploads.js"; import { buildMSTeamsPollCard } from "./polls.js"; import { resolveMSTeamsSendContext, type MSTeamsProactiveContext } from "./send-context.js";
export type SendMSTeamsMessageParams = { /** Full config (for credentials) */
cfg: OpenClawConfig; /** Conversation ID or user ID to send to */
to: string; /** Message text */
text: string; /** Optional media URL */
mediaUrl?: string; /** Optional filename override for uploaded media/files */
filename?: string;
mediaLocalRoots?: readonly string[];
mediaReadFile?: (filePath: string) => Promise<Buffer>;
};
export type SendMSTeamsMessageResult = {
messageId: string;
conversationId: string; /** If a FileConsentCard was sent instead of the file, this contains the upload ID */
pendingUploadId?: string;
};
/** Threshold for large files that require FileConsentCard flow in personal chats */ const FILE_CONSENT_THRESHOLD_BYTES = 4 * 1024 * 1024; // 4MB
/** * MSTeams-specific media size limit (100MB). * Higher than the default because OneDrive upload handles large files well.
*/ const MSTEAMS_MAX_MEDIA_BYTES = 100 * 1024 * 1024;
export type SendMSTeamsPollParams = { /** Full config (for credentials) */
cfg: OpenClawConfig; /** Conversation ID or user ID to send to */
to: string; /** Poll question */
question: string; /** Poll options */
options: string[]; /** Max selections (defaults to 1) */
maxSelections?: number;
};
export type SendMSTeamsCardParams = { /** Full config (for credentials) */
cfg: OpenClawConfig; /** Conversation ID or user ID to send to */
to: string; /** Adaptive Card JSON object */
card: Record<string, unknown>;
};
export type SendMSTeamsCardResult = {
messageId: string;
conversationId: string;
};
/** * Send a message to a Teams conversation or user. * * Uses the stored ConversationReference from previous interactions. * The bot must have received at least one message from the conversation * before proactive messaging works. * * File handling by conversation type: * - Personal (1:1) chats: small images (<4MB) use base64, large files and non-images use FileConsentCard * - Group chats / channels: files are uploaded to OneDrive and shared via link
*/
export async function sendMessageMSTeams(
params: SendMSTeamsMessageParams,
): Promise<SendMSTeamsMessageResult> { const { cfg, to, text, mediaUrl, filename, mediaLocalRoots, mediaReadFile } = params; const tableMode = resolveMarkdownTableMode({
cfg,
channel: "msteams",
}); const messageText = convertMarkdownTables(text ?? "", tableMode); const ctx = await resolveMSTeamsSendContext({ cfg, to }); const {
adapter,
appId,
conversationId,
ref,
log,
conversationType,
tokenProvider,
sharePointSiteId,
} = ctx;
// Personal chats: base64 only works for images; use FileConsentCard for large files or non-images if (
requiresFileConsent({
conversationType,
contentType: media.contentType,
bufferSize: media.buffer.length,
thresholdBytes: FILE_CONSENT_THRESHOLD_BYTES,
})
) { // Proactive CLI sends run in a different process from the gateway's // monitor that receives the fileConsent/invoke callback. Use the FS- // backed helper so the invoke handler can find the pending upload when // the user clicks "Allow". const { activity, uploadId } = await prepareFileConsentActivityFs({
media: { buffer: media.buffer, filename: fileName, contentType: media.contentType },
conversationId,
description: messageText || undefined,
});
// Store the activity ID so the accept handler can replace the consent // card in-place. Mirror it into the FS store too because the invoke // callback may be delivered to a different process than the CLI send.
setPendingUploadActivityId(uploadId, messageId);
await setPendingUploadActivityIdFs(uploadId, messageId);
// Personal chat with small image: use base64 (only works for images) if (conversationType === "personal") { // Small image in personal chat: use base64 (only works for images) const base64 = media.buffer.toString("base64"); const finalMediaUrl = `data:${media.contentType};base64,${base64}`;
if (isImage && !sharePointSiteId) { // Group chat/channel without SharePoint: send image inline (avoids OneDrive failures) const base64 = media.buffer.toString("base64"); const finalMediaUrl = `data:${media.contentType};base64,${base64}`; return sendTextWithMedia(ctx, messageText, finalMediaUrl);
}
// Group chat or channel: upload to SharePoint (if siteId configured) or OneDrive try { if (sharePointSiteId) { // Use SharePoint upload + Graph API for native file card
log.debug?.("uploading to SharePoint for native file card", {
fileName,
conversationType,
siteId: sharePointSiteId,
});
const uploaded = await uploadAndShareSharePoint({
buffer: media.buffer,
filename: fileName,
contentType: media.contentType,
tokenProvider,
siteId: sharePointSiteId, // Use the Graph-native chat ID (19:xxx format) — the Bot Framework conversationId // for personal DMs uses a different format that Graph API rejects.
chatId: ctx.graphChatId ?? conversationId,
usePerUserSharing: conversationType === "groupChat",
});
// Fallback: no SharePoint site configured, use OneDrive with markdown link
log.debug?.("uploading to OneDrive (no SharePoint site configured)", {
fileName,
conversationType,
});
export type EditMSTeamsMessageParams = { /** Full config (for credentials) */
cfg: OpenClawConfig; /** Conversation ID or user ID */
to: string; /** Activity ID of the message to edit */
activityId: string; /** New message text */
text: string;
};
export type EditMSTeamsMessageResult = {
conversationId: string;
};
export type DeleteMSTeamsMessageParams = { /** Full config (for credentials) */
cfg: OpenClawConfig; /** Conversation ID or user ID */
to: string; /** Activity ID of the message to delete */
activityId: string;
};
export type DeleteMSTeamsMessageResult = {
conversationId: string;
};
/** * Edit (update) a previously sent message in a Teams conversation. * * Uses the Bot Framework `continueConversation` → `updateActivity` flow * for proactive edits outside of the original turn context.
*/
export async function editMessageMSTeams(
params: EditMSTeamsMessageParams,
): Promise<EditMSTeamsMessageResult> { const { cfg, to, activityId, text } = params; const { adapter, appId, conversationId, ref, log } = await resolveMSTeamsSendContext({
cfg,
to,
});
/** * Delete a previously sent message in a Teams conversation. * * Uses the Bot Framework `continueConversation` → `deleteActivity` flow * for proactive deletes outside of the original turn context.
*/
export async function deleteMessageMSTeams(
params: DeleteMSTeamsMessageParams,
): Promise<DeleteMSTeamsMessageResult> { const { cfg, to, activityId } = params; const { adapter, appId, conversationId, ref, log } = await resolveMSTeamsSendContext({
cfg,
to,
});
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.