import { formatAllowFromLowercase } from "openclaw/plugin-sdk/allow-from"; import { createMessageToolCardSchema } from "openclaw/plugin-sdk/channel-actions"; import { createHybridChannelConfigAdapter } from "openclaw/plugin-sdk/channel-config-helpers"; import type { ChannelMessageActionAdapter, ChannelMessageToolDiscovery, } from "openclaw/plugin-sdk/channel-contract"; import { createPairingPrefixStripper, createTextPairingAdapter, } from "openclaw/plugin-sdk/channel-pairing"; import { createAllowlistProviderGroupPolicyWarningCollector, projectWarningCollector, } from "openclaw/plugin-sdk/channel-policy"; import { createChannelDirectoryAdapter, createRuntimeDirectoryLiveAdapter, } from "openclaw/plugin-sdk/directory-runtime"; import { createRuntimeOutboundDelegates } from "openclaw/plugin-sdk/infra-runtime"; import { createLazyRuntimeNamedExport } from "openclaw/plugin-sdk/lazy-runtime"; import type { ChannelMeta, ChannelPlugin, ClawdbotConfig } from "../runtime-api.js"; import { buildChannelConfigSchema, buildProbeChannelStatusSummary, createActionGate, buildRuntimeAccountStatusSnapshot, createDefaultChannelRuntimeState, DEFAULT_ACCOUNT_ID, PAIRING_APPROVED_MESSAGE, } from "../runtime-api.js"; import type { ChannelMessageActionName } from "../runtime-api.js"; import { resolveFeishuAccount, resolveFeishuCredentials, listFeishuAccountIds, listEnabledFeishuAccounts, resolveDefaultFeishuAccountId, } from "./accounts.js"; import { createFeishuClient } from "./client.js"; import { FeishuConfigSchema } from "./config-schema.js"; import { parseFeishuConversationId } from "./conversation-id.js"; import { listFeishuDirectoryPeers, listFeishuDirectoryGroups } from "./directory.static.js"; import { resolveFeishuGroupToolPolicy } from "./policy.js"; import { getFeishuRuntime } from "./runtime.js"; import { resolveFeishuOutboundSessionRoute } from "./session-route.js"; import { feishuSetupAdapter } from "./setup-core.js"; import { feishuSetupWizard } from "./setup-surface.js"; import { normalizeFeishuTarget, looksLikeFeishuId, formatFeishuTarget } from "./targets.js"; import type { ResolvedFeishuAccount, FeishuConfig } from "./types.js"; const meta: ChannelMeta = { id: "feishu", label: "Feishu", selectionLabel: "Feishu/Lark (飞书)", docsPath: "/channels/feishu", docsLabel: "feishu", blurb: "飞书/Lark enterprise messaging.", aliases: ["lark"], order: 70, }; const loadFeishuChannelRuntime = createLazyRuntimeNamedExport( () => import("./channel.runtime.js"), "feishuChannelRuntime", ); const collectFeishuSecurityWarnings = createAllowlistProviderGroupPolicyWarningCollector<{ cfg: ClawdbotConfig; accountId?: string | null; }>({ providerConfigPresent: (cfg) => cfg.channels?.feishu !== undefined, resolveGroupPolicy: ({ cfg, accountId }) => resolveFeishuAccount({ cfg, accountId }).config?.groupPolicy, collect: ({ cfg, accountId, groupPolicy }) => { if (groupPolicy !== "open") { return []; } const account = resolveFeishuAccount({ cfg, accountId }); return [ `- Feishu[${account.accountId}] groups: groupPolicy="open" allows any member to trigger (mention-gated). Set channels.feishu.groupPolicy="allowlist" + channels.feishu.groupAllowFrom to restrict senders.`, ]; }, }); function describeFeishuMessageTool({ cfg, }: Parameters< NonNullable >[0]): ChannelMessageToolDiscovery { const enabled = cfg.channels?.feishu?.enabled !== false && Boolean(resolveFeishuCredentials(cfg.channels?.feishu as FeishuConfig | undefined)); if (listEnabledFeishuAccounts(cfg).length === 0) { return { actions: [], capabilities: enabled ? ["cards"] : [], schema: enabled ? { properties: { card: createMessageToolCardSchema(), }, } : null, }; } const actions = new Set([ "send", "read", "edit", "thread-reply", "pin", "list-pins", "unpin", "member-info", "channel-info", "channel-list", ]); if (areAnyFeishuReactionActionsEnabled(cfg)) { actions.add("react"); actions.add("reactions"); } return { actions: Array.from(actions), capabilities: enabled ? ["cards"] : [], schema: enabled ? { properties: { card: createMessageToolCardSchema(), }, } : null, }; } function setFeishuNamedAccountEnabled( cfg: ClawdbotConfig, accountId: string, enabled: boolean, ): ClawdbotConfig { const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined; return { ...cfg, channels: { ...cfg.channels, feishu: { ...feishuCfg, accounts: { ...feishuCfg?.accounts, [accountId]: { ...feishuCfg?.accounts?.[accountId], enabled, }, }, }, }, }; } const feishuConfigAdapter = createHybridChannelConfigAdapter< ResolvedFeishuAccount, ResolvedFeishuAccount, ClawdbotConfig >({ sectionKey: "feishu", listAccountIds: listFeishuAccountIds, resolveAccount: (cfg, accountId) => resolveFeishuAccount({ cfg, accountId }), defaultAccountId: resolveDefaultFeishuAccountId, clearBaseFields: [], resolveAllowFrom: (account) => account.config.allowFrom, formatAllowFrom: (allowFrom) => formatAllowFromLowercase({ allowFrom }), }); function isFeishuReactionsActionEnabled(params: { cfg: ClawdbotConfig; account: ResolvedFeishuAccount; }): boolean { if (!params.account.enabled || !params.account.configured) { return false; } const gate = createActionGate( (params.account.config.actions ?? (params.cfg.channels?.feishu as { actions?: unknown } | undefined)?.actions) as Record< string, boolean | undefined >, ); return gate("reactions"); } function areAnyFeishuReactionActionsEnabled(cfg: ClawdbotConfig): boolean { for (const account of listEnabledFeishuAccounts(cfg)) { if (isFeishuReactionsActionEnabled({ cfg, account })) { return true; } } return false; } function isSupportedFeishuDirectConversationId(conversationId: string): boolean { const trimmed = conversationId.trim(); if (!trimmed || trimmed.includes(":")) { return false; } if (trimmed.startsWith("oc_") || trimmed.startsWith("on_")) { return false; } return true; } function normalizeFeishuAcpConversationId(conversationId: string) { const parsed = parseFeishuConversationId({ conversationId }); if ( !parsed || (parsed.scope !== "group_topic" && parsed.scope !== "group_topic_sender" && !isSupportedFeishuDirectConversationId(parsed.canonicalConversationId)) ) { return null; } return { conversationId: parsed.canonicalConversationId, parentConversationId: parsed.scope === "group_topic" || parsed.scope === "group_topic_sender" ? parsed.chatId : undefined, }; } function matchFeishuAcpConversation(params: { bindingConversationId: string; conversationId: string; parentConversationId?: string; }) { const binding = normalizeFeishuAcpConversationId(params.bindingConversationId); if (!binding) { return null; } const incoming = parseFeishuConversationId({ conversationId: params.conversationId, parentConversationId: params.parentConversationId, }); if ( !incoming || (incoming.scope !== "group_topic" && incoming.scope !== "group_topic_sender" && !isSupportedFeishuDirectConversationId(incoming.canonicalConversationId)) ) { return null; } const matchesCanonicalConversation = binding.conversationId === incoming.canonicalConversationId; const matchesParentTopicForSenderScopedConversation = incoming.scope === "group_topic_sender" && binding.parentConversationId === incoming.chatId && binding.conversationId === `${incoming.chatId}:topic:${incoming.topicId}`; if (!matchesCanonicalConversation && !matchesParentTopicForSenderScopedConversation) { return null; } return { conversationId: matchesParentTopicForSenderScopedConversation ? binding.conversationId : incoming.canonicalConversationId, parentConversationId: incoming.scope === "group_topic" || incoming.scope === "group_topic_sender" ? incoming.chatId : undefined, matchPriority: matchesCanonicalConversation ? 2 : 1, }; } function jsonActionResult(details: Record) { return { content: [{ type: "text" as const, text: JSON.stringify(details) }], details, }; } function readFirstString( params: Record, keys: string[], fallback?: string | null, ): string | undefined { for (const key of keys) { const value = params[key]; if (typeof value === "string" && value.trim()) { return value.trim(); } } if (typeof fallback === "string" && fallback.trim()) { return fallback.trim(); } return undefined; } function readOptionalNumber(params: Record, keys: string[]): number | undefined { for (const key of keys) { const value = params[key]; if (typeof value === "number" && Number.isFinite(value)) { return value; } if (typeof value === "string" && value.trim()) { const parsed = Number(value); if (Number.isFinite(parsed)) { return parsed; } } } return undefined; } function resolveFeishuActionTarget(ctx: { params: Record; toolContext?: { currentChannelId?: string } | null; }): string | undefined { return readFirstString(ctx.params, ["to", "target"], ctx.toolContext?.currentChannelId); } function resolveFeishuChatId(ctx: { params: Record; toolContext?: { currentChannelId?: string } | null; }): string | undefined { const raw = readFirstString( ctx.params, ["chatId", "chat_id", "channelId", "channel_id", "to", "target"], ctx.toolContext?.currentChannelId, ); if (!raw) { return undefined; } if (/^(user|dm|open_id):/i.test(raw)) { return undefined; } if (/^(chat|group|channel):/i.test(raw)) { return normalizeFeishuTarget(raw) ?? undefined; } return raw; } function resolveFeishuMessageId(params: Record): string | undefined { return readFirstString(params, ["messageId", "message_id", "replyTo", "reply_to"]); } function resolveFeishuMemberId(params: Record): string | undefined { return readFirstString(params, [ "memberId", "member_id", "userId", "user_id", "openId", "open_id", "unionId", "union_id", ]); } function resolveFeishuMemberIdType( params: Record, ): "open_id" | "user_id" | "union_id" { const raw = readFirstString(params, [ "memberIdType", "member_id_type", "userIdType", "user_id_type", ]); if (raw === "open_id" || raw === "user_id" || raw === "union_id") { return raw; } if ( readFirstString(params, ["userId", "user_id"]) && !readFirstString(params, ["openId", "open_id", "unionId", "union_id"]) ) { return "user_id"; } if ( readFirstString(params, ["unionId", "union_id"]) && !readFirstString(params, ["openId", "open_id"]) ) { return "union_id"; } return "open_id"; } export const feishuPlugin: ChannelPlugin = { id: "feishu", meta: { ...meta, }, pairing: createTextPairingAdapter({ idLabel: "feishuUserId", message: PAIRING_APPROVED_MESSAGE, normalizeAllowEntry: createPairingPrefixStripper(/^(feishu|user|open_id):/i), notify: async ({ cfg, id, message }) => { const { sendMessageFeishu } = await loadFeishuChannelRuntime(); await sendMessageFeishu({ cfg, to: id, text: message, }); }, }), capabilities: { chatTypes: ["direct", "channel"], polls: false, threads: true, media: true, reactions: true, edit: true, reply: true, }, agentPrompt: { messageToolHints: () => [ "- Feishu targeting: omit `target` to reply to the current conversation (auto-inferred). Explicit targets: `user:open_id` or `chat:chat_id`.", "- Feishu supports interactive cards plus native image, file, audio, and video/media delivery.", "- Feishu supports `send`, `read`, `edit`, `thread-reply`, pins, and channel/member lookup, plus reactions when enabled.", ], }, groups: { resolveToolPolicy: resolveFeishuGroupToolPolicy, }, mentions: { stripPatterns: () => ['[^<]*'], }, reload: { configPrefixes: ["channels.feishu"] }, configSchema: buildChannelConfigSchema(FeishuConfigSchema), config: { ...feishuConfigAdapter, setAccountEnabled: ({ cfg, accountId, enabled }) => { const isDefault = accountId === DEFAULT_ACCOUNT_ID; if (isDefault) { return { ...cfg, channels: { ...cfg.channels, feishu: { ...cfg.channels?.feishu, enabled, }, }, }; } return setFeishuNamedAccountEnabled(cfg, accountId, enabled); }, deleteAccount: ({ cfg, accountId }) => { const isDefault = accountId === DEFAULT_ACCOUNT_ID; if (isDefault) { // Delete entire feishu config const next = { ...cfg } as ClawdbotConfig; const nextChannels = { ...cfg.channels }; delete (nextChannels as Record).feishu; if (Object.keys(nextChannels).length > 0) { next.channels = nextChannels; } else { delete next.channels; } return next; } // Delete specific account from accounts const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined; const accounts = { ...feishuCfg?.accounts }; delete accounts[accountId]; return { ...cfg, channels: { ...cfg.channels, feishu: { ...feishuCfg, accounts: Object.keys(accounts).length > 0 ? accounts : undefined, }, }, }; }, isConfigured: (account) => account.configured, describeAccount: (account) => ({ accountId: account.accountId, enabled: account.enabled, configured: account.configured, name: account.name, appId: account.appId, domain: account.domain, }), }, actions: { describeMessageTool: describeFeishuMessageTool, handleAction: async (ctx) => { const account = resolveFeishuAccount({ cfg: ctx.cfg, accountId: ctx.accountId ?? undefined }); if ( (ctx.action === "react" || ctx.action === "reactions") && !isFeishuReactionsActionEnabled({ cfg: ctx.cfg, account }) ) { throw new Error("Feishu reactions are disabled via actions.reactions."); } if (ctx.action === "send" || ctx.action === "thread-reply") { const to = resolveFeishuActionTarget(ctx); if (!to) { throw new Error(`Feishu ${ctx.action} requires a target (to).`); } const replyToMessageId = ctx.action === "thread-reply" ? resolveFeishuMessageId(ctx.params) : undefined; if (ctx.action === "thread-reply" && !replyToMessageId) { throw new Error("Feishu thread-reply requires messageId."); } const card = ctx.params.card && typeof ctx.params.card === "object" ? (ctx.params.card as Record) : undefined; const text = readFirstString(ctx.params, ["text", "message"]); if (!card && !text) { throw new Error(`Feishu ${ctx.action} requires text/message or card.`); } const runtime = await loadFeishuChannelRuntime(); const result = card ? await runtime.sendCardFeishu({ cfg: ctx.cfg, to, card, accountId: ctx.accountId ?? undefined, replyToMessageId, replyInThread: ctx.action === "thread-reply", }) : await runtime.sendMessageFeishu({ cfg: ctx.cfg, to, text: text!, accountId: ctx.accountId ?? undefined, replyToMessageId, replyInThread: ctx.action === "thread-reply", }); return jsonActionResult({ ok: true, channel: "feishu", action: ctx.action, ...result, }); } if (ctx.action === "read") { const messageId = resolveFeishuMessageId(ctx.params); if (!messageId) { throw new Error("Feishu read requires messageId."); } const { getMessageFeishu } = await loadFeishuChannelRuntime(); const message = await getMessageFeishu({ cfg: ctx.cfg, messageId, accountId: ctx.accountId ?? undefined, }); if (!message) { return { isError: true, content: [ { type: "text" as const, text: JSON.stringify({ error: `Feishu read failed or message not found: ${messageId}`, }), }, ], details: { error: `Feishu read failed or message not found: ${messageId}` }, }; } return jsonActionResult({ ok: true, channel: "feishu", action: "read", message }); } if (ctx.action === "edit") { const messageId = resolveFeishuMessageId(ctx.params); if (!messageId) { throw new Error("Feishu edit requires messageId."); } const text = readFirstString(ctx.params, ["text", "message"]); const card = ctx.params.card && typeof ctx.params.card === "object" ? (ctx.params.card as Record) : undefined; const { editMessageFeishu } = await loadFeishuChannelRuntime(); const result = await editMessageFeishu({ cfg: ctx.cfg, messageId, text, card, accountId: ctx.accountId ?? undefined, }); return jsonActionResult({ ok: true, channel: "feishu", action: "edit", ...result, }); } if (ctx.action === "pin") { const messageId = resolveFeishuMessageId(ctx.params); if (!messageId) { throw new Error("Feishu pin requires messageId."); } const { createPinFeishu } = await loadFeishuChannelRuntime(); const pin = await createPinFeishu({ cfg: ctx.cfg, messageId, accountId: ctx.accountId ?? undefined, }); return jsonActionResult({ ok: true, channel: "feishu", action: "pin", pin }); } if (ctx.action === "unpin") { const messageId = resolveFeishuMessageId(ctx.params); if (!messageId) { throw new Error("Feishu unpin requires messageId."); } const { removePinFeishu } = await loadFeishuChannelRuntime(); await removePinFeishu({ cfg: ctx.cfg, messageId, accountId: ctx.accountId ?? undefined, }); return jsonActionResult({ ok: true, channel: "feishu", action: "unpin", messageId, }); } if (ctx.action === "list-pins") { const chatId = resolveFeishuChatId(ctx); if (!chatId) { throw new Error("Feishu list-pins requires chatId or channelId."); } const { listPinsFeishu } = await loadFeishuChannelRuntime(); const result = await listPinsFeishu({ cfg: ctx.cfg, chatId, startTime: readFirstString(ctx.params, ["startTime", "start_time"]), endTime: readFirstString(ctx.params, ["endTime", "end_time"]), pageSize: readOptionalNumber(ctx.params, ["pageSize", "page_size"]), pageToken: readFirstString(ctx.params, ["pageToken", "page_token"]), accountId: ctx.accountId ?? undefined, }); return jsonActionResult({ ok: true, channel: "feishu", action: "list-pins", ...result, }); } if (ctx.action === "channel-info") { const chatId = resolveFeishuChatId(ctx); if (!chatId) { throw new Error("Feishu channel-info requires chatId or channelId."); } const runtime = await loadFeishuChannelRuntime(); const client = createFeishuClient(account); const channel = await runtime.getChatInfo(client, chatId); const includeMembers = ctx.params.includeMembers === true || ctx.params.members === true; if (!includeMembers) { return jsonActionResult({ ok: true, provider: "feishu", action: "channel-info", channel, }); } const members = await runtime.getChatMembers( client, chatId, readOptionalNumber(ctx.params, ["pageSize", "page_size"]), readFirstString(ctx.params, ["pageToken", "page_token"]), resolveFeishuMemberIdType(ctx.params), ); return jsonActionResult({ ok: true, provider: "feishu", action: "channel-info", channel, members, }); } if (ctx.action === "member-info") { const runtime = await loadFeishuChannelRuntime(); const client = createFeishuClient(account); const memberId = resolveFeishuMemberId(ctx.params); if (memberId) { const member = await runtime.getFeishuMemberInfo( client, memberId, resolveFeishuMemberIdType(ctx.params), ); return jsonActionResult({ ok: true, channel: "feishu", action: "member-info", member, }); } const chatId = resolveFeishuChatId(ctx); if (!chatId) { throw new Error("Feishu member-info requires memberId or chatId/channelId."); } const members = await runtime.getChatMembers( client, chatId, readOptionalNumber(ctx.params, ["pageSize", "page_size"]), readFirstString(ctx.params, ["pageToken", "page_token"]), resolveFeishuMemberIdType(ctx.params), ); return jsonActionResult({ ok: true, channel: "feishu", action: "member-info", ...members, }); } if (ctx.action === "channel-list") { const runtime = await loadFeishuChannelRuntime(); const query = readFirstString(ctx.params, ["query"]); const limit = readOptionalNumber(ctx.params, ["limit"]); const scope = readFirstString(ctx.params, ["scope", "kind"]) ?? "all"; if ( scope === "groups" || scope === "group" || scope === "channels" || scope === "channel" ) { const groups = await runtime.listFeishuDirectoryGroupsLive({ cfg: ctx.cfg, query, limit, fallbackToStatic: false, accountId: ctx.accountId ?? undefined, }); return jsonActionResult({ ok: true, channel: "feishu", action: "channel-list", groups, }); } if ( scope === "peers" || scope === "peer" || scope === "members" || scope === "member" || scope === "users" || scope === "user" ) { const peers = await runtime.listFeishuDirectoryPeersLive({ cfg: ctx.cfg, query, limit, fallbackToStatic: false, accountId: ctx.accountId ?? undefined, }); return jsonActionResult({ ok: true, channel: "feishu", action: "channel-list", peers, }); } const [groups, peers] = await Promise.all([ runtime.listFeishuDirectoryGroupsLive({ cfg: ctx.cfg, query, limit, fallbackToStatic: false, accountId: ctx.accountId ?? undefined, }), runtime.listFeishuDirectoryPeersLive({ cfg: ctx.cfg, query, limit, fallbackToStatic: false, accountId: ctx.accountId ?? undefined, }), ]); return jsonActionResult({ ok: true, channel: "feishu", action: "channel-list", groups, peers, }); } if (ctx.action === "react") { const messageId = resolveFeishuMessageId(ctx.params); if (!messageId) { throw new Error("Feishu reaction requires messageId."); } const emoji = typeof ctx.params.emoji === "string" ? ctx.params.emoji.trim() : ""; const remove = ctx.params.remove === true; const clearAll = ctx.params.clearAll === true; if (remove) { if (!emoji) { throw new Error("Emoji is required to remove a Feishu reaction."); } const { listReactionsFeishu, removeReactionFeishu } = await loadFeishuChannelRuntime(); const matches = await listReactionsFeishu({ cfg: ctx.cfg, messageId, emojiType: emoji, accountId: ctx.accountId ?? undefined, }); const ownReaction = matches.find((entry) => entry.operatorType === "app"); if (!ownReaction) { return jsonActionResult({ ok: true, removed: null }); } await removeReactionFeishu({ cfg: ctx.cfg, messageId, reactionId: ownReaction.reactionId, accountId: ctx.accountId ?? undefined, }); return jsonActionResult({ ok: true, removed: emoji }); } if (!emoji) { if (!clearAll) { throw new Error( "Emoji is required to add a Feishu reaction. Set clearAll=true to remove all bot reactions.", ); } const { listReactionsFeishu, removeReactionFeishu } = await loadFeishuChannelRuntime(); const reactions = await listReactionsFeishu({ cfg: ctx.cfg, messageId, accountId: ctx.accountId ?? undefined, }); let removed = 0; for (const reaction of reactions.filter((entry) => entry.operatorType === "app")) { await removeReactionFeishu({ cfg: ctx.cfg, messageId, reactionId: reaction.reactionId, accountId: ctx.accountId ?? undefined, }); removed += 1; } return jsonActionResult({ ok: true, removed }); } const { addReactionFeishu } = await loadFeishuChannelRuntime(); await addReactionFeishu({ cfg: ctx.cfg, messageId, emojiType: emoji, accountId: ctx.accountId ?? undefined, }); return jsonActionResult({ ok: true, added: emoji }); } if (ctx.action === "reactions") { const messageId = resolveFeishuMessageId(ctx.params); if (!messageId) { throw new Error("Feishu reactions lookup requires messageId."); } const { listReactionsFeishu } = await loadFeishuChannelRuntime(); const reactions = await listReactionsFeishu({ cfg: ctx.cfg, messageId, accountId: ctx.accountId ?? undefined, }); return jsonActionResult({ ok: true, reactions }); } throw new Error(`Unsupported Feishu action: "${String(ctx.action)}"`); }, }, security: { collectWarnings: projectWarningCollector( ({ cfg, accountId }: { cfg: ClawdbotConfig; accountId?: string | null }) => ({ cfg, accountId, }), collectFeishuSecurityWarnings, ), }, bindings: { compileConfiguredBinding: ({ conversationId }) => normalizeFeishuAcpConversationId(conversationId), matchInboundConversation: ({ compiledBinding, conversationId, parentConversationId }) => matchFeishuAcpConversation({ bindingConversationId: compiledBinding.conversationId, conversationId, parentConversationId, }), }, setup: feishuSetupAdapter, setupWizard: feishuSetupWizard, messaging: { normalizeTarget: (raw) => normalizeFeishuTarget(raw) ?? undefined, resolveOutboundSessionRoute: (params) => resolveFeishuOutboundSessionRoute(params), targetResolver: { looksLikeId: looksLikeFeishuId, hint: "", }, }, directory: createChannelDirectoryAdapter({ listPeers: async ({ cfg, query, limit, accountId }) => listFeishuDirectoryPeers({ cfg, query: query ?? undefined, limit: limit ?? undefined, accountId: accountId ?? undefined, }), listGroups: async ({ cfg, query, limit, accountId }) => listFeishuDirectoryGroups({ cfg, query: query ?? undefined, limit: limit ?? undefined, accountId: accountId ?? undefined, }), ...createRuntimeDirectoryLiveAdapter({ getRuntime: loadFeishuChannelRuntime, listPeersLive: (runtime) => async ({ cfg, query, limit, accountId }) => await runtime.listFeishuDirectoryPeersLive({ cfg, query: query ?? undefined, limit: limit ?? undefined, accountId: accountId ?? undefined, }), listGroupsLive: (runtime) => async ({ cfg, query, limit, accountId }) => await runtime.listFeishuDirectoryGroupsLive({ cfg, query: query ?? undefined, limit: limit ?? undefined, accountId: accountId ?? undefined, }), }), }), outbound: { deliveryMode: "direct", chunker: (text, limit) => getFeishuRuntime().channel.text.chunkMarkdownText(text, limit), chunkerMode: "markdown", textChunkLimit: 4000, ...createRuntimeOutboundDelegates({ getRuntime: loadFeishuChannelRuntime, sendText: { resolve: (runtime) => runtime.feishuOutbound.sendText }, sendMedia: { resolve: (runtime) => runtime.feishuOutbound.sendMedia }, }), }, status: { defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID, { port: null }), buildChannelSummary: ({ snapshot }) => buildProbeChannelStatusSummary(snapshot, { port: snapshot.port ?? null, }), probeAccount: async ({ account }) => await (await loadFeishuChannelRuntime()).probeFeishu(account), buildAccountSnapshot: ({ account, runtime, probe }) => ({ accountId: account.accountId, enabled: account.enabled, configured: account.configured, name: account.name, appId: account.appId, domain: account.domain, ...buildRuntimeAccountStatusSnapshot({ runtime, probe }), port: runtime?.port ?? null, }), }, gateway: { startAccount: async (ctx) => { const { monitorFeishuProvider } = await import("./monitor.js"); const account = resolveFeishuAccount({ cfg: ctx.cfg, accountId: ctx.accountId }); const port = account.config?.webhookPort ?? null; ctx.setStatus({ accountId: ctx.accountId, port }); ctx.log?.info( `starting feishu[${ctx.accountId}] (mode: ${account.config?.connectionMode ?? "websocket"})`, ); return monitorFeishuProvider({ config: ctx.cfg, runtime: ctx.runtime, abortSignal: ctx.abortSignal, accountId: ctx.accountId, }); }, }, };