diff --git a/docs/superpowers/specs/2026-04-17-progressive-template-enhancement-design.md b/docs/superpowers/specs/2026-04-17-progressive-template-enhancement-design.md new file mode 100644 index 0000000..4c35ca7 --- /dev/null +++ b/docs/superpowers/specs/2026-04-17-progressive-template-enhancement-design.md @@ -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 可在内网运行