import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import type { KeyboardEvent as ReactKeyboardEvent, MouseEvent as ReactMouseEvent } from 'react'; import { Clock3, Download, Eye, PlayCircle, RefreshCw } from 'lucide-react'; import Avatar3D from '@/components/digital/Avatar3D'; import { basePath } from '@/lib/basePath'; import { digitalEmployeeReportPdfUrl, dispatchPlanNow, getDigitalEmployeeWorkday, getSchedulerJobs, governanceCommand, postDigitalEmployeeWorkdayAction, } from '@/lib/api'; import { isTauri } from '@/lib/tauri'; import type { CronJob, DigitalEmployeeDecision, DigitalEmployeeDuty, DigitalEmployeeRunDetail, DigitalEmployeeTaskExecutionSummary, DigitalEmployeeWorkEvent, DigitalEmployeeWorkdayProjection, } from '@/types/api'; import type { DigitalNavigationTarget } from './navigation'; const bp = basePath; const digitalAssetBase = isTauri() ? './digital-assets' : `${bp}/digital-assets`; const avatarAssetBase = `${digitalAssetBase}/avatars`; const DIGITAL_EMPLOYEE_TIME_ZONE = 'Asia/Shanghai'; type EmployeeVisualState = 'skill' | 'browser' | 'call' | 'redial' | 'evidence' | 'decision' | 'reporting'; type EventStatusFilter = 'all' | 'running' | 'success' | 'danger'; type TaskListFilter = 'all' | 'selected' | 'pending'; interface DigitalPlanItem { id: string; planId: string; title: string; summary: string; implemented: boolean; selectable: boolean; selected: boolean; status: string; statusLabel: string; statusTone: 'running' | 'success' | 'danger' | 'warning' | 'waiting' | 'info' | string; scheduleText: string; resultText: string; conversationId?: string | null; stepCount: number; taskCount: number; schedulerJob?: CronJob | null; source: 'plan_template'; } interface ManagementSyncStatus { running: boolean; syncing: boolean; enabled: boolean; endpoint: string | null; pending: number; failed: number; lastSyncAt: string | null; lastSyncError: string | null; recentFailures?: ManagementSyncFailure[]; } interface ManagementSyncFailure { id: string; entityType: string; entityId: string; operation: string; attempts: number; lastAttemptAt: string | null; error: string | null; createdAt: string; updatedAt: string; } // 技术侧 PlanTemplate 是任务模板来源;用户侧只展示为“任务”,运行时 Task/PlanStep 展示为“步骤”。 interface EmployeeVisualStateOption { id: EmployeeVisualState; label: string; shortLabel: string; phase: string; src: string; bubble: string; } const EMPLOYEE_STATE_VALUES: EmployeeVisualState[] = ['skill', 'browser', 'call', 'redial', 'evidence', 'decision', 'reporting']; const EVENT_STATUS_FILTERS: Array<{ id: EventStatusFilter; label: string }> = [ { id: 'all', label: '全部' }, { id: 'running', label: '执行中' }, { id: 'success', label: '已完成' }, { id: 'danger', label: '异常' }, ]; const WORKDAY_GUIDE_DISMISSED_STORAGE_KEY = 'sgrobot:digital-workday-guide-dismissed-date'; const MAIN_RUN_DETAIL_LIMIT = 80; const EVENT_HISTORY_RENDER_LIMIT = 300; const WORKDAY_GUIDE_STEPS = [ { title: '确认日期', body: '先确认今天的值班日期,避免把昨天或明天的任务混在一起。' }, { title: '选择任务', body: '从岗位职责里勾选今天要执行的任务,未接入能力保持待扩展。' }, { title: '任务执行', body: '点击任务执行后,数字员工开始按已选任务推进工作。' }, { title: '查看过程', body: '右侧滚动显示运行明细,任务中心可继续查看明细。' }, ]; const EMPLOYEE_STATE_OPTIONS: Record = { skill: { id: 'skill', label: '查看Skill', shortLabel: '技能', phase: '查看Skill与计划', src: `${avatarAssetBase}/states/employee-idle.png`, bubble: '我正在查看可用Skill、任务计划和今天要勾选的执行内容。', }, browser: { id: 'browser', label: '操作浏览器', shortLabel: '浏览器', phase: '操作浏览器执行', src: `${avatarAssetBase}/states/employee-browser.png`, bubble: '我正在打开页面、读取信息、填写表单,并把执行过程同步到右侧。', }, call: { id: 'call', label: '电话外呼', shortLabel: '外呼', phase: '电话外呼与重拨', src: `${avatarAssetBase}/states/employee-running.png`, bubble: '我正在按工单名单外呼现场人员,记录接通、未接通和后续重拨。', }, redial: { id: 'redial', label: '重拨跟进', shortLabel: '重拨', phase: '未接通人员重拨', src: `${avatarAssetBase}/states/employee-redial.png`, bubble: '我正在跟进未接通人员,按间隔重拨并更新每个人的通知状态。', }, evidence: { id: 'evidence', label: '数据核验', shortLabel: '核验', phase: '核验执行证据', src: `${avatarAssetBase}/states/employee-evidence.png`, bubble: '我正在核验页面数据、执行证据和任务结果,确认交付内容可信。', }, decision: { id: 'decision', label: '等待决策', shortLabel: '决策', phase: '等待人工确认', src: `${avatarAssetBase}/states/employee-decision.png`, bubble: '我已完成前置处理,现在需要人工确认后再继续推进。', }, reporting: { id: 'reporting', label: '汇报成果', shortLabel: '汇报', phase: '汇报今日成果', src: `${avatarAssetBase}/states/employee-reporting.png`, bubble: '我正在整理运行明细和日报,方便你直接查看成果和异常。', }, }; function todayInputDate(): string { const date = new Date(); return [ date.getFullYear(), String(date.getMonth() + 1).padStart(2, '0'), String(date.getDate()).padStart(2, '0'), ].join('-'); } function displayInputDate(date: string): string { return (date || todayInputDate()).split('-').join('/'); } function badgeClass(tone?: string): string { if (tone === 'success') return 'de-badge-success'; if (tone === 'danger') return 'de-badge-danger'; if (tone === 'warning' || tone === 'running') return 'de-badge-warning'; return 'de-badge-info'; } function dutyCardClass(planItem: DigitalPlanItem, selected: boolean): string { const classes = ['de-template-card-row', 'de-duty-card', 'de-duty-card--compact']; if (!planItem.implemented) classes.push('de-duty-card--disabled'); if (selected) classes.push('de-duty-card--selected'); if (planItem.statusTone === 'running') classes.push('de-duty-card--running'); return classes.join(' '); } function compactPlanText(value?: string | null): string { return String(value || '').replace(/\s+/g, ' ').trim(); } function cronExpressionFromJob(job?: CronJob | null): string { if (!job) return ''; if (job.expression) return job.expression; if (typeof job.schedule === 'string') return job.schedule; if (job.schedule && typeof job.schedule === 'object') { const schedule = job.schedule as Record; const expr = schedule.expr ?? schedule.expression ?? schedule.cron; if (typeof expr === 'string') return expr; } return ''; } function scheduleTextFromJob(job?: CronJob | null): string { const expr = cronExpressionFromJob(job); if (!expr) return '未定时'; const parts = expr.trim().split(/\s+/); const minute = parts[0] ?? ''; const hour = parts[1] ?? ''; if (parts.length >= 2 && /^\d+$/.test(minute) && /^\d+$/.test(hour)) { return `已定时 ${hour.padStart(2, '0')}:${minute.padStart(2, '0')} 执行`; } return `调度注册 ${expr}`; } function readRecordString(value: unknown, keys: string[]): string { if (!value || typeof value !== 'object') return ''; const record = value as Record; for (const key of keys) { const field = record[key]; if (typeof field === 'string' && field.trim()) return field.trim(); } return ''; } function templateIdForSchedulerJob(job: CronJob): string { const direct = compactPlanText(job.template_id); if (direct) return direct; const delivery = readRecordString(job.delivery, ['template_id', 'plan_template_id', 'templateId']); if (delivery) return delivery; const schedule = readRecordString(job.schedule, ['template_id', 'plan_template_id', 'templateId']); if (schedule) return schedule; return job.id.replace(/^schedule-/, ''); } function selectedDutyMatchesTemplate(selectedIds: Set, templateId: string, scheduleId?: string): boolean { if (selectedIds.has(templateId) || (scheduleId && selectedIds.has(scheduleId))) return true; return Array.from(selectedIds).some((id) => hasRuntimeSuffixForTemplate(id, templateId)); } function hasRuntimeSuffixForTemplate(value: string, templateId: string): boolean { if (!value.startsWith(`${templateId}-`)) return false; return /^-\d{8,}$/.test(value.slice(templateId.length)); } function dutyMatchesTemplate(duty: DigitalEmployeeDuty, templateId: string, title?: string | null): boolean { if (duty.id === templateId || duty.conversation_id === templateId) return true; if (hasRuntimeSuffixForTemplate(duty.id, templateId) || (duty.conversation_id ? hasRuntimeSuffixForTemplate(duty.conversation_id, templateId) : false)) return true; const normalizedTitle = compactPlanText(title).toLowerCase(); return Boolean(normalizedTitle && compactPlanText(duty.title).toLowerCase() === normalizedTitle); } function linkedDutyForTemplate(templateId: string, title: string | null | undefined, duties: DigitalEmployeeDuty[]): DigitalEmployeeDuty | null { const matches = duties.filter((duty) => dutyMatchesTemplate(duty, templateId, title)); return matches.find((duty) => duty.selected) ?? matches.find((duty) => duty.id === templateId || duty.conversation_id === templateId) ?? matches[0] ?? null; } function submissionDutyIdForPlanTemplate(planItem: DigitalPlanItem, duties: DigitalEmployeeDuty[]): string { return linkedDutyForTemplate(planItem.id, planItem.title, duties)?.id ?? planItem.id; } function templateStepCount(job: CronJob): number { if (typeof job.step_count === 'number' && Number.isFinite(job.step_count)) return job.step_count; return Array.isArray(job.skill_ids) ? job.skill_ids.length : 0; } function triggerKindLabel(job: CronJob): string { const trigger = compactPlanText(job.trigger_kind || readRecordString(job.schedule, ['kind'])).toLowerCase(); if (trigger === 'cron') return '定时任务'; if (trigger === 'webhook') return '触发任务'; return '手动任务'; } function buildPlanTemplateItem(job: CronJob, duties: DigitalEmployeeDuty[], selected: boolean): DigitalPlanItem { const templateId = templateIdForSchedulerJob(job); const linkedDuty = linkedDutyForTemplate(templateId, job.name, duties); const stepCount = templateStepCount(job); const skillCount = Array.isArray(job.skill_ids) ? job.skill_ids.length : stepCount; const status = linkedDuty?.status ?? job.last_status ?? (job.enabled ? 'plan_template' : 'disabled'); const statusLabel = linkedDuty?.status_label ?? (job.enabled ? triggerKindLabel(job) : '已停用'); const statusTone = linkedDuty?.status_tone ?? (job.enabled ? 'info' : 'waiting'); return { id: templateId, planId: templateId, title: compactPlanText(job.name) || templateId, summary: compactPlanText(job.prompt) || `任务:${templateId}`, implemented: job.enabled, selectable: true, selected, status, statusLabel, statusTone, scheduleText: linkedDuty?.schedule_text ?? scheduleTextFromJob(job), resultText: linkedDuty?.result_text ?? `${stepCount} 个步骤,${skillCount} 个Skill`, conversationId: linkedDuty?.conversation_id ?? templateId, stepCount, taskCount: skillCount, schedulerJob: job, source: 'plan_template', }; } function isPlanItemExecutable(planItem: DigitalPlanItem): boolean { return planItem.implemented; } function planExecutionDisabledReason(planItem: DigitalPlanItem): string { if (!planItem.implemented) return '当前任务未接入执行能力。'; return ''; } function eventMatchesPlanItem(event: DigitalEmployeeWorkEvent, planItem: DigitalPlanItem): boolean { const haystack = `${event.conversation_id ?? ''} ${event.plan_title} ${event.task_title} ${event.step_title ?? ''} ${event.detail}`; return haystack.includes(planItem.id) || haystack.includes(planItem.title); } function fallbackSummaryForPlanItem( planItem: DigitalPlanItem, events: DigitalEmployeeWorkEvent[], ): DigitalEmployeeTaskExecutionSummary { const matched = events.filter((event) => eventMatchesPlanItem(event, planItem)); const successCount = matched.filter(isSuccessfulEvent).length; const failureCount = matched.filter(isFailedEvent).length; const runningCount = matched.filter(isRunningEvent).length; const waitingCount = Math.max(matched.length - successCount - failureCount - runningCount, 0); const latest = matched.length > 0 ? matched[matched.length - 1] : undefined; return { id: planItem.id, plan_id: planItem.planId, title: planItem.title, description: planItem.summary, selected: true, total_count: matched.length, success_count: successCount, failure_count: failureCount, running_count: runningCount, waiting_count: waitingCount, latest_status_label: latest?.status_label ?? planItem.statusLabel, latest_status_tone: latest?.status_tone ?? planItem.statusTone, business_summary: matched.length > 0 ? `${planItem.title} 今日执行 ${matched.length} 次,成功 ${successCount} 次,失败 ${failureCount} 次。` : `${planItem.title} 尚未产生执行记录。`, ai_summary: matched.length > 0 ? `${planItem.title} 今日执行 ${matched.length} 次,成功 ${successCount} 次,失败 ${failureCount} 次。` : `${planItem.title} 尚未产生执行记录。`, }; } function summaryMatchesPlanItem(summary: DigitalEmployeeTaskExecutionSummary, planItem: DigitalPlanItem): boolean { const summaryPlanId = summary.plan_id ?? ''; if (summary.id === planItem.id || summaryPlanId === planItem.planId) return true; if (hasRuntimeSuffixForTemplate(summary.id, planItem.id) || hasRuntimeSuffixForTemplate(summaryPlanId, planItem.planId)) return true; return compactPlanText(summary.title).toLowerCase() === compactPlanText(planItem.title).toLowerCase(); } function runDetailFromEvent(event: DigitalEmployeeWorkEvent): DigitalEmployeeRunDetail { return { id: event.id, time: event.time, task_name: event.plan_title, step_name: event.step_title, operation_name: event.task_title, status_label: event.status_label, status_tone: event.status_tone, business_detail: eventBusinessResult(event), raw_detail: event.detail, updated_at: event.updated_at, conversation_id: event.conversation_id, }; } function cronExprForDailyTime(value: string): string { const [hour = '9', minute = '0'] = value.split(':'); const safeHour = Math.min(Math.max(Number(hour) || 9, 0), 23); const safeMinute = Math.min(Math.max(Number(minute) || 0, 0), 59); return `${safeMinute} ${safeHour} * * *`; } function eventStatusText(event: DigitalEmployeeWorkEvent): string { return event.status_label || event.status || '已同步'; } function compactText(value?: string | null): string { return String(value || '').replace(/\s+/g, ' ').trim(); } function eventScheduleTime(event: DigitalEmployeeWorkEvent): string { const source = `${event.plan_title} ${event.step_title ?? ''}`; const match = source.match(/(?:^|\D)(\d{1,2})[::](\d{2})(?:\D|$)/); if (match?.[1] && match[2]) return `${match[1].padStart(2, '0')}:${match[2]}`; const fallback = compactText(event.time); return fallback || '即时触发'; } function formatBeijingDateTime(value?: string | null): string { const raw = compactText(value); if (!raw) return ''; const date = new Date(raw); if (Number.isNaN(date.getTime())) return ''; const parts = new Intl.DateTimeFormat('zh-CN', { timeZone: DIGITAL_EMPLOYEE_TIME_ZONE, month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false, }).formatToParts(date); const part = (type: string) => parts.find((item) => item.type === type)?.value ?? ''; return `${part('month')}/${part('day')} ${part('hour')}:${part('minute')}`; } function summarizeSyncError(value?: string | null): string { const error = compactText(value); if (!error) return ''; return error.length > 140 ? `${error.slice(0, 140)}...` : error; } function syncStatusTone(status: ManagementSyncStatus | null): 'info' | 'success' | 'warning' | 'danger' { if (!status) return 'info'; if (!status.enabled || !status.endpoint) return 'warning'; if (status.failed > 0 || status.lastSyncError) return 'danger'; if (status.syncing || status.pending > 0) return 'warning'; return status.lastSyncAt ? 'success' : 'info'; } function syncStatusLabel(status: ManagementSyncStatus | null): string { if (!status) return '未连接'; if (!status.enabled) return '已停用'; if (!status.endpoint) return '未配置'; if (status.syncing) return '同步中'; if (status.failed > 0 || status.lastSyncError) return '同步异常'; if (status.pending > 0) return '待同步'; return status.lastSyncAt ? '已同步' : '等待数据'; } function syncStatusSummary(status: ManagementSyncStatus | null): string { if (!status) return '当前环境未连接 qimingclaw 同步桥。'; if (!status.enabled) return '管理端同步已停用。'; if (!status.endpoint) return '管理端同步地址未配置。'; const error = summarizeSyncError(status.lastSyncError); if (status.failed > 0 || error) { return [ status.failed > 0 ? `${status.failed} 条失败` : '', status.pending > 0 ? `${status.pending} 条待同步` : '', error ? `失败原因:${error}` : '', ].filter(Boolean).join(','); } if (status.syncing) return `正在同步,待同步 ${status.pending} 条。`; if (status.pending > 0) return `仍有 ${status.pending} 条待同步记录。`; return status.lastSyncAt ? `最近同步:${formatBeijingDateTime(status.lastSyncAt) || status.lastSyncAt}` : '等待本地 outbox 数据。'; } function entityTypeLabel(entityType: string): string { const labels: Record = { plan: 'Plan', task: 'Task', run: 'Run', event: 'Event', artifact: 'Artifact', approval: 'Approval', }; return labels[entityType] || entityType || 'Entity'; } function syncFailureSummary(failure: ManagementSyncFailure): string { const error = summarizeSyncError(failure.error); return [ `${entityTypeLabel(failure.entityType)} / ${failure.operation}`, `重试 ${failure.attempts} 次`, error, ].filter(Boolean).join(' · '); } function syncFailureTime(failure: ManagementSyncFailure): string { return formatBeijingDateTime(failure.lastAttemptAt || failure.updatedAt || failure.createdAt) || '未尝试'; } async function readManagementSyncStatus(): Promise { const bridge = window.QimingClawBridge?.digital; if (!bridge?.getSyncStatus) return null; try { return await bridge.getSyncStatus(); } catch { return null; } } function eventDisplayTime(event: DigitalEmployeeWorkEvent): string { const beijingTime = formatBeijingDateTime(event.updated_at); if (beijingTime) { return beijingTime; } return compactText(event.time) || '待同步'; } function runDetailDisplayTime(detail: DigitalEmployeeRunDetail): string { return formatBeijingDateTime(detail.updated_at) || compactText(detail.time) || '--:--'; } function eventBusinessResult(event: DigitalEmployeeWorkEvent): string { const detail = compactText(event.detail) .replace(/^触发来源[::]\s*$/u, '') .replace(/触发来源[::]\s*(?=。|,|,|;|;|$)/u, '') .trim(); if (!detail) { if (isSuccessfulEvent(event)) return '真实事件已完成,结果详情未回传。'; if (isRunningEvent(event)) return '真实事件执行中,等待结果回传。'; if (isFailedEvent(event)) return '执行异常,点击查看详情。'; return '结果详情待同步。'; } if (/^runner unavailable/i.test(detail)) { return '执行链路异常,点击查看详情。'; } return detail.length > 72 ? `${detail.slice(0, 72)}...` : detail; } function eventTimingSummary(event: DigitalEmployeeWorkEvent): string { return `计划时间 ${eventScheduleTime(event)} · 执行时间 ${eventDisplayTime(event)}`; } function eventMatchesFilter(event: DigitalEmployeeWorkEvent, filter: EventStatusFilter): boolean { if (filter === 'all') return true; if (filter === 'running') return isRunningEvent(event); if (filter === 'success') return isSuccessfulEvent(event); return isFailedEvent(event); } function projectionSelectedDutyIds(projection: DigitalEmployeeWorkdayProjection): Set { return new Set( projection.duties .filter((duty) => duty.selected) .map((duty) => duty.id), ); } function isDecisionOpen(status?: string): boolean { return ['pending', 'requested'].includes(String(status || '').toLowerCase()); } function isExecutionPanelInteractiveTarget(target: EventTarget | null): boolean { if (!(target instanceof Element)) return false; if (target.closest('button, a, input, select, textarea, [role="tab"]')) { return true; } const roleButton = target.closest('[role="button"]'); return Boolean(roleButton && !roleButton.classList.contains('de-workday-execution--clickable')); } function rotateEvents(events: DigitalEmployeeWorkEvent[], offset: number): DigitalEmployeeWorkEvent[] { if (events.length <= 4) return events; return events.slice(offset).concat(events.slice(0, offset)).slice(0, 4); } function isRunningEvent(event: DigitalEmployeeWorkEvent): boolean { const status = `${event.status} ${event.status_tone}`.toLowerCase(); return status.includes('running') || status.includes('execut') || status.includes('progress'); } function isSuccessfulEvent(event: DigitalEmployeeWorkEvent): boolean { const status = `${event.status} ${event.status_tone}`.toLowerCase(); return status.includes('success') || status.includes('complete') || status.includes('done'); } function isFailedEvent(event: DigitalEmployeeWorkEvent): boolean { const status = `${event.status} ${event.status_tone}`.toLowerCase(); return status.includes('danger') || status.includes('fail') || status.includes('error') || status.includes('异常'); } function isCallEvent(event: DigitalEmployeeWorkEvent): boolean { const text = `${event.plan_title} ${event.task_title} ${event.step_title ?? ''} ${event.detail}`.toLowerCase(); return ['外呼', '电话', '拨打', '接通', '未接通', '重拨', '人员'].some((keyword) => text.includes(keyword)); } function isRedialEvent(event: DigitalEmployeeWorkEvent): boolean { const text = `${event.plan_title} ${event.task_title} ${event.step_title ?? ''} ${event.detail}`.toLowerCase(); return ['重拨', '未接通', '再拨', '回拨', 'retry', 'callback'].some((keyword) => text.includes(keyword)); } function isEvidenceEvent(event: DigitalEmployeeWorkEvent): boolean { const text = `${event.plan_title} ${event.task_title} ${event.step_title ?? ''} ${event.detail}`.toLowerCase(); return ['核验', '校验', '证据', '汇总', '报告', '结果', '交付', '看板', '同步'].some((keyword) => text.includes(keyword)); } function getVisualStateOverride(): EmployeeVisualState | null { if (typeof window === 'undefined') return null; const value = new URLSearchParams(window.location.search).get('visual'); return EMPLOYEE_STATE_VALUES.includes(value as EmployeeVisualState) ? (value as EmployeeVisualState) : null; } function visualStateForEvent(event: DigitalEmployeeWorkEvent): EmployeeVisualState { if (isRedialEvent(event)) return 'redial'; if (isCallEvent(event)) return 'call'; if (isEvidenceEvent(event)) return 'evidence'; return 'browser'; } function resolveEmployeeVisualState(args: { confirmed: boolean; currentEvent: DigitalEmployeeWorkEvent | null; pendingDecisionCount: number; eventsCount: number; }): EmployeeVisualState { if (args.currentEvent) return visualStateForEvent(args.currentEvent); if (!args.confirmed && args.eventsCount === 0) return 'skill'; if (args.pendingDecisionCount > 0) return 'decision'; if (args.eventsCount > 0) return 'browser'; return 'skill'; } const DECISION_CHAIN_TODO_TEXT = '真实审批链路待接入'; function readGuideDismissedDate(): string | null { if (typeof window === 'undefined') return null; try { return window.sessionStorage.getItem(WORKDAY_GUIDE_DISMISSED_STORAGE_KEY); } catch { return null; } } function rememberGuideDismissedDate(date: string): void { if (typeof window === 'undefined') return; try { window.sessionStorage.setItem(WORKDAY_GUIDE_DISMISSED_STORAGE_KEY, date); } catch { // Session storage can be unavailable in restricted browser contexts. } } export default function CommandDeck({ onNavigate }: { onNavigate: (target: DigitalNavigationTarget) => void }) { const [selectedDate, setSelectedDate] = useState(todayInputDate()); const [workdayProjection, setWorkdayProjection] = useState(null); const [schedulerJobs, setSchedulerJobs] = useState([]); const [selectedDutyIds, setSelectedDutyIds] = useState>(new Set()); const [projectionLoading, setProjectionLoading] = useState(true); const [actionBusy, setActionBusy] = useState(null); const [actionNotice, setActionNotice] = useState(null); const [actionError, setActionError] = useState(null); const [planDataError, setPlanDataError] = useState(null); const [selectionDirty, setSelectionDirty] = useState(false); const [taskListFilter, setTaskListFilter] = useState('selected'); const [feedOffset, setFeedOffset] = useState(0); const [feedManuallyPaused, setFeedManuallyPaused] = useState(false); const [feedInteractionPaused, setFeedInteractionPaused] = useState(false); const [eventHistoryOpen, setEventHistoryOpen] = useState(false); const [selectedEvent, setSelectedEvent] = useState(null); const [eventStatusFilter, setEventStatusFilter] = useState('all'); const [taskManagerOpen, setTaskManagerOpen] = useState(false); const [schedulePlanItem, setSchedulePlanItem] = useState(null); const [executePlanItem, setExecutePlanItem] = useState(null); const [scheduleTime, setScheduleTime] = useState('09:00'); const [profileSettingsOpen, setProfileSettingsOpen] = useState(false); const [reportPreviewOpen, setReportPreviewOpen] = useState(false); const [profileForm, setProfileForm] = useState({ name: '', company: '', position: '', currentStatus: 'working', }); const [reportScheduleTime, setReportScheduleTime] = useState('18:00'); const [guideOpen, setGuideOpen] = useState(false); const [guideDismissedDate, setGuideDismissedDate] = useState(() => readGuideDismissedDate()); const [managementSyncStatus, setManagementSyncStatus] = useState(null); const mountedRef = useRef(false); const selectionDirtyRef = useRef(false); useEffect(() => { mountedRef.current = true; return () => { mountedRef.current = false; }; }, []); const fetchProjection = useCallback(async () => { let latestProjection: DigitalEmployeeWorkdayProjection | null = null; let latestJobs: CronJob[] | null = null; const syncDefaultSelection = () => { if (selectionDirtyRef.current || !latestJobs) return; const projectionIds = latestProjection?.source === 'live' ? projectionSelectedDutyIds(latestProjection) : new Set(); const matchedTemplateIds = latestJobs .map((job) => templateIdForSchedulerJob(job)) .filter((templateId) => selectedDutyMatchesTemplate(projectionIds, templateId)); const defaultTemplateIds = matchedTemplateIds.length > 0 ? matchedTemplateIds : latestJobs.slice(0, 3).map((job) => templateIdForSchedulerJob(job)); setSelectedDutyIds(new Set(defaultTemplateIds)); }; const projectionPromise = getDigitalEmployeeWorkday(selectedDate) .then((projection) => { if (!mountedRef.current) return; latestProjection = projection; setWorkdayProjection(projection); setActionError(null); syncDefaultSelection(); }) .catch((error: unknown) => { if (!mountedRef.current) return; setActionError(error instanceof Error ? error.message : '数字员工首页数据同步失败'); }); const jobsPromise = getSchedulerJobs() .then((jobs) => { if (!mountedRef.current) return; latestJobs = jobs.filter((job) => ( job.job_type === 'plan_template' || readRecordString(job.delivery, ['mode']) === 'plan_template' )); setSchedulerJobs(latestJobs); setPlanDataError(null); syncDefaultSelection(); }) .catch((error: unknown) => { if (!mountedRef.current) return; setPlanDataError(error instanceof Error ? error.message : '任务加载失败'); }); const syncStatusPromise = readManagementSyncStatus() .then((status) => { if (!mountedRef.current) return; setManagementSyncStatus(status); }); await Promise.allSettled([projectionPromise, jobsPromise, syncStatusPromise]); if (mountedRef.current) { setProjectionLoading(false); } }, [selectedDate]); useEffect(() => { setProjectionLoading(true); void fetchProjection(); }, [fetchProjection]); useEffect(() => { const intervalMs = workdayProjection?.workday.confirmed ? 8000 : 30000; const timer = window.setInterval(() => { void fetchProjection(); }, intervalMs); return () => window.clearInterval(timer); }, [fetchProjection, workdayProjection?.workday.confirmed]); useEffect(() => { const feedPaused = feedManuallyPaused || feedInteractionPaused || eventHistoryOpen || selectedEvent !== null; if (feedPaused) return undefined; const timer = window.setInterval(() => { setFeedOffset((current) => { const length = workdayProjection?.source === 'live' ? workdayProjection.events.length : 0; return length > 0 ? (current + 1) % length : 0; }); }, 3200); return () => window.clearInterval(timer); }, [eventHistoryOpen, feedInteractionPaused, feedManuallyPaused, selectedEvent, workdayProjection?.events.length, workdayProjection?.source]); const isLiveProjection = workdayProjection?.source === 'live'; const liveProjectionDuties = useMemo( () => (isLiveProjection ? (workdayProjection?.duties ?? []) : []), [isLiveProjection, workdayProjection?.duties], ); const planItems = useMemo(() => { return schedulerJobs .slice() .sort((left, right) => { const leftId = templateIdForSchedulerJob(left); const rightId = templateIdForSchedulerJob(right); const leftSelected = selectedDutyIds.has(leftId) ? 1 : 0; const rightSelected = selectedDutyIds.has(rightId) ? 1 : 0; if (leftSelected !== rightSelected) return rightSelected - leftSelected; const leftRun = new Date(left.next_run || '').getTime(); const rightRun = new Date(right.next_run || '').getTime(); const leftHasRun = Number.isFinite(leftRun); const rightHasRun = Number.isFinite(rightRun); if (leftHasRun !== rightHasRun) return leftHasRun ? -1 : 1; if (leftHasRun && rightHasRun && leftRun !== rightRun) return leftRun - rightRun; return compactPlanText(left.name).localeCompare(compactPlanText(right.name), 'zh-CN'); }) .map((job) => { const templateId = templateIdForSchedulerJob(job); return buildPlanTemplateItem(job, liveProjectionDuties, selectedDutyIds.has(templateId)); }); }, [liveProjectionDuties, schedulerJobs, selectedDutyIds]); const selectedDuties = useMemo( () => planItems.filter((planItem) => selectedDutyIds.has(planItem.id)), [planItems, selectedDutyIds], ); const selectedSubmissionDutyIds = useMemo( () => selectedDuties.map((planItem) => submissionDutyIdForPlanTemplate(planItem, liveProjectionDuties)), [liveProjectionDuties, selectedDuties], ); const totalDuties = planItems; const pendingDuties = planItems.filter((planItem) => !selectedDutyIds.has(planItem.id)); const visibleTaskItems = taskListFilter === 'selected' ? selectedDuties : taskListFilter === 'pending' ? pendingDuties : totalDuties; const allEvents = isLiveProjection ? (workdayProjection?.events ?? []) : []; const rotatingEvents = rotateEvents(allEvents, feedOffset); const visibleEvents = allEvents; const pendingDecisions = (isLiveProjection ? (workdayProjection?.decisions ?? []) : []).filter((decision) => ( isDecisionOpen(decision.status) )); const feedPaused = feedManuallyPaused || feedInteractionPaused || eventHistoryOpen || selectedEvent !== null; const filteredHistoryEvents = useMemo( () => allEvents.filter((event) => eventMatchesFilter(event, eventStatusFilter)), [allEvents, eventStatusFilter], ); const visibleHistoryEvents = filteredHistoryEvents.slice(0, EVENT_HISTORY_RENDER_LIMIT); const runningEvents = allEvents.filter(isRunningEvent); const completedEvents = allEvents.filter(isSuccessfulEvent); const failedEvents = allEvents.filter(isFailedEvent); const currentEvent = rotatingEvents[0] ?? runningEvents[0] ?? null; const employee = workdayProjection?.employee; const workday = workdayProjection?.workday; const delivery = workdayProjection?.delivery; const dailyReport = isLiveProjection ? workdayProjection?.daily_report : undefined; const runDetails = isLiveProjection ? (workdayProjection?.run_details?.length ? workdayProjection.run_details : allEvents.map(runDetailFromEvent)) : []; const visibleRunDetails = runDetails.slice(0, MAIN_RUN_DETAIL_LIMIT); const executionSummaries = isLiveProjection ? (workdayProjection?.execution_summaries ?? []) : []; const taskSummaryCards = selectedDuties.map((planItem) => ( executionSummaries.find((summary) => summaryMatchesPlanItem(summary, planItem)) ?? fallbackSummaryForPlanItem(planItem, allEvents) )); const selectedDateLabel = displayInputDate(selectedDate); const visibleTaskListLabel = taskListFilter === 'all' ? '全部任务' : taskListFilter === 'pending' ? '待选择任务' : '今日已选任务'; const visibleTaskListHint = taskListFilter === 'all' ? `总任务 ${totalDuties.length} 项,不代表当天都会执行。` : taskListFilter === 'pending' ? `当前日期 ${selectedDateLabel} 还有 ${pendingDuties.length} 项未选择。` : `当前日期 ${selectedDateLabel} 已选择 ${selectedDuties.length} 项任务。`; const readyReportTotals = dailyReport?.status === 'ready' ? dailyReport.totals : null; const reportTaskCount = readyReportTotals?.task_count ?? taskSummaryCards.length; const reportExecutionCount = readyReportTotals?.execution_count ?? taskSummaryCards.reduce((total, summary) => total + summary.total_count, 0); const reportSuccessCount = readyReportTotals?.success_count ?? taskSummaryCards.reduce((total, summary) => total + summary.success_count, 0); const reportFailureCount = readyReportTotals?.failure_count ?? taskSummaryCards.reduce((total, summary) => total + summary.failure_count, 0); const reportPreviewLines = dailyReport ? [dailyReport.summary, ...(dailyReport.detail_lines ?? [])].filter((line): line is string => Boolean(line && line.trim())) : []; const reportPreviewAvailable = dailyReport?.status === 'ready' && reportPreviewLines.length > 0; const currentStatus = workday?.current_status ?? employee?.current_status ?? 'working'; const currentStatusLabel = workday?.current_status_label ?? employee?.current_status_label ?? (currentStatus === 'resting' ? '休息中' : '工作中'); const isResting = currentStatus === 'resting'; const isDemoProjection = workdayProjection?.source === 'demo'; const projectionSourceLabel = isDemoProjection ? '演示兜底' : '真实数据'; const projectionSourceClass = isDemoProjection ? 'de-badge-warning' : 'de-badge-success'; const managementSyncTone = syncStatusTone(managementSyncStatus); const managementSyncLabel = syncStatusLabel(managementSyncStatus); const managementSyncSummary = syncStatusSummary(managementSyncStatus); const managementSyncError = summarizeSyncError(managementSyncStatus?.lastSyncError); const managementSyncFailures = managementSyncStatus?.recentFailures ?? []; const resolvedVisualState = resolveEmployeeVisualState({ confirmed: Boolean(workday?.confirmed), currentEvent, pendingDecisionCount: pendingDecisions.length, eventsCount: allEvents.length, }); const visualStateOverride = getVisualStateOverride(); const employeeVisualState = visualStateOverride ?? resolvedVisualState; const activeVisual = EMPLOYEE_STATE_OPTIONS[employeeVisualState]; const bubbleText = visualStateOverride || currentEvent || pendingDecisions.length > 0 ? activeVisual.bubble : (workday?.bubble_text ?? activeVisual.bubble); const workdaySummaryText = totalDuties.length > 0 ? `已加载 ${totalDuties.length} 项任务,当前选择 ${selectedDuties.length} 项;右侧展示真实执行状态、运行明细和日报。` : (workday?.summary ?? '等待同步当天任务计划。'); const currentActionTitle = currentEvent?.task_title ?? (workday?.confirmed ? '等待下一条执行事件' : '等待确认任务设置'); const currentActionDetail = currentEvent ? `${currentEvent.plan_title}:${eventBusinessResult(currentEvent)}(${eventTimingSummary(currentEvent)})` : '任务执行后会显示正在处理的任务、步骤或汇总动作。'; const nextStepText = pendingDecisions[0] ? `等待人工确认:${pendingDecisions[0].title}` : workday?.confirmed ? (delivery?.lines[0] ?? '继续按计划轮询新工单,并把结果同步到运行明细。') : '请先确认今天要执行的任务,确认后数字员工会开始执行。'; const needsWorkdayConfirmation = Boolean(workdayProjection && !workday?.confirmed); useEffect(() => { if (projectionLoading || !workdayProjection) return; if (workdayProjection.workday.confirmed) { setGuideOpen(false); return; } }, [guideDismissedDate, projectionLoading, selectedDate, workdayProjection]); useEffect(() => { if (!employee) return; setProfileForm({ name: employee.name || '', company: employee.company || '', position: employee.position || employee.major || '', currentStatus: employee.current_status || workday?.current_status || 'working', }); }, [employee, workday?.current_status]); useEffect(() => { setReportScheduleTime(dailyReport?.report_schedule_time || '18:00'); }, [dailyReport?.report_schedule_time]); useEffect(() => { setReportPreviewOpen(false); }, [selectedDate]); const toggleDuty = useCallback((planItem: DigitalPlanItem) => { if (!planItem.selectable || !planItem.implemented) return; setSelectedDutyIds((current) => { const next = new Set(current); if (next.has(planItem.id)) { next.delete(planItem.id); } else { next.add(planItem.id); } return next; }); selectionDirtyRef.current = true; setSelectionDirty(true); setActionNotice(null); }, []); const confirmWorkday = useCallback(async () => { if (selectedDutyIds.size === 0) { setActionError('请至少选择一项今天要执行的任务。'); return; } if (isResting) { setActionError('当前状态为休息中,请切换为工作中后再执行任务。'); return; } setActionBusy('confirm_workday'); setActionError(null); setActionNotice(null); try { const projection = await postDigitalEmployeeWorkdayAction({ action: 'confirm_workday', date: selectedDate, duty_ids: selectedSubmissionDutyIds, }); if (!mountedRef.current) return; setWorkdayProjection(projection); setSelectedDutyIds(new Set(selectedDutyIds)); selectionDirtyRef.current = true; setSelectionDirty(false); setGuideOpen(false); setActionNotice('任务设置已确认,数字员工开始按任务执行。'); } catch (error) { if (mountedRef.current) { setActionError(error instanceof Error ? error.message : '确认任务设置失败'); } } finally { if (mountedRef.current) setActionBusy(null); } }, [isResting, selectedDate, selectedDutyIds, selectedSubmissionDutyIds]); const flushManagementSync = useCallback(async () => { setActionBusy('flush_management_sync'); setActionError(null); setActionNotice(null); try { const bridge = window.QimingClawBridge?.digital; if (!bridge?.flushSync) { setActionNotice('当前环境未连接 qimingclaw 同步桥。'); return; } const syncStatus = await bridge.flushSync(); if (!mountedRef.current) return; setManagementSyncStatus(syncStatus); const latestProjection = await getDigitalEmployeeWorkday(selectedDate); if (!mountedRef.current) return; setWorkdayProjection(latestProjection); const statusText = syncStatus.failed > 0 ? `同步完成,仍有 ${syncStatus.failed} 条失败记录。` : syncStatus.pending > 0 ? `同步完成,仍有 ${syncStatus.pending} 条待同步记录。` : '管理端同步完成。'; setActionNotice(statusText); } catch (error) { if (mountedRef.current) { setActionError(error instanceof Error ? error.message : '同步管理端失败'); } } finally { if (mountedRef.current) setActionBusy(null); } }, [selectedDate]); const cancelAllTasks = useCallback(() => { setSelectedDutyIds(new Set()); selectionDirtyRef.current = true; setSelectionDirty(true); setActionNotice('已一键取消当前选择,重新选择后点击任务执行生效。'); setActionError(null); }, []); const saveProfileSettings = useCallback(async () => { setActionBusy('update_profile'); setActionError(null); setActionNotice(null); try { const projection = await postDigitalEmployeeWorkdayAction({ action: 'update_profile', date: selectedDate, name: profileForm.name, company: profileForm.company, position: profileForm.position, current_status: profileForm.currentStatus, }); if (!mountedRef.current) return; setWorkdayProjection(projection); setProfileSettingsOpen(false); setActionNotice('数字员工特性设置已保存。'); } catch (error) { if (mountedRef.current) { setActionError(error instanceof Error ? error.message : '保存特性设置失败'); } } finally { if (mountedRef.current) setActionBusy(null); } }, [profileForm, selectedDate]); const saveReportSchedule = useCallback(async () => { setActionBusy('update_report_schedule'); setActionError(null); setActionNotice(null); try { const projection = await postDigitalEmployeeWorkdayAction({ action: 'update_report_schedule', date: selectedDate, report_schedule_time: reportScheduleTime, }); if (!mountedRef.current) return; setWorkdayProjection(projection); setActionNotice(`日报生成时间已设置为 ${reportScheduleTime}。`); } catch (error) { if (mountedRef.current) { setActionError(error instanceof Error ? error.message : '设置日报时间失败'); } } finally { if (mountedRef.current) setActionBusy(null); } }, [reportScheduleTime, selectedDate]); const generateDailyReport = useCallback(async () => { setActionBusy('generate_daily_report'); setActionError(null); setActionNotice(null); try { const projection = await postDigitalEmployeeWorkdayAction({ action: 'generate_daily_report', date: selectedDate, duty_ids: selectedSubmissionDutyIds, report_schedule_time: reportScheduleTime, }); if (!mountedRef.current) return; const latestProjection = projection.daily_report?.status === 'ready' ? projection : await getDigitalEmployeeWorkday(selectedDate); if (!mountedRef.current) return; setWorkdayProjection(latestProjection); setActionNotice('日报已生成,可下载 PDF。'); if (latestProjection.daily_report?.status === 'ready') { setReportPreviewOpen(true); } } catch (error) { if (mountedRef.current) { setActionError(error instanceof Error ? error.message : '生成日报失败'); } } finally { if (mountedRef.current) setActionBusy(null); } }, [reportScheduleTime, selectedDate, selectedDutyIds, selectedSubmissionDutyIds]); const downloadDailyReportPdf = useCallback(() => { window.open(digitalEmployeeReportPdfUrl(selectedDate), '_blank', 'noopener,noreferrer'); }, [selectedDate]); const resolveDecision = useCallback(async (decision: DigitalEmployeeDecision, actionId: string) => { setActionBusy(`${decision.id}:${actionId}`); setActionError(null); setActionNotice(null); try { if (!mountedRef.current) return; const actionLabel = decision.actions.find((action) => action.id === actionId)?.label ?? actionId; setActionNotice(`待实现:已触发“${actionLabel}”演示效果,${DECISION_CHAIN_TODO_TEXT},未改动后端审批或执行状态。`); } catch (error) { if (mountedRef.current) { setActionError(error instanceof Error ? error.message : '处理决策失败'); } } finally { if (mountedRef.current) setActionBusy(null); } }, []); const openDuty = useCallback((planItem: DigitalPlanItem) => { onNavigate({ tab: 'missions', date: selectedDate, missionId: planItem.conversationId || planItem.id }); }, [onNavigate, selectedDate]); const createPlanSchedule = useCallback(async (planItem: DigitalPlanItem) => { const cronExpr = cronExprForDailyTime(scheduleTime); const stamp = Date.now(); const scheduleId = `digital-home-${planItem.planId}-${scheduleTime.replace(':', '')}`; setActionBusy(`schedule:${planItem.id}`); setActionError(null); setActionNotice(null); try { const response = await governanceCommand({ command_kind: 'create_schedule', context_refs: { plan_id: planItem.planId, schedule_id: scheduleId, }, payload: { source: 'digital-home', plan_id: planItem.planId, name: `定时执行:${planItem.title}`, description: `按首页任务管理定时任务:${planItem.title}`, prompt: `请按计划执行:${planItem.title}。${planItem.summary}`, cron_expr: cronExpr, schedule: cronExpr, }, correlation_id: `digital-home-create-schedule-${planItem.planId}-${stamp}`, idempotency_key: `digital-home-create-schedule-${planItem.planId}-${scheduleTime}-${stamp}`, }); if (!response.accepted) { throw new Error(response.message || `调度命令未被接受:${response.command_id || 'create_schedule'}`); } setSchedulePlanItem(null); setActionNotice(`已提交定时:${planItem.title},${scheduleTime} 按 cron 计划执行。`); await fetchProjection(); } catch (error) { if (mountedRef.current) { setActionError(error instanceof Error ? error.message : '定时执行失败'); } } finally { if (mountedRef.current) setActionBusy(null); } }, [fetchProjection, scheduleTime]); const confirmImmediatePlanExecution = useCallback(async (planItem: DigitalPlanItem) => { if (!isPlanItemExecutable(planItem)) { setActionError(planExecutionDisabledReason(planItem)); setActionNotice(null); return; } setActionBusy(`execute:${planItem.id}`); setActionError(null); setActionNotice(null); try { const response = await dispatchPlanNow(planItem.planId); if (response.ok !== true) { throw new Error(response.error || '执行接口未接受请求'); } const dispatchedCount = response.dispatched_tasks?.length ?? 0; setExecutePlanItem(null); setActionNotice(`已触发执行:${planItem.title},已派发 ${dispatchedCount} 个步骤。`); await fetchProjection(); } catch (error) { if (mountedRef.current) { setActionError(error instanceof Error ? error.message : '执行失败'); } } finally { if (mountedRef.current) setActionBusy(null); } }, [fetchProjection]); const dismissWorkdayGuide = useCallback(() => { setGuideOpen(false); setGuideDismissedDate(selectedDate); rememberGuideDismissedDate(selectedDate); }, [selectedDate]); const renderDutyChecklistCard = useCallback((planItem: DigitalPlanItem, mode: 'guide' | 'manager') => { const selected = selectedDutyIds.has(planItem.id); const executeDisabledReason = planExecutionDisabledReason(planItem); return (
{planItem.title} {planItem.implemented ? planItem.statusLabel : '待扩展'}

