/**
 * bridge_client.js
 * 詹老师的AIPROS - LLM API 直连客户端
 * 
 * 架构：
 *   - 直接调用 OpenAI 兼容的 LLM API（支持阿里云/智谱/DeepSeek/Kimi 等）
 *   - 纯前端运行，不依赖任何本地服务
 *   - 文档生成（Word/Excel/PPT）通过浏览器 Blob 下载
 * 
 * 对话模式：
 *   - 直接调用 LLM API，支持工具调用（generate.docx/xlsx/pptx, web.search）
 */

const TIMEOUT_MS = 60000; // 60秒超时

// ===== 性能优化：配置缓存 + API 预热 =====
let _cachedConfig = null;     // 缓存的 API 配置
let _configCacheTime = 0;     // 缓存时间戳
const CONFIG_CACHE_TTL = 30000; // 配置缓存 30 秒

/**
 * 预热：面板打开时立即调用，提前读取配置、缓存
 */
async function warmUpAPI() {
  try {
    _cachedConfig = await _getLLMConfigUncached();
    _configCacheTime = Date.now();
    console.log('[warmUp] API 配置已预热:', { apiUrl: _cachedConfig.apiUrl, model: _cachedConfig.model, hasKey: !!_cachedConfig.apiKey });
  } catch (e) {
    console.warn('[warmUp] 预热失败:', e.message);
  }
}

/**
 * 从 chrome.storage.local 读取完整插件配置
 */
async function _getLocalSettings() {
  try {
    if (typeof chrome === 'undefined' || !chrome.storage) return {};
    const stored = await chrome.storage.local.get(['llmSettings', 'bridgeSettings']);
    const current = stored.llmSettings || {};
    const legacy = stored.bridgeSettings || {};
    if (!Object.keys(legacy).length) return current;
    return {
      ...legacy,
      ...current,
      api_key: current.api_key || current.apiKey || current.aliyun_api_key || legacy.api_key || legacy.apiKey || legacy.aliyun_api_key || legacy.fallback_api_key || '',
      api_url: current.api_url || current.apiUrl || current.aliyun_api_url || legacy.api_url || legacy.apiUrl || legacy.aliyun_api_url || legacy.fallback_api_url || '',
      model: current.model || current.aliyun_model || legacy.model || legacy.aliyun_model || legacy.fallback_model || 'qwen-plus'
    };
  } catch (_) {
    return {};
  }
}

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;
}

