// ==============================
// 状态
// ==============================
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生成",
  "需求评审",
  "技术方案评审",
  "运营方案制定",
  "HTML生成",
  "公众号写作",
  "前端项目生成",
  "本地文件Agent",
  "浏览器操作Agent",
  "接口侦察Agent",
  "Agent执行工作台",
  "Skill运行工作台",
  "Skill生成",
  "Skill评测",
  "流程图构建",
  "流程文件构建",
  "用户故事拆解",
  "需求优先级排序"
];

// ---- 对话历史记忆（同一会话内多轮对话保持上下文） ----
let chatConversationHistory = []; // [{role:'user'|'assistant', content:'...'}]
let chatHistorySummary = ''; // 增量摘要：旧消息的压缩版本
let chatArtifacts = []; // 最近生成的交付物，供后续追问记忆使用
const MAX_FULL_MESSAGES = 6; // 最近保留6条完整消息（约3轮对话）
const MAX_SUMMARY_LENGTH = 100000; // 滚动摘要最大长度，面向长上下文模型
const MAX_SINGLE_MSG_IN_SUMMARY = 1200; // 每条旧消息压缩后的最大长度
const MAX_RECENT_MESSAGE_CHARS = 10000; // 最近完整消息单条上限
const MAX_HISTORY_CONTEXT_CHARS = 120000; // 发给模型的历史上下文总预算

// ---- 当前选中的 Skill（替代原 skillSelect 下拉框） ----
let _selectedSkill = '';
let _allPopupSkills = [];      // 浮层用的 skill 列表数据
let _pendingSkillClarification = null; // { skillName, skillLabel, originalMsg, answers, round, attachments, pageContextDecision }
let _pageContextDecisionOverride = null;
let _activeShortcutMode = null; // url | html，快捷操作等待用户补充输入
let _shortcutSkillInstruction = null; // 快捷 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('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-automation', 'click', () => switchSettingsTab('automation'));
  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('chatSendBtn', 'click', sendChat);
  safeBind('chatVoiceBtn', 'click', toggleVoiceInput);
  safeBind('agentLoopClear', 'click', clearShortcutMode);
  document.addEventListener('click', (e) => {
    if (!e.target.closest?.('#agentLoopClear')) return;
    clearShortcutMode();
  }, true);
  document.addEventListener('click', (e) => {
    const openBtn = e.target.closest('.artifact-open-btn');
    const downloadBtn = e.target.closest('.artifact-download-btn');
    const publishBtn = e.target.closest('.artifact-publish-btn');
    const publicBtn = e.target.closest('.artifact-public-btn');
    if (!openBtn && !downloadBtn && !publishBtn && !publicBtn) return;
    const id = (openBtn || downloadBtn || publishBtn || publicBtn).dataset.artifactId;
    if (openBtn) openArtifact(id);
    if (downloadBtn) downloadArtifact(id);
    if (publishBtn) publishArtifact(id, publishBtn);
    if (publicBtn) openPublicArtifact(id);
  });
  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);
  });
  document.addEventListener('click', (e) => {
    const copyBtn = e.target.closest('.deploy-copy-btn');
    if (!copyBtn) return;
    copyToClipboard(copyBtn.dataset.url || '');
    copyBtn.textContent = '已复制';
    setTimeout(() => { copyBtn.textContent = '复制'; }, 1200);
  });
  document.addEventListener('click', (e) => {
    const promptBtn = e.target.closest('.empty-prompt');
    if (!promptBtn) return;
    e.preventDefault();
    handleEmptyShortcut(promptBtn.dataset.action, promptBtn);
  });
  document.addEventListener('keydown', (e) => {
    if (!e.altKey || e.ctrlKey || e.metaKey || e.shiftKey || e.isComposing) return;
    const keyMap = { '1': 'summarize-page', '2': 'parse-url', '3': 'generate-html' };
    const action = keyMap[e.key];
    if (!action) return;
    e.preventDefault();
    handleEmptyShortcut(action);
  });
  const skillSearchInput = document.getElementById('skillSearchInput');
  if (skillSearchInput) skillSearchInput.addEventListener('input', function() {
    filterSkillList(this.value.trim());
  });
  const chatInput = document.getElementById('chatInput');
  if (chatInput) {
    chatInput.addEventListener('keydown', handleChatKey);
    chatInput.addEventListener('input', () => {
      autoResizeChat(chatInput);
      handleSkillPopup(chatInput);
    });
    chatInput.addEventListener('paste', handleAttachmentPaste);
  }
  const chatInputArea = document.querySelector('.chat-input-area');
  if (chatInputArea) {
    chatInputArea.addEventListener('dragenter', handleAttachmentDragEnter);
    chatInputArea.addEventListener('dragover', handleAttachmentDragOver);
    chatInputArea.addEventListener('dragleave', handleAttachmentDragLeave);
    chatInputArea.addEventListener('drop', handleAttachmentDrop);
  }
  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);
  safeBind('automationRefreshBtn', 'click', loadAutomationPanel);
  safeBind('automationQueueRefreshBtn', 'click', loadAutomationQueue);
  safeBind('automationQueueProcessBtn', 'click', processAutomationQueue);
  safeBind('agentAuditRefreshBtn', 'click', loadAgentAuditList);
  safeBind('automationSaveBtn', 'click', saveAutomationFromForm);
  safeBind('automationRunDraftBtn', 'click', runAutomationDraft);
  safeBind('automationResetBtn', 'click', resetAutomationForm);
  safeBind('savePolicyBtn', 'click', saveAgentPolicySettings);
  safeBind('resetPolicyBtn', 'click', resetAgentPolicySettings);
  safeBind('runSkillBenchmarkBtn', 'click', runSkillBenchmark);
  safeBind('automationTriggerType', 'change', updateAutomationTriggerVisibility);
  safeBind('automationSchedulePreset', 'change', updateAutomationScheduleVisibility);
  safeBind('automationIntervalUnit', 'change', updateAutomationScheduleVisibility);
  ['automationTime', 'automationMonthDay', 'automationIntervalValue', 'automationIntervalUnit'].forEach(id => {
    safeBind(id, 'input', syncAutomationScheduleToCron);
    safeBind(id, 'change', syncAutomationScheduleToCron);
  });
  document.querySelectorAll('.automation-weekday-btn').forEach(btn => {
    btn.addEventListener('click', () => {
      btn.classList.toggle('active');
      syncAutomationScheduleToCron();
    });
  });

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

  initVoiceRecognition();
  await loadSkillListToSelect();
  await loadAgentPolicySettings();

  // 每次打开插件都是全新状态：清空对话、清空上下文、强制读取当前页面
  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(urlOverride = '') {
  const input = document.getElementById('urlContextInput');
  const btn = document.getElementById('urlContextBtn');
  const dot = document.getElementById('contextDot');
  const text = document.getElementById('contextText');
  const url = normalizeUrlInput(urlOverride || input?.value);

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

  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('✅ 网页已解析');
    }
    return true;
  } catch (e) {
    if (dot) dot.className = 'context-dot error';
    if (text) text.textContent = '网页解析失败';
    showToast('⚠️ ' + (e.message || '网页解析失败'), 'warn');
    return false;
  } 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 = _shortcutSkillInstruction || chatInput?.value?.trim() || '';

  // ===== 页面上下文注入决策 =====
  // 重置确认结果。追问恢复时可复用上一轮页面选择，避免重复确认。
  const forcedPageDecision = _pageContextDecisionOverride;
  _pageContextDecisionOverride = null;
  _pageContextDecision = forcedPageDecision || null;

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

  const pageReady = pageAvailable && hasReadablePageContext(pageContext);

  if (forcedPageDecision) {
    _pageContextDecision = forcedPageDecision;
  } else if (pageReady && shouldAutoUsePageContext(userMsg, {
    hasAttachments,
    fullMsg: userMsg
  })) {
    // 用户显式或隐式指向当前页时，直接把页面交给 Skill，不再打断确认。
    _pageContextDecision = pageContext;
  } else {
    _pageContextDecision = {};
  }

  if (maybeAskSkillClarificationBeforeRun(skillName, userMsg, {
    attachments: pendingAttachments || [],
    pageContext: _pageContextDecision || {}
  })) {
    _shortcutSkillInstruction = null;
    return;
  }

  // 直接执行（场景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);
  thinkingBlock.style.display = 'none';
  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 = '';
  let conclusionDraftText = '';
  let generatedArtifactThisRun = null;
  const isHtmlGenerationSkill = skillName === 'HTML生成';
  const htmlProgressState = {
    chars: 0,
    startedAt: Date.now(),
    lastRenderAt: 0,
    lastAgentUpdateAt: 0,
    stage: '正在连接模型'
  };
  const markRuntimeStarted = () => {
    if (firstChunkReceived) return;
    firstChunkReceived = true;
    removeWaitMsg();
  };
  const renderHtmlProgress = (stage = '', options = {}) => {
    if (!isHtmlGenerationSkill || generatedArtifactThisRun) return;
    if (stage) htmlProgressState.stage = stage;
    const now = Date.now();
    if (!options.force && now - htmlProgressState.lastRenderAt < 220) return;
    htmlProgressState.lastRenderAt = now;
    markRuntimeStarted();
    const elapsed = Math.max(1, Math.floor((now - htmlProgressState.startedAt) / 1000));
    const chars = Math.max(0, htmlProgressState.chars);
    const sizeText = chars > 0 ? `${chars.toLocaleString('zh-CN')} 字` : '准备中';
    aiWrap.classList.add('html-progress-wrap');
    conclusionBubble.classList.add('html-progress-msg');
    conclusionBubble.innerHTML = `<div class="html-progress-card" role="status" aria-live="polite" title="生成完成后会自动打开预览"><div class="html-progress-head"><span class="html-progress-dot"></span><span class="html-progress-title">${escHtml(htmlProgressState.stage || '正在生成 HTML')}</span><span class="html-progress-meta">${escHtml(sizeText)} · ${elapsed}s · 预览</span></div><div class="html-progress-bar"><span></span></div></div>`;
    if (window.AgentLoop?.setStep && now - htmlProgressState.lastAgentUpdateAt > 900) {
      htmlProgressState.lastAgentUpdateAt = now;
      window.AgentLoop.setStep({
        id: 'execute',
        status: 'in_progress',
        message: `${htmlProgressState.stage || '正在生成 HTML'}${chars ? ` · ${chars.toLocaleString('zh-CN')} 字` : ''}`
      });
    }
    scrollChatBottom();
  };
  const updateHtmlProgress = (delta = 0, stage = '', options = {}) => {
    if (!isHtmlGenerationSkill || generatedArtifactThisRun) return;
    const amount = Number(delta || 0);
    if (Number.isFinite(amount) && amount > 0) {
      if (options.absolute) htmlProgressState.chars = Math.max(htmlProgressState.chars, amount);
      else htmlProgressState.chars += amount;
    }
    renderHtmlProgress(stage, options);
  };
  const renderThinkingText = () => {
    if (!thinkingText.trim()) return;
    thinkingBlock.style.display = '';
    thinkingBlock.classList.add('running');
    thinkingBody.innerHTML = formatOutput(thinkingText);
    thinkingBody.scrollTop = thinkingBody.scrollHeight;
    scrollChatBottom();
  };
  const renderConclusionDraft = () => {
    if (!conclusionDraftText.trim()) return;
    if (isHtmlGenerationSkill && !generatedArtifactThisRun) {
      renderHtmlProgress('正在生成 HTML 源码', { force: true });
      return;
    }
    conclusionBubble.innerHTML = formatOutput(conclusionDraftText);
    scrollChatBottom();
  };
  const routeRuntimeChunk = (text = '') => {
    if (!text) return;
    markRuntimeStarted();
    const rawBlockRe = /\x01[\s\S]*?\x02/g;
    let lastIndex = 0;
    let match;
    while ((match = rawBlockRe.exec(text))) {
      const plain = text.slice(lastIndex, match.index);
      if (plain) {
        conclusionDraftText += plain;
        updateHtmlProgress(plain.length, '正在生成 HTML 源码');
      }
      thinkingText += match[0];
      lastIndex = rawBlockRe.lastIndex;
    }
    const tail = text.slice(lastIndex);
    if (tail) {
      conclusionDraftText += tail;
      updateHtmlProgress(tail.length, '正在生成 HTML 源码');
    }
    renderThinkingText();
    renderConclusionDraft();
  };
  const isBrowserRuntimeTool = (tool) => /^browser\./.test(String(tool || ''));
  const browserToolLabels = {
    'browser.open': ['正在打开页面', '页面已打开'],
    'browser.navigate': ['正在跳转页面', '页面已跳转'],
    'browser.click': ['正在点击页面', '点击已完成'],
    'browser.type': ['正在输入内容', '输入已完成'],
    'browser.fill': ['正在填写页面', '填写已完成'],
    'browser.press': ['正在发送按键', '按键已完成'],
    'browser.observe': ['正在观察页面', '页面元素已识别'],
    'browser.waitFor': ['正在等待页面', '等待已完成'],
    'browser.getText': ['正在读取页面', '页面内容已读取'],
    'browser.screenshot': ['正在截图', '截图已完成'],
    'browser.pdf': ['正在生成 PDF', 'PDF 已生成'],
    'browser.network.start': ['正在启动接口捕获', '接口捕获已启动'],
    'browser.network.get': ['正在读取接口记录', '接口记录已读取'],
    'browser.network.clear': ['正在清空接口记录', '接口记录已清空'],
    'browser.network.stop': ['正在停止接口捕获', '接口捕获已停止'],
    'browser.acceptance.check': ['正在做浏览器验收', '浏览器验收已完成'],
    'browser.listTabs': ['正在读取标签页', '标签页已读取'],
    'browser.close': ['正在关闭标签页', '标签页已关闭']
  };
  const escapeRegExp = (value) => String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  const browserToolSummary = (info = {}) => {
    const result = info.result || {};
    const args = info.args || {};
    const pieces = [];
    if (result.title) pieces.push(result.title);
    if (result.url || args.url) pieces.push(result.url || args.url);
    if (result.tabId) pieces.push(`Tab ${result.tabId}`);
    if (info.tool === 'browser.getText' && typeof result.text === 'string') {
      pieces.push(`已读取 ${result.text.length.toLocaleString('zh-CN')} 字`);
    } else if (info.tool === 'browser.observe' && Array.isArray(result.elements)) {
      pieces.push(`识别 ${result.elements.length.toLocaleString('zh-CN')} 个可操作元素`);
    } else if (info.tool === 'browser.open' && Number.isFinite(Number(result.textLength))) {
      pieces.push(`页面文本约 ${Number(result.textLength).toLocaleString('zh-CN')} 字，按需读取`);
    } else if (Array.isArray(result.entries)) {
      pieces.push(`${result.entries.length} 条记录`);
    } else if (Array.isArray(result.tabs)) {
      pieces.push(`${result.tabs.length} 个标签页`);
    }
    if (result.error) pieces.push(result.error);
    return pieces.filter(Boolean).join(' · ') || '浏览器正在执行';
  };
  const renderBrowserToolProcessCard = (info = {}) => {
    const completed = info.status === 'completed';
    const failed = info.result?.error || info.result?.ok === false;
    const labels = browserToolLabels[info.tool] || ['正在执行浏览器操作', '浏览器操作已完成'];
    const title = failed ? '浏览器操作失败' : (completed ? labels[1] : labels[0]);
    const note = failed ? '请检查页面状态或重试。' : (completed ? '原始页面内容已收起，仅保留必要进度。' : '过程会在这张卡片内更新。');
    return `<div class="tool-result-block browser-tool-card${failed ? ' error' : ''}" id="${escAttr(info.callId)}">
  <div class="tool-result-header browser-tool-header">
    <span class="tool-icon">🌐</span>
    <span class="tool-name">浏览器操作</span>
    <span class="tool-status">${failed ? '✗' : (completed ? '✓' : '...')}</span>
  </div>
  <div class="browser-tool-progress">
    <span class="browser-tool-dot${failed ? ' failed' : ''}"></span>
    <div class="browser-tool-copy">
      <div class="browser-tool-action">${escHtml(title)}</div>
      <div class="browser-tool-meta">${escHtml(browserToolSummary(info))}</div>
      <div class="browser-tool-note">${escHtml(note)}</div>
    </div>
  </div>
</div>`;
  };
  const wrapBrowserToolProcessCard = (info) => `\x01<!--browser-tool:${info.callId}-->${renderBrowserToolProcessCard(info)}<!--/browser-tool:${info.callId}-->\x02`;
  const upsertBrowserToolProcessCard = (info) => {
    markRuntimeStarted();
    const html = wrapBrowserToolProcessCard(info);
    const marker = escapeRegExp(info.callId);
    const pattern = new RegExp(`\\x01<!--browser-tool:${marker}-->[\\s\\S]*?<!--\\/browser-tool:${marker}-->\\x02`);
    thinkingText = pattern.test(thinkingText) ? thinkingText.replace(pattern, html) : thinkingText + html;
    renderThinkingText();
  };
  const renderHtmlArtifactCardInConclusion = (artifact) => {
    if (!artifact) return;
    aiWrap.classList.remove('html-progress-wrap');
    conclusionBubble.classList.remove('html-progress-msg');
    conclusionBubble.classList.add('artifact-msg');
    conclusionBubble.innerHTML = renderArtifactCard(artifact);
    scrollChatBottom();
  };
  const createHtmlArtifactFromActualContent = (content, filename = '') => {
    const artifact = createHtmlArtifactFromContent(content, filename || 'AIPROS页面');
    if (!artifact) return null;
    generatedArtifactThisRun = artifact;
    renderHtmlArtifactCardInConclusion(artifact);
    return artifact;
  };
  const rememberGeneratedHtmlArtifact = (info = {}) => {
    const rawArtifact = info?.artifact || info?.result?.artifact || null;
    if (!rawArtifact) return null;
    const artifact = rememberArtifact(rawArtifact);
    generatedArtifactThisRun = artifact;
    renderHtmlArtifactCardInConclusion(artifact);
    return artifact;
  };

  // 新架构：使用插件内置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,
      autoFinishRun: !isHtmlGenerationSkill,
      onChunk: (text) => {
        routeRuntimeChunk(text);
      },
      onToolCall: (info) => {
        console.log('[Tool Call]', info);
        markRuntimeStarted();
        if (isBrowserRuntimeTool(info.tool)) {
          upsertBrowserToolProcessCard(info);
          return;
        }
        if (info.status === 'streaming') {
          if (isHtmlGenerationSkill && (info.tool === 'generate.html' || info.tool === 'tool_call')) {
            updateHtmlProgress(info.receivedChars || 0, '正在生成 HTML 源码', { absolute: true });
          }
          return;
        }
        if (info.status === 'calling') {
          if (isHtmlGenerationSkill && info.tool === 'generate.html') {
            updateHtmlProgress(0, '正在写入 HTML 文件', { force: true });
          }
          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;
          renderThinkingText();
        } else if (info.status === 'completed') {
          const statusEl = thinkingBody.querySelector(`#tc_${info.callId}`);
          if (statusEl) { statusEl.querySelector('.tool-call-spinner').textContent = '✓'; statusEl.classList.add('done'); }
          if (info.tool === 'generate.html') {
            updateHtmlProgress(0, '正在准备预览', { force: true });
            rememberGeneratedHtmlArtifact(info.result);
          }
          scrollChatBottom();
        }
      }
    });
    
    // 执行Skill：流式过程进沙箱，最终返回值作为结论输出
    // 使用已确认的页面上下文决策结果
    const effectivePageCtx = _pageContextDecision || {};
    const hasAtts = pendingAttachments && pendingAttachments.length > 0;
    const chatInputEl = document.getElementById('chatInput');
  const userInstr = _shortcutSkillInstruction || chatInputEl?.value?.trim() || '';
  _shortcutSkillInstruction = null;
  if (chatInputEl) {
    chatInputEl.value = '';
    autoResizeChat(chatInputEl);
  }
    let analysisHint = hasAtts
      ? `请基于用户提供的附件内容，严格按照任务说明中的步骤执行。${userInstr ? '用户额外需求：' + userInstr : ''}`
      : (userInstr || '请立即按照任务说明中的步骤逐一执行，直接输出执行结果。不要评价或介绍任务说明本身。');
    if (isHtmlGenerationSkill) {
      analysisHint += '\n\n【硬性要求】本任务必须真实调用 generate.html 工具生成并打开 HTML 文件。不要只写说明，不要说“我将生成”。content 必须是完整 HTML 文档；信息不足时在页面内标注“待补充”，但仍要生成真实文件。';
      renderHtmlProgress('正在连接模型', { force: true });
    }

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

    let result = await runtime.execute(skill, {
      pageContext: effectivePageCtx,
      attachments: pendingAttachments || []
    }, analysisHint);

    if (isHtmlGenerationSkill && !generatedArtifactThisRun) {
      updateHtmlProgress(0, '正在整理 HTML 交付物', { force: true });
      const artifactFromHtml = createHtmlArtifactFromActualContent(result || conclusionDraftText, 'AIPROS页面');
      if (!artifactFromHtml) {
        conclusionDraftText = '';
        htmlProgressState.chars = 0;
        updateHtmlProgress(0, '正在重新生成完整 HTML 文件', { force: true });
        const strictRetryHint = `${analysisHint}\n\n【重新执行】上一轮没有产生 HTML 文件，视为失败。现在必须直接调用 generate.html 工具，参数 filename 和 content 必填。除 <tool_call> 外不要输出任何解释。`;
        result = await runtime.execute(skill, {
          pageContext: effectivePageCtx,
          attachments: pendingAttachments || []
        }, strictRetryHint);
        if (!generatedArtifactThisRun) {
          createHtmlArtifactFromActualContent(result || conclusionDraftText, 'AIPROS页面');
        }
      }
      if (!generatedArtifactThisRun) {
        throw new Error('HTML 生成失败：模型没有调用 generate.html，也没有输出完整 HTML；未生成文件。');
      }
      if (window.AgentLoop?.setStep) {
        window.AgentLoop.setStep({ id: 'verify', status: 'completed', message: 'HTML 文件已生成并创建预览' });
        window.AgentLoop.setStep({ id: 'final', status: 'completed', message: generatedArtifactThisRun.filename });
      }
      if (window.AgentLoop?.finishRun) window.AgentLoop.finishRun('completed', 'HTML 生成完成');
    }
    
    // 完成：把最终结论输出到结论气泡
    analysisCancel = null;
    removeWaitMsg();
    aiWrap.classList.remove('html-progress-wrap');
    conclusionBubble.classList.remove('typing');

    // 结论 = execute() 返回的干净文本
    const conclusionText = isHtmlGenerationSkill && generatedArtifactThisRun
      ? `HTML 已生成：${generatedArtifactThisRun.filename}。可在下方交付物卡片打开预览、下载或发布。`
      : (result || conclusionDraftText || thinkingText);
    lastAnalysisText = conclusionText;
    _skillResultAccum = conclusionText;
    if (isHtmlGenerationSkill && generatedArtifactThisRun) {
      renderHtmlArtifactCardInConclusion(generatedArtifactThisRun);
    } else {
      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 = (isHtmlGenerationSkill && generatedArtifactThisRun) ? 'none' : 'inline-flex';
    scrollChatBottom();

    // HTML 交付物本身已有打开/下载/发布按钮，不再追加普通对话的追问和复制控件。
    if (!(isHtmlGenerationSkill && generatedArtifactThisRun)) {
      appendExportButtons(conclusionBubble, conclusionText);
      appendInsertButton(aiWrap, conclusionText, skillName);
      appendFollowupSuggestions(conclusionBubble, conclusionText);
      appendCopyButton(conclusionBubble, conclusionText);
    }
    
  } catch (error) {
    // 错误处理
    analysisCancel = null;
    removeWaitMsg();
    conclusionBubble.classList.remove('typing');
    conclusionBubble.classList.remove('html-progress-msg');
    conclusionBubble.textContent = '';
    const errorSpan = document.createElement('span');
    errorSpan.style.color = 'var(--ui-danger)';
    errorSpan.textContent = '❌ 错误：' + (error?.message || String(error));
    conclusionBubble.appendChild(errorSpan);
    resetAnalyzeBtn();
    if (isHtmlGenerationSkill && window.AgentLoop?.finishRun) {
      window.AgentLoop.finishRun('failed', error?.message || 'HTML 生成失败');
    }
    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();
}

