feat(generator): add multi-mode template for mode-aware script generation
Add browser_script_with_modes function that generates JavaScript with: - detectMode() to select mode based on args - buildModeRequest() with content-type handling (JSON/form-urlencoded) - normalizeModeRows() with validation rules - queryModeData() with jQuery + fetch dual HTTP client Modify browser_script() to check for modes first before falling back to business_logic or skeleton templates. 🤖 Generated with [Qoder][https://qoder.com]
This commit is contained in:
@@ -336,6 +336,9 @@ fn browser_script_skeleton(scene_id: &str, _analysis: &SceneSourceAnalysis) -> S
|
|||||||
|
|
||||||
fn browser_script(scene_id: &str, analysis: &SceneSourceAnalysis, scene_info: Option<&SceneInfoJson>) -> String {
|
fn browser_script(scene_id: &str, analysis: &SceneSourceAnalysis, scene_info: Option<&SceneInfoJson>) -> String {
|
||||||
match scene_info {
|
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() => {
|
Some(info) if !info.api_endpoints.is_empty() || !info.column_defs.is_empty() => {
|
||||||
browser_script_with_business_logic(scene_id, analysis, info)
|
browser_script_with_business_logic(scene_id, analysis, info)
|
||||||
}
|
}
|
||||||
@@ -542,6 +545,249 @@ if (typeof args !== 'undefined') {{
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
fn browser_script_test(tool_name: &str, _analysis: &SceneSourceAnalysis) -> String {
|
fn browser_script_test(tool_name: &str, _analysis: &SceneSourceAnalysis) -> String {
|
||||||
format!(
|
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",
|
"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