@openclaw/msteams 2026.9.5 → 2026.9.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.
Files changed (33) hide show
  1. package/assets/icon.png +0 -0
  2. package/dist/.setup/{adapter.runtime-ChuVK6k_.cjs → adapter.runtime-BiWeMKXE.cjs} +24 -30
  3. package/dist/.setup/{approval-card-CPNH_q3P.cjs → approval-card-DWPDItvq.cjs} +6 -3
  4. package/dist/.setup/{approval-handler.runtime-DDxGPJDz.cjs → approval-handler.runtime-D1s7UR3U.cjs} +4 -4
  5. package/dist/.setup/bot-framework-service-url-BUhTe8F3.cjs +55 -0
  6. package/dist/.setup/{channel-Dzpbbht8.cjs → channel-ByzS7QJ0.cjs} +338 -220
  7. package/dist/.setup/{channel.runtime-TE49QvFv.cjs → channel.runtime-Baz8XDFs.cjs} +47 -144
  8. package/dist/.setup/{channel.setup-BVDfgdCc.cjs → channel.setup-DL8lcWT_.cjs} +6 -6
  9. package/dist/.setup/{config-schema-D22Lr1Qk.cjs → config-schema-0KoBzb0t.cjs} +24 -38
  10. package/dist/.setup/{messenger-BR1l4zw-.cjs → conversation-store-state-B7JuyZTm.cjs} +697 -297
  11. package/dist/.setup/{src-CpKfpP1m.cjs → monitor-BJ7IyH4y.cjs} +440 -663
  12. package/dist/.setup/{oauth-C6m7dvHj.cjs → oauth-DQjOnJ4l.cjs} +1 -1
  13. package/dist/.setup/{reaction-types-C7RA7Wxt.cjs → reaction-types-CQ4oSUe7.cjs} +2 -6
  14. package/dist/.setup/{resolve-allowlist-CHzaaWiC.cjs → resolve-allowlist-Pl_NQFYH.cjs} +55 -312
  15. package/dist/.setup/{rolldown-runtime-VH7oDXx4.cjs → rolldown-runtime-BloeesfJ.cjs} +22 -0
  16. package/dist/.setup/{send-D8ATH4QI.cjs → send-VUM7Sryb.cjs} +47 -97
  17. package/dist/.setup/{setup-plugin-api-DpiaA8z1.cjs → setup-plugin-api-Dyf8kF-l.cjs} +1 -1
  18. package/dist/.setup/{thread-session-CXgHK-yi.cjs → thread-session--rw3BTal.cjs} +2 -5
  19. package/dist/api.cjs +2 -2
  20. package/dist/channel-config-api.cjs +1 -1
  21. package/dist/channel-plugin-api.cjs +1 -1
  22. package/dist/configured-state.cjs +0 -1
  23. package/dist/directory-contract-api.cjs +1 -1
  24. package/dist/doctor-contract-api.cjs +201 -371
  25. package/dist/qa-runner-api.cjs +1 -1
  26. package/dist/runtime-api.cjs +0 -6
  27. package/dist/setup-entry.cjs +1 -1
  28. package/dist/setup-plugin-api.cjs +1 -1
  29. package/openclaw.plugin.json +46 -5
  30. package/package.json +10 -10
  31. package/dist/.setup/polls-BJ4-4uZm.cjs +0 -668
  32. package/dist/.setup/secret-input-C65t6kjM.cjs +0 -1
  33. package/dist/.setup/sso-token-store-YYLHXgFH.cjs +0 -111
@@ -1,15 +1,14 @@
1
- const require_rolldown_runtime = require("./rolldown-runtime-VH7oDXx4.cjs");
1
+ const require_rolldown_runtime = require("./rolldown-runtime-BloeesfJ.cjs");
2
2
  const require_runtime = require("./runtime-gZTAuFux.cjs");
3
3
  require("../runtime-api.cjs");
4
- const require_resolve_allowlist = require("./resolve-allowlist-CHzaaWiC.cjs");
5
- const require_channel = require("./channel-Dzpbbht8.cjs");
6
- const require_thread_session = require("./thread-session-CXgHK-yi.cjs");
7
- const require_channel_setup = require("./channel.setup-BVDfgdCc.cjs");
8
- const require_polls = require("./polls-BJ4-4uZm.cjs");
9
- const require_sso_token_store = require("./sso-token-store-YYLHXgFH.cjs");
10
- const require_approval_card = require("./approval-card-CPNH_q3P.cjs");
11
- const require_messenger = require("./messenger-BR1l4zw-.cjs");
12
- const require_reaction_types = require("./reaction-types-C7RA7Wxt.cjs");
4
+ const require_resolve_allowlist = require("./resolve-allowlist-Pl_NQFYH.cjs");
5
+ const require_bot_framework_service_url = require("./bot-framework-service-url-BUhTe8F3.cjs");
6
+ const require_channel = require("./channel-ByzS7QJ0.cjs");
7
+ const require_thread_session = require("./thread-session--rw3BTal.cjs");
8
+ const require_channel_setup = require("./channel.setup-DL8lcWT_.cjs");
9
+ const require_approval_card = require("./approval-card-DWPDItvq.cjs");
10
+ const require_conversation_store_state = require("./conversation-store-state-B7JuyZTm.cjs");
11
+ const require_reaction_types = require("./reaction-types-CQ4oSUe7.cjs");
13
12
  let openclaw_plugin_sdk_channel_outbound = require("openclaw/plugin-sdk/channel-outbound");
14
13
  let openclaw_plugin_sdk_string_coerce_runtime = require("openclaw/plugin-sdk/string-coerce-runtime");
15
14
  let openclaw_plugin_sdk_allow_from = require("openclaw/plugin-sdk/allow-from");
