吸收数字员工步骤依赖调度
This commit is contained in:
@@ -1165,6 +1165,8 @@ function taskStatusTone(status: string): 'running' | 'success' | 'danger' | 'wai
|
||||
|
||||
function taskStatusLabel(status: string): string {
|
||||
const normalized = status.toLowerCase();
|
||||
if (normalized === 'ready') return '待启动';
|
||||
if (normalized === 'blocked') return '依赖阻塞';
|
||||
if (normalized === 'skipped') return '已跳过';
|
||||
if (normalized === 'cancelled') return '已取消';
|
||||
if (normalized === 'paused') return '已暂停';
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -16,7 +16,7 @@
|
||||
console.warn("[qimingclaw] digital employee auth bootstrap skipped", error);
|
||||
}
|
||||
</script>
|
||||
<script type="module" crossorigin src="./assets/index-BR7K6m9r.js"></script>
|
||||
<script type="module" crossorigin src="./assets/index-CRHKnsH7.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-CKIQdvco.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -481,6 +481,158 @@ describe("digital employee state service", () => {
|
||||
expect(mockState.db?.outbox.get("run:upsert:run-governance")).toMatchObject({ entity_type: "run" });
|
||||
});
|
||||
|
||||
it("marks dependent plan steps ready when skipped dependencies are satisfied", async () => {
|
||||
const { recordDigitalEmployeeGovernanceCommand } = await import("./stateService");
|
||||
|
||||
createGovernableTask();
|
||||
addPlanStep({ id: "step-dependent", title: "后续执行步骤", seq: 2, dependsOn: ["step-governance"] });
|
||||
|
||||
recordDigitalEmployeeGovernanceCommand({
|
||||
commandKind: "skip_task",
|
||||
commandId: "command-unlock-dependent",
|
||||
planId: "plan-governance",
|
||||
taskId: "task-governance",
|
||||
runId: "run-governance",
|
||||
stepId: "step-governance",
|
||||
accepted: true,
|
||||
payload: { reason: "跳过前置步骤" },
|
||||
});
|
||||
|
||||
expect(mockState.db?.planSteps.get("step-dependent")).toMatchObject({
|
||||
status: "ready",
|
||||
sync_status: "pending",
|
||||
});
|
||||
expect(JSON.parse(mockState.db?.planSteps.get("step-dependent")?.payload ?? "{}")).toMatchObject({
|
||||
dependencyGraph: {
|
||||
status: "ready",
|
||||
triggeredByStepId: "step-governance",
|
||||
satisfiedDependencies: ["step-governance"],
|
||||
},
|
||||
});
|
||||
expect(Array.from(mockState.db?.events.values() ?? [])).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ kind: "plan_step_ready", plan_id: "plan-governance" }),
|
||||
]));
|
||||
expect(mockState.db?.outbox.get("plan_step:upsert:step-dependent")).toMatchObject({
|
||||
entity_type: "plan_step",
|
||||
entity_id: "step-dependent",
|
||||
status: "pending",
|
||||
});
|
||||
});
|
||||
|
||||
it("blocks dependent plan steps on cancellation and returns them to pending on retry", async () => {
|
||||
const { recordDigitalEmployeeGovernanceCommand } = await import("./stateService");
|
||||
|
||||
createGovernableTask();
|
||||
addPlanStep({ id: "step-dependent", title: "后续执行步骤", seq: 2, dependsOn: ["step-governance"] });
|
||||
|
||||
recordDigitalEmployeeGovernanceCommand({
|
||||
commandKind: "cancel_task",
|
||||
commandId: "command-block-dependent",
|
||||
planId: "plan-governance",
|
||||
taskId: "task-governance",
|
||||
runId: "run-governance",
|
||||
stepId: "step-governance",
|
||||
accepted: true,
|
||||
payload: { reason: "取消前置步骤" },
|
||||
});
|
||||
|
||||
expect(mockState.db?.planSteps.get("step-dependent")).toMatchObject({ status: "blocked" });
|
||||
expect(Array.from(mockState.db?.events.values() ?? [])).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ kind: "plan_step_blocked" }),
|
||||
]));
|
||||
|
||||
recordDigitalEmployeeGovernanceCommand({
|
||||
commandKind: "retry_task",
|
||||
commandId: "command-wait-dependent",
|
||||
planId: "plan-governance",
|
||||
taskId: "task-governance",
|
||||
runId: "run-governance",
|
||||
stepId: "step-governance",
|
||||
accepted: true,
|
||||
payload: { reason: "重试前置步骤" },
|
||||
});
|
||||
|
||||
expect(mockState.db?.planSteps.get("step-governance")).toMatchObject({ status: "running" });
|
||||
expect(mockState.db?.planSteps.get("step-dependent")).toMatchObject({ status: "pending" });
|
||||
expect(Array.from(mockState.db?.events.values() ?? [])).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ kind: "plan_step_waiting" }),
|
||||
]));
|
||||
expect(JSON.parse(mockState.db?.planSteps.get("step-dependent")?.payload ?? "{}")).toMatchObject({
|
||||
dependencyGraph: {
|
||||
status: "pending",
|
||||
waitingDependencies: ["step-governance"],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("waits for all dependencies before marking a plan step ready", async () => {
|
||||
const { recordDigitalEmployeeGovernanceCommand } = await import("./stateService");
|
||||
|
||||
createGovernableTask();
|
||||
addPlanStep({ id: "step-other", title: "第二个前置步骤", seq: 2 });
|
||||
addGovernableTaskForStep({ taskId: "task-other", runId: "run-other", stepId: "step-other" });
|
||||
addPlanStep({ id: "step-dependent", title: "汇总步骤", seq: 3, dependsOn: ["step-governance", "step-other"] });
|
||||
|
||||
recordDigitalEmployeeGovernanceCommand({
|
||||
commandKind: "skip_task",
|
||||
commandId: "command-first-dependency",
|
||||
planId: "plan-governance",
|
||||
taskId: "task-governance",
|
||||
runId: "run-governance",
|
||||
stepId: "step-governance",
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
expect(mockState.db?.planSteps.get("step-dependent")).toMatchObject({ status: "pending" });
|
||||
|
||||
recordDigitalEmployeeGovernanceCommand({
|
||||
commandKind: "skip_task",
|
||||
commandId: "command-second-dependency",
|
||||
planId: "plan-governance",
|
||||
taskId: "task-other",
|
||||
runId: "run-other",
|
||||
stepId: "step-other",
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
expect(mockState.db?.planSteps.get("step-dependent")).toMatchObject({ status: "ready" });
|
||||
expect(JSON.parse(mockState.db?.planSteps.get("step-dependent")?.payload ?? "{}")).toMatchObject({
|
||||
dependencyGraph: {
|
||||
status: "ready",
|
||||
satisfiedDependencies: ["step-governance", "step-other"],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("does not overwrite already running or completed dependent plan steps", async () => {
|
||||
const { recordDigitalEmployeeGovernanceCommand } = await import("./stateService");
|
||||
|
||||
createGovernableTask();
|
||||
addPlanStep({ id: "step-running-dependent", status: "running", seq: 2, dependsOn: ["step-governance"] });
|
||||
addPlanStep({ id: "step-completed-dependent", status: "completed", seq: 3, dependsOn: ["step-governance"] });
|
||||
|
||||
recordDigitalEmployeeGovernanceCommand({
|
||||
commandKind: "skip_task",
|
||||
commandId: "command-preserve-dependent",
|
||||
planId: "plan-governance",
|
||||
taskId: "task-governance",
|
||||
runId: "run-governance",
|
||||
stepId: "step-governance",
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
expect(mockState.db?.planSteps.get("step-running-dependent")).toMatchObject({
|
||||
status: "running",
|
||||
sync_status: "synced",
|
||||
});
|
||||
expect(mockState.db?.planSteps.get("step-completed-dependent")).toMatchObject({
|
||||
status: "completed",
|
||||
sync_status: "synced",
|
||||
});
|
||||
expect(mockState.db?.outbox.has("plan_step:upsert:step-running-dependent")).toBe(false);
|
||||
expect(mockState.db?.outbox.has("plan_step:upsert:step-completed-dependent")).toBe(false);
|
||||
});
|
||||
|
||||
it("creates a retry run and preserves the failed source run", async () => {
|
||||
const { recordDigitalEmployeeGovernanceCommand } = await import("./stateService");
|
||||
|
||||
@@ -754,6 +906,59 @@ function createGovernableTask(options: {
|
||||
});
|
||||
}
|
||||
|
||||
function addPlanStep(options: {
|
||||
id: string;
|
||||
title?: string;
|
||||
status?: string;
|
||||
seq?: number;
|
||||
dependsOn?: string[];
|
||||
}): void {
|
||||
mockState.db?.planSteps.set(options.id, {
|
||||
id: options.id,
|
||||
plan_id: "plan-governance",
|
||||
title: options.title ?? options.id,
|
||||
status: options.status ?? "pending",
|
||||
seq: options.seq ?? 2,
|
||||
depends_on: JSON.stringify(options.dependsOn ?? []),
|
||||
preferred_skill_ids: JSON.stringify([]),
|
||||
payload: JSON.stringify({ source: "test" }),
|
||||
sync_status: "synced",
|
||||
created_at: "2026-06-07T07:00:00.000Z",
|
||||
updated_at: "2026-06-07T07:00:00.000Z",
|
||||
});
|
||||
}
|
||||
|
||||
function addGovernableTaskForStep(options: {
|
||||
taskId: string;
|
||||
runId: string;
|
||||
stepId: string;
|
||||
status?: string;
|
||||
}): void {
|
||||
mockState.db?.tasks.set(options.taskId, {
|
||||
id: options.taskId,
|
||||
plan_id: "plan-governance",
|
||||
title: `${options.stepId} 任务`,
|
||||
status: options.status ?? "running",
|
||||
assigned_skill_id: "qimingclaw-task-agent",
|
||||
payload: JSON.stringify({ source: "test", stepId: options.stepId }),
|
||||
sync_status: "synced",
|
||||
created_at: "2026-06-07T07:00:00.000Z",
|
||||
updated_at: "2026-06-07T07:00:00.000Z",
|
||||
});
|
||||
mockState.db?.runs.set(options.runId, {
|
||||
id: options.runId,
|
||||
plan_id: "plan-governance",
|
||||
task_id: options.taskId,
|
||||
status: options.status ?? "running",
|
||||
started_at: "2026-06-07T07:10:00.000Z",
|
||||
finished_at: null,
|
||||
payload: JSON.stringify({ source: "test" }),
|
||||
sync_status: "synced",
|
||||
created_at: "2026-06-07T07:10:00.000Z",
|
||||
updated_at: "2026-06-07T07:10:00.000Z",
|
||||
});
|
||||
}
|
||||
|
||||
interface TestPlanRow {
|
||||
id: string;
|
||||
title: string;
|
||||
@@ -808,7 +1013,7 @@ interface TestEventRow {
|
||||
id: string;
|
||||
plan_id: string;
|
||||
task_id: string;
|
||||
run_id: string;
|
||||
run_id: string | null;
|
||||
kind: string;
|
||||
message: string;
|
||||
payload: string;
|
||||
@@ -946,9 +1151,17 @@ class TestDb {
|
||||
};
|
||||
}
|
||||
|
||||
private all(sql: string, _args: unknown[]): unknown[] {
|
||||
private all(sql: string, args: unknown[]): unknown[] {
|
||||
if (sql.includes("FROM digital_plans")) return Array.from(this.plans.values());
|
||||
if (sql.includes("FROM digital_plan_steps")) return Array.from(this.planSteps.values());
|
||||
if (sql.includes("FROM digital_plan_steps")) {
|
||||
const rows = Array.from(this.planSteps.values());
|
||||
if (sql.includes("WHERE plan_id = ?")) {
|
||||
return rows
|
||||
.filter((step) => step.plan_id === args[0])
|
||||
.sort((left, right) => left.seq - right.seq || left.created_at.localeCompare(right.created_at));
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
if (sql.includes("FROM digital_schedules")) return Array.from(this.schedules.values());
|
||||
if (sql.includes("FROM digital_schedule_runs")) return Array.from(this.scheduleRuns.values());
|
||||
if (sql.includes("FROM digital_tasks")) return Array.from(this.tasks.values());
|
||||
@@ -1304,7 +1517,7 @@ class TestDb {
|
||||
string,
|
||||
string,
|
||||
string,
|
||||
string,
|
||||
string | null,
|
||||
string,
|
||||
string,
|
||||
string,
|
||||
|
||||
@@ -1921,7 +1921,7 @@ function shouldUpdateTaskForGovernance(commandKind: string): boolean {
|
||||
}
|
||||
|
||||
function shouldUpdateStepForGovernance(commandKind: string): boolean {
|
||||
return ["retry_task", "skip_task", "pause_task", "resume_task", "cancel_task", "start_run"].includes(commandKind);
|
||||
return ["retry_task", "skip_task", "pause_task", "resume_task", "cancel_task", "start_run", "cancel_run"].includes(commandKind);
|
||||
}
|
||||
|
||||
function mergeGovernancePayload(
|
||||
@@ -2003,6 +2003,195 @@ function updatePlanStepForGovernance(input: {
|
||||
payload,
|
||||
now: input.now,
|
||||
}, input.now);
|
||||
advancePlanStepDependencyGraph({
|
||||
planId: stringValue(row.plan_id),
|
||||
changedStepId: input.stepId,
|
||||
now: input.now,
|
||||
});
|
||||
}
|
||||
|
||||
function advancePlanStepDependencyGraph(input: {
|
||||
planId: string;
|
||||
changedStepId: string;
|
||||
now: string;
|
||||
}): void {
|
||||
const db = getDigitalEmployeeDb();
|
||||
if (!db || !input.planId || !input.changedStepId) return;
|
||||
const rows = db.prepare(`
|
||||
SELECT id, plan_id, title, status, seq, depends_on, preferred_skill_ids, payload, created_at
|
||||
FROM digital_plan_steps
|
||||
WHERE plan_id = ?
|
||||
ORDER BY seq ASC, created_at ASC
|
||||
`).all(input.planId) as Array<Record<string, unknown>>;
|
||||
if (rows.length === 0) return;
|
||||
|
||||
const stepsById = new Map(rows.map((row) => [stringValue(row.id), row]));
|
||||
const queue = [input.changedStepId];
|
||||
const visited = new Set<string>();
|
||||
|
||||
while (queue.length > 0) {
|
||||
const sourceStepId = queue.shift() || "";
|
||||
if (!sourceStepId || visited.has(sourceStepId)) continue;
|
||||
visited.add(sourceStepId);
|
||||
for (const row of rows) {
|
||||
const stepId = stringValue(row.id);
|
||||
const dependsOn = parseStringArray(row.depends_on);
|
||||
if (!stepId || !dependsOn.includes(sourceStepId)) continue;
|
||||
const currentStatus = normalizePlanStepStatus(row.status);
|
||||
if (!canAdvanceDependentPlanStep(currentStatus)) continue;
|
||||
const nextState = evaluatePlanStepDependencyState(row, stepsById, input.now, input.changedStepId);
|
||||
if (!nextState || nextState.status === currentStatus) continue;
|
||||
updatePlanStepForDependencyState({
|
||||
row,
|
||||
dependsOn,
|
||||
status: nextState.status,
|
||||
eventKind: nextState.eventKind,
|
||||
message: nextState.message,
|
||||
dependencyGraph: nextState.dependencyGraph,
|
||||
now: input.now,
|
||||
});
|
||||
row.status = nextState.status;
|
||||
row.payload = stringify({
|
||||
...(objectRecord(parseStoredPayload(row.payload)) ?? {}),
|
||||
dependencyGraph: nextState.dependencyGraph,
|
||||
});
|
||||
queue.push(stepId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function evaluatePlanStepDependencyState(
|
||||
row: Record<string, unknown>,
|
||||
stepsById: Map<string, Record<string, unknown>>,
|
||||
now: string,
|
||||
triggeredByStepId: string,
|
||||
): {
|
||||
status: "ready" | "blocked" | "pending";
|
||||
eventKind: "plan_step_ready" | "plan_step_blocked" | "plan_step_waiting";
|
||||
message: string;
|
||||
dependencyGraph: Record<string, unknown>;
|
||||
} | null {
|
||||
const stepId = stringValue(row.id);
|
||||
const dependsOn = parseStringArray(row.depends_on);
|
||||
if (!stepId || dependsOn.length === 0) return null;
|
||||
const dependencyStates = dependsOn.map((dependencyId) => {
|
||||
const dependency = stepsById.get(dependencyId);
|
||||
return {
|
||||
id: dependencyId,
|
||||
status: normalizePlanStepStatus(dependency?.status),
|
||||
exists: Boolean(dependency),
|
||||
};
|
||||
});
|
||||
const blockedDependencies = dependencyStates
|
||||
.filter((dependency) => isBlockingPlanStepStatus(dependency.status))
|
||||
.map((dependency) => dependency.id);
|
||||
const waitingDependencies = dependencyStates
|
||||
.filter((dependency) => !dependency.exists || !isSatisfiedPlanStepStatus(dependency.status))
|
||||
.map((dependency) => dependency.id);
|
||||
const satisfiedDependencies = dependencyStates
|
||||
.filter((dependency) => isSatisfiedPlanStepStatus(dependency.status))
|
||||
.map((dependency) => dependency.id);
|
||||
|
||||
const dependencyGraph = {
|
||||
evaluatedAt: now,
|
||||
triggeredByStepId,
|
||||
dependencies: dependencyStates,
|
||||
satisfiedDependencies,
|
||||
waitingDependencies,
|
||||
blockedDependencies,
|
||||
};
|
||||
|
||||
if (blockedDependencies.length > 0) {
|
||||
return {
|
||||
status: "blocked",
|
||||
eventKind: "plan_step_blocked",
|
||||
message: `计划步骤“${stringValue(row.title) || stepId}”因依赖阻塞,等待人工处理。`,
|
||||
dependencyGraph: { ...dependencyGraph, status: "blocked" },
|
||||
};
|
||||
}
|
||||
if (waitingDependencies.length === 0) {
|
||||
return {
|
||||
status: "ready",
|
||||
eventKind: "plan_step_ready",
|
||||
message: `计划步骤“${stringValue(row.title) || stepId}”依赖已满足,等待启动。`,
|
||||
dependencyGraph: { ...dependencyGraph, status: "ready" },
|
||||
};
|
||||
}
|
||||
return {
|
||||
status: "pending",
|
||||
eventKind: "plan_step_waiting",
|
||||
message: `计划步骤“${stringValue(row.title) || stepId}”重新等待依赖完成。`,
|
||||
dependencyGraph: { ...dependencyGraph, status: "pending" },
|
||||
};
|
||||
}
|
||||
|
||||
function updatePlanStepForDependencyState(input: {
|
||||
row: Record<string, unknown>;
|
||||
dependsOn: string[];
|
||||
status: "ready" | "blocked" | "pending";
|
||||
eventKind: "plan_step_ready" | "plan_step_blocked" | "plan_step_waiting";
|
||||
message: string;
|
||||
dependencyGraph: Record<string, unknown>;
|
||||
now: string;
|
||||
}): void {
|
||||
const db = getDigitalEmployeeDb();
|
||||
if (!db) return;
|
||||
const stepId = stringValue(input.row.id);
|
||||
const planId = stringValue(input.row.plan_id);
|
||||
const payload = {
|
||||
...(objectRecord(parseStoredPayload(input.row.payload)) ?? {}),
|
||||
dependencyGraph: input.dependencyGraph,
|
||||
};
|
||||
db.prepare(`
|
||||
UPDATE digital_plan_steps
|
||||
SET status = ?, payload = ?, sync_status = 'pending', updated_at = ?
|
||||
WHERE id = ?
|
||||
`).run(input.status, stringify(payload), input.now, stepId);
|
||||
markOutbox("plan_step", stepId, "upsert", {
|
||||
id: stepId,
|
||||
planId,
|
||||
title: stringValue(input.row.title),
|
||||
status: input.status,
|
||||
seq: typeof input.row.seq === "number" ? input.row.seq : Number(input.row.seq) || 0,
|
||||
dependsOn: input.dependsOn,
|
||||
preferredSkillIds: parseStringArray(input.row.preferred_skill_ids),
|
||||
payload,
|
||||
now: input.now,
|
||||
}, input.now);
|
||||
insertEvent(
|
||||
{
|
||||
event_id: `${stepId}:dependency:${input.status}:${safeId(stringValue(input.dependencyGraph.triggeredByStepId))}:${safeId(input.now)}`,
|
||||
kind: input.eventKind,
|
||||
occurred_at: input.now,
|
||||
message: input.message,
|
||||
plan_id: planId,
|
||||
task_id: null,
|
||||
},
|
||||
null,
|
||||
{
|
||||
source: "qimingclaw-plan-step-dependency",
|
||||
stepId,
|
||||
planId,
|
||||
status: input.status,
|
||||
dependencyGraph: input.dependencyGraph,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function canAdvanceDependentPlanStep(status: string): boolean {
|
||||
return ["pending", "ready", "blocked"].includes(status);
|
||||
}
|
||||
|
||||
function isSatisfiedPlanStepStatus(status: string): boolean {
|
||||
return ["completed", "succeeded", "success", "done", "skipped"].includes(status);
|
||||
}
|
||||
|
||||
function isBlockingPlanStepStatus(status: string): boolean {
|
||||
return ["failed", "error", "cancelled", "canceled", "rejected", "blocked"].includes(status);
|
||||
}
|
||||
|
||||
function normalizePlanStepStatus(status: unknown): string {
|
||||
return stringValue(status).toLowerCase() || "pending";
|
||||
}
|
||||
|
||||
function persistTaskGovernanceRun(input: {
|
||||
@@ -3247,7 +3436,7 @@ function upsertApproval(input: {
|
||||
|
||||
function insertEvent(
|
||||
event: DigitalEmployeeStateEvent,
|
||||
runId: string,
|
||||
runId: string | null,
|
||||
payload: unknown,
|
||||
): void {
|
||||
const db = getDigitalEmployeeDb();
|
||||
|
||||
@@ -538,11 +538,11 @@ GET /api/digital-employee/approvals
|
||||
|
||||
以下清单按当前 qimingclaw 已落地状态盘点,不重复列出已完成的前端嵌入、本地 Plan / Task / Run / Event / Artifact / Approval / MemoryEntry、outbox 同步、技能投影和同步诊断。
|
||||
|
||||
1. **PlanStep 与依赖图(已开始)**
|
||||
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。
|
||||
- 后续缺口:步骤依赖调度和跨任务编排执行仍待吸收。
|
||||
- 建议:下一轮围绕依赖图调度,把已完成/跳过/取消的步骤继续驱动到后续步骤。
|
||||
- 当前能力: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 并写入依赖图事件。
|
||||
- 后续缺口:跨任务编排执行、ready 步骤自动调度、依赖阻塞人工处理 UI 和管理端依赖图视图仍待吸收。
|
||||
- 建议:下一轮围绕入口路由/任务分流,把 ready PlanStep 接入安全的执行触发策略。
|
||||
|
||||
2. **调度 / 定时 / 周期任务**
|
||||
- 已吸收:`digital_schedules`、`digital_schedule_runs` 已进入 qimingclaw 本地 SQLite 与 outbox,entity type 为 `schedule` / `schedule_run`;旧 `/api/cron`、`/api/scheduler/jobs` 和治理命令会优先读取真实本地调度记录。
|
||||
|
||||
Reference in New Issue
Block a user