Files
Agent-System/web/src/engine/phaseEventMapper.ts
T
weichen 3d6dcc3ae5 feat(web): 用小队子页面展示思考与工具过程
气泡只聚合过程摘要并链到 hash 子页面;成员编制来自只读 squad 端点,思考与工具默认折叠。
2026-09-28 10:30:53 +00:00

127 lines
5.5 KiB
TypeScript
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.
import type { PhaseEvent } from '../types/workbench';
/** 后端 phase 事件载荷(与 StreamEvent['phase'] 结构兼容,保持 mapper 独立可测) */
export type PhaseEventPayload = {
phase_id?: string;
message?: string;
role?: string;
step_index?: number;
total_steps?: number;
progress?: number;
};
/** phase_id → 图标/短标签映射(与 bizdev-cli/main.go 的 CLI 映射保持一致) */
export const PHASE_META: Record<string, { icon: string; label: string }> = {
squad_assembly: { icon: '👥', label: '组建小队' },
knowledge_retrieval: { icon: '📚', label: '知识检索' },
knowledge_retrieved: { icon: '📚', label: '知识命中' },
step_executing: { icon: '⚙️', label: '专员执行' },
step_completed: { icon: '✅', label: '步骤完成' },
quality_gate: { icon: '🛡️', label: '质量门禁' },
advisor_briefing: { icon: '💡', label: '参谋简报' },
stage_transition: { icon: '🚀', label: '阶段跃迁' },
stage_started: { icon: '🚀', label: '阶段启动' },
stage_completed: { icon: '🎉', label: '阶段完成' },
step_failed: { icon: '⚠️', label: '步骤异常' },
task_failed: { icon: '⚠️', label: '任务异常' },
document_parsing: { icon: '📄', label: '文档解析' },
document_summarizing: { icon: '📝', label: '正文总结' },
};
/** 收口型 phase:不追加新节点,仅把当前 running 条目置为 completed */
const CLOSE_PHASES = new Set(['step_completed', 'stage_completed']);
/** 从 step key 还原 phase_id(去掉 "#<idx>" 后缀) */
function phaseIdOfStep(step: string): string {
const idx = step.indexOf('#');
return idx >= 0 ? step.slice(0, idx) : step;
}
/**
* settleStep 把处于 completed 终态的条目标签归一化为静态措辞(短标签 + "已完成"),
* 避免收口后 label 冻结成"正在…(NN%)"进度中叙述。
* 与后端 taskrun.NormalizeSettledStep 语义对齐:failed 有意保留原文案(错误气泡承载细节),
* pending / running 不改动。
*/
export function settleStep(e: PhaseEvent): PhaseEvent {
if (e.status !== 'completed') return e;
const short = PHASE_META[phaseIdOfStep(e.step)]?.label ?? '任务推进';
// 保留"(第 x/y 步)"结构后缀(多步任务需区分进步骤),丢弃百分比与进度中叙述。
const idxSuffix = e.label.match(/(第 \d+\/\d+ 步)/)?.[0] ?? '';
const label = short + idxSuffix + '已完成';
if (e.label === label) return e; // 幂等
return { ...e, label };
}
function decorateLabel(base: string, phase: PhaseEventPayload): string {
const { step_index, total_steps, progress } = phase;
if (step_index && step_index > 0 && total_steps && total_steps > 0) {
return `${base}(第 ${step_index}/${total_steps} 步)`;
}
if (progress && progress > 0) {
return `${base}(${progress}%)`;
}
return base;
}
/** 条目 key:同 phase 且无有效 step_index 时刷新文案;带 step_index 的重复 phase 各占一节点 */
function stepKeyOf(phase: PhaseEventPayload): string {
const idx = phase.step_index ?? 0;
return idx > 0 ? `${phase.phase_id}#${idx}` : `${phase.phase_id}`;
}
/**
* applyPhaseEvent 把一条后端 phase 事件折叠进 PhaseEventStrip 序列(纯函数)。
* 进度求真:仅由真实 phase 事件驱动,无任何人为进度推演。
*/
export function applyPhaseEvent(strip: PhaseEvent[], phase?: PhaseEventPayload): PhaseEvent[] {
if (!phase?.phase_id) return strip;
const meta = PHASE_META[phase.phase_id] ?? { icon: '⏳', label: '任务推进' };
// 进度求真:优先展示后端下发的真实进度文案,短标签仅作缺省
const detail = phase.message?.trim() || undefined;
const base = detail || meta.label;
const label = decorateLabel(base, phase);
const key = stepKeyOf(phase);
const role = phase.role?.trim() || undefined;
// 已存在的条目:刷新文案;非失败事件再次到达时,failed 条目恢复 running(重试/续跑重新执行该步骤)
const existingIdx = strip.findIndex((e) => e.step === key);
if (existingIdx >= 0) {
return strip.map((e, i) => {
if (i !== existingIdx) return e;
const nextRole = role || e.role;
if (e.status === 'failed' && phase.phase_id !== 'step_failed') {
return { ...e, label, detail, role: nextRole, status: 'running' as const };
}
// 已收口条目被重复事件刷新文案时,保持 label 与 status 语义一致。
return settleStep({ ...e, label, detail, role: nextRole });
});
}
// 步骤失败:把当前 running 条目置为 failed(红色视觉态);无 running 条目时追加失败节点
if (phase.phase_id === 'step_failed') {
if (strip.some((e) => e.status === 'running')) {
return strip.map((e) =>
e.status === 'running'
? { ...e, status: 'failed' as const, detail: detail ?? e.detail, role: role || e.role }
: e,
);
}
return [...strip, { step: key, icon: meta.icon, label, detail, role, status: 'failed' as const }];
}
// 收口型 phase:running 条目置 completed,不追加新节点
if (CLOSE_PHASES.has(phase.phase_id)) {
return strip.map((e) => (e.status === 'running' ? settleStep({ ...e, status: 'completed' as const }) : e));
}
// 新 phase:先行条目全部置 completed,追加当前节点为 running
const settled = strip.map((e) =>
e.status === 'running' ? settleStep({ ...e, status: 'completed' as const }) : e
);
return [
...settled,
{ step: key, icon: meta.icon, label, detail, role, status: 'running' as const },
];
}