ToolManager:统一 Python 工具入口
在小工具集基础上组织统一入口、插件界面和共享数据,记录平台化改造的取舍。
项目文稿说明:本文按现有笔记整理,代码是节选或设计示意;仓库实现、线上地址和测试结果尚未逐项核验。后续计划不代表已经完成。
ToolManager:手搓一个 Python 工具启动平台
菜牙爱吃菜 · 2026-10-20 · Python / PyQt6 / 浏览器自动化 / 工具开发
前言:为什么会有这个项目
先说结论:我受够了一个功能一个文件夹、一个工具一个 Git 仓库的日子了。
之前写浏览器隔离、Cursor 账号管理、代理网关、GoLogin 自动化……每个都是独立项目,各自有虚拟环境、各自有 requirements.txt、各自的 PyQt6 主窗口代码复制粘贴。改个通用的 UI 组件要改 N 个仓库,心态崩了。
于是某天夜里我打开 F 盘,数了一下——散落着 7、8 个 “final_final_v2” 文件夹的 Python 项目。那一刻我决定:搞一个统一的平台,把乱七八糟的工具收编进来。
这就是 ToolManager 的起点。
项目简介
ToolManager 是一个基于 Python 的工具启动平台,目标是:一个入口,管理多个功能模块。
| 模块 | 功能 |
|---|---|
browser_manager |
浏览器指纹隔离 + 代理管理 |
cursor |
Cursor AI 账号管理与重置 |
gateway_standalone |
独立代理网关 |
gologin_auto |
GoLogin 浏览器自动化 |
kookeey |
Kookeey 代理网关 |
reset_cursor |
Cursor 账号重置工具 |
test_imap |
IMAP 邮箱测试工具 |
一句话总结:浏览器隔离、账号管理、代理网关,统一在一个 PyQt6 窗口里。
技术栈
| 层面 | 技术 | 选型理由 |
|---|---|---|
| UI 框架 | PyQt6 | 比 Tkinter 漂亮,比 PySide6 资料多,Qt 的 dock/停靠系统天然适合多模块 |
| 浏览器自动化 | DrissionPage + Selenium + webdriver-manager | DrissionPage 对中文友好、API 设计人性化,Selenium 兜底 |
| 指纹隔离 | gologin(GoLogin) | 商业方案中性价比最高的,API 文档清晰,SDK 成熟 |
| 代理 | requests + 自定义 gateway | 轻量,够用 |
| 数据存储 | SQLite | 单文件、零配置、够用 |
| 环境管理 | venv + .env.example | 简单粗暴 |
| 打包部署 | Nuitka / PyInstaller(规划中) | 分发给不用命令行的人 |
选型的核心原则:不追新,追稳。 每个依赖至少发布 2 年以上,有活跃的 issue 区和可用的中文资料。
架构设计
整体架构
┌─────────────────────────────────────────────┐
│ host_window (QMainWindow) │
│ ┌───────────────────────────────────────┐ │
│ │ Plugin Manager (插件管理器) │ │
│ ├─────────┬─────────┬─────────┬─────────┤ │
│ │ browser │ cursor │ gologin │ ... │ │
│ │ _manager│ │ _auto │ │ │
│ ├─────────┴─────────┴─────────┴─────────┤ │
│ │ Plugin API (接口层) │ │
│ └───────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
插件接口设计
每个功能模块必须实现 BasePlugin 接口:
## core/plugin_api.py
from abc import ABC, abstractmethod
from typing import Optional, Dict, Any, List
from PyQt6.QtWidgets import QWidget
class PluginMetadata:
"""插件元数据"""
def __init__(
self,
name: str, # 插件唯一标识
display_name: str, # 显示名称
version: str, # 版本号
description: str, # 功能描述
author: str = "", # 作者
icon_path: str = "", # 图标路径
category: str = "tools", # 分类
):
self.name = name
self.display_name = display_name
self.version = version
self.description = description
self.author = author
self.icon_path = icon_path
self.category = category
class BasePlugin(ABC):
"""插件基类"""
@abstractmethod
def metadata(self) -> PluginMetadata:
"""返回插件元数据"""
...
@abstractmethod
def create_widget(self, parent: Optional[QWidget] = None) -> QWidget:
"""创建并返回插件的主界面"""
...
def initialize(self, config: Dict[str, Any]) -> bool:
"""初始化插件(可选重写)"""
return True
def shutdown(self) -> None:
"""关闭时清理资源(可选重写)"""
pass
def on_activated(self) -> None:
"""插件被激活时回调"""
pass
插件加载器
## core/plugin_manager.py
import os
import importlib
import inspect
from pathlib import Path
from typing import Dict, List, Type
from PyQt6.QtCore import QObject, pyqtSignal
class PluginManager(QObject):
"""插件管理器"""
plugin_loaded = pyqtSignal(str) # 加载成功
plugin_error = pyqtSignal(str, str) # 加载失败
def __init__(self, plugin_dirs: List[str] = None):
super().__init__()
self._plugins: Dict[str, BasePlugin] = {}
self._plugin_dirs = plugin_dirs or ["./plugins", "./modules"]
def discover(self) -> List[Type[BasePlugin]]:
"""发现所有插件类"""
plugin_classes = []
for plugin_dir in self._plugin_dirs:
if not os.path.isdir(plugin_dir):
continue
for file in Path(plugin_dir).glob("*.py"):
if file.name.startswith("_"):
continue
module_name = file.stem
try:
module = importlib.import_module(
f"{Path(plugin_dir).name}.{module_name}"
)
for name, obj in inspect.getmembers(module, inspect.isclass):
if (
issubclass(obj, BasePlugin)
and obj is not BasePlugin
and not inspect.isabstract(obj)
):
plugin_classes.append(obj)
except Exception as e:
self.plugin_error.emit(file.name, str(e))
return plugin_classes
def load(self, plugin_class: Type[BasePlugin]) -> BasePlugin:
"""实例化并加载插件"""
instance = plugin_class()
meta = instance.metadata()
self._plugins[meta.name] = instance
self.plugin_loaded.emit(meta.name)
return instance
def get(self, name: str) -> Optional[BasePlugin]:
return self._plugins.get(name)
主窗口:Dock 停靠
PyQt6 的 QDockWidget 天然适合多模块布局。每个插件创建一个 dock,用户可以自由拖拽、停靠、浮动:
## ui/host_window.py
from PyQt6.QtWidgets import QMainWindow, QDockWidget, QTabWidget
from PyQt6.QtCore import Qt
class HostWindow(QMainWindow):
def __init__(self, plugin_manager: PluginManager):
super().__init__()
self.plugin_manager = plugin_manager
self.setWindowTitle("ToolManager")
self.setMinimumSize(1200, 800)
self.tab_widget = QTabWidget()
self.setCentralWidget(self.tab_widget)
self._load_plugins()
def _load_plugins(self):
for plugin_class in self.plugin_manager.discover():
try:
plugin = self.plugin_manager.load(plugin_class)
widget = plugin.create_widget(self)
# 方案一:放到 Tab 里
self.tab_widget.addTab(
widget,
plugin.metadata().display_name
)
# 方案二:放到 Dock 里(更灵活)
# dock = QDockWidget(plugin.metadata().display_name)
# dock.setWidget(widget)
# self.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, dock)
plugin.initialize({})
except Exception as e:
self.statusBar().showMessage(
f"加载失败: {plugin_class.__name__} - {e}", 5000
)
核心模块详解
1. browser_manager:浏览器隔离
浏览器隔离是 ToolManager 最核心的模块。它基于 GoLogin 实现指纹隔离,让每个浏览器实例拥有独立的指纹环境(Canvas、WebGL、User-Agent、语言、时区等)。
## modules/browser_manager/browser_core.py
from gologin import GoLogin
from dataclasses import dataclass, field
from typing import Optional, Dict, Any
import json
@dataclass
class BrowserProfile:
"""浏览器配置"""
profile_id: str
name: str
proxy: Dict[str, Any] = field(default_factory=dict)
user_agent: str = ""
language: str = "en-US"
timezone: str = "America/New_York"
geolocation: Dict[str, float] = field(default_factory=dict)
canvas: str = "noise"
webgl: str = "noise"
class BrowserCore:
"""浏览器核心逻辑(无 UI)"""
def __init__(self, api_token: str):
self.token = api_token
self.active_profiles: Dict[str, GoLogin] = {}
def create_profile(self, profile: BrowserProfile) -> str:
"""创建浏览器配置"""
gologin = GoLogin({
"token": self.token,
"profile_id": profile.profile_id,
"proxy": profile.proxy,
"lang": profile.language,
"timezone": profile.timezone,
})
self.active_profiles[profile.profile_id] = gologin
return gologin.start()
def close_profile(self, profile_id: str):
"""关闭浏览器"""
if profile_id in self.active_profiles:
self.active_profiles[profile_id].stop()
del self.active_profiles[profile_id]
## modules/browser_manager/gui.py
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
QTableWidget, QTableWidgetItem, QLineEdit,
QComboBox, QLabel, QCheckBox
)
class BrowserManagerPlugin(BasePlugin):
"""浏览器管理插件"""
def metadata(self) -> PluginMetadata:
return PluginMetadata(
name="browser_manager",
display_name="浏览器管理",
version="1.0.0",
description="浏览器指纹隔离与代理管理",
)
def create_widget(self, parent=None) -> QWidget:
widget = QWidget()
layout = QVBoxLayout(widget)
# 工具栏
toolbar = QHBoxLayout()
self.btn_create = QPushButton("新建浏览器")
self.btn_start = QPushButton("启动")
self.btn_stop = QPushButton("关闭")
self.btn_delete = QPushButton("删除")
toolbar.addWidget(self.btn_create)
toolbar.addWidget(self.btn_start)
toolbar.addWidget(self.btn_stop)
toolbar.addWidget(self.btn_delete)
toolbar.addStretch()
layout.addLayout(toolbar)
# 列表
self.table = QTableWidget(0, 5)
self.table.setHorizontalHeaderLabels(
["ID", "名称", "代理", "状态", "操作"]
)
layout.addWidget(self.table)
self.btn_create.clicked.connect(self._on_create)
return widget
def _on_create(self):
# 弹出配置对话框...
pass
2. cursor:账号配置模块(待核实)
原稿列出了账号配置和使用情况查看模块,但没有提供可以验证的接口来源。所谓 /api/reset_usage 未经证实,不应当作重置服务商额度的方法,因此整理版不保留这段操作代码。该模块的真实用途、授权范围与完成情况待作者确认。
3. gateway_standalone + kookeey:代理网关
代理网关是连接本地浏览器和代理服务端的桥梁。gateway_standalone 是独立网关,kookeey 对接 Kookeey 代理服务。
## modules/gateway_standalone/gateway.py
import requests
import threading
from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import urlparse
import socket
import select
class ProxyGateway:
"""独立代理网关"""
def __init__(self, listen_port: int = 8080):
self.listen_port = listen_port
self.proxies: list = [] # 代理列表
self.current_index = 0
self.running = False
def add_proxy(self, host: str, port: int, user: str = "", pwd: str = ""):
self.proxies.append({
"host": host, "port": port,
"user": user, "password": pwd,
})
def start(self):
"""启动代理网关"""
self.running = True
threading.Thread(target=self._serve, daemon=True).start()
def _serve(self):
server = HTTPServer(("127.0.0.1", self.listen_port), self._make_handler())
while self.running:
server.handle_request()
def _make_handler(self):
gateway = self
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
proxy = gateway._get_next_proxy()
try:
resp = requests.get(
self.path,
headers=dict(self.headers),
proxies={
"http": f"http://{proxy['host']}:{proxy['port']}",
"https": f"http://{proxy['host']}:{proxy['port']}",
},
timeout=30,
)
self.send_response(resp.status_code)
for k, v in resp.headers.items():
if k.lower() not in ("transfer-encoding", "connection"):
self.send_header(k, v)
self.end_headers()
self.wfile.write(resp.content)
except Exception as e:
self.send_error(502, str(e))
def log_message(self, format, *args):
pass # 静默日志
return Handler
def _get_next_proxy(self) -> dict:
"""轮询选择代理"""
if not self.proxies:
raise RuntimeError("无可用代理")
proxy = self.proxies[self.current_index]
self.current_index = (self.current_index + 1) % len(self.proxies)
return proxy
4. gologin_auto:GoLogin 自动化
GoLogin 自动化模块把 GoLogin 的 API 封装成更高层的操作:
## modules/gologin_auto/gologin_core.py
from gologin import GoLogin
import time
import random
class GoLoginAutomation:
"""GoLogin 自动化操作"""
def __init__(self, token: str):
self.token = token
self.gologin = None
def create_profile_with_proxy(self, proxy_info: dict) -> str:
"""创建带代理的浏览器配置"""
profile_id = f"profile_{int(time.time())}"
self.gologin = GoLogin({
"token": self.token,
"profile_id": profile_id,
"proxy": {
"mode": proxy_info.get("mode", "http"),
"host": proxy_info["host"],
"port": proxy_info["port"],
"username": proxy_info.get("user", ""),
"password": proxy_info.get("password", ""),
},
"lang": proxy_info.get("lang", "en-US"),
"timezone": proxy_info.get("timezone", "America/New_York"),
})
return profile_id
def start_browser(self) -> dict:
"""启动浏览器,返回调试端口"""
response = self.gologin.start()
return {
"wsEndpoint": response.get("wsEndpoint", ""),
"debugPort": response.get("debugPort", ""),
}
def human_like_delay(self, min_ms=200, max_ms=800):
"""模拟人类延迟"""
delay = random.randint(min_ms, max_ms) / 1000
time.sleep(delay)
数据库设计
SQLite 存所有模块的共享数据:
-- 浏览器配置表
CREATE TABLE IF NOT EXISTS browser_profiles (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
proxy_config TEXT DEFAULT '{}',
user_agent TEXT DEFAULT '',
language TEXT DEFAULT 'en-US',
timezone TEXT DEFAULT 'America/New_York',
status TEXT DEFAULT 'inactive',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Cursor 账号表
CREATE TABLE IF NOT EXISTS cursor_accounts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
cookie TEXT DEFAULT '',
usage_count INTEGER DEFAULT 0,
is_active BOOLEAN DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 代理配置表
CREATE TABLE IF NOT EXISTS proxy_configs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
mode TEXT DEFAULT 'http',
host TEXT NOT NULL,
port INTEGER NOT NULL,
username TEXT DEFAULT '',
password TEXT DEFAULT '',
provider TEXT DEFAULT 'kookeey',
is_active BOOLEAN DEFAULT 1
);
.env 配置
## .env.example
## GoLogin API Token
GOLOGIN_TOKEN=your_gologin_api_token_here
## Kookeey 代理配置
KOOKEEY_API_KEY=your_kookeey_api_key
KOOKEEY_PROXY_HOST=proxy.kookeey.com
KOOKEEY_PROXY_PORT=10080
## Cursor 配置
CURSOR_BASE_URL=https://www.cursor.com
## 应用配置
APP_HOST=127.0.0.1
APP_PORT=8080
APP_DEBUG=false
踩过的坑(Lessons Learned)
1. PyQt6 的信号槽:类型安全很重要
一开始我图省事,用 pyqtSignal(object) 传一切,结果 debug 时根本不知道传了什么。后来改成强类型信号:
## 反面教材
data_ready = pyqtSignal(object) # 啥都能传,debug 地狱
## 正面教材
profile_created = pyqtSignal(str, dict) # profile_id, profile_data
教训:信号定义越明确,后期维护越轻松。Python 的类型注解 + PyQt6 的类型检查 = 救命稻草。
2. DrissionPage vs Selenium:别硬刚
DrissionPage 的 API 确实比 Selenium 好用,但有些网站的反爬策略会导致 DrissionPage 的连接模式失效。我的做法是:
- 默认用 DrissionPage(速度快、API 友好)
- 失败时降级到 Selenium(兼容性最好)
- 最后兜底 requests + BeautifulSoup(静态页面)
def open_page(url: str):
try:
# 优先 DrissionPage
page = ChromiumPage()
page.get(url)
return page
except Exception:
# 降级 Selenium
driver = webdriver.Chrome()
driver.get(url)
return driver
3. GoLogin 的 token 管理:别硬编码
GoLogin 的 API token 过期后,整个 browser_manager 就废了。一开始我把 token 写死在代码里,后来改成:
## 从环境变量读取
token = os.getenv("GOLOGIN_TOKEN")
if not token:
raise RuntimeError("请设置 GOLOGIN_TOKEN 环境变量")
原稿还提出 token 失效检测与邮件提醒,具体实现待核实;IMAP 用于收取邮件,发送通常使用 SMTP。
4. SQLite 并发:加锁!
多个插件同时访问同一个 SQLite 文件,不加锁就是灾难。Python 的 threading.Lock 是最简单的方案:
import threading
class Database:
_lock = threading.Lock()
def execute(self, sql: str, params=()):
with self._lock:
conn = sqlite3.connect(self.db_path)
try:
conn.execute(sql, params)
conn.commit()
finally:
conn.close()
还可评估 WAL 模式改善读写并发,它不能代替事务与线程同步:
conn.execute("PRAGMA journal_mode=WAL")
5. .env 文件:别提交到 Git
这个坑每个人都会踩一次。.env 里写着 API token、代理密码,commit 了就是历史记录里永远存在。
## .gitignore
.env
*.db
__pycache__/
*.pyc
教训:.gitignore 应在首次暂存敏感文件前准备好,写在 git init 前后都可以。别问我怎么知道的。
6. PyQt6 的资源文件:用 .qrc 还是直接路径?
一开始我用 .qrc 文件管理图标、样式表,结果编译 .qrc 成了额外负担。后来改成直接引用路径:
## 直接路径,简单粗暴
icon_path = os.path.join(ROOT_DIR, "assets", "browser_icon.png")
btn.setIcon(QIcon(icon_path))
如果项目变大再迁移到 .qrc。过早优化是万恶之源。
项目结构总览
ToolManager/
├── main.py # 程序入口
├── host_window.py # 主窗口
├── core/
│ ├── plugin_api.py # 插件接口定义
│ ├── plugin_manager.py # 插件管理器
│ ├── database.py # SQLite 封装
│ └── config.py # 配置加载
├── modules/
│ ├── browser_manager/
│ │ ├── browser_core.py # 核心逻辑
│ │ └── gui.py # UI 界面
│ ├── cursor/
│ │ ├── cursor_core.py
│ │ └── gui.py
│ ├── gateway_standalone/
│ │ ├── gateway.py
│ │ └── gui.py
│ ├── gologin_auto/
│ │ ├── gologin_core.py
│ │ └── gui.py
│ ├── kookeey/
│ │ ├── kookeey_core.py
│ │ └── gui.py
│ ├── reset_cursor/
│ │ ├── reset_core.py
│ │ └── gui.py
│ └── test_imap/
│ ├── imap_core.py
│ └── gui.py
├── assets/ # 图标、样式表
├── data/ # SQLite 数据库
├── .env.example
├── requirements.txt
└── README.md
后续计划
- [ ] 用 Nuitka 打包成独立 exe,分发给朋友
- [ ] 加一个日志模块,统一记录所有模块的运行日志
- [ ] 支持插件热重载(改了代码不用重启主程序)
- [ ] 浏览器隔离模块加一个批量操作界面
- [ ] 写一个自动更新检查(GitHub API 比对版本号)
- [ ] 完善单元测试(目前基本靠手动测试)
写在最后
这个项目写了大半年,断断续续,每次都是在实际需求驱动下加一个模块。它不优雅,架构不是一开始就设计好的,很多代码是 “先跑起来再说”。
但它解决了我的实际问题:浏览器隔离不用手动开 GoLogin、账号配置集中查看、代理网关不用写 N 份代码。一个窗口,全部搞定。
学 Python 最好的方式就是这样——找一个你真正需要的东西,然后把它做出来。不用一开始就想 “我要做一个伟大的项目”,先做一个 “够用的工具”,再慢慢打磨。
如果你也在学 Python 或者桌面开发,强烈建议动手试试。遇到坑就记下来,写进博客。过半年回头看,你会感谢当初记录下来的自己。
代码开源在 GitHub,欢迎交流。有问题评论区聊。
— 菜牙爱吃菜,某个周日的下午