@@ -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 } ) ,
} ;
}