/** 工作流面板:侧边抽屉,列出全部工作流,选中后可提问 / 诊断 / 修复 / 启停。 */ import { useSyncExternalStore, useEffect, useCallback } from 'react' import { getState, subscribe, update, pushToast } from './store.js' import { api } from './api.js' function useWorkbench() { return useSyncExternalStore(subscribe, getState) } const STATUS_LABELS = { success: '成功', failed: '失败', running: '运行中', cancelled: '已停止', none: '未运行', } function statusOf(workflow) { if (workflow.active_run) return 'running' return workflow.last_run?.status ?? 'none' } function formatTime(value) { if (!value) return '—' const date = new Date(value) if (Number.isNaN(date.getTime())) return String(value) const pad = (n) => String(n).padStart(2, '0') return `${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}` } function formatDuration(seconds) { if (seconds == null) return '' if (seconds < 60) return `${seconds}s` return `${Math.floor(seconds / 60)}m${seconds % 60 ? `${seconds % 60}s` : ''}` } export async function refreshWorkflows() { update({ loading: true, error: null }) try { const payload = await api.listWorkflows() update({ loading: false, workflows: payload.workflows ?? [], summary: payload.summary ?? null, generatedAt: payload.generated_at ?? null, }) } catch (error) { update({ loading: false, error: error.message }) } } async function selectWorkflow(workflowId) { update({ selectedId: workflowId, detailLoading: true, diagnosis: null, runs: [] }) try { const [detail, runs] = await Promise.all([ api.workflowDetail(workflowId).catch(() => null), api.workflowRuns(workflowId, 6).catch(() => ({ runs: [] })), ]) update({ detailLoading: false, detail, runs: runs.runs ?? [] }) } catch (error) { update({ detailLoading: false, error: error.message }) } } async function loadDiagnosis(workflowId, runId) { update({ diagnosisLoading: true }) try { const payload = await api.diagnosis(workflowId, runId) update({ diagnosisLoading: false, diagnosis: payload }) } catch (error) { update({ diagnosisLoading: false }) pushToast({ kind: 'error', text: `诊断数据加载失败:${error.message}` }) } } async function doAsk(action) { const s = getState() if (!s.selectedId) return const question = s.question.trim() try { const payload = await api.ask({ workflow_id: s.selectedId, action, question, }) const label = { ask: '提问', diagnose: '诊断', repair: '修复' }[action] ?? '会话' pushToast({ kind: 'ok', text: `已创建${label}会话(${String(payload.session_id).slice(0, 18)}…),请在左侧会话列表中查看`, }) update({ question: '' }) } catch (error) { pushToast({ kind: 'error', text: `创建会话失败:${error.message}` }) } } async function doTrigger(execute) { const s = getState() if (!s.selectedId) return if (execute) { const ok = window.confirm( `正式执行 ${s.selectedId}(业务日期 ${s.businessDate})会对外部系统产生真实写入。确认继续?`, ) if (!ok) return } try { await api.trigger({ workflow_id: s.selectedId, business_date: s.businessDate, execute, confirmed: execute, }) pushToast({ kind: 'ok', text: execute ? '已发起正式执行' : '已发起试运行(无外部副作用)' }) setTimeout(refreshWorkflows, 1500) } catch (error) { pushToast({ kind: 'error', text: `启动失败:${error.message}` }) } } async function doCancel() { const s = getState() const active = s.detail?.workflow?.active_run if (!s.selectedId) return try { if (active?.operation_id) { await api.cancel({ workflow_id: s.selectedId, operation_id: active.operation_id }) } else { await api.cancel({ workflow_id: s.selectedId, scheduled: true }) } pushToast({ kind: 'ok', text: '已发送停止指令' }) setTimeout(refreshWorkflows, 1500) } catch (error) { pushToast({ kind: 'error', text: `停止失败:${error.message}` }) } } function SummaryPills({ summary, workflows }) { const failed = workflows.filter((w) => statusOf(w) === 'failed').length const running = workflows.filter((w) => w.active_run).length return (
{step.error}}
{log.tail}