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 {
|
||||
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)
|
||||
}
|
||||
@@ -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 {
|
||||
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