// ==============================
// 状态
// ==============================
let pageContext = null;        // 从content_script收到的页面数据
let lastAnalysisText = '';     // 上次分析结果全文（用于填入输入框）
let analysisCancel = null;     // 取消当前分析的函数
let analysiswaitTimer = null;  // 分析等待计时器，停止时需要清除
let chatCancel = null;         // 取消当前对话的函数
let chatBusy = false;
let lastSkillContext = null;   // Skill 分析完成后的上下文（供对话框携带）

const AISC_BUILTIN_SKILL_FILES = [
  "PRD生成",
  "需求评审",
  "技术方案评审",
  "运营方案制定",
  "流程图构建",
  "流程文件构建",
  "用户故事拆解",
  "需求优先级排序"
];

// ---- 对话历史记忆（同一会话内多轮对话保持上下文） ----
let chatConversationHistory = []; // [{role:'user'|'assistant', content:'...'}]
let chatHistorySummary = ''; // 增量摘要：旧消息的压缩版本
const MAX_FULL_MESSAGES = 4; // 最近保留4条完整消息（2轮对话）
const MAX_SUMMARY_LENGTH = 500; // 摘要最大长度
const MAX_SINGLE_MSG_IN_SUMMARY = 80; // 每条消息在摘要中的最大长度

// ---- 当前选中的 Skill（替代原 skillSelect 下拉框） ----
let _selectedSkill = '';
let _allPopupSkills = [];      // 浮层用的 skill 列表数据

// ---- 预热分析缓存 ----
let preheatCache = null;       // { skill, text, done } | null
let preheatRunning = false;    // 是否正在后台跑预热

// ==============================
// 初始化
// ==============================
document.addEventListener('DOMContentLoaded', async () => {
  await migrateLegacySettings();
  // ===== 预热 API 配置（最高优先级，面板打开即执行） =====
  if (typeof warmUpAPI === 'function') warmUpAPI();

  // ==============================
  // 统一事件绑定（替代内联 onclick），加 null 守卫防崩溃
  // ==============================
  function safeBind(id, event, handler) {
    const el = document.getElementById(id);
    if (el) el.addEventListener(event, handler);
    else console.warn('[panel] 元素未找到，跳过事件绑定:', id);
  }
  safeBind('contextRefreshBtn', 'click', refreshContext);
  safeBind('contextReadBtn', 'click', refreshContext);
  safeBind('urlContextBtn', 'click', fetchUrlContext);
  safeBind('panelCloseBtn', 'click', () => {
    if (typeof chrome !== 'undefined' && chrome.runtime) {
      chrome.runtime.sendMessage({ type: 'CLOSE_PANEL_FROM_PANEL' });
    }
  });
  safeBind('panelSettingsBtn', 'click', () => toggleSettingsPanel('model'));
  safeBind('settingsCloseBtn', 'click', closeSettingsPanel);
  // 三个顶级 Tab
  safeBind('stab-skills', 'click', () => switchSettingsTab('skills'));
  safeBind('stab-model', 'click', () => switchSettingsTab('model'));
  safeBind('stab-knowledge', 'click', () => switchSettingsTab('knowledge'));
  // Skill 子 Tab
  safeBind('subtab-installed', 'click', () => switchSkillSubTab('installed'));
  safeBind('subtab-addskill', 'click', () => switchSkillSubTab('addskill'));
  safeBind('subtab-sources', 'click', () => switchSkillSubTab('sources'));
  safeBind('refreshSkillsBtn', 'click', loadSkillList);
  safeBind('installFromUrlBtn', 'click', installFromUrl);
  safeBind('installFromContentBtn', 'click', installFromContent);
  safeBind('saveModelSettingsBtn', 'click', saveSettings);
  safeBind('saveSkillSettingsBtn', 'click', saveSkillSourcesSettings);
  safeBind('fetchModelsBtn', 'click', fetchModels);
  safeBind('fillBtn', 'click', fillOpinion);
  safeBind('chatClearBtn', 'click', clearChat);
  safeBind('chatSendBtn', 'click', sendChat);
  safeBind('chatAttachBtn', 'click', () => {
    document.getElementById('chatFileInput')?.click();
  });
  safeBind('chatVoiceBtn', 'click', toggleVoiceInput);
  document.addEventListener('click', (e) => {
    const toolHeader = e.target.closest('.tool-result-header');
    if (!toolHeader) return;
    const block = toolHeader.closest('.tool-result-block');
    if (block?.id) toggleToolBlock(block.id);
  });
  const skillSearchInput = document.getElementById('skillSearchInput');
  if (skillSearchInput) skillSearchInput.addEventListener('input', function() {
    filterSkillList(this.value.trim());
  });
  const chatFileInput = document.getElementById('chatFileInput');
  if (chatFileInput) chatFileInput.addEventListener('change', handleFileSelect);
  const chatInput = document.getElementById('chatInput');
  if (chatInput) {
    chatInput.addEventListener('keydown', handleChatKey);
    chatInput.addEventListener('input', () => {
      autoResizeChat(chatInput);
      handleSkillPopup(chatInput);
    });
    chatInput.addEventListener('paste', handlePasteImage);
  }
  const urlContextInput = document.getElementById('urlContextInput');
  if (urlContextInput) {
    urlContextInput.addEventListener('keydown', (e) => {
      if (e.key === 'Enter') {
        e.preventDefault();
        fetchUrlContext();
      }
    });
  }
  // 服务商切换时动态更新模型列表
  const providerSelect = document.getElementById('settingsProvider');
  if (providerSelect) {
    providerSelect.addEventListener('change', () => {
      updateModelPlaceholder(providerSelect.value);
      const customSection = document.getElementById('customUrlSection');
      if (customSection) customSection.style.display = providerSelect.value === 'custom' ? 'block' : 'none';
    });
  }

  // 知识库：上传绑定 + 刷新
  const kbUploadZone = document.getElementById('kbUploadZone');
  const kbFileInput = document.getElementById('kbFileInput');
  if (kbUploadZone && kbFileInput) {
    kbUploadZone.addEventListener('click', () => kbFileInput.click());
    kbUploadZone.addEventListener('dragover', (e) => { e.preventDefault(); kbUploadZone.classList.add('kb-drag-over'); });
    kbUploadZone.addEventListener('dragleave', () => kbUploadZone.classList.remove('kb-drag-over'));
    kbUploadZone.addEventListener('drop', (e) => {
      e.preventDefault();
      kbUploadZone.classList.remove('kb-drag-over');
      handleKnowledgeUpload(e.dataTransfer.files);
    });
    kbFileInput.addEventListener('change', () => {
      handleKnowledgeUpload(kbFileInput.files);
      kbFileInput.value = '';
    });
  }
  safeBind('kbRefreshBtn', 'click', loadKnowledgeList);
  safeBind('kbClearAllBtn', 'click', clearAllKnowledge);
  safeBind('kbResultClose', 'click', _hideKBResult);

  // 初始化知识库
  initKnowledgeStore();

  initVoiceRecognition();
  await loadSkillListToSelect();

  // 每次打开插件都是全新状态：清空对话、清空上下文、强制读取当前页面
  clearChat(); // 清空对话和历史状态
  pageContext = null;
  const contextStorage = typeof chrome !== 'undefined' ? (chrome.storage?.session || chrome.storage?.local) : null;
  if (contextStorage?.remove) {
    try { await contextStorage.remove(['pageContext']); } catch (_) {}
  }
  // 强制读取当前页面（不再使用缓存）
  document.getElementById('contextText').textContent = '自动读取中...';
  setTimeout(() => refreshContext(), 400);
  if (typeof chrome !== 'undefined' && chrome.storage) {
    const changeApi = chrome.storage.onChanged || chrome.storage.session?.onChanged;
    if (changeApi?.addListener) changeApi.addListener(async (changes, areaName) => {
      if (areaName && areaName !== 'session' && areaName !== 'local') return;
      if (changes.pageContext) {
        const newCtx = changes.pageContext.newValue;
        const oldBillId = pageContext?.billId;
        pageContext = newCtx;
        updateContextBar(pageContext);
        // 页面切换到新对象时，重新预热
        if (newCtx?.billId && newCtx.billId !== oldBillId) {
          preheatCache = null;
          preheatRunning = false;
          setTimeout(() => triggerPreheat(), 800);
        }
      }
    });
  }
  if (typeof chrome !== 'undefined' && chrome.runtime) {
    chrome.runtime.onMessage.addListener((msg) => {
      if (msg.type === 'FILL_RESULT') {
        if (msg.ok) showToast('✅ 内容已填入，请确认');
        else showToast('⚠️ ' + (msg.msg || '填写失败，请手动填写'), 'warn');
      } else if (msg.type === 'VOICE_TO_PANEL_RUNTIME' && msg.payload) {
        handleVoiceMessage(msg.payload);
      }
    });
  }
  // 预热分析：页面就绪后延迟1.5秒（等页面上下文读取完）再静默跑
  setTimeout(() => triggerPreheat(), 1500);
});

async function migrateLegacySettings() {
  if (typeof chrome === 'undefined' || !chrome.storage) return;
  try {
    const stored = await chrome.storage.local.get(['llmSettings', 'bridgeSettings']);
    const current = stored.llmSettings || {};
    const legacy = stored.bridgeSettings || {};
    const needsMigration = Object.keys(legacy).length || current.apiKey || current.apiUrl;
    if (!needsMigration) return;
    const migrated = {
      ...legacy,
      ...current,
      provider: current.provider || legacy.provider || 'aliyun',
      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 || 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions',
      model: current.model || current.aliyun_model || legacy.model || legacy.aliyun_model || legacy.fallback_model || 'qwen-plus',
      aliyun_api_key: current.aliyun_api_key || current.api_key || current.apiKey || legacy.aliyun_api_key || legacy.api_key || legacy.apiKey || legacy.fallback_api_key || '',
      aliyun_api_url: current.aliyun_api_url || current.api_url || current.apiUrl || legacy.aliyun_api_url || legacy.api_url || legacy.apiUrl || legacy.fallback_api_url || 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions',
      aliyun_model: current.aliyun_model || current.model || legacy.aliyun_model || legacy.model || legacy.fallback_model || 'qwen-plus',
      skill_dirs: current.skill_dirs || legacy.skill_dirs || ['~/ai-skills'],
      remote_skill_apis: current.remote_skill_apis || legacy.remote_skill_apis || (legacy.remote_skill_api ? [legacy.remote_skill_api] : []),
      remote_skill_api_token: current.remote_skill_api_token || legacy.remote_skill_api_token || '',
      preheat_enabled: current.preheat_enabled ?? !!legacy.preheat_enabled,
      preheat_skill: current.preheat_skill || legacy.preheat_skill || ''
    };
    await chrome.storage.local.set({ llmSettings: migrated });
  } catch (e) {
    console.warn('[settings] 兼容旧配置失败:', e.message);
  }
}

// ==============================
// 上下文栏
// ==============================
function updateContextBar(context) {
  const dot = document.getElementById('contextDot');
  const text = document.getElementById('contextText');
  if (context && context.pageText && context.pageText.length > 50) {
    // 有实质内容就算读取成功
    dot.className = 'context-dot active';
    const title = context.pageTitle || context.url || '当前页面';
    text.textContent = `已读取：${title.slice(0, 30)}`;
  } else if (context) {
    dot.className = 'context-dot warn';
    text.textContent = '已读取页面（内容较少）';
  } else {
    dot.className = 'context-dot';
    text.textContent = '点击「读取页面」获取数据';
  }
}

