@openclaw/msteams 2026.3.12 → 2026.5.1-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 +161 -9
- 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 +174 -437
- 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 +148 -14
- 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 +258 -0
- package/src/graph-upload.ts +87 -8
- 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 +522 -45
- 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 +477 -174
- 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 +301 -106
- 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 +34 -40
- 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 -101
- 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
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import {
|
|
2
|
+
collectSecretInputAssignment,
|
|
3
|
+
getChannelRecord,
|
|
4
|
+
type ResolverContext,
|
|
5
|
+
type SecretDefaults,
|
|
6
|
+
} from "openclaw/plugin-sdk/channel-secret-basic-runtime";
|
|
7
|
+
|
|
8
|
+
export const secretTargetRegistryEntries: import("openclaw/plugin-sdk/channel-secret-basic-runtime").SecretTargetRegistryEntry[] =
|
|
9
|
+
[
|
|
10
|
+
{
|
|
11
|
+
id: "channels.msteams.appPassword",
|
|
12
|
+
targetType: "channels.msteams.appPassword",
|
|
13
|
+
configFile: "openclaw.json",
|
|
14
|
+
pathPattern: "channels.msteams.appPassword",
|
|
15
|
+
secretShape: "secret_input",
|
|
16
|
+
expectedResolvedValue: "string",
|
|
17
|
+
includeInPlan: true,
|
|
18
|
+
includeInConfigure: true,
|
|
19
|
+
includeInAudit: true,
|
|
20
|
+
},
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
export function collectRuntimeConfigAssignments(params: {
|
|
24
|
+
config: { channels?: Record<string, unknown> };
|
|
25
|
+
defaults?: SecretDefaults;
|
|
26
|
+
context: ResolverContext;
|
|
27
|
+
}): void {
|
|
28
|
+
const msteams = getChannelRecord(params.config, "msteams");
|
|
29
|
+
if (!msteams) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
collectSecretInputAssignment({
|
|
33
|
+
value: msteams.appPassword,
|
|
34
|
+
path: "channels.msteams.appPassword",
|
|
35
|
+
expected: "string",
|
|
36
|
+
defaults: params.defaults,
|
|
37
|
+
context: params.context,
|
|
38
|
+
active: msteams.enabled !== false,
|
|
39
|
+
inactiveReason: "Microsoft Teams channel is disabled.",
|
|
40
|
+
apply: (value) => {
|
|
41
|
+
msteams.appPassword = value;
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const channelSecrets = {
|
|
47
|
+
secretTargetRegistryEntries,
|
|
48
|
+
collectRuntimeConfigAssignments,
|
|
49
|
+
};
|
package/src/secret-input.ts
CHANGED
|
@@ -2,6 +2,6 @@ import {
|
|
|
2
2
|
hasConfiguredSecretInput,
|
|
3
3
|
normalizeResolvedSecretInputString,
|
|
4
4
|
normalizeSecretInputString,
|
|
5
|
-
} from "openclaw/plugin-sdk/
|
|
5
|
+
} from "openclaw/plugin-sdk/secret-input";
|
|
6
6
|
|
|
7
7
|
export { hasConfiguredSecretInput, normalizeResolvedSecretInputString, normalizeSecretInputString };
|
package/src/send-context.ts
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
|
+
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
|
|
1
2
|
import {
|
|
2
3
|
resolveChannelMediaMaxBytes,
|
|
3
4
|
type OpenClawConfig,
|
|
4
5
|
type PluginRuntime,
|
|
5
|
-
} from "
|
|
6
|
+
} from "../runtime-api.js";
|
|
6
7
|
import type { MSTeamsAccessTokenProvider } from "./attachments/types.js";
|
|
7
8
|
import { createMSTeamsConversationStoreFs } from "./conversation-store-fs.js";
|
|
8
9
|
import type {
|
|
9
10
|
MSTeamsConversationStore,
|
|
10
11
|
StoredConversationReference,
|
|
11
12
|
} from "./conversation-store.js";
|
|
13
|
+
import { formatUnknownError } from "./errors.js";
|
|
14
|
+
import { resolveGraphChatId } from "./graph-upload.js";
|
|
12
15
|
import type { MSTeamsAdapter } from "./messenger.js";
|
|
13
16
|
import { getMSTeamsRuntime } from "./runtime.js";
|
|
14
|
-
import { createMSTeamsAdapter, loadMSTeamsSdkWithAuth } from "./sdk.js";
|
|
17
|
+
import { createMSTeamsAdapter, createMSTeamsTokenProvider, loadMSTeamsSdkWithAuth } from "./sdk.js";
|
|
15
18
|
import { resolveMSTeamsCredentials } from "./token.js";
|
|
16
19
|
|
|
17
|
-
|
|
20
|
+
type MSTeamsConversationType = "personal" | "groupChat" | "channel";
|
|
18
21
|
|
|
19
22
|
export type MSTeamsProactiveContext = {
|
|
20
23
|
appId: string;
|
|
@@ -30,6 +33,13 @@ export type MSTeamsProactiveContext = {
|
|
|
30
33
|
sharePointSiteId?: string;
|
|
31
34
|
/** Resolved media max bytes from config (default: 100MB) */
|
|
32
35
|
mediaMaxBytes?: number;
|
|
36
|
+
/**
|
|
37
|
+
* Graph API-native chat ID for this conversation.
|
|
38
|
+
* Bot Framework personal DM IDs (`a:1xxx` / `8:orgid:xxx`) cannot be used directly
|
|
39
|
+
* with Graph chat endpoints. This field holds the resolved `19:xxx` format ID.
|
|
40
|
+
* Null if resolution failed or not applicable.
|
|
41
|
+
*/
|
|
42
|
+
graphChatId?: string | null;
|
|
33
43
|
};
|
|
34
44
|
|
|
35
45
|
/**
|
|
@@ -84,7 +94,7 @@ async function findConversationReference(recipient: {
|
|
|
84
94
|
return null;
|
|
85
95
|
}
|
|
86
96
|
|
|
87
|
-
const found = await recipient.store.
|
|
97
|
+
const found = await recipient.store.findPreferredDmByUserId(recipient.id);
|
|
88
98
|
if (!found) {
|
|
89
99
|
return null;
|
|
90
100
|
}
|
|
@@ -120,17 +130,34 @@ export async function resolveMSTeamsSendContext(params: {
|
|
|
120
130
|
}
|
|
121
131
|
|
|
122
132
|
const { conversationId, ref } = found;
|
|
133
|
+
|
|
134
|
+
// Safety check: when the caller targeted a specific user (DM), verify the
|
|
135
|
+
// resolved conversation is actually a personal DM. Without this guard a
|
|
136
|
+
// stale or mismatched conversation store could route a private DM reply
|
|
137
|
+
// into a shared channel or group chat -- see #54520.
|
|
138
|
+
if (recipient.type === "user") {
|
|
139
|
+
const resolvedType = normalizeLowercaseStringOrEmpty(ref.conversation?.conversationType ?? "");
|
|
140
|
+
if (resolvedType && resolvedType !== "personal") {
|
|
141
|
+
throw new Error(
|
|
142
|
+
`Conversation reference for user:${recipient.id} resolved to a ${resolvedType} ` +
|
|
143
|
+
`conversation (${conversationId}) instead of a personal DM. ` +
|
|
144
|
+
`The bot must receive a DM from this user before it can send proactively.`,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
123
148
|
const core = getMSTeamsRuntime();
|
|
124
149
|
const log = core.logging.getChildLogger({ name: "msteams:send" });
|
|
125
150
|
|
|
126
|
-
const { sdk,
|
|
127
|
-
const adapter = createMSTeamsAdapter(
|
|
151
|
+
const { sdk, app } = await loadMSTeamsSdkWithAuth(creds);
|
|
152
|
+
const adapter = createMSTeamsAdapter(app, sdk);
|
|
128
153
|
|
|
129
|
-
// Create token provider for Graph API / OneDrive operations
|
|
130
|
-
const tokenProvider =
|
|
154
|
+
// Create token provider adapter for Graph API / OneDrive operations
|
|
155
|
+
const tokenProvider: MSTeamsAccessTokenProvider = createMSTeamsTokenProvider(app);
|
|
131
156
|
|
|
132
157
|
// Determine conversation type from stored reference
|
|
133
|
-
const storedConversationType =
|
|
158
|
+
const storedConversationType = normalizeLowercaseStringOrEmpty(
|
|
159
|
+
ref.conversation?.conversationType ?? "",
|
|
160
|
+
);
|
|
134
161
|
let conversationType: MSTeamsConversationType;
|
|
135
162
|
if (storedConversationType === "personal") {
|
|
136
163
|
conversationType = "personal";
|
|
@@ -150,6 +177,45 @@ export async function resolveMSTeamsSendContext(params: {
|
|
|
150
177
|
resolveChannelLimitMb: ({ cfg }) => cfg.channels?.msteams?.mediaMaxMb,
|
|
151
178
|
});
|
|
152
179
|
|
|
180
|
+
// Resolve Graph API-native chat ID if needed for SharePoint per-user sharing.
|
|
181
|
+
// Bot Framework personal DM conversation IDs (e.g. `a:1xxx` or `8:orgid:xxx`) cannot
|
|
182
|
+
// be used directly with Graph /chats/{chatId} endpoints — the Graph API requires the
|
|
183
|
+
// `19:xxx@thread.tacv2` or `19:xxx@unq.gbl.spaces` format.
|
|
184
|
+
// We check the cached value first, then resolve via Graph API and cache for future sends.
|
|
185
|
+
let graphChatId: string | null | undefined = ref.graphChatId ?? undefined;
|
|
186
|
+
if (graphChatId === undefined && sharePointSiteId) {
|
|
187
|
+
// Only resolve when SharePoint is configured (the only place chatId matters currently)
|
|
188
|
+
try {
|
|
189
|
+
const resolved = await resolveGraphChatId({
|
|
190
|
+
botFrameworkConversationId: conversationId,
|
|
191
|
+
userAadObjectId: ref.user?.aadObjectId,
|
|
192
|
+
tokenProvider,
|
|
193
|
+
});
|
|
194
|
+
graphChatId = resolved;
|
|
195
|
+
|
|
196
|
+
// Cache in the conversation store so subsequent sends skip the Graph lookup.
|
|
197
|
+
// NOTE: We intentionally do NOT cache null results. Transient Graph API failures
|
|
198
|
+
// (network, 401, rate limit) should be retried on subsequent sends rather than
|
|
199
|
+
// permanently blocking file uploads for this conversation.
|
|
200
|
+
if (resolved) {
|
|
201
|
+
await store.upsert(conversationId, { ...ref, graphChatId: resolved });
|
|
202
|
+
} else {
|
|
203
|
+
log.warn?.("could not resolve Graph chat ID; file uploads may fail for this conversation", {
|
|
204
|
+
conversationId,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
} catch (err) {
|
|
208
|
+
log.warn?.(
|
|
209
|
+
"failed to resolve Graph chat ID; file uploads may fall back to Bot Framework ID",
|
|
210
|
+
{
|
|
211
|
+
conversationId,
|
|
212
|
+
error: formatUnknownError(err),
|
|
213
|
+
},
|
|
214
|
+
);
|
|
215
|
+
graphChatId = null;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
153
219
|
return {
|
|
154
220
|
appId: creds.appId,
|
|
155
221
|
conversationId,
|
|
@@ -160,5 +226,6 @@ export async function resolveMSTeamsSendContext(params: {
|
|
|
160
226
|
tokenProvider,
|
|
161
227
|
sharePointSiteId,
|
|
162
228
|
mediaMaxBytes,
|
|
229
|
+
graphChatId,
|
|
163
230
|
};
|
|
164
231
|
}
|
package/src/send.test.ts
CHANGED
|
@@ -1,20 +1,40 @@
|
|
|
1
|
-
import type { OpenClawConfig } from "openclaw/plugin-sdk/msteams";
|
|
2
1
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
-
import {
|
|
2
|
+
import type { OpenClawConfig } from "../runtime-api.js";
|
|
3
|
+
import { deleteMessageMSTeams, editMessageMSTeams, sendMessageMSTeams } from "./send.js";
|
|
4
4
|
|
|
5
5
|
const mockState = vi.hoisted(() => ({
|
|
6
6
|
loadOutboundMediaFromUrl: vi.fn(),
|
|
7
7
|
resolveMSTeamsSendContext: vi.fn(),
|
|
8
|
+
resolveMarkdownTableMode: vi.fn(() => "off"),
|
|
9
|
+
convertMarkdownTables: vi.fn((text: string) => text),
|
|
10
|
+
runtimeResolveMarkdownTableMode: vi.fn(() => "off"),
|
|
11
|
+
runtimeConvertMarkdownTables: vi.fn((text: string) => text),
|
|
8
12
|
requiresFileConsent: vi.fn(),
|
|
9
13
|
prepareFileConsentActivity: vi.fn(),
|
|
14
|
+
prepareFileConsentActivityFs: vi.fn(),
|
|
10
15
|
extractFilename: vi.fn(async () => "fallback.bin"),
|
|
11
16
|
sendMSTeamsMessages: vi.fn(),
|
|
17
|
+
uploadAndShareSharePoint: vi.fn(),
|
|
18
|
+
getDriveItemProperties: vi.fn(),
|
|
19
|
+
buildTeamsFileInfoCard: vi.fn(),
|
|
12
20
|
}));
|
|
13
21
|
|
|
14
|
-
vi.mock("openclaw/plugin-sdk/
|
|
22
|
+
vi.mock("openclaw/plugin-sdk/outbound-media", () => ({
|
|
15
23
|
loadOutboundMediaFromUrl: mockState.loadOutboundMediaFromUrl,
|
|
16
24
|
}));
|
|
17
25
|
|
|
26
|
+
vi.mock("openclaw/plugin-sdk/markdown-table-runtime", () => ({
|
|
27
|
+
resolveMarkdownTableMode: mockState.resolveMarkdownTableMode,
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
vi.mock("openclaw/plugin-sdk/text-runtime", async (importOriginal) => {
|
|
31
|
+
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/text-runtime")>();
|
|
32
|
+
return {
|
|
33
|
+
...actual,
|
|
34
|
+
convertMarkdownTables: mockState.convertMarkdownTables,
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
|
|
18
38
|
vi.mock("./send-context.js", () => ({
|
|
19
39
|
resolveMSTeamsSendContext: mockState.resolveMSTeamsSendContext,
|
|
20
40
|
}));
|
|
@@ -22,6 +42,7 @@ vi.mock("./send-context.js", () => ({
|
|
|
22
42
|
vi.mock("./file-consent-helpers.js", () => ({
|
|
23
43
|
requiresFileConsent: mockState.requiresFileConsent,
|
|
24
44
|
prepareFileConsentActivity: mockState.prepareFileConsentActivity,
|
|
45
|
+
prepareFileConsentActivityFs: mockState.prepareFileConsentActivityFs,
|
|
25
46
|
}));
|
|
26
47
|
|
|
27
48
|
vi.mock("./media-helpers.js", () => ({
|
|
@@ -38,21 +59,121 @@ vi.mock("./runtime.js", () => ({
|
|
|
38
59
|
getMSTeamsRuntime: () => ({
|
|
39
60
|
channel: {
|
|
40
61
|
text: {
|
|
41
|
-
resolveMarkdownTableMode:
|
|
42
|
-
convertMarkdownTables:
|
|
62
|
+
resolveMarkdownTableMode: mockState.runtimeResolveMarkdownTableMode,
|
|
63
|
+
convertMarkdownTables: mockState.runtimeConvertMarkdownTables,
|
|
43
64
|
},
|
|
44
65
|
},
|
|
45
66
|
}),
|
|
46
67
|
}));
|
|
47
68
|
|
|
69
|
+
vi.mock("./graph-upload.js", () => ({
|
|
70
|
+
uploadAndShareSharePoint: mockState.uploadAndShareSharePoint,
|
|
71
|
+
getDriveItemProperties: mockState.getDriveItemProperties,
|
|
72
|
+
uploadAndShareOneDrive: vi.fn(),
|
|
73
|
+
}));
|
|
74
|
+
|
|
75
|
+
vi.mock("./graph-chat.js", () => ({
|
|
76
|
+
buildTeamsFileInfoCard: mockState.buildTeamsFileInfoCard,
|
|
77
|
+
}));
|
|
78
|
+
|
|
79
|
+
function mockContinueConversationFailure(error: string) {
|
|
80
|
+
const mockContinueConversation = vi.fn().mockRejectedValue(new Error(error));
|
|
81
|
+
mockState.resolveMSTeamsSendContext.mockResolvedValue({
|
|
82
|
+
adapter: { continueConversation: mockContinueConversation },
|
|
83
|
+
appId: "app-id",
|
|
84
|
+
conversationId: "19:conversation@thread.tacv2",
|
|
85
|
+
ref: {
|
|
86
|
+
user: { id: "user-1" },
|
|
87
|
+
agent: { id: "agent-1" },
|
|
88
|
+
conversation: { id: "19:conversation@thread.tacv2" },
|
|
89
|
+
channelId: "msteams",
|
|
90
|
+
},
|
|
91
|
+
log: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
92
|
+
conversationType: "personal",
|
|
93
|
+
tokenProvider: {},
|
|
94
|
+
});
|
|
95
|
+
return mockContinueConversation;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function createSharePointSendContext(params: {
|
|
99
|
+
conversationId: string;
|
|
100
|
+
graphChatId: string | null;
|
|
101
|
+
siteId: string;
|
|
102
|
+
}) {
|
|
103
|
+
return {
|
|
104
|
+
adapter: {
|
|
105
|
+
continueConversation: vi.fn(
|
|
106
|
+
async (
|
|
107
|
+
_id: string,
|
|
108
|
+
_ref: unknown,
|
|
109
|
+
fn: (ctx: { sendActivity: () => { id: "msg-1" } }) => Promise<void>,
|
|
110
|
+
) => fn({ sendActivity: () => ({ id: "msg-1" }) }),
|
|
111
|
+
),
|
|
112
|
+
},
|
|
113
|
+
appId: "app-id",
|
|
114
|
+
conversationId: params.conversationId,
|
|
115
|
+
graphChatId: params.graphChatId,
|
|
116
|
+
ref: {},
|
|
117
|
+
log: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
118
|
+
conversationType: "groupChat" as const,
|
|
119
|
+
tokenProvider: { getAccessToken: vi.fn(async () => "token") },
|
|
120
|
+
mediaMaxBytes: 8 * 1024 * 1024,
|
|
121
|
+
sharePointSiteId: params.siteId,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function mockSharePointPdfUpload(params: {
|
|
126
|
+
bufferSize: number;
|
|
127
|
+
fileName: string;
|
|
128
|
+
itemId: string;
|
|
129
|
+
uniqueId: string;
|
|
130
|
+
}) {
|
|
131
|
+
mockState.loadOutboundMediaFromUrl.mockResolvedValueOnce({
|
|
132
|
+
buffer: Buffer.alloc(params.bufferSize, "pdf"),
|
|
133
|
+
contentType: "application/pdf",
|
|
134
|
+
fileName: params.fileName,
|
|
135
|
+
kind: "file",
|
|
136
|
+
});
|
|
137
|
+
mockState.requiresFileConsent.mockReturnValue(false);
|
|
138
|
+
mockState.uploadAndShareSharePoint.mockResolvedValue({
|
|
139
|
+
itemId: params.itemId,
|
|
140
|
+
webUrl: `https://sp.example.com/${params.fileName}`,
|
|
141
|
+
shareUrl: `https://sp.example.com/share/${params.fileName}`,
|
|
142
|
+
name: params.fileName,
|
|
143
|
+
});
|
|
144
|
+
mockState.getDriveItemProperties.mockResolvedValue({
|
|
145
|
+
eTag: `"${params.uniqueId},1"`,
|
|
146
|
+
webDavUrl: `https://sp.example.com/dav/${params.fileName}`,
|
|
147
|
+
name: params.fileName,
|
|
148
|
+
});
|
|
149
|
+
mockState.buildTeamsFileInfoCard.mockReturnValue({
|
|
150
|
+
contentType: "application/vnd.microsoft.teams.card.file.info",
|
|
151
|
+
contentUrl: `https://sp.example.com/dav/${params.fileName}`,
|
|
152
|
+
name: params.fileName,
|
|
153
|
+
content: { uniqueId: params.uniqueId, fileType: "pdf" },
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
48
157
|
describe("sendMessageMSTeams", () => {
|
|
49
158
|
beforeEach(() => {
|
|
50
159
|
mockState.loadOutboundMediaFromUrl.mockReset();
|
|
51
160
|
mockState.resolveMSTeamsSendContext.mockReset();
|
|
161
|
+
mockState.resolveMarkdownTableMode.mockReset();
|
|
162
|
+
mockState.resolveMarkdownTableMode.mockReturnValue("off");
|
|
163
|
+
mockState.convertMarkdownTables.mockReset();
|
|
164
|
+
mockState.convertMarkdownTables.mockImplementation((text: string) => text);
|
|
165
|
+
mockState.runtimeResolveMarkdownTableMode.mockReset();
|
|
166
|
+
mockState.runtimeResolveMarkdownTableMode.mockReturnValue("off");
|
|
167
|
+
mockState.runtimeConvertMarkdownTables.mockReset();
|
|
168
|
+
mockState.runtimeConvertMarkdownTables.mockImplementation((text: string) => text);
|
|
52
169
|
mockState.requiresFileConsent.mockReset();
|
|
53
170
|
mockState.prepareFileConsentActivity.mockReset();
|
|
171
|
+
mockState.prepareFileConsentActivityFs.mockReset();
|
|
54
172
|
mockState.extractFilename.mockReset();
|
|
55
173
|
mockState.sendMSTeamsMessages.mockReset();
|
|
174
|
+
mockState.uploadAndShareSharePoint.mockReset();
|
|
175
|
+
mockState.getDriveItemProperties.mockReset();
|
|
176
|
+
mockState.buildTeamsFileInfoCard.mockReset();
|
|
56
177
|
|
|
57
178
|
mockState.extractFilename.mockResolvedValue("fallback.bin");
|
|
58
179
|
mockState.requiresFileConsent.mockReturnValue(false);
|
|
@@ -106,4 +227,267 @@ describe("sendMessageMSTeams", () => {
|
|
|
106
227
|
}),
|
|
107
228
|
);
|
|
108
229
|
});
|
|
230
|
+
|
|
231
|
+
it("sends with provided cfg even when Teams runtime text helpers are unavailable", async () => {
|
|
232
|
+
mockState.runtimeResolveMarkdownTableMode.mockImplementation(() => {
|
|
233
|
+
throw new Error("MSTeams runtime not initialized");
|
|
234
|
+
});
|
|
235
|
+
mockState.runtimeConvertMarkdownTables.mockImplementation(() => {
|
|
236
|
+
throw new Error("MSTeams runtime not initialized");
|
|
237
|
+
});
|
|
238
|
+
mockState.resolveMarkdownTableMode.mockReturnValue("off");
|
|
239
|
+
mockState.convertMarkdownTables.mockReturnValue("hello");
|
|
240
|
+
|
|
241
|
+
await expect(
|
|
242
|
+
sendMessageMSTeams({
|
|
243
|
+
cfg: {} as OpenClawConfig,
|
|
244
|
+
to: "conversation:19:conversation@thread.tacv2",
|
|
245
|
+
text: "hello",
|
|
246
|
+
}),
|
|
247
|
+
).resolves.toEqual({
|
|
248
|
+
messageId: "message-1",
|
|
249
|
+
conversationId: "19:conversation@thread.tacv2",
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
expect(mockState.resolveMarkdownTableMode).toHaveBeenCalledWith({
|
|
253
|
+
cfg: {},
|
|
254
|
+
channel: "msteams",
|
|
255
|
+
});
|
|
256
|
+
expect(mockState.convertMarkdownTables).toHaveBeenCalledWith("hello", "off");
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("uses graphChatId instead of conversationId when uploading to SharePoint", async () => {
|
|
260
|
+
// Simulates a group chat where Bot Framework conversationId is valid but we have
|
|
261
|
+
// a resolved Graph chat ID cached from a prior send.
|
|
262
|
+
const graphChatId = "19:graph-native-chat-id@thread.tacv2";
|
|
263
|
+
const botFrameworkConversationId = "19:bot-framework-id@thread.tacv2";
|
|
264
|
+
|
|
265
|
+
mockState.resolveMSTeamsSendContext.mockResolvedValue(
|
|
266
|
+
createSharePointSendContext({
|
|
267
|
+
conversationId: botFrameworkConversationId,
|
|
268
|
+
graphChatId,
|
|
269
|
+
siteId: "site-123",
|
|
270
|
+
}),
|
|
271
|
+
);
|
|
272
|
+
mockSharePointPdfUpload({
|
|
273
|
+
bufferSize: 100,
|
|
274
|
+
fileName: "doc.pdf",
|
|
275
|
+
itemId: "item-1",
|
|
276
|
+
uniqueId: "{GUID-123}",
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
await sendMessageMSTeams({
|
|
280
|
+
cfg: {} as OpenClawConfig,
|
|
281
|
+
to: "conversation:19:bot-framework-id@thread.tacv2",
|
|
282
|
+
text: "here is a file",
|
|
283
|
+
mediaUrl: "https://example.com/doc.pdf",
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// The Graph-native chatId must be passed to SharePoint upload, not the Bot Framework ID
|
|
287
|
+
expect(mockState.uploadAndShareSharePoint).toHaveBeenCalledWith(
|
|
288
|
+
expect.objectContaining({
|
|
289
|
+
chatId: graphChatId,
|
|
290
|
+
siteId: "site-123",
|
|
291
|
+
}),
|
|
292
|
+
);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it("falls back to conversationId when graphChatId is not available", async () => {
|
|
296
|
+
const botFrameworkConversationId = "19:fallback-id@thread.tacv2";
|
|
297
|
+
|
|
298
|
+
mockState.resolveMSTeamsSendContext.mockResolvedValue(
|
|
299
|
+
createSharePointSendContext({
|
|
300
|
+
conversationId: botFrameworkConversationId,
|
|
301
|
+
graphChatId: null,
|
|
302
|
+
siteId: "site-456",
|
|
303
|
+
}),
|
|
304
|
+
);
|
|
305
|
+
mockSharePointPdfUpload({
|
|
306
|
+
bufferSize: 50,
|
|
307
|
+
fileName: "report.pdf",
|
|
308
|
+
itemId: "item-2",
|
|
309
|
+
uniqueId: "{GUID-456}",
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
await sendMessageMSTeams({
|
|
313
|
+
cfg: {} as OpenClawConfig,
|
|
314
|
+
to: "conversation:19:fallback-id@thread.tacv2",
|
|
315
|
+
text: "report",
|
|
316
|
+
mediaUrl: "https://example.com/report.pdf",
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// Falls back to conversationId when graphChatId is null
|
|
320
|
+
expect(mockState.uploadAndShareSharePoint).toHaveBeenCalledWith(
|
|
321
|
+
expect.objectContaining({
|
|
322
|
+
chatId: botFrameworkConversationId,
|
|
323
|
+
siteId: "site-456",
|
|
324
|
+
}),
|
|
325
|
+
);
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
describe("editMessageMSTeams", () => {
|
|
330
|
+
beforeEach(() => {
|
|
331
|
+
mockState.resolveMSTeamsSendContext.mockReset();
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("calls continueConversation and updateActivity with correct params", async () => {
|
|
335
|
+
const mockUpdateActivity = vi.fn();
|
|
336
|
+
const mockContinueConversation = vi.fn(
|
|
337
|
+
async (_appId: string, _ref: unknown, logic: (ctx: unknown) => Promise<void>) => {
|
|
338
|
+
await logic({
|
|
339
|
+
sendActivity: vi.fn(),
|
|
340
|
+
updateActivity: mockUpdateActivity,
|
|
341
|
+
deleteActivity: vi.fn(),
|
|
342
|
+
});
|
|
343
|
+
},
|
|
344
|
+
);
|
|
345
|
+
mockState.resolveMSTeamsSendContext.mockResolvedValue({
|
|
346
|
+
adapter: { continueConversation: mockContinueConversation },
|
|
347
|
+
appId: "app-id",
|
|
348
|
+
conversationId: "19:conversation@thread.tacv2",
|
|
349
|
+
ref: {
|
|
350
|
+
user: { id: "user-1" },
|
|
351
|
+
agent: { id: "agent-1" },
|
|
352
|
+
conversation: { id: "19:conversation@thread.tacv2", conversationType: "personal" },
|
|
353
|
+
channelId: "msteams",
|
|
354
|
+
},
|
|
355
|
+
log: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
356
|
+
conversationType: "personal",
|
|
357
|
+
tokenProvider: {},
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
const result = await editMessageMSTeams({
|
|
361
|
+
cfg: {} as OpenClawConfig,
|
|
362
|
+
to: "conversation:19:conversation@thread.tacv2",
|
|
363
|
+
activityId: "activity-123",
|
|
364
|
+
text: "Updated message text",
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
expect(result.conversationId).toBe("19:conversation@thread.tacv2");
|
|
368
|
+
expect(mockContinueConversation).toHaveBeenCalledTimes(1);
|
|
369
|
+
expect(mockContinueConversation).toHaveBeenCalledWith(
|
|
370
|
+
"app-id",
|
|
371
|
+
expect.objectContaining({ activityId: undefined }),
|
|
372
|
+
expect.any(Function),
|
|
373
|
+
);
|
|
374
|
+
expect(mockUpdateActivity).toHaveBeenCalledWith({
|
|
375
|
+
type: "message",
|
|
376
|
+
id: "activity-123",
|
|
377
|
+
text: "Updated message text",
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it("throws a descriptive error when continueConversation fails", async () => {
|
|
382
|
+
mockContinueConversationFailure("Service unavailable");
|
|
383
|
+
|
|
384
|
+
await expect(
|
|
385
|
+
editMessageMSTeams({
|
|
386
|
+
cfg: {} as OpenClawConfig,
|
|
387
|
+
to: "conversation:19:conversation@thread.tacv2",
|
|
388
|
+
activityId: "activity-123",
|
|
389
|
+
text: "Updated text",
|
|
390
|
+
}),
|
|
391
|
+
).rejects.toThrow("msteams edit failed");
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
describe("deleteMessageMSTeams", () => {
|
|
396
|
+
beforeEach(() => {
|
|
397
|
+
mockState.resolveMSTeamsSendContext.mockReset();
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it("calls continueConversation and deleteActivity with correct activityId", async () => {
|
|
401
|
+
const mockDeleteActivity = vi.fn();
|
|
402
|
+
const mockContinueConversation = vi.fn(
|
|
403
|
+
async (_appId: string, _ref: unknown, logic: (ctx: unknown) => Promise<void>) => {
|
|
404
|
+
await logic({
|
|
405
|
+
sendActivity: vi.fn(),
|
|
406
|
+
updateActivity: vi.fn(),
|
|
407
|
+
deleteActivity: mockDeleteActivity,
|
|
408
|
+
});
|
|
409
|
+
},
|
|
410
|
+
);
|
|
411
|
+
mockState.resolveMSTeamsSendContext.mockResolvedValue({
|
|
412
|
+
adapter: { continueConversation: mockContinueConversation },
|
|
413
|
+
appId: "app-id",
|
|
414
|
+
conversationId: "19:conversation@thread.tacv2",
|
|
415
|
+
ref: {
|
|
416
|
+
user: { id: "user-1" },
|
|
417
|
+
agent: { id: "agent-1" },
|
|
418
|
+
conversation: { id: "19:conversation@thread.tacv2", conversationType: "groupChat" },
|
|
419
|
+
channelId: "msteams",
|
|
420
|
+
},
|
|
421
|
+
log: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
422
|
+
conversationType: "groupChat",
|
|
423
|
+
tokenProvider: {},
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
const result = await deleteMessageMSTeams({
|
|
427
|
+
cfg: {} as OpenClawConfig,
|
|
428
|
+
to: "conversation:19:conversation@thread.tacv2",
|
|
429
|
+
activityId: "activity-456",
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
expect(result.conversationId).toBe("19:conversation@thread.tacv2");
|
|
433
|
+
expect(mockContinueConversation).toHaveBeenCalledTimes(1);
|
|
434
|
+
expect(mockContinueConversation).toHaveBeenCalledWith(
|
|
435
|
+
"app-id",
|
|
436
|
+
expect.objectContaining({ activityId: undefined }),
|
|
437
|
+
expect.any(Function),
|
|
438
|
+
);
|
|
439
|
+
expect(mockDeleteActivity).toHaveBeenCalledWith("activity-456");
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
it("throws a descriptive error when continueConversation fails", async () => {
|
|
443
|
+
mockContinueConversationFailure("Not found");
|
|
444
|
+
|
|
445
|
+
await expect(
|
|
446
|
+
deleteMessageMSTeams({
|
|
447
|
+
cfg: {} as OpenClawConfig,
|
|
448
|
+
to: "conversation:19:conversation@thread.tacv2",
|
|
449
|
+
activityId: "activity-456",
|
|
450
|
+
}),
|
|
451
|
+
).rejects.toThrow("msteams delete failed");
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it("passes the appId and proactive ref to continueConversation", async () => {
|
|
455
|
+
const mockContinueConversation = vi.fn(
|
|
456
|
+
async (_appId: string, _ref: unknown, logic: (ctx: unknown) => Promise<void>) => {
|
|
457
|
+
await logic({
|
|
458
|
+
sendActivity: vi.fn(),
|
|
459
|
+
updateActivity: vi.fn(),
|
|
460
|
+
deleteActivity: vi.fn(),
|
|
461
|
+
});
|
|
462
|
+
},
|
|
463
|
+
);
|
|
464
|
+
mockState.resolveMSTeamsSendContext.mockResolvedValue({
|
|
465
|
+
adapter: { continueConversation: mockContinueConversation },
|
|
466
|
+
appId: "my-app-id",
|
|
467
|
+
conversationId: "19:conv@thread.tacv2",
|
|
468
|
+
ref: {
|
|
469
|
+
activityId: "original-activity",
|
|
470
|
+
user: { id: "user-1" },
|
|
471
|
+
agent: { id: "agent-1" },
|
|
472
|
+
conversation: { id: "19:conv@thread.tacv2" },
|
|
473
|
+
channelId: "msteams",
|
|
474
|
+
},
|
|
475
|
+
log: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
476
|
+
conversationType: "personal",
|
|
477
|
+
tokenProvider: {},
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
await deleteMessageMSTeams({
|
|
481
|
+
cfg: {} as OpenClawConfig,
|
|
482
|
+
to: "conversation:19:conv@thread.tacv2",
|
|
483
|
+
activityId: "activity-789",
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
// appId should be forwarded correctly
|
|
487
|
+
expect(mockContinueConversation.mock.calls[0]?.[0]).toBe("my-app-id");
|
|
488
|
+
// activityId on the proactive ref should be cleared (undefined) — proactive pattern
|
|
489
|
+
expect(mockContinueConversation.mock.calls[0]?.[1]).toMatchObject({
|
|
490
|
+
activityId: undefined,
|
|
491
|
+
});
|
|
492
|
+
});
|
|
109
493
|
});
|