feat: deep link digital sync failures

This commit is contained in:
baiyanyun
2026-06-07 01:35:08 +08:00
parent 508406ff10
commit 646901b5fa
16 changed files with 142 additions and 33 deletions

View File

@@ -1701,8 +1701,14 @@
.de-event-modal-actions {
display: flex;
justify-content: flex-end;
flex-wrap: wrap;
gap: 8px;
}
.de-event-modal-actions .de-btn-sm {
display: inline-flex;
align-items: center;
gap: 6px;
}
.de-event-history-toolbar {
display: flex;
align-items: center;

View File

@@ -73,6 +73,7 @@ interface QimingclawSyncFailure {
lastAttemptAt: string | null;
nextRetryAt?: string | null;
dueForRetry?: boolean;
managementRecordUrl?: string | null;
error: string | null;
createdAt: string;
updatedAt: string;

View File

@@ -73,6 +73,7 @@ interface ManagementSyncFailure {
lastAttemptAt: string | null;
nextRetryAt?: string | null;
dueForRetry?: boolean;
managementRecordUrl?: string | null;
error: string | null;
createdAt: string;
updatedAt: string;
@@ -1837,12 +1838,36 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
<p>{selectedSyncFailure.error || '暂无错误详情。'}</p>
</div>
<div className="de-event-modal-actions">
<button type="button" className="de-btn-secondary de-btn-sm" onClick={() => setSelectedSyncFailure(null)}></button>
<button
type="button"
className="de-btn-secondary de-btn-sm"
onClick={() => setSelectedSyncFailure(null)}
>
</button>
{selectedSyncFailure.managementRecordUrl && (
<button
type="button"
className="de-btn-secondary de-btn-sm"
onClick={() =>
window.open(
selectedSyncFailure.managementRecordUrl || '',
'_blank',
'noopener,noreferrer',
)
}
>
<Eye size={14} />
</button>
)}
<button
type="button"
className="de-btn-primary de-btn-sm"
disabled={actionBusy === 'flush_management_sync'}
onClick={() => { void flushManagementSync().then(() => setSelectedSyncFailure(null)); }}
onClick={() => {
void flushManagementSync().then(() => setSelectedSyncFailure(null));
}}
>
{actionBusy === 'flush_management_sync' ? '同步中...' : '立即重试'}
</button>

View File

@@ -13,8 +13,8 @@
console.warn("[qimingclaw] digital employee auth bootstrap skipped", error);
}
</script>
<script type="module" crossorigin src="./assets/index-DHy6Ywa5.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-YI8Pgcr5.css">
<script type="module" crossorigin src="./assets/index-BmpavZF5.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-DDOlQz2O.css">
</head>
<body>
<div id="root"></div>

View File

@@ -4,6 +4,8 @@ import { getDomainTokenKey } from "@shared/utils/domain";
import { getDb, readSetting } from "../../db";
const DEFAULT_SYNC_PATH = "/api/digital-employee/sync/outbox";
const MANAGEMENT_SYNC_RECORD_PATH =
"/system/content/content-digital-employee-sync";
const DEFAULT_INTERVAL_MS = 30_000;
const DEFAULT_BATCH_SIZE = 50;
const REQUEST_TIMEOUT_MS = 15_000;
@@ -42,6 +44,7 @@ export interface DigitalEmployeeSyncFailure {
lastAttemptAt: string | null;
nextRetryAt: string | null;
dueForRetry: boolean;
managementRecordUrl: string | null;
error: string | null;
createdAt: string;
updatedAt: string;
@@ -109,7 +112,7 @@ export function getDigitalEmployeeSyncStatus(): DigitalEmployeeSyncStatus {
failed: countOutbox("failed"),
lastSyncAt,
lastSyncError,
recentFailures: readRecentFailures(),
recentFailures: readRecentFailures(config),
};
}
@@ -385,7 +388,10 @@ function countOutbox(status: string): number {
return row?.count ?? 0;
}
function readRecentFailures(limit = 3): DigitalEmployeeSyncFailure[] {
function readRecentFailures(
config: DigitalEmployeeSyncConfig,
limit = 3,
): DigitalEmployeeSyncFailure[] {
const db = getDb();
if (!db) return [];
const rows = db
@@ -422,6 +428,7 @@ function readRecentFailures(limit = 3): DigitalEmployeeSyncFailure[] {
lastAttemptAt: row.last_attempt_at,
nextRetryAt,
dueForRetry: !nextRetryAt || Date.now() >= Date.parse(nextRetryAt),
managementRecordUrl: buildManagementRecordUrl(config, row),
error: row.error,
createdAt: row.created_at,
updatedAt: row.updated_at,
@@ -429,6 +436,28 @@ function readRecentFailures(limit = 3): DigitalEmployeeSyncFailure[] {
});
}
function buildManagementRecordUrl(
config: DigitalEmployeeSyncConfig,
row: {
id: string;
entity_type: string;
entity_id: string;
},
): string | null {
if (!config.endpoint) return null;
try {
const endpoint = new URL(config.endpoint);
const url = new URL(MANAGEMENT_SYNC_RECORD_PATH, endpoint.origin);
if (config.clientKey) url.searchParams.set("client_key", config.clientKey);
url.searchParams.set("entity_type", row.entity_type);
url.searchParams.set("entity_id", row.entity_id);
url.searchParams.set("outbox_id", row.id);
return url.toString();
} catch {
return null;
}
}
function nextRetryAtForFailure(
lastAttemptAt: string | null,
attempts: number,