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 f46f35e4..238b60ab 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 @@ -996,6 +996,7 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye private boolean matchesAdminView(String view, DigitalEmployeeAdminWorkbenchRowDto row) { return switch (view) { case "interventions" -> isInterventionRow(row); + case "approval_governance" -> isApprovalGovernanceRow(row); case "artifact_lifecycle" -> isArtifactLifecycleRow(row); case "notifications" -> isNotificationRow(row); case "audits" -> isAuditRow(row); @@ -1012,6 +1013,24 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye || StringUtils.equals(row.getCategory(), "action_notification"); } + private boolean isApprovalGovernanceRow(DigitalEmployeeAdminWorkbenchRowDto row) { + String kind = firstString(row.getKind()); + String category = firstString(row.getCategory()); + return StringUtils.equals(category, "approval_conflict") + || StringUtils.equals(category, "risk_approval") + || contains(kind, "risk_approval") + || contains(kind, "approval_action_recorded") + || contains(kind, "approval_decision") + || contains(kind, "approval_conflict_resolved") + || contains(kind, "approval_conflict_resolution") + || (StringUtils.isNotBlank(row.getApprovalId()) && ( + StringUtils.equals(category, "route_intervention") + || StringUtils.equals(category, "route_governance") + || contains(kind, "route_decision_approval_required") + || contains(kind, "route_decision_blocked") + )); + } + 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 1885f57a..15f98aef 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 @@ -278,6 +278,77 @@ class DigitalEmployeeSyncApplicationServiceImplTest { assertThat(result.getRecords().get(1).getRouteDecisionId()).isEqualTo("route-1"); } + @Test + void queryAdminWorkbenchApprovalGovernanceReturnsApprovalConflictRiskAndHistoryRowsOnly() { + 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-conflict", "approval", "approval-1", 101L, new Date(10_000), + Map.of( + "entity_type", "approval", + "title", "远端审批冲突", + "status", "pending", + "approval_conflict_active", true, + "approval_conflict_reason", "terminal_status_mismatch", + "approval_id", "approval-1", + "plan_id", "plan-1", + "task_id", "task-1", + "priority", "high" + ), + Map.of("id", "approval-1", "status", "pending")), + record("outbox-risk-approval", "event", "event-risk-1", 101L, new Date(11_000), + Map.of( + "entity_type", "event", + "category", "risk_approval", + "kind", "risk_approval_required", + "status", "blocked", + "title", "高风险动作待审批", + "approval_id", "approval-risk-1", + "plan_id", "plan-1", + "risk_level", "high" + ), + Map.of("id", "event-risk-1", "kind", "risk_approval_required")), + record("outbox-route-approval", "event", "event-route-approval", 101L, new Date(12_000), + Map.of( + "entity_type", "event", + "category", "route_governance", + "kind", "route_decision_approval_required", + "status", "pending", + "title", "路由动作待审批", + "approval_id", "approval-route-1", + "route_decision_id", "route-1", + "reason", "route_decision_approval_required" + ), + Map.of("id", "event-route-approval", "kind", "route_decision_approval_required")), + record("outbox-approval-history", "event", "event-approval-history", 101L, new Date(13_000), + Map.of( + "entity_type", "event", + "kind", "approval_action_recorded", + "status", "recorded", + "title", "审批动作记录", + "approval_id", "approval-1", + "message", "已转交审批" + ), + Map.of("id", "event-approval-history", "kind", "approval_action_recorded")), + record("outbox-task", "task", "task-1", 101L, new Date(14_000), + Map.of("entity_type", "task", "title", "普通任务", "status", "running"), + Map.of("id", "task-1", "status", "running")) + ))); + + DigitalEmployeeAdminWorkbenchPageDto result = service.queryAdminWorkbench( + "approval_governance", 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("approval-1", "event-risk-1", "event-route-approval", "event-approval-history"); + assertThat(result.getRecords().get(0).getView()).isEqualTo("approval_governance"); + assertThat(result.getRecords().get(0).getApprovalId()).isEqualTo("approval-1"); + assertThat(result.getRecords().get(0).getCategory()).isEqualTo("approval_conflict"); + assertThat(result.getRecords().get(1).getCategory()).isEqualTo("risk_approval"); + assertThat(result.getRecords().get(2).getRouteDecisionId()).isEqualTo("route-1"); + assertThat(result.getRecords().get(3).getApprovalId()).isEqualTo("approval-1"); + } + @Test void queryAdminWorkbenchAuditsRedactsSensitivePayloadFields() { 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 5801680d..d4721e98 100644 --- a/qiming/src/pages/SystemManagement/Content/DigitalEmployeeOps/index.tsx +++ b/qiming/src/pages/SystemManagement/Content/DigitalEmployeeOps/index.tsx @@ -38,6 +38,7 @@ const WORKBENCH_TABS: Array<{ }> = [ { key: 'business', label: '业务视图' }, { key: 'interventions', label: '介入台' }, + { key: 'approval_governance', label: '审批治理' }, { key: 'artifact_lifecycle', label: '产物治理' }, { key: 'notifications', label: '通知运营' }, { key: 'audits', label: '审计诊断' }, @@ -135,6 +136,15 @@ function renderSummary(record: DigitalEmployeeAdminWorkbenchRow) { } function actionTarget(record: DigitalEmployeeAdminWorkbenchRow, view: DigitalEmployeeAdminWorkbenchView) { + if (view === 'approval_governance') { + if (record.category === 'approval_conflict' && record.approval_id) { + return { entityType: 'approval', entityId: record.approval_id }; + } + if (record.route_decision_id) { + return { entityType: 'route_decision', entityId: record.route_decision_id }; + } + return { entityType: 'event', entityId: record.entity_id }; + } if (view === 'interventions' && record.approval_id) { return { entityType: 'approval', entityId: record.approval_id }; } @@ -163,6 +173,25 @@ function actionTarget(record: DigitalEmployeeAdminWorkbenchRow, view: DigitalEmp } function allowedActions(record: DigitalEmployeeAdminWorkbenchRow, view: DigitalEmployeeAdminWorkbenchView) { + if (view === 'approval_governance') { + if (record.category === 'approval_conflict' && record.approval_id) { + return [ + { action: 'keep_local', label: '保留本地' }, + { action: 'mark_reviewed', label: '标记复核' }, + ]; + } + if (record.route_decision_id) { + return [ + { action: 'mark_reviewed', label: '标记已看' }, + { action: 'dismiss', label: '忽略' }, + { action: 'retry_policy_check', label: '重试策略检查' }, + ]; + } + return [ + { action: 'mark_reviewed', label: '标记已看' }, + { action: 'dismiss', label: '忽略' }, + ]; + } if (view === 'interventions' && record.approval_id) { return [ { action: 'keep_local', label: '保留本地' }, @@ -682,6 +711,20 @@ const DigitalEmployeeOps: React.FC = () => { )} )} + {activeView === 'approval_governance' && ( + + 审批治理: + {detailRecord.approval_id + ? `审批 ${detailRecord.approval_id}` + : '审批相关事件'} + {detailRecord.category && ` · ${detailRecord.category}`} + {detailRecord.priority && ` · 优先级 ${detailRecord.priority}`} + + {' '} + · 批量/高风险裁决不开放,动作等待客户端拉取执行。 + + + )} {allowedActions(detailRecord, activeView).length > 0 && (
低风险动作 diff --git a/qiming/src/types/interfaces/systemManage.ts b/qiming/src/types/interfaces/systemManage.ts index 00626db6..3b8abec2 100644 --- a/qiming/src/types/interfaces/systemManage.ts +++ b/qiming/src/types/interfaces/systemManage.ts @@ -559,6 +559,7 @@ export interface DigitalEmployeeSyncRecordPage { export type DigitalEmployeeAdminWorkbenchView = | 'business' | 'interventions' + | 'approval_governance' | 'artifact_lifecycle' | 'notifications' | 'audits' 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 6641eef3..6138484e 100644 --- a/qimingclaw/crates/agent-electron-client/scripts/check-digital-employee.js +++ b/qimingclaw/crates/agent-electron-client/scripts/check-digital-employee.js @@ -1382,6 +1382,73 @@ function checkDigitalAdminRecordBillingArchiveShell() { console.log("[DigitalEmployeeCheck] admin send/billing/audit archive shell hooks OK"); } +function checkDigitalAdminApprovalGovernanceWorkbench() { + console.log("\n[DigitalEmployeeCheck] Verify admin approval governance 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_governance", "backend approval governance view"], + [service, "isApprovalGovernanceRow", "backend approval governance matcher"], + [service, "approval_conflict_active", "backend approval conflict classifier"], + [service, "risk_approval", "backend risk approval classifier"], + [service, "approval_action_recorded", "backend approval action history classifier"], + [test, "queryAdminWorkbenchApprovalGovernanceReturnsApprovalConflictRiskAndHistoryRowsOnly", "backend approval governance test"], + [qimingTypes, "approval_governance", "qiming approval governance view type"], + [qimingOps, "审批治理", "qiming approval governance tab"], + [qimingOps, "保留本地", "qiming approval keep local action"], + [qimingOps, "标记复核", "qiming approval mark reviewed action"], + [qimingOps, "批量/高风险裁决不开放", "qiming approval governance safety copy"], + [docs, "正式管理端审批工作台预览", "docs approval governance absorption"], + ]; + const missing = requiredSnippets + .filter(([content, snippet]) => !content.includes(snippet)) + .map(([, , label]) => label); + if (missing.length > 0) { + throw new Error(`Missing admin approval governance workbench hooks: ${missing.join(", ")}`); + } + console.log("[DigitalEmployeeCheck] admin approval governance workbench hooks OK"); +} + function checkLocalDatabaseSchema() { console.log("\n[DigitalEmployeeCheck] Verify local SQLite digital schema"); const dbPath = path.join(os.homedir(), ".qimingclaw", "qimingclaw.db"); @@ -1444,6 +1511,7 @@ function main() { checkDigitalAdminPolicySnapshots(); checkDigitalBackendMaterializedFacts(); checkDigitalAdminRecordBillingArchiveShell(); + checkDigitalAdminApprovalGovernanceWorkbench(); checkLocalDatabaseSchema(); console.log("\n[DigitalEmployeeCheck] All checks passed"); } catch (error) { diff --git a/qimingclaw/docs/digital-employee-frontend-integration-plan.md b/qimingclaw/docs/digital-employee-frontend-integration-plan.md index c13b4428..c566d56c 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` 保护,避免扩大状态分叉。 - - 缺口:仍缺少正式管理端审批工作台和管理端裁决策略 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,并只开放保留本地、标记复核、标记已看、忽略和重试策略检查等低风险动作意图。 + - 缺口:仍缺少管理端裁决策略 UI、批量采纳远端和高风险审批自动执行。 - 建议:下一轮围绕正式管理端审批工作台,把 embedded 介入台预览推进到外部管理端批量排查和策略化回写。 9. **通知 / Inbox / 用户消息**