docs: add progressive template enhancement design spec
This commit is contained in:
@@ -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 可在内网运行
|
||||||
Reference in New Issue
Block a user