feat(client): show digital sync failure details

This commit is contained in:
baiyanyun
2026-06-07 01:05:49 +08:00
parent ac1a6fd0e6
commit 0672e82fc7
10 changed files with 280 additions and 126 deletions

View File

@@ -1207,6 +1207,23 @@
.de-management-sync-strip--danger {
color: #ad3434; border-color: rgba(224,72,72,0.2); background: rgba(224,72,72,0.08);
}
.de-management-sync-failures {
display: grid; gap: 6px; margin: -4px 0 10px;
}
.de-management-sync-failure {
display: grid; grid-template-columns: 56px 54px minmax(54px, 0.7fr) minmax(0, 1.6fr);
align-items: center; gap: 8px; min-height: 28px; padding: 6px 9px;
border-radius: 10px; border: 1px solid rgba(224,72,72,0.14);
background: rgba(224,72,72,0.055); color: #7b4450; font-size: 11px;
}
.de-management-sync-failure span,
.de-management-sync-failure em,
.de-management-sync-failure small {
min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.de-management-sync-failure strong { color: #ad3434; font-weight: 800; }
.de-management-sync-failure em { color: #59738b; font-style: normal; }
.de-management-sync-failure small { color: #6d5260; }
.de-duty-card { min-height: 120px; }
.de-duty-card--selected {
border-color: rgba(32,191,107,0.42);

View File

@@ -61,6 +61,19 @@ interface QimingclawSyncStatus {
failed: number;
lastSyncAt: string | null;
lastSyncError: string | null;
recentFailures?: QimingclawSyncFailure[];
}
interface QimingclawSyncFailure {
id: string;
entityType: string;
entityId: string;
operation: string;
attempts: number;
lastAttemptAt: string | null;
error: string | null;
createdAt: string;
updatedAt: string;
}
interface QimingclawSnapshot {
@@ -372,6 +385,16 @@ function syncLine(snapshot: QimingclawSnapshot | null): string {
return sync.lastSyncAt ? `管理端同步:已同步,最近 ${sync.lastSyncAt}` : '管理端同步:等待本地数据';
}
function compactSyncText(value?: string | null): string {
return String(value || '').replace(/\s+/g, ' ').trim();
}
function syncFailureLine(failure: QimingclawSyncFailure): string {
const error = compactSyncText(failure.error);
const detail = error.length > 120 ? `${error.slice(0, 120)}...` : error;
return `${failure.entityType}/${failure.operation} ${failure.id} 重试 ${failure.attempts}${detail ? `${detail}` : ''}`;
}
function withRuntimePlan(plan: DebugPlan, snapshot: QimingclawSnapshot | null): DebugPlan {
return { ...plan, status: runtimeStatusForPlan(plan, snapshot) };
}
@@ -543,13 +566,21 @@ function buildEvents(duties: DigitalEmployeeDuty[], confirmed: boolean, snapshot
}));
const sync = snapshot?.sync;
if (sync && (sync.pending > 0 || sync.failed > 0 || sync.lastSyncAt)) {
const recentFailureText = (sync.recentFailures || [])
.slice(0, 3)
.map(syncFailureLine)
.join('');
events.push({
id: 'event-management-sync',
time: sync.syncing ? '实时' : '同步',
plan_title: '管理端联动',
task_title: '同步数字员工本地状态',
step_title: sync.failed > 0 ? '同步补偿' : 'outbox 推送',
detail: `${syncLine(snapshot)}${sync.lastSyncError ? `。错误:${sync.lastSyncError}` : ''}`,
detail: [
syncLine(snapshot),
sync.lastSyncError ? `错误:${sync.lastSyncError}` : '',
recentFailureText ? `最近失败:${recentFailureText}` : '',
].filter(Boolean).join('。'),
status: sync.failed > 0 ? 'failed' : sync.syncing || sync.pending > 0 ? 'running' : 'completed',
status_label: sync.failed > 0 ? '异常' : sync.syncing || sync.pending > 0 ? '同步中' : '已同步',
status_tone: sync.failed > 0 ? 'danger' : sync.syncing || sync.pending > 0 ? 'running' : 'success',

View File

@@ -61,6 +61,19 @@ interface ManagementSyncStatus {
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 展示为“步骤”。
@@ -448,6 +461,31 @@ function syncStatusSummary(status: ManagementSyncStatus | null): string {
: '等待本地 outbox 数据。';
}
function entityTypeLabel(entityType: string): string {
const labels: Record<string, string> = {
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<ManagementSyncStatus | null> {
const bridge = window.QimingClawBridge?.digital;
if (!bridge?.getSyncStatus) return null;
@@ -824,6 +862,7 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
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,
@@ -1332,6 +1371,19 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
<span>{managementSyncSummary}</span>
</div>
{managementSyncFailures.length > 0 && (
<div className="de-management-sync-failures" aria-label="同步失败明细">
{managementSyncFailures.map((failure) => (
<div key={failure.id} className="de-management-sync-failure" title={failure.error || failure.id}>
<span>{syncFailureTime(failure)}</span>
<strong>{entityTypeLabel(failure.entityType)}</strong>
<em>{failure.id}</em>
<small>{syncFailureSummary(failure)}</small>
</div>
))}
</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}