# -*- coding: UTF-8 -*- ''' @Project :domainScanDemo @File :detect_worker.py @IDE :PyCharm @Author :梦伴 @Date :2026/4/10 22:40 @explain : 域名检测端程序 ''' import os import sys import json import time import threading import schedule from datetime import datetime from loguru import logger from queue import Queue from PySide6.QtWidgets import QApplication, QMainWindow, QTextEdit, QPushButton, QVBoxLayout, QHBoxLayout, QWidget, QLabel, QProgressBar, QGroupBox, QScrollArea, QFrame from PySide6.QtCore import Qt, QThread, Signal, QMetaObject, Q_ARG, QCoreApplication, QEvent, QTimer from PySide6.QtGui import QIcon from app.utils.database import Database from app.config import config from app.detectors.wayback_detector import WaybackDetector from app.utils.status_codes import ( DETECT_STATUS_BLACKLISTED, DETECT_STATUS_COMPLETED, DETECT_STATUS_FAILED, DETECT_STATUS_RUNNING, REGISTER_STATUS_AVAILABLE, REVIEW_STATUS_PENDING, THIRD_PARTY_STATUS_DONE, ) import redis from detect import aizhan, baidu, c360, chinaz, register, jucha, juziseo CONFIG_UPDATE_CHANNEL = "domain_tool:config_update" CONTROL_CHANNEL = "domain_tool:worker_control" RUNTIME_STATE_KEY = "domain_tool:detect_runtime_state" PENDING_CONTROL_KEY = "domain_tool:worker_pending_command" class DetectThread(QThread): """ 检测线程 """ log_signal = Signal(str) progress_signal = Signal(int, int) finished_signal = Signal() thread_count_signal = Signal(int, int) def __init__(self, worker): super().__init__() self.worker = worker # 设置线程名称 self.setObjectName("DetectThread") def run(self): # 将信号传递给worker self.worker.detect_thread = self self.worker.start_detection() self.finished_signal.emit() class DetectMainWindow(QMainWindow): """ 检测端主窗口 """ @staticmethod def _button_style( normal_color: str, hover_color: str, pressed_color: str, disabled_color: str = "#6c757d", *, fancy_effects: bool = True, ) -> str: hover_extra = "" pressed_extra = "" disabled_extra = "" if fancy_effects: hover_extra = "\n transform: translateY(-2px);\n box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);" pressed_extra = "\n transform: translateY(1px);\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);" disabled_extra = "\n transform: none;\n box-shadow: none;" return f""" QPushButton {{ background-color: {normal_color}; color: white; border: none; border-radius: 8px; font-size: 16px; font-weight: bold; padding: 10px 20px; min-width: 120px; }} QPushButton:hover {{ background-color: {hover_color};{hover_extra} }} QPushButton:pressed {{ background-color: {pressed_color};{pressed_extra} }} QPushButton:disabled {{ background-color: {disabled_color}; color: #adb5bd;{disabled_extra} }} """ def __init__(self): super().__init__() self.setWindowTitle("域名检测端") self.setGeometry(100, 100, 980, 820) self.setMinimumSize(860, 680) # 设置窗口图标 # 打印当前文件路径以便诊断 current_file = os.path.abspath(__file__) current_dir = os.path.dirname(current_file) logger.info(f"当前文件路径: {current_file}") logger.info(f"当前目录: {current_dir}") # 获取PyInstaller打包后的临时目录;开发/部署态优先使用脚本所在目录。 base_dir = current_dir if hasattr(sys, '_MEIPASS'): base_dir = sys._MEIPASS logger.info(f"PyInstaller临时目录: {base_dir}") else: logger.info(f"开发环境目录: {base_dir}") def resolve_asset_path(filename: str) -> str: candidates = [ os.path.join(base_dir, filename), os.path.join(current_dir, filename), os.path.join(os.path.dirname(current_dir), filename), ] for candidate in candidates: if os.path.exists(candidate): return candidate return candidates[0] # 构建图标路径 icon_path = resolve_asset_path("new_logo.svg") logger.info(f"SVG图标路径: {icon_path}") if os.path.exists(icon_path): logger.info(f"SVG图标文件存在: {icon_path}") try: icon = QIcon(icon_path) if icon.isNull(): logger.warning(f"SVG图标加载失败,图标为空: {icon_path}") else: self.setWindowIcon(icon) logger.info(f"设置SVG窗口图标成功: {icon_path}") except Exception as e: logger.error(f"加载SVG图标时出错: {e}") else: logger.warning(f"SVG图标文件不存在: {icon_path}") # 如果SVG加载失败,使用ico文件作为备用 ico_icon_path = resolve_asset_path("favicon2.ico") logger.info(f"ICO图标路径: {ico_icon_path}") if os.path.exists(ico_icon_path): logger.info(f"ICO图标文件存在: {ico_icon_path}") try: icon = QIcon(ico_icon_path) if icon.isNull(): logger.warning(f"ICO图标加载失败,图标为空: {ico_icon_path}") else: self.setWindowIcon(icon) logger.info(f"设置ICO窗口图标成功: {ico_icon_path}") except Exception as e: logger.error(f"加载ICO图标时出错: {e}") else: logger.warning(f"ICO图标文件不存在: {ico_icon_path}") # 设置窗口样式 self.setStyleSheet("" "QMainWindow {" " background-color: #f5f5f5;" " font-family: 'Microsoft YaHei', Arial, sans-serif;" "}" "QLabel {" " font-size: 14px;" " color: #333;" " padding: 5px 0;" "}" "QGroupBox {" " font-size: 16px;" " font-weight: bold;" " border: 1px solid #ddd;" " border-radius: 8px;" " margin-top: 15px;" " padding: 15px;" " background-color: #ffffff;" "}" "QGroupBox::title {" " subcontrol-origin: margin;" " subcontrol-position: top left;" " padding: 0 15px;" " background-color: #4CAF50;" " color: white;" " border-radius: 4px;" " font-size: 14px;" "}" "QProgressBar {" " border: 1px solid #ddd;" " border-radius: 6px;" " text-align: center;" " background-color: #f0f0f0;" " height: 25px;" "}" "QProgressBar::chunk {" " background-color: #4CAF50;" " border-radius: 6px;" "}" "QTextEdit {" " font-family: Consolas, 'Courier New', monospace;" " font-size: 12px;" " border: 1px solid #ddd;" " border-radius: 6px;" " background-color: #2d2d2d;" " color: #e0e0e0;" " padding: 10px;" "}" "QPushButton {" " font-family: 'PingFang SC', 'Helvetica Neue', 'Microsoft YaHei', sans-serif;" " font-size: 12px;" " font-weight: 600;" " padding: 6px 12px;" " border: none;" " border-radius: 4px;" " color: white;" " min-width: 80px;" " min-height: 28px;" " text-align: center;" "}" "QPushButton:hover {" " opacity: 0.9;" "}" "QPushButton:disabled {" " opacity: 0.5;" "}" "QPushButton#start_button {" " background-color: #4CAF50;" "}" "QPushButton#stop_button {" " background-color: #f44336;" "}" "QPushButton#exit_button {" " background-color: #2196F3;" "}" "") # 创建中心部件 + 内部滚动区域 scroll_area = QScrollArea() scroll_area.setWidgetResizable(True) scroll_area.setFrameShape(QFrame.NoFrame) scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.setCentralWidget(scroll_area) central_widget = QWidget() central_widget.setMinimumWidth(920) scroll_area.setWidget(central_widget) # 创建布局 layout = QVBoxLayout(central_widget) layout.setSpacing(10) layout.setContentsMargins(20, 20, 20, 20) # 创建状态标签 self.status_label = QLabel("状态: 就绪") self.status_label.setStyleSheet("font-family: 'PingFang SC', 'Helvetica Neue', 'Microsoft YaHei', sans-serif; font-size: 14px; font-weight: 600; color: #2c3e50;") layout.addWidget(self.status_label) # 创建配置信息显示区域 config_group = QGroupBox("配置信息") config_group.setStyleSheet(""" QGroupBox { font-size: 16px; font-weight: 600; color: #2c3e50; border: 1px solid #e2e8f0; border-radius: 10px; margin-top: 20px; padding: 0; background-color: #ffffff; } QGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; padding: 8px 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border-radius: 10px 10px 0 0; font-family: 'PingFang SC', 'Helvetica Neue', 'Microsoft YaHei', sans-serif; font-size: 14px; font-weight: 600; } """) config_layout = QVBoxLayout() config_layout.setSpacing(12) config_layout.setContentsMargins(20, 20, 20, 20) # 创建配置标签 self.config_labels = { 'detect_options': QLabel("检测选项: 加载中..."), 'proxy_config': QLabel("代理配置: 加载中..."), 'thread_count': QLabel("检测线程数: 加载中...") } # 添加配置标签到布局 for label in self.config_labels.values(): label.setStyleSheet(""" QLabel { font-family: 'PingFang SC', 'Helvetica Neue', 'Microsoft YaHei', sans-serif; font-size: 13px; font-weight: 400; color: #4a5568; background-color: #f7fafc; padding: 12px 18px; border-radius: 8px; border-left: 4px solid #667eea; margin: 0; min-height: 64px; white-space: normal; border: 1px solid #e2e8f0; } QLabel:hover { background-color: #edf2f7; border-left-color: #764ba2; } """) label.setWordWrap(True) # 启用自动换行 label.setAlignment(Qt.AlignLeft | Qt.AlignTop) config_layout.addWidget(label) config_group.setLayout(config_layout) layout.addWidget(config_group) # 创建进度条 self.progress_bar = QProgressBar() self.progress_bar.setValue(0) layout.addWidget(self.progress_bar) # 创建日志文本框 self.log_text = QTextEdit() self.log_text.setReadOnly(True) self.log_text.setStyleSheet("font-family: Consolas, monospace; font-size: 12px; border: 1px solid #ddd; border-radius: 5px; background-color: #2d2d2d; color: #e0e0e0;") self.log_text.setMinimumHeight(320) layout.addWidget(self.log_text, 1) # 创建按钮布局 button_layout = QHBoxLayout() button_layout.setSpacing(10) use_fancy_button_effects = os.environ.get("QT_QPA_PLATFORM", "").lower() != "offscreen" # 按钮样式表 start_button_style = self._button_style( "#28a745", "#218838", "#1e7e34", fancy_effects=use_fancy_button_effects, ) refresh_button_style = """ QPushButton { background-color: #6f42c1; color: white; border: none; border-radius: 8px; font-size: 16px; font-weight: bold; padding: 10px 20px; min-width: 120px; } QPushButton:hover { background-color: #5a32a3; } QPushButton:disabled { background-color: #6c757d; color: #adb5bd; } """ stop_button_style = self._button_style( "#dc3545", "#c82333", "#a71e2a", fancy_effects=use_fancy_button_effects, ) exit_button_style = self._button_style( "#007bff", "#0069d9", "#0056b3", fancy_effects=use_fancy_button_effects, ) # 创建开始检测按钮 self.start_button = QPushButton("开始检测") self.start_button.setObjectName("start_button") self.start_button.clicked.connect(self.start_detection) self.start_button.setFixedHeight(50) self.start_button.setStyleSheet(start_button_style) button_layout.addWidget(self.start_button) # 创建停止按钮 self.stop_button = QPushButton("停止检测") self.stop_button.setObjectName("stop_button") self.stop_button.clicked.connect(self.stop_detection) self.stop_button.setEnabled(False) self.stop_button.setFixedHeight(50) self.stop_button.setStyleSheet(stop_button_style) button_layout.addWidget(self.stop_button) self.refresh_proxy_button = QPushButton("刷新代理池") self.refresh_proxy_button.clicked.connect(self.refresh_proxy_pool_manually) self.refresh_proxy_button.setFixedHeight(50) self.refresh_proxy_button.setStyleSheet(refresh_button_style) button_layout.addWidget(self.refresh_proxy_button) # 创建退出按钮 self.exit_button = QPushButton("退出") self.exit_button.setObjectName("exit_button") self.exit_button.clicked.connect(self.close) self.exit_button.setFixedHeight(50) self.exit_button.setStyleSheet(exit_button_style) button_layout.addWidget(self.exit_button) layout.addLayout(button_layout) # 初始化检测端 self.worker = DetectWorker(self) # 启动Redis订阅线程(如果使用Redis) if self.worker.use_redis: self.worker.running = True self.worker.redis_sub_thread = threading.Thread(target=self.worker.start_redis_subscription) self.worker.redis_sub_thread.daemon = True self.worker.redis_sub_thread.start() logger.debug("Redis配置更新订阅已启动") # 连接信号 self.worker.connect_signals() # 初始化检测线程 self.detect_thread = None # 初始化日志队列和处理线程 self.log_queue = Queue() self.log_processing = True # 启动日志处理线程 self.log_thread = threading.Thread(target=self.process_log_queue) self.log_thread.daemon = True self.log_thread.start() # 配置日志 logger.add(self.enqueue_log_message, level="INFO") # 启动定时任务 self.scheduler_thread = threading.Thread(target=self.start_scheduler) self.scheduler_thread.daemon = True self.scheduler_thread.start() def enqueue_log_message(self, message): """ 日志回调函数,将日志消息放入队列 """ self.log_queue.put(message) def process_log_queue(self): """ 处理日志队列中的消息 """ while self.log_processing: try: message = self.log_queue.get(timeout=1) # 提取时间和消息部分 # 日志格式: 2026-04-13 00:22:05.365 | INFO | __main__:load_thread_count:846 - 从Redis加载检测线程数成功: 500 import re match = re.match(r'(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}) \| .*? \| .*? - (.*)', message) if match: time_str = match.group(1) msg_str = match.group(2) log_text = f"{time_str} | {msg_str}" else: log_text = message.replace("\n", "") # 使用QMetaObject.invokeMethod确保线程安全的GUI更新 QMetaObject.invokeMethod(self.log_text, "append", Qt.QueuedConnection, Q_ARG(str, log_text)) QMetaObject.invokeMethod(self.log_text.verticalScrollBar(), "setValue", Qt.QueuedConnection, Q_ARG(int, self.log_text.verticalScrollBar().maximum())) except Exception as e: pass def log_message(self, message): """ 日志回调函数(保留用于信号连接) """ self.enqueue_log_message(message) def start_detection(self): """ 开始检测 """ self.status_label.setText("状态: 检测中...") self.start_button.setEnabled(False) self.stop_button.setEnabled(True) # 创建并启动检测线程 self.detect_thread = DetectThread(self.worker) self.detect_thread.log_signal.connect(self.log_message) self.detect_thread.progress_signal.connect(self.update_progress) self.detect_thread.thread_count_signal.connect(self.update_thread_count_display) self.detect_thread.finished_signal.connect(self.detection_finished) self.detect_thread.start() def stop_detection(self): """ 停止检测 """ self.status_label.setText("状态: 停止中...") self.worker.stop() self.stop_button.setEnabled(False) def detection_finished(self): """ 检测完成 """ self.status_label.setText("状态: 检测完成") self.start_button.setEnabled(True) self.stop_button.setEnabled(False) def update_progress(self, current, total): """ 更新进度条 """ # 直接在当前线程执行,避免使用QMetaObject.invokeMethod try: if total > 0: progress = int((current / total) * 100) self.progress_bar.setValue(progress) except Exception as e: print(f"更新进度失败: {e}") def update_thread_count_display(self, active_count, total_count): """ 更新线程数量显示 :param active_count: 当前活跃线程数量 :param total_count: 总线程数量 """ try: # 检查是否有配置标签 if hasattr(self, 'config_labels') and 'thread_count' in self.config_labels: self.config_labels['thread_count'].setText(f"检测线程数: {total_count} (当前: {active_count})") except Exception as e: print(f"更新线程数量显示失败: {e}") def refresh_proxy_pool_manually(self): self.status_label.setText("状态: 正在刷新代理池...") self.refresh_proxy_button.setEnabled(False) def run_refresh(): try: self.worker.refresh_proxy_pool() with self.worker.proxy_pool_lock: proxy_count = len(self.worker.proxy_pool) self.worker.update_config_labels() QMetaObject.invokeMethod( self.status_label, "setText", Qt.QueuedConnection, Q_ARG(str, f"状态: 代理池刷新完成,可用代理 {proxy_count} 个") ) except Exception as e: QMetaObject.invokeMethod( self.status_label, "setText", Qt.QueuedConnection, Q_ARG(str, f"状态: 刷新代理池失败: {e}") ) finally: QMetaObject.invokeMethod( self.refresh_proxy_button, "setEnabled", Qt.QueuedConnection, Q_ARG(bool, True) ) threading.Thread(target=run_refresh, daemon=True).start() def start_scheduler(self): """ 启动定时任务 """ # 每天凌晨1点执行检测 schedule.every().day.at("01:00").do(self.scheduled_detection) # 循环执行定时任务 while True: schedule.run_pending() time.sleep(60) def scheduled_detection(self): """ 定时检测任务 """ self.log_message(f"[{datetime.now()}] 开始定时检测任务") self.start_detection() def refresh_config(self): """ 刷新配置 """ try: # 重新加载配置 new_detect_options = self.worker.load_detect_options() new_proxy_config = self.worker.load_proxy_config() new_thread_count = self.worker.load_thread_count() self.worker.load_cookies_from_remote() # 检查配置是否发生变化 config_changed = False if new_detect_options != self.worker.detect_options: self.worker.detect_options = new_detect_options config_changed = True if new_proxy_config != self.worker.proxy_config: self.worker.proxy_config = new_proxy_config config_changed = True if new_thread_count != self.worker.thread_count: self.worker.thread_count = new_thread_count config_changed = True # 如果配置发生变化,更新标签 if config_changed: self.worker.update_config_labels() self.log_message(f"[{datetime.now()}] 配置已更新") except Exception as e: self.log_message(f"[{datetime.now()}] 刷新配置失败: {e}") def event(self, event): """ 处理事件 """ if event.type() == ConfigUpdateEvent.Type: self.update_config_labels() return True return super().event(event) def update_config_labels(self): """ 更新GUI配置标签 """ logger.debug("开始更新GUI配置标签") try: if self.worker: try: logger.debug(f"worker对象存在,当前配置: {self.worker.detect_options}, {self.worker.proxy_config}, {self.worker.thread_count}") # 准备更新数据 detect_options = self.worker.detect_options free_options_display = [] paid_options_display = [] option_names = { 'detect_register': '检查注册', 'detect_baidu_site': '百度site查询', 'detect_360_site': '360的site查询', 'detect_chinaz': '站长之家查询', 'detect_aizhan': '爱站网查询', 'detect_wayback': '时光机检测', 'detect_jucha': '聚查查询', 'detect_juziseo': '桔子查询' } ordered_keys = detect_options.get('detect_order') or list(option_names.keys()) normalized_keys = [key for key in ordered_keys if key in option_names] for key in option_names: if key not in normalized_keys: normalized_keys.append(key) free_keys = { 'detect_register', 'detect_baidu_site', 'detect_360_site', 'detect_chinaz', 'detect_aizhan', 'detect_wayback', } for index, key in enumerate(normalized_keys, start=1): name = option_names[key] if key in detect_options and detect_options[key]: item_text = f"{index}.{name}" if key in free_keys: free_options_display.append(item_text) else: paid_options_display.append(item_text) free_text = " | ".join(free_options_display) if free_options_display else "无" paid_text = " | ".join(paid_options_display) if paid_options_display else "无" detect_options_text = ( "当前检测选项:\n" f"免费优先: {free_text}\n" f"付费后置: {paid_text}" ) # 准备代理配置文本 proxy_enable = self.worker.proxy_config.get('proxy_enable', False) allow_direct = self.worker.proxy_config.get('allow_direct', False) proxy_urls = self.worker.proxy_config.get('proxy_urls') or [] if not proxy_urls and self.worker.proxy_config.get('proxy_url'): proxy_urls = [self.worker.proxy_config.get('proxy_url', '')] with self.worker.proxy_pool_lock: proxy_pool_count = len(self.worker.proxy_pool) last_refresh_time = ( self.worker.proxy_last_refresh_time.strftime('%Y-%m-%d %H:%M:%S') if self.worker.proxy_last_refresh_time else '未刷新' ) proxy_display = ( f"启用: {'是' if proxy_enable else '否'} | " f"允许直连: {'是' if allow_direct else '否'} | " f"代理池链接数: {len([url for url in proxy_urls if url])} | " f"当前可用代理数: {proxy_pool_count}\n" f"最近刷新: {last_refresh_time} | " f"最近结果: {self.worker.proxy_last_refresh_status} | " f"原始返回数: {self.worker.proxy_last_refresh_total_items}" ) proxy_text = f"代理配置: {proxy_display}" # 准备线程数文本 thread_count_text = f"检测线程数: {self.worker.thread_count}" logger.debug(f"准备更新标签: {detect_options_text}, {proxy_text}, {thread_count_text}") # 检查配置标签是否存在 if hasattr(self, 'config_labels'): # 使用QMetaObject.invokeMethod确保在主线程中更新UI from PySide6.QtCore import QMetaObject, Qt, Q_ARG # 更新检测选项标签 if 'detect_options' in self.config_labels: QMetaObject.invokeMethod(self.config_labels['detect_options'], "setText", Qt.QueuedConnection, Q_ARG(str, detect_options_text)) # 更新代理配置标签 if 'proxy_config' in self.config_labels: QMetaObject.invokeMethod(self.config_labels['proxy_config'], "setText", Qt.QueuedConnection, Q_ARG(str, proxy_text)) # 更新线程数标签 if 'thread_count' in self.config_labels: QMetaObject.invokeMethod(self.config_labels['thread_count'], "setText", Qt.QueuedConnection, Q_ARG(str, thread_count_text)) logger.debug("GUI配置标签更新完成") else: logger.debug("配置标签不存在,跳过更新") except Exception as e: logger.error(f"更新GUI标签失败: {e}") import traceback logger.error(traceback.format_exc()) else: logger.debug("worker对象不存在") except Exception as e: logger.error(f"执行update_config_labels失败: {e}") import traceback logger.error(traceback.format_exc()) from PySide6.QtCore import QObject, Signal, QEvent class ConfigUpdateEvent(QEvent): """ 配置更新事件类 """ Type = QEvent.Type(QEvent.User + 1) def __init__(self): super().__init__(ConfigUpdateEvent.Type) class ConfigUpdateSignal(QObject): """ 配置更新信号类 """ config_updated = Signal() class DetectWorker: """ 域名检测端工作类 """ def __init__(self, main_window=None): """ 初始化检测端 :param main_window: 主窗口实例,用于更新GUI """ self.main_window = main_window self.running = False self.stop_requested = False self.detecting = False self.detect_threads = [] self.detect_thread = None # 检测线程实例 self.detect_command_thread = None self.detect_lock = threading.Lock() self.runtime_heartbeat_interval = 30 self._runtime_heartbeat_stop = threading.Event() self._last_runtime_phase = "idle" self._last_runtime_detail = "Worker 初始化中" self._last_runtime_extra = {} self.current_cycle_token = "" self.current_job_id = None self.current_job_code = "" # 初始化数据库连接 self.db = Database() try: self.db.ensure_cluster_runtime_tables() self.db.register_cluster_node( config.NODE_CODE, config.NODE_REGION, config.NODE_ROLE, status="online", current_load=0, metadata={"service": "detect-worker", "worker_mode": os.environ.get("WORKER_MODE", "")}, ) except Exception as e: logger.warning(f"初始化多机运行库骨架失败: {e}") try: recycled = self.db.recycle_running_domains(DETECT_STATUS_FAILED) if recycled: logger.warning(f"Worker 启动时回收了 {recycled} 个遗留的检测中域名,已标记为失败待重试") released = self.db.release_detect_job_items_for_node(config.NODE_CODE) if released: logger.warning(f"Worker 启动时释放了 {released} 个当前节点遗留任务项,已重新回到 pending") except Exception as e: logger.warning(f"Worker 启动时清理遗留运行态失败: {e}") # 初始化Redis连接 try: self.redis_client = redis.Redis( host=config.REDIS_HOST, port=config.REDIS_PORT, password=config.REDIS_PASSWORD, db=config.REDIS_DB, decode_responses=True, socket_connect_timeout=10, socket_timeout=10, retry_on_timeout=True, health_check_interval=30 ) # 测试连接 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 # 初始化配置更新信号 self.config_signal = ConfigUpdateSignal() # 加载配置 self.detect_options = self.load_detect_options() self.proxy_config = self.load_proxy_config() self.thread_count = self.load_thread_count() # 从配置文件加载线程数 # 初始化代理池 self.proxy_pool = [] self.proxy_pool_lock = threading.Lock() self.proxy_refresh_lock = threading.Lock() self.proxy_last_refresh_time = None self.proxy_last_refresh_status = "未刷新" self.proxy_last_refresh_source_count = 0 self.proxy_last_refresh_total_items = 0 self.proxy_last_validated_count = 0 self.proxy_last_available_count = 0 self.proxy_last_source_stats = [] self.proxy_refresh_cooldown_seconds = 30 self.proxy_next_refresh_time = 0.0 self.proxy_max_reuse_count = max(8, self.thread_count * 4) self.proxy_failure_lock = threading.Lock() self.proxy_failure_counts = {} self.proxy_quarantine_until = {} # 加载敏感词(只加载一次,所有线程共享) try: self.sensitive_words = self.db.get_all_sensitive_words() logger.info(f"加载敏感词完成,共 {len(self.sensitive_words)} 个敏感词") except Exception as e: logger.error(f"加载敏感词失败: {e}") import traceback logger.error(traceback.format_exc()) self.sensitive_words = [] # 初始化运行状态 self.running = False # 加载cookies self.load_cookies_from_remote() # 付费检测器按需初始化,避免在默认关闭时产生无效启动告警。 self.jc_instance = None self.juziseo_instance = None self._init_optional_detectors() self.wayback_detector = WaybackDetector() # 显示所有配置信息 logger.info("检测端配置信息:") logger.info(f"检测选项: {self.detect_options}") logger.info(f"代理配置: {self.proxy_config}") logger.info(f"检测线程数: {self.thread_count}") # Redis订阅线程将在start方法中启动 self._update_runtime_state("idle", "Worker 已启动,等待检测指令") self._start_runtime_heartbeat_loop() def _start_runtime_heartbeat_loop(self): existing = getattr(self, "_runtime_heartbeat_thread", None) if existing and existing.is_alive(): return self._runtime_heartbeat_stop.clear() self._runtime_heartbeat_thread = threading.Thread( target=self._runtime_heartbeat_loop, name="RuntimeHeartbeatThread", daemon=True, ) self._runtime_heartbeat_thread.start() def _runtime_heartbeat_loop(self): while not self._runtime_heartbeat_stop.wait(self.runtime_heartbeat_interval): try: self._update_runtime_state( self._last_runtime_phase, self._last_runtime_detail, **dict(self._last_runtime_extra or {}), ) except Exception as e: logger.debug(f"运行态心跳续期失败: {e}") def _consume_pending_control_command(self): if not self.use_redis or self.redis_client is None: return try: payload = self.redis_client.getdel(PENDING_CONTROL_KEY) except Exception: try: payload = self.redis_client.get(PENDING_CONTROL_KEY) if payload: self.redis_client.delete(PENDING_CONTROL_KEY) except Exception as e: logger.debug(f"读取待执行控制指令失败: {e}") return if not payload: return logger.info(f"发现待执行 Worker 控制指令: {payload}") self._handle_control_message(payload) def _update_runtime_state(self, phase: str, detail: str, **extra): """ 更新 Redis 中的检测运行态,供 Web 后台读取。 """ self._last_runtime_phase = phase self._last_runtime_detail = detail self._last_runtime_extra = dict(extra or {}) if not self.use_redis or self.redis_client is None: return with self.proxy_pool_lock: available_proxy_count = len(self.proxy_pool) payload_warning = "" if self.proxy_config.get('proxy_enable', False) and available_proxy_count <= 0: if ( self.proxy_last_refresh_status and self.proxy_last_refresh_status not in {"未刷新", "代理未启用", "未配置代理池链接"} and "刷新成功" not in self.proxy_last_refresh_status ): payload_warning = self.proxy_last_refresh_status payload = { "phase": phase, "detail": detail, "service_running": bool(self.running), "detecting": bool(self.detecting), "stop_requested": bool(self.stop_requested), "available_proxy_count": available_proxy_count, "proxy_last_refresh_status": self.proxy_last_refresh_status, "proxy_last_refresh_time": self.proxy_last_refresh_time.strftime("%Y-%m-%d %H:%M:%S") if self.proxy_last_refresh_time else "", "proxy_last_refresh_source_count": int(self.proxy_last_refresh_source_count or 0), "proxy_last_refresh_total_items": int(self.proxy_last_refresh_total_items or 0), "proxy_last_validated_count": int(self.proxy_last_validated_count or 0), "proxy_last_available_count": int(self.proxy_last_available_count or 0), "proxy_last_source_stats": list(self.proxy_last_source_stats or []), "recent_warning": payload_warning, "updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), } payload.update(extra) if self.current_cycle_token and not payload.get("cycle_token"): payload["cycle_token"] = self.current_cycle_token if self.current_job_id and not payload.get("job_id"): payload["job_id"] = self.current_job_id if self.current_job_code and not payload.get("job_code"): payload["job_code"] = self.current_job_code try: self.redis_client.set(RUNTIME_STATE_KEY, json.dumps(payload, ensure_ascii=False)) except Exception as e: logger.debug(f"写入检测运行态失败: {e}") try: current_load = int(extra.get("active_threads", payload.get("active_threads", 0)) or 0) if bool(payload.get("detecting")) and current_load <= 0: current_load = 1 self.db.register_cluster_node( config.NODE_CODE, config.NODE_REGION, config.NODE_ROLE, status="busy" if payload.get("detecting") else "online", current_load=current_load, metadata={ "phase": phase, "detail": detail, "detecting": bool(payload.get("detecting")), "available_proxy_count": int(payload.get("available_proxy_count", 0) or 0), "active_threads": current_load, "cycle_token": str(payload.get("cycle_token") or ""), "job_id": payload.get("job_id"), "job_code": str(payload.get("job_code") or ""), "proxy_runtime": { "sources": int(payload.get("proxy_last_refresh_source_count", 0) or 0), "raw_items": int(payload.get("proxy_last_refresh_total_items", 0) or 0), "validated": int(payload.get("proxy_last_validated_count", 0) or 0), "available": int(payload.get("proxy_last_available_count", 0) or 0), }, }, ) except Exception as e: logger.debug(f"更新检测节点心跳失败: {e}") def _mark_detection_phase(self, phase: str, detail: str, **extra): self._update_runtime_state(phase, detail, **extra) def _set_active_cycle_context(self, control_payload=None): payload = control_payload or {} self.current_cycle_token = str(payload.get("cycle_token") or "").strip() job_id = payload.get("job_id") try: self.current_job_id = int(job_id) if job_id not in (None, "", 0, "0") else None except Exception: self.current_job_id = None self.current_job_code = str(payload.get("job_code") or "").strip() def _clear_active_cycle_context(self): self.current_cycle_token = "" self.current_job_id = None self.current_job_code = "" def start_detection_async(self, source: str = "remote", control_payload=None): """ 异步启动一次检测任务,避免阻塞 Redis 订阅线程。 """ with self.detect_lock: if self.detect_command_thread and self.detect_command_thread.is_alive(): logger.warning("收到启动检测指令,但检测任务已在运行,忽略重复启动") self._update_runtime_state("running", "检测任务已在运行,忽略重复启动") return False self.stop_requested = False self.detect_command_thread = threading.Thread( target=self._run_detection_session, kwargs={"source": source, "control_payload": dict(control_payload or {})}, name="RemoteDetectCommandThread", daemon=True, ) self.detect_command_thread.start() logger.info(f"已接受检测启动指令,来源: {source}") self._set_active_cycle_context(control_payload) self._update_runtime_state( "starting", f"已接受检测启动指令,来源: {source}", source=source, cycle_token=self.current_cycle_token, job_id=self.current_job_id, job_code=self.current_job_code, ) return True def _run_detection_session(self, source: str = "remote", control_payload=None): self._set_active_cycle_context(control_payload) with self.detect_lock: self.detecting = True try: logger.info(f"开始执行远程检测任务,来源: {source}") self._update_runtime_state( "starting", f"开始执行检测任务,来源: {source}", source=source, cycle_token=self.current_cycle_token, job_id=self.current_job_id, job_code=self.current_job_code, ) self.start_detection() except Exception as e: logger.error(f"远程检测任务执行失败: {e}") import traceback logger.error(traceback.format_exc()) self._update_runtime_state( "failed", f"检测任务执行失败: {e}", source=source, cycle_token=self.current_cycle_token, job_id=self.current_job_id, job_code=self.current_job_code, ) finally: with self.detect_lock: self.detecting = False self.detect_command_thread = None if self.running and not self.stop_requested: self._update_runtime_state( "idle", "检测任务结束,Worker 保持待命", source=source, cycle_token=self.current_cycle_token, job_id=self.current_job_id, job_code=self.current_job_code, ) self._clear_active_cycle_context() def request_stop_detection(self, source: str = "remote"): """ 请求停止当前检测任务,但保留 Worker 进程存活。 """ if not self.detecting and not (self.detect_command_thread and self.detect_command_thread.is_alive()): logger.info("收到停止检测指令,但当前没有运行中的检测任务") self._update_runtime_state("idle", "当前没有运行中的检测任务") return False logger.info(f"收到停止检测指令,来源: {source}") self.stop_requested = True self._update_runtime_state( "stopping", f"收到停止检测指令,等待当前批次安全退出,来源: {source}", cycle_token=self.current_cycle_token, job_id=self.current_job_id, job_code=self.current_job_code, ) return True def _handle_control_message(self, payload): try: control_payload = json.loads(payload) if isinstance(payload, str) else payload except Exception: control_payload = {"action": str(payload)} action = str(control_payload.get("action", "")).strip() if action == "start_detection": self.start_detection_async(source="redis-control", control_payload=control_payload) elif action == "stop_detection": self.request_stop_detection(source="redis-control") elif action: logger.warning(f"收到未知 Worker 控制动作: {action}") def connect_signals(self): """ 连接信号到槽函数 """ if self.main_window: try: # 连接信号到槽函数 self.config_signal.config_updated.connect(self.main_window.update_config_labels) logger.debug("信号连接成功") # 连接后立即更新一次配置 self.update_config_labels() except Exception as e: logger.error(f"连接信号失败: {e}") def _init_optional_detectors(self): if self.detect_options.get('detect_jucha') and self.jc_instance is None: self.jc_instance = jucha.JC(proxies=None) self.jc_instance.load_juming_cookies() self.jc_instance.load_cookies() if self.detect_options.get('detect_juziseo') and self.juziseo_instance is None: self.juziseo_instance = juziseo.Juziseo(proxies=None) self.juziseo_instance.load_cookies() def load_detect_options(self): """ 加载检测选项 """ default_order = [ 'detect_register', 'detect_baidu_site', 'detect_360_site', 'detect_chinaz', 'detect_aizhan', 'detect_wayback', 'detect_jucha', 'detect_juziseo', ] default_options = { '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, } try: # 从Redis获取配置 if self.use_redis: detect_options_str = self.redis_client.get('domain_tool:detect_options') if detect_options_str: detect_options = default_options.copy() detect_options.update(json.loads(detect_options_str)) if detect_options.get('detect_whois') or detect_options.get('detect_beian') or detect_options.get('detect_intercept'): detect_options['detect_jucha'] = True if detect_options.get('detect_juziseo_outlink'): detect_options['detect_juziseo'] = True order = detect_options.get('detect_order') or [] detect_options['detect_order'] = [key for key in order if key in default_order] for key in default_order: if key not in detect_options['detect_order']: detect_options['detect_order'].append(key) logger.info(f"从Redis加载检测选项成功: {detect_options}") return detect_options # 从本地文件获取配置 if os.path.exists('detect_options.json'): with open('detect_options.json', 'r', encoding='utf-8') as f: detect_options = default_options.copy() detect_options.update(json.load(f)) if detect_options.get('detect_whois') or detect_options.get('detect_beian') or detect_options.get('detect_intercept'): detect_options['detect_jucha'] = True if detect_options.get('detect_juziseo_outlink'): detect_options['detect_juziseo'] = True order = detect_options.get('detect_order') or [] detect_options['detect_order'] = [key for key in order if key in default_order] for key in default_order: if key not in detect_options['detect_order']: detect_options['detect_order'].append(key) logger.info(f"从本地文件加载检测选项成功: {detect_options}") return detect_options else: logger.info(f"使用默认检测选项: {default_options}") return default_options except Exception as e: logger.error(f"加载检测选项失败: {e}") logger.info(f"使用默认检测选项: {default_options}") return default_options def load_proxy_config(self): """ 加载代理配置 """ default_config = {'proxy_enable': False, 'proxy_url': '', 'proxy_urls': [], 'allow_direct': False} try: # 从Redis获取配置 if self.use_redis: proxy_config_str = self.redis_client.get('domain_tool:proxy_config') if proxy_config_str: proxy_config = default_config.copy() proxy_config.update(json.loads(proxy_config_str)) 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', '')] proxy_config['proxy_urls'] = [url.strip() for url in proxy_urls if url and str(url).strip()] proxy_config['proxy_url'] = proxy_config['proxy_urls'][0] if proxy_config['proxy_urls'] else '' proxy_config['allow_direct'] = bool(proxy_config.get('allow_direct', False)) logger.info(f"从Redis加载代理配置成功: {proxy_config}") return proxy_config # 从本地文件获取配置 if os.path.exists('proxy_config.json'): with open('proxy_config.json', 'r', encoding='utf-8') as f: proxy_config = default_config.copy() proxy_config.update(json.load(f)) logger.info(f"从本地文件加载代理配置成功: {proxy_config}") 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', '')] proxy_config['proxy_urls'] = [url.strip() for url in proxy_urls if url and str(url).strip()] proxy_config['proxy_url'] = proxy_config['proxy_urls'][0] if proxy_config['proxy_urls'] else '' proxy_config['allow_direct'] = bool(proxy_config.get('allow_direct', False)) return proxy_config else: logger.info(f"使用默认代理配置: {default_config}") return default_config except Exception as e: logger.error(f"加载代理配置失败: {e}") logger.info(f"使用默认代理配置: {default_config}") return default_config def load_thread_count(self): """ 加载检测线程数 """ max_recommended_threads = 20 try: node_code = str(getattr(config, "NODE_CODE", "") or "").strip() node_thread_counts = {} # 从Redis获取配置 if self.use_redis: node_thread_counts_raw = self.redis_client.get('domain_tool:node_thread_counts') if node_thread_counts_raw: try: node_thread_counts = json.loads(node_thread_counts_raw) except Exception as e: logger.warning(f"解析 Redis 节点线程覆盖配置失败: {e}") if node_code and isinstance(node_thread_counts, dict): node_thread_count_str = node_thread_counts.get(node_code) if node_thread_count_str is not None: thread_count = min(max_recommended_threads, max(1, int(node_thread_count_str))) logger.info(f"从Redis加载节点专属检测线程数成功: {node_code} -> {thread_count}") return thread_count thread_count_str = self.redis_client.get('domain_tool:thread_count') if thread_count_str: thread_count = min(max_recommended_threads, max(1, int(thread_count_str))) logger.info(f"从Redis加载检测线程数成功: {thread_count}") return thread_count # 从本地文件获取配置 if os.path.exists('node_thread_counts.json'): with open('node_thread_counts.json', 'r', encoding='utf-8') as f: node_thread_counts = json.load(f) if node_code and isinstance(node_thread_counts, dict): node_thread_count = node_thread_counts.get(node_code) if node_thread_count is not None: thread_count = min(max_recommended_threads, max(1, int(node_thread_count))) logger.info(f"从本地文件加载节点专属检测线程数成功: {node_code} -> {thread_count}") return thread_count 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 = thread_config.get('thread_count', '10') thread_count = min(max_recommended_threads, max(1, int(thread_count))) logger.info(f"从本地文件加载检测线程数成功: {thread_count}") return thread_count else: default_thread_count = 4 # 默认线程数,降低线程数量以减轻系统负担 logger.info(f"使用默认检测线程数: {default_thread_count}") return default_thread_count except Exception as e: logger.error(f"加载检测线程数失败: {e}") default_thread_count = 4 # 默认线程数,降低线程数量以减轻系统负担 logger.info(f"使用默认检测线程数: {default_thread_count}") return default_thread_count def test_proxy(self, proxy_item, result_queue): """ 测试单个代理的可用性 :param proxy_item: 代理信息 :param result_queue: 结果队列 """ try: if 'ip' in proxy_item and 'port' in proxy_item: ip = proxy_item['ip'] port = proxy_item['port'] username = proxy_item.get('username', '') password = proxy_item.get('password', '') if username and password: proxy_url = f"http://{username}:{password}@{ip}:{port}" else: proxy_url = f"http://{ip}:{port}" # 检查代理是否可用 import requests test_proxies = { 'http': proxy_url, 'https': proxy_url } test_response = requests.get('https://m.baidu.com', proxies=test_proxies, timeout=5) if test_response.status_code == 200: # logger.info(f"代理可用性检查通过: {proxy_url}") result_queue.put(test_proxies) else: logger.warning(f"代理可用性检查失败,状态码: {test_response.status_code}, 代理: {proxy_url}") except Exception as e: # logger.error(f"代理可用性检查失败: {e}, 代理: {proxy_item}") pass finally: # 确保队列中添加一个标记,表示该线程已完成 result_queue.put(None) def _extract_proxy_items(self, proxy_data): if isinstance(proxy_data, dict): if 'list' in proxy_data and isinstance(proxy_data['list'], list): return proxy_data['list'] if 'ip' in proxy_data and 'port' in proxy_data: return [proxy_data] if isinstance(proxy_data, list): return proxy_data return [] def _proxy_key(self, proxy): if isinstance(proxy, dict): return json.dumps(proxy, sort_keys=True, ensure_ascii=False) return str(proxy) def _is_proxy_quarantined(self, proxy): proxy_key = self._proxy_key(proxy) now_ts = time.time() with self.proxy_failure_lock: retry_after = float(self.proxy_quarantine_until.get(proxy_key, 0) or 0) if retry_after <= now_ts: if proxy_key in self.proxy_quarantine_until: self.proxy_quarantine_until.pop(proxy_key, None) self.proxy_failure_counts.pop(proxy_key, None) return False return True def _mark_proxy_failure(self, proxy): proxy_key = self._proxy_key(proxy) with self.proxy_failure_lock: failure_count = int(self.proxy_failure_counts.get(proxy_key, 0) or 0) + 1 self.proxy_failure_counts[proxy_key] = failure_count cooldown_seconds = min(180, 20 * failure_count) retry_after = time.time() + cooldown_seconds self.proxy_quarantine_until[proxy_key] = retry_after logger.info(f"代理临时隔离 {cooldown_seconds}s: {proxy}") return failure_count, cooldown_seconds def _clear_proxy_failure(self, proxy): proxy_key = self._proxy_key(proxy) with self.proxy_failure_lock: self.proxy_failure_counts.pop(proxy_key, None) self.proxy_quarantine_until.pop(proxy_key, None) def _schedule_proxy_refresh_if_needed(self, available_count=None): with self.proxy_pool_lock: current_count = len(self.proxy_pool) if available_count is None else available_count threshold = max(2, self.thread_count // 2) if current_count < threshold and not self.proxy_refresh_lock.locked(): threading.Thread(target=self.refresh_proxy_pool, daemon=True).start() def refresh_proxy_pool(self): """ 刷新代理池 """ if not self.proxy_refresh_lock.acquire(blocking=False): logger.debug("代理池刷新已在进行中,跳过本次重复刷新") return if not self.proxy_config.get('proxy_enable', False): self.proxy_last_refresh_time = datetime.now() self.proxy_last_refresh_status = "代理未启用" self.proxy_last_refresh_source_count = 0 self.proxy_last_refresh_total_items = 0 self.proxy_last_validated_count = 0 self.proxy_last_available_count = 0 self.proxy_last_source_stats = [] self.proxy_next_refresh_time = 0.0 self._update_runtime_state( "idle" if not self.detecting else "running", "代理未启用,使用直接连接", ) self.proxy_refresh_lock.release() return try: now_ts = time.time() with self.proxy_pool_lock: has_cached_proxies = bool(self.proxy_pool) if not has_cached_proxies and self.proxy_next_refresh_time and now_ts < self.proxy_next_refresh_time: wait_seconds = int(max(1, self.proxy_next_refresh_time - now_ts)) self.proxy_last_refresh_status = f"冷却中,{wait_seconds} 秒后再试" logger.info(f"代理池刷新冷却中,{wait_seconds} 秒后再试") self._update_runtime_state( "refreshing_proxy" if self.detecting else "idle", f"代理池刷新冷却中,{wait_seconds} 秒后再试", ) return proxy_api_urls = self.proxy_config.get('proxy_urls') or [] if not proxy_api_urls and self.proxy_config.get('proxy_url'): proxy_api_urls = [self.proxy_config.get('proxy_url', '')] proxy_api_urls = [url for url in proxy_api_urls if url] self.proxy_last_refresh_source_count = len(proxy_api_urls) self.proxy_last_source_stats = [] if proxy_api_urls: import requests import threading import random from queue import Queue proxy_list = [] for proxy_api_url in proxy_api_urls: source_stat = { "url": proxy_api_url, "status": "unknown", "http_status": 0, "raw_items": 0, "error": "", } try: response = requests.get(proxy_api_url, timeout=10) source_stat["http_status"] = int(response.status_code or 0) if response.status_code == 200: proxy_data = response.json() current_items = self._extract_proxy_items(proxy_data) proxy_list.extend(current_items) source_stat["raw_items"] = len(current_items) source_stat["status"] = "ok" logger.info(f"代理池链接拉取成功: {proxy_api_url}, 原始代理数: {len(current_items)}") else: source_stat["status"] = "http_error" source_stat["error"] = f"HTTP {response.status_code}" logger.warning(f"代理池链接响应异常: {proxy_api_url}, 状态码: {response.status_code}") except Exception as api_error: source_stat["status"] = "request_error" source_stat["error"] = str(api_error) logger.warning(f"代理池链接拉取失败: {proxy_api_url}, 错误: {api_error}") self.proxy_last_source_stats.append(source_stat) if proxy_list: self.proxy_last_refresh_total_items = len(proxy_list) max_validate_count = max(24, self.thread_count * 3) if len(proxy_list) > max_validate_count: proxy_list = random.sample(proxy_list, max_validate_count) logger.info(f"代理池验证已启用抽样模式,本次抽样 {len(proxy_list)} 个代理进行可用性验证") self.proxy_last_validated_count = len(proxy_list) result_queue = Queue() threads = [] for proxy_item in proxy_list: thread = threading.Thread(target=self.test_proxy, args=(proxy_item, result_queue)) thread.daemon = True thread.start() threads.append(thread) new_proxies = [] seen_proxy_strings = set() completed = 0 start_time = time.time() timeout = 30 while completed < len(threads) and time.time() - start_time < timeout: try: result = result_queue.get(timeout=1) if result is not None: proxy_signature = json.dumps(result, sort_keys=True) if proxy_signature not in seen_proxy_strings: seen_proxy_strings.add(proxy_signature) new_proxies.append({'proxy': result, 'usage_count': 0}) completed += 1 except Exception: pass for thread in threads: try: thread.join(timeout=5) except Exception: pass with self.proxy_pool_lock: self.proxy_pool = new_proxies self.proxy_last_refresh_time = datetime.now() self.proxy_last_available_count = len(new_proxies) self.proxy_last_refresh_status = f"刷新成功,可用 {len(new_proxies)} 个" self.proxy_next_refresh_time = 0.0 if new_proxies else time.time() + self.proxy_refresh_cooldown_seconds logger.info(f"代理池刷新完成,共 {len(new_proxies)} 个可用代理,来源链接 {len(proxy_api_urls)} 个") self._update_runtime_state( "running" if self.detecting else "idle", f"代理池刷新完成,共 {len(new_proxies)} 个可用代理,来源链接 {len(proxy_api_urls)} 个,原始 {self.proxy_last_refresh_total_items} 个,验证 {self.proxy_last_validated_count} 个", ) else: with self.proxy_pool_lock: self.proxy_pool = [] self.proxy_last_refresh_time = datetime.now() self.proxy_last_refresh_total_items = 0 self.proxy_last_validated_count = 0 self.proxy_last_available_count = 0 self.proxy_last_refresh_status = "未取到可用代理数据" self.proxy_next_refresh_time = time.time() + self.proxy_refresh_cooldown_seconds logger.warning("所有代理池链接均未返回可用代理数据") self._update_runtime_state( "refreshing_proxy" if self.detecting else "idle", "所有代理池链接均未返回可用代理数据", ) else: with self.proxy_pool_lock: self.proxy_pool = [] self.proxy_last_refresh_time = datetime.now() self.proxy_last_refresh_total_items = 0 self.proxy_last_validated_count = 0 self.proxy_last_available_count = 0 self.proxy_last_refresh_status = "未配置代理池链接" self.proxy_next_refresh_time = time.time() + self.proxy_refresh_cooldown_seconds self._update_runtime_state( "idle" if not self.detecting else "running", "未配置代理池链接", ) except Exception as e: with self.proxy_pool_lock: self.proxy_pool = [] self.proxy_last_refresh_time = datetime.now() self.proxy_last_refresh_total_items = 0 self.proxy_last_validated_count = 0 self.proxy_last_available_count = 0 self.proxy_last_refresh_status = f"刷新失败: {e}" self.proxy_next_refresh_time = time.time() + self.proxy_refresh_cooldown_seconds logger.error(f"刷新代理池失败: {e}") self._update_runtime_state( "refreshing_proxy" if self.detecting else "failed", f"刷新代理池失败: {e}", ) finally: self.proxy_refresh_lock.release() def remove_proxy(self, proxy): """ 从代理池中移除失效的代理 :param proxy: 失效的代理 """ failure_count, cooldown_seconds = self._mark_proxy_failure(proxy) with self.proxy_pool_lock: remaining_count = len(self.proxy_pool) # 检查代理池中的代理结构 if self.proxy_pool and isinstance(self.proxy_pool[0], dict) and 'proxy' in self.proxy_pool[0]: # 新的代理池结构 for i, proxy_item in enumerate(self.proxy_pool): if proxy_item['proxy'] == proxy: proxy_item['usage_count'] = 0 self.proxy_pool.append(self.proxy_pool.pop(i)) remaining_count = len(self.proxy_pool) logger.info(f"代理进入隔离观察,暂不硬删除: {proxy}") break else: # 旧的代理池结构 if proxy in self.proxy_pool: remaining_count = len(self.proxy_pool) logger.info(f"代理进入隔离观察,暂不硬删除: {proxy}") logger.info( f"代理失败计数已更新: 第 {failure_count} 次失败,隔离 {cooldown_seconds}s,当前池内代理 {remaining_count} 个" ) self._schedule_proxy_refresh_if_needed(remaining_count) def get_proxies(self): """ 获取代理配置 """ if self.proxy_config.get('proxy_enable', False): with self.proxy_pool_lock: current_pool_size = len(self.proxy_pool) is_empty = current_pool_size == 0 is_insufficient = current_pool_size < max(2, self.thread_count // 2) if is_empty or is_insufficient: if is_empty: logger.info("代理池为空,刷新代理池") self.refresh_proxy_pool() if is_empty: wait_deadline = time.time() + 2.0 while time.time() < wait_deadline: with self.proxy_pool_lock: if self.proxy_pool: break if not self.proxy_refresh_lock.locked(): break time.sleep(0.1) with self.proxy_pool_lock: if self.proxy_pool: pool_size = len(self.proxy_pool) for _ in range(pool_size): if isinstance(self.proxy_pool[0], dict) and 'proxy' in self.proxy_pool[0]: proxy_item = self.proxy_pool.pop(0) proxy = proxy_item['proxy'] if self._is_proxy_quarantined(proxy): self.proxy_pool.append(proxy_item) continue usage_count = int(proxy_item.get('usage_count', 0)) + 1 if usage_count >= self.proxy_max_reuse_count: proxy_item['usage_count'] = 0 else: proxy_item['usage_count'] = usage_count self.proxy_pool.append(proxy_item) self._clear_proxy_failure(proxy) self._schedule_proxy_refresh_if_needed(len(self.proxy_pool)) return proxy proxy = self.proxy_pool.pop(0) if self._is_proxy_quarantined(proxy): self.proxy_pool.append(proxy) continue logger.info(f"从代理池选择代理: {proxy}") self.proxy_pool.append(proxy) self._clear_proxy_failure(proxy) self._schedule_proxy_refresh_if_needed(len(self.proxy_pool)) return proxy if self.proxy_config.get('proxy_enable', False): logger.warning("代理已启用,但当前无可用代理") fallback_detail = ( f"代理已启用,但当前无可用代理;已允许直连兜底,最近代理状态:{self.proxy_last_refresh_status}" if self.allow_direct_connection() else f"代理已启用,但当前无可用代理;且未允许直连,最近代理状态:{self.proxy_last_refresh_status}" ) self._update_runtime_state( "running" if self.detecting else "idle", fallback_detail, ) else: logger.info("未启用代理,使用直接连接") return None def allow_direct_connection(self): return bool(self.proxy_config.get('allow_direct', False)) def _get_proxy_for_step(self, domain_id, domain_name, step_name): proxy = self.get_proxies() if proxy: return proxy if self.proxy_config.get('proxy_enable', False) and not self.allow_direct_connection(): detail = self.proxy_last_refresh_status logger.error(f"{step_name} 无可用代理,且当前不允许直连兜底: {domain_name},最近代理状态: {detail}") self.db.update_domain_detect_status(domain_id, DETECT_STATUS_FAILED) return '__NO_PROXY__' return None def _mark_blacklisted(self, domain_id, domain_name, reason): self.db.update_domain_detect_status(domain_id, DETECT_STATUS_BLACKLISTED) self.db.add_to_blacklist(domain_name, reason) logger.info(f"域名已加入黑名单: {domain_name}, 原因: {reason}") def _step_result_payload(self, *, ok, state, message="", **extra): payload = { "status": bool(ok), "state": state, "message": message or "", "checked_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), } payload.update(extra) return payload def _record_step_result(self, domain_id, field_name, *, ok, state, message="", **extra): if not field_name: return self._upsert_json_detection(domain_id, **{field_name: self._step_result_payload(ok=ok, state=state, message=message, **extra)}) def _should_blacklist_result(self, message): if not message: return False lowered = str(message).lower() keywords = [ "检测到敏感词", "黑名单分类", "风险网站", "高危网站", "clienthold", "serverhold", "拦截", "敏感内容", ] return any(keyword.lower() in lowered for keyword in keywords) def _mark_detection_failed(self, domain_id, domain_name, step_name, reason, field_name=None): reason = reason or "未知错误" self.db.update_domain_detect_status(domain_id, DETECT_STATUS_FAILED) if field_name: self._record_step_result(domain_id, field_name, ok=False, state="failed", message=reason, step=step_name) logger.warning(f"{step_name} 技术失败,域名标记为检测失败: {domain_name}, 原因: {reason}") def _mark_detection_degraded(self, domain_id, domain_name, step_name, reason, field_name=None, review_required=True): reason = reason or "外部依赖异常" if field_name: self._record_step_result(domain_id, field_name, ok=False, state="degraded", message=reason, step=step_name) if review_required: try: self.db.update_domain_review_status(domain_id, REVIEW_STATUS_PENDING) except Exception as review_error: logger.warning(f"{step_name} 降级后设置待人工复核失败: {domain_name}, 错误: {review_error}") logger.warning(f"{step_name} 外部依赖异常,步骤降级继续执行: {domain_name}, 原因: {reason}") def _is_external_dependency_issue(self, message): if not message: return False lowered = str(message).lower() keywords = [ "timeout", "timed out", "connection", "connection reset", "connection refused", "max retries exceeded", "httpsconnectionpool", "proxyerror", "remotedisconnected", "temporarily unavailable", "502 bad gateway", "503 service unavailable", "504 gateway timeout", "network", "ssl", "read timeout", "connecttimeout", ] return any(keyword in lowered for keyword in keywords) def _complete_detection(self, domain_id, domain_name): self.db.update_domain_detect_status(domain_id, DETECT_STATUS_COMPLETED) domain_info = self.db.get_domain_by_id(domain_id) if not domain_info: return use_status = domain_info.get('use_status', 0) register_status = domain_info.get('register_status', 0) expire_date = domain_info.get('expire_date') if use_status == 0 and register_status == REGISTER_STATUS_AVAILABLE and expire_date: self.db.update_domain_expire_date(domain_id, None) logger.info(f"域名 {domain_name} 满足条件,已将expire_date置空") if register_status == REGISTER_STATUS_AVAILABLE: self.db.update_domain_review_status(domain_id, REVIEW_STATUS_PENDING) logger.info(f"域名 {domain_name} 满足条件,已设置为待人工复核") def _should_run_jucha(self, domain): return self.detect_options.get('detect_jucha', False) and domain.get('jucha_status', 0) != THIRD_PARTY_STATUS_DONE def _should_run_juziseo(self, domain): return self.detect_options.get('detect_juziseo', False) and domain.get('juziseo_status', 0) != THIRD_PARTY_STATUS_DONE def _get_detect_execution_order(self): default_order = [ 'detect_register', 'detect_baidu_site', 'detect_360_site', 'detect_chinaz', 'detect_aizhan', 'detect_wayback', 'detect_jucha', 'detect_juziseo', ] configured_order = self.detect_options.get('detect_order') or [] normalized_order = [key for key in configured_order if key in default_order] for key in default_order: if key not in normalized_order: normalized_order.append(key) free_keys = { 'detect_register', 'detect_baidu_site', 'detect_360_site', 'detect_chinaz', 'detect_aizhan', 'detect_wayback', } paid_keys = {'detect_jucha', 'detect_juziseo'} enabled_order = [key for key in normalized_order if self.detect_options.get(key, False)] free_order = [key for key in enabled_order if key in free_keys] paid_order = [key for key in enabled_order if key in paid_keys] return free_order + paid_order def _run_detect_register(self, domain_id, domain, domain_name): is_ykj = domain.get('source_type', 0) == 1 if is_ykj: logger.info(f"一口价域名跳过注册状态检测: {domain_name}") return True logger.info(f"检测注册状态: {domain_name}") proxy = self._get_proxy_for_step(domain_id, domain_name, '注册状态检测') if proxy == '__NO_PROXY__': return False try: tld = domain_name.split('.')[-1] status, expire_date = register.check_register(domain_name, tld, proxy if proxy else None) if status != -1: self.db.update_domain_register_status(domain_id, status) if expire_date: from datetime import datetime, timedelta try: date_part = expire_date.split(' ')[0] expire_date_obj = datetime.strptime(date_part, "%Y-%m-%d") new_expire_date = expire_date_obj + timedelta(days=75) self.db.update_domain_expire_date(domain_id, new_expire_date.strftime("%Y-%m-%d")) except Exception as e: logger.error(f"处理过期日期失败: {e}") self.db.update_domain_expire_date(domain_id, expire_date) logger.info(f"注册状态检测完成: {domain_name}, 状态: {status}, 过期日期: {expire_date}") else: logger.warning(f"注册状态检测失败,不更新数据: {domain_name}") if proxy: self.remove_proxy(proxy) self._mark_detection_failed(domain_id, domain_name, '注册状态检测', '注册状态检测返回未知状态') return False except Exception as e: logger.error(f"注册状态检测失败: {domain_name}, 错误: {e}") if proxy: self.remove_proxy(proxy) self._mark_detection_failed(domain_id, domain_name, '注册状态检测', str(e)) return False return True def _run_detect_wayback(self, domain_id, domain_name, sensitive_words): logger.info(f"检测时光机: {domain_name}") try: wayback_result = self.wayback_detector.scan_snapshots(domain_name, sensitive_words=sensitive_words) snapshot_years = wayback_result.get('snapshot_years') or [] if snapshot_years: self.db.update_domain_snapshot_years(domain_id, ",".join(str(year) for year in snapshot_years)) backlink_count = int(wayback_result.get('backlink_count', 0) or 0) self.db.execute("UPDATE domains SET backlink_count = %s WHERE id = %s", (backlink_count, domain_id)) self._upsert_json_detection( domain_id, backlink_count_gt_10=bool(wayback_result.get('backlink_count_gt_10')), ) if wayback_result.get('has_sensitive_content'): reason = f"时光机快照命中敏感词: {wayback_result.get('matched_word', '')}".strip() self._record_step_result(domain_id, 'wayback_info', ok=False, state="blacklisted", message=reason) self._mark_blacklisted(domain_id, domain_name, reason) return False if int(wayback_result.get('request_error_count', 0) or 0) > 0 and int(wayback_result.get('checked_snapshot_count', 0) or 0) == 0: reason = "时光机检测失败,快照索引请求异常: " + " | ".join( (wayback_result.get('request_errors') or [])[:3] ) self._mark_detection_degraded(domain_id, domain_name, '时光机检测', reason, 'wayback_info') return True if int(wayback_result.get('failed_snapshot_count', 0) or 0) > 0 and int(wayback_result.get('fetched_snapshot_count', 0) or 0) == 0: reason = ( f"时光机检测失败,未成功抓取任何快照;失败次数 {wayback_result.get('failed_snapshot_count', 0)}" ) self._mark_detection_degraded(domain_id, domain_name, '时光机检测', reason, 'wayback_info') return True self._record_step_result( domain_id, 'wayback_info', ok=True, state="passed", message="success", checked_snapshot_count=int(wayback_result.get('checked_snapshot_count', 0) or 0), fetched_snapshot_count=int(wayback_result.get('fetched_snapshot_count', 0) or 0), failed_snapshot_count=int(wayback_result.get('failed_snapshot_count', 0) or 0), backlink_count=int(wayback_result.get('backlink_count', 0) or 0), ) logger.info( "时光机检测完成: %s, 检查快照 %s 个, 成功抓取 %s 个, 失败 %s 个, 最大友链 %s, 耗时 %ss" % ( domain_name, wayback_result.get('checked_snapshot_count', 0), wayback_result.get('fetched_snapshot_count', 0), wayback_result.get('failed_snapshot_count', 0), wayback_result.get('backlink_count', 0), wayback_result.get('elapsed_seconds', 0), ) ) except Exception as e: logger.error(f"时光机检测失败: {domain_name}, 错误: {e}") self._mark_detection_degraded(domain_id, domain_name, '时光机检测', str(e), 'wayback_info') return True return True def _run_detect_chinaz(self, domain_id, domain_name, sensitive_words): logger.info(f"检测站长之家: {domain_name}") proxy = self._get_proxy_for_step(domain_id, domain_name, '站长之家检测') if proxy == '__NO_PROXY__': return False try: success, message, seo_data = chinaz.check_title(domain_name, sensitive_words, proxy) if not success: logger.warning(f"站长之家检测未通过: {domain_name}, 原因: {message}") if self._should_blacklist_result(message): self._record_step_result(domain_id, 'chinaz_info', ok=False, state="blacklisted", message=message) self._mark_blacklisted(domain_id, domain_name, message) return False if self._is_external_dependency_issue(message): self._mark_detection_degraded(domain_id, domain_name, '站长之家检测', message or 'external dependency issue', 'chinaz_info') return True self._mark_detection_failed(domain_id, domain_name, '站长之家检测', message or 'failure', 'chinaz_info') return False self._record_step_result(domain_id, 'chinaz_info', ok=True, state="passed", message=message or "success", seo=seo_data) logger.info(f"站长之家检测完成: {domain_name}") except Exception as e: logger.error(f"站长之家检测失败: {domain_name}, 错误: {e}") if proxy: self.remove_proxy(proxy) if self._is_external_dependency_issue(e): self._mark_detection_degraded(domain_id, domain_name, '站长之家检测', str(e), 'chinaz_info') return True self._mark_detection_failed(domain_id, domain_name, '站长之家检测', str(e), 'chinaz_info') return False return True def _run_detect_aizhan(self, domain_id, domain_name, sensitive_words): logger.info(f"检测爱站网: {domain_name}") proxy = self._get_proxy_for_step(domain_id, domain_name, '爱站网检测') if proxy == '__NO_PROXY__': return False try: success, message = aizhan.check_aizhan(domain_name, sensitive_words, proxy) if not success: logger.warning(f"爱站网检测未通过: {domain_name}, 原因: {message}") if self._should_blacklist_result(message): self._record_step_result(domain_id, 'aizhan_info', ok=False, state="blacklisted", message=message) self._mark_blacklisted(domain_id, domain_name, message) return False if self._is_external_dependency_issue(message): self._mark_detection_degraded(domain_id, domain_name, '爱站网检测', message or 'external dependency issue', 'aizhan_info') return True self._mark_detection_failed(domain_id, domain_name, '爱站网检测', message or 'failure', 'aizhan_info') return False self._record_step_result(domain_id, 'aizhan_info', ok=True, state="passed", message=message or "success") logger.info(f"爱站网检测完成: {domain_name}") except Exception as e: logger.error(f"爱站网检测失败: {domain_name}, 错误: {e}") if proxy: self.remove_proxy(proxy) if self._is_external_dependency_issue(e): self._mark_detection_degraded(domain_id, domain_name, '爱站网检测', str(e), 'aizhan_info') return True self._mark_detection_failed(domain_id, domain_name, '爱站网检测', str(e), 'aizhan_info') return False return True def _run_detect_baidu(self, domain_id, domain_name, sensitive_words): logger.info(f"检测百度: {domain_name}") proxy = self._get_proxy_for_step(domain_id, domain_name, '百度site检测') if proxy == '__NO_PROXY__': return False try: success, message = baidu.check_site(domain_name, sensitive_words, proxy) if not success: logger.warning(f"百度site检测未通过: {domain_name}, 原因: {message}") if proxy and ('timeout' in message.lower() or 'connection' in message.lower()) and self.allow_direct_connection(): logger.info(f"代理检测失败,尝试不使用代理重新检测: {domain_name}") success, message = baidu.check_site(domain_name, sensitive_words, None) if success: logger.info(f"不使用代理检测百度成功: {domain_name}") else: logger.warning(f"不使用代理检测百度仍未通过: {domain_name}, 原因: {message}") if self._should_blacklist_result(message): self._record_step_result(domain_id, 'baidu_site', ok=False, state="blacklisted", message=message) self._mark_blacklisted(domain_id, domain_name, message) elif self._is_external_dependency_issue(message): self._mark_detection_degraded(domain_id, domain_name, '百度site检测', message, 'baidu_site') return True else: self._mark_detection_failed(domain_id, domain_name, '百度site检测', message, 'baidu_site') return False else: if self._should_blacklist_result(message): self._record_step_result(domain_id, 'baidu_site', ok=False, state="blacklisted", message=message) self._mark_blacklisted(domain_id, domain_name, message) elif self._is_external_dependency_issue(message): self._mark_detection_degraded(domain_id, domain_name, '百度site检测', message, 'baidu_site') return True else: self._mark_detection_failed(domain_id, domain_name, '百度site检测', message, 'baidu_site') return False self._record_step_result(domain_id, 'baidu_site', ok=True, state="passed", message=message or "success") logger.info(f"百度site检测完成: {domain_name}") except Exception as e: logger.error(f"百度site检测失败: {domain_name}, 错误: {e}") if proxy: self.remove_proxy(proxy) if self.allow_direct_connection(): try: logger.info(f"尝试不使用代理重新检测百度: {domain_name}") success, message = baidu.check_site(domain_name, sensitive_words, None) if success: logger.info(f"不使用代理检测百度成功: {domain_name}") else: logger.warning(f"不使用代理检测百度仍未通过: {domain_name}, 原因: {message}") if self._should_blacklist_result(message): self._record_step_result(domain_id, 'baidu_site', ok=False, state="blacklisted", message=message) self._mark_blacklisted(domain_id, domain_name, message) elif self._is_external_dependency_issue(message): self._mark_detection_degraded(domain_id, domain_name, '百度site检测', message, 'baidu_site') return True else: self._mark_detection_failed(domain_id, domain_name, '百度site检测', message, 'baidu_site') return False except Exception as e2: logger.error(f"不使用代理检测百度也失败: {domain_name}, 错误: {e2}") if self._is_external_dependency_issue(e2): self._mark_detection_degraded(domain_id, domain_name, '百度site检测', str(e2), 'baidu_site') return True self._mark_detection_failed(domain_id, domain_name, '百度site检测', str(e2), 'baidu_site') return False else: if self._is_external_dependency_issue(e): self._mark_detection_degraded(domain_id, domain_name, '百度site检测', str(e), 'baidu_site') return True self._mark_detection_failed(domain_id, domain_name, '百度site检测', str(e), 'baidu_site') return False return True def _run_detect_360(self, domain_id, domain_name, sensitive_words): logger.info(f"检测360: {domain_name}") proxy = self._get_proxy_for_step(domain_id, domain_name, '360检测') if proxy == '__NO_PROXY__': return False try: passed, message = c360.check_domain(domain_name, sensitive_words, proxy) if not passed: logger.warning(f"360检测未通过: {domain_name}, 原因: {message}") if self._should_blacklist_result(message): self._record_step_result(domain_id, 'qihu360_site', ok=False, state="blacklisted", message=message) self._mark_blacklisted(domain_id, domain_name, message) return False if self._is_external_dependency_issue(message): self._mark_detection_degraded(domain_id, domain_name, '360检测', message or 'external dependency issue', 'qihu360_site') return True self._mark_detection_failed(domain_id, domain_name, '360检测', message or 'failure', 'qihu360_site') return False self._record_step_result(domain_id, 'qihu360_site', ok=True, state="passed", message=message or "success") logger.info(f"360检测完成: {domain_name}") except Exception as e: logger.error(f"360检测失败: {domain_name}, 错误: {e}") if self._is_external_dependency_issue(e): self._mark_detection_degraded(domain_id, domain_name, '360检测', str(e), 'qihu360_site') return True self._mark_detection_failed(domain_id, domain_name, '360检测', str(e), 'qihu360_site') return False return True def _run_detect_jucha(self, domain_id, domain, domain_name): if not self._should_run_jucha(domain): return True self._init_optional_detectors() if self.jc_instance is None: logger.warning("聚查检测器未初始化,跳过本次聚查检测") return True proxy = self._get_proxy_for_step(domain_id, domain_name, '聚查检测') if proxy == '__NO_PROXY__': return False self.jc_instance.session.proxies = proxy or {} logger.info(f"检测聚查WHOIS: {domain_name}") try: success, message, whois_info = self.jc_instance.check_whois_domain(domain_name) if not success: logger.warning(f"聚查WHOIS查询失败: {domain_name}, 原因: {message}") else: logger.info(f"WHOIS查询结果: {success}, {message}, {whois_info}") if 'clientHold' in whois_info or 'serverHold' in whois_info: logger.warning(f"聚查WHOIS检测未通过: {domain_name}, 原因: 域名状态包含clientHold或serverHold") self._mark_blacklisted(domain_id, domain_name, '域名状态包含clientHold或serverHold') return False logger.info(f"聚查WHOIS检测完成: {domain_name}") except Exception as e: logger.error(f"聚查WHOIS检测失败: {domain_name}, 错误: {e}") logger.info(f"检测聚查备案: {domain_name}") try: success, message, beian_info = self.jc_instance.beian_check_domain(domain_name) if not success: logger.warning(f"聚查备案查询失败: {domain_name}, 原因: {message}") else: logger.info(f"备案查询结果: {success}, {message}, {beian_info}") if isinstance(beian_info, tuple) and len(beian_info) == 4: beian_time, company_type, website_url, has_beian = beian_info has_beian_flag = 2 if has_beian == '当前存在' else 3 beian_year = None if beian_time: try: beian_year = beian_time.split('-')[0] except Exception: pass self.db.update_domain_beian_info(domain_id, company_type, website_url, has_beian_flag, beian_year) logger.info(f"聚查备案检测完成: {domain_name}") except Exception as e: logger.error(f"聚查备案检测失败: {domain_name}, 错误: {e}") logger.info(f"检测聚查拦截: {domain_name}") try: success, message, safe_info = self.jc_instance.safe_check_domain(domain_name) if not success: logger.warning(f"聚查安全检测失败: {domain_name}, 原因: {message}") else: logger.info(f"安全检测结果: {success}, {message}, {safe_info}") check_items = ['QQ检测', '微信检测', '抖音检测', '被墙检测', '百度检测', '谷歌检测', '火狐检测'] blacklist_reason = [] if isinstance(safe_info, tuple): for i, item in enumerate(safe_info): if isinstance(item, tuple): if len(item) >= 1 and item[0] == 3 and i < len(check_items): blacklist_reason.append(f"{check_items[i]}: 拦截") elif len(item) >= 2 and item[0] == 2 and '拦截' in item[1] and i < len(check_items): blacklist_reason.append(f"{check_items[i]}: 拦截") if blacklist_reason: reason_str = ";".join(blacklist_reason) logger.warning(f"聚查安全检测未通过: {domain_name}, 原因: {reason_str}") self._mark_blacklisted(domain_id, domain_name, reason_str) return False logger.info(f"聚查拦截检测完成: {domain_name}") except Exception as e: logger.error(f"聚查拦截检测失败: {domain_name}, 错误: {e}") finally: self.db.mark_jucha_detected(domain_id) return True def _run_detect_juziseo(self, domain_id, domain, domain_name, sensitive_words): if not self._should_run_juziseo(domain): return True self._init_optional_detectors() if self.juziseo_instance is None: logger.warning("桔子SEO检测器未初始化,跳过本次桔子检测") return True proxy = self._get_proxy_for_step(domain_id, domain_name, '桔子检测') if proxy == '__NO_PROXY__': return False self.juziseo_instance.session.proxies = proxy or {} logger.info(f"检测桔子历史: {domain_name}") try: success, message = self.juziseo_instance.check_history(domain_name, sensitive_words) if not success: logger.warning(f"桔子历史检测未通过: {domain_name}, 原因: {message}") self._mark_blacklisted(domain_id, domain_name, message) return False logger.info(f"桔子历史检测完成: {domain_name}") except Exception as e: logger.error(f"桔子历史检测失败: {domain_name}, 错误: {e}") logger.info(f"检测桔子外链: {domain_name}") try: success, message = self.juziseo_instance.check_external_link(domain_name, sensitive_words) if not success: logger.warning(f"桔子外链检测未通过: {domain_name}, 原因: {message}") self._mark_blacklisted(domain_id, domain_name, message) return False logger.info(f"桔子外链检测完成: {domain_name}") except Exception as e: logger.error(f"桔子外链检测失败: {domain_name}, 错误: {e}") finally: self.db.mark_juziseo_detected(domain_id) return True def _upsert_json_detection(self, domain_id, **payload): existing = self.db.fetch_one("SELECT id FROM domain_detections WHERE domain_id = %s", (domain_id,)) columns = [] params = [] for key, value in payload.items(): columns.append(key) params.append(json.dumps(value) if isinstance(value, (dict, list, tuple)) else value) if not columns: return True if existing: assignments = ", ".join([f"{column} = %s" for column in columns] + ["update_time = CURRENT_TIMESTAMP"]) params.append(domain_id) sql = f"UPDATE domain_detections SET {assignments} WHERE domain_id = %s" else: sql = f"INSERT INTO domain_detections (domain_id, {', '.join(columns)}) VALUES (%s, {', '.join(['%s'] * len(columns))})" params = [domain_id] + params return self.db.execute(sql, tuple(params)) def detect_domain(self, domain_id, domain, job_context=None): """ 检测单个域名 :param domain_id: 域名ID :param domain: 域名对象(包含id、domain、source_type等字段) """ import threading thread_id = threading.current_thread().ident # 从域名对象中获取域名字符串 domain_name = domain.get('domain', '') logger.info(f"线程 {thread_id} 开始检测域名: {domain_name}") job_context = job_context or {} job_item_id = job_context.get('job_item_id') job_id = job_context.get('job_id') claim_token = job_context.get('claim_token') cycle_token = str(job_context.get('cycle_token') or self.current_cycle_token or '').strip() job_code = str(job_context.get('job_code') or self.current_job_code or '').strip() def finalize_job_item(final_status, message=""): if not job_item_id or not claim_token: return try: if final_status in {'completed', 'blacklisted'}: self.db.complete_detect_job_item(job_item_id, claim_token, final_status) else: self.db.fail_detect_job_item(job_item_id, claim_token, message or final_status) self.db.append_detect_run_event( job_id, job_item_id, config.NODE_CODE, event_type=f"domain_{final_status}", message=message or f"{domain_name} -> {final_status}", level='error' if final_status == 'failed' else 'info', payload={ "domain_id": domain_id, "domain": domain_name, "status": final_status, "cycle_token": cycle_token, "job_code": job_code, }, ) except Exception as finalize_error: logger.warning(f"回写任务项状态失败: {domain_name}, 错误: {finalize_error}") def renew_job_item_lease(detail=""): if not job_item_id or not claim_token: return try: self.db.renew_detect_job_item_lease( job_item_id, claim_token, lease_seconds=max(1800, self.thread_count * 600), ) except Exception as renew_error: logger.warning(f"续租任务项失败: {domain_name}, 错误: {renew_error}") try: # 检查是否需要停止 if not self.running: logger.info(f"检测已停止,跳过域名: {domain_name}") finalize_job_item('failed', 'Worker 已停止,任务未执行') return logger.info(f"开始检测域名: {domain_name}") self.db.update_domain_detect_status(domain_id, DETECT_STATUS_RUNNING) if job_item_id and claim_token: self.db.mark_detect_job_item_running(job_item_id, claim_token) renew_job_item_lease("domain_started") self.db.append_detect_run_event( job_id, job_item_id, config.NODE_CODE, event_type='domain_started', message=f"开始检测域名: {domain_name}", payload={ "domain_id": domain_id, "domain": domain_name, "cycle_token": cycle_token, "job_code": job_code, }, ) # 使用共享的敏感词列表 sensitive_words = self.sensitive_words logger.debug(f"线程 {thread_id} 使用共享敏感词,共 {len(sensitive_words)} 个敏感词") for detect_key in self._get_detect_execution_order(): renew_job_item_lease(detect_key) if detect_key == 'detect_register': if not self._run_detect_register(domain_id, domain, domain_name): domain_snapshot = self.db.get_domain_by_id(domain_id) or {} finalize_job_item('blacklisted' if int(domain_snapshot.get('detect_status', 0) or 0) == DETECT_STATUS_BLACKLISTED else 'failed', '注册状态检测未通过') return elif detect_key == 'detect_baidu_site': if not self._run_detect_baidu(domain_id, domain_name, sensitive_words): domain_snapshot = self.db.get_domain_by_id(domain_id) or {} finalize_job_item('blacklisted' if int(domain_snapshot.get('detect_status', 0) or 0) == DETECT_STATUS_BLACKLISTED else 'failed', '百度检测未通过') return elif detect_key == 'detect_360_site': if not self._run_detect_360(domain_id, domain_name, sensitive_words): domain_snapshot = self.db.get_domain_by_id(domain_id) or {} finalize_job_item('blacklisted' if int(domain_snapshot.get('detect_status', 0) or 0) == DETECT_STATUS_BLACKLISTED else 'failed', '360检测未通过') return elif detect_key == 'detect_chinaz': if not self._run_detect_chinaz(domain_id, domain_name, sensitive_words): domain_snapshot = self.db.get_domain_by_id(domain_id) or {} finalize_job_item('blacklisted' if int(domain_snapshot.get('detect_status', 0) or 0) == DETECT_STATUS_BLACKLISTED else 'failed', '站长之家检测未通过') return elif detect_key == 'detect_aizhan': if not self._run_detect_aizhan(domain_id, domain_name, sensitive_words): domain_snapshot = self.db.get_domain_by_id(domain_id) or {} finalize_job_item('blacklisted' if int(domain_snapshot.get('detect_status', 0) or 0) == DETECT_STATUS_BLACKLISTED else 'failed', '爱站检测未通过') return elif detect_key == 'detect_wayback': if not self._run_detect_wayback(domain_id, domain_name, sensitive_words): domain_snapshot = self.db.get_domain_by_id(domain_id) or {} finalize_job_item('blacklisted' if int(domain_snapshot.get('detect_status', 0) or 0) == DETECT_STATUS_BLACKLISTED else 'failed', '时光机检测未通过') return elif detect_key == 'detect_jucha': if not self._run_detect_jucha(domain_id, domain, domain_name): domain_snapshot = self.db.get_domain_by_id(domain_id) or {} finalize_job_item('blacklisted' if int(domain_snapshot.get('detect_status', 0) or 0) == DETECT_STATUS_BLACKLISTED else 'failed', '聚查检测未通过') return elif detect_key == 'detect_juziseo': if not self._run_detect_juziseo(domain_id, domain, domain_name, sensitive_words): domain_snapshot = self.db.get_domain_by_id(domain_id) or {} finalize_job_item('blacklisted' if int(domain_snapshot.get('detect_status', 0) or 0) == DETECT_STATUS_BLACKLISTED else 'failed', '桔子检测未通过') return self._complete_detection(domain_id, domain_name) finalize_job_item('completed', f"域名检测完成: {domain_name}") logger.info(f"域名检测完成: {domain_name}") except Exception as e: logger.error(f"检测域名出错: {domain_name}, 错误: {e}") self.db.update_domain_detect_status(domain_id, DETECT_STATUS_FAILED) finalize_job_item('failed', str(e)) def start_detection(self): """ 开始检测 """ logger.info("开始执行域名检测任务") self.detecting = True self.stop_requested = False self._mark_detection_phase("preparing", "开始执行域名检测任务,正在加载配置") # 重新加载配置,确保获取最新的配置 try: self.detect_options = self.load_detect_options() self.proxy_config = self.load_proxy_config() self.thread_count = self.load_thread_count() self.proxy_max_reuse_count = max(8, self.thread_count * 4) logger.info(f"检测线程数设置为: {self.thread_count}") self.load_cookies_from_remote() self._init_optional_detectors() recycled = self.db.recycle_running_domains(DETECT_STATUS_FAILED) if recycled: logger.warning(f"检测启动前回收了 {recycled} 个遗留的检测中任务,已标记为检测失败待重试") released_job_items = self.db.release_detect_job_items_for_node(config.NODE_CODE) if released_job_items: logger.warning(f"检测启动前释放了 {released_job_items} 个当前节点遗留任务项,已重新回到 pending") except Exception as e: logger.error(f"加载配置失败: {e}") import traceback logger.error(traceback.format_exc()) self._mark_detection_phase("failed", f"加载配置失败: {e}") return # 重新加载cookies self.load_cookies_from_remote() # 重新加载JC实例的cookies if self.detect_options.get('detect_jucha') and self.jc_instance is not None: self.jc_instance.load_cookies() self.jc_instance.load_juming_cookies() # 重新加载Juziseo实例的cookies if self.detect_options.get('detect_juziseo') and self.juziseo_instance is not None: self.juziseo_instance.load_cookies() # 刷新代理池 if self.proxy_config.get('proxy_enable', False): logger.info("开始检测,刷新代理池") self._mark_detection_phase("refreshing_proxy", "开始检测,正在刷新代理池") self.refresh_proxy_pool() # 更新JC和Juziseo实例的代理设置 if self.detect_options.get('detect_jucha') and self.jc_instance is not None: self.jc_instance.proxies = self.get_proxies() if self.detect_options.get('detect_juziseo') and self.juziseo_instance is not None: self.juziseo_instance.proxies = self.get_proxies() # 重新加载敏感词,确保获取最新的敏感词列表 try: self.sensitive_words = self.db.get_all_sensitive_words() logger.info(f"重新加载敏感词完成,共 {len(self.sensitive_words)} 个敏感词") except Exception as e: logger.error(f"重新加载敏感词失败: {e}") import traceback logger.error(traceback.format_exc()) self.sensitive_words = [] # 更新配置标签 self.update_config_labels() try: batch_size = 1000 total_processed = 0 claim_batch_size = min(batch_size, max(50, self.thread_count * 8)) while self.running: if self.stop_requested: logger.info("检测任务收到停止请求,停止继续领取任务") self._mark_detection_phase("stopping", "检测任务收到停止请求,准备安全退出") break recycled_job_items = self.db.recycle_expired_detect_job_items() if recycled_job_items: logger.warning(f"已回收 {recycled_job_items} 个租约过期的任务项,重新回到 pending") # 获取需要检测的域名 domains = self.db.claim_detect_job_items( config.NODE_CODE, limit=claim_batch_size, lease_seconds=max(1800, self.thread_count * 600), ) using_job_queue = bool(domains) if not domains: domains = self.db.get_domains_to_detect(limit=batch_size, detect_options=self.detect_options) current_batch_size = len(domains) queue_label = "任务队列" if using_job_queue else "兼容旧链路" logger.info(f"从{queue_label}获取到 {current_batch_size} 个需要检测的域名") self._mark_detection_phase( "fetching", f"从{queue_label}获取到 {current_batch_size} 个需要检测的域名", batch_size=current_batch_size, queue_source="detect_job_items" if using_job_queue else "domains", ) if not domains: logger.info("没有需要检测的域名") self._mark_detection_phase("idle", "当前没有需要检测的域名,Worker 等待下一次启动") break # 旧链路直接扫 domains 表时,少于 batch_size 代表已接近尾批; # 任务队列模式会按小批量持续领取,不能把“小批量正常领取”误判为整轮结束。 should_stop = (not using_job_queue) and current_batch_size < batch_size # 创建线程池,限制同时运行的线程数量 active_threads = [] max_threads = self.thread_count try: logger.info(f"开始创建线程,当前批次域名数: {current_batch_size},最大线程数: {max_threads}") self._mark_detection_phase( "creating_threads", f"开始创建线程,当前批次域名数: {current_batch_size},最大线程数: {max_threads}", batch_size=current_batch_size, max_threads=max_threads, ) for i, domain in enumerate(domains): # 检查是否需要停止 if not self.running or self.stop_requested: logger.info("检测已停止,停止创建新线程") self._mark_detection_phase("stopping", "检测已停止,停止创建新线程") break # 等待线程数量降到最大值以下 while len([t for t in active_threads if t.is_alive()]) >= max_threads: # 清理已完成的线程 active_threads = [t for t in active_threads if t.is_alive()] # 短暂休眠,避免CPU占用过高 import time time.sleep(0.1) domain_id = domain['id'] domain_name = domain['domain'] # 更新进度 if self.detect_thread: try: # 通过信号发送进度更新 self.detect_thread.progress_signal.emit(total_processed + i, total_processed + current_batch_size) except Exception as e: logger.error(f"发送进度更新信号失败: {e}") import traceback logger.error(traceback.format_exc()) # 创建线程 try: job_context = { 'job_item_id': domain.get('job_item_id'), 'job_id': domain.get('job_id'), 'claim_token': domain.get('claim_token'), 'cycle_token': self.current_cycle_token, 'job_code': self.current_job_code, } if using_job_queue else None thread = threading.Thread(target=self.detect_domain, args=(domain_id, domain, job_context)) active_threads.append(thread) logger.debug(f"创建线程 {i+1} 成功,域名: {domain_name}") except Exception as e: logger.error(f"创建线程失败: {e}") import traceback logger.error(traceback.format_exc()) continue # 启动线程 try: thread.start() logger.debug(f"启动线程 {i+1} 成功,域名: {domain_name}") except Exception as e: logger.error(f"启动线程失败: {e}") import traceback logger.error(traceback.format_exc()) active_threads.remove(thread) continue # 显示当前实际线程数量 current_active = len([t for t in active_threads if t.is_alive()]) logger.info(f"当前实际线程数量: {current_active}/{max_threads}") self._mark_detection_phase( "running", f"当前实际线程数量: {current_active}/{max_threads}", active_threads=current_active, max_threads=max_threads, batch_size=current_batch_size, ) # 更新GUI显示 if self.detect_thread: try: # 通过信号发送线程数量更新 self.detect_thread.thread_count_signal.emit(current_active, max_threads) except Exception as e: logger.error(f"发送线程数量更新信号失败: {e}") import traceback logger.error(traceback.format_exc()) except Exception as e: logger.error(f"创建或启动线程失败: {e}") import traceback logger.error(traceback.format_exc()) # 等待所有线程完成 logger.info(f"等待剩余 {len(active_threads)} 个线程完成") for i, thread in enumerate(active_threads): # 检查是否需要停止 if not self.running or self.stop_requested: logger.info("检测已停止,停止等待线程完成") self._mark_detection_phase("stopping", "检测已停止,停止等待线程完成") break try: thread.join(timeout=30) # 添加超时,避免线程阻塞 current_active = len([t for t in active_threads if t.is_alive()]) self._mark_detection_phase( "running", f"当前实际线程数量: {current_active}/{max_threads}", active_threads=current_active, max_threads=max_threads, batch_size=current_batch_size, processed=total_processed + i, ) # 更新进度 if self.detect_thread: try: # 通过信号发送进度更新 self.detect_thread.progress_signal.emit(total_processed + i + 1, total_processed + current_batch_size) except Exception as e: logger.error(f"发送进度更新信号失败: {e}") import traceback logger.error(traceback.format_exc()) except Exception as e: logger.error(f"等待线程完成失败: {e}") import traceback logger.error(traceback.format_exc()) # 发送线程数量更新信号 if self.detect_thread: try: active_count = threading.active_count() self.detect_thread.thread_count_signal.emit(active_count, max_threads) except Exception as e: logger.error(f"发送线程数量更新信号失败: {e}") import traceback logger.error(traceback.format_exc()) # 完成当前批次进度 if self.detect_thread: try: # 通过信号发送进度更新 self.detect_thread.progress_signal.emit(total_processed + current_batch_size, total_processed + current_batch_size) except Exception as e: logger.error(f"发送进度更新信号失败: {e}") import traceback logger.error(traceback.format_exc()) total_processed += current_batch_size # 如果当前批次数量少于batch_size,检测完成后停止获取域名 if should_stop: logger.info(f"当前批次域名数量 ({current_batch_size}) 少于 {batch_size},停止获取域名") self._mark_detection_phase( "completing", f"当前批次域名数量 ({current_batch_size}) 少于 {batch_size},本轮检测即将完成", processed=total_processed, ) break logger.info(f"当前批次检测完成,累计处理 {total_processed} 个域名") self._mark_detection_phase("batch_completed", f"当前批次检测完成,累计处理 {total_processed} 个域名", processed=total_processed) # 完成进度 if self.detect_thread: try: # 通过信号发送进度更新 self.detect_thread.progress_signal.emit(100, 100) except Exception as e: logger.error(f"发送进度更新信号失败: {e}") import traceback logger.error(traceback.format_exc()) logger.info("域名检测任务完成") self._mark_detection_phase("completed", "域名检测任务完成") except Exception as e: logger.error(f"执行检测任务出错: {e}") self._mark_detection_phase("failed", f"执行检测任务出错: {e}") finally: self.detecting = False def run_daily_task(self): """ 执行每日检测任务 """ logger.info(f"[{datetime.now()}] 开始每日检测任务") self.start_detection() logger.info(f"[{datetime.now()}] 每日检测任务完成") def start_scheduler(self): """ 启动定时任务 """ # 避免重复注册相同定时任务 schedule.clear('daily_detection') # 每天凌晨1点执行检测;启动程序时不自动跑一轮,避免和人工启动混淆 schedule.every().day.at("01:00").do(self.run_daily_task).tag('daily_detection') logger.info("定时任务已启动,每天凌晨1点执行检测;程序启动后不会立即自动执行") # 循环执行定时任务 while self.running: schedule.run_pending() time.sleep(60) def start(self): """ 启动检测端 """ logger.info("启动域名检测端") self.running = True # 启动定时任务 scheduler_thread = threading.Thread(target=self.start_scheduler) scheduler_thread.daemon = True scheduler_thread.start() # 保持程序运行 try: while self.running: time.sleep(1) except KeyboardInterrupt: logger.info("检测端已停止") self.running = False def load_cookies_from_remote(self): """ 从远程加载cookies """ try: if self.use_redis: # 加载聚名cookies juming_cookies_str = self.redis_client.get('domain_tool:juming_cookies') if juming_cookies_str: # 将cookies保存到本地文件 import pickle from requests.cookies import RequestsCookieJar # 创建cookie jar cookie_jar = RequestsCookieJar() # 解析cookies字符串 try: import ast cookies_dict = ast.literal_eval(juming_cookies_str) for name, value in cookies_dict.items(): cookie_jar.set(name, value) except Exception as e: logger.error(f"解析聚名cookies失败: {e}") # 保存到本地文件 try: with open('juming_cookies.pkl', 'wb') as f: pickle.dump(cookie_jar, f) logger.info("从Redis加载聚名cookies成功并保存到本地") except Exception as e: logger.error(f"保存聚名cookies到本地失败: {e}") # 加载聚查cookies jucha_cookies_str = self.redis_client.get('domain_tool:jucha_cookies') if jucha_cookies_str: # 将cookies保存到本地文件 import pickle from requests.cookies import RequestsCookieJar # 创建cookie jar cookie_jar = RequestsCookieJar() # 解析cookies字符串 try: import ast cookies_dict = ast.literal_eval(jucha_cookies_str) for name, value in cookies_dict.items(): cookie_jar.set(name, value) except Exception as e: logger.error(f"解析聚查cookies失败: {e}") # 保存到本地文件 try: with open('jucha_cookies.pkl', 'wb') as f: pickle.dump(cookie_jar, f) logger.info("从Redis加载聚查cookies成功并保存到本地") except Exception as e: logger.error(f"保存聚查cookies到本地失败: {e}") # 加载桔子SEO cookies juziseo_cookies_str = self.redis_client.get('domain_tool:juziseo_cookies') if juziseo_cookies_str: # 将cookies保存到本地文件 import pickle from requests.cookies import RequestsCookieJar # 创建cookie jar cookie_jar = RequestsCookieJar() # 解析cookies字符串 try: import ast cookies_dict = ast.literal_eval(juziseo_cookies_str) for name, value in cookies_dict.items(): cookie_jar.set(name, value) except Exception as e: logger.error(f"解析桔子SEO cookies失败: {e}") # 保存到本地文件 try: with open('juziseo_cookies.pkl', 'wb') as f: pickle.dump(cookie_jar, f) logger.info("从Redis加载桔子SEO cookies成功并保存到本地") except Exception as e: logger.error(f"保存桔子SEO cookies到本地失败: {e}") except Exception as e: logger.error(f"从远程加载cookies失败: {e}") def update_config_labels(self): """ 更新GUI配置标签 """ # 使用QCoreApplication.postEvent发送配置更新事件,这是最安全的方法 try: logger.debug("发送配置更新事件") # 确保主窗口存在 if self.main_window: from PySide6.QtCore import QCoreApplication event = ConfigUpdateEvent() QCoreApplication.postEvent(self.main_window, event) logger.debug("配置更新事件已发送") else: logger.debug("主窗口不存在,跳过配置更新") except Exception as e: logger.error(f"发送配置更新事件失败: {e}") def start_redis_subscription(self): """ 启动Redis订阅,监听配置更新 """ while self.running: pubsub = None try: # 创建新的Redis客户端用于订阅 redis_sub_client = redis.Redis( host=config.REDIS_HOST, port=config.REDIS_PORT, password=config.REDIS_PASSWORD, db=config.REDIS_DB, decode_responses=True, socket_connect_timeout=30, # 增加连接超时时间 socket_timeout=60, # 增加读取超时时间 retry_on_timeout=True, health_check_interval=30 ) # 订阅配置更新频道 pubsub = redis_sub_client.pubsub() pubsub.subscribe(CONFIG_UPDATE_CHANNEL, CONTROL_CHANNEL) # 使用logger.debug输出到文件日志,不输出到GUI日志框 logger.debug("开始监听配置更新...") self._consume_pending_control_command() # 循环监听消息 while self.running: message = pubsub.get_message(ignore_subscribe_messages=True, timeout=1.0) if not message: continue if message['type'] == 'message': channel = message.get('channel', '') payload = message['data'] if channel == CONFIG_UPDATE_CHANNEL: config_type = payload logger.debug(f"收到配置更新消息: {config_type}") self.detect_options = self.load_detect_options() self.proxy_config = self.load_proxy_config() self.thread_count = self.load_thread_count() self.load_cookies_from_remote() self.update_config_labels() logger.debug("配置已更新") continue if channel == CONTROL_CHANNEL: logger.info(f"收到 Worker 控制消息: {payload}") self._handle_control_message(payload) except redis.exceptions.TimeoutError: # Pub/Sub 空闲超时在 Linux 服务态下属于常见现象,不需要中断订阅。 continue except Exception as e: logger.debug(f"Redis订阅失败: {e}") logger.debug("3秒后尝试重新连接...") time.sleep(3) # 增加重试间隔 finally: if pubsub is not None: try: pubsub.close() except Exception: pass def stop(self): """ 停止检测端 """ logger.info("停止域名检测端") self._runtime_heartbeat_stop.set() self.stop_requested = True self.running = False self.detecting = False self._update_runtime_state("stopped", "Worker 已停止") if __name__ == "__main__": try: # 配置日志 logger.add("detect_worker.log", rotation="1 day", level="DEBUG") logger.info("程序开始运行") # 创建应用程序 app = QApplication([]) logger.info("创建应用程序成功") # 创建并显示主窗口 window = DetectMainWindow() logger.info("创建主窗口成功") window.show() logger.info("显示主窗口成功") # 运行应用程序 logger.info("开始运行应用程序") app.exec() logger.info("应用程序运行结束") # 当窗口关闭时,停止检测端 if window.worker: logger.info("停止检测端") window.worker.stop() except Exception as e: logger.error(f"程序运行出错: {e}") import traceback logger.error(traceback.format_exc()) import time time.sleep(10) # 等待10秒,以便查看错误信息