@m1heng-clawd/feishu 0.1.10 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +280 -7
  2. package/index.ts +6 -6
  3. package/package.json +13 -3
  4. package/skills/feishu-doc/SKILL.md +64 -2
  5. package/skills/feishu-task/SKILL.md +210 -0
  6. package/src/bitable-tools/index.ts +1 -0
  7. package/src/bot.ts +182 -71
  8. package/src/channel.ts +2 -0
  9. package/src/config-schema.ts +4 -0
  10. package/src/doc-tools/actions.ts +341 -0
  11. package/src/doc-tools/common.ts +33 -0
  12. package/src/doc-tools/index.ts +2 -0
  13. package/src/doc-tools/register.ts +90 -0
  14. package/src/{doc-schema.ts → doc-tools/schemas.ts} +39 -1
  15. package/src/{docx.ts → doc-write-service.ts} +204 -351
  16. package/src/drive-tools/actions.ts +182 -0
  17. package/src/drive-tools/common.ts +18 -0
  18. package/src/drive-tools/index.ts +2 -0
  19. package/src/drive-tools/register.ts +71 -0
  20. package/src/{drive-schema.ts → drive-tools/schemas.ts} +2 -1
  21. package/src/media.ts +5 -3
  22. package/src/onboarding.ts +14 -4
  23. package/src/perm-tools/actions.ts +111 -0
  24. package/src/perm-tools/common.ts +18 -0
  25. package/src/perm-tools/index.ts +2 -0
  26. package/src/perm-tools/register.ts +65 -0
  27. package/src/policy.ts +13 -0
  28. package/src/reply-dispatcher.ts +6 -4
  29. package/src/send.ts +33 -2
  30. package/src/task-tools/actions.ts +466 -13
  31. package/src/task-tools/constants.ts +13 -0
  32. package/src/task-tools/index.ts +1 -0
  33. package/src/task-tools/register.ts +175 -13
  34. package/src/task-tools/schemas.ts +433 -4
  35. package/src/text/markdown-links.ts +104 -0
  36. package/src/types.ts +1 -0
  37. package/src/wiki-tools/actions.ts +166 -0
  38. package/src/wiki-tools/common.ts +18 -0
  39. package/src/wiki-tools/index.ts +2 -0
  40. package/src/wiki-tools/register.ts +66 -0
  41. package/src/bitable.ts +0 -1
  42. package/src/drive.ts +0 -264
  43. package/src/perm.ts +0 -165
  44. package/src/task.ts +0 -1
  45. package/src/wiki.ts +0 -223
  46. /package/src/{perm-schema.ts → perm-tools/schemas.ts} +0 -0
  47. /package/src/{wiki-schema.ts → wiki-tools/schemas.ts} +0 -0
package/src/send.ts CHANGED
@@ -3,6 +3,7 @@ import type { FeishuSendResult, ResolvedFeishuAccount } from "./types.js";
3
3
  import type { MentionTarget } from "./mention.js";
4
4
  import { buildMentionedMessage, buildMentionedCardContent } from "./mention.js";
5
5
  import { createFeishuClient } from "./client.js";
6
+ import { normalizeFeishuMarkdownLinks } from "./text/markdown-links.js";
6
7
  import { resolveReceiveIdType, normalizeFeishuTarget } from "./targets.js";
7
8
  import { getFeishuRuntime } from "./runtime.js";
8
9
  import { resolveFeishuAccount } from "./accounts.js";
