Files
clawdbot/src/auto-reply/reply/mentions.ts

139 lines
4.2 KiB
TypeScript

import { resolveAgentConfig } from "../../agents/agent-scope.js";
import type { ClawdbotConfig } from "../../config/config.js";
import type { MsgContext } from "../templating.js";
function escapeRegExp(text: string): string {
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function deriveMentionPatterns(identity?: { name?: string; emoji?: string }) {
const patterns: string[] = [];
const name = identity?.name?.trim();
if (name) {
const parts = name.split(/\s+/).filter(Boolean).map(escapeRegExp);
const re = parts.length ? parts.join(String.raw`\s+`) : escapeRegExp(name);
patterns.push(String.raw`\b@?${re}\b`);
}
const emoji = identity?.emoji?.trim();
if (emoji) {
patterns.push(escapeRegExp(emoji));
}
return patterns;
}
const BACKSPACE_CHAR = "\u0008";
export const CURRENT_MESSAGE_MARKER = "[Current message - respond to this]";
function normalizeMentionPattern(pattern: string): string {
if (!pattern.includes(BACKSPACE_CHAR)) return pattern;
return pattern.split(BACKSPACE_CHAR).join("\\b");
}
function normalizeMentionPatterns(patterns: string[]): string[] {
return patterns.map(normalizeMentionPattern);
}
function resolveMentionPatterns(
cfg: ClawdbotConfig | undefined,
agentId?: string,
): string[] {
if (!cfg) return [];
const agentConfig = agentId ? resolveAgentConfig(cfg, agentId) : undefined;
const agentGroupChat = agentConfig?.groupChat;
if (agentGroupChat && Object.hasOwn(agentGroupChat, "mentionPatterns")) {
return agentGroupChat.mentionPatterns ?? [];
}
const globalGroupChat = cfg.messages?.groupChat;
if (globalGroupChat && Object.hasOwn(globalGroupChat, "mentionPatterns")) {
return globalGroupChat.mentionPatterns ?? [];
}
const derived = deriveMentionPatterns(agentConfig?.identity);
return derived.length > 0 ? derived : [];
}
export function buildMentionRegexes(
cfg: ClawdbotConfig | undefined,
agentId?: string,
): RegExp[] {
const patterns = normalizeMentionPatterns(
resolveMentionPatterns(cfg, agentId),
);
return patterns
.map((pattern) => {
try {
return new RegExp(pattern, "i");
} catch {
return null;
}
})
.filter((value): value is RegExp => Boolean(value));
}
export function normalizeMentionText(text: string): string {
return (text ?? "")
.replace(/[\u200b-\u200f\u202a-\u202e\u2060-\u206f]/g, "")
.toLowerCase();
}
export function matchesMentionPatterns(
text: string,
mentionRegexes: RegExp[],
): boolean {
if (mentionRegexes.length === 0) return false;
const cleaned = normalizeMentionText(text ?? "");
if (!cleaned) return false;
return mentionRegexes.some((re) => re.test(cleaned));
}
export function stripStructuralPrefixes(text: string): string {
// Ignore wrapper labels, timestamps, and sender prefixes so directive-only
// detection still works in group batches that include history/context.
const afterMarker = text.includes(CURRENT_MESSAGE_MARKER)
? text
.slice(
text.indexOf(CURRENT_MESSAGE_MARKER) + CURRENT_MESSAGE_MARKER.length,
)
.trimStart()
: text;
return afterMarker
.replace(/\[[^\]]+\]\s*/g, "")
.replace(/^[ \t]*[A-Za-z0-9+()\-_. ]+:\s*/gm, "")
.replace(/\\n/g, " ")
.replace(/\s+/g, " ")
.trim();
}
export function stripMentions(
text: string,
ctx: MsgContext,
cfg: ClawdbotConfig | undefined,
agentId?: string,
): string {
let result = text;
const rawPatterns = resolveMentionPatterns(cfg, agentId);
const patterns = normalizeMentionPatterns(rawPatterns);
for (const p of patterns) {
try {
const re = new RegExp(p, "gi");
result = result.replace(re, " ");
} catch {
// ignore invalid regex
}
}
const selfE164 = (ctx.To ?? "").replace(/^whatsapp:/, "");
if (selfE164) {
const esc = selfE164.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
result = result
.replace(new RegExp(esc, "gi"), " ")
.replace(new RegExp(`@${esc}`, "gi"), " ");
}
// Generic mention patterns like @123456789 or plain digits
result = result.replace(/@[0-9+]{5,}/g, " ");
// Discord-style mentions (<@123> or <@!123>)
result = result.replace(/<@!?\d+>/g, " ");
return result.replace(/\s+/g, " ").trim();
}