function normalizeUrlInput(raw) {
  const value = String(raw || '').trim();
  if (!value) return '';
  if (/^https?:\/\//i.test(value)) return value;
  if (/^[\w.-]+\.[a-z]{2,}(?:[/:?#].*)?$/i.test(value)) return 'https://' + value;
  return value;
}

async function fetchUrlContext() {
  const input = document.getElementById('urlContextInput');
  const btn = document.getElementById('urlContextBtn');
  const dot = document.getElementById('contextDot');
  const text = document.getElementById('contextText');
  const url = normalizeUrlInput(input?.value);

  if (!url) {
    showToast('请先粘贴产品页面、竞品页面、PRD或方案网页 URL', 'warn');
    return;
  }
  if (!/^https?:\/\//i.test(url)) {
    showToast('URL 需要以 http:// 或 https:// 开头', 'warn');
    return;
  }
  if (typeof chrome === 'undefined' || !chrome.runtime) {
    showToast('当前不是插件运行环境，无法解析网页', 'warn');
    return;
  }

  if (input) input.value = url;
  if (btn) {
    btn.disabled = true;
    btn.textContent = '解析中...';
  }
  if (dot) dot.className = 'context-dot loading';
  if (text) text.textContent = '正在解析粘贴的网址...';

  try {
    const resp = await new Promise((resolve) => {
      chrome.runtime.sendMessage({ type: 'FETCH_WEB_PAGE', url, save: true }, (r) => {
        if (chrome.runtime.lastError) {
          resolve({ ok: false, error: chrome.runtime.lastError.message });
          return;
        }
        resolve(r);
      });
    });
    if (!resp?.ok) throw new Error(resp?.error || '网页解析失败');
    pageContext = resp.context || resp.data || null;
    updateContextBar(pageContext);
    if (pageContext?.pageTitle) {
      showToast(`✅ 已解析网页：${pageContext.pageTitle.slice(0, 22)}`);
    } else {
      showToast('✅ 网页已解析');
    }
  } catch (e) {
    if (dot) dot.className = 'context-dot error';
    if (text) text.textContent = '网页解析失败';
    showToast('⚠️ ' + (e.message || '网页解析失败'), 'warn');
  } finally {
    if (btn) {
      btn.disabled = false;
      btn.textContent = '解析网页';
    }
  }
}

/**
 * 主动读取当前页面数据。
 * 策略：走 background 的一次性安全快照，不向页面长期注入读取脚本。
 */
let _refreshing = false; // 防重入

async function refreshContext() {
  if (_refreshing) return; // 防止并发重复触发
  _refreshing = true;
  document.getElementById('contextDot').className = 'context-dot loading';
  document.getElementById('contextText').textContent = '读取中...';

  if (typeof chrome === 'undefined' || !chrome.runtime) {
    document.getElementById('contextText').textContent = '非插件环境';
    _refreshing = false;
    return;
  }

  injectAndExtract();
}

/**
 * 让 background 使用 scripting.executeScript 做一次性只读快照。
 * 适用于普通 http/https 页面；飞书/钉钉等协同编辑页面会自动走安全模式。
 */
function injectAndExtract() {
  document.getElementById('contextText').textContent = '安全读取中...';

  let settled = false; // 防止 timeout 和 callback 双重触发

  // 超时保护：8秒还没拿到数据就降级截图
  const timeout = setTimeout(() => {
    if (settled) return;
    settled = true;
    if (!pageContext) {
      document.getElementById('contextDot').className = 'context-dot error';
      document.getElementById('contextText').textContent = '自动读取超时，尝试截图识别...';
      captureAndAnalyzeScreenshot(true);
    }
    _refreshing = false;
  }, 8000);

  chrome.runtime.sendMessage({ type: 'INJECT_AND_EXTRACT' }, (resp) => {
    if (settled) return;
    settled = true;
    clearTimeout(timeout);
    if (chrome.runtime.lastError || !resp?.ok) {
      const err = chrome.runtime.lastError?.message || resp?.error || '未知错误';
      if (err.includes('内部页面') || err.includes('chrome-extension')) {
        document.getElementById('contextDot').className = 'context-dot';
        document.getElementById('contextText').textContent = '请先打开一个页面再使用插件';
        _refreshing = false;
        return;
      }
      document.getElementById('contextDot').className = 'context-dot error';
      document.getElementById('contextText').textContent = '读取失败，尝试截图识别...';
      _refreshing = false;
      setTimeout(() => captureAndAnalyzeScreenshot(true), 500);
      return;
    }
    // 安全快照成功：直接读取 background 写入的上下文。
    loadContextFromStorage();
    setTimeout(async () => { await loadContextFromStorage(); _refreshing = false; }, 200);
  });
}

async function loadContextFromStorage() {
  if (typeof chrome === 'undefined' || !chrome.storage) return;
  const contextStorage = chrome.storage.session || chrome.storage.local;
  if (!contextStorage?.get) {
    pageContext = null;
    updateContextBar(pageContext);
    return;
  }
  
  contextStorage.get(['pageContext'], (stored) => {
    pageContext = stored.pageContext || null;
    updateContextBar(pageContext);
    // poor 降级：只要有文字内容（>50字）就不触发截图，等 1s 再读一次看能不能升级
    if (pageContext?.quality === 'poor') {
      const hasText = (pageContext?.pageText || '').length > 50;
      if (hasText) {
        // 已有内容，只等升级，不跛展截图
        setTimeout(async () => {
          const recheck = await new Promise(r => contextStorage.get(['pageContext'], r));
          if (recheck.pageContext && recheck.pageContext.quality !== 'poor') {
            pageContext = recheck.pageContext;
            updateContextBar(pageContext);
          }
          // 有内容就不报错，仄保持当前状态
        }, 1000);
      } else {
        // 真的没内容，才降级到全选复制路径
        setTimeout(async () => {
          const recheck = await new Promise(r => contextStorage.get(['pageContext'], r));
          if (recheck.pageContext && recheck.pageContext.quality !== 'poor') {
            pageContext = recheck.pageContext;
            updateContextBar(pageContext);
            console.log('[panel] 二次读取成功，质量已升级:', pageContext.quality);
          } else {
            enrichContextBySelectCopy();
          }
        }, 1000);
      }
    }
  });
}

// ==============================
// 分析（结果统一进聊天区）
// ==============================
async function startAnalysis() {
  if (analysisCancel) {
    // 正在分析 → 取消，同时清理等待提示气泡和计时器
    analysisCancel();
    analysisCancel = null;
    if (analysiswaitTimer) { clearInterval(analysiswaitTimer); analysiswaitTimer = null; }
    resetAnalyzeBtn();
    // 清理残留的 waitMsg（通过约定的 data 属性找到）
    const pendingWait = document.querySelector('.chat-msg[data-waitid="analysis-wait"]');
    if (pendingWait?.parentNode) pendingWait.parentNode.removeChild(pendingWait);
    return;
  }

  const skillName = _selectedSkill;
  const chatInput = document.getElementById('chatInput');
  const userMsg = chatInput?.value?.trim() || '';

  // ===== 页面上下文注入决策 =====
  // 重置确认结果
  _pageContextDecision = null;

  const hasAttachments = pendingAttachments && pendingAttachments.length > 0;
  const pageAvailable = pageContext && Object.keys(pageContext).length > 0;
  const userWantsPage = detectPageIntent(userMsg);

  const pageReady = pageAvailable && hasReadablePageContext(pageContext);
  const userRejectsPage = detectNoPageIntent(userMsg);

  if (userWantsPage && pageReady && !userRejectsPage) {
    // 用户明确提到当前页面/这篇文章，直接把页面交给 Skill。
    _pageContextDecision = pageContext;
  } else if (!hasAttachments && pageReady && !userRejectsPage) {
    // 页面已读好，但用户没明确说是否使用，交互确认。
    _pendingStartAnalysis = skillName;
    const emptyState = document.getElementById('emptyState');
    if (emptyState) emptyState.style.display = 'none';
    showPageContextConfirmBar(pageContext, skillName);
    return;
  } else {
    _pageContextDecision = {};
  }

  // 直接执行（场景A/B 或无页面信息）
  _doStartAnalysis(skillName);
}

/**
 * startAnalysis 的实际执行体（拆分出来，供页面确认后回调）
 * @param {string} skillName
 */
async function _doStartAnalysis(skillName) {
  // 隐藏空状态
  const emptyState = document.getElementById('emptyState');
  if (emptyState) emptyState.style.display = 'none';

  // ---- 命中预热缓存：用打字机效果展示结果 ----
  if (preheatCache && preheatCache.skill === skillName && preheatCache.done && preheatCache.text) {
    const cachedText = preheatCache.text;
    preheatCache = null;
    appendChatMsg(`⚡ 开始AI分析 · Skill：${skillName || '默认'}`, 'user analysis-trigger');
    const aiMsg = appendChatMsg('', 'ai analysis typing');
    lastAnalysisText = '';
    showToast('⚡ 预热结果已就绪，即时展示');
    let i = 0;
    const CHAR_DELAY = 10;
    function typeNextChar() {
      if (i >= cachedText.length) {
        aiMsg.classList.remove('typing');
        lastAnalysisText = cachedText;
        lastSkillContext = { skill: skillName || '默认', pageContext: _pageContextDecision || {}, analysisResult: cachedText };
        const fillBtn = document.getElementById('fillBtn');
        if (fillBtn) fillBtn.style.display = 'inline-flex';
        const wrap = aiMsg.closest ? aiMsg.closest('.chat-msg-wrap') : aiMsg.parentNode;
        if (wrap) appendInsertButton(wrap, cachedText, skillName || '默认');
        appendExportButtons(aiMsg, cachedText);
        appendFollowupSuggestions(aiMsg, cachedText);
        appendCopyButton(aiMsg, cachedText);
        resetAnalyzeBtn(true);
        scrollChatBottom();
        return;
      }
      let chunk;
      if (cachedText[i] === '\n') { chunk = '\n'; i++; }
      else {
        const nl = cachedText.indexOf('\n', i);
        if (nl > i && nl - i <= 3) { chunk = cachedText.slice(i, nl + 1); i = nl + 1; }
        else { chunk = cachedText[i]; i++; }
      }
      lastAnalysisText += chunk;
      aiMsg.innerHTML = formatOutput(lastAnalysisText);
      scrollChatBottom();
      setTimeout(typeNextChar, CHAR_DELAY);
    }
    analysisCancel = () => {
      i = cachedText.length;
      aiMsg.classList.remove('typing');
      lastAnalysisText = cachedText;
      lastSkillContext = { skill: skillName || '默认', pageContext: _pageContextDecision || {}, analysisResult: cachedText };
      const wrap = aiMsg.closest ? aiMsg.closest('.chat-msg-wrap') : aiMsg.parentNode;
      if (wrap) appendInsertButton(wrap, cachedText, skillName || '默认');
      if (hasMarkdownTable(cachedText)) appendExportButtons(aiMsg, cachedText);
      resetAnalyzeBtn(true);
    };
    setTimeout(typeNextChar, CHAR_DELAY);
    return;
  }

  // 在聊天区插入一条"系统触发"提示（右侧小气泡）
  appendChatMsg(`⚡ 开始AI分析 · Skill：${skillName || '默认'}`, 'user analysis-trigger');

  // 将发送按钮切换为停止状态（用户可点击打断分析）
  const sendBtnAnalysis = document.getElementById('chatSendBtn');
  if (sendBtnAnalysis) {
    sendBtnAnalysis.textContent = '⬛';
    sendBtnAnalysis.disabled = false;
  }

  // 动态计时等待提示（每秒更新，首字到达即消失）
  const waitMsg = appendChatMsg('⏳ AI 正在思考...', 'ai analysis');
  waitMsg.dataset.waitid = 'analysis-wait';
  let waitSeconds = 0;
  analysiswaitTimer = setInterval(() => {
    waitSeconds++;
    if (waitSeconds < 5) {
      waitMsg.textContent = `⏳ AI 正在思考...（${waitSeconds}s）`;
    } else if (waitSeconds < 15) {
      waitMsg.textContent = `⏳ 正在调用 AI 分析中...（${waitSeconds}s）`;
    } else {
      waitMsg.textContent = `⏳ 深度分析中，内容较多请稍候...（${waitSeconds}s）`;
    }
  }, 1000);
  const removeWaitMsg = () => {
    if (analysiswaitTimer) { clearInterval(analysiswaitTimer); analysiswaitTimer = null; }
    if (waitMsg && waitMsg.parentNode) waitMsg.parentNode.removeChild(waitMsg);
  };
  scrollChatBottom();

  // 插入 AI 回复气泡（两段结构：思考沙箱 + 结论区）
  const aiWrap = document.createElement('div');
  aiWrap.className = 'chat-msg-wrap ai';

  // ---- 思考沙箱区块（折叠，显示过程性输出）----
  const thinkingBlock = document.createElement('div');
  thinkingBlock.className = 'skill-thinking-block';
  thinkingBlock.innerHTML = `
    <div class="skill-thinking-header">
      <span class="skill-thinking-icon">⚙️</span>
      <span class="skill-thinking-label">Skill 执行过程</span>
      <span class="skill-thinking-toggle">▶</span>
    </div>
    <div class="skill-thinking-body"></div>
  `;
  aiWrap.appendChild(thinkingBlock);
  const thinkingHeader = thinkingBlock.querySelector('.skill-thinking-header');
  if (thinkingHeader) {
    thinkingHeader.addEventListener('click', () => {
      thinkingBlock.classList.toggle('expanded');
    });
  }
  const thinkingBody = thinkingBlock.querySelector('.skill-thinking-body');

  // ---- 结论气泡（正常展示分析结果）----
  const conclusionBubble = document.createElement('div');
  conclusionBubble.className = 'chat-msg ai analysis typing';
  aiWrap.appendChild(conclusionBubble);

  // ---- 操作按钮：输出完成后追加到底部 ----
  
  document.getElementById('chatHistory').appendChild(aiWrap);

  lastAnalysisText = '';
  let _skillResultAccum = '';
  let firstChunkReceived = false;
  // 思考沙箱累积内容
  let thinkingText = '';

  // 新架构：使用插件内置Skill运行时
  try {
    // 获取Skill内容
    const skillContent = await loadSkillContent(skillName);
    if (!skillContent) {
      throw new Error(`Skill "${skillName}" 未找到`);
    }
    
    // 解析Skill
    const skill = parseSkill(skillContent);
    
    // 获取 AI 服务商配置
    const settings = await _getLocalSettings();
    const llmConfig = {
      apiKey: settings.api_key || settings.apiKey || settings.aliyun_api_key || '',
      apiUrl: settings.api_url || settings.apiUrl || settings.aliyun_api_url || 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions',
      model: settings.model || settings.aliyun_model || 'qwen-plus'
    };
    
    // 创建Skill运行时
    const runtime = new SkillRuntime({
      llmConfig,
      timeout: 60000,
      onChunk: (text) => {
        // 所有流式过程内容 → 进思考沙箱（执行中自动展开）
        if (!firstChunkReceived) {
          firstChunkReceived = true;
          removeWaitMsg();
          thinkingBlock.classList.add('running'); // 执行中自动展开
        }
        thinkingText += text;
        thinkingBody.innerHTML = formatOutput(thinkingText);
        // 沙箱自动滚到底
        thinkingBody.scrollTop = thinkingBody.scrollHeight;
        scrollChatBottom();
      },
      onToolCall: (info) => {
        console.log('[Tool Call]', info);
        if (!firstChunkReceived) { firstChunkReceived = true; removeWaitMsg(); thinkingBlock.classList.add('running'); }
        if (info.status === 'calling') {
          const callHtml = `\x01<div class="tool-call-status" id="tc_${info.callId}"><span class="tool-call-icon">⚙️</span><span class="tool-call-name">调用工具：${info.tool}</span><span class="tool-call-spinner">...</span></div>\x02`;
          thinkingText += callHtml;
          thinkingBody.innerHTML = formatOutput(thinkingText);
          thinkingBody.scrollTop = thinkingBody.scrollHeight;
          scrollChatBottom();
        } else if (info.status === 'completed') {
          const statusEl = thinkingBody.querySelector(`#tc_${info.callId}`);
          if (statusEl) { statusEl.querySelector('.tool-call-spinner').textContent = '✓'; statusEl.classList.add('done'); }
          scrollChatBottom();
        }
      }
    });
    
    // 执行Skill：流式过程进沙箱，最终返回值作为结论输出
    // 使用已确认的页面上下文决策结果
    const effectivePageCtx = _pageContextDecision || {};
    const hasAtts = pendingAttachments && pendingAttachments.length > 0;
    const chatInputEl = document.getElementById('chatInput');
    const userInstr = chatInputEl?.value?.trim() || '';
    const analysisHint = hasAtts
      ? `请基于用户提供的附件内容，严格按照任务说明中的步骤执行。${userInstr ? '用户额外需求：' + userInstr : ''}`
      : (userInstr || '请立即按照任务说明中的步骤逐一执行，直接输出执行结果。不要评价或介绍任务说明本身。');

    // 设置中止函数：点击停止按钮时调用 runtime.abort() 中断 LLM 流
    analysisCancel = () => {
      runtime.abort();
      analysisCancel = null;
      removeWaitMsg();
      thinkingBlock.classList.remove('running');
      conclusionBubble.classList.remove('typing');
      resetAnalyzeBtn(true);
      scrollChatBottom();
    };

    const result = await runtime.execute(skill, {
      pageContext: effectivePageCtx,
      attachments: pendingAttachments || []
    }, analysisHint);
    
    // 完成：把最终结论输出到结论气泡
    analysisCancel = null;
    removeWaitMsg();
    conclusionBubble.classList.remove('typing');

    // 结论 = execute() 返回的干净文本
    const conclusionText = result || thinkingText;
    lastAnalysisText = conclusionText;
    _skillResultAccum = conclusionText;
    conclusionBubble.innerHTML = formatOutput(conclusionText);

    // 思考沙箱：完成后移除 running，默认折叠
    thinkingBlock.classList.remove('running');
    thinkingBlock.classList.remove('expanded');

    resetAnalyzeBtn(true);
    
    // 存储 Skill 分析上下文
    lastSkillContext = {
      skill: skillName || '默认',
      pageContext: effectivePageCtx,
      analysisResult: _skillResultAccum || result
    };
    
    // 显示"填入结果"按钮
    const fillBtn = document.getElementById('fillBtn');
    if (fillBtn) fillBtn.style.display = 'inline-flex';
    scrollChatBottom();

    // 统一提供 Excel/Word 下载 + 复制全文按钮
    appendExportButtons(conclusionBubble, conclusionText);
    appendInsertButton(aiWrap, conclusionText, skillName);
    appendFollowupSuggestions(conclusionBubble, conclusionText);
    appendCopyButton(conclusionBubble, conclusionText);
    
  } catch (error) {
    // 错误处理
    analysisCancel = null;
    removeWaitMsg();
    conclusionBubble.classList.remove('typing');
    conclusionBubble.textContent = '';
    const errorSpan = document.createElement('span');
    errorSpan.style.color = '#ff4d4f';
    errorSpan.textContent = '❌ 错误：' + (error?.message || String(error));
    conclusionBubble.appendChild(errorSpan);
    resetAnalyzeBtn();
    console.error('[SkillRuntime] 执行失败:', error);
  }
}

function resetAnalyzeBtn(done = false) {
  // 分析按钮已移除，此函数保留以兼容旧调用
  // 恢复发送按钮为正常状态
  const sendBtn = document.getElementById('chatSendBtn');
  if (sendBtn) {
    sendBtn.textContent = '↑';
    sendBtn.disabled = false;
  }
}

// 简单的Markdown→HTML转换（只处理常用格式）
/**
 * 把 AI 输出的 markdown 转为干净的 HTML 展示（不暴露 * # ` 等符号）
 * 同时保留段落、标签、风险色彩等视觉结构
 */
/**
 * 将文本中的 Markdown 表格渲染为 HTML 卡片表格
 * @param {string} text - 原始文本
 * @param {string[]} htmlBlocks - HTML 块数组，渲染后的表格会追加进去
 * @returns {string} 表格区域被替换为占位符后的文本
 */
function renderMarkdownTablesInText(text, htmlBlocks) {
  // 匹配 Markdown 表格块：至少一个表头行 + 分隔行 + 至少一个数据行
  const tableBlockRegex = /(?:^\|.+\|[\t ]*\n)+(?:^\|[\s\-:|]+\|[\t ]*\n)(?:^\|.+\|[\t ]*\n?)+/gm;

  return text.replace(tableBlockRegex, (tableBlock) => {
    const rows = [];
    const lines = tableBlock.trim().split('\n');
    for (const line of lines) {
      const trimmed = line.trim();
      if (!trimmed.startsWith('|')) continue;
      // 跳过分隔行（如 |---|---|）
      if (/^\|[\s\-:|]+\|$/.test(trimmed)) continue;
      const cells = trimmed
        .split('|')
        .filter((_, i, arr) => i > 0 && i < arr.length - 1)
        .map(c => c.trim());
      if (cells.length > 0) rows.push(cells);
    }

    if (rows.length < 2) return tableBlock; // 至少需要表头+1行数据

    // 处理单元格内容：保留 <br> → 转义 HTML → 还原 <br> → 应用内联 markdown 格式
    const processCell = (cell) => {
      return cell
        .replace(/&/g, '&amp;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/&lt;br\s*\/?&gt;/gi, '<br>')       // 还原 <br> 为真实换行
        .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')  // 加粗
        .replace(/__(.+?)__/g, '<strong>$1</strong>')
        .replace(/`([^`]+)`/g, '<code style="background:#f1f5f9;padding:1px 4px;border-radius:3px;font-size:12px;">$1</code>')
        .replace(/\n/g, '<br>');                      // 单元格内换行
    };

    const headers = rows[0].map(processCell);
    const dataRows = rows.slice(1).map(row => row.map(processCell));

    // 渲染 HTML 卡片表格
    let html = '<div style="margin:12px 0;overflow-x:auto;border-radius:8px;border:1px solid #e2e8f0;background:#fff;box-shadow:0 1px 3px rgba(0,0,0,0.06);">';
    html += '<table style="width:100%;border-collapse:collapse;font-size:13px;line-height:1.65;">';

    // 表头
    html += '<thead><tr style="background:linear-gradient(180deg,#f0f4ff,#e8effc);">';
    for (const h of headers) {
      html += '<th style="padding:10px 14px;text-align:left;font-weight:600;color:#1e3a5f;border-bottom:2px solid #c0d0f0;white-space:nowrap;">' + h + '</th>';
    }
    html += '</tr></thead>';

    // 数据行（斑马纹）
    html += '<tbody>';
    for (let i = 0; i < dataRows.length; i++) {
      const bg = i % 2 === 0 ? '#ffffff' : '#f8fafc';
      html += '<tr style="background:' + bg + ';">';
      for (const cell of dataRows[i]) {
        html += '<td style="padding:10px 14px;border-bottom:1px solid #e8ecf1;vertical-align:top;color:#334155;">' + cell + '</td>';
      }
      html += '</tr>';
    }
    html += '</tbody></table></div>';

    // 放入 HTML 块数组并用占位符替换（\uE000 为 Unicode 私用区，不会被 HTML 解析器截断）
    // 注意：占位符不能包含下划线 _，否则会被后续的斜体处理规则破坏
    htmlBlocks.push(html);
    return `\uE000HTMBLOCK${htmlBlocks.length - 1}\uE000`;
  });
}

function formatOutput(text) {
  // 先提取出所有HTML标记块（以\x01开头的原始HTML），暂存占位符
  // 注意：占位符不能包含下划线 _，否则会被后续的斜体处理规则破坏
  const htmlBlocks = [];
  let t = text.replace(/\x01([^\x02]*)\x02/g, (_, html) => {
    htmlBlocks.push(html);
    return `\uE000HTMBLOCK${htmlBlocks.length - 1}\uE000`;
  });

  // 0.5. 渲染 Markdown 表格为 HTML 卡片（保护在占位符中不被后续转义破坏）
  t = renderMarkdownTablesInText(t, htmlBlocks);

  // 0.6. 渲染 Mermaid 代码块为 SVG 占位符（异步渲染，先标记）
  t = renderMermaidBlocks(t, htmlBlocks);

  // 1. 先转义 HTML 特殊字符
  t = t
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;');

  // 2. 去掉 markdown 语法符号，保留语义
  t = t
    // 标题 ## xxx → 加粗一行
    .replace(/^#{1,4}\s+(.+)$/gm, '<strong>$1</strong>')
    // 加粗 **xxx** 或 __xxx__
    .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
    .replace(/__(.*?)__/g, '<strong>$1</strong>')
    // 斜体 *x* _x_ 去掉符号（下划线斜体仅匹配单词边界，避免误伤变量名/URL中的下划线）
    .replace(/\*([^*]+)\*/g, '$1')
    .replace(/(?<=\s|^)_([^_]+)_(?=\s|[，。、；：！？,.;:!?\n]|$)/gm, '$1')
    // 行内代码 `xxx` → 去反引号，保留内容
    .replace(/`([^`]+)`/g, '$1')
    // 代码块 ```...``` → 去掉围栏，保留内容
    .replace(/```[\s\S]*?```/g, (m) => m.replace(/```\w*\n?/g, '').trim())
    // 无序列表 - xxx 或 * xxx → 保留"• "前缀
    .replace(/^[\*\-]\s+(.+)$/gm, '• $1')
    // 有序列表 1. xxx → 保留原编号
    .replace(/^\d+\.\s+(.+)$/gm, (m) => m);

  // 3. 业务语义着色（保留）
  t = t
    .replace(/【(.*?)】/g, '<span class="section-tag">【$1】</span>')
    .replace(/🔴([^\n]*)/g, '<span class="risk-high">🔴$1</span>')
    .replace(/🟡([^\n]*)/g, '<span class="risk-mid">🟡$1</span>')
    .replace(/🟢([^\n]*)/g, '<span class="risk-ok">🟢$1</span>');

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

  // 4.5. Markdown 水平分割线 --- → <hr>（须在换行替换之后处理）
  t = t.replace(/(?:<br>\s*){0,2}<br>\s*-{3,}\s*(?:<br>)?/g, '<br><hr style="border:none;border-top:1px dashed #d0d5dd;margin:12px 0;"><br>');

  // 5. 还原 HTML 块（工具调用状态、折叠区块等）
  // 注意：占位符不能包含下划线 _，否则会被斜体处理规则破坏，因此使用 HTMBLOCK 格式
  t = t.replace(/\uE000HTMBLOCK(\d+)\uE000/g, (_, idx) => htmlBlocks[parseInt(idx)] || '');

  // 6. 异步渲染 Mermaid 占位符（防抖到 DOM 更新后执行）
  scheduleMermaidRender();

  return t;
}

/**
 * 提取 Mermaid 代码块，替换为占位符（在 HTML 转义之前调用）
 */
function renderMermaidBlocks(text, htmlBlocks) {
  return text.replace(/```[ \t]*mermaid([^\n`]*)\n?([\s\S]*?)```/gi, (_, meta, body) => {
    const inlineHeader = (meta || '').trim();
    const bodyText = body || '';
    const code = inlineHeader && /^(flowchart|graph|sequenceDiagram|classDiagram|stateDiagram|erDiagram|gantt|pie|timeline|mindmap|journey|gitgraph)\b/i.test(inlineHeader)
      ? inlineHeader + '\n' + bodyText.trim()
      : bodyText.trim();
    if (!code) return '';
    const id = 'mermaid-' + hashMermaidCode(code) + '-' + Math.random().toString(36).slice(2, 7);
    const loadingSpinner = '<div class="mermaid-loading"><div class="mermaid-spinner"></div><span>流程图渲染中...</span></div>';
    const container = `<div class="mermaid-container" data-mermaid-id="${id}" data-mermaid-code="${encodeURIComponent(code)}">${loadingSpinner}</div>`;
    htmlBlocks.push(container);
    return `\uE000HTMBLOCK${htmlBlocks.length - 1}\uE000`;
  });
}

const mermaidSvgCache = new Map();
let mermaidRenderTimer = null;

function scheduleMermaidRender() {
  if (mermaidRenderTimer) clearTimeout(mermaidRenderTimer);
  mermaidRenderTimer = setTimeout(() => {
    mermaidRenderTimer = null;
    renderMermaidPlaceholders();
  }, 180);
}

function hashMermaidCode(code) {
  let hash = 0;
  for (let i = 0; i < code.length; i++) {
    hash = ((hash << 5) - hash + code.charCodeAt(i)) | 0;
  }
  return Math.abs(hash).toString(36);
}

function safeDecodeURIComponent(value) {
  try { return decodeURIComponent(value || ''); }
  catch (_) { return value || ''; }
}

function normalizeMermaidCode(code) {
  let processed = (code || '')
    .replace(/^\uFEFF/, '')
    .replace(/\r\n?/g, '\n')
    .replace(/\u201C/g, '"').replace(/\u201D/g, '"')
    .replace(/\u2018/g, "'").replace(/\u2019/g, "'")
    .replace(/：/g, ':').replace(/；/g, ';')
    .replace(/，/g, ',').replace(/！/g, '!')
    .replace(/＝/g, '=').replace(/＞/g, '>').replace(/＜/g, '<')
    .replace(/\u3001/g, ',')
    .replace(/\n\s*\n\s*\n/g, '\n\n')
    .replace(/\s+$/gm, '');

  const firstLine = processed.trim().split('\n')[0].trim();
  if (!/^(flowchart|graph|sequenceDiagram|classDiagram|stateDiagram|erDiagram|gantt|pie|timeline|mindmap|journey|gitgraph)\b/i.test(firstLine)) {
    processed = 'flowchart TD\n' + processed;
  }

  if (/^(flowchart|graph)\b/i.test(processed.trim())) {
    processed = processed
      .split('\n')
      .map(line => quoteFlowchartLabels(line))
      .join('\n');
  }

  return processed;
}

function quoteFlowchartLabels(line) {
  return line.replace(/(\b[A-Za-z][\w-]*)(\[[^\]\n]*\]|\{[^}\n]*\}|\([^)()\n]*\))/g, (match, id, shape) => {
    const open = shape[0];
    const close = shape[shape.length - 1];
    const inner = shape.slice(1, -1).trim();
    if (!inner || (inner.startsWith('"') && inner.endsWith('"'))) return match;
    if (!/[\u4e00-\u9fff\s（）()，。；：？！、<>≤≥"'/%&]/.test(inner)) return match;
    const safeInner = inner.replace(/"/g, "'");
    return `${id}${open}"${safeInner}"${close}`;
  });
}

function appendMermaidExpandButton(container) {
  if (container.querySelector('.mermaid-expand-btn')) return;
  const expandBtn = document.createElement('button');
  expandBtn.className = 'mermaid-expand-btn';
  expandBtn.title = '全屏预览';
  expandBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" y1="3" x2="14" y2="10"/><line x1="3" y1="21" x2="10" y2="14"/></svg>';
  expandBtn.addEventListener('click', (e) => {
    e.stopPropagation();
    openMermaidFullscreen(container);
  });
  container.appendChild(expandBtn);
}

/**
 * 异步渲染所有 Mermaid 占位符（DOM 更新后调用）
 */
async function renderMermaidPlaceholders() {
  if (typeof mermaid === 'undefined') {
    // mermaid 库未加载：将占位容器降级显示为代码块提示
    document.querySelectorAll('.mermaid-container:not([data-rendered])').forEach(c => {
      const code = safeDecodeURIComponent(c.dataset.mermaidCode || '');
      c.innerHTML = '<div style="padding:10px;background:#f8f9fa;border:1px dashed #d0d5dd;border-radius:8px;font-size:12px;color:#666;"><div style="margin-bottom:6px;color:#999;">⚠️ 流程图库未加载，显示源代码：</div><pre style="margin:0;white-space:pre-wrap;font-size:11px;color:#333;">' + code.replace(/</g, '&lt;').replace(/>/g, '&gt;') + '</pre></div>';
      c.dataset.rendered = 'true';
    });
    return;
  }
  
  const containers = document.querySelectorAll('.mermaid-container:not([data-rendered]):not([data-rendering])');
  for (const container of containers) {
    try {
      const code = safeDecodeURIComponent(container.dataset.mermaidCode || '');
      if (!code) continue;

      if (mermaidSvgCache.has(code)) {
        container.innerHTML = mermaidSvgCache.get(code);
        container.dataset.rendered = 'true';
        container.classList.add('mermaid-rendered');
        appendMermaidExpandButton(container);
        continue;
      }

      container.dataset.rendering = 'true';
      const processed = normalizeMermaidCode(code);
      const id = container.dataset.mermaidId || 'm' + Date.now();
      const { svg } = await mermaid.render(id, processed);
      mermaidSvgCache.set(code, svg);
      container.innerHTML = svg;
      container.dataset.rendered = 'true';
      delete container.dataset.rendering;
      container.classList.add('mermaid-rendered');
      appendMermaidExpandButton(container);
    } catch (e) {
      // 渲染失败：尝试降级为 flowchart TD 再试一次
      const rawCode = safeDecodeURIComponent(container.dataset.mermaidCode || '');
      try {
        let fallbackCode = normalizeMermaidCode(rawCode);
        const fbId = container.dataset.mermaidId + '-fb';
        const { svg } = await mermaid.render(fbId, fallbackCode);
        mermaidSvgCache.set(rawCode, svg);
        container.innerHTML = svg;
        container.dataset.rendered = 'true';
        delete container.dataset.rendering;
        container.classList.add('mermaid-rendered');
        appendMermaidExpandButton(container);
      } catch (_) {
        // 降级也失败，显示原始代码 + 错误提示
        container.innerHTML = '<div style="padding:10px;background:#fff7ed;border:1px solid #fed7aa;border-radius:8px;font-size:12px;"><div style="margin-bottom:6px;color:#c2410c;">⚠️ 流程图渲染失败，显示源代码：</div><pre style="margin:0;white-space:pre-wrap;font-size:11px;color:#333;background:#fff;padding:8px;border-radius:4px;">' + rawCode.replace(/</g, '&lt;').replace(/>/g, '&gt;') + '</pre></div>';
        container.dataset.rendered = 'true';
        delete container.dataset.rendering;
      }
    }
  }
}

/**
 * 全屏预览 Mermaid 流程图
 */
function openMermaidFullscreen(container) {
  // 移除已有弹窗
  const existing = document.getElementById('mermaidFullscreenOverlay');
  if (existing) existing.remove();

  const overlay = document.createElement('div');
  overlay.id = 'mermaidFullscreenOverlay';
  overlay.className = 'mermaid-fullscreen-overlay';

  // 关闭按钮
  const closeBtn = document.createElement('button');
  closeBtn.className = 'mermaid-fullscreen-close';
  closeBtn.innerHTML = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>';
  closeBtn.title = '关闭 (Esc)';
  closeBtn.addEventListener('click', () => overlay.remove());

  // 内容区（克隆 SVG）
  const content = document.createElement('div');
  content.className = 'mermaid-fullscreen-content';
  const svgEl = container.querySelector('svg');
  if (svgEl) {
    const cloned = svgEl.cloneNode(true);
    // 让 SVG 自适应缩放
    cloned.removeAttribute('width');
    cloned.removeAttribute('height');
    cloned.style.width = '100%';
    cloned.style.height = '100%';
    cloned.style.maxWidth = '100%';
    cloned.style.maxHeight = '100%';
    content.appendChild(cloned);
  }

  overlay.appendChild(closeBtn);
  overlay.appendChild(content);

  // 点击空白区域关闭
  overlay.addEventListener('click', (e) => {
    if (e.target === overlay) overlay.remove();
  });

  document.body.appendChild(overlay);

  // Esc 关闭
  const escHandler = (e) => {
    if (e.key === 'Escape') {
      overlay.remove();
      document.removeEventListener('keydown', escHandler);
    }
  };
  document.addEventListener('keydown', escHandler);
}

/**
 * 把 formatOutput 的 HTML 反转为纯文本（用于复制）
 */
function htmlToPlainText(html) {
  const tmp = document.createElement('div');
  tmp.innerHTML = html
    .replace(/<br\s*\/?>/gi, '\n')
    .replace(/<\/p>/gi, '\n')
    .replace(/<\/div>/gi, '\n');
  return (tmp.textContent || tmp.innerText || '').replace(/\n{3,}/g, '\n\n').trim();
}

// ==============================
// 一键填写内容到页面输入框
// ==============================
function fillOpinion() {
  // 从分析结果中提取可回填的建议内容（兼容多种 Section 标题变体）
  const opinionPattern = /【(?:建议动作|行动建议|综合建议|决策建议|市场建议|销售建议)】\s*([\s\S]+?)(?=【|$)/;
  const altPattern = /【(?:结论|摘要|高管摘要)】[\s\S]*?(?:建议|动作|下一步)[：:]?[\s\S]*?(?=【|$)/;

  let opinionText = '';
  
  // 尝试匹配明确的建议 Section
  const match = lastAnalysisText.match(opinionPattern);
  if (match) {
    opinionText = match[1].trim();
  }
  
  // 如果没有明确 Section，尝试从全文末尾提取"建议"部分
  if (!opinionText) {
    const lines = lastAnalysisText.split('\n');
    // 从后往前找包含“建议/行动/结论”的关键行
    for (let i = lines.length - 1; i >= 0; i--) {
      const line = lines[i].trim();
      if (!line) continue;
      if (/^建议[：:]|行动建议[：:]|下一步[：:]|综合建议|决策建议/.test(line)) {
        opinionText = lines.slice(i + 1).join('\n').trim();
        break;
      }
    }
  }
  
  // 最终兜底：取最后300字（而非500字），减少干扰
  if (!opinionText) {
    opinionText = lastAnalysisText.slice(-300).trim();
  }

  if (!opinionText) {
    showToast('⚠️ 未找到可填入的内容，请先完成分析', 'warn');
    return;
  }

  // 向当前Tab发送填写指令
  if (typeof chrome !== 'undefined' && chrome.tabs) {
    chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
      if (tabs[0]) {
        chrome.tabs.sendMessage(tabs[0].id, {
          type: 'FILL_OPINION',
          text: opinionText
        });
      }
    });
  } else {
    // 非插件环境：显示文本供手动复制
    showToast('✅ 分析内容已准备好，请手动复制到目标页面');
  }
}

// ==============================
// Skill 浮层（/ 斜杠调出）
// ==============================

/** 处理输入框的 / 斜杠调出 skill 浮层 */
function handleSkillPopup(input) {
  const val = input.value;
  const popup = document.getElementById('skillPopup');
  if (!popup) return;

  if (val.startsWith('/')) {
    const keyword = val.substring(1).trim().toLowerCase();
    showSkillPopup(keyword);
  } else {
    hideSkillPopup();
  }
}

/** 显示 skill 浮层，可选按 keyword 过滤 */
function showSkillPopup(keyword) {
  const popup = document.getElementById('skillPopup');
  const listEl = document.getElementById('skillPopupList');
  const searchEl = document.getElementById('skillPopupSearch');
  if (!popup || !listEl) return;

  const skills = _allPopupSkills.length > 0 ? _allPopupSkills : [];
  const filtered = keyword
    ? skills
        .map((s, idx) => ({ skill: s, score: getSkillSearchScore(s, keyword), idx }))
        .filter(item => item.score > 0)
        .sort((a, b) => b.score - a.score || a.idx - b.idx)
        .map(item => item.skill)
    : skills;

  listEl.innerHTML = '';
  if (filtered.length === 0) {
    listEl.innerHTML = '<div class="skill-popup-empty">未找到匹配的 Skill</div>';
  } else {
    filtered.forEach(s => {
      const item = document.createElement('div');
      item.className = 'skill-popup-item';
      const badge = s.source === 'remote' ? '🌐' : s.source === 'local' ? '📁' : '📦';
      item.innerHTML = `<span class="skill-popup-badge">${badge}</span> ${escapeHtml(s.name)}`;
      item.addEventListener('click', () => selectSkillFromPopup(s.name));
      listEl.appendChild(item);
    });
  }

  if (searchEl) {
    searchEl.value = keyword || '';
    searchEl.oninput = () => {
      const kw = searchEl.value.trim().toLowerCase();
      showSkillPopup(kw);
    };
  }

  popup.style.display = 'block';
}

function getSkillSearchText(skill) {
  const intent = _SKILL_INTENT_MAP[skill.filename] || _SKILL_INTENT_MAP[skill.name] || {};
  return [
    skill.name,
    skill.filename,
    skill.description,
    intent.name,
    intent.desc,
    ...(intent.keywords || [])
  ].filter(Boolean).join(' ').toLowerCase();
}

function getSkillSearchScore(skill, keyword) {
  const kw = (keyword || '').toLowerCase();
  if (!kw) return 1;
  const intent = _SKILL_INTENT_MAP[skill.filename] || _SKILL_INTENT_MAP[skill.name] || {};
  const name = (skill.name || '').toLowerCase();
  const filename = (skill.filename || '').toLowerCase();
  const desc = [skill.description, intent.desc].filter(Boolean).join(' ').toLowerCase();
  const keywords = (intent.keywords || []).map(k => k.toLowerCase());

  if (name === kw || filename === kw) return 120;
  if (name.startsWith(kw) || filename.startsWith(kw)) return 100;
  if (name.includes(kw) || filename.includes(kw)) return 90;
  if (keywords.some(k => k === kw)) return 80;
  if (keywords.some(k => k.startsWith(kw))) return 65;
  if (keywords.some(k => k.includes(kw))) return 55;
  if (desc.includes(kw)) return 35;
  return getSkillSearchText(skill).includes(kw) ? 10 : 0;
}

/** 隐藏 skill 浮层 */
function hideSkillPopup() {
  const popup = document.getElementById('skillPopup');
  if (popup) popup.style.display = 'none';
}

/** 从浮层选中 skill */
function selectSkillFromPopup(name) {
  _selectedSkill = name;
  const chatInput = document.getElementById('chatInput');
  if (chatInput) {
    chatInput.value = '';
    chatInput.focus();
  }
  hideSkillPopup();
  showSelectedSkillTag(name);
  showToast(`✅ 已选择 Skill：${name}`);

  // 自动触发分析
  startAnalysis();
}

/** 在输入框上方显示已选 skill 标签 */
function showSelectedSkillTag(label) {
  // 移除旧的
  const old = document.getElementById('selectedSkillTag');
  if (old) old.remove();

  const inputArea = document.querySelector('.chat-input-area');
  if (!inputArea) return;

  const tag = document.createElement('div');
  tag.id = 'selectedSkillTag';
  tag.className = 'selected-skill-tag';
  tag.innerHTML = `📌 ${escapeHtml(label)} <span class="tag-remove" title="取消选择">✕</span>`;
  tag.querySelector('.tag-remove').addEventListener('click', () => {
    _selectedSkill = '';
    tag.remove();
  });
  // 插到 attachments 后面，input-row 前面
  const inputRow = inputArea.querySelector('.chat-input-row');
  if (inputRow) {
    inputArea.insertBefore(tag, inputRow);
  } else {
    inputArea.appendChild(tag);
  }
}

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

// 点击外部关闭 skill 浮层
document.addEventListener('click', (e) => {
  const popup = document.getElementById('skillPopup');
  const chatInput = document.getElementById('chatInput');
  if (popup && popup.style.display !== 'none') {
    if (!popup.contains(e.target) && e.target !== chatInput) {
      hideSkillPopup();
    }
  }
});

// Esc 关闭浮层
document.addEventListener('keydown', (e) => {
  if (e.key === 'Escape') {
    hideSkillPopup();
  }
});

// ==============================
// Skill 意图识别引擎（全局作用域）
// ==============================

const _SKILL_INTENT_MAP = {
  "PRD生成": { name: "PRD生成", keywords: ["PRD","产品需求文档","需求文档","产品需求","功能规格","写需求","功能文档","需求规格","prd模板"], desc: "按照高质量PRD样例生成开发可交付的产品需求文档，覆盖背景、价值、功能、流程、字段、规则、风险和验收。" },
  "需求评审": { name: "需求评审", keywords: ["需求评审","评审需求","审PRD","PRD评审","需求质量","需求漏洞","验收标准检查"], desc: "对PRD或需求说明进行质量评审，识别背景、范围、规则、交互、数据、验收和风险缺口。" },
  "技术方案评审": { name: "技术方案评审", keywords: ["技术方案评审","评审技术方案","架构评审","技术设计评审","方案可行性","接口设计评审"], desc: "从产品价值、架构、数据、接口、性能、安全、成本、排期和可运维性评审技术方案。" },
  "运营方案制定": { name: "运营方案制定", keywords: ["运营方案","活动方案","上线运营","增长方案","拉新","促活","留存","转化","campaign"], desc: "围绕产品上线、增长、活动和用户运营制定目标、机制、渠道、节奏、预算、指标和复盘计划。" },
  "流程图构建": { name: "流程图构建", keywords: ["流程图","画流程","泳道图","业务流程图","用户流程","Mermaid","流程梳理"], desc: "把文字需求、PRD或流程说明转成可渲染的Mermaid流程图或泳道式流程说明。" },
  "流程文件构建": { name: "流程文件构建", keywords: ["流程文件","流程文档","SOP","写流程","流程说明","制度流程","流程规范"], desc: "生成企业级流程文件，覆盖目的、边界、角色、KCP、KPI、活动说明和流程图占位。" },
  "用户故事拆解": { name: "用户故事拆解", keywords: ["用户故事","User Story","拆需求","拆Epic","验收标准","AC","Story Map","backlog"], desc: "把Epic或大需求拆成可交付User Story，附验收标准、优先级、估算和依赖关系。" },
  "需求优先级排序": { name: "需求优先级排序", keywords: ["需求排序","优先级","RICE","ICE","MoSCoW","Kano","backlog排序","迭代规划"], desc: "用RICE、ICE、MoSCoW或Kano对需求池排序，输出取舍依据和迭代建议。" }
};

function matchSkillIntent(userMsg) {
  if (!userMsg || userMsg.length < 2) return null;
  const msg = userMsg.toLowerCase();
  let bestMatch = null;
  let bestScore = 0;
  for (const [filename, skill] of Object.entries(_SKILL_INTENT_MAP)) {
    if (!AISC_BUILTIN_SKILL_FILES.includes(filename)) continue;
    let score = 0;
    for (let i = 0; i < skill.keywords.length; i++) {
      const kw = skill.keywords[i].toLowerCase();
      if (msg.includes(kw)) {
        const weight = skill.keywords.length - i;
        score = Math.max(score, weight);
      }
    }
    if (score > bestScore) {
      bestScore = score;
      bestMatch = { filename, name: skill.name, desc: skill.desc, confidence: Math.min(95, 60 + score * 5) };
    }
  }
  return bestScore > 0 ? bestMatch : null;
}

let _pendingChatAfterIntent = null;

function showSkillIntentBar(skill, userMsg, fullMsg) {
  const history = document.getElementById('chatHistory');
  if (!history) return;
  const oldBar = history.querySelector('.skill-intent-bar');
  if (oldBar) oldBar.remove();

  const bar = document.createElement('div');
  bar.className = 'skill-intent-bar';
  bar.innerHTML = `<span class="skill-intent-icon">🎯</span>
    <span class="skill-intent-text">检测到可能需要 <strong>${escHtml(skill.name)}</strong> — ${escHtml(skill.desc)}（${skill.confidence}%）</span>
    <button class="skill-intent-run">执行</button>
    <button class="skill-intent-skip">跳过</button>`;

  bar.querySelector('.skill-intent-run').addEventListener('click', () => {
    bar.remove();
    _selectedSkill = skill.filename;
    showSelectedSkillTag(skill.name);
    // 恢复附件到 pendingAttachments（供 startAnalysis 使用）
    if (_pendingChatAfterIntent?.savedAttachments?.length) {
      pendingAttachments = _pendingChatAfterIntent.savedAttachments.slice();
    }
    // 用用户的原始消息作为分析指令
    const chatInput = document.getElementById('chatInput');
    if (chatInput) chatInput.value = userMsg;
    startAnalysis();
  });

  bar.querySelector('.skill-intent-skip').addEventListener('click', () => {
    bar.remove();
    if (!_pendingChatAfterIntent) return;
    const saved = _pendingChatAfterIntent;
    _pendingChatAfterIntent = null;
    if (shouldConfirmPageContextForChat(saved.msg, saved.fullMsg)) {
      _pendingChatPageContext = saved;
      showChatPageContextConfirmBar(pageContext, _pendingChatPageContext);
      return;
    }
    _doSendChat(saved.msg, saved.displayMsg, saved.fullMsg);
  });

  history.appendChild(bar);
  scrollChatBottom();
}

// ==============================
// 页面上下文意图检测（按需注入）
// ==============================

const _PAGE_INTENT_KEYWORDS = [
  '这个页面', '当前页面', '页面信息', '页面内容', '这个网页', '当前网页',
  '这篇文章', '这篇', '这段', '这份', '这条', '这张表', '这份合同', '这份报告', '这份计划书',
  '当前你看到', '你看到的', '你现在看到', '屏幕上', '左边页面', '上面内容', '这里面',
  '这个表单', '这个审批', '这个单据', '这个流程页面', '这个网站', '这个政策', '这个计划书',
  '这个竞品', '这个商品页', '这个价格页', '页面上的', '这个网站的', '网页上的',
  '分析页面', '看一下页面', '看看这个页面', '当前页面内容'
];

const _PAGE_NO_CONTEXT_KEYWORDS = [
  '不要看页面', '别看页面', '不用页面', '不要用页面', '不基于页面', '忽略页面',
  '不要参考当前页面', '别参考当前页面', '只回答问题', '不要结合页面'
];

const _PAGE_CONTEXT_CONFIRM_KEYWORDS = [
  '分析', '评估', '评价', '总结', '概括', '提炼', '审查', '诊断', '解读',
  '看看', '看一下', '帮我看', '判断', '给建议', '输出', '生成', '改写', '润色', '提取'
];

/**
 * 检测用户消息是否明确提到了页面信息
 * @param {string} msg - 用户消息
 * @returns {boolean}
 */
function detectPageIntent(msg) {
  if (!msg) return false;
  const lower = msg.toLowerCase();
  return _PAGE_INTENT_KEYWORDS.some(kw => lower.includes(kw));
}

function detectNoPageIntent(msg) {
  if (!msg) return false;
  const lower = msg.toLowerCase();
  return _PAGE_NO_CONTEXT_KEYWORDS.some(kw => lower.includes(kw));
}

function hasReadablePageContext(ctx) {
  if (!ctx || typeof ctx !== 'object') return false;
  const text = [ctx.pageText, ctx.text, ctx.selectCopyText, ctx.screenshotText]
    .filter(Boolean)
    .join('\n\n')
    .trim();
  if (text.length > 20) return true;
  if (ctx.pageTitle || ctx.title || ctx.pageUrl || ctx.url || ctx.billId) return true;
  if (ctx.fields && Object.keys(ctx.fields).length > 0) return true;
  if (Array.isArray(ctx.tableData) && ctx.tableData.length > 0) return true;
  if (Array.isArray(ctx.attachments) && ctx.attachments.length > 0) return true;
  return false;
}

function shouldConfirmPageContextForChat(msg, fullMsg = '') {
  if (!hasReadablePageContext(pageContext)) return false;
  if (detectPageIntent(msg) || detectNoPageIntent(msg)) return false;
  if (String(fullMsg || '').includes('[附件内容]')) return false;
  const lower = String(msg || '').toLowerCase();
  return _PAGE_CONTEXT_CONFIRM_KEYWORDS.some(kw => lower.includes(kw));
}

function resolveChatPageContextForMessage(msg, mode = 'auto') {
  if (!hasReadablePageContext(pageContext)) return {};
  if (mode === 'force') return pageContext || {};
  if (mode === 'none') return {};
  if (detectNoPageIntent(msg)) return {};
  return detectPageIntent(msg) ? (pageContext || {}) : {};
}

let _pendingChatPageContext = null;

function showChatPageContextConfirmBar(ctx, savedChat) {
  const history = document.getElementById('chatHistory');
  if (!history) return;
  const oldBar = history.querySelector('.page-context-bar.chat-context');
  if (oldBar) oldBar.remove();

  const pageTitle = ctx?.pageTitle || ctx?.title || '已读取页面';
  const text = [ctx?.pageText, ctx?.text, ctx?.selectCopyText, ctx?.screenshotText]
    .filter(Boolean)
    .join('\n\n')
    .trim();
  const excerpt = text ? text.slice(0, 80).replace(/\s+/g, ' ') : '';

  const bar = document.createElement('div');
  bar.className = 'page-context-bar chat-context';
  bar.innerHTML = `<span class="page-context-icon">📄</span>
    <span class="page-context-text">已读取页面「${escHtml(pageTitle)}」${excerpt ? '：' + escHtml(excerpt) + '...' : ''}。这次回答是否使用页面内容？</span>
    <button class="page-context-yes">使用页面</button>
    <button class="page-context-no">不使用</button>`;

  bar.querySelector('.page-context-yes').addEventListener('click', () => {
    bar.remove();
    const saved = savedChat || _pendingChatPageContext;
    _pendingChatPageContext = null;
    if (saved) _doSendChat(saved.msg, saved.displayMsg, saved.fullMsg, 'force');
  });

  bar.querySelector('.page-context-no').addEventListener('click', () => {
    bar.remove();
    const saved = savedChat || _pendingChatPageContext;
    _pendingChatPageContext = null;
    if (saved) _doSendChat(saved.msg, saved.displayMsg, saved.fullMsg, 'none');
  });

  history.appendChild(bar);
  scrollChatBottom();
}

// 页面上下文确认结果（由确认条设置，供 startAnalysis 读取）
let _pageContextDecision = null;

/**
 * 弹出页面上下文确认条
 * 场景：用户执行 Skill 但没附件且没明确提到页面，需要确认是否注入页面信息
 * @param {object} ctx - pageContext 对象
 * @param {string} skillName - Skill 名称
 */
function showPageContextConfirmBar(ctx, skillName) {
  const history = document.getElementById('chatHistory');
  if (!history) return;
  // 移除旧确认条
  const oldBar = history.querySelector('.page-context-bar');
  if (oldBar) oldBar.remove();

  const pageTitle = ctx.pageTitle || ctx.title || '未知页面';
  const billId = ctx.billId || '';

  const bar = document.createElement('div');
  bar.className = 'page-context-bar';
  bar.innerHTML = `<span class="page-context-icon">📄</span>
    <span class="page-context-text">检测到页面上下文「${escHtml(pageTitle)}${billId ? ' / ' + escHtml(billId) : ''}」，是否基于页面信息执行 <strong>${escHtml(skillName || '分析')}</strong>？</span>
    <button class="page-context-yes">基于页面执行</button>
    <button class="page-context-no">忽略页面</button>`;

  bar.querySelector('.page-context-yes').addEventListener('click', () => {
    bar.remove();
    _pageContextDecision = pageContext || {};
    _resumeStartAnalysis();
  });

  bar.querySelector('.page-context-no').addEventListener('click', () => {
    bar.remove();
    _pageContextDecision = {};
    _resumeStartAnalysis();
  });

  history.appendChild(bar);
  scrollChatBottom();
}

// 暂存 startAnalysis 被暂停时的 Skill 参数
let _pendingStartAnalysis = null;

/**
 * 页面确认后恢复执行 startAnalysis
 */
function _resumeStartAnalysis() {
  if (!_pendingStartAnalysis) return;
  const skillName = _pendingStartAnalysis;
  _pendingStartAnalysis = null;
  _selectedSkill = skillName;
  showSelectedSkillTag(skillName);
  _doStartAnalysis(skillName);
}

// ==============================
// 对话框
// ==============================
function handleChatKey(e) {
  // isComposing=true 表示正在用输入法组字（中文候选词阶段），此时 Enter 只是确认候选词，不提交
  if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) {
    e.preventDefault();
    sendChat();
  }
}

function autoResizeChat(el) {
  el.style.height = 'auto';
  el.style.height = Math.min(el.scrollHeight, 80) + 'px';
}

// ==============================
// 文件上传处理
// ==============================
let pendingAttachments = []; // { name, type, content(text) | dataUrl(binary) }

function handleFileSelect(e) {
  const files = Array.from(e.target.files);
  if (!files.length) return;

  // 需要用 parseFileToText 解析的文件类型
  const parsableExts = ['docx', 'pdf', 'xlsx', 'xls', 'doc'];

  files.forEach(file => {
    const ext = file.name.split('.').pop().toLowerCase();
    const isText = file.type.startsWith('text/') ||
      /\.(txt|md|csv|json|xml|yaml|yml|js|ts|py|java|html|css|log)$/i.test(file.name);
    const isParsable = parsableExts.includes(ext);

    if (isParsable) {
      // docx/pdf/xlsx 等需要用 parseFileToText 提取文本
      const att = {
        name: file.name,
        type: file.type || 'application/octet-stream',
        size: file.size,
        content: null,
        dataUrl: null,
        parsing: true
      };
      pendingAttachments.push(att);
      renderAttachments();

      parseFileToText(file).then(text => {
        att.content = text || '';
        att.parsing = false;
        renderAttachments();
      }).catch(err => {
        console.warn('[附件] 解析失败:', file.name, err);
        att.content = '[文件解析失败：' + (err.message || '未知错误') + ']';
        att.parsing = false;
        renderAttachments();
      });
    } else {
      // 文本或图片等其他文件
      const reader = new FileReader();
      reader.onload = (ev) => {
        pendingAttachments.push({
          name: file.name,
          type: file.type || 'application/octet-stream',
          size: file.size,
          content: isText ? ev.target.result : null,
          dataUrl: isText ? null : ev.target.result
        });
        renderAttachments();
      };

      if (isText) {
        reader.readAsText(file, 'utf-8');
      } else {
        reader.readAsDataURL(file);
      }
    }
  });

  // 清空 input 允许重复选同一文件
  e.target.value = '';
}

function renderAttachments() {
  const box = document.getElementById('chatAttachments');
  if (pendingAttachments.length === 0) {
    box.style.display = 'none';
    box.innerHTML = '';
    return;
  }
  box.style.display = 'flex';
  box.innerHTML = '';
  pendingAttachments.forEach((f, i) => {
    const tag = document.createElement('span');
    tag.className = 'attach-tag';
    tag.append(document.createTextNode((f.parsing ? '⏳' : '📄') + ' ' + f.name + (f.parsing ? ' (解析中...)' : '')));
    const remove = document.createElement('span');
    remove.className = 'attach-remove';
    remove.dataset.idx = String(i);
    remove.textContent = '✕';
    tag.appendChild(remove);
    box.appendChild(tag);
  });
  box.querySelectorAll('.attach-remove').forEach(btn => {
    btn.addEventListener('click', (e) => {
      const idx = parseInt(e.target.dataset.idx);
      pendingAttachments.splice(idx, 1);
      renderAttachments();
    });
  });
}

function buildAttachmentContext() {
  if (!pendingAttachments.length) return '';
  return '\n\n[附件内容]\n' + pendingAttachments.map(f => {
    if (f.parsing) {
      return `--- 文件：${f.name}（正在解析中，请稍后再发送）---`;
    } else if (f.content) {
      // 文本/解析后的内容直接嵌入（最多10000字）
      return `--- 文件：${f.name} ---\n${f.content.slice(0, 10000)}`;
    } else {
      return `--- 文件：${f.name}（二进制，${(f.size / 1024).toFixed(1)}KB，类型：${f.type}）---`;
    }
  }).join('\n\n');
}

// ==============================
// 对话发送
// ==============================
function sendChat() {
  // 如果 Skill 分析正在进行中，点击按钮 = 中断分析
  if (analysisCancel) {
    analysisCancel();
    analysisCancel = null;
    if (analysiswaitTimer) { clearInterval(analysiswaitTimer); analysiswaitTimer = null; }
    const pendingWait = document.querySelector('.chat-msg[data-waitid="analysis-wait"]');
    if (pendingWait?.parentNode) pendingWait.parentNode.removeChild(pendingWait);
    // 恢复发送按钮
    const sendBtn = document.getElementById('chatSendBtn');
    sendBtn.textContent = '↑';
    sendBtn.disabled = false;
    return;
  }

  // 如果对话正在进行中，点击按钮 = 中断当前对话
  if (chatBusy) {
    if (chatCancel) { chatCancel(); chatCancel = null; }
    chatBusy = false;
    const sendBtn = document.getElementById('chatSendBtn');
    sendBtn.textContent = '↑';
    sendBtn.disabled = false;
    return;
  }

  const input = document.getElementById('chatInput');
  const msg = input.value.trim();
  if (!msg && pendingAttachments.length === 0) return;

  chatBusy = true;
  input.value = '';
  // 如果正在录音，发送时静默停止（不把语音文字重新写回输入框）
  if (_voiceActive) {
    stopVoiceInput(true);
  }
  // 同步清空语音累积文字
  _voiceBaseText = '';
  _voiceInterimText = '';
  input.style.height = 'auto';
  const sendBtn = document.getElementById('chatSendBtn');
  sendBtn.textContent = '⬛';
  sendBtn.disabled = false; // 保持可点击，点击=中断

  // 附件内容拼入消息
  const attachCtx = buildAttachmentContext();
  const fullMsg = msg + attachCtx;
  // 在清空前快照附件（供意图识别后恢复用）
  const _attachmentsSnapshot = pendingAttachments.slice();

  // 显示用户气泡（只显示用户输入的文字 + 附件名，不显示全文）
  const displayMsg = msg + (pendingAttachments.length
    ? `\n📎 ${pendingAttachments.map(f => f.name).join('、')}`
    : '');
  appendChatMsg(displayMsg, 'user');

  // 清空附件
  pendingAttachments = [];
  renderAttachments();

  // ===== Skill 意图识别 =====
  // 先做本地关键词匹配（零延迟），如果匹配到则弹出确认条，暂停对话发送
  const intentMatch = matchSkillIntent(msg);
  if (intentMatch) {
    chatBusy = false;
    const sendBtn2 = document.getElementById('chatSendBtn');
    sendBtn2.textContent = '↑';
    sendBtn2.disabled = false;

    // 暂存消息和附件快照，供跳过/执行时使用
    _pendingChatAfterIntent = { msg, displayMsg, fullMsg, savedAttachments: _attachmentsSnapshot };

    // 显示确认条
    showSkillIntentBar(intentMatch, msg, fullMsg);
    return;
  }

  // 页面已读好但用户没有明确说要用页面时，先确认，避免页面内容污染普通问题。
  if (shouldConfirmPageContextForChat(msg, fullMsg)) {
    chatBusy = false;
    const sendBtn3 = document.getElementById('chatSendBtn');
    if (sendBtn3) { sendBtn3.textContent = '↑'; sendBtn3.disabled = false; }
    _pendingChatPageContext = { msg, displayMsg, fullMsg };
    showChatPageContextConfirmBar(pageContext, _pendingChatPageContext);
    return;
  }

  // 无意图匹配，正常发送对话
  _doSendChat(msg, displayMsg, fullMsg);
}

/**
 * 实际发送对话消息（内部函数）
 * 从 sendChat 拆出来，意图识别跳过后也调用此函数
 */
function _doSendChat(msg, displayMsg, fullMsg, pageContextMode = 'auto') {
  chatBusy = true;
  const sendBtn = document.getElementById('chatSendBtn');
  sendBtn.textContent = '⬛';
  sendBtn.disabled = false; // 保持可点击，点击=中断

  // 立即显示“AI产品经理正在为你分析...”即时回显气泡
  const quickAckMsg = appendChatMsg('', 'ai typing quick-ack');
  quickAckMsg.innerHTML = '<span class="quick-ack-text">AI产品经理正在为你分析，请稍候...</span>';
  scrollChatBottom();

  const aiMsg = appendChatMsg('', 'ai typing');
  aiMsg.style.display = 'none'; // 先隐藏，首字到达后显示
  let chatRawText = '';
  let chatFirstChunk = false;

  // 注册工具调用文本替换回调（工具执行完后调用）
  window.__replaceChatText = (newText) => {
    chatRawText = newText;
    aiMsg.innerHTML = formatOutput(chatRawText);
    scrollChatBottom();
  };

  // 注册文档生成进度回调（直接操作 DOM，不经过 typewriterQueue）
  window.__showDocGenProgress = (docName) => {
    // 清理之前可能残留的标记文本
    chatRawText = chatRawText.replace(/__DOC_PROGRESS_START__[\s\S]*?__DOC_PROGRESS_END__/g, '');
    chatRawText = chatRawText.replace(/__DOC_DONE_START__[\s\S]*?__DOC_DONE_END__/g, '');
    const progressHtml = `<div class="doc-gen-progress" id="__doc_gen_progress__">
      <div class="doc-gen-spinner"></div>
      <div class="doc-gen-info">
        <span class="doc-gen-label">正在生成${escHtml(docName)}，请稍候</span>
        <span class="doc-gen-timer" id="__doc_gen_timer__">0s</span>
      </div>
      <div class="doc-gen-steps">
        <div class="doc-gen-step active" id="__doc_step_prepare__">📋 准备内容</div>
        <div class="doc-gen-step" id="__doc_step_build__">🏗️ 构建文档</div>
        <div class="doc-gen-step" id="__doc_step_save__">💾 保存下载</div>
      </div>
    </div>`;
    aiMsg.innerHTML = formatOutput(chatRawText) + progressHtml;
    scrollChatBottom();
    // 启动秒级倒计时
    let elapsed = 0;
    if (window.__docGenTimer) clearInterval(window.__docGenTimer);
    window.__docGenTimer = setInterval(() => {
      elapsed++;
      const timerEl = document.getElementById('__doc_gen_timer__');
      if (timerEl) timerEl.textContent = elapsed + 's';
      else clearInterval(window.__docGenTimer);
    }, 1000);
  };

  // 注册文档生成步骤更新回调
  window.__updateDocGenStep = (step) => {
    const steps = ['prepare', 'build', 'save'];
    steps.forEach(s => {
      const el = document.getElementById('__doc_step_' + s + '__');
      if (el) {
        el.classList.remove('active', 'done');
        if (s === step) el.classList.add('active');
        else if (steps.indexOf(s) < steps.indexOf(step)) el.classList.add('done');
      }
    });
  };

  // 注册文档生成完成回调
  window.__showDocGenDone = (info) => {
    if (window.__docGenTimer) { clearInterval(window.__docGenTimer); window.__docGenTimer = null; }
    // 清理标记文本
    chatRawText = chatRawText.replace(/__DOC_PROGRESS_START__[\s\S]*?__DOC_PROGRESS_END__/g, '');
    chatRawText = chatRawText.replace(/__DOC_DONE_START__[\s\S]*?__DOC_DONE_END__/g, '');
    const doneHtml = `<div class="doc-gen-done">✅ ${escHtml(info)}</div>`;
    aiMsg.innerHTML = formatOutput(chatRawText) + doneHtml;
    scrollChatBottom();
  };

  // 注册文档生成失败回调
  window.__showDocGenError = (errMsg) => {
    if (window.__docGenTimer) { clearInterval(window.__docGenTimer); window.__docGenTimer = null; }
    chatRawText = chatRawText.replace(/__DOC_PROGRESS_START__[\s\S]*?__DOC_PROGRESS_END__/g, '');
    chatRawText = chatRawText.replace(/__DOC_DONE_START__[\s\S]*?__DOC_DONE_END__/g, '');
    const errHtml = `<div class="doc-gen-error">❌ ${escHtml(errMsg)}</div>`;
    aiMsg.innerHTML = formatOutput(chatRawText) + errHtml;
    scrollChatBottom();
  };

  chatCancel = null; // 先清空，sendChatMessage 可能同步抛异常
  
  // 对话模式按需携带页面：明确页面意图自动用，确认拒绝则完全不带。
  const _ctxForChat = resolveChatPageContextForMessage(msg, pageContextMode);

  // 记录用户消息到对话历史
  chatConversationHistory.push({ role: 'user', content: msg });

  try {
    chatCancel = sendChatMessage(
      fullMsg,
      _ctxForChat,
      // onChunk
      (text) => {
        if (!chatFirstChunk) {
          chatFirstChunk = true;
          // 首字到达：移除即时回显气泡，显示真实气泡
          if (quickAckMsg.parentNode) quickAckMsg.parentNode.removeChild(quickAckMsg);
          aiMsg.style.display = '';
        }
        chatRawText += text;
        
        // 清理可能残留的文档生成标记文本（新版使用直接回调，但保留兼容）
        if (chatRawText.includes('__DOC_PROGRESS_START__') || chatRawText.includes('__DOC_DONE_START__')) {
          chatRawText = chatRawText.replace(/__DOC_PROGRESS_START__[\s\S]*?__DOC_PROGRESS_END__/g, '');
          chatRawText = chatRawText.replace(/__DOC_DONE_START__[\s\S]*?__DOC_DONE_END__/g, '');
        }
        
        aiMsg.innerHTML = formatOutput(chatRawText);
        scrollChatBottom();
      },
      // onDone
      () => {
        console.log('[对话] onDone 触发，chatRawText 长度:', chatRawText?.length);
        // 清理文档生成进度计时器
        if (window.__docGenTimer) { clearInterval(window.__docGenTimer); window.__docGenTimer = null; }
        // 清理所有回调
        window.__replaceChatText = null;
        window.__showDocGenProgress = null;
        window.__updateDocGenStep = null;
        window.__showDocGenDone = null;
        window.__showDocGenError = null;
        if (quickAckMsg.parentNode) quickAckMsg.parentNode.removeChild(quickAckMsg);
        aiMsg.style.display = '';
        aiMsg.classList.remove('typing');
        chatBusy = false;
        chatCancel = null;
        const btn = document.getElementById('chatSendBtn');
        btn.textContent = '↑';
        btn.disabled = false;
        
        // 记录AI回复到对话历史
        if (chatRawText && chatRawText.trim()) {
          chatConversationHistory.push({ role: 'assistant', content: chatRawText.trim() });
          // 增量摘要：当消息超过 MAX_FULL_MESSAGES 时，把旧消息压缩进摘要
          compressOldMessages();
        }
        
        // 统一提供 Excel/Word 下载 + 复制全文按钮
        if (chatRawText.trim()) {
          const chatWrap = aiMsg.closest('.chat-msg-wrap');
          appendExportButtons(aiMsg, chatRawText);
          appendInsertButton(chatWrap, chatRawText, '对话');
        }
        // 追问建议始终显示（不依赖回答长度）
        appendFollowupSuggestions(aiMsg, chatRawText);
        // 右下角复制按钮
        if (chatRawText && chatRawText.trim()) {
          appendCopyButton(aiMsg, chatRawText);
        }
      },
      // onError
      (err) => {
        if (quickAckMsg.parentNode) quickAckMsg.parentNode.removeChild(quickAckMsg);
        aiMsg.style.display = '';
        aiMsg.textContent = `❌ 错误：${err}`;
        aiMsg.classList.remove('typing');
        chatBusy = false;
        chatCancel = null;
        const btn = document.getElementById('chatSendBtn');
        btn.textContent = '↑';
        btn.disabled = false;
        // 即使报错也显示追问和复制（方便用户重试）
        if (chatRawText && chatRawText.trim()) {
          appendFollowupSuggestions(aiMsg, chatRawText);
          appendCopyButton(aiMsg, chatRawText);
        }
      },
      // priorContext：携带 Skill 分析结果作为对话背景
      lastSkillContext,
      // conversationHistory：使用增量摘要+滑动窗口构建的上下文
      buildConversationContext()
    );
  } catch (e) {
    // sendChatMessage 同步抛出异常（如扩展上下文失效）
    if (quickAckMsg.parentNode) quickAckMsg.parentNode.removeChild(quickAckMsg);
    aiMsg.style.display = '';
    aiMsg.textContent = `❌ 错误：${e.message || e}`;
    aiMsg.classList.remove('typing');
    chatBusy = false;
    chatCancel = null;
    const btn = document.getElementById('chatSendBtn');
    btn.textContent = '↑';
    btn.disabled = false;
  }
}

/**
 * 向聊天区追加一条消息气泡
 * @param {string} text  初始文字
 * @param {string} className  'user' | 'ai' | 'ai typing' | 'ai analysis typing' 等
 * @returns {HTMLElement} 气泡内容区（用于流式更新 innerHTML）
 */
function appendChatMsg(text, className, options = {}) {
  const history = document.getElementById('chatHistory');
  const wrap = document.createElement('div');
  wrap.className = 'chat-msg-wrap ' + (className.includes('user') ? 'user' : 'ai');

  const bubble = document.createElement('div');
  bubble.className = 'chat-msg ' + className;
  if (text) bubble.textContent = text;
  wrap.appendChild(bubble);

  // AI 消息：添加操作按钮（下载），hover后显示
  if (!className.includes('user')) {
    const actions = document.createElement('div');
    actions.className = 'chat-msg-actions';
    
    // 下载按钮（如果有可下载内容）
    if (options.downloadable) {
      const downloadBtn = document.createElement('button');
      downloadBtn.className = 'chat-action-btn chat-download-btn';
      downloadBtn.title = '下载内容';
      downloadBtn.innerHTML = '💾';
      downloadBtn.addEventListener('click', (e) => {
        e.stopPropagation();
        downloadContent(
          options.downloadContent || htmlToPlainText(bubble.innerHTML || bubble.textContent || ''),
          options.downloadFilename || 'content.txt',
          options.downloadType || 'text/plain'
        );
      });
      actions.appendChild(downloadBtn);
    }
    
    wrap.appendChild(actions);
    
    // 存储下载选项供后续使用
    wrap.dataset.downloadable = options.downloadable ? 'true' : 'false';
    wrap.dataset.downloadContent = options.downloadContent || '';
    wrap.dataset.downloadFilename = options.downloadFilename || 'content.txt';
  }

  history.appendChild(wrap);
  scrollChatBottom();
  return bubble; // 返回气泡本体，便于流式更新
}

/**
 * 显示临时提示
 */
function showTooltip(element, text) {
  const tooltip = document.createElement('div');
  tooltip.className = 'chat-tooltip';
  tooltip.textContent = text;
  tooltip.style.cssText = `
    position: absolute;
    background: rgba(0,0,0,0.8);
    color: white;
    padding: 4px 8px;
    border-radius: 4px;
    font-size: 12px;
    pointer-events: none;
    z-index: 1000;
    white-space: nowrap;
  `;
  
  const rect = element.getBoundingClientRect();
  tooltip.style.left = rect.left + 'px';
  tooltip.style.top = (rect.top - 30) + 'px';
  
  document.body.appendChild(tooltip);
  setTimeout(() => tooltip.remove(), 1500);
}

/**
 * 下载内容
 */
function downloadContent(content, filename, type) {
  const blob = new Blob([content], { type });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = filename;
  document.body.appendChild(a);
  a.click();
  document.body.removeChild(a);
  URL.revokeObjectURL(url);
}

function scrollChatBottom() {
  const h = document.getElementById('chatHistory');
  h.scrollTop = h.scrollHeight;
}

function clearChat() {
  document.getElementById('chatHistory').innerHTML = '';
  // 清空对话历史记忆
  chatConversationHistory = [];
chatHistorySummary = '';
  // 同步重置所有运行时状态，避免残留导致死锁
  lastAnalysisText = '';
  lastSkillContext = null;
  analysisCancel = null;
  chatCancel = null;
  chatBusy = false;
  // 清理分析等待计时器（若 clearChat 时分析仍在跑）
  if (analysiswaitTimer) { clearInterval(analysiswaitTimer); analysiswaitTimer = null; }
  // 若预热正在跑，标记结束（不清缓存，让下次分析可复用）
  preheatRunning = false;
  // 清理残留的等待提示元素
  document.querySelectorAll('.chat-msg[data-waitid="analysis-wait"]').forEach(el => el.remove());
  // 重置语音状态
  if (_voiceActive) { stopVoiceInput(true); }
  _voiceBaseText = '';
  _voiceInterimText = '';
  document.getElementById('chatSendBtn').disabled = false;
  // 预热缓存不清空（下次分析仍可复用）
  resetAnalyzeBtn();
  const fillBtn = document.getElementById('fillBtn');
  if (fillBtn) fillBtn.style.display = 'none';
  // 恢复空状态占位
  const emptyState = document.getElementById('emptyState');
  if (emptyState) emptyState.style.display = 'flex';
}

// ==============================
// Skill 列表加载（填充浮层数据 + 抽屉列表）
// ==============================
async function loadSkillListToSelect() {
  // 加载内置 skill 列表
  const builtinSkills = await loadBuiltinSkillList();
  const allSkills = builtinSkills;

  // 存到浮层数据
  _allPopupSkills = allSkills;

  // 同步更新抽屉里的 skill 列表
  _allSkills = allSkills;
  renderSkillList(allSkills);
}

// ==============================
// 加载Skill内容
// ==============================
async function loadSkillContent(skillName) {
  if (!skillName) return null;
  
  // 1. 从自定义安装的 Skill 中查找（chrome.storage.local）
  try {
    if (typeof chrome !== 'undefined' && chrome.storage) {
      const stored = await chrome.storage.local.get(['customSkills']);
      const customSkills = stored.customSkills || {};
      // 先精确匹配 key
      if (customSkills[skillName]) {
        return customSkills[skillName].content;
      }
      // 再按 name 字段模糊匹配
      for (const [key, val] of Object.entries(customSkills)) {
        if (val.name === skillName) {
          return val.content;
        }
      }
    }
  } catch (_) {}

  // 2. 从内置skills目录获取
  try {
    const directResp = await fetch(chrome.runtime.getURL(`skills/${skillName}.md`));
    if (directResp.ok) {
      return await directResp.text();
    }
  } catch (e) {}

  // 如果 skillName 是解析后的 displayName 而非文件名，遍历所有内置 skill 文件查找
  try {
    const allSkillFiles = AISC_BUILTIN_SKILL_FILES;
    for (const filename of allSkillFiles) {
      try {
        const r = await fetch(chrome.runtime.getURL(`skills/${filename}.md`), { signal: AbortSignal.timeout(2000) });
        if (r.ok) {
          const content = await r.text();
          const parsed = parseSkill(content);
          if (parsed.name === skillName) {
            return content;
          }
        }
      } catch (_) {}
    }
  } catch (e) {
    console.warn('[Skill] 从内置目录获取失败:', e);
  }
  
  return null;
}

// ==============================
// 设置面板（全屏覆盖，三个Tab）
// ==============================
let settingsOpen = false;
let currentSettingsTab = 'model';

function toggleSettingsPanel(tab) {
  if (settingsOpen && currentSettingsTab === tab) {
    closeSettingsPanel();
    return;
  }
  const overlay = document.getElementById('settingsOverlay');
  overlay.style.display = 'flex';
  settingsOpen = true;
  switchSettingsTab(tab || 'model');
}

function closeSettingsPanel() {
  const overlay = document.getElementById('settingsOverlay');
  overlay.style.display = 'none';
  settingsOpen = false;
}

function switchSettingsTab(tab) {
  currentSettingsTab = tab;
  ['skills', 'model', 'knowledge'].forEach(t => {
    const tabEl = document.getElementById('stab-' + t);
    const panelEl = document.getElementById('spanel-' + t);
    if (tabEl) tabEl.classList.toggle('active', t === tab);
    if (panelEl) panelEl.style.display = t === tab ? 'block' : 'none';
  });
  if (tab === 'skills') loadSkillList();
  if (tab === 'model') loadSettingsForm();
  if (tab === 'knowledge') loadKnowledgeList();
}

function switchSkillSubTab(sub) {
  ['installed', 'addskill', 'sources'].forEach(s => {
    const tabEl = document.getElementById('subtab-' + s);
    const panelEl = document.getElementById('subpanel-' + s);
    if (tabEl) tabEl.classList.toggle('active', s === sub);
    if (panelEl) panelEl.style.display = s === sub ? 'block' : 'none';
  });
  if (sub === 'installed') loadSkillList();
}

// ==============================
// Skill 管理
// ==============================
let _allSkills = []; // 缓存完整列表，供筛选用

async function loadSkillList() {
  const container = document.getElementById('skillList');
  container.innerHTML = '<div class="skill-list-loading">加载中...</div>';
  // 清空搜索框
  const searchInput = document.getElementById('skillSearchInput');
  if (searchInput) searchInput.value = '';
  try {
    const builtinSkills = await loadBuiltinSkillList();
    if (builtinSkills.length > 0) {
      _allSkills = builtinSkills;
      renderSkillList(builtinSkills);
    } else {
      container.innerHTML = '<div class="skill-list-error">未检测到内置 Skill</div>';
    }
  } catch (e) {
    container.innerHTML = '<div class="skill-list-error">加载 Skill 列表失败</div>';
  }
}

/** 离线时从插件内置 skills/ 目录读取 Skill 列表 */
async function loadBuiltinSkillList() {
  const result = [];
  const knownSkills = AISC_BUILTIN_SKILL_FILES;

  for (const name of knownSkills) {
    try {
      const skillUrl = chrome.runtime.getURL(`skills/${name}.md`);
      const r = await fetch(skillUrl, { signal: AbortSignal.timeout(2000) });
      if (r.ok) {
        const content = await r.text();
        // 解析 frontmatter 获取 name 和 description
        const parsed = parseSkill(content);
        const displayName = parsed.name || name;
        const desc = parsed.description || '内置 Skill';
        result.push({ name: displayName, filename: name, source: 'builtin', description: desc });
      }
    } catch (_) {}
  }

  // 加载用户自定义安装的 Skill（从 chrome.storage.local）
  try {
    if (typeof chrome !== 'undefined' && chrome.storage) {
      const stored = await chrome.storage.local.get(['customSkills']);
      const customSkills = stored.customSkills || {};
      for (const [key, val] of Object.entries(customSkills)) {
        // 避免与内置 skill 重复
        if (!result.find(s => s.name === (val.name || key))) {
          const parsed = parseSkill(val.content || '');
          result.push({
            name: parsed.name || val.name || key,
            filename: key,
            source: 'local',
            description: parsed.description || '用户安装的 Skill',
            customContent: val.content
          });
        }
      }
    }
  } catch (_) {}

  return result;
}

function renderSkillList(skills) {
  const container = document.getElementById('skillList');
  if (skills.length === 0) {
    container.innerHTML = '<div class="skill-list-empty">暂无匹配的 Skill</div>';
    return;
  }
  container.innerHTML = '';
  skills.forEach(s => {
    const badge = s.source === 'remote' ? '🌐 远程' : s.source === 'local' ? '📁 本地' : '📦 内置';
    const badgeClass = s.source === 'remote' ? 'badge-remote' : s.source === 'local' ? 'badge-local' : 'badge-builtin';
    const desc = s.description ? s.description.slice(0, 80) : '';
    const div = document.createElement('div');
    div.className = 'skill-item';
    const info = document.createElement('div');
    info.className = 'skill-item-info';
    const nameEl = document.createElement('div');
    nameEl.className = 'skill-item-name';
    nameEl.textContent = s.name;
    info.appendChild(nameEl);
    if (desc) {
      const descEl = document.createElement('div');
      descEl.className = 'skill-item-desc';
      descEl.textContent = desc;
      info.appendChild(descEl);
    }
    const badgeEl = document.createElement('span');
    badgeEl.className = 'skill-badge ' + badgeClass;
    badgeEl.textContent = badge;
    const useBtn = document.createElement('button');
    useBtn.className = 'btn-use-skill';
    useBtn.textContent = '使用';
    useBtn.addEventListener('click', () => useSkill(s.name));
    div.append(info, badgeEl, useBtn);
    container.appendChild(div);
  });
}

function filterSkillList(keyword) {
  if (!keyword) {
    renderSkillList(_allSkills);
    return;
  }
  const kw = keyword.toLowerCase();
  const filtered = _allSkills.filter(s => s.name.toLowerCase().includes(kw));
  renderSkillList(filtered);
}

function useSkill(name) {
  _selectedSkill = name;
  showSelectedSkillTag(name);
  toggleSkillDrawer();
  showToast(`✅ 已切换到 Skill：${name}`);
}

// ----- 安装 -----
async function installFromUrl() {
  showInstallResult('产品经理版已关闭 URL 安装，请使用内置 Skill 或粘贴内容。', false);
}

async function installFromContent() {
  const name = document.getElementById('installName').value.trim();
  const content = document.getElementById('installContent').value.trim();
  if (!name) { showInstallResult('请填写 Skill 名称', false); return; }
  if (!content) { showInstallResult('请填写 Skill 内容', false); return; }

  showInstallResult('保存中...', null);
  try {
    await saveSkillToLocal(name, content);
    
    showInstallResult('✅ 保存成功: ' + name, true);
    await loadSkillList();
    await loadSkillListToSelect();
    switchSkillSubTab('installed');
    document.getElementById('installName').value = '';
    document.getElementById('installContent').value = '';
  } catch (e) {
    showInstallResult('保存失败: ' + e.message, false);
  }
}

/** 保存 Skill 到 chrome.storage.local */
async function saveSkillToLocal(name, content) {
  if (typeof chrome === 'undefined' || !chrome.storage) {
    throw new Error('存储不可用');
  }
  const stored = await chrome.storage.local.get(['customSkills']);
  const customSkills = stored.customSkills || {};
  customSkills[name] = { name, content, installedAt: Date.now() };
  await chrome.storage.local.set({ customSkills });
}

function showInstallResult(msg, ok) {
  const el = document.getElementById('installResult');
  el.style.display = 'block';
  el.textContent = msg;
  el.className = 'install-result ' + (ok === true ? 'ok' : ok === false ? 'err' : 'pending');
}

// ----- 设置 -----

// 服务商配置表：每个服务商的 API URL（不含 /chat/completions 路径，用于拼接 /models）
const PROVIDERS = {
  aliyun: {
    name: '阿里云百炼',
    baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
    defaultModel: 'qwen-plus',
    fallbackModels: ['qwen-plus', 'qwen-turbo', 'qwen-max', 'qwen-long']
  },
  kimi: {
    name: 'Kimi',
    baseUrl: 'https://api.moonshot.cn/v1',
    defaultModel: 'moonshot-v1-8k',
    fallbackModels: ['moonshot-v1-8k', 'moonshot-v1-32k', 'moonshot-v1-128k']
  },
  deepseek: {
    name: 'DeepSeek',
    baseUrl: 'https://api.deepseek.com/v1',
    defaultModel: 'deepseek-chat',
    fallbackModels: ['deepseek-chat', 'deepseek-reasoner']
  },
  zhipu: {
    name: '智谱',
    baseUrl: 'https://open.bigmodel.cn/api/paas/v4',
    defaultModel: 'glm-4-flash',
    fallbackModels: ['glm-4-flash', 'glm-4-plus', 'glm-4-air', 'glm-4-airx']
  },
  custom: {
    name: '自定义接口',
    baseUrl: '',
    defaultModel: 'gpt-4o-mini',
    fallbackModels: ['gpt-4o-mini', 'gpt-4o', 'gpt-4.1-mini', 'gpt-4.1']
  }
};

function getProviderFallbackModels(provider) {
  const p = PROVIDERS[provider] || PROVIDERS.aliyun;
  return [...new Set([p.defaultModel, ...(p.fallbackModels || [])].filter(Boolean))];
}

function fillModelSelect(models, selectedModel = '') {
  const modelSelect = document.getElementById('settingsModel');
  if (!modelSelect) return '';
  const uniqueModels = [...new Set((models || []).filter(Boolean))];
  if (uniqueModels.length === 0) return '';
  modelSelect.innerHTML = '';
  uniqueModels.forEach(id => {
    const opt = document.createElement('option');
    opt.value = id;
    opt.textContent = id;
    modelSelect.appendChild(opt);
  });
  const selected = selectedModel && uniqueModels.includes(selectedModel) ? selectedModel : uniqueModels[0];
  modelSelect.value = selected;
  return selected;
}

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 normalizeCustomChatUrl(rawUrl) {
  const value = String(rawUrl || '').trim();
  if (!value) throw new Error('请填写自定义接口地址');

  let url;
  try {
    url = new URL(value);
  } catch (_) {
    throw new Error('自定义接口地址格式不正确');
  }

  const isLocalHttp = url.protocol === 'http:' && ['localhost', '127.0.0.1'].includes(url.hostname);
  if (url.protocol !== 'https:' && !isLocalHttp) {
    throw new Error('自定义接口必须使用 HTTPS，本地调试可使用 localhost');
  }

  const normalizedPath = url.pathname.replace(/\/+$/, '');
  if (!normalizedPath.endsWith('/chat/completions')) {
    throw new Error('自定义接口地址需要填写完整的 /chat/completions 地址');
  }

  url.pathname = normalizedPath;
  url.hash = '';
  return url.toString();
}

function getStoredCustomChatUrl(existing = {}) {
  if (existing.custom_api_url) return existing.custom_api_url;
  return existing.provider === 'custom' ? (existing.api_url || existing.aliyun_api_url || '') : '';
}

function getChatCompletionsUrl(provider, customUrl, existing = {}) {
  if (provider === 'custom') {
    return normalizeCustomChatUrl(customUrl || getStoredCustomChatUrl(existing));
  }

  const p = PROVIDERS[provider];
  if (!p || !p.baseUrl) throw new Error('请选择有效的 AI 服务商');
  return p.baseUrl + '/chat/completions';
}

function getModelsUrlFromChatUrl(chatUrl) {
  const url = new URL(chatUrl);
  const normalizedPath = url.pathname.replace(/\/+$/, '');
  url.pathname = normalizedPath.replace(/\/chat\/completions$/, '/models');
  url.search = '';
  url.hash = '';
  return url.toString();
}

async function ensureCustomHostPermission(chatUrl) {
  if (typeof chrome === 'undefined' || !chrome.permissions) return true;

  let url;
  try {
    url = new URL(chatUrl);
  } catch (_) {
    return false;
  }

  const origin = `${url.protocol}//${url.hostname}/*`;
  return new Promise(resolve => {
    chrome.permissions.contains({ origins: [origin] }, hasPermission => {
      if (chrome.runtime?.lastError || hasPermission) {
        resolve(!!hasPermission);
        return;
      }
      chrome.permissions.request({ origins: [origin] }, granted => {
        resolve(!chrome.runtime?.lastError && !!granted);
      });
    });
  });
}

/** 从服务商 API 拉取可用模型列表 */
async function fetchModels() {
  const provider = document.getElementById('settingsProvider')?.value;
  const apiKeyRaw = document.getElementById('settingsApiKey')?.value || '';
  const modelSelect = document.getElementById('settingsModel');
  const fetchBtn = document.getElementById('fetchModelsBtn');
  if (!provider || !modelSelect) return;

  // 获取 API Key：表单输入优先，否则从存储读
  let key = '';
  try {
    key = normalizeApiKeyForHeader(apiKeyRaw);
  } catch (e) {
    showToast(e.message, 'warn');
    return;
  }
  if (!key) {
    try {
      const stored = typeof chrome !== 'undefined' && chrome.storage
        ? await chrome.storage.local.get(['llmSettings'])
        : {};
      key = normalizeApiKeyForHeader(stored.llmSettings?.api_key || stored.llmSettings?.apiKey || stored.llmSettings?.aliyun_api_key || '');
    } catch (_) {}
  }
  if (!key) {
    showToast('请先填入 API Key', 'warn');
    return;
  }

  const p = PROVIDERS[provider];
  if (!p) return;

  // 按钮状态
  if (fetchBtn) { fetchBtn.textContent = '获取中...'; fetchBtn.disabled = true; }

  try {
    const stored = typeof chrome !== 'undefined' && chrome.storage
      ? await chrome.storage.local.get(['llmSettings'])
      : {};
    const existing = stored.llmSettings || {};
    const customUrl = document.getElementById('settingsCustomUrl')?.value.trim() || '';
    const chatUrl = getChatCompletionsUrl(provider, customUrl, existing);
    if (provider === 'custom') {
      const granted = await ensureCustomHostPermission(chatUrl);
      if (!granted) throw new Error('需要授权访问自定义接口域名');
    }
    const modelsUrl = getModelsUrlFromChatUrl(chatUrl);
    const resp = await fetch(modelsUrl, {
      method: 'GET',
      headers: { 'Authorization': 'Bearer ' + key }
    });
    if (!resp.ok) throw new Error('API 返回 ' + resp.status);
    const data = await resp.json();

    // 解析模型列表（OpenAI 兼容格式: { data: [{ id: "model-name" }] }）
    const models = (data.data || data.models || []).map(m => m.id || m.name || m).filter(Boolean);
    // 去重排序
    const uniqueModels = [...new Set(models)].sort();

    if (uniqueModels.length === 0) {
      const fallback = getProviderFallbackModels(provider);
      fillModelSelect(fallback, existing.model || p.defaultModel);
      showToast('未获取到模型列表，已使用内置推荐模型', 'warn');
      return;
    }

    // 填充下拉框
    // 如果有已保存的 model，尝试选中
    const savedModel = (await _getLocalSettings()).model || '';
    fillModelSelect(uniqueModels, savedModel || p.defaultModel);

    showToast('获取到 ' + uniqueModels.length + ' 个模型');
  } catch (e) {
    const fallback = getProviderFallbackModels(provider);
    fillModelSelect(fallback, modelSelect.value || p.defaultModel);
    showToast('获取模型失败，已使用内置推荐模型: ' + e.message, 'warn');
  } finally {
    if (fetchBtn) { fetchBtn.textContent = '获取模型'; fetchBtn.disabled = false; }
  }
}

/** 服务商切换时更新模型提示 */
function updateModelPlaceholder(provider) {
  const modelSelect = document.getElementById('settingsModel');
  if (!modelSelect) return;
  const p = PROVIDERS[provider];
  if (!p) return;
  const current = modelSelect.value;
  if (modelSelect.options.length <= 1 || !current || !getProviderFallbackModels(provider).includes(current)) {
    fillModelSelect(getProviderFallbackModels(provider), current || p.defaultModel);
  }
}

async function loadSettingsForm() {
  try {
    const stored = typeof chrome !== 'undefined' && chrome.storage
      ? await chrome.storage.local.get(['llmSettings'])
      : {};
    const s = stored.llmSettings || {};

    // 服务商 & API Key 配置
    const provider = s.provider || 'aliyun';
    const providerSelect = document.getElementById('settingsProvider');
    if (providerSelect) providerSelect.value = provider;
    const customUrlInput = document.getElementById('settingsCustomUrl');
    if (customUrlInput) customUrlInput.value = getStoredCustomChatUrl(s);
    const customSection = document.getElementById('customUrlSection');
    if (customSection) customSection.style.display = provider === 'custom' ? 'block' : 'none';

    // 先更新模型提示
    updateModelPlaceholder(provider);

    const apiKeyInput = document.getElementById('settingsApiKey');
    const apiKeyStatus = document.getElementById('settingsApiKeyStatus');
    const hasApiKey = !!(s.api_key && s.api_key.trim());
    if (apiKeyInput) {
      apiKeyInput.value = '';
      apiKeyInput.placeholder = hasApiKey ? '已配置（重新输入可覆盖）' : 'sk-xxxx（API Key）';
    }
    if (apiKeyStatus) apiKeyStatus.style.display = hasApiKey ? 'inline' : 'none';

    const modelSelect = document.getElementById('settingsModel');
    if (modelSelect && s.model) {
      // 如果已保存的模型不在列表中，先加进去
      let found = false;
      for (const opt of modelSelect.options) {
        if (opt.value === s.model) { found = true; break; }
      }
      if (!found) {
        const opt = document.createElement('option');
        opt.value = s.model; opt.textContent = s.model;
        modelSelect.insertBefore(opt, modelSelect.firstChild);
      }
      modelSelect.value = s.model;
    }

    // Skill目录
    document.getElementById('settingsDirs').value = (s.skill_dirs || ['~/ai-skills']).join('\n');
    const sApis = Array.isArray(s.remote_skill_apis) ? s.remote_skill_apis : (s.remote_skill_api ? [s.remote_skill_api] : []);
    document.getElementById('settingsRemoteApi').value = sApis.join('\n');
    document.getElementById('settingsRemoteToken').value = s.remote_skill_api_token || '';
    
    // 预热配置
    document.getElementById('settingsPreheatEnabled').checked = !!s.preheat_enabled;
    document.getElementById('settingsPreheatSkill').value = s.preheat_skill || '';
  } catch (e) {}
}

async function saveSettings() {
  // 读取表单值
  const provider = document.getElementById('settingsProvider').value;
  let apiKey = '';
  try {
    apiKey = normalizeApiKeyForHeader(document.getElementById('settingsApiKey').value);
  } catch (e) {
    const el = document.getElementById('modelSettingsResult');
    if (el) {
      el.style.display = 'block';
      el.textContent = '保存失败: ' + e.message;
      el.className = 'install-result err';
    }
    showToast(e.message, 'warn');
    return;
  }
  const p = PROVIDERS[provider] || PROVIDERS.aliyun;
  let model = document.getElementById('settingsModel').value;
  if (!model) {
    model = p.defaultModel || getProviderFallbackModels(provider)[0] || 'qwen-plus';
    fillModelSelect(getProviderFallbackModels(provider), model);
  }
  const dirs = document.getElementById('settingsDirs').value.split('\n').map(s => s.trim()).filter(Boolean);
  const remoteApis = [];
  const remoteToken = '';
  const preheatEnabled = document.getElementById('settingsPreheatEnabled').checked;
  const preheatSkill = document.getElementById('settingsPreheatSkill').value.trim();
  const customUrlInput = document.getElementById('settingsCustomUrl');

  const el = document.getElementById('modelSettingsResult');
  el.style.display = 'block';
  el.textContent = '保存中...';
  el.className = 'install-result pending';

  // 1. 读取已有设置，合并后写入 chrome.storage.local
  let existing = {};
  try {
    if (typeof chrome !== 'undefined' && chrome.storage) {
      const stored = await chrome.storage.local.get(['llmSettings']);
      existing = stored.llmSettings || {};
    }
  } catch (_) {}

  let apiUrl = '';
  let customApiUrl = existing.custom_api_url || '';
  try {
    apiUrl = getChatCompletionsUrl(provider, customUrlInput?.value.trim() || '', existing);
    if (provider === 'custom') {
      const granted = await ensureCustomHostPermission(apiUrl);
      if (!granted) throw new Error('需要授权访问自定义接口域名');
      customApiUrl = apiUrl;
    }
  } catch (e) {
    el.textContent = '保存失败: ' + e.message;
    el.className = 'install-result err';
    return;
  }

  const newSettings = {
    ...existing,
    provider: provider,
    model: model,
    api_url: apiUrl,
    custom_api_url: customApiUrl,
    skill_dirs: dirs,
    remote_skill_apis: remoteApis,
    remote_skill_api_token: remoteToken,
    preheat_enabled: preheatEnabled,
    preheat_skill: preheatSkill
  };

  // 只有用户填写了新 key 才覆盖（避免清空已存 key）
  if (apiKey) {
    newSettings.api_key = apiKey;
  }

  // 兼容：同步写入旧字段，保证 skill_runtime 和旧逻辑正常工作
  newSettings.aliyun_api_key = newSettings.api_key || existing.aliyun_api_key || '';
  newSettings.aliyun_api_url = newSettings.api_url || existing.aliyun_api_url || '';
  newSettings.aliyun_model = newSettings.model || existing.aliyun_model || 'qwen-plus';

  if (typeof chrome !== 'undefined' && chrome.storage) {
    await chrome.storage.local.set({ llmSettings: newSettings });
  }
  // 清除 API 配置缓存，确保下次请求用新配置
  if (typeof _cachedConfig !== 'undefined') { _cachedConfig = null; _configCacheTime = 0; }

  const pName = PROVIDERS[provider]?.name || provider;
  el.textContent = '✅ 设置已保存（' + pName + '），立即生效';
  el.className = 'install-result ok';

  // 更新 API Key 状态显示
  if (apiKey) {
    const keyInput = document.getElementById('settingsApiKey');
    const keyStatus = document.getElementById('settingsApiKeyStatus');
    if (keyInput) { keyInput.value = ''; keyInput.placeholder = '已配置（重新输入可覆盖）'; }
    if (keyStatus) keyStatus.style.display = 'inline';
  }

  // 刷新 Skill 列表
  await loadSkillListToSelect();

  // 更新模型状态卡片
  updateModelStatusCard();
}

// ==============================
// Skill 来源设置保存
// ==============================
async function saveSkillSourcesSettings() {
  const dirs = document.getElementById('settingsDirs').value.split('\n').map(s => s.trim()).filter(Boolean);
  const remoteApis = [];
  const remoteToken = '';
  const preheatEnabled = document.getElementById('settingsPreheatEnabled').checked;
  const preheatSkill = document.getElementById('settingsPreheatSkill').value.trim();

  let existing = {};
  try {
    if (typeof chrome !== 'undefined' && chrome.storage) {
      const stored = await chrome.storage.local.get(['llmSettings']);
      existing = stored.llmSettings || {};
    }
  } catch (_) {}

  const merged = {
    ...existing,
    skill_dirs: dirs,
    remote_skill_apis: remoteApis,
    remote_skill_api_token: remoteToken,
    preheat_enabled: preheatEnabled,
    preheat_skill: preheatSkill
  };

  if (typeof chrome !== 'undefined' && chrome.storage) {
    await chrome.storage.local.set({ llmSettings: merged });
  }

  const el = document.getElementById('skillSettingsResult');
  el.style.display = 'block';
  el.textContent = '✅ Skill 来源设置已保存';
  el.className = 'install-result ok';
}

// ==============================
// 模型状态卡片
// ==============================
function updateModelStatusCard() {
  const badge = document.getElementById('modelStatusBadge');
  const info = document.getElementById('modelCurrentInfo');
  if (!badge || !info) return;

  _getLLMConfig().then(config => {
    if (config.apiKey) {
      badge.textContent = '已连接';
      badge.style.background = '#10b981';
      badge.style.color = '#fff';
      info.textContent = `${config.model || 'qwen-plus'} · ${PROVIDERS[document.getElementById('settingsProvider')?.value]?.name || '自定义'}`;
    } else {
      badge.textContent = '未配置';
      badge.style.background = '#fee2e2';
      badge.style.color = '#dc2626';
      info.textContent = '请在下方配置你的 AI 模型';
    }
  });
}

// ==============================
// 知识库管理 — IndexedDB + LLM 分段
// ==============================

/** 初始化知识库（IndexedDB 无需配置，直接就绪） */
async function initKnowledgeStore() {
  if (!window.knowledgeStore) return;
  await window.knowledgeStore.init();
  loadKnowledgeList();
}

/** 更新知识库状态栏 */
function updateKBStatusBar(segments) {
  const countEl = document.getElementById('kbStatusCount');
  if (countEl) countEl.textContent = segments.length + ' 个片段';
}

/** 设置进度条 */
function setKBProgress(percent, text) {
  const bar = document.getElementById('kbProgress');
  const fill = document.getElementById('kbProgressFill');
  const txt = document.getElementById('kbProgressText');
  if (!bar) return;
  bar.style.display = percent >= 0 ? 'block' : 'none';
  if (fill) fill.style.width = Math.max(0, Math.min(100, percent)) + '%';
  if (txt) txt.textContent = text || '';
}

/** 加载知识库片段列表 */
async function loadKnowledgeList() {
  const kbList = document.getElementById('kbList');
  const kbCount = document.getElementById('kbCount');
  if (!kbList || !window.knowledgeStore) return;

  try {
    const segments = await window.knowledgeStore.listSegments();
    if (kbCount) kbCount.textContent = segments.length + ' 个';
    updateKBStatusBar(segments);

    if (segments.length === 0) {
      kbList.innerHTML = '<div class="kb-empty">暂无知识片段，请上传文件</div>';
      return;
    }

    // 按来源文件分组显示
    const grouped = {};
    segments.forEach(seg => {
      const key = seg.source || '未知来源';
      if (!grouped[key]) grouped[key] = [];
      grouped[key].push(seg);
    });

    kbList.innerHTML = '';
    Object.entries(grouped).forEach(([source, segs]) => {
      const header = document.createElement('div');
      header.className = 'kb-source-header';
      const sourceLabel = document.createElement('span');
      sourceLabel.textContent = getFileIcon(source) + ' ' + source;
      const countLabel = document.createElement('span');
      countLabel.className = 'kb-seg-count';
      countLabel.textContent = segs.length + ' 个片段';
      const deleteSourceBtn = document.createElement('button');
      deleteSourceBtn.className = 'kb-source-delete';
      deleteSourceBtn.dataset.source = source;
      deleteSourceBtn.title = '删除此来源';
      deleteSourceBtn.textContent = '✕';
      header.append(sourceLabel, countLabel, deleteSourceBtn);
      kbList.appendChild(header);

      segs.forEach(seg => {
        const div = document.createElement('div');
        div.className = 'kb-item';
        const icon = document.createElement('div');
        icon.className = 'kb-item-icon';
        icon.textContent = '🔖';
        const info = document.createElement('div');
        info.className = 'kb-item-info';
        const name = document.createElement('div');
        name.className = 'kb-item-name';
        name.textContent = seg.name;
        const desc = document.createElement('div');
        desc.className = 'kb-item-desc';
        desc.textContent = seg.description || '';
        info.append(name, desc);
        const deleteBtn = document.createElement('button');
        deleteBtn.className = 'kb-item-delete';
        deleteBtn.dataset.id = seg.id;
        deleteBtn.title = '删除';
        deleteBtn.textContent = '✕';
        div.append(icon, info, deleteBtn);
        kbList.appendChild(div);
      });
    });

    kbList.querySelectorAll('.kb-item-delete').forEach(btn => {
      btn.addEventListener('click', async () => {
        await window.knowledgeStore.deleteSegment(btn.dataset.id);
        loadKnowledgeList();
      });
    });
    kbList.querySelectorAll('.kb-source-delete').forEach(btn => {
      btn.addEventListener('click', async () => {
        const n = await window.knowledgeStore.deleteBySource(btn.dataset.source);
        showToast('🗑 已删除 ' + n + ' 个片段');
        loadKnowledgeList();
      });
    });
  } catch (e) {
    kbList.innerHTML = '<div class="kb-empty">加载失败：' + escHtml(e.message) + '</div>';
  }
}

function escHtml(s) {
  const d = document.createElement('div');
  d.textContent = s;
  return d.innerHTML;
}

/** 获取文件图标 */
function getFileIcon(filename) {
  const ext = (filename || '').split('.').pop().toLowerCase();
  const icons = { pdf: '📕', docx: '📘', doc: '📘', xlsx: '📗', xls: '📗', csv: '📊', md: '📝', txt: '📄', json: '🔧' };
  return icons[ext] || '📄';
}

/** 清空全部知识库 */
async function clearAllKnowledge() {
  if (!confirm('确定清空全部知识片段？此操作不可恢复。')) return;
  await window.knowledgeStore.clearAll();
  loadKnowledgeList();
  showToast('🗑 知识库已清空');
}

// ===== 文件解析：Word / PDF / Excel / 文本 =====

/** 动态加载 mammoth.js（Word 解析） */
async function _loadMammoth() {
  if (window.mammoth) return;
  return new Promise((resolve, reject) => {
    const script = document.createElement('script');
    script.src = 'libs/mammoth.browser.min.js';
    script.onload = resolve;
    script.onerror = () => reject(new Error('加载 mammoth.js 失败'));
    document.head.appendChild(script);
  });
}

/** 动态加载 pdf.js */
async function _loadPDFJS() {
  if (window.pdfjsLib) return;
  return new Promise((resolve, reject) => {
    const script = document.createElement('script');
    script.src = 'libs/pdf.min.js';
    script.onload = () => {
      window.pdfjsLib.GlobalWorkerOptions.workerSrc = 'libs/pdf.worker.min.js';
      resolve();
    };
    script.onerror = () => reject(new Error('加载 pdf.js 失败'));
    document.head.appendChild(script);
  });
}

/**
 * 解析上传文件为纯文本
 * @param {File} file
 * @returns {Promise<string>}
 */
async function parseFileToText(file) {
  const ext = file.name.split('.').pop().toLowerCase();

  // 文本类
  if (['txt', 'md', 'csv', 'json', 'yaml', 'yml', 'log', 'xml', 'html', 'css', 'js'].includes(ext)) {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.onload = (e) => resolve(e.target.result);
      reader.onerror = reject;
      reader.readAsText(file);
    });
  }

  // Word .docx
  if (ext === 'docx') {
    await _loadMammoth();
    const arrayBuffer = await file.arrayBuffer();
    const result = await mammoth.extractRawText({ arrayBuffer });
    return result.value || '';
  }

  // PDF
  if (ext === 'pdf') {
    await _loadPDFJS();
    const arrayBuffer = await file.arrayBuffer();
    const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
    let text = '';
    for (let i = 1; i <= pdf.numPages; i++) {
      const page = await pdf.getPage(i);
      const content = await page.getTextContent();
      text += content.items.map(item => item.str).join(' ') + '\n';
    }
    return text;
  }

  // Excel .xlsx / .csv
  if (['xlsx', 'xls', 'csv'].includes(ext)) {
    if (!window.XLSX) {
      // 动态加载 XLSX
      await new Promise((resolve, reject) => {
        const script = document.createElement('script');
        script.src = 'libs/xlsx.min.js';
        script.onload = resolve;
        script.onerror = () => reject(new Error('加载 xlsx.js 失败'));
        document.head.appendChild(script);
      });
    }
    const data = await file.arrayBuffer();
    const workbook = XLSX.read(data, { type: 'array' });
    let text = '';
    workbook.SheetNames.forEach(name => {
      const sheet = workbook.Sheets[name];
      text += '--- ' + name + ' ---\n';
      text += XLSX.utils.sheet_to_csv(sheet) + '\n\n';
    });
    return text;
  }

  throw new Error('不支持的文件格式：' + ext);
}

// ===== 智能切分（规则引擎 + 可选LLM摘要） =====

/**
 * 智能文档切分：规则引擎瞬间切分 + 可选LLM摘要增强
 * 参照阿里云百炼"智能切分"策略：标题→段落→句子三级递归，带overlap重叠
 *
 * @param {string} text - 文档全文
 * @param {string} fileName - 来源文件名
 * @param {Function} onProgress - 进度回调 (percent, msg) => void
 * @returns {Promise<Array<{name, description, content}>>}
 */
async function smartSplit(text, fileName, onProgress) {
  if (!text || text.trim().length === 0) return [];

  const baseName = fileName.replace(/\.[^.]+$/, '');

  // 短文本直接作为单片段
  if (text.length < 500) {
    return [{
      name: baseName + '-概要',
      description: text.trim().slice(0, 100),
      content: text.trim()
    }];
  }

  // ===== 阶段1: 规则引擎切分（瞬间完成） =====
  onProgress?.(5, '正在智能切分...');

  const TARGET_MIN = 300;   // 片段最小字数
  const TARGET_MAX = 800;   // 片段最大字数
  const OVERLAP = 100;      // 重叠字数

  // 1. 识别标题边界，按章节分组
  const sections = _splitByHeading(text);

  // 2. 对每个章节按目标大小递归切分
  const rawChunks = [];
  for (const section of sections) {
    if (section.length <= TARGET_MAX) {
      rawChunks.push(section);
    } else {
      // 按段落拆
      const paragraphs = _splitByParagraph(section);
      let buffer = '';
      for (const para of paragraphs) {
        if ((buffer + '\n' + para).length > TARGET_MAX && buffer.length >= TARGET_MIN) {
          rawChunks.push(buffer.trim());
          buffer = para;
        } else {
          buffer = buffer ? buffer + '\n' + para : para;
        }
      }
      if (buffer.trim()) rawChunks.push(buffer.trim());

      // 如果某段仍超长，按句子拆
      const refined = [];
      for (const chunk of rawChunks) {
        if (chunk.length <= TARGET_MAX) {
          refined.push(chunk);
        } else {
          refined.push(..._splitBySentence(chunk, TARGET_MAX));
        }
      }
      rawChunks.length = 0;
      rawChunks.push(...refined);
    }
  }

  // 3. 添加 overlap 重叠
  const overlapped = [];
  for (let i = 0; i < rawChunks.length; i++) {
    let chunk = rawChunks[i];
    // 从前一个片段尾部取 overlap
    if (i > 0) {
      const prev = rawChunks[i - 1];
      const overlapText = prev.length > OVERLAP ? prev.slice(-OVERLAP) : prev;
      // 找最近的句子/段落边界，避免截断
      const boundary = _findSplitBoundary(overlapText);
      const cleanOverlap = boundary > 0 ? overlapText.slice(boundary).trim() : overlapText;
      if (cleanOverlap) chunk = cleanOverlap + '\n' + chunk;
    }
    overlapped.push(chunk);
  }

  // 4. 生成 name 和 description（规则兜底）
  const segments = overlapped.map((content, idx) => {
    const name = _extractTitle(content, baseName, idx);
    const description = content.replace(/\n/g, ' ').slice(0, 100);
    return { name, description, content };
  });

  onProgress?.(50, '规则切分完成，共 ' + segments.length + ' 个片段');

  // ===== 阶段2: LLM摘要增强（可选，并发批处理） =====
  onProgress?.(55, '正在增强片段摘要...');
  await _enhanceSegmentsWithLLM(segments, fileName, onProgress);

  return segments;
}

/**
 * 按标题行识别文档结构，返回各章节文本
 * 识别规则：# 开头、数字编号开头（1. / 1.1 / 第一章）、短行+后续空行
 */
function _splitByHeading(text) {
  const lines = text.split('\n');
  const headingPattern = /^(\s{0,4})(#{1,6}\s|第[一二三四五六七八九十百]+[章节目条]|[0-9]+(\.[0-9]+)*[\s.、）)]|[一二三四五六七八九十]+[\s.、）)])/;
  const sections = [];
  let current = [];
  let prevWasEmpty = false;

  for (let i = 0; i < lines.length; i++) {
    const line = lines[i];
    const trimmed = line.trim();
    const isEmpty = trimmed.length === 0;

    // 明确的标题（#开头 / 数字编号 / 第X章）vs 推测标题（短行+空行）
    const isExplicitHeading = !isEmpty && headingPattern.test(trimmed);
    const isHeuristicHeading = !isEmpty && !isExplicitHeading &&
      prevWasEmpty && trimmed.length < 40 && trimmed.length > 0 &&
      i + 1 < lines.length && lines[i + 1].trim().length === 0;
    const isHeading = isExplicitHeading || isHeuristicHeading;

    if (isHeading && current.length > 0) {
      // 明确标题：总是切分（不管长度）
      // 推测标题：只在累积内容足够长时切分（避免误切）
      const currentText = current.join('\n').trim();
      if (isExplicitHeading || currentText.length > 30) {
        sections.push(currentText);
        current = [line];
      } else {
        current.push(line);
      }
    } else {
      current.push(line);
    }
    prevWasEmpty = isEmpty;
  }
  if (current.join('\n').trim()) {
    sections.push(current.join('\n').trim());
  }
  return sections.length > 0 ? sections : [text];
}

/**
 * 按段落（连续空行或换行）拆分文本
 */
function _splitByParagraph(text) {
  // 按一个或多个空行拆
  const paras = text.split(/\n\s*\n/).map(p => p.trim()).filter(p => p.length > 0);
  if (paras.length <= 1) {
    // 没有空行分隔，按单个换行拆
    return text.split('\n').map(p => p.trim()).filter(p => p.length > 0);
  }
  return paras;
}

/**
 * 按句子边界拆分超长文本，尽量不截断句子
 */
function _splitBySentence(text, maxLen) {
  const result = [];
  let remaining = text;
  while (remaining.length > maxLen) {
    // 在 maxLen 附近找句子边界
    let cutPos = maxLen;
    // 优先找句号、问号、感叹号、换行
    const searchRange = Math.min(200, maxLen);
    for (let i = 0; i < searchRange; i++) {
      const pos = maxLen - i;
      if (pos <= 0) break;
      const ch = remaining[pos];
      if ('。！？\n；'.includes(ch)) {
        cutPos = pos + 1;
        break;
      }
    }
    result.push(remaining.slice(0, cutPos).trim());
    remaining = remaining.slice(cutPos);
  }
  if (remaining.trim()) result.push(remaining.trim());
  return result;
}

/**
 * 在 overlap 文本中找到合适的分割边界（句子/段落结束）
 */
function _findSplitBoundary(text) {
  const boundaries = ['。', '！', '？', '；', '\n', '，', '、', ' '];
  for (const ch of boundaries) {
    const idx = text.indexOf(ch);
    if (idx >= 0 && idx < text.length - 1) return idx + 1;
  }
  return 0;
}

/**
 * 从片段内容提取标题
 */
function _extractTitle(content, baseName, idx) {
  const firstLine = content.split('\n')[0].trim();
  // 如果首行像标题（短行、以#或数字开头）
  if (firstLine.length <= 40 && firstLine.length > 0) {
    return firstLine.replace(/^#+\s*/, '').slice(0, 20);
  }
  // 取第一个完整句子
  const firstSentence = content.match(/[^。！？\n]{2,30}[。！？\n]/);
  if (firstSentence) {
    return firstSentence[0].replace(/[。！？\n]$/, '').slice(0, 20);
  }
  return baseName + '-片段' + (idx + 1);
}

/**
 * LLM摘要增强：并发批量为片段生成更好的 name 和 description
 * 失败不影响已有片段（已有规则生成的兜底值）
 */
async function _enhanceSegmentsWithLLM(segments, fileName, onProgress) {
  if (segments.length === 0) return;
  if (typeof callLLM !== 'function') return;

  const BATCH_SIZE = 10;
  const CONCURRENCY = 3;
  const batches = [];
  for (let i = 0; i < segments.length; i += BATCH_SIZE) {
    batches.push({ startIdx: i, items: segments.slice(i, i + BATCH_SIZE) });
  }

  // 并发池
  let running = 0;
  let batchIdx = 0;
  let completed = 0;
  const totalBatches = batches.length;

  return new Promise((resolve) => {
    function next() {
      while (running < CONCURRENCY && batchIdx < totalBatches) {
        const batch = batches[batchIdx++];
        running++;
        _enhanceOneBatch(batch, fileName)
          .then(() => {
            completed++;
            running--;
            const pct = 55 + Math.round((completed / totalBatches) * 40);
            onProgress?.(pct, '摘要增强 ' + completed + '/' + totalBatches + ' 批...');
            if (completed >= totalBatches) resolve();
            else next();
          })
          .catch((e) => {
            console.warn('[知识库] LLM摘要增强失败（不影响入库）:', e);
            completed++;
            running--;
            const pct = 55 + Math.round((completed / totalBatches) * 40);
            onProgress?.(pct, '摘要增强 ' + completed + '/' + totalBatches + ' 批（部分失败）');
            if (completed >= totalBatches) resolve();
            else next();
          });
      }
      // 如果没有更多batch且没有在运行的，直接结束
      if (totalBatches === 0) resolve();
    }
    next();
  });
}

/**
 * 用LLM为一个批次的片段生成 name 和 description
 */
async function _enhanceOneBatch(batch, fileName) {
  // 构建紧凑的输入：只传 content 前200字 + 序号
  const items = batch.items.map((seg, i) => {
    const preview = seg.content.replace(/\n/g, ' ').slice(0, 200);
    return (batch.startIdx + i + 1) + '. 【' + seg.name + '】' + preview;
  }).join('\n');

  const prompt = '为以下文档片段生成简短标题（10字以内）和摘要（50字以内）。\n' +
    '严格按JSON数组输出：[{"name":"标题","description":"摘要"}]\n' +
    '每个元素对应一个片段，顺序一致。不要输出其他内容。\n\n' +
    '文档来源：' + fileName + '\n片段列表：\n' + items;

  const raw = await callLLM([
    { role: 'system', content: '你是文档摘要专家，严格按JSON数组输出。' },
    { role: 'user', content: prompt }
  ], { temperature: 0.2, maxTokens: 2048 });

  const parsed = _extractJSONArray(raw);
  if (parsed && parsed.length > 0) {
    for (let i = 0; i < Math.min(parsed.length, batch.items.length); i++) {
      if (parsed[i].name && parsed[i].name.trim()) {
        batch.items[i].name = parsed[i].name.trim().slice(0, 20);
      }
      if (parsed[i].description && parsed[i].description.trim()) {
        batch.items[i].description = parsed[i].description.trim().slice(0, 100);
      }
    }
  }
}

/**
 * 从LLM返回文本中提取JSON数组
 */
function _extractJSONArray(text) {
  if (!text) return null;
  let cleaned = text.replace(/```json\s*/gi, '').replace(/```\s*/g, '').trim();
  const start = cleaned.indexOf('[');
  const end = cleaned.lastIndexOf(']');
  if (start < 0 || end <= start) return null;
  const jsonStr = cleaned.slice(start, end + 1);
  try {
    return JSON.parse(jsonStr);
  } catch (e) {
    try {
      return JSON.parse(jsonStr.replace(/,\s*]/g, ']').replace(/,\s*}/g, '}'));
    } catch (_) {
      return null;
    }
  }
}

// ===== 上传处理主流程 =====

/**
 * 处理知识库文件上传：解析 → LLM分段 → 写入本地目录
 */
async function handleKnowledgeUpload(files) {
  if (!files || files.length === 0) return;
  if (!window.knowledgeStore) return;

  // 隐藏之前的结果摘要
  _hideKBResult();

  const totalFiles = files.length;
  const results = []; // 收集每个文件的处理结果
  let hasError = false;
  let hasWarning = false;

  for (let fi = 0; fi < totalFiles; fi++) {
    const file = files[fi];
    try {
      // 第一步：解析文件为文本（10%）
      const filePercent = Math.round((fi / totalFiles) * 100);
      setKBProgress(filePercent, '📄 解析文件：' + file.name + ' (' + (fi + 1) + '/' + totalFiles + ')');

      const text = await parseFileToText(file);
      if (!text || text.trim().length === 0) {
        showToast('⚠️ ' + file.name + ' 内容为空，已跳过', true);
        results.push({ file: file.name, status: 'warn', message: '内容为空，已跳过' });
        hasWarning = true;
        continue;
      }

      // 第二步：智能切分 + 可选LLM摘要增强（10%-95%）
      const segments = await smartSplit(text, file.name, (pct, msg) => {
        const overall = filePercent + Math.round((pct / 100) * (95 - filePercent));
        setKBProgress(overall, msg);
      });

      if (segments.length === 0) {
        showToast('⚠️ ' + file.name + ' 未能生成片段', true);
        results.push({ file: file.name, status: 'warn', message: '未能生成知识片段（文本可能过短或解析失败）' });
        hasWarning = true;
        continue;
      }

      // 第三步：写入本地目录（90%-100%）
      setKBProgress(95, '💾 正在保存 ' + segments.length + ' 个片段到本地...');
      const count = await window.knowledgeStore.writeSegments(segments, file.name);
      setKBProgress(100, '✅ ' + file.name + ' → ' + count + ' 个片段已保存');
      results.push({ file: file.name, status: 'ok', count: count, message: count + ' 个片段已保存' });
    } catch (e) {
      console.error('[知识库] 处理失败:', file.name, e);
      showToast('❌ ' + file.name + ' 处理失败：' + e.message, true);
      results.push({ file: file.name, status: 'error', message: e.message || '未知错误' });
      hasError = true;
    }
  }

  // 完成后刷新列表
  setTimeout(() => {
    setKBProgress(-1, '');
    loadKnowledgeList();

    // 持久显示上传结果摘要
    const okCount = results.filter(r => r.status === 'ok').reduce((sum, r) => sum + r.count, 0);
    const totalCount = results.length;

    let summaryType = 'success';
    let summaryLines = [];

    if (hasError) {
      summaryType = 'error';
      summaryLines.push('📤 上传处理完成（有错误）');
    } else if (hasWarning) {
      summaryType = 'warn';
      summaryLines.push('📤 上传处理完成（有警告）');
    } else {
      summaryLines.push('📤 上传处理完成');
    }

    summaryLines.push('共处理 ' + totalCount + ' 个文件，成功入库 ' + okCount + ' 个片段');
    summaryLines.push('');

    results.forEach(r => {
      if (r.status === 'ok') {
        summaryLines.push('✅ ' + r.file + ' → ' + r.message);
      } else if (r.status === 'warn') {
        summaryLines.push('⚠️ ' + r.file + ' → ' + r.message);
      } else {
        summaryLines.push('❌ ' + r.file + ' → ' + r.message);
      }
    });

    _showKBResult(summaryType, summaryLines.join('\n'));

    window.knowledgeStore.getStats().then(s => {
      showToast('✅ 知识库已更新，共 ' + s.total + ' 个片段');
    });
  }, 800);
}

/** 显示知识库上传结果摘要 */
function _showKBResult(type, html) {
  const box = document.getElementById('kbResultSummary');
  const content = document.getElementById('kbResultSummaryContent');
  if (!box || !content) return;
  box.className = 'kb-result-summary kb-result-' + type;
  box.style.display = 'block';
  content.textContent = html;
}

/** 隐藏知识库上传结果摘要 */
function _hideKBResult() {
  const box = document.getElementById('kbResultSummary');
  if (box) box.style.display = 'none';
}

// ===== 搜索（给对话时使用） =====

/**
 * 检索知识库 —— 基于本地文件的关键词搜索
 * 由 bridge_client.js 的 sendChatMessage() 调用
 */
async function searchKnowledge(query, maxResults = 3) {
  if (!query || !window.knowledgeStore) return [];
  try {
    const results = await window.knowledgeStore.search(query, maxResults);
    return results.map(r => ({
      filename: r.source,
      description: r.description,
      content: r.content
    }));
  } catch (_) {
    return [];
  }
}

// ==============================
// 服务商切换 → 显示/隐藏自定义URL
// ==============================
function setupProviderSwitch() {
  const providerSelect = document.getElementById('settingsProvider');
  if (!providerSelect) return;
  providerSelect.addEventListener('change', () => {
    const customSection = document.getElementById('customUrlSection');
    if (customSection) customSection.style.display = providerSelect.value === 'custom' ? 'block' : 'none';
  });
}

// ==============================
// 层2.5：全选复制补充页面文本
// 比截图更轻量，比DOM解析更完整，用于 quality=poor 时的自动增强
// ==============================
async function enrichContextBySelectCopy() {
  if (typeof chrome === 'undefined' || !chrome.tabs) return;

  const dot = document.getElementById('contextDot');
  const text = document.getElementById('contextText');

  // 向 content_script 发送全选复制指令
  chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
    if (!tabs[0]) return;
    chrome.tabs.sendMessage(tabs[0].id, { type: 'SELECT_COPY_TEXT' }, (resp) => {
      if (chrome.runtime.lastError || !resp || !resp.ok) {
        // 全选复制失败 → 降级到截图
        captureAndAnalyzeScreenshot(true);
        return;
      }

      const selectedText = resp.text || '';
      if (selectedText.length > 200) {
        // 成功拿到文字，合并进 pageContext
        if (!pageContext) pageContext = {};
        pageContext.selectCopyText = selectedText;
        pageContext.quality = 'fair';
        if (dot) dot.className = 'context-dot active';
        if (text) text.textContent = `全选读取完成（${selectedText.length} 字符）`;
        console.log('[AISC] 页面文本增强成功，字数:', selectedText.length);
      } else {
        // 文字太少 → 继续降级截图
        captureAndAnalyzeScreenshot(true);
      }
    });
  });
}

// ==============================
// 截图兜底（层2数据获取）
// ==============================

/**
 * 截取当前Tab可见区域 → 通过 LLM 多模态 API 识别
 * @param {boolean} silentMode - true时不弹Toast，只更新上下文栏提示
 */
async function captureAndAnalyzeScreenshot(silentMode = false) {
  if (!silentMode) {
    showToast('📸 正在截图并识别页面...', 'info');
  }

  // 更新上下文栏
  const dot = document.getElementById('contextDot');
  const text = document.getElementById('contextText');
  if (dot) dot.className = 'context-dot loading';
  if (text) text.textContent = '截图识别中...';

  // 1. 请求 background 截图
  if (typeof chrome === 'undefined' || !chrome.runtime) {
    if (!silentMode) showToast('⚠️ 截图功能需在插件环境中使用', 'warn');
    return;
  }

  chrome.runtime.sendMessage({ type: 'CAPTURE_SCREENSHOT' }, async (resp) => {
    if (!resp || !resp.ok) {
      if (dot) dot.className = 'context-dot warn';
      if (text) text.textContent = '截图失败：' + (resp?.error || '未知错误');
      if (!silentMode) showToast('⚠️ 截图失败', 'warn');
      return;
    }

    // 2. 通过 LLM 多模态 API 识别截图
    try {
      const config = await _getLLMConfig();
      if (!config.apiKey) {
        if (dot) dot.className = 'context-dot warn';
        if (text) text.textContent = '未配置 API Key，无法进行视觉识别';
        return;
      }

      const response = await fetch(config.apiUrl, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${config.apiKey}`
        },
        body: JSON.stringify({
          model: config.model,
          messages: [{
            role: 'user',
            content: [
              { type: 'text', text: '请从截图中提取当前页面的主要文本、表格、价格、产品、评价、新闻、按钮和关键信号，作为产品管理上下文。以结构化文本方式输出。' },
              { type: 'image_url', image_url: { url: resp.dataUrl } }
            ]
          }],
          max_tokens: 4096
        })
      });

      if (!response.ok) {
        if (dot) dot.className = 'context-dot warn';
        if (text) text.textContent = '视觉识别请求失败';
        return;
      }

      const data = await response.json();
      const fullText = data.choices?.[0]?.message?.content || '';

      // 3. 将截图识别文本合并进 pageContext
      if (fullText.trim()) {
        if (!pageContext) pageContext = {};
        pageContext.screenshotText = fullText.trim();
        pageContext.quality = 'fair';
        if (dot) dot.className = 'context-dot active';
        if (text) text.textContent = '截图识别完成，已补充页面数据';
        if (!silentMode) showToast('✅ 截图识别完成，数据已补充');
      } else {
        if (dot) dot.className = 'context-dot warn';
        if (text) text.textContent = '截图识别未返回内容';
      }
    } catch (e) {
      console.warn('[panel] 截图识别失败:', e.message);
      if (pageContext && (pageContext.pageText || '').length > 50) {
        updateContextBar(pageContext);
      } else {
        if (dot) dot.className = 'context-dot warn';
        if (text) text.textContent = '读取内容较少，可直接输入问题';
      }
    }
  });
}

// ==============================
// 预热分析
// ==============================
/**
 * 面板打开时在后台静默预跑指定 Skill。
 * 优先级：设置中的 preheat_skill（手动配置）
 * - 结果存入 preheatCache，startAnalysis 命中时直接展示
 */
async function triggerPreheat() {
  try {
    // 等待页面上下文（最多等 5 秒）
    let waited = 0;
    while (!pageContext && waited < 5000) {
      await new Promise(r => setTimeout(r, 300));
      waited += 300;
    }
    if (!pageContext) return;  // 没有页面数据，跳过

    // 确定预热哪个 Skill
    let preheatSkill = '';

    // 手动配置的预热 Skill（来自设置）
    if (!preheatSkill) {
      try {
        if (typeof chrome !== 'undefined' && chrome.storage) {
          const stored = await chrome.storage.local.get(['llmSettings']);
          const s = stored.llmSettings || {};
          if (s.preheat_enabled && s.preheat_skill) {
            preheatSkill = s.preheat_skill.trim();
          }
        }
      } catch (e) {}
    }

    if (!preheatSkill) return;   // 没有需要预热的 Skill
    if (preheatRunning) return;  // 已在跑，不重复
    if (preheatCache && preheatCache.skill === preheatSkill && preheatCache.done) return; // 已有结果

    preheatRunning = true;
    preheatCache = { skill: preheatSkill, text: '', done: false };
    console.log(`[预热] 开始后台分析 Skill：${preheatSkill}`);

    // 直接通过 SkillRuntime 预热（LLM直连）
    (async () => {
      try {
        const skillContent = await loadSkillContent(preheatSkill);
        if (!skillContent) throw new Error('Skill 内容为空');

        const skill = parseSkill(skillContent);
        const settings = await _getLocalSettings();
        const llmConfig = {
          apiKey: settings.api_key || settings.apiKey || settings.aliyun_api_key || '',
          apiUrl: settings.api_url || settings.apiUrl || settings.aliyun_api_url || 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions',
          model: settings.model || settings.aliyun_model || 'qwen-plus'
        };

        if (!llmConfig.apiKey) throw new Error('未配置 API Key，请在设置中填入');

        const runtime = new SkillRuntime({
          llmConfig,
          timeout: 60000,
          onChunk: (text) => { if (preheatCache) preheatCache.text += text; },
          onToolCall: () => {},
          onError: () => {}
        });

        const result = await runtime.execute(skill, {
          pageContext: pageContext || {},
          attachments: []
        }, '请基于当前页面和公开信息做产品管理分析，输出结构化结论和建议动作');

        if (preheatCache) preheatCache.text = result || preheatCache.text;
        if (preheatCache) preheatCache.done = true;
        preheatRunning = false;
        console.log(`[预热] Skill「${preheatSkill}」LLM直连分析完成`);
      } catch (e) {
        console.warn('[预热] SkillRuntime 执行失败，降级 streamAnalysis:', e.message);
        // 降级到 streamAnalysis（直接 LLM API 调用）
        streamAnalysis(
          { type: 'analyze', skill: preheatSkill, context: pageContext || {}, message: '请基于当前页面和公开信息做产品管理分析，输出结构化结论和建议动作' },
          (text) => { if (preheatCache) preheatCache.text += text; },
          () => { if (preheatCache) preheatCache.done = true; preheatRunning = false; console.log(`[预热] Skill「${preheatSkill}」备用直连分析完成`); },
          (err) => { preheatCache = null; preheatRunning = false; console.warn(`[预热] 分析失败（${err}）`); }
        );
      }
    })();
  } catch (e) {
    preheatRunning = false;
    console.warn('[预热] triggerPreheat 异常：', e);
  }
}

// ==============================
// Toast 提示
// ==============================
let toastTimer = null;
function showToast(msg, type = 'info') {
  let toast = document.getElementById('toast');
  if (!toast) {
    toast = document.createElement('div');
    toast.id = 'toast';
    document.body.appendChild(toast);
  }
  toast.textContent = msg;
  toast.className = 'toast show ' + type;
  clearTimeout(toastTimer);
  toastTimer = setTimeout(() => toast.classList.remove('show'), 2500);
}

// ==============================
// 工具调用区块折叠切换
// ==============================
function toggleToolBlock(blockId) {
  const block = document.getElementById(blockId);
  if (!block) return;
  
  const isExpanded = block.classList.contains('expanded');
  if (isExpanded) {
    block.classList.remove('expanded');
    block.classList.add('collapsed');
  } else {
    block.classList.add('expanded');
    block.classList.remove('collapsed');
  }
}

// ==============================
// 图片粘贴 (Ctrl+V)
// ==============================
function handlePasteImage(e) {
  const items = e.clipboardData && e.clipboardData.items;
  if (!items) return;
  for (const item of items) {
    if (item.type && item.type.startsWith('image/')) {
      e.preventDefault();
      const file = item.getAsFile();
      if (!file) continue;
      const reader = new FileReader();
      reader.onload = (ev) => {
        const name = `粘贴图片_${Date.now()}.${item.type.split('/')[1] || 'png'}`;
        pendingAttachments.push({
          name,
          type: item.type,
          size: file.size,
          content: null,
          dataUrl: ev.target.result
        });
        renderAttachments();
        showToast('📷 图片已添加，发送后 AI 将解析图片内容');
      };
      reader.readAsDataURL(file);
      return;
    }
  }
}

// ==============================
// 提取图片附件数组（多模态发送用）
// ==============================
function extractImageAttachments() {
  return pendingAttachments
    .filter(f => f.type && f.type.startsWith('image/') && f.dataUrl)
    .map(f => ({ name: f.name, dataUrl: f.dataUrl, type: f.type }));
}

// ==============================
// 表格检测 & Excel/Word 下载
// ==============================
function hasMarkdownTable(text) {
  return /^\|.+\|/m.test(text);
}

function appendExportButtons(bubbleEl, rawText) {
  // 已改为：不再每次显示所有下载按钮
  // 只在气泡右下角添加一个低调的 Word 导出图标（hover 显示）
  if (!bubbleEl) return;
  if (bubbleEl.querySelector('.doc-export-btn')) return;
  // 内容太短不需要导出
  if (!rawText || rawText.length < 200) return;

  const btn = document.createElement('button');
  btn.className = 'doc-export-btn';
  btn.title = '导出为 Word 文档';
  btn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>';
  btn.addEventListener('click', (e) => {
    e.stopPropagation();
    exportToWord(rawText);
  });
  bubbleEl.appendChild(btn);
}

// ==============================
// 追问建议按钮
// ==============================

/**
 * 根据 AI 回答内容自动生成追问建议按钮
 * 纯前端实现，不依赖 AI 输出特定格式
 */
function appendFollowupSuggestions(bubbleEl, rawText) {
  if (!rawText || rawText.trim().length < 5) {
    console.log('[追问] 跳过：文本过短', rawText?.length);
    return;
  }

  const wrap = bubbleEl.closest ? bubbleEl.closest('.chat-msg-wrap') : bubbleEl.parentNode;
  if (!wrap) {
    console.log('[追问] 跳过：找不到 wrap 容器');
    return;
  }

  // 移除旧的追问按钮（如果有的话）
  const oldSuggestions = wrap.querySelector('.followup-suggestions');
  if (oldSuggestions) oldSuggestions.remove();

  // 根据回答内容智能生成追问建议
  const suggestions = generateFollowupSuggestions(rawText);
  if (suggestions.length === 0) {
    console.log('[追问] 跳过：无建议生成');
    return;
  }
  console.log('[追问] 生成建议：', suggestions);

  const container = document.createElement('div');
  container.className = 'followup-suggestions';

  suggestions.forEach(text => {
    const btn = document.createElement('button');
    btn.className = 'followup-btn';
    btn.textContent = text;
    btn.addEventListener('click', (e) => {
      e.stopPropagation();
      container.remove();
      submitFollowup(text);
    });
    container.appendChild(btn);
  });

  wrap.appendChild(container);
  scrollChatBottom();
}

/**
 * 根据回答内容生成追问建议（基于内容话题提取 + 关键词匹配）
 */
function generateFollowupSuggestions(text) {
  const lower = text.toLowerCase();
  const suggestions = [];

  // 1. 从内容中提取核心话题，生成针对性追问
  const topics = extractTopicsFromText(text);
  
  if (topics.length > 0) {
    // 用提取到的话题生成追问
    topics.forEach(topic => {
      if (suggestions.length >= 3) return;
      // 从几个追问模板中选一个
      const templates = [
        `关于${topic}，能展开讲讲吗`,
        `${topic}在实际中怎么落地`,
        `${topic}有哪些常见的坑`,
        `帮我深入分析一下${topic}`,
        `${topic}的最佳实践是什么`,
      ];
      // 随机选一个不重复的
      const shuffled = templates.sort(() => Math.random() - 0.5);
      for (const t of shuffled) {
        if (!suggestions.includes(t)) {
          suggestions.push(t);
          break;
        }
      }
    });
  }

  // 2. 基于内容类型的追问
  if (/表格|数据|指标|统计|报表|对比|排名/.test(lower) && suggestions.length < 3) {
    suggestions.push('📊 导出为 Excel 表格');
  }
  if (/流程|步骤|架构|框架|节点/.test(lower) && suggestions.length < 3) {
    suggestions.push('🔗 帮我画一个流程图');
  }
  if (/问题|风险|注意|缺点|不足|隐患/.test(lower) && suggestions.length < 3) {
    suggestions.push('针对这些风险给出具体应对措施');
  }
  if (/汇报|演示|领导|老板|管理层/.test(lower) && suggestions.length < 3) {
    suggestions.push('📊 帮我生成一份 PPT 汇报材料');
  }

  // 3. 内容较长时（>500字），建议导出Word（低频但有价值）
  if (text.length > 500 && suggestions.length < 3) {
    suggestions.push('📄 帮我导出为 Word 文档');
  }

  // 4. 兜底：只加一个通用追问
  if (suggestions.length < 2) {
    suggestions.push('还有其他需要注意的吗');
  }

  // 最多4个
  return suggestions.slice(0, 4);
}

/**
 * 从 AI 回复文本中提取核心话题
 * 识别：标题、加粗关键词、括号内的概念等
 */
function extractTopicsFromText(text) {
  const topics = [];
  const seen = new Set();
  
  // 提取 Markdown 标题中的关键词
  const headings = text.match(/^#{1,4}\s+(.+)$/gm) || [];
  headings.forEach(h => {
    const clean = h.replace(/^#+\s+/, '').replace(/\*+/g, '').trim();
    // 取标题中的核心词（去掉"的""了""是"等虚词）
    const words = clean.split(/[，、：；\s|]/).filter(w => 
      w.length >= 2 && w.length <= 10 && 
      !/^(的|了|是|在|和|与|及|等|为|从|到|由|以|对|于|中|上|下|不|有|无|个|一|二|三|四|五|如何|什么|怎么|为什么|哪些|哪些)$/.test(w)
    );
    words.forEach(w => {
      if (!seen.has(w) && topics.length < 5) {
        seen.add(w);
        topics.push(w);
      }
    });
  });
  
  // 提取加粗关键词
  const boldItems = text.match(/\*\*([^*]+)\*\*/g) || [];
  boldItems.forEach(b => {
    const clean = b.replace(/\*\*/g, '').trim();
    if (clean.length >= 2 && clean.length <= 15 && !seen.has(clean) && topics.length < 5) {
      seen.add(clean);
      topics.push(clean);
    }
  });
  
  // 提取「」或《》中的概念
  const bracketItems = text.match(/[《「」『』【】]([^》「」『』【】]{2,15})[》「」『』【】]/g) || [];
  bracketItems.forEach(b => {
    const clean = b.replace(/[《「」『』【】]/g, '').trim();
    if (!seen.has(clean) && topics.length < 5) {
      seen.add(clean);
      topics.push(clean);
    }
  });

  // 提取关键词短语（如"XX管理"、"XX体系"、"XX流程"、"XX优化"等）
  const keywordPatterns = text.match(/[\u4e00-\u9fa5]{2,6}(?:管理|体系|流程|优化|模型|方法|策略|机制|标准|规范|框架|原则|指标|要素|阶段|步骤|工具|技术|模式|能力)/g) || [];
  keywordPatterns.forEach(k => {
    if (!seen.has(k) && topics.length < 5) {
      seen.add(k);
      topics.push(k);
    }
  });

  return topics;
}

/**
 * 提交追问（复用对话发送逻辑）
 */
function submitFollowup(text) {
  // 如果当前有任务在执行，不重复提交
  if (chatBusy) {
    showToast('AI 正在回答中，请稍候...', 'warn');
    return;
  }
  // 设置输入框内容并发送
  const chatInput = document.getElementById('chatInput');
  if (chatInput) {
    chatInput.value = text;
  }
  // 直接触发发送
  sendChat();
}

// ==============================
// 增量摘要上下文管理（业界最佳实践：Sliding Window + Summarization）
// ==============================

/**
 * 压缩旧消息：超过 MAX_FULL_MESSAGES 的部分提取关键词并入摘要
 * 核心思路：
 * - 最近 4 条消息保留原文（2轮对话的完整上下文）
 * - 更早的消息每条提取 ~80 字要点，追加到摘要中
 * - 摘要缓存，只增量更新新压缩的部分
 */
function compressOldMessages() {
  while (chatConversationHistory.length > MAX_FULL_MESSAGES) {
    // 取出最早的一条
    const oldMsg = chatConversationHistory.shift();
    if (!oldMsg || !oldMsg.content) continue;
    
    const prefix = oldMsg.role === 'user' ? '👤问：' : '🤖答：';
    const compressed = compressSingleMessage(oldMsg.content);
    const entry = prefix + compressed;
    
    // 追加到摘要
    if (chatHistorySummary) {
      chatHistorySummary = chatHistorySummary + '\n' + entry;
    } else {
      chatHistorySummary = entry;
    }
  }
  
  // 摘要过长时二次压缩（保留最近的部分，截掉最早的）
  if (chatHistorySummary.length > MAX_SUMMARY_LENGTH) {
    // 保留后半部分（更新的信息更重要）
    chatHistorySummary = chatHistorySummary.slice(-MAX_SUMMARY_LENGTH);
    // 从第一个完整行开始（避免截断到半句话）
    const firstNewline = chatHistorySummary.indexOf('\n');
    if (firstNewline > 0) {
      chatHistorySummary = chatHistorySummary.slice(firstNewline + 1);
    }
  }
}

/**
 * 压缩单条消息：提取核心内容
 * 策略：
 * 1. 取第一句话（通常是最重要的结论/问题）
 * 2. 提取 Markdown 标题
 * 3. 提取加粗关键词
 * 4. 限制在 MAX_SINGLE_MSG_IN_SUMMARY 字内
 */
function compressSingleMessage(text) {
  if (!text) return '';
  if (text.length <= MAX_SINGLE_MSG_IN_SUMMARY) return text;
  
  const parts = [];
  
  // 1. 提取 Markdown 标题（最重要的结构）
  const headings = text.match(/^#{1,4}\s+(.+)$/gm) || [];
  const headingTexts = headings.map(h => h.replace(/^#+\s+/, '').trim()).filter(h => h.length > 1);
  if (headingTexts.length > 0) {
    parts.push(headingTexts.slice(0, 3).join('、'));
  }
  
  // 2. 提取加粗关键词
  const boldItems = text.match(/\*\*([^*]{2,20})\*\*/g) || [];
  const boldTexts = boldItems.map(b => b.replace(/\*\*/g, '').trim());
  if (boldTexts.length > 0) {
    parts.push('[' + boldTexts.slice(0, 4).join(',') + ']');
  }
  
  // 3. 取文本开头（结论/问题）
  const cleanText = text.replace(/#{1,4}\s+.+$/gm, '').replace(/\*\*[^*]+\*\*/g, '').replace(/```[\s\S]*?```/g, '').trim();
  const firstSentence = cleanText.match(/^[^。\n！？]{2,60}[。！？]?/) || [];
  if (firstSentence[0] && firstSentence[0].length > 5) {
    parts.unshift(firstSentence[0]);
  }
  
  let result = parts.join(' | ');
  if (result.length > MAX_SINGLE_MSG_IN_SUMMARY) {
    result = result.slice(0, MAX_SINGLE_MSG_IN_SUMMARY - 2) + '…';
  }
  
  return result || text.slice(0, MAX_SINGLE_MSG_IN_SUMMARY - 2) + '…';
}

/**
 * 构建发送给 LLM 的对话上下文（摘要 + 最近完整消息）
 * 替代原来把全部历史塞入 prompt 的方式
 */
function buildConversationContext() {
  const context = [];
  
  // 如果有摘要，作为一条 user 消息注入（避免某些 API 不支持多条 system 消息）
  if (chatHistorySummary && chatHistorySummary.trim()) {
    context.push({
      role: 'user',
      content: `[之前的对话摘要，请记住这些信息：]\n${chatHistorySummary}`
    });
    // 加一条 assistant 确认，让对话交替更自然
    context.push({
      role: 'assistant',
      content: '好的，我记住了之前的对话内容。请继续。'
    });
  }
  
  // 最近的完整消息（排除最后一条，因为当前用户消息已通过 finalMsg 传入）
  const historyToSend = chatConversationHistory.slice(0, -1);
  historyToSend.forEach(msg => {
    // 每条消息截断到合理长度
    const content = msg.content.length > 1500 
      ? msg.content.slice(0, 1500) + '\n...[已截断]' 
      : msg.content;
    context.push({ role: msg.role, content });
  });
  
  return context;
}

// ==============================
// 右下角复制按钮（输出完成后出现）
// ==============================

/**
 * 在 AI 消息右下角追加一个小图标复制按钮
 * 复制的是渲染后的纯文本（非 Markdown 源码）
 */
function appendCopyButton(bubbleEl, rawText) {
  // 直接挂在气泡元素上（position: relative），按钮定位到气泡右下角
  if (!bubbleEl) return;
  // 防重复
  if (bubbleEl.querySelector('.msg-copy-btn')) return;

  const btn = document.createElement('button');
  btn.className = 'msg-copy-btn';
  btn.title = '复制内容';
  btn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
  btn.addEventListener('click', (e) => {
    e.stopPropagation();
    // 从 bubbleEl 的 innerHTML 转纯文本（保留视觉格式，不是 Markdown）
    const plain = htmlToPlainText(bubbleEl.innerHTML || bubbleEl.textContent || '');
    navigator.clipboard.writeText(plain).then(() => {
      btn.classList.add('copied');
      showToast('已复制');
      setTimeout(() => btn.classList.remove('copied'), 1500);
    }).catch(() => {
      const ta = document.createElement('textarea');
      ta.value = plain;
      document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta);
      btn.classList.add('copied');
      showToast('已复制');
      setTimeout(() => btn.classList.remove('copied'), 1500);
    });
  });
  bubbleEl.appendChild(btn);
}

function exportToExcel(text) {
  if (typeof XLSX === 'undefined') {
    showToast('⚠️ Excel 库未加载，请检查 libs/xlsx.min.js', 'warn');
    return;
  }
  const tables = parseMarkdownTables(text);
  const wb = XLSX.utils.book_new();
  const filename = 'AI输出_' + new Date().toLocaleDateString('zh-CN').replace(/\//g, '-') + '.xlsx';
  if (tables.length) {
    tables.forEach((table, idx) => {
      const ws = XLSX.utils.aoa_to_sheet(table);
      XLSX.utils.book_append_sheet(wb, ws, 'Sheet' + (idx + 1));
    });
    XLSX.writeFile(wb, filename);
    showToast('✅ Excel 已下载');
  } else {
    // 无表格：将全文每行作为一个 Sheet 行导出
    const lines = text.split('\n').map(function(line) { return [line]; });
    const ws = XLSX.utils.aoa_to_sheet(lines);
    XLSX.utils.book_append_sheet(wb, ws, 'AI输出');
    XLSX.writeFile(wb, filename);
    showToast('✅ Excel 已下载（全文）');
  }
}

function exportToWord(text) {
  // 先处理 Markdown 表格为 HTML 表格（在转义之前处理）
  let html = text
    .replace(/\*\*(.*?)\*\*/g, '<b>$1</b>')
    .replace(/^#{1,4}\s+(.+)$/gm, '<h3>$1</h3>')
    .replace(/\n/g, '<br>');

  // 处理 Markdown 表格（在 HTML 转义之前）
  html = html.replace(/(\|.+\|\s*<br>)+/g, function(matched) {
    const rows = matched.split('<br>').filter(function(r) { return r.trim() && r.includes('|'); });
    var tableHtml = '<table border="1" cellpadding="4" style="border-collapse:collapse;width:100%;">';
    rows.forEach(function(row, i) {
      if (/^\|[\s\-:|]+\|$/.test(row.trim())) return;
      const cells = row.split('|').filter(function(_, ci, arr) { return ci > 0 && ci < arr.length - 1; });
      const tag = i === 0 ? 'th' : 'td';
      // 对单元格内容进行 HTML 转义
      tableHtml += '<tr>' + cells.map(function(c) { return '<' + tag + ' style="padding:4px 8px;">' + c.trim().replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;') + '</' + tag + '>'; }).join('') + '</tr>';
    });
    tableHtml += '</table>';
    return tableHtml;
  });

  // 最后对非表格区域做 HTML 转义（保护特殊字符）
  // 注意：此时表格已经是 HTML 了，不能整体转义
  // 只转义非标签部分的 & 字符
  html = html.replace(/&(?!(?:amp|lt|gt|nbsp|quot|#[0-9]+);)/g, '&amp;');
  html = html.replace(/<(?!\/?(?:b|h3|table|tr|th|td|br|strong)(?:\s[^>]*)?\/?>)/g, '&lt;');

  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:"微软雅黑",Arial,sans-serif;font-size:12pt;line-height:1.8;padding:20px;}table{border-collapse:collapse;width:100%;margin:12px 0;}th,td{border:1px solid #999;padding:6px 10px;}th{background:#f0f4ff;font-weight:bold;}h3{color:#1f5bbf;margin-top:16px;}</style></head><body>' + html + '</body></html>';

  const blob = new Blob(['\ufeff' + fullHtml], { type: 'application/msword;charset=utf-8' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = 'AI输出_' + new Date().toLocaleDateString('zh-CN').replace(/\//g, '-') + '.doc';
  document.body.appendChild(a);
  a.click();
  document.body.removeChild(a);
  URL.revokeObjectURL(url);
  showToast('✅ Word 已下载');
}

function exportToPpt(text) {
  // 按 ## 或 --- 分割为幻灯片
  const slidesRaw = text.split(/\n---\n|\n(?=##\s)/);
  const slides = slidesRaw.filter(s => s.trim());
  
  if (slides.length === 0) {
    // 如果没有分割标记，按段落分页（每3段一页）
    const paragraphs = text.split(/\n\n+/).filter(p => p.trim());
    while (paragraphs.length > 0) {
      slides.push(paragraphs.splice(0, 3).join('\n\n'));
    }
  }

  const slidesHtml = slides.map((slide, i) => {
    let slideHtml = slide
      .replace(/^####\s+(.+)$/gm, '<h4>$1</h4>')
      .replace(/^###\s+(.+)$/gm, '<h3>$1</h3>')
      .replace(/^##\s+(.+)$/gm, '<h2>$1</h2>')
      .replace(/^#\s+(.+)$/gm, '<h1>$1</h1>')
      .replace(/\*\*(.+?)\*\*/g, '<b>$1</b>')
      .replace(/\*(.+?)\*/g, '<i>$1</i>')
      .replace(/^[-*]\s+(.+)$/gm, '<li>$1</li>');
    
    // 包裹列表
    slideHtml = slideHtml.replace(/((?:<li>[\s\S]*?<\/li>\s*)+)/g, '<ul>$1</ul>');
    // Mermaid 代码块保留
    slideHtml = slideHtml.replace(/```mermaid\s*\n([\s\S]*?)```/g, '<pre style="background:#f5f5f5;padding:8px;border-radius:4px;font-size:12px;text-align:left;">$1</pre>');
    slideHtml = slideHtml.replace(/```\w*\s*\n([\s\S]*?```)/g, '<pre style="background:#f5f5f5;padding:8px;border-radius:4px;font-size:12px;text-align:left;">$1</pre>');
    slideHtml = slideHtml.replace(/\n/g, '<br>');

    const isCover = i === 0;
    const bg = isCover ? '#1a1a2e' : '#ffffff';
    const color = isCover ? '#ffffff' : '#333333';
    const accentColor = '#6366f1';

    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','Microsoft YaHei',Arial,sans-serif;page-break-after:always;">
      <div style="max-width:900px;text-align:center;line-height:1.8;font-size:22px;">
        ${slideHtml}
      </div>
      <div style="position:absolute;bottom:20px;right:30px;font-size:12px;color:#aaa;">${i + 1} / ${slides.length}</div>
    </div>`;
  }).join('\n');

  const fullHtml = `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>AI演示文稿</title>
<style>
*{margin:0;padding:0;box-sizing:border-box;}
body{overflow:hidden;}
.slide{display:none !important;}
.slide.active{display:flex !important;}
h1{font-size:42px;margin-bottom:20px;font-weight:700;}
h2{font-size:32px;margin-bottom:16px;color:${'#6366f1'};}
h3{font-size:26px;margin-bottom:12px;color:${'#6366f1'};}
h4{font-size:22px;margin-bottom:10px;}
li{text-align:left;margin:6px 0;font-size:20px;line-height:1.6;}
ul{list-style:none;padding:0;}
ul li::before{content:"▸ ";color:${'#6366f1'};font-weight:bold;}
b{color:${'#6366f1'};}
pre{white-space:pre-wrap;word-break:break-all;}
</style>
</head>
<body>
${slidesHtml}
<script>
const slides=document.querySelectorAll('.slide');
let cur=0;
if(slides.length>0){
  slides[0].classList.add('active');
  document.addEventListener('keydown',e=>{
    slides[cur].classList.remove('active');
    if(e.key==='ArrowRight'||e.key===' '||e.key==='Enter')cur=Math.min(cur+1,slides.length-1);
    else if(e.key==='ArrowLeft'||e.key==='Backspace')cur=Math.max(cur-1,0);
    else return;
    slides[cur].classList.add('active');
    e.preventDefault();
  });
  let tx=0;
  document.addEventListener('touchstart',e=>tx=e.touches[0].clientX);
  document.addEventListener('touchend',e=>{
    const d=e.changedTouches[0].clientX-tx;
    if(Math.abs(d)>50){slides[cur].classList.remove('active');cur=d<0?Math.min(cur+1,slides.length-1):Math.max(cur-1,0);slides[cur].classList.add('active');}
  });
}
<\/script>
</body></html>`;

  const blob = new Blob([fullHtml], { type: 'text/html;charset=utf-8' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = 'AI演示文稿_' + new Date().toLocaleDateString('zh-CN').replace(/\//g, '-') + '.html';
  document.body.appendChild(a);
  a.click();
  document.body.removeChild(a);
  URL.revokeObjectURL(url);
  showToast('✅ PPT 已下载（HTML 幻灯片，浏览器全屏演示）');
}

function parseMarkdownTables(text) {
  const results = [];
  const lines = text.split('\n');
  let currentTable = null;
  for (const line of lines) {
    const trimmed = line.trim();
    if (!trimmed.startsWith('|')) {
      if (currentTable && currentTable.length > 1) results.push(currentTable);
      currentTable = null;
      continue;
    }
    if (/^\|[\s\-:|]+\|$/.test(trimmed)) continue;
    const cells = trimmed.split('|').filter(function(_, i, arr) { return i > 0 && i < arr.length - 1; }).map(function(c) { return c.trim(); });
    if (cells.length) {
      if (!currentTable) currentTable = [];
      currentTable.push(cells);
    }
  }
  if (currentTable && currentTable.length > 1) results.push(currentTable);
  return results;
}

// ==============================
// 复制全文功能（所有 Skill 结果通用）
// ==============================

/**
 * 给 AI 回复区块追加操作按钮（已精简，复制和导出改为右下角半透明图标）
 * 保留此函数以兼容调用处，但不再生成左下角的"复制全文"按钮
 */
function appendInsertButton(wrapEl, rawText, skillName) {
  // 已移除左下角"📋 复制全文"按钮，复制功能由右下角半透明图标承担
  // 此函数保留为空，避免调用处报错
}

// ==============================
// 语音输入模块
// ==============================
let _voiceActive = false;       // 是否录音中
let _voicePaused = false;       // 是否暂停中
let _voiceBaseText = '';        // 已确认的文字（累积）
let _voiceInterimText = '';     // 当前中间结果

function initVoiceRecognition() {
  // 语音结果通过 chrome.runtime.onMessage 接收，避免页面 postMessage 伪造。
}

function handleVoiceMessage(msg) {
  if (!msg) return;
  console.log('[voice-panel] 收到语音消息:', JSON.stringify(msg).slice(0, 200));

  // VOICE_RESULT: 有 finalChunk 或 interim 字段
  if (msg.finalChunk !== undefined || msg.interim !== undefined) {
    if (msg.finalChunk) {
      _voiceBaseText += msg.finalChunk;
      _voiceInterimText = '';
      console.log('[voice-panel] finalChunk 累积:', _voiceBaseText.slice(-50));
    } else {
      _voiceInterimText = msg.interim || '';
      console.log('[voice-panel] interim:', _voiceInterimText.slice(-50));
    }
    const input = document.getElementById('chatInput');
    if (input) {
      input.value = _voiceBaseText + _voiceInterimText;
      autoResizeChat(input);
    }
    return;
  }

  // VOICE_ERROR: 有 error 字段
  if (msg.error !== undefined) {
    console.error('[voice-panel] 收到错误:', msg.error, msg.detail || '');
    if (msg.error === 'not-supported') {
      showToast('当前浏览器不支持语音识别');
      const btn = document.getElementById('chatVoiceBtn');
      if (btn) btn.style.display = 'none';
    } else if (msg.error === 'not-allowed') {
      showToast('⚠️ 麦克风被拒绝，请在系统设置中允许 Chrome 使用麦克风');
    } else if (msg.error === 'no-speech-repeated') {
      showToast('⚠️ ' + (msg.detail || '麦克风未检测到语音，请检查麦克风和网络'), 'warn');
    } else if (msg.error === 'network') {
      showToast('⚠️ ' + (msg.detail || '无法连接语音识别服务，请检查网络'), 'warn');
    } else if (msg.error === 'audio-capture') {
      showToast('⚠️ ' + (msg.detail || '无法捕获音频，请检查麦克风权限'), 'warn');
    } else if (msg.error === 'start-failed') {
      showToast('⚠️ ' + (msg.detail || '语音识别启动失败'), 'warn');
    } else {
      showToast('语音识别出错: ' + msg.error);
    }
    _setVoiceState(false, false);
    return;
  }

  // VOICE_STOPPED
  if (msg.voiceStopped || msg.type === 'VOICE_STOPPED') {
    console.log('[voice-panel] 语音已停止');
    _setVoiceState(false, false);
  }
}

function toggleVoiceInput() {
  if (_voiceActive) {
    // 录音中点击 = 直接发送（sendChat 内部会自动停录音+清空语音变量）
    sendChat();
  } else {
    startVoiceInput();
  }
}

function startVoiceInput() {
  _voiceBaseText = document.getElementById('chatInput').value;
  _voiceInterimText = '';
  _voicePaused = false;
  _voiceActive = true;
  chrome.runtime.sendMessage({ type: 'VOICE_START' }, () => { void chrome.runtime.lastError; });
  _setVoiceState(true, false);
}

function pauseVoiceInput() {
  if (!_voiceActive) return;
  if (_voicePaused) {
    // 继续
    _voicePaused = false;
    chrome.runtime.sendMessage({ type: 'VOICE_RESUME' }, () => { void chrome.runtime.lastError; });
    _setVoiceState(true, false);
  } else {
    // 暂停：先把中间结果并入
    if (_voiceInterimText) {
      _voiceBaseText += _voiceInterimText;
      _voiceInterimText = '';
      document.getElementById('chatInput').value = _voiceBaseText;
    }
    _voicePaused = true;
    chrome.runtime.sendMessage({ type: 'VOICE_PAUSE' }, () => { void chrome.runtime.lastError; });
    _setVoiceState(true, true);
  }
}

// silent=true 时不更新输入框（发送时调用，避免把语音文字重新写回已清空的输入框）
function stopVoiceInput(silent = false) {
  _voiceActive = false;
  _voicePaused = false;
  if (!silent && _voiceInterimText) {
    _voiceBaseText += _voiceInterimText;
    _voiceInterimText = '';
    const input = document.getElementById('chatInput');
    input.value = _voiceBaseText;
    autoResizeChat(input);
  }
  _voiceInterimText = '';
  chrome.runtime.sendMessage({ type: 'VOICE_STOP' }, () => { void chrome.runtime.lastError; });
  _setVoiceState(false, false);
}

function _setVoiceState(active, paused) {
  _voiceActive = active;
  _voicePaused = paused;

  const btn = document.getElementById('chatVoiceBtn');
  if (!btn) return;

  if (active) {
    btn.classList.add('recording');
    btn.classList.toggle('paused', paused);
    btn.textContent = '↑';       // 录音中变成发送键
    btn.title = '发送（停止录音并提交）';
  } else {
    btn.classList.remove('recording', 'paused');
    btn.textContent = '🎤';      // 恢复麦克风图标
    btn.title = '语音输入';
  }
}
