网页文章总结助手

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

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

// ==UserScript==
// @name         网页文章总结助手
// @namespace    http://tampermonkey.net/
// @version      0.1.3
// @description  自动总结网页文章内容,支持多种格式输出,适用于各类文章网站
// @author       h7ml <[email protected]>
// @match        *://*/*
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_getResourceText
// @connect      api.gptgod.online
// @connect      api.deepseek.com
// @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';

  // 加载 marked.js 库
  function loadMarkedJS() {
    return new Promise((resolve) => {
      try {
        const markedScript = GM_getResourceText('marked');
        if (markedScript) {
          // 创建一个函数来执行 marked.js 的内容
          const executeMarked = new Function(markedScript);
          executeMarked();
          resolve();
        } else {
          console.error('无法加载 marked.js');
          resolve(); // 即使加载失败也继续
        }
      } catch (error) {
        console.error('加载 marked.js 失败:', error);
        resolve(); // 即使加载失败也继续
      }
    });
  }

  // 加载 highlight.js 库
  function loadHighlightJS() {
    return new Promise((resolve) => {
      try {
        const highlightScript = GM_getResourceText('highlight');
        const highlightStyle = GM_getResourceText('highlightStyle');

        if (highlightScript) {
          // 创建一个函数来执行 highlight.js 的内容
          const executeHighlight = new Function(highlightScript);
          executeHighlight();
        }

        if (highlightStyle) {
          const style = document.createElement('style');
          style.textContent = highlightStyle;
          document.head.appendChild(style);
        }

        resolve();
      } catch (error) {
        console.error('加载 highlight.js 失败:', error);
        resolve(); // 即使加载失败也继续
      }
    });
  }

  // 创建样式 - 使用普通 CSS
  const style = document.createElement('style');
  style.textContent = `
    /* 基础样式 */
    #article-summary-app {
      position: fixed;
      top: 1rem;
      right: 1rem;
      width: 24rem;
      z-index: 2147483647;
      min-width: 300px;
      min-height: 200px;
      resize: both;
      overflow: auto;
      cursor: move;
      font-family: system-ui, -apple-system, sans-serif;
      font-size: 0.875rem;
      line-height: 1.5;
      color: #374151;
      background-color: white;
      border-radius: 0.5rem;
      box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
      transition: all 0.2s;
    }
    
    /* 头部样式 */
    #summary-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 0.75rem 1rem;
      background-color: #2563eb;
      color: white;
    }
    
    #summary-header h3 {
      margin: 0;
      font-size: 1rem;
      font-weight: 600;
    }
    
    #summary-header-actions {
      display: flex;
      gap: 0.5rem;
    }
    
    .header-btn {
      background: transparent;
      border: none;
      color: white;
      cursor: pointer;
      padding: 0.25rem;
      border-radius: 0.25rem;
      display: flex;
      align-items: center;
      justify-content: center;
      width: 1.5rem;
      height: 1.5rem;
      transition: background-color 0.2s;
    }
    
    .header-btn:hover {
      background-color: rgba(255, 255, 255, 0.2);
    }
    
    /* 主体内容 */
    #summary-body {
      padding: 1rem;
    }
    
    /* 配置面板 */
    #config-section {
      margin-bottom: 1rem;
    }
    
    #configToggle {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 0.625rem 0.75rem;
      background-color: #f3f4f6;
      border-radius: 0.25rem;
      cursor: pointer;
      font-weight: 500;
      transition: background-color 0.2s;
    }
    
    #configToggle:hover {
      background-color: #e5e7eb;
    }
    
    #configToggle.collapsed .toggle-icon {
      transform: rotate(-90deg);
    }
    
    .toggle-icon {
      transition: transform 0.2s;
    }
    
    #configPanel {
      max-height: 500px;
      overflow: hidden;
      transition: all 0.3s ease-out;
      opacity: 1;
      margin-top: 0.75rem;
    }
    
    #configPanel.collapsed {
      max-height: 0;
      opacity: 0;
      margin-top: 0;
    }
    
    /* 表单元素 */
    .form-group {
      margin-bottom: 0.75rem;
    }
    
    .form-label {
      display: block;
      margin-bottom: 0.375rem;
      font-weight: 500;
      color: #374151;
    }
    
    .form-select, .form-input {
      width: 100%;
      padding: 0.5rem 0.75rem;
      border: 1px solid #d1d5db;
      border-radius: 0.25rem;
      background-color: #f9fafb;
      transition: all 0.2s;
    }
    
    .form-select:focus, .form-input:focus {
      outline: none;
      border-color: #3b82f6;
      box-shadow: 0 0 0 1px #3b82f6;
    }
    
    /* 格式选择按钮 */
    #formatOptions {
      display: flex;
      flex-wrap: wrap;
      gap: 0.5rem;
      margin-bottom: 0.75rem;
    }
    
    .format-btn {
      padding: 0.375rem 0.75rem;
      background-color: #f3f4f6;
      border: 1px solid #d1d5db;
      border-radius: 0.25rem;
      font-size: 0.875rem;
      cursor: pointer;
      transition: all 0.2s;
    }
    
    .format-btn:hover {
      background-color: #e5e7eb;
    }
    
    .format-btn.active {
      background-color: #2563eb;
      color: white;
      border-color: #2563eb;
    }
    
    /* 生成按钮 */
    #generateBtn {
      width: 100%;
      padding: 0.625rem;
      background-color: #2563eb;
      color: white;
      border: none;
      border-radius: 0.25rem;
      font-weight: 500;
      cursor: pointer;
      transition: background-color 0.2s;
      display: flex;
      justify-content: center;
      align-items: center;
      gap: 0.5rem;
    }
    
    #generateBtn:hover {
      background-color: #1d4ed8;
    }
    
    #generateBtn:disabled {
      background-color: #93c5fd;
      cursor: not-allowed;
    }
    
    /* 结果区域 */
    #summaryResult {
      margin-top: 1rem;
      display: none;
    }
    
    #summaryHeader {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 0.5rem;
    }
    
    #summaryHeader h4 {
      margin: 0;
      font-size: 1rem;
      font-weight: 600;
      color: #1f2937;
    }
    
    #summaryActions {
      display: flex;
      gap: 0.5rem;
    }
    
    .action-btn {
      padding: 0.25rem 0.5rem;
      background-color: #f3f4f6;
      border: 1px solid #d1d5db;
      border-radius: 0.25rem;
      font-size: 0.75rem;
      cursor: pointer;
      display: flex;
      align-items: center;
      gap: 0.25rem;
      transition: background-color 0.2s;
    }
    
    .action-btn:hover {
      background-color: #e5e7eb;
    }
    
    #summaryContent {
      border: 1px solid #e5e7eb;
      border-radius: 0.25rem;
      padding: 1rem;
      background-color: #f9fafb;
      max-height: 400px;
      overflow-y: auto;
      line-height: 1.625;
    }
    
    /* Markdown 样式 */
    #summaryContent.markdown-body {
      font-family: system-ui, -apple-system, sans-serif;
    }
    
    #summaryContent h1 {
      font-size: 1.25rem;
      margin: 0.5rem 0;
      font-weight: 600;
      color: #111827;
    }
    
    #summaryContent h2 {
      font-size: 1.125rem;
      margin: 0.5rem 0;
      font-weight: 600;
      color: #111827;
    }
    
    #summaryContent h3 {
      font-size: 1rem;
      margin: 0.5rem 0;
      font-weight: 600;
      color: #111827;
    }
    
    #summaryContent ul, #summaryContent ol {
      padding-left: 1.5rem;
      margin: 0.5rem 0;
    }
    
    #summaryContent p {
      margin: 0.5rem 0;
    }
    
    #summaryContent pre {
      background-color: #f3f4f6;
      padding: 0.75rem;
      border-radius: 0.25rem;
      overflow-x: auto;
      margin: 0.5rem 0;
    }
    
    #summaryContent code {
      font-family: ui-monospace, monospace;
      font-size: 0.875rem;
      background-color: #f3f4f6;
      padding: 0.125rem 0.375rem;
      border-radius: 0.25rem;
    }
    
    /* 加载指示器 */
    #loadingIndicator {
      display: none;
      text-align: center;
      padding: 1.25rem 0;
    }
    
    .spinner {
      width: 2.5rem;
      height: 2.5rem;
      margin: 0 auto;
      border: 3px solid #e5e7eb;
      border-radius: 9999px;
      border-top-color: #2563eb;
      animation: spin 1s linear infinite;
    }
    
    @keyframes spin {
      to {
        transform: rotate(360deg);
      }
    }
    
    /* 响应式调整 */
    @media (max-width: 640px) {
      #article-summary-app {
        width: 90%;
        right: 5%;
        left: 5%;
      }
    }
    
    /* 工具提示 */
    .tooltip {
      position: relative;
    }
    
    .tooltip:hover::after {
      content: attr(data-tooltip);
      position: absolute;
      bottom: 100%;
      left: 50%;
      transform: translateX(-50%);
      padding: 0.25rem 0.5rem;
      background-color: #1f2937;
      color: white;
      border-radius: 0.25rem;
      font-size: 0.75rem;
      white-space: nowrap;
      z-index: 10;
    }
    
    /* 图标 */
    .icon {
      width: 1rem;
      height: 1rem;
      display: inline-block;
    }

    /* 添加拖拽相关样式 */
    .draggable {
      user-select: none;
      cursor: move;
    }

    .resizable {
      resize: both;
      overflow: auto;
    }

    /* 总结内容样式优化 */
    .summary-container {
      font-family: system-ui, -apple-system, sans-serif;
      line-height: 1.6;
      color: #333;
      padding: 1rem;
    }

    .summary-container h1 {
      font-size: 1.5rem;
      color: #1a365d;
      margin-bottom: 1rem;
      padding-bottom: 0.5rem;
      border-bottom: 2px solid #e2e8f0;
    }

    .summary-container h2 {
      font-size: 1.25rem;
      color: #2d3748;
      margin: 1.5rem 0 1rem;
    }

    .summary-container ul {
      list-style: none;
      padding-left: 1.5rem;
      margin: 1rem 0;
    }

    .summary-container li {
      position: relative;
      padding-left: 1.5rem;
      margin-bottom: 0.5rem;
    }

    .summary-container li:before {
      content: "•";
      color: #4299e1;
      font-weight: bold;
      position: absolute;
      left: 0;
    }

    .summary-container strong {
      color: #2b6cb0;
      font-weight: 600;
    }

    .summary-container code {
      background: #f7fafc;
      padding: 0.2rem 0.4rem;
      border-radius: 0.25rem;
      font-family: monospace;
      font-size: 0.9em;
      color: #4a5568;
      border: 1px solid #edf2f7;
    }
  `;
  document.head.appendChild(style);

  // 创建应用容器
  const app = document.createElement('div');
  app.id = 'article-summary-app';

  // 创建HTML内容 - 使用更现代的UI设计
  app.innerHTML = `
    <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="gptgod">GPT God</option>
              <option value="deepseek">DeepSeek</option>
              <option value="custom">自定义</option>
            </select>
          </div>
          
          <div id="customApiUrlContainer" class="form-group" style="display: none;">
            <label class="form-label" for="customApiUrl">API地址</label>
            <input type="text" id="customApiUrl" class="form-input" placeholder="https://api.example.com/v1/chat/completions">
          </div>
          
          <div class="form-group">
            <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>
            <input type="text" id="modelName" class="form-input" placeholder="gpt-4o-all">
          </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>
      </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 class="content-textarea resizable"></textarea>
        </div>
      </div>
      
      <div id="loadingIndicator">
        <div class="spinner"></div>
        <p>正在生成总结,请稍候...</p>
      </div>
    </div>
  `;

  document.body.appendChild(app);

  // 加载 Markdown 处理库
  Promise.all([loadMarkedJS(), loadHighlightJS()]).then(() => {
    console.log('Markdown 渲染库加载完成');

    // 如果 marked 库加载成功,配置它
    if (window.marked) {
      marked.setOptions({
        renderer: new marked.Renderer(),
        highlight: function (code, lang) {
          if (window.hljs) {
            const language = lang && hljs.getLanguage(lang) ? lang : 'plaintext';
            return hljs.highlight(code, { language }).value;
          }
          return code;
        },
        langPrefix: 'hljs language-',
        pedantic: false,
        gfm: true,
        breaks: true,
        sanitize: false,
        smartypants: false,
        xhtml: false
      });
    }
  }).catch(error => {
    console.error('加载 Markdown 渲染库失败:', error);
  });

  // 获取DOM元素
  const apiServiceSelect = document.getElementById('apiService');
  const customApiUrlContainer = document.getElementById('customApiUrlContainer');
  const customApiUrlInput = document.getElementById('customApiUrl');
  const apiKeyInput = document.getElementById('apiKey');
  const modelNameInput = document.getElementById('modelName');
  const generateBtn = document.getElementById('generateBtn');
  const summaryResult = document.getElementById('summaryResult');
  const summaryContent = document.getElementById('summaryContent');
  const loadingIndicator = document.getElementById('loadingIndicator');
  const configToggle = document.getElementById('configToggle');
  const configPanel = document.getElementById('configPanel');
  const toggleMaxBtn = document.getElementById('toggleMaxBtn');
  const toggleMinBtn = document.getElementById('toggleMinBtn');
  const formatBtns = document.querySelectorAll('.format-btn');
  const copyBtn = document.getElementById('copyBtn');

  // 默认设置
  const DEFAULT_API_SERVICE = 'gptgod';
  const DEFAULT_CONFIGS = {
    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: ''
    }
  };
  const DEFAULT_FORMAT = 'markdown';

  // 从存储中恢复设置
  const savedConfigs = GM_getValue('apiConfigs', DEFAULT_CONFIGS);
  const savedApiService = GM_getValue('apiService', DEFAULT_API_SERVICE);
  const savedFormat = GM_getValue('outputFormat', DEFAULT_FORMAT);
  const savedConfigCollapsed = GM_getValue('configCollapsed', false);

  // 设置输入框的值
  const currentConfig = savedConfigs[savedApiService];
  if (currentConfig) {
    apiKeyInput.value = currentConfig.key;
    modelNameInput.value = currentConfig.model;
    if (savedApiService === 'custom') {
      customApiUrlInput.value = currentConfig.url;
      customApiUrlContainer.style.display = 'block';
    }
  }
  apiServiceSelect.value = savedApiService;

  // 设置输出格式按钮状态
  formatBtns.forEach(btn => {
    if (btn.dataset.format === savedFormat) {
      btn.classList.add('active');
    } else {
      btn.classList.remove('active');
    }
  });

  // 设置配置面板折叠状态
  if (savedConfigCollapsed) {
    configPanel.classList.add('collapsed');
    configToggle.querySelector('.toggle-icon').style.transform = 'rotate(-90deg)';
  }

  // 事件监听
  apiServiceSelect.addEventListener('change', function () {
    const service = this.value;
    const configs = GM_getValue('apiConfigs', DEFAULT_CONFIGS);
    const currentConfig = configs[service];

    // 更新输入框的值
    apiKeyInput.value = currentConfig.key;
    modelNameInput.value = currentConfig.model;

    if (service === 'custom') {
      customApiUrlInput.value = currentConfig.url;
      customApiUrlContainer.style.display = 'block';
    } else {
      customApiUrlContainer.style.display = 'none';
    }

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

  // 修改输入框的事件监听
  apiKeyInput.addEventListener('change', function () {
    const service = apiServiceSelect.value;
    const configs = GM_getValue('apiConfigs', DEFAULT_CONFIGS);
    configs[service].key = this.value;
    GM_setValue('apiConfigs', configs);
  });

  customApiUrlInput.addEventListener('change', function () {
    const service = apiServiceSelect.value;
    const configs = GM_getValue('apiConfigs', DEFAULT_CONFIGS);
    configs[service].url = this.value;
    GM_setValue('apiConfigs', configs);
  });

  modelNameInput.addEventListener('change', function () {
    const service = apiServiceSelect.value;
    const configs = GM_getValue('apiConfigs', DEFAULT_CONFIGS);
    configs[service].model = this.value;
    GM_setValue('apiConfigs', configs);
  });

  // 配置面板折叠/展开
  configToggle.addEventListener('click', function () {
    configPanel.classList.toggle('collapsed');
    const isCollapsed = configPanel.classList.contains('collapsed');
    const toggleIcon = this.querySelector('.toggle-icon');
    toggleIcon.style.transform = isCollapsed ? 'rotate(-90deg)' : '';
    GM_setValue('configCollapsed', isCollapsed);
  });

  // 拖拽相关变量声明
  let isDragging = false;
  let currentX;
  let currentY;
  let initialX;
  let initialY;

  const header = document.getElementById('summary-header');

  header.addEventListener('mousedown', dragStart);
  document.addEventListener('mousemove', drag);
  document.addEventListener('mouseup', dragEnd);

  function dragStart(e) {
    initialX = e.clientX - app.offsetLeft;
    initialY = e.clientY - app.offsetTop;
    isDragging = true;
  }

  function drag(e) {
    if (isDragging) {
      e.preventDefault();
      currentX = e.clientX - initialX;
      currentY = e.clientY - initialY;

      // 确保不超出屏幕边界
      currentX = Math.max(0, Math.min(currentX, window.innerWidth - app.offsetWidth));
      currentY = Math.max(0, Math.min(currentY, window.innerHeight - app.offsetHeight));

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

  function dragEnd() {
    isDragging = false;
    // 保存位置
    GM_setValue('appPosition', { x: currentX, y: currentY });
  }

  // 添加最大化/最小化功能
  let isMaximized = false;
  let previousSize = {};

  toggleMaxBtn.addEventListener('click', () => {
    if (!isMaximized) {
      // 保存当前大小和位置
      previousSize = {
        width: app.style.width,
        height: app.style.height,
        left: app.style.left,
        top: app.style.top
      };

      // 最大化
      app.style.width = '100%';
      app.style.height = '100vh';
      app.style.left = '0';
      app.style.top = '0';

      toggleMaxBtn.innerHTML = `
        <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>
      `;
    } else {
      // 恢复之前的大小和位置
      Object.assign(app.style, previousSize);

      toggleMaxBtn.innerHTML = `
        <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
          <path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"></path>
        </svg>
      `;
    }
    isMaximized = !isMaximized;
  });

  // 输出格式选择
  formatBtns.forEach(btn => {
    btn.addEventListener('click', function () {
      formatBtns.forEach(b => b.classList.remove('active'));
      this.classList.add('active');
      GM_setValue('outputFormat', this.dataset.format);
    });
  });

  // 复制按钮功能
  copyBtn.addEventListener('click', function () {
    const outputFormat = document.querySelector('.format-btn.active').dataset.format;
    let textToCopy;

    if (outputFormat === 'markdown') {
      // 获取原始的 Markdown 文本
      textToCopy = summaryContent.getAttribute('data-markdown') || summaryContent.textContent;
    } else {
      textToCopy = summaryContent.textContent;
    }

    navigator.clipboard.writeText(textToCopy).then(() => {
      const originalHTML = this.innerHTML;
      this.innerHTML = `
        <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
          <path d="M5 13l4 4L19 7"></path>
        </svg>
        已复制
      `;
      setTimeout(() => {
        this.innerHTML = originalHTML;
      }, 2000);
    }).catch(err => {
      console.error('复制失败:', err);
      alert('复制失败,请手动选择文本复制');
    });
  });

  // 修改 simpleMarkdownRender 函数
  function 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;
  }

  // 修改生成总结按钮的事件处理
  generateBtn.addEventListener('click', async function () {
    const apiService = apiServiceSelect.value;
    const configs = GM_getValue('apiConfigs', DEFAULT_CONFIGS);
    const currentConfig = configs[apiService];

    const apiKey = apiKeyInput.value.trim();
    const modelName = modelNameInput.value.trim();
    const customApiUrl = customApiUrlInput.value.trim();

    if (!apiKey) {
      alert('请输入有效的 API Key');
      return;
    }

    // 保存当前配置
    currentConfig.key = apiKey;
    currentConfig.model = modelName;
    if (apiService === 'custom') {
      currentConfig.url = customApiUrl;
    }
    GM_setValue('apiConfigs', configs);

    if (apiService === 'custom' && !customApiUrl) {
      alert('请输入自定义API地址');
      return;
    }

    // 显示加载指示器
    loadingIndicator.style.display = 'block';
    generateBtn.disabled = true;
    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>
      生成中...
    `;

    try {
      const content = await getArticleContent();
      if (!content || content.length < 10) {
        throw new Error('无法获取文章内容或内容太短');
      }

      const summary = await generateSummary(content, apiService, apiKey, customApiUrl, modelName, currentConfig.model);

      // 显示结果
      if (currentConfig.model === 'markdown') {
        // 保存原始 Markdown 文本用于复制
        summaryContent.setAttribute('data-markdown', summary);

        // 尝试使用 marked.js 渲染,如果不可用则使用简单渲染
        if (window.marked) {
          summaryContent.innerHTML = marked.parse(summary);
          // 如果有 highlight.js,应用代码高亮
          if (window.hljs) {
            document.querySelectorAll('#summaryContent pre code').forEach((block) => {
              hljs.highlightElement(block);
            });
          }
        } else {
          // 使用优化后的简单渲染
          summaryContent.innerHTML = simpleMarkdownRender(summary);
        }
      } else {
        summaryContent.innerHTML = simpleMarkdownRender(summary);
      }

      summaryResult.style.display = 'block';
    } catch (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);
    } finally {
      // 隐藏加载指示器
      loadingIndicator.style.display = 'none';
      generateBtn.disabled = false;
      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>
        生成总结
      `;
    }
  });

  // 获取文章内容
  async function getArticleContent() {
    // 常见的文章内容选择器
    const selectors = [
      // 微信公众号
      '#js_content',
      // 知乎
      '.RichText',
      // 简书
      '.article-content',
      // 掘金
      '.article-content',
      // CSDN
      '#article_content',
      // 博客园
      '#cnblogs_post_body',
      // 通用文章容器
      'article',
      '.article',
      '.post-content',
      '.content',
      '.entry-content',
      '.article-content',
      // 如果找不到特定容器,尝试获取主要内容区域
      'main',
      '#main',
      '.main'
    ];

    // 尝试使用不同的选择器获取内容
    for (const selector of selectors) {
      const element = document.querySelector(selector);
      if (element) {
        // 移除不需要的元素
        const clone = element.cloneNode(true);
        const 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'
        ];

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

        // 获取文本内容
        let content = clone.innerText.trim();

        // 清理文本
        content = content
          .replace(/\s+/g, ' ')  // 将多个空白字符替换为单个空格
          .replace(/\n\s*\n/g, '\n')  // 将多个空行替换为单个换行
          .trim();

        if (content.length > 100) {  // 确保内容足够长
          return content;
        }
      }
    }

    // 如果上述方法都失败,尝试获取整个页面的主要内容
    const body = document.body.cloneNode(true);
    const 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'  // 移除我们的应用界面
    ];

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

    let content = body.innerText.trim();

    // 清理文本
    content = content
      .replace(/\s+/g, ' ')  // 将多个空白字符替换为单个空格
      .replace(/\n\s*\n/g, '\n')  // 将多个空行替换为单个换行
      .trim();

    if (content.length < 100) {
      throw new Error('无法获取足够的文章内容');
    }

    return content;
  }

  // 生成总结
  async function generateSummary(content, apiService, apiKey, customApiUrl, modelName, outputFormat) {
    let apiEndpoint;

    if (apiService === 'deepseek') {
      apiEndpoint = 'https://api.deepseek.com/v1/chat/completions';
    } else if (apiService === 'gptgod') {
      apiEndpoint = 'https://api.gptgod.online/v1/chat/completions';
    } else {
      apiEndpoint = customApiUrl;
    }

    // 根据输出格式调整系统提示
    let systemPrompt;
    switch (outputFormat) {
      case 'markdown':
        systemPrompt = "请用中文总结以下文章的主要内容,以标准Markdown格式输出,包括标题、小标题和要点。确保格式规范,便于阅读。";
        break;
      case 'bullet':
        systemPrompt = "请用中文总结以下文章的主要内容,以简洁的要点列表形式输出,每个要点前使用'- '标记。";
        break;
      case 'paragraph':
        systemPrompt = "请用中文总结以下文章的主要内容,以连贯的段落形式输出,突出文章的核心观点和结论。";
        break;
      default:
        systemPrompt = "请用中文总结以下文章的主要内容,以简洁的方式列出重点。";
    }

    const messages = [
      {
        role: "system",
        content: systemPrompt
      },
      {
        role: "user",
        content: content
      }
    ];

    return new Promise((resolve, reject) => {
      GM_xmlhttpRequest({
        method: 'POST',
        url: apiEndpoint,
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${apiKey}`
        },
        data: JSON.stringify({
          model: modelName,
          messages: messages,
          stream: false
        }),
        onload: function (response) {
          try {
            console.log('API响应状态码:', response.status);
            console.log('API响应内容:', response.responseText.substring(0, 200) + '...');

            // 检查是否为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));
            } else if (data.choices && data.choices[0] && data.choices[0].message) {
              resolve(data.choices[0].message.content);
            } else {
              console.error('异常API响应结构:', data);
              reject(new Error('API 返回格式异常'));
            }
          } catch (error) {
            console.error('解析响应失败:', error, response.responseText.substring(0, 200));
            reject(new Error(`解析API响应失败: ${error.message}`));
          }
        },
        onerror: function (error) {
          console.error('请求错误:', error);
          reject(new Error('网络请求失败'));
        }
      });
    });
  }
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址