150 lines
6.8 KiB
JavaScript
150 lines
6.8 KiB
JavaScript
// Interface Service — entry point
|
|
// Wires the Express app, WebSocket servers, and background tasks together.
|
|
// All domain logic lives in the modules imported below.
|
|
|
|
const express = require("express");
|
|
const { WebSocketServer } = require("ws");
|
|
const { createServer } = require("http");
|
|
const { createProxyMiddleware } = require("http-proxy-middleware");
|
|
const path = require("path");
|
|
|
|
// Config must be required first — it loads .env before any other module reads
|
|
// process.env. broadcast.js patches console.* as a side effect on require.
|
|
const { PORT, SERVICE_ROOT, SUBAGENTS_MODE, PARALLEL_SUBAGENTS } = require("./config");
|
|
const { broadcast } = require("./broadcast");
|
|
|
|
const { registerLlmProxy } = require("./llm-proxy");
|
|
const { registerApiRoutes } = require("./routes/api");
|
|
const { registerDeliverableRoutes } = require("./routes/deliverables");
|
|
const { registerEventsWebSocket } = require("./websocket/events");
|
|
const { registerUploadWebSocket } = require("./websocket/upload");
|
|
const { startDeliverablesPoller } = require("./deliverables/poller");
|
|
const { startIdleDetector } = require("./idle-detector");
|
|
const { endSession } = require("./session/chat-log");
|
|
const { cleanupDesktop } = require("./pi/process");
|
|
const { workspace } = require("./workspace");
|
|
const { resolveBaseUrlForGuest } = require("./utils/env");
|
|
const { settings } = require("./config");
|
|
const { DELIVERABLES_DIR } = require("./config");
|
|
|
|
// ─── Express app ───────────────────────────────────────────────────────────
|
|
|
|
const app = express();
|
|
const server = createServer(app);
|
|
|
|
// Selectively apply JSON parsing — LLM proxy uses its own larger limit
|
|
app.use((req, res, next) => {
|
|
if (req.path.startsWith("/llm-proxy")) return next();
|
|
express.json()(req, res, next);
|
|
});
|
|
|
|
// ─── WebSocket servers ─────────────────────────────────────────────────────
|
|
|
|
const streamWss = new WebSocketServer({ noServer: true });
|
|
const uploadWss = new WebSocketServer({ noServer: true });
|
|
|
|
// ─── CORS ──────────────────────────────────────────────────────────────────
|
|
|
|
app.use((_req, res, next) => {
|
|
res.header("Access-Control-Allow-Origin", "*");
|
|
res.header("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
res.header("Access-Control-Allow-Headers", "Content-Type");
|
|
if (_req.method === "OPTIONS") return res.sendStatus(204);
|
|
next();
|
|
});
|
|
|
|
// ─── LLM proxy ─────────────────────────────────────────────────────────────
|
|
|
|
registerLlmProxy(app);
|
|
|
|
// ─── noVNC proxy ───────────────────────────────────────────────────────────
|
|
|
|
const desktopProxy = createProxyMiddleware({
|
|
target: "http://localhost:6080",
|
|
changeOrigin: true,
|
|
pathRewrite: { "^/desktop": "" },
|
|
});
|
|
|
|
app.use("/desktop", desktopProxy);
|
|
|
|
// ─── Static UI + API routes ────────────────────────────────────────────────
|
|
|
|
registerApiRoutes(app);
|
|
registerDeliverableRoutes(app, { deliverablesDir: DELIVERABLES_DIR });
|
|
app.use(express.static(path.join(SERVICE_ROOT, "public")));
|
|
|
|
// ─── WebSocket upgrade router ──────────────────────────────────────────────
|
|
|
|
server.on("upgrade", (request, socket, head) => {
|
|
const url = new URL(request.url, `http://${request.headers.host}`);
|
|
if (url.pathname.startsWith("/desktop")) {
|
|
desktopProxy.upgrade(request, socket, head);
|
|
} else if (url.pathname === "/ws/events") {
|
|
streamWss.handleUpgrade(request, socket, head, (ws) => {
|
|
streamWss.emit("connection", ws, request);
|
|
});
|
|
} else if (url.pathname === "/ws/upload") {
|
|
uploadWss.handleUpgrade(request, socket, head, (ws) => {
|
|
uploadWss.emit("connection", ws, request);
|
|
});
|
|
} else {
|
|
socket.destroy();
|
|
}
|
|
});
|
|
|
|
registerEventsWebSocket(server, streamWss);
|
|
registerUploadWebSocket(uploadWss);
|
|
|
|
// ─── Background tasks ──────────────────────────────────────────────────────
|
|
|
|
startDeliverablesPoller();
|
|
startIdleDetector();
|
|
|
|
// ─── Start ─────────────────────────────────────────────────────────────────
|
|
|
|
server.listen(PORT, "0.0.0.0", () => {
|
|
console.log(`[open-computer] Listening on :${PORT}`);
|
|
console.log(`[open-computer] Agent: ${settings.OPENAI_MODEL ? `${process.env.AGENT_NAME || "agent"} (model: ${settings.OPENAI_MODEL})` : process.env.AGENT_NAME || "agent"}`);
|
|
console.log(`[open-computer] noVNC proxy: /desktop → localhost:6080`);
|
|
console.log(`[open-computer] Extensions: ${path.join(SERVICE_ROOT, "extensions")}`);
|
|
|
|
if (!settings.OPENAI_API_KEY && !settings.OPENAI_BASE_URL) {
|
|
console.warn(
|
|
`[open-computer] WARNING: OPENAI_API_KEY not set — prompts will fail ` +
|
|
`(or set OPENAI_BASE_URL for local providers)`,
|
|
);
|
|
}
|
|
if (settings.OPENAI_BASE_URL) {
|
|
console.log(
|
|
`[open-computer] Base URL: ${settings.OPENAI_BASE_URL} ` +
|
|
`(guest: ${resolveBaseUrlForGuest(settings.OPENAI_BASE_URL)})`,
|
|
);
|
|
}
|
|
|
|
// Notify reconnecting clients on hot-reload
|
|
setTimeout(() => {
|
|
broadcast({ type: "agent_log", content: `[server] Reloaded at ${new Date().toLocaleTimeString()}` });
|
|
broadcast({ type: "agent_log", content: `[config] SUBAGENTS_MODE=${SUBAGENTS_MODE}` });
|
|
broadcast({ type: "agent_log", content: `[config] PARALLEL_SUBAGENTS=${PARALLEL_SUBAGENTS}` });
|
|
}, 500);
|
|
});
|
|
|
|
// ─── Graceful shutdown ─────────────────────────────────────────────────────
|
|
// Close the active session log cleanly on any exit signal so sessions-*.jsonl
|
|
// always has a proper ended_at and is never left as an orphan.
|
|
|
|
function shutdown(signal) {
|
|
console.log(`[open-computer] ${signal} received — shutting down`);
|
|
endSession();
|
|
if (workspace.piProcess) {
|
|
try { workspace.piProcess.kill("SIGTERM"); } catch {}
|
|
}
|
|
cleanupDesktop();
|
|
server.close(() => process.exit(0));
|
|
setTimeout(() => process.exit(0), 3000).unref();
|
|
}
|
|
|
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
process.on("exit", () => endSession());
|