吸收数字员工步骤依赖调度
This commit is contained in:
@@ -1165,6 +1165,8 @@ function taskStatusTone(status: string): 'running' | 'success' | 'danger' | 'wai
|
|||||||
|
|
||||||
function taskStatusLabel(status: string): string {
|
function taskStatusLabel(status: string): string {
|
||||||
const normalized = status.toLowerCase();
|
const normalized = status.toLowerCase();
|
||||||
|
if (normalized === 'ready') return '待启动';
|
||||||
|
if (normalized === 'blocked') return '依赖阻塞';
|
||||||
if (normalized === 'skipped') return '已跳过';
|
if (normalized === 'skipped') return '已跳过';
|
||||||
if (normalized === 'cancelled') return '已取消';
|
if (normalized === 'cancelled') return '已取消';
|
||||||
if (normalized === 'paused') 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);
|
console.warn("[qimingclaw] digital employee auth bootstrap skipped", error);
|
||||||
}
|
}
|
||||||
</script>
|
</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">
|
<link rel="stylesheet" crossorigin href="./assets/index-CKIQdvco.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -481,6 +481,158 @@ describe("digital employee state service", () => {
|
|||||||
expect(mockState.db?.outbox.get("run:upsert:run-governance")).toMatchObject({ entity_type: "run" });
|
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 () => {
|
it("creates a retry run and preserves the failed source run", async () => {
|
||||||
const { recordDigitalEmployeeGovernanceCommand } = await import("./stateService");
|
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 {
|
interface TestPlanRow {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
@@ -808,7 +1013,7 @@ interface TestEventRow {
|
|||||||
id: string;
|
id: string;
|
||||||
plan_id: string;
|
plan_id: string;
|
||||||
task_id: string;
|
task_id: string;
|
||||||
run_id: string;
|
run_id: string | null;
|
||||||
kind: string;
|
kind: string;
|
||||||
message: string;
|
message: string;
|
||||||
payload: 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_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_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_schedule_runs")) return Array.from(this.scheduleRuns.values());
|
||||||
if (sql.includes("FROM digital_tasks")) return Array.from(this.tasks.values());
|
if (sql.includes("FROM digital_tasks")) return Array.from(this.tasks.values());
|
||||||
@@ -1304,7 +1517,7 @@ class TestDb {
|
|||||||
string,
|
string,
|
||||||
string,
|
string,
|
||||||
string,
|
string,
|
||||||
string,
|
string | null,
|
||||||
string,
|
string,
|
||||||
string,
|
string,
|
||||||
string,
|
string,
|
||||||
|
|||||||
@@ -1921,7 +1921,7 @@ function shouldUpdateTaskForGovernance(commandKind: string): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function shouldUpdateStepForGovernance(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(
|
function mergeGovernancePayload(
|
||||||
@@ -2003,6 +2003,195 @@ function updatePlanStepForGovernance(input: {
|
|||||||
payload,
|
payload,
|
||||||
now: input.now,
|
now: input.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: {
|
function persistTaskGovernanceRun(input: {
|
||||||
@@ -3247,7 +3436,7 @@ function upsertApproval(input: {
|
|||||||
|
|
||||||
function insertEvent(
|
function insertEvent(
|
||||||
event: DigitalEmployeeStateEvent,
|
event: DigitalEmployeeStateEvent,
|
||||||
runId: string,
|
runId: string | null,
|
||||||
payload: unknown,
|
payload: unknown,
|
||||||
): void {
|
): void {
|
||||||
const db = getDigitalEmployeeDb();
|
const db = getDigitalEmployeeDb();
|
||||||
|
|||||||
@@ -538,11 +538,11 @@ GET /api/digital-employee/approvals
|
|||||||
|
|
||||||
以下清单按当前 qimingclaw 已落地状态盘点,不重复列出已完成的前端嵌入、本地 Plan / Task / Run / Event / Artifact / Approval / MemoryEntry、outbox 同步、技能投影和同步诊断。
|
以下清单按当前 qimingclaw 已落地状态盘点,不重复列出已完成的前端嵌入、本地 Plan / Task / Run / Event / Artifact / Approval / MemoryEntry、outbox 同步、技能投影和同步诊断。
|
||||||
|
|
||||||
1. **PlanStep 与依赖图(已开始)**
|
1. **PlanStep 与依赖图(依赖图调度已开始)**
|
||||||
- 已吸收:Plan / Task / Run 记录、计划模板、`create_plan` 中的 steps/tasks 可物化为任务;PlanStep 已开始作为正式本地实体落入 `digital_plan_steps`。
|
- 已吸收: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. **调度 / 定时 / 周期任务**
|
2. **调度 / 定时 / 周期任务**
|
||||||
- 已吸收:`digital_schedules`、`digital_schedule_runs` 已进入 qimingclaw 本地 SQLite 与 outbox,entity type 为 `schedule` / `schedule_run`;旧 `/api/cron`、`/api/scheduler/jobs` 和治理命令会优先读取真实本地调度记录。
|
- 已吸收:`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