@openclaw/msteams 2026.7.2-beta.5 → 2026.7.2-beta.7

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.
@@ -1,12 +1,11 @@
1
- const require_delegated_state = require("./delegated-state-DOSCHaAB.cjs");
1
+ const require_polls = require("./polls-BWKNpBSg.cjs");
2
2
  const require_runtime = require("./runtime-gZTAuFux.cjs");
3
3
  require("./runtime-api.cjs");
4
- const require_resolve_allowlist = require("./resolve-allowlist-Dr_1sOJh.cjs");
5
- const require_channel = require("./channel-DtZFlwhL.cjs");
6
- const require_setup_surface = require("./setup-surface-xVejKCJV.cjs");
7
- const require_polls = require("./polls-CWExq2xZ.cjs");
8
- const require_sso_token_store = require("./sso-token-store-OSg229re.cjs");
9
- const require_messenger = require("./messenger-BJ0KM2sD.cjs");
4
+ const require_resolve_allowlist = require("./resolve-allowlist-CEj9sABO.cjs");
5
+ const require_channel = require("./channel-C7QxRjBW.cjs");
6
+ const require_setup_surface = require("./setup-surface-ByWjaXEn.cjs");
7
+ const require_sso_token_store = require("./sso-token-store-TKYk_Ms7.cjs");
8
+ const require_messenger = require("./messenger-DTiaLTDO.cjs");
10
9
  let openclaw_plugin_sdk_channel_outbound = require("openclaw/plugin-sdk/channel-outbound");
11
10
  let openclaw_plugin_sdk_string_coerce_runtime = require("openclaw/plugin-sdk/string-coerce-runtime");
12
11
  let openclaw_plugin_sdk_allow_from = require("openclaw/plugin-sdk/allow-from");
