update markdown support to feishu

This commit is contained in:
alex xiang 2026-01-29 12:27:25 +08:00
parent 4abc18cec9
commit 3e36da2b84
7 changed files with 74 additions and 24 deletions

View File

@ -6,6 +6,7 @@ import {
// Import feishu functions directly from source // Import feishu functions directly from source
import { import {
getStartupChatIds,
listFeishuAccountIds, listFeishuAccountIds,
resolveFeishuAccount, resolveFeishuAccount,
resolveDefaultFeishuAccountId, resolveDefaultFeishuAccountId,
@ -157,19 +158,19 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
onMessage, onMessage,
}); });
// Send startup message if configured // Send startup message to all configured startup chat IDs
const startupChatId = account.config.startupChatId; const startupChatIds = getStartupChatIds(account.config);
if (startupChatId) {
try {
const timestamp = new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" }); const timestamp = new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" });
for (const chatId of startupChatIds) {
try {
await client.sendTextMessage( await client.sendTextMessage(
startupChatId, chatId,
`🚀 Clawdbot 飞书网关已启动 (${timestamp})`, `🚀 Clawdbot 飞书网关已启动 (${timestamp})`,
"chat_id", "chat_id",
); );
ctx.log?.info(`[${account.accountId}] sent startup message to ${startupChatId}`); ctx.log?.info(`[${account.accountId}] sent startup message to ${chatId}`);
} catch (err) { } 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}`);
} }
} }

View File

@ -42,8 +42,17 @@ export type FeishuAccountConfig = {
/** Encrypt key for event decryption (optional). */ /** Encrypt key for event decryption (optional). */
encryptKey?: string; 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: * Event subscription mode:

View File

@ -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 = { export type ResolvedFeishuAccount = {
accountId: string; accountId: string;
enabled: boolean; enabled: boolean;

View File

@ -23,6 +23,7 @@ export {
type FeishuInteractiveCard, type FeishuInteractiveCard,
} from "./send.js"; } from "./send.js";
export { export {
getStartupChatIds,
resolveFeishuAccount, resolveFeishuAccount,
listFeishuAccountIds, listFeishuAccountIds,
listEnabledFeishuAccounts, listEnabledFeishuAccounts,

View File

@ -7,7 +7,7 @@
import type { ClawdbotConfig } from "../config/config.js"; import type { ClawdbotConfig } from "../config/config.js";
import type { RuntimeEnv } from "../runtime.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 type { FeishuMessageContext } from "./monitor.js";
import { resolveAgentRoute } from "../routing/resolve-route.js"; import { resolveAgentRoute } from "../routing/resolve-route.js";
import { dispatchInboundMessageWithBufferedDispatcher } from "../auto-reply/dispatch.js"; import { dispatchInboundMessageWithBufferedDispatcher } from "../auto-reply/dispatch.js";
@ -55,6 +55,21 @@ function isFeishuSenderAllowed(
const isGroup = ctx.chatType === "group"; const isGroup = ctx.chatType === "group";
const senderId = ctx.senderId; 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) { if (isGroup) {
// Check group policy // Check group policy
const groupPolicySetting = account.config.groupPolicy ?? "open"; const groupPolicySetting = account.config.groupPolicy ?? "open";
@ -247,13 +262,18 @@ export async function dispatchFeishuMessage(params: DispatchFeishuMessageParams)
log( log(
`feishu: deliver callback called - hasText=${!!payload.text}, textLength=${payload.text?.length ?? 0}`, `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) { if (payload.text) {
let replyText = payload.text;
if (ctx.chatType === "group" && ctx.senderId) {
// Feishu text/interactive: <at id="open_id"></at> mentions the user
replyText = `<at id="${ctx.senderId}"></at> ${replyText}`;
}
try { try {
log(`feishu: sending reply to ${ctx.chatId}...`); log(`feishu: sending reply to ${ctx.chatId}...`);
await sendMessageFeishu({ await sendMessageFeishu({
to: ctx.chatId, to: ctx.chatId,
text: payload.text, text: replyText,
accountId: account.accountId, accountId: account.accountId,
config: cfg, config: cfg,
receiveIdType: "chat_id", receiveIdType: "chat_id",

View File

@ -15,7 +15,7 @@ import { computeBackoff, sleepWithAbort } from "../infra/backoff.js";
import { formatErrorMessage } from "../infra/errors.js"; import { formatErrorMessage } from "../infra/errors.js";
import { formatDurationMs } from "../infra/format-duration.js"; import { formatDurationMs } from "../infra/format-duration.js";
import type { RuntimeEnv } from "../runtime.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 { createFeishuClient, type FeishuClient } from "./client.js";
import { import {
parseFeishuEvent, parseFeishuEvent,
@ -693,9 +693,9 @@ export async function monitorFeishuProvider(opts: MonitorFeishuOpts = {}): Promi
throw new Error(`Feishu authentication failed: ${formatErrorMessage(err)}`); throw new Error(`Feishu authentication failed: ${formatErrorMessage(err)}`);
} }
// Send startup message if startupChatId is configured // Send startup message to all configured startup chat IDs
const startupChatId = account.config.startupChatId?.trim(); const startupChatIds = getStartupChatIds(account.config);
if (startupChatId) { if (startupChatIds.length > 0) {
const localIp = getLocalIPv4(); const localIp = getLocalIPv4();
const hostname = os.hostname(); const hostname = os.hostname();
const version = process.env.CLAWDBOT_VERSION ?? process.env.npm_package_version ?? "unknown"; const version = process.env.CLAWDBOT_VERSION ?? process.env.npm_package_version ?? "unknown";
@ -712,11 +712,13 @@ export async function monitorFeishuProvider(opts: MonitorFeishuOpts = {}): Promi
`启动时间: ${timestamp}`, `启动时间: ${timestamp}`,
].join("\n"); ].join("\n");
for (const chatId of startupChatIds) {
try { try {
await client.sendTextMessage(startupChatId, startupMessage); await client.sendTextMessage(chatId, startupMessage);
log(`feishu: sent startup message to chat ${startupChatId}`); log(`feishu: sent startup message to chat ${chatId}`);
} catch (err) { } catch (err) {
log(`feishu: failed to send startup message: ${formatErrorMessage(err)}`); log(`feishu: failed to send startup message to ${chatId}: ${formatErrorMessage(err)}`);
}
} }
} }

View File

@ -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 { export function hasMarkdown(text: string): boolean {
return ( return (
/\*\*.+?\*\*/.test(text) || // bold /\*\*.+?\*\*/.test(text) || // bold
/\*.+?\*/.test(text) || // italic /\*.+?\*/.test(text) || // italic
/~~.+?~~/.test(text) || // strikethrough /~~.+?~~/.test(text) || // strikethrough
/`.+?`/.test(text) || // code /`.+?`/.test(text) || // inline code
/\[.+?\]\(.+?\)/.test(text) // links /\[.+?\]\(.+?\)/.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
); );
} }