from __future__ import annotations import asyncio import logging import time from fastapi import APIRouter from woc_bridge.config import _state, _require_xdotool, _require_db_reader from woc_bridge.db.coordinator import with_db_retry, _resolve_db_state from woc_bridge.models import StatusResponse from woc_bridge.ui.metrics import send_queue_pending as send_queue_pending_gauge from woc_bridge.version import BRIDGE_VERSION, BRIDGE_CAPABILITIES, MEDIA_SUPPORTED logger = logging.getLogger("woc-bridge") router = APIRouter() @router.get("/api/status", response_model=StatusResponse) @with_db_retry async def get_status() -> StatusResponse: """聚合返回 bridge 运行状态。 一次请求内完成 4 类检测:进程运行、窗口存在、登录态、DB 可达性, 并返回当前账号信息与客户端轮询建议参数。所有调用方(channels 适配器、 面板健康检查、诊断 autofix)都应优先调本接口判断前置条件。 Returns: StatusResponse:含 bridge_version / wechat_running / wechat_window_found / login_state / db_accessible / current_wxid / current_nickname / uptime_seconds / display / max_batch_size / poll_interval_ms Notes: - 不抛业务错误:即使微信未运行 / DB 加密,也返回 200 + 对应 false 字段, 让调用方按字段判定下一步动作(而非按 HTTP 状态码) - login_state 内联判定(不调 detect_login_state),避免重复 pgrep/xdotool - DB 加密时细分:有 key 且验证过 → encrypted_key_ok + db_accessible=true; 无 key 且自动提取未启用 → encrypted_no_key; 自动提取已尝试但失败 → key_extract_failed; 二者 db_accessible 均为 false - current_wxid/nickname 仅在 db_accessible=true 且 login_state=logged_in 时才尝试读 DB,失败静默为空字符串 """ xdotool = _require_xdotool() db_reader = _require_db_reader() # 一次 status 请求内复用 pgrep/xdotool 结果,避免重复调用 wechat_running = await xdotool.is_wechat_running() window_id = await xdotool.find_wechat_window() if wechat_running else None wechat_window_found = window_id is not None # 登录态内联判定,避免 detect_login_state 内部再次 pgrep/xdotool search if not wechat_running: login_state = "not_running" elif not wechat_window_found: login_state = "not_logged_in" else: login_state = "logged_in" # DB 状态:统一走 _resolve_db_state(不触发自动提取,仅解析) db_state = await _resolve_db_state() db_accessible = db_state.db_accessible db_error_code = db_state.db_error_code init_in_progress = _state.init_state.state == "running" init_progress_pct = _state.init_state.progress_pct init_message = _state.init_state.message # current_wxid / current_nickname:从 DB 读,失败为空 current_wxid = "" current_nickname = "" if db_accessible and login_state == "logged_in": try: self_info = await asyncio.to_thread(db_reader.get_self_info) current_wxid = self_info.get("wxid", "") current_nickname = self_info.get("nickname", "") except Exception: current_wxid = "" current_nickname = "" uptime_seconds = time.monotonic() - _state.start_time send_queue = _state.send_queue send_queue_pending = send_queue.pending_count() if send_queue else 0 ui_scheduler_metrics = None if send_queue is not None and hasattr(send_queue, "get_metrics"): try: ui_scheduler_metrics = send_queue.get_metrics() except Exception as exc: logger.warning("[status] get_metrics failed: %s", exc) # 修复既有 Prometheus Gauge bug:每次 status 请求时 set 当前 pending 数 try: send_queue_pending_gauge.set(send_queue_pending) except Exception as exc: logger.debug("[status] send_queue_pending_gauge.set failed: %s", exc) return StatusResponse( bridge_version=BRIDGE_VERSION, wechat_running=wechat_running, wechat_window_found=wechat_window_found, login_state=login_state, db_accessible=db_accessible, db_error_code=db_error_code, init_in_progress=init_in_progress, init_progress_pct=init_progress_pct, init_message=init_message, current_wxid=current_wxid, current_nickname=current_nickname, uptime_seconds=uptime_seconds, display=_state.config.display, max_batch_size=_state.config.max_batch_size, poll_interval_ms=_state.config.poll_interval_ms, send_queue_pending=send_queue_pending, media_supported=MEDIA_SUPPORTED, bridge_capabilities=list(BRIDGE_CAPABILITIES), ui_scheduler=ui_scheduler_metrics, )