import { useState, useEffect, useCallback, useRef } from 'react'; import GlassCard from '@/components/digital/GlassCard'; import CardTitleBar from '@/components/digital/CardTitleBar'; import MissionCard from '@/components/digital/MissionCard'; import FlowDetailPanel from '@/components/digital/FlowDetailPanel'; import { adaptMissions, type Mission } from '@/lib/consoleDataAdapter'; import { getDebugPlans, governanceCommand, searchDebugPlans } from '@/lib/api'; import type { GovernanceCommandKind, GovernanceCommandResponse } from '@/types/api'; import { t } from '@/lib/i18n'; import { completeDemoMissionOnce, getDemoMissions, runDemoMissionOnce, } from './demoScenario'; import { domSafeId } from './navigation'; const REQUIRED_MISSION_IDS = [ 'qimingclaw-client-readiness', 'qimingclaw-remote-task-ingress', 'qimingclaw-daily-report', ]; const DELEGATION_FILTERS = [ { key: 'all', label: '全部委托' }, { key: 'daily', label: '日常任务' }, { key: 'weekly', label: '周任务' }, { key: 'monthly', label: '月度任务' }, { key: 'adhoc', label: '临时任务' }, ]; type MissionNotice = { kind: 'selected' | 'created' | 'dispatched'; missionId: string; title: string; planId?: string; }; function planSortTime(plan: Record): number { const value = typeof plan.updated_at === 'string' ? plan.updated_at : typeof plan.created_at === 'string' ? plan.created_at : ''; const time = value ? new Date(value).getTime() : 0; return Number.isFinite(time) ? time : 0; } function dateKey(value?: string | null): string { if (!value) return ''; const date = new Date(value); if (Number.isNaN(date.getTime())) return ''; return [ date.getFullYear(), String(date.getMonth() + 1).padStart(2, '0'), String(date.getDate()).padStart(2, '0'), ].join('-'); } function todayDateKey(): string { return dateKey(new Date().toISOString()); } function shiftDateKey(value: string, days: number): string { const date = value ? new Date(`${value}T12:00:00`) : new Date(); if (Number.isNaN(date.getTime())) return todayDateKey(); date.setDate(date.getDate() + days); return dateKey(date.toISOString()); } function plannedClockFromMission(mission: Mission): { text: string; minutes: number } | null { const match = `${mission.title} ${mission.id}`.match(/(?:^|\D)(\d{1,2})[::](\d{2})(?:\D|$)/); if (!match) return null; const hour = Number(match[1]); const minute = Number(match[2]); if (!Number.isInteger(hour) || !Number.isInteger(minute) || hour > 23 || minute > 59) return null; return { text: `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`, minutes: hour * 60 + minute, }; } function missionTaskTimes(mission: Mission): string[] { return mission.steps.flatMap((step) => ( step.tasks.flatMap((task) => [task.startedAt, task.finishedAt]) )).filter((value): value is string => Boolean(value)); } function missionMatchesDate(mission: Mission, selectedDate: string): boolean { const taskDates = missionTaskTimes(mission).map(dateKey).filter(Boolean); if (taskDates.includes(selectedDate)) return true; if (plannedClockFromMission(mission)) return true; return [mission.updatedAt, mission.createdAt].map(dateKey).includes(selectedDate); } function missionDateKey(mission: Mission): string { const planned = plannedClockFromMission(mission); if (!planned) return mission.id; return `${mission.title}`.trim().toLowerCase() || mission.id; } function missionHasActualWorkOnDate(mission: Mission, selectedDate: string): boolean { return missionTaskTimes(mission).some((value) => dateKey(value) === selectedDate); } function missionDedupScore(mission: Mission, selectedDate: string): number { const actualScore = missionHasActualWorkOnDate(mission, selectedDate) ? 10_000 : 0; const requiredScore = REQUIRED_MISSION_IDS.includes(mission.id) ? 1_000 : 0; const selectedSyncScore = [mission.updatedAt, mission.createdAt].map(dateKey).includes(selectedDate) ? 100 : 0; return actualScore + requiredScore + selectedSyncScore + planSortTime({ updated_at: mission.updatedAt || '', created_at: mission.createdAt || '', }) / 10_000_000_000_000; } function dedupeMissionsForDate(missions: Mission[], selectedDate: string): Mission[] { const byKey = new Map(); for (const mission of missions) { const key = missionDateKey(mission); const previous = byKey.get(key); if (!previous || missionDedupScore(mission, selectedDate) > missionDedupScore(previous, selectedDate)) { byKey.set(key, mission); } } return Array.from(byKey.values()); } function missionDateSortValue(mission: Mission, selectedDate: string): number { const planned = plannedClockFromMission(mission); if (planned) return planned.minutes; const taskTimes = missionTaskTimes(mission) .filter((value) => dateKey(value) === selectedDate) .sort(); if (taskTimes[0]) { const date = new Date(taskTimes[0]); return date.getHours() * 60 + date.getMinutes(); } return 24 * 60; } function missionDelegationGroup(mission: Mission): string { const text = `${mission.id} ${mission.title} ${mission.objective}`.toLowerCase(); if (text.includes('month') || text.includes('monthly') || text.includes('月')) return 'monthly'; if (text.includes('week') || text.includes('weekly') || text.includes('周')) return 'weekly'; if (text.includes('临时') || text.includes('adhoc') || text.includes('hotlist') || text.includes('monitor')) return 'adhoc'; return 'daily'; } async function sendMissionGovernanceCommand( commandKind: GovernanceCommandKind, contextRefs: Record, payload: Record = {}, ): Promise { const stamp = Date.now(); const response = await governanceCommand({ command_kind: commandKind, context_refs: contextRefs, payload: { source: 'digital-mission-center', ...payload }, correlation_id: `mission-${commandKind}-${stamp}`, idempotency_key: `mission-${commandKind}-${Object.values(contextRefs).join('-') || 'new'}-${stamp}`, }); if (!response.accepted) { throw new Error(response.message || `command 未被接受:${response.command_id || commandKind}`); } return response; } async function sendMissionPlanCommand( planId: string, commandKind: 'submit_plan' | 'approve_plan' | 'activate_plan' | 'amend_plan', payload: Record = {}, ): Promise { return sendMissionGovernanceCommand(commandKind, { plan_id: planId }, payload); } async function dispatchMissionPlan(mission: Mission): Promise { const planId = mission.sourceIds.planId; if (!planId) return null; const status = mission.status.toLowerCase(); if (status === 'draft') { await sendMissionPlanCommand(planId, 'submit_plan', { requested_action: 'mission_dispatch' }); await sendMissionPlanCommand(planId, 'approve_plan', { requested_action: 'mission_dispatch' }); } else if (status === 'reviewing') { await sendMissionPlanCommand(planId, 'approve_plan', { requested_action: 'mission_dispatch' }); } else if (!['approved', 'active', 'running'].includes(status)) { await sendMissionPlanCommand(planId, 'amend_plan', { requested_action: 'reopen_for_execution', summary: '数字员工任务中心请求重新执行任务', }); return null; } await sendMissionPlanCommand(planId, 'activate_plan', { requested_action: 'mission_dispatch' }); return planId; } function restartStepsForMission(mission: Mission) { const steps = mission.steps.map((step, index) => { const skills = new Set(); step.preferredSkills.forEach((skill) => { if (skill.trim()) skills.add(skill); }); step.tasks.forEach((task) => { if (task.assignedSkillId?.trim()) skills.add(task.assignedSkillId); }); const description = [ step.description, ...step.tasks.map((task) => task.description), step.title, mission.objective, mission.title, ].find((value) => typeof value === 'string' && value.trim()); return { title: step.title || `${mission.title} 第 ${index + 1} 步`, description: description || step.title || mission.objective || mission.title, skills: Array.from(skills), depends_on: index === 0 ? [] : [`step-${index}`], }; }).filter((step) => step.skills.length > 0); if (steps.length === 0) { throw new Error('当前任务没有可复用的 skill,不能创建真实重新执行计划。'); } return steps; } function restartMissionTitle(title: string): string { const base = title.replace(/(\s*重新执行)+$/u, '').trim(); return `${base || title} 重新执行`; } async function createRestartPlan(mission: Mission): Promise { const response = await sendMissionGovernanceCommand('create_plan', { session_id: 'digital-mission-center', plan_id: mission.sourceIds.planId || mission.id, }, { requested_action: 'mission_restart', title: restartMissionTitle(mission.title), description: mission.objective || mission.title, steps: restartStepsForMission(mission), original_plan_id: mission.sourceIds.planId || mission.id, }); const planId = typeof response.result?.plan_id === 'string' ? response.result.plan_id : ''; if (!planId) { throw new Error('重新执行计划已提交,但后端没有返回 plan_id。'); } const status = typeof response.result?.status === 'string' ? response.result.status.toLowerCase() : ''; if (status === 'draft') { await sendMissionPlanCommand(planId, 'submit_plan', { requested_action: 'mission_restart' }); await sendMissionPlanCommand(planId, 'approve_plan', { requested_action: 'mission_restart' }); await sendMissionPlanCommand(planId, 'activate_plan', { requested_action: 'mission_restart' }); } else if (status === 'reviewing') { await sendMissionPlanCommand(planId, 'approve_plan', { requested_action: 'mission_restart' }); await sendMissionPlanCommand(planId, 'activate_plan', { requested_action: 'mission_restart' }); } else if (status === 'approved') { await sendMissionPlanCommand(planId, 'activate_plan', { requested_action: 'mission_restart' }); } return planId; } export default function MissionCenter({ focusMissionId, focusTaskId, focusDate, }: { focusMissionId?: string | null; focusTaskId?: string | null; focusDate?: string | null; }) { const initialMissionDate = focusDate || todayDateKey(); const [missions, setMissions] = useState([]); const [selected, setSelected] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(false); const [filter, setFilter] = useState('all'); const [dateFilter, setDateFilter] = useState(() => initialMissionDate); const [showAllDates, setShowAllDates] = useState(false); const [runningMissionId, setRunningMissionId] = useState(null); const [runningAll, setRunningAll] = useState(false); const [demoMode, setDemoMode] = useState(false); const [actionError, setActionError] = useState(null); const [recentMissionId, setRecentMissionId] = useState(null); const [missionNotice, setMissionNotice] = useState(null); const detailRef = useRef(null); const mountedRef = useRef(false); useEffect(() => { mountedRef.current = true; return () => { mountedRef.current = false; }; }, []); const fetchMissions = useCallback(async (): Promise => { setLoading(true); setError(false); try { const data = await getDebugPlans(); if (!mountedRef.current) return []; const planMap = new Map>(); const addPlan = (plan: Record) => { const id = typeof plan.plan_id === 'string' ? plan.plan_id : ''; if (id && !planMap.has(id)) planMap.set(id, plan); }; (Array.isArray(data) ? data as Record[] : []).forEach(addPlan); const missing = REQUIRED_MISSION_IDS.filter((id) => !planMap.has(id)); if (missing.length > 0) { const extraResults = await Promise.allSettled( missing.map(async (id) => ({ id, plans: await searchDebugPlans(id, 20) })), ); for (const result of extraResults) { if (result.status === 'fulfilled') { const exact = result.value.plans.find((plan) => plan.plan_id === result.value.id); if (exact) { addPlan(exact as Record); } } } } const requiredPlans = REQUIRED_MISSION_IDS .map((id) => planMap.get(id)) .filter((plan): plan is Record => Boolean(plan)); const recentPlans = [...planMap.entries()] .filter(([id]) => !REQUIRED_MISSION_IDS.includes(id)) .map(([, plan]) => plan) .sort((a, b) => planSortTime(b) - planSortTime(a)); const orderedPlans = [...recentPlans, ...requiredPlans]; if (orderedPlans.length > 0) { const nextMissions = adaptMissions(orderedPlans); setDemoMode(false); setMissions(nextMissions); return nextMissions; } else { const demoMissions = getDemoMissions(); setDemoMode(true); setMissions(demoMissions); return demoMissions; } } catch (err) { if (!mountedRef.current) return []; console.error('[MissionCenter] fetch failed:', err); const demoMissions = getDemoMissions(); setDemoMode(true); setMissions(demoMissions); setError(false); return demoMissions; } finally { if (mountedRef.current) setLoading(false); } }, []); useEffect(() => { fetchMissions(); }, [fetchMissions]); useEffect(() => { setDateFilter(focusDate || todayDateKey()); setShowAllDates(false); }, [focusDate]); const focusMission = useCallback((mission: Mission) => { setSelected(mission); setRecentMissionId(mission.id); window.setTimeout(() => { detailRef.current?.scrollIntoView({ block: 'start', inline: 'nearest', behavior: 'smooth' }); }, 40); }, []); const selectMission = useCallback((mission: Mission) => { focusMission(mission); setMissionNotice({ kind: 'selected', missionId: mission.id, title: mission.title, planId: mission.sourceIds.planId, }); }, [focusMission]); useEffect(() => { if (!focusMissionId && !focusTaskId) return; const matched = missions.find((mission) => ( mission.id === focusMissionId || mission.sourceIds.planId === focusMissionId || (focusTaskId ? mission.sourceIds.taskIds.includes(focusTaskId) : false) )); if (!matched) return; setFilter('all'); selectMission(matched); window.setTimeout(() => { document.getElementById(`mission-card-${domSafeId(matched.id)}`)?.scrollIntoView({ block: 'center', behavior: 'smooth', }); }, 80); }, [focusMissionId, focusTaskId, missions, selectMission]); const runMission = useCallback(async (mission: Mission) => { setRunningMissionId(mission.id); setError(false); setActionError(null); if (demoMode || !mission.sourceIds.planId) { setMissions((current) => runDemoMissionOnce(current, mission.id)); setSelected((current) => { if (current?.id !== mission.id) return current; const next = runDemoMissionOnce([current], mission.id); return next[0] ?? current; }); window.setTimeout(() => { if (!mountedRef.current) return; setMissions((current) => completeDemoMissionOnce(current, mission.id)); setSelected((current) => { if (current?.id !== mission.id) return current; const next = completeDemoMissionOnce([current], mission.id); return next[0] ?? current; }); setRunningMissionId(null); }, 1200); return; } try { const newPlanId = await dispatchMissionPlan(mission); const nextMissions = await fetchMissions(); setFilter('all'); if (newPlanId) { const selectedMission = nextMissions.find((item) => item.id === newPlanId || item.sourceIds.planId === newPlanId); if (selectedMission) { focusMission(selectedMission); setMissionNotice({ kind: 'dispatched', missionId: selectedMission.id, title: selectedMission.title, planId: newPlanId, }); } else { const plans = await searchDebugPlans(newPlanId, 5); const fallback = adaptMissions(plans as unknown as Record[]).find((item) => item.id === newPlanId); if (fallback) { focusMission(fallback); setMissionNotice({ kind: 'dispatched', missionId: fallback.id, title: fallback.title, planId: newPlanId, }); } else setSelected(null); } } else { setSelected((current) => current?.id === mission.id ? null : current); } } catch (err) { console.error('[MissionCenter] dispatch failed:', err); setError(false); setActionError(err instanceof Error ? err.message : '任务委托失败'); } finally { setRunningMissionId(null); } }, [demoMode, fetchMissions, focusMission]); const restartMission = useCallback(async (mission: Mission) => { setRunningMissionId(mission.id); setError(false); setActionError(null); if (demoMode || !mission.sourceIds.planId) { await runMission(mission); return; } try { const newPlanId = await createRestartPlan(mission); const nextMissions = await fetchMissions(); setFilter('all'); const selectedMission = nextMissions.find((item) => item.id === newPlanId || item.sourceIds.planId === newPlanId); if (selectedMission) { focusMission(selectedMission); setMissionNotice({ kind: 'created', missionId: selectedMission.id, title: selectedMission.title, planId: newPlanId, }); } else { setRecentMissionId(newPlanId); setMissionNotice({ kind: 'created', missionId: newPlanId, title: restartMissionTitle(mission.title), planId: newPlanId, }); } } catch (err) { console.error('[MissionCenter] restart failed:', err); setError(false); setActionError(err instanceof Error ? err.message : '重新执行失败'); } finally { setRunningMissionId(null); } }, [demoMode, fetchMissions, focusMission, runMission]); const selectedDate = dateFilter || todayDateKey(); const filteredByDate = dedupeMissionsForDate( showAllDates ? missions : missions.filter((mission) => missionMatchesDate(mission, selectedDate)), selectedDate, ); const runnableMissions = filteredByDate.filter((mission) => ( mission.sourceIds.planId && ['draft', 'reviewing', 'approved'].includes(mission.status.toLowerCase()) )); const runAllMissions = useCallback(async () => { if (runnableMissions.length === 0) return; setRunningAll(true); setError(false); if (demoMode) { let delay = 0; for (const mission of runnableMissions.slice(0, 4)) { window.setTimeout(() => { if (!mountedRef.current) return; setRunningMissionId(mission.id); setMissions((current) => runDemoMissionOnce(current, mission.id)); }, delay); delay += 400; window.setTimeout(() => { if (!mountedRef.current) return; setMissions((current) => completeDemoMissionOnce(current, mission.id)); }, delay + 900); } window.setTimeout(() => { if (!mountedRef.current) return; setRunningMissionId(null); setRunningAll(false); }, delay + 1200); return; } try { for (const mission of runnableMissions) { setRunningMissionId(mission.id); await dispatchMissionPlan(mission); } await fetchMissions(); setSelected(null); } catch (err) { console.error('[MissionCenter] batch dispatch failed:', err); setError(false); setActionError(err instanceof Error ? err.message : '批量委托失败'); } finally { setRunningMissionId(null); setRunningAll(false); } }, [demoMode, fetchMissions, runnableMissions]); const filtered = filter === 'all' ? filteredByDate : filteredByDate.filter(m => missionDelegationGroup(m) === filter); const displayMissions = filtered .map((mission, index) => ({ mission, index })) .sort((a, b) => { const priority = (mission: Mission) => ( (mission.id === recentMissionId ? 2 : 0) + (mission.id === selected?.id ? 1 : 0) ); return priority(b.mission) - priority(a.mission) || missionDateSortValue(a.mission, selectedDate) - missionDateSortValue(b.mission, selectedDate) || a.index - b.index; }) .map(({ mission }) => mission); const noticeTitle = missionNotice?.kind === 'created' ? '刚创建重新执行任务' : missionNotice?.kind === 'dispatched' ? '刚委托执行任务' : '当前查看任务'; const focusLabelForMission = (mission: Mission): string | null => { if (mission.id !== recentMissionId) return null; if (missionNotice?.missionId === mission.id && missionNotice.kind === 'created') return '刚创建'; if (missionNotice?.missionId === mission.id && missionNotice.kind === 'dispatched') return '刚委托'; return '当前查看'; }; return (
{demoMode && (
当前没有加载到 qimingclaw 任务记录,页面暂时展示本地示例数据。
)} {actionError && (
{actionError}
)} {missionNotice && (
{noticeTitle} {missionNotice.title} {missionNotice.planId && {missionNotice.planId}}
)}
{showAllDates ? `全部 ${filteredByDate.length} 项任务` : `${filteredByDate.length} 项任务`}
{DELEGATION_FILTERS.map((item) => ( ))}
{loading ? (
) : error ? (

{t('console.state.backendDown')}

) : displayMissions.length === 0 ? (
当前日期暂无任务

这个日期没有匹配到 qimingclaw 任务记录,可以临时查看全部任务。

) : (
{displayMissions.map(m => ( ))}
)}
{selected ? ( setSelected(null)} /> ) : (
选择一个任务后,右侧会显示执行步骤、运行记录和交付结果。

任务详情会优先展示 qimingclaw 本地 Plan / Task / Run / Event 记录,以及从执行 payload 中整理出的产物线索。

)}
); }