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>
This commit is contained in:
131
services/no-reply-api/server.mjs
Normal file
131
services/no-reply-api/server.mjs
Normal file
@@ -0,0 +1,131 @@
|
||||
import http from "node:http";
|
||||
|
||||
const modelName = process.env.NO_REPLY_MODEL || "no-reply";
|
||||
const authToken = process.env.AUTH_TOKEN || "";
|
||||
|
||||
function sendJson(res, status, payload) {
|
||||
res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
|
||||
res.end(JSON.stringify(payload));
|
||||
}
|
||||
|
||||
function isAuthorized(req) {
|
||||
if (!authToken) return true;
|
||||
const header = req.headers.authorization || "";
|
||||
return header === `Bearer ${authToken}`;
|
||||
}
|
||||
|
||||
function noReplyChatCompletion(reqBody) {
|
||||
return {
|
||||
id: `chatcmpl_dirigent_${Date.now()}`,
|
||||
object: "chat.completion",
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
model: reqBody?.model || modelName,
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
message: { role: "assistant", content: "NO_REPLY" },
|
||||
finish_reason: "stop",
|
||||
},
|
||||
],
|
||||
usage: { prompt_tokens: 0, completion_tokens: 1, total_tokens: 1 },
|
||||
};
|
||||
}
|
||||
|
||||
function noReplyResponses(reqBody) {
|
||||
return {
|
||||
id: `resp_dirigent_${Date.now()}`,
|
||||
object: "response",
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
model: reqBody?.model || modelName,
|
||||
output: [
|
||||
{
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
content: [{ type: "output_text", text: "NO_REPLY" }],
|
||||
},
|
||||
],
|
||||
usage: { input_tokens: 0, output_tokens: 1, total_tokens: 1 },
|
||||
};
|
||||
}
|
||||
|
||||
function listModels() {
|
||||
return {
|
||||
object: "list",
|
||||
data: [
|
||||
{
|
||||
id: modelName,
|
||||
object: "model",
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
owned_by: "dirigent",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Node.js HTTP request handler for the no-reply API.
|
||||
* When used as a sub-service inside main.mjs, the caller strips
|
||||
* the "/no-reply" prefix from req.url before calling this handler.
|
||||
*/
|
||||
export function createNoReplyHandler() {
|
||||
return function noReplyHandler(req, res) {
|
||||
const url = req.url ?? "/";
|
||||
|
||||
if (req.method === "GET" && url === "/health") {
|
||||
return sendJson(res, 200, {
|
||||
ok: true,
|
||||
service: "dirigent-no-reply-api",
|
||||
model: modelName,
|
||||
});
|
||||
}
|
||||
|
||||
if (req.method === "GET" && url === "/v1/models") {
|
||||
if (!isAuthorized(req)) return sendJson(res, 401, { error: "unauthorized" });
|
||||
return sendJson(res, 200, listModels());
|
||||
}
|
||||
|
||||
if (req.method !== "POST") {
|
||||
return sendJson(res, 404, { error: "not_found" });
|
||||
}
|
||||
|
||||
if (!isAuthorized(req)) {
|
||||
return sendJson(res, 401, { error: "unauthorized" });
|
||||
}
|
||||
|
||||
let body = "";
|
||||
req.on("data", (chunk) => {
|
||||
body += chunk;
|
||||
if (body.length > 1_000_000) req.destroy();
|
||||
});
|
||||
|
||||
req.on("end", () => {
|
||||
let parsed = {};
|
||||
try {
|
||||
parsed = body ? JSON.parse(body) : {};
|
||||
} catch {
|
||||
return sendJson(res, 400, { error: "invalid_json" });
|
||||
}
|
||||
|
||||
if (url === "/v1/chat/completions") {
|
||||
return sendJson(res, 200, noReplyChatCompletion(parsed));
|
||||
}
|
||||
|
||||
if (url === "/v1/responses") {
|
||||
return sendJson(res, 200, noReplyResponses(parsed));
|
||||
}
|
||||
|
||||
return sendJson(res, 404, { error: "not_found" });
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// Standalone mode: run HTTP server if this file is the entry point
|
||||
const isMain = process.argv[1] && process.argv[1].endsWith("server.mjs");
|
||||
if (isMain) {
|
||||
const port = Number(process.env.PORT || 8787);
|
||||
const handler = createNoReplyHandler();
|
||||
const server = http.createServer(handler);
|
||||
server.listen(port, () => {
|
||||
console.log(`[dirigent-no-reply-api] listening on :${port}`);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user