feat(engine): node.finished 全事件补 attempt 审计字段(无人值守审批对账:对面 actionId=runId:stepId:attempt)

目标:审计侧可对账'第几次尝试成功/失败',为无人值守高危动作的审批绑定提供键
代码逻辑:六处 node.finished 发射点统一带 state.attempt(初值1,重试循环内递增);可选字段向后兼容,无事件对象精确断言受影响
验证:引擎域 24/24 全绿 + 新增契约测试(重试3次终态 attempt=3、跳过=1、全事件 ≥1);typecheck 归零
风险与回滚:低——纯新增可选字段,revert 即回
This commit is contained in:
yu committed 2026-11-06 17:21:45 +08:00
1 parent 10d125ce9d
commit 43ae705629
2 files changed
+36 -5

No files matched your search

+8 -5
View File
@@ -255,14 +255,14 @@ async function executeNode(node: FlowNode, ctx: RunContext, states: Map<string,
state.status = "failed";
state.error = error.toRunError();
state.finishedAt = new Date().toISOString();
ctx.emit({ type: "node.finished", nodeId: node.id, status: "failed", error: state.error, durationMs: 0, evidence: [] });
ctx.emit({ type: "node.finished", nodeId: node.id, status: "failed", error: state.error, durationMs: 0, evidence: [], attempt: state.attempt });
throw error;
}
if (node.enabled === false) {
state.status = "skipped";
state.finishedAt = new Date().toISOString();
ctx.emit({ type: "node.finished", nodeId: node.id, status: "skipped", durationMs: 0, evidence: [] });
ctx.emit({ type: "node.finished", nodeId: node.id, status: "skipped", durationMs: 0, evidence: [], attempt: state.attempt });
return undefined;
}
@@ -371,6 +371,7 @@ async function executeNode(node: FlowNode, ctx: RunContext, states: Map<string,
checkpointId: node.id,
resumable,
executor: "local",
attempt: state.attempt,
...(resumable ? { idempotencyKey: `${ctx.runId}:${node.id}` } : {})
});
return result;
@@ -379,7 +380,7 @@ async function executeNode(node: FlowNode, ctx: RunContext, states: Map<string,
state.status = "skipped";
state.finishedAt = new Date().toISOString();
state.durationMs = Date.now() - startMs;
ctx.emit({ type: "node.finished", nodeId: node.id, status: "skipped", durationMs: state.durationMs, evidence: [] });
ctx.emit({ type: "node.finished", nodeId: node.id, status: "skipped", durationMs: state.durationMs, evidence: [], attempt: state.attempt });
throw error;
}
const runtimeError = asRuntimeError(error, node, component.id);
@@ -395,7 +396,8 @@ async function executeNode(node: FlowNode, ctx: RunContext, states: Map<string,
status: "cancelled",
error: runtimeError,
durationMs: state.durationMs,
evidence: []
evidence: [],
attempt: state.attempt
});
throw error;
}
@@ -412,7 +414,8 @@ async function executeNode(node: FlowNode, ctx: RunContext, states: Map<string,
status: state.status,
error: runtimeError,
durationMs: state.durationMs,
evidence: []
evidence: [],
attempt: state.attempt
});
if (skip) return undefined;
throw error;