feat: split dirigent_tools + human @mention override #14
23
plugin/core/live-config.ts
Normal file
23
plugin/core/live-config.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||||
|
import type { DirigentConfig } from "../rules.js";
|
||||||
|
|
||||||
|
export function getLivePluginConfig(api: OpenClawPluginApi, fallback: DirigentConfig): DirigentConfig {
|
||||||
|
const root = (api.config as Record<string, unknown>) || {};
|
||||||
|
const plugins = (root.plugins as Record<string, unknown>) || {};
|
||||||
|
const entries = (plugins.entries as Record<string, unknown>) || {};
|
||||||
|
const entry = (entries.dirigent as Record<string, unknown>) || (entries.whispergate as Record<string, unknown>) || {};
|
||||||
|
const cfg = (entry.config as Record<string, unknown>) || {};
|
||||||
|
if (Object.keys(cfg).length > 0) {
|
||||||
|
return {
|
||||||
|
enableDiscordControlTool: true,
|
||||||
|
enableDirigentPolicyTool: true,
|
||||||
|
discordControlApiBaseUrl: "http://127.0.0.1:8790",
|
||||||
|
enableDebugLogs: false,
|
||||||
|
debugLogChannelIds: [],
|
||||||
|
schedulingIdentifier: "➡️",
|
||||||
|
waitIdentifier: "👤",
|
||||||
|
...cfg,
|
||||||
|
} as DirigentConfig;
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ import fs from "node:fs";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { spawn, type ChildProcess } from "node:child_process";
|
import { spawn, type ChildProcess } from "node:child_process";
|
||||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||||
import { evaluateDecision, resolvePolicy, type ChannelPolicy, type Decision, type DirigentConfig } from "./rules.js";
|
import type { Decision, DirigentConfig } from "./rules.js";
|
||||||
import { initTurnOrder } from "./turn-manager.js";
|
import { initTurnOrder } from "./turn-manager.js";
|
||||||
import { startModeratorPresence, stopModeratorPresence } from "./moderator-presence.js";
|
import { startModeratorPresence, stopModeratorPresence } from "./moderator-presence.js";
|
||||||
import { registerMessageReceivedHook } from "./hooks/message-received.js";
|
import { registerMessageReceivedHook } from "./hooks/message-received.js";
|
||||||
@@ -12,6 +12,8 @@ import { registerBeforeMessageWriteHook } from "./hooks/before-message-write.js"
|
|||||||
import { registerMessageSentHook } from "./hooks/message-sent.js";
|
import { registerMessageSentHook } from "./hooks/message-sent.js";
|
||||||
import { registerDirigentCommand } from "./commands/dirigent-command.js";
|
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 { ensurePolicyStateLoaded, persistPolicies, policyState } from "./policy/store.js";
|
||||||
|
|
||||||
// ── No-Reply API child process lifecycle ──────────────────────────────
|
// ── No-Reply API child process lifecycle ──────────────────────────────
|
||||||
let noReplyProcess: ChildProcess | null = null;
|
let noReplyProcess: ChildProcess | null = null;
|
||||||
@@ -64,11 +66,6 @@ type DecisionRecord = {
|
|||||||
needsRestore?: boolean;
|
needsRestore?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PolicyState = {
|
|
||||||
filePath: string;
|
|
||||||
channelPolicies: Record<string, ChannelPolicy>;
|
|
||||||
};
|
|
||||||
|
|
||||||
type DebugConfig = {
|
type DebugConfig = {
|
||||||
enableDebugLogs?: boolean;
|
enableDebugLogs?: boolean;
|
||||||
debugLogChannelIds?: string[];
|
debugLogChannelIds?: string[];
|
||||||
@@ -97,11 +94,6 @@ function buildSchedulingIdentifierInstruction(schedulingIdentifier: string): str
|
|||||||
return `\n\nScheduling identifier: "${schedulingIdentifier}". This identifier itself is meaningless — it carries no semantic content. When you receive a message containing <@YOUR_USER_ID> followed by the scheduling identifier, check recent chat history and decide whether you have something to say. If not, reply NO_REPLY.`;
|
return `\n\nScheduling identifier: "${schedulingIdentifier}". This identifier itself is meaningless — it carries no semantic content. When you receive a message containing <@YOUR_USER_ID> followed by the scheduling identifier, check recent chat history and decide whether you have something to say. If not, reply NO_REPLY.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const policyState: PolicyState = {
|
|
||||||
filePath: "",
|
|
||||||
channelPolicies: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
function pruneDecisionMap(now = Date.now()) {
|
function pruneDecisionMap(now = Date.now()) {
|
||||||
for (const [k, v] of sessionDecision.entries()) {
|
for (const [k, v] of sessionDecision.entries()) {
|
||||||
if (now - v.createdAt > DECISION_TTL_MS) sessionDecision.delete(k);
|
if (now - v.createdAt > DECISION_TTL_MS) sessionDecision.delete(k);
|
||||||
@@ -116,56 +108,6 @@ function pruneDecisionMap(now = Date.now()) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function getLivePluginConfig(api: OpenClawPluginApi, fallback: DirigentConfig): DirigentConfig {
|
|
||||||
const root = (api.config as Record<string, unknown>) || {};
|
|
||||||
const plugins = (root.plugins as Record<string, unknown>) || {};
|
|
||||||
const entries = (plugins.entries as Record<string, unknown>) || {};
|
|
||||||
// Support both "dirigent" and legacy "whispergate" config keys
|
|
||||||
const entry = (entries.dirigent as Record<string, unknown>) || (entries.whispergate as Record<string, unknown>) || {};
|
|
||||||
const cfg = (entry.config as Record<string, unknown>) || {};
|
|
||||||
if (Object.keys(cfg).length > 0) {
|
|
||||||
// Merge with defaults to ensure optional fields have values
|
|
||||||
return {
|
|
||||||
enableDiscordControlTool: true,
|
|
||||||
enableDirigentPolicyTool: true,
|
|
||||||
discordControlApiBaseUrl: "http://127.0.0.1:8790",
|
|
||||||
enableDebugLogs: false,
|
|
||||||
debugLogChannelIds: [],
|
|
||||||
schedulingIdentifier: "➡️",
|
|
||||||
waitIdentifier: "👤",
|
|
||||||
...cfg,
|
|
||||||
} as DirigentConfig;
|
|
||||||
}
|
|
||||||
return fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolvePoliciesPath(api: OpenClawPluginApi, config: DirigentConfig): string {
|
|
||||||
return api.resolvePath(config.channelPoliciesFile || "~/.openclaw/dirigent-channel-policies.json");
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensurePolicyStateLoaded(api: OpenClawPluginApi, config: DirigentConfig) {
|
|
||||||
if (policyState.filePath) return;
|
|
||||||
const filePath = resolvePoliciesPath(api, config);
|
|
||||||
policyState.filePath = filePath;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!fs.existsSync(filePath)) {
|
|
||||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
||||||
fs.writeFileSync(filePath, "{}\n", "utf8");
|
|
||||||
policyState.channelPolicies = {};
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const raw = fs.readFileSync(filePath, "utf8");
|
|
||||||
const parsed = JSON.parse(raw) as Record<string, ChannelPolicy>;
|
|
||||||
policyState.channelPolicies = parsed && typeof parsed === "object" ? parsed : {};
|
|
||||||
} catch (err) {
|
|
||||||
api.logger.warn(`dirigent: failed init policy file ${filePath}: ${String(err)}`);
|
|
||||||
policyState.channelPolicies = {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Resolve agentId → Discord accountId from config bindings */
|
/** Resolve agentId → Discord accountId from config bindings */
|
||||||
function resolveAccountId(api: OpenClawPluginApi, agentId: string): string | undefined {
|
function resolveAccountId(api: OpenClawPluginApi, agentId: string): string | undefined {
|
||||||
const root = (api.config as Record<string, unknown>) || {};
|
const root = (api.config as Record<string, unknown>) || {};
|
||||||
@@ -374,17 +316,6 @@ async function sendModeratorMessage(token: string, channelId: string, content: s
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function persistPolicies(api: OpenClawPluginApi): void {
|
|
||||||
const filePath = policyState.filePath;
|
|
||||||
if (!filePath) throw new Error("policy file path not initialized");
|
|
||||||
const before = JSON.stringify(policyState.channelPolicies, null, 2) + "\n";
|
|
||||||
const tmp = `${filePath}.tmp`;
|
|
||||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
||||||
fs.writeFileSync(tmp, before, "utf8");
|
|
||||||
fs.renameSync(tmp, filePath);
|
|
||||||
api.logger.info(`dirigent: policy file persisted: ${filePath}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function pickDefined(input: Record<string, unknown>) {
|
function pickDefined(input: Record<string, unknown>) {
|
||||||
const out: Record<string, unknown> = {};
|
const out: Record<string, unknown> = {};
|
||||||
for (const [k, v] of Object.entries(input)) {
|
for (const [k, v] of Object.entries(input)) {
|
||||||
|
|||||||
50
plugin/policy/store.ts
Normal file
50
plugin/policy/store.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||||
|
import type { ChannelPolicy, DirigentConfig } from "../rules.js";
|
||||||
|
|
||||||
|
export type PolicyState = {
|
||||||
|
filePath: string;
|
||||||
|
channelPolicies: Record<string, ChannelPolicy>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const policyState: PolicyState = {
|
||||||
|
filePath: "",
|
||||||
|
channelPolicies: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function resolvePoliciesPath(api: OpenClawPluginApi, config: DirigentConfig): string {
|
||||||
|
return api.resolvePath(config.channelPoliciesFile || "~/.openclaw/dirigent-channel-policies.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ensurePolicyStateLoaded(api: OpenClawPluginApi, config: DirigentConfig): void {
|
||||||
|
if (policyState.filePath) return;
|
||||||
|
const filePath = resolvePoliciesPath(api, config);
|
||||||
|
policyState.filePath = filePath;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||||
|
fs.writeFileSync(filePath, "{}\n", "utf8");
|
||||||
|
policyState.channelPolicies = {};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const raw = fs.readFileSync(filePath, "utf8");
|
||||||
|
const parsed = JSON.parse(raw) as Record<string, ChannelPolicy>;
|
||||||
|
policyState.channelPolicies = parsed && typeof parsed === "object" ? parsed : {};
|
||||||
|
} catch (err) {
|
||||||
|
api.logger.warn(`dirigent: failed init policy file ${filePath}: ${String(err)}`);
|
||||||
|
policyState.channelPolicies = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function persistPolicies(api: OpenClawPluginApi): void {
|
||||||
|
if (!policyState.filePath) throw new Error("policy state not initialized");
|
||||||
|
const dir = path.dirname(policyState.filePath);
|
||||||
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
|
const tmp = `${policyState.filePath}.tmp`;
|
||||||
|
fs.writeFileSync(tmp, `${JSON.stringify(policyState.channelPolicies, null, 2)}\n`, "utf8");
|
||||||
|
fs.renameSync(tmp, policyState.filePath);
|
||||||
|
api.logger.info(`dirigent: policy file updated at ${policyState.filePath}`);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user