吸收数字员工步骤治理闭环
This commit is contained in:
@@ -174,6 +174,9 @@ interface QimingclawGovernanceCommandRecord {
|
||||
commandId: string;
|
||||
correlationId?: string;
|
||||
planId?: string | null;
|
||||
taskId?: string | null;
|
||||
runId?: string | null;
|
||||
stepId?: string | null;
|
||||
scheduleId?: string | null;
|
||||
accepted?: boolean;
|
||||
message?: string;
|
||||
@@ -695,6 +698,20 @@ async function recordGovernanceCommand(
|
||||
|| stringFromUnknown(body.context_refs?.plan_id)
|
||||
|| stringFromUnknown(body.payload?.plan_id)
|
||||
|| null,
|
||||
taskId: stringFromUnknown(response.result?.task_id)
|
||||
|| stringFromUnknown(body.context_refs?.task_id)
|
||||
|| stringFromUnknown(body.payload?.task_id)
|
||||
|| null,
|
||||
runId: stringFromUnknown(response.result?.run_id)
|
||||
|| stringFromUnknown(body.context_refs?.run_id)
|
||||
|| stringFromUnknown(body.payload?.run_id)
|
||||
|| stringFromUnknown(body.payload?.latest_run_id)
|
||||
|| null,
|
||||
stepId: stringFromUnknown(response.result?.step_id)
|
||||
|| stringFromUnknown(body.context_refs?.step_id)
|
||||
|| stringFromUnknown(body.payload?.step_id)
|
||||
|| stringFromUnknown(body.payload?.stepId)
|
||||
|| null,
|
||||
scheduleId: stringFromUnknown(response.result?.schedule_id)
|
||||
|| stringFromUnknown(body.context_refs?.schedule_id)
|
||||
|| stringFromUnknown(body.payload?.schedule_id)
|
||||
@@ -1041,12 +1058,16 @@ function latestRuntimeTime(plan: QimingclawPlanRecord, tasks: QimingclawTaskReco
|
||||
function taskStatusTone(status: string): 'running' | 'success' | 'danger' | 'waiting' {
|
||||
const normalized = status.toLowerCase();
|
||||
if (['completed', 'succeeded', 'success', 'done'].includes(normalized)) return 'success';
|
||||
if (['failed', 'error', 'rejected'].includes(normalized)) return 'danger';
|
||||
if (['failed', 'error', 'rejected', 'cancelled'].includes(normalized)) return 'danger';
|
||||
if (['running', 'active', 'leased', 'syncing'].includes(normalized)) return 'running';
|
||||
return 'waiting';
|
||||
}
|
||||
|
||||
function taskStatusLabel(status: string): string {
|
||||
const normalized = status.toLowerCase();
|
||||
if (normalized === 'skipped') return '已跳过';
|
||||
if (normalized === 'cancelled') return '已取消';
|
||||
if (normalized === 'paused') return '已暂停';
|
||||
const tone = taskStatusTone(status);
|
||||
if (tone === 'success') return '已完成';
|
||||
if (tone === 'danger') return '异常';
|
||||
@@ -1107,25 +1128,39 @@ function buildTaskAgents(snapshot: QimingclawSnapshot | null): DigitalEmployeeTa
|
||||
return runtimeTasks(snapshot).map((task) => {
|
||||
const latestRun = latestRunForTask(runs, task.id);
|
||||
const tone = taskStatusTone(task.status);
|
||||
const normalizedStatus = task.status.toLowerCase();
|
||||
const stepId = taskStepId(task);
|
||||
return {
|
||||
agent_id: `task-agent-${slugifyIdPart(task.id)}`,
|
||||
task_id: task.id,
|
||||
plan_id: task.planId ?? null,
|
||||
step_id: stepId,
|
||||
title: task.title,
|
||||
execution_state: task.status,
|
||||
status_label: taskStatusLabel(task.status),
|
||||
status_tone: tone,
|
||||
assigned_skill_id: task.assignedSkillId ?? null,
|
||||
latest_run_id: latestRun?.id ?? null,
|
||||
can_retry: tone === 'danger',
|
||||
can_skip: tone === 'danger' || tone === 'waiting',
|
||||
can_cancel: tone === 'running',
|
||||
next_action: tone === 'danger' ? 'retry_or_skip' : tone === 'running' ? 'watch_progress' : tone === 'success' ? 'collect_artifacts' : 'start_run',
|
||||
can_retry: ['failed', 'cancelled', 'skipped'].includes(normalizedStatus),
|
||||
can_skip: ['pending', 'running'].includes(normalizedStatus),
|
||||
can_cancel: ['pending', 'running', 'paused'].includes(normalizedStatus),
|
||||
next_action: taskAgentNextAction(normalizedStatus, tone),
|
||||
updated_at: latestRun?.updatedAt ?? task.updatedAt,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function taskAgentNextAction(
|
||||
status: string,
|
||||
tone: ReturnType<typeof taskStatusTone>,
|
||||
): string {
|
||||
if (['failed', 'cancelled', 'skipped'].includes(status)) return 'retry_or_review';
|
||||
if (status === 'paused') return 'resume_or_cancel';
|
||||
if (tone === 'running') return 'watch_progress';
|
||||
if (tone === 'success') return 'collect_artifacts';
|
||||
return 'start_run';
|
||||
}
|
||||
|
||||
function latestRunForTask(runs: QimingclawRunRecord[], taskId: string): QimingclawRunRecord | null {
|
||||
const sorted = runs
|
||||
.filter((run) => run.taskId === taskId)
|
||||
@@ -2457,6 +2492,12 @@ async function handleGovernanceCommand(
|
||||
const taskId = stringFromUnknown(body.context_refs?.task_id)
|
||||
|| stringFromUnknown(body.payload?.task_id)
|
||||
|| stringFromUnknown(body.payload?.original_task_id);
|
||||
const runId = stringFromUnknown(body.context_refs?.run_id)
|
||||
|| stringFromUnknown(body.payload?.run_id)
|
||||
|| stringFromUnknown(body.payload?.latest_run_id);
|
||||
const stepId = stringFromUnknown(body.context_refs?.step_id)
|
||||
|| stringFromUnknown(body.payload?.step_id)
|
||||
|| stringFromUnknown(body.payload?.stepId);
|
||||
const scheduleId = stringFromUnknown(body.context_refs?.schedule_id)
|
||||
|| stringFromUnknown(body.payload?.schedule_id);
|
||||
|
||||
@@ -2529,9 +2570,14 @@ async function handleGovernanceCommand(
|
||||
|
||||
if (['retry_task', 'skip_task', 'pause_task', 'resume_task', 'provide_task_input', 'cancel_task', 'start_run', 'cancel_run'].includes(body.command_kind)) {
|
||||
const targetTask = taskId || runtimeTasks(snapshot)[0]?.id || 'qimingclaw-task-agent';
|
||||
const runtimeTask = runtimeTasks(snapshot).find((task) => task.id === targetTask);
|
||||
const targetRunId = runId || latestRunForTask(runtimeRuns(snapshot), targetTask)?.id || null;
|
||||
const targetStepId = stepId || (runtimeTask ? taskStepId(runtimeTask) : null);
|
||||
const response = governanceAccepted(commandId, correlationId, `已接受 ${body.command_kind},并记录到 qimingclaw Task Agent 控制平面。`, {
|
||||
plan_id: planId || runtimeTasks(snapshot).find((task) => task.id === targetTask)?.planId || 'qimingclaw-task-agent-plan',
|
||||
plan_id: planId || runtimeTask?.planId || 'qimingclaw-task-agent-plan',
|
||||
task_id: targetTask,
|
||||
run_id: targetRunId,
|
||||
step_id: targetStepId,
|
||||
task_agent_id: `task-agent-${slugifyIdPart(targetTask)}`,
|
||||
status: taskGovernanceStatus(body.command_kind),
|
||||
next_action: taskGovernanceNextAction(body.command_kind),
|
||||
|
||||
@@ -1356,6 +1356,7 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
|
||||
plan_id: agent.plan_id,
|
||||
task_id: agent.task_id,
|
||||
run_id: agent.latest_run_id,
|
||||
step_id: agent.step_id,
|
||||
task_agent_id: agent.agent_id,
|
||||
},
|
||||
payload: {
|
||||
@@ -1363,6 +1364,7 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
|
||||
task_agent_id: agent.agent_id,
|
||||
task_id: agent.task_id,
|
||||
plan_id: agent.plan_id,
|
||||
step_id: agent.step_id,
|
||||
latest_run_id: agent.latest_run_id,
|
||||
title: agent.title,
|
||||
requested_action: command,
|
||||
|
||||
@@ -629,6 +629,7 @@ export interface DigitalEmployeeTaskAgentState {
|
||||
agent_id: string;
|
||||
task_id: string;
|
||||
plan_id?: string | null;
|
||||
step_id?: string | null;
|
||||
title: string;
|
||||
execution_state: string;
|
||||
status_label: string;
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -16,7 +16,7 @@
|
||||
console.warn("[qimingclaw] digital employee auth bootstrap skipped", error);
|
||||
}
|
||||
</script>
|
||||
<script type="module" crossorigin src="./assets/index-CQeDXxH3.js"></script>
|
||||
<script type="module" crossorigin src="./assets/index-DksV4puj.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-CKIQdvco.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -425,8 +425,241 @@ describe("digital employee state service", () => {
|
||||
scheduleRuns: [expect.objectContaining({ scheduleId: "schedule-morning", triggerKind: "manual" })],
|
||||
});
|
||||
});
|
||||
|
||||
it("applies skip governance to the real task, plan step, event, and outbox", async () => {
|
||||
const { recordDigitalEmployeeGovernanceCommand } = await import("./stateService");
|
||||
|
||||
createGovernableTask();
|
||||
|
||||
const ids = recordDigitalEmployeeGovernanceCommand({
|
||||
commandKind: "skip_task",
|
||||
commandId: "command-skip-task",
|
||||
planId: "plan-governance",
|
||||
taskId: "task-governance",
|
||||
runId: "run-governance",
|
||||
stepId: "step-governance",
|
||||
accepted: true,
|
||||
message: "跳过当前任务",
|
||||
payload: { reason: "用户选择跳过" },
|
||||
result: { task_id: "task-governance", run_id: "run-governance", step_id: "step-governance" },
|
||||
});
|
||||
|
||||
expect(ids).toEqual({
|
||||
planId: "plan-governance",
|
||||
taskId: "task-governance",
|
||||
runId: "run-governance",
|
||||
stepId: "step-governance",
|
||||
});
|
||||
expect(mockState.db?.tasks.get("task-governance")).toMatchObject({
|
||||
status: "skipped",
|
||||
sync_status: "pending",
|
||||
});
|
||||
expect(mockState.db?.planSteps.get("step-governance")).toMatchObject({
|
||||
status: "skipped",
|
||||
sync_status: "pending",
|
||||
});
|
||||
expect(mockState.db?.runs.get("run-governance")).toMatchObject({
|
||||
status: "skipped",
|
||||
finished_at: "2026-06-07T08:00:00.000Z",
|
||||
sync_status: "pending",
|
||||
});
|
||||
expect(JSON.parse(mockState.db?.tasks.get("task-governance")?.payload ?? "{}")).toMatchObject({
|
||||
lastGovernanceCommand: {
|
||||
commandKind: "skip_task",
|
||||
taskId: "task-governance",
|
||||
stepId: "step-governance",
|
||||
status: "skipped",
|
||||
},
|
||||
});
|
||||
expect(mockState.db?.events.get("run-governance:skip_task:command-skip-task")).toMatchObject({
|
||||
kind: "governance_skip_task",
|
||||
message: "跳过当前任务",
|
||||
task_id: "task-governance",
|
||||
});
|
||||
expect(mockState.db?.outbox.get("task:upsert:task-governance")).toMatchObject({ entity_type: "task" });
|
||||
expect(mockState.db?.outbox.get("plan_step:upsert:step-governance")).toMatchObject({ entity_type: "plan_step" });
|
||||
expect(mockState.db?.outbox.get("run:upsert:run-governance")).toMatchObject({ entity_type: "run" });
|
||||
});
|
||||
|
||||
it("creates a retry run and preserves the failed source run", async () => {
|
||||
const { recordDigitalEmployeeGovernanceCommand } = await import("./stateService");
|
||||
|
||||
createGovernableTask({ taskStatus: "failed", runStatus: "failed", finishedAt: "2026-06-07T07:30:00.000Z" });
|
||||
|
||||
const ids = recordDigitalEmployeeGovernanceCommand({
|
||||
commandKind: "retry_task",
|
||||
commandId: "command-retry-task",
|
||||
planId: "plan-governance",
|
||||
taskId: "task-governance",
|
||||
runId: "run-governance",
|
||||
stepId: "step-governance",
|
||||
accepted: true,
|
||||
payload: { reason: "失败后重试", prompt: "重新执行任务" },
|
||||
result: { task_id: "task-governance", run_id: "run-governance", step_id: "step-governance" },
|
||||
});
|
||||
|
||||
expect(ids.runId).toBe("task-governance:run:retry_task:command-retry-task");
|
||||
expect(mockState.db?.tasks.get("task-governance")).toMatchObject({ status: "running" });
|
||||
expect(mockState.db?.planSteps.get("step-governance")).toMatchObject({ status: "running" });
|
||||
expect(mockState.db?.runs.get("run-governance")).toMatchObject({
|
||||
status: "failed",
|
||||
finished_at: "2026-06-07T07:30:00.000Z",
|
||||
});
|
||||
expect(mockState.db?.runs.get(ids.runId)).toMatchObject({
|
||||
plan_id: "plan-governance",
|
||||
task_id: "task-governance",
|
||||
status: "running",
|
||||
finished_at: null,
|
||||
});
|
||||
expect(JSON.parse(mockState.db?.runs.get(ids.runId)?.payload ?? "{}")).toMatchObject({
|
||||
commandKind: "retry_task",
|
||||
sourceRunId: "run-governance",
|
||||
attemptNo: 2,
|
||||
});
|
||||
expect(mockState.db?.outbox.get(`run:upsert:${ids.runId}`)).toMatchObject({ entity_type: "run" });
|
||||
});
|
||||
|
||||
it("cancels the current task run without deleting run history", async () => {
|
||||
const { recordDigitalEmployeeGovernanceCommand } = await import("./stateService");
|
||||
|
||||
createGovernableTask();
|
||||
mockState.db?.runs.set("run-history", {
|
||||
id: "run-history",
|
||||
plan_id: "plan-governance",
|
||||
task_id: "task-governance",
|
||||
status: "completed",
|
||||
started_at: "2026-06-07T07:00:00.000Z",
|
||||
finished_at: "2026-06-07T07:05:00.000Z",
|
||||
payload: JSON.stringify({ source: "history" }),
|
||||
sync_status: "synced",
|
||||
created_at: "2026-06-07T07:00:00.000Z",
|
||||
updated_at: "2026-06-07T07:05:00.000Z",
|
||||
});
|
||||
|
||||
recordDigitalEmployeeGovernanceCommand({
|
||||
commandKind: "cancel_task",
|
||||
commandId: "command-cancel-task",
|
||||
planId: "plan-governance",
|
||||
taskId: "task-governance",
|
||||
runId: "run-governance",
|
||||
stepId: "step-governance",
|
||||
accepted: true,
|
||||
payload: { reason: "用户取消" },
|
||||
});
|
||||
|
||||
expect(mockState.db?.tasks.get("task-governance")).toMatchObject({ status: "cancelled" });
|
||||
expect(mockState.db?.planSteps.get("step-governance")).toMatchObject({ status: "cancelled" });
|
||||
expect(mockState.db?.runs.get("run-governance")).toMatchObject({
|
||||
status: "cancelled",
|
||||
finished_at: "2026-06-07T08:00:00.000Z",
|
||||
});
|
||||
expect(mockState.db?.runs.get("run-history")).toMatchObject({
|
||||
status: "completed",
|
||||
finished_at: "2026-06-07T07:05:00.000Z",
|
||||
});
|
||||
});
|
||||
|
||||
it("resumes paused tasks and records provided task input", async () => {
|
||||
const { recordDigitalEmployeeGovernanceCommand } = await import("./stateService");
|
||||
|
||||
createGovernableTask({ taskStatus: "paused", stepStatus: "paused", runStatus: "paused" });
|
||||
|
||||
recordDigitalEmployeeGovernanceCommand({
|
||||
commandKind: "provide_task_input",
|
||||
commandId: "command-provide-input",
|
||||
planId: "plan-governance",
|
||||
taskId: "task-governance",
|
||||
runId: "run-governance",
|
||||
stepId: "step-governance",
|
||||
accepted: true,
|
||||
payload: { input: "继续使用最新客户名单" },
|
||||
});
|
||||
|
||||
expect(mockState.db?.tasks.get("task-governance")).toMatchObject({ status: "paused" });
|
||||
expect(JSON.parse(mockState.db?.tasks.get("task-governance")?.payload ?? "{}")).toMatchObject({
|
||||
lastGovernanceCommand: {
|
||||
commandKind: "provide_task_input",
|
||||
payload: { input: "继续使用最新客户名单" },
|
||||
},
|
||||
});
|
||||
|
||||
recordDigitalEmployeeGovernanceCommand({
|
||||
commandKind: "resume_task",
|
||||
commandId: "command-resume-task",
|
||||
planId: "plan-governance",
|
||||
taskId: "task-governance",
|
||||
runId: "run-governance",
|
||||
stepId: "step-governance",
|
||||
accepted: true,
|
||||
payload: { reason: "资料已补充" },
|
||||
});
|
||||
|
||||
expect(mockState.db?.tasks.get("task-governance")).toMatchObject({ status: "running" });
|
||||
expect(mockState.db?.planSteps.get("step-governance")).toMatchObject({ status: "running" });
|
||||
expect(mockState.db?.runs.get("run-governance")).toMatchObject({ status: "running" });
|
||||
expect(mockState.db?.events.get("run-governance:provide_task_input:command-provide-input")).toMatchObject({
|
||||
kind: "governance_provide_task_input",
|
||||
});
|
||||
expect(mockState.db?.events.get("run-governance:resume_task:command-resume-task")).toMatchObject({
|
||||
kind: "governance_resume_task",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createGovernableTask(options: {
|
||||
taskStatus?: string;
|
||||
stepStatus?: string;
|
||||
runStatus?: string;
|
||||
finishedAt?: string | null;
|
||||
} = {}): void {
|
||||
mockState.db?.plans.set("plan-governance", {
|
||||
id: "plan-governance",
|
||||
title: "步骤治理计划",
|
||||
objective: "验证任务治理闭环",
|
||||
status: "running",
|
||||
payload: JSON.stringify({ source: "test" }),
|
||||
sync_status: "synced",
|
||||
created_at: "2026-06-07T07:00:00.000Z",
|
||||
updated_at: "2026-06-07T07:00:00.000Z",
|
||||
});
|
||||
mockState.db?.planSteps.set("step-governance", {
|
||||
id: "step-governance",
|
||||
plan_id: "plan-governance",
|
||||
title: "执行治理步骤",
|
||||
status: options.stepStatus ?? options.taskStatus ?? "running",
|
||||
seq: 1,
|
||||
depends_on: JSON.stringify([]),
|
||||
preferred_skill_ids: JSON.stringify(["qimingclaw-task-agent"]),
|
||||
payload: JSON.stringify({ source: "test" }),
|
||||
sync_status: "synced",
|
||||
created_at: "2026-06-07T07:00:00.000Z",
|
||||
updated_at: "2026-06-07T07:00:00.000Z",
|
||||
});
|
||||
mockState.db?.tasks.set("task-governance", {
|
||||
id: "task-governance",
|
||||
plan_id: "plan-governance",
|
||||
title: "执行治理任务",
|
||||
status: options.taskStatus ?? "running",
|
||||
assigned_skill_id: "qimingclaw-task-agent",
|
||||
payload: JSON.stringify({ source: "test", stepId: "step-governance" }),
|
||||
sync_status: "synced",
|
||||
created_at: "2026-06-07T07:00:00.000Z",
|
||||
updated_at: "2026-06-07T07:00:00.000Z",
|
||||
});
|
||||
mockState.db?.runs.set("run-governance", {
|
||||
id: "run-governance",
|
||||
plan_id: "plan-governance",
|
||||
task_id: "task-governance",
|
||||
status: options.runStatus ?? "running",
|
||||
started_at: "2026-06-07T07:10:00.000Z",
|
||||
finished_at: options.finishedAt ?? 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 {
|
||||
id: string;
|
||||
title: string;
|
||||
@@ -615,9 +848,31 @@ class TestDb {
|
||||
}
|
||||
|
||||
private get(sql: string, args: unknown[]): unknown {
|
||||
if (sql.startsWith("SELECT COUNT(*) AS count FROM digital_runs")) {
|
||||
return { count: Array.from(this.runs.values()).filter((run) => run.task_id === args[0]).length };
|
||||
}
|
||||
if (sql.startsWith("SELECT title, objective FROM digital_plans")) {
|
||||
return this.plans.get(args[0] as string);
|
||||
}
|
||||
if (sql.includes("FROM digital_tasks") && sql.includes("WHERE id = ?")) {
|
||||
return this.tasks.get(args[0] as string);
|
||||
}
|
||||
if (sql.includes("FROM digital_runs") && sql.includes("WHERE id = ?")) {
|
||||
return this.runs.get(args[0] as string);
|
||||
}
|
||||
if (sql.includes("FROM digital_runs") && sql.includes("WHERE task_id = ?")) {
|
||||
return Array.from(this.runs.values())
|
||||
.filter((run) => run.task_id === args[0])
|
||||
.sort((left, right) => right.updated_at.localeCompare(left.updated_at) || right.created_at.localeCompare(left.created_at))[0];
|
||||
}
|
||||
if (sql.startsWith("SELECT id FROM digital_plan_steps")) {
|
||||
return Array.from(this.planSteps.values())
|
||||
.filter((step) => step.plan_id === args[0])
|
||||
.sort((left, right) => left.seq - right.seq || left.created_at.localeCompare(right.created_at))[0];
|
||||
}
|
||||
if (sql.includes("FROM digital_plan_steps") && sql.includes("WHERE id = ?")) {
|
||||
return this.planSteps.get(args[0] as string);
|
||||
}
|
||||
if (sql.startsWith("SELECT id, plan_id, task_id, run_id, title, status, payload, created_at FROM digital_approvals")) {
|
||||
return this.approvals.get(args[0] as string);
|
||||
}
|
||||
@@ -826,6 +1081,67 @@ class TestDb {
|
||||
return { changes: 1 };
|
||||
}
|
||||
|
||||
if (sql.startsWith("UPDATE digital_tasks")) {
|
||||
const [status, payload, updatedAt, id] = args as [string, string, string, string];
|
||||
const previous = this.tasks.get(id);
|
||||
if (previous) {
|
||||
this.tasks.set(id, {
|
||||
...previous,
|
||||
status,
|
||||
payload,
|
||||
updated_at: updatedAt,
|
||||
sync_status: "pending",
|
||||
});
|
||||
}
|
||||
return { changes: previous ? 1 : 0 };
|
||||
}
|
||||
|
||||
if (sql.startsWith("UPDATE digital_plan_steps")) {
|
||||
const [status, payload, updatedAt, id] = args as [string, string, string, string];
|
||||
const previous = this.planSteps.get(id);
|
||||
if (previous) {
|
||||
this.planSteps.set(id, {
|
||||
...previous,
|
||||
status,
|
||||
payload,
|
||||
updated_at: updatedAt,
|
||||
sync_status: "pending",
|
||||
});
|
||||
}
|
||||
return { changes: previous ? 1 : 0 };
|
||||
}
|
||||
|
||||
if (sql.startsWith("UPDATE digital_runs") && sql.includes("payload") && sql.includes("finished_at")) {
|
||||
const [status, payload, finishedAt, updatedAt, id] = args as [string, string, string, string, string];
|
||||
const previous = this.runs.get(id);
|
||||
if (previous) {
|
||||
this.runs.set(id, {
|
||||
...previous,
|
||||
status,
|
||||
payload,
|
||||
finished_at: finishedAt,
|
||||
updated_at: updatedAt,
|
||||
sync_status: "pending",
|
||||
});
|
||||
}
|
||||
return { changes: previous ? 1 : 0 };
|
||||
}
|
||||
|
||||
if (sql.startsWith("UPDATE digital_runs") && sql.includes("payload")) {
|
||||
const [status, payload, updatedAt, id] = args as [string, string, string, string];
|
||||
const previous = this.runs.get(id);
|
||||
if (previous) {
|
||||
this.runs.set(id, {
|
||||
...previous,
|
||||
status,
|
||||
payload,
|
||||
updated_at: updatedAt,
|
||||
sync_status: "pending",
|
||||
});
|
||||
}
|
||||
return { changes: previous ? 1 : 0 };
|
||||
}
|
||||
|
||||
if (sql.startsWith("UPDATE digital_runs")) {
|
||||
const [status, finishedAt, updatedAt, id] = args as [string, string, string, string];
|
||||
const previous = this.runs.get(id);
|
||||
|
||||
@@ -305,6 +305,9 @@ export interface DigitalEmployeeGovernanceCommandRecord {
|
||||
commandId: string;
|
||||
correlationId?: string;
|
||||
planId?: string | null;
|
||||
taskId?: string | null;
|
||||
runId?: string | null;
|
||||
stepId?: string | null;
|
||||
scheduleId?: string | null;
|
||||
accepted?: boolean;
|
||||
message?: string;
|
||||
@@ -1155,6 +1158,14 @@ export function recordDigitalEmployeeGovernanceCommand(
|
||||
const commandId = command.commandId || `governance-${Date.now()}`;
|
||||
const result = objectRecord(command.result) ?? {};
|
||||
const payload = objectRecord(command.payload) ?? {};
|
||||
if (isTaskGovernanceCommand(command.commandKind)) {
|
||||
return applyDigitalEmployeeTaskGovernanceCommand({
|
||||
...command,
|
||||
commandId,
|
||||
payload,
|
||||
result,
|
||||
});
|
||||
}
|
||||
const planId = stringValue(command.planId)
|
||||
|| stringValue(result.plan_id)
|
||||
|| stringValue(payload.plan_id)
|
||||
@@ -1267,6 +1278,152 @@ export function recordDigitalEmployeeGovernanceCommand(
|
||||
return ids;
|
||||
}
|
||||
|
||||
export function applyDigitalEmployeeTaskGovernanceCommand(
|
||||
command: DigitalEmployeeGovernanceCommandRecord,
|
||||
): { planId: string; taskId: string; runId: string; stepId?: string | null } {
|
||||
const now = new Date().toISOString();
|
||||
const commandId = command.commandId || `governance-${Date.now()}`;
|
||||
const result = objectRecord(command.result) ?? {};
|
||||
const payload = objectRecord(command.payload) ?? {};
|
||||
const db = getDigitalEmployeeDb();
|
||||
|
||||
const requestedRunId = stringValue(command.runId)
|
||||
|| stringValue(result.run_id)
|
||||
|| stringValue(result.latest_run_id)
|
||||
|| stringValue(payload.run_id)
|
||||
|| stringValue(payload.latest_run_id);
|
||||
const requestedTaskId = stringValue(command.taskId)
|
||||
|| stringValue(result.task_id)
|
||||
|| stringValue(payload.task_id)
|
||||
|| stringValue(payload.original_task_id);
|
||||
const requestedPlanId = stringValue(command.planId)
|
||||
|| stringValue(result.plan_id)
|
||||
|| stringValue(payload.plan_id)
|
||||
|| stringValue(payload.original_plan_id);
|
||||
const requestedStepId = stringValue(command.stepId)
|
||||
|| stringValue(result.step_id)
|
||||
|| stringValue(payload.step_id)
|
||||
|| stringValue(payload.stepId);
|
||||
|
||||
if (!db) {
|
||||
const fallbackTaskId = requestedTaskId || `governance-task-${safeId(commandId)}`;
|
||||
return {
|
||||
planId: requestedPlanId || "qimingclaw-task-agent-plan",
|
||||
taskId: fallbackTaskId,
|
||||
runId: requestedRunId || taskGovernanceRunId(command.commandKind, fallbackTaskId, commandId),
|
||||
stepId: requestedStepId || null,
|
||||
};
|
||||
}
|
||||
|
||||
const requestedRun = requestedRunId ? readDigitalEmployeeRunRow(requestedRunId) : null;
|
||||
const existingTaskId = requestedTaskId || stringValue(requestedRun?.task_id);
|
||||
const existingTask = existingTaskId ? readDigitalEmployeeTaskRow(existingTaskId) : null;
|
||||
const latestRun = existingTaskId ? readLatestDigitalEmployeeRunForTask(existingTaskId) : null;
|
||||
const targetRun = requestedRun || latestRun;
|
||||
const taskId = existingTaskId || `governance-task-${safeId(commandId)}`;
|
||||
const planId = requestedPlanId
|
||||
|| stringValue(existingTask?.plan_id)
|
||||
|| stringValue(targetRun?.plan_id)
|
||||
|| "qimingclaw-task-agent-plan";
|
||||
const existingTaskPayload = objectRecord(parseStoredPayload(existingTask?.payload)) ?? {};
|
||||
const stepId = requestedStepId
|
||||
|| stringValue(existingTaskPayload.stepId ?? existingTaskPayload.step_id)
|
||||
|| findPlanStepIdForTask(planId, taskId);
|
||||
const runId = taskGovernanceTargetRunId(command.commandKind, taskId, commandId, requestedRunId, targetRun);
|
||||
const status = taskGovernanceStatusForCommand(command.commandKind, existingTask);
|
||||
const nextAction = taskGovernanceNextAction(command.commandKind);
|
||||
const payloadEnvelope = {
|
||||
source: "qimingclaw-task-governance",
|
||||
commandKind: command.commandKind,
|
||||
commandId,
|
||||
correlationId: command.correlationId,
|
||||
planId,
|
||||
taskId,
|
||||
stepId: stepId || null,
|
||||
runId,
|
||||
sourceRunId: stringValue(targetRun?.id) || null,
|
||||
accepted: command.accepted !== false,
|
||||
message: command.message,
|
||||
error: command.error,
|
||||
status,
|
||||
nextAction,
|
||||
payload,
|
||||
result,
|
||||
};
|
||||
|
||||
const tx = db.transaction(() => {
|
||||
if (!existingTask) {
|
||||
upsertPlan({
|
||||
id: planId,
|
||||
title: stringValue(payload.plan_title ?? result.plan_title) || "Task Agent 控制平面",
|
||||
objective: "记录 qimingclaw 数字员工任务级治理命令。",
|
||||
status: "running",
|
||||
payload: payloadEnvelope,
|
||||
now,
|
||||
});
|
||||
upsertTask({
|
||||
id: taskId,
|
||||
planId,
|
||||
title: stringValue(payload.title ?? payload.task_title ?? result.title) || governanceCommandTitle(command.commandKind),
|
||||
status: taskStatusForCommand(command.commandKind, "pending"),
|
||||
assignedSkillId: stringValue(payload.assigned_skill_id ?? result.assigned_skill_id) || "qimingclaw-task-agent",
|
||||
payload: mergeGovernancePayload({}, payloadEnvelope, null),
|
||||
now,
|
||||
});
|
||||
} else if (shouldUpdateTaskForGovernance(command.commandKind)) {
|
||||
updateTaskForGovernance({
|
||||
task: existingTask,
|
||||
status: taskStatusForCommand(command.commandKind, stringValue(existingTask.status)),
|
||||
payload: mergeGovernancePayload(existingTaskPayload, payloadEnvelope, stringValue(existingTask.status)),
|
||||
now,
|
||||
});
|
||||
} else if (command.commandKind === "provide_task_input") {
|
||||
updateTaskForGovernance({
|
||||
task: existingTask,
|
||||
status: stringValue(existingTask.status) || "pending",
|
||||
payload: mergeGovernancePayload(existingTaskPayload, payloadEnvelope, stringValue(existingTask.status)),
|
||||
now,
|
||||
});
|
||||
}
|
||||
|
||||
if (stepId && shouldUpdateStepForGovernance(command.commandKind)) {
|
||||
updatePlanStepForGovernance({
|
||||
stepId,
|
||||
status: stepStatusForCommand(command.commandKind),
|
||||
payloadEnvelope,
|
||||
now,
|
||||
});
|
||||
}
|
||||
|
||||
persistTaskGovernanceRun({
|
||||
commandKind: command.commandKind,
|
||||
planId,
|
||||
taskId,
|
||||
runId,
|
||||
targetRun,
|
||||
payloadEnvelope,
|
||||
now,
|
||||
});
|
||||
|
||||
persistPayloadArtifactsAndApprovals({ planId, taskId, runId }, payloadEnvelope, now);
|
||||
insertEvent(
|
||||
{
|
||||
event_id: `${runId}:${command.commandKind}:${safeId(commandId)}`,
|
||||
kind: `governance_${command.commandKind}`,
|
||||
occurred_at: now,
|
||||
message: command.message || `${governanceCommandTitle(command.commandKind)} 已记录`,
|
||||
plan_id: planId,
|
||||
task_id: taskId,
|
||||
},
|
||||
runId,
|
||||
payloadEnvelope,
|
||||
);
|
||||
});
|
||||
tx();
|
||||
|
||||
return { planId, taskId, runId, stepId: stepId || null };
|
||||
}
|
||||
|
||||
function governanceCommandTitle(commandKind: string): string {
|
||||
switch (commandKind) {
|
||||
case "create_plan":
|
||||
@@ -1293,6 +1450,22 @@ function governanceCommandTitle(commandKind: string): string {
|
||||
return "暂停数字员工计划";
|
||||
case "cancel_plan":
|
||||
return "取消数字员工计划";
|
||||
case "retry_task":
|
||||
return "重试数字员工任务";
|
||||
case "skip_task":
|
||||
return "跳过数字员工任务";
|
||||
case "pause_task":
|
||||
return "暂停数字员工任务";
|
||||
case "resume_task":
|
||||
return "恢复数字员工任务";
|
||||
case "provide_task_input":
|
||||
return "补充数字员工任务输入";
|
||||
case "start_run":
|
||||
return "启动数字员工任务运行";
|
||||
case "cancel_task":
|
||||
return "取消数字员工任务";
|
||||
case "cancel_run":
|
||||
return "取消数字员工运行";
|
||||
default:
|
||||
return `数字员工治理命令:${commandKind || "unknown"}`;
|
||||
}
|
||||
@@ -1452,6 +1625,375 @@ function persistScheduleGovernanceCommand(input: {
|
||||
});
|
||||
}
|
||||
|
||||
const TASK_GOVERNANCE_COMMANDS = new Set([
|
||||
"retry_task",
|
||||
"skip_task",
|
||||
"pause_task",
|
||||
"resume_task",
|
||||
"provide_task_input",
|
||||
"cancel_task",
|
||||
"start_run",
|
||||
"cancel_run",
|
||||
]);
|
||||
|
||||
function isTaskGovernanceCommand(commandKind: string): boolean {
|
||||
return TASK_GOVERNANCE_COMMANDS.has(commandKind);
|
||||
}
|
||||
|
||||
function readDigitalEmployeeTaskRow(taskId: string): Record<string, unknown> | null {
|
||||
const db = getDigitalEmployeeDb();
|
||||
if (!db) return null;
|
||||
const row = 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
|
||||
FROM digital_tasks
|
||||
WHERE id = ?
|
||||
`).get(taskId) as Record<string, unknown> | undefined;
|
||||
return row ?? null;
|
||||
}
|
||||
|
||||
function readDigitalEmployeeRunRow(runId: string): Record<string, unknown> | null {
|
||||
const db = getDigitalEmployeeDb();
|
||||
if (!db) return null;
|
||||
const row = db.prepare(`
|
||||
SELECT id, remote_id, plan_id, task_id, status, started_at, finished_at,
|
||||
payload, sync_status, last_synced_at, sync_error, created_at, updated_at
|
||||
FROM digital_runs
|
||||
WHERE id = ?
|
||||
`).get(runId) as Record<string, unknown> | undefined;
|
||||
return row ?? null;
|
||||
}
|
||||
|
||||
function readLatestDigitalEmployeeRunForTask(taskId: string): Record<string, unknown> | null {
|
||||
const db = getDigitalEmployeeDb();
|
||||
if (!db) return null;
|
||||
const row = db.prepare(`
|
||||
SELECT id, remote_id, plan_id, task_id, status, started_at, finished_at,
|
||||
payload, sync_status, last_synced_at, sync_error, created_at, updated_at
|
||||
FROM digital_runs
|
||||
WHERE task_id = ?
|
||||
ORDER BY updated_at DESC, created_at DESC
|
||||
LIMIT 1
|
||||
`).get(taskId) as Record<string, unknown> | undefined;
|
||||
return row ?? null;
|
||||
}
|
||||
|
||||
function findPlanStepIdForTask(planId: string, taskId: string): string | null {
|
||||
const task = readDigitalEmployeeTaskRow(taskId);
|
||||
const taskPayload = objectRecord(parseStoredPayload(task?.payload)) ?? {};
|
||||
const explicitStepId = stringValue(taskPayload.stepId ?? taskPayload.step_id);
|
||||
if (explicitStepId) return explicitStepId;
|
||||
const db = getDigitalEmployeeDb();
|
||||
if (!db || !planId) return null;
|
||||
const row = db.prepare(`
|
||||
SELECT id
|
||||
FROM digital_plan_steps
|
||||
WHERE plan_id = ?
|
||||
ORDER BY seq ASC, created_at ASC
|
||||
LIMIT 1
|
||||
`).get(planId) as Record<string, unknown> | undefined;
|
||||
return stringValue(row?.id) || null;
|
||||
}
|
||||
|
||||
function taskGovernanceRunId(commandKind: string, taskId: string, commandId: string): string {
|
||||
return `${taskId}:run:${commandKind}:${safeId(commandId)}`;
|
||||
}
|
||||
|
||||
function taskGovernanceTargetRunId(
|
||||
commandKind: string,
|
||||
taskId: string,
|
||||
commandId: string,
|
||||
requestedRunId: string,
|
||||
targetRun: Record<string, unknown> | null,
|
||||
): string {
|
||||
if (commandKind === "retry_task" || commandKind === "start_run") {
|
||||
return taskGovernanceRunId(commandKind, taskId, commandId);
|
||||
}
|
||||
return requestedRunId || stringValue(targetRun?.id) || taskGovernanceRunId(commandKind, taskId, commandId);
|
||||
}
|
||||
|
||||
function taskGovernanceStatusForCommand(
|
||||
commandKind: string,
|
||||
existingTask: Record<string, unknown> | null,
|
||||
): string {
|
||||
if (commandKind === "cancel_run" || commandKind === "provide_task_input") {
|
||||
return stringValue(existingTask?.status) || "accepted";
|
||||
}
|
||||
return taskStatusForCommand(commandKind, stringValue(existingTask?.status) || "pending");
|
||||
}
|
||||
|
||||
function taskStatusForCommand(commandKind: string, previousStatus: string): string {
|
||||
switch (commandKind) {
|
||||
case "retry_task":
|
||||
case "start_run":
|
||||
case "resume_task":
|
||||
return "running";
|
||||
case "skip_task":
|
||||
return "skipped";
|
||||
case "cancel_task":
|
||||
return "cancelled";
|
||||
case "pause_task":
|
||||
return "paused";
|
||||
default:
|
||||
return previousStatus || "pending";
|
||||
}
|
||||
}
|
||||
|
||||
function stepStatusForCommand(commandKind: string): string {
|
||||
switch (commandKind) {
|
||||
case "retry_task":
|
||||
case "start_run":
|
||||
case "resume_task":
|
||||
return "running";
|
||||
case "skip_task":
|
||||
return "skipped";
|
||||
case "cancel_task":
|
||||
return "cancelled";
|
||||
case "pause_task":
|
||||
return "paused";
|
||||
default:
|
||||
return "pending";
|
||||
}
|
||||
}
|
||||
|
||||
function taskGovernanceNextAction(commandKind: string): string {
|
||||
switch (commandKind) {
|
||||
case "retry_task":
|
||||
return "watch_retry_run";
|
||||
case "skip_task":
|
||||
return "continue_next_task";
|
||||
case "pause_task":
|
||||
return "await_resume";
|
||||
case "resume_task":
|
||||
case "start_run":
|
||||
return "watch_progress";
|
||||
case "provide_task_input":
|
||||
return "resume_with_input";
|
||||
case "cancel_task":
|
||||
case "cancel_run":
|
||||
return "close_execution";
|
||||
default:
|
||||
return "record_control_event";
|
||||
}
|
||||
}
|
||||
|
||||
function shouldUpdateTaskForGovernance(commandKind: string): boolean {
|
||||
return ["retry_task", "skip_task", "pause_task", "resume_task", "cancel_task", "start_run"].includes(commandKind);
|
||||
}
|
||||
|
||||
function shouldUpdateStepForGovernance(commandKind: string): boolean {
|
||||
return ["retry_task", "skip_task", "pause_task", "resume_task", "cancel_task", "start_run"].includes(commandKind);
|
||||
}
|
||||
|
||||
function mergeGovernancePayload(
|
||||
existingPayload: Record<string, unknown>,
|
||||
payloadEnvelope: Record<string, unknown>,
|
||||
previousStatus: string | null,
|
||||
): Record<string, unknown> {
|
||||
const governanceHistory = unknownArray(existingPayload.governanceHistory).slice(-19);
|
||||
return {
|
||||
...existingPayload,
|
||||
lastGovernanceCommand: payloadEnvelope,
|
||||
governanceHistory: [
|
||||
...governanceHistory,
|
||||
{
|
||||
commandKind: payloadEnvelope.commandKind,
|
||||
commandId: payloadEnvelope.commandId,
|
||||
previousStatus,
|
||||
status: payloadEnvelope.status,
|
||||
occurredAt: new Date().toISOString(),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function updateTaskForGovernance(input: {
|
||||
task: Record<string, unknown>;
|
||||
status: string;
|
||||
payload: Record<string, unknown>;
|
||||
now: string;
|
||||
}): void {
|
||||
const db = getDigitalEmployeeDb();
|
||||
if (!db) return;
|
||||
const taskId = stringValue(input.task.id);
|
||||
db.prepare(`
|
||||
UPDATE digital_tasks
|
||||
SET status = ?, payload = ?, sync_status = 'pending', updated_at = ?
|
||||
WHERE id = ?
|
||||
`).run(input.status, stringify(input.payload), input.now, taskId);
|
||||
markOutbox("task", taskId, "upsert", {
|
||||
id: taskId,
|
||||
planId: stringValue(input.task.plan_id),
|
||||
title: stringValue(input.task.title),
|
||||
status: input.status,
|
||||
assignedSkillId: stringValue(input.task.assigned_skill_id),
|
||||
payload: input.payload,
|
||||
now: input.now,
|
||||
}, input.now);
|
||||
}
|
||||
|
||||
function updatePlanStepForGovernance(input: {
|
||||
stepId: string;
|
||||
status: string;
|
||||
payloadEnvelope: 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(input.stepId) as Record<string, unknown> | undefined;
|
||||
if (!row) return;
|
||||
const previousPayload = objectRecord(parseStoredPayload(row.payload)) ?? {};
|
||||
const payload = mergeGovernancePayload(previousPayload, input.payloadEnvelope, stringValue(row.status));
|
||||
db.prepare(`
|
||||
UPDATE digital_plan_steps
|
||||
SET status = ?, payload = ?, sync_status = 'pending', updated_at = ?
|
||||
WHERE id = ?
|
||||
`).run(input.status, stringify(payload), input.now, input.stepId);
|
||||
markOutbox("plan_step", input.stepId, "upsert", {
|
||||
id: input.stepId,
|
||||
planId: stringValue(row.plan_id),
|
||||
title: stringValue(row.title),
|
||||
status: input.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: input.now,
|
||||
}, input.now);
|
||||
}
|
||||
|
||||
function persistTaskGovernanceRun(input: {
|
||||
commandKind: string;
|
||||
planId: string;
|
||||
taskId: string;
|
||||
runId: string;
|
||||
targetRun: Record<string, unknown> | null;
|
||||
payloadEnvelope: Record<string, unknown>;
|
||||
now: string;
|
||||
}): void {
|
||||
if (input.commandKind === "retry_task" || input.commandKind === "start_run") {
|
||||
const attemptNo = countDigitalEmployeeRunsForTask(input.taskId) + 1;
|
||||
upsertRun({
|
||||
id: input.runId,
|
||||
planId: input.planId,
|
||||
taskId: input.taskId,
|
||||
status: "running",
|
||||
payload: {
|
||||
...input.payloadEnvelope,
|
||||
attemptNo,
|
||||
},
|
||||
now: input.now,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const existingRun = input.targetRun || readDigitalEmployeeRunRow(input.runId);
|
||||
const previousPayload = objectRecord(parseStoredPayload(existingRun?.payload)) ?? {};
|
||||
const status = runStatusForTaskGovernance(input.commandKind, stringValue(existingRun?.status) || "pending");
|
||||
const payload = mergeGovernancePayload(previousPayload, input.payloadEnvelope, stringValue(existingRun?.status) || null);
|
||||
if (existingRun) {
|
||||
updateRunForGovernance({
|
||||
run: existingRun,
|
||||
status,
|
||||
payload,
|
||||
finishedAt: terminalRunStatus(status) ? input.now : undefined,
|
||||
now: input.now,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
upsertRun({
|
||||
id: input.runId,
|
||||
planId: input.planId,
|
||||
taskId: input.taskId,
|
||||
status,
|
||||
payload,
|
||||
now: input.now,
|
||||
});
|
||||
if (terminalRunStatus(status)) finishRunWithOutbox(input.runId, input.planId, input.taskId, status, payload, input.now);
|
||||
}
|
||||
|
||||
function runStatusForTaskGovernance(commandKind: string, previousStatus: string): string {
|
||||
switch (commandKind) {
|
||||
case "skip_task":
|
||||
return "skipped";
|
||||
case "cancel_task":
|
||||
case "cancel_run":
|
||||
return "cancelled";
|
||||
case "pause_task":
|
||||
return "paused";
|
||||
case "resume_task":
|
||||
return "running";
|
||||
default:
|
||||
return previousStatus || "pending";
|
||||
}
|
||||
}
|
||||
|
||||
function terminalRunStatus(status: string): boolean {
|
||||
return ["completed", "succeeded", "failed", "cancelled", "skipped"].includes(status);
|
||||
}
|
||||
|
||||
function updateRunForGovernance(input: {
|
||||
run: Record<string, unknown>;
|
||||
status: string;
|
||||
payload: Record<string, unknown>;
|
||||
finishedAt?: string;
|
||||
now: string;
|
||||
}): void {
|
||||
const db = getDigitalEmployeeDb();
|
||||
if (!db) return;
|
||||
const runId = stringValue(input.run.id);
|
||||
if (input.finishedAt) {
|
||||
db.prepare(`
|
||||
UPDATE digital_runs
|
||||
SET status = ?, payload = ?, finished_at = ?, sync_status = 'pending', updated_at = ?
|
||||
WHERE id = ?
|
||||
`).run(input.status, stringify(input.payload), input.finishedAt, input.now, runId);
|
||||
} else {
|
||||
db.prepare(`
|
||||
UPDATE digital_runs
|
||||
SET status = ?, payload = ?, sync_status = 'pending', updated_at = ?
|
||||
WHERE id = ?
|
||||
`).run(input.status, stringify(input.payload), input.now, runId);
|
||||
}
|
||||
markOutbox("run", runId, "upsert", {
|
||||
id: runId,
|
||||
planId: stringValue(input.run.plan_id),
|
||||
taskId: stringValue(input.run.task_id),
|
||||
status: input.status,
|
||||
payload: input.payload,
|
||||
finishedAt: input.finishedAt ?? optionalStringField(input.run, "finished_at"),
|
||||
now: input.now,
|
||||
}, input.now);
|
||||
}
|
||||
|
||||
function finishRunWithOutbox(
|
||||
runId: string,
|
||||
planId: string,
|
||||
taskId: string,
|
||||
status: string,
|
||||
payload: Record<string, unknown>,
|
||||
now: string,
|
||||
): void {
|
||||
finishRun(runId, status, now);
|
||||
markOutbox("run", runId, "upsert", { id: runId, planId, taskId, status, payload, finishedAt: now }, now);
|
||||
}
|
||||
|
||||
function countDigitalEmployeeRunsForTask(taskId: string): number {
|
||||
const db = getDigitalEmployeeDb();
|
||||
if (!db) return 0;
|
||||
const row = db.prepare(`
|
||||
SELECT COUNT(*) AS count
|
||||
FROM digital_runs
|
||||
WHERE task_id = ?
|
||||
`).get(taskId) as { count?: number } | undefined;
|
||||
return Number(row?.count) || 0;
|
||||
}
|
||||
|
||||
function extractPlanStepInputs(
|
||||
payload: Record<string, unknown>,
|
||||
planId: string,
|
||||
|
||||
@@ -540,9 +540,9 @@ GET /api/digital-employee/approvals
|
||||
|
||||
1. **PlanStep 与依赖图(已开始)**
|
||||
- 已吸收: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 状态机补步骤级治理命令。
|
||||
- 当前能力: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。
|
||||
- 后续缺口:步骤依赖调度和跨任务编排执行仍待吸收。
|
||||
- 建议:下一轮围绕依赖图调度,把已完成/跳过/取消的步骤继续驱动到后续步骤。
|
||||
|
||||
2. **调度 / 定时 / 周期任务**
|
||||
- 已吸收:`digital_schedules`、`digital_schedule_runs` 已进入 qimingclaw 本地 SQLite 与 outbox,entity type 为 `schedule` / `schedule_run`;旧 `/api/cron`、`/api/scheduler/jobs` 和治理命令会优先读取真实本地调度记录。
|
||||
@@ -556,9 +556,9 @@ GET /api/digital-employee/approvals
|
||||
- 建议:先把 qimingclaw memory 列表投射到数字员工,再补记忆新增、更新、归档和同步。
|
||||
|
||||
4. **治理命令完整生命周期**
|
||||
- 已吸收:`create_plan`、`submit_plan`、`approve_plan`、`activate_plan` 等基础命令可记录本地运行事实。
|
||||
- 缺口:`cancel`、`retry`、`skip`、`pause`、`resume`、`escalate`、计划 revision、任务 action endpoint 和治理审计链还不完整。
|
||||
- 建议:围绕 Task Agent 状态机补命令映射,并把每次治理操作写入 Event / outbox。
|
||||
- 已吸收:`create_plan`、`submit_plan`、`approve_plan`、`activate_plan` 等基础命令可记录本地运行事实;任务级 `retry_task`、`skip_task`、`pause_task`、`resume_task`、`provide_task_input`、`cancel_task`、`start_run`、`cancel_run` 已具备本地事实闭环。
|
||||
- 缺口:`escalate`、计划 revision、任务 action endpoint、治理审计链和对真实 ACP 执行流的硬中断仍不完整。
|
||||
- 建议:下一轮围绕治理审计和依赖调度补齐更细的 action endpoint。
|
||||
|
||||
5. **ManagedService 控制面**
|
||||
- 已吸收:Agent、MCP、Computer Server、Lanproxy、File Server、GUI Server 状态已进入 snapshot / projection。
|
||||
@@ -1229,14 +1229,14 @@ User
|
||||
- Plan Agent 包含计划生命周期、托管任务数、失败任务数、下一步动作和是否可重规划。
|
||||
- Task Agent 包含执行状态、绑定 Skill、最新 Run、是否可 retry / skip / cancel,以及下一步动作。
|
||||
- 首页 overview 展示 Plan Agent、Task Agent 和可治理任务数量;播报区域优先提示可治理 Task Agent。
|
||||
- 首页“今日执行”面板已提供 Task Agent 控制条,可对可治理任务发起重试、跳过、取消,并复用 governance command 写入本地运行状态和 outbox。
|
||||
- `retry_task`、`skip_task`、`pause_task`、`resume_task`、`provide_task_input`、`cancel_task`、`start_run`、`cancel_run` governance command 会被 qimingclaw adapter 接受并写入正式本地 governance 记录。
|
||||
- 首页“今日执行”面板已提供 Task Agent 控制条,可对可治理任务发起重试、跳过、取消,并通过 governance command 更新真实 Task / PlanStep / Run / Event / outbox。
|
||||
- `retry_task`、`skip_task`、`pause_task`、`resume_task`、`provide_task_input`、`cancel_task`、`start_run`、`cancel_run` governance command 会被 qimingclaw adapter 接受并写入正式本地治理记录;其中取消和跳过只更新本地事实,不强制杀掉正在运行的 ACP / `/computer/chat` 流。
|
||||
|
||||
### 产出
|
||||
|
||||
- qimingclaw 从“Agent 客户端”升级为“数字员工任务操作系统”。
|
||||
- 计划/任务执行开始具备 Plan Agent / Task Agent 控制平面投影。
|
||||
- Task Agent 的 retry / skip / cancel 不再只是提示,已经具备用户可触发的本地治理入口。
|
||||
- Task Agent 的 retry / skip / cancel 不再只是提示,已经具备用户可触发、可刷新、可同步的本地治理闭环。
|
||||
|
||||
## 推荐实施顺序
|
||||
|
||||
|
||||
Reference in New Issue
Block a user