Files
claw/docs/superpowers/plans/2026-04-16-multi-scene-kind-generator-plan.md
木炎 45b54ab007 docs: add multi-scene-kind generator implementation plan
7 tasks covering:
- Task 1: Extend SceneKind enum and analyzer function
- Task 2: Add multi-template support to generator
- Task 3: Add --scene-kind CLI parameter
- Task 4-6: Pass sceneKind through Node.js stack to Web UI
- Task 7: E2E testing and verification

🤖 Generated with [Qoder][https://qoder.com]
2026-04-16 23:26:01 +08:00

811 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<Self> {
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<SceneKind>,
) -> Result<SceneSourceAnalysis, AnalyzeSceneError> {
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 固定为 BrowserScriptV1 只支持这一种)
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<SceneSourceAnalysis, AnalyzeSceneError> {
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<SceneKind>, // 新增
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<PathBuf, GenerateSceneError> {
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
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>设备监测状态</title>
<!-- 注意:没有 sgclaw-scene-kind meta 标签,测试 hint 参数 -->
</head>
<body>
<main>
<h1>设备监测状态</h1>
<div id="monitor-status">running</div>
</main>
</body>
</html>
```
- [ ] **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<SceneKind>, // 新增
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<Item = String>) -> Result<CliArgs, String> {
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<String> = 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 <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()
}
```
- [ ] **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
<div class="form-group">
<label for="sceneKind">场景类型</label>
<select id="sceneKind">
<option value="report_collection" selected>报表收集类</option>
<option value="monitoring">监测类</option>
</select>
<span class="hint">报表类:查询数据导出 Excel监测类定时检查状态</span>
</div>
```
- [ ] **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` 的运行时校验逻辑