吸收数字员工管理端审批治理预览
This commit is contained in:
@@ -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_")
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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 = () => {
|
||||
)}
|
||||
</Typography.Paragraph>
|
||||
)}
|
||||
{activeView === 'approval_governance' && (
|
||||
<Typography.Paragraph>
|
||||
<Typography.Text strong>审批治理:</Typography.Text>
|
||||
{detailRecord.approval_id
|
||||
? `审批 ${detailRecord.approval_id}`
|
||||
: '审批相关事件'}
|
||||
{detailRecord.category && ` · ${detailRecord.category}`}
|
||||
{detailRecord.priority && ` · 优先级 ${detailRecord.priority}`}
|
||||
<Typography.Text type="secondary">
|
||||
{' '}
|
||||
· 批量/高风险裁决不开放,动作等待客户端拉取执行。
|
||||
</Typography.Text>
|
||||
</Typography.Paragraph>
|
||||
)}
|
||||
{allowedActions(detailRecord, activeView).length > 0 && (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Typography.Title level={5}>低风险动作</Typography.Title>
|
||||
|
||||
@@ -559,6 +559,7 @@ export interface DigitalEmployeeSyncRecordPage {
|
||||
export type DigitalEmployeeAdminWorkbenchView =
|
||||
| 'business'
|
||||
| 'interventions'
|
||||
| 'approval_governance'
|
||||
| 'artifact_lifecycle'
|
||||
| 'notifications'
|
||||
| 'audits'
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 / 用户消息**
|
||||
|
||||
Reference in New Issue
Block a user