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"

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,7 +13,7 @@
console.warn("[qimingclaw] digital employee auth bootstrap skipped", error);
}
</script>
<script type="module" crossorigin src="./assets/index-gtzF6MAO.js"></script>
<script type="module" crossorigin src="./assets/index-Dfb8OO8K.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-ReBtI0Fy.css">
</head>
<body>

View File

@@ -447,7 +447,7 @@ crates/agent-electron-client/src/preload/webviewPerfBridge.ts
- 数字员工首页已增加管理端同步状态条,展示待同步数量、失败数量、可重试/退避分布、失败实体类型分布、最近同步时间和最近失败原因摘要。
- 同步状态通过 bridge 返回最近失败 outbox 明细首页可直接看到实体类型、outbox ID、操作类型、重试次数、失败时间和下次自动重试时间。
- 本地 SQLite 额外记录 `digital_sync_attempts`,每次 outbox 推送成功、失败或部分 ack 缺失都会留下一次尝试历史,默认保留最近 30 天。
- 点击失败 outbox 可打开详情弹窗,查看完整错误、实体 ID、退避状态、最近重试历史并可触发手动 `flushSync` 立即重试若已配置管理端同步地址,详情弹窗可直接打开对应的管理端同步记录深链。
- 点击失败 outbox 可打开详情弹窗,查看完整错误、实体 ID、退避状态、最近重试历史并可复制或导出诊断 JSON也可触发手动 `flushSync` 立即重试若已配置管理端同步地址,详情弹窗可直接打开对应的管理端同步记录深链。
- qimingclaw 自动生成的管理端深链只携带 `entity_type``entity_id``outbox_id`,避免在 URL 中暴露客户端 Key管理端列表仍保留客户端 Key 手动筛选能力。
新增 IPC / Bridge