fix(feishu): normalize all mentions in inbound agent context (#30252)
* fix(feishu): normalize all mentions in inbound agent context Convert Feishu mention placeholders to explicit <at user_id="..."> tags (including bot mentions), add mention semantics hints for the model, and remove unused mentionMessageBody parsing to keep context handling consistent. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(feishu): use replacer callback and escape only < > in normalizeMentions Switch String.replace to a function replacer to prevent $ sequences in display names from being interpolated as replacement patterns. Narrow escaping to < and > only — & does not need escaping in LLM prompt tag bodies and escaping it degrades readability (e.g. R&D → R&D). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(feishu): only use open_id in normalizeMentions tag, drop user_id fallback When a mention has no open_id, degrade to @name instead of emitting <at user_id="uid_...">. This keeps the tag user_id space exclusively open_id, so the bot self-reference hint (which uses botOpenId) is always consistent with what appears in the tags. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(feishu): register mention strip pattern for <at> tags in channel dock Add mentions.stripPatterns to feishuPlugin so that normalizeCommandBody receives a slash-clean string after normalizeMentions replaces Feishu placeholders with <at user_id="...">name</at> tags. Without this, group slash commands like @Bot /help had their leading / obscured by the tag prefix and no longer triggered command handlers. Pattern mirrors the approach used by Slack (<@[^>]+>) and Discord (<@!?\d+>). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(feishu): strip bot mention in p2p to preserve DM slash commands In p2p messages the bot mention is a pure addressing prefix; converting it to <at user_id="..."> breaks slash commands because buildCommandContext skips stripMentions for DMs. Extend normalizeMentions with a stripKeys set and populate it with bot mention keys in p2p, so @Bot /help arrives as /help. Non-bot mentions (mention-forward targets) are still normalized to <at> tags in both p2p and group contexts. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Changelog: note Feishu inbound mention normalization --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
@@ -18,12 +18,7 @@ import { tryRecordMessage, tryRecordMessagePersistent } from "./dedup.js";
|
||||
import { maybeCreateDynamicAgent } from "./dynamic-agent.js";
|
||||
import { normalizeFeishuExternalKey } from "./external-keys.js";
|
||||
import { downloadMessageResourceFeishu } from "./media.js";
|
||||
import {
|
||||
escapeRegExp,
|
||||
extractMentionTargets,
|
||||
extractMessageBody,
|
||||
isMentionForwardRequest,
|
||||
} from "./mention.js";
|
||||
import { extractMentionTargets, isMentionForwardRequest } from "./mention.js";
|
||||
import {
|
||||
resolveFeishuGroupConfig,
|
||||
resolveFeishuReplyPolicy,
|
||||
@@ -478,17 +473,30 @@ function checkBotMentioned(event: FeishuMessageEvent, botOpenId?: string, botNam
|
||||
return false;
|
||||
}
|
||||
|
||||
export function stripBotMention(
|
||||
function normalizeMentions(
|
||||
text: string,
|
||||
mentions?: FeishuMessageEvent["message"]["mentions"],
|
||||
botStripId?: string,
|
||||
): string {
|
||||
if (!mentions || mentions.length === 0) return text;
|
||||
|
||||
const escaped = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const escapeName = (value: string) => value.replace(/</g, "<").replace(/>/g, ">");
|
||||
let result = text;
|
||||
|
||||
for (const mention of mentions) {
|
||||
result = result.replace(new RegExp(`@${escapeRegExp(mention.name)}\\s*`, "g"), "");
|
||||
result = result.replace(new RegExp(escapeRegExp(mention.key), "g"), "");
|
||||
const mentionId = mention.id.open_id;
|
||||
const replacement =
|
||||
botStripId && mentionId === botStripId
|
||||
? ""
|
||||
: mentionId
|
||||
? `<at user_id="${mentionId}">${escapeName(mention.name)}</at>`
|
||||
: `@${mention.name}`;
|
||||
|
||||
result = result.replace(new RegExp(escaped(mention.key), "g"), () => replacement).trim();
|
||||
}
|
||||
return result.trim();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -760,7 +768,15 @@ export function parseFeishuMessageEvent(
|
||||
): FeishuMessageContext {
|
||||
const rawContent = parseMessageContent(event.message.content, event.message.message_type);
|
||||
const mentionedBot = checkBotMentioned(event, botOpenId, botName);
|
||||
const content = stripBotMention(rawContent, event.message.mentions);
|
||||
const hasAnyMention = (event.message.mentions?.length ?? 0) > 0;
|
||||
// In p2p, the bot mention is a pure addressing prefix with no semantic value;
|
||||
// strip it so slash commands like @Bot /help still have a leading /.
|
||||
// Non-bot mentions (e.g. mention-forward targets) are still normalized to <at> tags.
|
||||
const content = normalizeMentions(
|
||||
rawContent,
|
||||
event.message.mentions,
|
||||
event.message.chat_type === "p2p" ? botOpenId : undefined,
|
||||
);
|
||||
const senderOpenId = event.sender.sender_id.open_id?.trim();
|
||||
const senderUserId = event.sender.sender_id.user_id?.trim();
|
||||
const senderFallbackId = senderOpenId || senderUserId || "";
|
||||
@@ -774,6 +790,7 @@ export function parseFeishuMessageEvent(
|
||||
senderOpenId: senderFallbackId,
|
||||
chatType: event.message.chat_type,
|
||||
mentionedBot,
|
||||
hasAnyMention,
|
||||
rootId: event.message.root_id || undefined,
|
||||
parentId: event.message.parent_id || undefined,
|
||||
threadId: event.message.thread_id || undefined,
|
||||
@@ -786,9 +803,6 @@ export function parseFeishuMessageEvent(
|
||||
const mentionTargets = extractMentionTargets(event, botOpenId);
|
||||
if (mentionTargets.length > 0) {
|
||||
ctx.mentionTargets = mentionTargets;
|
||||
// Extract message body (remove all @ placeholders)
|
||||
const allMentionKeys = (event.message.mentions ?? []).map((m) => m.key);
|
||||
ctx.mentionMessageBody = extractMessageBody(content, allMentionKeys);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -798,12 +812,13 @@ export function parseFeishuMessageEvent(
|
||||
export function buildFeishuAgentBody(params: {
|
||||
ctx: Pick<
|
||||
FeishuMessageContext,
|
||||
"content" | "senderName" | "senderOpenId" | "mentionTargets" | "messageId"
|
||||
"content" | "senderName" | "senderOpenId" | "mentionTargets" | "messageId" | "hasAnyMention"
|
||||
>;
|
||||
quotedContent?: string;
|
||||
permissionErrorForAgent?: PermissionError;
|
||||
botOpenId?: string;
|
||||
}): string {
|
||||
const { ctx, quotedContent, permissionErrorForAgent } = params;
|
||||
const { ctx, quotedContent, permissionErrorForAgent, botOpenId } = params;
|
||||
let messageBody = ctx.content;
|
||||
if (quotedContent) {
|
||||
messageBody = `[Replying to: "${quotedContent}"]\n\n${ctx.content}`;
|
||||
@@ -813,6 +828,16 @@ export function buildFeishuAgentBody(params: {
|
||||
const speaker = ctx.senderName ?? ctx.senderOpenId;
|
||||
messageBody = `${speaker}: ${messageBody}`;
|
||||
|
||||
if (ctx.hasAnyMention) {
|
||||
const botIdHint = botOpenId?.trim();
|
||||
messageBody +=
|
||||
`\n\n[System: The content may include mention tags in the form <at user_id="...">name</at>. ` +
|
||||
`Treat these as real mentions of Feishu entities (users or bots).]`;
|
||||
if (botIdHint) {
|
||||
messageBody += `\n[System: If user_id is "${botIdHint}", that mention refers to you.]`;
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx.mentionTargets && ctx.mentionTargets.length > 0) {
|
||||
const targetNames = ctx.mentionTargets.map((t) => t.name).join(", ");
|
||||
messageBody += `\n\n[System: Your reply will automatically @mention: ${targetNames}. Do not write @xxx yourself.]`;
|
||||
@@ -1223,6 +1248,7 @@ export async function handleFeishuMessage(params: {
|
||||
ctx,
|
||||
quotedContent,
|
||||
permissionErrorForAgent,
|
||||
botOpenId,
|
||||
});
|
||||
const envelopeFrom = isGroup ? `${ctx.chatId}:${ctx.senderOpenId}` : ctx.senderOpenId;
|
||||
if (permissionErrorForAgent) {
|
||||
|
||||
Reference in New Issue
Block a user