@openclaw/msteams 2026.7.2-beta.4 → 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,15 +1,14 @@
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-CgA2KHN8.cjs");
5
- const require_channel = require("./channel-D_epy05a.cjs");
6
- const require_setup_surface = require("./setup-surface-BWSGtAxk.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-BcpkE82v.cjs");
10
- let openclaw_plugin_sdk_allow_from = require("openclaw/plugin-sdk/allow-from");
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");
11
9
  let openclaw_plugin_sdk_channel_outbound = require("openclaw/plugin-sdk/channel-outbound");
12
10
  let openclaw_plugin_sdk_string_coerce_runtime = require("openclaw/plugin-sdk/string-coerce-runtime");
11
+ let openclaw_plugin_sdk_allow_from = require("openclaw/plugin-sdk/allow-from");
13
12
  let openclaw_plugin_sdk_media_runtime = require("openclaw/plugin-sdk/media-runtime");
14
13
  let openclaw_plugin_sdk_ssrf_runtime = require("openclaw/plugin-sdk/ssrf-runtime");
15
14
  let openclaw_plugin_sdk_provider_http = require("openclaw/plugin-sdk/provider-http");
@@ -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,145 +1405,291 @@ 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 = () => {
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 = () => {
1379
1693
  if (!stream || wasCanceled() || streamFinalizationPending) return;
1380
1694
  const informativeText = (0, openclaw_plugin_sdk_channel_outbound.formatChannelProgressDraftText)({
1381
1695
  entry: params.msteamsConfig,
@@ -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,181 +2334,214 @@ 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
1977
2446
  }
1978
- }
1979
- return merged;
1980
- }
1981
- function hasMaterializedMedia(media) {
1982
- return media.some((entry) => Boolean(entry.path));
1983
- }
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({
1987
- attachments,
1988
- maxBytes,
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
+ },
1989
2466
  tokenProvider,
1990
- allowHosts,
1991
- authAllowHosts: params.authAllowHosts,
1992
- preserveFilenames,
1993
- deadline: params.deadline,
1994
- logger: log
2467
+ sharePointSiteId: cfg.channels?.msteams?.sharePointSiteId
1995
2468
  });
1996
- if (!hasMaterializedMedia(mediaList)) {
1997
- const attachmentIds = extractMSTeamsHtmlAttachmentIds(attachments);
1998
- const hasHtmlFileAttachment = attachmentIds.length > 0;
1999
- const hasChannelOrGroupHtml = shouldAttemptMSTeamsGraphMediaFallback({
2000
- 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)", {
2007
- 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
- });
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
2064
2477
  }
2065
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" };
2066
2541
  }
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
2074
- });
2075
- return mediaList;
2076
2542
  }
2077
2543
  //#endregion
