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 {
getStartupChatIds,
listFeishuAccountIds,
resolveFeishuAccount,
resolveDefaultFeishuAccountId,
@ -157,19 +158,19 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
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}`);
}
}

View File

@ -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:

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

View File

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

View File

@ -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: <at id="open_id"></at> mentions the user
replyText = `<at id="${ctx.senderId}"></at> ${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",

View File

@ -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)}`);
}
}
}

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 {
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
);
}