Files
claw/docs/superpowers/specs/2026-04-17-enhanced-llm-extraction-schema-design.md

16 KiB
Raw Blame History

增强 LLM 提取 Schema - 多模式业务逻辑自动化

Status: Draft Date: 2026-04-17 Author: Qoder

Problem Statement

当前 sg_scene_generate 自动生成的 skill 脚本与 Claude 手写的 skill 存在显著差距:

差距清单

差距类型 描述 严重程度
多模式支持 无法识别 month/week 等多模式场景,只生成单一逻辑 🔴
多 API 端点 定义多个 API 但只使用第一个 🔴
请求格式检测 默认 JSON body未检测 form-urlencoded 🟡
数据归一化 简单映射,无关键字段验证和空行过滤 🟡
参数标准化 无参数标准化处理,直接透传 🟡
分页参数 未提取和处理分页参数 (rows/page/sidx/sord) 🟡

对比分析

功能维度 tq-lineloss-report (手写) marketing-zero-consumer-report (自动)
模式切换 month/week 两套完整逻辑
API 端点 queryMonthData + queryWeekData 只用 API_ENDPOINTS[0]
列定义 MONTH/WEEK_COLUMN_DEFS 双套 单一 COLUMN_DEFS
请求构建 buildMonthRequest + buildWeekRequest 单一 buildRequest
请求格式 form-urlencoded application/json
数据验证 关键字段验证 + filter(Boolean) 无验证
参数标准化 normalized 对象 + 类型转换

Goal

增强 LLM 提取 Schema使其能够从 index.html 中自动识别:

  1. 多模式业务逻辑 (month/week 等)
  2. 模式切换条件 (如何判断使用哪个模式)
  3. 每个模式的专属配置 (API、列定义、请求格式、验证规则)
  4. 数据归一化规则 (关键字段、过滤条件)

Non-Goals

  • 不改变现有的两阶段架构 (LLM 提取 → Rust 渲染)
  • 不增加新的 CLI 参数
  • 不支持超过 2 种模式的复杂场景 (Phase 1)
  • 不处理认证/鉴权逻辑 (假设页面已登录)

Architecture

增强后的 Schema 结构

{
  "sceneId": "tq-lineloss-report",
  "sceneName": "台区线损报表",
  "sceneKind": "report_collection",

  "modes": [
    {
      "name": "month",
      "label": "月度报表",
      "condition": {
        "field": "period_mode",
        "operator": "equals",
        "value": "month"
      },
      "apiEndpoint": {
        "name": "月度线损查询",
        "url": "http://20.76.57.61:18080/gsllys/fourVerEightHor/fourVerEightHorLinelossRateList",
        "method": "POST",
        "contentType": "application/x-www-form-urlencoded"
      },
      "columnDefs": [
        ["ORG_NAME", "供电单位"],
        ["YGDL", "累计供电量"],
        ["YYDL", "累计售电量"],
        ["YXSL", "线损完成率(%)"],
        ["RAT_SCOPE", "线损率累计目标值"],
        ["BLANK3", "目标完成率"],
        ["BLANK2", "排行"]
      ],
      "requestTemplate": {
        "orgno": "${args.org_code}",
        "yn_flag": 0,
        "_search": false,
        "nd": "${Date.now()}",
        "rows": 1000,
        "page": 1,
        "sidx": "TO_NUMBER(ORG_NO)",
        "sord": "asc"
      },
      "normalizeRules": {
        "type": "validate_all_columns",
        "filterNull": true
      },
      "responsePath": "content"
    },
    {
      "name": "week",
      "label": "周报表",
      "condition": {
        "field": "period_mode",
        "operator": "equals",
        "value": "week"
      },
      "apiEndpoint": {
        "name": "周线损查询",
        "url": "http://20.76.57.61:18080/gsllys/tqLinelossStatis/getYearMonWeekLinelossAnalysisList",
        "method": "POST",
        "contentType": "application/x-www-form-urlencoded"
      },
      "columnDefs": [
        ["ORG_NAME", "供电单位"],
        ["LINE_LOSS_RATE", "综合线损率(%)"],
        ["PPQ", "供电量(Kwh)"],
        ["UPQ", "售电量(Kwh)"],
        ["LOSS_PQ", "损失电量(Kwh)"]
      ],
      "requestTemplate": {
        "orgno": "${args.org_code}",
        "tjzq": "week",
        "level": "00",
        "_search": false,
        "nd": "${Date.now()}",
        "rows": 1000,
        "page": 1,
        "sidx": "TO_NUMBER(ORG_NO)",
        "sord": "asc"
      },
      "normalizeRules": {
        "type": "validate_required",
        "requiredFields": ["ORG_NAME", "LINE_LOSS_RATE"],
        "filterNull": true
      },
      "responsePath": "content"
    }
  ],

  "defaultMode": "month",
  "modeSwitchField": "period_mode",

  "commonParams": {},
  "staticParams": {},

  "expectedDomain": "20.76.57.61",
  "targetUrl": "http://20.76.57.61:18080/gsllys"
}

