AI_PROJECT / src /ui-server /server.mjs
chenchenaoyang's picture
Deploy from Codex
f395b70 verified
#!/usr/bin/env node
import fs from "node:fs/promises";
import http from "node:http";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { ConfigSwitchStore } from "./config-switch-store.mjs";
import { HistoryStore } from "./history-store.mjs";
import { ApiPoolProxyManager } from "./api-pool-proxy-manager.mjs";
import { PoolStore } from "./pool-store.mjs";
import { ProxyManager } from "./proxy-manager.mjs";
import { RunManager } from "./run-manager.mjs";
import { createToolPayload } from "./tool-registry.mjs";
const REPO_ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname), "..", "..");
const DEFAULT_HOST = "127.0.0.1";
const DEFAULT_PORT = 8788;
const DEFAULT_DATA_DIR = path.join(REPO_ROOT, ".local-ui-data");
const DEFAULT_STATIC_DIR = path.join(REPO_ROOT, "dist", "ui");
function parseArgs(argv) {
const args = {};
for (const item of argv) {
if (!item.startsWith("--")) continue;
const [key, ...rest] = item.slice(2).split("=");
args[key] = rest.length > 0 ? rest.join("=") : "true";
}
return args;
}
function json(res, statusCode, value) {
res.statusCode = statusCode;
res.setHeader("content-type", "application/json; charset=utf-8");
res.end(`${JSON.stringify(value, null, 2)}\n`);
}
async function readJsonBody(req) {
const chunks = [];
for await (const chunk of req) {
chunks.push(chunk);
}
const text = Buffer.concat(chunks).toString("utf8").trim();
if (!text) return {};
return JSON.parse(text);
}
async function fileExists(filePath) {
try {
await fs.access(filePath);
return true;
} catch {
return false;
}
}
function contentTypeFor(filePath) {
if (filePath.endsWith(".html")) return "text/html; charset=utf-8";
if (filePath.endsWith(".js")) return "text/javascript; charset=utf-8";
if (filePath.endsWith(".css")) return "text/css; charset=utf-8";
if (filePath.endsWith(".json")) return "application/json; charset=utf-8";
if (filePath.endsWith(".svg")) return "image/svg+xml";
if (filePath.endsWith(".png")) return "image/png";
return "application/octet-stream";
}
async function serveStatic(req, res, staticDir) {
const url = new URL(req.url || "/", "http://localhost");
const pathname = url.pathname === "/" ? "/index.html" : url.pathname;
const requested = path.join(staticDir, pathname);
const resolved = path.resolve(requested);
if (!resolved.startsWith(path.resolve(staticDir))) {
json(res, 403, { error: "Forbidden" });
return true;
}
const filePath = (await fileExists(resolved))
? resolved
: path.join(staticDir, "index.html");
if (!(await fileExists(filePath))) {
return false;
}
const body = await fs.readFile(filePath);
res.statusCode = 200;
res.setHeader("content-type", contentTypeFor(filePath));
res.end(body);
return true;
}
export function createRequestListener({
staticDir,
historyStore,
poolStore,
runManager,
proxyManager,
apiPoolProxyManagerCodex,
apiPoolProxyManagerClaude,
configSwitchStore,
}) {
return async (req, res) => {
try {
const url = new URL(req.url || "/", "http://localhost");
const pathname = url.pathname;
if (req.method === "GET" && pathname === "/api/app-config") {
json(res, 200, {
mode: "local",
apiBase: "/api",
environment: "本地 Node + React",
user: null,
readOnly: false,
readOnlyReason: "",
});
return;
}
if (req.method === "GET" && pathname === "/api/tools") {
json(res, 200, { tools: createToolPayload() });
return;
}
if (req.method === "GET" && pathname === "/api/history") {
json(res, 200, { items: await historyStore.list() });
return;
}
if (req.method === "GET" && pathname === "/api/config-switch") {
json(res, 200, await configSwitchStore.getConfigSwitchData());
return;
}
if (pathname.startsWith("/api/config-switch/")) {
const parts = pathname.split("/").filter(Boolean);
const provider = decodeURIComponent(parts[2] || "");
const presetId = decodeURIComponent(parts[3] || "");
if (req.method === "POST" && parts.length === 3) {
const body = await readJsonBody(req);
const payload = await configSwitchStore.upsertPreset(provider, body);
json(res, body?.id ? 200 : 201, payload);
return;
}
if (req.method === "DELETE" && parts.length === 4) {
json(res, 200, await configSwitchStore.deletePreset(provider, presetId));
return;
}
if (req.method === "POST" && parts.length === 5 && parts[4] === "copy") {
json(res, 201, await configSwitchStore.copyPreset(provider, presetId));
return;
}
if (req.method === "POST" && parts.length === 5 && parts[4] === "activate") {
const body = await readJsonBody(req);
json(
res,
200,
await configSwitchStore.activatePreset(provider, presetId, body),
);
return;
}
}
if (req.method === "GET" && pathname === "/api/pools") {
json(res, 200, { items: poolStore.listPools() });
return;
}
if (req.method === "GET" && pathname.startsWith("/api/pools/")) {
const poolId = pathname.split("/")[3];
json(res, 200, await poolStore.loadPool(poolId));
return;
}
if (req.method === "PUT" && pathname.startsWith("/api/pools/")) {
const poolId = pathname.split("/")[3];
const body = await readJsonBody(req);
json(res, 200, await poolStore.savePool(poolId, body.items || []));
return;
}
if (
req.method === "POST" &&
pathname.startsWith("/api/pools/") &&
pathname.endsWith("/validate")
) {
const poolId = pathname.split("/")[3];
const body = await readJsonBody(req);
const result = poolStore.validatePoolItems(poolId, body.items || []);
json(res, result.ok ? 200 : 400, result);
return;
}
if (
req.method === "POST" &&
pathname.startsWith("/api/pools/") &&
pathname.endsWith("/update-local-token")
) {
const parts = pathname.split("/").filter(Boolean);
const poolId = parts[2];
const index = parts[3];
if (poolId !== "codex-accounts") {
json(res, 400, { error: "只有 Codex 账号池支持本地 auth.json 更新。" });
return;
}
const body = await readJsonBody(req);
json(
res,
200,
await poolStore.updateCodexAccountFromLocalAuth(index, body || {}),
);
return;
}
if (req.method === "POST" && pathname === "/api/runs") {
const body = await readJsonBody(req);
const result = await runManager.execute(body);
json(res, 202, result);
return;
}
if (req.method === "GET" && pathname.startsWith("/api/runs/") && pathname.endsWith("/logs")) {
const runId = pathname.split("/")[3];
const run = runManager.getRun(runId);
if (!run) {
json(res, 404, { error: "Run not found" });
return;
}
json(res, 200, { runId, status: run.status, logs: run.logs });
return;
}
if (req.method === "GET" && pathname.startsWith("/api/runs/")) {
const runId = pathname.split("/")[3];
const run = runManager.getRun(runId);
if (!run) {
json(res, 404, { error: "Run not found" });
return;
}
json(res, 200, {
run: {
id: run.id,
toolId: run.toolId,
status: run.status,
createdAt: run.createdAt,
startedAt: run.startedAt,
finishedAt: run.finishedAt,
exitCode: run.exitCode,
error: run.error,
commandPreview: run.commandPreview,
params: run.params,
},
});
return;
}
if (req.method === "POST" && pathname === "/api/proxy/start") {
const body = await readJsonBody(req);
const result = await proxyManager.start(body.params || {});
json(res, result.reused ? 200 : 201, result);
return;
}
if (req.method === "POST" && pathname === "/api/proxy/stop") {
json(res, 200, await proxyManager.stop());
return;
}
if (req.method === "GET" && pathname === "/api/proxy/status") {
json(res, 200, await proxyManager.getStatus());
return;
}
if (req.method === "POST" && pathname === "/api/api-pool/start") {
const body = await readJsonBody(req);
const params = body.params || {};
const provider = params.provider || "codex";
const manager = provider === "claude-code" ? apiPoolProxyManagerClaude : apiPoolProxyManagerCodex;
const result = await manager.start(params);
json(res, result.reused ? 200 : 201, result);
return;
}
if (req.method === "POST" && pathname === "/api/api-pool/stop") {
const body = await readJsonBody(req);
const params = body.params || {};
const provider = params.provider || "codex";
const manager = provider === "claude-code" ? apiPoolProxyManagerClaude : apiPoolProxyManagerCodex;
json(res, 200, await manager.stop());
return;
}
if (req.method === "GET" && pathname === "/api/api-pool/status") {
json(res, 200, await apiPoolProxyManagerCodex.getStatus());
return;
}
if (req.method === "POST" && pathname === "/api/api-pool/codex/start") {
const body = await readJsonBody(req);
const result = await apiPoolProxyManagerCodex.start(body.params || {});
json(res, result.reused ? 200 : 201, result);
return;
}
if (req.method === "POST" && pathname === "/api/api-pool/codex/stop") {
json(res, 200, await apiPoolProxyManagerCodex.stop());
return;
}
if (req.method === "GET" && pathname === "/api/api-pool/codex/status") {
json(res, 200, await apiPoolProxyManagerCodex.getStatus());
return;
}
if (req.method === "POST" && pathname === "/api/api-pool/claude-code/start") {
const body = await readJsonBody(req);
const result = await apiPoolProxyManagerClaude.start(body.params || {});
json(res, result.reused ? 200 : 201, result);
return;
}
if (req.method === "POST" && pathname === "/api/api-pool/claude-code/stop") {
json(res, 200, await apiPoolProxyManagerClaude.stop());
return;
}
if (req.method === "GET" && pathname === "/api/api-pool/claude-code/status") {
json(res, 200, await apiPoolProxyManagerClaude.getStatus());
return;
}
if (await serveStatic(req, res, staticDir)) {
return;
}
json(res, 404, { error: "Not found" });
} catch (error) {
json(res, error.statusCode || 500, {
error: error?.message || String(error),
});
}
};
}
export async function main() {
const args = parseArgs(process.argv.slice(2));
const host = args.host || DEFAULT_HOST;
const port = Number(args.port || DEFAULT_PORT);
const dataDir = path.resolve(args["data-dir"] || DEFAULT_DATA_DIR);
const staticDir = path.resolve(args["static-dir"] || DEFAULT_STATIC_DIR);
const historyStore = new HistoryStore(dataDir);
await historyStore.load();
const poolStore = new PoolStore();
const runManager = new RunManager(historyStore);
const proxyManager = new ProxyManager(historyStore);
const apiPoolProxyManagerCodex = new ApiPoolProxyManager(historyStore);
const apiPoolProxyManagerClaude = new ApiPoolProxyManager(historyStore);
const configSwitchStore = new ConfigSwitchStore(dataDir);
await configSwitchStore.load();
const server = http.createServer(
createRequestListener({
staticDir,
historyStore,
poolStore,
runManager,
proxyManager,
apiPoolProxyManagerCodex,
apiPoolProxyManagerClaude,
configSwitchStore,
}),
);
await new Promise((resolve, reject) => {
server.once("error", reject);
server.listen(port, host, () => {
server.off("error", reject);
console.log(`Local UI server listening on http://${host}:${port}`);
console.log(`Data dir: ${dataDir}`);
console.log(`Static dir: ${staticDir}`);
resolve();
});
});
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
main().catch((error) => {
console.error(error?.message || error);
process.exitCode = 1;
});
}