@openclaw/msteams 2026.3.13 → 2026.5.2-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api.ts +3 -0
- package/channel-config-api.ts +1 -0
- package/channel-plugin-api.ts +2 -0
- package/config-api.ts +4 -0
- package/contract-api.ts +4 -0
- package/index.ts +15 -12
- package/openclaw.plugin.json +553 -1
- package/package.json +46 -12
- package/runtime-api.ts +73 -0
- package/secret-contract-api.ts +5 -0
- package/setup-entry.ts +13 -0
- package/setup-plugin-api.ts +3 -0
- package/src/ai-entity.ts +7 -0
- package/src/approval-auth.ts +44 -0
- package/src/attachments/bot-framework.test.ts +461 -0
- package/src/attachments/bot-framework.ts +362 -0
- package/src/attachments/download.ts +63 -19
- package/src/attachments/graph.test.ts +416 -0
- package/src/attachments/graph.ts +163 -72
- package/src/attachments/html.ts +33 -1
- package/src/attachments/payload.ts +1 -1
- package/src/attachments/remote-media.test.ts +137 -0
- package/src/attachments/remote-media.ts +75 -8
- package/src/attachments/shared.test.ts +138 -1
- package/src/attachments/shared.ts +193 -26
- package/src/attachments/types.ts +10 -0
- package/src/attachments.graph.test.ts +342 -0
- package/src/attachments.helpers.test.ts +246 -0
- package/src/attachments.test-helpers.ts +17 -0
- package/src/attachments.test.ts +163 -418
- package/src/attachments.ts +5 -5
- package/src/block-streaming-config.test.ts +61 -0
- package/src/channel-api.ts +1 -0
- package/src/channel.actions.test.ts +742 -0
- package/src/channel.directory.test.ts +145 -4
- package/src/channel.runtime.ts +56 -0
- package/src/channel.setup.ts +77 -0
- package/src/channel.test.ts +128 -0
- package/src/channel.ts +1077 -395
- package/src/config-schema.ts +6 -0
- package/src/config-ui-hints.ts +12 -0
- package/src/conversation-store-fs.test.ts +4 -5
- package/src/conversation-store-fs.ts +35 -51
- package/src/conversation-store-helpers.test.ts +202 -0
- package/src/conversation-store-helpers.ts +105 -0
- package/src/conversation-store-memory.ts +27 -23
- package/src/conversation-store.shared.test.ts +225 -0
- package/src/conversation-store.ts +30 -0
- package/src/directory-live.test.ts +156 -0
- package/src/directory-live.ts +7 -4
- package/src/doctor.ts +27 -0
- package/src/errors.test.ts +64 -1
- package/src/errors.ts +50 -9
- package/src/feedback-reflection-prompt.ts +117 -0
- package/src/feedback-reflection-store.ts +114 -0
- package/src/feedback-reflection.test.ts +237 -0
- package/src/feedback-reflection.ts +283 -0
- package/src/file-consent-helpers.test.ts +83 -0
- package/src/file-consent-helpers.ts +64 -11
- package/src/file-consent-invoke.ts +150 -0
- package/src/file-consent.test.ts +363 -0
- package/src/file-consent.ts +165 -4
- package/src/graph-chat.ts +5 -3
- package/src/graph-group-management.test.ts +318 -0
- package/src/graph-group-management.ts +168 -0
- package/src/graph-members.test.ts +89 -0
- package/src/graph-members.ts +48 -0
- package/src/graph-messages.actions.test.ts +243 -0
- package/src/graph-messages.read.test.ts +391 -0
- package/src/graph-messages.search.test.ts +213 -0
- package/src/graph-messages.test-helpers.ts +50 -0
- package/src/graph-messages.ts +534 -0
- package/src/graph-teams.test.ts +215 -0
- package/src/graph-teams.ts +114 -0
- package/src/graph-thread.test.ts +246 -0
- package/src/graph-thread.ts +146 -0
- package/src/graph-upload.test.ts +161 -4
- package/src/graph-upload.ts +147 -56
- package/src/graph.test.ts +516 -0
- package/src/graph.ts +233 -21
- package/src/inbound.test.ts +156 -1
- package/src/inbound.ts +101 -1
- package/src/media-helpers.ts +1 -1
- package/src/mentions.test.ts +27 -18
- package/src/mentions.ts +2 -2
- package/src/messenger.test.ts +504 -23
- package/src/messenger.ts +133 -52
- package/src/monitor-handler/access.ts +125 -0
- package/src/monitor-handler/inbound-media.test.ts +289 -0
- package/src/monitor-handler/inbound-media.ts +57 -5
- package/src/monitor-handler/message-handler-mock-support.test-support.ts +28 -0
- package/src/monitor-handler/message-handler.authz.test.ts +588 -74
- package/src/monitor-handler/message-handler.dm-media.test.ts +54 -0
- package/src/monitor-handler/message-handler.test-support.ts +100 -0
- package/src/monitor-handler/message-handler.thread-parent.test.ts +223 -0
- package/src/monitor-handler/message-handler.thread-session.test.ts +77 -0
- package/src/monitor-handler/message-handler.ts +470 -164
- package/src/monitor-handler/reaction-handler.test.ts +267 -0
- package/src/monitor-handler/reaction-handler.ts +210 -0
- package/src/monitor-handler/thread-session.ts +17 -0
- package/src/monitor-handler.adaptive-card.test.ts +162 -0
- package/src/monitor-handler.feedback-authz.test.ts +314 -0
- package/src/monitor-handler.file-consent.test.ts +281 -79
- package/src/monitor-handler.sso.test.ts +563 -0
- package/src/monitor-handler.test-helpers.ts +180 -0
- package/src/monitor-handler.ts +459 -115
- package/src/monitor-handler.types.ts +27 -0
- package/src/monitor-types.ts +1 -0
- package/src/monitor.lifecycle.test.ts +74 -10
- package/src/monitor.test.ts +35 -1
- package/src/monitor.ts +143 -46
- package/src/oauth.flow.ts +77 -0
- package/src/oauth.shared.ts +37 -0
- package/src/oauth.test.ts +305 -0
- package/src/oauth.token.ts +158 -0
- package/src/oauth.ts +130 -0
- package/src/outbound.test.ts +10 -11
- package/src/outbound.ts +62 -44
- package/src/pending-uploads-fs.test.ts +246 -0
- package/src/pending-uploads-fs.ts +235 -0
- package/src/pending-uploads.test.ts +173 -0
- package/src/pending-uploads.ts +34 -2
- package/src/policy.test.ts +11 -5
- package/src/policy.ts +5 -5
- package/src/polls.test.ts +106 -5
- package/src/polls.ts +15 -7
- package/src/presentation.ts +68 -0
- package/src/probe.test.ts +27 -8
- package/src/probe.ts +43 -9
- package/src/reply-dispatcher.test.ts +437 -0
- package/src/reply-dispatcher.ts +259 -73
- package/src/reply-stream-controller.test.ts +235 -0
- package/src/reply-stream-controller.ts +147 -0
- package/src/resolve-allowlist.test.ts +105 -1
- package/src/resolve-allowlist.ts +112 -7
- package/src/runtime.ts +6 -3
- package/src/sdk-types.ts +43 -3
- package/src/sdk.test.ts +666 -0
- package/src/sdk.ts +867 -16
- package/src/secret-contract.ts +49 -0
- package/src/secret-input.ts +1 -1
- package/src/send-context.ts +76 -9
- package/src/send.test.ts +389 -5
- package/src/send.ts +140 -32
- package/src/sent-message-cache.ts +30 -18
- package/src/session-route.ts +40 -0
- package/src/setup-core.ts +160 -0
- package/src/setup-surface.test.ts +202 -0
- package/src/setup-surface.ts +320 -0
- package/src/sso-token-store.test.ts +72 -0
- package/src/sso-token-store.ts +166 -0
- package/src/sso.ts +300 -0
- package/src/storage.ts +1 -1
- package/src/store-fs.ts +2 -2
- package/src/streaming-message.test.ts +262 -0
- package/src/streaming-message.ts +297 -0
- package/src/test-runtime.ts +1 -1
- package/src/thread-parent-context.test.ts +224 -0
- package/src/thread-parent-context.ts +159 -0
- package/src/token.test.ts +237 -50
- package/src/token.ts +162 -7
- package/src/user-agent.test.ts +86 -0
- package/src/user-agent.ts +53 -0
- package/src/webhook-timeouts.ts +27 -0
- package/src/welcome-card.test.ts +81 -0
- package/src/welcome-card.ts +57 -0
- package/test-api.ts +1 -0
- package/tsconfig.json +16 -0
- package/CHANGELOG.md +0 -107
- package/src/file-lock.ts +0 -1
- package/src/graph-users.test.ts +0 -66
- package/src/onboarding.ts +0 -381
- package/src/polls-store.test.ts +0 -38
- package/src/revoked-context.test.ts +0 -39
- package/src/token-response.test.ts +0 -23
package/src/messenger.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
|
-
type ChunkMode,
|
|
3
2
|
isSilentReplyText,
|
|
4
|
-
loadWebMedia,
|
|
5
|
-
type MarkdownTableMode,
|
|
6
|
-
type MSTeamsReplyStyle,
|
|
7
|
-
type ReplyPayload,
|
|
8
3
|
SILENT_REPLY_TOKEN,
|
|
9
|
-
|
|
10
|
-
} from "openclaw/plugin-sdk/
|
|
4
|
+
type ChunkMode,
|
|
5
|
+
} from "openclaw/plugin-sdk/reply-chunking";
|
|
6
|
+
import {
|
|
7
|
+
resolveSendableOutboundReplyParts,
|
|
8
|
+
type ReplyPayload,
|
|
9
|
+
} from "openclaw/plugin-sdk/reply-payload";
|
|
10
|
+
import { normalizeOptionalLowercaseString, sleep } from "openclaw/plugin-sdk/text-runtime";
|
|
11
|
+
import { loadWebMedia } from "openclaw/plugin-sdk/web-media";
|
|
12
|
+
import type { MarkdownTableMode, MSTeamsReplyStyle, OpenClawConfig } from "../runtime-api.js";
|
|
11
13
|
import type { MSTeamsAccessTokenProvider } from "./attachments/types.js";
|
|
12
14
|
import type { StoredConversationReference } from "./conversation-store.js";
|
|
13
15
|
import { classifyMSTeamsSendError } from "./errors.js";
|
|
@@ -20,6 +22,7 @@ import {
|
|
|
20
22
|
} from "./graph-upload.js";
|
|
21
23
|
import { extractFilename, extractMessageId, getMimeType, isLocalPath } from "./media-helpers.js";
|
|
22
24
|
import { parseMentions } from "./mentions.js";
|
|
25
|
+
import { setPendingUploadActivityId } from "./pending-uploads.js";
|
|
23
26
|
import { withRevokedProxyFallback } from "./revoked-context.js";
|
|
24
27
|
import { getMSTeamsRuntime } from "./runtime.js";
|
|
25
28
|
|
|
@@ -37,9 +40,11 @@ const FILE_CONSENT_THRESHOLD_BYTES = 4 * 1024 * 1024;
|
|
|
37
40
|
|
|
38
41
|
type SendContext = {
|
|
39
42
|
sendActivity: (textOrActivity: string | object) => Promise<unknown>;
|
|
43
|
+
updateActivity: (activity: object) => Promise<{ id?: string } | void>;
|
|
44
|
+
deleteActivity: (activityId: string) => Promise<void>;
|
|
40
45
|
};
|
|
41
46
|
|
|
42
|
-
|
|
47
|
+
type MSTeamsConversationReference = {
|
|
43
48
|
activityId?: string;
|
|
44
49
|
user?: { id?: string; name?: string; aadObjectId?: string };
|
|
45
50
|
agent?: { id?: string; name?: string; aadObjectId?: string } | null;
|
|
@@ -47,6 +52,18 @@ export type MSTeamsConversationReference = {
|
|
|
47
52
|
channelId: string;
|
|
48
53
|
serviceUrl?: string;
|
|
49
54
|
locale?: string;
|
|
55
|
+
/**
|
|
56
|
+
* Top-level tenant ID echoed onto the Bot Framework connector request. Included
|
|
57
|
+
* alongside `conversation.tenantId` so the connector can route proactive sends
|
|
58
|
+
* to the correct Azure AD tenant. Missing it causes HTTP 403 on proactive
|
|
59
|
+
* (bot-initiated) messages.
|
|
60
|
+
*/
|
|
61
|
+
tenantId?: string;
|
|
62
|
+
/**
|
|
63
|
+
* Azure AD object ID of the target user, forwarded on proactive sends so
|
|
64
|
+
* Bot Framework can resolve the personal DM recipient on the connector side.
|
|
65
|
+
*/
|
|
66
|
+
aadObjectId?: string;
|
|
50
67
|
};
|
|
51
68
|
|
|
52
69
|
export type MSTeamsAdapter = {
|
|
@@ -60,9 +77,11 @@ export type MSTeamsAdapter = {
|
|
|
60
77
|
res: unknown,
|
|
61
78
|
logic: (context: unknown) => Promise<void>,
|
|
62
79
|
) => Promise<void>;
|
|
80
|
+
updateActivity: (context: unknown, activity: object) => Promise<void>;
|
|
81
|
+
deleteActivity: (context: unknown, reference: { activityId?: string }) => Promise<void>;
|
|
63
82
|
};
|
|
64
83
|
|
|
65
|
-
|
|
84
|
+
type MSTeamsReplyRenderOptions = {
|
|
66
85
|
textChunkLimit: number;
|
|
67
86
|
chunkText?: boolean;
|
|
68
87
|
mediaMode?: "split" | "inline";
|
|
@@ -79,13 +98,13 @@ export type MSTeamsRenderedMessage = {
|
|
|
79
98
|
mediaUrl?: string;
|
|
80
99
|
};
|
|
81
100
|
|
|
82
|
-
|
|
101
|
+
type MSTeamsSendRetryOptions = {
|
|
83
102
|
maxAttempts?: number;
|
|
84
103
|
baseDelayMs?: number;
|
|
85
104
|
maxDelayMs?: number;
|
|
86
105
|
};
|
|
87
106
|
|
|
88
|
-
|
|
107
|
+
type MSTeamsSendRetryEvent = {
|
|
89
108
|
messageIndex: number;
|
|
90
109
|
messageCount: number;
|
|
91
110
|
nextAttempt: number;
|
|
@@ -113,18 +132,26 @@ export function buildConversationReference(
|
|
|
113
132
|
if (!user?.id) {
|
|
114
133
|
throw new Error("Invalid stored reference: missing user.id");
|
|
115
134
|
}
|
|
135
|
+
// Bot Framework proactive sends require `tenantId` on the outbound activity
|
|
136
|
+
// so the connector routes to the correct Azure AD tenant; otherwise it rejects
|
|
137
|
+
// with HTTP 403. Prefer the explicit top-level `ref.tenantId` (captured from
|
|
138
|
+
// `channelData.tenant.id` inbound) and fall back to `conversation.tenantId`.
|
|
139
|
+
const tenantId = ref.tenantId ?? ref.conversation?.tenantId;
|
|
140
|
+
const aadObjectId = ref.aadObjectId ?? user.aadObjectId;
|
|
116
141
|
return {
|
|
117
142
|
activityId: ref.activityId,
|
|
118
|
-
user,
|
|
143
|
+
user: aadObjectId ? { ...user, aadObjectId } : user,
|
|
119
144
|
agent,
|
|
120
145
|
conversation: {
|
|
121
146
|
id: normalizeConversationId(conversationId),
|
|
122
147
|
conversationType: ref.conversation?.conversationType,
|
|
123
|
-
tenantId
|
|
148
|
+
tenantId,
|
|
124
149
|
},
|
|
125
150
|
channelId: ref.channelId ?? "msteams",
|
|
126
151
|
serviceUrl: ref.serviceUrl,
|
|
127
152
|
locale: ref.locale,
|
|
153
|
+
...(tenantId ? { tenantId } : {}),
|
|
154
|
+
...(aadObjectId ? { aadObjectId } : {}),
|
|
128
155
|
};
|
|
129
156
|
}
|
|
130
157
|
|
|
@@ -211,46 +238,44 @@ export function renderReplyPayloadsToMessages(
|
|
|
211
238
|
const tableMode =
|
|
212
239
|
options.tableMode ??
|
|
213
240
|
getMSTeamsRuntime().channel.text.resolveMarkdownTableMode({
|
|
214
|
-
cfg: getMSTeamsRuntime().config.
|
|
241
|
+
cfg: getMSTeamsRuntime().config.current() as OpenClawConfig,
|
|
215
242
|
channel: "msteams",
|
|
216
243
|
});
|
|
217
244
|
|
|
218
245
|
for (const payload of replies) {
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
tableMode,
|
|
223
|
-
);
|
|
246
|
+
const reply = resolveSendableOutboundReplyParts(payload, {
|
|
247
|
+
text: getMSTeamsRuntime().channel.text.convertMarkdownTables(payload.text ?? "", tableMode),
|
|
248
|
+
});
|
|
224
249
|
|
|
225
|
-
if (!
|
|
250
|
+
if (!reply.hasContent) {
|
|
226
251
|
continue;
|
|
227
252
|
}
|
|
228
253
|
|
|
229
|
-
if (
|
|
230
|
-
pushTextMessages(out, text, { chunkText, chunkLimit, chunkMode });
|
|
254
|
+
if (!reply.hasMedia) {
|
|
255
|
+
pushTextMessages(out, reply.text, { chunkText, chunkLimit, chunkMode });
|
|
231
256
|
continue;
|
|
232
257
|
}
|
|
233
258
|
|
|
234
259
|
if (mediaMode === "inline") {
|
|
235
260
|
// For inline mode, combine text with first media as attachment
|
|
236
|
-
const firstMedia =
|
|
261
|
+
const firstMedia = reply.mediaUrls[0];
|
|
237
262
|
if (firstMedia) {
|
|
238
|
-
out.push({ text: text || undefined, mediaUrl: firstMedia });
|
|
263
|
+
out.push({ text: reply.text || undefined, mediaUrl: firstMedia });
|
|
239
264
|
// Additional media URLs as separate messages
|
|
240
|
-
for (let i = 1; i <
|
|
241
|
-
if (
|
|
242
|
-
out.push({ mediaUrl:
|
|
265
|
+
for (let i = 1; i < reply.mediaUrls.length; i++) {
|
|
266
|
+
if (reply.mediaUrls[i]) {
|
|
267
|
+
out.push({ mediaUrl: reply.mediaUrls[i] });
|
|
243
268
|
}
|
|
244
269
|
}
|
|
245
270
|
} else {
|
|
246
|
-
pushTextMessages(out, text, { chunkText, chunkLimit, chunkMode });
|
|
271
|
+
pushTextMessages(out, reply.text, { chunkText, chunkLimit, chunkMode });
|
|
247
272
|
}
|
|
248
273
|
continue;
|
|
249
274
|
}
|
|
250
275
|
|
|
251
276
|
// mediaMode === "split"
|
|
252
|
-
pushTextMessages(out, text, { chunkText, chunkLimit, chunkMode });
|
|
253
|
-
for (const mediaUrl of
|
|
277
|
+
pushTextMessages(out, reply.text, { chunkText, chunkLimit, chunkMode });
|
|
278
|
+
for (const mediaUrl of reply.mediaUrls) {
|
|
254
279
|
if (!mediaUrl) {
|
|
255
280
|
continue;
|
|
256
281
|
}
|
|
@@ -261,24 +286,32 @@ export function renderReplyPayloadsToMessages(
|
|
|
261
286
|
return out;
|
|
262
287
|
}
|
|
263
288
|
|
|
264
|
-
|
|
289
|
+
import { AI_GENERATED_ENTITY } from "./ai-entity.js";
|
|
290
|
+
|
|
291
|
+
export async function buildActivity(
|
|
265
292
|
msg: MSTeamsRenderedMessage,
|
|
266
293
|
conversationRef: StoredConversationReference,
|
|
267
294
|
tokenProvider?: MSTeamsAccessTokenProvider,
|
|
268
295
|
sharePointSiteId?: string,
|
|
269
296
|
mediaMaxBytes?: number,
|
|
297
|
+
options?: { feedbackLoopEnabled?: boolean },
|
|
270
298
|
): Promise<Record<string, unknown>> {
|
|
271
299
|
const activity: Record<string, unknown> = { type: "message" };
|
|
272
300
|
|
|
301
|
+
// Mark as AI-generated so Teams renders the "AI generated" badge.
|
|
302
|
+
activity.channelData = {
|
|
303
|
+
feedbackLoopEnabled: options?.feedbackLoopEnabled ?? false,
|
|
304
|
+
};
|
|
305
|
+
|
|
273
306
|
if (msg.text) {
|
|
274
307
|
// Parse mentions from text (format: @[Name](id))
|
|
275
308
|
const { text: formattedText, entities } = parseMentions(msg.text);
|
|
276
309
|
activity.text = formattedText;
|
|
277
310
|
|
|
278
|
-
//
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
311
|
+
// Start with mention entities (if any) + AI-generated entity
|
|
312
|
+
activity.entities = [...(entities.length > 0 ? entities : []), AI_GENERATED_ENTITY];
|
|
313
|
+
} else {
|
|
314
|
+
activity.entities = [AI_GENERATED_ENTITY];
|
|
282
315
|
}
|
|
283
316
|
|
|
284
317
|
if (msg.mediaUrl) {
|
|
@@ -294,7 +327,9 @@ async function buildActivity(
|
|
|
294
327
|
|
|
295
328
|
// Determine conversation type and file type
|
|
296
329
|
// Teams only accepts base64 data URLs for images
|
|
297
|
-
const conversationType =
|
|
330
|
+
const conversationType = normalizeOptionalLowercaseString(
|
|
331
|
+
conversationRef.conversation?.conversationType,
|
|
332
|
+
);
|
|
298
333
|
const isPersonal = conversationType === "personal";
|
|
299
334
|
const isImage = media.kind === "image";
|
|
300
335
|
|
|
@@ -308,20 +343,25 @@ async function buildActivity(
|
|
|
308
343
|
) {
|
|
309
344
|
// Large file or non-image in personal chat: use FileConsentCard flow
|
|
310
345
|
const conversationId = conversationRef.conversation?.id ?? "unknown";
|
|
311
|
-
const { activity: consentActivity } = prepareFileConsentActivity({
|
|
346
|
+
const { activity: consentActivity, uploadId } = prepareFileConsentActivity({
|
|
312
347
|
media: { buffer: media.buffer, filename: fileName, contentType },
|
|
313
348
|
conversationId,
|
|
314
349
|
description: msg.text || undefined,
|
|
315
350
|
});
|
|
316
351
|
|
|
352
|
+
// Tag the activity so the caller can store the activity ID after sending
|
|
353
|
+
consentActivity._pendingUploadId = uploadId;
|
|
354
|
+
|
|
317
355
|
// Return the consent activity (caller sends it)
|
|
318
356
|
return consentActivity;
|
|
319
357
|
}
|
|
320
358
|
|
|
321
359
|
if (!isPersonal && !isImage && tokenProvider && sharePointSiteId) {
|
|
322
360
|
// Non-image in group chat/channel with SharePoint site configured:
|
|
323
|
-
// Upload to SharePoint and use native file card attachment
|
|
324
|
-
|
|
361
|
+
// Upload to SharePoint and use native file card attachment.
|
|
362
|
+
// Use the cached Graph-native chat ID when available — Bot Framework conversation IDs
|
|
363
|
+
// for personal DMs use a format (e.g. `a:1xxx`) that Graph API rejects.
|
|
364
|
+
const chatId = conversationRef.graphChatId ?? conversationRef.conversation?.id;
|
|
325
365
|
|
|
326
366
|
// Upload to SharePoint
|
|
327
367
|
const uploaded = await uploadAndShareSharePoint({
|
|
@@ -396,6 +436,8 @@ export async function sendMSTeamsMessages(params: {
|
|
|
396
436
|
sharePointSiteId?: string;
|
|
397
437
|
/** Max media size in bytes. Default: 100MB. */
|
|
398
438
|
mediaMaxBytes?: number;
|
|
439
|
+
/** Enable the Teams feedback loop (thumbs up/down) on sent messages. */
|
|
440
|
+
feedbackLoopEnabled?: boolean;
|
|
399
441
|
}): Promise<string[]> {
|
|
400
442
|
const messages = params.messages.filter(
|
|
401
443
|
(m) => (m.text && m.text.trim().length > 0) || m.mediaUrl,
|
|
@@ -447,20 +489,40 @@ export async function sendMSTeamsMessages(params: {
|
|
|
447
489
|
message: MSTeamsRenderedMessage,
|
|
448
490
|
messageIndex: number,
|
|
449
491
|
): Promise<string> => {
|
|
492
|
+
let pendingUploadId: string | undefined;
|
|
450
493
|
const response = await sendWithRetry(
|
|
451
|
-
async () =>
|
|
452
|
-
await
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
494
|
+
async () => {
|
|
495
|
+
const activity = await buildActivity(
|
|
496
|
+
message,
|
|
497
|
+
params.conversationRef,
|
|
498
|
+
params.tokenProvider,
|
|
499
|
+
params.sharePointSiteId,
|
|
500
|
+
params.mediaMaxBytes,
|
|
501
|
+
{ feedbackLoopEnabled: params.feedbackLoopEnabled },
|
|
502
|
+
);
|
|
503
|
+
|
|
504
|
+
// Extract and strip the internal-only pending upload tag before sending.
|
|
505
|
+
pendingUploadId =
|
|
506
|
+
typeof activity._pendingUploadId === "string" ? activity._pendingUploadId : undefined;
|
|
507
|
+
if (pendingUploadId) {
|
|
508
|
+
delete activity._pendingUploadId;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
return await ctx.sendActivity(activity);
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
messageIndex,
|
|
515
|
+
messageCount: messages.length,
|
|
516
|
+
},
|
|
462
517
|
);
|
|
463
|
-
|
|
518
|
+
const messageId = extractMessageId(response) ?? "unknown";
|
|
519
|
+
|
|
520
|
+
// Store the activity ID so the accept handler can replace the consent card in-place
|
|
521
|
+
if (pendingUploadId && messageId !== "unknown") {
|
|
522
|
+
setPendingUploadActivityId(pendingUploadId, messageId);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return messageId;
|
|
464
526
|
};
|
|
465
527
|
|
|
466
528
|
const sendMessageBatchInContext = async (
|
|
@@ -478,11 +540,21 @@ export async function sendMSTeamsMessages(params: {
|
|
|
478
540
|
const sendProactively = async (
|
|
479
541
|
batch: MSTeamsRenderedMessage[],
|
|
480
542
|
startIndex: number,
|
|
543
|
+
threadActivityId?: string,
|
|
481
544
|
): Promise<string[]> => {
|
|
482
545
|
const baseRef = buildConversationReference(params.conversationRef);
|
|
546
|
+
const isChannel = params.conversationRef.conversation?.conversationType === "channel";
|
|
547
|
+
// For Teams channels, reconstruct the threaded conversation ID so the
|
|
548
|
+
// proactive message lands in the correct thread instead of creating a
|
|
549
|
+
// new top-level post in the channel.
|
|
550
|
+
const conversationId =
|
|
551
|
+
isChannel && threadActivityId
|
|
552
|
+
? `${baseRef.conversation.id};messageid=${threadActivityId}`
|
|
553
|
+
: baseRef.conversation.id;
|
|
483
554
|
const proactiveRef: MSTeamsConversationReference = {
|
|
484
555
|
...baseRef,
|
|
485
556
|
activityId: undefined,
|
|
557
|
+
conversation: { ...baseRef.conversation, id: conversationId },
|
|
486
558
|
};
|
|
487
559
|
|
|
488
560
|
const messageIds: string[] = [];
|
|
@@ -492,6 +564,11 @@ export async function sendMSTeamsMessages(params: {
|
|
|
492
564
|
return messageIds;
|
|
493
565
|
};
|
|
494
566
|
|
|
567
|
+
// Resolve the thread root message ID for channel thread routing.
|
|
568
|
+
// `threadId` is the canonical thread root (set on inbound for channel threads);
|
|
569
|
+
// fall back to `activityId` for backward compatibility with older stored refs.
|
|
570
|
+
const resolvedThreadId = params.conversationRef.threadId ?? params.conversationRef.activityId;
|
|
571
|
+
|
|
495
572
|
if (params.replyStyle === "thread") {
|
|
496
573
|
const ctx = params.context;
|
|
497
574
|
if (!ctx) {
|
|
@@ -505,9 +582,13 @@ export async function sendMSTeamsMessages(params: {
|
|
|
505
582
|
fellBack: false,
|
|
506
583
|
}),
|
|
507
584
|
onRevoked: async () => {
|
|
585
|
+
// When the live turn context is revoked (e.g. debounced messages),
|
|
586
|
+
// reconstruct the threaded conversation ID so the proactive
|
|
587
|
+
// fallback delivers the reply into the correct channel thread.
|
|
508
588
|
const remaining = messages.slice(idx);
|
|
509
589
|
return {
|
|
510
|
-
ids:
|
|
590
|
+
ids:
|
|
591
|
+
remaining.length > 0 ? await sendProactively(remaining, idx, resolvedThreadId) : [],
|
|
511
592
|
fellBack: true,
|
|
512
593
|
};
|
|
513
594
|
},
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/text-runtime";
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_ACCOUNT_ID,
|
|
4
|
+
createChannelPairingController,
|
|
5
|
+
evaluateSenderGroupAccessForPolicy,
|
|
6
|
+
isDangerousNameMatchingEnabled,
|
|
7
|
+
readStoreAllowFromForDmPolicy,
|
|
8
|
+
resolveDefaultGroupPolicy,
|
|
9
|
+
resolveDmGroupAccessWithLists,
|
|
10
|
+
resolveEffectiveAllowFromLists,
|
|
11
|
+
resolveSenderScopedGroupPolicy,
|
|
12
|
+
type OpenClawConfig,
|
|
13
|
+
} from "../../runtime-api.js";
|
|
14
|
+
import { normalizeMSTeamsConversationId } from "../inbound.js";
|
|
15
|
+
import { resolveMSTeamsAllowlistMatch, resolveMSTeamsRouteConfig } from "../policy.js";
|
|
16
|
+
import { getMSTeamsRuntime } from "../runtime.js";
|
|
17
|
+
import type { MSTeamsTurnContext } from "../sdk-types.js";
|
|
18
|
+
|
|
19
|
+
export async function resolveMSTeamsSenderAccess(params: {
|
|
20
|
+
cfg: OpenClawConfig;
|
|
21
|
+
activity: MSTeamsTurnContext["activity"];
|
|
22
|
+
}) {
|
|
23
|
+
const activity = params.activity;
|
|
24
|
+
const msteamsCfg = params.cfg.channels?.msteams;
|
|
25
|
+
const conversationId = normalizeMSTeamsConversationId(activity.conversation?.id ?? "unknown");
|
|
26
|
+
const convType = normalizeOptionalLowercaseString(activity.conversation?.conversationType);
|
|
27
|
+
const isDirectMessage = convType === "personal" || (!convType && !activity.conversation?.isGroup);
|
|
28
|
+
const senderId = activity.from?.aadObjectId ?? activity.from?.id ?? "unknown";
|
|
29
|
+
const senderName = activity.from?.name ?? activity.from?.id ?? senderId;
|
|
30
|
+
|
|
31
|
+
const core = getMSTeamsRuntime();
|
|
32
|
+
const pairing = createChannelPairingController({
|
|
33
|
+
core,
|
|
34
|
+
channel: "msteams",
|
|
35
|
+
accountId: DEFAULT_ACCOUNT_ID,
|
|
36
|
+
});
|
|
37
|
+
const dmPolicy = msteamsCfg?.dmPolicy ?? "pairing";
|
|
38
|
+
const storedAllowFrom = await readStoreAllowFromForDmPolicy({
|
|
39
|
+
provider: "msteams",
|
|
40
|
+
accountId: pairing.accountId,
|
|
41
|
+
dmPolicy,
|
|
42
|
+
readStore: pairing.readStoreForDmPolicy,
|
|
43
|
+
});
|
|
44
|
+
const configuredDmAllowFrom = msteamsCfg?.allowFrom ?? [];
|
|
45
|
+
const groupAllowFrom = msteamsCfg?.groupAllowFrom;
|
|
46
|
+
const resolvedAllowFromLists = resolveEffectiveAllowFromLists({
|
|
47
|
+
allowFrom: configuredDmAllowFrom,
|
|
48
|
+
groupAllowFrom,
|
|
49
|
+
storeAllowFrom: storedAllowFrom,
|
|
50
|
+
dmPolicy,
|
|
51
|
+
});
|
|
52
|
+
const defaultGroupPolicy = resolveDefaultGroupPolicy(params.cfg);
|
|
53
|
+
const groupPolicy =
|
|
54
|
+
!isDirectMessage && msteamsCfg
|
|
55
|
+
? (msteamsCfg.groupPolicy ?? defaultGroupPolicy ?? "allowlist")
|
|
56
|
+
: "disabled";
|
|
57
|
+
const effectiveGroupAllowFrom = resolvedAllowFromLists.effectiveGroupAllowFrom;
|
|
58
|
+
const allowNameMatching = isDangerousNameMatchingEnabled(msteamsCfg);
|
|
59
|
+
const channelGate = resolveMSTeamsRouteConfig({
|
|
60
|
+
cfg: msteamsCfg,
|
|
61
|
+
teamId: activity.channelData?.team?.id,
|
|
62
|
+
teamName: activity.channelData?.team?.name,
|
|
63
|
+
conversationId,
|
|
64
|
+
channelName: activity.channelData?.channel?.name,
|
|
65
|
+
allowNameMatching,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// When a route-level (team/channel) allowlist is configured but the sender allowlist is
|
|
69
|
+
// empty, resolveSenderScopedGroupPolicy would otherwise downgrade the policy to "open",
|
|
70
|
+
// allowing any sender. To close this bypass (GHSA-g7cr-9h7q-4qxq), treat an empty sender
|
|
71
|
+
// allowlist as deny-all whenever the route allowlist is active.
|
|
72
|
+
const senderGroupPolicy =
|
|
73
|
+
channelGate.allowlistConfigured && effectiveGroupAllowFrom.length === 0
|
|
74
|
+
? groupPolicy
|
|
75
|
+
: resolveSenderScopedGroupPolicy({
|
|
76
|
+
groupPolicy,
|
|
77
|
+
groupAllowFrom: effectiveGroupAllowFrom,
|
|
78
|
+
});
|
|
79
|
+
const access = resolveDmGroupAccessWithLists({
|
|
80
|
+
isGroup: !isDirectMessage,
|
|
81
|
+
dmPolicy,
|
|
82
|
+
groupPolicy: senderGroupPolicy,
|
|
83
|
+
allowFrom: configuredDmAllowFrom,
|
|
84
|
+
groupAllowFrom,
|
|
85
|
+
storeAllowFrom: storedAllowFrom,
|
|
86
|
+
groupAllowFromFallbackToAllowFrom: false,
|
|
87
|
+
isSenderAllowed: (allowFrom) =>
|
|
88
|
+
resolveMSTeamsAllowlistMatch({
|
|
89
|
+
allowFrom,
|
|
90
|
+
senderId,
|
|
91
|
+
senderName,
|
|
92
|
+
allowNameMatching,
|
|
93
|
+
}).allowed,
|
|
94
|
+
});
|
|
95
|
+
const senderGroupAccess = evaluateSenderGroupAccessForPolicy({
|
|
96
|
+
groupPolicy,
|
|
97
|
+
groupAllowFrom: effectiveGroupAllowFrom,
|
|
98
|
+
senderId,
|
|
99
|
+
isSenderAllowed: (_senderId, allowFrom) =>
|
|
100
|
+
resolveMSTeamsAllowlistMatch({
|
|
101
|
+
allowFrom,
|
|
102
|
+
senderId,
|
|
103
|
+
senderName,
|
|
104
|
+
allowNameMatching,
|
|
105
|
+
}).allowed,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
msteamsCfg,
|
|
110
|
+
pairing,
|
|
111
|
+
isDirectMessage,
|
|
112
|
+
conversationId,
|
|
113
|
+
senderId,
|
|
114
|
+
senderName,
|
|
115
|
+
dmPolicy,
|
|
116
|
+
channelGate,
|
|
117
|
+
access,
|
|
118
|
+
senderGroupAccess,
|
|
119
|
+
configuredDmAllowFrom,
|
|
120
|
+
effectiveDmAllowFrom: access.effectiveAllowFrom,
|
|
121
|
+
effectiveGroupAllowFrom,
|
|
122
|
+
allowNameMatching,
|
|
123
|
+
groupPolicy,
|
|
124
|
+
};
|
|
125
|
+
}
|