feat(client): project digital employee runtime into UI
This commit is contained in:
@@ -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) => (
|
||||
|
||||
Reference in New Issue
Block a user