@openclaw/msteams 2026.1.29 → 2026.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/index.ts +0 -1
- package/openclaw.plugin.json +1 -3
- package/package.json +13 -10
- package/src/attachments/download.ts +98 -21
- package/src/attachments/graph.ts +50 -16
- package/src/attachments/html.ts +23 -9
- package/src/attachments/shared.ts +74 -18
- package/src/attachments.test.ts +37 -2
- package/src/channel.directory.test.ts +7 -5
- package/src/channel.ts +46 -23
- package/src/conversation-store-fs.test.ts +7 -8
- package/src/conversation-store-fs.ts +15 -5
- package/src/conversation-store-memory.ts +3 -1
- package/src/directory-live.ts +41 -15
- package/src/errors.test.ts +0 -1
- package/src/errors.ts +48 -16
- package/src/file-consent-helpers.test.ts +12 -3
- package/src/file-consent.ts +6 -2
- package/src/graph-chat.ts +5 -4
- package/src/graph-upload.ts +23 -15
- package/src/inbound.test.ts +0 -1
- package/src/inbound.ts +15 -5
- package/src/media-helpers.test.ts +9 -6
- package/src/media-helpers.ts +15 -6
- package/src/messenger.test.ts +7 -4
- package/src/messenger.ts +55 -20
- package/src/monitor-handler/inbound-media.ts +7 -2
- package/src/monitor-handler/message-handler.ts +66 -55
- package/src/monitor-handler.ts +3 -7
- package/src/monitor.ts +19 -14
- package/src/onboarding.ts +10 -11
- package/src/outbound.ts +0 -1
- package/src/pending-uploads.ts +7 -5
- package/src/policy.test.ts +1 -2
- package/src/policy.ts +39 -13
- package/src/polls-store-memory.ts +3 -1
- package/src/polls-store.test.ts +1 -3
- package/src/polls.test.ts +5 -6
- package/src/polls.ts +24 -9
- package/src/probe.test.ts +4 -3
- package/src/probe.ts +18 -10
- package/src/reply-dispatcher.ts +5 -3
- package/src/resolve-allowlist.ts +39 -19
- package/src/send-context.ts +12 -4
- package/src/send.ts +49 -19
- package/src/sent-message-cache.test.ts +0 -1
- package/src/sent-message-cache.ts +9 -3
- package/src/storage.ts +6 -3
- package/src/store-fs.ts +6 -3
package/src/errors.ts
CHANGED
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
export function formatUnknownError(err: unknown): string {
|
|
2
|
-
if (err instanceof Error)
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
if (err ===
|
|
2
|
+
if (err instanceof Error) {
|
|
3
|
+
return err.message;
|
|
4
|
+
}
|
|
5
|
+
if (typeof err === "string") {
|
|
6
|
+
return err;
|
|
7
|
+
}
|
|
8
|
+
if (err === null) {
|
|
9
|
+
return "null";
|
|
10
|
+
}
|
|
11
|
+
if (err === undefined) {
|
|
12
|
+
return "undefined";
|
|
13
|
+
}
|
|
6
14
|
if (typeof err === "number" || typeof err === "boolean" || typeof err === "bigint") {
|
|
7
15
|
return String(err);
|
|
8
16
|
}
|
|
9
|
-
if (typeof err === "symbol")
|
|
17
|
+
if (typeof err === "symbol") {
|
|
18
|
+
return err.description ?? err.toString();
|
|
19
|
+
}
|
|
10
20
|
if (typeof err === "function") {
|
|
11
21
|
return err.name ? `[function ${err.name}]` : "[function]";
|
|
12
22
|
}
|
|
@@ -22,21 +32,31 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
22
32
|
}
|
|
23
33
|
|
|
24
34
|
function extractStatusCode(err: unknown): number | null {
|
|
25
|
-
if (!isRecord(err))
|
|
35
|
+
if (!isRecord(err)) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
26
38
|
const direct = err.statusCode ?? err.status;
|
|
27
|
-
if (typeof direct === "number" && Number.isFinite(direct))
|
|
39
|
+
if (typeof direct === "number" && Number.isFinite(direct)) {
|
|
40
|
+
return direct;
|
|
41
|
+
}
|
|
28
42
|
if (typeof direct === "string") {
|
|
29
43
|
const parsed = Number.parseInt(direct, 10);
|
|
30
|
-
if (Number.isFinite(parsed))
|
|
44
|
+
if (Number.isFinite(parsed)) {
|
|
45
|
+
return parsed;
|
|
46
|
+
}
|
|
31
47
|
}
|
|
32
48
|
|
|
33
49
|
const response = err.response;
|
|
34
50
|
if (isRecord(response)) {
|
|
35
51
|
const status = response.status;
|
|
36
|
-
if (typeof status === "number" && Number.isFinite(status))
|
|
52
|
+
if (typeof status === "number" && Number.isFinite(status)) {
|
|
53
|
+
return status;
|
|
54
|
+
}
|
|
37
55
|
if (typeof status === "string") {
|
|
38
56
|
const parsed = Number.parseInt(status, 10);
|
|
39
|
-
if (Number.isFinite(parsed))
|
|
57
|
+
if (Number.isFinite(parsed)) {
|
|
58
|
+
return parsed;
|
|
59
|
+
}
|
|
40
60
|
}
|
|
41
61
|
}
|
|
42
62
|
|
|
@@ -44,7 +64,9 @@ function extractStatusCode(err: unknown): number | null {
|
|
|
44
64
|
}
|
|
45
65
|
|
|
46
66
|
function extractRetryAfterMs(err: unknown): number | null {
|
|
47
|
-
if (!isRecord(err))
|
|
67
|
+
if (!isRecord(err)) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
48
70
|
|
|
49
71
|
const direct = err.retryAfterMs ?? err.retry_after_ms;
|
|
50
72
|
if (typeof direct === "number" && Number.isFinite(direct) && direct >= 0) {
|
|
@@ -57,20 +79,28 @@ function extractRetryAfterMs(err: unknown): number | null {
|
|
|
57
79
|
}
|
|
58
80
|
if (typeof retryAfter === "string") {
|
|
59
81
|
const parsed = Number.parseFloat(retryAfter);
|
|
60
|
-
if (Number.isFinite(parsed) && parsed >= 0)
|
|
82
|
+
if (Number.isFinite(parsed) && parsed >= 0) {
|
|
83
|
+
return parsed * 1000;
|
|
84
|
+
}
|
|
61
85
|
}
|
|
62
86
|
|
|
63
87
|
const response = err.response;
|
|
64
|
-
if (!isRecord(response))
|
|
88
|
+
if (!isRecord(response)) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
65
91
|
|
|
66
92
|
const headers = response.headers;
|
|
67
|
-
if (!headers)
|
|
93
|
+
if (!headers) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
68
96
|
|
|
69
97
|
if (isRecord(headers)) {
|
|
70
98
|
const raw = headers["retry-after"] ?? headers["Retry-After"];
|
|
71
99
|
if (typeof raw === "string") {
|
|
72
100
|
const parsed = Number.parseFloat(raw);
|
|
73
|
-
if (Number.isFinite(parsed) && parsed >= 0)
|
|
101
|
+
if (Number.isFinite(parsed) && parsed >= 0) {
|
|
102
|
+
return parsed * 1000;
|
|
103
|
+
}
|
|
74
104
|
}
|
|
75
105
|
}
|
|
76
106
|
|
|
@@ -84,7 +114,9 @@ function extractRetryAfterMs(err: unknown): number | null {
|
|
|
84
114
|
const raw = (headers as { get: (name: string) => string | null }).get("retry-after");
|
|
85
115
|
if (raw) {
|
|
86
116
|
const parsed = Number.parseFloat(raw);
|
|
87
|
-
if (Number.isFinite(parsed) && parsed >= 0)
|
|
117
|
+
if (Number.isFinite(parsed) && parsed >= 0) {
|
|
118
|
+
return parsed * 1000;
|
|
119
|
+
}
|
|
88
120
|
}
|
|
89
121
|
}
|
|
90
122
|
|
|
@@ -186,7 +186,10 @@ describe("prepareFileConsentActivity", () => {
|
|
|
186
186
|
conversationId: "conv456",
|
|
187
187
|
});
|
|
188
188
|
|
|
189
|
-
const attachment = (result.activity.attachments as unknown[])[0] as Record<
|
|
189
|
+
const attachment = (result.activity.attachments as unknown[])[0] as Record<
|
|
190
|
+
string,
|
|
191
|
+
{ description: string }
|
|
192
|
+
>;
|
|
190
193
|
expect(attachment.content.description).toBe("File: document.docx");
|
|
191
194
|
});
|
|
192
195
|
|
|
@@ -201,7 +204,10 @@ describe("prepareFileConsentActivity", () => {
|
|
|
201
204
|
description: "Q4 Financial Report",
|
|
202
205
|
});
|
|
203
206
|
|
|
204
|
-
const attachment = (result.activity.attachments as unknown[])[0] as Record<
|
|
207
|
+
const attachment = (result.activity.attachments as unknown[])[0] as Record<
|
|
208
|
+
string,
|
|
209
|
+
{ description: string }
|
|
210
|
+
>;
|
|
205
211
|
expect(attachment.content.description).toBe("Q4 Financial Report");
|
|
206
212
|
});
|
|
207
213
|
|
|
@@ -215,7 +221,10 @@ describe("prepareFileConsentActivity", () => {
|
|
|
215
221
|
conversationId: "conv000",
|
|
216
222
|
});
|
|
217
223
|
|
|
218
|
-
const attachment = (result.activity.attachments as unknown[])[0] as Record<
|
|
224
|
+
const attachment = (result.activity.attachments as unknown[])[0] as Record<
|
|
225
|
+
string,
|
|
226
|
+
{ acceptContext: { uploadId: string } }
|
|
227
|
+
>;
|
|
219
228
|
expect(attachment.content.acceptContext.uploadId).toBe(mockUploadId);
|
|
220
229
|
});
|
|
221
230
|
|
package/src/file-consent.ts
CHANGED
|
@@ -78,7 +78,9 @@ export function parseFileConsentInvoke(activity: {
|
|
|
78
78
|
name?: string;
|
|
79
79
|
value?: unknown;
|
|
80
80
|
}): FileConsentResponse | null {
|
|
81
|
-
if (activity.name !== "fileConsent/invoke")
|
|
81
|
+
if (activity.name !== "fileConsent/invoke") {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
82
84
|
|
|
83
85
|
const value = activity.value as {
|
|
84
86
|
type?: string;
|
|
@@ -87,7 +89,9 @@ export function parseFileConsentInvoke(activity: {
|
|
|
87
89
|
context?: Record<string, unknown>;
|
|
88
90
|
};
|
|
89
91
|
|
|
90
|
-
if (value?.type !== "fileUpload")
|
|
92
|
+
if (value?.type !== "fileUpload") {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
91
95
|
|
|
92
96
|
return {
|
|
93
97
|
action: value.action === "accept" ? "accept" : "decline",
|
package/src/graph-chat.ts
CHANGED
|
@@ -31,10 +31,11 @@ export function buildTeamsFileInfoCard(file: DriveItemProperties): {
|
|
|
31
31
|
// Extract unique ID from eTag (remove quotes, braces, and version suffix)
|
|
32
32
|
// Example eTag formats: "{GUID},version" or "\"{GUID},version\""
|
|
33
33
|
const rawETag = file.eTag;
|
|
34
|
-
const uniqueId =
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
const uniqueId =
|
|
35
|
+
rawETag
|
|
36
|
+
.replace(/^["']|["']$/g, "") // Remove outer quotes
|
|
37
|
+
.replace(/[{}]/g, "") // Remove curly braces
|
|
38
|
+
.split(",")[0] ?? rawETag; // Take the GUID part before comma
|
|
38
39
|
|
|
39
40
|
// Extract file extension from filename
|
|
40
41
|
const lastDot = file.name.lastIndexOf(".");
|
package/src/graph-upload.ts
CHANGED
|
@@ -182,14 +182,17 @@ export async function uploadToSharePoint(params: {
|
|
|
182
182
|
// Use "OpenClawShared" folder to organize bot-uploaded files
|
|
183
183
|
const uploadPath = `/OpenClawShared/${encodeURIComponent(params.filename)}`;
|
|
184
184
|
|
|
185
|
-
const res = await fetchFn(
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
185
|
+
const res = await fetchFn(
|
|
186
|
+
`${GRAPH_ROOT}/sites/${params.siteId}/drive/root:${uploadPath}:/content`,
|
|
187
|
+
{
|
|
188
|
+
method: "PUT",
|
|
189
|
+
headers: {
|
|
190
|
+
Authorization: `Bearer ${token}`,
|
|
191
|
+
"Content-Type": params.contentType ?? "application/octet-stream",
|
|
192
|
+
},
|
|
193
|
+
body: new Uint8Array(params.buffer),
|
|
190
194
|
},
|
|
191
|
-
|
|
192
|
-
});
|
|
195
|
+
);
|
|
193
196
|
|
|
194
197
|
if (!res.ok) {
|
|
195
198
|
const body = await res.text().catch(() => "");
|
|
@@ -342,18 +345,23 @@ export async function createSharePointSharingLink(params: {
|
|
|
342
345
|
body.recipients = params.recipientObjectIds.map((id) => ({ objectId: id }));
|
|
343
346
|
}
|
|
344
347
|
|
|
345
|
-
const res = await fetchFn(
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
348
|
+
const res = await fetchFn(
|
|
349
|
+
`${apiRoot}/sites/${params.siteId}/drive/items/${params.itemId}/createLink`,
|
|
350
|
+
{
|
|
351
|
+
method: "POST",
|
|
352
|
+
headers: {
|
|
353
|
+
Authorization: `Bearer ${token}`,
|
|
354
|
+
"Content-Type": "application/json",
|
|
355
|
+
},
|
|
356
|
+
body: JSON.stringify(body),
|
|
350
357
|
},
|
|
351
|
-
|
|
352
|
-
});
|
|
358
|
+
);
|
|
353
359
|
|
|
354
360
|
if (!res.ok) {
|
|
355
361
|
const respBody = await res.text().catch(() => "");
|
|
356
|
-
throw new Error(
|
|
362
|
+
throw new Error(
|
|
363
|
+
`Create SharePoint sharing link failed: ${res.status} ${res.statusText} - ${respBody}`,
|
|
364
|
+
);
|
|
357
365
|
}
|
|
358
366
|
|
|
359
367
|
const data = (await res.json()) as {
|
package/src/inbound.test.ts
CHANGED
package/src/inbound.ts
CHANGED
|
@@ -11,16 +11,24 @@ export function normalizeMSTeamsConversationId(raw: string): string {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export function extractMSTeamsConversationMessageId(raw: string): string | undefined {
|
|
14
|
-
if (!raw)
|
|
14
|
+
if (!raw) {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
15
17
|
const match = /(?:^|;)messageid=([^;]+)/i.exec(raw);
|
|
16
18
|
const value = match?.[1]?.trim() ?? "";
|
|
17
19
|
return value || undefined;
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
export function parseMSTeamsActivityTimestamp(value: unknown): Date | undefined {
|
|
21
|
-
if (!value)
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
if (!value) {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
if (value instanceof Date) {
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
if (typeof value !== "string") {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
24
32
|
const date = new Date(value);
|
|
25
33
|
return Number.isNaN(date.getTime()) ? undefined : date;
|
|
26
34
|
}
|
|
@@ -32,7 +40,9 @@ export function stripMSTeamsMentionTags(text: string): string {
|
|
|
32
40
|
|
|
33
41
|
export function wasMSTeamsBotMentioned(activity: MentionableActivity): boolean {
|
|
34
42
|
const botId = activity.recipient?.id;
|
|
35
|
-
if (!botId)
|
|
43
|
+
if (!botId) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
36
46
|
const entities = activity.entities ?? [];
|
|
37
47
|
return entities.some((e) => e.type === "mention" && e.mentioned?.id === botId);
|
|
38
48
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
|
|
3
2
|
import { extractFilename, extractMessageId, getMimeType, isLocalPath } from "./media-helpers.js";
|
|
4
3
|
|
|
5
4
|
describe("msteams media-helpers", () => {
|
|
@@ -46,7 +45,9 @@ describe("msteams media-helpers", () => {
|
|
|
46
45
|
|
|
47
46
|
it("defaults to application/octet-stream for unknown extensions", async () => {
|
|
48
47
|
expect(await getMimeType("https://example.com/image")).toBe("application/octet-stream");
|
|
49
|
-
expect(await getMimeType("https://example.com/image.unknown")).toBe(
|
|
48
|
+
expect(await getMimeType("https://example.com/image.unknown")).toBe(
|
|
49
|
+
"application/octet-stream",
|
|
50
|
+
);
|
|
50
51
|
});
|
|
51
52
|
|
|
52
53
|
it("is case-insensitive", async () => {
|
|
@@ -110,15 +111,17 @@ describe("msteams media-helpers", () => {
|
|
|
110
111
|
|
|
111
112
|
it("extracts original filename with uppercase UUID", async () => {
|
|
112
113
|
expect(
|
|
113
|
-
await extractFilename(
|
|
114
|
+
await extractFilename(
|
|
115
|
+
"/media/inbound/Document---A1B2C3D4-E5F6-7890-ABCD-EF1234567890.docx",
|
|
116
|
+
),
|
|
114
117
|
).toBe("Document.docx");
|
|
115
118
|
});
|
|
116
119
|
|
|
117
120
|
it("falls back to UUID filename for legacy paths", async () => {
|
|
118
121
|
// UUID-only filename (legacy format, no embedded name)
|
|
119
|
-
expect(
|
|
120
|
-
|
|
121
|
-
)
|
|
122
|
+
expect(await extractFilename("/media/inbound/a1b2c3d4-e5f6-7890-abcd-ef1234567890.pdf")).toBe(
|
|
123
|
+
"a1b2c3d4-e5f6-7890-abcd-ef1234567890.pdf",
|
|
124
|
+
);
|
|
122
125
|
});
|
|
123
126
|
|
|
124
127
|
it("handles --- in filename without valid UUID pattern", async () => {
|
package/src/media-helpers.ts
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import path from "node:path";
|
|
6
|
-
|
|
7
6
|
import {
|
|
8
7
|
detectMime,
|
|
9
8
|
extensionForMime,
|
|
@@ -19,7 +18,9 @@ export async function getMimeType(url: string): Promise<string> {
|
|
|
19
18
|
// Handle data URLs: data:image/png;base64,...
|
|
20
19
|
if (url.startsWith("data:")) {
|
|
21
20
|
const match = url.match(/^data:([^;,]+)/);
|
|
22
|
-
if (match?.[1])
|
|
21
|
+
if (match?.[1]) {
|
|
22
|
+
return match[1];
|
|
23
|
+
}
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
// Use shared MIME detection (extension-based for URLs)
|
|
@@ -46,7 +47,9 @@ export async function extractFilename(url: string): Promise<string> {
|
|
|
46
47
|
const pathname = new URL(url).pathname;
|
|
47
48
|
const basename = path.basename(pathname);
|
|
48
49
|
const existingExt = getFileExtension(pathname);
|
|
49
|
-
if (basename && existingExt)
|
|
50
|
+
if (basename && existingExt) {
|
|
51
|
+
return basename;
|
|
52
|
+
}
|
|
50
53
|
// No extension in URL, derive from MIME
|
|
51
54
|
const mime = await getMimeType(url);
|
|
52
55
|
const ext = extensionForMime(mime) ?? ".bin";
|
|
@@ -69,9 +72,15 @@ export function isLocalPath(url: string): boolean {
|
|
|
69
72
|
* Extract the message ID from a Bot Framework response.
|
|
70
73
|
*/
|
|
71
74
|
export function extractMessageId(response: unknown): string | null {
|
|
72
|
-
if (!response || typeof response !== "object")
|
|
73
|
-
|
|
75
|
+
if (!response || typeof response !== "object") {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
if (!("id" in response)) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
74
81
|
const { id } = response as { id?: unknown };
|
|
75
|
-
if (typeof id !== "string" || !id)
|
|
82
|
+
if (typeof id !== "string" || !id) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
76
85
|
return id;
|
|
77
86
|
}
|
package/src/messenger.test.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it } from "vitest";
|
|
2
|
-
|
|
3
1
|
import { SILENT_REPLY_TOKEN, type PluginRuntime } from "openclaw/plugin-sdk";
|
|
2
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
4
3
|
import type { StoredConversationReference } from "./conversation-store.js";
|
|
5
4
|
import {
|
|
6
5
|
type MSTeamsAdapter,
|
|
@@ -10,8 +9,12 @@ import {
|
|
|
10
9
|
import { setMSTeamsRuntime } from "./runtime.js";
|
|
11
10
|
|
|
12
11
|
const chunkMarkdownText = (text: string, limit: number) => {
|
|
13
|
-
if (!text)
|
|
14
|
-
|
|
12
|
+
if (!text) {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
if (limit <= 0 || text.length <= limit) {
|
|
16
|
+
return [text];
|
|
17
|
+
}
|
|
15
18
|
const chunks: string[] = [];
|
|
16
19
|
for (let index = 0; index < text.length; index += limit) {
|
|
17
20
|
chunks.push(text.slice(index, index + limit));
|
package/src/messenger.ts
CHANGED
|
@@ -134,7 +134,9 @@ function pushTextMessages(
|
|
|
134
134
|
chunkMode: ChunkMode;
|
|
135
135
|
},
|
|
136
136
|
) {
|
|
137
|
-
if (!text)
|
|
137
|
+
if (!text) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
138
140
|
if (opts.chunkText) {
|
|
139
141
|
for (const chunk of getMSTeamsRuntime().channel.text.chunkMarkdownTextWithMode(
|
|
140
142
|
text,
|
|
@@ -142,26 +144,33 @@ function pushTextMessages(
|
|
|
142
144
|
opts.chunkMode,
|
|
143
145
|
)) {
|
|
144
146
|
const trimmed = chunk.trim();
|
|
145
|
-
if (!trimmed || isSilentReplyText(trimmed, SILENT_REPLY_TOKEN))
|
|
147
|
+
if (!trimmed || isSilentReplyText(trimmed, SILENT_REPLY_TOKEN)) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
146
150
|
out.push({ text: trimmed });
|
|
147
151
|
}
|
|
148
152
|
return;
|
|
149
153
|
}
|
|
150
154
|
|
|
151
155
|
const trimmed = text.trim();
|
|
152
|
-
if (!trimmed || isSilentReplyText(trimmed, SILENT_REPLY_TOKEN))
|
|
156
|
+
if (!trimmed || isSilentReplyText(trimmed, SILENT_REPLY_TOKEN)) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
153
159
|
out.push({ text: trimmed });
|
|
154
160
|
}
|
|
155
161
|
|
|
156
|
-
|
|
157
162
|
function clampMs(value: number, maxMs: number): number {
|
|
158
|
-
if (!Number.isFinite(value) || value < 0)
|
|
163
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
164
|
+
return 0;
|
|
165
|
+
}
|
|
159
166
|
return Math.min(value, maxMs);
|
|
160
167
|
}
|
|
161
168
|
|
|
162
169
|
async function sleep(ms: number): Promise<void> {
|
|
163
170
|
const delay = Math.max(0, ms);
|
|
164
|
-
if (delay === 0)
|
|
171
|
+
if (delay === 0) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
165
174
|
await new Promise<void>((resolve) => {
|
|
166
175
|
setTimeout(resolve, delay);
|
|
167
176
|
});
|
|
@@ -220,7 +229,9 @@ export function renderReplyPayloadsToMessages(
|
|
|
220
229
|
tableMode,
|
|
221
230
|
);
|
|
222
231
|
|
|
223
|
-
if (!text && mediaList.length === 0)
|
|
232
|
+
if (!text && mediaList.length === 0) {
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
224
235
|
|
|
225
236
|
if (mediaList.length === 0) {
|
|
226
237
|
pushTextMessages(out, text, { chunkText, chunkLimit, chunkMode });
|
|
@@ -234,7 +245,9 @@ export function renderReplyPayloadsToMessages(
|
|
|
234
245
|
out.push({ text: text || undefined, mediaUrl: firstMedia });
|
|
235
246
|
// Additional media URLs as separate messages
|
|
236
247
|
for (let i = 1; i < mediaList.length; i++) {
|
|
237
|
-
if (mediaList[i])
|
|
248
|
+
if (mediaList[i]) {
|
|
249
|
+
out.push({ mediaUrl: mediaList[i] });
|
|
250
|
+
}
|
|
238
251
|
}
|
|
239
252
|
} else {
|
|
240
253
|
pushTextMessages(out, text, { chunkText, chunkLimit, chunkMode });
|
|
@@ -245,7 +258,9 @@ export function renderReplyPayloadsToMessages(
|
|
|
245
258
|
// mediaMode === "split"
|
|
246
259
|
pushTextMessages(out, text, { chunkText, chunkLimit, chunkMode });
|
|
247
260
|
for (const mediaUrl of mediaList) {
|
|
248
|
-
if (!mediaUrl)
|
|
261
|
+
if (!mediaUrl) {
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
249
264
|
out.push({ mediaUrl });
|
|
250
265
|
}
|
|
251
266
|
}
|
|
@@ -283,12 +298,14 @@ async function buildActivity(
|
|
|
283
298
|
const isPersonal = conversationType === "personal";
|
|
284
299
|
const isImage = contentType?.startsWith("image/") ?? false;
|
|
285
300
|
|
|
286
|
-
if (
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
301
|
+
if (
|
|
302
|
+
requiresFileConsent({
|
|
303
|
+
conversationType,
|
|
304
|
+
contentType,
|
|
305
|
+
bufferSize: media.buffer.length,
|
|
306
|
+
thresholdBytes: FILE_CONSENT_THRESHOLD_BYTES,
|
|
307
|
+
})
|
|
308
|
+
) {
|
|
292
309
|
// Large file or non-image in personal chat: use FileConsentCard flow
|
|
293
310
|
const conversationId = conversationRef.conversation?.id ?? "unknown";
|
|
294
311
|
const { activity: consentActivity } = prepareFileConsentActivity({
|
|
@@ -382,7 +399,9 @@ export async function sendMSTeamsMessages(params: {
|
|
|
382
399
|
const messages = params.messages.filter(
|
|
383
400
|
(m) => (m.text && m.text.trim().length > 0) || m.mediaUrl,
|
|
384
401
|
);
|
|
385
|
-
if (messages.length === 0)
|
|
402
|
+
if (messages.length === 0) {
|
|
403
|
+
return [];
|
|
404
|
+
}
|
|
386
405
|
|
|
387
406
|
const retryOptions = resolveRetryOptions(params.retry);
|
|
388
407
|
|
|
@@ -390,7 +409,9 @@ export async function sendMSTeamsMessages(params: {
|
|
|
390
409
|
sendOnce: () => Promise<unknown>,
|
|
391
410
|
meta: { messageIndex: number; messageCount: number },
|
|
392
411
|
): Promise<unknown> => {
|
|
393
|
-
if (!retryOptions.enabled)
|
|
412
|
+
if (!retryOptions.enabled) {
|
|
413
|
+
return await sendOnce();
|
|
414
|
+
}
|
|
394
415
|
|
|
395
416
|
let attempt = 1;
|
|
396
417
|
while (true) {
|
|
@@ -399,7 +420,9 @@ export async function sendMSTeamsMessages(params: {
|
|
|
399
420
|
} catch (err) {
|
|
400
421
|
const classification = classifyMSTeamsSendError(err);
|
|
401
422
|
const canRetry = attempt < retryOptions.maxAttempts && shouldRetry(classification);
|
|
402
|
-
if (!canRetry)
|
|
423
|
+
if (!canRetry) {
|
|
424
|
+
throw err;
|
|
425
|
+
}
|
|
403
426
|
|
|
404
427
|
const delayMs = computeRetryDelayMs(attempt, classification, retryOptions);
|
|
405
428
|
const nextAttempt = attempt + 1;
|
|
@@ -428,7 +451,13 @@ export async function sendMSTeamsMessages(params: {
|
|
|
428
451
|
const response = await sendWithRetry(
|
|
429
452
|
async () =>
|
|
430
453
|
await ctx.sendActivity(
|
|
431
|
-
await buildActivity(
|
|
454
|
+
await buildActivity(
|
|
455
|
+
message,
|
|
456
|
+
params.conversationRef,
|
|
457
|
+
params.tokenProvider,
|
|
458
|
+
params.sharePointSiteId,
|
|
459
|
+
params.mediaMaxBytes,
|
|
460
|
+
),
|
|
432
461
|
),
|
|
433
462
|
{ messageIndex: idx, messageCount: messages.length },
|
|
434
463
|
);
|
|
@@ -449,7 +478,13 @@ export async function sendMSTeamsMessages(params: {
|
|
|
449
478
|
const response = await sendWithRetry(
|
|
450
479
|
async () =>
|
|
451
480
|
await ctx.sendActivity(
|
|
452
|
-
await buildActivity(
|
|
481
|
+
await buildActivity(
|
|
482
|
+
message,
|
|
483
|
+
params.conversationRef,
|
|
484
|
+
params.tokenProvider,
|
|
485
|
+
params.sharePointSiteId,
|
|
486
|
+
params.mediaMaxBytes,
|
|
487
|
+
),
|
|
453
488
|
),
|
|
454
489
|
{ messageIndex: idx, messageCount: messages.length },
|
|
455
490
|
);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { MSTeamsTurnContext } from "../sdk-types.js";
|
|
1
2
|
import {
|
|
2
3
|
buildMSTeamsGraphMessageUrls,
|
|
3
4
|
downloadMSTeamsAttachments,
|
|
@@ -7,7 +8,6 @@ import {
|
|
|
7
8
|
type MSTeamsHtmlAttachmentSummary,
|
|
8
9
|
type MSTeamsInboundMedia,
|
|
9
10
|
} from "../attachments.js";
|
|
10
|
-
import type { MSTeamsTurnContext } from "../sdk-types.js";
|
|
11
11
|
|
|
12
12
|
type MSTeamsLogger = {
|
|
13
13
|
debug: (message: string, meta?: Record<string, unknown>) => void;
|
|
@@ -18,6 +18,7 @@ export async function resolveMSTeamsInboundMedia(params: {
|
|
|
18
18
|
htmlSummary?: MSTeamsHtmlAttachmentSummary;
|
|
19
19
|
maxBytes: number;
|
|
20
20
|
allowHosts?: string[];
|
|
21
|
+
authAllowHosts?: string[];
|
|
21
22
|
tokenProvider: MSTeamsAccessTokenProvider;
|
|
22
23
|
conversationType: string;
|
|
23
24
|
conversationId: string;
|
|
@@ -46,6 +47,7 @@ export async function resolveMSTeamsInboundMedia(params: {
|
|
|
46
47
|
maxBytes,
|
|
47
48
|
tokenProvider,
|
|
48
49
|
allowHosts,
|
|
50
|
+
authAllowHosts: params.authAllowHosts,
|
|
49
51
|
preserveFilenames,
|
|
50
52
|
});
|
|
51
53
|
|
|
@@ -85,6 +87,7 @@ export async function resolveMSTeamsInboundMedia(params: {
|
|
|
85
87
|
tokenProvider,
|
|
86
88
|
maxBytes,
|
|
87
89
|
allowHosts,
|
|
90
|
+
authAllowHosts: params.authAllowHosts,
|
|
88
91
|
preserveFilenames,
|
|
89
92
|
});
|
|
90
93
|
attempts.push({
|
|
@@ -99,7 +102,9 @@ export async function resolveMSTeamsInboundMedia(params: {
|
|
|
99
102
|
mediaList = graphMedia.media;
|
|
100
103
|
break;
|
|
101
104
|
}
|
|
102
|
-
if (graphMedia.tokenError)
|
|
105
|
+
if (graphMedia.tokenError) {
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
103
108
|
}
|
|
104
109
|
if (mediaList.length === 0) {
|
|
105
110
|
log.debug("graph media fetch empty", { attempts });
|