吸收数字员工步骤自动调度
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user