@@ -17,12 +16,15 @@ let openclaw_plugin_sdk_number_runtime = require("openclaw/plugin-sdk/number-run
17
16
  let openclaw_plugin_sdk_text_utility_runtime = require("openclaw/plugin-sdk/text-utility-runtime");
18
17
  let openclaw_plugin_sdk_routing = require("openclaw/plugin-sdk/routing");
19
18
  let node_crypto = require("node:crypto");
20
- node_crypto = require_delegated_state.__toESM(node_crypto, 1);
19
+ node_crypto = require_polls.__toESM(node_crypto, 1);
20
+ let openclaw_plugin_sdk_error_runtime = require("openclaw/plugin-sdk/error-runtime");
21
21
  let openclaw_plugin_sdk_channel_inbound = require("openclaw/plugin-sdk/channel-inbound");
22
22
  let openclaw_plugin_sdk_channel_ingress_runtime = require("openclaw/plugin-sdk/channel-ingress-runtime");
23
23
  let openclaw_plugin_sdk_context_visibility_runtime = require("openclaw/plugin-sdk/context-visibility-runtime");
24
24
  let openclaw_plugin_sdk_reply_history = require("openclaw/plugin-sdk/reply-history");
25
25
  let openclaw_plugin_sdk_agent_runtime = require("openclaw/plugin-sdk/agent-runtime");
26
+ let openclaw_plugin_sdk_extension_shared = require("openclaw/plugin-sdk/extension-shared");
27
+ let openclaw_plugin_sdk_plugin_runtime = require("openclaw/plugin-sdk/plugin-runtime");
26
28
  let openclaw_plugin_sdk_dedupe_runtime = require("openclaw/plugin-sdk/dedupe-runtime");
27
29
  let openclaw_plugin_sdk_collection_runtime = require("openclaw/plugin-sdk/collection-runtime");
28
30
  let openclaw_plugin_sdk_channel_pairing = require("openclaw/plugin-sdk/channel-pairing");
@@ -168,20 +170,50 @@ const msteamsIngressIdentity = {
168
170
  aliases: [{
169
171
  key: "sender-name",
170
172
  kind: "plugin:msteams-sender-name",
171
- normalizeEntry: normalizeIngressValue,
172
- normalizeSubject: normalizeIngressValue,
173
+ normalizeEntry: normalizeSenderNameIngressValue,
174
+ normalizeSubject: normalizeSenderNameIngressValue,
173
175
  dangerous: true
176
+ }, {
177
+ key: "conversation-id",
178
+ kind: "plugin:msteams-conversation-id",
179
+ normalizeEntry: normalizeAllowlistConversationId,
180
+ normalizeSubject: normalizeAllowlistConversationId
174
181
  }],
175
182
  isWildcardEntry: (entry) => normalizeIngressValue(entry) === "*",
176
- resolveEntryId: ({ entryIndex, fieldKey }) => `msteams-entry-${entryIndex + 1}:${fieldKey === "sender-name" ? "name" : "id"}`
183
+ resolveEntryId: ({ entryIndex, fieldKey }) => `msteams-entry-${entryIndex + 1}:${fieldKey === "sender-name" ? "name" : fieldKey === "conversation-id" ? "conversation-id" : "id"}`
177
184
  };
178
185
  function normalizeIngressValue(value) {
179
186
  return (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeOptionalLowercaseString)(value) ?? null;
180
187
  }
188
+ function normalizeSenderNameIngressValue(value) {
189
+ const normalized = normalizeIngressValue(value);
190
+ if (!normalized) return null;
191
+ return require_resolve_allowlist.looksLikeMSTeamsConversationId(require_resolve_allowlist.normalizeMSTeamsConversationId(normalized)) ? null : normalized;
192
+ }
193
+ function normalizeAllowlistConversationId(value) {
194
+ const trimmed = value?.trim();
195
+ return trimmed ? require_resolve_allowlist.normalizeMSTeamsConversationId(trimmed) : null;
196
+ }
197
+ function formatMSTeamsSenderReason(params) {
198
+ switch (params.reasonCode) {
199
+ case "dm_policy_open": return "dmPolicy=open";
200
+ case "dm_policy_disabled": return "dmPolicy=disabled";
201
+ case "dm_policy_pairing_required": return "dmPolicy=pairing (not allowlisted)";
202
+ case "dm_policy_allowlisted": return `dmPolicy=${params.dmPolicy ?? "allowlist"} (allowlisted)`;
203
+ case "dm_policy_not_allowlisted": return `dmPolicy=${params.dmPolicy ?? "allowlist"} (not allowlisted)`;
204
+ case "group_policy_disabled": return "groupPolicy=disabled";
205
+ case "group_policy_empty_allowlist":
206
+ case "route_sender_empty": return "groupPolicy=allowlist (empty allowlist)";
207
+ case "group_policy_not_allowlisted": return "groupPolicy=allowlist (not allowlisted)";
208
+ case "group_policy_open": return "groupPolicy=open";
209
+ case "group_policy_allowed": return `groupPolicy=${params.groupPolicy ?? "allowlist"}`;
210
+ default: return params.reasonCode;
211
+ }
212
+ }
181
213
  async function resolveMSTeamsSenderAccess(params) {
182
214
  const activity = params.activity;
183
215
  const msteamsCfg = params.cfg.channels?.msteams;
184
- const conversationId = require_channel.normalizeMSTeamsConversationId(activity.conversation?.id ?? "unknown");
216
+ const conversationId = require_resolve_allowlist.normalizeMSTeamsConversationId(activity.conversation?.id ?? "unknown");
185
217
  const convType = (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeOptionalLowercaseString)(activity.conversation?.conversationType);
186
218
  const isDirectMessage = convType === "personal" || !convType && !activity.conversation?.isGroup;
187
219
  const senderId = activity.from?.aadObjectId ?? activity.from?.id ?? "unknown";
@@ -214,7 +246,10 @@ async function resolveMSTeamsSenderAccess(params) {
214
246
  readStoreAllowFrom: pairing.readAllowFromStore,
215
247
  subject: {
216
248
  stableId: senderId,
217
- aliases: { "sender-name": senderName }
249
+ aliases: {
250
+ "sender-name": senderName,
251
+ ...!isDirectMessage ? { "conversation-id": conversationId } : {}
252
+ }
218
253
  },
219
254
  conversation: {
220
255
  kind: isDirectMessage ? "direct" : convType === "channel" ? "channel" : "group",
@@ -258,6 +293,136 @@ async function resolveMSTeamsSenderAccess(params) {
258
293
  groupPolicy
259
294
  };
260
295
  }
296
+ async function admitMSTeamsMessage(params) {
297
+ const core = require_runtime.getMSTeamsRuntime();
298
+ const allowTextCommands = core.channel.commands.shouldHandleTextCommands({
299
+ cfg: params.cfg,
300
+ surface: "msteams"
301
+ });
302
+ const isControlCommand = allowTextCommands && core.channel.commands.isControlCommandMessage(params.text, params.cfg);
303
+ const access = await resolveMSTeamsSenderAccess({
304
+ cfg: params.cfg,
305
+ activity: params.activity,
306
+ hasControlCommand: isControlCommand
307
+ });
308
+ const { dmPolicy, senderId, senderName, pairing, isDirectMessage, channelGate, senderAccess, commandAccess, allowNameMatching, groupPolicy, msteamsCfg } = access;
309
+ const effectiveDmAllowFrom = senderAccess.effectiveAllowFrom;
310
+ const effectiveGroupAllowFrom = senderAccess.effectiveGroupAllowFrom;
311
+ if (isDirectMessage && msteamsCfg && senderAccess.decision !== "allow") {
312
+ if (senderAccess.reasonCode === "dm_policy_disabled") {
313
+ params.log.info("dropping dm (dms disabled)", {
314
+ sender: senderId,
315
+ label: senderName
316
+ });
317
+ params.log.debug?.("dropping dm (dms disabled)");
318
+ return null;
319
+ }
320
+ const allowMatch = require_channel.resolveMSTeamsAllowlistMatch({
321
+ allowFrom: effectiveDmAllowFrom,
322
+ senderId,
323
+ senderName,
324
+ allowNameMatching
325
+ });
326
+ if (senderAccess.decision === "pairing") {
327
+ params.conversationStore.upsert(params.conversationId, params.conversationRef).catch((err) => {
328
+ params.log.debug?.("failed to save conversation reference", { error: require_setup_surface.formatUnknownError(err) });
329
+ });
330
+ if (await pairing.upsertPairingRequest({
331
+ id: senderId,
332
+ meta: { name: senderName }
333
+ })) params.log.info("msteams pairing request created", {
334
+ sender: senderId,
335
+ label: senderName
336
+ });
337
+ }
338
+ params.log.debug?.("dropping dm (not allowlisted)", {
339
+ sender: senderId,
340
+ label: senderName,
341
+ allowlistMatch: (0, openclaw_plugin_sdk_allow_from.formatAllowlistMatchMeta)(allowMatch)
342
+ });
343
+ params.log.info("dropping dm (not allowlisted)", {
344
+ sender: senderId,
345
+ label: senderName,
346
+ dmPolicy,
347
+ reason: formatMSTeamsSenderReason({
348
+ reasonCode: senderAccess.reasonCode,
349
+ dmPolicy,
350
+ groupPolicy
351
+ }),
352
+ allowlistMatch: (0, openclaw_plugin_sdk_allow_from.formatAllowlistMatchMeta)(allowMatch)
353
+ });
354
+ return null;
355
+ }
356
+ if (!isDirectMessage && msteamsCfg) {
357
+ if (channelGate.allowlistConfigured && !channelGate.allowed) {
358
+ params.log.info("dropping group message (not in team/channel allowlist)", {
359
+ conversationId: params.conversationId,
360
+ teamKey: channelGate.teamKey ?? "none",
361
+ channelKey: channelGate.channelKey ?? "none",
362
+ channelMatchKey: channelGate.channelMatchKey ?? "none",
363
+ channelMatchSource: channelGate.channelMatchSource ?? "none"
364
+ });
365
+ params.log.debug?.("dropping group message (not in team/channel allowlist)", {
366
+ conversationId: params.conversationId,
367
+ teamKey: channelGate.teamKey ?? "none",
368
+ channelKey: channelGate.channelKey ?? "none",
369
+ channelMatchKey: channelGate.channelMatchKey ?? "none",
370
+ channelMatchSource: channelGate.channelMatchSource ?? "none"
371
+ });
372
+ return null;
373
+ }
374
+ if (!senderAccess.allowed && senderAccess.reasonCode === "group_policy_disabled") {
375
+ params.log.info("dropping group message (groupPolicy: disabled)", { conversationId: params.conversationId });
376
+ params.log.debug?.("dropping group message (groupPolicy: disabled)", { conversationId: params.conversationId });
377
+ return null;
378
+ }
379
+ if (!senderAccess.allowed && (senderAccess.reasonCode === "group_policy_empty_allowlist" || senderAccess.reasonCode === "route_sender_empty")) {
380
+ params.log.info("dropping group message (groupPolicy: allowlist, no allowlist)", { conversationId: params.conversationId });
381
+ params.log.debug?.("dropping group message (groupPolicy: allowlist, no allowlist)", { conversationId: params.conversationId });
382
+ return null;
383
+ }
384
+ if (!senderAccess.allowed && senderAccess.reasonCode === "group_policy_not_allowlisted") {
385
+ const allowMatch = require_channel.resolveMSTeamsAllowlistMatch({
386
+ allowFrom: effectiveGroupAllowFrom,
387
+ senderId,
388
+ senderName,
389
+ allowNameMatching
390
+ });
391
+ params.log.debug?.("dropping group message (not in groupAllowFrom)", {
392
+ sender: senderId,
393
+ label: senderName,
394
+ allowlistMatch: (0, openclaw_plugin_sdk_allow_from.formatAllowlistMatchMeta)(allowMatch)
395
+ });
396
+ params.log.info("dropping group message (not in groupAllowFrom)", {
397
+ sender: senderId,
398
+ label: senderName,
399
+ allowlistMatch: (0, openclaw_plugin_sdk_allow_from.formatAllowlistMatchMeta)(allowMatch)
400
+ });
401
+ return null;
402
+ }
403
+ }
404
+ if (commandAccess.shouldBlockControlCommand) {
405
+ (0, openclaw_plugin_sdk_channel_inbound.logInboundDrop)({
406
+ log: params.logVerboseMessage,
407
+ channel: "msteams",
408
+ reason: "control command (unauthorized)",
409
+ target: senderId
410
+ });
411
+ return null;
412
+ }
413
+ params.conversationStore.upsert(params.conversationId, params.conversationRef).catch((err) => {
414
+ params.log.debug?.("failed to save conversation reference", { error: require_setup_surface.formatUnknownError(err) });
415
+ });
416
+ return {
417
+ ...access,
418
+ allowTextCommands,
419
+ isControlCommand,
420
+ commandAuthorized: commandAccess.requested ? commandAccess.authorized : void 0,
421
+ effectiveDmAllowFrom,
422
+ effectiveGroupAllowFrom,
423
+ isChannel: params.isChannel
424
+ };
425
+ }
261
426
  //#endregion
262
427
  //#region extensions/msteams/src/attachments/bot-framework.ts
263
428
  /**
@@ -933,10 +1098,13 @@ async function fetchGraphCollection(params) {
933
1098
  });
934
1099
  try {
935
1100
  const status = response.status;
936
- if (!response.ok) return {
937
- status,
938
- items: []
939
- };
1101
+ if (!response.ok) {
1102
+ await response.body?.cancel().catch(() => void 0);
1103
+ return {
1104
+ status,
1105
+ items: []
1106
+ };
1107
+ }
940
1108
  try {
941
1109
  return {
942
1110
  status,
@@ -1237,147 +1405,293 @@ async function downloadMSTeamsGraphMedia(params) {
1237
1405
  };
1238
1406
  }
1239
1407
  //#endregion
1240
- //#region extensions/msteams/src/thread-parent-context.ts
1241
- const PARENT_CACHE_TTL_MS = 300 * 1e3;
1242
- const PARENT_CACHE_MAX = 100;
1243
- const parentCache = /* @__PURE__ */ new Map();
1244
- const INJECTED_MAX = 200;
1245
- const injectedParents = /* @__PURE__ */ new Map();
1246
- function touchLru(map, key, value, max) {
1247
- if (map.has(key)) map.delete(key);
1248
- else if (map.size >= max) {
1249
- const firstKey = map.keys().next().value;
1250
- if (firstKey !== void 0) map.delete(firstKey);
1251
- }
1252
- map.set(key, value);
1408
+ //#region extensions/msteams/src/monitor-handler/inbound-media.ts
1409
+ function shouldAttemptMSTeamsGraphMediaFallback(params) {
1410
+ const conversationType = params.conversationType.trim().toLowerCase();
1411
+ return params.graphMediaFallback === true && (conversationType === "channel" || conversationType === "groupchat") && (params.htmlSummary?.htmlAttachments ?? 0) > 0;
1253
1412
  }
1254
- function buildParentCacheKey(groupId, channelId, parentId) {
1255
- return `${groupId}\u0000${channelId}\u0000${parentId}`;
1413
+ function resolveMSTeamsInboundMediaBody(params) {
1414
+ const unavailableCount = params.materializedMedia.filter((media) => !media.path).length + Math.max(0, params.nativeMedia.length - params.materializedMedia.length);
1415
+ if (unavailableCount === 0) return params.body;
1416
+ return (0, openclaw_plugin_sdk_channel_inbound.formatInboundMediaUnavailableText)({
1417
+ body: params.body,
1418
+ notice: `[msteams ${unavailableCount > 1 ? `${unavailableCount} attachments` : "attachment"} unavailable]`
1419
+ });
1256
1420
  }
1257
- function resolveParentCacheExpiresAt(nowRaw) {
1258
- const nowMs = (0, openclaw_plugin_sdk_number_runtime.asDateTimestampMs)(nowRaw);
1259
- return nowMs === void 0 ? void 0 : (0, openclaw_plugin_sdk_number_runtime.resolveExpiresAtMsFromDurationMs)(PARENT_CACHE_TTL_MS, { nowMs });
1421
+ function hasDefinitiveContentType(media) {
1422
+ const contentType = media.contentType?.split(";", 1)[0]?.trim().toLowerCase();
1423
+ return Boolean(contentType && contentType !== "application/octet-stream" && contentType !== "binary/octet-stream");
1260
1424
  }
1261
- /**
1262
- * Fetch a channel parent message with an LRU+TTL cache.
1263
- *
1264
- * Uses the injected `fetchParent` (defaults to `fetchChannelMessage`) so
1265
- * tests can swap in a stub without mocking the Graph transport.
1266
- */
1267
- async function fetchParentMessageCached(token, groupId, channelId, parentId, fetchParent = require_messenger.fetchChannelMessage) {
1268
- const key = buildParentCacheKey(groupId, channelId, parentId);
1269
- const now = (0, openclaw_plugin_sdk_number_runtime.asDateTimestampMs)(Date.now());
1270
- const cached = parentCache.get(key);
1271
- const cachedExpiresAt = cached ? (0, openclaw_plugin_sdk_number_runtime.asDateTimestampMs)(cached.expiresAt) : void 0;
1272
- if (cached && now !== void 0 && cachedExpiresAt !== void 0 && cachedExpiresAt > now) {
1273
- parentCache.delete(key);
1274
- parentCache.set(key, cached);
1275
- return cached.message;
1425
+ function mergeMSTeamsMediaFacts(nativeMedia, materializedMedia, options = {}) {
1426
+ const merged = [...nativeMedia];
1427
+ const nativeSlotCount = nativeMedia.length;
1428
+ const nativeIndexBySourceId = /* @__PURE__ */ new Map();
1429
+ nativeMedia.forEach((media, index) => {
1430
+ if (media.sourceId && !nativeIndexBySourceId.has(media.sourceId)) nativeIndexBySourceId.set(media.sourceId, index);
1431
+ });
1432
+ for (const [index, materialized] of materializedMedia.entries()) {
1433
+ const sourceIndex = materialized.sourceId ? nativeIndexBySourceId.get(materialized.sourceId) : void 0;
1434
+ const positionalIndex = options.positionallyAligned === false || index >= nativeMedia.length ? void 0 : index;
1435
+ const mayUseFallbackOrder = options.positionallyAligned === false;
1436
+ const isEligibleUnresolved = (media) => !media.path && !media.sourceId;
1437
+ const sameKindUnresolvedIndexes = mayUseFallbackOrder ? merged.slice(0, nativeSlotCount).flatMap((media, mediaIndex) => isEligibleUnresolved(media) && media.kind === materialized.kind ? [mediaIndex] : []) : [];
1438
+ const unresolvedIndexes = sameKindUnresolvedIndexes.length === 0 && mayUseFallbackOrder ? merged.slice(0, nativeSlotCount).flatMap((media, mediaIndex) => isEligibleUnresolved(media) ? [mediaIndex] : []) : [];
1439
+ const fallbackIndex = sameKindUnresolvedIndexes.length > 0 ? sameKindUnresolvedIndexes[0] : unresolvedIndexes.length === 1 ? unresolvedIndexes[0] : void 0;
1440
+ const targetIndex = sourceIndex ?? positionalIndex ?? fallbackIndex;
1441
+ if (targetIndex === void 0) {
1442
+ if (materialized.sourceId) nativeIndexBySourceId.set(materialized.sourceId, merged.length);
1443
+ merged.push(materialized);
1444
+ } else {
1445
+ if (materialized.sourceId) nativeIndexBySourceId.set(materialized.sourceId, targetIndex);
1446
+ const current = merged[targetIndex];
1447
+ if (materialized.path) merged[targetIndex] = materialized;
1448
+ else if (!current?.path) merged[targetIndex] = {
1449
+ ...current,
1450
+ ...materialized,
1451
+ kind: hasDefinitiveContentType(materialized) || !current?.kind ? materialized.kind : current.kind
1452
+ };
1453
+ }
1276
1454
  }
1277
- if (cached) parentCache.delete(key);
1278
- const message = await fetchParent(token, groupId, channelId, parentId);
1279
- const expiresAt = resolveParentCacheExpiresAt(Date.now());
1280
- if (expiresAt !== void 0) touchLru(parentCache, key, {
1281
- message,
1282
- expiresAt
1283
- }, PARENT_CACHE_MAX);
1284
- return message;
1285
- }
1286
- const PARENT_TEXT_MAX_CHARS = 400;
1287
- /**
1288
- * Extract a compact summary (sender + plain-text body) from a Graph parent
1289
- * message. Returns undefined when the parent cannot be summarized (missing
1290
- * or blank body).
1291
- */
1292
- function summarizeParentMessage(message) {
1293
- if (!message) return;
1294
- const sender = message.from?.user?.displayName ?? message.from?.application?.displayName ?? "unknown";
1295
- const contentType = message.body?.contentType ?? "text";
1296
- const raw = message.body?.content ?? "";
1297
- const text = contentType === "html" ? require_messenger.stripHtmlFromTeamsMessage(raw) : raw.replace(/\s+/g, " ").trim();
1298
- if (!text) return;
1299
- return {
1300
- sender,
1301
- text: text.length > PARENT_TEXT_MAX_CHARS ? `${(0, openclaw_plugin_sdk_text_utility_runtime.truncateUtf16Safe)(text, PARENT_TEXT_MAX_CHARS - 1)}…` : text
1302
- };
1303
- }
1304
- /**
1305
- * Build the single-line `Replying to @sender: body` system event text.
1306
- * Callers should pass this text to `enqueueSystemEvent` together with a
1307
- * stable contextKey derived from the parent id.
1308
- */
1309
- function formatParentContextEvent(summary) {
1310
- return `Replying to @${summary.sender}: ${summary.text}`;
1311
- }
1312
- /**
1313
- * Decide whether a parent context event should be enqueued for the current
1314
- * session. Returns `false` when we already injected the same parent for this
1315
- * session recently (prevents re-prepending identical context on every reply
1316
- * in the thread).
1317
- */
1318
- function shouldInjectParentContext(sessionKey, parentId) {
1319
- const key = sessionKey;
1320
- return injectedParents.get(key) !== parentId;
1321
- }
1322
- /**
1323
- * Record that `parentId` was just injected for `sessionKey` so subsequent
1324
- * replies with the same parent can short-circuit via `shouldInjectParentContext`.
1325
- */
1326
- function markParentContextInjected(sessionKey, parentId) {
1327
- touchLru(injectedParents, sessionKey, parentId, INJECTED_MAX);
1455
+ return merged;
1328
1456
  }
1329
- //#endregion
1330
- //#region extensions/msteams/src/reply-stream-controller.ts
1331
- function isStreamCancelledError(err) {
1332
- return err instanceof Error && err.name === "StreamCancelledError";
1457
+ function hasMaterializedMedia(media) {
1458
+ return media.some((entry) => Boolean(entry.path));
1333
1459
  }
1334
- /**
1335
- * Bridges openclaw's reply pipeline callbacks to the SDK's `ctx.stream`.
1336
- * Streaming is enabled for personal (DM) conversations only; group/channel
1337
- * messages fall through to block delivery.
1338
- *
1339
- * Streaming modes (resolved from `cfg.channels.msteams.streaming.preview`):
1340
- * - "partial" (default): per-token streaming via `stream.emit(text)`. Each
1341
- * chunk goes onto the live preview card in Teams.
1342
- * - "progress": no per-token streaming; the preview card carries an
1343
- * informative status that updates as tools run (e.g. "Looking up the
1344
- * schema..." → "Generating SQL..."). When tool-progress streaming is also
1345
- * enabled, raw tool names appear as bullets above the label.
1346
- * - "block": disable native streaming entirely; the reply lands as a regular
1347
- * block message. We bypass the controller in that case.
1348
- */
1349
- function createTeamsReplyStreamController(params) {
1350
- const isPersonal = (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeOptionalLowercaseString)(params.conversationType) === "personal";
1351
- const streamMode = (0, openclaw_plugin_sdk_channel_outbound.resolveChannelPreviewStreamMode)(params.msteamsConfig, "partial");
1352
- const shouldUseNativeStream = isPersonal && (streamMode === "partial" || streamMode === "progress");
1353
- const shouldStreamPreviewToolProgress = streamMode === "progress" && (0, openclaw_plugin_sdk_channel_outbound.resolveChannelStreamingPreviewToolProgress)(params.msteamsConfig);
1354
- const stream = shouldUseNativeStream ? params.context.stream : void 0;
1355
- let tokensEmitted = false;
1356
- let streamFinalizationPending = false;
1357
- let canceledLocally = false;
1358
- let streamFailed = false;
1359
- let lastInformativeText = "";
1360
- let progressLines = [];
1361
- let latestPlan;
1362
- let latestPlanExplanation;
1363
- let pendingFinalPayload;
1364
- let emittedText = "";
1365
- const wasCanceled = () => canceledLocally || Boolean(stream?.canceled);
1366
- const fallbackPayloadForSuppressedFinal = (payload) => {
1367
- return Boolean(payload.mediaUrl || payload.mediaUrls?.length) ? {
1368
- ...payload,
1369
- mediaUrl: void 0,
1370
- mediaUrls: void 0
1371
- } : payload;
1372
- };
1373
- /**
1374
- * Render the current informative status line into the streaming card. Pulls
1375
- * the rotating "Thinking..." label from msteams config (or the plugin-sdk
1376
- * default) and prepends collected tool-progress lines when configured.
1377
- */
1378
- const renderInformativeUpdate = () => {
1379
- if (!stream || wasCanceled() || streamFinalizationPending) return;
1380
- const informativeText = (0, openclaw_plugin_sdk_channel_outbound.formatChannelProgressDraftText)({
1460
+ async function resolveMSTeamsInboundMedia(params) {
1461
+ const { attachments, htmlSummary, maxBytes, tokenProvider, allowHosts, conversationType, conversationId, conversationMessageId, teamAadGroupId, serviceUrl, activity, log, preserveFilenames } = params;
1462
+ let mediaList = await downloadMSTeamsAttachments({
1463
+ attachments,
1464
+ maxBytes,
1465
+ tokenProvider,
1466
+ allowHosts,
1467
+ authAllowHosts: params.authAllowHosts,
1468
+ preserveFilenames,
1469
+ deadline: params.deadline,
1470
+ logger: log
1471
+ });
1472
+ if (!hasMaterializedMedia(mediaList)) {
1473
+ const attachmentIds = extractMSTeamsHtmlAttachmentIds(attachments);
1474
+ const hasHtmlFileAttachment = attachmentIds.length > 0;
1475
+ const hasChannelOrGroupHtml = shouldAttemptMSTeamsGraphMediaFallback({
1476
+ conversationType,
1477
+ htmlSummary,
1478
+ graphMediaFallback: params.graphMediaFallback
1479
+ });
1480
+ const shouldFetchGraphMessage = hasHtmlFileAttachment || hasChannelOrGroupHtml;
1481
+ const isBotFrameworkPersonalChat = isBotFrameworkPersonalChatId(conversationId);
1482
+ if (hasHtmlFileAttachment && isBotFrameworkPersonalChat) if (!serviceUrl) log.debug?.("bot framework attachment skipped (missing serviceUrl)", {
1483
+ conversationType,
1484
+ conversationId
1485
+ });
1486
+ else {
1487
+ const bfMedia = await downloadMSTeamsBotFrameworkAttachments({
1488
+ serviceUrl,
1489
+ attachmentIds,
1490
+ tokenProvider,
1491
+ maxBytes,
1492
+ allowHosts,
1493
+ authAllowHosts: params.authAllowHosts,
1494
+ preserveFilenames,
1495
+ deadline: params.deadline
1496
+ });
1497
+ if (bfMedia.media.length > 0) mediaList = mergeMSTeamsMediaFacts(mediaList, bfMedia.media, { positionallyAligned: false });
1498
+ if (!hasMaterializedMedia(bfMedia.media)) log.debug?.("bot framework attachments fetch empty", {
1499
+ conversationType,
1500
+ attachmentCount: bfMedia.attachmentCount ?? attachmentIds.length
1501
+ });
1502
+ }
1503
+ if (shouldFetchGraphMessage && !hasMaterializedMedia(mediaList) && !isBotFrameworkPersonalChat) {
1504
+ const graphTeamAadGroupId = conversationType.trim().toLowerCase() === "channel" && !teamAadGroupId ? await params.resolveTeamAadGroupId?.() : teamAadGroupId;
1505
+ const messageUrl = buildMSTeamsGraphMessageUrl({
1506
+ conversationType,
1507
+ conversationId,
1508
+ messageId: activity.id ?? void 0,
1509
+ threadRootMessageId: conversationMessageId ?? activity.replyToId,
1510
+ teamAadGroupId: graphTeamAadGroupId,
1511
+ channelId: activity.channelData?.channel?.id
1512
+ });
1513
+ if (!messageUrl) log.debug?.("graph message url unavailable", {
1514
+ conversationType,
1515
+ hasChannelData: Boolean(activity.channelData),
1516
+ messageId: activity.id ?? void 0,
1517
+ replyToId: activity.replyToId ?? void 0
1518
+ });
1519
+ else {
1520
+ const graphMedia = await downloadMSTeamsGraphMedia({
1521
+ messageUrl,
1522
+ tokenProvider,
1523
+ maxBytes,
1524
+ allowHosts,
1525
+ authAllowHosts: params.authAllowHosts,
1526
+ preserveFilenames,
1527
+ deadline: params.deadline,
1528
+ logger: log
1529
+ });
1530
+ if (graphMedia.media.length > 0) mediaList = mergeMSTeamsMediaFacts(mediaList, graphMedia.media, { positionallyAligned: false });
1531
+ if (!hasMaterializedMedia(mediaList)) log.debug?.("graph media fetch empty", {
1532
+ messageUrl,
1533
+ hostedStatus: graphMedia.hostedStatus,
1534
+ attachmentStatus: graphMedia.attachmentStatus,
1535
+ hostedCount: graphMedia.hostedCount,
1536
+ attachmentCount: graphMedia.attachmentCount,
1537
+ tokenError: graphMedia.tokenError,
1538
+ attachmentIdCount: attachmentIds.length
1539
+ });
1540
+ }
1541
+ }
1542
+ }
1543
+ const materializedCount = mediaList.filter((media) => Boolean(media.path)).length;
1544
+ if (materializedCount > 0) log.debug?.("downloaded attachments", { count: materializedCount });
1545
+ else if (htmlSummary?.imgTags) log.debug?.("inline images detected but none downloaded", {
1546
+ imgTags: htmlSummary.imgTags,
1547
+ srcHosts: htmlSummary.srcHosts,
1548
+ dataImages: htmlSummary.dataImages,
1549
+ cidImages: htmlSummary.cidImages
1550
+ });
1551
+ return mediaList;
1552
+ }
1553
+ //#endregion
1554
+ //#region extensions/msteams/src/monitor-handler/inbound-content.ts
1555
+ async function prepareMSTeamsInboundContent(params) {
1556
+ const activity = params.entry.context.activity;
1557
+ const mayRecoverGraphMedia = Boolean(params.htmlSummary?.attachmentIds.length) || shouldAttemptMSTeamsGraphMediaFallback({
1558
+ conversationType: params.conversationType,
1559
+ htmlSummary: params.htmlSummary,
1560
+ graphMediaFallback: params.graphMediaFallback
1561
+ });
1562
+ if (!params.rawBody && params.advertisedMedia.length === 0 && !mayRecoverGraphMedia) {
1563
+ params.log.debug?.("skipping empty message after stripping mentions");
1564
+ return null;
1565
+ }
1566
+ let mediaList = [];
1567
+ try {
1568
+ mediaList = await require_resolve_allowlist.withMSTeamsRequestDeadline({
1569
+ deadline: params.deadline,
1570
+ label: "MS Teams inbound media",
1571
+ work: () => resolveMSTeamsInboundMedia({
1572
+ attachments: params.entry.attachments,
1573
+ htmlSummary: params.htmlSummary,
1574
+ maxBytes: params.mediaMaxBytes,
1575
+ tokenProvider: params.tokenProvider,
1576
+ allowHosts: params.mediaAllowHosts,
1577
+ authAllowHosts: params.mediaAuthAllowHosts,
1578
+ graphMediaFallback: params.graphMediaFallback,
1579
+ conversationType: params.conversationType,
1580
+ conversationId: params.conversationId,
1581
+ conversationMessageId: params.conversationMessageId,
1582
+ teamAadGroupId: params.teamAadGroupId,
1583
+ resolveTeamAadGroupId: params.resolveTeamAadGroupId,
1584
+ serviceUrl: activity.serviceUrl,
1585
+ activity: {
1586
+ id: activity.id,
1587
+ replyToId: activity.replyToId,
1588
+ channelData: activity.channelData
1589
+ },
1590
+ log: params.log,
1591
+ deadline: params.deadline,
1592
+ preserveFilenames: false
1593
+ })
1594
+ });
1595
+ } catch (err) {
1596
+ params.log.debug?.("failed to resolve inbound Teams media", { error: require_setup_surface.formatUnknownError(err) });
1597
+ }
1598
+ const inboundMedia = mergeMSTeamsMediaFacts(params.advertisedMedia, mediaList);
1599
+ const nativeMediaForComparison = [...params.advertisedMedia, ...mediaList.slice(params.advertisedMedia.length).map((media) => ({
1600
+ contentType: media.contentType,
1601
+ kind: media.kind
1602
+ }))];
1603
+ const agentBody = resolveMSTeamsInboundMediaBody({
1604
+ body: params.rawBody,
1605
+ nativeMedia: nativeMediaForComparison,
1606
+ materializedMedia: inboundMedia
1607
+ });
1608
+ if (!agentBody && inboundMedia.length === 0) {
1609
+ params.log.debug?.("skipping empty message after Graph media recovery");
1610
+ return null;
1611
+ }
1612
+ return {
1613
+ agentBody,
1614
+ inboundMedia
1615
+ };
1616
+ }
1617
+ //#endregion
1618
+ //#region extensions/msteams/src/reply-stream-controller.ts
1619
+ function isStreamCancelledError(err) {
1620
+ return err instanceof Error && err.name === "StreamCancelledError";
1621
+ }
1622
+ /**
1623
+ * Bridges openclaw's reply pipeline callbacks to the SDK's `ctx.stream`.
1624
+ * Streaming is enabled for personal (DM) conversations only; group/channel
1625
+ * messages fall through to block delivery.
1626
+ *
1627
+ * Streaming modes (resolved from `cfg.channels.msteams.streaming.preview`):
1628
+ * - "partial" (default): per-token streaming via `stream.emit(text)`. Each
1629
+ * chunk goes onto the live preview card in Teams.
1630
+ * - "progress": no per-token streaming; the preview card carries an
1631
+ * informative status that updates as tools run (e.g. "Looking up the
1632
+ * schema..." → "Generating SQL..."). When tool-progress streaming is also
1633
+ * enabled, raw tool names appear as bullets above the label.
1634
+ * - "block": disable native streaming entirely; the reply lands as a regular
1635
+ * block message. We bypass the controller in that case.
1636
+ */
1637
+ function createTeamsReplyStreamController(params) {
1638
+ const isPersonal = (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeOptionalLowercaseString)(params.conversationType) === "personal";
1639
+ const streamMode = (0, openclaw_plugin_sdk_channel_outbound.resolveChannelPreviewStreamMode)(params.msteamsConfig, "partial");
1640
+ const shouldUseNativeStream = params.allowProviderPreview && isPersonal && (streamMode === "partial" || streamMode === "progress");
1641
+ const shouldStreamPreviewToolProgress = streamMode === "progress" && (0, openclaw_plugin_sdk_channel_outbound.resolveChannelStreamingPreviewToolProgress)(params.msteamsConfig, true, streamMode);
1642
+ const stream = shouldUseNativeStream ? params.context.stream : void 0;
1643
+ let tokensEmitted = false;
1644
+ let nativeDispatchStarted = false;
1645
+ let nativeDeliveryClaimed = false;
1646
+ let streamFinalizationPending = false;
1647
+ let canceledLocally = false;
1648
+ let streamFailed = false;
1649
+ let lastInformativeText = "";
1650
+ let progressLines = [];
1651
+ let latestPlan;
1652
+ let latestPlanExplanation;
1653
+ let pendingFinalPayload;
1654
+ let emittedText = "";
1655
+ let acknowledgedText = "";
1656
+ let acknowledgedStreamId;
1657
+ const streamEvents = stream?.events;
1658
+ let streamChunkSubscription;
1659
+ if (typeof streamEvents?.on === "function" && typeof streamEvents.off === "function") streamChunkSubscription = streamEvents.on("chunk", (activity) => {
1660
+ if (activity.type !== "typing" || activity.channelData?.streamType !== "streaming" || !activity.id || !activity.text || acknowledgedStreamId !== void 0 && activity.id !== acknowledgedStreamId || !activity.text.startsWith(acknowledgedText) || !emittedText.startsWith(activity.text)) return;
1661
+ acknowledgedStreamId = activity.id;
1662
+ acknowledgedText = activity.text;
1663
+ });
1664
+ const wasCanceled = () => canceledLocally || Boolean(stream?.canceled);
1665
+ const releaseStreamChunkSubscription = () => {
1666
+ if (streamChunkSubscription === void 0) return;
1667
+ streamEvents?.off(streamChunkSubscription);
1668
+ streamChunkSubscription = void 0;
1669
+ };
1670
+ const fallbackPayloadAfterAcknowledgedText = (payload) => {
1671
+ if (!acknowledgedText || typeof payload.text !== "string" || !payload.text.startsWith(acknowledgedText)) return payload;
1672
+ const remainingText = payload.text.slice(acknowledgedText.length);
1673
+ const hasMedia = Boolean(payload.mediaUrl || payload.mediaUrls?.length);
1674
+ if (!remainingText && !hasMedia) return;
1675
+ return {
1676
+ ...payload,
1677
+ text: remainingText || void 0
1678
+ };
1679
+ };
1680
+ const fallbackPayloadForSuppressedFinal = (payload) => {
1681
+ return Boolean(payload.mediaUrl || payload.mediaUrls?.length) ? {
1682
+ ...payload,
1683
+ mediaUrl: void 0,
1684
+ mediaUrls: void 0
1685
+ } : payload;
1686
+ };
1687
+ /**
1688
+ * Render the current informative status line into the streaming card. Pulls
1689
+ * the rotating "Thinking..." label from msteams config (or the plugin-sdk
1690
+ * default) and prepends collected tool-progress lines when configured.
1691
+ */
1692
+ const renderInformativeUpdate = () => {
1693
+ if (!stream || wasCanceled() || streamFinalizationPending) return;
1694
+ const informativeText = (0, openclaw_plugin_sdk_channel_outbound.formatChannelProgressDraftText)({
1381
1695
  entry: params.msteamsConfig,
1382
1696
  lines: shouldStreamPreviewToolProgress ? progressLines : [],
1383
1697
  seed: params.progressSeed,
@@ -1418,6 +1732,7 @@ function createTeamsReplyStreamController(params) {
1418
1732
  stream.emit(delta);
1419
1733
  emittedText = fullText;
1420
1734
  tokensEmitted = true;
1735
+ nativeDispatchStarted = true;
1421
1736
  } catch (err) {
1422
1737
  if (isStreamCancelledError(err)) {
1423
1738
  canceledLocally = true;
@@ -1476,8 +1791,17 @@ function createTeamsReplyStreamController(params) {
1476
1791
  text: void 0
1477
1792
  } : void 0;
1478
1793
  }
1794
+ if (streamFailed) {
1795
+ const fallback = fallbackPayloadAfterAcknowledgedText(payload);
1796
+ pendingFinalPayload = void 0;
1797
+ acknowledgedText = "";
1798
+ acknowledgedStreamId = void 0;
1799
+ releaseStreamChunkSubscription();
1800
+ return fallback;
1801
+ }
1479
1802
  if (streamMode === "progress" && payload.text) try {
1480
1803
  stream.emit(payload.text);
1804
+ nativeDispatchStarted = true;
1481
1805
  pendingFinalPayload = fallbackPayloadForSuppressedFinal(payload);
1482
1806
  streamFinalizationPending = true;
1483
1807
  return Boolean(payload.mediaUrl || payload.mediaUrls?.length) ? {
@@ -1493,18 +1817,39 @@ function createTeamsReplyStreamController(params) {
1493
1817
  }
1494
1818
  return payload;
1495
1819
  },
1820
+ claimNativeDelivery() {
1821
+ if (!nativeDispatchStarted || nativeDeliveryClaimed) return false;
1822
+ nativeDeliveryClaimed = true;
1823
+ return true;
1824
+ },
1496
1825
  async finalize() {
1497
1826
  progressDraftGate.cancel();
1498
- if (!stream || !streamFinalizationPending || wasCanceled()) return;
1499
- const finalEntities = [{
1500
- type: "https://schema.org/Message",
1501
- "@type": "Message",
1502
- "@context": "https://schema.org",
1503
- "@id": "",
1504
- additionalType: ["AIGeneratedContent"]
1505
- }];
1506
- const finalChannelData = params.feedbackLoopEnabled ? { feedbackLoopEnabled: true } : {};
1827
+ if (!stream || !nativeDispatchStarted) {
1828
+ releaseStreamChunkSubscription();
1829
+ return { visibleReplySent: false };
1830
+ }
1831
+ const content = wasCanceled() ? acknowledgedText || void 0 : pendingFinalPayload?.text ?? (emittedText || void 0);
1507
1832
  try {
1833
+ if (wasCanceled()) {
1834
+ pendingFinalPayload = void 0;
1835
+ streamFinalizationPending = false;
1836
+ return {
1837
+ visibleReplySent: content !== void 0,
1838
+ ...content === void 0 ? {} : { content }
1839
+ };
1840
+ }
1841
+ if (!streamFinalizationPending) return {
1842
+ visibleReplySent: true,
1843
+ ...content === void 0 ? {} : { content }
1844
+ };
1845
+ const finalEntities = [{
1846
+ type: "https://schema.org/Message",
1847
+ "@type": "Message",
1848
+ "@context": "https://schema.org",
1849
+ "@id": "",
1850
+ additionalType: ["AIGeneratedContent"]
1851
+ }];
1852
+ const finalChannelData = params.feedbackLoopEnabled ? { feedbackLoopEnabled: true } : {};
1508
1853
  stream.emit({
1509
1854
  type: "message",
1510
1855
  entities: finalEntities,
@@ -1515,23 +1860,43 @@ function createTeamsReplyStreamController(params) {
1515
1860
  if (!result) {
1516
1861
  const fallback = pendingFinalPayload;
1517
1862
  pendingFinalPayload = void 0;
1518
- return fallback;
1863
+ const fallbackPayload = fallback && !wasCanceled() ? fallbackPayloadAfterAcknowledgedText(fallback) : void 0;
1864
+ return {
1865
+ visibleReplySent: true,
1866
+ ...content === void 0 ? {} : { content },
1867
+ ...fallbackPayload ? { fallbackPayload } : {}
1868
+ };
1519
1869
  }
1520
1870
  pendingFinalPayload = void 0;
1521
- return;
1871
+ const messageId = require_messenger.extractMessageId(result) ?? void 0;
1872
+ return {
1873
+ visibleReplySent: true,
1874
+ ...content === void 0 ? {} : { content },
1875
+ ...messageId ? { messageId } : {}
1876
+ };
1522
1877
  } catch (err) {
1523
1878
  if (isStreamCancelledError(err)) {
1524
1879
  canceledLocally = true;
1525
1880
  pendingFinalPayload = void 0;
1526
1881
  streamFinalizationPending = false;
1527
- return;
1882
+ return {
1883
+ visibleReplySent: true,
1884
+ ...content === void 0 ? {} : { content }
1885
+ };
1528
1886
  }
1529
1887
  streamFailed = true;
1530
1888
  streamFinalizationPending = false;
1531
1889
  params.log?.warn?.(`msteams stream finalize failed: ${err instanceof Error ? err.message : String(err)}`);
1532
1890
  const fallback = pendingFinalPayload;
1533
1891
  pendingFinalPayload = void 0;
1534
- return fallback;
1892
+ const fallbackPayload = fallback ? fallbackPayloadAfterAcknowledgedText(fallback) : void 0;
1893
+ return {
1894
+ visibleReplySent: true,
1895
+ ...content === void 0 ? {} : { content },
1896
+ ...fallbackPayload ? { fallbackPayload } : {}
1897
+ };
1898
+ } finally {
1899
+ releaseStreamChunkSubscription();
1535
1900
  }
1536
1901
  },
1537
1902
  hasStream() {
@@ -1616,7 +1981,9 @@ function createMSTeamsReplyDispatcher(params) {
1616
1981
  resolveChannelLimitMb: ({ cfg }) => cfg.channels?.msteams?.mediaMaxMb
1617
1982
  });
1618
1983
  const feedbackLoopEnabled = params.cfg.channels?.msteams?.feedbackEnabled !== false;
1984
+ const hookRunner = (0, openclaw_plugin_sdk_plugin_runtime.getGlobalHookRunner)();
1619
1985
  const streamController = createTeamsReplyStreamController({
1986
+ allowProviderPreview: !((hookRunner?.hasHooks("reply_payload_sending") ?? false) || (hookRunner?.hasHooks("message_sending") ?? false)),
1620
1987
  conversationType,
1621
1988
  context: params.context,
1622
1989
  feedbackLoopEnabled,
@@ -1630,7 +1997,7 @@ function createMSTeamsReplyDispatcher(params) {
1630
1997
  const blockStreamingResolved = teamsStreamMode === "block" ? true : (0, openclaw_plugin_sdk_channel_outbound.resolveChannelStreamingBlockEnabled)(msteamsCfg);
1631
1998
  const blockStreamingEnabled = blockStreamingResolved ?? false;
1632
1999
  const typingIndicatorEnabled = typeof msteamsCfg?.typingIndicator === "boolean" ? msteamsCfg.typingIndicator : true;
1633
- const pendingMessages = [];
2000
+ const pendingDeliveries = [];
1634
2001
  const sendMessages = async (messages) => {
1635
2002
  return require_messenger.sendMSTeamsMessages({
1636
2003
  replyStyle: params.replyStyle,
@@ -1669,33 +2036,71 @@ function createMSTeamsReplyDispatcher(params) {
1669
2036
  contextKey: `msteams:delivery-failure:${params.conversationRef.conversation?.id ?? "unknown"}`
1670
2037
  });
1671
2038
  };
1672
- const queueReplyPayload = (payload) => {
1673
- const messages = require_messenger.renderReplyPayloadsToMessages([payload], {
2039
+ const renderReplyPayload = (payload) => {
2040
+ return require_messenger.renderReplyPayloadsToMessages([payload], {
1674
2041
  textChunkLimit: params.textLimit,
1675
2042
  chunkText: true,
1676
2043
  mediaMode: "split",
1677
2044
  tableMode,
1678
2045
  chunkMode
1679
2046
  });
1680
- pendingMessages.push(...messages);
2047
+ };
2048
+ const deliveryOutcome = (delivery) => {
2049
+ const messageIds = [...delivery.nativeMessageId ? [delivery.nativeMessageId] : [], ...delivery.messageIds];
2050
+ return {
2051
+ visibleReplySent: delivery.visibleReplySent,
2052
+ ...messageIds.length > 0 ? { messageIds } : {},
2053
+ ...delivery.visibleReplySent && delivery.content !== void 0 ? { content: delivery.content } : {}
2054
+ };
2055
+ };
2056
+ const settlePendingDelivery = (delivery) => {
2057
+ if (delivery.settled || !delivery.blockSettled || delivery.native && !delivery.nativeSettled) return;
2058
+ delivery.settled = true;
2059
+ const outcome = deliveryOutcome(delivery);
2060
+ if (delivery.errors.length === 0) {
2061
+ delivery.finalization.resolve(outcome);
2062
+ return;
2063
+ }
2064
+ const error = delivery.errors.find((candidate) => !(candidate instanceof openclaw_plugin_sdk_error_runtime.PlatformMessageNotDispatchedError)) ?? delivery.errors[0];
2065
+ delivery.finalization.reject(delivery.visibleReplySent ? (0, openclaw_plugin_sdk_channel_inbound.createChannelPartialDeliveryError)(error, {
2066
+ ...outcome,
2067
+ visibleReplySent: true
2068
+ }) : error);
2069
+ };
2070
+ const queueReplyPayload = (payload, messages, native) => {
2071
+ const delivery = {
2072
+ messages,
2073
+ finalization: (0, openclaw_plugin_sdk_extension_shared.createDeferred)(),
2074
+ content: payload.text,
2075
+ native,
2076
+ nativeSettled: !native,
2077
+ blockSettled: messages.length === 0,
2078
+ settled: false,
2079
+ visibleReplySent: false,
2080
+ messageIds: [],
2081
+ errors: []
2082
+ };
2083
+ pendingDeliveries.push(delivery);
2084
+ return delivery;
1681
2085
  };
1682
2086
  const flushPendingMessages = async () => {
1683
- if (pendingMessages.length === 0) return;
1684
- const toSend = pendingMessages.splice(0);
1685
- const total = toSend.length;
1686
- let ids;
1687
- try {
1688
- ids = await sendMessages(toSend);
1689
- } catch (batchError) {
1690
- ids = [];
2087
+ for (const delivery of pendingDeliveries) {
2088
+ if (delivery.blockSettled) continue;
2089
+ const toSend = delivery.messages.splice(0);
2090
+ const total = toSend.length;
1691
2091
  let failed = 0;
1692
- let lastFailedError = batchError;
2092
+ let lastFailedError;
2093
+ const sentIds = [];
1693
2094
  for (const msg of toSend) try {
1694
2095
  const msgIds = await sendMessages([msg]);
1695
- ids.push(...msgIds);
2096
+ delivery.visibleReplySent ||= msgIds.length > 0;
2097
+ const validIds = msgIds.filter((id) => id.trim() && id !== "unknown");
2098
+ delivery.messageIds.push(...validIds);
2099
+ sentIds.push(...validIds);
1696
2100
  } catch (msgError) {
1697
2101
  failed += 1;
1698
2102
  lastFailedError = msgError;
2103
+ delivery.errors.push(msgError);
1699
2104
  params.log.debug?.("individual message send failed, continuing with remaining blocks");
1700
2105
  }
1701
2106
  if (failed > 0) {
@@ -1709,8 +2114,14 @@ function createMSTeamsReplyDispatcher(params) {
1709
2114
  error: lastFailedError
1710
2115
  });
1711
2116
  }
2117
+ delivery.blockSettled = true;
2118
+ settlePendingDelivery(delivery);
2119
+ if (sentIds.length > 0) try {
2120
+ params.onSentMessageIds?.(sentIds);
2121
+ } catch (error) {
2122
+ params.log.warn?.("failed to record sent Teams message ids", { error: require_setup_surface.formatUnknownError(error) });
2123
+ }
1712
2124
  }
1713
- if (ids.length > 0) params.onSentMessageIds?.(ids);
1714
2125
  };
1715
2126
  const dispatcherOptions = {
1716
2127
  ...replyPipeline,
@@ -1722,11 +2133,22 @@ function createMSTeamsReplyDispatcher(params) {
1722
2133
  typingCallbacks
1723
2134
  };
1724
2135
  const delivery = {
2136
+ observeMessageSent: true,
1725
2137
  deliver: async (payload) => {
1726
2138
  const preparedPayload = streamController.preparePayload(payload);
1727
- if (!preparedPayload) return;
1728
- queueReplyPayload(preparedPayload);
2139
+ const native = streamController.claimNativeDelivery();
2140
+ const messages = preparedPayload ? renderReplyPayload(preparedPayload) : [];
2141
+ if (!native && messages.length === 0) return {
2142
+ visibleReplySent: false,
2143
+ suppression: { reason: "no_visible_result" }
2144
+ };
2145
+ const pending = queueReplyPayload(payload, messages, native);
1729
2146
  if (blockStreamingEnabled) await flushPendingMessages();
2147
+ settlePendingDelivery(pending);
2148
+ return {
2149
+ visibleReplySent: false,
2150
+ finalization: pending.finalization.promise
2151
+ };
1730
2152
  },
1731
2153
  onError: (err, info) => {
1732
2154
  const errMsg = require_setup_surface.formatUnknownError(err);
@@ -1741,30 +2163,41 @@ function createMSTeamsReplyDispatcher(params) {
1741
2163
  });
1742
2164
  }
1743
2165
  };
1744
- const settleDelivery = () => {
1745
- return flushPendingMessages().catch((err) => {
1746
- const errMsg = require_setup_surface.formatUnknownError(err);
1747
- const classification = require_setup_surface.classifyMSTeamsSendError(err);
1748
- const hint = require_setup_surface.formatMSTeamsSendErrorHint(classification);
1749
- params.runtime.error?.(`msteams flush reply failed: ${errMsg}${hint ? ` (${hint})` : ""}`);
1750
- params.log.error("flush reply failed", {
1751
- error: errMsg,
1752
- classification,
1753
- hint
1754
- });
1755
- }).then(async () => {
1756
- const fallbackPayload = await streamController.finalize().catch((err) => {
1757
- params.log.debug?.("stream finalize failed", { error: require_setup_surface.formatUnknownError(err) });
1758
- });
1759
- if (fallbackPayload) {
1760
- queueReplyPayload(fallbackPayload);
1761
- await flushPendingMessages();
1762
- }
1763
- });
2166
+ const settleDelivery = async () => {
2167
+ await flushPendingMessages();
2168
+ const nativeDelivery = pendingDeliveries.find((candidate) => candidate.native && !candidate.nativeSettled);
2169
+ if (!nativeDelivery) {
2170
+ await streamController.finalize();
2171
+ return;
2172
+ }
2173
+ let nativeResult;
2174
+ try {
2175
+ nativeResult = await streamController.finalize();
2176
+ } catch (error) {
2177
+ nativeDelivery.errors.push(error);
2178
+ nativeDelivery.nativeSettled = true;
2179
+ settlePendingDelivery(nativeDelivery);
2180
+ return;
2181
+ }
2182
+ nativeDelivery.visibleReplySent ||= nativeResult.visibleReplySent;
2183
+ nativeDelivery.nativeMessageId = nativeResult.messageId;
2184
+ if (nativeResult.content !== void 0) nativeDelivery.content = nativeResult.content;
2185
+ if (nativeResult.fallbackPayload) {
2186
+ nativeDelivery.messages.push(...renderReplyPayload(nativeResult.fallbackPayload));
2187
+ nativeDelivery.blockSettled = nativeDelivery.messages.length === 0;
2188
+ }
2189
+ nativeDelivery.nativeSettled = true;
2190
+ if (!nativeDelivery.blockSettled) await flushPendingMessages();
2191
+ settlePendingDelivery(nativeDelivery);
2192
+ if (nativeResult.messageId) try {
2193
+ params.onSentMessageIds?.([nativeResult.messageId]);
2194
+ } catch (error) {
2195
+ params.log.warn?.("failed to record sent Teams message id", { error: require_setup_surface.formatUnknownError(error) });
2196
+ }
1764
2197
  };
1765
- const previewToolProgressEnabled = (0, openclaw_plugin_sdk_channel_outbound.resolveChannelStreamingPreviewToolProgress)(msteamsCfg);
2198
+ const previewToolProgressEnabled = (0, openclaw_plugin_sdk_channel_outbound.resolveChannelStreamingPreviewToolProgress)(msteamsCfg, true, teamsStreamMode);
1766
2199
  const suppressDefaultToolProgressMessages = (0, openclaw_plugin_sdk_channel_outbound.resolveChannelStreamingSuppressDefaultToolProgressMessages)(msteamsCfg);
1767
- const shouldSuppressDefaultToolProgressMessages = teamsStreamMode === "progress" && suppressDefaultToolProgressMessages && previewToolProgressEnabled;
2200
+ const shouldSuppressDefaultToolProgressMessages = streamController.hasStream() && teamsStreamMode === "progress" && suppressDefaultToolProgressMessages && previewToolProgressEnabled;
1768
2201
  const progressCallbacks = streamController.hasStream() ? {
1769
2202
  onReasoningStream: async (payload) => {
1770
2203
  const text = typeof payload?.text === "string" ? payload.text : void 0;
@@ -1901,238 +2334,571 @@ async function wasMSTeamsMessageSentWithPersistence(params) {
1901
2334
  return await sentMessages.lookup(makeKey(params.conversationId, params.messageId));
1902
2335
  }
1903
2336
  //#endregion
1904
- //#region extensions/msteams/src/team-identity.ts
1905
- const teamGroupIdCache = /* @__PURE__ */ new Map();
1906
- const TEAM_GROUP_ID_CACHE_MAX_ENTRIES = 500;
1907
- function cacheTeamGroupId(conversationTeamId, groupId) {
1908
- teamGroupIdCache.set(conversationTeamId, groupId);
1909
- (0, openclaw_plugin_sdk_collection_runtime.pruneMapToMaxSize)(teamGroupIdCache, TEAM_GROUP_ID_CACHE_MAX_ENTRIES);
1910
- }
1911
- /** Resolve the Graph team GUID without ever treating a Bot Framework team ID as equivalent. */
1912
- async function resolveTeamGroupId(params) {
1913
- const activityGroupId = params.aadGroupId?.trim();
1914
- if (activityGroupId) {
1915
- cacheTeamGroupId(params.conversationTeamId, activityGroupId);
1916
- return activityGroupId;
1917
- }
1918
- const cached = teamGroupIdCache.get(params.conversationTeamId);
1919
- if (cached) return cached;
1920
- const getTeamDetails = params.getTeamDetails;
1921
- if (!getTeamDetails) return;
1922
- const groupId = (await require_resolve_allowlist.withMSTeamsRequestDeadline({
1923
- deadline: params.deadline,
1924
- label: "MS Teams team details",
1925
- work: () => getTeamDetails(params.conversationTeamId)
1926
- })).aadGroupId?.trim();
1927
- if (!groupId) return;
1928
- cacheTeamGroupId(params.conversationTeamId, groupId);
1929
- return groupId;
1930
- }
1931
- //#endregion
1932
- //#region extensions/msteams/src/monitor-handler/inbound-media.ts
1933
- function shouldAttemptMSTeamsGraphMediaFallback(params) {
1934
- const conversationType = params.conversationType.trim().toLowerCase();
1935
- return params.graphMediaFallback === true && (conversationType === "channel" || conversationType === "groupchat") && (params.htmlSummary?.htmlAttachments ?? 0) > 0;
1936
- }
1937
- function resolveMSTeamsInboundMediaBody(params) {
1938
- const unavailableCount = params.materializedMedia.filter((media) => !media.path).length + Math.max(0, params.nativeMedia.length - params.materializedMedia.length);
1939
- if (unavailableCount === 0) return params.body;
1940
- return (0, openclaw_plugin_sdk_channel_inbound.formatInboundMediaUnavailableText)({
1941
- body: params.body,
1942
- notice: `[msteams ${unavailableCount > 1 ? `${unavailableCount} attachments` : "attachment"} unavailable]`
2337
+ //#region extensions/msteams/src/monitor-handler/inbound-dispatch.ts
2338
+ async function dispatchMSTeamsInboundTurn(params) {
2339
+ const core = require_runtime.getMSTeamsRuntime();
2340
+ const { cfg, runtime, appId, app, tokenProvider, textLimit, log, logVerboseMessage, facts, admission, content, routing, thread, replyStyle, timestamp, contextVisibilityMode, conversationHistories, historyLimit } = params;
2341
+ const { context, activity, rawBody, text, quoteInfo, conversationRef } = facts;
2342
+ const { senderId, senderName, isDirectMessage, allowNameMatching, groupPolicy, commandAuthorized, effectiveGroupAllowFrom } = admission;
2343
+ const { route } = routing;
2344
+ const { agentBody, inboundMedia } = content;
2345
+ const { teamAadGroupId, quoteBodyFull, quoteSenderId, quoteSenderName, threadContext } = thread;
2346
+ const { conversationId, conversationType, isChannel, teamId, graphChannelId } = facts;
2347
+ const teamsFrom = isDirectMessage ? `msteams:${senderId}` : isChannel ? `msteams:channel:${conversationId}` : `msteams:group:${conversationId}`;
2348
+ const teamsTo = isDirectMessage ? `user:${senderId}` : `conversation:${conversationId}`;
2349
+ const envelopeFrom = isDirectMessage ? senderName : conversationType;
2350
+ const buildEnvelope = (0, openclaw_plugin_sdk_channel_inbound.createChannelInboundEnvelopeBuilder)({
2351
+ cfg,
2352
+ route
1943
2353
  });
1944
- }
1945
- function hasDefinitiveContentType(media) {
1946
- const contentType = media.contentType?.split(";", 1)[0]?.trim().toLowerCase();
1947
- return Boolean(contentType && contentType !== "application/octet-stream" && contentType !== "binary/octet-stream");
1948
- }
1949
- function mergeMSTeamsMediaFacts(nativeMedia, materializedMedia, options = {}) {
1950
- const merged = [...nativeMedia];
1951
- const nativeSlotCount = nativeMedia.length;
1952
- const nativeIndexBySourceId = /* @__PURE__ */ new Map();
1953
- nativeMedia.forEach((media, index) => {
1954
- if (media.sourceId && !nativeIndexBySourceId.has(media.sourceId)) nativeIndexBySourceId.set(media.sourceId, index);
2354
+ let combinedBody = buildEnvelope({
2355
+ channel: "Teams",
2356
+ from: envelopeFrom,
2357
+ timestamp,
2358
+ body: agentBody
1955
2359
  });
1956
- for (const [index, materialized] of materializedMedia.entries()) {
1957
- const sourceIndex = materialized.sourceId ? nativeIndexBySourceId.get(materialized.sourceId) : void 0;
1958
- const positionalIndex = options.positionallyAligned === false || index >= nativeMedia.length ? void 0 : index;
1959
- const mayUseFallbackOrder = options.positionallyAligned === false;
1960
- const isEligibleUnresolved = (media) => !media.path && !media.sourceId;
1961
- const sameKindUnresolvedIndexes = mayUseFallbackOrder ? merged.slice(0, nativeSlotCount).flatMap((media, mediaIndex) => isEligibleUnresolved(media) && media.kind === materialized.kind ? [mediaIndex] : []) : [];
1962
- const unresolvedIndexes = sameKindUnresolvedIndexes.length === 0 && mayUseFallbackOrder ? merged.slice(0, nativeSlotCount).flatMap((media, mediaIndex) => isEligibleUnresolved(media) ? [mediaIndex] : []) : [];
1963
- const fallbackIndex = sameKindUnresolvedIndexes.length > 0 ? sameKindUnresolvedIndexes[0] : unresolvedIndexes.length === 1 ? unresolvedIndexes[0] : void 0;
1964
- const targetIndex = sourceIndex ?? positionalIndex ?? fallbackIndex;
1965
- if (targetIndex === void 0) {
1966
- if (materialized.sourceId) nativeIndexBySourceId.set(materialized.sourceId, merged.length);
1967
- merged.push(materialized);
1968
- } else {
1969
- if (materialized.sourceId) nativeIndexBySourceId.set(materialized.sourceId, targetIndex);
1970
- const current = merged[targetIndex];
1971
- if (materialized.path) merged[targetIndex] = materialized;
1972
- else if (!current?.path) merged[targetIndex] = {
1973
- ...current,
1974
- ...materialized,
1975
- kind: hasDefinitiveContentType(materialized) || !current?.kind ? materialized.kind : current.kind
1976
- };
2360
+ const isRoomish = !isDirectMessage;
2361
+ const historyKey = isRoomish ? conversationId : void 0;
2362
+ if (isRoomish && historyKey) combinedBody = (0, openclaw_plugin_sdk_reply_history.createChannelHistoryWindow)({ historyMap: conversationHistories }).buildPendingContext({
2363
+ historyKey,
2364
+ limit: historyLimit,
2365
+ currentMessage: combinedBody,
2366
+ formatEntry: (entry) => buildEnvelope({
2367
+ channel: "Teams",
2368
+ from: conversationType,
2369
+ timestamp: entry.timestamp,
2370
+ previousTimestamp: null,
2371
+ body: `${entry.sender}: ${entry.body}${entry.messageId ? ` [id:${entry.messageId}]` : ""}`
2372
+ })
2373
+ });
2374
+ const inboundHistory = isRoomish && historyKey && historyLimit > 0 ? (0, openclaw_plugin_sdk_reply_history.createChannelHistoryWindow)({ historyMap: conversationHistories }).buildInboundHistory({
2375
+ historyKey,
2376
+ limit: historyLimit
2377
+ }) : void 0;
2378
+ const commandBody = text.trim();
2379
+ const quoteSenderAllowed = quoteInfo && quoteInfo.sender ? (0, openclaw_plugin_sdk_channel_inbound.resolveInboundSupplementalSenderAllowed)({
2380
+ isGroup: !isDirectMessage,
2381
+ groupPolicy,
2382
+ allowFrom: effectiveGroupAllowFrom,
2383
+ isSenderAllowed: (allowFrom) => require_channel.resolveMSTeamsAllowlistMatch({
2384
+ allowFrom,
2385
+ senderId: quoteSenderId ?? "",
2386
+ senderName: quoteSenderName,
2387
+ allowNameMatching
2388
+ }).allowed
2389
+ }) : true;
2390
+ const bodyForAgent = threadContext ? `[Thread history]\n${threadContext}\n[/Thread history]\n\n${agentBody}` : agentBody;
2391
+ const nativeChannelId = isChannel && teamAadGroupId ? `${teamAadGroupId}/${graphChannelId}` : void 0;
2392
+ const ctxPayload = (0, openclaw_plugin_sdk_channel_inbound.buildChannelInboundEventContext)({
2393
+ channel: "msteams",
2394
+ contextVisibility: contextVisibilityMode,
2395
+ supplemental: { quote: quoteInfo ? {
2396
+ id: quoteInfo.id ?? activity.replyToId ?? void 0,
2397
+ body: quoteBodyFull ?? quoteInfo.body,
2398
+ sender: quoteInfo.sender,
2399
+ senderAllowed: quoteSenderAllowed,
2400
+ isQuote: true
2401
+ } : void 0 },
2402
+ media: await (0, openclaw_plugin_sdk_channel_inbound.toInboundMediaFactsWithMetadata)(inboundMedia),
2403
+ messageId: activity.id,
2404
+ timestamp: timestamp?.getTime() ?? Date.now(),
2405
+ from: teamsFrom,
2406
+ sender: {
2407
+ id: senderId,
2408
+ name: senderName
2409
+ },
2410
+ conversation: {
2411
+ kind: isDirectMessage ? "direct" : isChannel ? "channel" : "group",
2412
+ id: conversationId,
2413
+ label: envelopeFrom,
2414
+ spaceId: teamId,
2415
+ nativeChannelId
2416
+ },
2417
+ route: {
2418
+ agentId: route.agentId,
2419
+ dmScope: route.dmScope,
2420
+ accountId: route.accountId,
2421
+ routeSessionKey: route.sessionKey
2422
+ },
2423
+ reply: {
2424
+ to: teamsTo,
2425
+ replyToId: activity.replyToId ?? void 0,
2426
+ nativeChannelId
2427
+ },
2428
+ message: {
2429
+ body: combinedBody,
2430
+ bodyForAgent,
2431
+ inboundHistory,
2432
+ rawBody,
2433
+ commandBody
2434
+ },
2435
+ sessionTranscript: { historyLimit: isRoomish ? historyLimit : 0 },
2436
+ access: {
2437
+ mentions: {
2438
+ canDetectMention: !isDirectMessage,
2439
+ wasMentioned: isDirectMessage || params.mentionWasEffective
2440
+ },
2441
+ commands: { authorized: commandAuthorized === true }
2442
+ },
2443
+ extra: {
2444
+ GroupSubject: !isDirectMessage ? conversationType : void 0,
2445
+ ReplyToIsQuote: quoteInfo ? true : void 0
2446
+ }
2447
+ });
2448
+ const preview = (0, openclaw_plugin_sdk_text_utility_runtime.sliceUtf16Safe)(rawBody.replace(/\s+/g, " "), 0, 160);
2449
+ logVerboseMessage(`msteams inbound: from=${ctxPayload.From} preview="${preview}"`);
2450
+ const { dispatcherOptions, delivery, replyOptions } = createMSTeamsReplyDispatcher({
2451
+ cfg,
2452
+ agentId: route.agentId,
2453
+ sessionKey: route.sessionKey,
2454
+ accountId: route.accountId,
2455
+ runtime,
2456
+ log,
2457
+ app,
2458
+ appId,
2459
+ conversationRef,
2460
+ context,
2461
+ replyStyle,
2462
+ textLimit,
2463
+ onSentMessageIds: (ids) => {
2464
+ for (const id of ids) recordMSTeamsSentMessage(conversationId, id);
2465
+ },
2466
+ tokenProvider,
2467
+ sharePointSiteId: cfg.channels?.msteams?.sharePointSiteId
2468
+ });
2469
+ const senderTimezone = (activity.entities?.find((entity) => entity.type === "clientInfo"))?.timezone || conversationRef.timezone;
2470
+ const turnConfig = senderTimezone && !cfg.agents?.defaults?.userTimezone ? {
2471
+ ...cfg,
2472
+ agents: {
2473
+ ...cfg.agents,
2474
+ defaults: {
2475
+ ...cfg.agents?.defaults,
2476
+ userTimezone: senderTimezone
2477
+ }
1977
2478
  }
2479
+ } : cfg;
2480
+ log.info("dispatching to agent", { sessionKey: route.sessionKey });
2481
+ try {
2482
+ const turnResult = await core.channel.inbound.run({
2483
+ channel: "msteams",
2484
+ accountId: route.accountId,
2485
+ raw: context,
2486
+ adapter: {
2487
+ ingest: () => ({
2488
+ id: activity.id ?? `${teamsFrom}:${Date.now()}`,
2489
+ timestamp: timestamp?.getTime(),
2490
+ rawText: rawBody,
2491
+ textForAgent: bodyForAgent,
2492
+ textForCommands: commandBody,
2493
+ raw: activity
2494
+ }),
2495
+ resolveTurn: () => ({
2496
+ cfg: turnConfig,
2497
+ channel: "msteams",
2498
+ accountId: route.accountId,
2499
+ route: {
2500
+ agentId: route.agentId,
2501
+ sessionKey: route.sessionKey
2502
+ },
2503
+ ctxPayload,
2504
+ record: { onRecordError: (err) => {
2505
+ logVerboseMessage(`msteams: failed updating session meta: ${require_setup_surface.formatUnknownError(err)}`);
2506
+ } },
2507
+ history: {
2508
+ isGroup: isRoomish,
2509
+ historyKey,
2510
+ historyMap: conversationHistories,
2511
+ limit: historyLimit
2512
+ },
2513
+ dispatcherOptions,
2514
+ delivery,
2515
+ replyOptions: {
2516
+ ...replyOptions,
2517
+ ...facts.turnAdoptionLifecycle ? (0, openclaw_plugin_sdk_channel_outbound.bindIngressLifecycleToReplyOptions)(facts.turnAdoptionLifecycle) : {}
2518
+ }
2519
+ })
2520
+ }
2521
+ });
2522
+ const dispatchResult = turnResult.dispatched ? turnResult.dispatchResult : void 0;
2523
+ const counts = (0, openclaw_plugin_sdk_channel_inbound.resolveInboundReplyDispatchCounts)(dispatchResult);
2524
+ log.info("dispatch complete", {
2525
+ queuedFinal: dispatchResult?.queuedFinal ?? false,
2526
+ counts
2527
+ });
2528
+ if ((0, openclaw_plugin_sdk_channel_inbound.hasFinalInboundReplyDispatch)(dispatchResult)) logVerboseMessage(`msteams: delivered ${counts.final} repl${counts.final === 1 ? "y" : "ies"} to ${teamsTo}`);
2529
+ return {
2530
+ kind: "completed",
2531
+ finalResponses: counts.final
2532
+ };
2533
+ } catch (err) {
2534
+ log.error("dispatch failed", { error: require_setup_surface.formatUnknownError(err) });
2535
+ runtime.error(`msteams dispatch failed: ${require_setup_surface.formatUnknownError(err)}`);
2536
+ if (facts.turnAdoptionLifecycle) throw err;
2537
+ try {
2538
+ await context.sendActivity("⚠️ Something went wrong. Please try again.");
2539
+ } catch {}
2540
+ return { kind: "failed" };
1978
2541
  }
1979
- return merged;
1980
2542
  }
1981
- function hasMaterializedMedia(media) {
1982
- return media.some((entry) => Boolean(entry.path));
2543
+ //#endregion
2544
+ //#region extensions/msteams/src/monitor-handler/inbound-facts.ts
2545
+ function extractTextFromHtmlAttachments(attachments) {
2546
+ for (const attachment of attachments) {
2547
+ const raw = require_resolve_allowlist.extractHtmlFromAttachment(attachment);
2548
+ if (!raw) continue;
2549
+ const text = raw.replace(/<at[^>]*>.*?<\/at>/gis, " ").replace(/<a\b[^>]*href=["']([^"']+)["'][^>]*>(.*?)<\/a>/gis, "$2 $1").replace(/<br\s*\/?>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<[^>]+>/g, " ").replace(/&nbsp;/gi, " ").replace(/&amp;/gi, "&").replace(/\s+/g, " ").trim();
2550
+ if (text) return text;
2551
+ }
2552
+ return "";
1983
2553
  }
1984
- async function resolveMSTeamsInboundMedia(params) {
1985
- const { attachments, htmlSummary, maxBytes, tokenProvider, allowHosts, conversationType, conversationId, conversationMessageId, teamAadGroupId, serviceUrl, activity, log, preserveFilenames } = params;
1986
- let mediaList = await downloadMSTeamsAttachments({
2554
+ async function prepareMSTeamsDebounceEntry(params) {
2555
+ const activity = params.context.activity;
2556
+ const attachments = Array.isArray(activity.attachments) ? activity.attachments : [];
2557
+ const rawText = activity.text?.trim() ?? "";
2558
+ const htmlText = extractTextFromHtmlAttachments(attachments);
2559
+ const valueText = rawText || htmlText ? "" : serializeMSTeamsAdaptiveCardActionValue(activity.value);
2560
+ const text = require_resolve_allowlist.stripMSTeamsMentionTags(rawText || htmlText || valueText || "");
2561
+ const conversationId = require_resolve_allowlist.normalizeMSTeamsConversationId(activity.conversation?.id ?? "");
2562
+ const replyToId = activity.replyToId ?? void 0;
2563
+ const implicitMentionKinds = conversationId && replyToId && await wasMSTeamsMessageSentWithPersistence({
2564
+ conversationId,
2565
+ messageId: replyToId
2566
+ }) ? ["reply_to_bot"] : [];
2567
+ return {
2568
+ context: params.context,
2569
+ rawText,
2570
+ text,
1987
2571
  attachments,
1988
- maxBytes,
1989
- tokenProvider,
1990
- allowHosts,
1991
- authAllowHosts: params.authAllowHosts,
1992
- preserveFilenames,
1993
- deadline: params.deadline,
1994
- logger: log
1995
- });
1996
- if (!hasMaterializedMedia(mediaList)) {
1997
- const attachmentIds = extractMSTeamsHtmlAttachmentIds(attachments);
1998
- const hasHtmlFileAttachment = attachmentIds.length > 0;
1999
- const hasChannelOrGroupHtml = shouldAttemptMSTeamsGraphMediaFallback({
2572
+ wasMentioned: require_resolve_allowlist.wasMSTeamsBotMentioned(activity),
2573
+ implicitMentionKinds,
2574
+ turnAdoptionLifecycle: params.turnAdoptionLifecycle
2575
+ };
2576
+ }
2577
+ function buildStoredConversationReference(params) {
2578
+ const { activity, conversationId, conversationType, teamId, threadId } = params;
2579
+ const from = activity.from;
2580
+ const conversation = activity.conversation;
2581
+ const clientInfo = activity.entities?.find((entity) => entity.type === "clientInfo");
2582
+ const tenantId = activity.channelData?.tenant?.id ?? conversation?.tenantId;
2583
+ const aadObjectId = from?.aadObjectId;
2584
+ const serviceUrl = require_resolve_allowlist.tryNormalizeBotFrameworkServiceUrl(activity.serviceUrl);
2585
+ return {
2586
+ activityId: activity.id,
2587
+ user: from ? {
2588
+ id: from.id,
2589
+ name: from.name,
2590
+ aadObjectId: from.aadObjectId
2591
+ } : void 0,
2592
+ agent: activity.recipient,
2593
+ conversation: {
2594
+ id: conversationId,
2000
2595
  conversationType,
2001
- htmlSummary,
2002
- graphMediaFallback: params.graphMediaFallback
2003
- });
2004
- const shouldFetchGraphMessage = hasHtmlFileAttachment || hasChannelOrGroupHtml;
2005
- const isBotFrameworkPersonalChat = isBotFrameworkPersonalChatId(conversationId);
2006
- if (hasHtmlFileAttachment && isBotFrameworkPersonalChat) if (!serviceUrl) log.debug?.("bot framework attachment skipped (missing serviceUrl)", {
2596
+ tenantId
2597
+ },
2598
+ ...tenantId ? { tenantId } : {},
2599
+ ...aadObjectId ? { aadObjectId } : {},
2600
+ teamId,
2601
+ channelId: activity.channelId,
2602
+ ...serviceUrl ? { serviceUrl } : {},
2603
+ locale: activity.locale,
2604
+ ...clientInfo?.timezone ? { timezone: clientInfo.timezone } : {},
2605
+ ...threadId ? { threadId } : {}
2606
+ };
2607
+ }
2608
+ function assembleMSTeamsInboundFacts(params) {
2609
+ const { entry, mediaMaxBytes } = params;
2610
+ const activity = entry.context.activity;
2611
+ const conversation = activity.conversation;
2612
+ const rawConversationId = conversation?.id ?? "";
2613
+ const conversationId = require_resolve_allowlist.normalizeMSTeamsConversationId(rawConversationId);
2614
+ const conversationMessageId = require_resolve_allowlist.extractMSTeamsConversationMessageId(rawConversationId);
2615
+ const conversationType = conversation?.conversationType ?? "personal";
2616
+ const isChannel = conversationType === "channel";
2617
+ const teamId = activity.channelData?.team?.id;
2618
+ const threadId = isChannel ? conversationMessageId ?? activity.replyToId ?? void 0 : void 0;
2619
+ const advertisedMedia = resolveMSTeamsAdvertisedMedia(entry.attachments, {
2620
+ maxInlineBytes: mediaMaxBytes,
2621
+ maxInlineTotalBytes: mediaMaxBytes
2622
+ });
2623
+ return {
2624
+ ...entry,
2625
+ activity,
2626
+ from: activity.from,
2627
+ conversation,
2628
+ rawBody: entry.text,
2629
+ advertisedMedia,
2630
+ quoteInfo: require_resolve_allowlist.extractMSTeamsQuoteInfo(entry.attachments),
2631
+ attachmentTypes: entry.attachments.map((attachment) => typeof attachment.contentType === "string" ? attachment.contentType : void 0).filter(Boolean).slice(0, 3),
2632
+ htmlSummary: summarizeMSTeamsHtmlAttachments(entry.attachments),
2633
+ rawConversationId,
2634
+ conversationId,
2635
+ conversationMessageId,
2636
+ conversationType,
2637
+ isChannel,
2638
+ teamId,
2639
+ graphChannelId: activity.channelData?.channel?.id?.trim() || conversationId,
2640
+ threadId,
2641
+ conversationRef: buildStoredConversationReference({
2642
+ activity,
2643
+ conversationId,
2007
2644
  conversationType,
2008
- conversationId
2009
- });
2010
- else {
2011
- const bfMedia = await downloadMSTeamsBotFrameworkAttachments({
2012
- serviceUrl,
2013
- attachmentIds,
2014
- tokenProvider,
2015
- maxBytes,
2016
- allowHosts,
2017
- authAllowHosts: params.authAllowHosts,
2018
- preserveFilenames,
2019
- deadline: params.deadline
2020
- });
2021
- if (bfMedia.media.length > 0) mediaList = mergeMSTeamsMediaFacts(mediaList, bfMedia.media, { positionallyAligned: false });
2022
- if (!hasMaterializedMedia(bfMedia.media)) log.debug?.("bot framework attachments fetch empty", {
2023
- conversationType,
2024
- attachmentCount: bfMedia.attachmentCount ?? attachmentIds.length
2025
- });
2026
- }
2027
- if (shouldFetchGraphMessage && !hasMaterializedMedia(mediaList) && !isBotFrameworkPersonalChat) {
2028
- const graphTeamAadGroupId = conversationType.trim().toLowerCase() === "channel" && !teamAadGroupId ? await params.resolveTeamAadGroupId?.() : teamAadGroupId;
2029
- const messageUrl = buildMSTeamsGraphMessageUrl({
2030
- conversationType,
2031
- conversationId,
2032
- messageId: activity.id ?? void 0,
2033
- threadRootMessageId: conversationMessageId ?? activity.replyToId,
2034
- teamAadGroupId: graphTeamAadGroupId,
2035
- channelId: activity.channelData?.channel?.id
2036
- });
2037
- if (!messageUrl) log.debug?.("graph message url unavailable", {
2038
- conversationType,
2039
- hasChannelData: Boolean(activity.channelData),
2040
- messageId: activity.id ?? void 0,
2041
- replyToId: activity.replyToId ?? void 0
2042
- });
2043
- else {
2044
- const graphMedia = await downloadMSTeamsGraphMedia({
2045
- messageUrl,
2046
- tokenProvider,
2047
- maxBytes,
2048
- allowHosts,
2049
- authAllowHosts: params.authAllowHosts,
2050
- preserveFilenames,
2051
- deadline: params.deadline,
2052
- logger: log
2053
- });
2054
- if (graphMedia.media.length > 0) mediaList = mergeMSTeamsMediaFacts(mediaList, graphMedia.media, { positionallyAligned: false });
2055
- if (!hasMaterializedMedia(mediaList)) log.debug?.("graph media fetch empty", {
2056
- messageUrl,
2057
- hostedStatus: graphMedia.hostedStatus,
2058
- attachmentStatus: graphMedia.attachmentStatus,
2059
- hostedCount: graphMedia.hostedCount,
2060
- attachmentCount: graphMedia.attachmentCount,
2061
- tokenError: graphMedia.tokenError,
2062
- attachmentIdCount: attachmentIds.length
2063
- });
2064
- }
2065
- }
2645
+ teamId,
2646
+ threadId
2647
+ })
2648
+ };
2649
+ }
2650
+ //#endregion
2651
+ //#region extensions/msteams/src/team-identity.ts
2652
+ const teamGroupIdCache = /* @__PURE__ */ new Map();
2653
+ const TEAM_GROUP_ID_CACHE_MAX_ENTRIES = 500;
2654
+ function cacheTeamGroupId(conversationTeamId, groupId) {
2655
+ teamGroupIdCache.set(conversationTeamId, groupId);
2656
+ (0, openclaw_plugin_sdk_collection_runtime.pruneMapToMaxSize)(teamGroupIdCache, TEAM_GROUP_ID_CACHE_MAX_ENTRIES);
2657
+ }
2658
+ /** Resolve the Graph team GUID without ever treating a Bot Framework team ID as equivalent. */
2659
+ async function resolveTeamGroupId(params) {
2660
+ const activityGroupId = params.aadGroupId?.trim();
2661
+ if (activityGroupId) {
2662
+ cacheTeamGroupId(params.conversationTeamId, activityGroupId);
2663
+ return activityGroupId;
2066
2664
  }
2067
- const materializedCount = mediaList.filter((media) => Boolean(media.path)).length;
2068
- if (materializedCount > 0) log.debug?.("downloaded attachments", { count: materializedCount });
2069
- else if (htmlSummary?.imgTags) log.debug?.("inline images detected but none downloaded", {
2070
- imgTags: htmlSummary.imgTags,
2071
- srcHosts: htmlSummary.srcHosts,
2072
- dataImages: htmlSummary.dataImages,
2073
- cidImages: htmlSummary.cidImages
2665
+ const cached = teamGroupIdCache.get(params.conversationTeamId);
2666
+ if (cached) return cached;
2667
+ const getTeamDetails = params.getTeamDetails;
2668
+ if (!getTeamDetails) return;
2669
+ const groupId = (await require_resolve_allowlist.withMSTeamsRequestDeadline({
2670
+ deadline: params.deadline,
2671
+ label: "MS Teams team details",
2672
+ work: () => getTeamDetails(params.conversationTeamId)
2673
+ })).aadGroupId?.trim();
2674
+ if (!groupId) return;
2675
+ cacheTeamGroupId(params.conversationTeamId, groupId);
2676
+ return groupId;
2677
+ }
2678
+ //#endregion
2679
+ //#region extensions/msteams/src/thread-parent-context.ts
2680
+ const PARENT_CACHE_TTL_MS = 300 * 1e3;
2681
+ const PARENT_CACHE_MAX = 100;
2682
+ const parentCache = /* @__PURE__ */ new Map();
2683
+ const INJECTED_MAX = 200;
2684
+ const injectedParents = /* @__PURE__ */ new Map();
2685
+ function touchLru(map, key, value, max) {
2686
+ if (map.has(key)) map.delete(key);
2687
+ else if (map.size >= max) {
2688
+ const firstKey = map.keys().next().value;
2689
+ if (firstKey !== void 0) map.delete(firstKey);
2690
+ }
2691
+ map.set(key, value);
2692
+ }
2693
+ function buildParentCacheKey(groupId, channelId, parentId) {
2694
+ return `${groupId}\u0000${channelId}\u0000${parentId}`;
2695
+ }
2696
+ function resolveParentCacheExpiresAt(nowRaw) {
2697
+ const nowMs = (0, openclaw_plugin_sdk_number_runtime.asDateTimestampMs)(nowRaw);
2698
+ return nowMs === void 0 ? void 0 : (0, openclaw_plugin_sdk_number_runtime.resolveExpiresAtMsFromDurationMs)(PARENT_CACHE_TTL_MS, { nowMs });
2699
+ }
2700
+ /**
2701
+ * Fetch a channel parent message with an LRU+TTL cache.
2702
+ *
2703
+ * Uses the injected `fetchParent` (defaults to `fetchChannelMessage`) so
2704
+ * tests can swap in a stub without mocking the Graph transport.
2705
+ */
2706
+ async function fetchParentMessageCached(token, groupId, channelId, parentId, fetchParent = require_messenger.fetchChannelMessage) {
2707
+ const key = buildParentCacheKey(groupId, channelId, parentId);
2708
+ const now = (0, openclaw_plugin_sdk_number_runtime.asDateTimestampMs)(Date.now());
2709
+ const cached = parentCache.get(key);
2710
+ const cachedExpiresAt = cached ? (0, openclaw_plugin_sdk_number_runtime.asDateTimestampMs)(cached.expiresAt) : void 0;
2711
+ if (cached && now !== void 0 && cachedExpiresAt !== void 0 && cachedExpiresAt > now) {
2712
+ parentCache.delete(key);
2713
+ parentCache.set(key, cached);
2714
+ return cached.message;
2715
+ }
2716
+ if (cached) parentCache.delete(key);
2717
+ const message = await fetchParent(token, groupId, channelId, parentId);
2718
+ const expiresAt = resolveParentCacheExpiresAt(Date.now());
2719
+ if (expiresAt !== void 0) touchLru(parentCache, key, {
2720
+ message,
2721
+ expiresAt
2722
+ }, PARENT_CACHE_MAX);
2723
+ return message;
2724
+ }
2725
+ const PARENT_TEXT_MAX_CHARS = 400;
2726
+ /**
2727
+ * Extract a compact summary (sender + plain-text body) from a Graph parent
2728
+ * message. Returns undefined when the parent cannot be summarized (missing
2729
+ * or blank body).
2730
+ */
2731
+ function summarizeParentMessage(message) {
2732
+ if (!message) return;
2733
+ const sender = message.from?.user?.displayName ?? message.from?.application?.displayName ?? "unknown";
2734
+ const contentType = message.body?.contentType ?? "text";
2735
+ const raw = message.body?.content ?? "";
2736
+ const text = contentType === "html" ? require_messenger.stripHtmlFromTeamsMessage(raw) : raw.replace(/\s+/g, " ").trim();
2737
+ if (!text) return;
2738
+ return {
2739
+ sender,
2740
+ text: text.length > PARENT_TEXT_MAX_CHARS ? `${(0, openclaw_plugin_sdk_text_utility_runtime.truncateUtf16Safe)(text, PARENT_TEXT_MAX_CHARS - 1)}…` : text
2741
+ };
2742
+ }
2743
+ /**
2744
+ * Build the single-line `Replying to @sender: body` system event text.
2745
+ * Callers should pass this text to `enqueueSystemEvent` together with a
2746
+ * stable contextKey derived from the parent id.
2747
+ */
2748
+ function formatParentContextEvent(summary) {
2749
+ return `Replying to @${summary.sender}: ${summary.text}`;
2750
+ }
2751
+ /**
2752
+ * Decide whether a parent context event should be enqueued for the current
2753
+ * session. Returns `false` when we already injected the same parent for this
2754
+ * session recently (prevents re-prepending identical context on every reply
2755
+ * in the thread).
2756
+ */
2757
+ function shouldInjectParentContext(sessionKey, parentId) {
2758
+ const key = sessionKey;
2759
+ return injectedParents.get(key) !== parentId;
2760
+ }
2761
+ /**
2762
+ * Record that `parentId` was just injected for `sessionKey` so subsequent
2763
+ * replies with the same parent can short-circuit via `shouldInjectParentContext`.
2764
+ */
2765
+ function markParentContextInjected(sessionKey, parentId) {
2766
+ touchLru(injectedParents, sessionKey, parentId, INJECTED_MAX);
2767
+ }
2768
+ //#endregion
2769
+ //#region extensions/msteams/src/monitor-handler/thread-context.ts
2770
+ function prepareMSTeamsThreadRouting(params) {
2771
+ const route = require_runtime.getMSTeamsRuntime().channel.routing.resolveAgentRoute({
2772
+ cfg: params.cfg,
2773
+ channel: "msteams",
2774
+ teamId: params.teamId,
2775
+ peer: {
2776
+ kind: params.isDirectMessage ? "direct" : params.isChannel ? "channel" : "group",
2777
+ id: params.isDirectMessage ? params.senderId : params.conversationId
2778
+ }
2074
2779
  });
2075
- return mediaList;
2780
+ route.sessionKey = require_channel.resolveMSTeamsRouteSessionKey({
2781
+ baseSessionKey: route.sessionKey,
2782
+ isChannel: params.isChannel,
2783
+ conversationMessageId: params.conversationMessageId,
2784
+ replyToId: params.context.activity.replyToId
2785
+ });
2786
+ const deadline = require_resolve_allowlist.createMSTeamsInboundDeadline();
2787
+ let teamAadGroupId = params.context.activity.channelData?.team?.aadGroupId?.trim() || void 0;
2788
+ const conversationTeamId = params.isChannel ? params.teamId : void 0;
2789
+ let teamGroupIdPromise;
2790
+ const resolveTeamAadGroupId = async () => {
2791
+ if (!conversationTeamId) return;
2792
+ teamGroupIdPromise ??= resolveTeamGroupId({
2793
+ conversationTeamId,
2794
+ aadGroupId: teamAadGroupId,
2795
+ getTeamDetails: params.context.getTeamDetails,
2796
+ deadline
2797
+ }).catch((err) => {
2798
+ params.log.debug?.("failed to resolve Teams AAD group ID", {
2799
+ teamId: conversationTeamId,
2800
+ error: require_setup_surface.formatUnknownError(err)
2801
+ });
2802
+ });
2803
+ teamAadGroupId = await teamGroupIdPromise;
2804
+ return teamAadGroupId;
2805
+ };
2806
+ return {
2807
+ route,
2808
+ deadline,
2809
+ resolveTeamAadGroupId,
2810
+ getTeamAadGroupId: () => teamAadGroupId
2811
+ };
2076
2812
  }
2077
- //#endregion
2078
- //#region extensions/msteams/src/monitor-handler/message-handler.ts
2079
- function extractTextFromHtmlAttachments(attachments) {
2080
- for (const attachment of attachments) {
2081
- const raw = require_resolve_allowlist.extractHtmlFromAttachment(attachment);
2082
- if (!raw) continue;
2083
- const text = raw.replace(/<at[^>]*>.*?<\/at>/gis, " ").replace(/<a\b[^>]*href=["']([^"']+)["'][^>]*>(.*?)<\/a>/gis, "$2 $1").replace(/<br\s*\/?>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<[^>]+>/g, " ").replace(/&nbsp;/gi, " ").replace(/&amp;/gi, "&").replace(/\s+/g, " ").trim();
2084
- if (text) return text;
2813
+ async function resolveMSTeamsThreadContext(params) {
2814
+ const core = require_runtime.getMSTeamsRuntime();
2815
+ const activity = params.context.activity;
2816
+ const { route, deadline } = params.routing;
2817
+ const teamAadGroupId = await params.routing.resolveTeamAadGroupId();
2818
+ let quoteBodyFull;
2819
+ let quoteSenderId;
2820
+ let quoteSenderName;
2821
+ const quoteMessageId = params.quoteInfo?.id;
2822
+ if (quoteMessageId && params.isDirectMessage && params.conversationId.startsWith("19:")) try {
2823
+ const graphToken = await require_resolve_allowlist.withMSTeamsRequestDeadline({
2824
+ deadline,
2825
+ label: "MS Teams quote token",
2826
+ work: () => params.tokenProvider.getAccessToken("https://graph.microsoft.com")
2827
+ });
2828
+ quoteBodyFull = await require_resolve_allowlist.withMSTeamsRequestDeadline({
2829
+ deadline,
2830
+ label: "MS Teams quote lookup",
2831
+ work: () => require_messenger.fetchChatMessageText(graphToken, params.conversationId, quoteMessageId, deadline)
2832
+ });
2833
+ } catch (err) {
2834
+ params.log.debug?.("failed to fetch full quoted message text", { error: require_setup_surface.formatUnknownError(err) });
2085
2835
  }
2086
- return "";
2087
- }
2088
- function formatMSTeamsSenderReason(params) {
2089
- switch (params.reasonCode) {
2090
- case "dm_policy_open": return "dmPolicy=open";
2091
- case "dm_policy_disabled": return "dmPolicy=disabled";
2092
- case "dm_policy_pairing_required": return "dmPolicy=pairing (not allowlisted)";
2093
- case "dm_policy_allowlisted": return `dmPolicy=${params.dmPolicy ?? "allowlist"} (allowlisted)`;
2094
- case "dm_policy_not_allowlisted": return `dmPolicy=${params.dmPolicy ?? "allowlist"} (not allowlisted)`;
2095
- case "group_policy_disabled": return "groupPolicy=disabled";
2096
- case "group_policy_empty_allowlist":
2097
- case "route_sender_empty": return "groupPolicy=allowlist (empty allowlist)";
2098
- case "group_policy_not_allowlisted": return "groupPolicy=allowlist (not allowlisted)";
2099
- case "group_policy_open": return "groupPolicy=open";
2100
- case "group_policy_allowed": return `groupPolicy=${params.groupPolicy ?? "allowlist"}`;
2101
- default: return params.reasonCode;
2836
+ let threadContext;
2837
+ const threadParentId = activity.replyToId;
2838
+ if (threadParentId && params.isChannel && teamAadGroupId) try {
2839
+ const graphToken = await require_resolve_allowlist.withMSTeamsRequestDeadline({
2840
+ deadline,
2841
+ label: "MS Teams thread token",
2842
+ work: () => params.tokenProvider.getAccessToken("https://graph.microsoft.com")
2843
+ });
2844
+ const [parentResult, repliesResult] = await require_resolve_allowlist.withMSTeamsRequestDeadline({
2845
+ deadline,
2846
+ label: "MS Teams thread history",
2847
+ work: () => Promise.allSettled([fetchParentMessageCached(graphToken, teamAadGroupId, params.conversationId, threadParentId, (token, groupId, requestedChannelId, messageId) => require_messenger.fetchChannelMessage(token, groupId, requestedChannelId, messageId, deadline)), require_messenger.fetchThreadReplies(graphToken, teamAadGroupId, params.conversationId, threadParentId, 50, deadline)])
2848
+ });
2849
+ const parentMsg = parentResult.status === "fulfilled" ? parentResult.value : void 0;
2850
+ const replies = repliesResult.status === "fulfilled" ? repliesResult.value : [];
2851
+ if (parentResult.status === "rejected") params.log.debug?.("failed to fetch parent message", { error: require_setup_surface.formatUnknownError(parentResult.reason) });
2852
+ if (repliesResult.status === "rejected") params.log.debug?.("failed to fetch thread replies", { error: require_setup_surface.formatUnknownError(repliesResult.reason) });
2853
+ const isThreadSenderAllowed = (message) => (0, openclaw_plugin_sdk_channel_inbound.resolveInboundSupplementalSenderAllowed)({
2854
+ isGroup: params.isChannel,
2855
+ groupPolicy: params.groupPolicy,
2856
+ allowFrom: params.effectiveGroupAllowFrom,
2857
+ isSenderAllowed: (allowFrom) => require_channel.resolveMSTeamsAllowlistMatch({
2858
+ allowFrom,
2859
+ senderId: message.from?.user?.id ?? "",
2860
+ senderName: message.from?.user?.displayName,
2861
+ allowNameMatching: params.allowNameMatching
2862
+ }).allowed
2863
+ });
2864
+ const parentSummary = summarizeParentMessage(parentMsg);
2865
+ const visibleParentMessages = parentMsg ? (0, openclaw_plugin_sdk_context_visibility_runtime.filterSupplementalContextItems)({
2866
+ items: [parentMsg],
2867
+ mode: params.contextVisibilityMode,
2868
+ kind: "thread",
2869
+ isSenderAllowed: isThreadSenderAllowed
2870
+ }).items : [];
2871
+ if (parentSummary && visibleParentMessages.length > 0 && shouldInjectParentContext(route.sessionKey, threadParentId)) {
2872
+ core.system.enqueueSystemEvent(formatParentContextEvent(parentSummary), {
2873
+ sessionKey: route.sessionKey,
2874
+ contextKey: `msteams:thread-parent:${params.conversationId}:${threadParentId}`
2875
+ });
2876
+ markParentContextInjected(route.sessionKey, threadParentId);
2877
+ }
2878
+ const allMessages = parentMsg ? [parentMsg, ...replies] : replies;
2879
+ quoteSenderId = parentMsg?.from?.user?.id ?? parentMsg?.from?.application?.id ?? void 0;
2880
+ quoteSenderName = parentMsg?.from?.user?.displayName ?? parentMsg?.from?.application?.displayName ?? params.quoteInfo?.sender;
2881
+ const { items: threadMessages } = (0, openclaw_plugin_sdk_context_visibility_runtime.filterSupplementalContextItems)({
2882
+ items: allMessages,
2883
+ mode: params.contextVisibilityMode,
2884
+ kind: "thread",
2885
+ isSenderAllowed: isThreadSenderAllowed
2886
+ });
2887
+ threadContext = require_messenger.formatThreadContext(threadMessages, activity.id) || void 0;
2888
+ } catch (err) {
2889
+ params.log.debug?.("failed to fetch thread history", { error: require_setup_surface.formatUnknownError(err) });
2102
2890
  }
2103
- }
2104
- function buildStoredConversationReference(params) {
2105
- const { activity, conversationId, conversationType, teamId, threadId } = params;
2106
- const from = activity.from;
2107
- const conversation = activity.conversation;
2108
- const agent = activity.recipient;
2109
- const clientInfo = activity.entities?.find((e) => e.type === "clientInfo");
2110
- const tenantId = activity.channelData?.tenant?.id ?? conversation?.tenantId;
2111
- const aadObjectId = from?.aadObjectId;
2112
- const serviceUrl = require_resolve_allowlist.tryNormalizeBotFrameworkServiceUrl(activity.serviceUrl);
2891
+ quoteSenderName ??= params.quoteInfo?.sender;
2113
2892
  return {
2114
- activityId: activity.id,
2115
- user: from ? {
2116
- id: from.id,
2117
- name: from.name,
2118
- aadObjectId: from.aadObjectId
2119
- } : void 0,
2120
- agent,
2121
- conversation: {
2122
- id: conversationId,
2123
- conversationType,
2124
- tenantId
2125
- },
2126
- ...tenantId ? { tenantId } : {},
2127
- ...aadObjectId ? { aadObjectId } : {},
2128
- teamId,
2129
- channelId: activity.channelId,
2130
- ...serviceUrl ? { serviceUrl } : {},
2131
- locale: activity.locale,
2132
- ...clientInfo?.timezone ? { timezone: clientInfo.timezone } : {},
2133
- ...threadId ? { threadId } : {}
2893
+ teamAadGroupId,
2894
+ quoteBodyFull,
2895
+ quoteSenderId,
2896
+ quoteSenderName,
2897
+ threadContext
2134
2898
  };
2135
2899
  }
2900
+ //#endregion
2901
+ //#region extensions/msteams/src/monitor-handler/message-handler.ts
2136
2902
  function createMSTeamsMessageHandler(deps) {
2137
2903
  const { cfg, runtime, appId, app, tokenProvider, textLimit, mediaMaxBytes, conversationStore, pollStore, log } = deps;
2138
2904
  const core = require_runtime.getMSTeamsRuntime();
@@ -2151,24 +2917,12 @@ function createMSTeamsMessageHandler(deps) {
2151
2917
  channel: "msteams"
2152
2918
  });
2153
2919
  const handleTeamsMessageNow = async (params) => {
2154
- const context = params.context;
2155
- const activity = context.activity;
2156
- const rawText = params.rawText;
2157
- const text = params.text;
2158
- const attachments = params.attachments;
2159
- const advertisedMedia = resolveMSTeamsAdvertisedMedia(attachments, {
2160
- maxInlineBytes: mediaMaxBytes,
2161
- maxInlineTotalBytes: mediaMaxBytes
2162
- });
2163
- const rawBody = text;
2920
+ const facts = assembleMSTeamsInboundFacts({
2921
+ entry: params,
2922
+ mediaMaxBytes
2923
+ });
2924
+ const { context, activity, rawText, text, attachments, advertisedMedia, rawBody, quoteInfo, from, conversation, attachmentTypes, htmlSummary, conversationId, conversationMessageId, conversationType, isChannel, teamId, conversationRef } = facts;
2164
2925
  const historyBody = [text, (0, openclaw_plugin_sdk_channel_inbound.formatMediaPlaceholderText)(advertisedMedia)].filter(Boolean).join("\n");
2165
- const quoteInfo = require_channel.extractMSTeamsQuoteInfo(attachments);
2166
- let quoteSenderId;
2167
- let quoteSenderName;
2168
- const from = activity.from;
2169
- const conversation = activity.conversation;
2170
- const attachmentTypes = attachments.map((att) => typeof att.contentType === "string" ? att.contentType : void 0).filter(Boolean).slice(0, 3);
2171
- const htmlSummary = summarizeMSTeamsHtmlAttachments(attachments);
2172
2926
  log.info("received message", {
2173
2927
  rawText: (0, openclaw_plugin_sdk_text_utility_runtime.truncateUtf16Safe)(rawText, 50),
2174
2928
  text: (0, openclaw_plugin_sdk_text_utility_runtime.truncateUtf16Safe)(text, 50),
@@ -2182,138 +2936,19 @@ function createMSTeamsMessageHandler(deps) {
2182
2936
  log.debug?.("skipping message without from.id");
2183
2937
  return;
2184
2938
  }
2185
- const rawConversationId = conversation?.id ?? "";
2186
- const conversationId = require_channel.normalizeMSTeamsConversationId(rawConversationId);
2187
- const conversationMessageId = require_channel.extractMSTeamsConversationMessageId(rawConversationId);
2188
- const conversationType = conversation?.conversationType ?? "personal";
2189
- const teamId = activity.channelData?.team?.id;
2190
- const graphChannelId = activity.channelData?.channel?.id?.trim() || conversationId;
2191
- const conversationRef = buildStoredConversationReference({
2192
- activity,
2193
- conversationId,
2194
- conversationType,
2195
- teamId,
2196
- threadId: conversationType === "channel" ? conversationMessageId ?? activity.replyToId ?? void 0 : void 0
2197
- });
2198
- const allowTextCommands = core.channel.commands.shouldHandleTextCommands({
2199
- cfg,
2200
- surface: "msteams"
2201
- });
2202
- const isControlCommand = allowTextCommands && core.channel.commands.isControlCommandMessage(text, cfg);
2203
- const { dmPolicy, senderId, senderName, pairing, isDirectMessage, channelGate, senderAccess, commandAccess, allowNameMatching, groupPolicy } = await resolveMSTeamsSenderAccess({
2939
+ const admission = await admitMSTeamsMessage({
2204
2940
  cfg,
2205
2941
  activity,
2206
- hasControlCommand: isControlCommand
2207
- });
2208
- const commandAuthorized = commandAccess.requested ? commandAccess.authorized : void 0;
2209
- const effectiveDmAllowFrom = senderAccess.effectiveAllowFrom;
2210
- const effectiveGroupAllowFrom = senderAccess.effectiveGroupAllowFrom;
2211
- const isChannel = conversationType === "channel";
2212
- if (isDirectMessage && msteamsCfg && senderAccess.decision !== "allow") {
2213
- if (senderAccess.reasonCode === "dm_policy_disabled") {
2214
- log.info("dropping dm (dms disabled)", {
2215
- sender: senderId,
2216
- label: senderName
2217
- });
2218
- log.debug?.("dropping dm (dms disabled)");
2219
- return;
2220
- }
2221
- const allowMatch = require_channel.resolveMSTeamsAllowlistMatch({
2222
- allowFrom: effectiveDmAllowFrom,
2223
- senderId,
2224
- senderName,
2225
- allowNameMatching
2226
- });
2227
- if (senderAccess.decision === "pairing") {
2228
- conversationStore.upsert(conversationId, conversationRef).catch((err) => {
2229
- log.debug?.("failed to save conversation reference", { error: require_setup_surface.formatUnknownError(err) });
2230
- });
2231
- if (await pairing.upsertPairingRequest({
2232
- id: senderId,
2233
- meta: { name: senderName }
2234
- })) log.info("msteams pairing request created", {
2235
- sender: senderId,
2236
- label: senderName
2237
- });
2238
- }
2239
- log.debug?.("dropping dm (not allowlisted)", {
2240
- sender: senderId,
2241
- label: senderName,
2242
- allowlistMatch: (0, openclaw_plugin_sdk_allow_from.formatAllowlistMatchMeta)(allowMatch)
2243
- });
2244
- log.info("dropping dm (not allowlisted)", {
2245
- sender: senderId,
2246
- label: senderName,
2247
- dmPolicy,
2248
- reason: formatMSTeamsSenderReason({
2249
- reasonCode: senderAccess.reasonCode,
2250
- dmPolicy,
2251
- groupPolicy
2252
- }),
2253
- allowlistMatch: (0, openclaw_plugin_sdk_allow_from.formatAllowlistMatchMeta)(allowMatch)
2254
- });
2255
- return;
2256
- }
2257
- if (!isDirectMessage && msteamsCfg) {
2258
- if (channelGate.allowlistConfigured && !channelGate.allowed) {
2259
- log.info("dropping group message (not in team/channel allowlist)", {
2260
- conversationId,
2261
- teamKey: channelGate.teamKey ?? "none",
2262
- channelKey: channelGate.channelKey ?? "none",
2263
- channelMatchKey: channelGate.channelMatchKey ?? "none",
2264
- channelMatchSource: channelGate.channelMatchSource ?? "none"
2265
- });
2266
- log.debug?.("dropping group message (not in team/channel allowlist)", {
2267
- conversationId,
2268
- teamKey: channelGate.teamKey ?? "none",
2269
- channelKey: channelGate.channelKey ?? "none",
2270
- channelMatchKey: channelGate.channelMatchKey ?? "none",
2271
- channelMatchSource: channelGate.channelMatchSource ?? "none"
2272
- });
2273
- return;
2274
- }
2275
- if (!senderAccess.allowed && senderAccess.reasonCode === "group_policy_disabled") {
2276
- log.info("dropping group message (groupPolicy: disabled)", { conversationId });
2277
- log.debug?.("dropping group message (groupPolicy: disabled)", { conversationId });
2278
- return;
2279
- }
2280
- if (!senderAccess.allowed && (senderAccess.reasonCode === "group_policy_empty_allowlist" || senderAccess.reasonCode === "route_sender_empty")) {
2281
- log.info("dropping group message (groupPolicy: allowlist, no allowlist)", { conversationId });
2282
- log.debug?.("dropping group message (groupPolicy: allowlist, no allowlist)", { conversationId });
2283
- return;
2284
- }
2285
- if (!senderAccess.allowed && senderAccess.reasonCode === "group_policy_not_allowlisted") {
2286
- const allowMatch = require_channel.resolveMSTeamsAllowlistMatch({
2287
- allowFrom: effectiveGroupAllowFrom,
2288
- senderId,
2289
- senderName,
2290
- allowNameMatching
2291
- });
2292
- log.debug?.("dropping group message (not in groupAllowFrom)", {
2293
- sender: senderId,
2294
- label: senderName,
2295
- allowlistMatch: (0, openclaw_plugin_sdk_allow_from.formatAllowlistMatchMeta)(allowMatch)
2296
- });
2297
- log.info("dropping group message (not in groupAllowFrom)", {
2298
- sender: senderId,
2299
- label: senderName,
2300
- allowlistMatch: (0, openclaw_plugin_sdk_allow_from.formatAllowlistMatchMeta)(allowMatch)
2301
- });
2302
- return;
2303
- }
2304
- }
2305
- if (commandAccess.shouldBlockControlCommand) {
2306
- (0, openclaw_plugin_sdk_channel_inbound.logInboundDrop)({
2307
- log: logVerboseMessage,
2308
- channel: "msteams",
2309
- reason: "control command (unauthorized)",
2310
- target: senderId
2311
- });
2312
- return;
2313
- }
2314
- conversationStore.upsert(conversationId, conversationRef).catch((err) => {
2315
- log.debug?.("failed to save conversation reference", { error: require_setup_surface.formatUnknownError(err) });
2942
+ text,
2943
+ conversationId,
2944
+ conversationRef,
2945
+ isChannel,
2946
+ conversationStore,
2947
+ log,
2948
+ logVerboseMessage
2316
2949
  });
2950
+ if (!admission) return;
2951
+ const { senderId, senderName, isDirectMessage, channelGate, allowNameMatching, groupPolicy, commandAuthorized, effectiveGroupAllowFrom, allowTextCommands, isControlCommand } = admission;
2317
2952
  const pollVote = require_polls.extractMSTeamsPollVote(activity);
2318
2953
  if (pollVote) {
2319
2954
  try {
@@ -2335,33 +2970,18 @@ function createMSTeamsMessageHandler(deps) {
2335
2970
  }
2336
2971
  return;
2337
2972
  }
2338
- const mayRecoverGraphMedia = Boolean(htmlSummary?.attachmentIds.length) || shouldAttemptMSTeamsGraphMediaFallback({
2339
- conversationType,
2340
- htmlSummary: htmlSummary ?? void 0,
2341
- graphMediaFallback: msteamsCfg?.graphMediaFallback
2342
- });
2343
- if (!rawBody && advertisedMedia.length === 0 && !mayRecoverGraphMedia) {
2344
- log.debug?.("skipping empty message after stripping mentions");
2345
- return;
2346
- }
2347
- const teamsFrom = isDirectMessage ? `msteams:${senderId}` : isChannel ? `msteams:channel:${conversationId}` : `msteams:group:${conversationId}`;
2348
- const teamsTo = isDirectMessage ? `user:${senderId}` : `conversation:${conversationId}`;
2349
- const route = core.channel.routing.resolveAgentRoute({
2973
+ const threadRouting = prepareMSTeamsThreadRouting({
2350
2974
  cfg,
2351
- channel: "msteams",
2352
- teamId,
2353
- peer: {
2354
- kind: isDirectMessage ? "direct" : isChannel ? "channel" : "group",
2355
- id: isDirectMessage ? senderId : conversationId
2356
- }
2357
- });
2358
- route.sessionKey = require_channel.resolveMSTeamsRouteSessionKey({
2359
- baseSessionKey: route.sessionKey,
2975
+ context,
2976
+ isDirectMessage,
2360
2977
  isChannel,
2361
- conversationMessageId,
2362
- replyToId: activity.replyToId
2978
+ senderId,
2979
+ conversationId,
2980
+ conversationMessageId: conversationMessageId ?? void 0,
2981
+ teamId,
2982
+ log
2363
2983
  });
2364
- const preview = (0, openclaw_plugin_sdk_text_utility_runtime.sliceUtf16Safe)(rawBody.replace(/\s+/g, " "), 0, 160);
2984
+ const { route, deadline: preprocessingDeadline } = threadRouting;
2365
2985
  const inboundLabel = isDirectMessage ? `Teams DM from ${senderName}` : `Teams message in ${conversationType} from ${senderName}`;
2366
2986
  const enqueuePrimaryMessageSystemEvent = () => core.system.enqueueSystemEvent(inboundLabel, {
2367
2987
  sessionKey: route.sessionKey,
@@ -2375,7 +2995,7 @@ function createMSTeamsMessageHandler(deps) {
2375
2995
  teamConfig,
2376
2996
  channelConfig
2377
2997
  });
2378
- const timestamp = require_channel.parseMSTeamsActivityTimestamp(activity.timestamp);
2998
+ const timestamp = require_resolve_allowlist.parseMSTeamsActivityTimestamp(activity.timestamp);
2379
2999
  const mentionDecision = (0, openclaw_plugin_sdk_channel_inbound.resolveInboundMentionDecision)({
2380
3000
  facts: {
2381
3001
  canDetectMention: true,
@@ -2415,344 +3035,66 @@ function createMSTeamsMessageHandler(deps) {
2415
3035
  return;
2416
3036
  }
2417
3037
  }
2418
- const preprocessingDeadline = require_resolve_allowlist.createMSTeamsInboundDeadline();
2419
- let teamAadGroupId = activity.channelData?.team?.aadGroupId?.trim() || void 0;
2420
- const conversationTeamId = isChannel ? teamId : void 0;
2421
- let teamGroupIdPromise;
2422
- const resolveChannelTeamGroupId = async () => {
2423
- if (!conversationTeamId) return;
2424
- teamGroupIdPromise ??= resolveTeamGroupId({
2425
- conversationTeamId,
2426
- aadGroupId: teamAadGroupId,
2427
- getTeamDetails: context.getTeamDetails,
2428
- deadline: preprocessingDeadline
2429
- }).catch((err) => {
2430
- log.debug?.("failed to resolve Teams AAD group ID", {
2431
- teamId: conversationTeamId,
2432
- error: require_setup_surface.formatUnknownError(err)
2433
- });
2434
- });
2435
- teamAadGroupId = await teamGroupIdPromise;
2436
- return teamAadGroupId;
2437
- };
2438
- let mediaList = [];
2439
- try {
2440
- mediaList = await require_resolve_allowlist.withMSTeamsRequestDeadline({
2441
- deadline: preprocessingDeadline,
2442
- label: "MS Teams inbound media",
2443
- work: () => resolveMSTeamsInboundMedia({
2444
- attachments,
2445
- htmlSummary: htmlSummary ?? void 0,
2446
- maxBytes: mediaMaxBytes,
2447
- tokenProvider,
2448
- allowHosts: msteamsCfg?.mediaAllowHosts,
2449
- authAllowHosts: msteamsCfg?.mediaAuthAllowHosts,
2450
- graphMediaFallback: msteamsCfg?.graphMediaFallback,
2451
- conversationType,
2452
- conversationId,
2453
- conversationMessageId: conversationMessageId ?? void 0,
2454
- teamAadGroupId,
2455
- resolveTeamAadGroupId: resolveChannelTeamGroupId,
2456
- serviceUrl: activity.serviceUrl,
2457
- activity: {
2458
- id: activity.id,
2459
- replyToId: activity.replyToId,
2460
- channelData: activity.channelData
2461
- },
2462
- log,
2463
- deadline: preprocessingDeadline,
2464
- preserveFilenames: false
2465
- })
2466
- });
2467
- } catch (err) {
2468
- log.debug?.("failed to resolve inbound Teams media", { error: require_setup_surface.formatUnknownError(err) });
2469
- }
2470
- const inboundMedia = mergeMSTeamsMediaFacts(advertisedMedia, mediaList);
2471
- const agentBody = resolveMSTeamsInboundMediaBody({
2472
- body: rawBody,
2473
- nativeMedia: [...advertisedMedia, ...mediaList.slice(advertisedMedia.length).map((media) => ({
2474
- contentType: media.contentType,
2475
- kind: media.kind
2476
- }))],
2477
- materializedMedia: inboundMedia
2478
- });
2479
- if (!agentBody && inboundMedia.length === 0) {
2480
- log.debug?.("skipping empty message after Graph media recovery");
2481
- return;
2482
- }
2483
- enqueuePrimaryMessageSystemEvent();
2484
- teamAadGroupId = await resolveChannelTeamGroupId();
2485
- let quoteBodyFull;
2486
- const quoteMessageId = quoteInfo?.id;
2487
- if (quoteMessageId && isDirectMessage && conversationId.startsWith("19:")) try {
2488
- const graphToken = await require_resolve_allowlist.withMSTeamsRequestDeadline({
2489
- deadline: preprocessingDeadline,
2490
- label: "MS Teams quote token",
2491
- work: () => tokenProvider.getAccessToken("https://graph.microsoft.com")
2492
- });
2493
- quoteBodyFull = await require_resolve_allowlist.withMSTeamsRequestDeadline({
2494
- deadline: preprocessingDeadline,
2495
- label: "MS Teams quote lookup",
2496
- work: () => require_messenger.fetchChatMessageText(graphToken, conversationId, quoteMessageId, preprocessingDeadline)
2497
- });
2498
- } catch (err) {
2499
- log.debug?.("failed to fetch full quoted message text", { error: require_setup_surface.formatUnknownError(err) });
2500
- }
2501
- let threadContext;
2502
- const threadParentId = activity.replyToId;
2503
- const channelGroupId = teamAadGroupId;
2504
- if (threadParentId && isChannel && channelGroupId) try {
2505
- const graphToken = await require_resolve_allowlist.withMSTeamsRequestDeadline({
2506
- deadline: preprocessingDeadline,
2507
- label: "MS Teams thread token",
2508
- work: () => tokenProvider.getAccessToken("https://graph.microsoft.com")
2509
- });
2510
- const [parentResult, repliesResult] = await require_resolve_allowlist.withMSTeamsRequestDeadline({
2511
- deadline: preprocessingDeadline,
2512
- label: "MS Teams thread history",
2513
- work: () => Promise.allSettled([fetchParentMessageCached(graphToken, channelGroupId, conversationId, threadParentId, (token, groupId, requestedChannelId, messageId) => require_messenger.fetchChannelMessage(token, groupId, requestedChannelId, messageId, preprocessingDeadline)), require_messenger.fetchThreadReplies(graphToken, channelGroupId, conversationId, threadParentId, 50, preprocessingDeadline)])
2514
- });
2515
- const parentMsg = parentResult.status === "fulfilled" ? parentResult.value : void 0;
2516
- const replies = repliesResult.status === "fulfilled" ? repliesResult.value : [];
2517
- if (parentResult.status === "rejected") log.debug?.("failed to fetch parent message", { error: require_setup_surface.formatUnknownError(parentResult.reason) });
2518
- if (repliesResult.status === "rejected") log.debug?.("failed to fetch thread replies", { error: require_setup_surface.formatUnknownError(repliesResult.reason) });
2519
- const isThreadSenderAllowed = (msg) => (0, openclaw_plugin_sdk_channel_inbound.resolveInboundSupplementalSenderAllowed)({
2520
- isGroup: isChannel,
2521
- groupPolicy,
2522
- allowFrom: effectiveGroupAllowFrom,
2523
- isSenderAllowed: (allowFrom) => require_channel.resolveMSTeamsAllowlistMatch({
2524
- allowFrom,
2525
- senderId: msg.from?.user?.id ?? "",
2526
- senderName: msg.from?.user?.displayName,
2527
- allowNameMatching
2528
- }).allowed
2529
- });
2530
- const parentSummary = summarizeParentMessage(parentMsg);
2531
- const visibleParentMessages = parentMsg ? (0, openclaw_plugin_sdk_context_visibility_runtime.filterSupplementalContextItems)({
2532
- items: [parentMsg],
2533
- mode: contextVisibilityMode,
2534
- kind: "thread",
2535
- isSenderAllowed: isThreadSenderAllowed
2536
- }).items : [];
2537
- if (parentSummary && visibleParentMessages.length > 0 && shouldInjectParentContext(route.sessionKey, threadParentId)) {
2538
- core.system.enqueueSystemEvent(formatParentContextEvent(parentSummary), {
2539
- sessionKey: route.sessionKey,
2540
- contextKey: `msteams:thread-parent:${conversationId}:${threadParentId}`
2541
- });
2542
- markParentContextInjected(route.sessionKey, threadParentId);
2543
- }
2544
- const allMessages = parentMsg ? [parentMsg, ...replies] : replies;
2545
- quoteSenderId = parentMsg?.from?.user?.id ?? parentMsg?.from?.application?.id ?? void 0;
2546
- quoteSenderName = parentMsg?.from?.user?.displayName ?? parentMsg?.from?.application?.displayName ?? quoteInfo?.sender;
2547
- const { items: threadMessages } = (0, openclaw_plugin_sdk_context_visibility_runtime.filterSupplementalContextItems)({
2548
- items: allMessages,
2549
- mode: contextVisibilityMode,
2550
- kind: "thread",
2551
- isSenderAllowed: isThreadSenderAllowed
2552
- });
2553
- const formatted = require_messenger.formatThreadContext(threadMessages, activity.id);
2554
- if (formatted) threadContext = formatted;
2555
- } catch (err) {
2556
- log.debug?.("failed to fetch thread history", { error: require_setup_surface.formatUnknownError(err) });
2557
- }
2558
- quoteSenderName ??= quoteInfo?.sender;
2559
- const envelopeFrom = isDirectMessage ? senderName : conversationType;
2560
- const buildEnvelope = (0, openclaw_plugin_sdk_channel_inbound.createChannelInboundEnvelopeBuilder)({
2561
- cfg,
2562
- route
2563
- });
2564
- let combinedBody = buildEnvelope({
2565
- channel: "Teams",
2566
- from: envelopeFrom,
2567
- timestamp,
2568
- body: agentBody
2569
- });
2570
- const isRoomish = !isDirectMessage;
2571
- const historyKey = isRoomish ? conversationId : void 0;
2572
- if (isRoomish && historyKey) combinedBody = (0, openclaw_plugin_sdk_reply_history.createChannelHistoryWindow)({ historyMap: conversationHistories }).buildPendingContext({
2573
- historyKey,
2574
- limit: historyLimit,
2575
- currentMessage: combinedBody,
2576
- formatEntry: (entry) => buildEnvelope({
2577
- channel: "Teams",
2578
- from: conversationType,
2579
- timestamp: entry.timestamp,
2580
- previousTimestamp: null,
2581
- body: `${entry.sender}: ${entry.body}${entry.messageId ? ` [id:${entry.messageId}]` : ""}`
2582
- })
3038
+ const content = await prepareMSTeamsInboundContent({
3039
+ entry: params,
3040
+ rawBody,
3041
+ advertisedMedia,
3042
+ htmlSummary: htmlSummary ?? void 0,
3043
+ conversationType,
3044
+ conversationId,
3045
+ conversationMessageId: conversationMessageId ?? void 0,
3046
+ teamAadGroupId: threadRouting.getTeamAadGroupId(),
3047
+ resolveTeamAadGroupId: threadRouting.resolveTeamAadGroupId,
3048
+ mediaMaxBytes,
3049
+ tokenProvider,
3050
+ mediaAllowHosts: msteamsCfg?.mediaAllowHosts,
3051
+ mediaAuthAllowHosts: msteamsCfg?.mediaAuthAllowHosts,
3052
+ graphMediaFallback: msteamsCfg?.graphMediaFallback,
3053
+ deadline: preprocessingDeadline,
3054
+ log
2583
3055
  });
2584
- const inboundHistory = isRoomish && historyKey && historyLimit > 0 ? (0, openclaw_plugin_sdk_reply_history.createChannelHistoryWindow)({ historyMap: conversationHistories }).buildInboundHistory({
2585
- historyKey,
2586
- limit: historyLimit
2587
- }) : void 0;
2588
- const commandBody = text.trim();
2589
- const quoteSenderAllowed = quoteInfo && quoteInfo.sender ? (0, openclaw_plugin_sdk_channel_inbound.resolveInboundSupplementalSenderAllowed)({
2590
- isGroup: !isDirectMessage,
3056
+ if (!content) return;
3057
+ enqueuePrimaryMessageSystemEvent();
3058
+ const thread = await resolveMSTeamsThreadContext({
3059
+ routing: threadRouting,
3060
+ context,
3061
+ tokenProvider,
3062
+ quoteInfo,
3063
+ isDirectMessage,
3064
+ isChannel,
3065
+ conversationId,
3066
+ contextVisibilityMode,
2591
3067
  groupPolicy,
2592
- allowFrom: effectiveGroupAllowFrom,
2593
- isSenderAllowed: (allowFrom) => require_channel.resolveMSTeamsAllowlistMatch({
2594
- allowFrom,
2595
- senderId: quoteSenderId ?? "",
2596
- senderName: quoteSenderName,
2597
- allowNameMatching
2598
- }).allowed
2599
- }) : true;
2600
- const bodyForAgent = threadContext ? `[Thread history]\n${threadContext}\n[/Thread history]\n\n${agentBody}` : agentBody;
2601
- const nativeChannelId = isChannel && teamAadGroupId ? `${teamAadGroupId}/${graphChannelId}` : void 0;
2602
- const ctxPayload = (0, openclaw_plugin_sdk_channel_inbound.buildChannelInboundEventContext)({
2603
- channel: "msteams",
2604
- contextVisibility: contextVisibilityMode,
2605
- supplemental: { quote: quoteInfo ? {
2606
- id: quoteInfo.id ?? activity.replyToId ?? void 0,
2607
- body: quoteBodyFull ?? quoteInfo.body,
2608
- sender: quoteInfo.sender,
2609
- senderAllowed: quoteSenderAllowed,
2610
- isQuote: true
2611
- } : void 0 },
2612
- media: (0, openclaw_plugin_sdk_channel_inbound.toInboundMediaFacts)(inboundMedia),
2613
- messageId: activity.id,
2614
- timestamp: timestamp?.getTime() ?? Date.now(),
2615
- from: teamsFrom,
2616
- sender: {
2617
- id: senderId,
2618
- name: senderName
2619
- },
2620
- conversation: {
2621
- kind: isDirectMessage ? "direct" : isChannel ? "channel" : "group",
2622
- id: conversationId,
2623
- label: envelopeFrom,
2624
- spaceId: teamId,
2625
- nativeChannelId
2626
- },
2627
- route: {
2628
- agentId: route.agentId,
2629
- dmScope: route.dmScope,
2630
- accountId: route.accountId,
2631
- routeSessionKey: route.sessionKey
2632
- },
2633
- reply: {
2634
- to: teamsTo,
2635
- replyToId: activity.replyToId ?? void 0,
2636
- nativeChannelId
2637
- },
2638
- message: {
2639
- body: combinedBody,
2640
- bodyForAgent,
2641
- inboundHistory,
2642
- rawBody,
2643
- commandBody
2644
- },
2645
- sessionTranscript: { historyLimit: isRoomish ? historyLimit : 0 },
2646
- access: {
2647
- mentions: {
2648
- canDetectMention: !isDirectMessage,
2649
- wasMentioned: isDirectMessage || mentionDecision.effectiveWasMentioned
2650
- },
2651
- commands: { authorized: commandAuthorized === true }
2652
- },
2653
- extra: {
2654
- GroupSubject: !isDirectMessage ? conversationType : void 0,
2655
- ReplyToIsQuote: quoteInfo ? true : void 0
2656
- }
3068
+ effectiveGroupAllowFrom,
3069
+ allowNameMatching,
3070
+ log
2657
3071
  });
2658
- logVerboseMessage(`msteams inbound: from=${ctxPayload.From} preview="${preview}"`);
2659
- const sharePointSiteId = msteamsCfg?.sharePointSiteId;
2660
- const { dispatcherOptions, delivery, replyOptions } = createMSTeamsReplyDispatcher({
3072
+ await dispatchMSTeamsInboundTurn({
2661
3073
  cfg,
2662
- agentId: route.agentId,
2663
- sessionKey: route.sessionKey,
2664
- accountId: route.accountId,
2665
3074
  runtime,
2666
- log,
2667
- app,
2668
3075
  appId,
2669
- conversationRef,
2670
- context,
2671
- replyStyle,
2672
- textLimit,
2673
- onSentMessageIds: (ids) => {
2674
- for (const id of ids) recordMSTeamsSentMessage(conversationId, id);
2675
- },
3076
+ app,
2676
3077
  tokenProvider,
2677
- sharePointSiteId
2678
- });
2679
- const senderTimezone = (activity.entities?.find((e) => e.type === "clientInfo"))?.timezone || conversationRef.timezone;
2680
- const turnConfig = senderTimezone && !cfg.agents?.defaults?.userTimezone ? {
2681
- ...cfg,
2682
- agents: {
2683
- ...cfg.agents,
2684
- defaults: {
2685
- ...cfg.agents?.defaults,
2686
- userTimezone: senderTimezone
2687
- }
2688
- }
2689
- } : cfg;
2690
- log.info("dispatching to agent", { sessionKey: route.sessionKey });
2691
- try {
2692
- const turnResult = await core.channel.inbound.run({
2693
- channel: "msteams",
2694
- accountId: route.accountId,
2695
- raw: context,
2696
- adapter: {
2697
- ingest: () => ({
2698
- id: activity.id ?? `${teamsFrom}:${Date.now()}`,
2699
- timestamp: timestamp?.getTime(),
2700
- rawText: rawBody,
2701
- textForAgent: bodyForAgent,
2702
- textForCommands: commandBody,
2703
- raw: activity
2704
- }),
2705
- resolveTurn: () => ({
2706
- cfg: turnConfig,
2707
- channel: "msteams",
2708
- accountId: route.accountId,
2709
- route: {
2710
- agentId: route.agentId,
2711
- sessionKey: route.sessionKey
2712
- },
2713
- ctxPayload,
2714
- record: { onRecordError: (err) => {
2715
- logVerboseMessage(`msteams: failed updating session meta: ${require_setup_surface.formatUnknownError(err)}`);
2716
- } },
2717
- history: {
2718
- isGroup: isRoomish,
2719
- historyKey,
2720
- historyMap: conversationHistories,
2721
- limit: historyLimit
2722
- },
2723
- dispatcherOptions,
2724
- delivery,
2725
- replyOptions: {
2726
- ...replyOptions,
2727
- ...params.turnAdoptionLifecycle ? (0, openclaw_plugin_sdk_channel_outbound.bindIngressLifecycleToReplyOptions)(params.turnAdoptionLifecycle) : {}
2728
- }
2729
- })
2730
- }
2731
- });
2732
- const dispatchResult = turnResult.dispatched ? turnResult.dispatchResult : void 0;
2733
- const queuedFinal = dispatchResult?.queuedFinal ?? false;
2734
- const counts = (0, openclaw_plugin_sdk_channel_inbound.resolveInboundReplyDispatchCounts)(dispatchResult);
2735
- const hasFinalResponse = (0, openclaw_plugin_sdk_channel_inbound.hasFinalInboundReplyDispatch)(dispatchResult);
2736
- log.info("dispatch complete", {
2737
- queuedFinal,
2738
- counts
2739
- });
2740
- if (!hasFinalResponse) return;
2741
- const finalCount = counts.final;
2742
- logVerboseMessage(`msteams: delivered ${finalCount} reply${finalCount === 1 ? "" : "ies"} to ${teamsTo}`);
2743
- } catch (err) {
2744
- log.error("dispatch failed", { error: require_setup_surface.formatUnknownError(err) });
2745
- runtime.error(`msteams dispatch failed: ${require_setup_surface.formatUnknownError(err)}`);
2746
- if (params.turnAdoptionLifecycle) throw err;
2747
- try {
2748
- await context.sendActivity("⚠️ Something went wrong. Please try again.");
2749
- } catch {}
2750
- }
3078
+ textLimit,
3079
+ log,
3080
+ logVerboseMessage,
3081
+ facts,
3082
+ admission,
3083
+ content,
3084
+ routing: threadRouting,
3085
+ thread,
3086
+ replyStyle,
3087
+ timestamp,
3088
+ contextVisibilityMode,
3089
+ mentionWasEffective: mentionDecision.effectiveWasMentioned,
3090
+ conversationHistories,
3091
+ historyLimit
3092
+ });
2751
3093
  };
2752
3094
  const inboundDebouncer = core.channel.debounce.createInboundDebouncer({
2753
3095
  debounceMs: inboundDebounceMs,
2754
3096
  buildKey: (entry) => {
2755
- const conversationId = require_channel.normalizeMSTeamsConversationId(entry.context.activity.conversation?.id ?? "");
3097
+ const conversationId = require_resolve_allowlist.normalizeMSTeamsConversationId(entry.context.activity.conversation?.id ?? "");
2756
3098
  const senderId = entry.context.activity.from?.aadObjectId ?? entry.context.activity.from?.id ?? "";
2757
3099
  if (!senderId || !conversationId) return null;
2758
3100
  return `msteams:${appId}:${conversationId}:${senderId}`;
@@ -2762,65 +3104,53 @@ function createMSTeamsMessageHandler(deps) {
2762
3104
  if (entry.attachments.length > 0) return false;
2763
3105
  return !core.channel.commands.isControlCommandMessage(entry.text, cfg);
2764
3106
  },
2765
- onFlush: async (entries) => {
3107
+ onFlush: (entries, createFlush) => {
2766
3108
  const last = entries.at(-1);
2767
- if (!last) return;
2768
3109
  const { lifecycle, settle } = (0, openclaw_plugin_sdk_channel_ingress_runtime.fanInChannelIngressLifecycles)(entries.map((entry) => entry.turnAdoptionLifecycle));
2769
- try {
2770
- if (entries.length === 1) await handleTeamsMessageNow(lifecycle ? {
2771
- ...last,
2772
- turnAdoptionLifecycle: lifecycle
2773
- } : last);
2774
- else {
2775
- const combinedText = entries.map((entry) => entry.text).filter(Boolean).join("\n");
2776
- if (combinedText.trim()) {
2777
- const combinedRawText = entries.map((entry) => entry.rawText).filter(Boolean).join("\n");
2778
- const wasMentioned = entries.some((entry) => entry.wasMentioned);
2779
- const implicitMentionKinds = entries.flatMap((entry) => entry.implicitMentionKinds);
2780
- await handleTeamsMessageNow({
2781
- context: last.context,
2782
- rawText: combinedRawText,
2783
- text: combinedText,
2784
- attachments: [],
2785
- wasMentioned,
2786
- implicitMentionKinds,
2787
- ...lifecycle ? { turnAdoptionLifecycle: lifecycle } : {}
3110
+ return createFlush({
3111
+ lifecycle,
3112
+ dispatch: async (admissionLifecycle) => {
3113
+ if (!last) return;
3114
+ try {
3115
+ if (entries.length === 1) await handleTeamsMessageNow({
3116
+ ...last,
3117
+ turnAdoptionLifecycle: admissionLifecycle
2788
3118
  });
3119
+ else {
3120
+ const combinedText = entries.map((entry) => entry.text).filter(Boolean).join("\n");
3121
+ if (combinedText.trim()) {
3122
+ const combinedRawText = entries.map((entry) => entry.rawText).filter(Boolean).join("\n");
3123
+ const wasMentioned = entries.some((entry) => entry.wasMentioned);
3124
+ const implicitMentionKinds = entries.flatMap((entry) => entry.implicitMentionKinds);
3125
+ await handleTeamsMessageNow({
3126
+ context: last.context,
3127
+ rawText: combinedRawText,
3128
+ text: combinedText,
3129
+ attachments: [],
3130
+ wasMentioned,
3131
+ implicitMentionKinds,
3132
+ turnAdoptionLifecycle: admissionLifecycle
3133
+ });
3134
+ }
3135
+ }
3136
+ await settle();
3137
+ } catch (err) {
3138
+ await admissionLifecycle.onAbandoned();
3139
+ throw err;
2789
3140
  }
2790
3141
  }
2791
- await settle();
2792
- } catch (err) {
2793
- await lifecycle?.onAbandoned();
2794
- throw err;
2795
- }
3142
+ });
2796
3143
  },
2797
3144
  onError: (err) => {
2798
3145
  runtime.error(`msteams debounce flush failed: ${require_setup_surface.formatUnknownError(err)}`);
2799
3146
  }
2800
3147
  });
2801
3148
  return async function handleTeamsMessage(context, turnAdoptionLifecycle) {
2802
- const activity = context.activity;
2803
- const attachments = Array.isArray(activity.attachments) ? activity.attachments : [];
2804
- const rawText = activity.text?.trim() ?? "";
2805
- const htmlText = extractTextFromHtmlAttachments(attachments);
2806
- const valueText = rawText || htmlText ? "" : serializeMSTeamsAdaptiveCardActionValue(activity.value);
2807
- const text = require_channel.stripMSTeamsMentionTags(rawText || htmlText || valueText || "");
2808
- const wasMentioned = require_channel.wasMSTeamsBotMentioned(activity);
2809
- const conversationId = require_channel.normalizeMSTeamsConversationId(activity.conversation?.id ?? "");
2810
- const replyToId = activity.replyToId ?? void 0;
2811
- const implicitMentionKinds = conversationId && replyToId && await wasMSTeamsMessageSentWithPersistence({
2812
- conversationId,
2813
- messageId: replyToId
2814
- }) ? ["reply_to_bot"] : [];
2815
- await inboundDebouncer.enqueue({
3149
+ const entry = await prepareMSTeamsDebounceEntry({
2816
3150
  context,
2817
- rawText,
2818
- text,
2819
- attachments,
2820
- wasMentioned,
2821
- implicitMentionKinds,
2822
3151
  turnAdoptionLifecycle
2823
3152
  });
3153
+ await inboundDebouncer.enqueue(entry);
2824
3154
  if (turnAdoptionLifecycle) return { kind: "deferred" };
2825
3155
  };
2826
3156
  }
@@ -2846,7 +3176,7 @@ function createMSTeamsReactionHandler(deps) {
2846
3176
  log.debug?.("reaction activity missing from.id; skipping");
2847
3177
  return;
2848
3178
  }
2849
- const conversationId = require_channel.normalizeMSTeamsConversationId(activity.conversation?.id ?? "");
3179
+ const conversationId = require_resolve_allowlist.normalizeMSTeamsConversationId(activity.conversation?.id ?? "");
2850
3180
  const conversationType = activity.conversation?.conversationType ?? "personal";
2851
3181
  const isGroupChat = conversationType === "groupChat" || activity.conversation?.isGroup === true;
2852
3182
  const isChannel = conversationType === "channel";
@@ -3142,7 +3472,7 @@ async function runMSTeamsFeedbackInvokeHandler(context, deps) {
3142
3472
  userComment = JSON.parse(value.actionValue.feedback).feedbackText || void 0;
3143
3473
  } catch {}
3144
3474
  const rawConversationId = activity.conversation?.id ?? "unknown";
3145
- const conversationId = require_channel.normalizeMSTeamsConversationId(rawConversationId);
3475
+ const conversationId = require_resolve_allowlist.normalizeMSTeamsConversationId(rawConversationId);
3146
3476
  const senderId = activity.from?.aadObjectId ?? activity.from?.id ?? "unknown";
3147
3477
  const messageId = value.replyToId ?? activity.replyToId ?? "unknown";
3148
3478
  const isNegative = reaction === "dislike";
@@ -3157,7 +3487,7 @@ async function runMSTeamsFeedbackInvokeHandler(context, deps) {
3157
3487
  id: isDirectMessage ? senderId : conversationId
3158
3488
  }
3159
3489
  });
3160
- const feedbackThreadId = isChannel ? require_channel.extractMSTeamsConversationMessageId(rawConversationId) ?? activity.replyToId ?? void 0 : void 0;
3490
+ const feedbackThreadId = isChannel ? require_resolve_allowlist.extractMSTeamsConversationMessageId(rawConversationId) ?? activity.replyToId ?? void 0 : void 0;
3161
3491
  if (feedbackThreadId) route.sessionKey = (0, openclaw_plugin_sdk_routing.resolveThreadSessionKeys)({
3162
3492
  baseSessionKey: route.sessionKey,
3163
3493
  threadId: feedbackThreadId,
@@ -3239,8 +3569,8 @@ async function handleMSTeamsFileConsentInvoke(context, log) {
3239
3569
  const fsFile = inMemoryFile ? void 0 : await require_messenger.getPendingUploadFs(uploadId);
3240
3570
  const pendingFile = inMemoryFile ?? fsFile;
3241
3571
  if (pendingFile) {
3242
- const pendingConversationId = require_channel.normalizeMSTeamsConversationId(pendingFile.conversationId);
3243
- const invokeConversationId = require_channel.normalizeMSTeamsConversationId(activity.conversation?.id ?? "");
3572
+ const pendingConversationId = require_resolve_allowlist.normalizeMSTeamsConversationId(pendingFile.conversationId);
3573
+ const invokeConversationId = require_resolve_allowlist.normalizeMSTeamsConversationId(activity.conversation?.id ?? "");
3244
3574
  if (!invokeConversationId || pendingConversationId !== invokeConversationId) {
3245
3575
  log.info("file consent conversation mismatch", {
3246
3576
  uploadId,
@@ -3335,27 +3665,15 @@ const MSTEAMS_INGRESS_VERSION = 1;
3335
3665
  const MSTEAMS_INGRESS_DRAIN_INTERVAL_MS = 500;
3336
3666
  const MSTEAMS_INGRESS_MAX_CONCURRENT_DELIVERIES = 8;
3337
3667
  const MSTEAMS_INGRESS_SCAN_LIMIT = 100;
3338
- const MSTEAMS_INGRESS_TOMBSTONE_TTL_MS = 720 * 60 * 6e4;
3339
- const MSTEAMS_INGRESS_COMPLETED_MAX_ENTRIES = 2e4;
3340
- const MSTEAMS_INGRESS_FAILED_MAX_ENTRIES = 4096;
3341
- var MSTeamsIngressPayloadError = class extends Error {
3342
- constructor(reason, message, options) {
3343
- super(message, options);
3344
- this.reason = reason;
3345
- this.name = "MSTeamsIngressPayloadError";
3346
- }
3347
- };
3348
- function nonEmptyString(value) {
3349
- return typeof value === "string" && value.trim() ? value.trim() : null;
3350
- }
3668
+ const MSTeamsIngressPayloadError = (0, openclaw_plugin_sdk_channel_outbound.createChannelIngressError)("MSTeamsIngressPayloadError", { withReason: true });
3351
3669
  function isDispatchableActivity(activity) {
3352
3670
  return activity.type === "message" || activity.type === "invoke" && activity.name === "adaptiveCard/action";
3353
3671
  }
3354
3672
  function inspectMSTeamsIngressActivity(activity) {
3355
3673
  if (!isDispatchableActivity(activity)) return null;
3356
- const eventId = nonEmptyString(activity.id);
3674
+ const eventId = (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeNullableString)(activity.id);
3357
3675
  if (!eventId) throw new MSTeamsIngressPayloadError("invalid-activity", "Microsoft Teams dispatchable activity is missing activity.id.");
3358
- const laneKey = nonEmptyString(activity.conversation?.id);
3676
+ const laneKey = (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeNullableString)(activity.conversation?.id);
3359
3677
  if (!laneKey) throw new MSTeamsIngressPayloadError("invalid-activity", "Microsoft Teams dispatchable activity is missing conversation.id.");
3360
3678
  return {
3361
3679
  eventId,
@@ -3414,10 +3732,7 @@ function createMSTeamsIngress(options) {
3414
3732
  pollIntervalMs: MSTEAMS_INGRESS_DRAIN_INTERVAL_MS,
3415
3733
  retention: {
3416
3734
  pruneIntervalMs: 0,
3417
- completedTtlMs: MSTEAMS_INGRESS_TOMBSTONE_TTL_MS,
3418
- completedMaxEntries: MSTEAMS_INGRESS_COMPLETED_MAX_ENTRIES,
3419
- failedTtlMs: MSTEAMS_INGRESS_TOMBSTONE_TTL_MS,
3420
- failedMaxEntries: MSTEAMS_INGRESS_FAILED_MAX_ENTRIES
3735
+ failedMaxEntries: 4096
3421
3736
  },
3422
3737
  appendRetryDelaysMs: [0],
3423
3738
  waitForDeliveryIdleBeforeRepump: false,
@@ -3529,13 +3844,13 @@ async function monitorMSTeamsProvider(opts) {
3529
3844
  const configuredAllowFrom = msteamsCfg.allowFrom;
3530
3845
  const configuredGroupAllowFrom = msteamsCfg.groupAllowFrom;
3531
3846
  let allowFrom = require_resolve_allowlist.projectStableMSTeamsUserAllowlist(configuredAllowFrom);
3532
- let groupAllowFrom = require_resolve_allowlist.projectStableMSTeamsUserAllowlist(configuredGroupAllowFrom);
3847
+ let groupAllowFrom = require_resolve_allowlist.projectStableMSTeamsGroupAllowlist(configuredGroupAllowFrom ?? configuredAllowFrom);
3533
3848
  let teamsConfig = require_resolve_allowlist.projectStableMSTeamsTeamsConfig(msteamsCfg.teams);
3534
3849
  const allowNameMatching = (0, openclaw_plugin_sdk_dangerous_name_runtime.isDangerousNameMatchingEnabled)(msteamsCfg);
3535
3850
  const cleanAllowEntry = (entry) => entry.replace(/^(msteams|teams):/i, "").replace(/^user:/i, "").trim();
3536
3851
  const isStableUserId = (entry) => /^[0-9a-fA-F-]{16,}$/.test(entry);
3537
3852
  const cleanAllowEntries = (entries) => entries?.map((entry) => cleanAllowEntry(entry)).filter((entry) => entry && entry !== "*") ?? [];
3538
- const isMutableUserEntry = (entry) => !isStableUserId(entry) && !/^accessGroup:/i.test(entry);
3853
+ const isMutableUserEntry = (entry) => !isStableUserId(entry) && !/^accessGroup:/i.test(entry) && !require_resolve_allowlist.looksLikeMSTeamsConversationId(require_resolve_allowlist.normalizeMSTeamsConversationId(entry));
3539
3854
  const resolveAllowlistUsers = async (label, entries) => {
3540
3855
  if (entries.length === 0) return {
3541
3856
  additions: [],
@@ -3588,6 +3903,10 @@ async function monitorMSTeamsProvider(opts) {
3588
3903
  } catch (err) {
3589
3904
  runtime.error?.(`msteams resolve failed; mutable allowlist entries are disabled. ${require_setup_surface.formatUnknownError(err)}`);
3590
3905
  }
3906
+ if (configuredGroupAllowFrom == null && groupAllowFrom) groupAllowFrom = (0, openclaw_plugin_sdk_allow_from.mergeAllowlist)({
3907
+ existing: groupAllowFrom,
3908
+ additions: allowFrom ?? []
3909
+ });
3591
3910
  msteamsCfg = {
3592
3911
  ...msteamsCfg,
3593
3912
  allowFrom,
@@ -3697,8 +4016,8 @@ async function monitorMSTeamsProvider(opts) {
3697
4016
  value: "Poll not found."
3698
4017
  };
3699
4018
  }
3700
- const pollConversationId = existingPoll.conversationId ? require_channel.normalizeMSTeamsConversationId(existingPoll.conversationId) : void 0;
3701
- const activityConversationId = require_channel.normalizeMSTeamsConversationId(activity?.conversation?.id ?? "");
4019
+ const pollConversationId = existingPoll.conversationId ? require_resolve_allowlist.normalizeMSTeamsConversationId(existingPoll.conversationId) : void 0;
4020
+ const activityConversationId = require_resolve_allowlist.normalizeMSTeamsConversationId(activity?.conversation?.id ?? "");
3702
4021
  if (pollConversationId && pollConversationId !== activityConversationId) {
3703
4022
  log.info("poll vote ignored (conversation mismatch)", {
3704
4023
  pollId: vote.pollId,
@@ -3930,9 +4249,9 @@ function buildActivityHandler() {
3930
4249
  }
3931
4250
  function createMSTeamsReplayContext(activity, app, serviceUrlBoundary) {
3932
4251
  const rawConversationId = activity.conversation?.id ?? "";
3933
- const conversationId = require_channel.normalizeMSTeamsConversationId(rawConversationId);
4252
+ const conversationId = require_resolve_allowlist.normalizeMSTeamsConversationId(rawConversationId);
3934
4253
  const conversationType = activity.conversation?.conversationType ?? "personal";
3935
- const threadActivityId = conversationType.toLowerCase() === "channel" ? require_channel.extractMSTeamsConversationMessageId(rawConversationId) ?? activity.replyToId : void 0;
4254
+ const threadActivityId = conversationType.toLowerCase() === "channel" ? require_resolve_allowlist.extractMSTeamsConversationMessageId(rawConversationId) ?? activity.replyToId : void 0;
3936
4255
  const tenantId = activity.channelData?.tenant?.id ?? activity.conversation?.tenantId;
3937
4256
  const reference = {
3938
4257
  activityId: activity.id,