feat: deep link digital sync failures
This commit is contained in:
@@ -11,6 +11,7 @@ public interface DigitalEmployeeSyncApplicationService {
|
||||
String clientKey,
|
||||
String entityType,
|
||||
String entityId,
|
||||
String outboxId,
|
||||
Long pageNo,
|
||||
Long pageSize
|
||||
);
|
||||
|
||||
@@ -10,6 +10,7 @@ public interface DigitalEmployeeSyncRecordRepository extends IService<DigitalEmp
|
||||
String clientKey,
|
||||
String entityType,
|
||||
String entityId,
|
||||
String outboxId,
|
||||
long pageNo,
|
||||
long pageSize
|
||||
);
|
||||
|
||||
@@ -93,6 +93,7 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
|
||||
String clientKey,
|
||||
String entityType,
|
||||
String entityId,
|
||||
String outboxId,
|
||||
Long pageNo,
|
||||
Long pageSize
|
||||
) {
|
||||
@@ -115,6 +116,7 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
|
||||
clientKey,
|
||||
entityType,
|
||||
entityId,
|
||||
outboxId,
|
||||
currentPage,
|
||||
currentPageSize
|
||||
);
|
||||
|
||||
@@ -21,6 +21,7 @@ public class DigitalEmployeeSyncRecordRepositoryImpl
|
||||
String clientKey,
|
||||
String entityType,
|
||||
String entityId,
|
||||
String outboxId,
|
||||
long pageNo,
|
||||
long pageSize
|
||||
) {
|
||||
@@ -29,6 +30,7 @@ public class DigitalEmployeeSyncRecordRepositoryImpl
|
||||
.eq(StringUtils.isNotBlank(clientKey), DigitalEmployeeSyncRecord::getClientKey, clientKey)
|
||||
.eq(StringUtils.isNotBlank(entityType), DigitalEmployeeSyncRecord::getEntityType, entityType)
|
||||
.eq(StringUtils.isNotBlank(entityId), DigitalEmployeeSyncRecord::getEntityId, entityId)
|
||||
.eq(StringUtils.isNotBlank(outboxId), DigitalEmployeeSyncRecord::getOutboxId, outboxId)
|
||||
.orderByDesc(DigitalEmployeeSyncRecord::getSyncedAt)
|
||||
.orderByDesc(DigitalEmployeeSyncRecord::getId);
|
||||
return page(new Page<>(pageNo, pageSize), queryWrapper);
|
||||
|
||||
@@ -33,6 +33,7 @@ public class DigitalEmployeeSyncController {
|
||||
@RequestParam(name = "client_key", required = false) String clientKey,
|
||||
@RequestParam(name = "entity_type", required = false) String entityType,
|
||||
@RequestParam(name = "entity_id", required = false) String entityId,
|
||||
@RequestParam(name = "outbox_id", required = false) String outboxId,
|
||||
@RequestParam(name = "page_no", required = false) Long pageNo,
|
||||
@RequestParam(name = "page_size", required = false) Long pageSize
|
||||
) {
|
||||
@@ -40,6 +41,7 @@ public class DigitalEmployeeSyncController {
|
||||
clientKey,
|
||||
entityType,
|
||||
entityId,
|
||||
outboxId,
|
||||
pageNo,
|
||||
pageSize
|
||||
));
|
||||
|
||||
@@ -10,7 +10,7 @@ import type {
|
||||
} from '@ant-design/pro-components';
|
||||
import { Button, Drawer, message, Tag, Typography } from 'antd';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useLocation, useModel } from 'umi';
|
||||
import { useLocation, useModel, useSearchParams } from 'umi';
|
||||
|
||||
const ENTITY_TYPE_OPTIONS = {
|
||||
plan: { text: 'Plan' },
|
||||
@@ -39,11 +39,20 @@ function formatPayload(payload?: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
function readSearchValue(
|
||||
params: URLSearchParams,
|
||||
key: string,
|
||||
): string | undefined {
|
||||
const value = params.get(key)?.trim();
|
||||
return value || undefined;
|
||||
}
|
||||
|
||||
const DigitalEmployeeSync: React.FC = () => {
|
||||
const { hasPermission } = useModel('menuModel');
|
||||
const actionRef = useRef<ActionType>();
|
||||
const formRef = useRef<FormInstance>();
|
||||
const location = useLocation();
|
||||
const [, setSearchParams] = useSearchParams();
|
||||
const [payloadRecord, setPayloadRecord] =
|
||||
useState<DigitalEmployeeSyncRecordInfo | null>(null);
|
||||
const formattedPayload = useMemo(
|
||||
@@ -51,12 +60,32 @@ const DigitalEmployeeSync: React.FC = () => {
|
||||
[payloadRecord],
|
||||
);
|
||||
|
||||
const formInitialValues = useMemo(() => {
|
||||
const params = new URLSearchParams(location.search || '');
|
||||
const values = {
|
||||
client_key: readSearchValue(params, 'client_key'),
|
||||
entity_type: readSearchValue(params, 'entity_type'),
|
||||
entity_id: readSearchValue(params, 'entity_id'),
|
||||
outbox_id: readSearchValue(params, 'outbox_id'),
|
||||
};
|
||||
return Object.values(values).some(Boolean) ? values : undefined;
|
||||
}, [location.search]);
|
||||
|
||||
const handleReset = useCallback(() => {
|
||||
formRef.current?.resetFields();
|
||||
actionRef.current?.reset?.();
|
||||
formRef.current?.setFieldsValue({
|
||||
client_key: undefined,
|
||||
entity_type: undefined,
|
||||
entity_id: undefined,
|
||||
outbox_id: undefined,
|
||||
});
|
||||
actionRef.current?.setPageInfo?.({ current: 1, pageSize: 15 });
|
||||
if (location.search) {
|
||||
setSearchParams(new URLSearchParams());
|
||||
return;
|
||||
}
|
||||
actionRef.current?.reload();
|
||||
}, []);
|
||||
}, [location.search, setSearchParams]);
|
||||
|
||||
useEffect(() => {
|
||||
const state = location.state as any;
|
||||
@@ -116,7 +145,10 @@ const DigitalEmployeeSync: React.FC = () => {
|
||||
dataIndex: 'outbox_id',
|
||||
width: 220,
|
||||
ellipsis: true,
|
||||
hideInSearch: true,
|
||||
fieldProps: {
|
||||
placeholder: '输入 Outbox ID',
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
@@ -164,6 +196,7 @@ const DigitalEmployeeSync: React.FC = () => {
|
||||
client_key?: string;
|
||||
entity_type?: string;
|
||||
entity_id?: string;
|
||||
outbox_id?: string;
|
||||
}) => {
|
||||
const response = await apiDigitalEmployeeSyncRecordList({
|
||||
pageNo: params.current || 1,
|
||||
@@ -171,6 +204,7 @@ const DigitalEmployeeSync: React.FC = () => {
|
||||
clientKey: params.client_key,
|
||||
entityType: params.entity_type,
|
||||
entityId: params.entity_id,
|
||||
outboxId: params.outbox_id,
|
||||
});
|
||||
|
||||
if (response.code !== SUCCESS_CODE) {
|
||||
@@ -187,12 +221,16 @@ const DigitalEmployeeSync: React.FC = () => {
|
||||
return (
|
||||
<WorkspaceLayout title="数字员工同步记录" hideScroll>
|
||||
<XProTable<DigitalEmployeeSyncRecordInfo>
|
||||
key={location.pathname + (location.search || '')}
|
||||
actionRef={actionRef}
|
||||
formRef={formRef}
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
request={request}
|
||||
onReset={handleReset}
|
||||
form={
|
||||
formInitialValues ? { initialValues: formInitialValues } : undefined
|
||||
}
|
||||
showQueryButtons={hasPermission(
|
||||
'content_digital_employee_sync_query_list',
|
||||
)}
|
||||
|
||||
@@ -276,6 +276,7 @@ export async function apiDigitalEmployeeSyncRecordList(
|
||||
client_key: data.clientKey,
|
||||
entity_type: data.entityType,
|
||||
entity_id: data.entityId,
|
||||
outbox_id: data.outboxId,
|
||||
page_no: data.pageNo,
|
||||
page_size: data.pageSize,
|
||||
},
|
||||
|
||||
@@ -544,6 +544,7 @@ export interface DigitalEmployeeSyncRecordListParams
|
||||
clientKey?: string;
|
||||
entityType?: string;
|
||||
entityId?: string;
|
||||
outboxId?: string;
|
||||
}
|
||||
|
||||
// 数字员工同步记录分页响应
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -73,6 +73,7 @@ interface QimingclawSyncFailure {
|
||||
lastAttemptAt: string | null;
|
||||
nextRetryAt?: string | null;
|
||||
dueForRetry?: boolean;
|
||||
managementRecordUrl?: string | null;
|
||||
error: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
|
||||
@@ -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>
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -446,7 +446,7 @@ crates/agent-electron-client/src/preload/webviewPerfBridge.ts
|
||||
- 数字员工任务设置工具栏已增加“同步管理端”按钮,可通过 bridge 立即触发 `flushSync` 并刷新页面投影。
|
||||
- 数字员工首页已增加管理端同步状态条,展示待同步数量、失败数量、最近同步时间和最近失败原因摘要。
|
||||
- 同步状态通过 bridge 返回最近失败 outbox 明细,首页可直接看到实体类型、outbox ID、操作类型、重试次数、失败时间和下次自动重试时间。
|
||||
- 点击失败 outbox 可打开详情弹窗,查看完整错误、实体 ID、退避状态,并可触发手动 `flushSync` 立即重试。
|
||||
- 点击失败 outbox 可打开详情弹窗,查看完整错误、实体 ID、退避状态,并可触发手动 `flushSync` 立即重试;若已配置管理端同步地址,详情弹窗可直接打开对应的管理端同步记录深链。
|
||||
|
||||
新增 IPC / Bridge:
|
||||
|
||||
@@ -479,15 +479,15 @@ qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-a
|
||||
GET /api/digital-employee/sync/records
|
||||
```
|
||||
|
||||
- 支持 `client_key`、`entity_type`、`entity_id`、`page_no`、`page_size` 查询参数。
|
||||
- 支持 `client_key`、`entity_type`、`entity_id`、`outbox_id`、`page_no`、`page_size` 查询参数。
|
||||
- 按当前登录上下文租户隔离,默认每页 20 条,最大每页 100 条。
|
||||
- 当前返回通用同步记录和原始 payload,用于管理端页面或联调工具先查看同步结果;后续再拆分为更丰富的 Plan / Task / Run / Event 查询模型。
|
||||
- 管理端已增加“数字员工同步记录”菜单、权限资源和列表页,可按客户端 Key、实体类型、实体 ID 查询并查看 payload。
|
||||
- 管理端已增加“数字员工同步记录”菜单、权限资源和列表页,可按客户端 Key、实体类型、实体 ID、Outbox ID 查询并查看 payload,且支持 URL 查询参数预填筛选条件。
|
||||
|
||||
下一步:
|
||||
|
||||
- 后续再拆分管理端 Plan / Task / Run / Event 查询模型,让同步记录从通用 payload 观察面升级为业务视图。
|
||||
- 数字员工页面继续补充更完整的同步重试历史和管理端记录深链。
|
||||
- 数字员工页面继续补充更完整的同步重试历史。
|
||||
|
||||
## 管理端联动原则
|
||||
|
||||
|
||||
Reference in New Issue
Block a user