function _bridgeExtractCompleteHtmlDocument(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 _bridgeExtractLooseJsonStringValue(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 _bridgeExtractLenientGenerateHtmlTool(text = '') {
  const raw = String(text || '');
  const html = _bridgeExtractCompleteHtmlDocument(raw);
  if (!html) return null;
  return {
    tool: 'generate.html',
    args: {
      filename: _bridgeExtractLooseJsonStringValue(raw, 'filename') || 'AIPROS页面',
      content: html
    }
  };
}

/**
 * 不带缓存的读取 LLM API 配置
 */
async function _getLLMConfigUncached() {
  const s = await _getLocalSettings();
  return {
    apiKey: normalizeApiKeyForHeader(s.api_key || s.apiKey || s.aliyun_api_key || ''),
    apiUrl: s.api_url || s.apiUrl || s.aliyun_api_url || 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions',
    model: s.model || s.aliyun_model || 'qwen-plus'
  };
}

/**
 * 从 chrome.storage.local 读取 LLM API 配置（带缓存）
 * 返回 { apiKey, apiUrl, model } 或 {}
 */
async function _getLLMConfig() {
  // 命中缓存且未过期：直接返回（省掉 chrome.storage I/O）
  if (_cachedConfig && (Date.now() - _configCacheTime) < CONFIG_CACHE_TTL) {
    if (!_cachedConfig.apiKey) {
      _cachedConfig = await _getLLMConfigUncached();
      _configCacheTime = Date.now();
    }
    return _cachedConfig;
  }
  _cachedConfig = await _getLLMConfigUncached();
  _configCacheTime = Date.now();
  return _cachedConfig;
}

/**
 * 发送分析请求（SSE流式，直接调用 LLM API）
 * @param {Object} payload - { skill, context, message }
 * @param {Function} onChunk - 每收到一段文字回调 (text) => void
 * @param {Function} onDone  - 完成回调 () => void
 * @param {Function} onError - 错误回调 (err) => void
 * @returns {Function} cancel() 取消函数
 */
function streamAnalysis(payload, onChunk, onDone, onError) {
  let cancelled = false;
  const controller = new AbortController();

  // 打字机队列：收到的文字排队输出
  // 动态速度：队列积压少时慢（打字感），积压多时加速（避免远落后于网络）
  let typewriterQueue = '';
  let typewriterTimer = null;
  let networkDone = false;
  let pendingOnDone = null; // 缓存的 onDone 回调

  function getCharDelay() {
    const qLen = typewriterQueue.length;
    if (qLen > 500) return 2;   // 严重积压：2ms 极速追赶
    if (qLen > 200) return 5;   // 积压较多：5ms 快速
    if (qLen > 50)  return 8;   // 轻微积压：8ms 较快
    return 14;                   // 正常：14ms 打字感
  }

  function flushTypewriter(onAllDone) {
    // 缓存 onDone 回调（即使 timer 已在跑，也要记住新的 onDone）
    if (onAllDone) pendingOnDone = onAllDone;
    if (typewriterTimer) return; // 已在跑
    function tick() {
      if (cancelled) return;
      if (typewriterQueue.length === 0) {
        typewriterTimer = null;
        if (networkDone && pendingOnDone) {
          const cb = pendingOnDone;
          pendingOnDone = null;
          cb();
        }
        return;
      }
      // 根据积压量决定每次吐出多少字符
      const qLen = typewriterQueue.length;
      let chunk;
      if (qLen > 200) {
        // 积压严重：一次吐出多个字符（最多8个），但遇换行截断
        const nlIdx = typewriterQueue.indexOf('\n');
        const take = Math.min(qLen > 500 ? 10 : 5, nlIdx >= 0 ? nlIdx + 1 : qLen);
        chunk = typewriterQueue.slice(0, take);
        typewriterQueue = typewriterQueue.slice(take);
      } else {
        // 正常：逐字输出，换行符前有少量字符时一起带出
        const nlIdx = typewriterQueue.indexOf('\n');
        if (nlIdx === 0) {
          chunk = '\n';
          typewriterQueue = typewriterQueue.slice(1);
        } else if (nlIdx > 0 && nlIdx <= 4) {
          chunk = typewriterQueue.slice(0, nlIdx + 1);
          typewriterQueue = typewriterQueue.slice(nlIdx + 1);
        } else {
          chunk = typewriterQueue[0];
          typewriterQueue = typewriterQueue.slice(1);
        }
      }
      onChunk?.(chunk);
      typewriterTimer = setTimeout(tick, getCharDelay());
    }
    typewriterTimer = setTimeout(tick, getCharDelay());
  }

  (async () => {
    try {
      // 直接调用 LLM API（不依赖任何本地服务）
      const config = await _getLLMConfig();
      if (!config.apiKey) {
        throw new Error('LLM API未配置，请在设置中填入 API Key');
      }

      // 构建 Skill 分析的系统提示
      const skillPrompt = payload.skill 
        ? `你是「詹老师的AIPROS」，擅长网页理解、资料整理、任务规划、自动化执行，以及PRD、需求、技术方案、运营和流程材料分析。正在使用「${payload.skill}」Skill 处理页面、文档、网页或公开资料，请基于以下上下文给出清晰、可靠、可执行的结果。优先使用当前页面证据，可结合公开联网搜索；不要依赖企业内部深数据，不要编造事实：\n\n`
        : '你是「詹老师的AIPROS」，请基于页面、附件、用户问题、用户粘贴的网址和公开网页完成分析或整理；优先给结构化、可执行、方便用户直接使用的结论：\n\n';
      
      const contextStr = payload.context 
        ? `【页面上下文】\n${JSON.stringify(payload.context, null, 2).slice(0, 5000)}\n\n` 
        : '';
      
      const userMessage = contextStr + (payload.message || '');

      // 截断过长消息
      const MAX_CHARS = 20000;
      const finalMsg = userMessage.length > MAX_CHARS
        ? userMessage.slice(0, MAX_CHARS) + '\n\n[...内容过长已截断]'
        : userMessage;

      const resp = await fetch(config.apiUrl, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${config.apiKey}`
        },
        body: JSON.stringify({
          model: config.model,
          messages: [
            { role: 'system', content: skillPrompt },
            { role: 'user', content: finalMsg }
          ],
          stream: true,
          temperature: 0.7,
          max_tokens: 4096
        }),
        signal: controller.signal
      });

      if (!resp.ok) {
        throw new Error(`LLM API返回错误 ${resp.status}: ${await resp.text()}`);
      }

      const reader = resp.body?.getReader();
      if (!reader) throw new Error('流式读取不可用');
      const decoder = new TextDecoder('utf-8');
      let buffer = '';

      try {
        while (true) {
          if (cancelled) break;
          const { done, value } = await reader.read();
          if (done) break;

          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]') {
              networkDone = true;
              flushTypewriter(onDone);
              return;
            }
            if (!data) continue;
            try {
              const parsed = JSON.parse(data);
              const text = parsed.choices?.[0]?.delta?.content || '';
              if (text) {
                typewriterQueue += text;
                flushTypewriter(null);
              }
            } catch {
              // 非 JSON 数据忽略
            }
          }
        }

        networkDone = true;
        flushTypewriter(onDone);
      } finally {
        try { reader.cancel(); } catch (_) {}
      }
    } catch (err) {
      if (err.name === 'AbortError') return;
      onError?.(err.message || String(err));
    }
  })();

  return function cancel() {
    cancelled = true;
    if (typewriterTimer) clearTimeout(typewriterTimer);
    controller.abort();
  };
}

/**
 * 发送对话消息（直接调用LLM API，不经过Skill运行时）
 * @param {string} message - 用户输入
 * @param {Object} context - 当前页面上下文
 * @param {Function} onChunk
 * @param {Function} onDone
 * @param {Function} onError
 * @param {Object|null} priorContext - Skill 分析结果上下文 { skill, analysisResult }，有则拼入背景
 * @returns {Function} cancel()
 */
function sendChatMessage(message, context, onChunk, onDone, onError, priorContext, conversationHistory) {
  console.log(`[perf] sendChatMessage 开始, 消息长度=${message?.length || 0}`);
  const sendStart = Date.now();
  let fullMessage = message;
  globalThis.__skillRuntimeCurrentContext = { pageContext: context || {}, priorContext: priorContext || null };
  
  // 构建页面上下文前缀
  let contextPrefix = '';
  const ctx = context || {};
  // 合并常规读取、选区复制和截图识别的文字，避免“状态栏已读取但对话没内容”。
  const pageContent = [ctx.pageText, ctx.text, ctx.selectCopyText, ctx.screenshotText]
    .filter(Boolean)
    .join('\n\n');
  if (ctx && (ctx.billId || ctx.pageTitle || ctx.title || ctx.pageUrl || ctx.url || pageContent)) {
    const ctxParts = [];
    if (ctx.billId) ctxParts.push(`单号: ${ctx.billId}`);
    if (ctx.pageTitle) ctxParts.push(`页面标题: ${ctx.pageTitle}`);
    if (ctx.title) ctxParts.push(`标题: ${ctx.title}`);
    if (ctx.pageUrl || ctx.url) ctxParts.push(`页面地址: ${ctx.pageUrl || ctx.url}`);
    if (pageContent) {
      // 页面阅读是插件核心能力，对话默认给足正文上下文。
      const MAX_CONTEXT_CHARS = 8000;
      const text = pageContent.length > MAX_CONTEXT_CHARS 
        ? pageContent.slice(0, MAX_CONTEXT_CHARS) + '\n...[已截断]'
        : pageContent;
      ctxParts.push(`页面内容:\n${text}`);
    }
    if (ctx.fields && Object.keys(ctx.fields).length > 0) {
      const fieldText = Object.entries(ctx.fields).slice(0, 20).map(([k, v]) => `${k}: ${String(v).slice(0, 300)}`).join('\n');
      if (fieldText) ctxParts.push(`页面字段:\n${fieldText}`);
    }
    if (Array.isArray(ctx.tableData) && ctx.tableData.length > 0) {
      ctxParts.push(`页面表格: ${ctx.tableData.length} 个，可通过 sql.query 进一步分析`);
    }
    contextPrefix = `【当前页面上下文】\n${ctxParts.join('\n')}\n\n`;
  }
  
  if (priorContext && priorContext.analysisResult) {
    // 截断过长的分析结果（避免单次请求过大）
    const MAX_PRIOR_CHARS = 2000;
    const result = priorContext.analysisResult.length > MAX_PRIOR_CHARS
      ? priorContext.analysisResult.slice(0, MAX_PRIOR_CHARS) + '\n...[已截断]'
      : priorContext.analysisResult;
    fullMessage = `${contextPrefix}【背景】刚才 Skill「${priorContext.skill}」的分析结果：\n\n${result}\n\n【用户追问】${message}`;
  } else {
    fullMessage = contextPrefix + message;
  }
  
  // 知识库检索：完全不阻塞对话请求（异步后台执行，下轮对话才注入）
  let kbContext = '';
  if (typeof searchKnowledge === 'function') {
    // 异步搜索，不等待结果，不影响首字响应速度
    searchKnowledge(message, 2).then(kbResults => {
      if (kbResults && kbResults.length > 0) {
        console.log('[知识库] 后台检索完成，下次对话注入');
      }
    }).catch(() => {});
  }
  fullMessage = kbContext + fullMessage;
  
  console.log(`[perf] sendChatMessage 预处理完成: ${Date.now() - sendStart}ms, 最终消息长度=${fullMessage.length}`);
  
  // 对话模式直接调用LLM API
  return streamLLMChat(fullMessage, onChunk, onDone, onError, conversationHistory);
}

/**
 * LLM API流式对话（对话模式专用，带首字超时和重试）
 * @param {string} message - 完整消息
 * @param {Function} onChunk
 * @param {Function} onDone
 * @param {Function} onError
 * @returns {Function} cancel()
 */
function streamLLMChat(message, onChunk, onDone, onError, conversationHistory) {
  let cancelled = false;
  const controller = new AbortController();
  if (globalThis.AgentLoop?.startRun) {
    const activeRun = globalThis.AgentLoop.snapshot?.();
    if (activeRun?.source === 'shortcut' && activeRun.status === 'running') {
      globalThis.AgentLoop.setStep?.({ id: 'execute', status: 'in_progress', message: '模型对话已启动' });
      globalThis.AgentLoop.log?.('info', '模型对话已启动');
    } else {
      globalThis.AgentLoop.startRun({
        source: 'chat',
        title: '对话任务',
        objective: message
      });
    }
  }
  
  // 打字机队列
  let typewriterQueue = '';
  let typewriterTimer = null;
  let networkDone = false;
  let firstCharTimeoutId = null; // 移到外层作用域
  let pendingOnDone = null; // 缓存的 onDone 回调
  
  // 超时配置
  const FIRST_CHAR_TIMEOUT = 5000; // 首字5秒超时
  const TOTAL_TIMEOUT = 180000; // 总超时3分钟（长文本输出需要更久）
  const MAX_RETRIES = 2; // 最大重试次数
  
  function getCharDelay() {
    const qLen = typewriterQueue.length;
    if (qLen > 500) return 2;
    if (qLen > 200) return 5;
    if (qLen > 50) return 8;
    return 14;
  }
  
  function flushTypewriter(onAllDone) {
    // 缓存 onDone 回调（即使 timer 已在跑，也要记住新的 onDone）
    if (onAllDone) pendingOnDone = onAllDone;
    if (typewriterTimer) return;
    function tick() {
      if (cancelled) return;
      if (typewriterQueue.length === 0) {
        typewriterTimer = null;
        if (networkDone && pendingOnDone) {
          const cb = pendingOnDone;
          pendingOnDone = null;
          cb();
        }
        return;
      }
      const qLen = typewriterQueue.length;
      let chunk;
      if (qLen > 200) {
        const nlIdx = typewriterQueue.indexOf('\n');
        const take = Math.min(qLen > 500 ? 10 : 5, nlIdx >= 0 ? nlIdx + 1 : qLen);
        chunk = typewriterQueue.slice(0, take);
        typewriterQueue = typewriterQueue.slice(take);
      } else {
        const nlIdx = typewriterQueue.indexOf('\n');
        if (nlIdx === 0) {
          chunk = '\n';
          typewriterQueue = typewriterQueue.slice(1);
        } else if (nlIdx > 0 && nlIdx <= 4) {
          chunk = typewriterQueue.slice(0, nlIdx + 1);
          typewriterQueue = typewriterQueue.slice(nlIdx + 1);
        } else {
          chunk = typewriterQueue[0];
          typewriterQueue = typewriterQueue.slice(1);
        }
      }
      onChunk?.(chunk);
      typewriterTimer = setTimeout(tick, getCharDelay());
    }
    typewriterTimer = setTimeout(tick, getCharDelay());
  }
  
  // 带重试的请求
  async function fetchWithRetry(url, options, retryCount = 0) {
    try {
      const resp = await fetch(url, options);
      return resp;
    } catch (err) {
      if (retryCount < MAX_RETRIES && _isRetryableError(err)) {
        console.warn(`[bridge_client] 请求失败，${retryCount + 1}/${MAX_RETRIES}次尝试后重试...`);
        await _sleep(1000 * (retryCount + 1));
        return fetchWithRetry(url, options, retryCount + 1);
      }
      throw err;
    }
  }
  
  function _isRetryableError(err) {
    const msg = err.message?.toLowerCase() || '';
    return msg.includes('network') || msg.includes('timeout') || msg.includes('abort') ||
           msg.includes('failed to fetch') || msg.includes('connection');
  }
  
  function _sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
  
  (async () => {
    let retryAttempt = 0;
    const perfStart = Date.now();
    
    while (retryAttempt <= MAX_RETRIES) {
      try {
        if (retryAttempt > 0) {
          console.log(`[bridge_client] 第${retryAttempt + 1}次尝试...`);
          await _sleep(1000 * retryAttempt);
        }
        
        const t0 = Date.now();
        const config = await _getLLMConfig();
        console.log(`[perf] 读取配置: ${Date.now() - t0}ms, model=${config.model}`);
        
        if (!config.apiKey) {
          throw new Error('LLM API未配置，请在设置中填入 API Key');
        }
        
        // 截断过长消息
        const MAX_CHARS = 20000;
        const finalMsg = message.length > MAX_CHARS
          ? message.slice(0, MAX_CHARS) + '\n\n[...内容过长已截断]'
          : message;
        
        // 总超时控制
        const totalTimeoutId = setTimeout(() => controller.abort(), TOTAL_TIMEOUT);
        
        // 当前时间（对话模式也注入时间）
        const now = new Date();
        const chatTimeStr = now.toLocaleDateString('zh-CN', { year:'numeric', month:'long', day:'numeric', weekday:'long' })
          + ' ' + now.toLocaleTimeString('zh-CN', { hour:'2-digit', minute:'2-digit' });
        
        // 工具调用标签（用变量避免被 XML 解析器吞掉）
        const TC_OPEN = '<' + 'tool_call>';
        const TC_CLOSE = '</' + 'tool_call>';
        const TA_OPEN = '<' + 'tool_action>';
        const TA_CLOSE = '</' + 'tool_action>';
        const TOOL_OPEN_TAGS = [TC_OPEN, TA_OPEN];

        // 构建 messages（支持多轮工具调用）
        const messages = [
          { role: 'system', content: `你是「詹老师的AIPROS」，具备网页理解、资料整理、任务规划、文档生成、浏览器自动化和工作流协同能力，也能处理PRD、需求评审、技术方案、运营方案、流程图和流程文件等专业任务。当前时间：${chatTimeStr}。

专长：读取网页和文件、整理信息、生成文档、生成HTML页面、生成前端工程项目、规划任务、操作浏览器、设置自动化；也可以生成PRD、评审需求、评审技术方案、拆解用户故事、排序需求优先级、构建流程图、制定运营方案和流程文件。默认只使用当前页面、用户粘贴网址、附件和公开网页，不依赖企业内部深数据。涉及筛选、分组、排序、聚合、测算时，要调用 sql.query 真正执行，不要假装执行 SQL。

## 文档生成规则
1. 需求不明确时先追问，不要直接生成空洞内容
2. 调用任何 Skill 或生成正式文档前，先确认任务对象、目标、材料、范围和约束是否足够；不足时最多追问3个关键问题，不要空跑
3. 用户明确说“先出草稿/按现有信息/不用追问”时，才基于已有信息产出，并把缺口标为“待补充”
4. 生成文档时必须先在回复中输出完整内容(Markdown格式)
5. 只有用户明确说"下载/导出Word/Excel/PPT/HTML"时才调用工具，普通对话不调用
6. 当用户或Skill要求生成HTML页面/报告时，将完整HTML代码通过 generate.html 工具输出（content字段为完整HTML源码），系统会自动下载并在浏览器新标签页打开
7. 当用户或Skill要求生成 React/Vue/Vite/Tailwind、项目源码、组件库、可运行项目或工程级前端项目时，使用 project.generate 一次写入多文件项目，而不是只贴代码

## 通用网页操作规则
1. 用户要求“打开网页/点击/输入/填写/搜索/选择/勾选/登录后操作/自动操作网页”时，必须使用 browser.open/browser.observe/browser.click/browser.type/browser.fill/browser.press 等浏览器工具；不要只用 web.fetch、页面快照或口头说明替代真实操作
2. 进入陌生网页后，先调用 browser.observe 获取可操作元素清单；目标清楚时可直接用 selector、text、label、placeholder、role 或坐标点击/输入
3. 点击使用 browser.click，支持 selector、text/texts、label、placeholder、role、index、x/y；输入使用 browser.type 或 browser.fill，支持 selector、label、placeholder、name、role、search
4. 用户只要求“输入/填写/选择”时不要自动提交；用户明确说“搜索/回车/提交/确认”时再调用 browser.press 或 browser.click
5. 搜索框不要写死单一网站：百度可用 #kw/input[name=wd]，Google/Bing/DuckDuckGo 常见为 input[name=q]/textarea[name=q]/input[type=search]；不确定时先 browser.observe 再操作
6. 浏览器内部页、扩展页、权限禁止页和跨域 iframe 内部结构可能无法读取 DOM；此时优先用可见坐标点击、截图或让用户切到可操作页面

## 审批页面操作规则
1. 用户要求“填审批意见/写入意见/回填/注入到页面”时，应先拟好或提取审批意见，再调用 browser.fill 写入页面；不要只把意见写在聊天里
2. 用户明确说“提交/提交审批/同意并提交/通过并提交/批准并提交/驳回并提交”时，填入意见后继续调用 browser.click 点击提交/确认/同意/通过等按钮
3. 用户只说“填同意/写无异议/帮我想审批意见”但没有明确“提交”时，不要点击提交按钮
4. 审批意见要短、明确、适合粘贴到意见框，通常 1-3 句话

## 接口抓取规则
1. 用户要求“抓接口/看 XHR/fetch/获取接口信息”时，先用 browser.open 或当前标签页定位目标页面，再调用 browser.network.start 启动捕获
2. 启动捕获后执行用户指定的查询、保存、提交、导出等页面动作，再调用 browser.network.get 读取真实请求
3. 输出接口清单时要区分真实抓到的请求和推断线索；真实请求优先给 URL、方法、状态码、请求体、响应摘要和 curl 草稿

## Agent 执行闭环规则
1. 多步骤、会写文件、会操作页面、会执行命令或会调用多个工具的任务，先调用 agent.plan 生成任务计划
2. 关键步骤开始、完成或失败时调用 agent.step 更新状态
3. 本地写入、删除、移动、命令执行、项目生成等操作会触发权限确认；用户拒绝时停止执行并说明
4. 修改本地文件前系统会自动创建回滚点；需要恢复时使用 checkpoint.restore
5. 对页面生成、页面操作或接口抓取类交付，完成后优先调用 browser.acceptance.check 做真实浏览器验收

工具格式：
- web.fetch：${TC_OPEN}{"tool":"web.fetch","args":{"url":"https://example.com","save":false}}${TC_CLOSE}
- web.search：${TC_OPEN}{"tool":"web.search","args":{"query":"关键词","max_results":5}}${TC_CLOSE}
- sql.query：${TC_OPEN}{"tool":"sql.query","args":{"query":"SELECT segment, COUNT(*) AS n FROM market GROUP BY segment","tables":{"market":[{"segment":"A"},{"segment":"B"}]}}}${TC_CLOSE}
- generate.docx：${TC_OPEN}{"tool":"generate.docx","args":{"filename":"文件名","content":"内容"}}${TC_CLOSE}
- generate.xlsx：${TC_OPEN}{"tool":"generate.xlsx","args":{"filename":"文件名","content":"表格内容"}}${TC_CLOSE}
- generate.pptx：${TC_OPEN}{"tool":"generate.pptx","args":{"filename":"文件名","content":"幻灯片内容，---分隔"}}${TC_CLOSE}
- generate.html：${TC_OPEN}{"tool":"generate.html","args":{"filename":"文件名","content":"完整HTML代码"}}${TC_CLOSE}
- cloudflare.pages.deploy：${TC_OPEN}{"tool":"cloudflare.pages.deploy","args":{"projectName":"项目名","filename":"index.html","content":"完整HTML代码","branch":"main"}}${TC_CLOSE}
- project.generate：${TC_OPEN}{"tool":"project.generate","args":{"projectName":"项目名","framework":"react","files":[{"path":"package.json","content":"完整文件内容"},{"path":"src/main.jsx","content":"完整文件内容"}],"runInstall":false,"runBuild":false,"runGitInit":false}}${TC_CLOSE}
- browser.open：${TC_OPEN}{"tool":"browser.open","args":{"url":"https://example.com","newTab":true}}${TC_CLOSE}
- browser.click：${TC_OPEN}{"tool":"browser.click","args":{"selector":"button[type=submit]"}}${TC_CLOSE}
- browser.click 审批提交：${TC_OPEN}{"tool":"browser.click","args":{"intent":"approval-submit","texts":["提交审批","提交","确认提交","确定","确认","同意","通过","批准"],"waitAfter":800}}${TC_CLOSE}
- browser.observe：${TC_OPEN}{"tool":"browser.observe","args":{"limit":80,"maxText":4000}}${TC_CLOSE}
- browser.type：${TC_OPEN}{"tool":"browser.type","args":{"selector":"input[name=q]","text":"要输入的内容","clear":true}}${TC_CLOSE}；也可用 {"search":true,"text":"什么是AI","clear":true} 自动寻找搜索框
- browser.fill：${TC_OPEN}{"tool":"browser.fill","args":{"selector":"textarea[name=comment]","text":"要填写的内容","clear":true}}${TC_CLOSE}；selector 可省略，省略时会填写当前聚焦或页面中最像“意见/备注/评论”的输入框
- browser.getText：${TC_OPEN}{"tool":"browser.getText","args":{"selector":"main","maxLength":4000}}${TC_CLOSE}
- browser.network.start：${TC_OPEN}{"tool":"browser.network.start","args":{"resourceTypes":["XHR","Fetch"],"includeBodies":true,"maxEntries":300}}${TC_CLOSE}
- browser.network.get：${TC_OPEN}{"tool":"browser.network.get","args":{"limit":50,"includeBodies":true,"includeCurl":true}}${TC_CLOSE}
- browser.network.clear：${TC_OPEN}{"tool":"browser.network.clear","args":{}}${TC_CLOSE}
- browser.acceptance.check：${TC_OPEN}{"tool":"browser.acceptance.check","args":{"url":"https://example.com","startNetwork":true,"waitMs":1000,"checks":[{"type":"selector","selector":"main"},{"type":"text","text":"关键词"},{"type":"network","minCount":1}]}}${TC_CLOSE}
- agent.plan：${TC_OPEN}{"tool":"agent.plan","args":{"title":"任务名","objective":"目标","steps":[{"id":"s1","title":"读取材料","status":"pending"},{"id":"s2","title":"执行工具","status":"pending"}]}}${TC_CLOSE}
- agent.step：${TC_OPEN}{"tool":"agent.step","args":{"stepId":"s1","status":"completed","message":"已完成"}}${TC_CLOSE}
- agent.status：${TC_OPEN}{"tool":"agent.status","args":{}}${TC_CLOSE}
- agent.audit.list：${TC_OPEN}{"tool":"agent.audit.list","args":{"limit":20}}${TC_CLOSE}
- agent.audit.replay：${TC_OPEN}{"tool":"agent.audit.replay","args":{"runId":"审计运行ID"}}${TC_CLOSE}
- checkpoint.restore：${TC_OPEN}{"tool":"checkpoint.restore","args":{"id":"回滚点ID","confirm":true}}${TC_CLOSE}
- fs.write：${TC_OPEN}{"tool":"fs.write","args":{"path":"/绝对路径/文件.txt","content":"内容"}}${TC_CLOSE}
- fs.read：${TC_OPEN}{"tool":"fs.read","args":{"path":"/绝对路径/文件.txt","maxBytes":200000}}${TC_CLOSE}
- fs.list：${TC_OPEN}{"tool":"fs.list","args":{"path":"/绝对路径/目录","limit":100}}${TC_CLOSE}
- fs.search：${TC_OPEN}{"tool":"fs.search","args":{"path":"/绝对路径/目录","query":"关键词","includeContent":true,"maxResults":50}}${TC_CLOSE}
- fs.move：${TC_OPEN}{"tool":"fs.move","args":{"src":"/绝对路径/旧文件","dst":"/绝对路径/新文件","dryRun":true}}${TC_CLOSE}
- skill.list：${TC_OPEN}{"tool":"skill.list","args":{}}${TC_CLOSE}
- skill.read：${TC_OPEN}{"tool":"skill.read","args":{"name":"Skill名"}}${TC_CLOSE}
- skill.write：${TC_OPEN}{"tool":"skill.write","args":{"name":"Skill名","content":"完整SKILL.md","overwrite":false}}${TC_CLOSE}
- skill.validate：${TC_OPEN}{"tool":"skill.validate","args":{"content":"完整SKILL.md"}}${TC_CLOSE}
- skill.evaluate：${TC_OPEN}{"tool":"skill.evaluate","args":{"content":"完整SKILL.md","positivePrompts":["正例"],"negativePrompts":["反例"]}}${TC_CLOSE}
- skill.benchmark：${TC_OPEN}{"tool":"skill.benchmark","args":{"minScore":70}}${TC_CLOSE}
- shell.exec：${TC_OPEN}{"tool":"shell.exec","args":{"command":["npm","run","build"],"cwd":"/绝对路径"}}${TC_CLOSE}

禁止：只说"好的"不输出内容；输出空洞框架。

流程图或结构图：直接输出 \`\`\`mermaid 代码块，graph LR/sequenceDiagram/graph TD。` },
          { role: 'user', content: finalMsg }
        ];
        
        // === 注入对话历史记忆（已由 panel.js 的 buildConversationContext() 预处理） ===
        // conversationHistory 已经是：摘要 + 最近完整消息，无需再截断
        if (conversationHistory && conversationHistory.length > 0) {
          // 直接注入所有历史（已压缩，总量可控）
          // 注意：buildConversationContext() 不包含当前消息，所以直接 splice
          messages.splice(1, 0, ...conversationHistory);
        }
        
        const t1 = Date.now();
        console.log(`[perf] 构建请求: ${t1 - t0}ms, messages=${messages.length}条`);
        
        const resp = await fetchWithRetry(config.apiUrl, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${config.apiKey}`
          },
          body: JSON.stringify({
            model: config.model,
            messages: messages,
            stream: true,
            temperature: 0.7,
            max_tokens: 16384  // 需要足够大以容纳 generate.html 的完整 HTML 内容
          }),
          signal: controller.signal
        });
        
        if (!resp.ok) {
          const errText = await resp.text();
          throw new Error(`LLM API返回错误 ${resp.status}: ${errText}`);
        }
        
        const reader = resp.body?.getReader();
        if (!reader) throw new Error('流式读取不可用');
        const decoder = new TextDecoder('utf-8');
        let buffer = '';
        let firstCharReceived = false;
        firstCharTimeoutId = null; // 重置
        
        // 首字超时Promise
        const firstCharPromise = new Promise((_, reject) => {
          firstCharTimeoutId = setTimeout(() => {
            if (!firstCharReceived) {
              reject(new Error('首字响应超时'));
            }
          }, FIRST_CHAR_TIMEOUT);
        });
        
        // 读取流（累积全文用于工具调用检测）
        let fullResponseText = '';
        let toolCallDetected = false; // 标记是否已检测到工具调用（阻止后续文本进入打字机）
        
        const readStream = async () => {
          while (true) {
            if (cancelled) break;
            const { done, value } = await reader.read();
            if (done) break;
            
            // 首字到达
            if (!firstCharReceived) {
              firstCharReceived = true;
              clearTimeout(firstCharTimeoutId);
              console.log(`[perf] ⚡ 首字到达: ${Date.now() - t1}ms (总${Date.now() - perfStart}ms)`);
            }
            
            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]') {
                // 流结束：检测并执行工具调用
                await handleChatToolCalls();
                networkDone = true;
                flushTypewriter(onDone);
                return;
              }
              if (!data) continue;
              try {
                const parsed = JSON.parse(data);
                const text = parsed.choices?.[0]?.delta?.content || '';
                if (text) {
                  fullResponseText += text;
                  // 实时检测工具调用标签：如果文本中出现了 <tool_call、<tool_action 或 {"tool":
                  // 则暂缓输出到打字机（避免 JSON / 乱码泄露到界面）
                  if (!toolCallDetected && (fullResponseText.includes('<' + 'tool_call>') ||
                      fullResponseText.includes('<' + 'tool_action>') ||
                      /\{\s*"tool"\s*:/.test(fullResponseText))) {
                    toolCallDetected = true;
                    // 第一次检测到工具调用：截断打字机队列到工具调用之前的位置
                    const tcOpen = '<' + 'tool_call>';
                    const taOpen = '<' + 'tool_action>';
                    const jsonMarker = fullResponseText.search(/\{\s*"tool"\s*:/);
                    const starts = [fullResponseText.indexOf(tcOpen), fullResponseText.indexOf(taOpen), jsonMarker]
                      .filter(pos => pos >= 0);
                    let toolCallStart = starts.length ? Math.min(...starts) : -1;
                    if (toolCallStart > 0) {
                      // 只保留工具调用之前的安全文本
                      const safeText = fullResponseText.substring(0, toolCallStart);
                      typewriterQueue = safeText;
                    }
                    flushTypewriter(null);
                  }
                  // 工具调用检测后，不再向打字机追加文本
                  if (!toolCallDetected) {
                    typewriterQueue += text;
                    flushTypewriter(null);
                  }
                }
              } catch (_) {}
            }
          }
          
          // 流自然结束（无 [DONE] 标记）：也检测工具调用
          await handleChatToolCalls();
          networkDone = true;
          flushTypewriter(onDone);
        };
        
        // 工具调用检测与执行（流结束后调用）
        // 使用花括号计数法提取 JSON，比正则更可靠
        async function handleChatToolCalls() {
          const markerMatch = fullResponseText.match(/\{\s*"tool"\s*:/);
          const idx = markerMatch ? markerMatch.index : -1;
          if (idx === -1) {
            const looseGenerate = _bridgeExtractLenientGenerateHtmlTool(fullResponseText);
            if (looseGenerate) {
              if (typeof window.__replaceChatText === 'function') {
                window.__replaceChatText(fullResponseText.replace(/<tool_call>[\s\S]*?<\/tool_call>/g, '').trim());
              }
              try {
                if (typeof window.__showDocGenProgress === 'function') window.__showDocGenProgress('HTML 报告');
                if (typeof window.__updateDocGenStep === 'function') window.__updateDocGenStep('build');
                let generatedArtifact = null;
                const genResult = await executeChatToolWithAgentLoop('generate.html', looseGenerate.args || {}, async () => {
                  generatedArtifact = await _frontendLocalGenerate('html', looseGenerate.args || {});
                  return { ok: true, format: 'html', filename: generatedArtifact.filename, url: generatedArtifact.url, size: generatedArtifact.size };
                });
                if (genResult?.error) throw new Error(genResult.error);
                if (typeof window.__updateDocGenStep === 'function') window.__updateDocGenStep('save');
                if (typeof window.__showDocGenDone === 'function') window.__showDocGenDone(generatedArtifact);
              } catch (genErr) {
                if (typeof window.__showDocGenError === 'function') window.__showDocGenError('生成失败：' + genErr.message);
              }
              networkDone = true;
              flushTypewriter(onDone);
              return;
            }
            // 没有找到完整工具调用 JSON，但可能 LLM 输出了残缺的工具调用
            if (toolCallDetected) {
              // 清理：截断到工具调用标签之前
              const tagStarts = TOOL_OPEN_TAGS.map(tag => fullResponseText.indexOf(tag)).filter(pos => pos >= 0);
              const tcIdx = tagStarts.length ? Math.min(...tagStarts) : -1;
              const cleanTo = tcIdx > 0 ? tcIdx : 0;
              if (cleanTo > 0) {
                const safeText = fullResponseText.substring(0, cleanTo).trim();
                if (typeof window.__replaceChatText === 'function') {
                  window.__replaceChatText(safeText);
                }
              }
              typewriterQueue += '\n\n⚠️ 工具调用格式异常，LLM 可能输出了异常内容，请重试。';
              networkDone = true;
              flushTypewriter(onDone);
            }
            
            return;
          }
          
          // 引号感知的花括号计数，找到完整 JSON
          // 跳过字符串内的花括号，避免 HTML/CSS/JS 内容中的 { } 干扰 JSON 边界检测
          let depth = 0, end = -1;
          let inStr = false, strChar = '';
          for (let i = idx; i < fullResponseText.length; i++) {
            const ch = fullResponseText[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) { end = i + 1; break; }
            }
          }
          if (end === -1) {
            const looseGenerate = _bridgeExtractLenientGenerateHtmlTool(fullResponseText);
            if (looseGenerate) {
              try {
                if (typeof window.__showDocGenProgress === 'function') window.__showDocGenProgress('HTML 报告');
                let generatedArtifact = null;
                const genResult = await executeChatToolWithAgentLoop('generate.html', looseGenerate.args || {}, async () => {
                  generatedArtifact = await _frontendLocalGenerate('html', looseGenerate.args || {});
                  return { ok: true, format: 'html', filename: generatedArtifact.filename, url: generatedArtifact.url, size: generatedArtifact.size };
                });
                if (genResult?.error) throw new Error(genResult.error);
                if (typeof window.__showDocGenDone === 'function') window.__showDocGenDone(generatedArtifact);
              } catch (genErr) {
                if (typeof window.__showDocGenError === 'function') window.__showDocGenError('生成失败：' + genErr.message);
              }
              networkDone = true;
              flushTypewriter(onDone);
              return;
            }
            // JSON 不完整（LLM 可能输出了异常 token 后被截断）
            if (toolCallDetected) {
              const tagStarts = TOOL_OPEN_TAGS.map(tag => fullResponseText.indexOf(tag)).filter(pos => pos >= 0);
              const tcIdx = tagStarts.length ? Math.min(...tagStarts) : -1;
              const cleanTo = tcIdx > 0 ? tcIdx : idx;
              const safeText = fullResponseText.substring(0, cleanTo).trim();
              if (typeof window.__replaceChatText === 'function') {
                window.__replaceChatText(safeText);
              }
              typewriterQueue += '\n\n⚠️ 工具调用内容不完整（可能因输出过长被截断），请重试。';
              networkDone = true;
              flushTypewriter(onDone);
            }
            return;
          }
          
          const jsonStr = fullResponseText.substring(idx, end);
          let parsed;
          try { parsed = JSON.parse(jsonStr); } catch(e) {
            const looseGenerate = _bridgeExtractLenientGenerateHtmlTool(fullResponseText);
            if (looseGenerate) {
              parsed = looseGenerate;
            } else {
            // JSON 解析失败（可能包含异常字符），清理界面
              if (toolCallDetected) {
                const tagStarts = TOOL_OPEN_TAGS.map(tag => fullResponseText.indexOf(tag)).filter(pos => pos >= 0);
                const tcIdx = tagStarts.length ? Math.min(...tagStarts) : -1;
                const cleanTo = tcIdx > 0 ? tcIdx : idx;
                const safeText = fullResponseText.substring(0, cleanTo).trim();
                if (typeof window.__replaceChatText === 'function') {
                  window.__replaceChatText(safeText);
                }
                typewriterQueue += '\n\n⚠️ 工具调用解析失败，请重试。';
                networkDone = true;
                flushTypewriter(onDone);
              }
              return;
            }
          }
          
          const { tool, args } = parsed;
          if (!tool) return;
          
          // 清理气泡：移除工具调用 JSON 及其周围的 XML 标签
          // 找到 JSON 前的 <tool_call 标签起点
          let cleanStart = idx;
          const beforeJson = fullResponseText.substring(Math.max(0, idx - 30), idx);
          const tagOpenMatch = beforeJson.lastIndexOf('<');
          if (tagOpenMatch >= 0) {
            const prefix = beforeJson.substring(tagOpenMatch);
            if (prefix.startsWith('<tool_call') || prefix.startsWith('<tool_action')) {
              cleanStart = Math.max(0, idx - 30) + tagOpenMatch;
            }
          }
          // 找到 JSON 后的 </tool_call 标签终点
          let cleanEnd = end;
          const afterJson = fullResponseText.substring(end, Math.min(fullResponseText.length, end + 30));
          const closeMatch = afterJson.indexOf('>');
          if (closeMatch >= 0 && (/\/tool_call|\/tool_action/.test(afterJson.substring(0, closeMatch + 1)))) {
            cleanEnd = end + closeMatch + 1;
          }
          // 同时清理 JSON 前的 "现在为你生成：" 等引导文字
          let lineStart = fullResponseText.lastIndexOf('\n', cleanStart);
          if (lineStart === -1) lineStart = 0;
          const beforeLine = fullResponseText.substring(lineStart, cleanStart).trim();
          if (beforeLine.length < 30 && (beforeLine.includes('生成') || beforeLine.includes('为你') || beforeLine.includes('现在'))) {
            cleanStart = lineStart;
          }
          
          // 移除工具调用文本，保留其余内容
          const cleanedText = fullResponseText.substring(0, cleanStart) + fullResponseText.substring(cleanEnd);
          
          // 通过 window 回调更新 panel.js 中的气泡内容
          if (typeof window.__replaceChatText === 'function') {
            window.__replaceChatText(cleanedText);
          }
          
          // 执行工具
          try {
            if (tool === 'web.fetch') {
              const fetchResult = await executeChatToolWithAgentLoop(tool, args || {}, () => executeChatWebFetch(args.url, args.save === true));
              if (fetchResult?.error) {
                typewriterQueue += '\n\n⚠️ 网页解析失败：' + fetchResult.error;
                flushTypewriter(null);
              } else {
                typewriterQueue += `\n\n🌐 已解析网页：${fetchResult.title || fetchResult.url}\n正文长度：${fetchResult.textLength || 0} 字`;
                flushTypewriter(null);
                messages.push({ role: 'assistant', content: fullResponseText });
                messages.push({ role: 'user', content: `[TOOL_RESULT]\n工具: web.fetch\n结果:\n${JSON.stringify(fetchResult, null, 2)}\n[/TOOL_RESULT]\n\n请基于该网页解析结果继续回答。` });
              }
            } else if (tool === 'sql.query') {
              const sqlResult = await executeChatToolWithAgentLoop(tool, args || {}, () => executeChatSqlQuery(args || {}));
              typewriterQueue += '\n\n🧮 SQL 执行结果：\n```json\n' + JSON.stringify(sqlResult, null, 2).slice(0, 4000) + '\n```';
              flushTypewriter(null);
              messages.push({ role: 'assistant', content: fullResponseText });
              messages.push({ role: 'user', content: `[TOOL_RESULT]\n工具: sql.query\n结果:\n${JSON.stringify(sqlResult, null, 2)}\n[/TOOL_RESULT]\n\n请基于真实 SQL 结果继续回答。` });
            } else if (tool === 'web.search') {
              const query = args.query || '';
              const maxResults = args.max_results || 5;
              typewriterQueue += '\n\n';
              typewriterQueue += '🔍 正在联网搜索：' + query + '...';
              flushTypewriter(null);
              
              const searchResult = await executeChatToolWithAgentLoop(tool, args || {}, () => executeChatWebSearch(query, maxResults));
              if (searchResult && searchResult.length > 0) {
                let resultText = '\n\n--- 搜索结果 ---\n';
                searchResult.forEach((r, i) => {
                  resultText += `\n${i + 1}. **${r.title}**\n   ${r.snippet}\n   🔗 ${r.url}`;
                });
                typewriterQueue += resultText;
                
                // 将搜索结果作为上下文发送给 AI 继续回答
                const searchContext = searchResult.map(r => 
                  `[${r.title}](${r.url}): ${r.snippet}`
                ).join('\n');
                messages.push({ role: 'assistant', content: fullResponseText });
                messages.push({ role: 'user', content: `[TOOL_RESULT]\n工具: web.search\n查询: ${query}\n结果:\n${searchContext}\n[/TOOL_RESULT]\n\n请根据以上搜索结果继续回答用户的问题。` });
                
                // 第二次流式调用，追加结果
                try {
                  typewriterQueue += '\n\n💡 基于搜索结果：\n';
                  flushTypewriter(null);
                  const resp2 = await fetch(config.apiUrl, {
                    method: 'POST',
                    headers: {
                      'Content-Type': 'application/json',
                      'Authorization': `Bearer ${config.apiKey}`
                    },
                    body: JSON.stringify({
                      model: config.model,
                      messages: messages,
                      stream: true,
                      temperature: 0.7,
                      max_tokens: 2048
                    }),
                    signal: controller.signal
                  });
                  if (resp2.ok) {
                    const reader2 = resp2.body?.getReader();
                    if (reader2) {
                      const decoder2 = new TextDecoder('utf-8');
                      let buf2 = '';
                      try {
                        while (true) {
                          const { done: d2, value: v2 } = await reader2.read();
                          if (d2) break;
                          buf2 += decoder2.decode(v2, { stream: true });
                          const lines2 = buf2.split('\n');
                          buf2 = lines2.pop();
                          for (const l2 of lines2) {
                            if (!l2.startsWith('data:')) continue;
                            const d2 = l2.slice(5).trim();
                            if (d2 === '[DONE]' || !d2) continue;
                            try {
                              const p2 = JSON.parse(d2);
                              const t2 = p2.choices?.[0]?.delta?.content || '';
                              if (t2) { typewriterQueue += t2; flushTypewriter(null); }
                            } catch (_) {}
                          }
                        }
                      } finally {
                        try { reader2.cancel(); } catch (_) {}
                      }
                    }
                  }
                } catch (e2) {
                  typewriterQueue += '\n（基于搜索结果的补充回答暂时不可用）';
                }
              } else {
                typewriterQueue += '\n（未找到相关搜索结果）';
              }
            } else if (tool && tool.startsWith('agent.')) {
              const agentResult = await executeChatAgentTool(tool, args || {});
              typewriterQueue += '\n\nAgent 状态：\n```json\n' + JSON.stringify(agentResult, null, 2).slice(0, 4000) + '\n```';
              flushTypewriter(null);
              messages.push({ role: 'assistant', content: fullResponseText });
              messages.push({ role: 'user', content: `[TOOL_RESULT]\n工具: ${tool}\n结果:\n${JSON.stringify(agentResult, null, 2)}\n[/TOOL_RESULT]\n\n请基于该 Agent 状态继续执行。` });
            } else if (tool && tool.startsWith('generate.')) {
              // 文档生成工具：纯前端 Blob 下载（不依赖 bridge.py）
              const format = tool.replace('generate.', '');
              const formatNames = { docx: 'Word 文档', xlsx: 'Excel 表格', pptx: 'PPT 演示文稿', html: 'HTML 报告' };
              const displayName = formatNames[format] || format;
              
              // 直接调用 DOM 回调显示进度（不经过 typewriterQueue）
              if (typeof window.__showDocGenProgress === 'function') {
                window.__showDocGenProgress(displayName);
              }
              
              try {
                // 步骤1：准备内容
                if (typeof window.__updateDocGenStep === 'function') {
                  window.__updateDocGenStep('prepare');
                }
                // 短暂延迟让 UI 更新
                await new Promise(r => setTimeout(r, 100));
                
                // 步骤2：构建文档
                if (typeof window.__updateDocGenStep === 'function') {
                  window.__updateDocGenStep('build');
                }
                
                // 生成文档
                let generatedArtifact = null;
                const genResult = await executeChatToolWithAgentLoop(tool, args || {}, async () => {
                  generatedArtifact = await _frontendLocalGenerate(format, args);
                  return { ok: true, format, filename: generatedArtifact.filename, url: generatedArtifact.url, size: generatedArtifact.size };
                });
                if (genResult?.error) throw new Error(genResult.error);
                
                // 步骤3：保存完成
                if (typeof window.__updateDocGenStep === 'function') {
                  window.__updateDocGenStep('save');
                }
                await new Promise(r => setTimeout(r, 200));
                
                // 显示完成
                const resultName = args.filename || '文档';
                const extMap = { docx: '.doc', xlsx: '.csv', pptx: '.html', html: '.html' };
                const doneInfo = displayName + '已生成完毕：' + resultName + (extMap[format] || '');
                if (typeof window.__showDocGenDone === 'function') {
                  window.__showDocGenDone(generatedArtifact || doneInfo);
                }
              } catch (genErr) {
                if (typeof window.__showDocGenError === 'function') {
                  window.__showDocGenError('生成失败：' + genErr.message);
                }
              }
            } else if (tool && tool.startsWith('browser.')) {
              typewriterQueue += '\n\n🌐 正在执行浏览器操作：' + tool + '...';
              flushTypewriter(null);
              const browserResult = await executeChatToolWithAgentLoop(tool, args || {}, () => executeChatBrowserTool(tool, args || {}));
              typewriterQueue += '\n\n浏览器操作结果：\n```json\n' + JSON.stringify(browserResult, null, 2).slice(0, 4000) + '\n```';
              flushTypewriter(null);
              messages.push({ role: 'assistant', content: fullResponseText });
              messages.push({ role: 'user', content: `[TOOL_RESULT]\n工具: ${tool}\n结果:\n${JSON.stringify(browserResult, null, 2)}\n[/TOOL_RESULT]\n\n请基于该浏览器操作结果继续回答。` });
            } else if (tool && (tool.startsWith('fs.') || tool.startsWith('shell.') || tool.startsWith('clipboard.') || tool.startsWith('project.') || tool.startsWith('skill.') || tool.startsWith('checkpoint.') || tool.startsWith('cloudflare.'))) {
              typewriterQueue += tool.startsWith('project.')
                ? '\n\n🛠 正在生成前端项目...'
                  : tool.startsWith('cloudflare.')
                  ? '\n\n正在发布到 Cloudflare Pages...'
                  : tool.startsWith('skill.')
                  ? '\n\n🧩 正在执行 Skill 工具：' + tool + '...'
                  : '\n\n🖥 正在请求本地 Agent：' + tool + '...';
              flushTypewriter(null);
              const nativeResult = await executeChatToolWithAgentLoop(tool, args || {}, () => executeChatNativeTool(tool, args || {}));
              if (tool.startsWith('cloudflare.') && (nativeResult?.result?.url || nativeResult?.url)) {
                const result = nativeResult.result || nativeResult;
                typewriterQueue += '\n\nCloudflare Pages 已发布：' + result.url;
              } else if (tool.startsWith('project.') && nativeResult?.result?.path) {
                const result = nativeResult.result;
                const commands = (result.commands || []).map(c => '- `' + c + '`').join('\n');
                typewriterQueue += '\n\n✅ 前端项目已生成\n\n'
                  + '项目目录：`' + result.path + '`\n'
                  + '写入文件：' + (result.files?.length || 0) + ' 个\n\n'
                  + '运行命令：\n' + commands;
              } else {
                typewriterQueue += '\n\n本地 Agent 结果：\n```json\n' + JSON.stringify(nativeResult, null, 2).slice(0, 6000) + '\n```';
              }
              flushTypewriter(null);
              messages.push({ role: 'assistant', content: fullResponseText });
              messages.push({ role: 'user', content: `[TOOL_RESULT]\n工具: ${tool}\n结果:\n${JSON.stringify(nativeResult, null, 2)}\n[/TOOL_RESULT]\n\n请基于该工具结果继续回答；如果提示未安装本地 Agent，请说明需要先安装。` });
            }
          } catch (e) {
            typewriterQueue += '\n⚠️ 工具调用失败：' + e.message;
          }
        };
        
        // 竞争：首字超时 vs 流读取
        await Promise.race([firstCharPromise, readStream()]);
        clearTimeout(totalTimeoutId);
        globalThis.AgentLoop?.finishRun?.('completed', '对话执行完成');
        
        // 成功，跳出重试循环
        return;
        
      } catch (err) {
        if (firstCharTimeoutId) clearTimeout(firstCharTimeoutId);
        
        if (err.name === 'AbortError') return;
        
        console.error('[bridge_client] 请求错误:', err.message);
        
        // 判断是否可重试
        if (retryAttempt < MAX_RETRIES && _isRetryableError(err)) {
          console.warn(`[bridge_client] 请求失败(尝试${retryAttempt + 1}/${MAX_RETRIES}):`, err.message);
          retryAttempt++;
          continue;
        }
        
        // 不可重试或重试次数用完
        globalThis.AgentLoop?.finishRun?.('failed', err.message || String(err));
        onError?.(err.message || String(err));
        return;
      }
    }
  })();
  
  return function cancel() {
    cancelled = true;
    if (typewriterTimer) clearTimeout(typewriterTimer);
    controller.abort();
  };
}

/**
 * 浏览器自动化工具调用辅助函数
 * 通过 chrome.runtime.sendMessage 调用 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 || { success: true };
    }
    return { error: resp?.error || '操作失败' };
  } catch (e) {
    return { error: e.message };
  }
}

async function executeChatBrowserTool(tool, args = {}) {
  const map = {
    'browser.open': 'BROWSER_OPEN',
    'browser.navigate': 'BROWSER_NAVIGATE',
    'browser.click': 'BROWSER_CLICK',
    'browser.type': 'BROWSER_TYPE',
    'browser.fill': 'BROWSER_FILL',
    'browser.press': 'BROWSER_PRESS',
    'browser.observe': 'BROWSER_OBSERVE',
    'browser.waitFor': 'BROWSER_WAIT',
    'browser.getText': 'BROWSER_GET_TEXT',
    'browser.screenshot': 'BROWSER_SCREENSHOT',
    'browser.pdf': 'BROWSER_PDF',
    'browser.network.start': 'BROWSER_NETWORK_START',
    'browser.network.get': 'BROWSER_NETWORK_GET',
    'browser.network.clear': 'BROWSER_NETWORK_CLEAR',
    'browser.network.stop': 'BROWSER_NETWORK_STOP',
    'browser.acceptance.check': 'BROWSER_ACCEPTANCE_CHECK',
    'browser.listTabs': 'BROWSER_LIST_TABS',
    'browser.close': 'BROWSER_CLOSE'
  };
  const type = map[tool];
  if (!type) return { error: '未知浏览器工具: ' + tool };
  return await _callBrowserTool(type, args);
}

async function executeChatToolWithAgentLoop(tool, args = {}, executor) {
  if (globalThis.AgentLoop?.executeTool && typeof executor === 'function' && !tool.startsWith('agent.')) {
    return await globalThis.AgentLoop.executeTool({
      tool,
      args,
      retries: 1,
      executor
    });
  }
  return await executor();
}

async function executeChatAgentTool(tool, args = {}) {
  if (!globalThis.AgentLoop) return { ok: false, error: 'AgentLoop 未加载' };
  if (tool === 'agent.plan') return globalThis.AgentLoop.setPlan(args);
  if (tool === 'agent.step') return globalThis.AgentLoop.setStep(args);
  if (tool === 'agent.status') return { ok: true, run: globalThis.AgentLoop.snapshot() };
  if (tool === 'agent.audit.list') return { ok: true, audits: await globalThis.AgentLoop.listAudit(args) };
  if (tool === 'agent.audit.replay') return await globalThis.AgentLoop.replayAudit(args);
  return { ok: false, error: '未知 Agent 工具: ' + tool };
}

async function executeChatNativeTool(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,
      host: resp?.data?.host || 'com.zhenyu.ai_pm_agent'
    };
  } catch (e) {
    return { error: e.message };
  }
}

/**
 * 对话模式专用：解析粘贴 URL
 */
async function executeChatWebFetch(url, save = false) {
  try {
    if (!url) return { error: '缺少 url' };
    if (typeof chrome === 'undefined' || !chrome.runtime?.sendMessage) {
      return { error: '浏览器扩展运行环境不可用，无法解析网页' };
    }
    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 ctx = resp.context || resp.data || {};
    const current = globalThis.__skillRuntimeCurrentContext || {};
    const fetchedPages = Array.isArray(current.fetchedPages) ? current.fetchedPages : [];
    current.fetchedPages = [...fetchedPages, ctx].slice(-5);
    globalThis.__skillRuntimeCurrentContext = current;
    return {
      url: ctx.pageUrl || url,
      title: ctx.pageTitle || '',
      quality: ctx.quality || '',
      textLength: ctx.pageText?.length || 0,
      pageText: (ctx.pageText || '').slice(0, 6000),
      headings: (ctx.headings || []).slice(0, 30),
      tables: (ctx.tableData || []).length
    };
  } catch (e) {
    return { error: e.message, url };
  }
}

/**
 * 对话模式专用：执行内置 SQL
 */
async function executeChatSqlQuery(args = {}) {
  try {
    const runtimeContext = globalThis.__skillRuntimeCurrentContext || {};
    const contextTables = typeof buildContextSqlTables === 'function' ? buildContextSqlTables(runtimeContext) : {};
    const explicitTables = typeof normalizeSqlTables === 'function' ? normalizeSqlTables(args.tables || {}) : (args.tables || {});
    const fetchedTables = {};
    if (Array.isArray(runtimeContext.fetchedPages) && typeof buildContextSqlTables === 'function') {
      runtimeContext.fetchedPages.forEach((page, idx) => {
        const pageTables = buildContextSqlTables({ pageContext: page });
        Object.entries(pageTables).forEach(([name, rows]) => {
          fetchedTables[`fetched_${idx + 1}_${name}`] = rows;
        });
      });
    }
    if (typeof runMiniSql !== 'function') return { ok: false, error: 'SQL 引擎未加载' };
    return runMiniSql({ query: args.query, tables: { ...contextTables, ...fetchedTables, ...explicitTables } });
  } catch (e) {
    return { ok: false, error: e.message };
  }
}

/**
 * 对话模式专用：执行联网搜索
 * 通过 background Service Worker 代理（绕过 CORS，支持多引擎降级）
 * 降级：如果消息通道不可用，回退到前端直连
 */
async function executeChatWebSearch(query, maxResults = 5) {
  // 优先通过 background.js 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 },
          (r) => {
            if (chrome.runtime.lastError) {
              console.warn('[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 resp.results;
      }
    }
  } catch (e) {
    console.warn('[web-search] background proxy exception:', e.message);
  }

  // 降级：前端直连 DuckDuckGo（CORS 可能失败，但作为最后手段）
  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) return [];
    const html = await resp.text();
    const results = [];
    // 解析 DuckDuckGo HTML 结果
    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 >= maxResults) 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;
  } catch (_) {
    return [];
  }
}

/**
 * 非流式 LLM 调用（用于知识库分段等场景，需要一次性拿到完整响应）
 * @param {Array<{role, content}>} messages
 * @param {{temperature?, maxTokens?}} options
 * @returns {Promise<string>} 模型返回的完整文本
 */
async function callLLM(messages, options = {}) {
  const config = await _getLLMConfig();
  if (!config.apiKey) throw new Error('LLM API未配置，请在设置中填入 API Key');

  const resp = await fetch(config.apiUrl, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer ' + config.apiKey
    },
    body: JSON.stringify({
      model: options.model || config.model,
      messages,
      temperature: options.temperature ?? 0.3,
      max_tokens: options.maxTokens ?? 4096
    })
  });

  if (!resp.ok) {
    const errText = await resp.text();
    throw new Error('LLM API 返回错误 ' + resp.status + ': ' + errText);
  }

  const data = await resp.json();
  return data.choices?.[0]?.message?.content || '';
}

/**
 * 前端本地生成文档
 * 直接在浏览器中生成文件并下载
 */
async function _frontendLocalGenerate(format, args) {
  const content = args.content || '';
  const baseFilename = args.filename || 'AIPROS文档_' + new Date().toISOString().slice(0, 10);
  
  try {
    let blob, ext, mime = 'text/plain;charset=utf-8';
    
    if (format === 'docx') {
      // 生成 HTML 格式 Word 文档
      const html = _markdownToHtml(content);
      const fullHtml = `<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:w="urn:schemas-microsoft-com:office:word" xmlns="http://www.w3.org/TR/REC-html40"><head><meta charset="utf-8"><style>body{font-family:"微软雅黑","PingFang SC",Arial,sans-serif;font-size:12pt;line-height:1.8;padding:24px 36px;max-width:800px;margin:0 auto;}table{border-collapse:collapse;width:100%;margin:12px 0;}th{background:#e8edf5;font-weight:bold;border:1px solid #bbb;padding:8px 12px;text-align:left;}td{border:1px solid #ccc;padding:8px 12px;vertical-align:top;}h1{color:#0d47a1;font-size:22pt;margin:20px 0 10px;}h2{color:#1f5bbf;font-size:16pt;margin:16px 0 8px;border-bottom:1px solid #e0e0e0;padding-bottom:4px;}h3{color:#333;font-size:14pt;margin:14px 0 8px;}h4{color:#444;font-size:12pt;margin:12px 0 6px;}blockquote{border-left:4px solid #1f5bbf;padding:8px 16px;margin:10px 0;background:#f0f4ff;color:#333;font-style:italic;}hr{border:none;border-top:1px solid #ccc;margin:16px 0;}code{background:#f0f0f0;padding:1px 4px;border-radius:3px;font-family:Consolas,monospace;font-size:10pt;}a{color:#1f5bbf;}</style></head><body>${html}</body></html>`;
      blob = new Blob(['\ufeff' + fullHtml], { type: 'application/msword;charset=utf-8' });
      ext = '.doc';
      mime = 'application/msword;charset=utf-8';
    } else if (format === 'xlsx') {
      // 生成 CSV（Excel 可直接打开）
      const csv = _markdownTablesToCsv(content);
      blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8' });
      ext = '.csv';
      mime = 'text/csv;charset=utf-8';
    } else if (format === 'pptx') {
      // 生成 HTML 幻灯片
      const slidesHtml = _markdownToSlidesHtml(content);
      blob = new Blob([slidesHtml], { type: 'text/html;charset=utf-8' });
      ext = '.html';
      mime = 'text/html;charset=utf-8';
    } else if (format === 'html') {
      // 生成 HTML 报告（content 为完整 HTML 代码）
      const htmlContent = content;
      blob = new Blob([htmlContent], { type: 'text/html;charset=utf-8' });
      ext = '.html';
      mime = 'text/html;charset=utf-8';
    } else {
      blob = new Blob([content], { type: 'text/plain' });
      ext = '.txt';
      mime = 'text/plain;charset=utf-8';
    }
    
    // 触发下载（使用隐藏 iframe 方式，避免修改当前 document.body 导致闪烁）
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    const filename = _withExtension(baseFilename, ext);
    a.download = filename;
    a.style.display = 'none';
    document.body.appendChild(a);
    a.click();
    // HTML 格式额外在新标签页打开预览
    if (format === 'html') {
      try { window.open(url, '_blank'); } catch(e) {}
    }
    // 延迟清理，确保下载已触发
    setTimeout(() => {
      if (a.parentNode) a.parentNode.removeChild(a);
      // HTML 预览页需要保留 URL，不立即回收
      if (format !== 'html') URL.revokeObjectURL(url);
    }, 1000);
    return {
      id: 'artifact_' + Date.now() + '_' + Math.random().toString(16).slice(2),
      format,
      filename,
      title: filename,
      url,
      content: format === 'html' ? content : '',
      mime,
      size: blob.size,
      createdAt: new Date().toISOString()
    };
  } catch (e) {
    throw e; // 向上抛出，由调用者显示错误
  }
}

function _withExtension(filename, ext) {
  const name = String(filename || 'AIPROS文档').trim() || 'AIPROS文档';
  if (!ext) return name;
  return name.toLowerCase().endsWith(ext.toLowerCase()) ? name : name + ext;
}

/**
 * Markdown 转 HTML（生成 Word 文档用）
 * 处理：标题、加粗、斜体、表格、列表、引用、代码块、水平线、链接
 */
function _markdownToHtml(md) {
  let html = md;

  // 0. 提取代码块，保护其内容不被后续规则破坏
  const codeBlocks = [];
  html = html.replace(/```(\w*)\s*\n([\s\S]*?)```/g, (_, lang, code) => {
    const idx = codeBlocks.length;
    const langLabel = lang ? `<b style="color:#666;font-size:10px;">${lang}</b><br>` : '';
    codeBlocks.push(`<div style="background:#f5f5f5;border:1px solid #ddd;border-radius:4px;padding:10px;margin:8px 0;font-family:Consolas,monospace;font-size:11px;white-space:pre-wrap;word-break:break-all;">${langLabel}${code.replace(/</g, '&lt;').replace(/>/g, '&gt;')}</div>`);
    return `\x00CODEBLOCK${idx}\x00`;
  });

  // 1. 提取表格块，保护其内容
  const tableBlocks = [];
  html = html.replace(/((?:^\|.+\|[\t ]*\n?)+)/gm, function(match) {
    const rows = match.trim().split('\n').filter(r => {
      const t = r.trim();
      return t.startsWith('|') && t.endsWith('|') && !/^\|[\s\-:|]+\|$/.test(t);
    });
    if (rows.length < 2) return match; // 至少表头+1行数据
    let th = '<table border="1" cellpadding="6" cellspacing="0" style="border-collapse:collapse;width:100%;margin:12px 0;font-size:11pt;">';
    rows.forEach((row, i) => {
      const cells = row.split('|').filter((_, ci, arr) => ci > 0 && ci < arr.length - 1).map(c => c.trim());
      if (i === 0) {
        th += '<tr>' + cells.map(c => `<th style="background:#e8edf5;padding:8px 12px;text-align:left;font-weight:bold;border:1px solid #bbb;">${c}</th>`).join('') + '</tr>';
      } else {
        th += '<tr>' + cells.map(c => `<td style="padding:8px 12px;border:1px solid #ccc;vertical-align:top;">${c}</td>`).join('') + '</tr>';
      }
    });
    th += '</table>';
    const idx = tableBlocks.length;
    tableBlocks.push(th);
    return `\x00TABLEBLOCK${idx}\x00`;
  });

  // 2. 标题
  html = html.replace(/^####\s+(.+)$/gm, '<h4 style="margin:12px 0 6px;color:#444;">$1</h4>');
  html = html.replace(/^###\s+(.+)$/gm, '<h3 style="margin:14px 0 8px;color:#333;">$1</h3>');
  html = html.replace(/^##\s+(.+)$/gm, '<h2 style="margin:16px 0 8px;color:#1f5bbf;font-size:16pt;">$1</h2>');
  html = html.replace(/^#\s+(.+)$/gm, '<h1 style="margin:20px 0 10px;color:#0d47a1;font-size:22pt;">$1</h1>');

  // 3. 水平线 --- → <hr>
  html = html.replace(/^---+$/gm, '<hr style="border:none;border-top:1px solid #ccc;margin:16px 0;">');

  // 4. 引用块 > xxx
  html = html.replace(/^>\s+(.+)$/gm, '<blockquote style="border-left:4px solid #1f5bbf;padding:8px 16px;margin:10px 0;background:#f0f4ff;color:#333;font-style:italic;">$1</blockquote>');

  // 5. 加粗、斜体
  html = html.replace(/\*\*(.+?)\*\*/g, '<b>$1</b>');
  html = html.replace(/\*(.+?)\*/g, '<i>$1</i>');

  // 6. 行内代码
  html = html.replace(/`([^`]+)`/g, '<code style="background:#f0f0f0;padding:1px 4px;border-radius:3px;font-family:Consolas,monospace;font-size:10pt;">$1</code>');

  // 7. 列表（- xxx 或 * xxx）
  html = html.replace(/^[\-\*]\s+(.+)$/gm, '<div style="padding-left:20px;margin:2px 0;">• $1</div>');

  // 8. 有序列表（1. xxx）
  html = html.replace(/^(\d+)\.\s+(.+)$/gm, '<div style="padding-left:20px;margin:2px 0;">$1. $2</div>');

  // 9. 链接 [text](url)
  html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" style="color:#1f5bbf;">$1</a>');

  // 10. 换行
  html = html.replace(/\n/g, '<br>');

  // 11. 还原代码块和表格
  html = html.replace(/\x00CODEBLOCK(\d+)\x00/g, (_, idx) => codeBlocks[parseInt(idx)]);
  html = html.replace(/\x00TABLEBLOCK(\d+)\x00/g, (_, idx) => tableBlocks[parseInt(idx)]);

  return html;
}

/**
 * Markdown 表格转 CSV（前端降级用）
 */
function _markdownTablesToCsv(content) {
  const lines = content.split('\n');
  const csvRows = [];
  for (const line of lines) {
    const trimmed = line.trim();
    if (trimmed.startsWith('|') && trimmed.endsWith('|')) {
      if (/^\|[\s\-:|]+\|$/.test(trimmed)) continue;
      const cells = trimmed.split('|').slice(1, -1).map(c => '"' + c.trim().replace(/"/g, '""') + '"');
      csvRows.push(cells.join(','));
    } else if (trimmed) {
      csvRows.push('"' + trimmed.replace(/"/g, '""') + '"');
    }
  }
  return csvRows.join('\n');
}

/**
 * Markdown 转 HTML 幻灯片（前端降级用）
 */
function _markdownToSlidesHtml(content) {
  const slidesRaw = content.split(/\n---\n/);
  const slides = slidesRaw.map((s, i) => {
    let slideHtml = s.trim();
    slideHtml = slideHtml.replace(/^##\s+(.+)$/gm, '<h2>$1</h2>');
    slideHtml = slideHtml.replace(/^#\s+(.+)$/gm, '<h1>$1</h1>');
    slideHtml = slideHtml.replace(/\*\*(.+?)\*\*/g, '<b>$1</b>');
    slideHtml = slideHtml.replace(/^[-*]\s+(.+)$/gm, '<li>$1</li>');
    slideHtml = slideHtml.replace(/(<li>[\s\S]*?<\/li>)/g, '<ul>$1</ul>');
    slideHtml = slideHtml.replace(/\n/g, '<br>');
    
    const isCover = i === 0;
    const bg = isCover ? '#1a1a2e' : '#ffffff';
    const color = isCover ? '#ffffff' : '#1a1a2e';
    
    return `<div class="slide" style="width:100vw;height:100vh;display:none;flex-direction:column;justify-content:center;align-items:center;background:${bg};color:${color};padding:60px;font-family:'PingFang SC',Arial,sans-serif;"><div style="max-width:900px;text-align:center;line-height:1.8;font-size:24px;">${slideHtml}</div><div style="position:absolute;bottom:20px;right:30px;font-size:14px;color:#999;">${i+1} / ${slidesRaw.length}</div></div>`;
  });

  return `<!DOCTYPE html><html><head><meta charset="utf-8"><title>AI演示文稿</title><style>*{margin:0;padding:0;}.slide.active{display:flex!important;}h1{font-size:44px;margin-bottom:20px;}h2{font-size:36px;color:#1f5bbf;}li{text-align:left;margin:8px 0;font-size:22px;}ul{list-style:none;}ul li::before{content:"▸ ";color:#1f5bbf;}</style></head><body>${slides.join('')}<script>const s=document.querySelectorAll('.slide');let c=0;if(s.length>0){s[0].classList.add('active');document.addEventListener('keydown',e=>{s[c].classList.remove('active');if(e.key==='ArrowRight'||e.key===' ')c=Math.min(c+1,s.length-1);else if(e.key==='ArrowLeft')c=Math.max(c-1,0);s[c].classList.add('active');});}</script></body></html>`;
}