Schema 字段说明

顶层字段

字段 类型 说明
modes Array 模式配置数组
defaultMode string 默认模式名称
modeSwitchField string 用于切换模式的参数字段名

Mode 配置

字段 类型 说明
name string 模式标识符
label string 模式显示名称
condition object 模式激活条件
apiEndpoint object 该模式的 API 配置
columnDefs Array 该模式的列定义
requestTemplate object 请求参数模板
normalizeRules object 数据归一化规则
responsePath string 响应数据路径 (如 content)

Condition 结构

{
  "field": "period_mode",      // 检查的字段
  "operator": "equals",        // 操作符: equals, not_equals, in, contains
  "value": "month"             // 比较值
}

NormalizeRules 结构

{
  "type": "validate_required",  // validate_all_columns | validate_required
  "requiredFields": ["ORG_NAME", "LINE_LOSS_RATE"],  // 仅 validate_required 需要
  "filterNull": true            // 是否过滤空值行
}

数据流

用户请求 (args)
    ↓
validateArgs() → 参数验证
    ↓
detectMode(args[modeSwitchField]) → 检测当前模式
    ↓
selectModeConfig(mode) → 选择模式配置
    ↓
buildRequest(args, modeConfig) → 构建该模式的请求
    ↓
queryData(request, modeConfig.apiEndpoint) → HTTP 请求
    ↓
extractResponse(response, modeConfig.responsePath) → 提取数据
    ↓
normalizeRows(data, modeConfig.normalizeRules) → 数据归一化
    ↓
buildArtifact({ mode, columnDefs, rows, ... }) → 构建输出

Implementation Details

1. 增强 LLM 提取 Prompt

修改 frontend/scene-generator/llm-client.js 中的 DEEP_SYSTEM_PROMPTbuildDeepAnalyzePrompt

关键 Prompt 增强:

分析 index.html 中的业务逻辑模式:

1. **模式识别**
   - 查找条件分支逻辑 (if/switch) 中基于 period_mode、reportType 等字段的分支
   - 识别不同分支对应的 API 端点、列定义、请求格式

2. **API 提取**
   - 提取 $.ajax/fetch 调用中的 URL、method、contentType
   - 识别请求参数构造方式 (JSON.stringify vs 对象展开)
   - 检测分页参数 (rows/page/sidx/sord)

3. **请求格式检测**
   - contentType: application/json → JSON body
   - contentType: application/x-www-form-urlencoded → 对象展开
   - 无显式 contentType → 检查 data 参数格式

4. **数据归一化**
   - 查找数据渲染/表格填充逻辑
   - 识别字段映射关系
   - 检测数据验证条件 (哪些字段不能为空)

5. **响应路径**
   - 识别数据在响应中的位置 (response.content / response.data / response)

2. 增强 Rust Schema 结构

