feat(client): show digital sync failure details
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user