吸收数字员工跨设备策略冲突视图

This commit is contained in:
baiyanyun
2026-06-12 21:48:33 +08:00
parent b3f21adecb
commit 8a5d5d6474
13 changed files with 643 additions and 9 deletions

View File

@@ -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.DigitalEmployeeAdminActionResultDto;
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeBusinessViewPageDto; 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.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.DigitalEmployeeSyncRecordPageDto;
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncRequestDto; import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncRequestDto;
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncResponseDto; import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncResponseDto;
@@ -62,6 +63,17 @@ public interface DigitalEmployeeSyncApplicationService {
Long pageSize Long pageSize
); );
DigitalEmployeePolicySnapshotPageDto queryAdminPolicySnapshots(
String clientKey,
String deviceId,
String status,
String policyKind,
Date updatedFrom,
Date updatedTo,
Long pageNo,
Long pageSize
);
DigitalEmployeeAdminActionResultDto createAdminAction(DigitalEmployeeAdminActionRequestDto request); DigitalEmployeeAdminActionResultDto createAdminAction(DigitalEmployeeAdminActionRequestDto request);
DigitalEmployeeAdminActionPageDto queryPendingAdminActions( DigitalEmployeeAdminActionPageDto queryPendingAdminActions(

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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.DigitalEmployeeBusinessViewPageDto;
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeBusinessViewRowDto; 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.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.DigitalEmployeeSyncAckDto;
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncItemDto; import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncItemDto;
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncRecordDto; import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncRecordDto;
@@ -343,6 +345,120 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
return adminWorkbenchPage(rows, currentPage, currentPageSize); 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 @Override
@DSTransactional @DSTransactional
public DigitalEmployeeAdminActionResultDto createAdminAction(DigitalEmployeeAdminActionRequestDto request) { public DigitalEmployeeAdminActionResultDto createAdminAction(DigitalEmployeeAdminActionRequestDto request) {

View File

@@ -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.DigitalEmployeeAdminActionRequestDto;
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeAdminActionResultDto; 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.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.DigitalEmployeeSyncItemDto;
import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncRecordPageDto; 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.DigitalEmployeeSyncRequestDto;
@@ -334,6 +336,46 @@ class DigitalEmployeeSyncApplicationServiceImplTest {
assertThat(action.getReasonCodes()).contains("admin_action_applied"); 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) { private IPage<DigitalEmployeeSyncRecord> page(List<DigitalEmployeeSyncRecord> records) {
Page<DigitalEmployeeSyncRecord> page = new Page<>(1, 20, records.size()); Page<DigitalEmployeeSyncRecord> page = new Page<>(1, 20, records.size());
page.setRecords(records); page.setRecords(records);

View File

@@ -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;
}
}
}
}

View File

@@ -3,6 +3,7 @@ import WorkspaceLayout from '@/components/WorkspaceLayout';
import { SUCCESS_CODE } from '@/constants/codes.constants'; import { SUCCESS_CODE } from '@/constants/codes.constants';
import { import {
apiDigitalEmployeeAdminActionSubmit, apiDigitalEmployeeAdminActionSubmit,
apiDigitalEmployeeAdminPolicySnapshots,
apiDigitalEmployeeAdminWorkbenchList, apiDigitalEmployeeAdminWorkbenchList,
} from '@/services/systemManage'; } from '@/services/systemManage';
import type { import type {
@@ -29,6 +30,7 @@ const WORKBENCH_TABS: Array<{
{ key: 'audits', label: '审计诊断' }, { key: 'audits', label: '审计诊断' },
{ key: 'daily_reports', label: '日报运营' }, { key: 'daily_reports', label: '日报运营' },
{ key: 'route_governance', label: '路由治理' }, { key: 'route_governance', label: '路由治理' },
{ key: 'policy_conflicts', label: '策略冲突' },
]; ];
const ENTITY_TYPE_OPTIONS = { const ENTITY_TYPE_OPTIONS = {
@@ -38,6 +40,13 @@ const ENTITY_TYPE_OPTIONS = {
event: { text: 'Event' }, event: { text: 'Event' },
artifact: { text: 'Artifact' }, artifact: { text: 'Artifact' },
approval: { text: 'Approval' }, 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> = { const STATUS_TONE: Record<string, string> = {
@@ -80,6 +89,7 @@ function renderSummary(record: DigitalEmployeeAdminWorkbenchRow) {
record.approval_id && `Approval ${record.approval_id}`, record.approval_id && `Approval ${record.approval_id}`,
record.artifact_id && `Artifact ${record.artifact_id}`, record.artifact_id && `Artifact ${record.artifact_id}`,
record.route_decision_id && `Route ${record.route_decision_id}`, record.route_decision_id && `Route ${record.route_decision_id}`,
record.policy_kind && `Policy ${record.policy_kind}`,
].filter((item): item is string => Boolean(item)); ].filter((item): item is string => Boolean(item));
return ( return (
@@ -129,6 +139,12 @@ function actionTarget(record: DigitalEmployeeAdminWorkbenchRow, view: DigitalEmp
if (view === 'audits') { if (view === 'audits') {
return { entityType: 'audit', entityId: record.entity_id }; 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 }; return { entityType: record.entity_type, entityId: record.entity_id };
} }
@@ -286,6 +302,31 @@ const DigitalEmployeeOps: React.FC = () => {
hideInTable: true, hideInTable: true,
fieldProps: { placeholder: '输入 category', allowClear: 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: '等级', title: '等级',
dataIndex: 'level', dataIndex: 'level',
@@ -354,6 +395,52 @@ const DigitalEmployeeOps: React.FC = () => {
const request = async (params: Record<string, any>) => { const request = async (params: Record<string, any>) => {
const updatedRange = params.updated_range || []; 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({ const response = await apiDigitalEmployeeAdminWorkbenchList({
view: activeView, view: activeView,
pageNo: params.current || 1, pageNo: params.current || 1,
@@ -438,10 +525,25 @@ const DigitalEmployeeOps: React.FC = () => {
detailRecord.report_id && `Report ${detailRecord.report_id}`, detailRecord.report_id && `Report ${detailRecord.report_id}`,
detailRecord.route_decision_id && detailRecord.route_decision_id &&
`Route ${detailRecord.route_decision_id}`, `Route ${detailRecord.route_decision_id}`,
detailRecord.policy_kind && `Policy ${detailRecord.policy_kind}`,
] ]
.filter(Boolean) .filter(Boolean)
.join(' / ') || '--'} .join(' / ') || '--'}
</Typography.Paragraph> </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 && ( {allowedActions(detailRecord, activeView).length > 0 && (
<div style={{ marginBottom: 16 }}> <div style={{ marginBottom: 16 }}>
<Typography.Title level={5}></Typography.Title> <Typography.Title level={5}></Typography.Title>

View File

@@ -12,6 +12,8 @@ import type {
ConversationStatsResult, ConversationStatsResult,
DigitalEmployeeAdminWorkbenchListParams, DigitalEmployeeAdminWorkbenchListParams,
DigitalEmployeeAdminWorkbenchPage, DigitalEmployeeAdminWorkbenchPage,
DigitalEmployeePolicySnapshotListParams,
DigitalEmployeePolicySnapshotPage,
DigitalEmployeeSyncRecordListParams, DigitalEmployeeSyncRecordListParams,
DigitalEmployeeSyncRecordPage, DigitalEmployeeSyncRecordPage,
ModelConfigDto, 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( export async function apiDigitalEmployeeAdminActionSubmit(
data: DigitalEmployeeAdminActionSubmitParams, data: DigitalEmployeeAdminActionSubmitParams,

View File

@@ -563,7 +563,8 @@ export type DigitalEmployeeAdminWorkbenchView =
| 'notifications' | 'notifications'
| 'audits' | 'audits'
| 'daily_reports' | 'daily_reports'
| 'route_governance'; | 'route_governance'
| 'policy_conflicts';
export interface DigitalEmployeeAdminWorkbenchRow { export interface DigitalEmployeeAdminWorkbenchRow {
view: DigitalEmployeeAdminWorkbenchView | string; view: DigitalEmployeeAdminWorkbenchView | string;
@@ -587,8 +588,13 @@ export interface DigitalEmployeeAdminWorkbenchRow {
notification_id?: string; notification_id?: string;
report_id?: string; report_id?: string;
route_decision_id?: string; route_decision_id?: string;
policy_kind?: string;
policy_key?: string;
conflict_keys?: string[];
recommended_resolution?: string;
occurred_at?: string; occurred_at?: string;
synced_at?: string; synced_at?: string;
latest_synced_at?: string;
client_key_masked?: string; client_key_masked?: string;
device_id?: string; device_id?: string;
business_view?: Record<string, unknown>; business_view?: Record<string, unknown>;
@@ -618,6 +624,39 @@ export interface DigitalEmployeeAdminWorkbenchPage {
records: DigitalEmployeeAdminWorkbenchRow[]; 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 { export interface DigitalEmployeeAdminActionSubmitParams {
syncRecordId?: number; syncRecordId?: number;
clientKey?: string; clientKey?: string;

View File

@@ -1181,6 +1181,66 @@ function checkDigitalAdminActionLoop() {
console.log("[DigitalEmployeeCheck] admin action pull/apply loop hooks OK"); 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() { function checkLocalDatabaseSchema() {
console.log("\n[DigitalEmployeeCheck] Verify local SQLite digital schema"); console.log("\n[DigitalEmployeeCheck] Verify local SQLite digital schema");
const dbPath = path.join(os.homedir(), ".qimingclaw", "qimingclaw.db"); const dbPath = path.join(os.homedir(), ".qimingclaw", "qimingclaw.db");
@@ -1240,6 +1300,7 @@ function main() {
checkDigitalOpsNotificationReportWorkbenches(); checkDigitalOpsNotificationReportWorkbenches();
checkDigitalPolicySchedulerWorkbenches(); checkDigitalPolicySchedulerWorkbenches();
checkDigitalAdminActionLoop(); checkDigitalAdminActionLoop();
checkDigitalAdminPolicySnapshots();
checkLocalDatabaseSchema(); checkLocalDatabaseSchema();
console.log("\n[DigitalEmployeeCheck] All checks passed"); console.log("\n[DigitalEmployeeCheck] All checks passed");
} catch (error) { } catch (error) {

View File

@@ -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", { insertEventEntity("event-risk-approval", "risk_approval_required", {
action_kind: "managed_service.restart.crm-sync", action_kind: "managed_service.restart.crm-sync",
action_label: "重启 CRM 同步服务", 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-approval-subscriptions" },
{ entity_type: "event", entity_id: "event-plan-step-policy" }, { 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-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" }, { 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("state");
expect(businessView("event-approval-subscriptions")).not.toHaveProperty("approvalSubscriptionPreferences"); expect(businessView("event-approval-subscriptions")).not.toHaveProperty("approvalSubscriptionPreferences");
expect(businessView("event-plan-step-policy")).toMatchObject({ expect(businessView("event-plan-step-policy")).toMatchObject({
category: "dispatch", category: "policy_snapshot",
policy_kind: "dispatch",
policy_key: "dispatch",
action: "update_plan_step_dispatch_policy", action: "update_plan_step_dispatch_policy",
policy_enabled: false, enabled: false,
max_per_sweep: 1, max_per_sweep: 1,
auto_dispatch_ready_steps: false, 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("state");
expect(businessView("event-plan-step-policy")).not.toHaveProperty("planStepDispatchPolicy"); expect(businessView("event-plan-step-policy")).not.toHaveProperty("planStepDispatchPolicy");
expect(businessView("event-plan-step-policy-pull")).toMatchObject({ 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", action: "pull_plan_step_dispatch_policy",
source: "management-api", source: "management-api",
pull_ok: false, pull_ok: false,
pull_error: "policy service unavailable", pull_error: "policy service unavailable",
policy_enabled: true, enabled: true,
max_per_sweep: 2, max_per_sweep: 2,
auto_dispatch_ready_steps: true, auto_dispatch_ready_steps: true,
remote_updated_at: "2026-06-07T08:08:00.000Z", remote_updated_at: "2026-06-07T08:08:00.000Z",
last_pulled_at: "2026-06-07T08:09: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({ expect(businessView("event-risk-approval")).toMatchObject({
category: "approval", category: "approval",
action_kind: "managed_service.restart.crm-sync", action_kind: "managed_service.restart.crm-sync",

View File

@@ -1979,7 +1979,7 @@ function eventSpecificBusinessView(
if (isPlanStepDependencyEvent(kind)) return planStepDependencyBusinessView(payload); if (isPlanStepDependencyEvent(kind)) return planStepDependencyBusinessView(payload);
if (kind.startsWith("plan_step_dispatch_")) return planStepDispatchBusinessView(payload); if (kind.startsWith("plan_step_dispatch_")) return planStepDispatchBusinessView(payload);
if (isPlanStepLeaseEvent(kind)) return planStepLeaseBusinessView(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.startsWith("governance_")) return governanceCommandBusinessView(kind, payload);
if (kind === "sandbox_audit") return sandboxAuditBusinessView(payload); if (kind === "sandbox_audit") return sandboxAuditBusinessView(payload);
if (kind === "token_cost") return tokenCostBusinessView(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> { function governanceCommandBusinessView(kind: string, payload: Record<string, unknown>): Record<string, unknown> {
const commandPayload = objectRecord(payload.payload) ?? {}; const commandPayload = objectRecord(payload.payload) ?? {};
const result = objectRecord(payload.result) ?? {}; const result = objectRecord(payload.result) ?? {};
@@ -2383,7 +2425,7 @@ function eventBusinessCategory(kind: string): string {
if (isPlanStepDependencyEvent(kind)) return "dispatch"; if (isPlanStepDependencyEvent(kind)) return "dispatch";
if (kind.startsWith("plan_step_dispatch_")) return "dispatch"; if (kind.startsWith("plan_step_dispatch_")) return "dispatch";
if (isPlanStepLeaseEvent(kind)) return "dispatch"; if (isPlanStepLeaseEvent(kind)) return "dispatch";
if (isPlanStepDispatchPolicyEvent(kind)) return "dispatch"; if (isPolicySnapshotEvent(kind)) return "policy_snapshot";
if (isNotificationEvent(kind)) return "notification"; if (isNotificationEvent(kind)) return "notification";
if (kind.startsWith("governance_")) return "governance"; if (kind.startsWith("governance_")) return "governance";
if (kind === "sandbox_audit") return "sandbox_audit"; if (kind === "sandbox_audit") return "sandbox_audit";
@@ -2419,6 +2461,14 @@ function isPlanStepDispatchPolicyEvent(kind: string): boolean {
|| kind === "digital_workday_pull_plan_step_dispatch_policy"; || 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 { function isPlanStepLeaseEvent(kind: string): boolean {
return kind === "plan_step_lease_acquired" || kind === "plan_step_lease_released" || kind === "plan_step_lease_skipped"; return kind === "plan_step_lease_acquired" || kind === "plan_step_lease_released" || kind === "plan_step_lease_skipped";
} }

View File

@@ -609,7 +609,7 @@ GET /api/digital-employee/approvals
- 建议:下一轮围绕管理端正式审计视图和诊断修复动作,把本地可追溯审计继续推进到可运营处置。 - 建议:下一轮围绕管理端正式审计视图和诊断修复动作,把本地可追溯审计继续推进到可运营处置。
12. **管理端业务查询模型** 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 深链和脱敏客户端 KeyPlan 视图会聚合 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 深链和脱敏客户端 KeyPlan 视图会聚合 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 物理业务表。 - 建议:先使用 sync record 派生业务查询模型和 embedded 业务视图预览承接联调,再按容量和查询压力决定是否拆 Plan / Task / Run / Event 物理业务表。