Skip to content

Commit 5f6e1c1

Browse files
doodlewindclaude
authored andcommitted
feat(feishu): sync with clawdbot-feishu #137 (multi-account support)
- Sync latest changes from clawdbot-feishu including multi-account support - Add eslint-disable comments for SDK-related any types - Remove unused imports - Fix no-floating-promises in monitor.ts Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 7e005ac commit 5f6e1c1

22 files changed

Lines changed: 784 additions & 368 deletions

extensions/feishu/src/accounts.ts

Lines changed: 102 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,81 @@
11
import type { ClawdbotConfig } from "openclaw/plugin-sdk";
2-
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk";
3-
import type { FeishuConfig, FeishuDomain, ResolvedFeishuAccount } from "./types.js";
2+
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk";
3+
import type {
4+
FeishuConfig,
5+
FeishuAccountConfig,
6+
FeishuDomain,
7+
ResolvedFeishuAccount,
8+
} from "./types.js";
49

10+
/**
11+
* List all configured account IDs from the accounts field.
12+
*/
13+
function listConfiguredAccountIds(cfg: ClawdbotConfig): string[] {
14+
const accounts = (cfg.channels?.feishu as FeishuConfig)?.accounts;
15+
if (!accounts || typeof accounts !== "object") {
16+
return [];
17+
}
18+
return Object.keys(accounts).filter(Boolean);
19+
}
20+
21+
/**
22+
* List all Feishu account IDs.
23+
* If no accounts are configured, returns [DEFAULT_ACCOUNT_ID] for backward compatibility.
24+
*/
25+
export function listFeishuAccountIds(cfg: ClawdbotConfig): string[] {
26+
const ids = listConfiguredAccountIds(cfg);
27+
if (ids.length === 0) {
28+
// Backward compatibility: no accounts configured, use default
29+
return [DEFAULT_ACCOUNT_ID];
30+
}
31+
return [...ids].toSorted((a, b) => a.localeCompare(b));
32+
}
33+
34+
/**
35+
* Resolve the default account ID.
36+
*/
37+
export function resolveDefaultFeishuAccountId(cfg: ClawdbotConfig): string {
38+
const ids = listFeishuAccountIds(cfg);
39+
if (ids.includes(DEFAULT_ACCOUNT_ID)) {
40+
return DEFAULT_ACCOUNT_ID;
41+
}
42+
return ids[0] ?? DEFAULT_ACCOUNT_ID;
43+
}
44+
45+
/**
46+
* Get the raw account-specific config.
47+
*/
48+
function resolveAccountConfig(
49+
cfg: ClawdbotConfig,
50+
accountId: string,
51+
): FeishuAccountConfig | undefined {
52+
const accounts = (cfg.channels?.feishu as FeishuConfig)?.accounts;
53+
if (!accounts || typeof accounts !== "object") {
54+
return undefined;
55+
}
56+
return accounts[accountId];
57+
}
58+
59+
/**
60+
* Merge top-level config with account-specific config.
61+
* Account-specific fields override top-level fields.
62+
*/
63+
function mergeFeishuAccountConfig(cfg: ClawdbotConfig, accountId: string): FeishuConfig {
64+
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
65+
66+
// Extract base config (exclude accounts field to avoid recursion)
67+
const { accounts: _ignored, ...base } = feishuCfg ?? {};
68+
69+
// Get account-specific overrides
70+
const account = resolveAccountConfig(cfg, accountId) ?? {};
71+
72+
// Merge: account config overrides base config
73+
return { ...base, ...account } as FeishuConfig;
74+
}
75+
76+
/**
77+
* Resolve Feishu credentials from a config.
78+
*/
579
export function resolveFeishuCredentials(cfg?: FeishuConfig): {
680
appId: string;
781
appSecret: string;
@@ -23,31 +97,46 @@ export function resolveFeishuCredentials(cfg?: FeishuConfig): {
2397
};
2498
}
2599

