merge: sync upstream/r-dev and resolve real conflicts

This commit is contained in:
A-Dawn
2026-03-24 15:36:26 +08:00
114 changed files with 15841 additions and 5236 deletions

View File

@@ -4,6 +4,7 @@ from typing import Any, Callable, Mapping, Sequence, TypeVar
import asyncio
import copy
import inspect
import sys
import tomlkit
@@ -61,6 +62,7 @@ MODEL_CONFIG_VERSION: str = "1.12.0"
logger = get_logger("config")
T = TypeVar("T", bound="ConfigBase")
ConfigReloadCallback = Callable[[Sequence[str]], object] | Callable[[], object]
class Config(ConfigBase):
@@ -190,7 +192,7 @@ class ConfigManager:
self.global_config: Config | None = None
self.model_config: ModelConfig | None = None
self._reload_lock: asyncio.Lock = asyncio.Lock()
self._reload_callbacks: list[Callable[[], object]] = []
self._reload_callbacks: list[ConfigReloadCallback] = []
self._file_watcher: FileWatcher | None = None
self._file_watcher_subscription_id: str | None = None
self._hot_reload_min_interval_s: float = 1.0
@@ -226,16 +228,125 @@ class ConfigManager:
raise RuntimeError(t("config.model_not_initialized"))
return self.model_config
def register_reload_callback(self, callback: Callable[[], object]) -> None:
def register_reload_callback(self, callback: ConfigReloadCallback) -> None:
"""注册配置热重载回调。
Args:
callback: 配置热重载回调。允许无参回调,也允许接收
``Sequence[str]`` 类型的变更范围列表。
"""
self._reload_callbacks.append(callback)
def unregister_reload_callback(self, callback: Callable[[], object]) -> None:
def unregister_reload_callback(self, callback: ConfigReloadCallback) -> None:
"""注销配置热重载回调。
Args:
callback: 先前注册过的回调对象。
"""
try:
self._reload_callbacks.remove(callback)
except ValueError:
return
async def reload_config(self) -> bool:
@staticmethod
def _normalize_changed_scopes(changed_scopes: Sequence[str] | None) -> tuple[str, ...]:
"""规范化配置变更范围列表。
Args:
changed_scopes: 原始配置变更范围。
Returns:
tuple[str, ...]: 去重后的配置变更范围元组。
"""
if not changed_scopes:
return ("bot", "model")
normalized_scopes: list[str] = []
for scope in changed_scopes:
normalized_scope = str(scope or "").strip().lower()
if normalized_scope not in {"bot", "model"}:
continue
if normalized_scope not in normalized_scopes:
normalized_scopes.append(normalized_scope)
return tuple(normalized_scopes)
@staticmethod
def _resolve_changed_scopes(changes: Sequence[FileChange]) -> tuple[str, ...]:
"""根据文件变更列表推断配置变更范围。
Args:
changes: 文件监听器返回的变更列表。
Returns:
tuple[str, ...]: 命中的配置变更范围元组。
"""
changed_scopes: list[str] = []
for change in changes:
file_name = change.path.name
if file_name == "bot_config.toml" and "bot" not in changed_scopes:
changed_scopes.append("bot")
if file_name == "model_config.toml" and "model" not in changed_scopes:
changed_scopes.append("model")
return tuple(changed_scopes)
@staticmethod
def _callback_accepts_scopes(callback: ConfigReloadCallback) -> bool:
"""判断回调是否接收配置变更范围参数。
Args:
callback: 待检测的回调对象。
Returns:
bool: 若回调可接收一个位置参数或可变位置参数,则返回 ``True``。
"""
try:
parameters = inspect.signature(callback).parameters.values()
except (TypeError, ValueError):
return False
positional_params = {
inspect.Parameter.POSITIONAL_ONLY,
inspect.Parameter.POSITIONAL_OR_KEYWORD,
}
for parameter in parameters:
if parameter.kind == inspect.Parameter.VAR_POSITIONAL:
return True
if parameter.kind in positional_params:
return True
return False
async def _invoke_reload_callback(
self,
callback: ConfigReloadCallback,
changed_scopes: Sequence[str],
) -> None:
"""执行单个配置热重载回调。
Args:
callback: 要执行的回调对象。
changed_scopes: 本次热重载命中的配置范围。
"""
result = callback(changed_scopes) if self._callback_accepts_scopes(callback) else callback()
if asyncio.iscoroutine(result):
await result
async def reload_config(self, changed_scopes: Sequence[str] | None = None) -> bool:
"""重新加载主配置和模型配置。
Args:
changed_scopes: 本次触发热重载的配置范围。
Returns:
bool: 是否重载成功。
"""
normalized_scopes = self._normalize_changed_scopes(changed_scopes)
async with self._reload_lock:
try:
global_config_new, global_updated = load_config_from_file(
@@ -265,9 +376,7 @@ class ConfigManager:
for callback in list(self._reload_callbacks):
try:
result = callback()
if asyncio.iscoroutine(result):
await result
await self._invoke_reload_callback(callback, normalized_scopes)
except Exception as exc:
logger.warning(t("config.reload_callback_failed", error=exc))
return True
@@ -312,6 +421,12 @@ class ConfigManager:
self._file_watcher = None
async def _handle_file_changes(self, changes: Sequence[FileChange]) -> None:
"""处理主配置与模型配置文件变更。
Args:
changes: 当前批次收集到的文件变更列表。
"""
if not changes:
return
now_monotonic = asyncio.get_running_loop().time()
@@ -321,7 +436,11 @@ class ConfigManager:
self._last_hot_reload_monotonic = now_monotonic
logger.info(t("config.file_change_detected"))
try:
await asyncio.wait_for(self.reload_config(), timeout=self._hot_reload_timeout_s)
changed_scopes = self._resolve_changed_scopes(changes)
await asyncio.wait_for(
self.reload_config(changed_scopes=changed_scopes),
timeout=self._hot_reload_timeout_s,
)
except asyncio.TimeoutError:
logger.error(t("config.reload_timeout", timeout_seconds=self._hot_reload_timeout_s))

View File

@@ -1633,24 +1633,6 @@ class PluginRuntimeConfig(ConfigBase):
)
"""启用插件系统"""
builtin_plugin_dir: str = Field(
default="src/plugins/built_in",
json_schema_extra={
"x-widget": "input",
"x-icon": "folder",
},
)
"""内置插件目录(相对于项目根目录)"""
thirdparty_plugin_dir: str = Field(
default="plugins",
json_schema_extra={
"x-widget": "input",
"x-icon": "folder-open",
},
)
"""第三方插件目录(相对于项目根目录)"""
health_check_interval_sec: float = Field(
default=30.0,
json_schema_extra={
@@ -1678,14 +1660,14 @@ class PluginRuntimeConfig(ConfigBase):
)
"""等待 Runner 子进程启动并注册的超时时间(秒)"""
workflow_blocking_timeout_sec: float = Field(
default=120.0,
hook_blocking_timeout_sec: float = Field(
default=30,
json_schema_extra={
"x-widget": "number",
"x-icon": "timer",
},
)
"""Workflow 阻塞步骤的全局超时上限(秒)"""
"""Hook 阻塞步骤的全局超时上限(秒)"""
ipc_socket_path: str = Field(
default="",
@@ -1694,4 +1676,7 @@ class PluginRuntimeConfig(ConfigBase):
"x-icon": "link",
},
)
"""_wrap_\n 自定义 IPC Socket 路径(仅 Linux/macOS 生效)\n 留空则自动生成临时路径"""
"""
自定义 IPC Socket 路径(仅 Linux/macOS 生效)
留空则自动生成临时路径
"""