@m1heng-clawd/feishu 0.1.15 → 0.1.16
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/package.json +1 -1
- package/src/bot.ts +2 -0
- package/src/chat-tools/actions.ts +102 -33
- package/src/chat-tools/schemas.ts +57 -62
- package/src/doc-tools/actions.ts +63 -14
- package/src/doc-tools/schemas.ts +41 -77
- package/src/doc-write-service.ts +19 -0
- package/src/drive-tools/actions.ts +18 -6
- package/src/drive-tools/schemas.ts +45 -58
- package/src/media.ts +11 -43
- package/src/outbound.ts +2 -2
- package/src/perm-tools/actions.ts +27 -3
- package/src/perm-tools/schemas.ts +39 -45
- package/src/task-tools/actions.ts +145 -10
- package/src/task-tools/register.ts +56 -1
- package/src/task-tools/schemas.ts +23 -18
- package/src/tools-common/tool-context.ts +1 -0
- package/src/wiki-tools/actions.ts +24 -6
- package/src/wiki-tools/schemas.ts +32 -51
|
@@ -5,6 +5,13 @@ import type { FeishuDriveParams } from "./schemas.js";
|
|
|
5
5
|
type DriveMoveType = "doc" | "docx" | "sheet" | "bitable" | "folder" | "file" | "mindnote" | "slides";
|
|
6
6
|
type DriveDeleteType = DriveMoveType | "shortcut";
|
|
7
7
|
|
|
8
|
+
function requireString(value: unknown, field: string): string {
|
|
9
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
10
|
+
throw new Error(`${field} is required`);
|
|
11
|
+
}
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
|
|
8
15
|
async function getFileType(client: DriveClient, fileToken: string): Promise<string> {
|
|
9
16
|
let pageToken: string | undefined;
|
|
10
17
|
|
|
@@ -196,18 +203,23 @@ export async function runDriveAction(
|
|
|
196
203
|
case "list":
|
|
197
204
|
return listFolder(client, params.folder_token);
|
|
198
205
|
case "info":
|
|
199
|
-
return getFileInfo(client, params.file_token);
|
|
206
|
+
return getFileInfo(client, requireString(params.file_token, "file_token"));
|
|
200
207
|
case "create_folder":
|
|
201
|
-
return createFolder(client, params.name, params.folder_token);
|
|
208
|
+
return createFolder(client, requireString(params.name, "name"), params.folder_token);
|
|
202
209
|
case "move":
|
|
203
|
-
return moveFile(
|
|
210
|
+
return moveFile(
|
|
211
|
+
client,
|
|
212
|
+
requireString(params.file_token, "file_token"),
|
|
213
|
+
requireString(params.type, "type"),
|
|
214
|
+
requireString(params.folder_token, "folder_token"),
|
|
215
|
+
);
|
|
204
216
|
case "delete":
|
|
205
|
-
return deleteFile(client, params.file_token, params.type);
|
|
217
|
+
return deleteFile(client, requireString(params.file_token, "file_token"), params.type);
|
|
206
218
|
case "import_document":
|
|
207
219
|
return importDocument(
|
|
208
220
|
client,
|
|
209
|
-
params.title,
|
|
210
|
-
params.content,
|
|
221
|
+
requireString(params.title, "title"),
|
|
222
|
+
requireString(params.content, "content"),
|
|
211
223
|
mediaMaxBytes,
|
|
212
224
|
params.folder_token,
|
|
213
225
|
params.doc_type || "docx",
|
|
@@ -1,67 +1,54 @@
|
|
|
1
1
|
import { Type, type Static } from "@sinclair/typebox";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Type.
|
|
8
|
-
|
|
9
|
-
Type.Literal("file"),
|
|
10
|
-
Type.Literal("mindnote"),
|
|
11
|
-
Type.Literal("shortcut"),
|
|
12
|
-
]);
|
|
3
|
+
function stringEnum<T extends readonly string[]>(
|
|
4
|
+
values: T,
|
|
5
|
+
options: { description?: string; default?: T[number] } = {},
|
|
6
|
+
) {
|
|
7
|
+
return Type.Unsafe<T[number]>({ type: "string", enum: [...values], ...options });
|
|
8
|
+
}
|
|
13
9
|
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
10
|
+
const FILE_TYPE_VALUES = [
|
|
11
|
+
"doc",
|
|
12
|
+
"docx",
|
|
13
|
+
"sheet",
|
|
14
|
+
"bitable",
|
|
15
|
+
"folder",
|
|
16
|
+
"file",
|
|
17
|
+
"mindnote",
|
|
18
|
+
"shortcut",
|
|
19
|
+
] as const;
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}),
|
|
38
|
-
Type.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
type: FileType,
|
|
42
|
-
folder_token: Type.String({ description: "Target folder token" }),
|
|
43
|
-
}),
|
|
44
|
-
Type.Object({
|
|
45
|
-
action: Type.Literal("delete"),
|
|
46
|
-
file_token: Type.String({ description: "File token to delete" }),
|
|
47
|
-
type: Type.Optional(FileType),
|
|
48
|
-
}),
|
|
49
|
-
Type.Object({
|
|
50
|
-
action: Type.Literal("import_document"),
|
|
51
|
-
title: Type.String({
|
|
52
|
-
description: "Document title",
|
|
53
|
-
}),
|
|
54
|
-
content: Type.String({
|
|
21
|
+
const DOC_TYPE_VALUES = ["docx", "doc"] as const;
|
|
22
|
+
|
|
23
|
+
const DRIVE_ACTION_VALUES = [
|
|
24
|
+
"list",
|
|
25
|
+
"info",
|
|
26
|
+
"create_folder",
|
|
27
|
+
"move",
|
|
28
|
+
"delete",
|
|
29
|
+
"import_document",
|
|
30
|
+
] as const;
|
|
31
|
+
|
|
32
|
+
export const FeishuDriveSchema = Type.Object({
|
|
33
|
+
action: stringEnum(DRIVE_ACTION_VALUES, { description: "Drive action" }),
|
|
34
|
+
folder_token: Type.Optional(
|
|
35
|
+
Type.String({ description: "Folder token (optional, omit for root directory)" }),
|
|
36
|
+
),
|
|
37
|
+
file_token: Type.Optional(Type.String({ description: "File or folder token" })),
|
|
38
|
+
type: Type.Optional(stringEnum(FILE_TYPE_VALUES, { description: "File type" })),
|
|
39
|
+
name: Type.Optional(Type.String({ description: "Folder name (for create_folder)" })),
|
|
40
|
+
title: Type.Optional(Type.String({ description: "Document title (for import_document)" })),
|
|
41
|
+
content: Type.Optional(
|
|
42
|
+
Type.String({
|
|
55
43
|
description:
|
|
56
44
|
"Markdown content to import. Supports full Markdown syntax including tables, lists, code blocks, etc.",
|
|
57
45
|
}),
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
),
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
]);
|
|
46
|
+
),
|
|
47
|
+
doc_type: Type.Optional(
|
|
48
|
+
stringEnum(DOC_TYPE_VALUES, {
|
|
49
|
+
description: "Document type for import_document (docx default, doc legacy)",
|
|
50
|
+
}),
|
|
51
|
+
),
|
|
52
|
+
});
|
|
66
53
|
|
|
67
54
|
export type FeishuDriveParams = Static<typeof FeishuDriveSchema>;
|
package/src/media.ts
CHANGED
|
@@ -237,23 +237,6 @@ export async function uploadImageFeishu(params: {
|
|
|
237
237
|
return { imageKey };
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
-
/**
|
|
241
|
-
* Encode a filename for safe use in Feishu multipart/form-data uploads.
|
|
242
|
-
* Non-ASCII characters (Chinese, em-dash, full-width brackets, etc.) cause
|
|
243
|
-
* the upload to silently fail when passed raw through the SDK's form-data
|
|
244
|
-
* serialization. RFC 5987 percent-encoding keeps headers 7-bit clean while
|
|
245
|
-
* Feishu's server decodes and preserves the original display name.
|
|
246
|
-
*/
|
|
247
|
-
export function sanitizeFileNameForUpload(fileName: string): string {
|
|
248
|
-
const ASCII_ONLY = /^[\x20-\x7E]+$/;
|
|
249
|
-
if (ASCII_ONLY.test(fileName)) {
|
|
250
|
-
return fileName;
|
|
251
|
-
}
|
|
252
|
-
return encodeURIComponent(fileName)
|
|
253
|
-
.replace(/'/g, "%27")
|
|
254
|
-
.replace(/\(/g, "%28")
|
|
255
|
-
.replace(/\)/g, "%29");
|
|
256
|
-
}
|
|
257
240
|
|
|
258
241
|
/**
|
|
259
242
|
* Upload a file to Feishu and get a file_key for sending.
|
|
@@ -280,12 +263,10 @@ export async function uploadFileFeishu(params: {
|
|
|
280
263
|
const fileStream =
|
|
281
264
|
typeof file === "string" ? fs.createReadStream(file) : Readable.from(file);
|
|
282
265
|
|
|
283
|
-
const safeFileName = sanitizeFileNameForUpload(fileName);
|
|
284
|
-
|
|
285
266
|
const response = await client.im.file.create({
|
|
286
267
|
data: {
|
|
287
268
|
file_type: fileType,
|
|
288
|
-
file_name:
|
|
269
|
+
file_name: fileName,
|
|
289
270
|
file: fileStream as any,
|
|
290
271
|
...(duration !== undefined && { duration }),
|
|
291
272
|
},
|
|
@@ -496,9 +477,10 @@ export async function sendMediaFeishu(params: {
|
|
|
496
477
|
mediaBuffer?: Buffer;
|
|
497
478
|
fileName?: string;
|
|
498
479
|
replyToMessageId?: string;
|
|
480
|
+
mediaLocalRoots?: readonly string[];
|
|
499
481
|
accountId?: string;
|
|
500
482
|
}): Promise<SendMediaResult> {
|
|
501
|
-
const { cfg, to, mediaUrl, mediaBuffer, fileName, replyToMessageId, accountId } = params;
|
|
483
|
+
const { cfg, to, mediaUrl, mediaBuffer, fileName, replyToMessageId, mediaLocalRoots, accountId } = params;
|
|
502
484
|
const account = resolveFeishuAccount({ cfg, accountId });
|
|
503
485
|
if (!account.configured) {
|
|
504
486
|
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
|
@@ -513,30 +495,16 @@ export async function sendMediaFeishu(params: {
|
|
|
513
495
|
buffer = mediaBuffer;
|
|
514
496
|
name = fileName ?? "file";
|
|
515
497
|
} else if (mediaUrl) {
|
|
516
|
-
const
|
|
498
|
+
const configLocalRoots = (account.config?.mediaLocalRoots ?? [])
|
|
517
499
|
.map((root) => root.trim())
|
|
518
500
|
.filter((root) => root.length > 0);
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
const shouldRetryWithCustomRoots =
|
|
527
|
-
mediaLocalRoots.length > 0 &&
|
|
528
|
-
err instanceof Error &&
|
|
529
|
-
err.message.includes("Local media path is not under an allowed directory");
|
|
530
|
-
if (!shouldRetryWithCustomRoots) {
|
|
531
|
-
throw err;
|
|
532
|
-
}
|
|
533
|
-
return await getFeishuRuntime().media.loadWebMedia(mediaUrl, {
|
|
534
|
-
maxBytes: mediaMaxBytes,
|
|
535
|
-
optimizeImages: false,
|
|
536
|
-
localRoots: mediaLocalRoots,
|
|
537
|
-
});
|
|
538
|
-
}
|
|
539
|
-
})();
|
|
501
|
+
// Merge context-provided roots (includes agent workspace) with config roots.
|
|
502
|
+
const mergedLocalRoots = [...(mediaLocalRoots ?? []), ...configLocalRoots];
|
|
503
|
+
const loaded = await getFeishuRuntime().media.loadWebMedia(mediaUrl, {
|
|
504
|
+
maxBytes: mediaMaxBytes,
|
|
505
|
+
optimizeImages: false,
|
|
506
|
+
...(mergedLocalRoots.length > 0 ? { localRoots: mergedLocalRoots } : {}),
|
|
507
|
+
});
|
|
540
508
|
buffer = loaded.buffer;
|
|
541
509
|
name = fileName ?? loaded.fileName ?? "file";
|
|
542
510
|
contentType = loaded.contentType;
|
package/src/outbound.ts
CHANGED
|
@@ -12,7 +12,7 @@ export const feishuOutbound: ChannelOutboundAdapter = {
|
|
|
12
12
|
const result = await sendMessageFeishu({ cfg, to, text, accountId });
|
|
13
13
|
return { channel: "feishu", ...result };
|
|
14
14
|
},
|
|
15
|
-
sendMedia: async ({ cfg, to, text, mediaUrl, accountId }) => {
|
|
15
|
+
sendMedia: async ({ cfg, to, text, mediaUrl, mediaLocalRoots, accountId }) => {
|
|
16
16
|
// Send text first if provided
|
|
17
17
|
if (text?.trim()) {
|
|
18
18
|
await sendMessageFeishu({ cfg, to, text, accountId });
|
|
@@ -21,7 +21,7 @@ export const feishuOutbound: ChannelOutboundAdapter = {
|
|
|
21
21
|
// Upload and send media if URL provided
|
|
22
22
|
if (mediaUrl) {
|
|
23
23
|
try {
|
|
24
|
-
const result = await sendMediaFeishu({ cfg, to, mediaUrl, accountId });
|
|
24
|
+
const result = await sendMediaFeishu({ cfg, to, mediaUrl, mediaLocalRoots, accountId });
|
|
25
25
|
return { channel: "feishu", ...result };
|
|
26
26
|
} catch (err) {
|
|
27
27
|
// Log the error for debugging
|
|
@@ -33,6 +33,13 @@ type MemberType =
|
|
|
33
33
|
| "wikispaceid";
|
|
34
34
|
type PermType = "view" | "edit" | "full_access";
|
|
35
35
|
|
|
36
|
+
function requireString(value: unknown, field: string): string {
|
|
37
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
38
|
+
throw new Error(`${field} is required`);
|
|
39
|
+
}
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
|
|
36
43
|
async function listMembers(client: PermClient, token: string, type: string) {
|
|
37
44
|
const res = await runPermApiCall("drive.permissionMember.list", () =>
|
|
38
45
|
client.drive.permissionMember.list({
|
|
@@ -100,11 +107,28 @@ async function removeMember(
|
|
|
100
107
|
export async function runPermAction(client: PermClient, params: FeishuPermParams) {
|
|
101
108
|
switch (params.action) {
|
|
102
109
|
case "list":
|
|
103
|
-
return listMembers(
|
|
110
|
+
return listMembers(
|
|
111
|
+
client,
|
|
112
|
+
requireString(params.token, "token"),
|
|
113
|
+
requireString(params.type, "type"),
|
|
114
|
+
);
|
|
104
115
|
case "add":
|
|
105
|
-
return addMember(
|
|
116
|
+
return addMember(
|
|
117
|
+
client,
|
|
118
|
+
requireString(params.token, "token"),
|
|
119
|
+
requireString(params.type, "type"),
|
|
120
|
+
requireString(params.member_type, "member_type"),
|
|
121
|
+
requireString(params.member_id, "member_id"),
|
|
122
|
+
requireString(params.perm, "perm"),
|
|
123
|
+
);
|
|
106
124
|
case "remove":
|
|
107
|
-
return removeMember(
|
|
125
|
+
return removeMember(
|
|
126
|
+
client,
|
|
127
|
+
requireString(params.token, "token"),
|
|
128
|
+
requireString(params.type, "type"),
|
|
129
|
+
requireString(params.member_type, "member_type"),
|
|
130
|
+
requireString(params.member_id, "member_id"),
|
|
131
|
+
);
|
|
108
132
|
default:
|
|
109
133
|
return { error: `Unknown action: ${(params as any).action}` };
|
|
110
134
|
}
|
|
@@ -1,52 +1,46 @@
|
|
|
1
1
|
import { Type, type Static } from "@sinclair/typebox";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Type.
|
|
8
|
-
|
|
9
|
-
Type.Literal("file"),
|
|
10
|
-
Type.Literal("wiki"),
|
|
11
|
-
Type.Literal("mindnote"),
|
|
12
|
-
]);
|
|
3
|
+
function stringEnum<T extends readonly string[]>(
|
|
4
|
+
values: T,
|
|
5
|
+
options: { description?: string; default?: T[number] } = {},
|
|
6
|
+
) {
|
|
7
|
+
return Type.Unsafe<T[number]>({ type: "string", enum: [...values], ...options });
|
|
8
|
+
}
|
|
13
9
|
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
10
|
+
const TOKEN_TYPE_VALUES = [
|
|
11
|
+
"doc",
|
|
12
|
+
"docx",
|
|
13
|
+
"sheet",
|
|
14
|
+
"bitable",
|
|
15
|
+
"folder",
|
|
16
|
+
"file",
|
|
17
|
+
"wiki",
|
|
18
|
+
"mindnote",
|
|
19
|
+
] as const;
|
|
22
20
|
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
const MEMBER_TYPE_VALUES = [
|
|
22
|
+
"email",
|
|
23
|
+
"openid",
|
|
24
|
+
"userid",
|
|
25
|
+
"unionid",
|
|
26
|
+
"openchat",
|
|
27
|
+
"opendepartmentid",
|
|
28
|
+
] as const;
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}),
|
|
35
|
-
Type.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}),
|
|
43
|
-
|
|
44
|
-
action: Type.Literal("remove"),
|
|
45
|
-
token: Type.String({ description: "File token" }),
|
|
46
|
-
type: TokenType,
|
|
47
|
-
member_type: MemberType,
|
|
48
|
-
member_id: Type.String({ description: "Member ID to remove" }),
|
|
49
|
-
}),
|
|
50
|
-
]);
|
|
30
|
+
const PERMISSION_VALUES = ["view", "edit", "full_access"] as const;
|
|
31
|
+
const PERM_ACTION_VALUES = ["list", "add", "remove"] as const;
|
|
32
|
+
|
|
33
|
+
export const FeishuPermSchema = Type.Object({
|
|
34
|
+
action: stringEnum(PERM_ACTION_VALUES, { description: "Permission action" }),
|
|
35
|
+
token: Type.Optional(Type.String({ description: "File token" })),
|
|
36
|
+
type: Type.Optional(stringEnum(TOKEN_TYPE_VALUES, { description: "File token type" })),
|
|
37
|
+
member_type: Type.Optional(
|
|
38
|
+
stringEnum(MEMBER_TYPE_VALUES, {
|
|
39
|
+
description: "Member ID type (email/openid/userid/unionid/openchat/opendepartmentid)",
|
|
40
|
+
}),
|
|
41
|
+
),
|
|
42
|
+
member_id: Type.Optional(Type.String({ description: "Member ID" })),
|
|
43
|
+
perm: Type.Optional(stringEnum(PERMISSION_VALUES, { description: "Permission level" })),
|
|
44
|
+
});
|
|
51
45
|
|
|
52
46
|
export type FeishuPermParams = Static<typeof FeishuPermSchema>;
|
|
@@ -3,26 +3,33 @@ import os from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import type { TaskClient } from "./common.js";
|
|
5
5
|
import {
|
|
6
|
+
TASK_COMMENT_UPDATE_FIELD_VALUES,
|
|
6
7
|
TASKLIST_UPDATE_FIELD_VALUES,
|
|
7
8
|
TASK_UPDATE_FIELD_VALUES,
|
|
8
9
|
type AddTaskToTasklistParams,
|
|
9
10
|
type AddTasklistMembersParams,
|
|
11
|
+
type CreateTaskCommentParams,
|
|
10
12
|
type CreateTasklistParams,
|
|
11
|
-
CreateSubtaskParams,
|
|
12
|
-
CreateTaskParams,
|
|
13
|
-
DeleteTaskAttachmentParams,
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
type CreateSubtaskParams,
|
|
14
|
+
type CreateTaskParams,
|
|
15
|
+
type DeleteTaskAttachmentParams,
|
|
16
|
+
type DeleteTaskCommentParams,
|
|
17
|
+
type GetTaskAttachmentParams,
|
|
18
|
+
type GetTaskCommentParams,
|
|
19
|
+
type GetTaskParams,
|
|
16
20
|
type GetTasklistParams,
|
|
17
|
-
ListTaskAttachmentsParams,
|
|
21
|
+
type ListTaskAttachmentsParams,
|
|
22
|
+
type ListTaskCommentsParams,
|
|
18
23
|
type ListTasklistsParams,
|
|
19
24
|
type RemoveTaskFromTasklistParams,
|
|
20
25
|
type RemoveTasklistMembersParams,
|
|
21
|
-
|
|
26
|
+
type TaskCommentPatchComment,
|
|
27
|
+
type TaskUpdateTask,
|
|
22
28
|
type TasklistPatchTasklist,
|
|
23
|
-
UploadTaskAttachmentParams,
|
|
29
|
+
type UploadTaskAttachmentParams,
|
|
30
|
+
type UpdateTaskCommentParams,
|
|
24
31
|
type UpdateTasklistParams,
|
|
25
|
-
UpdateTaskParams,
|
|
32
|
+
type UpdateTaskParams,
|
|
26
33
|
} from "./schemas.js";
|
|
27
34
|
import {
|
|
28
35
|
DEFAULT_TASK_ATTACHMENT_FILENAME,
|
|
@@ -36,6 +43,7 @@ import { getFeishuRuntime } from "../runtime.js";
|
|
|
36
43
|
import { runTaskApiCall } from "./common.js";
|
|
37
44
|
|
|
38
45
|
const SUPPORTED_PATCH_FIELDS = new Set<string>(TASK_UPDATE_FIELD_VALUES);
|
|
46
|
+
const SUPPORTED_COMMENT_PATCH_FIELDS = new Set<string>(TASK_COMMENT_UPDATE_FIELD_VALUES);
|
|
39
47
|
const SUPPORTED_TASKLIST_PATCH_FIELDS = new Set<string>(TASKLIST_UPDATE_FIELD_VALUES);
|
|
40
48
|
|
|
41
49
|
function omitUndefined<T extends Record<string, unknown>>(obj: T): T {
|
|
@@ -50,10 +58,16 @@ function inferUpdateFields(task: TaskUpdateTask): string[] {
|
|
|
50
58
|
);
|
|
51
59
|
}
|
|
52
60
|
|
|
61
|
+
function inferCommentUpdateFields(comment: TaskCommentPatchComment): string[] {
|
|
62
|
+
return Object.keys(comment).filter((field) =>
|
|
63
|
+
SUPPORTED_COMMENT_PATCH_FIELDS.has(field),
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
53
67
|
function ensureSupportedUpdateFields(
|
|
54
68
|
updateFields: string[],
|
|
55
69
|
supported: Set<string>,
|
|
56
|
-
resource: "task" | "tasklist",
|
|
70
|
+
resource: "task" | "comment" | "tasklist",
|
|
57
71
|
) {
|
|
58
72
|
const invalid = updateFields.filter((field) => !supported.has(field));
|
|
59
73
|
if (invalid.length > 0) {
|
|
@@ -102,6 +116,20 @@ function formatTasklist(tasklist: Record<string, unknown> | undefined) {
|
|
|
102
116
|
};
|
|
103
117
|
}
|
|
104
118
|
|
|
119
|
+
function formatComment(comment: Record<string, unknown> | undefined) {
|
|
120
|
+
if (!comment) return undefined;
|
|
121
|
+
return {
|
|
122
|
+
id: comment.id,
|
|
123
|
+
content: comment.content,
|
|
124
|
+
creator: comment.creator,
|
|
125
|
+
reply_to_comment_id: comment.reply_to_comment_id,
|
|
126
|
+
created_at: comment.created_at,
|
|
127
|
+
updated_at: comment.updated_at,
|
|
128
|
+
resource_type: comment.resource_type,
|
|
129
|
+
resource_id: comment.resource_id,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
105
133
|
function formatAttachment(attachment: Record<string, unknown> | undefined) {
|
|
106
134
|
if (!attachment) return undefined;
|
|
107
135
|
return {
|
|
@@ -307,6 +335,113 @@ export async function getTask(client: TaskClient, params: GetTaskParams) {
|
|
|
307
335
|
};
|
|
308
336
|
}
|
|
309
337
|
|
|
338
|
+
export async function createTaskComment(client: TaskClient, params: CreateTaskCommentParams) {
|
|
339
|
+
const res = await runTaskApiCall("task.v2.comment.create", () =>
|
|
340
|
+
client.task.v2.comment.create({
|
|
341
|
+
data: omitUndefined({
|
|
342
|
+
content: params.content,
|
|
343
|
+
reply_to_comment_id: params.reply_to_comment_id,
|
|
344
|
+
resource_type: "task",
|
|
345
|
+
resource_id: params.task_guid,
|
|
346
|
+
}),
|
|
347
|
+
params: omitUndefined({
|
|
348
|
+
user_id_type: params.user_id_type,
|
|
349
|
+
}),
|
|
350
|
+
}),
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
return {
|
|
354
|
+
comment: formatComment((res.data?.comment ?? undefined) as Record<string, unknown> | undefined),
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export async function listTaskComments(client: TaskClient, params: ListTaskCommentsParams) {
|
|
359
|
+
const res = await runTaskApiCall("task.v2.comment.list", () =>
|
|
360
|
+
client.task.v2.comment.list({
|
|
361
|
+
params: omitUndefined({
|
|
362
|
+
resource_type: "task",
|
|
363
|
+
resource_id: params.task_guid,
|
|
364
|
+
page_size: params.page_size,
|
|
365
|
+
page_token: params.page_token,
|
|
366
|
+
direction: params.direction,
|
|
367
|
+
user_id_type: params.user_id_type,
|
|
368
|
+
}),
|
|
369
|
+
}),
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
const items = (res.data?.items ?? []) as Record<string, unknown>[];
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
items: items.map((item) => formatComment(item)),
|
|
376
|
+
page_token: res.data?.page_token,
|
|
377
|
+
has_more: res.data?.has_more,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export async function getTaskComment(client: TaskClient, params: GetTaskCommentParams) {
|
|
382
|
+
const res = await runTaskApiCall("task.v2.comment.get", () =>
|
|
383
|
+
client.task.v2.comment.get({
|
|
384
|
+
path: { comment_id: params.comment_id },
|
|
385
|
+
params: omitUndefined({
|
|
386
|
+
user_id_type: params.user_id_type,
|
|
387
|
+
}),
|
|
388
|
+
}),
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
comment: formatComment((res.data?.comment ?? undefined) as Record<string, unknown> | undefined),
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export async function updateTaskComment(client: TaskClient, params: UpdateTaskCommentParams) {
|
|
397
|
+
const comment = omitUndefined(params.comment as Record<string, unknown>) as TaskCommentPatchComment;
|
|
398
|
+
const updateFields = params.update_fields?.length
|
|
399
|
+
? params.update_fields
|
|
400
|
+
: inferCommentUpdateFields(comment);
|
|
401
|
+
|
|
402
|
+
if (params.update_fields?.length) {
|
|
403
|
+
ensureSupportedUpdateFields(updateFields, SUPPORTED_COMMENT_PATCH_FIELDS, "comment");
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (Object.keys(comment).length === 0) {
|
|
407
|
+
throw new Error("comment update payload is empty");
|
|
408
|
+
}
|
|
409
|
+
if (updateFields.length === 0) {
|
|
410
|
+
throw new Error("no valid update_fields provided or inferred from comment payload");
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const res = await runTaskApiCall("task.v2.comment.patch", () =>
|
|
414
|
+
client.task.v2.comment.patch({
|
|
415
|
+
path: { comment_id: params.comment_id },
|
|
416
|
+
data: {
|
|
417
|
+
comment,
|
|
418
|
+
update_fields: updateFields,
|
|
419
|
+
},
|
|
420
|
+
params: omitUndefined({
|
|
421
|
+
user_id_type: params.user_id_type,
|
|
422
|
+
}),
|
|
423
|
+
}),
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
return {
|
|
427
|
+
comment: formatComment((res.data?.comment ?? undefined) as Record<string, unknown> | undefined),
|
|
428
|
+
update_fields: updateFields,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export async function deleteTaskComment(client: TaskClient, params: DeleteTaskCommentParams) {
|
|
433
|
+
await runTaskApiCall("task.v2.comment.delete", () =>
|
|
434
|
+
client.task.v2.comment.delete({
|
|
435
|
+
path: { comment_id: params.comment_id },
|
|
436
|
+
}),
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
success: true,
|
|
441
|
+
comment_id: params.comment_id,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
310
445
|
export async function getTasklist(client: TaskClient, params: GetTasklistParams) {
|
|
311
446
|
const res = await runTaskApiCall("task.v2.tasklist.get", () =>
|
|
312
447
|
client.task.v2.tasklist.get({
|