@openclaw/msteams 2026.9.6 → 2026.9.8

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-CQWZeqB5.cjs → adapter.runtime-BiWeMKXE.cjs} +21 -29
  3. package/dist/.setup/{approval-card-CPNH_q3P.cjs → approval-card-DWPDItvq.cjs} +6 -3
  4. package/dist/.setup/{approval-handler.runtime-CUnanUdR.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-BDp16XRR.cjs → channel-ByzS7QJ0.cjs} +286 -180
  7. package/dist/.setup/{channel.runtime-Cv5F8JCa.cjs → channel.runtime-Baz8XDFs.cjs} +43 -142
  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-8RA1FUJa.cjs → conversation-store-state-B7JuyZTm.cjs} +693 -287
  11. package/dist/.setup/{src-BfOCwlHp.cjs → monitor-BJ7IyH4y.cjs} +422 -592
  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-CYN_N7O1.cjs → send-VUM7Sryb.cjs} +43 -96
  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 +9 -9
  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-BDp16XRR.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-8RA1FUJa.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
  });
@@ -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,6 +1783,19 @@ 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)({
1899
1800
  preparedItems: true,
1900
1801
  formatPlainText: (text) => text,
@@ -1915,7 +1816,6 @@ function createTeamsReplyStreamController(params) {
1915
1816
  }
1916
1817
  });
