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:
木炎
2026-04-17 13:10:21 +08:00
parent a325add167
commit 42eb716b7e

View File

@@ -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",