@openclaw/msteams 2026.3.13 → 2026.5.2-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/api.ts +3 -0
  2. package/channel-config-api.ts +1 -0
  3. package/channel-plugin-api.ts +2 -0
  4. package/config-api.ts +4 -0
  5. package/contract-api.ts +4 -0
  6. package/index.ts +15 -12
  7. package/openclaw.plugin.json +553 -1
  8. package/package.json +46 -12
  9. package/runtime-api.ts +73 -0
  10. package/secret-contract-api.ts +5 -0
  11. package/setup-entry.ts +13 -0
  12. package/setup-plugin-api.ts +3 -0
  13. package/src/ai-entity.ts +7 -0
  14. package/src/approval-auth.ts +44 -0
  15. package/src/attachments/bot-framework.test.ts +461 -0
  16. package/src/attachments/bot-framework.ts +362 -0
  17. package/src/attachments/download.ts +63 -19
  18. package/src/attachments/graph.test.ts +416 -0
  19. package/src/attachments/graph.ts +163 -72
  20. package/src/attachments/html.ts +33 -1
  21. package/src/attachments/payload.ts +1 -1
  22. package/src/attachments/remote-media.test.ts +137 -0
  23. package/src/attachments/remote-media.ts +75 -8
  24. package/src/attachments/shared.test.ts +138 -1
  25. package/src/attachments/shared.ts +193 -26
  26. package/src/attachments/types.ts +10 -0
  27. package/src/attachments.graph.test.ts +342 -0
  28. package/src/attachments.helpers.test.ts +246 -0
  29. package/src/attachments.test-helpers.ts +17 -0
  30. package/src/attachments.test.ts +163 -418
  31. package/src/attachments.ts +5 -5
  32. package/src/block-streaming-config.test.ts +61 -0
  33. package/src/channel-api.ts +1 -0
  34. package/src/channel.actions.test.ts +742 -0
  35. package/src/channel.directory.test.ts +145 -4
  36. package/src/channel.runtime.ts +56 -0
  37. package/src/channel.setup.ts +77 -0
  38. package/src/channel.test.ts +128 -0
  39. package/src/channel.ts +1077 -395
  40. package/src/config-schema.ts +6 -0
  41. package/src/config-ui-hints.ts +12 -0
  42. package/src/conversation-store-fs.test.ts +4 -5
  43. package/src/conversation-store-fs.ts +35 -51
  44. package/src/conversation-store-helpers.test.ts +202 -0
  45. package/src/conversation-store-helpers.ts +105 -0
  46. package/src/conversation-store-memory.ts +27 -23
  47. package/src/conversation-store.shared.test.ts +225 -0
  48. package/src/conversation-store.ts +30 -0
  49. package/src/directory-live.test.ts +156 -0
  50. package/src/directory-live.ts +7 -4
  51. package/src/doctor.ts +27 -0
  52. package/src/errors.test.ts +64 -1
  53. package/src/errors.ts +50 -9
  54. package/src/feedback-reflection-prompt.ts +117 -0
  55. package/src/feedback-reflection-store.ts +114 -0
  56. package/src/feedback-reflection.test.ts +237 -0
  57. package/src/feedback-reflection.ts +283 -0
  58. package/src/file-consent-helpers.test.ts +83 -0
  59. package/src/file-consent-helpers.ts +64 -11
  60. package/src/file-consent-invoke.ts +150 -0
  61. package/src/file-consent.test.ts +363 -0
  62. package/src/file-consent.ts +165 -4
  63. package/src/graph-chat.ts +5 -3
  64. package/src/graph-group-management.test.ts +318 -0
  65. package/src/graph-group-management.ts +168 -0
  66. package/src/graph-members.test.ts +89 -0
  67. package/src/graph-members.ts +48 -0
  68. package/src/graph-messages.actions.test.ts +243 -0
  69. package/src/graph-messages.read.test.ts +391 -0
  70. package/src/graph-messages.search.test.ts +213 -0
  71. package/src/graph-messages.test-helpers.ts +50 -0
  72. package/src/graph-messages.ts +534 -0
  73. package/src/graph-teams.test.ts +215 -0
  74. package/src/graph-teams.ts +114 -0
  75. package/src/graph-thread.test.ts +246 -0
  76. package/src/graph-thread.ts +146 -0
  77. package/src/graph-upload.test.ts +161 -4
  78. package/src/graph-upload.ts +147 -56
  79. package/src/graph.test.ts +516 -0
  80. package/src/graph.ts +233 -21
  81. package/src/inbound.test.ts +156 -1
  82. package/src/inbound.ts +101 -1
  83. package/src/media-helpers.ts +1 -1
  84. package/src/mentions.test.ts +27 -18
  85. package/src/mentions.ts +2 -2
  86. package/src/messenger.test.ts +504 -23
  87. package/src/messenger.ts +133 -52
  88. package/src/monitor-handler/access.ts +125 -0
  89. package/src/monitor-handler/inbound-media.test.ts +289 -0
  90. package/src/monitor-handler/inbound-media.ts +57 -5
  91. package/src/monitor-handler/message-handler-mock-support.test-support.ts +28 -0
  92. package/src/monitor-handler/message-handler.authz.test.ts +588 -74
  93. package/src/monitor-handler/message-handler.dm-media.test.ts +54 -0
  94. package/src/monitor-handler/message-handler.test-support.ts +100 -0
  95. package/src/monitor-handler/message-handler.thread-parent.test.ts +223 -0
  96. package/src/monitor-handler/message-handler.thread-session.test.ts +77 -0
  97. package/src/monitor-handler/message-handler.ts +470 -164
  98. package/src/monitor-handler/reaction-handler.test.ts +267 -0
  99. package/src/monitor-handler/reaction-handler.ts +210 -0
  100. package/src/monitor-handler/thread-session.ts +17 -0
  101. package/src/monitor-handler.adaptive-card.test.ts +162 -0
  102. package/src/monitor-handler.feedback-authz.test.ts +314 -0
  103. package/src/monitor-handler.file-consent.test.ts +281 -79
  104. package/src/monitor-handler.sso.test.ts +563 -0
  105. package/src/monitor-handler.test-helpers.ts +180 -0
  106. package/src/monitor-handler.ts +459 -115
  107. package/src/monitor-handler.types.ts +27 -0
  108. package/src/monitor-types.ts +1 -0
  109. package/src/monitor.lifecycle.test.ts +74 -10
  110. package/src/monitor.test.ts +35 -1
  111. package/src/monitor.ts +143 -46
  112. package/src/oauth.flow.ts +77 -0
  113. package/src/oauth.shared.ts +37 -0
  114. package/src/oauth.test.ts +305 -0
  115. package/src/oauth.token.ts +158 -0
  116. package/src/oauth.ts +130 -0
  117. package/src/outbound.test.ts +10 -11
  118. package/src/outbound.ts +62 -44
  119. package/src/pending-uploads-fs.test.ts +246 -0
  120. package/src/pending-uploads-fs.ts +235 -0
  121. package/src/pending-uploads.test.ts +173 -0
  122. package/src/pending-uploads.ts +34 -2
  123. package/src/policy.test.ts +11 -5
  124. package/src/policy.ts +5 -5
  125. package/src/polls.test.ts +106 -5
  126. package/src/polls.ts +15 -7
  127. package/src/presentation.ts +68 -0
  128. package/src/probe.test.ts +27 -8
  129. package/src/probe.ts +43 -9
  130. package/src/reply-dispatcher.test.ts +437 -0
  131. package/src/reply-dispatcher.ts +259 -73
  132. package/src/reply-stream-controller.test.ts +235 -0
  133. package/src/reply-stream-controller.ts +147 -0
  134. package/src/resolve-allowlist.test.ts +105 -1
  135. package/src/resolve-allowlist.ts +112 -7
  136. package/src/runtime.ts +6 -3
  137. package/src/sdk-types.ts +43 -3
  138. package/src/sdk.test.ts +666 -0
  139. package/src/sdk.ts +867 -16
  140. package/src/secret-contract.ts +49 -0
  141. package/src/secret-input.ts +1 -1
  142. package/src/send-context.ts +76 -9
  143. package/src/send.test.ts +389 -5
  144. package/src/send.ts +140 -32
  145. package/src/sent-message-cache.ts +30 -18
  146. package/src/session-route.ts +40 -0
  147. package/src/setup-core.ts +160 -0
  148. package/src/setup-surface.test.ts +202 -0
  149. package/src/setup-surface.ts +320 -0
  150. package/src/sso-token-store.test.ts +72 -0
  151. package/src/sso-token-store.ts +166 -0
  152. package/src/sso.ts +300 -0
  153. package/src/storage.ts +1 -1
  154. package/src/store-fs.ts +2 -2
  155. package/src/streaming-message.test.ts +262 -0
  156. package/src/streaming-message.ts +297 -0
  157. package/src/test-runtime.ts +1 -1
  158. package/src/thread-parent-context.test.ts +224 -0
  159. package/src/thread-parent-context.ts +159 -0
  160. package/src/token.test.ts +237 -50
  161. package/src/token.ts +162 -7
  162. package/src/user-agent.test.ts +86 -0
  163. package/src/user-agent.ts +53 -0
  164. package/src/webhook-timeouts.ts +27 -0
  165. package/src/welcome-card.test.ts +81 -0
  166. package/src/welcome-card.ts +57 -0
  167. package/test-api.ts +1 -0
  168. package/tsconfig.json +16 -0
  169. package/CHANGELOG.md +0 -107
  170. package/src/file-lock.ts +0 -1
  171. package/src/graph-users.test.ts +0 -66
  172. package/src/onboarding.ts +0 -381
  173. package/src/polls-store.test.ts +0 -38
  174. package/src/revoked-context.test.ts +0 -39
  175. package/src/token-response.test.ts +0 -23
