feat(client): show digital employee sync errors

This commit is contained in:
baiyanyun
2026-06-07 00:53:28 +08:00
parent e2a91d88e8
commit ac1a6fd0e6
8 changed files with 228 additions and 127 deletions

View File

@@ -1189,6 +1189,24 @@
border: 1px solid rgba(79,172,254,0.18); color: #16324e;
background: rgba(247,251,255,0.96);
}
.de-management-sync-strip {
display: flex; align-items: center; gap: 8px; min-height: 34px;
margin: -2px 0 10px; padding: 7px 10px; border-radius: 12px;
font-size: 12px; line-height: 1.35; color: #315576;
border: 1px solid rgba(79,172,254,0.14); background: rgba(79,172,254,0.07);
}
.de-management-sync-strip > span:last-child {
min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.de-management-sync-strip--success {
color: #176a3a; border-color: rgba(27,156,90,0.2); background: rgba(27,156,90,0.08);
}
.de-management-sync-strip--warning {
color: #7a5509; border-color: rgba(245,173,45,0.22); background: rgba(245,173,45,0.1);
}
.de-management-sync-strip--danger {
color: #ad3434; border-color: rgba(224,72,72,0.2); background: rgba(224,72,72,0.08);
}
.de-duty-card { min-height: 120px; }
.de-duty-card--selected {
border-color: rgba(32,191,107,0.42);

View File

@@ -52,6 +52,17 @@ interface DigitalPlanItem {
source: 'plan_template';
}
interface ManagementSyncStatus {
running: boolean;
syncing: boolean;
enabled: boolean;
endpoint: string | null;
pending: number;
failed: number;
lastSyncAt: string | null;
lastSyncError: string | null;
}
// 技术侧 PlanTemplate 是任务模板来源;用户侧只展示为“任务”,运行时 Task/PlanStep 展示为“步骤”。
interface EmployeeVisualStateOption {
@@ -394,6 +405,59 @@ function formatBeijingDateTime(value?: string | null): string {
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 数据。';
}
async function readManagementSyncStatus(): Promise<ManagementSyncStatus | null> {
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) {
@@ -569,6 +633,7 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
const [reportScheduleTime, setReportScheduleTime] = useState('18:00');
const [guideOpen, setGuideOpen] = useState(false);
const [guideDismissedDate, setGuideDismissedDate] = useState<string | null>(() => readGuideDismissedDate());
const [managementSyncStatus, setManagementSyncStatus] = useState<ManagementSyncStatus | null>(null);
const mountedRef = useRef(false);
const selectionDirtyRef = useRef(false);
@@ -623,7 +688,13 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
setPlanDataError(error instanceof Error ? error.message : '任务加载失败');
});
await Promise.allSettled([projectionPromise, jobsPromise]);
const syncStatusPromise = readManagementSyncStatus()
.then((status) => {
if (!mountedRef.current) return;
setManagementSyncStatus(status);
});
await Promise.allSettled([projectionPromise, jobsPromise, syncStatusPromise]);
if (mountedRef.current) {
setProjectionLoading(false);
}
@@ -749,6 +820,10 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
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 resolvedVisualState = resolveEmployeeVisualState({
confirmed: Boolean(workday?.confirmed),
currentEvent,
@@ -863,6 +938,7 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
}
const syncStatus = await bridge.flushSync();
if (!mountedRef.current) return;
setManagementSyncStatus(syncStatus);
const latestProjection = await getDigitalEmployeeWorkday(selectedDate);
if (!mountedRef.current) return;
setWorkdayProjection(latestProjection);
@@ -1251,6 +1327,11 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
</button>
</div>
<div className={`de-management-sync-strip de-management-sync-strip--${managementSyncTone}`} title={managementSyncError || managementSyncSummary}>
<span className={`de-badge ${badgeClass(managementSyncTone)}`}>{managementSyncLabel}</span>
<span>{managementSyncSummary}</span>
</div>
{(actionNotice || actionError || planDataError || workdayProjection?.demo_reason) && (
<div className={`de-schedule-result ${actionError ? 'de-schedule-result--error' : 'de-schedule-result--success'}`} style={{ marginBottom: 10 }}>
{actionError || actionNotice || planDataError || workdayProjection?.demo_reason}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -13,8 +13,8 @@
console.warn("[qimingclaw] digital employee auth bootstrap skipped", error);
}
</script>
<script type="module" crossorigin src="./assets/index-Ck2YE5qR.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-DqV9Vw1H.css">
<script type="module" crossorigin src="./assets/index-Bpj7JfZ9.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-BcFvWJ2w.css">
</head>
<body>
<div id="root"></div>