refactor(plugin): extract live config and policy store modules
This commit is contained in:
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 { spawn, type ChildProcess } from "node:child_process";
|
||||
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 { startModeratorPresence, stopModeratorPresence } from "./moderator-presence.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 { 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";
|
||||
|
||||
// ── No-Reply API child process lifecycle ──────────────────────────────
|
||||
let noReplyProcess: ChildProcess | null = null;
|
||||
@@ -64,11 +66,6 @@ type DecisionRecord = {
|
||||
needsRestore?: boolean;
|
||||
};
|
||||
|
||||
type PolicyState = {
|
||||
filePath: string;
|
||||
channelPolicies: Record<string, ChannelPolicy>;
|
||||
};
|
||||
|
||||
type DebugConfig = {
|
||||
enableDebugLogs?: boolean;
|
||||
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.`;
|
||||
}
|
||||
|
||||
const policyState: PolicyState = {
|
||||
filePath: "",
|
||||
channelPolicies: {},
|
||||
};
|
||||
|
||||
function pruneDecisionMap(now = Date.now()) {
|
||||
for (const [k, v] of sessionDecision.entries()) {
|
||||
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 */
|
||||
function resolveAccountId(api: OpenClawPluginApi, agentId: string): string | undefined {
|
||||
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>) {
|
||||
const out: Record<string, unknown> = {};
|
||||
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