From 6d463a4572287b107d8df22d98bc324d8ecd93e9 Mon Sep 17 00:00:00 2001 From: orion Date: Thu, 26 Feb 2026 00:18:05 +0000 Subject: [PATCH] feat(config): add hot-reload config + listMode (human-list/agent-list) --- docs/CONFIG.example.json | 4 ++- docs/INTEGRATION.md | 4 ++- docs/ROLLOUT.md | 4 +-- plugin/README.md | 5 +++- plugin/index.ts | 39 ++++++++++++++++++------- plugin/openclaw.plugin.json | 3 ++ plugin/rules.ts | 35 ++++++++++++++++++---- scripts/install-whispergate-openclaw.sh | 10 +++++-- 8 files changed, 79 insertions(+), 25 deletions(-) diff --git a/docs/CONFIG.example.json b/docs/CONFIG.example.json index ade6fec..0ed8f1e 100644 --- a/docs/CONFIG.example.json +++ b/docs/CONFIG.example.json @@ -9,7 +9,9 @@ "config": { "enabled": true, "discordOnly": true, - "bypassUserIds": ["561921120408698910"], + "listMode": "human-list", + "humanList": ["561921120408698910"], + "agentList": [], "endSymbols": ["🔚"], "noReplyProvider": "whisper-gateway", "noReplyModel": "no-reply", diff --git a/docs/INTEGRATION.md b/docs/INTEGRATION.md index 339762a..f1c8c12 100644 --- a/docs/INTEGRATION.md +++ b/docs/INTEGRATION.md @@ -51,7 +51,9 @@ Environment overrides: - `NO_REPLY_MODEL_ID` - `NO_REPLY_BASE_URL` - `NO_REPLY_API_KEY` -- `BYPASS_USER_IDS_JSON` +- `LIST_MODE` (`human-list` or `agent-list`) +- `HUMAN_LIST_JSON` +- `AGENT_LIST_JSON` - `END_SYMBOLS_JSON` The script: diff --git a/docs/ROLLOUT.md b/docs/ROLLOUT.md index 533d71f..48a60ee 100644 --- a/docs/ROLLOUT.md +++ b/docs/ROLLOUT.md @@ -10,14 +10,14 @@ - Enable plugin with: - `discordOnly=true` - - narrow `bypassUserIds` + - `listMode=human-list` with narrow `humanList` (or `agent-list` with narrow `agentList`) - strict `endSymbols` - Point no-reply provider/model to local API - Verify 4 rule paths in `docs/VERIFY.md` ## Stage 2: Wider channel rollout -- Expand `bypassUserIds` and symbol list based on canary outcomes +- Expand `humanList`/`agentList` and symbol list based on canary outcomes - Monitor false-silent turns - Keep fallback model available diff --git a/plugin/README.md b/plugin/README.md index 94feb2a..6e13cf2 100644 --- a/plugin/README.md +++ b/plugin/README.md @@ -26,7 +26,10 @@ Required: Optional: - `enabled` (default true) - `discordOnly` (default true) -- `bypassUserIds` (default []) +- `listMode` (`human-list` | `agent-list`, default `human-list`) +- `humanList` (default []) +- `agentList` (default []) +- `bypassUserIds` (deprecated alias of `humanList`) - `endSymbols` (default ["🔚"]) - `enableDiscordControlTool` (default true) - `discordControlApiBaseUrl` (default `http://127.0.0.1:8790`) diff --git a/plugin/index.ts b/plugin/index.ts index 3bf28a8..208c0be 100644 --- a/plugin/index.ts +++ b/plugin/index.ts @@ -52,7 +52,17 @@ function pruneDecisionMap(now = Date.now()) { } function shouldInjectEndMarker(reason: string): boolean { - return reason === "bypass_sender" || reason.startsWith("end_symbol:"); + return reason.startsWith("end_symbol:"); +} + +function getLivePluginConfig(api: OpenClawPluginApi, fallback: WhisperGateConfig): WhisperGateConfig { + const root = (api.config as Record) || {}; + const plugins = (root.plugins as Record) || {}; + const entries = (plugins.entries as Record) || {}; + const entry = (entries.whispergate as Record) || {}; + const cfg = (entry.config as Record) || {}; + if (Object.keys(cfg).length > 0) return cfg as unknown as WhisperGateConfig; + return fallback; } function pickDefined(input: Record) { @@ -67,14 +77,14 @@ export default { id: "whispergate", name: "WhisperGate", register(api: OpenClawPluginApi) { - const config = (api.pluginConfig || {}) as WhisperGateConfig & { + const baseConfig = (api.pluginConfig || {}) as WhisperGateConfig & { enableDiscordControlTool?: boolean; discordControlApiBaseUrl?: string; discordControlApiToken?: string; discordControlCallerId?: string; }; - if (config.enableDiscordControlTool !== false) { + if (baseConfig.enableDiscordControlTool !== false) { api.registerTool( { name: "discord_control", @@ -113,12 +123,17 @@ export default { }, async execute(_id: string, params: Record) { const action = String(params.action || "") as DiscordControlAction; - const baseUrl = (config.discordControlApiBaseUrl || "http://127.0.0.1:8790").replace(/\/$/, ""); + const live = getLivePluginConfig(api, baseConfig as WhisperGateConfig) as WhisperGateConfig & { + discordControlApiBaseUrl?: string; + discordControlApiToken?: string; + discordControlCallerId?: string; + }; + const baseUrl = (live.discordControlApiBaseUrl || "http://127.0.0.1:8790").replace(/\/$/, ""); const body = pickDefined({ ...params, action }); const headers: Record = { "Content-Type": "application/json" }; - if (config.discordControlApiToken) headers.Authorization = `Bearer ${config.discordControlApiToken}`; - if (config.discordControlCallerId) headers["X-OpenClaw-Caller-Id"] = config.discordControlCallerId; + if (live.discordControlApiToken) headers.Authorization = `Bearer ${live.discordControlApiToken}`; + if (live.discordControlCallerId) headers["X-OpenClaw-Caller-Id"] = live.discordControlCallerId; const r = await fetch(`${baseUrl}/v1/discord/action`, { method: "POST", @@ -153,7 +168,7 @@ export default { ); } - api.registerHook("message:received", async (event, ctx) => { + api.on("message_received", async (event, ctx) => { try { const c = (ctx || {}) as Record; const e = (event || {}) as Record; @@ -164,7 +179,8 @@ export default { const content = typeof e.content === "string" ? e.content : ""; const channel = normalizeChannel(c); - const decision = evaluateDecision({ config, channel, senderId, content }); + const live = getLivePluginConfig(api, baseConfig as WhisperGateConfig); + const decision = evaluateDecision({ config: live, channel, senderId, content }); sessionDecision.set(sessionKey, { decision, createdAt: Date.now() }); pruneDecisionMap(); api.logger.debug?.( @@ -190,13 +206,14 @@ export default { // no-reply path is consumed here sessionDecision.delete(key); + const live = getLivePluginConfig(api, baseConfig as WhisperGateConfig); api.logger.info( - `whispergate: override model for session=${key}, provider=${config.noReplyProvider}, model=${config.noReplyModel}, reason=${rec.decision.reason}`, + `whispergate: override model for session=${key}, provider=${live.noReplyProvider}, model=${live.noReplyModel}, reason=${rec.decision.reason}`, ); return { - providerOverride: config.noReplyProvider, - modelOverride: config.noReplyModel, + providerOverride: live.noReplyProvider, + modelOverride: live.noReplyModel, }; }); diff --git a/plugin/openclaw.plugin.json b/plugin/openclaw.plugin.json index b56dade..968c0bf 100644 --- a/plugin/openclaw.plugin.json +++ b/plugin/openclaw.plugin.json @@ -10,6 +10,9 @@ "properties": { "enabled": { "type": "boolean", "default": true }, "discordOnly": { "type": "boolean", "default": true }, + "listMode": { "type": "string", "enum": ["human-list", "agent-list"], "default": "human-list" }, + "humanList": { "type": "array", "items": { "type": "string" }, "default": [] }, + "agentList": { "type": "array", "items": { "type": "string" }, "default": [] }, "bypassUserIds": { "type": "array", "items": { "type": "string" }, "default": [] }, "endSymbols": { "type": "array", "items": { "type": "string" }, "default": ["🔚"] }, "noReplyProvider": { "type": "string" }, diff --git a/plugin/rules.ts b/plugin/rules.ts index 9fafcc2..fb00f3c 100644 --- a/plugin/rules.ts +++ b/plugin/rules.ts @@ -1,6 +1,10 @@ export type WhisperGateConfig = { enabled?: boolean; discordOnly?: boolean; + listMode?: "human-list" | "agent-list"; + humanList?: string[]; + agentList?: string[]; + // backward compatibility bypassUserIds?: string[]; endSymbols?: string[]; noReplyProvider: string; @@ -34,14 +38,33 @@ export function evaluateDecision(params: { return { shouldUseNoReply: false, reason: "non_discord" }; } - if (params.senderId && (config.bypassUserIds || []).includes(params.senderId)) { - return { shouldUseNoReply: false, reason: "bypass_sender" }; - } + const mode = config.listMode || "human-list"; + const humanList = config.humanList || config.bypassUserIds || []; + const agentList = config.agentList || []; + + const senderId = params.senderId || ""; + const inHumanList = !!senderId && humanList.includes(senderId); + const inAgentList = !!senderId && agentList.includes(senderId); const lastChar = getLastChar(params.content || ""); - if (lastChar && (config.endSymbols || []).includes(lastChar)) { - return { shouldUseNoReply: false, reason: `end_symbol:${lastChar}` }; + const hasEnd = !!lastChar && (config.endSymbols || []).includes(lastChar); + + if (mode === "human-list") { + if (inHumanList) { + return { shouldUseNoReply: false, reason: "human_list_sender" }; + } + if (hasEnd) { + return { shouldUseNoReply: false, reason: `end_symbol:${lastChar}` }; + } + return { shouldUseNoReply: true, reason: "rule_match_no_end_symbol" }; } - return { shouldUseNoReply: true, reason: "rule_match_no_end_symbol" }; + // agent-list mode: listed senders require end symbol; others bypass requirement. + if (!inAgentList) { + return { shouldUseNoReply: false, reason: "non_agent_list_sender" }; + } + if (hasEnd) { + return { shouldUseNoReply: false, reason: `end_symbol:${lastChar}` }; + } + return { shouldUseNoReply: true, reason: "agent_list_missing_end_symbol" }; } diff --git a/scripts/install-whispergate-openclaw.sh b/scripts/install-whispergate-openclaw.sh index 267b9fe..c91bcda 100755 --- a/scripts/install-whispergate-openclaw.sh +++ b/scripts/install-whispergate-openclaw.sh @@ -14,7 +14,9 @@ NO_REPLY_PROVIDER_ID="${NO_REPLY_PROVIDER_ID:-whisper-gateway}" NO_REPLY_MODEL_ID="${NO_REPLY_MODEL_ID:-no-reply}" NO_REPLY_BASE_URL="${NO_REPLY_BASE_URL:-http://127.0.0.1:8787/v1}" NO_REPLY_API_KEY="${NO_REPLY_API_KEY:-wg-local-test-token}" -BYPASS_USER_IDS_JSON="${BYPASS_USER_IDS_JSON:-[\"561921120408698910\",\"1474088632750047324\"]}" +LIST_MODE="${LIST_MODE:-human-list}" +HUMAN_LIST_JSON="${HUMAN_LIST_JSON:-[\"561921120408698910\",\"1474088632750047324\"]}" +AGENT_LIST_JSON="${AGENT_LIST_JSON:-[]}" END_SYMBOLS_JSON="${END_SYMBOLS_JSON:-[\"🔚\"]}" STATE_DIR="${STATE_DIR:-$HOME/.openclaw/whispergate-install-records}" @@ -136,7 +138,7 @@ PY current_plugins_json='{}' fi - new_plugins_json="$(CURRENT_PLUGINS_JSON="$current_plugins_json" PLUGIN_PATH="$PLUGIN_PATH" BYPASS_USER_IDS_JSON="$BYPASS_USER_IDS_JSON" END_SYMBOLS_JSON="$END_SYMBOLS_JSON" NO_REPLY_PROVIDER_ID="$NO_REPLY_PROVIDER_ID" NO_REPLY_MODEL_ID="$NO_REPLY_MODEL_ID" python3 - <<'PY' + new_plugins_json="$(CURRENT_PLUGINS_JSON="$current_plugins_json" PLUGIN_PATH="$PLUGIN_PATH" LIST_MODE="$LIST_MODE" HUMAN_LIST_JSON="$HUMAN_LIST_JSON" AGENT_LIST_JSON="$AGENT_LIST_JSON" END_SYMBOLS_JSON="$END_SYMBOLS_JSON" NO_REPLY_PROVIDER_ID="$NO_REPLY_PROVIDER_ID" NO_REPLY_MODEL_ID="$NO_REPLY_MODEL_ID" python3 - <<'PY' import json, os plugins=json.loads(os.environ['CURRENT_PLUGINS_JSON']) if not isinstance(plugins,dict): @@ -157,7 +159,9 @@ entries['whispergate']={ 'config': { 'enabled': True, 'discordOnly': True, - 'bypassUserIds': json.loads(os.environ['BYPASS_USER_IDS_JSON']), + 'listMode': os.environ['LIST_MODE'], + 'humanList': json.loads(os.environ['HUMAN_LIST_JSON']), + 'agentList': json.loads(os.environ['AGENT_LIST_JSON']), 'endSymbols': json.loads(os.environ['END_SYMBOLS_JSON']), 'noReplyProvider': os.environ['NO_REPLY_PROVIDER_ID'], 'noReplyModel': os.environ['NO_REPLY_MODEL_ID'],