@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/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 feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
31
- if (!feishuCfg) {
32
- throw new Error("Feishu channel not configured");
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(feishuCfg);
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 feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
109
- if (!feishuCfg) {
110
- throw new Error("Feishu channel not configured");
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(feishuCfg);
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 feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
202
- if (!feishuCfg) {
203
- throw new Error("Feishu channel not configured");
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(feishuCfg);
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 feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
248
- if (!feishuCfg) {
249
- throw new Error("Feishu channel not configured");
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(feishuCfg);
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 feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
293
- if (!feishuCfg) {
294
- throw new Error("Feishu channel not configured");
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(feishuCfg);
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 feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
355
- if (!feishuCfg) {
356
- throw new Error("Feishu channel not configured");
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(feishuCfg);
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 { FeishuConfig } from "./types.js";
4
+ import type { ResolvedFeishuAccount } from "./types.js";
4
5
  import { createFeishuWSClient, createEventDispatcher } from "./client.js";
5
- import { resolveFeishuCredentials } from "./accounts.js";
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
- let currentWsClient: Lark.WSClient | null = null;
17
- let botOpenId: string | undefined;
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(cfg: FeishuConfig): Promise<string | undefined> {
22
+ async function fetchBotOpenId(
23
+ account: ResolvedFeishuAccount,
24
+ ): Promise<string | undefined> {
20
25
  try {
21
- const result = await probeFeishu(cfg);
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
- export async function monitorFeishuProvider(opts: MonitorFeishuOpts = {}): Promise<void> {
29
- const cfg = opts.config;
30
- if (!cfg) {
31
- throw new Error("Config is required for Feishu monitor");
32
- }
33
-
34
- const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
35
- const creds = resolveFeishuCredentials(feishuCfg);
36
- if (!creds) {
37
- throw new Error("Feishu credentials not configured (appId, appSecret required)");
38
- }
39
-
40
- const log = opts.runtime?.log ?? console.log;
41
- const error = opts.runtime?.error ?? console.error;
42
-
43
- if (feishuCfg) {
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
- await handleFeishuMessage({
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 event: ${String(err)}`);
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
- if (currentWsClient === wsClient) {
115
- currentWsClient = null;
116
- }
154
+ wsClients.delete(accountId);
155
+ botOpenIds.delete(accountId);
117
156
  };
118
157
 
119
158
  const handleAbort = () => {
120
- log("feishu: abort signal received, stopping WebSocket client");
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
- eventDispatcher,
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
- export function stopFeishuMonitor(): void {
148
- if (currentWsClient) {
149
- currentWsClient = null;
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 type { FeishuConfig } from "./types.js";
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
- const feishuCfg = api.config?.channels?.feishu as FeishuConfig | undefined;
116
- if (!feishuCfg?.appId || !feishuCfg?.appSecret) {
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 toolsCfg = resolveToolsConfig(feishuCfg.tools);
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(feishuCfg);
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 { FeishuConfig, FeishuProbeResult } from "./types.js";
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(cfg?: FeishuConfig): Promise<FeishuProbeResult> {
6
- const creds = resolveFeishuCredentials(cfg);
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(cfg!);
16
- // Use im.chat.list as a simple connectivity test
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",