@m1heng-clawd/feishu 0.1.15 → 0.1.17
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/README.md +28 -0
- package/index.ts +4 -0
- package/package.json +2 -1
- package/skills/feishu-message/SKILL.md +111 -0
- package/skills/feishu-reaction/SKILL.md +120 -0
- package/src/bot.ts +4 -0
- package/src/channel.ts +2 -0
- package/src/chat-tools/actions.ts +102 -33
- package/src/chat-tools/schemas.ts +57 -62
- package/src/config-schema.ts +2 -0
- 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/register.ts +1 -1
- package/src/drive-tools/schemas.ts +45 -58
- package/src/media-duration.ts +43 -8
- package/src/media.ts +31 -53
- package/src/message-tools/actions.ts +154 -0
- package/src/message-tools/index.ts +2 -0
- package/src/message-tools/register.ts +58 -0
- package/src/message-tools/schemas.ts +56 -0
- package/src/outbound.ts +2 -2
- package/src/perm-tools/actions.ts +27 -3
- package/src/perm-tools/schemas.ts +39 -45
- package/src/reaction-tools/actions.ts +127 -0
- package/src/reaction-tools/index.ts +2 -0
- package/src/reaction-tools/register.ts +51 -0
- package/src/reaction-tools/schemas.ts +34 -0
- package/src/reply-dispatcher.ts +23 -33
- package/src/streaming-card.ts +34 -11
- 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 +2 -0
- package/src/tools-config.ts +2 -0
- package/src/types.ts +4 -0
- package/src/wiki-tools/actions.ts +24 -6
- package/src/wiki-tools/schemas.ts +32 -51
|
@@ -1,66 +1,61 @@
|
|
|
1
1
|
import { Type, type Static } from "@sinclair/typebox";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
})
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}),
|
|
31
|
-
Type.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
user_ids: Type.Array(Type.String(), { description: "List of user IDs to invite" }),
|
|
61
|
-
greeting: Type.Optional(Type.String({ description: "Greeting message to send (default: Hello! I've created this group chat for us to collaborate.)" })),
|
|
62
|
-
description: Type.Optional(Type.String({ description: "Group description" })),
|
|
63
|
-
}),
|
|
64
|
-
]);
|
|
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
|
+
}
|
|
9
|
+
|
|
10
|
+
const CHAT_ACTION_VALUES = [
|
|
11
|
+
"get_announcement_info",
|
|
12
|
+
"get_announcement",
|
|
13
|
+
"write_announcement",
|
|
14
|
+
"append_announcement",
|
|
15
|
+
"list_announcement_blocks",
|
|
16
|
+
"get_announcement_block",
|
|
17
|
+
"update_announcement_block",
|
|
18
|
+
"create_chat",
|
|
19
|
+
"add_members",
|
|
20
|
+
"check_bot_in_chat",
|
|
21
|
+
"delete_chat",
|
|
22
|
+
"create_session_chat",
|
|
23
|
+
] as const;
|
|
24
|
+
|
|
25
|
+
const USER_ID_TYPE_VALUES = ["open_id", "user_id", "union_id"] as const;
|
|
26
|
+
const MEMBER_ID_TYPE_VALUES = ["open_id", "user_id", "union_id", "app_id"] as const;
|
|
27
|
+
|
|
28
|
+
export const FeishuChatSchema = Type.Object({
|
|
29
|
+
action: stringEnum(CHAT_ACTION_VALUES, { description: "Chat action" }),
|
|
30
|
+
chat_id: Type.Optional(Type.String({ description: "Chat ID" })),
|
|
31
|
+
content: Type.Optional(Type.String({ description: "Announcement content / block content" })),
|
|
32
|
+
block_id: Type.Optional(Type.String({ description: "Announcement block ID" })),
|
|
33
|
+
|
|
34
|
+
name: Type.Optional(Type.String({ description: "Group chat name" })),
|
|
35
|
+
user_ids: Type.Optional(
|
|
36
|
+
Type.Array(Type.String(), {
|
|
37
|
+
description: "User/member IDs used by create_chat/add_members/create_session_chat",
|
|
38
|
+
}),
|
|
39
|
+
),
|
|
40
|
+
user_id_type: Type.Optional(
|
|
41
|
+
stringEnum(USER_ID_TYPE_VALUES, {
|
|
42
|
+
description: "ID type for user_ids in create_chat/create_session_chat",
|
|
43
|
+
default: "open_id",
|
|
44
|
+
}),
|
|
45
|
+
),
|
|
46
|
+
member_id_type: Type.Optional(
|
|
47
|
+
stringEnum(MEMBER_ID_TYPE_VALUES, {
|
|
48
|
+
description: "ID type for add_members (supports app_id for bots)",
|
|
49
|
+
default: "open_id",
|
|
50
|
+
}),
|
|
51
|
+
),
|
|
52
|
+
greeting: Type.Optional(
|
|
53
|
+
Type.String({
|
|
54
|
+
description:
|
|
55
|
+
"Greeting message for create_session_chat (default: Hello! I've created this group chat for us to collaborate.)",
|
|
56
|
+
}),
|
|
57
|
+
),
|
|
58
|
+
description: Type.Optional(Type.String({ description: "Group chat description" })),
|
|
59
|
+
});
|
|
65
60
|
|
|
66
61
|
export type FeishuChatParams = Static<typeof FeishuChatSchema>;
|
package/src/config-schema.ts
CHANGED
|
@@ -91,6 +91,8 @@ const FeishuToolsConfigSchema = z
|
|
|
91
91
|
task: z.boolean().optional(), // Task operations (default: true)
|
|
92
92
|
chat: z.boolean().optional(), // Chat management operations (default: true)
|
|
93
93
|
urgent: z.boolean().optional(), // Buzz/urgent notifications (default: true)
|
|
94
|
+
message: z.boolean().optional(), // Message reading (default: true)
|
|
95
|
+
reaction: z.boolean().optional(), // Emoji reactions (default: true)
|
|
94
96
|
})
|
|
95
97
|
.strict()
|
|
96
98
|
.optional();
|
package/src/doc-tools/actions.ts
CHANGED
|
@@ -44,6 +44,13 @@ function normalizePageSize(pageSize?: number) {
|
|
|
44
44
|
return pageSize;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
function requireString(value: unknown, field: string): string {
|
|
48
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
49
|
+
throw new Error(`${field} is required`);
|
|
50
|
+
}
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
|
|
47
54
|
function omitUndefined<T extends Record<string, unknown>>(obj: T): T {
|
|
48
55
|
return Object.fromEntries(Object.entries(obj).filter(([, value]) => value !== undefined)) as T;
|
|
49
56
|
}
|
|
@@ -304,37 +311,79 @@ export async function runDocAction(
|
|
|
304
311
|
) {
|
|
305
312
|
switch (params.action) {
|
|
306
313
|
case "read":
|
|
307
|
-
return readDoc(client, params.doc_token);
|
|
314
|
+
return readDoc(client, requireString(params.doc_token, "doc_token"));
|
|
308
315
|
case "write":
|
|
309
|
-
return writeDoc(
|
|
316
|
+
return writeDoc(
|
|
317
|
+
client,
|
|
318
|
+
requireString(params.doc_token, "doc_token"),
|
|
319
|
+
requireString(params.content, "content"),
|
|
320
|
+
mediaMaxBytes,
|
|
321
|
+
);
|
|
310
322
|
case "append":
|
|
311
|
-
return appendDoc(
|
|
323
|
+
return appendDoc(
|
|
324
|
+
client,
|
|
325
|
+
requireString(params.doc_token, "doc_token"),
|
|
326
|
+
requireString(params.content, "content"),
|
|
327
|
+
mediaMaxBytes,
|
|
328
|
+
);
|
|
312
329
|
case "create":
|
|
313
|
-
return createDoc(client, params.title, params.folder_token);
|
|
330
|
+
return createDoc(client, requireString(params.title, "title"), params.folder_token);
|
|
314
331
|
case "create_and_write":
|
|
315
332
|
return createAndWriteDoc(
|
|
316
333
|
client,
|
|
317
|
-
params.title,
|
|
318
|
-
params.content,
|
|
334
|
+
requireString(params.title, "title"),
|
|
335
|
+
requireString(params.content, "content"),
|
|
319
336
|
mediaMaxBytes,
|
|
320
337
|
params.folder_token,
|
|
321
338
|
);
|
|
322
339
|
case "list_blocks":
|
|
323
|
-
return listBlocks(client, params.doc_token);
|
|
340
|
+
return listBlocks(client, requireString(params.doc_token, "doc_token"));
|
|
324
341
|
case "get_block":
|
|
325
|
-
return getBlock(
|
|
342
|
+
return getBlock(
|
|
343
|
+
client,
|
|
344
|
+
requireString(params.doc_token, "doc_token"),
|
|
345
|
+
requireString(params.block_id, "block_id"),
|
|
346
|
+
);
|
|
326
347
|
case "update_block":
|
|
327
|
-
return updateBlock(
|
|
348
|
+
return updateBlock(
|
|
349
|
+
client,
|
|
350
|
+
requireString(params.doc_token, "doc_token"),
|
|
351
|
+
requireString(params.block_id, "block_id"),
|
|
352
|
+
requireString(params.content, "content"),
|
|
353
|
+
);
|
|
328
354
|
case "delete_block":
|
|
329
|
-
return deleteBlock(
|
|
355
|
+
return deleteBlock(
|
|
356
|
+
client,
|
|
357
|
+
requireString(params.doc_token, "doc_token"),
|
|
358
|
+
requireString(params.block_id, "block_id"),
|
|
359
|
+
);
|
|
330
360
|
case "list_comments":
|
|
331
|
-
return listComments(
|
|
361
|
+
return listComments(
|
|
362
|
+
client,
|
|
363
|
+
requireString(params.doc_token, "doc_token"),
|
|
364
|
+
params.page_token,
|
|
365
|
+
params.page_size,
|
|
366
|
+
);
|
|
332
367
|
case "create_comment":
|
|
333
|
-
return createComment(
|
|
368
|
+
return createComment(
|
|
369
|
+
client,
|
|
370
|
+
requireString(params.doc_token, "doc_token"),
|
|
371
|
+
requireString(params.content, "content"),
|
|
372
|
+
);
|
|
334
373
|
case "get_comment":
|
|
335
|
-
return getComment(
|
|
374
|
+
return getComment(
|
|
375
|
+
client,
|
|
376
|
+
requireString(params.doc_token, "doc_token"),
|
|
377
|
+
requireString(params.comment_id, "comment_id"),
|
|
378
|
+
);
|
|
336
379
|
case "list_comment_replies":
|
|
337
|
-
return listCommentReplies(
|
|
380
|
+
return listCommentReplies(
|
|
381
|
+
client,
|
|
382
|
+
requireString(params.doc_token, "doc_token"),
|
|
383
|
+
requireString(params.comment_id, "comment_id"),
|
|
384
|
+
params.page_token,
|
|
385
|
+
params.page_size,
|
|
386
|
+
);
|
|
338
387
|
default:
|
|
339
388
|
return { error: `Unknown action: ${(params as any).action}` };
|
|
340
389
|
}
|
package/src/doc-tools/schemas.ts
CHANGED
|
@@ -1,85 +1,49 @@
|
|
|
1
1
|
import { Type, type Static } from "@sinclair/typebox";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
+
}
|
|
9
|
+
|
|
10
|
+
const DOC_ACTION_VALUES = [
|
|
11
|
+
"read",
|
|
12
|
+
"write",
|
|
13
|
+
"append",
|
|
14
|
+
"create",
|
|
15
|
+
"create_and_write",
|
|
16
|
+
"list_blocks",
|
|
17
|
+
"get_block",
|
|
18
|
+
"update_block",
|
|
19
|
+
"delete_block",
|
|
20
|
+
"list_comments",
|
|
21
|
+
"create_comment",
|
|
22
|
+
"get_comment",
|
|
23
|
+
"list_comment_replies",
|
|
24
|
+
] as const;
|
|
25
|
+
|
|
26
|
+
export const FeishuDocSchema = Type.Object({
|
|
27
|
+
action: stringEnum(DOC_ACTION_VALUES, { description: "Document action" }),
|
|
28
|
+
doc_token: Type.Optional(
|
|
29
|
+
Type.String({
|
|
7
30
|
description:
|
|
8
31
|
"Document token (extract from URL /docx/XXX or /docs/XXX). Supports both new (docx) and legacy (doc) formats.",
|
|
9
32
|
}),
|
|
10
|
-
|
|
11
|
-
Type.
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
content: Type.String({
|
|
15
|
-
description: "Markdown content to write (replaces entire document content)",
|
|
16
|
-
}),
|
|
17
|
-
}),
|
|
18
|
-
Type.Object({
|
|
19
|
-
action: Type.Literal("append"),
|
|
20
|
-
doc_token: Type.String({ description: "Document token" }),
|
|
21
|
-
content: Type.String({ description: "Markdown content to append to end of document" }),
|
|
22
|
-
}),
|
|
23
|
-
Type.Object({
|
|
24
|
-
action: Type.Literal("create"),
|
|
25
|
-
title: Type.String({ description: "Document title" }),
|
|
26
|
-
folder_token: Type.Optional(Type.String({ description: "Target folder token (optional)" })),
|
|
27
|
-
}),
|
|
28
|
-
Type.Object({
|
|
29
|
-
action: Type.Literal("create_and_write"),
|
|
30
|
-
title: Type.String({ description: "Document title" }),
|
|
31
|
-
content: Type.String({
|
|
32
|
-
description: "Markdown content to write immediately after document creation",
|
|
33
|
+
),
|
|
34
|
+
content: Type.Optional(
|
|
35
|
+
Type.String({
|
|
36
|
+
description: "Markdown content for write/append/comment/update operations",
|
|
33
37
|
}),
|
|
34
|
-
|
|
35
|
-
}),
|
|
36
|
-
Type.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}),
|
|
40
|
-
Type.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}),
|
|
45
|
-
Type.Object({
|
|
46
|
-
action: Type.Literal("update_block"),
|
|
47
|
-
doc_token: Type.String({ description: "Document token" }),
|
|
48
|
-
block_id: Type.String({ description: "Block ID (from list_blocks)" }),
|
|
49
|
-
content: Type.String({ description: "New text content" }),
|
|
50
|
-
}),
|
|
51
|
-
Type.Object({
|
|
52
|
-
action: Type.Literal("delete_block"),
|
|
53
|
-
doc_token: Type.String({ description: "Document token" }),
|
|
54
|
-
block_id: Type.String({ description: "Block ID" }),
|
|
55
|
-
}),
|
|
56
|
-
Type.Object({
|
|
57
|
-
action: Type.Literal("list_comments"),
|
|
58
|
-
doc_token: Type.String({ description: "Document token" }),
|
|
59
|
-
page_token: Type.Optional(Type.String({ description: "Page token for pagination" })),
|
|
60
|
-
page_size: Type.Optional(
|
|
61
|
-
Type.Integer({ minimum: 1, description: "Page size, default 50 (positive integer)" }),
|
|
62
|
-
),
|
|
63
|
-
}),
|
|
64
|
-
Type.Object({
|
|
65
|
-
action: Type.Literal("create_comment"),
|
|
66
|
-
doc_token: Type.String({ description: "Document token" }),
|
|
67
|
-
content: Type.String({ description: "Comment content" }),
|
|
68
|
-
}),
|
|
69
|
-
Type.Object({
|
|
70
|
-
action: Type.Literal("get_comment"),
|
|
71
|
-
doc_token: Type.String({ description: "Document token" }),
|
|
72
|
-
comment_id: Type.String({ description: "Comment ID" }),
|
|
73
|
-
}),
|
|
74
|
-
Type.Object({
|
|
75
|
-
action: Type.Literal("list_comment_replies"),
|
|
76
|
-
doc_token: Type.String({ description: "Document token" }),
|
|
77
|
-
comment_id: Type.String({ description: "Comment ID" }),
|
|
78
|
-
page_token: Type.Optional(Type.String({ description: "Page token for pagination" })),
|
|
79
|
-
page_size: Type.Optional(
|
|
80
|
-
Type.Integer({ minimum: 1, description: "Page size, default 50 (positive integer)" }),
|
|
81
|
-
),
|
|
82
|
-
}),
|
|
83
|
-
]);
|
|
38
|
+
),
|
|
39
|
+
title: Type.Optional(Type.String({ description: "Document title (for create/create_and_write)" })),
|
|
40
|
+
folder_token: Type.Optional(Type.String({ description: "Target folder token (optional)" })),
|
|
41
|
+
block_id: Type.Optional(Type.String({ description: "Block ID (from list_blocks)" })),
|
|
42
|
+
comment_id: Type.Optional(Type.String({ description: "Comment ID" })),
|
|
43
|
+
page_token: Type.Optional(Type.String({ description: "Page token for pagination" })),
|
|
44
|
+
page_size: Type.Optional(
|
|
45
|
+
Type.Integer({ minimum: 1, description: "Page size, default 50 (positive integer)" }),
|
|
46
|
+
),
|
|
47
|
+
});
|
|
84
48
|
|
|
85
49
|
export type FeishuDocParams = Static<typeof FeishuDocSchema>;
|
package/src/doc-write-service.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type * as Lark from "@larksuiteoapi/node-sdk";
|
|
2
2
|
import { Readable } from "stream";
|
|
3
3
|
import { getFeishuRuntime } from "./runtime.js";
|
|
4
|
+
import { getCurrentFeishuToolContext } from "./tools-common/tool-context.js";
|
|
4
5
|
|
|
5
6
|
const BLOCK_TYPE_NAMES: Record<number, string> = {
|
|
6
7
|
1: "Page",
|
|
@@ -572,6 +573,24 @@ export async function createDoc(
|
|
|
572
573
|
});
|
|
573
574
|
if (res.code !== 0) throw new Error(res.msg);
|
|
574
575
|
const doc = res.data?.document;
|
|
576
|
+
|
|
577
|
+
// Auto-share with the requesting user so they can edit the document.
|
|
578
|
+
const senderOpenId = getCurrentFeishuToolContext()?.senderOpenId;
|
|
579
|
+
if (doc?.document_id && senderOpenId) {
|
|
580
|
+
try {
|
|
581
|
+
const permRes = await client.drive.permissionMember.create({
|
|
582
|
+
path: { token: doc.document_id },
|
|
583
|
+
params: { type: "docx", need_notification: false },
|
|
584
|
+
data: { member_type: "openid", member_id: senderOpenId, perm: "edit" },
|
|
585
|
+
});
|
|
586
|
+
if (permRes.code !== 0) {
|
|
587
|
+
console.warn(`[feishu_doc] Failed to auto-share doc ${doc.document_id}: ${permRes.msg}`);
|
|
588
|
+
}
|
|
589
|
+
} catch (err) {
|
|
590
|
+
console.warn(`[feishu_doc] Failed to auto-share doc ${doc.document_id}:`, err);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
575
594
|
return {
|
|
576
595
|
document_id: doc?.document_id,
|
|
577
596
|
title: doc?.title,
|
|
@@ -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",
|
|
@@ -59,7 +59,7 @@ export function registerFeishuDriveTools(api: OpenClawPluginApi) {
|
|
|
59
59
|
name: "feishu_drive",
|
|
60
60
|
label: "Feishu Drive",
|
|
61
61
|
description:
|
|
62
|
-
"Feishu cloud
|
|
62
|
+
"Feishu cloud Drive operations. This tool works with Feishu Drive folders/files/documents, not the OpenClaw agent's local filesystem workspace. Actions: list, info, create_folder, move, delete, import_document. Use 'import_document' to create documents from Markdown with better structure preservation than block-by-block writing.",
|
|
63
63
|
parameters: FeishuDriveSchema,
|
|
64
64
|
run: async ({ client, account }, params) => {
|
|
65
65
|
const mediaMaxBytes = (account.config?.mediaMaxMb ?? 30) * 1024 * 1024;
|
|
@@ -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-duration.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { parseBuffer } from "music-metadata";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Media duration parsers for Feishu file upload.
|
|
3
5
|
*
|
|
@@ -9,11 +11,38 @@
|
|
|
9
11
|
* - MP4 (ISO base media) — MP4, MOV, QuickTime, M4A, M4V, 3GP share this format
|
|
10
12
|
* - WAV (RIFF PCM) — uncompressed audio
|
|
11
13
|
*
|
|
12
|
-
*
|
|
13
|
-
* - MP3 (VBR makes byte-count estimation unreliable without scanning all frames)
|
|
14
|
-
* - Raw AAC / ADTS
|
|
14
|
+
* Raw AAC / ADTS is delegated to music-metadata on the audio path.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
async function parseAudioDurationWithMusicMetadata(params: {
|
|
18
|
+
buffer: Buffer;
|
|
19
|
+
fileName?: string;
|
|
20
|
+
contentType?: string;
|
|
21
|
+
}): Promise<number | undefined> {
|
|
22
|
+
const { buffer, fileName, contentType } = params;
|
|
23
|
+
try {
|
|
24
|
+
const metadata = await parseBuffer(
|
|
25
|
+
buffer,
|
|
26
|
+
{
|
|
27
|
+
mimeType: contentType,
|
|
28
|
+
path: fileName,
|
|
29
|
+
size: buffer.length,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
duration: true,
|
|
33
|
+
skipCovers: true,
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
const seconds = metadata.format.duration;
|
|
37
|
+
if (typeof seconds !== "number" || !Number.isFinite(seconds) || seconds <= 0) {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
return Math.round(seconds * 1000);
|
|
41
|
+
} catch {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
17
46
|
/**
|
|
18
47
|
* Parse duration from an OGG container (Opus or Vorbis).
|
|
19
48
|
* Reads the granule position from the last OGG page and divides by sample rate.
|
|
@@ -174,17 +203,23 @@ export function parseWavDurationMs(buffer: Buffer): number | undefined {
|
|
|
174
203
|
*
|
|
175
204
|
* Routes to the appropriate parser based on Feishu's file type:
|
|
176
205
|
* - "mp4" → MP4/MOV/QuickTime container parser
|
|
177
|
-
* - "opus" → OGG parser, then MP4 container
|
|
206
|
+
* - "opus" → music-metadata first, then OGG parser, then MP4 container, then WAV
|
|
178
207
|
*
|
|
179
208
|
* Returns duration in milliseconds, or undefined if not determinable.
|
|
180
209
|
*/
|
|
181
|
-
export function parseFeishuMediaDurationMs(
|
|
210
|
+
export async function parseFeishuMediaDurationMs(
|
|
182
211
|
buffer: Buffer,
|
|
183
212
|
fileType: "opus" | "mp4",
|
|
184
|
-
|
|
213
|
+
options?: { fileName?: string; contentType?: string },
|
|
214
|
+
): Promise<number | undefined> {
|
|
185
215
|
if (fileType === "mp4") {
|
|
186
216
|
return parseMp4DurationMs(buffer);
|
|
187
217
|
}
|
|
188
|
-
|
|
189
|
-
|
|
218
|
+
return (
|
|
219
|
+
await parseAudioDurationWithMusicMetadata({
|
|
220
|
+
buffer,
|
|
221
|
+
fileName: options?.fileName,
|
|
222
|
+
contentType: options?.contentType,
|
|
223
|
+
})
|
|
224
|
+
) ?? parseOggDurationMs(buffer) ?? parseMp4DurationMs(buffer) ?? parseWavDurationMs(buffer);
|
|
190
225
|
}
|