Files
getDomain/domainCheck/app/ui/system_settings.py
Your Name 7cbde2aa78 d
2026-04-22 14:13:21 +08:00

1403 lines
53 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: UTF-8 -*-
'''
@Project :domainScanDemo
@File :system_settings.py
@IDE :PyCharm
@Author :梦伴
@Date :2026/4/10 15:00
@explain : 系统设置页面
'''
from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QLineEdit, QGroupBox, QCheckBox, QListWidget, QListWidgetItem, QTextEdit, QApplication
from PySide6.QtCore import QTimer, QThreadPool, QRunnable, Slot
from PySide6.QtCore import Qt, QThread, Signal
from loguru import logger
import os
import json
import redis
import base64
from detect.juming import JM
from detect.juziseo import Juziseo
from detect.jucha import JC
from app.config import config
class RedisTask(QRunnable):
"""
Redis操作后台任务
"""
def __init__(self, func, *args, **kwargs):
"""
初始化Redis任务
:param func: 要执行的函数
:param args: 函数参数
:param kwargs: 函数关键字参数
"""
super().__init__()
self._loading_settings = True
self.func = func
self.args = args
self.kwargs = kwargs
@Slot()
def run(self):
"""
执行Redis操作
"""
try:
self.func(*self.args, **self.kwargs)
except Exception as e:
logger.error(f"Redis操作失败: {e}")
class LoginThread(QThread):
"""
登录线程
"""
finished = Signal(bool, str)
def __init__(self, email, password, platform):
"""
初始化登录线程
:param email: 账号
:param password: 密码
:param platform: 平台 (juming: 聚名, juziseo: 桔子SEO)
"""
super().__init__()
self.email = email
self.password = password
self.platform = platform
def run(self):
"""
运行登录线程
"""
try:
if self.platform == "juming":
# 初始化聚名客户端
jm = JM()
# 加载 Cookie
jm.load_cookies()
logger.info("已加载聚名 Cookie")
# 登录
logger.info(f"开始登录聚名网: {self.email}")
login_result = jm.user_zh_p_login(self.email, self.password)
if not login_result[0]:
self.finished.emit(False, f"登录失败: {login_result[1]}")
return
logger.info("登录成功")
# 保存cookies
jm.save_cookies()
logger.info("已保存聚名 Cookie")
self.finished.emit(True, "登录成功")
elif self.platform == "juziseo":
# 初始化桔子SEO客户端
juziseo = Juziseo()
# 加载 Cookie
juziseo.load_cookies()
logger.info("已加载桔子SEO Cookie")
# 登录
logger.info(f"开始登录桔子SEO: {self.email}")
login_result = juziseo.login(self.email, self.password)
if not login_result[0]:
self.finished.emit(False, f"登录失败: {login_result[1]}")
return
logger.info("登录成功")
# 保存cookies
juziseo.save_cookies()
logger.info("已保存桔子SEO Cookie")
self.finished.emit(True, "登录成功")
elif self.platform == "jucha":
# 初始化聚查客户端
jc = JC()
# 加载聚名 Cookie
jc.load_juming_cookies()
logger.info("已加载聚名 Cookie")
# 登录
logger.info("开始登录聚查网")
login_result = jc.auth_login()
if not login_result[0]:
self.finished.emit(False, f"登录失败: {login_result[1]}")
return
logger.info("登录成功")
# 保存cookies
jc.save_cookies()
logger.info("已保存聚查 Cookie")
self.finished.emit(True, "登录成功")
except Exception as e:
logger.error(f"登录失败: {e}")
self.finished.emit(False, f"登录失败: {str(e)}")
class SystemSettingsWidget(QWidget):
"""
系统设置页面
"""
def __init__(self):
"""
初始化系统设置页面
"""
super().__init__()
self._loading_settings = True
# 初始化Redis客户端
self.redis_client = None
self.use_redis = False
self.thread_pool = QThreadPool()
self.thread_pool.setMaxThreadCount(5) # 最大线程数
self.init_redis()
# 创建布局
layout = QVBoxLayout(self)
# 创建登录区域容器
login_container = QGroupBox("登录设置")
login_container.setStyleSheet("""
QGroupBox {
font-size: 14px;
font-weight: bold;
color: #333;
border: 1px solid #ddd;
border-radius: 4px;
margin-bottom: 20px;
padding: 15px;
}
""")
login_layout = QVBoxLayout(login_container)
# 聚名登录区域
juming_subgroup = QGroupBox("聚名网登录")
juming_subgroup.setStyleSheet("""
QGroupBox {
font-size: 13px;
font-weight: bold;
color: #333;
border: 1px solid #eee;
border-radius: 4px;
margin-bottom: 15px;
padding: 10px;
}
""")
juming_layout = QVBoxLayout(juming_subgroup)
# 聚名输入区域(两列布局)
juming_input_layout = QHBoxLayout()
juming_input_layout.setSpacing(20)
# 邮箱输入
email_layout = QVBoxLayout()
email_label = QLabel("邮箱:")
email_label.setStyleSheet("font-size: 14px; color: #666; margin-bottom: 8px;")
self.juming_email_input = QLineEdit()
self.juming_email_input.setPlaceholderText("请输入聚名网账号")
self.juming_email_input.setStyleSheet("""
QLineEdit {
font-size: 14px;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
min-width: 250px;
min-height: 22px;
}
QLineEdit:focus {
border-color: #4CAF50;
outline: none;
}
""")
email_layout.addWidget(email_label)
email_layout.addWidget(self.juming_email_input)
juming_input_layout.addLayout(email_layout)
# 密码输入
password_layout = QVBoxLayout()
password_label = QLabel("密码:")
password_label.setStyleSheet("font-size: 14px; color: #666; margin-bottom: 8px;")
self.juming_password_input = QLineEdit()
self.juming_password_input.setPlaceholderText("请输入聚名网密码")
self.juming_password_input.setEchoMode(QLineEdit.Password)
self.juming_password_input.setStyleSheet("""
QLineEdit {
font-size: 14px;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
min-width: 250px;
min-height: 22px;
}
QLineEdit:focus {
border-color: #4CAF50;
outline: none;
}
""")
password_layout.addWidget(password_label)
password_layout.addWidget(self.juming_password_input)
juming_input_layout.addLayout(password_layout)
juming_layout.addLayout(juming_input_layout)
# 登录说明
juming_note = QLabel("聚名聚查联名登录:登录聚名后会自动登录聚查")
juming_note.setStyleSheet("font-size: 12px; color: #666; margin-top: 12px; margin-bottom: 12px;")
juming_layout.addWidget(juming_note)
# 登录按钮布局
juming_btn_layout = QHBoxLayout()
juming_btn_layout.addStretch() # 添加弹性空间,将按钮推到右侧
juming_login_btn = QPushButton("登录聚名网")
juming_login_btn.clicked.connect(self.login_juming)
juming_login_btn.setStyleSheet("""
QPushButton {
font-size: 14px;
padding: 8px 16px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
}
QPushButton:hover {
background-color: #45a049;
}
QPushButton:disabled {
background-color: #cccccc;
}
""")
juming_btn_layout.addWidget(juming_login_btn)
juming_layout.addLayout(juming_btn_layout)
login_layout.addWidget(juming_subgroup)
# 桔子SEO登录区域
juziseo_subgroup = QGroupBox("桔子SEO登录")
juziseo_subgroup.setStyleSheet("""
QGroupBox {
font-size: 13px;
font-weight: bold;
color: #333;
border: 1px solid #eee;
border-radius: 4px;
margin-bottom: 15px;
padding: 10px;
}
""")
juziseo_layout = QVBoxLayout(juziseo_subgroup)
# 桔子SEO输入区域两列布局
juziseo_input_layout = QHBoxLayout()
juziseo_input_layout.setSpacing(20)
# 账号输入
juziseo_email_layout = QVBoxLayout()
juziseo_email_label = QLabel("账号:")
juziseo_email_label.setStyleSheet("font-size: 14px; color: #666; margin-bottom: 8px;")
self.juziseo_email_input = QLineEdit()
self.juziseo_email_input.setPlaceholderText("请输入桔子SEO账号")
self.juziseo_email_input.setStyleSheet("""
QLineEdit {
font-size: 14px;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
min-width: 250px;
min-height: 22px;
}
QLineEdit:focus {
border-color: #2196F3;
outline: none;
}
""")
juziseo_email_layout.addWidget(juziseo_email_label)
juziseo_email_layout.addWidget(self.juziseo_email_input)
juziseo_input_layout.addLayout(juziseo_email_layout)
# 密码输入
juziseo_password_layout = QVBoxLayout()
juziseo_password_label = QLabel("密码:")
juziseo_password_label.setStyleSheet("font-size: 14px; color: #666; margin-bottom: 8px;")
self.juziseo_password_input = QLineEdit()
self.juziseo_password_input.setPlaceholderText("请输入桔子SEO密码")
self.juziseo_password_input.setEchoMode(QLineEdit.Password)
self.juziseo_password_input.setStyleSheet("""
QLineEdit {
font-size: 14px;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
min-width: 250px;
min-height: 22px;
}
QLineEdit:focus {
border-color: #2196F3;
outline: none;
}
""")
juziseo_password_layout.addWidget(juziseo_password_label)
juziseo_password_layout.addWidget(self.juziseo_password_input)
juziseo_input_layout.addLayout(juziseo_password_layout)
juziseo_layout.addLayout(juziseo_input_layout)
# 登录按钮布局
juziseo_btn_layout = QHBoxLayout()
juziseo_btn_layout.addStretch() # 添加弹性空间,将按钮推到右侧
juziseo_login_btn = QPushButton("登录桔子SEO")
juziseo_login_btn.clicked.connect(self.login_juziseo)
juziseo_login_btn.setStyleSheet("""
QPushButton {
font-size: 14px;
padding: 8px 16px;
background-color: #2196F3;
color: white;
border: none;
border-radius: 4px;
}
QPushButton:hover {
background-color: #0b7dda;
}
QPushButton:disabled {
background-color: #cccccc;
}
""")
juziseo_btn_layout.addWidget(juziseo_login_btn)
juziseo_layout.addLayout(juziseo_btn_layout)
login_layout.addWidget(juziseo_subgroup)
layout.addWidget(login_container)
# 创建代理IP配置区域
proxy_group = QGroupBox("代理IP配置")
proxy_group.setStyleSheet("""
QGroupBox {
font-size: 14px;
font-weight: bold;
color: #333;
border: 1px solid #ddd;
border-radius: 4px;
margin-bottom: 20px;
padding: 15px;
}
""")
proxy_layout = QVBoxLayout(proxy_group)
# 启用代理IP复选框
proxy_enable_layout = QHBoxLayout()
proxy_enable_label = QLabel("启用代理IP:")
proxy_enable_label.setStyleSheet("font-size: 14px; color: #333; min-width: 100px; font-weight: bold;")
self.proxy_enable_checkbox = QCheckBox()
self.proxy_enable_checkbox.setStyleSheet("""
QCheckBox {
font-size: 14px;
spacing: 8px;
}
QCheckBox::indicator {
width: 20px;
height: 20px;
}
QCheckBox::indicator:unchecked {
border: 2px solid #999;
background-color: white;
border-radius: 4px;
}
QCheckBox::indicator:checked {
border: 2px solid #9c27b0;
background-color: #9c27b0;
border-radius: 4px;
}
QCheckBox::indicator:checked::after {
content: '';
color: white;
font-size: 14px;
font-weight: bold;
position: absolute;
top: 0px;
left: 3px;
}
""")
self.proxy_enable_checkbox.stateChanged.connect(self.save_proxy_config)
proxy_enable_layout.addWidget(proxy_enable_label)
proxy_enable_layout.addWidget(self.proxy_enable_checkbox)
proxy_enable_layout.setContentsMargins(0, 0, 0, 10)
proxy_layout.addLayout(proxy_enable_layout)
# 代理池链接输入
proxy_url_layout = QVBoxLayout()
proxy_url_label = QLabel("代理池链接:")
proxy_url_label.setStyleSheet("font-size: 14px; color: #666; min-width: 100px;")
self.proxy_url_input = QTextEdit()
self.proxy_url_input.setPlaceholderText("每行一个代理池链接,支持多个代理池")
self.proxy_url_input.setFixedHeight(100)
self.proxy_url_input.setStyleSheet("""
QTextEdit {
font-size: 14px;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
background: #fff;
}
QTextEdit:focus {
border-color: #9c27b0;
outline: none;
}
""")
self.proxy_url_input.textChanged.connect(self.save_proxy_config)
proxy_url_layout.addWidget(proxy_url_label)
proxy_url_layout.addWidget(self.proxy_url_input)
proxy_url_layout.setContentsMargins(0, 0, 0, 10)
proxy_layout.addLayout(proxy_url_layout)
proxy_direct_layout = QHBoxLayout()
proxy_direct_label = QLabel("允许直连兜底:")
proxy_direct_label.setStyleSheet("font-size: 14px; color: #666; min-width: 100px;")
self.proxy_allow_direct_checkbox = QCheckBox()
self.proxy_allow_direct_checkbox.setStyleSheet(self.proxy_enable_checkbox.styleSheet())
self.proxy_allow_direct_checkbox.stateChanged.connect(self.save_proxy_config)
proxy_direct_layout.addWidget(proxy_direct_label)
proxy_direct_layout.addWidget(self.proxy_allow_direct_checkbox)
proxy_direct_layout.setContentsMargins(0, 0, 0, 10)
proxy_layout.addLayout(proxy_direct_layout)
proxy_note = QLabel("提示:默认不允许直连。启用代理后会优先汇总多个代理池,只有勾选“允许直连兜底”时才会在无代理可用时走本机出口。")
proxy_note.setStyleSheet("font-size: 12px; color: #999; margin-bottom: 10px;")
proxy_note.setWordWrap(True)
proxy_layout.addWidget(proxy_note)
proxy_test_layout = QHBoxLayout()
proxy_test_layout.addStretch()
self.proxy_test_button = QPushButton("测试代理池")
self.proxy_test_button.setStyleSheet("""
QPushButton {
font-size: 13px;
padding: 8px 16px;
background-color: #9c27b0;
color: white;
border: none;
border-radius: 4px;
}
QPushButton:hover {
background-color: #7b1fa2;
}
QPushButton:disabled {
background-color: #cccccc;
}
""")
self.proxy_test_button.clicked.connect(self.test_proxy_pools)
proxy_test_layout.addWidget(self.proxy_test_button)
proxy_layout.addLayout(proxy_test_layout)
self.proxy_test_result = QTextEdit()
self.proxy_test_result.setReadOnly(True)
self.proxy_test_result.setFixedHeight(150)
self.proxy_test_result.setPlaceholderText("代理池测试结果会显示在这里")
self.proxy_test_result.setStyleSheet("""
QTextEdit {
font-size: 12px;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
background: #fafafa;
color: #333;
}
""")
proxy_layout.addWidget(self.proxy_test_result)
layout.addWidget(proxy_group)
# 创建域名后缀配置区域
domain_suffix_group = QGroupBox("域名后缀配置")
domain_suffix_group.setStyleSheet("""
QGroupBox {
font-size: 14px;
font-weight: bold;
color: #333;
border: 1px solid #ddd;
border-radius: 4px;
margin-bottom: 20px;
padding: 15px;
}
""")
domain_suffix_layout = QVBoxLayout(domain_suffix_group)
# 域名后缀输入
suffix_layout = QHBoxLayout()
suffix_label = QLabel("保留后缀:")
suffix_label.setStyleSheet("font-size: 14px; color: #666; min-width: 80px;")
self.suffix_input = QLineEdit()
self.suffix_input.setPlaceholderText("请输入需要保留的域名后缀,多个用逗号分隔,如:.com,.net")
self.suffix_input.setStyleSheet("""
QLineEdit {
font-size: 14px;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
QLineEdit:focus {
border-color: #ff9800;
outline: none;
}
""")
# 输入时自动保存
self.suffix_input.textChanged.connect(self.save_suffixes)
suffix_layout.addWidget(suffix_label)
suffix_layout.addWidget(self.suffix_input)
suffix_layout.setContentsMargins(0, 0, 0, 5)
domain_suffix_layout.addLayout(suffix_layout)
# 添加分隔符提示
suffix_note = QLabel("提示:多个域名后缀请用英文逗号分割")
suffix_note.setStyleSheet("font-size: 12px; color: #999; margin-bottom: 10px;")
domain_suffix_layout.addWidget(suffix_note)
layout.addWidget(domain_suffix_group)
# 创建检测选项配置区域
detect_group = QGroupBox("检测选项配置")
detect_group.setStyleSheet("""
QGroupBox {
font-size: 14px;
font-weight: bold;
color: #333;
border: 1px solid #ddd;
border-radius: 4px;
margin-bottom: 20px;
padding: 15px;
}
""")
detect_layout = QVBoxLayout(detect_group)
self.detect_option_defs = [
("detect_register", "检查注册"),
("detect_baidu_site", "百度site查询"),
("detect_360_site", "360的site查询"),
("detect_chinaz", "站长之家查询"),
("detect_aizhan", "爱站网查询"),
("detect_wayback", "时光机检测"),
("detect_jucha", "聚查查询"),
("detect_juziseo", "桔子查询"),
]
self.detect_option_labels = {key: label for key, label in self.detect_option_defs}
detect_list_container = QHBoxLayout()
detect_list_container.setSpacing(12)
detect_list_container.setContentsMargins(0, 0, 0, 0)
self.detect_option_list = QListWidget()
self.detect_option_list.setStyleSheet("""
QListWidget {
border: 1px solid #ddd;
border-radius: 4px;
background: #fff;
font-size: 14px;
padding: 6px;
}
QListWidget::item {
padding: 6px 8px;
}
QListWidget::item:selected {
background: #e8f4ff;
color: #333;
}
""")
self.detect_option_list.itemChanged.connect(self.save_detect_options)
detect_button_column = QVBoxLayout()
detect_button_column.setSpacing(8)
self.detect_move_up_btn = QPushButton("上移")
self.detect_move_down_btn = QPushButton("下移")
self.detect_move_up_btn.clicked.connect(self.move_detect_option_up)
self.detect_move_down_btn.clicked.connect(self.move_detect_option_down)
detect_button_column.addWidget(self.detect_move_up_btn)
detect_button_column.addWidget(self.detect_move_down_btn)
detect_button_column.addStretch()
detect_list_container.addWidget(self.detect_option_list, 1)
detect_list_container.addLayout(detect_button_column)
detect_layout.addLayout(detect_list_container)
detect_note = QLabel("提示:勾选控制是否执行;顺序从上到下代表默认执行优先级,运营可自行调整。")
detect_note.setStyleSheet("font-size: 12px; color: #999; margin-top: 8px;")
detect_layout.addWidget(detect_note)
# 创建检测配置容器
detect_config_container = QHBoxLayout()
detect_config_container.setSpacing(20)
detect_config_container.setContentsMargins(0, 0, 0, 20)
# 添加检测选项配置
detect_config_container.addWidget(detect_group, 2) # 占2份空间
# 创建检测线程配置区域
thread_group = QGroupBox("检测线程配置")
thread_group.setStyleSheet("""
QGroupBox {
font-size: 14px;
font-weight: bold;
color: #333;
border: 1px solid #ddd;
border-radius: 4px;
margin-bottom: 20px;
padding: 15px;
}
""")
thread_layout = QVBoxLayout(thread_group)
# 检测线程数输入
thread_count_layout = QHBoxLayout()
thread_count_label = QLabel("线程数量:")
thread_count_label.setStyleSheet("font-size: 14px; color: #666; min-width: 80px;")
self.thread_count_input = QLineEdit()
self.thread_count_input.setPlaceholderText("请输入检测线程数量默认为10")
self.thread_count_input.setStyleSheet("""
QLineEdit {
font-size: 14px;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
QLineEdit:focus {
border-color: #2196F3;
outline: none;
}
""")
# 输入时自动保存
self.thread_count_input.textChanged.connect(self.save_thread_count)
thread_count_layout.addWidget(thread_count_label)
thread_count_layout.addWidget(self.thread_count_input)
thread_count_layout.setContentsMargins(0, 0, 0, 5)
thread_layout.addLayout(thread_count_layout)
# 添加线程数提示
thread_note = QLabel("提示:线程数量不宜设置过大,建议根据服务器性能调整")
thread_note.setStyleSheet("font-size: 12px; color: #999; margin-bottom: 10px;")
thread_layout.addWidget(thread_note)
# 添加检测线程配置到容器
detect_config_container.addWidget(thread_group, 1) # 占1份空间
# 将容器添加到主布局
layout.addLayout(detect_config_container)
# 创建状态显示区域,支持复制
status_container = QVBoxLayout()
status_container.setSpacing(6)
status_title_layout = QHBoxLayout()
status_title = QLabel("状态信息")
status_title.setStyleSheet("font-size: 13px; font-weight: bold; color: #333;")
status_title_layout.addWidget(status_title)
status_title_layout.addStretch()
self.copy_status_button = QPushButton("复制状态")
self.copy_status_button.setStyleSheet("""
QPushButton {
font-size: 12px;
padding: 6px 12px;
background-color: #607d8b;
color: white;
border: none;
border-radius: 4px;
}
QPushButton:hover {
background-color: #546e7a;
}
""")
self.copy_status_button.clicked.connect(self.copy_status_message)
status_title_layout.addWidget(self.copy_status_button)
status_container.addLayout(status_title_layout)
self.status_label = QTextEdit()
self.status_label.setReadOnly(True)
self.status_label.setFixedHeight(88)
self.status_label.setPlaceholderText("登录、代理测试、保存配置等结果会显示在这里,可直接选择复制")
self.status_label.setStyleSheet("""
QTextEdit {
font-size: 14px;
color: #333;
margin-top: 4px;
padding: 10px;
background-color: #f0f8ff;
border-radius: 4px;
border: 1px solid #d8e6f3;
}
""")
status_container.addWidget(self.status_label)
layout.addLayout(status_container)
# 加载保存的账号密码
self.load_credentials()
# 加载域名后缀
self.load_suffixes()
# 加载检测线程数
self.load_thread_count()
# 加载cookie
self.load_cookies()
# 加载检测选项
self.load_detect_options()
# 加载代理IP配置
self.load_proxy_config()
self._loading_settings = False
logger.info("系统设置页面创建完成")
def _encrypt_secret(self, value):
if not value:
return ''
try:
import win32crypt
encrypted = win32crypt.CryptProtectData(value.encode('utf-8'), None, None, None, None, 0)
return "dpapi:" + base64.b64encode(encrypted).decode('ascii')
except Exception:
return value
def _decrypt_secret(self, value):
if not value:
return ''
if not isinstance(value, str) or not value.startswith("dpapi:"):
return value
try:
import win32crypt
raw = base64.b64decode(value[6:])
return win32crypt.CryptUnprotectData(raw, None, None, None, 0)[1].decode('utf-8')
except Exception:
return ''
def init_redis(self):
"""
初始化Redis客户端连接
"""
try:
# 使用连接池
pool = redis.ConnectionPool(
host=config.REDIS_HOST,
port=config.REDIS_PORT,
password=config.REDIS_PASSWORD,
db=config.REDIS_DB,
decode_responses=True,
socket_connect_timeout=3, # 减少连接超时时间
socket_timeout=3, # 减少操作超时时间
max_connections=10 # 连接池大小
)
self.redis_client = redis.Redis(connection_pool=pool)
# 测试连接
self.redis_client.ping()
logger.info(f"Redis 连接成功: {config.REDIS_HOST}:{config.REDIS_PORT}")
self.use_redis = True
except Exception as e:
logger.warning(f"Redis 连接失败: {e},将只保存到本地文件")
self.redis_client = None
self.use_redis = False
def check_redis_connection(self):
"""
检查Redis连接状态如果连接已断开则重新连接
"""
if not self.use_redis or self.redis_client is None:
return False
try:
# 测试连接
self.redis_client.ping()
return True
except Exception as e:
logger.warning(f"Redis 连接已断开,尝试重新连接: {e}")
# 重新连接
self.init_redis()
return self.use_redis
def save_credentials(self):
"""
保存账号密码到本地文件和Redis
"""
credentials = {
"juming": {
"email": self.juming_email_input.text().strip(),
"password": self._encrypt_secret(self.juming_password_input.text().strip())
},
"juziseo": {
"email": self.juziseo_email_input.text().strip(),
"password": self._encrypt_secret(self.juziseo_password_input.text().strip())
}
}
try:
# 保存到本地文件UI线程执行
with open('credentials.json', 'w', encoding='utf-8') as f:
json.dump(credentials, f, indent=2, ensure_ascii=False)
logger.info("账号密码保存成功")
# 保存到Redis后台线程执行
def save_to_redis():
if self.check_redis_connection():
self.redis_client.set('domain_tool:credentials', json.dumps(credentials))
logger.info("账号密码已同步到Redis")
# 发布配置更新消息
self.redis_client.publish('domain_tool:config_update', 'credentials')
# 提交到线程池执行
task = RedisTask(save_to_redis)
self.thread_pool.start(task)
except Exception as e:
logger.error(f"保存账号密码失败: {e}")
def save_suffixes(self):
"""
保存域名后缀到本地文件和Redis
"""
if self._loading_settings:
return
suffixes = self.suffix_input.text().strip()
try:
# 保存到本地文件UI线程执行
with open('domain_suffixes.json', 'w', encoding='utf-8') as f:
json.dump({"suffixes": suffixes}, f, indent=2, ensure_ascii=False)
logger.info("域名后缀保存成功")
# 保存到Redis后台线程执行
def save_to_redis():
if self.check_redis_connection():
self.redis_client.set('domain_tool:domain_suffixes', suffixes)
logger.info("域名后缀已同步到Redis")
# 发布配置更新消息
self.redis_client.publish('domain_tool:config_update', 'domain_suffixes')
# 提交到线程池执行
task = RedisTask(save_to_redis)
self.thread_pool.start(task)
except Exception as e:
logger.error(f"保存域名后缀失败: {e}")
def load_suffixes(self):
"""
从本地文件加载域名后缀
"""
try:
if os.path.exists('domain_suffixes.json'):
with open('domain_suffixes.json', 'r', encoding='utf-8') as f:
data = json.load(f)
suffixes = data.get('suffixes', '.com,.net')
self.suffix_input.setText(suffixes)
logger.info("域名后缀加载成功")
else:
# 默认值
self.suffix_input.setText('.com,.net')
self.save_suffixes()
logger.info("使用默认域名后缀")
except Exception as e:
logger.error(f"加载域名后缀失败: {e}")
# 使用默认值
self.suffix_input.setText('.com,.net')
def load_credentials(self):
"""
从本地文件加载账号密码
"""
try:
if os.path.exists('credentials.json'):
with open('credentials.json', 'r', encoding='utf-8') as f:
credentials = json.load(f)
# 加载聚名账号密码
if 'juming' in credentials:
self.juming_email_input.setText(credentials['juming'].get('email', ''))
self.juming_password_input.setText(self._decrypt_secret(credentials['juming'].get('password', '')))
# 加载桔子SEO账号密码
if 'juziseo' in credentials:
self.juziseo_email_input.setText(credentials['juziseo'].get('email', ''))
self.juziseo_password_input.setText(self._decrypt_secret(credentials['juziseo'].get('password', '')))
logger.info("账号密码加载成功")
except Exception as e:
logger.error(f"加载账号密码失败: {e}")
def load_cookies(self):
"""
加载cookie
"""
try:
# 加载聚名cookie
jm = JM()
jm.load_cookies()
logger.info("聚名cookie加载成功")
# 加载桔子SEO cookie
juziseo = Juziseo()
juziseo.load_cookies()
logger.info("桔子SEO cookie加载成功")
# 加载聚查cookie
jc = JC()
jc.load_cookies()
logger.info("聚查cookie加载成功")
except Exception as e:
logger.error(f"加载cookie失败: {e}")
def login_juming(self):
"""
登录聚名网
"""
email = self.juming_email_input.text().strip()
password = self.juming_password_input.text().strip()
if not email or not password:
self.status_label.setPlainText("请输入账号和密码")
return
self.status_label.setPlainText("正在登录聚名网...")
# 创建并启动登录线程
self.login_thread = LoginThread(email, password, "juming")
self.login_thread.finished.connect(self.login_finished)
self.login_thread.start()
logger.info(f"开始登录聚名网: {email}")
def login_juziseo(self):
"""
登录桔子SEO
"""
email = self.juziseo_email_input.text().strip()
password = self.juziseo_password_input.text().strip()
if not email or not password:
self.status_label.setPlainText("请输入账号和密码")
return
self.status_label.setPlainText("正在登录桔子SEO...")
# 创建并启动登录线程
self.login_thread = LoginThread(email, password, "juziseo")
self.login_thread.finished.connect(self.login_finished)
self.login_thread.start()
logger.info(f"开始登录桔子SEO: {email}")
def login_jucha(self):
"""
登录聚查网
"""
# 检查聚名Cookie是否存在
if not os.path.exists('juming_cookies.pkl'):
self.status_label.setPlainText("请先登录聚名网")
return
self.status_label.setPlainText("正在登录聚查网...")
# 创建并启动登录线程
self.login_thread = LoginThread("", "", "jucha")
self.login_thread.finished.connect(self.login_finished)
self.login_thread.start()
logger.info("开始登录聚查网")
def populate_detect_option_list(self, order, options):
self.detect_option_list.blockSignals(True)
self.detect_option_list.clear()
for index, key in enumerate(order, start=1):
label = self.detect_option_labels.get(key, key)
item = QListWidgetItem(f"{index}. {label}")
item.setData(Qt.UserRole, key)
item.setFlags(item.flags() | Qt.ItemIsUserCheckable | Qt.ItemIsSelectable | Qt.ItemIsEnabled)
item.setCheckState(Qt.Checked if options.get(key, False) else Qt.Unchecked)
self.detect_option_list.addItem(item)
if self.detect_option_list.count() > 0:
self.detect_option_list.setCurrentRow(0)
self.detect_option_list.blockSignals(False)
def refresh_detect_option_labels(self):
for index in range(self.detect_option_list.count()):
item = self.detect_option_list.item(index)
key = item.data(Qt.UserRole)
item.setText(f"{index + 1}. {self.detect_option_labels.get(key, key)}")
def get_detect_option_payload(self):
payload = {key: False for key, _ in self.detect_option_defs}
order = []
for index in range(self.detect_option_list.count()):
item = self.detect_option_list.item(index)
key = item.data(Qt.UserRole)
payload[key] = item.checkState() == Qt.Checked
order.append(key)
payload['detect_order'] = order
return payload
def move_detect_option_up(self):
current_row = self.detect_option_list.currentRow()
if current_row <= 0:
return
item = self.detect_option_list.takeItem(current_row)
self.detect_option_list.insertItem(current_row - 1, item)
self.detect_option_list.setCurrentRow(current_row - 1)
self.refresh_detect_option_labels()
self.save_detect_options()
def move_detect_option_down(self):
current_row = self.detect_option_list.currentRow()
if current_row < 0 or current_row >= self.detect_option_list.count() - 1:
return
item = self.detect_option_list.takeItem(current_row)
self.detect_option_list.insertItem(current_row + 1, item)
self.detect_option_list.setCurrentRow(current_row + 1)
self.refresh_detect_option_labels()
self.save_detect_options()
def save_detect_options(self):
"""
保存检测选项到本地文件和Redis
"""
if self._loading_settings:
return
detect_options = self.get_detect_option_payload()
try:
# 保存到本地文件UI线程执行
with open('detect_options.json', 'w', encoding='utf-8') as f:
json.dump(detect_options, f, indent=2, ensure_ascii=False)
logger.info("检测选项保存成功")
# 保存到Redis后台线程执行
def save_to_redis():
if self.check_redis_connection():
self.redis_client.set('domain_tool:detect_options', json.dumps(detect_options))
logger.info("检测选项已同步到Redis")
# 发布配置更新消息
self.redis_client.publish('domain_tool:config_update', 'detect_options')
# 提交到线程池执行
task = RedisTask(save_to_redis)
self.thread_pool.start(task)
except Exception as e:
logger.error(f"保存检测选项失败: {e}")
def load_detect_options(self):
"""
从本地文件加载检测选项
"""
try:
default_order = [key for key, _ in self.detect_option_defs]
defaults = {
"detect_register": True,
"detect_wayback": True,
"detect_chinaz": True,
"detect_aizhan": True,
"detect_baidu_site": True,
"detect_360_site": True,
"detect_jucha": False,
"detect_juziseo": False,
"detect_order": default_order,
}
if os.path.exists('detect_options.json'):
with open('detect_options.json', 'r', encoding='utf-8') as f:
defaults.update(json.load(f))
if defaults.get('detect_whois') or defaults.get('detect_beian') or defaults.get('detect_intercept'):
defaults['detect_jucha'] = True
if defaults.get('detect_juziseo_outlink'):
defaults['detect_juziseo'] = True
order = defaults.get('detect_order') or []
normalized_order = [key for key in order if key in self.detect_option_labels]
for key in default_order:
if key not in normalized_order:
normalized_order.append(key)
self.populate_detect_option_list(normalized_order, defaults)
logger.info("检测选项加载成功")
except Exception as e:
logger.error(f"加载检测选项失败: {e}")
fallback = {
"detect_register": True,
"detect_wayback": True,
"detect_chinaz": True,
"detect_aizhan": True,
"detect_baidu_site": True,
"detect_360_site": True,
"detect_jucha": False,
"detect_juziseo": False,
}
self.populate_detect_option_list([key for key, _ in self.detect_option_defs], fallback)
def save_proxy_config(self):
"""
保存代理IP配置到本地文件和Redis
"""
if self._loading_settings:
return
proxy_enable = self.proxy_enable_checkbox.isChecked()
proxy_urls = [line.strip() for line in self.proxy_url_input.toPlainText().splitlines() if line.strip()]
allow_direct = self.proxy_allow_direct_checkbox.isChecked()
payload = {
"proxy_enable": proxy_enable,
"proxy_urls": proxy_urls,
"proxy_url": proxy_urls[0] if proxy_urls else "",
"allow_direct": allow_direct,
}
try:
# 保存到本地文件UI线程执行
with open('proxy_config.json', 'w', encoding='utf-8') as f:
json.dump(payload, f, indent=2, ensure_ascii=False)
logger.info("代理IP配置保存成功")
# 保存到Redis后台线程执行
def save_to_redis():
if self.check_redis_connection():
self.redis_client.set('domain_tool:proxy_config', json.dumps(payload))
logger.info("代理IP配置已同步到Redis")
# 发布配置更新消息
self.redis_client.publish('domain_tool:config_update', 'proxy_config')
# 提交到线程池执行
task = RedisTask(save_to_redis)
self.thread_pool.start(task)
except Exception as e:
logger.error(f"保存代理IP配置失败: {e}")
def load_proxy_config(self):
"""
从本地文件加载代理IP配置
"""
try:
if os.path.exists('proxy_config.json'):
with open('proxy_config.json', 'r', encoding='utf-8') as f:
proxy_config = json.load(f)
proxy_enable = proxy_config.get('proxy_enable', False)
proxy_urls = proxy_config.get('proxy_urls') or []
if not proxy_urls and proxy_config.get('proxy_url'):
proxy_urls = [proxy_config.get('proxy_url', '')]
allow_direct = proxy_config.get('allow_direct', False)
self.proxy_enable_checkbox.setChecked(proxy_enable)
self.proxy_url_input.setPlainText("\n".join(proxy_urls))
self.proxy_allow_direct_checkbox.setChecked(allow_direct)
logger.info("代理IP配置加载成功")
else:
# 默认值:未启用,不允许直连
self.proxy_enable_checkbox.setChecked(False)
self.proxy_url_input.setPlainText('')
self.proxy_allow_direct_checkbox.setChecked(False)
self.save_proxy_config()
logger.info("使用默认代理IP配置")
except Exception as e:
logger.error(f"加载代理IP配置失败: {e}")
self.proxy_enable_checkbox.setChecked(False)
self.proxy_url_input.setPlainText('')
self.proxy_allow_direct_checkbox.setChecked(False)
def test_proxy_pools(self):
proxy_urls = [line.strip() for line in self.proxy_url_input.toPlainText().splitlines() if line.strip()]
if not proxy_urls:
self.status_label.setPlainText("请先填写至少一个代理池链接")
return
self.status_label.setPlainText("正在测试代理池...")
self.proxy_test_button.setEnabled(False)
self.proxy_test_result.setPlainText("正在逐个测试代理池,请稍候...")
def run_test():
import requests
import random
results = []
total_items = 0
success_count = 0
sampled_total = 0
sampled_success = 0
def build_proxy_url(proxy_item):
ip = proxy_item.get('ip')
port = proxy_item.get('port')
if not ip or not port:
return None
username = proxy_item.get('username', '')
password = proxy_item.get('password', '')
if username and password:
return f"http://{username}:{password}@{ip}:{port}"
return f"http://{ip}:{port}"
for url in proxy_urls:
try:
response = requests.get(url, timeout=10)
if response.status_code != 200:
results.append(f"[异常] {url}\n状态: HTTP {response.status_code}")
continue
data = response.json()
count = 0
if isinstance(data, dict):
if isinstance(data.get('list'), list):
count = len(data.get('list'))
proxy_items = data.get('list')
elif data.get('ip') and data.get('port'):
count = 1
proxy_items = [data]
else:
proxy_items = []
elif isinstance(data, list):
count = len(data)
proxy_items = data
else:
proxy_items = []
total_items += count
success_count += 1
sample_size = min(5, len(proxy_items))
sample_candidates = random.sample(proxy_items, sample_size) if sample_size else []
sample_ok = 0
for item in sample_candidates:
proxy_url = build_proxy_url(item)
if not proxy_url:
continue
sampled_total += 1
try:
test_response = requests.get(
'https://m.baidu.com',
proxies={'http': proxy_url, 'https': proxy_url},
timeout=3
)
if test_response.status_code == 200:
sample_ok += 1
sampled_success += 1
except Exception:
pass
results.append(
f"[成功] {url}\n返回代理数: {count}\n"
f"抽样测试: {sample_ok}/{sample_size} 可用"
)
except Exception as e:
results.append(f"[失败] {url}\n原因: {e}")
final_text = (
f"代理池测试完成:成功 {success_count}/{len(proxy_urls)} 个,"
f"累计返回 {total_items} 条代理,抽样可用 {sampled_success}/{sampled_total}"
)
detail_text = "\n\n".join(results) if results else "未返回任何测试结果"
def update_ui():
self.status_label.setPlainText(final_text)
self.proxy_test_result.setPlainText(detail_text)
self.proxy_test_button.setEnabled(True)
QTimer.singleShot(0, update_ui)
task = RedisTask(run_test)
self.thread_pool.start(task)
def copy_status_message(self):
text = self.status_label.toPlainText().strip()
if not text:
self.status_label.setPlainText("当前没有可复制的状态信息")
return
QApplication.clipboard().setText(text)
self.status_label.setPlainText(text + "\n\n[已复制到剪贴板]")
def toggle_password_visibility(self, password_input):
"""
切换密码输入框的可见性
:param password_input: 密码输入框
"""
if password_input.echoMode() == QLineEdit.Password:
password_input.setEchoMode(QLineEdit.Normal)
# 找到对应的按钮并修改图标
if password_input == self.juming_password_input:
self.juming_password_visibility_btn.setText("🙈")
elif password_input == self.juziseo_password_input:
self.juziseo_password_visibility_btn.setText("🙈")
else:
password_input.setEchoMode(QLineEdit.Password)
# 找到对应的按钮并修改图标
if password_input == self.juming_password_input:
self.juming_password_visibility_btn.setText("👁")
elif password_input == self.juziseo_password_input:
self.juziseo_password_visibility_btn.setText("👁")
def login_finished(self, success, message):
"""
登录完成
:param success: 是否成功
:param message: 消息
"""
self.status_label.setPlainText(message)
# 登录成功后保存账号密码
if success:
self.save_credentials()
# 聚名登录成功后自动登录聚查
if self.login_thread.platform == "juming":
self.status_label.setPlainText("聚名登录成功,正在自动登录聚查...")
# 延迟一秒后登录聚查确保聚名cookie已保存
QTimer.singleShot(1000, self.login_jucha)
logger.info(f"登录完成: {message}")
def save_thread_count(self):
"""
保存检测线程数到本地文件和Redis
"""
if self._loading_settings:
return
raw_value = self.thread_count_input.text().strip()
try:
thread_count = str(max(1, int(raw_value or '10')))
except Exception:
thread_count = '10'
if self.thread_count_input.text().strip() != thread_count:
self.thread_count_input.setText(thread_count)
return
try:
# 保存到本地文件UI线程执行
with open('thread_count.json', 'w', encoding='utf-8') as f:
json.dump({"thread_count": thread_count}, f, indent=2, ensure_ascii=False)
logger.info("检测线程数配置保存成功")
# 保存到Redis后台线程执行
def save_to_redis():
if self.check_redis_connection():
self.redis_client.set('domain_tool:thread_count', thread_count)
logger.info("检测线程数配置已同步到Redis")
# 发布配置更新消息
self.redis_client.publish('domain_tool:config_update', 'thread_count')
# 提交到线程池执行
task = RedisTask(save_to_redis)
self.thread_pool.start(task)
except Exception as e:
logger.error(f"保存检测线程数配置失败: {e}")
def load_thread_count(self):
"""
从本地文件加载检测线程数
"""
try:
if os.path.exists('thread_count.json'):
with open('thread_count.json', 'r', encoding='utf-8') as f:
thread_config = json.load(f)
thread_count = str(max(1, int(thread_config.get('thread_count', '10') or '10')))
self.thread_count_input.setText(thread_count)
logger.info("检测线程数配置加载成功")
else:
self.thread_count_input.setText('10')
self.save_thread_count()
logger.info("使用默认检测线程数配置")
except Exception as e:
logger.error(f"加载检测线程数配置失败: {e}")
self.thread_count_input.setText('10')