"""bridge 配置与全局状态。 集中管理: - DIAGNOSTIC_ITEMS:诊断项定义 - InitState:DB 初始化后台任务状态 - BridgeConfig:命令行参数 + 环境变量配置 - AppState:运行期共享状态容器(单例 _state) - _require_* 辅助函数:依赖检查(未初始化时抛 BridgeError) """ from __future__ import annotations import argparse import asyncio import logging import os import threading from dataclasses import dataclass, field from typing import Any, Callable, Optional from woc_bridge.db import DbReader, KeyCache from woc_bridge.messaging import MessageStreamer, SendQueue from woc_bridge.models import BridgeError from woc_bridge.ui import QrCapture, XdotoolDriver # --------------------------------------------------------------------------- # 诊断项定义(spec 11.1 节) # --------------------------------------------------------------------------- DIAGNOSTIC_ITEMS = [ { "check_id": "wechat_running", "name": "微信进程检查", "severity": "critical", "description": "检查微信客户端是否运行", "auto_repairable": True, }, { "check_id": "login_state", "name": "登录状态检查", "severity": "critical", "description": "检查微信是否已登录", "auto_repairable": False, }, { "check_id": "db_accessible", "name": "数据库可达性", "severity": "error", "description": "检查微信 DB 是否可读", "auto_repairable": True, # 可通过自动提取 key 修复 }, { "check_id": "xdotool_available", "name": "xdotool 可用性", "severity": "error", "description": "检查 xdotool 是否可用", "auto_repairable": True, }, ] # 诊断项 check_id → 定义映射,便于查找 _DIAGNOSTIC_ITEM_BY_ID = {item["check_id"]: item for item in DIAGNOSTIC_ITEMS} # --------------------------------------------------------------------------- # DB 初始化后台任务状态 # --------------------------------------------------------------------------- class InitState: """DB 初始化(密钥提取)后台任务状态。""" def __init__(self) -> None: self.state: str = "idle" # idle / running / success / failed self.progress_pct: Optional[float] = None self.message: Optional[str] = None self.key_count: Optional[int] = None self.error: Optional[str] = None self._thread: Optional[threading.Thread] = None def start(self, target: Callable, args: tuple = ()) -> None: """启动后台初始化线程。 若前一个线程仍存活,拒绝重复启动(调用方应先检查 state == running)。 """ if self._thread is not None and self._thread.is_alive(): return self.state = "running" self.progress_pct = 0.0 self.message = "开始初始化" self.error = None self._thread = threading.Thread(target=target, args=args, daemon=True) self._thread.start() def set_progress(self, pct: float, message: str) -> None: self.progress_pct = pct self.message = message def set_success(self, key_count: int, message: str) -> None: self.state = "success" self.progress_pct = 100.0 self.key_count = key_count self.message = message self.error = None def set_failed(self, error: str) -> None: self.state = "failed" self.progress_pct = None self.error = error self.message = f"初始化失败: {error}" # --------------------------------------------------------------------------- # BridgeConfig # --------------------------------------------------------------------------- def _parse_list(s: str) -> list[str]: """逗号分隔字符串转列表(供 BridgeConfig 解析环境变量用)。""" if not s.strip(): return [] return [item.strip() for item in s.split(",") if item.strip()] @dataclass class BridgeConfig: """bridge 服务的配置项(来自命令行参数 + 环境变量)。 在启动时一次性构造并注入 AppState,运行期只读不写。 所有 os.environ.get / 类型转换集中在此处,避免散落。 """ # 命令行参数 listen: str = "0.0.0.0:8088" wechat_db: str = "/config" display: str = ":1" # 环境变量 send_delay_ms: int = 3000 max_calls_per_sec: int = 10 max_queue_size: int = 100 send_queue_wait_timeout_ms: int = 15000 max_batch_size: int = 50 poll_interval_ms: int = 2000 db_key: str = "" auto_extract_enabled: bool = True # UI 自动化后端选择:flow(新六层架构,默认)/ legacy(旧 xdotool_driver 单文件) ui_backend: str = "flow" # 调试截图写盘开关:生产环境必须 false,调试模式 true 时异步队列后台写盘 ui_debug_shots: bool = False # 好友自动通过配置 auto_accept_enabled: bool = False auto_accept_all: bool = False auto_accept_whitelist_wxids: list[str] = field(default_factory=list) auto_accept_whitelist_nicknames: list[str] = field(default_factory=list) auto_accept_keywords: list[str] = field(default_factory=list) auto_accept_blacklist_wxids: list[str] = field(default_factory=list) auto_accept_blacklist_nicknames: list[str] = field(default_factory=list) auto_accept_allow_scenes: list[str] = field(default_factory=list) auto_accept_poll_interval: float = 3.0 # 聊天记录导出(Task 17-21) # 导出文件根目录,每个任务建 / 子目录 export_dir: str = "/config/woc-export" # 导出任务 TTL(秒),过期后下载链接失效并被懒清理 export_task_ttl_sec: int = 7200 # 群发消息风控(Task 13-16) batch_max_per_batch: int = 200 # 单批目标数上限(去重后) batch_daily_limit: int = 3 # 每日群发批次数上限 batch_min_interval_sec: int = 7200 # 相邻两批群发最小间隔(秒,默认 2 小时) @classmethod def from_args_and_env(cls, args: argparse.Namespace) -> "BridgeConfig": """从 argparse 结果 + 环境变量一次性构造配置。""" return cls( listen=args.listen, wechat_db=args.wechat_db, display=args.display, send_delay_ms=int(os.environ.get("WOC_BRIDGE_SEND_DELAY_MS", "3000")), max_calls_per_sec=int(os.environ.get("WOC_BRIDGE_MAX_CALLS_PER_SEC", "10")), max_queue_size=int(os.environ.get("WOC_BRIDGE_MAX_QUEUE_SIZE", "100")), send_queue_wait_timeout_ms=int(os.environ.get("WOC_BRIDGE_SEND_QUEUE_WAIT_TIMEOUT_MS", "15000")), max_batch_size=int(os.environ.get("WOC_BRIDGE_MAX_BATCH_SIZE", "50")), poll_interval_ms=int(os.environ.get("WOC_BRIDGE_POLL_INTERVAL_MS", "2000")), db_key=os.environ.get("WOC_DB_KEY", "").strip(), auto_extract_enabled=os.environ.get( "WOC_DB_KEY_AUTO_EXTRACT", "true" ).lower() not in ("false", "0", "no", "off"), ui_backend=os.environ.get("WOC_UI_BACKEND", "flow").lower(), ui_debug_shots=os.environ.get("WOC_UI_DEBUG_SHOTS", "false").lower() in ("true", "1", "yes", "on"), auto_accept_enabled=os.environ.get( "WOC_AUTO_ACCEPT_ENABLED", "false" ).lower() in ("true", "1", "yes", "on"), auto_accept_all=os.environ.get( "WOC_AUTO_ACCEPT_ALL", "false" ).lower() in ("true", "1", "yes", "on"), auto_accept_whitelist_wxids=_parse_list( os.environ.get("WOC_AUTO_ACCEPT_WHITELIST_WXIDS", "") ), auto_accept_whitelist_nicknames=_parse_list( os.environ.get("WOC_AUTO_ACCEPT_WHITELIST_NICKNAMES", "") ), auto_accept_keywords=_parse_list( os.environ.get("WOC_AUTO_ACCEPT_KEYWORDS", "") ), auto_accept_blacklist_wxids=_parse_list( os.environ.get("WOC_AUTO_ACCEPT_BLACKLIST_WXIDS", "") ), auto_accept_blacklist_nicknames=_parse_list( os.environ.get("WOC_AUTO_ACCEPT_BLACKLIST_NICKNAMES", "") ), auto_accept_allow_scenes=_parse_list( os.environ.get("WOC_AUTO_ACCEPT_ALLOW_SCENES", "") ), auto_accept_poll_interval=float( os.environ.get("WOC_AUTO_ACCEPT_POLL_INTERVAL", "3") ), # 聊天记录导出(Task 17-21) export_dir=os.environ.get("WOC_EXPORT_DIR", "/config/woc-export").strip(), export_task_ttl_sec=int( os.environ.get("WOC_EXPORT_TASK_TTL_SEC", "7200") ), # 群发消息风控(Task 13-16) batch_max_per_batch=int( os.environ.get("WOC_BATCH_MAX_PER_BATCH", "200") ), batch_daily_limit=int( os.environ.get("WOC_BATCH_DAILY_LIMIT", "3") ), batch_min_interval_sec=int( os.environ.get("WOC_BATCH_MIN_INTERVAL_SEC", "7200") ), ) # --------------------------------------------------------------------------- # AppState # --------------------------------------------------------------------------- class AppState: """运行期共享状态容器。""" def __init__(self) -> None: self.config: BridgeConfig = BridgeConfig() self.xdotool: XdotoolDriver | None = None self.db_reader: DbReader | None = None self.send_queue: SendQueue | None = None self.qr_capture: QrCapture | None = None self.message_streamer: MessageStreamer | None = None self.start_time: float = 0.0 # DB 解密密钥缓存(env / api / auto_extract / file 注入) self.key_cache = KeyCache() # DB 初始化后台任务状态 self.init_state = InitState() # 自动提取异步锁:防止并发请求同时触发内存扫描 # asyncio.Lock 需在事件循环中创建,延迟到 lifespan 首次使用时初始化 self._extract_lock: asyncio.Lock | None = None # P3 新增:UI 自动化新架构组件(仅 ui_backend != "legacy" 时构造) # 使用 Optional[Any] 避免循环 import(不直接 import UI 模块类型) self.capabilities: Optional[Any] = None # WeChatCapabilities self.orchestrator: Optional[Any] = None # FlowOrchestrator self.watchdog: Optional[Any] = None # WeChatWatchdog self.resource_reaper: Optional[Any] = None # ResourceReaper self.actions: Optional[Any] = None # Actions self.locator: Optional[Any] = None # LocatorRegistry self.opencv_backend: Optional[Any] = None # OpenCVBackend self.xdotool_backend: Optional[Any] = None # XdotoolBackend self.idem_cache: Optional[Any] = None # IdemCache # 好友自动通过组件 self.friend_watcher: Optional[Any] = None # FriendRequestWatcher self.rule_engine: Optional[Any] = None # AcceptRuleEngine self.accept_breaker: Optional[Any] = None # CircuitBreaker # 登录状态守卫(后台检测微信登录状态,供后台任务做登录前提保护) self.login_guard: Optional[Any] = None # LoginGuard # 群发消息后台协程(Task 13-16) self.batch_worker: Optional[Any] = None # BatchSendWorker def extract_lock(self) -> asyncio.Lock: """懒初始化 asyncio.Lock(需在事件循环中创建)。""" if self._extract_lock is None: self._extract_lock = asyncio.Lock() return self._extract_lock _state = AppState() # --------------------------------------------------------------------------- # 依赖检查辅助函数(替代 assert,兼容 python -O) # --------------------------------------------------------------------------- def _require_xdotool() -> XdotoolDriver: """返回 xdotool 驱动,未初始化时抛 BridgeError。""" if _state.xdotool is None: raise BridgeError( code="BRIDGE_INTERNAL_ERROR", message="xdotool driver 未初始化", ) return _state.xdotool def _require_db_reader() -> DbReader: """返回 DB 读取器,未初始化时抛 BridgeError。""" if _state.db_reader is None: raise BridgeError( code="BRIDGE_INTERNAL_ERROR", message="db_reader 未初始化", ) return _state.db_reader def _require_send_queue() -> SendQueue: """返回发送队列,未初始化时抛 BridgeError。""" if _state.send_queue is None: raise BridgeError( code="BRIDGE_INTERNAL_ERROR", message="send_queue 未初始化", ) return _state.send_queue def _require_qr_capture() -> QrCapture: """返回二维码截图器,未初始化时抛 BridgeError。""" if _state.qr_capture is None: raise BridgeError( code="BRIDGE_INTERNAL_ERROR", message="qr_capture 未初始化", ) return _state.qr_capture def _require_message_streamer() -> MessageStreamer: """返回消息流推送器,未初始化时抛 BridgeError。""" if _state.message_streamer is None: raise BridgeError( code="BRIDGE_INTERNAL_ERROR", message="message_streamer 未初始化", ) return _state.message_streamer def _require_rule_engine(): """返回好友自动通过规则引擎,未初始化时抛 BridgeError。""" if _state.rule_engine is None: raise BridgeError( code="BRIDGE_INTERNAL_ERROR", message="rule_engine 未初始化", ) return _state.rule_engine def _require_friend_watcher(): """返回好友申请监听器,未初始化时抛 BridgeError。""" if _state.friend_watcher is None: raise BridgeError( code="BRIDGE_INTERNAL_ERROR", message="friend_watcher 未初始化", ) return _state.friend_watcher def _require_batch_worker(): """返回群发消息后台协程,未初始化时抛 BridgeError。""" if _state.batch_worker is None: raise BridgeError( code="BRIDGE_INTERNAL_ERROR", message="batch_worker 未初始化(群发功能不可用)", ) return _state.batch_worker