吸收数字员工入口路由观测

This commit is contained in:
baiyanyun
2026-06-10 11:43:48 +08:00
parent d12db03216
commit 914ed81f19
11 changed files with 388 additions and 194 deletions

View File

@@ -2027,7 +2027,7 @@
min-height: 0;
overflow: hidden;
display: grid;
grid-template-rows: auto minmax(96px, 0.58fr) auto minmax(0, 1fr);
grid-template-rows: auto auto minmax(96px, 0.58fr) auto minmax(0, 1fr);
gap: 8px;
}
.de-execution-status-strip {
@@ -2044,6 +2044,21 @@
}
.de-execution-status-strip em { color: #5b7590; font-size: 11px; font-style: normal; white-space: nowrap; }
.de-execution-status-strip strong { color: #12344f; font-size: 18px; line-height: 1; }
.de-route-summary-strip {
display: grid;
grid-template-columns: repeat(3, minmax(54px, 0.36fr)) minmax(0, 1fr);
gap: 6px;
align-items: center;
padding: 7px 8px;
border-radius: 10px;
background: rgba(247,251,255,0.88);
border: 1px solid rgba(79,172,254,0.12);
}
.de-route-summary-strip span { min-width: 0; display: grid; gap: 2px; }
.de-route-summary-strip em { color: #5b7590; font-size: 10px; font-style: normal; white-space: nowrap; }
.de-route-summary-strip strong { color: #12344f; font-size: 14px; line-height: 1; }
.de-route-summary-strip .is-warning strong { color: #a33b3b; }
.de-route-summary-strip small { min-width: 0; color: #41627f; font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.de-task-summary-scroll {
min-height: 0;
overflow-y: auto;
@@ -3121,7 +3136,7 @@
}
.de-workday-execution-body {
grid-template-rows: auto auto auto auto;
grid-template-rows: auto auto auto auto auto;
}
.de-task-summary-scroll,

View File

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

View File

@@ -1080,6 +1080,8 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
const managedServiceIssues = managedServices.filter((service) => service.requires_human_action || service.status === 'degraded');
const taskAgents = isLiveProjection ? (workdayProjection?.task_agents ?? []) : [];
const actionableTaskAgents = taskAgents.filter((agent) => agent.can_retry || agent.can_skip || agent.can_cancel);
const routeSummary = isLiveProjection ? workdayProjection?.route_summary : null;
const latestRoute = routeSummary?.latest ?? null;
const dailyReport = isLiveProjection ? workdayProjection?.daily_report : undefined;
const runDetails = isLiveProjection
? (workdayProjection?.run_details?.length ? workdayProjection.run_details : allEvents.map(runDetailFromEvent))
@@ -2004,6 +2006,15 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
<span><em></em><strong>{failedEvents.length}</strong></span>
</div>
{routeSummary && routeSummary.total > 0 && (
<div className="de-route-summary-strip">
<span><em></em><strong>{routeSummary.total}</strong></span>
<span><em></em><strong>{routeSummary.mapped_count}</strong></span>
<span className={routeSummary.attention_count > 0 ? 'is-warning' : ''}><em></em><strong>{routeSummary.attention_count}</strong></span>
<small>{latestRoute ? `${latestRoute.route_kind} / ${latestRoute.intent_kind} · ${latestRoute.status_label}` : '暂无路由决策'}</small>
</div>
)}
<div className="de-task-summary-scroll">
<div className="de-task-summary-grid" aria-label="任务执行汇总">
{taskSummaryCards.length === 0 ? (

View File

@@ -835,6 +835,21 @@ export interface DigitalEmployeeDailyReport {
pdf_url?: string | null;
}
export interface DigitalEmployeeRouteSummary {
total: number;
mapped_count: number;
attention_count: number;
latest?: {
id: string;
route_kind: string;
intent_kind: string;
status_label: string;
attention_level: string;
created_command_id?: string | null;
recorded_at: string;
} | null;
}
export interface DigitalEmployeeWorkdayProjection {
source: DigitalEmployeeProjectionSource;
demo_reason?: string | null;
@@ -851,6 +866,7 @@ export interface DigitalEmployeeWorkdayProjection {
run_details?: DigitalEmployeeRunDetail[];
decisions: DigitalEmployeeDecision[];
delivery: DigitalEmployeeDelivery;
route_summary?: DigitalEmployeeRouteSummary;
daily_report?: DigitalEmployeeDailyReport;
}