{planItem.summary}

{planItem.scheduleText} {planItem.resultText}
{!planItem.implemented && 待扩展}
); }, [actionBusy, openDuty, selectedDutyIds, toggleDuty]); const openEventDetail = useCallback((event: DigitalEmployeeWorkEvent) => { setSelectedEvent(event); setFeedInteractionPaused(true); }, []); const closeEventDetail = useCallback(() => { setSelectedEvent(null); setFeedInteractionPaused(false); }, []); const closeEventHistory = useCallback(() => { setEventHistoryOpen(false); setFeedInteractionPaused(false); }, []); const openExecutionHistory = useCallback(() => { setEventHistoryOpen(true); setFeedInteractionPaused(true); }, []); const handleExecutionPanelClick = useCallback((event: ReactMouseEvent) => { if (isExecutionPanelInteractiveTarget(event.target)) return; openExecutionHistory(); }, [openExecutionHistory]); const handleExecutionPanelKeyDown = useCallback((event: ReactKeyboardEvent) => { if (event.key !== 'Enter' && event.key !== ' ') return; if (isExecutionPanelInteractiveTarget(event.target)) return; event.preventDefault(); openExecutionHistory(); }, [openExecutionHistory]); const openEvent = useCallback((event: DigitalEmployeeWorkEvent) => { setSelectedEvent(null); setEventHistoryOpen(false); setFeedInteractionPaused(false); onNavigate({ tab: 'missions', date: selectedDate, missionId: event.conversation_id || event.id }); }, [onNavigate, selectedDate]); return (
数字员工
{employee?.name ?? '同步中'}
{employee?.company ?? '同步中'}
{employee?.position ?? employee?.major ?? '同步中'}
{currentStatusLabel}
任务设置
{managementSyncLabel} {managementSyncSummary}
{managementSyncFailures.length > 0 && (
{managementSyncFailures.map((failure) => (
{syncFailureTime(failure)} {entityTypeLabel(failure.entityType)} {failure.id} {syncFailureSummary(failure)}
))}
)} {(actionNotice || actionError || planDataError || workdayProjection?.demo_reason) && (
{actionError || actionNotice || planDataError || workdayProjection?.demo_reason}
)} {selectionDirty && !actionNotice && !actionError && (
当前选择尚未确认,自动刷新不会覆盖你的勾选。
)}
当前日期 {selectedDateLabel} {visibleTaskListHint}
{selectedDateLabel} {visibleTaskListLabel} {visibleTaskItems.length}
{projectionLoading ? (
) : planItems.length === 0 ? (

暂无任务,首页不会再展示硬编码任务。

) : visibleTaskItems.length === 0 ? (

当前筛选下暂无任务,点击总任务查看全部内容。

) : ( visibleTaskItems.map((planItem) => (
)) )}
数字员工播报

{bubbleText}

今日执行

{projectionSourceLabel}
已选任务{selectedDuties.length} 运行记录{visibleEvents.length} 已完成{completedEvents.length} 异常{failedEvents.length}
{taskSummaryCards.length === 0 ? (

左侧选择任务后,这里会按任务生成执行次数和成功情况。

) : ( taskSummaryCards.map((summary) => ( )) )}
展示 {visibleEvents.length} / 共 {allEvents.length} 条 {feedPaused ? ' · 已暂停' : ' · 自动滚动'}
运行明细 展示 {visibleRunDetails.length} / 共 {runDetails.length} 条
时间 任务名称 步骤名称 执行状态 结果
{visibleRunDetails.length === 0 ? (

任务执行后,这里会滚动显示每个步骤的时间、任务、步骤和结果。

) : ( visibleRunDetails.map((detail) => ( )) )}

日报生成

{dailyReport?.status === 'ready' ? '已生成' : '待生成'}

{dailyReport?.summary ?? '点击生成日报后,AI 会基于当天所有任务写出总分结构日报。'}

{(dailyReport?.detail_lines ?? []).slice(0, 3).map((line) =>

{line}

)}
{reportPreviewOpen && dailyReport && (
setReportPreviewOpen(false)}>
event.stopPropagation()} >
DAILY REPORT

{dailyReport.title || '数字员工日报'}

{selectedDateLabel} 生成时间 {dailyReport.report_schedule_time},来源:{dailyReport.generated_by === 'ai' ? 'AI 生成' : '规则兜底'}

任务总数{reportTaskCount}
执行次数{reportExecutionCount}
成功次数{reportSuccessCount}
失败次数{reportFailureCount}
{reportPreviewLines.length === 0 ? (

当前日报暂无可预览文字。

) : ( reportPreviewLines.map((line, index) => (

{line}

)) )}
)} {profileSettingsOpen && (
setProfileSettingsOpen(false)}>
event.stopPropagation()} >
PROFILE

