docs: add implementation plan for enhanced LLM extraction schema
This commit is contained in:
@@ -0,0 +1,839 @@
|
||||
# Enhanced LLM Extraction Schema - 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:** Enhance the LLM extraction schema to support multi-mode business logic, enabling automatic generation of scripts like tq-lineloss-report that switch between month/week modes.
|
||||
|
||||
**Architecture:** Extend existing `SceneInfoJson` in Rust with new mode-related structs. Enhance LLM prompt in `llm-client.js` to detect multi-mode patterns. Add new template function `browser_script_with_modes()` for generating mode-aware JavaScript.
|
||||
|
||||
**Tech Stack:** Rust (serde_json), JavaScript (Node.js), LLM API
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
| File | Action | Purpose |
|
||||
|------|--------|---------|
|
||||
| `src/generated_scene/generator.rs` | Modify | Add mode-related schema structs and multi-mode template |
|
||||
| `frontend/scene-generator/llm-client.js` | Modify | Enhance DEEP_SYSTEM_PROMPT for mode detection |
|
||||
| `frontend/scene-generator/server.js` | Modify | Handle enhanced schema in deep analysis endpoint |
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Add Rust Schema Structs for Multi-Mode Support
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/generated_scene/generator.rs` (after line 21)
|
||||
|
||||
**Goal:** Add new Rust structs to parse the enhanced JSON schema with modes support.
|
||||
|
||||
- [ ] **Step 1: Add ModeConditionJson struct**
|
||||
|
||||
Add after `ApiEndpointJson` struct (line 21):
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ModeConditionJson {
|
||||
pub field: String,
|
||||
#[serde(default = "default_equals")]
|
||||
pub operator: String,
|
||||
pub value: serde_json::Value,
|
||||
}
|
||||
|
||||
fn default_equals() -> String {
|
||||
"equals".to_string()
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add NormalizeRulesJson struct**
|
||||
|
||||
Add after `ModeConditionJson`:
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct NormalizeRulesJson {
|
||||
#[serde(rename = "type", default = "default_validate_all")]
|
||||
pub rules_type: String,
|
||||
#[serde(default)]
|
||||
pub required_fields: Vec<String>,
|
||||
#[serde(default = "default_true")]
|
||||
pub filter_null: bool,
|
||||
}
|
||||
|
||||
fn default_validate_all() -> String {
|
||||
"validate_all_columns".to_string()
|
||||
}
|
||||
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add ModeConfigJson struct**
|
||||
|
||||
Add after `NormalizeRulesJson`:
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ModeConfigJson {
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub label: Option<String>,
|
||||
pub condition: ModeConditionJson,
|
||||
#[serde(rename = "apiEndpoint")]
|
||||
pub api_endpoint: ApiEndpointEnhancedJson,
|
||||
#[serde(rename = "columnDefs", default)]
|
||||
pub column_defs: Vec<(String, String)>,
|
||||
#[serde(rename = "requestTemplate", default)]
|
||||
pub request_template: Option<serde_json::Value>,
|
||||
#[serde(rename = "normalizeRules", default)]
|
||||
pub normalize_rules: Option<NormalizeRulesJson>,
|
||||
#[serde(rename = "responsePath", default)]
|
||||
pub response_path: Option<String>,
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Add ApiEndpointEnhancedJson struct**
|
||||
|
||||
Add before `ModeConfigJson`:
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ApiEndpointEnhancedJson {
|
||||
pub name: String,
|
||||
pub url: String,
|
||||
#[serde(default)]
|
||||
pub method: String,
|
||||
#[serde(rename = "contentType", default)]
|
||||
pub content_type: Option<String>,
|
||||
#[serde(default)]
|
||||
pub description: Option<String>,
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Enhance SceneInfoJson struct**
|
||||
|
||||
Modify `SceneInfoJson` to add mode fields (add after line 54, before the closing brace):
|
||||
|
||||
```rust
|
||||
// Multi-mode support (new fields)
|
||||
#[serde(default)]
|
||||
pub modes: Vec<ModeConfigJson>,
|
||||
#[serde(rename = "defaultMode", default)]
|
||||
pub default_mode: Option<String>,
|
||||
#[serde(rename = "modeSwitchField", default)]
|
||||
pub mode_switch_field: Option<String>,
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Verify the changes**
|
||||
|
||||
Run `cargo check` to verify:
|
||||
|
||||
```bash
|
||||
cargo check
|
||||
```
|
||||
|
||||
Expected: No compilation errors.
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add src/generated_scene/generator.rs
|
||||
git commit -m "feat(generator): add multi-mode schema structs for enhanced LLM extraction"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Enhance LLM Extraction Prompt
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/scene-generator/llm-client.js` (lines 16-46)
|
||||
|
||||
**Goal:** Enhance `DEEP_SYSTEM_PROMPT` to instruct LLM to detect multi-mode business logic.
|
||||
|
||||
- [ ] **Step 1: Replace DEEP_SYSTEM_PROMPT with enhanced version**
|
||||
|
||||
Replace the entire `DEEP_SYSTEM_PROMPT` constant (lines 16-46):
|
||||
|
||||
```javascript
|
||||
const DEEP_SYSTEM_PROMPT = `你是一个场景代码分析专家。分析场景源码,提取关键业务信息。
|
||||
|
||||
## 分析目标
|
||||
|
||||
1. **多模式识别** (关键):
|
||||
- 查找条件分支逻辑 (if/switch) 中基于 period_mode、reportType 等字段的分支
|
||||
- 识别不同分支对应的 API 端点、列定义、请求格式
|
||||
- 如果发现多模式,使用 modes 数组格式输出
|
||||
|
||||
2. **API 端点**: 识别所有 HTTP 请求地址 (URL, method, contentType, 用途)
|
||||
- 从 $.ajax/fetch 调用中提取 contentType
|
||||
- 检测请求格式: application/json 或 application/x-www-form-urlencoded
|
||||
|
||||
3. **请求模板**: 识别请求参数结构
|
||||
- 提取硬编码的分页参数 (rows, page, sidx, sord)
|
||||
- 识别模板变量如 \${args.org_code}
|
||||
|
||||
4. **数据归一化**: 识别数据处理规则
|
||||
- 查找数据渲染/表格填充逻辑
|
||||
- 检测数据验证条件 (哪些字段不能为空)
|
||||
|
||||
5. **响应路径**: 识别数据在响应中的位置
|
||||
- 如 response.content 或 response.data
|
||||
|
||||
## 输出格式
|
||||
|
||||
### 单模式场景 (无 modes 数组):
|
||||
{
|
||||
"sceneId": "string",
|
||||
"sceneName": "string",
|
||||
"sceneKind": "report_collection | monitoring",
|
||||
"expectedDomain": "string",
|
||||
"targetUrl": "string",
|
||||
"apiEndpoints": [{"name": "", "url": "", "method": "POST"}],
|
||||
"staticParams": {"key": "value"},
|
||||
"columnDefs": [["fieldName", "中文列名"]]
|
||||
}
|
||||
|
||||
### 多模式场景 (有 modes 数组):
|
||||
{
|
||||
"sceneId": "tq-lineloss-report",
|
||||
"sceneName": "台区线损报表",
|
||||
"sceneKind": "report_collection",
|
||||
"modes": [
|
||||
{
|
||||
"name": "month",
|
||||
"label": "月度报表",
|
||||
"condition": {"field": "period_mode", "operator": "equals", "value": "month"},
|
||||
"apiEndpoint": {
|
||||
"name": "月度线损查询",
|
||||
"url": "http://...",
|
||||
"method": "POST",
|
||||
"contentType": "application/x-www-form-urlencoded"
|
||||
},
|
||||
"columnDefs": [["ORG_NAME", "供电单位"], ...],
|
||||
"requestTemplate": {"orgno": "\${args.org_code}", "rows": 1000, "page": 1},
|
||||
"normalizeRules": {"type": "validate_all_columns", "filterNull": true},
|
||||
"responsePath": "content"
|
||||
},
|
||||
{
|
||||
"name": "week",
|
||||
"label": "周报表",
|
||||
"condition": {"field": "period_mode", "operator": "equals", "value": "week"},
|
||||
"apiEndpoint": {...},
|
||||
"columnDefs": [...],
|
||||
...
|
||||
}
|
||||
],
|
||||
"defaultMode": "month",
|
||||
"modeSwitchField": "period_mode"
|
||||
}
|
||||
|
||||
**重要**: 如果发现代码中有基于 period_mode 的 if/switch 分支,必须使用多模式格式输出!`;
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify JavaScript syntax**
|
||||
|
||||
```bash
|
||||
node --check frontend/scene-generator/llm-client.js
|
||||
```
|
||||
|
||||
Expected: No syntax errors.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add frontend/scene-generator/llm-client.js
|
||||
git commit -m "feat(llm): enhance DEEP_SYSTEM_PROMPT for multi-mode detection"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Implement Multi-Mode Template in Rust
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/generated_scene/generator.rs` (add new function after `browser_script_with_business_logic`)
|
||||
|
||||
**Goal:** Add a new template function that generates mode-aware JavaScript.
|
||||
|
||||
- [ ] **Step 1: Add browser_script_with_modes function**
|
||||
|
||||
Add after `browser_script_with_business_logic` function (after line 476):
|
||||
|
||||
```rust
|
||||
fn browser_script_with_modes(scene_id: &str, scene_info: &SceneInfoJson) -> String {
|
||||
let modes_json = serde_json::to_string_pretty(&scene_info.modes).unwrap_or_else(|_| "[]".to_string());
|
||||
let default_mode = scene_info.default_mode.as_deref().unwrap_or("month");
|
||||
let mode_switch_field = scene_info.mode_switch_field.as_deref().unwrap_or("period_mode");
|
||||
|
||||
format!(r#"const REPORT_NAME = '{scene_id}';
|
||||
const MODES = {modes_json};
|
||||
const DEFAULT_MODE = '{default_mode}';
|
||||
const MODE_SWITCH_FIELD = '{mode_switch_field}';
|
||||
|
||||
function normalizePayload(payload) {{
|
||||
if (typeof payload === 'string') {{
|
||||
try {{ return JSON.parse(payload); }} catch (_) {{ return {{}}; }}
|
||||
}}
|
||||
return payload && typeof payload === 'object' ? payload : {{}};
|
||||
}}
|
||||
|
||||
function validateArgs(args) {{
|
||||
const errors = [];
|
||||
if (!args.org_code) errors.push('Missing org_code');
|
||||
if (!args.period_value) errors.push('Missing period_value');
|
||||
return {{ valid: errors.length === 0, errors }};
|
||||
}}
|
||||
|
||||
function detectMode(args) {{
|
||||
const modeValue = args[MODE_SWITCH_FIELD] || DEFAULT_MODE;
|
||||
return MODES.find(m => m.condition.value === modeValue) || MODES[0];
|
||||
}}
|
||||
|
||||
function buildModeRequest(args, mode) {{
|
||||
const endpoint = mode.apiEndpoint;
|
||||
const template = mode.requestTemplate || {{}};
|
||||
const contentType = endpoint.contentType || 'application/json';
|
||||
const url = endpoint.url;
|
||||
const method = endpoint.method || 'POST';
|
||||
|
||||
let body;
|
||||
if (contentType === 'application/x-www-form-urlencoded') {{
|
||||
body = {{ ...template }};
|
||||
for (const [key, value] of Object.entries(body)) {{
|
||||
if (typeof value === 'string' && value.startsWith('${{') && value.endsWith('}}')) {{
|
||||
const expr = value.slice(2, -1);
|
||||
try {{
|
||||
body[key] = eval(expr);
|
||||
}} catch (e) {{
|
||||
body[key] = args.org_code;
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
body.orgno = args.org_code;
|
||||
}} else {{
|
||||
body = JSON.stringify({{ ...template, ...args }});
|
||||
}}
|
||||
|
||||
return {{ url, method, headers: {{ 'Content-Type': contentType }}, body }};
|
||||
}}
|
||||
|
||||
function normalizeModeRows(data, mode) {{
|
||||
const rules = mode.normalizeRules || {{ type: 'validate_all_columns', filterNull: true }};
|
||||
const columns = mode.columnDefs.map(([key]) => key);
|
||||
|
||||
if (!Array.isArray(data)) return [];
|
||||
|
||||
return data.map(row => {{
|
||||
const result = {{}};
|
||||
for (const key of columns) {{
|
||||
const v = row[key];
|
||||
result[key] = (v === null || v === undefined || v === '') ? '' : String(v).trim();
|
||||
}}
|
||||
return result;
|
||||
}}).filter(row => {{
|
||||
if (!rules.filterNull) return true;
|
||||
if (rules.type === 'validate_required' && rules.requiredFields) {{
|
||||
return rules.requiredFields.every(f => row[f] !== '');
|
||||
}}
|
||||
return columns.every(k => row[k] !== '');
|
||||
}});
|
||||
}}
|
||||
|
||||
function determineArtifactStatus({{ blockedReason = '', fatalError = '', reasons = [], rows = [] }}) {{
|
||||
if (blockedReason) return 'blocked';
|
||||
if (fatalError) return 'error';
|
||||
if (reasons.length > 0) return 'partial';
|
||||
if (!rows.length) return 'empty';
|
||||
return 'ok';
|
||||
}}
|
||||
|
||||
function buildArtifact({{ status, blockedReason = '', fatalError = '', reasons = [], rows = [], args, columnDefs, columns }}) {{
|
||||
return {{
|
||||
type: 'report-artifact',
|
||||
report_name: REPORT_NAME,
|
||||
status: status || determineArtifactStatus({{ blockedReason, fatalError, reasons, rows }}),
|
||||
period: {{
|
||||
mode: args.period_mode,
|
||||
mode_code: args.period_mode_code,
|
||||
value: args.period_value,
|
||||
payload: normalizePayload(args.period_payload)
|
||||
}},
|
||||
org: {{ label: args.org_label, code: args.org_code }},
|
||||
column_defs: columnDefs || [],
|
||||
columns: columns || [],
|
||||
rows,
|
||||
counts: {{ detail_rows: rows.length }},
|
||||
partial_reasons: reasons.filter(r => r && !r.startsWith('api_') && !r.startsWith('validation_')),
|
||||
reasons: Array.from(new Set(reasons.filter(Boolean)))
|
||||
}};
|
||||
}}
|
||||
|
||||
const defaultDeps = {{
|
||||
validatePageContext(args) {{
|
||||
const host = (globalThis.location?.hostname || '').trim();
|
||||
const expected = (args.expected_domain || '').trim();
|
||||
if (!host) return {{ ok: false, reason: 'page_context_unavailable' }};
|
||||
if (host !== expected) return {{ ok: false, reason: 'page_context_mismatch' }};
|
||||
return {{ ok: true }};
|
||||
}},
|
||||
|
||||
async queryModeData(args, mode) {{
|
||||
const endpoint = mode.apiEndpoint;
|
||||
const request = buildModeRequest(args, mode);
|
||||
const contentType = endpoint.contentType || 'application/json';
|
||||
|
||||
// Prefer jQuery
|
||||
if (typeof $ !== 'undefined' && typeof $.ajax === 'function') {{
|
||||
return new Promise((resolve, reject) => {{
|
||||
$.ajax({{
|
||||
url: request.url,
|
||||
type: request.method,
|
||||
data: request.body,
|
||||
contentType: contentType,
|
||||
dataType: 'json',
|
||||
success: resolve,
|
||||
error: (xhr, status, err) => reject(new Error(
|
||||
`API failed (${{xhr.status}}): ${{err}} | body=${{(xhr.responseText || '').substring(0, 200)}}`
|
||||
))
|
||||
}});
|
||||
}});
|
||||
}}
|
||||
|
||||
// Fallback: fetch
|
||||
if (typeof fetch === 'function') {{
|
||||
const response = await fetch(request.url, {{
|
||||
method: request.method,
|
||||
headers: request.headers,
|
||||
body: request.method !== 'GET' ? request.body : undefined
|
||||
}});
|
||||
if (!response.ok) {{
|
||||
const text = await response.text().catch(() => '');
|
||||
throw new Error(`HTTP ${{response.status}}: ${{text.substring(0, 200)}}`);
|
||||
}}
|
||||
return response.json();
|
||||
}}
|
||||
|
||||
throw new Error('No HTTP client available (need jQuery or fetch)');
|
||||
}}
|
||||
}};
|
||||
|
||||
async function buildBrowserEntrypointResult(args, deps = defaultDeps) {{
|
||||
// 1. Parameter validation
|
||||
const validation = validateArgs(args);
|
||||
if (!validation.valid) {{
|
||||
const mode = detectMode(args);
|
||||
return buildArtifact({{
|
||||
status: 'blocked',
|
||||
blockedReason: 'validation_failed',
|
||||
reasons: validation.errors,
|
||||
rows: [],
|
||||
args,
|
||||
columnDefs: mode.columnDefs,
|
||||
columns: mode.columnDefs.map(([key]) => key)
|
||||
}});
|
||||
}}
|
||||
|
||||
// 2. Page context validation
|
||||
const pageValidation = typeof deps.validatePageContext === 'function'
|
||||
? deps.validatePageContext(args)
|
||||
: {{ ok: true }};
|
||||
if (!pageValidation?.ok) {{
|
||||
const mode = detectMode(args);
|
||||
return buildArtifact({{
|
||||
status: 'blocked',
|
||||
blockedReason: pageValidation?.reason || 'page_context_mismatch',
|
||||
reasons: [pageValidation?.reason || 'page_context_mismatch'],
|
||||
rows: [],
|
||||
args,
|
||||
columnDefs: mode.columnDefs,
|
||||
columns: mode.columnDefs.map(([key]) => key)
|
||||
}});
|
||||
}}
|
||||
|
||||
// 3. Detect mode
|
||||
const mode = detectMode(args);
|
||||
|
||||
// 4. Data fetching
|
||||
const reasons = [];
|
||||
let rawData = null;
|
||||
try {{
|
||||
rawData = await (deps.queryModeData ? deps.queryModeData(args, mode) : Promise.resolve([]));
|
||||
}} catch (error) {{
|
||||
return buildArtifact({{
|
||||
status: 'error',
|
||||
fatalError: error.message,
|
||||
reasons: ['api_query_failed:' + error.message],
|
||||
rows: [],
|
||||
args,
|
||||
columnDefs: mode.columnDefs,
|
||||
columns: mode.columnDefs.map(([key]) => key)
|
||||
}});
|
||||
}}
|
||||
|
||||
// 5. Extract response data
|
||||
const responsePath = mode.responsePath || '';
|
||||
let data = rawData;
|
||||
if (responsePath && rawData) {{
|
||||
data = rawData[responsePath] || rawData;
|
||||
}}
|
||||
|
||||
// 6. Row normalization
|
||||
const rows = normalizeModeRows(data, mode);
|
||||
if (rows.length === 0 && Array.isArray(data) && data.length > 0) {{
|
||||
reasons.push('row_normalization_partial');
|
||||
}}
|
||||
|
||||
// 7. Build artifact
|
||||
return buildArtifact({{
|
||||
reasons,
|
||||
rows,
|
||||
args,
|
||||
columnDefs: mode.columnDefs,
|
||||
columns: mode.columnDefs.map(([key]) => key)
|
||||
}});
|
||||
}}
|
||||
|
||||
if (typeof module !== 'undefined') {{
|
||||
module.exports = {{ buildBrowserEntrypointResult, normalizePayload, validateArgs, detectMode, buildModeRequest, normalizeModeRows, buildArtifact, determineArtifactStatus, MODES, REPORT_NAME }};
|
||||
}}
|
||||
|
||||
if (typeof args !== 'undefined') {{
|
||||
return buildBrowserEntrypointResult(args);
|
||||
}}
|
||||
"#, scene_id = scene_id, modes_json = modes_json, default_mode = default_mode, mode_switch_field = mode_switch_field)
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Modify browser_script function to use multi-mode template**
|
||||
|
||||
Replace the `browser_script` function (lines 270-277):
|
||||
|
||||
```rust
|
||||
fn browser_script(scene_id: &str, analysis: &SceneSourceAnalysis, scene_info: Option<&SceneInfoJson>) -> String {
|
||||
match scene_info {
|
||||
Some(info) if !info.modes.is_empty() => {
|
||||
browser_script_with_modes(scene_id, info)
|
||||
}
|
||||
Some(info) if !info.api_endpoints.is_empty() || !info.column_defs.is_empty() => {
|
||||
browser_script_with_business_logic(scene_id, analysis, info)
|
||||
}
|
||||
_ => browser_script_skeleton(scene_id, analysis),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Verify compilation**
|
||||
|
||||
```bash
|
||||
cargo check
|
||||
```
|
||||
|
||||
Expected: No errors.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add src/generated_scene/generator.rs
|
||||
git commit -m "feat(generator): add multi-mode template for mode-aware script generation"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Add Unit Tests for Schema Parsing
|
||||
|
||||
**Files:**
|
||||
- Create: `src/generated_scene/generator_test.rs`
|
||||
|
||||
**Goal:** Add tests to verify the enhanced schema parses correctly.
|
||||
|
||||
- [ ] **Step 1: Create test file**
|
||||
|
||||
Create `src/generated_scene/generator_test.rs`:
|
||||
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_mode_condition() {
|
||||
let json = r#"{"field": "period_mode", "operator": "equals", "value": "month"}"#;
|
||||
let condition: ModeConditionJson = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(condition.field, "period_mode");
|
||||
assert_eq!(condition.operator, "equals");
|
||||
assert_eq!(condition.value.as_str().unwrap(), "month");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_normalize_rules() {
|
||||
let json = r#"{"type": "validate_required", "requiredFields": ["ORG_NAME"], "filterNull": true}"#;
|
||||
let rules: NormalizeRulesJson = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(rules.rules_type, "validate_required");
|
||||
assert_eq!(rules.required_fields, vec!["ORG_NAME"]);
|
||||
assert!(rules.filter_null);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_mode_config() {
|
||||
let json = r#"{
|
||||
"name": "month",
|
||||
"label": "月度报表",
|
||||
"condition": {"field": "period_mode", "operator": "equals", "value": "month"},
|
||||
"apiEndpoint": {"name": "test", "url": "http://example.com", "method": "POST"},
|
||||
"columnDefs": [["ORG_NAME", "供电单位"]],
|
||||
"responsePath": "content"
|
||||
}"#;
|
||||
let mode: ModeConfigJson = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(mode.name, "month");
|
||||
assert_eq!(mode.column_defs.len(), 1);
|
||||
assert_eq!(mode.response_path, Some("content".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_scene_info_with_modes() {
|
||||
let json = r#"{
|
||||
"sceneId": "test-report",
|
||||
"sceneName": "测试报表",
|
||||
"sceneKind": "report_collection",
|
||||
"modes": [
|
||||
{"name": "month", "condition": {"field": "period_mode", "value": "month"}, "apiEndpoint": {"name": "m", "url": "http://a"}, "columnDefs": []},
|
||||
{"name": "week", "condition": {"field": "period_mode", "value": "week"}, "apiEndpoint": {"name": "w", "url": "http://b"}, "columnDefs": []}
|
||||
],
|
||||
"defaultMode": "month",
|
||||
"modeSwitchField": "period_mode"
|
||||
}"#;
|
||||
let info: SceneInfoJson = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(info.scene_id, "test-report");
|
||||
assert_eq!(info.modes.len(), 2);
|
||||
assert_eq!(info.default_mode, Some("month".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_scene_info_backward_compatible() {
|
||||
// Old format without modes should still work
|
||||
let json = r#"{
|
||||
"sceneId": "old-report",
|
||||
"sceneName": "旧格式报表",
|
||||
"apiEndpoints": [{"name": "test", "url": "http://example.com"}],
|
||||
"columnDefs": [["col1", "列1"]]
|
||||
}"#;
|
||||
let info: SceneInfoJson = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(info.scene_id, "old-report");
|
||||
assert!(info.modes.is_empty());
|
||||
assert_eq!(info.api_endpoints.len(), 1);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add test module to generator.rs**
|
||||
|
||||
Add at the end of `generator.rs`:
|
||||
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod generator_test;
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run tests**
|
||||
|
||||
```bash
|
||||
cargo test --lib generator
|
||||
```
|
||||
|
||||
Expected: All tests pass.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add src/generated_scene/generator_test.rs src/generated_scene/generator.rs
|
||||
git commit -m "test(generator): add unit tests for multi-mode schema parsing"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Integration Test with tq-lineloss-report
|
||||
|
||||
**Files:**
|
||||
- Test: Generate skill from tq-lineloss-report source
|
||||
|
||||
**Goal:** Verify the enhanced template can generate a multi-mode script.
|
||||
|
||||
- [ ] **Step 1: Build the project**
|
||||
|
||||
```bash
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
Expected: Build succeeds.
|
||||
|
||||
- [ ] **Step 2: Create a test multi-mode scene-info JSON**
|
||||
|
||||
Create a test JSON file to simulate LLM output:
|
||||
|
||||
```json
|
||||
{
|
||||
"sceneId": "tq-lineloss-test",
|
||||
"sceneName": "台区线损测试报表",
|
||||
"sceneKind": "report_collection",
|
||||
"modes": [
|
||||
{
|
||||
"name": "month",
|
||||
"label": "月度报表",
|
||||
"condition": {"field": "period_mode", "operator": "equals", "value": "month"},
|
||||
"apiEndpoint": {
|
||||
"name": "月度线损查询",
|
||||
"url": "http://20.76.57.61:18080/gsllys/fourVerEightHor/fourVerEightHorLinelossRateList",
|
||||
"method": "POST",
|
||||
"contentType": "application/x-www-form-urlencoded"
|
||||
},
|
||||
"columnDefs": [["ORG_NAME", "供电单位"], ["YGDL", "供电量"], ["YYDL", "售电量"]],
|
||||
"requestTemplate": {"orgno": "${args.org_code}", "rows": 1000, "page": 1, "sidx": "ORG_NO", "sord": "asc"},
|
||||
"normalizeRules": {"type": "validate_all_columns", "filterNull": true},
|
||||
"responsePath": "content"
|
||||
},
|
||||
{
|
||||
"name": "week",
|
||||
"label": "周报表",
|
||||
"condition": {"field": "period_mode", "operator": "equals", "value": "week"},
|
||||
"apiEndpoint": {
|
||||
"name": "周线损查询",
|
||||
"url": "http://20.76.57.61:18080/gsllys/tqLinelossStatis/getYearMonWeekLinelossAnalysisList",
|
||||
"method": "POST",
|
||||
"contentType": "application/x-www-form-urlencoded"
|
||||
},
|
||||
"columnDefs": [["ORG_NAME", "供电单位"], ["LINE_LOSS_RATE", "线损率"]],
|
||||
"requestTemplate": {"orgno": "${args.org_code}", "tjzq": "week", "rows": 1000},
|
||||
"normalizeRules": {"type": "validate_required", "requiredFields": ["ORG_NAME", "LINE_LOSS_RATE"], "filterNull": true},
|
||||
"responsePath": "content"
|
||||
}
|
||||
],
|
||||
"defaultMode": "month",
|
||||
"modeSwitchField": "period_mode"
|
||||
}
|
||||
```
|
||||
|
||||
Save to `tmp_multi_mode_test.json`.
|
||||
|
||||
- [ ] **Step 3: Run generator with the multi-mode JSON**
|
||||
|
||||
```bash
|
||||
cargo run --bin sg_scene_generate -- --source-dir "examples/test-scene" --scene-id "tq-lineloss-test" --scene-name "台区线损测试" --output-root "tmp_multi_test" --scene-info-json "$(cat tmp_multi_mode_test.json)"
|
||||
```
|
||||
|
||||
Expected: Skill package generated without errors.
|
||||
|
||||
- [ ] **Step 4: Verify generated script syntax**
|
||||
|
||||
```bash
|
||||
node --check tmp_multi_test/skills/tq-lineloss-test/scripts/collect_tq_lineloss_test.js
|
||||
```
|
||||
|
||||
Expected: No syntax errors.
|
||||
|
||||
- [ ] **Step 5: Verify generated script has multi-mode logic**
|
||||
|
||||
Check that the generated script contains:
|
||||
- `detectMode()` function
|
||||
- `MODES` constant with mode configurations
|
||||
- `buildModeRequest()` function
|
||||
- `normalizeModeRows()` function
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "test: verify multi-mode template generates valid JavaScript"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Update Web UI to Display Mode Information
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/scene-generator/sg_scene_generator.html`
|
||||
|
||||
**Goal:** Add mode information display in the extraction preview panel.
|
||||
|
||||
- [ ] **Step 1: Add mode display section**
|
||||
|
||||
Add after the column defs display in the preview panel:
|
||||
|
||||
```html
|
||||
<div id="modes-preview" class="preview-section" style="display: none;">
|
||||
<h4>业务模式</h4>
|
||||
<div id="modes-list" class="preview-list"></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add JavaScript to populate mode info**
|
||||
|
||||
In the `showExtractionPreview` function, add:
|
||||
|
||||
```javascript
|
||||
// Show modes if present
|
||||
const modesSection = document.getElementById('modes-preview');
|
||||
const modesList = document.getElementById('modes-list');
|
||||
|
||||
if (data.modes && data.modes.length > 0) {
|
||||
modesSection.style.display = 'block';
|
||||
modesList.innerHTML = data.modes.map(mode => {
|
||||
const name = escapeHtml(mode.name || 'unknown');
|
||||
const label = escapeHtml(mode.label || '');
|
||||
const api = escapeHtml(mode.apiEndpoint?.url || '');
|
||||
return `<div class="preview-list-item">
|
||||
<strong>${name}</strong>${label ? ` (${label})` : ''}: ${api}
|
||||
</div>`;
|
||||
}).join('');
|
||||
} else {
|
||||
modesSection.style.display = 'none';
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Verify changes**
|
||||
|
||||
```bash
|
||||
node --check frontend/scene-generator/sg_scene_generator.html
|
||||
```
|
||||
|
||||
Note: HTML files can't be syntax-checked directly, just verify the server starts.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add frontend/scene-generator/sg_scene_generator.html
|
||||
git commit -m "feat(ui): add mode information display in extraction preview"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Self-Review Checklist
|
||||
|
||||
**1. Spec Coverage:**
|
||||
- [x] Multi-mode schema structs → Task 1
|
||||
- [x] Enhanced LLM prompt → Task 2
|
||||
- [x] Multi-mode template → Task 3
|
||||
- [x] Unit tests → Task 4
|
||||
- [x] Integration test → Task 5
|
||||
- [x] UI update → Task 6
|
||||
|
||||
**2. Placeholder Scan:**
|
||||
- No TBD, TODO, or placeholder text found
|
||||
- All code snippets are complete
|
||||
- All commands have expected output
|
||||
|
||||
**3. Type Consistency:**
|
||||
- `ModeConditionJson` field names match JSON schema
|
||||
- `ModeConfigJson` uses `apiEndpoint` (camelCase) matching JSON
|
||||
- `NormalizeRulesJson` uses `rules_type` with serde rename
|
||||
|
||||
---
|
||||
|
||||
## Execution Handoff
|
||||
|
||||
Plan complete and saved to `docs/superpowers/plans/2026-04-17-enhanced-llm-extraction-schema-plan.md`. Two execution options:
|
||||
|
||||
**1. Subagent-Driven (recommended)** - I dispatch a fresh subagent per task, review between tasks, fast iteration
|
||||
|
||||
**2. Inline Execution** - Execute tasks in this session using executing-plans, batch execution with checkpoints
|
||||
|
||||
Which approach?
|
||||
Reference in New Issue
Block a user