新增大量渠道适配器相关的协议、策略、工具类与基础设施代码,包括: 1. 多协议定义:认证、消息、配置、网关等核心接口 2. 策略模块:上下文、群聊、去重、防抖等业务策略 3. 工具集:重试、去重、文本分块、消息格式化等SDK工具 4. 基础设施:外部进程管理、事件广播、熔断机制等 5. 账户与管道系统:账户管理、消息处理管道实现 6. 运行时服务:状态收集、维护任务、日志等后台服务
126 lines
4.7 KiB
Python
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
|