<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:atom="http://www.w3.org/2005/Atom"
>
<channel>
<title><![CDATA[DeepSider]]></title> 
<atom:link href="https://deepsider.cn/rss.php" rel="self" type="application/rss+xml" />
<description><![CDATA[深度玩家，极客向导-------一个分享免费好用脚本的网站]]></description>
<link>https://deepsider.cn/</link>
<language>zh-cn</language>
<generator>emlog</generator>

<item>
    <title>EMLOG 智能文章发布工具 v2.0</title>
    <link>https://deepsider.cn/post-5.html</link>
    <description><![CDATA[<p><img src="https://deepsider.cn/content/uploadfile/202604/2df41776184473.jpg" alt="" /></p>
<pre><code class="language-python">import tkinter as tk
from tkinter import ttk, messagebox, scrolledtext
import requests
import hashlib
import time
import json
import threading
import os
import base64
from datetime import datetime
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC

# ---------------------------- 配置管理器（加密） ----------------------------
class ConfigManager:
    """配置管理器，负责加密存储和读取配置"""

    def __init__(self, config_file="emlog_config.enc", key_file="emlog_key.key"):
        self.config_file = config_file
        self.key_file = key_file
        self.cipher = None

    def _generate_key_from_password(self, password):
        salt = b'emlog_salt_2024'
        kdf = PBKDF2HMAC(
            algorithm=hashes.SHA256(),
            length=32,
            salt=salt,
            iterations=100000,
        )
        key = base64.urlsafe_b64encode(kdf.derive(password.encode()))
        return key

    def _get_cipher(self):
        if self.cipher is None:
            import platform
            import socket
            machine_id = platform.node() + platform.processor() + socket.gethostname()
            key = self._generate_key_from_password(machine_id)
            self.cipher = Fernet(key)
        return self.cipher

    def save_config(self, config):
        try:
            cipher = self._get_cipher()
            config_json = json.dumps(config, ensure_ascii=False, indent=2)
            encrypted_data = cipher.encrypt(config_json.encode('utf-8'))
            with open(self.config_file, 'wb') as f:
                f.write(encrypted_data)
            return True, "配置已加密保存"
        except Exception as e:
            return False, f"保存失败: {str(e)}"

    def load_config(self):
        try:
            if not os.path.exists(self.config_file):
                return None, "配置文件不存在"
            cipher = self._get_cipher()
            with open(self.config_file, 'rb') as f:
                encrypted_data = f.read()
            decrypted_data = cipher.decrypt(encrypted_data)
            config = json.loads(decrypted_data.decode('utf-8'))
            return config, "配置加载成功"
        except Exception as e:
            return None, f"加载失败: {str(e)}"

    def delete_config(self):
        try:
            if os.path.exists(self.config_file):
                os.remove(self.config_file)
            return True, "配置文件已删除"
        except Exception as e:
            return False, f"删除失败: {str(e)}"

    def set_config_file(self, config_file):
        """设置配置文件路径"""
        self.config_file = config_file

# ---------------------------- 主应用程序 ----------------------------
class EmlogPublisher:
    def __init__(self, root):
        self.root = root
        self.root.title("EMLOG 智能文章发布工具 v2.0 xjrx.net")
        self.root.geometry("1100x950")

        # 初始化配置管理器
        self.config_manager = ConfigManager()

        # EMLOG配置
        self.emlog_url = tk.StringVar()
        self.api_key = tk.StringVar()

        # 智谱AI配置
        self.zhipu_api_key = tk.StringVar()
        self.selected_model = tk.StringVar(value="glm-4.5-air (推荐·性价比最高·快速)")

        # 文章配置
        self.article_count = tk.IntVar(value=1)
        self.selected_category = tk.StringVar()
        self.selected_language = tk.StringVar(value="中文")
        self.keywords = tk.StringVar()

        self.categories = {}
        self.category_list = []

        # 存储生成的完整文章（累积存储）
        self.generated_articles = []   # 每个元素：{'title','content','excerpt','keywords','selected'}

        # 是否自动保存配置
        self.auto_save = tk.BooleanVar(value=True)

        self.setup_ui()

        # 启动时自动加载配置
        self.root.after(100, self.load_all_config)

    def setup_ui(self):
        # 设置窗口图标和样式
        self.root.configure(bg='#f5f5f5')
        self.root.geometry("1000x700")
        self.root.title("EMLOG 智能文章发布工具")

        # 创建样式
        style = ttk.Style()
        style.theme_use('clam')

        # 自定义样式 - 紧凑设计
        style.configure('TLabel', background='#ffffff', foreground='#333333', font=('微软雅黑', 9))
        style.configure('TButton', 
                      background='#4a90e2', 
                      foreground='white', 
                      font=('微软雅黑', 9, 'bold'),
                      padding=6)
        style.map('TButton', 
                 background=[('active', '#357abd')],
                 foreground=[('active', 'white')])
        style.configure('TEntry', 
                      font=('微软雅黑', 9),
                      padding=4,
                      relief='flat',
                      borderwidth=1)
        style.configure('TCombobox', 
                      font=('微软雅黑', 9),
                      padding=4,
                      relief='flat')
        style.configure('TLabelframe', 
                      background='#ffffff', 
                      foreground='#333333', 
                      font=('微软雅黑', 10, 'bold'),
                      relief='flat',
                      borderwidth=0)
        style.configure('TLabelframe.Label', 
                      background='#ffffff', 
                      foreground='#333333',
                      font=('微软雅黑', 10, 'bold'))
        style.configure('TFrame', background='#ffffff')
        style.configure('TSpinbox', font=('微软雅黑', 9), padding=4)
        style.configure('TCheckbutton', background='#ffffff', foreground='#333333', font=('微软雅黑', 9))
        style.configure('TScrollbar', background='#e0e0e0', troughcolor='#f5f5f5')
        style.configure('Treeview', 
                      background='#ffffff',
                      foreground='#333333',
                      font=('微软雅黑', 9),
                      rowheight=24)
        style.configure('Treeview.Heading', 
                      background='#f5f5f5',
                      foreground='#666666',
                      font=('微软雅黑', 9, 'bold'))

        # 主布局 - 侧边栏 + 主内容
        main_paned = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL)
        main_paned.pack(fill='both', expand=True)

        # 左侧导航栏
        sidebar = ttk.Frame(main_paned, width=160, padding=5)
        sidebar.configure(style='TFrame')
        main_paned.add(sidebar, weight=0)

        # 导航栏标题
        nav_header = ttk.Frame(sidebar)
        nav_header.pack(fill='x', pady=(0, 10))
        ttk.Label(nav_header, text="EMLOG智能发布", font=('微软雅黑', 12, 'bold'), foreground='#4a90e2').pack(anchor=tk.W, pady=(5, 2))
        ttk.Label(nav_header, text="智能文章管理", font=('微软雅黑', 8, 'italic'), foreground='#999999').pack(anchor=tk.W)

        # 导航菜单
        nav_menu = ttk.Frame(sidebar)
        nav_menu.pack(fill='both', expand=True)

        # 导航按钮样式
        style.configure('Nav.TButton', 
                      background='#ffffff',
                      foreground='#666666',
                      font=('微软雅黑', 10),
                      padding=6,
                      width=16)
        style.map('Nav.TButton', 
                 background=[('active', '#e8f0fe'), ('selected', '#4a90e2')],
                 foreground=[('active', '#4a90e2'), ('selected', 'white')])

        # 导航按钮
        self.nav_buttons = {}
        nav_items = [
            ("配置管理", self.show_config_tab),
            ("文章生成", self.show_generate_tab),
            ("发布记录", self.show_publish_tab),
            ("关于工具", self.show_about)
        ]

        for text, command in nav_items:
            btn = ttk.Button(nav_menu, text=text, command=command, style='Nav.TButton')
            btn.pack(fill='x', pady=1)
            self.nav_buttons[text] = btn

        # 右侧主内容区域
        content_area = ttk.Frame(main_paned, padding=10)
        content_area.configure(style='TFrame')
        main_paned.add(content_area, weight=1)

        # 顶部用户栏
        user_bar = ttk.Frame(content_area)
        user_bar.pack(fill='x', pady=(0, 10))

        # 左侧标题
        title_frame = ttk.Frame(user_bar)
        title_frame.pack(side=tk.LEFT)
        self.current_title = tk.StringVar(value="配置管理")
        ttk.Label(title_frame, textvariable=self.current_title, font=('微软雅黑', 14, 'bold'), foreground='#333333').pack(anchor=tk.W)

        # 右侧用户信息
        user_info = ttk.Frame(user_bar)
        user_info.pack(side=tk.RIGHT)
        ttk.Label(user_info, text="用户", font=('微软雅黑', 9), foreground='#666666').pack(side=tk.LEFT, padx=5)

        # 内容容器
        self.content_container = ttk.Frame(content_area)
        self.content_container.pack(fill='both', expand=True)

        # 创建各个选项卡的容器
        self.config_frame = ttk.Frame(self.content_container)
        self.generate_frame = ttk.Frame(self.content_container)
        self.publish_frame = ttk.Frame(self.content_container)

        # 初始化各个选项卡
        self.setup_config_tab(self.config_frame)
        self.setup_generate_tab(self.generate_frame)
        self.setup_publish_tab(self.publish_frame)

        # 默认显示配置管理
        self.show_config_tab()

    # ---------------------------- 配置选项卡 ----------------------------
    def setup_config_tab(self, parent):
        # 创建主框架
        main_frame = ttk.Frame(parent)
        main_frame.pack(fill='both', expand=True)

        # 配置卡片容器
        config_container = ttk.Frame(main_frame)
        config_container.pack(fill='both', expand=True)

        # EMLOG配置卡片
        emlog_card = ttk.LabelFrame(config_container, text="EMLOG 配置", padding=12)
        emlog_card.pack(fill='x', pady=(0, 10))

        # 网站地址
        url_frame = ttk.Frame(emlog_card)
        url_frame.pack(fill='x', pady=(0, 10))
        ttk.Label(url_frame, text="网站地址:", width=10, font=('微软雅黑', 9, 'bold')).pack(side=tk.LEFT, pady=3)
        url_entry = ttk.Entry(url_frame, textvariable=self.emlog_url, font=('微软雅黑', 9))
        url_entry.pack(side=tk.LEFT, fill='x', expand=True, padx=5, pady=3)
        ttk.Label(url_frame, text="例如: https://yourdomain.com", foreground="#999999", font=('微软雅黑', 8)).pack(side=tk.LEFT, padx=5, pady=3)

        # API密钥
        api_frame = ttk.Frame(emlog_card)
        api_frame.pack(fill='x')
        ttk.Label(api_frame, text="API密钥:", width=10, font=('微软雅黑', 9, 'bold')).pack(side=tk.LEFT, pady=3)
        api_key_entry = ttk.Entry(api_frame, textvariable=self.api_key, show="*", font=('微软雅黑', 9))
        api_key_entry.pack(side=tk.LEFT, fill='x', expand=True, padx=5, pady=3)
        ttk.Label(api_frame, text="在EMLOG后台API设置页面获取", foreground="#999999", font=('微软雅黑', 8)).pack(side=tk.LEFT, padx=5, pady=3)

        # 智谱AI配置卡片
        zhipu_card = ttk.LabelFrame(config_container, text="智谱AI 配置", padding=12)
        zhipu_card.pack(fill='x', pady=(0, 10))

        # AI API密钥
        zhipu_frame = ttk.Frame(zhipu_card)
        zhipu_frame.pack(fill='x', pady=(0, 10))
        ttk.Label(zhipu_frame, text="API密钥:", width=10, font=('微软雅黑', 9, 'bold')).pack(side=tk.LEFT, pady=3)
        zhipu_entry = ttk.Entry(zhipu_frame, textvariable=self.zhipu_api_key, show="*", font=('微软雅黑', 9))
        zhipu_entry.pack(side=tk.LEFT, fill='x', expand=True, padx=5, pady=3)
        ttk.Label(zhipu_frame, text="在智谱AI开放平台获取", foreground="#999999", font=('微软雅黑', 8)).pack(side=tk.LEFT, padx=5, pady=3)

        # 模型选择
        model_frame = ttk.Frame(zhipu_card)
        model_frame.pack(fill='x')
        ttk.Label(model_frame, text="AI模型:", width=10, font=('微软雅黑', 9, 'bold')).pack(side=tk.LEFT, pady=3)
        model_combo = ttk.Combobox(model_frame, textvariable=self.selected_model, 
                                   values=[
                                       "glm-4.5-air (推荐·性价比最高·快速)",
                                       "glm-4-flash (免费·速度最快·质量略低)",
                                       "glm-4-plus (最强·高质量·稍贵)"
                                   ], state='readonly', font=('微软雅黑', 9))
        model_combo.pack(side=tk.LEFT, fill='x', expand=True, padx=5, pady=3)

        # 模型说明
        desc_label = ttk.Label(zhipu_card, 
                              text="• glm-4.5-air：文本生成性价比之王，速度快，成本低，适合批量写文章。\n• glm-4-flash：完全免费，适合轻度使用，但高峰期可能不稳定。\n• glm-4-plus：能力最强，适合对质量要求极高的场景，价格较高。",
                              foreground="#666666", justify=tk.LEFT, font=('微软雅黑', 8))
        desc_label.pack(fill='x', pady=(10, 0), padx=0, anchor=tk.W)

        # 高级设置卡片
        advanced_card = ttk.LabelFrame(config_container, text="高级设置", padding=12)
        advanced_card.pack(fill='x', pady=(0, 10))

        # 语言和分类设置
        lang_cat_frame = ttk.Frame(advanced_card)
        lang_cat_frame.pack(fill='x')

        # 语言设置
        lang_frame = ttk.Frame(lang_cat_frame)
        lang_frame.pack(side=tk.LEFT, fill='x', expand=True, padx=(0, 10))
        ttk.Label(lang_frame, text="文章语种:", width=10, font=('微软雅黑', 9, 'bold')).pack(side=tk.LEFT, pady=3)
        lang_combo = ttk.Combobox(lang_frame, textvariable=self.selected_language, 
                                  values=["中文", "英文", "日文", "韩文", "法文", "德文"], 
                                  state='readonly', font=('微软雅黑', 9))
        lang_combo.pack(side=tk.LEFT, padx=5, pady=3)

        # 分类设置
        cat_frame = ttk.Frame(lang_cat_frame)
        cat_frame.pack(side=tk.LEFT, fill='x', expand=True)
        ttk.Label(cat_frame, text="默认分类:", width=10, font=('微软雅黑', 9, 'bold')).pack(side=tk.LEFT, pady=3)
        self.category_combo = ttk.Combobox(cat_frame, textvariable=self.selected_category, state='readonly', font=('微软雅黑', 9))
        self.category_combo.pack(side=tk.LEFT, fill='x', expand=True, padx=5, pady=3)
        refresh_btn = ttk.Button(cat_frame, text="刷新分类", command=self.load_categories, width=8)
        refresh_btn.pack(side=tk.LEFT, padx=5, pady=3)

        # 自动保存设置
        auto_frame = ttk.Frame(advanced_card)
        auto_frame.pack(fill='x', pady=(10, 0))
        ttk.Checkbutton(auto_frame, text="修改后自动保存配置", variable=self.auto_save, style='TCheckbutton').pack(anchor=tk.W, pady=3)

        # 操作按钮区域
        btn_frame = ttk.Frame(config_container)
        btn_frame.pack(fill='x', pady=10)

        # 按钮容器，居中显示
        btn_container = ttk.Frame(btn_frame)
        btn_container.pack(anchor=tk.CENTER)

        test_btn = ttk.Button(btn_container, text="测试连接", command=self.test_connection, width=12)
        test_btn.pack(side=tk.LEFT, padx=10, pady=3)

        save_btn = ttk.Button(btn_container, text="保存配置", command=self.save_all_config, width=12)
        save_btn.pack(side=tk.LEFT, padx=10, pady=3)

        load_btn = ttk.Button(btn_container, text="加载配置", command=self.load_all_config, width=12)
        load_btn.pack(side=tk.LEFT, padx=10, pady=3)

        # 状态栏
        self.status_var = tk.StringVar(value="就绪")
        status_bar = ttk.Label(config_container, textvariable=self.status_var, relief=tk.SUNKEN, font=('微软雅黑', 8), foreground='#666666')
        status_bar.pack(fill='x', pady=(5, 0))

    # ---------------------------- 生成文章选项卡 ----------------------------
    def setup_generate_tab(self, parent):
        # 创建主框架
        main_frame = ttk.Frame(parent)
        main_frame.pack(fill='both', expand=True)

        # 控制区域
        control_frame = ttk.Frame(main_frame)
        control_frame.pack(fill='x', pady=(0, 10))

        # 关键词卡片
        kw_card = ttk.LabelFrame(control_frame, text="文章关键词", padding=12)
        kw_card.pack(fill='x', pady=(0, 10))
        ttk.Label(kw_card, text="多个关键词用逗号分隔，将加密保存", foreground="#999999", font=('微软雅黑', 8)).pack(anchor=tk.W, pady=(0, 5))
        self.keyword_entry = ttk.Entry(kw_card, textvariable=self.keywords, font=('微软雅黑', 9))
        self.keyword_entry.pack(fill='x', pady=3, ipady=3)
        self.keywords.trace_add('write', lambda *args: self.auto_save_config())

        # 参数设置卡片
        param_card = ttk.LabelFrame(control_frame, text="生成参数", padding=12)
        param_card.pack(fill='x')

        param_grid = ttk.Frame(param_card)
        param_grid.pack(fill='x')

        # 生成数量
        count_frame = ttk.Frame(param_grid)
        count_frame.pack(side=tk.LEFT, fill='x', expand=True, padx=(0, 10))
        ttk.Label(count_frame, text="生成数量:", width=10, font=('微软雅黑', 9, 'bold')).pack(side=tk.LEFT, pady=3)
        count_spin = ttk.Spinbox(count_frame, from_=1, to=20, textvariable=self.article_count, font=('微软雅黑', 9))
        count_spin.pack(side=tk.LEFT, padx=5, pady=3)
        self.article_count.trace_add('write', lambda *args: self.auto_save_config())

        # 语种选择
        lang_frame = ttk.Frame(param_grid)
        lang_frame.pack(side=tk.LEFT, fill='x', expand=True, padx=(0, 10))
        ttk.Label(lang_frame, text="文章语种:", width=10, font=('微软雅黑', 9, 'bold')).pack(side=tk.LEFT, pady=3)
        lang_cb = ttk.Combobox(lang_frame, textvariable=self.selected_language, 
                               values=["中文","英文","日文","韩文","法文","德文"], state='readonly', font=('微软雅黑', 9))
        lang_cb.pack(side=tk.LEFT, padx=5, pady=3)

        # 分类选择
        cat_frame = ttk.Frame(param_grid)
        cat_frame.pack(side=tk.LEFT, fill='x', expand=True)
        ttk.Label(cat_frame, text="文章分类:", width=10, font=('微软雅黑', 9, 'bold')).pack(side=tk.LEFT, pady=3)
        cat_cb = ttk.Combobox(cat_frame, textvariable=self.selected_category, 
                              values=self.category_list, state='readonly', font=('微软雅黑', 9))
        cat_cb.pack(side=tk.LEFT, fill='x', expand=True, padx=5, pady=3)

        # 操作按钮区域
        btn_frame = ttk.Frame(main_frame)
        btn_frame.pack(fill='x', pady=(0, 10))

        # 按钮容器，居中显示
        btn_container = ttk.Frame(btn_frame)
        btn_container.pack(anchor=tk.CENTER)

        self.generate_btn = ttk.Button(btn_container, text="生成文章", command=self.generate_articles, width=12)
        self.generate_btn.pack(side=tk.LEFT, padx=10, pady=3)

        self.publish_selected_btn = ttk.Button(btn_container, text="发布选中文章", command=self.publish_selected_articles, width=12)
        self.publish_selected_btn.pack(side=tk.LEFT, padx=10, pady=3)

        clear_btn = ttk.Button(btn_container, text="清空全部", command=self.clear_all_articles, width=12)
        clear_btn.pack(side=tk.LEFT, padx=10, pady=3)

        # 文章列表与预览区域
        preview_card = ttk.LabelFrame(main_frame, text="生成的文章列表（双击预览）", padding=12)
        preview_card.pack(fill='both', expand=True)

        # 左右分屏
        paned = ttk.PanedWindow(preview_card, orient=tk.HORIZONTAL)
        paned.pack(fill='both', expand=True)

        # 左侧列表
        left_frame = ttk.Frame(paned)
        paned.add(left_frame, weight=1)

        # 列表控制按钮
        list_control = ttk.Frame(left_frame)
        list_control.pack(fill='x', pady=(0, 5))

        control_container = ttk.Frame(list_control)
        control_container.pack(anchor=tk.W)

        select_all_btn = ttk.Button(control_container, text="全选", command=self.select_all_articles, width=8)
        select_all_btn.pack(side=tk.LEFT, padx=3, pady=3)

        deselect_all_btn = ttk.Button(control_container, text="全不选", command=self.deselect_all_articles, width=8)
        deselect_all_btn.pack(side=tk.LEFT, padx=3, pady=3)

        # 使用Treeview显示文章列表
        columns = ("选择", "序号", "标题")
        self.article_tree = ttk.Treeview(left_frame, columns=columns, show="headings")

        # 设置Treeview样式
        self.article_tree.heading("选择", text="✓", anchor='center')
        self.article_tree.heading("序号", text="#", anchor='center')
        self.article_tree.heading("标题", text="文章标题", anchor='w')

        self.article_tree.column("选择", width=50, anchor='center')
        self.article_tree.column("序号", width=60, anchor='center')
        self.article_tree.column("标题", width=250, anchor='w')

        # 添加滚动条
        tree_scroll = ttk.Scrollbar(left_frame, orient=tk.VERTICAL, command=self.article_tree.yview)
        tree_scroll.pack(side=tk.RIGHT, fill=tk.Y)
        self.article_tree.configure(yscrollcommand=tree_scroll.set)

        self.article_tree.pack(fill='both', expand=True)

        # 绑定选择事件
        self.article_tree.bind("&lt;ButtonRelease-1&gt;", self.on_article_select)
        self.article_tree.bind("&lt;Double-1&gt;", self.on_article_double_click)

        # 右侧内容预览
        right_frame = ttk.Frame(paned)
        paned.add(right_frame, weight=2)

        preview_label = ttk.Label(right_frame, text="文章内容预览（支持Markdown）:", font=('微软雅黑', 10, 'bold'))
        preview_label.pack(anchor=tk.W, pady=(0, 5))

        # 预览文本框
        self.preview_text = scrolledtext.ScrolledText(right_frame, wrap=tk.WORD, font=('微软雅黑', 9))
        self.preview_text.pack(fill='both', expand=True)

        # 更新分类列表联动
        self.generate_category = cat_cb

    def setup_publish_tab(self, parent):
        # 创建主框架
        main_frame = ttk.Frame(parent)
        main_frame.pack(fill='both', expand=True)

        # 发布记录卡片
        log_card = ttk.LabelFrame(main_frame, text="发布日志", padding=12)
        log_card.pack(fill='both', expand=True)

        # 日志文本框
        self.publish_log = scrolledtext.ScrolledText(log_card, wrap=tk.WORD, font=('微软雅黑', 9))
        self.publish_log.pack(fill='both', expand=True)

        # 按钮区域
        btn_frame = ttk.Frame(main_frame)
        btn_frame.pack(fill='x', pady=10)

        # 按钮容器，右对齐
        btn_container = ttk.Frame(btn_frame)
        btn_container.pack(anchor=tk.E)

        clear_btn = ttk.Button(btn_container, text="清空日志", command=self.clear_log, width=12)
        clear_btn.pack(side=tk.RIGHT, padx=3, pady=3)

    # ---------------------------- 导航功能 ----------------------------
    def show_config_tab(self):
        # 隐藏所有容器
        self.config_frame.pack_forget()
        self.generate_frame.pack_forget()
        self.publish_frame.pack_forget()

        # 显示配置管理
        self.config_frame.pack(fill='both', expand=True)

        # 更新标题
        self.current_title.set("配置管理")

        # 更新导航按钮状态
        self.update_nav_buttons("配置管理")

    def show_generate_tab(self):
        # 隐藏所有容器
        self.config_frame.pack_forget()
        self.generate_frame.pack_forget()
        self.publish_frame.pack_forget()

        # 显示文章生成
        self.generate_frame.pack(fill='both', expand=True)

        # 更新标题
        self.current_title.set("文章生成")

        # 更新导航按钮状态
        self.update_nav_buttons("文章生成")

    def show_publish_tab(self):
        # 隐藏所有容器
        self.config_frame.pack_forget()
        self.generate_frame.pack_forget()
        self.publish_frame.pack_forget()

        # 显示发布记录
        self.publish_frame.pack(fill='both', expand=True)

        # 更新标题
        self.current_title.set("发布记录")

        # 更新导航按钮状态
        self.update_nav_buttons("发布记录")

    def update_nav_buttons(self, active_tab):
        # 更新导航按钮样式
        for text, btn in self.nav_buttons.items():
            if text == active_tab:
                btn.configure(style='TButton')
            else:
                btn.configure(style='Nav.TButton')

    # ---------------------------- 辅助方法 ----------------------------
    def auto_save_config(self):
        if self.auto_save.get():
            self.save_all_config(silent=True)

    def save_all_config(self, silent=False):
        # 提取实际模型名称（去掉括号描述）
        model_full = self.selected_model.get()
        if "(" in model_full:
            model_name = model_full.split("(")[0].strip()
        else:
            model_name = model_full
        config = {
            'emlog_url': self.emlog_url.get(),
            'api_key': self.api_key.get(),
            'zhipu_api_key': self.zhipu_api_key.get(),
            'selected_language': self.selected_language.get(),
            'article_count': self.article_count.get(),
            'keywords': self.keywords.get(),
            'selected_category': self.selected_category.get(),
            'selected_model': model_name
        }
        success, message = self.config_manager.save_config(config)
        if success:
            if not silent:
                self.status_var.set("配置已加密保存")
                self._log_message("配置已加密保存")
                messagebox.showinfo("成功", "配置已加密保存！")
        else:
            if not silent:
                self.status_var.set(f"保存失败: {message}")
                messagebox.showerror("错误", f"保存失败: {message}")

    def load_all_config(self):
        config, msg = self.config_manager.load_config()
        if config:
            self.emlog_url.set(config.get('emlog_url', ''))
            self.api_key.set(config.get('api_key', ''))
            self.zhipu_api_key.set(config.get('zhipu_api_key', ''))
            self.selected_language.set(config.get('selected_language', '中文'))
            self.article_count.set(config.get('article_count', 1))
            self.keywords.set(config.get('keywords', ''))
            self.selected_category.set(config.get('selected_category', ''))
            saved_model = config.get('selected_model', 'glm-4.5-air')
            for opt in ["glm-4.5-air (推荐·性价比最高·快速)", "glm-4-flash (免费·速度最快·质量略低)", "glm-4-plus (最强·高质量·稍贵)"]:
                if saved_model in opt:
                    self.selected_model.set(opt)
                    break
            else:
                self.selected_model.set("glm-4.5-air (推荐·性价比最高·快速)")
            self.status_var.set("配置已加载")
            self._log_message("已加载加密配置")
            if self.emlog_url.get():
                self.load_categories()
        else:
            self.status_var.set(msg)
            if msg != "配置文件不存在":
                self._log_message(msg)

    def delete_config_file(self):
        if messagebox.askyesno("确认", "删除配置文件后无法恢复，确定吗？"):
            success, msg = self.config_manager.delete_config()
            if success:
                self.status_var.set("配置文件已删除")
                self._log_message("配置文件已删除")
                messagebox.showinfo("成功", "配置文件已删除！")
            else:
                messagebox.showerror("错误", f"删除失败: {msg}")

    def show_about(self):
        about_text = """EMLOG 智能文章发布工具 v2.0 xjrx.net

功能特性：
- 支持多语种文章生成（中/英/日/韩/法/德）
- 基于关键词的智能内容生成
- 配置加密存储，保护敏感信息
- 一键发布到EMLOG
- 全文预览和批量发布
- 增量生成：保留已生成文章，方便累积发布
- 支持选择配置文件

技术支持：基于智谱AI GLM模型"""
        messagebox.showinfo("关于", about_text)

    def open_config_file(self):
        """打开配置文件对话框"""
        from tkinter import filedialog
        file_path = filedialog.askopenfilename(
            title="选择配置文件",
            filetypes=[("加密配置文件", "*.enc"), ("所有文件", "*.*")],
            initialdir=os.getcwd()
        )
        if file_path:
            try:
                self.config_manager.set_config_file(file_path)
                self.load_all_config()
                self.status_var.set(f"已打开配置文件: {os.path.basename(file_path)}")
                self._log_message(f"已打开配置文件: {file_path}")
            except Exception as e:
                messagebox.showerror("错误", f"打开配置文件失败: {str(e)}")

    def load_categories(self):
        if not self.emlog_url.get():
            self.status_var.set("请先配置EMLOG网站地址")
            return
        def load():
            try:
                self.status_var.set("正在加载分类...")
                url = f"{self.emlog_url.get()}/?rest-api=sort_list"
                resp = requests.get(url, timeout=10)
                if resp.status_code == 200:
                    data = resp.json()
                    if data.get('code') == 0:
                        categories = []
                        self.categories.clear()
                        sorts = data['data'].get('sorts', [])
                        for sort in sorts:
                            name = sort.get('sortname', '未命名')
                            sid = sort.get('sid', 0)
                            categories.append(name)
                            self.categories[name] = sid
                            for child in sort.get('children', []):
                                cname = child.get('sortname', '未命名')
                                cid = child.get('sid', 0)
                                display = f"  └─ {cname}"
                                categories.append(display)
                                self.categories[display] = cid
                        if categories:
                            self.category_list = categories
                            self.category_combo['values'] = categories
                            if hasattr(self, 'generate_category'):
                                self.generate_category['values'] = categories
                            if self.selected_category.get() not in categories:
                                self.selected_category.set(categories[0])
                            self.status_var.set(f"加载 {len(categories)} 个分类")
                            self._log_message(f"加载 {len(categories)} 个分类")
                        else:
                            self.status_var.set("未找到分类")
                    else:
                        self.status_var.set(f"API错误: {data.get('msg')}")
                else:
                    self.status_var.set(f"HTTP错误: {resp.status_code}")
            except Exception as e:
                self.status_var.set(f"加载分类失败: {str(e)}")
        threading.Thread(target=load, daemon=True).start()

    def test_connection(self):
        if not self.emlog_url.get():
            messagebox.showwarning("警告", "请先填写网站地址")
            return
        def test():
            try:
                self.status_var.set("测试连接中...")
                url = f"{self.emlog_url.get()}/?rest-api=sort_list"
                resp = requests.get(url, timeout=10)
                if resp.status_code == 200 and resp.json().get('code') == 0:
                    self.status_var.set("连接成功！")
                    messagebox.showinfo("成功", "连接成功！")
                    self.load_categories()
                else:
                    msg = "连接失败，请检查URL和API密钥"
                    self.status_var.set(msg)
                    messagebox.showerror("错误", msg)
            except Exception as e:
                self.status_var.set(f"连接异常: {str(e)}")
                messagebox.showerror("错误", f"连接异常: {str(e)}")
        threading.Thread(target=test, daemon=True).start()

    # ---------------------------- 生成文章核心（修改标题生成逻辑） ----------------------------
    def generate_articles(self):
        if not self.zhipu_api_key.get():
            messagebox.showwarning("警告", "请先配置智谱AI API密钥")
            return
        if not self.category_list:
            messagebox.showwarning("警告", "请先加载分类列表（点击配置页的刷新分类）")
            return
        kw = self.keywords.get().strip()
        if not kw:
            if not messagebox.askyesno("提示", "未输入关键词，将生成随机主题文章，是否继续？"):
                return

        self.generate_btn.config(state='disabled', text='生成中...')
        threading.Thread(target=self._generate_articles_thread, daemon=True).start()

    def _generate_articles_thread(self):
        try:
            count = self.article_count.get()
            language = self.selected_language.get()
            keywords = self.keywords.get().strip()
            # 获取选择的模型名
            model_full = self.selected_model.get()
            if "glm-4.5-air" in model_full:
                api_model = "glm-4.5-air"
            elif "glm-4-flash" in model_full:
                api_model = "glm-4-flash"
            elif "glm-4-plus" in model_full:
                api_model = "glm-4-plus"
            else:
                api_model = "glm-4.5-air"

            # 为每种语言创建完整的prompt模板
            language_templates = {
                "中文": {
                    "title": "请根据核心关键词'{keywords}'生成一个简洁、吸引人的博客文章标题。要求：\n1. 标题长度10-20字；\n2. 不要包含逗号、顿号等标点；\n3. 不要出现'关于...的文章'格式；\n4. 直接输出标题，不要带引号或额外解释。",
                    "content": "根据标题'{title}'和关键词'{keywords}'生成一篇800-1000字的博客文章。要求：结构清晰（引言、正文3小节、结语），使用Markdown格式（标题、列表），内容有价值。",
                    "title_random": "生成一个简洁的博客文章标题，10-20字，直接输出标题。"
                },
                "英文": {
                    "title": "Please generate a concise and attractive blog post title based on the core keyword '{keywords}'. Requirements:\n1. Title length 10-20 words;\n2. Do not include commas or other punctuation;\n3. Do not use the format 'Article about...';\n4. Output the title directly without quotes or additional explanation.",
                    "content": "Based on the title '{title}' and keywords '{keywords}', generate an 800-1000 word blog post. Requirements: Clear structure (introduction, 3 body sections, conclusion), use Markdown format (headings, lists), and valuable content.",
                    "title_random": "Generate a concise blog post title, 10-20 words, output the title directly."
                },
                "日文": {
                    "title": "コアキーワード'{keywords}'に基づいて、簡潔で魅力的なブログ記事のタイトルを生成してください。要件：\n1. タイトルの長さは10-20文字；\n2. コンマや句読点などの句読点を含めない；\n3. '...についての記事'形式を使用しない；\n4. 引用符や追加の説明なしに、タイトルを直接出力する。",
                    "content": "タイトル'{title}'とキーワード'{keywords}'に基づいて、800-1000語のブログ記事を生成してください。要件：構造が明確（序論、本文3セクション、結論）、Markdown形式（見出し、リスト）を使用、価値のある内容。",
                    "title_random": "簡潔なブログ記事のタイトルを生成してください、10-20文字、タイトルを直接出力してください。"
                },
                "韩文": {
                    "title": "핵심 키워드'{keywords}'를 바탕으로 간결하고 매력적인 블로그 글 제목을 생성해 주세요. 요구 사항:\n1. 제목 길이 10-20자；\n2. 쉼표나 다른 구두점을 포함하지 않음；\n3. '...에 관한 글' 형식을 사용하지 않음；\n4. 따옴표나 추가 설명 없이 제목을 직접 출력하세요。",
                    "content": "제목'{title}'과 키워드'{keywords}'를 바탕으로 800-1000단어의 블로그 글을 생성하세요. 요구 사항: 명확한 구조（서론, 본문 3단락, 결론）, Markdown 형식（제목, 목록）사용, 가치 있는 내용。",
                    "title_random": "간결한 블로그 글 제목을 생성하세요, 10-20자, 제목을 직접 출력하세요。"
                },
                "法文": {
                    "title": "Veuillez générer un titre de blog concis et attractif basé sur le mot-clé principal '{keywords}'. Exigences:\n1. Longueur du titre : 10-20 mots ;\n2. Ne pas inclure de virgules ou d'autres ponctuations ;\n3. Ne pas utiliser le format 'Article sur...' ;\n4. Sortir directement le titre sans guillemets ni explication supplémentaire.",
                    "content": "Sur la base du titre '{title}' et des mots-clés '{keywords}', générez un article de blog de 800 à 1000 mots. Exigences : Structure claire (introduction, 3 sections de corps, conclusion), utilisation du format Markdown (titres, listes), contenu valable.",
                    "title_random": "Générez un titre de blog concis, 10-20 mots, sortez directement le titre."
                },
                "德文": {
                    "title": "Bitte generieren Sie einen prägnanten und attraktiven Blogbeitragstitel basierend auf dem Kernschlüsselwort '{keywords}'. Anforderungen:\n1. Titel Länge 10-20 Wörter;\n2. Keine Kommas oder andere Interpunktion einbeziehen;\n3. Nicht das Format 'Artikel über...' verwenden;\n4. Geben Sie den Titel direkt ohne Anführungszeichen oder zusätzliche Erklärung aus.",
                    "content": "Basierend auf dem Titel '{title}' und den Schlüsselwörtern '{keywords}', generieren Sie einen 800-1000 Wörter langen Blogbeitrag. Anforderungen: Klare Struktur (Einführung, 3 Hauptabschnitte, Fazit), Verwendung des Markdown-Formats (Überschriften, Listen), wertvolle Inhalte.",
                    "title_random": "Generieren Sie einen prägnanten Blogbeitragstitel, 10-20 Wörter, geben Sie den Titel direkt aus."
                }
            }
            templates = language_templates.get(language, language_templates["中文"])

            new_articles = []
            for i in range(count):
                self.root.after(0, lambda i=i: self.status_var.set(f"生成第 {i+1}/{count} 篇..."))

                # 确定本次关键词（内容生成时可以使用完整关键词列表）
                if keywords:
                    kw_list = [k.strip() for k in keywords.split(',')]
                    if len(kw_list) &gt; 1 and count &gt; 1:
                        import random
                        selected = random.sample(kw_list, min(2, len(kw_list)))
                        current_kw = ', '.join(selected)
                    else:
                        current_kw = keywords
                else:
                    current_kw = "随机主题"

                # ========== 关键修改：标题只使用第一个关键词 ==========
                if current_kw and current_kw != "随机主题":
                    title_keyword = current_kw.split(',')[0].strip()
                else:
                    title_keyword = "精彩分享"
                # =================================================

                # 生成标题
                title = self._call_ai("title", language, title_keyword, api_model)
                if not title or len(title) &lt; 2:
                    # 备用标题：避免出现“关于...的文章 1”格式
                    if language == "英文":
                        title = f"The Charm of {title_keyword}" if title_keyword != "随机主题" else "Wonderful Content Sharing"
                    elif language == "日文":
                        title = f"{title_keyword}の魅力" if title_keyword != "随机主题" else "素晴らしいコンテンツ共有"
                    elif language == "韩文":
                        title = f"{title_keyword}의 매력" if title_keyword != "随机主题" else "멋진 콘텐츠 공유"
                    elif language == "法文":
                        title = f"Le Charme de {title_keyword}" if title_keyword != "随机主题" else "Partage de Contenu Merveilleux"
                    elif language == "德文":
                        title = f"Der Charme von {title_keyword}" if title_keyword != "随机主题" else "Wunderbare Inhaltsfreigabe"
                    else:
                        title = f"{title_keyword}的魅力" if title_keyword != "随机主题" else "精彩内容分享"

                # 生成内容（内容生成仍使用完整关键词，使文章更丰富）
                content = self._call_ai("content", language, current_kw, api_model, title)
                if not content:
                    content = f"# {title}\n\n## 引言\n\n自动生成内容失败，请检查网络或API密钥。"

                # 简单摘要
                excerpt = content[:200] + "..." if len(content) &gt; 200 else content

                new_articles.append({
                    'title': title,
                    'content': content,
                    'excerpt': excerpt,
                    'keywords': current_kw,
                    'selected': True
                })

            # 累积到总列表
            self.generated_articles.extend(new_articles)
            # 更新界面
            self.root.after(0, self.refresh_article_tree)
            self.root.after(0, lambda: self.status_var.set(f"生成完成！当前共 {len(self.generated_articles)} 篇文章"))
            self.root.after(0, lambda: self.generate_btn.config(state='normal', text='生成文章'))
            self.root.after(0, lambda: self.publish_selected_btn.config(state='normal'))
        except Exception as e:
            self.root.after(0, lambda: messagebox.showerror("错误", f"生成失败: {str(e)}"))
            self.root.after(0, lambda: self.status_var.set(f"生成失败: {str(e)}"))
            self.root.after(0, lambda: self.generate_btn.config(state='normal', text='生成文章'))

    def _call_ai(self, type_, language, keywords, model, title=None):
        """调用智谱AI API，type: 'title' 或 'content'"""
        try:
            # 获取对应语言的模板
            language_templates = {
                "中文": {
                    "title": "请根据核心关键词'{keywords}'生成一个简洁、吸引人的博客文章标题。要求：\n1. 标题长度10-20字；\n2. 不要包含逗号、顿号等标点；\n3. 不要出现'关于...的文章'格式；\n4. 直接输出标题，不要带引号或额外解释。",
                    "content": "根据标题'{title}'和关键词'{keywords}'生成一篇800-1000字的博客文章。要求：结构清晰（引言、正文3小节、结语），使用Markdown格式（标题、列表），内容有价值。",
                    "title_random": "生成一个简洁的博客文章标题，10-20字，直接输出标题。"
                },
                "英文": {
                    "title": "Please generate a concise and attractive blog post title based on the core keyword '{keywords}'. Requirements:\n1. Title length 10-20 words;\n2. Do not include commas or other punctuation;\n3. Do not use the format 'Article about...';\n4. Output the title directly without quotes or additional explanation.",
                    "content": "Based on the title '{title}' and keywords '{keywords}', generate an 800-1000 word blog post. Requirements: Clear structure (introduction, 3 body sections, conclusion), use Markdown format (headings, lists), and valuable content.",
                    "title_random": "Generate a concise blog post title, 10-20 words, output the title directly."
                },
                "日文": {
                    "title": "コアキーワード'{keywords}'に基づいて、簡潔で魅力的なブログ記事のタイトルを生成してください。要件：\n1. タイトルの長さは10-20文字；\n2. コンマや句読点などの句読点を含めない；\n3. '...についての記事'形式を使用しない；\n4. 引用符や追加の説明なしに、タイトルを直接出力する。",
                    "content": "タイトル'{title}'とキーワード'{keywords}'に基づいて、800-1000語のブログ記事を生成してください。要件：構造が明確（序論、本文3セクション、結論）、Markdown形式（見出し、リスト）を使用、価値のある内容。",
                    "title_random": "簡潔なブログ記事のタイトルを生成してください、10-20文字、タイトルを直接出力してください。"
                },
                "韩文": {
                    "title": "핵심 키워드'{keywords}'를 바탕으로 간결하고 매력적인 블로그 글 제목을 생성해 주세요. 요구 사항:\n1. 제목 길이 10-20자；\n2. 쉼표나 다른 구두점을 포함하지 않음；\n3. '...에 관한 글' 형식을 사용하지 않음；\n4. 따옴표나 추가 설명 없이 제목을 직접 출력하세요。",
                    "content": "제목'{title}'과 키워드'{keywords}'를 바탕으로 800-1000단어의 블로그 글을 생성하세요. 요구 사항: 명확한 구조（서론, 본문 3단락, 결론）, Markdown 형식（제목, 목록）사용, 가치 있는 내용。",
                    "title_random": "간결한 블로그 글 제목을 생성하세요, 10-20자, 제목을 직접 출력하세요。"
                },
                "法文": {
                    "title": "Veuillez générer un titre de blog concis et attractif basé sur le mot-clé principal '{keywords}'. Exigences:\n1. Longueur du titre : 10-20 mots ;\n2. Ne pas inclure de virgules ou d'autres ponctuations ;\n3. Ne pas utiliser le format 'Article sur...' ;\n4. Sortir directement le titre sans guillemets ni explication supplémentaire.",
                    "content": "Sur la base du titre '{title}' et des mots-clés '{keywords}', générez un article de blog de 800 à 1000 mots. Exigences : Structure claire (introduction, 3 sections de corps, conclusion), utilisation du format Markdown (titres, listes), contenu valable.",
                    "title_random": "Générez un titre de blog concis, 10-20 mots, sortez directement le titre."
                },
                "德文": {
                    "title": "Bitte generieren Sie einen prägnanten und attraktiven Blogbeitragstitel basierend auf dem Kernschlüsselwort '{keywords}'. Anforderungen:\n1. Titel Länge 10-20 Wörter;\n2. Keine Kommas oder andere Interpunktion einbeziehen;\n3. Nicht das Format 'Artikel über...' verwenden;\n4. Geben Sie den Titel direkt ohne Anführungszeichen oder zusätzliche Erklärung aus.",
                    "content": "Basierend auf dem Titel '{title}' und den Schlüsselwörtern '{keywords}', generieren Sie einen 800-1000 Wörter langen Blogbeitrag. Anforderungen: Klare Struktur (Einführung, 3 Hauptabschnitte, Fazit), Verwendung des Markdown-Formats (Überschriften, Listen), wertvolle Inhalte.",
                    "title_random": "Generieren Sie einen prägnanten Blogbeitragstitel, 10-20 Wörter, geben Sie den Titel direkt aus."
                }
            }
            templates = language_templates.get(language, language_templates["中文"])

            headers = {
                'Authorization': f'Bearer {self.zhipu_api_key.get()}',
                'Content-Type': 'application/json'
            }
            if type_ == 'title':
                if keywords and keywords != "随机主题":
                    prompt = templates["title"].format(keywords=keywords)
                else:
                    prompt = templates["title_random"]
            else:  # content
                prompt = templates["content"].format(title=title, keywords=keywords)

            data = {
                "model": model,
                "messages": [{"role": "user", "content": prompt}],
                "temperature": 0.7,
                "max_tokens": 100 if type_ == 'title' else 2000
            }
            resp = requests.post('https://open.bigmodel.cn/api/paas/v4/chat/completions', 
                                 headers=headers, json=data, timeout=60)
            if resp.status_code == 200:
                result = resp.json()
                raw = result['choices'][0]['message']['content'].strip()
                if type_ == 'title':
                    # 后处理清洗
                    raw = raw.strip('"\'“”‘’')
                    # 如果标题包含逗号或顿号，只取第一部分
                    for sep in [',', '，', '、']:
                        if sep in raw:
                            raw = raw.split(sep)[0].strip()
                            break
                    # 去除“关于...的文章”模板
                    if raw.startswith("关于") and "的文章" in raw:
                        raw = raw.replace("关于", "").replace("的文章", "").strip()
                    # 限制最大长度40字符
                    if len(raw) &gt; 40:
                        raw = raw[:37] + "..."
                    # 如果清洗后为空，使用备用
                    if not raw:
                        if language == "英文":
                            raw = f"{keywords} Journey" if keywords != "随机主题" else "Wonderful Sharing"
                        elif language == "日文":
                            raw = f"{keywords}の旅" if keywords != "随机主题" else "素晴らしい共有"
                        elif language == "韩文":
                            raw = f"{keywords}여행" if keywords != "随机主题" else "멋진 공유"
                        elif language == "法文":
                            raw = f"Voyage {keywords}" if keywords != "随机主题" else "Partage merveilleux"
                        elif language == "德文":
                            raw = f"{keywords} Reise" if keywords != "随机主题" else "Wunderbare Freigabe"
                        else:
                            raw = f"{keywords}之旅" if keywords != "随机主题" else "精彩分享"
                    return raw
                else:
                    return raw
            else:
                self._log_message(f"API错误({resp.status_code}): {resp.text}")
                return None
        except Exception as e:
            self._log_message(f"AI调用异常: {str(e)}")
            return None

    def refresh_article_tree(self):
        """刷新文章列表（Treeview）"""
        for row in self.article_tree.get_children():
            self.article_tree.delete(row)
        for idx, art in enumerate(self.generated_articles, start=1):
            check = "✓" if art.get('selected', True) else "□"
            title_short = art['title'][:55] + "..." if len(art['title']) &gt; 55 else art['title']
            self.article_tree.insert("", "end", values=(check, idx, title_short), iid=idx)
        self._sync_tree_selection()

    def _sync_tree_selection(self):
        """根据存储的selected状态更新树形控件显示"""
        for idx, art in enumerate(self.generated_articles, start=1):
            check = "✓" if art.get('selected', True) else "□"
            self.article_tree.set(idx, column="选择", value=check)

    def on_article_select(self, event):
        """点击选择框切换选中状态"""
        selected = self.article_tree.selection()
        if not selected:
            return
        item = selected[0]
        idx = int(item) - 1
        if 0 &lt;= idx &lt; len(self.generated_articles):
            # 切换选中状态
            self.generated_articles[idx]['selected'] = not self.generated_articles[idx].get('selected', True)
            self._sync_tree_selection()
            # 显示内容
            self.show_article_preview(idx)

    def on_article_double_click(self, event):
        """双击显示完整文章"""
        selected = self.article_tree.selection()
        if selected:
            idx = int(selected[0]) - 1
            self.show_article_preview(idx)

    def show_article_preview(self, idx):
        art = self.generated_articles[idx]
        self.preview_text.delete(1.0, tk.END)
        preview = f"标题：{art['title']}\n关键词：{art['keywords']}\n{'='*50}\n\n{art['content']}"
        self.preview_text.insert(1.0, preview)

    def select_all_articles(self):
        for art in self.generated_articles:
            art['selected'] = True
        self._sync_tree_selection()

    def deselect_all_articles(self):
        for art in self.generated_articles:
            art['selected'] = False
        self._sync_tree_selection()

    def clear_all_articles(self):
        if messagebox.askyesno("清空", "确定清空所有已生成的文章吗？"):
            self.generated_articles.clear()
            self.refresh_article_tree()
            self.preview_text.delete(1.0, tk.END)
            self.status_var.set("已清空所有文章")

    def publish_selected_articles(self):
        if not self.generated_articles:
            messagebox.showwarning("警告", "没有文章可发布")
            return
        selected = [a for a in self.generated_articles if a.get('selected', True)]
        if not selected:
            messagebox.showwarning("警告", "请先勾选要发布的文章")
            return
        if messagebox.askyesno("确认", f"确定发布选中的 {len(selected)} 篇文章吗？"):
            self.publish_articles(selected)

    def publish_articles(self, articles):
        if not self.emlog_url.get() or not self.api_key.get():
            messagebox.showwarning("警告", "请先配置EMLOG网站地址和API密钥")
            return
        def publish():
            success = 0
            cat_name = self.selected_category.get()
            cat_id = self.categories.get(cat_name, 0)
            self._log_message(f"开始发布 {len(articles)} 篇文章，分类：{cat_name}(ID:{cat_id})")
            for art in articles:
                try:
                    req_time = int(time.time())
                    sign = hashlib.md5(f"{req_time}{self.api_key.get()}".encode()).hexdigest()
                    data = {
                        'title': art['title'],
                        'content': art['content'],
                        'excerpt': art.get('excerpt', ''),
                        'sort_id': cat_id,
                        'draft': 'n',
                        'api_key': self.api_key.get(),
                        'req_sign': sign,
                        'req_time': req_time
                    }
                    url = f"{self.emlog_url.get()}/?rest-api=article_post"
                    resp = requests.post(url, data=data, timeout=30)
                    result = resp.json()
                    if result.get('code') == 0:
                        success += 1
                        self._log_message(f"✓ 发布成功：{art['title']} (ID:{result['data']['article_id']})")
                    else:
                        self._log_message(f"✗ 发布失败：{art['title']} - {result.get('msg')}")
                except Exception as e:
                    self._log_message(f"✗ 异常：{art['title']} - {str(e)}")
            self._log_message(f"发布完成，成功 {success}/{len(articles)}")
            self.status_var.set(f"发布完成，成功 {success}/{len(articles)}")
            messagebox.showinfo("完成", f"成功发布 {success} 篇文章")
        threading.Thread(target=publish, daemon=True).start()

    def clear_log(self):
        self.publish_log.delete(1.0, tk.END)

    def _log_message(self, msg):
        timestamp = datetime.now().strftime("%H:%M:%S")
        self.root.after(0, lambda: self.publish_log.insert(tk.END, f"[{timestamp}] {msg}\n"))
        self.root.after(0, lambda: self.publish_log.see(tk.END))

def main():
    try:
        import cryptography
    except ImportError:
        root = tk.Tk()
        root.withdraw()
        messagebox.showerror("缺少依赖", "请先安装 cryptography 库：\npip install cryptography")
        root.destroy()
        return
    root = tk.Tk()
    app = EmlogPublisher(root)
    root.mainloop()

if __name__ == "__main__":
    main()</code></pre>]]></description>
    <pubDate>Wed, 15 Apr 2026 00:35:21 +0800</pubDate>
    <dc:creator>emer</dc:creator>
    <guid>https://deepsider.cn/post-5.html</guid>
