From f08b0c39d9b6423fe9b385d2ba27e0f699bf9fb8 Mon Sep 17 00:00:00 2001 From: baiyanyun Date: Sat, 13 Jun 2026 14:42:16 +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=E7=AD=96=E7=95=A5=E8=B0=83?= =?UTF-8?q?=E5=BA=A6=E9=A2=84=E8=A7=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DigitalEmployeeAdminWorkbenchRowDto.java | 8 ++ ...talEmployeeSyncApplicationServiceImpl.java | 47 +++++++ ...mployeeSyncApplicationServiceImplTest.java | 124 ++++++++++++++++++ .../Content/DigitalEmployeeOps/index.tsx | 48 +++++++ qiming/src/types/interfaces/systemManage.ts | 6 +- .../scripts/check-digital-employee.js | 99 ++++++++++++++ ...ital-employee-frontend-integration-plan.md | 10 +- 7 files changed, 336 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/DigitalEmployeeAdminWorkbenchRowDto.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/DigitalEmployeeAdminWorkbenchRowDto.java index 38b68623..d27d8145 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/DigitalEmployeeAdminWorkbenchRowDto.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/DigitalEmployeeAdminWorkbenchRowDto.java @@ -55,6 +55,14 @@ public class DigitalEmployeeAdminWorkbenchRowDto { private String toolName; @JsonProperty("server_id") private String serverId; + @JsonProperty("policy_kind") + private String policyKind; + @JsonProperty("policy_key") + private String policyKey; + @JsonProperty("schedule_id") + private String scheduleId; + @JsonProperty("job_id") + private String jobId; @JsonProperty("occurred_at") private String occurredAt; @JsonProperty("synced_at") 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 d29e63ab..ac69a9b2 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 @@ -72,6 +72,8 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye "daily_report:mark_delivered", "daily_report:mark_confirmed", "daily_report:request_approval", "memory:archive_memory", "memory:restore_memory", "memory:mark_reviewed", "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", "audit:mark_reviewed", "audit:dismiss", "event:mark_reviewed", "event:dismiss" ); @@ -917,6 +919,10 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye if (StringUtils.isBlank(clientKey) || StringUtils.isBlank(entityType) || StringUtils.isBlank(entityId) || StringUtils.isBlank(action)) { return List.of("required_field_missing"); } + List disabledReasonCodes = disabledAdminActionReasonCodes(action); + if (!disabledReasonCodes.isEmpty()) { + return disabledReasonCodes; + } if (HIGH_RISK_ADMIN_ACTIONS.contains(action)) { return List.of("high_risk_action_disabled"); } @@ -926,6 +932,15 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye return List.of(); } + private List disabledAdminActionReasonCodes(String action) { + return switch (StringUtils.trimToEmpty(action)) { + 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"); + default -> List.of(); + }; + } + private DigitalEmployeeAdminActionDto toAdminActionDto(DigitalEmployeeAdminAction action) { if (action == null) return null; return DigitalEmployeeAdminActionDto.builder() @@ -987,6 +1002,10 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye .skillId(firstString(businessView.get("skill_id"), businessView.get("skillId"), payload.get("skill_id"), payload.get("skillId"))) .toolName(firstString(businessView.get("tool_name"), businessView.get("toolName"), payload.get("tool_name"), payload.get("toolName"))) .serverId(firstString(businessView.get("server_id"), businessView.get("serverId"), payload.get("server_id"), payload.get("serverId"))) + .policyKind(firstString(businessView.get("policy_kind"), businessView.get("policyKind"), payload.get("policy_kind"), payload.get("policyKind"))) + .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"))) .occurredAt(businessRow.getOccurredAt()) .syncedAt(record.getSyncedAt()) .clientKeyMasked(maskClientKey(record.getClientKey())) @@ -1014,6 +1033,8 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye case "route_governance" -> isRouteGovernanceRow(row); case "memory_governance" -> isMemoryGovernanceRow(row); case "skill_audits" -> isSkillAuditRow(row); + case "policy_center" -> isPolicyCenterRow(row); + case "scheduler_operations" -> isSchedulerOperationsRow(row); default -> true; }; } @@ -1121,6 +1142,30 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye || StringUtils.isNotBlank(row.getServerId()); } + private boolean isPolicyCenterRow(DigitalEmployeeAdminWorkbenchRowDto row) { + String category = firstString(row.getCategory()); + String kind = firstString(row.getKind()); + return StringUtils.equals(category, "policy_snapshot") + || StringUtils.equals(category, "policy_center") + || startsWithAny(kind, "policy_", "skill_policy_", "risk_approval_policy", "route_decision_policy", "plan_step_dispatch_policy") + || StringUtils.isNotBlank(row.getPolicyKind()) + || StringUtils.isNotBlank(row.getPolicyKey()); + } + + private boolean isSchedulerOperationsRow(DigitalEmployeeAdminWorkbenchRowDto row) { + String category = firstString(row.getCategory()); + String kind = firstString(row.getKind()); + return StringUtils.equals(category, "scheduler") + || StringUtils.equals(category, "scheduler_operation") + || StringUtils.equals(category, "cron") + || contains(kind, "scheduler") + || contains(kind, "cron") + || contains(kind, "lease") + || contains(kind, "plan_step_dispatch") + || StringUtils.isNotBlank(row.getScheduleId()) + || StringUtils.isNotBlank(row.getJobId()); + } + private String adminCategory(DigitalEmployeeBusinessViewRowDto row, Map businessView, Map payload) { if (booleanValue(businessView.get("approval_conflict_active"))) return "approval_conflict"; String kind = firstString(row.getKind(), businessView.get("kind"), payload.get("kind")); @@ -1140,6 +1185,8 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye if (startsWithAny(kind, "daily_report")) return "daily_report"; if (startsWithAny(kind, "memory_")) return "memory_governance"; 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, "governance_")) return "governance"; if (contains(kind, "audit")) return kind; return row.getEntityType(); 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 2afcf454..69792212 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 @@ -461,6 +461,112 @@ class DigitalEmployeeSyncApplicationServiceImplTest { assertThat(result.getRecords().get(2).getBusinessView()).containsEntry("total_tokens", 180); } + @Test + void queryAdminWorkbenchPolicyCenterReturnsPolicySnapshotRowsOnly() { + 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-route-policy", "event", "event-policy-1", 101L, new Date(10_000), + Map.of( + "entity_type", "event", + "category", "policy_snapshot", + "kind", "route_decision_policy_snapshot", + "status", "synced", + "title", "入口路由策略快照", + "policy_kind", "route_decision", + "policy_key", "route_decision", + "policy_version", "v2", + "source", "remote", + "enabled", true, + "auto_create_governance_commands", false, + "recommended_resolution", "review_latest_policy_snapshot" + ), + Map.of("id", "event-policy-1", "kind", "route_decision_policy_snapshot")), + record("outbox-dispatch-policy", "event", "event-policy-2", 101L, new Date(11_000), + Map.of( + "entity_type", "event", + "kind", "plan_step_dispatch_policy_pulled", + "status", "synced", + "title", "派发策略已拉取", + "policy_kind", "dispatch", + "policy_key", "plan_step_dispatch", + "max_per_sweep", 3 + ), + Map.of("id", "event-policy-2", "kind", "plan_step_dispatch_policy_pulled")), + record("outbox-scheduler", "event", "event-scheduler-1", 101L, new Date(12_000), + Map.of("entity_type", "event", "category", "scheduler", "title", "调度", "status", "ready", "job_id", "job-1"), + Map.of("id", "event-scheduler-1", "status", "ready")), + record("outbox-task", "task", "task-1", 101L, new Date(13_000), + Map.of("entity_type", "task", "title", "普通任务", "status", "running"), + Map.of("id", "task-1", "status", "running")) + ))); + + DigitalEmployeeAdminWorkbenchPageDto result = service.queryAdminWorkbench( + "policy_center", 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-policy-1", "event-policy-2"); + assertThat(result.getRecords().get(0).getView()).isEqualTo("policy_center"); + assertThat(result.getRecords().get(0).getPolicyKind()).isEqualTo("route_decision"); + assertThat(result.getRecords().get(0).getPolicyKey()).isEqualTo("route_decision"); + assertThat(result.getRecords().get(0).getBusinessView()).containsEntry("auto_create_governance_commands", false); + assertThat(result.getRecords().get(1).getPolicyKind()).isEqualTo("dispatch"); + } + + @Test + void queryAdminWorkbenchSchedulerOperationsReturnsSchedulerRowsOnly() { + 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-scheduler-job", "event", "event-scheduler-1", 101L, new Date(10_000), + Map.of( + "entity_type", "event", + "category", "scheduler", + "kind", "scheduler_job_snapshot", + "status", "enabled", + "title", "日报调度", + "schedule_id", "schedule-1", + "job_id", "job-daily", + "cron", "0 9 * * *", + "next_run_at", "2026-06-14T09:00:00+08:00", + "last_run_at", "2026-06-13T09:00:00+08:00", + "last_status", "completed", + "catch_up_on_startup", true, + "max_run_history", 20 + ), + Map.of("id", "event-scheduler-1", "kind", "scheduler_job_snapshot")), + record("outbox-scheduler-check", "event", "event-scheduler-2", 101L, new Date(11_000), + Map.of( + "entity_type", "event", + "kind", "scheduler_check_completed", + "status", "completed", + "title", "立即检查调度", + "schedule_id", "schedule-1", + "job_id", "job-daily" + ), + Map.of("id", "event-scheduler-2", "kind", "scheduler_check_completed")), + record("outbox-policy", "event", "event-policy-1", 101L, new Date(12_000), + Map.of("entity_type", "event", "category", "policy_snapshot", "title", "策略", "status", "synced", "policy_kind", "route_decision"), + Map.of("id", "event-policy-1", "status", "synced")), + record("outbox-approval", "approval", "approval-1", 101L, new Date(13_000), + Map.of("entity_type", "approval", "title", "普通审批", "status", "pending"), + Map.of("id", "approval-1", "status", "pending")) + ))); + + DigitalEmployeeAdminWorkbenchPageDto result = service.queryAdminWorkbench( + "scheduler_operations", 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-scheduler-1", "event-scheduler-2"); + assertThat(result.getRecords().get(0).getView()).isEqualTo("scheduler_operations"); + assertThat(result.getRecords().get(0).getScheduleId()).isEqualTo("schedule-1"); + assertThat(result.getRecords().get(0).getJobId()).isEqualTo("job-daily"); + assertThat(result.getRecords().get(0).getBusinessView()).containsEntry("catch_up_on_startup", true); + assertThat(result.getRecords().get(1).getKind()).isEqualTo("scheduler_check_completed"); + } + @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))) @@ -1020,6 +1126,24 @@ class DigitalEmployeeSyncApplicationServiceImplTest { assertThat(captor.getValue().getStatus()).isEqualTo("rejected"); } + @Test + void createAdminActionReturnsSpecificPolicyAndSchedulerDisabledReasons() { + DigitalEmployeeAdminActionResultDto editPolicy = service.createAdminAction( + adminActionRequest("policy_snapshot", "route_decision", "edit_policy_remote") + ); + DigitalEmployeeAdminActionResultDto runSchedule = service.createAdminAction( + adminActionRequest("scheduler", "job-daily", "run_schedule_now") + ); + DigitalEmployeeAdminActionResultDto startScheduler = service.createAdminAction( + adminActionRequest("scheduler", "job-daily", "start_scheduler") + ); + + 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"); + } + @Test void queryPendingAdminActionsFiltersTenantClientDeviceAndMasksKey() { when(adminActionRepository.queryActions(eq(101L), eq("client-key-001"), eq("device-001"), eq("pending"), 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 fdb68e10..fff28965 100644 --- a/qiming/src/pages/SystemManagement/Content/DigitalEmployeeOps/index.tsx +++ b/qiming/src/pages/SystemManagement/Content/DigitalEmployeeOps/index.tsx @@ -49,6 +49,8 @@ const WORKBENCH_TABS: Array<{ { key: 'policy_conflicts', label: '策略冲突' }, { key: 'memory_governance', label: '记忆治理' }, { key: 'skill_audits', label: '技能审计' }, + { key: 'policy_center', label: '策略中心' }, + { key: 'scheduler_operations', label: '调度运营' }, ]; const ENTITY_TYPE_OPTIONS = { @@ -112,6 +114,8 @@ function renderSummary(record: DigitalEmployeeAdminWorkbenchRow) { record.memory_id && `Memory ${record.memory_id}`, record.skill_id && `Skill ${record.skill_id}`, record.tool_name && `Tool ${record.tool_name}`, + record.schedule_id && `Schedule ${record.schedule_id}`, + record.job_id && `Job ${record.job_id}`, ].filter((item): item is string => Boolean(item)); return ( @@ -189,6 +193,12 @@ function actionTarget(record: DigitalEmployeeAdminWorkbenchRow, view: DigitalEmp if (view === 'skill_audits') { return { entityType: 'skill', entityId: record.skill_id || record.tool_name || record.entity_id }; } + if (view === 'policy_center') { + return { entityType: 'policy_snapshot', entityId: record.policy_key || record.policy_kind || record.entity_id }; + } + if (view === 'scheduler_operations') { + return { entityType: 'scheduler', entityId: record.job_id || record.schedule_id || record.entity_id }; + } return { entityType: record.entity_type, entityId: record.entity_id }; } @@ -276,6 +286,20 @@ function allowedActions(record: DigitalEmployeeAdminWorkbenchRow, view: DigitalE { action: 'dismiss', label: '忽略' }, ]; } + if (view === 'policy_center') { + return [ + { action: 'pull_policy', label: '批量拉取策略' }, + { action: 'mark_reviewed', label: '标记已看' }, + { action: 'dismiss', label: '忽略' }, + ]; + } + if (view === 'scheduler_operations') { + return [ + { action: 'retry_scheduler_check', label: '立即检查调度' }, + { action: 'mark_reviewed', label: '标记已看' }, + { action: 'dismiss', label: '忽略' }, + ]; + } return []; } @@ -744,6 +768,8 @@ const DigitalEmployeeOps: React.FC = () => { detailRecord.skill_id && `Skill ${detailRecord.skill_id}`, detailRecord.tool_name && `Tool ${detailRecord.tool_name}`, detailRecord.server_id && `Server ${detailRecord.server_id}`, + detailRecord.schedule_id && `Schedule ${detailRecord.schedule_id}`, + detailRecord.job_id && `Job ${detailRecord.job_id}`, ] .filter(Boolean) .join(' / ') || '--'} @@ -821,6 +847,28 @@ const DigitalEmployeeOps: React.FC = () => { )} + {activeView === 'policy_center' && ( + + 策略中心: + {detailRecord.policy_kind || detailRecord.policy_key || detailRecord.entity_id || '--'} + {detailRecord.status && ` · ${detailRecord.status}`} + + {' '} + · 支持批量拉取策略和复核标记,不开放远程策略编辑。 + + + )} + {activeView === 'scheduler_operations' && ( + + 调度运营: + {detailRecord.job_id || detailRecord.schedule_id || detailRecord.entity_id || '--'} + {detailRecord.status && ` · ${detailRecord.status}`} + + {' '} + · 支持立即检查调度和复核标记,不开放 start/stop 或高风险立即运行。 + + + )} {allowedActions(detailRecord, activeView).length > 0 && (
低风险动作 diff --git a/qiming/src/types/interfaces/systemManage.ts b/qiming/src/types/interfaces/systemManage.ts index 66ba6a0a..1abf1267 100644 --- a/qiming/src/types/interfaces/systemManage.ts +++ b/qiming/src/types/interfaces/systemManage.ts @@ -569,7 +569,9 @@ export type DigitalEmployeeAdminWorkbenchView = | 'route_governance' | 'policy_conflicts' | 'memory_governance' - | 'skill_audits'; + | 'skill_audits' + | 'policy_center' + | 'scheduler_operations'; export interface DigitalEmployeeAdminWorkbenchRow { view: DigitalEmployeeAdminWorkbenchView | string; @@ -601,6 +603,8 @@ export interface DigitalEmployeeAdminWorkbenchRow { server_id?: string; policy_kind?: string; policy_key?: string; + schedule_id?: string; + job_id?: string; conflict_keys?: string[]; recommended_resolution?: string; occurred_at?: 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 d059b5f0..7a1c2345 100644 --- a/qimingclaw/crates/agent-electron-client/scripts/check-digital-employee.js +++ b/qimingclaw/crates/agent-electron-client/scripts/check-digital-employee.js @@ -1545,6 +1545,104 @@ function checkDigitalAdminMemorySkillWorkbench() { console.log("[DigitalEmployeeCheck] admin memory governance and skill audit hooks OK"); } +function checkDigitalAdminPolicySchedulerWorkbenches() { + console.log("\n[DigitalEmployeeCheck] Verify admin policy center and scheduler operation 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, "policy_center", "backend policy center view"], + [service, "scheduler_operations", "backend scheduler operations view"], + [service, "isPolicyCenterRow", "backend policy center matcher"], + [service, "isSchedulerOperationsRow", "backend scheduler operations matcher"], + [service, "edit_policy_remote_disabled", "backend policy edit disabled reason"], + [service, "run_schedule_now_disabled", "backend scheduler run disabled reason"], + [service, "start_scheduler_disabled", "backend scheduler start disabled reason"], + [dto, "policy_kind", "backend policy kind row field"], + [dto, "policy_key", "backend policy key row field"], + [dto, "schedule_id", "backend schedule id row field"], + [dto, "job_id", "backend job id row field"], + [test, "queryAdminWorkbenchPolicyCenterReturnsPolicySnapshotRowsOnly", "backend policy center test"], + [test, "queryAdminWorkbenchSchedulerOperationsReturnsSchedulerRowsOnly", "backend scheduler operations test"], + [test, "createAdminActionReturnsSpecificPolicyAndSchedulerDisabledReasons", "backend disabled reason test"], + [qimingTypes, "policy_center", "qiming policy center view type"], + [qimingTypes, "scheduler_operations", "qiming scheduler operations view type"], + [qimingTypes, "schedule_id", "qiming schedule id field"], + [qimingOps, "策略中心", "qiming policy center tab"], + [qimingOps, "调度运营", "qiming scheduler operations tab"], + [qimingOps, "批量拉取策略", "qiming policy pull action"], + [qimingOps, "立即检查调度", "qiming scheduler check action"], + [qimingOps, "不开放远程策略编辑", "qiming policy edit guard copy"], + [qimingOps, "不开放 start/stop", "qiming scheduler start stop guard copy"], + [docs, "正式管理端策略中心预览", "docs policy center absorption"], + [docs, "正式管理端调度运营预览", "docs scheduler operations absorption"], + ]; + const missing = requiredSnippets + .filter(([content, snippet]) => !content.includes(snippet)) + .map(([, , label]) => label); + if (missing.length > 0) { + throw new Error(`Missing admin policy center and scheduler operation hooks: ${missing.join(", ")}`); + } + console.log("[DigitalEmployeeCheck] admin policy center and scheduler operation hooks OK"); +} + function checkDigitalAdminServiceOperationsWorkbench() { console.log("\n[DigitalEmployeeCheck] Verify admin service operations workbench hooks"); const backendRoot = path.resolve(packageRoot, "..", "..", "..", "qiming-backend"); @@ -2150,6 +2248,7 @@ function main() { checkDigitalAdminRecordBillingArchiveShell(); checkDigitalAdminApprovalGovernanceWorkbench(); checkDigitalAdminMemorySkillWorkbench(); + checkDigitalAdminPolicySchedulerWorkbenches(); checkDigitalAdminServiceOperationsWorkbench(); checkDigitalAdminRouteGovernanceWorkbench(); checkDigitalAdminNotificationWorkbench(); diff --git a/qimingclaw/docs/digital-employee-frontend-integration-plan.md b/qimingclaw/docs/digital-employee-frontend-integration-plan.md index 270d91ba..f058cbd2 100644 --- a/qimingclaw/docs/digital-employee-frontend-integration-plan.md +++ b/qimingclaw/docs/digital-employee-frontend-integration-plan.md @@ -550,13 +550,13 @@ GET /api/digital-employee/approvals 1. **PlanStep 与依赖图(依赖图调度已开始)** - 已吸收:Plan / Task / Run 记录、计划模板、`create_plan` 中的 steps/tasks 可物化为任务;PlanStep 已开始作为正式本地实体落入 `digital_plan_steps`。 - - 当前能力:PlanStep 会进入 `digital_sync_outbox`,entity type 为 `plan_step`;任务中心 flow 和 debug store 优先读取真实 PlanStep 生成步骤图;Task Agent 的 retry / skip / cancel / pause / resume / input / run 控制已可更新真实 Task、PlanStep、Run、Event 和 outbox;前置步骤跳过、取消或重试后会重新评估后继步骤,推进为 ready / blocked / pending 并写入依赖图事件;scheduler sweep 会识别可派发的 ready PlanStep,派发前先通过管理端 `POST /api/digital-employee/leases/plan-step-dispatch/acquire` 做跨设备强租约仲裁,管理端拒绝会以 `remote_lease_rejected` 跳过,管理端异常会降级为 5 分钟本地软租约 `payload.dispatchLease.source = local_fallback` 并记录 `remote_error`;调用本地 `/computer/chat` 后会写入 `plan_step_dispatch_*` 与 `governance_start_run` 事件,派发完成或失败会先尝试 `release` 管理端租约再释放本地 lease 并写入 `plan_step_lease_*` 事件;已有未过期租约的步骤仍会以 `lease_active` 跳过;auto sweep 会按节流从管理端 `GET /api/digital-employee/policies/plan-step-dispatch` 拉取远端派发策略,拉取失败时保留本地策略并记录 `last_pull_error`;embedded 已提供 `/api/digital-employee/plan-steps`、`/api/digital-employee/plan-step-graph`、`/api/digital-employee/plans/:planId/dispatch-ready-steps`、`/api/plan-step/dispatch-policy` 和 `/api/plan-step/dispatch-policy/pull`,ready PlanStep 安全派发、计划级跨任务推进、管理端依赖图视图、本地/远端派发策略、可观察租约状态和首页阻塞依赖处理入口已开始闭环。派发策略管理 UI 已开始吸收,embedded 新增“策略中心”集中展示和编辑 `enabled/max_per_sweep/auto_dispatch_ready_steps`。 - - 后续缺口:正式外部管理端路由 UI、跨设备审批策略 UI 和跨设备策略冲突合并仍待吸收。 + - 当前能力:PlanStep 会进入 `digital_sync_outbox`,entity type 为 `plan_step`;任务中心 flow 和 debug store 优先读取真实 PlanStep 生成步骤图;Task Agent 的 retry / skip / cancel / pause / resume / input / run 控制已可更新真实 Task、PlanStep、Run、Event 和 outbox;前置步骤跳过、取消或重试后会重新评估后继步骤,推进为 ready / blocked / pending 并写入依赖图事件;scheduler sweep 会识别可派发的 ready PlanStep,派发前先通过管理端 `POST /api/digital-employee/leases/plan-step-dispatch/acquire` 做跨设备强租约仲裁,管理端拒绝会以 `remote_lease_rejected` 跳过,管理端异常会降级为 5 分钟本地软租约 `payload.dispatchLease.source = local_fallback` 并记录 `remote_error`;调用本地 `/computer/chat` 后会写入 `plan_step_dispatch_*` 与 `governance_start_run` 事件,派发完成或失败会先尝试 `release` 管理端租约再释放本地 lease 并写入 `plan_step_lease_*` 事件;已有未过期租约的步骤仍会以 `lease_active` 跳过;auto sweep 会按节流从管理端 `GET /api/digital-employee/policies/plan-step-dispatch` 拉取远端派发策略,拉取失败时保留本地策略并记录 `last_pull_error`;embedded 已提供 `/api/digital-employee/plan-steps`、`/api/digital-employee/plan-step-graph`、`/api/digital-employee/plans/:planId/dispatch-ready-steps`、`/api/plan-step/dispatch-policy` 和 `/api/plan-step/dispatch-policy/pull`,ready PlanStep 安全派发、计划级跨任务推进、管理端依赖图视图、本地/远端派发策略、可观察租约状态和首页阻塞依赖处理入口已开始闭环。派发策略管理 UI 已开始吸收,embedded 新增“策略中心”集中展示和编辑 `enabled/max_per_sweep/auto_dispatch_ready_steps`。正式管理端策略中心预览已开始吸收,qiming-backend 新增 `policy_center` 运营台视图聚合派发、风险审批、入口路由和技能策略快照,qiming“数字员工运营台”的“策略中心”tab 可记录批量拉取策略、标记已看和忽略意图,不开放远程策略编辑。 + - 后续缺口:跨设备审批策略 UI、跨设备策略冲突合并和远程策略编辑安全模型仍待吸收。 - 建议:下一轮围绕管理端正式路由 UI 或跨端审批策略 UI,把本地可治理调度继续推进到跨端治理策略。 2. **调度 / 定时 / 周期任务** - 已吸收:`digital_schedules`、`digital_schedule_runs` 已进入 qimingclaw 本地 SQLite 与 outbox,entity type 为 `schedule` / `schedule_run`;旧 `/api/cron`、`/api/scheduler/jobs` 和治理命令会优先读取真实本地调度记录。 - - 当前能力:主进程 `DigitalEmployeeScheduler` 可按本机 5 字段 cron 到点触发本地 `/computer/chat`,支持启动 catch-up、立即运行、暂停/恢复/删除、失败重试、运行历史和同步诊断。多调度并发策略 UI 已开始吸收,embedded 新增“调度运营”入口,可查看 scheduler jobs、最近运行、`catch_up_on_startup`、`max_run_history`,并触发一次立即检查调度。 + - 当前能力:主进程 `DigitalEmployeeScheduler` 可按本机 5 字段 cron 到点触发本地 `/computer/chat`,支持启动 catch-up、立即运行、暂停/恢复/删除、失败重试、运行历史和同步诊断。多调度并发策略 UI 已开始吸收,embedded 新增“调度运营”入口,可查看 scheduler jobs、最近运行、`catch_up_on_startup`、`max_run_history`,并触发一次立即检查调度。正式管理端调度运营预览已开始吸收,qiming-backend 新增 `scheduler_operations` 运营台视图聚合 scheduler job、cron settings、lease、dispatch sweep 和 recent run 摘要,qiming“数字员工运营台”的“调度运营”tab 只开放立即检查调度、标记已看和忽略意图,不开放 start/stop 或高风险立即运行。 - 后续缺口:更细的秒级 cron、复杂日历排除规则、正式外部管理端调度策略下发和跨设备调度冲突合并仍待吸收。 - 建议:下一轮围绕入口路由/任务分流,把 schedule 触发的计划与具体 Skill Policy、Task Agent 状态机更精确地绑定。 @@ -599,7 +599,7 @@ 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-backend 的 `route_governance` 运营台视图会聚合 route blocked / approval required / missing context / not mapped / governance audit,qiming“数字员工运营台”的“路由治理”tab 只开放标记已看、忽略和重试策略检查等低风险动作意图。 + - 当前能力:入口请求会按上下文、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 仍待吸收。 - 建议:下一轮围绕管理端正式路由 UI 和跨设备冲突可视化,把 embedded 批量干预入口推进到管理端运营台。 @@ -609,7 +609,7 @@ GET /api/digital-employee/approvals - 建议:下一轮围绕管理端正式审计视图和诊断修复动作,把本地可追溯审计继续推进到可运营处置。 12. **管理端业务查询模型** - - 已吸收:管理端已有通用 sync record 接收、查询、payload 摘要和深链;后端 sync record 已开始保存客户端顶层 `contract_version`、`entity_summary` 和 `business_view`,并基于现有 `digital_employee_sync_record` 拆出 `/api/digital-employee/plans`、`/plans/{plan_id}`、`/tasks`、`/runs`、`/events`、`/artifacts`、`/approvals` 管理端业务查询模型;列表支持客户端 Key、设备、实体 ID、状态、同步状态和同步时间范围过滤,业务 row 会提炼 title/status/summary、Plan/Task/Run 关联、发生/开始/结束时间、sync record 深链和脱敏客户端 Key;Plan 视图会聚合 task/run 计数与最近事件,Plan 详情会返回关联 tasks/runs/events/artifacts/approvals 摘要。embedded adapter 已拆出同名只读业务查询接口,从 qimingclaw runtime、artifact、approval 和 canonical event 投影生成统一分页视图;embedded 数字员工页已新增“业务视图”入口,支持计划/任务/运行/事件/产物/审批分段、实体/状态/同步状态/时间筛选、分页和详情 JSON 查看;客户端 outbox 的 event `business_view` 已开始按 route decision、governance command、approval action、artifact lifecycle/access、skill call audit 和 ready PlanStep dispatch 细分常用字段,本地与管理端业务视图投影已开始闭环。正式外部管理端台面已开始吸收:qiming-backend 新增 `/api/digital-employee/admin/workbench/{view}`,qiming 管理端新增“数字员工运营台”,统一展示业务视图、介入台、产物治理、通知运营、审计诊断、日报运营和路由治理,只读消费 sync record `business_view` 并提供同步记录深链。管理端低风险动作闭环已开始吸收:后端新增 `/api/digital-employee/admin/actions` 记录动作意图和 `/pending` 拉取队列,qimingclaw 只应用保留本地、标记复核、通知已读/忽略、产物保留/到期、日报交付/确认/审批请求等低风险动作并写入 `admin_action_applied` / `admin_action_rejected` / `admin_action_failed`,qiming 运营台抽屉只提示“已记录,等待客户端拉取”。跨设备策略冲突视图已开始吸收:qimingclaw 将派发、风险审批和入口路由策略同步事件投影为 `policy_snapshot` business view,后端新增 `/api/digital-employee/admin/policies/snapshots` 聚合 client/device 维度差异并输出 `conflict_keys`,qiming 运营台新增“策略冲突” tab 展示 policy drift 与 sync record 深链。业务事实物化查询已开始吸收:后端新增 `digital_employee_business_fact`、`digital_employee_artifact_lifecycle`、`digital_employee_daily_report_fact` 三张增量物化表,同步入库时 upsert 安全业务摘要,业务查询优先返回 `materialized: true` 行,旧记录继续回退 sync record。 + - 已吸收:管理端已有通用 sync record 接收、查询、payload 摘要和深链;后端 sync record 已开始保存客户端顶层 `contract_version`、`entity_summary` 和 `business_view`,并基于现有 `digital_employee_sync_record` 拆出 `/api/digital-employee/plans`、`/plans/{plan_id}`、`/tasks`、`/runs`、`/events`、`/artifacts`、`/approvals` 管理端业务查询模型;列表支持客户端 Key、设备、实体 ID、状态、同步状态和同步时间范围过滤,业务 row 会提炼 title/status/summary、Plan/Task/Run 关联、发生/开始/结束时间、sync record 深链和脱敏客户端 Key;Plan 视图会聚合 task/run 计数与最近事件,Plan 详情会返回关联 tasks/runs/events/artifacts/approvals 摘要。embedded adapter 已拆出同名只读业务查询接口,从 qimingclaw runtime、artifact、approval 和 canonical event 投影生成统一分页视图;embedded 数字员工页已新增“业务视图”入口,支持计划/任务/运行/事件/产物/审批分段、实体/状态/同步状态/时间筛选、分页和详情 JSON 查看;客户端 outbox 的 event `business_view` 已开始按 route decision、governance command、approval action、artifact lifecycle/access、skill call audit 和 ready PlanStep dispatch 细分常用字段,本地与管理端业务视图投影已开始闭环。正式外部管理端台面已开始吸收:qiming-backend 新增 `/api/digital-employee/admin/workbench/{view}`,qiming 管理端新增“数字员工运营台”,统一展示业务视图、介入台、产物治理、通知运营、审计诊断、日报运营和路由治理,只读消费 sync record `business_view` 并提供同步记录深链。管理端低风险动作闭环已开始吸收:后端新增 `/api/digital-employee/admin/actions` 记录动作意图和 `/pending` 拉取队列,qimingclaw 只应用保留本地、标记复核、通知已读/忽略、产物保留/到期、日报交付/确认/审批请求等低风险动作并写入 `admin_action_applied` / `admin_action_rejected` / `admin_action_failed`,qiming 运营台抽屉只提示“已记录,等待客户端拉取”。跨设备策略冲突视图已开始吸收:qimingclaw 将派发、风险审批和入口路由策略同步事件投影为 `policy_snapshot` business view,后端新增 `/api/digital-employee/admin/policies/snapshots` 聚合 client/device 维度差异并输出 `conflict_keys`,qiming 运营台新增“策略冲突” tab 展示 policy drift 与 sync record 深链。正式管理端策略中心预览和正式管理端调度运营预览已开始吸收:`admin/workbench` 新增 `policy_center` 与 `scheduler_operations` 视图,统一消费策略快照、scheduler job、cron settings、lease、dispatch sweep 和 recent run 摘要,并通过低风险 admin action 记录策略拉取、立即检查调度、复核和忽略意图。业务事实物化查询已开始吸收:后端新增 `digital_employee_business_fact`、`digital_employee_artifact_lifecycle`、`digital_employee_daily_report_fact` 三张增量物化表,同步入库时 upsert 安全业务摘要,业务查询优先返回 `materialized: true` 行,旧记录继续回退 sync record。 - 缺口:复杂组合检索、跨设备聚合图、远程裁决/策略编辑动作和物化业务表仍待吸收。 - 建议:先使用 sync record 派生业务查询模型和 embedded 业务视图预览承接联调,再按容量和查询压力决定是否拆 Plan / Task / Run / Event 物理业务表。