import {
isSilentReplyText,
SILENT_REPLY_TOKEN,
type ChunkMode,
} from "openclaw/plugin-sdk/reply-chunking"; import {
resolveSendableOutboundReplyParts,
type ReplyPayload,
} from "openclaw/plugin-sdk/reply-payload"; import { normalizeOptionalLowercaseString, sleep } from "openclaw/plugin-sdk/text-runtime"; import { loadWebMedia } from "openclaw/plugin-sdk/web-media"; import type { MarkdownTableMode, MSTeamsReplyStyle } from "../runtime-api.js"; import type { MSTeamsAccessTokenProvider } from "./attachments/types.js"; import type { StoredConversationReference } from "./conversation-store.js"; import { classifyMSTeamsSendError } from "./errors.js"; import { prepareFileConsentActivity, requiresFileConsent } from "./file-consent-helpers.js"; import { buildTeamsFileInfoCard } from "./graph-chat.js"; import {
getDriveItemProperties,
uploadAndShareOneDrive,
uploadAndShareSharePoint,
} from "./graph-upload.js"; import { extractFilename, extractMessageId, getMimeType, isLocalPath } from "./media-helpers.js"; import { parseMentions } from "./mentions.js"; import { setPendingUploadActivityId } from "./pending-uploads.js"; import { withRevokedProxyFallback } from "./revoked-context.js"; import { getMSTeamsRuntime } from "./runtime.js";
/** * 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;
/** * Threshold for large files that require FileConsentCard flow in personal chats. * Files >= 4MB use consent flow; smaller images can use inline base64.
*/ const FILE_CONSENT_THRESHOLD_BYTES = 4 * 1024 * 1024;
/** * A rendered message that preserves media vs text distinction. * When mediaUrl is present, it will be sent as a Bot Framework attachment.
*/
export type MSTeamsRenderedMessage = {
text?: string;
mediaUrl?: string;
};
if (mediaMode === "inline") { // For inline mode, combine text with first media as attachment const firstMedia = reply.mediaUrls[0]; if (firstMedia) {
out.push({ text: reply.text || undefined, mediaUrl: firstMedia }); // Additional media URLs as separate messages for (let i = 1; i < reply.mediaUrls.length; i++) { if (reply.mediaUrls[i]) {
out.push({ mediaUrl: reply.mediaUrls[i] });
}
}
} else {
pushTextMessages(out, reply.text, { chunkText, chunkLimit, chunkMode });
} continue;
}
// mediaMode === "split"
pushTextMessages(out, reply.text, { chunkText, chunkLimit, chunkMode }); for (const mediaUrl of reply.mediaUrls) { if (!mediaUrl) { continue;
}
out.push({ mediaUrl });
}
}
return out;
}
import { AI_GENERATED_ENTITY } from "./ai-entity.js";
// Mark as AI-generated so Teams renders the "AI generated" badge.
activity.channelData = {
feedbackLoopEnabled: options?.feedbackLoopEnabled ?? false,
};
if (msg.text) { // Parse mentions from text (format: @[Name](id)) const { text: formattedText, entities } = parseMentions(msg.text);
activity.text = formattedText;
if (msg.mediaUrl) {
let contentUrl = msg.mediaUrl;
let contentType = await getMimeType(msg.mediaUrl);
let fileName = await extractFilename(msg.mediaUrl);
// Determine conversation type and file type // Teams only accepts base64 data URLs for images const conversationType = normalizeOptionalLowercaseString(
conversationRef.conversation?.conversationType,
); const isPersonal = conversationType === "personal"; const isImage = media.kind === "image";
if (
requiresFileConsent({
conversationType,
contentType,
bufferSize: media.buffer.length,
thresholdBytes: FILE_CONSENT_THRESHOLD_BYTES,
})
) { // Large file or non-image in personal chat: use FileConsentCard flow const conversationId = conversationRef.conversation?.id ?? "unknown"; const { activity: consentActivity, uploadId } = prepareFileConsentActivity({
media: { buffer: media.buffer, filename: fileName, contentType },
conversationId,
description: msg.text || undefined,
});
// Tag the activity so the caller can store the activity ID after sending
consentActivity._pendingUploadId = uploadId;
if (!isPersonal && !isImage && tokenProvider && sharePointSiteId) { // Non-image in group chat/channel with SharePoint site configured: // Upload to SharePoint and use native file card attachment. // Use the cached Graph-native chat ID when available — Bot Framework conversation IDs // for personal DMs use a format (e.g. `a:1xxx`) that Graph API rejects. const chatId = conversationRef.graphChatId ?? conversationRef.conversation?.id;
// Store the activity ID so the accept handler can replace the consent card in-place if (pendingUploadId && messageId !== "unknown") {
setPendingUploadActivityId(pendingUploadId, messageId);
}
// Resolve the thread root message ID for channel thread routing. // `threadId` is the canonical thread root (set on inbound for channel threads); // fall back to `activityId` for backward compatibility with older stored refs. const resolvedThreadId = params.conversationRef.threadId ?? params.conversationRef.activityId;
if (params.replyStyle === "thread") { const ctx = params.context; if (!ctx) { thrownew Error("Missing context for replyStyle=thread");
} const messageIds: string[] = []; for (const [idx, message] of messages.entries()) { const result = await withRevokedProxyFallback({
run: async () => ({
ids: [await sendMessageInContext(ctx, message, idx)],
fellBack: false,
}),
onRevoked: async () => { // When the live turn context is revoked (e.g. debounced messages), // reconstruct the threaded conversation ID so the proactive // fallback delivers the reply into the correct channel thread. const remaining = messages.slice(idx); return {
ids:
remaining.length > 0 ? await sendProactively(remaining, idx, resolvedThreadId) : [],
fellBack: true,
};
},
});
messageIds.push(...result.ids); if (result.fellBack) { return messageIds;
}
} return messageIds;
}
return await sendProactively(messages, 0);
}
Messung V0.5 in Prozent
¤ Dauer der Verarbeitung: 0.16 Sekunden
(vorverarbeitet am 2026-05-26)
¤
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.