吸收数字员工诊断审计观测
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;
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -16,8 +16,8 @@
|
||||
console.warn("[qimingclaw] digital employee auth bootstrap skipped", error);
|
||||
}
|
||||
</script>
|
||||
<script type="module" crossorigin src="./assets/index-YhUdeF7L.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-DSHN6Btj.css">
|
||||
<script type="module" crossorigin src="./assets/index-Cel_CqfI.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-DLC8VZMk.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -168,6 +168,33 @@ function checkSgrobotDigitalAssets() {
|
||||
console.log(`[DigitalEmployeeCheck] ${assetRefs.length} asset references OK`);
|
||||
}
|
||||
|
||||
function checkDigitalDiagnosticAuditProjection() {
|
||||
console.log("\n[DigitalEmployeeCheck] Verify diagnostic/audit projection hooks");
|
||||
const adapterPath = path.join(packageRoot, "embedded", "sgrobot-digital", "src", "lib", "qimingclawAdapter.ts");
|
||||
const typesPath = path.join(packageRoot, "embedded", "sgrobot-digital", "src", "types", "api.ts");
|
||||
const pagePath = path.join(packageRoot, "embedded", "sgrobot-digital", "src", "pages", "digital", "CommandDeck.tsx");
|
||||
const adapter = fs.readFileSync(adapterPath, "utf8");
|
||||
const types = fs.readFileSync(typesPath, "utf8");
|
||||
const page = fs.readFileSync(pagePath, "utf8");
|
||||
const requiredSnippets = [
|
||||
[adapter, "'audits'", "audit business view kind"],
|
||||
[adapter, "buildDiagnosticSummary", "diagnostic summary builder"],
|
||||
[adapter, "filterAuditRows", "audit filters"],
|
||||
[adapter, "diagnosticNotifications", "diagnostic notifications"],
|
||||
[adapter, "auditNotifications", "audit notifications"],
|
||||
[types, "DigitalEmployeeDiagnosticSummary", "diagnostic summary type"],
|
||||
[types, "diagnostic_summary?: DigitalEmployeeDiagnosticSummary", "workday diagnostic field"],
|
||||
[page, "de-diagnostic-summary-strip", "dashboard diagnostic strip"],
|
||||
];
|
||||
const missing = requiredSnippets
|
||||
.filter(([content, snippet]) => !content.includes(snippet))
|
||||
.map(([, , label]) => label);
|
||||
if (missing.length > 0) {
|
||||
throw new Error(`Missing diagnostic/audit projection hooks: ${missing.join(", ")}`);
|
||||
}
|
||||
console.log("[DigitalEmployeeCheck] diagnostic/audit projection hooks OK");
|
||||
}
|
||||
|
||||
function checkLocalDatabaseSchema() {
|
||||
console.log("\n[DigitalEmployeeCheck] Verify local SQLite digital schema");
|
||||
const dbPath = path.join(os.homedir(), ".qimingclaw", "qimingclaw.db");
|
||||
@@ -210,6 +237,7 @@ function main() {
|
||||
runStep("Build Electron main process", "npm", ["run", "build:main:dev"]);
|
||||
runStep("Build embedded digital employee", "npm", ["run", "build:sgrobot-digital"]);
|
||||
checkSgrobotDigitalAssets();
|
||||
checkDigitalDiagnosticAuditProjection();
|
||||
checkLocalDatabaseSchema();
|
||||
console.log("\n[DigitalEmployeeCheck] All checks passed");
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user