2026-07-08 23:25:58 +08:00
|
|
|
|
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
|
2026-07-18 03:43:28 +08:00
|
|
|
|
from woc_bridge.ui.metrics import send_queue_pending as send_queue_pending_gauge
|
2026-07-08 23:25:58 +08:00
|
|
|
|
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
|
|
|
|
|
|
|
2026-07-18 03:43:28 +08:00
|
|
|
|
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)
|
|
|
|
|
|
|
2026-07-08 23:25:58 +08:00
|
|
|
|
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,
|
2026-07-18 03:43:28 +08:00
|
|
|
|
send_queue_pending=send_queue_pending,
|
2026-07-08 23:25:58 +08:00
|
|
|
|
media_supported=MEDIA_SUPPORTED,
|
|
|
|
|
|
bridge_capabilities=list(BRIDGE_CAPABILITIES),
|
2026-07-18 03:43:28 +08:00
|
|
|
|
ui_scheduler=ui_scheduler_metrics,
|
2026-07-08 23:25:58 +08:00
|
|
|
|
)
|