Linux.do 帖子多功能助手

1.自动收集帖子内容并使用AI总结。 2.标记已回复。 3.增加发布者标签。 4.始皇曰解密

// ==UserScript==
// @name         Linux.do 帖子多功能助手
// @namespace    https://greasyfork.org/zh-CN/scripts/547708-linux-do-%E5%B8%96%E5%AD%90%E5%A4%9A%E5%8A%9F%E8%83%BD%E5%8A%A9%E6%89%8B
// @version      1.0.1
// @description  1.自动收集帖子内容并使用AI总结。 2.标记已回复。 3.增加发布者标签。 4.始皇曰解密
// @author       lishizhen
// @icon         https://www.google.com/s2/favicons?sz=64&domain=linux.do
// @match        https://linux.do/*
// @grant        GM.download
// @grant        GM.xmlHttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @grant        GM_addStyle
// @require      https://cdn.jsdelivr.net/npm/[email protected]/lib/marked.umd.min.js
// @connect      *
// @license      MIT
// ==/UserScript==

(function () {
  'use strict';

  // =============================================================================
  // 配置与常量
  // =============================================================================

  const CONFIG_KEY = 'LINUXDO_AI_SUMMARIZER_CONFIG_V6';
  const CACHE_PREFIX = 'LINUXDO_SUMMARY_CACHE_';

  const AI_ICON_SVG = `<svg viewBox="0 0 24 24" fill="none">
  <path d="M12 2c-4.4 0-8 3.6-8 8 0 2.1.8 4.1 2.3 5.6.4.4.7 1 .7 1.6v1.8c0 .6.4 1 1 1h8c.6 0 1-.4 1-1v-1.8c0-.6.3-1.2.7-1.6C19.2 14.1 20 12.1 20 10c0-4.4-3.6-8-8-8z" stroke="currentColor" stroke-width="2" fill="none"/>
  <circle cx="9" cy="9" r="1" fill="currentColor"/>
  <circle cx="15" cy="9" r="1" fill="currentColor"/>
  <path d="M9 13c.8.8 2.4.8 3.2 0" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
  <circle cx="18" cy="6" r="2" fill="#ffd700" opacity="0.8"/>
  <circle cx="6" cy="6" r="1.5" fill="#ff6b6b" opacity="0.8"/>
  <circle cx="19" cy="14" r="1" fill="#4ecdc4" opacity="0.8"/>
</svg>`;
  const POSTED_ICON_SVG = `<svg class="fa d-icon d-icon-circle svg-icon svg-string" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"><use href="#circle"></use></svg>`
  const DEFAULT_CONFIG = {
    apiProvider: 'openai',
    openai: {
      apiKey: '',
      baseUrl: 'https://api.openai.com',
      model: 'gpt-4o-mini'
    },
    gemini: {
      apiKey: 'AIzaSyCD4E-8rV6IrBCiP8cTqTE1wuYHfmRjCaQ',
      baseUrl: 'https://generativelanguage.googleapis.com',
      model: 'gemini-2.5-flash-lite-preview-06-17'
    },
    prompt: `你是一个善于总结论坛帖子的 AI 助手。请根据以下包含了楼主和所有回复的帖子内容,进行全面、客观、精炼的总结。总结应涵盖主要观点、关键信息、不同意见的交锋以及最终的普遍共识或结论。请使用简体中文,并以 Markdown 格式返回,以便于阅读。\n\n帖子内容如下:\n---\n{content}\n---`
  };

  // =============================================================================
  // 全局状态管理
  // =============================================================================

  class AppState {
    constructor() {
      this.reset();
    }

    reset() {
      this.status = 'idle'; // idle, collecting, collected, finished
      this.posts = [];
      this.processedIds = new Set();
      this.cachedSummary = null;
      this.currentSummaryText = null;
      this.topicData = null;
      this.isStreaming = false;
      this.streamController = null;
    }

    addPost(post) {
      if (!this.processedIds.has(post.id)) {
        this.posts.push(post);
        this.processedIds.add(post.id);
        return true;
      }
      return false;
    }

    clearPosts() {
      this.posts = [];
      this.processedIds.clear();
    }

    randomPosts() {
      // 确保始终包含第一条帖子,然后对剩余帖子进行随机抽样
      if (this.posts.length === 0) return [];

      // 过滤有效内容的帖子
      const validPosts = this.posts.filter(m => m.content.length >= 4);
      if (validPosts.length === 0) return [];

      // 第一条帖子(通常是楼主帖)
      const firstPost = validPosts[0];
      const result = [firstPost];

      // 如果只有一条帖子或需要的数量为1,直接返回第一条
      const maxCount = Math.min(validPosts.length, 100);
      if (maxCount <= 1) return result;

      // 对剩余帖子进行随机抽样
      const remainingPosts = validPosts.slice(1);
      const shuffled = [...remainingPosts].sort(() => 0.5 - Math.random());
      const sampled = shuffled.slice(0, maxCount - 1);

      return result.concat(sampled);

    }

    getTopicId() {
      const match = window.location.pathname.match(/\/t\/[^\/]+\/(\d+)/);
      return match ? match[1] : null;
    }

    getCacheKey() {
      const topicId = this.getTopicId();
      return topicId ? `${CACHE_PREFIX}${topicId}` : null;
    }

    isTopicPage() {
      return /\/t\/[^\/]+\/\d+/.test(window.location.pathname);
    }

    loadCache() {
      const cacheKey = this.getCacheKey();
      if (cacheKey) {
        this.cachedSummary = GM_getValue(cacheKey, null);
        if (this.cachedSummary) {
          const tempDiv = document.createElement('div');
          tempDiv.innerHTML = this.cachedSummary;
          this.currentSummaryText = tempDiv.textContent || tempDiv.innerText || '';
        }
      }
    }

    saveCache(summary) {
      const cacheKey = this.getCacheKey();
      if (cacheKey) {
        this.cachedSummary = summary;
        GM_setValue(cacheKey, summary);
      }
    }

    stopStreaming() {
      this.isStreaming = false;
      if (this.streamController) {
        this.streamController.abort = true;
      }
    }
  }

  const appState = new AppState();

  // =============================================================================
  // API 调用类
  // =============================================================================

  class TopicAPI {
    constructor() {
      this.baseUrl = window.location.origin;
    }

    async fetchTopicData(topicId, postNumber = 1) {
      let url = `${this.baseUrl}/t/${topicId}/${postNumber}.json`;
      if (postNumber == 1) {
        url = `${this.baseUrl}/t/${topicId}.json`;
      }
      return new Promise((resolve, reject) => {
        GM.xmlHttpRequest({
          method: 'GET',
          url: url,
          headers: {
            'Accept': 'application/json',
            'X-Requested-With': 'XMLHttpRequest'
          },
          responseType: 'json',
          onload: (response) => {
            if (response.status === 200) {
              resolve(response.response);
            } else {
              reject(new Error(`API 请求失败: ${response.status} ${response.statusText}`));
            }
          },
          onerror: () => reject(new Error('网络请求失败'))
        });
      });
    }
    async getTopicData(topicId) {
      try {
        const response = await this.fetchTopicData(topicId);
        if (!response || !response.id) {
          throw new Error('获取帖子数据失败,可能是帖子不存在或已被删除');
        }
        return response;
      } catch (error) {
        console.error('助手脚本:获取帖子数据失败:', error);
        throw error;
      }
    }

    async getAllPosts(topicId, callback) {
      const posts = [];
      const processedIds = new Set();
      let totalPosts = 0;
      let topicData = null;

      try {
        // 获取第一页数据来确定总帖子数
        const firstResponse = await this.fetchTopicData(topicId, 1);
        topicData = {
          id: firstResponse.id,
          title: firstResponse.title,
          fancy_title: firstResponse.fancy_title,
          posts_count: firstResponse.posts_count
        };

        totalPosts = firstResponse.posts_count;
        console.log(`助手脚本:开始收集帖子,总计 ${totalPosts} 条`);
        let currentPostNumber = 0;
        // 处理第一页的帖子 (1-20)
        if (firstResponse.post_stream && firstResponse.post_stream.posts) {
          firstResponse.post_stream.posts.forEach(post => {
            if (!processedIds.has(post.id)) {
              const cleanContent = this.cleanPostContent(post.cooked);
              if (cleanContent) {
                posts.push({
                  id: post.id,
                  username: post.username || post.name || '未知用户',
                  content: cleanContent
                });
                processedIds.add(post.id);
              } else {
                console.info(`助手脚本:跳过内容太短的帖子 ${post.post_number} - ${post.id}:${post.cooked}`);
              }
            }
            currentPostNumber = post.post_number;
          });
        }
        callback && callback(posts, topicData);
        // 如果总帖子数大于10,需要继续收集
        if (totalPosts > 10) {
          // 使用较小的区间,每次递增10

          while (currentPostNumber < totalPosts) {
            try {
              const response = await this.fetchTopicData(topicId, currentPostNumber);
              if (!response.post_stream || !response.post_stream.posts) {
                console.warn(`助手脚本:第 ${currentPost} 条附近没有返回有效数据`);
                currentPost += 10;
                continue;
              }

              let newPostsCount = 0;
              let lastNumber = 0;
              response.post_stream.posts.forEach(post => {
                if (!processedIds.has(post.id)) {
                  const cleanContent = this.cleanPostContent(post.cooked);
                  if (cleanContent) {
                    posts.push({
                      id: post.id,
                      username: post.username || post.name || '未知用户',
                      content: cleanContent
                    });
                    processedIds.add(post.id);
                    newPostsCount++;
                  } else {
                    console.info(`助手脚本:跳过内容太短的帖子 ${post.post_number} - ${post.id}:${post.cooked}`);
                  }
                }
                lastNumber = post.post_number;
              });

              // 较小的递增步长,减少遗漏
              if (lastNumber > 0) {
                currentPostNumber = lastNumber + 1; // 直接跳到最后一个帖子后面
              } else {
                currentPostNumber += (newPostsCount > 0 ? newPostsCount : 10);
              }
              callback && callback(posts, topicData);
              // 添加延时避免请求过快
              await new Promise(resolve => setTimeout(resolve, 300));

            } catch (error) {
              console.warn(`助手脚本:获取第 ${currentPost} 条附近数据失败:`, error);
              currentPost += 10; // 继续下一个区间
            }
          }
        }

        // 按帖子ID排序确保顺序正确
        posts.sort((a, b) => parseInt(a.id) - parseInt(b.id));

        console.log(`助手脚本:收集完成,共获得 ${posts.length}/${totalPosts} 条有效帖子`);
        return { posts, topicData };

      } catch (error) {
        console.error('助手脚本:收集帖子失败:', error);
        throw error;
      }
    }

    cleanPostContent(htmlContent) {
      if (!htmlContent) return '';

      // 创建临时div来处理HTML
      const tempDiv = document.createElement('div');
      tempDiv.innerHTML = htmlContent;

      // 移除引用、代码显示按钮等不需要的元素
      tempDiv.querySelectorAll('aside.quote, .cooked-selection-barrier, .action-code-show-code-btn, .lightbox-wrapper').forEach(el => el.remove());

      // 获取纯文本内容
      const content = tempDiv.innerText.trim().replace(/\n{2,}/g, '\n');

      // 过滤掉太短的内容
      return content;
    }
  }

  const topicAPI = new TopicAPI();

  // =============================================================================
  // 流式渲染类
  // =============================================================================

  class StreamRenderer {
    constructor(container) {
      this.container = container;
      this.content = '';
      this.lastRenderedLength = 0;

      // 配置marked
      if (typeof marked !== 'undefined') {
        marked.setOptions({
          breaks: true,
          gfm: true,
          sanitize: false
        });
      }
    }

    appendContent(chunk) {
      this.content += chunk;
      this.render();
    }

    setContent(content) {
      this.content = content;
      this.lastRenderedLength = 0;
      this.render();
    }

    render() {
      if (typeof marked === 'undefined') {
        // 降级到简单的文本渲染
        this.container.innerHTML = this.content.replace(/\n/g, '<br>');
        return;
      }

      try {
        // 只渲染新增的内容部分,提高性能
        const newContent = this.content.slice(this.lastRenderedLength);
        if (newContent.trim()) {
          const htmlContent = marked.parse(this.content);
          this.container.innerHTML = htmlContent;
          this.lastRenderedLength = this.content.length;

          this.scrollHandle();
        }
      } catch (error) {
        console.error('Markdown渲染失败:', error);
        // 降级到纯文本
        this.container.innerHTML = this.content.replace(/\n/g, '<br>');
      }
    }
    scrollHandle() {
      // 滚动到底部
      // 滚动父级容器到底部
      if (this.container.parentElement) {
        this.container.parentElement.scrollTop = this.container.parentElement.scrollHeight;
      } else {
        this.container.scrollTop = this.container.scrollHeight;
      }
    }
    clear() {
      this.content = '';
      this.lastRenderedLength = 0;
      this.container.innerHTML = '';
    }

    addTypingIndicator() {
      // 创建一个包裹容器
      const wrapper = document.createElement('div');
      wrapper.className = 'typing-indicator-wrapper';

      const indicator = document.createElement('div');
      indicator.className = 'typing-indicator';
      indicator.innerHTML = '<span>●</span><span>●</span><span>●</span>';

      wrapper.appendChild(indicator);
      this.container.appendChild(wrapper);
      this.scrollHandle();
    }

    removeTypingIndicator() {
      const indicator = this.container.querySelector('.typing-indicator-wrapper');
      if (indicator) {
        indicator.remove();
      }
    }
  }

  // =============================================================================
  // UI 组件管理
  // =============================================================================

  class UIManager {
    constructor() {
      this.elements = {};
      this.streamRenderer = null;
      this.topicData = {}
    }

    create() {
      if (!this.shouldShowUI()) return false;
      const targetArea = document.querySelector('div.timeline-controls');
      if (!targetArea || document.getElementById('userscript-summary-btn')) return false;

      this.addStyles();
      this.createButtons(targetArea);
      this.createModals();
      this.updateStatus();

      console.log('助手脚本:UI 已创建');
      return true;
    }

    shouldShowUI() {
      return appState.isTopicPage();
    }
    removeCreatedUserName() {
      const discourse_tags = document.querySelector(".discourse-tags");
      if (discourse_tags && discourse_tags.querySelector('.username')) {
        discourse_tags.removeChild(discourse_tags.querySelector('.username'));
      }
    }
    createCreatedUserName() {
      const { summaryBtn } = this.elements;
      const topicData = this.topicData;
      if (topicData.posted) {
        if (summaryBtn && !summaryBtn.querySelector('.posted-icon-span')) {
          const postedIcon = this.createElement('span', {
            className: 'posted-icon-span',
            innerHTML: POSTED_ICON_SVG,
          });
          summaryBtn.append(postedIcon);
        }
      }
      const created_by = topicData.details?.created_by;
      if (created_by) {

        const discourse_tags = document.querySelector(".discourse-tags");
        if (discourse_tags && !discourse_tags.querySelector('.username')) {
          const name = `${created_by.name || created_by.username} · ${created_by.username}`;
          const user_a = this.createElement('a', {
            className: 'username discourse-tag box',
            style: 'background: var(--d-button-primary-bg-color);color: rgb(255, 255, 255);border-radius: 3px;',
            href: '/u/' + created_by.username,
            innerHTML: '<span style="color: #669d34" class="tag-icon"><svg class="fa d-icon d-icon-user svg-icon svg-string" style="fill: #fff;" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"><use href="#user"></use></svg></span>' + name,
            // innerHTML: '@' + (created_by.name || created_by.username),
          });
          discourse_tags.append(user_a);
        }
      }
    }
    createButtons(targetArea) {
      // AI总结按钮
      const summaryIcon = this.createElement('span', {
        className: 'icon-span',
        innerHTML: AI_ICON_SVG,
      });
      // AI总结按钮
      const summaryBtn = this.createElement('button', {
        id: "userscript-summary-btn",
        className: 'summary-btn btn no-text btn-icon icon btn-default reader-mode-toggle',
        title: 'AI 一键收集并总结',
        onclick: () => this.startSummary()
      });
      // 状态显示
      const statusSpan = this.createElement('span', {
        className: 'userscript-counter',
        title: '已收集的帖子数量',
        textContent: '0'
      });
      try {
        setTimeout(() => {
          topicAPI.getTopicData(appState.getTopicId()).then(topicData => {
            this.topicData = topicData;
            this.createCreatedUserName();
          });
        }, 1000);
      } catch (error) {
        console.error('助手脚本:获取帖子数据失败:', error);
      }

      summaryBtn.append(summaryIcon);
      summaryBtn.append(statusSpan);
      targetArea.prepend(
        summaryBtn,
      );

      this.elements = { summaryBtn, summaryIcon, statusSpan };
    }

    createElement(tag, props) {
      const element = document.createElement(tag);
      Object.assign(element, props);
      return element;
    }

    updateStatus() {
      const { summaryBtn, summaryIcon, statusSpan } = this.elements;
      if (!summaryBtn) return;

      const count = appState.posts.length;
      const hasCache = appState.cachedSummary;

      // 更新计数器
      if (statusSpan) {
        statusSpan.textContent = count;
        statusSpan.classList.toggle('visible', count > 0);
      }

      // 更新按钮状态
      switch (appState.status) {
        case 'idle':
          summaryBtn.disabled = false;
          summaryIcon.innerHTML = AI_ICON_SVG;
          summaryBtn.title = hasCache ? 'AI 查看总结 (有缓存)' : 'AI 一键收集并总结';
          break;

        case 'collecting':
          summaryBtn.disabled = true;
          summaryIcon.innerHTML = `<svg class="fa d-icon d-icon-spinner fa-spin svg-icon svg-string"><use href="#spinner"></use></svg>`;
          summaryBtn.title = '正在收集帖子内容...';
          break;

        case 'collected':
          summaryBtn.disabled = true;
          summaryIcon.innerHTML = `<svg class="fa d-icon d-icon-spinner fa-spin svg-icon svg-string"><use href="#spinner"></use></svg>`;
          summaryBtn.title = appState.isStreaming ? '正在生成总结... (点击停止)' : '正在请求 AI 总结...';
          if (appState.isStreaming) {
            summaryBtn.disabled = false;
            summaryBtn.onclick = () => this.stopStreaming();
          }
          break;

        case 'finished':
          summaryBtn.disabled = false;
          summaryIcon.innerHTML = AI_ICON_SVG;
          summaryBtn.title = 'AI 查看总结 / 重新生成';
          summaryBtn.onclick = () => this.startSummary();
          break;
      }
    }

    stopStreaming() {
      appState.stopStreaming();
      this.updateStreamingUI(false);
      appState.status = 'finished';
      this.updateStatus();

      // 更新footer显示已停止
      const modal = document.getElementById('ai-summary-modal-container');
      const footer = modal.querySelector('.ai-summary-modal-footer');
      const statusDiv = footer.querySelector('.streaming-status');
      if (statusDiv) {
        statusDiv.innerHTML = '<span class="status-stopped">● 已停止生成</span>';
      }
    }

    hide() {
      const elements = ['userscript-summary-btn', 'userscript-download-li', 'ai-summary-modal-container'];
      elements.forEach(id => {
        const el = document.getElementById(id);
        if (el) el.style.display = 'none';
      });
      this.topicData = {};
    }

    show() {
      const elements = ['userscript-summary-btn', 'userscript-download-li'];
      elements.forEach(id => {
        const el = document.getElementById(id);
        if (el) el.style.display = '';
      });
      this.createCreatedUserName();
    }

    async startSummary() {
      // 检查缓存
      if (appState.cachedSummary && appState.status === 'idle') {
        this.showSummaryModal('success', appState.cachedSummary, true);
        return;
      }

      // 开始收集
      await this.collectPosts();
    }

    async collectPosts() {
      appState.status = 'collecting';
      appState.clearPosts();
      appState.saveCache('');
      this.updateStatus();

      try {
        const topicId = appState.getTopicId();
        if (!topicId) {
          throw new Error('无法获取帖子ID');
        }

        const { posts, topicData } = await topicAPI.getAllPosts(topicId, (posts, topicData) => {
          // 添加所有帖子到状态
          posts.forEach(post => {
            appState.addPost(post);
          });

          appState.topicData = topicData;
          this.updateStatus();
        });

        // 添加所有帖子到状态
        posts.forEach(post => {
          appState.addPost(post);
        });

        appState.topicData = topicData;
        this.updateStatus();

        if (appState.posts.length > 0) {
          appState.status = 'collected';
          this.updateStatus();
          setTimeout(() => this.requestAISummary(), 1000);
        } else {
          throw new Error('未收集到任何有效内容');
        }

      } catch (error) {
        console.error('助手脚本:收集失败:', error);
        alert(`收集失败: ${error.message}`);
        appState.status = 'idle';
        this.updateStatus();
      }
    }

    async requestAISummary(forceRegenerate = false, clearPosts = false) {
      if (forceRegenerate) {
        appState.saveCache('');
        if (appState.posts.length == 0) {
          clearPosts = true;
        }
        if (clearPosts) {
          appState.clearPosts();
          await this.collectPosts();
          return;
        }
      }
      if (!forceRegenerate && appState.cachedSummary) {
        this.showSummaryModal('success', appState.cachedSummary, true);
        appState.status = 'finished';
        this.updateStatus();
        return;
      }
      if (appState.posts.length == 0) {
        this.showSummaryModal('error', '没有收集到任何帖子内容,请先收集帖子。');
        appState.status = 'idle';
        this.updateStatus();
        return;
      }
      this.showSummaryModal('streaming');
      try {
        const panel = document.getElementById('ai-settings-panel');
        const config = GM_getValue(CONFIG_KEY, DEFAULT_CONFIG);
        panel.querySelector('#enable-streaming').checked = config.enableStreaming !== false;
        const content = this.formatPostsForAI();
        const prompt = config.prompt.replace('{content}', content);

        let summary;
        if (config.apiProvider === 'gemini') {
          summary = await this.callGeminiStream(prompt, config);
        } else {
          summary = await this.callOpenAIStream(prompt, config);
        }

        if (summary && !appState.streamController?.abort) {
          const htmlSummary = this.streamRenderer.container.innerHTML;
          appState.currentSummaryText = summary;
          appState.saveCache(htmlSummary);
        }

        this.updateStreamingUI(false);
        appState.status = 'finished';
        this.updateStatus();

      } catch (error) {
        if (!appState.streamController?.abort) {
          this.showSummaryModal('error', error.message);
        }
        appState.status = 'finished';
        this.updateStatus();
      }
    }

    formatPostsForAI() {
      const title = appState.topicData?.fancy_title || appState.topicData?.title || document.querySelector('#topic-title .fancy-title')?.innerText.trim() || '无标题';
      const posts = appState.randomPosts().map(p => `${p.username}: ${p.content}`).join('\n\n---\n\n');
      return `帖子标题: ${title}\n\n${posts}`;
    }

    async callOpenAIStream(prompt, config) {
      if (!config.openai.apiKey) {
        throw new Error('OpenAI API Key 未设置');
      }

      // 检查配置中是否启用流式输出
      const enableStreaming = GM_getValue(CONFIG_KEY, DEFAULT_CONFIG).enableStreaming !== false;

      if (!enableStreaming) {
        // 使用非流式调用
        const result = await this.callOpenAI(prompt, config);
        this.streamRenderer.setContent(result);
        return result;
      }

      appState.isStreaming = true;
      appState.streamController = { abort: false };

      try {
        const response = await fetch(`${config.openai.baseUrl.replace(/\/$/, '')}/v1/chat/completions`, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${config.openai.apiKey}`
          },
          body: JSON.stringify({
            model: config.openai.model,
            messages: [{ role: 'user', content: prompt }],
            stream: true
          }),
          signal: appState.streamController.signal
        });

        if (!response.ok) {
          let errorMessage = `OpenAI API 请求失败 (${response.status}): ${response.statusText}`;

          try {
            const errorData = await response.json();
            errorMessage = `OpenAI API 请求失败 (${response.status}): ${errorData.error?.message || response.statusText}`;
          } catch (e) {
            // 使用默认错误消息
          }

          throw new Error(errorMessage);
        }

        return await this.processStreamResponse(response);

      } catch (error) {
        appState.isStreaming = false;

        if (error.name === 'AbortError') {
          console.log('流式请求被用户取消');
          throw new Error('请求已取消');
        }

        throw error;
      }
    }

    async processStreamResponse(response) {
      const reader = response.body.getReader();
      const decoder = new TextDecoder();
      let fullContent = '';
      let buffer = '';

      try {
        while (true) {
          // 检查是否需要中止
          if (appState.streamController?.abort) {
            reader.cancel();
            break;
          }

          const { done, value } = await reader.read();

          if (done) {
            console.log('流式响应完成');
            break;
          }

          // 解码数据块
          const chunk = decoder.decode(value, { stream: true });
          buffer += chunk;

          // 处理完整的事件
          const events = buffer.split('\n\n');
          buffer = events.pop() || ''; // 保留最后一个可能不完整的事件

          for (const event of events) {
            if (event.trim()) {
              const processed = this.processOpenAIStreamEvent(event);
              if (processed) {
                fullContent += processed;
                this.streamRenderer.appendContent(processed);
              }
            }
          }
        }

        // 处理剩余的缓冲数据
        if (buffer.trim()) {
          const processed = this.processOpenAIStreamEvent(buffer);
          if (processed) {
            fullContent += processed;
            this.streamRenderer.appendContent(processed);
          }
        }

        return fullContent;

      } finally {
        appState.isStreaming = false;
        reader.releaseLock();
      }
    }

    processOpenAIStreamEvent(event) {
      const lines = event.split('\n');
      let content = '';

      for (const line of lines) {
        const trimmedLine = line.trim();

        if (trimmedLine.startsWith('data: ')) {
          const data = trimmedLine.slice(6).trim();

          if (data === '[DONE]') {
            console.log('收到流式结束标志');
            appState.isStreaming = false;
            continue;
          }

          try {
            const parsed = JSON.parse(data);
            const deltaContent = parsed.choices?.[0]?.delta?.content;

            if (deltaContent) {
              content += deltaContent;
            }

            // 检查是否完成
            const finishReason = parsed.choices?.[0]?.finish_reason;
            if (finishReason) {
              console.log('流式完成,原因:', finishReason);
              appState.isStreaming = false;
            }

          } catch (e) {
            console.warn('解析 SSE 数据时出错:', e, '数据:', data);
          }
        }
      }

      return content;
    }

    async callGeminiStream(prompt, config) {
      if (!config.gemini.apiKey) {
        throw new Error('Gemini API Key 未设置');
      }

      // 检查配置中是否启用流式输出
      const enableStreaming = GM_getValue(CONFIG_KEY, DEFAULT_CONFIG).enableStreaming !== false;

      if (!enableStreaming) {
        // 使用非流式调用
        const result = await this.callGemini(prompt, config);
        this.streamRenderer.setContent(result);
        return result;
      }

      appState.isStreaming = true;
      appState.streamController = { abort: false };

      const url = `${config.gemini.baseUrl.replace(/\/$/, '')}/v1beta/models/${config.gemini.model}:streamGenerateContent?key=${config.gemini.apiKey}`;

      try {
        const response = await fetch(url, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json'
          },
          body: JSON.stringify({
            contents: [{ parts: [{ text: prompt }] }]
          }),
          signal: appState.streamController.signal
        });

        if (!response.ok) {
          let errorMessage = `Gemini API 请求失败 (${response.status}): ${response.statusText}`;

          try {
            const errorData = await response.json();
            errorMessage = `Gemini API 请求失败 (${response.status}): ${errorData.error?.message || response.statusText}`;
          } catch (e) {
            // 使用默认错误消息
          }

          throw new Error(errorMessage);
        }

        return await this.processGeminiStreamResponse(response);

      } catch (error) {
        appState.isStreaming = false;

        if (error.name === 'AbortError') {
          console.log('Gemini 流式请求被用户取消');
          throw new Error('请求已取消');
        }

        throw error;
      }
    }

    async processGeminiStreamResponse(response) {
      const reader = response.body.getReader();
      const decoder = new TextDecoder();
      let fullContent = '';
      let buffer = '';

      try {
        while (true) {
          // 检查是否需要中止
          if (appState.streamController?.abort) {
            reader.cancel();
            break;
          }

          const { done, value } = await reader.read();

          if (done) {
            console.log('Gemini 流式响应完成');
            break;
          }

          // 解码数据块
          const chunk = decoder.decode(value, { stream: true });
          buffer += chunk;

          // 处理缓冲区中的完整 JSON 对象
          let processedData;
          ({ processedData, buffer } = this.extractCompleteJsonObjects(buffer));

          if (processedData) {
            const processed = this.processGeminiStreamData(processedData);
            if (processed) {
              fullContent += processed;
              this.streamRenderer.appendContent(processed);
            }
          }
        }

        // 处理剩余的缓冲数据
        if (buffer.trim()) {
          const processed = this.processGeminiStreamData(buffer);
          if (processed) {
            fullContent += processed;
            this.streamRenderer.appendContent(processed);
          }
        }

        return fullContent;

      } finally {
        appState.isStreaming = false;
        reader.releaseLock();
      }
    }

    extractCompleteJsonObjects(buffer) {
      let processedData = '';
      let remainingBuffer = buffer;

      // Gemini 流式响应通常是换行分隔的 JSON 对象
      const lines = buffer.split('\n');
      remainingBuffer = lines.pop() || ''; // 保留最后一行,可能不完整

      for (const line of lines) {
        const trimmedLine = line.trim();
        if (trimmedLine) {
          processedData += trimmedLine + '\n';
        }
      }

      return { processedData, buffer: remainingBuffer };
    }

    processGeminiStreamData(data) {
      const lines = data.split('\n');
      let content = '';

      for (const line of lines) {
        const trimmedLine = line.trim();

        if (trimmedLine && (trimmedLine.startsWith('{') || trimmedLine.startsWith('['))) {
          try {
            const parsed = JSON.parse(trimmedLine);

            // Gemini 流式响应结构
            const candidates = parsed.candidates;
            if (candidates && candidates.length > 0) {
              const candidate = candidates[0];
              const textContent = candidate.content?.parts?.[0]?.text;

              if (textContent) {
                content += textContent;
              }

              // 检查完成状态
              if (candidate.finishReason) {
                console.log('Gemini 流式完成,原因:', candidate.finishReason);
                appState.isStreaming = false;
              }
            }

            // 处理错误信息
            if (parsed.error) {
              console.error('Gemini 流式响应错误:', parsed.error);
              throw new Error(`Gemini API 错误: ${parsed.error.message}`);
            }

          } catch (e) {
            console.warn('解析 Gemini 流式数据时出错:', e, '数据:', trimmedLine);
          }
        }
      }

      return content;
    }

    // 降级方案:非流式调用
    async callOpenAI(prompt, config) {
      if (!config.openai.apiKey) {
        throw new Error('OpenAI API Key 未设置');
      }

      return new Promise((resolve, reject) => {
        GM.xmlHttpRequest({
          method: 'POST',
          url: `${config.openai.baseUrl.replace(/\/$/, '')}/v1/chat/completions`,
          headers: {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${config.openai.apiKey}`
          },
          data: JSON.stringify({
            model: config.openai.model,
            messages: [{ role: 'user', content: prompt }]
          }),
          responseType: 'json',
          onload: (response) => {
            if (response.status >= 200 && response.status < 300) {
              const content = response.response.choices?.[0]?.message?.content;
              if (content) {
                resolve(content);
              } else {
                reject(new Error('API 返回内容格式不正确'));
              }
            } else {
              reject(new Error(`API 请求失败 (${response.status}): ${response.response?.error?.message || response.statusText}`));
            }
          },
          onerror: () => reject(new Error('网络请求失败'))
        });
      });
    }

    async callGemini(prompt, config) {
      if (!config.gemini.apiKey) {
        throw new Error('Gemini API Key 未设置');
      }

      const url = `${config.gemini.baseUrl.replace(/\/$/, '')}/v1beta/models/${config.gemini.model}:generateContent?key=${config.gemini.apiKey}`;

      return new Promise((resolve, reject) => {
        GM.xmlHttpRequest({
          method: 'POST',
          url: url,
          headers: { 'Content-Type': 'application/json' },
          data: JSON.stringify({
            contents: [{ parts: [{ text: prompt }] }]
          }),
          responseType: 'json',
          onload: (response) => {
            if (response.status === 200) {
              const content = response.response.candidates?.[0]?.content?.parts?.[0]?.text;
              if (content) {
                resolve(content);
              } else {
                reject(new Error('Gemini API 返回内容格式不正确'));
              }
            } else {
              reject(new Error(`Gemini API 请求失败 (${response.status}): ${response.response?.error?.message || response.statusText}`));
            }
          },
          onerror: () => reject(new Error('网络请求失败'))
        });
      });
    }

    downloadPosts() {
      if (appState.posts.length === 0) {
        alert('尚未收集任何帖子!');
        return;
      }

      const title = appState.topicData?.fancy_title || appState.topicData?.title || document.querySelector('#topic-title .fancy-title')?.innerText.trim() || document.title.split(' - ')[0];
      const filename = `${title.replace(/[\\/:*?"<>|]/g, '_')} (共 ${appState.posts.length} 楼).txt`;

      let content = `帖子标题: ${title}\n帖子链接: ${window.location.href}\n收集时间: ${new Date().toLocaleString()}\n总帖子数: ${appState.topicData?.posts_count || appState.posts.length}\n\n`;

      if (appState.currentSummaryText) {
        content += "================ AI 总结 ================\n";
        content += appState.currentSummaryText + "\n\n";
      }

      content += "============== 帖子原文 ================\n\n";
      appState.posts.forEach((post, index) => {
        content += `#${index + 1} 楼 - ${post.username}:\n${post.content}\n\n---\n\n`;
      });

      const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
      GM.download({ url: URL.createObjectURL(blob), name: filename });
    }

    // Modal 相关方法
    createModals() {
      this.createSummaryModal();
      this.createSettingsPanel();
    }

    createSummaryModal() {
      if (document.getElementById('ai-summary-modal-container')) return;

      const modal = document.createElement('div');
      modal.id = 'ai-summary-modal-container';
      modal.className = 'ai-summary-modal-container';
      modal.innerHTML = `
        <div class="ai-summary-modal">
          <div class="ai-summary-modal-header">
            <h1><div class="ai-icon">🤖</div>AI 总结</h1>
            <button class="ai-summary-close-btn">×</button>
          </div>
          <div class="ai-summary-modal-body">
            <div class="generated-summary cooked"></div>
          </div>
          <div class="ai-summary-modal-footer"></div>
        </div>
      `;

      document.body.appendChild(modal);

      // 绑定关闭事件
      modal.querySelector('.ai-summary-close-btn').onclick = () => {
        if (appState.isStreaming) {
          this.stopStreaming();
        }
        modal.classList.remove('visible');
      };
    }

    showSummaryModal(state, content = '', isFromCache = false) {
      const modal = document.getElementById('ai-summary-modal-container');
      const body = modal.querySelector('.generated-summary.cooked');
      const footer = modal.querySelector('.ai-summary-modal-footer');
      modal.style.display = '';
      footer.innerHTML = '';

      switch (state) {
        case 'loading':
          body.innerHTML = `
            <div class="ai-summary-spinner">
              <div class="spinner-circle"></div>
              <div class="spinner-text">AI 正在分析帖子内容...</div>
            </div>
          `;
          break;

        case 'streaming':
          this.streamRenderer = new StreamRenderer(body);
          this.streamRenderer.clear();
          this.streamRenderer.addTypingIndicator();
          this.updateStreamingUI(true);
          break;

        case 'success':
          if (this.streamRenderer) {
            this.streamRenderer.setContent(content);
          } else {
            body.innerHTML = content;
          }
          const cacheInfo = isFromCache ? '<span class="cache-badge">缓存</span>' : '';
          footer.innerHTML = `
            <div class="summary-info">
              由 AI 在 ${new Date().toLocaleDateString()} 生成 ${cacheInfo}
            </div>
            <div class="summary-actions">
              <button class="ai-summary-btn secondary copy-btn">复制</button>
              <button class="ai-summary-btn primary regenerate-btn">重新生成</button>
            </div>
          `;

          // 绑定按钮事件
          footer.querySelector('.copy-btn').onclick = async () => {
            try {
              await navigator.clipboard.writeText(body.textContent);
              const btn = footer.querySelector('.copy-btn');
              const original = btn.textContent;
              btn.textContent = '已复制';
              setTimeout(() => btn.textContent = original, 2000);
            } catch (e) {
              console.error('复制失败:', e);
            }
          };

          footer.querySelector('.regenerate-btn').onclick = () => {
            modal.classList.remove('visible');
            this.requestAISummary(true);
          };
          break;

        case 'error':
          body.innerHTML = `
            <div class="error-content">
              <div class="error-icon">⚠️</div>
              <h3>总结生成失败</h3>
              <p>${content}</p>
            </div>
          `;
          footer.innerHTML = `
            <div class="summary-actions">
              <button class="ai-summary-btn secondary settings-btn">设置</button>
              <button class="ai-summary-btn primary retry-btn">重试</button>
            </div>
          `;

          footer.querySelector('.settings-btn').onclick = () => {
            modal.classList.remove('visible');
            this.showSettingsPanel();
          };

          footer.querySelector('.retry-btn').onclick = () => {
            modal.classList.remove('visible');
            this.requestAISummary(true);
          };
          break;
      }

      modal.classList.add('visible');
    }

    updateStreamingUI(isStreaming) {
      const modal = document.getElementById('ai-summary-modal-container');
      const footer = modal.querySelector('.ai-summary-modal-footer');

      if (isStreaming) {
        footer.innerHTML = `
          <div class="streaming-status">
            <span class="status-streaming">● 正在生成中...</span>
          </div>
          <div class="summary-actions">
            <button class="ai-summary-btn secondary stop-btn">停止生成</button>
            <button class="ai-summary-btn primary copy-btn">复制当前内容</button>
          </div>
        `;

        footer.querySelector('.stop-btn').onclick = () => {
          this.stopStreaming();
        };

        footer.querySelector('.copy-btn').onclick = async () => {
          try {
            const content = this.streamRenderer ? this.streamRenderer.content : '';
            await navigator.clipboard.writeText(content);
            const btn = footer.querySelector('.copy-btn');
            const original = btn.textContent;
            btn.textContent = '已复制';
            setTimeout(() => btn.textContent = original, 2000);
          } catch (e) {
            console.error('复制失败:', e);
          }
        };
      } else {
        if (this.streamRenderer) {
          this.streamRenderer.removeTypingIndicator();
        }

        footer.innerHTML = `
          <div class="summary-info">
            由 AI 在 ${new Date().toLocaleDateString()} 生成
          </div>
          <div class="summary-actions">
            <button class="ai-summary-btn secondary copy-btn">复制</button>
            <button class="ai-summary-btn primary regenerate-btn">重新生成</button>
          </div>
        `;

        footer.querySelector('.copy-btn').onclick = async () => {
          try {
            const content = this.streamRenderer ? this.streamRenderer.content : '';
            await navigator.clipboard.writeText(content);
            const btn = footer.querySelector('.copy-btn');
            const original = btn.textContent;
            btn.textContent = '已复制';
            setTimeout(() => btn.textContent = original, 2000);
          } catch (e) {
            console.error('复制失败:', e);
          }
        };

        footer.querySelector('.regenerate-btn').onclick = () => {
          const modal = document.getElementById('ai-summary-modal-container');
          modal.classList.remove('visible');
          this.requestAISummary(true);
        };
      }
    }

    createSettingsPanel() {
      if (document.getElementById('ai-settings-panel')) return;

      const config = GM_getValue(CONFIG_KEY, DEFAULT_CONFIG);
      const backdrop = document.createElement('div');
      backdrop.id = 'ai-settings-backdrop';

      const panel = document.createElement('div');
      panel.id = 'ai-settings-panel';
      panel.innerHTML = `
        <div class="settings-header">
          <h2>AI 总结器设置</h2>
          <button class="close-btn">×</button>
        </div>
        <div class="settings-content">
          <div class="settings-section">
            <h3>API 设置</h3>
            <label>API 提供商</label>
            <select id="api-provider">
              <option value="openai">OpenAI</option>
              <option value="gemini">Google Gemini</option>
            </select>

            <div id="openai-config">
              <label>OpenAI API Key</label>
              <input type="password" id="openai-key" value="${config.openai.apiKey}">
              <label>Base URL</label>
              <input type="text" id="openai-url" value="${config.openai.baseUrl}">
              <label>模型</label>
              <input type="text" id="openai-model" value="${config.openai.model}">
            </div>

            <div id="gemini-config" style="display: none;">
              <label>Gemini API Key</label>
              <input type="password" id="gemini-key" value="${config.gemini.apiKey}">
              <label>Base URL</label>
              <input type="text" id="gemini-url" value="${config.gemini.baseUrl}">
              <label>模型</label>
              <input type="text" id="gemini-model" value="${config.gemini.model}">
            </div>
          </div>

          <div class="settings-section">
            <label>Prompt 模板</label>
            <textarea id="prompt-template">${config.prompt}</textarea>
          </div>

          <div class="settings-section">
            <h3>流式输出设置</h3>
            <label>
              <input type="checkbox" id="enable-streaming" checked> 启用流式输出 (实时显示生成过程)
            </label>
            <p class="setting-description">流式输出可以实时看到AI生成内容的过程,但可能在某些网络环境下不稳定</p>
          </div>
        </div>
        <div class="settings-footer">
          <button id="cancel-btn">取消</button>
          <button id="save-btn">保存</button>
        </div>
      `;

      document.body.append(backdrop, panel);

      // 绑定事件
      const provider = panel.querySelector('#api-provider');
      const openaiConfig = panel.querySelector('#openai-config');
      const geminiConfig = panel.querySelector('#gemini-config');

      provider.value = config.apiProvider;
      provider.onchange = () => {
        const isGemini = provider.value === 'gemini';
        geminiConfig.style.display = isGemini ? 'block' : 'none';
        openaiConfig.style.display = isGemini ? 'none' : 'block';
      };
      provider.onchange();

      const hide = () => {
        backdrop.classList.remove('visible');
        panel.classList.remove('visible');
      };

      panel.querySelector('.close-btn').onclick = hide;
      backdrop.onclick = hide;
      panel.querySelector('#cancel-btn').onclick = hide;

      panel.querySelector('#save-btn').onclick = () => {
        const newConfig = {
          apiProvider: provider.value,
          openai: {
            apiKey: panel.querySelector('#openai-key').value.trim(),
            baseUrl: panel.querySelector('#openai-url').value.trim(),
            model: panel.querySelector('#openai-model').value.trim()
          },
          gemini: {
            apiKey: panel.querySelector('#gemini-key').value.trim(),
            baseUrl: panel.querySelector('#gemini-url').value.trim(),
            model: panel.querySelector('#gemini-model').value.trim()
          },
          prompt: panel.querySelector('#prompt-template').value.trim(),
          enableStreaming: panel.querySelector('#enable-streaming').checked
        };
        GM_setValue(CONFIG_KEY, newConfig);
        alert('设置已保存!');
        hide();
      };
    }

    showSettingsPanel() {
      const backdrop = document.getElementById('ai-settings-backdrop');
      const panel = document.getElementById('ai-settings-panel');
      backdrop.classList.add('visible');
      panel.classList.add('visible');
    }

    addStyles() {
      GM_addStyle(`
        /* 基础样式 */
        #userscript-status-li {
          display: flex;
          align-items: center;
          margin: 0 -5px 0 2px;
        }
        #userscript-summary-btn{
          position: relative;
          width: 38px;
          height: 38px;
          margin-right: 5px;
          padding: 5px;
          background: var(--d-button-default-bg-color);
        }
        #userscript-summary-bt:active{
          background-image: linear-gradient(to bottom, rgb(var(--primary-rgb), 0.3) 100%, rgb(var(--primary-rgb), 0.3) 100%);
          color: var(--d-button-default-text-color--hover);
        }
        #userscript-summary-btn .posted-icon-span{
          position: absolute;
          top: 2px;
          left: 2px;
        }
        #userscript-summary-btn .posted-icon-span svg{
          height: 0.5em;
          width: 0.5em;
          line-height: 1;
          display: inline-flex;
          vertical-align: text-top;
          color: var(--d-sidebar-suffix-color);
          fill: currentcolor;
          flex-shrink: 0;
        }
        #userscript-summary-btn .icon-span{
          width: 100%;
          height: 100%;
        }
        #userscript-summary-btn .icon-span svg{
          color: var(--d-button-default-icon-color);
        }
        #userscript-summary-btn:active{
          background-image: linear-gradient(to bottom, rgb(var(--primary-rgb), 0.6) 100%, rgb(var(--primary-rgb), 0.6) 100%);
          color: var(--d-button-default-text-color--hover);
        }
        #userscript-summary-btn:active .icon-span svg{
          color: var(--d-button-default-icon-color--hover);
        }
        .userscript-counter {
          font-size: 12px;
          color: #fff;
          background-color: var(--tertiary-med-or-tertiary);
          padding: 0 4px;
          border-radius: 10px;
          line-height: 18px;
          min-width: 18px;
          height: 18px;
          text-align: center;
          display: none;
          position: absolute;
          bottom: -14px;
          border: 2px solid var(--header_background);
        }

        .userscript-counter.visible { display: inline-block; }

        /* 总结窗口样式 */
        .ai-summary-modal-container {
          position: fixed;
          top: 0;
          right: 0;
          width: 100%;
          height: 100%;
          z-index: 1100;
          display: none;
          pointer-events: none;
        }

        .ai-summary-modal-container.visible { display: block; }

        .ai-summary-modal {
          position: fixed;
          right: 10px;
          top: 80px;
          height: calc(100% - 160px);
          width: 500px;
          max-width: 42vw;
          background: var(--secondary);
          border-left: 1px solid var(--primary-low);
          box-shadow: -4px 0 20px rgba(0, 0, 0, 0.15);
          display: flex;
          flex-direction: column;
          transform: translateX(100%);
          transition: transform 0.3s ease-out;
          pointer-events: all;
        }

        .ai-summary-modal-container.visible .ai-summary-modal {
          transform: translateX(0);
        }

        .ai-summary-modal-header {
          padding: 18px 24px;
          border-bottom: 1px solid var(--primary-low);
          display: flex;
          align-items: center;
          justify-content: space-between;
          background: var(--primary-very-low);
        }

        .ai-summary-modal-header h1 {
          margin: 0;
          font-size: 1.4em;
          display: flex;
          align-items: center;
          gap: 12px;
        }

        .ai-summary-modal-header .ai-icon {
          width: 30px;
          height: 30px;
          background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
          border-radius: 8px;
          display: flex;
          align-items: center;
          justify-content: center;
          color: white;
        }

        .ai-summary-close-btn {
          background: none;
          border: none;
          cursor: pointer;
          padding: 8px;
          border-radius: 8px;
          font-size: 20px;
          color: var(--primary-medium);
        }

        .ai-summary-close-btn:hover {
          background: var(--primary-low);
        }

        .ai-summary-modal-body {
          flex: 1;
          overflow-y: auto;
          padding: 0;
        }

        .generated-summary.cooked {
          padding: 24px 28px;
          line-height: 1.7;
          font-size: 15px;
          min-height: 100%;
          box-sizing: border-box;
        }
        .typing-indicator-wrapper{
          width: 100%;
          text-align: center;
        }
        .generated-summary.cooked h1{
          border-bottom: 2px solid #0088cc;
          padding-bottom: 10px;
        }

        .generated-summary.cooked h1, .generated-summary.cooked h2, .generated-summary.cooked h3 {
          color: var(--primary);
          margin: 20px 0 14px 0;
        }

        .generated-summary.cooked p {
          margin: 12px 0;
          color: var(--primary-high);
        }

        .generated-summary.cooked ul, .generated-summary.cooked ol {
          margin: 16px 0;
          padding-left: 24px;
        }

        .generated-summary.cooked li {
          margin: 6px 0;
        }

        .generated-summary.cooked strong {
          color: var(--primary);
          font-weight: 600;
        }

        .generated-summary.cooked code {
          background: var(--primary-very-low);
          padding: 3px 6px;
          border-radius: 4px;
          font-family: monospace;
        }

        .generated-summary.cooked pre {
          background: var(--primary-very-low);
          padding: 16px;
          border-radius: 8px;
          overflow-x: auto;
          margin: 16px 0;
        }

        .generated-summary.cooked blockquote {
          border-left: 4px solid var(--tertiary);
          padding-left: 16px;
          margin: 16px 0;
          color: var(--primary-medium);
          font-style: italic;
        }

        /* 流式输出相关样式 */
        .typing-indicator {
          display: inline-flex;
          align-items: center;
          gap: 4px;
          margin-left: 8px;
          animation: pulse 1.5s ease-in-out infinite;
        }

        .typing-indicator span {
          display: inline-block;
          width: 8px;
          height: 8px;
          border-radius: 50%;
          color: var(--tertiary);
          animation: typing 1.4s ease-in-out infinite;
        }

        .typing-indicator span:nth-child(2) {
          animation-delay: 0.2s;
          margin-left: 4px;
        }

        .typing-indicator span:nth-child(3) {
          animation-delay: 0.4s;
          margin-left: 4px;
        }

        @keyframes typing {
          0%, 60%, 100% {
            transform: translateY(0);
            opacity: 0.4;
          }
          30% {
            transform: translateY(-10px);
            opacity: 1;
          }
        }

        @keyframes pulse {
          0%, 100% { opacity: 0.6; }
          50% { opacity: 1; }
        }

        .streaming-status {
          display: flex;
          align-items: center;
          gap: 8px;
          color: var(--primary-medium);
          font-size: 13px;
        }

        .status-streaming {
          color: var(--success);
        }

        .status-stopped {
          color: var(--primary-medium);
        }

        .ai-summary-spinner {
          display: flex;
          flex-direction: column;
          align-items: center;
          justify-content: center;
          padding: 60px 24px;
          gap: 20px;
        }

        .spinner-circle {
          width: 36px;
          height: 36px;
          border: 3px solid var(--primary-low);
          border-radius: 50%;
          border-top-color: var(--tertiary);
          animation: spin 1s linear infinite;
        }

        @keyframes spin { to { transform: rotate(360deg); } }

        .ai-summary-modal-footer {
          padding: 18px 24px;
          border-top: 1px solid var(--primary-low);
          background: var(--primary-very-low);
          display: flex;
          justify-content: space-between;
          align-items: center;
        }

        .summary-info {
          color: var(--primary-medium);
          font-size: 13px;
        }

        .cache-badge {
          background: var(--success);
          color: white;
          padding: 2px 8px;
          border-radius: 12px;
          font-size: 11px;
          margin-left: 8px;
        }

        .summary-actions {
          display: flex;
          gap: 10px;
        }

        .ai-summary-btn {
          padding: 10px 18px;
          border: none;
          border-radius: 6px;
          font-size: 13px;
          cursor: pointer;
          transition: all 0.2s ease;
        }

        .ai-summary-btn.primary {
          background: var(--tertiary);
          color: white;
        }

        .ai-summary-btn.secondary {
          background: var(--primary-low);
          color: var(--primary);
        }

        .ai-summary-btn:hover {
          transform: translateY(-1px);
          box-shadow: 0 2px 8px rgba(0,0,0,0.15);
        }

        .error-content {
          display: flex;
          flex-direction: column;
          align-items: center;
          padding: 40px 24px;
          text-align: center;
          gap: 16px;
        }

        .error-icon {
          font-size: 48px;
        }

        /* 设置面板样式 */
        #ai-settings-backdrop {
          position: fixed;
          inset: 0;
          background-color: rgba(0,0,0,0.6);
          z-index: 1101;
          display: none;
        }

        #ai-settings-backdrop.visible { display: block; }

        #ai-settings-panel {
          position: fixed;
          left: 0;
          top: 0;
          height: 100%;
          width: 90%;
          max-width: 400px;
          background-color: var(--secondary);
          z-index: 1102;
          transform: translateX(-100%);
          transition: transform 0.3s ease-in-out;
          display: flex;
          flex-direction: column;
        }

        #ai-settings-panel.visible {
          transform: translateX(0);
        }

        .settings-header {
          display: flex;
          justify-content: space-between;
          align-items: center;
          padding: 15px 20px;
          border-bottom: 1px solid var(--primary-low);
        }

        .settings-header h2 {
          margin: 0;
          font-size: 1.2em;
        }

        .close-btn {
          background: none;
          border: none;
          cursor: pointer;
          font-size: 20px;
          padding: 5px;
        }

        .settings-content {
          padding: 20px;
          overflow-y: auto;
          flex: 1;
        }

        .settings-section {
          margin-bottom: 20px;
          padding: 15px;
          border: 1px solid var(--primary-low);
          border-radius: 5px;
        }

        .settings-section h3 {
          margin: 0 0 15px 0;
          font-size: 1.1em;
        }

        .settings-section label {
          display: block;
          margin: 15px 0 5px 0;
          font-weight: bold;
        }

        .settings-section input,
        .settings-section textarea,
        .settings-section select {
          width: 100%;
          padding: 8px;
          border-radius: 4px;
          border: 1px solid var(--primary-low);
          background-color: var(--primary-very-low);
          color: var(--primary-high);
          box-sizing: border-box;
        }

        .settings-section textarea {
          min-height: 500px;
          resize: vertical;
        }

        .setting-description {
          font-size: 12px;
          color: var(--primary-medium);
          margin-top: 5px;
          line-height: 1.4;
        }

        .settings-footer {
          padding: 15px 20px;
          border-top: 1px solid var(--primary-low);
          text-align: right;
        }

        .settings-footer button {
          padding: 8px 15px;
          border: none;
          border-radius: 5px;
          cursor: pointer;
          margin-left: 10px;
        }

        #save-btn {
          background-color: var(--tertiary);
          color: #fff;
        }

        #cancel-btn {
          background-color: var(--primary-low);
          color: var(--primary-high);
        }
      `);
    }
  }

  // =============================================================================
  // 始皇曰 解密 组件管理
  // =============================================================================
  class NeoDecodeManager {
    constructor() {
      // Base64到古汉字的映射表
      let base64ToAncient = {
        'A': '天', 'B': '地', 'C': '玄', 'D': '黄', 'E': '宇', 'F': '宙', 'G': '洪', 'H': '荒',
        'I': '日', 'J': '月', 'K': '盈', 'L': '昃', 'M': '辰', 'N': '宿', 'O': '列', 'P': '张',
        'Q': '寒', 'R': '来', 'S': '暑', 'T': '往', 'U': '秋', 'V': '收', 'W': '冬', 'X': '藏',
        'Y': '闰', 'Z': '余', 'a': '成', 'b': '岁', 'c': '律', 'd': '吕', 'e': '调', 'f': '阳',
        'g': '云', 'h': '腾', 'i': '致', 'j': '雨', 'k': '露', 'l': '结', 'm': '为', 'n': '霜',
        'o': '金', 'p': '生', 'q': '丽', 'r': '水', 's': '玉', 't': '出', 'u': '昆', 'v': '冈',
        'w': '剑', 'x': '号', 'y': '巨', 'z': '阙', '0': '珠', '1': '称', '2': '夜', '3': '光',
        '4': '果', '5': '珍', '6': '李', '7': '柰', '8': '菜', '9': '重', '+': '芥', '/': '姜',
        '=': '海'
      };

      // 古汉字到Base64的反向映射
      this.ancientToBase64 = Object.fromEntries(
        Object.entries(base64ToAncient).map(([k, v]) => [v, k])
      );

      // 注册菜单命令
      GM_registerMenuCommand('始皇曰解密', () => {
        const selectedText = window.getSelection().toString().trim();
        if (selectedText) {
          const decrypted = this.decrypt(selectedText);
          if (decrypted) {
            this.copy_text(decrypted).then(() => {
              alert('解密结果已复制到剪贴板!');
            });
          } else {
            alert('解密失败,请确认选中的是有效的始皇曰格式');
          }
        } else {
          alert('请先选中要解密的文本');
        }
      });
    }

    copy_text(text) {
      if (!text) {
        return Promise.reject('没有要复制的内容');
      }

      // 优先使用现代剪贴板API
      if (navigator.clipboard && navigator.clipboard.writeText) {
        return navigator.clipboard.writeText(text).then(() => {
          console.log('使用现代API复制成功');
          return true;
        }).catch(error => {
          console.warn('现代API复制失败,尝试降级方案:', error);
          return this.fallbackCopyText(text);
        });
      } else {
        // 降级到传统方案
        return this.fallbackCopyText(text);
      }
    }

    fallbackCopyText(text) {
      return new Promise((resolve, reject) => {
        try {
          // 创建临时文本区域
          const textArea = document.createElement('textarea');
          textArea.value = text;

          // 设置样式使其不可见
          textArea.style.position = 'fixed';
          textArea.style.left = '-999999px';
          textArea.style.top = '-999999px';
          textArea.style.opacity = '0';
          textArea.style.pointerEvents = 'none';
          textArea.style.tabIndex = '-1';

          document.body.appendChild(textArea);

          // 选择文本
          textArea.focus();
          textArea.select();
          textArea.setSelectionRange(0, text.length);

          // 执行复制命令
          const successful = document.execCommand('copy');

          // 清理
          document.body.removeChild(textArea);

          if (successful) {
            console.log('使用降级方案复制成功');
            resolve(true);
          } else {
            reject('降级复制方案失败');
          }
        } catch (error) {
          reject('复制操作失败: ' + error.message);
        }
      });
    }

    decrypt(input) {
      if (!input) {
        return '';
      }
      try {
        let text = input.trim();
        if (input.startsWith('始皇曰:')) {
          return this.decrypt_neo(text);
        }
        text = this.decrypt_base64(text);
        if (!text) {
          return '';
        }
        return this.decrypt_neo(text);
      } catch (error) {
      }
      return '';
    }

    decrypt_neo(input) {
      if (!input) {
        return '';
      }
      try {
        // 提取&quot;始皇曰:&quot;之后的内容
        let ancientText = input;
        if (input.startsWith('始皇曰:')) {
          ancientText = input.substring(4);
        }

        // 映射回Base64
        let base64 = '';
        for (let char of ancientText) {
          base64 += this.ancientToBase64[char] || char;
        }

        // 从Base64解码
        return decodeURIComponent(escape(atob(base64)));
      } catch (error) {
      }
      return '';
    }

    decrypt_base64(input) {
      if (!input) {
        return '';
      }

      try {
        // 从Base64解码
        return decodeURIComponent(escape(atob(input)));
      } catch (error) {
      }
      return '';
    }

    decode_els() {
      // 正则匹配.post-stream的字符串:“始皇曰:”,然后获取对应整行内容(包含“始皇曰:”)
      const postStreamElement = document.querySelector('.post-stream');
      if (!postStreamElement) {
        return;
      }

      const matchAll = postStreamElement.innerHTML.match(/始皇曰:([^<]+)/);
      if (!matchAll) {
        return;
      }

      const posts = document.querySelectorAll('.post-stream .topic-post');
      posts.forEach(post => {
        // 检查是否已经解码过,避免重复处理
        if (post.classList.contains('decoded')) {
          return;
        }
        post.classList.add('decoded');

        const match = post.innerHTML.match(/始皇曰:([^<]+)/);
        if (match) {
          const content = match[0];
          // 找到包含这句话的标签,比如 <code>始皇曰:xxx</code>,那么应该找到code元素,当然不一定是code标签
          let codeElement = post.querySelector('*');
          const walker = document.createTreeWalker(
            codeElement,
            NodeFilter.SHOW_TEXT,
            null,
            false
          );

          let node;
          while (node = walker.nextNode()) {
            if (node.textContent.includes(content)) {
              codeElement = node.parentElement;
              break;
            }
          }
          if (codeElement) {
            const decrypt_content = this.decrypt_neo(content);
            if (decrypt_content) {

              // 存储原始内容
              if (!codeElement.getAttribute('data-original-html')) {
                codeElement.setAttribute('data-original-html', codeElement.innerHTML);
              }

              // 使用原始内容拼接解密内容
              const originalHtml = codeElement.getAttribute('data-original-html');
              codeElement.innerHTML = originalHtml + `<br/>解密:${decrypt_content}`;
            }
          }
        }
      });
    }
  }

  // =============================================================================
  // 应用管理器
  // =============================================================================

  class AppManager {
    constructor() {
      this.ui = new UIManager();
      this.neoDecode = new NeoDecodeManager();
      this.lastUrl = '';
      this.lastTopicId = '';
    }

    init() {
      // 检查页面变化
      const currentUrl = window.location.href;
      const currentTopicId = appState.getTopicId();

      if (currentUrl !== this.lastUrl) {
        this.handleUrlChange(currentTopicId);
        this.lastUrl = currentUrl;
        this.lastTopicId = currentTopicId;
      }
      // 根据页面类型显示/隐藏UI
      if (appState.isTopicPage()) {
        if (!document.getElementById('userscript-summary-btn')) {
          this.ui.create();
        } else {
          this.ui.show();
        }
        this.neoDecode.decode_els();
      } else {
        this.ui.hide();
      }
    }

    handleUrlChange(newTopicId) {
      // console.log('助手脚本:页面变化检测');

      // 如果切换到不同的帖子,重置状态
      if (newTopicId && newTopicId !== this.lastTopicId) {
        console.log('助手脚本:切换帖子,重置状态');
        this.resetState();
      }

      // 如果离开帖子页面,清理状态
      if (!appState.isTopicPage()) {
        this.cleanup();
      }
    }

    resetState() {
      // 重置状态
      appState.reset();
      appState.loadCache();

      // 更新UI
      if (this.ui.elements.summaryBtn) {
        this.ui.updateStatus();
      }
    }

    cleanup() {
      appState.reset();
    }

    setupObservers() {
      // 监听DOM变化
      const observer = new MutationObserver(() => {
        this.init();
      });

      observer.observe(document.documentElement, {
        childList: true,
        subtree: true,
        attributes: true,
        attributeFilter: ['class', 'id']
      });

      // 监听路由变化
      let lastUrl = location.href;
      new MutationObserver(() => {
        const url = location.href;
        if (url !== lastUrl) {
          lastUrl = url;
          setTimeout(() => this.init(), 200);
        }
      }).observe(document, { subtree: true, childList: true });

      // 监听浏览器历史变化
      window.addEventListener('popstate', () => {
        setTimeout(() => this.init(), 200);
      });

      // 重写 history API
      const originalPushState = history.pushState;
      const originalReplaceState = history.replaceState;

      history.pushState = function (...args) {
        originalPushState.apply(history, args);
        setTimeout(() => app.init(), 200);
      };

      history.replaceState = function (...args) {
        originalReplaceState.apply(history, args);
        setTimeout(() => app.init(), 200);
      };
    }
  }

  // =============================================================================
  // 初始化
  // =============================================================================

  const app = new AppManager();

  // 注册菜单命令
  GM_registerMenuCommand('设置 AI 总结 API', () => {
    app.ui.showSettingsPanel();
  });


  // 启动应用
  function startup() {
    console.log('助手脚本:启动中...');

    // 初始加载缓存
    appState.loadCache();

    // 设置观察器
    app.setupObservers();

    // 初始检查
    setTimeout(() => {
      app.init();
    }, 1000);
    console.log('助手脚本:启动完成');
  }

  // DOM加载完成后启动
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', startup);
  } else {
    startup();
  }

})();