diff --git a/docs/superpowers/plans/2026-04-16-multi-scene-kind-generator-plan.md b/docs/superpowers/plans/2026-04-16-multi-scene-kind-generator-plan.md new file mode 100644 index 0000000..f81fa95 --- /dev/null +++ b/docs/superpowers/plans/2026-04-16-multi-scene-kind-generator-plan.md @@ -0,0 +1,810 @@ +# Multi-Scene-Kind Generator Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 扩展 `sg_scene_generate` 支持多种场景类型,让用户在 Web UI 上手动选择场景类型(报表收集类/监测类),不再依赖第三方场景目录中的 meta 标签。 + +**Architecture:** 放宽 analyzer.rs 的 meta 校验,让 meta 标签变为可选;在 CLI 增加 `--scene-kind` 参数;在 generator.rs 根据场景类型选择不同模板;在 Web UI 增加场景类型下拉框。 + +**Tech Stack:** Rust, Node.js, HTML/CSS/JS + +--- + +## File Map + +### Core Rust files (backend) + +- **Modify:** `src/generated_scene/analyzer.rs` — 放宽 meta 校验,新增 `SceneKind::Monitoring`,函数签名增加 `scene_kind_hint` 参数 +- **Modify:** `src/generated_scene/generator.rs` — 多模板支持,根据 `SceneKind` 路由到不同模板函数 +- **Modify:** `src/bin/sg_scene_generate.rs` — 新增 `--scene-kind` CLI 参数 + +### Frontend files (Web UI) + +- **Modify:** `frontend/scene-generator/sg_scene_generator.html` — 新增场景类型下拉框 +- **Modify:** `frontend/scene-generator/server.js` — `/generate` 接口传递 `sceneKind` 参数 +- **Modify:** `frontend/scene-generator/generator-runner.js` — `runGenerator` 增加 `sceneKind` 参数 + +### Test files + +- **Modify:** `tests/scene_generator_test.rs` — 新增监测类场景测试 +- **Create:** `tests/fixtures/generated_scene/monitoring/index.html` — 监测类 fixture + +--- + +### Task 1: 扩展 SceneKind 枚举和 analyzer 函数签名 + +**Files:** +- Modify: `src/generated_scene/analyzer.rs:1-127` +- Test: `tests/scene_generator_test.rs` + +- [ ] **Step 1: 写失败测试 — analyzer 接受 scene_kind_hint 参数** + +修改 `tests/scene_generator_test.rs`,新增测试: + +```rust +#[test] +fn analyzer_accepts_missing_meta_with_scene_kind_hint() { + // non_report fixture 没有 scene-kind meta 标签 + let analysis = analyze_scene_source_with_hint( + Path::new("tests/fixtures/generated_scene/non_report"), + Some(SceneKind::ReportCollection), + ) + .unwrap(); + + // 应该成功,使用 hint 参数作为类型 + assert_eq!(analysis.scene_kind, SceneKind::ReportCollection); +} + +#[test] +fn analyzer_uses_hint_when_meta_missing() { + let analysis = analyze_scene_source_with_hint( + Path::new("tests/fixtures/generated_scene/non_report"), + Some(SceneKind::Monitoring), + ) + .unwrap(); + + assert_eq!(analysis.scene_kind, SceneKind::Monitoring); +} + +#[test] +fn analyzer_uses_meta_when_present_and_no_hint() { + // report_collection fixture 有正确的 meta 标签 + let analysis = analyze_scene_source_with_hint( + Path::new("tests/fixtures/generated_scene/report_collection"), + None, + ) + .unwrap(); + + assert_eq!(analysis.scene_kind, SceneKind::ReportCollection); +} + +#[test] +fn analyzer_hint_overrides_meta() { + // 用户选择优先于 meta 标签 + let analysis = analyze_scene_source_with_hint( + Path::new("tests/fixtures/generated_scene/report_collection"), + Some(SceneKind::Monitoring), + ) + .unwrap(); + + assert_eq!(analysis.scene_kind, SceneKind::Monitoring); +} +``` + +- [ ] **Step 2: 运行测试确认失败** + +Run: +```bash +cargo test --test scene_generator_test -- --nocapture +``` + +Expected: FAIL,因为 `analyze_scene_source_with_hint` 函数不存在 + +- [ ] **Step 3: 实现 SceneKind::Monitoring 枚举变体** + +修改 `src/generated_scene/analyzer.rs`,扩展枚举: + +```rust +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SceneKind { + ReportCollection, + Monitoring, +} + +impl SceneKind { + pub fn from_str(s: &str) -> Option { + match s { + "report_collection" => Some(Self::ReportCollection), + "monitoring" => Some(Self::Monitoring), + _ => None, + } + } + + pub fn as_str(&self) -> &'static str { + match self { + Self::ReportCollection => "report_collection", + Self::Monitoring => "monitoring", + } + } +} +``` + +- [ ] **Step 4: 实现带 hint 参数的新函数** + +在 `src/generated_scene/analyzer.rs` 添加新函数: + +```rust +pub fn analyze_scene_source_with_hint( + source_dir: &Path, + scene_kind_hint: Option, +) -> Result { + let index_path = source_dir.join("index.html"); + let html = fs::read_to_string(&index_path).map_err(|err| { + AnalyzeSceneError::new(format!( + "failed to read scene source {}: {err}", + index_path.display() + )) + })?; + + // 从 meta 标签读取类型(可选) + let meta_scene_kind = meta_content(&html, "sgclaw-scene-kind"); + let meta_tool_kind = meta_content(&html, "sgclaw-tool-kind"); + + // 用户 hint 优先于 meta 标签,默认为 ReportCollection + let scene_kind = scene_kind_hint + .or_else(|| meta_scene_kind.as_deref().and_then(SceneKind::from_str)) + .unwrap_or(SceneKind::ReportCollection); + + // tool_kind 固定为 BrowserScript(V1 只支持这一种) + let tool_kind = ToolKind::BrowserScript; + + // 验证 meta 标签中的类型(如果存在)是否与最终类型兼容 + if let Some(meta) = meta_scene_kind.as_deref() { + if SceneKind::from_str(meta).is_none() { + return Err(AnalyzeSceneError::new(format!( + "unknown sgclaw-scene-kind: {}", + meta + ))); + } + } + + let target_url = meta_content(&html, "sgclaw-target-url"); + let expected_domain = meta_content(&html, "sgclaw-expected-domain"); + let entry_script = meta_content(&html, "sgclaw-entry-script"); + + // 对于 report_collection 类型,要求必须有 target_url、expected_domain、entry_script + // 对于 monitoring 类型,这些字段可选(生成简化模板) + if scene_kind == SceneKind::ReportCollection { + if target_url.as_deref().unwrap_or_default().trim().is_empty() + || expected_domain + .as_deref() + .unwrap_or_default() + .trim() + .is_empty() + || entry_script + .as_deref() + .unwrap_or_default() + .trim() + .is_empty() + { + return Err(AnalyzeSceneError::new( + "report_collection scene source must declare target url, expected domain, and entry script", + )); + } + } + + Ok(SceneSourceAnalysis { + scene_kind, + tool_kind, + bootstrap: BootstrapAnalysis { + target_url, + expected_domain, + }, + collection_entry_script: entry_script, + source_dir: source_dir.to_path_buf(), + }) +} + +// 保留原函数签名以兼容现有调用 +pub fn analyze_scene_source(source_dir: &Path) -> Result { + analyze_scene_source_with_hint(source_dir, None) +} +``` + +- [ ] **Step 5: 运行测试确认通过** + +Run: +```bash +cargo test --test scene_generator_test -- --nocapture +``` + +Expected: PASS + +- [ ] **Step 6: 提交 analyzer 改动** + +Run: +```bash +git add src/generated_scene/analyzer.rs tests/scene_generator_test.rs +git commit -m "feat: add SceneKind::Monitoring and scene_kind_hint param to analyzer" +``` + +--- + +### Task 2: 修改 generator 支持多模板 + +**Files:** +- Modify: `src/generated_scene/generator.rs:1-204` + +- [ ] **Step 1: 写失败测试 — generator 生成监测类模板** + +修改 `tests/scene_generator_test.rs`,新增测试: + +```rust +#[test] +fn generator_emits_monitoring_template() { + let output_root = temp_workspace("sgclaw-monitoring-generator"); + + generate_scene_package(GenerateSceneRequest { + source_dir: PathBuf::from("tests/fixtures/generated_scene/monitoring"), + scene_id: "sample-monitor-scene".to_string(), + scene_name: "示例监测场景".to_string(), + scene_kind: Some(SceneKind::Monitoring), + output_root: output_root.clone(), + lessons_path: PathBuf::from("docs/superpowers/references/tq-lineloss-lessons-learned.toml"), + }) + .unwrap(); + + let skill_root = output_root.join("skills/sample-monitor-scene"); + assert!(skill_root.join("SKILL.toml").exists()); + assert!(skill_root.join("scene.toml").exists()); + + let generated_manifest = fs::read_to_string(skill_root.join("scene.toml")).unwrap(); + assert!(generated_manifest.contains("category = \"monitoring\"")); + // 监测类不应该有 org/period resolver + assert!(!generated_manifest.contains("resolver = \"dictionary_entity\"")); +} +``` + +- [ ] **Step 2: 运行测试确认失败** + +Run: +```bash +cargo test --test scene_generator_test -- --nocapture +``` + +Expected: FAIL,因为 `GenerateSceneRequest` 没有 `scene_kind` 字段 + +- [ ] **Step 3: 修改 GenerateSceneRequest 增加 scene_kind 字段** + +修改 `src/generated_scene/generator.rs`: + +```rust +#[derive(Debug, Clone)] +pub struct GenerateSceneRequest { + pub source_dir: PathBuf, + pub scene_id: String, + pub scene_name: String, + pub scene_kind: Option, // 新增 + pub output_root: PathBuf, + pub lessons_path: PathBuf, +} +``` + +- [ ] **Step 4: 修改 generate_scene_package 使用新 analyzer 函数** + +修改 `src/generated_scene/generator.rs`: + +```rust +use crate::generated_scene::analyzer::{analyze_scene_source_with_hint, AnalyzeSceneError, SceneKind}; + +pub fn generate_scene_package( + request: GenerateSceneRequest, +) -> Result { + let analysis = analyze_scene_source_with_hint(&request.source_dir, request.scene_kind.clone())?; + // ... 后续代码 +``` + +- [ ] **Step 5: 实现监测类模板函数** + +在 `src/generated_scene/generator.rs` 添加: + +```rust +fn scene_toml_monitoring( + request: &GenerateSceneRequest, + analysis: &SceneSourceAnalysis, + tool_name: &str, +) -> String { + let expected_domain = analysis.bootstrap.expected_domain.as_deref().unwrap_or(""); + let target_url = analysis.bootstrap.target_url.as_deref().unwrap_or(""); + + 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 = \"{}\"\nrequires_target_page = true\n\n[deterministic]\nsuffix = \"。。。\"\ninclude_keywords = [\"{}\"]\nexclude_keywords = []\n\n# 参数部分留空,用户手动编辑\n# [[params]]\n# name = \"xxx\"\n# resolver = \"literal_passthrough\"\n\n[artifact]\ntype = \"monitoring-status\"\nsuccess_status = [\"ok\", \"running\"]\nfailure_status = [\"error\", \"timeout\"]\n\n# 后处理留空,用户手动编辑\n", + request.scene_id, + request.scene_id, + tool_name, + expected_domain, + target_url, + request.scene_name + ) +} +``` + +- [ ] **Step 6: 修改 scene_toml 函数路由到不同模板** + +修改 `src/generated_scene/generator.rs` 的 `scene_toml` 函数: + +```rust +fn scene_toml( + request: &GenerateSceneRequest, + analysis: &SceneSourceAnalysis, + tool_name: &str, +) -> String { + match analysis.scene_kind { + SceneKind::ReportCollection => scene_toml_report_collection(request, analysis, tool_name), + SceneKind::Monitoring => scene_toml_monitoring(request, analysis, tool_name), + } +} + +fn scene_toml_report_collection( + request: &GenerateSceneRequest, + analysis: &SceneSourceAnalysis, + tool_name: &str, +) -> String { + let expected_domain = analysis.bootstrap.expected_domain.as_deref().unwrap_or(""); + let target_url = analysis.bootstrap.target_url.as_deref().unwrap_or(""); + + // 现有的 report_collection 模板代码 + 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", + request.scene_id, + request.scene_id, + tool_name, + expected_domain, + target_url, + request.scene_name, + request.scene_name, + request.scene_name, + request.scene_name, + request.scene_name + ) +} +``` + +- [ ] **Step 7: 创建监测类 fixture** + +创建 `tests/fixtures/generated_scene/monitoring/index.html`: + +```html + + + + + 设备监测状态 + + + +
+