@@ -17,13 +16,16 @@ let openclaw_plugin_sdk_provider_http = require("openclaw/plugin-sdk/provider-ht
17
16
  let openclaw_plugin_sdk_account_id = require("openclaw/plugin-sdk/account-id");
18
17
  let openclaw_plugin_sdk_media_runtime = require("openclaw/plugin-sdk/media-runtime");
19
18
  let openclaw_plugin_sdk_ssrf_runtime = require("openclaw/plugin-sdk/ssrf-runtime");
19
+ let openclaw_plugin_sdk_ssrf_policy = require("openclaw/plugin-sdk/ssrf-policy");
20
20
  let openclaw_plugin_sdk_number_runtime = require("openclaw/plugin-sdk/number-runtime");
21
21
  let openclaw_plugin_sdk_text_utility_runtime = require("openclaw/plugin-sdk/text-utility-runtime");
22
22
  let openclaw_plugin_sdk_error_runtime = require("openclaw/plugin-sdk/error-runtime");
23
23
  let openclaw_plugin_sdk_routing = require("openclaw/plugin-sdk/routing");
24
+ let openclaw_plugin_sdk_channel_inbound = require("openclaw/plugin-sdk/channel-inbound");
24
25
  let node_crypto = require("node:crypto");
25
26
  node_crypto = require_rolldown_runtime.__toESM(node_crypto, 1);
26
- let openclaw_plugin_sdk_channel_inbound = require("openclaw/plugin-sdk/channel-inbound");
27
+ let openclaw_plugin_sdk_security_runtime = require("openclaw/plugin-sdk/security-runtime");
28
+ let openclaw_plugin_sdk_webhook_targets = require("openclaw/plugin-sdk/webhook-targets");
27
29
  let openclaw_plugin_sdk_approval_gateway_runtime = require("openclaw/plugin-sdk/approval-gateway-runtime");
28
30
  let openclaw_plugin_sdk_channel_ingress_runtime = require("openclaw/plugin-sdk/channel-ingress-runtime");
29
31
  let openclaw_plugin_sdk_context_visibility_runtime = require("openclaw/plugin-sdk/context-visibility-runtime");
@@ -32,10 +34,9 @@ let openclaw_plugin_sdk_runtime_config_snapshot = require("openclaw/plugin-sdk/r
32
34
  let openclaw_plugin_sdk_agent_runtime = require("openclaw/plugin-sdk/agent-runtime");
33
35
  let openclaw_plugin_sdk_extension_shared = require("openclaw/plugin-sdk/extension-shared");
34
36
  let openclaw_plugin_sdk_plugin_runtime = require("openclaw/plugin-sdk/plugin-runtime");
35
- let openclaw_plugin_sdk_dedupe_runtime = require("openclaw/plugin-sdk/dedupe-runtime");
36
- let openclaw_plugin_sdk_plugin_state_runtime = require("openclaw/plugin-sdk/plugin-state-runtime");
37
37
  let openclaw_plugin_sdk_collection_runtime = require("openclaw/plugin-sdk/collection-runtime");
38
38
  let openclaw_plugin_sdk_gateway_runtime = require("openclaw/plugin-sdk/gateway-runtime");
39
+ let openclaw_plugin_sdk_time_runtime = require("openclaw/plugin-sdk/time-runtime");
39
40
  let openclaw_plugin_sdk_channel_pairing = require("openclaw/plugin-sdk/channel-pairing");
40
41
  let openclaw_plugin_sdk_runtime_group_policy = require("openclaw/plugin-sdk/runtime-group-policy");
41
42
  let openclaw_plugin_sdk_dangerous_name_runtime = require("openclaw/plugin-sdk/dangerous-name-runtime");
@@ -150,7 +151,7 @@ async function runFeedbackReflection(params) {
150
151
  return;
151
152
  }
152
153
  try {
153
- await require_messenger.sendMSTeamsActivityWithReference(params.app, require_messenger.buildConversationReference(params.conversationRef), {
154
+ await require_conversation_store_state.sendMSTeamsActivityWithReference(params.app, require_conversation_store_state.buildConversationReference(params.conversationRef), {
154
155
  type: "message",
155
156
  text: reflection.userMessage
156
157
  }, { serviceUrlBoundary: require_resolve_allowlist.resolveMSTeamsSdkCloudOptions(cfg.channels?.msteams) });
@@ -248,12 +249,8 @@ async function maybeHandleMSTeamsApprovalCardSubmit(params) {
248
249
  senderId
249
250
  });
250
251
  await context.updateActivity({
251
- type: "message",
252
- id: consumed.activityId,
253
- attachments: [{
254
- contentType: "application/vnd.microsoft.card.adaptive",
255
- content: require_approval_card.buildMSTeamsCanonicalApprovalTerminalCard(result)
256
- }]
252
+ ...require_conversation_store_state.buildMSTeamsAdaptiveCardActivity(require_approval_card.buildMSTeamsCanonicalApprovalTerminalCard(result)),
253
+ id: consumed.activityId
257
254
  });
258
255
  return result;
259
256
  });
@@ -350,7 +347,7 @@ async function resolveMSTeamsSenderAccess(params) {
350
347
  channelName: activity.channelData?.channel?.name,
351
348
  allowNameMatching
352
349
  });
353
- const resolved = await (0, openclaw_plugin_sdk_channel_ingress_runtime.resolveStableChannelMessageIngress)({
350
+ const resolved = await core.channel.inbound.ingress.resolveStable({
354
351
  channelId: "msteams",
355
352
  accountId: pairing.accountId,
356
353
  identity: {
@@ -455,7 +452,7 @@ async function admitMSTeamsMessage(params) {
455
452
  params.log.debug?.("dropping dm (dms disabled)");
456
453
  return null;
457
454
  }
458
- const allowMatch = require_channel.resolveMSTeamsAllowlistMatch({
455
+ const allowMatch = (0, openclaw_plugin_sdk_allow_from.resolveAllowlistMatchSimple)({
459
456
  allowFrom: effectiveDmAllowFrom,
460
457
  senderId,
461
458
  senderName,
@@ -520,7 +517,7 @@ async function admitMSTeamsMessage(params) {
520
517
  return null;
521
518
  }
522
519
  if (!senderAccess.allowed) {
523
- const allowMatch = require_channel.resolveMSTeamsAllowlistMatch({
520
+ const allowMatch = (0, openclaw_plugin_sdk_allow_from.resolveAllowlistMatchSimple)({
524
521
  allowFrom: effectiveGroupAllowFrom,
525
522
  senderId,
526
523
  senderName,
@@ -562,11 +559,19 @@ async function admitMSTeamsMessage(params) {
562
559
  };
563
560
  }
564
561
  //#endregion
562
+ //#region src/channels/mention-gating.ts
563
+ function resolveBotThreadMentionPolicy(params) {
564
+ if (!params.isBotOwnedThread || params.requireMentionInBotThreads === void 0) return {
565
+ requireMention: params.requireMention,
566
+ implicitMentionKinds: params.implicitMentionKinds
567
+ };
568
+ return {
569
+ requireMention: params.requireMentionInBotThreads,
570
+ implicitMentionKinds: params.requireMentionInBotThreads ? params.implicitMentionKinds?.filter((kind) => kind === "native") : params.implicitMentionKinds
571
+ };
572
+ }
573
+ //#endregion
565
574
  //#region extensions/msteams/src/attachments/bot-framework.ts
566
- /**
567
- * Bot Framework Service token scope for requesting a token used against
568
- * the Bot Connector (v3) REST endpoints such as `/v3/attachments/{id}`.
569
- */
570
575
  const BOT_FRAMEWORK_SCOPE = "https://api.botframework.com";
571
576
  /**
572
577
  * Detect Bot Framework personal chat ("a:") and MSA orgid ("8:orgid:") conversation
@@ -580,45 +585,39 @@ function isBotFrameworkPersonalChatId(conversationId) {
580
585
  const trimmed = conversationId.trim();
581
586
  return trimmed.startsWith("a:") || trimmed.startsWith("8:orgid:");
582
587
  }
583
- function normalizeServiceUrl(serviceUrl) {
584
- return serviceUrl.replace(/\/+$/, "");
585
- }
586
- function buildBotFrameworkAttachmentHeaders(params) {
587
- const headers = require_resolve_allowlist.ensureUserAgentHeader();
588
- require_resolve_allowlist.applyAuthorizationHeaderForUrl({
589
- headers,
590
- url: params.url,
591
- authAllowHosts: params.policy.authAllowHosts,
592
- bearerToken: params.accessToken
593
- });
594
- return headers;
595
- }
596
- async function fetchBotFrameworkAttachmentInfo(params) {
597
- const url = `${normalizeServiceUrl(params.serviceUrl)}/v3/attachments/${encodeURIComponent(params.attachmentId)}`;
588
+ async function fetchBotFrameworkAttachment(params, kind) {
598
589
  let response;
599
590
  try {
591
+ const headers = require_resolve_allowlist.ensureUserAgentHeader();
592
+ require_resolve_allowlist.applyAuthorizationHeaderForUrl({
593
+ headers,
594
+ url: params.url,
595
+ authAllowHosts: params.policy.authAllowHosts,
596
+ bearerToken: params.accessToken
597
+ });
600
598
  response = await require_resolve_allowlist.safeFetchWithPolicy({
601
- url,
599
+ url: params.url,
602
600
  policy: params.policy,
603
601
  fetchFn: params.fetchFn,
604
602
  fetchFnSupportsDispatcher: params.fetchFnSupportsDispatcher,
605
603
  resolveFn: params.resolveFn,
606
- requestInit: { headers: buildBotFrameworkAttachmentHeaders({
607
- url,
608
- accessToken: params.accessToken,
609
- policy: params.policy
610
- }) },
604
+ requestInit: { headers },
611
605
  timeoutMs: require_resolve_allowlist.resolveMSTeamsRequestTimeoutMs(params.deadline)
612
606
  });
613
607
  } catch (err) {
614
- params.logger?.warn?.("msteams botFramework attachmentInfo fetch failed", { error: (0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err) });
608
+ params.logger?.warn?.(`msteams botFramework ${kind} fetch failed`, { error: (0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err) });
615
609
  return;
616
610
  }
617
611
  if (!response.ok) {
618
612
  await response.body?.cancel().catch(() => void 0);
619
- params.logger?.warn?.("msteams botFramework attachmentInfo non-ok", { status: response.status });
613
+ params.logger?.warn?.(`msteams botFramework ${kind} non-ok`, { status: response.status });
620
614
  return;
621
615
  }
616
+ return response;
617
+ }
618
+ async function fetchBotFrameworkAttachmentInfo(params) {
619
+ const response = await fetchBotFrameworkAttachment(params, "attachmentInfo");
620
+ if (!response) return;
622
621
  try {
623
622
  return await (0, openclaw_plugin_sdk_provider_http.readProviderJsonResponse)(response, "msteams botFramework attachmentInfo");
624
623
  } catch (err) {
@@ -627,31 +626,8 @@ async function fetchBotFrameworkAttachmentInfo(params) {
627
626
  }
628
627
  }
629
628
  async function saveBotFrameworkAttachmentView(params) {
630
- const url = `${normalizeServiceUrl(params.serviceUrl)}/v3/attachments/${encodeURIComponent(params.attachmentId)}/views/${encodeURIComponent(params.viewId)}`;
631
- let response;
632
- try {
633
- response = await require_resolve_allowlist.safeFetchWithPolicy({
634
- url,
635
- policy: params.policy,
636
- fetchFn: params.fetchFn,
637
- fetchFnSupportsDispatcher: params.fetchFnSupportsDispatcher,
638
- resolveFn: params.resolveFn,
639
- requestInit: { headers: buildBotFrameworkAttachmentHeaders({
640
- url,
641
- accessToken: params.accessToken,
642
- policy: params.policy
643
- }) },
644
- timeoutMs: require_resolve_allowlist.resolveMSTeamsRequestTimeoutMs(params.deadline)
645
- });
646
- } catch (err) {
647
- params.logger?.warn?.("msteams botFramework attachmentView fetch failed", { error: (0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err) });
648
- return;
649
- }
650
- if (!response.ok) {
651
- await response.body?.cancel().catch(() => void 0);
652
- params.logger?.warn?.("msteams botFramework attachmentView non-ok", { status: response.status });
653
- return;
654
- }
629
+ const response = await fetchBotFrameworkAttachment(params, "attachmentView");
630
+ if (!response) return;
655
631
  let contentLength;
656
632
  try {
657
633
  contentLength = (0, openclaw_plugin_sdk_media_runtime.parseMediaContentLength)(response.headers.get("content-length"));
@@ -666,7 +642,7 @@ async function saveBotFrameworkAttachmentView(params) {
666
642
  }
667
643
  try {
668
644
  return await require_runtime.getMSTeamsRuntime().channel.media.saveResponseMedia(response, {
669
- sourceUrl: url,
645
+ sourceUrl: params.url,
670
646
  filePathHint: params.fileNameHint,
671
647
  maxBytes: params.maxBytes,
672
648
  fallbackContentType: params.contentTypeHint,
@@ -680,12 +656,6 @@ async function saveBotFrameworkAttachmentView(params) {
680
656
  await response.body?.cancel().catch(() => void 0);
681
657
  }
682
658
  }
683
- /**
684
- * Download media for a single attachment via the Bot Framework v3 attachments
685
- * endpoint. Used for personal DM conversations where the Graph `/chats/{id}`
686
- * path is not usable because the Bot Framework conversation ID (`a:...`) is
687
- * not a valid Graph chat identifier.
688
- */
689
659
  async function downloadMSTeamsBotFrameworkAttachment(params) {
690
660
  if (!params.serviceUrl || !params.attachmentId || !params.tokenProvider) return;
691
661
  const tokenProvider = params.tokenProvider;
@@ -693,8 +663,8 @@ async function downloadMSTeamsBotFrameworkAttachment(params) {
693
663
  allowHosts: params.allowHosts,
694
664
  authAllowHosts: params.authAllowHosts
695
665
  });
696
- const baseUrl = `${normalizeServiceUrl(params.serviceUrl)}/v3/attachments/${encodeURIComponent(params.attachmentId)}`;
697
- if (!require_resolve_allowlist.isUrlAllowed(baseUrl, policy.allowHosts)) return;
666
+ const baseUrl = `${params.serviceUrl.replace(/\/+$/, "")}/v3/attachments/${encodeURIComponent(params.attachmentId)}`;
667
+ if (!(0, openclaw_plugin_sdk_ssrf_policy.isHttpsUrlAllowedByHostnameSuffixAllowlist)(baseUrl, policy.allowHosts)) return;
698
668
  let accessToken;
699
669
  try {
700
670
  accessToken = await require_resolve_allowlist.withMSTeamsRequestDeadline({
@@ -707,9 +677,8 @@ async function downloadMSTeamsBotFrameworkAttachment(params) {
707
677
  return;
708
678
  }
709
679
  if (!accessToken) return;
710
- const info = await fetchBotFrameworkAttachmentInfo({
711
- serviceUrl: params.serviceUrl,
712
- attachmentId: params.attachmentId,
680
+ const request = {
681
+ url: baseUrl,
713
682
  accessToken,
714
683
  policy,
715
684
  fetchFn: params.fetchFn,
@@ -717,7 +686,8 @@ async function downloadMSTeamsBotFrameworkAttachment(params) {
717
686
  resolveFn: params.resolveFn,
718
687
  logger: params.logger,
719
688
  deadline: params.deadline
720
- });
689
+ };
690
+ const info = await fetchBotFrameworkAttachmentInfo(request);
721
691
  if (!info) return;
722
692
  const views = Array.isArray(info.views) ? info.views : [];
723
693
  const candidateView = views.find((view) => view?.viewId === "original") ?? views.find((view) => typeof view?.viewId === "string");
@@ -727,20 +697,12 @@ async function downloadMSTeamsBotFrameworkAttachment(params) {
727
697
  const fileNameHint = typeof params.fileNameHint === "string" && params.fileNameHint || typeof info.name === "string" && info.name || void 0;
728
698
  const contentTypeHint = typeof params.contentTypeHint === "string" && params.contentTypeHint || typeof info.type === "string" && info.type || void 0;
729
699
  const saved = await saveBotFrameworkAttachmentView({
730
- serviceUrl: params.serviceUrl,
731
- attachmentId: params.attachmentId,
732
- viewId,
733
- accessToken,
700
+ ...request,
701
+ url: `${baseUrl}/views/${encodeURIComponent(viewId)}`,
734
702
  maxBytes: params.maxBytes,
735
703
  fileNameHint,
736
704
  contentTypeHint,
737
- preserveFilenames: params.preserveFilenames,
738
- policy,
739
- fetchFn: params.fetchFn,
740
- fetchFnSupportsDispatcher: params.fetchFnSupportsDispatcher,
741
- resolveFn: params.resolveFn,
742
- logger: params.logger,
743
- deadline: params.deadline
705
+ preserveFilenames: params.preserveFilenames
744
706
  });
745
707
  if (!saved) return;
746
708
  return {
@@ -759,15 +721,7 @@ async function downloadMSTeamsBotFrameworkAttachment(params) {
759
721
  * reuse the existing logging path.
760
722
  */
761
723
  async function downloadMSTeamsBotFrameworkAttachments(params) {
762
- const seen = /* @__PURE__ */ new Set();
763
- const unique = [];
764
- for (const id of params.attachmentIds ?? []) {
765
- if (typeof id !== "string") continue;
766
- const trimmed = id.trim();
767
- if (!trimmed || seen.has(trimmed)) continue;
768
- seen.add(trimmed);
769
- unique.push(trimmed);
770
- }
724
+ const unique = (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeUniqueTrimmedStringList)(params.attachmentIds);
771
725
  if (unique.length === 0 || !params.serviceUrl || !params.tokenProvider) return {
772
726
  media: [],
773
727
  attachmentCount: unique.length
@@ -775,20 +729,8 @@ async function downloadMSTeamsBotFrameworkAttachments(params) {
775
729
  const media = [];
776
730
  for (const attachmentId of unique) try {
777
731
  const item = await downloadMSTeamsBotFrameworkAttachment({
778
- serviceUrl: params.serviceUrl,
779
- attachmentId,
780
- tokenProvider: params.tokenProvider,
781
- maxBytes: params.maxBytes,
782
- allowHosts: params.allowHosts,
783
- authAllowHosts: params.authAllowHosts,
784
- fetchFn: params.fetchFn,
785
- fetchFnSupportsDispatcher: params.fetchFnSupportsDispatcher,
786
- resolveFn: params.resolveFn,
787
- deadline: params.deadline,
788
- fileNameHint: params.fileNameHint,
789
- contentTypeHint: params.contentTypeHint,
790
- preserveFilenames: params.preserveFilenames,
791
- logger: params.logger
732
+ ...params,
733
+ attachmentId
792
734
  });
793
735
  if (item) media.push({
794
736
  ...item,
@@ -828,11 +770,9 @@ function extractMSTeamsHtmlAttachmentIds(attachments) {
828
770
  const html = require_resolve_allowlist.extractHtmlFromAttachment(att);
829
771
  if (!html) continue;
830
772
  require_resolve_allowlist.ATTACHMENT_TAG_RE.lastIndex = 0;
831
- let match = require_resolve_allowlist.ATTACHMENT_TAG_RE.exec(html);
832
- while (match) {
773
+ for (const match of html.matchAll(require_resolve_allowlist.ATTACHMENT_TAG_RE)) {
833
774
  const id = match[1]?.trim();
834
775
  if (id) ids.add(id);
835
- match = require_resolve_allowlist.ATTACHMENT_TAG_RE.exec(html);
836
776
  }
837
777
  }
838
778
  return Array.from(ids);
@@ -852,8 +792,7 @@ function summarizeMSTeamsHtmlAttachments(attachments) {
852
792
  if (!html) continue;
853
793
  htmlAttachments += 1;
854
794
  require_resolve_allowlist.IMG_SRC_RE.lastIndex = 0;
855
- let match = require_resolve_allowlist.IMG_SRC_RE.exec(html);
856
- while (match) {
795
+ for (const match of html.matchAll(require_resolve_allowlist.IMG_SRC_RE)) {
857
796
  imgTags += 1;
858
797
  const src = match[1]?.trim();
859
798
  if (src) {
@@ -861,15 +800,12 @@ function summarizeMSTeamsHtmlAttachments(attachments) {
861
800
  else if (src.startsWith("cid:")) cidImages += 1;
862
801
  else srcHosts.add(require_resolve_allowlist.safeHostForUrl(src));
863
802
  }
864
- match = require_resolve_allowlist.IMG_SRC_RE.exec(html);
865
803
  }
866
804
  require_resolve_allowlist.ATTACHMENT_TAG_RE.lastIndex = 0;
867
- let attachmentMatch = require_resolve_allowlist.ATTACHMENT_TAG_RE.exec(html);
868
- while (attachmentMatch) {
805
+ for (const attachmentMatch of html.matchAll(require_resolve_allowlist.ATTACHMENT_TAG_RE)) {
869
806
  attachmentTags += 1;
870
807
  const id = attachmentMatch[1]?.trim();
871
808
  if (id) attachmentIds.add(id);
872
- attachmentMatch = require_resolve_allowlist.ATTACHMENT_TAG_RE.exec(html);
873
809
  }
874
810
  }
875
811
  if (htmlAttachments === 0) return;
@@ -893,9 +829,10 @@ function resolveUnrepresentedHtmlAttachmentIds(attachments) {
893
829
  return extractMSTeamsHtmlAttachmentIds(attachments).filter((id) => !representedIds.has(id));
894
830
  }
895
831
  function createAdvertisedMediaFact(kind, sourceId) {
896
- const media = { kind };
897
- if (sourceId) media.sourceId = sourceId;
898
- return media;
832
+ return {
833
+ kind,
834
+ ...sourceId ? { sourceId } : {}
835
+ };
899
836
  }
900
837
  function resolveMSTeamsAdvertisedMedia(attachments) {
901
838
  const list = Array.isArray(attachments) ? attachments : [];
@@ -919,49 +856,28 @@ function resolveMSTeamsAdvertisedMedia(attachments) {
919
856
  * implementation. This lets Teams-specific auth fallback own the request
920
857
  * sequence while keeping redirect and DNS pinning inside `safeFetchWithPolicy`.
921
858
  */
922
- async function saveRemoteMediaDirect(params) {
859
+ async function downloadAndStoreMSTeamsRemoteMedia(params) {
923
860
  const response = await params.fetchImpl(params.url, { redirect: "follow" });
924
861
  try {
925
- return await (0, openclaw_plugin_sdk_media_runtime.saveResponseMedia)(response, {
862
+ const saved = await (0, openclaw_plugin_sdk_media_runtime.saveResponseMedia)(response, {
926
863
  sourceUrl: params.url,
927
864
  filePathHint: params.filePathHint,
928
865
  maxBytes: params.maxBytes,
929
866
  fallbackContentType: params.contentTypeHint,
930
- originalFilename: params.originalFilename
867
+ originalFilename: params.preserveFilenames ? params.filePathHint : void 0
931
868
  });
869
+ return {
870
+ path: saved.path,
871
+ contentType: saved.contentType,
872
+ kind: params.kind ?? require_resolve_allowlist.resolveMSTeamsMediaKind({
873
+ contentType: saved.contentType,
874
+ fileName: params.filePathHint
875
+ })
876
+ };
932
877
  } finally {
933
878
  await response.body?.cancel().catch(() => void 0);
934
879
  }
935
880
  }
936
- async function downloadAndStoreMSTeamsRemoteMedia(params) {
937
- const originalFilename = params.preserveFilenames ? params.filePathHint : void 0;
938
- let saved;
939
- if (params.useDirectFetch && params.fetchImpl) saved = await saveRemoteMediaDirect({
940
- url: params.url,
941
- filePathHint: params.filePathHint,
942
- fetchImpl: params.fetchImpl,
943
- maxBytes: params.maxBytes,
944
- contentTypeHint: params.contentTypeHint,
945
- originalFilename
946
- });
947
- else saved = await require_runtime.getMSTeamsRuntime().channel.media.saveRemoteMedia({
948
- url: params.url,
949
- fetchImpl: params.fetchImpl,
950
- filePathHint: params.filePathHint,
951
- maxBytes: params.maxBytes,
952
- ssrfPolicy: params.ssrfPolicy,
953
- fallbackContentType: params.contentTypeHint,
954
- originalFilename
955
- });
956
- return {
957
- path: saved.path,
958
- contentType: saved.contentType,
959
- kind: params.kind ?? require_resolve_allowlist.resolveMSTeamsMediaKind({
960
- contentType: saved.contentType,
961
- fileName: params.filePathHint
962
- })
963
- };
964
- }
965
881
  //#endregion
966
882
  //#region extensions/msteams/src/attachments/download.ts
967
883
  function withSourceId(media, sourceId) {
@@ -1021,22 +937,8 @@ function scopeCandidatesForUrl(url) {
1021
937
  }
1022
938
  function canonicalizeInlineBase64Payload(value) {
1023
939
  let cleaned = "";
1024
- let padding = 0;
1025
- let sawPadding = false;
1026
- for (let index = 0; index < value.length; index += 1) {
1027
- const code = value.charCodeAt(index);
1028
- if (code <= 32) continue;
1029
- if (code === 61) {
1030
- padding += 1;
1031
- if (padding > 2) return;
1032
- sawPadding = true;
1033
- cleaned += "=";
1034
- continue;
1035
- }
1036
- if (sawPadding || !(code >= 65 && code <= 90 || code >= 97 && code <= 122 || code >= 48 && code <= 57 || code === 43 || code === 47)) return;
1037
- cleaned += value[index];
1038
- }
1039
- return cleaned && cleaned.length % 4 === 0 ? cleaned : void 0;
940
+ for (const char of value) if (char.charCodeAt(0) > 32) cleaned += char;
941
+ return cleaned.length % 4 === 0 && /^[A-Za-z0-9+/]+={0,2}$/.test(cleaned) ? cleaned : void 0;
1040
942
  }
1041
943
  function decodeInlineDataImage(src, maxBytes, totalBytes) {
1042
944
  const match = /^data:(image\/[a-z0-9.+-]+)?(;base64)?,(.*)$/i.exec(src);
@@ -1079,7 +981,7 @@ async function fetchWithAuthFallback(params) {
1079
981
  if (!params.tokenProvider) return firstAttempt;
1080
982
  const tokenProvider = params.tokenProvider;
1081
983
  if (firstAttempt.status !== 401 && firstAttempt.status !== 403) return firstAttempt;
1082
- if (!require_resolve_allowlist.isUrlAllowed(params.url, params.policy.authAllowHosts)) return firstAttempt;
984
+ if (!(0, openclaw_plugin_sdk_ssrf_policy.isHttpsUrlAllowedByHostnameSuffixAllowlist)(params.url, params.policy.authAllowHosts)) return firstAttempt;
1083
985
  let fallbackAttempt = firstAttempt;
1084
986
  const scopes = scopeCandidatesForUrl(params.url);
1085
987
  const fetchFn = params.fetchFn ?? fetch;
@@ -1109,10 +1011,6 @@ async function fetchWithAuthFallback(params) {
1109
1011
  } catch {}
1110
1012
  return fallbackAttempt;
1111
1013
  }
1112
- /**
1113
- * Download all file attachments from a Teams message (images, documents, etc.).
1114
- * Renamed from downloadMSTeamsImageAttachments to support all file types.
1115
- */
1116
1014
  async function downloadMSTeamsAttachments(params) {
1117
1015
  const list = Array.isArray(params.attachments) ? params.attachments : [];
1118
1016
  if (list.length === 0) return [];
@@ -1121,7 +1019,6 @@ async function downloadMSTeamsAttachments(params) {
1121
1019
  authAllowHosts: params.authAllowHosts
1122
1020
  });
1123
1021
  const allowHosts = policy.allowHosts;
1124
- const ssrfPolicy = require_resolve_allowlist.resolveMediaSsrfPolicy(allowHosts);
1125
1022
  const candidates = list.filter(require_resolve_allowlist.isAdvertisedFileAttachment).map((attachment) => {
1126
1023
  return (require_resolve_allowlist.isDownloadableAttachment(attachment) ? resolveDownloadCandidate(attachment) : null) ?? {
1127
1024
  kind: "unavailable",
@@ -1192,7 +1089,7 @@ async function downloadMSTeamsAttachments(params) {
1192
1089
  }
1193
1090
  continue;
1194
1091
  }
1195
- if (!require_resolve_allowlist.isUrlAllowed(candidate.url, allowHosts)) {
1092
+ if (!(0, openclaw_plugin_sdk_ssrf_policy.isHttpsUrlAllowedByHostnameSuffixAllowlist)(candidate.url, allowHosts)) {
1196
1093
  out.push(withSourceId({ kind: candidate.mediaKind }, candidate.sourceId));
1197
1094
  continue;
1198
1095
  }
@@ -1204,8 +1101,6 @@ async function downloadMSTeamsAttachments(params) {
1204
1101
  contentTypeHint: candidate.contentTypeHint,
1205
1102
  kind: candidate.mediaKind,
1206
1103
  preserveFilenames: params.preserveFilenames,
1207
- ssrfPolicy,
1208
- useDirectFetch: true,
1209
1104
  fetchImpl: (input, init) => fetchWithAuthFallback({
1210
1105
  url: require_resolve_allowlist.resolveRequestUrl(input),
1211
1106
  tokenProvider: params.tokenProvider,
@@ -1308,10 +1203,6 @@ function normalizeGraphAttachment(att) {
1308
1203
  content
1309
1204
  };
1310
1205
  }
1311
- /**
1312
- * Download all hosted content from a Teams message (images, documents, etc.).
1313
- * Renamed from downloadGraphHostedImages to support all file types.
1314
- */
1315
1206
  async function downloadGraphHostedContent(params) {
1316
1207
  let hosted;
1317
1208
  try {
@@ -1389,7 +1280,7 @@ async function downloadMSTeamsGraphMedia(params) {
1389
1280
  allowHosts: params.allowHosts,
1390
1281
  authAllowHosts: params.authAllowHosts
1391
1282
  });
1392
- const ssrfPolicy = require_resolve_allowlist.resolveMediaSsrfPolicy(policy.allowHosts);
1283
+ const ssrfPolicy = (0, openclaw_plugin_sdk_ssrf_policy.buildHostnameAllowlistPolicyFromSuffixAllowlist)(policy.allowHosts);
1393
1284
  const messageUrl = params.messageUrl;
1394
1285
  let accessToken;
1395
1286
  try {
@@ -1476,7 +1367,7 @@ async function downloadMSTeamsGraphMedia(params) {
1476
1367
  downloadedReferenceUrls.add(shareUrl);
1477
1368
  try {
1478
1369
  const sharesUrl = `${require_resolve_allowlist.GRAPH_ROOT}/shares/${require_resolve_allowlist.encodeGraphShareId(shareUrl)}/driveItem/content`;
1479
- if (!require_resolve_allowlist.isUrlAllowed(sharesUrl, policy.allowHosts)) {
1370
+ if (!(0, openclaw_plugin_sdk_ssrf_policy.isHttpsUrlAllowedByHostnameSuffixAllowlist)(sharesUrl, policy.allowHosts)) {
1480
1371
  params.logger?.debug?.("graph media sharepoint url not in allowHosts", {
1481
1372
  messageUrl,
1482
1373
  sharesUrl
@@ -1490,8 +1381,6 @@ async function downloadMSTeamsGraphMedia(params) {
1490
1381
  maxBytes: params.maxBytes,
1491
1382
  contentTypeHint: "application/octet-stream",
1492
1383
  preserveFilenames: params.preserveFilenames,
1493
- ssrfPolicy,
1494
- useDirectFetch: true,
1495
1384
  fetchImpl: async (input, init) => {
1496
1385
  const requestUrl = require_resolve_allowlist.resolveRequestUrl(input);
1497
1386
  const headers = require_resolve_allowlist.ensureUserAgentHeader(init?.headers);
@@ -1532,7 +1421,6 @@ async function downloadMSTeamsGraphMedia(params) {
1532
1421
  messageUrl,
1533
1422
  maxBytes: params.maxBytes,
1534
1423
  fetchFn: params.fetchFn,
1535
- preserveFilenames: params.preserveFilenames,
1536
1424
  ssrfPolicy,
1537
1425
  logger: params.logger,
1538
1426
  deadline: params.deadline
@@ -1875,7 +1763,7 @@ function createTeamsReplyStreamController(params) {
1875
1763
  } : payload;
1876
1764
  };
1877
1765
  const finalStreamActivity = (text) => ({
1878
- ...require_messenger.buildMSTeamsMessageActivity(text === void 0 ? void 0 : require_messenger.formatMSTeamsMarkdown(text, params.tableMode ?? "code")),
1766
+ ...require_conversation_store_state.buildMSTeamsMessageActivity(text === void 0 ? void 0 : require_conversation_store_state.formatMSTeamsMarkdown(text, params.tableMode ?? "code")),
1879
1767
  channelData: params.feedbackLoopEnabled ? { feedbackLoopEnabled: true } : {}
1880
1768
  });
1881
1769
  const deferredReplacementLogicalContent = () => {
@@ -1895,7 +1783,21 @@ function createTeamsReplyStreamController(params) {
1895
1783
  deferredReplacementEntries = [];
1896
1784
  return payloads;
1897
1785
  };
1786
+ const finalizeWithoutReceipt = (logicalContent, canceled = false) => {
1787
+ const pending = pendingFinalPayload;
1788
+ pendingFinalPayload = void 0;
1789
+ const fallback = pending && !canceled ? fallbackPayloadAfterAcknowledgedText(pending) : void 0;
1790
+ const postNativePayloads = replacementSettlementPending && !canceled ? takeDeferredReplacementPayloads(fallback) : [];
1791
+ if (canceled) deferredReplacementEntries = [];
1792
+ return {
1793
+ ...acknowledgedNativeDelivery(),
1794
+ ...!canceled && logicalContent ? { logicalContent } : {},
1795
+ ...!replacementSettlementPending && fallback ? { fallbackPayload: fallback } : {},
1796
+ ...postNativePayloads.length > 0 ? { postNativePayloads } : {}
1797
+ };
1798
+ };
1898
1799
  const progressDraft = (0, openclaw_plugin_sdk_channel_outbound.createChannelProgressDraftCompositor)({
1800
+ preparedItems: true,
1899
1801
  formatPlainText: (text) => text,
1900
1802
  entry: params.msteamsConfig,
1901
1803
  mode: streamMode,
@@ -1914,7 +1816,6 @@ function createTeamsReplyStreamController(params) {
1914
1816
  }
1915
1817
  });
1916
1818
  return {
1917
- async onReplyStart() {},
1918
1819
  onPartialReply(payload) {
1919
1820
  if (!stream || !payload.text || wasCanceled() || streamMode !== "partial") return;
1920
1821
  if (replacementSettlementPending && replacementFinalPending) {
@@ -1954,36 +1855,18 @@ function createTeamsReplyStreamController(params) {
1954
1855
  params.log?.warn?.(`msteams stream emit failed, falling back to block delivery: ${(0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err)}`);
1955
1856
  }
1956
1857
  },
1957
- async noteProgressWork(options) {
1958
- if (options?.toolName !== void 0 && !(0, openclaw_plugin_sdk_channel_outbound.isChannelProgressDraftWorkToolName)(options.toolName)) return;
1959
- await progressDraft.noteActivity();
1960
- },
1961
- async pushProgressLine(line, options) {
1962
- await progressDraft.pushToolProgress(line, options);
1963
- },
1964
- async pushReasoningProgress(text, options) {
1965
- await progressDraft.pushReasoningProgress(text, options);
1966
- },
1967
- resetReasoningProgress: () => progressDraft.resetReasoningProgress(),
1968
- async pushApprovalEvent(payload) {
1969
- await progressDraft.pushApprovalEvent(payload);
1970
- },
1971
- async pushPlanProgress(steps, options) {
1972
- await progressDraft.pushPlanProgress(steps, options);
1973
- },
1858
+ pushItemEvent: progressDraft.pushItemEvent.bind(progressDraft),
1859
+ pushToolEvent: progressDraft.pushToolEvent,
1860
+ pushReasoningProgress: progressDraft.pushReasoningProgress.bind(progressDraft),
1861
+ resetReasoningProgress: progressDraft.resetReasoningProgress,
1862
+ pushApprovalEvent: progressDraft.pushApprovalEvent.bind(progressDraft),
1863
+ pushPlanProgress: progressDraft.pushPlanProgress.bind(progressDraft),
1974
1864
  preparePayload(payload) {
1975
1865
  if (!stream) return payload;
1976
1866
  if (wasCanceled()) return;
1977
1867
  if (payload.text) progressDraft.markFinalReplyStarted();
1978
1868
  if (replacementSettlementPending) {
1979
- if (!replacementFinalPending) {
1980
- deferredReplacementEntries.push({
1981
- kind: "payload",
1982
- payload
1983
- });
1984
- return;
1985
- }
1986
- if (!payload.text) {
1869
+ if (!replacementFinalPending || !payload.text) {
1987
1870
  deferredReplacementEntries.push({
1988
1871
  kind: "payload",
1989
1872
  payload
@@ -2108,24 +1991,10 @@ function createTeamsReplyStreamController(params) {
2108
1991
  }
2109
1992
  const result = await stream.close();
2110
1993
  streamFinalizationPending = false;
2111
- if (!result) {
2112
- const fallback = pendingFinalPayload;
2113
- pendingFinalPayload = void 0;
2114
- const canceled = wasCanceled();
2115
- const replacementFallback = replacementSettlementPending && fallback && !canceled ? fallbackPayloadAfterAcknowledgedText(fallback) : void 0;
2116
- const fallbackPayload = !replacementSettlementPending && fallback && !canceled ? fallbackPayloadAfterAcknowledgedText(fallback) : void 0;
2117
- const postNativePayloads = replacementSettlementPending && !canceled ? takeDeferredReplacementPayloads(replacementFallback) : [];
2118
- if (canceled) deferredReplacementEntries = [];
2119
- return {
2120
- ...acknowledgedNativeDelivery(),
2121
- ...!canceled && logicalContent ? { logicalContent } : {},
2122
- ...fallbackPayload ? { fallbackPayload } : {},
2123
- ...postNativePayloads.length > 0 ? { postNativePayloads } : {}
2124
- };
2125
- }
1994
+ if (!result) return finalizeWithoutReceipt(logicalContent, wasCanceled());
2126
1995
  const replacementFallback = replacementSettlementPending && replacementEmitFailed && pendingFinalPayload ? fallbackPayloadAfterAcknowledgedText(pendingFinalPayload) : void 0;
2127
1996
  pendingFinalPayload = void 0;
2128
- const messageId = require_messenger.extractMessageId(result) ?? acknowledgedStreamId;
1997
+ const messageId = require_conversation_store_state.extractMessageId(result) ?? acknowledgedStreamId;
2129
1998
  const postNativePayloads = replacementSettlementPending ? takeDeferredReplacementPayloads(replacementFallback) : [];
2130
1999
  const nativeContent = replacementEmitFailed ? acknowledgedText || void 0 : content;
2131
2000
  return {
@@ -2146,17 +2015,7 @@ function createTeamsReplyStreamController(params) {
2146
2015
  streamFailed = true;
2147
2016
  streamFinalizationPending = false;
2148
2017
  params.log?.warn?.(`msteams stream finalize failed: ${(0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err)}`);
2149
- const fallback = pendingFinalPayload;
2150
- pendingFinalPayload = void 0;
2151
- const replacementFallback = replacementSettlementPending && fallback ? fallbackPayloadAfterAcknowledgedText(fallback) : void 0;
2152
- const fallbackPayload = !replacementSettlementPending && fallback ? fallbackPayloadAfterAcknowledgedText(fallback) : void 0;
2153
- const postNativePayloads = replacementSettlementPending ? takeDeferredReplacementPayloads(replacementFallback) : [];
2154
- return {
2155
- ...acknowledgedNativeDelivery(),
2156
- ...logicalContent ? { logicalContent } : {},
2157
- ...fallbackPayload ? { fallbackPayload } : {},
2158
- ...postNativePayloads.length > 0 ? { postNativePayloads } : {}
2159
- };
2018
+ return finalizeWithoutReceipt(logicalContent);
2160
2019
  } finally {
2161
2020
  failedSegmentFallbackPrepared = true;
2162
2021
  queuedFinalActivity = void 0;
@@ -2184,43 +2043,22 @@ function createMSTeamsReplyDispatcher(params) {
2184
2043
  const msteamsCfg = params.cfg.channels?.msteams;
2185
2044
  const conversationType = (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeOptionalLowercaseString)(params.conversationRef.conversation?.conversationType);
2186
2045
  const isTypingSupported = conversationType === "personal" || conversationType === "groupchat";
2187
- /**
2188
- * Keepalive cadence for the typing indicator while the bot is running
2189
- * (including long tool chains). Bot Framework 1:1 TurnContext proxies
2190
- * expire after ~30s of inactivity; sending a typing activity every 8s
2191
- * keeps the proxy alive so the post-tool reply can still land via the
2192
- * turn context. Sits in the middle of the 5-10s range recommended in
2193
- * #59731.
2194
- */
2195
2046
  const TYPING_KEEPALIVE_INTERVAL_MS = 8e3;
2196
- /**
2197
- * TTL ceiling for the typing keepalive loop. The default in
2198
- * createTypingCallbacks is 60s, which is too short for the Teams long tool
2199
- * chains described in #59731 (60s+ total runs are common). Give tool
2200
- * chains up to 10 minutes before auto-stopping the keepalive.
2201
- */
2202
2047
  const TYPING_KEEPALIVE_MAX_DURATION_MS = 6e5;
2203
- const streamActiveRef = { current: () => false };
2204
- const streamCanceledRef = { current: () => false };
2205
- const rawSendTypingIndicator = async () => {
2206
- await require_messenger.withRevokedProxyFallback({
2048
+ const sendTypingIndicator = async () => {
2049
+ if (!isTypingSupported || streamController.isStreamActive() || streamController.wasCanceled()) return;
2050
+ await require_conversation_store_state.withRevokedProxyFallback({
2207
2051
  run: async () => {
2208
2052
  await params.context.sendActivity({ type: "typing" });
2209
2053
  },
2210
2054
  onRevoked: async () => {
2211
- const baseRef = require_messenger.buildConversationReference(params.conversationRef);
2212
- await require_messenger.sendMSTeamsActivityWithReference(params.app, baseRef, { type: "typing" }, { serviceUrlBoundary: require_resolve_allowlist.resolveMSTeamsSdkCloudOptions(msteamsCfg) });
2055
+ await require_conversation_store_state.sendMSTeamsActivityWithReference(params.app, require_conversation_store_state.buildConversationReference(params.conversationRef), { type: "typing" }, { serviceUrlBoundary: require_resolve_allowlist.resolveMSTeamsSdkCloudOptions(msteamsCfg) });
2213
2056
  },
2214
2057
  onRevokedLog: () => {
2215
2058
  params.log.debug?.("turn context revoked, sending typing via proactive messaging");
2216
2059
  }
2217
2060
  });
2218
2061
  };
2219
- const sendTypingIndicator = isTypingSupported ? async () => {
2220
- if (streamActiveRef.current()) return;
2221
- if (streamCanceledRef.current()) return;
2222
- await rawSendTypingIndicator();
2223
- } : async () => {};
2224
2062
  const { onModelSelected, typingCallbacks, ...replyPipeline } = (0, openclaw_plugin_sdk_channel_outbound.createChannelMessageReplyPipeline)({
2225
2063
  cfg: params.cfg,
2226
2064
  agentId: params.agentId,
@@ -2261,8 +2099,6 @@ function createMSTeamsReplyDispatcher(params) {
2261
2099
  msteamsConfig: msteamsCfg,
2262
2100
  progressSeed: `${params.accountId ?? "default"}:${params.conversationRef.conversation?.id ?? ""}`
2263
2101
  });
2264
- streamActiveRef.current = () => streamController.isStreamActive();
2265
- streamCanceledRef.current = () => streamController.wasCanceled();
2266
2102
  const teamsStreamMode = (0, openclaw_plugin_sdk_channel_outbound.resolveChannelPreviewStreamMode)(msteamsCfg, "partial");
2267
2103
  const blockStreamingResolved = teamsStreamMode === "block" ? true : (0, openclaw_plugin_sdk_channel_outbound.resolveChannelStreamingBlockEnabled)(msteamsCfg);
2268
2104
  const blockStreamingEnabled = blockStreamingResolved ?? false;
@@ -2272,7 +2108,7 @@ function createMSTeamsReplyDispatcher(params) {
2272
2108
  const findPendingNativeDelivery = () => pendingDeliveries.find((candidate) => candidate.native && !candidate.nativeSettled);
2273
2109
  const joinAcceptedContents = (contents) => contents.filter((content) => Boolean(content)).join("\n");
2274
2110
  const sendMessages = async (messages) => {
2275
- return require_messenger.sendMSTeamsMessages({
2111
+ return require_conversation_store_state.sendMSTeamsMessages({
2276
2112
  replyStyle: params.replyStyle,
2277
2113
  app: params.app,
2278
2114
  appId: params.appId,
@@ -2311,7 +2147,7 @@ function createMSTeamsReplyDispatcher(params) {
2311
2147
  });
2312
2148
  };
2313
2149
  const renderReplyPayload = (payload) => {
2314
- return require_messenger.renderReplyPayloadsToMessages([payload], {
2150
+ return require_conversation_store_state.renderReplyPayloadsToMessages([payload], {
2315
2151
  textChunkLimit: params.textLimit,
2316
2152
  chunkText: true,
2317
2153
  mediaMode: "split",
@@ -2404,7 +2240,6 @@ function createMSTeamsReplyDispatcher(params) {
2404
2240
  ...replyPipeline,
2405
2241
  humanDelay: (0, openclaw_plugin_sdk_agent_runtime.resolveHumanDelayConfig)(params.cfg, params.agentId),
2406
2242
  onReplyStart: async () => {
2407
- await streamController.onReplyStart();
2408
2243
  if (typingIndicatorEnabled) await typingCallbacks?.onReplyStart?.();
2409
2244
  },
2410
2245
  typingCallbacks
@@ -2492,35 +2327,8 @@ function createMSTeamsReplyDispatcher(params) {
2492
2327
  streamController.resetReasoningProgress();
2493
2328
  return false;
2494
2329
  },
2495
- onToolStart: async (payload) => {
2496
- const name = typeof payload?.name === "string" ? payload.name : void 0;
2497
- const detailMode = typeof payload?.detailMode === "string" ? payload.detailMode : void 0;
2498
- await streamController.pushProgressLine((0, openclaw_plugin_sdk_channel_outbound.buildChannelProgressDraftLineForEntry)(msteamsCfg, {
2499
- event: "tool",
2500
- ...typeof payload?.itemId === "string" ? { itemId: payload.itemId } : {},
2501
- ...typeof payload?.toolCallId === "string" ? { toolCallId: payload.toolCallId } : {},
2502
- ...name ? { name } : {},
2503
- ...typeof payload?.phase === "string" ? { phase: payload.phase } : {},
2504
- ...payload?.args && typeof payload.args === "object" ? { args: payload.args } : {}
2505
- }, detailMode === "explain" || detailMode === "raw" ? { detailMode } : void 0), name ? { toolName: name } : void 0);
2506
- return false;
2507
- },
2508
- onItemEvent: async (payload) => {
2509
- await streamController.pushProgressLine((0, openclaw_plugin_sdk_channel_outbound.buildChannelProgressDraftLineForEntry)(msteamsCfg, {
2510
- event: "item",
2511
- ...typeof payload?.itemId === "string" ? { itemId: payload.itemId } : {},
2512
- ...typeof payload?.toolCallId === "string" ? { toolCallId: payload.toolCallId } : {},
2513
- ...typeof payload?.kind === "string" ? { itemKind: payload.kind } : {},
2514
- ...typeof payload?.title === "string" ? { title: payload.title } : {},
2515
- ...typeof payload?.name === "string" ? { name: payload.name } : {},
2516
- ...typeof payload?.phase === "string" ? { phase: payload.phase } : {},
2517
- ...typeof payload?.status === "string" ? { status: payload.status } : {},
2518
- ...typeof payload?.summary === "string" ? { summary: payload.summary } : {},
2519
- ...typeof payload?.progressText === "string" ? { progressText: payload.progressText } : {},
2520
- ...typeof payload?.meta === "string" ? { meta: payload.meta } : {}
2521
- }));
2522
- return false;
2523
- },
2330
+ onToolStart: streamController.pushToolEvent,
2331
+ onItemEvent: streamController.pushItemEvent,
2524
2332
  onPlanUpdate: async (payload) => {
2525
2333
  if (payload?.phase !== "update") return false;
2526
2334
  await streamController.pushPlanProgress((0, openclaw_plugin_sdk_channel_outbound.normalizeAgentPlanSteps)(payload.steps), {
@@ -2539,36 +2347,6 @@ function createMSTeamsReplyDispatcher(params) {
2539
2347
  ...typeof payload?.message === "string" ? { message: payload.message } : {}
2540
2348
  });
2541
2349
  return false;
2542
- },
2543
- onCommandOutput: async (payload) => {
2544
- if (payload?.phase !== "end") return false;
2545
- await streamController.pushProgressLine((0, openclaw_plugin_sdk_channel_outbound.buildChannelProgressDraftLineForEntry)(msteamsCfg, {
2546
- event: "command-output",
2547
- ...typeof payload?.itemId === "string" ? { itemId: payload.itemId } : {},
2548
- ...typeof payload?.toolCallId === "string" ? { toolCallId: payload.toolCallId } : {},
2549
- phase: payload.phase,
2550
- ...typeof payload?.title === "string" ? { title: payload.title } : {},
2551
- ...typeof payload?.name === "string" ? { name: payload.name } : {},
2552
- ...typeof payload?.status === "string" ? { status: payload.status } : {},
2553
- ...typeof payload?.exitCode === "number" ? { exitCode: payload.exitCode } : {}
2554
- }));
2555
- return false;
2556
- },
2557
- onPatchSummary: async (payload) => {
2558
- if (payload?.phase !== "end") return false;
2559
- await streamController.pushProgressLine((0, openclaw_plugin_sdk_channel_outbound.buildChannelProgressDraftLine)({
2560
- event: "patch",
2561
- ...typeof payload?.itemId === "string" ? { itemId: payload.itemId } : {},
2562
- ...typeof payload?.toolCallId === "string" ? { toolCallId: payload.toolCallId } : {},
2563
- phase: payload.phase,
2564
- ...typeof payload?.title === "string" ? { title: payload.title } : {},
2565
- ...typeof payload?.name === "string" ? { name: payload.name } : {},
2566
- ...Array.isArray(payload?.added) && payload.added.every((s) => typeof s === "string") ? { added: payload.added } : {},
2567
- ...Array.isArray(payload?.modified) && payload.modified.every((s) => typeof s === "string") ? { modified: payload.modified } : {},
2568
- ...Array.isArray(payload?.deleted) && payload.deleted.every((s) => typeof s === "string") ? { deleted: payload.deleted } : {},
2569
- ...typeof payload?.summary === "string" ? { summary: payload.summary } : {}
2570
- }));
2571
- return false;
2572
2350
  }
2573
2351
  } : {};
2574
2352
  return {
@@ -2578,6 +2356,8 @@ function createMSTeamsReplyDispatcher(params) {
2578
2356
  },
2579
2357
  delivery,
2580
2358
  replyOptions: {
2359
+ progressPreambleEnabled: shouldSuppressDefaultToolProgressMessages,
2360
+ commentaryProgressEnabled: shouldSuppressDefaultToolProgressMessages,
2581
2361
  ...streamController.hasStream() ? { onPartialReply: (payload) => {
2582
2362
  streamController.onPartialReply(payload);
2583
2363
  return false;
@@ -2590,32 +2370,6 @@ function createMSTeamsReplyDispatcher(params) {
2590
2370
  };
2591
2371
  }
2592
2372
  //#endregion
2593
- //#region extensions/msteams/src/sent-message-cache.ts
2594
- const sentMessages = (0, openclaw_plugin_sdk_dedupe_runtime.createPersistentDedupeCache)({
2595
- globalKey: Symbol.for("openclaw.msteamsSentMessages"),
2596
- ttlMs: 864e5,
2597
- maxSize: 2e4,
2598
- persistent: {
2599
- namespace: "msteams.sent-messages",
2600
- maxEntries: 1e3,
2601
- openStore: (options) => require_runtime.getOptionalMSTeamsRuntime()?.state.openKeyedStore(options),
2602
- logError: (0, openclaw_plugin_sdk_plugin_state_runtime.createPluginStateErrorReporter)(require_runtime.getOptionalMSTeamsRuntime, "msteams", "sent-message-state", "Microsoft Teams persistent sent-message state failed"),
2603
- readTimestamp: (record) => record.sentAt
2604
- }
2605
- });
2606
- function makeKey(conversationId, messageId) {
2607
- return `${conversationId}:${messageId}`;
2608
- }
2609
- function recordMSTeamsSentMessage(conversationId, messageId) {
2610
- if (!conversationId || !messageId) return;
2611
- const sentAt = Date.now();
2612
- sentMessages.register(makeKey(conversationId, messageId), { sentAt }, { at: sentAt });
2613
- }
2614
- async function wasMSTeamsMessageSentWithPersistence(params) {
2615
- if (!params.conversationId || !params.messageId) return false;
2616
- return await sentMessages.lookup(makeKey(params.conversationId, params.messageId));
2617
- }
2618
- //#endregion
2619
2373
  //#region extensions/msteams/src/monitor-handler/inbound-dispatch.ts
2620
2374
  async function dispatchMSTeamsInboundTurn(params) {
2621
2375
  const core = require_runtime.getMSTeamsRuntime();
@@ -2640,7 +2394,7 @@ async function dispatchMSTeamsInboundTurn(params) {
2640
2394
  body: agentBody
2641
2395
  });
2642
2396
  const isRoomish = !isDirectMessage;
2643
- const historyKey = isRoomish ? conversationId : void 0;
2397
+ const historyKey = isRoomish ? facts.historyKey : void 0;
2644
2398
  if (isRoomish && historyKey) combinedBody = (0, openclaw_plugin_sdk_reply_history.createChannelHistoryWindow)({ historyMap: conversationHistories }).buildPendingContext({
2645
2399
  historyKey,
2646
2400
  limit: historyLimit,
@@ -2662,7 +2416,7 @@ async function dispatchMSTeamsInboundTurn(params) {
2662
2416
  isGroup: !isDirectMessage,
2663
2417
  groupPolicy,
2664
2418
  allowFrom: effectiveGroupAllowFrom,
2665
- isSenderAllowed: (allowFrom) => require_channel.resolveMSTeamsAllowlistMatch({
2419
+ isSenderAllowed: (allowFrom) => (0, openclaw_plugin_sdk_allow_from.resolveAllowlistMatchSimple)({
2666
2420
  allowFrom,
2667
2421
  senderId: quoteSenderId ?? "",
2668
2422
  senderName: quoteSenderName,
@@ -2770,7 +2524,7 @@ async function dispatchMSTeamsInboundTurn(params) {
2770
2524
  replyStyle,
2771
2525
  textLimit,
2772
2526
  onSentMessageIds: (ids) => {
2773
- for (const id of ids) recordMSTeamsSentMessage(conversationId, id);
2527
+ for (const id of ids) require_conversation_store_state.recordMSTeamsSentMessage(conversationId, id);
2774
2528
  },
2775
2529
  tokenProvider,
2776
2530
  sharePointSiteId: cfg.channels?.msteams?.sharePointSiteId
@@ -2864,7 +2618,7 @@ async function prepareMSTeamsDebounceEntry(params) {
2864
2618
  const text = rawText ? require_resolve_allowlist.stripMSTeamsMentionTags(rawText) : extractTextFromHtmlAttachments(attachments) || require_resolve_allowlist.stripMSTeamsMentionTags(serializeMSTeamsAdaptiveCardActionValue(activity.value) || "");
2865
2619
  const conversationId = require_resolve_allowlist.normalizeMSTeamsConversationId(activity.conversation?.id ?? "");
2866
2620
  const replyToId = activity.replyToId ?? void 0;
2867
- const implicitMentionKinds = conversationId && replyToId && await wasMSTeamsMessageSentWithPersistence({
2621
+ const implicitMentionKinds = conversationId && replyToId && await require_conversation_store_state.wasMSTeamsMessageSentWithPersistence({
2868
2622
  conversationId,
2869
2623
  messageId: replyToId
2870
2624
  }) ? ["reply_to_bot"] : [];
@@ -2885,7 +2639,7 @@ function buildStoredConversationReference(params) {
2885
2639
  const clientInfo = activity.entities?.find((entity) => entity.type === "clientInfo");
2886
2640
  const tenantId = activity.channelData?.tenant?.id ?? conversation?.tenantId;
2887
2641
  const aadObjectId = from?.aadObjectId;
2888
- const serviceUrl = require_resolve_allowlist.tryNormalizeBotFrameworkServiceUrl(activity.serviceUrl);
2642
+ const serviceUrl = require_bot_framework_service_url.tryNormalizeBotFrameworkServiceUrl(activity.serviceUrl);
2889
2643
  return {
2890
2644
  activityId: activity.id,
2891
2645
  user: from ? {
@@ -2938,6 +2692,7 @@ function assembleMSTeamsInboundFacts(entry) {
2938
2692
  teamId,
2939
2693
  graphChannelId: activity.channelData?.channel?.id?.trim() || conversationId,
2940
2694
  threadId,
2695
+ historyKey: threadId ? `${conversationId}:thread:${threadId}` : conversationId,
2941
2696
  conversationRef: buildStoredConversationReference({
2942
2697
  activity,
2943
2698
  conversationId,
@@ -2983,12 +2738,9 @@ const parentCache = /* @__PURE__ */ new Map();
2983
2738
  const INJECTED_MAX = 200;
2984
2739
  const injectedParents = /* @__PURE__ */ new Map();
2985
2740
  function touchLru(map, key, value, max) {
2986
- if (map.has(key)) map.delete(key);
2987
- else if (map.size >= max) {
2988
- const firstKey = map.keys().next().value;
2989
- if (firstKey !== void 0) map.delete(firstKey);
2990
- }
2741
+ map.delete(key);
2991
2742
  map.set(key, value);
2743
+ (0, openclaw_plugin_sdk_collection_runtime.pruneMapToMaxSize)(map, max);
2992
2744
  }
2993
2745
  function buildParentCacheKey(groupId, channelId, parentId) {
2994
2746
  return `${groupId}\u0000${channelId}\u0000${parentId}`;
@@ -2997,12 +2749,6 @@ function resolveParentCacheExpiresAt(nowRaw) {
2997
2749
  const nowMs = (0, openclaw_plugin_sdk_number_runtime.asDateTimestampMs)(nowRaw);
2998
2750
  return nowMs === void 0 ? void 0 : (0, openclaw_plugin_sdk_number_runtime.resolveExpiresAtMsFromDurationMs)(PARENT_CACHE_TTL_MS, { nowMs });
2999
2751
  }
3000
- /**
3001
- * Fetch a channel parent message with an LRU+TTL cache.
3002
- *
3003
- * Uses the injected `fetchParent` (defaults to `fetchChannelMessage`) so
3004
- * tests can swap in a stub without mocking the Graph transport.
3005
- */
3006
2752
  async function fetchParentMessageCached(token, groupId, channelId, parentId, fetchParent = require_reaction_types.fetchChannelMessage) {
3007
2753
  const key = buildParentCacheKey(groupId, channelId, parentId);
3008
2754
  const now = (0, openclaw_plugin_sdk_number_runtime.asDateTimestampMs)(Date.now());
@@ -3023,11 +2769,6 @@ async function fetchParentMessageCached(token, groupId, channelId, parentId, fet
3023
2769
  return message;
3024
2770
  }
3025
2771
  const PARENT_TEXT_MAX_CHARS = 400;
3026
- /**
3027
- * Extract a compact summary (sender + plain-text body) from a Graph parent
3028
- * message. Returns undefined when the parent cannot be summarized (missing
3029
- * or blank body).
3030
- */
3031
2772
  function summarizeParentMessage(message) {
3032
2773
  if (!message) return;
3033
2774
  const sender = message.from?.user?.displayName ?? message.from?.application?.displayName ?? "unknown";
@@ -3040,28 +2781,12 @@ function summarizeParentMessage(message) {
3040
2781
  text: text.length > PARENT_TEXT_MAX_CHARS ? `${(0, openclaw_plugin_sdk_text_utility_runtime.truncateUtf16Safe)(text, 399)}…` : text
3041
2782
  };
3042
2783
  }
3043
- /**
3044
- * Build the single-line `Replying to @sender: body` system event text.
3045
- * Callers should pass this text to `enqueueSystemEvent` together with a
3046
- * stable contextKey derived from the parent id.
3047
- */
3048
2784
  function formatParentContextEvent(summary) {
3049
2785
  return `Replying to @${summary.sender}: ${summary.text}`;
3050
2786
  }
3051
- /**
3052
- * Decide whether a parent context event should be enqueued for the current
3053
- * session. Returns `false` when we already injected the same parent for this
3054
- * session recently (prevents re-prepending identical context on every reply
3055
- * in the thread).
3056
- */
3057
2787
  function shouldInjectParentContext(sessionKey, parentId) {
3058
- const key = sessionKey;
3059
- return injectedParents.get(key) !== parentId;
2788
+ return injectedParents.get(sessionKey) !== parentId;
3060
2789
  }
3061
- /**
3062
- * Record that `parentId` was just injected for `sessionKey` so subsequent
3063
- * replies with the same parent can short-circuit via `shouldInjectParentContext`.
3064
- */
3065
2790
  function markParentContextInjected(sessionKey, parentId) {
3066
2791
  touchLru(injectedParents, sessionKey, parentId, INJECTED_MAX);
3067
2792
  }
@@ -3155,7 +2880,7 @@ async function resolveMSTeamsThreadContext(params) {
3155
2880
  isGroup: params.isChannel,
3156
2881
  groupPolicy: params.groupPolicy,
3157
2882
  allowFrom: params.effectiveGroupAllowFrom,
3158
- isSenderAllowed: (allowFrom) => require_channel.resolveMSTeamsAllowlistMatch({
2883
+ isSenderAllowed: (allowFrom) => (0, openclaw_plugin_sdk_allow_from.resolveAllowlistMatchSimple)({
3159
2884
  allowFrom,
3160
2885
  senderId: message.from?.user?.id ?? "",
3161
2886
  senderName: message.from?.user?.displayName,
@@ -3211,7 +2936,7 @@ function createMSTeamsMessageHandler(deps) {
3211
2936
  cfg,
3212
2937
  channel: "msteams"
3213
2938
  });
3214
- const historyLimit = Math.max(0, msteamsCfg?.historyLimit ?? cfg.messages?.groupChat?.historyLimit ?? openclaw_plugin_sdk_reply_history.DEFAULT_GROUP_HISTORY_LIMIT);
2939
+ const historyLimit = (0, openclaw_plugin_sdk_number_runtime.resolvePromptHistoryLimit)(msteamsCfg?.historyLimit ?? cfg.messages?.groupChat?.historyLimit);
3215
2940
  const conversationHistories = /* @__PURE__ */ new Map();
3216
2941
  const readConfig = (0, openclaw_plugin_sdk_runtime_config_snapshot.createRuntimeConfigReader)(cfg);
3217
2942
  const resolveDebounceMs = () => core.channel.debounce.resolveInboundDebounceMs({
@@ -3235,8 +2960,35 @@ function createMSTeamsMessageHandler(deps) {
3235
2960
  log.debug?.("skipping message without from.id");
3236
2961
  return;
3237
2962
  }
2963
+ let currentCfg = readConfig();
2964
+ let isBotOwnedThread = false;
2965
+ if (isChannel && facts.threadId && activity.recipient?.id) {
2966
+ const channelConfig = currentCfg.channels?.msteams;
2967
+ const routeConfig = require_channel.resolveMSTeamsRouteConfig({
2968
+ cfg: channelConfig,
2969
+ teamId,
2970
+ teamName: activity.channelData?.team?.name,
2971
+ conversationId,
2972
+ channelName: activity.channelData?.channel?.name,
2973
+ allowNameMatching: channelConfig?.dangerouslyAllowNameMatching === true
2974
+ });
2975
+ if (require_channel.resolveMSTeamsReplyPolicy({
2976
+ isDirectMessage: false,
2977
+ globalConfig: channelConfig,
2978
+ ...routeConfig
2979
+ }).requireMentionInBotThreads !== void 0) {
2980
+ isBotOwnedThread = await require_conversation_store_state.wasMSTeamsMessageSentWithPersistence({
2981
+ conversationId,
2982
+ messageId: facts.threadId,
2983
+ botId: activity.recipient.id
2984
+ });
2985
+ currentCfg = readConfig();
2986
+ }
2987
+ }
2988
+ const currentMSTeamsCfg = currentCfg.channels?.msteams;
2989
+ if (currentMSTeamsCfg?.enabled === false || currentMSTeamsCfg?.appId && currentMSTeamsCfg.appId !== appId) return;
3238
2990
  const admission = await admitMSTeamsMessage({
3239
- cfg,
2991
+ cfg: currentCfg,
3240
2992
  activity,
3241
2993
  text,
3242
2994
  conversationId,
@@ -3248,7 +3000,7 @@ function createMSTeamsMessageHandler(deps) {
3248
3000
  });
3249
3001
  if (!admission) return;
3250
3002
  const { senderId, senderName, isDirectMessage, channelGate, allowNameMatching, groupPolicy, commandAuthorized, effectiveGroupAllowFrom, allowTextCommands, isControlCommand } = admission;
3251
- const pollVote = require_polls.extractMSTeamsPollVote(activity);
3003
+ const pollVote = require_conversation_store_state.extractMSTeamsPollVote(activity);
3252
3004
  if (pollVote) {
3253
3005
  try {
3254
3006
  if (!await pollStore.recordVote({
@@ -3270,7 +3022,7 @@ function createMSTeamsMessageHandler(deps) {
3270
3022
  return;
3271
3023
  }
3272
3024
  const threadRouting = prepareMSTeamsThreadRouting({
3273
- cfg,
3025
+ cfg: currentCfg,
3274
3026
  context,
3275
3027
  isDirectMessage,
3276
3028
  isChannel,
@@ -3288,18 +3040,25 @@ function createMSTeamsMessageHandler(deps) {
3288
3040
  });
3289
3041
  const channelId = conversationId;
3290
3042
  const { teamConfig, channelConfig } = channelGate;
3291
- const { requireMention, replyStyle } = require_channel.resolveMSTeamsReplyPolicy({
3043
+ const replyPolicy = require_channel.resolveMSTeamsReplyPolicy({
3292
3044
  isDirectMessage,
3293
- globalConfig: msteamsCfg,
3045
+ globalConfig: currentMSTeamsCfg,
3294
3046
  teamConfig,
3295
3047
  channelConfig
3296
3048
  });
3049
+ const { requireMention, implicitMentionKinds } = resolveBotThreadMentionPolicy({
3050
+ isBotOwnedThread,
3051
+ requireMention: replyPolicy.requireMention,
3052
+ requireMentionInBotThreads: replyPolicy.requireMentionInBotThreads,
3053
+ implicitMentionKinds: params.implicitMentionKinds
3054
+ });
3055
+ const { replyStyle } = replyPolicy;
3297
3056
  const timestamp = require_resolve_allowlist.parseMSTeamsActivityTimestamp(activity.timestamp);
3298
3057
  const mentionDecision = (0, openclaw_plugin_sdk_channel_inbound.resolveInboundMentionDecision)({
3299
3058
  facts: {
3300
3059
  canDetectMention: true,
3301
3060
  wasMentioned: params.wasMentioned,
3302
- implicitMentionKinds: params.implicitMentionKinds
3061
+ implicitMentionKinds
3303
3062
  },
3304
3063
  policy: {
3305
3064
  isGroup: !isDirectMessage,
@@ -3321,7 +3080,7 @@ function createMSTeamsMessageHandler(deps) {
3321
3080
  if (historyBody) {
3322
3081
  enqueuePrimaryMessageSystemEvent();
3323
3082
  (0, openclaw_plugin_sdk_reply_history.createChannelHistoryWindow)({ historyMap: conversationHistories }).record({
3324
- historyKey: conversationId,
3083
+ historyKey: facts.historyKey,
3325
3084
  limit: historyLimit,
3326
3085
  entry: {
3327
3086
  sender: senderName,
@@ -3346,9 +3105,9 @@ function createMSTeamsMessageHandler(deps) {
3346
3105
  resolveTeamAadGroupId: threadRouting.resolveTeamAadGroupId,
3347
3106
  mediaMaxBytes,
3348
3107
  tokenProvider,
3349
- mediaAllowHosts: msteamsCfg?.mediaAllowHosts,
3350
- mediaAuthAllowHosts: msteamsCfg?.mediaAuthAllowHosts,
3351
- graphMediaFallback: msteamsCfg?.graphMediaFallback,
3108
+ mediaAllowHosts: currentMSTeamsCfg?.mediaAllowHosts,
3109
+ mediaAuthAllowHosts: currentMSTeamsCfg?.mediaAuthAllowHosts,
3110
+ graphMediaFallback: currentMSTeamsCfg?.graphMediaFallback,
3352
3111
  deadline: preprocessingDeadline,
3353
3112
  log
3354
3113
  });
@@ -3369,7 +3128,7 @@ function createMSTeamsMessageHandler(deps) {
3369
3128
  log
3370
3129
  });
3371
3130
  await dispatchMSTeamsInboundTurn({
3372
- cfg,
3131
+ cfg: currentCfg,
3373
3132
  runtime,
3374
3133
  appId,
3375
3134
  app,
@@ -3394,10 +3153,19 @@ function createMSTeamsMessageHandler(deps) {
3394
3153
  debounceMs: resolveDebounceMs(),
3395
3154
  resolveDebounceMs,
3396
3155
  buildKey: (entry) => {
3397
- const conversationId = require_resolve_allowlist.normalizeMSTeamsConversationId(entry.context.activity.conversation?.id ?? "");
3156
+ const activity = entry.context.activity;
3157
+ const rawConversationId = activity.conversation?.id ?? "";
3158
+ const conversationId = require_resolve_allowlist.normalizeMSTeamsConversationId(rawConversationId);
3159
+ const threadId = activity.conversation?.conversationType === "channel" ? require_resolve_allowlist.extractMSTeamsConversationMessageId(rawConversationId) ?? activity.replyToId : void 0;
3398
3160
  const senderId = entry.context.activity.from?.aadObjectId ?? entry.context.activity.from?.id ?? "";
3399
3161
  if (!senderId || !conversationId) return null;
3400
- return `msteams:${appId}:${conversationId}:${senderId}`;
3162
+ return JSON.stringify([
3163
+ "msteams",
3164
+ appId,
3165
+ conversationId,
3166
+ threadId ?? null,
3167
+ senderId
3168
+ ]);
3401
3169
  },
3402
3170
  shouldDebounce: (entry) => {
3403
3171
  if (!entry.text.trim()) return false;
@@ -3520,6 +3288,11 @@ function createMSTeamsReactionHandler(deps) {
3520
3288
  },
3521
3289
  ...teamId ? { teamId } : {}
3522
3290
  });
3291
+ const sessionKey = require_thread_session.resolveMSTeamsRouteSessionKey({
3292
+ baseSessionKey: route.sessionKey,
3293
+ isChannel,
3294
+ conversationMessageId: require_resolve_allowlist.extractMSTeamsConversationMessageId(rawConversationId)
3295
+ });
3523
3296
  const targetMessageId = activity.replyToId ?? "unknown";
3524
3297
  for (const reaction of reactions) {
3525
3298
  const reactionType = reaction.type ?? "unknown";
@@ -3533,7 +3306,7 @@ function createMSTeamsReactionHandler(deps) {
3533
3306
  conversationId
3534
3307
  });
3535
3308
  core.system.enqueueSystemEvent(label, {
3536
- sessionKey: route.sessionKey,
3309
+ sessionKey,
3537
3310
  contextKey: `msteams:reaction:${conversationId}:${targetMessageId}:${senderId}:${reactionType}:${direction}`
3538
3311
  });
3539
3312
  }
@@ -3665,56 +3438,10 @@ async function isCardActionInvokeAuthorized(context, deps) {
3665
3438
  includeInvokeName: true
3666
3439
  });
3667
3440
  }
3668
- function registerMSTeamsHandlers(handler, deps) {
3441
+ function createMSTeamsActivityHandler(deps) {
3669
3442
  const handleTeamsMessage = createMSTeamsMessageHandler(deps);
3670
3443
  const handleReaction = createMSTeamsReactionHandler(deps);
3671
- const originalRun = handler.run;
3672
- if (originalRun) handler.run = async (context, turnAdoptionLifecycle) => {
3673
- const ctx = context;
3674
- if (ctx.activity?.type === "invoke" && ctx.activity?.name === "adaptiveCard/action") {
3675
- if (await maybeHandleMSTeamsApprovalCardSubmit({
3676
- context: ctx,
3677
- deps
3678
- })) return;
3679
- const text = serializeMSTeamsAdaptiveCardActionValue(ctx.activity?.value);
3680
- if (text) return await handleTeamsMessage({
3681
- ...ctx,
3682
- activity: {
3683
- ...ctx.activity,
3684
- type: "message",
3685
- text
3686
- }
3687
- }, turnAdoptionLifecycle);
3688
- return;
3689
- }
3690
- return originalRun.call(handler, context, turnAdoptionLifecycle);
3691
- };
3692
- handler.onMessage(async (context, next, turnAdoptionLifecycle) => {
3693
- let nextRan = false;
3694
- const runNext = async () => {
3695
- nextRan = true;
3696
- await next();
3697
- };
3698
- try {
3699
- const ctx = context;
3700
- if (await maybeHandleMSTeamsApprovalCardSubmit({
3701
- context: ctx,
3702
- deps
3703
- })) {
3704
- await runNext();
3705
- return;
3706
- }
3707
- const result = await handleTeamsMessage(ctx, turnAdoptionLifecycle);
3708
- await runNext();
3709
- return result;
3710
- } catch (err) {
3711
- if (turnAdoptionLifecycle) throw err;
3712
- deps.runtime.error(`msteams handler failed: ${require_channel_setup.formatUnknownError(err)}`);
3713
- }
3714
- if (!nextRan) await runNext();
3715
- });
3716
- handler.onMembersAdded(async (context, next) => {
3717
- const ctx = context;
3444
+ const handleMembersAdded = async (ctx) => {
3718
3445
  const membersAdded = ctx.activity?.membersAdded ?? [];
3719
3446
  const botId = ctx.activity?.recipient?.id;
3720
3447
  const msteamsCfg = deps.cfg.channels?.msteams;
@@ -3726,13 +3453,7 @@ function registerMSTeamsHandlers(handler, deps) {
3726
3453
  promptStarters: msteamsCfg?.promptStarters
3727
3454
  });
3728
3455
  try {
3729
- await ctx.sendActivity({
3730
- type: "message",
3731
- attachments: [{
3732
- contentType: "application/vnd.microsoft.card.adaptive",
3733
- content: card
3734
- }]
3735
- });
3456
+ await ctx.sendActivity(require_conversation_store_state.buildMSTeamsAdaptiveCardActivity(card));
3736
3457
  deps.log.info("sent welcome card");
3737
3458
  } catch (err) {
3738
3459
  deps.log.debug?.("failed to send welcome card", { error: require_channel_setup.formatUnknownError(err) });
@@ -3747,25 +3468,45 @@ function registerMSTeamsHandlers(handler, deps) {
3747
3468
  }
3748
3469
  } else deps.log.debug?.("skipping welcome (disabled by config or conversation type)");
3749
3470
  } else deps.log.debug?.("member added", { member: member.id });
3750
- await next();
3751
- });
3752
- handler.onReactionsAdded(async (context, next) => {
3753
- try {
3754
- await handleReaction(context, "added");
3755
- } catch (err) {
3756
- deps.runtime.error(`msteams reaction handler failed: ${String(err)}`);
3471
+ };
3472
+ return async (context, turnAdoptionLifecycle) => {
3473
+ const activity = context.activity;
3474
+ if (activity?.type === "invoke" && activity.name === "adaptiveCard/action") {
3475
+ if (await maybeHandleMSTeamsApprovalCardSubmit({
3476
+ context,
3477
+ deps
3478
+ })) return;
3479
+ const text = serializeMSTeamsAdaptiveCardActionValue(activity.value);
3480
+ if (text) return handleTeamsMessage({
3481
+ ...context,
3482
+ activity: {
3483
+ ...activity,
3484
+ type: "message",
3485
+ text
3486
+ }
3487
+ }, turnAdoptionLifecycle);
3488
+ return;
3757
3489
  }
3758
- await next();
3759
- });
3760
- handler.onReactionsRemoved(async (context, next) => {
3761
- try {
3762
- await handleReaction(context, "removed");
3490
+ if (activity?.type === "message") try {
3491
+ if (await maybeHandleMSTeamsApprovalCardSubmit({
3492
+ context,
3493
+ deps
3494
+ })) return;
3495
+ return await handleTeamsMessage(context, turnAdoptionLifecycle);
3763
3496
  } catch (err) {
3764
- deps.runtime.error(`msteams reaction handler failed: ${String(err)}`);
3497
+ if (turnAdoptionLifecycle) throw err;
3498
+ deps.runtime.error(`msteams handler failed: ${require_channel_setup.formatUnknownError(err)}`);
3765
3499
  }
3766
- await next();
3767
- });
3768
- return handler;
3500
+ else if (activity?.type === "conversationUpdate") await handleMembersAdded(context);
3501
+ else if (activity?.type === "messageReaction") for (const direction of ["added", "removed"]) {
3502
+ if (!(direction === "added" ? activity.reactionsAdded : activity.reactionsRemoved)?.length) continue;
3503
+ try {
3504
+ await handleReaction(context, direction);
3505
+ } catch (err) {
3506
+ deps.runtime.error(`msteams reaction handler failed: ${String(err)}`);
3507
+ }
3508
+ }
3509
+ };
3769
3510
  }
3770
3511
  //#endregion
3771
3512
  //#region extensions/msteams/src/feedback-invoke.ts
@@ -3892,14 +3633,14 @@ async function handleMSTeamsFileConsentInvoke(context, log) {
3892
3633
  const expiredUploadMessage = "The file upload request has expired. Please try sending the file again.";
3893
3634
  const activity = context.activity;
3894
3635
  if (activity.type !== "invoke" || activity.name !== "fileConsent/invoke") return false;
3895
- const consentResponse = require_messenger.parseFileConsentInvoke(activity);
3636
+ const consentResponse = require_conversation_store_state.parseFileConsentInvoke(activity);
3896
3637
  if (!consentResponse) {
3897
3638
  log.debug?.("invalid file consent invoke", { value: activity.value });
3898
3639
  return false;
3899
3640
  }
3900
3641
  const uploadId = typeof consentResponse.context?.uploadId === "string" ? consentResponse.context.uploadId : void 0;
3901
- const inMemoryFile = require_messenger.getPendingUpload(uploadId);
3902
- const fsFile = inMemoryFile ? void 0 : await require_messenger.getPendingUploadFs(uploadId);
3642
+ const inMemoryFile = require_conversation_store_state.getPendingUpload(uploadId);
3643
+ const fsFile = inMemoryFile ? void 0 : await require_conversation_store_state.getPendingUploadFs(uploadId);
3903
3644
  const pendingFile = inMemoryFile ?? fsFile;
3904
3645
  if (pendingFile) {
3905
3646
  const pendingConversationId = require_resolve_allowlist.normalizeMSTeamsConversationId(pendingFile.conversationId);
@@ -3922,33 +3663,29 @@ async function handleMSTeamsFileConsentInvoke(context, log) {
3922
3663
  size: pendingFile.buffer.length
3923
3664
  });
3924
3665
  try {
3925
- await require_messenger.uploadToConsentUrl({
3666
+ await require_conversation_store_state.uploadToConsentUrl({
3926
3667
  url: consentResponse.uploadInfo.uploadUrl,
3927
3668
  buffer: pendingFile.buffer,
3928
3669
  contentType: pendingFile.contentType
3929
3670
  });
3930
- const fileInfoCard = require_messenger.buildFileInfoCard({
3931
- filename: consentResponse.uploadInfo.name,
3932
- contentUrl: consentResponse.uploadInfo.contentUrl,
3933
- uniqueId: consentResponse.uploadInfo.uniqueId,
3934
- fileType: consentResponse.uploadInfo.fileType
3935
- });
3936
- if (!pendingFile.consentCardActivityId) await context.sendActivity({
3671
+ const fileActivity = {
3937
3672
  type: "message",
3938
- attachments: [fileInfoCard]
3939
- });
3673
+ attachments: [require_conversation_store_state.buildFileInfoCard({
3674
+ filename: consentResponse.uploadInfo.name,
3675
+ contentUrl: consentResponse.uploadInfo.contentUrl,
3676
+ uniqueId: consentResponse.uploadInfo.uniqueId,
3677
+ fileType: consentResponse.uploadInfo.fileType
3678
+ })]
3679
+ };
3940
3680
  if (pendingFile.consentCardActivityId) try {
3941
3681
  await context.updateActivity({
3942
- id: pendingFile.consentCardActivityId,
3943
- type: "message",
3944
- attachments: [fileInfoCard]
3682
+ ...fileActivity,
3683
+ id: pendingFile.consentCardActivityId
3945
3684
  });
3946
3685
  } catch {
3947
- await context.sendActivity({
3948
- type: "message",
3949
- attachments: [fileInfoCard]
3950
- });
3686
+ await context.sendActivity(fileActivity);
3951
3687
  }
3688
+ else await context.sendActivity(fileActivity);
3952
3689
  log.info("file upload complete", {
3953
3690
  uploadId,
3954
3691
  filename: consentResponse.uploadInfo.name,
@@ -3961,8 +3698,8 @@ async function handleMSTeamsFileConsentInvoke(context, log) {
3961
3698
  });
3962
3699
  await context.sendActivity("File upload failed. Please try again.");
3963
3700
  } finally {
3964
- require_messenger.removePendingUpload(uploadId);
3965
- await require_messenger.removePendingUploadFs(uploadId);
3701
+ require_conversation_store_state.removePendingUpload(uploadId);
3702
+ await require_conversation_store_state.removePendingUploadFs(uploadId);
3966
3703
  }
3967
3704
  } else {
3968
3705
  log.debug?.("pending file not found for consent", { uploadId });
@@ -3970,8 +3707,8 @@ async function handleMSTeamsFileConsentInvoke(context, log) {
3970
3707
  }
3971
3708
  } else {
3972
3709
  log.debug?.("user declined file consent", { uploadId });
3973
- require_messenger.removePendingUpload(uploadId);
3974
- await require_messenger.removePendingUploadFs(uploadId);
3710
+ require_conversation_store_state.removePendingUpload(uploadId);
3711
+ await require_conversation_store_state.removePendingUploadFs(uploadId);
3975
3712
  }
3976
3713
  return true;
3977
3714
  }
@@ -3982,7 +3719,7 @@ async function handleMSTeamsFileConsentInvoke(context, log) {
3982
3719
  */
3983
3720
  async function runMSTeamsFileConsentInvokeHandler(context, log) {
3984
3721
  try {
3985
- await require_messenger.withRevokedProxyFallback({
3722
+ await require_conversation_store_state.withRevokedProxyFallback({
3986
3723
  run: async () => await handleMSTeamsFileConsentInvoke(context, log),
3987
3724
  onRevoked: async () => true,
3988
3725
  onRevokedLog: () => {
@@ -4001,13 +3738,6 @@ function publishMSTeamsBlocked(statusSink, lastError) {
4001
3738
  function publishMSTeamsReady(statusSink, now = Date.now()) {
4002
3739
  statusSink?.((0, openclaw_plugin_sdk_gateway_runtime.channelReadyPatch)({ lastConnectedAt: now }));
4003
3740
  }
4004
- function publishMSTeamsRecovering(statusSink, lastError) {
4005
- statusSink?.({
4006
- connected: false,
4007
- lifecycle: "recovering",
4008
- lastError
4009
- });
4010
- }
4011
3741
  function publishMSTeamsStopped(statusSink) {
4012
3742
  statusSink?.((0, openclaw_plugin_sdk_gateway_runtime.channelStoppedPatch)());
4013
3743
  }
@@ -4177,7 +3907,7 @@ function createMSTeamsReplayContext(activity, app, serviceUrlBoundary) {
4177
3907
  ...threadActivityId ? { threadActivityId } : {},
4178
3908
  serviceUrlBoundary
4179
3909
  };
4180
- const sendActivity = (outbound) => require_messenger.sendMSTeamsActivityWithReference(app, reference, outbound, proactiveOptions);
3910
+ const sendActivity = (outbound) => require_conversation_store_state.sendMSTeamsActivityWithReference(app, reference, outbound, proactiveOptions);
4181
3911
  return {
4182
3912
  activity,
4183
3913
  sendActivity,
@@ -4187,26 +3917,108 @@ function createMSTeamsReplayContext(activity, app, serviceUrlBoundary) {
4187
3917
  return results;
4188
3918
  },
4189
3919
  updateActivity: async (outbound) => {
4190
- return await require_messenger.updateMSTeamsActivityWithReference(app, reference, typeof outbound.id === "string" ? outbound.id : "", outbound, proactiveOptions);
3920
+ return await require_conversation_store_state.updateMSTeamsActivityWithReference(app, reference, typeof outbound.id === "string" ? outbound.id : "", outbound, proactiveOptions);
4191
3921
  },
4192
3922
  deleteActivity: async (activityId) => {
4193
- await require_messenger.deleteMSTeamsActivityWithReference(app, reference, activityId, proactiveOptions);
3923
+ await require_conversation_store_state.deleteMSTeamsActivityWithReference(app, reference, activityId, proactiveOptions);
4194
3924
  },
4195
3925
  getTeamDetails: (teamId) => app.api.teams.getById(teamId)
4196
3926
  };
4197
3927
  }
4198
3928
  //#endregion
4199
- //#region extensions/msteams/src/webhook-timeouts.ts
4200
- const MSTEAMS_WEBHOOK_INACTIVITY_TIMEOUT_MS = 3e4;
4201
- const MSTEAMS_WEBHOOK_REQUEST_TIMEOUT_MS = 3e4;
4202
- const MSTEAMS_WEBHOOK_HEADERS_TIMEOUT_MS = 15e3;
4203
- function applyMSTeamsWebhookTimeouts(httpServer, opts) {
4204
- const inactivityTimeoutMs = opts?.inactivityTimeoutMs ?? MSTEAMS_WEBHOOK_INACTIVITY_TIMEOUT_MS;
4205
- const requestTimeoutMs = opts?.requestTimeoutMs ?? MSTEAMS_WEBHOOK_REQUEST_TIMEOUT_MS;
4206
- const headersTimeoutMs = Math.min(opts?.headersTimeoutMs ?? MSTEAMS_WEBHOOK_HEADERS_TIMEOUT_MS, requestTimeoutMs);
4207
- httpServer.setTimeout(inactivityTimeoutMs);
4208
- httpServer.requestTimeout = requestTimeoutMs;
4209
- httpServer.headersTimeout = headersTimeoutMs;
3929
+ //#region extensions/msteams/src/sso-token-store.ts
3930
+ /**
3931
+ * SQLite-backed store for Bot Framework OAuth SSO tokens.
3932
+ *
3933
+ * Tokens are keyed by (connectionName, userId). `userId` should be the
3934
+ * stable AAD object ID (`activity.from.aadObjectId`) when available,
3935
+ * falling back to the Bot Framework `activity.from.id`.
3936
+ *
3937
+ * The store is intentionally minimal: it persists the exchanged user
3938
+ * token plus its expiration so consumers (for example tool handlers
3939
+ * that call Microsoft Graph with delegated permissions) can fetch a
3940
+ * valid token without reaching back into Bot Framework every turn.
3941
+ */
3942
+ const MSTEAMS_SSO_TOKENS_NAMESPACE = "sso-tokens";
3943
+ const SSO_TOKEN_MUTATION_KEY = "sso-tokens";
3944
+ const MSTEAMS_MAX_SSO_TOKENS = 5e3;
3945
+ const STORE_KEY_VERSION_PREFIX = "v2:";
3946
+ function makeMSTeamsSsoTokenStoreKey(connectionName, userId) {
3947
+ return `${STORE_KEY_VERSION_PREFIX}${(0, node_crypto.createHash)("sha256").update(JSON.stringify([connectionName, userId])).digest("hex")}`;
3948
+ }
3949
+ function createTokenStore(params) {
3950
+ return require_runtime.getMSTeamsRuntime().state.openKeyedStore({
3951
+ namespace: MSTEAMS_SSO_TOKENS_NAMESPACE,
3952
+ maxEntries: MSTEAMS_MAX_SSO_TOKENS,
3953
+ env: require_conversation_store_state.resolveMSTeamsSqliteStateEnv(params)
3954
+ });
3955
+ }
3956
+ function createMSTeamsSsoTokenStoreFs(params) {
3957
+ const tokenStore = createTokenStore(params);
3958
+ return {
3959
+ async get({ connectionName, userId }) {
3960
+ return await tokenStore.lookup(makeMSTeamsSsoTokenStoreKey(connectionName, userId)) ?? null;
3961
+ },
3962
+ async save(token) {
3963
+ await require_conversation_store_state.withMSTeamsSqliteMutationLock(params, SSO_TOKEN_MUTATION_KEY, async () => {
3964
+ await tokenStore.register(makeMSTeamsSsoTokenStoreKey(token.connectionName, token.userId), require_conversation_store_state.toPluginJsonValue({ ...token }));
3965
+ });
3966
+ },
3967
+ async remove({ connectionName, userId }) {
3968
+ let removed = false;
3969
+ await require_conversation_store_state.withMSTeamsSqliteMutationLock(params, SSO_TOKEN_MUTATION_KEY, async () => {
3970
+ removed = await tokenStore.delete(makeMSTeamsSsoTokenStoreKey(connectionName, userId));
3971
+ });
3972
+ return removed;
3973
+ }
3974
+ };
3975
+ }
3976
+ //#endregion
3977
+ //#region extensions/msteams/src/webhook-handler.ts
3978
+ /** Keep Express response ownership until the channel's route can be released. */
3979
+ function createMSTeamsWebhookHandler(expressApp, warn) {
3980
+ const responses = /* @__PURE__ */ new Map();
3981
+ let closing = false;
3982
+ let closeTask;
3983
+ return {
3984
+ handler: ((req, res) => {
3985
+ if (closing) {
3986
+ res.writeHead(503, { "Retry-After": "1" });
3987
+ res.end("Service Unavailable");
3988
+ return Promise.resolve();
3989
+ }
3990
+ const response = (0, openclaw_plugin_sdk_extension_shared.createDeferred)();
3991
+ responses.set(res, response.promise);
3992
+ const done = () => {
3993
+ res.off("finish", done);
3994
+ res.off("close", done);
3995
+ responses.delete(res);
3996
+ response.resolve();
3997
+ };
3998
+ res.once("finish", done);
3999
+ res.once("close", done);
4000
+ try {
4001
+ expressApp(req, res);
4002
+ } catch (error) {
4003
+ done();
4004
+ throw error;
4005
+ }
4006
+ return response.promise;
4007
+ }),
4008
+ close: () => {
4009
+ closing = true;
4010
+ closeTask ??= (async () => {
4011
+ if (responses.size === 0) return;
4012
+ try {
4013
+ await (0, openclaw_plugin_sdk_time_runtime.withTimeout)(Promise.all(responses.values()), require_resolve_allowlist.MSTEAMS_REQUEST_TIMEOUT_MS, "Microsoft Teams webhook shutdown");
4014
+ } catch {
4015
+ warn("Microsoft Teams webhook shutdown exceeded 30 seconds; closing active responses");
4016
+ for (const response of responses.keys()) response.destroy();
4017
+ }
4018
+ })();
4019
+ return closeTask;
4020
+ }
4021
+ };
4210
4022
  }
4211
4023
  //#endregion
4212
4024
  //#region extensions/msteams/src/monitor.ts
@@ -4320,20 +4132,26 @@ async function monitorMSTeamsProvider(opts) {
4320
4132
  msteams: msteamsCfg
4321
4133
  }
4322
4134
  };
4323
- const port = msteamsCfg.webhook?.port ?? 3978;
4135
+ const legacyListener = require_channel.resolveMSTeamsLegacyWebhook(msteamsCfg);
4136
+ const pathIssue = require_channel.resolveMSTeamsWebhookPathIssue({ cfg });
4137
+ if (pathIssue) {
4138
+ if (!legacyListener) throw new Error(pathIssue);
4139
+ log.warn?.(pathIssue);
4140
+ }
4324
4141
  const textLimit = core.channel.text.resolveTextChunkLimit(cfg, "msteams");
4325
4142
  const mediaMaxBytes = (0, openclaw_plugin_sdk_account_helpers.resolveChannelMediaMaxBytes)({
4326
4143
  cfg,
4327
4144
  resolveChannelLimitMb: ({ cfg: channelCfg }) => channelCfg.channels?.msteams?.mediaMaxMb
4328
4145
  }) ?? 8388608;
4329
- const conversationStore = opts.conversationStore ?? require_polls.createMSTeamsConversationStoreState();
4330
- const pollStore = opts.pollStore ?? require_polls.createMSTeamsPollStoreState();
4331
- log.info(`starting provider (port ${port})`);
4146
+ const conversationStore = opts.conversationStore ?? require_conversation_store_state.createMSTeamsConversationStoreState();
4147
+ const pollStore = opts.pollStore ?? require_conversation_store_state.createMSTeamsPollStoreState();
4148
+ log.info("starting provider on Gateway HTTP routes");
4332
4149
  const express = await import("express");
4333
4150
  const expressApp = express.default();
4151
+ const privateQaToken = await require_resolve_allowlist.resolveMSTeamsPrivateQaRuntime()?.token();
4334
4152
  expressApp.use((req, res, next) => {
4335
4153
  const auth = req.headers.authorization;
4336
- if (!auth || !auth.startsWith("Bearer ")) {
4154
+ if (!auth || !auth.startsWith("Bearer ") || privateQaToken && !(0, openclaw_plugin_sdk_security_runtime.safeEqualSecret)(auth.slice(7), privateQaToken)) {
4337
4155
  res.status(401).json({ error: "Unauthorized" });
4338
4156
  return;
4339
4157
  }
@@ -4347,7 +4165,7 @@ async function monitorMSTeamsProvider(opts) {
4347
4165
  }
4348
4166
  next(err);
4349
4167
  });
4350
- const configuredPath = msteamsCfg.webhook?.path ?? "/api/messages";
4168
+ const configuredPath = msteamsCfg.webhook?.path || "/api/messages";
4351
4169
  const ssoConnectionName = msteamsCfg.sso?.enabled && msteamsCfg.sso.connectionName ? msteamsCfg.sso.connectionName : void 0;
4352
4170
  const { app } = await require_resolve_allowlist.loadMSTeamsSdkWithAuth(creds, {
4353
4171
  ...require_resolve_allowlist.resolveMSTeamsSdkCloudOptions(msteamsCfg),
@@ -4368,11 +4186,10 @@ async function monitorMSTeamsProvider(opts) {
4368
4186
  }
4369
4187
  const tokenProvider = require_resolve_allowlist.createMSTeamsTokenProvider(app);
4370
4188
  const ssoDeps = ssoConnectionName ? {
4371
- tokenStore: require_sso_token_store.createMSTeamsSsoTokenStoreFs(),
4189
+ tokenStore: createMSTeamsSsoTokenStoreFs(),
4372
4190
  connectionName: ssoConnectionName
4373
4191
  } : void 0;
4374
4192
  if (ssoDeps) log.debug?.("msteams sso enabled", { connectionName: ssoDeps.connectionName });
4375
- const handler = buildActivityHandler();
4376
4193
  const handlerDeps = {
4377
4194
  cfg,
4378
4195
  runtime,
@@ -4385,37 +4202,29 @@ async function monitorMSTeamsProvider(opts) {
4385
4202
  pollStore,
4386
4203
  log
4387
4204
  };
4388
- registerMSTeamsHandlers(handler, handlerDeps);
4205
+ const handleActivity = createMSTeamsActivityHandler(handlerDeps);
4389
4206
  const ingress = createMSTeamsIngress({
4390
4207
  accountId: appId,
4391
4208
  runtime,
4392
4209
  dispatch: async (activity, lifecycle, liveContext) => {
4393
4210
  if (liveContext) liveContext.activity = activity;
4394
4211
  const context = liveContext ?? createMSTeamsReplayContext(activity, app, require_resolve_allowlist.resolveMSTeamsSdkCloudOptions(msteamsCfg));
4395
- return await handler.run(context, lifecycle);
4212
+ return await handleActivity(context, lifecycle);
4396
4213
  }
4397
4214
  });
4398
4215
  app.on("card.action", async (ctx) => {
4399
4216
  const adaptedCtx = adaptSdkContext(ctx, app);
4400
4217
  try {
4401
4218
  const activity = adaptedCtx.activity;
4402
- const vote = require_polls.extractMSTeamsPollVote(activity);
4219
+ const vote = require_conversation_store_state.extractMSTeamsPollVote(activity);
4403
4220
  if (vote) {
4404
4221
  const voterId = activity?.from?.aadObjectId ?? activity?.from?.id ?? "unknown";
4405
4222
  try {
4406
- if (!await isCardActionInvokeAuthorized(adaptedCtx, handlerDeps)) return {
4407
- statusCode: 200,
4408
- type: "application/vnd.microsoft.activity.message",
4409
- value: "Not authorized."
4410
- };
4223
+ if (!await isCardActionInvokeAuthorized(adaptedCtx, handlerDeps)) return cardActionMessage("Not authorized.");
4411
4224
  const existingPoll = await pollStore.getPoll(vote.pollId);
4412
4225
  if (!existingPoll) {
4413
4226
  log.debug?.("poll vote ignored (poll not found)", { pollId: vote.pollId });
4414
- return {
4415
- statusCode: 200,
4416
- type: "application/vnd.microsoft.activity.message",
4417
- value: "Poll not found."
4418
- };
4227
+ return cardActionMessage("Poll not found.");
4419
4228
  }
4420
4229
  const pollConversationId = existingPoll.conversationId ? require_resolve_allowlist.normalizeMSTeamsConversationId(existingPoll.conversationId) : void 0;
4421
4230
  const activityConversationId = require_resolve_allowlist.normalizeMSTeamsConversationId(activity?.conversation?.id ?? "");
@@ -4425,11 +4234,7 @@ async function monitorMSTeamsProvider(opts) {
4425
4234
  expectedConversationId: pollConversationId,
4426
4235
  receivedConversationId: activityConversationId || void 0
4427
4236
  });
4428
- return {
4429
- statusCode: 200,
4430
- type: "application/vnd.microsoft.activity.message",
4431
- value: "Poll not found."
4432
- };
4237
+ return cardActionMessage("Poll not found.");
4433
4238
  }
4434
4239
  if (await pollStore.recordVote({
4435
4240
  pollId: vote.pollId,
@@ -4440,18 +4245,10 @@ async function monitorMSTeamsProvider(opts) {
4440
4245
  pollId: vote.pollId,
4441
4246
  voterId
4442
4247
  });
4443
- return {
4444
- statusCode: 200,
4445
- type: "application/vnd.microsoft.activity.message",
4446
- value: "Vote recorded."
4447
- };
4248
+ return cardActionMessage("Vote recorded.");
4448
4249
  }
4449
4250
  log.debug?.("poll vote ignored (poll not found)", { pollId: vote.pollId });
4450
- return {
4451
- statusCode: 200,
4452
- type: "application/vnd.microsoft.activity.message",
4453
- value: "Poll not found."
4454
- };
4251
+ return cardActionMessage("Poll not found.");
4455
4252
  } catch (err) {
4456
4253
  log.error("failed to record poll vote", {
4457
4254
  pollId: vote.pollId,
@@ -4472,11 +4269,7 @@ async function monitorMSTeamsProvider(opts) {
4472
4269
  }
4473
4270
  }
4474
4271
  await ingress.accept(activity, adaptedCtx);
4475
- return {
4476
- statusCode: 200,
4477
- type: "application/vnd.microsoft.activity.message",
4478
- value: "OK"
4479
- };
4272
+ return cardActionMessage("OK");
4480
4273
  } catch (err) {
4481
4274
  log.error("msteams card.action failed", { error: require_channel_setup.formatUnknownError(err) });
4482
4275
  return {
@@ -4499,26 +4292,27 @@ async function monitorMSTeamsProvider(opts) {
4499
4292
  app.on("file.consent.decline", (ctx) => {
4500
4293
  runMSTeamsFileConsentInvokeHandler(adaptSdkContext(ctx, app), log);
4501
4294
  });
4502
- const handleSdkSigninInvoke = async (ctx, delegateName) => {
4503
- const adaptedCtx = adaptSdkContext(ctx, app);
4504
- if (!await isSigninInvokeAuthorized(adaptedCtx, handlerDeps)) return {
4295
+ const processActivity = app.process.bind(app);
4296
+ app.process = async (event) => {
4297
+ const activity = event.body;
4298
+ if (activity.type !== "invoke" || !("name" in activity) || activity.name !== "signin/tokenExchange" && activity.name !== "signin/verifyState") return processActivity(event);
4299
+ if (!await isSigninInvokeAuthorized({ activity: {
4300
+ ...activity,
4301
+ type: activity.type,
4302
+ name: activity.name
4303
+ } }, handlerDeps)) return {
4505
4304
  status: 200,
4506
4305
  body: {}
4507
4306
  };
4508
4307
  if (!ssoDeps) {
4509
- log.debug?.("signin invoke received but msteams.sso is not configured", { name: adaptedCtx.activity?.name });
4308
+ log.debug?.("signin invoke received but msteams.sso is not configured", { name: activity.name });
4510
4309
  return {
4511
4310
  status: 200,
4512
4311
  body: {}
4513
4312
  };
4514
4313
  }
4515
- const sdkSigninApp = app;
4516
- const delegate = sdkSigninApp[delegateName];
4517
- if (typeof delegate !== "function") throw new Error(`Teams SDK ${delegateName} handler is unavailable`);
4518
- return delegate.call(sdkSigninApp, ctx);
4314
+ return processActivity(event);
4519
4315
  };
4520
- app.on("signin.token-exchange", (ctx) => handleSdkSigninInvoke(ctx, "onTokenExchange"));
4521
- app.on("signin.verify-state", (ctx) => handleSdkSigninInvoke(ctx, "onVerifyState"));
4522
4316
  if (ssoDeps) app.event("signin", (ctx) => {
4523
4317
  (async () => {
4524
4318
  if (!await isSigninInvokeAuthorized(adaptSdkContext(ctx, app), handlerDeps)) return;
@@ -4565,93 +4359,76 @@ async function monitorMSTeamsProvider(opts) {
4565
4359
  return;
4566
4360
  }
4567
4361
  try {
4568
- await handler.run(adaptedCtx);
4362
+ await handleActivity(adaptedCtx);
4569
4363
  } catch (err) {
4570
4364
  log.error("msteams non-turn activity failed", { error: require_channel_setup.formatUnknownError(err) });
4571
4365
  }
4572
4366
  });
4573
4367
  await app.initialize();
4574
4368
  ingress.start();
4575
- const privateQaRuntime = require_resolve_allowlist.resolveMSTeamsPrivateQaRuntime();
4576
- const httpServer = await new Promise((resolve, reject) => {
4577
- const onListen = (err) => err ? reject(err) : resolve(server);
4578
- const server = privateQaRuntime ? expressApp.listen(port, privateQaRuntime.listenHost, onListen) : expressApp.listen(port, onListen);
4579
- }).catch(async (err) => {
4580
- log.error("msteams server error", { error: require_channel_setup.formatUnknownError(err) });
4369
+ const unregisterRoutes = [];
4370
+ const webhook = createMSTeamsWebhookHandler(expressApp, (message) => log.warn?.(message));
4371
+ try {
4372
+ unregisterRoutes.push((0, openclaw_plugin_sdk_webhook_targets.registerPluginHttpRoute)({
4373
+ path: configuredPath,
4374
+ auth: "plugin",
4375
+ pluginId: "msteams",
4376
+ source: "msteams-webhook",
4377
+ accountId: appId,
4378
+ handler: webhook.handler,
4379
+ legacyListener: legacyListener ? {
4380
+ ...legacyListener,
4381
+ timeouts: {
4382
+ headers: 15e3,
4383
+ request: 3e4,
4384
+ socket: 3e4
4385
+ }
4386
+ } : void 0,
4387
+ throwOnFailure: true,
4388
+ log: (message) => log.warn?.(message)
4389
+ }));
4390
+ if (configuredPath !== "/api/messages") unregisterRoutes.push((0, openclaw_plugin_sdk_webhook_targets.registerPluginHttpRoute)({
4391
+ path: "/api/messages",
4392
+ auth: "plugin",
4393
+ pluginId: "msteams",
4394
+ source: "msteams-webhook-alias",
4395
+ accountId: appId,
4396
+ handler: webhook.handler,
4397
+ log: (message) => log.warn?.(message)
4398
+ }));
4399
+ } catch (error) {
4400
+ for (const unregister of unregisterRoutes) unregister();
4581
4401
  await ingress.stop();
4582
- throw err;
4583
- });
4584
- log.info(`msteams provider started on port ${port}`);
4402
+ throw error;
4403
+ }
4404
+ log.info(`msteams provider started on Gateway route ${configuredPath}`);
4585
4405
  publishMSTeamsReady(opts.statusSink);
4586
- applyMSTeamsWebhookTimeouts(httpServer);
4587
- httpServer.on("error", (err) => {
4588
- log.error("msteams server error", { error: require_channel_setup.formatUnknownError(err) });
4589
- publishMSTeamsRecovering(opts.statusSink, require_channel_setup.formatUnknownError(err));
4590
- });
4591
- const shutdown = async () => {
4592
- log.info("shutting down msteams provider");
4593
- await new Promise((resolve) => {
4594
- httpServer.close((err) => {
4595
- if (err) log.debug?.("msteams server close error", { error: require_channel_setup.formatUnknownError(err) });
4596
- resolve();
4597
- });
4598
- });
4599
- await ingress.stop();
4600
- publishMSTeamsStopped(opts.statusSink);
4406
+ let shutdownTask;
4407
+ const shutdown = () => {
4408
+ shutdownTask ??= (async () => {
4409
+ await webhook.close();
4410
+ for (const unregister of unregisterRoutes.splice(0)) unregister();
4411
+ await ingress.stop();
4412
+ publishMSTeamsStopped(opts.statusSink);
4413
+ })();
4414
+ return shutdownTask;
4601
4415
  };
4602
- await (0, openclaw_plugin_sdk_channel_outbound.keepHttpServerTaskAlive)({
4603
- server: httpServer,
4604
- abortSignal: opts.abortSignal,
4605
- onAbort: shutdown
4606
- });
4416
+ try {
4417
+ await (0, openclaw_plugin_sdk_channel_outbound.waitUntilAbort)(opts.abortSignal, shutdown);
4418
+ } finally {
4419
+ await shutdown();
4420
+ }
4607
4421
  return {
4608
4422
  app: expressApp,
4609
4423
  shutdown
4610
4424
  };
4611
4425
  }
4612
- /**
4613
- * Build a minimal ActivityHandler-compatible object that supports
4614
- * onMessage / onMembersAdded registration and a run() method.
4615
- */
4616
- function buildActivityHandler() {
4617
- const messageHandlers = [];
4618
- const membersAddedHandlers = [];
4619
- const reactionsAddedHandlers = [];
4620
- const reactionsRemovedHandlers = [];
4621
- const handler = {
4622
- onMessage(cb) {
4623
- messageHandlers.push(cb);
4624
- return handler;
4625
- },
4626
- onMembersAdded(cb) {
4627
- membersAddedHandlers.push(cb);
4628
- return handler;
4629
- },
4630
- onReactionsAdded(cb) {
4631
- reactionsAddedHandlers.push(cb);
4632
- return handler;
4633
- },
4634
- onReactionsRemoved(cb) {
4635
- reactionsRemovedHandlers.push(cb);
4636
- return handler;
4637
- },
4638
- async run(context, turnAdoptionLifecycle) {
4639
- const ctx = context;
4640
- const activityType = ctx?.activity?.type;
4641
- const noop = async () => {};
4642
- if (activityType === "message") for (const h of messageHandlers) {
4643
- const result = await h(context, noop, turnAdoptionLifecycle);
4644
- if (result) return result;
4645
- }
4646
- else if (activityType === "conversationUpdate") for (const h of membersAddedHandlers) await h(context, noop);
4647
- else if (activityType === "messageReaction") {
4648
- const activity = ctx?.activity;
4649
- if (activity?.reactionsAdded?.length) for (const h of reactionsAddedHandlers) await h(context, noop);
4650
- if (activity?.reactionsRemoved?.length) for (const h of reactionsRemovedHandlers) await h(context, noop);
4651
- }
4652
- }
4426
+ function cardActionMessage(value) {
4427
+ return {
4428
+ statusCode: 200,
4429
+ type: "application/vnd.microsoft.activity.message",
4430
+ value
4653
4431
  };
4654
- return handler;
4655
4432
  }
4656
4433
  /**
4657
4434
  * Adapt a new @microsoft/teams.apps SDK context to the MSTeamsTurnContext interface