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:
木炎
2026-04-17 00:23:09 +08:00
parent ce072c2ebe
commit f268668713
6 changed files with 252 additions and 118 deletions

View File

@@ -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 {