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