ForcePilot/backend/package/yuxi/channels/plugins/feishu/lifecycle.py
Kris 00092c818e chore: 批量代码优化与规范完善
本次提交包含多项代码优化与规范修正:
1. 文档与注释优化:修正注释术语、补充注解与FR编号
2. 代码格式调整:统一空格、换行与缩进规范
3. 类型与接口完善:补充__all__导出、修正返回类型注解
4. 错误处理增强:新增领域错误类与校验逻辑
5. 依赖与导入调整:修复路径引用、统一时区导入
6. 协议与契约更新:完善接口文档与一致性注解
2026-07-03 19:18:13 +08:00

171 lines
6.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""飞书渠道插件生命周期钩子处理。
实现 ``LifecycleHookHandler`` Protocol管理插件级资源httpx 连接池),
响应宿主生命周期状态变迁。
设计要点:
- LifecycleHandler 为单实例(非 per-account可持有 mutable 状态用于
资源管理(与适配器的 INV-5 无 mutable 状态约束不同)。
- httpx 连接池在 ``onInit`` 创建,``onUnload`` 关闭。
- WebSocket 长连接由 ``StreamWorker`` 通过 ``FeishuStreamConnectorAdapter``
统一管理,``LifecycleHandler`` 不再持有 WS 客户端句柄。
- ``onReconfigure`` 从配置 schema 派生不可热更新键列表
``hot_reloadable=False``),变更时抛 ``ConfigRestartRequiredError``。
- ``onFail`` 不抛异常不阻断降级流程logger 不可用时回退 ``stderr``。
依赖方向:仅 import ``yuxi.channels.contract.*`` + 标准库 + httpx
不污染框架层。
"""
from __future__ import annotations
import sys
from typing import Any
import httpx
from yuxi.channels.contract.dtos.config import ConfigField
from yuxi.channels.contract.errors import ConfigRestartRequiredError
from yuxi.channels.contract.ports.driven.cache_port import CachePort
from yuxi.channels.contract.ports.driven.config_port import ConfigPort
from yuxi.channels.contract.ports.driven.logger_port import LoggerPort
# 默认不可热更新键config_schema 未注入时的回退,与 manifest.json
# config_schema 中 hot_reloadable=False 的字段保持一致)
_DEFAULT_RESTART_REQUIRED_KEYS: tuple[str, ...] = (
"app_id",
"app_secret",
"event_mode",
"webhook_url",
)
# httpx 连接池配置
_HTTP_TIMEOUT_SECONDS = 30.0
_HTTP_MAX_CONNECTIONS = 50
_HTTP_MAX_KEEPALIVE_CONNECTIONS = 10
class FeishuLifecycleHandler:
"""飞书渠道插件生命周期钩子处理。
单实例,管理插件级资源生命周期。``_http_client`` 为共享连接池,
``_last_config`` 缓存上一次有效配置,供 ``onReconfigure`` 校验与回滚。
不可热更新键列表从配置 schema 派生(``hot_reloadable=False``schema
未注入时回退到 ``_DEFAULT_RESTART_REQUIRED_KEYS``。WebSocket 连接由
``StreamWorker`` 管理,本处理器不持有 WS 句柄。
"""
def __init__(
self,
config_port: ConfigPort,
logger_port: LoggerPort,
cache_port: CachePort,
config_schema: tuple[ConfigField, ...] | None = None,
) -> None:
self._config = config_port
self._logger = logger_port
self._cache = cache_port
self._http_client: httpx.AsyncClient | None = None
self._started: bool = False
self._last_config: dict[str, Any] | None = None
# 从配置 schema 派生不可热更新键hot_reloadable=False避免
# 硬编码与 manifest config_schema 不一致
if config_schema is not None:
self._restart_required_keys = tuple(field.key for field in config_schema if not field.hot_reloadable)
else:
self._restart_required_keys = _DEFAULT_RESTART_REQUIRED_KEYS
# ------------------------------------------------------------------
# LifecycleHookHandler Protocol 实现
# ------------------------------------------------------------------
async def onInit(self) -> None:
"""初始化资源:创建 httpx 连接池。"""
self._http_client = httpx.AsyncClient(
timeout=_HTTP_TIMEOUT_SECONDS,
limits=httpx.Limits(
max_connections=_HTTP_MAX_CONNECTIONS,
max_keepalive_connections=_HTTP_MAX_KEEPALIVE_CONNECTIONS,
),
)
await self._logger.info("Feishu plugin initialized")
async def onStart(self) -> None:
"""标记插件就绪。"""
self._started = True
await self._logger.info("Feishu plugin started")
async def onStop(self) -> None:
"""停止插件:标记未就绪,等待在途请求完成。"""
await self._logger.info(
"Feishu plugin stopping, waiting for in-flight requests",
)
self._started = False
async def onPause(self) -> None:
"""暂停插件:停止接收新请求。"""
self._started = False
await self._logger.info("Feishu plugin paused")
async def onResume(self) -> None:
"""恢复插件:重新接收请求。"""
self._started = True
await self._logger.info("Feishu plugin resumed")
async def onUnload(self) -> None:
"""释放所有资源:关闭 httpx 连接池。
WebSocket 连接由 ``TransportManager.stop()`` 在宿主关停时首先关闭,
本方法不再处理 WS 客户端清理。
"""
if self._http_client is not None:
try:
await self._http_client.aclose()
except Exception as exc:
await self._logger.warn(
"Feishu httpx client close failed during unload",
error=str(exc),
)
self._http_client = None
self._started = False
await self._logger.info("Feishu plugin unloaded")
async def onReconfigure(self, config: dict[str, Any]) -> None:
"""配置热更新:校验不可热更新字段,接受则缓存为新基线。
- 不可热更新键(``hot_reloadable=False``,如 ``app_id`` /
``app_secret`` / ``event_mode`` / ``webhook_url``)变更抛
``ConfigRestartRequiredError``FR-37
- 首次调用(``_last_config`` 为 None仅建立基线不做对比。
- 校验通过后更新 ``_last_config``;校验失败保持原基线,由宿主回滚。
"""
if self._last_config is None:
self._last_config = dict(config)
await self._logger.info("Feishu plugin initial config baseline set")
return
for key in self._restart_required_keys:
new_value = config.get(key)
old_value = self._last_config.get(key)
if new_value != old_value:
raise ConfigRestartRequiredError(key=key) from None
self._last_config = dict(config)
await self._logger.info("Feishu plugin reconfigured")
async def onFail(self, error: str) -> None:
"""失败清理不抛异常不阻断降级流程FR-36
logger 不可用时回退 ``sys.stderr.write``,确保失败信息不丢失。
不抛异常以遵守 FR-36「不阻断降级流程」约束。
"""
self._started = False
try:
await self._logger.error("Feishu plugin failed", error=error)
except Exception as exc:
sys.stderr.write(f"Feishu plugin failed: {error}\nLogger error: {exc}\n")
__all__ = ["FeishuLifecycleHandler"]