feat(rust): add --scene-info-json parameter for LLM extraction results
- Add SceneInfoJson struct with serde rename for camelCase compatibility - Add ApiEndpointJson and BusinessLogicJson support structs - Add scene_info_json field to GenerateSceneRequest (backward compatible) - Rename browser_script to browser_script_skeleton - Add new browser_script that delegates based on scene_info presence - Add browser_script_with_business_logic for enhanced script generation - Update CLI to accept --scene-info-json parameter - Update usage string to document new parameter Generated with [Qoder][https://qoder.com]
This commit is contained in:
@@ -2,7 +2,7 @@ use std::env;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use sgclaw::generated_scene::analyzer::SceneKind;
|
||||
use sgclaw::generated_scene::generator::{generate_scene_package, GenerateSceneRequest};
|
||||
use sgclaw::generated_scene::generator::{generate_scene_package, GenerateSceneRequest, SceneInfoJson};
|
||||
|
||||
fn main() {
|
||||
if let Err(err) = run() {
|
||||
@@ -13,6 +13,10 @@ fn main() {
|
||||
|
||||
fn run() -> Result<(), String> {
|
||||
let args = parse_args(env::args().skip(1))?;
|
||||
let scene_info: Option<SceneInfoJson> = args.scene_info_json
|
||||
.map(|json| serde_json::from_str(&json))
|
||||
.transpose()
|
||||
.map_err(|e| format!("Invalid scene-info-json: {}", e))?;
|
||||
let skill_root = generate_scene_package(GenerateSceneRequest {
|
||||
source_dir: args.source_dir,
|
||||
scene_id: args.scene_id,
|
||||
@@ -21,6 +25,7 @@ fn run() -> Result<(), String> {
|
||||
target_url: args.target_url,
|
||||
output_root: args.output_root,
|
||||
lessons_path: args.lessons_path,
|
||||
scene_info_json: scene_info,
|
||||
})
|
||||
.map_err(|err| err.to_string())?;
|
||||
|
||||
@@ -35,7 +40,8 @@ struct CliArgs {
|
||||
scene_kind: Option<SceneKind>,
|
||||
target_url: Option<String>,
|
||||
output_root: PathBuf,
|
||||
lessons_path: PathBuf,
|
||||
lessons_path: Option<PathBuf>,
|
||||
scene_info_json: Option<String>,
|
||||
}
|
||||
|
||||
fn parse_args(args: impl Iterator<Item = String>) -> Result<CliArgs, String> {
|
||||
@@ -46,6 +52,7 @@ fn parse_args(args: impl Iterator<Item = String>) -> Result<CliArgs, String> {
|
||||
let mut target_url = None;
|
||||
let mut output_root = None;
|
||||
let mut lessons_path = None;
|
||||
let mut scene_info_json = None;
|
||||
let mut pending_flag: Option<String> = None;
|
||||
|
||||
for arg in args {
|
||||
@@ -63,6 +70,7 @@ fn parse_args(args: impl Iterator<Item = String>) -> Result<CliArgs, String> {
|
||||
"--target-url" => target_url = Some(arg),
|
||||
"--output-root" => output_root = Some(PathBuf::from(arg)),
|
||||
"--lessons" => lessons_path = Some(PathBuf::from(arg)),
|
||||
"--scene-info-json" => scene_info_json = Some(arg),
|
||||
_ => return Err(format!("unsupported argument {flag}")),
|
||||
}
|
||||
continue;
|
||||
@@ -70,7 +78,7 @@ fn parse_args(args: impl Iterator<Item = String>) -> Result<CliArgs, String> {
|
||||
|
||||
match arg.as_str() {
|
||||
"--source-dir" | "--scene-id" | "--scene-name" | "--scene-kind" | "--target-url"
|
||||
| "--output-root" | "--lessons" => {
|
||||
| "--output-root" | "--lessons" | "--scene-info-json" => {
|
||||
pending_flag = Some(arg);
|
||||
}
|
||||
"--help" | "-h" => return Err(usage()),
|
||||
@@ -89,10 +97,11 @@ fn parse_args(args: impl Iterator<Item = String>) -> Result<CliArgs, String> {
|
||||
scene_kind,
|
||||
target_url,
|
||||
output_root: output_root.ok_or_else(usage)?,
|
||||
lessons_path: lessons_path.ok_or_else(usage)?,
|
||||
lessons_path,
|
||||
scene_info_json,
|
||||
})
|
||||
}
|
||||
|
||||
fn usage() -> String {
|
||||
"usage: sg_scene_generate --source-dir <scenario-dir> --scene-id <scene-id> --scene-name <display-name> [--scene-kind <report_collection|monitoring>] [--target-url <url>] --output-root <skill-staging-root> --lessons <lessons-toml>".to_string()
|
||||
"usage: sg_scene_generate --source-dir <scenario-dir> --scene-id <scene-id> --scene-name <display-name> [--scene-kind <report_collection|monitoring>] [--target-url <url>] --output-root <skill-staging-root> [--lessons <lessons-toml>] [--scene-info-json '<json>']".to_string()
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -5,7 +6,53 @@ use std::path::{Path, PathBuf};
|
||||
use crate::generated_scene::analyzer::{
|
||||
analyze_scene_source_with_hint, AnalyzeSceneError, SceneKind, SceneSourceAnalysis,
|
||||
};
|
||||
use crate::generated_scene::lessons::load_generation_lessons;
|
||||
use crate::generated_scene::lessons::{
|
||||
load_generation_lessons, GenerationLessons, BUILTIN_REPORT_COLLECTION_LESSONS,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ApiEndpointJson {
|
||||
pub name: String,
|
||||
pub url: String,
|
||||
#[serde(default)]
|
||||
pub method: String,
|
||||
#[serde(default)]
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct BusinessLogicJson {
|
||||
#[serde(default)]
|
||||
pub data_fetch: Option<String>,
|
||||
#[serde(default)]
|
||||
pub data_transform: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct SceneInfoJson {
|
||||
#[serde(rename = "sceneId")]
|
||||
pub scene_id: String,
|
||||
#[serde(rename = "sceneName")]
|
||||
pub scene_name: String,
|
||||
#[serde(rename = "sceneKind", default)]
|
||||
pub scene_kind: String,
|
||||
#[serde(rename = "sourceSystem", default)]
|
||||
pub source_system: Option<String>,
|
||||
#[serde(rename = "expectedDomain", default)]
|
||||
pub expected_domain: Option<String>,
|
||||
#[serde(rename = "targetUrl", default)]
|
||||
pub target_url: Option<String>,
|
||||
#[serde(rename = "apiEndpoints", default)]
|
||||
pub api_endpoints: Vec<ApiEndpointJson>,
|
||||
#[serde(rename = "staticParams", default)]
|
||||
pub static_params: HashMap<String, String>,
|
||||
#[serde(rename = "columnDefs", default)]
|
||||
pub column_defs: Vec<(String, String)>,
|
||||
#[serde(rename = "entryMethod", default)]
|
||||
pub entry_method: Option<String>,
|
||||
#[serde(rename = "businessLogic", default)]
|
||||
pub business_logic: Option<BusinessLogicJson>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GenerateSceneRequest {
|
||||
@@ -15,7 +62,8 @@ pub struct GenerateSceneRequest {
|
||||
pub scene_kind: Option<SceneKind>,
|
||||
pub target_url: Option<String>,
|
||||
pub output_root: PathBuf,
|
||||
pub lessons_path: PathBuf,
|
||||
pub lessons_path: Option<PathBuf>,
|
||||
pub scene_info_json: Option<SceneInfoJson>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -49,8 +97,16 @@ pub fn generate_scene_package(
|
||||
request: GenerateSceneRequest,
|
||||
) -> Result<PathBuf, GenerateSceneError> {
|
||||
let analysis = analyze_scene_source_with_hint(&request.source_dir, request.scene_kind.clone())?;
|
||||
let lessons =
|
||||
load_generation_lessons(&request.lessons_path).map_err(GenerateSceneError::new)?;
|
||||
let (lessons, lessons_source): (GenerationLessons, String) = match &request.lessons_path {
|
||||
Some(path) => (
|
||||
load_generation_lessons(path).map_err(GenerateSceneError::new)?,
|
||||
path.display().to_string(),
|
||||
),
|
||||
None => (
|
||||
GenerationLessons::default_report_collection(),
|
||||
BUILTIN_REPORT_COLLECTION_LESSONS.to_string(),
|
||||
),
|
||||
};
|
||||
if !lessons.routing.require_exact_suffix
|
||||
|| !lessons.bootstrap.require_expected_domain
|
||||
|| !lessons.bootstrap.require_target_url
|
||||
@@ -86,7 +142,7 @@ pub fn generate_scene_package(
|
||||
)?;
|
||||
write_file(
|
||||
&scripts_dir.join(format!("{tool_name}.js")),
|
||||
&browser_script(&request.scene_id, &analysis),
|
||||
&browser_script(&request.scene_id, &analysis, request.scene_info_json.as_ref()),
|
||||
)?;
|
||||
write_file(
|
||||
&scripts_dir.join(format!("{tool_name}.test.js")),
|
||||
@@ -97,7 +153,7 @@ pub fn generate_scene_package(
|
||||
&format!(
|
||||
"# Generation Lessons\n\nGenerated from `{}` with lessons `{}`.\n\nThis package is limited to report/collection browser_script scenes.\n",
|
||||
request.source_dir.display(),
|
||||
request.lessons_path.display()
|
||||
lessons_source
|
||||
),
|
||||
)?;
|
||||
|
||||
@@ -204,13 +260,166 @@ fn default_org_dictionary() -> &'static str {
|
||||
"#
|
||||
}
|
||||
|
||||
fn browser_script(scene_id: &str, _analysis: &SceneSourceAnalysis) -> String {
|
||||
fn browser_script_skeleton(scene_id: &str, _analysis: &SceneSourceAnalysis) -> String {
|
||||
format!(
|
||||
"function normalizePayload(payload) {{\n if (typeof payload === 'string') {{\n try {{ return JSON.parse(payload); }} catch (_) {{ return {{}}; }}\n }}\n return payload && typeof payload === 'object' ? payload : {{}};\n}}\n\nasync function buildBrowserEntrypointResult(args, deps = {{}}) {{\n const rows = typeof deps.collectRows === 'function'\n ? await deps.collectRows(args)\n : [{{\n org_label: args.org_label || '',\n org_code: args.org_code || '',\n period_mode: args.period_mode || '',\n period_value: args.period_value || '',\n value: ''\n }}];\n return {{\n type: 'report-artifact',\n report_name: '{}',\n status: rows.length > 0 ? 'ok' : 'empty',\n period: {{\n mode: args.period_mode,\n mode_code: args.period_mode_code,\n value: args.period_value,\n payload: normalizePayload(args.period_payload)\n }},\n org: {{ label: args.org_label, code: args.org_code }},\n column_defs: [\n ['org_label', '供电单位'],\n ['org_code', '供电单位编码'],\n ['period_mode', '统计周期类型'],\n ['period_value', '统计周期'],\n ['value', '采集值']\n ],\n columns: ['org_label', 'org_code', 'period_mode', 'period_value', 'value'],\n rows,\n counts: {{ detail_rows: rows.length }},\n partial_reasons: [],\n reasons: []\n }};\n}}\n\nif (typeof module !== 'undefined') {{\n module.exports = {{ buildBrowserEntrypointResult, normalizePayload }};\n}}\n\nif (typeof args !== 'undefined') {{\n return buildBrowserEntrypointResult(args);\n}}\n",
|
||||
scene_id
|
||||
)
|
||||
}
|
||||
|
||||
fn browser_script(scene_id: &str, analysis: &SceneSourceAnalysis, scene_info: Option<&SceneInfoJson>) -> String {
|
||||
match scene_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),
|
||||
}
|
||||
}
|
||||
|
||||
fn browser_script_with_business_logic(scene_id: &str, _analysis: &SceneSourceAnalysis, scene_info: &SceneInfoJson) -> String {
|
||||
let api_endpoints_json = serde_json::to_string_pretty(&scene_info.api_endpoints).unwrap_or_else(|_| "[]".to_string());
|
||||
let static_params_json = serde_json::to_string_pretty(&scene_info.static_params).unwrap_or_else(|_| "{}".to_string());
|
||||
let column_defs_json = serde_json::to_string_pretty(&scene_info.column_defs).unwrap_or_else(|_| "[]".to_string());
|
||||
|
||||
let column_names: Vec<&str> = scene_info.column_defs.iter().map(|(name, _)| name.as_str()).collect();
|
||||
let columns_json = serde_json::to_string(&column_names).unwrap_or_else(|_| "[]".to_string());
|
||||
|
||||
format!(
|
||||
r#"const API_ENDPOINTS = {api_endpoints_json};
|
||||
|
||||
const STATIC_PARAMS = {static_params_json};
|
||||
|
||||
const COLUMN_DEFS = {column_defs_json};
|
||||
|
||||
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 buildRequest(args, endpoint) {{
|
||||
const url = new URL(endpoint.url, window.location.origin);
|
||||
const params = {{ ...STATIC_PARAMS, ...args }};
|
||||
for (const [key, value] of Object.entries(params)) {{
|
||||
if (value !== undefined && value !== null) {{
|
||||
url.searchParams.set(key, String(value));
|
||||
}}
|
||||
}}
|
||||
return {{
|
||||
url: url.toString(),
|
||||
method: endpoint.method || 'GET',
|
||||
headers: {{ 'Content-Type': 'application/json' }}
|
||||
}};
|
||||
}}
|
||||
|
||||
function normalizeRows(rawData) {{
|
||||
if (!Array.isArray(rawData)) return [];
|
||||
return rawData.map((row, index) => {{
|
||||
const result = {{ _index: index }};
|
||||
for (const [key, label] of COLUMN_DEFS) {{
|
||||
result[key] = row[key] ?? '';
|
||||
}}
|
||||
return result;
|
||||
}});
|
||||
}}
|
||||
|
||||
function buildArtifact(args, rows) {{
|
||||
return {{
|
||||
type: 'report-artifact',
|
||||
report_name: '{scene_id}',
|
||||
status: rows.length > 0 ? 'ok' : 'empty',
|
||||
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: COLUMN_DEFS,
|
||||
columns: {columns_json},
|
||||
rows,
|
||||
counts: {{ detail_rows: rows.length }},
|
||||
partial_reasons: [],
|
||||
reasons: []
|
||||
}};
|
||||
}}
|
||||
|
||||
const defaultDeps = {{
|
||||
validatePageContext: async () => true,
|
||||
queryData: async (args) => {{
|
||||
const endpoint = API_ENDPOINTS[0];
|
||||
if (!endpoint) throw new Error('No API endpoint configured');
|
||||
const request = buildRequest(args, endpoint);
|
||||
const response = await fetch(request.url, {{
|
||||
method: request.method,
|
||||
headers: request.headers
|
||||
}});
|
||||
if (!response.ok) throw new Error(`HTTP ${{response.status}}: ${{response.statusText}}`);
|
||||
return response.json();
|
||||
}}
|
||||
}};
|
||||
|
||||
async function buildBrowserEntrypointResult(args, deps = defaultDeps) {{
|
||||
const validation = validateArgs(args);
|
||||
if (!validation.valid) {{
|
||||
return {{
|
||||
type: 'report-artifact',
|
||||
report_name: '{scene_id}',
|
||||
status: 'error',
|
||||
error: 'Validation failed: ' + validation.errors.join(', '),
|
||||
column_defs: COLUMN_DEFS,
|
||||
columns: {columns_json},
|
||||
rows: [],
|
||||
counts: {{ detail_rows: 0 }},
|
||||
partial_reasons: [],
|
||||
reasons: validation.errors
|
||||
}};
|
||||
}}
|
||||
|
||||
try {{
|
||||
const rawData = await (deps.queryData ? deps.queryData(args) : Promise.resolve([]));
|
||||
const rows = normalizeRows(rawData);
|
||||
return buildArtifact(args, rows);
|
||||
}} catch (error) {{
|
||||
return {{
|
||||
type: 'report-artifact',
|
||||
report_name: '{scene_id}',
|
||||
status: 'error',
|
||||
error: error.message,
|
||||
column_defs: COLUMN_DEFS,
|
||||
columns: {columns_json},
|
||||
rows: [],
|
||||
counts: {{ detail_rows: 0 }},
|
||||
partial_reasons: [],
|
||||
reasons: [error.message]
|
||||
}};
|
||||
}}
|
||||
}}
|
||||
|
||||
if (typeof module !== 'undefined') {{
|
||||
module.exports = {{ buildBrowserEntrypointResult, normalizePayload, validateArgs, buildRequest, normalizeRows, buildArtifact, API_ENDPOINTS, STATIC_PARAMS, COLUMN_DEFS }};
|
||||
}}
|
||||
|
||||
if (typeof args !== 'undefined') {{
|
||||
return buildBrowserEntrypointResult(args);
|
||||
}}
|
||||
"#,
|
||||
scene_id = scene_id,
|
||||
api_endpoints_json = api_endpoints_json,
|
||||
static_params_json = static_params_json,
|
||||
column_defs_json = column_defs_json,
|
||||
columns_json = columns_json
|
||||
)
|
||||
}
|
||||
|
||||
fn browser_script_test(tool_name: &str, _analysis: &SceneSourceAnalysis) -> String {
|
||||
format!(
|
||||
"const assert = require('assert');\nconst {{ buildBrowserEntrypointResult }} = require('./{}.js');\n\n(async () => {{\n const artifact = await buildBrowserEntrypointResult({{\n org_label: '国网兰州供电公司',\n org_code: '62401',\n period_mode: 'month',\n period_mode_code: '1',\n period_value: '2026-03',\n period_payload: '{{\"fdate\":\"2026-03\"}}'\n }});\n assert.equal(artifact.type, 'report-artifact');\n assert.ok(Array.isArray(artifact.column_defs));\n assert.equal(artifact.rows.length, 1);\n}})().catch((err) => {{\n console.error(err);\n process.exit(1);\n}});\n",
|
||||
|
||||
Reference in New Issue
Block a user