@m1heng-clawd/feishu 0.1.11 → 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 (46) hide show
  1. package/README.md +123 -4
  2. package/index.ts +6 -6
  3. package/package.json +13 -3
  4. package/skills/feishu-doc/SKILL.md +40 -2
  5. package/skills/feishu-task/SKILL.md +210 -0
  6. package/src/bitable-tools/index.ts +1 -0
  7. package/src/bot.ts +181 -70
  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} +31 -1
  15. package/src/drive-tools/actions.ts +182 -0
  16. package/src/drive-tools/common.ts +18 -0
  17. package/src/drive-tools/index.ts +2 -0
  18. package/src/drive-tools/register.ts +71 -0
  19. package/src/{drive-schema.ts → drive-tools/schemas.ts} +2 -1
  20. package/src/media.ts +5 -3
  21. package/src/perm-tools/actions.ts +111 -0
  22. package/src/perm-tools/common.ts +18 -0
  23. package/src/perm-tools/index.ts +2 -0
  24. package/src/perm-tools/register.ts +65 -0
  25. package/src/policy.ts +13 -0
  26. package/src/reply-dispatcher.ts +6 -4
  27. package/src/send.ts +33 -2
  28. package/src/task-tools/actions.ts +466 -13
  29. package/src/task-tools/constants.ts +13 -0
  30. package/src/task-tools/index.ts +1 -0
  31. package/src/task-tools/register.ts +175 -13
  32. package/src/task-tools/schemas.ts +433 -4
  33. package/src/text/markdown-links.ts +104 -0
  34. package/src/types.ts +1 -0
  35. package/src/wiki-tools/actions.ts +166 -0
  36. package/src/wiki-tools/common.ts +18 -0
  37. package/src/wiki-tools/index.ts +2 -0
  38. package/src/wiki-tools/register.ts +66 -0
  39. package/src/bitable.ts +0 -1
  40. package/src/docx.ts +0 -276
  41. package/src/drive.ts +0 -237
  42. package/src/perm.ts +0 -165
  43. package/src/task.ts +0 -1
  44. package/src/wiki.ts +0 -223
  45. /package/src/{perm-schema.ts → perm-tools/schemas.ts} +0 -0
  46. /package/src/{wiki-schema.ts → wiki-tools/schemas.ts} +0 -0
@@ -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";