100+
/**
101+
* Resolve a complete Feishu account with merged config.
102+
*/
26103
export function resolveFeishuAccount(params: {
27104
cfg: ClawdbotConfig;
28105
accountId?: string | null;
29106
}): ResolvedFeishuAccount {
107+
const accountId = normalizeAccountId(params.accountId);
30108
const feishuCfg = params.cfg.channels?.feishu as FeishuConfig | undefined;
31-
const enabled = feishuCfg?.enabled !== false;
32-
const creds = resolveFeishuCredentials(feishuCfg);
109+
110+
// Base enabled state (top-level)
111+
const baseEnabled = feishuCfg?.enabled !== false;
112+
113+
// Merge configs
114+
const merged = mergeFeishuAccountConfig(params.cfg, accountId);
115+
116+
// Account-level enabled state
117+
const accountEnabled = merged.enabled !== false;
118+
const enabled = baseEnabled && accountEnabled;
119+
120+
// Resolve credentials from merged config
121+
const creds = resolveFeishuCredentials(merged);
33122

34123
return {
35-
accountId: params.accountId?.trim() || DEFAULT_ACCOUNT_ID,
124+
accountId,
36125
enabled,
37126
configured: Boolean(creds),
127+
name: (merged as FeishuAccountConfig).name?.trim() || undefined,
38128
appId: creds?.appId,
129+
appSecret: creds?.appSecret,
130+
encryptKey: creds?.encryptKey,
131+
verificationToken: creds?.verificationToken,
39132
domain: creds?.domain ?? "feishu",
133+
config: merged,
40134
};
41135
}
42136

