feat: lifecycle management - no-reply API + moderator bot follow gateway start/stop

- Added gateway_start hook: spawns no-reply API as child process, then starts moderator bot
- Added gateway_stop hook: kills no-reply API process, stops moderator bot
- No-reply API server.mjs is located relative to plugin dir via import.meta.url
- Moderator presence moved from register() to gateway_start for proper lifecycle
This commit is contained in:
zhi
2026-03-03 16:01:34 +00:00
parent cb58e3d951
commit 8e26744162

View File

@@ -1,10 +1,50 @@
import fs from "node:fs"; import fs from "node:fs";
import path from "node:path"; import path from "node:path";
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 { evaluateDecision, resolvePolicy, type ChannelPolicy, type Decision, type DirigentConfig } from "./rules.js";
import { checkTurn, advanceTurn, resetTurn, onNewMessage, onSpeakerDone, initTurnOrder, getTurnDebugInfo } from "./turn-manager.js"; import { checkTurn, advanceTurn, resetTurn, onNewMessage, onSpeakerDone, initTurnOrder, getTurnDebugInfo } from "./turn-manager.js";
import { startModeratorPresence, stopModeratorPresence } from "./moderator-presence.js"; import { startModeratorPresence, stopModeratorPresence } from "./moderator-presence.js";
// ── No-Reply API child process lifecycle ──────────────────────────────
let noReplyProcess: ChildProcess | null = null;
function startNoReplyApi(logger: { info: (m: string) => void; warn: (m: string) => void }, pluginDir: string, port = 8787): void {
if (noReplyProcess) {
logger.info("dirigent: no-reply API already running, skipping");
return;
}
const serverPath = path.resolve(pluginDir, "..", "no-reply-api", "server.mjs");
if (!fs.existsSync(serverPath)) {
logger.warn(`dirigent: no-reply API server not found at ${serverPath}, skipping`);
return;
}
noReplyProcess = spawn(process.execPath, [serverPath], {
env: { ...process.env, PORT: String(port) },
stdio: ["ignore", "pipe", "pipe"],
detached: false,
});
noReplyProcess.stdout?.on("data", (d: Buffer) => logger.info(`dirigent: no-reply-api: ${d.toString().trim()}`));
noReplyProcess.stderr?.on("data", (d: Buffer) => logger.warn(`dirigent: no-reply-api: ${d.toString().trim()}`));
noReplyProcess.on("exit", (code, signal) => {
logger.info(`dirigent: no-reply API exited (code=${code}, signal=${signal})`);
noReplyProcess = null;
});
logger.info(`dirigent: no-reply API started (pid=${noReplyProcess.pid}, port=${port})`);
}
function stopNoReplyApi(logger: { info: (m: string) => void }): void {
if (!noReplyProcess) return;
logger.info("dirigent: stopping no-reply API");
noReplyProcess.kill("SIGTERM");
noReplyProcess = null;
}
type DiscordControlAction = "channel-private-create" | "channel-private-update" | "member-list"; type DiscordControlAction = "channel-private-create" | "channel-private-update" | "member-list";
type DecisionRecord = { type DecisionRecord = {
@@ -467,11 +507,25 @@ export default {
const liveAtRegister = getLivePluginConfig(api, baseConfig as DirigentConfig); const liveAtRegister = getLivePluginConfig(api, baseConfig as DirigentConfig);
ensurePolicyStateLoaded(api, liveAtRegister); ensurePolicyStateLoaded(api, liveAtRegister);
// Start moderator bot presence (keep it "online" on Discord) // Resolve plugin directory for locating sibling modules (no-reply-api/)
if (liveAtRegister.moderatorBotToken) { const pluginDir = path.dirname(new URL(import.meta.url).pathname);
startModeratorPresence(liveAtRegister.moderatorBotToken, api.logger);
api.logger.info("dirigent: moderator bot presence starting"); // Gateway lifecycle: start/stop no-reply API and moderator bot with the gateway
} api.on("gateway_start", () => {
startNoReplyApi(api.logger, pluginDir);
const live = getLivePluginConfig(api, baseConfig as DirigentConfig);
if (live.moderatorBotToken) {
startModeratorPresence(live.moderatorBotToken, api.logger);
api.logger.info("dirigent: moderator bot presence starting");
}
});
api.on("gateway_stop", () => {
stopNoReplyApi(api.logger);
stopModeratorPresence();
api.logger.info("dirigent: gateway stopping, services shut down");
});
api.registerTool( api.registerTool(
{ {