Files
douyin-imbot/douyin_imbot/ui/profile_manager_panel.py
2025-04-02 21:01:25 +08:00

1393 lines
55 KiB
Python

import os
import tkinter as tk
from tkinter import filedialog, messagebox
import customtkinter as ctk
import logging
import traceback
from datetime import datetime
from typing import Callable, Optional, Dict, Any, List
from douyin_imbot.browser.browser_manager import BrowserManager
logger = logging.getLogger(__name__)
class ProfileManagerPanel(ctk.CTkFrame):
"""浏览器配置文件管理界面."""
def __init__(self, parent, browser_manager, on_profile_change=None):
"""
初始化配置管理界面.
Args:
parent: 父组件
browser_manager: 浏览器管理器实例
on_profile_change: 配置变更时的回调函数
"""
try:
super().__init__(parent)
self.browser_manager = browser_manager
self.on_profile_change = on_profile_change
self.profiles = []
self.running_profiles = (
{}
) # 存储正在运行的配置文件 profile_name: browser_instance
# 配置网格
self.grid_columnconfigure(0, weight=1)
self.grid_rowconfigure(1, weight=1)
# 创建UI组件
self._create_header()
self._create_table()
# 加载配置文件列表
self.refresh_profiles()
logger.info("配置文件管理面板初始化完成")
except Exception as e:
logger.error(f"初始化配置文件管理面板失败: {str(e)}")
logger.error(traceback.format_exc())
raise
def _create_header(self):
"""创建头部区域,包含标题和添加按钮."""
try:
header_frame = ctk.CTkFrame(self)
header_frame.grid(row=0, column=0, sticky="ew", padx=10, pady=(10, 5))
header_frame.grid_columnconfigure(0, weight=1)
# 标题
title = ctk.CTkLabel(
header_frame,
text="浏览器配置文件管理",
font=ctk.CTkFont(size=18, weight="bold"),
)
title.grid(row=0, column=0, sticky="w", padx=10, pady=5)
# 添加配置按钮(右上角)
add_button = ctk.CTkButton(
header_frame,
text="添加配置",
command=self.add_profile,
width=120,
height=32,
)
add_button.grid(row=0, column=1, sticky="e", padx=10, pady=5)
logger.debug("创建头部区域完成")
except Exception as e:
logger.error(f"创建头部区域失败: {str(e)}")
logger.error(traceback.format_exc())
def _create_table(self):
"""创建配置文件表格界面."""
try:
# 表格容器
table_frame = ctk.CTkFrame(self)
table_frame.grid(row=1, column=0, sticky="nsew", padx=10, pady=5)
table_frame.grid_columnconfigure((0, 1, 2, 3, 4), weight=1)
# 添加选择列
headers = ["配置名称", "用户数据目录", "最后使用时间", "操作"]
for i, header in enumerate(headers):
header_label = ctk.CTkLabel(
table_frame,
text=header,
font=ctk.CTkFont(weight="bold"),
anchor="center",
)
header_label.grid(row=0, column=i, sticky="ew", padx=5, pady=5)
# 保存表格框架的引用,以便后续更新
self.table_frame = table_frame
# 创建表格内容的滚动区域
self.content_frame = ctk.CTkScrollableFrame(table_frame)
self.content_frame.grid(
row=1, column=0, columnspan=5, sticky="nsew", padx=5, pady=5
)
self.content_frame.grid_columnconfigure((0, 1, 2, 3, 4), weight=1)
# 使表格框架可滚动
table_frame.grid_rowconfigure(1, weight=1)
# 存储选择变量
self.selected_profiles = {}
logger.debug("创建表格界面完成")
except Exception as e:
logger.error(f"创建表格界面失败: {str(e)}")
logger.error(traceback.format_exc())
def _truncate_text(self, text, max_length=25):
"""
截断文本,使其不超过最大长度,并在截断处添加省略号。
Args:
text: 要截断的文本
max_length: 最大长度(默认25个字符)
Returns:
截断后的文本
"""
if not text:
return ""
if len(text) <= max_length:
return text
# 保留路径的特性,尝试保留开头和结尾
if "/" in text or "\\" in text:
# 对于路径,保留开头的几个字符和结尾的几个字符
parts = text.split(os.path.sep)
if len(parts) > 2:
# 保留第一部分和最后一部分
return f"{parts[0]}/.../{parts[-1]}"
else:
# 简单地从中间截断
return f"{text[:max_length//2]}...{text[-max_length//2:]}"
else:
# 非路径,简单地截断并添加省略号
return text[: max_length - 3] + "..."
def _create_tooltip(self, widget, text):
"""
为小部件创建一个简单的工具提示。
Args:
widget: 要添加工具提示的小部件
text: 工具提示显示的文本
"""
if not text:
return
def show_tooltip(event):
x, y, _, _ = widget.bbox("insert")
x += widget.winfo_rootx() + 25
y += widget.winfo_rooty() + 25
# 创建一个新的顶层窗口
tooltip = tk.Toplevel(widget)
tooltip.wm_overrideredirect(True)
tooltip.wm_geometry(f"+{x}+{y}")
label = tk.Label(
tooltip,
text=text,
justify="left",
background="#ffffe0",
relief="solid",
borderwidth=1,
font=("TkDefaultFont", "10", "normal"),
)
label.pack(padx=2, pady=2)
widget.tooltip = tooltip
def hide_tooltip(event):
if hasattr(widget, "tooltip"):
widget.tooltip.destroy()
widget.bind("<Enter>", show_tooltip)
widget.bind("<Leave>", hide_tooltip)
def refresh_profiles(self):
"""刷新配置文件列表."""
try:
# 获取所有配置文件
self.profiles = self.browser_manager.get_profiles() or []
# 清空表格内容
for widget in self.content_frame.winfo_children():
widget.destroy()
# 重置选择状态
self.selected_profiles = {}
if not self.profiles:
# 显示无数据提示
no_data_label = ctk.CTkLabel(
self.content_frame,
text="没有可用的配置文件,请点击'添加配置'按钮创建",
font=ctk.CTkFont(size=14),
anchor="center",
)
no_data_label.grid(
row=0, column=0, columnspan=5, sticky="ew", padx=10, pady=20
)
logger.info("没有找到配置文件")
return
# 添加所有配置文件到表格
for i, profile_name in enumerate(self.profiles):
# 获取详情
details = self.browser_manager.get_profile_details(profile_name) or {}
user_data_dir = details.get("user_data_dir", "未知")
last_used = details.get("last_used", "从未")
douyin_account = details.get("douyin_account", "未设置")
# 截断长文本以确保表格正常显示
display_name = self._truncate_text(profile_name, 20)
display_dir = self._truncate_text(user_data_dir, 30)
display_account = self._truncate_text(douyin_account, 20)
# 配置名称和抖音账号合并显示
name_text = f"{display_name} ({display_account})"
name_label = ctk.CTkLabel(
self.content_frame, text=name_text, anchor="w"
)
name_label.grid(row=i, column=1, sticky="ew", padx=5, pady=5)
# 为完整文本添加工具提示
full_name_text = f"配置名: {profile_name}\n抖音账号: {douyin_account}"
self._create_tooltip(name_label, full_name_text)
# 用户数据目录
dir_label = ctk.CTkLabel(
self.content_frame, text=display_dir, anchor="w"
)
dir_label.grid(row=i, column=2, sticky="ew", padx=5, pady=5)
# 为完整路径添加工具提示
if display_dir != user_data_dir:
self._create_tooltip(dir_label, user_data_dir)
# 最后使用时间
last_used_label = ctk.CTkLabel(
self.content_frame, text=last_used, anchor="w"
)
last_used_label.grid(row=i, column=3, sticky="ew", padx=5, pady=5)
# 状态
is_running = profile_name in self.running_profiles
status_color = "green" if is_running else "red"
status_text = "运行中" if is_running else "已停止"
status_label = ctk.CTkLabel(
self.content_frame,
text=status_text,
text_color=status_color,
anchor="center",
)
status_label.grid(row=i, column=3, sticky="ew", padx=5, pady=5)
# 操作按钮
actions_frame = ctk.CTkFrame(self.content_frame, fg_color="transparent")
actions_frame.grid(row=i, column=4, sticky="ew", padx=5, pady=2)
# 编辑按钮
edit_button = ctk.CTkButton(
actions_frame,
text="编辑",
command=lambda p=profile_name: self.edit_profile(p),
width=60,
height=28,
)
edit_button.grid(row=0, column=0, padx=2, pady=2)
# 删除按钮
delete_button = ctk.CTkButton(
actions_frame,
text="删除",
command=lambda p=profile_name: self.delete_profile(p),
width=60,
height=28,
fg_color="#d9534f",
hover_color="#c9302c",
)
delete_button.grid(row=0, column=1, padx=2, pady=2)
# 启动/停止按钮
if is_running:
stop_button = ctk.CTkButton(
actions_frame,
text="停止",
command=lambda p=profile_name: self.stop_robot(p),
width=60,
height=28,
fg_color="#f0ad4e",
hover_color="#ec971f",
)
stop_button.grid(row=0, column=2, padx=2, pady=2)
else:
start_button = ctk.CTkButton(
actions_frame,
text="启动",
command=lambda p=profile_name: self.start_robot(p),
width=60,
height=28,
fg_color="#5cb85c",
hover_color="#449d44",
)
start_button.grid(row=0, column=2, padx=2, pady=2)
logger.info(f"刷新了 {len(self.profiles)} 个配置文件")
except Exception as e:
logger.error(f"刷新配置文件列表失败: {str(e)}")
logger.error(traceback.format_exc())
def add_profile(self):
"""添加新的配置文件."""
try:
# 创建对话框
dialog = ctk.CTkToplevel(self)
dialog.title("添加配置文件")
dialog.geometry("400x350") # 增加高度以适应抖音账号字段
dialog.transient(self.winfo_toplevel())
dialog.grab_set() # 模态对话框
# 配置对话框
dialog.grid_columnconfigure(0, weight=1)
# 标题
title = ctk.CTkLabel(
dialog, text="添加新配置文件", font=ctk.CTkFont(size=16, weight="bold")
)
title.grid(row=0, column=0, padx=20, pady=(20, 10))
# 表单
form_frame = ctk.CTkFrame(dialog)
form_frame.grid(row=1, column=0, padx=20, pady=10, sticky="ew")
form_frame.grid_columnconfigure(1, weight=1)
# 配置名称
name_label = ctk.CTkLabel(form_frame, text="配置名称:")
name_label.grid(row=0, column=0, padx=10, pady=10, sticky="w")
name_entry = ctk.CTkEntry(form_frame, width=250)
name_entry.grid(row=0, column=1, padx=10, pady=10, sticky="ew")
# 抖音账号
douyin_label = ctk.CTkLabel(form_frame, text="抖音账号:")
douyin_label.grid(row=1, column=0, padx=10, pady=10, sticky="w")
douyin_entry = ctk.CTkEntry(form_frame, width=250)
douyin_entry.grid(row=1, column=1, padx=10, pady=10, sticky="ew")
# 用户数据目录说明
dir_info_label = ctk.CTkLabel(
form_frame,
text="用户数据目录将自动生成",
font=ctk.CTkFont(size=12, slant="italic"),
text_color="gray",
)
dir_info_label.grid(
row=2, column=0, columnspan=2, padx=10, pady=(0, 10), sticky="w"
)
# 按钮区域
button_frame = ctk.CTkFrame(dialog, fg_color="transparent")
button_frame.grid(row=2, column=0, padx=20, pady=20)
# 保存按钮
save_button = ctk.CTkButton(
button_frame,
text="保存",
command=lambda: self._save_profile(
dialog, name_entry.get(), douyin_entry.get()
),
width=100,
)
save_button.grid(row=0, column=0, padx=10)
# 取消按钮
cancel_button = ctk.CTkButton(
button_frame,
text="取消",
command=dialog.destroy,
width=100,
fg_color="#6c757d",
hover_color="#5a6268",
)
cancel_button.grid(row=0, column=1, padx=10)
logger.info("打开添加配置对话框")
except Exception as e:
logger.error(f"打开添加配置对话框失败: {str(e)}")
logger.error(traceback.format_exc())
def _save_profile(self, dialog, profile_name, douyin_account):
"""
保存新配置文件.
Args:
dialog: 对话框实例
profile_name: 配置文件名称
douyin_account: 抖音账号
"""
try:
if not profile_name:
messagebox.showerror("错误", "配置名称不能为空", parent=dialog)
return
if not douyin_account:
messagebox.showerror("错误", "抖音账号不能为空", parent=dialog)
return
# 检查是否已存在同名配置
existing_profiles = self.browser_manager.get_profiles() or []
if profile_name in existing_profiles:
messagebox.showerror(
"错误", f"配置名称 '{profile_name}' 已存在", parent=dialog
)
return
# 创建配置文件 - 将抖音账号信息传递给创建函数
# 注意配置文件目录会在browser_manager中自动生成
success = self.browser_manager.create_profile(
profile_name=profile_name, douyin_account=douyin_account
)
if success:
logger.info(
f"成功创建配置文件: {profile_name}, 抖音账号: {douyin_account}"
)
messagebox.showinfo(
"成功",
f"配置文件 '{profile_name}' 已成功创建\n关联抖音账号: {douyin_account}",
parent=dialog,
)
# 关闭对话框
dialog.destroy()
# 刷新列表
self.refresh_profiles()
# 调用回调函数
if self.on_profile_change:
self.on_profile_change()
else:
logger.error(f"创建配置文件失败: {profile_name}")
messagebox.showerror(
"错误", f"无法创建配置文件 '{profile_name}'", parent=dialog
)
except Exception as e:
logger.error(f"保存配置文件时出错: {str(e)}")
logger.error(traceback.format_exc())
messagebox.showerror("错误", f"保存配置文件时出错: {str(e)}", parent=dialog)
def edit_profile(self, profile_name):
"""编辑配置文件."""
try:
# 获取配置详情
details = self.browser_manager.get_profile_details(profile_name) or {}
user_data_dir = details.get("user_data_dir", "")
douyin_account = details.get("douyin_account", "")
# 创建对话框
dialog = ctk.CTkToplevel(self)
dialog.title(f"编辑配置: {profile_name}")
dialog.geometry("400x350") # 增加高度以适应抖音账号字段
dialog.transient(self.winfo_toplevel())
dialog.grab_set() # 模态对话框
# 配置对话框
dialog.grid_columnconfigure(0, weight=1)
# 标题
title = ctk.CTkLabel(
dialog,
text=f"编辑配置: {profile_name}",
font=ctk.CTkFont(size=16, weight="bold"),
)
title.grid(row=0, column=0, padx=20, pady=(20, 10))
# 表单
form_frame = ctk.CTkFrame(dialog)
form_frame.grid(row=1, column=0, padx=20, pady=10, sticky="ew")
form_frame.grid_columnconfigure(1, weight=1)
# 配置名称
name_label = ctk.CTkLabel(form_frame, text="配置名称:")
name_label.grid(row=0, column=0, padx=10, pady=10, sticky="w")
name_entry = ctk.CTkEntry(form_frame, width=250)
name_entry.grid(row=0, column=1, padx=10, pady=10, sticky="ew")
name_entry.insert(0, profile_name)
# 抖音账号
douyin_label = ctk.CTkLabel(form_frame, text="抖音账号:")
douyin_label.grid(row=1, column=0, padx=10, pady=10, sticky="w")
douyin_entry = ctk.CTkEntry(form_frame, width=250)
douyin_entry.grid(row=1, column=1, padx=10, pady=10, sticky="ew")
douyin_entry.insert(0, douyin_account)
# 用户数据目录(只读)
dir_label = ctk.CTkLabel(form_frame, text="用户数据目录:")
dir_label.grid(row=2, column=0, padx=10, pady=10, sticky="w")
dir_entry = ctk.CTkEntry(form_frame, width=250, state="readonly")
dir_entry.grid(row=2, column=1, padx=10, pady=10, sticky="ew")
dir_entry.insert(0, user_data_dir)
# 按钮区域
button_frame = ctk.CTkFrame(dialog, fg_color="transparent")
button_frame.grid(row=2, column=0, padx=20, pady=20)
# 保存按钮
save_button = ctk.CTkButton(
button_frame,
text="保存",
command=lambda: self._update_profile(
dialog, profile_name, name_entry.get(), douyin_entry.get()
),
width=100,
)
save_button.grid(row=0, column=0, padx=10)
# 取消按钮
cancel_button = ctk.CTkButton(
button_frame,
text="取消",
command=dialog.destroy,
width=100,
fg_color="#6c757d",
hover_color="#5a6268",
)
cancel_button.grid(row=0, column=1, padx=10)
logger.info(f"打开编辑配置对话框: {profile_name}")
except Exception as e:
logger.error(f"打开编辑配置对话框失败: {str(e)}")
logger.error(traceback.format_exc())
def _update_profile(self, dialog, old_name, new_name, douyin_account):
"""
更新配置文件.
Args:
dialog: 对话框实例
old_name: 原配置文件名称
new_name: 新配置文件名称
douyin_account: 抖音账号
"""
try:
if not new_name:
messagebox.showerror("错误", "配置名称不能为空", parent=dialog)
return
if not douyin_account:
messagebox.showerror("错误", "抖音账号不能为空", parent=dialog)
return
# 如果名称发生变化,检查是否已存在
if old_name != new_name:
existing_profiles = self.browser_manager.get_profiles() or []
if new_name in existing_profiles:
messagebox.showerror(
"错误", f"配置名称 '{new_name}' 已存在", parent=dialog
)
return
# 更新配置文件,包括抖音账号信息
success = self.browser_manager.update_profile(
old_name, new_name=new_name, douyin_account=douyin_account
)
if success:
logger.info(f"成功更新配置文件: {old_name} -> {new_name}")
messagebox.showinfo("成功", f"配置文件已成功更新", parent=dialog)
# 如果正在运行中,更新运行列表
if old_name in self.running_profiles:
self.running_profiles[new_name] = self.running_profiles.pop(
old_name
)
# 关闭对话框
dialog.destroy()
# 刷新列表
self.refresh_profiles()
# 调用回调函数
if self.on_profile_change:
self.on_profile_change()
else:
logger.error(f"更新配置文件失败: {old_name} -> {new_name}")
messagebox.showerror("错误", f"无法更新配置文件", parent=dialog)
except Exception as e:
logger.error(f"更新配置文件时出错: {str(e)}")
logger.error(traceback.format_exc())
messagebox.showerror("错误", f"更新配置文件时出错: {str(e)}", parent=dialog)
def delete_profile(self, profile_name):
"""删除配置文件."""
try:
# 确认对话框
confirm = messagebox.askyesno(
"确认删除",
f"您确定要删除配置文件 '{profile_name}' 吗?此操作不可撤销。",
parent=self.winfo_toplevel(),
)
if confirm:
self.update_delete_status(f"正在删除配置文件 '{profile_name}'...")
# 如果正在运行,先停止
if profile_name in self.running_profiles:
self.stop_robot(profile_name)
# 获取配置详情,记录目录便于后续验证
details = self.browser_manager.get_profile_details(profile_name) or {}
profile_dir = details.get("user_data_dir", "")
# 删除配置文件,强制使用delete_files=True
success = self.browser_manager.delete_profile(
profile_name, delete_files=True
)
# 验证删除结果
dir_deleted = not profile_dir or not os.path.exists(profile_dir)
if success and dir_deleted:
logger.info(
f"成功删除配置文件: {profile_name}, 目录已删除: {profile_dir}"
)
messagebox.showinfo(
"成功",
f"配置文件 '{profile_name}' 已成功删除",
parent=self.winfo_toplevel(),
)
# 如果在选择列表中,移除它
if profile_name in self.selected_profiles:
del self.selected_profiles[profile_name]
# 强制清除缓存
self.browser_manager.clear_profile_cache()
# 刷新列表
self.refresh_profiles()
# 调用回调函数
if self.on_profile_change:
self.on_profile_change()
else:
# 处理删除失败情况
failure_reason = ""
if not success:
failure_reason = "配置记录删除失败"
elif not dir_deleted:
failure_reason = f"目录删除失败 ({profile_dir})"
logger.error(f"删除配置文件失败: {profile_name} - {failure_reason}")
# 询问是否强制从列表中移除
force_remove = messagebox.askyesno(
"删除失败",
f"无法完全删除配置文件 '{profile_name}'。{failure_reason}\n"
f"是否从列表中强制移除此配置?",
parent=self.winfo_toplevel(),
)
if force_remove:
# 尝试手动清理列表
try:
# 从浏览器管理器的profiles缓存中移除
success = self._force_remove_profile(profile_name)
if success:
logger.info(f"强制移除配置文件成功: {profile_name}")
messagebox.showinfo(
"成功",
f"配置文件 '{profile_name}' 已从列表中移除",
parent=self.winfo_toplevel(),
)
# 如果在选择列表中,移除它
if profile_name in self.selected_profiles:
del self.selected_profiles[profile_name]
# 强制清除缓存
self.browser_manager.clear_profile_cache()
# 刷新列表
self.refresh_profiles()
# 调用回调函数
if self.on_profile_change:
self.on_profile_change()
else:
logger.error(f"强制移除配置文件失败: {profile_name}")
messagebox.showerror(
"错误",
f"无法从列表中移除配置文件 '{profile_name}'",
parent=self.winfo_toplevel(),
)
except Exception as e:
logger.error(f"强制移除配置文件时出错: {str(e)}")
logger.error(traceback.format_exc())
messagebox.showerror(
"错误",
f"强制移除配置文件时出错: {str(e)}",
parent=self.winfo_toplevel(),
)
else:
messagebox.showerror(
"错误",
f"无法删除配置文件 '{profile_name}'",
parent=self.winfo_toplevel(),
)
except Exception as e:
logger.error(f"删除配置文件时出错: {str(e)}")
logger.error(traceback.format_exc())
messagebox.showerror(
"错误", f"删除配置文件时出错: {str(e)}", parent=self.winfo_toplevel()
)
return False
return True
def update_delete_status(self, message):
"""更新删除状态到日志"""
try:
# 尝试访问主窗口的日志更新方法
# 假设主窗口有一个update_log方法
main_window = self.winfo_toplevel()
if hasattr(main_window, "update_log"):
main_window.update_log(message)
else:
# 记录到logger
logger.info(message)
except Exception as e:
logger.error(f"更新删除状态失败: {str(e)}")
def _force_remove_profile(self, profile_name):
"""
强制从配置记录中移除配置文件。
当配置文件实际已不存在但仍在列表中显示时使用此方法。
Args:
profile_name: 配置文件名称
Returns:
bool: 是否成功移除
"""
try:
# 检查是否有配置文件记录的设置文件
from douyin_imbot.config.settings import load_settings, save_settings
# 加载设置
settings = load_settings()
# 检查是否有browser_profiles设置
if "browser_profiles" in settings and isinstance(
settings["browser_profiles"], list
):
profiles = settings["browser_profiles"]
# 寻找匹配的配置
for i, profile in enumerate(profiles):
if (
isinstance(profile, dict)
and profile.get("name") == profile_name
):
# 找到了,从列表中移除
profiles.pop(i)
# 保存修改后的设置
settings["browser_profiles"] = profiles
if save_settings(settings):
logger.info(f"成功从设置中移除配置记录: {profile_name}")
return True
else:
logger.error(
f"保存设置失败,无法移除配置记录: {profile_name}"
)
return False
# 如果运行到这里,说明没有找到配置记录或没有browser_profiles设置
logger.warning(f"在设置中未找到配置记录: {profile_name}")
# 即使没有找到记录,也返回成功,因为我们的目的是从列表中移除
return True
except Exception as e:
logger.error(f"强制移除配置记录时出错: {str(e)}")
logger.error(traceback.format_exc())
return False
def start_robot(self, profile_name):
"""启动机器人."""
try:
if profile_name in self.running_profiles:
logger.warning(f"机器人已经在运行中: {profile_name}")
messagebox.showinfo(
"提示",
f"机器人 '{profile_name}' 已经在运行中",
parent=self.winfo_toplevel(),
)
return False
# 检查配置文件是否有效
details = self.browser_manager.get_profile_details(profile_name) or {}
if details.get("error") or not details.get("is_valid", False):
logger.error(f"配置文件不存在或无效: {profile_name}")
messagebox.showerror(
"错误",
f"配置文件 '{profile_name}' 不存在或已损坏,请先创建配置文件",
parent=self.winfo_toplevel(),
)
return False
# 确保使用正确的配置文件名称启动浏览器
self.update_delete_status(
f"正在使用配置文件 '{profile_name}' 启动机器人..."
)
# 启动浏览器 - 明确指定使用profile_name
browser = self.browser_manager.launch_browser(
profile_name=profile_name, headless=False
)
if browser:
# 导航到抖音
success = self.browser_manager.navigate_to("https://www.douyin.com/")
if not success:
logger.warning("导航到抖音网站失败")
self.update_delete_status("导航到抖音网站失败,但浏览器已启动")
# 记录正在运行的浏览器
self.running_profiles[profile_name] = browser
logger.info(f"成功启动机器人: {profile_name}")
messagebox.showinfo(
"成功",
f"机器人 '{profile_name}' 已成功启动",
parent=self.winfo_toplevel(),
)
# 刷新界面
self.refresh_profiles()
return True
else:
logger.error(f"启动机器人失败: {profile_name}")
messagebox.showerror(
"错误",
f"无法启动机器人 '{profile_name}',请检查配置文件是否有效",
parent=self.winfo_toplevel(),
)
return False
except Exception as e:
logger.error(f"启动机器人时出错: {str(e)}")
logger.error(traceback.format_exc())
messagebox.showerror(
"错误", f"启动机器人时出错: {str(e)}", parent=self.winfo_toplevel()
)
return False
def stop_robot(self, profile_name):
"""停止机器人."""
try:
if profile_name not in self.running_profiles:
logger.warning(f"机器人未在运行: {profile_name}")
messagebox.showinfo(
"提示",
f"机器人 '{profile_name}' 未在运行",
parent=self.winfo_toplevel(),
)
return
# 关闭浏览器
self.browser_manager.close_browser()
# 从运行列表中移除
if profile_name in self.running_profiles:
del self.running_profiles[profile_name]
logger.info(f"成功停止机器人: {profile_name}")
messagebox.showinfo(
"成功",
f"机器人 '{profile_name}' 已成功停止",
parent=self.winfo_toplevel(),
)
# 刷新界面
self.refresh_profiles()
except Exception as e:
logger.error(f"停止机器人时出错: {str(e)}")
logger.error(traceback.format_exc())
messagebox.showerror(
"错误", f"停止机器人时出错: {str(e)}", parent=self.winfo_toplevel()
)
def get_selected_profiles(self):
"""获取选中的配置文件列表"""
try:
selected = []
for profile_name, var in self.selected_profiles.items():
if var.get():
selected.append(profile_name)
return selected
except Exception as e:
logger.error(f"获取选中配置文件失败: {str(e)}")
logger.error(traceback.format_exc())
return []
def create_profile_list(self):
"""创建配置文件列表区域"""
try:
# 配置文件列表框
self.profile_list_frame = ctk.CTkFrame(self)
self.profile_list_frame.grid(row=1, column=0, columnspan=2, padx=10, pady=10, sticky="nsew")
self.profile_list_frame.grid_columnconfigure(0, weight=1)
self.profile_list_frame.grid_rowconfigure(1, weight=1)
# 列表标题
list_label = ctk.CTkLabel(self.profile_list_frame, text="可用配置文件", anchor="w")
list_label.grid(row=0, column=0, padx=10, pady=5, sticky="w")
# 配置文件列表
self.profile_listbox = ctk.CTkOptionMenu(
self.profile_list_frame,
values=["无可用配置"],
command=self.on_profile_selected
)
self.profile_listbox.grid(row=1, column=0, padx=10, pady=(0, 5), sticky="ew")
# 刷新按钮
refresh_button = ctk.CTkButton(
self.profile_list_frame,
text="刷新",
width=80,
command=self.refresh_profiles
)
refresh_button.grid(row=2, column=0, padx=10, pady=(0, 10), sticky="e")
except Exception as e:
logger.error(f"创建配置文件列表失败: {str(e)}")
logger.error(traceback.format_exc())
def create_profile_details(self):
"""创建配置文件详情区域"""
try:
# 配置文件详情框
self.details_frame = ctk.CTkFrame(self)
self.details_frame.grid(row=1, column=2, columnspan=2, padx=10, pady=10, sticky="nsew")
self.details_frame.grid_columnconfigure(0, weight=1)
self.details_frame.grid_rowconfigure(2, weight=1)
# 详情标题
details_label = ctk.CTkLabel(self.details_frame, text="配置详情", anchor="w")
details_label.grid(row=0, column=0, padx=10, pady=5, sticky="w")
# 详情内容
self.profile_details_label = ctk.CTkLabel(
self.details_frame,
text="未选择配置文件",
justify="left",
anchor="w"
)
self.profile_details_label.grid(row=1, column=0, padx=10, pady=5, sticky="w")
# 运行状态
self.status_label = ctk.CTkLabel(
self.details_frame,
text="状态: 未运行",
text_color="gray"
)
self.status_label.grid(row=2, column=0, padx=10, pady=5, sticky="w")
except Exception as e:
logger.error(f"创建配置文件详情区域失败: {str(e)}")
logger.error(traceback.format_exc())
def create_control_buttons(self):
"""创建控制按钮区域"""
try:
# 控制按钮区域
self.control_frame = ctk.CTkFrame(self)
self.control_frame.grid(row=2, column=0, columnspan=4, padx=10, pady=10, sticky="ew")
self.control_frame.grid_columnconfigure((0, 1, 2, 3), weight=1, uniform="equal")
# 创建配置按钮
self.create_button = ctk.CTkButton(
self.control_frame,
text="新建配置",
command=self.create_profile
)
self.create_button.grid(row=0, column=0, padx=10, pady=10, sticky="ew")
# 删除配置按钮
self.delete_button = ctk.CTkButton(
self.control_frame,
text="删除配置",
command=self.delete_profile
)
self.delete_button.grid(row=0, column=1, padx=10, pady=10, sticky="ew")
# 启动浏览器按钮
self.launch_button = ctk.CTkButton(
self.control_frame,
text="启动浏览器",
command=self.launch_browser
)
self.launch_button.grid(row=0, column=2, padx=10, pady=10, sticky="ew")
# 关闭浏览器按钮
self.close_button = ctk.CTkButton(
self.control_frame,
text="关闭浏览器",
command=self.close_browser
)
self.close_button.grid(row=0, column=3, padx=10, pady=10, sticky="ew")
except Exception as e:
logger.error(f"创建控制按钮区域失败: {str(e)}")
logger.error(traceback.format_exc())
def create_multi_browser_controls(self):
"""创建多浏览器控制区域"""
try:
# 多浏览器控制区域
self.multi_browser_frame = ctk.CTkFrame(self)
self.multi_browser_frame.grid(row=3, column=0, columnspan=4, padx=10, pady=10, sticky="ew")
self.multi_browser_frame.grid_columnconfigure((0, 1, 2), weight=1, uniform="equal")
# 标题
multi_title = ctk.CTkLabel(
self.multi_browser_frame,
text="多浏览器控制",
font=ctk.CTkFont(size=14, weight="bold")
)
multi_title.grid(row=0, column=0, columnspan=3, padx=10, pady=(10, 5), sticky="w")
# 启动全部浏览器按钮
self.launch_all_button = ctk.CTkButton(
self.multi_browser_frame,
text="启动全部浏览器",
command=self.launch_all_browsers
)
self.launch_all_button.grid(row=1, column=0, padx=10, pady=10, sticky="ew")
# 关闭全部浏览器按钮
self.close_all_button = ctk.CTkButton(
self.multi_browser_frame,
text="关闭全部浏览器",
command=self.close_all_browsers
)
self.close_all_button.grid(row=1, column=1, padx=10, pady=10, sticky="ew")
# 浏览器状态指示
self.browser_status = ctk.CTkLabel(
self.multi_browser_frame,
text="浏览器: 0/0 运行中",
font=ctk.CTkFont(size=14)
)
self.browser_status.grid(row=1, column=2, padx=10, pady=10)
except Exception as e:
logger.error(f"创建多浏览器控制区域失败: {str(e)}")
logger.error(traceback.format_exc())
def refresh_profiles(self):
"""刷新配置文件列表"""
try:
profiles = self.browser_manager.get_profiles()
if not profiles:
self.profile_listbox.configure(values=["无可用配置"])
self.profile_listbox.set("无可用配置")
self.current_profile = None
return
# 更新下拉框
self.profile_listbox.configure(values=profiles)
# 如果当前选择的配置不在列表中,重置选择
if self.current_profile not in profiles:
self.profile_listbox.set(profiles[0])
self.current_profile = profiles[0]
self.on_profile_selected(profiles[0])
else:
self.profile_listbox.set(self.current_profile)
# 更新浏览器状态
self.update_browser_status()
logger.debug(f"已刷新配置文件列表,共 {len(profiles)} 个配置")
except Exception as e:
logger.error(f"刷新配置文件列表失败: {str(e)}")
logger.error(traceback.format_exc())
def update_browser_status(self):
"""更新浏览器状态显示"""
try:
total_profiles = len(self.browser_manager.get_profiles())
running_browsers = len(self.browser_manager.browsers)
self.browser_status.configure(text=f"浏览器: {running_browsers}/{total_profiles} 运行中")
# 更新当前选择的配置文件的状态
if self.current_profile:
if self.current_profile in self.browser_manager.browsers:
self.status_label.configure(text="状态: 运行中", text_color="green")
else:
self.status_label.configure(text="状态: 未运行", text_color="gray")
except Exception as e:
logger.error(f"更新浏览器状态失败: {str(e)}")
def on_profile_selected(self, profile_name):
"""当用户选择配置文件时触发"""
try:
if not profile_name or profile_name == "无可用配置":
self.current_profile = None
self.profile_details_label.configure(text="未选择配置文件")
return
self.current_profile = profile_name
logger.info(f"选择了配置文件: {profile_name}")
# 更新配置文件详情
profile_details = self.browser_manager.get_profile_details(profile_name) or {}
details_text = f"用户目录: {profile_details.get('user_data_dir', '无')}\n"
details_text += f"上次使用: {profile_details.get('last_used', '从未')}"
self.profile_details_label.configure(text=details_text)
# 更新状态显示
if profile_name in self.browser_manager.browsers:
self.status_label.configure(text="状态: 运行中", text_color="green")
else:
self.status_label.configure(text="状态: 未运行", text_color="gray")
except Exception as e:
logger.error(f"选择配置文件时出错: {str(e)}")
logger.error(traceback.format_exc())
def create_profile(self):
"""创建新的配置文件"""
try:
# 创建对话框
dialog = ctk.CTkInputDialog(
text="请输入新配置文件名称:",
title="创建新配置文件"
)
profile_name = dialog.get_input()
if not profile_name:
return
if self.browser_manager.create_profile(profile_name):
logger.info(f"创建了新配置文件: {profile_name}")
self.refresh_profiles()
if self.on_profile_change:
self.on_profile_change()
# 选择新创建的配置文件
self.profile_listbox.set(profile_name)
self.current_profile = profile_name
self.on_profile_selected(profile_name)
else:
messagebox.showerror("错误", f"无法创建配置文件: {profile_name}")
except Exception as e:
logger.error(f"创建配置文件时出错: {str(e)}")
logger.error(traceback.format_exc())
messagebox.showerror("错误", f"创建配置文件时出错: {str(e)}")
def delete_profile(self):
"""删除选定的配置文件"""
try:
if not self.current_profile or self.current_profile == "无可用配置":
messagebox.showwarning("警告", "请先选择要删除的配置文件")
return
# 确认删除
if not messagebox.askyesno("确认删除", f"确定要删除配置文件 '{self.current_profile}' 吗?"):
return
if self.browser_manager.delete_profile(self.current_profile):
logger.info(f"删除了配置文件: {self.current_profile}")
# 重置当前选择
self.current_profile = None
self.refresh_profiles()
if self.on_profile_change:
self.on_profile_change()
else:
messagebox.showerror("错误", f"无法删除配置文件: {self.current_profile}")
except Exception as e:
logger.error(f"删除配置文件时出错: {str(e)}")
logger.error(traceback.format_exc())
messagebox.showerror("错误", f"删除配置文件时出错: {str(e)}")
def launch_browser(self):
"""启动选定配置的浏览器"""
try:
if not self.current_profile or self.current_profile == "无可用配置":
messagebox.showwarning("警告", "请先选择配置文件")
return
# 检查是否已经启动
if self.current_profile in self.browser_manager.browsers:
messagebox.showinfo("提示", f"配置 '{self.current_profile}' 的浏览器已在运行")
return
# 显示进度对话框
progress_window = ctk.CTkToplevel(self)
progress_window.title("启动浏览器")
progress_window.geometry("300x150")
progress_window.transient(self) # 设置为模态
progress_window.grab_set()
# 添加进度条
label = ctk.CTkLabel(progress_window, text=f"正在启动浏览器 '{self.current_profile}'...")
label.pack(pady=(20, 10))
progress = ctk.CTkProgressBar(progress_window, width=250)
progress.pack(pady=10)
progress.set(0.5) # 不确定模式
# 定义回调函数
def on_browser_launch_complete(browser, page, success):
progress_window.after(500, progress_window.destroy) # 延迟关闭,避免闪烁
if success:
logger.info(f"浏览器 '{self.current_profile}' 启动成功")
self.update_browser_status()
self.status_label.configure(text="状态: 运行中", text_color="green")
else:
logger.error(f"浏览器 '{self.current_profile}' 启动失败")
messagebox.showerror("错误", f"启动浏览器失败: {self.current_profile}")
# 启动浏览器
self.browser_manager.launch_browser(
self.current_profile,
headless=False,
on_complete=on_browser_launch_complete
)
except Exception as e:
logger.error(f"启动浏览器时出错: {str(e)}")
logger.error(traceback.format_exc())
messagebox.showerror("错误", f"启动浏览器时出错: {str(e)}")
def close_browser(self):
"""关闭选定配置的浏览器"""
try:
if not self.current_profile or self.current_profile == "无可用配置":
messagebox.showwarning("警告", "请先选择配置文件")
return
# 检查是否已经启动
if self.current_profile not in self.browser_manager.browsers:
messagebox.showinfo("提示", f"配置 '{self.current_profile}' 的浏览器未在运行")
return
# 关闭浏览器
self.browser_manager._close_browser(self.current_profile)
logger.info(f"关闭了浏览器: {self.current_profile}")
# 更新状态
self.update_browser_status()
self.status_label.configure(text="状态: 未运行", text_color="gray")
except Exception as e:
logger.error(f"关闭浏览器时出错: {str(e)}")
logger.error(traceback.format_exc())
messagebox.showerror("错误", f"关闭浏览器时出错: {str(e)}")
def launch_all_browsers(self):
"""启动所有配置的浏览器"""
try:
profiles = self.browser_manager.get_profiles()
if not profiles:
messagebox.showinfo("提示", "没有可用的配置文件")
return
# 过滤掉已经启动的浏览器
profiles_to_launch = [p for p in profiles if p not in self.browser_manager.browsers]
if not profiles_to_launch:
messagebox.showinfo("提示", "所有浏览器都已经在运行")
return
# 显示进度对话框
progress_window = ctk.CTkToplevel(self)
progress_window.title("启动多个浏览器")
progress_window.geometry("400x200")
progress_window.transient(self)
progress_window.grab_set()
# 添加进度信息
label = ctk.CTkLabel(
progress_window,
text=f"正在启动 {len(profiles_to_launch)} 个浏览器...",
font=ctk.CTkFont(size=16)
)
label.pack(pady=(20, 10))
# 创建状态文本框
status_text = ctk.CTkTextbox(progress_window, height=100, width=350)
status_text.pack(pady=10, padx=20)
status_text.insert("0.0", "准备启动浏览器...\n")
# 已完成计数
completed_count = [0]
# 定义单个浏览器完成回调
def on_browser_launch(browser, page, success, profile):
completed_count[0] += 1
status_text.configure(state="normal")
if success:
status_text.insert("end", f"✅ 浏览器 '{profile}' 启动成功\n")
self.running_profiles.add(profile)
else:
status_text.insert("end", f"❌ 浏览器 '{profile}' 启动失败\n")
status_text.see("end")
status_text.configure(state="disabled")
# 更新进度信息
label.configure(text=f"启动进度: {completed_count[0]}/{len(profiles_to_launch)}")
# 更新浏览器状态
self.update_browser_status()
# 全部完成回调
def on_all_complete(results):
progress_window.after(2000, progress_window.destroy) # 延迟关闭,让用户看到结果
# 计算成功和失败的数量
success_count = sum(1 for b, p, s in results.values() if s)
fail_count = len(results) - success_count
if fail_count == 0:
messagebox.showinfo("完成", f"成功启动了 {success_count} 个浏览器")
else:
messagebox.showwarning("完成", f"启动了 {success_count} 个浏览器,{fail_count} 个失败")
# 更新当前选择的配置文件状态
self.on_profile_selected(self.current_profile)
# 启动所有浏览器
self.browser_manager.launch_multiple_browsers(
profiles_to_launch,
headless=False,
on_each_complete=on_browser_launch,
on_all_complete=on_all_complete
)
except Exception as e:
logger.error(f"启动所有浏览器时出错: {str(e)}")
logger.error(traceback.format_exc())
messagebox.showerror("错误", f"启动所有浏览器时出错: {str(e)}")
def close_all_browsers(self):
"""关闭所有浏览器"""
try:
# 检查是否有浏览器在运行
if not self.browser_manager.browsers:
messagebox.showinfo("提示", "没有正在运行的浏览器")
return
# 确认关闭
if not messagebox.askyesno("确认", "确定要关闭所有浏览器吗?"):
return
# 关闭所有浏览器
self.browser_manager.close_all_browsers()
logger.info("关闭了所有浏览器")
# 清空运行中的浏览器集合
self.running_profiles.clear()
# 更新状态
self.update_browser_status()
self.status_label.configure(text="状态: 未运行", text_color="gray")
except Exception as e:
logger.error(f"关闭所有浏览器时出错: {str(e)}")
logger.error(traceback.format_exc())
messagebox.showerror("错误", f"关闭所有浏览器时出错: {str(e)}")