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:
@@ -2,7 +2,7 @@ const { spawn } = require("child_process");
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
function runGenerator(params, sseWriter, projectRoot) {
|
function runGenerator(params, sseWriter, projectRoot) {
|
||||||
const { sourceDir, sceneId, sceneName, sceneKind, outputRoot, lessons } = params;
|
const { sourceDir, sceneId, sceneName, sceneKind, targetUrl, outputRoot, lessons } = params;
|
||||||
|
|
||||||
const normalize = (p) => p.replace(/\\/g, "/");
|
const normalize = (p) => p.replace(/\\/g, "/");
|
||||||
|
|
||||||
@@ -24,6 +24,11 @@ function runGenerator(params, sseWriter, projectRoot) {
|
|||||||
args.push("--scene-kind", sceneKind);
|
args.push("--scene-kind", sceneKind);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果提供了 targetUrl,添加参数
|
||||||
|
if (targetUrl) {
|
||||||
|
args.push("--target-url", targetUrl);
|
||||||
|
}
|
||||||
|
|
||||||
args.push(
|
args.push(
|
||||||
"--output-root",
|
"--output-root",
|
||||||
normalize(outputRoot),
|
normalize(outputRoot),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
const http = require("http");
|
const http = require("http");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
const { spawn } = require("child_process");
|
||||||
const { loadConfig, getDefaults } = require("./config-loader");
|
const { loadConfig, getDefaults } = require("./config-loader");
|
||||||
const { analyzeScene } = require("./llm-client");
|
const { analyzeScene } = require("./llm-client");
|
||||||
const { runGenerator, readDirectory } = require("./generator-runner");
|
const { runGenerator, readDirectory } = require("./generator-runner");
|
||||||
@@ -126,7 +127,7 @@ async function handleGenerate(req, res) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { sourceDir, sceneId, sceneName, sceneKind, outputRoot, lessons } = body;
|
const { sourceDir, sceneId, sceneName, sceneKind, targetUrl, outputRoot, lessons } = body;
|
||||||
if (!sourceDir || !sceneId || !sceneName || !outputRoot || !lessons) {
|
if (!sourceDir || !sceneId || !sceneName || !outputRoot || !lessons) {
|
||||||
res.writeHead(400, { "Content-Type": "application/json" });
|
res.writeHead(400, { "Content-Type": "application/json" });
|
||||||
res.end(
|
res.end(
|
||||||
@@ -142,7 +143,7 @@ async function handleGenerate(req, res) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await runGenerator(
|
await runGenerator(
|
||||||
{ sourceDir, sceneId, sceneName, sceneKind, outputRoot, lessons },
|
{ sourceDir, sceneId, sceneName, sceneKind, targetUrl, outputRoot, lessons },
|
||||||
sseWriter,
|
sseWriter,
|
||||||
config.projectRoot
|
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 server = http.createServer(async (req, res) => {
|
||||||
const url = new URL(req.url, `http://${req.headers.host}`);
|
const url = new URL(req.url, `http://${req.headers.host}`);
|
||||||
const pathname = url.pathname;
|
const pathname = url.pathname;
|
||||||
@@ -187,6 +298,10 @@ const server = http.createServer(async (req, res) => {
|
|||||||
await handleAnalyze(req, res);
|
await handleAnalyze(req, res);
|
||||||
} else if (pathname === "/generate" && req.method === "POST") {
|
} else if (pathname === "/generate" && req.method === "POST") {
|
||||||
await handleGenerate(req, res);
|
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") {
|
} else if (pathname === "/" || pathname === "/index.html") {
|
||||||
serveStatic(res, path.join(__dirname, "sg_scene_generator.html"));
|
serveStatic(res, path.join(__dirname, "sg_scene_generator.html"));
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -48,15 +48,15 @@
|
|||||||
}
|
}
|
||||||
.hero h1 { margin: 0; font-size: clamp(1.8rem, 4vw, 2.6rem); line-height: 1.05; letter-spacing: 0.02em; }
|
.hero h1 { margin: 0; font-size: clamp(1.8rem, 4vw, 2.6rem); line-height: 1.05; letter-spacing: 0.02em; }
|
||||||
.hero p { margin: 10px 0 0; max-width: 60ch; color: var(--muted); line-height: 1.6; }
|
.hero p { margin: 10px 0 0; max-width: 60ch; color: var(--muted); line-height: 1.6; }
|
||||||
.content { display: grid; grid-template-columns: minmax(280px, 320px) minmax(0, 1fr); gap: 0; }
|
.content { display: grid; grid-template-columns: minmax(280px, 360px) minmax(0, 1fr); gap: 0; }
|
||||||
.sidebar, .stream-panel { padding: 24px; }
|
.sidebar, .stream-panel { padding: 24px; }
|
||||||
.sidebar { border-right: 1px solid var(--line); background: rgba(255, 255, 255, 0.38); }
|
.sidebar { border-right: 1px solid var(--line); background: rgba(255, 255, 255, 0.38); }
|
||||||
.section-label { margin: 0 0 14px; font-size: 0.83rem; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; color: var(--muted); }
|
.section-label { margin: 0 0 14px; font-size: 0.83rem; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; color: var(--muted); }
|
||||||
.field { margin-bottom: 18px; }
|
.field { margin-bottom: 16px; }
|
||||||
.field label { display: block; margin-bottom: 8px; font-size: 0.92rem; color: var(--muted); }
|
.field label { display: block; margin-bottom: 6px; font-size: 0.92rem; color: var(--muted); }
|
||||||
.input-row { display: flex; gap: 8px; }
|
.input-row { display: flex; gap: 8px; }
|
||||||
.input-row input { flex: 1; }
|
.input-row input { flex: 1; }
|
||||||
.input-row button { width: auto; min-width: 80px; }
|
.input-row .browse-btn { width: auto; min-width: 60px; padding: 10px 14px; font-size: 0.85rem; }
|
||||||
input, button {
|
input, button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
input {
|
input {
|
||||||
background: rgba(255, 255, 255, 0.92);
|
background: rgba(255, 255, 255, 0.92);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
padding: 14px 16px;
|
padding: 12px 14px;
|
||||||
outline: none;
|
outline: none;
|
||||||
transition: border-color 140ms ease, box-shadow 140ms ease;
|
transition: border-color 140ms ease, box-shadow 140ms ease;
|
||||||
}
|
}
|
||||||
@@ -77,26 +77,26 @@
|
|||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
background: rgba(255, 255, 255, 0.92);
|
background: rgba(255, 255, 255, 0.92);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
padding: 14px 16px;
|
padding: 12px 14px;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
outline: none;
|
outline: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: border-color 140ms ease, box-shadow 140ms ease;
|
transition: border-color 140ms ease, box-shadow 140ms ease;
|
||||||
}
|
}
|
||||||
select:focus { border-color: rgba(15, 118, 110, 0.5); box-shadow: 0 0 0 4px rgba(15, 118, 110, 0.12); }
|
select:focus { border-color: rgba(15, 118, 110, 0.5); box-shadow: 0 0 0 4px rgba(15, 118, 110, 0.12); }
|
||||||
button { border: 0; padding: 14px 16px; font-weight: 700; cursor: pointer; transition: transform 140ms ease, opacity 140ms ease; }
|
button { border: 0; padding: 12px 14px; font-weight: 700; cursor: pointer; transition: transform 140ms ease, opacity 140ms ease; }
|
||||||
button:hover:not(:disabled) { transform: translateY(-1px); }
|
button:hover:not(:disabled) { transform: translateY(-1px); }
|
||||||
button:disabled { cursor: not-allowed; opacity: 0.45; }
|
button:disabled { cursor: not-allowed; opacity: 0.45; }
|
||||||
.primary-btn { background: linear-gradient(135deg, var(--accent), var(--accent-strong)); color: #f6fffd; box-shadow: 0 14px 28px rgba(15, 118, 110, 0.18); }
|
.primary-btn { background: linear-gradient(135deg, var(--accent), var(--accent-strong)); color: #f6fffd; box-shadow: 0 14px 28px rgba(15, 118, 110, 0.18); }
|
||||||
.ghost-btn { background: rgba(255, 255, 255, 0.9); color: var(--text); border: 1px solid var(--line); }
|
.ghost-btn { background: rgba(255, 255, 255, 0.9); color: var(--text); border: 1px solid var(--line); }
|
||||||
.status-card { display: grid; gap: 8px; padding: 16px; border-radius: 20px; background: var(--panel-strong); border: 1px solid var(--line); margin-bottom: 18px; }
|
.status-card { display: grid; gap: 8px; padding: 14px; border-radius: 20px; background: var(--panel-strong); border: 1px solid var(--line); margin-bottom: 16px; }
|
||||||
.state-chip { display: inline-flex; align-items: center; width: fit-content; padding: 6px 10px; border-radius: 999px; font-size: 0.85rem; font-weight: 700; background: rgba(99, 107, 116, 0.12); color: var(--muted); }
|
.state-chip { display: inline-flex; align-items: center; width: fit-content; padding: 5px 10px; border-radius: 999px; font-size: 0.85rem; font-weight: 700; background: rgba(99, 107, 116, 0.12); color: var(--muted); }
|
||||||
.state-chip[data-state="ready"] { background: rgba(99, 107, 116, 0.12); color: var(--muted); }
|
.state-chip[data-state="ready"] { background: rgba(99, 107, 116, 0.12); color: var(--muted); }
|
||||||
.state-chip[data-state="analyzing"] { background: rgba(180, 83, 9, 0.12); color: var(--warn); }
|
.state-chip[data-state="analyzing"] { background: rgba(180, 83, 9, 0.12); color: var(--warn); }
|
||||||
.state-chip[data-state="generating"] { background: rgba(15, 118, 110, 0.12); color: var(--accent); }
|
.state-chip[data-state="generating"] { background: rgba(15, 118, 110, 0.12); color: var(--accent); }
|
||||||
.state-chip[data-state="complete"] { background: rgba(22, 101, 52, 0.12); color: var(--success); }
|
.state-chip[data-state="complete"] { background: rgba(22, 101, 52, 0.12); color: var(--success); }
|
||||||
.state-chip[data-state="error"] { background: rgba(180, 35, 24, 0.12); color: var(--error); }
|
.state-chip[data-state="error"] { background: rgba(180, 35, 24, 0.12); color: var(--error); }
|
||||||
.validation { min-height: 1.4em; margin: 10px 0 14px; color: var(--error); font-size: 0.92rem; }
|
.validation { min-height: 1.4em; margin: 8px 0 12px; color: var(--error); font-size: 0.92rem; }
|
||||||
.stream-panel { display: grid; grid-template-rows: auto minmax(320px, 1fr); gap: 18px; }
|
.stream-panel { display: grid; grid-template-rows: auto minmax(320px, 1fr); gap: 18px; }
|
||||||
.stream-head { display: flex; justify-content: space-between; align-items: end; gap: 16px; }
|
.stream-head { display: flex; justify-content: space-between; align-items: end; gap: 16px; }
|
||||||
.stream-head h2 { margin: 0; font-size: 1.35rem; }
|
.stream-head h2 { margin: 0; font-size: 1.35rem; }
|
||||||
@@ -111,7 +111,8 @@
|
|||||||
.row.error .row-badge { background: rgba(180, 35, 24, 0.14); color: var(--error); }
|
.row.error .row-badge { background: rgba(180, 35, 24, 0.14); color: var(--error); }
|
||||||
.row-text { margin: 0; line-height: 1.6; white-space: pre-wrap; word-break: break-word; }
|
.row-text { margin: 0; line-height: 1.6; white-space: pre-wrap; word-break: break-word; }
|
||||||
@keyframes rise { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
|
@keyframes rise { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
|
||||||
.hint { font-size: 0.82rem; color: var(--muted); margin-top: 4px; }
|
.hint { font-size: 0.8rem; color: var(--muted); margin-top: 4px; }
|
||||||
|
.divider { height: 1px; background: var(--line); margin: 12px 0; }
|
||||||
@media (max-width: 900px) { body { padding: 16px; } .content { grid-template-columns: 1fr; } .sidebar { border-right: 0; border-bottom: 1px solid var(--line); } .stream { max-height: none; } }
|
@media (max-width: 900px) { body { padding: 16px; } .content { grid-template-columns: 1fr; } .sidebar { border-right: 0; border-bottom: 1px solid var(--line); } .stream { max-height: none; } }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
@@ -119,23 +120,22 @@
|
|||||||
<div class="shell">
|
<div class="shell">
|
||||||
<div class="hero">
|
<div class="hero">
|
||||||
<h1>场景 Skill 生成器</h1>
|
<h1>场景 Skill 生成器</h1>
|
||||||
<p>输入场景目录路径,自动提取 scene-id 和 scene-name,一键生成 skill 包并实时查看进度。</p>
|
<p>选择场景目录,配置参数,一键生成 skill 包。</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="sidebar">
|
<div class="sidebar">
|
||||||
<p class="section-label">Status</p>
|
<p class="section-label">Status</p>
|
||||||
<div class="status-card">
|
<div class="status-card">
|
||||||
<span id="stateChip" class="state-chip" data-state="ready">就绪</span>
|
<span id="stateChip" class="state-chip" data-state="ready">就绪</span>
|
||||||
<span id="statusText">请输入场景目录路径</span>
|
<span id="statusText">请选择场景目录</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="section-label">Source</p>
|
<p class="section-label">Source</p>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="sourceDir">📂 场景目录路径</label>
|
<label>场景目录路径</label>
|
||||||
<div class="input-row">
|
<div class="input-row">
|
||||||
<input id="sourceDir" placeholder="例如:D:\data\ideaSpace\rust\sgClaw\claw-new\examples\generated_scene_platform\scenarios\tq-lineloss-report" />
|
<input id="sourceDir" placeholder="点击浏览选择目录..." readonly />
|
||||||
<button id="analyzeBtn" class="ghost-btn">分析</button>
|
<button id="browseSourceDir" class="ghost-btn browse-btn">浏览</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="hint">输入路径后点击"分析"或按回车,自动提取 scene-id 和 scene-name</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="sceneId">scene-id</label>
|
<label for="sceneId">scene-id</label>
|
||||||
@@ -152,16 +152,36 @@
|
|||||||
<option value="monitoring">监测类</option>
|
<option value="monitoring">监测类</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<button id="settingsBtn" class="ghost-btn" style="margin-bottom: 12px;">⚙ 设置</button>
|
<div class="field">
|
||||||
|
<label for="targetUrl">目标 URL (可选)</label>
|
||||||
|
<input id="targetUrl" placeholder="例如:http://20.76.57.61:18080/report" />
|
||||||
|
<p class="hint">场景要访问的目标页面地址,留空则使用自动提取的域名</p>
|
||||||
|
</div>
|
||||||
|
<div class="divider"></div>
|
||||||
|
<p class="section-label">Settings</p>
|
||||||
|
<div class="field">
|
||||||
|
<label>输出根路径</label>
|
||||||
|
<div class="input-row">
|
||||||
|
<input id="settingOutputRoot" placeholder="点击浏览选择目录..." readonly />
|
||||||
|
<button id="browseOutputRoot" class="ghost-btn browse-btn">浏览</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>Lessons 路径</label>
|
||||||
|
<div class="input-row">
|
||||||
|
<input id="settingLessons" placeholder="点击浏览选择文件..." readonly />
|
||||||
|
<button id="browseLessons" class="ghost-btn browse-btn">浏览</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div id="validationText" class="validation"></div>
|
<div id="validationText" class="validation"></div>
|
||||||
<button id="generateBtn" class="primary-btn" disabled>🚀 生成 Skill</button>
|
<button id="generateBtn" class="primary-btn" disabled>生成 Skill</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="stream-panel">
|
<div class="stream-panel">
|
||||||
<div class="stream-head">
|
<div class="stream-head">
|
||||||
<div>
|
<div>
|
||||||
<p class="section-label">Generation Log</p>
|
<p class="section-label">Generation Log</p>
|
||||||
<h2>实时日志</h2>
|
<h2>实时日志</h2>
|
||||||
<p>显示分析和生成过程的完整输出</p>
|
<p>显示生成过程的完整输出</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="messageStream" class="stream">
|
<div id="messageStream" class="stream">
|
||||||
@@ -171,33 +191,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="settingsModal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000; align-items: center; justify-content: center;">
|
|
||||||
<div style="background: var(--panel); border-radius: 20px; padding: 28px; width: min(520px, 90%); max-height: 85vh; overflow-y: auto; box-shadow: var(--shadow);">
|
|
||||||
<h3 style="margin: 0 0 20px; font-size: 1.2rem;">生成器设置</h3>
|
|
||||||
<div class="field">
|
|
||||||
<label for="settingOutputRoot">输出根路径</label>
|
|
||||||
<input id="settingOutputRoot" type="text" />
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label for="settingLessons">Lessons 路径</label>
|
|
||||||
<input id="settingLessons" type="text" />
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label for="settingLlmBaseUrl">LLM 服务地址</label>
|
|
||||||
<input id="settingLlmBaseUrl" type="text" />
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label for="settingLlmModel">LLM 模型</label>
|
|
||||||
<input id="settingLlmModel" type="text" />
|
|
||||||
</div>
|
|
||||||
<div id="settingsValidation" style="color: var(--error); font-size: 0.92rem; min-height: 1.4em; margin: 10px 0;"></div>
|
|
||||||
<div style="display: flex; gap: 12px; margin-top: 16px;">
|
|
||||||
<button id="settingsSaveBtn" class="primary-btn" style="flex: 1;">保存</button>
|
|
||||||
<button id="settingsCancelBtn" class="ghost-btn" style="flex: 1;">取消</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const SERVER_URL = "http://127.0.0.1:3210";
|
const SERVER_URL = "http://127.0.0.1:3210";
|
||||||
const els = {
|
const els = {
|
||||||
@@ -205,22 +198,18 @@
|
|||||||
sceneId: document.getElementById("sceneId"),
|
sceneId: document.getElementById("sceneId"),
|
||||||
sceneName: document.getElementById("sceneName"),
|
sceneName: document.getElementById("sceneName"),
|
||||||
sceneKind: document.getElementById("sceneKind"),
|
sceneKind: document.getElementById("sceneKind"),
|
||||||
analyzeBtn: document.getElementById("analyzeBtn"),
|
targetUrl: document.getElementById("targetUrl"),
|
||||||
|
browseSourceDir: document.getElementById("browseSourceDir"),
|
||||||
|
browseOutputRoot: document.getElementById("browseOutputRoot"),
|
||||||
|
browseLessons: document.getElementById("browseLessons"),
|
||||||
|
settingOutputRoot: document.getElementById("settingOutputRoot"),
|
||||||
|
settingLessons: document.getElementById("settingLessons"),
|
||||||
generateBtn: document.getElementById("generateBtn"),
|
generateBtn: document.getElementById("generateBtn"),
|
||||||
settingsBtn: document.getElementById("settingsBtn"),
|
|
||||||
validationText: document.getElementById("validationText"),
|
validationText: document.getElementById("validationText"),
|
||||||
stateChip: document.getElementById("stateChip"),
|
stateChip: document.getElementById("stateChip"),
|
||||||
statusText: document.getElementById("statusText"),
|
statusText: document.getElementById("statusText"),
|
||||||
messageStream: document.getElementById("messageStream"),
|
messageStream: document.getElementById("messageStream"),
|
||||||
emptyState: document.getElementById("emptyState"),
|
emptyState: document.getElementById("emptyState"),
|
||||||
settingsModal: document.getElementById("settingsModal"),
|
|
||||||
settingOutputRoot: document.getElementById("settingOutputRoot"),
|
|
||||||
settingLessons: document.getElementById("settingLessons"),
|
|
||||||
settingLlmBaseUrl: document.getElementById("settingLlmBaseUrl"),
|
|
||||||
settingLlmModel: document.getElementById("settingLlmModel"),
|
|
||||||
settingsValidation: document.getElementById("settingsValidation"),
|
|
||||||
settingsSaveBtn: document.getElementById("settingsSaveBtn"),
|
|
||||||
settingsCancelBtn: document.getElementById("settingsCancelBtn"),
|
|
||||||
};
|
};
|
||||||
let defaultsLoaded = false;
|
let defaultsLoaded = false;
|
||||||
|
|
||||||
@@ -253,6 +242,36 @@
|
|||||||
els.messageStream.scrollTop = els.messageStream.scrollHeight;
|
els.messageStream.scrollTop = els.messageStream.scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function selectFolder(defaultPath) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${SERVER_URL}/select-folder`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ defaultPath }),
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
return data.path;
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to select folder:", err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selectFile(defaultPath, filter) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${SERVER_URL}/select-file`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ defaultPath, filter }),
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
return data.path;
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to select file:", err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function loadDefaults() {
|
async function loadDefaults() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${SERVER_URL}/health`);
|
const res = await fetch(`${SERVER_URL}/health`);
|
||||||
@@ -264,6 +283,7 @@
|
|||||||
els.settingOutputRoot.value = root + "/examples/generated_scene_platform";
|
els.settingOutputRoot.value = root + "/examples/generated_scene_platform";
|
||||||
els.settingLessons.value = root + "/docs/superpowers/references/tq-lineloss-lessons-learned.toml";
|
els.settingLessons.value = root + "/docs/superpowers/references/tq-lineloss-lessons-learned.toml";
|
||||||
}
|
}
|
||||||
|
updateGenerateBtn();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to load defaults:", err);
|
console.error("Failed to load defaults:", err);
|
||||||
setState("error", "无法连接服务器");
|
setState("error", "无法连接服务器");
|
||||||
@@ -271,55 +291,25 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function analyze() {
|
|
||||||
const sourceDir = els.sourceDir.value.trim();
|
|
||||||
if (!sourceDir) { setValidation("请输入场景目录路径"); return; }
|
|
||||||
setValidation("");
|
|
||||||
setState("analyzing", "正在分析场景目录...");
|
|
||||||
els.analyzeBtn.disabled = true;
|
|
||||||
appendRow("status", `开始分析: ${sourceDir}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await fetch(`${SERVER_URL}/analyze`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ sourceDir }),
|
|
||||||
});
|
|
||||||
const data = await res.json();
|
|
||||||
if (!res.ok) throw new Error(data.error || "Analysis failed");
|
|
||||||
els.sceneId.value = data.sceneId || "";
|
|
||||||
els.sceneName.value = data.sceneName || "";
|
|
||||||
setState("ready", "分析完成");
|
|
||||||
appendRow("complete", `scene-id: ${data.sceneId}, scene-name: ${data.sceneName}`);
|
|
||||||
updateGenerateBtn();
|
|
||||||
} catch (err) {
|
|
||||||
setState("error", "分析失败");
|
|
||||||
appendRow("error", err.message);
|
|
||||||
if (err.message.includes("LLM")) appendRow("status", "你可以手动输入 scene-id 和 scene-name");
|
|
||||||
} finally {
|
|
||||||
els.analyzeBtn.disabled = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function generate() {
|
async function generate() {
|
||||||
const sourceDir = els.sourceDir.value.trim().replace(/\\/g, "/");
|
const sourceDir = els.sourceDir.value.trim().replace(/\\/g, "/");
|
||||||
const sceneId = els.sceneId.value.trim();
|
const sceneId = els.sceneId.value.trim();
|
||||||
const sceneName = els.sceneName.value.trim();
|
const sceneName = els.sceneName.value.trim();
|
||||||
const sceneKind = els.sceneKind.value;
|
const sceneKind = els.sceneKind.value;
|
||||||
|
const targetUrl = els.targetUrl.value.trim();
|
||||||
const outputRoot = els.settingOutputRoot.value.trim().replace(/\\/g, "/");
|
const outputRoot = els.settingOutputRoot.value.trim().replace(/\\/g, "/");
|
||||||
const lessons = els.settingLessons.value.trim().replace(/\\/g, "/");
|
const lessons = els.settingLessons.value.trim().replace(/\\/g, "/");
|
||||||
if (!sourceDir || !sceneId || !sceneName || !outputRoot || !lessons) { setValidation("所有字段均为必填"); return; }
|
if (!sourceDir || !sceneId || !sceneName || !outputRoot || !lessons) { setValidation("所有字段均为必填"); return; }
|
||||||
setValidation("");
|
setValidation("");
|
||||||
setState("generating", "正在生成 skill 包...");
|
setState("generating", "正在生成 skill 包...");
|
||||||
els.generateBtn.disabled = true;
|
els.generateBtn.disabled = true;
|
||||||
els.analyzeBtn.disabled = true;
|
|
||||||
appendRow("status", "开始生成 skill 包...");
|
appendRow("status", "开始生成 skill 包...");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${SERVER_URL}/generate`, {
|
const res = await fetch(`${SERVER_URL}/generate`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ sourceDir, sceneId, sceneName, sceneKind, outputRoot, lessons }),
|
body: JSON.stringify({ sourceDir, sceneId, sceneName, sceneKind, targetUrl: targetUrl || null, outputRoot, lessons }),
|
||||||
});
|
});
|
||||||
if (!res.ok) { const err = await res.json(); throw new Error(err.error || "Generation failed"); }
|
if (!res.ok) { const err = await res.json(); throw new Error(err.error || "Generation failed"); }
|
||||||
|
|
||||||
@@ -345,7 +335,7 @@
|
|||||||
case "status": appendRow("status", data.message); break;
|
case "status": appendRow("status", data.message); break;
|
||||||
case "log": appendRow("log", data.message); break;
|
case "log": appendRow("log", data.message); break;
|
||||||
case "complete":
|
case "complete":
|
||||||
if (data.success) { setState("complete", "生成完成"); appendRow("complete", `✅ 生成完成: ${data.skillRoot || ""}`); }
|
if (data.success) { setState("complete", "生成完成"); appendRow("complete", `生成完成: ${data.skillRoot || ""}`); }
|
||||||
else { setState("error", "生成失败"); appendRow("error", data.message || "生成失败"); }
|
else { setState("error", "生成失败"); appendRow("error", data.message || "生成失败"); }
|
||||||
break;
|
break;
|
||||||
case "error": setState("error", "生成失败"); appendRow("error", data.message); break;
|
case "error": setState("error", "生成失败"); appendRow("error", data.message); break;
|
||||||
@@ -360,32 +350,42 @@
|
|||||||
appendRow("error", err.message);
|
appendRow("error", err.message);
|
||||||
} finally {
|
} finally {
|
||||||
els.generateBtn.disabled = false;
|
els.generateBtn.disabled = false;
|
||||||
els.analyzeBtn.disabled = false;
|
|
||||||
updateGenerateBtn();
|
updateGenerateBtn();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function openSettings() { els.settingsValidation.textContent = ""; els.settingsModal.style.display = "flex"; }
|
// Browse buttons
|
||||||
function closeSettings() { els.settingsModal.style.display = "none"; }
|
els.browseSourceDir.addEventListener("click", async () => {
|
||||||
function saveSettings() {
|
const path = await selectFolder(els.sourceDir.value || null);
|
||||||
const outputRoot = els.settingOutputRoot.value.trim();
|
if (path) {
|
||||||
const lessons = els.settingLessons.value.trim();
|
els.sourceDir.value = path;
|
||||||
if (!outputRoot || !lessons) { els.settingsValidation.textContent = "输出路径和 Lessons 路径不能为空"; return; }
|
// Auto-fill scene-id from folder name
|
||||||
els.settingsValidation.textContent = "";
|
const parts = path.replace(/\\/g, "/").split("/");
|
||||||
defaultsLoaded = true;
|
const folderName = parts[parts.length - 1];
|
||||||
closeSettings();
|
if (folderName && !els.sceneId.value) {
|
||||||
updateGenerateBtn();
|
els.sceneId.value = folderName;
|
||||||
appendRow("status", "设置已保存");
|
}
|
||||||
}
|
updateGenerateBtn();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
els.browseOutputRoot.addEventListener("click", async () => {
|
||||||
|
const path = await selectFolder(els.settingOutputRoot.value || null);
|
||||||
|
if (path) {
|
||||||
|
els.settingOutputRoot.value = path;
|
||||||
|
updateGenerateBtn();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
els.browseLessons.addEventListener("click", async () => {
|
||||||
|
const path = await selectFile(els.settingLessons.value || null, "TOML 文件 (*.toml)|*.toml|所有文件 (*.*)|*.*");
|
||||||
|
if (path) {
|
||||||
|
els.settingLessons.value = path;
|
||||||
|
updateGenerateBtn();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
els.analyzeBtn.addEventListener("click", analyze);
|
|
||||||
els.sourceDir.addEventListener("keydown", (e) => { if (e.key === "Enter") analyze(); });
|
|
||||||
els.generateBtn.addEventListener("click", generate);
|
els.generateBtn.addEventListener("click", generate);
|
||||||
els.settingsBtn.addEventListener("click", openSettings);
|
|
||||||
els.settingsCancelBtn.addEventListener("click", closeSettings);
|
|
||||||
els.settingsSaveBtn.addEventListener("click", saveSettings);
|
|
||||||
els.settingsModal.addEventListener("click", (e) => { if (e.target === els.settingsModal) closeSettings(); });
|
|
||||||
els.sourceDir.addEventListener("input", updateGenerateBtn);
|
|
||||||
els.sceneId.addEventListener("input", updateGenerateBtn);
|
els.sceneId.addEventListener("input", updateGenerateBtn);
|
||||||
els.sceneName.addEventListener("input", updateGenerateBtn);
|
els.sceneName.addEventListener("input", updateGenerateBtn);
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ fn run() -> Result<(), String> {
|
|||||||
scene_id: args.scene_id,
|
scene_id: args.scene_id,
|
||||||
scene_name: args.scene_name,
|
scene_name: args.scene_name,
|
||||||
scene_kind: args.scene_kind,
|
scene_kind: args.scene_kind,
|
||||||
|
target_url: args.target_url,
|
||||||
output_root: args.output_root,
|
output_root: args.output_root,
|
||||||
lessons_path: args.lessons_path,
|
lessons_path: args.lessons_path,
|
||||||
})
|
})
|
||||||
@@ -32,6 +33,7 @@ struct CliArgs {
|
|||||||
scene_id: String,
|
scene_id: String,
|
||||||
scene_name: String,
|
scene_name: String,
|
||||||
scene_kind: Option<SceneKind>,
|
scene_kind: Option<SceneKind>,
|
||||||
|
target_url: Option<String>,
|
||||||
output_root: PathBuf,
|
output_root: PathBuf,
|
||||||
lessons_path: PathBuf,
|
lessons_path: PathBuf,
|
||||||
}
|
}
|
||||||
@@ -41,6 +43,7 @@ fn parse_args(args: impl Iterator<Item = String>) -> Result<CliArgs, String> {
|
|||||||
let mut scene_id = None;
|
let mut scene_id = None;
|
||||||
let mut scene_name = None;
|
let mut scene_name = None;
|
||||||
let mut scene_kind = None;
|
let mut scene_kind = None;
|
||||||
|
let mut target_url = None;
|
||||||
let mut output_root = None;
|
let mut output_root = None;
|
||||||
let mut lessons_path = None;
|
let mut lessons_path = None;
|
||||||
let mut pending_flag: Option<String> = None;
|
let mut pending_flag: Option<String> = None;
|
||||||
@@ -57,6 +60,7 @@ fn parse_args(args: impl Iterator<Item = String>) -> Result<CliArgs, String> {
|
|||||||
.ok_or_else(|| format!("invalid scene kind: {}", arg))?,
|
.ok_or_else(|| format!("invalid scene kind: {}", arg))?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
"--target-url" => target_url = Some(arg),
|
||||||
"--output-root" => output_root = Some(PathBuf::from(arg)),
|
"--output-root" => output_root = Some(PathBuf::from(arg)),
|
||||||
"--lessons" => lessons_path = Some(PathBuf::from(arg)),
|
"--lessons" => lessons_path = Some(PathBuf::from(arg)),
|
||||||
_ => return Err(format!("unsupported argument {flag}")),
|
_ => return Err(format!("unsupported argument {flag}")),
|
||||||
@@ -65,8 +69,8 @@ fn parse_args(args: impl Iterator<Item = String>) -> Result<CliArgs, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
match arg.as_str() {
|
match arg.as_str() {
|
||||||
"--source-dir" | "--scene-id" | "--scene-name" | "--scene-kind" | "--output-root"
|
"--source-dir" | "--scene-id" | "--scene-name" | "--scene-kind" | "--target-url"
|
||||||
| "--lessons" => {
|
| "--output-root" | "--lessons" => {
|
||||||
pending_flag = Some(arg);
|
pending_flag = Some(arg);
|
||||||
}
|
}
|
||||||
"--help" | "-h" => return Err(usage()),
|
"--help" | "-h" => return Err(usage()),
|
||||||
@@ -83,11 +87,12 @@ fn parse_args(args: impl Iterator<Item = String>) -> Result<CliArgs, String> {
|
|||||||
scene_id: scene_id.ok_or_else(usage)?,
|
scene_id: scene_id.ok_or_else(usage)?,
|
||||||
scene_name: scene_name.ok_or_else(usage)?,
|
scene_name: scene_name.ok_or_else(usage)?,
|
||||||
scene_kind,
|
scene_kind,
|
||||||
|
target_url,
|
||||||
output_root: output_root.ok_or_else(usage)?,
|
output_root: output_root.ok_or_else(usage)?,
|
||||||
lessons_path: lessons_path.ok_or_else(usage)?,
|
lessons_path: lessons_path.ok_or_else(usage)?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn usage() -> String {
|
fn usage() -> String {
|
||||||
"usage: sg_scene_generate --source-dir <scenario-dir> --scene-id <scene-id> --scene-name <display-name> [--scene-kind <report_collection|monitoring>] --output-root <skill-staging-root> --lessons <lessons-toml>".to_string()
|
"usage: sg_scene_generate --source-dir <scenario-dir> --scene-id <scene-id> --scene-name <display-name> [--scene-kind <report_collection|monitoring>] [--target-url <url>] --output-root <skill-staging-root> --lessons <lessons-toml>".to_string()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ pub struct GenerateSceneRequest {
|
|||||||
pub scene_id: String,
|
pub scene_id: String,
|
||||||
pub scene_name: String,
|
pub scene_name: String,
|
||||||
pub scene_kind: Option<SceneKind>,
|
pub scene_kind: Option<SceneKind>,
|
||||||
|
pub target_url: Option<String>,
|
||||||
pub output_root: PathBuf,
|
pub output_root: PathBuf,
|
||||||
pub lessons_path: PathBuf,
|
pub lessons_path: PathBuf,
|
||||||
}
|
}
|
||||||
@@ -153,7 +154,10 @@ fn scene_toml_report_collection(
|
|||||||
tool_name: &str,
|
tool_name: &str,
|
||||||
) -> String {
|
) -> String {
|
||||||
let expected_domain = analysis.bootstrap.expected_domain.as_deref().unwrap_or_default();
|
let expected_domain = analysis.bootstrap.expected_domain.as_deref().unwrap_or_default();
|
||||||
let target_url = analysis.bootstrap.target_url.as_deref().unwrap_or_default();
|
// Use request.target_url if provided, otherwise fall back to analysis
|
||||||
|
let target_url = request.target_url.as_deref()
|
||||||
|
.or(analysis.bootstrap.target_url.as_deref())
|
||||||
|
.unwrap_or_default();
|
||||||
format!(
|
format!(
|
||||||
"[scene]\nid = \"{}\"\nskill = \"{}\"\ntool = \"{}\"\nkind = \"browser_script\"\nversion = \"0.1.0\"\ncategory = \"report_collection\"\n\n[manifest]\nschema_version = \"1\"\n\n[bootstrap]\nexpected_domain = \"{}\"\ntarget_url = \"{}\"\npage_title_keywords = [\"报表\", \"线损\"]\nrequires_target_page = true\n\n[deterministic]\nsuffix = \"。。。\"\ninclude_keywords = [\"{}\", \"报表\", \"统计\"]\nexclude_keywords = [\"知乎\"]\n\n[[params]]\nname = \"org\"\nresolver = \"dictionary_entity\"\nrequired = true\nprompt_missing = \"已命中{},但缺少供电单位。\"\nprompt_ambiguous = \"已命中{},但供电单位存在歧义。\"\n\n[params.resolver_config]\ndictionary_ref = \"references/org-dictionary.json\"\noutput_label_field = \"org_label\"\noutput_code_field = \"org_code\"\n\n[[params]]\nname = \"period\"\nresolver = \"month_week_period\"\nrequired = true\nprompt_missing = \"已命中{},但缺少统计周期。\"\nprompt_ambiguous = \"已命中{},但统计周期存在歧义。\"\n\n[artifact]\ntype = \"report-artifact\"\nsuccess_status = [\"ok\", \"partial\", \"empty\"]\nfailure_status = [\"blocked\", \"error\"]\n\n[postprocess]\nexporter = \"xlsx_report\"\nauto_open = \"excel\"\n",
|
"[scene]\nid = \"{}\"\nskill = \"{}\"\ntool = \"{}\"\nkind = \"browser_script\"\nversion = \"0.1.0\"\ncategory = \"report_collection\"\n\n[manifest]\nschema_version = \"1\"\n\n[bootstrap]\nexpected_domain = \"{}\"\ntarget_url = \"{}\"\npage_title_keywords = [\"报表\", \"线损\"]\nrequires_target_page = true\n\n[deterministic]\nsuffix = \"。。。\"\ninclude_keywords = [\"{}\", \"报表\", \"统计\"]\nexclude_keywords = [\"知乎\"]\n\n[[params]]\nname = \"org\"\nresolver = \"dictionary_entity\"\nrequired = true\nprompt_missing = \"已命中{},但缺少供电单位。\"\nprompt_ambiguous = \"已命中{},但供电单位存在歧义。\"\n\n[params.resolver_config]\ndictionary_ref = \"references/org-dictionary.json\"\noutput_label_field = \"org_label\"\noutput_code_field = \"org_code\"\n\n[[params]]\nname = \"period\"\nresolver = \"month_week_period\"\nrequired = true\nprompt_missing = \"已命中{},但缺少统计周期。\"\nprompt_ambiguous = \"已命中{},但统计周期存在歧义。\"\n\n[artifact]\ntype = \"report-artifact\"\nsuccess_status = [\"ok\", \"partial\", \"empty\"]\nfailure_status = [\"blocked\", \"error\"]\n\n[postprocess]\nexporter = \"xlsx_report\"\nauto_open = \"excel\"\n",
|
||||||
request.scene_id,
|
request.scene_id,
|
||||||
@@ -175,7 +179,10 @@ fn scene_toml_monitoring(
|
|||||||
tool_name: &str,
|
tool_name: &str,
|
||||||
) -> String {
|
) -> String {
|
||||||
let expected_domain = analysis.bootstrap.expected_domain.as_deref().unwrap_or_default();
|
let expected_domain = analysis.bootstrap.expected_domain.as_deref().unwrap_or_default();
|
||||||
let target_url = analysis.bootstrap.target_url.as_deref().unwrap_or_default();
|
// Use request.target_url if provided, otherwise fall back to analysis
|
||||||
|
let target_url = request.target_url.as_deref()
|
||||||
|
.or(analysis.bootstrap.target_url.as_deref())
|
||||||
|
.unwrap_or_default();
|
||||||
format!(
|
format!(
|
||||||
"[scene]\nid = \"{}\"\nskill = \"{}\"\ntool = \"{}\"\nkind = \"browser_script\"\nversion = \"0.1.0\"\ncategory = \"monitoring\"\n\n[manifest]\nschema_version = \"1\"\n\n[bootstrap]\nexpected_domain = \"{}\"\ntarget_url = \"{}\"\npage_title_keywords = [\"监测\", \"状态\"]\nrequires_target_page = true\n\n[deterministic]\nsuffix = \"。。。\"\ninclude_keywords = [\"{}\", \"监测\", \"状态\"]\nexclude_keywords = [\"知乎\"]\n\n# 监测类场景参数留空,由用户手动编辑\n\n[artifact]\ntype = \"report-artifact\"\nsuccess_status = [\"ok\", \"partial\", \"empty\"]\nfailure_status = [\"blocked\", \"error\"]\n\n[postprocess]\nexporter = \"xlsx_report\"\nauto_open = \"excel\"\n",
|
"[scene]\nid = \"{}\"\nskill = \"{}\"\ntool = \"{}\"\nkind = \"browser_script\"\nversion = \"0.1.0\"\ncategory = \"monitoring\"\n\n[manifest]\nschema_version = \"1\"\n\n[bootstrap]\nexpected_domain = \"{}\"\ntarget_url = \"{}\"\npage_title_keywords = [\"监测\", \"状态\"]\nrequires_target_page = true\n\n[deterministic]\nsuffix = \"。。。\"\ninclude_keywords = [\"{}\", \"监测\", \"状态\"]\nexclude_keywords = [\"知乎\"]\n\n# 监测类场景参数留空,由用户手动编辑\n\n[artifact]\ntype = \"report-artifact\"\nsuccess_status = [\"ok\", \"partial\", \"empty\"]\nfailure_status = [\"blocked\", \"error\"]\n\n[postprocess]\nexporter = \"xlsx_report\"\nauto_open = \"excel\"\n",
|
||||||
request.scene_id,
|
request.scene_id,
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ fn generator_writes_registration_ready_package_with_scene_toml() {
|
|||||||
scene_id: "sample-report-scene".to_string(),
|
scene_id: "sample-report-scene".to_string(),
|
||||||
scene_name: "示例报表场景".to_string(),
|
scene_name: "示例报表场景".to_string(),
|
||||||
scene_kind: None,
|
scene_kind: None,
|
||||||
|
target_url: None,
|
||||||
output_root: output_root.clone(),
|
output_root: output_root.clone(),
|
||||||
lessons_path: PathBuf::from("docs/superpowers/references/tq-lineloss-lessons-learned.toml"),
|
lessons_path: PathBuf::from("docs/superpowers/references/tq-lineloss-lessons-learned.toml"),
|
||||||
})
|
})
|
||||||
@@ -152,6 +153,7 @@ fn generator_emits_monitoring_template() {
|
|||||||
scene_id: "sample-monitor-scene".to_string(),
|
scene_id: "sample-monitor-scene".to_string(),
|
||||||
scene_name: "示例监测场景".to_string(),
|
scene_name: "示例监测场景".to_string(),
|
||||||
scene_kind: Some(SceneKind::Monitoring),
|
scene_kind: Some(SceneKind::Monitoring),
|
||||||
|
target_url: None,
|
||||||
output_root: output_root.clone(),
|
output_root: output_root.clone(),
|
||||||
lessons_path: PathBuf::from("docs/superpowers/references/tq-lineloss-lessons-learned.toml"),
|
lessons_path: PathBuf::from("docs/superpowers/references/tq-lineloss-lessons-learned.toml"),
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user