ForcePilot/backend/package/yuxi/channels/application/pipeline/base.py
Kris 742299cb07 chore: 批量清理报告相关代码并完成多项功能迭代
本次提交包含多维度代码优化与功能增强:
1.  移除报告模块冗余导入与枚举,清理报表相关代码
2.  新增扫码登录支持方法与飞书适配器适配
3.  完善异常日志与健康检查信息
4.  扩展目录、配对管理、能力查询等接口
5.  优化出站管道与事务提交后钩子逻辑
6.  修复飞书消息解析与响应空值问题
7.  重构配置更新与服务账号创建逻辑
8.  统一传输错误分类契约与错误基类扩展
2026-07-06 20:49:35 +08:00

327 lines
13 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
# 记录异常 message 与 details便于排查 stage 失败根因。
# 此前仅记录 error_type类名导致日志中看不到具体异常原因。
if err is not None:
fields["error_message"] = err.message
if err.details:
fields["error_details"] = err.details
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