// ==============================
// 一键填写内容到页面输入框
// ==============================
async 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;
  }

  if (typeof chrome === 'undefined' || !chrome.runtime?.sendMessage) {
    showToast('✅ 分析内容已准备好，请手动复制到目标页面');
    return;
  }

  showToast('正在填入当前页面...');

  const result = await fillApprovalTextToPage(opinionText);
  if (result.ok) {
    showToast('✅ 内容已填入，请确认');
    return;
  }

  showToast('⚠️ ' + (result.error || '未找到可填写的文本框'), 'warn');
}

async function fillApprovalTextToPage(opinionText) {
  const contentResp = await automationMessage({
    type: 'SEND_TO_CONTENT',
    payload: {
      type: 'FILL_OPINION',
      text: opinionText,
      silent: true
    }
  });
  const contentResult = contentResp?.data || {};
  if (contentResp?.ok && (contentResult.ok || contentResult.success)) {
    return { ok: true, method: 'content-script', data: contentResult };
  }

  const browserResp = await automationMessage({
    type: 'BROWSER_FILL',
    text: opinionText,
    intent: 'opinion',
    clear: true
  });
  const browserResult = browserResp?.data || {};
  if (browserResp?.ok && (browserResult.ok || browserResult.filled || browserResult.typed || browserResult.success)) {
    return { ok: true, method: 'browser-fill', data: browserResult };
  }

  return {
    ok: false,
    error: browserResult.error || browserResp?.error || contentResult.error || contentResp?.error || '未找到可填写的文本框'
  };
}

function parseApprovalOperationIntent(message) {
  const raw = String(message || '').trim();
  if (!raw) return null;
  const compact = raw.replace(/\s+/g, '');
  const hasApprovalWord = /审批|审核|批复|意见|备注|说明|处理意见|办理意见/.test(compact);
  const hasActionWord = /填|填写|写入|写成|写为|回填|注入|提交|同意|通过|批准|驳回|退回|拒绝|不同意|点一下|点击/.test(compact) ||
    /(?:输入|录入)(?:为|成|同意|通过|批准|驳回|退回|拒绝|不同意|已阅|无异议|[:：])/.test(compact);
  const bareAction = /^(同意|通过|批准|已阅|无异议|驳回|退回|拒绝|不同意)(并)?(提交|提交审批)?$/.test(compact);
  if ((!hasApprovalWord || !hasActionWord) && !bareAction) return null;
  if (/评审|分析|总结|生成流程|流程图|需求|方案|PRD/i.test(raw) && !/填|填写|输入|写入|回填|注入|提交|点/.test(compact) && !bareAction) {
    return null;
  }

  const shouldSubmit = /提交|提交审批|确认提交|点提交|点击提交|并提交|同意并提交|通过并提交|批准并提交|驳回并提交|退回并提交/.test(compact);
  const isReject = /驳回|退回|拒绝|不同意/.test(compact);
  const isApprove = /同意|通过|批准|无异议|已阅/.test(compact);
  const needsAiOpinion = /想|生成|拟|起草|润色|组织|给.*意见|写一段|帮我写/.test(compact) && /意见/.test(compact);
  let opinionText = extractApprovalOpinionText(raw);

  if (!opinionText && !needsAiOpinion) {
    if (isReject) opinionText = '不同意，请退回修改';
    else if (isApprove || bareAction) opinionText = compact.includes('已阅') ? '已阅' : (compact.includes('无异议') ? '无异议' : '同意');
  }

  if (!opinionText && !shouldSubmit && !needsAiOpinion) return null;

  return {
    opinionText,
    shouldFill: !!opinionText || needsAiOpinion,
    shouldSubmit,
    action: isReject ? 'reject' : 'approve',
    needsAiOpinion: needsAiOpinion && !opinionText
  };
}

function extractApprovalOpinionText(raw) {
  const text = String(raw || '').trim();
  const patterns = [
    /(?:审批意见|审核意见|批复意见|处理意见|办理意见|意见|备注|说明)(?:写成|写为|填成|填为|填写为|输入为|写|填|输入|回填|注入)?[：:\s]+([\s\S]+)$/i,
    /(?:写成|写为|填成|填为|填写为|输入为|写|填|输入|回填|注入)[：:\s]*([\s\S]+)$/i
  ];
  for (const pattern of patterns) {
    const match = text.match(pattern);
    if (!match?.[1]) continue;
    const cleaned = cleanApprovalOpinionCandidate(match[1]);
    if (cleaned) return cleaned;
  }

  const shortMatch = text.match(/^(?:帮我|请|麻烦)?(?:填|填写|输入|写入|写)(同意|通过|批准|已阅|无异议|不同意|驳回|退回|拒绝)(?:并提交|提交|提交审批)?$/);
  if (shortMatch?.[1]) return cleanApprovalOpinionCandidate(shortMatch[1]);
  return '';
}

