本次提交涵盖了近百处代码优化与功能补全,包括: 1. 完善配置与数据模型:新增expired_at配对记录字段、路由绑定乐观锁版本控制、会话路由信息追踪字段 2. 优化业务流程:添加幂等记录操作人审计、会话合并领域服务文档更新、媒体处理异步化改造 3. 新增功能能力:健康检查时间更新、会话路由信息更新接口、内容审核/幂等记录清理定时任务 4. 修复与简化:移除废弃的max_message_length属性、修复微信iLink适配器配置读取路径、简化配对过期扫描逻辑 5. 代码规范优化:统一敏感词检测工具导入、完善事务上下文处理注释、调整wechat_woc入站适配器sender回退逻辑
588 lines
24 KiB
Python
588 lines
24 KiB
Python
"""RedisConfigAdapter:实现 ConfigPort,复用 Redis。
|
||
|
||
- 配置版本化更新(WATCH/MULTI/EXEC 原子乐观并发控制)
|
||
- 配置读取、更新、版本查询、回滚、热更新模式查询与 schema 查询
|
||
- 适配器不再构造/发布领域事件(CON-030 / ADP-029):``ConfigChanged`` /
|
||
``ConfigRollback`` 事件由应用层 ``ConfigManager`` 基于返回的
|
||
``ConfigUpdateResult`` 构造并发布
|
||
- 适配器不再承载热更新策略校验(ADP-030):``NON_HOT_RELOADABLE_KEYS``
|
||
校验由应用层 ``ConfigManager`` 执行
|
||
- Redis 客户端由外部构造注入(INV-5),适配器不管理其生命周期,
|
||
不再依赖 ``yuxi.services.run_queue_service``
|
||
|
||
依赖边界:只依赖 yuxi.channels.contract(端口 + DTO + 错误)、
|
||
redis.asyncio、标准库。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
from collections.abc import Mapping
|
||
from datetime import datetime
|
||
from typing import Any
|
||
|
||
from redis.asyncio import Redis
|
||
from redis.exceptions import WatchError
|
||
|
||
from yuxi.channels.contract.dtos.config import (
|
||
ConfigField,
|
||
ConfigHistoryEntry,
|
||
ConfigHotReloadMode,
|
||
ConfigScope,
|
||
ConfigValue,
|
||
ConfigVersion,
|
||
RollbackConfigCmd,
|
||
UpdateConfigCmd,
|
||
)
|
||
from yuxi.channels.contract.dtos.config_update_result import ConfigUpdateResult
|
||
from yuxi.channels.contract.dtos.streaming import StreamingConfig
|
||
from yuxi.channels.contract.errors import (
|
||
ConfigRollbackError,
|
||
ConfigVersionConflictError,
|
||
DependencyError,
|
||
NotFoundError,
|
||
)
|
||
from yuxi.channels.contract.errors.base import Error
|
||
from yuxi.channels.contract.policy.config_schema import CONFIG_SCHEMA
|
||
from yuxi.channels.contract.ports.driven.config_port import ConfigPort
|
||
from yuxi.channels.contract.ports.driven.logger_port import LoggerPort
|
||
from yuxi.utils.datetime_utils import utc_now_naive
|
||
|
||
__all__ = ["RedisConfigAdapter"]
|
||
|
||
|
||
class RedisConfigAdapter(ConfigPort):
|
||
"""Redis 配置适配器。
|
||
|
||
实现 ``ConfigPort``,基于构造注入的 ``redis.asyncio.Redis`` 客户端
|
||
操作 Redis,支持配置读取、更新、版本查询、回滚、热更新模式查询
|
||
与 schema 查询。配置以 ``config:{scope}:{target}:{key}`` 格式存储,
|
||
版本号存储在 ``{scoped_key}:version`` 中,历史记录存储在
|
||
``{scoped_key}:history`` 列表中。
|
||
|
||
配置更新与回滚使用 WATCH/MULTI/EXEC 保证读取-校验-写入的原子性。
|
||
适配器不再构造/发布领域事件(CON-030 / ADP-029),也不再承载热更新
|
||
策略校验(ADP-030);这两项职责由应用层 ``ConfigManager`` 承担。
|
||
``update`` / ``rollback`` 返回 ``ConfigUpdateResult``(含版本号与旧值),
|
||
供应用层构造 ``ConfigChanged`` / ``ConfigRollback`` 领域事件。
|
||
|
||
Redis 客户端由外部构造注入(INV-5),适配器不管理其生命周期。
|
||
通过 ``LoggerPort`` 记录日志(INV-7 / 端口抽象),logger 强制注入。
|
||
"""
|
||
|
||
def __init__(
|
||
self,
|
||
redis_client: Redis,
|
||
logger: LoggerPort,
|
||
key_to_scope_map: Mapping[str, ConfigScope] | None = None,
|
||
) -> None:
|
||
"""初始化 Redis 配置适配器。
|
||
|
||
Args:
|
||
redis_client: ``redis.asyncio.Redis`` 客户端实例,由外部构造并
|
||
注入;适配器不管理其生命周期(构造注入,INV-5)。需使用
|
||
``decode_responses=True`` 创建,保证读写值为字符串。
|
||
logger: 日志被驱动端口,强制注入,用于记录降级分支异常日志
|
||
(INV-10 可观测性)。
|
||
key_to_scope_map: F-02 配置作用域映射(``key → ConfigScope``),
|
||
由 ``ConfigScopeRegistry.key_to_scope_map`` 注入(只读视图)。
|
||
``_build_key`` 在 key 存在于映射时校验声明作用域与实际作用域
|
||
一致性,不一致产生 WARNING 日志(不阻断读写)。为 ``None`` 时不
|
||
校验(向后兼容)。registry 通过 ``dict.update`` 原地修改内部
|
||
dict,adapter 持有的只读视图在 discover 后可见插件声明的作用域。
|
||
"""
|
||
self._redis = redis_client
|
||
self._logger = logger
|
||
self._key_to_scope_map: Mapping[str, ConfigScope] | None = key_to_scope_map
|
||
|
||
async def get(
|
||
self,
|
||
key: str,
|
||
scope: ConfigScope = ConfigScope.GLOBAL,
|
||
target: str | None = None,
|
||
) -> ConfigValue:
|
||
"""读取配置值。
|
||
|
||
从 Redis 读取指定作用域与目标的配置值。若作用域级别不存在,回退到
|
||
全局配置(PLUGIN 作用域除外——插件配置独立,不回退全局)。若全局
|
||
配置也不存在,返回 schema 默认值(若定义);否则抛
|
||
``NotFoundError``(HTTP 404),与 driving 端口 ``getConfig @failure``
|
||
声明一致。
|
||
|
||
Args:
|
||
key: 配置键。
|
||
scope: 配置作用域(默认 GLOBAL)。
|
||
target: 目标(channel_type / account_id / plugin_id,可选)。
|
||
|
||
Returns:
|
||
ConfigValue,包含键、值、版本与作用域。
|
||
|
||
Raises:
|
||
NotFoundError: 配置不存在且 schema 中无默认值。
|
||
DependencyError: Redis 依赖故障。
|
||
"""
|
||
try:
|
||
scoped_key = await self._build_key(key, scope, target)
|
||
value = await self._redis.get(scoped_key)
|
||
actual_key = scoped_key
|
||
# PLUGIN 作用域配置独立,不回退到全局配置
|
||
# skip_scope_check=True:回退到 GLOBAL 时,声明作用域(如 CHANNEL)
|
||
# 与实际作用域(GLOBAL)必然不一致,此为正常回退语义而非配置错误,
|
||
# 跳过 F-02 校验避免误报 "config scope mismatch" 告警。
|
||
if value is None and scope != ConfigScope.PLUGIN:
|
||
actual_key = await self._build_key(key, ConfigScope.GLOBAL, None, skip_scope_check=True)
|
||
value = await self._redis.get(actual_key)
|
||
if value is None:
|
||
default = self._getSchemaDefault(key)
|
||
if default is not None:
|
||
return ConfigValue(
|
||
key=key,
|
||
value=default,
|
||
version=0,
|
||
scope=scope,
|
||
)
|
||
raise NotFoundError(
|
||
resource="config",
|
||
id=key,
|
||
)
|
||
version_str = await self._redis.get(f"{actual_key}:version")
|
||
return ConfigValue(
|
||
key=key,
|
||
value=json.loads(value),
|
||
version=int(version_str) if version_str else 0,
|
||
scope=scope,
|
||
)
|
||
except NotFoundError:
|
||
raise
|
||
except Exception as exc:
|
||
raise DependencyError("redis", Error(str(exc))) from exc
|
||
|
||
async def update(self, cmd: UpdateConfigCmd) -> ConfigUpdateResult:
|
||
"""更新配置。
|
||
|
||
使用 WATCH/MULTI/EXEC 保证读取版本-校验-写入的原子性,返回
|
||
``ConfigUpdateResult``(含键、新版本号、旧值、新值与写入时间),
|
||
供应用层构造 ``ConfigChanged`` 领域事件(CON-030)。
|
||
|
||
适配器不再执行不可热更新配置项校验(ADP-030),该校验由应用层
|
||
``ConfigManager`` 基于 ``NON_HOT_RELOADABLE_KEYS`` 执行。
|
||
|
||
Args:
|
||
cmd: 更新配置命令。
|
||
|
||
Returns:
|
||
ConfigUpdateResult,含键、新版本号、已反序列化的旧值(首次
|
||
创建时为 ``None``)、新值与写入时间。
|
||
|
||
Raises:
|
||
ConfigVersionConflictError: 配置版本冲突(并发修改)。
|
||
DependencyError: Redis 依赖故障。
|
||
"""
|
||
try:
|
||
scoped_key = await self._build_key(cmd.key, cmd.scope, cmd.target)
|
||
|
||
pipe = self._redis.pipeline()
|
||
await pipe.watch(f"{scoped_key}:version")
|
||
try:
|
||
version_str = await pipe.get(f"{scoped_key}:version")
|
||
current_version = int(version_str) if version_str else 0
|
||
|
||
if cmd.expected_version is not None and cmd.expected_version != current_version:
|
||
raise ConfigVersionConflictError(cmd.expected_version, current_version)
|
||
|
||
old_value_raw = await pipe.get(scoped_key)
|
||
# 读取被归档版本的原始写入时间,作为历史条目的 updated_at
|
||
# (而非本次更新时间),确保历史时间戳语义正确。
|
||
old_updated_at_raw = await pipe.get(f"{scoped_key}:updated_at")
|
||
new_version = current_version + 1
|
||
updated_at = utc_now_naive()
|
||
|
||
pipe.multi()
|
||
pipe.set(scoped_key, json.dumps(cmd.value, ensure_ascii=False))
|
||
pipe.set(f"{scoped_key}:version", str(new_version))
|
||
pipe.set(f"{scoped_key}:updated_at", updated_at.isoformat())
|
||
if old_value_raw is not None:
|
||
pipe.lpush(
|
||
f"{scoped_key}:history",
|
||
json.dumps(
|
||
{
|
||
"version": current_version,
|
||
"value": old_value_raw,
|
||
"updated_at": old_updated_at_raw,
|
||
},
|
||
ensure_ascii=False,
|
||
),
|
||
)
|
||
try:
|
||
result = await pipe.execute()
|
||
except WatchError:
|
||
result = None
|
||
finally:
|
||
await pipe.reset()
|
||
|
||
if result is None:
|
||
raise ConfigVersionConflictError(cmd.expected_version or 0, current_version)
|
||
|
||
old_value = json.loads(old_value_raw) if old_value_raw is not None else None
|
||
return ConfigUpdateResult(
|
||
key=cmd.key,
|
||
version=new_version,
|
||
old_value=old_value,
|
||
new_value=cmd.value,
|
||
updated_at=updated_at,
|
||
)
|
||
except ConfigVersionConflictError:
|
||
raise
|
||
except Exception as exc:
|
||
raise DependencyError("redis", Error(str(exc))) from exc
|
||
|
||
async def getVersion(
|
||
self,
|
||
key: str,
|
||
scope: ConfigScope = ConfigScope.GLOBAL,
|
||
target: str | None = None,
|
||
) -> ConfigVersion:
|
||
"""查询配置版本。
|
||
|
||
返回指定配置项的当前版本信息,用于乐观并发控制与版本回溯。
|
||
|
||
Args:
|
||
key: 配置键。
|
||
scope: 配置作用域(默认 GLOBAL)。
|
||
target: 目标(channel_type / account_id / plugin_id,可选)。
|
||
|
||
Returns:
|
||
ConfigVersion,包含键、版本号与更新时间。配置不存在时返回
|
||
版本 0 与 ``updated_at=None``,避免误报"刚刚更新"。
|
||
"""
|
||
try:
|
||
scoped_key = await self._build_key(key, scope, target)
|
||
version_str = await self._redis.get(f"{scoped_key}:version")
|
||
version = int(version_str) if version_str else 0
|
||
updated_at: datetime | None = None
|
||
if version > 0:
|
||
updated_at_raw = await self._redis.get(f"{scoped_key}:updated_at")
|
||
if updated_at_raw is not None:
|
||
try:
|
||
updated_at = datetime.fromisoformat(updated_at_raw)
|
||
except ValueError as exc:
|
||
await self._logger.warn(
|
||
"redis config updated_at parse failed",
|
||
key=key,
|
||
raw_value=updated_at_raw,
|
||
error=str(exc),
|
||
)
|
||
updated_at = None
|
||
return ConfigVersion(
|
||
key=key,
|
||
version=version,
|
||
updated_at=updated_at,
|
||
)
|
||
except Exception as exc:
|
||
raise DependencyError("redis", Error(str(exc))) from exc
|
||
|
||
async def rollback(self, cmd: RollbackConfigCmd) -> ConfigUpdateResult:
|
||
"""回滚配置版本。
|
||
|
||
将配置回滚至指定版本。从历史记录中查找目标版本,使用
|
||
WATCH/MULTI/EXEC 保证读取历史-读取当前值-写入的原子性。返回
|
||
``ConfigUpdateResult``(含键、新版本号、旧值即回滚前失败值、
|
||
新值即回滚后目标值、写入时间),供应用层构造 ``ConfigRollback``
|
||
领域事件(CON-030)。
|
||
|
||
Args:
|
||
cmd: 回滚配置命令。
|
||
|
||
Returns:
|
||
ConfigUpdateResult,含键、新版本号、已反序列化的旧值(即回滚前
|
||
失败值)、新值(即回滚后目标值)与写入时间。
|
||
|
||
Raises:
|
||
NotFoundError: 目标版本不存在于历史记录。
|
||
ConfigVersionConflictError: 配置版本冲突(并发修改)。
|
||
ConfigRollbackError: Redis 回滚失败。
|
||
"""
|
||
try:
|
||
scoped_key = await self._build_key(cmd.key, cmd.scope, cmd.target)
|
||
|
||
history_raw = await self._redis.lrange(f"{scoped_key}:history", 0, -1)
|
||
target_entry: dict[str, Any] | None = None
|
||
for raw in history_raw:
|
||
entry = json.loads(raw)
|
||
if entry.get("version") == cmd.target_version:
|
||
target_entry = entry
|
||
break
|
||
|
||
if target_entry is None:
|
||
raise NotFoundError(
|
||
resource="config_version",
|
||
id=f"{cmd.key}@v{cmd.target_version}",
|
||
)
|
||
|
||
pipe = self._redis.pipeline()
|
||
await pipe.watch(f"{scoped_key}:version")
|
||
try:
|
||
current_value_raw = await pipe.get(scoped_key)
|
||
# 读取被归档版本的原始写入时间,作为历史条目的 updated_at
|
||
# (而非本次回滚时间),确保历史时间戳语义正确。
|
||
old_updated_at_raw = await pipe.get(f"{scoped_key}:updated_at")
|
||
version_str = await pipe.get(f"{scoped_key}:version")
|
||
current_version = int(version_str) if version_str else 0
|
||
new_version = current_version + 1
|
||
updated_at = utc_now_naive()
|
||
|
||
pipe.multi()
|
||
# target_entry["value"] 已是 JSON 字符串(从历史记录反序列化得到),
|
||
# 直接写入 Redis,避免 json.dumps 导致双重编码。
|
||
pipe.set(scoped_key, target_entry["value"])
|
||
pipe.set(f"{scoped_key}:version", str(new_version))
|
||
pipe.set(f"{scoped_key}:updated_at", updated_at.isoformat())
|
||
if current_value_raw is not None:
|
||
pipe.lpush(
|
||
f"{scoped_key}:history",
|
||
json.dumps(
|
||
{
|
||
"version": current_version,
|
||
"value": current_value_raw,
|
||
"updated_at": old_updated_at_raw,
|
||
},
|
||
ensure_ascii=False,
|
||
),
|
||
)
|
||
try:
|
||
result = await pipe.execute()
|
||
except WatchError:
|
||
result = None
|
||
finally:
|
||
await pipe.reset()
|
||
|
||
if result is None:
|
||
raise ConfigVersionConflictError(cmd.target_version, current_version)
|
||
|
||
failed_value = json.loads(current_value_raw) if current_value_raw is not None else None
|
||
rollback_value = json.loads(target_entry["value"])
|
||
return ConfigUpdateResult(
|
||
key=cmd.key,
|
||
version=new_version,
|
||
old_value=failed_value,
|
||
new_value=rollback_value,
|
||
updated_at=updated_at,
|
||
)
|
||
except (
|
||
NotFoundError,
|
||
ConfigVersionConflictError,
|
||
):
|
||
raise
|
||
except Exception as exc:
|
||
raise ConfigRollbackError(
|
||
key=cmd.key,
|
||
reason=f"redis rollback failed: {exc}",
|
||
) from exc
|
||
|
||
async def listHistory(
|
||
self,
|
||
key: str,
|
||
scope: ConfigScope = ConfigScope.GLOBAL,
|
||
target: str | None = None,
|
||
) -> tuple[ConfigHistoryEntry, ...]:
|
||
"""查询配置版本历史列表。
|
||
|
||
从 Redis ``{scoped_key}:history`` 列表读取历史版本条目,解析为
|
||
``ConfigHistoryEntry`` 元组返回。历史条目按 LPUSH 顺序存储(最新
|
||
在前),适配器保持原始顺序返回。
|
||
|
||
兼容性:旧版历史条目未持久化 ``updated_at`` 字段,解析时缺失则
|
||
``updated_at`` 为 ``None``。新版条目(适配器 update/rollback
|
||
写入)携带 ``updated_at`` ISO 字符串。
|
||
|
||
Args:
|
||
key: 配置键。
|
||
scope: 配置作用域(默认 GLOBAL)。
|
||
target: 目标(channel_type / account_id / plugin_id,可选)。
|
||
|
||
Returns:
|
||
ConfigHistoryEntry 元组;无历史记录时返回空元组。
|
||
"""
|
||
try:
|
||
scoped_key = await self._build_key(key, scope, target)
|
||
history_raw = await self._redis.lrange(f"{scoped_key}:history", 0, -1)
|
||
entries: list[ConfigHistoryEntry] = []
|
||
for raw in history_raw:
|
||
entry = json.loads(raw)
|
||
updated_at_raw = entry.get("updated_at")
|
||
updated_at: datetime | None = None
|
||
if updated_at_raw is not None:
|
||
try:
|
||
updated_at = datetime.fromisoformat(updated_at_raw)
|
||
except ValueError as exc:
|
||
await self._logger.warn(
|
||
"redis config history updated_at parse failed",
|
||
key=key,
|
||
raw_value=updated_at_raw,
|
||
error=str(exc),
|
||
)
|
||
updated_at = None
|
||
# entry["value"] 为 JSON 字符串(写入时已序列化),
|
||
# 反序列化为 Python 对象返回。
|
||
value = json.loads(entry["value"])
|
||
entries.append(
|
||
ConfigHistoryEntry(
|
||
version=entry["version"],
|
||
value=value,
|
||
updated_at=updated_at,
|
||
)
|
||
)
|
||
return tuple(entries)
|
||
except Exception as exc:
|
||
raise DependencyError("redis", Error(str(exc))) from exc
|
||
|
||
async def getHotReloadMode(self, key: str) -> ConfigHotReloadMode:
|
||
"""查询配置热更新模式。
|
||
|
||
当前实现固定返回 HYBRID,未来可扩展为从 Redis 读取可配置模式。
|
||
|
||
Args:
|
||
key: 配置键。
|
||
|
||
Returns:
|
||
ConfigHotReloadMode,固定 HYBRID。
|
||
"""
|
||
return ConfigHotReloadMode.HYBRID
|
||
|
||
async def getSchema(self, scope: ConfigScope = ConfigScope.GLOBAL) -> tuple[ConfigField, ...]:
|
||
"""查询配置 schema。
|
||
|
||
返回指定作用域的配置字段 schema,描述所有配置字段的元信息,包括
|
||
可热更新与不可热更新的配置字段。schema 元数据由契约层
|
||
``config_schema.CONFIG_SCHEMA`` 集中定义,适配器直接引用,确保
|
||
单一真相源(§13.1 / §6.1)。
|
||
|
||
Args:
|
||
scope: 配置作用域(默认 GLOBAL)。
|
||
|
||
Returns:
|
||
ConfigField 元组,包含可热更新与不可热更新的配置字段。
|
||
"""
|
||
return CONFIG_SCHEMA
|
||
|
||
async def _build_key(
|
||
self,
|
||
key: str,
|
||
scope: ConfigScope,
|
||
target: str | None,
|
||
*,
|
||
skip_scope_check: bool = False,
|
||
) -> str:
|
||
"""构建 Redis 存储键并校验作用域一致性(F-02)。
|
||
|
||
依据作用域与目标构建分层配置键,支持全局、渠道级、账户级与插件级配置。
|
||
|
||
F-02 作用域一致性校验:当 ``key_to_scope_map`` 已注入且 ``key`` 在
|
||
映射中声明了作用域时,校验声明作用域与传入的实际作用域是否一致。
|
||
不一致时产生 WARNING 日志(含 key 名、声明作用域、实际作用域),
|
||
**不阻断读写**(向后兼容,允许迁移期存在暂时不一致)。未声明作用域
|
||
的 key 不产生告警(向后兼容)。
|
||
|
||
``skip_scope_check=True`` 用于 ``get`` 方法的 GLOBAL 回退路径——该
|
||
回退是设计内的降级行为(CHANNEL/ACCOUNT 未命中时查 GLOBAL),并非
|
||
作用域使用错误,不应触发 F-02 告警。
|
||
|
||
Args:
|
||
key: 配置键。
|
||
scope: 配置作用域(实际读写作用域)。
|
||
target: 目标(channel_type / account_id / plugin_id,可选)。
|
||
skip_scope_check: 跳过 F-02 校验(仅 ``get`` 回退路径使用)。
|
||
|
||
Returns:
|
||
Redis 存储键字符串。
|
||
"""
|
||
if not skip_scope_check and self._key_to_scope_map is not None:
|
||
declared_scope = self._key_to_scope_map.get(key)
|
||
if declared_scope is not None and declared_scope != scope:
|
||
await self._logger.warn(
|
||
"config scope mismatch",
|
||
key=key,
|
||
declared_scope=declared_scope.value,
|
||
actual_scope=scope.value,
|
||
target=target or "",
|
||
)
|
||
if scope == ConfigScope.GLOBAL:
|
||
return f"config:global:{key}"
|
||
if target is None:
|
||
return f"config:{scope.value}:{key}"
|
||
return f"config:{scope.value}:{target}:{key}"
|
||
|
||
async def getStreamingConfig(
|
||
self,
|
||
scope: ConfigScope = ConfigScope.GLOBAL,
|
||
target: str | None = None,
|
||
) -> StreamingConfig:
|
||
"""读取流式配置。
|
||
|
||
读取 ``streaming_enabled`` / ``enable_typing`` /
|
||
``streaming_min_chunk_interval_ms`` / ``streaming_ttl_seconds`` /
|
||
``typing_ttl_ms`` 五个配置项,未持久化时由 ``get`` 方法
|
||
返回 schema 默认值。``streaming_ttl_seconds`` 转换为毫秒返回,
|
||
与 ``min_chunk_interval_ms`` 单位一致。
|
||
|
||
Args:
|
||
scope: 配置作用域(默认 GLOBAL)。
|
||
target: 目标(channel_type 或 account_id,可选)。
|
||
|
||
Returns:
|
||
StreamingConfig,包含流式管道运行时配置。
|
||
|
||
Raises:
|
||
DependencyError: Redis 依赖故障。
|
||
"""
|
||
enabled_value = await self.get("streaming_enabled", scope, target)
|
||
typing_enabled_value = await self.get("enable_typing", scope, target)
|
||
interval_value = await self.get("streaming_min_chunk_interval_ms", scope, target)
|
||
ttl_value = await self.get("streaming_ttl_seconds", scope, target)
|
||
typing_ttl_value = await self.get("typing_ttl_ms", scope, target)
|
||
return StreamingConfig(
|
||
streaming_enabled=bool(enabled_value.value),
|
||
enable_typing=bool(typing_enabled_value.value),
|
||
min_chunk_interval_ms=int(interval_value.value),
|
||
streaming_ttl_ms=int(ttl_value.value) * 1000,
|
||
typing_ttl_ms=int(typing_ttl_value.value),
|
||
)
|
||
|
||
def _getSchemaDefault(self, key: str) -> Any:
|
||
"""查询配置 schema 默认值。
|
||
|
||
从契约层 ``CONFIG_SCHEMA`` 字段元组中查找指定键的默认值,用于配置
|
||
未持久化时返回 schema 默认值(Task 30)。直接引用 ``CONFIG_SCHEMA``
|
||
常量,避免在同步方法中调用 ``async getSchema``。
|
||
|
||
Args:
|
||
key: 配置键。
|
||
|
||
Returns:
|
||
默认值;schema 中不存在或默认值为 None 时返回 None。
|
||
"""
|
||
for field in CONFIG_SCHEMA:
|
||
if field.key == key:
|
||
return field.default
|
||
return None
|
||
|
||
async def ping(self) -> bool:
|
||
"""主动探测 Redis 连接可用性,故障时返回 False(降级不阻断)。
|
||
|
||
执行 Redis ``PING`` 验证连接可用性。故障时返回 False 并通过
|
||
``self._logger`` 记录 warning 日志,不抛异常,供 ``HostBootstrap``
|
||
启动期连通性检查使用。
|
||
|
||
Returns:
|
||
True 表示连接可用,False 表示故障。
|
||
"""
|
||
try:
|
||
await self._redis.ping()
|
||
return True
|
||
except Exception as exc:
|
||
await self._logger.warn(
|
||
"redis config ping failed",
|
||
error=str(exc),
|
||
)
|
||
return False
|