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

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

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