@openclaw/msteams 2026.10.10 → 2026.11.5-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/dist/.setup/{adapter.runtime-BMvpm7sS.cjs → adapter.runtime-UOD_H_ph.cjs} +20 -23
  2. package/dist/.setup/{approval-card-Bo8kHYbj.cjs → approval-card-CD5q0Nla.cjs} +5 -9
  3. package/dist/.setup/{approval-handler.runtime-BS3avHFu.cjs → approval-handler.runtime-DbPxvynk.cjs} +4 -4
  4. package/dist/.setup/{channel-CdCSk3Rg.cjs → channel-DQrXOfhf.cjs} +24 -224
  5. package/dist/.setup/{channel.runtime--ZjTYgkY.cjs → channel.runtime-Du4GPMES.cjs} +65 -106
  6. package/dist/.setup/{channel.setup-nSTtK77J.cjs → channel.setup--7ko_Kxv.cjs} +161 -262
  7. package/dist/.setup/{config-schema-Bm15lF-j.cjs → config-schema-BEXDIOC-.cjs} +120 -123
  8. package/dist/.setup/{conversation-store-state-NJU9osd-.cjs → conversation-store-state-AVgRyNE9.cjs} +411 -598
  9. package/dist/.setup/{delegated-state-CbMd3rIh.cjs → delegated-state-DhIHahf1.cjs} +68 -6
  10. package/dist/.setup/{monitor-BxLDs5fk.cjs → monitor-CXjIKz7o.cjs} +1883 -2284
  11. package/dist/.setup/{oauth-B-Y5pNM4.cjs → oauth-BWG6qWCd.cjs} +10 -20
  12. package/dist/.setup/presentation-CaLawc5j.cjs +211 -0
  13. package/dist/.setup/{reaction-types-BaL1EVBS.cjs → reaction-types-DOzN5oTl.cjs} +1 -1
  14. package/dist/.setup/{resolve-allowlist-CqNzS9bZ.cjs → resolve-allowlist-Drj1jaq0.cjs} +213 -90
  15. package/dist/.setup/{secret-contract-api-BOzdWSn9.cjs → secret-contract-api-DFqDvc_a.cjs} +4 -6
  16. package/dist/.setup/{secret-contract-api-5aH1Fl_c.cjs → secret-contract-api-YGHrfAT0.cjs} +1 -1
  17. package/dist/.setup/{send-BXjI3ufq.cjs → send-BRUztzVJ.cjs} +86 -182
  18. package/dist/.setup/{setup-plugin-api-lPpin6Gg.cjs → setup-plugin-api-BHEfter5.cjs} +1 -1
  19. package/dist/.setup/{thread-session-DZMoCsSR.cjs → thread-session-BUUV1zk_.cjs} +4 -6
  20. package/dist/.setup/{token-config-DTZ00Ibq.cjs → token-config-7K6UbNCo.cjs} +0 -2
  21. package/dist/.source-module-urls/7e43ee3ae4faab430a47269c8217f8d99ec60ecd85233f40e81d03efeb543068.json +1 -1
  22. package/dist/api.cjs +2 -2
  23. package/dist/channel-config-api.cjs +1 -1
  24. package/dist/channel-plugin-api.cjs +1 -1
  25. package/dist/directory-contract-api.cjs +1 -1
  26. package/dist/doctor-contract-api.cjs +6 -13
  27. package/dist/qa-runner-api.cjs +1 -1
  28. package/dist/secret-contract-api.cjs +1 -1
  29. package/dist/setup-entry.cjs +2 -2
  30. package/dist/setup-plugin-api.cjs +1 -1
  31. package/openclaw.plugin.json +0 -30
  32. package/package.json +11 -11
@@ -1,154 +1,49 @@
1
- const require_rolldown_runtime = require("./rolldown-runtime-CiKaZLfC.cjs");
2
1
  const require_bot_framework_service_url = require("./bot-framework-service-url-CgVuKiIR.cjs");
3
- const require_resolve_allowlist = require("./resolve-allowlist-CqNzS9bZ.cjs");
2
+ const require_resolve_allowlist = require("./resolve-allowlist-Drj1jaq0.cjs");
4
3
  const require_runtime = require("./runtime-CbKazLGv.cjs");
5
4
  require("../runtime-api.cjs");
6
- const require_delegated_state = require("./delegated-state-CbMd3rIh.cjs");
7
- const require_channel = require("./channel-CdCSk3Rg.cjs");
8
- const require_thread_session = require("./thread-session-DZMoCsSR.cjs");
9
- const require_channel_setup = require("./channel.setup-nSTtK77J.cjs");
10
- const require_approval_card = require("./approval-card-Bo8kHYbj.cjs");
11
- const require_conversation_store_state = require("./conversation-store-state-NJU9osd-.cjs");
12
- const require_reaction_types = require("./reaction-types-BaL1EVBS.cjs");
5
+ const require_delegated_state = require("./delegated-state-DhIHahf1.cjs");
6
+ const require_channel = require("./channel-DQrXOfhf.cjs");
7
+ const require_presentation = require("./presentation-CaLawc5j.cjs");
8
+ const require_thread_session = require("./thread-session-BUUV1zk_.cjs");
9
+ const require_channel_setup = require("./channel.setup--7ko_Kxv.cjs");
10
+ const require_approval_card = require("./approval-card-CD5q0Nla.cjs");
11
+ const require_conversation_store_state = require("./conversation-store-state-AVgRyNE9.cjs");
12
+ const require_reaction_types = require("./reaction-types-DOzN5oTl.cjs");
13
13
  let openclaw_plugin_sdk_channel_outbound = require("openclaw/plugin-sdk/channel-outbound");
14
+ let openclaw_plugin_sdk_interactive_runtime = require("openclaw/plugin-sdk/interactive-runtime");
14
15
  let openclaw_plugin_sdk_string_coerce_runtime = require("openclaw/plugin-sdk/string-coerce-runtime");
15
16
  let openclaw_plugin_sdk_account_id = require("openclaw/plugin-sdk/account-id");
16
17
  let openclaw_plugin_sdk_allow_from = require("openclaw/plugin-sdk/allow-from");
17
18
  let openclaw_plugin_sdk_provider_http = require("openclaw/plugin-sdk/provider-http");
18
19
  let openclaw_plugin_sdk_media_runtime = require("openclaw/plugin-sdk/media-runtime");
19
20
  let openclaw_plugin_sdk_ssrf_runtime = require("openclaw/plugin-sdk/ssrf-runtime");
20
- let node_dns_promises = require("node:dns/promises");
21
21
  let openclaw_plugin_sdk_ssrf_policy = require("openclaw/plugin-sdk/ssrf-policy");
22
22
  let openclaw_plugin_sdk_number_runtime = require("openclaw/plugin-sdk/number-runtime");
23
23
  let openclaw_plugin_sdk_text_utility_runtime = require("openclaw/plugin-sdk/text-utility-runtime");
24
24
  let openclaw_plugin_sdk_error_runtime = require("openclaw/plugin-sdk/error-runtime");
25
- let node_crypto = require("node:crypto");
26
- node_crypto = require_rolldown_runtime.__toESM(node_crypto, 1);
25
+ let openclaw_plugin_sdk_node_crypto_runtime = require("openclaw/plugin-sdk/node-crypto-runtime");
27
26
  let openclaw_plugin_sdk_webhook_targets = require("openclaw/plugin-sdk/webhook-targets");
28
- let openclaw_plugin_sdk_routing = require("openclaw/plugin-sdk/routing");
29
27
  let openclaw_plugin_sdk_channel_inbound = require("openclaw/plugin-sdk/channel-inbound");
28
+ let node_dns_promises = require("node:dns/promises");
29
+ let openclaw_plugin_sdk_gateway_runtime = require("openclaw/plugin-sdk/gateway-runtime");
30
30
  let openclaw_plugin_sdk_security_runtime = require("openclaw/plugin-sdk/security-runtime");
31
31
  let openclaw_plugin_sdk_approval_gateway_runtime = require("openclaw/plugin-sdk/approval-gateway-runtime");
32
32
  let openclaw_plugin_sdk_channel_ingress_runtime = require("openclaw/plugin-sdk/channel-ingress-runtime");
33
33
  let openclaw_plugin_sdk_context_visibility_runtime = require("openclaw/plugin-sdk/context-visibility-runtime");
34
34
  let openclaw_plugin_sdk_reply_history = require("openclaw/plugin-sdk/reply-history");
35
35
  let openclaw_plugin_sdk_runtime_config_snapshot = require("openclaw/plugin-sdk/runtime-config-snapshot");
36
- let openclaw_plugin_sdk_request_url = require("openclaw/plugin-sdk/request-url");
37
36
  let openclaw_plugin_sdk_agent_runtime = require("openclaw/plugin-sdk/agent-runtime");
38
37
  let openclaw_plugin_sdk_extension_shared = require("openclaw/plugin-sdk/extension-shared");
39
38
  let openclaw_plugin_sdk_plugin_runtime = require("openclaw/plugin-sdk/plugin-runtime");
40
39
  let openclaw_plugin_sdk_collection_runtime = require("openclaw/plugin-sdk/collection-runtime");
41
- let openclaw_plugin_sdk_gateway_runtime = require("openclaw/plugin-sdk/gateway-runtime");
40
+ let openclaw_plugin_sdk_request_url = require("openclaw/plugin-sdk/request-url");
42
41
  let openclaw_plugin_sdk_time_runtime = require("openclaw/plugin-sdk/time-runtime");
43
42
  let openclaw_plugin_sdk_channel_pairing = require("openclaw/plugin-sdk/channel-pairing");
44
43
  let openclaw_plugin_sdk_runtime_group_policy = require("openclaw/plugin-sdk/runtime-group-policy");
45
44
  let openclaw_plugin_sdk_dangerous_name_runtime = require("openclaw/plugin-sdk/dangerous-name-runtime");
46
45
  let openclaw_plugin_sdk_account_helpers = require("openclaw/plugin-sdk/account-helpers");
47
46
  let openclaw_plugin_sdk_webhook_ingress = require("openclaw/plugin-sdk/webhook-ingress");
48
- // extensions/msteams/src/feedback-reflection-store.ts
49
- const LEARNINGS_NAMESPACE = "feedback-learnings";
50
- const MAX_LEARNING_ENTRIES = 1e4;
51
- function learningStoreKey(storePath, sessionKey) {
52
- return node_crypto.default.createHash("sha256").update(`${storePath}\0${sessionKey}`, "utf8").digest("hex");
53
- }
54
- async function storeSessionLearning(params) {
55
- const store = require_runtime.getMSTeamsRuntime().state.openKeyedStore({
56
- namespace: LEARNINGS_NAMESPACE,
57
- maxEntries: MAX_LEARNING_ENTRIES
58
- });
59
- const key = learningStoreKey(params.storePath, params.sessionKey);
60
- if (!store.observe || !store.compareAndApply) throw new Error("plugin state atomic comparison is unavailable");
61
- const prepared = {
62
- sessionKey: params.sessionKey,
63
- learning: params.learning,
64
- updatedAt: Date.now()
65
- };
66
- let observed = await store.observe(key);
67
- while (true) {
68
- const result = await store.compareAndApply(key, observed.comparison, {
69
- operation: "update",
70
- action: "set",
71
- value: {
72
- sessionKey: prepared.sessionKey,
73
- learnings: [...observed.value?.learnings ?? [], prepared.learning].slice(-10),
74
- updatedAt: prepared.updatedAt
75
- }
76
- });
77
- if (result.status !== "conflict") return;
78
- observed = result.current;
79
- }
80
- }
81
- // extensions/msteams/src/feedback-reflection.ts
82
- /**
83
- * Run a background reflection after negative feedback.
84
- * This is designed to be called fire-and-forget (don't await in the invoke handler).
85
- */
86
- async function runFeedbackReflection(params) {
87
- const { cfg, log, sessionKey } = params;
88
- const msteamsCfg = require_resolve_allowlist.resolveMSTeamsAccountConfig(cfg, params.accountId);
89
- const cooldownMs = msteamsCfg.feedbackReflectionCooldownMs ?? openclaw_plugin_sdk_channel_inbound.DEFAULT_CHANNEL_FEEDBACK_REFLECTION_COOLDOWN_MS;
90
- let reflection;
91
- try {
92
- reflection = await (0, openclaw_plugin_sdk_channel_inbound.runChannelFeedbackReflection)({
93
- cfg,
94
- channel: "msteams",
95
- channelLabel: "Teams",
96
- agentId: params.agentId,
97
- sessionKey,
98
- conversationId: params.conversationId,
99
- conversationKind: params.conversationKind,
100
- userComment: params.userComment,
101
- cooldownMs,
102
- onRecordError: (err) => log.debug?.("reflection session record failed", { error: require_channel_setup.formatUnknownError(err) }),
103
- onDispatchError: (err) => log.debug?.("reflection reply error", { error: require_channel_setup.formatUnknownError(err) })
104
- });
105
- } catch (err) {
106
- log.error("reflection dispatch failed", { error: require_channel_setup.formatUnknownError(err) });
107
- return;
108
- }
109
- if (reflection.status === "cooldown") {
110
- log.debug?.("skipping reflection (cooldown active)", { sessionKey });
111
- return;
112
- }
113
- if (reflection.status === "empty") {
114
- log.debug?.("reflection produced no output");
115
- return;
116
- }
117
- log.info("reflection complete", {
118
- sessionKey,
119
- responseLength: reflection.responseLength,
120
- followUp: reflection.followUp
121
- });
122
- try {
123
- await storeSessionLearning({
124
- storePath: reflection.storePath,
125
- sessionKey,
126
- learning: reflection.learning
127
- });
128
- } catch (err) {
129
- log.debug?.("failed to store reflection learning", { error: require_channel_setup.formatUnknownError(err) });
130
- }
131
- const conversationType = (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeOptionalLowercaseString)(params.conversationRef.conversation?.conversationType);
132
- if (!(conversationType === "personal" && reflection.followUp && Boolean(reflection.userMessage))) {
133
- if (reflection.followUp && conversationType !== "personal") log.debug?.("skipping reflection follow-up outside direct message", {
134
- sessionKey,
135
- conversationType
136
- });
137
- return;
138
- }
139
- try {
140
- await require_conversation_store_state.sendMSTeamsActivityWithReference(params.app, require_conversation_store_state.buildConversationReference(params.conversationRef), {
141
- type: "message",
142
- text: reflection.userMessage
143
- }, {
144
- accountId: params.accountId,
145
- serviceUrlBoundary: require_resolve_allowlist.resolveMSTeamsSdkCloudOptions(msteamsCfg)
146
- });
147
- log.info("sent reflection follow-up", { sessionKey });
148
- } catch (err) {
149
- log.debug?.("failed to send reflection follow-up", { error: require_channel_setup.formatUnknownError(err) });
150
- }
151
- }
152
47
  // extensions/msteams/src/adaptive-card-submit.ts
