docs: add progressive template enhancement design spec

This commit is contained in:
木炎
2026-04-17 11:16:11 +08:00
parent 74c42af717
commit 78a36a73b4

View File

@@ -0,0 +1,333 @@
# 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 可在内网运行