From 3e36da2b8414d3067c7e6748566e5a282561d253 Mon Sep 17 00:00:00 2001 From: alex xiang Date: Thu, 29 Jan 2026 12:27:25 +0800 Subject: [PATCH] update markdown support to feishu --- extensions/feishu/src/channel.ts | 15 ++++++++------- src/config/types.feishu.ts | 13 +++++++++++-- src/feishu/accounts.ts | 12 ++++++++++++ src/feishu/index.ts | 1 + src/feishu/message-dispatch.ts | 26 +++++++++++++++++++++++--- src/feishu/monitor.ts | 20 +++++++++++--------- src/feishu/send.ts | 11 ++++++++--- 7 files changed, 74 insertions(+), 24 deletions(-) diff --git a/extensions/feishu/src/channel.ts b/extensions/feishu/src/channel.ts index 9bcfff147..95ec559d3 100644 --- a/extensions/feishu/src/channel.ts +++ b/extensions/feishu/src/channel.ts @@ -6,6 +6,7 @@ import { // Import feishu functions directly from source import { + getStartupChatIds, listFeishuAccountIds, resolveFeishuAccount, resolveDefaultFeishuAccountId, @@ -157,19 +158,19 @@ export const feishuPlugin: ChannelPlugin = { onMessage, }); - // Send startup message if configured - const startupChatId = account.config.startupChatId; - if (startupChatId) { + // Send startup message to all configured startup chat IDs + const startupChatIds = getStartupChatIds(account.config); + const timestamp = new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" }); + for (const chatId of startupChatIds) { try { - const timestamp = new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" }); await client.sendTextMessage( - startupChatId, + chatId, `🚀 Clawdbot 飞书网关已启动 (${timestamp})`, "chat_id", ); - ctx.log?.info(`[${account.accountId}] sent startup message to ${startupChatId}`); + ctx.log?.info(`[${account.accountId}] sent startup message to ${chatId}`); } catch (err) { - ctx.log?.warn(`[${account.accountId}] failed to send startup message: ${err}`); + ctx.log?.warn(`[${account.accountId}] failed to send startup message to ${chatId}: ${err}`); } } diff --git a/src/config/types.feishu.ts b/src/config/types.feishu.ts index 61a070924..b21656067 100644 --- a/src/config/types.feishu.ts +++ b/src/config/types.feishu.ts @@ -42,8 +42,17 @@ export type FeishuAccountConfig = { /** Encrypt key for event decryption (optional). */ encryptKey?: string; - /** Chat ID to send a startup message to when gateway starts. */ - startupChatId?: string; + /** + * Chat IDs to send a startup message to when gateway starts. + * Supports one or multiple group chat IDs (array or legacy single string). + */ + startupChatId?: string | string[]; + + /** + * When true, only allow messages from groups listed in startupChatId; + * private chats (DMs) are not allowed. Ignored if startupChatId is empty. + */ + allowOnlyStartupChats?: boolean; /** * Event subscription mode: diff --git a/src/feishu/accounts.ts b/src/feishu/accounts.ts index 514163b16..a729df4ca 100644 --- a/src/feishu/accounts.ts +++ b/src/feishu/accounts.ts @@ -11,6 +11,18 @@ const debugAccounts = (...args: unknown[]) => { } }; +/** Normalize startupChatId config (string or string[]) to a non-empty string array. */ +export function getStartupChatIds(config: FeishuAccountConfig): string[] { + const raw = config.startupChatId; + if (Array.isArray(raw)) { + return raw.map((s) => String(s).trim()).filter(Boolean); + } + if (raw != null && String(raw).trim()) { + return [String(raw).trim()]; + } + return []; +} + export type ResolvedFeishuAccount = { accountId: string; enabled: boolean; diff --git a/src/feishu/index.ts b/src/feishu/index.ts index 3159a2f56..c78dfc2d5 100644 --- a/src/feishu/index.ts +++ b/src/feishu/index.ts @@ -23,6 +23,7 @@ export { type FeishuInteractiveCard, } from "./send.js"; export { + getStartupChatIds, resolveFeishuAccount, listFeishuAccountIds, listEnabledFeishuAccounts, diff --git a/src/feishu/message-dispatch.ts b/src/feishu/message-dispatch.ts index bb198b3a8..e4742f5d3 100644 --- a/src/feishu/message-dispatch.ts +++ b/src/feishu/message-dispatch.ts @@ -7,7 +7,7 @@ import type { ClawdbotConfig } from "../config/config.js"; import type { RuntimeEnv } from "../runtime.js"; -import type { ResolvedFeishuAccount } from "./accounts.js"; +import { getStartupChatIds, type ResolvedFeishuAccount } from "./accounts.js"; import type { FeishuMessageContext } from "./monitor.js"; import { resolveAgentRoute } from "../routing/resolve-route.js"; import { dispatchInboundMessageWithBufferedDispatcher } from "../auto-reply/dispatch.js"; @@ -55,6 +55,21 @@ function isFeishuSenderAllowed( const isGroup = ctx.chatType === "group"; const senderId = ctx.senderId; + // When allowOnlyStartupChats is true: only groups in startupChatId may send; no DMs + if (account.config.allowOnlyStartupChats) { + const allowedChatIds = getStartupChatIds(account.config); + if (allowedChatIds.length === 0) { + return { allowed: false, reason: "allow_only_startup_chats_no_list" }; + } + if (!isGroup) { + return { allowed: false, reason: "allow_only_startup_chats_no_dm" }; + } + if (!allowedChatIds.includes(ctx.chatId)) { + return { allowed: false, reason: "allow_only_startup_chats_group_not_allowed" }; + } + return { allowed: true }; + } + if (isGroup) { // Check group policy const groupPolicySetting = account.config.groupPolicy ?? "open"; @@ -247,13 +262,18 @@ export async function dispatchFeishuMessage(params: DispatchFeishuMessageParams) log( `feishu: deliver callback called - hasText=${!!payload.text}, textLength=${payload.text?.length ?? 0}`, ); - // Send response back to Feishu + // Send response back to Feishu; in groups, @mention the user who asked if (payload.text) { + let replyText = payload.text; + if (ctx.chatType === "group" && ctx.senderId) { + // Feishu text/interactive: mentions the user + replyText = ` ${replyText}`; + } try { log(`feishu: sending reply to ${ctx.chatId}...`); await sendMessageFeishu({ to: ctx.chatId, - text: payload.text, + text: replyText, accountId: account.accountId, config: cfg, receiveIdType: "chat_id", diff --git a/src/feishu/monitor.ts b/src/feishu/monitor.ts index 59df8b6e8..dee89f48f 100644 --- a/src/feishu/monitor.ts +++ b/src/feishu/monitor.ts @@ -15,7 +15,7 @@ import { computeBackoff, sleepWithAbort } from "../infra/backoff.js"; import { formatErrorMessage } from "../infra/errors.js"; import { formatDurationMs } from "../infra/format-duration.js"; import type { RuntimeEnv } from "../runtime.js"; -import { resolveFeishuAccount, type ResolvedFeishuAccount } from "./accounts.js"; +import { getStartupChatIds, resolveFeishuAccount, type ResolvedFeishuAccount } from "./accounts.js"; import { createFeishuClient, type FeishuClient } from "./client.js"; import { parseFeishuEvent, @@ -693,9 +693,9 @@ export async function monitorFeishuProvider(opts: MonitorFeishuOpts = {}): Promi throw new Error(`Feishu authentication failed: ${formatErrorMessage(err)}`); } - // Send startup message if startupChatId is configured - const startupChatId = account.config.startupChatId?.trim(); - if (startupChatId) { + // Send startup message to all configured startup chat IDs + const startupChatIds = getStartupChatIds(account.config); + if (startupChatIds.length > 0) { const localIp = getLocalIPv4(); const hostname = os.hostname(); const version = process.env.CLAWDBOT_VERSION ?? process.env.npm_package_version ?? "unknown"; @@ -712,11 +712,13 @@ export async function monitorFeishuProvider(opts: MonitorFeishuOpts = {}): Promi `启动时间: ${timestamp}`, ].join("\n"); - try { - await client.sendTextMessage(startupChatId, startupMessage); - log(`feishu: sent startup message to chat ${startupChatId}`); - } catch (err) { - log(`feishu: failed to send startup message: ${formatErrorMessage(err)}`); + for (const chatId of startupChatIds) { + try { + await client.sendTextMessage(chatId, startupMessage); + log(`feishu: sent startup message to chat ${chatId}`); + } catch (err) { + log(`feishu: failed to send startup message to ${chatId}: ${formatErrorMessage(err)}`); + } } } diff --git a/src/feishu/send.ts b/src/feishu/send.ts index e8e4bae05..db04d8667 100644 --- a/src/feishu/send.ts +++ b/src/feishu/send.ts @@ -237,15 +237,20 @@ function parseMarkdownLine(line: string): FeishuPostElement[] { } /** - * Check if text contains markdown that would benefit from rich text rendering + * Check if text contains markdown that would benefit from rich text rendering. + * When true, sendMessageFeishu uses interactive card for full markdown support. */ export function hasMarkdown(text: string): boolean { return ( /\*\*.+?\*\*/.test(text) || // bold /\*.+?\*/.test(text) || // italic /~~.+?~~/.test(text) || // strikethrough - /`.+?`/.test(text) || // code - /\[.+?\]\(.+?\)/.test(text) // links + /`.+?`/.test(text) || // inline code + /\[.+?\]\(.+?\)/.test(text) || // links + /^#{1,6}\s+/m.test(text) || // headings (# to ######) + /^[-*]\s+/m.test(text) || // unordered list + /^\d+\.\s+/m.test(text) || // ordered list + /```[\s\S]*?```/.test(text) // fenced code blocks ); }