1917
1818
  return {
1918
- async onReplyStart() {},
1919
1819
  onPartialReply(payload) {
1920
1820
  if (!stream || !payload.text || wasCanceled() || streamMode !== "partial") return;
1921
1821
  if (replacementSettlementPending && replacementFinalPending) {
@@ -1957,29 +1857,16 @@ function createTeamsReplyStreamController(params) {
1957
1857
  },
1958
1858
  pushItemEvent: progressDraft.pushItemEvent.bind(progressDraft),
1959
1859
  pushToolEvent: progressDraft.pushToolEvent,
1960
- async pushReasoningProgress(text, options) {
1961
- await progressDraft.pushReasoningProgress(text, options);
1962
- },
1963
- resetReasoningProgress: () => progressDraft.resetReasoningProgress(),
1964
- async pushApprovalEvent(payload) {
1965
- await progressDraft.pushApprovalEvent(payload);
1966
- },
1967
- async pushPlanProgress(steps, options) {
1968
- await progressDraft.pushPlanProgress(steps, options);
1969
- },
1860
+ pushReasoningProgress: progressDraft.pushReasoningProgress.bind(progressDraft),
1861
+ resetReasoningProgress: progressDraft.resetReasoningProgress,
1862
+ pushApprovalEvent: progressDraft.pushApprovalEvent.bind(progressDraft),
1863
+ pushPlanProgress: progressDraft.pushPlanProgress.bind(progressDraft),
1970
1864
  preparePayload(payload) {
1971
1865
  if (!stream) return payload;
1972
1866
  if (wasCanceled()) return;
1973
1867
  if (payload.text) progressDraft.markFinalReplyStarted();
1974
1868
  if (replacementSettlementPending) {
1975
- if (!replacementFinalPending) {
1976
- deferredReplacementEntries.push({
1977
- kind: "payload",
1978
- payload
1979
- });
1980
- return;
1981
- }
1982
- if (!payload.text) {
1869
+ if (!replacementFinalPending || !payload.text) {
1983
1870
  deferredReplacementEntries.push({
1984
1871
  kind: "payload",
1985
1872
  payload
@@ -2104,24 +1991,10 @@ function createTeamsReplyStreamController(params) {
2104
1991
  }
2105
1992
  const result = await stream.close();
2106
1993
  streamFinalizationPending = false;
2107
- if (!result) {
2108
- const fallback = pendingFinalPayload;
2109
- pendingFinalPayload = void 0;
2110
- const canceled = wasCanceled();
2111
- const replacementFallback = replacementSettlementPending && fallback && !canceled ? fallbackPayloadAfterAcknowledgedText(fallback) : void 0;
2112
- const fallbackPayload = !replacementSettlementPending && fallback && !canceled ? fallbackPayloadAfterAcknowledgedText(fallback) : void 0;
2113
- const postNativePayloads = replacementSettlementPending && !canceled ? takeDeferredReplacementPayloads(replacementFallback) : [];
2114
- if (canceled) deferredReplacementEntries = [];
2115
- return {
2116
- ...acknowledgedNativeDelivery(),
2117
- ...!canceled && logicalContent ? { logicalContent } : {},
2118
- ...fallbackPayload ? { fallbackPayload } : {},
2119
- ...postNativePayloads.length > 0 ? { postNativePayloads } : {}
2120
- };
2121
- }
1994
+ if (!result) return finalizeWithoutReceipt(logicalContent, wasCanceled());
2122
1995
  const replacementFallback = replacementSettlementPending && replacementEmitFailed && pendingFinalPayload ? fallbackPayloadAfterAcknowledgedText(pendingFinalPayload) : void 0;
2123
1996
  pendingFinalPayload = void 0;
2124
- const messageId = require_messenger.extractMessageId(result) ?? acknowledgedStreamId;
1997
+ const messageId = require_conversation_store_state.extractMessageId(result) ?? acknowledgedStreamId;
2125
1998
  const postNativePayloads = replacementSettlementPending ? takeDeferredReplacementPayloads(replacementFallback) : [];
2126
1999
  const nativeContent = replacementEmitFailed ? acknowledgedText || void 0 : content;
2127
2000
  return {
@@ -2142,17 +2015,7 @@ function createTeamsReplyStreamController(params) {
2142
2015
  streamFailed = true;
2143
2016
  streamFinalizationPending = false;
2144
2017
  params.log?.warn?.(`msteams stream finalize failed: ${(0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err)}`);
2145
- const fallback = pendingFinalPayload;
2146
- pendingFinalPayload = void 0;
2147
- const replacementFallback = replacementSettlementPending && fallback ? fallbackPayloadAfterAcknowledgedText(fallback) : void 0;
2148
- const fallbackPayload = !replacementSettlementPending && fallback ? fallbackPayloadAfterAcknowledgedText(fallback) : void 0;
2149
- const postNativePayloads = replacementSettlementPending ? takeDeferredReplacementPayloads(replacementFallback) : [];
2150
- return {
2151
- ...acknowledgedNativeDelivery(),
2152
- ...logicalContent ? { logicalContent } : {},
2153
- ...fallbackPayload ? { fallbackPayload } : {},
2154
- ...postNativePayloads.length > 0 ? { postNativePayloads } : {}
2155
- };
2018
+ return finalizeWithoutReceipt(logicalContent);
2156
2019
  } finally {
2157
2020
  failedSegmentFallbackPrepared = true;
2158
2021
  queuedFinalActivity = void 0;
@@ -2180,43 +2043,22 @@ function createMSTeamsReplyDispatcher(params) {
2180
2043
  const msteamsCfg = params.cfg.channels?.msteams;
2181
2044
  const conversationType = (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeOptionalLowercaseString)(params.conversationRef.conversation?.conversationType);
2182
2045
  const isTypingSupported = conversationType === "personal" || conversationType === "groupchat";
2183
- /**
2184
- * Keepalive cadence for the typing indicator while the bot is running
2185
- * (including long tool chains). Bot Framework 1:1 TurnContext proxies
2186
- * expire after ~30s of inactivity; sending a typing activity every 8s
2187
- * keeps the proxy alive so the post-tool reply can still land via the
2188
- * turn context. Sits in the middle of the 5-10s range recommended in
2189
- * #59731.
2190
- */
2191
2046
  const TYPING_KEEPALIVE_INTERVAL_MS = 8e3;
2192
- /**
2193
- * TTL ceiling for the typing keepalive loop. The default in
2194
- * createTypingCallbacks is 60s, which is too short for the Teams long tool
2195
- * chains described in #59731 (60s+ total runs are common). Give tool
2196
- * chains up to 10 minutes before auto-stopping the keepalive.
2197
- */
2198
2047
  const TYPING_KEEPALIVE_MAX_DURATION_MS = 6e5;
2199
- const streamActiveRef = { current: () => false };
2200
- const streamCanceledRef = { current: () => false };
2201
- const rawSendTypingIndicator = async () => {
2202
- await require_messenger.withRevokedProxyFallback({
2048
+ const sendTypingIndicator = async () => {
2049
+ if (!isTypingSupported || streamController.isStreamActive() || streamController.wasCanceled()) return;
2050
+ await require_conversation_store_state.withRevokedProxyFallback({
2203
2051
  run: async () => {
2204
2052
  await params.context.sendActivity({ type: "typing" });
2205
2053
  },
2206
2054
  onRevoked: async () => {
2207
- const baseRef = require_messenger.buildConversationReference(params.conversationRef);
2208
- 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) });
2209
2056
  },
2210
2057
  onRevokedLog: () => {
2211
2058
  params.log.debug?.("turn context revoked, sending typing via proactive messaging");
2212
2059
  }
2213
2060
  });
2214
2061
  };
2215
- const sendTypingIndicator = isTypingSupported ? async () => {
2216
- if (streamActiveRef.current()) return;
2217
- if (streamCanceledRef.current()) return;
2218
- await rawSendTypingIndicator();
2219
- } : async () => {};
2220
2062
  const { onModelSelected, typingCallbacks, ...replyPipeline } = (0, openclaw_plugin_sdk_channel_outbound.createChannelMessageReplyPipeline)({
2221
2063
  cfg: params.cfg,
2222
2064
  agentId: params.agentId,
@@ -2257,8 +2099,6 @@ function createMSTeamsReplyDispatcher(params) {
2257
2099
  msteamsConfig: msteamsCfg,
2258
2100
  progressSeed: `${params.accountId ?? "default"}:${params.conversationRef.conversation?.id ?? ""}`
2259
2101
  });
2260
- streamActiveRef.current = () => streamController.isStreamActive();
2261
- streamCanceledRef.current = () => streamController.wasCanceled();
2262
2102
  const teamsStreamMode = (0, openclaw_plugin_sdk_channel_outbound.resolveChannelPreviewStreamMode)(msteamsCfg, "partial");
2263
2103
  const blockStreamingResolved = teamsStreamMode === "block" ? true : (0, openclaw_plugin_sdk_channel_outbound.resolveChannelStreamingBlockEnabled)(msteamsCfg);
2264
2104
  const blockStreamingEnabled = blockStreamingResolved ?? false;
@@ -2268,7 +2108,7 @@ function createMSTeamsReplyDispatcher(params) {
2268
2108
  const findPendingNativeDelivery = () => pendingDeliveries.find((candidate) => candidate.native && !candidate.nativeSettled);
2269
2109
  const joinAcceptedContents = (contents) => contents.filter((content) => Boolean(content)).join("\n");
2270
2110
  const sendMessages = async (messages) => {
2271
- return require_messenger.sendMSTeamsMessages({
2111
+ return require_conversation_store_state.sendMSTeamsMessages({
2272
2112
  replyStyle: params.replyStyle,
2273
2113
  app: params.app,
2274
2114
  appId: params.appId,
@@ -2307,7 +2147,7 @@ function createMSTeamsReplyDispatcher(params) {
2307
2147
  });
2308
2148
  };
2309
2149
  const renderReplyPayload = (payload) => {
2310
- return require_messenger.renderReplyPayloadsToMessages([payload], {
2150
+ return require_conversation_store_state.renderReplyPayloadsToMessages([payload], {
2311
2151
  textChunkLimit: params.textLimit,
2312
2152
  chunkText: true,
2313
2153
  mediaMode: "split",
@@ -2400,7 +2240,6 @@ function createMSTeamsReplyDispatcher(params) {
2400
2240
  ...replyPipeline,
2401
2241
  humanDelay: (0, openclaw_plugin_sdk_agent_runtime.resolveHumanDelayConfig)(params.cfg, params.agentId),
2402
2242
  onReplyStart: async () => {
2403
- await streamController.onReplyStart();
2404
2243
  if (typingIndicatorEnabled) await typingCallbacks?.onReplyStart?.();
2405
2244
  },
2406
2245
  typingCallbacks
@@ -2531,32 +2370,6 @@ function createMSTeamsReplyDispatcher(params) {
2531
2370
  };
2532
2371
  }
2533
2372
  //#endregion
2534
- //#region extensions/msteams/src/sent-message-cache.ts
2535
- const sentMessages = (0, openclaw_plugin_sdk_dedupe_runtime.createPersistentDedupeCache)({
2536
- globalKey: Symbol.for("openclaw.msteamsSentMessages"),
2537
- ttlMs: 864e5,
2538
- maxSize: 2e4,
2539
- persistent: {
2540
- namespace: "msteams.sent-messages",
2541
- maxEntries: 1e3,
2542
- openStore: (options) => require_runtime.getOptionalMSTeamsRuntime()?.state.openKeyedStore(options),
2543
- logError: (0, openclaw_plugin_sdk_plugin_state_runtime.createPluginStateErrorReporter)(require_runtime.getOptionalMSTeamsRuntime, "msteams", "sent-message-state", "Microsoft Teams persistent sent-message state failed"),
2544
- readTimestamp: (record) => record.sentAt
2545
- }
2546
- });
2547
- function makeKey(conversationId, messageId) {
2548
- return `${conversationId}:${messageId}`;
2549
- }
2550
- function recordMSTeamsSentMessage(conversationId, messageId) {
2551
- if (!conversationId || !messageId) return;
2552
- const sentAt = Date.now();
2553
- sentMessages.register(makeKey(conversationId, messageId), { sentAt }, { at: sentAt });
2554
- }
2555
- async function wasMSTeamsMessageSentWithPersistence(params) {
2556
- if (!params.conversationId || !params.messageId) return false;
2557
- return await sentMessages.lookup(makeKey(params.conversationId, params.messageId));
2558
- }
2559
- //#endregion
2560
2373
  //#region extensions/msteams/src/monitor-handler/inbound-dispatch.ts
2561
2374
  async function dispatchMSTeamsInboundTurn(params) {
2562
2375
  const core = require_runtime.getMSTeamsRuntime();
@@ -2603,7 +2416,7 @@ async function dispatchMSTeamsInboundTurn(params) {
2603
2416
  isGroup: !isDirectMessage,
2604
2417
  groupPolicy,
2605
2418
  allowFrom: effectiveGroupAllowFrom,
2606
- isSenderAllowed: (allowFrom) => require_channel.resolveMSTeamsAllowlistMatch({
2419
+ isSenderAllowed: (allowFrom) => (0, openclaw_plugin_sdk_allow_from.resolveAllowlistMatchSimple)({
2607
2420
  allowFrom,
2608
2421
  senderId: quoteSenderId ?? "",
2609
2422
  senderName: quoteSenderName,
@@ -2711,7 +2524,7 @@ async function dispatchMSTeamsInboundTurn(params) {
2711
2524
  replyStyle,
2712
2525
  textLimit,
2713
2526
  onSentMessageIds: (ids) => {
2714
- for (const id of ids) recordMSTeamsSentMessage(conversationId, id);
2527
+ for (const id of ids) require_conversation_store_state.recordMSTeamsSentMessage(conversationId, id);
2715
2528
  },
2716
2529
  tokenProvider,
2717
2530
  sharePointSiteId: cfg.channels?.msteams?.sharePointSiteId
@@ -2805,7 +2618,7 @@ async function prepareMSTeamsDebounceEntry(params) {
2805
2618
  const text = rawText ? require_resolve_allowlist.stripMSTeamsMentionTags(rawText) : extractTextFromHtmlAttachments(attachments) || require_resolve_allowlist.stripMSTeamsMentionTags(serializeMSTeamsAdaptiveCardActionValue(activity.value) || "");
2806
2619
  const conversationId = require_resolve_allowlist.normalizeMSTeamsConversationId(activity.conversation?.id ?? "");
2807
2620
  const replyToId = activity.replyToId ?? void 0;
2808
- const implicitMentionKinds = conversationId && replyToId && await wasMSTeamsMessageSentWithPersistence({
2621
+ const implicitMentionKinds = conversationId && replyToId && await require_conversation_store_state.wasMSTeamsMessageSentWithPersistence({
2809
2622
  conversationId,
2810
2623
  messageId: replyToId
2811
2624
  }) ? ["reply_to_bot"] : [];
@@ -2826,7 +2639,7 @@ function buildStoredConversationReference(params) {
2826
2639
  const clientInfo = activity.entities?.find((entity) => entity.type === "clientInfo");
2827
2640
  const tenantId = activity.channelData?.tenant?.id ?? conversation?.tenantId;
2828
2641
  const aadObjectId = from?.aadObjectId;
2829
- const serviceUrl = require_resolve_allowlist.tryNormalizeBotFrameworkServiceUrl(activity.serviceUrl);
2642
+ const serviceUrl = require_bot_framework_service_url.tryNormalizeBotFrameworkServiceUrl(activity.serviceUrl);
2830
2643
  return {
2831
2644
  activityId: activity.id,
2832
2645
  user: from ? {
@@ -2925,12 +2738,9 @@ const parentCache = /* @__PURE__ */ new Map();
2925
2738
  const INJECTED_MAX = 200;
2926
2739
  const injectedParents = /* @__PURE__ */ new Map();
2927
2740
  function touchLru(map, key, value, max) {
2928
- if (map.has(key)) map.delete(key);
2929
- else if (map.size >= max) {
2930
- const firstKey = map.keys().next().value;
2931
- if (firstKey !== void 0) map.delete(firstKey);
2932
- }
2741
+ map.delete(key);
2933
2742
  map.set(key, value);
2743
+ (0, openclaw_plugin_sdk_collection_runtime.pruneMapToMaxSize)(map, max);
2934
2744
  }
2935
2745
  function buildParentCacheKey(groupId, channelId, parentId) {
2936
2746
  return `${groupId}\u0000${channelId}\u0000${parentId}`;
@@ -2939,12 +2749,6 @@ function resolveParentCacheExpiresAt(nowRaw) {
2939
2749
  const nowMs = (0, openclaw_plugin_sdk_number_runtime.asDateTimestampMs)(nowRaw);
2940
2750
  return nowMs === void 0 ? void 0 : (0, openclaw_plugin_sdk_number_runtime.resolveExpiresAtMsFromDurationMs)(PARENT_CACHE_TTL_MS, { nowMs });
2941
2751
  }
2942
- /**
2943
- * Fetch a channel parent message with an LRU+TTL cache.
2944
- *
2945
- * Uses the injected `fetchParent` (defaults to `fetchChannelMessage`) so
2946
- * tests can swap in a stub without mocking the Graph transport.
2947
- */
2948
2752
  async function fetchParentMessageCached(token, groupId, channelId, parentId, fetchParent = require_reaction_types.fetchChannelMessage) {
2949
2753
  const key = buildParentCacheKey(groupId, channelId, parentId);
2950
2754
  const now = (0, openclaw_plugin_sdk_number_runtime.asDateTimestampMs)(Date.now());
@@ -2965,11 +2769,6 @@ async function fetchParentMessageCached(token, groupId, channelId, parentId, fet
2965
2769
  return message;
2966
2770
  }
2967
2771
  const PARENT_TEXT_MAX_CHARS = 400;
2968
- /**
2969
- * Extract a compact summary (sender + plain-text body) from a Graph parent
2970
- * message. Returns undefined when the parent cannot be summarized (missing
2971
- * or blank body).
2972
- */
2973
2772
  function summarizeParentMessage(message) {
2974
2773
  if (!message) return;
2975
2774
  const sender = message.from?.user?.displayName ?? message.from?.application?.displayName ?? "unknown";
@@ -2982,28 +2781,12 @@ function summarizeParentMessage(message) {
2982
2781
  text: text.length > PARENT_TEXT_MAX_CHARS ? `${(0, openclaw_plugin_sdk_text_utility_runtime.truncateUtf16Safe)(text, 399)}…` : text
2983
2782
  };
2984
2783
  }
2985
- /**
2986
- * Build the single-line `Replying to @sender: body` system event text.
2987
- * Callers should pass this text to `enqueueSystemEvent` together with a
2988
- * stable contextKey derived from the parent id.
2989
- */
2990
2784
  function formatParentContextEvent(summary) {
2991
2785
  return `Replying to @${summary.sender}: ${summary.text}`;
2992
2786
  }
2993
- /**
2994
- * Decide whether a parent context event should be enqueued for the current
2995
- * session. Returns `false` when we already injected the same parent for this
2996
- * session recently (prevents re-prepending identical context on every reply
2997
- * in the thread).
2998
- */
2999
2787
  function shouldInjectParentContext(sessionKey, parentId) {
3000
- const key = sessionKey;
3001
- return injectedParents.get(key) !== parentId;
2788
+ return injectedParents.get(sessionKey) !== parentId;
3002
2789
  }
3003
- /**
3004
- * Record that `parentId` was just injected for `sessionKey` so subsequent
3005
- * replies with the same parent can short-circuit via `shouldInjectParentContext`.
3006
- */
3007
2790
  function markParentContextInjected(sessionKey, parentId) {
3008
2791
  touchLru(injectedParents, sessionKey, parentId, INJECTED_MAX);
3009
2792
  }
@@ -3097,7 +2880,7 @@ async function resolveMSTeamsThreadContext(params) {
3097
2880
  isGroup: params.isChannel,
3098
2881
  groupPolicy: params.groupPolicy,
3099
2882
  allowFrom: params.effectiveGroupAllowFrom,
3100
- isSenderAllowed: (allowFrom) => require_channel.resolveMSTeamsAllowlistMatch({
2883
+ isSenderAllowed: (allowFrom) => (0, openclaw_plugin_sdk_allow_from.resolveAllowlistMatchSimple)({
3101
2884
  allowFrom,
3102
2885
  senderId: message.from?.user?.id ?? "",
3103
2886
  senderName: message.from?.user?.displayName,
@@ -3177,8 +2960,35 @@ function createMSTeamsMessageHandler(deps) {
3177
2960
  log.debug?.("skipping message without from.id");
3178
2961
  return;
3179
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;
3180
2990
  const admission = await admitMSTeamsMessage({
3181
- cfg,
2991
+ cfg: currentCfg,
3182
2992
  activity,
3183
2993
  text,
3184
2994
  conversationId,
@@ -3190,7 +3000,7 @@ function createMSTeamsMessageHandler(deps) {
3190
3000
  });
3191
3001
  if (!admission) return;
3192
3002
  const { senderId, senderName, isDirectMessage, channelGate, allowNameMatching, groupPolicy, commandAuthorized, effectiveGroupAllowFrom, allowTextCommands, isControlCommand } = admission;
3193
- const pollVote = require_polls.extractMSTeamsPollVote(activity);
3003
+ const pollVote = require_conversation_store_state.extractMSTeamsPollVote(activity);
3194
3004
  if (pollVote) {
3195
3005
  try {
3196
3006
  if (!await pollStore.recordVote({
@@ -3212,7 +3022,7 @@ function createMSTeamsMessageHandler(deps) {
3212
3022
  return;
3213
3023
  }
3214
3024
  const threadRouting = prepareMSTeamsThreadRouting({
3215
- cfg,
3025
+ cfg: currentCfg,
3216
3026
  context,
3217
3027
  isDirectMessage,
3218
3028
  isChannel,
@@ -3230,18 +3040,25 @@ function createMSTeamsMessageHandler(deps) {
3230
3040
  });
3231
3041
  const channelId = conversationId;
3232
3042
  const { teamConfig, channelConfig } = channelGate;
3233
- const { requireMention, replyStyle } = require_channel.resolveMSTeamsReplyPolicy({
3043
+ const replyPolicy = require_channel.resolveMSTeamsReplyPolicy({
3234
3044
  isDirectMessage,
3235
- globalConfig: msteamsCfg,
3045
+ globalConfig: currentMSTeamsCfg,
3236
3046
  teamConfig,
3237
3047
  channelConfig
3238
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;
3239
3056
  const timestamp = require_resolve_allowlist.parseMSTeamsActivityTimestamp(activity.timestamp);
3240
3057
  const mentionDecision = (0, openclaw_plugin_sdk_channel_inbound.resolveInboundMentionDecision)({
3241
3058
  facts: {
3242
3059
  canDetectMention: true,
3243
3060
  wasMentioned: params.wasMentioned,
3244
- implicitMentionKinds: params.implicitMentionKinds
3061
+ implicitMentionKinds
3245
3062
  },
3246
3063
  policy: {
3247
3064
  isGroup: !isDirectMessage,
@@ -3288,9 +3105,9 @@ function createMSTeamsMessageHandler(deps) {
3288
3105
  resolveTeamAadGroupId: threadRouting.resolveTeamAadGroupId,
3289
3106
  mediaMaxBytes,
3290
3107
  tokenProvider,
3291
- mediaAllowHosts: msteamsCfg?.mediaAllowHosts,
3292
- mediaAuthAllowHosts: msteamsCfg?.mediaAuthAllowHosts,
3293
- graphMediaFallback: msteamsCfg?.graphMediaFallback,
3108
+ mediaAllowHosts: currentMSTeamsCfg?.mediaAllowHosts,
3109
+ mediaAuthAllowHosts: currentMSTeamsCfg?.mediaAuthAllowHosts,
3110
+ graphMediaFallback: currentMSTeamsCfg?.graphMediaFallback,
3294
3111
  deadline: preprocessingDeadline,
3295
3112
  log
3296
3113
  });
@@ -3311,7 +3128,7 @@ function createMSTeamsMessageHandler(deps) {
3311
3128
  log
3312
3129
  });
3313
3130
  await dispatchMSTeamsInboundTurn({
3314
- cfg,
3131
+ cfg: currentCfg,
3315
3132
  runtime,
3316
3133
  appId,
3317
3134
  app,
@@ -3336,10 +3153,19 @@ function createMSTeamsMessageHandler(deps) {
3336
3153
  debounceMs: resolveDebounceMs(),
3337
3154
  resolveDebounceMs,
3338
3155
  buildKey: (entry) => {
3339
- 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;
3340
3160
  const senderId = entry.context.activity.from?.aadObjectId ?? entry.context.activity.from?.id ?? "";
3341
3161
  if (!senderId || !conversationId) return null;
3342
- return `msteams:${appId}:${conversationId}:${senderId}`;
3162
+ return JSON.stringify([
3163
+ "msteams",
3164
+ appId,
3165
+ conversationId,
3166
+ threadId ?? null,
3167
+ senderId
3168
+ ]);
3343
3169
  },
3344
3170
  shouldDebounce: (entry) => {
3345
3171
  if (!entry.text.trim()) return false;
@@ -3612,56 +3438,10 @@ async function isCardActionInvokeAuthorized(context, deps) {
3612
3438
  includeInvokeName: true
3613
3439
  });
3614
3440
  }
3615
- function registerMSTeamsHandlers(handler, deps) {
3441
+ function createMSTeamsActivityHandler(deps) {
3616
3442
  const handleTeamsMessage = createMSTeamsMessageHandler(deps);
3617
3443
  const handleReaction = createMSTeamsReactionHandler(deps);
3618
- const originalRun = handler.run;
3619
- if (originalRun) handler.run = async (context, turnAdoptionLifecycle) => {
3620
- const ctx = context;
3621
- if (ctx.activity?.type === "invoke" && ctx.activity?.name === "adaptiveCard/action") {
3622
- if (await maybeHandleMSTeamsApprovalCardSubmit({
3623
- context: ctx,
3624
- deps
3625
- })) return;
3626
- const text = serializeMSTeamsAdaptiveCardActionValue(ctx.activity?.value);
3627
- if (text) return await handleTeamsMessage({
3628
- ...ctx,
3629
- activity: {
3630
- ...ctx.activity,
3631
- type: "message",
3632
- text
3633
- }
3634
- }, turnAdoptionLifecycle);
3635
- return;
3636
- }
3637
- return originalRun.call(handler, context, turnAdoptionLifecycle);
3638
- };
3639
- handler.onMessage(async (context, next, turnAdoptionLifecycle) => {
3640
- let nextRan = false;
3641
- const runNext = async () => {
3642
- nextRan = true;
3643
- await next();
3644
- };
3645
- try {
3646
- const ctx = context;
3647
- if (await maybeHandleMSTeamsApprovalCardSubmit({
3648
- context: ctx,
3649
- deps
3650
- })) {
3651
- await runNext();
3652
- return;
3653
- }
3654
- const result = await handleTeamsMessage(ctx, turnAdoptionLifecycle);
3655
- await runNext();
3656
- return result;
3657
- } catch (err) {
3658
- if (turnAdoptionLifecycle) throw err;
3659
- deps.runtime.error(`msteams handler failed: ${require_channel_setup.formatUnknownError(err)}`);
3660
- }
3661
- if (!nextRan) await runNext();
3662
- });
3663
- handler.onMembersAdded(async (context, next) => {
3664
- const ctx = context;
3444
+ const handleMembersAdded = async (ctx) => {
3665
3445
  const membersAdded = ctx.activity?.membersAdded ?? [];
3666
3446
  const botId = ctx.activity?.recipient?.id;
3667
3447
  const msteamsCfg = deps.cfg.channels?.msteams;
@@ -3673,13 +3453,7 @@ function registerMSTeamsHandlers(handler, deps) {
3673
3453
  promptStarters: msteamsCfg?.promptStarters
3674
3454
  });
3675
3455
  try {
3676
- await ctx.sendActivity({
3677
- type: "message",
3678
- attachments: [{
3679
- contentType: "application/vnd.microsoft.card.adaptive",
3680
- content: card
3681
- }]
3682
- });
3456
+ await ctx.sendActivity(require_conversation_store_state.buildMSTeamsAdaptiveCardActivity(card));
3683
3457
  deps.log.info("sent welcome card");
3684
3458
  } catch (err) {
3685
3459
  deps.log.debug?.("failed to send welcome card", { error: require_channel_setup.formatUnknownError(err) });
@@ -3694,25 +3468,45 @@ function registerMSTeamsHandlers(handler, deps) {
3694
3468
  }
3695
3469
  } else deps.log.debug?.("skipping welcome (disabled by config or conversation type)");
3696
3470
  } else deps.log.debug?.("member added", { member: member.id });
3697
- await next();
3698
- });
3699
- handler.onReactionsAdded(async (context, next) => {
3700
- try {
3701
- await handleReaction(context, "added");
3702
- } catch (err) {
3703
- 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;
3704
3489
  }
3705
- await next();
3706
- });
3707
- handler.onReactionsRemoved(async (context, next) => {
3708
- try {
3709
- 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);
3710
3496
  } catch (err) {
3711
- 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)}`);
3712
3499
  }
3713
- await next();
3714
- });
3715
- 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
+ };
3716
3510
  }
3717
3511
  //#endregion
3718
3512
  //#region extensions/msteams/src/feedback-invoke.ts
@@ -3839,14 +3633,14 @@ async function handleMSTeamsFileConsentInvoke(context, log) {
3839
3633
  const expiredUploadMessage = "The file upload request has expired. Please try sending the file again.";
3840
3634
  const activity = context.activity;
3841
3635
  if (activity.type !== "invoke" || activity.name !== "fileConsent/invoke") return false;
3842
- const consentResponse = require_messenger.parseFileConsentInvoke(activity);
3636
+ const consentResponse = require_conversation_store_state.parseFileConsentInvoke(activity);
3843
3637
  if (!consentResponse) {
3844
3638
  log.debug?.("invalid file consent invoke", { value: activity.value });
3845
3639
  return false;
3846
3640
  }
3847
3641
  const uploadId = typeof consentResponse.context?.uploadId === "string" ? consentResponse.context.uploadId : void 0;
3848
- const inMemoryFile = require_messenger.getPendingUpload(uploadId);
3849
- 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);
3850
3644
  const pendingFile = inMemoryFile ?? fsFile;
3851
3645
  if (pendingFile) {
3852
3646
  const pendingConversationId = require_resolve_allowlist.normalizeMSTeamsConversationId(pendingFile.conversationId);
@@ -3869,33 +3663,29 @@ async function handleMSTeamsFileConsentInvoke(context, log) {
3869
3663
  size: pendingFile.buffer.length
3870
3664
  });
3871
3665
  try {
3872
- await require_messenger.uploadToConsentUrl({
3666
+ await require_conversation_store_state.uploadToConsentUrl({
3873
3667
  url: consentResponse.uploadInfo.uploadUrl,
3874
3668
  buffer: pendingFile.buffer,
3875
3669
  contentType: pendingFile.contentType
3876
3670
  });
3877
- const fileInfoCard = require_messenger.buildFileInfoCard({
3878
- filename: consentResponse.uploadInfo.name,
3879
- contentUrl: consentResponse.uploadInfo.contentUrl,
3880
- uniqueId: consentResponse.uploadInfo.uniqueId,
3881
- fileType: consentResponse.uploadInfo.fileType
3882
- });
3883
- if (!pendingFile.consentCardActivityId) await context.sendActivity({
3671
+ const fileActivity = {
3884
3672
  type: "message",
3885
- attachments: [fileInfoCard]
3886
- });
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
+ };
3887
3680
  if (pendingFile.consentCardActivityId) try {
3888
3681
  await context.updateActivity({
3889
- id: pendingFile.consentCardActivityId,
3890
- type: "message",
3891
- attachments: [fileInfoCard]
3682
+ ...fileActivity,
3683
+ id: pendingFile.consentCardActivityId
3892
3684
  });
3893
3685
  } catch {
3894
- await context.sendActivity({
3895
- type: "message",
3896
- attachments: [fileInfoCard]
3897
- });
3686
+ await context.sendActivity(fileActivity);
3898
3687
  }
3688
+ else await context.sendActivity(fileActivity);
3899
3689
  log.info("file upload complete", {
3900
3690
  uploadId,
3901
3691
  filename: consentResponse.uploadInfo.name,
@@ -3908,8 +3698,8 @@ async function handleMSTeamsFileConsentInvoke(context, log) {
3908
3698
  });
3909
3699
  await context.sendActivity("File upload failed. Please try again.");
3910
3700
  } finally {
3911
- require_messenger.removePendingUpload(uploadId);
3912
- await require_messenger.removePendingUploadFs(uploadId);
3701
+ require_conversation_store_state.removePendingUpload(uploadId);
3702
+ await require_conversation_store_state.removePendingUploadFs(uploadId);
3913
3703
  }
3914
3704
  } else {
3915
3705
  log.debug?.("pending file not found for consent", { uploadId });
@@ -3917,8 +3707,8 @@ async function handleMSTeamsFileConsentInvoke(context, log) {
3917
3707
  }
3918
3708
  } else {
3919
3709
  log.debug?.("user declined file consent", { uploadId });
3920
- require_messenger.removePendingUpload(uploadId);
3921
- await require_messenger.removePendingUploadFs(uploadId);
3710
+ require_conversation_store_state.removePendingUpload(uploadId);
3711
+ await require_conversation_store_state.removePendingUploadFs(uploadId);
3922
3712
  }
3923
3713
  return true;
3924
3714
  }
@@ -3929,7 +3719,7 @@ async function handleMSTeamsFileConsentInvoke(context, log) {
3929
3719
  */
3930
3720
  async function runMSTeamsFileConsentInvokeHandler(context, log) {
3931
3721
  try {
3932
- await require_messenger.withRevokedProxyFallback({
3722
+ await require_conversation_store_state.withRevokedProxyFallback({
3933
3723
  run: async () => await handleMSTeamsFileConsentInvoke(context, log),
3934
3724
  onRevoked: async () => true,
3935
3725
  onRevokedLog: () => {
@@ -3948,13 +3738,6 @@ function publishMSTeamsBlocked(statusSink, lastError) {
3948
3738
  function publishMSTeamsReady(statusSink, now = Date.now()) {
3949
3739
  statusSink?.((0, openclaw_plugin_sdk_gateway_runtime.channelReadyPatch)({ lastConnectedAt: now }));
3950
3740
  }
3951
- function publishMSTeamsRecovering(statusSink, lastError) {
3952
- statusSink?.({
3953
- connected: false,
3954
- lifecycle: "recovering",
3955
- lastError
3956
- });
3957
- }
3958
3741
  function publishMSTeamsStopped(statusSink) {
3959
3742
  statusSink?.((0, openclaw_plugin_sdk_gateway_runtime.channelStoppedPatch)());
3960
3743
  }
@@ -4124,7 +3907,7 @@ function createMSTeamsReplayContext(activity, app, serviceUrlBoundary) {
4124
3907
  ...threadActivityId ? { threadActivityId } : {},
4125
3908
  serviceUrlBoundary
4126
3909
  };
4127
- const sendActivity = (outbound) => require_messenger.sendMSTeamsActivityWithReference(app, reference, outbound, proactiveOptions);
3910
+ const sendActivity = (outbound) => require_conversation_store_state.sendMSTeamsActivityWithReference(app, reference, outbound, proactiveOptions);
4128
3911
  return {
4129
3912
  activity,
4130
3913
  sendActivity,
@@ -4134,26 +3917,108 @@ function createMSTeamsReplayContext(activity, app, serviceUrlBoundary) {
4134
3917
  return results;
4135
3918
  },
4136
3919
  updateActivity: async (outbound) => {
4137
- 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);
4138
3921
  },
4139
3922
  deleteActivity: async (activityId) => {
4140
- await require_messenger.deleteMSTeamsActivityWithReference(app, reference, activityId, proactiveOptions);
3923
+ await require_conversation_store_state.deleteMSTeamsActivityWithReference(app, reference, activityId, proactiveOptions);
4141
3924
  },
4142
3925
  getTeamDetails: (teamId) => app.api.teams.getById(teamId)
4143
3926
  };
4144
3927
  }
4145
3928
  //#endregion
4146
- //#region extensions/msteams/src/webhook-timeouts.ts
4147
- const MSTEAMS_WEBHOOK_INACTIVITY_TIMEOUT_MS = 3e4;
4148
- const MSTEAMS_WEBHOOK_REQUEST_TIMEOUT_MS = 3e4;
4149
- const MSTEAMS_WEBHOOK_HEADERS_TIMEOUT_MS = 15e3;
4150
- function applyMSTeamsWebhookTimeouts(httpServer, opts) {
4151
- const inactivityTimeoutMs = opts?.inactivityTimeoutMs ?? MSTEAMS_WEBHOOK_INACTIVITY_TIMEOUT_MS;
4152
- const requestTimeoutMs = opts?.requestTimeoutMs ?? MSTEAMS_WEBHOOK_REQUEST_TIMEOUT_MS;
4153
- const headersTimeoutMs = Math.min(opts?.headersTimeoutMs ?? MSTEAMS_WEBHOOK_HEADERS_TIMEOUT_MS, requestTimeoutMs);
4154
- httpServer.setTimeout(inactivityTimeoutMs);
4155
- httpServer.requestTimeout = requestTimeoutMs;
4156
- 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
+ };
4157
4022
  }
4158
4023
  //#endregion
4159
4024
  //#region extensions/msteams/src/monitor.ts
@@ -4267,20 +4132,26 @@ async function monitorMSTeamsProvider(opts) {
4267
4132
  msteams: msteamsCfg
4268
4133
  }
4269
4134
  };
4270
- 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
+ }
4271
4141
  const textLimit = core.channel.text.resolveTextChunkLimit(cfg, "msteams");
4272
4142
  const mediaMaxBytes = (0, openclaw_plugin_sdk_account_helpers.resolveChannelMediaMaxBytes)({
4273
4143
  cfg,
4274
4144
  resolveChannelLimitMb: ({ cfg: channelCfg }) => channelCfg.channels?.msteams?.mediaMaxMb
4275
4145
  }) ?? 8388608;
4276
- const conversationStore = opts.conversationStore ?? require_polls.createMSTeamsConversationStoreState();
4277
- const pollStore = opts.pollStore ?? require_polls.createMSTeamsPollStoreState();
4278
- 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");
4279
4149
  const express = await import("express");
4280
4150
  const expressApp = express.default();
4151
+ const privateQaToken = await require_resolve_allowlist.resolveMSTeamsPrivateQaRuntime()?.token();
4281
4152
  expressApp.use((req, res, next) => {
4282
4153
  const auth = req.headers.authorization;
4283
- if (!auth || !auth.startsWith("Bearer ")) {
4154
+ if (!auth || !auth.startsWith("Bearer ") || privateQaToken && !(0, openclaw_plugin_sdk_security_runtime.safeEqualSecret)(auth.slice(7), privateQaToken)) {
4284
4155
  res.status(401).json({ error: "Unauthorized" });
4285
4156
  return;
4286
4157
  }
@@ -4294,7 +4165,7 @@ async function monitorMSTeamsProvider(opts) {
4294
4165
  }
4295
4166
  next(err);
4296
4167
  });
4297
- const configuredPath = msteamsCfg.webhook?.path ?? "/api/messages";
4168
+ const configuredPath = msteamsCfg.webhook?.path || "/api/messages";
4298
4169
  const ssoConnectionName = msteamsCfg.sso?.enabled && msteamsCfg.sso.connectionName ? msteamsCfg.sso.connectionName : void 0;
4299
4170
  const { app } = await require_resolve_allowlist.loadMSTeamsSdkWithAuth(creds, {
4300
4171
  ...require_resolve_allowlist.resolveMSTeamsSdkCloudOptions(msteamsCfg),
@@ -4315,11 +4186,10 @@ async function monitorMSTeamsProvider(opts) {
4315
4186
  }
4316
4187
  const tokenProvider = require_resolve_allowlist.createMSTeamsTokenProvider(app);
4317
4188
  const ssoDeps = ssoConnectionName ? {
4318
- tokenStore: require_sso_token_store.createMSTeamsSsoTokenStoreFs(),
4189
+ tokenStore: createMSTeamsSsoTokenStoreFs(),
4319
4190
  connectionName: ssoConnectionName
4320
4191
  } : void 0;
4321
4192
  if (ssoDeps) log.debug?.("msteams sso enabled", { connectionName: ssoDeps.connectionName });
4322
- const handler = buildActivityHandler();
4323
4193
  const handlerDeps = {
4324
4194
  cfg,
4325
4195
  runtime,
@@ -4332,37 +4202,29 @@ async function monitorMSTeamsProvider(opts) {
4332
4202
  pollStore,
4333
4203
  log
4334
4204
  };
4335
- registerMSTeamsHandlers(handler, handlerDeps);
4205
+ const handleActivity = createMSTeamsActivityHandler(handlerDeps);
4336
4206
  const ingress = createMSTeamsIngress({
4337
4207
  accountId: appId,
4338
4208
  runtime,
4339
4209
  dispatch: async (activity, lifecycle, liveContext) => {
4340
4210
  if (liveContext) liveContext.activity = activity;
4341
4211
  const context = liveContext ?? createMSTeamsReplayContext(activity, app, require_resolve_allowlist.resolveMSTeamsSdkCloudOptions(msteamsCfg));
4342
- return await handler.run(context, lifecycle);
4212
+ return await handleActivity(context, lifecycle);
4343
4213
  }
4344
4214
  });
4345
4215
  app.on("card.action", async (ctx) => {
4346
4216
  const adaptedCtx = adaptSdkContext(ctx, app);
4347
4217
  try {
4348
4218
  const activity = adaptedCtx.activity;
4349
- const vote = require_polls.extractMSTeamsPollVote(activity);
4219
+ const vote = require_conversation_store_state.extractMSTeamsPollVote(activity);
4350
4220
  if (vote) {
4351
4221
  const voterId = activity?.from?.aadObjectId ?? activity?.from?.id ?? "unknown";
4352
4222
  try {
4353
- if (!await isCardActionInvokeAuthorized(adaptedCtx, handlerDeps)) return {
4354
- statusCode: 200,
4355
- type: "application/vnd.microsoft.activity.message",
4356
- value: "Not authorized."
4357
- };
4223
+ if (!await isCardActionInvokeAuthorized(adaptedCtx, handlerDeps)) return cardActionMessage("Not authorized.");
4358
4224
  const existingPoll = await pollStore.getPoll(vote.pollId);
4359
4225
  if (!existingPoll) {
4360
4226
  log.debug?.("poll vote ignored (poll not found)", { pollId: vote.pollId });
4361
- return {
4362
- statusCode: 200,
4363
- type: "application/vnd.microsoft.activity.message",
4364
- value: "Poll not found."
4365
- };
4227
+ return cardActionMessage("Poll not found.");
4366
4228
  }
4367
4229
  const pollConversationId = existingPoll.conversationId ? require_resolve_allowlist.normalizeMSTeamsConversationId(existingPoll.conversationId) : void 0;
4368
4230
  const activityConversationId = require_resolve_allowlist.normalizeMSTeamsConversationId(activity?.conversation?.id ?? "");
@@ -4372,11 +4234,7 @@ async function monitorMSTeamsProvider(opts) {
4372
4234
  expectedConversationId: pollConversationId,
4373
4235
  receivedConversationId: activityConversationId || void 0
4374
4236
  });
4375
- return {
4376
- statusCode: 200,
4377
- type: "application/vnd.microsoft.activity.message",
4378
- value: "Poll not found."
4379
- };
4237
+ return cardActionMessage("Poll not found.");
4380
4238
  }
4381
4239
  if (await pollStore.recordVote({
4382
4240
  pollId: vote.pollId,
@@ -4387,18 +4245,10 @@ async function monitorMSTeamsProvider(opts) {
4387
4245
  pollId: vote.pollId,
4388
4246
  voterId
4389
4247
  });
4390
- return {
4391
- statusCode: 200,
4392
- type: "application/vnd.microsoft.activity.message",
4393
- value: "Vote recorded."
4394
- };
4248
+ return cardActionMessage("Vote recorded.");
4395
4249
  }
4396
4250
  log.debug?.("poll vote ignored (poll not found)", { pollId: vote.pollId });
4397
- return {
4398
- statusCode: 200,
4399
- type: "application/vnd.microsoft.activity.message",
4400
- value: "Poll not found."
4401
- };
4251
+ return cardActionMessage("Poll not found.");
4402
4252
  } catch (err) {
4403
4253
  log.error("failed to record poll vote", {
4404
4254
  pollId: vote.pollId,
@@ -4419,11 +4269,7 @@ async function monitorMSTeamsProvider(opts) {
4419
4269
  }
4420
4270
  }
4421
4271
  await ingress.accept(activity, adaptedCtx);
4422
- return {
4423
- statusCode: 200,
4424
- type: "application/vnd.microsoft.activity.message",
4425
- value: "OK"
4426
- };
4272
+ return cardActionMessage("OK");
4427
4273
  } catch (err) {
4428
4274
  log.error("msteams card.action failed", { error: require_channel_setup.formatUnknownError(err) });
4429
4275
  return {
@@ -4446,26 +4292,27 @@ async function monitorMSTeamsProvider(opts) {
4446
4292
  app.on("file.consent.decline", (ctx) => {
4447
4293
  runMSTeamsFileConsentInvokeHandler(adaptSdkContext(ctx, app), log);
4448
4294
  });
4449
- const handleSdkSigninInvoke = async (ctx, delegateName) => {
4450
- const adaptedCtx = adaptSdkContext(ctx, app);
4451
- 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 {
4452
4304
  status: 200,
4453
4305
  body: {}
4454
4306
  };
4455
4307
  if (!ssoDeps) {
4456
- 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 });
4457
4309
  return {
4458
4310
  status: 200,
4459
4311
  body: {}
4460
4312
  };
4461
4313
  }
4462
- const sdkSigninApp = app;
4463
- const delegate = sdkSigninApp[delegateName];
4464
- if (typeof delegate !== "function") throw new Error(`Teams SDK ${delegateName} handler is unavailable`);
4465
- return delegate.call(sdkSigninApp, ctx);
4314
+ return processActivity(event);
4466
4315
  };
4467
- app.on("signin.token-exchange", (ctx) => handleSdkSigninInvoke(ctx, "onTokenExchange"));
4468
- app.on("signin.verify-state", (ctx) => handleSdkSigninInvoke(ctx, "onVerifyState"));
4469
4316
  if (ssoDeps) app.event("signin", (ctx) => {
4470
4317
  (async () => {
4471
4318
  if (!await isSigninInvokeAuthorized(adaptSdkContext(ctx, app), handlerDeps)) return;
@@ -4512,93 +4359,76 @@ async function monitorMSTeamsProvider(opts) {
4512
4359
  return;
4513
4360
  }
4514
4361
  try {
4515
- await handler.run(adaptedCtx);
4362
+ await handleActivity(adaptedCtx);
4516
4363
  } catch (err) {
4517
4364
  log.error("msteams non-turn activity failed", { error: require_channel_setup.formatUnknownError(err) });
4518
4365
  }
4519
4366
  });
4520
4367
  await app.initialize();
4521
4368
  ingress.start();
4522
- const privateQaRuntime = require_resolve_allowlist.resolveMSTeamsPrivateQaRuntime();
4523
- const httpServer = await new Promise((resolve, reject) => {
4524
- const onListen = (err) => err ? reject(err) : resolve(server);
4525
- const server = privateQaRuntime ? expressApp.listen(port, privateQaRuntime.listenHost, onListen) : expressApp.listen(port, onListen);
4526
- }).catch(async (err) => {
4527
- 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();
4528
4401
  await ingress.stop();
4529
- throw err;
4530
- });
4531
- log.info(`msteams provider started on port ${port}`);
4402
+ throw error;
4403
+ }
4404
+ log.info(`msteams provider started on Gateway route ${configuredPath}`);
4532
4405
  publishMSTeamsReady(opts.statusSink);
4533
- applyMSTeamsWebhookTimeouts(httpServer);
4534
- httpServer.on("error", (err) => {
4535
- log.error("msteams server error", { error: require_channel_setup.formatUnknownError(err) });
4536
- publishMSTeamsRecovering(opts.statusSink, require_channel_setup.formatUnknownError(err));
4537
- });
4538
- const shutdown = async () => {
4539
- log.info("shutting down msteams provider");
4540
- await new Promise((resolve) => {
4541
- httpServer.close((err) => {
4542
- if (err) log.debug?.("msteams server close error", { error: require_channel_setup.formatUnknownError(err) });
4543
- resolve();
4544
- });
4545
- });
4546
- await ingress.stop();
4547
- 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;
4548
4415
  };
4549
- await (0, openclaw_plugin_sdk_channel_outbound.keepHttpServerTaskAlive)({
4550
- server: httpServer,
4551
- abortSignal: opts.abortSignal,
4552
- onAbort: shutdown
4553
- });
4416
+ try {
4417
+ await (0, openclaw_plugin_sdk_channel_outbound.waitUntilAbort)(opts.abortSignal, shutdown);
4418
+ } finally {
4419
+ await shutdown();
4420
+ }
4554
4421
  return {
4555
4422
  app: expressApp,
4556
4423
  shutdown
4557
4424
  };
4558
4425
  }
4559
- /**
4560
- * Build a minimal ActivityHandler-compatible object that supports
4561
- * onMessage / onMembersAdded registration and a run() method.
4562
- */
4563
- function buildActivityHandler() {
4564
- const messageHandlers = [];
4565
- const membersAddedHandlers = [];
4566
- const reactionsAddedHandlers = [];
4567
- const reactionsRemovedHandlers = [];
4568
- const handler = {
4569
- onMessage(cb) {
4570
- messageHandlers.push(cb);
4571
- return handler;
4572
- },
4573
- onMembersAdded(cb) {
4574
- membersAddedHandlers.push(cb);
4575
- return handler;
4576
- },
4577
- onReactionsAdded(cb) {
4578
- reactionsAddedHandlers.push(cb);
4579
- return handler;
4580
- },
4581
- onReactionsRemoved(cb) {
4582
- reactionsRemovedHandlers.push(cb);
4583
- return handler;
4584
- },
4585
- async run(context, turnAdoptionLifecycle) {
4586
- const ctx = context;
4587
- const activityType = ctx?.activity?.type;
4588
- const noop = async () => {};
4589
- if (activityType === "message") for (const h of messageHandlers) {
4590
- const result = await h(context, noop, turnAdoptionLifecycle);
4591
- if (result) return result;
4592
- }
4593
- else if (activityType === "conversationUpdate") for (const h of membersAddedHandlers) await h(context, noop);
4594
- else if (activityType === "messageReaction") {
4595
- const activity = ctx?.activity;
4596
- if (activity?.reactionsAdded?.length) for (const h of reactionsAddedHandlers) await h(context, noop);
4597
- if (activity?.reactionsRemoved?.length) for (const h of reactionsRemovedHandlers) await h(context, noop);
4598
- }
4599
- }
4426
+ function cardActionMessage(value) {
4427
+ return {
4428
+ statusCode: 200,
4429
+ type: "application/vnd.microsoft.activity.message",
4430
+ value
4600
4431
  };
4601
- return handler;
4602
4432
  }
4603
4433
  /**
4604
4434
  * Adapt a new @microsoft/teams.apps SDK context to the MSTeamsTurnContext interface