吸收数字员工PlanStep跨任务编排执行

This commit is contained in:
baiyanyun
2026-06-10 21:00:59 +08:00
parent a6e0246418
commit 909f11dae5
17 changed files with 646 additions and 217 deletions

View File

@@ -34,6 +34,7 @@ import type {
MemoryEntry,
NotificationPreferences,
PlanDispatchResponse,
ReadyStepDispatchResult,
PlanStepDispatchPolicy,
PlanStepSummary,
RouteDecisionRecord,
@@ -63,6 +64,7 @@ declare global {
saveCronSettings?: (patch: Partial<QimingclawCronSettings>) => Promise<QimingclawCronSettings>;
runSchedulerCheck?: () => Promise<{ activated: number; due?: number; skipped?: number }>;
runScheduleNow?: (scheduleId: string) => Promise<{ ok?: boolean; scheduleRunId?: string; error?: string }>;
dispatchPlanReadySteps?: (planId: string) => Promise<ReadyStepDispatchResult>;
getScheduleRuns?: (scheduleId: string, limit?: number) => Promise<QimingclawScheduleRunRecord[]>;
listRouteDecisions?: (options?: { limit?: number }) => Promise<RouteDecisionRecord[]>;
recordRouteDecision?: (input: QimingclawRouteDecisionInput) => Promise<RouteDecisionRecord>;
@@ -925,6 +927,10 @@ export async function handleQimingclawDigitalApi<T>(
if (method === 'GET' && pathname.startsWith('/api/debug/task/') && pathname.endsWith('/flow')) {
return buildTaskFlow(decodeURIComponent(pathname.split('/')[4] || ''), await readQimingclawSnapshot()) as T;
}
const planReadyDispatchMatch = pathname.match(/^\/api\/digital-employee\/plans\/([^/]+)\/dispatch-ready-steps$/);
if (method === 'POST' && planReadyDispatchMatch) {
return await dispatchPlanReadySteps(decodeURIComponent(planReadyDispatchMatch[1] || ''), await readQimingclawSnapshot()) as T;
}
if (method === 'POST' && pathname.startsWith('/api/debug/plan/') && pathname.endsWith('/dispatch')) {
return await dispatchPlan(decodeURIComponent(pathname.split('/')[4] || ''), await readQimingclawSnapshot()) as T;
}
@@ -6123,6 +6129,89 @@ async function dispatchPlan(
};
}
async function dispatchPlanReadySteps(
planId: string,
snapshot: QimingclawSnapshot | null = null,
): Promise<ReadyStepDispatchResult> {
const targetPlanId = stringValue(planId);
const bridgeResult = targetPlanId ? await window.QimingClawBridge?.digital?.dispatchPlanReadySteps?.(targetPlanId).catch(() => null) : null;
if (bridgeResult) return bridgeResult;
const now = new Date().toISOString();
const orchestrationId = `manual-ready-steps-${slugifyIdPart(targetPlanId || 'unknown')}-${slugifyIdPart(now)}`;
const rows = planStepRows(snapshot).filter((row) => stringValue(row.plan_id) === targetPlanId && stringValue(row.status).toLowerCase() === 'ready');
const state = notificationAdapterState(snapshot);
const policy = planStepDispatchPolicy(state);
const result: ReadyStepDispatchResult = {
ok: true,
plan_id: targetPlanId || null,
trigger_source: 'manual',
orchestration_id: orchestrationId,
candidates: rows.length,
dispatched: 0,
skipped: 0,
failed: 0,
dispatched_step_ids: [],
skipped_step_ids: [],
failed_step_ids: [],
errors: [],
};
if (!targetPlanId || !policy.enabled) {
result.skipped = rows.length;
result.skipped_step_ids = rows.map((row) => stringValue(row.step_id)).filter(Boolean);
return result;
}
const maxPerSweep = Math.max(1, Math.min(Math.floor(policy.max_per_sweep), 10));
let attempts = 0;
for (const row of rows) {
const stepId = stringValue(row.step_id);
const taskId = stringValue(row.task_id);
if (attempts >= maxPerSweep || row.dispatchable !== true || !taskId) {
result.skipped += 1;
if (stepId) result.skipped_step_ids?.push(stepId);
continue;
}
attempts += 1;
try {
const runId = stringValue(row.latest_run_id) || `${taskId}:run:start_run:${slugifyIdPart(orchestrationId)}`;
const commandId = `manual-ready-step-${slugifyIdPart(stepId)}-${Date.now()}`;
await recordGovernanceCommand(
{
command_kind: 'start_run',
context_refs: { plan_id: targetPlanId, task_id: taskId, run_id: runId, step_id: stepId },
payload: {
source: 'digital-home-plan-orchestration',
plan_id: targetPlanId,
task_id: taskId,
run_id: runId,
step_id: stepId,
trigger_source: 'manual',
orchestration_id: orchestrationId,
candidate_count: rows.length,
},
correlation_id: commandId,
},
governanceAccepted(commandId, commandId, '已手动推进 Ready PlanStep。', {
plan_id: targetPlanId,
task_id: taskId,
run_id: runId,
step_id: stepId,
status: 'running',
trigger_source: 'manual',
orchestration_id: orchestrationId,
source: 'qimingclaw-plan-orchestration',
}),
);
result.dispatched += 1;
if (stepId) result.dispatched_step_ids?.push(stepId);
} catch (error) {
result.failed += 1;
if (stepId) result.failed_step_ids?.push(stepId);
result.errors?.push({ stepId, error: error instanceof Error ? error.message : 'dispatch failed' });
}
}
return result;
}
function buildPlanMessages(planId: string, snapshot: QimingclawSnapshot | null = null): Array<{ role: string; content: string; created_at?: string | null }> {
const runtimePlan = runtimePlans(snapshot).find((candidate) => candidate.id === planId);
if (runtimePlan) {