From 7eb1498412aff62a3f36f54db33d260acc024eeb Mon Sep 17 00:00:00 2001 From: baiyanyun Date: Sun, 7 Jun 2026 02:25:37 +0800 Subject: [PATCH] feat: summarize digital sync records --- .../Content/DigitalEmployeeSync/index.tsx | 160 +++++++++++++++++- ...ital-employee-frontend-integration-plan.md | 1 + 2 files changed, 158 insertions(+), 3 deletions(-) diff --git a/qiming/src/pages/SystemManagement/Content/DigitalEmployeeSync/index.tsx b/qiming/src/pages/SystemManagement/Content/DigitalEmployeeSync/index.tsx index 52729d4a..46013730 100644 --- a/qiming/src/pages/SystemManagement/Content/DigitalEmployeeSync/index.tsx +++ b/qiming/src/pages/SystemManagement/Content/DigitalEmployeeSync/index.tsx @@ -8,7 +8,7 @@ import type { FormInstance, ProColumns, } 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 { useLocation, useModel, useSearchParams } from 'umi'; @@ -30,6 +30,29 @@ const ENTITY_TYPE_TONE: Record = { approval: 'orange', }; +const STATUS_TONE: Record = { + 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 { if (!payload) return '{}'; 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 ( +
+ + + {summary.title} + + {summary.status && ( + {summary.status} + )} + + + {summary.description} + + {summary.meta.length > 0 && ( + + {summary.meta.map((item) => ( + + {item} + + ))} + + )} +
+ ); +} + function readSearchValue( params: URLSearchParams, key: string, @@ -139,13 +283,20 @@ const DigitalEmployeeSync: React.FC = () => { { title: '实体 ID', dataIndex: 'entity_id', - width: 220, + width: 180, ellipsis: true, fieldProps: { placeholder: '输入实体 ID', allowClear: true, }, }, + { + title: '业务摘要', + dataIndex: 'payload', + width: 360, + hideInSearch: true, + render: (_, record) => renderBusinessSummary(record), + }, { title: 'Outbox ID', dataIndex: 'outbox_id', @@ -258,12 +409,15 @@ const DigitalEmployeeSync: React.FC = () => { /> setPayloadRecord(null)} > {payloadRecord && ( <> +
+ {renderBusinessSummary(payloadRecord)} +
实体: {payloadRecord.entity_type} / {payloadRecord.entity_id} diff --git a/qimingclaw/docs/digital-employee-frontend-integration-plan.md b/qimingclaw/docs/digital-employee-frontend-integration-plan.md index 8635dab4..5e581ccf 100644 --- a/qimingclaw/docs/digital-employee-frontend-integration-plan.md +++ b/qimingclaw/docs/digital-employee-frontend-integration-plan.md @@ -486,6 +486,7 @@ GET /api/digital-employee/sync/records - 按当前登录上下文租户隔离,默认每页 20 条,最大每页 100 条。 - 当前返回通用同步记录和原始 payload,用于管理端页面或联调工具先查看同步结果;后续再拆分为更丰富的 Plan / Task / Run / Event 查询模型。 - 管理端已增加“数字员工同步记录”菜单、权限资源和列表页,可按客户端 Key、实体类型、实体 ID、Outbox ID 查询并查看 payload;URL 查询参数会预填筛选条件,并在命中 Outbox ID 时自动打开 payload 抽屉。 +- 管理端同步记录页会从通用 payload 中提炼标题、状态、Plan/Task/Run 关联、发生时间等业务摘要,列表和详情抽屉不再只能依赖原始 JSON 排障。 下一步: