# Progressive Browser Script Template Enhancement > **Status:** Draft > **Date:** 2026-04-17 > **Author:** Qoder ## Problem Statement 当前自动生成的 `browser_script_with_business_logic` 模板存在以下问题: ### 问题列表 | 问题 | 影响 | 严重程度 | |------|------|----------| | URL 构建错误 | `new URL(endpoint.url, window.location.origin)` 会错误地基于当前页面 URL | 高 | | 缺少 jQuery 支持 | 内网页面通常使用 jQuery `$.ajax`,fetch 可能遇到 CORS 问题 | 高 | | 状态判定简单 | 只有 ok/error 两种状态,缺少 blocked/partial/empty 细分 | 中 | | 缺少多端点支持 | 只有一个 `queryData` 方法,无法处理多 API 场景 | 中 | ### 对比 tq-lineloss-report | 功能 | tq-lineloss (完整) | 当前模板 (骨架) | |------|-------------------|-----------------| | HTTP 客户端 | jQuery $.ajax + 错误处理 | 仅 fetch | | URL 处理 | 硬编码完整 URL | 错误的 URL 构建 | | 状态判定 | determineArtifactStatus 函数 | 简单三元表达式 | | API 端点 | 多个端点方法 | 单一 queryData | ## Goal 增强 `browser_script_with_business_logic` 模板,使其生成的脚本能够: 1. 正确处理 API URL(修复 bug) 2. 同时支持 jQuery 和 fetch HTTP 客户端 3. 提供完整的状态判定逻辑(blocked/error/partial/empty/ok) 4. 支持多 API 端点场景 ## Non-Goals - 不改变 LLM 提取逻辑(本次仅增强 Rust 模板) - 不增加新的 CLI 参数 - 不改变现有 API 契约 - 不支持自动生成 tq-lineloss 的"月/周两套列定义"模式(需要 LLM 增强) ## Architecture ### 模板结构 ``` browser_script_with_business_logic() ├── 常量定义 │ ├── API_ENDPOINTS (from LLM) │ ├── STATIC_PARAMS (from LLM) │ └── COLUMN_DEFS (from LLM) ├── 工具函数 │ ├── normalizePayload() │ ├── pickFirstNonEmpty() │ └── isNonEmptyString() ├── 参数验证 │ └── validateArgs() - 增强版 ├── 请求构建 │ ├── buildRequest() - 修复 URL │ └── buildRequestBody() - 新增 ├── HTTP 客户端 │ ├── defaultDeps.queryData() - jQuery 优先 │ └── fetchFallback() - 新增 ├── 数据处理 │ └── normalizeRows() ├── 状态判定 │ └── determineArtifactStatus() - 新增 ├── Artifact 构建 │ └── buildArtifact() - 增强版 └── 入口函数 └── buildBrowserEntrypointResult() ``` ### 数据流 ``` 用户请求 (args) ↓ validateArgs() → 参数验证 ↓ (blocked if invalid) validatePageContext() → 页面上下文验证 ↓ (blocked if mismatch) buildRequest() → 构建请求参数 ↓ queryData() → HTTP 请求 (jQuery/fetch) ↓ normalizeRows() → 数据归一化 ↓ determineArtifactStatus() → 状态判定 ↓ buildArtifact() → 构建 artifact ``` ## Implementation Details ### 1. 修复 URL 构建 **当前代码 (有 bug)**: ```javascript function buildRequest(args, endpoint) { const url = new URL(endpoint.url, window.location.origin); // 错误! // ... } ``` **修复后**: ```javascript function buildRequest(args, endpoint) { // 直接使用完整 URL,不基于 window.location.origin const url = endpoint.url; const method = endpoint.method || 'POST'; const headers = { 'Content-Type': 'application/json' }; const body = JSON.stringify({ ...STATIC_PARAMS, ...args }); return { url, method, headers, body }; } ``` ### 2. jQuery + fetch 双支持 ```javascript 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 queryData(args) { const endpoint = API_ENDPOINTS[0]; if (!endpoint) throw new Error('No API endpoint configured'); const request = buildRequest(args, endpoint); // 优先使用 jQuery (内网页面通常有) if (typeof $ !== 'undefined' && typeof $.ajax === 'function') { return new Promise((resolve, reject) => { $.ajax({ url: request.url, type: request.method, data: request.body, contentType: 'application/json', dataType: 'json', success: resolve, error: (xhr, status, err) => reject(new Error( `API failed (${xhr.status}): ${err} | body=${(xhr.responseText || '').substring(0, 200)}` )) }); }); } // Fallback: fetch API 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)'); } }; ``` ### 3. 完整状态判定 ```javascript 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 }) { 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: COLUMN_DEFS, 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))) }; } ``` ### 4. 增强入口函数 ```javascript async function buildBrowserEntrypointResult(args, deps = defaultDeps) { // 1. 参数验证 const validation = validateArgs(args); if (!validation.valid) { return buildArtifact({ status: 'blocked', blockedReason: 'validation_failed', reasons: validation.errors, rows: [], args }); } // 2. 页面上下文验证 const pageValidation = typeof deps.validatePageContext === 'function' ? deps.validatePageContext(args) : { ok: true }; if (!pageValidation?.ok) { return buildArtifact({ status: 'blocked', blockedReason: pageValidation?.reason || 'page_context_mismatch', reasons: [pageValidation?.reason || 'page_context_mismatch'], rows: [], args }); } // 3. 数据获取 const reasons = []; let rawData = null; try { rawData = await (deps.queryData ? deps.queryData(args) : Promise.resolve([])); } catch (error) { return buildArtifact({ status: 'error', fatalError: error.message, reasons: ['api_query_failed:' + error.message], rows: [], args }); } // 4. 数据归一化 const rows = normalizeRows(rawData); if (rows.length === 0 && Array.isArray(rawData) && rawData.length > 0) { reasons.push('row_normalization_partial'); } // 5. 构建 Artifact return buildArtifact({ reasons, rows, args }); } ``` ### 5. Rust 模板变更 修改 `src/generated_scene/generator.rs` 中的 `browser_script_with_business_logic` 函数,将上述 JavaScript 模板硬编码进去。 关键改动: - 替换 `buildRequest` 函数(修复 URL bug) - 替换 `defaultDeps` 对象(添加 jQuery 支持) - 添加 `determineArtifactStatus` 函数 - 增强 `buildArtifact` 函数 - 增强 `buildBrowserEntrypointResult` 函数 ## Testing Strategy ### Unit Tests 1. **URL 构建测试** - 验证完整 URL 被正确传递 - 验证 GET/POST 方法正确处理 2. **状态判定测试** - 测试 blocked → blockedReason 存在 - 测试 error → fatalError 存在 - 测试 partial → reasons 非空 - 测试 empty → rows 为空 - 测试 ok → rows 非空 3. **HTTP 客户端测试** - Mock jQuery 环境,验证 $.ajax 被调用 - Mock fetch 环境,验证 fetch 被调用 - 验证错误处理 ### Integration Tests 1. **端到端测试** - 使用 fixture 场景目录 - 运行深度分析 → 生成 skill - 验证生成的脚本语法正确 - 验证生成的脚本可被 Node.js 加载 2. **真实场景测试** - 使用 marketing-zero-consumer-report 源场景 - 重新生成 skill - 验证脚本可运行(需要内网环境) ## Migration Path ### Phase 1: 修复 Bug - 修复 URL 构建问题 - 保持其他逻辑不变 - 验证现有场景不受影响 ### Phase 2: 增强 HTTP 客户端 - 添加 jQuery 支持 - 保留 fetch 作为 fallback - 验证两种方式都能工作 ### Phase 3: 完善状态判定 - 添加 determineArtifactStatus - 增强 buildArtifact - 验证各种状态场景 ## Risks and Mitigations | 风险 | 影响 | 缓解措施 | |------|------|----------| | 模板过大 | 维护困难 | 分段组织,添加注释 | | jQuery 全局变量检查 | 可能误判 | 同时检查 $ 和 $.ajax | | 状态判定过于严格 | 部分场景不兼容 | 提供配置选项 | | 向后兼容 | 现有 skill 可能受影响 | 仅修改有 scene_info 的场景 | ## Success Criteria 1. **URL 修复**: `buildRequest` 不再依赖 `window.location.origin` 2. **jQuery 支持**: 在有 jQuery 的页面优先使用 `$.ajax` 3. **状态完整**: 支持 blocked/error/partial/empty/ok 五种状态 4. **向后兼容**: 无 scene_info 时仍生成骨架模板 5. **可运行性**: 生成的 marketing-zero-consumer-report 可在内网运行