@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
package/src/media.ts
CHANGED
|
@@ -192,6 +192,12 @@ export type SendMediaResult = {
|
|
|
192
192
|
chatId: string;
|
|
193
193
|
};
|
|
194
194
|
|
|
195
|
+
function assertNonEmptyMediaBuffer(buffer: Buffer, name: string): void {
|
|
196
|
+
if (buffer.length === 0) {
|
|
197
|
+
throw new Error(`Feishu media upload failed: "${name}" is empty (0 bytes)`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
195
201
|
/**
|
|
196
202
|
* Upload an image to Feishu and get an image_key for sending.
|
|
197
203
|
* Supports: JPEG, PNG, WEBP, GIF, TIFF, BMP, ICO
|
|
@@ -210,15 +216,16 @@ export async function uploadImageFeishu(params: {
|
|
|
210
216
|
|
|
211
217
|
const client = createFeishuClient(account);
|
|
212
218
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
219
|
+
if (Buffer.isBuffer(image)) {
|
|
220
|
+
assertNonEmptyMediaBuffer(image, "image");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const imagePayload = typeof image === "string" ? fs.createReadStream(image) : image;
|
|
217
224
|
|
|
218
225
|
const response = await client.im.image.create({
|
|
219
226
|
data: {
|
|
220
227
|
image_type: imageType,
|
|
221
|
-
image:
|
|
228
|
+
image: imagePayload as any,
|
|
222
229
|
},
|
|
223
230
|
});
|
|
224
231
|
|
|
@@ -237,23 +244,6 @@ export async function uploadImageFeishu(params: {
|
|
|
237
244
|
return { imageKey };
|
|
238
245
|
}
|
|
239
246
|
|
|
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
247
|
|
|
258
248
|
/**
|
|
259
249
|
* Upload a file to Feishu and get a file_key for sending.
|
|
@@ -275,18 +265,17 @@ export async function uploadFileFeishu(params: {
|
|
|
275
265
|
|
|
276
266
|
const client = createFeishuClient(account);
|
|
277
267
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
typeof file === "string" ? fs.createReadStream(file) : Readable.from(file);
|
|
268
|
+
if (Buffer.isBuffer(file)) {
|
|
269
|
+
assertNonEmptyMediaBuffer(file, fileName);
|
|
270
|
+
}
|
|
282
271
|
|
|
283
|
-
const
|
|
272
|
+
const filePayload = typeof file === "string" ? fs.createReadStream(file) : file;
|
|
284
273
|
|
|
285
274
|
const response = await client.im.file.create({
|
|
286
275
|
data: {
|
|
287
276
|
file_type: fileType,
|
|
288
|
-
file_name:
|
|
289
|
-
file:
|
|
277
|
+
file_name: fileName,
|
|
278
|
+
file: filePayload as any,
|
|
290
279
|
...(duration !== undefined && { duration }),
|
|
291
280
|
},
|
|
292
281
|
});
|
|
@@ -496,9 +485,10 @@ export async function sendMediaFeishu(params: {
|
|
|
496
485
|
mediaBuffer?: Buffer;
|
|
497
486
|
fileName?: string;
|
|
498
487
|
replyToMessageId?: string;
|
|
488
|
+
mediaLocalRoots?: readonly string[];
|
|
499
489
|
accountId?: string;
|
|
500
490
|
}): Promise<SendMediaResult> {
|
|
501
|
-
const { cfg, to, mediaUrl, mediaBuffer, fileName, replyToMessageId, accountId } = params;
|
|
491
|
+
const { cfg, to, mediaUrl, mediaBuffer, fileName, replyToMessageId, mediaLocalRoots, accountId } = params;
|
|
502
492
|
const account = resolveFeishuAccount({ cfg, accountId });
|
|
503
493
|
if (!account.configured) {
|
|
504
494
|
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
|
@@ -513,30 +503,16 @@ export async function sendMediaFeishu(params: {
|
|
|
513
503
|
buffer = mediaBuffer;
|
|
514
504
|
name = fileName ?? "file";
|
|
515
505
|
} else if (mediaUrl) {
|
|
516
|
-
const
|
|
506
|
+
const configLocalRoots = (account.config?.mediaLocalRoots ?? [])
|
|
517
507
|
.map((root) => root.trim())
|
|
518
508
|
.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
|
-
})();
|
|
509
|
+
// Merge context-provided roots (includes agent workspace) with config roots.
|
|
510
|
+
const mergedLocalRoots = [...(mediaLocalRoots ?? []), ...configLocalRoots];
|
|
511
|
+
const loaded = await getFeishuRuntime().media.loadWebMedia(mediaUrl, {
|
|
512
|
+
maxBytes: mediaMaxBytes,
|
|
513
|
+
optimizeImages: false,
|
|
514
|
+
...(mergedLocalRoots.length > 0 ? { localRoots: mergedLocalRoots } : {}),
|
|
515
|
+
});
|
|
540
516
|
buffer = loaded.buffer;
|
|
541
517
|
name = fileName ?? loaded.fileName ?? "file";
|
|
542
518
|
contentType = loaded.contentType;
|
|
@@ -544,6 +520,8 @@ export async function sendMediaFeishu(params: {
|
|
|
544
520
|
throw new Error("Either mediaUrl or mediaBuffer must be provided");
|
|
545
521
|
}
|
|
546
522
|
|
|
523
|
+
assertNonEmptyMediaBuffer(buffer, name);
|
|
524
|
+
|
|
547
525
|
// Determine if it's an image based on extension
|
|
548
526
|
const ext = path.extname(name).toLowerCase();
|
|
549
527
|
const isImage = [".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".ico", ".tiff"].includes(ext);
|
|
@@ -562,7 +540,7 @@ export async function sendMediaFeishu(params: {
|
|
|
562
540
|
const msgType = fileType === "opus" ? "audio" : fileType === "mp4" ? "media" : "file";
|
|
563
541
|
const duration =
|
|
564
542
|
fileType === "opus" || fileType === "mp4"
|
|
565
|
-
? parseFeishuMediaDurationMs(buffer, fileType)
|
|
543
|
+
? await parseFeishuMediaDurationMs(buffer, fileType, { fileName: name, contentType })
|
|
566
544
|
: undefined;
|
|
567
545
|
const { fileKey } = await uploadFileFeishu({
|
|
568
546
|
cfg,
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import type * as Lark from "@larksuiteoapi/node-sdk";
|
|
2
|
+
import { runFeishuApiCall, type FeishuApiResponse } from "../tools-common/feishu-api.js";
|
|
3
|
+
import type { FeishuMessageParams } from "./schemas.js";
|
|
4
|
+
|
|
5
|
+
interface MessageGetResponse extends FeishuApiResponse {
|
|
6
|
+
data?: {
|
|
7
|
+
items?: Array<{
|
|
8
|
+
message_id?: string;
|
|
9
|
+
root_id?: string;
|
|
10
|
+
parent_id?: string;
|
|
11
|
+
msg_type?: string;
|
|
12
|
+
create_time?: string;
|
|
13
|
+
update_time?: string;
|
|
14
|
+
deleted?: boolean;
|
|
15
|
+
chat_id?: string;
|
|
16
|
+
sender?: { id: string; id_type: string; sender_type: string };
|
|
17
|
+
body?: { content: string };
|
|
18
|
+
mentions?: Array<{ key: string; id: string; id_type: string; name: string }>;
|
|
19
|
+
}>;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface MessageListResponse extends FeishuApiResponse {
|
|
24
|
+
data?: {
|
|
25
|
+
has_more?: boolean;
|
|
26
|
+
page_token?: string;
|
|
27
|
+
items?: Array<{
|
|
28
|
+
message_id?: string;
|
|
29
|
+
root_id?: string;
|
|
30
|
+
parent_id?: string;
|
|
31
|
+
msg_type?: string;
|
|
32
|
+
create_time?: string;
|
|
33
|
+
deleted?: boolean;
|
|
34
|
+
chat_id?: string;
|
|
35
|
+
sender?: { id: string; id_type: string; sender_type: string };
|
|
36
|
+
body?: { content: string };
|
|
37
|
+
}>;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function runMessageAction(
|
|
42
|
+
client: Lark.Client,
|
|
43
|
+
params: FeishuMessageParams,
|
|
44
|
+
): Promise<unknown> {
|
|
45
|
+
switch (params.action) {
|
|
46
|
+
case "get":
|
|
47
|
+
return getMessage(client, params);
|
|
48
|
+
case "list":
|
|
49
|
+
return listMessages(client, params);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function parseMessageContent(msgType: string | undefined, rawContent: string | undefined): string {
|
|
54
|
+
if (!rawContent) return "";
|
|
55
|
+
try {
|
|
56
|
+
const parsed = JSON.parse(rawContent);
|
|
57
|
+
if (msgType === "text") return (parsed.text as string) ?? rawContent;
|
|
58
|
+
if (msgType === "post") {
|
|
59
|
+
const title = (parsed.title as string) ?? "";
|
|
60
|
+
const lines: string[] = [];
|
|
61
|
+
for (const paragraph of (parsed.content as Array<Array<{ tag: string; text?: string }>>) ?? []) {
|
|
62
|
+
lines.push(paragraph.map((el) => el.text ?? "").join(""));
|
|
63
|
+
}
|
|
64
|
+
return title ? `${title}\n${lines.join("\n")}` : lines.join("\n");
|
|
65
|
+
}
|
|
66
|
+
if (msgType === "image") return "[image]";
|
|
67
|
+
if (msgType === "file") return `[file: ${(parsed.file_name as string) ?? ""}]`;
|
|
68
|
+
if (msgType === "audio") return "[audio]";
|
|
69
|
+
if (msgType === "sticker") return "[sticker]";
|
|
70
|
+
if (msgType === "share_chat") return "[share_chat]";
|
|
71
|
+
if (msgType === "share_user") return "[share_user]";
|
|
72
|
+
return `[${msgType ?? "unknown"}]`;
|
|
73
|
+
} catch {
|
|
74
|
+
return rawContent;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function getMessage(client: Lark.Client, params: FeishuMessageParams) {
|
|
79
|
+
if (!params.message_id) {
|
|
80
|
+
throw new Error("message_id is required for action=get");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const response = await runFeishuApiCall<MessageGetResponse>(
|
|
84
|
+
"Feishu get message",
|
|
85
|
+
() =>
|
|
86
|
+
client.im.message.get({
|
|
87
|
+
path: { message_id: params.message_id! },
|
|
88
|
+
}) as Promise<MessageGetResponse>,
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const item = response.data?.items?.[0];
|
|
92
|
+
if (!item) {
|
|
93
|
+
return { ok: true, action: "get", message_id: params.message_id, found: false };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
ok: true,
|
|
98
|
+
action: "get",
|
|
99
|
+
found: true,
|
|
100
|
+
message_id: item.message_id ?? params.message_id,
|
|
101
|
+
msg_type: item.msg_type ?? "",
|
|
102
|
+
content: parseMessageContent(item.msg_type, item.body?.content),
|
|
103
|
+
sender_id: item.sender?.id ?? "",
|
|
104
|
+
sender_type: item.sender?.sender_type ?? "",
|
|
105
|
+
chat_id: item.chat_id ?? "",
|
|
106
|
+
create_time: item.create_time ?? "",
|
|
107
|
+
update_time: item.update_time ?? "",
|
|
108
|
+
mentions: item.mentions?.map((m) => ({ id: m.id, name: m.name })) ?? [],
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function listMessages(client: Lark.Client, params: FeishuMessageParams) {
|
|
113
|
+
if (!params.chat_id) {
|
|
114
|
+
throw new Error("chat_id is required for action=list");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const pageSize = Math.min(Math.max(params.page_size ?? 10, 1), 50);
|
|
118
|
+
const sortType = params.sort_type ?? "ByCreateTimeDesc";
|
|
119
|
+
|
|
120
|
+
const response = await runFeishuApiCall<MessageListResponse>(
|
|
121
|
+
"Feishu list messages",
|
|
122
|
+
() =>
|
|
123
|
+
client.im.message.list({
|
|
124
|
+
params: {
|
|
125
|
+
container_id_type: "chat",
|
|
126
|
+
container_id: params.chat_id!,
|
|
127
|
+
sort_type: sortType,
|
|
128
|
+
page_size: pageSize,
|
|
129
|
+
...(params.start_time ? { start_time: params.start_time } : {}),
|
|
130
|
+
...(params.end_time ? { end_time: params.end_time } : {}),
|
|
131
|
+
},
|
|
132
|
+
}) as Promise<MessageListResponse>,
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const messages = (response.data?.items ?? [])
|
|
136
|
+
.filter((item) => !item.deleted)
|
|
137
|
+
.map((item) => ({
|
|
138
|
+
message_id: item.message_id ?? "",
|
|
139
|
+
msg_type: item.msg_type ?? "",
|
|
140
|
+
content_preview: parseMessageContent(item.msg_type, item.body?.content),
|
|
141
|
+
sender_id: item.sender?.id ?? "",
|
|
142
|
+
sender_type: item.sender?.sender_type ?? "",
|
|
143
|
+
create_time: item.create_time ?? "",
|
|
144
|
+
chat_id: item.chat_id ?? "",
|
|
145
|
+
}));
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
ok: true,
|
|
149
|
+
action: "list",
|
|
150
|
+
chat_id: params.chat_id,
|
|
151
|
+
total: messages.length,
|
|
152
|
+
messages,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
+
import { hasFeishuToolEnabledForAnyAccount, withFeishuToolClient } from "../tools-common/tool-exec.js";
|
|
3
|
+
import { getCurrentFeishuToolContext } from "../tools-common/tool-context.js";
|
|
4
|
+
import { errorResult, json } from "../tools-common/feishu-api.js";
|
|
5
|
+
import { runMessageAction } from "./actions.js";
|
|
6
|
+
import { FeishuMessageSchema, type FeishuMessageParams } from "./schemas.js";
|
|
7
|
+
|
|
8
|
+
export function registerFeishuMessageTools(api: OpenClawPluginApi) {
|
|
9
|
+
if (!api.config) {
|
|
10
|
+
api.logger.debug?.("feishu_message: No config available, skipping");
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (!hasFeishuToolEnabledForAnyAccount(api.config)) {
|
|
15
|
+
api.logger.debug?.("feishu_message: No Feishu accounts configured, skipping");
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!hasFeishuToolEnabledForAnyAccount(api.config, "message")) {
|
|
20
|
+
api.logger.debug?.("feishu_message: message tool disabled in config");
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
api.registerTool(
|
|
25
|
+
{
|
|
26
|
+
name: "feishu_message",
|
|
27
|
+
label: "Feishu Message",
|
|
28
|
+
description:
|
|
29
|
+
"Read Feishu messages. Actions: get (get a single message by message_id), list (list recent messages in a chat). " +
|
|
30
|
+
"Use list to discover message_ids for use with other tools (e.g., feishu_reaction).",
|
|
31
|
+
parameters: FeishuMessageSchema,
|
|
32
|
+
async execute(_toolCallId, params) {
|
|
33
|
+
const p = params as FeishuMessageParams;
|
|
34
|
+
if (p.action === "list") {
|
|
35
|
+
const isValidChatId = p.chat_id?.startsWith("oc_");
|
|
36
|
+
if (!isValidChatId) {
|
|
37
|
+
const ctx = getCurrentFeishuToolContext();
|
|
38
|
+
if (ctx?.chatId) p.chat_id = ctx.chatId;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
return await withFeishuToolClient({
|
|
44
|
+
api,
|
|
45
|
+
toolName: "feishu_message",
|
|
46
|
+
requiredTool: "message",
|
|
47
|
+
run: async ({ client }) => json(await runMessageAction(client, p)),
|
|
48
|
+
});
|
|
49
|
+
} catch (err) {
|
|
50
|
+
return errorResult(err);
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
{ name: "feishu_message" },
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
api.logger.debug?.("feishu_message: Registered feishu_message tool");
|
|
58
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Type, type Static } from "@sinclair/typebox";
|
|
2
|
+
|
|
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 ACTION_VALUES = ["get", "list"] as const;
|
|
11
|
+
const SORT_VALUES = ["ByCreateTimeAsc", "ByCreateTimeDesc"] as const;
|
|
12
|
+
|
|
13
|
+
export const FeishuMessageSchema = Type.Object({
|
|
14
|
+
action: stringEnum(ACTION_VALUES, {
|
|
15
|
+
description:
|
|
16
|
+
"Action to perform: get (get a single message by message_id), list (list recent messages in a chat by chat_id).",
|
|
17
|
+
}),
|
|
18
|
+
message_id: Type.Optional(
|
|
19
|
+
Type.String({
|
|
20
|
+
description: "Feishu message ID (e.g. om_xxx). Required for action=get.",
|
|
21
|
+
}),
|
|
22
|
+
),
|
|
23
|
+
chat_id: Type.Optional(
|
|
24
|
+
Type.String({
|
|
25
|
+
description: "Chat ID (e.g. oc_xxx). Required for action=list. Omit to use the current conversation's chat.",
|
|
26
|
+
}),
|
|
27
|
+
),
|
|
28
|
+
page_size: Type.Optional(
|
|
29
|
+
Type.Integer({
|
|
30
|
+
description: "Number of messages to fetch for action=list (default: 10, max: 50).",
|
|
31
|
+
minimum: 1,
|
|
32
|
+
maximum: 50,
|
|
33
|
+
default: 10,
|
|
34
|
+
}),
|
|
35
|
+
),
|
|
36
|
+
sort_type: Type.Optional(
|
|
37
|
+
stringEnum(SORT_VALUES, {
|
|
38
|
+
description: "Sort order for action=list. Default: ByCreateTimeDesc (newest first).",
|
|
39
|
+
default: "ByCreateTimeDesc",
|
|
40
|
+
}),
|
|
41
|
+
),
|
|
42
|
+
start_time: Type.Optional(
|
|
43
|
+
Type.String({
|
|
44
|
+
description:
|
|
45
|
+
"Start of time range for action=list, as Unix timestamp in seconds (e.g. \"1710000000\"). Omit for no lower bound.",
|
|
46
|
+
}),
|
|
47
|
+
),
|
|
48
|
+
end_time: Type.Optional(
|
|
49
|
+
Type.String({
|
|
50
|
+
description:
|
|
51
|
+
"End of time range for action=list, as Unix timestamp in seconds (e.g. \"1710086400\"). Omit for no upper bound.",
|
|
52
|
+
}),
|
|
53
|
+
),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
export type FeishuMessageParams = Static<typeof FeishuMessageSchema>;
|
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>;
|