feat: 支持群聊
This commit is contained in:
parent
963c5b5a7a
commit
e722c9233a
@ -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).
|
||||||
|
|||||||
@ -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) => {
|
||||||
|
|||||||
@ -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. */
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user