</item>
<item>
    <title>高级 M3U8 提取器（Network 级）</title>
    <link>https://deepsider.cn/post-3.html</link>
    <description><![CDATA[<pre><code class="language-javascript">// ==UserScript==
// @name         高级 M3U8 提取器（Network 级）
// @namespace    https://xjrx.net/
// @version      2.1.0
// @description  劫持 fetch 和 XHR，提取页面中所有 .m3u8 请求（包括开发者工具里才能看到的）
// @author       You
// @match        *://*/*
// @grant        GM_setClipboard
// @run-at       document-start
// ==/UserScript==

(function () {
    'use strict';

    const m3u8Set = new Set();

    function copyText(text) {
        GM_setClipboard(text, 'text');
        showToast('已复制: ' + text);
    }

    function showToast(msg) {
        const toast = document.createElement('div');
        Object.assign(toast.style, {
            position: 'fixed',
            left: '50%', top: '20%',
            transform: 'translateX(-50%)',
            background: 'rgba(0,0,0,0.75)',
            color: '#fff',
            padding: '8px 14px',
            borderRadius: '4px',
            fontSize: '14px',
            zIndex: 9999999,
            transition: 'opacity .3s'
        });
        toast.textContent = msg;
        document.body.appendChild(toast);
        setTimeout(() =&gt; (toast.style.opacity = '0'), 1500);
        setTimeout(() =&gt; toast.remove(), 1800);
    }

    function checkUrl(url) {
        if (url &amp;&amp; /\.m3u8(?:\?.*)?$/i.test(url)) {
            m3u8Set.add(url);
            renderFloatBox();
        }
    }

    // 劫持 fetch
    const originalFetch = window.fetch;
    window.fetch = function (...args) {
        const url = args[0];
        checkUrl(url);
        return originalFetch.apply(this, args);
    };

    // 劫持 XMLHttpRequest
    const originalOpen = XMLHttpRequest.prototype.open;
    XMLHttpRequest.prototype.open = function (method, url) {
        checkUrl(url);
        return originalOpen.apply(this, arguments);
    };

    // 渲染悬浮窗
    let box = null;
    function renderFloatBox() {
        if (box) box.remove();
        box = document.createElement('div');
        box.id = 'm3u8-float-box';
        Object.assign(box.style, {
            position: 'fixed',
            right: '16px', bottom: '16px',
            width: '320px', maxHeight: '400px',
            background: '#1a1a1a',
            boxShadow: '0 4px 15px rgba(0,0,0,.5)',
            borderRadius: '8px',
            fontSize: '14px',
            zIndex: 999999,
            display: 'flex', flexDirection: 'column',
            fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
        });

        // 头部区域
        const head = document.createElement('div');
        Object.assign(head.style, {
            padding: '12px 14px', 
            borderBottom: '1px solid #333',
            background: '#252525',
            borderRadius: '8px 8px 0 0',
            display: 'flex', 
            justifyContent: 'space-between', 
            alignItems: 'center'
        });

        // 左侧：M3U8数量 + 网页标题
        const leftInfo = document.createElement('div');
        leftInfo.style.display = 'flex';
        leftInfo.style.flexDirection = 'column';
        leftInfo.style.gap = '4px';

        // M3U8数量标签
        const countLabel = document.createElement('div');
        countLabel.innerHTML = `&lt;span style="color:#888;"&gt;M3U8&lt;/span&gt; &lt;strong style="color:#4CAF50;"&gt;${m3u8Set.size}&lt;/strong&gt; 条`;
        countLabel.style.fontSize = '13px';

        // 网页标题（可点击复制）
        const pageTitle = document.createElement('div');
        const titleText = document.title || '无标题';
        pageTitle.textContent = `📄 ${titleText.length &gt; 25 ? titleText.substring(0, 25) + '...' : titleText}`;
        pageTitle.title = titleText; // 悬停显示完整标题
        Object.assign(pageTitle.style, {
            fontSize: '12px',
            color: '#aaa',
            cursor: 'pointer',
            maxWidth: '200px',
            whiteSpace: 'nowrap',
            overflow: 'hidden',
            textOverflow: 'ellipsis',
            transition: 'color 0.2s'
        });
        pageTitle.onmouseenter = () =&gt; { pageTitle.style.color = '#4CAF50'; };
        pageTitle.onmouseleave = () =&gt; { pageTitle.style.color = '#aaa'; };
        pageTitle.onclick = () =&gt; {
            if (titleText) {
                copyText(titleText);
            } else {
                showToast('当前页面无标题');
            }
        };

        leftInfo.appendChild(countLabel);
        leftInfo.appendChild(pageTitle);

        // 右侧：关闭按钮
        const close = document.createElement('span');
        close.textContent = '×';
        Object.assign(close.style, {
            color: '#888',
            cursor: 'pointer', 
            fontSize: '22px',
            fontWeight: 'bold',
            lineHeight: '1',
            padding: '0 4px',
            transition: 'color 0.2s'
        });
        close.onmouseenter = () =&gt; { close.style.color = '#ff5555'; };
        close.onmouseleave = () =&gt; { close.style.color = '#888'; };
        close.onclick = () =&gt; box.remove();

        head.appendChild(leftInfo);
        head.appendChild(close);

        // 内容区域
        const body = document.createElement('div');
        Object.assign(body.style, {
            flex: '1', 
            overflowY: 'auto', 
            padding: '10px',
            background: '#1a1a1a'
        });

        // 自定义滚动条样式
        body.style.cssText += `
            scrollbar-width: thin;
            scrollbar-color: #444 #1a1a1a;
        `;
        body.onmouseenter = function() {
            this.style.cssText += `
                scrollbar-width: thin;
                scrollbar-color: #666 #222;
            `;
        };

        if (m3u8Set.size === 0) {
            const emptyTip = document.createElement('div');
            emptyTip.textContent = '暂无 M3U8 链接';
            Object.assign(emptyTip.style, {
                color: '#666',
                textAlign: 'center',
                padding: '30px 0',
                fontSize: '13px'
            });
            body.appendChild(emptyTip);
        } else {
            m3u8Set.forEach(url =&gt; {
                const line = document.createElement('div');
                line.style.marginBottom = '8px';
                line.style.display = 'flex';
                line.style.alignItems = 'center';

                const input = document.createElement('input');
                input.type = 'text';
                input.value = url;
                input.readOnly = true;
                Object.assign(input.style, {
                    flex: '1',
                    padding: '6px 8px',
                    border: '1px solid #333',
                    borderRadius: '4px',
                    fontSize: '11px',
                    background: '#252525',
                    color: '#e0e0e0',
                    outline: 'none'
                });

                const btn = document.createElement('button');
                btn.textContent = '复制';
                Object.assign(btn.style, {
                    width: '52px', 
                    padding: '6px 0',
                    marginLeft: '6px',
                    fontSize: '12px', 
                    cursor: 'pointer',
                    background: 'linear-gradient(135deg, #4CAF50, #45a049)',
                    color: '#fff',
                    border: 'none',
                    borderRadius: '4px',
                    fontWeight: '500',
                    transition: 'all 0.2s'
                });
                btn.onmouseenter = () =&gt; {
                    btn.style.background = 'linear-gradient(135deg, #5CBF60, #4CAF50)';
                    btn.style.transform = 'scale(1.02)';
                };
                btn.onmouseleave = () =&gt; {
                    btn.style.background = 'linear-gradient(135deg, #4CAF50, #45a049)';
                    btn.style.transform = 'scale(1)';
                };
                btn.onclick = () =&gt; copyText(url);

                line.appendChild(input);
                line.appendChild(btn);
                body.appendChild(line);
            });
        }

        box.appendChild(head);
        box.appendChild(body);
        document.body.appendChild(box);
    }

    // 页面加载后延迟渲染
    if (document.body) {
        renderFloatBox();
    } else {
        document.addEventListener('DOMContentLoaded', renderFloatBox);
    }
})();</code></pre>]]></description>
    <pubDate>Tue, 14 Apr 2026 01:03:44 +0800</pubDate>
    <dc:creator>emer</dc:creator>
    <guid>https://deepsider.cn/post-3.html</guid>
