694 lines
27 KiB
TypeScript
694 lines
27 KiB
TypeScript
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<string, unknown>): 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<string, Mission>();
|
||
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<string, unknown>,
|
||
payload: Record<string, unknown> = {},
|
||
): Promise<GovernanceCommandResponse> {
|
||
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<string, unknown> = {},
|
||
): Promise<GovernanceCommandResponse> {
|
||
return sendMissionGovernanceCommand(commandKind, { plan_id: planId }, payload);
|
||
}
|
||
|
||
async function dispatchMissionPlan(mission: Mission): Promise<string | null> {
|
||
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<string>();
|
||
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<string> {
|
||
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<Mission[]>([]);
|
||
const [selected, setSelected] = useState<Mission | null>(null);
|
||
const [loading, setLoading] = useState(true);
|
||
const [error, setError] = useState(false);
|
||
const [filter, setFilter] = useState<string>('all');
|
||
const [dateFilter, setDateFilter] = useState<string>(() => initialMissionDate);
|
||
const [showAllDates, setShowAllDates] = useState(false);
|
||
const [runningMissionId, setRunningMissionId] = useState<string | null>(null);
|
||
const [runningAll, setRunningAll] = useState(false);
|
||
const [demoMode, setDemoMode] = useState(false);
|
||
const [actionError, setActionError] = useState<string | null>(null);
|
||
const [recentMissionId, setRecentMissionId] = useState<string | null>(null);
|
||
const [missionNotice, setMissionNotice] = useState<MissionNotice | null>(null);
|
||
const detailRef = useRef<HTMLDivElement | null>(null);
|
||
|
||
const mountedRef = useRef(false);
|
||
useEffect(() => {
|
||
mountedRef.current = true;
|
||
return () => { mountedRef.current = false; };
|
||
}, []);
|
||
|
||
const fetchMissions = useCallback(async (): Promise<Mission[]> => {
|
||
setLoading(true); setError(false);
|
||
try {
|
||
const data = await getDebugPlans();
|
||
if (!mountedRef.current) return [];
|
||
const planMap = new Map<string, Record<string, unknown>>();
|
||
const addPlan = (plan: Record<string, unknown>) => {
|
||
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<string, unknown>[] : []).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<string, unknown>);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
const requiredPlans = REQUIRED_MISSION_IDS
|
||
.map((id) => planMap.get(id))
|
||
.filter((plan): plan is Record<string, unknown> => 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<string, unknown>[]).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 (
|
||
<div className="de-mission-center-page">
|
||
<div className="de-mission-center-layout">
|
||
<div className="de-mc-list">
|
||
<GlassCard>
|
||
<CardTitleBar title="任务委托">
|
||
<button
|
||
onClick={runAllMissions}
|
||
className="de-btn-primary de-btn-sm"
|
||
disabled={runningAll || runnableMissions.length === 0}
|
||
>
|
||
{runningAll ? '委托中...' : `委托可执行任务(${runnableMissions.length})`}
|
||
</button>
|
||
<button onClick={fetchMissions} className="de-btn-secondary de-btn-sm">{t('console.action.refresh')}</button>
|
||
</CardTitleBar>
|
||
<div className="de-card-body">
|
||
{demoMode && (
|
||
<div className="de-schedule-result de-schedule-result--success" style={{ marginBottom: 12 }}>
|
||
当前没有加载到 qimingclaw 任务记录,页面暂时展示本地示例数据。
|
||
</div>
|
||
)}
|
||
{actionError && (
|
||
<div className="de-schedule-result de-schedule-result--error" style={{ marginBottom: 12 }}>
|
||
{actionError}
|
||
</div>
|
||
)}
|
||
{missionNotice && (
|
||
<div className="de-mission-focus-notice">
|
||
<strong>{noticeTitle}</strong>
|
||
<span>{missionNotice.title}</span>
|
||
{missionNotice.planId && <code>{missionNotice.planId}</code>}
|
||
</div>
|
||
)}
|
||
<div className="de-date-filter-row">
|
||
<label className="de-date-filter-field">
|
||
<span>选择日期</span>
|
||
<input
|
||
type="date"
|
||
value={selectedDate}
|
||
onChange={(event) => {
|
||
setShowAllDates(false);
|
||
setDateFilter(event.target.value || todayDateKey());
|
||
}}
|
||
/>
|
||
</label>
|
||
<div className="de-date-filter-actions" aria-label="日期切换">
|
||
<button type="button" className="de-filter-chip" onClick={() => { setShowAllDates(false); setDateFilter(shiftDateKey(selectedDate, -1)); }}>前一天</button>
|
||
<button type="button" className="de-filter-chip active" onClick={() => { setShowAllDates(false); setDateFilter(todayDateKey()); }}>今天</button>
|
||
<button type="button" className="de-filter-chip" onClick={() => { setShowAllDates(false); setDateFilter(shiftDateKey(selectedDate, 1)); }}>后一天</button>
|
||
</div>
|
||
<span className="de-date-filter-summary">
|
||
{showAllDates ? `全部 ${filteredByDate.length} 项任务` : `${filteredByDate.length} 项任务`}
|
||
</span>
|
||
</div>
|
||
<div className="de-filter-row">
|
||
{DELEGATION_FILTERS.map((item) => (
|
||
<button
|
||
key={item.key}
|
||
className={`de-filter-chip ${filter === item.key ? 'active' : ''}`}
|
||
onClick={() => setFilter(item.key)}
|
||
>
|
||
{item.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
{loading ? (
|
||
<div className="de-loading-skeleton" style={{ height: 300 }} />
|
||
) : error ? (
|
||
<p className="de-empty-text">{t('console.state.backendDown')}</p>
|
||
) : displayMissions.length === 0 ? (
|
||
<div className="de-mission-empty-panel">
|
||
<strong>当前日期暂无任务</strong>
|
||
<p>这个日期没有匹配到 qimingclaw 任务记录,可以临时查看全部任务。</p>
|
||
<div className="de-mission-empty-actions">
|
||
<button type="button" className="de-btn-primary de-btn-sm" onClick={() => { setShowAllDates(true); setFilter('all'); }}>
|
||
查看全部任务
|
||
</button>
|
||
<button type="button" className="de-btn-secondary de-btn-sm" onClick={() => { setShowAllDates(false); setDateFilter(focusDate || todayDateKey()); }}>
|
||
返回当前日期
|
||
</button>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<div className="de-mc-card-list">
|
||
{displayMissions.map(m => (
|
||
<MissionCard
|
||
key={m.id}
|
||
mission={m}
|
||
selected={selected?.id === m.id}
|
||
focusLabel={focusLabelForMission(m)}
|
||
dateFilter={selectedDate}
|
||
running={runningMissionId === m.id}
|
||
onSelect={selectMission}
|
||
onRun={runMission}
|
||
onRestart={restartMission}
|
||
/>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</GlassCard>
|
||
</div>
|
||
<div className="de-mc-detail" ref={detailRef}>
|
||
{selected ? (
|
||
<FlowDetailPanel mission={selected} onClose={() => setSelected(null)} />
|
||
) : (
|
||
<GlassCard>
|
||
<div className="de-card-body">
|
||
<div className="de-mission-empty-panel de-mission-empty-panel--detail">
|
||
<strong>选择一个任务后,右侧会显示执行步骤、运行记录和交付结果。</strong>
|
||
<p>任务详情会优先展示 qimingclaw 本地 Plan / Task / Run / Event 记录,以及从执行 payload 中整理出的产物线索。</p>
|
||
</div>
|
||
</div>
|
||
</GlassCard>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|