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 映射保持一致) */ const PHASE_META: Record = { 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: '阶段完成' }, task_failed: { icon: '⚠️', label: '任务异常' }, }; /** 收口型 phase:不追加新节点,仅把当前 running 条目置为 completed */ const CLOSE_PHASES = new Set(['step_completed', 'stage_completed']); 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 base = phase.message?.trim() || meta.label; const label = decorateLabel(base, phase); const key = stepKeyOf(phase); // 已存在的条目:仅刷新文案(同阶段/同步骤进度更新) const existingIdx = strip.findIndex((e) => e.step === key); if (existingIdx >= 0) { return strip.map((e, i) => (i === existingIdx ? { ...e, label } : e)); } // 收口型 phase:running 条目置 completed,不追加新节点 if (CLOSE_PHASES.has(phase.phase_id)) { return strip.map((e) => (e.status === 'running' ? { ...e, status: 'completed' as const } : e)); } // 新 phase:先行条目全部置 completed,追加当前节点为 running const settled = strip.map((e) => e.status === 'running' ? { ...e, status: 'completed' as const } : e ); return [ ...settled, { step: key, icon: meta.icon, label, status: 'running' as const }, ]; }