吸收数字员工PlanStep跨任务编排执行
This commit is contained in:
@@ -16,6 +16,7 @@ import type {
|
|||||||
BusinessViewResponse,
|
BusinessViewResponse,
|
||||||
PlanStepBusinessRow,
|
PlanStepBusinessRow,
|
||||||
PlanStepDispatchPolicy,
|
PlanStepDispatchPolicy,
|
||||||
|
ReadyStepDispatchResult,
|
||||||
NotificationPreferences,
|
NotificationPreferences,
|
||||||
UserNotification,
|
UserNotification,
|
||||||
DebugStoreResponse,
|
DebugStoreResponse,
|
||||||
@@ -713,6 +714,13 @@ export function dispatchPlanNow(planId: string): Promise<PlanDispatchResponse> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function dispatchPlanReadySteps(planId: string): Promise<ReadyStepDispatchResult> {
|
||||||
|
return apiFetch<ReadyStepDispatchResult>(`/api/digital-employee/plans/${encodeURIComponent(planId)}/dispatch-ready-steps`, {
|
||||||
|
method: 'POST',
|
||||||
|
timeoutMs: DIGITAL_EMPLOYEE_WORKDAY_TIMEOUT_MS,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function getDebugPlans(): Promise<DebugPlan[]> {
|
export function getDebugPlans(): Promise<DebugPlan[]> {
|
||||||
return apiFetch<{ plans: DebugPlan[] }>('/api/debug/plans?limit=80')
|
return apiFetch<{ plans: DebugPlan[] }>('/api/debug/plans?limit=80')
|
||||||
.then((data) => data.plans ?? []);
|
.then((data) => data.plans ?? []);
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import type {
|
|||||||
MemoryEntry,
|
MemoryEntry,
|
||||||
NotificationPreferences,
|
NotificationPreferences,
|
||||||
PlanDispatchResponse,
|
PlanDispatchResponse,
|
||||||
|
ReadyStepDispatchResult,
|
||||||
PlanStepDispatchPolicy,
|
PlanStepDispatchPolicy,
|
||||||
PlanStepSummary,
|
PlanStepSummary,
|
||||||
RouteDecisionRecord,
|
RouteDecisionRecord,
|
||||||
@@ -63,6 +64,7 @@ declare global {
|
|||||||
saveCronSettings?: (patch: Partial<QimingclawCronSettings>) => Promise<QimingclawCronSettings>;
|
saveCronSettings?: (patch: Partial<QimingclawCronSettings>) => Promise<QimingclawCronSettings>;
|
||||||
runSchedulerCheck?: () => Promise<{ activated: number; due?: number; skipped?: number }>;
|
runSchedulerCheck?: () => Promise<{ activated: number; due?: number; skipped?: number }>;
|
||||||
runScheduleNow?: (scheduleId: string) => Promise<{ ok?: boolean; scheduleRunId?: string; error?: string }>;
|
runScheduleNow?: (scheduleId: string) => Promise<{ ok?: boolean; scheduleRunId?: string; error?: string }>;
|
||||||
|
dispatchPlanReadySteps?: (planId: string) => Promise<ReadyStepDispatchResult>;
|
||||||
getScheduleRuns?: (scheduleId: string, limit?: number) => Promise<QimingclawScheduleRunRecord[]>;
|
getScheduleRuns?: (scheduleId: string, limit?: number) => Promise<QimingclawScheduleRunRecord[]>;
|
||||||
listRouteDecisions?: (options?: { limit?: number }) => Promise<RouteDecisionRecord[]>;
|
listRouteDecisions?: (options?: { limit?: number }) => Promise<RouteDecisionRecord[]>;
|
||||||
recordRouteDecision?: (input: QimingclawRouteDecisionInput) => 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')) {
|
if (method === 'GET' && pathname.startsWith('/api/debug/task/') && pathname.endsWith('/flow')) {
|
||||||
return buildTaskFlow(decodeURIComponent(pathname.split('/')[4] || ''), await readQimingclawSnapshot()) as T;
|
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')) {
|
if (method === 'POST' && pathname.startsWith('/api/debug/plan/') && pathname.endsWith('/dispatch')) {
|
||||||
return await dispatchPlan(decodeURIComponent(pathname.split('/')[4] || ''), await readQimingclawSnapshot()) as T;
|
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 }> {
|
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);
|
const runtimePlan = runtimePlans(snapshot).find((candidate) => candidate.id === planId);
|
||||||
if (runtimePlan) {
|
if (runtimePlan) {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import Avatar3D from '@/components/digital/Avatar3D';
|
|||||||
import { basePath } from '@/lib/basePath';
|
import { basePath } from '@/lib/basePath';
|
||||||
import {
|
import {
|
||||||
dispatchPlanNow,
|
dispatchPlanNow,
|
||||||
|
dispatchPlanReadySteps,
|
||||||
getDigitalEmployeeWorkday,
|
getDigitalEmployeeWorkday,
|
||||||
getPlanStepGraph,
|
getPlanStepGraph,
|
||||||
getSchedulerJobs,
|
getSchedulerJobs,
|
||||||
@@ -12,7 +13,6 @@ import {
|
|||||||
postDigitalEmployeeWorkdayAction,
|
postDigitalEmployeeWorkdayAction,
|
||||||
recordApprovalAction,
|
recordApprovalAction,
|
||||||
restartManagedService,
|
restartManagedService,
|
||||||
triggerCronCheck,
|
|
||||||
} from '@/lib/api';
|
} from '@/lib/api';
|
||||||
import { isTauri } from '@/lib/tauri';
|
import { isTauri } from '@/lib/tauri';
|
||||||
import type {
|
import type {
|
||||||
@@ -60,10 +60,19 @@ interface DigitalPlanItem {
|
|||||||
conversationId?: string | null;
|
conversationId?: string | null;
|
||||||
stepCount: number;
|
stepCount: number;
|
||||||
taskCount: number;
|
taskCount: number;
|
||||||
|
readyStepCount: number;
|
||||||
|
runningStepCount: number;
|
||||||
|
blockedStepCount: number;
|
||||||
schedulerJob?: CronJob | null;
|
schedulerJob?: CronJob | null;
|
||||||
source: 'plan_template';
|
source: 'plan_template';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface PlanStepOrchestrationSummary {
|
||||||
|
ready: number;
|
||||||
|
running: number;
|
||||||
|
blocked: number;
|
||||||
|
}
|
||||||
|
|
||||||
interface BlockedPlanStepInputTarget {
|
interface BlockedPlanStepInputTarget {
|
||||||
step: PlanStepBusinessRow;
|
step: PlanStepBusinessRow;
|
||||||
dependency: PlanStepDependencyDetail | null;
|
dependency: PlanStepDependencyDetail | null;
|
||||||
@@ -405,6 +414,9 @@ function buildPlanTemplateItem(job: CronJob, duties: DigitalEmployeeDuty[], sele
|
|||||||
conversationId: linkedDuty?.conversation_id ?? templateId,
|
conversationId: linkedDuty?.conversation_id ?? templateId,
|
||||||
stepCount,
|
stepCount,
|
||||||
taskCount: skillCount,
|
taskCount: skillCount,
|
||||||
|
readyStepCount: 0,
|
||||||
|
runningStepCount: 0,
|
||||||
|
blockedStepCount: 0,
|
||||||
schedulerJob: job,
|
schedulerJob: job,
|
||||||
source: 'plan_template',
|
source: 'plan_template',
|
||||||
};
|
};
|
||||||
@@ -419,6 +431,26 @@ function planExecutionDisabledReason(planItem: DigitalPlanItem): string {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function planReadyStepActionLabel(planItem: DigitalPlanItem): string {
|
||||||
|
if (planItem.readyStepCount > 0) return `推进 ${planItem.readyStepCount}`;
|
||||||
|
return '推进';
|
||||||
|
}
|
||||||
|
|
||||||
|
function planStepOrchestrationSummary(rows: PlanStepBusinessRow[]): Map<string, PlanStepOrchestrationSummary> {
|
||||||
|
const summary = new Map<string, PlanStepOrchestrationSummary>();
|
||||||
|
rows.forEach((step) => {
|
||||||
|
const planId = step.plan_id || '';
|
||||||
|
if (!planId) return;
|
||||||
|
const current = summary.get(planId) ?? { ready: 0, running: 0, blocked: 0 };
|
||||||
|
const status = step.status.toLowerCase();
|
||||||
|
if (status === 'ready') current.ready += 1;
|
||||||
|
else if (['running', 'active', 'leased', 'queued'].includes(status)) current.running += 1;
|
||||||
|
else if (status === 'blocked') current.blocked += 1;
|
||||||
|
summary.set(planId, current);
|
||||||
|
});
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
|
||||||
function eventMatchesPlanItem(event: DigitalEmployeeWorkEvent, planItem: DigitalPlanItem): boolean {
|
function eventMatchesPlanItem(event: DigitalEmployeeWorkEvent, planItem: DigitalPlanItem): boolean {
|
||||||
const haystack = `${event.conversation_id ?? ''} ${event.plan_title} ${event.task_title} ${event.step_title ?? ''} ${event.detail}`;
|
const haystack = `${event.conversation_id ?? ''} ${event.plan_title} ${event.task_title} ${event.step_title ?? ''} ${event.detail}`;
|
||||||
return haystack.includes(planItem.id) || haystack.includes(planItem.title);
|
return haystack.includes(planItem.id) || haystack.includes(planItem.title);
|
||||||
@@ -946,6 +978,7 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
|
|||||||
const [reportPreviewOpen, setReportPreviewOpen] = useState(false);
|
const [reportPreviewOpen, setReportPreviewOpen] = useState(false);
|
||||||
const [selectedSyncFailure, setSelectedSyncFailure] = useState<ManagementSyncFailure | null>(null);
|
const [selectedSyncFailure, setSelectedSyncFailure] = useState<ManagementSyncFailure | null>(null);
|
||||||
const [blockedPlanSteps, setBlockedPlanSteps] = useState<PlanStepBusinessRow[]>([]);
|
const [blockedPlanSteps, setBlockedPlanSteps] = useState<PlanStepBusinessRow[]>([]);
|
||||||
|
const [planStepGraphRows, setPlanStepGraphRows] = useState<PlanStepBusinessRow[]>([]);
|
||||||
const [blockedPlanStepError, setBlockedPlanStepError] = useState<string | null>(null);
|
const [blockedPlanStepError, setBlockedPlanStepError] = useState<string | null>(null);
|
||||||
const [blockedInputTarget, setBlockedInputTarget] = useState<BlockedPlanStepInputTarget | null>(null);
|
const [blockedInputTarget, setBlockedInputTarget] = useState<BlockedPlanStepInputTarget | null>(null);
|
||||||
const [blockedInputDraft, setBlockedInputDraft] = useState('');
|
const [blockedInputDraft, setBlockedInputDraft] = useState('');
|
||||||
@@ -1019,14 +1052,16 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
|
|||||||
setManagementSyncStatus(status);
|
setManagementSyncStatus(status);
|
||||||
});
|
});
|
||||||
|
|
||||||
const planStepGraphPromise = getPlanStepGraph({ status: 'blocked', page_size: 6 })
|
const planStepGraphPromise = getPlanStepGraph({ page_size: 80 })
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (!mountedRef.current) return;
|
if (!mountedRef.current) return;
|
||||||
setBlockedPlanSteps(response.items.filter((step) => step.status === 'blocked'));
|
setPlanStepGraphRows(response.items);
|
||||||
|
setBlockedPlanSteps(response.items.filter((step) => step.status === 'blocked').slice(0, 6));
|
||||||
setBlockedPlanStepError(null);
|
setBlockedPlanStepError(null);
|
||||||
})
|
})
|
||||||
.catch((error: unknown) => {
|
.catch((error: unknown) => {
|
||||||
if (!mountedRef.current) return;
|
if (!mountedRef.current) return;
|
||||||
|
setPlanStepGraphRows([]);
|
||||||
setBlockedPlanSteps([]);
|
setBlockedPlanSteps([]);
|
||||||
setBlockedPlanStepError(error instanceof Error ? error.message : '阻塞步骤加载失败');
|
setBlockedPlanStepError(error instanceof Error ? error.message : '阻塞步骤加载失败');
|
||||||
});
|
});
|
||||||
@@ -1066,6 +1101,7 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
|
|||||||
[isLiveProjection, workdayProjection?.duties],
|
[isLiveProjection, workdayProjection?.duties],
|
||||||
);
|
);
|
||||||
const planItems = useMemo(() => {
|
const planItems = useMemo(() => {
|
||||||
|
const orchestrationByPlan = planStepOrchestrationSummary(planStepGraphRows);
|
||||||
return schedulerJobs
|
return schedulerJobs
|
||||||
.slice()
|
.slice()
|
||||||
.sort((left, right) => {
|
.sort((left, right) => {
|
||||||
@@ -1084,9 +1120,20 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
|
|||||||
})
|
})
|
||||||
.map((job) => {
|
.map((job) => {
|
||||||
const templateId = templateIdForSchedulerJob(job);
|
const templateId = templateIdForSchedulerJob(job);
|
||||||
return buildPlanTemplateItem(job, liveProjectionDuties, selectedDutyIds.has(templateId));
|
const item = buildPlanTemplateItem(job, liveProjectionDuties, selectedDutyIds.has(templateId));
|
||||||
|
const orchestration = orchestrationByPlan.get(item.planId) ?? orchestrationByPlan.get(templateId);
|
||||||
|
if (!orchestration) return item;
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
readyStepCount: orchestration.ready,
|
||||||
|
runningStepCount: orchestration.running,
|
||||||
|
blockedStepCount: orchestration.blocked,
|
||||||
|
resultText: orchestration.ready > 0 || orchestration.running > 0 || orchestration.blocked > 0
|
||||||
|
? `Ready ${orchestration.ready} · 运行 ${orchestration.running} · 阻塞 ${orchestration.blocked}`
|
||||||
|
: item.resultText,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}, [liveProjectionDuties, schedulerJobs, selectedDutyIds]);
|
}, [liveProjectionDuties, planStepGraphRows, schedulerJobs, selectedDutyIds]);
|
||||||
const selectedDuties = useMemo(
|
const selectedDuties = useMemo(
|
||||||
() => planItems.filter((planItem) => selectedDutyIds.has(planItem.id)),
|
() => planItems.filter((planItem) => selectedDutyIds.has(planItem.id)),
|
||||||
[planItems, selectedDutyIds],
|
[planItems, selectedDutyIds],
|
||||||
@@ -1717,13 +1764,14 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
|
|||||||
throw new Error(response.error || '执行接口未接受请求');
|
throw new Error(response.error || '执行接口未接受请求');
|
||||||
}
|
}
|
||||||
const dispatchedCount = response.dispatched_tasks?.length ?? 0;
|
const dispatchedCount = response.dispatched_tasks?.length ?? 0;
|
||||||
const schedulerResult = await triggerCronCheck();
|
const readyResult = await dispatchPlanReadySteps(planItem.planId);
|
||||||
const readyDispatched = schedulerResult.readyStepDispatch?.dispatched ?? 0;
|
const readyDispatched = readyResult.dispatched ?? 0;
|
||||||
const readyFailed = schedulerResult.readyStepDispatch?.failed ?? 0;
|
const readySkipped = readyResult.skipped ?? 0;
|
||||||
|
const readyFailed = readyResult.failed ?? 0;
|
||||||
setExecutePlanItem(null);
|
setExecutePlanItem(null);
|
||||||
setActionNotice(
|
setActionNotice(
|
||||||
readyDispatched > 0 || readyFailed > 0
|
readyDispatched > 0 || readySkipped > 0 || readyFailed > 0
|
||||||
? `已触发执行:${planItem.title},新建 ${dispatchedCount} 个步骤,Ready 步骤自动派发 ${readyDispatched} 个${readyFailed > 0 ? `,失败 ${readyFailed} 个` : ''}。`
|
? `已触发执行:${planItem.title},新建 ${dispatchedCount} 个步骤,推进 ${readyDispatched} 个${readySkipped > 0 ? `,跳过 ${readySkipped} 个` : ''}${readyFailed > 0 ? `,失败 ${readyFailed} 个` : ''}。`
|
||||||
: `已触发执行:${planItem.title},已派发 ${dispatchedCount} 个步骤。`,
|
: `已触发执行:${planItem.title},已派发 ${dispatchedCount} 个步骤。`,
|
||||||
);
|
);
|
||||||
await fetchProjection();
|
await fetchProjection();
|
||||||
@@ -1736,6 +1784,32 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
|
|||||||
}
|
}
|
||||||
}, [fetchProjection]);
|
}, [fetchProjection]);
|
||||||
|
|
||||||
|
const controlPlanReadySteps = useCallback(async (planItem: DigitalPlanItem) => {
|
||||||
|
if (!isPlanItemExecutable(planItem)) {
|
||||||
|
setActionError(planExecutionDisabledReason(planItem));
|
||||||
|
setActionNotice(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const actionKey = `orchestrate:${planItem.id}`;
|
||||||
|
setActionBusy(actionKey);
|
||||||
|
setActionError(null);
|
||||||
|
setActionNotice(null);
|
||||||
|
try {
|
||||||
|
const result = await dispatchPlanReadySteps(planItem.planId);
|
||||||
|
if (result.ok === false) {
|
||||||
|
throw new Error(result.errors?.[0]?.error || '计划推进未被接受');
|
||||||
|
}
|
||||||
|
setActionNotice(`已推进:${planItem.title},派发 ${result.dispatched ?? 0} 个,跳过 ${result.skipped ?? 0} 个${(result.failed ?? 0) > 0 ? `,失败 ${result.failed}` : ''}。`);
|
||||||
|
await fetchProjection();
|
||||||
|
} catch (error) {
|
||||||
|
if (mountedRef.current) {
|
||||||
|
setActionError(error instanceof Error ? error.message : '计划推进失败');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (mountedRef.current) setActionBusy(null);
|
||||||
|
}
|
||||||
|
}, [fetchProjection]);
|
||||||
|
|
||||||
const dismissWorkdayGuide = useCallback(() => {
|
const dismissWorkdayGuide = useCallback(() => {
|
||||||
setGuideOpen(false);
|
setGuideOpen(false);
|
||||||
setGuideDismissedDate(selectedDate);
|
setGuideDismissedDate(selectedDate);
|
||||||
@@ -1782,6 +1856,16 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
|
|||||||
进入处理
|
进入处理
|
||||||
</button>
|
</button>
|
||||||
<div className="de-duty-card-actions">
|
<div className="de-duty-card-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="de-btn-secondary de-btn-sm"
|
||||||
|
disabled={!planItem.implemented || planItem.readyStepCount === 0 || Boolean(actionBusy)}
|
||||||
|
title={planItem.readyStepCount > 0 ? '推进此计划的 Ready 步骤' : '暂无可推进 Ready 步骤'}
|
||||||
|
onClick={() => { void controlPlanReadySteps(planItem); }}
|
||||||
|
>
|
||||||
|
<TimerReset size={13} aria-hidden="true" />
|
||||||
|
<span>{actionBusy === `orchestrate:${planItem.id}` ? '推进中' : planReadyStepActionLabel(planItem)}</span>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="de-btn-secondary de-btn-sm"
|
className="de-btn-secondary de-btn-sm"
|
||||||
@@ -1807,7 +1891,7 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}, [actionBusy, openDuty, selectedDutyIds, toggleDuty]);
|
}, [actionBusy, controlPlanReadySteps, openDuty, selectedDutyIds, toggleDuty]);
|
||||||
|
|
||||||
const openEventDetail = useCallback((event: DigitalEmployeeWorkEvent) => {
|
const openEventDetail = useCallback((event: DigitalEmployeeWorkEvent) => {
|
||||||
setSelectedEvent(event);
|
setSelectedEvent(event);
|
||||||
@@ -2043,6 +2127,16 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
|
|||||||
<span>{planItem.resultText}</span>
|
<span>{planItem.resultText}</span>
|
||||||
</button>
|
</button>
|
||||||
<div className="de-template-item-actions">
|
<div className="de-template-item-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="de-btn-secondary de-btn-sm"
|
||||||
|
disabled={!planItem.implemented || planItem.readyStepCount === 0 || Boolean(actionBusy)}
|
||||||
|
title={planItem.readyStepCount > 0 ? '推进此计划的 Ready 步骤' : '暂无可推进 Ready 步骤'}
|
||||||
|
onClick={() => { void controlPlanReadySteps(planItem); }}
|
||||||
|
>
|
||||||
|
<TimerReset size={13} aria-hidden="true" />
|
||||||
|
<span>{actionBusy === `orchestrate:${planItem.id}` ? '推进中' : '推进'}</span>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="de-btn-secondary de-btn-sm"
|
className="de-btn-secondary de-btn-sm"
|
||||||
|
|||||||
@@ -445,10 +445,17 @@ export interface GovernanceCommandResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ReadyStepDispatchResult {
|
export interface ReadyStepDispatchResult {
|
||||||
|
ok?: boolean;
|
||||||
|
plan_id?: string | null;
|
||||||
|
trigger_source?: 'auto' | 'manual' | string;
|
||||||
|
orchestration_id?: string;
|
||||||
candidates: number;
|
candidates: number;
|
||||||
dispatched: number;
|
dispatched: number;
|
||||||
skipped: number;
|
skipped: number;
|
||||||
failed: number;
|
failed: number;
|
||||||
|
dispatched_step_ids?: string[];
|
||||||
|
skipped_step_ids?: string[];
|
||||||
|
failed_step_ids?: string[];
|
||||||
errors?: Array<{ stepId: string; error: string }>;
|
errors?: Array<{ stepId: string; error: 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);
|
console.warn("[qimingclaw] digital employee auth bootstrap skipped", error);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script type="module" crossorigin src="./assets/index-BRZXqRQH.js"></script>
|
<script type="module" crossorigin src="./assets/index-DxiObdwW.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="./assets/index-4TVkOCsf.css">
|
<link rel="stylesheet" crossorigin href="./assets/index-4TVkOCsf.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -293,6 +293,8 @@ function checkDigitalPlanStepGraphDispatchPolicy() {
|
|||||||
const stateServicePath = path.join(packageRoot, "src", "main", "services", "digitalEmployee", "stateService.ts");
|
const stateServicePath = path.join(packageRoot, "src", "main", "services", "digitalEmployee", "stateService.ts");
|
||||||
const schedulerServicePath = path.join(packageRoot, "src", "main", "services", "digitalEmployee", "schedulerService.ts");
|
const schedulerServicePath = path.join(packageRoot, "src", "main", "services", "digitalEmployee", "schedulerService.ts");
|
||||||
const syncServicePath = path.join(packageRoot, "src", "main", "services", "digitalEmployee", "syncService.ts");
|
const syncServicePath = path.join(packageRoot, "src", "main", "services", "digitalEmployee", "syncService.ts");
|
||||||
|
const ipcHandlersPath = path.join(packageRoot, "src", "main", "ipc", "digitalEmployeeHandlers.ts");
|
||||||
|
const preloadPath = path.join(packageRoot, "src", "preload", "webviewPerfBridge.ts");
|
||||||
const adapter = fs.readFileSync(adapterPath, "utf8");
|
const adapter = fs.readFileSync(adapterPath, "utf8");
|
||||||
const api = fs.readFileSync(apiPath, "utf8");
|
const api = fs.readFileSync(apiPath, "utf8");
|
||||||
const types = fs.readFileSync(typesPath, "utf8");
|
const types = fs.readFileSync(typesPath, "utf8");
|
||||||
@@ -301,9 +303,13 @@ function checkDigitalPlanStepGraphDispatchPolicy() {
|
|||||||
const stateService = fs.readFileSync(stateServicePath, "utf8");
|
const stateService = fs.readFileSync(stateServicePath, "utf8");
|
||||||
const schedulerService = fs.readFileSync(schedulerServicePath, "utf8");
|
const schedulerService = fs.readFileSync(schedulerServicePath, "utf8");
|
||||||
const syncService = fs.readFileSync(syncServicePath, "utf8");
|
const syncService = fs.readFileSync(syncServicePath, "utf8");
|
||||||
|
const ipcHandlers = fs.readFileSync(ipcHandlersPath, "utf8");
|
||||||
|
const preload = fs.readFileSync(preloadPath, "utf8");
|
||||||
const requiredSnippets = [
|
const requiredSnippets = [
|
||||||
[adapter, "/api/digital-employee/plan-steps", "adapter plan step endpoint"],
|
[adapter, "/api/digital-employee/plan-steps", "adapter plan step endpoint"],
|
||||||
[adapter, "/api/digital-employee/plan-step-graph", "adapter plan step graph endpoint"],
|
[adapter, "/api/digital-employee/plan-step-graph", "adapter plan step graph endpoint"],
|
||||||
|
[adapter, "dispatch-ready-steps", "adapter plan-scoped ready step dispatch endpoint"],
|
||||||
|
[adapter, "dispatchPlanReadySteps", "adapter plan-scoped ready step dispatcher"],
|
||||||
[adapter, "/api/plan-step/dispatch-policy", "adapter plan step dispatch policy endpoint"],
|
[adapter, "/api/plan-step/dispatch-policy", "adapter plan step dispatch policy endpoint"],
|
||||||
[adapter, "planStepRows", "management plan step rows"],
|
[adapter, "planStepRows", "management plan step rows"],
|
||||||
[adapter, "planStepGraphRows", "management plan step graph rows"],
|
[adapter, "planStepGraphRows", "management plan step graph rows"],
|
||||||
@@ -315,21 +321,34 @@ function checkDigitalPlanStepGraphDispatchPolicy() {
|
|||||||
[api, "patchPlanStepDispatchPolicy", "plan step dispatch policy API patcher"],
|
[api, "patchPlanStepDispatchPolicy", "plan step dispatch policy API patcher"],
|
||||||
[api, "getPlanSteps", "plan steps API getter"],
|
[api, "getPlanSteps", "plan steps API getter"],
|
||||||
[api, "getPlanStepGraph", "plan step graph API getter"],
|
[api, "getPlanStepGraph", "plan step graph API getter"],
|
||||||
|
[api, "dispatchPlanReadySteps", "plan-scoped ready step dispatch API"],
|
||||||
[api, "BusinessViewResponse<PlanStepBusinessRow>", "typed plan step business view API"],
|
[api, "BusinessViewResponse<PlanStepBusinessRow>", "typed plan step business view API"],
|
||||||
|
[types, "trigger_source", "ready step dispatch trigger source type"],
|
||||||
|
[types, "orchestration_id", "ready step dispatch orchestration id type"],
|
||||||
[types, "PlanStepDispatchPolicy", "plan step dispatch policy type"],
|
[types, "PlanStepDispatchPolicy", "plan step dispatch policy type"],
|
||||||
[types, "PlanStepDependencyDetail", "plan step blocked dependency detail type"],
|
[types, "PlanStepDependencyDetail", "plan step blocked dependency detail type"],
|
||||||
[types, "PlanStepBusinessRow", "plan step business row type"],
|
[types, "PlanStepBusinessRow", "plan step business row type"],
|
||||||
[types, "BusinessViewResponse", "business view response type"],
|
[types, "BusinessViewResponse", "business view response type"],
|
||||||
[commandDeck, "getPlanStepGraph", "home blocked plan step graph fetch"],
|
[commandDeck, "getPlanStepGraph", "home blocked plan step graph fetch"],
|
||||||
|
[commandDeck, "planStepOrchestrationSummary", "home plan step orchestration summary"],
|
||||||
|
[commandDeck, "controlPlanReadySteps", "home plan-scoped ready step control"],
|
||||||
[commandDeck, "controlBlockedPlanStep", "home blocked plan step control handler"],
|
[commandDeck, "controlBlockedPlanStep", "home blocked plan step control handler"],
|
||||||
[commandDeck, "de-blocked-step-control-strip", "home blocked plan step control strip"],
|
[commandDeck, "de-blocked-step-control-strip", "home blocked plan step control strip"],
|
||||||
[commandDeck, "provide_task_input", "home blocked plan step input command"],
|
[commandDeck, "provide_task_input", "home blocked plan step input command"],
|
||||||
[css, "de-blocked-input-field", "home blocked plan step input styles"],
|
[css, "de-blocked-input-field", "home blocked plan step input styles"],
|
||||||
[stateService, "planStepDispatchPolicy", "plan step dispatch policy UI state"],
|
[stateService, "planStepDispatchPolicy", "plan step dispatch policy UI state"],
|
||||||
[stateService, "update_plan_step_dispatch_policy", "plan step dispatch policy runtime event"],
|
[stateService, "update_plan_step_dispatch_policy", "plan step dispatch policy runtime event"],
|
||||||
|
[stateService, "planIdFilter", "plan-scoped ready step candidate filter"],
|
||||||
|
[schedulerService, "bypassAutoDispatchSwitch", "manual plan dispatch auto switch bypass"],
|
||||||
|
[schedulerService, "runDigitalEmployeePlanReadyStepsNow", "manual plan-scoped ready step dispatcher"],
|
||||||
|
[schedulerService, "triggerSource", "ready step dispatch trigger source"],
|
||||||
[schedulerService, "readDigitalEmployeePlanStepDispatchPolicy", "scheduler plan step dispatch policy reader"],
|
[schedulerService, "readDigitalEmployeePlanStepDispatchPolicy", "scheduler plan step dispatch policy reader"],
|
||||||
[schedulerService, "max_per_sweep", "scheduler max per sweep policy"],
|
[schedulerService, "max_per_sweep", "scheduler max per sweep policy"],
|
||||||
|
[ipcHandlers, "digitalEmployee:dispatchPlanReadySteps", "IPC plan-scoped ready step dispatch handler"],
|
||||||
|
[preload, "dispatchPlanReadySteps", "preload plan-scoped ready step bridge"],
|
||||||
[syncService, "planStepBusinessView", "plan step sync business view"],
|
[syncService, "planStepBusinessView", "plan step sync business view"],
|
||||||
|
[syncService, "trigger_source", "plan step dispatch sync trigger source"],
|
||||||
|
[syncService, "orchestration_id", "plan step dispatch sync orchestration id"],
|
||||||
[syncService, "planStepDispatchPolicyBusinessView", "plan step dispatch policy sync view"],
|
[syncService, "planStepDispatchPolicyBusinessView", "plan step dispatch policy sync view"],
|
||||||
[syncService, "digital_workday_update_plan_step_dispatch_policy", "plan step dispatch policy sync event kind"],
|
[syncService, "digital_workday_update_plan_step_dispatch_policy", "plan step dispatch policy sync event kind"],
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ import {
|
|||||||
type DigitalEmployeeUiStateUpdate,
|
type DigitalEmployeeUiStateUpdate,
|
||||||
} from "../services/digitalEmployee/stateService";
|
} from "../services/digitalEmployee/stateService";
|
||||||
import {
|
import {
|
||||||
|
runDigitalEmployeePlanReadyStepsNow,
|
||||||
runDigitalEmployeeScheduleNow,
|
runDigitalEmployeeScheduleNow,
|
||||||
runDigitalEmployeeSchedulerCheck,
|
runDigitalEmployeeSchedulerCheck,
|
||||||
} from "../services/digitalEmployee/schedulerService";
|
} from "../services/digitalEmployee/schedulerService";
|
||||||
@@ -203,6 +204,10 @@ export function registerDigitalEmployeeHandlers(ctx: HandlerContext): void {
|
|||||||
return runDigitalEmployeeScheduleNow(scheduleId);
|
return runDigitalEmployeeScheduleNow(scheduleId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("digitalEmployee:dispatchPlanReadySteps", async (_, planId: string) => {
|
||||||
|
return runDigitalEmployeePlanReadyStepsNow(planId);
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle("digitalEmployee:getScheduleRuns", async (_, scheduleId: string, limit?: number) => {
|
ipcMain.handle("digitalEmployee:getScheduleRuns", async (_, scheduleId: string, limit?: number) => {
|
||||||
return readDigitalEmployeeScheduleRuns(scheduleId, limit ?? 20);
|
return readDigitalEmployeeScheduleRuns(scheduleId, limit ?? 20);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -33,7 +33,10 @@ vi.mock("../constants", () => ({
|
|||||||
vi.mock("./stateService", () => ({
|
vi.mock("./stateService", () => ({
|
||||||
readDigitalEmployeeCronSettings: () => mockState.settings,
|
readDigitalEmployeeCronSettings: () => mockState.settings,
|
||||||
readDigitalEmployeePlanStepDispatchPolicy: () => mockState.dispatchPolicy,
|
readDigitalEmployeePlanStepDispatchPolicy: () => mockState.dispatchPolicy,
|
||||||
listDigitalEmployeeReadyPlanStepDispatchCandidates: () => mockState.readyCandidates,
|
listDigitalEmployeeReadyPlanStepDispatchCandidates: (options?: number | { planId?: string | null }) => {
|
||||||
|
const planId = typeof options === "object" && options ? options.planId : null;
|
||||||
|
return planId ? mockState.readyCandidates.filter((candidate) => candidate.planId === planId) : mockState.readyCandidates;
|
||||||
|
},
|
||||||
listDigitalEmployeeSchedules: () => mockState.schedules,
|
listDigitalEmployeeSchedules: () => mockState.schedules,
|
||||||
listDueDigitalEmployeeSchedules: () => mockState.schedules,
|
listDueDigitalEmployeeSchedules: () => mockState.schedules,
|
||||||
readDigitalEmployeeScheduleRuns: () => [],
|
readDigitalEmployeeScheduleRuns: () => [],
|
||||||
@@ -203,6 +206,58 @@ describe("digital employee scheduler service", () => {
|
|||||||
expect(mockState.dispatchEvents).toHaveLength(0);
|
expect(mockState.dispatchEvents).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("allows manual plan dispatch to bypass the auto dispatch switch", async () => {
|
||||||
|
mockState.readyCandidates = [readyStepCandidate()];
|
||||||
|
mockState.dispatchPolicy = { ...mockState.dispatchPolicy, auto_dispatch_ready_steps: false };
|
||||||
|
const { dispatchReadyPlanSteps } = await import("./schedulerService");
|
||||||
|
|
||||||
|
const result = await dispatchReadyPlanSteps({
|
||||||
|
planId: "plan-1",
|
||||||
|
triggerSource: "manual",
|
||||||
|
bypassAutoDispatchSwitch: true,
|
||||||
|
now: new Date("2026-06-07T01:00:10.000Z"),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toMatchObject({ plan_id: "plan-1", trigger_source: "manual", candidates: 1, dispatched: 1, skipped: 0, failed: 0 });
|
||||||
|
expect(mockState.dispatchEvents[0]).toMatchObject({ status: "started", payload: expect.objectContaining({ trigger_source: "manual" }) });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps the enabled switch as a manual plan dispatch kill switch", async () => {
|
||||||
|
mockState.readyCandidates = [readyStepCandidate()];
|
||||||
|
mockState.dispatchPolicy = { ...mockState.dispatchPolicy, enabled: false, auto_dispatch_ready_steps: false };
|
||||||
|
const { dispatchReadyPlanSteps } = await import("./schedulerService");
|
||||||
|
|
||||||
|
const result = await dispatchReadyPlanSteps({
|
||||||
|
planId: "plan-1",
|
||||||
|
triggerSource: "manual",
|
||||||
|
bypassAutoDispatchSwitch: true,
|
||||||
|
now: new Date("2026-06-07T01:00:10.000Z"),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toMatchObject({ candidates: 1, dispatched: 0, skipped: 1, failed: 0 });
|
||||||
|
expect(fetch).not.toHaveBeenCalled();
|
||||||
|
expect(mockState.dispatchEvents).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("dispatches only the requested plan in manual plan dispatch", async () => {
|
||||||
|
mockState.readyCandidates = [
|
||||||
|
readyStepCandidate(),
|
||||||
|
readyStepCandidate({ planId: "plan-2", stepId: "step-plan-2", taskId: "task-plan-2" }),
|
||||||
|
];
|
||||||
|
const { dispatchReadyPlanSteps } = await import("./schedulerService");
|
||||||
|
|
||||||
|
const result = await dispatchReadyPlanSteps({
|
||||||
|
planId: "plan-2",
|
||||||
|
triggerSource: "manual",
|
||||||
|
bypassAutoDispatchSwitch: true,
|
||||||
|
now: new Date("2026-06-07T01:00:10.000Z"),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toMatchObject({ plan_id: "plan-2", candidates: 1, dispatched: 1, skipped: 0, failed: 0 });
|
||||||
|
expect(result.dispatched_step_ids).toEqual(["step-plan-2"]);
|
||||||
|
expect(mockState.dispatchEvents.map((event) => event.stepId)).toEqual(["step-plan-2", "step-plan-2"]);
|
||||||
|
});
|
||||||
|
|
||||||
it("limits ready step dispatches by policy max per sweep", async () => {
|
it("limits ready step dispatches by policy max per sweep", async () => {
|
||||||
mockState.readyCandidates = [
|
mockState.readyCandidates = [
|
||||||
readyStepCandidate(),
|
readyStepCandidate(),
|
||||||
@@ -219,18 +274,22 @@ describe("digital employee scheduler service", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function readyStepCandidate(): Record<string, unknown> {
|
function readyStepCandidate(overrides: Record<string, unknown> = {}): Record<string, unknown> {
|
||||||
|
const stepId = typeof overrides.stepId === "string" ? overrides.stepId : "step-ready";
|
||||||
|
const taskId = typeof overrides.taskId === "string" ? overrides.taskId : "task-1";
|
||||||
|
const planId = typeof overrides.planId === "string" ? overrides.planId : "plan-1";
|
||||||
return {
|
return {
|
||||||
stepId: "step-ready",
|
stepId,
|
||||||
planId: "plan-1",
|
planId,
|
||||||
taskId: "task-1",
|
taskId,
|
||||||
title: "汇总客户跟进",
|
title: "汇总客户跟进",
|
||||||
taskTitle: "执行客户跟进汇总",
|
taskTitle: "执行客户跟进汇总",
|
||||||
prompt: "执行客户跟进汇总",
|
prompt: "执行客户跟进汇总",
|
||||||
preferredSkillIds: ["qimingclaw-acp-session"],
|
preferredSkillIds: ["qimingclaw-acp-session"],
|
||||||
dispatchable: true,
|
dispatchable: true,
|
||||||
step: { id: "step-ready", payload: {} },
|
step: { id: stepId, payload: {} },
|
||||||
task: { id: "task-1", payload: {} },
|
task: { id: taskId, payload: {} },
|
||||||
plan: { id: "plan-1" },
|
plan: { id: planId },
|
||||||
|
...overrides,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,13 +35,29 @@ export interface DigitalEmployeeSchedulerCheckResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface DigitalEmployeeReadyStepDispatchResult {
|
export interface DigitalEmployeeReadyStepDispatchResult {
|
||||||
|
ok?: boolean;
|
||||||
|
plan_id?: string | null;
|
||||||
|
trigger_source?: DigitalEmployeeReadyStepDispatchTriggerSource;
|
||||||
|
orchestration_id?: string;
|
||||||
candidates: number;
|
candidates: number;
|
||||||
dispatched: number;
|
dispatched: number;
|
||||||
skipped: number;
|
skipped: number;
|
||||||
failed: number;
|
failed: number;
|
||||||
|
dispatched_step_ids?: string[];
|
||||||
|
skipped_step_ids?: string[];
|
||||||
|
failed_step_ids?: string[];
|
||||||
errors: Array<{ stepId: string; error: string }>;
|
errors: Array<{ stepId: string; error: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type DigitalEmployeeReadyStepDispatchTriggerSource = "auto" | "manual";
|
||||||
|
|
||||||
|
export interface DigitalEmployeeReadyStepDispatchOptions {
|
||||||
|
now?: Date;
|
||||||
|
planId?: string | null;
|
||||||
|
triggerSource?: DigitalEmployeeReadyStepDispatchTriggerSource;
|
||||||
|
bypassAutoDispatchSwitch?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export function startDigitalEmployeeScheduler(): void {
|
export function startDigitalEmployeeScheduler(): void {
|
||||||
if (schedulerTimer) return;
|
if (schedulerTimer) return;
|
||||||
schedulerTimer = setInterval(() => {
|
schedulerTimer = setInterval(() => {
|
||||||
@@ -107,7 +123,7 @@ export async function runDigitalEmployeeSchedulerCheck(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result.readyStepDispatch = await dispatchReadyPlanSteps(now);
|
result.readyStepDispatch = await dispatchReadyPlanSteps({ now, triggerSource: "auto" });
|
||||||
} finally {
|
} finally {
|
||||||
schedulerSweeping = false;
|
schedulerSweeping = false;
|
||||||
}
|
}
|
||||||
@@ -116,19 +132,32 @@ export async function runDigitalEmployeeSchedulerCheck(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function dispatchReadyPlanSteps(
|
export async function dispatchReadyPlanSteps(
|
||||||
now = new Date(),
|
options: Date | DigitalEmployeeReadyStepDispatchOptions = {},
|
||||||
): Promise<DigitalEmployeeReadyStepDispatchResult> {
|
): Promise<DigitalEmployeeReadyStepDispatchResult> {
|
||||||
const candidates = listDigitalEmployeeReadyPlanStepDispatchCandidates(20);
|
const now = options instanceof Date ? options : options.now ?? new Date();
|
||||||
|
const planId = options instanceof Date ? null : stringFromUnknown(options.planId) || null;
|
||||||
|
const triggerSource = options instanceof Date ? "auto" : options.triggerSource ?? "auto";
|
||||||
|
const bypassAutoDispatchSwitch = !(options instanceof Date) && options.bypassAutoDispatchSwitch === true;
|
||||||
|
const orchestrationId = `${triggerSource}-ready-steps-${safeId(planId || "all")}-${safeId(now.toISOString())}`;
|
||||||
|
const candidates = listDigitalEmployeeReadyPlanStepDispatchCandidates({ limit: 20, planId });
|
||||||
const result: DigitalEmployeeReadyStepDispatchResult = {
|
const result: DigitalEmployeeReadyStepDispatchResult = {
|
||||||
|
ok: true,
|
||||||
|
plan_id: planId,
|
||||||
|
trigger_source: triggerSource,
|
||||||
|
orchestration_id: orchestrationId,
|
||||||
candidates: candidates.length,
|
candidates: candidates.length,
|
||||||
dispatched: 0,
|
dispatched: 0,
|
||||||
skipped: 0,
|
skipped: 0,
|
||||||
failed: 0,
|
failed: 0,
|
||||||
|
dispatched_step_ids: [],
|
||||||
|
skipped_step_ids: [],
|
||||||
|
failed_step_ids: [],
|
||||||
errors: [],
|
errors: [],
|
||||||
};
|
};
|
||||||
const policy = readDigitalEmployeePlanStepDispatchPolicy();
|
const policy = readDigitalEmployeePlanStepDispatchPolicy();
|
||||||
if (!policy.enabled || !policy.auto_dispatch_ready_steps) {
|
if (!policy.enabled || (!policy.auto_dispatch_ready_steps && !bypassAutoDispatchSwitch)) {
|
||||||
result.skipped = candidates.length;
|
result.skipped = candidates.length;
|
||||||
|
result.skipped_step_ids = candidates.map((candidate) => candidate.stepId);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
const maxPerSweep = Math.max(1, Math.min(Math.floor(policy.max_per_sweep), 10));
|
const maxPerSweep = Math.max(1, Math.min(Math.floor(policy.max_per_sweep), 10));
|
||||||
@@ -136,19 +165,23 @@ export async function dispatchReadyPlanSteps(
|
|||||||
for (const candidate of candidates) {
|
for (const candidate of candidates) {
|
||||||
if (!candidate.dispatchable || !candidate.taskId) {
|
if (!candidate.dispatchable || !candidate.taskId) {
|
||||||
result.skipped += 1;
|
result.skipped += 1;
|
||||||
|
result.skipped_step_ids?.push(candidate.stepId);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (attempts >= maxPerSweep) {
|
if (attempts >= maxPerSweep) {
|
||||||
result.skipped += 1;
|
result.skipped += 1;
|
||||||
|
result.skipped_step_ids?.push(candidate.stepId);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
attempts += 1;
|
attempts += 1;
|
||||||
try {
|
try {
|
||||||
await dispatchReadyPlanStep(candidate, now);
|
await dispatchReadyPlanStep(candidate, now, { triggerSource, orchestrationId, candidateCount: candidates.length });
|
||||||
result.dispatched += 1;
|
result.dispatched += 1;
|
||||||
|
result.dispatched_step_ids?.push(candidate.stepId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = normalizeError(error);
|
const message = normalizeError(error);
|
||||||
result.failed += 1;
|
result.failed += 1;
|
||||||
|
result.failed_step_ids?.push(candidate.stepId);
|
||||||
result.errors.push({ stepId: candidate.stepId, error: message });
|
result.errors.push({ stepId: candidate.stepId, error: message });
|
||||||
recordDigitalEmployeePlanStepDispatch({
|
recordDigitalEmployeePlanStepDispatch({
|
||||||
stepId: candidate.stepId,
|
stepId: candidate.stepId,
|
||||||
@@ -156,6 +189,8 @@ export async function dispatchReadyPlanSteps(
|
|||||||
taskId: candidate.taskId,
|
taskId: candidate.taskId,
|
||||||
status: "failed",
|
status: "failed",
|
||||||
reason: message,
|
reason: message,
|
||||||
|
source: "qimingclaw-ready-step-dispatcher",
|
||||||
|
payload: { trigger_source: triggerSource, orchestration_id: orchestrationId, candidate_count: candidates.length },
|
||||||
occurredAt: now.toISOString(),
|
occurredAt: now.toISOString(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -164,12 +199,22 @@ export async function dispatchReadyPlanSteps(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function emptyReadyStepDispatchResult(): DigitalEmployeeReadyStepDispatchResult {
|
function emptyReadyStepDispatchResult(): DigitalEmployeeReadyStepDispatchResult {
|
||||||
return { candidates: 0, dispatched: 0, skipped: 0, failed: 0, errors: [] };
|
return {
|
||||||
|
candidates: 0,
|
||||||
|
dispatched: 0,
|
||||||
|
skipped: 0,
|
||||||
|
failed: 0,
|
||||||
|
dispatched_step_ids: [],
|
||||||
|
skipped_step_ids: [],
|
||||||
|
failed_step_ids: [],
|
||||||
|
errors: [],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function dispatchReadyPlanStep(
|
async function dispatchReadyPlanStep(
|
||||||
candidate: DigitalEmployeeReadyPlanStepDispatchCandidate,
|
candidate: DigitalEmployeeReadyPlanStepDispatchCandidate,
|
||||||
now: Date,
|
now: Date,
|
||||||
|
orchestration: { triggerSource: DigitalEmployeeReadyStepDispatchTriggerSource; orchestrationId: string; candidateCount: number },
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const occurredAt = now.toISOString();
|
const occurredAt = now.toISOString();
|
||||||
const commandId = `ready-step-${safeId(candidate.stepId)}-${safeId(occurredAt)}`;
|
const commandId = `ready-step-${safeId(candidate.stepId)}-${safeId(occurredAt)}`;
|
||||||
@@ -180,7 +225,13 @@ async function dispatchReadyPlanStep(
|
|||||||
taskId: candidate.taskId,
|
taskId: candidate.taskId,
|
||||||
runId,
|
runId,
|
||||||
status: "started",
|
status: "started",
|
||||||
payload: { command_id: commandId, preferred_skill_ids: candidate.preferredSkillIds },
|
payload: {
|
||||||
|
command_id: commandId,
|
||||||
|
preferred_skill_ids: candidate.preferredSkillIds,
|
||||||
|
trigger_source: orchestration.triggerSource,
|
||||||
|
orchestration_id: orchestration.orchestrationId,
|
||||||
|
candidate_count: orchestration.candidateCount,
|
||||||
|
},
|
||||||
occurredAt,
|
occurredAt,
|
||||||
});
|
});
|
||||||
const chatRequest = buildReadyStepChatRequest(candidate, runId, occurredAt);
|
const chatRequest = buildReadyStepChatRequest(candidate, runId, occurredAt);
|
||||||
@@ -200,6 +251,8 @@ async function dispatchReadyPlanStep(
|
|||||||
plan_id: candidate.planId,
|
plan_id: candidate.planId,
|
||||||
task_id: candidate.taskId,
|
task_id: candidate.taskId,
|
||||||
step_id: candidate.stepId,
|
step_id: candidate.stepId,
|
||||||
|
trigger_source: orchestration.triggerSource,
|
||||||
|
orchestration_id: orchestration.orchestrationId,
|
||||||
title: candidate.title,
|
title: candidate.title,
|
||||||
task_title: candidate.taskTitle,
|
task_title: candidate.taskTitle,
|
||||||
prompt: candidate.prompt,
|
prompt: candidate.prompt,
|
||||||
@@ -214,7 +267,13 @@ async function dispatchReadyPlanStep(
|
|||||||
taskId: candidate.taskId,
|
taskId: candidate.taskId,
|
||||||
runId: ids.runId,
|
runId: ids.runId,
|
||||||
status: "completed",
|
status: "completed",
|
||||||
payload: { command_id: commandId, response },
|
payload: {
|
||||||
|
command_id: commandId,
|
||||||
|
response,
|
||||||
|
trigger_source: orchestration.triggerSource,
|
||||||
|
orchestration_id: orchestration.orchestrationId,
|
||||||
|
candidate_count: orchestration.candidateCount,
|
||||||
|
},
|
||||||
occurredAt,
|
occurredAt,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -232,6 +291,20 @@ export async function runDigitalEmployeeScheduleNow(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function runDigitalEmployeePlanReadyStepsNow(
|
||||||
|
planId: string,
|
||||||
|
): Promise<DigitalEmployeeReadyStepDispatchResult> {
|
||||||
|
const targetPlanId = stringFromUnknown(planId);
|
||||||
|
if (!targetPlanId) {
|
||||||
|
return { ...emptyReadyStepDispatchResult(), ok: false, plan_id: null, trigger_source: "manual", errors: [{ stepId: "", error: "plan id required" }] };
|
||||||
|
}
|
||||||
|
return dispatchReadyPlanSteps({
|
||||||
|
planId: targetPlanId,
|
||||||
|
triggerSource: "manual",
|
||||||
|
bypassAutoDispatchSwitch: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function initializeScheduleNextRuns(now = new Date()): Promise<void> {
|
async function initializeScheduleNextRuns(now = new Date()): Promise<void> {
|
||||||
for (const schedule of listDigitalEmployeeSchedules()) {
|
for (const schedule of listDigitalEmployeeSchedules()) {
|
||||||
if (!schedule.enabled || schedule.status !== "enabled" || schedule.deletedAt) continue;
|
if (!schedule.enabled || schedule.status !== "enabled" || schedule.deletedAt) continue;
|
||||||
|
|||||||
@@ -1391,6 +1391,20 @@ describe("digital employee state service", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("filters ready plan step dispatch candidates by plan", async () => {
|
||||||
|
const { listDigitalEmployeeReadyPlanStepDispatchCandidates } = await import("./stateService");
|
||||||
|
|
||||||
|
createGovernableTask({ taskStatus: "pending", stepStatus: "ready", runStatus: "completed", finishedAt: "2026-06-07T07:20:00.000Z" });
|
||||||
|
addReadyPlanStepWithTask({ planId: "plan-other", stepId: "step-other-ready", taskId: "task-other-ready" });
|
||||||
|
|
||||||
|
expect(listDigitalEmployeeReadyPlanStepDispatchCandidates({ planId: "plan-governance" })).toEqual([
|
||||||
|
expect.objectContaining({ planId: "plan-governance", stepId: "step-governance" }),
|
||||||
|
]);
|
||||||
|
expect(listDigitalEmployeeReadyPlanStepDispatchCandidates({ planId: "plan-other" })).toEqual([
|
||||||
|
expect.objectContaining({ planId: "plan-other", stepId: "step-other-ready" }),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it("records ready plan step dispatch events and updates step status", async () => {
|
it("records ready plan step dispatch events and updates step status", async () => {
|
||||||
const { recordDigitalEmployeePlanStepDispatch } = await import("./stateService");
|
const { recordDigitalEmployeePlanStepDispatch } = await import("./stateService");
|
||||||
|
|
||||||
@@ -1512,6 +1526,11 @@ describe("digital employee state service", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(mockState.db?.planSteps.get("step-dependent")).toMatchObject({ status: "ready" });
|
expect(mockState.db?.planSteps.get("step-dependent")).toMatchObject({ status: "ready" });
|
||||||
|
addReadyPlanStepWithTask({ planId: "plan-governance", stepId: "step-dependent", taskId: "task-dependent" });
|
||||||
|
const { listDigitalEmployeeReadyPlanStepDispatchCandidates } = await import("./stateService");
|
||||||
|
expect(listDigitalEmployeeReadyPlanStepDispatchCandidates({ planId: "plan-governance" })).toEqual(expect.arrayContaining([
|
||||||
|
expect.objectContaining({ stepId: "step-dependent", dispatchable: true }),
|
||||||
|
]));
|
||||||
expect(JSON.parse(mockState.db?.planSteps.get("step-dependent")?.payload ?? "{}")).toMatchObject({
|
expect(JSON.parse(mockState.db?.planSteps.get("step-dependent")?.payload ?? "{}")).toMatchObject({
|
||||||
dependencyGraph: {
|
dependencyGraph: {
|
||||||
status: "ready",
|
status: "ready",
|
||||||
@@ -1856,6 +1875,50 @@ function addPlanStep(options: {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addReadyPlanStepWithTask(options: {
|
||||||
|
planId: string;
|
||||||
|
stepId: string;
|
||||||
|
taskId: string;
|
||||||
|
}): void {
|
||||||
|
if (!mockState.db?.plans.has(options.planId)) {
|
||||||
|
mockState.db?.plans.set(options.planId, {
|
||||||
|
id: options.planId,
|
||||||
|
title: options.planId,
|
||||||
|
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",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const existingStep = mockState.db?.planSteps.get(options.stepId);
|
||||||
|
mockState.db?.planSteps.set(options.stepId, {
|
||||||
|
id: options.stepId,
|
||||||
|
plan_id: options.planId,
|
||||||
|
title: existingStep?.title ?? options.stepId,
|
||||||
|
status: "ready",
|
||||||
|
seq: existingStep?.seq ?? 1,
|
||||||
|
depends_on: existingStep?.depends_on ?? JSON.stringify([]),
|
||||||
|
preferred_skill_ids: existingStep?.preferred_skill_ids ?? JSON.stringify(["qimingclaw-task-agent"]),
|
||||||
|
payload: existingStep?.payload ?? JSON.stringify({ source: "test" }),
|
||||||
|
sync_status: existingStep?.sync_status ?? "synced",
|
||||||
|
created_at: existingStep?.created_at ?? "2026-06-07T07:00:00.000Z",
|
||||||
|
updated_at: "2026-06-07T07:00:00.000Z",
|
||||||
|
});
|
||||||
|
mockState.db?.tasks.set(options.taskId, {
|
||||||
|
id: options.taskId,
|
||||||
|
plan_id: options.planId,
|
||||||
|
title: `${options.stepId} 任务`,
|
||||||
|
status: "pending",
|
||||||
|
assigned_skill_id: "qimingclaw-task-agent",
|
||||||
|
payload: JSON.stringify({ source: "test", stepId: options.stepId }),
|
||||||
|
sync_status: "synced",
|
||||||
|
created_at: "2026-06-07T07:00:00.000Z",
|
||||||
|
updated_at: "2026-06-07T07:00:00.000Z",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function addGovernableTaskForStep(options: {
|
function addGovernableTaskForStep(options: {
|
||||||
taskId: string;
|
taskId: string;
|
||||||
runId: string;
|
runId: string;
|
||||||
|
|||||||
@@ -1385,8 +1385,10 @@ export function recordDigitalEmployeeApprovalAction(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function listDigitalEmployeeReadyPlanStepDispatchCandidates(
|
export function listDigitalEmployeeReadyPlanStepDispatchCandidates(
|
||||||
limit = 20,
|
options: number | { limit?: number; planId?: string | null } = 20,
|
||||||
): DigitalEmployeeReadyPlanStepDispatchCandidate[] {
|
): DigitalEmployeeReadyPlanStepDispatchCandidate[] {
|
||||||
|
const limit = typeof options === "number" ? options : options.limit ?? 20;
|
||||||
|
const planIdFilter = typeof options === "number" ? "" : stringValue(options.planId);
|
||||||
const records = readDigitalEmployeeRuntimeRecords();
|
const records = readDigitalEmployeeRuntimeRecords();
|
||||||
const tasksByStepId = new Map<string, DigitalEmployeeTaskRecord>();
|
const tasksByStepId = new Map<string, DigitalEmployeeTaskRecord>();
|
||||||
for (const task of records.tasks) {
|
for (const task of records.tasks) {
|
||||||
@@ -1408,8 +1410,9 @@ export function listDigitalEmployeeReadyPlanStepDispatchCandidates(
|
|||||||
const openApprovals = records.approvals.filter((approval) => isOpenApprovalStatus(approval.status));
|
const openApprovals = records.approvals.filter((approval) => isOpenApprovalStatus(approval.status));
|
||||||
|
|
||||||
return records.planSteps
|
return records.planSteps
|
||||||
|
.filter((step) => !planIdFilter || step.planId === planIdFilter)
|
||||||
.filter((step) => normalizePlanStepStatus(step.status) === "ready")
|
.filter((step) => normalizePlanStepStatus(step.status) === "ready")
|
||||||
.sort((left, right) => left.seq - right.seq || left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id))
|
.sort((left, right) => left.planId.localeCompare(right.planId) || left.seq - right.seq || left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id))
|
||||||
.slice(0, Math.max(1, limit))
|
.slice(0, Math.max(1, limit))
|
||||||
.map((step) => {
|
.map((step) => {
|
||||||
const task = tasksByStepId.get(step.id) ?? firstRunnableTaskForStep(step, tasksByPlanId.get(step.planId) ?? []) ?? null;
|
const task = tasksByStepId.get(step.id) ?? firstRunnableTaskForStep(step, tasksByPlanId.get(step.planId) ?? []) ?? null;
|
||||||
|
|||||||
@@ -564,6 +564,8 @@ describe("digital employee sync service", () => {
|
|||||||
run_id: "run-1",
|
run_id: "run-1",
|
||||||
status: "failed",
|
status: "failed",
|
||||||
reason: "computer chat failed",
|
reason: "computer chat failed",
|
||||||
|
trigger_source: "manual",
|
||||||
|
orchestration_id: "manual-ready-steps-plan-1",
|
||||||
});
|
});
|
||||||
insertEventEntity("event-plan-step-ready", "plan_step_ready", {
|
insertEventEntity("event-plan-step-ready", "plan_step_ready", {
|
||||||
stepId: "step-2",
|
stepId: "step-2",
|
||||||
@@ -795,6 +797,8 @@ describe("digital employee sync service", () => {
|
|||||||
run_id: "run-1",
|
run_id: "run-1",
|
||||||
status: "failed",
|
status: "failed",
|
||||||
reason: "computer chat failed",
|
reason: "computer chat failed",
|
||||||
|
trigger_source: "manual",
|
||||||
|
orchestration_id: "manual-ready-steps-plan-1",
|
||||||
});
|
});
|
||||||
expect(businessView("event-plan-step-ready")).toMatchObject({
|
expect(businessView("event-plan-step-ready")).toMatchObject({
|
||||||
category: "dispatch",
|
category: "dispatch",
|
||||||
|
|||||||
@@ -693,6 +693,8 @@ function planStepDispatchBusinessView(payload: Record<string, unknown>): Record<
|
|||||||
run_id: stringField(payload.run_id ?? payload.runId) || null,
|
run_id: stringField(payload.run_id ?? payload.runId) || null,
|
||||||
status: stringField(payload.status) || null,
|
status: stringField(payload.status) || null,
|
||||||
reason: stringField(payload.reason) || null,
|
reason: stringField(payload.reason) || null,
|
||||||
|
trigger_source: stringField(payload.trigger_source ?? payload.triggerSource) || null,
|
||||||
|
orchestration_id: stringField(payload.orchestration_id ?? payload.orchestrationId) || null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -140,6 +140,9 @@ contextBridge.exposeInMainWorld("QimingClawBridge", {
|
|||||||
async runScheduleNow(scheduleId: string) {
|
async runScheduleNow(scheduleId: string) {
|
||||||
return ipcRenderer.invoke("digitalEmployee:runScheduleNow", scheduleId);
|
return ipcRenderer.invoke("digitalEmployee:runScheduleNow", scheduleId);
|
||||||
},
|
},
|
||||||
|
async dispatchPlanReadySteps(planId: string) {
|
||||||
|
return ipcRenderer.invoke("digitalEmployee:dispatchPlanReadySteps", planId);
|
||||||
|
},
|
||||||
async getScheduleRuns(scheduleId: string, limit?: number) {
|
async getScheduleRuns(scheduleId: string, limit?: number) {
|
||||||
return ipcRenderer.invoke("digitalEmployee:getScheduleRuns", scheduleId, limit);
|
return ipcRenderer.invoke("digitalEmployee:getScheduleRuns", scheduleId, limit);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -540,9 +540,9 @@ GET /api/digital-employee/approvals
|
|||||||
|
|
||||||
1. **PlanStep 与依赖图(依赖图调度已开始)**
|
1. **PlanStep 与依赖图(依赖图调度已开始)**
|
||||||
- 已吸收:Plan / Task / Run 记录、计划模板、`create_plan` 中的 steps/tasks 可物化为任务;PlanStep 已开始作为正式本地实体落入 `digital_plan_steps`。
|
- 已吸收: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 的 retry / skip / cancel / pause / resume / input / run 控制已可更新真实 Task、PlanStep、Run、Event 和 outbox;前置步骤跳过、取消或重试后会重新评估后继步骤,推进为 ready / blocked / pending 并写入依赖图事件;scheduler sweep 会识别可派发的 ready PlanStep,调用本地 `/computer/chat` 并写入 `plan_step_dispatch_*` 与 `governance_start_run` 事件;embedded 已提供 `/api/digital-employee/plan-steps`、`/api/digital-employee/plan-step-graph` 和 `/api/plan-step/dispatch-policy`,ready PlanStep 安全派发、管理端依赖图视图、本地派发策略和首页阻塞依赖处理入口已开始闭环。
|
- 当前能力: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;前置步骤跳过、取消或重试后会重新评估后继步骤,推进为 ready / blocked / pending 并写入依赖图事件;scheduler sweep 会识别可派发的 ready PlanStep,调用本地 `/computer/chat` 并写入 `plan_step_dispatch_*` 与 `governance_start_run` 事件;embedded 已提供 `/api/digital-employee/plan-steps`、`/api/digital-employee/plan-step-graph`、`/api/digital-employee/plans/:planId/dispatch-ready-steps` 和 `/api/plan-step/dispatch-policy`,ready PlanStep 安全派发、计划级跨任务推进、管理端依赖图视图、本地派发策略和首页阻塞依赖处理入口已开始闭环。
|
||||||
- 后续缺口:跨任务编排执行、多设备 lease 和远端策略拉取仍待吸收。
|
- 后续缺口:多设备 lease 和远端策略拉取仍待吸收。
|
||||||
- 建议:下一轮围绕跨任务编排或多设备 lease,把本地可治理调度推进到更完整的执行控制面。
|
- 建议:下一轮围绕多设备 lease 或远端策略拉取,把本地可治理调度推进到跨端治理闭环。
|
||||||
|
|
||||||
2. **调度 / 定时 / 周期任务**
|
2. **调度 / 定时 / 周期任务**
|
||||||
- 已吸收:`digital_schedules`、`digital_schedule_runs` 已进入 qimingclaw 本地 SQLite 与 outbox,entity type 为 `schedule` / `schedule_run`;旧 `/api/cron`、`/api/scheduler/jobs` 和治理命令会优先读取真实本地调度记录。
|
- 已吸收:`digital_schedules`、`digital_schedule_runs` 已进入 qimingclaw 本地 SQLite 与 outbox,entity type 为 `schedule` / `schedule_run`;旧 `/api/cron`、`/api/scheduler/jobs` 和治理命令会优先读取真实本地调度记录。
|
||||||
|
|||||||
Reference in New Issue
Block a user