吸收数字员工PlanStep跨任务编排执行
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user