Files
claw/docs/superpowers/specs/2026-04-17-progressive-template-enhancement-design.md

334 lines
9.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 可在内网运行