修改 src/generated_scene/generator.rs

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ModeConditionJson {
    pub field: String,
    #[serde(default)]
    pub operator: String,  // equals, not_equals, in, contains
    pub value: serde_json::Value,
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct NormalizeRulesJson {
    #[serde(rename = "type")]
    pub rules_type: String,  // validate_all_columns, validate_required
    #[serde(default)]
    pub required_fields: Vec<String>,
    #[serde(default)]
    pub filter_null: bool,
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ApiEndpointEnhancedJson {
    pub name: String,
    pub url: String,
    #[serde(default)]
    pub method: String,
    #[serde(default)]
    pub content_type: Option<String>,
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ModeConfigJson {
    pub name: String,
    pub label: Option<String>,
    pub condition: ModeConditionJson,
    pub api_endpoint: ApiEndpointEnhancedJson,
    pub column_defs: Vec<(String, String)>,
    pub request_template: Option<serde_json::Value>,
    pub normalize_rules: Option<NormalizeRulesJson>,
    pub response_path: Option<String>,
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct SceneInfoEnhancedJson {
    #[serde(rename = "sceneId")]
    pub scene_id: String,
    #[serde(rename = "sceneName")]
    pub scene_name: String,

    // 新增:多模式支持
    pub modes: Vec<ModeConfigJson>,
    pub default_mode: Option<String>,
    pub mode_switch_field: Option<String>,

    // 向后兼容:单模式场景
    #[serde(rename = "apiEndpoints", default)]
    pub api_endpoints: Vec<ApiEndpointJson>,
    #[serde(rename = "columnDefs", default)]
    pub column_defs: Vec<(String, String)>,

    // 其他字段保持不变
    // ...
}

3. 增强模板生成逻辑

fn browser_script_with_modes(scene_id: &str, scene_info: &SceneInfoEnhancedJson) -> String {
    if scene_info.modes.is_empty() {
        // 向后兼容:使用现有单模式模板
        return browser_script_with_business_logic(scene_id, scene_info);
    }

    // 多模式模板
    generate_multi_mode_script(scene_id, scene_info)
}

fn generate_multi_mode_script(scene_id: &str, scene_info: &SceneInfoEnhancedJson) -> String {
    let modes_json = serde_json::to_string_pretty(&scene_info.modes).unwrap();
    let default_mode = scene_info.default_mode.as_deref().unwrap_or("month");

    format!(r#"
const REPORT_NAME = '{scene_id}';
const MODES = {modes_json};
const DEFAULT_MODE = '{default_mode}';
const MODE_SWITCH_FIELD = '{mode_switch_field}';

function detectMode(args) {{
  const modeValue = args[MODE_SWITCH_FIELD] || DEFAULT_MODE;
  return MODES.find(m => m.condition.value === modeValue) || MODES[0];
}}

function buildModeRequest(args, mode) {{
  const endpoint = mode.api_endpoint;
  const template = mode.request_template || {{}};

  // 根据 contentType 构建请求
  const contentType = endpoint.content_type || 'application/json';
  const url = endpoint.url;
  const method = endpoint.method || 'POST';

  // 解析模板,替换变量
  let body;
  if (contentType === 'application/x-www-form-urlencoded') {{
    body = {{ ...template }};
    // 替换模板变量
    for (const [key, value] of Object.entries(body)) {{
      if (typeof value === 'string' && value.startsWith('${{') && value.endsWith('}}')) {{
        const expr = value.slice(2, -1);
        body[key] = eval(expr);
      }}
    }}
    body.orgno = args.org_code;
  }} else {{
    body = JSON.stringify({{ ...template, ...args }});
  }}

  return {{ url, method, headers: {{ 'Content-Type': contentType }}, body }};
}}

function normalizeModeRows(data, mode) {{
  const rules = mode.normalize_rules || {{ type: 'validate_all_columns' }};
  const columns = mode.column_defs.map(([key]) => key);

  if (!Array.isArray(data)) return [];

  return data.map(row => {{
    const result = {{}};
    for (const key of columns) {{
      result[key] = row[key] ?? '';
    }}
    return result;
  }}).filter(row => {{
    if (!rules.filter_null) return true;

    if (rules.type === 'validate_required') {{
      return rules.required_fields.every(f => row[f] !== '');
    }}
    return columns.every(k => row[k] !== '');
  }});
}}

async function queryModeData(args, mode) {{
  const endpoint = mode.api_endpoint;
  const request = buildModeRequest(args, mode);

  // jQuery 优先
  if (typeof $ !== 'undefined' && typeof $.ajax === 'function') {{
    const contentType = endpoint.content_type || 'application/json';
    return new Promise((resolve, reject) => {{
      $.ajax({{
        url: request.url,
        type: request.method,
        data: request.body,
        contentType: contentType,
        dataType: 'json',
        success: resolve,
        error: (xhr, status, err) => reject(new Error(
          `API failed (${{xhr.status}}): ${{err}}`
        ))
      }});
    }});
  }}

  // fetch fallback
  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');
}}

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 mode = detectMode(args);

  // 4. 数据获取
  const reasons = [];
  let rawData = null;
  try {{
    rawData = await queryModeData(args, mode);
  }} catch (error) {{
    return buildArtifact({{
      status: 'error',
      fatalError: error.message,
      reasons: ['api_query_failed:' + error.message],
      rows: [],
      args
    }});
  }}

  // 5. 提取响应数据
  const responsePath = mode.response_path || '';
  let data = rawData;
  if (responsePath && rawData) {{
    data = rawData[responsePath] || rawData;
  }}

  // 6. 数据归一化
  const rows = normalizeModeRows(data, mode);
  if (rows.length === 0 && Array.isArray(data) && data.length > 0) {{
    reasons.push('row_normalization_partial');
  }}

  // 7. 构建 Artifact
  return buildArtifact({{
    reasons,
    rows,
    args,
    columnDefs: mode.column_defs,
    columns: mode.column_defs.map(([key]) => key)
  }});
}}
"#, scene_id = scene_id, modes_json = modes_json, default_mode = default_mode, mode_switch_field = scene_info.mode_switch_field.as_deref().unwrap_or("period_mode"))
}

Testing Strategy

单元测试

  1. Schema 解析测试

    • 测试多模式 JSON 正确解析
    • 测试向后兼容(无 modes 字段时退化)
  2. 模式检测测试

    • 测试 detectMode() 根据参数正确选择模式
  3. 请求构建测试

    • 测试 form-urlencoded 格式正确
    • 测试 JSON 格式正确
    • 测试模板变量替换
  4. 数据归一化测试

    • 测试 validate_required 类型
    • 测试 validate_all_columns 类型
    • 测试空行过滤

集成测试

  1. 端到端测试

    • 使用 tq-lineloss-report 源场景
    • 验证生成的脚本与手写版本功能一致
  2. 回归测试

    • 验证单模式场景仍正常工作
    • 验证现有测试用例通过

Migration Path

Phase 1: Schema 增强

  1. 增强 Rust schema 结构
  2. 更新 LLM 提取 prompt
  3. 验证 schema 解析正确

Phase 2: 模板实现

  1. 实现多模式模板
  2. 实现请求格式检测
  3. 实现数据归一化规则

Phase 3: 测试验证

  1. 使用 tq-lineloss-report 源场景测试
  2. 对比生成代码与手写代码
  3. 修复差异

Risks and Mitigations

风险 影响 缓解措施
LLM 提取不准确 生成代码不可用 提供 few-shot 示例,增加验证步骤
模式条件复杂 无法正确切换 Phase 1 只支持 equals 操作符
请求模板变量 表达能力有限 支持常用表达式,复杂场景用 lessons 补充
向后兼容 现有场景受影响 无 modes 时使用旧模板

Success Criteria

  1. 多模式支持:能够生成 month/week 双模式脚本
  2. 请求格式正确:自动检测 form-urlencoded vs JSON
  3. 数据验证:支持关键字段验证和空行过滤
  4. 向后兼容:单模式场景不受影响
  5. 代码质量:生成的代码与手写 tq-lineloss-report 功能对等