feat: add generator-runner.js for cargo subprocess + SSE streaming
This commit is contained in:
174
frontend/scene-generator/generator-runner.js
Normal file
174
frontend/scene-generator/generator-runner.js
Normal file
@@ -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 };
|
||||
Reference in New Issue
Block a user