package/src/send.ts CHANGED
@@ -1,12 +1,12 @@
1
- import type { OpenClawConfig } from "openclaw/plugin-sdk/msteams";
2
- import { loadOutboundMediaFromUrl } from "openclaw/plugin-sdk/msteams";
3
- import { createMSTeamsConversationStoreFs } from "./conversation-store-fs.js";
1
+ import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/markdown-table-runtime";
2
+ import { convertMarkdownTables } from "openclaw/plugin-sdk/text-runtime";
3
+ import { loadOutboundMediaFromUrl, type OpenClawConfig } from "../runtime-api.js";
4
4
  import {
5
5
  classifyMSTeamsSendError,
6
6
  formatMSTeamsSendErrorHint,
7
7
  formatUnknownError,
8
8
  } from "./errors.js";
9
- import { prepareFileConsentActivity, requiresFileConsent } from "./file-consent-helpers.js";
9
+ import { prepareFileConsentActivityFs, requiresFileConsent } from "./file-consent-helpers.js";
10
10
  import { buildTeamsFileInfoCard } from "./graph-chat.js";
11
11
  import {
12
12
  getDriveItemProperties,
@@ -15,11 +15,12 @@ import {
15
15
  } from "./graph-upload.js";
16
16
  import { extractFilename, extractMessageId } from "./media-helpers.js";
17
17
  import { buildConversationReference, sendMSTeamsMessages } from "./messenger.js";
18
+ import { setPendingUploadActivityIdFs } from "./pending-uploads-fs.js";
19
+ import { setPendingUploadActivityId } from "./pending-uploads.js";
18
20
  import { buildMSTeamsPollCard } from "./polls.js";
19
- import { getMSTeamsRuntime } from "./runtime.js";
20
21
  import { resolveMSTeamsSendContext, type MSTeamsProactiveContext } from "./send-context.js";
21
22
 
22
- export type SendMSTeamsMessageParams = {
23
+ type SendMSTeamsMessageParams = {
23
24
  /** Full config (for credentials) */
24
25
  cfg: OpenClawConfig;
25
26
  /** Conversation ID or user ID to send to */
@@ -28,10 +29,13 @@ export type SendMSTeamsMessageParams = {
28
29
  text: string;
29
30
  /** Optional media URL */
30
31
  mediaUrl?: string;
32
+ /** Optional filename override for uploaded media/files */
33
+ filename?: string;
31
34
  mediaLocalRoots?: readonly string[];
35
+ mediaReadFile?: (filePath: string) => Promise<Buffer>;
32
36
  };
33
37
 
34
- export type SendMSTeamsMessageResult = {
38
+ type SendMSTeamsMessageResult = {
35
39
  messageId: string;
36
40
  conversationId: string;
37
41
  /** If a FileConsentCard was sent instead of the file, this contains the upload ID */
@@ -47,7 +51,7 @@ const FILE_CONSENT_THRESHOLD_BYTES = 4 * 1024 * 1024; // 4MB
47
51
  */
48
52
  const MSTEAMS_MAX_MEDIA_BYTES = 100 * 1024 * 1024;
49
53
 
50
- export type SendMSTeamsPollParams = {
54
+ type SendMSTeamsPollParams = {
51
55
  /** Full config (for credentials) */
52
56
  cfg: OpenClawConfig;
53
57
  /** Conversation ID or user ID to send to */
@@ -60,13 +64,13 @@ export type SendMSTeamsPollParams = {
60
64
  maxSelections?: number;
61
65
  };
62
66
 
63
- export type SendMSTeamsPollResult = {
67
+ type SendMSTeamsPollResult = {
64
68
  pollId: string;
65
69
  messageId: string;
66
70
  conversationId: string;
67
71
  };
68
72
 
69
- export type SendMSTeamsCardParams = {
73
+ type SendMSTeamsCardParams = {
70
74
  /** Full config (for credentials) */
71
75
  cfg: OpenClawConfig;
72
76
  /** Conversation ID or user ID to send to */
@@ -75,7 +79,7 @@ export type SendMSTeamsCardParams = {
75
79
  card: Record<string, unknown>;
76
80
  };
77
81
 
78
- export type SendMSTeamsCardResult = {
82
+ type SendMSTeamsCardResult = {
79
83
  messageId: string;
80
84
  conversationId: string;
81
85
  };
@@ -94,12 +98,12 @@ export type SendMSTeamsCardResult = {
94
98
  export async function sendMessageMSTeams(
95
99
  params: SendMSTeamsMessageParams,
96
100
  ): Promise<SendMSTeamsMessageResult> {
97
- const { cfg, to, text, mediaUrl, mediaLocalRoots } = params;
98
- const tableMode = getMSTeamsRuntime().channel.text.resolveMarkdownTableMode({
101
+ const { cfg, to, text, mediaUrl, filename, mediaLocalRoots, mediaReadFile } = params;
102
+ const tableMode = resolveMarkdownTableMode({
99
103
  cfg,
100
104
  channel: "msteams",
101
105
  });
102
- const messageText = getMSTeamsRuntime().channel.text.convertMarkdownTables(text ?? "", tableMode);
106
+ const messageText = convertMarkdownTables(text ?? "", tableMode);
103
107
  const ctx = await resolveMSTeamsSendContext({ cfg, to });
104
108
  const {
105
109
  adapter,
@@ -125,11 +129,12 @@ export async function sendMessageMSTeams(
125
129
  const media = await loadOutboundMediaFromUrl(mediaUrl, {
126
130
  maxBytes: mediaMaxBytes,
127
131
  mediaLocalRoots,
132
+ mediaReadFile,
128
133
  });
129
134
  const isLargeFile = media.buffer.length >= FILE_CONSENT_THRESHOLD_BYTES;
130
135
  const isImage = media.contentType?.startsWith("image/") ?? false;
131
136
  const fallbackFileName = await extractFilename(mediaUrl);
132
- const fileName = media.fileName ?? fallbackFileName;
137
+ const fileName = filename?.trim() || media.fileName || fallbackFileName;
133
138
 
134
139
  log.debug?.("processing media", {
135
140
  fileName,
@@ -149,7 +154,11 @@ export async function sendMessageMSTeams(
149
154
  thresholdBytes: FILE_CONSENT_THRESHOLD_BYTES,
150
155
  })
151
156
  ) {
152
- const { activity, uploadId } = prepareFileConsentActivity({
157
+ // Proactive CLI sends run in a different process from the gateway's
158
+ // monitor that receives the fileConsent/invoke callback. Use the FS-
159
+ // backed helper so the invoke handler can find the pending upload when
160
+ // the user clicks "Allow".
161
+ const { activity, uploadId } = await prepareFileConsentActivityFs({
153
162
  media: { buffer: media.buffer, filename: fileName, contentType: media.contentType },
154
163
  conversationId,
155
164
  description: messageText || undefined,
@@ -165,6 +174,12 @@ export async function sendMessageMSTeams(
165
174
  errorPrefix: "msteams consent card send",
166
175
  });
167
176
 
177
+ // Store the activity ID so the accept handler can replace the consent
178
+ // card in-place. Mirror it into the FS store too because the invoke
179
+ // callback may be delivered to a different process than the CLI send.
180
+ setPendingUploadActivityId(uploadId, messageId);
181
+ await setPendingUploadActivityIdFs(uploadId, messageId);
182
+
168
183
  log.info("sent file consent card", { conversationId, messageId, uploadId });
169
184
 
170
185
  return {
@@ -206,7 +221,9 @@ export async function sendMessageMSTeams(
206
221
  contentType: media.contentType,
207
222
  tokenProvider,
208
223
  siteId: sharePointSiteId,
209
- chatId: conversationId,
224
+ // Use the Graph-native chat ID (19:xxx format) — the Bot Framework conversationId
225
+ // for personal DMs uses a different format that Graph API rejects.
226
+ chatId: ctx.graphChatId ?? conversationId,
210
227
  usePerUserSharing: conversationType === "groupChat",
211
228
  });
212
229
 
@@ -509,21 +526,112 @@ export async function sendAdaptiveCardMSTeams(
509
526
  };
510
527
  }
511
528
 
529
+ type EditMSTeamsMessageParams = {
530
+ /** Full config (for credentials) */
531
+ cfg: OpenClawConfig;
532
+ /** Conversation ID or user ID */
533
+ to: string;
534
+ /** Activity ID of the message to edit */
535
+ activityId: string;
536
+ /** New message text */
537
+ text: string;
538
+ };
539
+
540
+ type EditMSTeamsMessageResult = {
541
+ conversationId: string;
542
+ };
543
+
544
+ type DeleteMSTeamsMessageParams = {
545
+ /** Full config (for credentials) */
546
+ cfg: OpenClawConfig;
547
+ /** Conversation ID or user ID */
548
+ to: string;
549
+ /** Activity ID of the message to delete */
550
+ activityId: string;
551
+ };
552
+
553
+ type DeleteMSTeamsMessageResult = {
554
+ conversationId: string;
555
+ };
556
+
512
557
  /**
513
- * List all known conversation references (for debugging/CLI).
558
+ * Edit (update) a previously sent message in a Teams conversation.
559
+ *
560
+ * Uses the Bot Framework `continueConversation` → `updateActivity` flow
561
+ * for proactive edits outside of the original turn context.
514
562
  */
515
- export async function listMSTeamsConversations(): Promise<
516
- Array<{
517
- conversationId: string;
518
- userName?: string;
519
- conversationType?: string;
520
- }>
521
- > {
522
- const store = createMSTeamsConversationStoreFs();
523
- const all = await store.list();
524
- return all.map(({ conversationId, reference }) => ({
525
- conversationId,
526
- userName: reference.user?.name,
527
- conversationType: reference.conversation?.conversationType,
528
- }));
563
+ export async function editMessageMSTeams(
564
+ params: EditMSTeamsMessageParams,
565
+ ): Promise<EditMSTeamsMessageResult> {
566
+ const { cfg, to, activityId, text } = params;
567
+ const { adapter, appId, conversationId, ref, log } = await resolveMSTeamsSendContext({
568
+ cfg,
569
+ to,
570
+ });
571
+
572
+ log.debug?.("editing proactive message", { conversationId, activityId, textLength: text.length });
573
+
574
+ const baseRef = buildConversationReference(ref);
575
+ const proactiveRef = { ...baseRef, activityId: undefined };
576
+
577
+ try {
578
+ await adapter.continueConversation(appId, proactiveRef, async (ctx) => {
579
+ await ctx.updateActivity({
580
+ type: "message",
581
+ id: activityId,
582
+ text,
583
+ });
584
+ });
585
+ } catch (err) {
586
+ const classification = classifyMSTeamsSendError(err);
587
+ const hint = formatMSTeamsSendErrorHint(classification);
588
+ const status = classification.statusCode ? ` (HTTP ${classification.statusCode})` : "";
589
+ throw new Error(
590
+ `msteams edit failed${status}: ${formatUnknownError(err)}${hint ? ` (${hint})` : ""}`,
591
+ { cause: err },
592
+ );
593
+ }
594
+
595
+ log.info("edited proactive message", { conversationId, activityId });
596
+
597
+ return { conversationId };
598
+ }
599
+
600
+ /**
601
+ * Delete a previously sent message in a Teams conversation.
602
+ *
603
+ * Uses the Bot Framework `continueConversation` → `deleteActivity` flow
604
+ * for proactive deletes outside of the original turn context.
605
+ */
606
+ export async function deleteMessageMSTeams(
607
+ params: DeleteMSTeamsMessageParams,
608
+ ): Promise<DeleteMSTeamsMessageResult> {
609
+ const { cfg, to, activityId } = params;
610
+ const { adapter, appId, conversationId, ref, log } = await resolveMSTeamsSendContext({
611
+ cfg,
612
+ to,
613
+ });
614
+
615
+ log.debug?.("deleting proactive message", { conversationId, activityId });
616
+
617
+ const baseRef = buildConversationReference(ref);
618
+ const proactiveRef = { ...baseRef, activityId: undefined };
619
+
620
+ try {
621
+ await adapter.continueConversation(appId, proactiveRef, async (ctx) => {
622
+ await ctx.deleteActivity(activityId);
623
+ });
624
+ } catch (err) {
625
+ const classification = classifyMSTeamsSendError(err);
626
+ const hint = formatMSTeamsSendErrorHint(classification);
627
+ const status = classification.statusCode ? ` (HTTP ${classification.statusCode})` : "";
628
+ throw new Error(
629
+ `msteams delete failed${status}: ${formatUnknownError(err)}${hint ? ` (${hint})` : ""}`,
630
+ { cause: err },
631
+ );
632
+ }
633
+
634
+ log.info("deleted proactive message", { conversationId, activityId });
635
+
636
+ return { conversationId };
529
637
  }
@@ -1,44 +1,56 @@
1
1
  const TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
2
+ const MSTEAMS_SENT_MESSAGES_KEY = Symbol.for("openclaw.msteamsSentMessages");
2
3
 
3
- type CacheEntry = {
4
- timestamps: Map<string, number>;
5
- };
4
+ let sentMessageCache: Map<string, Map<string, number>> | undefined;
6
5
 
7
- const sentMessages = new Map<string, CacheEntry>();
6
+ function getSentMessageCache(): Map<string, Map<string, number>> {
7
+ if (!sentMessageCache) {
8
+ const globalStore = globalThis as Record<PropertyKey, unknown>;
9
+ sentMessageCache =
10
+ (globalStore[MSTEAMS_SENT_MESSAGES_KEY] as Map<string, Map<string, number>> | undefined) ??
11
+ new Map<string, Map<string, number>>();
12
+ globalStore[MSTEAMS_SENT_MESSAGES_KEY] = sentMessageCache;
13
+ }
14
+ return sentMessageCache;
15
+ }
8
16
 
9
- function cleanupExpired(entry: CacheEntry): void {
10
- const now = Date.now();
11
- for (const [msgId, timestamp] of entry.timestamps) {
17
+ function cleanupExpired(scopeKey: string, entry: Map<string, number>, now: number): void {
18
+ for (const [id, timestamp] of entry) {
12
19
  if (now - timestamp > TTL_MS) {
13
- entry.timestamps.delete(msgId);
20
+ entry.delete(id);
14
21
  }
15
22
  }
23
+ if (entry.size === 0) {
24
+ getSentMessageCache().delete(scopeKey);
25
+ }
16
26
  }
17
27
 
18
28
  export function recordMSTeamsSentMessage(conversationId: string, messageId: string): void {
19
29
  if (!conversationId || !messageId) {
20
30
  return;
21
31
  }
22
- let entry = sentMessages.get(conversationId);
32
+ const now = Date.now();
33
+ const store = getSentMessageCache();
34
+ let entry = store.get(conversationId);
23
35
  if (!entry) {
24
- entry = { timestamps: new Map() };
25
- sentMessages.set(conversationId, entry);
36
+ entry = new Map<string, number>();
37
+ store.set(conversationId, entry);
26
38
  }
27
- entry.timestamps.set(messageId, Date.now());
28
- if (entry.timestamps.size > 200) {
29
- cleanupExpired(entry);
39
+ entry.set(messageId, now);
40
+ if (entry.size > 200) {
41
+ cleanupExpired(conversationId, entry, now);
30
42
  }
31
43
  }
32
44
 
33
45
  export function wasMSTeamsMessageSent(conversationId: string, messageId: string): boolean {
34
- const entry = sentMessages.get(conversationId);
46
+ const entry = getSentMessageCache().get(conversationId);
35
47
  if (!entry) {
36
48
  return false;
37
49
  }
38
- cleanupExpired(entry);
39
- return entry.timestamps.has(messageId);
50
+ cleanupExpired(conversationId, entry, Date.now());
51
+ return entry.has(messageId);
40
52
  }
41
53
 
42
54
  export function clearMSTeamsSentMessageCache(): void {
43
- sentMessages.clear();
55
+ getSentMessageCache().clear();
44
56
  }
@@ -0,0 +1,40 @@
1
+ import {
2
+ buildChannelOutboundSessionRoute,
3
+ stripChannelTargetPrefix,
4
+ stripTargetKindPrefix,
5
+ type ChannelOutboundSessionRouteParams,
6
+ } from "openclaw/plugin-sdk/channel-core";
7
+ import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
8
+
9
+ export function resolveMSTeamsOutboundSessionRoute(params: ChannelOutboundSessionRouteParams) {
10
+ let trimmed = stripChannelTargetPrefix(params.target, "msteams", "teams");
11
+ if (!trimmed) {
12
+ return null;
13
+ }
14
+
15
+ const lower = normalizeLowercaseStringOrEmpty(trimmed);
16
+ const isUser = lower.startsWith("user:");
17
+ const rawId = stripTargetKindPrefix(trimmed);
18
+ if (!rawId) {
19
+ return null;
20
+ }
21
+ const conversationId = rawId.split(";")[0] ?? rawId;
22
+ const isChannel = !isUser && /@thread\.tacv2/i.test(conversationId);
23
+ return buildChannelOutboundSessionRoute({
24
+ cfg: params.cfg,
25
+ agentId: params.agentId,
26
+ channel: "msteams",
27
+ accountId: params.accountId,
28
+ peer: {
29
+ kind: isUser ? "direct" : isChannel ? "channel" : "group",
30
+ id: conversationId,
31
+ },
32
+ chatType: isUser ? "direct" : isChannel ? "channel" : "group",
33
+ from: isUser
34
+ ? `msteams:${conversationId}`
35
+ : isChannel
36
+ ? `msteams:channel:${conversationId}`
37
+ : `msteams:group:${conversationId}`,
38
+ to: isUser ? `user:${conversationId}` : `conversation:${conversationId}`,
39
+ });
40
+ }
@@ -0,0 +1,160 @@
1
+ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
2
+ import {
3
+ createStandardChannelSetupStatus,
4
+ DEFAULT_ACCOUNT_ID,
5
+ type ChannelSetupAdapter,
6
+ type ChannelSetupWizard,
7
+ type WizardPrompter,
8
+ } from "openclaw/plugin-sdk/setup";
9
+ import { formatDocsLink } from "openclaw/plugin-sdk/setup-tools";
10
+ import { normalizeSecretInputString } from "./secret-input.js";
11
+ import { hasConfiguredMSTeamsCredentials, resolveMSTeamsCredentials } from "./token.js";
12
+
13
+ export const msteamsSetupAdapter: ChannelSetupAdapter = {
14
+ resolveAccountId: () => DEFAULT_ACCOUNT_ID,
15
+ applyAccountConfig: ({ cfg }) => ({
16
+ ...cfg,
17
+ channels: {
18
+ ...cfg.channels,
19
+ msteams: {
20
+ ...cfg.channels?.msteams,
21
+ enabled: true,
22
+ },
23
+ },
24
+ }),
25
+ };
26
+
27
+ const channel = "msteams" as const;
28
+
29
+ async function promptMSTeamsCredentials(prompter: WizardPrompter): Promise<{
30
+ appId: string;
31
+ appPassword: string;
32
+ tenantId: string;
33
+ }> {
34
+ const appId = (
35
+ await prompter.text({
36
+ message: "Enter MS Teams App ID",
37
+ validate: (value) => (value?.trim() ? undefined : "Required"),
38
+ })
39
+ ).trim();
40
+ const appPassword = (
41
+ await prompter.text({
42
+ message: "Enter MS Teams App Password",
43
+ validate: (value) => (value?.trim() ? undefined : "Required"),
44
+ })
45
+ ).trim();
46
+ const tenantId = (
47
+ await prompter.text({
48
+ message: "Enter MS Teams Tenant ID",
49
+ validate: (value) => (value?.trim() ? undefined : "Required"),
50
+ })
51
+ ).trim();
52
+ return { appId, appPassword, tenantId };
53
+ }
54
+
55
+ async function noteMSTeamsCredentialHelp(prompter: WizardPrompter): Promise<void> {
56
+ await prompter.note(
57
+ [
58
+ "1) Azure Bot registration -> get App ID + Tenant ID",
59
+ "2) Add a client secret (App Password)",
60
+ "3) Set webhook URL + messaging endpoint",
61
+ "Tip: you can also set MSTEAMS_APP_ID / MSTEAMS_APP_PASSWORD / MSTEAMS_TENANT_ID.",
62
+ `Docs: ${formatDocsLink("/channels/msteams", "msteams")}`,
63
+ ].join("\n"),
64
+ "MS Teams credentials",
65
+ );
66
+ }
67
+
68
+ export function createMSTeamsSetupWizardBase(): Pick<
69
+ ChannelSetupWizard,
70
+ | "channel"
71
+ | "resolveAccountIdForConfigure"
72
+ | "resolveShouldPromptAccountIds"
73
+ | "status"
74
+ | "credentials"
75
+ | "finalize"
76
+ > {
77
+ return {
78
+ channel,
79
+ resolveAccountIdForConfigure: () => DEFAULT_ACCOUNT_ID,
80
+ resolveShouldPromptAccountIds: () => false,
81
+ status: createStandardChannelSetupStatus({
82
+ channelLabel: "MS Teams",
83
+ configuredLabel: "configured",
84
+ unconfiguredLabel: "needs app credentials",
85
+ configuredHint: "configured",
86
+ unconfiguredHint: "needs app creds",
87
+ configuredScore: 2,
88
+ unconfiguredScore: 0,
89
+ includeStatusLine: true,
90
+ resolveConfigured: ({ cfg }) =>
91
+ Boolean(resolveMSTeamsCredentials(cfg.channels?.msteams)) ||
92
+ hasConfiguredMSTeamsCredentials(cfg.channels?.msteams),
93
+ }),
94
+ credentials: [],
95
+ finalize: async ({ cfg, prompter }) => {
96
+ const resolved = resolveMSTeamsCredentials(cfg.channels?.msteams);
97
+ const hasConfigCreds = hasConfiguredMSTeamsCredentials(cfg.channels?.msteams);
98
+ const canUseEnv = Boolean(
99
+ !hasConfigCreds &&
100
+ normalizeSecretInputString(process.env.MSTEAMS_APP_ID) &&
101
+ normalizeSecretInputString(process.env.MSTEAMS_APP_PASSWORD) &&
102
+ normalizeSecretInputString(process.env.MSTEAMS_TENANT_ID),
103
+ );
104
+
105
+ let next: OpenClawConfig = cfg;
106
+ let appId: string | null = null;
107
+ let appPassword: string | null = null;
108
+ let tenantId: string | null = null;
109
+
110
+ if (!resolved && !hasConfigCreds) {
111
+ await noteMSTeamsCredentialHelp(prompter);
112
+ }
113
+
114
+ if (canUseEnv) {
115
+ const keepEnv = await prompter.confirm({
116
+ message:
117
+ "MSTEAMS_APP_ID + MSTEAMS_APP_PASSWORD + MSTEAMS_TENANT_ID detected. Use env vars?",
118
+ initialValue: true,
119
+ });
120
+ if (keepEnv) {
121
+ next = msteamsSetupAdapter.applyAccountConfig({
122
+ cfg: next,
123
+ accountId: DEFAULT_ACCOUNT_ID,
124
+ input: {},
125
+ });
126
+ } else {
127
+ ({ appId, appPassword, tenantId } = await promptMSTeamsCredentials(prompter));
128
+ }
129
+ } else if (hasConfigCreds) {
130
+ const keep = await prompter.confirm({
131
+ message: "MS Teams credentials already configured. Keep them?",
132
+ initialValue: true,
133
+ });
134
+ if (!keep) {
135
+ ({ appId, appPassword, tenantId } = await promptMSTeamsCredentials(prompter));
136
+ }
137
+ } else {
138
+ ({ appId, appPassword, tenantId } = await promptMSTeamsCredentials(prompter));
139
+ }
140
+
141
+ if (appId && appPassword && tenantId) {
142
+ next = {
143
+ ...next,
144
+ channels: {
145
+ ...next.channels,
146
+ msteams: {
147
+ ...next.channels?.msteams,
148
+ enabled: true,
149
+ appId,
150
+ appPassword,
151
+ tenantId,
152
+ },
153
+ },
154
+ };
155
+ }
156
+
157
+ return { cfg: next, accountId: DEFAULT_ACCOUNT_ID };
158
+ },
159
+ };
160
+ }