153
48
  function extractAdaptiveCardSubmittedData(value) {
154
49
  if (!(0, openclaw_plugin_sdk_string_coerce_runtime.isRecord)(value)) return value;
@@ -332,7 +227,7 @@ async function resolveMSTeamsSenderAccess(params) {
332
227
  const defaultGroupPolicy = (0, openclaw_plugin_sdk_runtime_group_policy.resolveDefaultGroupPolicy)(params.cfg);
333
228
  const groupPolicy = !isDirectMessage && msteamsCfg ? msteamsCfg.groupPolicy ?? defaultGroupPolicy ?? "allowlist" : "disabled";
334
229
  const allowNameMatching = (0, openclaw_plugin_sdk_dangerous_name_runtime.isDangerousNameMatchingEnabled)(msteamsCfg);
335
- const channelGate = require_channel.resolveMSTeamsRouteConfig({
230
+ const channelGate = require_presentation.resolveMSTeamsRouteConfig({
336
231
  cfg: msteamsCfg,
337
232
  teamId: activity.channelData?.team?.id,
338
233
  teamName: activity.channelData?.team?.name,
@@ -533,7 +428,6 @@ async function admitMSTeamsMessage(params) {
533
428
  allowTextCommands,
534
429
  isControlCommand,
535
430
  commandAuthorized: commandAccess.requested ? commandAccess.authorized : void 0,
536
- effectiveDmAllowFrom,
537
431
  effectiveGroupAllowFrom,
538
432
  isChannel: params.isChannel
539
433
  };
@@ -549,1877 +443,1532 @@ function resolveBotThreadMentionPolicy(params) {
549
443
  implicitMentionKinds: params.requireMentionInBotThreads ? params.implicitMentionKinds?.filter((kind) => kind === "native") : params.implicitMentionKinds
550
444
  };
551
445
  }
552
- // extensions/msteams/src/monitor-handler/config.ts
553
- function createMSTeamsHandlerConfigReader(deps) {
554
- const source = deps.accountPolicyCfg ?? deps.cfg;
555
- const read = (0, openclaw_plugin_sdk_runtime_config_snapshot.createRuntimeConfigReader)(source);
556
- const originallyConfigured = require_resolve_allowlist.listMSTeamsAccountIds(source).includes(deps.accountId);
557
- let lastSource;
558
- let lastProjection = deps.cfg;
559
- return () => {
560
- const current = read();
561
- if (current === lastSource) return lastProjection;
562
- lastSource = current;
563
- if (current === source && deps.accountPolicyCfg) return lastProjection = deps.cfg;
564
- const retired = originallyConfigured && !require_resolve_allowlist.listMSTeamsAccountIds(current).includes(deps.accountId);
565
- if (deps.accountId === "default" && !current.channels?.msteams?.accounts && !retired) return lastProjection = current;
566
- const accountConfig = require_resolve_allowlist.resolveMSTeamsAccountConfig(current, deps.accountId);
567
- return lastProjection = require_resolve_allowlist.withAccountScopedMSTeamsConfig({
568
- cfg: current,
569
- accountId: deps.accountId,
570
- accountConfig: {
571
- ...accountConfig,
572
- ...current.channels?.msteams?.enabled === false || retired ? { enabled: false } : {}
573
- }
574
- });
575
- };
576
- }
577
- // extensions/msteams/src/attachments/bot-framework.ts
578
- const BOT_FRAMEWORK_SCOPE = "https://api.botframework.com";
579
- /**
580
- * Detect Bot Framework personal chat ("a:") and MSA orgid ("8:orgid:") conversation
581
- * IDs. These identifiers are not recognized by Graph's `/chats/{id}` endpoint, so we
582
- * must fetch media via the Bot Framework v3 attachments endpoint instead.
583
- *
584
- * Graph-compatible IDs start with `19:` and are left untouched by this detector.
585
- */
586
- function isBotFrameworkPersonalChatId(conversationId) {
587
- if (typeof conversationId !== "string") return false;
588
- const trimmed = conversationId.trim();
589
- return trimmed.startsWith("a:") || trimmed.startsWith("8:orgid:");
590
- }
591
- async function fetchBotFrameworkAttachment(params, kind) {
592
- let response;
593
- try {
594
- const headers = require_resolve_allowlist.ensureUserAgentHeader();
595
- require_resolve_allowlist.applyAuthorizationHeaderForUrl({
596
- headers,
597
- url: params.url,
598
- authAllowHosts: params.policy.authAllowHosts,
599
- bearerToken: params.accessToken
600
- });
601
- response = await require_resolve_allowlist.safeFetchWithPolicy({
602
- url: params.url,
603
- policy: params.policy,
604
- resolveFn: node_dns_promises.lookup,
605
- requestInit: { headers },
606
- timeoutMs: require_resolve_allowlist.resolveMSTeamsRequestTimeoutMs(params.deadline)
607
- });
608
- } catch (err) {
609
- params.logger?.warn?.(`msteams botFramework ${kind} fetch failed`, { error: (0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err) });
610
- return;
611
- }
612
- if (!response.ok) {
613
- await response.body?.cancel().catch(() => void 0);
614
- params.logger?.warn?.(`msteams botFramework ${kind} non-ok`, { status: response.status });
615
- return;
616
- }
617
- return response;
618
- }
619
- async function fetchBotFrameworkAttachmentInfo(params) {
620
- const response = await fetchBotFrameworkAttachment(params, "attachmentInfo");
621
- if (!response) return;
622
- try {
623
- return await (0, openclaw_plugin_sdk_provider_http.readProviderJsonResponse)(response, "msteams botFramework attachmentInfo");
624
- } catch (err) {
625
- params.logger?.warn?.("msteams botFramework attachmentInfo parse failed", { error: (0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err) });
626
- return;
446
+ // extensions/msteams/src/informative-status.ts
447
+ const INFORMATIVE_MAX_CHARS = 1e3;
448
+ const INFORMATIVE_MAX_BYTES = 1024;
449
+ const ELLIPSIS = "…";
450
+ const segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
451
+ const utf8 = new TextEncoder();
452
+ function flattenInformativeStatus(text) {
453
+ const joined = text.split("\n").map((line) => line.trim().replace(/^[•-]\s+/u, "")).filter(Boolean).join(" · ");
454
+ if (joined.length <= INFORMATIVE_MAX_CHARS && utf8.encode(joined).length <= INFORMATIVE_MAX_BYTES) return joined;
455
+ const graphemes = Array.from(segmenter.segment(joined), (s) => s.segment);
456
+ let chars = 1;
457
+ let bytes = utf8.encode(ELLIPSIS).length;
458
+ let start = graphemes.length;
459
+ while (start > 0) {
460
+ const next = graphemes[start - 1];
461
+ const nextBytes = utf8.encode(next).length;
462
+ if (chars + next.length > INFORMATIVE_MAX_CHARS || bytes + nextBytes > INFORMATIVE_MAX_BYTES) break;
463
+ chars += next.length;
464
+ bytes += nextBytes;
465
+ start -= 1;
627
466
  }
467
+ return ELLIPSIS + graphemes.slice(start).join("");
628
468
  }
629
- async function saveBotFrameworkAttachmentView(params) {
630
- const response = await fetchBotFrameworkAttachment(params, "attachmentView");
631
- if (!response) return;
632
- try {
633
- return await require_runtime.getMSTeamsRuntime().channel.media.saveResponseMedia(response, {
634
- sourceUrl: params.url,
635
- filePathHint: params.fileNameHint,
636
- maxBytes: params.maxBytes,
637
- fallbackContentType: params.contentTypeHint,
638
- subdir: "inbound"
639
- });
640
- } catch (err) {
641
- params.logger?.warn?.("msteams botFramework attachmentView save failed", { error: (0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err) });
642
- return;
643
- } finally {
644
- await response.body?.cancel().catch(() => void 0);
645
- }
469
+ // extensions/msteams/src/reply-stream-controller.ts
470
+ function hasUnstreamedContent(payload) {
471
+ return Boolean(payload.presentation || payload.mediaUrl || payload.mediaUrls?.length);
646
472
  }
647
- async function downloadMSTeamsBotFrameworkAttachment(params) {
648
- if (!params.serviceUrl || !params.attachmentId || !params.tokenProvider) return;
649
- const tokenProvider = params.tokenProvider;
650
- const policy = require_resolve_allowlist.resolveAttachmentFetchPolicy({
651
- allowHosts: params.allowHosts,
652
- authAllowHosts: params.authAllowHosts
473
+ /** Bridge reply callbacks to the SDK stream; shared conversations use block delivery. */
474
+ function createTeamsReplyStreamController(params) {
475
+ const isPersonal = (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeOptionalLowercaseString)(params.conversationType) === "personal";
476
+ const streamMode = (0, openclaw_plugin_sdk_channel_outbound.resolveChannelPreviewStreamMode)(params.msteamsConfig, "partial");
477
+ const stream = params.allowProviderPreview && isPersonal && (streamMode === "partial" || streamMode === "progress") ? params.context.stream : void 0;
478
+ let tokensEmitted = false;
479
+ let nativeDispatchStarted = false;
480
+ let nativeDeliveryClaimed = false;
481
+ let streamFinalizationPending = false;
482
+ let pendingFinalPayload;
483
+ let emittedText = "";
484
+ let acknowledgedText = "";
485
+ let acknowledgedLogicalText = "";
486
+ let acknowledgedStreamId;
487
+ let replacementFinalPending = false;
488
+ let replacementSettlementPending = false;
489
+ let replacementTextAwaitingAcknowledgement;
490
+ let deferredReplacementEntries = [];
491
+ let queuedFinalActivity;
492
+ const streamEvents = stream?.events;
493
+ let streamChunkSubscription;
494
+ if (typeof streamEvents?.on === "function" && typeof streamEvents.off === "function") streamChunkSubscription = streamEvents.on("chunk", (activity) => {
495
+ const replacementAcknowledgementPending = replacementTextAwaitingAcknowledgement !== void 0;
496
+ const replacementAcknowledgement = typeof activity.text === "string" && replacementAcknowledgementPending && activity.text === replacementTextAwaitingAcknowledgement?.text;
497
+ if (activity.type !== "typing" || activity.channelData?.streamType !== "streaming" || !activity.id || !activity.text || acknowledgedStreamId !== void 0 && activity.id !== acknowledgedStreamId || (replacementAcknowledgementPending ? !replacementAcknowledgement : !activity.text.startsWith(acknowledgedText)) || !replacementAcknowledgement && !emittedText.startsWith(activity.text)) return;
498
+ acknowledgedStreamId = activity.id;
499
+ acknowledgedText = activity.text;
500
+ acknowledgedLogicalText = replacementAcknowledgement ? replacementTextAwaitingAcknowledgement.logicalText : activity.text;
501
+ if (replacementAcknowledgement) replacementTextAwaitingAcknowledgement = void 0;
653
502
  });
654
- const baseUrl = `${params.serviceUrl.replace(/\/+$/, "")}/v3/attachments/${encodeURIComponent(params.attachmentId)}`;
655
- if (!(0, openclaw_plugin_sdk_ssrf_policy.isHttpsUrlAllowedByHostnameSuffixAllowlist)(baseUrl, policy.allowHosts)) return;
656
- let accessToken;
657
- try {
658
- accessToken = await require_resolve_allowlist.withMSTeamsRequestDeadline({
659
- deadline: params.deadline,
660
- label: "MS Teams Bot Framework token",
661
- work: () => tokenProvider.getAccessToken(BOT_FRAMEWORK_SCOPE)
662
- });
663
- } catch (err) {
664
- params.logger?.warn?.("msteams botFramework token acquisition failed", { error: (0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err) });
665
- return;
666
- }
667
- if (!accessToken) return;
668
- const request = {
669
- url: baseUrl,
670
- accessToken,
671
- policy,
672
- logger: params.logger,
673
- deadline: params.deadline
503
+ const wasCanceled = () => Boolean(stream?.canceled);
504
+ const releaseStreamChunkSubscription = () => {
505
+ if (streamChunkSubscription === void 0) return;
506
+ streamEvents?.off(streamChunkSubscription);
507
+ streamChunkSubscription = void 0;
674
508
  };
675
- const info = await fetchBotFrameworkAttachmentInfo(request);
676
- if (!info) return;
677
- const views = Array.isArray(info.views) ? info.views : [];
678
- const candidateView = views.find((view) => view?.viewId === "original") ?? views.find((view) => typeof view?.viewId === "string");
679
- const viewId = typeof candidateView?.viewId === "string" && candidateView.viewId ? candidateView.viewId : void 0;
680
- if (!viewId) return;
681
- if (typeof candidateView?.size === "number" && candidateView.size > 0 && candidateView.size > params.maxBytes) return;
682
- const fileNameHint = typeof info.name === "string" && info.name || void 0;
683
- const contentTypeHint = typeof info.type === "string" && info.type || void 0;
684
- const saved = await saveBotFrameworkAttachmentView({
685
- ...request,
686
- url: `${baseUrl}/views/${encodeURIComponent(viewId)}`,
687
- maxBytes: params.maxBytes,
688
- fileNameHint,
689
- contentTypeHint
690
- });
691
- if (!saved) return;
692
- return {
693
- path: saved.path,
694
- contentType: saved.contentType,
695
- kind: require_resolve_allowlist.resolveMSTeamsMediaKind({
696
- contentType: saved.contentType,
697
- fileName: fileNameHint
698
- })
509
+ const acknowledgedNativeDelivery = () => {
510
+ if (!acknowledgedStreamId || !acknowledgedText) return { visibleReplySent: false };
511
+ return {
512
+ visibleReplySent: true,
513
+ content: acknowledgedText,
514
+ messageId: acknowledgedStreamId
515
+ };
699
516
  };
700
- }
701
- /**
702
- * Download media for every attachment referenced by a Bot Framework personal
703
- * chat activity. Returns all successfully fetched media along with diagnostics
704
- * compatible with `downloadMSTeamsGraphMedia`'s result shape so callers can
705
- * reuse the existing logging path.
706
- */
707
- async function downloadMSTeamsBotFrameworkAttachments(params) {
708
- const unique = (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeUniqueTrimmedStringList)(params.attachmentIds);
709
- if (unique.length === 0 || !params.serviceUrl || !params.tokenProvider) return {
710
- media: [],
711
- attachmentCount: unique.length
517
+ const fallbackPayloadAfterAcknowledgedText = (payload) => {
518
+ if (!acknowledgedLogicalText || typeof payload.text !== "string" || !payload.text.startsWith(acknowledgedLogicalText)) return payload;
519
+ const remainingText = payload.text.slice(acknowledgedLogicalText.length);
520
+ if (!remainingText && !hasUnstreamedContent(payload)) return;
521
+ return {
522
+ ...payload,
523
+ text: remainingText || void 0
524
+ };
712
525
  };
713
- const media = [];
714
- for (const attachmentId of unique) try {
715
- const item = await downloadMSTeamsBotFrameworkAttachment({
716
- ...params,
717
- attachmentId
718
- });
719
- if (item) media.push({
720
- ...item,
721
- sourceId: attachmentId
722
- });
723
- else media.push({
724
- kind: "document",
725
- sourceId: attachmentId
726
- });
727
- } catch (err) {
728
- media.push({
729
- kind: "document",
730
- sourceId: attachmentId
731
- });
732
- params.logger?.warn?.("msteams botFramework attachment download failed", {
733
- error: (0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err),
734
- attachmentId
735
- });
736
- }
737
- return {
738
- media,
739
- attachmentCount: unique.length
526
+ const fallbackPayloadForSuppressedFinal = (payload) => {
527
+ return Boolean(payload.mediaUrl || payload.mediaUrls?.length) ? {
528
+ ...payload,
529
+ mediaUrl: void 0,
530
+ mediaUrls: void 0
531
+ } : payload;
740
532
  };
741
- }
742
- // extensions/msteams/src/attachments/html.ts
743
- /**
744
- * Extract every `<attachment id="...">` reference from the HTML attachments in
745
- * the inbound activity. Returns the complete (non-sliced) list; callers that
746
- * need a capped diagnostic summary can truncate after calling this helper.
747
- */
748
- function extractMSTeamsHtmlAttachmentIds(attachments) {
749
- const list = Array.isArray(attachments) ? attachments : [];
750
- if (list.length === 0) return [];
751
- const ids = /* @__PURE__ */ new Set();
752
- for (const att of list) {
753
- const html = require_resolve_allowlist.extractHtmlFromAttachment(att);
754
- if (!html) continue;
755
- for (const value of require_resolve_allowlist.htmlTagAttributeValues(html, "attachment", "id")) {
756
- const id = value.trim();
757
- if (id) ids.add(id);
758
- }
759
- }
760
- return Array.from(ids);
761
- }
762
- function summarizeMSTeamsHtmlAttachments(attachments) {
763
- const list = Array.isArray(attachments) ? attachments : [];
764
- if (list.length === 0) return;
765
- let htmlAttachments = 0;
766
- let imgTags = 0;
767
- let dataImages = 0;
768
- let cidImages = 0;
769
- const srcHosts = /* @__PURE__ */ new Set();
770
- let attachmentTags = 0;
771
- const attachmentIds = /* @__PURE__ */ new Set();
772
- for (const att of list) {
773
- const html = require_resolve_allowlist.extractHtmlFromAttachment(att);
774
- if (!html) continue;
775
- htmlAttachments += 1;
776
- for (const value of require_resolve_allowlist.htmlTagAttributeValues(html, "img", "src")) {
777
- imgTags += 1;
778
- const src = value.trim();
779
- if (src) {
780
- if (src.startsWith("data:")) dataImages += 1;
781
- else if (src.startsWith("cid:")) cidImages += 1;
782
- else srcHosts.add(require_resolve_allowlist.safeHostForUrl(src));
783
- }
784
- }
785
- for (const value of require_resolve_allowlist.htmlTagAttributeValues(html, "attachment", "id")) {
786
- attachmentTags += 1;
787
- const id = value.trim();
788
- if (id) attachmentIds.add(id);
789
- }
790
- }
791
- if (htmlAttachments === 0) return;
792
- return {
793
- htmlAttachments,
794
- imgTags,
795
- dataImages,
796
- cidImages,
797
- srcHosts: Array.from(srcHosts).slice(0, 5),
798
- attachmentTags,
799
- attachmentIds: Array.from(attachmentIds).slice(0, 5)
533
+ const finalStreamActivity = (text) => ({
534
+ ...require_conversation_store_state.buildMSTeamsMessageActivity(text === void 0 ? void 0 : require_conversation_store_state.formatMSTeamsMarkdown(text, params.tableMode ?? "code")),
535
+ channelData: params.feedbackLoopEnabled ? { feedbackLoopEnabled: true } : {}
536
+ });
537
+ const queueFinalReplacement = (activeStream, text) => {
538
+ const activity = finalStreamActivity(text);
539
+ replacementTextAwaitingAcknowledgement = {
540
+ text: activity.text,
541
+ logicalText: text
542
+ };
543
+ activeStream.emit(activity);
544
+ queuedFinalActivity = activity;
545
+ emittedText = text;
800
546
  };
801
- }
802
- function resolveUnrepresentedHtmlAttachmentIds(attachments) {
803
- const representedIds = /* @__PURE__ */ new Set();
804
- for (const attachment of attachments) {
805
- if ((require_resolve_allowlist.normalizeContentType(attachment.contentType) ?? "").startsWith("text/html")) continue;
806
- const id = attachment.id?.trim();
807
- if (id) representedIds.add(id);
808
- }
809
- return extractMSTeamsHtmlAttachmentIds(attachments).filter((id) => !representedIds.has(id));
810
- }
811
- function createAdvertisedMediaFact(kind, sourceId) {
812
- return {
813
- kind,
814
- ...sourceId ? { sourceId } : {}
547
+ const deferredReplacementLogicalContent = () => {
548
+ return deferredReplacementEntries.map((entry) => entry.payload.text).filter((text) => Boolean(text)).join("\n") || void 0;
815
549
  };
816
- }
817
- function resolveMSTeamsAdvertisedMedia(attachments) {
818
- const list = Array.isArray(attachments) ? attachments : [];
819
- if (list.length === 0) return [];
820
- const fileAttachments = list.filter(require_resolve_allowlist.isAdvertisedFileAttachment);
821
- const inlineMedia = require_resolve_allowlist.extractInlineImageReferences(list).map((candidate) => createAdvertisedMediaFact("image", candidate.sourceId));
822
- const htmlAttachmentIds = resolveUnrepresentedHtmlAttachmentIds(list);
823
- return [
824
- ...fileAttachments.map((attachment) => createAdvertisedMediaFact(require_resolve_allowlist.isLikelyImageAttachment(attachment) ? "image" : require_resolve_allowlist.resolveMSTeamsMediaKind({
825
- contentType: require_resolve_allowlist.normalizeContentType(attachment.contentType),
826
- fileName: attachment.name ?? void 0
827
- }), attachment.id?.trim() || void 0)),
828
- ...inlineMedia,
829
- ...htmlAttachmentIds.map((sourceId) => createAdvertisedMediaFact("document", sourceId))
830
- ];
831
- }
832
- // extensions/msteams/src/attachments/remote-media.ts
833
- /**
834
- * Direct save path used when the caller supplies the already-guarded fetch
835
- * implementation. This lets Teams-specific auth fallback own the request
836
- * sequence while keeping redirect and DNS pinning inside `safeFetchWithPolicy`.
837
- */
838
- async function downloadAndStoreMSTeamsRemoteMedia(params) {
839
- const response = await params.fetchImpl(params.url, { redirect: "follow" });
840
- try {
841
- const saved = await (0, openclaw_plugin_sdk_media_runtime.saveResponseMedia)(response, {
842
- sourceUrl: params.url,
843
- filePathHint: params.filePathHint,
844
- maxBytes: params.maxBytes,
845
- fallbackContentType: params.contentTypeHint,
846
- originalFilename: params.preserveFilenames ? params.filePathHint : void 0
847
- });
848
- return {
849
- path: saved.path,
850
- contentType: saved.contentType,
851
- kind: params.kind ?? require_resolve_allowlist.resolveMSTeamsMediaKind({
852
- contentType: saved.contentType,
853
- fileName: params.filePathHint
854
- })
855
- };
856
- } finally {
857
- await response.body?.cancel().catch(() => void 0);
858
- }
859
- }
860
- // extensions/msteams/src/attachments/download.ts
861
- function resolveDownloadCandidate(att, graphReferenceToken) {
862
- const contentType = require_resolve_allowlist.normalizeContentType(att.contentType);
863
- const graphReference = graphReferenceToken !== void 0 && contentType === "reference";
864
- const name = (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeOptionalString)(att.name) ?? "";
865
- if (contentType === "application/vnd.microsoft.teams.file.download.info") {
866
- if (!(0, openclaw_plugin_sdk_string_coerce_runtime.isRecord)(att.content)) return null;
867
- const downloadUrl = (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeOptionalString)(att.content.downloadUrl) ?? "";
868
- if (!downloadUrl) return null;
869
- const fileType = (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeOptionalString)(att.content.fileType) ?? "";
870
- const uniqueId = (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeOptionalString)(att.content.uniqueId) ?? "";
871
- const fileName = (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeOptionalString)(att.content.fileName) ?? "";
872
- const fileHint = name || fileName || (uniqueId && fileType ? `${uniqueId}.${fileType}` : "");
550
+ const deferredReplacementPayloads = (replacementFallback) => deferredReplacementEntries.flatMap((entry) => {
551
+ if (entry.kind === "payload") return [entry.payload];
552
+ const text = replacementFallback?.text;
553
+ if (!text && !hasUnstreamedContent(entry.payload)) return [];
554
+ return [{
555
+ ...entry.payload,
556
+ text: text || void 0
557
+ }];
558
+ });
559
+ const finalizeWithoutReceipt = (logicalContent, canceled = false) => {
560
+ const fallback = pendingFinalPayload && !canceled ? fallbackPayloadAfterAcknowledgedText(pendingFinalPayload) : void 0;
561
+ const postNativePayloads = replacementSettlementPending && !canceled ? deferredReplacementPayloads(fallback) : [];
873
562
  return {
874
- kind: "remote",
875
- mediaKind: require_resolve_allowlist.resolveMSTeamsMediaKind({
876
- contentType,
877
- fileName: fileHint,
878
- fileType
879
- }),
880
- url: downloadUrl,
881
- fileHint: fileHint || void 0,
882
- contentTypeHint: void 0,
883
- sourceId: att.id?.trim() || void 0
563
+ ...acknowledgedNativeDelivery(),
564
+ ...!canceled && logicalContent ? { logicalContent } : {},
565
+ ...!replacementSettlementPending && fallback ? { fallbackPayload: fallback } : {},
566
+ ...postNativePayloads.length > 0 ? { postNativePayloads } : {}
884
567
  };
885
- }
886
- const contentUrl = graphReference ? att.contentUrl ?? "" : (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeOptionalString)(att.contentUrl) ?? "";
887
- if (!contentUrl) return null;
888
- const sharesUrl = graphReference ? `${require_resolve_allowlist.GRAPH_ROOT}/shares/${require_resolve_allowlist.encodeGraphShareId(contentUrl)}/driveItem/content` : require_resolve_allowlist.tryBuildGraphSharesUrlForSharedLink(contentUrl);
889
- const resolvedUrl = sharesUrl ?? contentUrl;
890
- const resolvedContentTypeHint = graphReference ? "application/octet-stream" : sharesUrl ? void 0 : contentType;
891
- return {
892
- kind: "remote",
893
- mediaKind: require_resolve_allowlist.resolveMSTeamsMediaKind({
894
- contentType,
895
- fileName: name
896
- }),
897
- url: resolvedUrl,
898
- fileHint: graphReference ? att.name ?? void 0 : name || void 0,
899
- contentTypeHint: resolvedContentTypeHint,
900
- sourceId: att.id?.trim() || void 0,
901
- graphReference
902
568
  };
903
- }
904
- function scopeCandidatesForUrl(url) {
905
- const host = (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeLowercaseStringOrEmpty)(URL.parse(url)?.hostname);
906
- return host.endsWith("graph.microsoft.com") || host.endsWith("sharepoint.com") || host.endsWith("1drv.ms") || host.includes("sharepoint") ? ["https://graph.microsoft.com", "https://api.botframework.com"] : ["https://api.botframework.com", "https://graph.microsoft.com"];
907
- }
908
- function canonicalizeInlineBase64Payload(value) {
909
- let cleaned = "";
910
- for (const char of value) if (char.charCodeAt(0) > 32) cleaned += char;
911
- return cleaned.length % 4 === 0 && /^[A-Za-z0-9+/]+={0,2}$/.test(cleaned) ? cleaned : void 0;
912
- }
913
- function decodeInlineDataImage(src, maxBytes, totalBytes) {
914
- const match = /^data:(image\/[a-z0-9.+-]+)?(;base64)?,(.*)$/i.exec(src);
915
- if (!match) return null;
916
- const contentType = (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeLowercaseStringOrEmpty)(match[1] ?? "");
917
- if (!Boolean(match[2])) return null;
918
- const canonicalPayload = canonicalizeInlineBase64Payload(match[3] ?? "");
919
- if (!canonicalPayload) return null;
920
- const estimatedBytes = Buffer.byteLength(canonicalPayload, "base64");
921
- if (estimatedBytes <= 0 || typeof maxBytes === "number" && (estimatedBytes > maxBytes || totalBytes + estimatedBytes > maxBytes)) return null;
922
- try {
923
- return {
924
- data: Buffer.from(canonicalPayload, "base64"),
925
- contentType,
926
- estimatedBytes
927
- };
928
- } catch {
929
- return null;
930
- }
931
- }
932
- async function resolveInlineDataImageMime(inline) {
933
- const detectedMime = await require_runtime.getMSTeamsRuntime().media.detectMime({
934
- buffer: inline.data,
935
- headerMime: inline.contentType
936
- });
937
- const mime = (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeOptionalLowercaseString)(detectedMime ?? inline.contentType);
938
- return mime?.startsWith("image/") ? mime : void 0;
939
- }
940
- async function fetchWithAuthFallback(params) {
941
- const fetchAttempt = (fetchFn, requestInit) => require_resolve_allowlist.safeFetchWithPolicy({
942
- url: params.url,
943
- policy: params.policy,
944
- fetchFn,
945
- requestInit,
946
- resolveFn: params.resolveFn,
947
- timeoutMs: require_resolve_allowlist.resolveMSTeamsRequestTimeoutMs(params.deadline)
948
- });
949
- if (params.bearerToken !== void 0) {
950
- const headers = require_resolve_allowlist.ensureUserAgentHeader(params.requestInit?.headers);
951
- require_resolve_allowlist.applyAuthorizationHeaderForUrl({
952
- headers,
953
- url: params.url,
954
- authAllowHosts: params.policy.authAllowHosts,
955
- bearerToken: params.bearerToken
956
- });
957
- return fetchAttempt(params.fetchFn, {
958
- ...params.requestInit,
959
- headers
960
- });
961
- }
962
- const firstAttempt = await fetchAttempt(params.fetchFn, params.requestInit);
963
- if (firstAttempt.ok) return firstAttempt;
964
- if (!params.tokenProvider) return firstAttempt;
965
- const tokenProvider = params.tokenProvider;
966
- if (firstAttempt.status !== 401 && firstAttempt.status !== 403) return firstAttempt;
967
- if (!(0, openclaw_plugin_sdk_ssrf_policy.isHttpsUrlAllowedByHostnameSuffixAllowlist)(params.url, params.policy.authAllowHosts)) return firstAttempt;
968
- let fallbackAttempt = firstAttempt;
969
- const scopes = scopeCandidatesForUrl(params.url);
970
- const fetchFn = params.fetchFn ?? fetch;
971
- for (const scope of scopes) try {
972
- const token = await require_resolve_allowlist.withMSTeamsRequestDeadline({
973
- deadline: params.deadline,
974
- label: "MS Teams attachment token",
975
- work: () => tokenProvider.getAccessToken(scope)
976
- });
977
- const authHeaders = new Headers(params.requestInit?.headers);
978
- authHeaders.set("Authorization", `Bearer ${token}`);
979
- const authAttempt = await fetchAttempt(fetchFn, {
980
- ...params.requestInit,
981
- headers: authHeaders
982
- });
983
- await fallbackAttempt.body?.cancel().catch(() => void 0);
984
- if (authAttempt.ok || require_resolve_allowlist.isRedirectStatus(authAttempt.status)) return authAttempt;
985
- fallbackAttempt = authAttempt;
986
- } catch {}
987
- return fallbackAttempt;
988
- }
989
- async function downloadMSTeamsAttachments(params) {
990
- const list = Array.isArray(params.attachments) ? params.attachments : [];
991
- if (list.length === 0) return [];
992
- const policy = require_resolve_allowlist.resolveAttachmentFetchPolicy({
993
- allowHosts: params.allowHosts,
994
- authAllowHosts: params.authAllowHosts
569
+ const progressDraft = (0, openclaw_plugin_sdk_channel_outbound.createChannelProgressDraftCompositor)({
570
+ preparedItems: true,
571
+ formatPlainText: (text) => text,
572
+ entry: params.msteamsConfig,
573
+ mode: streamMode,
574
+ active: Boolean(stream) && streamMode === "progress",
575
+ seed: params.progressSeed ?? "msteams",
576
+ update: (text) => {
577
+ if (!stream || wasCanceled() || streamFinalizationPending) return false;
578
+ stream.update(flattenInformativeStatus(text));
579
+ return true;
580
+ }
995
581
  });
996
- const allowHosts = policy.allowHosts;
997
- const candidates = list.filter(require_resolve_allowlist.isAdvertisedFileAttachment).map((attachment) => {
998
- return (require_resolve_allowlist.isDownloadableAttachment(attachment) ? resolveDownloadCandidate(attachment, params.graphReferenceToken) : null) ?? {
999
- kind: "unavailable",
1000
- mediaKind: require_resolve_allowlist.resolveMSTeamsMediaKind({
1001
- contentType: require_resolve_allowlist.normalizeContentType(attachment.contentType),
1002
- fileName: attachment.name ?? void 0
1003
- }),
1004
- sourceId: attachment.id?.trim() || void 0
1005
- };
1006
- });
1007
- const maxInlineBytes = params.maxBytes;
1008
- candidates.push(...require_resolve_allowlist.extractInlineImageReferences(list).map((candidate) => {
1009
- if (candidate.kind === "url") return {
1010
- kind: "remote",
1011
- mediaKind: "image",
1012
- url: candidate.url,
1013
- fileHint: candidate.fileHint,
1014
- contentTypeHint: candidate.contentType,
1015
- sourceId: candidate.sourceId
1016
- };
1017
- return Object.assign(candidate, { mediaKind: "image" });
1018
- }));
1019
- for (const sourceId of resolveUnrepresentedHtmlAttachmentIds(list)) candidates.push({
1020
- kind: "unavailable",
1021
- mediaKind: "document",
1022
- sourceId
1023
- });
1024
- if (candidates.length === 0) return [];
1025
- let totalInlineBytes = 0;
1026
- const downloadCandidate = async (candidate) => {
1027
- if (candidate.kind === "unavailable") return;
1028
- if (candidate.kind === "data") {
1029
- const decoded = decodeInlineDataImage(candidate.src, maxInlineBytes, totalInlineBytes);
1030
- if (!decoded) return;
1031
- totalInlineBytes += decoded.estimatedBytes;
582
+ return {
583
+ onPartialReply(payload) {
584
+ if (!stream || !payload.text || wasCanceled() || streamMode !== "partial") return;
585
+ if (replacementSettlementPending && replacementFinalPending) {
586
+ pendingFinalPayload = { text: payload.text };
587
+ return;
588
+ }
589
+ if (streamFinalizationPending || nativeDeliveryClaimed) return;
590
+ const fullText = payload.text;
591
+ let prefixLength = 0;
592
+ while (prefixLength < emittedText.length && prefixLength < fullText.length && emittedText[prefixLength] === fullText[prefixLength]) prefixLength += 1;
593
+ const previousRemainder = emittedText.slice(prefixLength);
594
+ const delta = fullText.slice(prefixLength);
595
+ if (!delta) return;
596
+ if (previousRemainder.trim()) {
597
+ stream.clearText();
598
+ replacementFinalPending = true;
599
+ replacementSettlementPending = true;
600
+ pendingFinalPayload = { text: fullText };
601
+ streamFinalizationPending = true;
602
+ return;
603
+ }
604
+ stream.emit(delta);
605
+ emittedText = fullText;
606
+ tokensEmitted = true;
607
+ nativeDispatchStarted = true;
608
+ },
609
+ pushItemEvent: progressDraft.pushItemEvent.bind(progressDraft),
610
+ pushToolEvent: progressDraft.pushToolEvent,
611
+ pushReasoningProgress: progressDraft.pushReasoningProgress.bind(progressDraft),
612
+ resetReasoningProgress: progressDraft.resetReasoningProgress,
613
+ pushApprovalEvent: progressDraft.pushApprovalEvent.bind(progressDraft),
614
+ pushPlanProgress: progressDraft.pushPlanProgress.bind(progressDraft),
615
+ preparePayload(payload) {
616
+ if (!stream) return payload;
617
+ if (wasCanceled()) return;
618
+ if (payload.text) progressDraft.markFinalReplyStarted();
619
+ if (replacementSettlementPending) {
620
+ if (!replacementFinalPending || !payload.text) {
621
+ deferredReplacementEntries.push({
622
+ kind: "payload",
623
+ payload
624
+ });
625
+ return;
626
+ }
627
+ replacementFinalPending = false;
628
+ deferredReplacementEntries.push({
629
+ kind: "replacement",
630
+ payload
631
+ });
632
+ pendingFinalPayload = fallbackPayloadForSuppressedFinal(payload);
633
+ queueFinalReplacement(stream, payload.text);
634
+ tokensEmitted = false;
635
+ return;
636
+ }
637
+ if (streamMode === "progress" && payload.text && !nativeDispatchStarted) {
638
+ stream.emit(payload.text);
639
+ emittedText = payload.text;
640
+ nativeDispatchStarted = true;
641
+ tokensEmitted = true;
642
+ }
643
+ if (tokensEmitted) {
644
+ const hasMedia = Boolean(payload.mediaUrl || payload.mediaUrls?.length);
645
+ pendingFinalPayload = fallbackPayloadForSuppressedFinal(payload);
646
+ streamFinalizationPending = true;
647
+ tokensEmitted = false;
648
+ if (!hasMedia) return;
649
+ const { presentation: _presentation, presentationTextMode: _mode, ...mediaPayload } = payload;
650
+ return {
651
+ ...mediaPayload,
652
+ text: void 0
653
+ };
654
+ }
655
+ return payload;
656
+ },
657
+ claimNativeDelivery() {
658
+ if (!nativeDispatchStarted || nativeDeliveryClaimed) return false;
659
+ nativeDeliveryClaimed = true;
660
+ return true;
661
+ },
662
+ async finalize() {
663
+ progressDraft.markFinalReplyStarted();
664
+ if (!stream || !nativeDispatchStarted) {
665
+ releaseStreamChunkSubscription();
666
+ return { visibleReplySent: false };
667
+ }
668
+ let logicalContent;
1032
669
  try {
1033
- const contentType = await resolveInlineDataImageMime(decoded);
1034
- if (!contentType) return;
1035
- const saved = await require_runtime.getMSTeamsRuntime().channel.media.saveMediaBuffer(decoded.data, contentType, "inbound", params.maxBytes);
670
+ if (wasCanceled() || !streamFinalizationPending) return acknowledgedNativeDelivery();
671
+ if (replacementSettlementPending && replacementFinalPending && pendingFinalPayload?.text) {
672
+ replacementFinalPending = false;
673
+ deferredReplacementEntries.push({
674
+ kind: "replacement",
675
+ payload: pendingFinalPayload
676
+ });
677
+ logicalContent = deferredReplacementLogicalContent();
678
+ queueFinalReplacement(stream, pendingFinalPayload.text);
679
+ }
680
+ logicalContent ??= replacementSettlementPending ? deferredReplacementLogicalContent() : void 0;
681
+ const logicalText = pendingFinalPayload?.text ?? (emittedText || void 0);
682
+ const finalActivity = queuedFinalActivity ?? finalStreamActivity(logicalText);
683
+ const content = finalActivity.text;
684
+ logicalContent ??= content !== logicalText ? logicalText : void 0;
685
+ if (!queuedFinalActivity) {
686
+ if (content !== void 0 && content !== emittedText) {
687
+ stream.clearText();
688
+ replacementTextAwaitingAcknowledgement = {
689
+ text: content,
690
+ logicalText
691
+ };
692
+ stream.emit(finalActivity);
693
+ } else stream.emit({
694
+ ...finalActivity,
695
+ text: void 0
696
+ });
697
+ }
698
+ const result = await stream.close();
699
+ if (!result) return finalizeWithoutReceipt(logicalContent, wasCanceled());
700
+ const messageId = require_conversation_store_state.extractMessageId(result) ?? acknowledgedStreamId;
701
+ const postNativePayloads = replacementSettlementPending ? deferredReplacementPayloads(void 0) : pendingFinalPayload?.presentation ? [{
702
+ ...pendingFinalPayload,
703
+ text: void 0
704
+ }] : [];
1036
705
  return {
1037
- path: saved.path,
1038
- contentType: saved.contentType,
1039
- kind: "image"
706
+ visibleReplySent: true,
707
+ ...content === void 0 ? {} : { content },
708
+ ...logicalContent ? { logicalContent } : {},
709
+ ...messageId ? { messageId } : {},
710
+ ...postNativePayloads.length > 0 ? { postNativePayloads } : {}
1040
711
  };
1041
712
  } catch (err) {
1042
- params.logger?.warn?.("msteams inline attachment decode failed", { error: (0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err) });
1043
- }
1044
- return;
1045
- }
1046
- if (!(0, openclaw_plugin_sdk_ssrf_policy.isHttpsUrlAllowedByHostnameSuffixAllowlist)(candidate.url, allowHosts)) return;
1047
- try {
1048
- return await downloadAndStoreMSTeamsRemoteMedia({
1049
- url: candidate.url,
1050
- filePathHint: candidate.fileHint ?? candidate.url,
1051
- maxBytes: params.maxBytes,
1052
- contentTypeHint: candidate.contentTypeHint,
1053
- kind: candidate.graphReference ? void 0 : candidate.mediaKind,
1054
- preserveFilenames: params.preserveFilenames,
1055
- fetchImpl: (input, init) => fetchWithAuthFallback({
1056
- url: (0, openclaw_plugin_sdk_request_url.resolveRequestUrl)(input),
1057
- tokenProvider: params.tokenProvider,
1058
- fetchFn: params.fetchFn,
1059
- requestInit: init,
1060
- resolveFn: params.resolveFn,
1061
- policy,
1062
- deadline: params.deadline,
1063
- bearerToken: candidate.graphReference ? params.graphReferenceToken : void 0
1064
- })
1065
- });
1066
- } catch (err) {
1067
- if (candidate.graphReference) {
1068
- params.logger?.warn?.("msteams SharePoint reference download failed", {
1069
- error: (0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err),
1070
- name: candidate.fileHint
1071
- });
1072
- return;
713
+ if (err instanceof Error && err.name === "StreamCancelledError") return acknowledgedNativeDelivery();
714
+ params.log?.warn?.(`msteams stream finalize failed: ${(0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err)}`);
715
+ return finalizeWithoutReceipt(logicalContent);
716
+ } finally {
717
+ pendingFinalPayload = void 0;
718
+ streamFinalizationPending = false;
719
+ queuedFinalActivity = void 0;
720
+ replacementFinalPending = false;
721
+ replacementSettlementPending = false;
722
+ replacementTextAwaitingAcknowledgement = void 0;
723
+ deferredReplacementEntries = [];
724
+ releaseStreamChunkSubscription();
1073
725
  }
1074
- const msg = (0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err);
1075
- params.logger?.warn?.(`msteams attachment download failed host=${safeHostForLog(candidate.url)} error=${msg}`);
1076
- }
1077
- };
1078
- const out = [];
1079
- for (const candidate of candidates) {
1080
- const media = await downloadCandidate(candidate) ?? { kind: candidate.mediaKind };
1081
- out.push(candidate.sourceId ? {
1082
- ...media,
1083
- sourceId: candidate.sourceId
1084
- } : media);
1085
- }
1086
- return out;
1087
- }
1088
- function safeHostForLog(url) {
1089
- return URL.parse(url)?.host ?? "invalid-url";
1090
- }
1091
- // extensions/msteams/src/attachments/graph.ts
1092
- function createGraphHostedContentFact(item) {
1093
- const contentType = require_resolve_allowlist.normalizeContentType(item.contentType);
1094
- return {
1095
- kind: require_resolve_allowlist.resolveMSTeamsMediaKind({ contentType }),
1096
- ...contentType ? { contentType } : {},
1097
- ...item.id ? { sourceId: item.id } : {}
726
+ },
727
+ hasStream() {
728
+ return Boolean(stream);
729
+ },
730
+ isStreamActive() {
731
+ return Boolean(stream) && tokensEmitted && !wasCanceled();
732
+ },
733
+ wasCanceled
1098
734
  };
1099
735
  }
1100
- function buildMSTeamsGraphMessageUrl(params) {
1101
- const conversationType = (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeLowercaseStringOrEmpty)(params.conversationType ?? "");
1102
- const messageId = (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeOptionalString)(params.messageId);
1103
- if (!messageId) return;
1104
- if (conversationType === "channel") {
1105
- const teamAadGroupId = (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeOptionalString)(params.teamAadGroupId);
1106
- const channelId = (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeOptionalString)(params.channelId);
1107
- if (!teamAadGroupId || !channelId) return;
1108
- const messageRoot = `${require_resolve_allowlist.GRAPH_ROOT}/teams/${encodeURIComponent(teamAadGroupId)}/channels/${encodeURIComponent(channelId)}/messages`;
1109
- const threadRootMessageId = (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeOptionalString)(params.threadRootMessageId);
1110
- return threadRootMessageId && threadRootMessageId !== messageId ? `${messageRoot}/${encodeURIComponent(threadRootMessageId)}/replies/${encodeURIComponent(messageId)}` : `${messageRoot}/${encodeURIComponent(messageId)}`;
1111
- }
1112
- const chatId = (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeOptionalString)(params.conversationId);
1113
- if (!chatId) return;
1114
- return `${require_resolve_allowlist.GRAPH_ROOT}/chats/${encodeURIComponent(chatId)}/messages/${encodeURIComponent(messageId)}`;
1115
- }
1116
- async function withGraphMediaResponse(params, url, kind, read) {
1117
- const { response, release } = await (0, openclaw_plugin_sdk_ssrf_runtime.fetchWithSsrFGuard)({
1118
- url,
1119
- fetchImpl: params.fetchFn ?? fetch,
1120
- init: { headers: require_resolve_allowlist.ensureUserAgentHeader({ Authorization: `Bearer ${params.accessToken}` }) },
1121
- policy: params.ssrfPolicy,
1122
- auditContext: `msteams.graph.${kind}`,
1123
- timeoutMs: require_resolve_allowlist.resolveMSTeamsRequestTimeoutMs(params.deadline)
736
+ // extensions/msteams/src/reply-dispatcher.ts
737
+ function createMSTeamsReplyDispatcher(params) {
738
+ const core = require_runtime.getMSTeamsRuntime();
739
+ const msteamsCfg = require_resolve_allowlist.resolveMSTeamsAccountConfig(params.cfg, params.accountId);
740
+ const mediaMaxBytes = (0, openclaw_plugin_sdk_account_helpers.resolveChannelMediaMaxBytes)({
741
+ cfg: params.cfg,
742
+ resolveChannelLimitMb: () => msteamsCfg.mediaMaxMb
1124
743
  });
1125
- try {
1126
- return await read(response);
1127
- } finally {
1128
- if (!response.bodyUsed) response.body?.cancel().catch(() => void 0);
1129
- await release();
1130
- }
1131
- }
1132
- async function fetchGraphCollection(params) {
1133
- return withGraphMediaResponse(params, params.url, "collection", async (response) => {
1134
- const status = response.status;
1135
- if (!response.ok) return {
1136
- status,
1137
- items: []
1138
- };
1139
- try {
1140
- return {
1141
- status,
1142
- items: await (0, openclaw_plugin_sdk_provider_http.readProviderJsonArrayFieldResponse)(response, "MS Teams Graph collection", "value")
1143
- };
1144
- } catch {
1145
- return {
1146
- status,
1147
- items: []
1148
- };
744
+ const feedbackLoopEnabled = msteamsCfg.feedbackEnabled !== false;
745
+ const conversationType = (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeOptionalLowercaseString)(params.conversationRef.conversation?.conversationType);
746
+ const isTypingSupported = conversationType === "personal" || conversationType === "groupchat";
747
+ const TYPING_KEEPALIVE_INTERVAL_MS = 8e3;
748
+ const TYPING_KEEPALIVE_MAX_DURATION_MS = 6e5;
749
+ const sendTypingIndicator = async () => {
750
+ if (!isTypingSupported || streamController.isStreamActive() || streamController.wasCanceled()) return;
751
+ await params.context.sendActivity({ type: "typing" });
752
+ };
753
+ const { onModelSelected, typingCallbacks, ...replyPipeline } = (0, openclaw_plugin_sdk_channel_outbound.createChannelMessageReplyPipeline)({
754
+ cfg: params.cfg,
755
+ agentId: params.agentId,
756
+ channel: "msteams",
757
+ accountId: params.accountId,
758
+ typing: {
759
+ start: sendTypingIndicator,
760
+ keepaliveIntervalMs: TYPING_KEEPALIVE_INTERVAL_MS,
761
+ maxDurationMs: TYPING_KEEPALIVE_MAX_DURATION_MS,
762
+ onStartError: (err) => {
763
+ (0, openclaw_plugin_sdk_channel_outbound.logTypingFailure)({
764
+ log: (message) => params.log.debug?.(message),
765
+ channel: "msteams",
766
+ action: "start",
767
+ error: err
768
+ });
769
+ }
1149
770
  }
1150
771
  });
1151
- }
1152
- function normalizeGraphAttachment(att) {
1153
- let content = att.content;
1154
- if (typeof content === "string") try {
1155
- content = JSON.parse(content);
1156
- } catch {}
1157
- return {
1158
- id: att.id ?? void 0,
1159
- contentType: require_resolve_allowlist.normalizeContentType(att.contentType) ?? void 0,
1160
- contentUrl: att.contentUrl ?? void 0,
1161
- name: att.name ?? void 0,
1162
- thumbnailUrl: att.thumbnailUrl ?? void 0,
1163
- content
772
+ const chunkMode = core.channel.text.resolveChunkMode(params.cfg, "msteams", params.accountId);
773
+ const tableMode = core.channel.text.resolveMarkdownTableMode({
774
+ cfg: params.cfg,
775
+ channel: "msteams",
776
+ accountId: params.accountId
777
+ });
778
+ const hookRunner = (0, openclaw_plugin_sdk_plugin_runtime.getGlobalHookRunner)();
779
+ const streamController = createTeamsReplyStreamController({
780
+ tableMode,
781
+ allowProviderPreview: !((hookRunner?.hasHooks("reply_payload_sending") ?? false) || (hookRunner?.hasHooks("message_sending") ?? false)),
782
+ conversationType,
783
+ context: params.context,
784
+ feedbackLoopEnabled,
785
+ log: params.log,
786
+ msteamsConfig: msteamsCfg,
787
+ progressSeed: `${params.accountId ?? "default"}:${params.conversationRef.conversation?.id ?? ""}`
788
+ });
789
+ const teamsStreamMode = (0, openclaw_plugin_sdk_channel_outbound.resolveChannelPreviewStreamMode)(msteamsCfg, "partial");
790
+ const blockStreamingResolved = teamsStreamMode === "block" ? true : (0, openclaw_plugin_sdk_channel_outbound.resolveChannelStreamingBlockEnabled)(msteamsCfg);
791
+ const blockStreamingEnabled = blockStreamingResolved ?? false;
792
+ const typingIndicatorEnabled = typeof msteamsCfg?.typingIndicator === "boolean" ? msteamsCfg.typingIndicator : true;
793
+ const pendingDeliveries = [];
794
+ let pendingSettlement;
795
+ const findPendingNativeDelivery = () => pendingDeliveries.find((candidate) => !candidate.nativeSettled);
796
+ const queueDeliveryFailureSystemEvent = (failure) => {
797
+ const classification = require_channel_setup.classifyMSTeamsSendError(failure.error);
798
+ const errorText = require_channel_setup.formatUnknownError(failure.error);
799
+ const failedAll = failure.failed >= failure.total;
800
+ const ambiguous = classification.kind === "ambiguous";
801
+ const sentences = [
802
+ `Microsoft Teams delivery failed: ${ambiguous ? failedAll ? "the delivery outcome is unknown for the previous reply" : `the delivery outcome is unknown for ${failure.failed} of ${failure.total} message blocks` : failedAll ? "the previous reply was not delivered" : `${failure.failed} of ${failure.total} message blocks were not delivered`}.`,
803
+ ambiguous ? void 0 : `The user may not have received ${failedAll ? "that reply" : "the full reply"}.`,
804
+ `Error: ${errorText}.`,
805
+ classification.statusCode != null ? `Status: ${classification.statusCode}.` : void 0,
806
+ require_channel_setup.formatMSTeamsDeliveryFailureGuidance(classification)
807
+ ].filter(Boolean);
808
+ core.system.enqueueSystemEvent(sentences.join(" "), {
809
+ sessionKey: params.sessionKey,
810
+ contextKey: `msteams:delivery-failure:${params.conversationRef.conversation?.id ?? "unknown"}`
811
+ });
1164
812
  };
1165
- }
1166
- async function downloadGraphHostedContent(params) {
1167
- let hosted;
1168
- try {
1169
- hosted = await fetchGraphCollection({
1170
- ...params,
1171
- url: `${params.messageUrl}/hostedContents`
813
+ const renderReplyPayload = (payload) => {
814
+ return require_conversation_store_state.renderReplyPayloadsToMessages([payload], {
815
+ textChunkLimit: params.textLimit,
816
+ tableMode,
817
+ chunkMode
1172
818
  });
1173
- } catch (err) {
1174
- params.logger?.warn?.("msteams graph hostedContents fetch failed", { error: (0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err) });
1175
- return {
1176
- media: [],
1177
- count: 0
1178
- };
1179
- }
1180
- if (hosted.items.length === 0) return {
1181
- media: [],
1182
- status: hosted.status,
1183
- count: 0
1184
819
  };
1185
- const out = [];
1186
- for (const item of hosted.items) {
1187
- if (!item.id) {
1188
- out.push(createGraphHostedContentFact(item));
1189
- continue;
820
+ const settlePendingDelivery = (delivery) => {
821
+ if (delivery.settled || !delivery.blockSettled || !delivery.nativeSettled) return;
822
+ delivery.settled = true;
823
+ const acceptedParts = [...delivery.nativeResult ? [delivery.nativeResult] : [], ...delivery.blockResults];
824
+ const messageIds = acceptedParts.flatMap((part) => part.messageIds);
825
+ const content = delivery.errors.length > 0 ? acceptedParts.map((part) => part.content).filter(Boolean).join("\n") : delivery.content;
826
+ const outcome = {
827
+ visibleReplySent: acceptedParts.length > 0,
828
+ ...messageIds.length > 0 ? { messageIds } : {},
829
+ ...acceptedParts.length > 0 && content !== void 0 ? { content } : {}
830
+ };
831
+ if (delivery.errors.length === 0) {
832
+ delivery.finalization.resolve(outcome);
833
+ return;
1190
834
  }
1191
- try {
1192
- const sourceId = item.id;
1193
- const valueUrl = `${params.messageUrl}/hostedContents/${encodeURIComponent(sourceId)}/$value`;
1194
- await withGraphMediaResponse(params, valueUrl, "hostedContent.value", async (valRes) => {
1195
- if (!valRes.ok) {
1196
- out.push(createGraphHostedContentFact(item));
1197
- return;
1198
- }
1199
- const saved = await require_runtime.getMSTeamsRuntime().channel.media.saveResponseMedia(valRes, {
1200
- sourceUrl: valueUrl,
1201
- maxBytes: params.maxBytes,
1202
- fallbackContentType: item.contentType ?? void 0,
1203
- subdir: "inbound"
835
+ const error = delivery.errors.find((candidate) => !(candidate instanceof openclaw_plugin_sdk_error_runtime.PlatformMessageNotDispatchedError)) ?? delivery.errors[0];
836
+ delivery.finalization.reject(outcome.visibleReplySent ? (0, openclaw_plugin_sdk_channel_inbound.createChannelPartialDeliveryError)(error, {
837
+ ...outcome,
838
+ visibleReplySent: true
839
+ }) : error);
840
+ };
841
+ const flushPendingMessages = async () => {
842
+ for (const delivery of pendingDeliveries) {
843
+ if (delivery.blockSettled) continue;
844
+ const toSend = delivery.messages.splice(0);
845
+ const total = toSend.length;
846
+ let failed = 0;
847
+ let lastFailedError;
848
+ const sentIds = [];
849
+ for (const msg of toSend) try {
850
+ const msgIds = await require_conversation_store_state.sendMSTeamsMessages({
851
+ accountId: params.accountId,
852
+ replyStyle: params.replyStyle,
853
+ app: params.app,
854
+ conversationRef: params.conversationRef,
855
+ context: params.context,
856
+ messages: [msg],
857
+ onRetry: (event) => {
858
+ params.log.debug?.("retrying send", {
859
+ replyStyle: params.replyStyle,
860
+ ...event
861
+ });
862
+ },
863
+ tokenProvider: params.tokenProvider,
864
+ sharePointSiteId: params.sharePointSiteId,
865
+ mediaMaxBytes,
866
+ feedbackLoopEnabled,
867
+ serviceUrlBoundary: require_resolve_allowlist.resolveMSTeamsSdkCloudOptions(msteamsCfg)
1204
868
  });
1205
- out.push({
1206
- path: saved.path,
1207
- contentType: saved.contentType,
1208
- kind: require_resolve_allowlist.resolveMSTeamsMediaKind({ contentType: saved.contentType }),
1209
- sourceId
869
+ const validIds = msgIds.filter((id) => id.trim() && id !== "unknown");
870
+ if (msgIds.length > 0) delivery.blockResults.push({
871
+ messageIds: validIds,
872
+ ...msg.text ? { content: msg.text } : {}
1210
873
  });
1211
- });
1212
- } catch (err) {
1213
- out.push(createGraphHostedContentFact(item));
1214
- params.logger?.warn?.("msteams graph hostedContent value fetch failed", { error: (0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err) });
1215
- continue;
874
+ sentIds.push(...validIds);
875
+ } catch (msgError) {
876
+ failed += 1;
877
+ lastFailedError = msgError;
878
+ delivery.errors.push(msgError);
879
+ params.log.debug?.("individual message send failed, continuing with remaining blocks");
880
+ }
881
+ if (failed > 0) {
882
+ const classification = require_channel_setup.classifyMSTeamsSendError(lastFailedError);
883
+ const hint = require_channel_setup.formatMSTeamsSendErrorHint(classification);
884
+ params.log.warn?.(`failed to deliver ${failed} of ${total} message blocks`, {
885
+ failed,
886
+ total,
887
+ error: require_channel_setup.formatUnknownError(lastFailedError),
888
+ classification,
889
+ ...hint ? { hint } : {}
890
+ });
891
+ queueDeliveryFailureSystemEvent({
892
+ failed,
893
+ total,
894
+ error: lastFailedError
895
+ });
896
+ }
897
+ delivery.blockSettled = true;
898
+ settlePendingDelivery(delivery);
899
+ if (sentIds.length > 0) try {
900
+ params.onSentMessageIds?.(sentIds);
901
+ } catch (error) {
902
+ params.log.warn?.("failed to record sent Teams message ids", { error: require_channel_setup.formatUnknownError(error) });
903
+ }
1216
904
  }
1217
- }
1218
- return {
1219
- media: out,
1220
- status: hosted.status,
1221
- count: hosted.items.length
1222
905
  };
1223
- }
1224
- async function downloadMSTeamsGraphMedia(params) {
1225
- if (!params.messageUrl || !params.tokenProvider) return { media: [] };
1226
- const tokenProvider = params.tokenProvider;
1227
- const policy = require_resolve_allowlist.resolveAttachmentFetchPolicy({
1228
- allowHosts: params.allowHosts,
1229
- authAllowHosts: params.authAllowHosts
1230
- });
1231
- const ssrfPolicy = (0, openclaw_plugin_sdk_ssrf_policy.buildHostnameAllowlistPolicyFromSuffixAllowlist)(policy.allowHosts);
1232
- const messageUrl = params.messageUrl;
1233
- const logGraphFailure = (phase, err) => {
1234
- params.logger?.debug?.(`graph media ${phase} failed`, {
1235
- messageUrl,
1236
- error: (0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err)
1237
- });
1238
- params.logger?.warn?.(`msteams graph ${phase} failed`, {
1239
- error: (0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err),
1240
- ...phase === "message parse" ? { messageUrl } : {}
1241
- });
906
+ const dispatcherOptions = {
907
+ ...replyPipeline,
908
+ humanDelay: (0, openclaw_plugin_sdk_agent_runtime.resolveHumanDelayConfig)(params.cfg, params.agentId),
909
+ onReplyStart: async () => {
910
+ if (typingIndicatorEnabled) await typingCallbacks?.onReplyStart?.();
911
+ },
912
+ typingCallbacks
1242
913
  };
1243
- let accessToken;
1244
- try {
1245
- accessToken = await require_resolve_allowlist.withMSTeamsRequestDeadline({
1246
- deadline: params.deadline,
1247
- label: "MS Teams Graph media token",
1248
- work: () => tokenProvider.getAccessToken("https://graph.microsoft.com")
1249
- });
1250
- } catch (err) {
1251
- logGraphFailure("token acquisition", err);
1252
- return {
1253
- media: [],
1254
- messageUrl,
1255
- tokenError: true
1256
- };
1257
- }
1258
- let messageAttachments = [];
1259
- let referenceAttachments = [];
1260
- let messageStatus;
1261
- try {
1262
- await withGraphMediaResponse({
1263
- ...params,
1264
- accessToken,
1265
- ssrfPolicy
1266
- }, messageUrl, "message", async (msgRes) => {
1267
- messageStatus = msgRes.status;
1268
- if (msgRes.ok) {
1269
- let msgData;
1270
- try {
1271
- msgData = await (0, openclaw_plugin_sdk_provider_http.readProviderJsonResponse)(msgRes, "MS Teams Graph message");
1272
- } catch (err) {
1273
- logGraphFailure("message parse", err);
1274
- msgData = {};
1275
- }
1276
- messageAttachments = Array.isArray(msgData.attachments) ? msgData.attachments : [];
1277
- referenceAttachments = messageAttachments.filter((a) => a.contentType === "reference" && a.contentUrl && a.name);
1278
- } else params.logger?.debug?.("graph media message fetch not ok", {
1279
- messageUrl,
1280
- status: messageStatus
914
+ const delivery = {
915
+ observeMessageSent: true,
916
+ deliver: async (payload) => {
917
+ if (pendingSettlement) await pendingSettlement;
918
+ let resolvedPayload = payload;
919
+ if (payload.presentation) {
920
+ let nativePayload;
921
+ const fallbackPayload = await (0, openclaw_plugin_sdk_interactive_runtime.renderPresentationForDelivery)({
922
+ presentationCapabilities: require_presentation.MSTEAMS_PRESENTATION_CAPABILITIES,
923
+ renderPresentation: (adapted) => {
924
+ nativePayload = adapted;
925
+ return adapted;
926
+ }
927
+ }, payload);
928
+ resolvedPayload = nativePayload ?? fallbackPayload;
929
+ }
930
+ const preparedPayload = streamController.preparePayload(resolvedPayload);
931
+ const native = streamController.claimNativeDelivery();
932
+ if (preparedPayload && !native && findPendingNativeDelivery()) await settleDelivery();
933
+ const messages = preparedPayload && !streamController.wasCanceled() ? renderReplyPayload(preparedPayload) : [];
934
+ if (!native && messages.length === 0) return {
935
+ visibleReplySent: false,
936
+ suppression: { reason: "no_visible_result" }
937
+ };
938
+ const pending = {
939
+ messages,
940
+ finalization: (0, openclaw_plugin_sdk_extension_shared.createDeferred)(),
941
+ content: payload.text,
942
+ blockResults: [],
943
+ nativeSettled: !native,
944
+ blockSettled: messages.length === 0,
945
+ settled: false,
946
+ errors: []
947
+ };
948
+ pendingDeliveries.push(pending);
949
+ if (blockStreamingEnabled) await flushPendingMessages();
950
+ settlePendingDelivery(pending);
951
+ return {
952
+ visibleReplySent: false,
953
+ finalization: pending.finalization.promise
954
+ };
955
+ },
956
+ onError: (err, info) => {
957
+ const errMsg = require_channel_setup.formatUnknownError(err);
958
+ const classification = require_channel_setup.classifyMSTeamsSendError(err);
959
+ const hint = require_channel_setup.formatMSTeamsSendErrorHint(classification);
960
+ params.log.error("reply failed", {
961
+ kind: info.kind,
962
+ error: errMsg,
963
+ classification,
964
+ hint
1281
965
  });
1282
- });
1283
- } catch (err) {
1284
- logGraphFailure("message fetch", err);
1285
- }
1286
- const sharePointMedia = await downloadMSTeamsAttachments({
1287
- ...params,
1288
- attachments: referenceAttachments,
1289
- graphReferenceToken: accessToken,
1290
- allowHosts: policy.allowHosts,
1291
- authAllowHosts: policy.authAllowHosts
1292
- });
1293
- const hosted = await downloadGraphHostedContent({
1294
- accessToken,
1295
- messageUrl,
1296
- maxBytes: params.maxBytes,
1297
- fetchFn: params.fetchFn,
1298
- ssrfPolicy,
1299
- logger: params.logger,
1300
- deadline: params.deadline
966
+ }
967
+ };
968
+ const settleDelivery = () => pendingSettlement ??= Promise.resolve().then(async () => {
969
+ await flushPendingMessages();
970
+ const nativeDelivery = findPendingNativeDelivery();
971
+ if (!nativeDelivery) {
972
+ await streamController.finalize();
973
+ return;
974
+ }
975
+ let nativeResult;
976
+ try {
977
+ nativeResult = await streamController.finalize();
978
+ } catch (error) {
979
+ nativeDelivery.errors.push(error);
980
+ nativeDelivery.nativeSettled = true;
981
+ settlePendingDelivery(nativeDelivery);
982
+ return;
983
+ }
984
+ if (nativeResult.visibleReplySent) nativeDelivery.nativeResult = {
985
+ messageIds: nativeResult.messageId ? [nativeResult.messageId] : [],
986
+ ...nativeResult.content !== void 0 ? { content: nativeResult.content } : {}
987
+ };
988
+ const hasPostNativePayloads = Boolean(nativeResult.postNativePayloads?.length);
989
+ if (nativeResult.logicalContent !== void 0) nativeDelivery.content = nativeResult.logicalContent;
990
+ else if (nativeResult.content !== void 0 && (!nativeResult.fallbackPayload && !hasPostNativePayloads || nativeDelivery.content === void 0)) nativeDelivery.content = nativeResult.content;
991
+ const afterNativePayloads = [...nativeResult.fallbackPayload ? [nativeResult.fallbackPayload] : [], ...nativeResult.postNativePayloads ?? []];
992
+ if (afterNativePayloads.length > 0) {
993
+ nativeDelivery.messages.push(...afterNativePayloads.flatMap((payload) => renderReplyPayload(payload)));
994
+ nativeDelivery.blockSettled = nativeDelivery.messages.length === 0;
995
+ }
996
+ nativeDelivery.nativeSettled = true;
997
+ if (!nativeDelivery.blockSettled) await flushPendingMessages();
998
+ settlePendingDelivery(nativeDelivery);
999
+ if (nativeResult.messageId) try {
1000
+ params.onSentMessageIds?.([nativeResult.messageId]);
1001
+ } catch (error) {
1002
+ params.log.warn?.("failed to record sent Teams message id", { error: require_channel_setup.formatUnknownError(error) });
1003
+ }
1004
+ }).finally(() => {
1005
+ pendingSettlement = void 0;
1301
1006
  });
1302
- const normalizedAttachments = messageAttachments.map(normalizeGraphAttachment);
1303
- const referenceUrls = new Set(referenceAttachments.map((att) => att.contentUrl));
1304
- const filteredAttachments = normalizedAttachments.filter((att) => att.contentType !== "reference" || !referenceUrls.has(att.contentUrl));
1305
- let attachmentMedia = [];
1306
- try {
1307
- attachmentMedia = await downloadMSTeamsAttachments({
1308
- attachments: filteredAttachments,
1309
- maxBytes: params.maxBytes,
1310
- tokenProvider: params.tokenProvider,
1311
- allowHosts: policy.allowHosts,
1312
- authAllowHosts: policy.authAllowHosts,
1313
- fetchFn: params.fetchFn,
1314
- resolveFn: params.resolveFn,
1315
- deadline: params.deadline,
1316
- preserveFilenames: params.preserveFilenames,
1317
- logger: params.logger
1318
- });
1319
- } catch (err) {
1320
- params.logger?.warn?.("msteams graph attachment download failed", {
1321
- error: (0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err),
1322
- messageUrl
1323
- });
1324
- }
1007
+ const shouldSuppressDefaultToolProgressMessages = streamController.hasStream() && teamsStreamMode === "progress";
1008
+ const progressCallbacks = streamController.hasStream() ? {
1009
+ onReasoningStream: async (payload) => {
1010
+ await streamController.pushReasoningProgress(payload.text, { snapshot: payload.isReasoningSnapshot === true });
1011
+ return false;
1012
+ },
1013
+ onReasoningEnd: () => {
1014
+ streamController.resetReasoningProgress();
1015
+ return false;
1016
+ },
1017
+ onToolStart: streamController.pushToolEvent,
1018
+ onItemEvent: streamController.pushItemEvent,
1019
+ onPlanUpdate: async (payload) => {
1020
+ if (payload.phase !== "update") return false;
1021
+ await streamController.pushPlanProgress(payload.steps, payload);
1022
+ return false;
1023
+ },
1024
+ onApprovalEvent: async (payload) => {
1025
+ await streamController.pushApprovalEvent(payload);
1026
+ return false;
1027
+ }
1028
+ } : {};
1325
1029
  return {
1326
- media: [
1327
- ...sharePointMedia,
1328
- ...hosted.media,
1329
- ...attachmentMedia
1330
- ],
1331
- hostedCount: hosted.count,
1332
- attachmentCount: filteredAttachments.length + sharePointMedia.length,
1333
- hostedStatus: hosted.status,
1334
- attachmentStatus: messageStatus,
1335
- messageUrl
1030
+ dispatcherOptions: {
1031
+ ...dispatcherOptions,
1032
+ onSettled: settleDelivery
1033
+ },
1034
+ delivery,
1035
+ replyOptions: {
1036
+ progressPreambleEnabled: shouldSuppressDefaultToolProgressMessages,
1037
+ commentaryProgressEnabled: shouldSuppressDefaultToolProgressMessages,
1038
+ ...streamController.hasStream() ? { onPartialReply: (payload) => {
1039
+ streamController.onPartialReply(payload);
1040
+ return false;
1041
+ } } : {},
1042
+ ...progressCallbacks,
1043
+ ...shouldSuppressDefaultToolProgressMessages ? { suppressDefaultToolProgressMessages: true } : {},
1044
+ disableBlockStreaming: blockStreamingResolved == null ? void 0 : !blockStreamingResolved,
1045
+ onModelSelected
1046
+ }
1336
1047
  };
1337
1048
  }
1338
- // extensions/msteams/src/monitor-handler/inbound-media.ts
1339
- function shouldAttemptMSTeamsGraphMediaFallback(params) {
1340
- const conversationType = params.conversationType.trim().toLowerCase();
1341
- return params.graphMediaFallback === true && (conversationType === "channel" || conversationType === "groupchat") && (params.htmlSummary?.htmlAttachments ?? 0) > 0;
1342
- }
1343
- function resolveMSTeamsInboundMediaBody(params) {
1344
- const unavailableCount = params.materializedMedia.filter((media) => !media.path).length + Math.max(0, params.nativeMedia.length - params.materializedMedia.length);
1345
- if (unavailableCount === 0) return params.body;
1346
- return (0, openclaw_plugin_sdk_channel_inbound.formatInboundMediaUnavailableText)({
1347
- body: params.body,
1348
- notice: `[msteams ${unavailableCount > 1 ? `${unavailableCount} attachments` : "attachment"} unavailable]`
1349
- });
1350
- }
1351
- function hasDefinitiveContentType(media) {
1352
- const contentType = media.contentType?.split(";", 1)[0]?.trim().toLowerCase();
1353
- return Boolean(contentType && contentType !== "application/octet-stream" && contentType !== "binary/octet-stream");
1049
+ // extensions/msteams/src/team-identity.ts
1050
+ const teamGroupIdCache = /* @__PURE__ */ new Map();
1051
+ const TEAM_GROUP_ID_CACHE_MAX_ENTRIES = 500;
1052
+ function cacheTeamGroupId(conversationTeamId, groupId) {
1053
+ teamGroupIdCache.set(conversationTeamId, groupId);
1054
+ (0, openclaw_plugin_sdk_collection_runtime.pruneMapToMaxSize)(teamGroupIdCache, TEAM_GROUP_ID_CACHE_MAX_ENTRIES);
1354
1055
  }
1355
- function mergeMSTeamsMediaFacts(nativeMedia, materializedMedia, options = {}) {
1356
- const merged = [...nativeMedia];
1357
- const nativeSlotCount = nativeMedia.length;
1358
- const nativeIndexBySourceId = /* @__PURE__ */ new Map();
1359
- nativeMedia.forEach((media, index) => {
1360
- if (media.sourceId && !nativeIndexBySourceId.has(media.sourceId)) nativeIndexBySourceId.set(media.sourceId, index);
1361
- });
1362
- for (const [index, materialized] of materializedMedia.entries()) {
1363
- const sourceIndex = materialized.sourceId ? nativeIndexBySourceId.get(materialized.sourceId) : void 0;
1364
- const positionalIndex = options.positionallyAligned === false || index >= nativeMedia.length ? void 0 : index;
1365
- const unresolvedIndexes = options.positionallyAligned === false ? merged.slice(0, nativeSlotCount).flatMap((media, mediaIndex) => !media.path && !media.sourceId ? [mediaIndex] : []) : [];
1366
- const fallbackIndex = unresolvedIndexes.find((mediaIndex) => merged[mediaIndex]?.kind === materialized.kind) ?? (unresolvedIndexes.length === 1 ? unresolvedIndexes[0] : void 0);
1367
- const targetIndex = sourceIndex ?? positionalIndex ?? fallbackIndex;
1368
- if (targetIndex === void 0) {
1369
- if (materialized.sourceId) nativeIndexBySourceId.set(materialized.sourceId, merged.length);
1370
- merged.push(materialized);
1371
- } else {
1372
- if (materialized.sourceId) nativeIndexBySourceId.set(materialized.sourceId, targetIndex);
1373
- const current = merged[targetIndex];
1374
- if (materialized.path) merged[targetIndex] = materialized;
1375
- else if (!current?.path) merged[targetIndex] = {
1376
- ...current,
1377
- ...materialized,
1378
- kind: hasDefinitiveContentType(materialized) || !current?.kind ? materialized.kind : current.kind
1379
- };
1380
- }
1056
+ /** Resolve the Graph team GUID without ever treating a Bot Framework team ID as equivalent. */
1057
+ async function resolveTeamGroupId(params) {
1058
+ const activityGroupId = params.aadGroupId?.trim();
1059
+ if (activityGroupId) {
1060
+ cacheTeamGroupId(params.conversationTeamId, activityGroupId);
1061
+ return activityGroupId;
1381
1062
  }
1382
- return merged;
1383
- }
1384
- function hasMaterializedMedia(media) {
1385
- return media.some((entry) => Boolean(entry.path));
1386
- }
1387
- async function resolveMSTeamsInboundMedia(params) {
1388
- const { attachments, htmlSummary, maxBytes, tokenProvider, allowHosts, conversationType, conversationId, conversationMessageId, teamAadGroupId, serviceUrl, activity, log } = params;
1389
- let mediaList = await downloadMSTeamsAttachments({
1390
- attachments,
1391
- maxBytes,
1392
- tokenProvider,
1393
- allowHosts,
1394
- authAllowHosts: params.authAllowHosts,
1063
+ const cached = teamGroupIdCache.get(params.conversationTeamId);
1064
+ if (cached) return cached;
1065
+ const getTeamDetails = params.getTeamDetails;
1066
+ if (!getTeamDetails) return;
1067
+ const groupId = (await require_resolve_allowlist.withMSTeamsRequestDeadline({
1395
1068
  deadline: params.deadline,
1396
- logger: log
1397
- });
1398
- if (!hasMaterializedMedia(mediaList)) {
1399
- const attachmentIds = extractMSTeamsHtmlAttachmentIds(attachments);
1400
- const hasHtmlFileAttachment = attachmentIds.length > 0;
1401
- const hasChannelOrGroupHtml = shouldAttemptMSTeamsGraphMediaFallback({
1402
- conversationType,
1403
- htmlSummary,
1404
- graphMediaFallback: params.graphMediaFallback
1405
- });
1406
- const shouldFetchGraphMessage = hasHtmlFileAttachment || hasChannelOrGroupHtml;
1407
- const isBotFrameworkPersonalChat = isBotFrameworkPersonalChatId(conversationId);
1408
- if (hasHtmlFileAttachment && isBotFrameworkPersonalChat) {
1409
- if (!serviceUrl) log.debug?.("bot framework attachment skipped (missing serviceUrl)", {
1410
- conversationType,
1411
- conversationId
1412
- });
1413
- else {
1414
- const bfMedia = await downloadMSTeamsBotFrameworkAttachments({
1415
- serviceUrl,
1416
- attachmentIds,
1417
- tokenProvider,
1418
- maxBytes,
1419
- allowHosts,
1420
- authAllowHosts: params.authAllowHosts,
1421
- deadline: params.deadline
1422
- });
1423
- mediaList = mergeMSTeamsMediaFacts(mediaList, bfMedia.media, { positionallyAligned: false });
1424
- if (!hasMaterializedMedia(bfMedia.media)) log.debug?.("bot framework attachments fetch empty", {
1425
- conversationType,
1426
- attachmentCount: bfMedia.attachmentCount ?? attachmentIds.length
1427
- });
1428
- }
1429
- }
1430
- if (shouldFetchGraphMessage && !isBotFrameworkPersonalChat) {
1431
- const graphTeamAadGroupId = conversationType.trim().toLowerCase() === "channel" && !teamAadGroupId ? await params.resolveTeamAadGroupId?.() : teamAadGroupId;
1432
- const messageUrl = buildMSTeamsGraphMessageUrl({
1433
- conversationType,
1434
- conversationId,
1435
- messageId: activity.id ?? void 0,
1436
- threadRootMessageId: conversationMessageId ?? activity.replyToId,
1437
- teamAadGroupId: graphTeamAadGroupId,
1438
- channelId: activity.channelData?.channel?.id
1439
- });
1440
- if (!messageUrl) log.debug?.("graph message url unavailable", {
1441
- conversationType,
1442
- hasChannelData: Boolean(activity.channelData),
1443
- messageId: activity.id ?? void 0,
1444
- replyToId: activity.replyToId ?? void 0
1445
- });
1446
- else {
1447
- const graphMedia = await downloadMSTeamsGraphMedia({
1448
- messageUrl,
1449
- tokenProvider,
1450
- maxBytes,
1451
- allowHosts,
1452
- authAllowHosts: params.authAllowHosts,
1453
- deadline: params.deadline,
1454
- logger: log
1455
- });
1456
- mediaList = mergeMSTeamsMediaFacts(mediaList, graphMedia.media, { positionallyAligned: false });
1457
- if (!hasMaterializedMedia(mediaList)) log.debug?.("graph media fetch empty", {
1458
- messageUrl,
1459
- hostedStatus: graphMedia.hostedStatus,
1460
- attachmentStatus: graphMedia.attachmentStatus,
1461
- hostedCount: graphMedia.hostedCount,
1462
- attachmentCount: graphMedia.attachmentCount,
1463
- tokenError: graphMedia.tokenError,
1464
- attachmentIdCount: attachmentIds.length
1465
- });
1466
- }
1467
- }
1468
- }
1469
- const materializedCount = mediaList.filter((media) => Boolean(media.path)).length;
1470
- if (materializedCount > 0) log.debug?.("downloaded attachments", { count: materializedCount });
1471
- else if (htmlSummary?.imgTags) log.debug?.("inline images detected but none downloaded", {
1472
- imgTags: htmlSummary.imgTags,
1473
- srcHosts: htmlSummary.srcHosts,
1474
- dataImages: htmlSummary.dataImages,
1475
- cidImages: htmlSummary.cidImages
1476
- });
1477
- return mediaList;
1069
+ label: "MS Teams team details",
1070
+ work: () => getTeamDetails(params.conversationTeamId)
1071
+ })).aadGroupId?.trim();
1072
+ if (!groupId) return;
1073
+ cacheTeamGroupId(params.conversationTeamId, groupId);
1074
+ return groupId;
1478
1075
  }
1479
- // extensions/msteams/src/monitor-handler/inbound-content.ts
1480
- async function prepareMSTeamsInboundContent(params) {
1481
- const activity = params.entry.context.activity;
1482
- const mayRecoverGraphMedia = Boolean(params.htmlSummary?.attachmentIds.length) || shouldAttemptMSTeamsGraphMediaFallback({
1483
- conversationType: params.conversationType,
1484
- htmlSummary: params.htmlSummary,
1485
- graphMediaFallback: params.graphMediaFallback
1076
+ // extensions/msteams/src/monitor-handler/config.ts
1077
+ function createMSTeamsHandlerConfigReader(deps) {
1078
+ const source = deps.accountPolicyCfg ?? deps.cfg;
1079
+ const read = (0, openclaw_plugin_sdk_runtime_config_snapshot.createRuntimeConfigReader)(source);
1080
+ const originallyConfigured = require_resolve_allowlist.listMSTeamsAccountIds(source).includes(deps.accountId);
1081
+ let lastSource;
1082
+ let lastProjection = deps.cfg;
1083
+ return () => {
1084
+ const current = read();
1085
+ if (current === lastSource) return lastProjection;
1086
+ lastSource = current;
1087
+ if (current === source && deps.accountPolicyCfg) return lastProjection = deps.cfg;
1088
+ const retired = originallyConfigured && !require_resolve_allowlist.listMSTeamsAccountIds(current).includes(deps.accountId);
1089
+ if (deps.accountId === "default" && !current.channels?.msteams?.accounts && !retired) return lastProjection = current;
1090
+ const accountConfig = require_resolve_allowlist.resolveMSTeamsAccountConfig(current, deps.accountId);
1091
+ return lastProjection = require_resolve_allowlist.withAccountScopedMSTeamsConfig({
1092
+ cfg: current,
1093
+ accountId: deps.accountId,
1094
+ accountConfig: {
1095
+ ...accountConfig,
1096
+ ...current.channels?.msteams?.enabled === false || retired ? { enabled: false } : {}
1097
+ }
1098
+ });
1099
+ };
1100
+ }
1101
+ // extensions/msteams/src/attachments/bot-framework.ts
1102
+ const BOT_FRAMEWORK_SCOPE = "https://api.botframework.com";
1103
+ /**
1104
+ * Detect Bot Framework personal chat ("a:") and MSA orgid ("8:orgid:") conversation
1105
+ * IDs. These identifiers are not recognized by Graph's `/chats/{id}` endpoint, so we
1106
+ * must fetch media via the Bot Framework v3 attachments endpoint instead.
1107
+ *
1108
+ * Graph-compatible IDs start with `19:` and are left untouched by this detector.
1109
+ */
1110
+ function isBotFrameworkPersonalChatId(conversationId) {
1111
+ if (typeof conversationId !== "string") return false;
1112
+ const trimmed = conversationId.trim();
1113
+ return trimmed.startsWith("a:") || trimmed.startsWith("8:orgid:");
1114
+ }
1115
+ async function fetchBotFrameworkAttachment(params, kind) {
1116
+ let response;
1117
+ try {
1118
+ const headers = require_resolve_allowlist.ensureUserAgentHeader();
1119
+ require_resolve_allowlist.applyAuthorizationHeaderForUrl({
1120
+ headers,
1121
+ url: params.url,
1122
+ authAllowHosts: params.policy.authAllowHosts,
1123
+ bearerToken: params.accessToken
1124
+ });
1125
+ response = await require_resolve_allowlist.safeFetchWithPolicy({
1126
+ url: params.url,
1127
+ policy: params.policy,
1128
+ resolveFn: node_dns_promises.lookup,
1129
+ requestInit: { headers },
1130
+ timeoutMs: require_resolve_allowlist.resolveMSTeamsRequestTimeoutMs(params.deadline)
1131
+ });
1132
+ } catch (err) {
1133
+ params.logger?.warn?.(`msteams botFramework ${kind} fetch failed`, { error: (0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err) });
1134
+ return;
1135
+ }
1136
+ if (!response.ok) {
1137
+ await response.body?.cancel().catch(() => void 0);
1138
+ params.logger?.warn?.(`msteams botFramework ${kind} non-ok`, { status: response.status });
1139
+ return;
1140
+ }
1141
+ return response;
1142
+ }
1143
+ async function downloadMSTeamsBotFrameworkAttachment(params) {
1144
+ if (!params.serviceUrl || !params.attachmentId || !params.tokenProvider) return;
1145
+ const tokenProvider = params.tokenProvider;
1146
+ const policy = require_resolve_allowlist.resolveAttachmentFetchPolicy({
1147
+ allowHosts: params.allowHosts,
1148
+ authAllowHosts: params.authAllowHosts
1486
1149
  });
1487
- if (!params.rawBody && params.advertisedMedia.length === 0 && !mayRecoverGraphMedia) {
1488
- params.log.debug?.("skipping empty message after stripping mentions");
1489
- return null;
1150
+ const baseUrl = `${params.serviceUrl.replace(/\/+$/, "")}/v3/attachments/${encodeURIComponent(params.attachmentId)}`;
1151
+ if (!(0, openclaw_plugin_sdk_ssrf_policy.isHttpsUrlAllowedByHostnameSuffixAllowlist)(baseUrl, policy.allowHosts)) return;
1152
+ let accessToken;
1153
+ try {
1154
+ accessToken = await require_resolve_allowlist.withMSTeamsRequestDeadline({
1155
+ deadline: params.deadline,
1156
+ label: "MS Teams Bot Framework token",
1157
+ work: () => tokenProvider.getAccessToken(BOT_FRAMEWORK_SCOPE)
1158
+ });
1159
+ } catch (err) {
1160
+ params.logger?.warn?.("msteams botFramework token acquisition failed", { error: (0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err) });
1161
+ return;
1490
1162
  }
1491
- let mediaList = [];
1492
- const deadline = require_resolve_allowlist.createMSTeamsInboundMediaDeadline();
1163
+ if (!accessToken) return;
1164
+ const request = {
1165
+ url: baseUrl,
1166
+ accessToken,
1167
+ policy,
1168
+ logger: params.logger,
1169
+ deadline: params.deadline
1170
+ };
1171
+ const infoResponse = await fetchBotFrameworkAttachment(request, "attachmentInfo");
1172
+ if (!infoResponse) return;
1173
+ let info;
1493
1174
  try {
1494
- mediaList = await require_resolve_allowlist.withMSTeamsRequestDeadline({
1495
- deadline,
1496
- label: "MS Teams inbound media",
1497
- work: () => resolveMSTeamsInboundMedia({
1498
- attachments: params.entry.attachments,
1499
- htmlSummary: params.htmlSummary,
1500
- maxBytes: params.mediaMaxBytes,
1501
- tokenProvider: params.tokenProvider,
1502
- allowHosts: params.mediaAllowHosts,
1503
- authAllowHosts: params.mediaAuthAllowHosts,
1504
- graphMediaFallback: params.graphMediaFallback,
1505
- conversationType: params.conversationType,
1506
- conversationId: params.conversationId,
1507
- conversationMessageId: params.conversationMessageId,
1508
- teamAadGroupId: params.teamAadGroupId,
1509
- resolveTeamAadGroupId: () => params.resolveTeamAadGroupId(deadline),
1510
- serviceUrl: activity.serviceUrl,
1511
- activity: {
1512
- id: activity.id,
1513
- replyToId: activity.replyToId,
1514
- channelData: activity.channelData
1515
- },
1516
- log: params.log,
1517
- deadline
1518
- })
1175
+ info = await (0, openclaw_plugin_sdk_provider_http.readProviderJsonResponse)(infoResponse, "msteams botFramework attachmentInfo");
1176
+ } catch (err) {
1177
+ request.logger?.warn?.("msteams botFramework attachmentInfo parse failed", { error: (0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err) });
1178
+ return;
1179
+ }
1180
+ if (!info) return;
1181
+ const views = Array.isArray(info.views) ? info.views : [];
1182
+ const candidateView = views.find((view) => view?.viewId === "original") ?? views.find((view) => typeof view?.viewId === "string");
1183
+ const viewId = typeof candidateView?.viewId === "string" && candidateView.viewId ? candidateView.viewId : void 0;
1184
+ if (!viewId) return;
1185
+ if (typeof candidateView?.size === "number" && candidateView.size > 0 && candidateView.size > params.maxBytes) return;
1186
+ const fileNameHint = typeof info.name === "string" && info.name || void 0;
1187
+ const contentTypeHint = typeof info.type === "string" && info.type || void 0;
1188
+ const viewUrl = `${baseUrl}/views/${encodeURIComponent(viewId)}`;
1189
+ const maxBytes = params.maxBytes;
1190
+ const response = await fetchBotFrameworkAttachment({
1191
+ ...request,
1192
+ url: viewUrl
1193
+ }, "attachmentView");
1194
+ if (!response) return;
1195
+ let saved;
1196
+ try {
1197
+ saved = await require_runtime.getMSTeamsRuntime().channel.media.saveResponseMedia(response, {
1198
+ sourceUrl: viewUrl,
1199
+ filePathHint: fileNameHint,
1200
+ maxBytes,
1201
+ fallbackContentType: contentTypeHint,
1202
+ subdir: "inbound"
1519
1203
  });
1520
1204
  } catch (err) {
1521
- params.log.debug?.("failed to resolve inbound Teams media", { error: require_channel_setup.formatUnknownError(err) });
1205
+ request.logger?.warn?.("msteams botFramework attachmentView save failed", { error: (0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err) });
1206
+ return;
1207
+ } finally {
1208
+ await response.body?.cancel().catch(() => void 0);
1522
1209
  }
1523
- const inboundMedia = mergeMSTeamsMediaFacts(params.advertisedMedia, mediaList);
1524
- const nativeMediaForComparison = [...params.advertisedMedia, ...mediaList.slice(params.advertisedMedia.length).map((media) => ({
1525
- contentType: media.contentType,
1526
- kind: media.kind
1527
- }))];
1528
- const agentBody = resolveMSTeamsInboundMediaBody({
1529
- body: params.rawBody,
1530
- nativeMedia: nativeMediaForComparison,
1531
- materializedMedia: inboundMedia
1532
- });
1533
- if (!agentBody && inboundMedia.length === 0) {
1534
- params.log.debug?.("skipping empty message after Graph media recovery");
1535
- return null;
1210
+ if (!saved) return;
1211
+ return {
1212
+ path: saved.path,
1213
+ contentType: saved.contentType,
1214
+ kind: require_resolve_allowlist.resolveMSTeamsMediaKind({
1215
+ contentType: saved.contentType,
1216
+ fileName: fileNameHint
1217
+ })
1218
+ };
1219
+ }
1220
+ /**
1221
+ * Download media for every attachment referenced by a Bot Framework personal
1222
+ * chat activity. Returns all successfully fetched media along with diagnostics
1223
+ * compatible with `downloadMSTeamsGraphMedia`'s result shape so callers can
1224
+ * reuse the existing logging path.
1225
+ */
1226
+ async function downloadMSTeamsBotFrameworkAttachments(params) {
1227
+ const unique = (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeUniqueTrimmedStringList)(params.attachmentIds);
1228
+ if (unique.length === 0 || !params.serviceUrl || !params.tokenProvider) return {
1229
+ media: [],
1230
+ attachmentCount: unique.length
1231
+ };
1232
+ const media = [];
1233
+ for (const attachmentId of unique) try {
1234
+ const item = await downloadMSTeamsBotFrameworkAttachment({
1235
+ ...params,
1236
+ attachmentId
1237
+ });
1238
+ if (item) media.push({
1239
+ ...item,
1240
+ sourceId: attachmentId
1241
+ });
1242
+ else media.push({
1243
+ kind: "document",
1244
+ sourceId: attachmentId
1245
+ });
1246
+ } catch (err) {
1247
+ media.push({
1248
+ kind: "document",
1249
+ sourceId: attachmentId
1250
+ });
1251
+ params.logger?.warn?.("msteams botFramework attachment download failed", {
1252
+ error: (0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err),
1253
+ attachmentId
1254
+ });
1536
1255
  }
1537
1256
  return {
1538
- agentBody,
1539
- inboundMedia
1257
+ media,
1258
+ attachmentCount: unique.length
1540
1259
  };
1541
1260
  }
1542
- // extensions/msteams/src/informative-status.ts
1543
- const INFORMATIVE_MAX_CHARS = 1e3;
1544
- const INFORMATIVE_MAX_BYTES = 1024;
1545
- const ELLIPSIS = "…";
1546
- const segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
1547
- const utf8 = new TextEncoder();
1548
- function flattenInformativeStatus(text) {
1549
- const joined = text.split("\n").map((line) => line.trim().replace(/^[•-]\s+/u, "")).filter(Boolean).join(" · ");
1550
- if (joined.length <= INFORMATIVE_MAX_CHARS && utf8.encode(joined).length <= INFORMATIVE_MAX_BYTES) return joined;
1551
- const graphemes = Array.from(segmenter.segment(joined), (s) => s.segment);
1552
- let chars = 1;
1553
- let bytes = utf8.encode(ELLIPSIS).length;
1554
- let start = graphemes.length;
1555
- while (start > 0) {
1556
- const next = graphemes[start - 1];
1557
- const nextBytes = utf8.encode(next).length;
1558
- if (chars + next.length > INFORMATIVE_MAX_CHARS || bytes + nextBytes > INFORMATIVE_MAX_BYTES) break;
1559
- chars += next.length;
1560
- bytes += nextBytes;
1561
- start -= 1;
1261
+ // extensions/msteams/src/attachments/html.ts
1262
+ function collectHtmlAttachmentIds(html, ids) {
1263
+ for (const value of require_resolve_allowlist.htmlTagAttributeValues(html, "attachment", "id")) {
1264
+ const id = value.trim();
1265
+ if (id) ids.add(id);
1562
1266
  }
1563
- return ELLIPSIS + graphemes.slice(start).join("");
1564
1267
  }
1565
- // extensions/msteams/src/reply-stream-controller.ts
1566
- function isStreamCancelledError(err) {
1567
- return err instanceof Error && err.name === "StreamCancelledError";
1268
+ /**
1269
+ * Extract every `<attachment id="...">` reference from the HTML attachments in
1270
+ * the inbound activity. Returns the complete (non-sliced) list; callers that
1271
+ * need a capped diagnostic summary can truncate after calling this helper.
1272
+ */
1273
+ function extractMSTeamsHtmlAttachmentIds(attachments) {
1274
+ const list = attachments ?? [];
1275
+ const ids = /* @__PURE__ */ new Set();
1276
+ for (const att of list) {
1277
+ const html = require_resolve_allowlist.extractHtmlFromAttachment(att);
1278
+ if (!html) continue;
1279
+ collectHtmlAttachmentIds(html, ids);
1280
+ }
1281
+ return Array.from(ids);
1568
1282
  }
1569
- /** Bridge reply callbacks to the SDK stream; shared conversations use block delivery. */
1570
- function createTeamsReplyStreamController(params) {
1571
- const isPersonal = (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeOptionalLowercaseString)(params.conversationType) === "personal";
1572
- const streamMode = (0, openclaw_plugin_sdk_channel_outbound.resolveChannelPreviewStreamMode)(params.msteamsConfig, "partial");
1573
- const stream = params.allowProviderPreview && isPersonal && (streamMode === "partial" || streamMode === "progress") ? params.context.stream : void 0;
1574
- let tokensEmitted = false;
1575
- let nativeDispatchStarted = false;
1576
- let nativeDeliveryClaimed = false;
1577
- let streamFinalizationPending = false;
1578
- let canceledLocally = false;
1579
- let pendingFinalPayload;
1580
- let emittedText = "";
1581
- let acknowledgedText = "";
1582
- let acknowledgedLogicalText = "";
1583
- let acknowledgedStreamId;
1584
- let replacementFinalPending = false;
1585
- let replacementSettlementPending = false;
1586
- let replacementTextAwaitingAcknowledgement;
1587
- let deferredReplacementEntries = [];
1588
- let queuedFinalActivity;
1589
- const streamEvents = stream?.events;
1590
- let streamChunkSubscription;
1591
- if (typeof streamEvents?.on === "function" && typeof streamEvents.off === "function") streamChunkSubscription = streamEvents.on("chunk", (activity) => {
1592
- const replacementAcknowledgementPending = replacementTextAwaitingAcknowledgement !== void 0;
1593
- const replacementAcknowledgement = typeof activity.text === "string" && replacementAcknowledgementPending && activity.text === replacementTextAwaitingAcknowledgement?.text;
1594
- if (activity.type !== "typing" || activity.channelData?.streamType !== "streaming" || !activity.id || !activity.text || acknowledgedStreamId !== void 0 && activity.id !== acknowledgedStreamId || (replacementAcknowledgementPending ? !replacementAcknowledgement : !activity.text.startsWith(acknowledgedText)) || !replacementAcknowledgement && !emittedText.startsWith(activity.text)) return;
1595
- acknowledgedStreamId = activity.id;
1596
- acknowledgedText = activity.text;
1597
- acknowledgedLogicalText = replacementAcknowledgement ? replacementTextAwaitingAcknowledgement.logicalText : activity.text;
1598
- if (replacementAcknowledgement) replacementTextAwaitingAcknowledgement = void 0;
1599
- });
1600
- const wasCanceled = () => canceledLocally || Boolean(stream?.canceled);
1601
- const releaseStreamChunkSubscription = () => {
1602
- if (streamChunkSubscription === void 0) return;
1603
- streamEvents?.off(streamChunkSubscription);
1604
- streamChunkSubscription = void 0;
1605
- };
1606
- const acknowledgedNativeDelivery = () => {
1607
- if (!acknowledgedStreamId || !acknowledgedText) return { visibleReplySent: false };
1608
- return {
1609
- visibleReplySent: true,
1610
- content: acknowledgedText,
1611
- messageId: acknowledgedStreamId
1612
- };
1613
- };
1614
- const fallbackPayloadAfterAcknowledgedText = (payload) => {
1615
- if (!acknowledgedLogicalText || typeof payload.text !== "string" || !payload.text.startsWith(acknowledgedLogicalText)) return payload;
1616
- const remainingText = payload.text.slice(acknowledgedLogicalText.length);
1617
- const hasMedia = Boolean(payload.mediaUrl || payload.mediaUrls?.length);
1618
- if (!remainingText && !hasMedia) return;
1619
- return {
1620
- ...payload,
1621
- text: remainingText || void 0
1622
- };
1623
- };
1624
- const fallbackPayloadForSuppressedFinal = (payload) => {
1625
- return Boolean(payload.mediaUrl || payload.mediaUrls?.length) ? {
1626
- ...payload,
1627
- mediaUrl: void 0,
1628
- mediaUrls: void 0
1629
- } : payload;
1630
- };
1631
- const finalStreamActivity = (text) => ({
1632
- ...require_conversation_store_state.buildMSTeamsMessageActivity(text === void 0 ? void 0 : require_conversation_store_state.formatMSTeamsMarkdown(text, params.tableMode ?? "code")),
1633
- channelData: params.feedbackLoopEnabled ? { feedbackLoopEnabled: true } : {}
1634
- });
1635
- const queueFinalReplacement = (activeStream, text) => {
1636
- const activity = finalStreamActivity(text);
1637
- replacementTextAwaitingAcknowledgement = {
1638
- text: activity.text,
1639
- logicalText: text
1640
- };
1641
- activeStream.emit(activity);
1642
- queuedFinalActivity = activity;
1643
- emittedText = text;
1644
- };
1645
- const deferredReplacementLogicalContent = () => {
1646
- return deferredReplacementEntries.map((entry) => entry.payload.text).filter((text) => Boolean(text)).join("\n") || void 0;
1647
- };
1648
- const deferredReplacementPayloads = (replacementFallback) => deferredReplacementEntries.flatMap((entry) => {
1649
- if (entry.kind === "payload") return [entry.payload];
1650
- const hasMedia = Boolean(entry.payload.mediaUrl || entry.payload.mediaUrls?.length);
1651
- const text = replacementFallback?.text;
1652
- if (!text && !hasMedia) return [];
1653
- return [{
1654
- ...entry.payload,
1655
- text: text || void 0
1656
- }];
1657
- });
1658
- const finalizeWithoutReceipt = (logicalContent, canceled = false) => {
1659
- const fallback = pendingFinalPayload && !canceled ? fallbackPayloadAfterAcknowledgedText(pendingFinalPayload) : void 0;
1660
- const postNativePayloads = replacementSettlementPending && !canceled ? deferredReplacementPayloads(fallback) : [];
1661
- return {
1662
- ...acknowledgedNativeDelivery(),
1663
- ...!canceled && logicalContent ? { logicalContent } : {},
1664
- ...!replacementSettlementPending && fallback ? { fallbackPayload: fallback } : {},
1665
- ...postNativePayloads.length > 0 ? { postNativePayloads } : {}
1666
- };
1667
- };
1668
- const progressDraft = (0, openclaw_plugin_sdk_channel_outbound.createChannelProgressDraftCompositor)({
1669
- preparedItems: true,
1670
- formatPlainText: (text) => text,
1671
- entry: params.msteamsConfig,
1672
- mode: streamMode,
1673
- active: Boolean(stream) && streamMode === "progress",
1674
- seed: params.progressSeed ?? "msteams",
1675
- update: (text) => {
1676
- if (!stream || wasCanceled() || streamFinalizationPending) return false;
1677
- try {
1678
- stream.update(flattenInformativeStatus(text));
1679
- return true;
1680
- } catch (err) {
1681
- if (isStreamCancelledError(err)) canceledLocally = true;
1682
- else params.log?.debug?.(`stream informative update failed: ${(0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err)}`);
1683
- return false;
1283
+ function summarizeMSTeamsHtmlAttachments(attachments) {
1284
+ const list = attachments ?? [];
1285
+ let htmlAttachments = 0;
1286
+ let imgTags = 0;
1287
+ let dataImages = 0;
1288
+ let cidImages = 0;
1289
+ const srcHosts = /* @__PURE__ */ new Set();
1290
+ const attachmentIds = /* @__PURE__ */ new Set();
1291
+ for (const att of list) {
1292
+ const html = require_resolve_allowlist.extractHtmlFromAttachment(att);
1293
+ if (!html) continue;
1294
+ htmlAttachments += 1;
1295
+ for (const value of require_resolve_allowlist.htmlTagAttributeValues(html, "img", "src")) {
1296
+ imgTags += 1;
1297
+ const src = value.trim();
1298
+ if (src) {
1299
+ if (src.startsWith("data:")) dataImages += 1;
1300
+ else if (src.startsWith("cid:")) cidImages += 1;
1301
+ else srcHosts.add(require_resolve_allowlist.safeHostForUrl(src));
1684
1302
  }
1685
1303
  }
1686
- });
1304
+ collectHtmlAttachmentIds(html, attachmentIds);
1305
+ }
1306
+ if (htmlAttachments === 0) return;
1687
1307
  return {
1688
- onPartialReply(payload) {
1689
- if (!stream || !payload.text || wasCanceled() || streamMode !== "partial") return;
1690
- if (replacementSettlementPending && replacementFinalPending) {
1691
- pendingFinalPayload = { text: payload.text };
1692
- return;
1693
- }
1694
- if (streamFinalizationPending || nativeDeliveryClaimed) return;
1695
- const fullText = payload.text;
1696
- let prefixLength = 0;
1697
- while (prefixLength < emittedText.length && prefixLength < fullText.length && emittedText[prefixLength] === fullText[prefixLength]) prefixLength += 1;
1698
- const previousRemainder = emittedText.slice(prefixLength);
1699
- const delta = fullText.slice(prefixLength);
1700
- if (!delta) return;
1701
- if (previousRemainder.trim()) {
1702
- stream.clearText();
1703
- replacementFinalPending = true;
1704
- replacementSettlementPending = true;
1705
- pendingFinalPayload = { text: fullText };
1706
- streamFinalizationPending = true;
1707
- return;
1708
- }
1709
- try {
1710
- stream.emit(delta);
1711
- emittedText = fullText;
1712
- tokensEmitted = true;
1713
- nativeDispatchStarted = true;
1714
- } catch (err) {
1715
- if (isStreamCancelledError(err)) {
1716
- canceledLocally = true;
1717
- return;
1718
- }
1719
- throw err;
1720
- }
1721
- },
1722
- pushItemEvent: progressDraft.pushItemEvent.bind(progressDraft),
1723
- pushToolEvent: progressDraft.pushToolEvent,
1724
- pushReasoningProgress: progressDraft.pushReasoningProgress.bind(progressDraft),
1725
- resetReasoningProgress: progressDraft.resetReasoningProgress,
1726
- pushApprovalEvent: progressDraft.pushApprovalEvent.bind(progressDraft),
1727
- pushPlanProgress: progressDraft.pushPlanProgress.bind(progressDraft),
1728
- preparePayload(payload) {
1729
- if (!stream) return payload;
1730
- if (wasCanceled()) return;
1731
- if (payload.text) progressDraft.markFinalReplyStarted();
1732
- if (replacementSettlementPending) {
1733
- if (!replacementFinalPending || !payload.text) {
1734
- deferredReplacementEntries.push({
1735
- kind: "payload",
1736
- payload
1737
- });
1738
- return;
1739
- }
1740
- replacementFinalPending = false;
1741
- deferredReplacementEntries.push({
1742
- kind: "replacement",
1743
- payload
1744
- });
1745
- pendingFinalPayload = fallbackPayloadForSuppressedFinal(payload);
1746
- try {
1747
- queueFinalReplacement(stream, payload.text);
1748
- tokensEmitted = false;
1749
- return;
1750
- } catch (err) {
1751
- replacementTextAwaitingAcknowledgement = void 0;
1752
- tokensEmitted = false;
1753
- if (isStreamCancelledError(err)) {
1754
- canceledLocally = true;
1755
- pendingFinalPayload = void 0;
1756
- deferredReplacementEntries = [];
1757
- return;
1758
- }
1759
- throw err;
1760
- }
1761
- }
1762
- if (streamMode === "progress" && payload.text && !nativeDispatchStarted) try {
1763
- stream.emit(payload.text);
1764
- emittedText = payload.text;
1765
- nativeDispatchStarted = true;
1766
- tokensEmitted = true;
1767
- } catch (err) {
1768
- if (isStreamCancelledError(err)) {
1769
- canceledLocally = true;
1770
- return;
1771
- }
1772
- throw err;
1773
- }
1774
- if (tokensEmitted) {
1775
- const hasMedia = Boolean(payload.mediaUrl || payload.mediaUrls?.length);
1776
- pendingFinalPayload = fallbackPayloadForSuppressedFinal(payload);
1777
- streamFinalizationPending = true;
1778
- tokensEmitted = false;
1779
- return hasMedia ? {
1780
- ...payload,
1781
- text: void 0
1782
- } : void 0;
1783
- }
1784
- return payload;
1785
- },
1786
- claimNativeDelivery() {
1787
- if (!nativeDispatchStarted || nativeDeliveryClaimed) return false;
1788
- nativeDeliveryClaimed = true;
1789
- return true;
1790
- },
1791
- async finalize() {
1792
- progressDraft.markFinalReplyStarted();
1793
- if (!stream || !nativeDispatchStarted) {
1794
- releaseStreamChunkSubscription();
1795
- return { visibleReplySent: false };
1796
- }
1797
- let logicalContent;
1798
- try {
1799
- if (wasCanceled() || !streamFinalizationPending) return acknowledgedNativeDelivery();
1800
- if (replacementSettlementPending && replacementFinalPending && pendingFinalPayload?.text) {
1801
- replacementFinalPending = false;
1802
- deferredReplacementEntries.push({
1803
- kind: "replacement",
1804
- payload: pendingFinalPayload
1805
- });
1806
- logicalContent = deferredReplacementLogicalContent();
1807
- queueFinalReplacement(stream, pendingFinalPayload.text);
1808
- }
1809
- logicalContent ??= replacementSettlementPending ? deferredReplacementLogicalContent() : void 0;
1810
- const logicalText = pendingFinalPayload?.text ?? (emittedText || void 0);
1811
- const finalActivity = queuedFinalActivity ?? finalStreamActivity(logicalText);
1812
- const content = finalActivity.text;
1813
- logicalContent ??= content !== logicalText ? logicalText : void 0;
1814
- if (!queuedFinalActivity) {
1815
- if (content !== void 0 && content !== emittedText) {
1816
- stream.clearText();
1817
- replacementTextAwaitingAcknowledgement = {
1818
- text: content,
1819
- logicalText
1820
- };
1821
- stream.emit(finalActivity);
1822
- } else stream.emit({
1823
- ...finalActivity,
1824
- text: void 0
1825
- });
1826
- }
1827
- const result = await stream.close();
1828
- if (!result) return finalizeWithoutReceipt(logicalContent, wasCanceled());
1829
- const messageId = require_conversation_store_state.extractMessageId(result) ?? acknowledgedStreamId;
1830
- const postNativePayloads = replacementSettlementPending ? deferredReplacementPayloads(void 0) : [];
1831
- return {
1832
- visibleReplySent: true,
1833
- ...content === void 0 ? {} : { content },
1834
- ...logicalContent ? { logicalContent } : {},
1835
- ...messageId ? { messageId } : {},
1836
- ...postNativePayloads.length > 0 ? { postNativePayloads } : {}
1837
- };
1838
- } catch (err) {
1839
- if (isStreamCancelledError(err)) {
1840
- canceledLocally = true;
1841
- return acknowledgedNativeDelivery();
1842
- }
1843
- params.log?.warn?.(`msteams stream finalize failed: ${(0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err)}`);
1844
- return finalizeWithoutReceipt(logicalContent);
1845
- } finally {
1846
- pendingFinalPayload = void 0;
1847
- streamFinalizationPending = false;
1848
- queuedFinalActivity = void 0;
1849
- replacementFinalPending = false;
1850
- replacementSettlementPending = false;
1851
- replacementTextAwaitingAcknowledgement = void 0;
1852
- deferredReplacementEntries = [];
1853
- releaseStreamChunkSubscription();
1854
- }
1855
- },
1856
- hasStream() {
1857
- return Boolean(stream);
1858
- },
1859
- isStreamActive() {
1860
- return Boolean(stream) && tokensEmitted && !wasCanceled();
1861
- },
1862
- wasCanceled
1308
+ htmlAttachments,
1309
+ imgTags,
1310
+ dataImages,
1311
+ cidImages,
1312
+ srcHosts: Array.from(srcHosts).slice(0, 5),
1313
+ attachmentIds: Array.from(attachmentIds).slice(0, 5)
1863
1314
  };
1864
1315
  }
1865
- // extensions/msteams/src/reply-dispatcher.ts
1866
- function createMSTeamsReplyDispatcher(params) {
1867
- const core = require_runtime.getMSTeamsRuntime();
1868
- const msteamsCfg = require_resolve_allowlist.resolveMSTeamsAccountConfig(params.cfg, params.accountId);
1869
- const mediaMaxBytes = (0, openclaw_plugin_sdk_account_helpers.resolveChannelMediaMaxBytes)({
1870
- cfg: params.cfg,
1871
- resolveChannelLimitMb: () => msteamsCfg.mediaMaxMb
1872
- });
1873
- const feedbackLoopEnabled = msteamsCfg.feedbackEnabled !== false;
1874
- const conversationType = (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeOptionalLowercaseString)(params.conversationRef.conversation?.conversationType);
1875
- const isTypingSupported = conversationType === "personal" || conversationType === "groupchat";
1876
- const TYPING_KEEPALIVE_INTERVAL_MS = 8e3;
1877
- const TYPING_KEEPALIVE_MAX_DURATION_MS = 6e5;
1878
- const sendTypingIndicator = async () => {
1879
- if (!isTypingSupported || streamController.isStreamActive() || streamController.wasCanceled()) return;
1880
- await require_conversation_store_state.withRevokedProxyFallback({
1881
- run: async () => {
1882
- await params.context.sendActivity({ type: "typing" });
1883
- },
1884
- onRevoked: async () => {
1885
- await require_conversation_store_state.sendMSTeamsActivityWithReference(params.app, require_conversation_store_state.buildConversationReference(params.conversationRef), { type: "typing" }, {
1886
- accountId: params.accountId,
1887
- serviceUrlBoundary: require_resolve_allowlist.resolveMSTeamsSdkCloudOptions(msteamsCfg)
1888
- });
1889
- },
1890
- onRevokedLog: () => {
1891
- params.log.debug?.("turn context revoked, sending typing via proactive messaging");
1892
- }
1893
- });
1894
- };
1895
- const { onModelSelected, typingCallbacks, ...replyPipeline } = (0, openclaw_plugin_sdk_channel_outbound.createChannelMessageReplyPipeline)({
1896
- cfg: params.cfg,
1897
- agentId: params.agentId,
1898
- channel: "msteams",
1899
- accountId: params.accountId,
1900
- typing: {
1901
- start: sendTypingIndicator,
1902
- keepaliveIntervalMs: TYPING_KEEPALIVE_INTERVAL_MS,
1903
- maxDurationMs: TYPING_KEEPALIVE_MAX_DURATION_MS,
1904
- onStartError: (err) => {
1905
- (0, openclaw_plugin_sdk_channel_outbound.logTypingFailure)({
1906
- log: (message) => params.log.debug?.(message),
1907
- channel: "msteams",
1908
- action: "start",
1909
- error: err
1910
- });
1911
- }
1912
- }
1913
- });
1914
- const chunkMode = core.channel.text.resolveChunkMode(params.cfg, "msteams", params.accountId);
1915
- const tableMode = core.channel.text.resolveMarkdownTableMode({
1916
- cfg: params.cfg,
1917
- channel: "msteams",
1918
- accountId: params.accountId
1919
- });
1920
- const hookRunner = (0, openclaw_plugin_sdk_plugin_runtime.getGlobalHookRunner)();
1921
- const streamController = createTeamsReplyStreamController({
1922
- tableMode,
1923
- allowProviderPreview: !((hookRunner?.hasHooks("reply_payload_sending") ?? false) || (hookRunner?.hasHooks("message_sending") ?? false)),
1924
- conversationType,
1925
- context: params.context,
1926
- feedbackLoopEnabled,
1927
- log: params.log,
1928
- msteamsConfig: msteamsCfg,
1929
- progressSeed: `${params.accountId ?? "default"}:${params.conversationRef.conversation?.id ?? ""}`
1930
- });
1931
- const teamsStreamMode = (0, openclaw_plugin_sdk_channel_outbound.resolveChannelPreviewStreamMode)(msteamsCfg, "partial");
1932
- const blockStreamingResolved = teamsStreamMode === "block" ? true : (0, openclaw_plugin_sdk_channel_outbound.resolveChannelStreamingBlockEnabled)(msteamsCfg);
1933
- const blockStreamingEnabled = blockStreamingResolved ?? false;
1934
- const typingIndicatorEnabled = typeof msteamsCfg?.typingIndicator === "boolean" ? msteamsCfg.typingIndicator : true;
1935
- const pendingDeliveries = [];
1936
- let pendingSettlement;
1937
- const findPendingNativeDelivery = () => pendingDeliveries.find((candidate) => !candidate.nativeSettled);
1938
- const queueDeliveryFailureSystemEvent = (failure) => {
1939
- const classification = require_channel_setup.classifyMSTeamsSendError(failure.error);
1940
- const errorText = require_channel_setup.formatUnknownError(failure.error);
1941
- const failedAll = failure.failed >= failure.total;
1942
- const ambiguous = classification.kind === "ambiguous";
1943
- const sentences = [
1944
- `Microsoft Teams delivery failed: ${ambiguous ? failedAll ? "the delivery outcome is unknown for the previous reply" : `the delivery outcome is unknown for ${failure.failed} of ${failure.total} message blocks` : failedAll ? "the previous reply was not delivered" : `${failure.failed} of ${failure.total} message blocks were not delivered`}.`,
1945
- ambiguous ? void 0 : `The user may not have received ${failedAll ? "that reply" : "the full reply"}.`,
1946
- `Error: ${errorText}.`,
1947
- classification.statusCode != null ? `Status: ${classification.statusCode}.` : void 0,
1948
- require_channel_setup.formatMSTeamsDeliveryFailureGuidance(classification)
1949
- ].filter(Boolean);
1950
- core.system.enqueueSystemEvent(sentences.join(" "), {
1951
- sessionKey: params.sessionKey,
1952
- contextKey: `msteams:delivery-failure:${params.conversationRef.conversation?.id ?? "unknown"}`
1953
- });
1316
+ function resolveUnrepresentedHtmlAttachmentIds(attachments) {
1317
+ const representedIds = /* @__PURE__ */ new Set();
1318
+ for (const attachment of attachments) {
1319
+ if ((require_resolve_allowlist.normalizeContentType(attachment.contentType) ?? "").startsWith("text/html")) continue;
1320
+ const id = attachment.id?.trim();
1321
+ if (id) representedIds.add(id);
1322
+ }
1323
+ return extractMSTeamsHtmlAttachmentIds(attachments).filter((id) => !representedIds.has(id));
1324
+ }
1325
+ function createAdvertisedMediaFact(kind, sourceId) {
1326
+ return {
1327
+ kind,
1328
+ ...sourceId ? { sourceId } : {}
1954
1329
  };
1955
- const renderReplyPayload = (payload) => {
1956
- return require_conversation_store_state.renderReplyPayloadsToMessages([payload], {
1957
- textChunkLimit: params.textLimit,
1958
- tableMode,
1959
- chunkMode
1330
+ }
1331
+ function resolveMSTeamsAdvertisedMedia(attachments) {
1332
+ const list = attachments ?? [];
1333
+ const fileAttachments = list.filter(require_resolve_allowlist.isAdvertisedFileAttachment);
1334
+ const inlineMedia = require_resolve_allowlist.extractInlineImageReferences(list).map((candidate) => createAdvertisedMediaFact("image", candidate.sourceId));
1335
+ const htmlAttachmentIds = resolveUnrepresentedHtmlAttachmentIds(list);
1336
+ return [
1337
+ ...fileAttachments.map((attachment) => createAdvertisedMediaFact(require_resolve_allowlist.isLikelyImageAttachment(attachment) ? "image" : require_resolve_allowlist.resolveMSTeamsMediaKind({
1338
+ contentType: require_resolve_allowlist.normalizeContentType(attachment.contentType),
1339
+ fileName: attachment.name ?? void 0
1340
+ }), attachment.id?.trim() || void 0)),
1341
+ ...inlineMedia,
1342
+ ...htmlAttachmentIds.map((sourceId) => createAdvertisedMediaFact("document", sourceId))
1343
+ ];
1344
+ }
1345
+ // extensions/msteams/src/attachments/remote-media.ts
1346
+ /**
1347
+ * Direct save path used when the caller supplies the already-guarded fetch
1348
+ * implementation. This lets Teams-specific auth fallback own the request
1349
+ * sequence while keeping redirect and DNS pinning inside `safeFetchWithPolicy`.
1350
+ */
1351
+ async function downloadAndStoreMSTeamsRemoteMedia(params) {
1352
+ const response = await params.fetchImpl(params.url, { redirect: "follow" });
1353
+ try {
1354
+ const saved = await (0, openclaw_plugin_sdk_media_runtime.saveResponseMedia)(response, {
1355
+ sourceUrl: params.url,
1356
+ filePathHint: params.filePathHint,
1357
+ maxBytes: params.maxBytes,
1358
+ fallbackContentType: params.contentTypeHint
1960
1359
  });
1961
- };
1962
- const settlePendingDelivery = (delivery) => {
1963
- if (delivery.settled || !delivery.blockSettled || !delivery.nativeSettled) return;
1964
- delivery.settled = true;
1965
- const acceptedParts = [...delivery.nativeResult ? [delivery.nativeResult] : [], ...delivery.blockResults];
1966
- const messageIds = acceptedParts.flatMap((part) => part.messageIds);
1967
- const content = delivery.errors.length > 0 ? acceptedParts.map((part) => part.content).filter(Boolean).join("\n") : delivery.content;
1968
- const outcome = {
1969
- visibleReplySent: acceptedParts.length > 0,
1970
- ...messageIds.length > 0 ? { messageIds } : {},
1971
- ...acceptedParts.length > 0 && content !== void 0 ? { content } : {}
1360
+ return {
1361
+ path: saved.path,
1362
+ contentType: saved.contentType,
1363
+ kind: params.kind ?? require_resolve_allowlist.resolveMSTeamsMediaKind({
1364
+ contentType: saved.contentType,
1365
+ fileName: params.filePathHint
1366
+ })
1972
1367
  };
1973
- if (delivery.errors.length === 0) {
1974
- delivery.finalization.resolve(outcome);
1975
- return;
1976
- }
1977
- const error = delivery.errors.find((candidate) => !(candidate instanceof openclaw_plugin_sdk_error_runtime.PlatformMessageNotDispatchedError)) ?? delivery.errors[0];
1978
- delivery.finalization.reject(outcome.visibleReplySent ? (0, openclaw_plugin_sdk_channel_inbound.createChannelPartialDeliveryError)(error, {
1979
- ...outcome,
1980
- visibleReplySent: true
1981
- }) : error);
1982
- };
1983
- const flushPendingMessages = async () => {
1984
- for (const delivery of pendingDeliveries) {
1985
- if (delivery.blockSettled) continue;
1986
- const toSend = delivery.messages.splice(0);
1987
- const total = toSend.length;
1988
- let failed = 0;
1989
- let lastFailedError;
1990
- const sentIds = [];
1991
- for (const msg of toSend) try {
1992
- const msgIds = await require_conversation_store_state.sendMSTeamsMessages({
1993
- accountId: params.accountId,
1994
- replyStyle: params.replyStyle,
1995
- app: params.app,
1996
- conversationRef: params.conversationRef,
1997
- context: params.context,
1998
- messages: [msg],
1999
- onRetry: (event) => {
2000
- params.log.debug?.("retrying send", {
2001
- replyStyle: params.replyStyle,
2002
- ...event
2003
- });
2004
- },
2005
- tokenProvider: params.tokenProvider,
2006
- sharePointSiteId: params.sharePointSiteId,
2007
- mediaMaxBytes,
2008
- feedbackLoopEnabled,
2009
- serviceUrlBoundary: require_resolve_allowlist.resolveMSTeamsSdkCloudOptions(msteamsCfg)
2010
- });
2011
- const validIds = msgIds.filter((id) => id.trim() && id !== "unknown");
2012
- if (msgIds.length > 0) delivery.blockResults.push({
2013
- messageIds: validIds,
2014
- ...msg.text ? { content: msg.text } : {}
2015
- });
2016
- sentIds.push(...validIds);
2017
- } catch (msgError) {
2018
- failed += 1;
2019
- lastFailedError = msgError;
2020
- delivery.errors.push(msgError);
2021
- params.log.debug?.("individual message send failed, continuing with remaining blocks");
2022
- }
2023
- if (failed > 0) {
2024
- const classification = require_channel_setup.classifyMSTeamsSendError(lastFailedError);
2025
- const hint = require_channel_setup.formatMSTeamsSendErrorHint(classification);
2026
- params.log.warn?.(`failed to deliver ${failed} of ${total} message blocks`, {
2027
- failed,
2028
- total,
2029
- error: require_channel_setup.formatUnknownError(lastFailedError),
2030
- classification,
2031
- ...hint ? { hint } : {}
2032
- });
2033
- queueDeliveryFailureSystemEvent({
2034
- failed,
2035
- total,
2036
- error: lastFailedError
2037
- });
2038
- }
2039
- delivery.blockSettled = true;
2040
- settlePendingDelivery(delivery);
2041
- if (sentIds.length > 0) try {
2042
- params.onSentMessageIds?.(sentIds);
2043
- } catch (error) {
2044
- params.log.warn?.("failed to record sent Teams message ids", { error: require_channel_setup.formatUnknownError(error) });
2045
- }
2046
- }
2047
- };
2048
- const dispatcherOptions = {
2049
- ...replyPipeline,
2050
- humanDelay: (0, openclaw_plugin_sdk_agent_runtime.resolveHumanDelayConfig)(params.cfg, params.agentId),
2051
- onReplyStart: async () => {
2052
- if (typingIndicatorEnabled) await typingCallbacks?.onReplyStart?.();
2053
- },
2054
- typingCallbacks
2055
- };
2056
- const delivery = {
2057
- observeMessageSent: true,
2058
- deliver: async (payload) => {
2059
- if (pendingSettlement) await pendingSettlement;
2060
- const preparedPayload = streamController.preparePayload(payload);
2061
- const native = streamController.claimNativeDelivery();
2062
- if (preparedPayload && !native && findPendingNativeDelivery()) await settleDelivery();
2063
- const messages = preparedPayload && !streamController.wasCanceled() ? renderReplyPayload(preparedPayload) : [];
2064
- if (!native && messages.length === 0) return {
2065
- visibleReplySent: false,
2066
- suppression: { reason: "no_visible_result" }
2067
- };
2068
- const pending = {
2069
- messages,
2070
- finalization: (0, openclaw_plugin_sdk_extension_shared.createDeferred)(),
2071
- content: payload.text,
2072
- blockResults: [],
2073
- nativeSettled: !native,
2074
- blockSettled: messages.length === 0,
2075
- settled: false,
2076
- errors: []
2077
- };
2078
- pendingDeliveries.push(pending);
2079
- if (blockStreamingEnabled) await flushPendingMessages();
2080
- settlePendingDelivery(pending);
2081
- return {
2082
- visibleReplySent: false,
2083
- finalization: pending.finalization.promise
2084
- };
2085
- },
2086
- onError: (err, info) => {
2087
- const errMsg = require_channel_setup.formatUnknownError(err);
2088
- const classification = require_channel_setup.classifyMSTeamsSendError(err);
2089
- const hint = require_channel_setup.formatMSTeamsSendErrorHint(classification);
2090
- params.runtime.error?.(`msteams ${info.kind} reply failed: ${errMsg}${hint ? ` (${hint})` : ""}`);
2091
- params.log.error("reply failed", {
2092
- kind: info.kind,
2093
- error: errMsg,
2094
- classification,
2095
- hint
2096
- });
2097
- }
1368
+ } finally {
1369
+ await response.body?.cancel().catch(() => void 0);
1370
+ }
1371
+ }
1372
+ // extensions/msteams/src/attachments/download.ts
1373
+ function resolveDownloadCandidate(att, graphReferenceToken) {
1374
+ const contentType = require_resolve_allowlist.normalizeContentType(att.contentType);
1375
+ const graphReference = graphReferenceToken !== void 0 && contentType === "reference";
1376
+ const name = (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeOptionalString)(att.name) ?? "";
1377
+ if (contentType === "application/vnd.microsoft.teams.file.download.info") {
1378
+ if (!(0, openclaw_plugin_sdk_string_coerce_runtime.isRecord)(att.content)) return null;
1379
+ const downloadUrl = (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeOptionalString)(att.content.downloadUrl) ?? "";
1380
+ if (!downloadUrl) return null;
1381
+ const fileType = (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeOptionalString)(att.content.fileType) ?? "";
1382
+ const uniqueId = (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeOptionalString)(att.content.uniqueId) ?? "";
1383
+ const fileName = (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeOptionalString)(att.content.fileName) ?? "";
1384
+ const fileHint = name || fileName || (uniqueId && fileType ? `${uniqueId}.${fileType}` : "");
1385
+ return {
1386
+ kind: "remote",
1387
+ mediaKind: require_resolve_allowlist.resolveMSTeamsMediaKind({
1388
+ contentType,
1389
+ fileName: fileHint,
1390
+ fileType
1391
+ }),
1392
+ url: downloadUrl,
1393
+ fileHint: fileHint || void 0,
1394
+ contentTypeHint: void 0,
1395
+ sourceId: att.id?.trim() || void 0
1396
+ };
1397
+ }
1398
+ const contentUrl = graphReference ? att.contentUrl ?? "" : (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeOptionalString)(att.contentUrl) ?? "";
1399
+ if (!contentUrl) return null;
1400
+ const sharesUrl = graphReference ? `${require_resolve_allowlist.GRAPH_ROOT}/shares/${require_resolve_allowlist.encodeGraphShareId(contentUrl)}/driveItem/content` : require_resolve_allowlist.tryBuildGraphSharesUrlForSharedLink(contentUrl);
1401
+ const resolvedUrl = sharesUrl ?? contentUrl;
1402
+ const resolvedContentTypeHint = graphReference ? "application/octet-stream" : sharesUrl ? void 0 : contentType;
1403
+ return {
1404
+ kind: "remote",
1405
+ mediaKind: require_resolve_allowlist.resolveMSTeamsMediaKind({
1406
+ contentType,
1407
+ fileName: name
1408
+ }),
1409
+ url: resolvedUrl,
1410
+ fileHint: graphReference ? att.name ?? void 0 : name || void 0,
1411
+ contentTypeHint: resolvedContentTypeHint,
1412
+ sourceId: att.id?.trim() || void 0,
1413
+ graphReference
2098
1414
  };
2099
- const settleDelivery = () => pendingSettlement ??= Promise.resolve().then(async () => {
2100
- await flushPendingMessages();
2101
- const nativeDelivery = findPendingNativeDelivery();
2102
- if (!nativeDelivery) {
2103
- await streamController.finalize();
1415
+ }
1416
+ function scopeCandidatesForUrl(url) {
1417
+ const host = (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeLowercaseStringOrEmpty)(URL.parse(url)?.hostname);
1418
+ return host.endsWith("graph.microsoft.com") || host.endsWith("sharepoint.com") || host.endsWith("1drv.ms") || host.includes("sharepoint") ? ["https://graph.microsoft.com", "https://api.botframework.com"] : ["https://api.botframework.com", "https://graph.microsoft.com"];
1419
+ }
1420
+ function canonicalizeInlineBase64Payload(value) {
1421
+ let cleaned = "";
1422
+ for (const char of value) if (char.charCodeAt(0) > 32) cleaned += char;
1423
+ return cleaned.length % 4 === 0 && /^[A-Za-z0-9+/]+={0,2}$/.test(cleaned) ? cleaned : void 0;
1424
+ }
1425
+ async function resolveInlineDataImageMime(inline) {
1426
+ const detectedMime = await require_runtime.getMSTeamsRuntime().media.detectMime({
1427
+ buffer: inline.data,
1428
+ headerMime: inline.contentType
1429
+ });
1430
+ const mime = (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeOptionalLowercaseString)(detectedMime ?? inline.contentType);
1431
+ return mime?.startsWith("image/") ? mime : void 0;
1432
+ }
1433
+ async function downloadMSTeamsAttachments(params) {
1434
+ const list = params.attachments ?? [];
1435
+ if (list.length === 0) return [];
1436
+ const policy = require_resolve_allowlist.resolveAttachmentFetchPolicy({
1437
+ allowHosts: params.allowHosts,
1438
+ authAllowHosts: params.authAllowHosts
1439
+ });
1440
+ const allowHosts = policy.allowHosts;
1441
+ const candidates = list.filter(require_resolve_allowlist.isAdvertisedFileAttachment).map((attachment) => {
1442
+ return (require_resolve_allowlist.isDownloadableAttachment(attachment) ? resolveDownloadCandidate(attachment, params.graphReferenceToken) : null) ?? {
1443
+ kind: "unavailable",
1444
+ mediaKind: require_resolve_allowlist.resolveMSTeamsMediaKind({
1445
+ contentType: require_resolve_allowlist.normalizeContentType(attachment.contentType),
1446
+ fileName: attachment.name ?? void 0
1447
+ }),
1448
+ sourceId: attachment.id?.trim() || void 0
1449
+ };
1450
+ });
1451
+ const maxInlineBytes = params.maxBytes;
1452
+ candidates.push(...require_resolve_allowlist.extractInlineImageReferences(list).map((candidate) => {
1453
+ if (candidate.kind === "url") return {
1454
+ kind: "remote",
1455
+ mediaKind: "image",
1456
+ url: candidate.url,
1457
+ fileHint: candidate.fileHint,
1458
+ contentTypeHint: candidate.contentType,
1459
+ sourceId: candidate.sourceId
1460
+ };
1461
+ return Object.assign(candidate, { mediaKind: "image" });
1462
+ }));
1463
+ for (const sourceId of resolveUnrepresentedHtmlAttachmentIds(list)) candidates.push({
1464
+ kind: "unavailable",
1465
+ mediaKind: "document",
1466
+ sourceId
1467
+ });
1468
+ if (candidates.length === 0) return [];
1469
+ let totalInlineBytes = 0;
1470
+ const downloadCandidate = async (candidate) => {
1471
+ if (candidate.kind === "unavailable") return;
1472
+ if (candidate.kind === "data") {
1473
+ const match = /^data:(image\/[a-z0-9.+-]+)?(;base64)?,(.*)$/i.exec(candidate.src);
1474
+ if (!match) return;
1475
+ const declaredContentType = (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeLowercaseStringOrEmpty)(match[1] ?? "");
1476
+ if (!Boolean(match[2])) return;
1477
+ const canonicalPayload = canonicalizeInlineBase64Payload(match[3] ?? "");
1478
+ if (!canonicalPayload) return;
1479
+ const estimatedBytes = Buffer.byteLength(canonicalPayload, "base64");
1480
+ if (estimatedBytes <= 0 || estimatedBytes > maxInlineBytes || totalInlineBytes + estimatedBytes > maxInlineBytes) return;
1481
+ const decoded = {
1482
+ data: Buffer.from(canonicalPayload, "base64"),
1483
+ contentType: declaredContentType,
1484
+ estimatedBytes
1485
+ };
1486
+ totalInlineBytes += decoded.estimatedBytes;
1487
+ try {
1488
+ const contentType = await resolveInlineDataImageMime(decoded);
1489
+ if (!contentType) return;
1490
+ const saved = await require_runtime.getMSTeamsRuntime().channel.media.saveMediaBuffer(decoded.data, contentType, "inbound", params.maxBytes);
1491
+ return {
1492
+ path: saved.path,
1493
+ contentType: saved.contentType,
1494
+ kind: "image"
1495
+ };
1496
+ } catch (err) {
1497
+ params.logger?.warn?.("msteams inline attachment decode failed", { error: (0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err) });
1498
+ }
2104
1499
  return;
2105
1500
  }
2106
- let nativeResult;
1501
+ if (!(0, openclaw_plugin_sdk_ssrf_policy.isHttpsUrlAllowedByHostnameSuffixAllowlist)(candidate.url, allowHosts)) return;
2107
1502
  try {
2108
- nativeResult = await streamController.finalize();
2109
- } catch (error) {
2110
- nativeDelivery.errors.push(error);
2111
- nativeDelivery.nativeSettled = true;
2112
- settlePendingDelivery(nativeDelivery);
2113
- return;
2114
- }
2115
- if (nativeResult.visibleReplySent) nativeDelivery.nativeResult = {
2116
- messageIds: nativeResult.messageId ? [nativeResult.messageId] : [],
2117
- ...nativeResult.content !== void 0 ? { content: nativeResult.content } : {}
2118
- };
2119
- const hasPostNativePayloads = Boolean(nativeResult.postNativePayloads?.length);
2120
- if (nativeResult.logicalContent !== void 0) nativeDelivery.content = nativeResult.logicalContent;
2121
- else if (nativeResult.content !== void 0 && (!nativeResult.fallbackPayload && !hasPostNativePayloads || nativeDelivery.content === void 0)) nativeDelivery.content = nativeResult.content;
2122
- const afterNativePayloads = [...nativeResult.fallbackPayload ? [nativeResult.fallbackPayload] : [], ...nativeResult.postNativePayloads ?? []];
2123
- if (afterNativePayloads.length > 0) {
2124
- nativeDelivery.messages.push(...afterNativePayloads.flatMap((payload) => renderReplyPayload(payload)));
2125
- nativeDelivery.blockSettled = nativeDelivery.messages.length === 0;
2126
- }
2127
- nativeDelivery.nativeSettled = true;
2128
- if (!nativeDelivery.blockSettled) await flushPendingMessages();
2129
- settlePendingDelivery(nativeDelivery);
2130
- if (nativeResult.messageId) try {
2131
- params.onSentMessageIds?.([nativeResult.messageId]);
2132
- } catch (error) {
2133
- params.log.warn?.("failed to record sent Teams message id", { error: require_channel_setup.formatUnknownError(error) });
2134
- }
2135
- }).finally(() => {
2136
- pendingSettlement = void 0;
2137
- });
2138
- const shouldSuppressDefaultToolProgressMessages = streamController.hasStream() && teamsStreamMode === "progress";
2139
- const progressCallbacks = streamController.hasStream() ? {
2140
- onReasoningStream: async (payload) => {
2141
- await streamController.pushReasoningProgress(payload.text, { snapshot: payload.isReasoningSnapshot === true });
2142
- return false;
2143
- },
2144
- onReasoningEnd: () => {
2145
- streamController.resetReasoningProgress();
2146
- return false;
2147
- },
2148
- onToolStart: streamController.pushToolEvent,
2149
- onItemEvent: streamController.pushItemEvent,
2150
- onPlanUpdate: async (payload) => {
2151
- if (payload.phase !== "update") return false;
2152
- await streamController.pushPlanProgress(payload.steps, payload);
2153
- return false;
2154
- },
2155
- onApprovalEvent: async (payload) => {
2156
- await streamController.pushApprovalEvent(payload);
2157
- return false;
1503
+ return await downloadAndStoreMSTeamsRemoteMedia({
1504
+ url: candidate.url,
1505
+ filePathHint: candidate.fileHint ?? candidate.url,
1506
+ maxBytes: params.maxBytes,
1507
+ contentTypeHint: candidate.contentTypeHint,
1508
+ kind: candidate.graphReference ? void 0 : candidate.mediaKind,
1509
+ fetchImpl: async (input, init) => {
1510
+ const url = (0, openclaw_plugin_sdk_request_url.resolveRequestUrl)(input);
1511
+ const providedTokenProvider = params.tokenProvider;
1512
+ const deadline = params.deadline;
1513
+ const bearerToken = candidate.graphReference ? params.graphReferenceToken : void 0;
1514
+ const fetchAttempt = (requestInit) => require_resolve_allowlist.safeFetchWithPolicy({
1515
+ url,
1516
+ policy,
1517
+ resolveFn: node_dns_promises.lookup,
1518
+ requestInit,
1519
+ timeoutMs: require_resolve_allowlist.resolveMSTeamsRequestTimeoutMs(deadline)
1520
+ });
1521
+ if (bearerToken !== void 0) {
1522
+ const headers = require_resolve_allowlist.ensureUserAgentHeader(init?.headers);
1523
+ require_resolve_allowlist.applyAuthorizationHeaderForUrl({
1524
+ headers,
1525
+ url,
1526
+ authAllowHosts: policy.authAllowHosts,
1527
+ bearerToken
1528
+ });
1529
+ return fetchAttempt({
1530
+ ...init,
1531
+ headers
1532
+ });
1533
+ }
1534
+ const firstAttempt = await fetchAttempt(init);
1535
+ if (firstAttempt.ok) return firstAttempt;
1536
+ if (!providedTokenProvider) return firstAttempt;
1537
+ const tokenProvider = providedTokenProvider;
1538
+ if (firstAttempt.status !== 401 && firstAttempt.status !== 403) return firstAttempt;
1539
+ if (!(0, openclaw_plugin_sdk_ssrf_policy.isHttpsUrlAllowedByHostnameSuffixAllowlist)(url, policy.authAllowHosts)) return firstAttempt;
1540
+ let fallbackAttempt = firstAttempt;
1541
+ const scopes = scopeCandidatesForUrl(url);
1542
+ for (const scope of scopes) try {
1543
+ const token = await require_resolve_allowlist.withMSTeamsRequestDeadline({
1544
+ deadline,
1545
+ label: "MS Teams attachment token",
1546
+ work: () => tokenProvider.getAccessToken(scope)
1547
+ });
1548
+ const authHeaders = new Headers(init?.headers);
1549
+ authHeaders.set("Authorization", `Bearer ${token}`);
1550
+ const authAttempt = await fetchAttempt({
1551
+ ...init,
1552
+ headers: authHeaders
1553
+ });
1554
+ await fallbackAttempt.body?.cancel().catch(() => void 0);
1555
+ if (authAttempt.ok || require_resolve_allowlist.isRedirectStatus(authAttempt.status)) return authAttempt;
1556
+ fallbackAttempt = authAttempt;
1557
+ } catch {}
1558
+ return fallbackAttempt;
1559
+ }
1560
+ });
1561
+ } catch (err) {
1562
+ if (candidate.graphReference) {
1563
+ params.logger?.warn?.("msteams SharePoint reference download failed", {
1564
+ error: (0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err),
1565
+ name: candidate.fileHint
1566
+ });
1567
+ return;
1568
+ }
1569
+ const msg = (0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err);
1570
+ params.logger?.warn?.(`msteams attachment download failed host=${safeHostForLog(candidate.url)} error=${msg}`);
2158
1571
  }
2159
- } : {};
1572
+ };
1573
+ const out = [];
1574
+ for (const candidate of candidates) {
1575
+ const media = await downloadCandidate(candidate) ?? { kind: candidate.mediaKind };
1576
+ out.push(candidate.sourceId ? {
1577
+ ...media,
1578
+ sourceId: candidate.sourceId
1579
+ } : media);
1580
+ }
1581
+ return out;
1582
+ }
1583
+ function safeHostForLog(url) {
1584
+ return URL.parse(url)?.host ?? "invalid-url";
1585
+ }
1586
+ // extensions/msteams/src/attachments/graph.ts
1587
+ function createGraphHostedContentFact(item) {
1588
+ const contentType = require_resolve_allowlist.normalizeContentType(item.contentType);
2160
1589
  return {
2161
- dispatcherOptions: {
2162
- ...dispatcherOptions,
2163
- onSettled: settleDelivery
2164
- },
2165
- delivery,
2166
- replyOptions: {
2167
- progressPreambleEnabled: shouldSuppressDefaultToolProgressMessages,
2168
- commentaryProgressEnabled: shouldSuppressDefaultToolProgressMessages,
2169
- ...streamController.hasStream() ? { onPartialReply: (payload) => {
2170
- streamController.onPartialReply(payload);
2171
- return false;
2172
- } } : {},
2173
- ...progressCallbacks,
2174
- ...shouldSuppressDefaultToolProgressMessages ? { suppressDefaultToolProgressMessages: true } : {},
2175
- disableBlockStreaming: blockStreamingResolved == null ? void 0 : !blockStreamingResolved,
2176
- onModelSelected
2177
- }
1590
+ kind: require_resolve_allowlist.resolveMSTeamsMediaKind({ contentType }),
1591
+ ...contentType ? { contentType } : {},
1592
+ ...item.id ? { sourceId: item.id } : {}
2178
1593
  };
2179
1594
  }
2180
- // extensions/msteams/src/monitor-handler/inbound-dispatch.ts
2181
- async function dispatchMSTeamsInboundTurn(params) {
2182
- const core = require_runtime.getMSTeamsRuntime();
2183
- const { cfg, runtime, app, tokenProvider, textLimit, log, logVerboseMessage, facts, admission, content, routing, thread, replyStyle, timestamp, contextVisibilityMode, conversationHistories, historyLimit } = params;
2184
- const { context, activity, rawBody, text, quoteInfo, conversationRef } = facts;
2185
- const { senderId, senderName, isDirectMessage, allowNameMatching, groupPolicy, commandAuthorized, effectiveGroupAllowFrom, msteamsCfg } = admission;
2186
- const { route } = routing;
2187
- const { agentBody, inboundMedia } = content;
2188
- const { teamAadGroupId, quoteBodyFull, quoteSenderId, quoteSenderName, threadContext } = thread;
2189
- const { conversationId, conversationType, isChannel, teamId, graphChannelId } = facts;
2190
- const teamsFrom = isDirectMessage ? `msteams:${senderId}` : isChannel ? `msteams:channel:${conversationId}` : `msteams:group:${conversationId}`;
2191
- const teamsTo = isDirectMessage ? `user:${senderId}` : `conversation:${conversationId}`;
2192
- const envelopeFrom = isDirectMessage ? senderName : conversationType;
2193
- const buildEnvelope = await (0, openclaw_plugin_sdk_channel_inbound.createChannelInboundEnvelopeBuilderAsync)({
2194
- cfg,
2195
- route
2196
- });
2197
- let combinedBody = buildEnvelope({
2198
- channel: "Teams",
2199
- from: envelopeFrom,
2200
- timestamp,
2201
- body: agentBody
2202
- });
2203
- const isRoomish = !isDirectMessage;
2204
- const historyKey = isRoomish ? facts.historyKey : void 0;
2205
- if (isRoomish && historyKey) combinedBody = (0, openclaw_plugin_sdk_reply_history.createChannelHistoryWindow)({ historyMap: conversationHistories }).buildPendingContext({
2206
- historyKey,
2207
- limit: historyLimit,
2208
- currentMessage: combinedBody,
2209
- formatEntry: (entry) => buildEnvelope({
2210
- channel: "Teams",
2211
- from: conversationType,
2212
- timestamp: entry.timestamp,
2213
- previousTimestamp: null,
2214
- body: `${entry.sender}: ${entry.body}${entry.messageId ? ` [id:${entry.messageId}]` : ""}`
2215
- })
1595
+ function buildMSTeamsGraphMessageUrl(params) {
1596
+ const conversationType = (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeLowercaseStringOrEmpty)(params.conversationType ?? "");
1597
+ const messageId = (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeOptionalString)(params.messageId);
1598
+ if (!messageId) return;
1599
+ if (conversationType === "channel") {
1600
+ const teamAadGroupId = (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeOptionalString)(params.teamAadGroupId);
1601
+ const channelId = (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeOptionalString)(params.channelId);
1602
+ if (!teamAadGroupId || !channelId) return;
1603
+ const messageRoot = `${require_resolve_allowlist.GRAPH_ROOT}/teams/${encodeURIComponent(teamAadGroupId)}/channels/${encodeURIComponent(channelId)}/messages`;
1604
+ const threadRootMessageId = (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeOptionalString)(params.threadRootMessageId);
1605
+ return threadRootMessageId && threadRootMessageId !== messageId ? `${messageRoot}/${encodeURIComponent(threadRootMessageId)}/replies/${encodeURIComponent(messageId)}` : `${messageRoot}/${encodeURIComponent(messageId)}`;
1606
+ }
1607
+ const chatId = (0, openclaw_plugin_sdk_string_coerce_runtime.normalizeOptionalString)(params.conversationId);
1608
+ if (!chatId) return;
1609
+ return `${require_resolve_allowlist.GRAPH_ROOT}/chats/${encodeURIComponent(chatId)}/messages/${encodeURIComponent(messageId)}`;
1610
+ }
1611
+ async function withGraphMediaResponse(params, url, kind, read) {
1612
+ const { response, release } = await (0, openclaw_plugin_sdk_ssrf_runtime.fetchWithSsrFGuard)({
1613
+ url,
1614
+ init: { headers: require_resolve_allowlist.ensureUserAgentHeader({ Authorization: `Bearer ${params.accessToken}` }) },
1615
+ policy: params.ssrfPolicy,
1616
+ auditContext: `msteams.graph.${kind}`,
1617
+ timeoutMs: require_resolve_allowlist.resolveMSTeamsRequestTimeoutMs(params.deadline)
2216
1618
  });
2217
- const inboundHistory = isRoomish && historyKey && historyLimit > 0 ? (0, openclaw_plugin_sdk_reply_history.createChannelHistoryWindow)({ historyMap: conversationHistories }).buildInboundHistory({
2218
- historyKey,
2219
- limit: historyLimit
2220
- }) : void 0;
2221
- const commandBody = text.trim();
2222
- const quoteSenderAllowed = quoteInfo && quoteInfo.sender ? (0, openclaw_plugin_sdk_channel_inbound.resolveInboundSupplementalSenderAllowed)({
2223
- isGroup: !isDirectMessage,
2224
- groupPolicy,
2225
- allowFrom: effectiveGroupAllowFrom,
2226
- isSenderAllowed: (allowFrom) => (0, openclaw_plugin_sdk_allow_from.resolveAllowlistMatchSimple)({
2227
- allowFrom,
2228
- senderId: quoteSenderId ?? "",
2229
- senderName: quoteSenderName,
2230
- allowNameMatching
2231
- }).allowed
2232
- }) : true;
2233
- const nativeChannelId = isChannel && teamAadGroupId ? `${teamAadGroupId}/${graphChannelId}` : void 0;
2234
- const boundIngress = await resolveMSTeamsSenderAccess({
2235
- cfg: route.accountId === params.listenerAccountId ? params.readConfig() : params.accountPolicyCfg,
2236
- accountId: route.accountId,
2237
- activity,
2238
- hasControlCommand: admission.isControlCommand,
2239
- conversationThreadId: facts.threadId,
2240
- contextBinding: {
2241
- agentId: route.agentId,
2242
- sessionKey: route.sessionKey,
2243
- ...activity.id ? { messageId: activity.id } : {},
2244
- ...nativeChannelId ? { nativeChannelId } : {},
2245
- inboundEventKind: "user_request"
2246
- }
1619
+ try {
1620
+ return await read(response);
1621
+ } finally {
1622
+ if (!response.bodyUsed) response.body?.cancel().catch(() => void 0);
1623
+ await release();
1624
+ }
1625
+ }
1626
+ function normalizeGraphAttachment(att) {
1627
+ let content = att.content;
1628
+ if (typeof content === "string") try {
1629
+ content = JSON.parse(content);
1630
+ } catch {}
1631
+ return {
1632
+ id: att.id ?? void 0,
1633
+ contentType: require_resolve_allowlist.normalizeContentType(att.contentType),
1634
+ contentUrl: att.contentUrl ?? void 0,
1635
+ name: att.name ?? void 0,
1636
+ content
1637
+ };
1638
+ }
1639
+ async function downloadMSTeamsGraphMedia(params) {
1640
+ if (!params.messageUrl || !params.tokenProvider) return { media: [] };
1641
+ const tokenProvider = params.tokenProvider;
1642
+ const policy = require_resolve_allowlist.resolveAttachmentFetchPolicy({
1643
+ allowHosts: params.allowHosts,
1644
+ authAllowHosts: params.authAllowHosts
2247
1645
  });
2248
- if (!boundIngress.senderAccess.allowed || boundIngress.hasConflictingConversationScope || boundIngress.msteamsCfg?.enabled === false || boundIngress.commandAccess.shouldBlockControlCommand) {
2249
- log.info("dropping message (dispatch-time account authorization changed)");
1646
+ const ssrfPolicy = (0, openclaw_plugin_sdk_ssrf_policy.buildHostnameAllowlistPolicyFromSuffixAllowlist)(policy.allowHosts);
1647
+ const messageUrl = params.messageUrl;
1648
+ const logGraphFailure = (phase, err) => {
1649
+ params.logger?.debug?.(`graph media ${phase} failed`, {
1650
+ messageUrl,
1651
+ error: (0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err)
1652
+ });
1653
+ params.logger?.warn?.(`msteams graph ${phase} failed`, {
1654
+ error: (0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err),
1655
+ ...phase === "message parse" ? { messageUrl } : {}
1656
+ });
1657
+ };
1658
+ let accessToken;
1659
+ try {
1660
+ accessToken = await require_resolve_allowlist.withMSTeamsRequestDeadline({
1661
+ deadline: params.deadline,
1662
+ label: "MS Teams Graph media token",
1663
+ work: () => tokenProvider.getAccessToken("https://graph.microsoft.com")
1664
+ });
1665
+ } catch (err) {
1666
+ logGraphFailure("token acquisition", err);
2250
1667
  return {
2251
- kind: "completed",
2252
- finalResponses: 0
1668
+ media: [],
1669
+ tokenError: true
2253
1670
  };
2254
1671
  }
2255
- const ctxPayload = core.channel.inbound.buildContext({
2256
- channelIngress: boundIngress.channelIngress,
2257
- channel: "msteams",
2258
- contextVisibility: contextVisibilityMode,
2259
- supplemental: {
2260
- channelStructuredContext: threadContext.length ? [{
2261
- label: "Thread history",
2262
- source: "msteams",
2263
- type: "chat_window",
2264
- sessionTranscriptMode: "preserve",
2265
- payload: {
2266
- order: "chronological",
2267
- messages: threadContext
1672
+ let messageAttachments = [];
1673
+ let referenceAttachments = [];
1674
+ let messageStatus;
1675
+ try {
1676
+ await withGraphMediaResponse({
1677
+ ...params,
1678
+ accessToken,
1679
+ ssrfPolicy
1680
+ }, messageUrl, "message", async (msgRes) => {
1681
+ messageStatus = msgRes.status;
1682
+ if (msgRes.ok) {
1683
+ let msgData;
1684
+ try {
1685
+ msgData = await (0, openclaw_plugin_sdk_provider_http.readProviderJsonResponse)(msgRes, "MS Teams Graph message");
1686
+ } catch (err) {
1687
+ logGraphFailure("message parse", err);
1688
+ msgData = {};
2268
1689
  }
2269
- }] : void 0,
2270
- quote: quoteInfo ? {
2271
- id: quoteInfo.id ?? activity.replyToId ?? void 0,
2272
- body: quoteBodyFull ?? quoteInfo.body,
2273
- sender: quoteInfo.sender,
2274
- senderAllowed: quoteSenderAllowed,
2275
- isQuote: true
2276
- } : void 0
2277
- },
2278
- media: await (0, openclaw_plugin_sdk_channel_inbound.toInboundMediaFactsWithMetadata)(inboundMedia),
2279
- messageId: activity.id,
2280
- timestamp: timestamp?.getTime() ?? Date.now(),
2281
- from: teamsFrom,
2282
- sender: {
2283
- id: senderId,
2284
- name: senderName
2285
- },
2286
- conversation: {
2287
- kind: isDirectMessage ? "direct" : isChannel ? "channel" : "group",
2288
- id: conversationId,
2289
- label: envelopeFrom,
2290
- spaceId: teamId,
2291
- nativeChannelId
2292
- },
2293
- route: {
2294
- agentId: route.agentId,
2295
- dmScope: route.dmScope,
2296
- accountId: route.accountId,
2297
- routeSessionKey: route.sessionKey
2298
- },
2299
- reply: {
2300
- to: teamsTo,
2301
- originatingTo: `conversation:${conversationId}`,
2302
- messageThreadId: facts.threadId ?? void 0,
2303
- replyToId: activity.replyToId ?? void 0,
2304
- nativeChannelId
2305
- },
2306
- message: {
2307
- body: combinedBody,
2308
- bodyForAgent: agentBody,
2309
- inboundHistory,
2310
- rawBody,
2311
- commandBody
2312
- },
2313
- sessionTranscript: { historyLimit: isRoomish ? historyLimit : 0 },
2314
- access: {
2315
- mentions: {
2316
- canDetectMention: !isDirectMessage,
2317
- wasMentioned: isDirectMessage || params.mentionWasEffective
2318
- },
2319
- commands: { authorized: commandAuthorized === true }
2320
- },
2321
- extra: {
2322
- GroupSubject: !isDirectMessage ? conversationType : void 0,
2323
- ReplyToIsQuote: quoteInfo ? true : void 0
2324
- }
2325
- });
2326
- const preview = (0, openclaw_plugin_sdk_text_utility_runtime.sliceUtf16Safe)(rawBody.replace(/\s+/g, " "), 0, 160);
2327
- logVerboseMessage(`msteams inbound: from=${ctxPayload.From} preview="${preview}"`);
2328
- const { dispatcherOptions, delivery, replyOptions } = createMSTeamsReplyDispatcher({
2329
- cfg,
2330
- agentId: route.agentId,
2331
- sessionKey: route.sessionKey,
2332
- accountId: route.accountId,
2333
- runtime,
2334
- log,
2335
- app,
2336
- conversationRef,
2337
- context,
2338
- replyStyle,
2339
- textLimit,
2340
- onSentMessageIds: (ids) => {
2341
- for (const id of ids) require_conversation_store_state.recordMSTeamsSentMessage(conversationId, id, { accountId: route.accountId });
2342
- },
2343
- tokenProvider,
2344
- sharePointSiteId: msteamsCfg?.sharePointSiteId
1690
+ messageAttachments = Array.isArray(msgData.attachments) ? msgData.attachments : [];
1691
+ referenceAttachments = messageAttachments.filter((a) => a.contentType === "reference" && a.contentUrl && a.name);
1692
+ } else params.logger?.debug?.("graph media message fetch not ok", {
1693
+ messageUrl,
1694
+ status: messageStatus
1695
+ });
1696
+ });
1697
+ } catch (err) {
1698
+ logGraphFailure("message fetch", err);
1699
+ }
1700
+ const sharePointMedia = await downloadMSTeamsAttachments({
1701
+ ...params,
1702
+ attachments: referenceAttachments,
1703
+ graphReferenceToken: accessToken,
1704
+ allowHosts: policy.allowHosts,
1705
+ authAllowHosts: policy.authAllowHosts
2345
1706
  });
2346
- const senderTimezone = (activity.entities?.find((entity) => entity.type === "clientInfo"))?.timezone || conversationRef.timezone;
2347
- const turnConfig = senderTimezone && !cfg.agents?.defaults?.userTimezone ? {
2348
- ...cfg,
2349
- agents: {
2350
- ...cfg.agents,
2351
- defaults: {
2352
- ...cfg.agents?.defaults,
2353
- userTimezone: senderTimezone
1707
+ const { maxBytes, logger, deadline } = params;
1708
+ const hostedRequest = {
1709
+ accessToken,
1710
+ ssrfPolicy,
1711
+ deadline
1712
+ };
1713
+ let hosted = { items: [] };
1714
+ try {
1715
+ hosted = await withGraphMediaResponse(hostedRequest, `${messageUrl}/hostedContents`, "collection", async (response) => {
1716
+ const status = response.status;
1717
+ if (!response.ok) return {
1718
+ status,
1719
+ items: []
1720
+ };
1721
+ try {
1722
+ return {
1723
+ status,
1724
+ items: await (0, openclaw_plugin_sdk_provider_http.readProviderJsonArrayFieldResponse)(response, "MS Teams Graph collection", "value")
1725
+ };
1726
+ } catch {
1727
+ return {
1728
+ status,
1729
+ items: []
1730
+ };
2354
1731
  }
1732
+ });
1733
+ } catch (err) {
1734
+ logger?.warn?.("msteams graph hostedContents fetch failed", { error: (0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err) });
1735
+ }
1736
+ const hostedMedia = [];
1737
+ for (const item of hosted.items) {
1738
+ if (!item.id) {
1739
+ hostedMedia.push(createGraphHostedContentFact(item));
1740
+ continue;
1741
+ }
1742
+ try {
1743
+ const sourceId = item.id;
1744
+ const valueUrl = `${messageUrl}/hostedContents/${encodeURIComponent(sourceId)}/$value`;
1745
+ await withGraphMediaResponse(hostedRequest, valueUrl, "hostedContent.value", async (valRes) => {
1746
+ if (!valRes.ok) {
1747
+ hostedMedia.push(createGraphHostedContentFact(item));
1748
+ return;
1749
+ }
1750
+ const saved = await require_runtime.getMSTeamsRuntime().channel.media.saveResponseMedia(valRes, {
1751
+ sourceUrl: valueUrl,
1752
+ maxBytes,
1753
+ fallbackContentType: item.contentType ?? void 0,
1754
+ subdir: "inbound"
1755
+ });
1756
+ hostedMedia.push({
1757
+ path: saved.path,
1758
+ contentType: saved.contentType,
1759
+ kind: require_resolve_allowlist.resolveMSTeamsMediaKind({ contentType: saved.contentType }),
1760
+ sourceId
1761
+ });
1762
+ });
1763
+ } catch (err) {
1764
+ hostedMedia.push(createGraphHostedContentFact(item));
1765
+ logger?.warn?.("msteams graph hostedContent value fetch failed", { error: (0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err) });
1766
+ continue;
2355
1767
  }
2356
- } : cfg;
2357
- log.info("dispatching to agent", { sessionKey: route.sessionKey });
1768
+ }
1769
+ const normalizedAttachments = messageAttachments.map(normalizeGraphAttachment);
1770
+ const referenceUrls = new Set(referenceAttachments.map((att) => att.contentUrl));
1771
+ const filteredAttachments = normalizedAttachments.filter((att) => att.contentType !== "reference" || !referenceUrls.has(att.contentUrl));
1772
+ let attachmentMedia = [];
2358
1773
  try {
2359
- const turnResult = await core.channel.inbound.run({
2360
- channel: "msteams",
2361
- accountId: route.accountId,
2362
- raw: context,
2363
- adapter: {
2364
- ingest: () => ({
2365
- id: activity.id ?? `${teamsFrom}:${Date.now()}`,
2366
- timestamp: timestamp?.getTime(),
2367
- rawText: rawBody,
2368
- textForAgent: agentBody,
2369
- textForCommands: commandBody,
2370
- raw: activity
2371
- }),
2372
- resolveTurn: () => ({
2373
- cfg: turnConfig,
2374
- channel: "msteams",
2375
- accountId: route.accountId,
2376
- route: {
2377
- agentId: route.agentId,
2378
- sessionKey: route.sessionKey
2379
- },
2380
- ctxPayload,
2381
- record: { onRecordError: (err) => {
2382
- logVerboseMessage(`msteams: failed updating session meta: ${require_channel_setup.formatUnknownError(err)}`);
2383
- } },
2384
- history: {
2385
- isGroup: isRoomish,
2386
- historyKey,
2387
- historyMap: conversationHistories,
2388
- limit: historyLimit
2389
- },
2390
- dispatcherOptions,
2391
- delivery,
2392
- replyOptions: {
2393
- ...replyOptions,
2394
- ...facts.turnAdoptionLifecycle ? (0, openclaw_plugin_sdk_channel_outbound.bindIngressLifecycleToReplyOptions)(facts.turnAdoptionLifecycle) : {}
1774
+ attachmentMedia = await downloadMSTeamsAttachments({
1775
+ attachments: filteredAttachments,
1776
+ maxBytes: params.maxBytes,
1777
+ tokenProvider: params.tokenProvider,
1778
+ allowHosts: policy.allowHosts,
1779
+ authAllowHosts: policy.authAllowHosts,
1780
+ deadline: params.deadline,
1781
+ logger: params.logger
1782
+ });
1783
+ } catch (err) {
1784
+ params.logger?.warn?.("msteams graph attachment download failed", {
1785
+ error: (0, openclaw_plugin_sdk_error_runtime.coerceErrorMessage)(err),
1786
+ messageUrl
1787
+ });
1788
+ }
1789
+ return {
1790
+ media: [
1791
+ ...sharePointMedia,
1792
+ ...hostedMedia,
1793
+ ...attachmentMedia
1794
+ ],
1795
+ hostedCount: hosted.items.length,
1796
+ attachmentCount: filteredAttachments.length + sharePointMedia.length,
1797
+ hostedStatus: hosted.status,
1798
+ attachmentStatus: messageStatus
1799
+ };
1800
+ }
1801
+ // extensions/msteams/src/monitor-handler/inbound-content.ts
1802
+ async function prepareMSTeamsInboundContent(params) {
1803
+ const activity = params.entry.context.activity;
1804
+ const mayRecoverGraphMedia = Boolean(params.htmlSummary?.attachmentIds.length) || shouldAttemptMSTeamsGraphMediaFallback({
1805
+ conversationType: params.conversationType,
1806
+ htmlSummary: params.htmlSummary,
1807
+ graphMediaFallback: params.graphMediaFallback
1808
+ });
1809
+ if (!params.rawBody && params.advertisedMedia.length === 0 && !mayRecoverGraphMedia) {
1810
+ params.log.debug?.("skipping empty message after stripping mentions");
1811
+ return null;
1812
+ }
1813
+ let resolvedMedia = [];
1814
+ const deadline = require_resolve_allowlist.createMSTeamsInboundMediaDeadline();
1815
+ try {
1816
+ resolvedMedia = await require_resolve_allowlist.withMSTeamsRequestDeadline({
1817
+ deadline,
1818
+ label: "MS Teams inbound media",
1819
+ work: async () => {
1820
+ let mediaList = await downloadMSTeamsAttachments({
1821
+ attachments: params.entry.attachments,
1822
+ maxBytes: params.mediaMaxBytes,
1823
+ tokenProvider: params.tokenProvider,
1824
+ allowHosts: params.mediaAllowHosts,
1825
+ authAllowHosts: params.mediaAuthAllowHosts,
1826
+ deadline,
1827
+ logger: params.log
1828
+ });
1829
+ if (!hasMaterializedMedia(mediaList)) {
1830
+ const attachmentIds = extractMSTeamsHtmlAttachmentIds(params.entry.attachments);
1831
+ const hasHtmlFileAttachment = attachmentIds.length > 0;
1832
+ const shouldFetchGraphMessage = hasHtmlFileAttachment || shouldAttemptMSTeamsGraphMediaFallback(params);
1833
+ const isBotFrameworkPersonalChat = isBotFrameworkPersonalChatId(params.conversationId);
1834
+ if (hasHtmlFileAttachment && isBotFrameworkPersonalChat) {
1835
+ if (!activity.serviceUrl) params.log.debug?.("bot framework attachment skipped (missing serviceUrl)", {
1836
+ conversationType: params.conversationType,
1837
+ conversationId: params.conversationId
1838
+ });
1839
+ else {
1840
+ const bfMedia = await downloadMSTeamsBotFrameworkAttachments({
1841
+ serviceUrl: activity.serviceUrl,
1842
+ attachmentIds,
1843
+ tokenProvider: params.tokenProvider,
1844
+ maxBytes: params.mediaMaxBytes,
1845
+ allowHosts: params.mediaAllowHosts,
1846
+ authAllowHosts: params.mediaAuthAllowHosts,
1847
+ deadline
1848
+ });
1849
+ mediaList = mergeMSTeamsMediaFacts(mediaList, bfMedia.media, { positionallyAligned: false });
1850
+ if (!hasMaterializedMedia(bfMedia.media)) params.log.debug?.("bot framework attachments fetch empty", {
1851
+ conversationType: params.conversationType,
1852
+ attachmentCount: bfMedia.attachmentCount ?? attachmentIds.length
1853
+ });
1854
+ }
1855
+ }
1856
+ if (shouldFetchGraphMessage && !isBotFrameworkPersonalChat) {
1857
+ const graphTeamAadGroupId = params.conversationType.trim().toLowerCase() === "channel" && !params.teamAadGroupId ? await params.resolveTeamAadGroupId(deadline) : params.teamAadGroupId;
1858
+ const messageUrl = buildMSTeamsGraphMessageUrl({
1859
+ conversationType: params.conversationType,
1860
+ conversationId: params.conversationId,
1861
+ messageId: activity.id,
1862
+ threadRootMessageId: params.conversationMessageId ?? activity.replyToId,
1863
+ teamAadGroupId: graphTeamAadGroupId,
1864
+ channelId: activity.channelData?.channel?.id
1865
+ });
1866
+ if (!messageUrl) params.log.debug?.("graph message url unavailable", {
1867
+ conversationType: params.conversationType,
1868
+ hasChannelData: Boolean(activity.channelData),
1869
+ messageId: activity.id,
1870
+ replyToId: activity.replyToId
1871
+ });
1872
+ else {
1873
+ const graphMedia = await downloadMSTeamsGraphMedia({
1874
+ messageUrl,
1875
+ tokenProvider: params.tokenProvider,
1876
+ maxBytes: params.mediaMaxBytes,
1877
+ allowHosts: params.mediaAllowHosts,
1878
+ authAllowHosts: params.mediaAuthAllowHosts,
1879
+ deadline,
1880
+ logger: params.log
1881
+ });
1882
+ mediaList = mergeMSTeamsMediaFacts(mediaList, graphMedia.media, { positionallyAligned: false });
1883
+ if (!hasMaterializedMedia(mediaList)) params.log.debug?.("graph media fetch empty", {
1884
+ messageUrl,
1885
+ hostedStatus: graphMedia.hostedStatus,
1886
+ attachmentStatus: graphMedia.attachmentStatus,
1887
+ hostedCount: graphMedia.hostedCount,
1888
+ attachmentCount: graphMedia.attachmentCount,
1889
+ tokenError: graphMedia.tokenError,
1890
+ attachmentIdCount: attachmentIds.length
1891
+ });
1892
+ }
2395
1893
  }
2396
- })
1894
+ }
1895
+ const materializedCount = mediaList.filter((media) => Boolean(media.path)).length;
1896
+ if (materializedCount > 0) params.log.debug?.("downloaded attachments", { count: materializedCount });
1897
+ else if (params.htmlSummary?.imgTags) params.log.debug?.("inline images detected but none downloaded", {
1898
+ imgTags: params.htmlSummary.imgTags,
1899
+ srcHosts: params.htmlSummary.srcHosts,
1900
+ dataImages: params.htmlSummary.dataImages,
1901
+ cidImages: params.htmlSummary.cidImages
1902
+ });
1903
+ return mediaList;
2397
1904
  }
2398
1905
  });
2399
- const dispatchResult = turnResult.dispatched ? turnResult.dispatchResult : void 0;
2400
- const counts = (0, openclaw_plugin_sdk_channel_inbound.resolveInboundReplyDispatchCounts)(dispatchResult);
2401
- log.info("dispatch complete", { counts });
2402
- if ((0, openclaw_plugin_sdk_channel_inbound.hasFinalInboundReplyDispatch)(dispatchResult)) logVerboseMessage(`msteams: delivered ${counts.final} repl${counts.final === 1 ? "y" : "ies"} to ${teamsTo}`);
2403
- return {
2404
- kind: "completed",
2405
- finalResponses: counts.final
2406
- };
2407
1906
  } catch (err) {
2408
- log.error("dispatch failed", { error: require_channel_setup.formatUnknownError(err) });
2409
- runtime.error(`msteams dispatch failed: ${require_channel_setup.formatUnknownError(err)}`);
2410
- if (facts.turnAdoptionLifecycle) throw err;
2411
- try {
2412
- await context.sendActivity("⚠️ Something went wrong. Please try again.");
2413
- } catch {}
2414
- return { kind: "failed" };
1907
+ params.log.debug?.("failed to resolve inbound Teams media", { error: require_channel_setup.formatUnknownError(err) });
1908
+ }
1909
+ const inboundMedia = mergeMSTeamsMediaFacts(params.advertisedMedia, resolvedMedia);
1910
+ const body = params.rawBody;
1911
+ const expectedMediaCount = Math.max(params.advertisedMedia.length, resolvedMedia.length);
1912
+ const unavailableCount = inboundMedia.filter((media) => !media.path).length + Math.max(0, expectedMediaCount - inboundMedia.length);
1913
+ const agentBody = unavailableCount === 0 ? body : (0, openclaw_plugin_sdk_channel_inbound.formatInboundMediaUnavailableText)({
1914
+ body,
1915
+ notice: `[msteams ${unavailableCount > 1 ? `${unavailableCount} attachments` : "attachment"} unavailable]`
1916
+ });
1917
+ if (!agentBody && inboundMedia.length === 0) {
1918
+ params.log.debug?.("skipping empty message after Graph media recovery");
1919
+ return null;
1920
+ }
1921
+ return {
1922
+ agentBody,
1923
+ inboundMedia
1924
+ };
1925
+ }
1926
+ function shouldAttemptMSTeamsGraphMediaFallback(params) {
1927
+ const conversationType = params.conversationType.trim().toLowerCase();
1928
+ return params.graphMediaFallback === true && (conversationType === "channel" || conversationType === "groupchat") && (params.htmlSummary?.htmlAttachments ?? 0) > 0;
1929
+ }
1930
+ function hasDefinitiveContentType(media) {
1931
+ const contentType = media.contentType?.split(";", 1)[0]?.trim().toLowerCase();
1932
+ return Boolean(contentType && contentType !== "application/octet-stream" && contentType !== "binary/octet-stream");
1933
+ }
1934
+ function mergeMSTeamsMediaFacts(nativeMedia, materializedMedia, options = {}) {
1935
+ const merged = [...nativeMedia];
1936
+ const nativeSlotCount = nativeMedia.length;
1937
+ const nativeIndexBySourceId = /* @__PURE__ */ new Map();
1938
+ nativeMedia.forEach((media, index) => {
1939
+ if (media.sourceId && !nativeIndexBySourceId.has(media.sourceId)) nativeIndexBySourceId.set(media.sourceId, index);
1940
+ });
1941
+ for (const [index, materialized] of materializedMedia.entries()) {
1942
+ const sourceIndex = materialized.sourceId ? nativeIndexBySourceId.get(materialized.sourceId) : void 0;
1943
+ const positionalIndex = options.positionallyAligned === false || index >= nativeMedia.length ? void 0 : index;
1944
+ const unresolvedIndexes = options.positionallyAligned === false ? merged.slice(0, nativeSlotCount).flatMap((media, mediaIndex) => !media.path && !media.sourceId ? [mediaIndex] : []) : [];
1945
+ const fallbackIndex = unresolvedIndexes.find((mediaIndex) => merged[mediaIndex]?.kind === materialized.kind) ?? (unresolvedIndexes.length === 1 ? unresolvedIndexes[0] : void 0);
1946
+ const targetIndex = sourceIndex ?? positionalIndex ?? fallbackIndex;
1947
+ if (targetIndex === void 0) {
1948
+ if (materialized.sourceId) nativeIndexBySourceId.set(materialized.sourceId, merged.length);
1949
+ merged.push(materialized);
1950
+ } else {
1951
+ if (materialized.sourceId) nativeIndexBySourceId.set(materialized.sourceId, targetIndex);
1952
+ const current = merged[targetIndex];
1953
+ if (materialized.path) merged[targetIndex] = materialized;
1954
+ else if (!current?.path) merged[targetIndex] = {
1955
+ ...current,
1956
+ ...materialized,
1957
+ kind: hasDefinitiveContentType(materialized) || !current?.kind ? materialized.kind : current.kind
1958
+ };
1959
+ }
2415
1960
  }
1961
+ return merged;
1962
+ }
1963
+ function hasMaterializedMedia(media) {
1964
+ return media.some((entry) => Boolean(entry.path));
2416
1965
  }
2417
1966
  // extensions/msteams/src/monitor-handler/inbound-facts.ts
2418
- function extractTextFromHtmlAttachments(attachments) {
1967
+ function extractTextFromHtmlAttachments(attachments, activity) {
2419
1968
  for (const attachment of attachments) {
2420
1969
  const raw = require_resolve_allowlist.extractHtmlFromAttachment(attachment);
2421
1970
  if (!raw) continue;
2422
- const text = require_resolve_allowlist.htmlToPlainText(raw.replace(/<at[^>]*>.*?<\/at>/gis, " ").replace(/<a\b[^>]*href=["']([^"']+)["'][^>]*>(.*?)<\/a>/gis, "$2 $1"));
1971
+ const text = require_resolve_allowlist.htmlToPlainText(require_resolve_allowlist.normalizeMSTeamsMentionTags(require_resolve_allowlist.replaceTeamsEmojiSpriteImagesWithAltText(raw), activity, { preserveHtml: true }).replace(/<a\b[^>]*href=["']([^"']+)["'][^>]*>(.*?)<\/a>/gis, "$2 $1"));
2423
1972
  if (text) return text;
2424
1973
  }
2425
1974
  return "";
@@ -2428,9 +1977,9 @@ async function prepareMSTeamsDebounceEntry(params) {
2428
1977
  const activity = params.context.activity;
2429
1978
  const attachments = Array.isArray(activity.attachments) ? activity.attachments : [];
2430
1979
  const rawText = activity.text?.trim() ?? "";
2431
- const text = rawText ? require_resolve_allowlist.stripMSTeamsMentionTags(rawText) : extractTextFromHtmlAttachments(attachments) || require_resolve_allowlist.stripMSTeamsMentionTags(serializeMSTeamsAdaptiveCardActionValue(activity.value) || "");
1980
+ const text = rawText ? require_resolve_allowlist.normalizeMSTeamsMentionTags(rawText, activity) : extractTextFromHtmlAttachments(attachments, activity) || require_resolve_allowlist.stripMSTeamsMentionTags(serializeMSTeamsAdaptiveCardActionValue(activity.value) || "");
2432
1981
  const conversationId = require_resolve_allowlist.normalizeMSTeamsConversationId(activity.conversation?.id ?? "");
2433
- const replyToId = activity.replyToId ?? void 0;
1982
+ const replyToId = activity.replyToId;
2434
1983
  const implicitMentionKinds = conversationId && replyToId && await require_conversation_store_state.wasMSTeamsMessageSentWithPersistence({
2435
1984
  conversationId,
2436
1985
  messageId: replyToId,
@@ -2446,15 +1995,23 @@ async function prepareMSTeamsDebounceEntry(params) {
2446
1995
  turnAdoptionLifecycle: params.turnAdoptionLifecycle
2447
1996
  };
2448
1997
  }
2449
- function buildStoredConversationReference(params) {
2450
- const { activity, conversationId, conversationType, teamId, threadId } = params;
1998
+ function assembleMSTeamsInboundFacts(entry) {
1999
+ const activity = entry.context.activity;
2000
+ const conversation = activity.conversation;
2001
+ const rawConversationId = conversation?.id ?? "";
2002
+ const conversationId = require_resolve_allowlist.normalizeMSTeamsConversationId(rawConversationId);
2003
+ const conversationMessageId = require_resolve_allowlist.extractMSTeamsConversationMessageId(rawConversationId);
2004
+ const conversationType = conversation?.conversationType ?? "personal";
2005
+ const isChannel = conversationType === "channel";
2006
+ const teamId = activity.channelData?.team?.id;
2007
+ const threadId = isChannel ? conversationMessageId ?? activity.replyToId : void 0;
2008
+ const advertisedMedia = resolveMSTeamsAdvertisedMedia(entry.attachments);
2451
2009
  const from = activity.from;
2452
- const conversation = activity.conversation;
2453
2010
  const clientInfo = activity.entities?.find((entity) => entity.type === "clientInfo");
2454
2011
  const tenantId = activity.channelData?.tenant?.id ?? conversation?.tenantId;
2455
2012
  const aadObjectId = from?.aadObjectId;
2456
2013
  const serviceUrl = require_bot_framework_service_url.tryNormalizeBotFrameworkServiceUrl(activity.serviceUrl);
2457
- return {
2014
+ const conversationRef = {
2458
2015
  activityId: activity.id,
2459
2016
  user: from ? {
2460
2017
  id: from.id,
@@ -2476,18 +2033,6 @@ function buildStoredConversationReference(params) {
2476
2033
  ...clientInfo?.timezone ? { timezone: clientInfo.timezone } : {},
2477
2034
  ...threadId ? { threadId } : {}
2478
2035
  };
2479
- }
2480
- function assembleMSTeamsInboundFacts(entry) {
2481
- const activity = entry.context.activity;
2482
- const conversation = activity.conversation;
2483
- const rawConversationId = conversation?.id ?? "";
2484
- const conversationId = require_resolve_allowlist.normalizeMSTeamsConversationId(rawConversationId);
2485
- const conversationMessageId = require_resolve_allowlist.extractMSTeamsConversationMessageId(rawConversationId);
2486
- const conversationType = conversation?.conversationType ?? "personal";
2487
- const isChannel = conversationType === "channel";
2488
- const teamId = activity.channelData?.team?.id;
2489
- const threadId = isChannel ? conversationMessageId ?? activity.replyToId ?? void 0 : void 0;
2490
- const advertisedMedia = resolveMSTeamsAdvertisedMedia(entry.attachments);
2491
2036
  return {
2492
2037
  ...entry,
2493
2038
  activity,
@@ -2496,9 +2041,7 @@ function assembleMSTeamsInboundFacts(entry) {
2496
2041
  rawBody: entry.text,
2497
2042
  advertisedMedia,
2498
2043
  quoteInfo: require_resolve_allowlist.extractMSTeamsQuoteInfo(entry.attachments),
2499
- attachmentTypes: entry.attachments.map((attachment) => typeof attachment.contentType === "string" ? attachment.contentType : void 0).filter(Boolean).slice(0, 3),
2500
2044
  htmlSummary: summarizeMSTeamsHtmlAttachments(entry.attachments),
2501
- rawConversationId,
2502
2045
  conversationId,
2503
2046
  conversationMessageId,
2504
2047
  conversationType,
@@ -2507,42 +2050,9 @@ function assembleMSTeamsInboundFacts(entry) {
2507
2050
  graphChannelId: activity.channelData?.channel?.id?.trim() || conversationId,
2508
2051
  threadId,
2509
2052
  historyKey: threadId ? `${conversationId}:thread:${threadId}` : conversationId,
2510
- conversationRef: buildStoredConversationReference({
2511
- activity,
2512
- conversationId,
2513
- conversationType,
2514
- teamId,
2515
- threadId
2516
- })
2053
+ conversationRef
2517
2054
  };
2518
2055
  }
2519
- // extensions/msteams/src/team-identity.ts
2520
- const teamGroupIdCache = /* @__PURE__ */ new Map();
2521
- const TEAM_GROUP_ID_CACHE_MAX_ENTRIES = 500;
2522
- function cacheTeamGroupId(conversationTeamId, groupId) {
2523
- teamGroupIdCache.set(conversationTeamId, groupId);
2524
- (0, openclaw_plugin_sdk_collection_runtime.pruneMapToMaxSize)(teamGroupIdCache, TEAM_GROUP_ID_CACHE_MAX_ENTRIES);
2525
- }
2526
- /** Resolve the Graph team GUID without ever treating a Bot Framework team ID as equivalent. */
2527
- async function resolveTeamGroupId(params) {
2528
- const activityGroupId = params.aadGroupId?.trim();
2529
- if (activityGroupId) {
2530
- cacheTeamGroupId(params.conversationTeamId, activityGroupId);
2531
- return activityGroupId;
2532
- }
2533
- const cached = teamGroupIdCache.get(params.conversationTeamId);
2534
- if (cached) return cached;
2535
- const getTeamDetails = params.getTeamDetails;
2536
- if (!getTeamDetails) return;
2537
- const groupId = (await require_resolve_allowlist.withMSTeamsRequestDeadline({
2538
- deadline: params.deadline,
2539
- label: "MS Teams team details",
2540
- work: () => getTeamDetails(params.conversationTeamId)
2541
- })).aadGroupId?.trim();
2542
- if (!groupId) return;
2543
- cacheTeamGroupId(params.conversationTeamId, groupId);
2544
- return groupId;
2545
- }
2546
2056
  // extensions/msteams/src/thread-parent-context.ts
2547
2057
  const PARENT_CACHE_TTL_MS = 3e5;
2548
2058
  const PARENT_CACHE_MAX = 100;
@@ -2554,18 +2064,19 @@ function touchLru(map, key, value, max) {
2554
2064
  map.set(key, value);
2555
2065
  (0, openclaw_plugin_sdk_collection_runtime.pruneMapToMaxSize)(map, max);
2556
2066
  }
2557
- function buildParentCacheKey(groupId, channelId, parentId) {
2558
- return `${groupId}\u0000${channelId}\u0000${parentId}`;
2559
- }
2560
2067
  async function fetchParentMessageCached(token, groupId, channelId, parentId, deadline) {
2561
- const key = buildParentCacheKey(groupId, channelId, parentId);
2068
+ const key = JSON.stringify([
2069
+ (0, openclaw_plugin_sdk_node_crypto_runtime.sha256Hex)(token),
2070
+ groupId,
2071
+ channelId,
2072
+ parentId
2073
+ ]);
2562
2074
  const cached = parentCache.get(key);
2563
2075
  if (cached && cached.expiresAt > Date.now()) {
2564
2076
  parentCache.delete(key);
2565
2077
  parentCache.set(key, cached);
2566
2078
  return cached.message;
2567
2079
  }
2568
- if (cached) parentCache.delete(key);
2569
2080
  const message = await require_reaction_types.fetchChannelMessage(token, groupId, channelId, parentId, deadline);
2570
2081
  touchLru(parentCache, key, {
2571
2082
  message,
@@ -2574,20 +2085,14 @@ async function fetchParentMessageCached(token, groupId, channelId, parentId, dea
2574
2085
  return message;
2575
2086
  }
2576
2087
  const PARENT_TEXT_MAX_CHARS = 400;
2577
- function summarizeParentMessage(message) {
2088
+ function formatParentContextEvent(message) {
2578
2089
  if (!message) return;
2579
2090
  const sender = message.from?.user?.displayName ?? message.from?.application?.displayName ?? "unknown";
2580
2091
  const contentType = message.body?.contentType ?? "text";
2581
2092
  const raw = message.body?.content ?? "";
2582
2093
  const text = contentType === "html" ? require_reaction_types.stripHtmlFromTeamsMessage(raw) : raw.replace(/\s+/g, " ").trim();
2583
2094
  if (!text) return;
2584
- return {
2585
- sender,
2586
- text: text.length > PARENT_TEXT_MAX_CHARS ? `${(0, openclaw_plugin_sdk_text_utility_runtime.truncateUtf16Safe)(text, 399)}…` : text
2587
- };
2588
- }
2589
- function formatParentContextEvent(summary) {
2590
- return `Replying to @${summary.sender}: ${summary.text}`;
2095
+ return `Replying to @${sender}: ${text.length > PARENT_TEXT_MAX_CHARS ? `${(0, openclaw_plugin_sdk_text_utility_runtime.truncateUtf16Safe)(text, 399)}…` : text}`;
2591
2096
  }
2592
2097
  function shouldInjectParentContext(sessionKey, parentId) {
2593
2098
  return injectedParents.get(sessionKey) !== parentId;
@@ -2596,55 +2101,12 @@ function markParentContextInjected(sessionKey, parentId) {
2596
2101
  touchLru(injectedParents, sessionKey, parentId, INJECTED_MAX);
2597
2102
  }
2598
2103
  // extensions/msteams/src/monitor-handler/thread-context.ts
2599
- function prepareMSTeamsThreadRouting(params) {
2600
- const route = require_runtime.getMSTeamsRuntime().channel.routing.resolveAgentRoute({
2601
- cfg: params.cfg,
2602
- channel: "msteams",
2603
- accountId: params.accountId,
2604
- teamId: params.teamId,
2605
- peer: {
2606
- kind: params.isDirectMessage ? "direct" : params.isChannel ? "channel" : "group",
2607
- id: params.isDirectMessage ? params.senderId : params.conversationId
2608
- }
2609
- });
2610
- route.sessionKey = require_thread_session.resolveMSTeamsRouteSessionKey({
2611
- baseSessionKey: route.sessionKey,
2612
- isChannel: params.isChannel,
2613
- conversationMessageId: params.conversationMessageId,
2614
- replyToId: params.context.activity.replyToId
2615
- });
2616
- let teamAadGroupId = params.context.activity.channelData?.team?.aadGroupId?.trim() || void 0;
2617
- const conversationTeamId = params.isChannel ? params.teamId : void 0;
2618
- let teamGroupIdPromise;
2619
- const resolveTeamAadGroupId = async (deadline) => {
2620
- if (!conversationTeamId) return;
2621
- teamGroupIdPromise ??= resolveTeamGroupId({
2622
- conversationTeamId,
2623
- aadGroupId: teamAadGroupId,
2624
- getTeamDetails: params.context.getTeamDetails,
2625
- deadline
2626
- }).catch((err) => {
2627
- params.log.debug?.("failed to resolve Teams AAD group ID", {
2628
- teamId: conversationTeamId,
2629
- error: require_channel_setup.formatUnknownError(err)
2630
- });
2631
- });
2632
- teamAadGroupId = await teamGroupIdPromise;
2633
- return teamAadGroupId;
2634
- };
2635
- return {
2636
- route,
2637
- threadRootId: params.conversationMessageId ?? params.context.activity.replyToId,
2638
- resolveTeamAadGroupId,
2639
- getTeamAadGroupId: () => teamAadGroupId
2640
- };
2641
- }
2642
2104
  async function resolveMSTeamsThreadContext(params) {
2643
2105
  const core = require_runtime.getMSTeamsRuntime();
2644
2106
  const activity = params.context.activity;
2645
- const { route } = params.routing;
2107
+ const { route } = params;
2646
2108
  const deadline = require_resolve_allowlist.createMSTeamsInboundEnrichmentDeadline();
2647
- const teamAadGroupId = await params.routing.resolveTeamAadGroupId(deadline);
2109
+ const teamAadGroupId = await params.resolveTeamAadGroupId(deadline);
2648
2110
  let quoteBodyFull;
2649
2111
  let quoteSenderId;
2650
2112
  let quoteSenderName;
@@ -2664,7 +2126,7 @@ async function resolveMSTeamsThreadContext(params) {
2664
2126
  params.log.debug?.("failed to fetch full quoted message text", { error: require_channel_setup.formatUnknownError(err) });
2665
2127
  }
2666
2128
  let threadContext = [];
2667
- const threadParentId = params.routing.threadRootId;
2129
+ const threadParentId = params.threadRootId;
2668
2130
  if (threadParentId && threadParentId !== activity.id && params.isChannel && teamAadGroupId) try {
2669
2131
  const graphToken = await require_resolve_allowlist.withMSTeamsRequestDeadline({
2670
2132
  deadline,
@@ -2691,29 +2153,22 @@ async function resolveMSTeamsThreadContext(params) {
2691
2153
  allowNameMatching: params.allowNameMatching
2692
2154
  }).allowed
2693
2155
  });
2694
- const parentSummary = summarizeParentMessage(parentMsg);
2695
- const visibleParentMessages = parentMsg ? (0, openclaw_plugin_sdk_context_visibility_runtime.filterSupplementalContextItems)({
2696
- items: [parentMsg],
2156
+ const parentContextEvent = formatParentContextEvent(parentMsg);
2157
+ const { items: threadMessages } = (0, openclaw_plugin_sdk_context_visibility_runtime.filterSupplementalContextItems)({
2158
+ items: parentMsg ? [parentMsg, ...replies] : replies,
2697
2159
  mode: params.contextVisibilityMode,
2698
2160
  kind: "thread",
2699
2161
  isSenderAllowed: isThreadSenderAllowed
2700
- }).items : [];
2701
- if (parentSummary && visibleParentMessages.length > 0 && shouldInjectParentContext(route.sessionKey, threadParentId)) {
2702
- core.system.enqueueSystemEvent(formatParentContextEvent(parentSummary), {
2162
+ });
2163
+ if (parentContextEvent && parentMsg && threadMessages.includes(parentMsg) && shouldInjectParentContext(route.sessionKey, threadParentId)) {
2164
+ core.system.enqueueSystemEvent(parentContextEvent, {
2703
2165
  sessionKey: route.sessionKey,
2704
2166
  contextKey: `msteams:thread-parent:${params.conversationId}:${threadParentId}`
2705
2167
  });
2706
2168
  markParentContextInjected(route.sessionKey, threadParentId);
2707
2169
  }
2708
- const allMessages = parentMsg ? [parentMsg, ...replies] : replies;
2709
- quoteSenderId = parentMsg?.from?.user?.id ?? parentMsg?.from?.application?.id ?? void 0;
2170
+ quoteSenderId = parentMsg?.from?.user?.id ?? parentMsg?.from?.application?.id;
2710
2171
  quoteSenderName = parentMsg?.from?.user?.displayName ?? parentMsg?.from?.application?.displayName ?? params.quoteInfo?.sender;
2711
- const { items: threadMessages } = (0, openclaw_plugin_sdk_context_visibility_runtime.filterSupplementalContextItems)({
2712
- items: allMessages,
2713
- mode: params.contextVisibilityMode,
2714
- kind: "thread",
2715
- isSenderAllowed: isThreadSenderAllowed
2716
- });
2717
2172
  threadContext = require_reaction_types.buildThreadContext(threadMessages, activity.id);
2718
2173
  } catch (err) {
2719
2174
  params.log.debug?.("failed to fetch thread history", { error: require_channel_setup.formatUnknownError(err) });
@@ -2750,17 +2205,8 @@ function createMSTeamsMessageHandler(deps) {
2750
2205
  });
2751
2206
  const handleTeamsMessageNow = async (params) => {
2752
2207
  const facts = assembleMSTeamsInboundFacts(params);
2753
- const { context, activity, rawText, text, attachments, advertisedMedia, rawBody, quoteInfo, from, conversation, attachmentTypes, htmlSummary, conversationId, conversationMessageId, conversationType, isChannel, teamId, conversationRef } = facts;
2208
+ const { context, activity, text, advertisedMedia, rawBody, quoteInfo, from, htmlSummary, conversationId, conversationMessageId, conversationType, isChannel, teamId, graphChannelId, conversationRef } = facts;
2754
2209
  const historyBody = [text, (0, openclaw_plugin_sdk_channel_inbound.formatMediaPlaceholderText)(advertisedMedia)].filter(Boolean).join("\n");
2755
- log.info("received message", {
2756
- rawText: (0, openclaw_plugin_sdk_text_utility_runtime.truncateUtf16Safe)(rawText, 50),
2757
- text: (0, openclaw_plugin_sdk_text_utility_runtime.truncateUtf16Safe)(text, 50),
2758
- attachments: attachments.length,
2759
- attachmentTypes,
2760
- from: from?.id,
2761
- conversation: conversation?.id
2762
- });
2763
- if (htmlSummary) log.debug?.("html attachment summary", htmlSummary);
2764
2210
  if (!from?.id) {
2765
2211
  log.debug?.("skipping message without from.id");
2766
2212
  return;
@@ -2769,7 +2215,7 @@ function createMSTeamsMessageHandler(deps) {
2769
2215
  let isBotOwnedThread = false;
2770
2216
  if (isChannel && facts.threadId && activity.recipient?.id) {
2771
2217
  const channelConfig = currentCfg.channels?.msteams;
2772
- const routeConfig = require_channel.resolveMSTeamsRouteConfig({
2218
+ const routeConfig = require_presentation.resolveMSTeamsRouteConfig({
2773
2219
  cfg: channelConfig,
2774
2220
  teamId,
2775
2221
  teamName: activity.channelData?.team?.name,
@@ -2777,7 +2223,7 @@ function createMSTeamsMessageHandler(deps) {
2777
2223
  channelName: activity.channelData?.channel?.name,
2778
2224
  allowNameMatching: channelConfig?.dangerouslyAllowNameMatching === true
2779
2225
  });
2780
- if (require_channel.resolveMSTeamsReplyPolicy({
2226
+ if (require_presentation.resolveMSTeamsReplyPolicy({
2781
2227
  isDirectMessage: false,
2782
2228
  globalConfig: channelConfig,
2783
2229
  ...routeConfig
@@ -2806,7 +2252,7 @@ function createMSTeamsMessageHandler(deps) {
2806
2252
  logVerboseMessage
2807
2253
  });
2808
2254
  if (!admission) return;
2809
- const { senderId, senderName, isDirectMessage, channelGate, allowNameMatching, groupPolicy, commandAuthorized, effectiveGroupAllowFrom, allowTextCommands, isControlCommand } = admission;
2255
+ const { senderId, senderName, isDirectMessage, channelGate, allowNameMatching, groupPolicy, commandAuthorized, effectiveGroupAllowFrom, allowTextCommands, isControlCommand, msteamsCfg: admittedMSTeamsCfg } = admission;
2810
2256
  const pollVote = require_conversation_store_state.extractMSTeamsPollVote(activity);
2811
2257
  if (pollVote) {
2812
2258
  try {
@@ -2815,11 +2261,6 @@ function createMSTeamsMessageHandler(deps) {
2815
2261
  voterId: senderId,
2816
2262
  selections: pollVote.selections
2817
2263
  })) log.debug?.("poll vote ignored (poll not found)", { pollId: pollVote.pollId });
2818
- else log.info("recorded poll vote", {
2819
- pollId: pollVote.pollId,
2820
- voter: senderId,
2821
- selections: pollVote.selections
2822
- });
2823
2264
  } catch (err) {
2824
2265
  log.error("failed to record poll vote", {
2825
2266
  pollId: pollVote.pollId,
@@ -2828,19 +2269,42 @@ function createMSTeamsMessageHandler(deps) {
2828
2269
  }
2829
2270
  return;
2830
2271
  }
2831
- const threadRouting = prepareMSTeamsThreadRouting({
2272
+ const route = require_runtime.getMSTeamsRuntime().channel.routing.resolveAgentRoute({
2832
2273
  cfg: currentCfg,
2274
+ channel: "msteams",
2833
2275
  accountId,
2834
- context,
2835
- isDirectMessage,
2836
- isChannel,
2837
- senderId,
2838
- conversationId,
2839
- conversationMessageId: conversationMessageId ?? void 0,
2840
2276
  teamId,
2841
- log
2277
+ peer: {
2278
+ kind: isDirectMessage ? "direct" : isChannel ? "channel" : "group",
2279
+ id: isDirectMessage ? senderId : conversationId
2280
+ }
2842
2281
  });
2843
- const { route } = threadRouting;
2282
+ route.sessionKey = require_thread_session.resolveMSTeamsRouteSessionKey({
2283
+ baseSessionKey: route.sessionKey,
2284
+ isChannel,
2285
+ conversationMessageId,
2286
+ replyToId: context.activity.replyToId
2287
+ });
2288
+ let resolvedTeamAadGroupId = context.activity.channelData?.team?.aadGroupId?.trim() || void 0;
2289
+ const conversationTeamId = isChannel ? teamId : void 0;
2290
+ let teamGroupIdPromise;
2291
+ const resolveTeamAadGroupId = async (deadline) => {
2292
+ if (!conversationTeamId) return;
2293
+ teamGroupIdPromise ??= resolveTeamGroupId({
2294
+ conversationTeamId,
2295
+ aadGroupId: resolvedTeamAadGroupId,
2296
+ getTeamDetails: context.getTeamDetails,
2297
+ deadline
2298
+ }).catch((err) => {
2299
+ log.debug?.("failed to resolve Teams AAD group ID", {
2300
+ teamId: conversationTeamId,
2301
+ error: require_channel_setup.formatUnknownError(err)
2302
+ });
2303
+ });
2304
+ resolvedTeamAadGroupId = await teamGroupIdPromise;
2305
+ return resolvedTeamAadGroupId;
2306
+ };
2307
+ const threadRootId = conversationMessageId ?? context.activity.replyToId;
2844
2308
  const inboundLabel = isDirectMessage ? `Teams DM from ${senderName}` : `Teams message in ${conversationType} from ${senderName}`;
2845
2309
  const enqueuePrimaryMessageSystemEvent = () => core.system.enqueueSystemEvent(inboundLabel, {
2846
2310
  sessionKey: route.sessionKey,
@@ -2848,7 +2312,7 @@ function createMSTeamsMessageHandler(deps) {
2848
2312
  });
2849
2313
  const channelId = conversationId;
2850
2314
  const { teamConfig, channelConfig } = channelGate;
2851
- const replyPolicy = require_channel.resolveMSTeamsReplyPolicy({
2315
+ const replyPolicy = require_presentation.resolveMSTeamsReplyPolicy({
2852
2316
  isDirectMessage,
2853
2317
  globalConfig: currentMSTeamsCfg,
2854
2318
  teamConfig,
@@ -2894,7 +2358,7 @@ function createMSTeamsMessageHandler(deps) {
2894
2358
  sender: senderName,
2895
2359
  body: historyBody,
2896
2360
  timestamp: timestamp?.getTime(),
2897
- messageId: activity.id ?? void 0
2361
+ messageId: activity.id
2898
2362
  }
2899
2363
  });
2900
2364
  }
@@ -2905,12 +2369,12 @@ function createMSTeamsMessageHandler(deps) {
2905
2369
  entry: params,
2906
2370
  rawBody,
2907
2371
  advertisedMedia,
2908
- htmlSummary: htmlSummary ?? void 0,
2372
+ htmlSummary,
2909
2373
  conversationType,
2910
2374
  conversationId,
2911
- conversationMessageId: conversationMessageId ?? void 0,
2912
- teamAadGroupId: threadRouting.getTeamAadGroupId(),
2913
- resolveTeamAadGroupId: threadRouting.resolveTeamAadGroupId,
2375
+ conversationMessageId,
2376
+ teamAadGroupId: resolvedTeamAadGroupId,
2377
+ resolveTeamAadGroupId,
2914
2378
  mediaMaxBytes,
2915
2379
  tokenProvider,
2916
2380
  mediaAllowHosts: currentMSTeamsCfg?.mediaAllowHosts,
@@ -2921,7 +2385,9 @@ function createMSTeamsMessageHandler(deps) {
2921
2385
  if (!content) return;
2922
2386
  enqueuePrimaryMessageSystemEvent();
2923
2387
  const thread = await resolveMSTeamsThreadContext({
2924
- routing: threadRouting,
2388
+ route,
2389
+ threadRootId,
2390
+ resolveTeamAadGroupId,
2925
2391
  context,
2926
2392
  tokenProvider,
2927
2393
  quoteInfo,
@@ -2934,29 +2400,219 @@ function createMSTeamsMessageHandler(deps) {
2934
2400
  allowNameMatching,
2935
2401
  log
2936
2402
  });
2937
- await dispatchMSTeamsInboundTurn({
2403
+ const dispatchAccountPolicyCfg = readFullConfig();
2404
+ const dispatchCore = require_runtime.getMSTeamsRuntime();
2405
+ const { agentBody, inboundMedia } = content;
2406
+ const { teamAadGroupId, quoteBodyFull, quoteSenderId, quoteSenderName, threadContext } = thread;
2407
+ const teamsFrom = isDirectMessage ? `msteams:${senderId}` : isChannel ? `msteams:channel:${conversationId}` : `msteams:group:${conversationId}`;
2408
+ const teamsTo = isDirectMessage ? `user:${senderId}` : `conversation:${conversationId}`;
2409
+ const envelopeFrom = isDirectMessage ? senderName : conversationType;
2410
+ const buildEnvelope = await (0, openclaw_plugin_sdk_channel_inbound.createChannelInboundEnvelopeBuilderAsync)({
2938
2411
  cfg: currentCfg,
2939
- accountPolicyCfg: readFullConfig(),
2940
- readConfig,
2941
- listenerAccountId: accountId,
2942
- runtime,
2943
- app,
2944
- tokenProvider,
2945
- textLimit,
2946
- log,
2947
- logVerboseMessage,
2948
- facts,
2949
- admission,
2950
- content,
2951
- routing: threadRouting,
2952
- thread,
2953
- replyStyle,
2412
+ route
2413
+ });
2414
+ let combinedBody = buildEnvelope({
2415
+ channel: "Teams",
2416
+ from: envelopeFrom,
2954
2417
  timestamp,
2955
- contextVisibilityMode,
2956
- mentionWasEffective: mentionDecision.effectiveWasMentioned,
2957
- conversationHistories,
2958
- historyLimit
2418
+ body: agentBody
2419
+ });
2420
+ const isRoomish = !isDirectMessage;
2421
+ const historyKey = isRoomish ? facts.historyKey : void 0;
2422
+ if (isRoomish && historyKey) combinedBody = (0, openclaw_plugin_sdk_reply_history.createChannelHistoryWindow)({ historyMap: conversationHistories }).buildPendingContext({
2423
+ historyKey,
2424
+ limit: historyLimit,
2425
+ currentMessage: combinedBody,
2426
+ formatEntry: (entry) => buildEnvelope({
2427
+ channel: "Teams",
2428
+ from: conversationType,
2429
+ timestamp: entry.timestamp,
2430
+ previousTimestamp: null,
2431
+ body: `${entry.sender}: ${entry.body}${entry.messageId ? ` [id:${entry.messageId}]` : ""}`
2432
+ })
2433
+ });
2434
+ const inboundHistory = isRoomish && historyKey && historyLimit > 0 ? (0, openclaw_plugin_sdk_reply_history.createChannelHistoryWindow)({ historyMap: conversationHistories }).buildInboundHistory({
2435
+ historyKey,
2436
+ limit: historyLimit
2437
+ }) : void 0;
2438
+ const commandBody = text.trim();
2439
+ const quoteSenderAllowed = quoteInfo && quoteInfo.sender ? (0, openclaw_plugin_sdk_channel_inbound.resolveInboundSupplementalSenderAllowed)({
2440
+ isGroup: !isDirectMessage,
2441
+ groupPolicy,
2442
+ allowFrom: effectiveGroupAllowFrom,
2443
+ isSenderAllowed: (allowFrom) => (0, openclaw_plugin_sdk_allow_from.resolveAllowlistMatchSimple)({
2444
+ allowFrom,
2445
+ senderId: quoteSenderId ?? "",
2446
+ senderName: quoteSenderName,
2447
+ allowNameMatching
2448
+ }).allowed
2449
+ }) : true;
2450
+ const nativeChannelId = isChannel && teamAadGroupId ? `${teamAadGroupId}/${graphChannelId}` : void 0;
2451
+ const boundIngress = await resolveMSTeamsSenderAccess({
2452
+ cfg: route.accountId === accountId ? readConfig() : dispatchAccountPolicyCfg,
2453
+ accountId: route.accountId,
2454
+ activity,
2455
+ hasControlCommand: admission.isControlCommand,
2456
+ conversationThreadId: facts.threadId,
2457
+ contextBinding: {
2458
+ agentId: route.agentId,
2459
+ sessionKey: route.sessionKey,
2460
+ ...activity.id ? { messageId: activity.id } : {},
2461
+ ...nativeChannelId ? { nativeChannelId } : {},
2462
+ inboundEventKind: "user_request"
2463
+ }
2464
+ });
2465
+ if (!boundIngress.senderAccess.allowed || boundIngress.hasConflictingConversationScope || boundIngress.msteamsCfg?.enabled === false || boundIngress.commandAccess.shouldBlockControlCommand) {
2466
+ log.info("dropping message (dispatch-time account authorization changed)");
2467
+ return;
2468
+ }
2469
+ const ctxPayload = dispatchCore.channel.inbound.buildContext({
2470
+ channelIngress: boundIngress.channelIngress,
2471
+ channel: "msteams",
2472
+ contextVisibility: contextVisibilityMode,
2473
+ supplemental: {
2474
+ channelStructuredContext: threadContext.length ? [{
2475
+ label: "Thread history",
2476
+ source: "msteams",
2477
+ type: "chat_window",
2478
+ sessionTranscriptMode: "preserve",
2479
+ payload: {
2480
+ order: "chronological",
2481
+ messages: threadContext
2482
+ }
2483
+ }] : void 0,
2484
+ quote: quoteInfo ? {
2485
+ id: quoteInfo.id ?? activity.replyToId,
2486
+ body: quoteBodyFull ?? quoteInfo.body,
2487
+ sender: quoteInfo.sender,
2488
+ senderAllowed: quoteSenderAllowed,
2489
+ isQuote: true
2490
+ } : void 0
2491
+ },
2492
+ media: await (0, openclaw_plugin_sdk_channel_inbound.toInboundMediaFactsWithMetadata)(inboundMedia),
2493
+ messageId: activity.id,
2494
+ timestamp: timestamp?.getTime() ?? Date.now(),
2495
+ from: teamsFrom,
2496
+ sender: {
2497
+ id: senderId,
2498
+ name: senderName
2499
+ },
2500
+ conversation: {
2501
+ kind: isDirectMessage ? "direct" : isChannel ? "channel" : "group",
2502
+ id: conversationId,
2503
+ label: envelopeFrom,
2504
+ spaceId: teamId,
2505
+ nativeChannelId
2506
+ },
2507
+ route: {
2508
+ agentId: route.agentId,
2509
+ dmScope: route.dmScope,
2510
+ accountId: route.accountId,
2511
+ routeSessionKey: route.sessionKey
2512
+ },
2513
+ reply: {
2514
+ to: teamsTo,
2515
+ originatingTo: `conversation:${conversationId}`,
2516
+ messageThreadId: facts.threadId,
2517
+ replyToId: activity.replyToId,
2518
+ nativeChannelId
2519
+ },
2520
+ message: {
2521
+ body: combinedBody,
2522
+ bodyForAgent: agentBody,
2523
+ inboundHistory,
2524
+ rawBody,
2525
+ commandBody
2526
+ },
2527
+ sessionTranscript: { historyLimit: isRoomish ? historyLimit : 0 },
2528
+ access: {
2529
+ mentions: {
2530
+ canDetectMention: !isDirectMessage,
2531
+ wasMentioned: isDirectMessage || mentionDecision.effectiveWasMentioned
2532
+ },
2533
+ commands: { authorized: commandAuthorized === true }
2534
+ },
2535
+ extra: {
2536
+ GroupSubject: !isDirectMessage ? conversationType : void 0,
2537
+ ReplyToIsQuote: quoteInfo ? true : void 0
2538
+ }
2959
2539
  });
2540
+ const { dispatcherOptions, delivery, replyOptions } = createMSTeamsReplyDispatcher({
2541
+ cfg: currentCfg,
2542
+ agentId: route.agentId,
2543
+ sessionKey: route.sessionKey,
2544
+ accountId: route.accountId,
2545
+ log,
2546
+ app,
2547
+ conversationRef,
2548
+ context,
2549
+ replyStyle,
2550
+ textLimit,
2551
+ onSentMessageIds: (ids) => {
2552
+ for (const id of ids) require_conversation_store_state.recordMSTeamsSentMessage(conversationId, id, { accountId: route.accountId });
2553
+ },
2554
+ tokenProvider,
2555
+ sharePointSiteId: admittedMSTeamsCfg?.sharePointSiteId
2556
+ });
2557
+ const senderTimezone = conversationRef.timezone;
2558
+ const turnConfig = senderTimezone && !currentCfg.agents?.defaults?.userTimezone ? {
2559
+ ...currentCfg,
2560
+ agents: {
2561
+ ...currentCfg.agents,
2562
+ defaults: {
2563
+ ...currentCfg.agents?.defaults,
2564
+ userTimezone: senderTimezone
2565
+ }
2566
+ }
2567
+ } : currentCfg;
2568
+ try {
2569
+ await dispatchCore.channel.inbound.run({
2570
+ channel: "msteams",
2571
+ accountId: route.accountId,
2572
+ raw: context,
2573
+ adapter: {
2574
+ ingest: () => ({
2575
+ id: activity.id ?? `${teamsFrom}:${Date.now()}`,
2576
+ timestamp: timestamp?.getTime(),
2577
+ rawText: rawBody,
2578
+ textForAgent: agentBody,
2579
+ textForCommands: commandBody,
2580
+ raw: activity
2581
+ }),
2582
+ resolveTurn: () => ({
2583
+ cfg: turnConfig,
2584
+ channel: "msteams",
2585
+ accountId: route.accountId,
2586
+ route: {
2587
+ agentId: route.agentId,
2588
+ sessionKey: route.sessionKey
2589
+ },
2590
+ ctxPayload,
2591
+ record: { onRecordError: (err) => {
2592
+ logVerboseMessage(`msteams: failed updating session meta: ${require_channel_setup.formatUnknownError(err)}`);
2593
+ } },
2594
+ history: {
2595
+ isGroup: isRoomish,
2596
+ historyKey,
2597
+ historyMap: conversationHistories,
2598
+ limit: historyLimit
2599
+ },
2600
+ dispatcherOptions,
2601
+ delivery,
2602
+ replyOptions: {
2603
+ ...replyOptions,
2604
+ ...facts.turnAdoptionLifecycle ? (0, openclaw_plugin_sdk_channel_outbound.bindIngressLifecycleToReplyOptions)(facts.turnAdoptionLifecycle) : {}
2605
+ }
2606
+ })
2607
+ }
2608
+ });
2609
+ } catch (err) {
2610
+ runtime.error(`msteams dispatch failed: ${require_channel_setup.formatUnknownError(err)}`);
2611
+ if (facts.turnAdoptionLifecycle) throw err;
2612
+ try {
2613
+ await context.sendActivity("⚠️ Something went wrong. Please try again.");
2614
+ } catch {}
2615
+ }
2960
2616
  };
2961
2617
  const inboundDebouncer = core.channel.debounce.createInboundDebouncer({
2962
2618
  debounceMs: resolveDebounceMs(),
@@ -3107,13 +2763,6 @@ function createMSTeamsReactionHandler(deps) {
3107
2763
  const reactionType = reaction.type ?? "unknown";
3108
2764
  const emoji = require_reaction_types.resolveMSTeamsReactionEmoji(reactionType);
3109
2765
  const label = direction === "added" ? `Teams reaction ${emoji} added by ${senderName} on message ${targetMessageId}` : `Teams reaction ${emoji} removed by ${senderName} from message ${targetMessageId}`;
3110
- log.info(`reaction ${direction}`, {
3111
- sender: senderId,
3112
- reactionType,
3113
- emoji,
3114
- targetMessageId,
3115
- conversationId
3116
- });
3117
2766
  core.system.enqueueSystemEvent(label, {
3118
2767
  sessionKey,
3119
2768
  contextKey: `msteams:reaction:${conversationId}:${targetMessageId}:${senderId}:${reactionType}:${direction}`
@@ -3121,43 +2770,12 @@ function createMSTeamsReactionHandler(deps) {
3121
2770
  }
3122
2771
  };
3123
2772
  }
3124
- // extensions/msteams/src/welcome-card.ts
2773
+ // extensions/msteams/src/monitor-handler.ts
3125
2774
  const DEFAULT_PROMPT_STARTERS = [
3126
2775
  "What can you do?",
3127
2776
  "Summarize my last meeting",
3128
2777
  "Help me draft an email"
3129
2778
  ];
3130
- function buildWelcomeCard(options) {
3131
- const botName = options?.botName || "OpenClaw";
3132
- const starters = options?.promptStarters?.length ? options.promptStarters : DEFAULT_PROMPT_STARTERS;
3133
- return {
3134
- type: "AdaptiveCard",
3135
- version: "1.5",
3136
- body: [{
3137
- type: "TextBlock",
3138
- text: `Hi! I'm ${botName}.`,
3139
- weight: "Bolder",
3140
- size: "Medium"
3141
- }, {
3142
- type: "TextBlock",
3143
- text: "I can help you with questions, tasks, and more. Here are some things to try:",
3144
- wrap: true
3145
- }],
3146
- actions: starters.map((label) => ({
3147
- type: "Action.Submit",
3148
- title: label,
3149
- data: { msteams: {
3150
- type: "imBack",
3151
- value: label
3152
- } }
3153
- }))
3154
- };
3155
- }
3156
- function buildGroupWelcomeText(botName) {
3157
- const name = botName || "OpenClaw";
3158
- return `Hi! I'm ${name}. Mention me with @${name} to get started.`;
3159
- }
3160
- // extensions/msteams/src/monitor-handler.ts
3161
2779
  async function isMSTeamsInvokeAuthorized(params) {
3162
2780
  const { context, deps, invokeKind } = params;
3163
2781
  const cfg = deps.readConfig?.() ?? deps.cfg;
@@ -3215,10 +2833,30 @@ function createMSTeamsActivityHandler(deps) {
3215
2833
  for (const member of membersAdded) if (member.id === botId) {
3216
2834
  const isPersonal = ((0, openclaw_plugin_sdk_string_coerce_runtime.normalizeOptionalLowercaseString)(ctx.activity?.conversation?.conversationType) ?? "personal") === "personal";
3217
2835
  if (isPersonal && msteamsCfg?.welcomeCard !== false) {
3218
- const card = buildWelcomeCard({
3219
- botName: ctx.activity?.recipient?.name ?? void 0,
3220
- promptStarters: msteamsCfg?.promptStarters
3221
- });
2836
+ const botName = ctx.activity?.recipient?.name || "OpenClaw";
2837
+ const starters = msteamsCfg?.promptStarters?.length ? msteamsCfg.promptStarters : DEFAULT_PROMPT_STARTERS;
2838
+ const card = {
2839
+ type: "AdaptiveCard",
2840
+ version: "1.5",
2841
+ body: [{
2842
+ type: "TextBlock",
2843
+ text: `Hi! I'm ${botName}.`,
2844
+ weight: "Bolder",
2845
+ size: "Medium"
2846
+ }, {
2847
+ type: "TextBlock",
2848
+ text: "I can help you with questions, tasks, and more. Here are some things to try:",
2849
+ wrap: true
2850
+ }],
2851
+ actions: starters.map((label) => ({
2852
+ type: "Action.Submit",
2853
+ title: label,
2854
+ data: { msteams: {
2855
+ type: "imBack",
2856
+ value: label
2857
+ } }
2858
+ }))
2859
+ };
3222
2860
  try {
3223
2861
  await ctx.sendActivity(require_conversation_store_state.buildMSTeamsAdaptiveCardActivity(card));
3224
2862
  deps.log.info("sent welcome card");
@@ -3226,9 +2864,9 @@ function createMSTeamsActivityHandler(deps) {
3226
2864
  deps.log.debug?.("failed to send welcome card", { error: require_channel_setup.formatUnknownError(err) });
3227
2865
  }
3228
2866
  } else if (!isPersonal && msteamsCfg?.groupWelcomeCard === true) {
3229
- const botName = ctx.activity?.recipient?.name ?? void 0;
2867
+ const botName = ctx.activity?.recipient?.name || "OpenClaw";
3230
2868
  try {
3231
- await ctx.sendActivity(buildGroupWelcomeText(botName));
2869
+ await ctx.sendActivity(`Hi! I'm ${botName}. Mention me with @${botName} to get started.`);
3232
2870
  deps.log.info("sent group welcome message");
3233
2871
  } catch (err) {
3234
2872
  deps.log.debug?.("failed to send group welcome", { error: require_channel_setup.formatUnknownError(err) });
@@ -3319,12 +2957,12 @@ async function runMSTeamsFeedbackInvokeHandler(context, deps) {
3319
2957
  id: isDirectMessage ? senderId : conversationId
3320
2958
  }
3321
2959
  });
3322
- const feedbackThreadId = isChannel ? require_resolve_allowlist.extractMSTeamsConversationMessageId(rawConversationId) ?? activity.replyToId ?? void 0 : void 0;
3323
- if (feedbackThreadId) route.sessionKey = (0, openclaw_plugin_sdk_routing.resolveThreadSessionKeys)({
2960
+ route.sessionKey = require_thread_session.resolveMSTeamsRouteSessionKey({
3324
2961
  baseSessionKey: route.sessionKey,
3325
- threadId: feedbackThreadId,
3326
- parentSessionKey: route.sessionKey
3327
- }).sessionKey;
2962
+ isChannel,
2963
+ conversationMessageId: require_resolve_allowlist.extractMSTeamsConversationMessageId(rawConversationId),
2964
+ replyToId: activity.replyToId
2965
+ });
3328
2966
  const feedbackEvent = {
3329
2967
  type: "custom",
3330
2968
  event: "feedback",
@@ -3370,57 +3008,94 @@ async function runMSTeamsFeedbackInvokeHandler(context, deps) {
3370
3008
  serviceUrl: activity.serviceUrl,
3371
3009
  locale: activity.locale
3372
3010
  };
3373
- if (isNegative && msteamsCfg?.feedbackReflection !== false) runFeedbackReflection({
3374
- cfg,
3375
- app: deps.app,
3376
- accountId: deps.accountId,
3377
- conversationRef,
3378
- sessionKey: route.sessionKey,
3379
- agentId: route.agentId,
3380
- conversationId,
3381
- conversationKind: isDirectMessage ? "direct" : isChannel ? "channel" : "group",
3382
- userComment,
3383
- log: deps.log
3384
- }).catch((err) => {
3385
- deps.log.error("feedback reflection failed", { error: require_channel_setup.formatUnknownError(err) });
3386
- });
3011
+ if (isNegative && msteamsCfg?.feedbackReflection !== false) {
3012
+ const { app, accountId, log } = deps;
3013
+ const { sessionKey, agentId } = route;
3014
+ const conversationKind = isDirectMessage ? "direct" : isChannel ? "channel" : "group";
3015
+ (async () => {
3016
+ const cooldownMs = msteamsCfg.feedbackReflectionCooldownMs ?? openclaw_plugin_sdk_channel_inbound.DEFAULT_CHANNEL_FEEDBACK_REFLECTION_COOLDOWN_MS;
3017
+ let reflection;
3018
+ try {
3019
+ reflection = await (0, openclaw_plugin_sdk_channel_inbound.runChannelFeedbackReflection)({
3020
+ cfg,
3021
+ channel: "msteams",
3022
+ channelLabel: "Teams",
3023
+ agentId,
3024
+ sessionKey,
3025
+ conversationId,
3026
+ conversationKind,
3027
+ userComment,
3028
+ cooldownMs,
3029
+ onRecordError: (err) => log.debug?.("reflection session record failed", { error: require_channel_setup.formatUnknownError(err) }),
3030
+ onDispatchError: (err) => log.debug?.("reflection reply error", { error: require_channel_setup.formatUnknownError(err) })
3031
+ });
3032
+ } catch (err) {
3033
+ log.error("reflection dispatch failed", { error: require_channel_setup.formatUnknownError(err) });
3034
+ return;
3035
+ }
3036
+ if (reflection.status === "cooldown") {
3037
+ log.debug?.("skipping reflection (cooldown active)", { sessionKey });
3038
+ return;
3039
+ }
3040
+ if (reflection.status === "empty") {
3041
+ log.debug?.("reflection produced no output");
3042
+ return;
3043
+ }
3044
+ if (!(convType === "personal" && reflection.followUp && Boolean(reflection.userMessage))) {
3045
+ if (reflection.followUp && convType !== "personal") log.debug?.("skipping reflection follow-up outside direct message", {
3046
+ sessionKey,
3047
+ conversationType: convType
3048
+ });
3049
+ return;
3050
+ }
3051
+ try {
3052
+ await require_conversation_store_state.sendMSTeamsActivityWithReference(app, require_conversation_store_state.buildConversationReference(conversationRef), {
3053
+ type: "message",
3054
+ text: reflection.userMessage
3055
+ }, {
3056
+ accountId,
3057
+ serviceUrlBoundary: require_resolve_allowlist.resolveMSTeamsSdkCloudOptions(msteamsCfg)
3058
+ });
3059
+ } catch (err) {
3060
+ log.debug?.("failed to send reflection follow-up", { error: require_channel_setup.formatUnknownError(err) });
3061
+ }
3062
+ })().catch((err) => {
3063
+ deps.log.error("feedback reflection failed", { error: require_channel_setup.formatUnknownError(err) });
3064
+ });
3065
+ }
3387
3066
  return true;
3388
3067
  }
3389
3068
  // extensions/msteams/src/file-consent-invoke.ts
3390
- async function handleMSTeamsFileConsentInvoke(context, log) {
3391
- const expiredUploadMessage = "The file upload request has expired. Please try sending the file again.";
3392
- const activity = context.activity;
3393
- if (activity.type !== "invoke" || activity.name !== "fileConsent/invoke") return false;
3394
- const consentResponse = require_conversation_store_state.parseFileConsentInvoke(activity);
3395
- if (!consentResponse) {
3396
- log.debug?.("invalid file consent invoke");
3397
- return false;
3398
- }
3399
- const uploadId = typeof consentResponse.context?.uploadId === "string" ? consentResponse.context.uploadId : void 0;
3400
- const inMemoryFile = require_conversation_store_state.getPendingUpload(uploadId);
3401
- const fsFile = inMemoryFile ? void 0 : await require_conversation_store_state.getPendingUploadFs(uploadId);
3402
- const pendingFile = inMemoryFile ?? fsFile;
3403
- if (pendingFile) {
3404
- const pendingConversationId = require_resolve_allowlist.normalizeMSTeamsConversationId(pendingFile.conversationId);
3405
- const invokeConversationId = require_resolve_allowlist.normalizeMSTeamsConversationId(activity.conversation?.id ?? "");
3406
- if (!invokeConversationId || pendingConversationId !== invokeConversationId) {
3407
- log.info("file consent conversation mismatch", {
3408
- uploadId,
3409
- expectedConversationId: pendingConversationId,
3410
- receivedConversationId: invokeConversationId || void 0
3411
- });
3412
- if (consentResponse.action === "accept") await context.sendActivity(expiredUploadMessage);
3413
- return true;
3069
+ /** Run delayed upload/update work after the SDK route acknowledges the invoke. */
3070
+ async function runMSTeamsFileConsentInvokeHandler(context, log) {
3071
+ try {
3072
+ const expiredUploadMessage = "The file upload request has expired. Please try sending the file again.";
3073
+ const activity = context.activity;
3074
+ if (activity.type !== "invoke" || activity.name !== "fileConsent/invoke") return;
3075
+ const consentResponse = require_conversation_store_state.parseFileConsentInvoke(activity);
3076
+ if (!consentResponse) {
3077
+ log.debug?.("invalid file consent invoke");
3078
+ return;
3414
3079
  }
3415
- }
3416
- if (consentResponse.action === "accept" && consentResponse.uploadInfo) {
3080
+ const uploadId = typeof consentResponse.context?.uploadId === "string" ? consentResponse.context.uploadId : void 0;
3081
+ const inMemoryFile = require_conversation_store_state.getPendingUpload(uploadId);
3082
+ const fsFile = inMemoryFile ? void 0 : await require_conversation_store_state.getPendingUploadFs(uploadId);
3083
+ const pendingFile = inMemoryFile ?? fsFile;
3417
3084
  if (pendingFile) {
3418
- log.debug?.("user accepted file consent, uploading", {
3419
- uploadId,
3420
- filename: pendingFile.filename,
3421
- size: pendingFile.buffer.length
3422
- });
3423
- try {
3085
+ const pendingConversationId = require_resolve_allowlist.normalizeMSTeamsConversationId(pendingFile.conversationId);
3086
+ const invokeConversationId = require_resolve_allowlist.normalizeMSTeamsConversationId(activity.conversation?.id ?? "");
3087
+ if (!invokeConversationId || pendingConversationId !== invokeConversationId) {
3088
+ log.info("file consent conversation mismatch", {
3089
+ uploadId,
3090
+ expectedConversationId: pendingConversationId,
3091
+ receivedConversationId: invokeConversationId || void 0
3092
+ });
3093
+ if (consentResponse.action === "accept") await context.sendActivity(expiredUploadMessage);
3094
+ return;
3095
+ }
3096
+ }
3097
+ if (consentResponse.action === "accept" && consentResponse.uploadInfo) {
3098
+ if (pendingFile) try {
3424
3099
  await require_conversation_store_state.uploadToConsentUrl({
3425
3100
  url: consentResponse.uploadInfo.uploadUrl,
3426
3101
  buffer: pendingFile.buffer,
@@ -3444,11 +3119,6 @@ async function handleMSTeamsFileConsentInvoke(context, log) {
3444
3119
  await context.sendActivity(fileActivity);
3445
3120
  }
3446
3121
  else await context.sendActivity(fileActivity);
3447
- log.info("file upload complete", {
3448
- uploadId,
3449
- filename: consentResponse.uploadInfo.name,
3450
- uniqueId: consentResponse.uploadInfo.uniqueId
3451
- });
3452
3122
  } catch (err) {
3453
3123
  log.error("file upload failed", {
3454
3124
  uploadId,
@@ -3459,52 +3129,19 @@ async function handleMSTeamsFileConsentInvoke(context, log) {
3459
3129
  require_conversation_store_state.removePendingUpload(uploadId);
3460
3130
  await require_conversation_store_state.removePendingUploadFs(uploadId);
3461
3131
  }
3132
+ else {
3133
+ log.debug?.("pending file not found for consent", { uploadId });
3134
+ await context.sendActivity(expiredUploadMessage);
3135
+ }
3462
3136
  } else {
3463
- log.debug?.("pending file not found for consent", { uploadId });
3464
- await context.sendActivity(expiredUploadMessage);
3137
+ log.debug?.("user declined file consent", { uploadId });
3138
+ require_conversation_store_state.removePendingUpload(uploadId);
3139
+ await require_conversation_store_state.removePendingUploadFs(uploadId);
3465
3140
  }
3466
- } else {
3467
- log.debug?.("user declined file consent", { uploadId });
3468
- require_conversation_store_state.removePendingUpload(uploadId);
3469
- await require_conversation_store_state.removePendingUploadFs(uploadId);
3470
- }
3471
- return true;
3472
- }
3473
- /**
3474
- * Run the file-consent invoke handler after the SDK route has acknowledged the
3475
- * invoke. This intentionally does not send its own invokeResponse; it only does
3476
- * the delayed upload/update work.
3477
- */
3478
- async function runMSTeamsFileConsentInvokeHandler(context, log) {
3479
- try {
3480
- await require_conversation_store_state.withRevokedProxyFallback({
3481
- run: () => handleMSTeamsFileConsentInvoke(context, log),
3482
- onRevoked: async () => true,
3483
- onRevokedLog: () => {
3484
- log.debug?.("turn context revoked during file consent invoke; skipping delayed response");
3485
- }
3486
- });
3487
3141
  } catch (err) {
3488
3142
  log.debug?.("file consent handler error", { error: require_channel_setup.formatUnknownError(err) });
3489
3143
  }
3490
3144
  }
3491
- // extensions/msteams/src/monitor-account-stores.ts
3492
- function createMSTeamsMonitorStores(accountId, overrides) {
3493
- return {
3494
- conversationStore: overrides.conversationStore ?? require_conversation_store_state.createMSTeamsConversationStoreState({ accountId }),
3495
- pollStore: overrides.pollStore ?? require_conversation_store_state.createMSTeamsPollStoreState({ accountId })
3496
- };
3497
- }
3498
- // extensions/msteams/src/monitor-status.ts
3499
- function publishMSTeamsBlocked(statusSink, lastError) {
3500
- statusSink?.((0, openclaw_plugin_sdk_gateway_runtime.channelBlockedPatch)(lastError, { running: true }));
3501
- }
3502
- function publishMSTeamsReady(statusSink, now = Date.now()) {
3503
- statusSink?.((0, openclaw_plugin_sdk_gateway_runtime.channelReadyPatch)({ lastConnectedAt: now }));
3504
- }
3505
- function publishMSTeamsStopped(statusSink) {
3506
- statusSink?.((0, openclaw_plugin_sdk_gateway_runtime.channelStoppedPatch)());
3507
- }
3508
3145
  // extensions/msteams/src/msteams-ingress.ts
3509
3146
  const MSTEAMS_INGRESS_VERSION = 1;
3510
3147
  const MSTEAMS_INGRESS_DRAIN_INTERVAL_MS = 500;
@@ -3668,17 +3305,9 @@ function createMSTeamsReplayContext(activity, app, serviceUrlBoundary, accountId
3668
3305
  return {
3669
3306
  activity,
3670
3307
  sendActivity,
3671
- sendActivities: async (activities) => {
3672
- const results = [];
3673
- for (const outbound of activities) results.push(await sendActivity(outbound));
3674
- return results;
3675
- },
3676
3308
  updateActivity: async (outbound) => {
3677
3309
  return await require_conversation_store_state.updateMSTeamsActivityWithReference(app, reference, typeof outbound.id === "string" ? outbound.id : "", outbound, proactiveOptions);
3678
3310
  },
3679
- deleteActivity: async (activityId) => {
3680
- await require_conversation_store_state.deleteMSTeamsActivityWithReference(app, reference, activityId, proactiveOptions);
3681
- },
3682
3311
  getTeamDetails: (teamId) => app.api.teams.getById(teamId)
3683
3312
  };
3684
3313
  }
@@ -3694,7 +3323,7 @@ const MSTEAMS_SSO_TOKENS_NAMESPACE = "sso-tokens";
3694
3323
  const MSTEAMS_MAX_SSO_TOKENS = 5e3;
3695
3324
  const STORE_KEY_VERSION_PREFIX = "v2:";
3696
3325
  function makeMSTeamsSsoTokenStoreKey(connectionName, userId) {
3697
- return `${STORE_KEY_VERSION_PREFIX}${(0, node_crypto.createHash)("sha256").update(JSON.stringify([connectionName, userId])).digest("hex")}`;
3326
+ return `${STORE_KEY_VERSION_PREFIX}${(0, openclaw_plugin_sdk_node_crypto_runtime.sha256Hex)(JSON.stringify([connectionName, userId]))}`;
3698
3327
  }
3699
3328
  function createMSTeamsSsoTokenStoreFs(params) {
3700
3329
  const namespace = require_delegated_state.resolveMSTeamsAccountStateNamespace(MSTEAMS_SSO_TOKENS_NAMESPACE, params?.accountId);
@@ -3707,12 +3336,10 @@ function createMSTeamsSsoTokenStoreFs(params) {
3707
3336
  return await tokenStore.lookup(makeMSTeamsSsoTokenStoreKey(connectionName, userId)) ?? null;
3708
3337
  },
3709
3338
  async save(token) {
3710
- await require_delegated_state.withMSTeamsSqliteMutationLock(namespace, async () => {
3711
- await tokenStore.register(makeMSTeamsSsoTokenStoreKey(token.connectionName, token.userId), require_delegated_state.toPluginJsonValue({ ...token }));
3712
- });
3339
+ await tokenStore.register(makeMSTeamsSsoTokenStoreKey(token.connectionName, token.userId), require_delegated_state.toPluginJsonValue(token));
3713
3340
  },
3714
3341
  async remove({ connectionName, userId }) {
3715
- return require_delegated_state.withMSTeamsSqliteMutationLock(namespace, () => tokenStore.delete(makeMSTeamsSsoTokenStoreKey(connectionName, userId)));
3342
+ return tokenStore.delete(makeMSTeamsSsoTokenStoreKey(connectionName, userId));
3716
3343
  }
3717
3344
  };
3718
3345
  }
@@ -3721,7 +3348,6 @@ function createMSTeamsSsoTokenStoreFs(params) {
3721
3348
  function createMSTeamsWebhookHandler(expressApp, warn) {
3722
3349
  const responses = /* @__PURE__ */ new Map();
3723
3350
  let closing = false;
3724
- let closeTask;
3725
3351
  return {
3726
3352
  handler: ((req, res) => {
3727
3353
  if (closing) {
@@ -3747,18 +3373,15 @@ function createMSTeamsWebhookHandler(expressApp, warn) {
3747
3373
  }
3748
3374
  return response.promise;
3749
3375
  }),
3750
- close: () => {
3376
+ close: async () => {
3751
3377
  closing = true;
3752
- closeTask ??= (async () => {
3753
- if (responses.size === 0) return;
3754
- try {
3755
- await (0, openclaw_plugin_sdk_time_runtime.withTimeout)(Promise.all(responses.values()), require_resolve_allowlist.MSTEAMS_REQUEST_TIMEOUT_MS, "Microsoft Teams webhook shutdown");
3756
- } catch {
3757
- warn("Microsoft Teams webhook shutdown exceeded 30 seconds; closing active responses");
3758
- for (const response of responses.keys()) response.destroy();
3759
- }
3760
- })();
3761
- return closeTask;
3378
+ if (responses.size === 0) return;
3379
+ try {
3380
+ await (0, openclaw_plugin_sdk_time_runtime.withTimeout)(Promise.all(responses.values()), require_resolve_allowlist.MSTEAMS_REQUEST_TIMEOUT_MS, "Microsoft Teams webhook shutdown");
3381
+ } catch {
3382
+ warn("Microsoft Teams webhook shutdown exceeded 30 seconds; closing active responses");
3383
+ for (const response of responses.keys()) response.destroy();
3384
+ }
3762
3385
  }
3763
3386
  };
3764
3387
  }
@@ -3773,19 +3396,13 @@ async function monitorMSTeamsProvider(opts) {
3773
3396
  let msteamsCfg = account.config;
3774
3397
  if (opts.cfg.channels?.msteams?.enabled !== true || msteamsCfg.enabled === false) {
3775
3398
  log.debug?.("msteams provider disabled");
3776
- publishMSTeamsBlocked(opts.statusSink, "Microsoft Teams provider is disabled");
3777
- return {
3778
- app: null,
3779
- shutdown: async () => {}
3780
- };
3399
+ opts.statusSink?.((0, openclaw_plugin_sdk_gateway_runtime.channelBlockedPatch)("Microsoft Teams provider is disabled", { running: true }));
3400
+ return;
3781
3401
  }
3782
3402
  if (!creds) {
3783
3403
  log.error("msteams credentials not configured");
3784
- publishMSTeamsBlocked(opts.statusSink, "Microsoft Teams credentials are not configured");
3785
- return {
3786
- app: null,
3787
- shutdown: async () => {}
3788
- };
3404
+ opts.statusSink?.((0, openclaw_plugin_sdk_gateway_runtime.channelBlockedPatch)("Microsoft Teams credentials are not configured", { running: true }));
3405
+ return;
3789
3406
  }
3790
3407
  const collisionIssue = require_channel.resolveMSTeamsWebhookCollisionIssue(accountPolicyCfg);
3791
3408
  if (collisionIssue) throw new Error(collisionIssue);
@@ -3894,7 +3511,8 @@ async function monitorMSTeamsProvider(opts) {
3894
3511
  cfg,
3895
3512
  resolveChannelLimitMb: ({ cfg: channelCfg }) => channelCfg.channels?.msteams?.mediaMaxMb
3896
3513
  }) ?? 8388608;
3897
- const { conversationStore, pollStore } = createMSTeamsMonitorStores(accountId, opts);
3514
+ const conversationStore = require_conversation_store_state.createMSTeamsConversationStoreState({ accountId });
3515
+ const pollStore = require_conversation_store_state.createMSTeamsPollStoreState({ accountId });
3898
3516
  log.info("starting provider on Gateway HTTP routes");
3899
3517
  const express = await import("express");
3900
3518
  const expressApp = express.default();
@@ -4146,26 +3764,15 @@ async function monitorMSTeamsProvider(opts) {
4146
3764
  throw error;
4147
3765
  }
4148
3766
  log.info(`msteams provider started on Gateway route ${configuredPath}`);
4149
- publishMSTeamsReady(opts.statusSink);
4150
- let shutdownTask;
4151
- const shutdown = () => {
4152
- shutdownTask ??= (async () => {
4153
- await webhook.close();
4154
- for (const unregister of unregisterRoutes.splice(0)) unregister();
4155
- await ingress.stop();
4156
- publishMSTeamsStopped(opts.statusSink);
4157
- })();
4158
- return shutdownTask;
4159
- };
3767
+ opts.statusSink?.((0, openclaw_plugin_sdk_gateway_runtime.channelReadyPatch)());
4160
3768
  try {
4161
- await (0, openclaw_plugin_sdk_channel_outbound.waitUntilAbort)(opts.abortSignal, shutdown);
3769
+ await (0, openclaw_plugin_sdk_channel_outbound.waitUntilAbort)(opts.abortSignal);
4162
3770
  } finally {
4163
- await shutdown();
3771
+ await webhook.close();
3772
+ for (const unregister of unregisterRoutes) unregister();
3773
+ await ingress.stop();
3774
+ opts.statusSink?.((0, openclaw_plugin_sdk_gateway_runtime.channelStoppedPatch)());
4164
3775
  }
4165
- return {
4166
- app: expressApp,
4167
- shutdown
4168
- };
4169
3776
  }
4170
3777
  function cardActionMessage(value) {
4171
3778
  return {
@@ -4204,18 +3811,10 @@ function adaptSdkContext(ctx, app) {
4204
3811
  const sendActivity = (activity) => isThreadable ? sdkCtx.reply(activity) : sdkCtx.send(activity);
4205
3812
  return Object.assign(Object.create(Object.getPrototypeOf(ctx)), ctx, {
4206
3813
  sendActivity,
4207
- sendActivities: async (activities) => {
4208
- const results = [];
4209
- for (const a of activities) results.push(await sendActivity(a));
4210
- return results;
4211
- },
4212
3814
  updateActivity: async (activity) => {
4213
3815
  const activityId = activity.id ?? "";
4214
3816
  return activityApi.conversations.activities(conversationId).update(activityId, activity);
4215
3817
  },
4216
- deleteActivity: async (activityId) => {
4217
- return activityApi.conversations.activities(conversationId).delete(activityId);
4218
- },
4219
3818
  getTeamDetails,
4220
3819
  stream: sdkCtx.stream
4221
3820
  });