/**
 * skill_runtime.js - 詹老师的AIPROS Skill运行时核心
 * 
 * 职责：解析并执行Skill，管理工具调用，协调AI交互
 * 主路径：插件内置运行时 → LLM API（OpenAI兼容接口，支持阿里云/智谱/DeepSeek/Kimi等）
 */

// ==================== Skill解析器 ====================

function cleanYamlScalar(value = '') {
  return String(value || '')
    .trim()
    .replace(/^["'“”‘’]+|["'“”‘’]+$/g, '')
    .trim();
}

/**
 * 解析Markdown格式的Skill
 * 支持两种格式：
 * 1. YAML frontmatter 格式（--- 包裹的 name/description 头部）
 * 2. 简单 Markdown 格式（# 标题 + ## 章节）
 * @param {string} content - Skill原始内容
 * @returns {Object} 解析后的Skill对象
 */
function parseSkill(content) {
  const skill = {
    name: '',
    description: '',
    role: '',
    tools: [],
    workflow: [],
    prompt: ''
  };

  // 优先尝试 YAML frontmatter 格式（--- 包裹）
  const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n/);
  if (fmMatch) {
    const fm = fmMatch[1];
    // 解析 name
    const nameMatch = fm.match(/^name:\s*(.+)$/m);
    if (nameMatch) skill.name = cleanYamlScalar(nameMatch[1]);
    // 解析 description（支持多行 | 块和单行）
    const descBlockMatch = fm.match(/^description:\s*\|\s*\n([\s\S]*?)(?=\n\S|^\n---)/m);
    const descInlineMatch = fm.match(/^description:\s*['"]?(.+?)['"]?\s*$/m);
    if (descBlockMatch) {
      skill.description = descBlockMatch[1].replace(/^\s+/gm, ' ').trim();
    } else if (descInlineMatch) {
      skill.description = cleanYamlScalar(descInlineMatch[1]);
    }
    // description_zh 作为中文描述补充
    const descZhMatch = fm.match(/^description_zh:\s*["'](.+?)["']\s*$/m);
    if (descZhMatch && !skill.description) {
      skill.description = cleanYamlScalar(descZhMatch[1]);
    }
    // frontmatter 之后的内容作为完整 prompt
    const bodyContent = content.slice(fmMatch[0].length).trim();
    skill.prompt = bodyContent;
    // 从 body 中提取角色（## 角色定义 / ## 你的角色 / ## 角色扮演规则）
    const roleMatch = bodyContent.match(/^##\s+(?:角色|角色定义|你的角色|角色扮演规则)[^\n]*\n+([^#]+)/m);
    if (roleMatch) {
      skill.role = roleMatch[1].trim().split('\n')[0];
    }
    // 从 body 中提取工具
    const toolsMatch = bodyContent.match(/^##\s+(?:工具|可用工具|可用工具列表)[^\n]*\n+([^#]+)/m);
    if (toolsMatch) {
      const toolsText = toolsMatch[1];
      const toolMatches = toolsText.matchAll(/-\s*(\w+(?:\.\w+)*):?\s*(.+)/g);
      for (const m of toolMatches) {
        skill.tools.push({ name: m[1].trim(), description: m[2].trim() });
      }
    }
    // 从 body 中提取工作流
    const workflowMatch = bodyContent.match(/^##\s+(?:工作流|工作流程|Workflow)[^\n]*\n+([^#]+)/m);
    if (workflowMatch) {
      const steps = workflowMatch[1].split('\n').filter(l => l.trim());
      skill.workflow = steps.map(s => s.replace(/^-\s*/, '').trim());
    }
    return skill;
  }

  // 兜底：简单 Markdown 格式解析
  // 提取标题
  const titleMatch = content.match(/^#\s+(.+)$/m);
  if (titleMatch) skill.name = titleMatch[1].trim();

  // 提取描述
  const descMatch = content.match(/^##\s+描述\s*\n+([^#]+)/m);
  if (descMatch) skill.description = descMatch[1].trim();

  // 提取角色定义
  const roleMatch = content.match(/^##\s+角色\s*\n+([^#]+)/m);
  if (roleMatch) skill.role = roleMatch[1].trim();

  // 提取工具定义
  const toolsMatch = content.match(/^##\s+工具\s*\n+([^#]+)/m);
  if (toolsMatch) {
    const toolsText = toolsMatch[1];
    const toolMatches = toolsText.matchAll(/-\s*(\w+(?:\.\w+)*):?\s*(.+)/g);
    for (const match of toolMatches) {
      skill.tools.push({
        name: match[1].trim(),
        description: match[2].trim()
      });
    }
  }

  // 提取工作流
  const workflowMatch = content.match(/^##\s+工作流\s*\n+([^#]+)/m);
  if (workflowMatch) {
    const steps = workflowMatch[1].split('\n').filter(l => l.trim());
    skill.workflow = steps.map(s => s.replace(/^-\s*/, '').trim());
  }

  // 提取主提示词（剩余内容）
  const promptMatch = content.match(/^##\s+提示词\s*\n+([\s\S]+)$/m);
  if (promptMatch) {
    skill.prompt = promptMatch[1].trim();
  } else {
    // 如果没有提示词章节，取角色+描述作为提示词
    skill.prompt = `${skill.role}\n\n${skill.description}`.trim();
  }

  return skill;
}

// ==================== 内置 SQL 查询引擎 ====================

function _isPlainObject(value) {
  return value && typeof value === 'object' && !Array.isArray(value);
}

function _safeSqlName(value, fallback = 'col') {
  const cleaned = String(value || '')
    .trim()
    .replace(/[^\w\u4e00-\u9fa5]+/g, '_')
    .replace(/^_+|_+$/g, '');
  return cleaned || fallback;
}

function _splitCommaAware(value = '') {
  const parts = [];
  let buf = '';
  let depth = 0;
  let quote = '';
  for (const ch of String(value)) {
    if (quote) {
      buf += ch;
      if (ch === quote) quote = '';
      continue;
    }
    if (ch === '"' || ch === "'") {
      quote = ch;
      buf += ch;
      continue;
    }
    if (ch === '(') depth++;
    if (ch === ')') depth = Math.max(0, depth - 1);
    if (ch === ',' && depth === 0) {
      parts.push(buf.trim());
      buf = '';
      continue;
    }
    buf += ch;
  }
  if (buf.trim()) parts.push(buf.trim());
  return parts;
}

function _sqlLiteral(value) {
  const raw = String(value ?? '').trim();
  if ((raw.startsWith("'") && raw.endsWith("'")) || (raw.startsWith('"') && raw.endsWith('"'))) {
    return raw.slice(1, -1);
  }
  if (/^-?\d+(?:\.\d+)?$/.test(raw)) return Number(raw);
  return raw;
}

function _toNumber(value) {
  if (typeof value === 'number') return value;
  const cleaned = String(value ?? '').replace(/[,，%\s]/g, '');
  const num = Number(cleaned);
  return Number.isFinite(num) ? num : 0;
}

function _normalizeSqlRows(rows) {
  if (!Array.isArray(rows)) return [];
  if (rows.length === 0) return [];
  if (Array.isArray(rows[0])) {
    const matrix = rows.filter(r => Array.isArray(r));
    if (!matrix.length) return [];
    const first = matrix[0].map((h, i) => _safeSqlName(h, `col_${i + 1}`));
    const hasHeader = first.some((h, i) => h && !/^[-+]?\d+(?:\.\d+)?$/.test(String(matrix[0][i] || '').trim()));
    const headers = hasHeader ? first : matrix[0].map((_, i) => `col_${i + 1}`);
    const dataRows = hasHeader ? matrix.slice(1) : matrix;
    return dataRows.map(row => {
      const out = {};
      headers.forEach((h, i) => { out[h] = row[i] ?? ''; });
      return out;
    });
  }
  if (_isPlainObject(rows[0])) {
    return rows.map(row => {
      const out = {};
      Object.entries(row || {}).forEach(([key, val]) => {
        out[_safeSqlName(key)] = val;
      });
      return out;
    });
  }
  return rows.map((value, idx) => ({ idx: idx + 1, value }));
}

function normalizeSqlTables(tables = {}) {
  const normalized = {};
  if (!_isPlainObject(tables)) return normalized;
  Object.entries(tables).forEach(([name, rows]) => {
    const tableName = _safeSqlName(name, 'table');
    normalized[tableName] = _normalizeSqlRows(rows);
  });
  return normalized;
}

function buildContextSqlTables(context = {}) {
  const ctx = context.pageContext || context || {};
  const tables = {};
  if (_isPlainObject(ctx.fields)) {
    tables.page_fields = Object.entries(ctx.fields).map(([key, value]) => ({
      key,
      value: typeof value === 'object' ? JSON.stringify(value) : String(value ?? '')
    }));
  }
  const text = ctx.pageText || ctx.text || ctx.selectCopyText || '';
  if (text) {
    tables.page_text_lines = String(text)
      .split(/\n+/)
      .map(line => line.trim())
      .filter(Boolean)
      .slice(0, 1000)
      .map((line, idx) => ({ line_no: idx + 1, text: line, length: line.length }));
  }
  if (Array.isArray(ctx.tableData)) {
    ctx.tableData.forEach((table, idx) => {
      const rows = _normalizeSqlRows(table);
      if (rows.length) tables[`page_table_${idx + 1}`] = rows;
    });
  }
  if (Array.isArray(ctx.headings)) {
    tables.page_headings = ctx.headings.map((h, idx) => ({
      idx: idx + 1,
      level: h.level || '',
      text: h.text || String(h)
    }));
  }
  if (Array.isArray(ctx.links)) {
    tables.page_links = ctx.links.map((link, idx) => ({
      idx: idx + 1,
      text: link.text || '',
      url: link.url || link.href || ''
    }));
  }
  if (Array.isArray(context.searchResults)) {
    tables.search_results = context.searchResults.map((r, idx) => ({
      idx: idx + 1,
      title: r.title || '',
      url: r.url || '',
      snippet: r.snippet || ''
    }));
  }
  return tables;
}

function describeSqlTables(tables = {}) {
  return Object.entries(tables).map(([name, rows]) => ({
    name,
    rowCount: rows.length,
    columns: rows[0] ? Object.keys(rows[0]) : []
  }));
}

function _compileWhere(whereText) {
  if (!whereText) return () => true;
  const clauses = String(whereText).split(/\s+and\s+/i).map(s => s.trim()).filter(Boolean);
  const tests = clauses.map(clause => {
    const m = clause.match(/^([\w\u4e00-\u9fa5_]+)\s*(=|!=|<>|>=|<=|>|<|like)\s*(.+)$/i);
    if (!m) throw new Error(`暂不支持的 WHERE 条件: ${clause}`);
    const [, col, opRaw, rawValue] = m;
    const op = opRaw.toLowerCase();
    const expected = _sqlLiteral(rawValue);
    return (row) => {
      const actual = row[col];
      if (op === 'like') {
        const pattern = String(expected).replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/%/g, '.*');
        return new RegExp(`^${pattern}$`, 'i').test(String(actual ?? ''));
      }
      if (['>', '>=', '<', '<='].includes(op)) {
        const a = _toNumber(actual);
        const b = _toNumber(expected);
        if (op === '>') return a > b;
        if (op === '>=') return a >= b;
        if (op === '<') return a < b;
        return a <= b;
      }
      if (op === '=' ) return String(actual ?? '') === String(expected ?? '');
      return String(actual ?? '') !== String(expected ?? '');
    };
  });
  return (row) => tests.every(fn => fn(row));
}

function _parseSelectItem(item) {
  const text = String(item).trim();
  if (text === '*') return { type: 'star', alias: '*' };
  const agg = text.match(/^(count|sum|avg|min|max)\s*\(\s*(\*|[\w\u4e00-\u9fa5_]+)\s*\)(?:\s+as\s+([\w\u4e00-\u9fa5_]+))?$/i);
  if (agg) {
    const fn = agg[1].toLowerCase();
    const col = agg[2];
    return { type: 'aggregate', fn, col, alias: agg[3] || (fn === 'count' ? 'count' : `${fn}_${col}`) };
  }
  const plain = text.match(/^([\w\u4e00-\u9fa5_]+)(?:\s+as\s+([\w\u4e00-\u9fa5_]+))?$/i);
  if (!plain) throw new Error(`暂不支持的 SELECT 字段: ${item}`);
  return { type: 'column', col: plain[1], alias: plain[2] || plain[1] };
}

function _aggregateValue(rows, item) {
  if (item.fn === 'count') return rows.length;
  const nums = rows.map(row => _toNumber(row[item.col]));
  if (item.fn === 'sum') return nums.reduce((a, b) => a + b, 0);
  if (item.fn === 'avg') return nums.length ? nums.reduce((a, b) => a + b, 0) / nums.length : 0;
  if (item.fn === 'min') return nums.length ? Math.min(...nums) : 0;
  if (item.fn === 'max') return nums.length ? Math.max(...nums) : 0;
  return null;
}

function _sortRows(rows, orderText) {
  if (!orderText) return rows;
  const parts = String(orderText).trim().split(/\s+/);
  const col = parts[0];
  const dir = (parts[1] || 'asc').toLowerCase() === 'desc' ? -1 : 1;
  return rows.slice().sort((a, b) => {
    const av = a[col];
    const bv = b[col];
    const an = _toNumber(av);
    const bn = _toNumber(bv);
    if (String(av ?? '').match(/^-?[\d,.% ]+$/) && String(bv ?? '').match(/^-?[\d,.% ]+$/)) {
      return (an - bn) * dir;
    }
    return String(av ?? '').localeCompare(String(bv ?? ''), 'zh-CN') * dir;
  });
}

function runMiniSql({ query, tables = {} } = {}) {
  const normalizedTables = normalizeSqlTables(tables);
  const availableTables = describeSqlTables(normalizedTables);
  if (!query || !String(query).trim()) {
    return { ok: true, query: '', availableTables, columns: [], rows: [], rowCount: 0 };
  }

  const sql = String(query).trim().replace(/;$/, '');
  const main = sql.match(/^select\s+([\s\S]+?)\s+from\s+([`"\w\u4e00-\u9fa5_-]+)([\s\S]*)$/i);
  if (!main) throw new Error('仅支持 SELECT ... FROM ... 查询');
  const selectText = main[1].trim();
  const tableName = main[2].replace(/^[`"]|[`"]$/g, '');
  let rest = main[3] || '';

  let limit = null;
  const limitMatch = rest.match(/\s+limit\s+(\d+)\s*$/i);
  if (limitMatch) {
    limit = Math.max(0, Math.min(1000, Number(limitMatch[1])));
    rest = rest.slice(0, limitMatch.index).trim();
  }
  let orderText = '';
  const orderMatch = rest.match(/(?:^|\s+)order\s+by\s+([\s\S]+)$/i);
  if (orderMatch) {
    orderText = orderMatch[1].trim();
    rest = rest.slice(0, orderMatch.index).trim();
  }
  let groupText = '';
  const groupMatch = rest.match(/(?:^|\s+)group\s+by\s+([\s\S]+)$/i);
  if (groupMatch) {
    groupText = groupMatch[1].trim();
    rest = rest.slice(0, groupMatch.index).trim();
  }
  let whereText = '';
  const whereMatch = rest.match(/^\s*where\s+([\s\S]+)$/i);
  if (whereMatch) whereText = whereMatch[1].trim();

  const sourceRows = normalizedTables[tableName];
  if (!sourceRows) {
    throw new Error(`表不存在: ${tableName}。可用表: ${availableTables.map(t => t.name).join(', ') || '无'}`);
  }
  const whereFn = _compileWhere(whereText);
  const filtered = sourceRows.filter(whereFn);
  const selectItems = _splitCommaAware(selectText).map(_parseSelectItem);
  const hasAggregate = selectItems.some(item => item.type === 'aggregate');
  const groupCols = groupText ? _splitCommaAware(groupText).map(s => s.trim()) : [];

  let rows;
  if (groupCols.length > 0) {
    const groups = new Map();
    for (const row of filtered) {
      const key = JSON.stringify(groupCols.map(col => row[col] ?? ''));
      if (!groups.has(key)) groups.set(key, []);
      groups.get(key).push(row);
    }
    rows = Array.from(groups.values()).map(groupRows => {
      const first = groupRows[0] || {};
      const out = {};
      for (const col of groupCols) out[col] = first[col] ?? '';
      for (const item of selectItems) {
        if (item.type === 'aggregate') out[item.alias] = _aggregateValue(groupRows, item);
        if (item.type === 'column') out[item.alias] = first[item.col] ?? '';
      }
      return out;
    });
  } else if (hasAggregate) {
    const out = {};
    for (const item of selectItems) {
      if (item.type === 'aggregate') out[item.alias] = _aggregateValue(filtered, item);
      if (item.type === 'column') out[item.alias] = filtered[0]?.[item.col] ?? '';
    }
    rows = [out];
  } else if (selectItems.some(item => item.type === 'star')) {
    rows = filtered.map(row => ({ ...row }));
  } else {
    rows = filtered.map(row => {
      const out = {};
      for (const item of selectItems) out[item.alias] = row[item.col] ?? '';
      return out;
    });
  }

  rows = _sortRows(rows, orderText);
  if (limit !== null) rows = rows.slice(0, limit);
  const columns = rows[0] ? Object.keys(rows[0]) : selectItems.filter(i => i.type !== 'star').map(i => i.alias);
  return { ok: true, query: sql, table: tableName, columns, rows, rowCount: rows.length, availableTables };
}

// ==================== 工具注册表 ====================

const ToolRegistry = {
  // DOM操作工具
  'dom.query': async (selector) => {
    const element = document.querySelector(selector);
    return element ? {
      text: element.textContent,
      html: element.innerHTML,
      value: element.value,
      attributes: Object.fromEntries(
        Array.from(element.attributes).map(a => [a.name, a.value])
      )
    } : null;
  },

  'dom.queryAll': async (selector) => {
    const elements = document.querySelectorAll(selector);
    return Array.from(elements).map(el => ({
      text: el.textContent.slice(0, 500),
      tag: el.tagName,
      id: el.id,
      class: el.className
    }));
  },

  'dom.click': async (selector) => {
    const element = document.querySelector(selector);
    if (element) {
      element.click();
      return { success: true };
    }
    return { success: false, error: 'Element not found' };
  },

  'dom.fill': async ({ selector, value }) => {
    const element = document.querySelector(selector);
    if (element) {
      element.value = value;
      element.dispatchEvent(new Event('input', { bubbles: true }));
      element.dispatchEvent(new Event('change', { bubbles: true }));
      return { success: true };
    }
    return { success: false, error: 'Element not found' };
  },

  // HTTP请求工具：AIPROS 版禁用任意外联，避免 Skill 成为数据外发入口。
  'http.get': async (url, headers = {}) => {
    return { error: 'http.get 已在 AIPROS 版中禁用，请使用 web.search 或已配置的模型接口' };
  },

  'http.post': async ({ url, body, headers = {} }) => {
    return { error: 'http.post 已在 AIPROS 版中禁用，请使用已配置的模型接口' };
  },

  // 文件读取工具（需要FileParser支持）
  'file.read': async (filePath) => {
    // 通过chrome.fileSystem或拖放文件处理
    throw new Error('file.read requires file handle from user interaction');
  },

  // 代码执行工具（沙箱内）
  'code.execute': async (code) => {
    return { error: 'code.execute 已在 AIPROS 版中禁用' };
  },

  // 执行外部接口（已移除bridge依赖）
  'exec.run': async (command) => {
    throw new Error('exec.run 已在 AIPROS 版中禁用');
  },

  // 等待工具
  'wait': async (ms) => {
    await new Promise(r => setTimeout(r, ms));
    return { waited: ms };
  },

  // 数据提取工具
  'extract.json': async (text) => {
    try {
      return JSON.parse(text);
    } catch (e) {
      return { error: e.message };
    }
  },

  'extract.regex': async ({ text, pattern, flags = 'g' }) => {
    const regex = new RegExp(pattern, flags);
    const matches = text.match(regex);
    return { matches: matches || [] };
  },

  // ===== 内容创作类工具（纯前端实现，不依赖本地服务）=====

  /**
   * web.search — 联网搜索（通过 background.js 代理，支持多引擎降级）
   */
  'web.search': async ({ query, max_results = 5 }) => {
    // 优先通过 background Service Worker 代理（CORS 豁免 + 多引擎降级）
    try {
      if (typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.sendMessage) {
        const resp = await new Promise((resolve) => {
          chrome.runtime.sendMessage(
            { type: 'WEB_SEARCH', query, maxResults: max_results },
            (r) => {
              if (chrome.runtime.lastError) {
                console.warn('[skill:web.search] background proxy failed:', chrome.runtime.lastError.message);
                resolve(null);
                return;
              }
              resolve(r);
            }
          );
        });
        if (resp && resp.ok && resp.results && resp.results.length > 0) {
          return { results: resp.results, query };
        }
      }
    } catch (e) {
      console.warn('[skill:web.search] background proxy exception:', e.message);
    }

    // 降级：前端直连 DuckDuckGo（最后手段）
    try {
      const ddgUrl = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
      const resp = await fetch(ddgUrl, {
        headers: {
          'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
          'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8'
        },
        signal: AbortSignal.timeout(10000)
      });
      if (!resp.ok) throw new Error(`搜索请求失败: ${resp.status}`);
      const html = await resp.text();
      const results = [];
      const blockRegex = /<a[^>]+class="result__a"[^>]+href="([^"]+)"[^>]*>([\s\S]*?)<\/a>[\s\S]*?(?:<a[^>]+class="result__snippet"[^>]*>([\s\S]*?)<\/a>)?/gi;
      let m;
      while ((m = blockRegex.exec(html)) !== null) {
        if (results.length >= max_results) break;
        const href = m[1].trim();
        const title = m[2].replace(/<[^>]+>/g, '').trim();
        const snippet = (m[3] || '').replace(/<[^>]+>/g, '').trim();
        if (href.startsWith('/') || href.includes('duckduckgo.com')) continue;
        if (title) results.push({ title, url: href, snippet });
      }
      return { results, query };
    } catch (e) {
      return { results: [], query, error: e.message };
    }
  },

  /**
   * web.fetch — 解析指定网页，适用于官网、政策页、公开计划书页面
   */
  'web.fetch': async ({ url, save = false }) => {
    try {
      if (!url) return { error: '缺少 url' };
      if (typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.sendMessage) {
        const resp = await new Promise((resolve) => {
          chrome.runtime.sendMessage({ type: 'FETCH_WEB_PAGE', url, save }, (r) => {
            if (chrome.runtime.lastError) {
              resolve({ ok: false, error: chrome.runtime.lastError.message });
              return;
            }
            resolve(r);
          });
        });
        if (!resp?.ok) return { error: resp?.error || '网页解析失败', url };
        const fetched = resp.context || resp.data;
        if (fetched) {
          const current = globalThis.__skillRuntimeCurrentContext || {};
          const fetchedPages = Array.isArray(current.fetchedPages) ? current.fetchedPages : [];
          current.fetchedPages = [...fetchedPages, fetched].slice(-5);
          globalThis.__skillRuntimeCurrentContext = current;
        }
        return {
          url: fetched?.pageUrl || url,
          title: fetched?.pageTitle || '',
          quality: fetched?.quality || '',
          textLength: fetched?.pageText?.length || 0,
          pageText: (fetched?.pageText || '').slice(0, 6000),
          headings: (fetched?.headings || []).slice(0, 30),
          tables: (fetched?.tableData || []).length
        };
      }
      return { error: '浏览器扩展运行环境不可用，无法解析网页', url };
    } catch (e) {
      return { error: e.message, url };
    }
  },

  /**
   * sql.query — 对页面表格、页面文本行或显式传入的数据表执行轻量 SQL
   */
  'sql.query': async (args = {}) => {
    try {
      const input = typeof args === 'string' ? { query: args } : (args || {});
      const runtimeContext = globalThis.__skillRuntimeCurrentContext || {};
      const contextTables = buildContextSqlTables(runtimeContext);
      const explicitTables = normalizeSqlTables(input.tables || {});
      const fetchedTables = {};
      if (Array.isArray(runtimeContext.fetchedPages)) {
        runtimeContext.fetchedPages.forEach((page, idx) => {
          const pageTables = buildContextSqlTables({ pageContext: page });
          Object.entries(pageTables).forEach(([name, rows]) => {
            fetchedTables[`fetched_${idx + 1}_${name}`] = rows;
          });
        });
      }
      const tables = { ...contextTables, ...fetchedTables, ...explicitTables };
      return runMiniSql({ query: input.query, tables });
    } catch (e) {
      return {
        ok: false,
        error: e.message,
        hint: '支持示例：SELECT segment, COUNT(*) AS n, SUM(revenue) AS total FROM market GROUP BY segment ORDER BY total DESC LIMIT 5'
      };
    }
  },

  'sql.tables': async () => {
    const runtimeContext = globalThis.__skillRuntimeCurrentContext || {};
    return { ok: true, availableTables: describeSqlTables(buildContextSqlTables(runtimeContext)) };
  },

  /**
   * file.save — 通过浏览器下载保存文件
   */
  'file.save': async ({ filename, content, dir }) => {
    // 浏览器环境下通过 Blob 下载
    const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = filename;
    a.style.display = 'none';
    document.body.appendChild(a);
    a.click();
    setTimeout(() => {
      document.body.removeChild(a);
      URL.revokeObjectURL(url);
    }, 200);
    return { path: filename, msg: `文件 ${filename} 已下载` };
  },

  /**
   * generate.html — 生成 HTML 报告并保存+新标签页打开
   * 参数：{ filename: "文件名.html", content: "完整HTML源码" }
   */
  'generate.html': async ({ filename, content }) => {
    const fname = (filename || 'AIPROS报告_' + new Date().toISOString().slice(0, 10)) + (filename?.endsWith('.html') ? '' : '.html');
    const blob = new Blob([content || ''], { type: 'text/html;charset=utf-8' });
    const url = URL.createObjectURL(blob);
    // 1. 触发下载
    const a = document.createElement('a');
    a.href = url;
    a.download = fname;
    a.style.display = 'none';
    document.body.appendChild(a);
    a.click();
    setTimeout(() => {
      if (a.parentNode) a.parentNode.removeChild(a);
    }, 1000);
    // 2. 新标签页打开预览
    try { window.open(url, '_blank'); } catch(e) {}
    const artifact = {
      id: 'artifact_' + Date.now() + '_' + Math.random().toString(16).slice(2),
      format: 'html',
      filename: fname,
      title: fname,
      url,
      content: content || '',
      mime: 'text/html;charset=utf-8',
      size: blob.size,
      createdAt: new Date().toISOString()
    };
    return { ok: true, path: fname, msg: `HTML 报告已生成并打开：${fname}`, artifact };
  },

  /**
   * file.open — 在浏览器新标签页中打开（限于HTML等浏览器可展示的文件）
   */
  'file.open': async ({ path: filePath }) => {
    // 浏览器环境下无法直接打开本地文件，返回提示
    return { msg: '浏览器环境不支持直接打开本地文件，请使用文件管理器打开', path: filePath };
  },

  // ===== 浏览器深度控制：通过 background 的 chrome.debugger/CDP 执行 =====
  'browser.open': async (args = {}) => _callBrowserTool('BROWSER_OPEN', args),
  'browser.navigate': async (args = {}) => _callBrowserTool('BROWSER_NAVIGATE', args),
  'browser.click': async (args = {}) => _callBrowserTool('BROWSER_CLICK', args),
  'browser.type': async (args = {}) => _callBrowserTool('BROWSER_TYPE', args),
  'browser.fill': async (args = {}) => _callBrowserTool('BROWSER_FILL', args),
  'browser.press': async (args = {}) => _callBrowserTool('BROWSER_PRESS', args),
  'browser.observe': async (args = {}) => _callBrowserTool('BROWSER_OBSERVE', args),
  'browser.waitFor': async (args = {}) => _callBrowserTool('BROWSER_WAIT', args),
  'browser.getText': async (args = {}) => _callBrowserTool('BROWSER_GET_TEXT', args),
  'browser.screenshot': async (args = {}) => _callBrowserTool('BROWSER_SCREENSHOT', args),
  'browser.pdf': async (args = {}) => _callBrowserTool('BROWSER_PDF', args),
  'browser.network.start': async (args = {}) => _callBrowserTool('BROWSER_NETWORK_START', args),
  'browser.network.get': async (args = {}) => _callBrowserTool('BROWSER_NETWORK_GET', args),
  'browser.network.clear': async (args = {}) => _callBrowserTool('BROWSER_NETWORK_CLEAR', args),
  'browser.network.stop': async (args = {}) => _callBrowserTool('BROWSER_NETWORK_STOP', args),
  'browser.acceptance.check': async (args = {}) => _callBrowserTool('BROWSER_ACCEPTANCE_CHECK', args),
  'browser.listTabs': async (args = {}) => _callBrowserTool('BROWSER_LIST_TABS', args),
  'browser.close': async (args = {}) => _callBrowserTool('BROWSER_CLOSE', args),

  // ===== Agent 执行闭环：计划、步骤、状态、回滚点 =====
  'agent.plan': async (args = {}) => {
    if (!globalThis.AgentLoop?.setPlan) return { ok: false, error: 'AgentLoop 未加载' };
    return globalThis.AgentLoop.setPlan(args);
  },
  'agent.step': async (args = {}) => {
    if (!globalThis.AgentLoop?.setStep) return { ok: false, error: 'AgentLoop 未加载' };
    return globalThis.AgentLoop.setStep(args);
  },
  'agent.status': async () => {
    if (!globalThis.AgentLoop?.snapshot) return { ok: false, error: 'AgentLoop 未加载' };
    return { ok: true, run: globalThis.AgentLoop.snapshot() };
  },
  'agent.audit.list': async (args = {}) => {
    if (!globalThis.AgentLoop?.listAudit) return { ok: false, error: 'AgentLoop 未加载' };
    return { ok: true, audits: await globalThis.AgentLoop.listAudit(args) };
  },
  'agent.audit.replay': async (args = {}) => {
    if (!globalThis.AgentLoop?.replayAudit) return { ok: false, error: 'AgentLoop 未加载' };
    return await globalThis.AgentLoop.replayAudit(args);
  },
  'checkpoint.create': async (args = {}) => _callNativeTool('checkpoint.create', args),
  'checkpoint.list': async (args = {}) => _callNativeTool('checkpoint.list', args),
  'checkpoint.restore': async (args = {}) => _callNativeTool('checkpoint.restore', args),

  // ===== Native Agent 预留：安装本地 Agent 后可写磁盘/执行白名单命令 =====
  'fs.write': async (args = {}) => _callNativeTool('fs.write', args),
  'fs.read': async (args = {}) => _callNativeTool('fs.read', args),
  'fs.list': async (args = {}) => _callNativeTool('fs.list', args),
  'fs.stat': async (args = {}) => _callNativeTool('fs.stat', args),
  'fs.search': async (args = {}) => _callNativeTool('fs.search', args),
  'fs.copy': async (args = {}) => _callNativeTool('fs.copy', args),
  'fs.move': async (args = {}) => _callNativeTool('fs.move', args),
  'fs.delete': async (args = {}) => _callNativeTool('fs.delete', args),
  'shell.exec': async (args = {}) => _callNativeTool('shell.exec', args),
  'cloudflare.pages.deploy': async (args = {}) => _callNativeTool('cloudflare.pages.deploy', args),
  'clipboard.read': async (args = {}) => _callNativeTool('clipboard.read', args),
  'clipboard.write': async (args = {}) => _callNativeTool('clipboard.write', args),
  'project.generate': async (args = {}) => _callNativeTool('project.generate', args),
  'skill.list': async (args = {}) => _callNativeTool('skill.list', args),
  'skill.read': async (args = {}) => _callNativeTool('skill.read', args),
  'skill.write': async (args = {}) => _callNativeTool('skill.write', args),
  'skill.validate': async (args = {}) => _callNativeTool('skill.validate', args),
  'skill.evaluate': async (args = {}) => _callNativeTool('skill.evaluate', args),
  'skill.benchmark': async (args = {}) => _callNativeTool('skill.benchmark', args),
  'skill.scaffold': async (args = {}) => _callNativeTool('skill.scaffold', args)
};

function normalizeApiKeyForHeader(raw) {
  let value = String(raw || '')
    .replace(/\u00a0/g, ' ')
    .replace(/[\u200B-\u200D\uFEFF]/g, '')
    .trim()
    .replace(/^["'“”‘’]+|["'“”‘’]+$/g, '');

  value = value
    .replace(/^authorization\s*:\s*/i, '')
    .replace(/^bearer\s+/i, '')
    .trim();

  if (!value) return '';

  const tokenMatch = value.match(/(?:sk-[A-Za-z0-9._~+/=-]{6,}|[A-Za-z0-9][A-Za-z0-9._~+/=-]{15,})/);
  if (/[^\x20-\x7E]/.test(value) && tokenMatch) {
    value = tokenMatch[0];
  }

  value = value.replace(/\s+/g, '');
  if (/[^\x21-\x7E]/.test(value)) {
    throw new Error('API Key 含有中文、全角或不可见字符，请只粘贴纯英文数字 Key，不要粘贴说明文字');
  }
  return value;
}

// ===== 浏览器自动化辅助函数 =====

/**
 * 调用 background.js 中的浏览器自动化工具
 * @param {string} type - 消息类型（如 BROWSER_OPEN）
 * @param {Object} args - 参数
 * @returns {Promise<Object>} 工具执行结果
 */
async function _callBrowserTool(type, args) {
  try {
    if (typeof chrome === 'undefined' || !chrome.runtime?.sendMessage) {
      return { error: '浏览器环境不可用，无法执行自动化操作' };
    }
    const resp = await new Promise((resolve) => {
      chrome.runtime.sendMessage({ type, ...args }, (r) => {
        if (chrome.runtime.lastError) {
          console.warn('[browser-tool] 调用失败:', chrome.runtime.lastError.message);
          resolve({ ok: false, error: chrome.runtime.lastError.message });
          return;
        }
        resolve(r);
      });
    });
    if (resp && resp.ok) {
      return resp.data;
    }
    return { error: resp?.error || '工具执行失败' };
  } catch (e) {
    return { error: e.message };
  }
}

async function _callNativeTool(tool, args = {}) {
  try {
    if (typeof chrome === 'undefined' || !chrome.runtime?.sendMessage) {
      return { error: '浏览器环境不可用，无法连接本地 Agent' };
    }
    const resp = await new Promise((resolve) => {
      chrome.runtime.sendMessage({ type: 'NATIVE_TOOL', tool, args }, (r) => {
        if (chrome.runtime.lastError) {
          resolve({ ok: false, error: chrome.runtime.lastError.message });
          return;
        }
        resolve(r);
      });
    });
    if (resp && resp.ok) return resp.data || { ok: true };
    return {
      error: resp?.error || resp?.data?.error || '本地 Agent 未安装或未授权',
      installRequired: resp?.data?.installRequired !== false
    };
  } catch (e) {
    return { error: e.message };
  }
}

// ==================== 工具调度器 ====================

class ToolDispatcher {
  constructor() {
    this.tools = new Map();
    this.registerDefaults();
  }

  registerDefaults() {
    Object.entries(ToolRegistry).forEach(([name, fn]) => {
      this.register(name, fn);
    });
  }

  register(name, fn) {
    this.tools.set(name, fn);
  }

  async execute(name, args) {
    const tool = this.tools.get(name);
    if (!tool) {
      throw new Error(`Tool not found: ${name}`);
    }
    try {
      if (globalThis.AgentLoop?.executeTool && !name.startsWith('agent.')) {
        return await globalThis.AgentLoop.executeTool({
          tool: name,
          args,
          retries: 1,
          executor: () => tool(args)
        });
      }
      return await tool(args);
    } catch (error) {
      return { error: error.message, tool: name };
    }
  }

  has(name) {
    return this.tools.has(name);
  }
}

// ==================== 引号感知的 JSON 提取工具 ====================

/**
 * 从指定位置开始，用引号感知的花括号计数法提取完整 JSON
 * 跳过字符串内的花括号，避免 HTML/CSS/JS 内容中的 { } 干扰 JSON 边界检测
 * @param {string} text - 完整文本
 * @param {number} startIdx - JSON 起始位置（必须是 '{' 字符）
 * @returns {string|null} 完整 JSON 字符串，如果不完整返回 null
 */
function _extractCompleteJson(text, startIdx) {
  if (startIdx < 0 || startIdx >= text.length || text[startIdx] !== '{') return null;
  let depth = 0;
  let inStr = false, strChar = '';
  for (let i = startIdx; i < text.length; i++) {
    const ch = text[i];
    if (inStr) {
      if (ch === '\\') { i++; continue; } // 跳过转义字符
      if (ch === strChar) inStr = false;
      continue;
    }
    if (ch === '"' || ch === "'") { inStr = true; strChar = ch; continue; }
    if (ch === '{') depth++;
    else if (ch === '}') {
      depth--;
      if (depth === 0) return text.substring(startIdx, i + 1);
    }
  }
  return null; // JSON 不完整
}

function _extractCompleteHtmlDocumentFromText(text = '') {
  const raw = String(text || '');
  const startMatch = raw.match(/<!doctype\s+html|<html[\s>]/i);
  if (!startMatch) return '';
  const start = startMatch.index || 0;
  const endRe = /<\/html\s*>/ig;
  let end = -1;
  let match;
  while ((match = endRe.exec(raw))) {
    end = match.index + match[0].length;
  }
  if (end < start) return '';
  return raw.slice(start, end).trim()
    .replace(/\\n/g, '\n')
    .replace(/\\"/g, '"');
}

function _extractLooseJsonStringValue(text = '', key = '') {
  const re = new RegExp(`"${key}"\\s*:\\s*"([^"\\r\\n]{1,160})"`, 'i');
  const match = String(text || '').match(re);
  return match ? match[1].trim() : '';
}

function _extractLenientGenerateHtmlToolCall(text = '') {
  const raw = String(text || '');
  if (!/generate\.html/i.test(raw)) return null;
  const html = _extractCompleteHtmlDocumentFromText(raw);
  if (!html) return null;
  const filename = _extractLooseJsonStringValue(raw, 'filename') || 'AIPROS页面';
  return JSON.stringify({
    tool: 'generate.html',
    args: { filename, content: html }
  });
}

// ==================== Skill执行引擎 ====================

class SkillRuntime {
  constructor(options = {}) {
    this.dispatcher = new ToolDispatcher();
    this.llmConfig = options.llmConfig || null;
    this.timeout = options.timeout || 180000; // 总超时3分钟（长文本输出需要更久）
    this.firstCharTimeout = options.firstCharTimeout || 5000; // 首字超时5秒
    this.onChunk = options.onChunk || (() => {});
    this.onToolCall = options.onToolCall || (() => {});
    this.onError = options.onError || (() => {});
    this.context = {};
    this.retryCount = options.retryCount || 2; // 默认重试2次
    this.retryDelay = options.retryDelay || 1000; // 重试间隔1秒
    this.autoFinishRun = options.autoFinishRun !== false;
    this._currentController = null; // 当前 AbortController，供外部 abort() 使用
    this._currentReader = null;
    this._aborted = false; // 是否已被外部中止
    this._lastToolStreamProgressAt = 0;
  }

  /**
   * 外部中止：立即中断当前 LLM 流式调用
   */
  abort() {
    this._aborted = true;
    try { this._currentController?.abort(); } catch (_) {}
    try { this._currentReader?.cancel(); } catch (_) {}
  }

  /**
   * 执行Skill（带重试）
   * @param {Object} skill - 解析后的Skill对象
   * @param {Object} context - 执行上下文（页面数据、附件等）
   * @param {string} userMessage - 用户输入
   * @returns {Promise<string>} 执行结果
   */
  async execute(skill, context = {}, userMessage = '') {
    this.context = { ...context, skill };
    globalThis.__skillRuntimeCurrentContext = this.context;
    this._aborted = false; // 重置中止标记
    if (globalThis.AgentLoop?.startRun) {
      globalThis.AgentLoop.startRun({
        source: 'skill',
        title: `Skill：${skill.name || '未命名'}`,
        skillName: skill.name || '',
        objective: userMessage || skill.description || ''
      });
    }
    
    // 构建完整提示词
    const fullPrompt = this.buildPrompt(skill, context, userMessage);
    
    // 直接调用 LLM API（带重试）
    let lastError = null;
    for (let attempt = 0; attempt < this.retryCount; attempt++) {
      try {
        if (attempt > 0) {
          console.log(`[SkillRuntime] 第${attempt + 1}次尝试...`);
          await this._sleep(this.retryDelay * attempt);
        }
        const result = await this.executeWithLLM(fullPrompt, skill);
        if (this.autoFinishRun) globalThis.AgentLoop?.finishRun?.('completed', 'Skill 执行完成');
        return result;
      } catch (error) {
        lastError = error;
        console.warn(`[SkillRuntime] LLM执行失败(尝试${attempt + 1}/${this.retryCount}):`, error.message);
        
        // 非网络错误不重试
        if (!this._isRetryableError(error)) {
          break;
        }
      }
    }
    
    if (this.autoFinishRun) globalThis.AgentLoop?.finishRun?.('failed', lastError?.message || 'Skill 执行失败');
    throw new Error(`执行失败（重试${this.retryCount}次）: ${lastError?.message}`);
  }

  /**
   * 判断错误是否可重试
   */
  _isRetryableError(error) {
    const retryableMessages = ['network', 'timeout', 'abort', 'rate limit', '503', '502', '504'];
    const errorMsg = error.message?.toLowerCase() || '';
    return retryableMessages.some(msg => errorMsg.includes(msg));
  }

  /**
   * 延迟函数
   */
  _sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  _emitToolStreamProgress(fullResponse = '', round = 1) {
    const now = Date.now();
    if (now - this._lastToolStreamProgressAt < 300) return;
    this._lastToolStreamProgressAt = now;
    const raw = String(fullResponse || '');
    const tool = /generate\.html/i.test(raw) || /<!doctype\s+html|<html[\s>]/i.test(raw)
      ? 'generate.html'
      : 'tool_call';
    this.onToolCall({
      tool,
      args: {},
      callId: `tool_stream_${round}`,
      status: 'streaming',
      receivedChars: raw.length
    });
  }

  /**
   * 构建完整提示词
   */
  buildPrompt(skill, context, userMessage) {
    const parts = [];
    
    // 当前时间（让 AI 知道真实的日期时间）
    const now = new Date();
    const timeStr = now.toLocaleDateString('zh-CN', { year:'numeric', month:'long', day:'numeric', weekday:'long' })
      + ' ' + now.toLocaleTimeString('zh-CN', { hour:'2-digit', minute:'2-digit' });
    parts.push(`【环境信息】\n当前时间：${timeStr}\n请基于以上真实时间进行判断和回答。`);
    
    // 角色定义
    if (skill.role) {
      parts.push(`【角色】\n${skill.role}`);
    }
    
    // 系统级工具（始终可用）
    const systemTools = [
      { name: 'web.fetch', description: '解析指定网页。参数: { url: "https://...", save: false }。适合读取用户粘贴的产品页面、竞品页面、需求文档网页，并返回标题、正文、标题层级和表格数量。' },
      { name: 'web.search', description: '联网搜索。参数: { query: "搜索关键词", max_results: 5 }。返回搜索结果列表，含标题、URL和摘要。当你需要获取最新信息或验证事实时使用。' },
      { name: 'sql.query', description: '执行内置 SQL。参数: { query: "SELECT ...", tables: { 表名: [行对象数组或二维数组] } }。如果不传 tables，会自动使用当前页面字段、页面文本行、网页表格和已解析网页。支持 WHERE/LIKE/GROUP BY/ORDER BY/LIMIT 与 COUNT/SUM/AVG/MIN/MAX。' },
      { name: 'sql.tables', description: '列出当前可用 SQL 表。参数: {}。当不确定页面表名和字段名时先调用。' },
      { name: 'generate.html', description: '生成HTML页面或报告并自动下载+在新标签页打开预览。参数: { filename: "文件名.html", content: "完整HTML源码" }。当用户或Skill要求生成HTML页面/报告时使用。' },
      { name: 'cloudflare.pages.deploy', description: '把完整 HTML 发布到 Cloudflare Pages 公网。参数: { projectName: "项目名", filename: "index.html", content: "完整HTML源码", branch: "main" }。发布前会触发权限确认，需要本地 Agent 和 Wrangler 登录或 CLOUDFLARE_API_TOKEN。' },
      { name: 'file.save', description: '保存文件到本地（触发浏览器下载）。参数: { filename: "文件名", content: "文件内容" }。' },
      { name: 'project.generate', description: '通过本地 Native Agent 生成可运行的前端工程项目。参数: { projectName: "项目名", framework: "react|vue|static", targetDir: "/可选/绝对路径", files: [{ path: "相对路径", content: "文件内容" }], runInstall: false, runBuild: false, runGitInit: false }。当用户要求 React/Vue/Vite/Tailwind、项目源码、组件库、可运行前端项目或工程级交付时使用。' },
      { name: 'browser.open', description: '打开或导航到网页。参数: { url: "https://...", newTab: true }。会通过 chrome.debugger/CDP 控制浏览器，Chrome 会显示调试提示条。' },
      { name: 'browser.click', description: '点击页面元素。参数: { selector: "CSS选择器" } 或 { text: "按钮文本" } 或 { x: 100, y: 200 }。' },
      { name: 'browser.type', description: '向输入框输入文本。参数: { selector: "CSS选择器", text: "内容", clear: true }。' },
      { name: 'browser.fill', description: '填写表单字段。参数: { selector: "CSS选择器", text: "内容", clear: true }。selector 可省略，省略时会填写当前聚焦或页面中最像“意见/备注/评论”的输入框。' },
      { name: 'browser.waitFor', description: '等待元素出现。参数: { selector: "CSS选择器", timeout: 8000 }。' },
      { name: 'browser.getText', description: '读取页面或元素文本。参数: { selector: "CSS选择器", maxLength: 8000 }；selector 可省略。' },
      { name: 'browser.screenshot', description: '截取当前自动化页面。参数: { fullPage: false }。' },
      { name: 'browser.network.start', description: '启动 CDP Network 捕获。参数: { tabId: 可选, resourceTypes: ["XHR","Fetch"], includeBodies: true, maxEntries: 300 }。抓接口前先打开目标页，再启动捕获，然后执行点击/提交/查询动作。' },
      { name: 'browser.network.get', description: '读取已捕获的 XHR/fetch 请求。参数: { tabId: 可选, limit: 50, includeBodies: true, includeCurl: true, format: "json|har" }。返回 URL、方法、状态码、请求/响应头、postData、responseBody 和 curl 草稿。' },
      { name: 'browser.network.clear', description: '清空当前标签页的 Network 捕获记录。参数: { tabId: 可选 }。' },
      { name: 'browser.network.stop', description: '停止当前标签页的 Network 捕获并返回摘要。参数: { tabId: 可选 }。' },
      { name: 'browser.acceptance.check', description: '真实浏览器验收页面。参数: { url: "可选", startNetwork: true, waitMs: 1000, checks: [{ type: "selector", selector: "main" }, { type: "text", text: "关键词" }, { type: "network", minCount: 1 }] }。返回通过/失败和证据。' },
      { name: 'agent.plan', description: '创建或更新 Agent 执行计划。参数: { title: "任务名", objective: "目标", steps: [{ id: "step1", title: "步骤", status: "pending" }] }。多步骤任务应先调用。' },
      { name: 'agent.step', description: '更新 Agent 步骤状态。参数: { stepId: "step1", status: "pending|in_progress|completed|failed", message: "说明" }。' },
      { name: 'agent.status', description: '读取当前 Agent 执行闭环状态。参数: {}。返回计划、步骤、工具日志、权限确认和回滚点。' },
      { name: 'agent.audit.list', description: '读取最近的 Agent 工具调用审计记录。参数: { limit: 20 }。用于复盘和回放历史执行。' },
      { name: 'agent.audit.replay', description: '回放指定 Agent 审计记录。参数: { runId: "审计运行ID" }。会把执行轨迹恢复到面板中查看。' },
      { name: 'checkpoint.create', description: '通过本地 Agent 手动创建回滚点。参数: { paths: ["/绝对路径"], label: "说明" }。通常由 AgentLoop 在修改文件前自动创建。' },
      { name: 'checkpoint.restore', description: '恢复指定回滚点。参数: { id: "checkpoint id", confirm: true }。会恢复或删除被 checkpoint 记录的路径。' },
      { name: 'fs.write', description: '通过本地 Native Agent 写磁盘文件。参数: { path: "/绝对路径/文件名", content: "内容" }。未安装本地 Agent 时会返回安装提示。' },
      { name: 'fs.read', description: '通过本地 Native Agent 读取磁盘文件。参数: { path: "/绝对路径/文件名", maxBytes: 200000 }。' },
      { name: 'fs.list', description: '通过本地 Native Agent 列目录。参数: { path: "/绝对路径/目录", limit: 100 }。' },
      { name: 'fs.search', description: '通过本地 Native Agent 搜索文件名和文本内容。参数: { path: "/绝对路径/目录", query: "关键词", includeContent: true, maxResults: 50 }。' },
      { name: 'fs.copy', description: '复制文件或目录。默认 dryRun:true；真正执行需 { dryRun:false, confirm:true }。' },
      { name: 'fs.move', description: '移动或重命名文件或目录。默认 dryRun:true；真正执行需 { dryRun:false, confirm:true }。' },
      { name: 'fs.delete', description: '删除文件或目录。默认 dryRun:true；真正执行需 { dryRun:false, confirm:true }。危险操作，执行前必须说明影响。' },
      { name: 'skill.list', description: '列出本地插件 Skill。参数: { skillsDir: "/可选/绝对路径" }。' },
      { name: 'skill.read', description: '读取 Skill 内容并校验。参数: { name: "Skill名" } 或 { path: "/绝对路径/SKILL.md" }。' },
      { name: 'skill.write', description: '写入或更新 Skill。参数: { name: "Skill名", content: "完整SKILL.md", overwrite: false }。写入前会做结构校验。' },
      { name: 'skill.validate', description: '静态校验 Skill 结构。参数: { content: "完整SKILL.md" } 或 { path: "/绝对路径/SKILL.md" }。' },
      { name: 'skill.evaluate', description: '对 Skill 触发词和结构做轻量评测。参数: { content/path, positivePrompts: [], negativePrompts: [] }。' },
      { name: 'skill.benchmark', description: '批量评测本地 Skill。参数: { skillsDir: "/可选/绝对路径", minScore: 70, positivePrompts: [], negativePrompts: [] }。返回数量、均分、通过/失败和优先修复清单。' },
      { name: 'skill.scaffold', description: '生成 Skill 草稿骨架。参数: { name: "Skill名", purpose: "用途", triggers: ["触发词"] }。' },
      { name: 'shell.exec', description: '通过本地 Native Agent 执行白名单命令。参数: { command: ["npm","run","build"], cwd: "/绝对路径" }。仅在用户明确要求安装、构建或验证时使用。' }
    ];
    // 合并 Skill 声明的工具和系统工具
    const allTools = [...systemTools, ...(skill.tools || [])];
    if (allTools.length > 0) {
      parts.push(`【可用工具】\n${allTools.map(t => `- ${t.name}: ${t.description}`).join('\n')}`);
    }
    
    // 上下文数据：把 JSON 拍平为可读的文字，防止 AI 原文复述 JSON
    if (context.pageContext && Object.keys(context.pageContext).length > 0) {
      const ctx = context.pageContext;
      const ctxLines = [];
      // 优先展示高价值字段
      if (ctx.billId) ctxLines.push(`单号：${ctx.billId}`);
      if (ctx.pageTitle || ctx.title) ctxLines.push(`标题：${ctx.pageTitle || ctx.title}`);
      if (ctx.pageUrl) ctxLines.push(`页面：${ctx.pageUrl}`);
      // 文字内容（截断）
      const rawText = [ctx.pageText, ctx.text, ctx.selectCopyText, ctx.screenshotText]
        .filter(Boolean)
        .join('\n\n');
      if (rawText) {
        const truncated = rawText.length > 8000 ? rawText.slice(0, 8000) + '\n...[内容已截断]' : rawText;
        ctxLines.push(`页面内容：\n${truncated}`);
      } else {
        // 没有纯文字时，把 JSON 扁平化为键值对文字（最多20个字段）
        const entries = Object.entries(ctx)
          .filter(([k]) => !['quality', 'pageText', 'text', 'selectCopyText', 'screenshotText'].includes(k))
          .slice(0, 20);
        for (const [k, v] of entries) {
          const val = typeof v === 'object' ? JSON.stringify(v).slice(0, 200) : String(v).slice(0, 200);
          ctxLines.push(`${k}：${val}`);
        }
      }
      parts.push(`【页面信息】\n${ctxLines.join('\n')}`);
    }

    const availableSqlTables = describeSqlTables(buildContextSqlTables(context));
    if (availableSqlTables.length > 0) {
      parts.push(`【可用SQL表】\n${availableSqlTables.map(t => `- ${t.name}：${t.rowCount} 行；字段：${t.columns.join(', ') || '暂无'}`).join('\n')}`);
    }
    
    // 附件内容
    if (context.attachments?.length > 0) {
      const attachmentContent = context.attachments.map(att => {
        return `--- 文件：${att.name} ---\n${att.content?.slice(0, 5000) || '[二进制文件]'}`;
      }).join('\n\n');
      parts.push(`【附件内容】\n${attachmentContent}`);
    }
    
    // Skill主提示词
    if (skill.prompt) {
      parts.push(`【任务说明 - 请严格执行】
你现在已经接收到一个 Skill 的执行指令。以下是一个可执行的 Skill 规范，描述了你需要完成的具体任务和步骤。
请注意：你的任务是**按照这个 Skill 规范执行任务**，而不是评价、介绍或解释这个 Skill 本身。
请严格按照以下 Skill 规范中的步骤逐一执行，直接输出执行结果：

${skill.prompt}`);
    }

    parts.push(`【Skill执行前的信息门槛】
在正式执行 Skill 前，先判断用户输入、页面和附件是否足够支撑任务。若缺少任务对象、目标、材料、范围、约束或输出用途，不要直接生成空泛模板；只提出最多3个关键追问。若用户明确要求先出草稿，或系统提示已经达到追问上限，则基于已有信息执行，并把缺口标为“待补充”。`);
    
    // 用户输入
    if (userMessage) {
      parts.push(`【用户指令】\n${userMessage}`);
    }
    
    // 始终开放工具调用（因为至少有 web.search 系统工具）
    parts.push(`【工具调用格式】
当需要使用工具（如解析网页、联网搜索、执行 SQL、操作网页、生成 HTML 报告）时，请按以下格式输出：
<tool_call>
{"tool": "工具名", "args": {参数对象}}
</tool_call>
工具调用结果将被自动处理并返回给你。凡涉及表格筛选、分组、排序、聚合，请优先使用 sql.query 并基于真实返回结果继续给出最终分析内容。
生成 HTML 时，content 必须是完整 HTML 文档；若无法保证 JSON 字符串转义正确，就直接输出完整的 \`\`\`html 代码块，系统会兜底生成交付物。`);
    
    return parts.join('\n\n');
  }

  /**
   * 使用LLM API执行（流式，每个chunk立即推送给UI）
   * 支持多轮工具调用闭环：工具执行结果自动回传 AI 继续推理
   */
  async executeWithLLM(prompt, skill) {
    if (!this.llmConfig?.apiKey) {
      throw new Error('LLM API未配置');
    }

    const { apiKey, apiUrl = 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions', model = 'qwen-plus' } = this.llmConfig;
    const cleanApiKey = normalizeApiKeyForHeader(apiKey);
    if (!cleanApiKey) {
      throw new Error('LLM API未配置，请在设置中填入 API Key');
    }

    // 多轮对话消息历史（初始化：system + user）
    const now = new Date();
    const timeStr = now.toLocaleDateString('zh-CN', { year:'numeric', month:'long', day:'numeric', weekday:'long' })
      + ' ' + now.toLocaleTimeString('zh-CN', { hour:'2-digit', minute:'2-digit' });
    const messages = [
      { role: 'system', content: `你是「詹老师的AIPROS」，面向真实业务流程的智能体驱动工作平台，具备网页理解、资料整理、任务规划、文档生成、浏览器自动化和工作流协同能力；也能处理PRD、需求评审、技术方案、运营方案、流程图和流程文件等专业任务。当前真实时间是：${timeStr}。请始终基于此时间进行判断和回答。默认只使用当前页面、附件、用户输入、用户粘贴网址和公开联网资料，不依赖企业内部深数据；输出结构化、可执行、方便用户直接使用的中文结论。

重要行为规则：
1. 当你收到【任务说明 - 请严格执行】时，你必须按照其中描述的步骤逐一执行，直接输出执行结果
2. 执行任何 Skill 前先判断用户是否给了足够的任务对象、目标、材料和约束；信息不足时不要空跑、不要生成空模板，只追问最多3个关键问题
3. 如果用户已经明确说“先出草稿/按现有信息/不用追问”，或已经经过多轮补充仍不完整，才基于已有信息产出，并把缺口标为“待补充”
4. 绝对不要评价、介绍、解释或复述任务说明本身——你是执行者，不是评论员
5. 如果任务说明要求生成文件（HTML/Word/Excel等），请使用 generate.html 等工具直接生成；如果要求生成前端工程/React/Vue/Vite项目，请使用 project.generate 写入多文件项目，而不是描述你会怎么生成
6. 如果任务说明包含多个步骤，请逐一执行所有步骤，不要跳过
7. 审批页面操作：用户要求填审批意见/写入意见/回填/注入到页面时，应调用 browser.fill 写入页面；只有用户明确说“提交/提交审批/同意并提交/通过并提交/批准并提交/驳回并提交”时，才继续调用 browser.click 点击提交/确认/同意/通过等按钮；没有明确提交时不要点击提交
8. 多步骤、会写文件、会操作网页或会调用多个工具的任务，先调用 agent.plan 生成执行计划；每完成关键步骤可调用 agent.step 更新状态；修改本地文件前系统会自动做权限确认和回滚点

你可以使用 web.fetch 解析指定网页，使用 web.search 获取公开资料，使用 sql.query 对页面表格或显式数据执行真实 SQL，也可以在用户明确要求操作网页时使用 browser.open/browser.click/browser.type/browser.fill/browser.getText 等浏览器自动化工具。涉及排序、筛选、分组、聚合、测算时优先调用 sql.query，不要假装已经执行 SQL。当需要使用工具时，用 <tool_call>{"tool": "工具名", "args": {...}}</tool_call> 格式调用工具。工具结果会以 [TOOL_RESULT] 标记反馈给你，请据此继续完成任务。严禁原文复述或重复输出JSON数据、页面原始内容。只输出结构化的中文分析结果。` },
      { role: 'user', content: prompt }
    ];

    const MAX_TOOL_ROUNDS = 5; // 防止无限循环
    let totalResult = '';
    let round = 0;

    while (round < MAX_TOOL_ROUNDS) {
      round++;
      const roundResult = await this._callLLMRound(cleanApiKey, apiUrl, model, messages, skill, round);
      totalResult += (totalResult ? '\n' : '') + roundResult.text;

      if (roundResult.toolCalls.length === 0) {
        // 没有工具调用，AI 已完成推理，退出循环
        break;
      }

      // 有工具调用：执行工具，把结果追加到 messages，继续下一轮
      const toolResultParts = [];
      for (const toolJson of roundResult.toolCalls) {
        const toolResult = await this.handleToolCall(toolJson);
        const toolName = JSON.parse(toolJson).tool;
        const modelResult = this.formatToolResultForModel(toolName, toolResult);
        toolResultParts.push(`[TOOL_RESULT]\n工具: ${toolName}\n结果: ${JSON.stringify(modelResult, null, 2)}\n[/TOOL_RESULT]`);
      }

      // 把 AI 本轮回复（含工具调用标签）加入 messages
      messages.push({ role: 'assistant', content: roundResult.rawText });
      // 把工具结果作为 user 消息注入（兼容不支持 tool role 的模型）
      messages.push({ role: 'user', content: toolResultParts.join('\n\n') + '\n\n请根据以上工具结果继续完成任务。' });

      if (round >= MAX_TOOL_ROUNDS) {
        this.onChunk('\n\n> ⚠️ 已达到最大工具调用轮数限制。');
        break;
      }
    }

    return totalResult;
  }

  /**
   * 执行单轮 LLM API 调用，返回文本和工具调用列表
   * @private
   */
  async _callLLMRound(apiKey, apiUrl, model, messages, skill, round) {
    const controller = new AbortController();
    this._currentController = controller; // 保存引用，供 abort() 使用
    const totalTimeoutId = setTimeout(() => controller.abort(), this.timeout);

    // 首字超时定时器
    let firstCharTimeoutId = null;
    let firstCharReceived = false;
    let reader = null;  // 提至 try 外层，确保 catch 可访问

    try {
      const response = await fetch(apiUrl, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${apiKey}`
        },
        body: JSON.stringify({
          model: model,
          messages: messages,
          stream: true,
          temperature: 0.7,
          max_tokens: 16384
        }),
        signal: controller.signal
      });

      if (!response.ok) {
        const errText = await response.text().catch(() => '');
        throw new Error(`LLM API返回错误: ${response.status} ${errText}`);
      }

      // 流式处理响应
      reader = response.body.getReader();
      this._currentReader = reader; // 保存引用，供 abort() 使用
      const decoder = new TextDecoder();
      let buffer = '';
      let fullResponse = '';
      let handledToolCalls = new Set();
      const toolCallJsonList = []; // 本轮收集到的工具调用

      // 设置首字超时：5秒内未收到第一个字符就报错
      const firstCharPromise = new Promise((_, reject) => {
        firstCharTimeoutId = setTimeout(() => {
          if (!firstCharReceived) {
            reject(new Error('首字响应超时（5s）'));
          }
        }, this.firstCharTimeout);
      });

      // 读取流 —— 每个SSE chunk立即调用 onChunk，不走打字机缓冲
      this._jsonCutDone = false;
      const readStream = async () => {
        while (true) {
          const { done, value } = await reader.read();
          if (done) break;

          // 首字到达，清除首字超时
          if (!firstCharReceived) {
            firstCharReceived = true;
            clearTimeout(firstCharTimeoutId);
          }

          buffer += decoder.decode(value, { stream: true });
          const lines = buffer.split('\n');
          buffer = lines.pop(); // 保留未完成的行

          for (const line of lines) {
            if (!line.startsWith('data:')) continue;
            const data = line.slice(5).trim();
            if (data === '[DONE]') continue;

            try {
              const parsed = JSON.parse(data);
              const content = parsed.choices?.[0]?.delta?.content || '';
              if (content) {
                fullResponse += content;

                // 检查是否在 <tool_call> 标签内部
                const inXmlToolCall = (fullResponse.includes('<tool_call>') && !fullResponse.includes('</tool_call>')) ||
                                   (fullResponse.split('<tool_call>').length > fullResponse.split('</tool_call>').length);

                // 检查是否出现裸 JSON 工具调用（{"tool": 或 {"tool_call": 开头）
                const hasNakedToolCall = /\{"tool"\s*:/.test(fullResponse) || /\{"tool_call"\s*:/.test(fullResponse);

                if (!inXmlToolCall && !hasNakedToolCall) {
                  // 普通文字内容：立即推送给UI
                  const cleanContent = content.replace(/<\/?tool_call>/g, '');
                  if (cleanContent) {
                    this.onChunk(cleanContent);
                  }
                } else if (hasNakedToolCall && !inXmlToolCall) {
                  this._emitToolStreamProgress(fullResponse, round);
                  // AI 输出了裸 JSON 工具调用，截取 JSON 之前的正常文字推送（只推一次）
                  if (!this._jsonCutDone) {
                    this._jsonCutDone = true;
                    const jsonStart = fullResponse.search(/\{"tool"\s*:/);
                    if (jsonStart > 0) {
                      const prevLen = fullResponse.length - content.length;
                      if (jsonStart > prevLen) {
                        const safeContent = fullResponse.slice(prevLen, jsonStart);
                        if (safeContent.trim()) this.onChunk(safeContent);
                      }
                    }
                  }
                } else {
                  this._emitToolStreamProgress(fullResponse, round);
                }
              }
            } catch (e) { /* JSON解析错误忽略 */ }
          }
        }

        // 流结束后，处理所有完整的工具调用
        // 使用引号感知的 JSON 提取，避免 HTML 内容中的花括号干扰正则匹配
        const tcOpenTag = '<' + 'tool_call>';
        const tcCloseTag = '</' + 'tool_call>';
        let tcSearchIdx = 0;
        while (true) {
          const openIdx = fullResponse.indexOf(tcOpenTag, tcSearchIdx);
          if (openIdx === -1) break;
          const jsonStart = openIdx + tcOpenTag.length;
          const closeIdx = fullResponse.indexOf(tcCloseTag, jsonStart);
          if (closeIdx === -1) break;
          
          // 提取 tool_call 标签间的文本，找到 JSON 起点
          const innerText = fullResponse.substring(jsonStart, closeIdx);
          const jsonIdx = innerText.indexOf('{');
          if (jsonIdx === -1) {
            const lenientJson = _extractLenientGenerateHtmlToolCall(innerText);
            if (lenientJson) {
              const matchKey = fullResponse.substring(openIdx, closeIdx + tcCloseTag.length);
              if (!handledToolCalls.has(matchKey + '_executed')) {
                handledToolCalls.add(matchKey + '_executed');
                toolCallJsonList.push(lenientJson);
              }
            }
            tcSearchIdx = closeIdx + 1;
            continue;
          }
          
          const absJsonStart = jsonStart + jsonIdx;
          const extractedJson = _extractCompleteJson(fullResponse, absJsonStart);
          if (extractedJson) {
            try {
              JSON.parse(extractedJson);
              const matchKey = fullResponse.substring(openIdx, closeIdx + tcCloseTag.length);
              if (!handledToolCalls.has(matchKey + '_executed')) {
                handledToolCalls.add(matchKey + '_executed');
                toolCallJsonList.push(extractedJson);
              }
            } catch(e) {
              const lenientJson = _extractLenientGenerateHtmlToolCall(innerText);
              if (lenientJson) {
                const matchKey = fullResponse.substring(openIdx, closeIdx + tcCloseTag.length);
                if (!handledToolCalls.has(matchKey + '_executed')) {
                  handledToolCalls.add(matchKey + '_executed');
                  toolCallJsonList.push(lenientJson);
                }
              }
            }
          } else {
            const lenientJson = _extractLenientGenerateHtmlToolCall(innerText);
            if (lenientJson) {
              const matchKey = fullResponse.substring(openIdx, closeIdx + tcCloseTag.length);
              if (!handledToolCalls.has(matchKey + '_executed')) {
                handledToolCalls.add(matchKey + '_executed');
                toolCallJsonList.push(lenientJson);
              }
            }
          }
          tcSearchIdx = closeIdx + tcCloseTag.length;
        }
      };

      // 竞争：首字超时 vs 流读取
      await Promise.race([firstCharPromise, readStream()]);

      clearTimeout(totalTimeoutId);

      // 返回去除工具调用标签和裸JSON工具调用的纯文本 + 工具列表
      const cleanText = fullResponse
        .replace(/<tool_call>[\s\S]*?<\/tool_call>/g, '')  // 去除 XML 格式工具调用
        .replace(/\{"tool"\s*:[\s\S]*?\}\s*\}?/g, '')       // 去除裸 JSON 工具调用
        .trim();
      return { text: cleanText, rawText: fullResponse, toolCalls: toolCallJsonList };

    } catch (error) {
      clearTimeout(totalTimeoutId);
      clearTimeout(firstCharTimeoutId);
      // 确保 reader 被释放，防止流泄漏
      try { reader?.cancel(); } catch (_) {}
      this._currentController = null;
      this._currentReader = null;
      throw error;
    }
  }

  /**
   * 打字机效果控制（保留接口兼容性，但executeWithLLM已不依赖）
   */
  _startTypewriter() {
    // 已废弃：流式直推模式下不需要打字机缓冲
  }

  _stopTypewriter() {
    if (this._typewriterInterval) {
      clearInterval(this._typewriterInterval);
      this._typewriterInterval = null;
    }
    if (this._typewriterQueue) {
      this.onChunk(this._typewriterQueue);
      this._typewriterQueue = '';
    }
  }

  async _flushTypewriter(remaining) {
    if (remaining) {
      this.onChunk(remaining);
    }
  }

  /**
   * 处理工具调用
   */
  async handleToolCall(toolJson) {
    try {
      const { tool, args } = JSON.parse(toolJson);
      
      // 生成唯一ID用于折叠区块
      const callId = 'tool_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
      
      // 通知UI显示工具调用（可折叠）
      this.onToolCall({ 
        tool, 
        args, 
        callId,
        status: 'calling'
      });
      
      const result = await this.dispatcher.execute(tool, args);
      
      // 通知UI更新工具调用结果
      this.onToolCall({ 
        tool, 
        args, 
        result,
        callId,
        status: 'completed'
      });
      
      // 大结果工具由专门 UI 展示，避免把 JSON/HTML 再倒进过程区。
      if (!this.isCompactResultTool(tool)) {
        const resultId = 'result_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
        const resultHtml = this.formatToolResultHtml(tool, result, resultId);
        this.onChunk('\x01' + resultHtml + '\x02');
      }
      
      // TODO: 如果需要多轮工具调用，这里应该将结果返回给AI继续对话
      // 当前版本：工具执行结果直接展示给用户
      
      return result;
    } catch (error) {
      const errorId = 'error_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
      const errorHtml = this.formatToolErrorHtml(error.message, errorId);
      this.onChunk('\x01' + errorHtml + '\x02');
      return { error: error.message };
    }
  }

  /**
   * 格式化工具结果为可折叠HTML
   */
  formatToolResultHtml(tool, result, id) {
    if (this.isBrowserTool(tool)) {
      return this.formatBrowserToolResultHtml(tool, result, id);
    }

    const resultJson = JSON.stringify(result, null, 2);
    const lines = resultJson.split('\n');
    const previewLines = lines.slice(0, 3).join('\n');
    const hasMore = lines.length > 3;
    
    return `\n<div class="tool-result-block" id="${id}">
  <div class="tool-result-header">
    <span class="tool-icon">🔧</span>
    <span class="tool-name">${this.escapeHtml(tool)}</span>
    <span class="tool-status">✓</span>
    <span class="tool-toggle">▼</span>
  </div>
  <div class="tool-result-content collapsed">
    <pre class="tool-code"><code>${this.escapeHtml(resultJson)}</code></pre>
  </div>
  ${hasMore ? `<div class="tool-preview">${this.escapeHtml(previewLines)}...</div>` : ''}
</div>\n`;
  }

  isBrowserTool(tool) {
    return /^browser\./.test(String(tool || ''));
  }

  isCompactResultTool(tool) {
    return this.isBrowserTool(tool) || tool === 'generate.html';
  }

  getBrowserToolAction(tool, result = {}) {
    const failed = result?.error || result?.ok === false;
    if (failed) return '浏览器操作失败';
    const labels = {
      'browser.open': '页面已打开',
      'browser.navigate': '页面已跳转',
      'browser.click': '点击已完成',
      'browser.type': '输入已完成',
      'browser.fill': '填写已完成',
      'browser.press': '按键已完成',
      'browser.observe': '页面元素已识别',
      'browser.waitFor': '等待已完成',
      'browser.getText': '页面内容已读取',
      'browser.screenshot': '截图已完成',
      'browser.pdf': 'PDF 已生成',
      'browser.network.start': '接口捕获已启动',
      'browser.network.get': '接口记录已读取',
      'browser.network.clear': '接口记录已清空',
      'browser.network.stop': '接口捕获已停止',
      'browser.acceptance.check': '浏览器验收已完成',
      'browser.listTabs': '标签页已读取',
      'browser.close': '标签页已关闭'
    };
    return labels[tool] || '浏览器操作已完成';
  }

  getBrowserToolMeta(tool, result = {}) {
    const parts = [];
    if (result?.title) parts.push(result.title);
    if (result?.url) parts.push(result.url);
    if (result?.tabId) parts.push(`Tab ${result.tabId}`);
    if (tool === 'browser.getText' && typeof result?.text === 'string') {
      parts.push(`已读取 ${result.text.length.toLocaleString('zh-CN')} 字`);
    } else if (tool === 'browser.observe' && Array.isArray(result?.elements)) {
      parts.push(`识别 ${result.elements.length.toLocaleString('zh-CN')} 个可操作元素`);
    } else if (tool === 'browser.open' && Number.isFinite(Number(result?.textLength))) {
      parts.push(`页面文本约 ${Number(result.textLength).toLocaleString('zh-CN')} 字，按需读取`);
    } else if (Array.isArray(result?.entries)) {
      parts.push(`${result.entries.length} 条记录`);
    } else if (Array.isArray(result?.tabs)) {
      parts.push(`${result.tabs.length} 个标签页`);
    }
    if (result?.error) parts.push(result.error);
    return parts.filter(Boolean).join(' · ') || '结果已记录';
  }

  formatBrowserToolResultHtml(tool, result, id) {
    const failed = result?.error || result?.ok === false;
    const action = this.getBrowserToolAction(tool, result);
    const meta = this.getBrowserToolMeta(tool, result);
    const note = failed ? '请检查页面状态或重试。' : '原始页面内容已收起，仅保留必要进度。';

    return `\n<div class="tool-result-block browser-tool-card${failed ? ' error' : ''}" id="${id}">
  <div class="tool-result-header browser-tool-header">
    <span class="tool-icon">🌐</span>
    <span class="tool-name">浏览器操作</span>
    <span class="tool-status">${failed ? '✗' : '✓'}</span>
  </div>
  <div class="browser-tool-progress">
    <span class="browser-tool-dot${failed ? ' failed' : ''}"></span>
    <div class="browser-tool-copy">
      <div class="browser-tool-action">${this.escapeHtml(action)}</div>
      <div class="browser-tool-meta">${this.escapeHtml(meta)}</div>
      <div class="browser-tool-note">${this.escapeHtml(note)}</div>
    </div>
  </div>
</div>\n`;
  }

  formatToolResultForModel(tool, result) {
    if (!this.isBrowserTool(tool) || !result || typeof result !== 'object') return result;
    if (tool !== 'browser.open') return result;
    const { text, ...rest } = result;
    return {
      ...rest,
      textLength: rest.textLength ?? (typeof text === 'string' ? text.length : 0),
      note: rest.note || 'browser.open 只返回导航摘要；如需正文内容，请调用 browser.getText。'
    };
  }

  /**
   * 格式化工具错误为可折叠HTML
   */
  formatToolErrorHtml(message, id) {
    return `\n<div class="tool-result-block error" id="${id}">
  <div class="tool-result-header">
    <span class="tool-icon">⚠️</span>
    <span class="tool-name">工具调用失败</span>
    <span class="tool-status">✗</span>
    <span class="tool-toggle">▼</span>
  </div>
  <div class="tool-result-content collapsed">
    <pre class="tool-code error"><code>${this.escapeHtml(message)}</code></pre>
  </div>
</div>\n`;
  }

  /**
   * HTML转义
   */
  escapeHtml(text) {
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
  }
}

// ==================== 导出 ====================

if (typeof module !== 'undefined' && module.exports) {
  module.exports = { SkillRuntime, ToolDispatcher, parseSkill, runMiniSql, buildContextSqlTables, normalizeSqlTables };
} else {
  window.SkillRuntime = SkillRuntime;
  window.ToolDispatcher = ToolDispatcher;
  window.parseSkill = parseSkill;
  window.runMiniSql = runMiniSql;
}
