From e7a41795130fc5ab73c953e688176323dcbfaf79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=A8=E7=82=8E?= <635735027@qq.com> Date: Thu, 16 Apr 2026 22:18:32 +0800 Subject: [PATCH] feat: add server.js with /health, /analyze, /generate routes --- frontend/scene-generator/server.js | 226 +++++++++++++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100644 frontend/scene-generator/server.js diff --git a/frontend/scene-generator/server.js b/frontend/scene-generator/server.js new file mode 100644 index 0000000..3d812a0 --- /dev/null +++ b/frontend/scene-generator/server.js @@ -0,0 +1,226 @@ +#!/usr/bin/env node +"use strict"; + +const http = require("http"); +const fs = require("fs"); +const path = require("path"); +const { loadConfig, getDefaults } = require("./config-loader"); +const { analyzeScene } = require("./llm-client"); +const { runGenerator, readDirectory } = require("./generator-runner"); + +let config; +let defaults; +try { + config = loadConfig(); + defaults = getDefaults(); + console.log(`[config] Loaded from: ${config.configPath}`); + console.log(`[config] Project root: ${config.projectRoot}`); +} catch (err) { + console.error(`[error] Failed to load config: ${err.message}`); + process.exit(1); +} + +const PORT = parseInt(process.env.SG_SCENE_GENERATOR_PORT, 10) || 3210; +const HOST = "127.0.0.1"; + +const MIME_TYPES = { + ".html": "text/html; charset=utf-8", + ".js": "application/javascript", + ".css": "text/css", + ".json": "application/json", +}; + +function serveStatic(res, filePath) { + const ext = path.extname(filePath); + const contentType = MIME_TYPES[ext] || "application/octet-stream"; + + fs.readFile(filePath, (err, data) => { + if (err) { + res.writeHead(404); + res.end("Not found"); + return; + } + res.writeHead(200, { "Content-Type": contentType }); + res.end(data); + }); +} + +function initSSE(res) { + res.writeHead(200, { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", + "Access-Control-Allow-Origin": "*", + }); + res.write(":\n"); + return res; +} + +function writeSSE(res, event, data) { + res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`); +} + +function parseBody(req) { + return new Promise((resolve, reject) => { + let body = ""; + req.on("data", (chunk) => (body += chunk)); + req.on("end", () => { + try { + resolve(JSON.parse(body)); + } catch (err) { + reject(new Error("Invalid JSON")); + } + }); + req.on("error", reject); + }); +} + +async function handleAnalyze(req, res) { + let body; + try { + body = await parseBody(req); + } catch { + res.writeHead(400, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: "Invalid JSON body" })); + return; + } + + const sourceDir = (body.sourceDir || "").replace(/\\/g, "/"); + if (!sourceDir) { + res.writeHead(400, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: "sourceDir is required" })); + return; + } + + let dirContents; + try { + dirContents = readDirectory(sourceDir); + } catch (err) { + res.writeHead(400, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: err.message })); + return; + } + + try { + const result = await analyzeScene(sourceDir, dirContents, config); + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify(result)); + } catch (err) { + res.writeHead(502, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + error: `LLM analysis failed: ${err.message}`, + hint: "You can still enter scene-id and scene-name manually", + }) + ); + } +} + +async function handleGenerate(req, res) { + let body; + try { + body = await parseBody(req); + } catch { + res.writeHead(400, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: "Invalid JSON body" })); + return; + } + + const { sourceDir, sceneId, sceneName, outputRoot, lessons } = body; + if (!sourceDir || !sceneId || !sceneName || !outputRoot || !lessons) { + res.writeHead(400, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + error: + "All fields required: sourceDir, sceneId, sceneName, outputRoot, lessons", + }) + ); + return; + } + + const sseWriter = initSSE(res); + + try { + await runGenerator( + { sourceDir, sceneId, sceneName, outputRoot, lessons }, + sseWriter, + config.projectRoot + ); + } catch (err) { + writeSSE(sseWriter, "error", { message: `Server error: ${err.message}` }); + } + + sseWriter.end(); +} + +function handleHealth(req, res) { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + status: "ok", + pid: process.pid, + configLoaded: true, + configPath: config.configPath, + projectRoot: config.projectRoot, + }) + ); +} + +const server = http.createServer(async (req, res) => { + const url = new URL(req.url, `http://${req.headers.host}`); + const pathname = url.pathname; + + if (req.method === "OPTIONS") { + res.writeHead(204, { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type", + }); + res.end(); + return; + } + + try { + if (pathname === "/health" && req.method === "GET") { + handleHealth(req, res); + } else if (pathname === "/analyze" && req.method === "POST") { + await handleAnalyze(req, res); + } else if (pathname === "/generate" && req.method === "POST") { + await handleGenerate(req, res); + } else if (pathname === "/" || pathname === "/index.html") { + serveStatic(res, path.join(__dirname, "sg_scene_generator.html")); + } else { + const filePath = path.join(__dirname, pathname); + if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) { + serveStatic(res, filePath); + } else { + res.writeHead(404, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: "Not found" })); + } + } + } catch (err) { + console.error(`[error] ${req.method} ${pathname}: ${err.message}`); + if (!res.headersSent) { + res.writeHead(500, { "Content-Type": "application/json" }); + } + res.end(JSON.stringify({ error: err.message })); + } +}); + +server.listen(PORT, HOST, () => { + console.log(""); + console.log(" ╔══════════════════════════════════════════════════╗"); + console.log(" ║ sgClaw · Scene Skill Generator ║"); + console.log(" ╠══════════════════════════════════════════════════╣"); + console.log(" ║ ║"); + console.log(` ║ 访问地址: http://${HOST}:${PORT}/ ║`); + console.log(" ║ ║"); + console.log(" ║ 按 Ctrl+C 停止服务 ║"); + console.log(" ╚══════════════════════════════════════════════════╝"); + console.log(""); +}); + +process.on("SIGINT", () => { + console.log("\n[info] Shutting down..."); + server.close(() => process.exit(0)); +});