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