import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; import { evaluateDecision, type Decision, type WhisperGateConfig } from "./rules.js"; const sessionDecision = new Map(); const MAX_SESSION_DECISIONS = 2000; function normalizeChannel(ctx: Record): string { const candidates = [ctx.commandSource, ctx.messageProvider, ctx.channelId, ctx.channel]; for (const c of candidates) { if (typeof c === "string" && c.trim()) return c.trim().toLowerCase(); } return ""; } function pruneDecisionMap() { if (sessionDecision.size <= MAX_SESSION_DECISIONS) return; const keys = sessionDecision.keys(); while (sessionDecision.size > MAX_SESSION_DECISIONS) { const k = keys.next(); if (k.done) break; sessionDecision.delete(k.value); } } export default { id: "whispergate", name: "WhisperGate", register(api: OpenClawPluginApi) { const config = (api.pluginConfig || {}) as WhisperGateConfig; api.registerHook("message:received", async (event, ctx) => { try { const c = (ctx || {}) as Record; const e = (event || {}) as Record; const sessionKey = typeof c.sessionKey === "string" ? c.sessionKey : undefined; if (!sessionKey) return; const senderId = typeof c.senderId === "string" ? c.senderId : typeof e.from === "string" ? e.from : undefined; const content = typeof e.content === "string" ? e.content : ""; const channel = normalizeChannel(c); const decision = evaluateDecision({ config, channel, senderId, content }); sessionDecision.set(sessionKey, decision); pruneDecisionMap(); api.logger.debug?.(`whispergate: session=${sessionKey} decision=${decision.reason}`); } catch (err) { api.logger.warn(`whispergate: message hook failed: ${String(err)}`); } }); api.on("before_model_resolve", async (_event, ctx) => { const key = ctx.sessionKey; if (!key) return; const decision = sessionDecision.get(key); if (!decision?.shouldUseNoReply) return; api.logger.info( `whispergate: override model for session=${key}, provider=${config.noReplyProvider}, model=${config.noReplyModel}, reason=${decision.reason}`, ); return { providerOverride: config.noReplyProvider, modelOverride: config.noReplyModel, }; }); }, };