@@ -71,6 +72,31 @@ export async function getMessageFeishu(params: {
71
72
  const parsed = JSON.parse(content);
72
73
  if (item.msg_type === "text" && parsed.text) {
73
74
  content = parsed.text;
75
+ } else if (parsed.content || parsed.elements) {
76
+ // Extract plain text from rich text (post) or interactive (card) format.
77
+ // Both use nested arrays: Array<Array<{tag, text?, href?, ...}>>
78
+ const blocks = parsed.content ?? parsed.elements ?? [];
79
+ const lines: string[] = [];
80
+ for (const paragraph of blocks) {
81
+ if (!Array.isArray(paragraph)) continue;
82
+ const line = paragraph
83
+ .map((node: { tag?: string; text?: string; href?: string }) => {
84
+ if (node.tag === "text") return node.text ?? "";
85
+ if (node.tag === "a") return node.text ?? node.href ?? "";
86
+ if (node.tag === "at") return "";
87
+ if (node.tag === "img") return "[图片]";
88
+ return node.text ?? "";
89
+ })
90
+ .join("");
91
+ if (line.trim()) lines.push(line);
92
+ }
93
+ const extracted = (parsed.title ? parsed.title + "\n" : "") + lines.join("\n");
94
+ // Filter out Feishu's degraded card placeholder text
95
+ if (extracted.trim() && !extracted.includes("请升级至最新版本客户端")) {
96
+ content = extracted;
97
+ } else if (extracted.includes("请升级至最新版本客户端")) {
98
+ content = "[卡片消息]";
99
+ }
74
100
  }
75
101
  } catch {
76
102
  // Keep raw content if parsing fails
@@ -147,7 +173,9 @@ export async function sendMessageFeishu(params: SendFeishuMessageParams): Promis
147
173
  if (mentions && mentions.length > 0) {
148
174
  rawText = buildMentionedMessage(mentions, rawText);
149
175
  }
150
- const messageText = getFeishuRuntime().channel.text.convertMarkdownTables(rawText, tableMode);
176
+ const messageText = normalizeFeishuMarkdownLinks(
177
+ getFeishuRuntime().channel.text.convertMarkdownTables(rawText, tableMode),
178
+ );
151
179
 
152
180
  const { content, msgType } = buildFeishuPostMessagePayload({ messageText });
153
181
 
@@ -317,6 +345,7 @@ export async function sendMarkdownCardFeishu(params: {
317
345
  if (mentions && mentions.length > 0) {
318
346
  cardText = buildMentionedCardContent(mentions, text);
319
347
  }
348
+ cardText = normalizeFeishuMarkdownLinks(cardText);
320
349
  const card = buildMarkdownCard(cardText);
321
350
  return sendCardFeishu({ cfg, to, card, replyToMessageId, accountId });
322
351
  }
@@ -342,7 +371,9 @@ export async function editMessageFeishu(params: {
342
371
  cfg,
343
372
  channel: "feishu",
344
373
  });
345
- const messageText = getFeishuRuntime().channel.text.convertMarkdownTables(text ?? "", tableMode);
374
+ const messageText = normalizeFeishuMarkdownLinks(
375
+ getFeishuRuntime().channel.text.convertMarkdownTables(text ?? "", tableMode),
376
+ );
346
377
 
347
378
  const { content, msgType } = buildFeishuPostMessagePayload({ messageText });
348
379
 
@@ -1,23 +1,42 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
1
4
  import type { TaskClient } from "./common.js";
2
- import type {
5
+ import {
6
+ TASKLIST_UPDATE_FIELD_VALUES,
7
+ TASK_UPDATE_FIELD_VALUES,
8
+ type AddTaskToTasklistParams,
9
+ type AddTasklistMembersParams,
10
+ type CreateTasklistParams,
11
+ CreateSubtaskParams,
3
12
  CreateTaskParams,
13
+ DeleteTaskAttachmentParams,
14
+ GetTaskAttachmentParams,
4
15
  GetTaskParams,
16
+ type GetTasklistParams,
17
+ ListTaskAttachmentsParams,
18
+ type ListTasklistsParams,
19
+ type RemoveTaskFromTasklistParams,
20
+ type RemoveTasklistMembersParams,
5
21
  TaskUpdateTask,
22
+ type TasklistPatchTasklist,
23
+ UploadTaskAttachmentParams,
24
+ type UpdateTasklistParams,
6
25
  UpdateTaskParams,
7
26
  } from "./schemas.js";
27
+ import {
28
+ DEFAULT_TASK_ATTACHMENT_FILENAME,
29
+ DEFAULT_TASK_ATTACHMENT_MAX_BYTES,
30
+ BYTES_PER_MEGABYTE,
31
+ HEX_RADIX,
32
+ RANDOM_TOKEN_PREFIX_LENGTH,
33
+ SIZE_DISPLAY_FRACTION_DIGITS,
34
+ } from "./constants.js";
35
+ import { getFeishuRuntime } from "../runtime.js";
8
36
  import { runTaskApiCall } from "./common.js";
9
37
 
10
- const SUPPORTED_PATCH_FIELDS = new Set<keyof TaskUpdateTask>([
11
- "summary",
12
- "description",
13
- "due",
14
- "start",
15
- "extra",
16
- "completed_at",
17
- "repeat_rule",
18
- "mode",
19
- "is_milestone",
20
- ]);
38
+ const SUPPORTED_PATCH_FIELDS = new Set<string>(TASK_UPDATE_FIELD_VALUES);
39
+ const SUPPORTED_TASKLIST_PATCH_FIELDS = new Set<string>(TASKLIST_UPDATE_FIELD_VALUES);
21
40
 
22
41
  function omitUndefined<T extends Record<string, unknown>>(obj: T): T {
23
42
  return Object.fromEntries(
@@ -27,7 +46,24 @@ function omitUndefined<T extends Record<string, unknown>>(obj: T): T {
27
46
 
28
47
  function inferUpdateFields(task: TaskUpdateTask): string[] {
29
48
  return Object.keys(task).filter((field) =>
30
- SUPPORTED_PATCH_FIELDS.has(field as keyof TaskUpdateTask),
49
+ SUPPORTED_PATCH_FIELDS.has(field),
50
+ );
51
+ }
52
+
53
+ function ensureSupportedUpdateFields(
54
+ updateFields: string[],
55
+ supported: Set<string>,
56
+ resource: "task" | "tasklist",
57
+ ) {
58
+ const invalid = updateFields.filter((field) => !supported.has(field));
59
+ if (invalid.length > 0) {
60
+ throw new Error(`unsupported ${resource} update_fields: ${invalid.join(", ")}`);
61
+ }
62
+ }
63
+
64
+ function inferTasklistUpdateFields(tasklist: TasklistPatchTasklist): string[] {
65
+ return Object.keys(tasklist).filter((field) =>
66
+ SUPPORTED_TASKLIST_PATCH_FIELDS.has(field),
31
67
  );
32
68
  }
33
69
 
@@ -51,6 +87,96 @@ function formatTask(task: Record<string, unknown> | undefined) {
51
87
  };
52
88
  }
53
89
 
90
+ function formatTasklist(tasklist: Record<string, unknown> | undefined) {
91
+ if (!tasklist) return undefined;
92
+ return {
93
+ guid: tasklist.guid,
94
+ name: tasklist.name,
95
+ creator: tasklist.creator,
96
+ owner: tasklist.owner,
97
+ members: tasklist.members,
98
+ url: tasklist.url,
99
+ created_at: tasklist.created_at,
100
+ updated_at: tasklist.updated_at,
101
+ archive_msec: tasklist.archive_msec,
102
+ };
103
+ }
104
+
105
+ function formatAttachment(attachment: Record<string, unknown> | undefined) {
106
+ if (!attachment) return undefined;
107
+ return {
108
+ guid: attachment.guid,
109
+ file_token: attachment.file_token,
110
+ name: attachment.name,
111
+ size: attachment.size,
112
+ uploader: attachment.uploader,
113
+ is_cover: attachment.is_cover,
114
+ uploaded_at: attachment.uploaded_at,
115
+ url: attachment.url,
116
+ resource: attachment.resource,
117
+ };
118
+ }
119
+
120
+ function sanitizeUploadFilename(input: string) {
121
+ const base = path.basename(input.trim());
122
+ return base.length > 0 ? base : DEFAULT_TASK_ATTACHMENT_FILENAME;
123
+ }
124
+
125
+ async function ensureUploadableLocalFile(filePath: string, maxBytes: number) {
126
+ let stat: fs.Stats;
127
+ try {
128
+ stat = await fs.promises.stat(filePath);
129
+ } catch {
130
+ throw new Error(`file_path not found: ${filePath}`);
131
+ }
132
+
133
+ if (!stat.isFile()) {
134
+ throw new Error(`file_path is not a regular file: ${filePath}`);
135
+ }
136
+
137
+ if (stat.size > maxBytes) {
138
+ throw new Error(
139
+ `file_path exceeds ${(maxBytes / BYTES_PER_MEGABYTE).toFixed(SIZE_DISPLAY_FRACTION_DIGITS)}MB limit: ${filePath}`,
140
+ );
141
+ }
142
+ }
143
+
144
+ async function saveBufferToTempFile(buffer: Buffer, fileName: string) {
145
+ const safeName = sanitizeUploadFilename(fileName);
146
+ const tempPath = path.join(
147
+ os.tmpdir(),
148
+ `feishu-task-attachment-${Date.now()}-${Math.random().toString(HEX_RADIX).slice(RANDOM_TOKEN_PREFIX_LENGTH)}-${safeName}`,
149
+ );
150
+
151
+ await fs.promises.writeFile(tempPath, buffer);
152
+
153
+ return {
154
+ tempPath,
155
+ cleanup: async () => {
156
+ await fs.promises.unlink(tempPath).catch(() => undefined);
157
+ },
158
+ };
159
+ }
160
+
161
+ async function downloadToTempFile(fileUrl: string, filename: string | undefined, maxBytes: number) {
162
+ const loaded = await getFeishuRuntime().media.loadWebMedia(fileUrl, {
163
+ maxBytes,
164
+ optimizeImages: false,
165
+ });
166
+
167
+ const parsedPath = (() => {
168
+ try {
169
+ return new URL(fileUrl).pathname;
170
+ } catch {
171
+ return "";
172
+ }
173
+ })();
174
+
175
+ const fallbackName = path.basename(parsedPath) || DEFAULT_TASK_ATTACHMENT_FILENAME;
176
+ const preferredName = filename?.trim() ? filename : loaded.fileName ?? fallbackName;
177
+ return saveBufferToTempFile(loaded.buffer, preferredName);
178
+ }
179
+
54
180
  export async function createTask(client: TaskClient, params: CreateTaskParams) {
55
181
  const res = await runTaskApiCall("task.v2.task.create", () =>
56
182
  client.task.v2.task.create({
@@ -78,6 +204,68 @@ export async function createTask(client: TaskClient, params: CreateTaskParams) {
78
204
  };
79
205
  }
80
206
 
207
+ export async function createSubtask(client: TaskClient, params: CreateSubtaskParams) {
208
+ const res = await runTaskApiCall("task.v2.taskSubtask.create", () =>
209
+ client.task.v2.taskSubtask.create({
210
+ path: { task_guid: params.task_guid },
211
+ data: omitUndefined({
212
+ summary: params.summary,
213
+ description: params.description,
214
+ due: params.due,
215
+ start: params.start,
216
+ extra: params.extra,
217
+ completed_at: params.completed_at,
218
+ members: params.members,
219
+ repeat_rule: params.repeat_rule,
220
+ tasklists: params.tasklists,
221
+ mode: params.mode,
222
+ is_milestone: params.is_milestone,
223
+ }),
224
+ params: omitUndefined({
225
+ user_id_type: params.user_id_type,
226
+ }),
227
+ }),
228
+ );
229
+
230
+ return {
231
+ subtask: formatTask((res.data?.subtask ?? undefined) as Record<string, unknown> | undefined),
232
+ };
233
+ }
234
+
235
+ export async function createTasklist(client: TaskClient, params: CreateTasklistParams) {
236
+ const res = await runTaskApiCall("task.v2.tasklist.create", () =>
237
+ client.task.v2.tasklist.create({
238
+ data: omitUndefined({
239
+ name: params.name,
240
+ members: params.members,
241
+ archive_tasklist: params.archive_tasklist,
242
+ }),
243
+ params: omitUndefined({
244
+ user_id_type: params.user_id_type,
245
+ }),
246
+ }),
247
+ );
248
+
249
+ return {
250
+ tasklist: formatTasklist(
251
+ (res.data?.tasklist ?? undefined) as Record<string, unknown> | undefined,
252
+ ),
253
+ };
254
+ }
255
+
256
+ export async function deleteTaskAttachment(client: TaskClient, params: DeleteTaskAttachmentParams) {
257
+ await runTaskApiCall("task.v2.attachment.delete", () =>
258
+ client.task.v2.attachment.delete({
259
+ path: { attachment_guid: params.attachment_guid },
260
+ }),
261
+ );
262
+
263
+ return {
264
+ success: true,
265
+ attachment_guid: params.attachment_guid,
266
+ };
267
+ }
268
+
81
269
  export async function deleteTask(client: TaskClient, taskGuid: string) {
82
270
  await runTaskApiCall("task.v2.task.delete", () =>
83
271
  client.task.v2.task.delete({
@@ -91,6 +279,19 @@ export async function deleteTask(client: TaskClient, taskGuid: string) {
91
279
  };
92
280
  }
93
281
 
282
+ export async function deleteTasklist(client: TaskClient, tasklistGuid: string) {
283
+ await runTaskApiCall("task.v2.tasklist.delete", () =>
284
+ client.task.v2.tasklist.delete({
285
+ path: { tasklist_guid: tasklistGuid },
286
+ }),
287
+ );
288
+
289
+ return {
290
+ success: true,
291
+ tasklist_guid: tasklistGuid,
292
+ };
293
+ }
294
+
94
295
  export async function getTask(client: TaskClient, params: GetTaskParams) {
95
296
  const res = await runTaskApiCall("task.v2.task.get", () =>
96
297
  client.task.v2.task.get({
@@ -106,10 +307,91 @@ export async function getTask(client: TaskClient, params: GetTaskParams) {
106
307
  };
107
308
  }
108
309
 
310
+ export async function getTasklist(client: TaskClient, params: GetTasklistParams) {
311
+ const res = await runTaskApiCall("task.v2.tasklist.get", () =>
312
+ client.task.v2.tasklist.get({
313
+ path: { tasklist_guid: params.tasklist_guid },
314
+ params: omitUndefined({
315
+ user_id_type: params.user_id_type,
316
+ }),
317
+ }),
318
+ );
319
+
320
+ return {
321
+ tasklist: formatTasklist(
322
+ (res.data?.tasklist ?? undefined) as Record<string, unknown> | undefined,
323
+ ),
324
+ };
325
+ }
326
+
327
+ export async function listTasklists(client: TaskClient, params: ListTasklistsParams) {
328
+ const res = await runTaskApiCall("task.v2.tasklist.list", () =>
329
+ client.task.v2.tasklist.list({
330
+ params: omitUndefined({
331
+ page_size: params.page_size,
332
+ page_token: params.page_token,
333
+ user_id_type: params.user_id_type,
334
+ }),
335
+ }),
336
+ );
337
+
338
+ const items = (res.data?.items ?? []) as Record<string, unknown>[];
339
+
340
+ return {
341
+ items: items.map((item) => formatTasklist(item)),
342
+ page_token: res.data?.page_token,
343
+ has_more: res.data?.has_more,
344
+ };
345
+ }
346
+
347
+ export async function getTaskAttachment(client: TaskClient, params: GetTaskAttachmentParams) {
348
+ const res = await runTaskApiCall("task.v2.attachment.get", () =>
349
+ client.task.v2.attachment.get({
350
+ path: { attachment_guid: params.attachment_guid },
351
+ params: omitUndefined({
352
+ user_id_type: params.user_id_type,
353
+ }),
354
+ }),
355
+ );
356
+
357
+ return {
358
+ attachment: formatAttachment(
359
+ (res.data?.attachment ?? undefined) as Record<string, unknown> | undefined,
360
+ ),
361
+ };
362
+ }
363
+
364
+ export async function listTaskAttachments(client: TaskClient, params: ListTaskAttachmentsParams) {
365
+ const res = await runTaskApiCall("task.v2.attachment.list", () =>
366
+ client.task.v2.attachment.list({
367
+ params: omitUndefined({
368
+ resource_type: "task",
369
+ resource_id: params.task_guid,
370
+ page_size: params.page_size,
371
+ page_token: params.page_token,
372
+ updated_mesc: params.updated_mesc,
373
+ user_id_type: params.user_id_type,
374
+ }),
375
+ }),
376
+ );
377
+
378
+ const items = (res.data?.items ?? []) as Record<string, unknown>[];
379
+
380
+ return {
381
+ items: items.map((item) => formatAttachment(item)),
382
+ page_token: res.data?.page_token,
383
+ has_more: res.data?.has_more,
384
+ };
385
+ }
386
+
109
387
  export async function updateTask(client: TaskClient, params: UpdateTaskParams) {
110
388
  const task = omitUndefined(params.task as Record<string, unknown>) as TaskUpdateTask;
111
389
  const updateFields = params.update_fields?.length ? params.update_fields : inferUpdateFields(task);
112
390
 
391
+ if (params.update_fields?.length) {
392
+ ensureSupportedUpdateFields(updateFields, SUPPORTED_PATCH_FIELDS, "task");
393
+ }
394
+
113
395
  if (Object.keys(task).length === 0) {
114
396
  throw new Error("task update payload is empty");
115
397
  }
@@ -135,3 +417,174 @@ export async function updateTask(client: TaskClient, params: UpdateTaskParams) {
135
417
  update_fields: updateFields,
136
418
  };
137
419
  }
420
+
421
+ export async function addTaskToTasklist(client: TaskClient, params: AddTaskToTasklistParams) {
422
+ const res = await runTaskApiCall("task.v2.task.add_tasklist", () =>
423
+ client.task.v2.task.addTasklist({
424
+ path: { task_guid: params.task_guid },
425
+ data: omitUndefined({
426
+ tasklist_guid: params.tasklist_guid,
427
+ section_guid: params.section_guid,
428
+ }),
429
+ params: omitUndefined({
430
+ user_id_type: params.user_id_type,
431
+ }),
432
+ }),
433
+ );
434
+
435
+ return {
436
+ task: formatTask((res.data?.task ?? undefined) as Record<string, unknown> | undefined),
437
+ };
438
+ }
439
+
440
+ export async function removeTaskFromTasklist(
441
+ client: TaskClient,
442
+ params: RemoveTaskFromTasklistParams,
443
+ ) {
444
+ const res = await runTaskApiCall("task.v2.task.remove_tasklist", () =>
445
+ client.task.v2.task.removeTasklist({
446
+ path: { task_guid: params.task_guid },
447
+ data: omitUndefined({
448
+ tasklist_guid: params.tasklist_guid,
449
+ }),
450
+ params: omitUndefined({
451
+ user_id_type: params.user_id_type,
452
+ }),
453
+ }),
454
+ );
455
+
456
+ return {
457
+ task: formatTask((res.data?.task ?? undefined) as Record<string, unknown> | undefined),
458
+ };
459
+ }
460
+
461
+ export async function updateTasklist(client: TaskClient, params: UpdateTasklistParams) {
462
+ const tasklist = omitUndefined(params.tasklist as Record<string, unknown>) as TasklistPatchTasklist;
463
+ const updateFields = params.update_fields?.length
464
+ ? params.update_fields
465
+ : inferTasklistUpdateFields(tasklist);
466
+
467
+ if (params.update_fields?.length) {
468
+ ensureSupportedUpdateFields(updateFields, SUPPORTED_TASKLIST_PATCH_FIELDS, "tasklist");
469
+ }
470
+
471
+ if (Object.keys(tasklist).length === 0) {
472
+ throw new Error("tasklist update payload is empty");
473
+ }
474
+ if (updateFields.length === 0) {
475
+ throw new Error("no valid update_fields provided or inferred from tasklist payload");
476
+ }
477
+
478
+ const res = await runTaskApiCall("task.v2.tasklist.patch", () =>
479
+ client.task.v2.tasklist.patch({
480
+ path: { tasklist_guid: params.tasklist_guid },
481
+ data: omitUndefined({
482
+ tasklist,
483
+ update_fields: updateFields,
484
+ origin_owner_to_role: params.origin_owner_to_role,
485
+ }),
486
+ params: omitUndefined({
487
+ user_id_type: params.user_id_type,
488
+ }),
489
+ }),
490
+ );
491
+
492
+ return {
493
+ tasklist: formatTasklist(
494
+ (res.data?.tasklist ?? undefined) as Record<string, unknown> | undefined,
495
+ ),
496
+ update_fields: updateFields,
497
+ };
498
+ }
499
+
500
+ export async function addTasklistMembers(client: TaskClient, params: AddTasklistMembersParams) {
501
+ const res = await runTaskApiCall("task.v2.tasklist.addMembers", () =>
502
+ client.task.v2.tasklist.addMembers({
503
+ path: { tasklist_guid: params.tasklist_guid },
504
+ data: {
505
+ members: params.members,
506
+ },
507
+ params: omitUndefined({
508
+ user_id_type: params.user_id_type,
509
+ }),
510
+ }),
511
+ );
512
+
513
+ return {
514
+ tasklist: formatTasklist(
515
+ (res.data?.tasklist ?? undefined) as Record<string, unknown> | undefined,
516
+ ),
517
+ };
518
+ }
519
+
520
+ export async function removeTasklistMembers(
521
+ client: TaskClient,
522
+ params: RemoveTasklistMembersParams,
523
+ ) {
524
+ const res = await runTaskApiCall("task.v2.tasklist.removeMembers", () =>
525
+ client.task.v2.tasklist.removeMembers({
526
+ path: { tasklist_guid: params.tasklist_guid },
527
+ data: {
528
+ members: params.members,
529
+ },
530
+ params: omitUndefined({
531
+ user_id_type: params.user_id_type,
532
+ }),
533
+ }),
534
+ );
535
+
536
+ return {
537
+ tasklist: formatTasklist(
538
+ (res.data?.tasklist ?? undefined) as Record<string, unknown> | undefined,
539
+ ),
540
+ };
541
+ }
542
+
543
+ export async function uploadTaskAttachment(
544
+ client: TaskClient,
545
+ params: UploadTaskAttachmentParams,
546
+ options?: { maxBytes?: number },
547
+ ) {
548
+ const maxBytes =
549
+ typeof options?.maxBytes === "number" && options.maxBytes > 0
550
+ ? options.maxBytes
551
+ : DEFAULT_TASK_ATTACHMENT_MAX_BYTES;
552
+
553
+ let tempCleanup: (() => Promise<void>) | undefined;
554
+ let filePath: string;
555
+
556
+ if ("file_path" in params) {
557
+ filePath = params.file_path;
558
+ await ensureUploadableLocalFile(filePath, maxBytes);
559
+ } else {
560
+ const download = await downloadToTempFile(params.file_url, params.filename, maxBytes);
561
+ filePath = download.tempPath;
562
+ tempCleanup = download.cleanup;
563
+ }
564
+
565
+ try {
566
+ const res = await runTaskApiCall("task.v2.attachment.upload", async () => {
567
+ const data = await client.task.v2.attachment.upload({
568
+ data: {
569
+ resource_type: "task",
570
+ resource_id: params.task_guid,
571
+ file: fs.createReadStream(filePath),
572
+ },
573
+ params: omitUndefined({
574
+ user_id_type: params.user_id_type,
575
+ }),
576
+ });
577
+ return { code: 0, data } as { code: number; data: typeof data };
578
+ });
579
+
580
+ const items = (res.data?.items ?? []) as Record<string, unknown>[];
581
+
582
+ return {
583
+ items: items.map((item) => formatAttachment(item)),
584
+ };
585
+ } finally {
586
+ if (tempCleanup) {
587
+ await tempCleanup();
588
+ }
589
+ }
590
+ }
@@ -0,0 +1,13 @@
1
+ export const BYTES_PER_MEGABYTE = 1024 * 1024;
2
+
3
+ export const DEFAULT_TASK_MEDIA_MAX_MB = 30;
4
+
5
+ export const DEFAULT_TASK_ATTACHMENT_MAX_BYTES = DEFAULT_TASK_MEDIA_MAX_MB * BYTES_PER_MEGABYTE;
6
+
7
+ export const DEFAULT_TASK_ATTACHMENT_FILENAME = "attachment";
8
+
9
+ export const HEX_RADIX = 16;
10
+
11
+ export const RANDOM_TOKEN_PREFIX_LENGTH = 2;
12
+
13
+ export const SIZE_DISPLAY_FRACTION_DIGITS = 0;
@@ -0,0 +1 @@
1
+ export { registerFeishuTaskTools } from "./register.js";