Files
2026-07-13 12:20:32 +08:00

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());