@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.
- package/api.ts +3 -0
- package/channel-config-api.ts +1 -0
- package/channel-plugin-api.ts +2 -0
- package/config-api.ts +4 -0
- package/contract-api.ts +4 -0
- package/index.ts +15 -12
- package/openclaw.plugin.json +553 -1
- package/package.json +46 -12
- package/runtime-api.ts +73 -0
- package/secret-contract-api.ts +5 -0
- package/setup-entry.ts +13 -0
- package/setup-plugin-api.ts +3 -0
- package/src/ai-entity.ts +7 -0
- package/src/approval-auth.ts +44 -0
- package/src/attachments/bot-framework.test.ts +461 -0
- package/src/attachments/bot-framework.ts +362 -0
- package/src/attachments/download.ts +63 -19
- package/src/attachments/graph.test.ts +416 -0
- package/src/attachments/graph.ts +163 -72
- package/src/attachments/html.ts +33 -1
- package/src/attachments/payload.ts +1 -1
- package/src/attachments/remote-media.test.ts +137 -0
- package/src/attachments/remote-media.ts +75 -8
- package/src/attachments/shared.test.ts +138 -1
- package/src/attachments/shared.ts +193 -26
- package/src/attachments/types.ts +10 -0
- package/src/attachments.graph.test.ts +342 -0
- package/src/attachments.helpers.test.ts +246 -0
- package/src/attachments.test-helpers.ts +17 -0
- package/src/attachments.test.ts +163 -418
- package/src/attachments.ts +5 -5
- package/src/block-streaming-config.test.ts +61 -0
- package/src/channel-api.ts +1 -0
- package/src/channel.actions.test.ts +742 -0
- package/src/channel.directory.test.ts +145 -4
- package/src/channel.runtime.ts +56 -0
- package/src/channel.setup.ts +77 -0
- package/src/channel.test.ts +128 -0
- package/src/channel.ts +1077 -395
- package/src/config-schema.ts +6 -0
- package/src/config-ui-hints.ts +12 -0
- package/src/conversation-store-fs.test.ts +4 -5
- package/src/conversation-store-fs.ts +35 -51
- package/src/conversation-store-helpers.test.ts +202 -0
- package/src/conversation-store-helpers.ts +105 -0
- package/src/conversation-store-memory.ts +27 -23
- package/src/conversation-store.shared.test.ts +225 -0
- package/src/conversation-store.ts +30 -0
- package/src/directory-live.test.ts +156 -0
- package/src/directory-live.ts +7 -4
- package/src/doctor.ts +27 -0
- package/src/errors.test.ts +64 -1
- package/src/errors.ts +50 -9
- package/src/feedback-reflection-prompt.ts +117 -0
- package/src/feedback-reflection-store.ts +114 -0
- package/src/feedback-reflection.test.ts +237 -0
- package/src/feedback-reflection.ts +283 -0
- package/src/file-consent-helpers.test.ts +83 -0
- package/src/file-consent-helpers.ts +64 -11
- package/src/file-consent-invoke.ts +150 -0
- package/src/file-consent.test.ts +363 -0
- package/src/file-consent.ts +165 -4
- package/src/graph-chat.ts +5 -3
- package/src/graph-group-management.test.ts +318 -0
- package/src/graph-group-management.ts +168 -0
- package/src/graph-members.test.ts +89 -0
- package/src/graph-members.ts +48 -0
- package/src/graph-messages.actions.test.ts +243 -0
- package/src/graph-messages.read.test.ts +391 -0
- package/src/graph-messages.search.test.ts +213 -0
- package/src/graph-messages.test-helpers.ts +50 -0
- package/src/graph-messages.ts +534 -0
- package/src/graph-teams.test.ts +215 -0
- package/src/graph-teams.ts +114 -0
- package/src/graph-thread.test.ts +246 -0
- package/src/graph-thread.ts +146 -0
- package/src/graph-upload.test.ts +161 -4
- package/src/graph-upload.ts +147 -56
- package/src/graph.test.ts +516 -0
- package/src/graph.ts +233 -21
- package/src/inbound.test.ts +156 -1
- package/src/inbound.ts +101 -1
- package/src/media-helpers.ts +1 -1
- package/src/mentions.test.ts +27 -18
- package/src/mentions.ts +2 -2
- package/src/messenger.test.ts +504 -23
- package/src/messenger.ts +133 -52
- package/src/monitor-handler/access.ts +125 -0
- package/src/monitor-handler/inbound-media.test.ts +289 -0
- package/src/monitor-handler/inbound-media.ts +57 -5
- package/src/monitor-handler/message-handler-mock-support.test-support.ts +28 -0
- package/src/monitor-handler/message-handler.authz.test.ts +588 -74
- package/src/monitor-handler/message-handler.dm-media.test.ts +54 -0
- package/src/monitor-handler/message-handler.test-support.ts +100 -0
- package/src/monitor-handler/message-handler.thread-parent.test.ts +223 -0
- package/src/monitor-handler/message-handler.thread-session.test.ts +77 -0
- package/src/monitor-handler/message-handler.ts +470 -164
- package/src/monitor-handler/reaction-handler.test.ts +267 -0
- package/src/monitor-handler/reaction-handler.ts +210 -0
- package/src/monitor-handler/thread-session.ts +17 -0
- package/src/monitor-handler.adaptive-card.test.ts +162 -0
- package/src/monitor-handler.feedback-authz.test.ts +314 -0
- package/src/monitor-handler.file-consent.test.ts +281 -79
- package/src/monitor-handler.sso.test.ts +563 -0
- package/src/monitor-handler.test-helpers.ts +180 -0
- package/src/monitor-handler.ts +459 -115
- package/src/monitor-handler.types.ts +27 -0
- package/src/monitor-types.ts +1 -0
- package/src/monitor.lifecycle.test.ts +74 -10
- package/src/monitor.test.ts +35 -1
- package/src/monitor.ts +143 -46
- package/src/oauth.flow.ts +77 -0
- package/src/oauth.shared.ts +37 -0
- package/src/oauth.test.ts +305 -0
- package/src/oauth.token.ts +158 -0
- package/src/oauth.ts +130 -0
- package/src/outbound.test.ts +10 -11
- package/src/outbound.ts +62 -44
- package/src/pending-uploads-fs.test.ts +246 -0
- package/src/pending-uploads-fs.ts +235 -0
- package/src/pending-uploads.test.ts +173 -0
- package/src/pending-uploads.ts +34 -2
- package/src/policy.test.ts +11 -5
- package/src/policy.ts +5 -5
- package/src/polls.test.ts +106 -5
- package/src/polls.ts +15 -7
- package/src/presentation.ts +68 -0
- package/src/probe.test.ts +27 -8
- package/src/probe.ts +43 -9
- package/src/reply-dispatcher.test.ts +437 -0
- package/src/reply-dispatcher.ts +259 -73
- package/src/reply-stream-controller.test.ts +235 -0
- package/src/reply-stream-controller.ts +147 -0
- package/src/resolve-allowlist.test.ts +105 -1
- package/src/resolve-allowlist.ts +112 -7
- package/src/runtime.ts +6 -3
- package/src/sdk-types.ts +43 -3
- package/src/sdk.test.ts +666 -0
- package/src/sdk.ts +867 -16
- package/src/secret-contract.ts +49 -0
- package/src/secret-input.ts +1 -1
- package/src/send-context.ts +76 -9
- package/src/send.test.ts +389 -5
- package/src/send.ts +140 -32
- package/src/sent-message-cache.ts +30 -18
- package/src/session-route.ts +40 -0
- package/src/setup-core.ts +160 -0
- package/src/setup-surface.test.ts +202 -0
- package/src/setup-surface.ts +320 -0
- package/src/sso-token-store.test.ts +72 -0
- package/src/sso-token-store.ts +166 -0
- package/src/sso.ts +300 -0
- package/src/storage.ts +1 -1
- package/src/store-fs.ts +2 -2
- package/src/streaming-message.test.ts +262 -0
- package/src/streaming-message.ts +297 -0
- package/src/test-runtime.ts +1 -1
- package/src/thread-parent-context.test.ts +224 -0
- package/src/thread-parent-context.ts +159 -0
- package/src/token.test.ts +237 -50
- package/src/token.ts +162 -7
- package/src/user-agent.test.ts +86 -0
- package/src/user-agent.ts +53 -0
- package/src/webhook-timeouts.ts +27 -0
- package/src/welcome-card.test.ts +81 -0
- package/src/welcome-card.ts +57 -0
- package/test-api.ts +1 -0
- package/tsconfig.json +16 -0
- package/CHANGELOG.md +0 -107
- package/src/file-lock.ts +0 -1
- package/src/graph-users.test.ts +0 -66
- package/src/onboarding.ts +0 -381
- package/src/polls-store.test.ts +0 -38
- package/src/revoked-context.test.ts +0 -39
- package/src/token-response.test.ts +0 -23
package/src/send.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
67
|
+
type SendMSTeamsPollResult = {
|
|
64
68
|
pollId: string;
|
|
65
69
|
messageId: string;
|
|
66
70
|
conversationId: string;
|
|
67
71
|
};
|
|
68
72
|
|
|
69
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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
|
-
|
|
4
|
-
timestamps: Map<string, number>;
|
|
5
|
-
};
|
|
4
|
+
let sentMessageCache: Map<string, Map<string, number>> | undefined;
|
|
6
5
|
|
|
7
|
-
|
|
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:
|
|
10
|
-
const
|
|
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.
|
|
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
|
-
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
const store = getSentMessageCache();
|
|
34
|
+
let entry = store.get(conversationId);
|
|
23
35
|
if (!entry) {
|
|
24
|
-
entry =
|
|
25
|
-
|
|
36
|
+
entry = new Map<string, number>();
|
|
37
|
+
store.set(conversationId, entry);
|
|
26
38
|
}
|
|
27
|
-
entry.
|
|
28
|
-
if (entry.
|
|
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 =
|
|
46
|
+
const entry = getSentMessageCache().get(conversationId);
|
|
35
47
|
if (!entry) {
|
|
36
48
|
return false;
|
|
37
49
|
}
|
|
38
|
-
cleanupExpired(entry);
|
|
39
|
-
return entry.
|
|
50
|
+
cleanupExpired(conversationId, entry, Date.now());
|
|
51
|
+
return entry.has(messageId);
|
|
40
52
|
}
|
|
41
53
|
|
|
42
54
|
export function clearMSTeamsSentMessageCache(): void {
|
|
43
|
-
|
|
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
|
+
}
|