feat: split dirigent_tools + human @mention override #14
79
plugin/core/identity.ts
Normal file
79
plugin/core/identity.ts
Normal file
@@ -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<string, unknown>) || {};
|
||||||
|
const channels = (root.channels as Record<string, unknown>) || {};
|
||||||
|
const discord = (channels.discord as Record<string, unknown>) || {};
|
||||||
|
const accounts = (discord.accounts as Record<string, Record<string, unknown>>) || {};
|
||||||
|
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<string, unknown>) || {};
|
||||||
|
const bindings = root.bindings as Array<Record<string, unknown>> | undefined;
|
||||||
|
if (!Array.isArray(bindings)) return undefined;
|
||||||
|
for (const b of bindings) {
|
||||||
|
if (b.agentId === agentId) {
|
||||||
|
const match = b.match as Record<string, unknown> | 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<string, unknown>) || {};
|
||||||
|
const bindings = root.bindings as Array<Record<string, unknown>> | undefined;
|
||||||
|
const agents = ((root.agents as Record<string, unknown>)?.list as Array<Record<string, unknown>>) || [];
|
||||||
|
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<string, unknown> | undefined;
|
||||||
|
if (match?.channel === "discord" && typeof match.accountId === "string") {
|
||||||
|
accountId = match.accountId;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!accountId) return undefined;
|
||||||
|
|
||||||
|
const agent = agents.find((a: Record<string, unknown>) => 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<string, string> {
|
||||||
|
const root = (api.config as Record<string, unknown>) || {};
|
||||||
|
const channels = (root.channels as Record<string, unknown>) || {};
|
||||||
|
const discord = (channels.discord as Record<string, unknown>) || {};
|
||||||
|
const accounts = (discord.accounts as Record<string, Record<string, unknown>>) || {};
|
||||||
|
const map = new Map<string, string>();
|
||||||
|
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;
|
||||||
|
}
|
||||||
31
plugin/core/mentions.ts
Normal file
31
plugin/core/mentions.ts
Normal file
@@ -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<string>();
|
||||||
|
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);
|
||||||
|
}
|
||||||
101
plugin/index.ts
101
plugin/index.ts
@@ -14,6 +14,8 @@ import { registerDirigentCommand } from "./commands/dirigent-command.js";
|
|||||||
import { registerDirigentTools } from "./tools/register-tools.js";
|
import { registerDirigentTools } from "./tools/register-tools.js";
|
||||||
import { getLivePluginConfig } from "./core/live-config.js";
|
import { getLivePluginConfig } from "./core/live-config.js";
|
||||||
import { ensurePolicyStateLoaded, persistPolicies, policyState } from "./policy/store.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 ──────────────────────────────
|
// ── No-Reply API child process lifecycle ──────────────────────────────
|
||||||
let noReplyProcess: ChildProcess | null = null;
|
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<string, unknown>) || {};
|
|
||||||
const bindings = root.bindings as Array<Record<string, unknown>> | undefined;
|
|
||||||
if (!Array.isArray(bindings)) return undefined;
|
|
||||||
for (const b of bindings) {
|
|
||||||
if (b.agentId === agentId) {
|
|
||||||
const match = b.match as Record<string, unknown> | undefined;
|
|
||||||
if (match?.channel === "discord" && typeof match.accountId === "string") {
|
|
||||||
return match.accountId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all Discord bot accountIds from config bindings.
|
* 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<string, unknown>) || {};
|
|
||||||
const bindings = root.bindings as Array<Record<string, unknown>> | undefined;
|
|
||||||
const agents = ((root.agents as Record<string, unknown>)?.list as Array<Record<string, unknown>>) || [];
|
|
||||||
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<string, unknown> | 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<string, unknown>) => 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 ---
|
// --- Moderator bot helpers ---
|
||||||
|
|
||||||
/** Extract Discord user ID from a bot token (base64-encoded in first segment) */
|
/** 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);
|
return userIdFromToken(acct.token);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Build a reverse map: Discord userId → accountId for all configured Discord accounts.
|
|
||||||
*/
|
|
||||||
function buildUserIdToAccountIdMap(api: OpenClawPluginApi): Map<string, string> {
|
|
||||||
const root = (api.config as Record<string, unknown>) || {};
|
|
||||||
const channels = (root.channels as Record<string, unknown>) || {};
|
|
||||||
const discord = (channels.discord as Record<string, unknown>) || {};
|
|
||||||
const accounts = (discord.accounts as Record<string, Record<string, unknown>>) || {};
|
|
||||||
const map = new Map<string, string>();
|
|
||||||
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<string>();
|
|
||||||
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 */
|
/** 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<boolean> {
|
async function sendModeratorMessage(token: string, channelId: string, content: string, logger: { info: (msg: string) => void; warn: (msg: string) => void }): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user