吸收数字员工管理端策略调度预览

This commit is contained in:
baiyanyun
2026-06-13 14:42:16 +08:00
parent de501c0252
commit f08b0c39d9
7 changed files with 336 additions and 6 deletions

View File

@@ -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")

View File

@@ -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<String> 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<String> 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<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"));
@@ -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();

View File

@@ -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)))

View File

@@ -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 = () => {
</Typography.Text>
</Typography.Paragraph>
)}
{activeView === 'policy_center' && (
<Typography.Paragraph>
<Typography.Text strong></Typography.Text>
{detailRecord.policy_kind || detailRecord.policy_key || detailRecord.entity_id || '--'}
{detailRecord.status && ` · ${detailRecord.status}`}
<Typography.Text type="secondary">
{' '}
·
</Typography.Text>
</Typography.Paragraph>
)}
{activeView === 'scheduler_operations' && (
<Typography.Paragraph>
<Typography.Text strong></Typography.Text>
{detailRecord.job_id || detailRecord.schedule_id || detailRecord.entity_id || '--'}
{detailRecord.status && ` · ${detailRecord.status}`}
<Typography.Text type="secondary">
{' '}
· start/stop
</Typography.Text>
</Typography.Paragraph>
)}
{allowedActions(detailRecord, activeView).length > 0 && (
<div style={{ marginBottom: 16 }}>
<Typography.Title level={5}></Typography.Title>

View File

@@ -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;

View File

@@ -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();

View File

@@ -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 与 outboxentity 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 auditqiming“数字员工运营台”的“路由治理”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 auditqiming“数字员工运营台”的“路由治理”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 深链和脱敏客户端 KeyPlan 视图会聚合 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 深链和脱敏客户端 KeyPlan 视图会聚合 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 物理业务表。