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>
This commit is contained in:
Yifeng Wang
2026-02-05 19:18:25 +08:00
committed by cpojer
parent 7e005acd3c
commit 5f6e1c19bd
22 changed files with 785 additions and 369 deletions

View File

@@ -6,7 +6,8 @@ import {
DEFAULT_GROUP_HISTORY_LIMIT,
type HistoryEntry,
} from "openclaw/plugin-sdk";
import type { FeishuConfig, FeishuMessageContext, FeishuMediaInfo } from "./types.js";
import type { FeishuMessageContext, FeishuMediaInfo, ResolvedFeishuAccount } from "./types.js";
import { resolveFeishuAccount } from "./accounts.js";
import { createFeishuClient } from "./client.js";
import { downloadMessageResourceFeishu } from "./media.js";
import { extractMentionTargets, extractMessageBody, isMentionForwardRequest } from "./mention.js";
@@ -79,12 +80,13 @@ type SenderNameResult = {
};
async function resolveFeishuSenderName(params: {
feishuCfg?: FeishuConfig;
account: ResolvedFeishuAccount;
senderOpenId: string;
log: (...args: unknown[]) => void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- generic log function
log: (...args: any[]) => void;
}): Promise<SenderNameResult> {
const { feishuCfg, senderOpenId, log } = params;
if (!feishuCfg) {
const { account, senderOpenId, log } = params;
if (!account.configured) {
return {};
}
if (!senderOpenId) {
@@ -98,10 +100,11 @@ async function resolveFeishuSenderName(params: {
}
try {
const client = createFeishuClient(feishuCfg);
const client = createFeishuClient(account);
// contact/v3/users/:user_id?user_id_type=open_id
const res = await client.contact.user.get({
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK response type
const res: any = await client.contact.user.get({
path: { user_id: senderOpenId },
params: { user_id_type: "open_id" },
});
@@ -325,8 +328,9 @@ async function resolveFeishuMediaList(params: {
content: string;
maxBytes: number;
log?: (msg: string) => void;
accountId?: string;
}): Promise<FeishuMediaInfo[]> {
const { cfg, messageId, messageType, content, maxBytes, log } = params;
const { cfg, messageId, messageType, content, maxBytes, log, accountId } = params;
// Only process media message types (including post for embedded images)
const mediaTypes = ["image", "file", "audio", "video", "sticker", "post"];
@@ -354,6 +358,7 @@ async function resolveFeishuMediaList(params: {
messageId,
fileKey: imageKey,
type: "image",
accountId,
});
let contentType = result.contentType;
@@ -407,6 +412,7 @@ async function resolveFeishuMediaList(params: {
messageId,
fileKey,
type: resourceType,
accountId,
});
buffer = result.buffer;
contentType = result.contentType;
@@ -506,9 +512,14 @@ export async function handleFeishuMessage(params: {
botOpenId?: string;
runtime?: RuntimeEnv;
chatHistories?: Map<string, HistoryEntry[]>;
accountId?: string;
}): Promise<void> {
const { cfg, event, botOpenId, runtime, chatHistories } = params;
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
const { cfg, event, botOpenId, runtime, chatHistories, accountId } = params;
// Resolve account with merged config
const account = resolveFeishuAccount({ cfg, accountId });
const feishuCfg = account.config;
const log = runtime?.log ?? console.log;
const error = runtime?.error ?? console.error;
@@ -517,7 +528,7 @@ export async function handleFeishuMessage(params: {
// Resolve sender display name (best-effort) so the agent can attribute messages correctly.
const senderResult = await resolveFeishuSenderName({
feishuCfg,
account,
senderOpenId: ctx.senderOpenId,
log,
});
@@ -528,7 +539,7 @@ export async function handleFeishuMessage(params: {
// Track permission error to inform agent later (with cooldown to avoid repetition)
let permissionErrorForAgent: PermissionError | undefined;
if (senderResult.permissionError) {
const appKey = feishuCfg?.appId ?? "default";
const appKey = account.appId ?? "default";
const now = Date.now();
const lastNotified = permissionErrorNotifiedAt.get(appKey) ?? 0;
@@ -538,12 +549,14 @@ export async function handleFeishuMessage(params: {
}
}
log(`feishu: received message from ${ctx.senderOpenId} in ${ctx.chatId} (${ctx.chatType})`);
log(
`feishu[${account.accountId}]: received message from ${ctx.senderOpenId} in ${ctx.chatId} (${ctx.chatType})`,
);
// Log mention targets if detected
if (ctx.mentionTargets && ctx.mentionTargets.length > 0) {
const names = ctx.mentionTargets.map((t) => t.name).join(", ");
log(`feishu: detected @ forward request, targets: [${names}]`);
log(`feishu[${account.accountId}]: detected @ forward request, targets: [${names}]`);
}
const historyLimit = Math.max(
@@ -554,6 +567,7 @@ export async function handleFeishuMessage(params: {
if (isGroup) {
const groupPolicy = feishuCfg?.groupPolicy ?? "open";
const groupAllowFrom = feishuCfg?.groupAllowFrom ?? [];
// DEBUG: log(`feishu[${account.accountId}]: groupPolicy=${groupPolicy}`);
const groupConfig = resolveFeishuGroupConfig({ cfg: feishuCfg, groupId: ctx.chatId });
// 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: {
});
if (!groupAllowed) {
log(`feishu: group ${ctx.chatId} not in allowlist`);
log(`feishu[${account.accountId}]: sender ${ctx.senderOpenId} not in group allowlist`);
return;
}
@@ -591,7 +605,9 @@ export async function handleFeishuMessage(params: {
});
if (requireMention && !ctx.mentionedBot) {
log(`feishu: message in group ${ctx.chatId} did not mention bot, recording to history`);
log(
`feishu[${account.accountId}]: message in group ${ctx.chatId} did not mention bot, recording to history`,
);
if (chatHistories) {
recordPendingHistoryEntryIfEnabled({
historyMap: chatHistories,
@@ -617,7 +633,7 @@ export async function handleFeishuMessage(params: {
senderId: ctx.senderOpenId,
});
if (!match.allowed) {
log(`feishu: sender ${ctx.senderOpenId} not in DM allowlist`);
log(`feishu[${account.accountId}]: sender ${ctx.senderOpenId} not in DM allowlist`);
return;
}
}
@@ -634,6 +650,7 @@ export async function handleFeishuMessage(params: {
const route = core.channel.routing.resolveAgentRoute({
cfg,
channel: "feishu",
accountId: account.accountId,
peer: {
kind: isGroup ? "group" : "dm",
id: isGroup ? ctx.chatId : ctx.senderOpenId,
@@ -642,8 +659,8 @@ export async function handleFeishuMessage(params: {
const preview = ctx.content.replace(/\s+/g, " ").slice(0, 160);
const inboundLabel = isGroup
? `Feishu message in group ${ctx.chatId}`
: `Feishu DM from ${ctx.senderOpenId}`;
? `Feishu[${account.accountId}] message in group ${ctx.chatId}`
: `Feishu[${account.accountId}] DM from ${ctx.senderOpenId}`;
core.system.enqueueSystemEvent(`${inboundLabel}: ${preview}`, {
sessionKey: route.sessionKey,
@@ -659,6 +676,7 @@ export async function handleFeishuMessage(params: {
content: event.message.content,
maxBytes: mediaMaxBytes,
log,
accountId: account.accountId,
});
const mediaPayload = buildFeishuMediaPayload(mediaList);
@@ -666,13 +684,19 @@ export async function handleFeishuMessage(params: {
let quotedContent: string | undefined;
if (ctx.parentId) {
try {
const quotedMsg = await getMessageFeishu({ cfg, messageId: ctx.parentId });
const quotedMsg = await getMessageFeishu({
cfg,
messageId: ctx.parentId,
accountId: account.accountId,
});
if (quotedMsg) {
quotedContent = quotedMsg.content;
log(`feishu: fetched quoted message: ${quotedContent?.slice(0, 100)}`);
log(
`feishu[${account.accountId}]: fetched quoted message: ${quotedContent?.slice(0, 100)}`,
);
}
} catch (err) {
log(`feishu: failed to fetch quoted message: ${String(err)}`);
log(`feishu[${account.accountId}]: failed to fetch quoted message: ${String(err)}`);
}
}
@@ -742,9 +766,10 @@ export async function handleFeishuMessage(params: {
runtime: runtime as RuntimeEnv,
chatId: ctx.chatId,
replyToMessageId: ctx.messageId,
accountId: account.accountId,
});
log(`feishu: dispatching permission error notification to agent`);
log(`feishu[${account.accountId}]: dispatching permission error notification to agent`);
await core.channel.reply.dispatchReplyFromConfig({
ctx: permissionCtx,
@@ -815,9 +840,10 @@ export async function handleFeishuMessage(params: {
chatId: ctx.chatId,
replyToMessageId: ctx.messageId,
mentionTargets: ctx.mentionTargets,
accountId: account.accountId,
});
log(`feishu: dispatching to agent (session=${route.sessionKey})`);
log(`feishu[${account.accountId}]: dispatching to agent (session=${route.sessionKey})`);
const { queuedFinal, counts } = await core.channel.reply.dispatchReplyFromConfig({
ctx: ctxPayload,
@@ -836,8 +862,10 @@ export async function handleFeishuMessage(params: {
});
}
log(`feishu: dispatch complete (queuedFinal=${queuedFinal}, replies=${counts.final})`);
log(
`feishu[${account.accountId}]: dispatch complete (queuedFinal=${queuedFinal}, replies=${counts.final})`,
);
} catch (err) {
error(`feishu: failed to dispatch message: ${String(err)}`);
error(`feishu[${account.accountId}]: failed to dispatch message: ${String(err)}`);
}
}