吸收数字员工诊断审计观测

This commit is contained in:
baiyanyun
2026-06-10 14:49:49 +08:00
parent 914ed81f19
commit 4f489f402a
10 changed files with 432 additions and 200 deletions

View File

@@ -2044,7 +2044,13 @@
}
.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 {
.de-observation-strip-stack {
display: grid;
gap: 6px;
min-width: 0;
}
.de-route-summary-strip,
.de-diagnostic-summary-strip {
display: grid;
grid-template-columns: repeat(3, minmax(54px, 0.36fr)) minmax(0, 1fr);
gap: 6px;
@@ -2054,11 +2060,16 @@
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-route-summary-strip span,
.de-diagnostic-summary-strip span { min-width: 0; display: grid; gap: 2px; }
.de-route-summary-strip em,
.de-diagnostic-summary-strip em { color: #5b7590; font-size: 10px; font-style: normal; white-space: nowrap; }
.de-route-summary-strip strong,
.de-diagnostic-summary-strip strong { color: #12344f; font-size: 14px; line-height: 1; }
.de-route-summary-strip .is-warning strong,
.de-diagnostic-summary-strip .is-warning strong { color: #a33b3b; }
.de-route-summary-strip small,
.de-diagnostic-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;
@@ -2996,7 +3007,7 @@
.de-dashboard--compact .de-report-summary-title-row h3 { font-size: 15px; }
.de-dashboard--compact .de-workday-execution-body {
gap: 6px;
grid-template-rows: auto minmax(80px, 0.48fr) auto minmax(0, 1fr);
grid-template-rows: auto auto minmax(80px, 0.48fr) auto minmax(0, 1fr);
}
.de-dashboard--compact .de-execution-status-strip { gap: 5px; }
.de-dashboard--compact .de-execution-status-strip span { padding: 6px 7px; }

View File

@@ -14,6 +14,7 @@ import type {
DiagResult,
DigitalEmployeeDailyReport,
DigitalEmployeeDecision,
DigitalEmployeeDiagnosticSummary,
DigitalEmployeeDuty,
DigitalEmployeeManagedService,
DigitalEmployeePlanAgentState,
@@ -672,7 +673,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' | 'route-decisions';
type BusinessViewKind = 'plans' | 'tasks' | 'runs' | 'events' | 'artifacts' | 'approvals' | 'route-decisions' | 'audits';
type BusinessViewRow = Record<string, unknown>;
export function isQimingclawDigitalApiEnabled(): boolean {
@@ -756,7 +757,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|route-decisions)$/);
const businessViewMatch = pathname.match(/^\/api\/digital-employee\/(plans|tasks|runs|events|artifacts|approvals|route-decisions|audits)$/);
if (method === 'GET' && businessViewMatch) {
return buildBusinessViewResponse(businessViewMatch[1] as BusinessViewKind, await readQimingclawSnapshot(), url.searchParams) as T;
}
@@ -2365,6 +2366,106 @@ function buildAuditEvents(snapshot: QimingclawSnapshot | null, searchParams: URL
.slice(0, limit);
}
function buildDiagnosticSummary(snapshot: QimingclawSnapshot | null): DigitalEmployeeDiagnosticSummary {
const doctorResults = buildDoctor(snapshot).results;
const auditEvents = buildAuditEvents(snapshot, new URLSearchParams({ limit: '300' }));
const cost = buildCostSummary(snapshot);
const latestIssue = latestDiagnosticIssue(doctorResults, auditEvents);
return {
doctor: {
ok: doctorResults.filter((result) => result.severity === 'ok').length,
warn: doctorResults.filter((result) => result.severity === 'warn').length,
error: doctorResults.filter((result) => result.severity === 'error').length,
},
audit: {
info: auditEvents.filter((event) => stringValue(event.level) === 'info').length,
warn: auditEvents.filter((event) => stringValue(event.level) === 'warn').length,
error: auditEvents.filter((event) => stringValue(event.level) === 'error').length,
},
cost: {
request_count: cost.request_count,
estimated: Boolean(cost.estimated),
updated_at: cost.updated_at ?? null,
},
latest_issue: latestIssue,
};
}
function latestDiagnosticIssue(
doctorResults: DiagResult[],
auditEvents: Array<Record<string, unknown>>,
): DigitalEmployeeDiagnosticSummary['latest_issue'] {
const doctorError = latestDoctorIssue(doctorResults, 'error');
if (doctorError) return doctorError;
const auditError = latestAuditIssue(auditEvents, 'error');
if (auditError) return auditError;
const doctorWarn = latestDoctorIssue(doctorResults, 'warn');
if (doctorWarn) return doctorWarn;
return latestAuditIssue(auditEvents, 'warn');
}
function latestDoctorIssue(
doctorResults: DiagResult[],
level: 'warn' | 'error',
): DigitalEmployeeDiagnosticSummary['latest_issue'] {
const result = doctorResults
.filter((item) => item.severity === level)
.sort((left, right) => stringValue(right.created_at).localeCompare(stringValue(left.created_at)))[0];
if (!result) return null;
return {
id: stringValue(result.id) || `${result.category}:${level}`,
source: 'doctor',
level,
category: stringValue(result.category) || 'diagnostic',
message: result.message,
occurred_at: result.created_at ?? null,
};
}
function latestAuditIssue(
auditEvents: Array<Record<string, unknown>>,
level: 'warn' | 'error',
): DigitalEmployeeDiagnosticSummary['latest_issue'] {
const event = auditEvents.find((item) => stringValue(item.level) === level);
if (!event) return null;
return {
id: stringValue(event.audit_id) || `${stringValue(event.category) || 'audit'}:${level}`,
source: 'audit',
level,
category: stringValue(event.category) || stringValue(event.entity_type) || 'audit',
message: stringValue(event.message) || stringValue(event.action) || '审计事件需要关注',
occurred_at: stringValue(event.occurred_at) || null,
};
}
function auditRows(snapshot: QimingclawSnapshot | null): BusinessViewRow[] {
return buildAuditEvents(snapshot, new URLSearchParams({ limit: '300' })).map((event) => ({
...event,
audit_id: stringValue(event.audit_id),
status: stringValue(event.level) || 'info',
status_label: auditLevelLabel(stringValue(event.level)),
entity_id: stringValue(event.entity_id) || stringValue(event.audit_id),
updated_at: stringValue(event.occurred_at),
source: BUSINESS_VIEW_SOURCE,
}));
}
function filterAuditRows(rows: BusinessViewRow[], searchParams: URLSearchParams): BusinessViewRow[] {
const category = stringValue(searchParams.get('category')) || stringValue(searchParams.get('kind'));
const level = stringValue(searchParams.get('level'));
const id = stringValue(searchParams.get('id'));
return filterBusinessRows(rows, searchParams)
.filter((row) => !category || stringValue(row.category) === category || stringValue(row.entity_type) === category)
.filter((row) => !level || stringValue(row.level) === level)
.filter((row) => !id || stringValue(row.entity_id) === id || stringValue(row.audit_id).includes(id));
}
function auditLevelLabel(level: string): string {
if (level === 'error') return '异常';
if (level === 'warn') return '警告';
return '信息';
}
function buildBusinessViewResponse(kind: BusinessViewKind, snapshot: QimingclawSnapshot | null, searchParams: URLSearchParams): Record<string, unknown> {
const rowsByKind: Record<BusinessViewKind, BusinessViewRow[]> = {
plans: businessPlanRows(snapshot),
@@ -2374,10 +2475,13 @@ function buildBusinessViewResponse(kind: BusinessViewKind, snapshot: QimingclawS
artifacts: businessArtifactRows(snapshot),
approvals: businessApprovalRows(snapshot),
'route-decisions': routeDecisionRows(snapshot),
audits: auditRows(snapshot),
};
const rows = kind === 'route-decisions'
? filterRouteDecisionRows(rowsByKind[kind], searchParams)
: filterBusinessRows(rowsByKind[kind], searchParams);
: kind === 'audits'
? filterAuditRows(rowsByKind[kind], searchParams)
: filterBusinessRows(rowsByKind[kind], searchParams);
return {
...paginateBusinessRows(rows, searchParams),
source: BUSINESS_VIEW_SOURCE,
@@ -3339,6 +3443,8 @@ function buildNotifications(snapshot: QimingclawSnapshot | null, state: AdapterS
...managedServiceNotifications(snapshot),
...planStepDispatchNotifications(snapshot),
...routeDecisionNotifications(snapshot),
...diagnosticNotifications(snapshot),
...auditNotifications(snapshot),
...taskNotifications(snapshot),
]);
const overlays = state.notificationStateById ?? {};
@@ -3498,6 +3604,48 @@ function routeDecisionNotifications(snapshot: QimingclawSnapshot | null): UserNo
});
}
function diagnosticNotifications(snapshot: QimingclawSnapshot | null): UserNotification[] {
return buildDoctor(snapshot).results
.filter((result) => result.severity === 'error' || result.severity === 'warn')
.map((result) => {
const level = result.severity === 'error' ? 'error' : 'warn';
const diagnosticId = stringValue(result.id) || `${stringValue(result.category) || 'diagnostic'}:${level}`;
return {
notification_id: `diagnostic:${diagnosticId}`,
plan_id: '',
task_id: '',
kind: level === 'error' ? 'task_failed' : 'need_input',
title: level === 'error' ? '诊断发现异常' : '诊断需要关注',
body: result.message,
level,
status: 'unread',
correlation_id: diagnosticId,
created_at: result.created_at ?? new Date().toISOString(),
} satisfies UserNotification;
});
}
function auditNotifications(snapshot: QimingclawSnapshot | null): UserNotification[] {
return auditRows(snapshot)
.filter((event) => ['error', 'warn'].includes(stringValue(event.level)))
.map((event) => {
const level = stringValue(event.level) === 'error' ? 'error' : 'warn';
const auditId = stringValue(event.audit_id) || stringValue(event.entity_id) || 'audit';
return {
notification_id: `audit:${auditId}`,
plan_id: stringValue(event.plan_id),
task_id: stringValue(event.task_id) || (stringValue(event.entity_type) === 'task' ? stringValue(event.entity_id) : ''),
kind: level === 'error' ? 'task_failed' : 'need_input',
title: level === 'error' ? '审计发现异常' : '审计需要关注',
body: stringValue(event.message) || stringValue(event.action) || auditId,
level,
status: 'unread',
correlation_id: auditId,
created_at: stringValue(event.occurred_at) || new Date().toISOString(),
} satisfies UserNotification;
});
}
function applyNotificationOverlay(notification: UserNotification, overlay?: NotificationState): UserNotification {
if (!overlay) return notification;
return {
@@ -4189,6 +4337,7 @@ function buildWorkday(date: string, snapshot: QimingclawSnapshot | null): Digita
],
},
route_summary: buildRouteDecisionSummary(snapshot),
diagnostic_summary: buildDiagnosticSummary(snapshot),
daily_report: buildDailyReportProjection({ date, state, generatedAt, selectedCount, events, executionSummaries, runDetails, artifactSources, successCount, failureCount, runningCount, snapshot }),
};
}

View File

@@ -1082,6 +1082,10 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
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 diagnosticSummary = isLiveProjection ? workdayProjection?.diagnostic_summary : null;
const diagnosticIssueCount = diagnosticSummary ? diagnosticSummary.doctor.error + diagnosticSummary.doctor.warn : 0;
const auditRiskCount = diagnosticSummary ? diagnosticSummary.audit.error + diagnosticSummary.audit.warn : 0;
const latestDiagnosticIssue = diagnosticSummary?.latest_issue ?? null;
const dailyReport = isLiveProjection ? workdayProjection?.daily_report : undefined;
const runDetails = isLiveProjection
? (workdayProjection?.run_details?.length ? workdayProjection.run_details : allEvents.map(runDetailFromEvent))
@@ -2006,12 +2010,25 @@ 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>
{(diagnosticSummary || (routeSummary && routeSummary.total > 0)) && (
<div className="de-observation-strip-stack">
{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>
)}
{diagnosticSummary && (
<div className="de-diagnostic-summary-strip">
<span className={diagnosticIssueCount > 0 ? 'is-warning' : ''}><em></em><strong>{diagnosticIssueCount}</strong></span>
<span className={auditRiskCount > 0 ? 'is-warning' : ''}><em></em><strong>{auditRiskCount}</strong></span>
<span><em></em><strong>{diagnosticSummary.cost.request_count}</strong></span>
<small>{latestDiagnosticIssue ? `${latestDiagnosticIssue.category} · ${latestDiagnosticIssue.message}` : '诊断、审计和成本投影正常'}</small>
</div>
)}
</div>
)}

View File

@@ -850,6 +850,32 @@ export interface DigitalEmployeeRouteSummary {
} | null;
}
export interface DigitalEmployeeDiagnosticSummary {
doctor: {
ok: number;
warn: number;
error: number;
};
audit: {
info: number;
warn: number;
error: number;
};
cost: {
request_count: number;
estimated: boolean;
updated_at?: string | null;
};
latest_issue?: {
id: string;
source: 'doctor' | 'audit';
level: 'warn' | 'error';
category: string;
message: string;
occurred_at?: string | null;
} | null;
}
export interface DigitalEmployeeWorkdayProjection {
source: DigitalEmployeeProjectionSource;
demo_reason?: string | null;
@@ -867,6 +893,7 @@ export interface DigitalEmployeeWorkdayProjection {
decisions: DigitalEmployeeDecision[];
delivery: DigitalEmployeeDelivery;
route_summary?: DigitalEmployeeRouteSummary;
diagnostic_summary?: DigitalEmployeeDiagnosticSummary;
daily_report?: DigitalEmployeeDailyReport;
}