吸收数字员工步骤自动调度

This commit is contained in:
baiyanyun
2026-06-10 11:23:22 +08:00
parent 047651e404
commit d12db03216
12 changed files with 772 additions and 191 deletions

View File

@@ -289,6 +289,40 @@ export interface DigitalEmployeeApprovalActionRecord {
payload: Record<string, unknown>;
}
export interface DigitalEmployeeReadyPlanStepDispatchCandidate {
stepId: string;
planId: string;
taskId: string | null;
title: string;
taskTitle: string | null;
prompt: string;
preferredSkillIds: string[];
dispatchable: boolean;
skipReason?: string | null;
step: DigitalEmployeePlanStepRecord;
task?: DigitalEmployeeTaskRecord | null;
plan?: DigitalEmployeePlanRecord | null;
}
export interface DigitalEmployeePlanStepDispatchInput {
stepId: string;
planId?: string | null;
taskId?: string | null;
runId?: string | null;
status: "started" | "completed" | "failed" | "skipped";
reason?: string | null;
message?: string | null;
source?: string | null;
payload?: Record<string, unknown> | null;
occurredAt?: string | null;
}
export interface DigitalEmployeePlanStepDispatchRecord {
eventId: string;
kind: "plan_step_dispatch_started" | "plan_step_dispatch_completed" | "plan_step_dispatch_failed" | "plan_step_dispatch_skipped";
payload: Record<string, unknown>;
}
export interface DigitalEmployeeRouteDecisionRecord {
id: string;
route_kind: string;
@@ -1224,6 +1258,204 @@ export function recordDigitalEmployeeApprovalAction(
return { eventId, kind, payload };
}
export function listDigitalEmployeeReadyPlanStepDispatchCandidates(
limit = 20,
): DigitalEmployeeReadyPlanStepDispatchCandidate[] {
const records = readDigitalEmployeeRuntimeRecords();
const tasksByStepId = new Map<string, DigitalEmployeeTaskRecord>();
for (const task of records.tasks) {
const payload = objectRecord(task.payload) ?? {};
const stepId = stringValue(payload.stepId ?? payload.step_id);
if (stepId && !tasksByStepId.has(stepId)) tasksByStepId.set(stepId, task);
}
const tasksByPlanId = new Map<string, DigitalEmployeeTaskRecord[]>();
for (const task of records.tasks) {
const planId = stringValue(task.planId);
if (!planId) continue;
tasksByPlanId.set(planId, [...(tasksByPlanId.get(planId) ?? []), task]);
}
const plansById = new Map(records.plans.map((plan) => [plan.id, plan]));
const activeRunsByTaskId = new Set(records.runs
.filter((run) => isActiveRunStatus(run.status))
.map((run) => stringValue(run.taskId))
.filter(Boolean));
const openApprovals = records.approvals.filter((approval) => isOpenApprovalStatus(approval.status));
return records.planSteps
.filter((step) => normalizePlanStepStatus(step.status) === "ready")
.sort((left, right) => left.seq - right.seq || left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id))
.slice(0, Math.max(1, limit))
.map((step) => {
const task = tasksByStepId.get(step.id) ?? firstRunnableTaskForStep(step, tasksByPlanId.get(step.planId) ?? []) ?? null;
const plan = plansById.get(step.planId) ?? null;
const taskPayload = objectRecord(task?.payload) ?? {};
const stepPayload = objectRecord(step.payload) ?? {};
const preferredSkillIds = normalizeSkillIds([
...step.preferredSkillIds,
...unknownArray(taskPayload.skills),
stringValue(task?.assignedSkillId),
]);
const approvalOpen = openApprovals.some((approval) => {
const payload = objectRecord(approval.payload) ?? {};
return approval.planId === step.planId
|| (task && approval.taskId === task.id)
|| stringValue(payload.stepId ?? payload.step_id) === step.id;
});
const skipReason = readyPlanStepSkipReason({ step, task, activeRunsByTaskId, approvalOpen });
const title = step.title || task?.title || step.id;
const prompt = stringValue(stepPayload.prompt ?? stepPayload.description)
|| stringValue(taskPayload.prompt ?? taskPayload.description)
|| `执行计划步骤:${title}`;
return {
stepId: step.id,
planId: step.planId,
taskId: task?.id ?? null,
title,
taskTitle: task?.title ?? null,
prompt,
preferredSkillIds,
dispatchable: !skipReason,
skipReason,
step,
task,
plan,
};
});
}
export function recordDigitalEmployeePlanStepDispatch(
input: DigitalEmployeePlanStepDispatchInput,
): DigitalEmployeePlanStepDispatchRecord | null {
const db = getDigitalEmployeeDb();
if (!db) return null;
const now = input.occurredAt || new Date().toISOString();
const stepId = stringValue(input.stepId) || "unknown-step";
const planId = stringValue(input.planId) || null;
const taskId = stringValue(input.taskId) || null;
const runId = stringValue(input.runId) || null;
const status = input.status;
const kind = `plan_step_dispatch_${status}` as DigitalEmployeePlanStepDispatchRecord["kind"];
const payload: Record<string, unknown> = {
source: stringValue(input.source) || "qimingclaw-ready-step-dispatcher",
step_id: stepId,
plan_id: planId,
task_id: taskId,
run_id: runId,
status,
reason: stringValue(input.reason) || null,
...(input.payload ?? {}),
};
const eventId = `plan-step-dispatch:${safeId(stepId)}:${status}:${safeId(now)}`;
const tx = db.transaction(() => {
if (status === "started") {
updatePlanStepDispatchStatus(stepId, "running", payload, now);
} else if (status === "failed") {
updatePlanStepDispatchStatus(stepId, "blocked", payload, now);
}
insertEvent(
{
event_id: eventId,
kind,
occurred_at: now,
message: input.message || planStepDispatchMessage(status, stepId, stringValue(input.reason)),
plan_id: planId,
task_id: taskId,
},
runId,
payload,
);
});
tx();
return { eventId, kind, payload };
}
function firstRunnableTaskForStep(
step: DigitalEmployeePlanStepRecord,
tasks: DigitalEmployeeTaskRecord[],
): DigitalEmployeeTaskRecord | null {
const open = tasks.filter((task) => !isTerminalTaskStatus(task.status));
return open.find((task) => {
const payload = objectRecord(task.payload) ?? {};
const dependsOn = normalizeSkillIds(unknownArray(payload.dependsOn ?? payload.depends_on));
return dependsOn.includes(step.id);
}) ?? open[0] ?? null;
}
function readyPlanStepSkipReason(input: {
step: DigitalEmployeePlanStepRecord;
task: DigitalEmployeeTaskRecord | null;
activeRunsByTaskId: Set<string>;
approvalOpen: boolean;
}): string | null {
if (!input.task) return "missing_task";
if (isTerminalTaskStatus(input.task.status)) return "task_terminal";
if (input.activeRunsByTaskId.has(input.task.id)) return "task_already_running";
if (input.approvalOpen) return "approval_pending";
const payload = objectRecord(input.step.payload) ?? {};
if (payload.auto_dispatch === false || payload.autoDispatch === false) return "auto_dispatch_disabled";
return null;
}
function isActiveRunStatus(status: string): boolean {
return ["running", "pending", "queued", "started"].includes(stringValue(status).toLowerCase());
}
function isTerminalTaskStatus(status: string): boolean {
return ["completed", "succeeded", "success", "done", "skipped", "cancelled", "canceled"].includes(stringValue(status).toLowerCase());
}
function isOpenApprovalStatus(status: string): boolean {
return ["pending", "reviewing", "open", "waiting"].includes(stringValue(status).toLowerCase() || "pending");
}
function updatePlanStepDispatchStatus(
stepId: string,
status: string,
dispatchPayload: Record<string, unknown>,
now: string,
): void {
const db = getDigitalEmployeeDb();
if (!db) return;
const row = db.prepare(`
SELECT id, plan_id, title, status, seq, depends_on, preferred_skill_ids, payload, created_at
FROM digital_plan_steps
WHERE id = ?
`).get(stepId) as Record<string, unknown> | undefined;
if (!row) return;
const previousPayload = objectRecord(parseStoredPayload(row.payload)) ?? {};
const payload = {
...previousPayload,
lastDispatch: {
status,
occurredAt: now,
payload: dispatchPayload,
},
};
db.prepare(`
UPDATE digital_plan_steps
SET status = ?, payload = ?, sync_status = 'pending', updated_at = ?
WHERE id = ?
`).run(status, stringify(payload), now, stepId);
markOutbox("plan_step", stepId, "upsert", {
id: stepId,
planId: stringValue(row.plan_id),
title: stringValue(row.title),
status,
seq: typeof row.seq === "number" ? row.seq : Number(row.seq) || 0,
dependsOn: parseStringArray(row.depends_on),
preferredSkillIds: parseStringArray(row.preferred_skill_ids),
payload,
now,
}, now);
}
function planStepDispatchMessage(status: string, stepId: string, reason: string): string {
if (status === "started") return `计划步骤已自动派发:${stepId}`;
if (status === "completed") return `计划步骤派发已触发执行:${stepId}`;
if (status === "failed") return `计划步骤自动派发失败:${stepId}${reason ? `${reason}` : ""}`;
return `计划步骤自动派发已跳过:${stepId}${reason ? `${reason}` : ""}`;
}
function isDigitalEmployeeApprovalAction(action: string): action is DigitalEmployeeApprovalActionKind {
return ["comment", "delegate", "mark_timeout", "mark_risk", "clear_risk"].includes(action);
}