43-
export function listFeishuAccountIds(_cfg: ClawdbotConfig): string[] {
44-
return [DEFAULT_ACCOUNT_ID];
45-
}
46-
47-
export function resolveDefaultFeishuAccountId(_cfg: ClawdbotConfig): string {
48-
return DEFAULT_ACCOUNT_ID;
49-
}
50-
137+
/**
138+
* List all enabled and configured accounts.
139+
*/
51140
export function listEnabledFeishuAccounts(cfg: ClawdbotConfig): ResolvedFeishuAccount[] {
52141
return listFeishuAccountIds(cfg)
53142
.map((accountId) => resolveFeishuAccount({ cfg, accountId }))

extensions/feishu/src/bot.ts

Lines changed: 54 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import {
66
DEFAULT_GROUP_HISTORY_LIMIT,
77
type HistoryEntry,
88
} from "openclaw/plugin-sdk";
9-
import type { FeishuConfig, FeishuMessageContext, FeishuMediaInfo } from "./types.js";
9+
import type { FeishuMessageContext, FeishuMediaInfo, ResolvedFeishuAccount } from "./types.js";
10+
import { resolveFeishuAccount } from "./accounts.js";
1011
import { createFeishuClient } from "./client.js";
1112
import { downloadMessageResourceFeishu } from "./media.js";
1213
import { extractMentionTargets, extractMessageBody, isMentionForwardRequest } from "./mention.js";
@@ -79,12 +80,13 @@ type SenderNameResult = {
7980
};
8081

8182
async function resolveFeishuSenderName(params: {
82-
feishuCfg?: FeishuConfig;
83+
account: ResolvedFeishuAccount;
8384
senderOpenId: string;
84-
log: (...args: unknown[]) => void;
85+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- generic log function
86+
log: (...args: any[]) => void;
8587
}): Promise<SenderNameResult> {
86-
const { feishuCfg, senderOpenId, log } = params;
87-
if (!feishuCfg) {
88+
const { account, senderOpenId, log } = params;
89+
if (!account.configured) {
8890
return {};
8991
}
9092
if (!senderOpenId) {
@@ -98,10 +100,11 @@ async function resolveFeishuSenderName(params: {
98100
}
99101

100102
try {
101-
const client = createFeishuClient(feishuCfg);
103+
const client = createFeishuClient(account);
102104

103105
// contact/v3/users/:user_id?user_id_type=open_id
104-
const res = await client.contact.user.get({
106+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK response type
107+
const res: any = await client.contact.user.get({
105108
path: { user_id: senderOpenId },
106109
params: { user_id_type: "open_id" },
107110
});
@@ -325,8 +328,9 @@ async function resolveFeishuMediaList(params: {
325328
content: string;
326329
maxBytes: number;
327330
log?: (msg: string) => void;
331+
accountId?: string;
328332
}): Promise<FeishuMediaInfo[]> {
329-
const { cfg, messageId, messageType, content, maxBytes, log } = params;
333+
const { cfg, messageId, messageType, content, maxBytes, log, accountId } = params;
330334

331335
// Only process media message types (including post for embedded images)
332336
const mediaTypes = ["image", "file", "audio", "video", "sticker", "post"];
@@ -354,6 +358,7 @@ async function resolveFeishuMediaList(params: {
354358
messageId,
355359
fileKey: imageKey,
356360
type: "image",
361+
accountId,
357362
});
358363

359364
let contentType = result.contentType;
@@ -407,6 +412,7 @@ async function resolveFeishuMediaList(params: {
407412
messageId,
408413
fileKey,
409414
type: resourceType,
415+
accountId,
410416
});
411417
buffer = result.buffer;
412418
contentType = result.contentType;
@@ -506,9 +512,14 @@ export async function handleFeishuMessage(params: {
506512
botOpenId?: string;
507513
runtime?: RuntimeEnv;
508514
chatHistories?: Map<string, HistoryEntry[]>;
515+
accountId?: string;
509516
}): Promise<void> {
510-
const { cfg, event, botOpenId, runtime, chatHistories } = params;
511-
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
517+
const { cfg, event, botOpenId, runtime, chatHistories, accountId } = params;
518+
519+
// Resolve account with merged config
520+
const account = resolveFeishuAccount({ cfg, accountId });
521+
const feishuCfg = account.config;
522+
512523
const log = runtime?.log ?? console.log;
513524
const error = runtime?.error ?? console.error;
514525

@@ -517,7 +528,7 @@ export async function handleFeishuMessage(params: {
517528

518529
// Resolve sender display name (best-effort) so the agent can attribute messages correctly.
519530
const senderResult = await resolveFeishuSenderName({
520-
feishuCfg,
531+
account,
521532
senderOpenId: ctx.senderOpenId,
522533
log,
523534
});
@@ -528,7 +539,7 @@ export async function handleFeishuMessage(params: {
528539
// Track permission error to inform agent later (with cooldown to avoid repetition)
529540
let permissionErrorForAgent: PermissionError | undefined;
530541
if (senderResult.permissionError) {
531-
const appKey = feishuCfg?.appId ?? "default";
542+
const appKey = account.appId ?? "default";
532543
const now = Date.now();
533544
const lastNotified = permissionErrorNotifiedAt.get(appKey) ?? 0;
534545

@@ -538,12 +549,14 @@ export async function handleFeishuMessage(params: {
538549
}
539550
}
540551

541-
log(`feishu: received message from ${ctx.senderOpenId} in ${ctx.chatId} (${ctx.chatType})`);
552+
log(
553+
`feishu[${account.accountId}]: received message from ${ctx.senderOpenId} in ${ctx.chatId} (${ctx.chatType})`,
554+
);
542555

543556
// Log mention targets if detected
544557
if (ctx.mentionTargets && ctx.mentionTargets.length > 0) {
545558
const names = ctx.mentionTargets.map((t) => t.name).join(", ");
546-
log(`feishu: detected @ forward request, targets: [${names}]`);
559+
log(`feishu[${account.accountId}]: detected @ forward request, targets: [${names}]`);
547560
}
548561

549562
const historyLimit = Math.max(
@@ -554,6 +567,7 @@ export async function handleFeishuMessage(params: {
554567
if (isGroup) {
555568
const groupPolicy = feishuCfg?.groupPolicy ?? "open";
556569
const groupAllowFrom = feishuCfg?.groupAllowFrom ?? [];
570+
// DEBUG: log(`feishu[${account.accountId}]: groupPolicy=${groupPolicy}`);
557571
const groupConfig = resolveFeishuGroupConfig({ cfg: feishuCfg, groupId: ctx.chatId });
558572

559573
// Check if this GROUP is allowed (groupAllowFrom contains group IDs like oc_xxx, not user IDs)
@@ -565,7 +579,7 @@ export async function handleFeishuMessage(params: {
565579
});
566580

567581
if (!groupAllowed) {
568-
log(`feishu: group ${ctx.chatId} not in allowlist`);
582+
log(`feishu[${account.accountId}]: sender ${ctx.senderOpenId} not in group allowlist`);
569583
return;
570584
}
571585

@@ -591,7 +605,9 @@ export async function handleFeishuMessage(params: {
591605
});
592606

593607
if (requireMention && !ctx.mentionedBot) {
594-
log(`feishu: message in group ${ctx.chatId} did not mention bot, recording to history`);
608+
log(
609+
`feishu[${account.accountId}]: message in group ${ctx.chatId} did not mention bot, recording to history`,
610+
);
595611
if (chatHistories) {
596612
recordPendingHistoryEntryIfEnabled({
597613
historyMap: chatHistories,
@@ -617,7 +633,7 @@ export async function handleFeishuMessage(params: {
617633
senderId: ctx.senderOpenId,
618634
});
619635
if (!match.allowed) {
620-
log(`feishu: sender ${ctx.senderOpenId} not in DM allowlist`);
636+
log(`feishu[${account.accountId}]: sender ${ctx.senderOpenId} not in DM allowlist`);
621637
return;
622638
}
623639
}
@@ -634,6 +650,7 @@ export async function handleFeishuMessage(params: {
634650
const route = core.channel.routing.resolveAgentRoute({
635651
cfg,
636652
channel: "feishu",
653+
accountId: account.accountId,
637654
peer: {
638655
kind: isGroup ? "group" : "dm",
639656
id: isGroup ? ctx.chatId : ctx.senderOpenId,
@@ -642,8 +659,8 @@ export async function handleFeishuMessage(params: {
642659

643660
const preview = ctx.content.replace(/\s+/g, " ").slice(0, 160);
644661
const inboundLabel = isGroup
645-
? `Feishu message in group ${ctx.chatId}`
646-
: `Feishu DM from ${ctx.senderOpenId}`;
662+
? `Feishu[${account.accountId}] message in group ${ctx.chatId}`
663+
: `Feishu[${account.accountId}] DM from ${ctx.senderOpenId}`;
647664

648665
core.system.enqueueSystemEvent(`${inboundLabel}: ${preview}`, {
649666
sessionKey: route.sessionKey,
@@ -659,20 +676,27 @@ export async function handleFeishuMessage(params: {
659676
content: event.message.content,
660677
maxBytes: mediaMaxBytes,
661678
log,
679+
accountId: account.accountId,
662680
});
663681
const mediaPayload = buildFeishuMediaPayload(mediaList);
664682

665683
// Fetch quoted/replied message content if parentId exists
666684
let quotedContent: string | undefined;
667685
if (ctx.parentId) {
668686
try {
669-
const quotedMsg = await getMessageFeishu({ cfg, messageId: ctx.parentId });
687+
const quotedMsg = await getMessageFeishu({
688+
cfg,
689+
messageId: ctx.parentId,
690+
accountId: account.accountId,
691+
});
670692
if (quotedMsg) {
671693
quotedContent = quotedMsg.content;
672-
log(`feishu: fetched quoted message: ${quotedContent?.slice(0, 100)}`);
694+
log(
695+
`feishu[${account.accountId}]: fetched quoted message: ${quotedContent?.slice(0, 100)}`,
696+
);
673697
}
674698
} catch (err) {
675-
log(`feishu: failed to fetch quoted message: ${String(err)}`);
699+
log(`feishu[${account.accountId}]: failed to fetch quoted message: ${String(err)}`);
676700
}
677701
}
678702

@@ -742,9 +766,10 @@ export async function handleFeishuMessage(params: {
742766
runtime: runtime as RuntimeEnv,
743767
chatId: ctx.chatId,
744768
replyToMessageId: ctx.messageId,
769+
accountId: account.accountId,
745770
});
746771

747-
log(`feishu: dispatching permission error notification to agent`);
772+
log(`feishu[${account.accountId}]: dispatching permission error notification to agent`);
748773

749774
await core.channel.reply.dispatchReplyFromConfig({
750775
ctx: permissionCtx,
@@ -815,9 +840,10 @@ export async function handleFeishuMessage(params: {
815840
chatId: ctx.chatId,
816841
replyToMessageId: ctx.messageId,
817842
mentionTargets: ctx.mentionTargets,
843+
accountId: account.accountId,
818844
});
819845

820-
log(`feishu: dispatching to agent (session=${route.sessionKey})`);
846+
log(`feishu[${account.accountId}]: dispatching to agent (session=${route.sessionKey})`);
821847

822848
const { queuedFinal, counts } = await core.channel.reply.dispatchReplyFromConfig({
823849
ctx: ctxPayload,
@@ -836,8 +862,10 @@ export async function handleFeishuMessage(params: {
836862
});
837863
}
838864

839-
log(`feishu: dispatch complete (queuedFinal=${queuedFinal}, replies=${counts.final})`);
865+
log(
866+
`feishu[${account.accountId}]: dispatch complete (queuedFinal=${queuedFinal}, replies=${counts.final})`,
867+
);
840868
} catch (err) {
841-
error(`feishu: failed to dispatch message: ${String(err)}`);
869+
error(`feishu[${account.accountId}]: failed to dispatch message: ${String(err)}`);
842870
}
843871
}

0 commit comments

Comments
 (0)