update markdown support to feishu
This commit is contained in:
parent
4abc18cec9
commit
3e36da2b84
@ -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}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -23,6 +23,7 @@ export {
|
|||||||
type FeishuInteractiveCard,
|
type FeishuInteractiveCard,
|
||||||
} from "./send.js";
|
} from "./send.js";
|
||||||
export {
|
export {
|
||||||
|
getStartupChatIds,
|
||||||
resolveFeishuAccount,
|
resolveFeishuAccount,
|
||||||
listFeishuAccountIds,
|
listFeishuAccountIds,
|
||||||
listEnabledFeishuAccounts,
|
listEnabledFeishuAccounts,
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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)}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user