mikecoding / 知乎回答评论导出

// ==UserScript==
// @name         知乎回答评论导出
// @namespace    https://github.com/zhihu-comment-exporter
// @version      1.0.0
// @description  导出知乎回答内容、评论及子评论为 JSON/Markdown
// @copyright 2026, mikecoding (https://openuserjs.org/users/mikecoding)
// @match        https://www.zhihu.com/question/*
// @match        https://zhihu.com/question/*
// @grant        GM_addStyle
// @run-at       document-end
// @license MIT
// ==/UserScript==

(function () {
  'use strict';

  // ======================= 配置 =======================
  const CONFIG = {
    commentPageSize: 20,     // 根评论每页数量
    maxRootComments: 0,      // 最大根评论数(0 = 不限)
    maxChildComments: 0,     // 最大子评论数(0 = 不限)
    requestDelay: 300,       // API 请求间隔 (ms)
    expandRetryCount: 10,    // 展开内容后重试次数
    expandRetryDelay: 500,   // 展开内容后重试间隔 (ms)
  };

  // ======================= 样式 =======================
  GM_addStyle(`
    .zh-ce-panel {
      position: fixed;
      bottom: 24px;
      right: 24px;
      z-index: 99999;
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
      background: #fff;
      border-radius: 10px;
      box-shadow: 0 4px 24px rgba(0,0,0,0.15);
      overflow: hidden;
      min-width: 220px;
      transition: box-shadow 0.2s;
    }
    .zh-ce-panel:hover {
      box-shadow: 0 6px 32px rgba(0,0,0,0.22);
    }

    .zh-ce-header {
      display: flex;
      align-items: center;
      justify-content: space-between;
      padding: 10px 14px;
      background: linear-gradient(135deg, #0066ff, #0052cc);
      color: #fff;
      cursor: pointer;
      user-select: none;
      font-size: 13px;
      font-weight: 600;
      gap: 8px;
    }
    .zh-ce-header:hover { background: linear-gradient(135deg, #005ce6, #0047b3); }

    .zh-ce-arrow {
      display: inline-block;
      transition: transform 0.25s;
      font-size: 11px;
      flex-shrink: 0;
    }
    .zh-ce-panel.collapsed .zh-ce-arrow { transform: rotate(-90deg); }

    .zh-ce-body {
      display: flex;
      flex-direction: column;
      gap: 8px;
      padding: 12px 14px;
      max-height: 200px;
      overflow: hidden;
      transition: max-height 0.3s, padding 0.3s, opacity 0.3s;
    }
    .zh-ce-panel.collapsed .zh-ce-body {
      max-height: 0;
      padding-top: 0;
      padding-bottom: 0;
      opacity: 0;
      pointer-events: none;
    }

    .zh-ce-body button {
      width: 100%;
      padding: 9px 0;
      border: none;
      border-radius: 6px;
      font-size: 13px;
      font-weight: 500;
      cursor: pointer;
      color: #fff;
      transition: all 0.15s;
      display: flex;
      align-items: center;
      justify-content: center;
      gap: 6px;
      white-space: nowrap;
    }
    .zh-ce-body button:hover { filter: brightness(1.1); transform: scale(1.02); }
    .zh-ce-body button:active { transform: scale(0.98); }
    .zh-ce-body button:disabled { pointer-events: none; opacity: 0.5; transform: none; }

    .zh-ce-btn-json { background: linear-gradient(135deg, #0066ff, #0052cc); }
    .zh-ce-btn-md  { background: linear-gradient(135deg, #52c41a, #389e0d); }

    @keyframes zh-ce-spin {
      to { transform: rotate(360deg); }
    }
    .zh-ce-spinner {
      display: none;
      width: 12px; height: 12px;
      border: 2px solid rgba(255,255,255,0.3);
      border-top-color: #fff;
      border-radius: 50%;
      animation: zh-ce-spin 0.6s linear infinite;
    }
    button.zh-ce-loading .zh-ce-spinner { display: inline-block; }
    button.zh-ce-loading .zh-ce-label { display: none; }

    .zh-ce-toast {
      position: fixed; top: 20px; left: 50%;
      transform: translateX(-50%);
      z-index: 199999;
      padding: 10px 24px;
      border-radius: 6px;
      color: #fff;
      font-size: 14px;
      opacity: 0;
      transition: opacity 0.3s;
      pointer-events: none;
      max-width: 80vw;
      text-align: center;
    }
    .zh-ce-toast.show { opacity: 1; }
    .zh-ce-toast.success { background: #52c41a; }
    .zh-ce-toast.error { background: #ff4d4f; }
    .zh-ce-toast.info { background: #1890ff; }
  `);

  // ======================= 工具 =======================
  function toast(msg, type) {
    type = type || 'info';
    var existing = document.querySelector('.zh-ce-toast');
    if (existing) existing.remove();
    var el = document.createElement('div');
    el.className = 'zh-ce-toast ' + type;
    el.textContent = msg;
    document.body.appendChild(el);
    requestAnimationFrame(function () { el.classList.add('show'); });
    setTimeout(function () {
      el.classList.remove('show');
      setTimeout(function () { el.remove(); }, 300);
    }, 4000);
  }

  function sleep(ms) {
    return new Promise(function (r) { setTimeout(r, ms); });
  }

  function parseUrl() {
    var m = location.pathname.match(/\/question\/(\d+)(?:\/answer\/(\d+))?/);
    return m ? { questionId: m[1], answerId: m[2] || null } : null;
  }

  function stripHtml(html) {
    if (!html) return '';
    var div = document.createElement('div');
    div.innerHTML = html;
    return div.textContent || div.innerText || '';
  }

  // ======================= 答案提取 =======================

  /**
   * 展开所有折叠内容,并轮询等待 DOM 更新
   * 返回是否展开了任何内容
   */
  function autoExpandAndWait() {
    return new Promise(function (resolve) {
      var expandBtns = document.querySelectorAll(
        '.ContentItem-expandButton, .QuestionRichText-more'
      );
      var clicked = 0;

      expandBtns.forEach(function (btn) {
        try {
          // 只点 "阅读全文" 类的按钮,不要点 "收起"
          if (/收起|折叠|收起评论/.test(btn.textContent)) return;
          btn.click();
          clicked++;
        } catch (e) { /* ignore */ }
      });

      if (clicked === 0) {
        resolve(0);
        return;
      }

      console.log('[知乎导出] 展开了 ' + clicked + ' 个折叠内容,等待 DOM 更新...');

      // 轮询检查:等 DOM 内容长度不再增长了再继续
      var retries = 0;
      var lastLength = 0;

      function check() {
        var contentEls = document.querySelectorAll('.RichContent-inner, .RichContent');
        var currentLength = 0;
        contentEls.forEach(function (el) { currentLength += el.textContent.length; });

        retries++;

        if (currentLength === lastLength && currentLength > 0 && retries >= 3) {
          // 内容不再变化,认为加载完成
          console.log('[知乎导出] 内容稳定,总长度: ' + currentLength);
          resolve(clicked);
        } else if (retries >= CONFIG.expandRetryCount) {
          // 超时
          console.log('[知乎导出] 等待超时,总长度: ' + currentLength);
          resolve(clicked);
        } else {
          lastLength = currentLength;
          setTimeout(check, CONFIG.expandRetryDelay);
        }
      }

      // 首次等待后开始检查
      setTimeout(check, 600);
    });
  }

  /**
   * 从 SSR + DOM 提取所有答案
   */
  function extractAllAnswers() {
    var ssrScript = document.getElementById('js-initialData');
    var entities = {};

    if (ssrScript) {
      try {
        var ssrData = JSON.parse(ssrScript.textContent);
        entities = ssrData.initialState && ssrData.initialState.entities || {};
      } catch (e) { /* ignore */ }
    }

    var answerMap = {};

    // ---- 步骤 1: 从 SSR entities.answers 获取 ----
    if (entities.answers) {
      Object.keys(entities.answers).forEach(function (id) {
        var answer = entities.answers[id];
        if (!answer || answer.type !== 'answer') return;

        answerMap[id] = {
          id: id,
          author: {
            name: (answer.author && answer.author.name) || '未知',
            urlToken: (answer.author && answer.author.url_token) || '',
            headline: (answer.author && answer.author.headline) || '',
            avatarUrl: (answer.author && answer.author.avatar_url) || '',
          },
          content: answer.content || '',
          contentText: stripHtml(answer.content) || answer.excerpt || '',
          voteupCount: answer.voteupCount || 0,
          commentCount: answer.commentCount || 0,
          createdTime: answer.createdTime || 0,
          updatedTime: answer.updatedTime || 0,
          questionTitle: entities.questions && entities.questions[answer.question && answer.question.id] ? entities.questions[answer.question.id].title : '',
          url: answer.url || '',
          _source: 'ssr',
        };
      });
    }

    // ---- 步骤 2: 从 DOM .AnswerItem 获取(覆盖内容 + 补充滚动加载的答案) ----
    var cards = document.querySelectorAll('.AnswerItem');
    cards.forEach(function (card) {
      // 提取 answerId
      var dataZopEl = card.querySelector('[data-zop]');
      var dataZop = dataZopEl ? dataZopEl.getAttribute('data-zop') || '' : '';
      var answerLink = card.querySelector('a[href*="/answer/"]');
      var answerLinkHref = answerLink ? answerLink.getAttribute('href') || '' : '';

      var answerId =
        (dataZop.match(/answer-(\d+)/) || [])[1] ||
        (answerLinkHref.match(/\/answer\/(\d+)/) || [])[1] ||
        '';

      // 提取 DOM 内容
      var contentEl =
        card.querySelector('.RichContent-inner') ||
        card.querySelector('.RichContent');
      var domContent = contentEl ? contentEl.innerHTML : '';
      var domText = stripHtml(domContent);

      if (answerId && answerMap[answerId]) {
        // SSR 已有,用 DOM 内容覆盖(优先用展开后的完整内容)
        var existing = answerMap[answerId];
        if (domContent && domContent.length > existing.content.length) {
          existing.content = domContent;
          existing.contentText = domText;
        }
        // 用 DOM 修正 commentCount
        var btns = card.querySelectorAll('button');
        for (var i = 0; i < btns.length; i++) {
          if (/条评论/.test(btns[i].textContent)) {
            var domCount = parseInt(btns[i].textContent.replace(/[^\d]/g, '') || '0');
            if (domCount > 0) existing.commentCount = domCount;
            break;
          }
        }
        existing._source = 'ssr+dom';
      } else if (answerId) {
        // 只在 DOM 中(通过滚动加载的答案)
        var authorEl = card.querySelector(
          '.AuthorInfo-name a, .UserLink-link, .AnswerAuthor-user-name a'
        );
        var headlineEl = card.querySelector(
          '.AuthorInfo-headline, .AnswerAuthor-user-headline span'
        );
        var voteupBtn = card.querySelector('button[aria-label*="赞同"], .VoteButton');
        var commentBtn = null;
        var allBtns = card.querySelectorAll('button');
        for (var j = 0; j < allBtns.length; j++) {
          if (/条评论/.test(allBtns[j].textContent)) {
            commentBtn = allBtns[j];
            break;
          }
        }

        answerMap[answerId] = {
          id: answerId,
          author: {
            name: (authorEl && authorEl.textContent && authorEl.textContent.trim()) || '未知',
            urlToken: '',
            headline: (headlineEl && headlineEl.textContent && headlineEl.textContent.trim()) || '',
            avatarUrl: '',
          },
          content: domContent,
          contentText: domText,
          voteupCount: parseInt((voteupBtn && voteupBtn.textContent || '').replace(/[^\d]/g, '') || '0'),
          commentCount: parseInt((commentBtn && commentBtn.textContent || '').replace(/[^\d]/g, '') || '0'),
          createdTime: 0,
          updatedTime: 0,
          questionTitle:
            (entities.questions && Object.keys(entities.questions || {})[0] && entities.questions[Object.keys(entities.questions)[0]].title) ||
            (function () { var t = document.querySelector('.QuestionHeader-title'); return t ? t.textContent.trim() : ''; })() ||
            '',
          url: 'https://www.zhihu.com/question/' + (parseUrl() || {}).questionId + '/answer/' + answerId,
          _source: 'dom-only',
        };
      }
    });

    var result = [];
    Object.keys(answerMap).forEach(function (k) { result.push(answerMap[k]); });
    return result;
  }

  // ======================= 评论 API =======================

  function normalizeComment(c) {
    if (!c) return null;
    return {
      id: c.id || '',
      content: stripHtml(c.content || ''),
      contentHtml: c.content || '',
      author: {
        name: (c.author && c.author.name) || '匿名用户',
        urlToken: (c.author && c.author.url_token) || '',
        headline: (c.author && c.author.headline) || '',
        avatarUrl: (c.author && c.author.avatar_url) || '',
        isAuthor: !!(c.is_author || (c.author_tag || []).some(function (t) { return t.type === 'content_author'; })),
      },
      createdTime: c.created_time || 0,
      createdTimeStr: c.created_time
        ? new Date(c.created_time * 1000).toLocaleString('zh-CN')
        : '',
      likeCount: c.like_count || 0,
      dislikeCount: c.dislike_count || 0,
      isCollapsed: c.collapsed || false,
      isAuthorTop: c.is_author_top || false,
      childCommentCount: c.child_comment_count || 0,
      childComments: (c.child_comments || []).map(function (cc) { return normalizeComment(cc); }).filter(Boolean),
      childCommentNextOffset: c.child_comment_next_offset || null,
      replyToCommentId: c.reply_comment_id || '0',
      replyToRootCommentId: c.reply_root_comment_id || '',
      location: ((c.comment_tag || []).find(function (t) { return t.type === 'ip_info'; }) || {}).text || '',
      tags: (c.author_tag || []).map(function (t) { return t.text; }),
    };
  }

  function fetchRootComments(answerId) {
    return new Promise(function (resolve) {
      var all = [];
      var nextUrl = 'https://www.zhihu.com/api/v4/comment_v5/answers/' + answerId +
        '/root_comment?order_by=score&limit=' + CONFIG.commentPageSize;

      function fetchNext() {
        if (!nextUrl || all.length >= (CONFIG.maxRootComments || Infinity)) {
          if (CONFIG.maxRootComments > 0) all.length = Math.min(all.length, CONFIG.maxRootComments);
          resolve(all);
          return;
        }

        fetch(nextUrl)
          .then(function (resp) {
            if (!resp.ok) { resolve(all); return; }
            return resp.json();
          })
          .then(function (data) {
            if (!data || !data.data) { resolve(all); return; }

            var comments = data.data.map(normalizeComment);
            all = all.concat(comments);

            console.log('[知乎导出] 根评论: ' + all.length + ' / ' + (data.counts ? data.counts.total_counts : '?'));

            if (data.paging && !data.paging.is_end && data.paging.next) {
              nextUrl = data.paging.next;
              setTimeout(fetchNext, CONFIG.requestDelay);
            } else {
              if (CONFIG.maxRootComments > 0) all.length = Math.min(all.length, CONFIG.maxRootComments);
              resolve(all);
            }
          })
          .catch(function (e) {
            console.error('[知乎导出] 根评论网络错误:', e);
            resolve(all);
          });
      }

      fetchNext();
    });
  }

  function fetchChildComments(answerId, parentComment) {
    return new Promise(function (resolve) {
      if (parentComment.childCommentCount === 0) { resolve([]); return; }

      var children = (parentComment.childComments || []).slice();

      if (children.length >= parentComment.childCommentCount) {
        resolve(children.map(function (c) { return normalizeComment(c); }).filter(Boolean));
        return;
      }

      var nextOffset = parentComment.childCommentNextOffset;

      function fetchNextOffset() {
        if (!nextOffset || children.length >= parentComment.childCommentCount) {
          if (CONFIG.maxChildComments > 0) children.length = Math.min(children.length, CONFIG.maxChildComments);
          resolve(children);
          return;
        }

        var url = 'https://www.zhihu.com/api/v4/comment_v5/answers/' + answerId +
          '/root_comment?order_by=score&limit=1&child_comment_offset=' + encodeURIComponent(nextOffset);

        fetch(url)
          .then(function (resp) {
            if (!resp.ok) { resolve(children); return; }
            return resp.json();
          })
          .then(function (data) {
            if (!data || !data.data || !data.data[0] || !data.data[0].child_comments) {
              resolve(children);
              return;
            }
            var more = data.data[0].child_comments.map(normalizeComment).filter(Boolean);
            children = children.concat(more);
            nextOffset = data.data[0].child_comment_next_offset;

            if (CONFIG.maxChildComments > 0 && children.length >= CONFIG.maxChildComments) {
              children.length = CONFIG.maxChildComments;
              resolve(children);
              return;
            }

            setTimeout(fetchNextOffset, CONFIG.requestDelay);
          })
          .catch(function (e) {
            console.error('[知乎导出] 子评论网络错误:', e);
            resolve(children);
          });
      }

      fetchNextOffset();
    });
  }

  // ======================= 导出主流程 =======================

  function exportData(format) {
    format = format || 'json';
    var parsed = parseUrl();
    if (!parsed) {
      toast('无法解析 URL,请确保在知乎问题/回答页面', 'error');
      return;
    }

    var questionId = parsed.questionId;
    var answerId = parsed.answerId;

    // 设置按钮加载状态
    var btns = document.querySelectorAll('.zh-ce-panel .zh-ce-body button');
    btns.forEach(function (b) {
      b.disabled = true;
      b.classList.add('zh-ce-loading');
    });

    // 异步主流程
    var run = async function () {
      try {
        // 0. 展开折叠内容并等待 DOM 更新
        toast('展开折叠内容...', 'info');
        var expandedCount = await autoExpandAndWait();
        if (expandedCount > 0) {
          console.log('[知乎导出] 折叠内容已展开');
        }

        // 1. 提取答案
        toast('提取答案信息...', 'info');
        var answers = extractAllAnswers();

        // 去重(按 id)
        var seen = {};
        answers = answers.filter(function (a) {
          if (seen[a.id]) return false;
          seen[a.id] = true;
          return true;
        });

        // 筛选目标答案
        var targetAnswers = answers;
        if (answerId) {
          targetAnswers = answers.filter(function (a) { return a.id === answerId; });
          if (targetAnswers.length === 0 && answers.length > 0) {
            targetAnswers = [answers[0]];
          }
        }

        console.log('[知乎导出] 共 ' + answers.length + ' 个答案,目标 ' + targetAnswers.length + ' 个');

        if (targetAnswers.length === 0) {
          toast('未找到任何回答,请刷新页面后重试', 'error');
          return;
        }

        // 2. 逐个答案获取评论
        var results = [];
        for (var i = 0; i < targetAnswers.length; i++) {
          var answer = targetAnswers[i];
          var aid = answer.id;

          if (!aid) {
            results.push({
              id: answer.id, author: answer.author, content: answer.content,
              contentText: answer.contentText, voteupCount: answer.voteupCount,
              commentCount: answer.commentCount, createdTime: answer.createdTime,
              updatedTime: answer.updatedTime, questionTitle: answer.questionTitle,
              url: answer.url, comments: [], totalRootComments: 0, totalChildComments: 0,
            });
            continue;
          }

          toast('获取评论 ' + (i + 1) + '/' + targetAnswers.length + ' (' + answer.author.name + ')...', 'info');

          var rootComments = await fetchRootComments(aid);

          var totalChildren = 0;
          for (var j = 0; j < rootComments.length; j++) {
            var comment = rootComments[j];
            if (comment.childCommentCount === 0) continue;
            var children = await fetchChildComments(aid, comment);
            comment.childComments = children;
            totalChildren += children.length;
          }

          console.log('[知乎导出] ' + answer.author.name + ': ' + rootComments.length + ' 评论 + ' + totalChildren + ' 子评论');

          results.push({
            id: answer.id,
            author: answer.author,
            content: answer.content,
            contentText: answer.contentText,
            voteupCount: answer.voteupCount,
            commentCount: answer.commentCount,
            createdTime: answer.createdTime,
            updatedTime: answer.updatedTime,
            questionTitle: answer.questionTitle,
            url: answer.url,
            comments: rootComments,
            totalRootComments: rootComments.length,
            totalChildComments: totalChildren,
          });
        }

        // 3. 构建导出对象
        var exportObj = {
          exportedAt: new Date().toISOString(),
          url: location.href,
          questionId: questionId,
          questionTitle: results[0].questionTitle || (function () {
            var t = document.querySelector('.QuestionHeader-title');
            return t ? t.textContent.trim() : '';
          })() || '',
          totalAnswers: results.length,
          answers: results,
        };

        // 4. 生成文件
        var filename, content, mimeType;
        var titleSlug = (exportObj.questionTitle || 'question-' + questionId)
          .replace(/[\\/:*?"<>|]/g, '')
          .substring(0, 50);

        if (format === 'markdown') {
          filename = titleSlug + (answerId ? '-' + answerId : '') + '.md';
          content = generateMarkdown(exportObj);
          mimeType = 'text/markdown;charset=utf-8';
        } else {
          filename = titleSlug + (answerId ? '-' + answerId : '') + '.json';
          content = JSON.stringify(exportObj, null, 2);
          mimeType = 'application/json;charset=utf-8';
        }

        // 5. 下载 - 直接触发,不使用 GM_download(blob URL 在沙箱中不可靠)
        downloadFile(filename, content, mimeType);

        // 6. 显示统计
        var stats = results.map(function (r) {
          return '「' + (r.author.name || '未知') + '」: ' + r.totalRootComments + '评+' + r.totalChildComments + '子评';
        });
        toast('✅ 导出完成! ' + stats.join(' | '), 'success');

        console.log('[知乎导出] 完成:', exportObj.questionTitle, results.length + ' 个答案');
      } catch (e) {
        console.error('[知乎导出] 错误:', e);
        toast('导出失败: ' + e.message, 'error');
      } finally {
        btns.forEach(function (b) {
          b.disabled = false;
          b.classList.remove('zh-ce-loading');
        });
      }
    };

    run();
  }

  // ======================= Markdown 生成 =======================

  function generateMarkdown(data) {
    var lines = [];

    lines.push('# ' + data.questionTitle);
    lines.push('');
    lines.push('> 📅 导出时间: ' + data.exportedAt);
    lines.push('> 🔗 原链接: ' + data.url);
    lines.push('');

    data.answers.forEach(function (answer) {
      lines.push('---');
      lines.push('');
      lines.push('## ' + answer.author.name);
      if (answer.author.headline) {
        lines.push('*' + answer.author.headline + '*');
      }
      lines.push('');
      lines.push('👍 ' + answer.voteupCount + ' 赞同 | 💬 ' + answer.totalRootComments + ' 评论 | ' + answer.totalChildComments + ' 子评论');
      lines.push('');

      // === 回答正文 ===
      lines.push('### 📝 回答内容');
      lines.push('');
      var contentText = answer.contentText || stripHtml(answer.content);
      if (contentText) {
        contentText.split('\n').forEach(function (line) {
          lines.push(line.trim() || '');
        });
      } else {
        lines.push('*(未获取到回答内容)*');
      }
      lines.push('');

      // === 评论 ===
      if (answer.comments.length > 0) {
        lines.push('### 💬 评论 (' + answer.totalRootComments + ' 条)');
        lines.push('');

        answer.comments.forEach(function (comment) {
          var time = comment.createdTimeStr || '';
          var badge = comment.author.isAuthor ? ' 🅰️' : '';
          var loc = comment.location ? ' 📍' + comment.location : '';
          var topBadge = comment.isAuthorTop ? ' 📌置顶' : '';

          lines.push('**' + comment.author.name + '**' + badge + topBadge);
          lines.push('> ' + comment.content.replace(/\n/g, '\n> '));
          lines.push('> 👍' + comment.likeCount + loc + ' · ' + time);
          lines.push('');

          // 子评论
          if (comment.childComments && comment.childComments.length > 0) {
            comment.childComments.forEach(function (child) {
              var childTime = child.createdTimeStr || '';
              var childBadge = child.author.isAuthor ? ' 🅰️' : '';
              var childLoc = child.location ? ' 📍' + child.location : '';

              lines.push('  **' + child.author.name + '**' + childBadge);
              lines.push('  > ' + (child.content || '').replace(/\n/g, '\n  > '));
              lines.push('  > 👍' + child.likeCount + childLoc + ' · ' + childTime);
              lines.push('');
            });
          }
        });
      } else {
        lines.push('*暂无评论*');
        lines.push('');
      }
    });

    return lines.join('\n');
  }

  // ======================= 下载(可靠方案) =======================

  function downloadFile(filename, content, mimeType) {
    // 直接用 a.click() 触发下载。CSP 不拦截 <a> 点击触发的浏览器下载行为。
    // 不用 script 注入(知乎 CSP 禁止内联脚本),不用 GM_download(blob URL 可能被沙箱拦截)。
    var blob = new Blob([content], { type: mimeType });
    var url = URL.createObjectURL(blob);
    var a = document.createElement('a');
    a.href = url;
    a.download = filename;
    a.style.display = 'none';
    document.body.appendChild(a);
    a.click();
    // 延迟清理,确保浏览器有足够时间开始下载
    setTimeout(function () {
      document.body.removeChild(a);
      URL.revokeObjectURL(url);
    }, 60000);

    console.log('[知乎导出] 下载已触发: ' + filename + ' (' + (content.length / 1024).toFixed(1) + ' KB)');
  }

  // ======================= UI =======================

  function createUI() {
    // 避免重复创建
    if (document.querySelector('.zh-ce-panel')) return;

    var panel = document.createElement('div');
    panel.className = 'zh-ce-panel';

    // ---- 标题栏 ----
    var header = document.createElement('div');
    header.className = 'zh-ce-header';
    header.innerHTML = '<span>📋 知乎回答·评论导出</span><span class="zh-ce-arrow">▼</span>';
    header.title = '点击展开/收起';
    header.addEventListener('click', function () {
      panel.classList.toggle('collapsed');
    });

    // ---- 按钮区 ----
    var body = document.createElement('div');
    body.className = 'zh-ce-body';

    var jsonBtn = document.createElement('button');
    jsonBtn.className = 'zh-ce-btn-json';
    jsonBtn.innerHTML = '<span class="zh-ce-spinner"></span><span class="zh-ce-label">📥 导出 JSON</span>';
    jsonBtn.title = '导出 JSON(完整结构化数据)';
    jsonBtn.addEventListener('click', function (e) {
      e.stopPropagation();
      exportData('json');
    });

    var mdBtn = document.createElement('button');
    mdBtn.className = 'zh-ce-btn-md';
    mdBtn.innerHTML = '<span class="zh-ce-spinner"></span><span class="zh-ce-label">📝 导出 Markdown</span>';
    mdBtn.title = '导出 Markdown(方便阅读)';
    mdBtn.addEventListener('click', function (e) {
      e.stopPropagation();
      exportData('markdown');
    });

    body.appendChild(jsonBtn);
    body.appendChild(mdBtn);
    panel.appendChild(header);
    panel.appendChild(body);
    document.body.appendChild(panel);

    console.log('[知乎导出] 面板已加载 ✓');
  }

  // ======================= 入口 =======================

  function init() {
    if (!/\/question\/\d+/.test(location.pathname)) return;

    if (document.readyState === 'loading') {
      document.addEventListener('DOMContentLoaded', function () {
        setTimeout(createUI, 1500);
      });
    } else {
      setTimeout(createUI, 1500);
    }
  }

  init();

  // SPA 导航监听(知乎是 React SPA)
  var lastPath = location.pathname;
  var observer = new MutationObserver(function () {
    if (location.pathname !== lastPath) {
      lastPath = location.pathname;
      if (/\/question\/\d+/.test(location.pathname)) {
        setTimeout(createUI, 1500);
      }
    }
  });
  observer.observe(document.body, { childList: true, subtree: true });
})();