吸收数字员工跨设备策略冲突视图
This commit is contained in:
@@ -6,6 +6,7 @@ import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminActionRe
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminActionResultDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeBusinessViewPageDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeePlanBusinessViewDetailDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeePolicySnapshotPageDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncRecordPageDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncRequestDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncResponseDto;
|
||||
@@ -62,6 +63,17 @@ public interface DigitalEmployeeSyncApplicationService {
|
||||
Long pageSize
|
||||
);
|
||||
|
||||
DigitalEmployeePolicySnapshotPageDto queryAdminPolicySnapshots(
|
||||
String clientKey,
|
||||
String deviceId,
|
||||
String status,
|
||||
String policyKind,
|
||||
Date updatedFrom,
|
||||
Date updatedTo,
|
||||
Long pageNo,
|
||||
Long pageSize
|
||||
);
|
||||
|
||||
DigitalEmployeeAdminActionResultDto createAdminAction(DigitalEmployeeAdminActionRequestDto request);
|
||||
|
||||
DigitalEmployeeAdminActionPageDto queryPendingAdminActions(
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.xspaceagi.agent.core.adapter.dto.digital;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class DigitalEmployeePolicySnapshotPageDto {
|
||||
private Long total;
|
||||
@JsonProperty("page_no")
|
||||
private Long pageNo;
|
||||
@JsonProperty("page_size")
|
||||
private Long pageSize;
|
||||
private List<DigitalEmployeePolicySnapshotRowDto> records;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.xspaceagi.agent.core.adapter.dto.digital;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class DigitalEmployeePolicySnapshotRowDto {
|
||||
@JsonProperty("policy_kind")
|
||||
private String policyKind;
|
||||
@JsonProperty("policy_key")
|
||||
private String policyKey;
|
||||
private String status;
|
||||
private String summary;
|
||||
@JsonProperty("client_count")
|
||||
private Integer clientCount;
|
||||
@JsonProperty("device_count")
|
||||
private Integer deviceCount;
|
||||
@JsonProperty("conflict_keys")
|
||||
private List<String> conflictKeys;
|
||||
@JsonProperty("latest_synced_at")
|
||||
private Date latestSyncedAt;
|
||||
@JsonProperty("recommended_resolution")
|
||||
private String recommendedResolution;
|
||||
@JsonProperty("sync_record_url")
|
||||
private String syncRecordUrl;
|
||||
@JsonProperty("client_key_masked")
|
||||
private String clientKeyMasked;
|
||||
@JsonProperty("device_id")
|
||||
private String deviceId;
|
||||
@JsonProperty("business_view")
|
||||
private Map<String, Object> businessView;
|
||||
}
|
||||
@@ -16,6 +16,8 @@ import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminWorkbenc
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeBusinessViewPageDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeBusinessViewRowDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeePlanBusinessViewDetailDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeePolicySnapshotPageDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeePolicySnapshotRowDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncAckDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncItemDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncRecordDto;
|
||||
@@ -343,6 +345,120 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
|
||||
return adminWorkbenchPage(rows, currentPage, currentPageSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DigitalEmployeePolicySnapshotPageDto queryAdminPolicySnapshots(
|
||||
String clientKey,
|
||||
String deviceId,
|
||||
String status,
|
||||
String policyKind,
|
||||
Date updatedFrom,
|
||||
Date updatedTo,
|
||||
Long pageNo,
|
||||
Long pageSize
|
||||
) {
|
||||
long currentPage = pageNo == null || pageNo < 1 ? DEFAULT_PAGE_NO : pageNo;
|
||||
long currentPageSize = pageSize == null || pageSize < 1
|
||||
? DEFAULT_PAGE_SIZE
|
||||
: Math.min(pageSize, MAX_PAGE_SIZE);
|
||||
Long tenantId = currentTenantId();
|
||||
if (tenantId == null) {
|
||||
return policySnapshotPage(List.of(), currentPage, currentPageSize);
|
||||
}
|
||||
IPage<DigitalEmployeeSyncRecord> page = digitalEmployeeSyncRecordRepository.queryBusinessRecords(
|
||||
tenantId,
|
||||
clientKey,
|
||||
deviceId,
|
||||
"event",
|
||||
null,
|
||||
updatedFrom,
|
||||
updatedTo,
|
||||
null,
|
||||
DEFAULT_PAGE_NO,
|
||||
MAX_PAGE_SIZE
|
||||
);
|
||||
Map<String, List<DigitalEmployeeSyncRecord>> grouped = new LinkedHashMap<>();
|
||||
for (DigitalEmployeeSyncRecord record : page.getRecords()) {
|
||||
Map<String, Object> view = readJsonObject(record.getBusinessView());
|
||||
String kind = firstString(view.get("policy_kind"));
|
||||
if (StringUtils.isBlank(kind)) continue;
|
||||
if (StringUtils.isNotBlank(policyKind) && !StringUtils.equals(kind, policyKind)) continue;
|
||||
grouped.computeIfAbsent(kind, ignored -> new ArrayList<>()).add(record);
|
||||
}
|
||||
List<DigitalEmployeePolicySnapshotRowDto> rows = grouped.entrySet().stream()
|
||||
.map(entry -> toPolicySnapshotRow(entry.getKey(), entry.getValue()))
|
||||
.filter(row -> matchesText(status, row.getStatus()))
|
||||
.toList();
|
||||
return policySnapshotPage(rows, currentPage, currentPageSize);
|
||||
}
|
||||
|
||||
private DigitalEmployeePolicySnapshotPageDto policySnapshotPage(List<DigitalEmployeePolicySnapshotRowDto> rows, long pageNo, long pageSize) {
|
||||
int fromIndex = (int) Math.min(Math.max(pageNo - 1, 0) * pageSize, rows.size());
|
||||
int toIndex = (int) Math.min(fromIndex + pageSize, rows.size());
|
||||
return DigitalEmployeePolicySnapshotPageDto.builder()
|
||||
.total((long) rows.size())
|
||||
.pageNo(pageNo)
|
||||
.pageSize(pageSize)
|
||||
.records(rows.subList(fromIndex, toIndex))
|
||||
.build();
|
||||
}
|
||||
|
||||
private DigitalEmployeePolicySnapshotRowDto toPolicySnapshotRow(String policyKind, List<DigitalEmployeeSyncRecord> records) {
|
||||
List<Map<String, Object>> views = records.stream().map(record -> readJsonObject(record.getBusinessView())).toList();
|
||||
List<String> signatures = views.stream().map(this::policySignature).distinct().toList();
|
||||
DigitalEmployeeSyncRecord latest = records.stream()
|
||||
.max(Comparator.comparing(DigitalEmployeeSyncRecord::getSyncedAt, Comparator.nullsLast(Comparator.naturalOrder())))
|
||||
.orElse(records.get(0));
|
||||
Map<String, Object> latestView = readJsonObject(latest.getBusinessView());
|
||||
String status = signatures.size() > 1 ? "conflict" : "consistent";
|
||||
return DigitalEmployeePolicySnapshotRowDto.builder()
|
||||
.policyKind(policyKind)
|
||||
.policyKey(firstString(latestView.get("policy_key"), policyKind))
|
||||
.status(status)
|
||||
.summary(firstString(latestView.get("summary"), latestView.get("title"), policyKind))
|
||||
.clientCount((int) records.stream().map(DigitalEmployeeSyncRecord::getClientKey).filter(StringUtils::isNotBlank).distinct().count())
|
||||
.deviceCount((int) records.stream().map(DigitalEmployeeSyncRecord::getDeviceId).filter(StringUtils::isNotBlank).distinct().count())
|
||||
.conflictKeys("conflict".equals(status) ? policyConflictKeys(views) : List.of())
|
||||
.latestSyncedAt(latest.getSyncedAt())
|
||||
.recommendedResolution("conflict".equals(status) ? "review_latest_policy_snapshot" : "keep_current")
|
||||
.syncRecordUrl(syncRecordUrl(latest.getOutboxId()))
|
||||
.clientKeyMasked(maskClientKey(latest.getClientKey()))
|
||||
.deviceId(latest.getDeviceId())
|
||||
.businessView(latestView)
|
||||
.build();
|
||||
}
|
||||
|
||||
private String policySignature(Map<String, Object> view) {
|
||||
Map<String, Object> signature = new LinkedHashMap<>();
|
||||
for (String key : policyConflictCandidateKeys()) {
|
||||
signature.put(key, view.get(key));
|
||||
}
|
||||
return toJson(signature);
|
||||
}
|
||||
|
||||
private List<String> policyConflictKeys(List<Map<String, Object>> views) {
|
||||
List<String> keys = new ArrayList<>();
|
||||
for (String key : policyConflictCandidateKeys()) {
|
||||
if (views.stream().map(view -> Objects.toString(view.get(key), "")).distinct().count() > 1) {
|
||||
keys.add(key);
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
private List<String> policyConflictCandidateKeys() {
|
||||
return List.of(
|
||||
"enabled",
|
||||
"policy_version",
|
||||
"policy_source",
|
||||
"auto_create_governance_commands",
|
||||
"max_per_sweep",
|
||||
"catch_up_on_startup",
|
||||
"approval_required_risk_levels",
|
||||
"approval_required_actions",
|
||||
"auto_reject_actions"
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
@DSTransactional
|
||||
public DigitalEmployeeAdminActionResultDto createAdminAction(DigitalEmployeeAdminActionRequestDto request) {
|
||||
|
||||
@@ -12,6 +12,8 @@ import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminActionPa
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminActionRequestDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminActionResultDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeePlanBusinessViewDetailDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeePolicySnapshotPageDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeePolicySnapshotRowDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncItemDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncRecordPageDto;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncRequestDto;
|
||||
@@ -334,6 +336,46 @@ class DigitalEmployeeSyncApplicationServiceImplTest {
|
||||
assertThat(action.getReasonCodes()).contains("admin_action_applied");
|
||||
}
|
||||
|
||||
@Test
|
||||
void queryAdminPolicySnapshotsDetectsCrossDeviceConflict() {
|
||||
when(repository.queryBusinessRecords(eq(101L), eq(null), eq(null), eq("event"), eq(null), eq(null), eq(null), eq(null), eq(1L), eq(100L)))
|
||||
.thenReturn(page(List.of(
|
||||
record("outbox-policy-1", "event", "event-policy-1", 101L, new Date(10_000),
|
||||
Map.of(
|
||||
"entity_type", "event",
|
||||
"category", "policy_snapshot",
|
||||
"kind", "digital_workday_pull_route_decision_policy",
|
||||
"policy_kind", "route_decision",
|
||||
"policy_key", "route_decision",
|
||||
"enabled", true,
|
||||
"auto_create_governance_commands", true,
|
||||
"title", "入口路由策略"
|
||||
),
|
||||
Map.of("kind", "digital_workday_pull_route_decision_policy")),
|
||||
record("outbox-policy-2", "event", "event-policy-2", 101L, new Date(11_000),
|
||||
Map.of(
|
||||
"entity_type", "event",
|
||||
"category", "policy_snapshot",
|
||||
"kind", "digital_workday_pull_route_decision_policy",
|
||||
"policy_kind", "route_decision",
|
||||
"policy_key", "route_decision",
|
||||
"enabled", true,
|
||||
"auto_create_governance_commands", false,
|
||||
"title", "入口路由策略"
|
||||
),
|
||||
Map.of("kind", "digital_workday_pull_route_decision_policy"))
|
||||
)));
|
||||
|
||||
DigitalEmployeePolicySnapshotPageDto result = service.queryAdminPolicySnapshots(null, null, "conflict", "route_decision", null, null, 1L, 20L);
|
||||
|
||||
assertThat(result.getTotal()).isEqualTo(1);
|
||||
DigitalEmployeePolicySnapshotRowDto row = result.getRecords().get(0);
|
||||
assertThat(row.getPolicyKind()).isEqualTo("route_decision");
|
||||
assertThat(row.getStatus()).isEqualTo("conflict");
|
||||
assertThat(row.getConflictKeys()).contains("auto_create_governance_commands");
|
||||
assertThat(row.getRecommendedResolution()).isEqualTo("review_latest_policy_snapshot");
|
||||
}
|
||||
|
||||
private IPage<DigitalEmployeeSyncRecord> page(List<DigitalEmployeeSyncRecord> records) {
|
||||
Page<DigitalEmployeeSyncRecord> page = new Page<>(1, 20, records.size());
|
||||
page.setRecords(records);
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.xspaceagi.agent.web.ui.controller;
|
||||
|
||||
import com.xspaceagi.agent.core.adapter.application.DigitalEmployeeSyncApplicationService;
|
||||
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeePolicySnapshotPageDto;
|
||||
import com.xspaceagi.system.spec.annotation.RequireResource;
|
||||
import com.xspaceagi.system.spec.dto.ReqResult;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Date;
|
||||
|
||||
import static com.xspaceagi.system.spec.enums.ResourceEnum.CONTENT_DIGITAL_EMPLOYEE_OPS_QUERY_LIST;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/digital-employee/admin/policies")
|
||||
public class DigitalEmployeeAdminPolicyController {
|
||||
|
||||
@Resource
|
||||
private DigitalEmployeeSyncApplicationService digitalEmployeeSyncApplicationService;
|
||||
|
||||
@RequireResource(CONTENT_DIGITAL_EMPLOYEE_OPS_QUERY_LIST)
|
||||
@RequestMapping(path = "/snapshots", method = RequestMethod.GET)
|
||||
public ReqResult<DigitalEmployeePolicySnapshotPageDto> querySnapshots(
|
||||
@RequestParam(name = "client_key", required = false) String clientKey,
|
||||
@RequestParam(name = "device_id", required = false) String deviceId,
|
||||
@RequestParam(name = "status", required = false) String status,
|
||||
@RequestParam(name = "policy_kind", required = false) String policyKind,
|
||||
@RequestParam(name = "updated_from", required = false) String updatedFrom,
|
||||
@RequestParam(name = "updated_to", required = false) String updatedTo,
|
||||
@RequestParam(name = "page_no", required = false) Long pageNo,
|
||||
@RequestParam(name = "page_size", required = false) Long pageSize
|
||||
) {
|
||||
return ReqResult.success(digitalEmployeeSyncApplicationService.queryAdminPolicySnapshots(
|
||||
clientKey,
|
||||
deviceId,
|
||||
status,
|
||||
policyKind,
|
||||
parseDate(updatedFrom),
|
||||
parseDate(updatedTo),
|
||||
pageNo,
|
||||
pageSize
|
||||
));
|
||||
}
|
||||
|
||||
private Date parseDate(String value) {
|
||||
if (StringUtils.isBlank(value)) return null;
|
||||
String trimmed = value.trim();
|
||||
try {
|
||||
return Date.from(Instant.parse(trimmed));
|
||||
} catch (Exception ignored) {
|
||||
try {
|
||||
LocalDateTime localDateTime = LocalDateTime.parse(trimmed, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
|
||||
return Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
|
||||
} catch (Exception ignoredAgain) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import WorkspaceLayout from '@/components/WorkspaceLayout';
|
||||
import { SUCCESS_CODE } from '@/constants/codes.constants';
|
||||
import {
|
||||
apiDigitalEmployeeAdminActionSubmit,
|
||||
apiDigitalEmployeeAdminPolicySnapshots,
|
||||
apiDigitalEmployeeAdminWorkbenchList,
|
||||
} from '@/services/systemManage';
|
||||
import type {
|
||||
@@ -29,6 +30,7 @@ const WORKBENCH_TABS: Array<{
|
||||
{ key: 'audits', label: '审计诊断' },
|
||||
{ key: 'daily_reports', label: '日报运营' },
|
||||
{ key: 'route_governance', label: '路由治理' },
|
||||
{ key: 'policy_conflicts', label: '策略冲突' },
|
||||
];
|
||||
|
||||
const ENTITY_TYPE_OPTIONS = {
|
||||
@@ -38,6 +40,13 @@ const ENTITY_TYPE_OPTIONS = {
|
||||
event: { text: 'Event' },
|
||||
artifact: { text: 'Artifact' },
|
||||
approval: { text: 'Approval' },
|
||||
policy_snapshot: { text: 'Policy Snapshot' },
|
||||
};
|
||||
|
||||
const POLICY_KIND_OPTIONS = {
|
||||
dispatch: { text: '派发策略' },
|
||||
risk_approval: { text: '风险审批策略' },
|
||||
route_decision: { text: '入口路由策略' },
|
||||
};
|
||||
|
||||
const STATUS_TONE: Record<string, string> = {
|
||||
@@ -80,6 +89,7 @@ function renderSummary(record: DigitalEmployeeAdminWorkbenchRow) {
|
||||
record.approval_id && `Approval ${record.approval_id}`,
|
||||
record.artifact_id && `Artifact ${record.artifact_id}`,
|
||||
record.route_decision_id && `Route ${record.route_decision_id}`,
|
||||
record.policy_kind && `Policy ${record.policy_kind}`,
|
||||
].filter((item): item is string => Boolean(item));
|
||||
|
||||
return (
|
||||
@@ -129,6 +139,12 @@ function actionTarget(record: DigitalEmployeeAdminWorkbenchRow, view: DigitalEmp
|
||||
if (view === 'audits') {
|
||||
return { entityType: 'audit', entityId: record.entity_id };
|
||||
}
|
||||
if (view === 'policy_conflicts') {
|
||||
return {
|
||||
entityType: 'policy_snapshot',
|
||||
entityId: record.policy_key || record.policy_kind,
|
||||
};
|
||||
}
|
||||
return { entityType: record.entity_type, entityId: record.entity_id };
|
||||
}
|
||||
|
||||
@@ -286,6 +302,31 @@ const DigitalEmployeeOps: React.FC = () => {
|
||||
hideInTable: true,
|
||||
fieldProps: { placeholder: '输入 category', allowClear: true },
|
||||
},
|
||||
{
|
||||
title: '策略类型',
|
||||
dataIndex: 'policy_kind',
|
||||
width: 140,
|
||||
valueType: 'select',
|
||||
valueEnum: POLICY_KIND_OPTIONS,
|
||||
fieldProps: { placeholder: '选择策略类型', allowClear: true },
|
||||
render: (_, record) => record.policy_kind ? <Tag color="purple">{record.policy_kind}</Tag> : '--',
|
||||
},
|
||||
{
|
||||
title: '冲突字段',
|
||||
dataIndex: 'conflict_keys',
|
||||
width: 220,
|
||||
hideInSearch: true,
|
||||
render: (_, record) => (
|
||||
<Space size={[4, 4]} wrap>
|
||||
{(record.conflict_keys || []).slice(0, 3).map((item) => (
|
||||
<Tag key={item} color="orange" style={{ marginInlineEnd: 0 }}>
|
||||
{item}
|
||||
</Tag>
|
||||
))}
|
||||
{(!record.conflict_keys || record.conflict_keys.length === 0) && '--'}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '等级',
|
||||
dataIndex: 'level',
|
||||
@@ -354,6 +395,52 @@ const DigitalEmployeeOps: React.FC = () => {
|
||||
|
||||
const request = async (params: Record<string, any>) => {
|
||||
const updatedRange = params.updated_range || [];
|
||||
if (activeView === 'policy_conflicts') {
|
||||
const response = await apiDigitalEmployeeAdminPolicySnapshots({
|
||||
pageNo: params.current || 1,
|
||||
pageSize: params.pageSize || 15,
|
||||
clientKey: params.client_key,
|
||||
deviceId: params.device_id,
|
||||
status: params.status,
|
||||
policyKind: params.policy_kind || params.kind,
|
||||
updatedFrom: updatedRange[0],
|
||||
updatedTo: updatedRange[1],
|
||||
});
|
||||
|
||||
if (response.code !== SUCCESS_CODE) {
|
||||
message.error(response.message || '查询数字员工策略冲突失败');
|
||||
}
|
||||
|
||||
return {
|
||||
data: (response.data?.records || []).map((record, index) => ({
|
||||
view: 'policy_conflicts',
|
||||
sync_record_id: index + 1,
|
||||
sync_record_url: record.sync_record_url,
|
||||
entity_type: 'policy_snapshot',
|
||||
entity_id: record.policy_key || record.policy_kind || `policy-${index}`,
|
||||
title: record.summary || record.policy_kind,
|
||||
status: record.status,
|
||||
kind: record.policy_kind,
|
||||
category: 'policy_snapshot',
|
||||
summary: record.conflict_keys?.length
|
||||
? `冲突字段:${record.conflict_keys.join(', ')}`
|
||||
: record.summary,
|
||||
policy_kind: record.policy_kind,
|
||||
policy_key: record.policy_key,
|
||||
conflict_keys: record.conflict_keys,
|
||||
recommended_resolution: record.recommended_resolution,
|
||||
latest_synced_at: record.latest_synced_at,
|
||||
synced_at: record.latest_synced_at,
|
||||
client_key_masked: record.client_key_masked,
|
||||
device_id: record.device_id,
|
||||
business_view: record.business_view,
|
||||
payload: record.business_view,
|
||||
})),
|
||||
total: response.data?.total || 0,
|
||||
success: response.code === SUCCESS_CODE,
|
||||
};
|
||||
}
|
||||
|
||||
const response = await apiDigitalEmployeeAdminWorkbenchList({
|
||||
view: activeView,
|
||||
pageNo: params.current || 1,
|
||||
@@ -438,10 +525,25 @@ const DigitalEmployeeOps: React.FC = () => {
|
||||
detailRecord.report_id && `Report ${detailRecord.report_id}`,
|
||||
detailRecord.route_decision_id &&
|
||||
`Route ${detailRecord.route_decision_id}`,
|
||||
detailRecord.policy_kind && `Policy ${detailRecord.policy_kind}`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' / ') || '--'}
|
||||
</Typography.Paragraph>
|
||||
{activeView === 'policy_conflicts' && (
|
||||
<Typography.Paragraph>
|
||||
<Typography.Text strong>策略冲突:</Typography.Text>
|
||||
{detailRecord.conflict_keys?.length
|
||||
? detailRecord.conflict_keys.join(' / ')
|
||||
: '暂无冲突字段'}
|
||||
{detailRecord.recommended_resolution && (
|
||||
<Typography.Text type="secondary">
|
||||
{' '}
|
||||
· 建议:{detailRecord.recommended_resolution}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</Typography.Paragraph>
|
||||
)}
|
||||
{allowedActions(detailRecord, activeView).length > 0 && (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Typography.Title level={5}>低风险动作</Typography.Title>
|
||||
|
||||
@@ -12,6 +12,8 @@ import type {
|
||||
ConversationStatsResult,
|
||||
DigitalEmployeeAdminWorkbenchListParams,
|
||||
DigitalEmployeeAdminWorkbenchPage,
|
||||
DigitalEmployeePolicySnapshotListParams,
|
||||
DigitalEmployeePolicySnapshotPage,
|
||||
DigitalEmployeeSyncRecordListParams,
|
||||
DigitalEmployeeSyncRecordPage,
|
||||
ModelConfigDto,
|
||||
@@ -312,6 +314,25 @@ export async function apiDigitalEmployeeAdminWorkbenchList(
|
||||
});
|
||||
}
|
||||
|
||||
// 查询数字员工跨设备策略快照冲突
|
||||
export async function apiDigitalEmployeeAdminPolicySnapshots(
|
||||
data: DigitalEmployeePolicySnapshotListParams,
|
||||
): Promise<RequestResponse<DigitalEmployeePolicySnapshotPage>> {
|
||||
return request('/api/digital-employee/admin/policies/snapshots', {
|
||||
method: 'GET',
|
||||
params: {
|
||||
client_key: data.clientKey,
|
||||
device_id: data.deviceId,
|
||||
status: data.status,
|
||||
policy_kind: data.policyKind,
|
||||
updated_from: data.updatedFrom,
|
||||
updated_to: data.updatedTo,
|
||||
page_no: data.pageNo,
|
||||
page_size: data.pageSize,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 记录数字员工管理端低风险动作意图
|
||||
export async function apiDigitalEmployeeAdminActionSubmit(
|
||||
data: DigitalEmployeeAdminActionSubmitParams,
|
||||
|
||||
@@ -563,7 +563,8 @@ export type DigitalEmployeeAdminWorkbenchView =
|
||||
| 'notifications'
|
||||
| 'audits'
|
||||
| 'daily_reports'
|
||||
| 'route_governance';
|
||||
| 'route_governance'
|
||||
| 'policy_conflicts';
|
||||
|
||||
export interface DigitalEmployeeAdminWorkbenchRow {
|
||||
view: DigitalEmployeeAdminWorkbenchView | string;
|
||||
@@ -587,8 +588,13 @@ export interface DigitalEmployeeAdminWorkbenchRow {
|
||||
notification_id?: string;
|
||||
report_id?: string;
|
||||
route_decision_id?: string;
|
||||
policy_kind?: string;
|
||||
policy_key?: string;
|
||||
conflict_keys?: string[];
|
||||
recommended_resolution?: string;
|
||||
occurred_at?: string;
|
||||
synced_at?: string;
|
||||
latest_synced_at?: string;
|
||||
client_key_masked?: string;
|
||||
device_id?: string;
|
||||
business_view?: Record<string, unknown>;
|
||||
@@ -618,6 +624,39 @@ export interface DigitalEmployeeAdminWorkbenchPage {
|
||||
records: DigitalEmployeeAdminWorkbenchRow[];
|
||||
}
|
||||
|
||||
export interface DigitalEmployeePolicySnapshotRow {
|
||||
policy_kind?: string;
|
||||
policy_key?: string;
|
||||
status?: string;
|
||||
summary?: string;
|
||||
client_count?: number;
|
||||
device_count?: number;
|
||||
conflict_keys?: string[];
|
||||
latest_synced_at?: string;
|
||||
recommended_resolution?: string;
|
||||
sync_record_url?: string;
|
||||
client_key_masked?: string;
|
||||
device_id?: string;
|
||||
business_view?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface DigitalEmployeePolicySnapshotListParams
|
||||
extends SystemPaginationParams {
|
||||
clientKey?: string;
|
||||
deviceId?: string;
|
||||
status?: string;
|
||||
policyKind?: string;
|
||||
updatedFrom?: string;
|
||||
updatedTo?: string;
|
||||
}
|
||||
|
||||
export interface DigitalEmployeePolicySnapshotPage {
|
||||
total: number;
|
||||
page_no: number;
|
||||
page_size: number;
|
||||
records: DigitalEmployeePolicySnapshotRow[];
|
||||
}
|
||||
|
||||
export interface DigitalEmployeeAdminActionSubmitParams {
|
||||
syncRecordId?: number;
|
||||
clientKey?: string;
|
||||
|
||||
@@ -1181,6 +1181,66 @@ function checkDigitalAdminActionLoop() {
|
||||
console.log("[DigitalEmployeeCheck] admin action pull/apply loop hooks OK");
|
||||
}
|
||||
|
||||
function checkDigitalAdminPolicySnapshots() {
|
||||
console.log("\n[DigitalEmployeeCheck] Verify admin policy snapshot conflict view hooks");
|
||||
const syncServicePath = path.join(packageRoot, "src", "main", "services", "digitalEmployee", "syncService.ts");
|
||||
const syncTestPath = path.join(packageRoot, "src", "main", "services", "digitalEmployee", "syncService.test.ts");
|
||||
const docsPath = path.resolve(packageRoot, "..", "..", "docs", "digital-employee-frontend-integration-plan.md");
|
||||
const qimingRoot = path.resolve(packageRoot, "..", "..", "..", "qiming");
|
||||
const backendRoot = path.resolve(packageRoot, "..", "..", "..", "qiming-backend");
|
||||
const qimingOpsPath = path.join(qimingRoot, "src", "pages", "SystemManagement", "Content", "DigitalEmployeeOps", "index.tsx");
|
||||
const qimingServicePath = path.join(qimingRoot, "src", "services", "systemManage.ts");
|
||||
const qimingTypesPath = path.join(qimingRoot, "src", "types", "interfaces", "systemManage.ts");
|
||||
const backendControllerPath = path.join(
|
||||
backendRoot,
|
||||
"app-platform-modules",
|
||||
"app-platform-agent",
|
||||
"app-platform-agent-core-ui",
|
||||
"src",
|
||||
"main",
|
||||
"java",
|
||||
"com",
|
||||
"xspaceagi",
|
||||
"agent",
|
||||
"web",
|
||||
"ui",
|
||||
"controller",
|
||||
"DigitalEmployeeAdminPolicyController.java",
|
||||
);
|
||||
const syncService = fs.readFileSync(syncServicePath, "utf8");
|
||||
const syncTest = fs.readFileSync(syncTestPath, "utf8");
|
||||
const docs = fs.readFileSync(docsPath, "utf8");
|
||||
const qimingOps = fs.readFileSync(qimingOpsPath, "utf8");
|
||||
const qimingService = fs.readFileSync(qimingServicePath, "utf8");
|
||||
const qimingTypes = fs.readFileSync(qimingTypesPath, "utf8");
|
||||
const backendController = fs.readFileSync(backendControllerPath, "utf8");
|
||||
const requiredSnippets = [
|
||||
[syncService, "policySnapshotBusinessView", "client policy snapshot business view"],
|
||||
[syncService, "policy_snapshot", "client policy snapshot category"],
|
||||
[syncService, "policy_kind", "client policy kind field"],
|
||||
[syncService, "auto_create_governance_commands", "route policy conflict field"],
|
||||
[syncService, "max_per_sweep", "dispatch policy conflict field"],
|
||||
[syncService, "catch_up_on_startup", "scheduler policy conflict field"],
|
||||
[syncTest, "event-route-policy-pull", "route policy snapshot sync test"],
|
||||
[syncTest, "event-risk-policy-pull", "risk policy snapshot sync test"],
|
||||
[backendController, "/snapshots", "backend policy snapshot endpoint"],
|
||||
[qimingService, "apiDigitalEmployeeAdminPolicySnapshots", "qiming policy snapshot service"],
|
||||
[qimingService, "/api/digital-employee/admin/policies/snapshots", "qiming policy snapshot endpoint"],
|
||||
[qimingTypes, "DigitalEmployeePolicySnapshotRow", "qiming policy snapshot row type"],
|
||||
[qimingTypes, "policy_conflicts", "qiming policy conflicts view type"],
|
||||
[qimingOps, "策略冲突", "qiming policy conflict tab text"],
|
||||
[qimingOps, "conflict_keys", "qiming conflict key display"],
|
||||
[docs, "跨设备策略冲突视图", "docs policy conflict absorption"],
|
||||
];
|
||||
const missing = requiredSnippets
|
||||
.filter(([content, snippet]) => !content.includes(snippet))
|
||||
.map(([, , label]) => label);
|
||||
if (missing.length > 0) {
|
||||
throw new Error(`Missing admin policy snapshot hooks: ${missing.join(", ")}`);
|
||||
}
|
||||
console.log("[DigitalEmployeeCheck] admin policy snapshot conflict view hooks OK");
|
||||
}
|
||||
|
||||
function checkLocalDatabaseSchema() {
|
||||
console.log("\n[DigitalEmployeeCheck] Verify local SQLite digital schema");
|
||||
const dbPath = path.join(os.homedir(), ".qimingclaw", "qimingclaw.db");
|
||||
@@ -1240,6 +1300,7 @@ function main() {
|
||||
checkDigitalOpsNotificationReportWorkbenches();
|
||||
checkDigitalPolicySchedulerWorkbenches();
|
||||
checkDigitalAdminActionLoop();
|
||||
checkDigitalAdminPolicySnapshots();
|
||||
checkLocalDatabaseSchema();
|
||||
console.log("\n[DigitalEmployeeCheck] All checks passed");
|
||||
} catch (error) {
|
||||
|
||||
@@ -1655,6 +1655,41 @@ describe("digital employee sync service", () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
insertEventEntity("event-risk-policy-pull", "digital_workday_pull_risk_approval_policy", {
|
||||
metadata: {
|
||||
action: "pull_risk_approval_policy",
|
||||
source: "management-api",
|
||||
ok: true,
|
||||
risk_approval_policy: {
|
||||
enabled: true,
|
||||
source: "remote",
|
||||
version: "risk-policy-v2",
|
||||
approval_required_risk_levels: ["high", "critical"],
|
||||
approval_required_actions: ["managed_service.restart.*"],
|
||||
auto_reject_actions: ["service.stop.*"],
|
||||
remote_updated_at: "2026-06-07T08:10:00.000Z",
|
||||
last_pulled_at: "2026-06-07T08:11:00.000Z",
|
||||
},
|
||||
},
|
||||
});
|
||||
insertEventEntity("event-route-policy-pull", "digital_workday_pull_route_decision_policy", {
|
||||
metadata: {
|
||||
action: "pull_route_decision_policy",
|
||||
source: "management-api",
|
||||
ok: true,
|
||||
route_decision_policy: {
|
||||
enabled: true,
|
||||
source: "remote",
|
||||
version: "route-policy-v3",
|
||||
auto_create_governance_commands: false,
|
||||
blocked_intents: ["delete"],
|
||||
approval_required_intents: ["restart_service"],
|
||||
preferred_route_kinds: ["PlanStepDispatch"],
|
||||
remote_updated_at: "2026-06-07T08:12:00.000Z",
|
||||
last_pulled_at: "2026-06-07T08:13:00.000Z",
|
||||
},
|
||||
},
|
||||
});
|
||||
insertEventEntity("event-risk-approval", "risk_approval_required", {
|
||||
action_kind: "managed_service.restart.crm-sync",
|
||||
action_label: "重启 CRM 同步服务",
|
||||
@@ -1701,6 +1736,8 @@ describe("digital employee sync service", () => {
|
||||
{ entity_type: "event", entity_id: "event-approval-subscriptions" },
|
||||
{ entity_type: "event", entity_id: "event-plan-step-policy" },
|
||||
{ entity_type: "event", entity_id: "event-plan-step-policy-pull" },
|
||||
{ entity_type: "event", entity_id: "event-risk-policy-pull" },
|
||||
{ entity_type: "event", entity_id: "event-route-policy-pull" },
|
||||
{ entity_type: "event", entity_id: "event-risk-approval" },
|
||||
],
|
||||
}),
|
||||
@@ -1990,27 +2027,58 @@ describe("digital employee sync service", () => {
|
||||
expect(businessView("event-approval-subscriptions")).not.toHaveProperty("state");
|
||||
expect(businessView("event-approval-subscriptions")).not.toHaveProperty("approvalSubscriptionPreferences");
|
||||
expect(businessView("event-plan-step-policy")).toMatchObject({
|
||||
category: "dispatch",
|
||||
category: "policy_snapshot",
|
||||
policy_kind: "dispatch",
|
||||
policy_key: "dispatch",
|
||||
action: "update_plan_step_dispatch_policy",
|
||||
policy_enabled: false,
|
||||
enabled: false,
|
||||
max_per_sweep: 1,
|
||||
auto_dispatch_ready_steps: false,
|
||||
updated_at: "2026-06-07T08:07:00.000Z",
|
||||
policy_version: "2026-06-07T08:07:00.000Z",
|
||||
});
|
||||
expect(businessView("event-plan-step-policy")).not.toHaveProperty("state");
|
||||
expect(businessView("event-plan-step-policy")).not.toHaveProperty("planStepDispatchPolicy");
|
||||
expect(businessView("event-plan-step-policy-pull")).toMatchObject({
|
||||
category: "dispatch",
|
||||
category: "policy_snapshot",
|
||||
policy_kind: "dispatch",
|
||||
policy_key: "dispatch",
|
||||
action: "pull_plan_step_dispatch_policy",
|
||||
source: "management-api",
|
||||
pull_ok: false,
|
||||
pull_error: "policy service unavailable",
|
||||
policy_enabled: true,
|
||||
enabled: true,
|
||||
max_per_sweep: 2,
|
||||
auto_dispatch_ready_steps: true,
|
||||
remote_updated_at: "2026-06-07T08:08:00.000Z",
|
||||
last_pulled_at: "2026-06-07T08:09:00.000Z",
|
||||
});
|
||||
expect(businessView("event-risk-policy-pull")).toMatchObject({
|
||||
category: "policy_snapshot",
|
||||
policy_kind: "risk_approval",
|
||||
policy_key: "risk_approval",
|
||||
policy_version: "risk-policy-v2",
|
||||
policy_source: "management-api",
|
||||
enabled: true,
|
||||
approval_required_risk_levels: ["high", "critical"],
|
||||
approval_required_actions: ["managed_service.restart.*"],
|
||||
auto_reject_actions: ["service.stop.*"],
|
||||
remote_updated_at: "2026-06-07T08:10:00.000Z",
|
||||
last_pulled_at: "2026-06-07T08:11:00.000Z",
|
||||
});
|
||||
expect(businessView("event-route-policy-pull")).toMatchObject({
|
||||
category: "policy_snapshot",
|
||||
policy_kind: "route_decision",
|
||||
policy_key: "route_decision",
|
||||
policy_version: "route-policy-v3",
|
||||
policy_source: "management-api",
|
||||
enabled: true,
|
||||
auto_create_governance_commands: false,
|
||||
blocked_intents: ["delete"],
|
||||
approval_required_intents: ["restart_service"],
|
||||
preferred_route_kinds: ["PlanStepDispatch"],
|
||||
remote_updated_at: "2026-06-07T08:12:00.000Z",
|
||||
last_pulled_at: "2026-06-07T08:13:00.000Z",
|
||||
});
|
||||
expect(businessView("event-risk-approval")).toMatchObject({
|
||||
category: "approval",
|
||||
action_kind: "managed_service.restart.crm-sync",
|
||||
|
||||
@@ -1979,7 +1979,7 @@ function eventSpecificBusinessView(
|
||||
if (isPlanStepDependencyEvent(kind)) return planStepDependencyBusinessView(payload);
|
||||
if (kind.startsWith("plan_step_dispatch_")) return planStepDispatchBusinessView(payload);
|
||||
if (isPlanStepLeaseEvent(kind)) return planStepLeaseBusinessView(payload);
|
||||
if (isPlanStepDispatchPolicyEvent(kind)) return planStepDispatchPolicyBusinessView(payload);
|
||||
if (isPolicySnapshotEvent(kind)) return policySnapshotBusinessView(kind, payload);
|
||||
if (kind.startsWith("governance_")) return governanceCommandBusinessView(kind, payload);
|
||||
if (kind === "sandbox_audit") return sandboxAuditBusinessView(payload);
|
||||
if (kind === "token_cost") return tokenCostBusinessView(payload);
|
||||
@@ -2268,6 +2268,48 @@ function planStepDispatchPolicyBusinessView(payload: Record<string, unknown>): R
|
||||
};
|
||||
}
|
||||
|
||||
function policySnapshotBusinessView(kind: string, payload: Record<string, unknown>): Record<string, unknown> {
|
||||
const metadata = objectRecord(payload.metadata) ?? {};
|
||||
const state = objectRecord(payload.state) ?? {};
|
||||
const policyKind = kind.includes("risk_approval")
|
||||
? "risk_approval"
|
||||
: kind.includes("route_decision")
|
||||
? "route_decision"
|
||||
: "dispatch";
|
||||
const policy = policyKind === "risk_approval"
|
||||
? objectRecord(metadata.risk_approval_policy) ?? objectRecord(state.riskApprovalPolicy) ?? {}
|
||||
: policyKind === "route_decision"
|
||||
? objectRecord(metadata.route_decision_policy) ?? objectRecord(state.routeDecisionPolicy) ?? {}
|
||||
: objectRecord(metadata.plan_step_dispatch_policy) ?? objectRecord(state.planStepDispatchPolicy) ?? {};
|
||||
return {
|
||||
category: "policy_snapshot",
|
||||
policy_kind: policyKind,
|
||||
policy_key: policyKind,
|
||||
policy_version: stringField(policy.version) || stringField(policy.updated_at ?? policy.updatedAt) || null,
|
||||
policy_source: stringField(metadata.source ?? policy.source) || null,
|
||||
source: stringField(metadata.source ?? policy.source) || null,
|
||||
action: stringField(metadata.action ?? payload.action) || kind.replace(/^digital_workday_/, ""),
|
||||
status: metadata.ok === false ? "pull_failed" : "snapshot",
|
||||
enabled: typeof policy.enabled === "boolean" ? policy.enabled : null,
|
||||
pull_ok: typeof metadata.ok === "boolean" ? metadata.ok : null,
|
||||
pull_error: stringField(metadata.error ?? policy.last_pull_error) || null,
|
||||
pull_endpoint: stringField(metadata.endpoint) || null,
|
||||
max_per_sweep: numberField(policy.max_per_sweep ?? policy.maxPerSweep),
|
||||
auto_dispatch_ready_steps: typeof policy.auto_dispatch_ready_steps === "boolean" ? policy.auto_dispatch_ready_steps : typeof policy.autoDispatchReadySteps === "boolean" ? policy.autoDispatchReadySteps : null,
|
||||
auto_create_governance_commands: typeof policy.auto_create_governance_commands === "boolean" ? policy.auto_create_governance_commands : typeof policy.autoCreateGovernanceCommands === "boolean" ? policy.autoCreateGovernanceCommands : null,
|
||||
catch_up_on_startup: typeof policy.catch_up_on_startup === "boolean" ? policy.catch_up_on_startup : typeof policy.catchUpOnStartup === "boolean" ? policy.catchUpOnStartup : null,
|
||||
approval_required_risk_levels: stringArrayField(policy.approval_required_risk_levels ?? policy.approvalRequiredRiskLevels),
|
||||
approval_required_actions: stringArrayField(policy.approval_required_actions ?? policy.approvalRequiredActions),
|
||||
auto_reject_actions: stringArrayField(policy.auto_reject_actions ?? policy.autoRejectActions),
|
||||
blocked_intents: stringArrayField(policy.blocked_intents ?? policy.blockedIntents),
|
||||
approval_required_intents: stringArrayField(policy.approval_required_intents ?? policy.approvalRequiredIntents),
|
||||
preferred_route_kinds: stringArrayField(policy.preferred_route_kinds ?? policy.preferredRouteKinds),
|
||||
remote_updated_at: stringField(policy.remote_updated_at ?? policy.remoteUpdatedAt) || null,
|
||||
last_pulled_at: stringField(policy.last_pulled_at ?? policy.lastPulledAt) || null,
|
||||
summary: `${policyKind} policy snapshot`,
|
||||
};
|
||||
}
|
||||
|
||||
function governanceCommandBusinessView(kind: string, payload: Record<string, unknown>): Record<string, unknown> {
|
||||
const commandPayload = objectRecord(payload.payload) ?? {};
|
||||
const result = objectRecord(payload.result) ?? {};
|
||||
@@ -2383,7 +2425,7 @@ function eventBusinessCategory(kind: string): string {
|
||||
if (isPlanStepDependencyEvent(kind)) return "dispatch";
|
||||
if (kind.startsWith("plan_step_dispatch_")) return "dispatch";
|
||||
if (isPlanStepLeaseEvent(kind)) return "dispatch";
|
||||
if (isPlanStepDispatchPolicyEvent(kind)) return "dispatch";
|
||||
if (isPolicySnapshotEvent(kind)) return "policy_snapshot";
|
||||
if (isNotificationEvent(kind)) return "notification";
|
||||
if (kind.startsWith("governance_")) return "governance";
|
||||
if (kind === "sandbox_audit") return "sandbox_audit";
|
||||
@@ -2419,6 +2461,14 @@ function isPlanStepDispatchPolicyEvent(kind: string): boolean {
|
||||
|| kind === "digital_workday_pull_plan_step_dispatch_policy";
|
||||
}
|
||||
|
||||
function isPolicySnapshotEvent(kind: string): boolean {
|
||||
return isPlanStepDispatchPolicyEvent(kind)
|
||||
|| kind === "digital_workday_pull_risk_approval_policy"
|
||||
|| kind === "digital_workday_pull_route_decision_policy"
|
||||
|| kind === "digital_workday_update_risk_approval_policy"
|
||||
|| kind === "digital_workday_update_route_decision_policy";
|
||||
}
|
||||
|
||||
function isPlanStepLeaseEvent(kind: string): boolean {
|
||||
return kind === "plan_step_lease_acquired" || kind === "plan_step_lease_released" || kind === "plan_step_lease_skipped";
|
||||
}
|
||||
|
||||
@@ -609,7 +609,7 @@ GET /api/digital-employee/approvals
|
||||
- 建议:下一轮围绕管理端正式审计视图和诊断修复动作,把本地可追溯审计继续推进到可运营处置。
|
||||
|
||||
12. **管理端业务查询模型**
|
||||
- 已吸收:管理端已有通用 sync record 接收、查询、payload 摘要和深链;后端 sync record 已开始保存客户端顶层 `contract_version`、`entity_summary` 和 `business_view`,并基于现有 `digital_employee_sync_record` 拆出 `/api/digital-employee/plans`、`/plans/{plan_id}`、`/tasks`、`/runs`、`/events`、`/artifacts`、`/approvals` 管理端业务查询模型;列表支持客户端 Key、设备、实体 ID、状态、同步状态和同步时间范围过滤,业务 row 会提炼 title/status/summary、Plan/Task/Run 关联、发生/开始/结束时间、sync record 深链和脱敏客户端 Key;Plan 视图会聚合 task/run 计数与最近事件,Plan 详情会返回关联 tasks/runs/events/artifacts/approvals 摘要。embedded adapter 已拆出同名只读业务查询接口,从 qimingclaw runtime、artifact、approval 和 canonical event 投影生成统一分页视图;embedded 数字员工页已新增“业务视图”入口,支持计划/任务/运行/事件/产物/审批分段、实体/状态/同步状态/时间筛选、分页和详情 JSON 查看;客户端 outbox 的 event `business_view` 已开始按 route decision、governance command、approval action、artifact lifecycle/access、skill call audit 和 ready PlanStep dispatch 细分常用字段,本地与管理端业务视图投影已开始闭环。正式外部管理端台面已开始吸收:qiming-backend 新增 `/api/digital-employee/admin/workbench/{view}`,qiming 管理端新增“数字员工运营台”,统一展示业务视图、介入台、产物治理、通知运营、审计诊断、日报运营和路由治理,只读消费 sync record `business_view` 并提供同步记录深链。管理端低风险动作闭环已开始吸收:后端新增 `/api/digital-employee/admin/actions` 记录动作意图和 `/pending` 拉取队列,qimingclaw 只应用保留本地、标记复核、通知已读/忽略、产物保留/到期、日报交付/确认/审批请求等低风险动作并写入 `admin_action_applied` / `admin_action_rejected` / `admin_action_failed`,qiming 运营台抽屉只提示“已记录,等待客户端拉取”。
|
||||
- 已吸收:管理端已有通用 sync record 接收、查询、payload 摘要和深链;后端 sync record 已开始保存客户端顶层 `contract_version`、`entity_summary` 和 `business_view`,并基于现有 `digital_employee_sync_record` 拆出 `/api/digital-employee/plans`、`/plans/{plan_id}`、`/tasks`、`/runs`、`/events`、`/artifacts`、`/approvals` 管理端业务查询模型;列表支持客户端 Key、设备、实体 ID、状态、同步状态和同步时间范围过滤,业务 row 会提炼 title/status/summary、Plan/Task/Run 关联、发生/开始/结束时间、sync record 深链和脱敏客户端 Key;Plan 视图会聚合 task/run 计数与最近事件,Plan 详情会返回关联 tasks/runs/events/artifacts/approvals 摘要。embedded adapter 已拆出同名只读业务查询接口,从 qimingclaw runtime、artifact、approval 和 canonical event 投影生成统一分页视图;embedded 数字员工页已新增“业务视图”入口,支持计划/任务/运行/事件/产物/审批分段、实体/状态/同步状态/时间筛选、分页和详情 JSON 查看;客户端 outbox 的 event `business_view` 已开始按 route decision、governance command、approval action、artifact lifecycle/access、skill call audit 和 ready PlanStep dispatch 细分常用字段,本地与管理端业务视图投影已开始闭环。正式外部管理端台面已开始吸收:qiming-backend 新增 `/api/digital-employee/admin/workbench/{view}`,qiming 管理端新增“数字员工运营台”,统一展示业务视图、介入台、产物治理、通知运营、审计诊断、日报运营和路由治理,只读消费 sync record `business_view` 并提供同步记录深链。管理端低风险动作闭环已开始吸收:后端新增 `/api/digital-employee/admin/actions` 记录动作意图和 `/pending` 拉取队列,qimingclaw 只应用保留本地、标记复核、通知已读/忽略、产物保留/到期、日报交付/确认/审批请求等低风险动作并写入 `admin_action_applied` / `admin_action_rejected` / `admin_action_failed`,qiming 运营台抽屉只提示“已记录,等待客户端拉取”。跨设备策略冲突视图已开始吸收:qimingclaw 将派发、风险审批和入口路由策略同步事件投影为 `policy_snapshot` business view,后端新增 `/api/digital-employee/admin/policies/snapshots` 聚合 client/device 维度差异并输出 `conflict_keys`,qiming 运营台新增“策略冲突” tab 展示 policy drift 与 sync record 深链。
|
||||
- 缺口:复杂组合检索、跨设备聚合图、远程裁决/策略编辑动作和物化业务表仍待吸收。
|
||||
- 建议:先使用 sync record 派生业务查询模型和 embedded 业务视图预览承接联调,再按容量和查询压力决定是否拆 Plan / Task / Run / Event 物理业务表。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user