import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; type Config = { enabled?: boolean; discordOnly?: boolean; bypassUserIds?: string[]; endSymbols?: string[]; noReplyProvider: string; noReplyModel: string; }; type Decision = { shouldUseNoReply: boolean; reason: string; }; const sessionDecision = new Map(); function getLastChar(input: string): string { const t = input.trim(); return t.length ? t[t.length - 1] : ""; } function shouldUseNoReply(params: { config: Config; channel?: string; senderId?: string; content?: string; }): Decision { const { config } = params; if (config.enabled === false) { return { shouldUseNoReply: false, reason: "disabled" }; } const channel = (params.channel || "").toLowerCase(); if (config.discordOnly !== false && channel !== "discord") { return { shouldUseNoReply: false, reason: "non_discord" }; } if (params.senderId && (config.bypassUserIds || []).includes(params.senderId)) { return { shouldUseNoReply: false, reason: "bypass_sender" }; } const lastChar = getLastChar(params.content || ""); if (lastChar && (config.endSymbols || []).includes(lastChar)) { return { shouldUseNoReply: false, reason: `end_symbol:${lastChar}` }; } return { shouldUseNoReply: true, reason: "rule_match_no_end_symbol" }; } export default { id: "whispergate", name: "WhisperGate", register(api: OpenClawPluginApi) { const config = (api.pluginConfig || {}) as Config; 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 = typeof c.commandSource === "string" ? c.commandSource : typeof c.channelId === "string" ? c.channelId : ""; const decision = shouldUseNoReply({ config, channel, senderId, content }); sessionDecision.set(sessionKey, decision); 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, }; }); }, };