feat(client): summarize digital sync failures

This commit is contained in:
baiyanyun
2026-06-07 01:51:15 +08:00
parent cef3fe4a43
commit 38ece7e3d3
6 changed files with 95 additions and 8 deletions

View File

@@ -61,9 +61,17 @@ interface QimingclawSyncStatus {
failed: number;
lastSyncAt: string | null;
lastSyncError: string | null;
failureSummary?: QimingclawSyncFailureSummary;
recentFailures?: QimingclawSyncFailure[];
}
interface QimingclawSyncFailureSummary {
total: number;
dueForRetry: number;
backoff: number;
byEntityType: Array<{ entityType: string; count: number }>;
}
interface QimingclawSyncAttempt {
attemptNo: number;
status: string;

View File

@@ -61,9 +61,17 @@ interface ManagementSyncStatus {
failed: number;
lastSyncAt: string | null;
lastSyncError: string | null;
failureSummary?: ManagementSyncFailureSummary;
recentFailures?: ManagementSyncFailure[];
}
interface ManagementSyncFailureSummary {
total: number;
dueForRetry: number;
backoff: number;
byEntityType: Array<{ entityType: string; count: number }>;
}
interface ManagementSyncAttempt {
attemptNo: number;
status: string;
@@ -461,9 +469,21 @@ function syncStatusSummary(status: ManagementSyncStatus | null): string {
if (!status.endpoint) return '管理端同步地址未配置。';
const error = summarizeSyncError(status.lastSyncError);
if (status.failed > 0 || error) {
const retrySummary = status.failureSummary
? [
status.failureSummary.dueForRetry > 0
? `${status.failureSummary.dueForRetry} 条可重试`
: '',
status.failureSummary.backoff > 0
? `${status.failureSummary.backoff} 条退避中`
: '',
failureGroupSummary(status.failureSummary),
].filter(Boolean).join('')
: '';
return [
status.failed > 0 ? `${status.failed} 条失败` : '',
status.pending > 0 ? `${status.pending} 条待同步` : '',
retrySummary,
error ? `失败原因:${error}` : '',
].filter(Boolean).join('');
}
@@ -486,6 +506,14 @@ function entityTypeLabel(entityType: string): string {
return labels[entityType] || entityType || 'Entity';
}
function failureGroupSummary(summary: ManagementSyncFailureSummary): string {
const groups = summary.byEntityType
.slice(0, 3)
.map((group) => `${entityTypeLabel(group.entityType)} ${group.count}`)
.join(' / ');
return groups ? `类型:${groups}` : '';
}
function syncFailureSummary(failure: ManagementSyncFailure): string {
const error = summarizeSyncError(failure.error);
return [

View File

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

View File

@@ -45,6 +45,18 @@ export interface DigitalEmployeeSyncAttempt {
finishedAt: string;
}
export interface DigitalEmployeeSyncFailureGroup {
entityType: string;
count: number;
}
export interface DigitalEmployeeSyncFailureSummary {
total: number;
dueForRetry: number;
backoff: number;
byEntityType: DigitalEmployeeSyncFailureGroup[];
}
export interface DigitalEmployeeSyncFailure {
id: string;
entityType: string;
@@ -87,6 +99,7 @@ export interface DigitalEmployeeSyncStatus {
failed: number;
lastSyncAt: string | null;
lastSyncError: string | null;
failureSummary: DigitalEmployeeSyncFailureSummary;
recentFailures: DigitalEmployeeSyncFailure[];
}
@@ -123,6 +136,7 @@ export function getDigitalEmployeeSyncStatus(): DigitalEmployeeSyncStatus {
failed: countOutbox("failed"),
lastSyncAt,
lastSyncError,
failureSummary: readFailureSummary(),
recentFailures: readRecentFailures(config),
};
}
@@ -467,6 +481,43 @@ function countOutbox(status: string): number {
return row?.count ?? 0;
}
function readFailureSummary(): DigitalEmployeeSyncFailureSummary {
const db = getDb();
if (!db) return { total: 0, dueForRetry: 0, backoff: 0, byEntityType: [] };
const rows = db
.prepare(
`
SELECT entity_type, attempts, last_attempt_at
FROM digital_sync_outbox
WHERE status = 'failed'
`,
)
.all() as Array<{
entity_type: string;
attempts: number;
last_attempt_at: string | null;
}>;
const groups = new Map<string, number>();
let dueForRetry = 0;
for (const row of rows) {
groups.set(row.entity_type, (groups.get(row.entity_type) || 0) + 1);
const nextRetryAt = nextRetryAtForFailure(row.last_attempt_at, row.attempts);
if (!nextRetryAt || Date.now() >= Date.parse(nextRetryAt)) {
dueForRetry += 1;
}
}
return {
total: rows.length,
dueForRetry,
backoff: rows.length - dueForRetry,
byEntityType: Array.from(groups.entries())
.map(([entityType, count]) => ({ entityType, count }))
.sort((left, right) => right.count - left.count),
};
}
function readRecentFailures(
config: DigitalEmployeeSyncConfig,
limit = 3,