ForcePilot/backend/package/yuxi/channels/infra/config_watcher.py
Kris 7a972055b7 feat: 新增渠道服务基础框架与核心工具类
新增大量渠道适配器相关的协议、策略、工具类与基础设施代码,包括:
1.  多协议定义:认证、消息、配置、网关等核心接口
2.  策略模块:上下文、群聊、去重、防抖等业务策略
3.  工具集:重试、去重、文本分块、消息格式化等SDK工具
4.  基础设施:外部进程管理、事件广播、熔断机制等
5.  账户与管道系统:账户管理、消息处理管道实现
6.  运行时服务:状态收集、维护任务、日志等后台服务
2026-05-12 00:53:57 +08:00

126 lines
4.7 KiB
Python

from __future__ import annotations
import asyncio
import hashlib
import json
from typing import Any, TYPE_CHECKING
from yuxi.utils.logging_config import logger
if TYPE_CHECKING:
from yuxi.channels.manager import ChannelManager
class ConfigWatcher:
def __init__(self, channel_manager: ChannelManager):
self._manager = channel_manager
self._last_config_hash: str | None = None
self._watched_prefixes: list[str] = ["channels"]
self._noop_prefixes: list[str] = []
self._running = False
self._task: asyncio.Task | None = None
def set_watched_prefixes(self, prefixes: list[str]) -> None:
self._watched_prefixes = list(prefixes)
def set_noop_prefixes(self, prefixes: list[str]) -> None:
self._noop_prefixes = list(prefixes)
async def watch(self, interval: float = 30.0) -> None:
if self._running:
return
self._running = True
self._last_config_hash = self._compute_config_hash()
self._task = asyncio.create_task(self._watch_loop(interval))
async def stop(self) -> None:
self._running = False
if self._task:
self._task.cancel()
try:
await self._task
except asyncio.CancelledError:
pass
self._task = None
async def reload_now(self, changed_keys: list[str] | None = None) -> dict[str, Any]:
if changed_keys is None:
diff = self._compute_config_diff()
else:
diff = self._compute_diff_from_keys(changed_keys)
return await self._apply_diff(diff)
async def _watch_loop(self, interval: float) -> None:
while self._running:
await asyncio.sleep(interval)
if not self._running:
break
try:
current = self._compute_config_hash()
if current != self._last_config_hash:
logger.info("Config change detected, computing diff")
diff = self._compute_config_diff()
await self._apply_diff(diff)
self._last_config_hash = current
except asyncio.CancelledError:
break
except Exception:
logger.exception("ConfigWatcher loop error")
def _compute_config_hash(self) -> str:
raw = json.dumps(self._manager._channels_config, sort_keys=True, default=str)
return hashlib.sha256(raw.encode()).hexdigest()
def _compute_config_diff(self) -> dict[str, dict[str, Any]]:
diff: dict[str, dict[str, Any]] = {}
current_channels = set(self._manager._channels_config.keys())
running_channels = set(self._manager._adapters.keys())
added = current_channels - running_channels
removed = running_channels - current_channels
for channel_id in added:
diff[channel_id] = {"action": "start", "config": self._manager._channels_config[channel_id]}
for channel_id in removed:
diff[channel_id] = {"action": "stop"}
for channel_id in current_channels & running_channels:
diff[channel_id] = {"action": "restart"}
return diff
def _compute_diff_from_keys(self, changed_keys: list[str]) -> dict[str, dict[str, Any]]:
diff: dict[str, dict[str, Any]] = {}
for key in changed_keys:
for prefix in self._watched_prefixes:
if not key.startswith(prefix):
continue
if any(key.startswith(noop) for noop in self._noop_prefixes):
continue
parts = key.split(".")
if len(parts) >= 2:
channel_id = parts[1]
diff[channel_id] = {"action": "restart"}
break
return diff
async def _apply_diff(self, diff: dict) -> dict[str, Any]:
results: dict[str, Any] = {}
for channel_id, action in diff.items():
try:
if action.get("action") == "start":
if self._manager._channels_config.get(channel_id, {}).get("enabled", False):
await self._manager.start_channel(channel_id, action.get("config"))
results[channel_id] = "started"
elif action.get("action") == "stop":
await self._manager.stop_channel(channel_id)
results[channel_id] = "stopped"
elif action.get("action") == "restart":
await self._manager.restart_channel(channel_id)
results[channel_id] = "restarted"
except Exception:
logger.exception(f"ConfigWatcher failed to apply diff for {channel_id}")
results[channel_id] = "failed"
return results