@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/bot.ts CHANGED
@@ -6,9 +6,10 @@ import {
6
6
  DEFAULT_GROUP_HISTORY_LIMIT,
7
7
  type HistoryEntry,
8
8
  } from "openclaw/plugin-sdk";
9
- import type { FeishuConfig, FeishuMessageContext, FeishuMediaInfo } from "./types.js";
9
+ import type { FeishuConfig, FeishuMessageContext, FeishuMediaInfo, ResolvedFeishuAccount } from "./types.js";
10
10
  import { getFeishuRuntime } from "./runtime.js";
11
11
  import { createFeishuClient } from "./client.js";
12
+ import { resolveFeishuAccount } from "./accounts.js";
12
13
  import {
13
14
  resolveFeishuGroupConfig,
14
15
  resolveFeishuReplyPolicy,
@@ -23,6 +24,39 @@ import {
23
24
  extractMessageBody,
24
25
  isMentionForwardRequest,
25
26
  } from "./mention.js";
27
+ import { maybeCreateDynamicAgent } from "./dynamic-agent.js";
28
+ import type { DynamicAgentCreationConfig } from "./types.js";
29
+
30
+ // --- Message deduplication ---
31
+ // Prevent duplicate processing when WebSocket reconnects or Feishu redelivers messages.
32
+ const DEDUP_TTL_MS = 30 * 60 * 1000; // 30 minutes
33
+ const DEDUP_MAX_SIZE = 1_000;
34
+ const DEDUP_CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // cleanup every 5 minutes
35
+ const processedMessageIds = new Map<string, number>(); // messageId -> timestamp
36
+ let lastCleanupTime = Date.now();
37
+
38
+ function tryRecordMessage(messageId: string): boolean {
39
+ const now = Date.now();
40
+
41
+ // Throttled cleanup: evict expired entries at most once per interval
42
+ if (now - lastCleanupTime > DEDUP_CLEANUP_INTERVAL_MS) {
43
+ for (const [id, ts] of processedMessageIds) {
44
+ if (now - ts > DEDUP_TTL_MS) processedMessageIds.delete(id);
45
+ }
46
+ lastCleanupTime = now;
47
+ }
48
+
49
+ if (processedMessageIds.has(messageId)) return false;
50
+
51
+ // Evict oldest entries if cache is full
52
+ if (processedMessageIds.size >= DEDUP_MAX_SIZE) {
53
+ const first = processedMessageIds.keys().next().value!;
54
+ processedMessageIds.delete(first);
55
+ }
56
+
57
+ processedMessageIds.set(messageId, now);
58
+ return true;
59
+ }
26
60
 
27
61
  // --- Permission error extraction ---
28
62
  // Extract permission grant URL from Feishu API error response.
@@ -51,7 +85,7 @@ function extractPermissionError(err: unknown): PermissionError | null {
51
85
 
52
86
  // Extract the grant URL from the error message (contains the direct link)
53
87
  const msg = feishuErr.msg ?? "";
54
- const urlMatch = msg.match(/https:\/\/open\.feishu\.cn\/app\/[^\s,]+/);
88
+ const urlMatch = msg.match(/https:\/\/[^\s,]+\/app\/[^\s,]+/);
55
89
  const grantUrl = urlMatch?.[0];
56
90
 
57
91
  return {
@@ -77,12 +111,12 @@ type SenderNameResult = {
77
111
  };
78
112
 
79
113
  async function resolveFeishuSenderName(params: {
80
- feishuCfg?: FeishuConfig;
114
+ account: ResolvedFeishuAccount;
81
115
  senderOpenId: string;
82
116
  log: (...args: any[]) => void;
83
117
  }): Promise<SenderNameResult> {
84
- const { feishuCfg, senderOpenId, log } = params;
85
- if (!feishuCfg) return {};
118
+ const { account, senderOpenId, log } = params;
119
+ if (!account.configured) return {};
86
120
  if (!senderOpenId) return {};
87
121
 
88
122
  const cached = senderNameCache.get(senderOpenId);
@@ -90,7 +124,7 @@ async function resolveFeishuSenderName(params: {
90
124
  if (cached && cached.expireAt > now) return { name: cached.name };
91
125
 
92
126
  try {
93
- const client = createFeishuClient(feishuCfg);
127
+ const client = createFeishuClient(account);
94
128
 
95
129
  // contact/v3/users/:user_id?user_id_type=open_id
96
130
  const res: any = await client.contact.user.get({
@@ -308,8 +342,9 @@ async function resolveFeishuMediaList(params: {
308
342
  content: string;
309
343
  maxBytes: number;
310
344
  log?: (msg: string) => void;
345
+ accountId?: string;
311
346
  }): Promise<FeishuMediaInfo[]> {
312
- const { cfg, messageId, messageType, content, maxBytes, log } = params;
347
+ const { cfg, messageId, messageType, content, maxBytes, log, accountId } = params;
313
348
 
314
349
  // Only process media message types (including post for embedded images)
315
350
  const mediaTypes = ["image", "file", "audio", "video", "sticker", "post"];
@@ -337,6 +372,7 @@ async function resolveFeishuMediaList(params: {
337
372
  messageId,
338
373
  fileKey: imageKey,
339
374
  type: "image",
375
+ accountId,
340
376
  });
341
377
 
342
378
  let contentType = result.contentType;
@@ -390,6 +426,7 @@ async function resolveFeishuMediaList(params: {
390
426
  messageId,
391
427
  fileKey,
392
428
  type: resourceType,
429
+ accountId,
393
430
  });
394
431
  buffer = result.buffer;
395
432
  contentType = result.contentType;
@@ -491,18 +528,30 @@ export async function handleFeishuMessage(params: {
491
528
  botOpenId?: string;
492
529
  runtime?: RuntimeEnv;
493
530
  chatHistories?: Map<string, HistoryEntry[]>;
531
+ accountId?: string;
494
532
  }): Promise<void> {
495
- const { cfg, event, botOpenId, runtime, chatHistories } = params;
496
- const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
533
+ const { cfg, event, botOpenId, runtime, chatHistories, accountId } = params;
534
+
535
+ // Resolve account with merged config
536
+ const account = resolveFeishuAccount({ cfg, accountId });
537
+ const feishuCfg = account.config;
538
+
497
539
  const log = runtime?.log ?? console.log;
498
540
  const error = runtime?.error ?? console.error;
499
541
 
542
+ // Dedup check: skip if this message was already processed
543
+ const messageId = event.message.message_id;
544
+ if (!tryRecordMessage(messageId)) {
545
+ log(`feishu: skipping duplicate message ${messageId}`);
546
+ return;
547
+ }
548
+
500
549
  let ctx = parseFeishuMessageEvent(event, botOpenId);
501
550
  const isGroup = ctx.chatType === "group";
502
551
 
503
552
  // Resolve sender display name (best-effort) so the agent can attribute messages correctly.
504
553
  const senderResult = await resolveFeishuSenderName({
505
- feishuCfg,
554
+ account,
506
555
  senderOpenId: ctx.senderOpenId,
507
556
  log,
508
557
  });
@@ -511,7 +560,7 @@ export async function handleFeishuMessage(params: {
511
560
  // Track permission error to inform agent later (with cooldown to avoid repetition)
512
561
  let permissionErrorForAgent: PermissionError | undefined;
513
562
  if (senderResult.permissionError) {
514
- const appKey = feishuCfg?.appId ?? "default";
563
+ const appKey = account.appId ?? "default";
515
564
  const now = Date.now();
516
565
  const lastNotified = permissionErrorNotifiedAt.get(appKey) ?? 0;
517
566
 
@@ -521,12 +570,12 @@ export async function handleFeishuMessage(params: {
521
570
  }
522
571
  }
523
572
 
524
- log(`feishu: received message from ${ctx.senderOpenId} in ${ctx.chatId} (${ctx.chatType})`);
573
+ log(`feishu[${account.accountId}]: received message from ${ctx.senderOpenId} in ${ctx.chatId} (${ctx.chatType})`);
525
574
 
526
575
  // Log mention targets if detected
527
576
  if (ctx.mentionTargets && ctx.mentionTargets.length > 0) {
528
577
  const names = ctx.mentionTargets.map((t) => t.name).join(", ");
529
- log(`feishu: detected @ forward request, targets: [${names}]`);
578
+ log(`feishu[${account.accountId}]: detected @ forward request, targets: [${names}]`);
530
579
  }
531
580
 
532
581
  const historyLimit = Math.max(
@@ -537,6 +586,7 @@ export async function handleFeishuMessage(params: {
537
586
  if (isGroup) {
538
587
  const groupPolicy = feishuCfg?.groupPolicy ?? "open";
539
588
  const groupAllowFrom = feishuCfg?.groupAllowFrom ?? [];
589
+ // DEBUG: log(`feishu[${account.accountId}]: groupPolicy=${groupPolicy}`);
540
590
  const groupConfig = resolveFeishuGroupConfig({ cfg: feishuCfg, groupId: ctx.chatId });
541
591
 
542
592
  // Check if this GROUP is allowed (groupAllowFrom contains group IDs like oc_xxx, not user IDs)
@@ -548,7 +598,7 @@ export async function handleFeishuMessage(params: {
548
598
  });
549
599
 
550
600
  if (!groupAllowed) {
551
- log(`feishu: group ${ctx.chatId} not in allowlist`);
601
+ log(`feishu[${account.accountId}]: sender ${ctx.senderOpenId} not in group allowlist`);
552
602
  return;
553
603
  }
554
604
 
@@ -574,7 +624,7 @@ export async function handleFeishuMessage(params: {
574
624
  });
575
625
 
576
626
  if (requireMention && !ctx.mentionedBot) {
577
- log(`feishu: message in group ${ctx.chatId} did not mention bot, recording to history`);
627
+ log(`feishu[${account.accountId}]: message in group ${ctx.chatId} did not mention bot, recording to history`);
578
628
  if (chatHistories) {
579
629
  recordPendingHistoryEntryIfEnabled({
580
630
  historyMap: chatHistories,
@@ -600,7 +650,7 @@ export async function handleFeishuMessage(params: {
600
650
  senderId: ctx.senderOpenId,
601
651
  });
602
652
  if (!match.allowed) {
603
- log(`feishu: sender ${ctx.senderOpenId} not in DM allowlist`);
653
+ log(`feishu[${account.accountId}]: sender ${ctx.senderOpenId} not in DM allowlist`);
604
654
  return;
605
655
  }
606
656
  }
@@ -614,19 +664,62 @@ export async function handleFeishuMessage(params: {
614
664
  const feishuFrom = `feishu:${ctx.senderOpenId}`;
615
665
  const feishuTo = isGroup ? `chat:${ctx.chatId}` : `user:${ctx.senderOpenId}`;
616
666
 
617
- const route = core.channel.routing.resolveAgentRoute({
667
+ // Resolve peer ID for session routing
668
+ // When topicSessionMode is enabled, messages within a topic (identified by root_id)
669
+ // get a separate session from the main group chat.
670
+ let peerId = isGroup ? ctx.chatId : ctx.senderOpenId;
671
+ if (isGroup && ctx.rootId) {
672
+ const groupConfig = resolveFeishuGroupConfig({ cfg: feishuCfg, groupId: ctx.chatId });
673
+ const topicSessionMode = groupConfig?.topicSessionMode ?? feishuCfg?.topicSessionMode ?? "disabled";
674
+ if (topicSessionMode === "enabled") {
675
+ // Use chatId:topic:rootId as peer ID for topic-scoped sessions
676
+ peerId = `${ctx.chatId}:topic:${ctx.rootId}`;
677
+ log(`feishu[${account.accountId}]: topic session isolation enabled, peer=${peerId}`);
678
+ }
679
+ }
680
+
681
+ let route = core.channel.routing.resolveAgentRoute({
618
682
  cfg,
619
683
  channel: "feishu",
684
+ accountId: account.accountId,
620
685
  peer: {
621
- kind: isGroup ? "group" : "dm",
622
- id: isGroup ? ctx.chatId : ctx.senderOpenId,
686
+ kind: isGroup ? "group" : "direct",
687
+ id: peerId,
623
688
  },
624
689
  });
625
690
 
691
+ // Dynamic agent creation for DM users
692
+ // When enabled, creates a unique agent instance with its own workspace for each DM user.
693
+ let effectiveCfg = cfg;
694
+ if (!isGroup && route.matchedBy === "default") {
695
+ const dynamicCfg = feishuCfg?.dynamicAgentCreation as DynamicAgentCreationConfig | undefined;
696
+ if (dynamicCfg?.enabled) {
697
+ const runtime = getFeishuRuntime();
698
+ const result = await maybeCreateDynamicAgent({
699
+ cfg,
700
+ runtime,
701
+ senderOpenId: ctx.senderOpenId,
702
+ dynamicCfg,
703
+ log: (msg) => log(msg),
704
+ });
705
+ if (result.created) {
706
+ effectiveCfg = result.updatedCfg;
707
+ // Re-resolve route with updated config
708
+ route = core.channel.routing.resolveAgentRoute({
709
+ cfg: result.updatedCfg,
710
+ channel: "feishu",
711
+ accountId: account.accountId,
712
+ peer: { kind: "dm", id: ctx.senderOpenId },
713
+ });
714
+ log(`feishu[${account.accountId}]: dynamic agent created, new route: ${route.sessionKey}`);
715
+ }
716
+ }
717
+ }
718
+
626
719
  const preview = ctx.content.replace(/\s+/g, " ").slice(0, 160);
627
720
  const inboundLabel = isGroup
628
- ? `Feishu message in group ${ctx.chatId}`
629
- : `Feishu DM from ${ctx.senderOpenId}`;
721
+ ? `Feishu[${account.accountId}] message in group ${ctx.chatId}`
722
+ : `Feishu[${account.accountId}] DM from ${ctx.senderOpenId}`;
630
723
 
631
724
  core.system.enqueueSystemEvent(`${inboundLabel}: ${preview}`, {
632
725
  sessionKey: route.sessionKey,
@@ -642,6 +735,7 @@ export async function handleFeishuMessage(params: {
642
735
  content: event.message.content,
643
736
  maxBytes: mediaMaxBytes,
644
737
  log,
738
+ accountId: account.accountId,
645
739
  });
646
740
  const mediaPayload = buildFeishuMediaPayload(mediaList);
647
741
 
@@ -649,13 +743,13 @@ export async function handleFeishuMessage(params: {
649
743
  let quotedContent: string | undefined;
650
744
  if (ctx.parentId) {
651
745
  try {
652
- const quotedMsg = await getMessageFeishu({ cfg, messageId: ctx.parentId });
746
+ const quotedMsg = await getMessageFeishu({ cfg, messageId: ctx.parentId, accountId: account.accountId });
653
747
  if (quotedMsg) {
654
748
  quotedContent = quotedMsg.content;
655
- log(`feishu: fetched quoted message: ${quotedContent?.slice(0, 100)}`);
749
+ log(`feishu[${account.accountId}]: fetched quoted message: ${quotedContent?.slice(0, 100)}`);
656
750
  }
657
751
  } catch (err) {
658
- log(`feishu: failed to fetch quoted message: ${String(err)}`);
752
+ log(`feishu[${account.accountId}]: failed to fetch quoted message: ${String(err)}`);
659
753
  }
660
754
  }
661
755
 
@@ -722,9 +816,10 @@ export async function handleFeishuMessage(params: {
722
816
  runtime: runtime as RuntimeEnv,
723
817
  chatId: ctx.chatId,
724
818
  replyToMessageId: ctx.messageId,
819
+ accountId: account.accountId,
725
820
  });
726
821
 
727
- log(`feishu: dispatching permission error notification to agent`);
822
+ log(`feishu[${account.accountId}]: dispatching permission error notification to agent`);
728
823
 
729
824
  await core.channel.reply.dispatchReplyFromConfig({
730
825
  ctx: permissionCtx,
@@ -795,9 +890,10 @@ export async function handleFeishuMessage(params: {
795
890
  chatId: ctx.chatId,
796
891
  replyToMessageId: ctx.messageId,
797
892
  mentionTargets: ctx.mentionTargets,
893
+ accountId: account.accountId,
798
894
  });
799
895
 
800
- log(`feishu: dispatching to agent (session=${route.sessionKey})`);
896
+ log(`feishu[${account.accountId}]: dispatching to agent (session=${route.sessionKey})`);
801
897
 
802
898
  const { queuedFinal, counts } = await core.channel.reply.dispatchReplyFromConfig({
803
899
  ctx: ctxPayload,
@@ -816,8 +912,8 @@ export async function handleFeishuMessage(params: {
816
912
  });
817
913
  }
818
914
 
819
- log(`feishu: dispatch complete (queuedFinal=${queuedFinal}, replies=${counts.final})`);
915
+ log(`feishu[${account.accountId}]: dispatch complete (queuedFinal=${queuedFinal}, replies=${counts.final})`);
820
916
  } catch (err) {
821
- error(`feishu: failed to dispatch message: ${String(err)}`);
917
+ error(`feishu[${account.accountId}]: failed to dispatch message: ${String(err)}`);
822
918
  }
823
919
  }
package/src/channel.ts CHANGED
@@ -1,7 +1,12 @@
1
1
  import type { ChannelPlugin, ClawdbotConfig } from "openclaw/plugin-sdk";
2
2
  import { DEFAULT_ACCOUNT_ID, PAIRING_APPROVED_MESSAGE } from "openclaw/plugin-sdk";
3
3
  import type { ResolvedFeishuAccount, FeishuConfig } from "./types.js";
4
- import { resolveFeishuAccount, resolveFeishuCredentials } from "./accounts.js";
4
+ import {
5
+ resolveFeishuAccount,
6
+ resolveFeishuCredentials,
7
+ listFeishuAccountIds,
8
+ resolveDefaultFeishuAccountId,
9
+ } from "./accounts.js";
5
10
  import { feishuOutbound } from "./outbound.js";
6
11
  import { probeFeishu } from "./probe.js";
7
12
  import { resolveFeishuGroupToolPolicy } from "./policy.js";
@@ -34,11 +39,12 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
34
39
  pairing: {
35
40
  idLabel: "feishuUserId",
36
41
  normalizeAllowEntry: (entry) => entry.replace(/^(feishu|user|open_id):/i, ""),
37
- notifyApproval: async ({ cfg, id }) => {
42
+ notifyApproval: async ({ cfg, id, accountId }) => {
38
43
  await sendMessageFeishu({
39
44
  cfg,
40
45
  to: id,
41
46
  text: PAIRING_APPROVED_MESSAGE,
47
+ accountId,
42
48
  });
43
49
  },
44
50
  },
@@ -71,7 +77,12 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
71
77
  appSecret: { type: "string" },
72
78
  encryptKey: { type: "string" },
73
79
  verificationToken: { type: "string" },
74
- domain: { type: "string", enum: ["feishu", "lark"] },
80
+ domain: {
81
+ oneOf: [
82
+ { type: "string", enum: ["feishu", "lark"] },
83
+ { type: "string", format: "uri", pattern: "^https://" },
84
+ ],
85
+ },
75
86
  connectionMode: { type: "string", enum: ["websocket", "webhook"] },
76
87
  webhookPath: { type: "string" },
77
88
  webhookPort: { type: "integer", minimum: 1 },
@@ -80,49 +91,118 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
80
91
  groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
81
92
  groupAllowFrom: { type: "array", items: { oneOf: [{ type: "string" }, { type: "number" }] } },
82
93
  requireMention: { type: "boolean" },
94
+ topicSessionMode: { type: "string", enum: ["disabled", "enabled"] },
83
95
  historyLimit: { type: "integer", minimum: 0 },
84
96
  dmHistoryLimit: { type: "integer", minimum: 0 },
85
97
  textChunkLimit: { type: "integer", minimum: 1 },
86
98
  chunkMode: { type: "string", enum: ["length", "newline"] },
87
99
  mediaMaxMb: { type: "number", minimum: 0 },
88
100
  renderMode: { type: "string", enum: ["auto", "raw", "card"] },
101
+ accounts: {
102
+ type: "object",
103
+ additionalProperties: {
104
+ type: "object",
105
+ properties: {
106
+ enabled: { type: "boolean" },
107
+ name: { type: "string" },
108
+ appId: { type: "string" },
109
+ appSecret: { type: "string" },
110
+ encryptKey: { type: "string" },
111
+ verificationToken: { type: "string" },
112
+ domain: { type: "string", enum: ["feishu", "lark"] },
113
+ connectionMode: { type: "string", enum: ["websocket", "webhook"] },
114
+ },
115
+ },
116
+ },
89
117
  },
90
118
  },
91
119
  },
92
120
  config: {
93
- listAccountIds: () => [DEFAULT_ACCOUNT_ID],
94
- resolveAccount: (cfg) => resolveFeishuAccount({ cfg }),
95
- defaultAccountId: () => DEFAULT_ACCOUNT_ID,
96
- setAccountEnabled: ({ cfg, enabled }) => ({
97
- ...cfg,
98
- channels: {
99
- ...cfg.channels,
100
- feishu: {
101
- ...cfg.channels?.feishu,
102
- enabled,
121
+ listAccountIds: (cfg) => listFeishuAccountIds(cfg),
122
+ resolveAccount: (cfg, accountId) => resolveFeishuAccount({ cfg, accountId }),
123
+ defaultAccountId: (cfg) => resolveDefaultFeishuAccountId(cfg),
124
+ setAccountEnabled: ({ cfg, accountId, enabled }) => {
125
+ const account = resolveFeishuAccount({ cfg, accountId });
126
+ const isDefault = accountId === DEFAULT_ACCOUNT_ID;
127
+
128
+ if (isDefault) {
129
+ // For default account, set top-level enabled
130
+ return {
131
+ ...cfg,
132
+ channels: {
133
+ ...cfg.channels,
134
+ feishu: {
135
+ ...cfg.channels?.feishu,
136
+ enabled,
137
+ },
138
+ },
139
+ };
140
+ }
141
+
142
+ // For named accounts, set enabled in accounts[accountId]
143
+ const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
144
+ return {
145
+ ...cfg,
146
+ channels: {
147
+ ...cfg.channels,
148
+ feishu: {
149
+ ...feishuCfg,
150
+ accounts: {
151
+ ...feishuCfg?.accounts,
152
+ [accountId]: {
153
+ ...feishuCfg?.accounts?.[accountId],
154
+ enabled,
155
+ },
156
+ },
157
+ },
103
158
  },
104
- },
105
- }),
106
- deleteAccount: ({ cfg }) => {
107
- const next = { ...cfg } as ClawdbotConfig;
108
- const nextChannels = { ...cfg.channels };
109
- delete (nextChannels as Record<string, unknown>).feishu;
110
- if (Object.keys(nextChannels).length > 0) {
111
- next.channels = nextChannels;
112
- } else {
113
- delete next.channels;
159
+ };
160
+ },
161
+ deleteAccount: ({ cfg, accountId }) => {
162
+ const isDefault = accountId === DEFAULT_ACCOUNT_ID;
163
+
164
+ if (isDefault) {
165
+ // Delete entire feishu config
166
+ const next = { ...cfg } as ClawdbotConfig;
167
+ const nextChannels = { ...cfg.channels };
168
+ delete (nextChannels as Record<string, unknown>).feishu;
169
+ if (Object.keys(nextChannels).length > 0) {
170
+ next.channels = nextChannels;
171
+ } else {
172
+ delete next.channels;
173
+ }
174
+ return next;
114
175
  }
115
- return next;
176
+
177
+ // Delete specific account from accounts
178
+ const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
179
+ const accounts = { ...feishuCfg?.accounts };
180
+ delete accounts[accountId];
181
+
182
+ return {
183
+ ...cfg,
184
+ channels: {
185
+ ...cfg.channels,
186
+ feishu: {
187
+ ...feishuCfg,
188
+ accounts: Object.keys(accounts).length > 0 ? accounts : undefined,
189
+ },
190
+ },
191
+ };
116
192
  },
117
- isConfigured: (_account, cfg) =>
118
- Boolean(resolveFeishuCredentials(cfg.channels?.feishu as FeishuConfig | undefined)),
193
+ isConfigured: (account) => account.configured,
119
194
  describeAccount: (account) => ({
120
195
  accountId: account.accountId,
121
196
  enabled: account.enabled,
122
197
  configured: account.configured,
198
+ name: account.name,
199
+ appId: account.appId,
200
+ domain: account.domain,
123
201
  }),
124
- resolveAllowFrom: ({ cfg }) =>
125
- (cfg.channels?.feishu as FeishuConfig | undefined)?.allowFrom ?? [],
202
+ resolveAllowFrom: ({ cfg, accountId }) => {
203
+ const account = resolveFeishuAccount({ cfg, accountId });
204
+ return account.config?.allowFrom ?? [];
205
+ },
126
206
  formatAllowFrom: ({ allowFrom }) =>
127
207
  allowFrom
128
208
  .map((entry) => String(entry).trim())
@@ -130,28 +210,53 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
130
210
  .map((entry) => entry.toLowerCase()),
131
211
  },
132
212
  security: {
133
- collectWarnings: ({ cfg }) => {
134
- const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
213
+ collectWarnings: ({ cfg, accountId }) => {
214
+ const account = resolveFeishuAccount({ cfg, accountId });
215
+ const feishuCfg = account.config;
135
216
  const defaultGroupPolicy = (cfg.channels as Record<string, { groupPolicy?: string }> | undefined)?.defaults?.groupPolicy;
136
217
  const groupPolicy = feishuCfg?.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
137
218
  if (groupPolicy !== "open") return [];
138
219
  return [
139
- `- Feishu groups: groupPolicy="open" allows any member to trigger (mention-gated). Set channels.feishu.groupPolicy="allowlist" + channels.feishu.groupAllowFrom to restrict senders.`,
220
+ `- Feishu[${account.accountId}] groups: groupPolicy="open" allows any member to trigger (mention-gated). Set channels.feishu.groupPolicy="allowlist" + channels.feishu.groupAllowFrom to restrict senders.`,
140
221
  ];
141
222
  },
142
223
  },
143
224
  setup: {
144
225
  resolveAccountId: () => DEFAULT_ACCOUNT_ID,
145
- applyAccountConfig: ({ cfg }) => ({
146
- ...cfg,
147
- channels: {
148
- ...cfg.channels,
149
- feishu: {
150
- ...cfg.channels?.feishu,
151
- enabled: true,
226
+ applyAccountConfig: ({ cfg, accountId }) => {
227
+ const isDefault = !accountId || accountId === DEFAULT_ACCOUNT_ID;
228
+
229
+ if (isDefault) {
230
+ return {
231
+ ...cfg,
232
+ channels: {
233
+ ...cfg.channels,
234
+ feishu: {
235
+ ...cfg.channels?.feishu,
236
+ enabled: true,
237
+ },
238
+ },
239
+ };
240
+ }
241
+
242
+ const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
243
+ return {
244
+ ...cfg,
245
+ channels: {
246
+ ...cfg.channels,
247
+ feishu: {
248
+ ...feishuCfg,
249
+ accounts: {
250
+ ...feishuCfg?.accounts,
251
+ [accountId]: {
252
+ ...feishuCfg?.accounts?.[accountId],
253
+ enabled: true,
254
+ },
255
+ },
256
+ },
152
257
  },
153
- },
154
- }),
258
+ };
259
+ },
155
260
  },
156
261
  onboarding: feishuOnboardingAdapter,
157
262
  messaging: {
@@ -163,14 +268,14 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
163
268
  },
164
269
  directory: {
165
270
  self: async () => null,
166
- listPeers: async ({ cfg, query, limit }) =>
167
- listFeishuDirectoryPeers({ cfg, query, limit }),
168
- listGroups: async ({ cfg, query, limit }) =>
169
- listFeishuDirectoryGroups({ cfg, query, limit }),
170
- listPeersLive: async ({ cfg, query, limit }) =>
171
- listFeishuDirectoryPeersLive({ cfg, query, limit }),
172
- listGroupsLive: async ({ cfg, query, limit }) =>
173
- listFeishuDirectoryGroupsLive({ cfg, query, limit }),
271
+ listPeers: async ({ cfg, query, limit, accountId }) =>
272
+ listFeishuDirectoryPeers({ cfg, query, limit, accountId }),
273
+ listGroups: async ({ cfg, query, limit, accountId }) =>
274
+ listFeishuDirectoryGroups({ cfg, query, limit, accountId }),
275
+ listPeersLive: async ({ cfg, query, limit, accountId }) =>
276
+ listFeishuDirectoryPeersLive({ cfg, query, limit, accountId }),
277
+ listGroupsLive: async ({ cfg, query, limit, accountId }) =>
278
+ listFeishuDirectoryGroupsLive({ cfg, query, limit, accountId }),
174
279
  },
175
280
  outbound: feishuOutbound,
176
281
  status: {
@@ -192,12 +297,17 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
192
297
  probe: snapshot.probe,
193
298
  lastProbeAt: snapshot.lastProbeAt ?? null,
194
299
  }),
195
- probeAccount: async ({ cfg }) =>
196
- await probeFeishu(cfg.channels?.feishu as FeishuConfig | undefined),
300
+ probeAccount: async ({ cfg, accountId }) => {
301
+ const account = resolveFeishuAccount({ cfg, accountId });
302
+ return await probeFeishu(account);
303
+ },
197
304
  buildAccountSnapshot: ({ account, runtime, probe }) => ({
198
305
  accountId: account.accountId,
199
306
  enabled: account.enabled,
200
307
  configured: account.configured,
308
+ name: account.name,
309
+ appId: account.appId,
310
+ domain: account.domain,
201
311
  running: runtime?.running ?? false,
202
312
  lastStartAt: runtime?.lastStartAt ?? null,
203
313
  lastStopAt: runtime?.lastStopAt ?? null,
@@ -209,10 +319,10 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
209
319
  gateway: {
210
320
  startAccount: async (ctx) => {
211
321
  const { monitorFeishuProvider } = await import("./monitor.js");
212
- const feishuCfg = ctx.cfg.channels?.feishu as FeishuConfig | undefined;
213
- const port = feishuCfg?.webhookPort ?? null;
322
+ const account = resolveFeishuAccount({ cfg: ctx.cfg, accountId: ctx.accountId });
323
+ const port = account.config?.webhookPort ?? null;
214
324
  ctx.setStatus({ accountId: ctx.accountId, port });
215
- ctx.log?.info(`starting feishu provider (mode: ${feishuCfg?.connectionMode ?? "websocket"})`);
325
+ ctx.log?.info(`starting feishu[${ctx.accountId}] (mode: ${account.config?.connectionMode ?? "websocket"})`);
216
326
  return monitorFeishuProvider({
217
327
  config: ctx.cfg,
218
328
  runtime: ctx.runtime,