mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 00:31:58 +00:00
驳回节点的画法(前端):被驳回的那一步画成「被驳回」,不画成失败
服务端给被驳回的那一步的 node.failed 带上了 reason=rejected(上一个提交)。界面上读 node.failed 的几处都认它 (05-已知遗留 (a)、总方案 X9:驳回是人的决定,不是故障),没有 reason 的老运行照旧画失败: - run/decode:问数据过程、记录页、画布右栏的时间线里,这一步状态 rejected(StepStatus 加一种),副标题写谁驳回、 备注(REJECTED_TEXT.step),原话放展开里;不是 error 级别,不当成出错 - run/trace、run/derive:NodeState 加 rejected;那一段收成 rejected,不记成「失败的节点」,下游是「未到达」不是 「被阻断」;回放投影同样是 rejected - 画布节点卡:rejected 一种读数(耗时 + 原话第一句,整句悬停),中性色、停止剪影,不挂失败红框、不抖动;小地图、 运行胶囊的计数、时间轴泳道认得它;记录页航迹的节点卡写「被驳回:原话」 - check-decode、check-run-states 各加一段:新旧两种形状
This commit is contained in:
@@ -196,6 +196,8 @@ const MINI_FILL: Partial<Record<NodeState, string>> = {
|
||||
done: 'color-mix(in srgb, var(--st-done) 70%, transparent)',
|
||||
skipped: 'var(--st-skipped)',
|
||||
cancelled: 'var(--st-cancelled)',
|
||||
// 被驳回:人的决定,和取消同一种中性色(不画失败色)
|
||||
rejected: 'var(--st-cancelled)',
|
||||
suspended: 'var(--st-suspended)',
|
||||
blocked: 'var(--st-blocked)',
|
||||
unreached: 'color-mix(in srgb, var(--st-unreached) 45%, transparent)',
|
||||
|
||||
@@ -18,7 +18,7 @@ import { explainRunError } from '../lib/explain'
|
||||
import { formatDuration, formatLapse, formatNumber, formatTokens, NONE, shortId } from '../lib/format'
|
||||
import { statusMeta } from '../lib/status'
|
||||
import {
|
||||
CHALLENGE_TEXT, claimTally, DUAL_CHECK_TEXT, issuanceLabel, reportStampText, RUN_CLASS_LABEL, UPGRADE_POLICY_LABEL,
|
||||
CHALLENGE_TEXT, claimTally, DUAL_CHECK_TEXT, issuanceLabel, REJECTED_TEXT, reportStampText, RUN_CLASS_LABEL, UPGRADE_POLICY_LABEL,
|
||||
} from '../lib/terms'
|
||||
import { useReportStamp, type ReportStamp } from './reportStamp'
|
||||
import { ApprovalCard } from '../run/RunPanel'
|
||||
@@ -559,6 +559,14 @@ function readingOf({ state, view, type, config, skewMs, timed, fanIn, facts, sin
|
||||
noteTitle: why.title,
|
||||
}
|
||||
}
|
||||
case 'rejected':
|
||||
// 被人驳回(node.failed 带 reason=rejected):人的决定,不是失败。状态字已经写了「被驳回」,槽里写原话的第一句
|
||||
// (「人工驳回:数字对不上」),整句放悬停;不按失败去归类、讲处理建议
|
||||
return {
|
||||
main: formatDuration(n?.lastDurationMs ?? runtime?.durationMs),
|
||||
note: error ? firstSentence(error) : REJECTED_TEXT.card,
|
||||
noteTitle: error || undefined,
|
||||
}
|
||||
case 'queued':
|
||||
return { main: '', note: fanIn > 1 ? '等待其余上游完成' : '上游已交付,等待开始' }
|
||||
case 'skipped':
|
||||
|
||||
@@ -1443,6 +1443,8 @@
|
||||
background: color-mix(in srgb, var(--st-failed) 45%, transparent);
|
||||
}
|
||||
.tl-s-cancelled { --c: var(--st-cancelled); }
|
||||
/* 被驳回的那一段:人的决定,和取消同一种中性色 */
|
||||
.tl-s-rejected { --c: var(--st-cancelled); }
|
||||
.tl-s-suspended { --c: var(--st-suspended); }
|
||||
.tl-s-waiting { --c: var(--st-waiting); }
|
||||
.tl-s-waited { --c: var(--st-waiting); opacity: 0.8; }
|
||||
|
||||
@@ -830,6 +830,15 @@
|
||||
.node-cancelled .nc-slot {
|
||||
background: var(--st-cancelled);
|
||||
}
|
||||
/* 被人驳回的那一步(node.failed 带 reason=rejected):人的决定,不是故障。和取消同一种中性色,不画失败的红框、不抖动 */
|
||||
.nc.node-rejected {
|
||||
--st-c: var(--st-cancelled);
|
||||
border-color: color-mix(in srgb, var(--st-cancelled) 70%, var(--border));
|
||||
}
|
||||
.node-rejected .nc-slot {
|
||||
width: 5px;
|
||||
background: var(--st-cancelled);
|
||||
}
|
||||
.nc.node-suspended {
|
||||
--st-c: var(--st-suspended);
|
||||
border-color: color-mix(in srgb, var(--st-suspended) 70%, var(--border));
|
||||
|
||||
@@ -5439,6 +5439,18 @@ export const REJECTED_TEXT = {
|
||||
},
|
||||
/** 问数据:这一轮的运行被驳回结束时,补救区的那一句 */
|
||||
chatReason: '这一轮的运行被审批人驳回,已经结束。这是人的决定,不是故障;改好内容后可以重新运行。',
|
||||
/**
|
||||
* 被驳回的那一步自己(node.failed 带 reason=rejected):时间线、问数据过程里这一步的副标题。步骤名就是节点名,不再写一遍
|
||||
* 「在哪一步」;谁、备注照写,超时驳回写按设置驳回
|
||||
*/
|
||||
step: (d: { actor?: unknown; note?: unknown; decision?: unknown }): string => {
|
||||
const who = typeof d.actor === 'string' ? d.actor.trim() : ''
|
||||
const note = typeof d.note === 'string' ? d.note.trim() : ''
|
||||
if (d.decision === 'expired_reject') return '超过时限,按设置驳回'
|
||||
return `${who ? `${who} 驳回` : '被驳回'}${note ? `:${note}` : ''}`
|
||||
},
|
||||
/** 画布节点卡上被驳回的那一步:一句话,悬停写原话 */
|
||||
card: '被驳回',
|
||||
}
|
||||
|
||||
/** 来源标签:老运行没有来源时写「来源未记录」 */
|
||||
|
||||
@@ -612,6 +612,8 @@ function NodeCard({ id, trace, proj, state, graph, facts, at, labelOf, href, gon
|
||||
if (n?.takenHandle && (state === 'done' || state === 'failed')) rows.push(['出口', <span className="mono">{n.takenHandle}</span>])
|
||||
if (n?.reason) rows.push(['理由', n.reason])
|
||||
if (state === 'skipped' && n?.skippedReason) rows.push(['跳过', n.skippedReason])
|
||||
// 被人驳回的那一步(node.failed 带 reason=rejected):写原话(谁驳回、为什么),不按失败去讲原因和处理建议
|
||||
if (state === 'rejected' && n?.error) rows.push([REJECTED_TEXT.title, n.error])
|
||||
// 卡片说的是游标那一刻所在的那一次执行(和画布卡片同一个划分):循环上一轮、接着跑之前
|
||||
// 的事不挂到这一次干净的执行上;回放到更早的一次时,也不因为后面还有一件就看不到这一次的
|
||||
const since = execStart(n, at, facts)
|
||||
|
||||
@@ -458,7 +458,7 @@ function Cells({ cells, onPick }: { cells: RunGlance['cells']; onPick: (id: stri
|
||||
}
|
||||
|
||||
const COUNT_ORDER: NodeState[] = [
|
||||
'running', 'waiting', 'failed', 'suspended', 'done', 'queued', 'blocked', 'unreached', 'skipped', 'cancelled',
|
||||
'running', 'waiting', 'failed', 'rejected', 'suspended', 'done', 'queued', 'blocked', 'unreached', 'skipped', 'cancelled',
|
||||
]
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@@ -93,7 +93,8 @@ export type StepKind =
|
||||
* suspended:服务重启时还在跑的步骤,断点还在,可以接着跑。
|
||||
* skipped:skip_if 成立、这一步没有执行。和"根本没轮到"是两回事,得留痕。
|
||||
*/
|
||||
export type StepStatus = 'running' | 'done' | 'failed' | 'waiting' | 'cancelled' | 'suspended' | 'skipped'
|
||||
/** rejected:这一步被人驳回(node.failed 带 reason=rejected)。人的决定,不是失败:不画红、不算出错 */
|
||||
export type StepStatus = 'running' | 'done' | 'failed' | 'waiting' | 'cancelled' | 'suspended' | 'skipped' | 'rejected'
|
||||
|
||||
/** 节点的某一次执行。循环体、轮询类节点一次运行里会执行几十上百次 */
|
||||
export interface Exec {
|
||||
@@ -394,7 +395,7 @@ export function decodePhase(events: RunEvent[], final?: RunFinal): RunPhase {
|
||||
|
||||
const STEP_OF: Partial<Record<NodeState, StepStatus>> = {
|
||||
done: 'done', failed: 'failed', waiting: 'waiting', cancelled: 'cancelled',
|
||||
suspended: 'suspended', running: 'running', skipped: 'skipped',
|
||||
suspended: 'suspended', running: 'running', skipped: 'skipped', rejected: 'rejected',
|
||||
}
|
||||
const stepStatusOf = (state: NodeState): StepStatus => STEP_OF[state] ?? 'done'
|
||||
|
||||
@@ -2018,10 +2019,31 @@ export function decodeRun(events: RunEvent[], final?: RunFinal, opts?: DecodeOpt
|
||||
}
|
||||
|
||||
case 'node.failed': {
|
||||
// 节点失败时还在进行中的那次调用就是出错的那次,跟节点一起算失败,不留给清扫写成已取消
|
||||
closeLlm(nodeId, 'failed', at)
|
||||
const step = nodeId ? nodeSteps.get(nodeId) : undefined
|
||||
const ms = num(d.duration_ms)
|
||||
if (d.reason === 'rejected') {
|
||||
// 人的驳回(服务端给被驳回的那一步的 node.failed 带 reason=rejected,总方案 X9):这一步写「被驳回」,副标题写谁、
|
||||
// 备注,原话放展开里;不画红、不当成出错(老运行的 node.failed 没有 reason,照旧按失败画)
|
||||
closeLlm(nodeId, 'cancelled', at)
|
||||
const sub = REJECTED_TEXT.step(d)
|
||||
if (step) {
|
||||
const cur = execOf(nodeId)
|
||||
if (cur) { cur.status = 'rejected'; if (ms != null) cur.ms = ms }
|
||||
step.status = 'rejected'
|
||||
step.level = undefined
|
||||
step.sub = sub
|
||||
step.detail = String(d.error ?? '')
|
||||
step.ms = ms
|
||||
step.meta = dur(ms)
|
||||
} else {
|
||||
push({ id: `nr-${seq}`, seq, kind: 'lifecycle', status: 'rejected', title: REJECTED_TEXT.title, sub,
|
||||
...(d.error ? { detail: String(d.error) } : {}), nodeId }, nodeId)
|
||||
}
|
||||
trackTeam(event)
|
||||
break
|
||||
}
|
||||
// 节点失败时还在进行中的那次调用就是出错的那次,跟节点一起算失败,不留给清扫写成已取消
|
||||
closeLlm(nodeId, 'failed', at)
|
||||
// detail 是原始异常:给排查用,不给人读,折起来
|
||||
const raw = typeof d.detail === 'string' && d.detail ? d.detail : undefined
|
||||
if (step) {
|
||||
|
||||
@@ -224,7 +224,7 @@ export function heldEdgesOf(trace: Trace, graph: GraphLike): string[] {
|
||||
return out
|
||||
}
|
||||
|
||||
const STARTED = new Set<NodeState>(['running', 'waiting', 'done', 'failed', 'skipped', 'cancelled', 'suspended'])
|
||||
const STARTED = new Set<NodeState>(['running', 'waiting', 'done', 'failed', 'skipped', 'cancelled', 'suspended', 'rejected'])
|
||||
|
||||
/**
|
||||
* 这次运行实际走过的边:源放行过这条出口(循环里任何一轮都算),目标也确实开跑过。
|
||||
|
||||
@@ -21,9 +21,13 @@ import { applyDerived, type GraphLike } from './derive'
|
||||
export type RunPhase =
|
||||
| 'idle' | 'queued' | 'running' | 'waiting' | 'succeeded' | 'failed' | 'cancelled' | 'suspended'
|
||||
|
||||
/**
|
||||
* rejected:这一步被人驳回(node.failed 带 reason=rejected,总方案 X9)。是人的决定、不是故障:不画失败色,不算「失败于 X」,
|
||||
* 下游也不是「被阻断」(运行收成了 cancelled,没走到的照旧是未到达)。老运行的 node.failed 没有 reason,照旧是 failed
|
||||
*/
|
||||
export type NodeState =
|
||||
| 'idle' | 'queued' | 'running' | 'waiting' | 'done' | 'failed' | 'skipped'
|
||||
| 'cancelled' | 'suspended' | 'blocked' | 'unreached'
|
||||
| 'cancelled' | 'suspended' | 'blocked' | 'unreached' | 'rejected'
|
||||
|
||||
export interface Segment {
|
||||
start: number
|
||||
@@ -568,6 +572,8 @@ export function foldEvent(t: Trace, ev: RunEvent): Trace {
|
||||
if (!nodeId) break
|
||||
const n = node(nodeId)
|
||||
const failed = type === 'node.failed'
|
||||
// 被人驳回的那一步(服务端在 node.failed 上带 reason=rejected):这一段收成「被驳回」,不是失败
|
||||
const rejected = failed && d.reason === 'rejected'
|
||||
// 节点跑完了,决定已经生效(兜底 human.resolved 没发出来的老运行)
|
||||
if (!failed && next.book.asking[nodeId]) delete book().asking[nodeId]
|
||||
const ms = num(d.duration_ms)
|
||||
@@ -583,7 +589,7 @@ export function foldEvent(t: Trace, ev: RunEvent): Trace {
|
||||
if (open >= 0) {
|
||||
const seg = n.segments[open]
|
||||
if (!tsOk && ms != null) end = seg.start + ms
|
||||
patchSeg(n, open, { end, status: failed ? 'failed' : 'done' })
|
||||
patchSeg(n, open, { end, status: rejected ? 'rejected' : failed ? 'failed' : 'done' })
|
||||
}
|
||||
// 容器里还开着的成员、调度段:节点都结束了,它们不可能还在跑
|
||||
n.segments.forEach((s, i) => {
|
||||
@@ -597,10 +603,11 @@ export function foldEvent(t: Trace, ev: RunEvent): Trace {
|
||||
// 漏了 tool.end 的工具随节点一起收掉;记一条刻度,下一轮回放时不带着上一轮的「在跑」
|
||||
if (n.toolsRunning) { n.toolsRunning = 0; mark(n) }
|
||||
n.looping = looping
|
||||
n.state = failed ? 'failed' : looping ? 'running' : 'done'
|
||||
n.state = rejected ? 'rejected' : failed ? 'failed' : looping ? 'running' : 'done'
|
||||
if (failed) {
|
||||
// 驳回的原话照样留着(卡片、航迹悬停写得出是谁驳回、为什么),但它不是「最近一次失败」:不拿它当失败的节点
|
||||
n.error = String(d.error ?? '')
|
||||
book().lastFailed = nodeId
|
||||
if (!rejected) book().lastFailed = nodeId
|
||||
} else {
|
||||
n.attempt = num(d.attempt) ?? n.attempt
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ export function replayTraceCached(trace: Trace, at: number, graph: GraphLike): T
|
||||
}
|
||||
const state = p?.state ?? 'idle'
|
||||
// 人工审批的通过 / 驳回只记在节点上,不在段上:它结束了就用节点上的
|
||||
const finished = state === 'done' || state === 'failed'
|
||||
const finished = state === 'done' || state === 'failed' || state === 'rejected'
|
||||
const decided = taken.length ? taken : finished ? n.taken ?? [] : []
|
||||
nodes[id] = {
|
||||
...n, state, count: p?.count ?? 0, iteration: p?.iteration, taken: decided,
|
||||
|
||||
@@ -1506,7 +1506,8 @@ export interface UpgradeResult {
|
||||
/** 单个节点在一次运行中的实时状态,驱动画布上的高亮。 */
|
||||
export interface NodeRuntime {
|
||||
/** cancelled / suspended 是终态清扫时收的:运行被取消或服务重启时还没跑完的节点 */
|
||||
status: 'idle' | 'running' | 'done' | 'failed' | 'waiting' | 'skipped' | 'cancelled' | 'suspended'
|
||||
/** rejected:这一步被人驳回(node.failed 带 reason=rejected,run/trace 的 NodeState 同名) */
|
||||
status: 'idle' | 'running' | 'done' | 'failed' | 'waiting' | 'skipped' | 'cancelled' | 'suspended' | 'rejected'
|
||||
durationMs?: number
|
||||
preview?: any
|
||||
error?: string
|
||||
|
||||
@@ -2842,5 +2842,54 @@ await section('人的驳回 run.cancelled reason=rejected(05-已知遗留 (a)
|
||||
check('老事件(没有 reason)照旧「已取消」', legacy?.title === '已取消', legacy?.title)
|
||||
})
|
||||
|
||||
await section('被驳回的那一步:node.failed 带 reason=rejected 时画「被驳回」,不画成失败;没有 reason 的老运行照旧失败(05 (a)、X9)', async () => {
|
||||
const trace = await import(await load('/src/run/trace.ts'))
|
||||
const derive = await import(await load('/src/run/derive.ts'))
|
||||
const E = (seq, type, node_id, data = {}) => ({ seq, type, node_id, ts: seq, data })
|
||||
const start = [E(1, 'run.started', null, { nodes: 3 }), E(2, 'node.started', 'pay', { label: '付款' }),
|
||||
E(3, 'human.requested', 'pay', { title: '确认发送:付款' }), E(4, 'human.resolved', 'pay', { approved: false, note: '金额不对', actor: '张工' })]
|
||||
const rejectedFail = E(5, 'node.failed', 'pay', { error: '「付款」未确认(被驳回),没有发送:金额不对', duration_ms: 1200,
|
||||
reason: 'rejected', kind: 'action_confirm', decision: 'reject', actor: '张工', note: '金额不对' })
|
||||
const legacyFail = E(5, 'node.failed', 'pay', { error: '人工驳回:金额不对', duration_ms: 1200 })
|
||||
const end = E(6, 'run.cancelled', null, { reason: 'rejected', node_id: 'pay', label: '付款', kind: 'action_confirm',
|
||||
decision: 'reject', actor: '张工', note: '金额不对' })
|
||||
const node = (events) => flatten(mod.decodeRun(events)).find((s) => s.kind === 'node' && s.nodeId === 'pay')
|
||||
|
||||
const step = node([...start, rejectedFail, end])
|
||||
check('这一步写「被驳回」:状态 rejected,不是 failed、不是 error', step?.status === 'rejected' && step?.level !== 'error',
|
||||
`${step?.status} / ${step?.level}`)
|
||||
check('副标题写谁驳回、备注(步骤名就是节点名,不再写一遍)', step?.sub === '张工 驳回:金额不对', step?.sub)
|
||||
check('原话放展开里,一字不改', step?.detail === '「付款」未确认(被驳回),没有发送:金额不对', step?.detail)
|
||||
check('这一次执行也记成被驳回', step?.execs?.at(-1)?.status === 'rejected', step?.execs?.at(-1)?.status)
|
||||
const expired = node([...start, { ...rejectedFail, data: { ...rejectedFail.data, decision: 'expired_reject', actor: null,
|
||||
note: '超过时限,按设置驳回' } }, end])
|
||||
check('超时驳回:写按设置驳回,备注不重复', expired?.sub === '超过时限,按设置驳回', expired?.sub)
|
||||
const legacy = node([...start, legacyFail, end])
|
||||
check('老运行(node.failed 没有 reason)照旧画失败', legacy?.status === 'failed' && legacy?.level === 'error',
|
||||
`${legacy?.status} / ${legacy?.level}`)
|
||||
|
||||
// 航迹:节点状态是 rejected,不是「失败于 X」,下游是「未到达」不是「被阻断」
|
||||
const graph = { nodes: [{ id: 'start', type: 'input' }, { id: 'pay', type: 'tool' }, { id: 'out', type: 'output' }],
|
||||
edges: [{ source: 'start', target: 'pay' }, { source: 'pay', target: 'out' }] }
|
||||
const fold = (events) => derive.applyDerived(events.reduce(trace.foldEvent, trace.emptyTrace()), graph)
|
||||
// 航迹照真实的先后:停在确认上(run.interrupted)→ 人回复后恢复(run.resumed、重放的 node.started)→ 驳回
|
||||
const real = [E(1, 'run.started', null, { nodes: 3 }), E(2, 'node.started', 'pay', { label: '付款' }),
|
||||
E(3, 'human.requested', 'pay', { title: '确认发送:付款' }), E(4, 'run.interrupted', 'pay', { payload: { node_id: 'pay' } }),
|
||||
E(5, 'run.resumed', null, {}), E(6, 'node.started', 'pay', { label: '付款', resumed: true }),
|
||||
E(7, 'human.resolved', 'pay', { approved: false, note: '金额不对', actor: '张工' })]
|
||||
const at = (e, seq) => ({ ...e, seq, ts: seq })
|
||||
const t = fold([...real, at(rejectedFail, 8), at(end, 9)])
|
||||
check('航迹:被驳回的节点状态是 rejected', t.nodes.pay?.state === 'rejected', t.nodes.pay?.state)
|
||||
check('航迹:不记成失败的节点(没有「失败于 付款」)', t.failedNodeId === undefined, t.failedNodeId)
|
||||
check('航迹:那一段也收成 rejected', t.nodes.pay?.segments?.at(-1)?.status === 'rejected', t.nodes.pay?.segments?.at(-1)?.status)
|
||||
check('航迹:下游是「未到达」,不是「被阻断」', t.nodes.out?.state === 'unreached', t.nodes.out?.state)
|
||||
check('航迹:原话留着(卡片悬停写得出谁、为什么)', t.nodes.pay?.error === '「付款」未确认(被驳回),没有发送:金额不对')
|
||||
check('回放到驳回之后那一刻:投影也是 rejected', trace.project(t, 8500).nodes.pay?.state === 'rejected',
|
||||
trace.project(t, 8500).nodes.pay?.state)
|
||||
const old = fold([...real, at(legacyFail, 8), E(9, 'run.failed', null, { error: '人工驳回:金额不对', node_id: 'pay' })])
|
||||
check('老运行照旧:失败、失败于付款、下游被阻断', old.nodes.pay?.state === 'failed' && old.failedNodeId === 'pay'
|
||||
&& old.nodes.out?.state === 'blocked', `${old.nodes.pay?.state} / ${old.failedNodeId} / ${old.nodes.out?.state}`)
|
||||
})
|
||||
|
||||
console.log(failed ? `\n✗ ${failed} 项未通过` : '\n✓ 解码器全部通过')
|
||||
process.exit(failed ? 1 : 0)
|
||||
|
||||
@@ -1508,6 +1508,60 @@ await section('exits', async () => {
|
||||
}
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- 被驳回的那一步
|
||||
|
||||
await section('rejected', async () => {
|
||||
// 驳回是人的决定,不是故障(05 (a)、总方案 X9):被驳回的那一步节点层仍发 node.failed,但带 reason=rejected。画布上它画成
|
||||
// 「被驳回」——中性色、停止的剪影,不是失败的三角、红框、抖动;下游是「未到达」,不是「被阻断」;运行胶囊的计数写被驳回。
|
||||
// 没有 reason 的老运行照旧画失败
|
||||
console.log('\n=== 被驳回的那一步 ===')
|
||||
const REJECT_TAIL = [
|
||||
[3, 'human.resolved', 'human', { approved: false, note: '数字对不上', actor: '李工' }],
|
||||
[0.05, 'run.resumed', null, { actor: '李工' }],
|
||||
[0.05, 'node.started', 'human', { resumed: true }],
|
||||
[0.1, 'node.failed', 'human', { duration_ms: 40, error: '人工驳回:数字对不上', reason: 'rejected', kind: 'approve',
|
||||
decision: 'reject', actor: '李工', note: '数字对不上' }],
|
||||
[0.05, 'run.cancelled', null, { reason: 'rejected', node_id: 'human', label: '班长审批', kind: 'approve', decision: 'reject',
|
||||
actor: '李工', note: '数字对不上', timing: { wall_ms: 60000, active_ms: 40000, wait_ms: 20000 } }],
|
||||
]
|
||||
const REJECTED = script([...HEAD, ...TEAM_ROUND0, ...TEAM_DONE, ...LOOP_DONE, ...TO_GATE, ...REJECT_TAIL])
|
||||
const LEGACY = script([...HEAD, ...TEAM_ROUND0, ...TEAM_DONE, ...LOOP_DONE, ...TO_GATE,
|
||||
...REJECT_TAIL.slice(0, 3),
|
||||
[0.1, 'node.failed', 'human', { duration_ms: 40, error: '人工驳回:数字对不上' }],
|
||||
[0.05, 'run.failed', null, { error: '人工驳回:数字对不上', node_id: 'human', timing: { wall_ms: 60000, active_ms: 40000, wait_ms: 20000 } }]])
|
||||
const { browser, page, errors } = await open({ theme: 'dark' })
|
||||
await setLod(page, 'full')
|
||||
await feed(page, REJECTED(Date.now()))
|
||||
await page.waitForTimeout(400)
|
||||
const all = await cards(page)
|
||||
check('被驳回的那一步:卡片状态是 rejected', all.human?.state === 'rejected', all.human?.state)
|
||||
check('……剪影是停止(和取消同一个),不是失败的三角', all.human?.shape === 'stop', all.human?.shape)
|
||||
check('……槽里写「被驳回」和原话的第一句', /被驳回/.test(all.human?.text ?? '') && /数字对不上/.test(all.human?.text ?? ''),
|
||||
JSON.stringify(all.human?.text))
|
||||
check('……不写「失败」', !/失败/.test(all.human?.text ?? ''), JSON.stringify(all.human?.text))
|
||||
const look = await page.evaluate(() => {
|
||||
const card = document.querySelector('.react-flow__node[data-id="human"] .nc')
|
||||
const note = card?.querySelector('.nc-tele-note')
|
||||
return { failedClass: card?.classList.contains('node-failed'), rejectedClass: card?.classList.contains('node-rejected'),
|
||||
noteTitle: note?.getAttribute('title') ?? '' }
|
||||
})
|
||||
check('……不挂失败的红框(node-failed),挂 node-rejected', !look.failedClass && look.rejectedClass, JSON.stringify(look))
|
||||
check('……悬停写原话(谁驳回、为什么)', look.noteTitle.includes('数字对不上'), look.noteTitle)
|
||||
check('下游是「未到达」,不是「被阻断」', all.out?.state === 'unreached', all.out?.state)
|
||||
const failedId = await page.evaluate(() => window.__studio.getState().trace.failedNodeId ?? null)
|
||||
check('航迹里没有「失败的节点」', failedId === null, String(failedId))
|
||||
check('运行收成 cancelled(不是 failed)', await page.evaluate(() => window.__studio.getState().runPhase) === 'cancelled')
|
||||
shots.push(await shot(page, 'dark-rejected'))
|
||||
|
||||
await feed(page, LEGACY(Date.now()))
|
||||
await page.waitForTimeout(400)
|
||||
const old = await cards(page)
|
||||
check('老运行(node.failed 没有 reason)照旧画失败、下游被阻断', old.human?.state === 'failed' && old.out?.state === 'blocked',
|
||||
`${old.human?.state} / ${old.out?.state}`)
|
||||
check('没有运行时报错', errors.length === 0, errors.slice(0, 3).join(' | '))
|
||||
await browser.close()
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- 关掉动效
|
||||
|
||||
await section('reduced', async () => {
|
||||
|
||||
Reference in New Issue
Block a user