const { spawn } = require("child_process"); const path = require("path"); function runGenerator(params, sseWriter, projectRoot) { const { sourceDir, sceneId, sceneName, sceneKind, targetUrl, outputRoot, lessons, sceneInfoJson } = params; const normalize = (p) => p.replace(/\\/g, "/"); const args = [ "run", "--bin", "sg_scene_generate", "--", "--source-dir", normalize(sourceDir), "--scene-id", sceneId, "--scene-name", sceneName, ]; // 只有明确指定 sceneKind 时才添加参数(否则使用默认值 report_collection) if (sceneKind) { args.push("--scene-kind", sceneKind); } // 如果提供了 targetUrl,添加参数 if (targetUrl) { args.push("--target-url", targetUrl); } args.push("--output-root", normalize(outputRoot)); if (lessons) { args.push("--lessons", normalize(lessons)); } // Pass scene info JSON if available if (sceneInfoJson) { args.push("--scene-info-json", sceneInfoJson); } 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"); } // Read index.html const indexHtmlPath = p.join(sourceDir, "index.html"); if (fs.existsSync(indexHtmlPath)) { result.indexHtml = fs.readFileSync(indexHtmlPath, "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 };