From 05b902c0b2441ad9b532a5ce23d272f0ca657788 Mon Sep 17 00:00:00 2001 From: orion Date: Sun, 8 Mar 2026 05:19:40 +0000 Subject: [PATCH] refactor(plugin): extract identity and mention helpers into core modules --- plugin/core/identity.ts | 79 +++++++++++++++++++++++++++++++ plugin/core/mentions.ts | 31 ++++++++++++ plugin/index.ts | 101 +--------------------------------------- 3 files changed, 112 insertions(+), 99 deletions(-) create mode 100644 plugin/core/identity.ts create mode 100644 plugin/core/mentions.ts diff --git a/plugin/core/identity.ts b/plugin/core/identity.ts new file mode 100644 index 0000000..6b35d2b --- /dev/null +++ b/plugin/core/identity.ts @@ -0,0 +1,79 @@ +import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; + +function userIdFromToken(token: string): string | undefined { + try { + const segment = token.split(".")[0]; + const padded = segment + "=".repeat((4 - (segment.length % 4)) % 4); + return Buffer.from(padded, "base64").toString("utf8"); + } catch { + return undefined; + } +} + +function resolveDiscordUserIdFromAccount(api: OpenClawPluginApi, accountId: string): string | undefined { + const root = (api.config as Record) || {}; + const channels = (root.channels as Record) || {}; + const discord = (channels.discord as Record) || {}; + const accounts = (discord.accounts as Record>) || {}; + const acct = accounts[accountId]; + if (!acct?.token || typeof acct.token !== "string") return undefined; + return userIdFromToken(acct.token); +} + +export function resolveAccountId(api: OpenClawPluginApi, agentId: string): string | undefined { + const root = (api.config as Record) || {}; + const bindings = root.bindings as Array> | undefined; + if (!Array.isArray(bindings)) return undefined; + for (const b of bindings) { + if (b.agentId === agentId) { + const match = b.match as Record | undefined; + if (match?.channel === "discord" && typeof match.accountId === "string") { + return match.accountId; + } + } + } + return undefined; +} + +export function buildAgentIdentity(api: OpenClawPluginApi, agentId: string): string | undefined { + const root = (api.config as Record) || {}; + const bindings = root.bindings as Array> | undefined; + const agents = ((root.agents as Record)?.list as Array>) || []; + if (!Array.isArray(bindings)) return undefined; + + let accountId: string | undefined; + for (const b of bindings) { + if (b.agentId === agentId) { + const match = b.match as Record | undefined; + if (match?.channel === "discord" && typeof match.accountId === "string") { + accountId = match.accountId; + break; + } + } + } + if (!accountId) return undefined; + + const agent = agents.find((a: Record) => a.id === agentId); + const name = (agent?.name as string) || agentId; + const discordUserId = resolveDiscordUserIdFromAccount(api, accountId); + + let identity = `You are ${name} (Discord account: ${accountId}`; + if (discordUserId) identity += `, Discord userId: ${discordUserId}`; + identity += `).`; + return identity; +} + +export function buildUserIdToAccountIdMap(api: OpenClawPluginApi): Map { + const root = (api.config as Record) || {}; + const channels = (root.channels as Record) || {}; + const discord = (channels.discord as Record) || {}; + const accounts = (discord.accounts as Record>) || {}; + const map = new Map(); + for (const [accountId, acct] of Object.entries(accounts)) { + if (typeof acct.token === "string") { + const userId = userIdFromToken(acct.token); + if (userId) map.set(userId, accountId); + } + } + return map; +} diff --git a/plugin/core/mentions.ts b/plugin/core/mentions.ts new file mode 100644 index 0000000..6fa041e --- /dev/null +++ b/plugin/core/mentions.ts @@ -0,0 +1,31 @@ +import type { DirigentConfig } from "../rules.js"; + +function userIdFromToken(token: string): string | undefined { + try { + const segment = token.split(".")[0]; + const padded = segment + "=".repeat((4 - (segment.length % 4)) % 4); + return Buffer.from(padded, "base64").toString("utf8"); + } catch { + return undefined; + } +} + +export function extractMentionedUserIds(content: string): string[] { + const regex = /<@!?(\d+)>/g; + const ids: string[] = []; + const seen = new Set(); + let match: RegExpExecArray | null; + while ((match = regex.exec(content)) !== null) { + const id = match[1]; + if (!seen.has(id)) { + seen.add(id); + ids.push(id); + } + } + return ids; +} + +export function getModeratorUserId(config: DirigentConfig): string | undefined { + if (!config.moderatorBotToken) return undefined; + return userIdFromToken(config.moderatorBotToken); +} diff --git a/plugin/index.ts b/plugin/index.ts index 5e62598..8556d83 100644 --- a/plugin/index.ts +++ b/plugin/index.ts @@ -14,6 +14,8 @@ import { registerDirigentCommand } from "./commands/dirigent-command.js"; import { registerDirigentTools } from "./tools/register-tools.js"; import { getLivePluginConfig } from "./core/live-config.js"; import { ensurePolicyStateLoaded, persistPolicies, policyState } from "./policy/store.js"; +import { buildAgentIdentity, buildUserIdToAccountIdMap, resolveAccountId } from "./core/identity.js"; +import { extractMentionedUserIds, getModeratorUserId } from "./core/mentions.js"; // ── No-Reply API child process lifecycle ────────────────────────────── let noReplyProcess: ChildProcess | null = null; @@ -108,22 +110,6 @@ function pruneDecisionMap(now = Date.now()) { } } -/** Resolve agentId → Discord accountId from config bindings */ -function resolveAccountId(api: OpenClawPluginApi, agentId: string): string | undefined { - const root = (api.config as Record) || {}; - const bindings = root.bindings as Array> | undefined; - if (!Array.isArray(bindings)) return undefined; - for (const b of bindings) { - if (b.agentId === agentId) { - const match = b.match as Record | undefined; - if (match?.channel === "discord" && typeof match.accountId === "string") { - return match.accountId; - } - } - } - return undefined; -} - /** * Get all Discord bot accountIds from config bindings. */ @@ -184,45 +170,6 @@ function ensureTurnOrder(api: OpenClawPluginApi, channelId: string): void { } } -/** - * Build agent identity string for injection into group chat prompts. - * Includes agent name, Discord accountId, and Discord userId. - */ -function buildAgentIdentity(api: OpenClawPluginApi, agentId: string): string | undefined { - const root = (api.config as Record) || {}; - const bindings = root.bindings as Array> | undefined; - const agents = ((root.agents as Record)?.list as Array>) || []; - if (!Array.isArray(bindings)) return undefined; - - // Find accountId for this agent - let accountId: string | undefined; - for (const b of bindings) { - if (b.agentId === agentId) { - const match = b.match as Record | undefined; - if (match?.channel === "discord" && typeof match.accountId === "string") { - accountId = match.accountId; - break; - } - } - } - if (!accountId) return undefined; - - // Find agent name - const agent = agents.find((a: Record) => a.id === agentId); - const name = (agent?.name as string) || agentId; - - // Resolve Discord userId from bot token - const discordUserId = resolveDiscordUserId(api, accountId); - - let identity = `You are ${name} (Discord account: ${accountId}`; - if (discordUserId) { - identity += `, Discord userId: ${discordUserId}`; - } - identity += `).`; - - return identity; -} - // --- Moderator bot helpers --- /** Extract Discord user ID from a bot token (base64-encoded in first segment) */ @@ -248,50 +195,6 @@ function resolveDiscordUserId(api: OpenClawPluginApi, accountId: string): string return userIdFromToken(acct.token); } -/** - * Build a reverse map: Discord userId → accountId for all configured Discord accounts. - */ -function buildUserIdToAccountIdMap(api: OpenClawPluginApi): Map { - const root = (api.config as Record) || {}; - const channels = (root.channels as Record) || {}; - const discord = (channels.discord as Record) || {}; - const accounts = (discord.accounts as Record>) || {}; - const map = new Map(); - for (const [accountId, acct] of Object.entries(accounts)) { - if (typeof acct.token === "string") { - const userId = userIdFromToken(acct.token); - if (userId) map.set(userId, accountId); - } - } - return map; -} - -/** - * Extract Discord @mention user IDs from message content. - * Matches <@USER_ID> and <@!USER_ID> patterns. - * Returns user IDs in the order they appear. - */ -function extractMentionedUserIds(content: string): string[] { - const regex = /<@!?(\d+)>/g; - const ids: string[] = []; - const seen = new Set(); - let match; - while ((match = regex.exec(content)) !== null) { - const id = match[1]; - if (!seen.has(id)) { - seen.add(id); - ids.push(id); - } - } - return ids; -} - -/** Get the moderator bot's Discord user ID from its token */ -function getModeratorUserId(config: DirigentConfig): string | undefined { - if (!config.moderatorBotToken) return undefined; - return userIdFromToken(config.moderatorBotToken); -} - /** Send a message as the moderator bot via Discord REST API */ async function sendModeratorMessage(token: string, channelId: string, content: string, logger: { info: (msg: string) => void; warn: (msg: string) => void }): Promise { try {