From c01c69941012ad8f0c75c6417b1427d00fd71fb7 Mon Sep 17 00:00:00 2001 From: baiyanyun Date: Sun, 14 Jun 2026 19:54:45 +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=E7=AE=A1=E7=90=86=E7=AB=AF=E5=AE=A1=E6=89=B9=E7=AD=96?= =?UTF-8?q?=E7=95=A5=E9=A2=84=E8=A7=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...talEmployeeSyncApplicationServiceImpl.java | 16 ++++ ...mployeeSyncApplicationServiceImplTest.java | 92 +++++++++++++++++++ .../Content/DigitalEmployeeOps/index.tsx | 35 +++++++ qiming/src/types/interfaces/systemManage.ts | 1 + .../scripts/check-digital-employee.js | 68 ++++++++++++++ ...ital-employee-frontend-integration-plan.md | 4 +- 6 files changed, 214 insertions(+), 2 deletions(-) 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 333812ef..6aed5767 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 @@ -1148,6 +1148,7 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye return switch (view) { case "interventions" -> isInterventionRow(row); case "approval_governance" -> isApprovalGovernanceRow(row); + case "approval_policies" -> isApprovalPolicyRow(row); case "service_operations" -> isServiceOperationsRow(row); case "artifact_lifecycle" -> isArtifactLifecycleRow(row); case "notifications" -> isNotificationRow(row); @@ -1191,6 +1192,21 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye )); } + private boolean isApprovalPolicyRow(DigitalEmployeeAdminWorkbenchRowDto row) { + String category = firstString(row.getCategory()); + String kind = firstString(row.getKind()); + return Set.of("approval_policy", "approval_decision_policy", "conflict_policy", "risk_approval_policy").contains(category) + || contains(kind, "approval_policy") + || contains(kind, "approval_decision_policy") + || contains(kind, "conflict_policy") + || contains(kind, "risk_approval_policy") + || StringUtils.isNotBlank(row.getApprovalPolicyId()) + || StringUtils.isNotBlank(row.getConflictPolicyId()) + || StringUtils.isNotBlank(row.getDecisionGuardStatus()) + || row.getBatchAcceptRemoteDisabled() != null + || row.getAutoDecisionDisabled() != null; + } + private boolean isArtifactLifecycleRow(DigitalEmployeeAdminWorkbenchRowDto row) { return StringUtils.equals(row.getEntityType(), "artifact") || startsWithAny(row.getKind(), "artifact_") 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 aabce53b..f2c7b7c7 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 @@ -361,6 +361,98 @@ class DigitalEmployeeSyncApplicationServiceImplTest { assertThat(result.getRecords().get(3).getApprovalId()).isEqualTo("approval-1"); } + @Test + void queryAdminWorkbenchApprovalPoliciesReturnsPolicyRowsOnly() { + when(repository.queryBusinessRecords(eq(101L), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(1L), eq(20L))) + .thenReturn(page(List.of( + record("outbox-approval-policy", "event", "event-approval-policy-1", 101L, new Date(10_000), + Map.ofEntries( + Map.entry("entity_type", "event"), + Map.entry("category", "approval_policy"), + Map.entry("kind", "approval_policy_snapshot"), + Map.entry("status", "active"), + Map.entry("title", "审批策略快照"), + Map.entry("approval_policy_id", "approval-policy-1"), + Map.entry("decision_guard_status", "protected"), + Map.entry("batch_accept_remote_disabled", true), + Map.entry("auto_decision_disabled", true), + Map.entry("policy_reason", "manual_review_required"), + Map.entry("policy_source", "admin"), + Map.entry("policy_status", "synced") + ), + Map.of("id", "event-approval-policy-1", "kind", "approval_policy_snapshot")), + record("outbox-conflict-policy", "event", "event-conflict-policy-1", 101L, new Date(11_000), + Map.of( + "entity_type", "event", + "category", "conflict_policy", + "kind", "approval_conflict_policy_snapshot", + "status", "active", + "title", "冲突策略快照", + "conflict_policy_id", "conflict-policy-1", + "decision_guard_status", "manual_only" + ), + Map.of("id", "event-conflict-policy-1", "kind", "approval_conflict_policy_snapshot")), + record("outbox-risk-approval-policy", "event", "event-risk-policy-1", 101L, new Date(12_000), + Map.of( + "entity_type", "event", + "category", "risk_approval_policy", + "kind", "risk_approval_policy_pulled", + "status", "synced", + "title", "风险审批策略", + "approval_policy_id", "risk-policy-1" + ), + Map.of("id", "event-risk-policy-1", "kind", "risk_approval_policy_pulled")), + record("outbox-policy-snapshot", "policy_snapshot", "approval-policy-snapshot", 101L, new Date(13_000), + Map.ofEntries( + Map.entry("entity_type", "policy_snapshot"), + Map.entry("category", "policy_snapshot"), + Map.entry("kind", "approval_decision_policy_snapshot"), + Map.entry("status", "synced"), + Map.entry("title", "裁决策略保护"), + Map.entry("approval_policy_id", "approval-policy-2"), + Map.entry("conflict_policy_id", "conflict-policy-2"), + Map.entry("decision_guard_status", "protected"), + Map.entry("batch_accept_remote_disabled", true), + Map.entry("auto_decision_disabled", true), + Map.entry("policy_reason", "batch_disabled") + ), + Map.of("id", "approval-policy-snapshot", "kind", "approval_decision_policy_snapshot")), + record("outbox-approval", "approval", "approval-1", 101L, new Date(14_000), + Map.of("entity_type", "approval", "title", "普通审批", "status", "pending"), + Map.of("id", "approval-1", "status", "pending")), + record("outbox-notification", "notification", "notification-1", 101L, new Date(15_000), + Map.of("entity_type", "notification", "title", "普通通知", "status", "unread"), + Map.of("id", "notification-1", "status", "unread")), + record("outbox-artifact", "artifact", "artifact-1", 101L, new Date(16_000), + Map.of("entity_type", "artifact", "title", "普通产物", "status", "active"), + Map.of("id", "artifact-1", "status", "active")), + record("outbox-route", "event", "event-route-1", 101L, new Date(17_000), + Map.of("entity_type", "event", "category", "route_governance", "title", "路由", "status", "blocked"), + Map.of("id", "event-route-1", "status", "blocked")), + record("outbox-audit", "event", "event-audit-1", 101L, new Date(18_000), + Map.of("entity_type", "event", "category", "tool_call_audit", "title", "审计", "status", "warn"), + Map.of("id", "event-audit-1", "status", "warn")) + ))); + + DigitalEmployeeAdminWorkbenchPageDto result = service.queryAdminWorkbench( + "approval_policies", null, null, null, null, null, null, null, null, null, null, null, 1L, 20L + ); + + assertThat(result.getTotal()).isEqualTo(4); + assertThat(result.getRecords()).extracting(DigitalEmployeeAdminWorkbenchRowDto::getEntityId) + .containsExactly("event-approval-policy-1", "event-conflict-policy-1", "event-risk-policy-1", "approval-policy-snapshot"); + assertThat(result.getRecords().get(0).getView()).isEqualTo("approval_policies"); + assertThat(result.getRecords().get(0).getApprovalPolicyId()).isEqualTo("approval-policy-1"); + assertThat(result.getRecords().get(0).getDecisionGuardStatus()).isEqualTo("protected"); + assertThat(result.getRecords().get(0).getBatchAcceptRemoteDisabled()).isTrue(); + assertThat(result.getRecords().get(0).getAutoDecisionDisabled()).isTrue(); + assertThat(result.getRecords().get(0).getPolicyReason()).isEqualTo("manual_review_required"); + assertThat(result.getRecords().get(1).getConflictPolicyId()).isEqualTo("conflict-policy-1"); + assertThat(result.getRecords().get(2).getApprovalPolicyId()).isEqualTo("risk-policy-1"); + assertThat(result.getRecords().get(3).getConflictPolicyId()).isEqualTo("conflict-policy-2"); + assertThat(result.getRecords().get(3).getTitle()).isEqualTo("裁决策略保护"); + } + @Test void queryAdminWorkbenchMemoryGovernanceReturnsMemoryRowsOnly() { when(repository.queryBusinessRecords(eq(101L), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(1L), eq(20L))) diff --git a/qiming/src/pages/SystemManagement/Content/DigitalEmployeeOps/index.tsx b/qiming/src/pages/SystemManagement/Content/DigitalEmployeeOps/index.tsx index 42921d3b..e31264fc 100644 --- a/qiming/src/pages/SystemManagement/Content/DigitalEmployeeOps/index.tsx +++ b/qiming/src/pages/SystemManagement/Content/DigitalEmployeeOps/index.tsx @@ -39,6 +39,7 @@ const WORKBENCH_TABS: Array<{ { key: 'business', label: '业务视图' }, { key: 'interventions', label: '介入台' }, { key: 'approval_governance', label: '审批治理' }, + { key: 'approval_policies', label: '审批策略' }, { key: 'service_operations', label: '服务运营' }, { key: 'artifact_lifecycle', label: '产物治理' }, { key: 'notifications', label: '通知运营' }, @@ -189,6 +190,16 @@ function actionTarget(record: DigitalEmployeeAdminWorkbenchRow, view: DigitalEmp } return { entityType: 'event', entityId: record.entity_id }; } + if (view === 'approval_policies') { + return { + entityType: 'policy_snapshot', + entityId: + record.approval_policy_id || + record.conflict_policy_id || + record.policy_key || + record.entity_id, + }; + } if (view === 'interventions' && record.approval_id) { return { entityType: 'approval', entityId: record.approval_id }; } @@ -263,6 +274,13 @@ function allowedActions(record: DigitalEmployeeAdminWorkbenchRow, view: DigitalE { action: 'dismiss', label: '忽略' }, ]; } + if (view === 'approval_policies') { + return [ + { action: 'pull_policy', label: '批量拉取策略' }, + { action: 'mark_reviewed', label: '标记已看' }, + { action: 'dismiss', label: '忽略' }, + ]; + } if (view === 'interventions' && record.approval_id) { return [ { action: 'keep_local', label: '保留本地' }, @@ -918,6 +936,23 @@ const DigitalEmployeeOps: React.FC = () => { )} + {activeView === 'approval_policies' && ( + + 审批策略: + {detailRecord.approval_policy_id && `审批策略 ${detailRecord.approval_policy_id}`} + {detailRecord.conflict_policy_id && ` · 冲突策略 ${detailRecord.conflict_policy_id}`} + {detailRecord.decision_guard_status && ` · 裁决保护 ${detailRecord.decision_guard_status}`} + {detailRecord.policy_source && ` · 来源 ${detailRecord.policy_source}`} + {detailRecord.policy_status && ` · 状态 ${detailRecord.policy_status}`} + {detailRecord.policy_reason && ` · 策略原因 ${detailRecord.policy_reason}`} + {detailRecord.batch_accept_remote_disabled && ' · 批量采纳远端禁用'} + {detailRecord.auto_decision_disabled && ' · 自动裁决禁用'} + + {' '} + · 只展示审批裁决策略和保护状态,不远程编辑策略、不自动裁决、不批量采纳远端。 + + + )} {activeView === 'service_operations' && ( 服务运营: diff --git a/qiming/src/types/interfaces/systemManage.ts b/qiming/src/types/interfaces/systemManage.ts index 970bfcbc..2f3e2fc5 100644 --- a/qiming/src/types/interfaces/systemManage.ts +++ b/qiming/src/types/interfaces/systemManage.ts @@ -560,6 +560,7 @@ export type DigitalEmployeeAdminWorkbenchView = | 'business' | 'interventions' | 'approval_governance' + | 'approval_policies' | 'service_operations' | 'artifact_lifecycle' | 'notifications' 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 2d6087ff..71ffed99 100644 --- a/qimingclaw/crates/agent-electron-client/scripts/check-digital-employee.js +++ b/qimingclaw/crates/agent-electron-client/scripts/check-digital-employee.js @@ -1523,6 +1523,73 @@ function checkDigitalAdminApprovalGovernanceWorkbench() { console.log("[DigitalEmployeeCheck] admin approval governance workbench hooks OK"); } +function checkDigitalAdminApprovalPoliciesWorkbench() { + console.log("\n[DigitalEmployeeCheck] Verify admin approval policies workbench hooks"); + const backendRoot = path.resolve(packageRoot, "..", "..", "..", "qiming-backend"); + const qimingRoot = path.resolve(packageRoot, "..", "..", "..", "qiming"); + const servicePath = 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 testPath = 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 qimingOpsPath = path.join(qimingRoot, "src", "pages", "SystemManagement", "Content", "DigitalEmployeeOps", "index.tsx"); + const qimingTypesPath = path.join(qimingRoot, "src", "types", "interfaces", "systemManage.ts"); + const docsPath = path.resolve(packageRoot, "..", "..", "docs", "digital-employee-frontend-integration-plan.md"); + const service = fs.readFileSync(servicePath, "utf8"); + const test = fs.readFileSync(testPath, "utf8"); + const qimingOps = fs.readFileSync(qimingOpsPath, "utf8"); + const qimingTypes = fs.readFileSync(qimingTypesPath, "utf8"); + const docs = fs.readFileSync(docsPath, "utf8"); + const requiredSnippets = [ + [service, "approval_policies", "backend approval policies view"], + [service, "isApprovalPolicyRow", "backend approval policy matcher"], + [service, "approval_policy_id", "backend approval policy id projection"], + [service, "conflict_policy_id", "backend conflict policy id projection"], + [service, "decision_guard_status", "backend decision guard projection"], + [test, "queryAdminWorkbenchApprovalPoliciesReturnsPolicyRowsOnly", "backend approval policies test"], + [qimingTypes, "approval_policies", "qiming approval policies view type"], + [qimingOps, "审批策略", "qiming approval policies tab"], + [qimingOps, "不远程编辑策略", "qiming no remote policy edit copy"], + [qimingOps, "不自动裁决", "qiming no auto decision copy"], + [qimingOps, "不批量采纳远端", "qiming no batch remote accept copy"], + [docs, "正式管理端审批策略预览", "docs approval policy absorption"], + ]; + const missing = requiredSnippets + .filter(([content, snippet]) => !content.includes(snippet)) + .map(([, , label]) => label); + if (missing.length > 0) { + throw new Error(`Missing admin approval policies workbench hooks: ${missing.join(", ")}`); + } + console.log("[DigitalEmployeeCheck] admin approval policies workbench hooks OK"); +} + function checkDigitalAdminMemorySkillWorkbench() { console.log("\n[DigitalEmployeeCheck] Verify admin memory governance and skill audit workbench hooks"); const backendRoot = path.resolve(packageRoot, "..", "..", "..", "qiming-backend"); @@ -2675,6 +2742,7 @@ function main() { checkDigitalBackendMaterializedFacts(); checkDigitalAdminRecordBillingArchiveShell(); checkDigitalAdminApprovalGovernanceWorkbench(); + checkDigitalAdminApprovalPoliciesWorkbench(); checkDigitalAdminMemorySkillWorkbench(); checkDigitalAdminPolicySchedulerWorkbenches(); checkDigitalAdminPlanGovernanceWorkbenches(); diff --git a/qimingclaw/docs/digital-employee-frontend-integration-plan.md b/qimingclaw/docs/digital-employee-frontend-integration-plan.md index c2b84cf6..4c455a30 100644 --- a/qimingclaw/docs/digital-employee-frontend-integration-plan.md +++ b/qimingclaw/docs/digital-employee-frontend-integration-plan.md @@ -588,8 +588,8 @@ GET /api/digital-employee/approvals - 建议:下一轮围绕正式管理端生命周期工作台和跨设备图谱,把 embedded 预览聚合推进到跨端运营治理。 8. **审批 / 人工介入增强** - - 已吸收:approval 记录、workday decision、ACP permission 响应桥已有雏形;用户处理审批时会写入 `approval_decision` runtime event;评论、转交、超时、风险标记会写入 `approval_action_recorded` / `approval_action_rejected`,并联动 embedded `/api/approval/:approvalId/timeline`、审批列表摘要、管理端 `/api/digital-employee/approval-history` 历史视图、`/api/approval/subscriptions` 订阅策略和 notification 投影;风险审批策略已支持管理端远端下发、手动拉取、本地策略评估、高风险动作前置拦截和本地设置页管理,`/api/risk-approval/policy` 可读写本地策略并记录 `update_risk_approval_policy`;多步审批 workflow 已复用 `digital_approvals.payload`,支持步骤推进、转交/超时/风险动作更新当前步骤、管理端审批更新拉取和审批处理回写,审批动作历史、风险策略、多步流程与通知联动已开始闭环;跨端审批冲突可视化已开始吸收,管理端远端更新与本地终态/更新时间/current step 不一致时会保留本地状态、记录 `approval_conflict_detected`、在业务视图投影 `approval_conflict_*` 字段,并在首页审批卡暂停直接同意/驳回;冲突解决动作闭环已开始吸收,客户端可通过独立 `resolveApprovalConflict` bridge 选择保留本地、采纳管理端或标记已复核,裁决会写入 `approval_conflict_resolved` / `approval_conflict_resolution_rejected`,并在审批业务视图投影 `approval_conflict_resolution`、`approval_conflict_resolved_at` 和裁决人摘要。跨设备批量冲突排查已开始吸收,embedded 新增“介入台”聚合审批冲突、风险审批、路由干预和待处理通知,支持筛选、分页、批量保留本地和批量标记复核;批量采纳管理端被 `batch_accept_remote_disabled` 保护,避免扩大状态分叉。正式管理端审批工作台预览已开始吸收,qiming-backend 新增 `approval_governance` 运营台视图聚合审批冲突、风险审批、路由待审和审批动作历史,qiming“数字员工运营台”新增“审批治理” tab,并只开放保留本地、标记复核、标记已看、忽略和重试策略检查等低风险动作意图。正式管理端审批裁决策略预览已开始吸收,审批治理详情可展示 `approval_policy_id`、`conflict_policy_id`、`decision_guard_status`、`batch_accept_remote_disabled`、`auto_decision_disabled` 和策略原因,用于解释为什么只允许低风险复核或保留本地。 - - 缺口:仍缺少管理端裁决策略 UI、批量采纳远端和高风险审批自动执行。 + - 已吸收:approval 记录、workday decision、ACP permission 响应桥已有雏形;用户处理审批时会写入 `approval_decision` runtime event;评论、转交、超时、风险标记会写入 `approval_action_recorded` / `approval_action_rejected`,并联动 embedded `/api/approval/:approvalId/timeline`、审批列表摘要、管理端 `/api/digital-employee/approval-history` 历史视图、`/api/approval/subscriptions` 订阅策略和 notification 投影;风险审批策略已支持管理端远端下发、手动拉取、本地策略评估、高风险动作前置拦截和本地设置页管理,`/api/risk-approval/policy` 可读写本地策略并记录 `update_risk_approval_policy`;多步审批 workflow 已复用 `digital_approvals.payload`,支持步骤推进、转交/超时/风险动作更新当前步骤、管理端审批更新拉取和审批处理回写,审批动作历史、风险策略、多步流程与通知联动已开始闭环;跨端审批冲突可视化已开始吸收,管理端远端更新与本地终态/更新时间/current step 不一致时会保留本地状态、记录 `approval_conflict_detected`、在业务视图投影 `approval_conflict_*` 字段,并在首页审批卡暂停直接同意/驳回;冲突解决动作闭环已开始吸收,客户端可通过独立 `resolveApprovalConflict` bridge 选择保留本地、采纳管理端或标记已复核,裁决会写入 `approval_conflict_resolved` / `approval_conflict_resolution_rejected`,并在审批业务视图投影 `approval_conflict_resolution`、`approval_conflict_resolved_at` 和裁决人摘要。跨设备批量冲突排查已开始吸收,embedded 新增“介入台”聚合审批冲突、风险审批、路由干预和待处理通知,支持筛选、分页、批量保留本地和批量标记复核;批量采纳管理端被 `batch_accept_remote_disabled` 保护,避免扩大状态分叉。正式管理端审批工作台预览已开始吸收,qiming-backend 新增 `approval_governance` 运营台视图聚合审批冲突、风险审批、路由待审和审批动作历史,qiming“数字员工运营台”新增“审批治理” tab,并只开放保留本地、标记复核、标记已看、忽略和重试策略检查等低风险动作意图。正式管理端审批裁决策略预览已开始吸收,审批治理详情可展示 `approval_policy_id`、`conflict_policy_id`、`decision_guard_status`、`batch_accept_remote_disabled`、`auto_decision_disabled` 和策略原因,用于解释为什么只允许低风险复核或保留本地。正式管理端审批策略预览已开始吸收,qiming“数字员工运营台”的“审批策略”tab 可独立筛选审批策略、冲突策略、风险审批策略和裁决保护快照,只记录批量拉取策略、标记已看和忽略意图,不远程编辑策略、不自动裁决、不批量采纳远端。 + - 缺口:仍缺少管理端可编辑裁决策略 UI、批量采纳远端和高风险审批自动执行。 - 建议:下一轮围绕正式管理端审批工作台,把 embedded 介入台预览推进到外部管理端批量排查和策略化回写。 9. **通知 / Inbox / 用户消息**