mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-17 07:37:33 +00:00
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:
@@ -1,7 +1,81 @@
|
||||
import type { ClawdbotConfig } from "openclaw/plugin-sdk";
|
||||
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk";
|
||||
import type { FeishuConfig, FeishuDomain, ResolvedFeishuAccount } from "./types.js";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk";
|
||||
import type {
|
||||
FeishuConfig,
|
||||
FeishuAccountConfig,
|
||||
FeishuDomain,
|
||||
ResolvedFeishuAccount,
|
||||
} from "./types.js";
|
||||
|
||||
/**
|
||||
* List all configured account IDs from the accounts field.
|
||||
*/
|
||||
function listConfiguredAccountIds(cfg: ClawdbotConfig): string[] {
|
||||
const accounts = (cfg.channels?.feishu as FeishuConfig)?.accounts;
|
||||
if (!accounts || typeof accounts !== "object") {
|
||||
return [];
|
||||
}
|
||||
return Object.keys(accounts).filter(Boolean);
|
||||
}
|
||||
|
||||
/**
|
||||
* List all Feishu account IDs.
|
||||
* If no accounts are configured, returns [DEFAULT_ACCOUNT_ID] for backward compatibility.
|
||||
*/
|
||||
export function listFeishuAccountIds(cfg: ClawdbotConfig): string[] {
|
||||
const ids = listConfiguredAccountIds(cfg);
|
||||
if (ids.length === 0) {
|
||||
// Backward compatibility: no accounts configured, use default
|
||||
return [DEFAULT_ACCOUNT_ID];
|
||||
}
|
||||
return [...ids].toSorted((a, b) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the default account ID.
|
||||
*/
|
||||
export function resolveDefaultFeishuAccountId(cfg: ClawdbotConfig): string {
|
||||
const ids = listFeishuAccountIds(cfg);
|
||||
if (ids.includes(DEFAULT_ACCOUNT_ID)) {
|
||||
return DEFAULT_ACCOUNT_ID;
|
||||
}
|
||||
return ids[0] ?? DEFAULT_ACCOUNT_ID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the raw account-specific config.
|
||||
*/
|
||||
function resolveAccountConfig(
|
||||
cfg: ClawdbotConfig,
|
||||
accountId: string,
|
||||
): FeishuAccountConfig | undefined {
|
||||
const accounts = (cfg.channels?.feishu as FeishuConfig)?.accounts;
|
||||
if (!accounts || typeof accounts !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
return accounts[accountId];
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge top-level config with account-specific config.
|
||||
* Account-specific fields override top-level fields.
|
||||
*/
|
||||
function mergeFeishuAccountConfig(cfg: ClawdbotConfig, accountId: string): FeishuConfig {
|
||||
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
||||
|
||||
// Extract base config (exclude accounts field to avoid recursion)
|
||||
const { accounts: _ignored, ...base } = feishuCfg ?? {};
|
||||
|
||||
// Get account-specific overrides
|
||||
const account = resolveAccountConfig(cfg, accountId) ?? {};
|
||||
|
||||
// Merge: account config overrides base config
|
||||
return { ...base, ...account } as FeishuConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve Feishu credentials from a config.
|
||||
*/
|
||||
export function resolveFeishuCredentials(cfg?: FeishuConfig): {
|
||||
appId: string;
|
||||
appSecret: string;
|
||||
@@ -23,31 +97,46 @@ export function resolveFeishuCredentials(cfg?: FeishuConfig): {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a complete Feishu account with merged config.
|
||||
*/
|
||||
export function resolveFeishuAccount(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
accountId?: string | null;
|
||||
}): ResolvedFeishuAccount {
|
||||
const accountId = normalizeAccountId(params.accountId);
|
||||
const feishuCfg = params.cfg.channels?.feishu as FeishuConfig | undefined;
|
||||
const enabled = feishuCfg?.enabled !== false;
|
||||
const creds = resolveFeishuCredentials(feishuCfg);
|
||||
|
||||
// Base enabled state (top-level)
|
||||
const baseEnabled = feishuCfg?.enabled !== false;
|
||||
|
||||
// Merge configs
|
||||
const merged = mergeFeishuAccountConfig(params.cfg, accountId);
|
||||
|
||||
// Account-level enabled state
|
||||
const accountEnabled = merged.enabled !== false;
|
||||
const enabled = baseEnabled && accountEnabled;
|
||||
|
||||
// Resolve credentials from merged config
|
||||
const creds = resolveFeishuCredentials(merged);
|
||||
|
||||
return {
|
||||
accountId: params.accountId?.trim() || DEFAULT_ACCOUNT_ID,
|
||||
accountId,
|
||||
enabled,
|
||||
configured: Boolean(creds),
|
||||
name: (merged as FeishuAccountConfig).name?.trim() || undefined,
|
||||
appId: creds?.appId,
|
||||
appSecret: creds?.appSecret,
|
||||
encryptKey: creds?.encryptKey,
|
||||
verificationToken: creds?.verificationToken,
|
||||
domain: creds?.domain ?? "feishu",
|
||||
config: merged,
|
||||
};
|
||||
}
|
||||
|
||||
export function listFeishuAccountIds(_cfg: ClawdbotConfig): string[] {
|
||||
return [DEFAULT_ACCOUNT_ID];
|
||||
}
|
||||
|
||||
export function resolveDefaultFeishuAccountId(_cfg: ClawdbotConfig): string {
|
||||
return DEFAULT_ACCOUNT_ID;
|
||||
}
|
||||
|
||||
/**
|
||||
* List all enabled and configured accounts.
|
||||
*/
|
||||
export function listEnabledFeishuAccounts(cfg: ClawdbotConfig): ResolvedFeishuAccount[] {
|
||||
return listFeishuAccountIds(cfg)
|
||||
.map((accountId) => resolveFeishuAccount({ cfg, accountId }))
|
||||
|
||||
@@ -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)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import type { ChannelPlugin, ClawdbotConfig } from "openclaw/plugin-sdk";
|
||||
import { DEFAULT_ACCOUNT_ID, PAIRING_APPROVED_MESSAGE } from "openclaw/plugin-sdk";
|
||||
import type { ResolvedFeishuAccount, FeishuConfig } from "./types.js";
|
||||
import { resolveFeishuAccount, resolveFeishuCredentials } from "./accounts.js";
|
||||
import {
|
||||
resolveFeishuAccount,
|
||||
listFeishuAccountIds,
|
||||
resolveDefaultFeishuAccountId,
|
||||
} from "./accounts.js";
|
||||
import {
|
||||
listFeishuDirectoryPeers,
|
||||
listFeishuDirectoryGroups,
|
||||
@@ -34,11 +38,12 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
||||
pairing: {
|
||||
idLabel: "feishuUserId",
|
||||
normalizeAllowEntry: (entry) => entry.replace(/^(feishu|user|open_id):/i, ""),
|
||||
notifyApproval: async ({ cfg, id }) => {
|
||||
notifyApproval: async ({ cfg, id, accountId }) => {
|
||||
await sendMessageFeishu({
|
||||
cfg,
|
||||
to: id,
|
||||
text: PAIRING_APPROVED_MESSAGE,
|
||||
accountId,
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -94,43 +99,111 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
||||
chunkMode: { type: "string", enum: ["length", "newline"] },
|
||||
mediaMaxMb: { type: "number", minimum: 0 },
|
||||
renderMode: { type: "string", enum: ["auto", "raw", "card"] },
|
||||
accounts: {
|
||||
type: "object",
|
||||
additionalProperties: {
|
||||
type: "object",
|
||||
properties: {
|
||||
enabled: { type: "boolean" },
|
||||
name: { type: "string" },
|
||||
appId: { type: "string" },
|
||||
appSecret: { type: "string" },
|
||||
encryptKey: { type: "string" },
|
||||
verificationToken: { type: "string" },
|
||||
domain: { type: "string", enum: ["feishu", "lark"] },
|
||||
connectionMode: { type: "string", enum: ["websocket", "webhook"] },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
config: {
|
||||
listAccountIds: () => [DEFAULT_ACCOUNT_ID],
|
||||
resolveAccount: (cfg) => resolveFeishuAccount({ cfg }),
|
||||
defaultAccountId: () => DEFAULT_ACCOUNT_ID,
|
||||
setAccountEnabled: ({ cfg, enabled }) => ({
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
feishu: {
|
||||
...cfg.channels?.feishu,
|
||||
enabled,
|
||||
},
|
||||
},
|
||||
}),
|
||||
deleteAccount: ({ cfg }) => {
|
||||
const next = { ...cfg } as ClawdbotConfig;
|
||||
const nextChannels = { ...cfg.channels };
|
||||
delete (nextChannels as Record<string, unknown>).feishu;
|
||||
if (Object.keys(nextChannels).length > 0) {
|
||||
next.channels = nextChannels;
|
||||
} else {
|
||||
delete next.channels;
|
||||
listAccountIds: (cfg) => listFeishuAccountIds(cfg),
|
||||
resolveAccount: (cfg, accountId) => resolveFeishuAccount({ cfg, accountId }),
|
||||
defaultAccountId: (cfg) => resolveDefaultFeishuAccountId(cfg),
|
||||
setAccountEnabled: ({ cfg, accountId, enabled }) => {
|
||||
const _account = resolveFeishuAccount({ cfg, accountId });
|
||||
const isDefault = accountId === DEFAULT_ACCOUNT_ID;
|
||||
|
||||
if (isDefault) {
|
||||
// For default account, set top-level enabled
|
||||
return {
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
feishu: {
|
||||
...cfg.channels?.feishu,
|
||||
enabled,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
return next;
|
||||
|
||||
// For named accounts, set enabled in accounts[accountId]
|
||||
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
||||
return {
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
feishu: {
|
||||
...feishuCfg,
|
||||
accounts: {
|
||||
...feishuCfg?.accounts,
|
||||
[accountId]: {
|
||||
...feishuCfg?.accounts?.[accountId],
|
||||
enabled,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
isConfigured: (_account, cfg) =>
|
||||
Boolean(resolveFeishuCredentials(cfg.channels?.feishu as FeishuConfig | undefined)),
|
||||
deleteAccount: ({ cfg, accountId }) => {
|
||||
const isDefault = accountId === DEFAULT_ACCOUNT_ID;
|
||||
|
||||
if (isDefault) {
|
||||
// Delete entire feishu config
|
||||
const next = { ...cfg } as ClawdbotConfig;
|
||||
const nextChannels = { ...cfg.channels };
|
||||
delete (nextChannels as Record<string, unknown>).feishu;
|
||||
if (Object.keys(nextChannels).length > 0) {
|
||||
next.channels = nextChannels;
|
||||
} else {
|
||||
delete next.channels;
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
// Delete specific account from accounts
|
||||
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
||||
const accounts = { ...feishuCfg?.accounts };
|
||||
delete accounts[accountId];
|
||||
|
||||
return {
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
feishu: {
|
||||
...feishuCfg,
|
||||
accounts: Object.keys(accounts).length > 0 ? accounts : undefined,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
isConfigured: (account) => account.configured,
|
||||
describeAccount: (account) => ({
|
||||
accountId: account.accountId,
|
||||
enabled: account.enabled,
|
||||
configured: account.configured,
|
||||
name: account.name,
|
||||
appId: account.appId,
|
||||
domain: account.domain,
|
||||
}),
|
||||
resolveAllowFrom: ({ cfg }) =>
|
||||
(cfg.channels?.feishu as FeishuConfig | undefined)?.allowFrom ?? [],
|
||||
resolveAllowFrom: ({ cfg, accountId }) => {
|
||||
const account = resolveFeishuAccount({ cfg, accountId });
|
||||
return account.config?.allowFrom ?? [];
|
||||
},
|
||||
formatAllowFrom: ({ allowFrom }) =>
|
||||
allowFrom
|
||||
.map((entry) => String(entry).trim())
|
||||
@@ -138,8 +211,9 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
||||
.map((entry) => entry.toLowerCase()),
|
||||
},
|
||||
security: {
|
||||
collectWarnings: ({ cfg }) => {
|
||||
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
||||
collectWarnings: ({ cfg, accountId }) => {
|
||||
const account = resolveFeishuAccount({ cfg, accountId });
|
||||
const feishuCfg = account.config;
|
||||
const defaultGroupPolicy = (
|
||||
cfg.channels as Record<string, { groupPolicy?: string }> | undefined
|
||||
)?.defaults?.groupPolicy;
|
||||
@@ -148,22 +222,46 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
`- Feishu groups: groupPolicy="open" allows any member to trigger (mention-gated). Set channels.feishu.groupPolicy="allowlist" + channels.feishu.groupAllowFrom to restrict senders.`,
|
||||
`- Feishu[${account.accountId}] groups: groupPolicy="open" allows any member to trigger (mention-gated). Set channels.feishu.groupPolicy="allowlist" + channels.feishu.groupAllowFrom to restrict senders.`,
|
||||
];
|
||||
},
|
||||
},
|
||||
setup: {
|
||||
resolveAccountId: () => DEFAULT_ACCOUNT_ID,
|
||||
applyAccountConfig: ({ cfg }) => ({
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
feishu: {
|
||||
...cfg.channels?.feishu,
|
||||
enabled: true,
|
||||
applyAccountConfig: ({ cfg, accountId }) => {
|
||||
const isDefault = !accountId || accountId === DEFAULT_ACCOUNT_ID;
|
||||
|
||||
if (isDefault) {
|
||||
return {
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
feishu: {
|
||||
...cfg.channels?.feishu,
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
||||
return {
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
feishu: {
|
||||
...feishuCfg,
|
||||
accounts: {
|
||||
...feishuCfg?.accounts,
|
||||
[accountId]: {
|
||||
...feishuCfg?.accounts?.[accountId],
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
},
|
||||
},
|
||||
onboarding: feishuOnboardingAdapter,
|
||||
messaging: {
|
||||
@@ -175,12 +273,14 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
||||
},
|
||||
directory: {
|
||||
self: async () => null,
|
||||
listPeers: async ({ cfg, query, limit }) => listFeishuDirectoryPeers({ cfg, query, limit }),
|
||||
listGroups: async ({ cfg, query, limit }) => listFeishuDirectoryGroups({ cfg, query, limit }),
|
||||
listPeersLive: async ({ cfg, query, limit }) =>
|
||||
listFeishuDirectoryPeersLive({ cfg, query, limit }),
|
||||
listGroupsLive: async ({ cfg, query, limit }) =>
|
||||
listFeishuDirectoryGroupsLive({ cfg, query, limit }),
|
||||
listPeers: async ({ cfg, query, limit, accountId }) =>
|
||||
listFeishuDirectoryPeers({ cfg, query, limit, accountId }),
|
||||
listGroups: async ({ cfg, query, limit, accountId }) =>
|
||||
listFeishuDirectoryGroups({ cfg, query, limit, accountId }),
|
||||
listPeersLive: async ({ cfg, query, limit, accountId }) =>
|
||||
listFeishuDirectoryPeersLive({ cfg, query, limit, accountId }),
|
||||
listGroupsLive: async ({ cfg, query, limit, accountId }) =>
|
||||
listFeishuDirectoryGroupsLive({ cfg, query, limit, accountId }),
|
||||
},
|
||||
outbound: feishuOutbound,
|
||||
status: {
|
||||
@@ -202,12 +302,17 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
||||
probe: snapshot.probe,
|
||||
lastProbeAt: snapshot.lastProbeAt ?? null,
|
||||
}),
|
||||
probeAccount: async ({ cfg }) =>
|
||||
await probeFeishu(cfg.channels?.feishu as FeishuConfig | undefined),
|
||||
probeAccount: async ({ cfg, accountId }) => {
|
||||
const account = resolveFeishuAccount({ cfg, accountId });
|
||||
return await probeFeishu(account);
|
||||
},
|
||||
buildAccountSnapshot: ({ account, runtime, probe }) => ({
|
||||
accountId: account.accountId,
|
||||
enabled: account.enabled,
|
||||
configured: account.configured,
|
||||
name: account.name,
|
||||
appId: account.appId,
|
||||
domain: account.domain,
|
||||
running: runtime?.running ?? false,
|
||||
lastStartAt: runtime?.lastStartAt ?? null,
|
||||
lastStopAt: runtime?.lastStopAt ?? null,
|
||||
@@ -219,10 +324,12 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
||||
gateway: {
|
||||
startAccount: async (ctx) => {
|
||||
const { monitorFeishuProvider } = await import("./monitor.js");
|
||||
const feishuCfg = ctx.cfg.channels?.feishu as FeishuConfig | undefined;
|
||||
const port = feishuCfg?.webhookPort ?? null;
|
||||
const account = resolveFeishuAccount({ cfg: ctx.cfg, accountId: ctx.accountId });
|
||||
const port = account.config?.webhookPort ?? null;
|
||||
ctx.setStatus({ accountId: ctx.accountId, port });
|
||||
ctx.log?.info(`starting feishu provider (mode: ${feishuCfg?.connectionMode ?? "websocket"})`);
|
||||
ctx.log?.info(
|
||||
`starting feishu[${ctx.accountId}] (mode: ${account.config?.connectionMode ?? "websocket"})`,
|
||||
);
|
||||
return monitorFeishuProvider({
|
||||
config: ctx.cfg,
|
||||
runtime: ctx.runtime,
|
||||
|
||||
@@ -1,72 +1,118 @@
|
||||
import * as Lark from "@larksuiteoapi/node-sdk";
|
||||
import type { FeishuConfig, FeishuDomain } from "./types.js";
|
||||
import { resolveFeishuCredentials } from "./accounts.js";
|
||||
import type { FeishuDomain, ResolvedFeishuAccount } from "./types.js";
|
||||
|
||||
let cachedClient: Lark.Client | null = null;
|
||||
let cachedConfig: { appId: string; appSecret: string; domain: FeishuDomain } | null = null;
|
||||
// Multi-account client cache
|
||||
const clientCache = new Map<
|
||||
string,
|
||||
{
|
||||
client: Lark.Client;
|
||||
config: { appId: string; appSecret: string; domain?: FeishuDomain };
|
||||
}
|
||||
>();
|
||||
|
||||
function resolveDomain(domain: FeishuDomain): Lark.Domain | string {
|
||||
function resolveDomain(domain: FeishuDomain | undefined): Lark.Domain | string {
|
||||
if (domain === "lark") {
|
||||
return Lark.Domain.Lark;
|
||||
}
|
||||
if (domain === "feishu") {
|
||||
if (domain === "feishu" || !domain) {
|
||||
return Lark.Domain.Feishu;
|
||||
}
|
||||
return domain.replace(/\/+$/, ""); // Custom URL, remove trailing slashes
|
||||
return domain.replace(/\/+$/, ""); // Custom URL for private deployment
|
||||
}
|
||||
|
||||
export function createFeishuClient(cfg: FeishuConfig): Lark.Client {
|
||||
const creds = resolveFeishuCredentials(cfg);
|
||||
if (!creds) {
|
||||
throw new Error("Feishu credentials not configured (appId, appSecret required)");
|
||||
/**
|
||||
* Credentials needed to create a Feishu client.
|
||||
* Both FeishuConfig and ResolvedFeishuAccount satisfy this interface.
|
||||
*/
|
||||
export type FeishuClientCredentials = {
|
||||
accountId?: string;
|
||||
appId?: string;
|
||||
appSecret?: string;
|
||||
domain?: FeishuDomain;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create or get a cached Feishu client for an account.
|
||||
* Accepts any object with appId, appSecret, and optional domain/accountId.
|
||||
*/
|
||||
export function createFeishuClient(creds: FeishuClientCredentials): Lark.Client {
|
||||
const { accountId = "default", appId, appSecret, domain } = creds;
|
||||
|
||||
if (!appId || !appSecret) {
|
||||
throw new Error(`Feishu credentials not configured for account "${accountId}"`);
|
||||
}
|
||||
|
||||
// Check cache
|
||||
const cached = clientCache.get(accountId);
|
||||
if (
|
||||
cachedClient &&
|
||||
cachedConfig &&
|
||||
cachedConfig.appId === creds.appId &&
|
||||
cachedConfig.appSecret === creds.appSecret &&
|
||||
cachedConfig.domain === creds.domain
|
||||
cached &&
|
||||
cached.config.appId === appId &&
|
||||
cached.config.appSecret === appSecret &&
|
||||
cached.config.domain === domain
|
||||
) {
|
||||
return cachedClient;
|
||||
return cached.client;
|
||||
}
|
||||
|
||||
// Create new client
|
||||
const client = new Lark.Client({
|
||||
appId: creds.appId,
|
||||
appSecret: creds.appSecret,
|
||||
appId,
|
||||
appSecret,
|
||||
appType: Lark.AppType.SelfBuild,
|
||||
domain: resolveDomain(creds.domain),
|
||||
domain: resolveDomain(domain),
|
||||
});
|
||||
|
||||
cachedClient = client;
|
||||
cachedConfig = { appId: creds.appId, appSecret: creds.appSecret, domain: creds.domain };
|
||||
// Cache it
|
||||
clientCache.set(accountId, {
|
||||
client,
|
||||
config: { appId, appSecret, domain },
|
||||
});
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
export function createFeishuWSClient(cfg: FeishuConfig): Lark.WSClient {
|
||||
const creds = resolveFeishuCredentials(cfg);
|
||||
if (!creds) {
|
||||
throw new Error("Feishu credentials not configured (appId, appSecret required)");
|
||||
/**
|
||||
* Create a Feishu WebSocket client for an account.
|
||||
* Note: WSClient is not cached since each call creates a new connection.
|
||||
*/
|
||||
export function createFeishuWSClient(account: ResolvedFeishuAccount): Lark.WSClient {
|
||||
const { accountId, appId, appSecret, domain } = account;
|
||||
|
||||
if (!appId || !appSecret) {
|
||||
throw new Error(`Feishu credentials not configured for account "${accountId}"`);
|
||||
}
|
||||
|
||||
return new Lark.WSClient({
|
||||
appId: creds.appId,
|
||||
appSecret: creds.appSecret,
|
||||
domain: resolveDomain(creds.domain),
|
||||
appId,
|
||||
appSecret,
|
||||
domain: resolveDomain(domain),
|
||||
loggerLevel: Lark.LoggerLevel.info,
|
||||
});
|
||||
}
|
||||
|
||||
export function createEventDispatcher(cfg: FeishuConfig): Lark.EventDispatcher {
|
||||
const creds = resolveFeishuCredentials(cfg);
|
||||
/**
|
||||
* Create an event dispatcher for an account.
|
||||
*/
|
||||
export function createEventDispatcher(account: ResolvedFeishuAccount): Lark.EventDispatcher {
|
||||
return new Lark.EventDispatcher({
|
||||
encryptKey: creds?.encryptKey,
|
||||
verificationToken: creds?.verificationToken,
|
||||
encryptKey: account.encryptKey,
|
||||
verificationToken: account.verificationToken,
|
||||
});
|
||||
}
|
||||
|
||||
export function clearClientCache() {
|
||||
cachedClient = null;
|
||||
cachedConfig = null;
|
||||
/**
|
||||
* Get a cached client for an account (if exists).
|
||||
*/
|
||||
export function getFeishuClient(accountId: string): Lark.Client | null {
|
||||
return clientCache.get(accountId)?.client ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear client cache for a specific account or all accounts.
|
||||
*/
|
||||
export function clearClientCache(accountId?: string): void {
|
||||
if (accountId) {
|
||||
clientCache.delete(accountId);
|
||||
} else {
|
||||
clientCache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,9 +83,48 @@ export const FeishuGroupSchema = z
|
||||
})
|
||||
.strict();
|
||||
|
||||
/**
|
||||
* Per-account configuration.
|
||||
* All fields are optional - missing fields inherit from top-level config.
|
||||
*/
|
||||
export const FeishuAccountConfigSchema = z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
name: z.string().optional(), // Display name for this account
|
||||
appId: z.string().optional(),
|
||||
appSecret: z.string().optional(),
|
||||
encryptKey: z.string().optional(),
|
||||
verificationToken: z.string().optional(),
|
||||
domain: FeishuDomainSchema.optional(),
|
||||
connectionMode: FeishuConnectionModeSchema.optional(),
|
||||
webhookPath: z.string().optional(),
|
||||
webhookPort: z.number().int().positive().optional(),
|
||||
capabilities: z.array(z.string()).optional(),
|
||||
markdown: MarkdownConfigSchema,
|
||||
configWrites: z.boolean().optional(),
|
||||
dmPolicy: DmPolicySchema.optional(),
|
||||
allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
groupPolicy: GroupPolicySchema.optional(),
|
||||
groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
requireMention: z.boolean().optional(),
|
||||
groups: z.record(z.string(), FeishuGroupSchema.optional()).optional(),
|
||||
historyLimit: z.number().int().min(0).optional(),
|
||||
dmHistoryLimit: z.number().int().min(0).optional(),
|
||||
dms: z.record(z.string(), DmConfigSchema).optional(),
|
||||
textChunkLimit: z.number().int().positive().optional(),
|
||||
chunkMode: z.enum(["length", "newline"]).optional(),
|
||||
blockStreamingCoalesce: BlockStreamingCoalesceSchema,
|
||||
mediaMaxMb: z.number().positive().optional(),
|
||||
heartbeat: ChannelHeartbeatVisibilitySchema,
|
||||
renderMode: RenderModeSchema,
|
||||
tools: FeishuToolsConfigSchema,
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const FeishuConfigSchema = z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
// Top-level credentials (backward compatible for single-account mode)
|
||||
appId: z.string().optional(),
|
||||
appSecret: z.string().optional(),
|
||||
encryptKey: z.string().optional(),
|
||||
@@ -113,6 +152,8 @@ export const FeishuConfigSchema = z
|
||||
heartbeat: ChannelHeartbeatVisibilitySchema,
|
||||
renderMode: RenderModeSchema, // raw = plain text (default), card = interactive card with markdown
|
||||
tools: FeishuToolsConfigSchema,
|
||||
// Multi-account configuration
|
||||
accounts: z.record(z.string(), FeishuAccountConfigSchema.optional()).optional(),
|
||||
})
|
||||
.strict()
|
||||
.superRefine((value, ctx) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ClawdbotConfig } from "openclaw/plugin-sdk";
|
||||
import type { FeishuConfig } from "./types.js";
|
||||
import { resolveFeishuAccount } from "./accounts.js";
|
||||
import { createFeishuClient } from "./client.js";
|
||||
import { normalizeFeishuTarget } from "./targets.js";
|
||||
|
||||
@@ -19,8 +19,10 @@ export async function listFeishuDirectoryPeers(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
query?: string;
|
||||
limit?: number;
|
||||
accountId?: string;
|
||||
}): Promise<FeishuDirectoryPeer[]> {
|
||||
const feishuCfg = params.cfg.channels?.feishu as FeishuConfig | undefined;
|
||||
const account = resolveFeishuAccount({ cfg: params.cfg, accountId: params.accountId });
|
||||
const feishuCfg = account.config;
|
||||
const q = params.query?.trim().toLowerCase() || "";
|
||||
const ids = new Set<string>();
|
||||
|
||||
@@ -51,8 +53,10 @@ export async function listFeishuDirectoryGroups(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
query?: string;
|
||||
limit?: number;
|
||||
accountId?: string;
|
||||
}): Promise<FeishuDirectoryGroup[]> {
|
||||
const feishuCfg = params.cfg.channels?.feishu as FeishuConfig | undefined;
|
||||
const account = resolveFeishuAccount({ cfg: params.cfg, accountId: params.accountId });
|
||||
const feishuCfg = account.config;
|
||||
const q = params.query?.trim().toLowerCase() || "";
|
||||
const ids = new Set<string>();
|
||||
|
||||
@@ -82,14 +86,15 @@ export async function listFeishuDirectoryPeersLive(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
query?: string;
|
||||
limit?: number;
|
||||
accountId?: string;
|
||||
}): Promise<FeishuDirectoryPeer[]> {
|
||||
const feishuCfg = params.cfg.channels?.feishu as FeishuConfig | undefined;
|
||||
if (!feishuCfg?.appId || !feishuCfg?.appSecret) {
|
||||
const account = resolveFeishuAccount({ cfg: params.cfg, accountId: params.accountId });
|
||||
if (!account.configured) {
|
||||
return listFeishuDirectoryPeers(params);
|
||||
}
|
||||
|
||||
try {
|
||||
const client = createFeishuClient(feishuCfg);
|
||||
const client = createFeishuClient(account);
|
||||
const peers: FeishuDirectoryPeer[] = [];
|
||||
const limit = params.limit ?? 50;
|
||||
|
||||
@@ -128,14 +133,15 @@ export async function listFeishuDirectoryGroupsLive(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
query?: string;
|
||||
limit?: number;
|
||||
accountId?: string;
|
||||
}): Promise<FeishuDirectoryGroup[]> {
|
||||
const feishuCfg = params.cfg.channels?.feishu as FeishuConfig | undefined;
|
||||
if (!feishuCfg?.appId || !feishuCfg?.appSecret) {
|
||||
const account = resolveFeishuAccount({ cfg: params.cfg, accountId: params.accountId });
|
||||
if (!account.configured) {
|
||||
return listFeishuDirectoryGroups(params);
|
||||
}
|
||||
|
||||
try {
|
||||
const client = createFeishuClient(feishuCfg);
|
||||
const client = createFeishuClient(account);
|
||||
const groups: FeishuDirectoryGroup[] = [];
|
||||
const limit = params.limit ?? 50;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import type * as Lark from "@larksuiteoapi/node-sdk";
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { Readable } from "stream";
|
||||
import type { FeishuConfig } from "./types.js";
|
||||
import { listEnabledFeishuAccounts } from "./accounts.js";
|
||||
import { createFeishuClient } from "./client.js";
|
||||
import { FeishuDocSchema, type FeishuDocParams } from "./doc-schema.js";
|
||||
import { resolveToolsConfig } from "./tools-config.js";
|
||||
@@ -55,8 +55,8 @@ const BLOCK_TYPE_NAMES: Record<number, string> = {
|
||||
const UNSUPPORTED_CREATE_TYPES = new Set([31, 32]);
|
||||
|
||||
/** Clean blocks for insertion (remove unsupported types and read-only fields) */
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK block type
|
||||
function cleanBlocksForInsert(blocks: any[]): { cleaned: unknown[]; skipped: string[] } {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK block types
|
||||
function cleanBlocksForInsert(blocks: any[]): { cleaned: any[]; skipped: string[] } {
|
||||
const skipped: string[] = [];
|
||||
const cleaned = blocks
|
||||
.filter((block) => {
|
||||
@@ -92,13 +92,14 @@ async function convertMarkdown(client: Lark.Client, markdown: string) {
|
||||
};
|
||||
}
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any -- SDK block types */
|
||||
async function insertBlocks(
|
||||
client: Lark.Client,
|
||||
docToken: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK block type
|
||||
blocks: any[],
|
||||
parentBlockId?: string,
|
||||
): Promise<{ children: unknown[]; skipped: string[] }> {
|
||||
): Promise<{ children: any[]; skipped: string[] }> {
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
const { cleaned, skipped } = cleanBlocksForInsert(blocks);
|
||||
const blockId = parentBlockId ?? docToken;
|
||||
|
||||
@@ -154,7 +155,7 @@ async function uploadImageToDocx(
|
||||
parent_type: "docx_image",
|
||||
parent_node: blockId,
|
||||
size: imageBuffer.length,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK expects stream
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK stream type
|
||||
file: Readable.from(imageBuffer) as any,
|
||||
},
|
||||
});
|
||||
@@ -174,13 +175,14 @@ async function downloadImage(url: string): Promise<Buffer> {
|
||||
return Buffer.from(await response.arrayBuffer());
|
||||
}
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any -- SDK block types */
|
||||
async function processImages(
|
||||
client: Lark.Client,
|
||||
docToken: string,
|
||||
markdown: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK block type
|
||||
insertedBlocks: any[],
|
||||
): Promise<number> {
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
const imageUrls = extractImageUrls(markdown);
|
||||
if (imageUrls.length === 0) {
|
||||
return 0;
|
||||
@@ -426,14 +428,24 @@ async function listAppScopes(client: Lark.Client) {
|
||||
// ============ Tool Registration ============
|
||||
|
||||
export function registerFeishuDocTools(api: OpenClawPluginApi) {
|
||||
const feishuCfg = api.config?.channels?.feishu as FeishuConfig | undefined;
|
||||
if (!feishuCfg?.appId || !feishuCfg?.appSecret) {
|
||||
api.logger.debug?.("feishu_doc: Feishu credentials not configured, skipping doc tools");
|
||||
if (!api.config) {
|
||||
api.logger.debug?.("feishu_doc: No config available, skipping doc tools");
|
||||
return;
|
||||
}
|
||||
|
||||
const toolsCfg = resolveToolsConfig(feishuCfg.tools);
|
||||
const getClient = () => createFeishuClient(feishuCfg);
|
||||
// Check if any account is configured
|
||||
const accounts = listEnabledFeishuAccounts(api.config);
|
||||
if (accounts.length === 0) {
|
||||
api.logger.debug?.("feishu_doc: No Feishu accounts configured, skipping doc tools");
|
||||
return;
|
||||
}
|
||||
|
||||
// Use first account's config for tools configuration
|
||||
const firstAccount = accounts[0];
|
||||
const toolsCfg = resolveToolsConfig(firstAccount.config.tools);
|
||||
|
||||
// Helper to get client for the default account
|
||||
const getClient = () => createFeishuClient(firstAccount);
|
||||
const registered: string[] = [];
|
||||
|
||||
// Main document tool with action-based dispatch
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type * as Lark from "@larksuiteoapi/node-sdk";
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||
import type { FeishuConfig } from "./types.js";
|
||||
import { listEnabledFeishuAccounts } from "./accounts.js";
|
||||
import { createFeishuClient } from "./client.js";
|
||||
import { FeishuDriveSchema, type FeishuDriveParams } from "./drive-schema.js";
|
||||
import { resolveToolsConfig } from "./tools-config.js";
|
||||
@@ -169,19 +169,25 @@ async function deleteFile(client: Lark.Client, fileToken: string, type: string)
|
||||
// ============ Tool Registration ============
|
||||
|
||||
export function registerFeishuDriveTools(api: OpenClawPluginApi) {
|
||||
const feishuCfg = api.config?.channels?.feishu as FeishuConfig | undefined;
|
||||
if (!feishuCfg?.appId || !feishuCfg?.appSecret) {
|
||||
api.logger.debug?.("feishu_drive: Feishu credentials not configured, skipping drive tools");
|
||||
if (!api.config) {
|
||||
api.logger.debug?.("feishu_drive: No config available, skipping drive tools");
|
||||
return;
|
||||
}
|
||||
|
||||
const toolsCfg = resolveToolsConfig(feishuCfg.tools);
|
||||
const accounts = listEnabledFeishuAccounts(api.config);
|
||||
if (accounts.length === 0) {
|
||||
api.logger.debug?.("feishu_drive: No Feishu accounts configured, skipping drive tools");
|
||||
return;
|
||||
}
|
||||
|
||||
const firstAccount = accounts[0];
|
||||
const toolsCfg = resolveToolsConfig(firstAccount.config.tools);
|
||||
if (!toolsCfg.drive) {
|
||||
api.logger.debug?.("feishu_drive: drive tool disabled in config");
|
||||
return;
|
||||
}
|
||||
|
||||
const getClient = () => createFeishuClient(feishuCfg);
|
||||
const getClient = () => createFeishuClient(firstAccount);
|
||||
|
||||
api.registerTool(
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@ import fs from "fs";
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
import { Readable } from "stream";
|
||||
import type { FeishuConfig } from "./types.js";
|
||||
import { resolveFeishuAccount } from "./accounts.js";
|
||||
import { createFeishuClient } from "./client.js";
|
||||
import { resolveReceiveIdType, normalizeFeishuTarget } from "./targets.js";
|
||||
|
||||
@@ -25,20 +25,21 @@ export type DownloadMessageResourceResult = {
|
||||
export async function downloadImageFeishu(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
imageKey: string;
|
||||
accountId?: string;
|
||||
}): Promise<DownloadImageResult> {
|
||||
const { cfg, imageKey } = params;
|
||||
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
||||
if (!feishuCfg) {
|
||||
throw new Error("Feishu channel not configured");
|
||||
const { cfg, imageKey, accountId } = params;
|
||||
const account = resolveFeishuAccount({ cfg, accountId });
|
||||
if (!account.configured) {
|
||||
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
||||
}
|
||||
|
||||
const client = createFeishuClient(feishuCfg);
|
||||
const client = createFeishuClient(account);
|
||||
|
||||
const response = await client.im.image.get({
|
||||
path: { image_key: imageKey },
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK response type varies
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK response type
|
||||
const responseAny = response as any;
|
||||
if (responseAny.code !== undefined && responseAny.code !== 0) {
|
||||
throw new Error(
|
||||
@@ -104,21 +105,22 @@ export async function downloadMessageResourceFeishu(params: {
|
||||
messageId: string;
|
||||
fileKey: string;
|
||||
type: "image" | "file";
|
||||
accountId?: string;
|
||||
}): Promise<DownloadMessageResourceResult> {
|
||||
const { cfg, messageId, fileKey, type } = params;
|
||||
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
||||
if (!feishuCfg) {
|
||||
throw new Error("Feishu channel not configured");
|
||||
const { cfg, messageId, fileKey, type, accountId } = params;
|
||||
const account = resolveFeishuAccount({ cfg, accountId });
|
||||
if (!account.configured) {
|
||||
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
||||
}
|
||||
|
||||
const client = createFeishuClient(feishuCfg);
|
||||
const client = createFeishuClient(account);
|
||||
|
||||
const response = await client.im.messageResource.get({
|
||||
path: { message_id: messageId, file_key: fileKey },
|
||||
params: { type },
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK response type varies
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK response type
|
||||
const responseAny = response as any;
|
||||
if (responseAny.code !== undefined && responseAny.code !== 0) {
|
||||
throw new Error(
|
||||
@@ -198,14 +200,15 @@ export async function uploadImageFeishu(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
image: Buffer | string; // Buffer or file path
|
||||
imageType?: "message" | "avatar";
|
||||
accountId?: string;
|
||||
}): Promise<UploadImageResult> {
|
||||
const { cfg, image, imageType = "message" } = params;
|
||||
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
||||
if (!feishuCfg) {
|
||||
throw new Error("Feishu channel not configured");
|
||||
const { cfg, image, imageType = "message", accountId } = params;
|
||||
const account = resolveFeishuAccount({ cfg, accountId });
|
||||
if (!account.configured) {
|
||||
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
||||
}
|
||||
|
||||
const client = createFeishuClient(feishuCfg);
|
||||
const client = createFeishuClient(account);
|
||||
|
||||
// SDK expects a Readable stream, not a Buffer
|
||||
// Use type assertion since SDK actually accepts any Readable at runtime
|
||||
@@ -214,14 +217,14 @@ export async function uploadImageFeishu(params: {
|
||||
const response = await client.im.image.create({
|
||||
data: {
|
||||
image_type: imageType,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK expects stream
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK stream type
|
||||
image: imageStream as any,
|
||||
},
|
||||
});
|
||||
|
||||
// SDK v1.30+ returns data directly without code wrapper on success
|
||||
// On error, it throws or returns { code, msg }
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK response type varies
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK response type
|
||||
const responseAny = response as any;
|
||||
if (responseAny.code !== undefined && responseAny.code !== 0) {
|
||||
throw new Error(`Feishu image upload failed: ${responseAny.msg || `code ${responseAny.code}`}`);
|
||||
@@ -245,14 +248,15 @@ export async function uploadFileFeishu(params: {
|
||||
fileName: string;
|
||||
fileType: "opus" | "mp4" | "pdf" | "doc" | "xls" | "ppt" | "stream";
|
||||
duration?: number; // Required for audio/video files, in milliseconds
|
||||
accountId?: string;
|
||||
}): Promise<UploadFileResult> {
|
||||
const { cfg, file, fileName, fileType, duration } = params;
|
||||
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
||||
if (!feishuCfg) {
|
||||
throw new Error("Feishu channel not configured");
|
||||
const { cfg, file, fileName, fileType, duration, accountId } = params;
|
||||
const account = resolveFeishuAccount({ cfg, accountId });
|
||||
if (!account.configured) {
|
||||
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
||||
}
|
||||
|
||||
const client = createFeishuClient(feishuCfg);
|
||||
const client = createFeishuClient(account);
|
||||
|
||||
// SDK expects a Readable stream, not a Buffer
|
||||
// Use type assertion since SDK actually accepts any Readable at runtime
|
||||
@@ -262,14 +266,14 @@ export async function uploadFileFeishu(params: {
|
||||
data: {
|
||||
file_type: fileType,
|
||||
file_name: fileName,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK expects stream
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK stream type
|
||||
file: fileStream as any,
|
||||
...(duration !== undefined && { duration }),
|
||||
},
|
||||
});
|
||||
|
||||
// SDK v1.30+ returns data directly without code wrapper on success
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK response type varies
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK response type
|
||||
const responseAny = response as any;
|
||||
if (responseAny.code !== undefined && responseAny.code !== 0) {
|
||||
throw new Error(`Feishu file upload failed: ${responseAny.msg || `code ${responseAny.code}`}`);
|
||||
@@ -291,14 +295,15 @@ export async function sendImageFeishu(params: {
|
||||
to: string;
|
||||
imageKey: string;
|
||||
replyToMessageId?: string;
|
||||
accountId?: string;
|
||||
}): Promise<SendMediaResult> {
|
||||
const { cfg, to, imageKey, replyToMessageId } = params;
|
||||
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
||||
if (!feishuCfg) {
|
||||
throw new Error("Feishu channel not configured");
|
||||
const { cfg, to, imageKey, replyToMessageId, accountId } = params;
|
||||
const account = resolveFeishuAccount({ cfg, accountId });
|
||||
if (!account.configured) {
|
||||
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
||||
}
|
||||
|
||||
const client = createFeishuClient(feishuCfg);
|
||||
const client = createFeishuClient(account);
|
||||
const receiveId = normalizeFeishuTarget(to);
|
||||
if (!receiveId) {
|
||||
throw new Error(`Invalid Feishu target: ${to}`);
|
||||
@@ -353,14 +358,15 @@ export async function sendFileFeishu(params: {
|
||||
to: string;
|
||||
fileKey: string;
|
||||
replyToMessageId?: string;
|
||||
accountId?: string;
|
||||
}): Promise<SendMediaResult> {
|
||||
const { cfg, to, fileKey, replyToMessageId } = params;
|
||||
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
||||
if (!feishuCfg) {
|
||||
throw new Error("Feishu channel not configured");
|
||||
const { cfg, to, fileKey, replyToMessageId, accountId } = params;
|
||||
const account = resolveFeishuAccount({ cfg, accountId });
|
||||
if (!account.configured) {
|
||||
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
||||
}
|
||||
|
||||
const client = createFeishuClient(feishuCfg);
|
||||
const client = createFeishuClient(account);
|
||||
const receiveId = normalizeFeishuTarget(to);
|
||||
if (!receiveId) {
|
||||
throw new Error(`Invalid Feishu target: ${to}`);
|
||||
@@ -465,8 +471,9 @@ export async function sendMediaFeishu(params: {
|
||||
mediaBuffer?: Buffer;
|
||||
fileName?: string;
|
||||
replyToMessageId?: string;
|
||||
accountId?: string;
|
||||
}): Promise<SendMediaResult> {
|
||||
const { cfg, to, mediaUrl, mediaBuffer, fileName, replyToMessageId } = params;
|
||||
const { cfg, to, mediaUrl, mediaBuffer, fileName, replyToMessageId, accountId } = params;
|
||||
|
||||
let buffer: Buffer;
|
||||
let name: string;
|
||||
@@ -504,8 +511,8 @@ export async function sendMediaFeishu(params: {
|
||||
const isImage = [".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".ico", ".tiff"].includes(ext);
|
||||
|
||||
if (isImage) {
|
||||
const { imageKey } = await uploadImageFeishu({ cfg, image: buffer });
|
||||
return sendImageFeishu({ cfg, to, imageKey, replyToMessageId });
|
||||
const { imageKey } = await uploadImageFeishu({ cfg, image: buffer, accountId });
|
||||
return sendImageFeishu({ cfg, to, imageKey, replyToMessageId, accountId });
|
||||
} else {
|
||||
const fileType = detectFileType(name);
|
||||
const { fileKey } = await uploadFileFeishu({
|
||||
@@ -513,7 +520,8 @@ export async function sendMediaFeishu(params: {
|
||||
file: buffer,
|
||||
fileName: name,
|
||||
fileType,
|
||||
accountId,
|
||||
});
|
||||
return sendFileFeishu({ cfg, to, fileKey, replyToMessageId });
|
||||
return sendFileFeishu({ cfg, to, fileKey, replyToMessageId, accountId });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ClawdbotConfig, RuntimeEnv, HistoryEntry } from "openclaw/plugin-sdk";
|
||||
import * as Lark from "@larksuiteoapi/node-sdk";
|
||||
import type { FeishuConfig } from "./types.js";
|
||||
import { resolveFeishuCredentials } from "./accounts.js";
|
||||
import type { ResolvedFeishuAccount } from "./types.js";
|
||||
import { resolveFeishuAccount, listEnabledFeishuAccounts } from "./accounts.js";
|
||||
import { handleFeishuMessage, type FeishuMessageEvent, type FeishuBotAddedEvent } from "./bot.js";
|
||||
import { createFeishuWSClient, createEventDispatcher } from "./client.js";
|
||||
import { probeFeishu } from "./probe.js";
|
||||
@@ -13,71 +13,52 @@ export type MonitorFeishuOpts = {
|
||||
accountId?: string;
|
||||
};
|
||||
|
||||
let currentWsClient: Lark.WSClient | null = null;
|
||||
let botOpenId: string | undefined;
|
||||
// Per-account WebSocket clients and bot info
|
||||
const wsClients = new Map<string, Lark.WSClient>();
|
||||
const botOpenIds = new Map<string, string>();
|
||||
|
||||
async function fetchBotOpenId(cfg: FeishuConfig): Promise<string | undefined> {
|
||||
async function fetchBotOpenId(account: ResolvedFeishuAccount): Promise<string | undefined> {
|
||||
try {
|
||||
const result = await probeFeishu(cfg);
|
||||
const result = await probeFeishu(account);
|
||||
return result.ok ? result.botOpenId : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export async function monitorFeishuProvider(opts: MonitorFeishuOpts = {}): Promise<void> {
|
||||
const cfg = opts.config;
|
||||
if (!cfg) {
|
||||
throw new Error("Config is required for Feishu monitor");
|
||||
}
|
||||
|
||||
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
||||
const creds = resolveFeishuCredentials(feishuCfg);
|
||||
if (!creds) {
|
||||
throw new Error("Feishu credentials not configured (appId, appSecret required)");
|
||||
}
|
||||
|
||||
const log = opts.runtime?.log ?? console.log;
|
||||
|
||||
if (feishuCfg) {
|
||||
botOpenId = await fetchBotOpenId(feishuCfg);
|
||||
log(`feishu: bot open_id resolved: ${botOpenId ?? "unknown"}`);
|
||||
}
|
||||
|
||||
const connectionMode = feishuCfg?.connectionMode ?? "websocket";
|
||||
|
||||
if (connectionMode === "websocket") {
|
||||
return monitorWebSocket({
|
||||
cfg,
|
||||
feishuCfg: feishuCfg!,
|
||||
runtime: opts.runtime,
|
||||
abortSignal: opts.abortSignal,
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
"feishu: webhook mode not implemented in monitor. Use websocket mode or configure an external HTTP server.",
|
||||
);
|
||||
}
|
||||
|
||||
async function monitorWebSocket(params: {
|
||||
/**
|
||||
* Monitor a single Feishu account.
|
||||
*/
|
||||
async function monitorSingleAccount(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
feishuCfg: FeishuConfig;
|
||||
account: ResolvedFeishuAccount;
|
||||
runtime?: RuntimeEnv;
|
||||
abortSignal?: AbortSignal;
|
||||
}): Promise<void> {
|
||||
const { cfg, feishuCfg, runtime, abortSignal } = params;
|
||||
const { cfg, account, runtime, abortSignal } = params;
|
||||
const { accountId } = account;
|
||||
const log = runtime?.log ?? console.log;
|
||||
const error = runtime?.error ?? console.error;
|
||||
|
||||
log("feishu: starting WebSocket connection...");
|
||||
// Fetch bot open_id
|
||||
const botOpenId = await fetchBotOpenId(account);
|
||||
botOpenIds.set(accountId, botOpenId ?? "");
|
||||
log(`feishu[${accountId}]: bot open_id resolved: ${botOpenId ?? "unknown"}`);
|
||||
|
||||
const wsClient = createFeishuWSClient(feishuCfg);
|
||||
currentWsClient = wsClient;
|
||||
const connectionMode = account.config.connectionMode ?? "websocket";
|
||||
|
||||
if (connectionMode !== "websocket") {
|
||||
log(`feishu[${accountId}]: webhook mode not implemented in monitor`);
|
||||
return;
|
||||
}
|
||||
|
||||
log(`feishu[${accountId}]: starting WebSocket connection...`);
|
||||
|
||||
const wsClient = createFeishuWSClient(account);
|
||||
wsClients.set(accountId, wsClient);
|
||||
|
||||
const chatHistories = new Map<string, HistoryEntry[]>();
|
||||
|
||||
const eventDispatcher = createEventDispatcher(feishuCfg);
|
||||
const eventDispatcher = createEventDispatcher(account);
|
||||
|
||||
eventDispatcher.register({
|
||||
"im.message.receive_v1": async (data) => {
|
||||
@@ -86,12 +67,13 @@ async function monitorWebSocket(params: {
|
||||
await handleFeishuMessage({
|
||||
cfg,
|
||||
event,
|
||||
botOpenId,
|
||||
botOpenId: botOpenIds.get(accountId),
|
||||
runtime,
|
||||
chatHistories,
|
||||
accountId,
|
||||
});
|
||||
} catch (err) {
|
||||
error(`feishu: error handling message event: ${String(err)}`);
|
||||
error(`feishu[${accountId}]: error handling message: ${String(err)}`);
|
||||
}
|
||||
},
|
||||
"im.message.message_read_v1": async () => {
|
||||
@@ -100,30 +82,29 @@ async function monitorWebSocket(params: {
|
||||
"im.chat.member.bot.added_v1": async (data) => {
|
||||
try {
|
||||
const event = data as unknown as FeishuBotAddedEvent;
|
||||
log(`feishu: bot added to chat ${event.chat_id}`);
|
||||
log(`feishu[${accountId}]: bot added to chat ${event.chat_id}`);
|
||||
} catch (err) {
|
||||
error(`feishu: error handling bot added event: ${String(err)}`);
|
||||
error(`feishu[${accountId}]: error handling bot added event: ${String(err)}`);
|
||||
}
|
||||
},
|
||||
"im.chat.member.bot.deleted_v1": async (data) => {
|
||||
try {
|
||||
const event = data as unknown as { chat_id: string };
|
||||
log(`feishu: bot removed from chat ${event.chat_id}`);
|
||||
log(`feishu[${accountId}]: bot removed from chat ${event.chat_id}`);
|
||||
} catch (err) {
|
||||
error(`feishu: error handling bot removed event: ${String(err)}`);
|
||||
error(`feishu[${accountId}]: error handling bot removed event: ${String(err)}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const cleanup = () => {
|
||||
if (currentWsClient === wsClient) {
|
||||
currentWsClient = null;
|
||||
}
|
||||
wsClients.delete(accountId);
|
||||
botOpenIds.delete(accountId);
|
||||
};
|
||||
|
||||
const handleAbort = () => {
|
||||
log("feishu: abort signal received, stopping WebSocket client");
|
||||
log(`feishu[${accountId}]: abort signal received, stopping`);
|
||||
cleanup();
|
||||
resolve();
|
||||
};
|
||||
@@ -137,11 +118,8 @@ async function monitorWebSocket(params: {
|
||||
abortSignal?.addEventListener("abort", handleAbort, { once: true });
|
||||
|
||||
try {
|
||||
void wsClient.start({
|
||||
eventDispatcher,
|
||||
});
|
||||
|
||||
log("feishu: WebSocket client started");
|
||||
void wsClient.start({ eventDispatcher });
|
||||
log(`feishu[${accountId}]: WebSocket client started`);
|
||||
} catch (err) {
|
||||
cleanup();
|
||||
abortSignal?.removeEventListener("abort", handleAbort);
|
||||
@@ -150,8 +128,63 @@ async function monitorWebSocket(params: {
|
||||
});
|
||||
}
|
||||
|
||||
export function stopFeishuMonitor(): void {
|
||||
if (currentWsClient) {
|
||||
currentWsClient = null;
|
||||
/**
|
||||
* Main entry: start monitoring for all enabled accounts.
|
||||
*/
|
||||
export async function monitorFeishuProvider(opts: MonitorFeishuOpts = {}): Promise<void> {
|
||||
const cfg = opts.config;
|
||||
if (!cfg) {
|
||||
throw new Error("Config is required for Feishu monitor");
|
||||
}
|
||||
|
||||
const log = opts.runtime?.log ?? console.log;
|
||||
|
||||
// If accountId is specified, only monitor that account
|
||||
if (opts.accountId) {
|
||||
const account = resolveFeishuAccount({ cfg, accountId: opts.accountId });
|
||||
if (!account.enabled || !account.configured) {
|
||||
throw new Error(`Feishu account "${opts.accountId}" not configured or disabled`);
|
||||
}
|
||||
return monitorSingleAccount({
|
||||
cfg,
|
||||
account,
|
||||
runtime: opts.runtime,
|
||||
abortSignal: opts.abortSignal,
|
||||
});
|
||||
}
|
||||
|
||||
// Otherwise, start all enabled accounts
|
||||
const accounts = listEnabledFeishuAccounts(cfg);
|
||||
if (accounts.length === 0) {
|
||||
throw new Error("No enabled Feishu accounts configured");
|
||||
}
|
||||
|
||||
log(
|
||||
`feishu: starting ${accounts.length} account(s): ${accounts.map((a) => a.accountId).join(", ")}`,
|
||||
);
|
||||
|
||||
// Start all accounts in parallel
|
||||
await Promise.all(
|
||||
accounts.map((account) =>
|
||||
monitorSingleAccount({
|
||||
cfg,
|
||||
account,
|
||||
runtime: opts.runtime,
|
||||
abortSignal: opts.abortSignal,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop monitoring for a specific account or all accounts.
|
||||
*/
|
||||
export function stopFeishuMonitor(accountId?: string): void {
|
||||
if (accountId) {
|
||||
wsClients.delete(accountId);
|
||||
botOpenIds.delete(accountId);
|
||||
} else {
|
||||
wsClients.clear();
|
||||
botOpenIds.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,33 +8,33 @@ export const feishuOutbound: ChannelOutboundAdapter = {
|
||||
chunker: (text, limit) => getFeishuRuntime().channel.text.chunkMarkdownText(text, limit),
|
||||
chunkerMode: "markdown",
|
||||
textChunkLimit: 4000,
|
||||
sendText: async ({ cfg, to, text }) => {
|
||||
const result = await sendMessageFeishu({ cfg, to, text });
|
||||
sendText: async ({ cfg, to, text, accountId }) => {
|
||||
const result = await sendMessageFeishu({ cfg, to, text, accountId });
|
||||
return { channel: "feishu", ...result };
|
||||
},
|
||||
sendMedia: async ({ cfg, to, text, mediaUrl }) => {
|
||||
sendMedia: async ({ cfg, to, text, mediaUrl, accountId }) => {
|
||||
// Send text first if provided
|
||||
if (text?.trim()) {
|
||||
await sendMessageFeishu({ cfg, to, text });
|
||||
await sendMessageFeishu({ cfg, to, text, accountId });
|
||||
}
|
||||
|
||||
// Upload and send media if URL provided
|
||||
if (mediaUrl) {
|
||||
try {
|
||||
const result = await sendMediaFeishu({ cfg, to, mediaUrl });
|
||||
const result = await sendMediaFeishu({ cfg, to, mediaUrl, accountId });
|
||||
return { channel: "feishu", ...result };
|
||||
} catch (err) {
|
||||
// Log the error for debugging
|
||||
console.error(`[feishu] sendMediaFeishu failed:`, err);
|
||||
// Fallback to URL link if upload fails
|
||||
const fallbackText = `📎 ${mediaUrl}`;
|
||||
const result = await sendMessageFeishu({ cfg, to, text: fallbackText });
|
||||
const result = await sendMessageFeishu({ cfg, to, text: fallbackText, accountId });
|
||||
return { channel: "feishu", ...result };
|
||||
}
|
||||
}
|
||||
|
||||
// No media URL, just return text result
|
||||
const result = await sendMessageFeishu({ cfg, to, text: text ?? "" });
|
||||
const result = await sendMessageFeishu({ cfg, to, text: text ?? "", accountId });
|
||||
return { channel: "feishu", ...result };
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type * as Lark from "@larksuiteoapi/node-sdk";
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||
import type { FeishuConfig } from "./types.js";
|
||||
import { listEnabledFeishuAccounts } from "./accounts.js";
|
||||
import { createFeishuClient } from "./client.js";
|
||||
import { FeishuPermSchema, type FeishuPermParams } from "./perm-schema.js";
|
||||
import { resolveToolsConfig } from "./tools-config.js";
|
||||
@@ -118,19 +118,25 @@ async function removeMember(
|
||||
// ============ Tool Registration ============
|
||||
|
||||
export function registerFeishuPermTools(api: OpenClawPluginApi) {
|
||||
const feishuCfg = api.config?.channels?.feishu as FeishuConfig | undefined;
|
||||
if (!feishuCfg?.appId || !feishuCfg?.appSecret) {
|
||||
api.logger.debug?.("feishu_perm: Feishu credentials not configured, skipping perm tools");
|
||||
if (!api.config) {
|
||||
api.logger.debug?.("feishu_perm: No config available, skipping perm tools");
|
||||
return;
|
||||
}
|
||||
|
||||
const toolsCfg = resolveToolsConfig(feishuCfg.tools);
|
||||
const accounts = listEnabledFeishuAccounts(api.config);
|
||||
if (accounts.length === 0) {
|
||||
api.logger.debug?.("feishu_perm: No Feishu accounts configured, skipping perm tools");
|
||||
return;
|
||||
}
|
||||
|
||||
const firstAccount = accounts[0];
|
||||
const toolsCfg = resolveToolsConfig(firstAccount.config.tools);
|
||||
if (!toolsCfg.perm) {
|
||||
api.logger.debug?.("feishu_perm: perm tool disabled in config (default: false)");
|
||||
return;
|
||||
}
|
||||
|
||||
const getClient = () => createFeishuClient(feishuCfg);
|
||||
const getClient = () => createFeishuClient(firstAccount);
|
||||
|
||||
api.registerTool(
|
||||
{
|
||||
|
||||
@@ -58,7 +58,6 @@ export function resolveFeishuGroupConfig(params: {
|
||||
|
||||
export function resolveFeishuGroupToolPolicy(
|
||||
params: ChannelGroupContext,
|
||||
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents -- type resolution issue with plugin-sdk
|
||||
): GroupToolPolicyConfig | undefined {
|
||||
const cfg = params.cfg.channels?.feishu as FeishuConfig | undefined;
|
||||
if (!cfg) {
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import type { FeishuConfig, FeishuProbeResult } from "./types.js";
|
||||
import { resolveFeishuCredentials } from "./accounts.js";
|
||||
import { createFeishuClient } from "./client.js";
|
||||
import type { FeishuProbeResult } from "./types.js";
|
||||
import { createFeishuClient, type FeishuClientCredentials } from "./client.js";
|
||||
|
||||
export async function probeFeishu(cfg?: FeishuConfig): Promise<FeishuProbeResult> {
|
||||
const creds = resolveFeishuCredentials(cfg);
|
||||
if (!creds) {
|
||||
export async function probeFeishu(creds?: FeishuClientCredentials): Promise<FeishuProbeResult> {
|
||||
if (!creds?.appId || !creds?.appSecret) {
|
||||
return {
|
||||
ok: false,
|
||||
error: "missing credentials (appId, appSecret)",
|
||||
@@ -12,10 +10,9 @@ export async function probeFeishu(cfg?: FeishuConfig): Promise<FeishuProbeResult
|
||||
}
|
||||
|
||||
try {
|
||||
const client = createFeishuClient(cfg!);
|
||||
// Use im.chat.list as a simple connectivity test
|
||||
// The bot info API path varies by SDK version
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- accessing internal SDK method
|
||||
const client = createFeishuClient(creds);
|
||||
// Use bot/v3/info API to get bot information
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK generic request method
|
||||
const response = await (client as any).request({
|
||||
method: "GET",
|
||||
url: "/open-apis/bot/v3/info",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ClawdbotConfig } from "openclaw/plugin-sdk";
|
||||
import type { FeishuConfig } from "./types.js";
|
||||
import { resolveFeishuAccount } from "./accounts.js";
|
||||
import { createFeishuClient } from "./client.js";
|
||||
|
||||
export type FeishuReaction = {
|
||||
@@ -18,14 +18,15 @@ export async function addReactionFeishu(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
messageId: string;
|
||||
emojiType: string;
|
||||
accountId?: string;
|
||||
}): Promise<{ reactionId: string }> {
|
||||
const { cfg, messageId, emojiType } = params;
|
||||
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
||||
if (!feishuCfg) {
|
||||
throw new Error("Feishu channel not configured");
|
||||
const { cfg, messageId, emojiType, accountId } = params;
|
||||
const account = resolveFeishuAccount({ cfg, accountId });
|
||||
if (!account.configured) {
|
||||
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
||||
}
|
||||
|
||||
const client = createFeishuClient(feishuCfg);
|
||||
const client = createFeishuClient(account);
|
||||
|
||||
const response = (await client.im.messageReaction.create({
|
||||
path: { message_id: messageId },
|
||||
@@ -59,14 +60,15 @@ export async function removeReactionFeishu(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
messageId: string;
|
||||
reactionId: string;
|
||||
accountId?: string;
|
||||
}): Promise<void> {
|
||||
const { cfg, messageId, reactionId } = params;
|
||||
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
||||
if (!feishuCfg) {
|
||||
throw new Error("Feishu channel not configured");
|
||||
const { cfg, messageId, reactionId, accountId } = params;
|
||||
const account = resolveFeishuAccount({ cfg, accountId });
|
||||
if (!account.configured) {
|
||||
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
||||
}
|
||||
|
||||
const client = createFeishuClient(feishuCfg);
|
||||
const client = createFeishuClient(account);
|
||||
|
||||
const response = (await client.im.messageReaction.delete({
|
||||
path: {
|
||||
@@ -87,14 +89,15 @@ export async function listReactionsFeishu(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
messageId: string;
|
||||
emojiType?: string;
|
||||
accountId?: string;
|
||||
}): Promise<FeishuReaction[]> {
|
||||
const { cfg, messageId, emojiType } = params;
|
||||
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
||||
if (!feishuCfg) {
|
||||
throw new Error("Feishu channel not configured");
|
||||
const { cfg, messageId, emojiType, accountId } = params;
|
||||
const account = resolveFeishuAccount({ cfg, accountId });
|
||||
if (!account.configured) {
|
||||
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
||||
}
|
||||
|
||||
const client = createFeishuClient(feishuCfg);
|
||||
const client = createFeishuClient(account);
|
||||
|
||||
const response = (await client.im.messageReaction.list({
|
||||
path: { message_id: messageId },
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
type ReplyPayload,
|
||||
} from "openclaw/plugin-sdk";
|
||||
import type { MentionTarget } from "./mention.js";
|
||||
import type { FeishuConfig } from "./types.js";
|
||||
import { resolveFeishuAccount } from "./accounts.js";
|
||||
import { getFeishuRuntime } from "./runtime.js";
|
||||
import { sendMessageFeishu, sendMarkdownCardFeishu } from "./send.js";
|
||||
import { addTypingIndicator, removeTypingIndicator, type TypingIndicatorState } from "./typing.js";
|
||||
@@ -36,11 +36,16 @@ export type CreateFeishuReplyDispatcherParams = {
|
||||
replyToMessageId?: string;
|
||||
/** Mention targets, will be auto-included in replies */
|
||||
mentionTargets?: MentionTarget[];
|
||||
/** Account ID for multi-account support */
|
||||
accountId?: string;
|
||||
};
|
||||
|
||||
export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherParams) {
|
||||
const core = getFeishuRuntime();
|
||||
const { cfg, agentId, chatId, replyToMessageId, mentionTargets } = params;
|
||||
const { cfg, agentId, chatId, replyToMessageId, mentionTargets, accountId } = params;
|
||||
|
||||
// Resolve account for config access
|
||||
const account = resolveFeishuAccount({ cfg, accountId });
|
||||
|
||||
const prefixContext = createReplyPrefixContext({
|
||||
cfg,
|
||||
@@ -56,16 +61,16 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
|
||||
if (!replyToMessageId) {
|
||||
return;
|
||||
}
|
||||
typingState = await addTypingIndicator({ cfg, messageId: replyToMessageId });
|
||||
params.runtime.log?.(`feishu: added typing indicator reaction`);
|
||||
typingState = await addTypingIndicator({ cfg, messageId: replyToMessageId, accountId });
|
||||
params.runtime.log?.(`feishu[${account.accountId}]: added typing indicator reaction`);
|
||||
},
|
||||
stop: async () => {
|
||||
if (!typingState) {
|
||||
return;
|
||||
}
|
||||
await removeTypingIndicator({ cfg, state: typingState });
|
||||
await removeTypingIndicator({ cfg, state: typingState, accountId });
|
||||
typingState = null;
|
||||
params.runtime.log?.(`feishu: removed typing indicator reaction`);
|
||||
params.runtime.log?.(`feishu[${account.accountId}]: removed typing indicator reaction`);
|
||||
},
|
||||
onStartError: (err) => {
|
||||
logTypingFailure({
|
||||
@@ -103,15 +108,17 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
|
||||
humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, agentId),
|
||||
onReplyStart: typingCallbacks.onReplyStart,
|
||||
deliver: async (payload: ReplyPayload) => {
|
||||
params.runtime.log?.(`feishu deliver called: text=${payload.text?.slice(0, 100)}`);
|
||||
params.runtime.log?.(
|
||||
`feishu[${account.accountId}] deliver called: text=${payload.text?.slice(0, 100)}`,
|
||||
);
|
||||
const text = payload.text ?? "";
|
||||
if (!text.trim()) {
|
||||
params.runtime.log?.(`feishu deliver: empty text, skipping`);
|
||||
params.runtime.log?.(`feishu[${account.accountId}] deliver: empty text, skipping`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check render mode: auto (default), raw, or card
|
||||
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
||||
const feishuCfg = account.config;
|
||||
const renderMode = feishuCfg?.renderMode ?? "auto";
|
||||
|
||||
// Determine if we should use card for this message
|
||||
@@ -123,7 +130,9 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
|
||||
if (useCard) {
|
||||
// Card mode: send as interactive card with markdown rendering
|
||||
const chunks = core.channel.text.chunkTextWithMode(text, textChunkLimit, chunkMode);
|
||||
params.runtime.log?.(`feishu deliver: sending ${chunks.length} card chunks to ${chatId}`);
|
||||
params.runtime.log?.(
|
||||
`feishu[${account.accountId}] deliver: sending ${chunks.length} card chunks to ${chatId}`,
|
||||
);
|
||||
for (const chunk of chunks) {
|
||||
await sendMarkdownCardFeishu({
|
||||
cfg,
|
||||
@@ -131,6 +140,7 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
|
||||
text: chunk,
|
||||
replyToMessageId,
|
||||
mentions: isFirstChunk ? mentionTargets : undefined,
|
||||
accountId,
|
||||
});
|
||||
isFirstChunk = false;
|
||||
}
|
||||
@@ -138,7 +148,9 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
|
||||
// Raw mode: send as plain text with table conversion
|
||||
const converted = core.channel.text.convertMarkdownTables(text, tableMode);
|
||||
const chunks = core.channel.text.chunkTextWithMode(converted, textChunkLimit, chunkMode);
|
||||
params.runtime.log?.(`feishu deliver: sending ${chunks.length} text chunks to ${chatId}`);
|
||||
params.runtime.log?.(
|
||||
`feishu[${account.accountId}] deliver: sending ${chunks.length} text chunks to ${chatId}`,
|
||||
);
|
||||
for (const chunk of chunks) {
|
||||
await sendMessageFeishu({
|
||||
cfg,
|
||||
@@ -146,13 +158,16 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
|
||||
text: chunk,
|
||||
replyToMessageId,
|
||||
mentions: isFirstChunk ? mentionTargets : undefined,
|
||||
accountId,
|
||||
});
|
||||
isFirstChunk = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
onError: (err, info) => {
|
||||
params.runtime.error?.(`feishu ${info.kind} reply failed: ${String(err)}`);
|
||||
params.runtime.error?.(
|
||||
`feishu[${account.accountId}] ${info.kind} reply failed: ${String(err)}`,
|
||||
);
|
||||
typingCallbacks.onIdle?.();
|
||||
},
|
||||
onIdle: typingCallbacks.onIdle,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { PluginRuntime } from "openclaw/plugin-sdk";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
|
||||
let runtime: PluginRuntime | null = null;
|
||||
|
||||
export function setFeishuRuntime(next: PluginRuntime) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ClawdbotConfig } from "openclaw/plugin-sdk";
|
||||
import type { MentionTarget } from "./mention.js";
|
||||
import type { FeishuConfig, FeishuSendResult } from "./types.js";
|
||||
import type { FeishuSendResult } from "./types.js";
|
||||
import { resolveFeishuAccount } from "./accounts.js";
|
||||
import { createFeishuClient } from "./client.js";
|
||||
import { buildMentionedMessage, buildMentionedCardContent } from "./mention.js";
|
||||
import { getFeishuRuntime } from "./runtime.js";
|
||||
@@ -23,14 +24,15 @@ export type FeishuMessageInfo = {
|
||||
export async function getMessageFeishu(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
messageId: string;
|
||||
accountId?: string;
|
||||
}): Promise<FeishuMessageInfo | null> {
|
||||
const { cfg, messageId } = params;
|
||||
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
||||
if (!feishuCfg) {
|
||||
throw new Error("Feishu channel not configured");
|
||||
const { cfg, messageId, accountId } = params;
|
||||
const account = resolveFeishuAccount({ cfg, accountId });
|
||||
if (!account.configured) {
|
||||
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
||||
}
|
||||
|
||||
const client = createFeishuClient(feishuCfg);
|
||||
const client = createFeishuClient(account);
|
||||
|
||||
try {
|
||||
const response = (await client.im.message.get({
|
||||
@@ -95,9 +97,11 @@ export type SendFeishuMessageParams = {
|
||||
replyToMessageId?: string;
|
||||
/** Mention target users */
|
||||
mentions?: MentionTarget[];
|
||||
/** Account ID (optional, uses default if not specified) */
|
||||
accountId?: string;
|
||||
};
|
||||
|
||||
function buildFeishuPostMessagePayload(params: { feishuCfg: FeishuConfig; messageText: string }): {
|
||||
function buildFeishuPostMessagePayload(params: { messageText: string }): {
|
||||
content: string;
|
||||
msgType: string;
|
||||
} {
|
||||
@@ -122,13 +126,13 @@ function buildFeishuPostMessagePayload(params: { feishuCfg: FeishuConfig; messag
|
||||
export async function sendMessageFeishu(
|
||||
params: SendFeishuMessageParams,
|
||||
): Promise<FeishuSendResult> {
|
||||
const { cfg, to, text, replyToMessageId, mentions } = params;
|
||||
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
||||
if (!feishuCfg) {
|
||||
throw new Error("Feishu channel not configured");
|
||||
const { cfg, to, text, replyToMessageId, mentions, accountId } = params;
|
||||
const account = resolveFeishuAccount({ cfg, accountId });
|
||||
if (!account.configured) {
|
||||
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
||||
}
|
||||
|
||||
const client = createFeishuClient(feishuCfg);
|
||||
const client = createFeishuClient(account);
|
||||
const receiveId = normalizeFeishuTarget(to);
|
||||
if (!receiveId) {
|
||||
throw new Error(`Invalid Feishu target: ${to}`);
|
||||
@@ -147,10 +151,7 @@ export async function sendMessageFeishu(
|
||||
}
|
||||
const messageText = getFeishuRuntime().channel.text.convertMarkdownTables(rawText, tableMode);
|
||||
|
||||
const { content, msgType } = buildFeishuPostMessagePayload({
|
||||
feishuCfg,
|
||||
messageText,
|
||||
});
|
||||
const { content, msgType } = buildFeishuPostMessagePayload({ messageText });
|
||||
|
||||
if (replyToMessageId) {
|
||||
const response = await client.im.message.reply({
|
||||
@@ -195,16 +196,17 @@ export type SendFeishuCardParams = {
|
||||
to: string;
|
||||
card: Record<string, unknown>;
|
||||
replyToMessageId?: string;
|
||||
accountId?: string;
|
||||
};
|
||||
|
||||
export async function sendCardFeishu(params: SendFeishuCardParams): Promise<FeishuSendResult> {
|
||||
const { cfg, to, card, replyToMessageId } = params;
|
||||
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
||||
if (!feishuCfg) {
|
||||
throw new Error("Feishu channel not configured");
|
||||
const { cfg, to, card, replyToMessageId, accountId } = params;
|
||||
const account = resolveFeishuAccount({ cfg, accountId });
|
||||
if (!account.configured) {
|
||||
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
||||
}
|
||||
|
||||
const client = createFeishuClient(feishuCfg);
|
||||
const client = createFeishuClient(account);
|
||||
const receiveId = normalizeFeishuTarget(to);
|
||||
if (!receiveId) {
|
||||
throw new Error(`Invalid Feishu target: ${to}`);
|
||||
@@ -255,14 +257,15 @@ export async function updateCardFeishu(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
messageId: string;
|
||||
card: Record<string, unknown>;
|
||||
accountId?: string;
|
||||
}): Promise<void> {
|
||||
const { cfg, messageId, card } = params;
|
||||
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
||||
if (!feishuCfg) {
|
||||
throw new Error("Feishu channel not configured");
|
||||
const { cfg, messageId, card, accountId } = params;
|
||||
const account = resolveFeishuAccount({ cfg, accountId });
|
||||
if (!account.configured) {
|
||||
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
||||
}
|
||||
|
||||
const client = createFeishuClient(feishuCfg);
|
||||
const client = createFeishuClient(account);
|
||||
const content = JSON.stringify(card);
|
||||
|
||||
const response = await client.im.message.patch({
|
||||
@@ -304,15 +307,16 @@ export async function sendMarkdownCardFeishu(params: {
|
||||
replyToMessageId?: string;
|
||||
/** Mention target users */
|
||||
mentions?: MentionTarget[];
|
||||
accountId?: string;
|
||||
}): Promise<FeishuSendResult> {
|
||||
const { cfg, to, text, replyToMessageId, mentions } = params;
|
||||
const { cfg, to, text, replyToMessageId, mentions, accountId } = params;
|
||||
// Build message content (with @mention support)
|
||||
let cardText = text;
|
||||
if (mentions && mentions.length > 0) {
|
||||
cardText = buildMentionedCardContent(mentions, text);
|
||||
}
|
||||
const card = buildMarkdownCard(cardText);
|
||||
return sendCardFeishu({ cfg, to, card, replyToMessageId });
|
||||
return sendCardFeishu({ cfg, to, card, replyToMessageId, accountId });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -323,24 +327,22 @@ export async function editMessageFeishu(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
messageId: string;
|
||||
text: string;
|
||||
accountId?: string;
|
||||
}): Promise<void> {
|
||||
const { cfg, messageId, text } = params;
|
||||
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
||||
if (!feishuCfg) {
|
||||
throw new Error("Feishu channel not configured");
|
||||
const { cfg, messageId, text, accountId } = params;
|
||||
const account = resolveFeishuAccount({ cfg, accountId });
|
||||
if (!account.configured) {
|
||||
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
||||
}
|
||||
|
||||
const client = createFeishuClient(feishuCfg);
|
||||
const client = createFeishuClient(account);
|
||||
const tableMode = getFeishuRuntime().channel.text.resolveMarkdownTableMode({
|
||||
cfg,
|
||||
channel: "feishu",
|
||||
});
|
||||
const messageText = getFeishuRuntime().channel.text.convertMarkdownTables(text ?? "", tableMode);
|
||||
|
||||
const { content, msgType } = buildFeishuPostMessagePayload({
|
||||
feishuCfg,
|
||||
messageText,
|
||||
});
|
||||
const { content, msgType } = buildFeishuPostMessagePayload({ messageText });
|
||||
|
||||
const response = await client.im.message.update({
|
||||
path: { message_id: messageId },
|
||||
|
||||
@@ -57,8 +57,7 @@ export function resolveReceiveIdType(id: string): "chat_id" | "open_id" | "user_
|
||||
if (trimmed.startsWith(OPEN_ID_PREFIX)) {
|
||||
return "open_id";
|
||||
}
|
||||
// Default to user_id for other alphanumeric IDs (e.g., enterprise user IDs)
|
||||
return "user_id";
|
||||
return "open_id";
|
||||
}
|
||||
|
||||
export function looksLikeFeishuId(raw: string): boolean {
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import type { FeishuConfigSchema, FeishuGroupSchema, z } from "./config-schema.js";
|
||||
import type {
|
||||
FeishuConfigSchema,
|
||||
FeishuGroupSchema,
|
||||
FeishuAccountConfigSchema,
|
||||
z,
|
||||
} from "./config-schema.js";
|
||||
import type { MentionTarget } from "./mention.js";
|
||||
|
||||
export type FeishuConfig = z.infer<typeof FeishuConfigSchema>;
|
||||
export type FeishuGroupConfig = z.infer<typeof FeishuGroupSchema>;
|
||||
export type FeishuAccountConfig = z.infer<typeof FeishuAccountConfigSchema>;
|
||||
|
||||
export type FeishuDomain = "feishu" | "lark" | (string & {});
|
||||
export type FeishuConnectionMode = "websocket" | "webhook";
|
||||
@@ -11,8 +17,14 @@ export type ResolvedFeishuAccount = {
|
||||
accountId: string;
|
||||
enabled: boolean;
|
||||
configured: boolean;
|
||||
name?: string;
|
||||
appId?: string;
|
||||
appSecret?: string;
|
||||
encryptKey?: string;
|
||||
verificationToken?: string;
|
||||
domain: FeishuDomain;
|
||||
/** Merged config (top-level defaults + account-specific overrides) */
|
||||
config: FeishuConfig;
|
||||
};
|
||||
|
||||
export type FeishuIdType = "open_id" | "user_id" | "union_id" | "chat_id";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ClawdbotConfig } from "openclaw/plugin-sdk";
|
||||
import type { FeishuConfig } from "./types.js";
|
||||
import { resolveFeishuAccount } from "./accounts.js";
|
||||
import { createFeishuClient } from "./client.js";
|
||||
|
||||
// Feishu emoji types for typing indicator
|
||||
@@ -18,14 +18,15 @@ export type TypingIndicatorState = {
|
||||
export async function addTypingIndicator(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
messageId: string;
|
||||
accountId?: string;
|
||||
}): Promise<TypingIndicatorState> {
|
||||
const { cfg, messageId } = params;
|
||||
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
||||
if (!feishuCfg) {
|
||||
const { cfg, messageId, accountId } = params;
|
||||
const account = resolveFeishuAccount({ cfg, accountId });
|
||||
if (!account.configured) {
|
||||
return { messageId, reactionId: null };
|
||||
}
|
||||
|
||||
const client = createFeishuClient(feishuCfg);
|
||||
const client = createFeishuClient(account);
|
||||
|
||||
try {
|
||||
const response = await client.im.messageReaction.create({
|
||||
@@ -51,18 +52,19 @@ export async function addTypingIndicator(params: {
|
||||
export async function removeTypingIndicator(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
state: TypingIndicatorState;
|
||||
accountId?: string;
|
||||
}): Promise<void> {
|
||||
const { cfg, state } = params;
|
||||
const { cfg, state, accountId } = params;
|
||||
if (!state.reactionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
||||
if (!feishuCfg) {
|
||||
const account = resolveFeishuAccount({ cfg, accountId });
|
||||
if (!account.configured) {
|
||||
return;
|
||||
}
|
||||
|
||||
const client = createFeishuClient(feishuCfg);
|
||||
const client = createFeishuClient(account);
|
||||
|
||||
try {
|
||||
await client.im.messageReaction.delete({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type * as Lark from "@larksuiteoapi/node-sdk";
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||
import type { FeishuConfig } from "./types.js";
|
||||
import { listEnabledFeishuAccounts } from "./accounts.js";
|
||||
import { createFeishuClient } from "./client.js";
|
||||
import { resolveToolsConfig } from "./tools-config.js";
|
||||
import { FeishuWikiSchema, type FeishuWikiParams } from "./wiki-schema.js";
|
||||
@@ -157,19 +157,25 @@ async function renameNode(client: Lark.Client, spaceId: string, nodeToken: strin
|
||||
// ============ Tool Registration ============
|
||||
|
||||
export function registerFeishuWikiTools(api: OpenClawPluginApi) {
|
||||
const feishuCfg = api.config?.channels?.feishu as FeishuConfig | undefined;
|
||||
if (!feishuCfg?.appId || !feishuCfg?.appSecret) {
|
||||
api.logger.debug?.("feishu_wiki: Feishu credentials not configured, skipping wiki tools");
|
||||
if (!api.config) {
|
||||
api.logger.debug?.("feishu_wiki: No config available, skipping wiki tools");
|
||||
return;
|
||||
}
|
||||
|
||||
const toolsCfg = resolveToolsConfig(feishuCfg.tools);
|
||||
const accounts = listEnabledFeishuAccounts(api.config);
|
||||
if (accounts.length === 0) {
|
||||
api.logger.debug?.("feishu_wiki: No Feishu accounts configured, skipping wiki tools");
|
||||
return;
|
||||
}
|
||||
|
||||
const firstAccount = accounts[0];
|
||||
const toolsCfg = resolveToolsConfig(firstAccount.config.tools);
|
||||
if (!toolsCfg.wiki) {
|
||||
api.logger.debug?.("feishu_wiki: wiki tool disabled in config");
|
||||
return;
|
||||
}
|
||||
|
||||
const getClient = () => createFeishuClient(feishuCfg);
|
||||
const getClient = () => createFeishuClient(firstAccount);
|
||||
|
||||
api.registerTool(
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user