#!/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)); });