@openclaw/msteams 2026.11.3 → 2026.11.5-beta.2

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