吸收数字员工入口路由观测
This commit is contained in:
@@ -17,6 +17,7 @@ import type {
|
||||
DigitalEmployeeDuty,
|
||||
DigitalEmployeeManagedService,
|
||||
DigitalEmployeePlanAgentState,
|
||||
DigitalEmployeeRouteSummary,
|
||||
DigitalEmployeeRunDetail,
|
||||
DigitalEmployeeTaskAgentState,
|
||||
DigitalEmployeeTaskExecutionSummary,
|
||||
@@ -671,7 +672,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';
|
||||
type BusinessViewKind = 'plans' | 'tasks' | 'runs' | 'events' | 'artifacts' | 'approvals' | 'route-decisions';
|
||||
type BusinessViewRow = Record<string, unknown>;
|
||||
|
||||
export function isQimingclawDigitalApiEnabled(): boolean {
|
||||
@@ -755,7 +756,7 @@ export async function handleQimingclawDigitalApi<T>(
|
||||
const approvalId = decodeURIComponent(approvalActionMatch[1] || '');
|
||||
return await handleApprovalAction(approvalId, parseOptionalJsonBody(options.body), await readQimingclawSnapshot()) as T;
|
||||
}
|
||||
const businessViewMatch = pathname.match(/^\/api\/digital-employee\/(plans|tasks|runs|events|artifacts|approvals)$/);
|
||||
const businessViewMatch = pathname.match(/^\/api\/digital-employee\/(plans|tasks|runs|events|artifacts|approvals|route-decisions)$/);
|
||||
if (method === 'GET' && businessViewMatch) {
|
||||
return buildBusinessViewResponse(businessViewMatch[1] as BusinessViewKind, await readQimingclawSnapshot(), url.searchParams) as T;
|
||||
}
|
||||
@@ -2372,14 +2373,109 @@ function buildBusinessViewResponse(kind: BusinessViewKind, snapshot: QimingclawS
|
||||
events: businessEventRows(snapshot),
|
||||
artifacts: businessArtifactRows(snapshot),
|
||||
approvals: businessApprovalRows(snapshot),
|
||||
'route-decisions': routeDecisionRows(snapshot),
|
||||
};
|
||||
const rows = filterBusinessRows(rowsByKind[kind], searchParams);
|
||||
const rows = kind === 'route-decisions'
|
||||
? filterRouteDecisionRows(rowsByKind[kind], searchParams)
|
||||
: filterBusinessRows(rowsByKind[kind], searchParams);
|
||||
return {
|
||||
...paginateBusinessRows(rows, searchParams),
|
||||
source: BUSINESS_VIEW_SOURCE,
|
||||
};
|
||||
}
|
||||
|
||||
function routeDecisionRows(snapshot: QimingclawSnapshot | null): BusinessViewRow[] {
|
||||
return runtimeEvents(snapshot)
|
||||
.filter((event) => event.kind === 'route_decision')
|
||||
.map((event) => {
|
||||
const payload = asRecord(event.payload) ?? {};
|
||||
const decision = routeDecisionFromRuntimePayload(payload, event);
|
||||
const attentionLevel = routeDecisionAttentionLevel(decision);
|
||||
return {
|
||||
...decision,
|
||||
route_decision_id: decision.id,
|
||||
event_id: event.id,
|
||||
status: decision.policy_result || 'accepted',
|
||||
status_label: routeDecisionStatusLabel(decision),
|
||||
attention_level: attentionLevel,
|
||||
entity_type: 'route_decision',
|
||||
entity_id: decision.id,
|
||||
occurred_at: event.occurredAt,
|
||||
created_at: event.createdAt,
|
||||
updated_at: decision.recorded_at || event.occurredAt,
|
||||
payload,
|
||||
};
|
||||
})
|
||||
.sort((left, right) => stringValue(right.recorded_at).localeCompare(stringValue(left.recorded_at)) || stringValue(right.event_id).localeCompare(stringValue(left.event_id)));
|
||||
}
|
||||
|
||||
function filterRouteDecisionRows(rows: BusinessViewRow[], searchParams: URLSearchParams): BusinessViewRow[] {
|
||||
const routeKind = stringValue(searchParams.get('route_kind'));
|
||||
const intentKind = stringValue(searchParams.get('intent_kind'));
|
||||
const policyResult = stringValue(searchParams.get('policy_result'));
|
||||
return filterBusinessRows(rows, searchParams)
|
||||
.filter((row) => !routeKind || stringValue(row.route_kind) === routeKind)
|
||||
.filter((row) => !intentKind || stringValue(row.intent_kind) === intentKind)
|
||||
.filter((row) => !policyResult || stringValue(row.policy_result) === policyResult);
|
||||
}
|
||||
|
||||
function routeDecisionFromRuntimePayload(payload: Record<string, unknown>, event: QimingclawEventRecord): RouteDecisionRecord {
|
||||
const decision = asRecord(payload.routeDecision ?? payload.route_decision ?? payload) ?? {};
|
||||
return {
|
||||
id: stringValue(decision.id) || event.id,
|
||||
route_kind: stringValue(decision.route_kind ?? decision.routeKind) || 'ChatOnly',
|
||||
intent_kind: stringValue(decision.intent_kind ?? decision.intentKind) || 'chat',
|
||||
reason_codes: uniqueStrings(arrayValue(decision.reason_codes ?? decision.reasonCodes)),
|
||||
candidate_skills: uniqueStrings(arrayValue(decision.candidate_skills ?? decision.candidateSkills)),
|
||||
context_refs: asRecord(decision.context_refs ?? decision.contextRefs) ?? {},
|
||||
created_command_id: stringValue(decision.created_command_id ?? decision.createdCommandId) || null,
|
||||
correlation_id: stringValue(decision.correlation_id ?? decision.correlationId) || event.id,
|
||||
creates_plan: Boolean(decision.creates_plan ?? decision.createsPlan),
|
||||
creates_task_run: Boolean(decision.creates_task_run ?? decision.createsTaskRun),
|
||||
approval_mode: stringValue(decision.approval_mode ?? decision.approvalMode) || null,
|
||||
policy_result: stringValue(decision.policy_result ?? decision.policyResult) || 'accepted',
|
||||
recorded_at: stringValue(decision.recorded_at ?? decision.recordedAt) || event.occurredAt,
|
||||
entrypoint: stringValue(decision.entrypoint) || null,
|
||||
actor: stringValue(decision.actor) || null,
|
||||
};
|
||||
}
|
||||
|
||||
function routeDecisionAttentionLevel(decision: RouteDecisionRecord): 'info' | 'action' | 'warn' | 'error' {
|
||||
if (decision.policy_result && decision.policy_result !== 'accepted') return 'error';
|
||||
if (decision.reason_codes.includes('candidate_skills_disabled') || decision.reason_codes.includes('route_action_missing_context')) return 'warn';
|
||||
if (decision.created_command_id) return 'action';
|
||||
return 'info';
|
||||
}
|
||||
|
||||
function routeDecisionStatusLabel(decision: RouteDecisionRecord): string {
|
||||
const attentionLevel = routeDecisionAttentionLevel(decision);
|
||||
if (attentionLevel === 'error') return '策略拒绝';
|
||||
if (attentionLevel === 'warn') return '需要关注';
|
||||
if (decision.created_command_id) return '已映射命令';
|
||||
return '已记录';
|
||||
}
|
||||
|
||||
function buildRouteDecisionSummary(snapshot: QimingclawSnapshot | null): DigitalEmployeeRouteSummary {
|
||||
const rows = routeDecisionRows(snapshot);
|
||||
const latest = rows[0] ?? null;
|
||||
return {
|
||||
total: rows.length,
|
||||
mapped_count: rows.filter((row) => stringValue(row.created_command_id)).length,
|
||||
attention_count: rows.filter((row) => ['warn', 'error'].includes(stringValue(row.attention_level))).length,
|
||||
latest: latest
|
||||
? {
|
||||
id: stringValue(latest.route_decision_id),
|
||||
route_kind: stringValue(latest.route_kind),
|
||||
intent_kind: stringValue(latest.intent_kind),
|
||||
status_label: stringValue(latest.status_label),
|
||||
attention_level: stringValue(latest.attention_level),
|
||||
created_command_id: stringValue(latest.created_command_id) || null,
|
||||
recorded_at: stringValue(latest.recorded_at),
|
||||
}
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
function businessPlanRows(snapshot: QimingclawSnapshot | null): BusinessViewRow[] {
|
||||
const runtimeById = new Map(runtimePlans(snapshot).map((plan) => [plan.id, plan]));
|
||||
const tasks = buildTasks(null, snapshot);
|
||||
@@ -3242,6 +3338,7 @@ function buildNotifications(snapshot: QimingclawSnapshot | null, state: AdapterS
|
||||
...syncFailureNotifications(snapshot),
|
||||
...managedServiceNotifications(snapshot),
|
||||
...planStepDispatchNotifications(snapshot),
|
||||
...routeDecisionNotifications(snapshot),
|
||||
...taskNotifications(snapshot),
|
||||
]);
|
||||
const overlays = state.notificationStateById ?? {};
|
||||
@@ -3376,6 +3473,31 @@ function planStepDispatchNotifications(snapshot: QimingclawSnapshot | null): Use
|
||||
});
|
||||
}
|
||||
|
||||
function routeDecisionNotifications(snapshot: QimingclawSnapshot | null): UserNotification[] {
|
||||
return routeDecisionRows(snapshot)
|
||||
.filter((row) => ['warn', 'error', 'action'].includes(stringValue(row.attention_level)))
|
||||
.map((row) => {
|
||||
const attentionLevel = stringValue(row.attention_level);
|
||||
const routeId = stringValue(row.route_decision_id) || stringValue(row.event_id);
|
||||
const commandId = stringValue(row.created_command_id);
|
||||
const reasonCodes = arrayValue(row.reason_codes).map(stringValue).filter(Boolean).join(' / ');
|
||||
return {
|
||||
notification_id: `route-decision:${routeId}:${slugifyIdPart(stringValue(row.recorded_at))}`,
|
||||
plan_id: stringValue(asRecord(row.context_refs)?.plan_id),
|
||||
task_id: stringValue(asRecord(row.context_refs)?.task_id),
|
||||
kind: attentionLevel === 'action' ? 'task_progress' : 'task_failed',
|
||||
title: attentionLevel === 'action' ? '入口路由已映射' : '入口路由需要关注',
|
||||
body: commandId
|
||||
? `${row.route_kind} / ${row.intent_kind} -> ${commandId}`
|
||||
: `${row.route_kind} / ${row.intent_kind}${reasonCodes ? `:${reasonCodes}` : ''}`,
|
||||
level: attentionLevel === 'error' ? 'error' : attentionLevel === 'warn' ? 'warn' : 'action',
|
||||
status: 'unread',
|
||||
correlation_id: routeId,
|
||||
created_at: stringValue(row.recorded_at) || new Date().toISOString(),
|
||||
} satisfies UserNotification;
|
||||
});
|
||||
}
|
||||
|
||||
function applyNotificationOverlay(notification: UserNotification, overlay?: NotificationState): UserNotification {
|
||||
if (!overlay) return notification;
|
||||
return {
|
||||
@@ -4066,6 +4188,7 @@ function buildWorkday(date: string, snapshot: QimingclawSnapshot | null): Digita
|
||||
})),
|
||||
],
|
||||
},
|
||||
route_summary: buildRouteDecisionSummary(snapshot),
|
||||
daily_report: buildDailyReportProjection({ date, state, generatedAt, selectedCount, events, executionSummaries, runDetails, artifactSources, successCount, failureCount, runningCount, snapshot }),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user