9.9 KiB
9.9 KiB
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 模板,使其生成的脚本能够:
- 正确处理 API URL(修复 bug)
- 同时支持 jQuery 和 fetch HTTP 客户端
- 提供完整的状态判定逻辑(blocked/error/partial/empty/ok)
- 支持多 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):
function buildRequest(args, endpoint) {
const url = new URL(endpoint.url, window.location.origin); // 错误!
// ...
}
修复后:
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 双支持
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. 完整状态判定
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. 增强入口函数
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
-
URL 构建测试
- 验证完整 URL 被正确传递
- 验证 GET/POST 方法正确处理
-
状态判定测试
- 测试 blocked → blockedReason 存在
- 测试 error → fatalError 存在
- 测试 partial → reasons 非空
- 测试 empty → rows 为空
- 测试 ok → rows 非空
-
HTTP 客户端测试
- Mock jQuery 环境,验证 $.ajax 被调用
- Mock fetch 环境,验证 fetch 被调用
- 验证错误处理
Integration Tests
-
端到端测试
- 使用 fixture 场景目录
- 运行深度分析 → 生成 skill
- 验证生成的脚本语法正确
- 验证生成的脚本可被 Node.js 加载
-
真实场景测试
- 使用 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
- URL 修复:
buildRequest不再依赖window.location.origin - jQuery 支持: 在有 jQuery 的页面优先使用
$.ajax - 状态完整: 支持 blocked/error/partial/empty/ok 五种状态
- 向后兼容: 无 scene_info 时仍生成骨架模板
- 可运行性: 生成的 marketing-zero-consumer-report 可在内网运行