Tieba Post Backup Tool

Automatically backup Tieba posts in one single click

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey, Greasemonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

You will need to install an extension such as Tampermonkey to install this script.

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Tieba Post Backup Tool
// @namespace    https://github.com/ZXPrism/TiebaPostBackupTool
// @version      2.1.0
// @description  Automatically backup Tieba posts in one single click
// @author       ZXPrism
// @license      MIT
// @match        https://tieba.baidu.com/p/*
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @connect      gss0.bdstatic.com
// @connect      gss1.bdstatic.com
// @connect      gss2.bdstatic.com
// @connect      gss3.bdstatic.com
// @connect      gss4.bdstatic.com
// @connect      gsp0.baidu.com
// @connect      himg.bdimg.com
// @connect      tb0.bdstatic.com
// @connect      tb1.bdstatic.com
// @connect      tb2.bdstatic.com
// @connect      tiebapic.baidu.com
// @connect      imgsa.baidu.com
// @connect      static.tieba.baidu.com
// @connect      tieba.baidu.com
// @run-at       document-idle
// ==/UserScript==
"use strict";
(() => {
  // src/image_handler.ts
  var ImageHandler = class {
    constructor() {
      this.image_map = {};
      this.image_counter = 1;
    }
    /**
     * Extract all image URLs from post content and avatars.
     * Returns mapped image URLs.
     */
    process_images(post_html, avatar_urls) {
      this.image_map = {};
      this.image_counter = 1;
      const img_tags = post_html.match(/<img[^>]+>/g) || [];
      img_tags.forEach((img_tag) => {
        const src_match = img_tag.match(/src=["']([^"']+)["']/);
        if (src_match) {
          this._add_to_image_map(src_match[1]);
        }
      });
      avatar_urls.forEach((url) => {
        if (url) {
          this._add_to_image_map(url);
        }
      });
      return this.image_map;
    }
    /**
     * Add image URL to map with deduplication.
     */
    _add_to_image_map(original_url) {
      if (!this.image_map[original_url]) {
        const ext = this._get_extension(original_url);
        const new_name = `image_${this.image_counter++}.${ext}`;
        this.image_map[original_url] = new_name;
      }
    }
    /**
     * Get file extension from URL.
     */
    _get_extension(url) {
      if (url.includes("static.tieba.baidu.com")) {
        return "png";
      }
      const match = url.match(/\.([a-z]{3,4})(?:\?|$)/i);
      return match ? match[1] : "jpg";
    }
    /**
     * Replace image URLs in HTML content with new names.
     */
    replace_image_urls(html_content) {
      return html_content.replace(/src=["']([^"']+)["']/g, (match, url) => {
        const new_name = this.image_map[url];
        if (new_name) {
          return `src="./images/${new_name}"`;
        }
        return match;
      });
    }
    /**
     * Download all images as blobs with batching to prevent IP ban.
     * @param on_progress Callback for progress updates (current, total, filename)
     */
    async download_images(on_progress) {
      const blobs = {};
      const entries = Object.entries(this.image_map);
      const total = entries.length;
      let current = 0;
      const BATCH_SIZE = 5;
      const BATCH_DELAY = 1e3;
      for (let i = 0; i < entries.length; i += BATCH_SIZE) {
        const batch = entries.slice(i, Math.min(i + BATCH_SIZE, entries.length));
        await Promise.all(batch.map(async ([original_url, new_name]) => {
          try {
            const blob = await this._fetch_image(original_url);
            blobs[new_name] = blob;
            current++;
            if (on_progress) {
              on_progress(current, total, new_name);
            }
          } catch {
            blobs[new_name] = this._create_placeholder(new_name);
            current++;
            if (on_progress) {
              on_progress(current, total, new_name);
            }
          }
        }));
        if (i + BATCH_SIZE < entries.length) {
          await new Promise((resolve) => setTimeout(resolve, BATCH_DELAY));
        }
      }
      return blobs;
    }
    /**
     * Fetch single image as blob using GM_xmlhttpRequest to bypass CORS.
     */
    async _fetch_image(url) {
      return new Promise((resolve, reject) => {
        if (typeof GM_xmlhttpRequest !== "undefined") {
          GM_xmlhttpRequest({
            method: "GET",
            url,
            responseType: "blob",
            onload: (response) => {
              if (response.status === 200) {
                resolve(response.response);
              } else {
                reject(new Error(`HTTP ${response.status}`));
              }
            },
            onerror: (_error) => {
              reject(new Error("GM_xmlhttpRequest error"));
            }
          });
        } else {
          fetch(url).then((response) => {
            if (!response.ok) {
              throw new Error(`HTTP ${response.status}`);
            }
            return response.blob();
          }).then((blob) => resolve(blob)).catch((error) => reject(error));
        }
      });
    }
    /**
     * Create placeholder image for failed downloads.
     */
    _create_placeholder(_filename) {
      const svg = `
            <svg width="100" height="100" xmlns="http://www.w3.org/2000/svg">
                <rect width="100" height="100" fill="#ccc"/>
                <text x="50" y="50" text-anchor="middle" dy=".3em" font-size="12">Failed</text>
            </svg>
        `;
      return new Blob([svg], { type: "image/svg+xml" });
    }
    /**
     * Replace avatar URLs in floor list with mapped filenames.
     */
    replace_avatar_urls(floor_list) {
      floor_list.forEach((floor) => {
        if (floor.comment.avatar_url && this.image_map[floor.comment.avatar_url]) {
          floor.comment.avatar_url = this.image_map[floor.comment.avatar_url];
        }
        if (floor.sub_comment_list) {
          floor.sub_comment_list.forEach((sub) => {
            if (sub.avatar_url && this.image_map[sub.avatar_url]) {
              sub.avatar_url = this.image_map[sub.avatar_url];
            }
          });
        }
      });
    }
    /**
     * Create viewer HTML file.
     */
    create_viewer_html(post) {
      const json_data = JSON.stringify(post, null, 2);
      return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>${post.title} - Tieba Backup</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
            background: #f5f5f5;
        }
        .header {
            background: white;
            padding: 20px;
            border-radius: 8px;
            margin-bottom: 20px;
        }
        .header h1 {
            margin: 0 0 10px 0;
            color: #333;
        }
        .header .meta {
            color: #666;
            font-size: 14px;
        }
        .floor {
            background: white;
            padding: 15px;
            border-radius: 8px;
            margin-bottom: 15px;
        }
        .floor .author {
            font-weight: bold;
            color: #0055cc;
        }
        .floor .date {
            color: #999;
            font-size: 12px;
            margin-left: 10px;
        }
        .floor .ip {
            color: #999;
            font-size: 12px;
            margin-left: 10px;
        }
        .floor .content {
            margin: 10px 0;
            line-height: 1.6;
        }
        .floor .content img {
            max-width: 100%;
            height: auto;
        }
        .floor .avatar {
            width: 50px;
            height: 50px;
            border-radius: 4px;
            margin-right: 10px;
            vertical-align: middle;
            object-fit: cover;
        }
        .floor .avatar[alt] {
            display: inline-block;
        }
        .floor .floor-index {
            color: #999;
            font-size: 12px;
            margin-bottom: 5px;
        }
        .floor .author-line {
            display: flex;
            align-items: center;
            margin-bottom: 10px;
        }
        .sub-comments {
            margin-left: 20px;
            padding-left: 15px;
            border-left: 2px solid #e0e0e0;
            margin-top: 10px;
        }
        .sub-comment {
            margin: 8px 0;
            padding: 8px;
            background: #f9f9f9;
            border-radius: 4px;
            display: flex;
            align-items: center;
            gap: 8px;
        }
        .sub-comment .content {
            flex: 1;
        }
        .sub-comment .author {
            font-size: 12px;
        }
        .sub-comment .date {
            font-size: 11px;
        }
        .sub-comment .sub-avatar {
            width: 30px;
            height: 30px;
            border-radius: 3px;
            margin-right: 8px;
            vertical-align: middle;
            object-fit: cover;
        }
        .author-link {
            color: #0055cc;
            text-decoration: none;
            font-weight: bold;
        }
        .author-link:hover {
            text-decoration: underline;
        }
        .reply-to {
            color: #666;
            font-size: 12px;
        }
        .reply-to a {
            color: #0055cc;
            text-decoration: none;
        }
        .reply-to a:hover {
            text-decoration: underline;
        }
    </style>
</head>
<body>
    <div class="header">
        <h1>${post.title}</h1>
        <div class="meta">
            \u8D34\u5427: <a class="author-link" href="${post.tieba_url}" target="_blank">${post.tieba_name}</a> |
            \u697C\u5C42: ${post.floor_list.length} |
            \u56DE\u590D: ${post.comment_cnt}
        </div>
    </div>
    <div id="content"></div>

    <script>
        try {
            const post = ${json_data};

            function renderFloor(floor) {
            let html = '<div class="floor">';

            // Floor index
            html += '<div class="floor-index">' + floor.index + '\u697C</div>';

            // Author with avatar
            html += '<div class="author-line">';
            if (floor.comment.avatar_url) {
                html += '<img class="avatar" src="./images/' + floor.comment.avatar_url + '" alt="">';
            }
            // Author as link if homepage exists
            if (floor.comment.homepage_url) {
                html += '<a class="author-link" href="' + floor.comment.homepage_url + '" target="_blank">' + floor.comment.author + '</a>';
            } else {
                html += '<span class="author">' + floor.comment.author + '</span>';
            }
            html += '<span class="date">' + floor.comment.date + '</span>';
            if (floor.comment.ip_location) {
                html += '<span class="ip">\u6765\u81EA: ' + floor.comment.ip_location + '</span>';
            }
            html += '</div>';

            // Content
            html += '<div class="content">' + floor.comment.content + '</div>';

            // Sub-comments
            if (floor.sub_comment_list && floor.sub_comment_list.length > 0) {
                html += '<div class="sub-comments">';
                floor.sub_comment_list.forEach(sub => {
                    html += renderSubComment(sub);
                });
                html += '</div>';
            }

            html += '</div>';
            return html;
        }

        function renderSubComment(comment) {
            let html = '<div class="sub-comment">';

            // Avatar
            if (comment.avatar_url) {
                html += '<img class="sub-avatar" src="./images/' + comment.avatar_url + '" alt="">';
            }

            // Debug logging
            if (!comment.author || comment.author === 'Unknown') {
            }

            // Author with fallback for missing names, as link if homepage exists
            const author_name = comment.author || '\u672A\u77E5\u7528\u6237';
            if (comment.homepage_url) {
                html += '<a class="author-link" href="' + comment.homepage_url + '" target="_blank">' + author_name + '</a>';
            } else {
                html += '<span class="author">' + author_name + '</span>';
            }
            html += '<span class="date">' + comment.date + '</span>';

            // Content with reply-to filtering
            let content = comment.content;
            // Remove the original "\u56DE\u590D <a>username</a>" pattern since we'll render it separately
            // Use string methods instead of regex to avoid escaping issues
            const replyIndex = content.indexOf('\u56DE\u590D');
            if (replyIndex !== -1) {
                // Find the closing </a> and the colon after it
                const anchorEnd = content.indexOf('</a>', replyIndex);
                if (anchorEnd !== -1) {
                    const colonIndex = content.indexOf(':', anchorEnd);
                    if (colonIndex !== -1 && colonIndex < anchorEnd + 10) {
                        // Remove everything from "\u56DE\u590D" to the colon
                        content = content.substring(0, replyIndex) + content.substring(colonIndex + 1);
                    }
                }
            }
            html += '<div class="content">' + content + '</div>';

            // Reply-to information
            if (comment.reply_to_author) {
                html += '<div class="reply-to">\u56DE\u590D ';
                if (comment.reply_to_homepage_url) {
                    html += '<a href="' + comment.reply_to_homepage_url + '" target="_blank">' + comment.reply_to_author + '</a>';
                } else {
                    html += comment.reply_to_author;
                }
                html += '</div>';
            }

            html += '</div>';
            return html;
        }

        // Render all floors
        const contentDiv = document.getElementById('content');
        if (!contentDiv) {
        } else {
            post.floor_list.forEach((floor, index) => {
                try {
                    contentDiv.innerHTML += renderFloor(floor);
                } catch (e) {
                }
            });
        }
        } catch (error) {
            document.getElementById('content').innerHTML = '<p style="color: red; padding: 20px;">Error rendering post: ' + error.message + '</p>';
        }
    </script>
</body>
</html>`;
    }
    /**
     * Create tar archive from files.
     */
    async create_tar(files) {
      const chunks = [];
      const encoder = new TextEncoder();
      for (const [filename, content] of Object.entries(files)) {
        let file_data;
        let file_size;
        if (typeof content === "string") {
          file_data = encoder.encode(content);
          file_size = file_data.length;
        } else {
          file_data = await this._blob_to_uint8array(content);
          file_size = file_data.length;
        }
        const header = this._create_tar_header(filename, file_size);
        chunks.push(header);
        chunks.push(file_data);
        const padding_needed = (512 - file_size % 512) % 512;
        if (padding_needed > 0) {
          chunks.push(new Uint8Array(padding_needed));
        }
      }
      chunks.push(new Uint8Array(1024));
      const total_size = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
      const combined = new Uint8Array(total_size);
      let offset = 0;
      for (const chunk of chunks) {
        combined.set(chunk, offset);
        offset += chunk.length;
      }
      return new Blob([combined], { type: "application/x-tar" });
    }
    /**
     * Convert blob to Uint8Array.
     */
    async _blob_to_uint8array(blob) {
      const buffer = await blob.arrayBuffer();
      return new Uint8Array(buffer);
    }
    /**
     * Create tar header for a file.
     */
    _create_tar_header(filename, file_size) {
      const header = new Uint8Array(512);
      const encoder = new TextEncoder();
      const name_bytes = encoder.encode(filename);
      header.set(name_bytes.subarray(0, Math.min(100, name_bytes.length)), 0);
      encoder.encode("0000644 ").forEach((byte, i) => header[100 + i] = byte);
      encoder.encode("0000000 ").forEach((byte, i) => header[108 + i] = byte);
      encoder.encode("0000000 ").forEach((byte, i) => header[116 + i] = byte);
      const size_octal = file_size.toString(8).padStart(11, "0") + "\0";
      encoder.encode(size_octal).forEach((byte, i) => header[124 + i] = byte);
      const timestamp = Math.floor(Date.now() / 1e3).toString(8).padStart(11, "0") + "\0";
      encoder.encode(timestamp).forEach((byte, i) => header[136 + i] = byte);
      header[156] = 48;
      encoder.encode("        ").forEach((byte, i) => header[148 + i] = byte);
      let checksum = 0;
      for (let i = 0; i < 512; i++) {
        checksum += header[i];
      }
      const checksum_str = checksum.toString(8).padStart(6, "0") + "\0 ";
      encoder.encode(checksum_str).forEach((byte, i) => header[148 + i] = byte);
      return header;
    }
    /**
     * Download blob as file.
     */
    download_blob(blob, filename) {
      const url = URL.createObjectURL(blob);
      const a = document.createElement("a");
      a.href = url;
      a.download = filename;
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      URL.revokeObjectURL(url);
    }
  };

  // src/parser.ts
  var Parser = class {
    constructor() {
      this.image_handler = new ImageHandler();
      this.base_url = window.location.href.split("?")[0];
      this.current_page = this._get_current_page_number();
    }
    /**
     * Get current page number from URL query parameter.
     */
    _get_current_page_number() {
      const url_params = new URLSearchParams(window.location.search);
      const pn = url_params.get("pn");
      return pn ? parseInt(pn) : 1;
    }
    /**
     * Check if reset mode is active (don't auto-save state).
     */
    is_reset_mode() {
      return GM_getValue("reset_mode", false);
    }
    /**
     * Reset parsing state (clear all stored data).
     */
    reset_parsing() {
      GM_deleteValue("parse_state");
    }
    /**
     * Save parsing state to GM storage (respects reset mode).
     */
    _save_state(state) {
      if (!this.is_reset_mode()) {
        GM_setValue("parse_state", state);
      }
    }
    /**
     * Load parsing state from GM storage.
     */
    _load_state() {
      const state = GM_getValue("parse_state", void 0);
      return state || null;
    }
    /**
     * Clear parsing state from GM storage.
     */
    _clear_state() {
      GM_deleteValue("parse_state");
    }
    /**
     * Check if state is stale (older than 30 minutes).
     */
    _is_state_stale(state) {
      const THIRTY_MINUTES = 30 * 60 * 1e3;
      return Date.now() - state.timestamp > THIRTY_MINUTES;
    }
    /**
     * Check if parsing is in progress for this post.
     */
    is_parsing_in_progress() {
      const state = this._load_state();
      if (!state || !state.active) {
        return false;
      }
      if (this._is_state_stale(state)) {
        this._clear_state();
        return false;
      }
      if (state.post_id !== this._parse_post_id()) {
        return false;
      }
      return true;
    }
    /**
     * Cancel ongoing parsing (user manually aborted).
     */
    cancel_parsing() {
      const state = this._load_state();
      if (state && state.active) {
        this._clear_state();
      }
    }
    /**
     * Get the image handler instance (for accessing image_map and downloading).
     */
    get_image_handler() {
      return this.image_handler;
    }
    /**
     * Parse the complete post from current page DOM.
     * Automatically loads all content before parsing.
     * Handles multi-page posts with state persistence.
     */
    async parse_post() {
      const post_id = this._parse_post_id();
      const existing_state = this._load_state();
      if (existing_state && existing_state.active && existing_state.post_id === post_id) {
        if (this._is_state_stale(existing_state)) {
          this._clear_state();
        } else {
          return await this._resume_parse(existing_state);
        }
      }
      return await this._start_parse();
    }
    /**
     * Start a new parsing session.
     */
    async _start_parse() {
      const post_id = this._parse_post_id();
      const page_cnt = this._parse_page_count();
      if (this.current_page !== 1) {
        const state2 = {
          active: true,
          post_id,
          current_page: 1,
          total_pages: page_cnt,
          collected_floors: [],
          timestamp: Date.now()
        };
        this._save_state(state2);
        this._navigate_to_page(1);
        throw new Error("NAVIGATING");
      }
      const state = {
        active: true,
        post_id,
        current_page: this.current_page,
        total_pages: page_cnt,
        collected_floors: [],
        timestamp: Date.now()
      };
      this._save_state(state);
      const page_floors = await this._parse_current_page();
      state.collected_floors = state.collected_floors.concat(page_floors);
      state.current_page = this.current_page + 1;
      this._save_state(state);
      if (state.current_page <= state.total_pages) {
        this._navigate_to_page(state.current_page);
        throw new Error("NAVIGATING");
      } else {
        return await this._finalize_parse(state);
      }
    }
    /**
     * Resume an existing parsing session.
     */
    async _resume_parse(state) {
      const page_floors = await this._parse_current_page();
      state.collected_floors = state.collected_floors.concat(page_floors);
      state.current_page = this.current_page + 1;
      this._save_state(state);
      if (state.current_page <= state.total_pages) {
        this._navigate_to_page(state.current_page);
        throw new Error("NAVIGATING");
      } else {
        return await this._finalize_parse(state);
      }
    }
    /**
     * Finalize parsing and create the complete post.
     */
    async _finalize_parse(state) {
      const title = this._parse_title();
      const tieba_name = this._parse_tieba_name();
      const tieba_url = this._parse_tieba_url();
      const post_id = state.post_id;
      const page_cnt = state.total_pages;
      const comment_cnt = this._parse_comment_count();
      const avatar_urls = this._extract_all_avatar_urls(state.collected_floors);
      const all_html = this._collect_all_html(state.collected_floors);
      const image_map = this.image_handler.process_images(all_html, avatar_urls);
      this._replace_image_urls_in_floors(state.collected_floors);
      this.image_handler.replace_avatar_urls(state.collected_floors);
      const post = {
        title,
        tieba_name,
        tieba_url,
        id: post_id,
        page_cnt,
        comment_cnt,
        floor_list: state.collected_floors,
        image_map
      };
      this._clear_state();
      return post;
    }
    /**
     * Navigate to a specific page.
     */
    _navigate_to_page(page_number) {
      const page_url = `${this.base_url}?pn=${page_number}`;
      window.location.href = page_url;
    }
    /**
     * Parse the current page with full loading process.
     */
    async _parse_current_page() {
      const original_zoom = this._save_original_zoom();
      this._set_zoom(0.3);
      await new Promise((resolve) => setTimeout(resolve, 500));
      await this._scroll_to_load_floors();
      await this._expand_all_sub_comments();
      const floor_list = await this._parse_floors();
      this._set_zoom(original_zoom);
      return floor_list;
    }
    /**
     * Extract all avatar URLs from floor list.
     */
    _extract_all_avatar_urls(floor_list) {
      const avatar_urls = [];
      floor_list.forEach((floor) => {
        if (floor.comment.avatar_url) {
          avatar_urls.push(floor.comment.avatar_url);
        }
        if (floor.sub_comment_list) {
          floor.sub_comment_list.forEach((sub) => {
            if (sub.avatar_url) {
              avatar_urls.push(sub.avatar_url);
            }
          });
        }
      });
      return avatar_urls;
    }
    /**
     * Collect all HTML content from floor list.
     */
    _collect_all_html(floor_list) {
      let all_html = "";
      floor_list.forEach((floor) => {
        all_html += floor.comment.content;
        if (floor.sub_comment_list) {
          floor.sub_comment_list.forEach((sub) => {
            all_html += sub.content;
          });
        }
      });
      return all_html;
    }
    /**
     * Replace image URLs in all floor content.
     */
    _replace_image_urls_in_floors(floor_list) {
      floor_list.forEach((floor) => {
        floor.comment.content = this.image_handler.replace_image_urls(floor.comment.content);
        if (floor.sub_comment_list) {
          floor.sub_comment_list.forEach((sub) => {
            sub.content = this.image_handler.replace_image_urls(sub.content);
          });
        }
      });
    }
    /**
     * Save original zoom level.
     */
    _save_original_zoom() {
      const current_zoom = document.body.style.zoom || "1";
      return parseFloat(current_zoom);
    }
    /**
     * Set zoom level.
     */
    _set_zoom(level) {
      document.body.style.zoom = level.toString();
    }
    /**
     * Stage 1: Scroll to load all lazy-loaded floors.
     */
    async _scroll_to_load_floors() {
      return new Promise((resolve) => {
        window.scrollTo(0, 0);
        const scroll_task = setInterval(() => {
          window.scrollBy(0, 100);
          const max_scroll = document.documentElement.scrollHeight - window.innerHeight;
          if (window.scrollY + window.innerHeight >= max_scroll - 50) {
            setTimeout(() => {
              clearInterval(scroll_task);
              window.scrollTo(0, document.documentElement.scrollHeight);
              setTimeout(() => {
                resolve();
              }, 1e3);
            }, 500);
          }
        }, 100);
      });
    }
    /**
     * Stage 2: Expand all sub-comment sections.
     */
    async _expand_all_sub_comments() {
      const expand_buttons = document.querySelectorAll(".j_lzl_m");
      expand_buttons.forEach((button) => {
        try {
          button.click();
        } catch {
        }
      });
      await this._wait_for_dom_update();
    }
    /**
     * Parse post title from DOM.
     */
    _parse_title() {
      const title_element = document.querySelector(".core_title_txt");
      if (!title_element) {
        throw new Error("Post title not found");
      }
      return title_element.textContent?.trim() || "";
    }
    /**
     * Parse tieba name from DOM.
     */
    _parse_tieba_name() {
      const tieba_element = document.querySelector(".card_title_fname");
      if (!tieba_element) {
        throw new Error("Tieba name not found");
      }
      return tieba_element.textContent?.trim() || "";
    }
    /**
     * Parse tieba URL from DOM.
     */
    _parse_tieba_url() {
      const tieba_link = document.querySelector(".card_title_fname");
      if (tieba_link && tieba_link.tagName === "A") {
        const href = tieba_link.getAttribute("href");
        if (href) {
          if (href.startsWith("/")) {
            return "https://tieba.baidu.com" + href;
          }
          return href;
        }
      }
      const tieba_name = this._parse_tieba_name();
      return `https://tieba.baidu.com/f?kw=${encodeURIComponent(tieba_name)}&fr=index`;
    }
    /**
     * Parse post ID from URL.
     * URL format: tieba.baidu.com/p/{id}
     */
    _parse_post_id() {
      const url_match = window.location.href.match(/\/p\/(\d+)/);
      if (!url_match) {
        throw new Error("Post ID not found in URL");
      }
      return parseInt(url_match[1]);
    }
    /**
     * Parse total page count from DOM.
     */
    _parse_page_count() {
      const posts_num_element = document.querySelector(".l_posts_num");
      if (!posts_num_element) {
        throw new Error("Page count not found");
      }
      const text = posts_num_element.textContent || "";
      const page_match = text.match(/共(\d+)页/);
      if (!page_match) {
        throw new Error("Page count format not recognized");
      }
      return parseInt(page_match[1]);
    }
    /**
     * Parse total comment count from DOM.
     */
    _parse_comment_count() {
      const posts_num_element = document.querySelector(".l_posts_num");
      if (!posts_num_element) {
        throw new Error("Comment count not found");
      }
      const text = posts_num_element.textContent || "";
      const comment_match = text.match(/(\d+)回复贴/);
      if (!comment_match) {
        throw new Error("Comment count format not recognized");
      }
      return parseInt(comment_match[1]);
    }
    /**
     * Parse all floors from DOM.
     */
    async _parse_floors() {
      const floors = [];
      const floor_elements = Array.from(document.querySelectorAll(".l_post.l_post_bright"));
      for (let i = 0; i < floor_elements.length; i++) {
        const floor_element = floor_elements[i];
        const data_field = floor_element.getAttribute("data-field");
        if (data_field === "{}") {
          continue;
        }
        try {
          const floor = await this._parse_floor(floor_element);
          floors.push(floor);
        } catch {
        }
      }
      return floors;
    }
    /**
     * Parse single floor from DOM element.
     */
    async _parse_floor(floor_element) {
      const comment = this._parse_main_comment(floor_element);
      const index = this._parse_floor_index(floor_element);
      const sub_comments = await this._parse_sub_comments(floor_element);
      return {
        comment,
        index,
        sub_comment_list: sub_comments.length > 0 ? sub_comments : void 0
      };
    }
    /**
     * Parse main comment from floor element.
     */
    _parse_main_comment(floor_element) {
      const author = this._parse_author_name(floor_element);
      const date = this._parse_date(floor_element);
      const content = this._parse_content(floor_element);
      const ip_location = this._parse_ip_location(floor_element);
      const avatar_url = this._parse_avatar_url(floor_element);
      const homepage_url = this._parse_homepage_url(floor_element);
      return {
        author,
        date,
        content,
        ip_location,
        avatar_url,
        homepage_url
      };
    }
    /**
     * Parse author name from floor element.
     */
    _parse_author_name(floor_element) {
      const author_element = floor_element.querySelector(".p_author_name");
      if (!author_element) {
        throw new Error("Author name not found");
      }
      return author_element.textContent?.trim() || "";
    }
    /**
     * Parse homepage URL from floor element.
     */
    _parse_homepage_url(floor_element) {
      const author_link = floor_element.querySelector(".p_author_name");
      if (author_link && author_link.tagName === "A") {
        const href = author_link.getAttribute("href");
        if (href) {
          if (href.startsWith("/")) {
            return "https://tieba.baidu.com" + href;
          }
          return href;
        }
      }
      const home_link = floor_element.querySelector('a[href*="/home/main"]');
      if (home_link) {
        const href = home_link.getAttribute("href");
        if (href) {
          if (href.startsWith("/")) {
            return "https://tieba.baidu.com" + href;
          }
          return href;
        }
      }
      return void 0;
    }
    /**
     * Parse avatar URL from floor element.
     */
    _parse_avatar_url(floor_element) {
      const avatar_img = floor_element.querySelector(".p_author_face img");
      if (avatar_img) {
        return avatar_img.getAttribute("src") || void 0;
      }
      const fallback_avatar = floor_element.querySelector('img[src*="static.tieba.baidu.com"]');
      if (fallback_avatar) {
        return fallback_avatar.getAttribute("src") || void 0;
      }
      return void 0;
    }
    /**
     * Parse date from floor element.
     * Handles both old and new Tieba DOM formats.
     */
    _parse_date(floor_element) {
      const new_format_tail = floor_element.querySelector(".core_reply_tail");
      if (new_format_tail) {
        const text = new_format_tail.textContent || "";
        const date_match = text.match(/(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2})/);
        if (date_match) {
          return date_match[1];
        }
      }
      const old_format_date = floor_element.querySelector(".tail-info");
      if (old_format_date) {
        const text = old_format_date.textContent || "";
        const date_match = text.match(/(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2})/);
        if (date_match) {
          return date_match[1];
        }
      }
      throw new Error("Date pattern not found in either old or new format");
    }
    /**
     * Parse content from floor element.
     */
    _parse_content(floor_element) {
      const content_element = floor_element.querySelector(".d_post_content");
      if (!content_element) {
        throw new Error("Content element not found");
      }
      let content = content_element.innerHTML.trim();
      content = content.replace(/点击展开,查看完整图片。/g, "");
      content = content.replace(/点击展开,查看完整图片<\/a>/g, "");
      content = content.replace(/\s*点击展开,查看完整图片。?\s*/g, "");
      content = content.replace(/\s*点击展开,查看完整图片<\/a>\s*/g, "");
      return content;
    }
    /**
     * Parse IP location from floor element.
     * Returns undefined if IP location not present.
     * Handles both old and new Tieba DOM formats.
     */
    _parse_ip_location(floor_element) {
      const ip_element = floor_element.querySelector(".ip-location span");
      if (ip_element) {
        const text = ip_element.textContent || "";
        const ip_match = text.match(/IP属地:(.+)/);
        if (ip_match) {
          return ip_match[1].trim();
        }
      }
      const ip_spans = floor_element.querySelectorAll("span");
      for (const span of Array.from(ip_spans)) {
        const text = span.textContent || "";
        const ip_match = text.match(/IP属地:(.+)/);
        if (ip_match) {
          return ip_match[1].trim();
        }
      }
      return void 0;
    }
    /**
     * Parse floor index from floor element.
     * Handles both old and new Tieba DOM formats.
     */
    _parse_floor_index(floor_element) {
      const new_format_tail = floor_element.querySelector(".p_tail");
      if (new_format_tail) {
        const floor_span = new_format_tail.querySelector("span");
        if (floor_span) {
          const text = floor_span.textContent || "";
          const floor_match = text.match(/(\d+)楼/);
          if (floor_match) {
            return parseInt(floor_match[1]);
          }
        }
      }
      const tail_info_spans = Array.from(floor_element.querySelectorAll(".tail-info"));
      for (const span of tail_info_spans) {
        const text = span.textContent || "";
        const floor_match = text.match(/(\d+)楼/);
        if (floor_match) {
          return parseInt(floor_match[1]);
        }
      }
      throw new Error("Floor index pattern not found in either old or new format");
    }
    /**
     * Parse sub-comments from floor element.
     * Handles pagination by clicking through all pages.
     */
    async _parse_sub_comments(floor_element) {
      const all_sub_comments = [];
      try {
        const pager = floor_element.querySelector(".j_pager");
        if (!pager) {
          return this._parse_current_sub_comments(floor_element);
        }
        const pager_html = pager;
        if (pager_html.style.display === "none") {
          return this._parse_current_sub_comments(floor_element);
        }
        const total_pages = this._get_sub_comment_page_count(pager);
        if (total_pages === 0) {
          return this._parse_current_sub_comments(floor_element);
        }
        const first_page_comments = this._parse_current_sub_comments(floor_element);
        all_sub_comments.push(...first_page_comments);
        for (let page = 2; page <= total_pages; page++) {
          const page_link = this._find_page_button(floor_element, page);
          if (!page_link) {
            break;
          }
          page_link.click();
          await this._wait_for_dom_update();
          const page_comments = this._parse_current_sub_comments(floor_element);
          all_sub_comments.push(...page_comments);
        }
      } catch {
        return this._parse_current_sub_comments(floor_element);
      }
      return all_sub_comments;
    }
    /**
     * Get total page count from sub-comment pager.
     */
    _get_sub_comment_page_count(pager) {
      const last_page_links = Array.from(pager.querySelectorAll("a"));
      for (const link of last_page_links) {
        const text = link.textContent?.trim();
        if (text === "\u5C3E\u9875" || text === "Last") {
          const href = link.getAttribute("href");
          if (href) {
            const match = href.match(/#(\d+)/);
            if (match) {
              return parseInt(match[1]);
            }
          }
        }
      }
      const page_numbers = [];
      const all_links = Array.from(pager.querySelectorAll("a"));
      for (const link of all_links) {
        const text = link.textContent?.trim();
        if (text && /^\d+$/.test(text)) {
          page_numbers.push(parseInt(text));
        }
      }
      if (page_numbers.length > 0) {
        return Math.max(...page_numbers);
      }
      return 0;
    }
    /**
     * Find specific page button for sub-comment pagination.
     */
    _find_page_button(floor_element, page_num) {
      const pager = floor_element.querySelector(".j_pager");
      if (!pager) {
        return null;
      }
      const page_links = Array.from(pager.querySelectorAll("a"));
      for (const link of page_links) {
        const href = link.getAttribute("href");
        if (href === `#${page_num}`) {
          return link;
        }
      }
      for (const link of page_links) {
        const text = link.textContent?.trim();
        if (text === page_num.toString()) {
          return link;
        }
      }
      return null;
    }
    /**
     * Parse sub-comments currently visible in DOM.
     */
    _parse_current_sub_comments(floor_element) {
      const sub_comments = [];
      const sub_comment_elements = floor_element.querySelectorAll(".lzl_single_post, .j_lzl_s_p");
      sub_comment_elements.forEach((sub_element) => {
        try {
          const comment = this._parse_sub_comment(sub_element);
          sub_comments.push(comment);
        } catch {
        }
      });
      return sub_comments;
    }
    /**
     * Wait for DOM to update after pagination click.
     */
    async _wait_for_dom_update() {
      return new Promise((resolve) => {
        setTimeout(resolve, 1e3);
      });
    }
    /**
     * Parse single sub-comment from DOM element.
     */
    _parse_sub_comment(sub_element) {
      const author = this._parse_sub_author(sub_element);
      const date = this._parse_sub_date(sub_element);
      const content = this._parse_sub_content(sub_element);
      const avatar_url = this._parse_sub_avatar_url(sub_element);
      const homepage_url = this._parse_sub_homepage_url(sub_element);
      const reply_info = this._parse_reply_info(content);
      return {
        author,
        date,
        content,
        avatar_url,
        homepage_url,
        reply_to_author: reply_info.author,
        reply_to_homepage_url: reply_info.homepage_url
      };
    }
    /**
     * Parse sub-comment avatar URL.
     */
    _parse_sub_avatar_url(sub_element) {
      const avatar_link = sub_element.querySelector(".lzl_p_p img");
      if (avatar_link) {
        const src = avatar_link.getAttribute("src");
        if (src) {
          if (src.startsWith("//")) {
            return "https:" + src;
          }
          return src;
        }
      }
      return void 0;
    }
    /**
     * Parse sub-comment homepage URL.
     */
    _parse_sub_homepage_url(sub_element) {
      const user_card = sub_element.querySelector(".at.j_user_card");
      if (user_card && user_card.tagName === "A") {
        const href = user_card.getAttribute("href");
        if (href && href.includes("/home/main")) {
          if (href.startsWith("/")) {
            return "https://tieba.baidu.com" + href;
          }
          return href;
        }
      }
      const home_link = sub_element.querySelector('a[href*="/home/main"]');
      if (home_link) {
        const href = home_link.getAttribute("href");
        if (href) {
          if (href.startsWith("/")) {
            return "https://tieba.baidu.com" + href;
          }
          return href;
        }
      }
      return void 0;
    }
    /**
     * Parse reply-to information from sub-comment content.
     * Returns the author and homepage URL if this is a reply to another user.
     */
    _parse_reply_info(content) {
      const reply_pattern = /回复\s+<a[^>]+href="([^"]+)"[^>]*>([^<]+)<\/a>\s*:/;
      const match = content.match(reply_pattern);
      if (match) {
        let href = match[1];
        const author = match[2];
        if (href.startsWith("/")) {
          href = "https://tieba.baidu.com" + href;
        }
        return { author, homepage_url: href };
      }
      return {};
    }
    /**
     * Parse sub-comment author name.
     */
    _parse_sub_author(sub_element) {
      const user_card = sub_element.querySelector(".at.j_user_card, .j_user_card");
      if (user_card) {
        const data_field = user_card.getAttribute("data-field");
        if (data_field) {
          try {
            const parsed = JSON.parse(data_field.replace(/'/g, '"'));
            if (parsed.showname) {
              return parsed.showname;
            }
          } catch {
          }
        }
        const text = user_card.textContent?.trim();
        if (text) {
          return text;
        }
      }
      const author_span = sub_element.querySelector("[data-lzl-author]");
      if (author_span) {
        const username = author_span.getAttribute("data-lzl-author");
        if (username) {
          return username;
        }
      }
      const any_user = sub_element.querySelector("[username]");
      if (any_user) {
        const username = any_user.getAttribute("username");
        if (username) {
          return username;
        }
      }
      return "Unknown";
    }
    /**
     * Parse sub-comment date.
     * Format: "2026-3-28 11:02" (no zero-padding)
     */
    _parse_sub_date(sub_element) {
      const time_element = sub_element.querySelector(".lzl_time");
      if (!time_element) {
        throw new Error("Sub-comment date not found");
      }
      const text = (time_element.textContent || "").replace(/\u00a0/, " ");
      return text.trim();
    }
    /**
     * Parse sub-comment content.
     */
    _parse_sub_content(sub_element) {
      const content_element = sub_element.querySelector(".lzl_content_main");
      if (!content_element) {
        throw new Error("Sub-comment content not found");
      }
      return content_element.innerHTML.trim();
    }
  };

  // src/main.ts
  var DownloadModal = class {
    constructor() {
      this.modal = document.createElement("div");
      this.modal.id = "tieba-download-modal";
      this.modal.style.cssText = `
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0, 0, 0, 0.7);
            display: none;
            justify-content: center;
            align-items: center;
            z-index: 10000;
        `;
      const content = document.createElement("div");
      content.style.cssText = `
            background: white;
            padding: 30px;
            border-radius: 12px;
            box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
            max-width: 500px;
            width: 90%;
        `;
      const title = document.createElement("h2");
      title.textContent = "\u5907\u4EFD\u5B8C\u6210";
      title.style.cssText = `
            margin: 0 0 20px 0;
            color: #333;
            text-align: center;
        `;
      this.summary_text = document.createElement("div");
      this.summary_text.style.cssText = `
            margin-bottom: 20px;
            color: #666;
            line-height: 1.6;
        `;
      const progress_container = document.createElement("div");
      progress_container.style.cssText = `
            margin-bottom: 15px;
        `;
      const progress_bg = document.createElement("div");
      progress_bg.style.cssText = `
            width: 100%;
            height: 24px;
            background: #e0e0e0;
            border-radius: 12px;
            overflow: hidden;
            position: relative;
        `;
      this.progress_bar = document.createElement("div");
      this.progress_bar.style.cssText = `
            height: 100%;
            background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
            width: 0%;
            transition: width 0.3s ease;
        `;
      const progress_overlay = document.createElement("div");
      progress_overlay.style.cssText = `
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            display: flex;
            align-items: center;
            justify-content: center;
            color: white;
            font-weight: bold;
            font-size: 12px;
            text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
        `;
      progress_overlay.id = "tieba-progress-overlay";
      progress_overlay.textContent = "0%";
      this.progress_text = document.createElement("div");
      this.progress_text.style.cssText = `
            text-align: center;
            color: #999;
            font-size: 14px;
            margin-top: 10px;
        `;
      progress_bg.appendChild(this.progress_bar);
      progress_bg.appendChild(progress_overlay);
      progress_container.appendChild(progress_bg);
      content.appendChild(title);
      content.appendChild(this.summary_text);
      content.appendChild(progress_container);
      content.appendChild(this.progress_text);
      this.modal.appendChild(content);
      document.body.appendChild(this.modal);
    }
    /**
     * Show modal with summary info
     */
    show(post, image_count) {
      this.summary_text.innerHTML = `
            <div><strong>\u8D34\u5B50\u6807\u9898:</strong> ${post.title}</div>
            <div><strong>\u8D34\u5427:</strong> ${post.tieba_name}</div>
            <div><strong>\u697C\u5C42:</strong> ${post.floor_list.length}</div>
            <div><strong>\u56DE\u590D:</strong> ${post.comment_cnt}</div>
            <div><strong>\u56FE\u7247:</strong> ${image_count}</div>
        `;
      this.modal.style.display = "flex";
      this.progress_text.textContent = "\u6B63\u5728\u4E0B\u8F7D\u56FE\u7247...";
    }
    /**
     * Update progress bar
     */
    update_progress(current, total, filename) {
      const percentage = Math.round(current / total * 100);
      this.progress_bar.style.width = `${percentage}%`;
      const overlay = document.getElementById("tieba-progress-overlay");
      if (overlay) {
        overlay.textContent = `${percentage}%`;
      }
      this.progress_text.textContent = `\u6B63\u5728\u4E0B\u8F7D: ${filename} (${current}/${total})`;
    }
    /**
     * Hide modal
     */
    hide() {
      this.modal.style.display = "none";
    }
    /**
     * Show complete message
     */
    show_complete(filename) {
      this.progress_text.textContent = `\u2705 \u5907\u4EFD\u5B8C\u6210! \u6B63\u5728\u4E0B\u8F7D: ${filename}`;
      this.progress_bar.style.width = "100%";
      const overlay = document.getElementById("tieba-progress-overlay");
      if (overlay) {
        overlay.textContent = "100%";
      }
    }
  };
  var download_modal = null;
  async function backup_post() {
    const backup_button = document.getElementById("tieba-backup-btn");
    try {
      const parser = new Parser();
      if (backup_button) {
        backup_button.textContent = "\u5907\u4EFD\u4E2D...";
        backup_button.disabled = true;
      }
      const post = await parser.parse_post();
      if (!download_modal) {
        download_modal = new DownloadModal();
      }
      const image_handler = parser.get_image_handler();
      const image_map = image_handler.image_map || {};
      const image_count = Object.keys(image_map).length;
      download_modal.show(post, image_count);
      const image_blobs = await image_handler.download_images((current, total, filename2) => {
        if (download_modal) {
          download_modal.update_progress(current, total, filename2);
        }
      });
      const viewer_html = image_handler.create_viewer_html(post);
      const json_data = JSON.stringify(post, null, 2);
      const files = {
        "post.json": json_data,
        "viewer.html": viewer_html
      };
      for (const [name, blob] of Object.entries(image_blobs)) {
        files[`images/${name}`] = blob;
      }
      const tar_blob = await image_handler.create_tar(files);
      const filename = `${post.tieba_name}_${post.id}.tar`;
      if (download_modal) {
        download_modal.show_complete(filename);
      }
      await new Promise((resolve) => setTimeout(resolve, 500));
      image_handler.download_blob(tar_blob, filename);
      setTimeout(() => {
        if (download_modal) {
          download_modal.hide();
        }
      }, 2e3);
      if (backup_button) {
        backup_button.textContent = "\u5907\u4EFD\u672C\u8D34";
        backup_button.disabled = false;
      }
      return post;
    } catch (error) {
      if (error instanceof Error && error.message === "NAVIGATING") {
        return void 0;
      }
      if (error instanceof Error) {
        const error_info = {
          message: error.message,
          stack: error.stack,
          timestamp: (/* @__PURE__ */ new Date()).toISOString(),
          url: window.location.href,
          user_agent: navigator.userAgent
        };
        window.GM_setValue("last_error", error_info);
      }
      if (download_modal) {
        download_modal.hide();
      }
      if (backup_button) {
        backup_button.textContent = "\u5907\u4EFD\u5931\u8D25";
        setTimeout(() => {
          backup_button.textContent = "\u5907\u4EFD\u672C\u8D34";
          backup_button.disabled = false;
        }, 2e3);
      }
      show_view_error_button();
      throw error;
    }
  }
  function inject_backup_button() {
    if (document.getElementById("tieba-backup-btn")) {
      return;
    }
    const container = document.createElement("div");
    container.id = "tieba-backup-container";
    container.style.cssText = `
        position: fixed;
        top: 100px;
        right: 20px;
        z-index: 9999;
        display: flex;
        flex-direction: column;
        gap: 10px;
    `;
    const backup_button = document.createElement("button");
    backup_button.id = "tieba-backup-btn";
    backup_button.textContent = "\u5907\u4EFD\u672C\u8D34";
    backup_button.style.cssText = `
        padding: 10px 20px;
        background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
        color: white;
        border: none;
        border-radius: 8px;
        cursor: pointer;
        font-size: 14px;
        font-weight: bold;
        box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
        transition: all 0.3s ease;
    `;
    backup_button.addEventListener("mouseenter", () => {
      backup_button.style.transform = "translateY(-2px)";
      backup_button.style.boxShadow = "0 6px 12px rgba(0, 0, 0, 0.15)";
    });
    backup_button.addEventListener("mouseleave", () => {
      backup_button.style.transform = "translateY(0)";
      backup_button.style.boxShadow = "0 4px 6px rgba(0, 0, 0, 0.1)";
    });
    backup_button.addEventListener("click", () => {
      backup_post();
    });
    const reset_button = document.createElement("button");
    reset_button.id = "tieba-reset-btn";
    reset_button.textContent = "\u91CD\u7F6E\u72B6\u6001";
    reset_button.style.cssText = `
        padding: 8px 16px;
        background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
        color: white;
        border: none;
        border-radius: 6px;
        cursor: pointer;
        font-size: 12px;
        font-weight: bold;
        box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
        transition: all 0.3s ease;
        opacity: 0.8;
    `;
    reset_button.addEventListener("mouseenter", () => {
      reset_button.style.transform = "translateY(-2px)";
      reset_button.style.boxShadow = "0 4px 8px rgba(0, 0, 0, 0.15)";
      reset_button.style.opacity = "1";
    });
    reset_button.addEventListener("mouseleave", () => {
      reset_button.style.transform = "translateY(0)";
      reset_button.style.boxShadow = "0 2px 4px rgba(0, 0, 0, 0.1)";
      reset_button.style.opacity = "0.8";
    });
    reset_button.addEventListener("click", () => {
      const parser = new Parser();
      parser.reset_parsing();
      reset_button.textContent = "\u5DF2\u91CD\u7F6E";
      setTimeout(() => {
        reset_button.textContent = "\u91CD\u7F6E\u72B6\u6001";
      }, 1e3);
    });
    container.appendChild(backup_button);
    container.appendChild(reset_button);
    document.body.appendChild(container);
  }
  function auto_resume_if_needed() {
    const parser = new Parser();
    if (parser.is_parsing_in_progress()) {
      const reset_button = document.getElementById("tieba-reset-btn");
      if (reset_button) {
        reset_button.style.opacity = "1";
        reset_button.style.fontWeight = "bold";
      }
      setTimeout(() => {
        if (parser.is_parsing_in_progress()) {
          backup_post();
        }
      }, 2e3);
      return true;
    }
    return false;
  }
  function show_view_error_button() {
    if (document.getElementById("tieba-view-error-btn")) {
      return;
    }
    const container = document.getElementById("tieba-backup-container");
    if (!container) {
      return;
    }
    const error_button = document.createElement("button");
    error_button.id = "tieba-view-error-btn";
    error_button.textContent = "\u67E5\u770B\u9519\u8BEF";
    error_button.style.cssText = `
        padding: 8px 16px;
        background: #ff4444;
        color: white;
        border: none;
        border-radius: 4px;
        cursor: pointer;
        font-size: 12px;
        font-weight: bold;
        opacity: 0.9;
    `;
    error_button.addEventListener("click", () => {
      const error_data = window.GM_getValue("last_error", void 0);
      if (!error_data) {
        alert("\u6CA1\u6709\u9519\u8BEF\u4FE1\u606F");
        return;
      }
      const error_text = `
\u9519\u8BEF\u4FE1\u606F: ${error_data.message}

\u65F6\u95F4: ${error_data.timestamp}

\u9875\u9762: ${error_data.url}

\u6D4F\u89C8\u5668: ${error_data.user_agent}

\u5806\u6808:
${error_data.stack}
        `.trim();
      alert(error_text);
    });
    const reset_button = document.getElementById("tieba-reset-btn");
    if (reset_button) {
      container.insertBefore(error_button, reset_button);
    } else {
      container.appendChild(error_button);
    }
    setTimeout(() => {
      error_button.remove();
    }, 3e4);
  }
  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", () => {
      inject_backup_button();
      auto_resume_if_needed();
    });
  } else {
    inject_backup_button();
    auto_resume_if_needed();
  }
  window.backup_post = backup_post;
})();