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 type { ClawdbotConfig } from "openclaw/plugin-sdk";
|
||||||
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk";
|
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk";
|
||||||
import type { FeishuConfig, FeishuDomain, ResolvedFeishuAccount } from "./types.js";
|
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): {
|
export function resolveFeishuCredentials(cfg?: FeishuConfig): {
|
||||||
appId: string;
|
appId: string;
|
||||||
appSecret: string;
|
appSecret: string;
|
||||||
@@ -23,31 +97,46 @@ export function resolveFeishuCredentials(cfg?: FeishuConfig): {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a complete Feishu account with merged config.
|
||||||
|
*/
|
||||||
export function resolveFeishuAccount(params: {
|
export function resolveFeishuAccount(params: {
|
||||||
cfg: ClawdbotConfig;
|
cfg: ClawdbotConfig;
|
||||||
accountId?: string | null;
|
accountId?: string | null;
|
||||||
}): ResolvedFeishuAccount {
|
}): ResolvedFeishuAccount {
|
||||||
|
const accountId = normalizeAccountId(params.accountId);
|
||||||
const feishuCfg = params.cfg.channels?.feishu as FeishuConfig | undefined;
|
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 {
|
return {
|
||||||
accountId: params.accountId?.trim() || DEFAULT_ACCOUNT_ID,
|
accountId,
|
||||||
enabled,
|
enabled,
|
||||||
configured: Boolean(creds),
|
configured: Boolean(creds),
|
||||||
|
name: (merged as FeishuAccountConfig).name?.trim() || undefined,
|
||||||
appId: creds?.appId,
|
appId: creds?.appId,
|
||||||
|
appSecret: creds?.appSecret,
|
||||||
|
encryptKey: creds?.encryptKey,
|
||||||
|
verificationToken: creds?.verificationToken,
|
||||||
domain: creds?.domain ?? "feishu",
|
domain: creds?.domain ?? "feishu",
|
||||||
|
config: merged,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listFeishuAccountIds(_cfg: ClawdbotConfig): string[] {
|
/**
|
||||||
return [DEFAULT_ACCOUNT_ID];
|
* List all enabled and configured accounts.
|
||||||
}
|
*/
|
||||||
|
|
||||||
export function resolveDefaultFeishuAccountId(_cfg: ClawdbotConfig): string {
|
|
||||||
return DEFAULT_ACCOUNT_ID;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function listEnabledFeishuAccounts(cfg: ClawdbotConfig): ResolvedFeishuAccount[] {
|
export function listEnabledFeishuAccounts(cfg: ClawdbotConfig): ResolvedFeishuAccount[] {
|
||||||
return listFeishuAccountIds(cfg)
|
return listFeishuAccountIds(cfg)
|
||||||
.map((accountId) => resolveFeishuAccount({ cfg, accountId }))
|
.map((accountId) => resolveFeishuAccount({ cfg, accountId }))
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import {
|
|||||||
DEFAULT_GROUP_HISTORY_LIMIT,
|
DEFAULT_GROUP_HISTORY_LIMIT,
|
||||||
type HistoryEntry,
|
type HistoryEntry,
|
||||||
} from "openclaw/plugin-sdk";
|
} 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 { createFeishuClient } from "./client.js";
|
||||||
import { downloadMessageResourceFeishu } from "./media.js";
|
import { downloadMessageResourceFeishu } from "./media.js";
|
||||||
import { extractMentionTargets, extractMessageBody, isMentionForwardRequest } from "./mention.js";
|
import { extractMentionTargets, extractMessageBody, isMentionForwardRequest } from "./mention.js";
|
||||||
@@ -79,12 +80,13 @@ type SenderNameResult = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
async function resolveFeishuSenderName(params: {
|
async function resolveFeishuSenderName(params: {
|
||||||
feishuCfg?: FeishuConfig;
|
account: ResolvedFeishuAccount;
|
||||||
senderOpenId: string;
|
senderOpenId: string;
|
||||||
log: (...args: unknown[]) => void;
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- generic log function
|
||||||
|
log: (...args: any[]) => void;
|
||||||
}): Promise<SenderNameResult> {
|
}): Promise<SenderNameResult> {
|
||||||
const { feishuCfg, senderOpenId, log } = params;
|
const { account, senderOpenId, log } = params;
|
||||||
if (!feishuCfg) {
|
if (!account.configured) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
if (!senderOpenId) {
|
if (!senderOpenId) {
|
||||||
@@ -98,10 +100,11 @@ async function resolveFeishuSenderName(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const client = createFeishuClient(feishuCfg);
|
const client = createFeishuClient(account);
|
||||||
|
|
||||||
// contact/v3/users/:user_id?user_id_type=open_id
|
// 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 },
|
path: { user_id: senderOpenId },
|
||||||
params: { user_id_type: "open_id" },
|
params: { user_id_type: "open_id" },
|
||||||
});
|
});
|
||||||
@@ -325,8 +328,9 @@ async function resolveFeishuMediaList(params: {
|
|||||||
content: string;
|
content: string;
|
||||||
maxBytes: number;
|
maxBytes: number;
|
||||||
log?: (msg: string) => void;
|
log?: (msg: string) => void;
|
||||||
|
accountId?: string;
|
||||||
}): Promise<FeishuMediaInfo[]> {
|
}): 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)
|
// Only process media message types (including post for embedded images)
|
||||||
const mediaTypes = ["image", "file", "audio", "video", "sticker", "post"];
|
const mediaTypes = ["image", "file", "audio", "video", "sticker", "post"];
|
||||||
@@ -354,6 +358,7 @@ async function resolveFeishuMediaList(params: {
|
|||||||
messageId,
|
messageId,
|
||||||
fileKey: imageKey,
|
fileKey: imageKey,
|
||||||
type: "image",
|
type: "image",
|
||||||
|
accountId,
|
||||||
});
|
});
|
||||||
|
|
||||||
let contentType = result.contentType;
|
let contentType = result.contentType;
|
||||||
@@ -407,6 +412,7 @@ async function resolveFeishuMediaList(params: {
|
|||||||
messageId,
|
messageId,
|
||||||
fileKey,
|
fileKey,
|
||||||
type: resourceType,
|
type: resourceType,
|
||||||
|
accountId,
|
||||||
});
|
});
|
||||||
buffer = result.buffer;
|
buffer = result.buffer;
|
||||||
contentType = result.contentType;
|
contentType = result.contentType;
|
||||||
@@ -506,9 +512,14 @@ export async function handleFeishuMessage(params: {
|
|||||||
botOpenId?: string;
|
botOpenId?: string;
|
||||||
runtime?: RuntimeEnv;
|
runtime?: RuntimeEnv;
|
||||||
chatHistories?: Map<string, HistoryEntry[]>;
|
chatHistories?: Map<string, HistoryEntry[]>;
|
||||||
|
accountId?: string;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const { cfg, event, botOpenId, runtime, chatHistories } = params;
|
const { cfg, event, botOpenId, runtime, chatHistories, accountId } = params;
|
||||||
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
|
||||||
|
// Resolve account with merged config
|
||||||
|
const account = resolveFeishuAccount({ cfg, accountId });
|
||||||
|
const feishuCfg = account.config;
|
||||||
|
|
||||||
const log = runtime?.log ?? console.log;
|
const log = runtime?.log ?? console.log;
|
||||||
const error = runtime?.error ?? console.error;
|
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.
|
// Resolve sender display name (best-effort) so the agent can attribute messages correctly.
|
||||||
const senderResult = await resolveFeishuSenderName({
|
const senderResult = await resolveFeishuSenderName({
|
||||||
feishuCfg,
|
account,
|
||||||
senderOpenId: ctx.senderOpenId,
|
senderOpenId: ctx.senderOpenId,
|
||||||
log,
|
log,
|
||||||
});
|
});
|
||||||
@@ -528,7 +539,7 @@ export async function handleFeishuMessage(params: {
|
|||||||
// Track permission error to inform agent later (with cooldown to avoid repetition)
|
// Track permission error to inform agent later (with cooldown to avoid repetition)
|
||||||
let permissionErrorForAgent: PermissionError | undefined;
|
let permissionErrorForAgent: PermissionError | undefined;
|
||||||
if (senderResult.permissionError) {
|
if (senderResult.permissionError) {
|
||||||
const appKey = feishuCfg?.appId ?? "default";
|
const appKey = account.appId ?? "default";
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const lastNotified = permissionErrorNotifiedAt.get(appKey) ?? 0;
|
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
|
// Log mention targets if detected
|
||||||
if (ctx.mentionTargets && ctx.mentionTargets.length > 0) {
|
if (ctx.mentionTargets && ctx.mentionTargets.length > 0) {
|
||||||
const names = ctx.mentionTargets.map((t) => t.name).join(", ");
|
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(
|
const historyLimit = Math.max(
|
||||||
@@ -554,6 +567,7 @@ export async function handleFeishuMessage(params: {
|
|||||||
if (isGroup) {
|
if (isGroup) {
|
||||||
const groupPolicy = feishuCfg?.groupPolicy ?? "open";
|
const groupPolicy = feishuCfg?.groupPolicy ?? "open";
|
||||||
const groupAllowFrom = feishuCfg?.groupAllowFrom ?? [];
|
const groupAllowFrom = feishuCfg?.groupAllowFrom ?? [];
|
||||||
|
// DEBUG: log(`feishu[${account.accountId}]: groupPolicy=${groupPolicy}`);
|
||||||
const groupConfig = resolveFeishuGroupConfig({ cfg: feishuCfg, groupId: ctx.chatId });
|
const groupConfig = resolveFeishuGroupConfig({ cfg: feishuCfg, groupId: ctx.chatId });
|
||||||
|
|
||||||
// Check if this GROUP is allowed (groupAllowFrom contains group IDs like oc_xxx, not user IDs)
|
// 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) {
|
if (!groupAllowed) {
|
||||||
log(`feishu: group ${ctx.chatId} not in allowlist`);
|
log(`feishu[${account.accountId}]: sender ${ctx.senderOpenId} not in group allowlist`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -591,7 +605,9 @@ export async function handleFeishuMessage(params: {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (requireMention && !ctx.mentionedBot) {
|
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) {
|
if (chatHistories) {
|
||||||
recordPendingHistoryEntryIfEnabled({
|
recordPendingHistoryEntryIfEnabled({
|
||||||
historyMap: chatHistories,
|
historyMap: chatHistories,
|
||||||
@@ -617,7 +633,7 @@ export async function handleFeishuMessage(params: {
|
|||||||
senderId: ctx.senderOpenId,
|
senderId: ctx.senderOpenId,
|
||||||
});
|
});
|
||||||
if (!match.allowed) {
|
if (!match.allowed) {
|
||||||
log(`feishu: sender ${ctx.senderOpenId} not in DM allowlist`);
|
log(`feishu[${account.accountId}]: sender ${ctx.senderOpenId} not in DM allowlist`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -634,6 +650,7 @@ export async function handleFeishuMessage(params: {
|
|||||||
const route = core.channel.routing.resolveAgentRoute({
|
const route = core.channel.routing.resolveAgentRoute({
|
||||||
cfg,
|
cfg,
|
||||||
channel: "feishu",
|
channel: "feishu",
|
||||||
|
accountId: account.accountId,
|
||||||
peer: {
|
peer: {
|
||||||
kind: isGroup ? "group" : "dm",
|
kind: isGroup ? "group" : "dm",
|
||||||
id: isGroup ? ctx.chatId : ctx.senderOpenId,
|
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 preview = ctx.content.replace(/\s+/g, " ").slice(0, 160);
|
||||||
const inboundLabel = isGroup
|
const inboundLabel = isGroup
|
||||||
? `Feishu message in group ${ctx.chatId}`
|
? `Feishu[${account.accountId}] message in group ${ctx.chatId}`
|
||||||
: `Feishu DM from ${ctx.senderOpenId}`;
|
: `Feishu[${account.accountId}] DM from ${ctx.senderOpenId}`;
|
||||||
|
|
||||||
core.system.enqueueSystemEvent(`${inboundLabel}: ${preview}`, {
|
core.system.enqueueSystemEvent(`${inboundLabel}: ${preview}`, {
|
||||||
sessionKey: route.sessionKey,
|
sessionKey: route.sessionKey,
|
||||||
@@ -659,6 +676,7 @@ export async function handleFeishuMessage(params: {
|
|||||||
content: event.message.content,
|
content: event.message.content,
|
||||||
maxBytes: mediaMaxBytes,
|
maxBytes: mediaMaxBytes,
|
||||||
log,
|
log,
|
||||||
|
accountId: account.accountId,
|
||||||
});
|
});
|
||||||
const mediaPayload = buildFeishuMediaPayload(mediaList);
|
const mediaPayload = buildFeishuMediaPayload(mediaList);
|
||||||
|
|
||||||
@@ -666,13 +684,19 @@ export async function handleFeishuMessage(params: {
|
|||||||
let quotedContent: string | undefined;
|
let quotedContent: string | undefined;
|
||||||
if (ctx.parentId) {
|
if (ctx.parentId) {
|
||||||
try {
|
try {
|
||||||
const quotedMsg = await getMessageFeishu({ cfg, messageId: ctx.parentId });
|
const quotedMsg = await getMessageFeishu({
|
||||||
|
cfg,
|
||||||
|
messageId: ctx.parentId,
|
||||||
|
accountId: account.accountId,
|
||||||
|
});
|
||||||
if (quotedMsg) {
|
if (quotedMsg) {
|
||||||
quotedContent = quotedMsg.content;
|
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) {
|
} 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,
|
runtime: runtime as RuntimeEnv,
|
||||||
chatId: ctx.chatId,
|
chatId: ctx.chatId,
|
||||||
replyToMessageId: ctx.messageId,
|
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({
|
await core.channel.reply.dispatchReplyFromConfig({
|
||||||
ctx: permissionCtx,
|
ctx: permissionCtx,
|
||||||
@@ -815,9 +840,10 @@ export async function handleFeishuMessage(params: {
|
|||||||
chatId: ctx.chatId,
|
chatId: ctx.chatId,
|
||||||
replyToMessageId: ctx.messageId,
|
replyToMessageId: ctx.messageId,
|
||||||
mentionTargets: ctx.mentionTargets,
|
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({
|
const { queuedFinal, counts } = await core.channel.reply.dispatchReplyFromConfig({
|
||||||
ctx: ctxPayload,
|
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) {
|
} 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 type { ChannelPlugin, ClawdbotConfig } from "openclaw/plugin-sdk";
|
||||||
import { DEFAULT_ACCOUNT_ID, PAIRING_APPROVED_MESSAGE } from "openclaw/plugin-sdk";
|
import { DEFAULT_ACCOUNT_ID, PAIRING_APPROVED_MESSAGE } from "openclaw/plugin-sdk";
|
||||||
import type { ResolvedFeishuAccount, FeishuConfig } from "./types.js";
|
import type { ResolvedFeishuAccount, FeishuConfig } from "./types.js";
|
||||||
import { resolveFeishuAccount, resolveFeishuCredentials } from "./accounts.js";
|
import {
|
||||||
|
resolveFeishuAccount,
|
||||||
|
listFeishuAccountIds,
|
||||||
|
resolveDefaultFeishuAccountId,
|
||||||
|
} from "./accounts.js";
|
||||||
import {
|
import {
|
||||||
listFeishuDirectoryPeers,
|
listFeishuDirectoryPeers,
|
||||||
listFeishuDirectoryGroups,
|
listFeishuDirectoryGroups,
|
||||||
@@ -34,11 +38,12 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
|||||||
pairing: {
|
pairing: {
|
||||||
idLabel: "feishuUserId",
|
idLabel: "feishuUserId",
|
||||||
normalizeAllowEntry: (entry) => entry.replace(/^(feishu|user|open_id):/i, ""),
|
normalizeAllowEntry: (entry) => entry.replace(/^(feishu|user|open_id):/i, ""),
|
||||||
notifyApproval: async ({ cfg, id }) => {
|
notifyApproval: async ({ cfg, id, accountId }) => {
|
||||||
await sendMessageFeishu({
|
await sendMessageFeishu({
|
||||||
cfg,
|
cfg,
|
||||||
to: id,
|
to: id,
|
||||||
text: PAIRING_APPROVED_MESSAGE,
|
text: PAIRING_APPROVED_MESSAGE,
|
||||||
|
accountId,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -94,43 +99,111 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
|||||||
chunkMode: { type: "string", enum: ["length", "newline"] },
|
chunkMode: { type: "string", enum: ["length", "newline"] },
|
||||||
mediaMaxMb: { type: "number", minimum: 0 },
|
mediaMaxMb: { type: "number", minimum: 0 },
|
||||||
renderMode: { type: "string", enum: ["auto", "raw", "card"] },
|
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: {
|
config: {
|
||||||
listAccountIds: () => [DEFAULT_ACCOUNT_ID],
|
listAccountIds: (cfg) => listFeishuAccountIds(cfg),
|
||||||
resolveAccount: (cfg) => resolveFeishuAccount({ cfg }),
|
resolveAccount: (cfg, accountId) => resolveFeishuAccount({ cfg, accountId }),
|
||||||
defaultAccountId: () => DEFAULT_ACCOUNT_ID,
|
defaultAccountId: (cfg) => resolveDefaultFeishuAccountId(cfg),
|
||||||
setAccountEnabled: ({ cfg, enabled }) => ({
|
setAccountEnabled: ({ cfg, accountId, enabled }) => {
|
||||||
...cfg,
|
const _account = resolveFeishuAccount({ cfg, accountId });
|
||||||
channels: {
|
const isDefault = accountId === DEFAULT_ACCOUNT_ID;
|
||||||
...cfg.channels,
|
|
||||||
feishu: {
|
if (isDefault) {
|
||||||
...cfg.channels?.feishu,
|
// For default account, set top-level enabled
|
||||||
enabled,
|
return {
|
||||||
},
|
...cfg,
|
||||||
},
|
channels: {
|
||||||
}),
|
...cfg.channels,
|
||||||
deleteAccount: ({ cfg }) => {
|
feishu: {
|
||||||
const next = { ...cfg } as ClawdbotConfig;
|
...cfg.channels?.feishu,
|
||||||
const nextChannels = { ...cfg.channels };
|
enabled,
|
||||||
delete (nextChannels as Record<string, unknown>).feishu;
|
},
|
||||||
if (Object.keys(nextChannels).length > 0) {
|
},
|
||||||
next.channels = nextChannels;
|
};
|
||||||
} else {
|
|
||||||
delete next.channels;
|
|
||||||
}
|
}
|
||||||
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) =>
|
deleteAccount: ({ cfg, accountId }) => {
|
||||||
Boolean(resolveFeishuCredentials(cfg.channels?.feishu as FeishuConfig | undefined)),
|
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) => ({
|
describeAccount: (account) => ({
|
||||||
accountId: account.accountId,
|
accountId: account.accountId,
|
||||||
enabled: account.enabled,
|
enabled: account.enabled,
|
||||||
configured: account.configured,
|
configured: account.configured,
|
||||||
|
name: account.name,
|
||||||
|
appId: account.appId,
|
||||||
|
domain: account.domain,
|
||||||
}),
|
}),
|
||||||
resolveAllowFrom: ({ cfg }) =>
|
resolveAllowFrom: ({ cfg, accountId }) => {
|
||||||
(cfg.channels?.feishu as FeishuConfig | undefined)?.allowFrom ?? [],
|
const account = resolveFeishuAccount({ cfg, accountId });
|
||||||
|
return account.config?.allowFrom ?? [];
|
||||||
|
},
|
||||||
formatAllowFrom: ({ allowFrom }) =>
|
formatAllowFrom: ({ allowFrom }) =>
|
||||||
allowFrom
|
allowFrom
|
||||||
.map((entry) => String(entry).trim())
|
.map((entry) => String(entry).trim())
|
||||||
@@ -138,8 +211,9 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
|||||||
.map((entry) => entry.toLowerCase()),
|
.map((entry) => entry.toLowerCase()),
|
||||||
},
|
},
|
||||||
security: {
|
security: {
|
||||||
collectWarnings: ({ cfg }) => {
|
collectWarnings: ({ cfg, accountId }) => {
|
||||||
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
const account = resolveFeishuAccount({ cfg, accountId });
|
||||||
|
const feishuCfg = account.config;
|
||||||
const defaultGroupPolicy = (
|
const defaultGroupPolicy = (
|
||||||
cfg.channels as Record<string, { groupPolicy?: string }> | undefined
|
cfg.channels as Record<string, { groupPolicy?: string }> | undefined
|
||||||
)?.defaults?.groupPolicy;
|
)?.defaults?.groupPolicy;
|
||||||
@@ -148,22 +222,46 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
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: {
|
setup: {
|
||||||
resolveAccountId: () => DEFAULT_ACCOUNT_ID,
|
resolveAccountId: () => DEFAULT_ACCOUNT_ID,
|
||||||
applyAccountConfig: ({ cfg }) => ({
|
applyAccountConfig: ({ cfg, accountId }) => {
|
||||||
...cfg,
|
const isDefault = !accountId || accountId === DEFAULT_ACCOUNT_ID;
|
||||||
channels: {
|
|
||||||
...cfg.channels,
|
if (isDefault) {
|
||||||
feishu: {
|
return {
|
||||||
...cfg.channels?.feishu,
|
...cfg,
|
||||||
enabled: true,
|
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,
|
onboarding: feishuOnboardingAdapter,
|
||||||
messaging: {
|
messaging: {
|
||||||
@@ -175,12 +273,14 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
|||||||
},
|
},
|
||||||
directory: {
|
directory: {
|
||||||
self: async () => null,
|
self: async () => null,
|
||||||
listPeers: async ({ cfg, query, limit }) => listFeishuDirectoryPeers({ cfg, query, limit }),
|
listPeers: async ({ cfg, query, limit, accountId }) =>
|
||||||
listGroups: async ({ cfg, query, limit }) => listFeishuDirectoryGroups({ cfg, query, limit }),
|
listFeishuDirectoryPeers({ cfg, query, limit, accountId }),
|
||||||
listPeersLive: async ({ cfg, query, limit }) =>
|
listGroups: async ({ cfg, query, limit, accountId }) =>
|
||||||
listFeishuDirectoryPeersLive({ cfg, query, limit }),
|
listFeishuDirectoryGroups({ cfg, query, limit, accountId }),
|
||||||
listGroupsLive: async ({ cfg, query, limit }) =>
|
listPeersLive: async ({ cfg, query, limit, accountId }) =>
|
||||||
listFeishuDirectoryGroupsLive({ cfg, query, limit }),
|
listFeishuDirectoryPeersLive({ cfg, query, limit, accountId }),
|
||||||
|
listGroupsLive: async ({ cfg, query, limit, accountId }) =>
|
||||||
|
listFeishuDirectoryGroupsLive({ cfg, query, limit, accountId }),
|
||||||
},
|
},
|
||||||
outbound: feishuOutbound,
|
outbound: feishuOutbound,
|
||||||
status: {
|
status: {
|
||||||
@@ -202,12 +302,17 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
|||||||
probe: snapshot.probe,
|
probe: snapshot.probe,
|
||||||
lastProbeAt: snapshot.lastProbeAt ?? null,
|
lastProbeAt: snapshot.lastProbeAt ?? null,
|
||||||
}),
|
}),
|
||||||
probeAccount: async ({ cfg }) =>
|
probeAccount: async ({ cfg, accountId }) => {
|
||||||
await probeFeishu(cfg.channels?.feishu as FeishuConfig | undefined),
|
const account = resolveFeishuAccount({ cfg, accountId });
|
||||||
|
return await probeFeishu(account);
|
||||||
|
},
|
||||||
buildAccountSnapshot: ({ account, runtime, probe }) => ({
|
buildAccountSnapshot: ({ account, runtime, probe }) => ({
|
||||||
accountId: account.accountId,
|
accountId: account.accountId,
|
||||||
enabled: account.enabled,
|
enabled: account.enabled,
|
||||||
configured: account.configured,
|
configured: account.configured,
|
||||||
|
name: account.name,
|
||||||
|
appId: account.appId,
|
||||||
|
domain: account.domain,
|
||||||
running: runtime?.running ?? false,
|
running: runtime?.running ?? false,
|
||||||
lastStartAt: runtime?.lastStartAt ?? null,
|
lastStartAt: runtime?.lastStartAt ?? null,
|
||||||
lastStopAt: runtime?.lastStopAt ?? null,
|
lastStopAt: runtime?.lastStopAt ?? null,
|
||||||
@@ -219,10 +324,12 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
|||||||
gateway: {
|
gateway: {
|
||||||
startAccount: async (ctx) => {
|
startAccount: async (ctx) => {
|
||||||
const { monitorFeishuProvider } = await import("./monitor.js");
|
const { monitorFeishuProvider } = await import("./monitor.js");
|
||||||
const feishuCfg = ctx.cfg.channels?.feishu as FeishuConfig | undefined;
|
const account = resolveFeishuAccount({ cfg: ctx.cfg, accountId: ctx.accountId });
|
||||||
const port = feishuCfg?.webhookPort ?? null;
|
const port = account.config?.webhookPort ?? null;
|
||||||
ctx.setStatus({ accountId: ctx.accountId, port });
|
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({
|
return monitorFeishuProvider({
|
||||||
config: ctx.cfg,
|
config: ctx.cfg,
|
||||||
runtime: ctx.runtime,
|
runtime: ctx.runtime,
|
||||||
|
|||||||
@@ -1,72 +1,118 @@
|
|||||||
import * as Lark from "@larksuiteoapi/node-sdk";
|
import * as Lark from "@larksuiteoapi/node-sdk";
|
||||||
import type { FeishuConfig, FeishuDomain } from "./types.js";
|
import type { FeishuDomain, ResolvedFeishuAccount } from "./types.js";
|
||||||
import { resolveFeishuCredentials } from "./accounts.js";
|
|
||||||
|
|
||||||
let cachedClient: Lark.Client | null = null;
|
// Multi-account client cache
|
||||||
let cachedConfig: { appId: string; appSecret: string; domain: FeishuDomain } | null = null;
|
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") {
|
if (domain === "lark") {
|
||||||
return Lark.Domain.Lark;
|
return Lark.Domain.Lark;
|
||||||
}
|
}
|
||||||
if (domain === "feishu") {
|
if (domain === "feishu" || !domain) {
|
||||||
return Lark.Domain.Feishu;
|
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);
|
* Credentials needed to create a Feishu client.
|
||||||
if (!creds) {
|
* Both FeishuConfig and ResolvedFeishuAccount satisfy this interface.
|
||||||
throw new Error("Feishu credentials not configured (appId, appSecret required)");
|
*/
|
||||||
|
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 (
|
if (
|
||||||
cachedClient &&
|
cached &&
|
||||||
cachedConfig &&
|
cached.config.appId === appId &&
|
||||||
cachedConfig.appId === creds.appId &&
|
cached.config.appSecret === appSecret &&
|
||||||
cachedConfig.appSecret === creds.appSecret &&
|
cached.config.domain === domain
|
||||||
cachedConfig.domain === creds.domain
|
|
||||||
) {
|
) {
|
||||||
return cachedClient;
|
return cached.client;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create new client
|
||||||
const client = new Lark.Client({
|
const client = new Lark.Client({
|
||||||
appId: creds.appId,
|
appId,
|
||||||
appSecret: creds.appSecret,
|
appSecret,
|
||||||
appType: Lark.AppType.SelfBuild,
|
appType: Lark.AppType.SelfBuild,
|
||||||
domain: resolveDomain(creds.domain),
|
domain: resolveDomain(domain),
|
||||||
});
|
});
|
||||||
|
|
||||||
cachedClient = client;
|
// Cache it
|
||||||
cachedConfig = { appId: creds.appId, appSecret: creds.appSecret, domain: creds.domain };
|
clientCache.set(accountId, {
|
||||||
|
client,
|
||||||
|
config: { appId, appSecret, domain },
|
||||||
|
});
|
||||||
|
|
||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createFeishuWSClient(cfg: FeishuConfig): Lark.WSClient {
|
/**
|
||||||
const creds = resolveFeishuCredentials(cfg);
|
* Create a Feishu WebSocket client for an account.
|
||||||
if (!creds) {
|
* Note: WSClient is not cached since each call creates a new connection.
|
||||||
throw new Error("Feishu credentials not configured (appId, appSecret required)");
|
*/
|
||||||
|
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({
|
return new Lark.WSClient({
|
||||||
appId: creds.appId,
|
appId,
|
||||||
appSecret: creds.appSecret,
|
appSecret,
|
||||||
domain: resolveDomain(creds.domain),
|
domain: resolveDomain(domain),
|
||||||
loggerLevel: Lark.LoggerLevel.info,
|
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({
|
return new Lark.EventDispatcher({
|
||||||
encryptKey: creds?.encryptKey,
|
encryptKey: account.encryptKey,
|
||||||
verificationToken: creds?.verificationToken,
|
verificationToken: account.verificationToken,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearClientCache() {
|
/**
|
||||||
cachedClient = null;
|
* Get a cached client for an account (if exists).
|
||||||
cachedConfig = null;
|
*/
|
||||||
|
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();
|
.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
|
export const FeishuConfigSchema = z
|
||||||
.object({
|
.object({
|
||||||
enabled: z.boolean().optional(),
|
enabled: z.boolean().optional(),
|
||||||
|
// Top-level credentials (backward compatible for single-account mode)
|
||||||
appId: z.string().optional(),
|
appId: z.string().optional(),
|
||||||
appSecret: z.string().optional(),
|
appSecret: z.string().optional(),
|
||||||
encryptKey: z.string().optional(),
|
encryptKey: z.string().optional(),
|
||||||
@@ -113,6 +152,8 @@ export const FeishuConfigSchema = z
|
|||||||
heartbeat: ChannelHeartbeatVisibilitySchema,
|
heartbeat: ChannelHeartbeatVisibilitySchema,
|
||||||
renderMode: RenderModeSchema, // raw = plain text (default), card = interactive card with markdown
|
renderMode: RenderModeSchema, // raw = plain text (default), card = interactive card with markdown
|
||||||
tools: FeishuToolsConfigSchema,
|
tools: FeishuToolsConfigSchema,
|
||||||
|
// Multi-account configuration
|
||||||
|
accounts: z.record(z.string(), FeishuAccountConfigSchema.optional()).optional(),
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
.superRefine((value, ctx) => {
|
.superRefine((value, ctx) => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { ClawdbotConfig } from "openclaw/plugin-sdk";
|
import type { ClawdbotConfig } from "openclaw/plugin-sdk";
|
||||||
import type { FeishuConfig } from "./types.js";
|
import { resolveFeishuAccount } from "./accounts.js";
|
||||||
import { createFeishuClient } from "./client.js";
|
import { createFeishuClient } from "./client.js";
|
||||||
import { normalizeFeishuTarget } from "./targets.js";
|
import { normalizeFeishuTarget } from "./targets.js";
|
||||||
|
|
||||||
@@ -19,8 +19,10 @@ export async function listFeishuDirectoryPeers(params: {
|
|||||||
cfg: ClawdbotConfig;
|
cfg: ClawdbotConfig;
|
||||||
query?: string;
|
query?: string;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
|
accountId?: string;
|
||||||
}): Promise<FeishuDirectoryPeer[]> {
|
}): 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 q = params.query?.trim().toLowerCase() || "";
|
||||||
const ids = new Set<string>();
|
const ids = new Set<string>();
|
||||||
|
|
||||||
@@ -51,8 +53,10 @@ export async function listFeishuDirectoryGroups(params: {
|
|||||||
cfg: ClawdbotConfig;
|
cfg: ClawdbotConfig;
|
||||||
query?: string;
|
query?: string;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
|
accountId?: string;
|
||||||
}): Promise<FeishuDirectoryGroup[]> {
|
}): 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 q = params.query?.trim().toLowerCase() || "";
|
||||||
const ids = new Set<string>();
|
const ids = new Set<string>();
|
||||||
|
|
||||||
@@ -82,14 +86,15 @@ export async function listFeishuDirectoryPeersLive(params: {
|
|||||||
cfg: ClawdbotConfig;
|
cfg: ClawdbotConfig;
|
||||||
query?: string;
|
query?: string;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
|
accountId?: string;
|
||||||
}): Promise<FeishuDirectoryPeer[]> {
|
}): Promise<FeishuDirectoryPeer[]> {
|
||||||
const feishuCfg = params.cfg.channels?.feishu as FeishuConfig | undefined;
|
const account = resolveFeishuAccount({ cfg: params.cfg, accountId: params.accountId });
|
||||||
if (!feishuCfg?.appId || !feishuCfg?.appSecret) {
|
if (!account.configured) {
|
||||||
return listFeishuDirectoryPeers(params);
|
return listFeishuDirectoryPeers(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const client = createFeishuClient(feishuCfg);
|
const client = createFeishuClient(account);
|
||||||
const peers: FeishuDirectoryPeer[] = [];
|
const peers: FeishuDirectoryPeer[] = [];
|
||||||
const limit = params.limit ?? 50;
|
const limit = params.limit ?? 50;
|
||||||
|
|
||||||
@@ -128,14 +133,15 @@ export async function listFeishuDirectoryGroupsLive(params: {
|
|||||||
cfg: ClawdbotConfig;
|
cfg: ClawdbotConfig;
|
||||||
query?: string;
|
query?: string;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
|
accountId?: string;
|
||||||
}): Promise<FeishuDirectoryGroup[]> {
|
}): Promise<FeishuDirectoryGroup[]> {
|
||||||
const feishuCfg = params.cfg.channels?.feishu as FeishuConfig | undefined;
|
const account = resolveFeishuAccount({ cfg: params.cfg, accountId: params.accountId });
|
||||||
if (!feishuCfg?.appId || !feishuCfg?.appSecret) {
|
if (!account.configured) {
|
||||||
return listFeishuDirectoryGroups(params);
|
return listFeishuDirectoryGroups(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const client = createFeishuClient(feishuCfg);
|
const client = createFeishuClient(account);
|
||||||
const groups: FeishuDirectoryGroup[] = [];
|
const groups: FeishuDirectoryGroup[] = [];
|
||||||
const limit = params.limit ?? 50;
|
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 { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||||
import { Type } from "@sinclair/typebox";
|
import { Type } from "@sinclair/typebox";
|
||||||
import { Readable } from "stream";
|
import { Readable } from "stream";
|
||||||
import type { FeishuConfig } from "./types.js";
|
import { listEnabledFeishuAccounts } from "./accounts.js";
|
||||||
import { createFeishuClient } from "./client.js";
|
import { createFeishuClient } from "./client.js";
|
||||||
import { FeishuDocSchema, type FeishuDocParams } from "./doc-schema.js";
|
import { FeishuDocSchema, type FeishuDocParams } from "./doc-schema.js";
|
||||||
import { resolveToolsConfig } from "./tools-config.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]);
|
const UNSUPPORTED_CREATE_TYPES = new Set([31, 32]);
|
||||||
|
|
||||||
/** Clean blocks for insertion (remove unsupported types and read-only fields) */
|
/** Clean blocks for insertion (remove unsupported types and read-only fields) */
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK block type
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK block types
|
||||||
function cleanBlocksForInsert(blocks: any[]): { cleaned: unknown[]; skipped: string[] } {
|
function cleanBlocksForInsert(blocks: any[]): { cleaned: any[]; skipped: string[] } {
|
||||||
const skipped: string[] = [];
|
const skipped: string[] = [];
|
||||||
const cleaned = blocks
|
const cleaned = blocks
|
||||||
.filter((block) => {
|
.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(
|
async function insertBlocks(
|
||||||
client: Lark.Client,
|
client: Lark.Client,
|
||||||
docToken: string,
|
docToken: string,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK block type
|
|
||||||
blocks: any[],
|
blocks: any[],
|
||||||
parentBlockId?: string,
|
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 { cleaned, skipped } = cleanBlocksForInsert(blocks);
|
||||||
const blockId = parentBlockId ?? docToken;
|
const blockId = parentBlockId ?? docToken;
|
||||||
|
|
||||||
@@ -154,7 +155,7 @@ async function uploadImageToDocx(
|
|||||||
parent_type: "docx_image",
|
parent_type: "docx_image",
|
||||||
parent_node: blockId,
|
parent_node: blockId,
|
||||||
size: imageBuffer.length,
|
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,
|
file: Readable.from(imageBuffer) as any,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -174,13 +175,14 @@ async function downloadImage(url: string): Promise<Buffer> {
|
|||||||
return Buffer.from(await response.arrayBuffer());
|
return Buffer.from(await response.arrayBuffer());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any -- SDK block types */
|
||||||
async function processImages(
|
async function processImages(
|
||||||
client: Lark.Client,
|
client: Lark.Client,
|
||||||
docToken: string,
|
docToken: string,
|
||||||
markdown: string,
|
markdown: string,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK block type
|
|
||||||
insertedBlocks: any[],
|
insertedBlocks: any[],
|
||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
|
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||||
const imageUrls = extractImageUrls(markdown);
|
const imageUrls = extractImageUrls(markdown);
|
||||||
if (imageUrls.length === 0) {
|
if (imageUrls.length === 0) {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -426,14 +428,24 @@ async function listAppScopes(client: Lark.Client) {
|
|||||||
// ============ Tool Registration ============
|
// ============ Tool Registration ============
|
||||||
|
|
||||||
export function registerFeishuDocTools(api: OpenClawPluginApi) {
|
export function registerFeishuDocTools(api: OpenClawPluginApi) {
|
||||||
const feishuCfg = api.config?.channels?.feishu as FeishuConfig | undefined;
|
if (!api.config) {
|
||||||
if (!feishuCfg?.appId || !feishuCfg?.appSecret) {
|
api.logger.debug?.("feishu_doc: No config available, skipping doc tools");
|
||||||
api.logger.debug?.("feishu_doc: Feishu credentials not configured, skipping doc tools");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const toolsCfg = resolveToolsConfig(feishuCfg.tools);
|
// Check if any account is configured
|
||||||
const getClient = () => createFeishuClient(feishuCfg);
|
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[] = [];
|
const registered: string[] = [];
|
||||||
|
|
||||||
// Main document tool with action-based dispatch
|
// Main document tool with action-based dispatch
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type * as Lark from "@larksuiteoapi/node-sdk";
|
import type * as Lark from "@larksuiteoapi/node-sdk";
|
||||||
import type { OpenClawPluginApi } from "openclaw/plugin-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 { createFeishuClient } from "./client.js";
|
||||||
import { FeishuDriveSchema, type FeishuDriveParams } from "./drive-schema.js";
|
import { FeishuDriveSchema, type FeishuDriveParams } from "./drive-schema.js";
|
||||||
import { resolveToolsConfig } from "./tools-config.js";
|
import { resolveToolsConfig } from "./tools-config.js";
|
||||||
@@ -169,19 +169,25 @@ async function deleteFile(client: Lark.Client, fileToken: string, type: string)
|
|||||||
// ============ Tool Registration ============
|
// ============ Tool Registration ============
|
||||||
|
|
||||||
export function registerFeishuDriveTools(api: OpenClawPluginApi) {
|
export function registerFeishuDriveTools(api: OpenClawPluginApi) {
|
||||||
const feishuCfg = api.config?.channels?.feishu as FeishuConfig | undefined;
|
if (!api.config) {
|
||||||
if (!feishuCfg?.appId || !feishuCfg?.appSecret) {
|
api.logger.debug?.("feishu_drive: No config available, skipping drive tools");
|
||||||
api.logger.debug?.("feishu_drive: Feishu credentials not configured, skipping drive tools");
|
|
||||||
return;
|
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) {
|
if (!toolsCfg.drive) {
|
||||||
api.logger.debug?.("feishu_drive: drive tool disabled in config");
|
api.logger.debug?.("feishu_drive: drive tool disabled in config");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getClient = () => createFeishuClient(feishuCfg);
|
const getClient = () => createFeishuClient(firstAccount);
|
||||||
|
|
||||||
api.registerTool(
|
api.registerTool(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import fs from "fs";
|
|||||||
import os from "os";
|
import os from "os";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { Readable } from "stream";
|
import { Readable } from "stream";
|
||||||
import type { FeishuConfig } from "./types.js";
|
import { resolveFeishuAccount } from "./accounts.js";
|
||||||
import { createFeishuClient } from "./client.js";
|
import { createFeishuClient } from "./client.js";
|
||||||
import { resolveReceiveIdType, normalizeFeishuTarget } from "./targets.js";
|
import { resolveReceiveIdType, normalizeFeishuTarget } from "./targets.js";
|
||||||
|
|
||||||
@@ -25,20 +25,21 @@ export type DownloadMessageResourceResult = {
|
|||||||
export async function downloadImageFeishu(params: {
|
export async function downloadImageFeishu(params: {
|
||||||
cfg: ClawdbotConfig;
|
cfg: ClawdbotConfig;
|
||||||
imageKey: string;
|
imageKey: string;
|
||||||
|
accountId?: string;
|
||||||
}): Promise<DownloadImageResult> {
|
}): Promise<DownloadImageResult> {
|
||||||
const { cfg, imageKey } = params;
|
const { cfg, imageKey, accountId } = params;
|
||||||
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
const account = resolveFeishuAccount({ cfg, accountId });
|
||||||
if (!feishuCfg) {
|
if (!account.configured) {
|
||||||
throw new Error("Feishu channel not 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({
|
const response = await client.im.image.get({
|
||||||
path: { image_key: imageKey },
|
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;
|
const responseAny = response as any;
|
||||||
if (responseAny.code !== undefined && responseAny.code !== 0) {
|
if (responseAny.code !== undefined && responseAny.code !== 0) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -104,21 +105,22 @@ export async function downloadMessageResourceFeishu(params: {
|
|||||||
messageId: string;
|
messageId: string;
|
||||||
fileKey: string;
|
fileKey: string;
|
||||||
type: "image" | "file";
|
type: "image" | "file";
|
||||||
|
accountId?: string;
|
||||||
}): Promise<DownloadMessageResourceResult> {
|
}): Promise<DownloadMessageResourceResult> {
|
||||||
const { cfg, messageId, fileKey, type } = params;
|
const { cfg, messageId, fileKey, type, accountId } = params;
|
||||||
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
const account = resolveFeishuAccount({ cfg, accountId });
|
||||||
if (!feishuCfg) {
|
if (!account.configured) {
|
||||||
throw new Error("Feishu channel not 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({
|
const response = await client.im.messageResource.get({
|
||||||
path: { message_id: messageId, file_key: fileKey },
|
path: { message_id: messageId, file_key: fileKey },
|
||||||
params: { type },
|
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;
|
const responseAny = response as any;
|
||||||
if (responseAny.code !== undefined && responseAny.code !== 0) {
|
if (responseAny.code !== undefined && responseAny.code !== 0) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -198,14 +200,15 @@ export async function uploadImageFeishu(params: {
|
|||||||
cfg: ClawdbotConfig;
|
cfg: ClawdbotConfig;
|
||||||
image: Buffer | string; // Buffer or file path
|
image: Buffer | string; // Buffer or file path
|
||||||
imageType?: "message" | "avatar";
|
imageType?: "message" | "avatar";
|
||||||
|
accountId?: string;
|
||||||
}): Promise<UploadImageResult> {
|
}): Promise<UploadImageResult> {
|
||||||
const { cfg, image, imageType = "message" } = params;
|
const { cfg, image, imageType = "message", accountId } = params;
|
||||||
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
const account = resolveFeishuAccount({ cfg, accountId });
|
||||||
if (!feishuCfg) {
|
if (!account.configured) {
|
||||||
throw new Error("Feishu channel not 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
|
// SDK expects a Readable stream, not a Buffer
|
||||||
// Use type assertion since SDK actually accepts any Readable at runtime
|
// 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({
|
const response = await client.im.image.create({
|
||||||
data: {
|
data: {
|
||||||
image_type: imageType,
|
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,
|
image: imageStream as any,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// SDK v1.30+ returns data directly without code wrapper on success
|
// SDK v1.30+ returns data directly without code wrapper on success
|
||||||
// On error, it throws or returns { code, msg }
|
// 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;
|
const responseAny = response as any;
|
||||||
if (responseAny.code !== undefined && responseAny.code !== 0) {
|
if (responseAny.code !== undefined && responseAny.code !== 0) {
|
||||||
throw new Error(`Feishu image upload failed: ${responseAny.msg || `code ${responseAny.code}`}`);
|
throw new Error(`Feishu image upload failed: ${responseAny.msg || `code ${responseAny.code}`}`);
|
||||||
@@ -245,14 +248,15 @@ export async function uploadFileFeishu(params: {
|
|||||||
fileName: string;
|
fileName: string;
|
||||||
fileType: "opus" | "mp4" | "pdf" | "doc" | "xls" | "ppt" | "stream";
|
fileType: "opus" | "mp4" | "pdf" | "doc" | "xls" | "ppt" | "stream";
|
||||||
duration?: number; // Required for audio/video files, in milliseconds
|
duration?: number; // Required for audio/video files, in milliseconds
|
||||||
|
accountId?: string;
|
||||||
}): Promise<UploadFileResult> {
|
}): Promise<UploadFileResult> {
|
||||||
const { cfg, file, fileName, fileType, duration } = params;
|
const { cfg, file, fileName, fileType, duration, accountId } = params;
|
||||||
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
const account = resolveFeishuAccount({ cfg, accountId });
|
||||||
if (!feishuCfg) {
|
if (!account.configured) {
|
||||||
throw new Error("Feishu channel not 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
|
// SDK expects a Readable stream, not a Buffer
|
||||||
// Use type assertion since SDK actually accepts any Readable at runtime
|
// Use type assertion since SDK actually accepts any Readable at runtime
|
||||||
@@ -262,14 +266,14 @@ export async function uploadFileFeishu(params: {
|
|||||||
data: {
|
data: {
|
||||||
file_type: fileType,
|
file_type: fileType,
|
||||||
file_name: fileName,
|
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,
|
file: fileStream as any,
|
||||||
...(duration !== undefined && { duration }),
|
...(duration !== undefined && { duration }),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// SDK v1.30+ returns data directly without code wrapper on success
|
// 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;
|
const responseAny = response as any;
|
||||||
if (responseAny.code !== undefined && responseAny.code !== 0) {
|
if (responseAny.code !== undefined && responseAny.code !== 0) {
|
||||||
throw new Error(`Feishu file upload failed: ${responseAny.msg || `code ${responseAny.code}`}`);
|
throw new Error(`Feishu file upload failed: ${responseAny.msg || `code ${responseAny.code}`}`);
|
||||||
@@ -291,14 +295,15 @@ export async function sendImageFeishu(params: {
|
|||||||
to: string;
|
to: string;
|
||||||
imageKey: string;
|
imageKey: string;
|
||||||
replyToMessageId?: string;
|
replyToMessageId?: string;
|
||||||
|
accountId?: string;
|
||||||
}): Promise<SendMediaResult> {
|
}): Promise<SendMediaResult> {
|
||||||
const { cfg, to, imageKey, replyToMessageId } = params;
|
const { cfg, to, imageKey, replyToMessageId, accountId } = params;
|
||||||
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
const account = resolveFeishuAccount({ cfg, accountId });
|
||||||
if (!feishuCfg) {
|
if (!account.configured) {
|
||||||
throw new Error("Feishu channel not configured");
|
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = createFeishuClient(feishuCfg);
|
const client = createFeishuClient(account);
|
||||||
const receiveId = normalizeFeishuTarget(to);
|
const receiveId = normalizeFeishuTarget(to);
|
||||||
if (!receiveId) {
|
if (!receiveId) {
|
||||||
throw new Error(`Invalid Feishu target: ${to}`);
|
throw new Error(`Invalid Feishu target: ${to}`);
|
||||||
@@ -353,14 +358,15 @@ export async function sendFileFeishu(params: {
|
|||||||
to: string;
|
to: string;
|
||||||
fileKey: string;
|
fileKey: string;
|
||||||
replyToMessageId?: string;
|
replyToMessageId?: string;
|
||||||
|
accountId?: string;
|
||||||
}): Promise<SendMediaResult> {
|
}): Promise<SendMediaResult> {
|
||||||
const { cfg, to, fileKey, replyToMessageId } = params;
|
const { cfg, to, fileKey, replyToMessageId, accountId } = params;
|
||||||
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
const account = resolveFeishuAccount({ cfg, accountId });
|
||||||
if (!feishuCfg) {
|
if (!account.configured) {
|
||||||
throw new Error("Feishu channel not configured");
|
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = createFeishuClient(feishuCfg);
|
const client = createFeishuClient(account);
|
||||||
const receiveId = normalizeFeishuTarget(to);
|
const receiveId = normalizeFeishuTarget(to);
|
||||||
if (!receiveId) {
|
if (!receiveId) {
|
||||||
throw new Error(`Invalid Feishu target: ${to}`);
|
throw new Error(`Invalid Feishu target: ${to}`);
|
||||||
@@ -465,8 +471,9 @@ export async function sendMediaFeishu(params: {
|
|||||||
mediaBuffer?: Buffer;
|
mediaBuffer?: Buffer;
|
||||||
fileName?: string;
|
fileName?: string;
|
||||||
replyToMessageId?: string;
|
replyToMessageId?: string;
|
||||||
|
accountId?: string;
|
||||||
}): Promise<SendMediaResult> {
|
}): Promise<SendMediaResult> {
|
||||||
const { cfg, to, mediaUrl, mediaBuffer, fileName, replyToMessageId } = params;
|
const { cfg, to, mediaUrl, mediaBuffer, fileName, replyToMessageId, accountId } = params;
|
||||||
|
|
||||||
let buffer: Buffer;
|
let buffer: Buffer;
|
||||||
let name: string;
|
let name: string;
|
||||||
@@ -504,8 +511,8 @@ export async function sendMediaFeishu(params: {
|
|||||||
const isImage = [".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".ico", ".tiff"].includes(ext);
|
const isImage = [".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".ico", ".tiff"].includes(ext);
|
||||||
|
|
||||||
if (isImage) {
|
if (isImage) {
|
||||||
const { imageKey } = await uploadImageFeishu({ cfg, image: buffer });
|
const { imageKey } = await uploadImageFeishu({ cfg, image: buffer, accountId });
|
||||||
return sendImageFeishu({ cfg, to, imageKey, replyToMessageId });
|
return sendImageFeishu({ cfg, to, imageKey, replyToMessageId, accountId });
|
||||||
} else {
|
} else {
|
||||||
const fileType = detectFileType(name);
|
const fileType = detectFileType(name);
|
||||||
const { fileKey } = await uploadFileFeishu({
|
const { fileKey } = await uploadFileFeishu({
|
||||||
@@ -513,7 +520,8 @@ export async function sendMediaFeishu(params: {
|
|||||||
file: buffer,
|
file: buffer,
|
||||||
fileName: name,
|
fileName: name,
|
||||||
fileType,
|
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 type { ClawdbotConfig, RuntimeEnv, HistoryEntry } from "openclaw/plugin-sdk";
|
||||||
import * as Lark from "@larksuiteoapi/node-sdk";
|
import * as Lark from "@larksuiteoapi/node-sdk";
|
||||||
import type { FeishuConfig } from "./types.js";
|
import type { ResolvedFeishuAccount } from "./types.js";
|
||||||
import { resolveFeishuCredentials } from "./accounts.js";
|
import { resolveFeishuAccount, listEnabledFeishuAccounts } from "./accounts.js";
|
||||||
import { handleFeishuMessage, type FeishuMessageEvent, type FeishuBotAddedEvent } from "./bot.js";
|
import { handleFeishuMessage, type FeishuMessageEvent, type FeishuBotAddedEvent } from "./bot.js";
|
||||||
import { createFeishuWSClient, createEventDispatcher } from "./client.js";
|
import { createFeishuWSClient, createEventDispatcher } from "./client.js";
|
||||||
import { probeFeishu } from "./probe.js";
|
import { probeFeishu } from "./probe.js";
|
||||||
@@ -13,71 +13,52 @@ export type MonitorFeishuOpts = {
|
|||||||
accountId?: string;
|
accountId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
let currentWsClient: Lark.WSClient | null = null;
|
// Per-account WebSocket clients and bot info
|
||||||
let botOpenId: string | undefined;
|
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 {
|
try {
|
||||||
const result = await probeFeishu(cfg);
|
const result = await probeFeishu(account);
|
||||||
return result.ok ? result.botOpenId : undefined;
|
return result.ok ? result.botOpenId : undefined;
|
||||||
} catch {
|
} catch {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function monitorFeishuProvider(opts: MonitorFeishuOpts = {}): Promise<void> {
|
/**
|
||||||
const cfg = opts.config;
|
* Monitor a single Feishu account.
|
||||||
if (!cfg) {
|
*/
|
||||||
throw new Error("Config is required for Feishu monitor");
|
async function monitorSingleAccount(params: {
|
||||||
}
|
|
||||||
|
|
||||||
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: {
|
|
||||||
cfg: ClawdbotConfig;
|
cfg: ClawdbotConfig;
|
||||||
feishuCfg: FeishuConfig;
|
account: ResolvedFeishuAccount;
|
||||||
runtime?: RuntimeEnv;
|
runtime?: RuntimeEnv;
|
||||||
abortSignal?: AbortSignal;
|
abortSignal?: AbortSignal;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const { cfg, feishuCfg, runtime, abortSignal } = params;
|
const { cfg, account, runtime, abortSignal } = params;
|
||||||
|
const { accountId } = account;
|
||||||
const log = runtime?.log ?? console.log;
|
const log = runtime?.log ?? console.log;
|
||||||
const error = runtime?.error ?? console.error;
|
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);
|
const connectionMode = account.config.connectionMode ?? "websocket";
|
||||||
currentWsClient = wsClient;
|
|
||||||
|
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 chatHistories = new Map<string, HistoryEntry[]>();
|
||||||
|
const eventDispatcher = createEventDispatcher(account);
|
||||||
const eventDispatcher = createEventDispatcher(feishuCfg);
|
|
||||||
|
|
||||||
eventDispatcher.register({
|
eventDispatcher.register({
|
||||||
"im.message.receive_v1": async (data) => {
|
"im.message.receive_v1": async (data) => {
|
||||||
@@ -86,12 +67,13 @@ async function monitorWebSocket(params: {
|
|||||||
await handleFeishuMessage({
|
await handleFeishuMessage({
|
||||||
cfg,
|
cfg,
|
||||||
event,
|
event,
|
||||||
botOpenId,
|
botOpenId: botOpenIds.get(accountId),
|
||||||
runtime,
|
runtime,
|
||||||
chatHistories,
|
chatHistories,
|
||||||
|
accountId,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error(`feishu: error handling message event: ${String(err)}`);
|
error(`feishu[${accountId}]: error handling message: ${String(err)}`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"im.message.message_read_v1": async () => {
|
"im.message.message_read_v1": async () => {
|
||||||
@@ -100,30 +82,29 @@ async function monitorWebSocket(params: {
|
|||||||
"im.chat.member.bot.added_v1": async (data) => {
|
"im.chat.member.bot.added_v1": async (data) => {
|
||||||
try {
|
try {
|
||||||
const event = data as unknown as FeishuBotAddedEvent;
|
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) {
|
} 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) => {
|
"im.chat.member.bot.deleted_v1": async (data) => {
|
||||||
try {
|
try {
|
||||||
const event = data as unknown as { chat_id: string };
|
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) {
|
} 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) => {
|
return new Promise((resolve, reject) => {
|
||||||
const cleanup = () => {
|
const cleanup = () => {
|
||||||
if (currentWsClient === wsClient) {
|
wsClients.delete(accountId);
|
||||||
currentWsClient = null;
|
botOpenIds.delete(accountId);
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAbort = () => {
|
const handleAbort = () => {
|
||||||
log("feishu: abort signal received, stopping WebSocket client");
|
log(`feishu[${accountId}]: abort signal received, stopping`);
|
||||||
cleanup();
|
cleanup();
|
||||||
resolve();
|
resolve();
|
||||||
};
|
};
|
||||||
@@ -137,11 +118,8 @@ async function monitorWebSocket(params: {
|
|||||||
abortSignal?.addEventListener("abort", handleAbort, { once: true });
|
abortSignal?.addEventListener("abort", handleAbort, { once: true });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
void wsClient.start({
|
void wsClient.start({ eventDispatcher });
|
||||||
eventDispatcher,
|
log(`feishu[${accountId}]: WebSocket client started`);
|
||||||
});
|
|
||||||
|
|
||||||
log("feishu: WebSocket client started");
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
cleanup();
|
cleanup();
|
||||||
abortSignal?.removeEventListener("abort", handleAbort);
|
abortSignal?.removeEventListener("abort", handleAbort);
|
||||||
@@ -150,8 +128,63 @@ async function monitorWebSocket(params: {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function stopFeishuMonitor(): void {
|
/**
|
||||||
if (currentWsClient) {
|
* Main entry: start monitoring for all enabled accounts.
|
||||||
currentWsClient = null;
|
*/
|
||||||
|
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),
|
chunker: (text, limit) => getFeishuRuntime().channel.text.chunkMarkdownText(text, limit),
|
||||||
chunkerMode: "markdown",
|
chunkerMode: "markdown",
|
||||||
textChunkLimit: 4000,
|
textChunkLimit: 4000,
|
||||||
sendText: async ({ cfg, to, text }) => {
|
sendText: async ({ cfg, to, text, accountId }) => {
|
||||||
const result = await sendMessageFeishu({ cfg, to, text });
|
const result = await sendMessageFeishu({ cfg, to, text, accountId });
|
||||||
return { channel: "feishu", ...result };
|
return { channel: "feishu", ...result };
|
||||||
},
|
},
|
||||||
sendMedia: async ({ cfg, to, text, mediaUrl }) => {
|
sendMedia: async ({ cfg, to, text, mediaUrl, accountId }) => {
|
||||||
// Send text first if provided
|
// Send text first if provided
|
||||||
if (text?.trim()) {
|
if (text?.trim()) {
|
||||||
await sendMessageFeishu({ cfg, to, text });
|
await sendMessageFeishu({ cfg, to, text, accountId });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upload and send media if URL provided
|
// Upload and send media if URL provided
|
||||||
if (mediaUrl) {
|
if (mediaUrl) {
|
||||||
try {
|
try {
|
||||||
const result = await sendMediaFeishu({ cfg, to, mediaUrl });
|
const result = await sendMediaFeishu({ cfg, to, mediaUrl, accountId });
|
||||||
return { channel: "feishu", ...result };
|
return { channel: "feishu", ...result };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Log the error for debugging
|
// Log the error for debugging
|
||||||
console.error(`[feishu] sendMediaFeishu failed:`, err);
|
console.error(`[feishu] sendMediaFeishu failed:`, err);
|
||||||
// Fallback to URL link if upload fails
|
// Fallback to URL link if upload fails
|
||||||
const fallbackText = `📎 ${mediaUrl}`;
|
const fallbackText = `📎 ${mediaUrl}`;
|
||||||
const result = await sendMessageFeishu({ cfg, to, text: fallbackText });
|
const result = await sendMessageFeishu({ cfg, to, text: fallbackText, accountId });
|
||||||
return { channel: "feishu", ...result };
|
return { channel: "feishu", ...result };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// No media URL, just return text 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 };
|
return { channel: "feishu", ...result };
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type * as Lark from "@larksuiteoapi/node-sdk";
|
import type * as Lark from "@larksuiteoapi/node-sdk";
|
||||||
import type { OpenClawPluginApi } from "openclaw/plugin-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 { createFeishuClient } from "./client.js";
|
||||||
import { FeishuPermSchema, type FeishuPermParams } from "./perm-schema.js";
|
import { FeishuPermSchema, type FeishuPermParams } from "./perm-schema.js";
|
||||||
import { resolveToolsConfig } from "./tools-config.js";
|
import { resolveToolsConfig } from "./tools-config.js";
|
||||||
@@ -118,19 +118,25 @@ async function removeMember(
|
|||||||
// ============ Tool Registration ============
|
// ============ Tool Registration ============
|
||||||
|
|
||||||
export function registerFeishuPermTools(api: OpenClawPluginApi) {
|
export function registerFeishuPermTools(api: OpenClawPluginApi) {
|
||||||
const feishuCfg = api.config?.channels?.feishu as FeishuConfig | undefined;
|
if (!api.config) {
|
||||||
if (!feishuCfg?.appId || !feishuCfg?.appSecret) {
|
api.logger.debug?.("feishu_perm: No config available, skipping perm tools");
|
||||||
api.logger.debug?.("feishu_perm: Feishu credentials not configured, skipping perm tools");
|
|
||||||
return;
|
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) {
|
if (!toolsCfg.perm) {
|
||||||
api.logger.debug?.("feishu_perm: perm tool disabled in config (default: false)");
|
api.logger.debug?.("feishu_perm: perm tool disabled in config (default: false)");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getClient = () => createFeishuClient(feishuCfg);
|
const getClient = () => createFeishuClient(firstAccount);
|
||||||
|
|
||||||
api.registerTool(
|
api.registerTool(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -58,7 +58,6 @@ export function resolveFeishuGroupConfig(params: {
|
|||||||
|
|
||||||
export function resolveFeishuGroupToolPolicy(
|
export function resolveFeishuGroupToolPolicy(
|
||||||
params: ChannelGroupContext,
|
params: ChannelGroupContext,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents -- type resolution issue with plugin-sdk
|
|
||||||
): GroupToolPolicyConfig | undefined {
|
): GroupToolPolicyConfig | undefined {
|
||||||
const cfg = params.cfg.channels?.feishu as FeishuConfig | undefined;
|
const cfg = params.cfg.channels?.feishu as FeishuConfig | undefined;
|
||||||
if (!cfg) {
|
if (!cfg) {
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import type { FeishuConfig, FeishuProbeResult } from "./types.js";
|
import type { FeishuProbeResult } from "./types.js";
|
||||||
import { resolveFeishuCredentials } from "./accounts.js";
|
import { createFeishuClient, type FeishuClientCredentials } from "./client.js";
|
||||||
import { createFeishuClient } from "./client.js";
|
|
||||||
|
|
||||||
export async function probeFeishu(cfg?: FeishuConfig): Promise<FeishuProbeResult> {
|
export async function probeFeishu(creds?: FeishuClientCredentials): Promise<FeishuProbeResult> {
|
||||||
const creds = resolveFeishuCredentials(cfg);
|
if (!creds?.appId || !creds?.appSecret) {
|
||||||
if (!creds) {
|
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
error: "missing credentials (appId, appSecret)",
|
error: "missing credentials (appId, appSecret)",
|
||||||
@@ -12,10 +10,9 @@ export async function probeFeishu(cfg?: FeishuConfig): Promise<FeishuProbeResult
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const client = createFeishuClient(cfg!);
|
const client = createFeishuClient(creds);
|
||||||
// Use im.chat.list as a simple connectivity test
|
// Use bot/v3/info API to get bot information
|
||||||
// The bot info API path varies by SDK version
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK generic request method
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- accessing internal SDK method
|
|
||||||
const response = await (client as any).request({
|
const response = await (client as any).request({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/open-apis/bot/v3/info",
|
url: "/open-apis/bot/v3/info",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { ClawdbotConfig } from "openclaw/plugin-sdk";
|
import type { ClawdbotConfig } from "openclaw/plugin-sdk";
|
||||||
import type { FeishuConfig } from "./types.js";
|
import { resolveFeishuAccount } from "./accounts.js";
|
||||||
import { createFeishuClient } from "./client.js";
|
import { createFeishuClient } from "./client.js";
|
||||||
|
|
||||||
export type FeishuReaction = {
|
export type FeishuReaction = {
|
||||||
@@ -18,14 +18,15 @@ export async function addReactionFeishu(params: {
|
|||||||
cfg: ClawdbotConfig;
|
cfg: ClawdbotConfig;
|
||||||
messageId: string;
|
messageId: string;
|
||||||
emojiType: string;
|
emojiType: string;
|
||||||
|
accountId?: string;
|
||||||
}): Promise<{ reactionId: string }> {
|
}): Promise<{ reactionId: string }> {
|
||||||
const { cfg, messageId, emojiType } = params;
|
const { cfg, messageId, emojiType, accountId } = params;
|
||||||
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
const account = resolveFeishuAccount({ cfg, accountId });
|
||||||
if (!feishuCfg) {
|
if (!account.configured) {
|
||||||
throw new Error("Feishu channel not 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({
|
const response = (await client.im.messageReaction.create({
|
||||||
path: { message_id: messageId },
|
path: { message_id: messageId },
|
||||||
@@ -59,14 +60,15 @@ export async function removeReactionFeishu(params: {
|
|||||||
cfg: ClawdbotConfig;
|
cfg: ClawdbotConfig;
|
||||||
messageId: string;
|
messageId: string;
|
||||||
reactionId: string;
|
reactionId: string;
|
||||||
|
accountId?: string;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const { cfg, messageId, reactionId } = params;
|
const { cfg, messageId, reactionId, accountId } = params;
|
||||||
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
const account = resolveFeishuAccount({ cfg, accountId });
|
||||||
if (!feishuCfg) {
|
if (!account.configured) {
|
||||||
throw new Error("Feishu channel not 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({
|
const response = (await client.im.messageReaction.delete({
|
||||||
path: {
|
path: {
|
||||||
@@ -87,14 +89,15 @@ export async function listReactionsFeishu(params: {
|
|||||||
cfg: ClawdbotConfig;
|
cfg: ClawdbotConfig;
|
||||||
messageId: string;
|
messageId: string;
|
||||||
emojiType?: string;
|
emojiType?: string;
|
||||||
|
accountId?: string;
|
||||||
}): Promise<FeishuReaction[]> {
|
}): Promise<FeishuReaction[]> {
|
||||||
const { cfg, messageId, emojiType } = params;
|
const { cfg, messageId, emojiType, accountId } = params;
|
||||||
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
const account = resolveFeishuAccount({ cfg, accountId });
|
||||||
if (!feishuCfg) {
|
if (!account.configured) {
|
||||||
throw new Error("Feishu channel not 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({
|
const response = (await client.im.messageReaction.list({
|
||||||
path: { message_id: messageId },
|
path: { message_id: messageId },
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
type ReplyPayload,
|
type ReplyPayload,
|
||||||
} from "openclaw/plugin-sdk";
|
} from "openclaw/plugin-sdk";
|
||||||
import type { MentionTarget } from "./mention.js";
|
import type { MentionTarget } from "./mention.js";
|
||||||
import type { FeishuConfig } from "./types.js";
|
import { resolveFeishuAccount } from "./accounts.js";
|
||||||
import { getFeishuRuntime } from "./runtime.js";
|
import { getFeishuRuntime } from "./runtime.js";
|
||||||
import { sendMessageFeishu, sendMarkdownCardFeishu } from "./send.js";
|
import { sendMessageFeishu, sendMarkdownCardFeishu } from "./send.js";
|
||||||
import { addTypingIndicator, removeTypingIndicator, type TypingIndicatorState } from "./typing.js";
|
import { addTypingIndicator, removeTypingIndicator, type TypingIndicatorState } from "./typing.js";
|
||||||
@@ -36,11 +36,16 @@ export type CreateFeishuReplyDispatcherParams = {
|
|||||||
replyToMessageId?: string;
|
replyToMessageId?: string;
|
||||||
/** Mention targets, will be auto-included in replies */
|
/** Mention targets, will be auto-included in replies */
|
||||||
mentionTargets?: MentionTarget[];
|
mentionTargets?: MentionTarget[];
|
||||||
|
/** Account ID for multi-account support */
|
||||||
|
accountId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherParams) {
|
export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherParams) {
|
||||||
const core = getFeishuRuntime();
|
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({
|
const prefixContext = createReplyPrefixContext({
|
||||||
cfg,
|
cfg,
|
||||||
@@ -56,16 +61,16 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
|
|||||||
if (!replyToMessageId) {
|
if (!replyToMessageId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
typingState = await addTypingIndicator({ cfg, messageId: replyToMessageId });
|
typingState = await addTypingIndicator({ cfg, messageId: replyToMessageId, accountId });
|
||||||
params.runtime.log?.(`feishu: added typing indicator reaction`);
|
params.runtime.log?.(`feishu[${account.accountId}]: added typing indicator reaction`);
|
||||||
},
|
},
|
||||||
stop: async () => {
|
stop: async () => {
|
||||||
if (!typingState) {
|
if (!typingState) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await removeTypingIndicator({ cfg, state: typingState });
|
await removeTypingIndicator({ cfg, state: typingState, accountId });
|
||||||
typingState = null;
|
typingState = null;
|
||||||
params.runtime.log?.(`feishu: removed typing indicator reaction`);
|
params.runtime.log?.(`feishu[${account.accountId}]: removed typing indicator reaction`);
|
||||||
},
|
},
|
||||||
onStartError: (err) => {
|
onStartError: (err) => {
|
||||||
logTypingFailure({
|
logTypingFailure({
|
||||||
@@ -103,15 +108,17 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
|
|||||||
humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, agentId),
|
humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, agentId),
|
||||||
onReplyStart: typingCallbacks.onReplyStart,
|
onReplyStart: typingCallbacks.onReplyStart,
|
||||||
deliver: async (payload: ReplyPayload) => {
|
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 ?? "";
|
const text = payload.text ?? "";
|
||||||
if (!text.trim()) {
|
if (!text.trim()) {
|
||||||
params.runtime.log?.(`feishu deliver: empty text, skipping`);
|
params.runtime.log?.(`feishu[${account.accountId}] deliver: empty text, skipping`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check render mode: auto (default), raw, or card
|
// 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";
|
const renderMode = feishuCfg?.renderMode ?? "auto";
|
||||||
|
|
||||||
// Determine if we should use card for this message
|
// Determine if we should use card for this message
|
||||||
@@ -123,7 +130,9 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
|
|||||||
if (useCard) {
|
if (useCard) {
|
||||||
// Card mode: send as interactive card with markdown rendering
|
// Card mode: send as interactive card with markdown rendering
|
||||||
const chunks = core.channel.text.chunkTextWithMode(text, textChunkLimit, chunkMode);
|
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) {
|
for (const chunk of chunks) {
|
||||||
await sendMarkdownCardFeishu({
|
await sendMarkdownCardFeishu({
|
||||||
cfg,
|
cfg,
|
||||||
@@ -131,6 +140,7 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
|
|||||||
text: chunk,
|
text: chunk,
|
||||||
replyToMessageId,
|
replyToMessageId,
|
||||||
mentions: isFirstChunk ? mentionTargets : undefined,
|
mentions: isFirstChunk ? mentionTargets : undefined,
|
||||||
|
accountId,
|
||||||
});
|
});
|
||||||
isFirstChunk = false;
|
isFirstChunk = false;
|
||||||
}
|
}
|
||||||
@@ -138,7 +148,9 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
|
|||||||
// Raw mode: send as plain text with table conversion
|
// Raw mode: send as plain text with table conversion
|
||||||
const converted = core.channel.text.convertMarkdownTables(text, tableMode);
|
const converted = core.channel.text.convertMarkdownTables(text, tableMode);
|
||||||
const chunks = core.channel.text.chunkTextWithMode(converted, textChunkLimit, chunkMode);
|
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) {
|
for (const chunk of chunks) {
|
||||||
await sendMessageFeishu({
|
await sendMessageFeishu({
|
||||||
cfg,
|
cfg,
|
||||||
@@ -146,13 +158,16 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
|
|||||||
text: chunk,
|
text: chunk,
|
||||||
replyToMessageId,
|
replyToMessageId,
|
||||||
mentions: isFirstChunk ? mentionTargets : undefined,
|
mentions: isFirstChunk ? mentionTargets : undefined,
|
||||||
|
accountId,
|
||||||
});
|
});
|
||||||
isFirstChunk = false;
|
isFirstChunk = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: (err, info) => {
|
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?.();
|
typingCallbacks.onIdle?.();
|
||||||
},
|
},
|
||||||
onIdle: typingCallbacks.onIdle,
|
onIdle: typingCallbacks.onIdle,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { PluginRuntime } from "openclaw/plugin-sdk";
|
import type { PluginRuntime } from "openclaw/plugin-sdk";
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
|
|
||||||
let runtime: PluginRuntime | null = null;
|
let runtime: PluginRuntime | null = null;
|
||||||
|
|
||||||
export function setFeishuRuntime(next: PluginRuntime) {
|
export function setFeishuRuntime(next: PluginRuntime) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { ClawdbotConfig } from "openclaw/plugin-sdk";
|
import type { ClawdbotConfig } from "openclaw/plugin-sdk";
|
||||||
import type { MentionTarget } from "./mention.js";
|
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 { createFeishuClient } from "./client.js";
|
||||||
import { buildMentionedMessage, buildMentionedCardContent } from "./mention.js";
|
import { buildMentionedMessage, buildMentionedCardContent } from "./mention.js";
|
||||||
import { getFeishuRuntime } from "./runtime.js";
|
import { getFeishuRuntime } from "./runtime.js";
|
||||||
@@ -23,14 +24,15 @@ export type FeishuMessageInfo = {
|
|||||||
export async function getMessageFeishu(params: {
|
export async function getMessageFeishu(params: {
|
||||||
cfg: ClawdbotConfig;
|
cfg: ClawdbotConfig;
|
||||||
messageId: string;
|
messageId: string;
|
||||||
|
accountId?: string;
|
||||||
}): Promise<FeishuMessageInfo | null> {
|
}): Promise<FeishuMessageInfo | null> {
|
||||||
const { cfg, messageId } = params;
|
const { cfg, messageId, accountId } = params;
|
||||||
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
const account = resolveFeishuAccount({ cfg, accountId });
|
||||||
if (!feishuCfg) {
|
if (!account.configured) {
|
||||||
throw new Error("Feishu channel not configured");
|
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = createFeishuClient(feishuCfg);
|
const client = createFeishuClient(account);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = (await client.im.message.get({
|
const response = (await client.im.message.get({
|
||||||
@@ -95,9 +97,11 @@ export type SendFeishuMessageParams = {
|
|||||||
replyToMessageId?: string;
|
replyToMessageId?: string;
|
||||||
/** Mention target users */
|
/** Mention target users */
|
||||||
mentions?: MentionTarget[];
|
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;
|
content: string;
|
||||||
msgType: string;
|
msgType: string;
|
||||||
} {
|
} {
|
||||||
@@ -122,13 +126,13 @@ function buildFeishuPostMessagePayload(params: { feishuCfg: FeishuConfig; messag
|
|||||||
export async function sendMessageFeishu(
|
export async function sendMessageFeishu(
|
||||||
params: SendFeishuMessageParams,
|
params: SendFeishuMessageParams,
|
||||||
): Promise<FeishuSendResult> {
|
): Promise<FeishuSendResult> {
|
||||||
const { cfg, to, text, replyToMessageId, mentions } = params;
|
const { cfg, to, text, replyToMessageId, mentions, accountId } = params;
|
||||||
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
const account = resolveFeishuAccount({ cfg, accountId });
|
||||||
if (!feishuCfg) {
|
if (!account.configured) {
|
||||||
throw new Error("Feishu channel not configured");
|
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = createFeishuClient(feishuCfg);
|
const client = createFeishuClient(account);
|
||||||
const receiveId = normalizeFeishuTarget(to);
|
const receiveId = normalizeFeishuTarget(to);
|
||||||
if (!receiveId) {
|
if (!receiveId) {
|
||||||
throw new Error(`Invalid Feishu target: ${to}`);
|
throw new Error(`Invalid Feishu target: ${to}`);
|
||||||
@@ -147,10 +151,7 @@ export async function sendMessageFeishu(
|
|||||||
}
|
}
|
||||||
const messageText = getFeishuRuntime().channel.text.convertMarkdownTables(rawText, tableMode);
|
const messageText = getFeishuRuntime().channel.text.convertMarkdownTables(rawText, tableMode);
|
||||||
|
|
||||||
const { content, msgType } = buildFeishuPostMessagePayload({
|
const { content, msgType } = buildFeishuPostMessagePayload({ messageText });
|
||||||
feishuCfg,
|
|
||||||
messageText,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (replyToMessageId) {
|
if (replyToMessageId) {
|
||||||
const response = await client.im.message.reply({
|
const response = await client.im.message.reply({
|
||||||
@@ -195,16 +196,17 @@ export type SendFeishuCardParams = {
|
|||||||
to: string;
|
to: string;
|
||||||
card: Record<string, unknown>;
|
card: Record<string, unknown>;
|
||||||
replyToMessageId?: string;
|
replyToMessageId?: string;
|
||||||
|
accountId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function sendCardFeishu(params: SendFeishuCardParams): Promise<FeishuSendResult> {
|
export async function sendCardFeishu(params: SendFeishuCardParams): Promise<FeishuSendResult> {
|
||||||
const { cfg, to, card, replyToMessageId } = params;
|
const { cfg, to, card, replyToMessageId, accountId } = params;
|
||||||
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
const account = resolveFeishuAccount({ cfg, accountId });
|
||||||
if (!feishuCfg) {
|
if (!account.configured) {
|
||||||
throw new Error("Feishu channel not configured");
|
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = createFeishuClient(feishuCfg);
|
const client = createFeishuClient(account);
|
||||||
const receiveId = normalizeFeishuTarget(to);
|
const receiveId = normalizeFeishuTarget(to);
|
||||||
if (!receiveId) {
|
if (!receiveId) {
|
||||||
throw new Error(`Invalid Feishu target: ${to}`);
|
throw new Error(`Invalid Feishu target: ${to}`);
|
||||||
@@ -255,14 +257,15 @@ export async function updateCardFeishu(params: {
|
|||||||
cfg: ClawdbotConfig;
|
cfg: ClawdbotConfig;
|
||||||
messageId: string;
|
messageId: string;
|
||||||
card: Record<string, unknown>;
|
card: Record<string, unknown>;
|
||||||
|
accountId?: string;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const { cfg, messageId, card } = params;
|
const { cfg, messageId, card, accountId } = params;
|
||||||
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
const account = resolveFeishuAccount({ cfg, accountId });
|
||||||
if (!feishuCfg) {
|
if (!account.configured) {
|
||||||
throw new Error("Feishu channel not configured");
|
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = createFeishuClient(feishuCfg);
|
const client = createFeishuClient(account);
|
||||||
const content = JSON.stringify(card);
|
const content = JSON.stringify(card);
|
||||||
|
|
||||||
const response = await client.im.message.patch({
|
const response = await client.im.message.patch({
|
||||||
@@ -304,15 +307,16 @@ export async function sendMarkdownCardFeishu(params: {
|
|||||||
replyToMessageId?: string;
|
replyToMessageId?: string;
|
||||||
/** Mention target users */
|
/** Mention target users */
|
||||||
mentions?: MentionTarget[];
|
mentions?: MentionTarget[];
|
||||||
|
accountId?: string;
|
||||||
}): Promise<FeishuSendResult> {
|
}): Promise<FeishuSendResult> {
|
||||||
const { cfg, to, text, replyToMessageId, mentions } = params;
|
const { cfg, to, text, replyToMessageId, mentions, accountId } = params;
|
||||||
// Build message content (with @mention support)
|
// Build message content (with @mention support)
|
||||||
let cardText = text;
|
let cardText = text;
|
||||||
if (mentions && mentions.length > 0) {
|
if (mentions && mentions.length > 0) {
|
||||||
cardText = buildMentionedCardContent(mentions, text);
|
cardText = buildMentionedCardContent(mentions, text);
|
||||||
}
|
}
|
||||||
const card = buildMarkdownCard(cardText);
|
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;
|
cfg: ClawdbotConfig;
|
||||||
messageId: string;
|
messageId: string;
|
||||||
text: string;
|
text: string;
|
||||||
|
accountId?: string;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const { cfg, messageId, text } = params;
|
const { cfg, messageId, text, accountId } = params;
|
||||||
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
const account = resolveFeishuAccount({ cfg, accountId });
|
||||||
if (!feishuCfg) {
|
if (!account.configured) {
|
||||||
throw new Error("Feishu channel not configured");
|
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = createFeishuClient(feishuCfg);
|
const client = createFeishuClient(account);
|
||||||
const tableMode = getFeishuRuntime().channel.text.resolveMarkdownTableMode({
|
const tableMode = getFeishuRuntime().channel.text.resolveMarkdownTableMode({
|
||||||
cfg,
|
cfg,
|
||||||
channel: "feishu",
|
channel: "feishu",
|
||||||
});
|
});
|
||||||
const messageText = getFeishuRuntime().channel.text.convertMarkdownTables(text ?? "", tableMode);
|
const messageText = getFeishuRuntime().channel.text.convertMarkdownTables(text ?? "", tableMode);
|
||||||
|
|
||||||
const { content, msgType } = buildFeishuPostMessagePayload({
|
const { content, msgType } = buildFeishuPostMessagePayload({ messageText });
|
||||||
feishuCfg,
|
|
||||||
messageText,
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await client.im.message.update({
|
const response = await client.im.message.update({
|
||||||
path: { message_id: messageId },
|
path: { message_id: messageId },
|
||||||
|
|||||||
@@ -57,8 +57,7 @@ export function resolveReceiveIdType(id: string): "chat_id" | "open_id" | "user_
|
|||||||
if (trimmed.startsWith(OPEN_ID_PREFIX)) {
|
if (trimmed.startsWith(OPEN_ID_PREFIX)) {
|
||||||
return "open_id";
|
return "open_id";
|
||||||
}
|
}
|
||||||
// Default to user_id for other alphanumeric IDs (e.g., enterprise user IDs)
|
return "open_id";
|
||||||
return "user_id";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function looksLikeFeishuId(raw: string): boolean {
|
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";
|
import type { MentionTarget } from "./mention.js";
|
||||||
|
|
||||||
export type FeishuConfig = z.infer<typeof FeishuConfigSchema>;
|
export type FeishuConfig = z.infer<typeof FeishuConfigSchema>;
|
||||||
export type FeishuGroupConfig = z.infer<typeof FeishuGroupSchema>;
|
export type FeishuGroupConfig = z.infer<typeof FeishuGroupSchema>;
|
||||||
|
export type FeishuAccountConfig = z.infer<typeof FeishuAccountConfigSchema>;
|
||||||
|
|
||||||
export type FeishuDomain = "feishu" | "lark" | (string & {});
|
export type FeishuDomain = "feishu" | "lark" | (string & {});
|
||||||
export type FeishuConnectionMode = "websocket" | "webhook";
|
export type FeishuConnectionMode = "websocket" | "webhook";
|
||||||
@@ -11,8 +17,14 @@ export type ResolvedFeishuAccount = {
|
|||||||
accountId: string;
|
accountId: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
configured: boolean;
|
configured: boolean;
|
||||||
|
name?: string;
|
||||||
appId?: string;
|
appId?: string;
|
||||||
|
appSecret?: string;
|
||||||
|
encryptKey?: string;
|
||||||
|
verificationToken?: string;
|
||||||
domain: FeishuDomain;
|
domain: FeishuDomain;
|
||||||
|
/** Merged config (top-level defaults + account-specific overrides) */
|
||||||
|
config: FeishuConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FeishuIdType = "open_id" | "user_id" | "union_id" | "chat_id";
|
export type FeishuIdType = "open_id" | "user_id" | "union_id" | "chat_id";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { ClawdbotConfig } from "openclaw/plugin-sdk";
|
import type { ClawdbotConfig } from "openclaw/plugin-sdk";
|
||||||
import type { FeishuConfig } from "./types.js";
|
import { resolveFeishuAccount } from "./accounts.js";
|
||||||
import { createFeishuClient } from "./client.js";
|
import { createFeishuClient } from "./client.js";
|
||||||
|
|
||||||
// Feishu emoji types for typing indicator
|
// Feishu emoji types for typing indicator
|
||||||
@@ -18,14 +18,15 @@ export type TypingIndicatorState = {
|
|||||||
export async function addTypingIndicator(params: {
|
export async function addTypingIndicator(params: {
|
||||||
cfg: ClawdbotConfig;
|
cfg: ClawdbotConfig;
|
||||||
messageId: string;
|
messageId: string;
|
||||||
|
accountId?: string;
|
||||||
}): Promise<TypingIndicatorState> {
|
}): Promise<TypingIndicatorState> {
|
||||||
const { cfg, messageId } = params;
|
const { cfg, messageId, accountId } = params;
|
||||||
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
const account = resolveFeishuAccount({ cfg, accountId });
|
||||||
if (!feishuCfg) {
|
if (!account.configured) {
|
||||||
return { messageId, reactionId: null };
|
return { messageId, reactionId: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = createFeishuClient(feishuCfg);
|
const client = createFeishuClient(account);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await client.im.messageReaction.create({
|
const response = await client.im.messageReaction.create({
|
||||||
@@ -51,18 +52,19 @@ export async function addTypingIndicator(params: {
|
|||||||
export async function removeTypingIndicator(params: {
|
export async function removeTypingIndicator(params: {
|
||||||
cfg: ClawdbotConfig;
|
cfg: ClawdbotConfig;
|
||||||
state: TypingIndicatorState;
|
state: TypingIndicatorState;
|
||||||
|
accountId?: string;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const { cfg, state } = params;
|
const { cfg, state, accountId } = params;
|
||||||
if (!state.reactionId) {
|
if (!state.reactionId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
const account = resolveFeishuAccount({ cfg, accountId });
|
||||||
if (!feishuCfg) {
|
if (!account.configured) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = createFeishuClient(feishuCfg);
|
const client = createFeishuClient(account);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await client.im.messageReaction.delete({
|
await client.im.messageReaction.delete({
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type * as Lark from "@larksuiteoapi/node-sdk";
|
import type * as Lark from "@larksuiteoapi/node-sdk";
|
||||||
import type { OpenClawPluginApi } from "openclaw/plugin-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 { createFeishuClient } from "./client.js";
|
||||||
import { resolveToolsConfig } from "./tools-config.js";
|
import { resolveToolsConfig } from "./tools-config.js";
|
||||||
import { FeishuWikiSchema, type FeishuWikiParams } from "./wiki-schema.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 ============
|
// ============ Tool Registration ============
|
||||||
|
|
||||||
export function registerFeishuWikiTools(api: OpenClawPluginApi) {
|
export function registerFeishuWikiTools(api: OpenClawPluginApi) {
|
||||||
const feishuCfg = api.config?.channels?.feishu as FeishuConfig | undefined;
|
if (!api.config) {
|
||||||
if (!feishuCfg?.appId || !feishuCfg?.appSecret) {
|
api.logger.debug?.("feishu_wiki: No config available, skipping wiki tools");
|
||||||
api.logger.debug?.("feishu_wiki: Feishu credentials not configured, skipping wiki tools");
|
|
||||||
return;
|
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) {
|
if (!toolsCfg.wiki) {
|
||||||
api.logger.debug?.("feishu_wiki: wiki tool disabled in config");
|
api.logger.debug?.("feishu_wiki: wiki tool disabled in config");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getClient = () => createFeishuClient(feishuCfg);
|
const getClient = () => createFeishuClient(firstAccount);
|
||||||
|
|
||||||
api.registerTool(
|
api.registerTool(
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user