@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
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
import type { ChatClient } from "./common.js";
|
|
2
|
+
import type { FeishuChatParams } from "./schemas.js";
|
|
3
|
+
import { runChatApiCall } from "./common.js";
|
|
4
|
+
|
|
5
|
+
type UserIdType = "open_id" | "user_id" | "union_id";
|
|
6
|
+
type MemberIdType = UserIdType | "app_id";
|
|
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
|
+
|
|
15
|
+
function requireStringArray(value: unknown, field: string): string[] {
|
|
16
|
+
if (!Array.isArray(value) || value.length === 0 || value.some((item) => typeof item !== "string")) {
|
|
17
|
+
throw new Error(`${field} must be a non-empty string array`);
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const BLOCK_TYPE_NAMES: Record<number, string> = {
|
|
23
|
+
1: "Page",
|
|
24
|
+
2: "Text",
|
|
25
|
+
3: "Heading1",
|
|
26
|
+
4: "Heading2",
|
|
27
|
+
5: "Heading3",
|
|
28
|
+
12: "Bullet",
|
|
29
|
+
13: "Ordered",
|
|
30
|
+
14: "Code",
|
|
31
|
+
15: "Quote",
|
|
32
|
+
17: "Todo",
|
|
33
|
+
18: "Bitable",
|
|
34
|
+
21: "Diagram",
|
|
35
|
+
22: "Divider",
|
|
36
|
+
23: "File",
|
|
37
|
+
27: "Image",
|
|
38
|
+
30: "Sheet",
|
|
39
|
+
31: "Table",
|
|
40
|
+
32: "TableCell",
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const STRUCTURED_BLOCK_TYPES = new Set([14, 18, 21, 23, 27, 30, 31, 32]);
|
|
44
|
+
|
|
45
|
+
async function getAnnouncement(client: ChatClient, chatId: string) {
|
|
46
|
+
// Use docx.chatAnnouncement.get first — it works for both doc and docx announcements
|
|
47
|
+
// and returns announcement_type in its response, avoiding the noisy 232097 error
|
|
48
|
+
// that would occur when calling the legacy im API on a docx announcement.
|
|
49
|
+
const infoRes = await runChatApiCall("docx.chatAnnouncement.get", () =>
|
|
50
|
+
(client as any).docx.chatAnnouncement.get({
|
|
51
|
+
path: { chat_id: chatId },
|
|
52
|
+
}),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const announcementType = (infoRes as any).data?.announcement_type;
|
|
56
|
+
|
|
57
|
+
if (announcementType === "doc") {
|
|
58
|
+
// Legacy doc format: fetch actual content via the im API
|
|
59
|
+
const docRes = await runChatApiCall("im.chatAnnouncement.get", () =>
|
|
60
|
+
(client as any).im.chatAnnouncement.get({
|
|
61
|
+
path: { chat_id: chatId },
|
|
62
|
+
}),
|
|
63
|
+
);
|
|
64
|
+
return {
|
|
65
|
+
announcement_type: "doc" as const,
|
|
66
|
+
...(docRes as any).data,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// docx format (or unrecognised new format): fetch blocks
|
|
71
|
+
const blocksRes = await runChatApiCall("docx.chatAnnouncementBlock.list", () =>
|
|
72
|
+
(client as any).docx.chatAnnouncementBlock.list({
|
|
73
|
+
path: { chat_id: chatId },
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const blocks = (blocksRes as any).data?.items ?? [];
|
|
78
|
+
const blockCounts: Record<string, number> = {};
|
|
79
|
+
const structuredTypes: string[] = [];
|
|
80
|
+
|
|
81
|
+
for (const b of blocks) {
|
|
82
|
+
const type = b.block_type ?? 0;
|
|
83
|
+
const name = BLOCK_TYPE_NAMES[type] || `type_${type}`;
|
|
84
|
+
blockCounts[name] = (blockCounts[name] || 0) + 1;
|
|
85
|
+
|
|
86
|
+
if (STRUCTURED_BLOCK_TYPES.has(type) && !structuredTypes.includes(name)) {
|
|
87
|
+
structuredTypes.push(name);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let hint: string | undefined;
|
|
92
|
+
if (structuredTypes.length > 0) {
|
|
93
|
+
hint = `This announcement contains ${structuredTypes.join(", ")} which are NOT included in the basic info. Use action: "list_announcement_blocks" to get full content.`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
announcement_type: "docx" as const,
|
|
98
|
+
info: (infoRes as any).data,
|
|
99
|
+
blocks,
|
|
100
|
+
block_count: blocks.length,
|
|
101
|
+
block_types: blockCounts,
|
|
102
|
+
...(hint && { hint }),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function listAnnouncementBlocks(client: ChatClient, chatId: string) {
|
|
107
|
+
const res = await runChatApiCall("docx.chatAnnouncementBlock.list", () =>
|
|
108
|
+
(client as any).docx.chatAnnouncementBlock.list({
|
|
109
|
+
path: { chat_id: chatId },
|
|
110
|
+
}),
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
blocks: (res as any).data?.items ?? [],
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function getAnnouncementBlock(client: ChatClient, chatId: string, blockId: string) {
|
|
119
|
+
const res = await runChatApiCall("docx.chatAnnouncementBlock.get", () =>
|
|
120
|
+
(client as any).docx.chatAnnouncementBlock.get({
|
|
121
|
+
path: { chat_id: chatId, block_id: blockId },
|
|
122
|
+
}),
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
block: (res as any).data?.block,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function writeDocAnnouncement(client: ChatClient, chatId: string, content: string) {
|
|
131
|
+
const current = await runChatApiCall("im.chatAnnouncement.get", () =>
|
|
132
|
+
(client as any).im.chatAnnouncement.get({
|
|
133
|
+
path: { chat_id: chatId },
|
|
134
|
+
}),
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const revision = (current as any).data?.revision;
|
|
138
|
+
if (!revision) {
|
|
139
|
+
throw new Error("Failed to load current announcement revision");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let res: unknown;
|
|
143
|
+
try {
|
|
144
|
+
// Prefer SDK current payload shape (revision + requests[]).
|
|
145
|
+
res = await runChatApiCall("im.chatAnnouncement.patch", () =>
|
|
146
|
+
(client as any).im.chatAnnouncement.patch({
|
|
147
|
+
path: { chat_id: chatId },
|
|
148
|
+
data: {
|
|
149
|
+
revision,
|
|
150
|
+
requests: [content],
|
|
151
|
+
},
|
|
152
|
+
}),
|
|
153
|
+
);
|
|
154
|
+
} catch {
|
|
155
|
+
// Backward compatibility for tenants still accepting legacy content payload.
|
|
156
|
+
res = await runChatApiCall("im.chatAnnouncement.patch", () =>
|
|
157
|
+
(client as any).im.chatAnnouncement.patch({
|
|
158
|
+
path: { chat_id: chatId },
|
|
159
|
+
data: {
|
|
160
|
+
content,
|
|
161
|
+
revision,
|
|
162
|
+
},
|
|
163
|
+
}),
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
success: true,
|
|
169
|
+
announcement_type: "doc",
|
|
170
|
+
...(res as any).data,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function createAnnouncementBlockChild(
|
|
175
|
+
client: ChatClient,
|
|
176
|
+
chatId: string,
|
|
177
|
+
parentBlockId: string,
|
|
178
|
+
blockData: any,
|
|
179
|
+
) {
|
|
180
|
+
const res = await runChatApiCall("docx.chatAnnouncementBlockChildren.create", () =>
|
|
181
|
+
(client as any).docx.chatAnnouncementBlockChildren.create({
|
|
182
|
+
path: { chat_id: chatId, block_id: parentBlockId },
|
|
183
|
+
data: blockData,
|
|
184
|
+
}),
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
success: true,
|
|
189
|
+
block: (res as any).data,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function createTextBlock(
|
|
194
|
+
client: ChatClient,
|
|
195
|
+
chatId: string,
|
|
196
|
+
parentBlockId: string,
|
|
197
|
+
text: string,
|
|
198
|
+
) {
|
|
199
|
+
const blockData = {
|
|
200
|
+
children: [
|
|
201
|
+
{
|
|
202
|
+
block_type: 2,
|
|
203
|
+
text: {
|
|
204
|
+
elements: [
|
|
205
|
+
{
|
|
206
|
+
text_run: {
|
|
207
|
+
content: text,
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
],
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
return createAnnouncementBlockChild(client, chatId, parentBlockId, blockData);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function batchUpdateAnnouncementBlocks(
|
|
220
|
+
client: ChatClient,
|
|
221
|
+
chatId: string,
|
|
222
|
+
requests: any[],
|
|
223
|
+
) {
|
|
224
|
+
const info = await runChatApiCall("docx.chatAnnouncement.get", () =>
|
|
225
|
+
(client as any).docx.chatAnnouncement.get({
|
|
226
|
+
path: { chat_id: chatId },
|
|
227
|
+
}),
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
const res = await runChatApiCall("docx.chatAnnouncementBlock.batchUpdate", () =>
|
|
231
|
+
(client as any).docx.chatAnnouncementBlock.batchUpdate({
|
|
232
|
+
path: { chat_id: chatId },
|
|
233
|
+
params: {
|
|
234
|
+
revision_id: (info as any).data?.revision_id,
|
|
235
|
+
},
|
|
236
|
+
data: {
|
|
237
|
+
requests,
|
|
238
|
+
},
|
|
239
|
+
}),
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
success: true,
|
|
244
|
+
...(res as any).data,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ============== New Chat Management Functions ==============
|
|
249
|
+
|
|
250
|
+
async function createChat(
|
|
251
|
+
client: ChatClient,
|
|
252
|
+
name: string,
|
|
253
|
+
userIds?: string[],
|
|
254
|
+
description?: string,
|
|
255
|
+
userIdType: UserIdType = "open_id",
|
|
256
|
+
) {
|
|
257
|
+
const data: any = { name };
|
|
258
|
+
if (userIds && userIds.length > 0) {
|
|
259
|
+
data.user_id_list = userIds;
|
|
260
|
+
}
|
|
261
|
+
if (description) {
|
|
262
|
+
data.description = description;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const res = await runChatApiCall("im.chat.create", () =>
|
|
266
|
+
(client as any).im.chat.create({
|
|
267
|
+
data,
|
|
268
|
+
params: { user_id_type: userIdType },
|
|
269
|
+
}),
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
success: true,
|
|
274
|
+
chat_id: (res as any).data?.chat_id,
|
|
275
|
+
...(res as any).data,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function addMembers(
|
|
280
|
+
client: ChatClient,
|
|
281
|
+
chatId: string,
|
|
282
|
+
userIds: string[],
|
|
283
|
+
memberIdType: MemberIdType = "open_id",
|
|
284
|
+
) {
|
|
285
|
+
const res = await runChatApiCall("im.chatMembers.create", () =>
|
|
286
|
+
(client as any).im.chatMembers.create({
|
|
287
|
+
path: { chat_id: chatId },
|
|
288
|
+
params: { member_id_type: memberIdType },
|
|
289
|
+
data: { id_list: userIds },
|
|
290
|
+
}),
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
success: true,
|
|
295
|
+
chat_id: chatId,
|
|
296
|
+
added_user_ids: userIds,
|
|
297
|
+
...(res as any).data,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function checkBotInChat(client: ChatClient, chatId: string) {
|
|
302
|
+
try {
|
|
303
|
+
const res = await runChatApiCall("im.chat.get", () =>
|
|
304
|
+
(client as any).im.chat.get({ path: { chat_id: chatId } }),
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
success: true,
|
|
309
|
+
chat_id: chatId,
|
|
310
|
+
in_chat: true,
|
|
311
|
+
chat_info: (res as any).data,
|
|
312
|
+
};
|
|
313
|
+
} catch (err: any) {
|
|
314
|
+
if (err?.message?.includes("90003")) {
|
|
315
|
+
return {
|
|
316
|
+
success: true,
|
|
317
|
+
chat_id: chatId,
|
|
318
|
+
in_chat: false,
|
|
319
|
+
error: "Bot is not in this chat",
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
throw err;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async function sendMessage(client: ChatClient, chatId: string, content: string) {
|
|
327
|
+
const res = await runChatApiCall("im.message.create", () =>
|
|
328
|
+
(client as any).im.message.create({
|
|
329
|
+
params: { receive_id_type: "chat_id" },
|
|
330
|
+
data: {
|
|
331
|
+
receive_id: chatId,
|
|
332
|
+
msg_type: "text",
|
|
333
|
+
content: JSON.stringify({ text: content }),
|
|
334
|
+
},
|
|
335
|
+
}),
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
success: true,
|
|
340
|
+
message_id: (res as any).data?.message_id,
|
|
341
|
+
...(res as any).data,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async function createSessionChat(
|
|
346
|
+
client: ChatClient,
|
|
347
|
+
name: string,
|
|
348
|
+
userIds: string[],
|
|
349
|
+
greeting?: string,
|
|
350
|
+
description?: string,
|
|
351
|
+
userIdType: UserIdType = "open_id",
|
|
352
|
+
) {
|
|
353
|
+
// Step 1: Create the chat
|
|
354
|
+
const createResult = await createChat(client, name, userIds, description, userIdType);
|
|
355
|
+
const chatId = createResult.chat_id;
|
|
356
|
+
|
|
357
|
+
if (!chatId) {
|
|
358
|
+
return {
|
|
359
|
+
success: false,
|
|
360
|
+
error: "Failed to create chat - no chat_id returned",
|
|
361
|
+
create_result: createResult,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Step 2: Send greeting message
|
|
366
|
+
const defaultGreeting = "Hello! I've created this group chat for us to collaborate.";
|
|
367
|
+
const greetingMessage = greeting || defaultGreeting;
|
|
368
|
+
|
|
369
|
+
let messageResult;
|
|
370
|
+
try {
|
|
371
|
+
messageResult = await sendMessage(client, chatId, greetingMessage);
|
|
372
|
+
} catch (err: any) {
|
|
373
|
+
// Even if message fails, the chat was created successfully
|
|
374
|
+
return {
|
|
375
|
+
success: true,
|
|
376
|
+
chat_id: chatId,
|
|
377
|
+
create_result: createResult,
|
|
378
|
+
message_error: err?.message || "Failed to send greeting message",
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
success: true,
|
|
384
|
+
chat_id: chatId,
|
|
385
|
+
create_result: createResult,
|
|
386
|
+
message_result: messageResult,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async function deleteChat(client: ChatClient, chatId: string) {
|
|
391
|
+
const res = await runChatApiCall("im.chat.delete", () =>
|
|
392
|
+
(client as any).im.chat.delete({
|
|
393
|
+
path: { chat_id: chatId },
|
|
394
|
+
}),
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
return {
|
|
398
|
+
success: true,
|
|
399
|
+
chat_id: chatId,
|
|
400
|
+
message: "Chat has been successfully disbanded/deleted",
|
|
401
|
+
...(res as any).data,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Main action handler - MUST BE EXPORTED
|
|
406
|
+
export async function runChatAction(client: ChatClient, params: FeishuChatParams) {
|
|
407
|
+
switch (params.action) {
|
|
408
|
+
case "get_announcement_info":
|
|
409
|
+
case "get_announcement":
|
|
410
|
+
return getAnnouncement(client, requireString(params.chat_id, "chat_id"));
|
|
411
|
+
case "list_announcement_blocks":
|
|
412
|
+
return listAnnouncementBlocks(client, requireString(params.chat_id, "chat_id"));
|
|
413
|
+
case "get_announcement_block":
|
|
414
|
+
return getAnnouncementBlock(
|
|
415
|
+
client,
|
|
416
|
+
requireString(params.chat_id, "chat_id"),
|
|
417
|
+
requireString(params.block_id, "block_id"),
|
|
418
|
+
);
|
|
419
|
+
case "write_announcement": {
|
|
420
|
+
const chatId = requireString(params.chat_id, "chat_id");
|
|
421
|
+
const content = requireString(params.content, "content");
|
|
422
|
+
const current = await getAnnouncement(client, chatId);
|
|
423
|
+
if (current.announcement_type === "doc") {
|
|
424
|
+
return writeDocAnnouncement(client, chatId, content);
|
|
425
|
+
} else {
|
|
426
|
+
// For docx announcements, append a text block under the Page root block.
|
|
427
|
+
// Full replacement is not supported via API; use update_announcement_block to edit existing blocks.
|
|
428
|
+
const blocks: any[] = (current as any).blocks ?? [];
|
|
429
|
+
const pageBlock = blocks.find((b: any) => b.block_type === 1);
|
|
430
|
+
if (!pageBlock?.block_id) {
|
|
431
|
+
return { error: "Could not find the Page root block for docx announcement. Use list_announcement_blocks to inspect the structure." };
|
|
432
|
+
}
|
|
433
|
+
return createTextBlock(client, chatId, pageBlock.block_id, content);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
case "append_announcement": {
|
|
437
|
+
const chatId = requireString(params.chat_id, "chat_id");
|
|
438
|
+
const content = requireString(params.content, "content");
|
|
439
|
+
const current = await getAnnouncement(client, chatId);
|
|
440
|
+
if (current.announcement_type === "doc") {
|
|
441
|
+
const existingContent = (current as any).content || "";
|
|
442
|
+
const newContent = existingContent + "\n" + content;
|
|
443
|
+
return writeDocAnnouncement(client, chatId, newContent);
|
|
444
|
+
} else {
|
|
445
|
+
// For docx format, the parent block must be the Page root block (block_type: 1)
|
|
446
|
+
const blocks: any[] = (current as any).blocks ?? [];
|
|
447
|
+
const pageBlock = blocks.find((b: any) => b.block_type === 1);
|
|
448
|
+
if (!pageBlock?.block_id) {
|
|
449
|
+
return { error: "Could not find the Page root block for docx announcement. Use list_announcement_blocks to inspect the structure." };
|
|
450
|
+
}
|
|
451
|
+
return createTextBlock(client, chatId, pageBlock.block_id, content);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
case "update_announcement_block": {
|
|
455
|
+
const requests = [
|
|
456
|
+
{
|
|
457
|
+
block_id: requireString(params.block_id, "block_id"),
|
|
458
|
+
update_text_elements: {
|
|
459
|
+
elements: [{ text_run: { content: requireString(params.content, "content") } }],
|
|
460
|
+
},
|
|
461
|
+
},
|
|
462
|
+
];
|
|
463
|
+
return batchUpdateAnnouncementBlocks(client, requireString(params.chat_id, "chat_id"), requests);
|
|
464
|
+
}
|
|
465
|
+
// ============== New Chat Management Actions ==============
|
|
466
|
+
case "create_chat": {
|
|
467
|
+
return createChat(
|
|
468
|
+
client,
|
|
469
|
+
requireString(params.name, "name"),
|
|
470
|
+
params.user_ids,
|
|
471
|
+
params.description,
|
|
472
|
+
params.user_id_type,
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
case "add_members": {
|
|
476
|
+
return addMembers(
|
|
477
|
+
client,
|
|
478
|
+
requireString(params.chat_id, "chat_id"),
|
|
479
|
+
requireStringArray(params.user_ids, "user_ids"),
|
|
480
|
+
params.member_id_type,
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
case "check_bot_in_chat": {
|
|
484
|
+
return checkBotInChat(client, requireString(params.chat_id, "chat_id"));
|
|
485
|
+
}
|
|
486
|
+
case "delete_chat": {
|
|
487
|
+
return deleteChat(client, requireString(params.chat_id, "chat_id"));
|
|
488
|
+
}
|
|
489
|
+
case "create_session_chat": {
|
|
490
|
+
return createSessionChat(
|
|
491
|
+
client,
|
|
492
|
+
requireString(params.name, "name"),
|
|
493
|
+
requireStringArray(params.user_ids, "user_ids"),
|
|
494
|
+
params.greeting,
|
|
495
|
+
params.description,
|
|
496
|
+
params.user_id_type,
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
default:
|
|
500
|
+
return { error: `Unknown action: ${(params as any).action}` };
|
|
501
|
+
}
|
|
502
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createFeishuClient } from "../client.js";
|
|
2
|
+
import { errorResult, json, runFeishuApiCall, type FeishuApiResponse } from "../tools-common/feishu-api.js";
|
|
3
|
+
|
|
4
|
+
export type ChatClient = ReturnType<typeof createFeishuClient>;
|
|
5
|
+
|
|
6
|
+
export { json, errorResult };
|
|
7
|
+
|
|
8
|
+
export async function runChatApiCall<T extends FeishuApiResponse>(
|
|
9
|
+
context: string,
|
|
10
|
+
fn: () => Promise<T>,
|
|
11
|
+
): Promise<T> {
|
|
12
|
+
return runFeishuApiCall(context, fn);
|
|
13
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
+
import type { ResolvedFeishuAccount } from "../types.js";
|
|
3
|
+
import { hasFeishuToolEnabledForAnyAccount, withFeishuToolClient } from "../tools-common/tool-exec.js";
|
|
4
|
+
import { runChatAction } from "./actions.js";
|
|
5
|
+
import { errorResult, json, type ChatClient } from "./common.js";
|
|
6
|
+
import { FeishuChatSchema, type FeishuChatParams } from "./schemas.js";
|
|
7
|
+
|
|
8
|
+
type ChatToolSpec<P> = {
|
|
9
|
+
name: string;
|
|
10
|
+
label: string;
|
|
11
|
+
description: string;
|
|
12
|
+
parameters: any;
|
|
13
|
+
requiredTool?: "chat";
|
|
14
|
+
run: (args: { client: ChatClient; account: ResolvedFeishuAccount }, params: P) => Promise<unknown>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function registerChatTool<P>(api: OpenClawPluginApi, spec: ChatToolSpec<P>) {
|
|
18
|
+
api.registerTool(
|
|
19
|
+
{
|
|
20
|
+
name: spec.name,
|
|
21
|
+
label: spec.label,
|
|
22
|
+
description: spec.description,
|
|
23
|
+
parameters: spec.parameters,
|
|
24
|
+
async execute(_toolCallId, params) {
|
|
25
|
+
try {
|
|
26
|
+
return await withFeishuToolClient({
|
|
27
|
+
api,
|
|
28
|
+
toolName: spec.name,
|
|
29
|
+
requiredTool: spec.requiredTool,
|
|
30
|
+
run: async ({ client, account }) =>
|
|
31
|
+
json(await spec.run({ client: client as ChatClient, account }, params as P)),
|
|
32
|
+
});
|
|
33
|
+
} catch (err) {
|
|
34
|
+
return errorResult(err);
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{ name: spec.name },
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function registerFeishuChatTools(api: OpenClawPluginApi) {
|
|
43
|
+
if (!api.config) {
|
|
44
|
+
api.logger.debug?.("feishu_chat: No config available, skipping chat tools");
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!hasFeishuToolEnabledForAnyAccount(api.config)) {
|
|
49
|
+
api.logger.debug?.("feishu_chat: No Feishu accounts configured, skipping chat tools");
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const chatEnabled = hasFeishuToolEnabledForAnyAccount(api.config, "chat");
|
|
54
|
+
const registered: string[] = [];
|
|
55
|
+
|
|
56
|
+
if (chatEnabled) {
|
|
57
|
+
registerChatTool<FeishuChatParams>(api, {
|
|
58
|
+
name: "feishu_chat",
|
|
59
|
+
label: "Feishu Chat",
|
|
60
|
+
description:
|
|
61
|
+
"Feishu chat operations. Actions: get_announcement, get_announcement_info, list_announcement_blocks, get_announcement_block, write_announcement, append_announcement, update_announcement_block, create_chat, add_members, check_bot_in_chat, create_session_chat, delete_chat. Use to manage group chats and announcements.",
|
|
62
|
+
parameters: FeishuChatSchema,
|
|
63
|
+
requiredTool: "chat",
|
|
64
|
+
run: async ({ client }, params) => runChatAction(client, params),
|
|
65
|
+
});
|
|
66
|
+
registered.push("feishu_chat");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (registered.length > 0) {
|
|
70
|
+
api.logger.debug?.(`feishu_chat: Registered ${registered.join(", ")}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
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 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
|
+
});
|
|
60
|
+
|
|
61
|
+
export type FeishuChatParams = Static<typeof FeishuChatSchema>;
|
package/src/client.ts
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import * as Lark from "@larksuiteoapi/node-sdk";
|
|
2
|
+
import { HttpsProxyAgent } from "https-proxy-agent";
|
|
2
3
|
import type { FeishuDomain, ResolvedFeishuAccount } from "./types.js";
|
|
3
4
|
|
|
5
|
+
function getWsProxyAgent(): HttpsProxyAgent<string> | undefined {
|
|
6
|
+
const proxyUrl =
|
|
7
|
+
process.env.https_proxy ||
|
|
8
|
+
process.env.HTTPS_PROXY ||
|
|
9
|
+
process.env.http_proxy ||
|
|
10
|
+
process.env.HTTP_PROXY;
|
|
11
|
+
if (!proxyUrl) return undefined;
|
|
12
|
+
return new HttpsProxyAgent(proxyUrl);
|
|
13
|
+
}
|
|
14
|
+
|
|
4
15
|
// Multi-account client cache
|
|
5
16
|
const clientCache = new Map<
|
|
6
17
|
string,
|
|
@@ -77,11 +88,13 @@ export function createFeishuWSClient(account: ResolvedFeishuAccount): Lark.WSCli
|
|
|
77
88
|
throw new Error(`Feishu credentials not configured for account "${accountId}"`);
|
|
78
89
|
}
|
|
79
90
|
|
|
91
|
+
const agent = getWsProxyAgent();
|
|
80
92
|
return new Lark.WSClient({
|
|
81
93
|
appId,
|
|
82
94
|
appSecret,
|
|
83
95
|
domain: resolveDomain(domain),
|
|
84
96
|
loggerLevel: Lark.LoggerLevel.info,
|
|
97
|
+
...(agent ? { agent } : {}),
|
|
85
98
|
});
|
|
86
99
|
}
|
|
87
100
|
|