Greasy Fork

Greasy Fork is available in English.

网页文章总结助手

自动总结网页文章内容,支持多种格式输出,适用于各类文章网站

当前为 2025-03-14 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         网页文章总结助手
// @namespace    http://tampermonkey.net/
// @version      0.2.2
// @description  自动总结网页文章内容,支持多种格式输出,适用于各类文章网站
// @author       h7ml <[email protected]>
// @match        *://*/*
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @grant        GM_getResourceText
// @connect      api.gptgod.online
// @connect      api.deepseek.com
// @connect      localhost
// @connect      *
// @resource     marked https://cdn.bootcdn.net/ajax/libs/marked/4.3.0/marked.min.js
// @resource     highlight https://cdn.bootcdn.net/ajax/libs/highlight.js/11.7.0/highlight.min.js
// @resource     highlightStyle https://cdn.bootcdn.net/ajax/libs/highlight.js/11.7.0/styles/github.min.css
// ==/UserScript==

(function () {
  'use strict';

  // 配置管理类
  class ConfigManager {
    constructor() {
      this.DEFAULT_API_SERVICE = 'ollama';
      this.DEFAULT_CONFIGS = {
        ollama: {
          url: 'http://localhost:11434/api/chat',
          model: 'llama2',
          key: ''  // Ollama 不需要 API key
        },
        gptgod: {
          url: 'https://api.gptgod.online/v1/chat/completions',
          model: 'gpt-4o-all',
          key: 'sk-L1rbJXBp3aDrZLgyrUq8FugKU54FxElTbzt7RfnBaWgHOtFj'
        },
        deepseek: {
          url: 'https://api.deepseek.com/v1/chat/completions',
          model: 'deepseek-chat',
          key: ''
        },
        custom: {
          url: '',
          model: '',
          key: ''
        }
      };
      this.DEFAULT_FORMAT = 'markdown';
      this.DEFAULT_APP_SIZE = {
        width: 400,
        height: 500
      };
      this.OLLAMA_MODELS = [
        'llama2',
        'llama2:13b',
        'llama2:70b',
        'mistral',
        'mixtral',
        'gemma:2b',
        'gemma:7b',
        'qwen:14b',
        'qwen:72b',
        'phi3:mini',
        'phi3:small',
        'phi3:medium',
        'yi:34b',
        'vicuna:13b',
        'vicuna:33b',
        'codellama',
        'wizardcoder',
        'nous-hermes2',
        'neural-chat',
        'openchat',
        'dolphin-mixtral',
        'starling-lm'
      ];

      // 添加API转发配置,默认关闭
      this.DEFAULT_API_PROXY = false;
      this.DEFAULT_API_PROXY_DOMAIN = 'https://nakoruru.h7ml.cn';
    }

    getConfigs() {
      return GM_getValue('apiConfigs', this.DEFAULT_CONFIGS);
    }

    getApiService() {
      return GM_getValue('apiService', this.DEFAULT_API_SERVICE);
    }

    getOutputFormat() {
      return GM_getValue('outputFormat', this.DEFAULT_FORMAT);
    }

    getConfigCollapsed() {
      return GM_getValue('configCollapsed', false);
    }

    getAppMinimized() {
      return GM_getValue('appMinimized', false);
    }

    getAppPosition() {
      return GM_getValue('appPosition', null);
    }

    getIconPosition() {
      return GM_getValue('iconPosition', null);
    }

    getAppSize() {
      return GM_getValue('appSize', this.DEFAULT_APP_SIZE);
    }

    setConfigs(configs) {
      GM_setValue('apiConfigs', configs);
    }

    setApiService(service) {
      GM_setValue('apiService', service);
    }

    setOutputFormat(format) {
      GM_setValue('outputFormat', format);
    }

    setConfigCollapsed(collapsed) {
      GM_setValue('configCollapsed', collapsed);
    }

    setAppMinimized(minimized) {
      GM_setValue('appMinimized', minimized);
    }

    setAppPosition(position) {
      GM_setValue('appPosition', position);
    }

    setIconPosition(position) {
      GM_setValue('iconPosition', position);
    }

    setAppSize(size) {
      GM_setValue('appSize', size);
    }

    // 添加获取和设置API转发配置的方法
    getApiProxyEnabled() {
      return GM_getValue('apiProxyEnabled', this.DEFAULT_API_PROXY);
    }

    getApiProxyDomain() {
      return GM_getValue('apiProxyDomain', this.DEFAULT_API_PROXY_DOMAIN);
    }

    setApiProxyEnabled(enabled) {
      GM_setValue('apiProxyEnabled', enabled);
    }

    setApiProxyDomain(domain) {
      GM_setValue('apiProxyDomain', domain);
    }
  }

  // UI管理类
  class UIManager {
    constructor(configManager) {
      this.configManager = configManager;
      this.app = null;
      this.iconElement = null;
      this.elements = {};
      this.isDragging = false;
      this.isIconDragging = false;
      this.isMaximized = false;
      this.previousSize = {};
      this.apiService = null; // 将在 init 中初始化
    }

    async init() {
      this.apiService = new APIService(this.configManager);
      await this.loadLibraries();
      this.createApp();
      this.createIcon();
      this.bindEvents();
      this.restoreState();

      // 如果当前服务是 Ollama,尝试获取模型列表
      if (this.configManager.getApiService() === 'ollama') {
        this.fetchOllamaModels();
      }
    }

    async loadLibraries() {
      // 添加基础样式
      GM_addStyle(`
        /* 基础样式 */
        #article-summary-app {
          position: fixed;
          top: 20px;
          right: 20px;
          width: 400px;
          max-height: 80vh;
          min-width: 320px;
          min-height: 300px;
          background: rgba(255, 255, 255, 0.98);
          border-radius: 12px;
          box-shadow: 0 4px 24px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.08);
          z-index: 999999;
          display: flex;
          flex-direction: column;
          resize: both;
          overflow: hidden;
          font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Helvetica Neue', sans-serif;
          backdrop-filter: blur(10px);
          -webkit-backdrop-filter: blur(10px);
          border: 1px solid rgba(0, 0, 0, 0.06);
          transition: all 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
        }

        #article-summary-icon {
          position: fixed;
          bottom: 20px;
          right: 20px;
          width: 48px;
          height: 48px;
          background: #007AFF;
          border-radius: 50%;
          display: none; /* 默认隐藏图标 */
          align-items: center;
          justify-content: center;
          cursor: pointer;
          box-shadow: 0 4px 12px rgba(0, 122, 255, 0.3);
          z-index: 999999;
          color: white;
          transition: transform 0.2s ease, box-shadow 0.2s ease;
        }
        
        #article-summary-icon:hover {
          transform: scale(1.05);
          box-shadow: 0 6px 16px rgba(0, 122, 255, 0.4);
        }
        
        #article-summary-icon:active {
          transform: scale(0.98);
        }

        #summary-header {
          padding: 16px;
          border-bottom: 1px solid rgba(0, 0, 0, 0.06);
          display: flex;
          justify-content: space-between;
          align-items: center;
          cursor: move;
          -webkit-app-region: drag;
          user-select: none;
        }

        #summary-header h3 {
          margin: 0;
          font-size: 16px;
          font-weight: 500;
          color: #1D1D1F;
        }

        #summary-header-actions {
          display: flex;
          gap: 12px;
          -webkit-app-region: no-drag;
        }

        .header-btn {
          background: none;
          border: none;
          padding: 6px;
          cursor: pointer;
          color: #8E8E93;
          border-radius: 6px;
          transition: all 0.2s ease;
          display: flex;
          align-items: center;
          justify-content: center;
        }

        .header-btn:hover {
          background: rgba(0, 0, 0, 0.05);
          color: #1D1D1F;
        }
        
        .header-btn:active {
          transform: scale(0.95);
        }

        #summary-body {
          padding: 20px;
          overflow-y: auto;
          flex: 1;
        }

        .form-group {
          margin-bottom: 20px;
        }
        
        .form-label {
          display: block;
          margin-bottom: 6px;
          color: #6E6E73;
          font-size: 13px;
          font-weight: 500;
        }
        
        .form-input, .form-select {
          width: 100%;
          padding: 10px 12px;
          border: 1px solid rgba(0, 0, 0, 0.1);
          border-radius: 8px;
          font-size: 14px;
          background-color: rgba(0, 0, 0, 0.02);
          color: #1D1D1F;
          transition: all 0.2s ease;
          -webkit-appearance: none;
          appearance: none;
        }
        
        .form-input:focus, .form-select:focus {
          outline: none;
          border-color: #007AFF;
          box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.15);
          background-color: #fff;
        }
        
        .form-select {
          background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%238E8E93' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
          background-repeat: no-repeat;
          background-position: right 12px center;
          background-size: 16px;
          padding-right: 36px;
        }

        #configPanel {
          margin-top: 12px;
          padding: 16px;
          background: rgba(0, 0, 0, 0.02);
          border-radius: 10px;
          border: 1px solid rgba(0, 0, 0, 0.05);
          transition: all 0.3s ease;
        }

        #configPanel.collapsed {
          display: none;
        }

        #formatOptions {
          display: flex;
          gap: 8px;
        }
        
        .format-btn {
          padding: 8px 12px;
          border: 1px solid rgba(0, 0, 0, 0.1);
          border-radius: 8px;
          cursor: pointer;
          font-size: 13px;
          transition: all 0.2s ease;
          background: rgba(255, 255, 255, 0.8);
          color: #1D1D1F;
        }
        
        .format-btn:hover {
          background: rgba(0, 0, 0, 0.05);
        }
        
        .format-btn.active {
          background: #007AFF;
          color: white;
          border-color: #007AFF;
          font-weight: 500;
        }
        
        #generateBtn {
          width: 100%;
          padding: 14px;
          background: #007AFF;
          color: white;
          border: none;
          border-radius: 10px;
          cursor: pointer;
          font-size: 15px;
          font-weight: 500;
          display: flex;
          align-items: center;
          justify-content: center;
          gap: 8px;
          transition: all 0.2s ease;
          box-shadow: 0 2px 8px rgba(0, 122, 255, 0.3);
        }
        
        #generateBtn:hover {
          background: #0071E3;
          transform: translateY(-1px);
          box-shadow: 0 4px 12px rgba(0, 122, 255, 0.4);
        }
        
        #generateBtn:active {
          transform: translateY(1px);
          box-shadow: 0 1px 4px rgba(0, 122, 255, 0.3);
        }
        
        #generateBtn:disabled {
          background: #A2A2A7;
          cursor: not-allowed;
          box-shadow: none;
          transform: none;
        }
        
        #summaryResult {
          margin-top: 20px;
          display: none;
          flex-direction: column;
          height: 100%;
          animation: fadeIn 0.3s ease;
        }
        
        @keyframes fadeIn {
          from { opacity: 0; transform: translateY(10px); }
          to { opacity: 1; transform: translateY(0); }
        }
        
        #summaryHeader {
          display: flex;
          justify-content: space-between;
          align-items: center;
          margin-bottom: 12px;
        }
        
        #summaryHeader h4 {
          margin: 0;
          color: #1D1D1F;
          font-size: 15px;
          font-weight: 500;
        }
        
        .action-btn {
          background: none;
          border: none;
          padding: 6px 10px;
          cursor: pointer;
          color: #007AFF;
          display: flex;
          align-items: center;
          gap: 6px;
          border-radius: 6px;
          transition: all 0.2s ease;
          font-size: 13px;
          font-weight: 500;
        }
        
        .action-btn:hover {
          background: rgba(0, 122, 255, 0.1);
        }
        
        .action-btn:active {
          transform: scale(0.95);
        }

        #loadingIndicator {
          display: none;
          text-align: center;
          padding: 30px;
          animation: fadeIn 0.3s ease;
        }

        .spinner {
          width: 36px;
          height: 36px;
          margin: 0 auto 16px;
          border: 3px solid rgba(0, 0, 0, 0.05);
          border-top: 3px solid #007AFF;
          border-radius: 50%;
          animation: spin 1s linear infinite;
        }

        @keyframes spin {
          0% { transform: rotate(0deg); }
          100% { transform: rotate(360deg); }
        }

        .app-minimized {
          display: none;
        }

        .icon {
          width: 18px;
          height: 18px;
        }

        .toggle-icon {
          transition: transform 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
        }

        .markdown-body {
          font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Helvetica Neue', sans-serif;
          line-height: 1.6;
          color: #1D1D1F;
          flex: 1;
          display: flex;
          flex-direction: column;
        }

        .markdown-body h1 { font-size: 1.5rem; margin: 1.2rem 0; font-weight: 600; }
        .markdown-body h2 { font-size: 1.25rem; margin: 1.1rem 0; font-weight: 600; }
        .markdown-body h3 { font-size: 1.1rem; margin: 1rem 0; font-weight: 600; }
        .markdown-body p { margin: 0.8rem 0; }
        .markdown-body code {
          background: rgba(0, 0, 0, 0.04);
          padding: 0.2em 0.4em;
          border-radius: 4px;
          font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace;
          font-size: 0.9em;
        }
        .markdown-body pre {
          background: rgba(0, 0, 0, 0.04);
          padding: 1rem;
          border-radius: 8px;
          overflow-x: auto;
        }

        #modelSelect {
          width: 100%;
          padding: 10px 12px;
          border: 1px solid rgba(0, 0, 0, 0.1);
          border-radius: 8px;
          font-size: 14px;
          background-color: rgba(0, 0, 0, 0.02);
          background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%238E8E93' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
          background-repeat: no-repeat;
          background-position: right 12px center;
          background-size: 16px;
          padding-right: 36px;
          -webkit-appearance: none;
          appearance: none;
          transition: all 0.2s ease;
        }
        
        #modelSelect:focus {
          outline: none;
          border-color: #007AFF;
          box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.15);
          background-color: #fff;
        }
        
        #modelName {
          display: none;
        }
        
        .ollama-service #modelSelect {
          display: block;
        }
        
        .ollama-service #modelName {
          display: none;
        }
        
        .non-ollama-service #modelSelect {
          display: none;
        }
        
        .non-ollama-service #modelName {
          display: block;
        }

        .content-textarea {
          width: 100%;
          height: 100%;
          min-height: 200px;
          padding: 14px;
          border: 1px solid rgba(0, 0, 0, 0.1);
          border-radius: 10px;
          font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Helvetica Neue', sans-serif;
          font-size: 14px;
          line-height: 1.6;
          resize: vertical;
          flex: 1;
          box-sizing: border-box;
          background-color: rgba(255, 255, 255, 0.8);
          color: #1D1D1F;
          transition: all 0.2s ease;
        }

        .content-textarea:focus {
          outline: none;
          border-color: #007AFF;
          box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.15);
          background-color: #fff;
        }

        .resize-handle {
          position: absolute;
          bottom: 0;
          right: 0;
          width: 16px;
          height: 16px;
          cursor: nwse-resize;
          background: linear-gradient(135deg, transparent 50%, rgba(0, 0, 0, 0.1) 50%, rgba(0, 0, 0, 0.1) 100%);
          border-radius: 0 0 10px 0;
          transition: opacity 0.2s ease;
          opacity: 0.5;
        }
        
        .resize-handle:hover {
          opacity: 1;
        }

        .proxy-settings {
          margin-top: 12px;
        }
        
        .checkbox-container {
          display: flex;
          align-items: center;
          margin-bottom: 10px;
        }
        
        .form-checkbox {
          margin-right: 10px;
          -webkit-appearance: none;
          appearance: none;
          width: 18px;
          height: 18px;
          border: 1px solid rgba(0, 0, 0, 0.2);
          border-radius: 4px;
          background-color: white;
          cursor: pointer;
          position: relative;
          transition: all 0.2s ease;
        }
        
        .form-checkbox:checked {
          background-color: #007AFF;
          border-color: #007AFF;
        }
        
        .form-checkbox:checked::after {
          content: '';
          position: absolute;
          left: 6px;
          top: 2px;
          width: 4px;
          height: 9px;
          border: solid white;
          border-width: 0 2px 2px 0;
          transform: rotate(45deg);
        }
        
        .form-checkbox:focus {
          outline: none;
          box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.15);
        }
        
        #configToggle {
          display: flex;
          align-items: center;
          justify-content: space-between;
          padding: 10px 12px;
          background: rgba(0, 0, 0, 0.03);
          border-radius: 8px;
          cursor: pointer;
          margin-bottom: 12px;
          transition: all 0.2s ease;
          user-select: none;
        }
        
        #configToggle:hover {
          background: rgba(0, 0, 0, 0.05);
        }
        
        #configToggle span {
          font-weight: 500;
          font-size: 14px;
          color: #1D1D1F;
        }
      `);

      console.log('Markdown 渲染库加载完成');
    }

    createApp() {
      this.app = document.createElement('div');
      this.app.id = 'article-summary-app';
      this.app.innerHTML = this.getAppHTML();
      document.body.appendChild(this.app);
      this.initializeElements();
    }

    createIcon() {
      this.iconElement = document.createElement('div');
      this.iconElement.id = 'article-summary-icon';
      this.iconElement.innerHTML = this.getIconHTML();
      document.body.appendChild(this.iconElement);

      // 不需要在这里设置display,因为CSS已经默认设置为none
    }

    initializeElements() {
      this.elements = {
        apiService: document.getElementById('apiService'),
        apiUrl: document.getElementById('apiUrl'),
        apiUrlContainer: document.getElementById('apiUrlContainer'),
        apiKey: document.getElementById('apiKey'),
        apiKeyContainer: document.getElementById('apiKeyContainer'),
        modelName: document.getElementById('modelName'),
        modelSelect: document.getElementById('modelSelect'),
        generateBtn: document.getElementById('generateBtn'),
        summaryResult: document.getElementById('summaryResult'),
        summaryContent: document.getElementById('summaryContent'),
        loadingIndicator: document.getElementById('loadingIndicator'),
        configToggle: document.getElementById('configToggle'),
        configPanel: document.getElementById('configPanel'),
        toggleMaxBtn: document.getElementById('toggleMaxBtn'),
        toggleMinBtn: document.getElementById('toggleMinBtn'),
        formatBtns: document.querySelectorAll('.format-btn'),
        copyBtn: document.getElementById('copyBtn'),
        apiProxyEnabled: document.getElementById('apiProxyEnabled'),
        apiProxyDomain: document.getElementById('apiProxyDomain'),
        proxyDomainContainer: document.getElementById('proxyDomainContainer')
      };
    }

    bindEvents() {
      this.bindAppEvents();
      this.bindIconEvents();
      this.bindConfigEvents();
      this.bindResizeEvents();
    }

    bindAppEvents() {
      const header = document.getElementById('summary-header');
      header.addEventListener('mousedown', this.dragStart.bind(this));
      document.addEventListener('mousemove', this.drag.bind(this));
      document.addEventListener('mouseup', this.dragEnd.bind(this));

      this.elements.toggleMaxBtn.addEventListener('click', this.toggleMaximize.bind(this));
      this.elements.toggleMinBtn.addEventListener('click', this.toggleMinimize.bind(this));
      this.elements.copyBtn.addEventListener('click', this.copyContent.bind(this));
    }

    bindIconEvents() {
      this.iconElement.addEventListener('mousedown', this.iconDragStart.bind(this));
      document.addEventListener('mousemove', this.iconDrag.bind(this));
      document.addEventListener('mouseup', this.iconDragEnd.bind(this));
      this.iconElement.addEventListener('click', this.toggleApp.bind(this));
    }

    bindConfigEvents() {
      this.elements.apiService.addEventListener('change', this.handleApiServiceChange.bind(this));
      this.elements.apiUrl.addEventListener('change', this.handleConfigChange.bind(this));
      this.elements.apiKey.addEventListener('change', this.handleConfigChange.bind(this));
      this.elements.modelName.addEventListener('change', this.handleConfigChange.bind(this));
      this.elements.modelSelect.addEventListener('change', this.handleModelSelectChange.bind(this));
      this.elements.configToggle.addEventListener('click', this.toggleConfig.bind(this));
      this.elements.formatBtns.forEach(btn => {
        btn.addEventListener('click', this.handleFormatChange.bind(this));
      });
      this.elements.apiProxyEnabled.addEventListener('change', this.handleProxyConfigChange.bind(this));
      this.elements.apiProxyDomain.addEventListener('change', this.handleProxyConfigChange.bind(this));
    }

    bindResizeEvents() {
      // 添加窗口大小调整事件
      const resizeHandle = document.querySelector('.resize-handle');
      if (resizeHandle) {
        resizeHandle.addEventListener('mousedown', this.startResize.bind(this));
      }
    }

    startResize(e) {
      e.preventDefault();
      e.stopPropagation();

      // 初始位置
      this.isResizing = true;
      this.initialWidth = this.app.offsetWidth;
      this.initialHeight = this.app.offsetHeight;
      this.initialX = e.clientX;
      this.initialY = e.clientY;

      // 创建绑定的处理函数
      this.resizeHandler = this.resize.bind(this);
      this.stopResizeHandler = this.stopResize.bind(this);

      // 添加临时事件监听器
      document.addEventListener('mousemove', this.resizeHandler);
      document.addEventListener('mouseup', this.stopResizeHandler);
    }

    resize(e) {
      if (!this.isResizing) return;

      // 计算新尺寸,设置最小值限制
      const minWidth = 320;
      const minHeight = 300;
      const newWidth = Math.max(minWidth, this.initialWidth + (e.clientX - this.initialX));
      const newHeight = Math.max(minHeight, this.initialHeight + (e.clientY - this.initialY));

      // 应用新尺寸
      this.app.style.width = newWidth + 'px';
      this.app.style.height = newHeight + 'px';

      // 保存尺寸到配置
      this.saveAppSize(newWidth, newHeight);
    }

    saveAppSize(width, height) {
      // 保存应用尺寸到配置
      const appSize = { width, height };
      this.configManager.setAppSize(appSize);
    }

    stopResize() {
      this.isResizing = false;

      // 移除临时事件监听器
      document.removeEventListener('mousemove', this.resizeHandler);
      document.removeEventListener('mouseup', this.stopResizeHandler);
    }

    restoreState() {
      try {
        const configs = this.configManager.getConfigs();
        const apiService = this.configManager.getApiService();

        // 确保服务配置存在
        if (!configs[apiService]) {
          configs[apiService] = {
            url: apiService === 'ollama' ? 'http://localhost:11434/api/chat' : '',
            model: apiService === 'ollama' ? 'llama2' : '',
            key: ''
          };
          // 保存新创建的配置
          this.configManager.setConfigs(configs);
        }

        const currentConfig = configs[apiService];

        // 设置表单值
        this.elements.apiKey.value = currentConfig.key || '';
        this.elements.modelName.value = currentConfig.model || '';
        this.elements.apiUrl.value = currentConfig.url || '';

        // 显示/隐藏 API Key 输入框
        this.elements.apiKeyContainer.style.display = apiService === 'ollama' ? 'none' : 'block';

        // 根据服务类型添加类名
        if (apiService === 'ollama') {
          this.app.classList.add('ollama-service');
          this.app.classList.remove('non-ollama-service');

          // 设置选中的模型
          const modelValue = currentConfig.model || 'llama2';
          const option = Array.from(this.elements.modelSelect.options).find(opt => opt.value === modelValue);
          if (option) {
            this.elements.modelSelect.value = modelValue;
          } else {
            this.elements.modelSelect.value = 'llama2';
          }

          // 尝试获取 Ollama 模型列表
          this.fetchOllamaModels();
        } else {
          this.app.classList.remove('ollama-service');
          this.app.classList.add('non-ollama-service');
        }

        this.elements.apiService.value = apiService;

        const format = this.configManager.getOutputFormat();
        this.elements.formatBtns.forEach(btn => {
          if (btn.dataset.format === format) {
            btn.classList.add('active');
          } else {
            btn.classList.remove('active');
          }
        });

        const configCollapsed = this.configManager.getConfigCollapsed();
        if (configCollapsed) {
          this.elements.configPanel.classList.add('collapsed');
          this.elements.configToggle.querySelector('.toggle-icon').style.transform = 'rotate(-90deg)';
        }

        // 恢复最小化状态 - 使用直接的DOM操作
        const appMinimized = this.configManager.getAppMinimized();
        console.log('恢复状态: 最小化状态 =', appMinimized);

        if (appMinimized) {
          // 直接设置显示状态
          document.getElementById('article-summary-app').style.display = 'none';
          document.getElementById('article-summary-icon').style.display = 'flex';

          console.log('已恢复最小化状态,图标显示状态:', document.getElementById('article-summary-icon').style.display);
        } else {
          // 直接设置显示状态
          document.getElementById('article-summary-app').style.display = 'flex';
          document.getElementById('article-summary-icon').style.display = 'none';

          console.log('已恢复正常状态,图标显示状态:', document.getElementById('article-summary-icon').style.display);
        }

        // 恢复位置
        const appPosition = this.configManager.getAppPosition();
        if (appPosition) {
          this.app.style.left = appPosition.x + 'px';
          this.app.style.top = appPosition.y + 'px';
          // 确保right和bottom属性被移除,避免位置冲突
          this.app.style.right = 'auto';
          this.app.style.bottom = 'auto';
        }

        // 恢复尺寸
        const appSize = this.configManager.getAppSize();
        if (appSize) {
          this.app.style.width = appSize.width + 'px';
          this.app.style.height = appSize.height + 'px';
        }

        const iconPosition = this.configManager.getIconPosition();
        if (iconPosition) {
          this.iconElement.style.left = iconPosition.x + 'px';
          this.iconElement.style.top = iconPosition.y + 'px';
          // 确保right和bottom属性被移除,避免位置冲突
          this.iconElement.style.right = 'auto';
          this.iconElement.style.bottom = 'auto';
        }

        // 恢复转发配置
        const proxyEnabled = this.configManager.getApiProxyEnabled();
        const proxyDomain = this.configManager.getApiProxyDomain();

        this.elements.apiProxyEnabled.checked = proxyEnabled;
        this.elements.apiProxyDomain.value = proxyDomain;
        this.elements.proxyDomainContainer.style.display = proxyEnabled ? 'block' : 'none';
      } catch (error) {
        console.error('恢复状态过程中出错:', error);
      }
    }

    // 拖拽相关方法
    dragStart(e) {
      this.isDragging = true;
      this.initialX = e.clientX - this.app.offsetLeft;
      this.initialY = e.clientY - this.app.offsetTop;
    }

    drag(e) {
      if (this.isDragging) {
        e.preventDefault();
        const currentX = Math.max(0, Math.min(
          e.clientX - this.initialX,
          window.innerWidth - this.app.offsetWidth
        ));
        const currentY = Math.max(0, Math.min(
          e.clientY - this.initialY,
          window.innerHeight - this.app.offsetHeight
        ));

        this.app.style.left = currentX + 'px';
        this.app.style.top = currentY + 'px';
      }
    }

    dragEnd() {
      if (this.isDragging) {
        this.isDragging = false;
        const position = {
          x: parseInt(this.app.style.left),
          y: parseInt(this.app.style.top)
        };
        this.configManager.setAppPosition(position);
      }
    }

    // 图标拖拽相关方法
    iconDragStart(e) {
      this.isIconDragging = true;
      this.iconInitialX = e.clientX - this.iconElement.offsetLeft;
      this.iconInitialY = e.clientY - this.iconElement.offsetTop;
      this.iconElement.style.cursor = 'grabbing';
    }

    iconDrag(e) {
      if (this.isIconDragging) {
        e.preventDefault();
        const currentX = Math.max(0, Math.min(
          e.clientX - this.iconInitialX,
          window.innerWidth - this.iconElement.offsetWidth
        ));
        const currentY = Math.max(0, Math.min(
          e.clientY - this.iconInitialY,
          window.innerHeight - this.iconElement.offsetHeight
        ));

        this.iconElement.style.left = currentX + 'px';
        this.iconElement.style.top = currentY + 'px';
        this.iconElement.style.right = 'auto';
      }
    }

    iconDragEnd() {
      if (this.isIconDragging) {
        this.isIconDragging = false;
        this.iconElement.style.cursor = 'pointer';
        const position = {
          x: parseInt(this.iconElement.style.left),
          y: parseInt(this.iconElement.style.top)
        };
        this.configManager.setIconPosition(position);
      }
    }

    // 配置相关方法
    handleApiServiceChange() {
      const service = this.elements.apiService.value;
      const configs = this.configManager.getConfigs();

      // 确保服务配置存在,如果不存在则创建默认配置
      if (!configs[service]) {
        configs[service] = {
          url: service === 'ollama' ? 'http://localhost:11434/api/chat' : '',
          model: service === 'ollama' ? 'llama2' : '',
          key: ''
        };
        // 保存新创建的配置
        this.configManager.setConfigs(configs);
      }

      const currentConfig = configs[service];

      // 设置表单值
      this.elements.apiKey.value = currentConfig.key || '';
      this.elements.modelName.value = currentConfig.model || '';
      this.elements.apiUrl.value = currentConfig.url || '';

      // 显示/隐藏 API Key 输入框
      this.elements.apiKeyContainer.style.display = service === 'ollama' ? 'none' : 'block';

      // 根据服务类型添加类名
      if (service === 'ollama') {
        this.app.classList.add('ollama-service');
        this.app.classList.remove('non-ollama-service');

        // 设置选中的模型
        const modelValue = currentConfig.model || 'llama2';
        const option = Array.from(this.elements.modelSelect.options).find(opt => opt.value === modelValue);
        if (option) {
          this.elements.modelSelect.value = modelValue;
        } else {
          this.elements.modelSelect.value = 'llama2';
        }

        // 尝试获取 Ollama 模型列表
        this.fetchOllamaModels();
      } else {
        this.app.classList.remove('ollama-service');
        this.app.classList.add('non-ollama-service');
      }

      this.configManager.setApiService(service);
    }

    handleConfigChange() {
      const service = this.elements.apiService.value;
      const configs = this.configManager.getConfigs();

      // 确保服务配置存在
      if (!configs[service]) {
        configs[service] = {
          url: service === 'ollama' ? 'http://localhost:11434/api/chat' : '',
          model: service === 'ollama' ? 'llama2' : '',
          key: ''
        };
      }

      // 获取当前表单值
      const apiKey = this.elements.apiKey.value || '';
      const modelName = service === 'ollama' ?
        (this.elements.modelSelect.value || 'llama2') :
        (this.elements.modelName.value || '');
      const apiUrl = this.elements.apiUrl.value ||
        (service === 'ollama' ? 'http://localhost:11434/api/chat' : '');

      // 更新配置
      configs[service] = {
        ...configs[service],
        key: apiKey,
        model: modelName,
        url: apiUrl
      };

      // 保存配置
      this.configManager.setConfigs(configs);
    }

    toggleConfig() {
      this.elements.configPanel.classList.toggle('collapsed');
      const isCollapsed = this.elements.configPanel.classList.contains('collapsed');
      const toggleIcon = this.elements.configToggle.querySelector('.toggle-icon');
      toggleIcon.style.transform = isCollapsed ? 'rotate(-90deg)' : '';
      this.configManager.setConfigCollapsed(isCollapsed);
    }

    handleFormatChange(e) {
      this.elements.formatBtns.forEach(btn => btn.classList.remove('active'));
      e.target.classList.add('active');
      this.configManager.setOutputFormat(e.target.dataset.format);
    }

    handleModelSelectChange() {
      // 确保选择的值有效
      const selectedModel = this.elements.modelSelect.value || 'llama2';
      this.elements.modelName.value = selectedModel;

      // 触发配置更新
      this.handleConfigChange();
    }

    // UI状态相关方法
    toggleMaximize() {
      if (!this.isMaximized) {
        this.previousSize = {
          width: this.app.style.width,
          height: this.app.style.height,
          left: this.app.style.left,
          top: this.app.style.top
        };

        this.app.style.width = '100%';
        this.app.style.height = '100vh';
        this.app.style.left = '0';
        this.app.style.top = '0';

        this.elements.toggleMaxBtn.innerHTML = this.getMaximizeIcon();
      } else {
        Object.assign(this.app.style, this.previousSize);
        this.elements.toggleMaxBtn.innerHTML = this.getRestoreIcon();
      }
      this.isMaximized = !this.isMaximized;
    }

    toggleMinimize() {
      try {
        // 直接操作DOM元素
        document.getElementById('article-summary-app').style.display = 'none';
        document.getElementById('article-summary-icon').style.display = 'flex';

        // 保存状态
        this.configManager.setAppMinimized(true);

        console.log('应用已最小化,图标显示状态:', document.getElementById('article-summary-icon').style.display);
      } catch (error) {
        console.error('最小化过程中出错:', error);
      }
    }

    toggleApp() {
      try {
        // 直接操作DOM元素
        document.getElementById('article-summary-app').style.display = 'flex';
        document.getElementById('article-summary-icon').style.display = 'none';

        // 保存状态
        this.configManager.setAppMinimized(false);

        console.log('应用已恢复,图标显示状态:', document.getElementById('article-summary-icon').style.display);
      } catch (error) {
        console.error('恢复应用过程中出错:', error);
      }
    }

    // 工具方法
    copyContent() {
      const outputFormat = document.querySelector('.format-btn.active').dataset.format;
      const summaryTextarea = document.getElementById('summaryTextarea');
      let textToCopy = summaryTextarea.value;

      navigator.clipboard.writeText(textToCopy).then(() => {
        const originalHTML = this.elements.copyBtn.innerHTML;
        this.elements.copyBtn.innerHTML = this.getCopiedIcon();
        setTimeout(() => {
          this.elements.copyBtn.innerHTML = originalHTML;
        }, 2000);
      }).catch(err => {
        console.error('复制失败:', err);
        alert('复制失败,请手动选择文本复制');
      });
    }

    // HTML模板方法
    getAppHTML() {
      return `
    <div id="summary-header">
      <h3>文章总结助手</h3>
      <div id="summary-header-actions">
        <button id="toggleMaxBtn" class="header-btn" data-tooltip="最大化">
          <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
            <path d="M8 3v3a2 2 0 01-2 2H3m18 0h-3a2 2 0 01-2-2V3m0 18v-3a2 2 0 012-2h3M3 16h3a2 2 0 012 2v3"></path>
          </svg>
        </button>
        <button id="toggleMinBtn" class="header-btn" data-tooltip="最小化">
          <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
            <path d="M20 12H4"></path>
          </svg>
        </button>
      </div>
    </div>
    <div id="summary-body">
      <div id="config-section">
        <div id="configToggle">
          <span>配置选项</span>
          <svg class="icon toggle-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
            <path d="M19 9l-7 7-7-7"></path>
          </svg>
        </div>
        <div id="configPanel">
          <div class="form-group">
            <label class="form-label" for="apiService">API服务</label>
            <select id="apiService" class="form-select">
                  <option value="ollama">Ollama</option>
              <option value="gptgod">GPT God</option>
              <option value="deepseek">DeepSeek</option>
              <option value="custom">自定义</option>
            </select>
          </div>
          
              <div id="apiUrlContainer" class="form-group">
                <label class="form-label" for="apiUrl">API地址</label>
                <input type="text" id="apiUrl" class="form-input" placeholder="http://localhost:11434/api/chat">
          </div>
          
              <div class="form-group" id="apiKeyContainer">
            <label class="form-label" for="apiKey">API Key</label>
            <input type="password" id="apiKey" class="form-input" placeholder="sk-...">
          </div>
          
          <div class="form-group">
            <label class="form-label" for="modelName">模型</label>
                <select id="modelSelect" class="form-select">
                  <option value="llama2">llama2</option>
                  <option value="llama2:13b">llama2:13b</option>
                  <option value="llama2:70b">llama2:70b</option>
                  <option value="mistral">mistral</option>
                  <option value="mixtral">mixtral</option>
                  <option value="gemma:2b">gemma:2b</option>
                  <option value="gemma:7b">gemma:7b</option>
                  <option value="qwen:14b">qwen:14b</option>
                  <option value="qwen:72b">qwen:72b</option>
                  <option value="phi3:mini">phi3:mini</option>
                  <option value="phi3:small">phi3:small</option>
                  <option value="phi3:medium">phi3:medium</option>
                  <option value="yi:34b">yi:34b</option>
                  <option value="vicuna:13b">vicuna:13b</option>
                  <option value="vicuna:33b">vicuna:33b</option>
                  <option value="codellama">codellama</option>
                  <option value="wizardcoder">wizardcoder</option>
                  <option value="nous-hermes2">nous-hermes2</option>
                  <option value="neural-chat">neural-chat</option>
                  <option value="openchat">openchat</option>
                  <option value="dolphin-mixtral">dolphin-mixtral</option>
                  <option value="starling-lm">starling-lm</option>
                </select>
                <input type="text" id="modelName" class="form-input" placeholder="模型名称">
          </div>
          
          <div class="form-group">
            <label class="form-label">输出格式</label>
            <div id="formatOptions">
              <span class="format-btn active" data-format="markdown">Markdown</span>
              <span class="format-btn" data-format="bullet">要点列表</span>
              <span class="format-btn" data-format="paragraph">段落</span>
            </div>
          </div>
          
          <div class="form-group">
            <label class="form-label">API转发设置</label>
            <div class="proxy-settings">
              <div class="checkbox-container">
                <input type="checkbox" id="apiProxyEnabled" class="form-checkbox">
                <label for="apiProxyEnabled">启用API转发</label>
              </div>
              <div id="proxyDomainContainer" class="form-group" style="display: none;">
                <label class="form-label" for="apiProxyDomain">转发服务域名</label>
                <input type="text" id="apiProxyDomain" class="form-input" placeholder="https://nakoruru.h7ml.cn">
              </div>
            </div>
          </div>
        </div>
      </div>
      
      <button type="button" id="generateBtn">
        <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
          <path d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path>
        </svg>
        生成总结
      </button>
      
      <div id="summaryResult">
        <div id="summaryHeader">
          <h4>文章总结</h4>
          <div id="summaryActions">
            <button id="copyBtn" class="action-btn" data-tooltip="复制到剪贴板">
              <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                <path d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"></path>
              </svg>
              复制
            </button>
          </div>
        </div>
        <div id="summaryContent" class="markdown-body">
          <textarea id="summaryTextarea" class="content-textarea" placeholder="生成的总结将显示在这里..."></textarea>
        </div>
      </div>
      
      <div id="loadingIndicator">
        <div class="spinner"></div>
        <p>正在生成总结,请稍候...</p>
      </div>
      <div class="resize-handle"></div>
    </div>
  `;
    }

    getIconHTML() {
      return `
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
      <path d="M9 12h6m-6 4h6m2-10H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V8a2 2 0 00-2-2z"></path>
    </svg>
  `;
    }

    getMaximizeIcon() {
      return `
    <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
      <path d="M8 3v3a2 2 0 01-2 2H3m18 0h-3a2 2 0 01-2-2V3m0 18v-3a2 2 0 012-2h3M3 16h3a2 2 0 012 2v3"></path>
    </svg>
  `;
    }

    getRestoreIcon() {
      return `
    <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
      <path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"></path>
    </svg>
  `;
    }

    getCopiedIcon() {
      return `
    <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
      <path d="M20 6L9 17l-5-5"></path>
    </svg>
    已复制
  `;
    }

    async fetchOllamaModels() {
      try {
        const models = await this.apiService.fetchOllamaModels();
        if (models && models.length > 0) {
          // 清空现有选项
          this.elements.modelSelect.innerHTML = '';

          // 添加获取到的模型选项
          models.forEach(model => {
            const option = document.createElement('option');
            option.value = model;
            option.textContent = model;
            this.elements.modelSelect.appendChild(option);
          });

          // 设置当前选中的模型
          const configs = this.configManager.getConfigs();
          const currentModel = configs.ollama.model;
          if (currentModel && models.includes(currentModel)) {
            this.elements.modelSelect.value = currentModel;
          } else if (models.includes('llama2')) {
            this.elements.modelSelect.value = 'llama2';
          } else if (models.length > 0) {
            this.elements.modelSelect.value = models[0];
          }

          console.log('成功获取 Ollama 模型列表:', models);
        }
      } catch (error) {
        console.error('获取 Ollama 模型列表失败:', error);
      }
    }

    // 添加处理转发配置变更的方法
    handleProxyConfigChange() {
      const proxyEnabled = this.elements.apiProxyEnabled.checked;
      const proxyDomain = this.elements.apiProxyDomain.value.trim();

      // 更新配置
      this.configManager.setApiProxyEnabled(proxyEnabled);
      if (proxyDomain) {
        this.configManager.setApiProxyDomain(proxyDomain);
      }

      // 显示/隐藏域名输入框
      this.elements.proxyDomainContainer.style.display = proxyEnabled ? 'block' : 'none';
    }
  }

  // 文章提取类
  class ArticleExtractor {
    constructor() {
      this.selectors = [
        '#js_content',
        '.RichText',
        '.article-content',
        '#article_content',
        '#cnblogs_post_body',
        'article',
        '.article',
        '.post-content',
        '.content',
        '.entry-content',
        '.article-content',
        'main',
        '#main',
        '.main'
      ];

      this.removeSelectors = [
        'script',
        'style',
        'iframe',
        'nav',
        'header',
        'footer',
        '.advertisement',
        '.ad',
        '.ads',
        '.social-share',
        '.related-posts',
        '.comments',
        '.comment',
        '.author-info',
        '.article-meta',
        '.article-info',
        '.article-header',
        '.article-footer',
        '#article-summary-app'
      ];
    }

    async extract() {
      // 尝试使用不同的选择器获取内容
      for (const selector of this.selectors) {
        const element = document.querySelector(selector);
        if (element) {
          const content = this.processElement(element);
          if (content.length > 100) {
            return content;
          }
        }
      }

      // 如果上述方法都失败,尝试获取整个页面的主要内容
      const content = this.processElement(document.body);
      if (content.length < 100) {
        throw new Error('无法获取足够的文章内容');
      }

      return content;
    }

    processElement(element) {
      const clone = element.cloneNode(true);
      this.removeUnwantedElements(clone);
      return this.cleanText(clone.innerText);
    }

    removeUnwantedElements(element) {
      this.removeSelectors.forEach(selector => {
        const elements = element.querySelectorAll(selector);
        elements.forEach(el => el.remove());
      });
    }

    cleanText(text) {
      return text
        .replace(/\s+/g, ' ')
        .replace(/\n\s*\n/g, '\n')
        .trim();
    }
  }

  // API服务类
  class APIService {
    constructor(configManager) {
      this.configManager = configManager;
    }

    async generateSummary(content) {
      const configs = this.configManager.getConfigs();
      const apiService = this.configManager.getApiService();
      const currentConfig = configs[apiService];
      const outputFormat = this.configManager.getOutputFormat();

      const apiEndpoint = this.getApiEndpoint(apiService, currentConfig);
      const transformedEndpoint = this.transformUrl(apiEndpoint);
      const systemPrompt = this.getSystemPrompt(outputFormat);
      const messages = this.createMessages(systemPrompt, content);

      return this.makeRequest(transformedEndpoint, currentConfig, messages);
    }

    async fetchOllamaModels() {
      return new Promise((resolve, reject) => {
        const ollamaConfig = this.configManager.getConfigs().ollama;
        // 从 API URL 中提取基础 URL
        const baseUrl = ollamaConfig.url.split('/api/')[0] || 'http://localhost:11434';
        const modelsEndpoint = `${baseUrl}/api/tags`;

        const transformedEndpoint = this.transformUrl(modelsEndpoint);

        GM_xmlhttpRequest({
          method: 'GET',
          url: transformedEndpoint,
          headers: {
            'Content-Type': 'application/json'
          },
          onload: (response) => {
            try {
              if (response.status >= 400) {
                console.warn('获取 Ollama 模型列表失败:', response.statusText);
                resolve([]); // 失败时返回空数组,使用默认模型列表
                return;
              }

              const data = JSON.parse(response.responseText);
              if (data.models && Array.isArray(data.models)) {
                // 提取模型名称
                const models = data.models.map(model => model.name);
                resolve(models);
              } else {
                console.warn('Ollama API 返回的模型列表格式异常:', data);
                resolve([]);
              }
            } catch (error) {
              console.error('解析 Ollama 模型列表失败:', error);
              resolve([]);
            }
          },
          onerror: (error) => {
            console.error('获取 Ollama 模型列表请求失败:', error);
            resolve([]); // 失败时返回空数组,使用默认模型列表
          }
        });
      });
    }

    getApiEndpoint(apiService, config) {
      return config.url;
    }

    getSystemPrompt(format) {
      const prompts = {
        markdown: "请用中文总结以下文章的主要内容,以标准Markdown格式输出,包括标题、小标题和要点。确保格式规范,便于阅读。",
        bullet: "请用中文总结以下文章的主要内容,以简洁的要点列表形式输出,每个要点前使用'- '标记。",
        paragraph: "请用中文总结以下文章的主要内容,以连贯的段落形式输出,突出文章的核心观点和结论。"
      };
      return prompts[format] || "请用中文总结以下文章的主要内容,以简洁的方式列出重点。";
    }

    createMessages(systemPrompt, content) {
      const apiService = this.configManager.getApiService();
      if (apiService === 'ollama') {
        return [
          { role: "system", content: systemPrompt },
          { role: "user", content: content }
        ];
      } else {
        return [
          { role: "system", content: systemPrompt },
          { role: "user", content: content }
        ];
      }
    }

    makeRequest(endpoint, config, messages) {
      return new Promise((resolve, reject) => {
        const apiService = this.configManager.getApiService();

        // 确保配置有效
        if (!endpoint) {
          reject(new Error('API 地址无效'));
          return;
        }

        if (!config.model) {
          reject(new Error('模型名称无效'));
          return;
        }

        // 构建请求数据
        const requestData = {
          model: config.model,
          messages: messages,
          stream: false
        };

        // 构建请求头
        const headers = {
          'Content-Type': 'application/json'
        };

        // 非 Ollama 服务需要 API Key
        if (apiService !== 'ollama' && config.key) {
          headers['Authorization'] = `Bearer ${config.key}`;
        }

        // 发送请求
        GM_xmlhttpRequest({
          method: 'POST',
          url: endpoint,
          headers: headers,
          data: JSON.stringify(requestData),
          onload: this.handleResponse.bind(this, resolve, reject, apiService),
          onerror: (error) => reject(new Error('网络请求失败: ' + (error.message || '未知错误')))
        });
      });
    }

    handleResponse(resolve, reject, apiService, response) {
      try {
        // 检查响应是否为 HTML
        if (response.responseText.trim().startsWith('<')) {
          reject(new Error(`API返回了HTML而不是JSON (状态码: ${response.status})`));
          return;
        }

        // 检查状态码
        if (response.status >= 400) {
          try {
            const data = JSON.parse(response.responseText);
            reject(new Error(data.error?.message || `请求失败 (${response.status})`));
          } catch (e) {
            reject(new Error(`请求失败 (${response.status}): ${response.responseText.substring(0, 100)}`));
          }
          return;
        }

        // 解析响应数据
        const data = JSON.parse(response.responseText);

        // 检查错误
        if (data.error) {
          reject(new Error(data.error.message || '未知错误'));
          return;
        }

        // 根据不同的 API 服务提取内容
        if (apiService === 'ollama' && data.message) {
          // Ollama API 响应格式
          resolve(data.message.content);
        } else if (data.choices && data.choices.length > 0 && data.choices[0].message) {
          // OpenAI 兼容的 API 响应格式
          resolve(data.choices[0].message.content);
        } else {
          // 未知的响应格式
          console.warn('未知的 API 响应格式:', data);

          // 尝试从响应中提取可能的内容
          if (data.content) {
            resolve(data.content);
          } else if (data.text) {
            resolve(data.text);
          } else if (data.result) {
            resolve(data.result);
          } else if (data.response) {
            resolve(data.response);
          } else if (data.output) {
            resolve(data.output);
          } else if (data.generated_text) {
            resolve(data.generated_text);
          } else {
            reject(new Error('API 返回格式异常,无法提取内容'));
          }
        }
      } catch (error) {
        reject(new Error(`解析API响应失败: ${error.message || '未知错误'}`));
      }
    }

    // 添加URL转换函数
    transformUrl(url) {
      // 检查是否启用了转发
      if (!this.configManager.getApiProxyEnabled()) {
        return url; // 如果未启用转发,直接返回原始URL
      }

      try {
        // 获取转发服务域名
        const proxyDomain = this.configManager.getApiProxyDomain();
        if (!proxyDomain) {
          return url; // 如果未设置转发域名,直接返回原始URL
        }

        // 解析原始URL
        const urlObj = new URL(url);
        const protocol = urlObj.protocol;
        const hostname = urlObj.hostname;
        const pathname = urlObj.pathname;
        const search = urlObj.search;

        // 根据规则转换URL
        let proxyUrl;
        if (protocol === 'https:') {
          // HTTPS转发
          proxyUrl = `${proxyDomain}/proxy/${hostname}${pathname}${search}`;
        } else if (protocol === 'http:') {
          // HTTP转发
          proxyUrl = `${proxyDomain}/httpproxy/${hostname}${pathname}${search}`;
        } else if (url.includes('api.')) {
          // API转发 - 针对包含api.的域名
          proxyUrl = `${proxyDomain}/api/${hostname}${pathname}${search}`;
        } else {
          return url; // 不符合转发规则,返回原始URL
        }

        console.log(`API转发: ${url} -> ${proxyUrl}`);
        return proxyUrl;
      } catch (error) {
        console.error('URL转换失败:', error);
        return url; // 出错时返回原始URL
      }
    }
  }

  // 主应用类
  class ArticleSummaryApp {
    constructor() {
      this.configManager = new ConfigManager();
      this.uiManager = new UIManager(this.configManager);
      this.articleExtractor = new ArticleExtractor();
      this.apiService = new APIService(this.configManager);
      this.version = '0.2.1'; // 更新版本号
    }

    async init() {
      this.logScriptInfo();
      await this.uiManager.init();
      this.bindGenerateButton();
    }

    logScriptInfo() {
      const styles = {
        title: 'font-size: 16px; font-weight: bold; color: #4CAF50;',
        subtitle: 'font-size: 14px; font-weight: bold; color: #2196F3;',
        normal: 'font-size: 12px; color: #333;',
        key: 'font-size: 12px; color: #E91E63;',
        value: 'font-size: 12px; color: #3F51B5;'
      };

      console.log('%c网页文章总结助手', styles.title);
      console.log('%c基本信息', styles.subtitle);
      console.log(`%c版本:%c ${this.version}`, styles.key, styles.value);
      console.log(`%c作者:%c h7ml <[email protected]>`, styles.key, styles.value);
      console.log(`%c描述:%c 自动总结网页文章内容,支持多种格式输出,适用于各类文章网站`, styles.key, styles.value);

      console.log('%c支持的API服务', styles.subtitle);
      console.log(`%c- Ollama:%c 本地大语言模型服务,无需API Key`, styles.key, styles.normal);
      console.log(`%c- GPT God:%c 支持多种OpenAI模型`, styles.key, styles.normal);
      console.log(`%c- DeepSeek:%c 支持DeepSeek系列模型`, styles.key, styles.normal);
      console.log(`%c- 自定义:%c 支持任何兼容OpenAI API格式的服务`, styles.key, styles.normal);

      console.log('%c支持的功能', styles.subtitle);
      console.log(`%c- 自动提取:%c 智能提取网页文章内容`, styles.key, styles.normal);
      console.log(`%c- 多种格式:%c 支持Markdown、要点列表、段落等输出格式`, styles.key, styles.normal);
      console.log(`%c- 动态获取:%c 自动获取Ollama本地已安装模型列表`, styles.key, styles.normal);
      console.log(`%c- 界面定制:%c 支持拖拽、最小化、最大化等操作`, styles.key, styles.normal);

      console.log('%c当前配置', styles.subtitle);
      const configs = this.configManager.getConfigs();
      const apiService = this.configManager.getApiService();
      const currentConfig = configs[apiService] || {};
      console.log(`%c当前API服务:%c ${apiService}`, styles.key, styles.value);
      console.log(`%c当前模型:%c ${currentConfig.model || '未设置'}`, styles.key, styles.value);
      console.log(`%c当前API地址:%c ${currentConfig.url || '未设置'}`, styles.key, styles.value);
      console.log(`%c输出格式:%c ${this.configManager.getOutputFormat()}`, styles.key, styles.value);

      console.log('%c使用提示', styles.subtitle);
      console.log(`%c- 点击右上角按钮可最小化或最大化界面`, styles.normal);
      console.log(`%c- 最小化后可通过右下角图标恢复界面`, styles.normal);
      console.log(`%c- 可拖动顶部标题栏移动位置`, styles.normal);
      console.log(`%c- 使用Ollama服务时会自动获取本地已安装模型`, styles.normal);
    }

    bindGenerateButton() {
      this.uiManager.elements.generateBtn.addEventListener('click', this.handleGenerate.bind(this));
    }

    async handleGenerate() {
      const apiService = this.uiManager.elements.apiService.value;
      const apiKey = this.uiManager.elements.apiKey.value.trim();
      const apiUrl = this.uiManager.elements.apiUrl.value.trim();

      // 获取当前配置
      const configs = this.configManager.getConfigs();
      const currentConfig = configs[apiService] || {
        url: apiService === 'ollama' ? 'http://localhost:11434/api/chat' : '',
        model: apiService === 'ollama' ? 'llama2' : '',
        key: ''
      };

      // 检查 API URL 是否有效
      if (!apiUrl) {
        alert('请输入有效的 API 地址');
        return;
      }

      // 检查 API Key(Ollama 不需要)
      if (apiService !== 'ollama' && !apiKey) {
        alert('请输入有效的 API Key');
        return;
      }

      // 检查模型是否有效
      const modelName = apiService === 'ollama' ?
        (this.uiManager.elements.modelSelect.value || 'llama2') :
        (this.uiManager.elements.modelName.value || '');

      if (!modelName) {
        alert('请选择或输入有效的模型名称');
        return;
      }

      this.showLoading();

      try {
        const content = await this.articleExtractor.extract();
        const summary = await this.apiService.generateSummary(content);
        this.displaySummary(summary);
      } catch (error) {
        this.handleError(error);
      } finally {
        this.hideLoading();
      }
    }

    showLoading() {
      this.uiManager.elements.loadingIndicator.style.display = 'block';
      this.uiManager.elements.generateBtn.disabled = true;
      this.uiManager.elements.generateBtn.innerHTML = `
          <svg class="icon spinner" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
            <circle cx="12" cy="12" r="10" stroke-opacity="0.25" stroke-dasharray="30" stroke-dashoffset="0"></circle>
            <circle cx="12" cy="12" r="10" stroke-dasharray="30" stroke-dashoffset="15"></circle>
          </svg>
          生成中...
        `;
    }

    hideLoading() {
      this.uiManager.elements.loadingIndicator.style.display = 'none';
      this.uiManager.elements.generateBtn.disabled = false;
      this.uiManager.elements.generateBtn.innerHTML = `
          <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
            <path d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path>
          </svg>
          生成总结
        `;
    }

    displaySummary(summary) {
      const outputFormat = this.configManager.getOutputFormat();
      const summaryContent = this.uiManager.elements.summaryContent;
      const summaryTextarea = document.getElementById('summaryTextarea');

      if (outputFormat === 'markdown') {
        summaryTextarea.value = summary;
        summaryContent.setAttribute('data-markdown', summary);
      } else {
        summaryTextarea.value = summary;
      }

      this.uiManager.elements.summaryResult.style.display = 'flex';
    }

    handleError(error) {
      let errorMsg = error.message;
      if (errorMsg.includes('Authentication Fails') || errorMsg.includes('no such user')) {
        errorMsg = 'API Key 无效或已过期,请更新您的 API Key';
      } else if (errorMsg.includes('rate limit')) {
        errorMsg = 'API 调用次数已达上限,请稍后再试';
      }
      alert('生成总结失败:' + errorMsg);
      console.error('API 错误详情:', error);
    }

    simpleMarkdownRender(text) {
      let html = '<div class="summary-container">';
      const content = text
        .replace(/^# (.*$)/gm, '<h1>$1</h1>')
        .replace(/^## (.*$)/gm, '<h2>$1</h2>')
        .replace(/^### (.*$)/gm, '<h3>$1</h3>')
        .replace(/^\d+\.\s+\*\*(.*?)\*\*:([\s\S]*?)(?=(?:\d+\.|$))/gm, (match, title, items) => {
          const listItems = items
            .split(/\n\s*-\s+/)
            .filter(item => item.trim())
            .map(item => `<li>${item.trim()}</li>`)
            .join('');
          return `<h2>${title}</h2><ul>${listItems}</ul>`;
        })
        .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
        .replace(/`([^`]+)`/g, '<code>$1</code>')
        .replace(/([^\n]+)(?:\n|$)/g, (match, p1) => {
          if (!p1.startsWith('<') && p1.trim()) {
            return `<p>${p1}</p>`;
          }
          return p1;
        });
      html += content + '</div>';
      return html;
    }
  }

  // 初始化应用
  const app = new ArticleSummaryApp();
  app.init();
})();