吸收数字员工计划步骤依赖图

This commit is contained in:
baiyanyun
2026-06-09 10:21:12 +08:00
parent e4715bbabb
commit f35d945d9c
10 changed files with 641 additions and 74 deletions

View File

@@ -88,6 +88,23 @@ export interface DigitalEmployeeTaskRecord {
updatedAt: string;
}
export interface DigitalEmployeePlanStepRecord {
id: string;
remoteId?: string | null;
planId: string;
title: string;
status: string;
seq: number;
dependsOn: string[];
preferredSkillIds: string[];
payload: unknown;
syncStatus: string;
lastSyncedAt?: string | null;
syncError?: string | null;
createdAt: string;
updatedAt: string;
}
export interface DigitalEmployeeRunRecord {
id: string;
remoteId?: string | null;
@@ -155,6 +172,7 @@ export interface DigitalEmployeeApprovalRecord {
export interface DigitalEmployeeRuntimeRecords {
generatedAt: string;
plans: DigitalEmployeePlanRecord[];
planSteps: DigitalEmployeePlanStepRecord[];
tasks: DigitalEmployeeTaskRecord[];
runs: DigitalEmployeeRunRecord[];
events: DigitalEmployeeEventRecord[];
@@ -479,6 +497,7 @@ export function readDigitalEmployeeRuntimeRecords(
return {
generatedAt: new Date().toISOString(),
plans: [],
planSteps: [],
tasks: [],
runs: [],
events: [],
@@ -494,6 +513,14 @@ export function readDigitalEmployeeRuntimeRecords(
ORDER BY updated_at DESC, created_at DESC
LIMIT ?
`).all(safeLimit) as Array<Record<string, unknown>>;
const planSteps = db.prepare(`
SELECT id, remote_id, plan_id, title, status, seq, depends_on,
preferred_skill_ids, payload, sync_status, last_synced_at,
sync_error, created_at, updated_at
FROM digital_plan_steps
ORDER BY plan_id ASC, seq ASC, created_at ASC
LIMIT ?
`).all(safeLimit) as Array<Record<string, unknown>>;
const tasks = db.prepare(`
SELECT id, remote_id, plan_id, title, status, assigned_skill_id, payload,
sync_status, last_synced_at, sync_error, created_at, updated_at
@@ -533,6 +560,7 @@ export function readDigitalEmployeeRuntimeRecords(
return {
generatedAt: new Date().toISOString(),
plans: plans.map(toPlanRecord),
planSteps: planSteps.map(toPlanStepRecord),
tasks: tasks.map(toTaskRecord),
runs: runs.map(toRunRecord),
events: events.map(toEventRecord),
@@ -802,6 +830,13 @@ export function recordDigitalEmployeeGovernanceCommand(
now,
});
if (command.commandKind === "create_plan") {
upsertCreatedPlanSteps({
planId: ids.planId,
payload,
status: "pending",
commandId,
now,
});
upsertCreatedPlanTasks({
planId: ids.planId,
payload,
@@ -878,6 +913,34 @@ function readExistingPlanSummary(planId: string): { title: string; objective: st
}
}
function upsertCreatedPlanSteps(input: {
planId: string;
payload: Record<string, unknown>;
status: string;
commandId: string;
now: string;
}): void {
extractPlanStepInputs(input.payload, input.planId).forEach((step) => {
upsertPlanStep({
id: step.id,
planId: input.planId,
title: step.title,
status: step.status || input.status,
seq: step.seq,
dependsOn: step.dependsOn,
preferredSkillIds: step.preferredSkillIds,
payload: {
source: "qimingclaw-create-plan",
commandId: input.commandId,
dependsOn: step.dependsOn,
preferredSkillIds: step.preferredSkillIds,
original: step.original,
},
now: input.now,
});
});
}
function upsertCreatedPlanTasks(input: {
planId: string;
payload: Record<string, unknown>;
@@ -885,7 +948,7 @@ function upsertCreatedPlanTasks(input: {
commandId: string;
now: string;
}): void {
extractPlanTaskInputs(input.payload).forEach((task, index) => {
extractPlanTaskInputs(input.payload, input.planId).forEach((task, index) => {
const title = task.title || `计划任务 ${index + 1}`;
const taskId = task.id || `${input.planId}:task:${safeId(`${index + 1}-${title}`)}`;
upsertTask({
@@ -908,7 +971,60 @@ function upsertCreatedPlanTasks(input: {
});
}
function extractPlanTaskInputs(payload: Record<string, unknown>): Array<{
function extractPlanStepInputs(
payload: Record<string, unknown>,
planId: string,
): Array<{
id: string;
title: string;
status: string;
seq: number;
dependsOn: string[];
preferredSkillIds: string[];
original: unknown;
}> {
const steps = unknownArray(payload.steps)
.map((step, index) => planStepInputFromRecord(objectRecord(step), index, planId))
.filter((step): step is NonNullable<typeof step> => Boolean(step));
if (steps.length > 0) return steps;
if (unknownArray(payload.tasks).length === 0) return [];
return [{
id: `${planId}:step:default`,
title: "默认执行步骤",
status: "pending",
seq: 1,
dependsOn: [],
preferredSkillIds: [],
original: { source: "default_step", task_count: unknownArray(payload.tasks).length },
}];
}
function planStepInputFromRecord(
record: Record<string, unknown> | null,
index: number,
planId: string,
): ReturnType<typeof extractPlanStepInputs>[number] | null {
if (!record) return null;
const title = stringValue(record.title ?? record.name ?? record.summary)
|| `计划步骤 ${index + 1}`;
const stepId = stringValue(record.step_id ?? record.stepId ?? record.id)
|| `${planId}:step:${index + 1}-${safeId(title)}`;
return {
id: stepId,
title,
status: stringValue(record.status) || "pending",
seq: typeof record.seq === "number" ? record.seq : Number(record.seq) || index + 1,
dependsOn: normalizeSkillIds(unknownArray(record.depends_on ?? record.dependsOn)),
preferredSkillIds: normalizeSkillIds([
...unknownArray(record.skills),
...unknownArray(record.preferred_skills),
...unknownArray(record.preferredSkills),
]),
original: record,
};
}
function extractPlanTaskInputs(payload: Record<string, unknown>, planId: string): Array<{
id: string;
stepId: string;
title: string;
@@ -918,9 +1034,11 @@ function extractPlanTaskInputs(payload: Record<string, unknown>): Array<{
skills: string[];
original: unknown;
}> {
const rawSteps = unknownArray(payload.steps);
const defaultStepId = rawSteps.length > 0 ? "" : `${planId}:step:default`;
const explicitTasks = unknownArray(payload.tasks)
.map((task, index) => planTaskInputFromRecord(objectRecord(task), index, ""));
const stepTasks = unknownArray(payload.steps).flatMap((step, stepIndex) => {
.map((task, index) => planTaskInputFromRecord(objectRecord(task), index, defaultStepId));
const stepTasks = rawSteps.flatMap((step, stepIndex) => {
const stepRecord = objectRecord(step);
if (!stepRecord) return [];
const nestedTasks = unknownArray(stepRecord.tasks);
@@ -1091,6 +1209,12 @@ function parseStoredPayload(value: unknown): unknown {
}
}
function parseStringArray(value: unknown): string[] {
const parsed = parseStoredPayload(value);
if (!Array.isArray(parsed)) return [];
return parsed.filter((item): item is string => typeof item === "string" && item.trim().length > 0);
}
function toPlanRecord(row: Record<string, unknown>): DigitalEmployeePlanRecord {
return {
id: stringField(row, "id"),
@@ -1125,6 +1249,25 @@ function toTaskRecord(row: Record<string, unknown>): DigitalEmployeeTaskRecord {
};
}
function toPlanStepRecord(row: Record<string, unknown>): DigitalEmployeePlanStepRecord {
return {
id: stringField(row, "id"),
remoteId: optionalStringField(row, "remote_id"),
planId: stringField(row, "plan_id"),
title: stringField(row, "title"),
status: stringField(row, "status"),
seq: typeof row.seq === "number" ? row.seq : Number(row.seq) || 0,
dependsOn: parseStringArray(row.depends_on),
preferredSkillIds: parseStringArray(row.preferred_skill_ids),
payload: parseStoredPayload(row.payload),
syncStatus: stringField(row, "sync_status"),
lastSyncedAt: optionalStringField(row, "last_synced_at"),
syncError: optionalStringField(row, "sync_error"),
createdAt: stringField(row, "created_at"),
updatedAt: stringField(row, "updated_at"),
};
}
function toRunRecord(row: Record<string, unknown>): DigitalEmployeeRunRecord {
return {
id: stringField(row, "id"),
@@ -1279,6 +1422,52 @@ function upsertPlan(input: {
markOutbox("plan", input.id, "upsert", input, input.now);
}
function upsertPlanStep(input: {
id: string;
planId: string;
title: string;
status: string;
seq: number;
dependsOn: string[];
preferredSkillIds: string[];
payload: unknown;
now: string;
}): void {
const db = getDigitalEmployeeDb();
if (!db) return;
db.prepare(`
INSERT INTO digital_plan_steps (
id, plan_id, title, status, seq, depends_on, preferred_skill_ids,
payload, sync_status, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?)
ON CONFLICT(id) DO UPDATE SET
plan_id = excluded.plan_id,
title = excluded.title,
status = excluded.status,
seq = excluded.seq,
depends_on = excluded.depends_on,
preferred_skill_ids = excluded.preferred_skill_ids,
payload = excluded.payload,
sync_status = CASE
WHEN digital_plan_steps.sync_status = 'synced' THEN 'pending'
ELSE digital_plan_steps.sync_status
END,
updated_at = excluded.updated_at
`).run(
input.id,
input.planId,
input.title,
input.status,
input.seq,
stringify(input.dependsOn),
stringify(input.preferredSkillIds),
stringify(input.payload),
input.now,
input.now,
);
markOutbox("plan_step", input.id, "upsert", input, input.now);
}
function upsertTask(input: {
id: string;
planId: string;