function cleanApprovalOpinionCandidate(value) {
  return String(value || '')
    .replace(/^["'“”‘’]+|["'“”‘’]+$/g, '')
    .replace(/[，,。；;]?\s*(?:并)?(?:帮我|请|麻烦)?(?:点击|点一下|点)?(?:提交审批|确认提交|提交|通过审批)$/g, '')
    .replace(/[，,。；;]?\s*(?:并)?(?:点击|点一下|点)?(?:确认|确定)$/g, '')
    .trim();
}

function buildApprovalAiInstruction(originalMsg, fullMsg, approvalIntent) {
  const submitRule = approvalIntent.shouldSubmit
    ? '用户已明确要求提交：先调用 browser.fill 填入审批意见，再调用 browser.click 点击提交/确认/同意/通过按钮。'
    : '用户没有明确要求提交：只调用 browser.fill 填入审批意见，不要点击提交、确认、同意、通过等按钮。';
  return `【审批操作模式】
用户原话：${originalMsg}

请基于当前页面内容，先拟一段适合审批意见框的简洁意见（通常1-3句话，不要长篇分析），然后执行页面操作。
${submitRule}

工具调用要求：
1. 填写意见必须调用 browser.fill，args 用 {"text":"你拟好的审批意见","clear":true}，selector 可省略。
2. 只有用户明确要求提交时，才继续调用 browser.click，args 用 {"intent":"approval-submit","texts":["提交审批","提交","确认提交","确定","确认","同意","通过","批准"],"waitAfter":800}。
3. 工具执行后，用一句话告诉用户已完成哪些动作；不要输出大段过程。

【用户消息】
${fullMsg}`;
}

async function runApprovalOperation(intent) {
  const aiMsg = appendChatMsg('正在处理审批操作...', 'ai typing');
  const lines = [];
  try {
    if (intent.opinionText) {
      const fillResult = await fillApprovalTextToPage(intent.opinionText);
      if (!fillResult.ok) throw new Error(fillResult.error || '审批意见填入失败');
      lines.push(`已填入审批意见：${intent.opinionText}`);
    }

    if (intent.shouldSubmit) {
      const clickResult = await clickApprovalSubmitButton(intent.action);
      if (!clickResult.ok) throw new Error(clickResult.error || '未找到提交按钮');
      lines.push(`已点击：${clickResult.text || '提交按钮'}`);

      const confirmResult = await clickApprovalConfirmButton();
      if (confirmResult.ok) lines.push(`已确认：${confirmResult.text || '确认按钮'}`);
    }

    if (!lines.length) lines.push('没有识别到需要执行的审批操作');
    aiMsg.classList.remove('typing');
    aiMsg.innerHTML = formatOutput(lines.join('\n'));
    chatConversationHistory.push({ role: 'assistant', content: lines.join('\n') });
    showToast('✅ 审批操作已执行，请确认页面状态');
  } catch (e) {
    aiMsg.classList.remove('typing');
    aiMsg.textContent = '⚠️ ' + (e.message || String(e));
    showToast('⚠️ ' + (e.message || String(e)), 'warn');
  } finally {
    chatBusy = false;
    chatCancel = null;
    const btn = document.getElementById('chatSendBtn');
    if (btn) { btn.textContent = '↑'; btn.disabled = false; }
    scrollChatBottom();
  }
}

async function clickApprovalSubmitButton(action = 'approve') {
  const texts = action === 'reject'
    ? ['驳回', '退回', '拒绝', '不同意', '提交', '确定', '确认']
    : ['提交审批', '提交', '确认提交', '同意', '通过', '批准', '办理', '确定', '确认'];
  const resp = await automationMessage({
    type: 'BROWSER_CLICK',
    intent: action === 'reject' ? 'approval-reject' : 'approval-submit',
    texts,
    waitAfter: 800
  });
  const data = resp?.data || {};
  if (resp?.ok && (data.clicked || data.ok || data.success)) {
    return { ok: true, text: data.target?.text || data.text || texts[0], data };
  }
  return { ok: false, error: data.error || resp?.error || '未找到提交按钮' };
}

async function clickApprovalConfirmButton() {
  const resp = await automationMessage({
    type: 'BROWSER_CLICK',
    intent: 'approval-submit',
    texts: ['确认提交', '确定提交', '确定', '确认'],
    waitAfter: 500
  });
  const data = resp?.data || {};
  if (resp?.ok && (data.clicked || data.ok || data.success)) {
    return { ok: true, text: data.target?.text || data.text || '确认', data };
  }
  return { ok: false, error: data.error || resp?.error || '未出现确认按钮' };
}

// ==============================
// 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}，请补充任务信息`);

  const rule = _SKILL_INPUT_REQUIREMENTS[name];
  if (rule) {
    showSkillClarification(name, '', { ready: false, questions: rule.questions || [] }, {
      round: 1,
      originalMsg: '',
      answers: [],
      attachments: pendingAttachments || [],
      pageContextDecision: {}
    });
  }
}

/** 在输入框上方显示已选 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);
  }
}

function clearSelectedSkillTag() {
  _selectedSkill = '';
  const tag = document.getElementById('selectedSkillTag');
  if (tag) tag.remove();
}

/** 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: "围绕产品上线、增长、活动和用户运营制定目标、机制、渠道、节奏、预算、指标和复盘计划。" },
  "HTML生成": { name: "HTML生成", keywords: ["生成HTML","导出HTML","做个HTML","写HTML","制作HTML","HTML文件","HTML页面","html页面","网页生成","生成网页","落地页","报告页","可视化页面","H5页面","单页网页","预览页"], desc: "把需求、资料、文章或数据生成可下载、可预览的单文件HTML页面，适合报告页、落地页、文章页、仪表盘和轻量工具页。" },
  "公众号写作": { name: "公众号写作", keywords: ["公众号","公众号文章","公众号长文","公众号HTML","写文章","爆款文章","软文","推文","微信文章","每日文章","内容创作"], desc: "根据公开信息、素材或内置范文生成可直接发布的公众号 HTML，适合企业AI、流程管理、Agent落地和数字化转型选题。" },
  "前端项目生成": { name: "前端项目生成", keywords: ["前端项目","React项目","Vue项目","Vite项目","Tailwind项目","工程级","前端工程","生成项目","项目源码","可运行项目","组件库","构建项目","npm run dev","部署到Vercel","部署到Netlify"], desc: "生成可运行的前端工程项目，包含 Vite/React/Vue、样式系统、组件拆分、README、运行脚本和本地落盘。" },
  "本地文件Agent": { name: "本地文件Agent", keywords: ["读文件","写文件","查目录","查名单","搜索文件","整理文件","批量重命名","移动文件","复制文件","删除文件","本地Agent"], desc: "通过 Native Agent 读取、写入、列目录、搜索、复制、移动、删除和整理本地文件。" },
  "浏览器操作Agent": { name: "浏览器操作Agent", keywords: ["打开页面","点击按钮","填写页面","填表单","读取页面","自动操作网页","截图","导出PDF","浏览器Agent"], desc: "通过浏览器自动化打开页面、读取内容、点击、填写、等待、截图和打印 PDF。" },
  "接口侦察Agent": { name: "接口侦察Agent", keywords: ["获取接口信息","分析接口","抓接口","看请求","生成curl","接口清单","OpenAPI","XHR","fetch","页面API"], desc: "从页面、文档、源码或操作线索中整理接口清单、请求参数、返回结构、curl 和 OpenAPI 草稿。" },
  "Agent执行工作台": { name: "Agent执行工作台", keywords: ["Agent执行闭环","任务计划","步骤状态","工具调用日志","失败重试","回滚点","权限确认","执行轨迹","checkpoint","审计回放","浏览器验收"], desc: "为多步骤 Agent 任务创建计划、更新步骤、记录工具调用、处理重试、回滚点、权限确认、审计回放和真实浏览器验收。" },
  "Skill运行工作台": { name: "Skill运行工作台", keywords: ["运行skill","调用skill","列出skill","选择skill","skill工作台","skill路由","本地skill管理","哪个skill"], desc: "列出、读取、路由、校验和组织本地 Skill，帮助用户选择并运行合适能力。" },
  "Skill生成": { name: "Skill生成", keywords: ["生成skill","创建skill","写一个skill","把流程做成skill","优化skill","新增技能","QoderWork技能","Skill Creator"], desc: "把工作流、提示词、SOP 或业务能力生成可复用的 Agent Skill，并支持校验和本地写入。" },
  "Skill评测": { name: "Skill评测", keywords: ["评测skill","审一下skill","skill质量检查","skill打分","测试skill","benchmark skill","trigger测试","skill写得怎么样"], desc: "对 SKILL.md 做结构校验、触发词测试、质量评分和整改建议。" },
  "流程图构建": { 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对需求池排序，输出取舍依据和迭代建议。" }
};

const _SKILL_INPUT_REQUIREMENTS = {
  "PRD生成": {
    minChars: 80,
    minWithContext: 18,
    questions: [
      "要写哪个产品/功能/流程的 PRD？请用一句话说明对象和目标。",
      "目标用户是谁，最核心的使用场景是什么？",
      "这版的一期范围包含哪些核心能力，哪些先不做？"
    ],
    signals: [
      /功能|模块|页面|流程|系统|平台|工具|小程序|app|应用|后台|插件|产品/i,
      /用户|客户|角色|运营|管理员|员工|访客|商家|审核|销售|客服/i,
      /目标|解决|痛点|背景|场景|需求|价值|指标|范围|一期|版本|上线/i
    ]
  },
  "需求评审": {
    minChars: 70,
    minWithContext: 10,
    questions: [
      "要评审哪份需求？请粘贴需求正文、上传文件，或说明基于当前页面。",
      "这次最关心哪类问题：范围、流程、交互、数据权限、验收，还是风险依赖？",
      "评审结论要面向谁使用：自己改稿、研发评审会，还是管理层决策？"
    ],
    signals: [
      /需求|PRD|说明|功能|页面|流程|规则|验收|AC|用户故事/i,
      /评审|检查|漏洞|问题|风险|缺口|优化|建议/i
    ]
  },
  "技术方案评审": {
    minChars: 70,
    minWithContext: 10,
    questions: [
      "要评审哪份技术方案？请粘贴方案正文、上传文件，或说明基于当前页面。",
      "这次重点看什么：架构、接口、数据、安全、性能、成本，还是排期风险？",
      "方案对应的业务目标或需求范围是什么？"
    ],
    signals: [
      /技术方案|架构|接口|API|数据库|模型|服务|系统|部署|链路|方案/i,
      /评审|可行|风险|性能|安全|成本|排期|运维/i
    ]
  },
  "运营方案制定": {
    minChars: 70,
    minWithContext: 18,
    questions: [
      "运营目标是什么：拉新、促活、留存、转化、召回，还是功能上线承接？",
      "目标用户/人群是谁，活动或运营对象是什么？",
      "约束条件有哪些：时间、预算、渠道、资源、KPI？"
    ],
    signals: [
      /拉新|促活|留存|转化|召回|上线|增长|活动|运营|campaign|用户/i,
      /目标|人群|渠道|预算|时间|KPI|指标|资源|节奏/i
    ]
  },
  "HTML生成": {
    minChars: 45,
    minWithContext: 10,
    questions: [
      "要生成哪类 HTML：报告页、落地页、文章页、仪表盘、流程页，还是轻量工具页？",
      "页面内容素材来自哪里：当前页面、附件、粘贴文本，还是你的一句话主题？",
      "有没有风格和使用要求：移动端优先、可打印、一键复制、图表、CTA 或品牌色？"
    ],
    signals: [
      /html|网页|页面|落地页|h5|报告页|可视化|静态页|单页|预览|导出/i,
      /主题|内容|素材|数据|风格|移动端|打印|复制|按钮|图表|卡片|仪表盘|cta/i
    ]
  },
  "公众号写作": {
    minChars: 20,
    minWithContext: 8,
    questions: [
      "这篇公众号想写哪个主题或观点？",
      "目标读者是谁：老板、流程负责人、产品经理、业务负责人，还是普通读者？",
      "想要什么风格：观点锋利、案例拆解、方法论、爆款诊断，还是活动转化？"
    ],
    signals: [
      /公众号|文章|推文|软文|长文|选题|标题|爆款|微信/i,
      /企业AI|流程管理|Agent|数字化|审批|自动化|产品|转型/i
    ]
  },
  "前端项目生成": {
    minChars: 45,
    minWithContext: 10,
    questions: [
      "要生成哪类前端项目：React、Vue、静态站点，还是数据看板/工具型应用？",
      "项目主题、核心页面和目标用户是什么？",
      "希望用什么技术和交付方式：Vite、Tailwind、组件拆分、本地运行、构建或部署？"
    ],
    signals: [
      /前端|项目|工程|源码|react|vue|vite|tailwind|组件|构建|部署|npm|vercel|netlify/i,
      /页面|应用|工具|看板|落地页|网站|路由|状态|交互|表单|图表/i
    ]
  },
  "本地文件Agent": {
    minChars: 20,
    minWithContext: 8,
    questions: [
      "要操作哪个文件或目录？请提供绝对路径或说明位置。",
      "要执行什么动作：读取、搜索、写入、整理、复制、移动还是删除？",
      "如果是批量或破坏性操作，是否只先预演变更清单？"
    ],
    signals: [
      /文件|目录|路径|名单|本地|读取|写入|搜索|整理|复制|移动|删除|重命名/i,
      /txt|md|csv|json|xlsx|docx|pdf|folder|desktop|download/i
    ]
  },
  "浏览器操作Agent": {
    minChars: 20,
    minWithContext: 8,
    questions: [
      "要操作哪个页面或当前页面？",
      "要执行什么动作：读取、点击、填写、截图、导出 PDF，还是提交？",
      "如果涉及提交或确认按钮，是否明确授权提交？"
    ],
    signals: [
      /网页|页面|浏览器|打开|点击|填写|填表|按钮|截图|PDF|提交|读取/i,
      /url|http|https|selector|表单|输入框|审批|登录/i
    ]
  },
  "接口侦察Agent": {
    minChars: 20,
    minWithContext: 8,
    questions: [
      "要侦察哪个页面、接口文档或业务动作？",
      "希望输出接口清单、curl、OpenAPI 草稿，还是调试建议？",
      "是否有本地源码、日志或接口材料可以读取？"
    ],
    signals: [
      /接口|API|请求|XHR|fetch|curl|OpenAPI|参数|返回|抓包|Network/i,
      /页面|按钮|表单|日志|源码|接口文档|后端/i
    ]
  },
  "Agent执行工作台": {
    minChars: 15,
    minWithContext: 8,
    questions: [
      "这次要追踪的是哪个具体任务？",
      "是否会涉及本地文件改动、页面提交或命令执行？",
      "失败时希望自动重试、停止等待确认，还是优先回滚？"
    ],
    signals: [
      /agent|执行闭环|任务计划|步骤状态|执行轨迹|工具调用|日志/i,
      /重试|回滚|checkpoint|权限|确认|审计|计划/i
    ]
  },
  "Skill运行工作台": {
    minChars: 15,
    minWithContext: 8,
    questions: [
      "你想列出、选择、读取、运行还是诊断哪个 Skill？",
      "任务目标是什么，我可以帮你匹配合适 Skill。",
      "是否需要读取本地 skills 目录做路由判断？"
    ],
    signals: [
      /skill|技能|运行|调用|列出|选择|路由|工作台|本地skill/i,
      /哪个|适合|匹配|管理|读取|校验/i
    ]
  },
  "Skill生成": {
    minChars: 25,
    minWithContext: 8,
    questions: [
      "这个 Skill 要让 Agent 做什么？",
      "什么用户说法应该触发它？请给 3 个例子。",
      "期望输出格式和成功标准是什么？"
    ],
    signals: [
      /skill|技能|生成|创建|新增|写一个|做成|SOP|流程|提示词/i,
      /触发|输出格式|工作流|校验|写入|QoderWork/i
    ]
  },
  "Skill评测": {
    minChars: 20,
    minWithContext: 8,
    questions: [
      "要评测哪个 Skill？请提供名称、路径或粘贴 SKILL.md。",
      "有没有正例和反例问法？没有的话我会自动构造。",
      "要只输出报告，还是也帮你生成整改版本？"
    ],
    signals: [
      /skill|技能|评测|测试|打分|质量|trigger|benchmark|审一下|写得怎么样/i,
      /SKILL.md|description|触发词|正例|反例|整改/i
    ]
  },
  "流程图构建": {
    minChars: 50,
    minWithContext: 10,
    questions: [
      "要画哪个流程？请说明流程名称或业务对象。",
      "流程的起点、终点分别是什么？",
      "涉及哪些角色/系统，关键分支有哪些？"
    ],
    signals: [
      /流程|步骤|节点|审批|提交|处理|流转|触发|结束|分支/i,
      /角色|系统|用户|管理员|部门|起点|终点|条件/i
    ]
  },
  "流程文件构建": {
    minChars: 70,
    minWithContext: 12,
    questions: [
      "要写哪个流程/SOP？请说明流程名称和适用范围。",
      "流程的触发事件、结束标志和参与角色是什么？",
      "有没有关键控制点、KPI 或必须遵守的规则？"
    ],
    signals: [
      /流程|SOP|规范|制度|说明|适用范围|触发|结束|角色/i,
      /控制点|KPI|指标|规则|职责|RACI|输入|输出/i
    ]
  },
  "用户故事拆解": {
    minChars: 60,
    minWithContext: 12,
    questions: [
      "要拆解哪个 Epic/大需求？请说明目标或粘贴需求片段。",
      "涉及哪些用户角色，用户要完成什么任务？",
      "希望拆到什么粒度：迭代 backlog、研发估算，还是验收用故事？"
    ],
    signals: [
      /Epic|用户故事|User Story|需求|功能|角色|任务|场景|backlog/i,
      /验收|AC|拆解|估算|迭代|优先级|依赖/i
    ]
  },
  "需求优先级排序": {
    minChars: 70,
    minWithContext: 12,
    questions: [
      "请提供要排序的需求列表，至少包含需求名称或简述。",
      "排序目标是什么：冲刺迭代、路线图、资源取舍，还是管理层汇报？",
      "有没有评分维度或约束：价值、影响用户数、成本、风险、截止时间？"
    ],
    signals: [
      /需求|backlog|列表|功能|任务|事项|优先级|排序/i,
      /RICE|ICE|MoSCoW|Kano|价值|成本|影响|用户数|风险|迭代|路线图/i
    ]
  }
};

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 getSkillLabel(skillName) {
  return _SKILL_INTENT_MAP[skillName]?.name || skillName || 'Skill';
}

function hasExplicitDraftIntent(text = '') {
  return /先出|先写|直接写|直接生成|不用问|无需追问|按现有|基于现有|待补充|草稿|初稿|先给/.test(String(text || ''));
}

function collectSkillContextText(userMsg = '', attachments = [], ctx = {}) {
  const attachmentText = (attachments || []).map(att =>
    [att.name, att.content || ''].join('\n').slice(0, 1800)
  ).join('\n\n');
  const pageText = [
    ctx?.pageTitle,
    ctx?.pageUrl,
    ctx?.pageText,
    ctx?.text,
    ctx?.selectCopyText,
    ctx?.screenshotText
  ].filter(Boolean).join('\n\n').slice(0, 3000);
  return [userMsg, attachmentText, pageText].filter(Boolean).join('\n\n');
}

function assessSkillInput(skillName, userMsg, options = {}) {
  const rule = _SKILL_INPUT_REQUIREMENTS[skillName];
  if (!rule) return { ready: true, questions: [] };

  const attachments = options.attachments || [];
  const ctx = options.pageContext || {};
  const contextText = collectSkillContextText(userMsg, attachments, ctx).trim();
  const hasContext = attachments.length > 0 || hasReadablePageContext(ctx);
  const normalizedText = contextText.replace(/\s+/g, '');
  const rawUserText = String(userMsg || '').trim();

  if (hasExplicitDraftIntent(rawUserText) && normalizedText.length >= 8) {
    return { ready: true, questions: [] };
  }

  const minLen = hasContext ? (rule.minWithContext || 10) : (rule.minChars || 60);
  const signalHits = (rule.signals || []).filter(re => re.test(contextText)).length;
  const enoughLength = normalizedText.length >= minLen;
  const enoughSignals = hasContext ? signalHits >= 1 : signalHits >= Math.min(2, (rule.signals || []).length || 1);
  const richSignals = signalHits >= Math.min(3, (rule.signals || []).length || 1) && normalizedText.length >= 35;

  if ((enoughLength && enoughSignals) || richSignals) {
    return { ready: true, questions: [] };
  }

  const questions = (rule.questions || []).slice(0, 3);
  return {
    ready: false,
    questions,
    reason: !enoughLength ? '输入太短，缺少任务对象和上下文' : '缺少关键业务信息'
  };
}

function showSkillClarification(skillName, userMsg, assessment, options = {}) {
  const history = document.getElementById('chatHistory');
  if (!history) return;
  const oldBar = history.querySelector('.skill-clarification-bar');
  if (oldBar) oldBar.remove();

  const skillLabel = getSkillLabel(skillName);
  const round = Math.max(1, Number(options.round || 1));
  const questions = (assessment.questions || []).slice(0, 3);
  const bar = document.createElement('div');
  bar.className = 'skill-intent-bar skill-clarification-bar';
  bar.innerHTML = `<span class="skill-intent-icon">❓</span>
    <span class="skill-intent-text">
      <strong>${escHtml(skillLabel)}</strong> 还缺关键信息（第 ${round}/3 轮）。请补充：<br>
      ${questions.map((q, i) => `${i + 1}. ${escHtml(q)}`).join('<br>')}
    </span>
    <button class="skill-intent-skip">先出草稿</button>`;

  const originalMsg = options.originalMsg || userMsg || '';
  const answers = options.answers || [];
  _pendingSkillClarification = {
    skillName,
    skillLabel,
    originalMsg,
    answers,
    round,
    attachments: (options.attachments || []).slice(),
    pageContextDecision: options.pageContextDecision || {},
    questions
  };

  bar.querySelector('.skill-intent-skip').addEventListener('click', () => {
    bar.remove();
    const pending = _pendingSkillClarification;
    if (!pending) return;
    _pendingSkillClarification = null;
    _selectedSkill = pending.skillName;
    showSelectedSkillTag(pending.skillLabel);
    const chatInput = document.getElementById('chatInput');
    const fallback = buildClarifiedSkillInstruction(pending, '请先基于现有信息出一版草稿，信息不足处标注“待补充”。');
    if (chatInput) chatInput.value = fallback;
    pendingAttachments = pending.attachments || [];
    renderAttachments();
    _pageContextDecisionOverride = pending.pageContextDecision || {};
    _skillClarificationResume = { skillName: pending.skillName, round: 3 };
    startAnalysis();
  });

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

let _skillClarificationResume = null;

function buildClarifiedSkillInstruction(pending, latestAnswer = '') {
  const parts = [];
  if (pending.originalMsg) parts.push(`原始需求：${pending.originalMsg}`);
  const allAnswers = [...(pending.answers || []), latestAnswer].filter(Boolean);
  if (allAnswers.length) {
    parts.push('用户补充信息：\n' + allAnswers.map((a, i) => `${i + 1}. ${a}`).join('\n'));
  }
  return parts.join('\n\n') || latestAnswer || `请执行 ${pending.skillLabel || pending.skillName}`;
}

function maybeAskSkillClarificationBeforeRun(skillName, userMsg, options = {}) {
  const rule = _SKILL_INPUT_REQUIREMENTS[skillName];
  if (!rule) return false;
  if (skillName === 'HTML生成' && (
    String(userMsg || '').trim()
    || (options.attachments || []).length > 0
    || hasReadablePageContext(options.pageContext || {})
  )) {
    return false;
  }
  const resume = _skillClarificationResume && _skillClarificationResume.skillName === skillName
    ? _skillClarificationResume
    : null;
  const round = resume?.round || 1;
  _skillClarificationResume = null;

  const assessment = assessSkillInput(skillName, userMsg, {
    attachments: options.attachments || [],
    pageContext: options.pageContext || {}
  });

  if (assessment.ready) return false;
  if (round >= 3) {
    const chatInput = document.getElementById('chatInput');
    const current = chatInput?.value?.trim() || userMsg || '';
    if (chatInput && !hasExplicitDraftIntent(current)) {
      chatInput.value = current
        ? current + '\n\n请基于以上已有信息执行；仍缺失的信息请标注为“待补充”，不要再追问。'
        : '请基于已有信息执行；仍缺失的信息请标注为“待补充”，不要再追问。';
    }
    return false;
  }

  showSkillClarification(skillName, userMsg, assessment, {
    round,
    originalMsg: resume?.originalMsg || userMsg,
    answers: resume?.answers || [],
    attachments: options.attachments || [],
    pageContextDecision: options.pageContext || {}
  });
  resetAnalyzeBtn();
  return true;
}

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;
    _pendingChatAfterIntent = null;
    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 = [
  '分析', '评估', '评价', '总结', '概括', '提炼', '审查', '诊断', '解读',
  '看看', '看一下', '帮我看', '判断', '给建议', '输出', '生成', '改写', '润色', '提取'
];

const _PAGE_IMPLICIT_CONTEXT_PATTERNS = [
  /^(帮我)?(总结|概括|提炼|分析|解读|评估|审查|诊断)(一下|下|重点|要点)?[。！？!?]*$/i,
  /^(帮我)?(看看|看一下|看下|瞅一眼|判断一下|给点建议)[。！？!?]*$/i,
  /^(帮我)?(总结|分析|解读|评估|审查|诊断|看看|看一下|看下)(一下|下)?(这个|这里|这页|当前页)[。！？!?]*$/i,
  /^(这是什么|这个是什么|哪里有问题|有什么问题|重点是什么|核心是什么)[。！？!?]*$/i
];

const _PAGE_IMPLICIT_NON_PAGE_HINT = /https?:\/\/|www\.|\.com|\.cn|\.net|\.org|\.io|\.html|\.pdf|\.docx?|\.xlsx?|\.csv|\.json|代码|函数|报错|bug|模型|api|接口|插件|扩展|html|前端|项目|文件|目录|公众号|文章|写一篇|生成一个|做一个/i;

/**
 * 检测用户消息是否明确提到了页面信息
 * @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 detectImplicitPageIntent(msg) {
  const text = String(msg || '').replace(/\s+/g, '').trim();
  if (!text || text.length > 24) return false;
  if (_PAGE_IMPLICIT_NON_PAGE_HINT.test(text)) return false;
  return _PAGE_IMPLICIT_CONTEXT_PATTERNS.some(re => re.test(text));
}

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 = '') {
  return false;
}

function shouldAutoUsePageContext(msg, options = {}) {
  if (!hasReadablePageContext(pageContext)) return false;
  if (detectNoPageIntent(msg)) return false;
  if (String(options.fullMsg || '').includes('[附件内容]')) return false;
  if (detectPageIntent(msg)) return true;
  if (!options.hasAttachments && detectImplicitPageIntent(msg)) return true;
  return false;
}

function resolveChatPageContextForMessage(msg, mode = 'auto', fullMsg = '') {
  if (!hasReadablePageContext(pageContext)) return {};
  if (mode === 'force') return pageContext || {};
  if (mode === 'none') return {};
  return shouldAutoUsePageContext(msg, { fullMsg: fullMsg || 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);
  const chatInput = document.getElementById('chatInput');
  const userMsg = chatInput?.value?.trim() || '';
  if (maybeAskSkillClarificationBeforeRun(skillName, userMsg, {
    attachments: pendingAttachments || [],
    pageContext: _pageContextDecision || {}
  })) {
    return;
  }
  _doStartAnalysis(skillName);
}

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

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

function getChatInputValue() {
  return document.getElementById('chatInput')?.value?.trim() || '';
}

function setChatInputValue(value = '') {
  const input = document.getElementById('chatInput');
  if (!input) return;
  input.value = value;
  autoResizeChat(input);
}

function focusChatInput(placeholder = '') {
  const input = document.getElementById('chatInput');
  if (!input) return;
  if (placeholder) input.placeholder = placeholder;
  input.focus();
  autoResizeChat(input);
}

function setEmptyShortcutActive(action = '') {
  document.querySelectorAll('.empty-prompt').forEach(btn => {
    btn.classList.toggle('is-active', !!action && btn.dataset.action === action);
  });
}

function clearShortcutMode() {
  _activeShortcutMode = null;
  setEmptyShortcutActive('');
  const input = document.getElementById('chatInput');
  if (input) input.placeholder = '粘贴网址、图片、文件或文件夹，或直接提问... (Enter 发送)';
  if (window.AgentLoop?.dismiss) {
    window.AgentLoop.dismiss();
  } else if (window.AgentLoop?.reset) {
    window.AgentLoop.reset();
  }
  const panel = document.getElementById('agentLoopPanel');
  if (panel) panel.style.display = 'none';
}

function showShortcutAgentLoop(action, options = {}) {
  const loop = window.AgentLoop;
  if (!loop?.startRun) return;
  const labelMap = {
    'summarize-page': '总结当前页',
    'parse-url': '解析网址',
    'generate-html': 'HTML 生成'
  };
  const waitingMap = {
    'parse-url': '等待用户粘贴网址',
    'generate-html': '等待用户描述 HTML 需求'
  };
  const label = options.label || labelMap[action] || '快捷任务';
  const waitingMessage = options.waitingMessage || waitingMap[action] || '快捷任务已触发';
  loop.startRun({
    source: 'shortcut',
    title: label,
    objective: options.objective || waitingMessage,
    skillName: options.skillName || '',
    steps: [
      { id: 'trigger', title: '触发快捷入口', status: 'completed', message: label },
      { id: 'input', title: '确认输入内容', status: options.waiting ? 'in_progress' : 'completed', message: waitingMessage },
      { id: 'execute', title: '执行任务', status: options.waiting ? 'pending' : 'in_progress', message: options.executeMessage || '' },
      { id: 'verify', title: '检查结果', status: 'pending' },
      { id: 'final', title: '汇总交付结果', status: 'pending' }
    ]
  });
  loop.setCollapsed?.(false);
  loop.log?.('info', waitingMessage, { action });
}

function updateShortcutAgentLoopStep(stepId, status, message = '') {
  const loop = window.AgentLoop;
  if (!loop?.setStep) return;
  loop.setStep({ id: stepId, status, message });
  if (message) loop.log?.(status === 'failed' ? 'warn' : 'info', message);
}

function armShortcutMode(mode, action, placeholder, toastText) {
  _activeShortcutMode = mode;
  setEmptyShortcutActive(action);
  focusChatInput(placeholder);
  if (action === 'parse-url' || action === 'generate-html') {
    showShortcutAgentLoop(action, {
      waiting: true,
      skillName: action === 'generate-html' ? 'HTML生成' : '',
      objective: action === 'generate-html' ? '等待用户描述 HTML 页面需求' : '等待用户粘贴待解析网址'
    });
  }
  if (toastText) showToast(toastText);
}

function normalizeShortcutUrl(raw = '') {
  const text = String(raw || '').trim();
  const match = text.match(/https?:\/\/[^\s]+|[\w.-]+\.[a-z]{2,}(?:[/:?#][^\s]*)?/i);
  return normalizeUrlInput(match ? match[0] : text);
}

function hideEmptyState() {
  const emptyState = document.getElementById('emptyState');
  if (emptyState) emptyState.style.display = 'none';
}

function runShortcutChat({ msg, displayMsg = msg, fullMsg = msg, pageContextMode = 'auto' }) {
  if (analysisCancel || chatBusy) {
    showToast('当前任务正在执行，请稍候', 'warn');
    return false;
  }
  hideEmptyState();
  clearShortcutMode();
  setChatInputValue('');
  showShortcutAgentLoop(msg === '总结当前页' ? 'summarize-page' : '', {
    waiting: false,
    label: displayMsg || msg || '快捷任务',
    objective: fullMsg || msg || '',
    waitingMessage: '快捷任务已触发',
    executeMessage: '正在启动分析'
  });
  appendChatMsg(displayMsg, 'user');
  _doSendChat(msg, displayMsg, fullMsg, pageContextMode);
  return true;
}

function runShortcutClarification({ action, mode, displayMsg, fullMsg, placeholder, skillName = '' }) {
  if (analysisCancel || chatBusy) {
    showToast('当前任务正在执行，请稍候', 'warn');
    return false;
  }
  hideEmptyState();
  _activeShortcutMode = mode;
  setEmptyShortcutActive(action);
  focusChatInput(placeholder);
  setChatInputValue('');
  showShortcutAgentLoop(action, {
    waiting: false,
    skillName,
    objective: fullMsg || displayMsg,
    waitingMessage: '快捷意图已提交给大模型',
    executeMessage: '大模型正在追问必要信息'
  });
  appendChatMsg(displayMsg, 'user');
  _doSendChat(displayMsg, displayMsg, fullMsg, 'auto');
  return true;
}

async function runUrlShortcut(rawUrl) {
  if (analysisCancel || chatBusy) {
    showToast('当前任务正在执行，请稍候', 'warn');
    return false;
  }
  const url = normalizeShortcutUrl(rawUrl);
  if (!/^https?:\/\//i.test(url)) {
    armShortcutMode('url', 'parse-url', '粘贴网址、图片或文件后按 Enter，AIPROS 会直接解析', '请粘贴一个网址');
    return true;
  }

  hideEmptyState();
  clearShortcutMode();
  setChatInputValue('');
  showShortcutAgentLoop('parse-url', {
    waiting: false,
    objective: `解析网址：${url}`,
    waitingMessage: '网址已确认',
    executeMessage: '正在读取网页内容'
  });
  appendChatMsg(`解析网址：${url}`, 'user');
  const parsed = await fetchUrlContext(url);
  updateShortcutAgentLoopStep('execute', parsed ? 'completed' : 'failed', parsed ? '网页内容读取完成' : '网页内容读取失败，交给模型继续分析');
  const fullMsg = parsed
    ? `请基于已解析的网址内容，输出结构化报告：${url}`
    : `请分析这个公开网址并输出结构化报告：${url}`;
  _doSendChat('解析网址', `解析网址：${url}`, fullMsg, parsed ? 'force' : 'auto');
  return true;
}

function runSkillShortcut(skillName, instruction, options = {}) {
  if (analysisCancel || chatBusy) {
    showToast('当前任务正在执行，请稍候', 'warn');
    return false;
  }
  const finalInstruction = String(instruction || '').trim();
  if (!finalInstruction) return false;
  hideEmptyState();
  clearShortcutMode();
  setChatInputValue('');
  _selectedSkill = skillName;
  showSelectedSkillTag(getSkillLabel(skillName));
  _shortcutSkillInstruction = finalInstruction;
  _pageContextDecisionOverride = options.pageContextDecision || {};
  showShortcutAgentLoop('generate-html', {
    waiting: false,
    label: getSkillLabel(skillName),
    skillName,
    objective: finalInstruction,
    waitingMessage: 'HTML 需求已确认',
    executeMessage: '正在启动 HTML 生成'
  });
  startAnalysis();
  return true;
}

function runActiveShortcutFromInput() {
  if (!_activeShortcutMode) return false;
  const value = getChatInputValue();
  if (_activeShortcutMode === 'url') {
    if (!value) {
      showToast('请先粘贴网址', 'warn');
      focusChatInput('粘贴网址、图片或文件后按 Enter，AIPROS 会直接解析');
      updateShortcutAgentLoopStep('input', 'in_progress', '等待用户粘贴网址');
      return true;
    }
    runUrlShortcut(value);
    return true;
  }
  if (_activeShortcutMode === 'html') {
    if (!value) {
      showToast('请描述要生成的 HTML 页面', 'warn');
      focusChatInput('描述 HTML 需求；也可以粘贴图片、文件或文件夹后按 Enter');
      updateShortcutAgentLoopStep('input', 'in_progress', '等待用户描述 HTML 需求');
      return true;
    }
    runSkillShortcut('HTML生成', value, { pageContextDecision: {} });
    return true;
  }
  return false;
}

function handleEmptyShortcut(action, sourceBtn = null) {
  if (analysisCancel || chatBusy) {
    showToast('当前任务正在执行，请稍候', 'warn');
    return;
  }
  const currentInput = getChatInputValue();
  if (action === 'summarize-page') {
    runShortcutChat({
      msg: '总结当前页',
      fullMsg: '读取当前页面，帮我提炼重点、关键结论和下一步动作。',
      pageContextMode: 'force'
    });
    return;
  }
  if (action === 'parse-url') {
    if (currentInput) {
      runUrlShortcut(currentInput);
    } else {
      runShortcutClarification({
        action: 'parse-url',
        mode: 'url',
        displayMsg: '解析网址',
        placeholder: '回答 AIPROS 的追问，或直接粘贴网址后按 Enter',
        fullMsg: '用户点击了【解析网址】快捷入口，但还没有提供网址。请用简短、自然的中文追问用户粘贴要解析的网址；只问一个问题，不要开始分析。',
      });
    }
    return;
  }
  if (action === 'generate-html') {
    if (currentInput) {
      runSkillShortcut('HTML生成', currentInput, { pageContextDecision: {} });
    } else {
      runShortcutClarification({
        action: 'generate-html',
        mode: 'html',
        displayMsg: '生成 HTML',
        skillName: 'HTML生成',
        placeholder: '回答 AIPROS 的追问，或直接描述 HTML 需求后按 Enter',
        fullMsg: '用户点击了【生成 HTML】快捷入口，但还没有描述页面需求。请像专业产品助理一样，用简短中文追问用户：要做什么页面、面向谁、需要哪些内容或风格。最多问 3 个关键问题，不要生成 HTML。',
      });
    }
  }
}

// ==============================
// 文件上传处理
// ==============================
let pendingAttachments = []; // { name, type, content(text) | dataUrl(binary) }
const ATTACHMENT_MAX_FILES = 80;
const ATTACHMENT_INLINE_BINARY_LIMIT = 15 * 1024 * 1024;

function isTextAttachmentFile(file) {
  return file.type.startsWith('text/') ||
    /\.(txt|md|csv|json|xml|yaml|yml|js|ts|tsx|jsx|py|java|html|css|scss|less|log|sql|ini|env|toml|vue|svelte|go|rs|php|rb|swift|kt|c|cpp|h|hpp)$/i.test(file.name);
}

function isParsableAttachmentFile(file) {
  return /\.(docx|pdf|xlsx|xls|doc)$/i.test(file.name);
}

function normalizeAttachmentName(file, path = '') {
  return String(path || file.webkitRelativePath || file.name || `文件_${Date.now()}`)
    .replace(/^\/+/, '')
    .replace(/\0/g, '')
    .trim() || file.name || `文件_${Date.now()}`;
}

function queueAttachmentFile(file, path = '') {
  if (!file) return false;
  const name = normalizeAttachmentName(file, path);
  const isText = isTextAttachmentFile(file);
  const isParsable = isParsableAttachmentFile(file);

  if (isParsable) {
    const att = {
      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('[附件] 解析失败:', name, err);
      att.content = '[文件解析失败：' + (err.message || '未知错误') + ']';
      att.parsing = false;
      renderAttachments();
    });
    return true;
  }

  if (!isText && file.size > ATTACHMENT_INLINE_BINARY_LIMIT) {
    pendingAttachments.push({
      name,
      type: file.type || 'application/octet-stream',
      size: file.size,
      content: `[文件较大，已作为附件记录：${name}，大小 ${(file.size / 1024 / 1024).toFixed(1)}MB]`,
      dataUrl: null
    });
    renderAttachments();
    return true;
  }

  const reader = new FileReader();
  reader.onload = (ev) => {
    pendingAttachments.push({
      name,
      type: file.type || 'application/octet-stream',
      size: file.size,
      content: isText ? ev.target.result : null,
      dataUrl: isText ? null : ev.target.result
    });
    renderAttachments();
  };
  reader.onerror = () => {
    pendingAttachments.push({
      name,
      type: file.type || 'application/octet-stream',
      size: file.size,
      content: '[文件读取失败]',
      dataUrl: null
    });
    renderAttachments();
  };

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

function addAttachmentFiles(fileEntries = [], source = '粘贴') {
  const normalized = (fileEntries || [])
    .map(item => item?.file ? item : { file: item, path: item?.webkitRelativePath || item?.name })
    .filter(item => item.file);
  if (!normalized.length) return 0;

  const remaining = Math.max(0, ATTACHMENT_MAX_FILES - pendingAttachments.length);
  if (!remaining) {
    showToast(`最多同时添加 ${ATTACHMENT_MAX_FILES} 个文件`, 'warn');
    return 0;
  }

  const accepted = normalized.slice(0, remaining);
  accepted.forEach(item => queueAttachmentFile(item.file, item.path));
  if (normalized.length > accepted.length) {
    showToast(`已添加 ${accepted.length} 个文件，超出部分已跳过`, 'warn');
  } else {
    const folderHint = accepted.some(item => String(item.path || '').includes('/')) ? ' / 文件夹' : '';
    showToast(`${source}已添加 ${accepted.length} 个文件${folderHint}`);
  }
  return accepted.length;
}

function handleFileSelect(e) {
  const files = Array.from(e.target.files);
  if (!files.length) return;
  addAttachmentFiles(files, '已选择');
  // 清空 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';
    const icon = f.parsing ? '⏳' : (String(f.type || '').startsWith('image/') ? '🖼' : (String(f.name || '').includes('/') ? '📁' : '📄'));
    tag.title = f.name;
    tag.append(document.createTextNode(icon + ' ' + 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;
  }

  if (runActiveShortcutFromInput()) 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();

  // ===== 最近 HTML 交付物发布意图 =====
  // 这类命令必须优先于当前选中的 Skill，否则「HTML生成」会误把“发布上线”当成继续做网页。
  if (detectPublishExistingArtifactIntent(msg)) {
    _pendingSkillClarification = null;
    _pendingChatAfterIntent = null;
    clearSelectedSkillTag();
    chatConversationHistory.push({ role: 'user', content: msg });
    runArtifactPublishFromChat(msg).then((result) => {
      chatConversationHistory.push({
        role: 'assistant',
        content: result?.ok
          ? `已按用户要求处理最近 HTML 交付物的公网发布${result.url ? '：' + result.url : '。'}`
          : `未能发布 HTML：${result?.error || '未知原因'}`
      });
    }).catch(err => {
      appendChatMsg('发布失败：' + (err?.message || String(err)), 'ai');
      showToast('发布失败：' + (err?.message || String(err)), 'warn');
    }).finally(() => {
      chatBusy = false;
      chatCancel = null;
      const btn = document.getElementById('chatSendBtn');
      if (btn) { btn.textContent = '↑'; btn.disabled = false; }
      scrollChatBottom();
    });
    return;
  }

  // ===== 强 HTML 生成意图 =====
  // “我要做 html / 生成 HTML 页面”要直接进入 HTML 生成 Skill，不走普通对话。
  if (detectStrongHtmlGenerationIntent(msg)) {
    _pendingSkillClarification = null;
    _pendingChatAfterIntent = null;
    clearSelectedSkillTag();
    chatBusy = false;
    const sendBtnHtml = document.getElementById('chatSendBtn');
    if (sendBtnHtml) { sendBtnHtml.textContent = '↑'; sendBtnHtml.disabled = false; }
    pendingAttachments = _attachmentsSnapshot;
    renderAttachments();
    if (!runSkillShortcut('HTML生成', msg, { pageContextDecision: {} })) {
      _selectedSkill = 'HTML生成';
      showSelectedSkillTag('HTML生成');
      _shortcutSkillInstruction = msg;
      startAnalysis();
    }
    return;
  }

  // ===== Skill 信息补充回合 =====
  // 上一轮 Skill 因信息不足停在追问，这里把用户补充合并回原始任务，再尝试执行。
  if (_pendingSkillClarification) {
    const pending = _pendingSkillClarification;
    _pendingSkillClarification = null;
    chatBusy = false;
    const sendBtnClarify = document.getElementById('chatSendBtn');
    if (sendBtnClarify) { sendBtnClarify.textContent = '↑'; sendBtnClarify.disabled = false; }

    const latestAnswer = fullMsg || msg;
    const nextAnswers = [...(pending.answers || []), latestAnswer].filter(Boolean);
    const nextRound = Math.min(3, Number(pending.round || 1) + 1);
    const combinedInstruction = buildClarifiedSkillInstruction({ ...pending, answers: pending.answers || [] }, latestAnswer);

    _selectedSkill = pending.skillName;
    showSelectedSkillTag(pending.skillLabel || pending.skillName);
    pendingAttachments = [...(pending.attachments || []), ..._attachmentsSnapshot];
    renderAttachments();
    _pageContextDecisionOverride = pending.pageContextDecision || {};
    _skillClarificationResume = {
      skillName: pending.skillName,
      round: nextRound,
      originalMsg: pending.originalMsg || '',
      answers: nextAnswers
    };
    if (input) input.value = combinedInstruction;
    startAnalysis();
    return;
  }

  // ===== 手动选中的 Skill =====
  // 用户在设置面板/Skill 浮层里先选 Skill，再发送任务时，也必须走同一套信息门槛。
  if (_selectedSkill) {
    chatBusy = false;
    const sendBtnSelected = document.getElementById('chatSendBtn');
    if (sendBtnSelected) { sendBtnSelected.textContent = '↑'; sendBtnSelected.disabled = false; }
    pendingAttachments = _attachmentsSnapshot;
    renderAttachments();
    if (input) input.value = msg;
    startAnalysis();
    return;
  }

  // ===== 审批意见/提交操作意图 =====
  // 例如：“填同意”“审批意见写无异议”“同意并提交”“帮我想审批意见并提交”。
  const approvalIntent = parseApprovalOperationIntent(msg);
  if (approvalIntent) {
    if (approvalIntent.needsAiOpinion) {
      chatBusy = false;
      const sendBtnApprovalAi = document.getElementById('chatSendBtn');
      if (sendBtnApprovalAi) { sendBtnApprovalAi.textContent = '↑'; sendBtnApprovalAi.disabled = false; }
      _doSendChat(msg, displayMsg, buildApprovalAiInstruction(msg, fullMsg, approvalIntent), 'force');
    } else {
      chatConversationHistory.push({ role: 'user', content: msg });
      runApprovalOperation(approvalIntent);
    }
    return;
  }

  // ===== 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; // 保持可点击，点击=中断

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

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

  // 注册工具调用文本替换回调（工具执行完后调用）
  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, '');
    let doneHtml;
    if (info && typeof info === 'object') {
      const artifact = rememberArtifact(info);
      chatGeneratedArtifact = artifact;
      chatArtifactNotes.push(`已生成交付物：${artifact.filename}。用户可以点击交付物卡片打开预览或下载。`);
      doneHtml = renderArtifactCard(artifact);
      if (isHtmlArtifact(artifact)) {
        chatRawText = '';
        aiMsg.classList.add('artifact-msg');
      } else if (!chatRawText.trim()) {
        aiMsg.classList.add('artifact-msg');
      }
    } else {
      aiMsg.classList.remove('artifact-msg');
      doneHtml = `<div class="doc-gen-done">${escHtml(info)}</div>`;
    }
    aiMsg.innerHTML = isHtmlArtifact(chatGeneratedArtifact)
      ? doneHtml
      : 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, fullMsg);

  // 记录用户消息到对话历史
  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;

        if (!chatGeneratedArtifact) {
          const artifact = createHtmlArtifactFromContent(chatRawText, 'AIPROS页面');
          if (artifact) {
            chatGeneratedArtifact = artifact;
            chatArtifactNotes.push(`已生成交付物：${artifact.filename}。用户可以点击交付物卡片打开预览、下载或发布。`);
            chatRawText = '';
            aiMsg.classList.add('artifact-msg');
            aiMsg.innerHTML = renderArtifactCard(artifact);
          }
        }
        
        // 记录AI回复和交付物到对话历史
        const memoryText = [chatRawText.trim(), ...chatArtifactNotes].filter(Boolean).join('\n\n');
        if (memoryText) {
          chatConversationHistory.push({ role: 'assistant', content: memoryText });
          // 增量摘要：当消息超过 MAX_FULL_MESSAGES 时，把旧消息压缩进摘要
          compressOldMessages();
        }
        
        const artifactOnlyHtml = isHtmlArtifact(chatGeneratedArtifact);
        // 统一提供 Excel/Word 下载 + 复制全文按钮；HTML 交付物卡片自带打开/下载/发布。
        if (!artifactOnlyHtml && chatRawText.trim()) {
          const chatWrap = aiMsg.closest('.chat-msg-wrap');
          appendExportButtons(aiMsg, chatRawText);
          appendInsertButton(chatWrap, chatRawText, '对话');
        }
        if (!artifactOnlyHtml) appendFollowupSuggestions(aiMsg, chatRawText);
        // 右下角复制按钮
        if (!artifactOnlyHtml && 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 copyToClipboard(text) {
  const value = String(text || '');
  if (!value) return;
  if (navigator.clipboard?.writeText) {
    navigator.clipboard.writeText(value).catch(() => fallbackCopy(value));
  } else {
    fallbackCopy(value);
  }
}

function fallbackCopy(text) {
  const ta = document.createElement('textarea');
  ta.value = text;
  ta.style.position = 'fixed';
  ta.style.left = '-9999px';
  document.body.appendChild(ta);
  ta.select();
  try { document.execCommand('copy'); } catch (_) {}
  document.body.removeChild(ta);
}

function rememberArtifact(info = {}) {
  const id = info.id || ('artifact_' + Date.now() + '_' + Math.random().toString(16).slice(2));
  const filename = info.filename || info.name || 'AIPROS交付物.html';
  const artifact = {
    id,
    format: info.format || 'html',
    filename,
    title: info.title || filename,
    url: info.url || info.previewUrl || '',
    publicUrl: info.publicUrl || info.deployUrl || '',
    deployStatus: info.deployStatus || '',
    deployedAt: info.deployedAt || '',
    projectName: info.projectName || '',
    deployError: info.deployError || '',
    deployments: Array.isArray(info.deployments) ? info.deployments : [],
    content: info.content || '',
    mime: info.mime || (String(filename).endsWith('.html') ? 'text/html;charset=utf-8' : 'text/plain;charset=utf-8'),
    size: Number(info.size || (info.content ? info.content.length : 0)),
    createdAt: info.createdAt || new Date().toISOString()
  };
  chatArtifacts = [artifact, ...chatArtifacts.filter(item => item.id !== id)].slice(0, 12);
  window.__aiprosArtifacts = chatArtifacts;
  return artifact;
}

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

function createHtmlArtifactFromContent(content = '', filename = 'AIPROS页面') {
  const html = extractCompleteHtmlDocumentFromText(content);
  if (!html) return null;
  const safeBase = String(filename || 'AIPROS页面')
    .replace(/[\\/:*?"<>|#%{}[\]^~`]+/g, '-')
    .replace(/\s+/g, '-')
    .slice(0, 60) || 'AIPROS页面';
  const finalName = /\.html?$/i.test(safeBase) ? safeBase : `${safeBase}.html`;
  const blob = new Blob([html], { type: 'text/html;charset=utf-8' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = finalName;
  a.style.display = 'none';
  document.body.appendChild(a);
  a.click();
  setTimeout(() => { if (a.parentNode) a.parentNode.removeChild(a); }, 1000);
  try { window.open(url, '_blank'); } catch (_) {}
  return rememberArtifact({
    format: 'html',
    filename: finalName,
    title: finalName,
    url,
    content: html,
    mime: 'text/html;charset=utf-8',
    size: blob.size,
    createdAt: new Date().toISOString()
  });
}

function isHtmlArtifact(artifact) {
  if (!artifact) return false;
  return artifact.format === 'html' || /\.html?$/i.test(artifact.filename || '') || /html/i.test(artifact.mime || '');
}

function findLatestPublishableHtmlArtifact() {
  return chatArtifacts.find(item => isHtmlArtifact(item) && item.content) || null;
}

function detectPublishExistingArtifactIntent(text = '') {
  const msg = String(text || '').toLowerCase();
  if (!msg.trim()) return false;
  const publishSignal = /发布|上线|部署|公网|公开链接|外网链接|生成链接|分享链接|publish|deploy|online|public url|cloudflare|pages/.test(msg);
  if (!publishSignal) return false;
  const createSignal = /新建|重新做|再做|生成一个|做一个|制作|写一个|改成|重写|重做/.test(msg);
  return !createSignal;
}

function detectStrongHtmlGenerationIntent(text = '') {
  const msg = String(text || '').trim();
  if (!msg) return false;
  const lower = msg.toLowerCase();
  if (detectPublishExistingArtifactIntent(lower)) return false;
  return /(?:做|生成|写|制作|创建|导出|出一版|弄|搞).{0,12}html|html.{0,10}(页面|网页|报告|落地页|h5|可视化|预览|文件)|(?:我要|我想|帮我|给我).{0,8}html/i.test(lower);
}

async function runArtifactPublishFromChat(msg = '') {
  const artifact = findLatestPublishableHtmlArtifact();
  if (!artifact) {
    appendChatMsg('我没找到可发布的 HTML 交付物，请先生成一个 HTML 页面。', 'ai');
    showToast('没有可发布的 HTML 交付物', 'warn');
    return;
  }

  if (artifact.publicUrl) {
    const bubble = appendChatMsg('', 'ai deploy-msg');
    bubble.innerHTML = renderDeployResultCard(artifact, {
      ok: true,
      url: artifact.publicUrl,
      projectName: artifact.projectName || 'Cloudflare Pages'
    });
    try { window.open(artifact.publicUrl, '_blank'); } catch (_) {}
    showToast('这个 HTML 已有公网链接');
    return;
  }

  appendChatMsg(`正在发布最近生成的 HTML：${artifact.filename}`, 'ai');
  return await publishArtifact(artifact.id);
}

function getArtifact(id) {
  return chatArtifacts.find(item => item.id === id) || null;
}

function ensureArtifactUrl(artifact) {
  if (!artifact) return '';
  if (artifact.url) return artifact.url;
  if (artifact.content) {
    const blob = new Blob([artifact.content], { type: artifact.mime || 'text/html;charset=utf-8' });
    artifact.url = URL.createObjectURL(blob);
    return artifact.url;
  }
  return '';
}

function openArtifact(id) {
  const artifact = getArtifact(id);
  const url = ensureArtifactUrl(artifact);
  if (!url) {
    showToast('交付物预览地址不可用', 'warn');
    return;
  }
  window.open(url, '_blank');
}

function downloadArtifact(id) {
  const artifact = getArtifact(id);
  if (!artifact) {
    showToast('交付物不存在', 'warn');
    return;
  }
  if (artifact.content) {
    downloadContent(artifact.content, artifact.filename, artifact.mime || 'text/html;charset=utf-8');
    return;
  }
  const url = ensureArtifactUrl(artifact);
  if (!url) return;
  const a = document.createElement('a');
  a.href = url;
  a.download = artifact.filename;
  document.body.appendChild(a);
  a.click();
  document.body.removeChild(a);
}

function openPublicArtifact(id) {
  const artifact = getArtifact(id);
  if (!artifact?.publicUrl) {
    showToast('还没有公网地址', 'warn');
    return;
  }
  window.open(artifact.publicUrl, '_blank');
}

function makeCloudflareProjectName(filename = '') {
  const base = String(filename || 'html')
    .replace(/\.[^.]+$/, '')
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, '-')
    .replace(/^-+|-+$/g, '')
    .slice(0, 42);
  const suffix = new Date().toISOString().slice(2, 10).replace(/-/g, '');
  return `aipros-${base || 'html'}-${suffix}`.slice(0, 58);
}

function refreshArtifactCards(id) {
  const artifact = getArtifact(id);
  if (!artifact) return;
  document.querySelectorAll('.artifact-card').forEach(card => {
    if (card.dataset.artifactId === id) card.outerHTML = renderArtifactCard(artifact);
  });
}

function renderDeployResultCard(artifact, result) {
  const url = result.url || artifact.publicUrl || '';
  const projectName = result.projectName || artifact.projectName || 'Cloudflare Pages';
  return `
    <div class="deploy-result-card">
      <div class="deploy-result-main">
        <span class="deploy-result-kicker">Cloudflare Pages</span>
        <strong>${escHtml(projectName)}</strong>
        <a href="${escAttr(url)}" target="_blank" rel="noopener noreferrer">${escHtml(url || '已发布，未解析到链接')}</a>
      </div>
      ${url ? `<button type="button" class="deploy-copy-btn" data-url="${escAttr(url)}">复制</button>` : ''}
    </div>
  `;
}

function callCloudflareDeployTool(args) {
  const executor = async () => {
    const resp = await callPanelNativeTool('cloudflare.pages.deploy', args);
    return resp?.result || resp;
  };
  if (window.AgentLoop?.executeTool) {
    return window.AgentLoop.executeTool({
      tool: 'cloudflare.pages.deploy',
      args,
      retries: 0,
      executor
    });
  }
  if (!confirm('即将把这个 HTML 发布到公网 Cloudflare Pages，是否继续？')) {
    return Promise.resolve({ ok: false, error: '用户取消了发布' });
  }
  return executor();
}

async function publishArtifact(id, sourceBtn = null) {
  const artifact = getArtifact(id);
  if (!artifact) {
    showToast('交付物不存在', 'warn');
    return { ok: false, error: '交付物不存在' };
  }
  if (artifact.format !== 'html' && !/\.html?$/i.test(artifact.filename || '')) {
    showToast('当前只支持发布 HTML 交付物', 'warn');
    return { ok: false, error: '当前只支持发布 HTML 交付物' };
  }
  if (!artifact.content) {
    showToast('这个交付物没有可发布的 HTML 源码，请重新生成一次', 'warn');
    return { ok: false, error: '这个交付物没有可发布的 HTML 源码' };
  }

  artifact.deployStatus = 'deploying';
  artifact.deployError = '';
  refreshArtifactCards(id);
  if (sourceBtn) {
    sourceBtn.disabled = true;
    sourceBtn.textContent = '发布中';
  }

  const projectName = artifact.projectName || makeCloudflareProjectName(artifact.filename);
  if (window.AgentLoop?.startRun) {
    window.AgentLoop.startRun({
      source: 'deploy',
      title: '发布到 Cloudflare',
      objective: `将 ${artifact.filename} 发布到 Cloudflare Pages 公网`,
      skillName: 'Cloudflare Pages',
      steps: [
        { id: 'prepare', title: '准备 HTML 文件', status: 'completed', message: artifact.filename },
        { id: 'permission', title: '确认公网发布权限', status: 'pending' },
        { id: 'tools', title: '调用发布工具', status: 'pending' },
        { id: 'verify', title: '检查发布结果', status: 'pending' },
        { id: 'final', title: '返回公网链接', status: 'pending' }
      ]
    });
    window.AgentLoop.setCollapsed?.(false);
  }

  const rawResult = await callCloudflareDeployTool({
    projectName,
    filename: artifact.filename,
    content: artifact.content,
    branch: 'main'
  });
  const result = rawResult?.result || rawResult;

  if (!rawResult?.ok || !result?.ok) {
    artifact.deployStatus = 'failed';
    artifact.deployError = result?.error || rawResult?.error || '发布失败';
    refreshArtifactCards(id);
    if (window.AgentLoop?.finishRun) window.AgentLoop.finishRun('failed', artifact.deployError);
    appendChatMsg(`发布失败：${artifact.deployError}`, 'ai');
    showToast('发布失败：' + artifact.deployError, 'warn');
    return { ok: false, error: artifact.deployError };
  }

  const publicUrl = result.url || result.urls?.[0] || '';
  artifact.deployStatus = 'deployed';
  artifact.publicUrl = publicUrl;
  artifact.projectName = result.projectName || projectName;
  artifact.deployedAt = result.deployedAt || new Date().toISOString();
  artifact.deployments = [{ ...result, url: publicUrl }, ...(artifact.deployments || [])].slice(0, 5);
  rememberArtifact(artifact);
  refreshArtifactCards(id);
  if (window.AgentLoop?.setStep) window.AgentLoop.setStep({ id: 'final', status: 'completed', message: publicUrl || '发布完成' });
  if (window.AgentLoop?.finishRun) window.AgentLoop.finishRun('completed', 'Cloudflare 发布完成');

  const bubble = appendChatMsg('', 'ai deploy-msg');
  bubble.innerHTML = renderDeployResultCard(artifact, result);
  scrollChatBottom();
  if (publicUrl) window.open(publicUrl, '_blank');
  showToast('已发布到 Cloudflare Pages');
  return { ok: true, url: publicUrl, artifact };
}

function renderArtifactCard(artifact) {
  const sizeText = artifact.size ? `${Math.max(1, Math.round(artifact.size / 1024))} KB` : '已生成';
  const typeText = artifact.format === 'html' ? 'HTML 页面' : String(artifact.format || '交付物').toUpperCase();
  const id = escHtml(artifact.id);
  const canPublish = artifact.format === 'html' && !!artifact.content;
  const deployButton = artifact.publicUrl
    ? `<button type="button" class="artifact-public-btn" data-artifact-id="${id}">公网</button>`
    : (canPublish ? `<button type="button" class="artifact-publish-btn" data-artifact-id="${id}"${artifact.deployStatus === 'deploying' ? ' disabled' : ''}>${artifact.deployStatus === 'deploying' ? '发布中' : '发布'}</button>` : '');
  return `<div class="artifact-card" data-artifact-id="${id}"><div class="artifact-thumb" aria-hidden="true"><span></span></div><div class="artifact-main"><div class="artifact-title">${escHtml(artifact.title || artifact.filename)}</div><div class="artifact-meta">${escHtml(typeText)} · ${escHtml(sizeText)}${artifact.publicUrl ? ' · 已发布' : ''}</div></div><div class="artifact-actions"><button type="button" class="artifact-open-btn" data-artifact-id="${id}">打开</button><button type="button" class="artifact-download-btn" data-artifact-id="${id}">下载</button>${deployButton}</div></div>`;
}

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

function renderEmptyState() {
  const history = document.getElementById('chatHistory');
  if (!history) return;
  history.innerHTML = `
    <div class="panel-empty" id="emptyState">
      <div class="empty-card">
        <div class="panel-empty-icon">✦</div>
        <div class="empty-kicker">AIPROS Workbench</div>
        <div class="empty-title">面向真实业务流程的智能体驱动工作平台</div>
        <div class="empty-subtitle">粘贴网址、输入需求，或直接说“总结当前页”。</div>
        <div class="empty-prompts">
          <button class="empty-prompt" type="button" data-action="summarize-page" title="快捷键 Alt+1" aria-keyshortcuts="Alt+1">总结当前页</button>
          <button class="empty-prompt" type="button" data-action="parse-url" title="快捷键 Alt+2" aria-keyshortcuts="Alt+2">解析网址</button>
          <button class="empty-prompt" type="button" data-action="generate-html" title="快捷键 Alt+3" aria-keyshortcuts="Alt+3">生成 HTML</button>
        </div>
      </div>
    </div>
  `;
}

function clearChat() {
  const history = document.getElementById('chatHistory');
  if (history) history.innerHTML = '';
  if (window.AgentLoop?.reset) window.AgentLoop.reset();
  // 清空对话历史记忆
  chatConversationHistory = [];
  chatHistorySummary = '';
  chatArtifacts = [];
  window.__aiprosArtifacts = chatArtifacts;
  // 同步重置所有运行时状态，避免残留导致死锁
  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';
  renderEmptyState();
}

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

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

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

// ==============================
// 加载Skill内容
// ==============================
function getRuntimeAssetUrl(path) {
  if (typeof chrome !== 'undefined' && chrome.runtime?.getURL) {
    return chrome.runtime.getURL(path);
  }
  return path;
}

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(getRuntimeAssetUrl(`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(getRuntimeAssetUrl(`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', 'automation', '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 === 'automation') loadAutomationPanel();
  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 = getRuntimeAssetUrl(`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('AIPROS 已关闭 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();
}

function collectModelIds(value, out = []) {
  if (!value) return out;
  if (typeof value === 'string') {
    out.push(value);
    return out;
  }
  if (Array.isArray(value)) {
    value.forEach(item => collectModelIds(item, out));
    return out;
  }
  if (typeof value === 'object') {
    const direct = value.id || value.name || value.model || value.model_name || value.modelName;
    if (typeof direct === 'string') out.push(direct);
    const nestedKeys = ['data', 'models', 'items', 'list', 'result'];
    nestedKeys.forEach(key => {
      if (value[key] && value[key] !== value) collectModelIds(value[key], out);
    });
  }
  return out;
}

function parseModelsFromResponse(data) {
  return [...new Set(collectModelIds(data)
    .map(item => String(item || '').trim())
    .filter(Boolean))].sort();
}

async function fetchModelsViaBackground(modelsUrl, apiKey) {
  if (typeof chrome === 'undefined' || !chrome.runtime?.sendMessage) {
    throw new Error('当前环境无法通过后台请求模型列表');
  }
  return await new Promise((resolve, reject) => {
    chrome.runtime.sendMessage({
      type: 'FETCH_MODEL_LIST',
      url: modelsUrl,
      apiKey
    }, (resp) => {
      if (chrome.runtime.lastError) {
        reject(new Error(chrome.runtime.lastError.message));
        return;
      }
      if (resp?.ok) resolve(resp.data || {});
      else reject(new Error(resp?.error || '后台获取模型失败'));
    });
  });
}

async function requestModelListJson(modelsUrl, apiKey) {
  try {
    const resp = await fetch(modelsUrl, {
      method: 'GET',
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json',
        'Authorization': 'Bearer ' + apiKey
      }
    });
    if (!resp.ok) {
      const errText = await resp.text().catch(() => '');
      throw new Error('API 返回 ' + resp.status + (errText ? '：' + errText.slice(0, 160) : ''));
    }
    return await resp.json();
  } catch (frontErr) {
    try {
      return await fetchModelsViaBackground(modelsUrl, apiKey);
    } catch (bgErr) {
      throw new Error(frontErr?.message || bgErr?.message || '获取模型失败');
    }
  }
}

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.host}/*`;
  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 storedSettings = await _getLocalSettings();
      key = normalizeApiKeyForHeader(storedSettings.api_key || storedSettings.apiKey || storedSettings.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 data = await requestModelListJson(modelsUrl, key);

    // 解析模型列表（OpenAI 兼容格式: { data: [{ id: "model-name" }] }）
    const models = parseModelsFromResponse(data);
    // 去重排序
    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 = 'var(--ui-success)';
      badge.style.color = '#fff';
      info.textContent = `${config.model || 'qwen-plus'} · ${PROVIDERS[document.getElementById('settingsProvider')?.value]?.name || '自定义'}`;
    } else {
      badge.textContent = '未配置';
      badge.style.background = 'var(--ui-danger-soft)';
      badge.style.color = 'var(--ui-danger)';
      info.textContent = '请在下方配置你的 AI 模型';
    }
  });
}

// ==============================
// Agent 权限策略中心 + Skill 批量评测
// ==============================
const DEFAULT_AGENT_POLICY_SETTINGS = {
  lowRisk: 'auto',
  mediumRisk: 'confirm',
  highRisk: 'confirm',
  checkpoint: 'auto',
  retryCount: 1,
  trustedPaths: [],
  toolRules: [],
  pathRules: []
};

function normalizeAgentRuleLines(rawRules = []) {
  const allowed = ['auto', 'confirm', 'block'];
  const lines = Array.isArray(rawRules)
    ? rawRules
    : String(rawRules || '').split('\n').map(line => {
      const match = line.match(/^\s*([^:=]+?)\s*[:=]\s*(auto|confirm|block)\s*$/i);
      return match ? { pattern: match[1], action: match[2].toLowerCase() } : null;
    }).filter(Boolean);
  return lines.map(rule => ({
    pattern: String(rule.pattern || rule.tool || rule.path || '').trim(),
    action: String(rule.action || '').trim().toLowerCase()
  })).filter(rule => rule.pattern && allowed.includes(rule.action));
}

function agentRulesToText(rules = []) {
  return normalizeAgentRuleLines(rules).map(rule => `${rule.pattern}=${rule.action}`).join('\n');
}

function normalizeAgentPolicySettings(raw = {}) {
  const pick = (value, allowed, fallback) => allowed.includes(value) ? value : fallback;
  const trustedPaths = Array.isArray(raw.trustedPaths)
    ? raw.trustedPaths.map(x => String(x || '').trim()).filter(Boolean)
    : String(raw.trustedPaths || '').split('\n').map(x => x.trim()).filter(Boolean);
  return {
    lowRisk: pick(raw.lowRisk, ['auto', 'confirm', 'block'], DEFAULT_AGENT_POLICY_SETTINGS.lowRisk),
    mediumRisk: pick(raw.mediumRisk, ['auto', 'confirm', 'block'], DEFAULT_AGENT_POLICY_SETTINGS.mediumRisk),
    highRisk: pick(raw.highRisk, ['confirm', 'block'], DEFAULT_AGENT_POLICY_SETTINGS.highRisk),
    checkpoint: pick(raw.checkpoint, ['auto', 'ask', 'off'], DEFAULT_AGENT_POLICY_SETTINGS.checkpoint),
    retryCount: Math.max(0, Math.min(5, Number(raw.retryCount ?? DEFAULT_AGENT_POLICY_SETTINGS.retryCount) || 0)),
    trustedPaths,
    toolRules: normalizeAgentRuleLines(raw.toolRules),
    pathRules: normalizeAgentRuleLines(raw.pathRules)
  };
}

function setAgentPolicyForm(policy = {}) {
  const p = normalizeAgentPolicySettings(policy);
  const low = document.getElementById('policyLowRisk');
  const medium = document.getElementById('policyMediumRisk');
  const high = document.getElementById('policyHighRisk');
  const checkpoint = document.getElementById('policyCheckpoint');
  const retry = document.getElementById('policyRetryCount');
  const paths = document.getElementById('policyTrustedPaths');
  const toolRules = document.getElementById('policyToolRules');
  const pathRules = document.getElementById('policyPathRules');
  if (low) low.value = p.lowRisk;
  if (medium) medium.value = p.mediumRisk;
  if (high) high.value = p.highRisk;
  if (checkpoint) checkpoint.value = p.checkpoint;
  if (retry) retry.value = String(p.retryCount);
  if (paths) paths.value = p.trustedPaths.join('\n');
  if (toolRules) toolRules.value = agentRulesToText(p.toolRules);
  if (pathRules) pathRules.value = agentRulesToText(p.pathRules);
}

function readAgentPolicyForm() {
  return normalizeAgentPolicySettings({
    lowRisk: document.getElementById('policyLowRisk')?.value,
    mediumRisk: document.getElementById('policyMediumRisk')?.value,
    highRisk: document.getElementById('policyHighRisk')?.value,
    checkpoint: document.getElementById('policyCheckpoint')?.value,
    retryCount: document.getElementById('policyRetryCount')?.value,
    trustedPaths: document.getElementById('policyTrustedPaths')?.value || '',
    toolRules: document.getElementById('policyToolRules')?.value || '',
    pathRules: document.getElementById('policyPathRules')?.value || ''
  });
}

function showPolicyResult(message, ok = true) {
  const el = document.getElementById('policyResult');
  if (!el) return;
  el.style.display = message ? 'block' : 'none';
  el.textContent = message || '';
  el.className = 'install-result ' + (ok ? 'ok' : 'err');
}

async function loadAgentPolicySettings() {
  let policy = DEFAULT_AGENT_POLICY_SETTINGS;
  try {
    if (typeof chrome !== 'undefined' && chrome.storage?.local) {
      const stored = await chrome.storage.local.get(['agentPolicySettings']);
      policy = normalizeAgentPolicySettings(stored.agentPolicySettings || {});
    }
  } catch (_) {}
  setAgentPolicyForm(policy);
  if (window.AgentLoop?.savePolicy) await window.AgentLoop.savePolicy(policy);
}

async function saveAgentPolicySettings() {
  try {
    const policy = readAgentPolicyForm();
    if (typeof chrome !== 'undefined' && chrome.storage?.local) {
      await chrome.storage.local.set({ agentPolicySettings: policy });
    }
    if (window.AgentLoop?.savePolicy) await window.AgentLoop.savePolicy(policy);
    showPolicyResult('✅ 权限策略已保存');
  } catch (e) {
    showPolicyResult('保存失败：' + (e.message || String(e)), false);
  }
}

async function resetAgentPolicySettings() {
  setAgentPolicyForm(DEFAULT_AGENT_POLICY_SETTINGS);
  await saveAgentPolicySettings();
}

async function callPanelNativeTool(tool, args = {}) {
  if (typeof chrome === 'undefined' || !chrome.runtime?.sendMessage) {
    return { ok: false, error: '当前不是插件运行环境' };
  }
  return await new Promise((resolve) => {
    chrome.runtime.sendMessage({ type: 'NATIVE_TOOL', tool, args }, (resp) => {
      if (chrome.runtime.lastError) {
        resolve({ ok: false, error: chrome.runtime.lastError.message });
        return;
      }
      if (resp?.ok) resolve(resp.data || { ok: true });
      else resolve({ ok: false, error: resp?.error || resp?.data?.error || '本地 Agent 不可用' });
    });
  });
}

function renderSkillBenchmarkResult(result) {
  const el = document.getElementById('skillBenchmarkResult');
  if (!el) return;
  if (!result?.ok) {
    el.style.display = 'block';
    el.className = 'skill-benchmark-result err';
    el.textContent = '评测失败：' + (result?.error || '未知错误');
    return;
  }
  const weak = (result.items || []).filter(item => !item.passed).slice(0, 6);
  el.style.display = 'block';
  el.className = 'skill-benchmark-result';
  el.innerHTML = `
    <div><strong>总数：</strong>${escHtml(result.count)}　<strong>通过：</strong>${escHtml(result.passed)}　<strong>未过：</strong>${escHtml(result.failed)}　<strong>均分：</strong>${escHtml(result.avgScore)}</div>
    ${weak.length ? `<div style="margin-top:6px;"><strong>优先修复：</strong>${weak.map(item => `${escHtml(item.name)}(${escHtml(item.score)})`).join('、')}</div>` : '<div style="margin-top:6px;">暂无低于阈值的 Skill。</div>'}
  `;
}

async function runSkillBenchmark() {
  const el = document.getElementById('skillBenchmarkResult');
  if (el) {
    el.style.display = 'block';
    el.className = 'skill-benchmark-result';
    el.textContent = '正在评测本地 Skills...';
  }
  const minScore = Number(document.getElementById('skillBenchmarkMinScore')?.value || 70);
  const result = await callPanelNativeTool('skill.benchmark', {
    minScore
  });
  renderSkillBenchmarkResult(result?.result || result);
}

// ==============================
// 自动化任务设置
// ==============================
let automationEditingId = '';
let automationTasksCache = [];

const AUTOMATION_DOW_TO_CRON = { 0: 'SUN', 1: 'MON', 2: 'TUE', 3: 'WED', 4: 'THU', 5: 'FRI', 6: 'SAT' };
const AUTOMATION_DOW_LABEL = { 0: '周日', 1: '周一', 2: '周二', 3: '周三', 4: '周四', 5: '周五', 6: '周六' };
const DEFAULT_AUTOMATION_INSTRUCTION = '读取当前页面，整理重点信息，然后通知我。';
const DEFAULT_AUTOMATION_STEPS = [
  { tool: 'assistant.run', args: { instruction: DEFAULT_AUTOMATION_INSTRUCTION } },
  { tool: 'notify', args: { title: '詹老师的AIPROS自动化完成', message: '{{task.name}} 已完成' } }
];

function pad2(n) {
  return String(Math.max(0, Number(n) || 0)).padStart(2, '0');
}

function clampNumber(value, min, max, fallback) {
  const n = Number(value);
  if (!Number.isFinite(n)) return fallback;
  return Math.max(min, Math.min(max, Math.round(n)));
}

function parseAutomationTime(value) {
  const match = String(value || '09:00').match(/^(\d{1,2}):(\d{2})$/);
  const hour = clampNumber(match?.[1], 0, 23, 9);
  const minute = clampNumber(match?.[2], 0, 59, 0);
  return { hour, minute, text: `${pad2(hour)}:${pad2(minute)}` };
}

function getAutomationSelectedWeekdays() {
  return Array.from(document.querySelectorAll('.automation-weekday-btn.active'))
    .map(btn => Number(btn.dataset.weekday))
    .filter(n => Number.isFinite(n))
    .sort((a, b) => {
      const orderA = a === 0 ? 7 : a;
      const orderB = b === 0 ? 7 : b;
      return orderA - orderB;
    });
}

function setAutomationSelectedWeekdays(days = [1]) {
  const normalized = new Set((days || []).map(Number));
  document.querySelectorAll('.automation-weekday-btn').forEach(btn => {
    const day = Number(btn.dataset.weekday);
    btn.classList.toggle('active', normalized.has(day));
  });
}

function readAutomationScheduleControls() {
  const preset = document.getElementById('automationSchedulePreset')?.value || 'daily';
  const time = parseAutomationTime(document.getElementById('automationTime')?.value || '09:00');
  const monthDay = clampNumber(document.getElementById('automationMonthDay')?.value, 1, 31, 1);
  const intervalUnit = document.getElementById('automationIntervalUnit')?.value || 'minutes';
  const maxInterval = intervalUnit === 'hours' ? 23 : 59;
  const intervalValue = clampNumber(document.getElementById('automationIntervalValue')?.value, 1, maxInterval, intervalUnit === 'hours' ? 1 : 30);
  return {
    preset,
    time: time.text,
    hour: time.hour,
    minute: time.minute,
    weekdays: getAutomationSelectedWeekdays(),
    monthDay,
    intervalValue,
    intervalUnit
  };
}

function cronRuleFromSchedule(schedule) {
  const minute = schedule.minute;
  const hour = schedule.hour;
  if (schedule.preset === 'daily') return `${minute} ${hour} * * *`;
  if (schedule.preset === 'weekdays') return `${minute} ${hour} * * MON-FRI`;
  if (schedule.preset === 'weekly') {
    if (!schedule.weekdays.length) throw new Error('请选择每周的执行日');
    const days = schedule.weekdays.map(d => AUTOMATION_DOW_TO_CRON[d]).join(',');
    return `${minute} ${hour} * * ${days}`;
  }
  if (schedule.preset === 'monthly') return `${minute} ${hour} ${schedule.monthDay} * *`;
  if (schedule.preset === 'interval') {
    if (schedule.intervalUnit === 'hours') return `${minute} */${schedule.intervalValue} * * *`;
    return `*/${schedule.intervalValue} * * * *`;
  }
  return '';
}

function describeAutomationSchedule(schedule) {
  if (schedule.preset === 'daily') return `每天 ${schedule.time} 自动运行`;
  if (schedule.preset === 'weekdays') return `每个工作日 ${schedule.time} 自动运行`;
  if (schedule.preset === 'weekly') {
    const days = schedule.weekdays.map(d => AUTOMATION_DOW_LABEL[d]).join('、') || '未选择日期';
    return `每周 ${days} ${schedule.time} 自动运行`;
  }
  if (schedule.preset === 'monthly') return `每月第 ${schedule.monthDay} 天 ${schedule.time} 自动运行`;
  if (schedule.preset === 'interval') {
    const unit = schedule.intervalUnit === 'hours' ? '小时' : '分钟';
    return `每隔 ${schedule.intervalValue} ${unit}自动运行`;
  }
  return '按时间自动运行';
}

function syncAutomationScheduleToCron() {
  const hiddenCron = document.getElementById('automationCronRule');
  const summary = document.getElementById('automationScheduleSummary');
  const schedule = readAutomationScheduleControls();
  try {
    const rule = cronRuleFromSchedule(schedule);
    if (hiddenCron) hiddenCron.value = rule;
    if (summary) summary.textContent = describeAutomationSchedule(schedule);
    return { rule, schedule, summary: describeAutomationSchedule(schedule) };
  } catch (e) {
    if (hiddenCron) hiddenCron.value = '';
    if (summary) summary.textContent = e.message || '请完善时间设置';
    return { rule: '', schedule, summary: e.message || '' };
  }
}

function updateAutomationScheduleVisibility() {
  const preset = document.getElementById('automationSchedulePreset')?.value || 'daily';
  const weekWrap = document.getElementById('automationWeekdayWrap');
  const monthWrap = document.getElementById('automationMonthWrap');
  const intervalWrap = document.getElementById('automationIntervalWrap');
  const timeInput = document.getElementById('automationTime');
  const intervalInput = document.getElementById('automationIntervalValue');
  const intervalUnit = document.getElementById('automationIntervalUnit')?.value || 'minutes';
  if (weekWrap) weekWrap.style.display = preset === 'weekly' ? 'flex' : 'none';
  if (monthWrap) monthWrap.style.display = preset === 'monthly' ? 'grid' : 'none';
  if (intervalWrap) intervalWrap.style.display = preset === 'interval' ? 'grid' : 'none';
  if (timeInput) timeInput.style.display = preset === 'interval' ? 'none' : 'block';
  if (intervalInput) {
    intervalInput.max = intervalUnit === 'hours' ? '23' : '59';
    if (Number(intervalInput.value) > Number(intervalInput.max)) intervalInput.value = intervalInput.max;
  }
  syncAutomationScheduleToCron();
}

function extractFirstUrl(text = '') {
  const match = String(text).match(/https?:\/\/[^\s，。；;）)]+/i);
  return match ? match[0] : '';
}

function extractUrlPatternFromInstruction(instruction = '') {
  const url = extractFirstUrl(instruction);
  if (!url) return '';
  try {
    return new URL(url).hostname;
  } catch (_) {
    return url.replace(/^https?:\/\//i, '').split('/')[0];
  }
}

function deriveAutomationName(instruction = '') {
  const compact = String(instruction)
    .replace(/https?:\/\/[^\s，。；;）)]+/ig, '')
    .replace(/\s+/g, ' ')
    .trim();
  return (compact || '新的自动化任务').slice(0, 24);
}

function detectAutomationSkill(instruction = '') {
  const text = String(instruction).toLowerCase();
  let bestSkill = '';
  let bestScore = 0;
  for (const filename of AISC_BUILTIN_SKILL_FILES) {
    const intent = _SKILL_INTENT_MAP[filename] || {};
    let score = 0;
    [filename, intent.name].filter(Boolean).forEach(item => {
      if (text.includes(String(item).toLowerCase())) score += 100;
    });
    (intent.keywords || []).forEach((item, index) => {
      if (text.includes(String(item).toLowerCase())) score += Math.max(5, 30 - index);
    });
    if (score > bestScore) {
      bestScore = score;
      bestSkill = filename;
    }
  }
  return bestSkill;
}

function safeAutomationFileBase(name = '') {
  return String(name || '詹老师的AIPROS自动化结果')
    .replace(/[\\/:*?"<>|]+/g, '_')
    .replace(/\s+/g, '_')
    .slice(0, 40) || '詹老师的AIPROS自动化结果';
}

function extractAutomationOutputPath(instruction = '', taskName = '') {
  const text = String(instruction || '');
  const explicit = text.match(/(?:保存到|写入到|输出到|写到)\s*([~\/][^\s，。；;]+)/);
  if (explicit) return explicit[1];
  if (/保存|写入|输出|文件|磁盘/.test(text)) {
    return `~/Desktop/${safeAutomationFileBase(taskName)}.md`;
  }
  return '';
}

function buildAutomationSteps(instruction, triggerType, taskName) {
  const steps = [];
  const url = extractFirstUrl(instruction);
  const wantsPage = /当前页面|这个页面|网页|网站|页面|打开|读取|抓取|获取|竞品|URL/i.test(instruction);
  const skillName = detectAutomationSkill(instruction);
  const runInstruction = `${instruction}\n\n自动化执行要求：如果材料不足，请基于已有信息执行，并把缺口标为“待补充”。`;

  if (url) {
    steps.push({ tool: 'browser.open', args: { url, newTab: true } });
    steps.push({ tool: 'browser.getText', args: { selector: 'body', maxLength: 8000 } });
  } else if (triggerType === 'url' || wantsPage) {
    steps.push({ tool: 'page.read', args: {} });
  }

  if (skillName) {
    steps.push({ tool: 'skill.run', args: { skill: skillName, instruction: runInstruction } });
  } else {
    steps.push({ tool: 'assistant.run', args: { instruction: runInstruction } });
  }

  const outputPath = extractAutomationOutputPath(instruction, taskName);
  if (outputPath) {
    steps.push({ tool: 'fs.write', args: { path: outputPath, content: '{{last.text}}' } });
  }
  steps.push({ tool: 'notify', args: { title: '詹老师的AIPROS自动化完成', message: '{{task.name}} 已完成' } });
  return steps;
}

function summarizeLegacyAutomationSteps(steps = []) {
  const tools = (steps || []).map(s => s.tool || s.type || s.action).filter(Boolean);
  if (!tools.length) return '';
  return `旧任务：按已保存的 ${tools.join(' → ')} 流程执行。`;
}

function automationMessage(message) {
  return new Promise((resolve) => {
    if (typeof chrome === 'undefined' || !chrome.runtime?.sendMessage) {
      resolve({ ok: false, error: '当前不是插件运行环境' });
      return;
    }
    chrome.runtime.sendMessage(message, (resp) => {
      if (chrome.runtime.lastError) {
        resolve({ ok: false, error: chrome.runtime.lastError.message });
        return;
      }
      resolve(resp || { ok: false, error: '无响应' });
    });
  });
}

function queueStatusLabel(status = '') {
  return {
    queued: '排队',
    running: '执行中',
    success: '成功',
    error: '失败',
    canceled: '已取消'
  }[status] || status || '未知';
}

function renderAutomationQueue(queue = []) {
  const list = document.getElementById('automationQueueList');
  const summary = document.getElementById('automationQueueSummary');
  const active = queue.filter(item => !['success', 'canceled'].includes(item.status));
  const running = queue.filter(item => item.status === 'running').length;
  const queued = queue.filter(item => item.status === 'queued').length;
  const error = queue.filter(item => item.status === 'error').length;
  if (summary) summary.textContent = `排队 ${queued} 个 · 执行中 ${running} 个 · 失败 ${error} 个 · 最近记录 ${queue.length} 条`;
  if (!list) return;
  if (!queue.length) {
    list.innerHTML = '<div class="kb-empty">暂无队列记录</div>';
    return;
  }
  const visible = (active.length ? active : queue).slice(0, 8);
  list.innerHTML = visible.map(item => {
    const time = item.updatedAt ? new Date(item.updatedAt).toLocaleString('zh-CN', { month:'2-digit', day:'2-digit', hour:'2-digit', minute:'2-digit' }) : '';
    return `
      <div class="automation-queue-item">
        <div>
          <div class="automation-queue-title">${escHtml(item.taskName || item.taskId || '未命名任务')}</div>
          <div class="automation-queue-meta">${escHtml(item.trigger || '')} · 第 ${escHtml(item.attempts || 0)}/${escHtml(item.maxAttempts || 1)} 次 · ${escHtml(time)}${item.error ? ' · ' + escHtml(item.error) : ''}</div>
        </div>
        <span class="automation-queue-status ${escHtml(item.status)}">${escHtml(queueStatusLabel(item.status))}</span>
      </div>
    `;
  }).join('');
}

async function loadAutomationQueue() {
  const resp = await automationMessage({ type: 'AUTOMATION_QUEUE', includeDone: true });
  if (!resp.ok) {
    const list = document.getElementById('automationQueueList');
    if (list) list.innerHTML = '<div class="skill-list-error">队列读取失败：' + escHtml(resp.error || '') + '</div>';
    return;
  }
  renderAutomationQueue(resp.data?.queue || []);
}

async function processAutomationQueue() {
  const summary = document.getElementById('automationQueueSummary');
  if (summary) summary.textContent = '正在继续执行队列...';
  const resp = await automationMessage({ type: 'AUTOMATION_QUEUE_PROCESS', limit: 3 });
  if (!resp.ok) {
    if (summary) summary.textContent = '队列执行失败：' + (resp.error || '');
    return;
  }
  renderAutomationQueue(resp.data?.queue || []);
}

async function loadAgentAuditList() {
  const list = document.getElementById('agentAuditList');
  if (!list || !window.AgentLoop?.listAudit) return;
  const audits = await window.AgentLoop.listAudit({ limit: 8 });
  if (!audits.length) {
    list.innerHTML = '<div class="kb-empty">暂无审计记录</div>';
    return;
  }
  list.innerHTML = audits.map(item => {
    const time = item.updatedAt ? new Date(item.updatedAt).toLocaleString('zh-CN', { month:'2-digit', day:'2-digit', hour:'2-digit', minute:'2-digit' }) : '';
    return `
      <div class="automation-queue-item">
        <div>
          <div class="automation-queue-title">${escHtml(item.title || 'Agent 执行任务')}</div>
          <div class="automation-queue-meta">${escHtml(time)} · 工具 ${escHtml(item.toolCount || 0)} · 权限 ${escHtml(item.permissionCount || 0)} · 回滚点 ${escHtml(item.checkpointCount || 0)}</div>
        </div>
        <button class="automation-link-btn" data-audit-run="${escHtml(item.runId)}">回放</button>
      </div>
    `;
  }).join('');
  list.querySelectorAll('[data-audit-run]').forEach(btn => {
    btn.addEventListener('click', async () => {
      await window.AgentLoop.replayAudit(btn.dataset.auditRun);
      showToast('已回放审计记录');
    });
  });
}

function updateAutomationTriggerVisibility() {
  const type = document.getElementById('automationTriggerType')?.value || 'manual';
  const scheduleWrap = document.getElementById('automationScheduleWrap');
  const urlWrap = document.getElementById('automationUrlWrap');
  if (scheduleWrap) scheduleWrap.style.display = type === 'cron' ? 'block' : 'none';
  if (urlWrap) urlWrap.style.display = type === 'url' ? 'block' : 'none';
  updateAutomationScheduleVisibility();
}

function resetAutomationForm() {
  automationEditingId = '';
  const name = document.getElementById('automationName');
  const instruction = document.getElementById('automationInstruction');
  const enabled = document.getElementById('automationEnabled');
  const triggerType = document.getElementById('automationTriggerType');
  const cron = document.getElementById('automationCronRule');
  const url = document.getElementById('automationUrlPattern');
  const steps = document.getElementById('automationSteps');
  const preset = document.getElementById('automationSchedulePreset');
  const time = document.getElementById('automationTime');
  const monthDay = document.getElementById('automationMonthDay');
  const intervalValue = document.getElementById('automationIntervalValue');
  const intervalUnit = document.getElementById('automationIntervalUnit');
  if (name) name.value = '';
  if (instruction) instruction.value = '';
  if (enabled) enabled.checked = true;
  if (triggerType) triggerType.value = 'manual';
  if (preset) preset.value = 'daily';
  if (time) time.value = '09:00';
  if (monthDay) monthDay.value = '1';
  if (intervalValue) intervalValue.value = '30';
  if (intervalUnit) intervalUnit.value = 'minutes';
  setAutomationSelectedWeekdays([1]);
  if (cron) cron.value = '0 9 * * *';
  if (url) url.value = '';
  if (steps) steps.value = JSON.stringify(DEFAULT_AUTOMATION_STEPS, null, 2);
  updateAutomationTriggerVisibility();
  showAutomationResult('', null, false);
}

function readAutomationForm() {
  const nameEl = document.getElementById('automationName');
  let name = nameEl?.value.trim() || '';
  const instruction = document.getElementById('automationInstruction')?.value.trim() || '';
  const enabled = document.getElementById('automationEnabled')?.checked !== false;
  const type = document.getElementById('automationTriggerType')?.value || 'manual';
  const scheduleState = syncAutomationScheduleToCron();
  const existingTask = automationEditingId ? automationTasksCache.find(t => t.id === automationEditingId) : null;
  const preservedTimeZone = type === 'cron'
    ? (existingTask?.trigger?.timeZone || existingTask?.schedule?.timeZone || '')
    : '';
  const cronDescription = preservedTimeZone === 'Asia/Shanghai'
    ? scheduleState.summary.replace('自动运行', '（北京时间）自动运行')
    : preservedTimeZone
      ? `${scheduleState.summary}（${preservedTimeZone}）`
      : scheduleState.summary;
  let urlPattern = document.getElementById('automationUrlPattern')?.value.trim() || '';
  if (!instruction || instruction.replace(/\s+/g, '').length < 8) {
    throw new Error('请用一句话写清楚要做什么，例如：每天早上9点打开竞品网页，总结更新并通知我');
  }
  if (!name) {
    name = deriveAutomationName(instruction);
    if (nameEl) nameEl.value = name;
  }
  if (type === 'cron' && !scheduleState.rule) throw new Error('请完善时间设置');
  if (type === 'url' && !urlPattern) urlPattern = extractUrlPatternFromInstruction(instruction);
  if (type === 'url' && !urlPattern) throw new Error('请填写 URL 正则或关键词');
  const steps = buildAutomationSteps(instruction, type, name);
  const stepsEl = document.getElementById('automationSteps');
  if (stepsEl) stepsEl.value = JSON.stringify(steps, null, 2);
  return {
    id: automationEditingId || undefined,
    name,
    enabled,
    instruction,
    sourceUrl: extractFirstUrl(instruction),
    schedule: type === 'cron' ? { ...scheduleState.schedule, ...(preservedTimeZone ? { timeZone: preservedTimeZone } : {}) } : null,
    trigger: {
      type,
      rule: type === 'cron' ? scheduleState.rule : '',
      urlPattern: type === 'url' ? urlPattern : '',
      timeZone: preservedTimeZone,
      description: type === 'cron' ? cronDescription : type === 'url' ? `打开包含「${urlPattern}」的网页时运行` : '手动运行'
    },
    steps
  };
}

function showAutomationResult(msg, ok, visible = true) {
  const el = document.getElementById('automationResult');
  if (!el) return;
  el.style.display = visible && msg ? 'block' : 'none';
  el.textContent = msg || '';
  el.className = 'install-result ' + (ok === true ? 'ok' : ok === false ? 'err' : 'pending');
}

async function loadAutomationPanel() {
  if (!document.getElementById('automationSteps')?.value.trim()) resetAutomationForm();
  updateAutomationTriggerVisibility();
  const list = document.getElementById('automationList');
  const logList = document.getElementById('automationLogList');
  if (list) list.innerHTML = '<div class="skill-list-loading">加载中...</div>';
  const resp = await automationMessage({ type: 'AUTOMATION_LIST' });
  if (!resp.ok) {
    if (list) list.innerHTML = '<div class="skill-list-error">加载失败：' + escHtml(resp.error || '') + '</div>';
    return;
  }
  automationTasksCache = resp.data?.tasks || [];
  renderAutomationTasks(automationTasksCache);
  renderAutomationLogs(resp.data?.logs || []);
  await loadAutomationQueue();
  await loadAgentAuditList();
  if (logList && !(resp.data?.logs || []).length) logList.innerHTML = '<div class="kb-empty">暂无运行记录</div>';
}

function scheduleFromCronRule(rule = '') {
  const parts = String(rule || '').trim().split(/\s+/);
  if (parts.length !== 5) return null;
  const [minute, hour, day, _month, dow] = parts;
  const time = `${pad2(hour)}:${pad2(minute)}`;
  const base = {
    preset: 'daily',
    time,
    hour: clampNumber(hour, 0, 23, 9),
    minute: clampNumber(minute, 0, 59, 0),
    weekdays: [1],
    monthDay: 1,
    intervalValue: 30,
    intervalUnit: 'minutes'
  };
  if (/^\*\/\d+$/.test(minute)) {
    return { ...base, preset: 'interval', intervalUnit: 'minutes', intervalValue: clampNumber(minute.slice(2), 1, 59, 30) };
  }
  if (/^\*\/\d+$/.test(hour)) {
    return { ...base, preset: 'interval', intervalUnit: 'hours', intervalValue: clampNumber(hour.slice(2), 1, 23, 1) };
  }
  if (dow === 'MON-FRI') return { ...base, preset: 'weekdays' };
  if (dow && dow !== '*') {
    const byCron = Object.entries(AUTOMATION_DOW_TO_CRON).reduce((acc, [k, v]) => ({ ...acc, [v]: Number(k) }), {});
    const weekdays = dow.split(',').map(item => byCron[item]).filter(n => Number.isFinite(n));
    return { ...base, preset: 'weekly', weekdays: weekdays.length ? weekdays : [1] };
  }
  if (day && day !== '*') return { ...base, preset: 'monthly', monthDay: clampNumber(day, 1, 31, 1) };
  return base;
}

function applyAutomationScheduleToForm(task = {}) {
  const schedule = task.schedule || scheduleFromCronRule(task.trigger?.rule) || {
    preset: 'daily',
    time: '09:00',
    weekdays: [1],
    monthDay: 1,
    intervalValue: 30,
    intervalUnit: 'minutes'
  };
  const preset = document.getElementById('automationSchedulePreset');
  const time = document.getElementById('automationTime');
  const monthDay = document.getElementById('automationMonthDay');
  const intervalValue = document.getElementById('automationIntervalValue');
  const intervalUnit = document.getElementById('automationIntervalUnit');
  if (preset) preset.value = schedule.preset || 'daily';
  if (time) time.value = schedule.time || '09:00';
  if (monthDay) monthDay.value = schedule.monthDay || 1;
  if (intervalValue) intervalValue.value = schedule.intervalValue || 30;
  if (intervalUnit) intervalUnit.value = schedule.intervalUnit || 'minutes';
  setAutomationSelectedWeekdays(schedule.weekdays?.length ? schedule.weekdays : [1]);
  updateAutomationScheduleVisibility();
}

function automationTriggerLabel(task = {}) {
  if (task.trigger?.type === 'cron') {
    if (task.trigger.description) return task.trigger.description;
    if (task.schedule) return describeAutomationSchedule(task.schedule);
    return '按时间自动运行';
  }
  if (task.trigger?.type === 'url') {
    return task.trigger.description || `打开包含「${task.trigger.urlPattern || task.trigger.rule || ''}」的网页时运行`;
  }
  return '手动运行';
}

function renderAutomationTasks(tasks) {
  const list = document.getElementById('automationList');
  if (!list) return;
  if (!tasks.length) {
    list.innerHTML = '<div class="kb-empty">暂无自动化任务</div>';
    return;
  }
  list.innerHTML = '';
  tasks.forEach(task => {
    const item = document.createElement('div');
    item.className = 'automation-item';
    const triggerLabel = automationTriggerLabel(task);
    const status = task.lastStatus === 'success' ? '成功' : task.lastStatus === 'error' ? '失败' : '未运行';
    const instructionLabel = task.instruction ? ' · ' + task.instruction.slice(0, 26) : '';
    item.innerHTML = `
      <div class="automation-item-main">
        <div class="automation-item-title">${escHtml(task.name)}</div>
        <div class="automation-item-meta">${escHtml(triggerLabel)} · ${status}${escHtml(instructionLabel)}</div>
      </div>
      <div class="automation-item-actions">
        <button data-action="toggle" data-id="${escHtml(task.id)}">${task.enabled ? '暂停' : '启用'}</button>
        <button data-action="run" data-id="${escHtml(task.id)}">运行</button>
        <button data-action="edit" data-id="${escHtml(task.id)}">编辑</button>
        <button data-action="delete" data-id="${escHtml(task.id)}">删除</button>
      </div>
    `;
    if (!task.enabled) item.classList.add('disabled');
    list.appendChild(item);
  });
  list.querySelectorAll('button[data-action]').forEach(btn => {
    btn.addEventListener('click', () => handleAutomationAction(btn.dataset.action, btn.dataset.id));
  });
}

function renderAutomationLogs(logs) {
  const list = document.getElementById('automationLogList');
  if (!list) return;
  if (!logs.length) {
    list.innerHTML = '<div class="kb-empty">暂无运行记录</div>';
    return;
  }
  list.innerHTML = '';
  logs.slice(0, 12).forEach(log => {
    const row = document.createElement('div');
    row.className = 'automation-log ' + (log.status || 'info');
    const time = log.createdAt ? new Date(log.createdAt).toLocaleString('zh-CN', { month:'2-digit', day:'2-digit', hour:'2-digit', minute:'2-digit' }) : '';
    row.innerHTML = `
      <span class="automation-log-time">${escHtml(time)}</span>
      <span class="automation-log-status">${escHtml(log.status || '')}</span>
      <span class="automation-log-text">${escHtml(log.taskName || '自动化')}：${escHtml(log.message || '')}</span>
    `;
    list.appendChild(row);
  });
}

async function saveAutomationFromForm() {
  try {
    const task = readAutomationForm();
    showAutomationResult('保存中...', null);
    const resp = await automationMessage({ type: 'AUTOMATION_SAVE', task });
    if (!resp.ok) throw new Error(resp.error || '保存失败');
    automationEditingId = resp.data?.task?.id || task.id || '';
    showAutomationResult('✅ 自动化任务已保存', true);
    await loadAutomationPanel();
  } catch (e) {
    showAutomationResult('保存失败：' + e.message, false);
  }
}

async function runAutomationDraft() {
  try {
    const task = readAutomationForm();
    showAutomationResult('正在试运行，Chrome 可能会显示调试提示条...', null);
    const resp = await automationMessage({ type: 'AUTOMATION_RUN_DRAFT', task });
    if (!resp.ok) throw new Error(resp.error || '运行失败');
    showAutomationResult('✅ 试运行完成', true);
    await loadAutomationPanel();
  } catch (e) {
    showAutomationResult('运行失败：' + e.message, false);
  }
}

async function handleAutomationAction(action, id) {
  const task = automationTasksCache.find(t => t.id === id);
  try {
    if (action === 'edit' && task) {
      automationEditingId = task.id;
      document.getElementById('automationName').value = task.name || '';
      document.getElementById('automationInstruction').value = task.instruction || summarizeLegacyAutomationSteps(task.steps || []);
      document.getElementById('automationEnabled').checked = task.enabled !== false;
      document.getElementById('automationTriggerType').value = task.trigger?.type || 'manual';
      document.getElementById('automationCronRule').value = task.trigger?.rule || '0 9 * * MON-FRI';
      document.getElementById('automationUrlPattern').value = task.trigger?.urlPattern || '';
      document.getElementById('automationSteps').value = JSON.stringify(task.steps || DEFAULT_AUTOMATION_STEPS, null, 2);
      applyAutomationScheduleToForm(task);
      updateAutomationTriggerVisibility();
      showAutomationResult('已载入任务，可修改后保存', null);
      return;
    }
    if (action === 'run') {
      showAutomationResult('正在运行：' + (task?.name || id), null);
      const resp = await automationMessage({ type: 'AUTOMATION_RUN', id });
      if (!resp.ok) throw new Error(resp.error || '运行失败');
      showAutomationResult('✅ 任务运行完成', true);
    } else if (action === 'toggle') {
      const resp = await automationMessage({ type: 'AUTOMATION_TOGGLE', id, enabled: !(task?.enabled !== false) });
      if (!resp.ok) throw new Error(resp.error || '切换失败');
      showToast(task?.enabled ? '已暂停自动化任务' : '已启用自动化任务');
    } else if (action === 'delete') {
      if (!confirm('确定删除这个自动化任务？')) return;
      const resp = await automationMessage({ type: 'AUTOMATION_DELETE', id });
      if (!resp.ok) throw new Error(resp.error || '删除失败');
      showToast('已删除自动化任务');
      if (automationEditingId === id) resetAutomationForm();
    }
    await loadAutomationPanel();
  } catch (e) {
    showAutomationResult(e.message || String(e), false);
  }
}

// ==============================
// 知识库管理 — 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 escAttr(s) {
  return escHtml(s).replace(/"/g, '&quot;');
}

/** 获取文件图标 */
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');
  }
}

// ==============================
// 输入框粘贴 / 拖放附件
// ==============================
function dataTransferHasFileLike(dataTransfer) {
  if (!dataTransfer) return false;
  if (dataTransfer.files && dataTransfer.files.length > 0) return true;
  return Array.from(dataTransfer.items || []).some(item => {
    if (item.kind === 'file') return true;
    try { return !!item.webkitGetAsEntry?.(); } catch (_) { return false; }
  });
}

function readDirectoryEntries(reader) {
  return new Promise((resolve, reject) => {
    const entries = [];
    const readBatch = () => {
      reader.readEntries(batch => {
        if (!batch.length) {
          resolve(entries);
          return;
        }
        entries.push(...batch);
        readBatch();
      }, reject);
    };
    readBatch();
  });
}

function readEntryFile(entry) {
  return new Promise((resolve, reject) => {
    entry.file(resolve, reject);
  });
}

async function collectEntryFiles(entry, parentPath = '', acc = []) {
  if (!entry || acc.length >= ATTACHMENT_MAX_FILES) return acc;
  const entryPath = parentPath ? `${parentPath}/${entry.name}` : entry.name;

  if (entry.isFile) {
    const file = await readEntryFile(entry);
    acc.push({ file, path: entryPath });
    return acc;
  }

  if (entry.isDirectory) {
    const reader = entry.createReader();
    const entries = await readDirectoryEntries(reader);
    for (const child of entries) {
      if (acc.length >= ATTACHMENT_MAX_FILES) break;
      await collectEntryFiles(child, entryPath, acc);
    }
  }
  return acc;
}

async function collectFilesFromDataTransfer(dataTransfer) {
  if (!dataTransfer) return [];
  const items = Array.from(dataTransfer.items || []);
  const entries = items
    .map(item => {
      try { return item.webkitGetAsEntry?.() || null; } catch (_) { return null; }
    })
    .filter(Boolean);

  if (entries.length) {
    const files = [];
    for (const entry of entries) {
      if (files.length >= ATTACHMENT_MAX_FILES) break;
      await collectEntryFiles(entry, '', files);
    }
    return files;
  }

  const directFiles = Array.from(dataTransfer.files || []);
  if (directFiles.length) {
    return directFiles.map(file => ({ file, path: file.webkitRelativePath || file.name }));
  }

  return items
    .filter(item => item.kind === 'file')
    .map(item => item.getAsFile())
    .filter(Boolean)
    .map(file => ({ file, path: file.name }));
}

async function attachFromDataTransfer(dataTransfer, source = '粘贴') {
  const files = await collectFilesFromDataTransfer(dataTransfer);
  if (!files.length) {
    showToast('没有发现可添加的文件', 'warn');
    return 0;
  }
  return addAttachmentFiles(files, source);
}

function handleAttachmentPaste(e) {
  if (!dataTransferHasFileLike(e.clipboardData)) return;
  e.preventDefault();
  attachFromDataTransfer(e.clipboardData, '粘贴').catch(err => {
    console.warn('[附件] 粘贴失败:', err);
    showToast('附件粘贴失败：' + (err.message || '未知错误'), 'warn');
  });
}

function setAttachmentDropState(active) {
  document.querySelector('.chat-input-area')?.classList.toggle('is-attachment-dragging', !!active);
}

function handleAttachmentDragEnter(e) {
  if (!dataTransferHasFileLike(e.dataTransfer)) return;
  e.preventDefault();
  setAttachmentDropState(true);
}

function handleAttachmentDragOver(e) {
  if (!dataTransferHasFileLike(e.dataTransfer)) return;
  e.preventDefault();
  e.dataTransfer.dropEffect = 'copy';
  setAttachmentDropState(true);
}

function handleAttachmentDragLeave(e) {
  if (e.currentTarget?.contains(e.relatedTarget)) return;
  setAttachmentDropState(false);
}

function handleAttachmentDrop(e) {
  if (!dataTransferHasFileLike(e.dataTransfer)) return;
  e.preventDefault();
  setAttachmentDropState(false);
  attachFromDataTransfer(e.dataTransfer, '拖放').catch(err => {
    console.warn('[附件] 拖放失败:', err);
    showToast('附件拖放失败：' + (err.message || '未知错误'), 'warn');
  });
}

// ==============================
// 提取图片附件数组（多模态发送用）
// ==============================
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 的部分压进滚动摘要
 * 核心思路：
 * - 最近 6 条消息保留原文（约3轮完整上下文）
 * - 更早的消息每条提取标题、结论、业务对象和关键字段
 * - 摘要按行滚动裁剪，控制长期会话的上下文体积
 */
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) {
    const lines = chatHistorySummary.split('\n').filter(Boolean);
    while (lines.join('\n').length > MAX_SUMMARY_LENGTH && lines.length > 1) {
      lines.shift();
    }
    chatHistorySummary = lines.join('\n');
    if (chatHistorySummary.length > MAX_SUMMARY_LENGTH) {
      chatHistorySummary = chatHistorySummary.slice(-MAX_SUMMARY_LENGTH);
    }
  }
}

/**
 * 压缩单条消息：提取核心内容
 * 策略：
 * 1. 取第一句话（通常是最重要的结论/问题）
 * 2. 提取 Markdown 标题
 * 3. 提取加粗关键词
 * 4. 保留数字、URL、文件名、任务名等可复用线索
 * 5. 限制在 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 durableSignals = text.match(/(?:https?:\/\/[^\s)）]+|[\w.-]+\.(?:html|pdf|docx?|xlsx?|csv|json|md|png|jpe?g)|\b[A-Z][A-Z0-9_-]{2,}\b|[\d.]+%?|第[一二三四五六七八九十\d]+[阶段步轮版])/g) || [];
  if (durableSignals.length > 0) {
    parts.push('线索:' + [...new Set(durableSignals)].slice(0, 6).join(','));
  }

  // 4. 取文本开头（结论/问题）
  const cleanText = text.replace(/#{1,4}\s+.+$/gm, '').replace(/\*\*[^*]+\*\*/g, '').replace(/```[\s\S]*?```/g, '').trim();
  const firstSentence = cleanText.match(/^[^。\n！？]{2,120}[。！？]?/) || [];
  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 = [];
  if (chatArtifacts.length > 0) {
    const artifactSummary = chatArtifacts.slice(0, 5).map((item, index) => {
      const typeText = item.format === 'html' ? 'HTML页面' : String(item.format || '交付物');
      const publicText = item.publicUrl ? `，公网地址：${item.publicUrl}` : '';
      return `${index + 1}. ${item.filename}（${typeText}，${item.size ? Math.max(1, Math.round(item.size / 1024)) + 'KB' : '已生成'}${publicText}）`;
    }).join('\n');
    context.push({
      role: 'user',
      content: `[本轮会话已生成的交付物，用户追问“在哪里看/打开/下载/刚才那个HTML”时必须引用：]\n${artifactSummary}`
    });
    context.push({
      role: 'assistant',
      content: '好的，我会记住这些交付物，并在用户追问时说明可通过交付物卡片打开或下载。'
    });
  }
  
  // 如果有摘要，作为一条 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);
  let usedChars = context.reduce((sum, msg) => sum + (msg.content?.length || 0), 0);
  historyToSend.forEach(msg => {
    if (usedChars >= MAX_HISTORY_CONTEXT_CHARS) return;
    const remaining = MAX_HISTORY_CONTEXT_CHARS - usedChars;
    const limit = Math.min(MAX_RECENT_MESSAGE_CHARS, remaining);
    if (limit <= 120) return;
    const rawContent = String(msg.content || '');
    const content = rawContent.length > limit
      ? rawContent.slice(0, Math.max(80, limit - 12)) + '\n...[已截断]'
      : rawContent;
    usedChars += content.length;
    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 = '语音输入';
  }
}
