feat: 支持群聊

This commit is contained in:
maxwell 2026-01-29 18:11:33 +08:00
parent 963c5b5a7a
commit e722c9233a
4 changed files with 55 additions and 11 deletions

View File

@ -57,3 +57,4 @@ Multi-account example:
- `channels.feishu.dm.allowFrom`: allowlist for DMs when policy is `allowlist` or `open`. - `channels.feishu.dm.allowFrom`: allowlist for DMs when policy is `allowlist` or `open`.
- `channels.feishu.groupPolicy`: `open`, `allowlist`, or `disabled`. - `channels.feishu.groupPolicy`: `open`, `allowlist`, or `disabled`.
- `channels.feishu.groups`: per-chat overrides keyed by `chat_id` (supports `requireMention`, `tools`, `users`). - `channels.feishu.groups`: per-chat overrides keyed by `chat_id` (supports `requireMention`, `tools`, `users`).
- `channels.feishu.sessionPerMessage`: start a new session for each inbound message (no history).

View File

@ -32,6 +32,12 @@ type FeishuSender = {
}; };
}; };
type FeishuMention = {
id?: { open_id?: string; user_id?: string; union_id?: string };
key?: string;
name?: string;
};
type FeishuMessage = { type FeishuMessage = {
chat_id?: string; chat_id?: string;
chat_type?: string; chat_type?: string;
@ -41,11 +47,13 @@ type FeishuMessage = {
create_time?: string; create_time?: string;
root_id?: string; root_id?: string;
parent_id?: string; parent_id?: string;
mentions?: FeishuMention[];
}; };
type FeishuMessageEvent = { type FeishuMessageEvent = {
message?: FeishuMessage; message?: FeishuMessage;
sender?: FeishuSender; sender?: FeishuSender;
mentions?: FeishuMention[];
}; };
type FeishuCoreRuntime = ReturnType<typeof getFeishuRuntime>; type FeishuCoreRuntime = ReturnType<typeof getFeishuRuntime>;
@ -91,18 +99,27 @@ function resolveSenderInfo(
return null; return null;
} }
function parseMessageText(params: { type ParsedMessageContent = {
text: string;
hasAnyMention: boolean;
};
function parseMessageContent(params: {
content: string; content: string;
runtime: FeishuRuntimeEnv; runtime: FeishuRuntimeEnv;
accountId: string; accountId: string;
}): string | null { }): ParsedMessageContent | null {
const trimmed = params.content.trim(); const trimmed = params.content.trim();
if (!trimmed) return null; if (!trimmed) return null;
try { try {
const parsed = JSON.parse(trimmed) as unknown; const parsed = JSON.parse(trimmed) as unknown;
if (!parsed || typeof parsed !== "object") return null; if (!parsed || typeof parsed !== "object") return null;
const text = (parsed as { text?: unknown }).text; const text = (parsed as { text?: unknown }).text;
return typeof text === "string" ? text : null; if (typeof text !== "string") return null;
const mentions = (parsed as { mentions?: unknown }).mentions;
const hasMentions = Array.isArray(mentions) && mentions.length > 0;
const hasInlineMention = /<at\b/i.test(text);
return { text, hasAnyMention: hasMentions || hasInlineMention };
} catch (err) { } catch (err) {
params.runtime.error?.( params.runtime.error?.(
`[${params.accountId}] Feishu message content parse failed: ${String(err)}`, `[${params.accountId}] Feishu message content parse failed: ${String(err)}`,
@ -111,8 +128,22 @@ function parseMessageText(params: {
} }
} }
function resolveMentionState(text: string): { hasAnyMention: boolean; wasMentioned: boolean } { function hasMentionEntries(entries: FeishuMention[] | undefined): boolean {
const hasAnyMention = /<at\b/i.test(text); return Array.isArray(entries) && entries.length > 0;
}
function resolvePerMessageSessionKey(baseKey: string, message: FeishuMessage): string {
const messageId = message.message_id?.trim();
const messageTime = message.create_time?.trim();
const suffix = messageId || messageTime || String(Date.now());
return `${baseKey}:msg:${suffix}`.toLowerCase();
}
function resolveMentionState(params: {
text: string;
hasAnyMention?: boolean;
}): { hasAnyMention: boolean; wasMentioned: boolean } {
const hasAnyMention = params.hasAnyMention ?? /<at\b/i.test(params.text);
return { hasAnyMention, wasMentioned: hasAnyMention }; return { hasAnyMention, wasMentioned: hasAnyMention };
} }
@ -214,12 +245,13 @@ async function handleFeishuMessage(params: {
return; return;
} }
if (typeof message.content !== "string") return; if (typeof message.content !== "string") return;
const rawBody = parseMessageText({ const parsedContent = parseMessageContent({
content: message.content, content: message.content,
runtime, runtime,
accountId: account.accountId, accountId: account.accountId,
}); });
if (rawBody === null) return; if (parsedContent === null) return;
const rawBody = parsedContent.text;
const chatType = resolveChatType(message.chat_type); const chatType = resolveChatType(message.chat_type);
const isGroup = chatType !== "direct"; const isGroup = chatType !== "direct";
@ -284,7 +316,12 @@ async function handleFeishuMessage(params: {
let effectiveWasMentioned: boolean | undefined; let effectiveWasMentioned: boolean | undefined;
if (isGroup) { if (isGroup) {
const requireMention = groupEntry?.requireMention ?? account.config.requireMention ?? true; const requireMention = groupEntry?.requireMention ?? account.config.requireMention ?? true;
const mentionState = resolveMentionState(rawBody); const hasMentions = [
parsedContent.hasAnyMention,
hasMentionEntries(message.mentions),
hasMentionEntries(event.mentions),
].some(Boolean);
const mentionState = resolveMentionState({ text: rawBody, hasAnyMention: hasMentions });
const allowTextCommands = core.channel.commands.shouldHandleTextCommands({ const allowTextCommands = core.channel.commands.shouldHandleTextCommands({
cfg: config, cfg: config,
surface: "feishu", surface: "feishu",
@ -364,6 +401,9 @@ async function handleFeishuMessage(params: {
}, },
}); });
const sessionKey = account.config.sessionPerMessage
? resolvePerMessageSessionKey(route.sessionKey, message)
: route.sessionKey;
const fromLabel = isGroup ? `chat:${chatId}` : `user:${senderId}`; const fromLabel = isGroup ? `chat:${chatId}` : `user:${senderId}`;
const storePath = core.channel.session.resolveStorePath(config.session?.store, { const storePath = core.channel.session.resolveStorePath(config.session?.store, {
agentId: route.agentId, agentId: route.agentId,
@ -371,7 +411,7 @@ async function handleFeishuMessage(params: {
const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(config); const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(config);
const previousTimestamp = core.channel.session.readSessionUpdatedAt({ const previousTimestamp = core.channel.session.readSessionUpdatedAt({
storePath, storePath,
sessionKey: route.sessionKey, sessionKey,
}); });
const timestampMs = Number(message.create_time); const timestampMs = Number(message.create_time);
const timestamp = Number.isFinite(timestampMs) ? timestampMs : undefined; const timestamp = Number.isFinite(timestampMs) ? timestampMs : undefined;
@ -392,7 +432,7 @@ async function handleFeishuMessage(params: {
CommandBody: rawBody, CommandBody: rawBody,
From: `feishu:${senderId}`, From: `feishu:${senderId}`,
To: `feishu:${chatId}`, To: `feishu:${chatId}`,
SessionKey: route.sessionKey, SessionKey: sessionKey,
AccountId: route.accountId, AccountId: route.accountId,
ChatType: isGroup ? "channel" : "direct", ChatType: isGroup ? "channel" : "direct",
ConversationLabel: fromLabel, ConversationLabel: fromLabel,
@ -414,7 +454,7 @@ async function handleFeishuMessage(params: {
void core.channel.session void core.channel.session
.recordSessionMetaFromInbound({ .recordSessionMetaFromInbound({
storePath, storePath,
sessionKey: ctxPayload.SessionKey ?? route.sessionKey, sessionKey: ctxPayload.SessionKey ?? sessionKey,
ctx: ctxPayload, ctx: ctxPayload,
}) })
.catch((err) => { .catch((err) => {

View File

@ -78,6 +78,8 @@ export type FeishuAccountConfig = {
mediaMaxMb?: number; mediaMaxMb?: number;
/** Control reply threading when reply tags are present (off|first|all). */ /** Control reply threading when reply tags are present (off|first|all). */
replyToMode?: ReplyToMode; replyToMode?: ReplyToMode;
/** Start a new session for every inbound message (no history). */
sessionPerMessage?: boolean;
dm?: FeishuDmConfig; dm?: FeishuDmConfig;
groups?: Record<string, FeishuGroupConfig>; groups?: Record<string, FeishuGroupConfig>;
/** Heartbeat visibility settings for this channel. */ /** Heartbeat visibility settings for this channel. */

View File

@ -333,6 +333,7 @@ export const FeishuAccountSchema = z
blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(), blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(),
mediaMaxMb: z.number().positive().optional(), mediaMaxMb: z.number().positive().optional(),
replyToMode: ReplyToModeSchema.optional(), replyToMode: ReplyToModeSchema.optional(),
sessionPerMessage: z.boolean().optional(),
dm: FeishuDmSchema.optional(), dm: FeishuDmSchema.optional(),
groups: z.record(z.string(), FeishuGroupSchema.optional()).optional(), groups: z.record(z.string(), FeishuGroupSchema.optional()).optional(),
heartbeat: ChannelHeartbeatVisibilitySchema, heartbeat: ChannelHeartbeatVisibilitySchema,