2078
- //#region extensions/msteams/src/monitor-handler/message-handler.ts
2544
+ //#region extensions/msteams/src/monitor-handler/inbound-facts.ts
2079
2545
  function extractTextFromHtmlAttachments(attachments) {
2080
2546
  for (const attachment of attachments) {
2081
2547
  const raw = require_resolve_allowlist.extractHtmlFromAttachment(attachment);
@@ -2085,54 +2551,354 @@ function extractTextFromHtmlAttachments(attachments) {
2085
2551
  }
2086
2552
  return "";
2087
2553
  }
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;
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,
2571
+ attachments,
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,
2595
+ conversationType,
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,
2644
+ conversationType,
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;
2664
+ }
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
+ }
2779
+ });
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
+ };
2812
+ }
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) });
2102
2835
  }
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);
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) });
2890
+ }
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,379 +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
- function buildFlushIngressLifecycle(entries) {
2753
- const lifecycles = entries.map((entry) => entry.turnAdoptionLifecycle).filter((lifecycle) => lifecycle !== void 0);
2754
- const [firstLifecycle] = lifecycles;
2755
- if (!firstLifecycle) return {
2756
- lifecycle: void 0,
2757
- settle: async () => {}
2758
- };
2759
- let handedOff = false;
2760
- const adoptAll = async () => {
2761
- for (const lifecycle of lifecycles) await lifecycle.onAdopted();
2762
- };
2763
- return {
2764
- lifecycle: {
2765
- abortSignal: lifecycles.length === 1 ? firstLifecycle.abortSignal : AbortSignal.any(lifecycles.map((lifecycle) => lifecycle.abortSignal)),
2766
- onAdopted: async () => {
2767
- handedOff = true;
2768
- await adoptAll();
2769
- },
2770
- onDeferred: () => {
2771
- handedOff = true;
2772
- for (const lifecycle of lifecycles) lifecycle.onDeferred();
2773
- },
2774
- onAdoptionFinalizing: () => {
2775
- for (const lifecycle of lifecycles) lifecycle.onAdoptionFinalizing();
2776
- },
2777
- onAbandoned: async () => {
2778
- handedOff = true;
2779
- for (const lifecycle of lifecycles) await lifecycle.onAbandoned();
2780
- }
2781
- },
2782
- settle: async () => {
2783
- if (!handedOff) await adoptAll();
2784
- }
2785
- };
2786
- }
2787
3094
  const inboundDebouncer = core.channel.debounce.createInboundDebouncer({
2788
3095
  debounceMs: inboundDebounceMs,
2789
3096
  buildKey: (entry) => {
2790
- const conversationId = require_channel.normalizeMSTeamsConversationId(entry.context.activity.conversation?.id ?? "");
3097
+ const conversationId = require_resolve_allowlist.normalizeMSTeamsConversationId(entry.context.activity.conversation?.id ?? "");
2791
3098
  const senderId = entry.context.activity.from?.aadObjectId ?? entry.context.activity.from?.id ?? "";
2792
3099
  if (!senderId || !conversationId) return null;
2793
3100
  return `msteams:${appId}:${conversationId}:${senderId}`;
@@ -2797,65 +3104,53 @@ function createMSTeamsMessageHandler(deps) {
2797
3104
  if (entry.attachments.length > 0) return false;
2798
3105
  return !core.channel.commands.isControlCommandMessage(entry.text, cfg);
2799
3106
  },
2800
- onFlush: async (entries) => {
3107
+ onFlush: (entries, createFlush) => {
2801
3108
  const last = entries.at(-1);
2802
- if (!last) return;
2803
- const { lifecycle, settle } = buildFlushIngressLifecycle(entries);
2804
- try {
2805
- if (entries.length === 1) await handleTeamsMessageNow(lifecycle ? {
2806
- ...last,
2807
- turnAdoptionLifecycle: lifecycle
2808
- } : last);
2809
- else {
2810
- const combinedText = entries.map((entry) => entry.text).filter(Boolean).join("\n");
2811
- if (combinedText.trim()) {
2812
- const combinedRawText = entries.map((entry) => entry.rawText).filter(Boolean).join("\n");
2813
- const wasMentioned = entries.some((entry) => entry.wasMentioned);
2814
- const implicitMentionKinds = entries.flatMap((entry) => entry.implicitMentionKinds);
2815
- await handleTeamsMessageNow({
2816
- context: last.context,
2817
- rawText: combinedRawText,
2818
- text: combinedText,
2819
- attachments: [],
2820
- wasMentioned,
2821
- implicitMentionKinds,
2822
- ...lifecycle ? { turnAdoptionLifecycle: lifecycle } : {}
3109
+ const { lifecycle, settle } = (0, openclaw_plugin_sdk_channel_ingress_runtime.fanInChannelIngressLifecycles)(entries.map((entry) => entry.turnAdoptionLifecycle));
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
2823
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;
2824
3140
  }
2825
3141
  }
2826
- await settle();
2827
- } catch (err) {
2828
- await lifecycle?.onAbandoned();
2829
- throw err;
2830
- }
3142
+ });
2831
3143
  },
2832
3144
  onError: (err) => {
2833
3145
  runtime.error(`msteams debounce flush failed: ${require_setup_surface.formatUnknownError(err)}`);
2834
3146
  }
2835
3147
  });
2836
3148
  return async function handleTeamsMessage(context, turnAdoptionLifecycle) {
2837
- const activity = context.activity;
2838
- const attachments = Array.isArray(activity.attachments) ? activity.attachments : [];
2839
- const rawText = activity.text?.trim() ?? "";
2840
- const htmlText = extractTextFromHtmlAttachments(attachments);
2841
- const valueText = rawText || htmlText ? "" : serializeMSTeamsAdaptiveCardActionValue(activity.value);
2842
- const text = require_channel.stripMSTeamsMentionTags(rawText || htmlText || valueText || "");
2843
- const wasMentioned = require_channel.wasMSTeamsBotMentioned(activity);
2844
- const conversationId = require_channel.normalizeMSTeamsConversationId(activity.conversation?.id ?? "");
2845
- const replyToId = activity.replyToId ?? void 0;
2846
- const implicitMentionKinds = conversationId && replyToId && await wasMSTeamsMessageSentWithPersistence({
2847
- conversationId,
2848
- messageId: replyToId
2849
- }) ? ["reply_to_bot"] : [];
2850
- await inboundDebouncer.enqueue({
3149
+ const entry = await prepareMSTeamsDebounceEntry({
2851
3150
  context,
2852
- rawText,
2853
- text,
2854
- attachments,
2855
- wasMentioned,
2856
- implicitMentionKinds,
2857
3151
  turnAdoptionLifecycle
2858
3152
  });
3153
+ await inboundDebouncer.enqueue(entry);
2859
3154
  if (turnAdoptionLifecycle) return { kind: "deferred" };
2860
3155
  };
2861
3156
  }
@@ -2881,7 +3176,7 @@ function createMSTeamsReactionHandler(deps) {
2881
3176
  log.debug?.("reaction activity missing from.id; skipping");
2882
3177
  return;
2883
3178
  }
2884
- const conversationId = require_channel.normalizeMSTeamsConversationId(activity.conversation?.id ?? "");
3179
+ const conversationId = require_resolve_allowlist.normalizeMSTeamsConversationId(activity.conversation?.id ?? "");
2885
3180
  const conversationType = activity.conversation?.conversationType ?? "personal";
2886
3181
  const isGroupChat = conversationType === "groupChat" || activity.conversation?.isGroup === true;
2887
3182
  const isChannel = conversationType === "channel";
@@ -3177,7 +3472,7 @@ async function runMSTeamsFeedbackInvokeHandler(context, deps) {
3177
3472
  userComment = JSON.parse(value.actionValue.feedback).feedbackText || void 0;
3178
3473
  } catch {}
3179
3474
  const rawConversationId = activity.conversation?.id ?? "unknown";
3180
- const conversationId = require_channel.normalizeMSTeamsConversationId(rawConversationId);
3475
+ const conversationId = require_resolve_allowlist.normalizeMSTeamsConversationId(rawConversationId);
3181
3476
  const senderId = activity.from?.aadObjectId ?? activity.from?.id ?? "unknown";
3182
3477
  const messageId = value.replyToId ?? activity.replyToId ?? "unknown";
3183
3478
  const isNegative = reaction === "dislike";
@@ -3192,7 +3487,7 @@ async function runMSTeamsFeedbackInvokeHandler(context, deps) {
3192
3487
  id: isDirectMessage ? senderId : conversationId
3193
3488
  }
3194
3489
  });
3195
- 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;
3196
3491
  if (feedbackThreadId) route.sessionKey = (0, openclaw_plugin_sdk_routing.resolveThreadSessionKeys)({
3197
3492
  baseSessionKey: route.sessionKey,
3198
3493
  threadId: feedbackThreadId,
@@ -3274,8 +3569,8 @@ async function handleMSTeamsFileConsentInvoke(context, log) {
3274
3569
  const fsFile = inMemoryFile ? void 0 : await require_messenger.getPendingUploadFs(uploadId);
3275
3570
  const pendingFile = inMemoryFile ?? fsFile;
3276
3571
  if (pendingFile) {
3277
- const pendingConversationId = require_channel.normalizeMSTeamsConversationId(pendingFile.conversationId);
3278
- 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 ?? "");
3279
3574
  if (!invokeConversationId || pendingConversationId !== invokeConversationId) {
3280
3575
  log.info("file consent conversation mismatch", {
3281
3576
  uploadId,
@@ -3370,27 +3665,15 @@ const MSTEAMS_INGRESS_VERSION = 1;
3370
3665
  const MSTEAMS_INGRESS_DRAIN_INTERVAL_MS = 500;
3371
3666
  const MSTEAMS_INGRESS_MAX_CONCURRENT_DELIVERIES = 8;
3372
3667
  const MSTEAMS_INGRESS_SCAN_LIMIT = 100;
3373
- const MSTEAMS_INGRESS_TOMBSTONE_TTL_MS = 720 * 60 * 6e4;
3374
- const MSTEAMS_INGRESS_COMPLETED_MAX_ENTRIES = 2e4;
3375
- const MSTEAMS_INGRESS_FAILED_MAX_ENTRIES = 4096;
3376
- var MSTeamsIngressPayloadError = class extends Error {
3377
- constructor(reason, message, options) {
3378
- super(message, options);
3379
- this.reason = reason;
3380
- this.name = "MSTeamsIngressPayloadError";
3381
- }
3382
- };
3383
- function nonEmptyString(value) {
3384
- return typeof value === "string" && value.trim() ? value.trim() : null;
3385
- }
3668
+ const MSTeamsIngressPayloadError = (0, openclaw_plugin_sdk_channel_outbound.createChannelIngressError)("MSTeamsIngressPayloadError", { withReason: true });
3386
3669
  function isDispatchableActivity(activity) {
3387
3670
  return activity.type === "message" || activity.type === "invoke" && activity.name === "adaptiveCard/action";
3388
3671
  }
3389
3672
  function inspectMSTeamsIngressActivity(activity) {
3390
3673
  if (!isDispatchableActivity(activity)) return null;
3391
- const eventId = nonEmptyString(activity.id);
3674
+ const eventId = (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeNullableString)(activity.id);
3392
3675
  if (!eventId) throw new MSTeamsIngressPayloadError("invalid-activity", "Microsoft Teams dispatchable activity is missing activity.id.");
3393
- const laneKey = nonEmptyString(activity.conversation?.id);
3676
+ const laneKey = (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeNullableString)(activity.conversation?.id);
3394
3677
  if (!laneKey) throw new MSTeamsIngressPayloadError("invalid-activity", "Microsoft Teams dispatchable activity is missing conversation.id.");
3395
3678
  return {
3396
3679
  eventId,
@@ -3449,10 +3732,7 @@ function createMSTeamsIngress(options) {
3449
3732
  pollIntervalMs: MSTEAMS_INGRESS_DRAIN_INTERVAL_MS,
3450
3733
  retention: {
3451
3734
  pruneIntervalMs: 0,
3452
- completedTtlMs: MSTEAMS_INGRESS_TOMBSTONE_TTL_MS,
3453
- completedMaxEntries: MSTEAMS_INGRESS_COMPLETED_MAX_ENTRIES,
3454
- failedTtlMs: MSTEAMS_INGRESS_TOMBSTONE_TTL_MS,
3455
- failedMaxEntries: MSTEAMS_INGRESS_FAILED_MAX_ENTRIES
3735
+ failedMaxEntries: 4096
3456
3736
  },
3457
3737
  appendRetryDelaysMs: [0],
3458
3738
  waitForDeliveryIdleBeforeRepump: false,
@@ -3564,13 +3844,13 @@ async function monitorMSTeamsProvider(opts) {
3564
3844
  const configuredAllowFrom = msteamsCfg.allowFrom;
3565
3845
  const configuredGroupAllowFrom = msteamsCfg.groupAllowFrom;
3566
3846
  let allowFrom = require_resolve_allowlist.projectStableMSTeamsUserAllowlist(configuredAllowFrom);
3567
- let groupAllowFrom = require_resolve_allowlist.projectStableMSTeamsUserAllowlist(configuredGroupAllowFrom);
3847
+ let groupAllowFrom = require_resolve_allowlist.projectStableMSTeamsGroupAllowlist(configuredGroupAllowFrom ?? configuredAllowFrom);
3568
3848
  let teamsConfig = require_resolve_allowlist.projectStableMSTeamsTeamsConfig(msteamsCfg.teams);
3569
3849
  const allowNameMatching = (0, openclaw_plugin_sdk_dangerous_name_runtime.isDangerousNameMatchingEnabled)(msteamsCfg);
3570
3850
  const cleanAllowEntry = (entry) => entry.replace(/^(msteams|teams):/i, "").replace(/^user:/i, "").trim();
3571
3851
  const isStableUserId = (entry) => /^[0-9a-fA-F-]{16,}$/.test(entry);
3572
3852
  const cleanAllowEntries = (entries) => entries?.map((entry) => cleanAllowEntry(entry)).filter((entry) => entry && entry !== "*") ?? [];
3573
- 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));
3574
3854
  const resolveAllowlistUsers = async (label, entries) => {
3575
3855
  if (entries.length === 0) return {
3576
3856
  additions: [],
@@ -3623,6 +3903,10 @@ async function monitorMSTeamsProvider(opts) {
3623
3903
  } catch (err) {
3624
3904
  runtime.error?.(`msteams resolve failed; mutable allowlist entries are disabled. ${require_setup_surface.formatUnknownError(err)}`);
3625
3905
  }
3906
+ if (configuredGroupAllowFrom == null && groupAllowFrom) groupAllowFrom = (0, openclaw_plugin_sdk_allow_from.mergeAllowlist)({
3907
+ existing: groupAllowFrom,
3908
+ additions: allowFrom ?? []
3909
+ });
3626
3910
  msteamsCfg = {
3627
3911
  ...msteamsCfg,
3628
3912
  allowFrom,
@@ -3732,8 +4016,8 @@ async function monitorMSTeamsProvider(opts) {
3732
4016
  value: "Poll not found."
3733
4017
  };
3734
4018
  }
3735
- const pollConversationId = existingPoll.conversationId ? require_channel.normalizeMSTeamsConversationId(existingPoll.conversationId) : void 0;
3736
- 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 ?? "");
3737
4021
  if (pollConversationId && pollConversationId !== activityConversationId) {
3738
4022
  log.info("poll vote ignored (conversation mismatch)", {
3739
4023
  pollId: vote.pollId,
@@ -3965,9 +4249,9 @@ function buildActivityHandler() {
3965
4249
  }
3966
4250
  function createMSTeamsReplayContext(activity, app, serviceUrlBoundary) {
3967
4251
  const rawConversationId = activity.conversation?.id ?? "";
3968
- const conversationId = require_channel.normalizeMSTeamsConversationId(rawConversationId);
4252
+ const conversationId = require_resolve_allowlist.normalizeMSTeamsConversationId(rawConversationId);
3969
4253
  const conversationType = activity.conversation?.conversationType ?? "personal";
3970
- 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;
3971
4255
  const tenantId = activity.channelData?.tenant?.id ?? activity.conversation?.tenantId;
3972
4256
  const reference = {
3973
4257
  activityId: activity.id,