@m1heng-clawd/feishu 0.1.7 → 0.1.9
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 +180 -8
- package/package.json +3 -3
- package/src/accounts.ts +100 -13
- package/src/bot.ts +124 -28
- package/src/channel.ts +163 -53
- package/src/client.ts +83 -35
- package/src/config-schema.ts +73 -1
- package/src/directory.ts +15 -9
- package/src/docx.ts +16 -6
- package/src/drive.ts +12 -6
- package/src/dynamic-agent.ts +131 -0
- package/src/media.ts +43 -35
- package/src/monitor.ts +215 -70
- package/src/outbound.ts +7 -7
- package/src/perm.ts +12 -6
- package/src/probe.ts +6 -9
- package/src/reactions.ts +19 -16
- package/src/reply-dispatcher.ts +19 -11
- package/src/send.ts +49 -43
- package/src/types.ts +16 -2
- package/src/typing.ts +11 -9
- package/src/wiki.ts +12 -6
package/src/media.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ClawdbotConfig } from "openclaw/plugin-sdk";
|
|
2
|
-
import type { FeishuConfig } from "./types.js";
|
|
3
2
|
import { createFeishuClient } from "./client.js";
|
|
3
|
+
import { resolveFeishuAccount } from "./accounts.js";
|
|
4
4
|
import { resolveReceiveIdType, normalizeFeishuTarget } from "./targets.js";
|
|
5
5
|
import fs from "fs";
|
|
6
6
|
import path from "path";
|
|
@@ -25,14 +25,15 @@ export type DownloadMessageResourceResult = {
|
|
|
25
25
|
export async function downloadImageFeishu(params: {
|
|
26
26
|
cfg: ClawdbotConfig;
|
|
27
27
|
imageKey: string;
|
|
28
|
+
accountId?: string;
|
|
28
29
|
}): Promise<DownloadImageResult> {
|
|
29
|
-
const { cfg, imageKey } = params;
|
|
30
|
-
const
|
|
31
|
-
if (!
|
|
32
|
-
throw new Error(
|
|
30
|
+
const { cfg, imageKey, accountId } = params;
|
|
31
|
+
const account = resolveFeishuAccount({ cfg, accountId });
|
|
32
|
+
if (!account.configured) {
|
|
33
|
+
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
const client = createFeishuClient(
|
|
36
|
+
const client = createFeishuClient(account);
|
|
36
37
|
|
|
37
38
|
const response = await client.im.image.get({
|
|
38
39
|
path: { image_key: imageKey },
|
|
@@ -103,14 +104,15 @@ export async function downloadMessageResourceFeishu(params: {
|
|
|
103
104
|
messageId: string;
|
|
104
105
|
fileKey: string;
|
|
105
106
|
type: "image" | "file";
|
|
107
|
+
accountId?: string;
|
|
106
108
|
}): Promise<DownloadMessageResourceResult> {
|
|
107
|
-
const { cfg, messageId, fileKey, type } = params;
|
|
108
|
-
const
|
|
109
|
-
if (!
|
|
110
|
-
throw new Error(
|
|
109
|
+
const { cfg, messageId, fileKey, type, accountId } = params;
|
|
110
|
+
const account = resolveFeishuAccount({ cfg, accountId });
|
|
111
|
+
if (!account.configured) {
|
|
112
|
+
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
|
111
113
|
}
|
|
112
114
|
|
|
113
|
-
const client = createFeishuClient(
|
|
115
|
+
const client = createFeishuClient(account);
|
|
114
116
|
|
|
115
117
|
const response = await client.im.messageResource.get({
|
|
116
118
|
path: { message_id: messageId, file_key: fileKey },
|
|
@@ -196,14 +198,15 @@ export async function uploadImageFeishu(params: {
|
|
|
196
198
|
cfg: ClawdbotConfig;
|
|
197
199
|
image: Buffer | string; // Buffer or file path
|
|
198
200
|
imageType?: "message" | "avatar";
|
|
201
|
+
accountId?: string;
|
|
199
202
|
}): Promise<UploadImageResult> {
|
|
200
|
-
const { cfg, image, imageType = "message" } = params;
|
|
201
|
-
const
|
|
202
|
-
if (!
|
|
203
|
-
throw new Error(
|
|
203
|
+
const { cfg, image, imageType = "message", accountId } = params;
|
|
204
|
+
const account = resolveFeishuAccount({ cfg, accountId });
|
|
205
|
+
if (!account.configured) {
|
|
206
|
+
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
|
204
207
|
}
|
|
205
208
|
|
|
206
|
-
const client = createFeishuClient(
|
|
209
|
+
const client = createFeishuClient(account);
|
|
207
210
|
|
|
208
211
|
// SDK expects a Readable stream, not a Buffer
|
|
209
212
|
// Use type assertion since SDK actually accepts any Readable at runtime
|
|
@@ -242,14 +245,15 @@ export async function uploadFileFeishu(params: {
|
|
|
242
245
|
fileName: string;
|
|
243
246
|
fileType: "opus" | "mp4" | "pdf" | "doc" | "xls" | "ppt" | "stream";
|
|
244
247
|
duration?: number; // Required for audio/video files, in milliseconds
|
|
248
|
+
accountId?: string;
|
|
245
249
|
}): Promise<UploadFileResult> {
|
|
246
|
-
const { cfg, file, fileName, fileType, duration } = params;
|
|
247
|
-
const
|
|
248
|
-
if (!
|
|
249
|
-
throw new Error(
|
|
250
|
+
const { cfg, file, fileName, fileType, duration, accountId } = params;
|
|
251
|
+
const account = resolveFeishuAccount({ cfg, accountId });
|
|
252
|
+
if (!account.configured) {
|
|
253
|
+
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
|
250
254
|
}
|
|
251
255
|
|
|
252
|
-
const client = createFeishuClient(
|
|
256
|
+
const client = createFeishuClient(account);
|
|
253
257
|
|
|
254
258
|
// SDK expects a Readable stream, not a Buffer
|
|
255
259
|
// Use type assertion since SDK actually accepts any Readable at runtime
|
|
@@ -287,14 +291,15 @@ export async function sendImageFeishu(params: {
|
|
|
287
291
|
to: string;
|
|
288
292
|
imageKey: string;
|
|
289
293
|
replyToMessageId?: string;
|
|
294
|
+
accountId?: string;
|
|
290
295
|
}): Promise<SendMediaResult> {
|
|
291
|
-
const { cfg, to, imageKey, replyToMessageId } = params;
|
|
292
|
-
const
|
|
293
|
-
if (!
|
|
294
|
-
throw new Error(
|
|
296
|
+
const { cfg, to, imageKey, replyToMessageId, accountId } = params;
|
|
297
|
+
const account = resolveFeishuAccount({ cfg, accountId });
|
|
298
|
+
if (!account.configured) {
|
|
299
|
+
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
|
295
300
|
}
|
|
296
301
|
|
|
297
|
-
const client = createFeishuClient(
|
|
302
|
+
const client = createFeishuClient(account);
|
|
298
303
|
const receiveId = normalizeFeishuTarget(to);
|
|
299
304
|
if (!receiveId) {
|
|
300
305
|
throw new Error(`Invalid Feishu target: ${to}`);
|
|
@@ -349,14 +354,15 @@ export async function sendFileFeishu(params: {
|
|
|
349
354
|
to: string;
|
|
350
355
|
fileKey: string;
|
|
351
356
|
replyToMessageId?: string;
|
|
357
|
+
accountId?: string;
|
|
352
358
|
}): Promise<SendMediaResult> {
|
|
353
|
-
const { cfg, to, fileKey, replyToMessageId } = params;
|
|
354
|
-
const
|
|
355
|
-
if (!
|
|
356
|
-
throw new Error(
|
|
359
|
+
const { cfg, to, fileKey, replyToMessageId, accountId } = params;
|
|
360
|
+
const account = resolveFeishuAccount({ cfg, accountId });
|
|
361
|
+
if (!account.configured) {
|
|
362
|
+
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
|
357
363
|
}
|
|
358
364
|
|
|
359
|
-
const client = createFeishuClient(
|
|
365
|
+
const client = createFeishuClient(account);
|
|
360
366
|
const receiveId = normalizeFeishuTarget(to);
|
|
361
367
|
if (!receiveId) {
|
|
362
368
|
throw new Error(`Invalid Feishu target: ${to}`);
|
|
@@ -461,8 +467,9 @@ export async function sendMediaFeishu(params: {
|
|
|
461
467
|
mediaBuffer?: Buffer;
|
|
462
468
|
fileName?: string;
|
|
463
469
|
replyToMessageId?: string;
|
|
470
|
+
accountId?: string;
|
|
464
471
|
}): Promise<SendMediaResult> {
|
|
465
|
-
const { cfg, to, mediaUrl, mediaBuffer, fileName, replyToMessageId } = params;
|
|
472
|
+
const { cfg, to, mediaUrl, mediaBuffer, fileName, replyToMessageId, accountId } = params;
|
|
466
473
|
|
|
467
474
|
let buffer: Buffer;
|
|
468
475
|
let name: string;
|
|
@@ -500,8 +507,8 @@ export async function sendMediaFeishu(params: {
|
|
|
500
507
|
const isImage = [".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".ico", ".tiff"].includes(ext);
|
|
501
508
|
|
|
502
509
|
if (isImage) {
|
|
503
|
-
const { imageKey } = await uploadImageFeishu({ cfg, image: buffer });
|
|
504
|
-
return sendImageFeishu({ cfg, to, imageKey, replyToMessageId });
|
|
510
|
+
const { imageKey } = await uploadImageFeishu({ cfg, image: buffer, accountId });
|
|
511
|
+
return sendImageFeishu({ cfg, to, imageKey, replyToMessageId, accountId });
|
|
505
512
|
} else {
|
|
506
513
|
const fileType = detectFileType(name);
|
|
507
514
|
const { fileKey } = await uploadFileFeishu({
|
|
@@ -509,7 +516,8 @@ export async function sendMediaFeishu(params: {
|
|
|
509
516
|
file: buffer,
|
|
510
517
|
fileName: name,
|
|
511
518
|
fileType,
|
|
519
|
+
accountId,
|
|
512
520
|
});
|
|
513
|
-
return sendFileFeishu({ cfg, to, fileKey, replyToMessageId });
|
|
521
|
+
return sendFileFeishu({ cfg, to, fileKey, replyToMessageId, accountId });
|
|
514
522
|
}
|
|
515
523
|
}
|
package/src/monitor.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import * as Lark from "@larksuiteoapi/node-sdk";
|
|
2
|
+
import * as http from "http";
|
|
2
3
|
import type { ClawdbotConfig, RuntimeEnv, HistoryEntry } from "openclaw/plugin-sdk";
|
|
3
|
-
import type {
|
|
4
|
+
import type { ResolvedFeishuAccount } from "./types.js";
|
|
4
5
|
import { createFeishuWSClient, createEventDispatcher } from "./client.js";
|
|
5
|
-
import {
|
|
6
|
+
import { resolveFeishuAccount, listEnabledFeishuAccounts } from "./accounts.js";
|
|
6
7
|
import { handleFeishuMessage, type FeishuMessageEvent, type FeishuBotAddedEvent } from "./bot.js";
|
|
7
8
|
import { probeFeishu } from "./probe.js";
|
|
8
9
|
|
|
@@ -13,79 +14,62 @@ export type MonitorFeishuOpts = {
|
|
|
13
14
|
accountId?: string;
|
|
14
15
|
};
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
// Per-account WebSocket clients, HTTP servers, and bot info
|
|
18
|
+
const wsClients = new Map<string, Lark.WSClient>();
|
|
19
|
+
const httpServers = new Map<string, http.Server>();
|
|
20
|
+
const botOpenIds = new Map<string, string>();
|
|
18
21
|
|
|
19
|
-
async function fetchBotOpenId(
|
|
22
|
+
async function fetchBotOpenId(
|
|
23
|
+
account: ResolvedFeishuAccount,
|
|
24
|
+
): Promise<string | undefined> {
|
|
20
25
|
try {
|
|
21
|
-
const result = await probeFeishu(
|
|
26
|
+
const result = await probeFeishu(account);
|
|
22
27
|
return result.ok ? result.botOpenId : undefined;
|
|
23
28
|
} catch {
|
|
24
29
|
return undefined;
|
|
25
30
|
}
|
|
26
31
|
}
|
|
27
32
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
botOpenId = await fetchBotOpenId(feishuCfg);
|
|
45
|
-
log(`feishu: bot open_id resolved: ${botOpenId ?? "unknown"}`);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const connectionMode = feishuCfg?.connectionMode ?? "websocket";
|
|
49
|
-
|
|
50
|
-
if (connectionMode === "websocket") {
|
|
51
|
-
return monitorWebSocket({ cfg, feishuCfg: feishuCfg!, runtime: opts.runtime, abortSignal: opts.abortSignal });
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
log("feishu: webhook mode not implemented in monitor, use HTTP server directly");
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async function monitorWebSocket(params: {
|
|
58
|
-
cfg: ClawdbotConfig;
|
|
59
|
-
feishuCfg: FeishuConfig;
|
|
60
|
-
runtime?: RuntimeEnv;
|
|
61
|
-
abortSignal?: AbortSignal;
|
|
62
|
-
}): Promise<void> {
|
|
63
|
-
const { cfg, feishuCfg, runtime, abortSignal } = params;
|
|
33
|
+
/**
|
|
34
|
+
* Register common event handlers on an EventDispatcher.
|
|
35
|
+
* When fireAndForget is true (webhook mode), message handling is not awaited
|
|
36
|
+
* to avoid blocking the HTTP response (Lark requires <3s response).
|
|
37
|
+
*/
|
|
38
|
+
function registerEventHandlers(
|
|
39
|
+
eventDispatcher: Lark.EventDispatcher,
|
|
40
|
+
context: {
|
|
41
|
+
cfg: ClawdbotConfig;
|
|
42
|
+
accountId: string;
|
|
43
|
+
runtime?: RuntimeEnv;
|
|
44
|
+
chatHistories: Map<string, HistoryEntry[]>;
|
|
45
|
+
fireAndForget?: boolean;
|
|
46
|
+
},
|
|
47
|
+
) {
|
|
48
|
+
const { cfg, accountId, runtime, chatHistories, fireAndForget } = context;
|
|
64
49
|
const log = runtime?.log ?? console.log;
|
|
65
50
|
const error = runtime?.error ?? console.error;
|
|
66
51
|
|
|
67
|
-
log("feishu: starting WebSocket connection...");
|
|
68
|
-
|
|
69
|
-
const wsClient = createFeishuWSClient(feishuCfg);
|
|
70
|
-
currentWsClient = wsClient;
|
|
71
|
-
|
|
72
|
-
const chatHistories = new Map<string, HistoryEntry[]>();
|
|
73
|
-
|
|
74
|
-
const eventDispatcher = createEventDispatcher(feishuCfg);
|
|
75
|
-
|
|
76
52
|
eventDispatcher.register({
|
|
77
53
|
"im.message.receive_v1": async (data) => {
|
|
78
54
|
try {
|
|
79
55
|
const event = data as unknown as FeishuMessageEvent;
|
|
80
|
-
|
|
56
|
+
const promise = handleFeishuMessage({
|
|
81
57
|
cfg,
|
|
82
58
|
event,
|
|
83
|
-
botOpenId,
|
|
59
|
+
botOpenId: botOpenIds.get(accountId),
|
|
84
60
|
runtime,
|
|
85
61
|
chatHistories,
|
|
62
|
+
accountId,
|
|
86
63
|
});
|
|
64
|
+
if (fireAndForget) {
|
|
65
|
+
promise.catch((err) => {
|
|
66
|
+
error(`feishu[${accountId}]: error handling message: ${String(err)}`);
|
|
67
|
+
});
|
|
68
|
+
} else {
|
|
69
|
+
await promise;
|
|
70
|
+
}
|
|
87
71
|
} catch (err) {
|
|
88
|
-
error(`feishu: error handling message
|
|
72
|
+
error(`feishu[${accountId}]: error handling message: ${String(err)}`);
|
|
89
73
|
}
|
|
90
74
|
},
|
|
91
75
|
"im.message.message_read_v1": async () => {
|
|
@@ -94,30 +78,85 @@ async function monitorWebSocket(params: {
|
|
|
94
78
|
"im.chat.member.bot.added_v1": async (data) => {
|
|
95
79
|
try {
|
|
96
80
|
const event = data as unknown as FeishuBotAddedEvent;
|
|
97
|
-
log(`feishu: bot added to chat ${event.chat_id}`);
|
|
81
|
+
log(`feishu[${accountId}]: bot added to chat ${event.chat_id}`);
|
|
98
82
|
} catch (err) {
|
|
99
|
-
error(`feishu: error handling bot added event: ${String(err)}`);
|
|
83
|
+
error(`feishu[${accountId}]: error handling bot added event: ${String(err)}`);
|
|
100
84
|
}
|
|
101
85
|
},
|
|
102
86
|
"im.chat.member.bot.deleted_v1": async (data) => {
|
|
103
87
|
try {
|
|
104
88
|
const event = data as unknown as { chat_id: string };
|
|
105
|
-
log(`feishu: bot removed from chat ${event.chat_id}`);
|
|
89
|
+
log(`feishu[${accountId}]: bot removed from chat ${event.chat_id}`);
|
|
106
90
|
} catch (err) {
|
|
107
|
-
error(`feishu: error handling bot removed event: ${String(err)}`);
|
|
91
|
+
error(`feishu[${accountId}]: error handling bot removed event: ${String(err)}`);
|
|
108
92
|
}
|
|
109
93
|
},
|
|
110
94
|
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
type MonitorAccountParams = {
|
|
98
|
+
cfg: ClawdbotConfig;
|
|
99
|
+
account: ResolvedFeishuAccount;
|
|
100
|
+
runtime?: RuntimeEnv;
|
|
101
|
+
abortSignal?: AbortSignal;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Monitor a single Feishu account.
|
|
106
|
+
*/
|
|
107
|
+
async function monitorSingleAccount(params: MonitorAccountParams): Promise<void> {
|
|
108
|
+
const { cfg, account, runtime, abortSignal } = params;
|
|
109
|
+
const { accountId } = account;
|
|
110
|
+
const log = runtime?.log ?? console.log;
|
|
111
|
+
|
|
112
|
+
// Fetch bot open_id
|
|
113
|
+
const botOpenId = await fetchBotOpenId(account);
|
|
114
|
+
botOpenIds.set(accountId, botOpenId ?? "");
|
|
115
|
+
log(`feishu[${accountId}]: bot open_id resolved: ${botOpenId ?? "unknown"}`);
|
|
116
|
+
|
|
117
|
+
const connectionMode = account.config.connectionMode ?? "websocket";
|
|
118
|
+
const eventDispatcher = createEventDispatcher(account);
|
|
119
|
+
const chatHistories = new Map<string, HistoryEntry[]>();
|
|
120
|
+
|
|
121
|
+
registerEventHandlers(eventDispatcher, {
|
|
122
|
+
cfg,
|
|
123
|
+
accountId,
|
|
124
|
+
runtime,
|
|
125
|
+
chatHistories,
|
|
126
|
+
fireAndForget: connectionMode === "webhook",
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
if (connectionMode === "webhook") {
|
|
130
|
+
return monitorWebhook({ params, accountId, eventDispatcher });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return monitorWebSocket({ params, accountId, eventDispatcher });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
type ConnectionParams = {
|
|
137
|
+
params: MonitorAccountParams;
|
|
138
|
+
accountId: string;
|
|
139
|
+
eventDispatcher: Lark.EventDispatcher;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
async function monitorWebSocket({ params, accountId, eventDispatcher }: ConnectionParams): Promise<void> {
|
|
143
|
+
const { account, runtime, abortSignal } = params;
|
|
144
|
+
const log = runtime?.log ?? console.log;
|
|
145
|
+
const error = runtime?.error ?? console.error;
|
|
146
|
+
|
|
147
|
+
log(`feishu[${accountId}]: starting WebSocket connection...`);
|
|
148
|
+
|
|
149
|
+
const wsClient = createFeishuWSClient(account);
|
|
150
|
+
wsClients.set(accountId, wsClient);
|
|
111
151
|
|
|
112
152
|
return new Promise((resolve, reject) => {
|
|
113
153
|
const cleanup = () => {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
154
|
+
wsClients.delete(accountId);
|
|
155
|
+
botOpenIds.delete(accountId);
|
|
117
156
|
};
|
|
118
157
|
|
|
119
158
|
const handleAbort = () => {
|
|
120
|
-
log(
|
|
159
|
+
log(`feishu[${accountId}]: abort signal received, stopping`);
|
|
121
160
|
cleanup();
|
|
122
161
|
resolve();
|
|
123
162
|
};
|
|
@@ -131,11 +170,8 @@ async function monitorWebSocket(params: {
|
|
|
131
170
|
abortSignal?.addEventListener("abort", handleAbort, { once: true });
|
|
132
171
|
|
|
133
172
|
try {
|
|
134
|
-
wsClient.start({
|
|
135
|
-
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
log("feishu: WebSocket client started");
|
|
173
|
+
wsClient.start({ eventDispatcher });
|
|
174
|
+
log(`feishu[${accountId}]: WebSocket client started`);
|
|
139
175
|
} catch (err) {
|
|
140
176
|
cleanup();
|
|
141
177
|
abortSignal?.removeEventListener("abort", handleAbort);
|
|
@@ -144,8 +180,117 @@ async function monitorWebSocket(params: {
|
|
|
144
180
|
});
|
|
145
181
|
}
|
|
146
182
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
183
|
+
async function monitorWebhook({ params, accountId, eventDispatcher }: ConnectionParams): Promise<void> {
|
|
184
|
+
const { account, runtime, abortSignal } = params;
|
|
185
|
+
const log = runtime?.log ?? console.log;
|
|
186
|
+
const error = runtime?.error ?? console.error;
|
|
187
|
+
|
|
188
|
+
const port = account.config.webhookPort ?? 3000;
|
|
189
|
+
const path = account.config.webhookPath ?? "/feishu/events";
|
|
190
|
+
|
|
191
|
+
log(`feishu[${accountId}]: starting Webhook server on port ${port}, path ${path}...`);
|
|
192
|
+
|
|
193
|
+
const server = http.createServer();
|
|
194
|
+
server.on("request", Lark.adaptDefault(path, eventDispatcher, { autoChallenge: true }));
|
|
195
|
+
httpServers.set(accountId, server);
|
|
196
|
+
|
|
197
|
+
return new Promise((resolve, reject) => {
|
|
198
|
+
const cleanup = () => {
|
|
199
|
+
server.close();
|
|
200
|
+
httpServers.delete(accountId);
|
|
201
|
+
botOpenIds.delete(accountId);
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const handleAbort = () => {
|
|
205
|
+
log(`feishu[${accountId}]: abort signal received, stopping Webhook server`);
|
|
206
|
+
cleanup();
|
|
207
|
+
resolve();
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
if (abortSignal?.aborted) {
|
|
211
|
+
cleanup();
|
|
212
|
+
resolve();
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
abortSignal?.addEventListener("abort", handleAbort, { once: true });
|
|
217
|
+
|
|
218
|
+
server.listen(port, () => {
|
|
219
|
+
log(`feishu[${accountId}]: Webhook server listening on port ${port}`);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
server.on("error", (err) => {
|
|
223
|
+
error(`feishu[${accountId}]: Webhook server error: ${err}`);
|
|
224
|
+
abortSignal?.removeEventListener("abort", handleAbort);
|
|
225
|
+
reject(err);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Main entry: start monitoring for all enabled accounts.
|
|
232
|
+
*/
|
|
233
|
+
export async function monitorFeishuProvider(opts: MonitorFeishuOpts = {}): Promise<void> {
|
|
234
|
+
const cfg = opts.config;
|
|
235
|
+
if (!cfg) {
|
|
236
|
+
throw new Error("Config is required for Feishu monitor");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const log = opts.runtime?.log ?? console.log;
|
|
240
|
+
|
|
241
|
+
// If accountId is specified, only monitor that account
|
|
242
|
+
if (opts.accountId) {
|
|
243
|
+
const account = resolveFeishuAccount({ cfg, accountId: opts.accountId });
|
|
244
|
+
if (!account.enabled || !account.configured) {
|
|
245
|
+
throw new Error(`Feishu account "${opts.accountId}" not configured or disabled`);
|
|
246
|
+
}
|
|
247
|
+
return monitorSingleAccount({
|
|
248
|
+
cfg,
|
|
249
|
+
account,
|
|
250
|
+
runtime: opts.runtime,
|
|
251
|
+
abortSignal: opts.abortSignal,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Otherwise, start all enabled accounts
|
|
256
|
+
const accounts = listEnabledFeishuAccounts(cfg);
|
|
257
|
+
if (accounts.length === 0) {
|
|
258
|
+
throw new Error("No enabled Feishu accounts configured");
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
log(`feishu: starting ${accounts.length} account(s): ${accounts.map((a) => a.accountId).join(", ")}`);
|
|
262
|
+
|
|
263
|
+
// Start all accounts in parallel
|
|
264
|
+
await Promise.all(
|
|
265
|
+
accounts.map((account) =>
|
|
266
|
+
monitorSingleAccount({
|
|
267
|
+
cfg,
|
|
268
|
+
account,
|
|
269
|
+
runtime: opts.runtime,
|
|
270
|
+
abortSignal: opts.abortSignal,
|
|
271
|
+
}),
|
|
272
|
+
),
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Stop monitoring for a specific account or all accounts.
|
|
278
|
+
*/
|
|
279
|
+
export function stopFeishuMonitor(accountId?: string): void {
|
|
280
|
+
if (accountId) {
|
|
281
|
+
wsClients.delete(accountId);
|
|
282
|
+
const server = httpServers.get(accountId);
|
|
283
|
+
if (server) {
|
|
284
|
+
server.close();
|
|
285
|
+
httpServers.delete(accountId);
|
|
286
|
+
}
|
|
287
|
+
botOpenIds.delete(accountId);
|
|
288
|
+
} else {
|
|
289
|
+
wsClients.clear();
|
|
290
|
+
for (const server of httpServers.values()) {
|
|
291
|
+
server.close();
|
|
292
|
+
}
|
|
293
|
+
httpServers.clear();
|
|
294
|
+
botOpenIds.clear();
|
|
150
295
|
}
|
|
151
296
|
}
|
package/src/outbound.ts
CHANGED
|
@@ -8,33 +8,33 @@ export const feishuOutbound: ChannelOutboundAdapter = {
|
|
|
8
8
|
chunker: (text, limit) => getFeishuRuntime().channel.text.chunkMarkdownText(text, limit),
|
|
9
9
|
chunkerMode: "markdown",
|
|
10
10
|
textChunkLimit: 4000,
|
|
11
|
-
sendText: async ({ cfg, to, text }) => {
|
|
12
|
-
const result = await sendMessageFeishu({ cfg, to, text });
|
|
11
|
+
sendText: async ({ cfg, to, text, accountId }) => {
|
|
12
|
+
const result = await sendMessageFeishu({ cfg, to, text, accountId });
|
|
13
13
|
return { channel: "feishu", ...result };
|
|
14
14
|
},
|
|
15
|
-
sendMedia: async ({ cfg, to, text, mediaUrl }) => {
|
|
15
|
+
sendMedia: async ({ cfg, to, text, mediaUrl, accountId }) => {
|
|
16
16
|
// Send text first if provided
|
|
17
17
|
if (text?.trim()) {
|
|
18
|
-
await sendMessageFeishu({ cfg, to, text });
|
|
18
|
+
await sendMessageFeishu({ cfg, to, text, accountId });
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
// Upload and send media if URL provided
|
|
22
22
|
if (mediaUrl) {
|
|
23
23
|
try {
|
|
24
|
-
const result = await sendMediaFeishu({ cfg, to, mediaUrl });
|
|
24
|
+
const result = await sendMediaFeishu({ cfg, to, mediaUrl, accountId });
|
|
25
25
|
return { channel: "feishu", ...result };
|
|
26
26
|
} catch (err) {
|
|
27
27
|
// Log the error for debugging
|
|
28
28
|
console.error(`[feishu] sendMediaFeishu failed:`, err);
|
|
29
29
|
// Fallback to URL link if upload fails
|
|
30
30
|
const fallbackText = `📎 ${mediaUrl}`;
|
|
31
|
-
const result = await sendMessageFeishu({ cfg, to, text: fallbackText });
|
|
31
|
+
const result = await sendMessageFeishu({ cfg, to, text: fallbackText, accountId });
|
|
32
32
|
return { channel: "feishu", ...result };
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
// No media URL, just return text result
|
|
37
|
-
const result = await sendMessageFeishu({ cfg, to, text: text ?? "" });
|
|
37
|
+
const result = await sendMessageFeishu({ cfg, to, text: text ?? "", accountId });
|
|
38
38
|
return { channel: "feishu", ...result };
|
|
39
39
|
},
|
|
40
40
|
};
|
package/src/perm.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
2
|
import { createFeishuClient } from "./client.js";
|
|
3
|
-
import
|
|
3
|
+
import { listEnabledFeishuAccounts } from "./accounts.js";
|
|
4
4
|
import type * as Lark from "@larksuiteoapi/node-sdk";
|
|
5
5
|
import { FeishuPermSchema, type FeishuPermParams } from "./perm-schema.js";
|
|
6
6
|
import { resolveToolsConfig } from "./tools-config.js";
|
|
@@ -112,19 +112,25 @@ async function removeMember(
|
|
|
112
112
|
// ============ Tool Registration ============
|
|
113
113
|
|
|
114
114
|
export function registerFeishuPermTools(api: OpenClawPluginApi) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
api.logger.debug?.("feishu_perm: Feishu credentials not configured, skipping perm tools");
|
|
115
|
+
if (!api.config) {
|
|
116
|
+
api.logger.debug?.("feishu_perm: No config available, skipping perm tools");
|
|
118
117
|
return;
|
|
119
118
|
}
|
|
120
119
|
|
|
121
|
-
const
|
|
120
|
+
const accounts = listEnabledFeishuAccounts(api.config);
|
|
121
|
+
if (accounts.length === 0) {
|
|
122
|
+
api.logger.debug?.("feishu_perm: No Feishu accounts configured, skipping perm tools");
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const firstAccount = accounts[0];
|
|
127
|
+
const toolsCfg = resolveToolsConfig(firstAccount.config.tools);
|
|
122
128
|
if (!toolsCfg.perm) {
|
|
123
129
|
api.logger.debug?.("feishu_perm: perm tool disabled in config (default: false)");
|
|
124
130
|
return;
|
|
125
131
|
}
|
|
126
132
|
|
|
127
|
-
const getClient = () => createFeishuClient(
|
|
133
|
+
const getClient = () => createFeishuClient(firstAccount);
|
|
128
134
|
|
|
129
135
|
api.registerTool(
|
|
130
136
|
{
|
package/src/probe.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import { createFeishuClient } from "./client.js";
|
|
3
|
-
import { resolveFeishuCredentials } from "./accounts.js";
|
|
1
|
+
import type { FeishuProbeResult } from "./types.js";
|
|
2
|
+
import { createFeishuClient, type FeishuClientCredentials } from "./client.js";
|
|
4
3
|
|
|
5
|
-
export async function probeFeishu(
|
|
6
|
-
|
|
7
|
-
if (!creds) {
|
|
4
|
+
export async function probeFeishu(creds?: FeishuClientCredentials): Promise<FeishuProbeResult> {
|
|
5
|
+
if (!creds?.appId || !creds?.appSecret) {
|
|
8
6
|
return {
|
|
9
7
|
ok: false,
|
|
10
8
|
error: "missing credentials (appId, appSecret)",
|
|
@@ -12,9 +10,8 @@ export async function probeFeishu(cfg?: FeishuConfig): Promise<FeishuProbeResult
|
|
|
12
10
|
}
|
|
13
11
|
|
|
14
12
|
try {
|
|
15
|
-
const client = createFeishuClient(
|
|
16
|
-
// Use
|
|
17
|
-
// The bot info API path varies by SDK version
|
|
13
|
+
const client = createFeishuClient(creds);
|
|
14
|
+
// Use bot/v3/info API to get bot information
|
|
18
15
|
const response = await (client as any).request({
|
|
19
16
|
method: "GET",
|
|
20
17
|
url: "/open-apis/bot/v3/info",
|