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 064855d9..b4be41a8 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 @@ -33,6 +33,22 @@ public class DigitalEmployeeAdminWorkbenchRowDto { private String taskId; @JsonProperty("run_id") private String runId; + @JsonProperty("step_id") + private String stepId; + @JsonProperty("dependency_status") + private String dependencyStatus; + @JsonProperty("lease_state") + private String leaseState; + @JsonProperty("lease_source") + private String leaseSource; + @JsonProperty("dispatch_status") + private String dispatchStatus; + @JsonProperty("dispatch_policy_key") + private String dispatchPolicyKey; + @JsonProperty("orchestration_id") + private String orchestrationId; + @JsonProperty("trigger_source") + private String triggerSource; @JsonProperty("approval_id") private String approvalId; @JsonProperty("artifact_id") 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 aab7e815..6d9c2658 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 @@ -74,6 +74,7 @@ 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_step:mark_reviewed", "plan_step:dismiss", "plan_step:retry_policy_check", "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", @@ -995,6 +996,14 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye .planId(businessRow.getPlanId()) .taskId(businessRow.getTaskId()) .runId(businessRow.getRunId()) + .stepId(firstString(businessView.get("step_id"), businessView.get("stepId"), payload.get("step_id"), payload.get("stepId"), "plan_step".equals(record.getEntityType()) ? record.getEntityId() : null)) + .dependencyStatus(firstString(businessView.get("dependency_status"), businessView.get("dependencyStatus"), payload.get("dependency_status"), payload.get("dependencyStatus"))) + .leaseState(firstString(businessView.get("lease_state"), businessView.get("leaseState"), payload.get("lease_state"), payload.get("leaseState"))) + .leaseSource(firstString(businessView.get("lease_source"), businessView.get("leaseSource"), payload.get("lease_source"), payload.get("leaseSource"))) + .dispatchStatus(firstString(businessView.get("dispatch_status"), businessView.get("dispatchStatus"), payload.get("dispatch_status"), payload.get("dispatchStatus"))) + .dispatchPolicyKey(firstString(businessView.get("dispatch_policy_key"), businessView.get("dispatchPolicyKey"), businessView.get("policy_key"), payload.get("dispatch_policy_key"), payload.get("dispatchPolicyKey"), payload.get("policy_key"))) + .orchestrationId(firstString(businessView.get("orchestration_id"), businessView.get("orchestrationId"), payload.get("orchestration_id"), payload.get("orchestrationId"))) + .triggerSource(firstString(businessView.get("trigger_source"), businessView.get("triggerSource"), payload.get("trigger_source"), payload.get("triggerSource"))) .approvalId(firstString(businessView.get("approval_id"), payload.get("approval_id"), "approval".equals(record.getEntityType()) ? record.getEntityId() : null)) .artifactId(firstString(businessView.get("artifact_id"), payload.get("artifact_id"), "artifact".equals(record.getEntityType()) ? record.getEntityId() : null)) .cleanupRequestId(firstString(businessView.get("cleanup_request_id"), businessView.get("cleanupRequestId"), payload.get("cleanup_request_id"), payload.get("cleanupRequestId"))) @@ -1050,6 +1059,7 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye case "skill_audits" -> isSkillAuditRow(row); case "policy_center" -> isPolicyCenterRow(row); case "scheduler_operations" -> isSchedulerOperationsRow(row); + case "plan_step_dispatch" -> isPlanStepDispatchRow(row); case "plan_governance" -> isPlanGovernanceRow(row); case "plan_revisions" -> isPlanRevisionRow(row); default -> true; @@ -1183,6 +1193,36 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye || StringUtils.isNotBlank(row.getJobId()); } + private boolean isPlanStepDispatchRow(DigitalEmployeeAdminWorkbenchRowDto row) { + String category = firstString(row.getCategory()); + String kind = firstString(row.getKind()); + String status = firstString(row.getStatus()); + String source = firstString( + row.getLeaseSource(), row.getTriggerSource(), + row.getBusinessView().get("source"), row.getPayload().get("source"), + row.getBusinessView().get("lease_source"), row.getPayload().get("lease_source") + ); + return StringUtils.equals(row.getEntityType(), "plan_step") + || StringUtils.equals(category, "plan_step_dispatch") + || StringUtils.equals(category, "plan_step") + || startsWithAny(kind, "plan_step_dispatch", "plan_step_lease", "dispatch_ready_steps") + || contains(kind, "plan_step_dispatch") + || contains(kind, "plan_step_lease") + || contains(kind, "dispatch_ready_steps") + || contains(kind, "lease_active") + || contains(kind, "remote_lease_rejected") + || contains(kind, "local_fallback") + || contains(status, "lease_active") + || contains(status, "remote_lease_rejected") + || contains(status, "local_fallback") + || contains(source, "remote_lease_rejected") + || contains(source, "local_fallback") + || StringUtils.isNotBlank(row.getStepId()) + || StringUtils.isNotBlank(row.getLeaseState()) + || StringUtils.isNotBlank(row.getDispatchStatus()) + || StringUtils.isNotBlank(row.getDispatchPolicyKey()); + } + private boolean isPlanGovernanceRow(DigitalEmployeeAdminWorkbenchRowDto row) { String category = firstString(row.getCategory()); String kind = firstString(row.getKind()); @@ -1206,6 +1246,11 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye 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")); + if (StringUtils.equals(row.getEntityType(), "plan_step") + || startsWithAny(kind, "plan_step_dispatch", "plan_step_lease") + || contains(kind, "dispatch_ready_steps")) { + return "plan_step_dispatch"; + } if (contains(kind, "route_decision_blocked") || contains(kind, "route_decision_approval_required") || contains(kind, "route_action_policy_blocked") || contains(kind, "route_action_missing_context") || contains(kind, "route_action_not_mapped")) { 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 22c99b65..7d44d3d8 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 @@ -567,6 +567,94 @@ class DigitalEmployeeSyncApplicationServiceImplTest { assertThat(result.getRecords().get(1).getKind()).isEqualTo("scheduler_check_completed"); } + @Test + void queryAdminWorkbenchPlanStepDispatchReturnsStepLeaseAndDispatchRowsOnly() { + 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-step", "plan_step", "step-1", 101L, new Date(10_000), + Map.of( + "entity_type", "plan_step", + "category", "plan_step", + "title", "步骤一", + "status", "ready", + "summary", "依赖已满足", + "plan_id", "plan-1", + "task_id", "task-1", + "step_id", "step-1", + "dependency_status", "ready" + ), + Map.of("id", "step-1", "status", "ready")), + record("outbox-dispatch", "event", "event-step-1", 101L, new Date(11_000), + Map.ofEntries( + Map.entry("entity_type", "event"), + Map.entry("kind", "plan_step_dispatch_started"), + Map.entry("status", "running"), + Map.entry("title", "步骤派发开始"), + Map.entry("plan_id", "plan-1"), + Map.entry("task_id", "task-1"), + Map.entry("run_id", "run-1"), + Map.entry("step_id", "step-1"), + Map.entry("dispatch_status", "started"), + Map.entry("orchestration_id", "orch-1"), + Map.entry("trigger_source", "scheduler_sweep") + ), + Map.of("id", "event-step-1", "kind", "plan_step_dispatch_started")), + record("outbox-lease", "event", "event-step-2", 101L, new Date(12_000), + Map.of( + "entity_type", "event", + "kind", "plan_step_lease_acquired", + "status", "lease_active", + "title", "步骤租约已获取", + "plan_id", "plan-1", + "step_id", "step-1", + "lease_state", "active", + "lease_source", "remote" + ), + Map.of("id", "event-step-2", "kind", "plan_step_lease_acquired")), + record("outbox-policy", "event", "event-step-3", 101L, new Date(13_000), + Map.of( + "entity_type", "event", + "kind", "plan_step_dispatch_policy_pulled", + "status", "synced", + "title", "步骤派发策略已拉取", + "step_id", "step-1", + "dispatch_policy_key", "plan_step_dispatch" + ), + Map.of("id", "event-step-3", "kind", "plan_step_dispatch_policy_pulled")), + 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", "available"), + Map.of("id", "artifact-1", "status", "available")), + record("outbox-audit", "event", "event-audit-1", 101L, new Date(17_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( + "plan_step_dispatch", 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("step-1", "event-step-1", "event-step-2", "event-step-3"); + assertThat(result.getRecords().get(0).getView()).isEqualTo("plan_step_dispatch"); + assertThat(result.getRecords().get(0).getStepId()).isEqualTo("step-1"); + assertThat(result.getRecords().get(0).getDependencyStatus()).isEqualTo("ready"); + assertThat(result.getRecords().get(1).getDispatchStatus()).isEqualTo("started"); + assertThat(result.getRecords().get(1).getOrchestrationId()).isEqualTo("orch-1"); + assertThat(result.getRecords().get(1).getTriggerSource()).isEqualTo("scheduler_sweep"); + assertThat(result.getRecords().get(2).getLeaseState()).isEqualTo("active"); + assertThat(result.getRecords().get(2).getLeaseSource()).isEqualTo("remote"); + assertThat(result.getRecords().get(3).getDispatchPolicyKey()).isEqualTo("plan_step_dispatch"); + assertThat(result.getRecords().get(3).getTitle()).isEqualTo("步骤派发策略已拉取"); + assertThat(result.getRecords().get(0).getSummary()).isEqualTo("依赖已满足"); + } + @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))) @@ -1281,6 +1369,27 @@ class DigitalEmployeeSyncApplicationServiceImplTest { assertThat(activatePlan.getReasonCodes()).contains("high_risk_governance_action_disabled"); } + @Test + void createAdminActionRecordsPlanStepLowRiskAndRejectsRemoteDispatch() { + DigitalEmployeeAdminActionResultDto retryPolicyCheck = service.createAdminAction( + adminActionRequest("plan_step", "step-1", "retry_policy_check") + ); + DigitalEmployeeAdminActionResultDto dispatchReadyStep = service.createAdminAction( + adminActionRequest("plan_step", "step-1", "dispatch_ready_step") + ); + DigitalEmployeeAdminActionResultDto takeoverLease = service.createAdminAction( + adminActionRequest("plan_step", "step-1", "takeover_lease") + ); + + assertThat(retryPolicyCheck.getOk()).isTrue(); + assertThat(retryPolicyCheck.getStatus()).isEqualTo("pending"); + assertThat(retryPolicyCheck.getReasonCodes()).contains("action_recorded"); + assertThat(dispatchReadyStep.getOk()).isFalse(); + assertThat(dispatchReadyStep.getReasonCodes()).contains("unsupported_admin_action"); + assertThat(takeoverLease.getOk()).isFalse(); + assertThat(takeoverLease.getReasonCodes()).contains("unsupported_admin_action"); + } + @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 db00afc1..d3fde07f 100644 --- a/qiming/src/pages/SystemManagement/Content/DigitalEmployeeOps/index.tsx +++ b/qiming/src/pages/SystemManagement/Content/DigitalEmployeeOps/index.tsx @@ -51,6 +51,7 @@ const WORKBENCH_TABS: Array<{ { key: 'skill_audits', label: '技能审计' }, { key: 'policy_center', label: '策略中心' }, { key: 'scheduler_operations', label: '调度运营' }, + { key: 'plan_step_dispatch', label: '步骤调度' }, { key: 'plan_governance', label: '计划治理' }, { key: 'plan_revisions', label: '计划版本' }, ]; @@ -105,6 +106,10 @@ function renderSummary(record: DigitalEmployeeAdminWorkbenchRow) { const title = compactText(record.title || record.entity_id, 72); const summary = compactText(record.summary || record.kind || record.category); const meta = [ + record.step_id && `Step ${record.step_id}`, + record.lease_state && `Lease ${record.lease_state}`, + record.dispatch_status && `Dispatch ${record.dispatch_status}`, + record.dispatch_policy_key && `Policy ${record.dispatch_policy_key}`, record.plan_id && `Plan ${record.plan_id}`, record.task_id && `Task ${record.task_id}`, record.run_id && `Run ${record.run_id}`, @@ -142,7 +147,7 @@ function renderSummary(record: DigitalEmployeeAdminWorkbenchRow) { {meta.length > 0 && ( - {meta.slice(0, 3).map((item) => ( + {meta.slice(0, 4).map((item) => ( {item} @@ -205,6 +210,9 @@ 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_step_dispatch') { + return { entityType: 'plan_step', entityId: record.step_id || record.entity_id }; + } if (view === 'plan_governance') { return { entityType: 'plan', entityId: record.plan_id || record.entity_id }; } @@ -313,6 +321,13 @@ function allowedActions(record: DigitalEmployeeAdminWorkbenchRow, view: DigitalE { action: 'dismiss', label: '忽略' }, ]; } + if (view === 'plan_step_dispatch') { + return [ + { action: 'mark_reviewed', label: '标记已看' }, + { action: 'dismiss', label: '忽略' }, + { action: 'retry_policy_check', label: '重试策略检查' }, + ]; + } if (view === 'plan_governance') { return [ { action: 'mark_reviewed', label: '标记已看' }, @@ -782,6 +797,7 @@ const DigitalEmployeeOps: React.FC = () => { detailRecord.plan_id && `Plan ${detailRecord.plan_id}`, detailRecord.task_id && `Task ${detailRecord.task_id}`, detailRecord.run_id && `Run ${detailRecord.run_id}`, + detailRecord.step_id && `Step ${detailRecord.step_id}`, detailRecord.approval_id && `Approval ${detailRecord.approval_id}`, detailRecord.service_id && `Service ${detailRecord.service_id}`, detailRecord.artifact_id && `Artifact ${detailRecord.artifact_id}`, @@ -950,6 +966,20 @@ const DigitalEmployeeOps: React.FC = () => { )} + {activeView === 'plan_step_dispatch' && ( + + 步骤调度: + {detailRecord.step_id || detailRecord.entity_id || '--'} + {detailRecord.dependency_status && ` · 依赖 ${detailRecord.dependency_status}`} + {detailRecord.lease_state && ` · 租约 ${detailRecord.lease_state}`} + {detailRecord.dispatch_status && ` · 派发 ${detailRecord.dispatch_status}`} + {detailRecord.dispatch_policy_key && ` · 策略 ${detailRecord.dispatch_policy_key}`} + + {' '} + · 不远程派发、不抢占租约、不释放租约,只记录复核、忽略和策略复查意图。 + + + )} {activeView === 'plan_governance' && ( 计划治理: diff --git a/qiming/src/types/interfaces/systemManage.ts b/qiming/src/types/interfaces/systemManage.ts index f3406694..d610ca06 100644 --- a/qiming/src/types/interfaces/systemManage.ts +++ b/qiming/src/types/interfaces/systemManage.ts @@ -572,6 +572,7 @@ export type DigitalEmployeeAdminWorkbenchView = | 'skill_audits' | 'policy_center' | 'scheduler_operations' + | 'plan_step_dispatch' | 'plan_governance' | 'plan_revisions'; @@ -592,6 +593,14 @@ export interface DigitalEmployeeAdminWorkbenchRow { plan_id?: string; task_id?: string; run_id?: string; + step_id?: string; + dependency_status?: string; + lease_state?: string; + lease_source?: string; + dispatch_status?: string; + dispatch_policy_key?: string; + orchestration_id?: string; + trigger_source?: string; approval_id?: string; artifact_id?: string; cleanup_request_id?: 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 b756bec4..dbcabf86 100644 --- a/qimingclaw/crates/agent-electron-client/scripts/check-digital-employee.js +++ b/qimingclaw/crates/agent-electron-client/scripts/check-digital-employee.js @@ -1737,6 +1737,100 @@ function checkDigitalAdminPlanGovernanceWorkbenches() { console.log("[DigitalEmployeeCheck] admin plan governance and revision hooks OK"); } +function checkDigitalAdminPlanStepDispatchWorkbench() { + console.log("\n[DigitalEmployeeCheck] Verify admin plan step dispatch 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_step_dispatch", "backend plan step dispatch view"], + [service, "isPlanStepDispatchRow", "backend plan step dispatch matcher"], + [service, "plan_step:retry_policy_check", "backend plan step retry policy action"], + [service, "remote_lease_rejected", "backend remote lease rejection matcher"], + [service, "local_fallback", "backend local fallback matcher"], + [dto, "step_id", "backend step id field"], + [dto, "lease_state", "backend lease state field"], + [dto, "dispatch_status", "backend dispatch status field"], + [dto, "dispatch_policy_key", "backend dispatch policy key field"], + [test, "queryAdminWorkbenchPlanStepDispatchReturnsStepLeaseAndDispatchRowsOnly", "backend plan step dispatch test"], + [qimingTypes, "plan_step_dispatch", "qiming plan step dispatch view type"], + [qimingTypes, "step_id", "qiming step id field"], + [qimingTypes, "lease_state", "qiming lease state field"], + [qimingTypes, "dispatch_status", "qiming dispatch status field"], + [qimingTypes, "dispatch_policy_key", "qiming dispatch policy field"], + [qimingOps, "步骤调度", "qiming plan step dispatch tab"], + [qimingOps, "重试策略检查", "qiming plan step retry policy action"], + [qimingOps, "不远程派发", "qiming remote dispatch guard copy"], + [qimingOps, "不抢占租约", "qiming lease takeover guard copy"], + [qimingOps, "不释放租约", "qiming lease release guard copy"], + [docs, "正式管理端步骤调度预览", "docs plan step dispatch absorption"], + ]; + const missing = requiredSnippets + .filter(([content, snippet]) => !content.includes(snippet)) + .map(([, , label]) => label); + if (missing.length > 0) { + throw new Error(`Missing admin plan step dispatch hooks: ${missing.join(", ")}`); + } + console.log("[DigitalEmployeeCheck] admin plan step dispatch hooks OK"); +} + function checkDigitalAdminCollaborationOperationsClosure() { console.log("\n[DigitalEmployeeCheck] Verify admin artifact, notification and daily report collaboration closure hooks"); const backendRoot = path.resolve(packageRoot, "..", "..", "..", "qiming-backend"); @@ -2437,6 +2531,7 @@ function main() { checkDigitalAdminMemorySkillWorkbench(); checkDigitalAdminPolicySchedulerWorkbenches(); checkDigitalAdminPlanGovernanceWorkbenches(); + checkDigitalAdminPlanStepDispatchWorkbench(); checkDigitalAdminCollaborationOperationsClosure(); checkDigitalAdminServiceOperationsWorkbench(); checkDigitalAdminRouteGovernanceWorkbench(); diff --git a/qimingclaw/docs/digital-employee-frontend-integration-plan.md b/qimingclaw/docs/digital-employee-frontend-integration-plan.md index a535f0ad..7a5cf8dc 100644 --- a/qimingclaw/docs/digital-employee-frontend-integration-plan.md +++ b/qimingclaw/docs/digital-employee-frontend-integration-plan.md @@ -550,8 +550,8 @@ 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`。正式管理端策略中心预览已开始吸收,qiming-backend 新增 `policy_center` 运营台视图聚合派发、风险审批、入口路由和技能策略快照,qiming“数字员工运营台”的“策略中心”tab 可记录批量拉取策略、标记已看和忽略意图,不开放远程策略编辑。 - - 后续缺口:跨设备审批策略 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 可记录批量拉取策略、标记已看和忽略意图,不开放远程策略编辑。正式管理端步骤调度预览已开始吸收,qiming“数字员工运营台”的“步骤调度”tab 会聚合 PlanStep fact、依赖状态、租约、派发事件和策略拉取审计,只记录标记已看、忽略和重试策略检查意图,不远程派发、不抢占租约、不释放租约。 + - 后续缺口:跨设备审批策略 UI、跨设备策略冲突合并、远程策略编辑安全模型、真实派发/租约裁决和跨设备策略合并仍待吸收。 - 建议:下一轮围绕管理端正式路由 UI 或跨端审批策略 UI,把本地可治理调度继续推进到跨端治理策略。 2. **调度 / 定时 / 周期任务**