feat(client): materialize restarted plans

This commit is contained in:
baiyanyun
2026-06-08 18:58:39 +08:00
parent 913c6da8f3
commit 451cda7aa6
6 changed files with 233 additions and 45 deletions

View File

@@ -739,8 +739,6 @@ export function recordDigitalEmployeeGovernanceCommand(
taskId: `governance-task-${safeId(commandId)}`,
runId: `governance-run-${safeId(commandId)}`,
};
const title = stringValue(payload.title ?? result.title)
|| governanceCommandTitle(command.commandKind);
const status = governanceCommandStatus(command);
const payloadEnvelope = {
source: "qimingclaw-governance-command",
@@ -759,13 +757,20 @@ export function recordDigitalEmployeeGovernanceCommand(
const db = getDigitalEmployeeDb();
if (!db) return ids;
const existingPlan = readExistingPlanSummary(planId);
const title = stringValue(payload.title ?? result.title)
|| existingPlan?.title
|| governanceCommandTitle(command.commandKind);
const objective = stringValue(payload.objective ?? payload.description ?? payload.requested_action)
|| existingPlan?.objective
|| command.message
|| title;
const tx = db.transaction(() => {
upsertPlan({
id: ids.planId,
title,
objective: stringValue(payload.objective ?? payload.description ?? payload.requested_action)
|| command.message
|| title,
objective,
status,
payload: payloadEnvelope,
now,
@@ -779,6 +784,15 @@ export function recordDigitalEmployeeGovernanceCommand(
payload: payloadEnvelope,
now,
});
if (command.commandKind === "create_plan") {
upsertCreatedPlanTasks({
planId: ids.planId,
payload,
status: "pending",
commandId,
now,
});
}
upsertRun({
id: ids.runId,
planId: ids.planId,
@@ -832,6 +846,122 @@ function governanceCommandTitle(commandKind: string): string {
}
}
function readExistingPlanSummary(planId: string): { title: string; objective: string } | null {
const db = getDigitalEmployeeDb();
if (!db) return null;
try {
const row = db.prepare("SELECT title, objective FROM digital_plans WHERE id = ?").get(planId) as Record<string, unknown> | undefined;
if (!row) return null;
return {
title: stringValue(row.title),
objective: stringValue(row.objective),
};
} catch {
return null;
}
}
function upsertCreatedPlanTasks(input: {
planId: string;
payload: Record<string, unknown>;
status: string;
commandId: string;
now: string;
}): void {
extractPlanTaskInputs(input.payload).forEach((task, index) => {
const title = task.title || `计划任务 ${index + 1}`;
const taskId = task.id || `${input.planId}:task:${safeId(`${index + 1}-${title}`)}`;
upsertTask({
id: taskId,
planId: input.planId,
title,
status: input.status,
assignedSkillId: task.assignedSkillId,
payload: {
source: "qimingclaw-create-plan",
commandId: input.commandId,
stepId: task.stepId,
description: task.description,
dependsOn: task.dependsOn,
skills: task.skills,
original: task.original,
},
now: input.now,
});
});
}
function extractPlanTaskInputs(payload: Record<string, unknown>): Array<{
id: string;
stepId: string;
title: string;
description: string;
assignedSkillId: string;
dependsOn: string[];
skills: string[];
original: unknown;
}> {
const explicitTasks = unknownArray(payload.tasks)
.map((task, index) => planTaskInputFromRecord(objectRecord(task), index, ""));
const stepTasks = unknownArray(payload.steps).flatMap((step, stepIndex) => {
const stepRecord = objectRecord(step);
if (!stepRecord) return [];
const nestedTasks = unknownArray(stepRecord.tasks);
if (nestedTasks.length > 0) {
return nestedTasks.map((task, taskIndex) => planTaskInputFromRecord(
objectRecord(task),
taskIndex,
stringValue(stepRecord.step_id ?? stepRecord.id) || `step-${stepIndex + 1}`,
stepRecord,
));
}
return [planTaskInputFromRecord(stepRecord, stepIndex, stringValue(stepRecord.step_id ?? stepRecord.id) || `step-${stepIndex + 1}`)];
});
return dedupeByKey(
[...explicitTasks, ...stepTasks].filter((task): task is NonNullable<typeof task> => Boolean(task)),
(task) => task.id || `${task.stepId}:${task.title}`,
);
}
function planTaskInputFromRecord(
record: Record<string, unknown> | null,
index: number,
fallbackStepId: string,
parentStep?: Record<string, unknown>,
): ReturnType<typeof extractPlanTaskInputs>[number] | null {
if (!record) return null;
const stepId = stringValue(record.step_id ?? record.stepId) || fallbackStepId;
const title = stringValue(record.title ?? record.name ?? record.task_title)
|| stringValue(parentStep?.title ?? parentStep?.name)
|| `计划任务 ${index + 1}`;
const description = stringValue(record.description ?? record.summary)
|| stringValue(parentStep?.description ?? parentStep?.summary)
|| title;
const skills = normalizeSkillIds([
...unknownArray(record.skills),
...unknownArray(record.preferred_skills),
...unknownArray(parentStep?.skills),
...unknownArray(parentStep?.preferred_skills),
stringValue(record.assigned_skill_id ?? record.assignedSkillId),
]);
return {
id: stringValue(record.task_id ?? record.id),
stepId,
title,
description,
assignedSkillId: skills[0] ?? "qimingclaw-plan-template",
dependsOn: normalizeSkillIds(unknownArray(record.depends_on ?? parentStep?.depends_on)),
skills,
original: record,
};
}
function normalizeSkillIds(values: unknown[]): string[] {
return [...new Set(values
.map((value) => stringValue(value))
.filter(Boolean))];
}
function governanceCommandStatus(
command: DigitalEmployeeGovernanceCommandRecord,
): string {