搜索结果

×

搜索结果将在这里显示。

EMLOG 智能文章发布工具 v2.0

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("<ButtonRelease-1>", self.on_article_select)
        self.article_tree.bind("<Double-1>", 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) > 1 and count > 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) < 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) > 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) > 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']) > 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 <= idx < 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()
发布时间: