ForcePilot/backend/package/yuxi/channels/application/pipeline/base.py
Kris 8eead29de0 refactor: 批量清理冗余空行,优化部分枚举使用方式
1.  移除所有适配器文件中多余的空导入行
2.  调整ValidationError继承,移除不必要的ValueError继承
3.  修正多处ChannelType使用方式,从.value改为直接使用枚举实例
4.  优化飞书插件部分硬编码渠道类型为枚举实例
5.  更新wechat_ilink插件清单与适配器配置
6.  新增飞书目录适配器缓存清理支持判断与iLink生命周期适配器凭据轮换支持判断
7.  优化配置处理器历史查询逻辑,区分键不存在与无历史记录场景
2026-07-04 00:14:56 +08:00

321 lines
12 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.

"""管道基础设施。
定义应用层管道基类 ``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