fix(hooks): tighten thread ownership mention matching

This commit is contained in:
Vincent Koc
2026-04-22 12:36:11 -07:00
parent 2aaac45c07
commit 4ed2ea5035
2 changed files with 57 additions and 2 deletions

View File

@@ -325,5 +325,49 @@ describe("thread-ownership plugin", () => {
expect(result).toBeUndefined();
expect(globalThis.fetch).not.toHaveBeenCalled();
});
it("does not treat superset handles as agent-name mentions", async () => {
await hooks.message_received(
{
content: "hey @testbot2 help",
threadId: "8888.0003",
metadata: { channelId: "C789" },
},
{ channelId: "slack", conversationId: "C789" },
);
vi.mocked(globalThis.fetch).mockResolvedValue(
new Response(JSON.stringify({ owner: "test-agent" }), { status: 200 }),
);
await hooks.message_sending(
{ content: "On it!", replyToId: "8888.0003", metadata: { channelId: "C789" }, to: "C789" },
{ channelId: "slack", conversationId: "C789" },
);
expect(globalThis.fetch).toHaveBeenCalled();
});
it("does not treat email-like text as an agent-name mention", async () => {
await hooks.message_received(
{
content: "send mail to foo@testbot.com",
threadId: "8888.0004",
metadata: { channelId: "C789" },
},
{ channelId: "slack", conversationId: "C789" },
);
vi.mocked(globalThis.fetch).mockResolvedValue(
new Response(JSON.stringify({ owner: "test-agent" }), { status: 200 }),
);
await hooks.message_sending(
{ content: "On it!", replyToId: "8888.0004", metadata: { channelId: "C789" }, to: "C789" },
{ channelId: "slack", conversationId: "C789" },
);
expect(globalThis.fetch).toHaveBeenCalled();
});
});
});

View File

@@ -24,6 +24,10 @@ function resolveThreadToken(value: unknown): string {
return typeof value === "string" || typeof value === "number" ? String(value) : "";
}
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function resolveSlackConversationId(value: unknown): string {
const raw = normalizeOptionalString(value) ?? "";
if (!raw) {
@@ -44,6 +48,14 @@ function cleanExpiredMentions(): void {
}
}
function containsAgentNameMention(text: string, agentName: string): boolean {
const trimmedName = agentName.trim();
if (!trimmedName) {
return false;
}
return new RegExp(`(^|[^\\w])@${escapeRegExp(trimmedName)}(?=$|[^\\w])`, "i").test(text);
}
function resolveOwnershipAgent(config: OpenClawConfig): { id: string; name: string } {
const list = Array.isArray(config.agents?.list)
? config.agents.list.filter(
@@ -91,7 +103,6 @@ export default definePluginEntry({
}
const text = event.content ?? "";
const normalizedText = text.toLowerCase();
const threadTs =
resolveThreadToken(event.threadId) ||
resolveThreadToken(event.metadata?.threadId) ||
@@ -105,7 +116,7 @@ export default definePluginEntry({
}
const mentioned =
(agentName && normalizedText.includes(`@${agentName.toLowerCase()}`)) ||
containsAgentNameMention(text, agentName) ||
(botUserId && text.includes(`<@${botUserId}>`));
if (mentioned) {
cleanExpiredMentions();