WechatOnCloud/bridge/woc_bridge/config.py
Kris 31953eedf4 feat: 新增好友申请自动通过功能
- 新增好友申请XML解析器,支持解析微信4.x Linux的fmessage系统消息
- 新增好友申请自动通过规则引擎,支持黑白名单、关键词、场景过滤
- 新增xdotool驱动的好友申请UI自动化流程
- 新增后台好友申请监听器,支持增量轮询与状态统计
- 新增配套API接口,支持查询申请列表、手动/自动通过、配置管理
- 新增多分辨率UI定位配置,适配1280x720和1920x1080分辨率
- 新增配置项支持自动通过功能的开关与规则配置
2026-07-16 13:03:58 +08:00

321 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""bridge 配置与全局状态。
集中管理:
- DIAGNOSTIC_ITEMS诊断项定义
- InitStateDB 初始化后台任务状态
- 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_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
@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_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")
),
)
# ---------------------------------------------------------------------------
# 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
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