UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

315 lines (314 loc) 14.2 kB
import { c as isRecord } from "./record-coerce-DItp3I4t.js"; import { c as normalizeOptionalLowercaseString, l as normalizeOptionalString, o as normalizeLowercaseStringOrEmpty } from "./string-coerce-CIXf7egm.js"; import { C as compileConfigRegexes } from "./redact-BtvPPfTi.js"; import "./utils-P__uGsPB.js"; import { t as escapeRegExp } from "./regexp-BZyMFTlj.js"; import { c as resolveAgentConfig } from "./agent-scope-config-DcbEhP0R.js"; import { t as createSubsystemLogger } from "./subsystem-Dy2tqXOS.js"; import "./agent-scope-DbtJyKUL.js"; import { t as normalizeAnyChannelId } from "./registry-normalize-CY8V_nbx.js"; import "./registry-COqNvmCg.js"; import { t as getLoadedChannelPluginById } from "./registry-loaded-Bh7xuMJh.js"; import "./history-DLKGD0Dj.js"; //#region src/channels/mention-pattern-policy.ts /** * Mention-pattern policy resolver. * * Applies provider and conversation allow/deny rules to mention pattern matching. */ function normalizeIdList(values) { const normalized = /* @__PURE__ */ new Set(); for (const value of values ?? []) { const next = normalizeOptionalString(value); if (next) normalized.add(next); } return normalized; } function isMentionPatternsPolicyConfig(value) { return isRecord(value); } function resolveProviderMentionPatternsPolicy(cfg, provider) { if (!cfg || !provider) return; const channelConfig = cfg.channels?.[provider]; const policy = isRecord(channelConfig) ? channelConfig.mentionPatterns : void 0; return isMentionPatternsPolicyConfig(policy) ? policy : void 0; } /** * Resolves provider-scoped mention-pattern policy for a single conversation. */ function resolveMentionPatternPolicy(params) { const conversationId = normalizeOptionalString(params.conversationId ?? void 0) ?? void 0; const providerPolicy = params.providerPolicy ?? resolveProviderMentionPatternsPolicy(params.cfg, params.provider); const effectiveMode = providerPolicy?.mode === "allow" || providerPolicy?.mode === "deny" ? providerPolicy.mode : "allow"; const allowMatched = conversationId != null && normalizeIdList(providerPolicy?.allowIn).has(conversationId); const denyMatched = conversationId != null && normalizeIdList(providerPolicy?.denyIn).has(conversationId); return { effectiveMode, allowMatched, denyMatched, enabled: effectiveMode === "allow" ? !denyMatched : allowMatched && !denyMatched }; } //#endregion //#region src/auto-reply/reply/mentions.ts /** Mention matching, stripping, and explicit mention handling for group triggers. */ const NAME_IDENTITY_CHARS = String.raw`\p{L}\p{N}\p{Pc}`; const NAME_TOKEN_CHARS = String.raw`${NAME_IDENTITY_CHARS}\p{M}`; const JOINER_CHARS = String.raw`\u200C\u200D`; const DECORATION_SPACING = String.raw`[${JOINER_CHARS}\s]*`; const OPTIONAL_JOINER_GAP = String.raw`[${JOINER_CHARS}]*`; const UNICODE_WORD_CHAR = String.raw`[${NAME_TOKEN_CHARS}${JOINER_CHARS}]`; const JOINER_RUN = new RegExp(`[${JOINER_CHARS}]+`, "u"); const JOINER_ONLY = new RegExp(`^[${JOINER_CHARS}]+$`, "u"); const OMISSIBLE_DECORATION_CHAR = new RegExp(String.raw`[\p{So}\p{M}\u{1F3FB}-\u{1F3FF}\u200B-\u200F\u202A-\u202E\u2060-\u206F\u{E0020}-\u{E007F}]`, "u"); const EMOJI_PRESENTATION_MARKS = /* @__PURE__ */ new Set(["️", "⃣"]); const EMOJI_PRESENTATION_BASE = /\p{Emoji}/u; const NAME_IDENTITY_GRAPHEME = new RegExp(`[${NAME_IDENTITY_CHARS}]`, "u"); const NAME_GRAPHEME_SEGMENTER = new Intl.Segmenter(void 0, { granularity: "grapheme" }); function wrapDerivedMentionPattern(parts) { const leading = parts.leading ? `(?:${parts.leading}${DECORATION_SPACING}|)` : ""; const trailing = parts.trailing ? `(?:${DECORATION_SPACING}${parts.trailing}(?!${UNICODE_WORD_CHAR})|(?!${UNICODE_WORD_CHAR})(?!${DECORATION_SPACING}${parts.trailing}${UNICODE_WORD_CHAR}))` : `(?!${UNICODE_WORD_CHAR})`; return `(?:@|(?<!${UNICODE_WORD_CHAR}${leading}))${leading}${parts.core}${trailing}`; } function encodeOptionalJoiners(literal) { return literal.split(/([\u200C\u200D]+)/u).filter(Boolean).map((part) => JOINER_ONLY.test(part) ? `(?:${escapeRegExp(part)}|)` : escapeRegExp(part)).join(""); } function escapeJoinerTolerantLiteral(literal) { if (Array.from(literal).every((character) => JOINER_ONLY.test(character))) return ""; return encodeOptionalJoiners(literal); } function isEmojiPresentationGrapheme(grapheme) { const characters = Array.from(grapheme); const first = characters[0]; return Boolean(first && EMOJI_PRESENTATION_BASE.test(first) && characters.some((character) => EMOJI_PRESENTATION_MARKS.has(character))); } function isIdentityGrapheme(grapheme) { return NAME_IDENTITY_GRAPHEME.test(grapheme) && !isEmojiPresentationGrapheme(grapheme); } function isDecorationGrapheme(grapheme) { if (isEmojiPresentationGrapheme(grapheme)) return true; return Array.from(grapheme).every((character) => /\s/u.test(character) || JOINER_RUN.test(character) || OMISSIBLE_DECORATION_CHAR.test(character)); } function parseNameUnits(name) { const graphemes = Array.from(NAME_GRAPHEME_SEGMENTER.segment(name), (part) => part.segment); const runs = []; for (const grapheme of graphemes) { const identity = isIdentityGrapheme(grapheme); const previous = runs.at(-1); if (previous?.identity === identity) previous.literal += grapheme; else runs.push({ identity, literal: grapheme }); } return runs.map((run) => { if (run.identity) return { kind: "token", literal: run.literal }; const gapGraphemes = Array.from(NAME_GRAPHEME_SEGMENTER.segment(run.literal), (part) => part.segment); if (!gapGraphemes.every(isDecorationGrapheme)) return { kind: "separator", literal: run.literal }; return { kind: "decoration", literal: run.literal, spellings: gapGraphemes.filter((grapheme) => !/^\s+$/u.test(grapheme) && !JOINER_ONLY.test(grapheme)).map(escapeJoinerTolerantLiteral), spaced: /\s/u.test(run.literal) }; }); } function encodeSeparator(unit) { return unit.literal.split(/(\s+|[\u200C\u200D]+)/u).filter(Boolean).map((piece) => /^\s+$/u.test(piece) ? String.raw`\s+` : JOINER_ONLY.test(piece) ? encodeOptionalJoiners(piece) : escapeRegExp(piece)).join(""); } function encodeEdgeDecorationLiteral(unit) { if (unit?.kind !== "decoration") return ""; const spelled = unit.spellings.join(OPTIONAL_JOINER_GAP); if (!spelled) return encodeOptionalJoiners(Array.from(unit.literal).filter((character) => JOINER_ONLY.test(character)).join("")); return spelled; } function encodeInteriorDecoration(unit) { const spelled = unit.spellings.join(DECORATION_SPACING); if (!spelled) { const joiners = encodeOptionalJoiners(Array.from(unit.literal).filter((character) => JOINER_ONLY.test(character)).join("")); return unit.spaced ? String.raw`${joiners}\s${DECORATION_SPACING}` : joiners; } return `(?:${DECORATION_SPACING}${spelled}${DECORATION_SPACING}|\\s${unit.spaced ? "+" : "*"})`; } function deriveNameParts(name) { const units = parseNameUnits(name); if (!units.some((unit) => unit.kind === "token")) return { leading: "", core: escapeJoinerTolerantLiteral(name), trailing: "" }; const start = units[0]?.kind === "decoration" ? 1 : 0; const end = units.at(-1)?.kind === "decoration" ? units.length - 1 : units.length; let core = ""; for (const unit of units.slice(start, end)) core += unit.kind === "token" ? escapeJoinerTolerantLiteral(unit.literal) : unit.kind === "separator" ? encodeSeparator(unit) : encodeInteriorDecoration(unit); return { leading: encodeEdgeDecorationLiteral(units[0]), core, trailing: encodeEdgeDecorationLiteral(units.at(-1)) }; } function deriveMentionPatterns(identity) { const patterns = []; const name = normalizeOptionalString(identity?.name); const parts = name ? deriveNameParts(name) : void 0; if (parts?.core) patterns.push(wrapDerivedMentionPattern(parts)); const emoji = normalizeOptionalString(identity?.emoji); const emojiPattern = emoji ? escapeJoinerTolerantLiteral(emoji) : ""; if (emojiPattern) patterns.push(emojiPattern); return patterns; } const BACKSPACE_CHAR = "\b"; const mentionMatchRegexCompileCache = /* @__PURE__ */ new Map(); const mentionStripRegexCompileCache = /* @__PURE__ */ new Map(); const MAX_MENTION_REGEX_COMPILE_CACHE_KEYS = 512; const mentionPatternWarningCache = /* @__PURE__ */ new Set(); const MAX_MENTION_PATTERN_WARNING_KEYS = 512; const log = createSubsystemLogger("mentions"); function normalizeMentionPattern(pattern) { if (!pattern.includes(BACKSPACE_CHAR)) return pattern; return pattern.split(BACKSPACE_CHAR).join("\\b"); } function normalizeMentionPatterns(patterns) { return patterns.map(normalizeMentionPattern); } function warnRejectedMentionPattern(pattern, flags, reason) { const key = `${flags}::${reason}::${pattern}`; if (mentionPatternWarningCache.has(key)) return; mentionPatternWarningCache.add(key); if (mentionPatternWarningCache.size > MAX_MENTION_PATTERN_WARNING_KEYS) { mentionPatternWarningCache.clear(); mentionPatternWarningCache.add(key); } log.warn("Ignoring unsupported group mention pattern", { pattern, flags, reason }); } function cacheMentionRegexes(cache, cacheKey, regexes) { cache.set(cacheKey, regexes); if (cache.size > MAX_MENTION_REGEX_COMPILE_CACHE_KEYS) { cache.clear(); cache.set(cacheKey, regexes); } return [...regexes]; } function compileMentionPatternsCached(params) { if (params.patterns.length === 0) return []; const cacheKey = `${params.flags}\u001e${params.patterns.join("")}`; const cached = params.cache.get(cacheKey); if (cached) return [...cached]; const compiled = compileConfigRegexes(params.patterns, params.flags); if (params.warnRejected) for (const rejected of compiled.rejected) warnRejectedMentionPattern(rejected.pattern, rejected.flags, rejected.reason); return cacheMentionRegexes(params.cache, cacheKey, compiled.regexes); } function resolveMentionPatterns(cfg, agentId) { if (!cfg) return { patterns: [], unicode: false }; const agentConfig = agentId ? resolveAgentConfig(cfg, agentId) : void 0; const agentGroupChat = agentConfig?.groupChat; if (agentGroupChat && Object.hasOwn(agentGroupChat, "mentionPatterns")) return { patterns: agentGroupChat.mentionPatterns ?? [], unicode: false }; const globalGroupChat = cfg.messages?.groupChat; if (globalGroupChat && Object.hasOwn(globalGroupChat, "mentionPatterns")) return { patterns: globalGroupChat.mentionPatterns ?? [], unicode: false }; const derived = deriveMentionPatterns(agentConfig?.identity); return { patterns: derived, unicode: derived.length > 0 }; } /** Builds mention regexes from config, agent identity, and channel policy. */ function buildMentionRegexes(cfg, agentId, options) { if (!resolveMentionPatternPolicy({ ...options, cfg, agentId }).enabled) return []; const resolved = resolveMentionPatterns(cfg, agentId); return compileMentionPatternsCached({ patterns: normalizeMentionPatterns(resolved.patterns), flags: resolved.unicode ? "iu" : "i", cache: mentionMatchRegexCompileCache, warnRejected: true }); } /** Normalizes text before mention matching. */ function normalizeMentionText(text) { return normalizeLowercaseStringOrEmpty((text ?? "").replace(/[\u200b-\u200f\u202a-\u202e\u2060-\u206f]/g, "")); } /** Returns true when text matches one of the configured mention patterns. */ function matchesMentionPatterns(text, mentionRegexes) { if (mentionRegexes.length === 0) return false; const cleaned = normalizeMentionText(text ?? ""); return mentionRegexes.some((re) => re.test(cleaned)); } /** Combines regex mention matching with provider-native explicit mention metadata. */ function matchesMentionWithExplicit(params) { const cleaned = normalizeMentionText(params.text ?? ""); const explicit = params.explicit?.isExplicitlyMentioned === true; const transcriptCleaned = params.transcript ? normalizeMentionText(params.transcript) : ""; const textToCheck = cleaned || transcriptCleaned; return explicit || params.mentionRegexes.some((re) => re.test(textToCheck)); } /** Removes structural prompt prefixes before mention stripping. */ function stripStructuralPrefixes(text) { if (!text) return ""; if (text.trimStart().startsWith("[Chat messages since your last reply - for context]")) return text.trim(); const afterMarker = text; const afterEnvelope = afterMarker.replace(/^(?:[ \t]*\[[^\]\n]+\][ \t]*)+/, ""); const senderPrefixPattern = afterEnvelope === afterMarker ? /^[ \t]*(?!\/)[^\n:]{1,120}:\s+/gm : /^[ \t]*[^\n:]{1,120}:\s+/gm; const stripped = afterEnvelope.replace(senderPrefixPattern, "").replace(/\\n/g, " ").trim(); if (stripped.startsWith("/")) return stripped.replace(/[ \t]+/g, " "); return stripped.replace(/\s+/g, " "); } /** Removes bot mentions from command text before command normalization. */ function stripMentions(text, ctx, cfg, agentId) { let result = text; const providerId = (ctx.Provider ? normalizeAnyChannelId(ctx.Provider) : null) ?? normalizeOptionalLowercaseString(ctx.Provider) ?? null; const providerMentions = providerId ? getLoadedChannelPluginById(providerId)?.mentions : void 0; const resolvedPatterns = resolveMentionPatterns(cfg, agentId); const configRegexes = compileMentionPatternsCached({ patterns: normalizeMentionPatterns(resolvedPatterns.patterns), flags: resolvedPatterns.unicode ? "giu" : "gi", cache: mentionStripRegexCompileCache, warnRejected: true }); const providerRegexes = providerMentions?.stripRegexes?.({ ctx, cfg, agentId }) ?? compileMentionPatternsCached({ patterns: normalizeMentionPatterns(providerMentions?.stripPatterns?.({ ctx, cfg, agentId }) ?? []), flags: "gi", cache: mentionStripRegexCompileCache, warnRejected: false }); for (const re of [...configRegexes, ...providerRegexes]) result = result.replace(re, " "); if (providerMentions?.stripMentions) result = providerMentions.stripMentions({ text: result, ctx, cfg, agentId }); result = result.replace(/@[0-9+]{5,}/g, " "); return result.replace(/\s+/g, " ").trim(); } //#endregion export { stripMentions as a, normalizeMentionText as i, matchesMentionPatterns as n, stripStructuralPrefixes as o, matchesMentionWithExplicit as r, resolveMentionPatternPolicy as s, buildMentionRegexes as t };