吸收数字员工PlanStep依赖图派发策略
This commit is contained in:
@@ -13,6 +13,7 @@ import type {
|
|||||||
ChannelDetail,
|
ChannelDetail,
|
||||||
BrowserSession,
|
BrowserSession,
|
||||||
ApprovalSubscriptionPreferences,
|
ApprovalSubscriptionPreferences,
|
||||||
|
PlanStepDispatchPolicy,
|
||||||
NotificationPreferences,
|
NotificationPreferences,
|
||||||
UserNotification,
|
UserNotification,
|
||||||
DebugStoreResponse,
|
DebugStoreResponse,
|
||||||
@@ -544,7 +545,7 @@ export function getEventLog(params: {
|
|||||||
// Notifications
|
// Notifications
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export { type ApprovalSubscriptionPreferences, type NotificationPreferences, type UserNotification };
|
export { type ApprovalSubscriptionPreferences, type NotificationPreferences, type PlanStepDispatchPolicy, type UserNotification };
|
||||||
|
|
||||||
export function getNotifications(
|
export function getNotifications(
|
||||||
status: string = 'unread',
|
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}` : ''}`);
|
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> {
|
export function markNotificationRead(id: string): Promise<boolean> {
|
||||||
return apiFetch<{ ok: boolean }>(`/api/notifications/${encodeURIComponent(id)}/read`, {
|
return apiFetch<{ ok: boolean }>(`/api/notifications/${encodeURIComponent(id)}/read`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import type {
|
|||||||
MemoryEntry,
|
MemoryEntry,
|
||||||
NotificationPreferences,
|
NotificationPreferences,
|
||||||
PlanDispatchResponse,
|
PlanDispatchResponse,
|
||||||
|
PlanStepDispatchPolicy,
|
||||||
PlanStepSummary,
|
PlanStepSummary,
|
||||||
RouteDecisionRecord,
|
RouteDecisionRecord,
|
||||||
RouteDecisionTimelineResponse,
|
RouteDecisionTimelineResponse,
|
||||||
@@ -89,6 +90,7 @@ interface AdapterState {
|
|||||||
notificationStateById?: Record<string, NotificationState>;
|
notificationStateById?: Record<string, NotificationState>;
|
||||||
notificationPreferences?: NotificationPreferences;
|
notificationPreferences?: NotificationPreferences;
|
||||||
approvalSubscriptionPreferences?: ApprovalSubscriptionPreferences;
|
approvalSubscriptionPreferences?: ApprovalSubscriptionPreferences;
|
||||||
|
planStepDispatchPolicy?: PlanStepDispatchPolicy;
|
||||||
consoleRole?: string;
|
consoleRole?: string;
|
||||||
rolePreferences?: Record<string, unknown>;
|
rolePreferences?: Record<string, unknown>;
|
||||||
profile: {
|
profile: {
|
||||||
@@ -691,7 +693,7 @@ const PLAN_SCHEDULES: Record<string, { expression: string; nextRun: string; sche
|
|||||||
|
|
||||||
const BUSINESS_VIEW_SOURCE = 'qimingclaw-runtime-projection';
|
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>;
|
type BusinessViewRow = Record<string, unknown>;
|
||||||
|
|
||||||
export function isQimingclawDigitalApiEnabled(): boolean {
|
export function isQimingclawDigitalApiEnabled(): boolean {
|
||||||
@@ -784,11 +786,28 @@ export async function handleQimingclawDigitalApi<T>(
|
|||||||
await readQimingclawSnapshot(),
|
await readQimingclawSnapshot(),
|
||||||
) as T;
|
) 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') {
|
if (method === 'GET' && pathname === '/api/digital-employee/approval-history') {
|
||||||
const rows = filterApprovalHistoryRows(approvalHistoryRows(await readQimingclawSnapshot()), url.searchParams);
|
const rows = filterApprovalHistoryRows(approvalHistoryRows(await readQimingclawSnapshot()), url.searchParams);
|
||||||
return { ...paginateBusinessRows(rows, url.searchParams), source: BUSINESS_VIEW_SOURCE } as T;
|
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) {
|
if (method === 'GET' && businessViewMatch) {
|
||||||
return buildBusinessViewResponse(businessViewMatch[1] as BusinessViewKind, await readQimingclawSnapshot(), url.searchParams) as T;
|
return buildBusinessViewResponse(businessViewMatch[1] as BusinessViewKind, await readQimingclawSnapshot(), url.searchParams) as T;
|
||||||
}
|
}
|
||||||
@@ -964,6 +983,7 @@ function defaultState(): AdapterState {
|
|||||||
notificationStateById: {},
|
notificationStateById: {},
|
||||||
notificationPreferences: defaultNotificationPreferences(),
|
notificationPreferences: defaultNotificationPreferences(),
|
||||||
approvalSubscriptionPreferences: defaultApprovalSubscriptionPreferences(),
|
approvalSubscriptionPreferences: defaultApprovalSubscriptionPreferences(),
|
||||||
|
planStepDispatchPolicy: defaultPlanStepDispatchPolicy(),
|
||||||
consoleRole: 'sales',
|
consoleRole: 'sales',
|
||||||
rolePreferences: {},
|
rolePreferences: {},
|
||||||
profile: {
|
profile: {
|
||||||
@@ -990,6 +1010,7 @@ function normalizeAdapterState(value: Partial<AdapterState>): AdapterState {
|
|||||||
...value,
|
...value,
|
||||||
notificationPreferences: notificationPreferences(value),
|
notificationPreferences: notificationPreferences(value),
|
||||||
approvalSubscriptionPreferences: approvalSubscriptionPreferences(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> {
|
function buildBusinessViewResponse(kind: BusinessViewKind, snapshot: QimingclawSnapshot | null, searchParams: URLSearchParams): Record<string, unknown> {
|
||||||
const rowsByKind: Record<BusinessViewKind, BusinessViewRow[]> = {
|
const rowsByKind: Record<BusinessViewKind, BusinessViewRow[]> = {
|
||||||
plans: businessPlanRows(snapshot),
|
plans: businessPlanRows(snapshot),
|
||||||
|
'plan-steps': planStepRows(snapshot),
|
||||||
|
'plan-step-graph': planStepGraphRows(snapshot),
|
||||||
tasks: businessTaskRows(snapshot),
|
tasks: businessTaskRows(snapshot),
|
||||||
runs: businessRunRows(snapshot),
|
runs: businessRunRows(snapshot),
|
||||||
events: businessEventRows(snapshot),
|
events: businessEventRows(snapshot),
|
||||||
@@ -2532,19 +2555,140 @@ function buildBusinessViewResponse(kind: BusinessViewKind, snapshot: QimingclawS
|
|||||||
};
|
};
|
||||||
const rows = kind === 'route-decisions'
|
const rows = kind === 'route-decisions'
|
||||||
? filterRouteDecisionRows(rowsByKind[kind], searchParams)
|
? filterRouteDecisionRows(rowsByKind[kind], searchParams)
|
||||||
: kind === 'approval-history'
|
: kind === 'plan-steps' || kind === 'plan-step-graph'
|
||||||
? filterApprovalHistoryRows(rowsByKind[kind], searchParams)
|
? filterPlanStepRows(rowsByKind[kind], searchParams)
|
||||||
: kind === 'notifications'
|
: kind === 'approval-history'
|
||||||
? filterNotificationRows(rowsByKind[kind], searchParams)
|
? filterApprovalHistoryRows(rowsByKind[kind], searchParams)
|
||||||
: kind === 'audits'
|
: kind === 'notifications'
|
||||||
? filterAuditRows(rowsByKind[kind], searchParams)
|
? filterNotificationRows(rowsByKind[kind], searchParams)
|
||||||
: filterBusinessRows(rowsByKind[kind], searchParams);
|
: kind === 'audits'
|
||||||
|
? filterAuditRows(rowsByKind[kind], searchParams)
|
||||||
|
: filterBusinessRows(rowsByKind[kind], searchParams);
|
||||||
return {
|
return {
|
||||||
...paginateBusinessRows(rows, searchParams),
|
...paginateBusinessRows(rows, searchParams),
|
||||||
source: BUSINESS_VIEW_SOURCE,
|
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[] {
|
function routeDecisionRows(snapshot: QimingclawSnapshot | null): BusinessViewRow[] {
|
||||||
return runtimeEvents(snapshot)
|
return runtimeEvents(snapshot)
|
||||||
.filter((event) => event.kind === 'route_decision')
|
.filter((event) => event.kind === 'route_decision')
|
||||||
@@ -3325,6 +3469,8 @@ function businessRowEntityIds(row: BusinessViewRow): string[] {
|
|||||||
row.run_id,
|
row.run_id,
|
||||||
row.event_id,
|
row.event_id,
|
||||||
row.history_id,
|
row.history_id,
|
||||||
|
row.step_id,
|
||||||
|
row.node_id,
|
||||||
row.artifact_id,
|
row.artifact_id,
|
||||||
row.approval_id,
|
row.approval_id,
|
||||||
row.notification_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 {
|
function approvalSubscriptionPreferences(state: Partial<AdapterState> | null | undefined): ApprovalSubscriptionPreferences {
|
||||||
const stored = state?.approvalSubscriptionPreferences;
|
const stored = state?.approvalSubscriptionPreferences;
|
||||||
const fallback = defaultApprovalSubscriptionPreferences();
|
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 {
|
function notificationPreferences(state: Partial<AdapterState> | null | undefined): NotificationPreferences {
|
||||||
const stored = state?.notificationPreferences;
|
const stored = state?.notificationPreferences;
|
||||||
const fallback = defaultNotificationPreferences();
|
const fallback = defaultNotificationPreferences();
|
||||||
@@ -3749,6 +3917,31 @@ async function handleApprovalSubscriptionPreferencesPatch(
|
|||||||
return next;
|
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[] {
|
function approvalNotifications(snapshot: QimingclawSnapshot | null): UserNotification[] {
|
||||||
const summaries = new Map(businessApprovalRows(snapshot).map((approval) => [stringValue(approval.approval_id), approval]));
|
const summaries = new Map(businessApprovalRows(snapshot).map((approval) => [stringValue(approval.approval_id), approval]));
|
||||||
return buildApprovals(snapshot)
|
return buildApprovals(snapshot)
|
||||||
|
|||||||
@@ -195,6 +195,13 @@ export interface ApprovalSubscriptionPreferences {
|
|||||||
updated_at: string | null;
|
updated_at: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PlanStepDispatchPolicy {
|
||||||
|
enabled: boolean;
|
||||||
|
max_per_sweep: number;
|
||||||
|
auto_dispatch_ready_steps: boolean;
|
||||||
|
updated_at: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export type ChatRouteMode = 'auto' | 'chat' | 'direct' | 'plan';
|
export type ChatRouteMode = 'auto' | 'chat' | 'direct' | 'plan';
|
||||||
|
|
||||||
export interface WsMessage {
|
export interface WsMessage {
|
||||||
|
|||||||
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-PvwLx0FH.js"></script>
|
<script type="module" crossorigin src="./assets/index-Dar5kQHh.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="./assets/index-DqdYYbRq.css">
|
<link rel="stylesheet" crossorigin href="./assets/index-DqdYYbRq.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -283,6 +283,50 @@ function checkDigitalApprovalHistorySubscriptions() {
|
|||||||
console.log("[DigitalEmployeeCheck] approval history/subscription hooks OK");
|
console.log("[DigitalEmployeeCheck] approval history/subscription hooks OK");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function checkDigitalPlanStepGraphDispatchPolicy() {
|
||||||
|
console.log("\n[DigitalEmployeeCheck] Verify PlanStep graph/dispatch policy hooks");
|
||||||
|
const adapterPath = path.join(packageRoot, "embedded", "sgrobot-digital", "src", "lib", "qimingclawAdapter.ts");
|
||||||
|
const apiPath = path.join(packageRoot, "embedded", "sgrobot-digital", "src", "lib", "api.ts");
|
||||||
|
const typesPath = path.join(packageRoot, "embedded", "sgrobot-digital", "src", "types", "api.ts");
|
||||||
|
const stateServicePath = path.join(packageRoot, "src", "main", "services", "digitalEmployee", "stateService.ts");
|
||||||
|
const schedulerServicePath = path.join(packageRoot, "src", "main", "services", "digitalEmployee", "schedulerService.ts");
|
||||||
|
const syncServicePath = path.join(packageRoot, "src", "main", "services", "digitalEmployee", "syncService.ts");
|
||||||
|
const adapter = fs.readFileSync(adapterPath, "utf8");
|
||||||
|
const api = fs.readFileSync(apiPath, "utf8");
|
||||||
|
const types = fs.readFileSync(typesPath, "utf8");
|
||||||
|
const stateService = fs.readFileSync(stateServicePath, "utf8");
|
||||||
|
const schedulerService = fs.readFileSync(schedulerServicePath, "utf8");
|
||||||
|
const syncService = fs.readFileSync(syncServicePath, "utf8");
|
||||||
|
const requiredSnippets = [
|
||||||
|
[adapter, "/api/digital-employee/plan-steps", "adapter plan step endpoint"],
|
||||||
|
[adapter, "/api/digital-employee/plan-step-graph", "adapter plan step graph endpoint"],
|
||||||
|
[adapter, "/api/plan-step/dispatch-policy", "adapter plan step dispatch policy endpoint"],
|
||||||
|
[adapter, "planStepRows", "management plan step rows"],
|
||||||
|
[adapter, "planStepGraphRows", "management plan step graph rows"],
|
||||||
|
[adapter, "filterPlanStepRows", "management plan step filters"],
|
||||||
|
[adapter, "update_plan_step_dispatch_policy", "adapter plan step dispatch policy save action"],
|
||||||
|
[api, "getPlanStepDispatchPolicy", "plan step dispatch policy API getter"],
|
||||||
|
[api, "patchPlanStepDispatchPolicy", "plan step dispatch policy API patcher"],
|
||||||
|
[api, "getPlanSteps", "plan steps API getter"],
|
||||||
|
[api, "getPlanStepGraph", "plan step graph API getter"],
|
||||||
|
[types, "PlanStepDispatchPolicy", "plan step dispatch policy type"],
|
||||||
|
[stateService, "planStepDispatchPolicy", "plan step dispatch policy UI state"],
|
||||||
|
[stateService, "update_plan_step_dispatch_policy", "plan step dispatch policy runtime event"],
|
||||||
|
[schedulerService, "readDigitalEmployeePlanStepDispatchPolicy", "scheduler plan step dispatch policy reader"],
|
||||||
|
[schedulerService, "max_per_sweep", "scheduler max per sweep policy"],
|
||||||
|
[syncService, "planStepBusinessView", "plan step sync business view"],
|
||||||
|
[syncService, "planStepDispatchPolicyBusinessView", "plan step dispatch policy sync view"],
|
||||||
|
[syncService, "digital_workday_update_plan_step_dispatch_policy", "plan step dispatch policy sync event kind"],
|
||||||
|
];
|
||||||
|
const missing = requiredSnippets
|
||||||
|
.filter(([content, snippet]) => !content.includes(snippet))
|
||||||
|
.map(([, , label]) => label);
|
||||||
|
if (missing.length > 0) {
|
||||||
|
throw new Error(`Missing PlanStep graph/dispatch policy hooks: ${missing.join(", ")}`);
|
||||||
|
}
|
||||||
|
console.log("[DigitalEmployeeCheck] PlanStep graph/dispatch policy hooks OK");
|
||||||
|
}
|
||||||
|
|
||||||
function checkLocalDatabaseSchema() {
|
function checkLocalDatabaseSchema() {
|
||||||
console.log("\n[DigitalEmployeeCheck] Verify local SQLite digital schema");
|
console.log("\n[DigitalEmployeeCheck] Verify local SQLite digital schema");
|
||||||
const dbPath = path.join(os.homedir(), ".qimingclaw", "qimingclaw.db");
|
const dbPath = path.join(os.homedir(), ".qimingclaw", "qimingclaw.db");
|
||||||
@@ -328,6 +372,7 @@ function main() {
|
|||||||
checkDigitalDiagnosticAuditProjection();
|
checkDigitalDiagnosticAuditProjection();
|
||||||
checkDigitalNotificationPreferences();
|
checkDigitalNotificationPreferences();
|
||||||
checkDigitalApprovalHistorySubscriptions();
|
checkDigitalApprovalHistorySubscriptions();
|
||||||
|
checkDigitalPlanStepGraphDispatchPolicy();
|
||||||
checkLocalDatabaseSchema();
|
checkLocalDatabaseSchema();
|
||||||
console.log("\n[DigitalEmployeeCheck] All checks passed");
|
console.log("\n[DigitalEmployeeCheck] All checks passed");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -14,6 +14,12 @@ const mockState = vi.hoisted(() => ({
|
|||||||
max_catch_up_runs: 5,
|
max_catch_up_runs: 5,
|
||||||
max_concurrent_schedule_runs: 1,
|
max_concurrent_schedule_runs: 1,
|
||||||
},
|
},
|
||||||
|
dispatchPolicy: {
|
||||||
|
enabled: true,
|
||||||
|
max_per_sweep: 3,
|
||||||
|
auto_dispatch_ready_steps: true,
|
||||||
|
updated_at: null as string | null,
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../startupPorts", () => ({
|
vi.mock("../startupPorts", () => ({
|
||||||
@@ -26,6 +32,7 @@ vi.mock("../constants", () => ({
|
|||||||
|
|
||||||
vi.mock("./stateService", () => ({
|
vi.mock("./stateService", () => ({
|
||||||
readDigitalEmployeeCronSettings: () => mockState.settings,
|
readDigitalEmployeeCronSettings: () => mockState.settings,
|
||||||
|
readDigitalEmployeePlanStepDispatchPolicy: () => mockState.dispatchPolicy,
|
||||||
listDigitalEmployeeReadyPlanStepDispatchCandidates: () => mockState.readyCandidates,
|
listDigitalEmployeeReadyPlanStepDispatchCandidates: () => mockState.readyCandidates,
|
||||||
listDigitalEmployeeSchedules: () => mockState.schedules,
|
listDigitalEmployeeSchedules: () => mockState.schedules,
|
||||||
listDueDigitalEmployeeSchedules: () => mockState.schedules,
|
listDueDigitalEmployeeSchedules: () => mockState.schedules,
|
||||||
@@ -62,6 +69,12 @@ describe("digital employee scheduler service", () => {
|
|||||||
mockState.finishedRuns = [];
|
mockState.finishedRuns = [];
|
||||||
mockState.dispatchEvents = [];
|
mockState.dispatchEvents = [];
|
||||||
mockState.governanceCommands = [];
|
mockState.governanceCommands = [];
|
||||||
|
mockState.dispatchPolicy = {
|
||||||
|
enabled: true,
|
||||||
|
max_per_sweep: 3,
|
||||||
|
auto_dispatch_ready_steps: true,
|
||||||
|
updated_at: null,
|
||||||
|
};
|
||||||
mockState.schedules = [{
|
mockState.schedules = [{
|
||||||
id: "schedule-1",
|
id: "schedule-1",
|
||||||
planId: "plan-1",
|
planId: "plan-1",
|
||||||
@@ -165,6 +178,45 @@ describe("digital employee scheduler service", () => {
|
|||||||
expect(mockState.dispatchEvents.map((event) => event.status)).toEqual(["started", "failed"]);
|
expect(mockState.dispatchEvents.map((event) => event.status)).toEqual(["started", "failed"]);
|
||||||
expect(mockState.governanceCommands).toHaveLength(0);
|
expect(mockState.governanceCommands).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("honors disabled ready step dispatch policy", async () => {
|
||||||
|
mockState.readyCandidates = [readyStepCandidate()];
|
||||||
|
mockState.dispatchPolicy = { ...mockState.dispatchPolicy, enabled: false };
|
||||||
|
const { dispatchReadyPlanSteps } = await import("./schedulerService");
|
||||||
|
|
||||||
|
const result = await dispatchReadyPlanSteps(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("honors ready step 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(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("limits ready step dispatches by policy max per sweep", async () => {
|
||||||
|
mockState.readyCandidates = [
|
||||||
|
readyStepCandidate(),
|
||||||
|
{ ...readyStepCandidate(), stepId: "step-ready-2", taskId: "task-2" },
|
||||||
|
];
|
||||||
|
mockState.dispatchPolicy = { ...mockState.dispatchPolicy, max_per_sweep: 1 };
|
||||||
|
const { dispatchReadyPlanSteps } = await import("./schedulerService");
|
||||||
|
|
||||||
|
const result = await dispatchReadyPlanSteps(new Date("2026-06-07T01:00:10.000Z"));
|
||||||
|
|
||||||
|
expect(result).toMatchObject({ candidates: 2, dispatched: 1, skipped: 1, failed: 0 });
|
||||||
|
expect(fetch).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockState.dispatchEvents.map((event) => event.stepId)).toEqual(["step-ready", "step-ready"]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function readyStepCandidate(): Record<string, unknown> {
|
function readyStepCandidate(): Record<string, unknown> {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
listDigitalEmployeeSchedules,
|
listDigitalEmployeeSchedules,
|
||||||
listDueDigitalEmployeeSchedules,
|
listDueDigitalEmployeeSchedules,
|
||||||
readDigitalEmployeeCronSettings,
|
readDigitalEmployeeCronSettings,
|
||||||
|
readDigitalEmployeePlanStepDispatchPolicy,
|
||||||
readDigitalEmployeeScheduleRuns,
|
readDigitalEmployeeScheduleRuns,
|
||||||
recordDigitalEmployeeGovernanceCommand,
|
recordDigitalEmployeeGovernanceCommand,
|
||||||
recordDigitalEmployeePlanStepDispatch,
|
recordDigitalEmployeePlanStepDispatch,
|
||||||
@@ -19,7 +20,6 @@ import {
|
|||||||
const SWEEP_INTERVAL_MS = 30_000;
|
const SWEEP_INTERVAL_MS = 30_000;
|
||||||
const MAX_SCAN_MINUTES = 366 * 24 * 60;
|
const MAX_SCAN_MINUTES = 366 * 24 * 60;
|
||||||
const RETRY_DELAYS_MS = [60_000, 5 * 60_000, 15 * 60_000];
|
const RETRY_DELAYS_MS = [60_000, 5 * 60_000, 15 * 60_000];
|
||||||
const MAX_READY_STEP_DISPATCH_PER_SWEEP = 3;
|
|
||||||
|
|
||||||
let schedulerTimer: ReturnType<typeof setInterval> | null = null;
|
let schedulerTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
let schedulerSweeping = false;
|
let schedulerSweeping = false;
|
||||||
@@ -126,13 +126,19 @@ export async function dispatchReadyPlanSteps(
|
|||||||
failed: 0,
|
failed: 0,
|
||||||
errors: [],
|
errors: [],
|
||||||
};
|
};
|
||||||
|
const policy = readDigitalEmployeePlanStepDispatchPolicy();
|
||||||
|
if (!policy.enabled || !policy.auto_dispatch_ready_steps) {
|
||||||
|
result.skipped = candidates.length;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
const maxPerSweep = Math.max(1, Math.min(Math.floor(policy.max_per_sweep), 10));
|
||||||
let attempts = 0;
|
let attempts = 0;
|
||||||
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;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (attempts >= MAX_READY_STEP_DISPATCH_PER_SWEEP) {
|
if (attempts >= maxPerSweep) {
|
||||||
result.skipped += 1;
|
result.skipped += 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -938,6 +938,70 @@ describe("digital employee state service", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("persists plan step dispatch policy and records runtime events", async () => {
|
||||||
|
const { readDigitalEmployeeUiState, saveDigitalEmployeeUiState } = await import("./stateService");
|
||||||
|
|
||||||
|
saveDigitalEmployeeUiState({
|
||||||
|
action: "update_plan_step_dispatch_policy",
|
||||||
|
metadata: { source: "plan-step-settings" },
|
||||||
|
state: {
|
||||||
|
selectedDutyIdsByDate: {},
|
||||||
|
confirmedDates: {},
|
||||||
|
reportScheduleTime: "18:00",
|
||||||
|
reportGeneratedAtByDate: {},
|
||||||
|
decisionResultsByDate: {},
|
||||||
|
notificationStateById: {},
|
||||||
|
notificationPreferences: {
|
||||||
|
enabled: true,
|
||||||
|
muted_kinds: [],
|
||||||
|
muted_levels: [],
|
||||||
|
updated_at: null,
|
||||||
|
},
|
||||||
|
approvalSubscriptionPreferences: {
|
||||||
|
enabled: true,
|
||||||
|
subscribed_actions: ["comment", "delegate", "mark_timeout", "mark_risk", "clear_risk", "decision"],
|
||||||
|
subscribed_risk_levels: ["high", "critical"],
|
||||||
|
notify_on_timeout: true,
|
||||||
|
updated_at: null,
|
||||||
|
},
|
||||||
|
planStepDispatchPolicy: {
|
||||||
|
enabled: true,
|
||||||
|
max_per_sweep: 11,
|
||||||
|
auto_dispatch_ready_steps: false,
|
||||||
|
updated_at: "2026-06-07T08:07:00.000Z",
|
||||||
|
},
|
||||||
|
consoleRole: "sales",
|
||||||
|
rolePreferences: {},
|
||||||
|
profile: {
|
||||||
|
name: "飞天数字员工",
|
||||||
|
company: "qimingclaw 客户端",
|
||||||
|
position: "客户端任务执行员",
|
||||||
|
currentStatus: "working",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(readDigitalEmployeeUiState()).toMatchObject({
|
||||||
|
planStepDispatchPolicy: {
|
||||||
|
enabled: true,
|
||||||
|
max_per_sweep: 10,
|
||||||
|
auto_dispatch_ready_steps: false,
|
||||||
|
updated_at: "2026-06-07T08:07:00.000Z",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const policyEvent = Array.from(mockState.db?.events.values() ?? [])
|
||||||
|
.find((event) => event.kind === "digital_workday_update_plan_step_dispatch_policy");
|
||||||
|
expect(policyEvent).toMatchObject({
|
||||||
|
kind: "digital_workday_update_plan_step_dispatch_policy",
|
||||||
|
message: "数字员工PlanStep派发策略已更新",
|
||||||
|
});
|
||||||
|
expect(mockState.db?.outbox.get(`event:insert:${policyEvent?.id}`)).toMatchObject({
|
||||||
|
entity_type: "event",
|
||||||
|
operation: "insert",
|
||||||
|
status: "pending",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("records governance commands as formal runtime records", async () => {
|
it("records governance commands as formal runtime records", async () => {
|
||||||
const { recordDigitalEmployeeGovernanceCommand } = await import("./stateService");
|
const { recordDigitalEmployeeGovernanceCommand } = await import("./stateService");
|
||||||
|
|
||||||
|
|||||||
@@ -323,6 +323,13 @@ export interface DigitalEmployeePlanStepDispatchRecord {
|
|||||||
payload: Record<string, unknown>;
|
payload: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DigitalEmployeePlanStepDispatchPolicy {
|
||||||
|
enabled: boolean;
|
||||||
|
max_per_sweep: number;
|
||||||
|
auto_dispatch_ready_steps: boolean;
|
||||||
|
updated_at: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface DigitalEmployeeRouteDecisionRecord {
|
export interface DigitalEmployeeRouteDecisionRecord {
|
||||||
id: string;
|
id: string;
|
||||||
route_kind: string;
|
route_kind: string;
|
||||||
@@ -480,6 +487,7 @@ export interface DigitalEmployeeUiState {
|
|||||||
notify_on_timeout: boolean;
|
notify_on_timeout: boolean;
|
||||||
updated_at: string | null;
|
updated_at: string | null;
|
||||||
};
|
};
|
||||||
|
planStepDispatchPolicy?: DigitalEmployeePlanStepDispatchPolicy;
|
||||||
consoleRole?: string;
|
consoleRole?: string;
|
||||||
rolePreferences?: Record<string, unknown>;
|
rolePreferences?: Record<string, unknown>;
|
||||||
profile: {
|
profile: {
|
||||||
@@ -657,6 +665,7 @@ function defaultUiState(): DigitalEmployeeUiState {
|
|||||||
notificationStateById: {},
|
notificationStateById: {},
|
||||||
notificationPreferences: defaultNotificationPreferences(),
|
notificationPreferences: defaultNotificationPreferences(),
|
||||||
approvalSubscriptionPreferences: defaultApprovalSubscriptionPreferences(),
|
approvalSubscriptionPreferences: defaultApprovalSubscriptionPreferences(),
|
||||||
|
planStepDispatchPolicy: defaultPlanStepDispatchPolicy(),
|
||||||
consoleRole: "sales",
|
consoleRole: "sales",
|
||||||
rolePreferences: {},
|
rolePreferences: {},
|
||||||
profile: {
|
profile: {
|
||||||
@@ -729,6 +738,15 @@ function defaultApprovalSubscriptionPreferences(): NonNullable<DigitalEmployeeUi
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function defaultPlanStepDispatchPolicy(): DigitalEmployeePlanStepDispatchPolicy {
|
||||||
|
return {
|
||||||
|
enabled: true,
|
||||||
|
max_per_sweep: 3,
|
||||||
|
auto_dispatch_ready_steps: true,
|
||||||
|
updated_at: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeStringArray(value: unknown): string[] {
|
function normalizeStringArray(value: unknown): string[] {
|
||||||
if (!Array.isArray(value)) return [];
|
if (!Array.isArray(value)) return [];
|
||||||
return [...new Set(value.filter((item): item is string => typeof item === "string" && Boolean(item.trim())))];
|
return [...new Set(value.filter((item): item is string => typeof item === "string" && Boolean(item.trim())))];
|
||||||
@@ -768,6 +786,25 @@ function normalizeApprovalSubscriptionPreferences(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function boundedInteger(value: unknown, fallback: number, min: number, max: number): number {
|
||||||
|
const raw = typeof value === "number" ? value : Number(value);
|
||||||
|
if (!Number.isFinite(raw)) return fallback;
|
||||||
|
return Math.max(min, Math.min(Math.floor(raw), max));
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePlanStepDispatchPolicy(value: unknown): DigitalEmployeePlanStepDispatchPolicy {
|
||||||
|
const fallback = defaultPlanStepDispatchPolicy();
|
||||||
|
const record = value && typeof value === "object" && !Array.isArray(value)
|
||||||
|
? value as Record<string, unknown>
|
||||||
|
: {};
|
||||||
|
return {
|
||||||
|
enabled: record.enabled !== false,
|
||||||
|
max_per_sweep: boundedInteger(record.max_per_sweep, fallback.max_per_sweep, 1, 10),
|
||||||
|
auto_dispatch_ready_steps: record.auto_dispatch_ready_steps !== false,
|
||||||
|
updated_at: typeof record.updated_at === "string" ? record.updated_at : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeUiState(
|
function normalizeUiState(
|
||||||
value: Partial<DigitalEmployeeUiState>,
|
value: Partial<DigitalEmployeeUiState>,
|
||||||
): DigitalEmployeeUiState {
|
): DigitalEmployeeUiState {
|
||||||
@@ -784,6 +821,7 @@ function normalizeUiState(
|
|||||||
notificationStateById: normalizeRecord(value.notificationStateById),
|
notificationStateById: normalizeRecord(value.notificationStateById),
|
||||||
notificationPreferences: normalizeNotificationPreferences(value.notificationPreferences),
|
notificationPreferences: normalizeNotificationPreferences(value.notificationPreferences),
|
||||||
approvalSubscriptionPreferences: normalizeApprovalSubscriptionPreferences(value.approvalSubscriptionPreferences),
|
approvalSubscriptionPreferences: normalizeApprovalSubscriptionPreferences(value.approvalSubscriptionPreferences),
|
||||||
|
planStepDispatchPolicy: normalizePlanStepDispatchPolicy(value.planStepDispatchPolicy),
|
||||||
consoleRole: typeof value.consoleRole === "string" && value.consoleRole.trim()
|
consoleRole: typeof value.consoleRole === "string" && value.consoleRole.trim()
|
||||||
? value.consoleRole
|
? value.consoleRole
|
||||||
: fallback.consoleRole,
|
: fallback.consoleRole,
|
||||||
@@ -826,6 +864,8 @@ function digitalEmployeeUiActionMessage(action: string, date?: string): string {
|
|||||||
return "数字员工通知订阅已更新";
|
return "数字员工通知订阅已更新";
|
||||||
case "update_approval_subscriptions":
|
case "update_approval_subscriptions":
|
||||||
return "数字员工审批订阅已更新";
|
return "数字员工审批订阅已更新";
|
||||||
|
case "update_plan_step_dispatch_policy":
|
||||||
|
return "数字员工PlanStep派发策略已更新";
|
||||||
default:
|
default:
|
||||||
return `数字员工页面状态已更新${suffix}`;
|
return `数字员工页面状态已更新${suffix}`;
|
||||||
}
|
}
|
||||||
@@ -855,6 +895,10 @@ export function readDigitalEmployeeUiState(): DigitalEmployeeUiState {
|
|||||||
return normalizeUiState(state as Partial<DigitalEmployeeUiState>);
|
return normalizeUiState(state as Partial<DigitalEmployeeUiState>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function readDigitalEmployeePlanStepDispatchPolicy(): DigitalEmployeePlanStepDispatchPolicy {
|
||||||
|
return readDigitalEmployeeUiState().planStepDispatchPolicy ?? defaultPlanStepDispatchPolicy();
|
||||||
|
}
|
||||||
|
|
||||||
export function saveDigitalEmployeeUiState(
|
export function saveDigitalEmployeeUiState(
|
||||||
update: DigitalEmployeeUiStateUpdate,
|
update: DigitalEmployeeUiStateUpdate,
|
||||||
): DigitalEmployeeUiState {
|
): DigitalEmployeeUiState {
|
||||||
|
|||||||
@@ -334,6 +334,15 @@ describe("digital employee sync service", () => {
|
|||||||
seq: 2,
|
seq: 2,
|
||||||
dependsOn: ["step-0"],
|
dependsOn: ["step-0"],
|
||||||
preferredSkillIds: ["qimingclaw-mcp-tool-crm-crm-search"],
|
preferredSkillIds: ["qimingclaw-mcp-tool-crm-crm-search"],
|
||||||
|
payload: {
|
||||||
|
dependencyGraph: {
|
||||||
|
status: "blocked",
|
||||||
|
blockedDependencies: ["step-0"],
|
||||||
|
waitingDependencies: ["step-2"],
|
||||||
|
},
|
||||||
|
lastDispatch: { status: "failed" },
|
||||||
|
skipReason: "approval_pending",
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
mockState.fetch.mockResolvedValue(
|
mockState.fetch.mockResolvedValue(
|
||||||
@@ -365,7 +374,16 @@ describe("digital employee sync service", () => {
|
|||||||
status: "pending",
|
status: "pending",
|
||||||
seq: 2,
|
seq: 2,
|
||||||
depends_on: ["step-0"],
|
depends_on: ["step-0"],
|
||||||
|
dependency_count: 1,
|
||||||
|
dependency_status: "blocked",
|
||||||
|
blocked_dependencies: ["step-0"],
|
||||||
|
blocked_dependency_count: 1,
|
||||||
|
waiting_dependencies: ["step-2"],
|
||||||
|
waiting_dependency_count: 1,
|
||||||
preferred_skill_ids: ["qimingclaw-mcp-tool-crm-crm-search"],
|
preferred_skill_ids: ["qimingclaw-mcp-tool-crm-crm-search"],
|
||||||
|
dispatchable: false,
|
||||||
|
skip_reason: "approval_pending",
|
||||||
|
last_dispatch_status: "failed",
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
@@ -541,9 +559,24 @@ describe("digital employee sync service", () => {
|
|||||||
});
|
});
|
||||||
insertEventEntity("event-dispatch", "plan_step_dispatch_failed", {
|
insertEventEntity("event-dispatch", "plan_step_dispatch_failed", {
|
||||||
step_id: "step-1",
|
step_id: "step-1",
|
||||||
|
plan_id: "plan-1",
|
||||||
|
task_id: "task-1",
|
||||||
|
run_id: "run-1",
|
||||||
status: "failed",
|
status: "failed",
|
||||||
reason: "computer chat failed",
|
reason: "computer chat failed",
|
||||||
});
|
});
|
||||||
|
insertEventEntity("event-plan-step-ready", "plan_step_ready", {
|
||||||
|
stepId: "step-2",
|
||||||
|
planId: "plan-1",
|
||||||
|
status: "ready",
|
||||||
|
dependsOn: ["step-1"],
|
||||||
|
dependencyGraph: {
|
||||||
|
status: "ready",
|
||||||
|
blockedDependencies: [],
|
||||||
|
waitingDependencies: [],
|
||||||
|
satisfiedDependencies: ["step-1"],
|
||||||
|
},
|
||||||
|
});
|
||||||
insertEventEntity("event-governance-input", "governance_provide_task_input", {
|
insertEventEntity("event-governance-input", "governance_provide_task_input", {
|
||||||
commandKind: "provide_task_input",
|
commandKind: "provide_task_input",
|
||||||
commandId: "command-provide-input",
|
commandId: "command-provide-input",
|
||||||
@@ -648,6 +681,25 @@ describe("digital employee sync service", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
insertEventEntity("event-plan-step-policy", "digital_workday_update_plan_step_dispatch_policy", {
|
||||||
|
metadata: {
|
||||||
|
action: "update_plan_step_dispatch_policy",
|
||||||
|
plan_step_dispatch_policy: {
|
||||||
|
enabled: false,
|
||||||
|
max_per_sweep: 1,
|
||||||
|
auto_dispatch_ready_steps: false,
|
||||||
|
updated_at: "2026-06-07T08:07:00.000Z",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
planStepDispatchPolicy: {
|
||||||
|
enabled: false,
|
||||||
|
max_per_sweep: 1,
|
||||||
|
auto_dispatch_ready_steps: false,
|
||||||
|
updated_at: "2026-06-07T08:07:00.000Z",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
mockState.fetch.mockResolvedValue(
|
mockState.fetch.mockResolvedValue(
|
||||||
jsonResponse({
|
jsonResponse({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -658,12 +710,14 @@ describe("digital employee sync service", () => {
|
|||||||
{ entity_type: "event", entity_id: "event-artifact" },
|
{ entity_type: "event", entity_id: "event-artifact" },
|
||||||
{ entity_type: "event", entity_id: "event-skill" },
|
{ entity_type: "event", entity_id: "event-skill" },
|
||||||
{ entity_type: "event", entity_id: "event-dispatch" },
|
{ entity_type: "event", entity_id: "event-dispatch" },
|
||||||
|
{ entity_type: "event", entity_id: "event-plan-step-ready" },
|
||||||
{ entity_type: "event", entity_id: "event-governance-input" },
|
{ entity_type: "event", entity_id: "event-governance-input" },
|
||||||
{ entity_type: "event", entity_id: "event-notification-read" },
|
{ entity_type: "event", entity_id: "event-notification-read" },
|
||||||
{ entity_type: "event", entity_id: "event-notification-dismiss" },
|
{ entity_type: "event", entity_id: "event-notification-dismiss" },
|
||||||
{ entity_type: "event", entity_id: "event-notification-reply" },
|
{ entity_type: "event", entity_id: "event-notification-reply" },
|
||||||
{ entity_type: "event", entity_id: "event-notification-preferences" },
|
{ entity_type: "event", entity_id: "event-notification-preferences" },
|
||||||
{ entity_type: "event", entity_id: "event-approval-subscriptions" },
|
{ entity_type: "event", entity_id: "event-approval-subscriptions" },
|
||||||
|
{ entity_type: "event", entity_id: "event-plan-step-policy" },
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -736,9 +790,22 @@ describe("digital employee sync service", () => {
|
|||||||
expect(businessView("event-dispatch")).toMatchObject({
|
expect(businessView("event-dispatch")).toMatchObject({
|
||||||
category: "dispatch",
|
category: "dispatch",
|
||||||
step_id: "step-1",
|
step_id: "step-1",
|
||||||
|
plan_id: "plan-1",
|
||||||
|
task_id: "task-1",
|
||||||
|
run_id: "run-1",
|
||||||
status: "failed",
|
status: "failed",
|
||||||
reason: "computer chat failed",
|
reason: "computer chat failed",
|
||||||
});
|
});
|
||||||
|
expect(businessView("event-plan-step-ready")).toMatchObject({
|
||||||
|
category: "dispatch",
|
||||||
|
step_id: "step-2",
|
||||||
|
plan_id: "plan-1",
|
||||||
|
status: "ready",
|
||||||
|
dependency_count: 1,
|
||||||
|
blocked_dependencies: [],
|
||||||
|
waiting_dependencies: [],
|
||||||
|
satisfied_dependencies: ["step-1"],
|
||||||
|
});
|
||||||
expect(businessView("event-governance-input")).toMatchObject({
|
expect(businessView("event-governance-input")).toMatchObject({
|
||||||
category: "governance",
|
category: "governance",
|
||||||
command_kind: "provide_task_input",
|
command_kind: "provide_task_input",
|
||||||
@@ -790,6 +857,16 @@ describe("digital employee sync service", () => {
|
|||||||
});
|
});
|
||||||
expect(businessView("event-approval-subscriptions")).not.toHaveProperty("state");
|
expect(businessView("event-approval-subscriptions")).not.toHaveProperty("state");
|
||||||
expect(businessView("event-approval-subscriptions")).not.toHaveProperty("approvalSubscriptionPreferences");
|
expect(businessView("event-approval-subscriptions")).not.toHaveProperty("approvalSubscriptionPreferences");
|
||||||
|
expect(businessView("event-plan-step-policy")).toMatchObject({
|
||||||
|
category: "dispatch",
|
||||||
|
action: "update_plan_step_dispatch_policy",
|
||||||
|
policy_enabled: false,
|
||||||
|
max_per_sweep: 1,
|
||||||
|
auto_dispatch_ready_steps: false,
|
||||||
|
updated_at: "2026-06-07T08:07:00.000Z",
|
||||||
|
});
|
||||||
|
expect(businessView("event-plan-step-policy")).not.toHaveProperty("state");
|
||||||
|
expect(businessView("event-plan-step-policy")).not.toHaveProperty("planStepDispatchPolicy");
|
||||||
expect(readEntity("event", "event-route")).toMatchObject({
|
expect(readEntity("event", "event-route")).toMatchObject({
|
||||||
sync_status: "synced",
|
sync_status: "synced",
|
||||||
sync_error: null,
|
sync_error: null,
|
||||||
|
|||||||
@@ -458,18 +458,7 @@ function buildBusinessView(
|
|||||||
assigned_skill_id: stringField(record.assignedSkillId ?? record.assigned_skill_id) || null,
|
assigned_skill_id: stringField(record.assignedSkillId ?? record.assigned_skill_id) || null,
|
||||||
};
|
};
|
||||||
case "plan_step":
|
case "plan_step":
|
||||||
return {
|
return planStepBusinessView(base, row, record);
|
||||||
...base,
|
|
||||||
plan_id: stringField(record.planId ?? record.plan_id) || null,
|
|
||||||
step_id: row.entity_id,
|
|
||||||
seq: typeof record.seq === "number" ? record.seq : null,
|
|
||||||
depends_on: Array.isArray(record.dependsOn ?? record.depends_on)
|
|
||||||
? record.dependsOn ?? record.depends_on
|
|
||||||
: [],
|
|
||||||
preferred_skill_ids: Array.isArray(record.preferredSkillIds ?? record.preferred_skill_ids)
|
|
||||||
? record.preferredSkillIds ?? record.preferred_skill_ids
|
|
||||||
: [],
|
|
||||||
};
|
|
||||||
case "run":
|
case "run":
|
||||||
return {
|
return {
|
||||||
...base,
|
...base,
|
||||||
@@ -511,6 +500,36 @@ function buildBusinessView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function planStepBusinessView(
|
||||||
|
base: Record<string, unknown>,
|
||||||
|
row: OutboxRow,
|
||||||
|
record: Record<string, unknown>,
|
||||||
|
): Record<string, unknown> {
|
||||||
|
const payload = objectRecord(record.payload) ?? {};
|
||||||
|
const dependencyGraph = objectRecord(payload.dependencyGraph ?? payload.dependency_graph) ?? {};
|
||||||
|
const lastDispatch = objectRecord(payload.lastDispatch ?? payload.last_dispatch) ?? {};
|
||||||
|
const dependsOn = stringArrayField(record.dependsOn ?? record.depends_on);
|
||||||
|
const blockedDependencies = stringArrayField(dependencyGraph.blockedDependencies ?? dependencyGraph.blocked_dependencies);
|
||||||
|
const waitingDependencies = stringArrayField(dependencyGraph.waitingDependencies ?? dependencyGraph.waiting_dependencies);
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
plan_id: stringField(record.planId ?? record.plan_id) || null,
|
||||||
|
step_id: row.entity_id,
|
||||||
|
seq: numberField(record.seq),
|
||||||
|
depends_on: dependsOn,
|
||||||
|
dependency_count: dependsOn.length,
|
||||||
|
dependency_status: stringField(dependencyGraph.status) || null,
|
||||||
|
blocked_dependencies: blockedDependencies,
|
||||||
|
blocked_dependency_count: blockedDependencies.length,
|
||||||
|
waiting_dependencies: waitingDependencies,
|
||||||
|
waiting_dependency_count: waitingDependencies.length,
|
||||||
|
preferred_skill_ids: stringArrayField(record.preferredSkillIds ?? record.preferred_skill_ids),
|
||||||
|
dispatchable: stringField(record.status) === "ready" && blockedDependencies.length === 0 && waitingDependencies.length === 0,
|
||||||
|
skip_reason: stringField(payload.skipReason ?? payload.skip_reason) || null,
|
||||||
|
last_dispatch_status: stringField(lastDispatch.status) || null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function buildEventBusinessView(
|
function buildEventBusinessView(
|
||||||
base: Record<string, unknown>,
|
base: Record<string, unknown>,
|
||||||
row: OutboxRow,
|
row: OutboxRow,
|
||||||
@@ -569,7 +588,9 @@ function eventSpecificBusinessView(
|
|||||||
return artifactEventBusinessView(payload);
|
return artifactEventBusinessView(payload);
|
||||||
}
|
}
|
||||||
if (kind.startsWith("skill_call_")) return skillCallBusinessView(payload);
|
if (kind.startsWith("skill_call_")) return skillCallBusinessView(payload);
|
||||||
|
if (isPlanStepDependencyEvent(kind)) return planStepDependencyBusinessView(payload);
|
||||||
if (kind.startsWith("plan_step_dispatch_")) return planStepDispatchBusinessView(payload);
|
if (kind.startsWith("plan_step_dispatch_")) return planStepDispatchBusinessView(payload);
|
||||||
|
if (isPlanStepDispatchPolicyEvent(kind)) return planStepDispatchPolicyBusinessView(payload);
|
||||||
if (kind === "governance_provide_task_input") return governanceProvideTaskInputBusinessView(payload);
|
if (kind === "governance_provide_task_input") return governanceProvideTaskInputBusinessView(payload);
|
||||||
if (isNotificationEvent(kind)) return notificationBusinessView(payload);
|
if (isNotificationEvent(kind)) return notificationBusinessView(payload);
|
||||||
return {};
|
return {};
|
||||||
@@ -667,11 +688,43 @@ function skillCallBusinessView(payload: Record<string, unknown>): Record<string,
|
|||||||
function planStepDispatchBusinessView(payload: Record<string, unknown>): Record<string, unknown> {
|
function planStepDispatchBusinessView(payload: Record<string, unknown>): Record<string, unknown> {
|
||||||
return {
|
return {
|
||||||
step_id: stringField(payload.step_id ?? payload.stepId) || null,
|
step_id: stringField(payload.step_id ?? payload.stepId) || null,
|
||||||
|
plan_id: stringField(payload.plan_id ?? payload.planId) || null,
|
||||||
|
task_id: stringField(payload.task_id ?? payload.taskId) || 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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function planStepDependencyBusinessView(payload: Record<string, unknown>): Record<string, unknown> {
|
||||||
|
const dependencyGraph = objectRecord(payload.dependencyGraph ?? payload.dependency_graph) ?? {};
|
||||||
|
const dependencies = stringArrayField(payload.dependsOn ?? payload.depends_on);
|
||||||
|
return {
|
||||||
|
step_id: stringField(payload.stepId ?? payload.step_id) || null,
|
||||||
|
plan_id: stringField(payload.planId ?? payload.plan_id) || null,
|
||||||
|
status: stringField(payload.status ?? dependencyGraph.status) || null,
|
||||||
|
dependency_count: dependencies.length,
|
||||||
|
blocked_dependencies: stringArrayField(dependencyGraph.blockedDependencies ?? dependencyGraph.blocked_dependencies),
|
||||||
|
waiting_dependencies: stringArrayField(dependencyGraph.waitingDependencies ?? dependencyGraph.waiting_dependencies),
|
||||||
|
satisfied_dependencies: stringArrayField(dependencyGraph.satisfiedDependencies ?? dependencyGraph.satisfied_dependencies),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function planStepDispatchPolicyBusinessView(payload: Record<string, unknown>): Record<string, unknown> {
|
||||||
|
const metadata = objectRecord(payload.metadata) ?? {};
|
||||||
|
const state = objectRecord(payload.state) ?? {};
|
||||||
|
const policy = objectRecord(metadata.plan_step_dispatch_policy)
|
||||||
|
?? objectRecord(state.planStepDispatchPolicy)
|
||||||
|
?? {};
|
||||||
|
return {
|
||||||
|
action: stringField(metadata.action ?? payload.action) || "update_plan_step_dispatch_policy",
|
||||||
|
policy_enabled: typeof policy.enabled === "boolean" ? policy.enabled : null,
|
||||||
|
max_per_sweep: numberField(policy.max_per_sweep),
|
||||||
|
auto_dispatch_ready_steps: typeof policy.auto_dispatch_ready_steps === "boolean" ? policy.auto_dispatch_ready_steps : null,
|
||||||
|
updated_at: stringField(policy.updated_at) || null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function governanceProvideTaskInputBusinessView(payload: Record<string, unknown>): Record<string, unknown> {
|
function governanceProvideTaskInputBusinessView(payload: Record<string, unknown>): Record<string, unknown> {
|
||||||
const commandPayload = objectRecord(payload.payload) ?? {};
|
const commandPayload = objectRecord(payload.payload) ?? {};
|
||||||
const result = objectRecord(payload.result) ?? {};
|
const result = objectRecord(payload.result) ?? {};
|
||||||
@@ -714,7 +767,9 @@ function eventBusinessCategory(kind: string): string {
|
|||||||
if (isApprovalSubscriptionEvent(kind)) return "approval";
|
if (isApprovalSubscriptionEvent(kind)) return "approval";
|
||||||
if (kind.startsWith("artifact_")) return "artifact";
|
if (kind.startsWith("artifact_")) return "artifact";
|
||||||
if (kind.startsWith("skill_call_")) return "skill";
|
if (kind.startsWith("skill_call_")) return "skill";
|
||||||
|
if (isPlanStepDependencyEvent(kind)) return "dispatch";
|
||||||
if (kind.startsWith("plan_step_dispatch_")) return "dispatch";
|
if (kind.startsWith("plan_step_dispatch_")) return "dispatch";
|
||||||
|
if (isPlanStepDispatchPolicyEvent(kind)) return "dispatch";
|
||||||
if (isNotificationEvent(kind)) return "notification";
|
if (isNotificationEvent(kind)) return "notification";
|
||||||
if (kind.startsWith("governance_")) return "governance";
|
if (kind.startsWith("governance_")) return "governance";
|
||||||
return "runtime";
|
return "runtime";
|
||||||
@@ -724,6 +779,14 @@ function isApprovalSubscriptionEvent(kind: string): boolean {
|
|||||||
return kind === "digital_workday_update_approval_subscriptions";
|
return kind === "digital_workday_update_approval_subscriptions";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isPlanStepDependencyEvent(kind: string): boolean {
|
||||||
|
return kind === "plan_step_ready" || kind === "plan_step_blocked" || kind === "plan_step_waiting";
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPlanStepDispatchPolicyEvent(kind: string): boolean {
|
||||||
|
return kind === "digital_workday_update_plan_step_dispatch_policy";
|
||||||
|
}
|
||||||
|
|
||||||
function isNotificationEvent(kind: string): boolean {
|
function isNotificationEvent(kind: string): boolean {
|
||||||
return kind === "digital_workday_update_notification_preferences"
|
return kind === "digital_workday_update_notification_preferences"
|
||||||
|| kind === "digital_workday_notification_read"
|
|| kind === "digital_workday_notification_read"
|
||||||
|
|||||||
@@ -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` 事件,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/plan-step/dispatch-policy`,ready PlanStep 安全派发、管理端依赖图视图和本地派发策略已开始闭环。
|
||||||
- 后续缺口:跨任务编排执行、依赖阻塞人工处理 UI、派发并发策略下发和管理端依赖图视图仍待吸收。
|
- 后续缺口:跨任务编排执行、依赖阻塞人工处理 UI、多设备 lease 和远端策略拉取仍待吸收。
|
||||||
- 建议:下一轮围绕管理端依赖图和派发策略下发,把 ready PlanStep 从本地安全派发推进到跨端可治理调度。
|
- 建议:下一轮围绕依赖阻塞人工处理 UI 或跨任务编排,把本地可治理调度推进到更完整的执行控制面。
|
||||||
|
|
||||||
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