diff --git a/plugin/README.md b/plugin/README.md index c0998eb..38f4f2c 100644 --- a/plugin/README.md +++ b/plugin/README.md @@ -31,6 +31,11 @@ Optional: - `agentList` (default []) - `channelPoliciesFile` (per-channel overrides in a standalone JSON file) - `enableWhispergatePolicyTool` (default true) + +Unified optional tool: +- `whispergateway_tools` + - Discord actions: `channel-private-create`, `channel-private-update`, `member-list` + - Policy actions: `policy-get`, `policy-set-channel`, `policy-delete-channel` - `bypassUserIds` (deprecated alias of `humanList`) - `endSymbols` (default ["🔚"]) - `enableDiscordControlTool` (default true) @@ -44,16 +49,15 @@ Policy file behavior: - loaded once on startup into memory - runtime decisions read memory state only - direct file edits do NOT affect memory state -- `whispergate_policy` tool updates memory first, then persists to file (atomic write) +- `whispergateway_tools` policy actions update memory first, then persist to file (atomic write) -## Optional tool: `discord_control` +## Optional tool: `whispergateway_tools` -This plugin now registers an optional tool named `discord_control`. +This plugin registers one unified optional tool: `whispergateway_tools`. To use it, add tool allowlist entry for either: -- tool name: `discord_control` +- tool name: `whispergateway_tools` - plugin id: `whispergate` Supported actions: -- `channel-private-create` -- `channel-private-update` -- `member-list` +- Discord: `channel-private-create`, `channel-private-update`, `member-list` +- Policy: `policy-get`, `policy-set-channel`, `policy-delete-channel` diff --git a/plugin/index.ts b/plugin/index.ts index 057caec..e36bb0e 100644 --- a/plugin/index.ts +++ b/plugin/index.ts @@ -137,53 +137,65 @@ export default { const liveAtRegister = getLivePluginConfig(api, baseConfig as WhisperGateConfig); ensurePolicyStateLoaded(api, liveAtRegister); - if (baseConfig.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" }, + api.registerTool( + { + name: "whispergateway_tools", + description: "WhisperGate unified tool: Discord admin actions + in-memory policy management.", + parameters: { + type: "object", + additionalProperties: false, + properties: { + action: { + type: "string", + enum: ["channel-private-create", "channel-private-update", "member-list", "policy-get", "policy-set-channel", "policy-delete-channel"], }, - required: ["action", "guildId"], + 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" }, + listMode: { type: "string", enum: ["human-list", "agent-list"] }, + humanList: { type: "array", items: { type: "string" } }, + agentList: { type: "array", items: { type: "string" } }, + endSymbols: { type: "array", items: { type: "string" } }, }, - async execute(_id: string, params: Record) { - const action = String(params.action || "") as DiscordControlAction; - 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 }); + required: ["action"], + }, + async execute(_id: string, params: Record) { + const live = getLivePluginConfig(api, baseConfig as WhisperGateConfig) as WhisperGateConfig & { + discordControlApiBaseUrl?: string; + discordControlApiToken?: string; + discordControlCallerId?: string; + enableDiscordControlTool?: boolean; + enableWhispergatePolicyTool?: boolean; + }; + ensurePolicyStateLoaded(api, live); + const action = String(params.action || ""); + const discordActions = new Set(["channel-private-create", "channel-private-update", "member-list"]); + + if (discordActions.has(action)) { + if (live.enableDiscordControlTool === false) { + return { content: [{ type: "text", text: "discord actions disabled by config" }], isError: true }; + } + const baseUrl = (live.discordControlApiBaseUrl || "http://127.0.0.1:8790").replace(/\/$/, ""); + const body = pickDefined({ ...params, action: action as DiscordControlAction }); const headers: Record = { "Content-Type": "application/json" }; if (live.discordControlApiToken) headers.Authorization = `Bearer ${live.discordControlApiToken}`; if (live.discordControlCallerId) headers["X-OpenClaw-Caller-Id"] = live.discordControlCallerId; @@ -194,99 +206,65 @@ export default { body: JSON.stringify(body), }); const text = await r.text(); - if (!r.ok) { return { - content: [{ type: "text", text: `discord_control failed (${r.status}): ${text}` }], + content: [{ type: "text", text: `whispergateway_tools discord failed (${r.status}): ${text}` }], isError: true, }; } - return { content: [{ type: "text", text }] }; - }, - }, - { optional: true }, - ); - } + } - if (baseConfig.enableWhispergatePolicyTool !== false) { - api.registerTool( - { - name: "whispergate_policy", - description: "Manage WhisperGate in-memory channel policies and persist to file.", - parameters: { - type: "object", - additionalProperties: false, - properties: { - action: { - type: "string", - enum: ["get", "set-channel", "delete-channel"], - }, - channelId: { type: "string" }, - listMode: { type: "string", enum: ["human-list", "agent-list"] }, - humanList: { type: "array", items: { type: "string" } }, - agentList: { type: "array", items: { type: "string" } }, - endSymbols: { type: "array", items: { type: "string" } }, - }, - required: ["action"], - }, - async execute(_id: string, params: Record) { - const live = getLivePluginConfig(api, baseConfig as WhisperGateConfig); - ensurePolicyStateLoaded(api, live); - const action = String(params.action || ""); + if (live.enableWhispergatePolicyTool === false) { + return { content: [{ type: "text", text: "policy actions disabled by config" }], isError: true }; + } - if (action === "get") { - return { - content: [ - { - type: "text", - text: JSON.stringify({ file: policyState.filePath, policies: policyState.channelPolicies }, null, 2), - }, - ], + if (action === "policy-get") { + return { + content: [{ type: "text", text: JSON.stringify({ file: policyState.filePath, policies: policyState.channelPolicies }, null, 2) }], + }; + } + + if (action === "policy-set-channel") { + const channelId = String(params.channelId || "").trim(); + if (!channelId) return { content: [{ type: "text", text: "channelId is required" }], isError: true }; + + const prev = JSON.parse(JSON.stringify(policyState.channelPolicies)); + try { + const next: ChannelPolicy = { + listMode: (params.listMode as "human-list" | "agent-list" | undefined) || undefined, + humanList: Array.isArray(params.humanList) ? (params.humanList as string[]) : undefined, + agentList: Array.isArray(params.agentList) ? (params.agentList as string[]) : undefined, + endSymbols: Array.isArray(params.endSymbols) ? (params.endSymbols as string[]) : undefined, }; + policyState.channelPolicies[channelId] = pickDefined(next as unknown as Record) as ChannelPolicy; + persistPolicies(api); + return { content: [{ type: "text", text: JSON.stringify({ ok: true, channelId, policy: policyState.channelPolicies[channelId] }) }] }; + } catch (err) { + policyState.channelPolicies = prev; + return { content: [{ type: "text", text: `persist failed: ${String(err)}` }], isError: true }; } + } - if (action === "set-channel") { - const channelId = String(params.channelId || "").trim(); - if (!channelId) return { content: [{ type: "text", text: "channelId is required" }], isError: true }; - - const prev = JSON.parse(JSON.stringify(policyState.channelPolicies)); - try { - const next: ChannelPolicy = { - listMode: (params.listMode as "human-list" | "agent-list" | undefined) || undefined, - humanList: Array.isArray(params.humanList) ? (params.humanList as string[]) : undefined, - agentList: Array.isArray(params.agentList) ? (params.agentList as string[]) : undefined, - endSymbols: Array.isArray(params.endSymbols) ? (params.endSymbols as string[]) : undefined, - }; - policyState.channelPolicies[channelId] = pickDefined(next as unknown as Record) as ChannelPolicy; - persistPolicies(api); - return { content: [{ type: "text", text: JSON.stringify({ ok: true, channelId, policy: policyState.channelPolicies[channelId] }) }] }; - } catch (err) { - policyState.channelPolicies = prev; - return { content: [{ type: "text", text: `persist failed: ${String(err)}` }], isError: true }; - } + if (action === "policy-delete-channel") { + const channelId = String(params.channelId || "").trim(); + if (!channelId) return { content: [{ type: "text", text: "channelId is required" }], isError: true }; + const prev = JSON.parse(JSON.stringify(policyState.channelPolicies)); + try { + delete policyState.channelPolicies[channelId]; + persistPolicies(api); + return { content: [{ type: "text", text: JSON.stringify({ ok: true, channelId, deleted: true }) }] }; + } catch (err) { + policyState.channelPolicies = prev; + return { content: [{ type: "text", text: `persist failed: ${String(err)}` }], isError: true }; } + } - if (action === "delete-channel") { - const channelId = String(params.channelId || "").trim(); - if (!channelId) return { content: [{ type: "text", text: "channelId is required" }], isError: true }; - const prev = JSON.parse(JSON.stringify(policyState.channelPolicies)); - try { - delete policyState.channelPolicies[channelId]; - persistPolicies(api); - return { content: [{ type: "text", text: JSON.stringify({ ok: true, channelId, deleted: true }) }] }; - } catch (err) { - policyState.channelPolicies = prev; - return { content: [{ type: "text", text: `persist failed: ${String(err)}` }], isError: true }; - } - } - - return { content: [{ type: "text", text: `unsupported action: ${action}` }], isError: true }; - }, + return { content: [{ type: "text", text: `unsupported action: ${action}` }], isError: true }; }, - { optional: true }, - ); - } + }, + { optional: true }, + ); api.on("message_received", async (event, ctx) => { try {