吸收数字员工管理端治理执行安全壳

This commit is contained in:
baiyanyun
2026-06-13 14:56:54 +08:00
parent f08b0c39d9
commit 95838dba04
7 changed files with 297 additions and 4 deletions

View File

@@ -63,6 +63,8 @@ public class DigitalEmployeeAdminWorkbenchRowDto {
private String scheduleId;
@JsonProperty("job_id")
private String jobId;
@JsonProperty("revision_id")
private String revisionId;
@JsonProperty("occurred_at")
private String occurredAt;
@JsonProperty("synced_at")

View File

@@ -74,6 +74,8 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
"skill:mark_reviewed", "skill:dismiss",
"policy_snapshot:pull_policy", "policy_snapshot:mark_reviewed", "policy_snapshot:dismiss",
"scheduler:retry_scheduler_check", "scheduler:mark_reviewed", "scheduler:dismiss",
"plan:mark_reviewed", "plan:dismiss", "plan:request_revision_review", "plan:record_escalation",
"revision:mark_reviewed", "revision:dismiss", "revision:request_revision_review",
"audit:mark_reviewed", "audit:dismiss",
"event:mark_reviewed", "event:dismiss"
);
@@ -937,6 +939,8 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
case "edit_policy_remote" -> List.of("edit_policy_remote_disabled");
case "run_schedule_now" -> List.of("run_schedule_now_disabled");
case "start_scheduler" -> List.of("start_scheduler_disabled");
case "hard_interrupt" -> List.of("hard_interrupt_disabled");
case "activate_plan", "cancel_run", "cancel_task", "accept_remote_approval_batch" -> List.of("high_risk_governance_action_disabled");
default -> List.of();
};
}
@@ -1006,6 +1010,7 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
.policyKey(firstString(businessView.get("policy_key"), businessView.get("policyKey"), payload.get("policy_key"), payload.get("policyKey")))
.scheduleId(firstString(businessView.get("schedule_id"), businessView.get("scheduleId"), payload.get("schedule_id"), payload.get("scheduleId")))
.jobId(firstString(businessView.get("job_id"), businessView.get("jobId"), payload.get("job_id"), payload.get("jobId")))
.revisionId(firstString(businessView.get("revision_id"), businessView.get("revisionId"), payload.get("revision_id"), payload.get("revisionId")))
.occurredAt(businessRow.getOccurredAt())
.syncedAt(record.getSyncedAt())
.clientKeyMasked(maskClientKey(record.getClientKey()))
@@ -1035,6 +1040,8 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
case "skill_audits" -> isSkillAuditRow(row);
case "policy_center" -> isPolicyCenterRow(row);
case "scheduler_operations" -> isSchedulerOperationsRow(row);
case "plan_governance" -> isPlanGovernanceRow(row);
case "plan_revisions" -> isPlanRevisionRow(row);
default -> true;
};
}
@@ -1166,6 +1173,26 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
|| StringUtils.isNotBlank(row.getJobId());
}
private boolean isPlanGovernanceRow(DigitalEmployeeAdminWorkbenchRowDto row) {
String category = firstString(row.getCategory());
String kind = firstString(row.getKind());
return StringUtils.equals(category, "plan_governance")
|| StringUtils.equals(category, "governance")
|| StringUtils.equals(category, "route_governance")
|| StringUtils.equals(category, "manual_intervention")
|| startsWithAny(kind, "governance_", "plan_action_", "escalation_", "manual_intervention_")
|| contains(kind, "route_governance");
}
private boolean isPlanRevisionRow(DigitalEmployeeAdminWorkbenchRowDto row) {
String category = firstString(row.getCategory());
String kind = firstString(row.getKind());
return StringUtils.equals(category, "plan_revision")
|| StringUtils.equals(category, "plan_revisions")
|| startsWithAny(kind, "plan_revision", "governance_revision")
|| StringUtils.isNotBlank(row.getRevisionId());
}
private String adminCategory(DigitalEmployeeBusinessViewRowDto row, Map<String, Object> businessView, Map<String, Object> payload) {
if (booleanValue(businessView.get("approval_conflict_active"))) return "approval_conflict";
String kind = firstString(row.getKind(), businessView.get("kind"), payload.get("kind"));
@@ -1187,6 +1214,8 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
if (startsWithAny(kind, "skill_call_")) return "skill_call_audit";
if (contains(kind, "policy")) return "policy_snapshot";
if (contains(kind, "scheduler") || contains(kind, "cron") || contains(kind, "lease")) return "scheduler_operation";
if (startsWithAny(kind, "plan_revision", "governance_revision")) return "plan_revision";
if (startsWithAny(kind, "plan_action_", "escalation_", "manual_intervention_")) return "plan_governance";
if (startsWithAny(kind, "governance_")) return "governance";
if (contains(kind, "audit")) return kind;
return row.getEntityType();

View File

@@ -567,6 +567,115 @@ class DigitalEmployeeSyncApplicationServiceImplTest {
assertThat(result.getRecords().get(1).getKind()).isEqualTo("scheduler_check_completed");
}
@Test
void queryAdminWorkbenchPlanGovernanceReturnsGovernanceEscalationRowsOnly() {
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-plan-action", "event", "event-plan-1", 101L, new Date(10_000),
Map.of(
"entity_type", "event",
"category", "plan_governance",
"kind", "plan_action_recorded",
"status", "recorded",
"title", "计划治理动作",
"plan_id", "plan-1",
"reason", "record_escalation"
),
Map.of("id", "event-plan-1", "kind", "plan_action_recorded")),
record("outbox-governance", "event", "event-plan-2", 101L, new Date(11_000),
Map.of(
"entity_type", "event",
"category", "governance",
"kind", "governance_pause_plan",
"status", "accepted",
"title", "暂停计划",
"plan_id", "plan-1",
"risk_level", "low"
),
Map.of("id", "event-plan-2", "kind", "governance_pause_plan")),
record("outbox-escalation", "event", "event-plan-3", 101L, new Date(12_000),
Map.of(
"entity_type", "event",
"kind", "escalation_requested",
"status", "pending",
"title", "升级人工复核",
"plan_id", "plan-2",
"priority", "high"
),
Map.of("id", "event-plan-3", "kind", "escalation_requested")),
record("outbox-plan", "plan", "plan-ordinary", 101L, new Date(12_500),
Map.of(
"entity_type", "plan",
"title", "普通计划",
"status", "running",
"plan_id", "plan-ordinary"
),
Map.of("id", "plan-ordinary", "status", "running")),
record("outbox-notification", "notification", "notification-1", 101L, new Date(13_000),
Map.of("entity_type", "notification", "title", "通知", "status", "unread"),
Map.of("id", "notification-1", "status", "unread"))
)));
DigitalEmployeeAdminWorkbenchPageDto result = service.queryAdminWorkbench(
"plan_governance", null, null, null, null, null, null, null, null, null, null, null, 1L, 20L
);
assertThat(result.getTotal()).isEqualTo(3);
assertThat(result.getRecords()).extracting(DigitalEmployeeAdminWorkbenchRowDto::getEntityId)
.containsExactly("event-plan-1", "event-plan-2", "event-plan-3");
assertThat(result.getRecords().get(0).getView()).isEqualTo("plan_governance");
assertThat(result.getRecords().get(0).getPlanId()).isEqualTo("plan-1");
assertThat(result.getRecords().get(1).getCategory()).isEqualTo("governance");
assertThat(result.getRecords().get(2).getPriority()).isEqualTo("high");
}
@Test
void queryAdminWorkbenchPlanRevisionsReturnsRevisionRowsOnly() {
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-plan-revision", "event", "event-revision-1", 101L, new Date(10_000),
Map.of(
"entity_type", "event",
"category", "plan_revision",
"kind", "plan_revision_created",
"status", "draft",
"title", "计划版本草稿",
"plan_id", "plan-1",
"revision_id", "rev-1",
"revision_status", "draft",
"base_revision_id", "rev-0",
"changed_fields", List.of("objective", "steps"),
"created_by", "admin"
),
Map.of("id", "event-revision-1", "kind", "plan_revision_created")),
record("outbox-governance-revision", "event", "event-revision-2", 101L, new Date(11_000),
Map.of(
"entity_type", "event",
"kind", "governance_revision_review_requested",
"status", "pending",
"title", "版本复核请求",
"plan_id", "plan-1",
"revision_id", "rev-2"
),
Map.of("id", "event-revision-2", "kind", "governance_revision_review_requested")),
record("outbox-task", "task", "task-1", 101L, new Date(12_000),
Map.of("entity_type", "task", "title", "普通任务", "status", "running"),
Map.of("id", "task-1", "status", "running"))
)));
DigitalEmployeeAdminWorkbenchPageDto result = service.queryAdminWorkbench(
"plan_revisions", null, null, null, null, null, null, null, null, null, null, null, 1L, 20L
);
assertThat(result.getTotal()).isEqualTo(2);
assertThat(result.getRecords()).extracting(DigitalEmployeeAdminWorkbenchRowDto::getEntityId)
.containsExactly("event-revision-1", "event-revision-2");
assertThat(result.getRecords().get(0).getView()).isEqualTo("plan_revisions");
assertThat(result.getRecords().get(0).getRevisionId()).isEqualTo("rev-1");
assertThat(result.getRecords().get(0).getBusinessView()).containsEntry("base_revision_id", "rev-0");
assertThat(result.getRecords().get(1).getRevisionId()).isEqualTo("rev-2");
}
@Test
void queryAdminWorkbenchServiceOperationsReturnsServiceHealthAndRestartRowsOnly() {
when(repository.queryBusinessRecords(eq(101L), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(1L), eq(20L)))
@@ -1137,11 +1246,19 @@ class DigitalEmployeeSyncApplicationServiceImplTest {
DigitalEmployeeAdminActionResultDto startScheduler = service.createAdminAction(
adminActionRequest("scheduler", "job-daily", "start_scheduler")
);
DigitalEmployeeAdminActionResultDto hardInterrupt = service.createAdminAction(
adminActionRequest("plan", "plan-1", "hard_interrupt")
);
DigitalEmployeeAdminActionResultDto activatePlan = service.createAdminAction(
adminActionRequest("plan", "plan-1", "activate_plan")
);
assertThat(editPolicy.getOk()).isFalse();
assertThat(editPolicy.getReasonCodes()).contains("edit_policy_remote_disabled");
assertThat(runSchedule.getReasonCodes()).contains("run_schedule_now_disabled");
assertThat(startScheduler.getReasonCodes()).contains("start_scheduler_disabled");
assertThat(hardInterrupt.getReasonCodes()).contains("hard_interrupt_disabled");
assertThat(activatePlan.getReasonCodes()).contains("high_risk_governance_action_disabled");
}
@Test

View File

@@ -51,6 +51,8 @@ const WORKBENCH_TABS: Array<{
{ key: 'skill_audits', label: '技能审计' },
{ key: 'policy_center', label: '策略中心' },
{ key: 'scheduler_operations', label: '调度运营' },
{ key: 'plan_governance', label: '计划治理' },
{ key: 'plan_revisions', label: '计划版本' },
];
const ENTITY_TYPE_OPTIONS = {
@@ -116,6 +118,7 @@ function renderSummary(record: DigitalEmployeeAdminWorkbenchRow) {
record.tool_name && `Tool ${record.tool_name}`,
record.schedule_id && `Schedule ${record.schedule_id}`,
record.job_id && `Job ${record.job_id}`,
record.revision_id && `Revision ${record.revision_id}`,
].filter((item): item is string => Boolean(item));
return (
@@ -199,6 +202,12 @@ function actionTarget(record: DigitalEmployeeAdminWorkbenchRow, view: DigitalEmp
if (view === 'scheduler_operations') {
return { entityType: 'scheduler', entityId: record.job_id || record.schedule_id || record.entity_id };
}
if (view === 'plan_governance') {
return { entityType: 'plan', entityId: record.plan_id || record.entity_id };
}
if (view === 'plan_revisions') {
return { entityType: 'revision', entityId: record.revision_id || record.entity_id };
}
return { entityType: record.entity_type, entityId: record.entity_id };
}
@@ -300,6 +309,21 @@ function allowedActions(record: DigitalEmployeeAdminWorkbenchRow, view: DigitalE
{ action: 'dismiss', label: '忽略' },
];
}
if (view === 'plan_governance') {
return [
{ action: 'mark_reviewed', label: '标记已看' },
{ action: 'dismiss', label: '忽略' },
{ action: 'request_revision_review', label: '请求版本复核' },
{ action: 'record_escalation', label: '记录升级' },
];
}
if (view === 'plan_revisions') {
return [
{ action: 'mark_reviewed', label: '标记已看' },
{ action: 'dismiss', label: '忽略' },
{ action: 'request_revision_review', label: '请求版本复核' },
];
}
return [];
}
@@ -770,6 +794,7 @@ const DigitalEmployeeOps: React.FC = () => {
detailRecord.server_id && `Server ${detailRecord.server_id}`,
detailRecord.schedule_id && `Schedule ${detailRecord.schedule_id}`,
detailRecord.job_id && `Job ${detailRecord.job_id}`,
detailRecord.revision_id && `Revision ${detailRecord.revision_id}`,
]
.filter(Boolean)
.join(' / ') || '--'}
@@ -869,6 +894,28 @@ const DigitalEmployeeOps: React.FC = () => {
</Typography.Text>
</Typography.Paragraph>
)}
{activeView === 'plan_governance' && (
<Typography.Paragraph>
<Typography.Text strong></Typography.Text>
{detailRecord.plan_id || detailRecord.entity_id || '--'}
{detailRecord.status && ` · ${detailRecord.status}`}
<Typography.Text type="secondary">
{' '}
· hard interrupt
</Typography.Text>
</Typography.Paragraph>
)}
{activeView === 'plan_revisions' && (
<Typography.Paragraph>
<Typography.Text strong></Typography.Text>
{detailRecord.revision_id || detailRecord.plan_id || detailRecord.entity_id || '--'}
{detailRecord.status && ` · ${detailRecord.status}`}
<Typography.Text type="secondary">
{' '}
·
</Typography.Text>
</Typography.Paragraph>
)}
{allowedActions(detailRecord, activeView).length > 0 && (
<div style={{ marginBottom: 16 }}>
<Typography.Title level={5}></Typography.Title>

View File

@@ -571,7 +571,9 @@ export type DigitalEmployeeAdminWorkbenchView =
| 'memory_governance'
| 'skill_audits'
| 'policy_center'
| 'scheduler_operations';
| 'scheduler_operations'
| 'plan_governance'
| 'plan_revisions';
export interface DigitalEmployeeAdminWorkbenchRow {
view: DigitalEmployeeAdminWorkbenchView | string;
@@ -605,6 +607,7 @@ export interface DigitalEmployeeAdminWorkbenchRow {
policy_key?: string;
schedule_id?: string;
job_id?: string;
revision_id?: string;
conflict_keys?: string[];
recommended_resolution?: string;
occurred_at?: string;

View File

@@ -1643,6 +1643,100 @@ function checkDigitalAdminPolicySchedulerWorkbenches() {
console.log("[DigitalEmployeeCheck] admin policy center and scheduler operation hooks OK");
}
function checkDigitalAdminPlanGovernanceWorkbenches() {
console.log("\n[DigitalEmployeeCheck] Verify admin plan governance and revision 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 dtoPath = path.join(
backendRoot,
"app-platform-modules",
"app-platform-agent",
"app-platform-agent-core-adapter",
"src",
"main",
"java",
"com",
"xspaceagi",
"agent",
"core",
"adapter",
"dto",
"digital",
"DigitalEmployeeAdminWorkbenchRowDto.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 dto = fs.readFileSync(dtoPath, "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, "plan_governance", "backend plan governance view"],
[service, "plan_revisions", "backend plan revisions view"],
[service, "isPlanGovernanceRow", "backend plan governance matcher"],
[service, "isPlanRevisionRow", "backend plan revision matcher"],
[service, "record_escalation", "backend plan escalation action"],
[service, "hard_interrupt_disabled", "backend hard interrupt disabled reason"],
[service, "high_risk_governance_action_disabled", "backend high risk governance disabled reason"],
[dto, "revision_id", "backend revision id row field"],
[test, "queryAdminWorkbenchPlanGovernanceReturnsGovernanceEscalationRowsOnly", "backend plan governance test"],
[test, "queryAdminWorkbenchPlanRevisionsReturnsRevisionRowsOnly", "backend plan revisions test"],
[qimingTypes, "plan_governance", "qiming plan governance view type"],
[qimingTypes, "plan_revisions", "qiming plan revisions view type"],
[qimingTypes, "revision_id", "qiming revision id field"],
[qimingOps, "计划治理", "qiming plan governance tab"],
[qimingOps, "计划版本", "qiming plan revisions tab"],
[qimingOps, "请求版本复核", "qiming revision review action"],
[qimingOps, "记录升级", "qiming escalation action"],
[qimingOps, "不开放 hard interrupt", "qiming hard interrupt guard copy"],
[qimingOps, "不自动激活计划", "qiming high risk execution guard copy"],
[docs, "正式管理端治理执行安全壳预览", "docs plan governance absorption"],
[docs, "正式管理端计划版本预览", "docs plan revision absorption"],
];
const missing = requiredSnippets
.filter(([content, snippet]) => !content.includes(snippet))
.map(([, , label]) => label);
if (missing.length > 0) {
throw new Error(`Missing admin plan governance and revision hooks: ${missing.join(", ")}`);
}
console.log("[DigitalEmployeeCheck] admin plan governance and revision hooks OK");
}
function checkDigitalAdminServiceOperationsWorkbench() {
console.log("\n[DigitalEmployeeCheck] Verify admin service operations workbench hooks");
const backendRoot = path.resolve(packageRoot, "..", "..", "..", "qiming-backend");
@@ -2249,6 +2343,7 @@ function main() {
checkDigitalAdminApprovalGovernanceWorkbench();
checkDigitalAdminMemorySkillWorkbench();
checkDigitalAdminPolicySchedulerWorkbenches();
checkDigitalAdminPlanGovernanceWorkbenches();
checkDigitalAdminServiceOperationsWorkbench();
checkDigitalAdminRouteGovernanceWorkbench();
checkDigitalAdminNotificationWorkbench();

View File

@@ -568,9 +568,9 @@ GET /api/digital-employee/approvals
4. **治理命令完整生命周期**
- 已吸收:`create_plan``submit_plan``approve_plan``activate_plan` 等基础命令可记录本地运行事实;任务级 `retry_task``skip_task``pause_task``resume_task``provide_task_input``cancel_task``start_run``cancel_run` 已具备本地事实闭环embedded plan / task action endpoint 已开始映射为正式 governance command治理审计链和低风险 action policy 已开始吸收route decision 自动映射 governance command 前会先经过低风险/受保护高风险 action guard未命中的动作写入 `route_action_policy_blocked`,所有 `governance_*` event 的同步 `business_view` 会提炼 command、route、实体目标、风险等级和结果字段`provide_task_input` 只同步 `input_length`
- 当前能力:`/api/plan/:id/submit``/api/debug/plan/:id/:action``/api/task/:id/:action` 会进入 qimingclaw 本地治理记录plan revision v1 可从 `governance_*` 事件投影只读时间线;数字员工首页会展示“治理审计/治理拦截”摘要,管理端通用 sync record 可通过 `business_view.category = governance` 消费治理命令审计字段。正式管理端治理命令预览已开始吸收qiming-backend 的 `governance_commands` 运营台视图会聚合治理命令记录、拒绝和低风险输入审计qiming“数字员工运营台”的“治理命令”tab 只开放标记已看和忽略意图,不开放 ACP 硬中断或高风险执行。
- 缺口:`escalate`正式计划 revision 表、正式管理端治理工作台和对真实 ACP 执行流的硬中断仍不完整。
- 建议:下一轮围绕正式管理端治理工作台和 ACP 硬中断,把本地治理审计推进到完整跨端裁决与执行控制。
- 当前能力:`/api/plan/:id/submit``/api/debug/plan/:id/:action``/api/task/:id/:action` 会进入 qimingclaw 本地治理记录plan revision v1 可从 `governance_*` 事件投影只读时间线;数字员工首页会展示“治理审计/治理拦截”摘要,管理端通用 sync record 可通过 `business_view.category = governance` 消费治理命令审计字段。正式管理端治理命令预览已开始吸收qiming-backend 的 `governance_commands` 运营台视图会聚合治理命令记录、拒绝和低风险输入审计qiming“数字员工运营台”的“治理命令”tab 只开放标记已看和忽略意图,不开放 ACP 硬中断或高风险执行。正式管理端治理执行安全壳预览和正式管理端计划版本预览已开始吸收qiming-backend 新增 `plan_governance` / `plan_revisions` 视图聚合治理动作、升级请求、人工介入和版本复核摘要qiming“数字员工运营台”的“计划治理”“计划版本”tab 只记录标记已看、忽略、请求版本复核和记录升级等低风险意图;`hard_interrupt``activate_plan``cancel_run``cancel_task``accept_remote_approval_batch` 继续返回明确禁用 reason。
- 缺口:正式计划 revision 物理表、治理策略编辑 UI 和对真实 ACP 执行流的硬中断仍不完整。
- 建议:下一轮围绕正式管理端治理策略和 ACP 硬中断安全设计,把本地治理审计推进到完整跨端裁决与执行控制。
5. **ManagedService 控制面**
- 已吸收Agent、MCP、Computer Server、Lanproxy、File Server、GUI Server 状态已进入 snapshot / projectionFile Server 与 GUI Server 已开放白名单式 restart bridge并会写入 `managed_service_restart` / `managed_service_restart_rejected` 事件,安全服务 restart bridge 已开始闭环。完整人机介入入口已开始吸收embedded 新增“处置台”聚合服务异常、诊断风险、审计风险和策略拉取错误支持批量标记已看、忽略、策略重拉和白名单服务安全重启。正式外部管理端服务运营台预览已开始吸收qiming-backend 新增 `service_operations` 运营台视图聚合 managed service 快照、服务诊断和重启审计qiming“数字员工运营台”新增“服务运营” tab并只开放复核/忽略等低风险动作意图,不开放 start/stop。