feat: add folder picker and target_url input to Web UI
- Add /select-folder and /select-file APIs using PowerShell dialogs - Add --target-url parameter to CLI for explicit target URL override - Redesign Web UI with folder browse buttons for all path inputs - Add target_url optional input field for specifying target page URL - Auto-fill scene-id from selected folder name 🤖 Generated with [Qoder][https://qoder.com]
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
const http = require("http");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { spawn } = require("child_process");
|
||||
const { loadConfig, getDefaults } = require("./config-loader");
|
||||
const { analyzeScene } = require("./llm-client");
|
||||
const { runGenerator, readDirectory } = require("./generator-runner");
|
||||
@@ -126,7 +127,7 @@ async function handleGenerate(req, res) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { sourceDir, sceneId, sceneName, sceneKind, outputRoot, lessons } = body;
|
||||
const { sourceDir, sceneId, sceneName, sceneKind, targetUrl, outputRoot, lessons } = body;
|
||||
if (!sourceDir || !sceneId || !sceneName || !outputRoot || !lessons) {
|
||||
res.writeHead(400, { "Content-Type": "application/json" });
|
||||
res.end(
|
||||
@@ -142,7 +143,7 @@ async function handleGenerate(req, res) {
|
||||
|
||||
try {
|
||||
await runGenerator(
|
||||
{ sourceDir, sceneId, sceneName, sceneKind, outputRoot, lessons },
|
||||
{ sourceDir, sceneId, sceneName, sceneKind, targetUrl, outputRoot, lessons },
|
||||
sseWriter,
|
||||
config.projectRoot
|
||||
);
|
||||
@@ -166,6 +167,116 @@ function handleHealth(req, res) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a native Windows folder selection dialog using PowerShell.
|
||||
* Returns the selected folder path or null if cancelled.
|
||||
*/
|
||||
function openFolderDialog(defaultPath) {
|
||||
return new Promise((resolve) => {
|
||||
const psScript = `
|
||||
Add-Type -AssemblyName System.Windows.Forms
|
||||
$dialog = New-Object System.Windows.Forms.FolderBrowserDialog
|
||||
$dialog.Description = "选择文件夹"
|
||||
$dialog.ShowNewFolderButton = true
|
||||
${defaultPath ? `$dialog.SelectedPath = '${defaultPath.replace(/'/g, "''")}'` : ""}
|
||||
if ($dialog.ShowDialog() -eq 'OK') {
|
||||
Write-Output $dialog.SelectedPath
|
||||
}
|
||||
`.trim();
|
||||
|
||||
const ps = spawn("powershell.exe", [
|
||||
"-NoProfile",
|
||||
"-NonInteractive",
|
||||
"-Command",
|
||||
psScript,
|
||||
]);
|
||||
|
||||
let output = "";
|
||||
let error = "";
|
||||
|
||||
ps.stdout.on("data", (data) => {
|
||||
output += data.toString();
|
||||
});
|
||||
|
||||
ps.stderr.on("data", (data) => {
|
||||
error += data.toString();
|
||||
});
|
||||
|
||||
ps.on("close", (code) => {
|
||||
if (code === 0 && output.trim()) {
|
||||
resolve(output.trim());
|
||||
} else {
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
|
||||
ps.on("error", () => {
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function handleSelectFolder(req, res) {
|
||||
let body = {};
|
||||
try {
|
||||
body = await parseBody(req);
|
||||
} catch {
|
||||
// ignore parse error, use empty body
|
||||
}
|
||||
|
||||
const selectedPath = await openFolderDialog(body.defaultPath || "");
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({ path: selectedPath }));
|
||||
}
|
||||
|
||||
async function handleSelectFile(req, res) {
|
||||
let body = {};
|
||||
try {
|
||||
body = await parseBody(req);
|
||||
} catch {
|
||||
// ignore parse error
|
||||
}
|
||||
|
||||
const filter = body.filter || "所有文件 (*.*)|*.*";
|
||||
const psScript = `
|
||||
Add-Type -AssemblyName System.Windows.Forms
|
||||
$dialog = New-Object System.Windows.Forms.OpenFileDialog
|
||||
$dialog.Filter = '${filter}'
|
||||
$dialog.Title = "选择文件"
|
||||
${body.defaultPath ? `$dialog.InitialDirectory = '${body.defaultPath.replace(/'/g, "''")}'` : ""}
|
||||
if ($dialog.ShowDialog() -eq 'OK') {
|
||||
Write-Output $dialog.FileName
|
||||
}
|
||||
`.trim();
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const ps = spawn("powershell.exe", [
|
||||
"-NoProfile",
|
||||
"-NonInteractive",
|
||||
"-Command",
|
||||
psScript,
|
||||
]);
|
||||
|
||||
let output = "";
|
||||
|
||||
ps.stdout.on("data", (data) => {
|
||||
output += data.toString();
|
||||
});
|
||||
|
||||
ps.on("close", (code) => {
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({ path: code === 0 && output.trim() ? output.trim() : null }));
|
||||
resolve();
|
||||
});
|
||||
|
||||
ps.on("error", () => {
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({ path: null }));
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const server = http.createServer(async (req, res) => {
|
||||
const url = new URL(req.url, `http://${req.headers.host}`);
|
||||
const pathname = url.pathname;
|
||||
@@ -187,6 +298,10 @@ const server = http.createServer(async (req, res) => {
|
||||
await handleAnalyze(req, res);
|
||||
} else if (pathname === "/generate" && req.method === "POST") {
|
||||
await handleGenerate(req, res);
|
||||
} else if (pathname === "/select-folder" && req.method === "POST") {
|
||||
await handleSelectFolder(req, res);
|
||||
} else if (pathname === "/select-file" && req.method === "POST") {
|
||||
await handleSelectFile(req, res);
|
||||
} else if (pathname === "/" || pathname === "/index.html") {
|
||||
serveStatic(res, path.join(__dirname, "sg_scene_generator.html"));
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user