feat(client): summarize digital sync failures
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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 [
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -444,7 +444,7 @@ crates/agent-electron-client/src/preload/webviewPerfBridge.ts
|
||||
- 同步成功后回填 outbox 状态,并根据后端 ack 回填实体 `remote_id` / `last_synced_at`。
|
||||
- 同步失败时标记 failed 和错误信息;定时 worker 按指数退避补偿,手动 `flushSync` 可强制立即重试,不影响 `/computer/chat` 和 ACP 执行链路。
|
||||
- 数字员工任务设置工具栏已增加“同步管理端”按钮,可通过 bridge 立即触发 `flushSync` 并刷新页面投影。
|
||||
- 数字员工首页已增加管理端同步状态条,展示待同步数量、失败数量、最近同步时间和最近失败原因摘要。
|
||||
- 数字员工首页已增加管理端同步状态条,展示待同步数量、失败数量、可重试/退避分布、失败实体类型分布、最近同步时间和最近失败原因摘要。
|
||||
- 同步状态通过 bridge 返回最近失败 outbox 明细,首页可直接看到实体类型、outbox ID、操作类型、重试次数、失败时间和下次自动重试时间。
|
||||
- 本地 SQLite 额外记录 `digital_sync_attempts`,每次 outbox 推送成功、失败或部分 ack 缺失都会留下一次尝试历史,默认保留最近 30 天。
|
||||
- 点击失败 outbox 可打开详情弹窗,查看完整错误、实体 ID、退避状态、最近重试历史,并可触发手动 `flushSync` 立即重试;若已配置管理端同步地址,详情弹窗可直接打开对应的管理端同步记录深链。
|
||||
|
||||
Reference in New Issue
Block a user