特性设置

维护数字员工在首页展示的姓名、单位、岗位和当前状态。

当前状态
)} {selectedEvent && (
event.stopPropagation()} >
EXECUTION DETAIL

执行记录详情

这条记录已被锁定,滚动不会把它刷掉。

业务{selectedEvent.plan_title}
操作{selectedEvent.task_title}
状态{eventStatusText(selectedEvent)}
计划时间{eventScheduleTime(selectedEvent)}
执行时间{eventDisplayTime(selectedEvent)}
数据来源{projectionSourceLabel}
操作结果

{eventBusinessResult(selectedEvent)}

)} {eventHistoryOpen && (
event.stopPropagation()} >
EXECUTION HISTORY

运行明细

当前日期共 {allEvents.length} 条记录,本窗口展示最新 {Math.min(filteredHistoryEvents.length, EVENT_HISTORY_RENDER_LIMIT)} 条。

{EVENT_STATUS_FILTERS.map((filter) => ( ))}
{filteredHistoryEvents.length === 0 ? (

当前筛选下没有执行记录。

) : ( visibleHistoryEvents.map((event) => ( )) )}
)} {schedulePlanItem && (
setSchedulePlanItem(null)}>
event.stopPropagation()} >
PLAN SCHEDULE

定时任务

定时会把任务注册到后端调度链路,每天按设置时间自动派发。

