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

9.9 KiB
Raw Blame History

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 $.ajaxfetch 可能遇到 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):

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

  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 可在内网运行