ForcePilot/backend/package/yuxi/channels/infra/config_watcher.py
Kris 29dac24600 feat(channels): 完成渠道模块多维度功能迭代与优化
本次提交对渠道模块进行了全面升级,包含以下核心改进:
1. 新增二维码登录相关协议方法,完善登录流程
2. 优化配置监听逻辑,增加渠道运行状态前置校验
3. 重构动作注册机制,支持动态注册渠道动作并新增批量操作能力
4. 扩展渠道能力模型,新增广播、文件传输等支持
5. 优化适配器加载路径,新增元宝适配器支持
6. 新增凭证过期检查与告警能力,完善运维监控
7. 重构统计收集器,支持多维度渠道统计数据
8. 优化消息路由策略,新增策略缓存与安全处理逻辑
9. 重构基础适配器,新增凭证管理工具方法
10. 完善状态存储功能,支持凭证数据管理与批量清理
11. 重构渠道管理器,新增配置校验、动态渠道管理、限流能力
12. 优化健康检查与状态上报逻辑,完善审计日志与异常处理
2026-05-14 02:08:36 +08:00

129 lines
4.9 KiB
Python

from __future__ import annotations
import asyncio
import hashlib
import json
from typing import TYPE_CHECKING, Any
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":
if self._manager.is_running(channel_id):
await self._manager.stop_channel(channel_id)
results[channel_id] = "stopped"
else:
results[channel_id] = "already_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