From 0f526346f48121fbc79f1d4f1ab6cf2a6b056267 Mon Sep 17 00:00:00 2001 From: orion Date: Wed, 25 Feb 2026 23:37:15 +0000 Subject: [PATCH] feat(tool): register optional discord_control tool in whispergate plugin and align defaults --- docs/CONFIG.example.json | 40 +++++++++++++--- docs/DISCORD_CONTROL.md | 3 ++ plugin/README.md | 18 ++++++- plugin/index.ts | 96 ++++++++++++++++++++++++++++++++++++- plugin/openclaw.plugin.json | 8 +++- 5 files changed, 154 insertions(+), 11 deletions(-) diff --git a/docs/CONFIG.example.json b/docs/CONFIG.example.json index 4556397..ade6fec 100644 --- a/docs/CONFIG.example.json +++ b/docs/CONFIG.example.json @@ -1,7 +1,7 @@ { "plugins": { "load": { - "paths": ["/path/to/WhisperGate/plugin"] + "paths": ["/path/to/WhisperGate/dist/whispergate"] }, "entries": { "whispergate": { @@ -10,19 +10,45 @@ "enabled": true, "discordOnly": true, "bypassUserIds": ["561921120408698910"], - "endSymbols": ["。", "!", "?", ".", "!", "?"], - "noReplyProvider": "openai", - "noReplyModel": "whispergate-no-reply-v1" + "endSymbols": ["🔚"], + "noReplyProvider": "whisper-gateway", + "noReplyModel": "no-reply", + "enableDiscordControlTool": true, + "discordControlApiBaseUrl": "http://127.0.0.1:8790", + "discordControlApiToken": "", + "discordControlCallerId": "agent-main" } } } }, "models": { "providers": { - "openai": { - "apiKey": "", - "baseURL": "http://127.0.0.1:8787/v1" + "whisper-gateway": { + "apiKey": "", + "baseUrl": "http://127.0.0.1:8787/v1", + "api": "openai-completions", + "models": [ + { + "id": "no-reply", + "name": "No Reply", + "reasoning": false, + "input": ["text"], + "cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 }, + "contextWindow": 4096, + "maxTokens": 64 + } + ] } } + }, + "agents": { + "list": [ + { + "id": "main", + "tools": { + "allow": ["whispergate"] + } + } + ] } } diff --git a/docs/DISCORD_CONTROL.md b/docs/DISCORD_CONTROL.md index 58daa3a..0df2780 100644 --- a/docs/DISCORD_CONTROL.md +++ b/docs/DISCORD_CONTROL.md @@ -2,6 +2,9 @@ 目标:补齐 OpenClaw 内置 message 工具当前未覆盖的两个能力: +> 现在可以通过 WhisperGate 插件内置的可选工具 `discord_control` 直接调用(无需手写 curl)。 +> 注意:该工具是 optional,需要在 agent tools allowlist 中显式允许(例如允许 `whispergate` 或 `discord_control`)。 + 1. 创建指定名单可见的私人频道 2. 查看 server 成员列表(分页) diff --git a/plugin/README.md b/plugin/README.md index 2373162..94feb2a 100644 --- a/plugin/README.md +++ b/plugin/README.md @@ -27,4 +27,20 @@ Optional: - `enabled` (default true) - `discordOnly` (default true) - `bypassUserIds` (default []) -- `endSymbols` (default punctuation set) +- `endSymbols` (default ["🔚"]) +- `enableDiscordControlTool` (default true) +- `discordControlApiBaseUrl` (default `http://127.0.0.1:8790`) +- `discordControlApiToken` +- `discordControlCallerId` + +## Optional tool: `discord_control` + +This plugin now registers an optional tool named `discord_control`. +To use it, add tool allowlist entry for either: +- tool name: `discord_control` +- plugin id: `whispergate` + +Supported actions: +- `channel-private-create` +- `channel-private-update` +- `member-list` diff --git a/plugin/index.ts b/plugin/index.ts index 5310078..3bf28a8 100644 --- a/plugin/index.ts +++ b/plugin/index.ts @@ -1,6 +1,8 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; import { evaluateDecision, type Decision, type WhisperGateConfig } from "./rules.js"; +type DiscordControlAction = "channel-private-create" | "channel-private-update" | "member-list"; + type DecisionRecord = { decision: Decision; createdAt: number; @@ -53,11 +55,103 @@ function shouldInjectEndMarker(reason: string): boolean { return reason === "bypass_sender" || reason.startsWith("end_symbol:"); } +function pickDefined(input: Record) { + const out: Record = {}; + for (const [k, v] of Object.entries(input)) { + if (v !== undefined) out[k] = v; + } + return out; +} + export default { id: "whispergate", name: "WhisperGate", register(api: OpenClawPluginApi) { - const config = (api.pluginConfig || {}) as WhisperGateConfig; + const config = (api.pluginConfig || {}) as WhisperGateConfig & { + enableDiscordControlTool?: boolean; + discordControlApiBaseUrl?: string; + discordControlApiToken?: string; + discordControlCallerId?: string; + }; + + if (config.enableDiscordControlTool !== false) { + api.registerTool( + { + name: "discord_control", + description: "Discord admin extension actions: private channel create/update and member list.", + parameters: { + type: "object", + additionalProperties: false, + properties: { + action: { + type: "string", + enum: ["channel-private-create", "channel-private-update", "member-list"], + }, + guildId: { type: "string" }, + name: { type: "string" }, + type: { type: "number" }, + parentId: { type: "string" }, + topic: { type: "string" }, + position: { type: "number" }, + nsfw: { type: "boolean" }, + allowedUserIds: { type: "array", items: { type: "string" } }, + allowedRoleIds: { type: "array", items: { type: "string" } }, + allowMask: { type: "string" }, + denyEveryoneMask: { type: "string" }, + channelId: { type: "string" }, + mode: { type: "string", enum: ["merge", "replace"] }, + addUserIds: { type: "array", items: { type: "string" } }, + addRoleIds: { type: "array", items: { type: "string" } }, + removeTargetIds: { type: "array", items: { type: "string" } }, + denyMask: { type: "string" }, + limit: { type: "number" }, + after: { type: "string" }, + fields: { anyOf: [{ type: "string" }, { type: "array", items: { type: "string" } }] }, + dryRun: { type: "boolean" }, + }, + required: ["action", "guildId"], + }, + 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 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; + + const r = await fetch(`${baseUrl}/v1/discord/action`, { + method: "POST", + headers, + body: JSON.stringify(body), + }); + const text = await r.text(); + + if (!r.ok) { + return { + content: [ + { + type: "text", + text: `discord_control failed (${r.status}): ${text}`, + }, + ], + isError: true, + }; + } + + return { + content: [ + { + type: "text", + text, + }, + ], + }; + }, + }, + { optional: true }, + ); + } api.registerHook("message:received", async (event, ctx) => { try { diff --git a/plugin/openclaw.plugin.json b/plugin/openclaw.plugin.json index bfba37e..b56dade 100644 --- a/plugin/openclaw.plugin.json +++ b/plugin/openclaw.plugin.json @@ -11,9 +11,13 @@ "enabled": { "type": "boolean", "default": true }, "discordOnly": { "type": "boolean", "default": true }, "bypassUserIds": { "type": "array", "items": { "type": "string" }, "default": [] }, - "endSymbols": { "type": "array", "items": { "type": "string" }, "default": ["。", "!", "?", ".", "!", "?"] }, + "endSymbols": { "type": "array", "items": { "type": "string" }, "default": ["🔚"] }, "noReplyProvider": { "type": "string" }, - "noReplyModel": { "type": "string" } + "noReplyModel": { "type": "string" }, + "enableDiscordControlTool": { "type": "boolean", "default": true }, + "discordControlApiBaseUrl": { "type": "string", "default": "http://127.0.0.1:8790" }, + "discordControlApiToken": { "type": "string" }, + "discordControlCallerId": { "type": "string" } }, "required": ["noReplyProvider", "noReplyModel"] }