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]
811 lines
24 KiB
Markdown
811 lines
24 KiB
Markdown
# 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 固定为 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<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` 的运行时校验逻辑
|