feat: summarize digital sync records
This commit is contained in:
@@ -8,7 +8,7 @@ import type {
|
|||||||
FormInstance,
|
FormInstance,
|
||||||
ProColumns,
|
ProColumns,
|
||||||
} from '@ant-design/pro-components';
|
} from '@ant-design/pro-components';
|
||||||
import { Button, Drawer, message, Tag, Typography } from 'antd';
|
import { Button, Drawer, message, Space, Tag, Typography } from 'antd';
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useLocation, useModel, useSearchParams } from 'umi';
|
import { useLocation, useModel, useSearchParams } from 'umi';
|
||||||
|
|
||||||
@@ -30,6 +30,29 @@ const ENTITY_TYPE_TONE: Record<string, string> = {
|
|||||||
approval: 'orange',
|
approval: 'orange',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const STATUS_TONE: Record<string, string> = {
|
||||||
|
running: 'processing',
|
||||||
|
pending: 'warning',
|
||||||
|
completed: 'success',
|
||||||
|
success: 'success',
|
||||||
|
synced: 'success',
|
||||||
|
failed: 'error',
|
||||||
|
error: 'error',
|
||||||
|
cancelled: 'default',
|
||||||
|
};
|
||||||
|
|
||||||
|
interface PayloadObject {
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SyncBusinessSummary {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
status?: string;
|
||||||
|
statusTone: string;
|
||||||
|
meta: string[];
|
||||||
|
}
|
||||||
|
|
||||||
function formatPayload(payload?: string): string {
|
function formatPayload(payload?: string): string {
|
||||||
if (!payload) return '{}';
|
if (!payload) return '{}';
|
||||||
try {
|
try {
|
||||||
@@ -39,6 +62,127 @@ function formatPayload(payload?: string): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parsePayload(payload?: string): unknown {
|
||||||
|
if (!payload) return null;
|
||||||
|
try {
|
||||||
|
return JSON.parse(payload);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPayloadObject(value: unknown): value is PayloadObject {
|
||||||
|
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function valueAsString(value: unknown): string {
|
||||||
|
if (typeof value === 'string') return value.trim();
|
||||||
|
if (typeof value === 'number' || typeof value === 'boolean')
|
||||||
|
return String(value);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickString(payload: PayloadObject, keys: string[]): string {
|
||||||
|
for (const key of keys) {
|
||||||
|
const value = valueAsString(payload[key]);
|
||||||
|
if (value) return value;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function compactSummaryText(value: string, maxLength = 96): string {
|
||||||
|
const text = value.replace(/\s+/g, ' ').trim();
|
||||||
|
return text.length > maxLength ? `${text.slice(0, maxLength)}...` : text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function entityLabel(entityType: string): string {
|
||||||
|
return (
|
||||||
|
ENTITY_TYPE_OPTIONS[entityType as keyof typeof ENTITY_TYPE_OPTIONS]?.text ||
|
||||||
|
entityType ||
|
||||||
|
'Entity'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusTone(status?: string): string {
|
||||||
|
return status ? STATUS_TONE[status.toLowerCase()] || 'default' : 'default';
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildBusinessSummary(
|
||||||
|
record: DigitalEmployeeSyncRecordInfo,
|
||||||
|
): SyncBusinessSummary {
|
||||||
|
const payload = parsePayload(record.payload);
|
||||||
|
const entity = entityLabel(record.entity_type);
|
||||||
|
if (!isPayloadObject(payload)) {
|
||||||
|
return {
|
||||||
|
title: `${entity} ${record.entity_id}`,
|
||||||
|
description: 'Payload 暂无法解析,查看详情可读取原始内容。',
|
||||||
|
status: record.operation,
|
||||||
|
statusTone: 'default',
|
||||||
|
meta: [record.outbox_id],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = pickString(payload, ['status', 'currentStatus', 'kind']);
|
||||||
|
const title =
|
||||||
|
pickString(payload, ['title', 'name', 'message', 'objective']) ||
|
||||||
|
`${entity} ${record.entity_id}`;
|
||||||
|
const description = compactSummaryText(
|
||||||
|
pickString(payload, ['objective', 'summary', 'message', 'uri']) ||
|
||||||
|
record.outbox_id,
|
||||||
|
);
|
||||||
|
const meta = [
|
||||||
|
pickString(payload, ['planId', 'plan_id']) &&
|
||||||
|
`Plan ${pickString(payload, ['planId', 'plan_id'])}`,
|
||||||
|
pickString(payload, ['taskId', 'task_id']) &&
|
||||||
|
`Task ${pickString(payload, ['taskId', 'task_id'])}`,
|
||||||
|
pickString(payload, ['runId', 'run_id']) &&
|
||||||
|
`Run ${pickString(payload, ['runId', 'run_id'])}`,
|
||||||
|
pickString(payload, ['assignedSkillId']) &&
|
||||||
|
`Skill ${pickString(payload, ['assignedSkillId'])}`,
|
||||||
|
pickString(payload, ['occurred_at', 'started_at', 'finished_at']),
|
||||||
|
].filter((item): item is string => Boolean(item));
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: compactSummaryText(title, 72),
|
||||||
|
description,
|
||||||
|
status: status || record.operation,
|
||||||
|
statusTone: statusTone(status),
|
||||||
|
meta: meta.slice(0, 3),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderBusinessSummary(record: DigitalEmployeeSyncRecordInfo) {
|
||||||
|
const summary = buildBusinessSummary(record);
|
||||||
|
return (
|
||||||
|
<div style={{ minWidth: 0 }}>
|
||||||
|
<Space size={6} wrap>
|
||||||
|
<Typography.Text strong ellipsis style={{ maxWidth: 260 }}>
|
||||||
|
{summary.title}
|
||||||
|
</Typography.Text>
|
||||||
|
{summary.status && (
|
||||||
|
<Tag color={summary.statusTone}>{summary.status}</Tag>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
<Typography.Text
|
||||||
|
type="secondary"
|
||||||
|
ellipsis
|
||||||
|
style={{ display: 'block', maxWidth: 330, fontSize: 12 }}
|
||||||
|
>
|
||||||
|
{summary.description}
|
||||||
|
</Typography.Text>
|
||||||
|
{summary.meta.length > 0 && (
|
||||||
|
<Space size={[4, 4]} wrap style={{ marginTop: 4 }}>
|
||||||
|
{summary.meta.map((item) => (
|
||||||
|
<Tag key={item} color="default" style={{ marginInlineEnd: 0 }}>
|
||||||
|
{item}
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
</Space>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function readSearchValue(
|
function readSearchValue(
|
||||||
params: URLSearchParams,
|
params: URLSearchParams,
|
||||||
key: string,
|
key: string,
|
||||||
@@ -139,13 +283,20 @@ const DigitalEmployeeSync: React.FC = () => {
|
|||||||
{
|
{
|
||||||
title: '实体 ID',
|
title: '实体 ID',
|
||||||
dataIndex: 'entity_id',
|
dataIndex: 'entity_id',
|
||||||
width: 220,
|
width: 180,
|
||||||
ellipsis: true,
|
ellipsis: true,
|
||||||
fieldProps: {
|
fieldProps: {
|
||||||
placeholder: '输入实体 ID',
|
placeholder: '输入实体 ID',
|
||||||
allowClear: true,
|
allowClear: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: '业务摘要',
|
||||||
|
dataIndex: 'payload',
|
||||||
|
width: 360,
|
||||||
|
hideInSearch: true,
|
||||||
|
render: (_, record) => renderBusinessSummary(record),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'Outbox ID',
|
title: 'Outbox ID',
|
||||||
dataIndex: 'outbox_id',
|
dataIndex: 'outbox_id',
|
||||||
@@ -258,12 +409,15 @@ const DigitalEmployeeSync: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
<Drawer
|
<Drawer
|
||||||
width={720}
|
width={720}
|
||||||
title="同步 Payload"
|
title="同步详情"
|
||||||
open={Boolean(payloadRecord)}
|
open={Boolean(payloadRecord)}
|
||||||
onClose={() => setPayloadRecord(null)}
|
onClose={() => setPayloadRecord(null)}
|
||||||
>
|
>
|
||||||
{payloadRecord && (
|
{payloadRecord && (
|
||||||
<>
|
<>
|
||||||
|
<div style={{ marginBottom: 16 }}>
|
||||||
|
{renderBusinessSummary(payloadRecord)}
|
||||||
|
</div>
|
||||||
<Typography.Paragraph>
|
<Typography.Paragraph>
|
||||||
<Typography.Text strong>实体:</Typography.Text>
|
<Typography.Text strong>实体:</Typography.Text>
|
||||||
{payloadRecord.entity_type} / {payloadRecord.entity_id}
|
{payloadRecord.entity_type} / {payloadRecord.entity_id}
|
||||||
|
|||||||
@@ -486,6 +486,7 @@ GET /api/digital-employee/sync/records
|
|||||||
- 按当前登录上下文租户隔离,默认每页 20 条,最大每页 100 条。
|
- 按当前登录上下文租户隔离,默认每页 20 条,最大每页 100 条。
|
||||||
- 当前返回通用同步记录和原始 payload,用于管理端页面或联调工具先查看同步结果;后续再拆分为更丰富的 Plan / Task / Run / Event 查询模型。
|
- 当前返回通用同步记录和原始 payload,用于管理端页面或联调工具先查看同步结果;后续再拆分为更丰富的 Plan / Task / Run / Event 查询模型。
|
||||||
- 管理端已增加“数字员工同步记录”菜单、权限资源和列表页,可按客户端 Key、实体类型、实体 ID、Outbox ID 查询并查看 payload;URL 查询参数会预填筛选条件,并在命中 Outbox ID 时自动打开 payload 抽屉。
|
- 管理端已增加“数字员工同步记录”菜单、权限资源和列表页,可按客户端 Key、实体类型、实体 ID、Outbox ID 查询并查看 payload;URL 查询参数会预填筛选条件,并在命中 Outbox ID 时自动打开 payload 抽屉。
|
||||||
|
- 管理端同步记录页会从通用 payload 中提炼标题、状态、Plan/Task/Run 关联、发生时间等业务摘要,列表和详情抽屉不再只能依赖原始 JSON 排障。
|
||||||
|
|
||||||
下一步:
|
下一步:
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user