吸收数字员工PlanStep依赖图派发策略
This commit is contained in:
@@ -13,6 +13,7 @@ import type {
|
||||
ChannelDetail,
|
||||
BrowserSession,
|
||||
ApprovalSubscriptionPreferences,
|
||||
PlanStepDispatchPolicy,
|
||||
NotificationPreferences,
|
||||
UserNotification,
|
||||
DebugStoreResponse,
|
||||
@@ -544,7 +545,7 @@ export function getEventLog(params: {
|
||||
// Notifications
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export { type ApprovalSubscriptionPreferences, type NotificationPreferences, type UserNotification };
|
||||
export { type ApprovalSubscriptionPreferences, type NotificationPreferences, type PlanStepDispatchPolicy, type UserNotification };
|
||||
|
||||
export function getNotifications(
|
||||
status: string = 'unread',
|
||||
@@ -595,6 +596,37 @@ export function getApprovalHistory(params: Record<string, string | number | null
|
||||
return apiFetch(`/api/digital-employee/approval-history${suffix ? `?${suffix}` : ''}`);
|
||||
}
|
||||
|
||||
export function getPlanStepDispatchPolicy(): Promise<PlanStepDispatchPolicy> {
|
||||
return apiFetch<PlanStepDispatchPolicy>('/api/plan-step/dispatch-policy');
|
||||
}
|
||||
|
||||
export function patchPlanStepDispatchPolicy(
|
||||
patch: Partial<PlanStepDispatchPolicy>,
|
||||
): Promise<PlanStepDispatchPolicy> {
|
||||
return apiFetch<PlanStepDispatchPolicy>('/api/plan-step/dispatch-policy', {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(patch),
|
||||
});
|
||||
}
|
||||
|
||||
export function getPlanSteps(params: Record<string, string | number | null | undefined> = {}): Promise<any> {
|
||||
const query = new URLSearchParams();
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null && `${value}`.trim() !== '') query.set(key, `${value}`);
|
||||
});
|
||||
const suffix = query.toString();
|
||||
return apiFetch(`/api/digital-employee/plan-steps${suffix ? `?${suffix}` : ''}`);
|
||||
}
|
||||
|
||||
export function getPlanStepGraph(params: Record<string, string | number | null | undefined> = {}): Promise<any> {
|
||||
const query = new URLSearchParams();
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null && `${value}`.trim() !== '') query.set(key, `${value}`);
|
||||
});
|
||||
const suffix = query.toString();
|
||||
return apiFetch(`/api/digital-employee/plan-step-graph${suffix ? `?${suffix}` : ''}`);
|
||||
}
|
||||
|
||||
export function markNotificationRead(id: string): Promise<boolean> {
|
||||
return apiFetch<{ ok: boolean }>(`/api/notifications/${encodeURIComponent(id)}/read`, {
|
||||
method: 'POST',
|
||||
|
||||
@@ -34,6 +34,7 @@ import type {
|
||||
MemoryEntry,
|
||||
NotificationPreferences,
|
||||
PlanDispatchResponse,
|
||||
PlanStepDispatchPolicy,
|
||||
PlanStepSummary,
|
||||
RouteDecisionRecord,
|
||||
RouteDecisionTimelineResponse,
|
||||
@@ -89,6 +90,7 @@ interface AdapterState {
|
||||
notificationStateById?: Record<string, NotificationState>;
|
||||
notificationPreferences?: NotificationPreferences;
|
||||
approvalSubscriptionPreferences?: ApprovalSubscriptionPreferences;
|
||||
planStepDispatchPolicy?: PlanStepDispatchPolicy;
|
||||
consoleRole?: string;
|
||||
rolePreferences?: Record<string, unknown>;
|
||||
profile: {
|
||||
@@ -691,7 +693,7 @@ const PLAN_SCHEDULES: Record<string, { expression: string; nextRun: string; sche
|
||||
|
||||
const BUSINESS_VIEW_SOURCE = 'qimingclaw-runtime-projection';
|
||||
|
||||
type BusinessViewKind = 'plans' | 'tasks' | 'runs' | 'events' | 'artifacts' | 'approvals' | 'approval-history' | 'route-decisions' | 'notifications' | 'audits';
|
||||
type BusinessViewKind = 'plans' | 'plan-steps' | 'plan-step-graph' | 'tasks' | 'runs' | 'events' | 'artifacts' | 'approvals' | 'approval-history' | 'route-decisions' | 'notifications' | 'audits';
|
||||
type BusinessViewRow = Record<string, unknown>;
|
||||
|
||||
export function isQimingclawDigitalApiEnabled(): boolean {
|
||||
@@ -784,11 +786,28 @@ export async function handleQimingclawDigitalApi<T>(
|
||||
await readQimingclawSnapshot(),
|
||||
) as T;
|
||||
}
|
||||
if (method === 'GET' && pathname === '/api/plan-step/dispatch-policy') {
|
||||
return planStepDispatchPolicy(notificationAdapterState(await readQimingclawSnapshot())) as T;
|
||||
}
|
||||
if (method === 'PATCH' && pathname === '/api/plan-step/dispatch-policy') {
|
||||
return await handlePlanStepDispatchPolicyPatch(
|
||||
parseJsonBody<Partial<PlanStepDispatchPolicy>>(options.body),
|
||||
await readQimingclawSnapshot(),
|
||||
) as T;
|
||||
}
|
||||
if (method === 'GET' && pathname === '/api/digital-employee/plan-steps') {
|
||||
const rows = filterPlanStepRows(planStepRows(await readQimingclawSnapshot()), url.searchParams);
|
||||
return { ...paginateBusinessRows(rows, url.searchParams), source: BUSINESS_VIEW_SOURCE } as T;
|
||||
}
|
||||
if (method === 'GET' && pathname === '/api/digital-employee/plan-step-graph') {
|
||||
const rows = filterPlanStepRows(planStepGraphRows(await readQimingclawSnapshot()), url.searchParams);
|
||||
return { ...paginateBusinessRows(rows, url.searchParams), source: BUSINESS_VIEW_SOURCE } as T;
|
||||
}
|
||||
if (method === 'GET' && pathname === '/api/digital-employee/approval-history') {
|
||||
const rows = filterApprovalHistoryRows(approvalHistoryRows(await readQimingclawSnapshot()), url.searchParams);
|
||||
return { ...paginateBusinessRows(rows, url.searchParams), source: BUSINESS_VIEW_SOURCE } as T;
|
||||
}
|
||||
const businessViewMatch = pathname.match(/^\/api\/digital-employee\/(plans|tasks|runs|events|artifacts|approvals|approval-history|route-decisions|notifications|audits)$/);
|
||||
const businessViewMatch = pathname.match(/^\/api\/digital-employee\/(plans|plan-steps|plan-step-graph|tasks|runs|events|artifacts|approvals|approval-history|route-decisions|notifications|audits)$/);
|
||||
if (method === 'GET' && businessViewMatch) {
|
||||
return buildBusinessViewResponse(businessViewMatch[1] as BusinessViewKind, await readQimingclawSnapshot(), url.searchParams) as T;
|
||||
}
|
||||
@@ -964,6 +983,7 @@ function defaultState(): AdapterState {
|
||||
notificationStateById: {},
|
||||
notificationPreferences: defaultNotificationPreferences(),
|
||||
approvalSubscriptionPreferences: defaultApprovalSubscriptionPreferences(),
|
||||
planStepDispatchPolicy: defaultPlanStepDispatchPolicy(),
|
||||
consoleRole: 'sales',
|
||||
rolePreferences: {},
|
||||
profile: {
|
||||
@@ -990,6 +1010,7 @@ function normalizeAdapterState(value: Partial<AdapterState>): AdapterState {
|
||||
...value,
|
||||
notificationPreferences: notificationPreferences(value),
|
||||
approvalSubscriptionPreferences: approvalSubscriptionPreferences(value),
|
||||
planStepDispatchPolicy: planStepDispatchPolicy(value),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2520,6 +2541,8 @@ function auditLevelLabel(level: string): string {
|
||||
function buildBusinessViewResponse(kind: BusinessViewKind, snapshot: QimingclawSnapshot | null, searchParams: URLSearchParams): Record<string, unknown> {
|
||||
const rowsByKind: Record<BusinessViewKind, BusinessViewRow[]> = {
|
||||
plans: businessPlanRows(snapshot),
|
||||
'plan-steps': planStepRows(snapshot),
|
||||
'plan-step-graph': planStepGraphRows(snapshot),
|
||||
tasks: businessTaskRows(snapshot),
|
||||
runs: businessRunRows(snapshot),
|
||||
events: businessEventRows(snapshot),
|
||||
@@ -2532,19 +2555,140 @@ function buildBusinessViewResponse(kind: BusinessViewKind, snapshot: QimingclawS
|
||||
};
|
||||
const rows = kind === 'route-decisions'
|
||||
? filterRouteDecisionRows(rowsByKind[kind], searchParams)
|
||||
: kind === 'approval-history'
|
||||
? filterApprovalHistoryRows(rowsByKind[kind], searchParams)
|
||||
: kind === 'notifications'
|
||||
? filterNotificationRows(rowsByKind[kind], searchParams)
|
||||
: kind === 'audits'
|
||||
? filterAuditRows(rowsByKind[kind], searchParams)
|
||||
: filterBusinessRows(rowsByKind[kind], searchParams);
|
||||
: kind === 'plan-steps' || kind === 'plan-step-graph'
|
||||
? filterPlanStepRows(rowsByKind[kind], searchParams)
|
||||
: kind === 'approval-history'
|
||||
? filterApprovalHistoryRows(rowsByKind[kind], searchParams)
|
||||
: kind === 'notifications'
|
||||
? filterNotificationRows(rowsByKind[kind], searchParams)
|
||||
: kind === 'audits'
|
||||
? filterAuditRows(rowsByKind[kind], searchParams)
|
||||
: filterBusinessRows(rowsByKind[kind], searchParams);
|
||||
return {
|
||||
...paginateBusinessRows(rows, searchParams),
|
||||
source: BUSINESS_VIEW_SOURCE,
|
||||
};
|
||||
}
|
||||
|
||||
function planStepRows(snapshot: QimingclawSnapshot | null): BusinessViewRow[] {
|
||||
return runtimePlanSteps(snapshot)
|
||||
.map((step) => {
|
||||
const payload = asRecord(step.payload) ?? {};
|
||||
const dependencyGraph = asRecord(payload.dependencyGraph ?? payload.dependency_graph) ?? {};
|
||||
const lastDispatch = asRecord(payload.lastDispatch ?? payload.last_dispatch) ?? {};
|
||||
const readiness = planStepDispatchReadiness(step, snapshot, payload);
|
||||
const blockedDependencies = uniqueStrings(arrayValue(dependencyGraph.blockedDependencies ?? dependencyGraph.blocked_dependencies));
|
||||
const waitingDependencies = uniqueStrings(arrayValue(dependencyGraph.waitingDependencies ?? dependencyGraph.waiting_dependencies));
|
||||
const satisfiedDependencies = uniqueStrings(arrayValue(dependencyGraph.satisfiedDependencies ?? dependencyGraph.satisfied_dependencies));
|
||||
return {
|
||||
step_id: step.id,
|
||||
node_id: step.id,
|
||||
remote_id: step.remoteId ?? null,
|
||||
plan_id: step.planId,
|
||||
title: step.title,
|
||||
status: step.status,
|
||||
seq: step.seq,
|
||||
depends_on: step.dependsOn,
|
||||
dependency_count: step.dependsOn.length,
|
||||
dependency_status: stringValue(dependencyGraph.status) || stepDependencyStatus(step, blockedDependencies, waitingDependencies),
|
||||
blocked_dependencies: blockedDependencies,
|
||||
blocked_dependency_count: blockedDependencies.length,
|
||||
waiting_dependencies: waitingDependencies,
|
||||
waiting_dependency_count: waitingDependencies.length,
|
||||
satisfied_dependencies: satisfiedDependencies,
|
||||
preferred_skill_ids: step.preferredSkillIds,
|
||||
task_id: readiness.taskId,
|
||||
dispatchable: readiness.dispatchable,
|
||||
skip_reason: readiness.skipReason,
|
||||
last_dispatch_status: stringValue(lastDispatch.status) || null,
|
||||
last_dispatch_at: stringValue(lastDispatch.occurredAt ?? lastDispatch.occurred_at) || null,
|
||||
sync_status: step.syncStatus,
|
||||
last_synced_at: step.lastSyncedAt ?? null,
|
||||
sync_error: step.syncError ?? null,
|
||||
created_at: step.createdAt,
|
||||
updated_at: step.updatedAt,
|
||||
entity_type: 'plan_step',
|
||||
entity_id: step.id,
|
||||
source: BUSINESS_VIEW_SOURCE,
|
||||
};
|
||||
})
|
||||
.sort((left, right) => stringValue(left.plan_id).localeCompare(stringValue(right.plan_id)) || Number(left.seq ?? 0) - Number(right.seq ?? 0) || stringValue(left.step_id).localeCompare(stringValue(right.step_id)));
|
||||
}
|
||||
|
||||
function planStepGraphRows(snapshot: QimingclawSnapshot | null): BusinessViewRow[] {
|
||||
return planStepRows(snapshot).map((row) => ({
|
||||
node_id: stringValue(row.node_id) || stringValue(row.step_id),
|
||||
step_id: stringValue(row.step_id),
|
||||
plan_id: stringValue(row.plan_id) || null,
|
||||
title: stringValue(row.title),
|
||||
status: stringValue(row.status),
|
||||
seq: row.seq,
|
||||
depends_on: arrayValue(row.depends_on),
|
||||
dependency_status: stringValue(row.dependency_status) || null,
|
||||
blocked_dependencies: arrayValue(row.blocked_dependencies),
|
||||
waiting_dependencies: arrayValue(row.waiting_dependencies),
|
||||
satisfied_dependencies: arrayValue(row.satisfied_dependencies),
|
||||
dispatchable: row.dispatchable === true,
|
||||
skip_reason: stringValue(row.skip_reason) || null,
|
||||
sync_status: stringValue(row.sync_status) || null,
|
||||
updated_at: stringValue(row.updated_at) || null,
|
||||
entity_type: 'plan_step_graph_node',
|
||||
entity_id: stringValue(row.step_id),
|
||||
source: BUSINESS_VIEW_SOURCE,
|
||||
}));
|
||||
}
|
||||
|
||||
function filterPlanStepRows(rows: BusinessViewRow[], searchParams: URLSearchParams): BusinessViewRow[] {
|
||||
const planId = stringValue(searchParams.get('plan_id'));
|
||||
const stepId = stringValue(searchParams.get('step_id')) || stringValue(searchParams.get('id'));
|
||||
const status = stringValue(searchParams.get('status'));
|
||||
const syncStatus = stringValue(searchParams.get('sync_status'));
|
||||
return filterBusinessRows(rows, searchParams)
|
||||
.filter((row) => !planId || stringValue(row.plan_id) === planId)
|
||||
.filter((row) => !stepId || stringValue(row.step_id) === stepId || stringValue(row.node_id) === stepId || stringValue(row.entity_id) === stepId)
|
||||
.filter((row) => !status || stringValue(row.status) === status)
|
||||
.filter((row) => !syncStatus || stringValue(row.sync_status) === syncStatus);
|
||||
}
|
||||
|
||||
function stepDependencyStatus(step: QimingclawPlanStepRecord, blockedDependencies: string[], waitingDependencies: string[]): string {
|
||||
if (blockedDependencies.length > 0) return 'blocked';
|
||||
if (waitingDependencies.length > 0) return 'waiting';
|
||||
if (step.dependsOn.length > 0) return 'ready';
|
||||
return 'independent';
|
||||
}
|
||||
|
||||
function planStepDispatchReadiness(
|
||||
step: QimingclawPlanStepRecord,
|
||||
snapshot: QimingclawSnapshot | null,
|
||||
payload: Record<string, unknown>,
|
||||
): { taskId: string | null; dispatchable: boolean; skipReason: string | null } {
|
||||
const tasks = runtimePlanTasks(snapshot, step.planId);
|
||||
const task = tasks.find((candidate) => taskStepId(candidate) === step.id)
|
||||
?? tasks.find((candidate) => !isTerminalRuntimeTaskStatus(candidate.status))
|
||||
?? null;
|
||||
const taskId = task?.id ?? null;
|
||||
let skipReason: string | null = null;
|
||||
if (step.status.toLowerCase() !== 'ready') skipReason = 'not_ready';
|
||||
else if (!task) skipReason = 'missing_task';
|
||||
else if (isTerminalRuntimeTaskStatus(task.status)) skipReason = 'task_terminal';
|
||||
else if (runtimeRuns(snapshot).some((run) => run.taskId === task.id && isActiveRuntimeRunStatus(run.status))) skipReason = 'task_already_running';
|
||||
else if (runtimeApprovals(snapshot).some((approval) => isOpenRuntimeApprovalStatus(approval.status) && (approval.planId === step.planId || approval.taskId === task.id || stringValue(asRecord(approval.payload)?.stepId ?? asRecord(approval.payload)?.step_id) === step.id))) skipReason = 'approval_pending';
|
||||
else if (payload.auto_dispatch === false || payload.autoDispatch === false) skipReason = 'auto_dispatch_disabled';
|
||||
return { taskId, dispatchable: !skipReason, skipReason };
|
||||
}
|
||||
|
||||
function isTerminalRuntimeTaskStatus(status: string): boolean {
|
||||
return ['completed', 'succeeded', 'success', 'done', 'skipped', 'cancelled', 'canceled'].includes(status.toLowerCase());
|
||||
}
|
||||
|
||||
function isActiveRuntimeRunStatus(status: string): boolean {
|
||||
return ['running', 'pending', 'queued', 'started'].includes(status.toLowerCase());
|
||||
}
|
||||
|
||||
function isOpenRuntimeApprovalStatus(status: string): boolean {
|
||||
return ['pending', 'reviewing', 'open', 'waiting'].includes(status.toLowerCase() || 'pending');
|
||||
}
|
||||
|
||||
function routeDecisionRows(snapshot: QimingclawSnapshot | null): BusinessViewRow[] {
|
||||
return runtimeEvents(snapshot)
|
||||
.filter((event) => event.kind === 'route_decision')
|
||||
@@ -3325,6 +3469,8 @@ function businessRowEntityIds(row: BusinessViewRow): string[] {
|
||||
row.run_id,
|
||||
row.event_id,
|
||||
row.history_id,
|
||||
row.step_id,
|
||||
row.node_id,
|
||||
row.artifact_id,
|
||||
row.approval_id,
|
||||
row.notification_id,
|
||||
@@ -3652,6 +3798,15 @@ function defaultApprovalSubscriptionPreferences(): ApprovalSubscriptionPreferenc
|
||||
};
|
||||
}
|
||||
|
||||
function defaultPlanStepDispatchPolicy(): PlanStepDispatchPolicy {
|
||||
return {
|
||||
enabled: true,
|
||||
max_per_sweep: 3,
|
||||
auto_dispatch_ready_steps: true,
|
||||
updated_at: null,
|
||||
};
|
||||
}
|
||||
|
||||
function approvalSubscriptionPreferences(state: Partial<AdapterState> | null | undefined): ApprovalSubscriptionPreferences {
|
||||
const stored = state?.approvalSubscriptionPreferences;
|
||||
const fallback = defaultApprovalSubscriptionPreferences();
|
||||
@@ -3671,6 +3826,19 @@ function approvalSubscriptionPreferences(state: Partial<AdapterState> | null | u
|
||||
};
|
||||
}
|
||||
|
||||
function planStepDispatchPolicy(state: Partial<AdapterState> | null | undefined): PlanStepDispatchPolicy {
|
||||
const stored = state?.planStepDispatchPolicy;
|
||||
const fallback = defaultPlanStepDispatchPolicy();
|
||||
if (!stored || typeof stored !== 'object') return fallback;
|
||||
const maxPerSweep = Number(stored.max_per_sweep);
|
||||
return {
|
||||
enabled: stored.enabled !== false,
|
||||
max_per_sweep: Number.isFinite(maxPerSweep) ? Math.max(1, Math.min(Math.floor(maxPerSweep), 10)) : fallback.max_per_sweep,
|
||||
auto_dispatch_ready_steps: stored.auto_dispatch_ready_steps !== false,
|
||||
updated_at: typeof stored.updated_at === 'string' ? stored.updated_at : fallback.updated_at,
|
||||
};
|
||||
}
|
||||
|
||||
function notificationPreferences(state: Partial<AdapterState> | null | undefined): NotificationPreferences {
|
||||
const stored = state?.notificationPreferences;
|
||||
const fallback = defaultNotificationPreferences();
|
||||
@@ -3749,6 +3917,31 @@ async function handleApprovalSubscriptionPreferencesPatch(
|
||||
return next;
|
||||
}
|
||||
|
||||
async function handlePlanStepDispatchPolicyPatch(
|
||||
patch: Partial<PlanStepDispatchPolicy>,
|
||||
snapshot: QimingclawSnapshot | null,
|
||||
): Promise<PlanStepDispatchPolicy> {
|
||||
const state = snapshot?.uiState ?? await readBridgeUiState() ?? readState();
|
||||
const current = planStepDispatchPolicy(state);
|
||||
const next = planStepDispatchPolicy({
|
||||
planStepDispatchPolicy: {
|
||||
...current,
|
||||
...patch,
|
||||
updated_at: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
await saveState(
|
||||
{
|
||||
...normalizeAdapterState(state),
|
||||
planStepDispatchPolicy: next,
|
||||
},
|
||||
'update_plan_step_dispatch_policy',
|
||||
undefined,
|
||||
{ plan_step_dispatch_policy: next },
|
||||
);
|
||||
return next;
|
||||
}
|
||||
|
||||
function approvalNotifications(snapshot: QimingclawSnapshot | null): UserNotification[] {
|
||||
const summaries = new Map(businessApprovalRows(snapshot).map((approval) => [stringValue(approval.approval_id), approval]));
|
||||
return buildApprovals(snapshot)
|
||||
|
||||
Reference in New Issue
Block a user