WechatOnCloud/bridge/woc_bridge/config.py
Kris 054c4676aa refactor: 重构发送校验逻辑,移除DB轮询与熔断器,改用SSE校验
主要变更:
1. 新增SSEVerifyBus实现基于推送的发送结果校验,替代原DB轮询方案
2. 移除FlowOrchestrator中的DB熔断器,避免雪崩风险
3. 重构SendTextFlow,改用SSE校验流程,删除原DB校验相关代码
4. 新增RoiSpec与ROI搜索支持,优化UI元素匹配精度
5. 更新所有分辨率配置文件,添加ROI配置与调整搜索框坐标
6. 新增WOC_DISABLE_OPENCV环境变量开关,支持纯几何定位模式
7. 清理FlowContext中废弃的before_msg_id字段
2026-07-17 23:50:03 +08:00

365 lines
14 KiB
Python
Raw Permalink 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_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
# 导出文件根目录,每个任务建 <task_id>/ 子目录
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
# SSE 发送结果校验总线send_text 校验,避免 DB 轮询雪崩)
self.verify_bus: Optional[Any] = None # SSEVerifyBus
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