feat(client): project digital employee runtime into UI

This commit is contained in:
baiyanyun
2026-06-07 17:46:07 +08:00
parent 8ff1c57b83
commit b07f133ed6
15 changed files with 2639 additions and 252 deletions

View File

@@ -4,7 +4,6 @@ import { Clipboard, Clock3, Download, Eye, PlayCircle, RefreshCw } from 'lucide-
import Avatar3D from '@/components/digital/Avatar3D';
import { basePath } from '@/lib/basePath';
import {
digitalEmployeeReportPdfUrl,
dispatchPlanNow,
getDigitalEmployeeWorkday,
getSchedulerJobs,
@@ -14,6 +13,7 @@ import {
import { isTauri } from '@/lib/tauri';
import type {
CronJob,
DigitalEmployeeDailyReport,
DigitalEmployeeDecision,
DigitalEmployeeDuty,
DigitalEmployeeRunDetail,
@@ -590,8 +590,8 @@ function syncFailureDiagnosticFileName(failure: ManagementSyncFailure): string {
return `qimingclaw-sync-failure-${entity || failure.id || 'diagnostic'}.json`;
}
function downloadTextFile(filename: string, content: string): void {
const blob = new Blob([content], { type: 'application/json;charset=utf-8' });
function downloadTextFile(filename: string, content: string, mimeType = 'application/json;charset=utf-8'): void {
const blob = new Blob([content], { type: mimeType });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
@@ -602,6 +602,47 @@ function downloadTextFile(filename: string, content: string): void {
window.setTimeout(() => URL.revokeObjectURL(url), 1000);
}
function dailyReportFileName(date: string): string {
return `qimingclaw-digital-report-${date}.txt`;
}
function buildDailyReportDownloadText(
dateLabel: string,
report: DigitalEmployeeDailyReport,
summaries: DigitalEmployeeTaskExecutionSummary[],
details: DigitalEmployeeRunDetail[],
): string {
return [
report.title || `${dateLabel} 数字员工日报`,
`日期:${dateLabel}`,
`生成时间:${report.generated_at ? formatBeijingDateTime(report.generated_at) : '暂未生成'}`,
`生成来源:${report.generated_by === 'ai' ? 'AI' : 'qimingclaw adapter'}`,
'',
'## 摘要',
report.summary || '暂无摘要。',
'',
'## 指标',
`任务总数:${report.totals.task_count}`,
`执行次数:${report.totals.execution_count}`,
`成功次数:${report.totals.success_count}`,
`失败次数:${report.totals.failure_count}`,
`运行中:${report.totals.running_count ?? 0}`,
'',
'## 任务汇总',
...(summaries.length > 0
? summaries.map((summary) => `- ${summary.title}${summary.latest_status_label},执行 ${summary.total_count} 次,成功 ${summary.success_count},失败 ${summary.failure_count}${summary.ai_summary || summary.business_summary || ''}`)
: ['暂无任务汇总。']),
'',
'## 运行明细',
...(details.length > 0
? details.map((detail) => `- ${detail.time || '--:--'} ${detail.task_name} / ${detail.step_name || detail.operation_name || '任务执行'}${detail.status_label}${detail.business_detail}`)
: ['暂无运行明细。']),
'',
'## 详情',
...((report.detail_lines ?? []).length > 0 ? report.detail_lines : ['暂无详情。']),
].join('\n');
}
async function readManagementSyncStatus(): Promise<ManagementSyncStatus | null> {
const bridge = window.QimingClawBridge?.digital;
if (!bridge?.getSyncStatus) return null;
@@ -734,8 +775,6 @@ function resolveEmployeeVisualState(args: {
return 'skill';
}
const DECISION_CHAIN_TODO_TEXT = '真实审批链路待接入';
function readGuideDismissedDate(): string | null {
if (typeof window === 'undefined') return null;
try {
@@ -1207,7 +1246,7 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
: await getDigitalEmployeeWorkday(selectedDate);
if (!mountedRef.current) return;
setWorkdayProjection(latestProjection);
setActionNotice('日报已生成,可下载 PDF。');
setActionNotice('日报已生成,可下载本地文本。');
if (latestProjection.daily_report?.status === 'ready') {
setReportPreviewOpen(true);
}
@@ -1220,18 +1259,31 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
}
}, [reportScheduleTime, selectedDate, selectedDutyIds, selectedSubmissionDutyIds]);
const downloadDailyReportPdf = useCallback(() => {
window.open(digitalEmployeeReportPdfUrl(selectedDate), '_blank', 'noopener,noreferrer');
}, [selectedDate]);
const downloadDailyReport = useCallback(() => {
if (!dailyReport || dailyReport.status !== 'ready') return;
downloadTextFile(
dailyReportFileName(selectedDate),
buildDailyReportDownloadText(selectedDateLabel, dailyReport, taskSummaryCards, visibleRunDetails),
'text/plain;charset=utf-8',
);
setActionNotice('日报文本已下载。');
}, [dailyReport, selectedDate, selectedDateLabel, taskSummaryCards, visibleRunDetails]);
const resolveDecision = useCallback(async (decision: DigitalEmployeeDecision, actionId: string) => {
setActionBusy(`${decision.id}:${actionId}`);
setActionError(null);
setActionNotice(null);
try {
const projection = await postDigitalEmployeeWorkdayAction({
action: 'decide_approval',
date: selectedDate,
decision_id: decision.id,
decision: actionId,
});
if (!mountedRef.current) return;
const actionLabel = decision.actions.find((action) => action.id === actionId)?.label ?? actionId;
setActionNotice(`待实现:已触发“${actionLabel}”演示效果,${DECISION_CHAIN_TODO_TEXT},未改动后端审批或执行状态。`);
setWorkdayProjection(projection);
setActionNotice(`已记录“${actionLabel}”处理结果,并写入 qimingclaw 本地运行状态。`);
} catch (error) {
if (mountedRef.current) {
setActionError(error instanceof Error ? error.message : '处理决策失败');
@@ -1239,7 +1291,7 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
} finally {
if (mountedRef.current) setActionBusy(null);
}
}, []);
}, [selectedDate]);
const openDuty = useCallback((planItem: DigitalPlanItem) => {
onNavigate({ tab: 'missions', date: selectedDate, missionId: planItem.conversationId || planItem.id });
@@ -1804,8 +1856,8 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
type="button"
className="de-btn-secondary de-btn-sm de-report-download-btn"
disabled={!dailyReport?.pdf_available}
onClick={downloadDailyReportPdf}
title={dailyReport?.pdf_available ? '下载日报 PDF' : '生成日报后可下载日报 PDF'}
onClick={downloadDailyReport}
title={dailyReport?.pdf_available ? '下载本地日报文本' : '生成日报后可下载本地日报'}
>
<Download size={14} aria-hidden="true" />
@@ -1859,7 +1911,7 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
type="button"
className="de-btn-secondary de-btn-sm de-report-download-btn"
disabled={!dailyReport.pdf_available}
onClick={downloadDailyReportPdf}
onClick={downloadDailyReport}
>
<Download size={14} aria-hidden="true" />
@@ -2392,8 +2444,8 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
</div>
<p>{decision.scenario}</p>
<div className="de-decision-chain-note">
<span className="de-badge de-badge-warning"></span>
<span>{DECISION_CHAIN_TODO_TEXT}</span>
<span className="de-badge de-badge-success"></span>
<span>{isDecisionOpen(decision.status) ? '处理结果会写入 qimingclaw runtime event。' : '该事项已在本地状态中处理。'}</span>
</div>
<div className="de-workbench-actions">
{decision.actions.map((action) => (