Files
Dirigent/plugin/web/dirigent-api.ts
hzhang 32dc9a4233 refactor: new design — sidecar services, moderator Gateway client, tool execute API
- Replace standalone no-reply-api Docker service with unified sidecar (services/main.mjs)
  that routes /no-reply/* and /moderator/* and starts/stops with openclaw-gateway
- Add moderator Discord Gateway client (services/moderator/index.mjs) for real-time
  MESSAGE_CREATE push instead of polling; notifies plugin via HTTP callback
- Add plugin HTTP routes (plugin/web/dirigent-api.ts) for moderator → plugin callbacks
  (wake-from-dormant, interrupt tail-match)
- Fix tool registration format: AgentTool requires execute: not handler:; factory form
  for tools needing ctx
- Rename no-reply-process.ts → sidecar-process.ts, startNoReplyApi → startSideCar
- Remove dead config fields from openclaw.plugin.json (humanList, agentList, listMode,
  channelPoliciesFile, endSymbols, waitIdentifier, multiMessage*, bypassUserIds, etc.)
- Rename noReplyPort → sideCarPort
- Remove docker-compose.yml, dev-up/down scripts, package-plugin.mjs, test-no-reply-api.mjs
- Update install.mjs: clean dist before build, copy services/, drop dead config writes
- Update README, Makefile, smoke script for new architecture

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 08:07:59 +01:00

113 lines
3.6 KiB
TypeScript

import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import type { ChannelStore } from "../core/channel-store.js";
type Deps = {
api: OpenClawPluginApi;
channelStore: ChannelStore;
moderatorBotUserId: string | undefined;
scheduleIdentifier: string;
moderatorServiceUrl: string | undefined;
moderatorServiceToken: string | undefined;
debugMode: boolean;
onNewMessage: (event: {
channelId: string;
messageId: string;
senderId: string;
guildId?: string;
}) => Promise<void>;
};
function sendJson(res: import("node:http").ServerResponse, status: number, payload: unknown): void {
res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
res.end(JSON.stringify(payload));
}
function readBody(req: import("node:http").IncomingMessage): Promise<Record<string, unknown>> {
return new Promise((resolve, reject) => {
let body = "";
req.on("data", (chunk: Buffer) => {
body += chunk.toString();
if (body.length > 1_000_000) {
req.destroy();
reject(new Error("body too large"));
}
});
req.on("end", () => {
try {
resolve(body ? (JSON.parse(body) as Record<string, unknown>) : {});
} catch {
reject(new Error("invalid_json"));
}
});
req.on("error", reject);
});
}
/**
* Register Dirigent plugin HTTP routes that the moderator service calls back into.
*
* Routes:
* POST /dirigent/api/moderator/message — inbound message notification from moderator service
* GET /dirigent/api/moderator/status — health/status check
*/
export function registerDirigentApi(deps: Deps): void {
const { api, moderatorServiceUrl, onNewMessage } = deps;
// ── POST /dirigent/api/moderator/message ─────────────────────────────────────
// Called by the moderator service on every Discord MESSAGE_CREATE event.
api.registerHttpRoute({
path: "/dirigent/api/moderator/message",
auth: "plugin",
match: "exact",
handler: async (req, res) => {
if (req.method !== "POST") {
res.writeHead(405);
res.end();
return;
}
let body: Record<string, unknown>;
try {
body = await readBody(req);
} catch (err) {
return sendJson(res, 400, { ok: false, error: String(err) });
}
const channelId = typeof body.channelId === "string" ? body.channelId : undefined;
const messageId = typeof body.messageId === "string" ? body.messageId : undefined;
const senderId = typeof body.senderId === "string" ? body.senderId : undefined;
const guildId = typeof body.guildId === "string" ? body.guildId : undefined;
if (!channelId || !senderId) {
return sendJson(res, 400, { ok: false, error: "channelId and senderId required" });
}
try {
await onNewMessage({
channelId,
messageId: messageId ?? "",
senderId,
guildId,
});
return sendJson(res, 200, { ok: true });
} catch (err) {
api.logger.warn(`dirigent: moderator/message handler error: ${String(err)}`);
return sendJson(res, 500, { ok: false, error: String(err) });
}
},
});
// ── GET /dirigent/api/moderator/status ───────────────────────────────────────
api.registerHttpRoute({
path: "/dirigent/api/moderator/status",
auth: "plugin",
match: "exact",
handler: (_req, res) => {
return sendJson(res, 200, {
ok: true,
moderatorServiceUrl: moderatorServiceUrl ?? null,
});
},
});
}