</item>
<item>
    <title>抖音评论区监控脚本</title>
    <link>https://deepsider.cn/post-2.html</link>
    <description><![CDATA[<p>直接上脚本<br />
<img src="https://deepsider.cn/content/uploadfile/202604/98a41776187899.webp" alt="" /><br />
<img src="https://deepsider.cn/content/uploadfile/202604/18b11776187907.webp" alt="" /><br />
<img src="https://deepsider.cn/content/uploadfile/202604/729a1776187916.webp" alt="" /></p>
<pre><code class="language-javascript">// ==UserScript==
// @name         抖音评论监控 · 弹幕版 (历史记录+导出)
// @namespace    https://www.douyin.com/
// @version      2.7.0
// @description  监控评论关键词，右侧悬浮窗始终显示最新两条，历史记录包含评论内容
// @author       You
// @match        *://*.douyin.com/*
// @grant        GM_setClipboard
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @run-at       document-idle
// ==/UserScript==

(function () {
    'use strict';

    // ==================== 配置存储 ====================
    const STORAGE_KEY = 'dy_monitor_config_v7';
    const HISTORY_KEY = 'dy_monitor_history_v7'; // 新增版本号避免冲突

    const DEFAULT_CONFIG = {
        keywords: ['福利', '加我', '微信', 'qq', '进群', '兼职', '赚钱', '秒杀'],
        regions: ['广西', '全部'],
        enableRegionFilter: true,
        alertMode: 'right', // 默认改为右侧
        autoOpenComment: true,
        autoScrollComments: true,
        scrollInterval: 2000,
        maxMarquees: 5,
        showVideoLink: true,
        showVideoInfo: true,
        maxHistoryItems: 100,
        maxSideAlerts: 2 // 新增：右侧最大显示数量
    };

    // 从存储加载配置
    let config = GM_getValue(STORAGE_KEY, DEFAULT_CONFIG);

    // 转换为运行时变量
    let keywords = new Set(config.keywords);
    let monitorRegions = new Set(config.regions);
    let enableRegionFilter = config.enableRegionFilter;
    let alertMode = config.alertMode;
    let autoOpenComment = config.autoOpenComment;
    let autoScrollComments = config.autoScrollComments;
    let scrollInterval = config.scrollInterval;
    let maxMarquees = config.maxMarquees;
    let showVideoLink = config.showVideoLink;
    let showVideoInfo = config.showVideoInfo;
    let maxHistoryItems = config.maxHistoryItems;
    let maxSideAlerts = config.maxSideAlerts || 2; // 默认2条

    const triggeredComments = new Set();

    // 历史记录
    let historyRecords = GM_getValue(HISTORY_KEY, []);

    // 状态变量
    let isCommentPanelOpen = false;
    let scrollTimer = null;
    let commentContainer = null;

    // 弹幕容器
    let marqueeContainer = null;

    // 侧边弹窗数组 - 用于管理显示数量
    let sideAlerts = [];

    // ==================== 保存配置 ====================
    function saveConfig() {
        config = {
            keywords: Array.from(keywords),
            regions: Array.from(monitorRegions),
            enableRegionFilter: enableRegionFilter,
            alertMode: alertMode,
            autoOpenComment: autoOpenComment,
            autoScrollComments: autoScrollComments,
            scrollInterval: scrollInterval,
            maxMarquees: maxMarquees,
            showVideoLink: showVideoLink,
            showVideoInfo: showVideoInfo,
            maxHistoryItems: maxHistoryItems,
            maxSideAlerts: maxSideAlerts
        };

        GM_setValue(STORAGE_KEY, config);
        showToast('💾 配置已保存', 1000);
    }

    // ==================== 历史记录管理 ====================
    function addHistoryRecord(commentData, videoInfo) {
        const record = {
            id: Date.now() + Math.random().toString(36).substr(2, 5),
            timestamp: Date.now(),
            date: new Date().toLocaleString(),
            comment: {
                text: commentData.text,
                author: commentData.author,
                time: commentData.time,
                timeStr: formatTime(commentData.time),
                ipLocation: commentData.ipLocation,
                matchedKeywords: commentData.matchedKeywords
            },
            video: {
                author: videoInfo.author,
                publishTime: videoInfo.publishTime,
                title: videoInfo.fullTitle,
                url: videoInfo.videoUrl
            }
        };

        historyRecords.unshift(record); // 新记录插入开头

        // 限制数量
        if (historyRecords.length &gt; maxHistoryItems) {
            historyRecords = historyRecords.slice(0, maxHistoryItems);
        }

        GM_setValue(HISTORY_KEY, historyRecords);
    }

    function clearHistory() {
        if (confirm('确定要清空所有历史记录吗？')) {
            historyRecords = [];
            GM_setValue(HISTORY_KEY, []);
            showToast('🗑️ 历史记录已清空', 1500);
        }
    }

    // ==================== 导出功能 ====================
    function exportHistoryToCSV() {
        if (historyRecords.length === 0) {
            showToast('📭 没有历史记录可导出', 1500);
            return;
        }

        // CSV 表头 - 新增评论内容列
        const headers = [
            '序号',
            '视频作者',
            '视频标题',
            '视频发布时间',
            '评论人',
            '评论时间',
            '评论地区',
            '评论内容',      // 新增
            '命中关键词',
            '视频链接',
            '记录时间'
        ];

        // 转换数据 - 新增评论内容
        const rows = historyRecords.map((record, index) =&gt; {
            return [
                index + 1,
                record.video.author,
                record.video.title.replace(/,/g, '，'),
                record.video.publishTime,
                record.comment.author,
                record.comment.timeStr,
                record.comment.ipLocation,
                record.comment.text.replace(/,/g, '，'), // 评论内容，替换逗号
                record.comment.matchedKeywords.join('、'),
                record.video.url,
                record.date
            ];
        });

        // 构建CSV内容
        const csvContent = [
            headers.join(','),
            ...rows.map(row =&gt; row.map(cell =&gt; `"${cell}"`).join(','))
        ].join('\n');

        // 创建下载链接
        const blob = new Blob(['\uFEFF' + csvContent], { type: 'text/csv;charset=utf-8;' });
        const url = URL.createObjectURL(blob);
        const link = document.createElement('a');
        link.href = url;
        link.setAttribute('download', `抖音评论监控_${new Date().toLocaleDateString()}.csv`);
        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link);
        URL.revokeObjectURL(url);

        showToast('📥 历史记录已导出', 1500);
    }

    // ==================== 工具函数 ====================
    function showToast(msg, duration = 2000, isWarning = false) {
        const toast = document.createElement('div');
        Object.assign(toast.style, {
            position: 'fixed',
            left: '50%',
            top: '30%',
            transform: 'translateX(-50%)',
            background: isWarning ? 'rgba(255, 80, 80, 0.95)' : 'rgba(0,0,0,0.8)',
            color: '#fff',
            padding: '10px 20px',
            borderRadius: '40px',
            fontSize: '15px',
            fontWeight: '500',
            zIndex: 99999999,
            boxShadow: '0 4px 12px rgba(0,0,0,0.3)',
            transition: 'opacity 0.2s',
            pointerEvents: 'none'
        });
        toast.textContent = msg;
        document.body.appendChild(toast);
        setTimeout(() =&gt; {
            toast.style.opacity = '0';
            setTimeout(() =&gt; toast.remove(), 300);
        }, duration);
    }

    function formatTime(timestamp) {
        if (!timestamp) return '未知时间';
        let date = new Date(timestamp * 1000);
        if (isNaN(date.getTime())) {
            date = new Date(timestamp);
            if (isNaN(date.getTime())) return '未知时间';
        }
        const pad = (n) =&gt; n.toString().padStart(2, '0');
        return `${date.getFullYear()}-${pad(date.getMonth()+1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
    }

    function generateCommentId(comment) {
        return `${comment.text || comment.content}_${comment.user?.nickname || ''}_${comment.create_time || comment.time || ''}`;
    }

    function isRegionMatched(ipLocation) {
        if (!enableRegionFilter) return true;
        if (!ipLocation || ipLocation === '未知') return false;
        if (monitorRegions.has('全部')) return true;

        for (let region of monitorRegions) {
            if (!region || region === '全部') continue;
            if (ipLocation.includes(region)) {
                return true;
            }
        }
        return false;
    }

    // ==================== 提取视频信息 ====================
    function getVideoInfo() {
        const info = {
            author: '未知作者',
            publishTime: '未知时间',
            title: '未知标题',
            fullTitle: '未知标题',
            videoUrl: window.location.href
        };

        try {
            const authorElement = document.querySelector('[data-e2e="feed-video-nickname"] span, .account-name-text span');
            if (authorElement) {
                info.author = authorElement.textContent.trim().replace('@', '');
            }

            const timeElement = document.querySelector('.video-create-time .time, [class*="time"]');
            if (timeElement) {
                info.publishTime = timeElement.textContent.trim();
            }

            const titleContainer = document.querySelector('.title .FJhgcCvF, [data-e2e="video-desc"]');
            if (titleContainer) {
                const titleSpans = titleContainer.querySelectorAll('span');
                let fullTitle = '';
                titleSpans.forEach(span =&gt; {
                    const text = span.textContent.trim();
                    if (text &amp;&amp; !text.includes('展开') &amp;&amp; !text.includes('收起')) {
                        fullTitle += text + ' ';
                    }
                });
                info.fullTitle = fullTitle.trim() || titleContainer.textContent.trim();
                info.title = info.fullTitle.length &gt; 80 ? info.fullTitle.substring(0, 80) + '...' : info.fullTitle;
            }

            info.videoUrl = window.location.href;

        } catch (e) {
            console.log('提取视频信息出错:', e);
        }

        return info;
    }

    function formatVideoInfo(info) {
        return {
            author: info.author,
            publishTime: info.publishTime,
            title: info.title,
            fullTitle: info.fullTitle,
            videoUrl: info.videoUrl
        };
    }

    function copyFullComment(commentData, videoInfo) {
        const { text, author, time, ipLocation, matchedKeywords } = commentData;

        const timeStr = formatTime(time);
        const keywordStr = matchedKeywords.length &gt; 0 ? `[关键词: ${matchedKeywords.join('、')}]` : '';

        const fullInfo = `👤 评论用户：${author || '匿名'}
📍 评论地区：${ipLocation || '未知'}
🕒 评论时间：${timeStr}
🔑 ${keywordStr}

📹 视频信息：
   👤 发布者：${videoInfo.author}
   🕒 发布时间：${videoInfo.publishTime}
   📝 标题：${videoInfo.fullTitle}
   🔗 链接：${videoInfo.videoUrl}

💬 评论内容：${text}

-----
来自抖音评论监控插件`;

        if (typeof GM_setClipboard !== 'undefined') {
            GM_setClipboard(fullInfo, 'text');
            showToast('✅ 已复制完整信息', 1500);
        } else {
            const textarea = document.createElement('textarea');
            textarea.value = fullInfo;
            document.body.appendChild(textarea);
            textarea.select();
            document.execCommand('copy');
            document.body.removeChild(textarea);
            showToast('📋 已复制完整信息', 1500);
        }
    }

    // ==================== 弹幕模式 ====================
    function showMarquee(commentData) {
        const { text, author, time, ipLocation, matchedKeywords } = commentData;
        const videoInfo = formatVideoInfo(getVideoInfo());

        // 添加到历史记录
        addHistoryRecord(commentData, videoInfo);

        if (!marqueeContainer) {
            marqueeContainer = document.createElement('div');
            marqueeContainer.id = 'marquee-container';
            Object.assign(marqueeContainer.style, {
                position: 'fixed',
                top: '70px',
                left: '0',
                right: '0',
                display: 'flex',
                flexDirection: 'column',
                alignItems: 'center',
                gap: '8px',
                zIndex: 999999999,
                pointerEvents: 'none'
            });
            document.body.appendChild(marqueeContainer);
        }

        const marquee = document.createElement('div');
        marquee.className = 'dy-comment-marquee';
        Object.assign(marquee.style, {
            width: '700px',
            maxWidth: '90%',
            background: '#ffffff',
            borderRadius: '8px',
            padding: '16px 20px',
            color: '#333',
            fontSize: '14px',
            boxShadow: '0 4px 16px rgba(0,0,0,0.15)',
            border: '1px solid #eaeaea',
            pointerEvents: 'auto',
            marginBottom: '4px',
            transition: 'opacity 0.2s',
            fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
            lineHeight: '1.5'
        });

        if (!document.getElementById('marquee-styles')) {
            const style = document.createElement('style');
            style.id = 'marquee-styles';
            style.textContent = `
                @keyframes marqueeFadeIn {
                    from { opacity: 0; transform: translateY(-10px); }
                    to { opacity: 1; transform: translateY(0); }
                }
                .video-link {
                    color: #2196F3;
                    text-decoration: none;
                    font-size: 12px;
                    word-break: break-all;
                }
                .video-link:hover {
                    text-decoration: underline;
                    color: #4CAF50;
                }
                .video-title {
                    font-weight: 500;
                    color: #1a1a1a;
                    line-height: 1.4;
                }
                .video-meta {
                    color: #666;
                    font-size: 12px;
                }
                .keyword-tag {
                    background: #ff4d4d;
                    color: #fff;
                    padding: 2px 8px;
                    border-radius: 4px;
                    font-size: 12px;
                    font-weight: 500;
                }
            `;
            document.head.appendChild(style);
        }

        // 视频信息区域
        if (showVideoInfo) {
            const videoInfoDiv = document.createElement('div');
            videoInfoDiv.style.marginBottom = '12px';
            videoInfoDiv.style.padding = '10px 12px';
            videoInfoDiv.style.backgroundColor = '#f5f9ff';
            videoInfoDiv.style.borderRadius = '6px';
            videoInfoDiv.style.borderLeft = '3px solid #4CAF50';

            const authorLine = document.createElement('div');
            authorLine.style.display = 'flex';
            authorLine.style.alignItems = 'center';
            authorLine.style.gap = '12px';
            authorLine.style.marginBottom = '6px';
            authorLine.style.flexWrap = 'wrap';
            authorLine.innerHTML = `
                &lt;span style="font-weight:600; color:#1a1a1a;"&gt;👤 ${videoInfo.author}&lt;/span&gt;
                &lt;span style="color:#999; font-size:12px;"&gt;🕒 ${videoInfo.publishTime}&lt;/span&gt;
            `;

            const titleLine = document.createElement('div');
            titleLine.className = 'video-title';
            titleLine.style.marginBottom = '4px';
            titleLine.style.fontSize = '13px';
            titleLine.style.color = '#333';
            titleLine.innerHTML = `📝 ${videoInfo.title}`;

            videoInfoDiv.appendChild(authorLine);
            videoInfoDiv.appendChild(titleLine);

            if (showVideoLink) {
                const linkLine = document.createElement('div');
                linkLine.style.marginTop = '6px';
                linkLine.style.fontSize = '12px';
                linkLine.style.display = 'flex';
                linkLine.style.alignItems = 'center';
                linkLine.style.gap = '6px';
                linkLine.innerHTML = `
                    &lt;span style="color:#666;"&gt;🔗&lt;/span&gt;
                    &lt;a href="${videoInfo.videoUrl}" target="_blank" class="video-link"&gt;打开视频&lt;/a&gt;
                `;
                videoInfoDiv.appendChild(linkLine);
            }

            marquee.appendChild(videoInfoDiv);
        }

        // 评论信息区域
        const commentInfoDiv = document.createElement('div');
        commentInfoDiv.style.marginBottom = '12px';
        commentInfoDiv.style.padding = '10px 12px';
        commentInfoDiv.style.backgroundColor = '#f8f8f8';
        commentInfoDiv.style.borderRadius = '6px';
        commentInfoDiv.style.borderLeft = '3px solid #ff4d4d';

        const metaLine = document.createElement('div');
        metaLine.style.display = 'flex';
        metaLine.style.alignItems = 'center';
        metaLine.style.gap = '12px';
        metaLine.style.marginBottom = '8px';
        metaLine.style.flexWrap = 'wrap';
        metaLine.innerHTML = `
            &lt;span style="font-weight:600; color:#1a1a1a;"&gt;👤 ${author || '匿名'}&lt;/span&gt;
            &lt;span style="color:#666;"&gt;📍 ${ipLocation || '未知'}&lt;/span&gt;
            &lt;span style="color:#999; font-size:12px;"&gt;🕒 ${formatTime(time)}&lt;/span&gt;
        `;

        const keywordsBar = document.createElement('div');
        keywordsBar.style.display = 'flex';
        keywordsBar.style.alignItems = 'center';
        keywordsBar.style.gap = '6px';
        keywordsBar.style.marginBottom = '8px';
        keywordsBar.style.flexWrap = 'wrap';

        matchedKeywords.forEach(kw =&gt; {
            const tag = document.createElement('span');
            tag.className = 'keyword-tag';
            tag.textContent = `# ${kw}`;
            keywordsBar.appendChild(tag);
        });

        const contentDiv = document.createElement('div');
        contentDiv.style.fontSize = '14px';
        contentDiv.style.lineHeight = '1.6';
        contentDiv.style.wordBreak = 'break-all';
        contentDiv.style.padding = '4px 0';
        contentDiv.style.color = '#444';
        contentDiv.textContent = text;

        commentInfoDiv.appendChild(metaLine);
        commentInfoDiv.appendChild(keywordsBar);
        commentInfoDiv.appendChild(contentDiv);
        marquee.appendChild(commentInfoDiv);

        const actionsBar = document.createElement('div');
        actionsBar.style.display = 'flex';
        actionsBar.style.justifyContent = 'flex-end';
        actionsBar.style.gap = '10px';
        actionsBar.style.marginTop = '8px';

        const copyBtn = document.createElement('button');
        copyBtn.textContent = '📋 复制完整信息';
        Object.assign(copyBtn.style, {
            background: '#4CAF50',
            border: 'none',
            color: '#fff',
            padding: '6px 14px',
            borderRadius: '4px',
            fontSize: '12px',
            cursor: 'pointer',
            transition: 'all 0.2s',
            fontWeight: '500'
        });
        copyBtn.onmouseenter = () =&gt; copyBtn.style.background = '#66BB6A';
        copyBtn.onmouseleave = () =&gt; copyBtn.style.background = '#4CAF50';
        copyBtn.onclick = (e) =&gt; {
            e.stopPropagation();
            copyFullComment(commentData, videoInfo);
        };

        const closeBtn = document.createElement('button');
        closeBtn.textContent = '✕ 关闭';
        Object.assign(closeBtn.style, {
            background: '#ff4d4d',
            border: 'none',
            color: '#fff',
            padding: '6px 14px',
            borderRadius: '4px',
            fontSize: '12px',
            cursor: 'pointer',
            transition: 'all 0.2s',
            fontWeight: '500'
        });
        closeBtn.onmouseenter = () =&gt; closeBtn.style.background = '#ff6666';
        closeBtn.onmouseleave = () =&gt; closeBtn.style.background = '#ff4d4d';
        closeBtn.onclick = (e) =&gt; {
            e.stopPropagation();
            marquee.remove();
            if (marqueeContainer.children.length === 0) {
                marqueeContainer.remove();
                marqueeContainer = null;
            }
        };

        actionsBar.appendChild(copyBtn);
        actionsBar.appendChild(closeBtn);
        marquee.appendChild(actionsBar);

        marquee.addEventListener('click', (e) =&gt; {
            if (e.target === copyBtn || e.target === closeBtn || 
                copyBtn.contains(e.target) || closeBtn.contains(e.target) ||
                e.target.tagName === 'A') {
                return;
            }
            copyFullComment(commentData, videoInfo);
        });

        marqueeContainer.appendChild(marquee);

        while (marqueeContainer.children.length &gt; maxMarquees) {
            marqueeContainer.removeChild(marqueeContainer.firstChild);
        }
    }

    // ==================== 侧边弹窗（修改为始终显示最新两条）====================
    function showSideAlert(commentData, side = 'right') {
        const { text, author, time, ipLocation, matchedKeywords } = commentData;
        const videoInfo = formatVideoInfo(getVideoInfo());

        // 添加到历史记录
        addHistoryRecord(commentData, videoInfo);

        // 创建新弹窗
        const overlay = document.createElement('div');
        overlay.className = 'dy-comment-alert';
        overlay.dataset.timestamp = Date.now(); // 用于排序
        Object.assign(overlay.style, {
            position: 'fixed',
            top: '20px',
            [side]: '20px',
            width: '380px',
            background: '#1e1e1e',
            borderRadius: '12px',
            boxShadow: '0 8px 20px rgba(0,0,0,0.5)',
            color: '#fff',
            zIndex: 999999999,
            border: '1px solid #ff4d4d',
            animation: `sideSlideIn-${side} 0.2s ease-out`,
            marginBottom: '10px',
            fontFamily: 'system-ui, -apple-system, "Segoe UI", Roboto, sans-serif'
        });

        if (!document.getElementById('side-styles')) {
            const style = document.createElement('style');
            style.id = 'side-styles';
            style.textContent = `
                @keyframes sideSlideIn-right {
                    from { transform: translateX(100%); opacity: 0; }
                    to { transform: translateX(0); opacity: 1; }
                }
                @keyframes sideSlideIn-left {
                    from { transform: translateX(-100%); opacity: 0; }
                    to { transform: translateX(0); opacity: 1; }
                }
                .video-link-dark {
                    color: #64B5F6;
                    text-decoration: none;
                }
                .video-link-dark:hover {
                    text-decoration: underline;
                    color: #90CAF9;
                }
            `;
            document.head.appendChild(style);
        }

        const card = document.createElement('div');
        card.style.padding = '16px';

        const header = document.createElement('div');
        header.style.display = 'flex';
        header.style.justifyContent = 'space-between';
        header.style.alignItems = 'center';
        header.style.marginBottom = '16px';

        const title = document.createElement('div');
        title.style.display = 'flex';
        title.style.alignItems = 'center';
        title.style.gap = '8px';
        title.innerHTML = `
            &lt;span style="background:#ff4d4d; width:10px; height:10px; border-radius:50%;"&gt;&lt;/span&gt;
            &lt;span style="font-weight:600; font-size:15px;"&gt;🔔 触发 ${matchedKeywords.length} 个关键词&lt;/span&gt;
        `;

        const closeBtn = document.createElement('button');
        closeBtn.textContent = '×';
        Object.assign(closeBtn.style, {
            background: 'none',
            border: 'none',
            color: '#aaa',
            fontSize: '24px',
            cursor: 'pointer',
            padding: '0',
            lineHeight: '1'
        });
        closeBtn.onclick = () =&gt; {
            overlay.remove();
            // 从数组中移除
            sideAlerts = sideAlerts.filter(el =&gt; el !== overlay);
        };

        header.appendChild(title);
        header.appendChild(closeBtn);

        // 视频信息区域
        if (showVideoInfo) {
            const videoInfoDiv = document.createElement('div');
            videoInfoDiv.style.marginBottom = '16px';
            videoInfoDiv.style.padding = '12px';
            videoInfoDiv.style.background = '#2a2a2a';
            videoInfoDiv.style.borderRadius = '8px';
            videoInfoDiv.style.borderLeft = '3px solid #4CAF50';

            const authorLine = document.createElement('div');
            authorLine.style.display = 'flex';
            authorLine.style.alignItems = 'center';
            authorLine.style.gap = '10px';
            authorLine.style.marginBottom = '6px';
            authorLine.style.flexWrap = 'wrap';
            authorLine.innerHTML = `
                &lt;span style="font-weight:600; color:#4CAF50;"&gt;👤 ${videoInfo.author}&lt;/span&gt;
                &lt;span style="color:#aaa; font-size:12px;"&gt;🕒 ${videoInfo.publishTime}&lt;/span&gt;
            `;

            const titleLine = document.createElement('div');
            titleLine.style.fontSize = '13px';
            titleLine.style.color = '#ddd';
            titleLine.style.marginBottom = '6px';
            titleLine.style.lineHeight = '1.5';
            titleLine.textContent = videoInfo.title;

            videoInfoDiv.appendChild(authorLine);
            videoInfoDiv.appendChild(titleLine);

            if (showVideoLink) {
                const linkLine = document.createElement('div');
                linkLine.style.marginTop = '8px';
                linkLine.style.fontSize = '12px';
                linkLine.innerHTML = `
                    &lt;span style="color:#aaa;"&gt;🔗&lt;/span&gt;
                    &lt;a href="${videoInfo.videoUrl}" target="_blank" class="video-link-dark" style="margin-left:4px;"&gt;打开视频&lt;/a&gt;
                `;
                videoInfoDiv.appendChild(linkLine);
            }

            card.appendChild(videoInfoDiv);
        }

        // 评论信息区域
        const commentDiv = document.createElement('div');
        commentDiv.style.marginBottom = '16px';
        commentDiv.style.padding = '12px';
        commentDiv.style.background = '#2a2a2a';
        commentDiv.style.borderRadius = '8px';
        commentDiv.style.borderLeft = '3px solid #ff4d4d';

        const metaLine = document.createElement('div');
        metaLine.style.display = 'flex';
        metaLine.style.flexWrap = 'wrap';
        metaLine.style.gap = '10px';
        metaLine.style.marginBottom = '8px';
        metaLine.style.color = '#aaa';
        metaLine.style.fontSize = '12px';

        const isMatchedRegion = isRegionMatched(ipLocation);
        const ipStyle = isMatchedRegion ? 'color: #4CAF50; font-weight: bold;' : '';

        metaLine.innerHTML = `
            &lt;span&gt;👤 ${author || '匿名'}&lt;/span&gt;
            &lt;span&gt;🕒 ${formatTime(time)}&lt;/span&gt;
            &lt;span style="${ipStyle}"&gt;📍 ${ipLocation || '未知'}&lt;/span&gt;
        `;

        const tagsContainer = document.createElement('div');
        tagsContainer.style.display = 'flex';
        tagsContainer.style.flexWrap = 'wrap';
        tagsContainer.style.gap = '6px';
        tagsContainer.style.marginBottom = '8px';

        matchedKeywords.forEach(kw =&gt; {
            const tag = document.createElement('span');
            tag.style.background = '#ff4d4d';
            tag.style.padding = '4px 10px';
            tag.style.borderRadius = '16px';
            tag.style.fontSize = '11px';
            tag.textContent = `# ${kw}`;
            tagsContainer.appendChild(tag);
        });

        const contentDiv = document.createElement('div');
        contentDiv.style.fontSize = '13px';
        contentDiv.style.lineHeight = '1.6';
        contentDiv.style.wordBreak = 'break-all';
        contentDiv.style.maxHeight = '100px';
        contentDiv.style.overflowY = 'auto';
        contentDiv.style.color = '#eee';
        contentDiv.textContent = text;

        commentDiv.appendChild(metaLine);
        commentDiv.appendChild(tagsContainer);
        commentDiv.appendChild(contentDiv);
        card.appendChild(commentDiv);

        const actions = document.createElement('div');
        actions.style.display = 'flex';
        actions.style.gap = '8px';
        actions.style.justifyContent = 'flex-end';

        const copyBtn = document.createElement('button');
        copyBtn.textContent = '📋 复制完整信息';
        Object.assign(copyBtn.style, {
            background: '#4CAF50',
            border: 'none',
            color: '#fff',
            padding: '6px 14px',
            borderRadius: '6px',
            fontSize: '12px',
            cursor: 'pointer',
            transition: 'background 0.2s'
        });
        copyBtn.onmouseenter = () =&gt; copyBtn.style.background = '#66BB6A';
        copyBtn.onmouseleave = () =&gt; copyBtn.style.background = '#4CAF50';
        copyBtn.onclick = () =&gt; copyFullComment(commentData, videoInfo);

        const closeCardBtn = document.createElement('button');
        closeCardBtn.textContent = '关闭';
        Object.assign(closeCardBtn.style, {
            background: '#ff4d4d',
            border: 'none',
            color: '#fff',
            padding: '6px 14px',
            borderRadius: '6px',
            fontSize: '12px',
            cursor: 'pointer',
            transition: 'background 0.2s'
        });
        closeCardBtn.onmouseenter = () =&gt; closeCardBtn.style.background = '#ff6666';
        closeCardBtn.onmouseleave = () =&gt; closeCardBtn.style.background = '#ff4d4d';
        closeCardBtn.onclick = () =&gt; {
            overlay.remove();
            sideAlerts = sideAlerts.filter(el =&gt; el !== overlay);
        };

        actions.appendChild(copyBtn);
        actions.appendChild(closeCardBtn);

        card.appendChild(header);
        card.appendChild(actions);
        overlay.appendChild(card);

        // 添加到文档
        document.body.appendChild(overlay);

        // 添加到管理数组
        sideAlerts.push(overlay);

        // 按时间戳排序（最新的在最后）
        sideAlerts.sort((a, b) =&gt; (a.dataset.timestamp || 0) - (b.dataset.timestamp || 0));

        // 如果超过最大显示数量，移除最早的
        while (sideAlerts.length &gt; maxSideAlerts) {
            const oldest = sideAlerts.shift();
            if (oldest &amp;&amp; oldest.parentNode) {
                oldest.remove();
            }
        }

        // 重新计算所有弹窗的位置（从下往上排列，最新的在底部）
        updateSideAlertPositions(side);
    }

    // 更新侧边弹窗位置（从下往上排列，最新的在底部）
    function updateSideAlertPositions(side) {
        const alerts = document.querySelectorAll('.dy-comment-alert');
        const baseTop = 20;
        const offset = 10;
        let currentTop = baseTop;

        // 转为数组并排序（老的在上，新的在下）
        const sorted = Array.from(alerts).sort((a, b) =&gt; 
            (a.dataset.timestamp || 0) - (b.dataset.timestamp || 0)
        );

        sorted.forEach((el, index) =&gt; {
            el.style.top = currentTop + 'px';
            currentTop += el.offsetHeight + offset;
        });
    }

    // ==================== 根据模式显示弹窗 ====================
    function showAlert(commentData) {
        switch(alertMode) {
            case 'left':
                showSideAlert(commentData, 'left');
                break;
            case 'right':
                showSideAlert(commentData, 'right');
                break;
            case 'marquee':
                showMarquee(commentData);
                break;
            default:
                showSideAlert(commentData, 'right');
        }
    }

    // ==================== 历史记录表格弹窗（新增评论内容列）====================
    function showHistoryWindow() {
        // 创建遮罩层
        const overlay = document.createElement('div');
        overlay.id = 'history-window-overlay';
        Object.assign(overlay.style, {
            position: 'fixed',
            top: '0',
            left: '0',
            right: '0',
            bottom: '0',
            background: 'rgba(0,0,0,0.7)',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            zIndex: 999999999,
            backdropFilter: 'blur(4px)'
        });

        // 创建窗口
        const windowDiv = document.createElement('div');
        Object.assign(windowDiv.style, {
            width: '90%',
            maxWidth: '1300px', // 加宽以适应新列
            height: '80%',
            background: '#1e1e1e',
            borderRadius: '12px',
            boxShadow: '0 20px 40px rgba(0,0,0,0.5)',
            color: '#fff',
            display: 'flex',
            flexDirection: 'column',
            overflow: 'hidden',
            border: '1px solid #333'
        });

        // 标题栏
        const titleBar = document.createElement('div');
        Object.assign(titleBar.style, {
            padding: '16px 20px',
            borderBottom: '1px solid #333',
            display: 'flex',
            justifyContent: 'space-between',
            alignItems: 'center',
            background: '#252525'
        });

        const title = document.createElement('h3');
        title.style.margin = '0';
        title.style.fontSize = '16px';
        title.style.fontWeight = '500';
        title.innerHTML = `📋 历史记录 (${historyRecords.length})`;

        const closeBtn = document.createElement('button');
        closeBtn.textContent = '×';
        Object.assign(closeBtn.style, {
            background: 'none',
            border: 'none',
            color: '#aaa',
            fontSize: '24px',
            cursor: 'pointer',
            padding: '0 8px'
        });
        closeBtn.onmouseenter = () =&gt; closeBtn.style.color = '#fff';
        closeBtn.onmouseleave = () =&gt; closeBtn.style.color = '#aaa';
        closeBtn.onclick = () =&gt; overlay.remove();

        titleBar.appendChild(title);
        titleBar.appendChild(closeBtn);

        // 工具栏
        const toolbar = document.createElement('div');
        Object.assign(toolbar.style, {
            padding: '12px 20px',
            borderBottom: '1px solid #333',
            display: 'flex',
            gap: '10px',
            background: '#1a1a1a'
        });

        const exportBtn = document.createElement('button');
        exportBtn.textContent = '📥 导出CSV';
        Object.assign(exportBtn.style, {
            background: '#4CAF50',
            border: 'none',
            borderRadius: '4px',
            padding: '6px 16px',
            color: '#fff',
            fontSize: '13px',
            cursor: 'pointer'
        });
        exportBtn.onclick = exportHistoryToCSV;

        const clearBtn = document.createElement('button');
        clearBtn.textContent = '🗑️ 清空记录';
        Object.assign(clearBtn.style, {
            background: '#ff4d4d',
            border: 'none',
            borderRadius: '4px',
            padding: '6px 16px',
            color: '#fff',
            fontSize: '13px',
            cursor: 'pointer'
        });
        clearBtn.onclick = () =&gt; {
            clearHistory();
            overlay.remove();
        };

        toolbar.appendChild(exportBtn);
        toolbar.appendChild(clearBtn);

        // 表格容器
        const tableContainer = document.createElement('div');
        Object.assign(tableContainer.style, {
            flex: '1',
            overflow: 'auto',
            padding: '20px'
        });

        if (historyRecords.length === 0) {
            const emptyMsg = document.createElement('div');
            emptyMsg.style.textAlign = 'center';
            emptyMsg.style.padding = '50px';
            emptyMsg.style.color = '#666';
            emptyMsg.textContent = '暂无历史记录';
            tableContainer.appendChild(emptyMsg);
        } else {
            const table = document.createElement('table');
            Object.assign(table.style, {
                width: '100%',
                borderCollapse: 'collapse',
                fontSize: '13px',
                minWidth: '1200px' // 加宽
            });

            // 表头 - 新增评论内容列
            const thead = document.createElement('thead');
            const headerRow = document.createElement('tr');
            headerRow.style.background = '#2a2a2a';

            const headers = [
                '序号', '视频作者', '视频标题', '视频发布时间',
                '评论人', '评论时间', '评论地区', '评论内容', // 新增
                '命中关键词', '视频链接'
            ];

            headers.forEach(text =&gt; {
                const th = document.createElement('th');
                th.textContent = text;
                Object.assign(th.style, {
                    padding: '12px 8px',
                    textAlign: 'left',
                    borderBottom: '2px solid #444',
                    color: '#aaa',
                    fontWeight: '500'
                });
                headerRow.appendChild(th);
            });
            thead.appendChild(headerRow);
            table.appendChild(thead);

            // 表体 - 新增评论内容
            const tbody = document.createElement('tbody');
            historyRecords.forEach((record, index) =&gt; {
                const row = document.createElement('tr');
                row.style.borderBottom = '1px solid #333';
                row.onmouseenter = () =&gt; row.style.background = '#2a2a2a';
                row.onmouseleave = () =&gt; row.style.background = 'transparent';

                const cells = [
                    index + 1,
                    record.video.author,
                    record.video.title,
                    record.video.publishTime,
                    record.comment.author,
                    record.comment.timeStr,
                    record.comment.ipLocation,
                    record.comment.text, // 新增：评论内容
                    record.comment.matchedKeywords.join('、'),
                    record.video.url
                ];

                cells.forEach((cell, i) =&gt; {
                    const td = document.createElement('td');
                    Object.assign(td.style, {
                        padding: '10px 8px',
                        color: i === 8 ? '#ffb3b3' : '#e0e0e0', // 关键词列索引变为8
                        maxWidth: i === 2 ? '200px' : (i === 7 ? '250px' : 'none'), // 标题和评论内容列宽限制
                        overflow: 'hidden',
                        textOverflow: 'ellipsis',
                        whiteSpace: 'nowrap'
                    });

                    if (i === 9) { // 视频链接列索引变为9
                        const link = document.createElement('a');
                        link.href = cell;
                        link.target = '_blank';
                        link.textContent = '打开';
                        link.style.color = '#4CAF50';
                        link.style.textDecoration = 'none';
                        link.onmouseenter = () =&gt; link.style.textDecoration = 'underline';
                        link.onmouseleave = () =&gt; link.style.textDecoration = 'none';
                        td.appendChild(link);
                    } else {
                        td.textContent = cell;
                        if (i === 8 &amp;&amp; cell) { // 关键词列
                            td.style.fontWeight = '500';
                        }
                    }

                    row.appendChild(td);
                });

                tbody.appendChild(row);
            });
            table.appendChild(tbody);
            tableContainer.appendChild(table);
        }

        windowDiv.appendChild(titleBar);
        windowDiv.appendChild(toolbar);
        windowDiv.appendChild(tableContainer);
        overlay.appendChild(windowDiv);
        document.body.appendChild(overlay);
    }

    // ==================== 模拟X键打开评论区 ====================
    function simulateKeyX() {
        const event = new KeyboardEvent('keydown', {
            key: 'x',
            keyCode: 88,
            which: 88,
            code: 'KeyX',
            bubbles: true,
            cancelable: true
        });
        document.dispatchEvent(event);

        const eventUp = new KeyboardEvent('keyup', {
            key: 'x',
            keyCode: 88,
            which: 88,
            code: 'KeyX',
            bubbles: true,
            cancelable: true
        });
        document.dispatchEvent(eventUp);
    }

    function openCommentPanel() {
        if (isCommentPanelOpen) return;

        simulateKeyX();

        isCommentPanelOpen = true;
        showToast('💬 尝试打开评论区 (模拟X键)', 1500);

        setTimeout(() =&gt; {
            const commentEl = findCommentContainer();
            if (commentEl) {
                showToast('✅ 评论区已打开', 1000);
                if (autoScrollComments) {
                    startCommentScroll();
                }
            } else {
                isCommentPanelOpen = false;
            }
        }, 2000);
    }

    // ==================== 查找评论区容器 ====================
    function findCommentContainer() {
        const selectors = [
            '[class*="comment-list"]',
            '[class*="CommentList"]',
            '[class*="commentContainer"]',
            '[class*="comments-container"]',
            '[class*="comment_list"]',
            '.LWSPvSJk',
            '#comment-container',
            '[data-e2e="comment-list"]'
        ];

        for (let selector of selectors) {
            const el = document.querySelector(selector);
            if (el &amp;&amp; el.children.length &gt; 0) return el;
        }
        return null;
    }

    // ==================== 开始滚动评论区 ====================
    function startCommentScroll() {
        if (scrollTimer) {
            clearInterval(scrollTimer);
            scrollTimer = null;
        }

        commentContainer = findCommentContainer();
        if (!commentContainer) {
            showToast('⚠️ 未找到评论区', 1500);
            return;
        }

        showToast('🔄 开始滚动加载评论', 1500);

        scrollTimer = setInterval(() =&gt; {
            if (commentContainer) {
                commentContainer.scrollTop = commentContainer.scrollHeight;
                commentContainer.scrollBy(0, 200);
            }
        }, scrollInterval);
    }

    function stopCommentScroll() {
        if (scrollTimer) {
            clearInterval(scrollTimer);
            scrollTimer = null;
            showToast('⏸️ 停止滚动', 1000);
        }
    }

    // ==================== 劫持评论接口 ====================
    function inspectResponseBody(url, bodyText) {
        if (!bodyText || typeof bodyText !== 'string') return;

        const commentAPIs = [
            '/comment/',
            '/v2/comment/',
            '/aweme/v1/comment/',
            '/aweme/v1/web/comment/list/'
        ];

        if (!commentAPIs.some(api =&gt; url.includes(api))) return;

        try {
            const data = JSON.parse(bodyText);
            let comments = [];

            if (data.comments) comments = data.comments;
            else if (data.data &amp;&amp; Array.isArray(data.data)) comments = data.data;
            else if (data.data &amp;&amp; data.data.comments) comments = data.data.comments;
            else if (data.comment_list) comments = data.comment_list;
            else return;

            comments.forEach(comment =&gt; {
                const commentId = generateCommentId(comment);
                if (triggeredComments.has(commentId)) return;

                let text = comment.text || comment.content || '';
                let user = comment.user?.nickname || comment.user?.unique_id || comment.author || '匿名';
                let createTime = comment.create_time || comment.time || null;
                let ipLabel = comment.ip_label || comment.ip_location || comment.region || '未知';

                if (!ipLabel || ipLabel === '未知') {
                    ipLabel = comment.user?.ip_location || comment.ip || '未知';
                }

                if (text) {
                    const matchedKeywords = [];
                    for (let kw of keywords) {
                        if (text.includes(kw)) {
                            matchedKeywords.push(kw);
                        }
                    }

                    if (matchedKeywords.length &gt; 0 &amp;&amp; isRegionMatched(ipLabel)) {
                        triggeredComments.add(commentId);

                        showAlert({
                            text: text,
                            author: user,
                            time: createTime,
                            ipLocation: ipLabel,
                            matchedKeywords: matchedKeywords
                        });
                    }
                }
            });
        } catch (e) {}
    }

    // 劫持 fetch
    const originalFetch = window.fetch;
    window.fetch = function (...args) {
        const url = args[0] instanceof Request ? args[0].url : args[0];
        return originalFetch.apply(this, args).then(response =&gt; {
            const cloned = response.clone();
            cloned.text().then(body =&gt; inspectResponseBody(url, body)).catch(() =&gt; {});
            return response;
        });
    };

    // 劫持 XHR
    const originalXHROpen = XMLHttpRequest.prototype.open;
    const originalXHRSend = XMLHttpRequest.prototype.send;
    XMLHttpRequest.prototype.open = function (method, url) {
        this._monitorUrl = url;
        return originalXHROpen.apply(this, arguments);
    };
    XMLHttpRequest.prototype.send = function (...args) {
        if (this._monitorUrl) {
            const url = this._monitorUrl;
            const originalOnLoad = this.onload;
            this.onload = function (e) {
                if (this.readyState === 4 &amp;&amp; this.status === 200) {
                    inspectResponseBody(url, this.responseText);
                }
                if (originalOnLoad) originalOnLoad.call(this, e);
            };
        }
        return originalXHRSend.apply(this, args);
    };

    // ==================== 悬浮配置面板 ====================
    let panel = null;
    let isDragging = false;
    let offsetX = 0, offsetY = 0;
    let isMinimized = false;

    function createPanel() {
        if (panel) panel.remove();

        panel = document.createElement('div');
        panel.id = 'comment-monitor-panel';
        Object.assign(panel.style, {
            position: 'fixed',
            bottom: '20px',
            right: '20px',
            width: isMinimized ? '160px' : '420px',
            background: '#1f1f1f',
            borderRadius: '12px',
            boxShadow: '0 8px 20px rgba(0,0,0,0.5)',
            color: '#e0e0e0',
            fontSize: '13px',
            zIndex: 9999999,
            border: '1px solid #333',
            fontFamily: 'system-ui, -apple-system, "Segoe UI", Roboto, sans-serif',
            cursor: 'default',
            userSelect: 'none',
            transition: 'width 0.2s ease'
        });

        const header = document.createElement('div');
        header.id = 'panel-header';
        Object.assign(header.style, {
            padding: '10px 14px',
            borderBottom: isMinimized ? 'none' : '1px solid #333',
            display: 'flex',
            justifyContent: 'space-between',
            alignItems: 'center',
            background: '#252525',
            borderRadius: '12px 12px 0 0',
            cursor: 'move',
            userSelect: 'none'
        });

        const titleArea = document.createElement('div');
        titleArea.style.display = 'flex';
        titleArea.style.alignItems = 'center';
        titleArea.style.gap = '6px';

        const minimizeBtn = document.createElement('span');
        minimizeBtn.textContent = isMinimized ? '□' : '—';
        minimizeBtn.title = isMinimized ? '展开' : '最小化';
        Object.assign(minimizeBtn.style, {
            cursor: 'pointer',
            color: '#888',
            fontSize: '14px',
            padding: '2px 6px',
            borderRadius: '4px',
            transition: 'all 0.2s'
        });
        minimizeBtn.onmouseenter = () =&gt; minimizeBtn.style.color = '#fff';
        minimizeBtn.onmouseleave = () =&gt; minimizeBtn.style.color = '#888';
        minimizeBtn.onclick = (e) =&gt; {
            e.stopPropagation();
            isMinimized = !isMinimized;
            createPanel();
        };

        const title = document.createElement('div');
        title.style.fontWeight = '500';
        title.style.fontSize = isMinimized ? '13px' : '14px';
        title.innerHTML = isMinimized ? '🎯 监控' : '弹幕监控 v2.7';

        titleArea.appendChild(minimizeBtn);
        titleArea.appendChild(title);

        const historyBtn = document.createElement('span');
        historyBtn.textContent = '📋';
        historyBtn.title = '查看历史记录';
        Object.assign(historyBtn.style, {
            cursor: 'pointer',
            color: '#4CAF50',
            fontSize: '14px',
            padding: '2px 6px',
            borderRadius: '4px',
            marginLeft: '4px',
            transition: 'all 0.2s'
        });
        historyBtn.onmouseenter = () =&gt; { historyBtn.style.background = '#333'; };
        historyBtn.onmouseleave = () =&gt; { historyBtn.style.background = 'transparent'; };
        historyBtn.onclick = (e) =&gt; {
            e.stopPropagation();
            showHistoryWindow();
        };
        titleArea.appendChild(historyBtn);

        const saveBtn = document.createElement('span');
        saveBtn.textContent = '💾';
        saveBtn.title = '保存配置';
        Object.assign(saveBtn.style, {
            cursor: 'pointer',
            color: '#4CAF50',
            fontSize: '14px',
            padding: '2px 6px',
            borderRadius: '4px',
            marginLeft: '4px',
            transition: 'all 0.2s'
        });
        saveBtn.onmouseenter = () =&gt; { saveBtn.style.background = '#333'; };
        saveBtn.onmouseleave = () =&gt; { saveBtn.style.background = 'transparent'; };
        saveBtn.onclick = (e) =&gt; {
            e.stopPropagation();
            saveConfig();
        };
        titleArea.appendChild(saveBtn);

        const closeBtn = document.createElement('span');
        closeBtn.textContent = '×';
        Object.assign(closeBtn.style, {
            cursor: 'pointer',
            color: '#888',
            fontSize: '18px',
            fontWeight: '400',
            padding: '0 4px',
            borderRadius: '4px',
            transition: 'all 0.2s'
        });
        closeBtn.onmouseenter = () =&gt; { closeBtn.style.background = '#333'; closeBtn.style.color = '#fff'; };
        closeBtn.onmouseleave = () =&gt; { closeBtn.style.background = 'transparent'; closeBtn.style.color = '#888'; };
        closeBtn.onclick = () =&gt; panel.remove();

        header.appendChild(titleArea);
        header.appendChild(closeBtn);

        // 拖拽功能
        header.addEventListener('mousedown', (e) =&gt; {
            if (e.button !== 0 || e.target === closeBtn || e.target === minimizeBtn || e.target === saveBtn || e.target === historyBtn) return;
            e.preventDefault();

            const rect = panel.getBoundingClientRect();
            offsetX = e.clientX - rect.left;
            offsetY = e.clientY - rect.top;

            isDragging = true;
            panel.style.transition = 'none';
            panel.style.cursor = 'grabbing';
        });

        document.addEventListener('mousemove', (e) =&gt; {
            if (!isDragging) return;
            e.preventDefault();

            const newLeft = e.clientX - offsetX;
            const newTop = e.clientY - offsetY;

            const maxX = window.innerWidth - panel.offsetWidth;
            const maxY = window.innerHeight - panel.offsetHeight;

            panel.style.left = Math.min(Math.max(0, newLeft), maxX) + 'px';
            panel.style.top = Math.min(Math.max(0, newTop), maxY) + 'px';
            panel.style.right = 'auto';
            panel.style.bottom = 'auto';
        });

        document.addEventListener('mouseup', () =&gt; {
            if (isDragging) {
                isDragging = false;
                panel.style.cursor = 'default';
                panel.style.transition = 'width 0.2s ease';
            }
        });

        header.addEventListener('dragstart', (e) =&gt; e.preventDefault());

        panel.appendChild(header);

        if (!isMinimized) {
            const body = document.createElement('div');
            body.style.padding = '14px';
            body.style.maxHeight = '500px';
            body.style.overflowY = 'auto';

            // ========== 状态显示 ==========
            const statusSection = document.createElement('div');
            statusSection.style.marginBottom = '14px';
            statusSection.style.padding = '10px';
            statusSection.style.background = '#1a1a1a';
            statusSection.style.borderRadius = '8px';

            const recordCount = document.createElement('div');
            recordCount.style.marginBottom = '8px';
            recordCount.style.fontSize = '12px';
            recordCount.style.color = '#aaa';
            recordCount.innerHTML = `📋 历史记录: ${historyRecords.length} 条`;

            const controlBtns = document.createElement('div');
            controlBtns.style.display = 'flex';
            controlBtns.style.gap = '8px';
            controlBtns.style.marginTop = '4px';

            const openBtn = document.createElement('button');
            openBtn.textContent = '打开评论区 (X)';
            Object.assign(openBtn.style, {
                flex: '2',
                background: '#4CAF50',
                border: 'none',
                borderRadius: '4px',
                padding: '6px 0',
                color: '#fff',
                fontSize: '12px',
                cursor: 'pointer'
            });
            openBtn.onclick = openCommentPanel;

            const startScrollBtn = document.createElement('button');
            startScrollBtn.textContent = '开始滚动';
            Object.assign(startScrollBtn.style, {
                flex: '1',
                background: '#2196F3',
                border: 'none',
                borderRadius: '4px',
                padding: '6px 0',
                color: '#fff',
                fontSize: '12px',
                cursor: 'pointer'
            });
            startScrollBtn.onclick = startCommentScroll;

            const stopScrollBtn = document.createElement('button');
            stopScrollBtn.textContent = '停止滚动';
            Object.assign(stopScrollBtn.style, {
                flex: '1',
                background: '#ff4d4d',
                border: 'none',
                borderRadius: '4px',
                padding: '6px 0',
                color: '#fff',
                fontSize: '12px',
                cursor: 'pointer'
            });
            stopScrollBtn.onclick = stopCommentScroll;

            controlBtns.appendChild(openBtn);
            controlBtns.appendChild(startScrollBtn);
            controlBtns.appendChild(stopScrollBtn);

            statusSection.appendChild(recordCount);
            statusSection.appendChild(controlBtns);

            // ========== 弹窗模式切换 ==========
            const modeSection = document.createElement('div');
            modeSection.style.marginBottom = '14px';
            modeSection.style.padding = '10px';
            modeSection.style.background = '#1a1a1a';
            modeSection.style.borderRadius = '8px';

            const modeTitle = document.createElement('div');
            modeTitle.style.marginBottom = '8px';
            modeTitle.style.color = '#aaa';
            modeTitle.style.fontSize = '12px';
            modeTitle.innerHTML = '🎨 弹窗模式';

            const modeButtons = document.createElement('div');
            modeButtons.style.display = 'flex';
            modeButtons.style.gap = '6px';

            const leftBtn = document.createElement('button');
            leftBtn.textContent = '左侧';
            leftBtn.style.background = alertMode === 'left' ? '#4CAF50' : '#3a3a3a';
            Object.assign(leftBtn.style, {
                flex: '1',
                border: 'none',
                borderRadius: '4px',
                padding: '6px 0',
                color: '#fff',
                fontSize: '12px',
                cursor: 'pointer'
            });
            leftBtn.onclick = () =&gt; {
                alertMode = 'left';
                leftBtn.style.background = '#4CAF50';
                rightBtn.style.background = '#3a3a3a';
                marqueeBtn.style.background = '#3a3a3a';
                saveConfig();
                showToast('✅ 已切换到左侧弹窗', 1000);
            };

            const rightBtn = document.createElement('button');
            rightBtn.textContent = '右侧';
            rightBtn.style.background = alertMode === 'right' ? '#2196F3' : '#3a3a3a';
            Object.assign(rightBtn.style, {
                flex: '1',
                border: 'none',
                borderRadius: '4px',
                padding: '6px 0',
                color: '#fff',
                fontSize: '12px',
                cursor: 'pointer'
            });
            rightBtn.onclick = () =&gt; {
                alertMode = 'right';
                rightBtn.style.background = '#2196F3';
                leftBtn.style.background = '#3a3a3a';
                marqueeBtn.style.background = '#3a3a3a';
                saveConfig();
                showToast('✅ 已切换到右侧弹窗', 1000);
            };

            const marqueeBtn = document.createElement('button');
            marqueeBtn.textContent = '弹幕';
            marqueeBtn.style.background = alertMode === 'marquee' ? '#FF9800' : '#3a3a3a';
            Object.assign(marqueeBtn.style, {
                flex: '1',
                border: 'none',
                borderRadius: '4px',
                padding: '6px 0',
                color: '#fff',
                fontSize: '12px',
                cursor: 'pointer'
            });
            marqueeBtn.onclick = () =&gt; {
                alertMode = 'marquee';
                marqueeBtn.style.background = '#FF9800';
                leftBtn.style.background = '#3a3a3a';
                rightBtn.style.background = '#3a3a3a';
                saveConfig();
                showToast('✅ 已切换到弹幕模式', 1000);
            };

            modeButtons.appendChild(leftBtn);
            modeButtons.appendChild(rightBtn);
            modeButtons.appendChild(marqueeBtn);

            modeSection.appendChild(modeTitle);
            modeSection.appendChild(modeButtons);

            // ========== 右侧显示数量设置 ==========
            const countSection = document.createElement('div');
            countSection.style.marginBottom = '14px';
            countSection.style.padding = '10px';
            countSection.style.background = '#1a1a1a';
            countSection.style.borderRadius = '8px';

            const countTitle = document.createElement('div');
            countTitle.style.marginBottom = '8px';
            countTitle.style.color = '#aaa';
            countTitle.style.fontSize = '12px';
            countTitle.innerHTML = '📊 右侧弹窗数量';

            const countControl = document.createElement('div');
            countControl.style.display = 'flex';
            countControl.style.alignItems = 'center';
            countControl.style.gap = '8px';

            const countInput = document.createElement('input');
            countInput.type = 'number';
            countInput.min = '1';
            countInput.max = '5';
            countInput.value = maxSideAlerts;
            Object.assign(countInput.style, {
                width: '60px',
                background: '#2a2a2a',
                border: '1px solid #3a3a3a',
                borderRadius: '4px',
                padding: '4px',
                color: '#fff',
                textAlign: 'center'
            });

            const countLabel = document.createElement('span');
            countLabel.style.color = '#ccc';
            countLabel.style.fontSize = '11px';
            countLabel.textContent = '条 (1-5)';

            const countApply = document.createElement('button');
            countApply.textContent = '应用';
            Object.assign(countApply.style, {
                background: '#4CAF50',
                border: 'none',
                borderRadius: '4px',
                padding: '4px 12px',
                color: '#fff',
                fontSize: '11px',
                cursor: 'pointer',
                marginLeft: 'auto'
            });
            countApply.onclick = () =&gt; {
                const val = parseInt(countInput.value);
                if (val &gt;= 1 &amp;&amp; val &lt;= 5) {
                    maxSideAlerts = val;
                    saveConfig();
                    showToast(`✅ 右侧弹窗数量已设为 ${val}`);
                } else {
                    showToast('❌ 请输入1-5之间的数字', 1500, true);
                }
            };

            countControl.appendChild(countInput);
            countControl.appendChild(countLabel);
            countControl.appendChild(countApply);
            countSection.appendChild(countTitle);
            countSection.appendChild(countControl);

            // ========== 视频信息开关 ==========
            const infoSection = document.createElement('div');
            infoSection.style.marginBottom = '14px';
            infoSection.style.padding = '10px';
            infoSection.style.background = '#1a1a1a';
            infoSection.style.borderRadius = '8px';

            const infoToggle = document.createElement('div');
            infoToggle.style.display = 'flex';
            infoToggle.style.justifyContent = 'space-between';
            infoToggle.style.alignItems = 'center';
            infoToggle.style.marginBottom = '8px';

            const infoLabel = document.createElement('span');
            infoLabel.style.color = '#aaa';
            infoLabel.style.fontSize = '12px';
            infoLabel.innerHTML = '📹 显示视频信息';

            const infoCheckbox = document.createElement('input');
            infoCheckbox.type = 'checkbox';
            infoCheckbox.checked = showVideoInfo;
            infoCheckbox.addEventListener('change', (e) =&gt; {
                showVideoInfo = e.target.checked;
                saveConfig();
                showToast(`📹 视频信息: ${e.target.checked ? '显示' : '隐藏'}`);
            });

            infoToggle.appendChild(infoLabel);
            infoToggle.appendChild(infoCheckbox);

            const linkToggle = document.createElement('div');
            linkToggle.style.display = 'flex';
            linkToggle.style.justifyContent = 'space-between';
            linkToggle.style.alignItems = 'center';

            const linkLabel = document.createElement('span');
            linkLabel.style.color = '#aaa';
            linkLabel.style.fontSize = '12px';
            linkLabel.innerHTML = '🔗 显示视频链接';

            const linkCheckbox = document.createElement('input');
            linkCheckbox.type = 'checkbox';
            linkCheckbox.checked = showVideoLink;
            linkCheckbox.addEventListener('change', (e) =&gt; {
                showVideoLink = e.target.checked;
                saveConfig();
                showToast(`🔗 视频链接: ${e.target.checked ? '显示' : '隐藏'}`);
            });

            linkToggle.appendChild(linkLabel);
            linkToggle.appendChild(linkCheckbox);

            infoSection.appendChild(infoToggle);
            infoSection.appendChild(linkToggle);

            // ========== 地区管理 ==========
            const regionSection = document.createElement('div');
            regionSection.style.marginBottom = '14px';
            regionSection.style.padding = '10px';
            regionSection.style.background = '#1a1a1a';
            regionSection.style.borderRadius = '8px';

            const regionHeader = document.createElement('div');
            regionHeader.style.display = 'flex';
            regionHeader.style.justifyContent = 'space-between';
            regionHeader.style.marginBottom = '8px';

            const regionTitle = document.createElement('div');
            regionTitle.style.color = '#aaa';
            regionTitle.style.fontSize = '12px';
            regionTitle.innerHTML = `📍 地区 (${monitorRegions.size})`;

            const regionToggle = document.createElement('label');
            regionToggle.style.display = 'flex';
            regionToggle.style.alignItems = 'center';
            regionToggle.style.gap = '4px';
            regionToggle.style.color = '#4CAF50';
            regionToggle.style.fontSize = '11px';

            const regionCheckbox = document.createElement('input');
            regionCheckbox.type = 'checkbox';
            regionCheckbox.checked = enableRegionFilter;
            regionCheckbox.addEventListener('change', (e) =&gt; {
                enableRegionFilter = e.target.checked;
                saveConfig();
                showToast(`📍 筛选: ${enableRegionFilter ? '开启' : '关闭'}`);
            });

            regionToggle.appendChild(regionCheckbox);
            regionToggle.appendChild(document.createTextNode('开启'));

            regionHeader.appendChild(regionTitle);
            regionHeader.appendChild(regionToggle);

            const regionList = document.createElement('div');
            regionList.style.marginBottom = '8px';
            regionList.style.maxHeight = '70px';
            regionList.style.overflowY = 'auto';
            regionList.style.background = '#252525';
            regionList.style.borderRadius = '4px';
            regionList.style.padding = '4px';

            function renderRegions() {
                regionList.innerHTML = '';
                if (monitorRegions.size === 0) {
                    regionList.innerHTML = '&lt;div style="padding:6px; text-align:center; color:#666;"&gt;暂无&lt;/div&gt;';
                    return;
                }
                monitorRegions.forEach(region =&gt; {
                    const item = document.createElement('div');
                    item.style.display = 'flex';
                    item.style.justifyContent = 'space-between';
                    item.style.padding = '4px 6px';
                    item.style.borderBottom = '1px solid #333';
                    item.style.fontSize = '11px';
                    item.innerHTML = `&lt;span&gt;${region}&lt;/span&gt;`;

                    const delBtn = document.createElement('span');
                    delBtn.textContent = '✕';
                    delBtn.style.cursor = 'pointer';
                    delBtn.style.color = '#ff4d4d';
                    delBtn.style.opacity = '0.6';
                    delBtn.style.padding = '2px 6px';
                    delBtn.onmouseenter = () =&gt; delBtn.style.opacity = '1';
                    delBtn.onmouseleave = () =&gt; delBtn.style.opacity = '0.6';
                    delBtn.onclick = (e) =&gt; {
                        e.stopPropagation();
                        monitorRegions.delete(region);
                        saveConfig();
                        renderRegions();
                        regionTitle.innerHTML = `📍 地区 (${monitorRegions.size})`;
                        showToast(`❌ 已移除: ${region}`);
                    };
                    item.appendChild(delBtn);
                    regionList.appendChild(item);
                });
            }
            renderRegions();

            const regionAddArea = document.createElement('div');
            regionAddArea.style.display = 'flex';
            regionAddArea.style.gap = '4px';

            const regionInput = document.createElement('input');
            regionInput.type = 'text';
            regionInput.placeholder = '地区';
            Object.assign(regionInput.style, {
                flex: '1',
                background: '#2a2a2a',
                border: '1px solid #3a3a3a',
                borderRadius: '4px',
                padding: '4px 8px',
                color: '#fff',
                outline: 'none',
                fontSize: '11px'
            });

            const regionAddBtn = document.createElement('button');
            regionAddBtn.textContent = '+';
            Object.assign(regionAddBtn.style, {
                background: '#4CAF50',
                border: 'none',
                borderRadius: '4px',
                width: '24px',
                color: '#fff',
                cursor: 'pointer',
                fontSize: '14px',
                display: 'flex',
                alignItems: 'center',
                justifyContent: 'center'
            });
            regionAddBtn.onclick = () =&gt; {
                const val = regionInput.value.trim();
                if (val) {
                    monitorRegions.add(val);
                    saveConfig();
                    renderRegions();
                    regionTitle.innerHTML = `📍 地区 (${monitorRegions.size})`;
                    regionInput.value = '';
                    showToast(`✅ 已添加: ${val}`);
                }
            };

            regionAddArea.appendChild(regionInput);
            regionAddArea.appendChild(regionAddBtn);

            regionSection.appendChild(regionHeader);
            regionSection.appendChild(regionList);
            regionSection.appendChild(regionAddArea);

            // ========== 关键词管理 ==========
            const keywordSection = document.createElement('div');
            keywordSection.style.marginBottom = '8px';
            keywordSection.style.padding = '10px';
            keywordSection.style.background = '#1a1a1a';
            keywordSection.style.borderRadius = '8px';

            const keywordHeader = document.createElement('div');
            keywordHeader.style.display = 'flex';
            keywordHeader.style.justifyContent = 'space-between';
            keywordHeader.style.marginBottom = '8px';
            keywordHeader.innerHTML = `
                &lt;span style="color:#aaa; font-size:12px;"&gt;🔑 关键词 (${keywords.size})&lt;/span&gt;
                &lt;span style="color:#4CAF50; font-size:11px;"&gt;批量添加&lt;/span&gt;
            `;

            const keywordList = document.createElement('div');
            keywordList.style.marginBottom = '8px';
            keywordList.style.maxHeight = '80px';
            keywordList.style.overflowY = 'auto';
            keywordList.style.background = '#252525';
            keywordList.style.borderRadius = '4px';
            keywordList.style.padding = '4px';

            function renderKeywords() {
                keywordList.innerHTML = '';
                if (keywords.size === 0) {
                    keywordList.innerHTML = '&lt;div style="padding:6px; text-align:center; color:#666;"&gt;暂无&lt;/div&gt;';
                    return;
                }
                keywords.forEach(kw =&gt; {
                    const item = document.createElement('div');
                    item.style.display = 'flex';
                    item.style.justifyContent = 'space-between';
                    item.style.padding = '4px 6px';
                    item.style.borderBottom = '1px solid #333';
                    item.style.fontSize = '11px';
                    item.innerHTML = `&lt;span&gt;${kw}&lt;/span&gt;`;

                    const delBtn = document.createElement('span');
                    delBtn.textContent = '✕';
                    delBtn.style.cursor = 'pointer';
                    delBtn.style.color = '#ff4d4d';
                    delBtn.style.opacity = '0.6';
                    delBtn.style.padding = '2px 6px';
                    delBtn.onmouseenter = () =&gt; delBtn.style.opacity = '1';
                    delBtn.onmouseleave = () =&gt; delBtn.style.opacity = '0.6';
                    delBtn.onclick = (e) =&gt; {
                        e.stopPropagation();
                        keywords.delete(kw);
                        saveConfig();
                        renderKeywords();
                        keywordHeader.innerHTML = `
                            &lt;span style="color:#aaa; font-size:12px;"&gt;🔑 关键词 (${keywords.size})&lt;/span&gt;
                            &lt;span style="color:#4CAF50; font-size:11px;"&gt;批量添加&lt;/span&gt;
                        `;
                        showToast(`❌ 已删除: ${kw}`);
                    };
                    item.appendChild(delBtn);
                    keywordList.appendChild(item);
                });
            }
            renderKeywords();

            const keywordAddArea = document.createElement('div');
            keywordAddArea.style.display = 'flex';
            keywordAddArea.style.gap = '4px';
            keywordAddArea.style.marginBottom = '8px';

            const keywordInput = document.createElement('input');
            keywordInput.type = 'text';
            keywordInput.placeholder = '添加关键词';
            Object.assign(keywordInput.style, {
                flex: '1',
                background: '#2a2a2a',
                border: '1px solid #3a3a3a',
                borderRadius: '4px',
                padding: '4px 8px',
                color: '#fff',
                outline: 'none',
                fontSize: '11px'
            });

            const keywordAddBtn = document.createElement('button');
            keywordAddBtn.textContent = '+';
            Object.assign(keywordAddBtn.style, {
                background: '#4CAF50',
                border: 'none',
                borderRadius: '4px',
                width: '24px',
                color: '#fff',
                cursor: 'pointer',
                fontSize: '14px',
                display: 'flex',
                alignItems: 'center',
                justifyContent: 'center'
            });
            keywordAddBtn.onclick = () =&gt; {
                const val = keywordInput.value.trim();
                if (val) {
                    keywords.add(val);
                    saveConfig();
                    renderKeywords();
                    keywordHeader.innerHTML = `
                        &lt;span style="color:#aaa; font-size:12px;"&gt;🔑 关键词 (${keywords.size})&lt;/span&gt;
                        &lt;span style="color:#4CAF50; font-size:11px;"&gt;批量添加&lt;/span&gt;
                    `;
                    keywordInput.value = '';
                    showToast(`✅ 已添加: ${val}`);
                }
            };

            keywordAddArea.appendChild(keywordInput);
            keywordAddArea.appendChild(keywordAddBtn);

            const batchTextarea = document.createElement('textarea');
            batchTextarea.placeholder = '批量添加(逗号/空格分隔)';
            Object.assign(batchTextarea.style, {
                width: '100%',
                background: '#2a2a2a',
                border: '1px solid #3a3a3a',
                borderRadius: '4px',
                padding: '6px',
                color: '#fff',
                fontSize: '11px',
                marginTop: '4px',
                resize: 'vertical',
                minHeight: '40px'
            });

            const batchBtn = document.createElement('button');
            batchBtn.textContent = '批量添加';
            Object.assign(batchBtn.style, {
                width: '100%',
                background: '#3a3a3a',
                border: 'none',
                borderRadius: '4px',
                padding: '6px 0',
                color: '#fff',
                fontSize: '11px',
                cursor: 'pointer',
                marginTop: '4px'
            });
            batchBtn.onclick = () =&gt; {
                const raw = batchTextarea.value.trim();
                if (!raw) return;
                const parts = raw.split(/[,\s]+/).filter(p =&gt; p.length &gt; 0);
                let added = 0;
                parts.forEach(p =&gt; {
                    if (!keywords.has(p)) {
                        keywords.add(p);
                        added++;
                    }
                });
                if (added &gt; 0) {
                    saveConfig();
                    renderKeywords();
                    keywordHeader.innerHTML = `
                        &lt;span style="color:#aaa; font-size:12px;"&gt;🔑 关键词 (${keywords.size})&lt;/span&gt;
                        &lt;span style="color:#4CAF50; font-size:11px;"&gt;批量添加&lt;/span&gt;
                    `;
                    showToast(`✅ 批量添加 ${added} 个`);
                }
                batchTextarea.value = '';
            };

            keywordSection.appendChild(keywordHeader);
            keywordSection.appendChild(keywordList);
            keywordSection.appendChild(keywordAddArea);
            keywordSection.appendChild(batchTextarea);
            keywordSection.appendChild(batchBtn);

            body.appendChild(statusSection);
            body.appendChild(modeSection);
            body.appendChild(countSection);
            body.appendChild(infoSection);
            body.appendChild(regionSection);
            body.appendChild(keywordSection);

            panel.appendChild(body);
        }

        document.body.appendChild(panel);
    }

    // ==================== 初始化 ====================
    function init() {
        console.log('抖音评论监控插件 v2.7 已启动 (右侧最新两条)');
        createPanel();

        if (autoOpenComment) {
            setTimeout(openCommentPanel, 2000);
        }

        setInterval(() =&gt; {
            if (!commentContainer) {
                commentContainer = findCommentContainer();
            }
        }, 3000);
    }

    // 注册菜单
    if (typeof GM_registerMenuCommand !== 'undefined') {
        GM_registerMenuCommand('📋 打开监控面板', createPanel);
        GM_registerMenuCommand('📋 查看历史记录', showHistoryWindow);
        GM_registerMenuCommand('📥 导出历史记录', exportHistoryToCSV);
        GM_registerMenuCommand('💬 打开评论区 (X)', openCommentPanel);
        GM_registerMenuCommand('▶️ 开始滚动', startCommentScroll);
        GM_registerMenuCommand('⏸️ 停止滚动', stopCommentScroll);
        GM_registerMenuCommand('💾 保存配置', saveConfig);
    }

    // 启动
    if (document.body) {
        setTimeout(init, 1500);
    } else {
        document.addEventListener('DOMContentLoaded', () =&gt; setTimeout(init, 1500));
    }

})();</code></pre>]]></description>
    <pubDate>Fri, 20 Mar 2026 16:24:22 +0800</pubDate>
    <dc:creator>emer</dc:creator>
    <guid>https://deepsider.cn/post-2.html</guid>
</item>
</channel>
</rss>