@m1heng-clawd/feishu 0.1.14 → 0.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -2
- package/index.ts +7 -2
- package/package.json +5 -4
- package/skills/feishu-chat/SKILL.md +139 -0
- package/skills/feishu-urgent/SKILL.md +107 -0
- package/src/bot.ts +391 -45
- package/src/channel.ts +2 -0
- package/src/chat-tools/actions.ts +502 -0
- package/src/chat-tools/common.ts +13 -0
- package/src/chat-tools/index.ts +2 -0
- package/src/chat-tools/register.ts +72 -0
- package/src/chat-tools/schemas.ts +61 -0
- package/src/client.ts +13 -0
- package/src/config-schema.ts +6 -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 +75 -27
- package/src/drive-tools/schemas.ts +45 -58
- package/src/media-duration.ts +190 -0
- package/src/media.ts +50 -27
- package/src/mention.ts +1 -1
- package/src/monitor.ts +32 -8
- package/src/outbound.ts +2 -2
- package/src/perm-tools/actions.ts +27 -3
- package/src/perm-tools/schemas.ts +39 -45
- package/src/policy.ts +7 -3
- package/src/reply-dispatcher.ts +97 -13
- package/src/send.ts +360 -42
- package/src/streaming-card.ts +32 -4
- package/src/task-tools/actions.ts +145 -10
- package/src/task-tools/register.ts +56 -1
- package/src/task-tools/schemas.ts +23 -18
- package/src/tools-common/tool-context.ts +1 -0
- package/src/tools-config.ts +9 -2
- package/src/types.ts +8 -1
- package/src/typing.ts +115 -5
- package/src/urgent-tools/actions.ts +84 -0
- package/src/urgent-tools/index.ts +3 -0
- package/src/urgent-tools/register.ts +64 -0
- package/src/urgent-tools/schemas.ts +25 -0
- package/src/wiki-tools/actions.ts +24 -6
- package/src/wiki-tools/schemas.ts +32 -51
|
@@ -3,26 +3,33 @@ import os from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import type { TaskClient } from "./common.js";
|
|
5
5
|
import {
|
|
6
|
+
TASK_COMMENT_UPDATE_FIELD_VALUES,
|
|
6
7
|
TASKLIST_UPDATE_FIELD_VALUES,
|
|
7
8
|
TASK_UPDATE_FIELD_VALUES,
|
|
8
9
|
type AddTaskToTasklistParams,
|
|
9
10
|
type AddTasklistMembersParams,
|
|
11
|
+
type CreateTaskCommentParams,
|
|
10
12
|
type CreateTasklistParams,
|
|
11
|
-
CreateSubtaskParams,
|
|
12
|
-
CreateTaskParams,
|
|
13
|
-
DeleteTaskAttachmentParams,
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
type CreateSubtaskParams,
|
|
14
|
+
type CreateTaskParams,
|
|
15
|
+
type DeleteTaskAttachmentParams,
|
|
16
|
+
type DeleteTaskCommentParams,
|
|
17
|
+
type GetTaskAttachmentParams,
|
|
18
|
+
type GetTaskCommentParams,
|
|
19
|
+
type GetTaskParams,
|
|
16
20
|
type GetTasklistParams,
|
|
17
|
-
ListTaskAttachmentsParams,
|
|
21
|
+
type ListTaskAttachmentsParams,
|
|
22
|
+
type ListTaskCommentsParams,
|
|
18
23
|
type ListTasklistsParams,
|
|
19
24
|
type RemoveTaskFromTasklistParams,
|
|
20
25
|
type RemoveTasklistMembersParams,
|
|
21
|
-
|
|
26
|
+
type TaskCommentPatchComment,
|
|
27
|
+
type TaskUpdateTask,
|
|
22
28
|
type TasklistPatchTasklist,
|
|
23
|
-
UploadTaskAttachmentParams,
|
|
29
|
+
type UploadTaskAttachmentParams,
|
|
30
|
+
type UpdateTaskCommentParams,
|
|
24
31
|
type UpdateTasklistParams,
|
|
25
|
-
UpdateTaskParams,
|
|
32
|
+
type UpdateTaskParams,
|
|
26
33
|
} from "./schemas.js";
|
|
27
34
|
import {
|
|
28
35
|
DEFAULT_TASK_ATTACHMENT_FILENAME,
|
|
@@ -36,6 +43,7 @@ import { getFeishuRuntime } from "../runtime.js";
|
|
|
36
43
|
import { runTaskApiCall } from "./common.js";
|
|
37
44
|
|
|
38
45
|
const SUPPORTED_PATCH_FIELDS = new Set<string>(TASK_UPDATE_FIELD_VALUES);
|
|
46
|
+
const SUPPORTED_COMMENT_PATCH_FIELDS = new Set<string>(TASK_COMMENT_UPDATE_FIELD_VALUES);
|
|
39
47
|
const SUPPORTED_TASKLIST_PATCH_FIELDS = new Set<string>(TASKLIST_UPDATE_FIELD_VALUES);
|
|
40
48
|
|
|
41
49
|
function omitUndefined<T extends Record<string, unknown>>(obj: T): T {
|
|
@@ -50,10 +58,16 @@ function inferUpdateFields(task: TaskUpdateTask): string[] {
|
|
|
50
58
|
);
|
|
51
59
|
}
|
|
52
60
|
|
|
61
|
+
function inferCommentUpdateFields(comment: TaskCommentPatchComment): string[] {
|
|
62
|
+
return Object.keys(comment).filter((field) =>
|
|
63
|
+
SUPPORTED_COMMENT_PATCH_FIELDS.has(field),
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
53
67
|
function ensureSupportedUpdateFields(
|
|
54
68
|
updateFields: string[],
|
|
55
69
|
supported: Set<string>,
|
|
56
|
-
resource: "task" | "tasklist",
|
|
70
|
+
resource: "task" | "comment" | "tasklist",
|
|
57
71
|
) {
|
|
58
72
|
const invalid = updateFields.filter((field) => !supported.has(field));
|
|
59
73
|
if (invalid.length > 0) {
|
|
@@ -102,6 +116,20 @@ function formatTasklist(tasklist: Record<string, unknown> | undefined) {
|
|
|
102
116
|
};
|
|
103
117
|
}
|
|
104
118
|
|
|
119
|
+
function formatComment(comment: Record<string, unknown> | undefined) {
|
|
120
|
+
if (!comment) return undefined;
|
|
121
|
+
return {
|
|
122
|
+
id: comment.id,
|
|
123
|
+
content: comment.content,
|
|
124
|
+
creator: comment.creator,
|
|
125
|
+
reply_to_comment_id: comment.reply_to_comment_id,
|
|
126
|
+
created_at: comment.created_at,
|
|
127
|
+
updated_at: comment.updated_at,
|
|
128
|
+
resource_type: comment.resource_type,
|
|
129
|
+
resource_id: comment.resource_id,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
105
133
|
function formatAttachment(attachment: Record<string, unknown> | undefined) {
|
|
106
134
|
if (!attachment) return undefined;
|
|
107
135
|
return {
|
|
@@ -307,6 +335,113 @@ export async function getTask(client: TaskClient, params: GetTaskParams) {
|
|
|
307
335
|
};
|
|
308
336
|
}
|
|
309
337
|
|
|
338
|
+
export async function createTaskComment(client: TaskClient, params: CreateTaskCommentParams) {
|
|
339
|
+
const res = await runTaskApiCall("task.v2.comment.create", () =>
|
|
340
|
+
client.task.v2.comment.create({
|
|
341
|
+
data: omitUndefined({
|
|
342
|
+
content: params.content,
|
|
343
|
+
reply_to_comment_id: params.reply_to_comment_id,
|
|
344
|
+
resource_type: "task",
|
|
345
|
+
resource_id: params.task_guid,
|
|
346
|
+
}),
|
|
347
|
+
params: omitUndefined({
|
|
348
|
+
user_id_type: params.user_id_type,
|
|
349
|
+
}),
|
|
350
|
+
}),
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
return {
|
|
354
|
+
comment: formatComment((res.data?.comment ?? undefined) as Record<string, unknown> | undefined),
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export async function listTaskComments(client: TaskClient, params: ListTaskCommentsParams) {
|
|
359
|
+
const res = await runTaskApiCall("task.v2.comment.list", () =>
|
|
360
|
+
client.task.v2.comment.list({
|
|
361
|
+
params: omitUndefined({
|
|
362
|
+
resource_type: "task",
|
|
363
|
+
resource_id: params.task_guid,
|
|
364
|
+
page_size: params.page_size,
|
|
365
|
+
page_token: params.page_token,
|
|
366
|
+
direction: params.direction,
|
|
367
|
+
user_id_type: params.user_id_type,
|
|
368
|
+
}),
|
|
369
|
+
}),
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
const items = (res.data?.items ?? []) as Record<string, unknown>[];
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
items: items.map((item) => formatComment(item)),
|
|
376
|
+
page_token: res.data?.page_token,
|
|
377
|
+
has_more: res.data?.has_more,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export async function getTaskComment(client: TaskClient, params: GetTaskCommentParams) {
|
|
382
|
+
const res = await runTaskApiCall("task.v2.comment.get", () =>
|
|
383
|
+
client.task.v2.comment.get({
|
|
384
|
+
path: { comment_id: params.comment_id },
|
|
385
|
+
params: omitUndefined({
|
|
386
|
+
user_id_type: params.user_id_type,
|
|
387
|
+
}),
|
|
388
|
+
}),
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
comment: formatComment((res.data?.comment ?? undefined) as Record<string, unknown> | undefined),
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export async function updateTaskComment(client: TaskClient, params: UpdateTaskCommentParams) {
|
|
397
|
+
const comment = omitUndefined(params.comment as Record<string, unknown>) as TaskCommentPatchComment;
|
|
398
|
+
const updateFields = params.update_fields?.length
|
|
399
|
+
? params.update_fields
|
|
400
|
+
: inferCommentUpdateFields(comment);
|
|
401
|
+
|
|
402
|
+
if (params.update_fields?.length) {
|
|
403
|
+
ensureSupportedUpdateFields(updateFields, SUPPORTED_COMMENT_PATCH_FIELDS, "comment");
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (Object.keys(comment).length === 0) {
|
|
407
|
+
throw new Error("comment update payload is empty");
|
|
408
|
+
}
|
|
409
|
+
if (updateFields.length === 0) {
|
|
410
|
+
throw new Error("no valid update_fields provided or inferred from comment payload");
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const res = await runTaskApiCall("task.v2.comment.patch", () =>
|
|
414
|
+
client.task.v2.comment.patch({
|
|
415
|
+
path: { comment_id: params.comment_id },
|
|
416
|
+
data: {
|
|
417
|
+
comment,
|
|
418
|
+
update_fields: updateFields,
|
|
419
|
+
},
|
|
420
|
+
params: omitUndefined({
|
|
421
|
+
user_id_type: params.user_id_type,
|
|
422
|
+
}),
|
|
423
|
+
}),
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
return {
|
|
427
|
+
comment: formatComment((res.data?.comment ?? undefined) as Record<string, unknown> | undefined),
|
|
428
|
+
update_fields: updateFields,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export async function deleteTaskComment(client: TaskClient, params: DeleteTaskCommentParams) {
|
|
433
|
+
await runTaskApiCall("task.v2.comment.delete", () =>
|
|
434
|
+
client.task.v2.comment.delete({
|
|
435
|
+
path: { comment_id: params.comment_id },
|
|
436
|
+
}),
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
success: true,
|
|
441
|
+
comment_id: params.comment_id,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
310
445
|
export async function getTasklist(client: TaskClient, params: GetTasklistParams) {
|
|
311
446
|
const res = await runTaskApiCall("task.v2.tasklist.get", () =>
|
|
312
447
|
client.task.v2.tasklist.get({
|
|
@@ -6,19 +6,24 @@ import {
|
|
|
6
6
|
addTaskToTasklist,
|
|
7
7
|
addTasklistMembers,
|
|
8
8
|
createSubtask,
|
|
9
|
+
createTaskComment,
|
|
9
10
|
createTask,
|
|
10
11
|
createTasklist,
|
|
11
12
|
deleteTaskAttachment,
|
|
13
|
+
deleteTaskComment,
|
|
12
14
|
deleteTask,
|
|
13
15
|
deleteTasklist,
|
|
14
16
|
getTaskAttachment,
|
|
17
|
+
getTaskComment,
|
|
15
18
|
getTask,
|
|
16
19
|
getTasklist,
|
|
20
|
+
listTaskComments,
|
|
17
21
|
listTasklists,
|
|
18
22
|
removeTaskFromTasklist,
|
|
19
23
|
removeTasklistMembers,
|
|
20
24
|
listTaskAttachments,
|
|
21
25
|
uploadTaskAttachment,
|
|
26
|
+
updateTaskComment,
|
|
22
27
|
updateTask,
|
|
23
28
|
updateTasklist,
|
|
24
29
|
} from "./actions.js";
|
|
@@ -36,26 +41,36 @@ import {
|
|
|
36
41
|
type CreateTasklistParams,
|
|
37
42
|
DeleteTaskAttachmentSchema,
|
|
38
43
|
type DeleteTaskAttachmentParams,
|
|
44
|
+
DeleteTaskCommentSchema,
|
|
45
|
+
type DeleteTaskCommentParams,
|
|
39
46
|
DeleteTaskSchema,
|
|
40
47
|
type DeleteTaskParams,
|
|
41
48
|
DeleteTasklistSchema,
|
|
42
49
|
type DeleteTasklistParams,
|
|
43
50
|
GetTaskAttachmentSchema,
|
|
44
51
|
type GetTaskAttachmentParams,
|
|
52
|
+
GetTaskCommentSchema,
|
|
53
|
+
type GetTaskCommentParams,
|
|
45
54
|
GetTaskSchema,
|
|
46
55
|
type GetTaskParams,
|
|
47
56
|
GetTasklistSchema,
|
|
48
57
|
type GetTasklistParams,
|
|
49
58
|
ListTaskAttachmentsSchema,
|
|
50
59
|
type ListTaskAttachmentsParams,
|
|
60
|
+
ListTaskCommentsSchema,
|
|
61
|
+
type ListTaskCommentsParams,
|
|
51
62
|
ListTasklistsSchema,
|
|
52
63
|
type ListTasklistsParams,
|
|
53
64
|
RemoveTaskFromTasklistSchema,
|
|
54
65
|
type RemoveTaskFromTasklistParams,
|
|
55
66
|
RemoveTasklistMembersSchema,
|
|
56
67
|
type RemoveTasklistMembersParams,
|
|
68
|
+
CreateTaskCommentSchema,
|
|
69
|
+
type CreateTaskCommentParams,
|
|
57
70
|
UploadTaskAttachmentSchema,
|
|
58
71
|
type UploadTaskAttachmentParams,
|
|
72
|
+
UpdateTaskCommentSchema,
|
|
73
|
+
type UpdateTaskCommentParams,
|
|
59
74
|
UpdateTaskSchema,
|
|
60
75
|
UpdateTasklistSchema,
|
|
61
76
|
type UpdateTaskParams,
|
|
@@ -259,5 +274,45 @@ export function registerFeishuTaskTools(api: OpenClawPluginApi) {
|
|
|
259
274
|
run: async ({ client }, params) => updateTask(client, params),
|
|
260
275
|
});
|
|
261
276
|
|
|
262
|
-
api
|
|
277
|
+
registerTaskTool<CreateTaskCommentParams>(api, {
|
|
278
|
+
name: "feishu_task_comment_create",
|
|
279
|
+
label: "Feishu Task Comment Create",
|
|
280
|
+
description: "Create a comment on a Feishu task (task v2)",
|
|
281
|
+
parameters: CreateTaskCommentSchema,
|
|
282
|
+
run: async ({ client }, params) => createTaskComment(client, params),
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
registerTaskTool<ListTaskCommentsParams>(api, {
|
|
286
|
+
name: "feishu_task_comment_list",
|
|
287
|
+
label: "Feishu Task Comment List",
|
|
288
|
+
description: "List comments for a Feishu task (task v2)",
|
|
289
|
+
parameters: ListTaskCommentsSchema,
|
|
290
|
+
run: async ({ client }, params) => listTaskComments(client, params),
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
registerTaskTool<GetTaskCommentParams>(api, {
|
|
294
|
+
name: "feishu_task_comment_get",
|
|
295
|
+
label: "Feishu Task Comment Get",
|
|
296
|
+
description: "Get a Feishu task comment by comment_id (task v2)",
|
|
297
|
+
parameters: GetTaskCommentSchema,
|
|
298
|
+
run: async ({ client }, params) => getTaskComment(client, params),
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
registerTaskTool<UpdateTaskCommentParams>(api, {
|
|
302
|
+
name: "feishu_task_comment_update",
|
|
303
|
+
label: "Feishu Task Comment Update",
|
|
304
|
+
description: "Update a Feishu task comment by comment_id (task v2 patch)",
|
|
305
|
+
parameters: UpdateTaskCommentSchema,
|
|
306
|
+
run: async ({ client }, params) => updateTaskComment(client, params),
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
registerTaskTool<DeleteTaskCommentParams>(api, {
|
|
310
|
+
name: "feishu_task_comment_delete",
|
|
311
|
+
label: "Feishu Task Comment Delete",
|
|
312
|
+
description: "Delete a Feishu task comment by comment_id (task v2)",
|
|
313
|
+
parameters: DeleteTaskCommentSchema,
|
|
314
|
+
run: async ({ client }, params) => deleteTaskComment(client, params),
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
api.logger.debug?.("feishu_task: Registered task, comment, tasklist, subtask, and attachment tools");
|
|
263
318
|
}
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import type { TaskClient } from "./common.js";
|
|
3
3
|
|
|
4
|
+
// NOTE: Avoid Type.Union([Type.Literal(...)]) which compiles to anyOf.
|
|
5
|
+
// Some providers reject anyOf in tool schemas; a flat string enum is safer.
|
|
6
|
+
function stringEnum<T extends readonly string[]>(
|
|
7
|
+
values: T,
|
|
8
|
+
options: { description?: string; default?: T[number] } = {},
|
|
9
|
+
) {
|
|
10
|
+
return Type.Unsafe<T[number]>({ type: "string", enum: [...values], ...options });
|
|
11
|
+
}
|
|
12
|
+
|
|
4
13
|
type TaskCreatePayload = NonNullable<Parameters<TaskClient["task"]["v2"]["task"]["create"]>[0]>;
|
|
5
14
|
type TaskUpdatePayload = NonNullable<Parameters<TaskClient["task"]["v2"]["task"]["patch"]>[0]>;
|
|
6
15
|
type TaskDeletePayload = NonNullable<Parameters<TaskClient["task"]["v2"]["task"]["delete"]>[0]>;
|
|
@@ -75,6 +84,7 @@ export const TASK_UPDATE_FIELD_VALUES = [
|
|
|
75
84
|
"mode",
|
|
76
85
|
"is_milestone",
|
|
77
86
|
] as const;
|
|
87
|
+
export const TASK_COMMENT_UPDATE_FIELD_VALUES = ["content"] as const;
|
|
78
88
|
export const TASKLIST_UPDATE_FIELD_VALUES = ["name", "owner", "archive_tasklist"] as const;
|
|
79
89
|
|
|
80
90
|
export type CreateTaskParams = {
|
|
@@ -245,23 +255,19 @@ const TasklistRefSchema = Type.Object({
|
|
|
245
255
|
section_guid: Type.Optional(Type.String({ description: "Section GUID in tasklist" })),
|
|
246
256
|
});
|
|
247
257
|
|
|
248
|
-
const TaskUpdateFieldSchema =
|
|
249
|
-
TASK_UPDATE_FIELD_VALUES.map((field) => Type.Literal(field)),
|
|
250
|
-
);
|
|
258
|
+
const TaskUpdateFieldSchema = stringEnum(TASK_UPDATE_FIELD_VALUES);
|
|
251
259
|
|
|
252
|
-
const
|
|
253
|
-
[Type.Literal("owner"), Type.Literal("editor"), Type.Literal("viewer")],
|
|
254
|
-
{ description: "Member role (owner/editor/viewer)" },
|
|
255
|
-
);
|
|
260
|
+
const TaskCommentUpdateFieldSchema = stringEnum(TASK_COMMENT_UPDATE_FIELD_VALUES);
|
|
256
261
|
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
);
|
|
262
|
+
const TasklistMemberRoleSchema = stringEnum(["owner", "editor", "viewer"] as const, {
|
|
263
|
+
description: "Member role (owner/editor/viewer)",
|
|
264
|
+
});
|
|
260
265
|
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
266
|
+
const TasklistUpdateFieldSchema = stringEnum(TASKLIST_UPDATE_FIELD_VALUES);
|
|
267
|
+
|
|
268
|
+
const TasklistOriginOwnerRoleSchema = stringEnum(["editor", "viewer", "none"] as const, {
|
|
269
|
+
description: "Role for original owner after owner transfer",
|
|
270
|
+
});
|
|
265
271
|
|
|
266
272
|
const TasklistMemberSchema = Type.Object({
|
|
267
273
|
id: Type.String({ description: "Member ID (with type controlled by user_id_type)" }),
|
|
@@ -372,9 +378,7 @@ export const ListTaskCommentsSchema = Type.Object({
|
|
|
372
378
|
),
|
|
373
379
|
page_token: Type.Optional(Type.String({ description: "Pagination token" })),
|
|
374
380
|
direction: Type.Optional(
|
|
375
|
-
|
|
376
|
-
description: "Sort direction",
|
|
377
|
-
}),
|
|
381
|
+
stringEnum(["asc", "desc"] as const, { description: "Sort direction" }),
|
|
378
382
|
),
|
|
379
383
|
user_id_type: Type.Optional(
|
|
380
384
|
Type.String({ description: "User ID type for returned creators" }),
|
|
@@ -399,9 +403,10 @@ export const UpdateTaskCommentSchema = Type.Object({
|
|
|
399
403
|
comment_id: Type.String({ description: "Comment ID to update" }),
|
|
400
404
|
comment: TaskCommentUpdateContentSchema,
|
|
401
405
|
update_fields: Type.Optional(
|
|
402
|
-
Type.Array(
|
|
406
|
+
Type.Array(TaskCommentUpdateFieldSchema, {
|
|
403
407
|
description: "Fields to update. If omitted, this tool infers from keys in comment (content)",
|
|
404
408
|
minItems: 1,
|
|
409
|
+
uniqueItems: true,
|
|
405
410
|
}),
|
|
406
411
|
),
|
|
407
412
|
user_id_type: Type.Optional(
|
package/src/tools-config.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { FeishuToolsConfig } from "./types.js";
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Default tool configuration.
|
|
5
|
-
* - doc, wiki, drive, scopes, task: enabled by default
|
|
5
|
+
* - doc, wiki, drive, scopes, task, chat: enabled by default
|
|
6
6
|
* - perm: disabled by default (sensitive operation)
|
|
7
7
|
*/
|
|
8
8
|
export const DEFAULT_TOOLS_CONFIG: Required<FeishuToolsConfig> = {
|
|
@@ -12,11 +12,18 @@ export const DEFAULT_TOOLS_CONFIG: Required<FeishuToolsConfig> = {
|
|
|
12
12
|
perm: false,
|
|
13
13
|
scopes: true,
|
|
14
14
|
task: true,
|
|
15
|
+
chat: true,
|
|
16
|
+
urgent: true,
|
|
15
17
|
};
|
|
16
18
|
|
|
17
19
|
/**
|
|
18
20
|
* Resolve tools config with defaults.
|
|
21
|
+
* Only includes keys that are present in the input config or defaults.
|
|
19
22
|
*/
|
|
20
23
|
export function resolveToolsConfig(cfg?: FeishuToolsConfig): Required<FeishuToolsConfig> {
|
|
21
|
-
|
|
24
|
+
const res = {} as Required<FeishuToolsConfig>;
|
|
25
|
+
for (const key in DEFAULT_TOOLS_CONFIG) {
|
|
26
|
+
res[key] = cfg?.[key] ?? DEFAULT_TOOLS_CONFIG[key];
|
|
27
|
+
}
|
|
28
|
+
return res;
|
|
22
29
|
}
|
package/src/types.ts
CHANGED
|
@@ -30,13 +30,17 @@ export type FeishuMessageContext = {
|
|
|
30
30
|
senderId: string;
|
|
31
31
|
senderOpenId: string;
|
|
32
32
|
senderName?: string;
|
|
33
|
-
chatType: "p2p" | "group";
|
|
33
|
+
chatType: "p2p" | "group" | "private";
|
|
34
34
|
mentionedBot: boolean;
|
|
35
35
|
hasAnyMention?: boolean;
|
|
36
36
|
rootId?: string;
|
|
37
37
|
parentId?: string;
|
|
38
38
|
content: string;
|
|
39
39
|
contentType: string;
|
|
40
|
+
/** Media placeholder token (e.g. <media:audio>) when inbound message is media. */
|
|
41
|
+
placeholder?: string;
|
|
42
|
+
/** Raw provider payload string (original event.message.content). */
|
|
43
|
+
rawPayload?: string;
|
|
40
44
|
/** Mention forward targets (excluding the bot itself) */
|
|
41
45
|
mentionTargets?: MentionTarget[];
|
|
42
46
|
/** Extracted message body (after removing @ placeholders) */
|
|
@@ -69,6 +73,9 @@ export type FeishuToolsConfig = {
|
|
|
69
73
|
perm?: boolean;
|
|
70
74
|
scopes?: boolean;
|
|
71
75
|
task?: boolean;
|
|
76
|
+
chat?: boolean;
|
|
77
|
+
/** Enable the feishu_urgent tool (buzz/urgent notifications). Enabled by default. */
|
|
78
|
+
urgent?: boolean;
|
|
72
79
|
};
|
|
73
80
|
|
|
74
81
|
export type DynamicAgentCreationConfig = {
|
package/src/typing.ts
CHANGED
|
@@ -7,13 +7,94 @@ import { resolveFeishuAccount } from "./accounts.js";
|
|
|
7
7
|
// Full list: https://github.com/go-lark/lark/blob/main/emoji.go
|
|
8
8
|
const TYPING_EMOJI = "Typing"; // Typing indicator emoji
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Feishu API error codes that indicate the caller should back off.
|
|
12
|
+
* These must propagate to the typing circuit breaker so the keepalive loop
|
|
13
|
+
* can trip and stop retrying.
|
|
14
|
+
*
|
|
15
|
+
* - 99991400: Rate limit (too many requests per second)
|
|
16
|
+
* - 99991403: Monthly API call quota exceeded
|
|
17
|
+
* - 429: Standard HTTP 429 returned as a Feishu SDK error code
|
|
18
|
+
*
|
|
19
|
+
* @see https://open.feishu.cn/document/server-docs/api-call-guide/generic-error-code
|
|
20
|
+
*/
|
|
21
|
+
const FEISHU_BACKOFF_CODES = new Set([99991400, 99991403, 429]);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Custom error class for Feishu backoff conditions detected from non-throwing
|
|
25
|
+
* SDK responses. Carries a numeric `.code` so that `isFeishuBackoffError()`
|
|
26
|
+
* recognises it when the error is caught downstream.
|
|
27
|
+
*/
|
|
28
|
+
export class FeishuBackoffError extends Error {
|
|
29
|
+
code: number;
|
|
30
|
+
constructor(code: number) {
|
|
31
|
+
super(`Feishu API backoff: code ${code}`);
|
|
32
|
+
this.name = "FeishuBackoffError";
|
|
33
|
+
this.code = code;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Check whether an error represents a rate-limit or quota-exceeded condition
|
|
39
|
+
* from the Feishu API that should stop the typing keepalive loop.
|
|
40
|
+
*
|
|
41
|
+
* Handles two shapes:
|
|
42
|
+
* 1. AxiosError with `response.status` and `response.data.code`
|
|
43
|
+
* 2. Feishu SDK error with a top-level `code` property
|
|
44
|
+
*/
|
|
45
|
+
export function isFeishuBackoffError(err: unknown): boolean {
|
|
46
|
+
if (typeof err !== "object" || err === null) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// AxiosError shape: err.response.status / err.response.data.code
|
|
51
|
+
const response = (err as { response?: { status?: number; data?: { code?: number } } }).response;
|
|
52
|
+
if (response) {
|
|
53
|
+
if (response.status === 429) {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
if (typeof response.data?.code === "number" && FEISHU_BACKOFF_CODES.has(response.data.code)) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Feishu SDK error shape: err.code
|
|
62
|
+
const code = (err as { code?: number }).code;
|
|
63
|
+
if (typeof code === "number" && FEISHU_BACKOFF_CODES.has(code)) {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Check whether a Feishu SDK response object contains a backoff error code.
|
|
72
|
+
*
|
|
73
|
+
* The Feishu SDK sometimes returns a normal response (no throw) with an
|
|
74
|
+
* API-level error code in the response body. This must be detected so the
|
|
75
|
+
* circuit breaker can trip.
|
|
76
|
+
*/
|
|
77
|
+
export function getBackoffCodeFromResponse(response: unknown): number | undefined {
|
|
78
|
+
if (typeof response !== "object" || response === null) {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
const code = (response as { code?: number }).code;
|
|
82
|
+
if (typeof code === "number" && FEISHU_BACKOFF_CODES.has(code)) {
|
|
83
|
+
return code;
|
|
84
|
+
}
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
10
88
|
export type TypingIndicatorState = {
|
|
11
89
|
messageId: string;
|
|
12
90
|
reactionId: string | null;
|
|
13
91
|
};
|
|
14
92
|
|
|
15
93
|
/**
|
|
16
|
-
* Add a typing indicator (reaction) to a message
|
|
94
|
+
* Add a typing indicator (reaction) to a message.
|
|
95
|
+
*
|
|
96
|
+
* Rate-limit and quota errors are re-thrown so the circuit breaker in
|
|
97
|
+
* `createTypingCallbacks` can trip and stop the keepalive loop.
|
|
17
98
|
*/
|
|
18
99
|
export async function addTypingIndicator(params: {
|
|
19
100
|
cfg: ClawdbotConfig;
|
|
@@ -36,17 +117,33 @@ export async function addTypingIndicator(params: {
|
|
|
36
117
|
},
|
|
37
118
|
});
|
|
38
119
|
|
|
120
|
+
// Feishu SDK may return a normal response with an API-level error code
|
|
121
|
+
// instead of throwing. Detect backoff codes and throw to trip the breaker.
|
|
122
|
+
const backoffCode = getBackoffCodeFromResponse(response);
|
|
123
|
+
if (backoffCode !== undefined) {
|
|
124
|
+
console.log(
|
|
125
|
+
`[feishu] typing indicator response contains backoff code ${backoffCode}, stopping keepalive`,
|
|
126
|
+
);
|
|
127
|
+
throw new FeishuBackoffError(backoffCode);
|
|
128
|
+
}
|
|
129
|
+
|
|
39
130
|
const reactionId = (response as any)?.data?.reaction_id ?? null;
|
|
40
131
|
return { messageId, reactionId };
|
|
41
132
|
} catch (err) {
|
|
42
|
-
|
|
133
|
+
if (isFeishuBackoffError(err)) {
|
|
134
|
+
console.log(`[feishu] typing indicator hit rate-limit/quota, stopping keepalive`);
|
|
135
|
+
throw err;
|
|
136
|
+
}
|
|
137
|
+
// Silently fail for other non-critical errors (e.g. message deleted, permission issues)
|
|
43
138
|
console.log(`[feishu] failed to add typing indicator: ${err}`);
|
|
44
139
|
return { messageId, reactionId: null };
|
|
45
140
|
}
|
|
46
141
|
}
|
|
47
142
|
|
|
48
143
|
/**
|
|
49
|
-
* Remove a typing indicator (reaction) from a message
|
|
144
|
+
* Remove a typing indicator (reaction) from a message.
|
|
145
|
+
*
|
|
146
|
+
* Rate-limit and quota errors are re-thrown for the same reason as above.
|
|
50
147
|
*/
|
|
51
148
|
export async function removeTypingIndicator(params: {
|
|
52
149
|
cfg: ClawdbotConfig;
|
|
@@ -62,14 +159,27 @@ export async function removeTypingIndicator(params: {
|
|
|
62
159
|
const client = createFeishuClient(account);
|
|
63
160
|
|
|
64
161
|
try {
|
|
65
|
-
await client.im.messageReaction.delete({
|
|
162
|
+
const result = await client.im.messageReaction.delete({
|
|
66
163
|
path: {
|
|
67
164
|
message_id: state.messageId,
|
|
68
165
|
reaction_id: state.reactionId,
|
|
69
166
|
},
|
|
70
167
|
});
|
|
168
|
+
|
|
169
|
+
// Check for backoff codes in non-throwing SDK responses
|
|
170
|
+
const backoffCode = getBackoffCodeFromResponse(result);
|
|
171
|
+
if (backoffCode !== undefined) {
|
|
172
|
+
console.log(
|
|
173
|
+
`[feishu] typing indicator removal response contains backoff code ${backoffCode}, stopping keepalive`,
|
|
174
|
+
);
|
|
175
|
+
throw new FeishuBackoffError(backoffCode);
|
|
176
|
+
}
|
|
71
177
|
} catch (err) {
|
|
72
|
-
|
|
178
|
+
if (isFeishuBackoffError(err)) {
|
|
179
|
+
console.log(`[feishu] typing indicator removal hit rate-limit/quota, stopping keepalive`);
|
|
180
|
+
throw err;
|
|
181
|
+
}
|
|
182
|
+
// Silently fail for other non-critical errors
|
|
73
183
|
console.log(`[feishu] failed to remove typing indicator: ${err}`);
|
|
74
184
|
}
|
|
75
185
|
}
|