From 7ea963dc9a6e9c3087c8268102971b7668ebe86e Mon Sep 17 00:00:00 2001 From: baiyanyun Date: Sat, 13 Jun 2026 15:58:10 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=B8=E6=94=B6=E6=95=B0=E5=AD=97=E5=91=98?= =?UTF-8?q?=E5=B7=A5=E8=B7=A8=E8=AE=BE=E5=A4=87=E8=B7=AF=E7=94=B1=E5=B9=B2?= =?UTF-8?q?=E9=A2=84=E5=86=B2=E7=AA=81=E9=A2=84=E8=A7=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DigitalEmployeePolicySnapshotRowDto.java | 12 +++ ...talEmployeeSyncApplicationServiceImpl.java | 70 +++++++++++++++- ...mployeeSyncApplicationServiceImplTest.java | 80 +++++++++++++++++++ .../Content/DigitalEmployeeOps/index.tsx | 23 ++++++ qiming/src/types/interfaces/systemManage.ts | 12 +++ .../scripts/check-digital-employee.js | 63 +++++++++++++++ ...ital-employee-frontend-integration-plan.md | 4 +- 7 files changed, 258 insertions(+), 6 deletions(-) diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/digital/DigitalEmployeePolicySnapshotRowDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/digital/DigitalEmployeePolicySnapshotRowDto.java index 75712637..c2b4abcd 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/digital/DigitalEmployeePolicySnapshotRowDto.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/digital/DigitalEmployeePolicySnapshotRowDto.java @@ -27,6 +27,18 @@ public class DigitalEmployeePolicySnapshotRowDto { private Date latestSyncedAt; @JsonProperty("recommended_resolution") private String recommendedResolution; + @JsonProperty("route_decision_count") + private Integer routeDecisionCount; + @JsonProperty("blocked_count") + private Integer blockedCount; + @JsonProperty("approval_required_count") + private Integer approvalRequiredCount; + @JsonProperty("missing_context_count") + private Integer missingContextCount; + @JsonProperty("not_mapped_count") + private Integer notMappedCount; + @JsonProperty("intervention_conflict_status") + private String interventionConflictStatus; @JsonProperty("sync_record_url") private String syncRecordUrl; @JsonProperty("client_key_masked") diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/DigitalEmployeeSyncApplicationServiceImpl.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/DigitalEmployeeSyncApplicationServiceImpl.java index 6d9c2658..26937f9e 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/DigitalEmployeeSyncApplicationServiceImpl.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/DigitalEmployeeSyncApplicationServiceImpl.java @@ -546,6 +546,9 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye for (DigitalEmployeeSyncRecord record : page.getRecords()) { Map view = readJsonObject(record.getBusinessView()); String kind = firstString(view.get("policy_kind")); + if (StringUtils.isBlank(kind) && isRouteInterventionPolicySnapshotView(view)) { + kind = "route_decision"; + } if (StringUtils.isBlank(kind)) continue; if (StringUtils.isNotBlank(policyKind) && !StringUtils.equals(kind, policyKind)) continue; grouped.computeIfAbsent(kind, ignored -> new ArrayList<>()).add(record); @@ -570,22 +573,40 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye private DigitalEmployeePolicySnapshotRowDto toPolicySnapshotRow(String policyKind, List records) { List> views = records.stream().map(record -> readJsonObject(record.getBusinessView())).toList(); - List signatures = views.stream().map(this::policySignature).distinct().toList(); + List> policyViews = views.stream() + .filter(view -> StringUtils.isNotBlank(firstString(view.get("policy_kind")))) + .toList(); + List> routeInterventionViews = views.stream() + .filter(this::isRouteInterventionPolicySnapshotView) + .toList(); + List signatures = policyViews.stream().map(this::policySignature).distinct().toList(); DigitalEmployeeSyncRecord latest = records.stream() .max(Comparator.comparing(DigitalEmployeeSyncRecord::getSyncedAt, Comparator.nullsLast(Comparator.naturalOrder()))) .orElse(records.get(0)); Map latestView = readJsonObject(latest.getBusinessView()); - String status = signatures.size() > 1 ? "conflict" : "consistent"; + String interventionConflictStatus = routeInterventionConflictStatus(routeInterventionViews); + boolean routeConflict = StringUtils.equals(interventionConflictStatus, "conflict"); + String status = signatures.size() > 1 || routeConflict ? "conflict" : "consistent"; + List conflictKeys = new ArrayList<>("conflict".equals(status) ? policyConflictKeys(policyViews) : List.of()); + if (routeConflict && !conflictKeys.contains("route_intervention_state")) { + conflictKeys.add("route_intervention_state"); + } return DigitalEmployeePolicySnapshotRowDto.builder() .policyKind(policyKind) .policyKey(firstString(latestView.get("policy_key"), policyKind)) .status(status) - .summary(firstString(latestView.get("summary"), latestView.get("title"), policyKind)) + .summary(routeConflict ? "跨设备路由干预冲突" : 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()) + .conflictKeys(conflictKeys) .latestSyncedAt(latest.getSyncedAt()) .recommendedResolution("conflict".equals(status) ? "review_latest_policy_snapshot" : "keep_current") + .routeDecisionCount(routeInterventionViews.size()) + .blockedCount(routeInterventionCount(routeInterventionViews, "blocked")) + .approvalRequiredCount(routeInterventionCount(routeInterventionViews, "approval_required")) + .missingContextCount(routeInterventionCount(routeInterventionViews, "missing_context")) + .notMappedCount(routeInterventionCount(routeInterventionViews, "not_mapped")) + .interventionConflictStatus(interventionConflictStatus) .syncRecordUrl(syncRecordUrl(latest.getOutboxId())) .clientKeyMasked(maskClientKey(latest.getClientKey())) .deviceId(latest.getDeviceId()) @@ -593,6 +614,47 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye .build(); } + private boolean isRouteInterventionPolicySnapshotView(Map view) { + String category = firstString(view.get("category")); + String kind = firstString(view.get("kind")); + return StringUtils.equals(category, "route_intervention") + || StringUtils.equals(category, "route_governance") + || contains(kind, "route_decision_blocked") + || contains(kind, "route_decision_approval_required") + || contains(kind, "route_action_policy_blocked") + || contains(kind, "route_action_missing_context") + || contains(kind, "route_action_not_mapped") + || StringUtils.isNotBlank(firstString(view.get("route_decision_id"), view.get("routeDecisionId"))); + } + + private String routeInterventionConflictStatus(List> views) { + if (views.isEmpty()) return "none"; + return views.stream().map(this::routeInterventionState).distinct().count() > 1 ? "conflict" : "consistent"; + } + + private int routeInterventionCount(List> views, String state) { + return (int) views.stream().filter(view -> StringUtils.equals(routeInterventionState(view), state)).count(); + } + + private String routeInterventionState(Map view) { + String kind = firstString(view.get("kind")); + String policyResult = firstString(view.get("policy_result"), view.get("policyResult")); + String status = firstString(view.get("status")); + if (contains(kind, "route_decision_approval_required") || contains(policyResult, "approval_required") || contains(status, "approval_required")) { + return "approval_required"; + } + if (contains(kind, "route_action_missing_context") || contains(status, "missing_context")) { + return "missing_context"; + } + if (contains(kind, "route_action_not_mapped") || contains(status, "not_mapped")) { + return "not_mapped"; + } + if (contains(kind, "route_decision_blocked") || contains(kind, "route_action_policy_blocked") || contains(policyResult, "blocked") || contains(status, "blocked")) { + return "blocked"; + } + return firstString(policyResult, status, "observed"); + } + private String policySignature(Map view) { Map signature = new LinkedHashMap<>(); for (String key : policyConflictCandidateKeys()) { diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/test/java/com/xspaceagi/agent/core/application/service/DigitalEmployeeSyncApplicationServiceImplTest.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/test/java/com/xspaceagi/agent/core/application/service/DigitalEmployeeSyncApplicationServiceImplTest.java index 7d44d3d8..b6e1a322 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/test/java/com/xspaceagi/agent/core/application/service/DigitalEmployeeSyncApplicationServiceImplTest.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/test/java/com/xspaceagi/agent/core/application/service/DigitalEmployeeSyncApplicationServiceImplTest.java @@ -1460,6 +1460,86 @@ class DigitalEmployeeSyncApplicationServiceImplTest { assertThat(row.getRecommendedResolution()).isEqualTo("review_latest_policy_snapshot"); } + @Test + void queryAdminPolicySnapshotsSummarizesRouteInterventionConflicts() { + DigitalEmployeeSyncRecord routeBlocked = record("outbox-route-blocked", "event", "event-route-1", 101L, new Date(10_000), + Map.of( + "entity_type", "event", + "category", "route_intervention", + "kind", "route_decision_blocked", + "status", "blocked", + "title", "入口路由被阻断", + "route_decision_id", "route-1", + "policy_result", "blocked", + "blocked_reason", "sensitive_action" + ), + Map.of("id", "event-route-1", "kind", "route_decision_blocked")); + routeBlocked.setClientKey("client-key-a"); + routeBlocked.setDeviceId("device-a"); + DigitalEmployeeSyncRecord routeApproval = record("outbox-route-approval", "event", "event-route-2", 101L, new Date(11_000), + Map.of( + "entity_type", "event", + "category", "route_intervention", + "kind", "route_decision_approval_required", + "status", "approval_required", + "title", "入口路由待审批", + "route_decision_id", "route-2", + "policy_result", "approval_required", + "approval_id", "approval-1" + ), + Map.of("id", "event-route-2", "kind", "route_decision_approval_required")); + routeApproval.setClientKey("client-key-b"); + routeApproval.setDeviceId("device-b"); + DigitalEmployeeSyncRecord routeMissingContext = record("outbox-route-missing-context", "event", "event-route-3", 101L, new Date(12_000), + Map.of( + "entity_type", "event", + "category", "route_governance", + "kind", "route_action_missing_context", + "status", "missing_context", + "title", "路由动作缺少上下文", + "route_decision_id", "route-3" + ), + Map.of("id", "event-route-3", "kind", "route_action_missing_context")); + routeMissingContext.setClientKey("client-key-c"); + routeMissingContext.setDeviceId("device-c"); + + 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( + routeBlocked, + routeApproval, + routeMissingContext, + record("outbox-policy", "event", "event-policy-1", 101L, new Date(13_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-skill", "event", "event-skill-1", 101L, new Date(14_000), + Map.of("entity_type", "event", "category", "skill_call_audit", "title", "技能审计", "status", "allowed"), + Map.of("id", "event-skill-1", "status", "allowed")) + ))); + + 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.getRouteDecisionCount()).isEqualTo(3); + assertThat(row.getBlockedCount()).isEqualTo(1); + assertThat(row.getApprovalRequiredCount()).isEqualTo(1); + assertThat(row.getMissingContextCount()).isEqualTo(1); + assertThat(row.getNotMappedCount()).isZero(); + assertThat(row.getInterventionConflictStatus()).isEqualTo("conflict"); + assertThat(row.getConflictKeys()).contains("route_intervention_state"); + } + @Test void recordDailyReportSendResultStoresRecordedObservation() { DigitalEmployeeAdminRecordRequestDto request = adminRecordRequest("daily_report", "report-1"); diff --git a/qiming/src/pages/SystemManagement/Content/DigitalEmployeeOps/index.tsx b/qiming/src/pages/SystemManagement/Content/DigitalEmployeeOps/index.tsx index d3fde07f..9558bb92 100644 --- a/qiming/src/pages/SystemManagement/Content/DigitalEmployeeOps/index.tsx +++ b/qiming/src/pages/SystemManagement/Content/DigitalEmployeeOps/index.tsx @@ -119,6 +119,7 @@ function renderSummary(record: DigitalEmployeeAdminWorkbenchRow) { record.cleanup_request_id && `Cleanup ${record.cleanup_request_id}`, record.route_decision_id && `Route ${record.route_decision_id}`, record.policy_kind && `Policy ${record.policy_kind}`, + record.intervention_conflict_status && `Route ${record.intervention_conflict_status}`, record.subscription_policy_id && `Subscription ${record.subscription_policy_id}`, record.daily_report_fact_id && `ReportFact ${record.daily_report_fact_id}`, record.memory_id && `Memory ${record.memory_id}`, @@ -675,6 +676,12 @@ const DigitalEmployeeOps: React.FC = () => { policy_key: record.policy_key, conflict_keys: record.conflict_keys, recommended_resolution: record.recommended_resolution, + route_decision_count: record.route_decision_count, + blocked_count: record.blocked_count, + approval_required_count: record.approval_required_count, + missing_context_count: record.missing_context_count, + not_mapped_count: record.not_mapped_count, + intervention_conflict_status: record.intervention_conflict_status, latest_synced_at: record.latest_synced_at, synced_at: record.latest_synced_at, client_key_masked: record.client_key_masked, @@ -833,12 +840,28 @@ const DigitalEmployeeOps: React.FC = () => { {detailRecord.conflict_keys?.length ? detailRecord.conflict_keys.join(' / ') : '暂无冲突字段'} + {detailRecord.intervention_conflict_status && + ` · 跨设备路由干预冲突 ${detailRecord.intervention_conflict_status}`} + {detailRecord.route_decision_count != null && + ` · 路由 ${detailRecord.route_decision_count} 条`} + {detailRecord.blocked_count != null && + ` · 阻断 ${detailRecord.blocked_count}`} + {detailRecord.approval_required_count != null && + ` · 待审批 ${detailRecord.approval_required_count}`} + {detailRecord.missing_context_count != null && + ` · 缺上下文 ${detailRecord.missing_context_count}`} + {detailRecord.not_mapped_count != null && + ` · 未映射 ${detailRecord.not_mapped_count}`} {detailRecord.recommended_resolution && ( {' '} · 建议:{detailRecord.recommended_resolution} )} + + {' '} + · 不恢复执行、不绕过审批、不远程改策略。 + )} {activeView === 'approval_governance' && ( diff --git a/qiming/src/types/interfaces/systemManage.ts b/qiming/src/types/interfaces/systemManage.ts index d610ca06..a4a43a7b 100644 --- a/qiming/src/types/interfaces/systemManage.ts +++ b/qiming/src/types/interfaces/systemManage.ts @@ -629,6 +629,12 @@ export interface DigitalEmployeeAdminWorkbenchRow { revision_id?: string; conflict_keys?: string[]; recommended_resolution?: string; + route_decision_count?: number; + blocked_count?: number; + approval_required_count?: number; + missing_context_count?: number; + not_mapped_count?: number; + intervention_conflict_status?: string; occurred_at?: string; synced_at?: string; latest_synced_at?: string; @@ -672,6 +678,12 @@ export interface DigitalEmployeePolicySnapshotRow { conflict_keys?: string[]; latest_synced_at?: string; recommended_resolution?: string; + route_decision_count?: number; + blocked_count?: number; + approval_required_count?: number; + missing_context_count?: number; + not_mapped_count?: number; + intervention_conflict_status?: string; sync_record_url?: string; client_key_masked?: string; device_id?: string; diff --git a/qimingclaw/crates/agent-electron-client/scripts/check-digital-employee.js b/qimingclaw/crates/agent-electron-client/scripts/check-digital-employee.js index dbcabf86..3e03348f 100644 --- a/qimingclaw/crates/agent-electron-client/scripts/check-digital-employee.js +++ b/qimingclaw/crates/agent-electron-client/scripts/check-digital-employee.js @@ -1191,6 +1191,55 @@ function checkDigitalAdminPolicySnapshots() { 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 backendServicePath = path.join( + backendRoot, + "app-platform-modules", + "app-platform-agent", + "app-platform-agent-core-application", + "src", + "main", + "java", + "com", + "xspaceagi", + "agent", + "core", + "application", + "service", + "DigitalEmployeeSyncApplicationServiceImpl.java", + ); + const backendDtoPath = path.join( + backendRoot, + "app-platform-modules", + "app-platform-agent", + "app-platform-agent-core-adapter", + "src", + "main", + "java", + "com", + "xspaceagi", + "agent", + "core", + "adapter", + "dto", + "digital", + "DigitalEmployeePolicySnapshotRowDto.java", + ); + const backendTestPath = path.join( + backendRoot, + "app-platform-modules", + "app-platform-agent", + "app-platform-agent-core-application", + "src", + "test", + "java", + "com", + "xspaceagi", + "agent", + "core", + "application", + "service", + "DigitalEmployeeSyncApplicationServiceImplTest.java", + ); const backendControllerPath = path.join( backendRoot, "app-platform-modules", @@ -1213,6 +1262,9 @@ function checkDigitalAdminPolicySnapshots() { const qimingOps = fs.readFileSync(qimingOpsPath, "utf8"); const qimingService = fs.readFileSync(qimingServicePath, "utf8"); const qimingTypes = fs.readFileSync(qimingTypesPath, "utf8"); + const backendService = fs.readFileSync(backendServicePath, "utf8"); + const backendDto = fs.readFileSync(backendDtoPath, "utf8"); + const backendTest = fs.readFileSync(backendTestPath, "utf8"); const backendController = fs.readFileSync(backendControllerPath, "utf8"); const requiredSnippets = [ [syncService, "policySnapshotBusinessView", "client policy snapshot business view"], @@ -1224,13 +1276,24 @@ function checkDigitalAdminPolicySnapshots() { [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"], + [backendService, "routeInterventionConflictStatus", "backend route intervention conflict aggregation"], + [backendService, "route_intervention_state", "backend route intervention conflict key"], + [backendDto, "intervention_conflict_status", "backend route intervention conflict status field"], + [backendDto, "route_decision_count", "backend route decision count field"], + [backendTest, "queryAdminPolicySnapshotsSummarizesRouteInterventionConflicts", "backend route intervention conflict test"], [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"], + [qimingTypes, "intervention_conflict_status", "qiming route intervention conflict type field"], [qimingOps, "策略冲突", "qiming policy conflict tab text"], [qimingOps, "conflict_keys", "qiming conflict key display"], + [qimingOps, "跨设备路由干预冲突", "qiming route intervention conflict copy"], + [qimingOps, "不恢复执行", "qiming route recovery guard copy"], + [qimingOps, "不绕过审批", "qiming approval guard copy"], + [qimingOps, "不远程改策略", "qiming remote policy edit guard copy"], [docs, "跨设备策略冲突视图", "docs policy conflict absorption"], + [docs, "跨设备路由干预冲突可视化", "docs route intervention conflict absorption"], ]; const missing = requiredSnippets .filter(([content, snippet]) => !content.includes(snippet)) diff --git a/qimingclaw/docs/digital-employee-frontend-integration-plan.md b/qimingclaw/docs/digital-employee-frontend-integration-plan.md index 7a5cf8dc..b0d4e3b4 100644 --- a/qimingclaw/docs/digital-employee-frontend-integration-plan.md +++ b/qimingclaw/docs/digital-employee-frontend-integration-plan.md @@ -599,8 +599,8 @@ GET /api/digital-employee/approvals 10. **入口路由 / 任务分流(路由业务视图与通知联动已开始)** - 已吸收:管理端通过 `/computer/chat` 进入 qimingclaw 后会生成本地任务事实;embedded `/api/ingress/route`、`/api/route-decisions` 和 `/api/digital-employee/route-decisions` 已由 qimingclaw adapter 接管。 - - 当前能力:入口请求会按上下文、ready PlanStep、调度、查询和控制动作生成 route decision,并写入 `digital_events.kind = route_decision` 与现有 event outbox;路由时间线优先从 qimingclaw 本地事件读取;低风险 route decision 已开始映射为明确的本地 governance command,并把 `created_command_id` 回填到 route decision;自动映射前会执行低风险/受保护高风险 action guard,未允许的控制动作会记录 `route_action_policy_blocked` 或 `route_action_missing_context`,避免静默创建命令;入口路由策略可从管理端远端下发,支持按 intent 阻断、按 intent 要求审批、限制自动创建 governance command,并在策略干预时先记录 route decision 再停止本地命令执行;首页已展示路由干预队列和治理审计摘要,审批通过/拒绝会写入本地 approval decision 并回写管理端,管理端审批更新拉取后会自动扫描已批准 route approval 并按幂等规则恢复执行;scheduler check 已接入 ready PlanStep 安全派发,首页立即执行后会触发一次派发 sweep 并展示派发结果;路由决策本地业务视图、首页摘要、治理审计、策略来源/拉取错误、阻断/待审/待干预/已恢复计数和通知联动已开始闭环。管理端正式路由 UI 预览已开始吸收,“策略中心”会把入口路由策略纳入统一拉取与状态展示。正式管理端策略中心预览已开始吸收,qiming 运营台可从 `policy_center` 查看入口路由策略快照和拉取状态。正式管理端路由治理预览已开始吸收,qiming-backend 的 `route_governance` 运营台视图会聚合 route blocked / approval required / missing context / not mapped / governance audit,qiming“数字员工运营台”的“路由治理”tab 只开放标记已看、忽略和重试策略检查等低风险动作意图。 - - 后续缺口:路由策略编辑、跨设备路由干预冲突可视化和高风险恢复执行策略 UI 仍待吸收。 + - 当前能力:入口请求会按上下文、ready PlanStep、调度、查询和控制动作生成 route decision,并写入 `digital_events.kind = route_decision` 与现有 event outbox;路由时间线优先从 qimingclaw 本地事件读取;低风险 route decision 已开始映射为明确的本地 governance command,并把 `created_command_id` 回填到 route decision;自动映射前会执行低风险/受保护高风险 action guard,未允许的控制动作会记录 `route_action_policy_blocked` 或 `route_action_missing_context`,避免静默创建命令;入口路由策略可从管理端远端下发,支持按 intent 阻断、按 intent 要求审批、限制自动创建 governance command,并在策略干预时先记录 route decision 再停止本地命令执行;首页已展示路由干预队列和治理审计摘要,审批通过/拒绝会写入本地 approval decision 并回写管理端,管理端审批更新拉取后会自动扫描已批准 route approval 并按幂等规则恢复执行;scheduler check 已接入 ready PlanStep 安全派发,首页立即执行后会触发一次派发 sweep 并展示派发结果;路由决策本地业务视图、首页摘要、治理审计、策略来源/拉取错误、阻断/待审/待干预/已恢复计数和通知联动已开始闭环。管理端正式路由 UI 预览已开始吸收,“策略中心”会把入口路由策略纳入统一拉取与状态展示。正式管理端策略中心预览已开始吸收,qiming 运营台可从 `policy_center` 查看入口路由策略快照和拉取状态。正式管理端路由治理预览已开始吸收,qiming-backend 的 `route_governance` 运营台视图会聚合 route blocked / approval required / missing context / not mapped / governance audit,qiming“数字员工运营台”的“路由治理”tab 只开放标记已看、忽略和重试策略检查等低风险动作意图。跨设备路由干预冲突可视化已开始吸收,qiming-backend 的策略快照读模型会把 route blocked / approval required / missing context / not mapped 汇总为 `intervention_conflict_status` 和干预计数,qiming“策略冲突”tab 可展示跨设备路由干预冲突,但不恢复执行、不绕过审批、不远程改策略。 + - 后续缺口:路由策略编辑和高风险恢复执行策略 UI 仍待吸收。 - 建议:下一轮围绕管理端正式路由 UI 和跨设备冲突可视化,把 embedded 批量干预入口推进到管理端运营台。 11. **诊断 / 成本 / 审计视图**