2026-07-02 03:22:12 +08:00
|
|
|
|
"""管道基础设施。
|
|
|
|
|
|
|
|
|
|
|
|
定义应用层管道基类 ``Pipeline`` 与阶段协议 ``AppStage``,提供阶段顺序执行、
|
|
|
|
|
|
条件跳过、失败策略处理与补偿执行能力。``FailureStrategy`` 从契约层复用,
|
|
|
|
|
|
不重新定义,保证失败语义在全链路一致。
|
|
|
|
|
|
|
|
|
|
|
|
可观测性(§11.2 / INV-10):``Pipeline.run()`` 在每个阶段执行后输出结构化
|
|
|
|
|
|
日志,必填字段 ``trace_id`` / ``stage_id`` / ``duration_ms`` / ``status`` /
|
|
|
|
|
|
``error_type``(失败时),支持阶段延迟分布与成功率度量。
|
2026-07-09 13:51:58 +08:00
|
|
|
|
|
|
|
|
|
|
协程取消清理(C-O2):``Pipeline.run()`` 在阶段循环外包裹 ``try/finally``,
|
|
|
|
|
|
finally 块执行带 ``cleanup=True`` 标记的阶段,确保协程被取消时流式会话与
|
|
|
|
|
|
输入指示资源被释放。``asyncio.CancelledError`` 不被 ``except Exception``
|
|
|
|
|
|
捕获(Python 3.8+ 继承 ``BaseException``),finally 是唯一可靠的清理点。
|
2026-07-02 03:22:12 +08:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
2026-07-09 13:51:58 +08:00
|
|
|
|
import asyncio
|
2026-07-02 03:22:12 +08:00
|
|
|
|
import sys
|
|
|
|
|
|
import time
|
|
|
|
|
|
from collections.abc import Callable
|
|
|
|
|
|
from typing import Any, Protocol, runtime_checkable
|
|
|
|
|
|
|
2026-07-09 04:21:28 +08:00
|
|
|
|
from yuxi.channels.contract.dtos.trace import Span
|
2026-07-02 03:22:12 +08:00
|
|
|
|
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
|
2026-07-09 04:21:28 +08:00
|
|
|
|
from yuxi.channels.contract.ports.driven.tracer_port import TracerPort
|
2026-07-02 03:22:12 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@runtime_checkable
|
|
|
|
|
|
class AppStage(Protocol):
|
|
|
|
|
|
"""应用层管道阶段协议。
|
|
|
|
|
|
|
|
|
|
|
|
对齐契约层 ``Stage`` Protocol 并扩展:
|
|
|
|
|
|
- ``compensate`` 由 ``bool`` 调整为 ``str | None``,携带补偿阶段名称
|
|
|
|
|
|
(如 "outbox-rollback"),None 表示无补偿;
|
|
|
|
|
|
- 新增 ``condition`` 字段,支持基于上下文的阶段执行条件,None 表示
|
2026-07-09 13:51:58 +08:00
|
|
|
|
无条件执行;
|
|
|
|
|
|
- 新增 ``cleanup`` 字段(C-O2),标记为清理阶段的阶段会在 ``Pipeline.run``
|
|
|
|
|
|
的 finally 块中被重新执行,用于协程取消时释放资源(如流式会话)。
|
|
|
|
|
|
默认 False,未标记的阶段不参与清理。
|
2026-07-02 03:22:12 +08:00
|
|
|
|
|
|
|
|
|
|
阶段处理方法 ``process`` 返回 ``bool``:True 表示成功,False 或抛出
|
|
|
|
|
|
异常表示失败。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
id: str
|
|
|
|
|
|
reads: tuple[str, ...]
|
|
|
|
|
|
writes: tuple[str, ...]
|
|
|
|
|
|
idempotent: bool
|
|
|
|
|
|
thread_safe: bool
|
|
|
|
|
|
failure: FailureStrategy
|
|
|
|
|
|
compensate: str | None
|
2026-07-09 13:51:58 +08:00
|
|
|
|
cleanup: bool
|
2026-07-02 03:22:12 +08:00
|
|
|
|
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
|
2026-07-09 04:21:28 +08:00
|
|
|
|
tracer_port: TracerPort | None
|
2026-07-02 03:22:12 +08:00
|
|
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
|
|
self,
|
|
|
|
|
|
name: str,
|
|
|
|
|
|
stages: list[AppStage],
|
|
|
|
|
|
logger: LoggerPort | None = None,
|
2026-07-09 04:21:28 +08:00
|
|
|
|
tracer_port: TracerPort | None = None,
|
2026-07-02 03:22:12 +08:00
|
|
|
|
) -> None:
|
|
|
|
|
|
"""初始化管道。
|
|
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
|
name: 管道名称(如 "inbound" / "outbound" / "control-plane")。
|
|
|
|
|
|
stages: 阶段列表,按列表顺序执行。
|
|
|
|
|
|
logger: 可选日志端口,用于记录补偿阶段异常,None 时不记录。
|
2026-07-09 04:21:28 +08:00
|
|
|
|
tracer_port: 可选追踪端口,用于为每个 stage 包裹独立 span
|
|
|
|
|
|
(H-4 / §11.2),None 时不生成 span(向后兼容)。
|
2026-07-02 03:22:12 +08:00
|
|
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
|
PipelineConfigError: 任一阶段的 ``compensate`` 字段指向不存在
|
|
|
|
|
|
的阶段 ID。构造期 fail-fast,避免运行期 ``_runCompensate``
|
|
|
|
|
|
按名称查找静默失败(§3.1 补偿执行约束)。
|
|
|
|
|
|
"""
|
|
|
|
|
|
self.name = name
|
|
|
|
|
|
self.stages = stages
|
|
|
|
|
|
self.logger = logger
|
2026-07-09 04:21:28 +08:00
|
|
|
|
self.tracer_port = tracer_port
|
2026-07-02 03:22:12 +08:00
|
|
|
|
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``(失败时),日志失败不影响管道执行。
|
|
|
|
|
|
|
2026-07-09 13:51:58 +08:00
|
|
|
|
协程取消清理(C-O2):阶段循环包裹在 ``try/finally`` 中。
|
|
|
|
|
|
``asyncio.CancelledError`` 继承 ``BaseException``(Python 3.8+),
|
|
|
|
|
|
不被 ``except Error`` / ``except Exception`` 捕获,直接触发 finally。
|
|
|
|
|
|
finally 块执行带 ``cleanup=True`` 标记的阶段(如 typing-stop),
|
|
|
|
|
|
释放流式会话与输入指示资源。清理阶段异常仅 warn 日志,不掩盖原始
|
|
|
|
|
|
``CancelledError``;清理过程中再次被取消时捕获并继续剩余清理,
|
|
|
|
|
|
最终重新抛出 ``CancelledError`` 以保留取消语义。
|
|
|
|
|
|
|
2026-07-02 03:22:12 +08:00
|
|
|
|
参数:
|
|
|
|
|
|
ctx: 管道上下文。
|
|
|
|
|
|
|
|
|
|
|
|
返回:
|
|
|
|
|
|
元组 (success, error)。全部阶段成功时 success 为 True、
|
|
|
|
|
|
error 为 None;否则 success 为 False、error 携带失败原因。
|
2026-07-09 13:51:58 +08:00
|
|
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
|
asyncio.CancelledError: 协程被取消时,清理完成后重新抛出。
|
2026-07-02 03:22:12 +08:00
|
|
|
|
"""
|
2026-07-09 13:51:58 +08:00
|
|
|
|
try:
|
|
|
|
|
|
for stage in self.stages:
|
|
|
|
|
|
if stage.condition is not None and not stage.condition(ctx):
|
|
|
|
|
|
continue
|
2026-07-02 03:22:12 +08:00
|
|
|
|
|
2026-07-09 13:51:58 +08:00
|
|
|
|
ok = False
|
|
|
|
|
|
err: Error | None = None
|
|
|
|
|
|
trace_id = getattr(ctx, "trace_id", None)
|
|
|
|
|
|
start = time.monotonic()
|
|
|
|
|
|
# H-4 / §11.2:为每个 stage 包裹独立 span,tracer_port 为 None 时
|
|
|
|
|
|
# 跳过(向后兼容)。span 名格式 {pipeline_name}.{stage_id},使运维
|
|
|
|
|
|
# 可定位慢调用发生在哪个 stage。
|
|
|
|
|
|
span: Span | None = None
|
|
|
|
|
|
if self.tracer_port is not None:
|
|
|
|
|
|
# M-11: tracer 失败不得中断管道主链路,降级为无 span 执行
|
2026-07-09 04:21:28 +08:00
|
|
|
|
try:
|
2026-07-09 13:51:58 +08:00
|
|
|
|
span = await self.tracer_port.startSpan(f"{self.name}.{stage.id}", trace_id=trace_id)
|
2026-07-09 04:21:28 +08:00
|
|
|
|
except Exception as exc:
|
2026-07-09 13:51:58 +08:00
|
|
|
|
span = None
|
2026-07-09 04:21:28 +08:00
|
|
|
|
try:
|
|
|
|
|
|
if self.logger is not None:
|
|
|
|
|
|
await self.logger.warn(
|
2026-07-09 13:51:58 +08:00
|
|
|
|
f"tracer startSpan failed: {stage.id}",
|
2026-07-09 04:21:28 +08:00
|
|
|
|
trace_id=trace_id,
|
|
|
|
|
|
stage_id=stage.id,
|
|
|
|
|
|
error=str(exc),
|
|
|
|
|
|
)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
2026-07-09 13:51:58 +08:00
|
|
|
|
try:
|
|
|
|
|
|
ok = await stage.process(ctx)
|
|
|
|
|
|
except Error as e:
|
|
|
|
|
|
err = e
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
err = InternalError(
|
|
|
|
|
|
trace_id=trace_id,
|
|
|
|
|
|
message=f"stage '{stage.id}' raised native exception: {type(e).__name__}: {e}",
|
|
|
|
|
|
cause=e,
|
|
|
|
|
|
)
|
|
|
|
|
|
finally:
|
|
|
|
|
|
if span is not None:
|
|
|
|
|
|
# M-11: endSpan 失败不得中断管道主链路,降级记录日志
|
|
|
|
|
|
try:
|
|
|
|
|
|
await self.tracer_port.endSpan(span, status="ok" if ok else "error")
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
try:
|
|
|
|
|
|
if self.logger is not None:
|
|
|
|
|
|
await self.logger.warn(
|
|
|
|
|
|
f"tracer endSpan failed: {stage.id}",
|
|
|
|
|
|
trace_id=trace_id,
|
|
|
|
|
|
stage_id=stage.id,
|
|
|
|
|
|
error=str(exc),
|
|
|
|
|
|
)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
duration_ms = (time.monotonic() - start) * 1000
|
|
|
|
|
|
|
|
|
|
|
|
# 结构化可观测性日志(§11.2 / INV-10)
|
|
|
|
|
|
await self._logStageExecution(ctx, stage.id, duration_ms, ok, err)
|
|
|
|
|
|
|
|
|
|
|
|
if ok:
|
|
|
|
|
|
continue
|
2026-07-02 03:22:12 +08:00
|
|
|
|
|
2026-07-09 13:51:58 +08:00
|
|
|
|
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
|
|
|
|
|
|
finally:
|
|
|
|
|
|
# C-O2:协程取消清理。CancelledError 不被上方 except 捕获,
|
|
|
|
|
|
# 直接触发本 finally。仅当流式资源已启动时执行清理阶段。
|
|
|
|
|
|
await self._runCleanup(ctx)
|
|
|
|
|
|
|
|
|
|
|
|
async def _runCleanup(self, ctx: Any) -> None:
|
|
|
|
|
|
"""执行清理阶段(C-O2)。
|
|
|
|
|
|
|
|
|
|
|
|
在 ``Pipeline.run`` 的 finally 块中调用,查找并执行带
|
|
|
|
|
|
``cleanup=True`` 标记的阶段(如 typing-stop),释放流式会话与
|
|
|
|
|
|
输入指示资源。
|
|
|
|
|
|
|
|
|
|
|
|
清理策略:
|
|
|
|
|
|
- 仅当上下文 ``streaming_started`` 或 ``typing_started`` 为 True
|
|
|
|
|
|
时执行(无流式资源启动时跳过,避免无谓工作);
|
|
|
|
|
|
- 清理阶段异常仅 warn 日志,不掩盖原始异常(CancelledError 或
|
|
|
|
|
|
管道返回的错误);
|
|
|
|
|
|
- 清理过程中若再次被取消(``CancelledError``),捕获并继续剩余
|
|
|
|
|
|
清理阶段,最终重新抛出 ``CancelledError`` 以保留取消语义。
|
2026-07-02 03:22:12 +08:00
|
|
|
|
|
2026-07-09 13:51:58 +08:00
|
|
|
|
参数:
|
|
|
|
|
|
ctx: 管道上下文。
|
|
|
|
|
|
"""
|
|
|
|
|
|
# 仅当流式资源已启动时执行清理,避免无谓工作
|
|
|
|
|
|
streaming_started = getattr(ctx, "streaming_started", False)
|
|
|
|
|
|
typing_started = getattr(ctx, "typing_started", False)
|
|
|
|
|
|
if not (streaming_started or typing_started):
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
trace_id = getattr(ctx, "trace_id", None)
|
|
|
|
|
|
cleanup_cancelled = False
|
|
|
|
|
|
for stage in self.stages:
|
|
|
|
|
|
# 严格匹配 True,避免 MagicMock 桩的 truthy 属性误判为清理阶段
|
|
|
|
|
|
if getattr(stage, "cleanup", False) is not True:
|
2026-07-02 03:22:12 +08:00
|
|
|
|
continue
|
2026-07-09 13:51:58 +08:00
|
|
|
|
if stage.condition is not None and not stage.condition(ctx):
|
|
|
|
|
|
continue
|
|
|
|
|
|
try:
|
|
|
|
|
|
await stage.process(ctx)
|
|
|
|
|
|
except asyncio.CancelledError:
|
|
|
|
|
|
# 清理过程中被取消:记录并继续剩余清理阶段,最终重新抛出
|
|
|
|
|
|
cleanup_cancelled = True
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
# 清理阶段异常仅 warn 日志,不掩盖原始异常
|
|
|
|
|
|
if self.logger is not None:
|
|
|
|
|
|
try:
|
|
|
|
|
|
await self.logger.warn(
|
|
|
|
|
|
f"cleanup stage {stage.id} failed: {exc}",
|
|
|
|
|
|
trace_id=trace_id,
|
|
|
|
|
|
stage_id=stage.id,
|
|
|
|
|
|
)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
if cleanup_cancelled:
|
|
|
|
|
|
raise asyncio.CancelledError()
|
2026-07-02 03:22:12 +08:00
|
|
|
|
|
|
|
|
|
|
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:
|
2026-07-04 00:14:56 +08:00
|
|
|
|
fields["channel_type"] = channel_type
|
2026-07-02 03:22:12 +08:00
|
|
|
|
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:
|
2026-07-04 00:14:56 +08:00
|
|
|
|
fields["channel_id"] = f"{channel_type}:{account_id}"
|
2026-07-02 03:22:12 +08:00
|
|
|
|
if not ok:
|
|
|
|
|
|
fields["error_type"] = type(err).__name__ if err else None
|
2026-07-06 20:49:35 +08:00
|
|
|
|
# 记录异常 message 与 details,便于排查 stage 失败根因。
|
|
|
|
|
|
# 此前仅记录 error_type(类名),导致日志中看不到具体异常原因。
|
|
|
|
|
|
if err is not None:
|
|
|
|
|
|
fields["error_message"] = err.message
|
|
|
|
|
|
if err.details:
|
|
|
|
|
|
fields["error_details"] = err.details
|
2026-07-02 03:22:12 +08:00
|
|
|
|
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
|