设备监测状态

+
running
+
+ + +``` + +- [ ] **Step 8: 运行测试确认通过** + +Run: +```bash +cargo test --test scene_generator_test -- --nocapture +``` + +Expected: PASS + +- [ ] **Step 9: 提交 generator 改动** + +Run: +```bash +git add src/generated_scene/generator.rs tests/scene_generator_test.rs tests/fixtures/generated_scene/monitoring +git commit -m "feat: add monitoring template support to generator" +``` + +--- + +### Task 3: 修改 CLI 增加 --scene-kind 参数 + +**Files:** +- Modify: `src/bin/sg_scene_generate.rs:1-82` + +- [ ] **Step 1: 修改 CliArgs 结构体增加 scene_kind 字段** + +修改 `src/bin/sg_scene_generate.rs`: + +```rust +use sgclaw::generated_scene::analyzer::SceneKind; + +struct CliArgs { + source_dir: PathBuf, + scene_id: String, + scene_name: String, + scene_kind: Option, // 新增 + output_root: PathBuf, + lessons_path: PathBuf, +} +``` + +- [ ] **Step 2: 修改 parse_args 解析 --scene-kind 参数** + +修改 `src/bin/sg_scene_generate.rs`: + +```rust +fn parse_args(args: impl Iterator) -> Result { + let mut source_dir = None; + let mut scene_id = None; + let mut scene_name = None; + let mut scene_kind = None; // 新增 + let mut output_root = None; + let mut lessons_path = None; + let mut pending_flag: Option = None; + + for arg in args { + if let Some(flag) = pending_flag.take() { + match flag.as_str() { + "--source-dir" => source_dir = Some(PathBuf::from(arg)), + "--scene-id" => scene_id = Some(arg), + "--scene-name" => scene_name = Some(arg), + "--scene-kind" => { + scene_kind = Some(SceneKind::from_str(&arg).ok_or_else(|| { + format!("invalid scene-kind: {}, expected report_collection or monitoring", arg) + })?); + } + "--output-root" => output_root = Some(PathBuf::from(arg)), + "--lessons" => lessons_path = Some(PathBuf::from(arg)), + _ => return Err(format!("unsupported argument {flag}")), + } + continue; + } + + match arg.as_str() { + "--source-dir" | "--scene-id" | "--scene-name" | "--scene-kind" | "--output-root" | "--lessons" => { + pending_flag = Some(arg); + } + "--help" | "-h" => return Err(usage()), + _ => return Err(format!("unsupported argument {arg}\n{}", usage())), + } + } + + if let Some(flag) = pending_flag { + return Err(format!("missing value for {flag}")); + } + + Ok(CliArgs { + source_dir: source_dir.ok_or_else(usage)?, + scene_id: scene_id.ok_or_else(usage)?, + scene_name: scene_name.ok_or_else(usage)?, + scene_kind, // 可选,默认 None + output_root: output_root.ok_or_else(usage)?, + lessons_path: lessons_path.ok_or_else(usage)?, + }) +} +``` + +- [ ] **Step 3: 修改 run 函数传递 scene_kind** + +修改 `src/bin/sg_scene_generate.rs`: + +```rust +fn run() -> Result<(), String> { + let args = parse_args(env::args().skip(1))?; + let skill_root = generate_scene_package(GenerateSceneRequest { + source_dir: args.source_dir, + scene_id: args.scene_id, + scene_name: args.scene_name, + scene_kind: args.scene_kind, // 新增 + output_root: args.output_root, + lessons_path: args.lessons_path, + }) + .map_err(|err| err.to_string())?; + + println!("generated scene package: {}", skill_root.display()); + Ok(()) +} +``` + +- [ ] **Step 4: 更新 usage 函数** + +修改 `src/bin/sg_scene_generate.rs`: + +```rust +fn usage() -> String { + "usage: sg_scene_generate --source-dir --scene-id --scene-name [--scene-kind ] --output-root --lessons ".to_string() +} +``` + +- [ ] **Step 5: 运行测试确认编译通过** + +Run: +```bash +cargo build --bin sg_scene_generate +``` + +Expected: 编译成功 + +- [ ] **Step 6: 手动测试 CLI** + +Run: +```bash +cargo run --bin sg_scene_generate -- --source-dir tests/fixtures/generated_scene/monitoring --scene-id test-monitor --scene-name "测试监测" --scene-kind monitoring --output-root ./tmp_test --lessons docs/superpowers/references/tq-lineloss-lessons-learned.toml +``` + +Expected: 生成成功,scene.toml 包含 `category = "monitoring"` + +- [ ] **Step 7: 提交 CLI 改动** + +Run: +```bash +git add src/bin/sg_scene_generate.rs +git commit -m "feat: add --scene-kind CLI param to sg_scene_generate" +``` + +--- + +### Task 4: 修改 Node.js generator-runner 传递 sceneKind + +**Files:** +- Modify: `frontend/scene-generator/generator-runner.js:1-175` + +- [ ] **Step 1: 修改 runGenerator 函数签名和 args 数组** + +修改 `frontend/scene-generator/generator-runner.js`: + +```javascript +function runGenerator(params, sseWriter, projectRoot) { + const { sourceDir, sceneId, sceneName, sceneKind, 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, + ]; + + // 只有明确指定 sceneKind 时才添加参数(否则使用默认值 report_collection) + if (sceneKind) { + args.push("--scene-kind", sceneKind); + } + + args.push( + "--output-root", + normalize(outputRoot), + "--lessons", + normalize(lessons) + ); + + // ... 后续代码不变 +``` + +- [ ] **Step 2: 提交 generator-runner 改动** + +Run: +```bash +git add frontend/scene-generator/generator-runner.js +git commit -m "feat: add sceneKind param to generator-runner" +``` + +--- + +### Task 5: 修改 Node.js server 传递 sceneKind + +**Files:** +- Modify: `frontend/scene-generator/server.js:119-154` + +- [ ] **Step 1: 修改 handleGenerate 解构 sceneKind** + +修改 `frontend/scene-generator/server.js`: + +```javascript +async function handleGenerate(req, res) { + let body; + try { + body = await parseBody(req); + } catch { + res.writeHead(400, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: "Invalid JSON body" })); + return; + } + + const { sourceDir, sceneId, sceneName, sceneKind, outputRoot, lessons } = body; + if (!sourceDir || !sceneId || !sceneName || !outputRoot || !lessons) { + res.writeHead(400, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + error: + "All fields required: sourceDir, sceneId, sceneName, outputRoot, lessons", + }) + ); + return; + } + + const sseWriter = initSSE(res); + + try { + await runGenerator( + { sourceDir, sceneId, sceneName, sceneKind, outputRoot, lessons }, // 增加 sceneKind + sseWriter, + config.projectRoot + ); + } catch (err) { + writeSSE(sseWriter, "error", { message: `Server error: ${err.message}` }); + } + + sseWriter.end(); +} +``` + +- [ ] **Step 2: 提交 server 改动** + +Run: +```bash +git add frontend/scene-generator/server.js +git commit -m "feat: pass sceneKind from /generate request to generator" +``` + +--- + +### Task 6: 修改 Web UI 增加场景类型下拉框 + +**Files:** +- Modify: `frontend/scene-generator/sg_scene_generator.html` + +- [ ] **Step 1: 在 HTML 中增加场景类型下拉框** + +在 `sg_scene_generator.html` 的表单区域,scene-name 输入框后面添加: + +```html +
+ + + 报表类:查询数据导出 Excel;监测类:定时检查状态 +
+``` + +- [ ] **Step 2: 修改 generate() 函数读取 sceneKind** + +修改 `sg_scene_generator.html` 中的 `generate()` 函数: + +```javascript +async function generate() { + const sourceDir = document.getElementById('sourceDir').value.trim(); + const sceneId = document.getElementById('sceneId').value.trim(); + const sceneName = document.getElementById('sceneName').value.trim(); + const sceneKind = document.getElementById('sceneKind').value; // 新增 + const outputRoot = document.getElementById('outputRoot').value.trim(); + const lessons = document.getElementById('lessons').value.trim(); + + // ... 验证逻辑不变 + + const response = await fetch('/generate', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + sourceDir, + sceneId, + sceneName, + sceneKind, // 新增 + outputRoot, + lessons + }) + }); + + // ... 后续代码不变 +} +``` + +- [ ] **Step 3: 提交 HTML 改动** + +Run: +```bash +git add frontend/scene-generator/sg_scene_generator.html +git commit -m "feat: add sceneKind dropdown to Web UI" +``` + +--- + +### Task 7: 端到端测试和最终验证 + +**Files:** +- Verify only + +- [ ] **Step 1: 运行所有 Rust 测试** + +Run: +```bash +cargo test --test scene_generator_test -- --nocapture +cargo test --test scene_registry_test -- --nocapture +``` + +Expected: PASS + +- [ ] **Step 2: 重启 Node.js 服务器** + +Run: +```bash +cd frontend/scene-generator && node server.js +``` + +Expected: 服务启动成功 + +- [ ] **Step 3: 手动测试 Web UI 报表类场景** + +1. 打开 `http://127.0.0.1:3210/` +2. 输入场景路径 `D:\desk\智能体资料\场景\营销2.0零度户报表数据生成` +3. 场景类型选择"报表收集类" +4. 点击"分析" → 等待 LLM 提取 scene-id/scene-name +5. 点击"生成 Skill" → 等待生成完成 +6. 检查输出目录下生成的文件 + +Expected: 生成成功,scene.toml 包含 `category = "report_collection"` + +- [ ] **Step 4: 提交最终验证** + +Run: +```bash +git add -A +git status +``` + +确认无未提交改动。 + +--- + +## Verification Checklist + +### Rust 层 + +```bash +cargo test --test scene_generator_test -- --nocapture +cargo build --bin sg_scene_generate +``` + +Expected: +- `analyze_scene_source_with_hint` 接受可选的 `SceneKind` 参数 +- `GenerateSceneRequest` 包含 `scene_kind` 字段 +- generator 根据类型生成不同模板 +- CLI 支持 `--scene-kind` 参数 + +### Node.js 层 + +```bash +node frontend/scene-generator/server.js +``` + +Expected: +- `/generate` 接口接受 `sceneKind` 参数 +- `runGenerator` 正确传递参数给 CLI + +### Web UI 层 + +手动测试: +- 场景类型下拉框正常显示 +- 选择报表类生成 `category = "report_collection"` +- 选择监测类生成 `category = "monitoring"` + +--- + +## Notes For The Engineer + +- 配对的 spec 文件是 `docs/superpowers/specs/2026-04-16-multi-scene-kind-generator-design.md` +- 用户选择 `scene_kind_hint` 优先于 meta 标签 +- 监测类模板是简化版,用户需要手动编辑参数部分 +- V1 不修改 `registry.rs` 的运行时校验逻辑