吸收数字员工诊断审计观测
This commit is contained in:
@@ -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 }),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user