1. 移除所有适配器文件中多余的空导入行 2. 调整ValidationError继承,移除不必要的ValueError继承 3. 修正多处ChannelType使用方式,从.value改为直接使用枚举实例 4. 优化飞书插件部分硬编码渠道类型为枚举实例 5. 更新wechat_ilink插件清单与适配器配置 6. 新增飞书目录适配器缓存清理支持判断与iLink生命周期适配器凭据轮换支持判断 7. 优化配置处理器历史查询逻辑,区分键不存在与无历史记录场景
321 lines
12 KiB
Python
321 lines
12 KiB
Python
"""管道基础设施。
|
||
|
||
定义应用层管道基类 ``Pipeline`` 与阶段协议 ``AppStage``,提供阶段顺序执行、
|
||
条件跳过、失败策略处理与补偿执行能力。``FailureStrategy`` 从契约层复用,
|
||
不重新定义,保证失败语义在全链路一致。
|
||
|
||
可观测性(§11.2 / INV-10):``Pipeline.run()`` 在每个阶段执行后输出结构化
|
||
日志,必填字段 ``trace_id`` / ``stage_id`` / ``duration_ms`` / ``status`` /
|
||
``error_type``(失败时),支持阶段延迟分布与成功率度量。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import sys
|
||
import time
|
||
from collections.abc import Callable
|
||
from typing import Any, Protocol, runtime_checkable
|
||
|
||
from yuxi.channels.contract.errors import Error, InternalError, PipelineConfigError
|
||
from yuxi.channels.contract.plugin.extension_point import FailureStrategy
|
||
from yuxi.channels.contract.ports.driven.logger_port import LoggerPort
|
||
|
||
|
||
@runtime_checkable
|
||
class AppStage(Protocol):
|
||
"""应用层管道阶段协议。
|
||
|
||
对齐契约层 ``Stage`` Protocol 并扩展:
|
||
- ``compensate`` 由 ``bool`` 调整为 ``str | None``,携带补偿阶段名称
|
||
(如 "outbox-rollback"),None 表示无补偿;
|
||
- 新增 ``condition`` 字段,支持基于上下文的阶段执行条件,None 表示
|
||
无条件执行。
|
||
|
||
阶段处理方法 ``process`` 返回 ``bool``:True 表示成功,False 或抛出
|
||
异常表示失败。
|
||
"""
|
||
|
||
id: str
|
||
reads: tuple[str, ...]
|
||
writes: tuple[str, ...]
|
||
idempotent: bool
|
||
thread_safe: bool
|
||
failure: FailureStrategy
|
||
compensate: str | None
|
||
condition: Callable[[Any], bool] | None
|
||
|
||
async def process(self, context: Any) -> bool:
|
||
"""处理管道阶段。
|
||
|
||
参数:
|
||
context: 管道上下文。
|
||
|
||
返回:
|
||
True 表示成功,False 或抛出异常表示失败。
|
||
"""
|
||
...
|
||
|
||
|
||
class Pipeline:
|
||
"""管道基类。
|
||
|
||
顺序执行阶段,按 ``AppStage.failure`` 策略处理失败。支持条件跳过、
|
||
异常翻译与补偿执行。
|
||
|
||
失败策略处理:
|
||
- TERMINATE:终止管道,返回失败;
|
||
- SKIP:跳过当前阶段,继续下一阶段;
|
||
- COMPENSATE:执行补偿阶段后返回失败;
|
||
- DEGRADE:标记降级并继续下一阶段。
|
||
|
||
命名等价说明(6-P2-01):上下文 ``trace_id`` 与 Agent 层 ``turn_id``
|
||
为同一概念的不同命名,均标识一次请求/会话回合的链路追踪 ID。
|
||
``_logStageExecution`` 输出的 ``trace_id`` 字段即等价于 ``turn_id``。
|
||
"""
|
||
|
||
name: str
|
||
stages: list[AppStage]
|
||
logger: LoggerPort | None
|
||
|
||
def __init__(
|
||
self,
|
||
name: str,
|
||
stages: list[AppStage],
|
||
logger: LoggerPort | None = None,
|
||
) -> None:
|
||
"""初始化管道。
|
||
|
||
参数:
|
||
name: 管道名称(如 "inbound" / "outbound" / "control-plane")。
|
||
stages: 阶段列表,按列表顺序执行。
|
||
logger: 可选日志端口,用于记录补偿阶段异常,None 时不记录。
|
||
|
||
Raises:
|
||
PipelineConfigError: 任一阶段的 ``compensate`` 字段指向不存在
|
||
的阶段 ID。构造期 fail-fast,避免运行期 ``_runCompensate``
|
||
按名称查找静默失败(§3.1 补偿执行约束)。
|
||
"""
|
||
self.name = name
|
||
self.stages = stages
|
||
self.logger = logger
|
||
self._validateCompensateReferences()
|
||
|
||
def _validateCompensateReferences(self) -> None:
|
||
"""校验所有阶段的 ``compensate`` 引用指向已注册的阶段 ID。
|
||
|
||
``compensate`` 字段为字符串约定,``_runCompensate`` 按名称查找
|
||
阶段,未找到时**静默返回原错误**(不报错)。若因 typo 导致引用
|
||
不存在,补偿逻辑永远不会执行且无任何告警。
|
||
|
||
本方法在构造期校验所有 ``compensate`` 引用,发现不存在的引用
|
||
立即抛出 ``PipelineConfigError``(fail-fast),将运行期静默失败
|
||
转换为构造期显式失败。
|
||
"""
|
||
stage_ids = {stage.id for stage in self.stages}
|
||
for stage in self.stages:
|
||
if stage.compensate is not None and stage.compensate not in stage_ids:
|
||
raise PipelineConfigError(
|
||
pipeline=self.name,
|
||
stage_id=stage.id,
|
||
compensate=stage.compensate,
|
||
)
|
||
|
||
async def run(self, ctx: Any) -> tuple[bool, Error | None]:
|
||
"""执行管道。
|
||
|
||
按 ``stages`` 列表顺序执行每个阶段。阶段返回 True 表示成功,继续
|
||
下一阶段;返回 False 或抛出异常表示失败,按 ``failure`` 策略处理。
|
||
若 ``condition`` 返回 False 则跳过该阶段。
|
||
|
||
异常翻译:``Error`` 子类直接处理,原生 ``Exception`` 翻译为
|
||
``InternalError``(携带 ``trace_id`` 与异常信息),不得让原生异常
|
||
穿透管道边界(INV-7 / FF-ERR-01)。
|
||
|
||
可观测性(§11.2 / INV-10):每个阶段执行后输出结构化日志,必填
|
||
字段 ``trace_id`` / ``stage_id`` / ``duration_ms`` / ``status`` /
|
||
``error_type``(失败时),日志失败不影响管道执行。
|
||
|
||
参数:
|
||
ctx: 管道上下文。
|
||
|
||
返回:
|
||
元组 (success, error)。全部阶段成功时 success 为 True、
|
||
error 为 None;否则 success 为 False、error 携带失败原因。
|
||
"""
|
||
for stage in self.stages:
|
||
if stage.condition is not None and not stage.condition(ctx):
|
||
continue
|
||
|
||
ok = False
|
||
err: Error | None = None
|
||
start = time.monotonic()
|
||
try:
|
||
ok = await stage.process(ctx)
|
||
except Error as e:
|
||
err = e
|
||
except Exception as e:
|
||
trace_id = getattr(ctx, "trace_id", None)
|
||
err = InternalError(
|
||
trace_id=trace_id,
|
||
message=f"stage '{stage.id}' raised native exception: {type(e).__name__}: {e}",
|
||
cause=e,
|
||
)
|
||
duration_ms = (time.monotonic() - start) * 1000
|
||
|
||
# 结构化可观测性日志(§11.2 / INV-10)
|
||
await self._logStageExecution(ctx, stage.id, duration_ms, ok, err)
|
||
|
||
if ok:
|
||
continue
|
||
|
||
match stage.failure:
|
||
case FailureStrategy.TERMINATE:
|
||
return False, err
|
||
case FailureStrategy.SKIP:
|
||
continue
|
||
case FailureStrategy.COMPENSATE:
|
||
return False, await self._runCompensate(ctx, stage.compensate, err)
|
||
case FailureStrategy.DEGRADE:
|
||
ctx.degraded = True
|
||
ctx.degraded_reason = err
|
||
continue
|
||
return True, None
|
||
|
||
async def _logStageExecution(
|
||
self,
|
||
ctx: Any,
|
||
stage_id: str,
|
||
duration_ms: float,
|
||
ok: bool,
|
||
err: Error | None,
|
||
) -> None:
|
||
"""输出阶段执行结构化日志(§11.2 / INV-10)。
|
||
|
||
必填字段:``trace_id`` / ``stage_id`` / ``duration_ms`` / ``status`` /
|
||
``error_type``(失败时)。日志端口为 None 或日志写入失败时不影响
|
||
管道执行(降级至 stderr)。
|
||
|
||
渠道语义字段(6-P0-04 / 6-P1-07 / 6-P2-02):从 ctx 读取并输出
|
||
``channel_type`` / ``account_id`` / ``agent_run_id`` / ``plugin_id`` /
|
||
``pairing_id`` / ``dm_decision`` / ``channel_format_spec`` /
|
||
``channel_context_note``(仅当值非 None 时输出)。当 ``channel_type``
|
||
与 ``account_id`` 均非 None 时派生 ``channel_id`` 并输出,
|
||
``channel_id`` 等价于 ``${channel_type}:${account_id}``。
|
||
|
||
参数:
|
||
ctx: 管道上下文(用于提取 trace_id 与渠道语义字段)。
|
||
stage_id: 阶段唯一标识。
|
||
duration_ms: 阶段执行耗时(毫秒)。
|
||
ok: 阶段是否成功。
|
||
err: 失败时的错误对象,成功时为 None。
|
||
"""
|
||
if self.logger is None:
|
||
return
|
||
trace_id = getattr(ctx, "trace_id", None)
|
||
# 收集必填字段
|
||
fields: dict[str, Any] = {
|
||
"trace_id": trace_id,
|
||
"pipeline": self.name,
|
||
"stage_id": stage_id,
|
||
"duration_ms": duration_ms,
|
||
"status": "ok" if ok else "failed",
|
||
}
|
||
# 收集渠道语义字段(仅当值非 None 时输出,6-P0-04 / 6-P1-07 / 6-P2-02)
|
||
channel_type = getattr(ctx, "channel_type", None)
|
||
account_id = getattr(ctx, "account_id", None)
|
||
agent_run_id = getattr(ctx, "agent_run_id", None)
|
||
plugin_id = getattr(ctx, "plugin_id", None)
|
||
pairing_id = getattr(ctx, "pairing_id", None)
|
||
dm_decision = getattr(ctx, "dm_decision", None)
|
||
channel_format_spec = getattr(ctx, "channel_format_spec", None)
|
||
channel_context_note = getattr(ctx, "channel_context_note", None)
|
||
if channel_type is not None:
|
||
fields["channel_type"] = channel_type
|
||
if account_id is not None:
|
||
fields["account_id"] = account_id
|
||
if agent_run_id is not None:
|
||
fields["agent_run_id"] = agent_run_id
|
||
if plugin_id is not None:
|
||
fields["plugin_id"] = plugin_id
|
||
if pairing_id is not None:
|
||
fields["pairing_id"] = pairing_id
|
||
if dm_decision is not None:
|
||
fields["dm_decision"] = dm_decision.value
|
||
if channel_format_spec is not None:
|
||
fields["channel_format_spec"] = channel_format_spec
|
||
if channel_context_note is not None:
|
||
fields["channel_context_note"] = channel_context_note
|
||
# 派生 channel_id,等价于 ${channel_type}:${account_id}
|
||
if channel_type is not None and account_id is not None:
|
||
fields["channel_id"] = f"{channel_type}:{account_id}"
|
||
if not ok:
|
||
fields["error_type"] = type(err).__name__ if err else None
|
||
try:
|
||
if ok:
|
||
await self.logger.info(
|
||
f"stage executed: {stage_id}",
|
||
**fields,
|
||
)
|
||
else:
|
||
await self.logger.warn(
|
||
f"stage failed: {stage_id}",
|
||
**fields,
|
||
)
|
||
except Exception as e:
|
||
sys.stderr.write(f"[trace_id={trace_id}] stage logging failed: {stage_id}: {e}\n")
|
||
|
||
async def _runCompensate(
|
||
self,
|
||
ctx: Any,
|
||
compensate_name: str | None,
|
||
original_error: Error | None,
|
||
) -> Error | None:
|
||
"""执行补偿阶段。
|
||
|
||
查找补偿阶段并执行。补偿阶段自身的异常 **不** 覆盖原错误,仅通过
|
||
日志端口记录(若注入),保留原始失败原因(§3.1 补偿执行约束)。
|
||
|
||
参数:
|
||
ctx: 管道上下文。
|
||
compensate_name: 补偿阶段名称,None 表示无补偿。
|
||
original_error: 原始失败错误。
|
||
|
||
返回:
|
||
原始失败错误(补偿不改变管道失败状态)。
|
||
"""
|
||
if compensate_name is None:
|
||
return original_error
|
||
compensate_stage = self._findStage(compensate_name)
|
||
if compensate_stage is None:
|
||
return original_error
|
||
try:
|
||
await compensate_stage.process(ctx)
|
||
except Exception as e:
|
||
trace_id = getattr(ctx, "trace_id", None)
|
||
if self.logger is not None:
|
||
try:
|
||
await self.logger.error(
|
||
f"compensate stage {compensate_name} failed: {e}",
|
||
trace_id=trace_id,
|
||
compensate_stage=compensate_name,
|
||
)
|
||
except Exception:
|
||
sys.stderr.write(f"[trace_id={trace_id}] compensate stage {compensate_name} failed: {e}\n")
|
||
else:
|
||
sys.stderr.write(f"[trace_id={trace_id}] compensate stage {compensate_name} failed: {e}\n")
|
||
return original_error
|
||
|
||
def _findStage(self, stage_id: str) -> AppStage | None:
|
||
"""按 id 查找阶段。
|
||
|
||
参数:
|
||
stage_id: 阶段唯一标识。
|
||
|
||
返回:
|
||
匹配的阶段实例,未找到时返回 None。
|
||
"""
|
||
for stage in self.stages:
|
||
if stage.id == stage_id:
|
||
return stage
|
||
return None
|