feat(client): export digital sync diagnostics

This commit is contained in:
baiyanyun
2026-06-07 02:22:37 +08:00
parent df2ab51e70
commit 2bf9192fc5
5 changed files with 219 additions and 124 deletions

View File

@@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type { KeyboardEvent as ReactKeyboardEvent, MouseEvent as ReactMouseEvent } from 'react';
import { Clock3, Download, Eye, PlayCircle, RefreshCw } from 'lucide-react';
import { Clipboard, Clock3, Download, Eye, PlayCircle, RefreshCw } from 'lucide-react';
import Avatar3D from '@/components/digital/Avatar3D';
import { basePath } from '@/lib/basePath';
import {
@@ -551,6 +551,57 @@ function syncAttemptTone(status: string): 'success' | 'danger' | 'info' {
return 'info';
}
function buildSyncFailureDiagnostic(
failure: ManagementSyncFailure,
status: ManagementSyncStatus | null,
): string {
return JSON.stringify(
{
generatedAt: new Date().toISOString(),
source: 'qimingclaw-digital-employee',
failure,
sync: status
? {
running: status.running,
syncing: status.syncing,
enabled: status.enabled,
endpoint: status.endpoint,
missingCredentials: status.missingCredentials ?? [],
pending: status.pending,
failed: status.failed,
lastSyncAt: status.lastSyncAt,
lastSyncError: status.lastSyncError,
failureSummary: status.failureSummary ?? null,
}
: null,
},
null,
2,
);
}
function syncFailureDiagnosticFileName(failure: ManagementSyncFailure): string {
const entity = [failure.entityType, failure.entityId]
.filter(Boolean)
.join('-')
.replace(/[^a-z0-9._-]+/gi, '-')
.replace(/^-+|-+$/g, '')
.slice(0, 80);
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' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
link.remove();
window.setTimeout(() => URL.revokeObjectURL(url), 1000);
}
async function readManagementSyncStatus(): Promise<ManagementSyncStatus | null> {
const bridge = window.QimingClawBridge?.digital;
if (!bridge?.getSyncStatus) return null;
@@ -1062,6 +1113,27 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
}
}, [selectedDate]);
const copySyncFailureDiagnostic = useCallback(async (failure: ManagementSyncFailure) => {
const diagnostic = buildSyncFailureDiagnostic(failure, managementSyncStatus);
try {
await navigator.clipboard.writeText(diagnostic);
setActionError(null);
setActionNotice('同步失败诊断已复制。');
} catch (error) {
setActionNotice(null);
setActionError(error instanceof Error ? error.message : '复制同步失败诊断失败');
}
}, [managementSyncStatus]);
const exportSyncFailureDiagnostic = useCallback((failure: ManagementSyncFailure) => {
downloadTextFile(
syncFailureDiagnosticFileName(failure),
buildSyncFailureDiagnostic(failure, managementSyncStatus),
);
setActionError(null);
setActionNotice('同步失败诊断已导出。');
}, [managementSyncStatus]);
const cancelAllTasks = useCallback(() => {
setSelectedDutyIds(new Set());
selectionDirtyRef.current = true;
@@ -1926,6 +1998,24 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
>
</button>
<button
type="button"
className="de-btn-secondary de-btn-sm"
onClick={() => {
void copySyncFailureDiagnostic(selectedSyncFailure);
}}
>
<Clipboard size={14} />
</button>
<button
type="button"
className="de-btn-secondary de-btn-sm"
onClick={() => exportSyncFailureDiagnostic(selectedSyncFailure)}
>
<Download size={14} />
</button>
{selectedSyncFailure.managementRecordUrl && (
<button
type="button"