diff --git a/frontend/scene-generator/generator-runner.js b/frontend/scene-generator/generator-runner.js new file mode 100644 index 0000000..730a491 --- /dev/null +++ b/frontend/scene-generator/generator-runner.js @@ -0,0 +1,174 @@ +const { spawn } = require("child_process"); +const path = require("path"); + +function runGenerator(params, sseWriter, projectRoot) { + const { sourceDir, sceneId, sceneName, outputRoot, lessons } = params; + + const normalize = (p) => p.replace(/\\/g, "/"); + + const args = [ + "run", + "--bin", + "sg_scene_generate", + "--", + "--source-dir", + normalize(sourceDir), + "--scene-id", + sceneId, + "--scene-name", + sceneName, + "--output-root", + normalize(outputRoot), + "--lessons", + normalize(lessons), + ]; + + return new Promise((resolve, reject) => { + sseWriter.write( + `event: status\ndata: ${JSON.stringify({ + message: "开始生成 skill 包...", + })}\n\n` + ); + sseWriter.write( + `event: status\ndata: ${JSON.stringify({ + message: `执行: cargo ${args.join(" ")}`, + })}\n\n` + ); + + const child = spawn("cargo", args, { + cwd: projectRoot, + stdio: ["ignore", "pipe", "pipe"], + env: { ...process.env, RUST_BACKTRACE: "1" }, + }); + + let stdout = ""; + let stderr = ""; + let timedOut = false; + + const timeout = setTimeout(() => { + timedOut = true; + child.kill("SIGTERM"); + sseWriter.write( + `event: error\ndata: ${JSON.stringify({ + message: "生成超时(5分钟)", + })}\n\n` + ); + resolve({ success: false, error: "timeout" }); + }, 5 * 60 * 1000); + + child.stdout.on("data", (data) => { + const text = data.toString(); + stdout += text; + sseWriter.write( + `event: log\ndata: ${JSON.stringify({ message: text.trim() })}\n\n` + ); + }); + + child.stderr.on("data", (data) => { + const text = data.toString(); + stderr += text; + sseWriter.write( + `event: log\ndata: ${JSON.stringify({ message: text.trim() })}\n\n` + ); + }); + + child.on("close", (code) => { + clearTimeout(timeout); + if (timedOut) return; + + if (code === 0) { + const match = stdout.match(/generated scene package:\s*(.+)/); + const skillRoot = match ? match[1] : null; + sseWriter.write( + `event: status\ndata: ${JSON.stringify({ + message: "✅ 生成成功", + })}\n\n` + ); + sseWriter.write( + `event: complete\ndata: ${JSON.stringify({ + success: true, + skillRoot, + })}\n\n` + ); + resolve({ success: true, skillRoot }); + } else { + sseWriter.write( + `event: error\ndata: ${JSON.stringify({ + message: `生成失败 (exit code ${code})`, + })}\n\n` + ); + if (stderr.trim()) { + sseWriter.write( + `event: error\ndata: ${JSON.stringify({ + message: stderr.substring(0, 500), + })}\n\n` + ); + } + resolve({ success: false, code, stderr }); + } + }); + + child.on("error", (err) => { + clearTimeout(timeout); + sseWriter.write( + `event: error\ndata: ${JSON.stringify({ + message: `无法启动 cargo: ${err.message}`, + })}\n\n` + ); + reject(err); + }); + }); +} + +function readDirectory(sourceDir) { + const fs = require("fs"); + const p = require("path"); + + if (!fs.existsSync(sourceDir)) { + throw new Error(`Directory not found: ${sourceDir}`); + } + + const stat = fs.statSync(sourceDir); + if (!stat.isDirectory()) { + throw new Error(`Not a directory: ${sourceDir}`); + } + + const result = {}; + const entries = fs.readdirSync(sourceDir, { withFileTypes: true }); + + const treeLines = []; + for (const entry of entries) { + treeLines.push(`├── ${entry.name}`); + } + result.tree = treeLines.join("\n"); + + const sceneTomlPath = p.join(sourceDir, "scene.toml"); + if (fs.existsSync(sceneTomlPath)) { + result["scene.toml"] = fs.readFileSync(sceneTomlPath, "utf-8"); + } + + const skillTomlPath = p.join(sourceDir, "SKILL.toml"); + if (fs.existsSync(skillTomlPath)) { + result["SKILL.toml"] = fs.readFileSync(skillTomlPath, "utf-8"); + } + + const skillMdPath = p.join(sourceDir, "SKILL.md"); + if (fs.existsSync(skillMdPath)) { + result["SKILL.md"] = fs.readFileSync(skillMdPath, "utf-8"); + } + + const scripts = {}; + for (const entry of entries) { + if (entry.isFile() && entry.name.endsWith(".js")) { + const scriptPath = p.join(sourceDir, entry.name); + scripts[entry.name] = fs.readFileSync(scriptPath, "utf-8"); + } + } + if (Object.keys(scripts).length > 0) { + result.scripts = scripts; + } + + return result; +} + +module.exports = { runGenerator, readDirectory };