/**
 * 小栈AI 全局对话弹窗 — Vanilla JS 实现 V3.1.6
 *
 * 功能:
 * - 主题适配层 (Zibll深度/通用降级)
 * - SSE流式对话 + 非流式降级
 * - 会话管理 + sessionStorage 浏览器会话持久化（关浏览器即清零）
 * - Proactive AI 前端行为监控 (scroll/idle/stay/exit/search)
 * - 未读消息红点徽章
 *
 * 加载: wp_footer / admin_footer footer scripts
 */
(function () {
  'use strict'

  if (window.__xzdChat) return
  window.__xzdChat = true

  // ═══════════════════════════════
  // 工具函数
  // ═══════════════════════════════
  const $ = (sel, ctx) => (ctx || document).querySelector(sel)
  const $$ = (sel, ctx) => [...(ctx || document).querySelectorAll(sel)]

  function escapeHtml(str) {
    const d = document.createElement('div')
    d.textContent = str
    return d.innerHTML
  }

  // V3.1.5: 安全读取 CSS 变量（优先 base64，兼容旧 json 格式，防止 SVG 属性双引号丢失）
  function readCssVar(name, fallback) {
    var raw = ''
    try { raw = window.getComputedStyle(document.documentElement).getPropertyValue(name + '-b64').trim() } catch(e) {}
    if (raw) {
      try { return atob(raw) } catch(e) { /* 非 base64 值，回退旧格式 */ }
    }
    // 回退：旧 json 编码格式（只去掉首尾 json 引号，保留内部双引号）
    raw = (window.getComputedStyle(document.documentElement).getPropertyValue(name) || '').trim()
    if (raw && raw.charAt(0) === '"' && raw.charAt(raw.length - 1) === '"') return raw.slice(1, -1)
    return raw || fallback
  }

  // 工具状态 → 中文标签
  const STATUS_LABELS = {
    pending: { icon: '⏳', text: '处理中' },
    confirm: { icon: '⚠️', text: '待确认' },
    success: { icon: '✅', text: '成功' },
    error:   { icon: '❌', text: '失败' },
    repeat:  { icon: '🔄', text: '已处理' },
    executing: { icon: '⚡', text: '执行中' },
    running: { icon: '🏃', text: '运行中' },
    completed: { icon: '✅', text: '已完成' },
    partial: { icon: '⚠️', text: '部分完成' },
    failed: { icon: '❌', text: '失败' },
    skipped: { icon: '⏭️', text: '已跳过' },
    excluded: { icon: '🚫', text: '已排除' },
    cooling: { icon: '❄️', text: '冷却中' },
    already_processed: { icon: '✔️', text: '已处理过' },
    scheduled: { icon: '📅', text: '已排期' },
  }

  // 工具ID → 中文名称映射
  const TOOL_NAMES = {
    generate_tags:'生成标签', generate_seo:'SEO优化', generate_summary:'生成摘要',
    polish_content:'内容润色', translate_content:'内容翻译', rewrite_content:'内容改写',
    expand_content:'内容扩展', build_autolink:'构建内链', score_quality:'质量评分',
    generate_cover:'生成封面', generate_alt:'生成ALT', check_duplicate:'去重检测',
    write_article:'AI写文章', wp_create_post:'创建文章', wp_update_post:'更新文章',
    wp_delete_post:'删除文章', wp_publish_post:'发布文章', wp_get_post:'查看文章',
    wp_search_posts:'搜索文章', wp_list_users:'列出用户', wp_get_user:'查询用户',
    wp_create_user:'创建用户', wp_update_user:'更新用户', wp_delete_user:'删除用户',
    wp_change_role:'修改角色', wp_site_stats:'站点统计', wp_manage_comments:'管理评论',
    wp_manage_categories:'管理分类', wp_manage_tags:'管理标签', wp_list_plugins:'查看插件',
    wp_get_settings:'查看设置', wp_clear_cache:'清除缓存', wp_reply_comment:'回复评论',
    theme_get_points:'查询积分', theme_add_points:'增加积分', theme_deduct_points:'扣减积分',
    theme_get_balance:'查询余额', theme_add_balance:'增加余额', theme_deduct_balance:'扣减余额',
    theme_get_vip:'查询VIP', theme_set_vip:'设置VIP', theme_remove_vip:'移除VIP',
    theme_check_ban:'检查封禁', theme_ban_user:'封禁用户', theme_unban_user:'解封用户',
    theme_send_notify:'发送通知', theme_get_checkin:'查询签到', theme_do_checkin:'执行签到',
    theme_get_medals:'查询勋章', theme_add_medal:'添加勋章', theme_remove_medal:'移除勋章',
    theme_get_orders:'查询订单', theme_get_invites:'查询邀请',
    score_seo:'SEO评分', generate_schema:'生成Schema', push_search:'搜索推送',
    scan_deadlink:'扫描死链', run_pipeline:'执行流水线',
    apply_watermark:'应用水印', localize_image:'图片本地化', compress_image:'压缩图片',
    search_stock:'搜索图库', generate_inline_image:'生成插图',
    search_knowledge:'搜索知识库', add_faq:'添加FAQ', delete_faq:'删除FAQ', rebuild_index:'重建索引',
    create_write_plan:'创建写文章计划', trigger_write_now:'立即写文章', view_queue:'查看队列',
    get_available_events:'获取触发事件', get_available_actions:'获取可用动作',
    create_automation_rule:'创建自动化规则', list_automation_rules:'列出规则',
    get_automation_rule:'查看规则', update_automation_rule:'修改规则',
    delete_automation_rule:'删除规则', toggle_automation_rule:'启用/禁用规则',
    run_automation_rule:'手动执行规则',
    get_pipeline_config:'查看流水线配置', toggle_pipeline_stage:'开关流水线阶段',
    toggle_pipeline_trigger:'开关流水线触发', set_pipeline_exclude:'设置流水线排除',
    list_tools_status:'查看工具状态', toggle_tool:'开关工具',
    toggle_tool_for_role:'开关角色工具', enable_tools_for_role:'批量启用工具',
    disable_tools_for_role:'批量禁用工具', set_all_tools_for_role:'全开全关工具',
    list_ai_connections:'查看AI连接', toggle_ai_connection:'开关AI连接',
    set_primary_connection:'设为主用连接', get_rate_config:'查看速率配置',
    set_rate_config:'设置速率限制', set_ai_identity:'设置AI身份',
    get_summary_config:'查看摘要配置', set_summary_config:'设置摘要显示',
    theme_bulk_notify:'批量通知', theme_send_webhook:'Webhook通知',
    theme_forum_plates:'论坛版块', theme_forum_search:'论坛搜索', theme_forum_stats:'论坛统计',
    // 内容处理
    process_text:'文本处理', generate_block:'生成块', content_shorten:'缩短内容',
    content_fixGrammar:'语法修正', expand_title:'标题扩写', editorial_notes:'编辑建议',
    restore_backup:'恢复备份',
    // WP管理
    wp_list_terms:'列出分类/标签', cleanup_plugin_data:'清理插件数据',
    // 媒体
    convert_to_webp:'WebP转换',
    // 友链
    list_friend_links:'列出友链', add_friend_link:'添加友链', delete_friend_link:'删除友链',
    verify_friend_link:'验证友链', approve_friend_link:'通过友链', reject_friend_link:'拒绝友链',
    check_all_friend_links:'检查全部友链',
    // 自动化
    batch_process:'批量处理', schedule_write:'定时写文章', list_scheduled_writes:'查看定时写文章',
    delete_scheduled_write:'删除定时写文章', check_processing_queue:'查看处理队列',
    worker_status:'Worker状态',
    // 配方
    recipe_create:'创建配方', recipe_list:'配方列表', recipe_execute:'执行配方',
    recipe_delete:'删除配方',
  }

  // 工具结果数据 Key → 中文标签映射
  const KEY_LABELS = {
    posts:'文章', pages:'页面', comments:'评论', users:'用户', categories:'分类', tags:'标签',
    products:'商品', orders:'订单', forums:'论坛帖子', plates:'版块', links:'链接',
    media:'媒体', drafts:'草稿', published:'已发布', pending:'待审', private:'私有',
    roles:'角色', capabilities:'权限', themes:'主题', plugins:'插件',
    score:'评分', readability:'可读性', originality:'原创性', structure:'结构',
    title_length:'标题长度', slug:'别名', has_h1:'有H1', has_h2:'H2数量', word_count:'字数',
    has_images:'图片数', has_links:'链接数', meta_title:'Meta标题', meta_description:'Meta描述',
    schema:'Schema标记',
    seo_score:'SEO评分', seo_title:'SEO标题', seo_keywords:'SEO关键词', seo_description:'SEO描述', seo_title_alt:'备选标题', keywords:'关键词', title:'标题', description:'描述',
    id:'ID', name:'名称', confidence:'置信度', count:'数量', total:'总计',
    message:'消息', status:'状态', type:'类型', author:'作者', date:'日期',
    summary:'摘要', content:'内容', url:'链接', email:'邮箱', role:'角色',
    post_id:'文章ID', post_title:'文章标题', post_type:'文章类型', post_status:'文章状态',
    term_id:'分类ID', term_name:'分类名', taxonomy:'分类法',
    user_id:'用户ID', display_name:'显示名称', user_email:'用户邮箱',
    alt_text:'ALT文本', image_url:'图片URL', file_size:'文件大小',
    dead_links:'死链', redirects:'重定向', broken:'已损坏', ok:'正常',
    similarity:'相似度', duplicate_of:'重复于',
    over_18_score:'成人内容', hate_score:'仇恨言论', spam_score:'垃圾信息',
    stages:'阶段', actions:'动作', enabled:'已启用', rule_type:'规则类型',
    trigger_event:'触发事件', execution_count:'执行次数', max_executions:'最大次数',
    created_at:'创建时间', updated_at:'更新时间', expires_at:'过期时间',
    model:'模型', total_tokens:'Token用量', provider:'提供商',
    event:'事件', description_text:'描述',
    skipped:'已跳过', reason:'原因', undo_available:'可撤销', confirm_required:'需确认',
    success:'成功', code:'状态码', data:'数据',
    ai_data:'AI处理数据', modified:'修改时间', comment_count:'评论数',
    现有标签:'现有标签', 标签数量:'标签数量', 处理历史:'处理历史', AI标签:'AI标签',
    处理时间:'处理时间', 处理模式:'处理模式', 结果摘要:'结果摘要',
    SEO数据:'SEO数据', AI摘要:'AI摘要', 润色版本:'润色版本', 已翻译语言:'已翻译语言',
    已改写风格:'已改写风格', 已扩展:'已扩展', 封面描述词:'封面描述词', 内链:'内链',
    质量评分:'质量评分', ALT文字:'ALT文字', 当前版本数:'当前版本数', 历史版本数:'历史版本数',
    name_en:'英文名', category:'分类', danger:'危险等级', wp_cap:'WP权限', min_role:'最低角色',
    // 工具结果数据扩展字段
    mode:'处理模式', lang:'目标语言', lang_name:'语言名称', ratio:'扩展倍数', style_name:'风格名称', registered:'注册时间', attempted:'尝试数', localized:'已本地化',
    compressed:'已压缩', saved_kb:'节省KB', scanned:'已扫描',
    original:'原文', polished:'润色后', translated:'翻译后', rewritten:'改写后', expanded:'扩展后',
    cover_prompt:'封面描述', attachment_id:'附件ID', source:'来源',
    overall:'综合评分', suggestions:'改进建议', duplicates:'重复文章',
    images:'图片数', results:'结果列表', documents:'文档数', chunks:'分块数',
    plan_id:'计划ID', rule_id:'规则ID', events:'事件列表', queue:'队列',
    rule:'规则详情', rules:'规则列表', schema:'Schema标记',
    repeat_detected:'检测到重复', tool_name:'工具名', last_time:'上次处理时间',
    last_mode:'上次模式', last_summary:'上次摘要', choices:'可选操作',
    duration_ms:'耗时', tokens_used:'Token用量', cost_rmb:'费用',
    undo_data:'撤销数据', notice:'提示',
    site_title:'站点标题', site_description:'站点描述', site_url:'站点链接', admin_email:'管理员邮箱',
    // 主题工具返回值字段中文化
    before:'操作前', after:'操作后', amount:'金额', balance:'余额', points:'积分',
    expire:'到期时间', level:'VIP等级', vip_level:'VIP等级', medal_id:'勋章ID', medal_name:'勋章名称',
    reason:'原因', days:'天数', duration:'时长', invite_count:'邀请数',
    order_count:'订单数', consecutive_days:'连续天数', checked_in:'已签到',
    user_name:'用户名', nick_name:'昵称', invite_code:'邀请码',
    ban_reason:'封禁原因', notified:'已通知', title_text:'标题',
    is_banned:'已封禁', medals:'勋章列表', topics:'论坛帖子',
    // 友链
    link_name:'友链名称', link_url:'友链URL', is_mutual:'已互链', reciprocal_status:'互链状态',
    verified_at:'验证时间', lost:'丢失互链', uncertain:'不确定', checked:'已检测', visible:'可见',
    category_name:'分类名',
    // 媒体
    watermarked:'已打水印', converted:'已转换',
    // 内容
    inserted:'文中插入', appended:'底部附加', unmatched:'未匹配',
    // WP
    terms:'分类/标签项', tagline:'站点副标题',
    // 配置
    cleaned:'已清理项', retention_days:'保留天数',
    // 处理历史中文化 Key (WPManager collectAIData 已用中文，这里做英文 fallback)
    处理历史:'处理历史', 处理时间:'处理时间', 结果摘要:'结果摘要',
    现有标签:'现有标签', 标签数量:'标签数量',
    SEO数据:'SEO数据', AI摘要:'AI摘要', 润色版本:'润色版本',
    已翻译语言:'已翻译语言', 已改写风格:'已改写风格', 已扩展:'已扩展',
    封面描述词:'封面描述词', 内链:'内链', 质量评分:'质量评分', ALT文字:'ALT文字',
    当前版本数:'当前版本数', 历史版本数:'历史版本数',
  };

  // 常见英文枚举值 → 中文映射
  var VALUE_LABELS = {
    // 文章状态
    publish:'已发布', draft:'草稿', pending:'待审核', private:'私有', trash:'回收站',
    future:'定时发布', inherit:'继承', auto_draft:'自动草稿',
    // 通用状态
    open:'开放', closed:'关闭',
    active:'启用', inactive:'禁用', completed:'已完成', processing:'处理中', executing:'执行中', pending:'等待中',
    enabled:'已启用', disabled:'已禁用',
    // 用户角色
    admin:'管理员', user:'普通用户', guest:'游客', vip:'VIP用户',
    // 规则类型
    permanent:'永久', oneshot:'一次性', expiring:'过期型', limited:'限制次数',
    // 内容类型
    post:'文章', page:'页面', attachment:'附件',
    // 工具模式
    ai:'AI模式', library:'词库模式', hybrid:'混合模式',
    prompt:'描述词模式', full:'完整模式',
    // 处理模式
    overwrite:'覆盖模式', append:'追加模式', fill_empty:'填充模式', skip:'跳过',
    // 危险等级
    none:'无', low:'低', medium:'中', high:'高',
    // 确认模式
    auto:'自动确认', single:'单次确认', double:'双重确认',
    // 润色深度
    light:'轻微', medium:'中等', deep:'深度',
    // 改写风格
    professional:'专业化', casual:'通俗化', creative:'创意化',
    // 图片来源
    stock:'免费图库', public:'公共域图库',
    // ALT生成模式
    rule:'规则模板',
    // 内链来源
    keyword:'关键词匹配', category:'分类相关', tags:'标签',
    // 批量处理范围
    recent:'最近', all:'全部', ids:'指定ID', date_range:'日期范围',
    // Schema类型
    Article:'文章', FAQPage:'FAQ页', Product:'产品',
    // 流水线触发
    publish:'发布时', update:'更新时', schedule:'定时扫描',
    // Webhook类型
    dingtalk:'钉钉', feishu:'飞书', wecom:'企业微信',
    // 评论/分类操作
    approve:'批准', spam:'垃圾', create:'创建', list:'列表',
    // 预算操作
    warn:'警告', block:'阻止',
    // WP 权限
    edit_posts:'编辑文章', manage_options:'管理选项', publish_posts:'发布文章',
    delete_posts:'删除文章', read:'阅读', create_users:'创建用户',
    edit_users:'编辑用户', delete_users:'删除用户', promote_users:'提升用户',
    list_users:'列出用户', moderate_comments:'审核评论', manage_categories:'管理分类',
    upload_files:'上传文件',
    // 自动化事件ID
    publish_post:'文章发布', save_post:'文章更新', user_register:'用户注册',
    wp_login:'用户登录', comment_post:'新评论', xzd_first_post:'首篇文章',
    vip_expiring:'VIP到期', points_threshold:'积分阈值', views_threshold_1000:'浏览量达标',
    daily_cron_3am:'每日定时', xzd_ai_schedule_write:'AI定时写文章',
    link_submitted:'友链申请', link_reciprocal_lost:'友链被下链',
    // 通用
    success_text:'成功', error_text:'失败', ok_text:'正常', yes:'是', no:'否',
    'true':'是', 'false':'否', ok:'正常', error:'错误',
    // 语言
    en:'英文', ja:'日文', ko:'韩文', zh:'中文',
  };

  // 格式化字符串值，映射常见英文枚举，URL自动转链接

    // 微信风格相对时间格式化
    function formatTimeLabel(timestamp) {
      if (!timestamp) return ''
      const now = Date.now()
      const diff = now - timestamp
      const d = new Date(timestamp)
      if (diff < 60000) return '刚刚'
      if (diff < 3600000) return Math.floor(diff / 60000) + '分钟前'
      const today = new Date()
      if (d.toDateString() === today.toDateString()) {
        return d.toLocaleTimeString('zh-CN', {hour:'2-digit', minute:'2-digit'})
      }
      const yesterday = new Date(today)
      yesterday.setDate(yesterday.getDate() - 1)
      if (d.toDateString() === yesterday.toDateString()) {
        return '昨天 ' + d.toLocaleTimeString('zh-CN', {hour:'2-digit', minute:'2-digit'})
      }
      if (d.getFullYear() === today.getFullYear()) {
        return (d.getMonth() + 1) + '-' + d.getDate() + ' ' + d.toLocaleTimeString('zh-CN', {hour:'2-digit', minute:'2-digit'})
      }
      return d.getFullYear() + '-' + (d.getMonth() + 1) + '-' + d.getDate()
    }

  function translateValue(v) {
    if (typeof v !== 'string') return escapeHtml(String(v))
    if (VALUE_LABELS[v]) return escapeHtml(VALUE_LABELS[v])
    // 布尔字符串
    if (v === 'true') return '<span class="xzd-tc-bool-true">是</span>'
    if (v === 'false') return '<span class="xzd-tc-bool-false">否</span>'
    // 图片URL → 直接展示图片（封面图/插图/图库结果）
    if (/^https?:\/\/[^\s]+\.(png|jpg|jpeg|gif|webp|svg)(\?[^\s]*)?$/i.test(v)) {
      return '<img src="' + escapeHtml(v) + '" class="xzd-tc-inline-img" loading="lazy" onload="var b=this.closest(\'.xzd-chat-body\');if(b)b.scrollTop=b.scrollHeight" />'
    }
    // URL → 可点击链接
    if (/^https?:\/\/\S+$/i.test(v)) {
      var display = v.length > 50 ? v.substring(0, 47) + '...' : v
      return '<a href="' + escapeHtml(v) + '" target="_blank" rel="noopener" class="xzd-tc-link">' + escapeHtml(display) + '</a>'
    }
    return escapeHtml(v)
  }

  // 格式化工具返回值，确保中文友好显示
  function formatValue(v, depth) {
    if (depth === undefined) depth = 0
    if (depth > 2) return escapeHtml(String(v))
    if (v === null || v === undefined) return '<span class="xzd-tc-empty">(无)</span>'
    if (Array.isArray(v)) {
      if (v.length === 0) return '<span class="xzd-tc-empty">(空)</span>'
      var MAX_SHOW = 10
      var truncated = v.length > MAX_SHOW
      var showItems = truncated ? v.slice(0, MAX_SHOW) : v
      var result = ''
      if (typeof v[0] === 'object' && v[0] !== null) {
        result = showItems.map(function(item) {
          var name = item.name || item.title || item.display_name || item.post_title || item.term_name || item.photographer || ''
          // 检测图片URL用于缩略图预览（图库搜索等）
          var imgUrl = item.url || item.image_url || item.thumbnail || item.thumb_url || item.src || item.original || ''
          var isItemImg = typeof imgUrl === 'string' && /^https?:\/\/[^\s]+\.(png|jpg|jpeg|gif|webp)(\?[^\s]*)?$/i.test(imgUrl)
          if (name) {
            var extra = ''
            if (item.confidence !== undefined) extra += ' ' + item.confidence + '%'
            if (item.score !== undefined) extra += ' ' + item.score + '分'
            if (item.id !== undefined && Object.keys(item).length <= 3) extra += ' #' + item.id
            if (item.url) {
              var imgPreview = isItemImg ? '<img src="' + escapeHtml(imgUrl) + '" class="xzd-tc-thumb" loading="lazy" />' : ''
              return '<a href="' + escapeHtml(item.url) + '" target="_blank" rel="noopener" class="xzd-tc-link">' + imgPreview + escapeHtml(String(name)) + '</a>' + (extra ? '<small>' + escapeHtml(String(extra)) + '</small>' : '')
            }
            return '<span class="xzd-tc-tag">' + escapeHtml(String(name)) + (extra ? '<small>' + escapeHtml(String(extra)) + '</small>' : '') + '</span>'
          }
          // 无name但可能有图片 → 展示缩略图+摄影师名
          if (isItemImg) {
            var label = item.photographer || item.user || item.author || ''
            return '<span class="xzd-tc-tag xzd-tc-tag-img">' + '<img src="' + escapeHtml(imgUrl) + '" class="xzd-tc-thumb" loading="lazy" />' + (label ? escapeHtml(label) : escapeHtml(formatPlainText(item))) + '</span>'
          }
          return '<span class="xzd-tc-tag">' + escapeHtml(formatPlainText(item)) + '</span>'
        }).join('')
      } else {
        result = showItems.map(translateValue).join(', ')
      }
      if (truncated) {
        result += '<div class="xzd-tc-truncate-hint">📋 仅显示前' + MAX_SHOW + '项，还有 <b>' + (v.length - MAX_SHOW) + '</b> 项未显示，共 <b>' + v.length + '</b> 项</div>'
      }
      return result
    }
    if (typeof v === 'object') {
      var entries = Object.entries(v)
      if (entries.length === 0) return '<span class="xzd-tc-empty">(空)</span>'
      return entries.map(function(e) {
        var val = e[1]
        var isImg = typeof val === 'string' && /^https?:\/\/[^\s]+\.(png|jpg|jpeg|gif|webp|svg)(\?[^\s]*)?$/i.test(val)
        if (isImg) {
          return '<div class="xzd-tc-kv-img-row"><span class="xzd-tc-k">' + escapeHtml(KEY_LABELS[e[0]] || e[0]) + '</span><img src="' + escapeHtml(val) + '" class="xzd-tc-inline-img" loading="lazy" onload="var b=this.closest(\'.xzd-chat-body\');if(b)b.scrollTop=b.scrollHeight" /></div>'
        }
        return '<span class="xzd-tc-kv-inner"><span class="xzd-tc-k">' + escapeHtml(KEY_LABELS[e[0]] || e[0]) + '</span><span class="xzd-tc-v">' + formatValue(e[1], depth + 1) + '</span></span>'
      }).join('')
    }
    if (typeof v === 'boolean') return v ? '<span class="xzd-tc-bool-true">是</span>' : '<span class="xzd-tc-bool-false">否</span>'
    if (typeof v === 'number') return escapeHtml(String(v))
    return translateValue(v)
  }

  function formatPlainText(v) {
    if (v === null || v === undefined) return ''
    if (Array.isArray(v)) return v.map(formatPlainText).join(', ')
    if (typeof v === 'object') {
      var name = v.name || v.title || v.display_name || ''
      if (name) return name
      return Object.values(v).map(formatPlainText).join(' ')
    }
    return String(v)
  }

  function safeJsonParse(raw) {
    if (!raw) return null
    try { return typeof raw === 'string' ? JSON.parse(raw) : raw } catch (e) { return null }
  }

  function parseToolCalls(raw) {
    if (!raw) return []
    if (Array.isArray(raw)) return raw
    if (typeof raw === 'string') {
      try { return JSON.parse(raw) } catch (e) { return [] }
    }
    return []
  }

  // ═══════════════════════════════
  // Proactive AI 前端监控模块
  // ═══════════════════════════════
  class ProactiveMonitor {
    constructor(chat) {
      this.chat = chat
      this.siteTypes = []
      this.state = {
        pageEnterTime: Date.now(),
        maxScrollDepth: 0,
        idleTime: 0,
        idleTimer: null,
        stayTimer: null,
        idleTriggerCount: 0,
        pageViews: (parseInt(sessionStorage.getItem('xzd_pv') || '0') || 0) + 1,
        productViews: 0,
        forumViews: 0,
        lastProductUrl: '',
        lastForumUrl: '',
      }
      this.localCooldowns = {}
      this._handlers = {}
    }

    async init() {
      sessionStorage.setItem('xzd_pv', this.state.pageViews)
      // 加载站点类型缓存
      await this.loadSiteType()
      this.trackScroll()
      this.trackIdle()
      this.trackExitIntent()
      this.trackStay()
      this.trackSearch()
      this.checkFirstVisit()
      this.checkReturnVisitor()
      this.checkMultiPageView()
      this.trackArticleReading()
      this.trackPage404()
      this.trackContentCopied()
      // 站点类型专用追踪
      if (this.siteTypes.includes('shop')) this.trackProductBrowsing()
      if (this.siteTypes.includes('forum')) this.trackForumBrowsing()
    }

    async loadSiteType() {
      try {
        const cached = sessionStorage.getItem('xzd_site_types')
        if (cached) { this.siteTypes = JSON.parse(cached); return }
        const r = await this.chat.apiGet('site-type')
        if (r && r.success && r.data && r.data.types) {
          this.siteTypes = r.data.types || []
          sessionStorage.setItem('xzd_site_types', JSON.stringify(this.siteTypes))
        }
      } catch (e) { this.siteTypes = [] }
    }

    // 获取当前页面上下文
    getPageContext() {
      const ctx = {
        page_url: location.href,
        page_title: document.title,
        page_type: 'page',
        post_id: 0,
      }
      // 检测文章页
      const article = document.querySelector('article, .article-content, .post-content, .entry-content, .single-post, .single')
      if (article) {
        ctx.page_type = 'post'
        const bodyClass = document.body.className || ''
        const rules = this.pageContextRules || {}
        const idPattern = rules.postIdPattern || 'postid-(\\d+)|post-(\\d+)'
        const postIdMatch = bodyClass.match(new RegExp(idPattern))
        if (postIdMatch) ctx.post_id = parseInt(postIdMatch[1] || postIdMatch[2])
      }
      // 检测产品页
      if (document.body.classList.contains('single-product') || document.querySelector('.product')) {
        ctx.page_type = 'product'
      }
      // 检测论坛页（通过适配器配置的class名）
      const rules = this.pageContextRules || {}
      const forumCls = rules.forumClass || 'single-forum_post'
      if (document.body.classList.contains(forumCls) || document.querySelector('.forum-post')) {
        ctx.page_type = 'forum_post'
      }
      return ctx
    }

    // ── 产品浏览追踪（商城站） ──
    trackProductBrowsing() {
      if (document.body.classList.contains('single-product') || document.querySelector('.product')) {
        this.state.productViews++
        const url = location.pathname
        if (this.state.lastProductUrl && this.state.lastProductUrl !== url) {
          this.state.productViews++
        }
        this.state.lastProductUrl = url
        if (this.state.productViews >= 3) {
          this.trigger('product_browse_3')
          this.state.productViews = 0
        }
      }
    }

    // ── 论坛浏览追踪（论坛站） ──
    trackForumBrowsing() {
      const rules = this.pageContextRules || {}
      const forumCls = rules.forumClass || 'single-forum_post'
      if (document.body.classList.contains(forumCls) || document.querySelector('.forum-post')) {
        this.state.forumViews++
        const url = location.pathname
        if (this.state.lastForumUrl && this.state.lastForumUrl !== url) {
          this.state.forumViews++
        }
        this.state.lastForumUrl = url
        if (this.state.forumViews >= 3) {
          this.trigger('forum_browse_3')
          this.state.forumViews = 0
        }
      }
    }

    // ── 滚动深度 ──
    trackScroll() {
      let ticking = false
      this._handlers.scroll = () => {
        if (!ticking) {
          requestAnimationFrame(() => {
            const scrollH = document.documentElement.scrollHeight - window.innerHeight
            const pct = scrollH > 0 ? Math.round((window.scrollY / scrollH) * 100) : 0
            if (pct > this.state.maxScrollDepth) {
              this.state.maxScrollDepth = pct
              if (pct >= 80) this.trigger('scroll_depth_80')
            }
            ticking = false
          })
          ticking = true
        }
      }
      window.addEventListener('scroll', this._handlers.scroll, { passive: true })
    }

    // ── 空闲检测 ──
    trackIdle() {
      const reset = () => {
        this.state.idleTime = Date.now()
        if (this.state.idleTimer) clearTimeout(this.state.idleTimer)
        this.state.idleTimer = setTimeout(() => {
          if (this.state.idleTriggerCount < 2) {
            this.state.idleTriggerCount++
            this.trigger('idle_60s')
          }
        }, 60000)
      }
      this._handlers.activity = reset
      ;['mousemove', 'keydown', 'scroll', 'click', 'touchstart'].forEach(evt => {
        window.addEventListener(evt, reset, { passive: true })
      })
      reset()
    }

    // ── 退出意图 ──
    trackExitIntent() {
      this._handlers.exit = (e) => {
        if (e.clientY <= 0) this.trigger('exit_intent')
      }
      document.addEventListener('mouseleave', this._handlers.exit)
    }

    // ── 停留时间 ──
    trackStay() {
      this.state.stayTimer = setTimeout(() => {
        this.trigger('stay_120s')
      }, 120000)
    }

    // ── 搜索无结果 ──
    trackSearch() {
      if (window.location.search.includes('s=')) {
        // 优先检查 body class（Zibll 添加 search-no-results）
        if (document.body.classList.contains('search-no-results')) {
          setTimeout(() => this.trigger('search_no_result'), 2000)
          return
        }
        // 回退：按结果元素数量判断
        const results = document.querySelectorAll('.posts-row .ajax-item, .search-result, article.post, .no-results')
        if (results.length === 0) {
          setTimeout(() => this.trigger('search_no_result'), 2000)
        }
      }
    }

    // ── 回访检测 ──
    checkReturnVisitor() {
      const lastVisit = localStorage.getItem('xzd_last_visit')
      const now = Date.now()
      if (lastVisit && (now - parseInt(lastVisit)) > 259200000) { // 3天
        this.trigger('return_visitor')
      }
      localStorage.setItem('xzd_last_visit', now)
    }

    // ── 多页浏览 ──
    checkMultiPageView() {
      if (this.state.pageViews >= 5) {
        this.trigger('multi_page_view')
      }
    }

    // ── 长文章阅读检测 ──
    trackArticleReading() {
      const article = document.querySelector('article, .article-content, .post-content, .entry-content')
      if (!article) return
      const text = article.textContent || ''
      if (text.length < 2000) return
      this.state._articleTimer = setTimeout(() => {
        this.trigger('article_reading', { content_length: text.length })
      }, 90000)
    }

    // ── 404 页面检测 ──
    trackPage404() {
      const is404 = document.body.classList.contains('error404') ||
        (document.title && /404|not found|页面不存在/i.test(document.title))
      if (is404) {
        setTimeout(() => this.trigger('page_404'), 1500)
      }
    }

    // ── 复制内容检测 ──
    trackContentCopied() {
      this._handlers.copy = () => {
        const sel = window.getSelection()
        if (sel && sel.toString().length >= 20) {
          this.trigger('content_copied', { copied_length: sel.toString().length })
        }
      }
      document.addEventListener('copy', this._handlers.copy)
    }

    // ── 首次访问检测 ──
    checkFirstVisit() {
      if (!localStorage.getItem('xzd_last_visit')) {
        setTimeout(() => this.trigger('first_visit'), 3000)
      }
    }

    // ── 触发 ──
    async trigger(event, data) {
      // 单事件去重（先设标记再发请求，避免 API 慢时重复触发）
      if (this.localCooldowns[event]) return
      this.localCooldowns[event] = true
      // 每会话最多弹窗 3 次，避免骚扰用户
      this.state.engageCount = (this.state.engageCount || 0)
      if (this.state.engageCount >= 3) return
      const ctx = this.getPageContext()
      const r = await this.chat.apiPost('chat/proactive-trigger', Object.assign({
        event: event,
        page_url: ctx.page_url,
        page_title: ctx.page_title,
        post_id: ctx.post_id,
        page_type: ctx.page_type,
      }, data || {}))
      if (r && r.success && r.data && r.data.should_engage) {
        this.state.engageCount++
        this.chat.showProactiveMessage(r.data.message || '', r.data.suggestions || [])
      }
    }

    destroy() {
      if (this._handlers.scroll) window.removeEventListener('scroll', this._handlers.scroll)
      if (this._handlers.exit) document.removeEventListener('mouseleave', this._handlers.exit)
      if (this._handlers.copy) document.removeEventListener('copy', this._handlers.copy)
      if (this.state.idleTimer) clearTimeout(this.state.idleTimer)
      if (this.state.stayTimer) clearTimeout(this.state.stayTimer)
      if (this.state._articleTimer) clearTimeout(this.state._articleTimer)
    }
  }

  // ═══════════════════════════════
  // FloatingChat 主类
  // ═══════════════════════════════
  class FloatingChat {
    constructor(cfg) {
      this.cfg = cfg || {}
      this.themeAdapter = this.cfg.themeAdapter || 'generic'
      // 能力flag（替代字符串比较）
      this.hasIntegratedButton = this.cfg.themeHasIntegratedButton || false
      this.btnContainerSelector = this.cfg.themeButtonSelector || null
      this.pageContextRules = this.cfg.pageContextRules || {}
      this.isAdminPage = this.cfg.isAdminPage || false

      // 主题适配数据
      this.themeColors = this.cfg.themeColors || {}
      this.bubblePosition = this.cfg.bubblePosition || { bottom: '24px', right: '24px' }

      // 状态
      this.open = false
      this.closing = false
      this.mobile = window.innerWidth < 768
      this.showSessions = false
      this.streaming = false
      this.streamAbort = null
      this._streamingContentEl = null
      this._streamingMsgIdx = -1
      this.activeSessionKey = ''
      this.sessions = []
      this.messages = []
      this.unreadCount = 0
      this.inputText = ''
      this.quotaText = '—'
      this.modelName = this.cfg.defaultModel || ''
      this.isAdmin = this.cfg.isAdmin || false

      // 用户和 AI 身份 (来自PHP配置)
      this.userName = this.cfg.userName || '用户'
      this.userAvatar = this.cfg.userAvatar || ''
      this.userRoles = this.cfg.userRoles || []
      this.userIdentity = ''
      this.aiName = this.cfg.aiName || 'AI 助手'
      this.aiAvatar = this.cfg.aiAvatar || ''
      this.defaultAvatar = this.cfg.defaultAvatar || ''

      this.el = {}
      this.monitor = null
      this.toasts = []
      this._closeGeneration = 0        // 关闭代次计数器，防止 transitionend 竞态
      this._gotDoneEvent = false       // SSE 'done' 事件标记，避免双重渲染
      this._fxRunning = true           // Canvas 动画运行标志
      this._fxDraw = null             // Canvas draw 函数引用，用于手动重启
    }

    // ═══════════════════════════
    // 初始化
    // ═══════════════════════════
    init() {
      this._initFxCanvas()
      this._timeRefresh = setInterval(() => {
        if (this.showSessions) return
        // 每60秒刷新时间标签（直接更新DOM，不重建）
        if (this.messages.length > 0 && this._lastRefresh && Date.now() - this._lastRefresh > 55000) {
          this._lastRefresh = Date.now()
          this._refreshTimeLabels()
        }
      }, 3000)
      this._lastRefresh = Date.now()
      this.cacheDom()
      if (!this.el.root) return
      this.applyThemePosition()
      this.detectTheme()
      this.bindEvents()
      this.restoreSession()
      this.loadSessions()
      this.loadQuota()
      this.showReady()
      this.observeDarkMode()
      this.initProactive()
    }

    /**
     * 应用主题适配器的定位和颜色到 CSS 变量（仅主题适配模式）
     */
    applyThemePosition() {
      const rootStyle = getComputedStyle(document.documentElement)
      const integrationMode = (rootStyle.getPropertyValue('--xzd-integration-mode') || 'theme').trim()
      // 完全自定义模式：不覆盖用户设置的位置
      if (integrationMode === 'standalone') return

      const bp = this.bubblePosition
      if (this.el.root) {
        this.el.root.style.setProperty('--xzd-trigger-bottom', bp.bottom || '24px')
        this.el.root.style.setProperty('--xzd-trigger-right', bp.right || '24px')
        this.el.root.style.setProperty('--xzd-trigger-top', 'auto')
        this.el.root.style.setProperty('--xzd-trigger-left', 'auto')
      }
      if (this.themeColors && this.themeColors.primary) {
        if (this.el.root) this.el.root.style.setProperty('--xzd-theme-primary', this.themeColors.primary)
      }
    }

    detectTheme() {
      const cls = document.body.classList
      const rootStyle = getComputedStyle(document.documentElement)
      const integrationMode = (rootStyle.getPropertyValue('--xzd-integration-mode') || 'theme').trim()

      if (this.hasIntegratedButton) {
        this.el.panel.classList.add('xzd-theme-deep')
        if (integrationMode !== 'standalone') {
          this.integrateThemeButton()
        } else {
          // 完全自定义模式：显示独立触发按钮
          this.el.trigger.style.display = 'flex'
          this.el.trigger.style.position = 'fixed'
        }
      } else {
        // 通用主题：用独立触发按钮
        this.el.trigger.style.display = 'flex'
      }

      if (integrationMode === 'standalone' || !this.hasIntegratedButton) {
        this.applyStandaloneTooltip()
      }

      if (cls.contains('dark-theme')) {
        this.el.panel.classList.add('xzd-dark')
      }
    }

    /** 独立模式下读取自定义悬浮提示并应用到独立按钮 */
    applyStandaloneTooltip() {
      const rootStyle = getComputedStyle(document.documentElement)
      const tooltip = (rootStyle.getPropertyValue('--xzd-trigger-tooltip') || '""').replace(/"/g, '').trim()
      if (this.el.trigger && tooltip) {
        this.el.trigger.title = tooltip
      }
    }

    /**
     * 获取主题按钮容器（通过适配器配置）
     */
    getThemeButtonContainer() {
      if (this.btnContainerSelector) {
        return document.querySelector(this.btnContainerSelector)
      }
      // 通用降级：寻找常见浮动按钮容器
      return document.querySelector('.float-right') ||
             document.querySelector('.theme-float-btns') ||
             null
    }

    /**
     * 在指定容器中创建集成按钮（通用逻辑）
     */
    createIntegratedButton(container) {
      if (!container) return

      // 读取 PHP 注入的 CSS 变量
      const rootStyle = getComputedStyle(document.documentElement)
      const iconKey = (rootStyle.getPropertyValue('--xzd-trigger-icon') || '"chat"').replace(/"/g, '')
      const iconType = (rootStyle.getPropertyValue('--xzd-trigger-icon-type') || '"preset"').replace(/"/g, '')
      const iconImage = (rootStyle.getPropertyValue('--xzd-trigger-icon-image') || '""').replace(/"/g, '')
      const iconColor = (rootStyle.getPropertyValue('--xzd-trigger-icon-color') || '"white"').replace(/"/g, '')
      const iconColorCustom = (rootStyle.getPropertyValue('--xzd-trigger-icon-color-custom') || '"#ffffff"').replace(/"/g, '')
      const iconCustom = readCssVar('--xzd-trigger-icon-custom', '')
      const iconViewBox = readCssVar('--xzd-trigger-icon-viewbox', '0 0 24 24')
      const iconInner = readCssVar('--xzd-trigger-icon-inner', '')
      const iconIsFull = (rootStyle.getPropertyValue('--xzd-trigger-icon-full') || '"0"').replace(/"/g, '') === '1'
      const tooltip = (rootStyle.getPropertyValue('--xzd-trigger-tooltip') || '""').replace(/"/g, '').trim()
      const hoverEffect = (rootStyle.getPropertyValue('--xzd-trigger-hover') || '"scale"').replace(/"/g, '')
      const bgColor = rootStyle.getPropertyValue('--xzd-trigger-bg').trim()

      // 图标 SVG 映射（与 PHP 端保持一致）
      const iconSVGs = {
        chat: '<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>',
        robot: '<rect x="4" y="4" width="16" height="16" rx="2"/><circle cx="9" cy="10" r="1.5"/><circle cx="15" cy="10" r="1.5"/><path d="M9 15c1.5 2 4.5 2 6 0"/>',
        question: '<circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/>',
        settings: '<circle cx="12" cy="12" r="3"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/>',
        search: '<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>',
        star: '<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>',
        heart: '<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/>',
        download: '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>',
        book: '<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/>',
        mail: '<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/>',
        bell: '<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/>',
        home: '<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/>',
        user: '<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>',
        cart: '<circle cx="9" cy="21" r="1"/><circle cx="20" cy="21" r="1"/><path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"/>',
        headset: '<path d="M3 11a9 9 0 1 1 18 0v3a2 2 0 0 1-2 2h-1v-4h1a7 7 0 0 0-14 0h1v4H5a2 2 0 0 1-2-2v-3z"/>',
        news: '<rect x="2" y="4" width="20" height="16" rx="2"/><line x1="6" y1="8" x2="10" y2="8"/><line x1="6" y1="12" x2="14" y2="12"/><line x1="6" y1="16" x2="18" y2="16"/><rect x="16" y="8" width="4" height="8"/>',
        building: '<rect x="4" y="2" width="16" height="20" rx="1"/><line x1="9" y1="6" x2="9" y2="6.01"/><line x1="15" y1="6" x2="15" y2="6.01"/><line x1="9" y1="10" x2="9" y2="10.01"/><line x1="15" y1="10" x2="15" y2="10.01"/><line x1="9" y1="14" x2="9" y2="14.01"/><line x1="15" y1="14" x2="15" y2="14.01"/><path d="M9 22v-4h6v4"/>',
        palette: '<circle cx="13.5" cy="6.5" r="1.5"/><circle cx="17.5" cy="10.5" r="1.5"/><circle cx="8.5" cy="7.5" r="1.5"/><circle cx="6.5" cy="12.5" r="1.5"/><path d="M12 2C6.49 2 2 6.49 2 12s4.49 10 10 10c.42 0 .83-.04 1.24-.11a2 2 0 0 0 1.48-3.12l-.72-1.15a1 1 0 0 1 .86-1.52H16c4.43 0 8-3.58 8-8S17.51 2 12 2z"/>',
        crown: '<path d="M2 4l3 12h14l3-12-6 4-4-6-4 6-6-4z"/><path d="M4 20h16"/><path d="M5 17h14"/>',
        globe: '<circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>'
      }
      const iconSvg = iconCustom || iconSVGs[iconKey] || iconSVGs.chat

      let iconFill = '#ffffff'
      if (iconColor === 'dark') iconFill = '#18181B'
      else if (iconColor === 'custom' && iconColorCustom) iconFill = iconColorCustom

      const btn = document.createElement('div')
      btn.className = 'float-btn xzd-ai-btn xzd-theme-integrated-btn'
      btn.title = tooltip || this.aiName || 'AI 助手'

      let btnContent = ''
      if (iconType === 'image') {
        const imgUrl = iconImage || (window.xzdChatConfig?.pluginUrl || '') + 'assets/images/default-fab.gif'
        btnContent = '<img src="' + escapeHtml(imgUrl) + '" style="position:absolute;inset:0;width:100%;height:100%;border-radius:50%;object-fit:cover" alt="" />'
        btn.style.overflow = 'hidden'
        btn.style.position = 'relative'
        btn.style.background = 'transparent'
        btn.style.boxShadow = 'none'
      } else if (iconType === 'svg' && iconIsFull) {
        btnContent = '<svg width="18" height="18" viewBox="' + escapeHtml(iconViewBox) + '" style="display:block">' + iconInner + '</svg>'
      } else {
        btnContent = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="' + iconFill + '" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' + iconSvg + '</svg>'
      }
      btn.innerHTML = btnContent + '<span class="xzd-chat-badge" style="display:none;"></span>'
      btn.addEventListener('click', () => this.togglePanel())

      const hoverClassMap = { scale: 'xzd-hover-scale', rotate: 'xzd-hover-rotate', pulse: 'xzd-hover-pulse' }
      if (hoverClassMap[hoverEffect]) {
        btn.classList.add(hoverClassMap[hoverEffect])
      }

      if (iconType !== 'image' && bgColor && bgColor !== 'none' && !bgColor.startsWith('linear-gradient')) {
        btn.style.setProperty('--xzd-ai-btn-bg', bgColor)
      }

      // 插入到 ontop（回顶部）按钮之前（通用兼容）
      const ontop = container.querySelector('.ontop')
      if (ontop) {
        ontop.parentNode.insertBefore(btn, ontop)
      } else {
        container.appendChild(btn)
      }

      this.el.themeBtn = btn
      this.el.themeBtnBadge = btn.querySelector('.xzd-chat-badge')

      if (this.el.trigger) {
        this.el.trigger.style.display = 'none'
      }
    }

    /**
     * 适配：AI 按钮融入主题悬浮按钮序列
     */
    integrateThemeButton() {
      const container = this.getThemeButtonContainer()
      if (container) {
        this.createIntegratedButton(container)
      } else {
        // 无主题容器，降级到独立按钮
        if (this.el.trigger) {
          this.el.trigger.style.display = 'flex'
        }
      }
    }

    /**
     * 冰晶折射Canvas背景动效 — 棱镜光斑 + 冰晶粒子
     */
    _initFxCanvas() {
      var self = this
      var canvas = document.getElementById('xzd-fx-canvas')
      if (!canvas) return
      var ctx = canvas.getContext('2d')
      var W, H, t = 0
      function resize() { W = canvas.width = canvas.offsetWidth || 400; H = canvas.height = canvas.offsetHeight || 600 }
      resize()

      // 6个棱镜光斑
      var prisms = []
      for (var i = 0; i < 6; i++) {
        prisms.push({
          x: Math.random() * 500, y: Math.random() * 700,
          size: 30 + Math.random() * 60, rotation: Math.random() * Math.PI * 2,
          rotSpeed: (Math.random() - 0.5) * 0.003,
          driftX: (Math.random() - 0.5) * 0.15, driftY: (Math.random() - 0.5) * 0.1,
          hue: Math.random() * 60 + 160, opacity: 0.04 + Math.random() * 0.06
        })
      }
      // 40个冰晶粒子
      var sparkles = []
      for (var j = 0; j < 40; j++) {
        sparkles.push({
          x: Math.random() * 500, y: Math.random() * 700,
          size: 1 + Math.random() * 2.5,
          speed: 0.5 + Math.random() * 1.5, phase: Math.random() * Math.PI * 2,
          hue: 180 + Math.random() * 60
        })
      }

      function draw() {
        // ★ 修复：完全停止 rAF 循环而非空转
        if (!self._fxRunning) return
        if (self.el.panel && getComputedStyle(self.el.panel).display === 'none') {
          requestAnimationFrame(draw); return
        }
        ctx.clearRect(0, 0, W, H)
        t += 0.016
        var dk = document.body.classList.contains('dark-theme')
        var om = dk ? 1.8 : 1

        // 棱镜
        for (var pi = 0; pi < prisms.length; pi++) {
          var p = prisms[pi]
          p.rotation += p.rotSpeed; p.x += p.driftX; p.y += p.driftY
          if (p.x < -80) p.x = W + 80; if (p.x > W + 80) p.x = -80
          if (p.y < -80) p.y = H + 80; if (p.y > H + 80) p.y = -80
          ctx.save(); ctx.translate(p.x, p.y); ctx.rotate(p.rotation)
          ctx.beginPath(); ctx.moveTo(0, -p.size); ctx.lineTo(p.size * 0.6, 0); ctx.lineTo(0, p.size); ctx.lineTo(-p.size * 0.6, 0); ctx.closePath()
          var g = ctx.createRadialGradient(0, 0, 0, 0, 0, p.size)
          var bo = p.opacity * om
          g.addColorStop(0, 'hsla(' + p.hue + ',70%,' + (dk ? 70 : 55) + '%,' + bo + ')')
          g.addColorStop(0.5, 'hsla(' + (p.hue + 30) + ',60%,' + (dk ? 60 : 50) + '%,' + (bo * 0.5) + ')')
          g.addColorStop(1, 'transparent')
          ctx.fillStyle = g; ctx.fill()
          // 折射光线
          ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(p.size * 1.2, -p.size * 0.8)
          var lg = ctx.createLinearGradient(0, 0, p.size * 1.2, -p.size * 0.8)
          lg.addColorStop(0, 'hsla(' + (p.hue + 60) + ',80%,' + (dk ? 70 : 50) + '%,' + (bo * 0.8) + ')')
          lg.addColorStop(1, 'transparent')
          ctx.strokeStyle = lg; ctx.lineWidth = 1; ctx.stroke()
          ctx.restore()
        }

        // 冰晶粒子
        for (var si = 0; si < sparkles.length; si++) {
          var s = sparkles[si]
          s.y -= s.speed * 0.3; s.x += Math.sin(t * 2 + s.phase) * 0.2
          if (s.y < -10) { s.y = H + 10; s.x = Math.random() * W }
          var fl = 0.3 + Math.sin(t * 3 + s.phase) * 0.3 + Math.sin(t * 7 + s.phase * 2) * 0.2
          ctx.beginPath(); ctx.arc(s.x, s.y, s.size, 0, Math.PI * 2)
          ctx.fillStyle = 'hsla(' + s.hue + ',70%,' + (dk ? 80 : 45) + '%,' + (fl * (dk ? 0.5 : 0.4)) + ')'; ctx.fill()
        }
        requestAnimationFrame(draw)
      }
      draw()
      // ★ 修复：保存 draw 引用，面板重新打开时可手动重启动画
      self._fxDraw = draw

      // 尺寸变化监听
      if (window.ResizeObserver) {
        new ResizeObserver(function () { resize() }).observe(canvas)
      }
    }

    cacheDom() {
      this.el.root     = $('#xzd-chat-root')
      this.el.trigger  = $('#xzd-chat-trigger')
      this.el.panel    = $('#xzd-chat-panel')
      this.el.messages = $('#xzd-chat-messages')
      this.el.sessions = $('#xzd-chat-sessions')
      this.el.sessionList = $('#xzd-chat-session-list')
      this.el.input    = $('#xzd-chat-input')
      this.el.sendBtn  = $('#xzd-chat-send-btn')
      this.el.quota    = $('#xzd-chat-quota')
      this.el.identity = $('#xzd-chat-identity')
      this.el.badge    = $('#xzd-chat-badge')
      this.el.welcome  = $('#xzd-chat-welcome')
      this.el.quickbar = $('#xzd-chat-quickbar')
      this.el.statusBar = $('#xzd-chat-status-bar')
      this.el.closeBtn = $('#xzd-chat-close')
      this.el.newBtn   = $('#xzd-chat-new')
      this.el.sessionsBtn = $('#xzd-chat-sessions-btn')
      this.el.sessionsClose = $('#xzd-chat-sessions-close')
      // 缓存 welcome HTML 防止被 container.innerHTML = '' 清除
      if (this.el.welcome) this._cachedWelcome = this.el.welcome.outerHTML
    }

    bindEvents() {
      this.el.trigger.addEventListener('click', () => this.togglePanel())
      this.el.closeBtn.addEventListener('click', () => this.closePanel())
      this.el.newBtn.addEventListener('click', () => this.newSession())
      this.el.sessionsBtn.addEventListener('click', () => this.toggleSessions())
      this.el.sessionsClose.addEventListener('click', () => { this.showSessions = false; this.renderView() })
      this.el.sendBtn.addEventListener('click', () => {
        if (this.streaming) { this.stopStreaming() }
        else { this.send() }
      })
      this.el.input.addEventListener('keydown', (e) => {
        if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); this.send() }
        if (e.key === 'Escape') this.closePanel()
      })
      this.el.input.addEventListener('input', () => { this.autoResize(); this.toggleSendBtn() })
      this.el.quickbar && this.el.quickbar.addEventListener('click', (e) => {
        const btn = e.target.closest('[data-hint]')
        if (btn) { this.inputText = btn.dataset.hint; this.send() }
      })
      this.el.sessionList.addEventListener('click', (e) => {
        const del = e.target.closest('[data-delete-session]')
        const item = e.target.closest('[data-session-key]')
        if (del) { e.stopPropagation(); this.deleteSession(del.dataset.deleteSession); return }
        if (item) this.switchSession(item.dataset.sessionKey)
      })
      this.el.messages.addEventListener('click', (e) => {
        const confirm = e.target.closest('[data-confirm-key]')
        const confirmBatch = e.target.closest('[data-confirm-keys]')
        const cancel = e.target.closest('[data-cancel-confirm]')
        const copyMsg = e.target.closest('.xzd-msg-copy-btn')
        const copyCode = e.target.closest('.xzd-code-copy-btn')
        const followup = e.target.closest('.xzd-followup-pill')
        if (confirm) { this.confirmAction(confirm.dataset.confirmKey, confirm.dataset.choice || '') }
        if (confirmBatch) { this.confirmBatch(confirmBatch.dataset.confirmKeys) }
        if (cancel) { this.cancelConfirm() }
        if (copyMsg) {
          e.stopPropagation()
          var content = copyMsg.dataset.copyContent || ''
          this.copyMessage(content)
          copyMsg.style.opacity = '1'
          copyMsg.style.color = 'var(--xzd-success)'
          setTimeout(function () { copyMsg.style.color = '' }, 1200)
        }
        if (copyCode) {
          e.stopPropagation()
          var code = copyCode.dataset.code || ''
          this.copyMessage(code)
          copyCode.classList.add('xzd-copied')
          copyCode.textContent = '已复制'
          setTimeout(function () { copyCode.classList.remove('xzd-copied'); copyCode.textContent = '复制代码' }, 2000)
        }
        if (followup) {
          e.stopPropagation()
          this.inputText = followup.dataset.hint || followup.textContent
          this.send()
        }
        // 欢迎界面卡片和提示按钮（事件委托，兼容重建的DOM）
        const welcomeHint = e.target.closest('[data-hint]')
        if (welcomeHint && !followup) {
          e.stopPropagation()
          this.inputText = welcomeHint.dataset.hint
          this.send()
        }
      })
      this.el.welcome.addEventListener('click', (e) => {
        const hint = e.target.closest('[data-hint]')
        if (hint) { this.inputText = hint.dataset.hint; this.send() }
      })
      window.addEventListener('resize', () => {
        this.mobile = window.innerWidth < 768
        this.renderView()
      })
      document.addEventListener('keydown', (e) => {
        if (e.key === 'Escape' && this.open) { this.closePanel() }
      })
    }

    /**
     * 暗色模式变化监听
     */
    observeDarkMode() {
      const observer = new MutationObserver(() => {
        const isDark = document.body.classList.contains('dark-theme')
        this.el.panel.classList.toggle('xzd-dark', isDark)
      })
      observer.observe(document.body, { attributes: true, attributeFilter: ['class'] })
    }

    /**
     * 初始化 Proactive AI 监控（仅前台 + 非管理员用户）
     */
    initProactive() {
      if (this.isAdminPage) return // 后台不触发
      if (this.isAdmin) return    // 管理员不监控
      this.monitor = new ProactiveMonitor(this)
      this.monitor.init()
    }

    /**
     * Proactive AI 消息 — Toast 浮动提示 + 红点，不自动弹开对话框
     */
    showProactiveMessage(text, suggestions) {
      suggestions = suggestions || []
      // 写入对话记录（纯文本 + 结构化suggestions，渲染时构建链接）
      this.messages.push({ role: 'assistant', content: text, tool_calls: [], streaming: false, confirm_required: false, suggestions: suggestions })
      // 递增未读 + 更新红点
      this.unreadCount++
      this.renderBadge()
      // 弹出 Toast 浮动通知（传递suggestions用于渲染链接）
      this.showProactiveToast(text, suggestions)
      // 如果对话框已打开，刷新消息列表
      if (this.open) this.renderMessages()
    }

    /**
     * 显示主动通知 Toast — 位于按钮/对话框左侧
     */
    showProactiveToast(text, suggestions) {
      suggestions = suggestions || []
      const toast = document.createElement('div')
      toast.className = 'xzd-proactive-toast'
      const defAvt = escapeHtml(this.defaultAvatar || '')
      const aiIcon = `<img src="${escapeHtml(this.aiAvatar || defAvt)}" style="width:28px;height:28px;border-radius:50%;object-fit:cover" alt="" onerror="if(this.src!==this.getAttribute('data-fb')){this.src=this.getAttribute('data-fb')}" data-fb="${defAvt}">`
      // 构建建议链接HTML
      var linksHtml = ''
      if (suggestions.length > 0) {
        linksHtml = '<div class="xzd-toast-links">' + suggestions.map(function(s) {
          var u = s.url || ''
          return u ? '<a href="' + escapeHtml(u) + '" target="_blank" rel="noopener" class="xzd-toast-link">' + escapeHtml(s.title) + '</a>' : ''
        }).filter(Boolean).join('') + '</div>'
      }
      toast.innerHTML = `<div class="xzd-toast-icon">${aiIcon}</div><div class="xzd-toast-body"><div class="xzd-toast-name">${escapeHtml(this.aiName || 'AI 助手')}</div><div class="xzd-toast-text">${escapeHtml(text)}</div>${linksHtml}</div><button class="xzd-toast-close">&times;</button>`
      document.body.appendChild(toast)
      // 定位
      this.positionToast(toast, this.toasts.length)
      this.toasts.push(toast)
      // 入场动画 — 双帧确保浏览器先渲染初始 opacity:0 再过渡到可见
      requestAnimationFrame(() => {
        requestAnimationFrame(() => { toast.classList.add('xzd-toast-visible') })
      })
      // 关闭按钮
      toast.querySelector('.xzd-toast-close').addEventListener('click', () => this.dismissToast(toast))
      // 6秒自动消失
      const timer = setTimeout(() => this.dismissToast(toast), 6000)
      toast._dismissTimer = timer
    }

    /**
     * Toast 定位 — 对话框关闭时在悬浮按钮左侧，打开时在面板左侧
     */
    positionToast(toast, offsetIndex) {
      const bp = this.bubblePosition
      const btnBottom = parseInt(bp.bottom || '24')
      const btnRight = parseInt(bp.right || '24')
      const verticalOffset = offsetIndex * 72
      if (this.mobile) {
        // 移动端：右下角对齐
        toast.style.right = '12px'
        toast.style.left = 'auto'
        toast.style.bottom = (Math.max(16, btnBottom) + verticalOffset) + 'px'
      } else if (this.open) {
        toast.style.right = '444px'
        toast.style.left = 'auto'
        toast.style.bottom = (btnBottom + 28 + verticalOffset) + 'px'
      } else {
        toast.style.right = (btnRight + 52 + 16) + 'px'
        toast.style.left = 'auto'
        toast.style.bottom = (btnBottom + verticalOffset) + 'px'
      }
    }

    /**
     * 关闭 Toast — 出场动画后移除
     */
    dismissToast(toast) {
      if (toast._dismissed) return
      toast._dismissed = true
      if (toast._dismissTimer) clearTimeout(toast._dismissTimer)
      toast.classList.add('xzd-toast-hiding')
      setTimeout(() => {
        if (toast.parentNode) toast.parentNode.removeChild(toast)
        this.toasts = this.toasts.filter(t => t !== toast)
        // 重新排列剩余 toast
        this.toasts.forEach((t, i) => this.positionToast(t, i))
      }, 250)
    }

    showReady() {
      if (this.cfg.hideTrigger) return
      const delay = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--xzd-trigger-show-delay') || '0')
      const showFn = () => {
        if (this.hasIntegratedButton && this.el.themeBtn) {
          this.el.themeBtn.style.display = ''
          this.maybeBreathe()
        } else {
          this.el.trigger.style.display = 'flex'
          this.maybeBreathe()
        }
      }
      if (delay > 0) {
        setTimeout(showFn, delay * 1000)
      } else {
        showFn()
      }
      this.renderView()
    }

    maybeBreathe() {
      const rootStyle = getComputedStyle(document.documentElement)
      const breathingOff = rootStyle.getPropertyValue('--xzd-trigger-breathing').trim()
      const fabAnim = (rootStyle.getPropertyValue('--xzd-fab-animation') || '"breath"').replace(/"/g, '').trim()
      const target = (this.hasIntegratedButton && this.el.themeBtn) ? this.el.themeBtn : this.el.trigger
      if (!target) return

      // 清除所有动效class
      target.classList.remove('xzd-breathing', 'xzd-breathing-off', 'xzd-fab-pulse', 'xzd-fab-glow', 'xzd-fab-morph')

      // 应用选中的按钮动效
      const animClassMap = { breath: 'xzd-breathing', pulse: 'xzd-fab-pulse', glow: 'xzd-fab-glow', morph: 'xzd-fab-morph' }
      if (breathingOff === 'none' || fabAnim === 'none') {
        target.classList.add('xzd-breathing-off')
      } else if (animClassMap[fabAnim]) {
        target.classList.add(animClassMap[fabAnim])
      } else {
        target.classList.add('xzd-breathing') // fallback
      }
    }

    // ═══════════════════════════
    // DOM 更新
    // ═══════════════════════════
    /**
     * 根据悬浮按钮屏幕位置，动态计算弹窗弹出位置和transform-origin
     */
    _positionPanel() {
      var triggerEl = (this.hasIntegratedButton && this.el.themeBtn) ? this.el.themeBtn : this.el.trigger
      if (!triggerEl || this.mobile) return
            // Fix: when panel is open, trigger is hidden (display:none).
      // Temporarily show it to get true position, then re-hide.
      var wasHidden = triggerEl.style.display === 'none'
      if (wasHidden) triggerEl.style.display = 'flex'
      var btnRect = triggerEl.getBoundingClientRect()
      if (wasHidden) triggerEl.style.display = 'none'
      var panel = this.el.panel
      var gap = 12
      var edgePadding = 10

      // ★ 修复：检测 WP 后台管理栏高度，防止面板覆盖管理栏
      var wpAdminBarH = 0
      if (document.body.classList.contains('wp-admin')) {
        var adminBar = document.getElementById('wpadminbar')
        if (adminBar) wpAdminBarH = adminBar.offsetHeight || 32
      }

      var vw = window.innerWidth
      var vh = window.innerHeight
      var btnCenterX = btnRect.left + btnRect.width / 2
      var btnCenterY = btnRect.top + btnRect.height / 2
      var isLeft = btnCenterX < vw / 2
      var isTop = btnCenterY < vh / 2

      panel.style.top = 'auto'
      panel.style.bottom = 'auto'
      panel.style.left = 'auto'
      panel.style.right = 'auto'

      // 水平
      if (isLeft) {
        panel.style.left = Math.max(edgePadding, btnRect.left) + 'px'
        panel.style.transformOrigin = 'left ' + (isTop ? 'top' : 'bottom')
      } else {
        panel.style.right = Math.max(edgePadding, vw - btnRect.right) + 'px'
        panel.style.transformOrigin = 'right ' + (isTop ? 'top' : 'bottom')
      }

      // 垂直 — 固定600px，按钮在上方向下弹、在下方向上弹
      var FIXED_H = 600
      if (isTop) {
        // 按钮在上方 → 面板向下弹出
        var topPos = Math.max(edgePadding + wpAdminBarH, btnRect.bottom + gap)
        panel.style.top = topPos + 'px'
        var availDown = vh - topPos - edgePadding
        panel.style.height = Math.min(FIXED_H, Math.max(300, availDown)) + 'px'
      } else {
        // 按钮在下方 → 面板向上弹出
        var availUp = btnRect.top - gap - edgePadding - wpAdminBarH
        panel.style.height = Math.min(FIXED_H, Math.max(300, availUp)) + 'px'
        panel.style.bottom = Math.max(edgePadding, vh - btnRect.top + gap) + 'px'
      }
    }

    renderView() {
      if (this.open) {
        // 读取弹出动画变体
        if (!this._popupVariant) {
          var animVar = getComputedStyle(document.documentElement).getPropertyValue('--xzd-popup-animation').replace(/"/g, '').trim()
          this._popupVariant = animVar || 'scale'
        }
        // 应用变体class
        if (this._popupVariant === 'slide') {
          this.el.panel.classList.add('xzd-popup-slide')
        } else {
          this.el.panel.classList.remove('xzd-popup-slide')
        }
        this.el.panel.classList.add('xzd-open')
        this.el.panel.classList.toggle('xzd-fullscreen', this.mobile)
        this._positionPanel()
        // 移动端全屏模式不需要固定高度
        if (this.mobile) this.el.panel.style.height = ''
        // 移动端锁定body滚动，防止滚动穿透
        this._lockBodyScroll()
      } else if (this.closing) {
        this.el.panel.classList.remove('xzd-open')
        this.el.panel.style.height = ''
      } else {
        this.el.panel.classList.remove('xzd-open')
        this.showSessions = false
        this.el.panel.style.height = ''
        // 面板完全关闭后解锁body滚动
        this._unlockBodyScroll()
      }
      // Toast 位置随对话框开关联动
      this.toasts.forEach((t, i) => this.positionToast(t, i))
      // 触发按钮（编辑器页隐藏）
      if (!this.cfg.hideTrigger && (!this.hasIntegratedButton || !this.el.themeBtn)) {
        this.el.trigger.style.display = this.open ? 'none' : 'flex'
      } else if (this.cfg.hideTrigger) {
        this.el.trigger.style.display = 'none'
      }
      // 徽章
      this.renderBadge()
      // 快捷栏
      if (this.el.quickbar) {
        this.el.quickbar.style.display = (this.isAdmin && this.open && !this.showSessions) ? 'flex' : 'none'
      }
      if (this.el.statusBar) {
        this.el.statusBar.style.display = (this.open && !this.showSessions) ? 'flex' : 'none'
      }
      this.renderSessions()
      this.renderMessages()
    }

    renderBadge() {
      const badge = (this.hasIntegratedButton && this.el.themeBtnBadge) ? this.el.themeBtnBadge : this.el.badge
      if (!badge) return
      if (this.unreadCount > 0) {
        badge.style.display = 'flex'
        badge.textContent = this.unreadCount > 99 ? '99+' : this.unreadCount
      } else {
        badge.style.display = 'none'
      }
    }

    renderSessions() {
      const list = this.el.sessionList
      if (!list) return
      if (!this.showSessions) { this.el.sessions.style.display = 'none'; return }
      this.el.sessions.style.display = 'flex'
      if (this.sessions.length === 0) {
        list.innerHTML = '<div class="xzd-chat-empty-text">暂无历史对话</div>'
        return
      }
      list.innerHTML = this.sessions.map(s => {
        const active = s.session_key === this.activeSessionKey ? ' active' : ''
        return `<div class="xzd-chat-session-item${active}" data-session-key="${s.session_key}">
          <div class="xzd-chat-session-title">${escapeHtml(s.title || '新对话')}</div>
          <div class="xzd-chat-session-meta">
            <span>${s.message_count || 0} 条消息</span>
            <button class="xzd-chat-btn-delete" data-delete-session="${s.session_key}" title="删除">
              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
            </button>
          </div>
        </div>`
      }).join('')
    }

    renderMessages() {
      const container = this.el.messages
      if (!container || this.showSessions) return
      if (this.messages.length === 0 && !this.streaming) {
        // 保护 welcome 元素不被 innerHTML 清除
        var welcomeHTML = this.el.welcome ? this.el.welcome.outerHTML : ''
        if (!this.el.welcome || !container.querySelector('#xzd-chat-welcome')) {
          // welcome 元素已被销毁，用缓存HTML重建
          if (!welcomeHTML && this._cachedWelcome) welcomeHTML = this._cachedWelcome
        }
        container.innerHTML = ''
        if (welcomeHTML) {
          container.innerHTML = welcomeHTML
          this.el.welcome = $('#xzd-chat-welcome', container)
        }
        if (this.el.welcome) this.el.welcome.style.display = 'flex'
        this._fastPath = false
        return
      }
      this.el.welcome && (this.el.welcome.style.display = 'none')

      // ★ 修复：流式输出中不完整重建 — 由 handleChunk 增量更新
      if (this.streaming && this._streamingMsgIdx >= 0) {
        // 验证流式气泡引用仍然有效
        if (this._streamingContentEl && document.contains(this._streamingContentEl)) {
          return
        }
        // 引用丢失但流仍在进行 → 尝试从 DOM 恢复引用
        var msgs = this.el.messages.querySelectorAll('.xzd-chat-msg-assistant')
        if (this._streamingMsgIdx < msgs.length) {
          this._streamingContentEl = msgs[this._streamingMsgIdx].querySelector('.xzd-chat-content')
          if (this._streamingContentEl) {
            // 恢复文本内容
            var lastMsg = null
            for (var ri = this.messages.length - 1; ri >= 0; ri--) {
              if (this.messages[ri].role === 'assistant') { lastMsg = this.messages[ri]; break }
            }
            if (lastMsg) this._streamingContentEl.textContent = lastMsg.content || ''
            return
          }
        }
        // 完全无法恢复 → 重建流式消息索引，允许全量渲染
        this._streamingMsgIdx = -1
        this._streamingContentEl = null
      }

      const defAvt = escapeHtml(this.defaultAvatar || '')
      const userAvt = `<img src="${escapeHtml(this.userAvatar || defAvt)}" class="xzd-chat-avatar" width="32" height="32" alt="" onerror="if(this.src!==this.getAttribute('data-fb')){this.src=this.getAttribute('data-fb')}" data-fb="${defAvt}">`
      const aiAvt = `<img src="${escapeHtml(this.aiAvatar || defAvt)}" class="xzd-chat-avatar" width="32" height="32" alt="" onerror="if(this.src!==this.getAttribute('data-fb')){this.src=this.getAttribute('data-fb')}" data-fb="${defAvt}">`

      let html = ''
      let lastTime = 0
      for (let mi = 0; mi < this.messages.length; mi++) {
        const msg = this.messages[mi]
        const msgTime = msg.timestamp || 0
        // 微信风格时间分隔线：每5分钟或第一条消息显示
        const showTimeDivider = msgTime && (mi === 0 || (msgTime - lastTime) > 300000)
        if (showTimeDivider && msgTime) {
          const timeLabel = formatTimeLabel(msgTime)
          html += `<div class="xzd-chat-time-divider"><span>${timeLabel}</span></div>`
          lastTime = msgTime
        } else if (msgTime) {
          lastTime = msgTime
        }
        if (msg.role === 'user') {
          html += `<div class="xzd-chat-message xzd-chat-msg-user">`
          html += `<div class="xzd-chat-msg-content">`
          html += `<div class="xzd-chat-msg-meta"><span class="xzd-chat-msg-name">${escapeHtml(this.userName || '用户')}</span></div>`
          html += `<div class="xzd-chat-bubble xzd-chat-bubble-user"><div class="xzd-chat-content">${escapeHtml(msg.content)}</div></div>`
          html += `</div>`
          html += `<div class="xzd-chat-msg-avatar">${userAvt}</div>`
          html += `</div>`
        } else if (msg.role === 'assistant') {
          html += `<div class="xzd-chat-message xzd-chat-msg-assistant">`
          html += `<div class="xzd-chat-msg-avatar">${aiAvt}</div>`
          html += `<div class="xzd-chat-msg-content">`
          html += `<div class="xzd-chat-msg-meta"><span class="xzd-chat-msg-name">${escapeHtml(this.aiName || 'AI 助手')}</span></div>`
          html += `<div class="xzd-chat-bubble xzd-chat-bubble-ai"><div class="xzd-chat-content">${this.renderMarkdown(msg.content || '')}</div><button class="xzd-msg-copy-btn" data-copy-content="${escapeHtml(msg.content || '')}" title="复制"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button>`
          // 渲染主动推荐链接（suggestions数组）
          if (msg.suggestions && msg.suggestions.length > 0) {
            html += '<div class="xzd-suggestion-links">'
            for (const s of msg.suggestions) {
              if (s.url) html += '<a href="' + escapeHtml(s.url) + '" target="_blank" rel="noopener">' + escapeHtml(s.title || '') + '</a>'
              else html += '<span>' + escapeHtml(s.title || '') + '</span>'
            }
            html += '</div>'
          }
          // V3.1.9: 工具卡片已移除，AI文本已包含结果总结
          if (msg.confirm_required) {
            var keys = msg.confirm_keys || (msg.confirm_key ? [msg.confirm_key] : []);
            if (keys.length > 1) {
              // 批量确认：多个危险操作一起确认
              var batchKeysJson = escapeHtml(JSON.stringify(keys));
              html += '<div class="xzd-chat-confirm-card"><p>' + escapeHtml(msg.confirm_message || '确认执行以下操作？') + '</p><div class="xzd-chat-confirm-actions"><button class="xzd-chat-btn-confirm" data-confirm-keys="' + batchKeysJson + '">全部确认</button><button class="xzd-chat-btn-cancel" data-cancel-confirm>取消</button></div></div>'
            } else if (msg.confirm_choices && msg.confirm_choices.length > 0) {
              // 多选项确认卡片（重复处理等场景）
              var choiceBtns = msg.confirm_choices.map(function(c) {
                return '<button class="xzd-chat-btn-choice" data-confirm-key="' + escapeHtml(msg.confirm_key || '') + '" data-choice="' + escapeHtml(c.key) + '">' + escapeHtml(c.label) + '<small>' + escapeHtml(c.desc || '') + '</small></button>'
              }).join('')
              html += '<div class="xzd-chat-confirm-card xzd-chat-confirm-choices"><p>' + escapeHtml(msg.confirm_message || '已处理过，请选择操作') + '</p><div class="xzd-chat-confirm-actions choice-actions">' + choiceBtns + '</div></div>'
            } else {
              html += '<div class="xzd-chat-confirm-card"><p>' + escapeHtml(msg.confirm_message || '确认执行此操作？') + '</p><div class="xzd-chat-confirm-actions"><button class="xzd-chat-btn-confirm" data-confirm-key="' + escapeHtml(msg.confirm_key || '') + '">确认执行</button><button class="xzd-chat-btn-cancel" data-cancel-confirm>取消</button></div></div>'
            }
          }
          // 追问建议 pills（流结束后显示）
          if (!msg.streaming && !msg.thinking && msg._followups && msg._followups.length > 0) {
            html += '<div class="xzd-followup-pills">'
            for (var fi = 0; fi < msg._followups.length; fi++) {
              html += '<button class="xzd-followup-pill" data-hint="' + escapeHtml(msg._followups[fi]) + '">' + escapeHtml(msg._followups[fi]) + '</button>'
            }
            html += '</div>'
          }
          if (msg.thinking && !msg.content) {
            // 思考中状态：纯CSS动画轮播（3个span用animation-delay错开显隐）
            html += '<div class="xzd-chat-thinking"><span class="xzd-spinner"></span><span class="xzd-chat-thinking-text">正在思考<span class="xzd-chat-thinking-dots"><span></span><span></span><span></span></span></span></div>'
          } else if (msg.streaming && msg.content) {
            html += '<span class="xzd-chat-cursor"></span>'
          }
          html += '</div></div></div>'
        }
      }
      container.innerHTML = html
      this.scrollToBottom()
    }

    scrollToBottom() {
      if (this.el.messages) this.el.messages.scrollTop = this.el.messages.scrollHeight
    }

    /**
     * 流式输出：直接textContent更新，零DOM重建
     * 首次文本会通过 _ensureStreamingBubble() 创建气泡并缓存 _streamingContentEl 引用
     */
    _updateStreamingText(content) {
      var el = this._streamingContentEl
      if (!el) {
        // 引用丢失 → 尝试从DOM查找
        el = this.el.messages.querySelector('.xzd-chat-msg-assistant:last-child .xzd-chat-content')
        if (el) this._streamingContentEl = el
        else return // DOM完全丢失，等下次全量渲染
      }
      el.textContent = content
      this._scrollIfAtBottom()
    }

    /**
     * 纯文本转义 + 换行，不做 Markdown 解析
     */
    _escapeText(text) {
      var div = document.createElement('div')
      div.textContent = text
      return div.innerHTML.replace(/\n/g, '<br>')
    }

    /**
     * 仅当用户在底部（距底 ≤50px）时才自动滚动
     */
    _scrollIfAtBottom() {
      var el = this.el.messages
      if (!el) return
      var atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 50
      if (atBottom) el.scrollTop = el.scrollHeight
    }

    /**
     * 直接更新时间标签（不重建DOM）
     */
    _refreshTimeLabels() {
      var dividers = this.el.messages.querySelectorAll('.xzd-chat-time-divider span')
      var msgs = this.messages
      var dividerIdx = 0
      var lastTime = 0
      for (var mi = 0; mi < msgs.length; mi++) {
        var mt = msgs[mi].timestamp || 0
        if (mt && (mi === 0 || (mt - lastTime) > 300000)) {
          if (dividerIdx < dividers.length) {
            dividers[dividerIdx].textContent = formatTimeLabel(mt)
            dividerIdx++
          }
          lastTime = mt
        } else if (mt) {
          lastTime = mt
        }
      }
    }

    /**
     * 更新发送按钮为停止按钮
     */
    _updateSendBtnUI() {
      if (!this.el.sendBtn) return
      if (this.streaming) {
        this.el.sendBtn.classList.add('xzd-stop-btn')
        this.el.sendBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>'
        this.el.sendBtn.disabled = false
      } else {
        this.el.sendBtn.classList.remove('xzd-stop-btn')
        this.el.sendBtn.innerHTML = '<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>'
        this.toggleSendBtn()
      }
    }

    autoResize() {
      const el = this.el.input
      if (!el) return
      el.style.height = 'auto'
      el.style.height = Math.min(el.scrollHeight, 120) + 'px'
    }

    toggleSendBtn() {
      if (!this.el.sendBtn || !this.el.input) return
      const hasText = (this.el.input.value || '').trim().length > 0
      this.el.sendBtn.disabled = !hasText || this.streaming
    }

    renderMarkdown(text) {
      if (!text) return ''
      // 1. 转义 HTML
      var h = escapeHtml(text)
      // 2. 提取保护块：代码块 → 占位符
      var codeBlocks = []
      h = h.replace(/```(\w*)\n([\s\S]*?)```/g, function (_, lang, code) {
        var idx = codeBlocks.length
        codeBlocks.push({ lang: lang, code: code })
        return '___CB' + idx + '___'
      })
      // 3. 按双换行切分为段落块
      var blocks = h.split(/\n\n+/)
      var result = ''
      var inUl = false, inOl = false
      for (var bi = 0; bi < blocks.length; bi++) {
        var block = blocks[bi].trim()
        if (!block) continue
        var blockLines = block.split('\n')
        var allUlItems = true, allOlItems = true
        for (var li = 0; li < blockLines.length; li++) {
          var bline = blockLines[li].trim()
          if (!bline) continue
          if (!/^[\-\*]\s/.test(bline)) allUlItems = false
          if (!/^\d+\.\s/.test(bline)) allOlItems = false
        }
        if (allUlItems && blockLines.some(function(l) { return /^[\-\*]\s/.test(l.trim()) })) {
          if (!inUl) { result += '<ul>'; inUl = true }
          for (var ui = 0; ui < blockLines.length; ui++) {
            var uline = blockLines[ui].trim()
            if (!uline) continue
            result += '<li>' + this._renderInline(uline.replace(/^[\-\*]\s+/, '')) + '</li>'
          }
          continue
        } else if (inUl) { result += '</ul>'; inUl = false }
        if (allOlItems && blockLines.some(function(l) { return /^\d+\.\s/.test(l.trim()) })) {
          if (!inOl) { result += '<ol>'; inOl = true }
          for (var oi = 0; oi < blockLines.length; oi++) {
            var oline = blockLines[oi].trim()
            if (!oline) continue
            result += '<li>' + this._renderInline(oline.replace(/^\d+\.\s+/, '')) + '</li>'
          }
          continue
        } else if (inOl) { result += '</ol>'; inOl = false }
        if (inUl) { result += '</ul>'; inUl = false }
        if (inOl) { result += '</ol>'; inOl = false }
        if (/^###\s/.test(block)) {
          result += '<h3>' + this._renderInline(block.replace(/^###\s+/, '')) + '</h3>'
        } else if (/^##\s/.test(block)) {
          result += '<h2>' + this._renderInline(block.replace(/^##\s+/, '')) + '</h2>'
        } else if (/^#\s/.test(block)) {
          result += '<h1>' + this._renderInline(block.replace(/^#\s+/, '')) + '</h1>'
        } else if (/^>\s/.test(block)) {
          result += '<blockquote><p>' + this._renderInline(block.replace(/^>\s?/, '').replace(/\n/g, '<br>')) + '</p></blockquote>'
        } else if (/^---/.test(block) || /^\*\*\*/.test(block)) {
          result += '<hr>'
        } else if (/\|.*\|/.test(block)) {
          result += this._renderTable(block)
        } else if (this._isKeyValueBlock(blockLines)) {
          result += this._renderKeyValueGrid(blockLines)
        } else {
          result += '<p>' + this._renderInline(block.replace(/\n/g, '<br>')) + '</p>'
        }
      }
      if (inUl) result += '</ul>'
      if (inOl) result += '</ol>'
      // 4. 还原代码块
      for (var ci = 0; ci < codeBlocks.length; ci++) {
        var cb = codeBlocks[ci]
        var codeHtml = '<div class="xzd-code-block-wrapper"><pre><code>' + cb.code + '</code></pre><button class="xzd-code-copy-btn" data-code="' + escapeHtml(cb.code).replace(/"/g, '&quot;') + '">复制代码</button></div>'
        result = result.replace('___CB' + ci + '___', codeHtml)
      }
      return result
    }
    _renderInline(text) {
      if (!text) return ''
      var codes = []
      text = text.replace(/`([^`]+)`/g, function (_, code) {
        var idx = codes.length
        codes.push('<code>' + code + '</code>')
        return '___IC' + idx + '___'
      })
      text = text.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
      text = text.replace(/\*(.+?)\*/g, '<em>$1</em>')
      text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>')
      for (var ci = 0; ci < codes.length; ci++) {
        text = text.replace('___IC' + ci + '___', codes[ci])
      }
      return text
    }
    _renderTable(block) {
      var tableLines = block.split('\n').filter(function(l) { return l.trim() })
      if (tableLines.length < 2) return '<p>' + this._renderInline(block) + '</p>'
      var hasHeader = tableLines.length >= 2 && /^\|[\s\-:|]+\|$/.test(tableLines[1])
      var html = '<table>'
      var startRow = 0
      if (hasHeader) {
        html += '<thead><tr>'
        var headers = tableLines[0].split('|').filter(function(c) { return c.trim() })
        for (var hi = 0; hi < headers.length; hi++) {
          html += '<th>' + this._renderInline(headers[hi].trim()) + '</th>'
        }
        html += '</tr></thead><tbody>'
        startRow = 2
      } else {
        html += '<tbody>'
      }
      for (var ri = startRow; ri < tableLines.length; ri++) {
        var cells = tableLines[ri].split('|').filter(function(c) { return c.trim() })
        if (cells.length === 0) continue
        html += '<tr>'
        for (var ci = 0; ci < cells.length; ci++) {
          html += '<td>' + this._renderInline(cells[ci].trim()) + '</td>'
        }
        html += '</tr>'
      }
      html += '</tbody></table>'
      return html
    }
    _isKeyValueBlock(lines) {
      // 检测连续3行以上的 key: value 或 key：value 模式
      var kvLines = 0, totalLen = 0
      for (var i = 0; i < lines.length; i++) {
        var l = lines[i].trim()
        if (!l) continue
        // 匹配 "关键词: 值" 或 "关键词：值"（中英文冒号都支持）
        var m = l.match(/^([^:：\n]{1,20})[：:]\s*(.+)$/)
        if (m && m[1].length <= 20 && m[2].length > 0) {
          kvLines++
          totalLen += m[1].length
        }
      }
      // 至少3行且平均key长度≤12（过滤掉长句子伪匹配）
      return kvLines >= 3 && (totalLen / kvLines) <= 12
    }
    _renderKeyValueGrid(lines) {
      var items = []
      for (var i = 0; i < lines.length; i++) {
        var l = lines[i].trim()
        if (!l) continue
        var m = l.match(/^([^:：\n]{1,20})[：:]\s*(.+)$/)
        if (m) {
          items.push({ k: m[1].trim(), v: this._renderInline(m[2].trim()) })
        }
      }
      if (items.length === 0) return ''
      var html = '<div class="xzd-data-grid">'
      for (var j = 0; j < items.length; j++) {
        html += '<div class="xzd-data-kv"><span class="xzd-data-k">' + escapeHtml(items[j].k) + '</span><span class="xzd-data-v">' + items[j].v + '</span></div>'
      }
      html += '</div>'
      return html
    }

    // ═══════════════════════════
    // API 通信
    // ═══════════════════════════
    _base() { return (this.cfg.restUrl || '/wp-json/xzdbk-ai/v1/').replace(/\/$/, '') }
    _nonce() { return this.cfg.restNonce || '' }

    async apiGet(path) {
      try {
        const sep = path.includes('?') ? '&' : '?'
        const r = await fetch(this._base() + '/' + path.replace(/^\//, '') + sep + 'guest_token=' + encodeURIComponent(this.guestToken()), { headers: { 'X-WP-Nonce': this._nonce() } })
        return r.ok ? r.json() : null
      } catch (e) { return null }
    }
    async apiPost(path, body) {
      try {
        const b = body || {}
        const gt = this.guestToken()
        if (gt) b.guest_token = gt
        const sep = path.includes('?') ? '&' : '?'
        const r = await fetch(this._base() + '/' + path.replace(/^\//, '') + sep + 'guest_token=' + encodeURIComponent(gt), {
          method: 'POST', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': this._nonce() },
          body: JSON.stringify(b)
        })
        return r.ok ? r.json() : null
      } catch (e) { return null }
    }
    async apiDelete(path) {
      try {
        const sep = path.includes('?') ? '&' : '?'
        const r = await fetch(this._base() + '/' + path.replace(/^\//, '') + sep + 'guest_token=' + encodeURIComponent(this.guestToken()), {
          method: 'DELETE', headers: { 'X-WP-Nonce': this._nonce() }
        })
        return r.ok ? r.json() : null
      } catch (e) { return null }
    }

    // ═══════════════════════════
    // 会话管理
    // ═══════════════════════════
    async loadSessions() {
      const r = await this.apiGet('chat/sessions')
      if (r && r.success) this.sessions = (r.data && r.data.sessions) || []
      this.renderView()
    }
    async createSession() {
      const r = await this.apiPost('chat/sessions', { title: '新对话' })
      if (r && r.success && r.data && r.data.session_key) {
        this.activeSessionKey = r.data.session_key
        this.messages = []
        this.showSessions = false
        this.saveSessionKey()
        this.loadSessions()
      }
    }
    async switchSession(key) {
      this.activeSessionKey = key
      this.showSessions = false
      this.saveSessionKey()
      await this.loadMessages()
    }
    async loadMessages() {
      if (!this.activeSessionKey) { this.messages = []; this.renderView(); return }
      try {
        const r = await this.apiGet('chat/sessions/' + this.activeSessionKey)
        if (r && r.success && r.data && r.data.messages) {
          this.messages = r.data.messages.map(m => ({
            role: m.role, content: m.content || '',
            tool_calls: parseToolCalls(m.tool_calls),
            confirm_required: false, confirm_key: '', confirm_message: '',
            streaming: false, thinking: false,
            timestamp: m.timestamp || m.created_at ? new Date(m.timestamp || m.created_at).getTime() : Date.now()
          }))
        }
      } catch (_) {}
      // ★ 优先恢复 localStorage 中的完整数据（含工具结果状态），再以服务器数据补齐
      this.mergeLocalMessages()
      this.saveMessagesLocal()
      this.renderView()
    }

    mergeLocalMessages() {
      // ★ 修复：按内容指纹匹配而非索引，防止消息顺序不一致时工具数据错位
      try {
        const raw = sessionStorage.getItem(this._msgKey())
        if (!raw) return
        const local = JSON.parse(raw)
        if (!Array.isArray(local)) return
        // 构建本地消息的指纹映射（role + 内容前40字符）
        const localMap = new Map()
        for (const lm of local) {
          const fp = (lm.role || '') + ':' + (lm.content || '').slice(0, 40)
          if (!localMap.has(fp)) localMap.set(fp, lm)
        }
        // 按指纹匹配并恢复工具调用数据
        for (const sm of this.messages) {
          const fp = (sm.role || '') + ':' + (sm.content || '').slice(0, 40)
          const match = localMap.get(fp)
          if (match && match.tool_calls && match.tool_calls.length > 0) {
            sm.tool_calls = match.tool_calls
          }
          if (match && match.timestamp) {
            sm.timestamp = match.timestamp
          }
        }
      } catch (e) {}
    }

    saveMessagesLocal() {
      try {
        // V3.1.9: 最多保留50条，防止localStorage溢出
        const recent = this.messages.slice(-50)
        const data = recent.map(m => ({ role: m.role, content: m.content, tool_calls: m.tool_calls, timestamp: m.timestamp }))
        sessionStorage.setItem(this._msgKey(), JSON.stringify(data))
      } catch (e) {}
    }

    loadMessagesLocal() {
      try {
        const raw = sessionStorage.getItem(this._msgKey())
        if (raw) {
          this.messages = JSON.parse(raw).map(m => ({ ...m, streaming: false, thinking: false, confirm_required: false }))
          this.renderView()
        }
      } catch (e) {}
    }
    async deleteSession(key) {
      if (!(await this.showConfirm('删除对话', '确定删除此对话吗？删除后无法恢复。'))) return
      await this.apiDelete('chat/sessions/' + key)
      if (key === this.activeSessionKey) { this.activeSessionKey = ''; this.messages = []; this.clearSessionKey() }
      this.loadSessions()
    }

    /**
     * 自定义确认弹窗 — 主题适配，返回 Promise<boolean>
     */
    showConfirm(title, message) {
      return new Promise((resolve) => {
        const overlay = document.createElement('div')
        overlay.className = 'xzd-confirm-overlay'
        overlay.innerHTML = `<div class="xzd-confirm-card">
          <div class="xzd-confirm-title">${escapeHtml(title)}</div>
          <div class="xzd-confirm-body">${escapeHtml(message)}</div>
          <div class="xzd-confirm-actions">
            <button class="xzd-confirm-btn-cancel">取消</button>
            <button class="xzd-confirm-btn-ok">确认删除</button>
          </div>
        </div>`
        const panel = this.el.panel
        panel.appendChild(overlay)
        // 入场动画
        requestAnimationFrame(() => { overlay.classList.add('xzd-confirm-visible') })
        // 事件绑定
        const cleanup = (result) => {
          overlay.classList.remove('xzd-confirm-visible')
          setTimeout(() => { if (overlay.parentNode) overlay.parentNode.removeChild(overlay); resolve(result) }, 200)
        }
        overlay.querySelector('.xzd-confirm-btn-cancel').addEventListener('click', () => cleanup(false))
        overlay.querySelector('.xzd-confirm-btn-ok').addEventListener('click', () => cleanup(true))
        overlay.addEventListener('click', (e) => { if (e.target === overlay) cleanup(false) })
        document.addEventListener('keydown', function onEsc(e) {
          if (e.key === 'Escape') { document.removeEventListener('keydown', onEsc); cleanup(false) }
        })
      })
    }
    newSession() { this.activeSessionKey = ''; this.messages = []; this.clearSessionKey(); this.showSessions = false; this.renderView() }
    toggleSessions() { this.showSessions = !this.showSessions; if (this.showSessions) this.loadSessions(); this.renderView() }

    /**
     * 外部触发消息（Dashboard、快捷操作等调用）
     * 打开面板 → 填入消息 → 发送 → 显示结果
     */
    async triggerMessage(text) {
      if (!text || this.streaming) return
      // 确保面板打开
      if (!this.open) { this.open = true; this.renderView() }
      // 显示sessions列表关闭
      this.showSessions = false
      // 设置输入并发送（与send()流程一致）
      this.inputText = text
      this.el.input.value = text
      await this.send()
    }

    // ═══════════════════════════
    // 发送消息
    // ═══════════════════════════
    // 构建包含页面上下文的聊天消息
    _buildContextualMessage(text) {
      const ctx = this.monitor ? this.monitor.getPageContext() : { page_url: location.href, page_title: document.title, page_type: 'page', post_id: 0 }
      let header = ''
      if (ctx.page_type === 'post' || (document.querySelector('article, .post-content, .single-post'))) {
        header = '我在看这篇文章：' + (ctx.page_title || location.href)
      } else if (ctx.page_type === 'product') {
        header = '我在看这个产品：' + (ctx.page_title || location.href)
      } else if (ctx.page_type === 'forum_post') {
        header = '我在看这个论坛帖子：' + (ctx.page_title || location.href)
      }
      if (header && !text.startsWith(header)) {
        return header + '\n\n' + text
      }
      return text
    }

    async send() {
      // 授权检查
      if (!this.cfg.licenseActive) {
        this.messages.push({ role: 'assistant', content: '插件未授权，请前往 <a href="' + (this.cfg.isAdminPage ? (window.xzdConfig?.adminUrl || 'admin.php') + '?page=xzdbk-ai#/license' : 'javascript:void(0)') + '">授权管理</a> 页面激活授权。', timestamp: Date.now() })
        this.renderView()
        return
      }
      // 同步 hint 按钮设置的输入文本到 textarea
      if (this.inputText && !this.el.input.value) {
        this.el.input.value = this.inputText
        this.inputText = ''
      }
      const rawText = (this.el.input.value || '').trim()
      if (!rawText || this.streaming) return
      this.el.input.value = ''; this.autoResize()
      if (!this.activeSessionKey) { await this.createSession(); if (!this.activeSessionKey) return }
      // ★ 注入页面上下文
      const text = this._buildContextualMessage(rawText)
      this.showSessions = false
      this.messages.push({ role: 'user', content: rawText, timestamp: Date.now() })
      const aiMsg = { role: 'assistant', content: '', tool_calls: [], streaming: true, thinking: true, confirm_required: false, timestamp: Date.now() }
      this.messages.push(aiMsg)
      const aiIdx = this.messages.length - 1
      this._updateSendBtnUI()
      this.renderView()
      let ok = false
      try { ok = await this.tryStream(text, aiIdx) } catch (e) {}
      if (!ok) {
        try { await this.tryNonStream(text, aiIdx) } catch (e) {
          const m = this.messages[aiIdx]; if (m) { m.content = '抱歉，请求失败。请检查 AI 连接设置。'; m.streaming = false; m.thinking = false }
        }
      }
      this.streaming = false
      const lastMsg = this.messages[aiIdx]
      if (lastMsg) { lastMsg.thinking = false; lastMsg.streaming = false }
      this._updateSendBtnUI()
      // ★ 保存消息到 localStorage 做跨页持久化
      this.saveMessagesLocal()
      // ★ SSE 完成时面板未打开 → 未读+1
      if (!this.open) { this.unreadCount++; this.renderBadge() }
      this.renderView()
      this.loadSessions()
      this.loadQuota()
    }

    async tryStream(text, aiIdx) {
      const qs = 'session_key=' + encodeURIComponent(this.activeSessionKey) + '&message=' + encodeURIComponent(text) + '&guest_token=' + encodeURIComponent(this.guestToken())
      const url = this._base() + '/chat/stream?' + qs
      const ctrl = new AbortController(); this.streamAbort = ctrl; this.streaming = true; this._updateSendBtnUI(); this.renderView()
      let resp
      try {
        resp = await fetch(url, { headers: { 'Accept': 'text/event-stream', 'X-WP-Nonce': this._nonce() }, signal: ctrl.signal })
      } catch (e) { this.streaming = false; return e.name === 'AbortError' }
      if (!resp.ok || !resp.body) { this.streaming = false; return false }
      const reader = resp.body.getReader(); const decoder = new TextDecoder()
      let buf = ''; let got = false; let currentEvent = 'message'
      try {
        while (true) {
          const chunk = await reader.read(); if (chunk.done) break
          buf += decoder.decode(chunk.value, { stream: true })
          const lines = buf.split('\n'); buf = lines.pop() || ''
          for (const line of lines) {
            if (line.startsWith('event: ')) { currentEvent = line.slice(7).trim(); continue }
            if (!line.startsWith('data: ')) continue
            const raw = line.slice(6).trim(); if (!raw || raw === '[DONE]') continue
            const ch = safeJsonParse(raw); if (!ch) continue
            got = true; this.handleChunk(ch, aiIdx, currentEvent); currentEvent = 'message'
          }
        }
      } catch (e) { if (e.name === 'AbortError') return got; throw e }
      const m = this.messages[aiIdx]; if (m) m.streaming = false
      this.streaming = false
      // ★ 修复：避免双重渲染 — handleChunk('done') 已渲染则跳过
      if (got) {
        if (!this._gotDoneEvent) {
          this.renderMessages()
          var doneM = this.messages[aiIdx]
          if (doneM) this._generateFollowups(doneM)
        }
        this.scrollToBottom()
      }
      this._gotDoneEvent = false
      return got
    }

    handleChunk(ch, aiIdx, eventType) {
      const m = this.messages[aiIdx]; if (!m) return
      const type = ch.type || eventType || 'message'
      if (type === 'text' || (!type && ch.delta)) {
        if (m.thinking) {
          m.thinking = false
          // 首次文本到达 → 增量创建气泡DOM替换thinking占位，不重建整个列表
          this._ensureStreamingBubble()
        }
        m.content += ch.delta || ch.content || ''
        this._updateStreamingText(m.content)
      } else if (type === 'tool_exec') {
        if (!m.tool_calls) m.tool_calls = []
        m.tool_calls.push({ id: ch.tool_call_id || '', name: ch.tool_name || '', status: 'executing' })
        this._incrementalToolCard(m, ch.tool_call_id, ch.tool_name)
      } else if (type === 'tool_result') {
        if (m.tool_calls) {
          const tc = m.tool_calls.find(t => t.id === ch.tool_call_id)
          if (tc) {
            const result = safeJsonParse(ch.delta) || ch.delta
            tc.result = result
            // 捕获图片 URL 用于直显
            if (ch.image_url) tc.image_url = ch.image_url
            if (!ch.tool_success && result && result.confirm_required) {
              tc.status = 'confirm'
              m.confirm_required = true
              m.confirm_key = result.confirm_key || ''
              m.confirm_message = result.confirm_message || result.confirm_detail || '请确认操作'
              if (result.confirm_choices) m.confirm_choices = result.confirm_choices
              if (result.confirm_timeout) m.confirm_timeout = result.confirm_timeout
              this._appendConfirmCard(m)
            } else {
              tc.status = ch.tool_success ? 'success' : 'error'
            }
            // V3.1.9: 工具卡片已移除，状态跟踪由AI文本总结呈现
          }
        }
      } else if (type === 'confirm_required') {
        m.confirm_required = true; m.confirm_key = ch.confirm_key || ''; m.confirm_message = ch.confirm_message || ch.delta || '请确认操作'
        if (ch.confirm_choices) m.confirm_choices = ch.confirm_choices
        if (m.tool_calls && ch.tool_call_id) {
          const tc = m.tool_calls.find(t => t.id === ch.tool_call_id)
          if (tc && tc.status === 'executing') tc.status = 'confirm'
        }
        this._appendConfirmCard(m)
      } else if (type === 'error') {
        m.content += '\n\n❌ ' + (ch.error || ch.delta || '')
        this._appendErrorToBubble()
      } else if (type === 'done') {
        // ★ 修复：标记已收到 done 事件，防止 tryStream 重复渲染
        this._gotDoneEvent = true
        m.streaming = false
        // V3.1.9: 使用服务端返回的角色化追问
        if (ch.followups && Array.isArray(ch.followups)) {
          m._followups = ch.followups.slice(0, 3)
        }
        this._updateSendBtnUI()
        this._streamingContentEl = null
        this._streamingMsgIdx = -1
        // 流结束 → 一次完整渲染，应用markdown格式
        this.renderMessages()
        this.scrollToBottom()
      }
    }

    /**
     * 流式首次文本到达 → 用AI气泡DOM替换thinking占位符
     * 纯增量DOM操作，不触发renderMessages
     */
    _ensureStreamingBubble() {
      var container = this.el.messages
      if (!container) return
      // 查找thinking占位元素
      var thinkingEl = container.querySelector('.xzd-chat-thinking')
      if (!thinkingEl) {
        // thinking占位不存在（可能已被替换），降级到完整渲染
        this._streamingMsgIdx = -1
        this.renderMessages()
        return
      }
      // 找到thinking的父级消息wrapper
      var msgWrapper = thinkingEl.closest('.xzd-chat-message')
      if (!msgWrapper) { this.renderMessages(); return }

      // 构建AI气泡（增量DOM）
      var avatarHtml = this._buildAiAvatar()
      var bubble = document.createElement('div')
      bubble.className = 'xzd-chat-msg-content'
      bubble.innerHTML = '<div class="xzd-chat-msg-meta"><span class="xzd-chat-msg-name">' + escapeHtml(this.aiName || 'AI 助手') + '</span></div>' +
        '<div class="xzd-chat-bubble xzd-chat-bubble-ai">' +
        '<div class="xzd-chat-content"></div>' +
        '<button class="xzd-msg-copy-btn" title="复制"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button>' +
        '<span class="xzd-chat-cursor"></span>' +
        '</div>'

      // 替换：保留avatar，替换content部分
      var oldContent = msgWrapper.querySelector('.xzd-chat-msg-content')
      if (oldContent) {
        oldContent.parentNode.replaceChild(bubble, oldContent)
      } else {
        msgWrapper.appendChild(bubble)
      }

      // 保存引用，后续文本chunk直接textContent更新
      this._streamingContentEl = bubble.querySelector('.xzd-chat-content')
      // 记录正在流式输出的消息索引，用于增量append操作定位
      var msgs = container.querySelectorAll('.xzd-chat-msg-assistant')
      this._streamingMsgIdx = msgs.length - 1
    }

    /**
     * 构建AI头像HTML（与renderMessages保持一致）
     */
    _buildAiAvatar() {
      var defAvt = escapeHtml(this.defaultAvatar || '')
      var aiAvt = escapeHtml(this.aiAvatar || defAvt)
      return '<img src="' + aiAvt + '" class="xzd-chat-avatar" width="32" height="32" alt="" onerror="if(this.src!==this.getAttribute(\'data-fb\')){this.src=this.getAttribute(\'data-fb\')}" data-fb="' + defAvt + '">'
    }

    /**
     * 增量追加确认卡片到当前流式气泡底部
     */
    _appendConfirmCard(m) {
      if (!m) return
      var bubble = this._getStreamingBubble()
      if (!bubble) { this.renderMessages(); return }
      // 移除已有的确认卡片
      var existing = bubble.querySelector('.xzd-chat-confirm-card')
      if (existing) existing.parentNode.removeChild(existing)
      // 构建确认卡片
      var html = ''
      var keys = m.confirm_keys || (m.confirm_key ? [m.confirm_key] : [])
      if (keys.length > 1) {
        var batchKeysJson = escapeHtml(JSON.stringify(keys))
        html = '<div class="xzd-chat-confirm-card"><p>' + escapeHtml(m.confirm_message || '确认执行以下操作？') + '</p><div class="xzd-chat-confirm-actions"><button class="xzd-chat-btn-confirm" data-confirm-keys="' + batchKeysJson + '">全部确认</button><button class="xzd-chat-btn-cancel" data-cancel-confirm>取消</button></div></div>'
      } else if (m.confirm_choices && m.confirm_choices.length > 0) {
        var choiceBtns = m.confirm_choices.map(function(c) {
          return '<button class="xzd-chat-btn-choice" data-confirm-key="' + escapeHtml(m.confirm_key || '') + '" data-choice="' + escapeHtml(c.key) + '">' + escapeHtml(c.label) + '<small>' + escapeHtml(c.desc || '') + '</small></button>'
        }).join('')
        html = '<div class="xzd-chat-confirm-card xzd-chat-confirm-choices"><p>' + escapeHtml(m.confirm_message || '已处理过，请选择操作') + '</p><div class="xzd-chat-confirm-actions choice-actions">' + choiceBtns + '</div></div>'
      } else {
        html = '<div class="xzd-chat-confirm-card"><p>' + escapeHtml(m.confirm_message || '确认执行此操作？') + '</p><div class="xzd-chat-confirm-actions"><button class="xzd-chat-btn-confirm" data-confirm-key="' + escapeHtml(m.confirm_key || '') + '">确认执行</button><button class="xzd-chat-btn-cancel" data-cancel-confirm>取消</button></div></div>'
      }
      var card = document.createElement('div')
      card.innerHTML = html
      bubble.appendChild(card.firstElementChild)
      this.scrollToBottom()
    }

    /**
     * 增量追加错误文本到当前流式气泡
     */
    _appendErrorToBubble() {
      var contentEl = this._streamingContentEl
      if (!contentEl) {
        // 尝试从DOM查找
        contentEl = this.el.messages.querySelector('.xzd-chat-msg-assistant:last-child .xzd-chat-content')
      }
      if (contentEl) {
        // 取当前消息对象的最新content，直接textContent更新
        var lastAssist = null
        for (var i = this.messages.length - 1; i >= 0; i--) {
          if (this.messages[i].role === 'assistant') { lastAssist = this.messages[i]; break }
        }
        if (lastAssist) contentEl.textContent = lastAssist.content
      }
      this.scrollToBottom()
    }

    /**
     * 获取当前流式气泡的DOM引用
     */
    _getStreamingBubble() {
      var container = this.el.messages
      if (!container) return null
      if (this._streamingMsgIdx >= 0) {
        var msgs = container.querySelectorAll('.xzd-chat-msg-assistant')
        if (this._streamingMsgIdx < msgs.length) {
          return msgs[this._streamingMsgIdx].querySelector('.xzd-chat-bubble-ai')
        }
      }
      // 回退到最后一个
      var lastMsg = container.querySelector('.xzd-chat-msg-assistant:last-child')
      return lastMsg ? lastMsg.querySelector('.xzd-chat-bubble-ai') : null
    }

    /**
     * 增量添加工具卡片 — 不重建消息列表
     */
    _incrementalToolCard(msg, toolId, toolName) {
      // ★ 修复：使用 _streamingMsgIdx 精准定位流式消息，而非 :last-child
      var targetMsgEl = null
      if (this._streamingMsgIdx >= 0) {
        var msgs = this.el.messages.querySelectorAll('.xzd-chat-msg-assistant')
        if (this._streamingMsgIdx < msgs.length) {
          targetMsgEl = msgs[this._streamingMsgIdx]
        }
      }
      if (!targetMsgEl) {
        targetMsgEl = this.el.messages.querySelector('.xzd-chat-msg-assistant:last-child')
      }
      if (!targetMsgEl) { this.renderMessages(); return }
      var toolContainer = targetMsgEl.querySelector('.xzd-chat-tool-calls')
      if (!toolContainer) {
        // 第一张工具卡片 — 创建容器
        var bubble = targetMsgEl.querySelector('.xzd-chat-bubble-ai')
        if (!bubble) { this.renderMessages(); return }
        toolContainer = document.createElement('div')
        toolContainer.className = 'xzd-chat-tool-calls'
        bubble.appendChild(toolContainer)
      }
      var rawName = toolName || toolId || ''
      var name = TOOL_NAMES[rawName] || rawName
      var card = document.createElement('div')
      card.className = 'xzd-chat-tool-card xzd-tc-pending'
      card.setAttribute('data-tool-id', toolId)
      var execLabel = STATUS_LABELS.executing || { icon: '⏳', text: '执行中' }
      card.innerHTML = '<div class="xzd-tc-header"><span class="xzd-tc-status">' + execLabel.icon + '</span><span class="xzd-tc-name">' + escapeHtml(name) + '</span><span class="xzd-tc-toggle xzd-tc-expanded">收起</span></div><div class="xzd-tc-detail xzd-tc-expanded"></div>'
      toolContainer.appendChild(card)
      this.scrollToBottom()
    }

    /**
     * 更新工具卡片状态 — 仅改className，不重建
     */
    _updateToolCardStatus(toolId, status, resultData) {
      var card = this.el.messages.querySelector('.xzd-chat-tool-card[data-tool-id="' + toolId + '"]')
      if (!card) { this.renderMessages(); return }
      var clsMap = { success:'xzd-tc-success', error:'xzd-tc-error', confirm:'xzd-tc-confirm', executing:'xzd-tc-pending', running:'xzd-tc-pending', pending:'xzd-tc-pending' }
      card.className = 'xzd-chat-tool-card ' + (clsMap[status] || 'xzd-tc-pending')
      var statusEl = card.querySelector('.xzd-tc-status')
      if (statusEl) {
        var sl = STATUS_LABELS[status] || STATUS_LABELS.pending
        statusEl.textContent = sl.icon
      }
      // 增量更新卡片内容：tool_result 到达时直接显示结果数据
      if (resultData && resultData !== null && resultData !== undefined) {
        var detail = card.querySelector('.xzd-tc-detail')
        if (detail) {
          if (typeof resultData === 'string') {
            detail.innerHTML = '<div class="xzd-tc-section xzd-tc-data-text">' + escapeHtml(resultData) + '</div>'
          } else if (typeof resultData === 'object' && !Array.isArray(resultData)) {
            var items = Object.entries(resultData).map(function(e) {
              return '<span class="xzd-tc-kv"><span class="xzd-tc-k">' + escapeHtml(KEY_LABELS[e[0]] || e[0]) + '</span><span class="xzd-tc-v">' + formatValue(e[1]) + '</span></span>'
            }).join('')
            detail.innerHTML = '<div class="xzd-tc-section xzd-tc-data-grid">' + items + '</div>'
          }
        }
      }
    }

    /**
     * 停止生成 — 中止 SSE 流
     */
    stopStreaming() {
      if (!this.streaming) return
      if (this.streamAbort) {
        try { this.streamAbort.abort() } catch (e) {}
        this.streamAbort = null
      }
      this.streaming = false
      this._streamingContentEl = null
      this._streamingMsgIdx = -1
      var lastMsg = this.messages[this.messages.length - 1]
      if (lastMsg && lastMsg.role === 'assistant') {
        lastMsg.streaming = false
        lastMsg.thinking = false
        if (!lastMsg.content) lastMsg.content = '⏸️ 已停止生成'
      }
      this._updateSendBtnUI()
      this.renderMessages()
    }

    /**
     * 复制消息内容
     */
    copyMessage(content) {
      if (!content || !navigator.clipboard) return
      navigator.clipboard.writeText(content).catch(function () {})
    }

    /**
     * 生成追问建议 — AI回复完成后自动生成3个追问pill
     */
    // V3.1.9: 追问建议由服务端生成（按角色区分），此处仅做兜底
    _generateFollowups(msg) {
      if (!msg || !msg.content) return
      // 服务端已在 done/send 响应中返回 followups 数组
      // 前端从 msg._followups 读取（由 processStreamEvent/send 设置）
    }

    _renderNow() {
      this.renderMessages()
    }

    async tryNonStream(text, aiIdx) {
      const r = await this.apiPost('chat/send', { session_key: this.activeSessionKey, message: text })
      const m = this.messages[aiIdx]; if (!m) return
      m.thinking = false
      if (!r || !r.success) { m.content = (r && r.message) || '请求失败'; m.streaming = false; return }
      const d = r.data || {}
      m.content = d.content || ''; m.tool_calls = parseToolCalls(d.tool_calls)
      m.confirm_required = d.confirm_required || false; m.confirm_key = d.confirm_key || ''
      m.confirm_message = d.confirm_message || d.confirm_detail || ''
      // V3.1.9: 服务端返回角色化追问
      if (d.followups && Array.isArray(d.followups)) m._followups = d.followups.slice(0, 3)
      m.streaming = false
    }

    // ═══════════════════════════
    // 确认操作
    // ═══════════════════════════
    async confirmAction(key, choice) {
      let msg = null
      for (let i = this.messages.length - 1; i >= 0; i--) {
        if (this.messages[i].confirm_required && this.messages[i].confirm_key === key) { msg = this.messages[i]; break }
      }
      if (!msg) return
      msg.confirm_required = false; this.renderView()
      var body = { session_key: this.activeSessionKey, confirm_key: key }
      if (choice) body.repeat_choice = choice
      const r = await this.apiPost('chat/send', body)
      var labelMap = { overwrite: '覆盖原有', append: '追加共存', skip: '跳过' }
      var choiceLabel = labelMap[choice] || choice || '操作'
      if (r && r.success) {
        var resultData = (r.data && r.data.result) ? r.data.result : null
        var resultMsg = (r.data && r.data.message) ? r.data.message : (r.message || '')
        msg.content += '\n\n✅ 已选择「' + choiceLabel + '」' + (resultMsg ? ' — ' + resultMsg : '')
        // Update the pending/confirm tool card with result
        if (msg.tool_calls && resultData) {
          for (var ti = msg.tool_calls.length - 1; ti >= 0; ti--) {
            var tc = msg.tool_calls[ti]
            if (tc.status === 'executing' || tc.status === 'confirm') {
              tc.status = resultData.success ? 'success' : 'error'
              tc.result = resultData
              tc.confirm_required = false
              break
            }
          }
        }
        // ★ 确认后 AI 继续多步骤任务 — 追加新消息
        var continueContent = (r.data && r.data.continue_content) || ''
        var continueToolCalls = (r.data && r.data.continue_tool_calls) || null
        var continueConfirmReq = (r.data && r.data.continue_confirm_required) || false
        var continueConfirmKey = (r.data && r.data.continue_confirm_key) || ''
        if (continueContent || continueToolCalls) {
          var newMsg = {
            role: 'assistant',
            content: continueContent,
            tool_calls: parseToolCalls(continueToolCalls) || [],
            streaming: false,
            thinking: false,
            confirm_required: continueConfirmReq,
            confirm_key: continueConfirmKey,
            confirm_message: '',
            timestamp: Date.now()
          }
          this.messages.push(newMsg)
        }
      } else {
        msg.content += '\n\n❌ ' + ((r && r.message) || '操作失败')
        // Mark pending/confirm tool card as error
        if (msg.tool_calls) {
          for (var ti2 = msg.tool_calls.length - 1; ti2 >= 0; ti2--) {
            var tc2 = msg.tool_calls[ti2]
            if (tc2.status === 'executing' || tc2.status === 'confirm') { tc2.status = 'error'; break }
          }
        }
      }
      this.renderView()
    }

    cancelConfirm() {
      for (let i = this.messages.length - 1; i >= 0; i--) {
        if (this.messages[i].confirm_required) { this.messages[i].confirm_required = false; this.messages[i].content += '\n\n⏸️ 已取消。'; this.renderView(); break }
      }
    }

    async confirmBatch(keysJson) {
      var keys = [];
      try { keys = JSON.parse(keysJson); } catch(e) { return }
      if (!keys.length) return

      // 清除确认状态
      for (var i = this.messages.length - 1; i >= 0; i--) {
        if (this.messages[i].confirm_required) {
          this.messages[i].confirm_required = false
        }
      }
      this.renderView()

      // 逐个确认执行
      var allOk = true
      for (var ki = 0; ki < keys.length; ki++) {
        var r = await this.apiPost('chat/send', { session_key: this.activeSessionKey, confirm_key: keys[ki] })
        if (!r || !r.success) {
          allOk = false
        }
      }

      // 追加结果到最新助手消息
      for (var mi = this.messages.length - 1; mi >= 0; mi--) {
        if (this.messages[mi].role === 'assistant') {
          this.messages[mi].content += allOk ? '\n\n✅ 已全部确认执行' : '\n\n⚠️ 部分操作执行失败'
          break
        }
      }
      this.renderView()
    }

    async loadQuota() {
      const r = await this.apiGet('chat/quota')
      if (r && r.success && r.data) {
        this.quotaText = r.data.unlimited ? '无限' : ((r.data.remaining || '?') + ' / ' + (r.data.limit || '?'))
      }
      if (this.el.quota) this.el.quota.textContent = this.quotaText
      if (this.el.identity) this.el.identity.textContent = this.getUserIdentity()
    }

    /**
     * 用户身份识别 — 游客 / WP角色(订阅者/作者/编辑/管理员等) 中文映射
     */
    getUserIdentity() {
      if (!this.cfg.isLoggedIn) return '游客'
      const ROLE_MAP = {
        administrator: '管理员',
        editor: '编辑',
        author: '作者',
        contributor: '贡献者',
        subscriber: '订阅者',
      }
      const roles = this.userRoles || []
      // 按优先级匹配：管理员 > 编辑 > 作者 > 贡献者 > 订阅者
      for (const r of ['administrator', 'editor', 'author', 'contributor', 'subscriber']) {
        if (roles.indexOf(r) !== -1) return ROLE_MAP[r]
      }
      // 兜底：有角色但非标准WP角色
      if (roles.length > 0) return this.userName || '已登录'
      return '已登录'
    }

    /**
     * 移动端锁定 body 滚动，防止对话框滚动时背景页面跟着动
     */
    _lockBodyScroll() {
      if (!this.mobile) return
      this._bodyScrollY = window.scrollY
      document.body.style.position = 'fixed'
      document.body.style.top = '-' + this._bodyScrollY + 'px'
      document.body.style.width = '100%'
      document.body.style.overflowY = 'scroll'
    }

    /**
     * 解锁 body 滚动，恢复页面原始位置
     */
    _unlockBodyScroll() {
      if (!this.mobile) return
      document.body.style.position = ''
      document.body.style.top = ''
      document.body.style.width = ''
      document.body.style.overflowY = ''
      window.scrollTo(0, this._bodyScrollY || 0)
    }

    togglePanel() {
      // ★ 修复：关闭动画进行中时忽略点击，防止竞态
      if (this.closing) return
      this.open = !this.open
      if (this.open) {
        this.unreadCount = 0
        this.renderBadge()
        delete this._popupVariant  // 重新读取CSS变量
        // ★ 修复：重新打开时启动 Canvas 动画
        this._fxRunning = true
        if (this._fxDraw) requestAnimationFrame(this._fxDraw)
        const target = (this.hasIntegratedButton && this.el.themeBtn) ? this.el.themeBtn : this.el.trigger
        if (target) {
          target.classList.remove('xzd-breathing', 'xzd-breathing-off', 'xzd-fab-pulse', 'xzd-fab-glow', 'xzd-fab-morph')
        }
        this.renderView()
        setTimeout(() => { if (this.el.input) this.el.input.focus() }, 200)
      } else {
        this.maybeBreathe()
        this.closePanel()
      }
    }

    closePanel() {
      // ★ 修复：已在关闭动画中则忽略
      if (this.closing) return
      if (this._timeRefresh) { clearInterval(this._timeRefresh); this._timeRefresh = null }
      if (this.streamAbort) { try { this.streamAbort.abort() } catch (e) {}; this.streaming = false }
      this._updateSendBtnUI()
      delete this._popupVariant

      // ★ 修复：Canvas 动画完全停止省电
      this._fxRunning = false

      var self = this
      this.closing = true
      this._closeGeneration++
      var closeGen = this._closeGeneration   // 捕获当前代次

      var onTransitionEnd = function () {
        self.el.panel.removeEventListener('transitionend', onTransitionEnd)
        // ★ 修复：代次不匹配说明期间被 reopen，忽略
        if (self._closeGeneration !== closeGen) return
        self.open = false
        self.closing = false
        self.showSessions = false
        self.renderView()
      }
      this.el.panel.addEventListener('transitionend', onTransitionEnd, { once: false })

      this.renderView()

      // 兜底：350ms后强制清理（防止transitionend不触发）
      setTimeout(function () {
        self.el.panel.removeEventListener('transitionend', onTransitionEnd)
        // ★ 修复：代次不匹配则忽略
        if (self._closeGeneration !== closeGen) return
        if (self.closing) {
          self.open = false
          self.closing = false
          self.showSessions = false
          self.renderView()
        }
      }, 350)
    }

    _sessionKey() { return 'xzd_chat_session_' + (this.cfg.userId || 0) + '_' + this.guestToken() }
    _msgKey(sk) { return 'xzd_chat_msgs_' + (this.cfg.userId || 0) + '_' + (sk || this.activeSessionKey) + '_' + this.guestToken() }
    saveSessionKey() { try { sessionStorage.setItem(this._sessionKey(), this.activeSessionKey) } catch (e) {} }
    clearSessionKey() { try { sessionStorage.removeItem(this._sessionKey()) } catch (e) {} }
    restoreSession() { try { const k = sessionStorage.getItem(this._sessionKey()); if (k) { this.activeSessionKey = k; this.loadMessages() } } catch (e) {} }

    // 访客令牌：UUID v4，存 sessionStorage，关浏览器即清除
    guestToken() {
      if (this._guestToken) return this._guestToken
      if (this.cfg.userId && this.cfg.userId > 0) return ''
      try {
        let t = sessionStorage.getItem('xzd_guest_token')
        if (!t) { t = this._uuid(); sessionStorage.setItem('xzd_guest_token', t) }
        this._guestToken = t
        return t
      } catch (e) { return '' }
    }
    _uuid() {
      return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
        var r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8)
        return v.toString(16)
      })
    }
    // 获取 guest_token 参数，用于拼接到 URL 或 body 中
    _guestParam() {
      const t = this.guestToken()
      return t ? '&guest_token=' + encodeURIComponent(t) : ''
    }
  }

  // ═══════════════════════════════
  // 启动
  // ═══════════════════════════════
  function boot() {
    if (typeof xzdChatConfig === 'undefined') { setTimeout(boot, 50); return }
    if (!xzdChatConfig.userId && xzdChatConfig.userId !== 0) return
    const chat = new FloatingChat(xzdChatConfig)
    chat.init()
    window.xzdChatInstance = chat
  }

  if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', boot) }
  else { boot() }
})()