任务

{schedulePlanItem.title}

调度注册{cronExprForDailyTime(scheduleTime)}
任务步骤{schedulePlanItem.stepCount} 步
执行步骤{schedulePlanItem.taskCount} 项
这里不是本地假定时;提交后如果后端调度链路不可用,会直接显示真实失败原因。
)} {executePlanItem && (
setExecutePlanItem(null)}>
event.stopPropagation()} >
PLAN EXECUTION

执行任务

确认后立即创建并派发任务步骤,不需要先进入任务中心设置。

任务

{executePlanItem.title}

当前状态{executePlanItem.statusLabel}
任务步骤{executePlanItem.stepCount} 步
执行步骤{executePlanItem.taskCount} 项
{isPlanItemExecutable(executePlanItem) ? '确认后立即调用后端派发接口,按任务步骤进入真实执行链路。' : planExecutionDisabledReason(executePlanItem)}
)} {guideOpen && needsWorkdayConfirmation && (
event.stopPropagation()} >
WORKDAY START

开始今天的任务

像值班员早上交接一样,先确认日期和执行内容,再让数字员工开始接单、外呼、汇总和汇报。

{WORKDAY_GUIDE_STEPS.map((step, index) => (
{index + 1} {step.title}

{step.body}

))}
选择今天要执行的任务 {selectedDuties.length} 项已选,{totalDuties.length} 项总任务,{pendingDuties.length} 项待选择
{projectionSourceLabel}
{projectionLoading ? (
) : planItems.length === 0 ? (

暂无任务,首页不再展示硬编码任务。请先在任务中心创建或同步。

) : ( planItems.map((planItem) => renderDutyChecklistCard(planItem, 'guide')) )}
)} {taskManagerOpen && (
setTaskManagerOpen(false)}>
event.stopPropagation()} >
TASK MANAGER

任务设置

像员工早上确认工作一样,勾选今天要执行的内容,必要时进入详情或处理待决策事项。

任务设置清单 {selectedDuties.length} 项已选,{totalDuties.length} 项总任务,{pendingDuties.length} 项待选择
{projectionLoading ? (
) : planItems.length === 0 ? (

暂无任务,首页不会显示硬编码任务。可进入任务中心创建。

) : ( planItems.map((planItem) => renderDutyChecklistCard(planItem, 'manager')) )}
)}
); }