mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-18 20:17:28 +00:00
* 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>
123 lines
4.0 KiB
TypeScript
123 lines
4.0 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { parseFeishuMessageEvent } from "./bot.js";
|
|
|
|
function makeEvent(
|
|
text: string,
|
|
mentions?: Array<{ key: string; name: string; id: { open_id?: string; user_id?: string } }>,
|
|
chatType: "p2p" | "group" = "p2p",
|
|
) {
|
|
return {
|
|
sender: { sender_id: { user_id: "u1", open_id: "ou_sender" } },
|
|
message: {
|
|
message_id: "msg_1",
|
|
chat_id: "oc_chat1",
|
|
chat_type: chatType,
|
|
message_type: "text",
|
|
content: JSON.stringify({ text }),
|
|
mentions,
|
|
},
|
|
};
|
|
}
|
|
|
|
const BOT_OPEN_ID = "ou_bot";
|
|
|
|
describe("normalizeMentions (via parseFeishuMessageEvent)", () => {
|
|
it("returns original text when mentions are missing", () => {
|
|
const ctx = parseFeishuMessageEvent(makeEvent("hello world", undefined) as any, BOT_OPEN_ID);
|
|
expect(ctx.content).toBe("hello world");
|
|
});
|
|
|
|
it("strips bot mention in p2p (addressing prefix, not semantic content)", () => {
|
|
const ctx = parseFeishuMessageEvent(
|
|
makeEvent("@_bot_1 hello", [
|
|
{ key: "@_bot_1", name: "Bot", id: { open_id: "ou_bot" } },
|
|
]) as any,
|
|
BOT_OPEN_ID,
|
|
);
|
|
expect(ctx.content).toBe("hello");
|
|
});
|
|
|
|
it("normalizes bot mention to <at> tag in group (semantic content)", () => {
|
|
const ctx = parseFeishuMessageEvent(
|
|
makeEvent(
|
|
"@_bot_1 hello",
|
|
[{ key: "@_bot_1", name: "Bot", id: { open_id: "ou_bot" } }],
|
|
"group",
|
|
) as any,
|
|
BOT_OPEN_ID,
|
|
);
|
|
expect(ctx.content).toBe('<at user_id="ou_bot">Bot</at> hello');
|
|
});
|
|
|
|
it("strips bot mention but normalizes other mentions in p2p (mention-forward)", () => {
|
|
const ctx = parseFeishuMessageEvent(
|
|
makeEvent("@_bot_1 @_user_alice hello", [
|
|
{ key: "@_bot_1", name: "Bot", id: { open_id: "ou_bot" } },
|
|
{ key: "@_user_alice", name: "Alice", id: { open_id: "ou_alice" } },
|
|
]) as any,
|
|
BOT_OPEN_ID,
|
|
);
|
|
expect(ctx.content).toBe('<at user_id="ou_alice">Alice</at> hello');
|
|
});
|
|
|
|
it("falls back to @name when open_id is absent", () => {
|
|
const ctx = parseFeishuMessageEvent(
|
|
makeEvent("@_user_1 hi", [
|
|
{ key: "@_user_1", name: "Alice", id: { user_id: "uid_alice" } },
|
|
]) as any,
|
|
BOT_OPEN_ID,
|
|
);
|
|
expect(ctx.content).toBe("@Alice hi");
|
|
});
|
|
|
|
it("falls back to plain @name when no id is present", () => {
|
|
const ctx = parseFeishuMessageEvent(
|
|
makeEvent("@_unknown hey", [{ key: "@_unknown", name: "Nobody", id: {} }]) as any,
|
|
BOT_OPEN_ID,
|
|
);
|
|
expect(ctx.content).toBe("@Nobody hey");
|
|
});
|
|
|
|
it("treats mention key regex metacharacters as literal text", () => {
|
|
const ctx = parseFeishuMessageEvent(
|
|
makeEvent("hello world", [{ key: ".*", name: "Bot", id: { open_id: "ou_bot" } }]) as any,
|
|
BOT_OPEN_ID,
|
|
);
|
|
expect(ctx.content).toBe("hello world");
|
|
});
|
|
|
|
it("normalizes multiple mentions in one pass", () => {
|
|
const ctx = parseFeishuMessageEvent(
|
|
makeEvent("@_bot_1 hi @_user_2", [
|
|
{ key: "@_bot_1", name: "Bot One", id: { open_id: "ou_bot_1" } },
|
|
{ key: "@_user_2", name: "User Two", id: { open_id: "ou_user_2" } },
|
|
]) as any,
|
|
BOT_OPEN_ID,
|
|
);
|
|
expect(ctx.content).toBe(
|
|
'<at user_id="ou_bot_1">Bot One</at> hi <at user_id="ou_user_2">User Two</at>',
|
|
);
|
|
});
|
|
|
|
it("treats $ in display name as literal (no replacement-pattern interpolation)", () => {
|
|
const ctx = parseFeishuMessageEvent(
|
|
makeEvent("@_user_1 hi", [
|
|
{ key: "@_user_1", name: "$& the user", id: { open_id: "ou_x" } },
|
|
]) as any,
|
|
BOT_OPEN_ID,
|
|
);
|
|
// $ is preserved literally (no $& pattern substitution); & is not escaped in tag body
|
|
expect(ctx.content).toBe('<at user_id="ou_x">$& the user</at> hi');
|
|
});
|
|
|
|
it("escapes < and > in mention name to protect tag structure", () => {
|
|
const ctx = parseFeishuMessageEvent(
|
|
makeEvent("@_user_1 test", [
|
|
{ key: "@_user_1", name: "<script>", id: { open_id: "ou_x" } },
|
|
]) as any,
|
|
BOT_OPEN_ID,
|
|
);
|
|
expect(ctx.content).toBe('<at user_id="ou_x"><script></at> test');
|
|
});
|
|
});
|