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

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 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-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; display: grid;
grid-template-columns: repeat(3, minmax(54px, 0.36fr)) minmax(0, 1fr); grid-template-columns: repeat(3, minmax(54px, 0.36fr)) minmax(0, 1fr);
gap: 6px; gap: 6px;
@@ -2054,11 +2060,16 @@
background: rgba(247,251,255,0.88); background: rgba(247,251,255,0.88);
border: 1px solid rgba(79,172,254,0.12); 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 span,
.de-route-summary-strip em { color: #5b7590; font-size: 10px; font-style: normal; white-space: nowrap; } .de-diagnostic-summary-strip span { min-width: 0; display: grid; gap: 2px; }
.de-route-summary-strip strong { color: #12344f; font-size: 14px; line-height: 1; } .de-route-summary-strip em,
.de-route-summary-strip .is-warning strong { color: #a33b3b; } .de-diagnostic-summary-strip em { color: #5b7590; font-size: 10px; font-style: normal; white-space: nowrap; }
.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 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 { .de-task-summary-scroll {
min-height: 0; min-height: 0;
overflow-y: auto; overflow-y: auto;
@@ -2996,7 +3007,7 @@
.de-dashboard--compact .de-report-summary-title-row h3 { font-size: 15px; } .de-dashboard--compact .de-report-summary-title-row h3 { font-size: 15px; }
.de-dashboard--compact .de-workday-execution-body { .de-dashboard--compact .de-workday-execution-body {
gap: 6px; 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 { gap: 5px; }
.de-dashboard--compact .de-execution-status-strip span { padding: 6px 7px; } .de-dashboard--compact .de-execution-status-strip span { padding: 6px 7px; }

View File

@@ -14,6 +14,7 @@ import type {
DiagResult, DiagResult,
DigitalEmployeeDailyReport, DigitalEmployeeDailyReport,
DigitalEmployeeDecision, DigitalEmployeeDecision,
DigitalEmployeeDiagnosticSummary,
DigitalEmployeeDuty, DigitalEmployeeDuty,
DigitalEmployeeManagedService, DigitalEmployeeManagedService,
DigitalEmployeePlanAgentState, DigitalEmployeePlanAgentState,
@@ -672,7 +673,7 @@ const PLAN_SCHEDULES: Record<string, { expression: string; nextRun: string; sche
const BUSINESS_VIEW_SOURCE = 'qimingclaw-runtime-projection'; 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>; type BusinessViewRow = Record<string, unknown>;
export function isQimingclawDigitalApiEnabled(): boolean { export function isQimingclawDigitalApiEnabled(): boolean {
@@ -756,7 +757,7 @@ export async function handleQimingclawDigitalApi<T>(
const approvalId = decodeURIComponent(approvalActionMatch[1] || ''); const approvalId = decodeURIComponent(approvalActionMatch[1] || '');
return await handleApprovalAction(approvalId, parseOptionalJsonBody(options.body), await readQimingclawSnapshot()) as T; 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) { if (method === 'GET' && businessViewMatch) {
return buildBusinessViewResponse(businessViewMatch[1] as BusinessViewKind, await readQimingclawSnapshot(), url.searchParams) as T; 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); .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> { function buildBusinessViewResponse(kind: BusinessViewKind, snapshot: QimingclawSnapshot | null, searchParams: URLSearchParams): Record<string, unknown> {
const rowsByKind: Record<BusinessViewKind, BusinessViewRow[]> = { const rowsByKind: Record<BusinessViewKind, BusinessViewRow[]> = {
plans: businessPlanRows(snapshot), plans: businessPlanRows(snapshot),
@@ -2374,10 +2475,13 @@ function buildBusinessViewResponse(kind: BusinessViewKind, snapshot: QimingclawS
artifacts: businessArtifactRows(snapshot), artifacts: businessArtifactRows(snapshot),
approvals: businessApprovalRows(snapshot), approvals: businessApprovalRows(snapshot),
'route-decisions': routeDecisionRows(snapshot), 'route-decisions': routeDecisionRows(snapshot),
audits: auditRows(snapshot),
}; };
const rows = kind === 'route-decisions' const rows = kind === 'route-decisions'
? filterRouteDecisionRows(rowsByKind[kind], searchParams) ? filterRouteDecisionRows(rowsByKind[kind], searchParams)
: filterBusinessRows(rowsByKind[kind], searchParams); : kind === 'audits'
? filterAuditRows(rowsByKind[kind], searchParams)
: filterBusinessRows(rowsByKind[kind], searchParams);
return { return {
...paginateBusinessRows(rows, searchParams), ...paginateBusinessRows(rows, searchParams),
source: BUSINESS_VIEW_SOURCE, source: BUSINESS_VIEW_SOURCE,
@@ -3339,6 +3443,8 @@ function buildNotifications(snapshot: QimingclawSnapshot | null, state: AdapterS
...managedServiceNotifications(snapshot), ...managedServiceNotifications(snapshot),
...planStepDispatchNotifications(snapshot), ...planStepDispatchNotifications(snapshot),
...routeDecisionNotifications(snapshot), ...routeDecisionNotifications(snapshot),
...diagnosticNotifications(snapshot),
...auditNotifications(snapshot),
...taskNotifications(snapshot), ...taskNotifications(snapshot),
]); ]);
const overlays = state.notificationStateById ?? {}; 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 { function applyNotificationOverlay(notification: UserNotification, overlay?: NotificationState): UserNotification {
if (!overlay) return notification; if (!overlay) return notification;
return { return {
@@ -4189,6 +4337,7 @@ function buildWorkday(date: string, snapshot: QimingclawSnapshot | null): Digita
], ],
}, },
route_summary: buildRouteDecisionSummary(snapshot), route_summary: buildRouteDecisionSummary(snapshot),
diagnostic_summary: buildDiagnosticSummary(snapshot),
daily_report: buildDailyReportProjection({ date, state, generatedAt, selectedCount, events, executionSummaries, runDetails, artifactSources, successCount, failureCount, runningCount, 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 actionableTaskAgents = taskAgents.filter((agent) => agent.can_retry || agent.can_skip || agent.can_cancel);
const routeSummary = isLiveProjection ? workdayProjection?.route_summary : null; const routeSummary = isLiveProjection ? workdayProjection?.route_summary : null;
const latestRoute = routeSummary?.latest ?? 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 dailyReport = isLiveProjection ? workdayProjection?.daily_report : undefined;
const runDetails = isLiveProjection const runDetails = isLiveProjection
? (workdayProjection?.run_details?.length ? workdayProjection.run_details : allEvents.map(runDetailFromEvent)) ? (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> <span><em></em><strong>{failedEvents.length}</strong></span>
</div> </div>
{routeSummary && routeSummary.total > 0 && ( {(diagnosticSummary || (routeSummary && routeSummary.total > 0)) && (
<div className="de-route-summary-strip"> <div className="de-observation-strip-stack">
<span><em></em><strong>{routeSummary.total}</strong></span> {routeSummary && routeSummary.total > 0 && (
<span><em></em><strong>{routeSummary.mapped_count}</strong></span> <div className="de-route-summary-strip">
<span className={routeSummary.attention_count > 0 ? 'is-warning' : ''}><em></em><strong>{routeSummary.attention_count}</strong></span> <span><em></em><strong>{routeSummary.total}</strong></span>
<small>{latestRoute ? `${latestRoute.route_kind} / ${latestRoute.intent_kind} · ${latestRoute.status_label}` : '暂无路由决策'}</small> <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> </div>
)} )}

View File

@@ -850,6 +850,32 @@ export interface DigitalEmployeeRouteSummary {
} | null; } | 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 { export interface DigitalEmployeeWorkdayProjection {
source: DigitalEmployeeProjectionSource; source: DigitalEmployeeProjectionSource;
demo_reason?: string | null; demo_reason?: string | null;
@@ -867,6 +893,7 @@ export interface DigitalEmployeeWorkdayProjection {
decisions: DigitalEmployeeDecision[]; decisions: DigitalEmployeeDecision[];
delivery: DigitalEmployeeDelivery; delivery: DigitalEmployeeDelivery;
route_summary?: DigitalEmployeeRouteSummary; route_summary?: DigitalEmployeeRouteSummary;
diagnostic_summary?: DigitalEmployeeDiagnosticSummary;
daily_report?: DigitalEmployeeDailyReport; 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

View File

@@ -16,8 +16,8 @@
console.warn("[qimingclaw] digital employee auth bootstrap skipped", error); console.warn("[qimingclaw] digital employee auth bootstrap skipped", error);
} }
</script> </script>
<script type="module" crossorigin src="./assets/index-YhUdeF7L.js"></script> <script type="module" crossorigin src="./assets/index-Cel_CqfI.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-DSHN6Btj.css"> <link rel="stylesheet" crossorigin href="./assets/index-DLC8VZMk.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@@ -168,6 +168,33 @@ function checkSgrobotDigitalAssets() {
console.log(`[DigitalEmployeeCheck] ${assetRefs.length} asset references OK`); 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() { function checkLocalDatabaseSchema() {
console.log("\n[DigitalEmployeeCheck] Verify local SQLite digital schema"); console.log("\n[DigitalEmployeeCheck] Verify local SQLite digital schema");
const dbPath = path.join(os.homedir(), ".qimingclaw", "qimingclaw.db"); 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 Electron main process", "npm", ["run", "build:main:dev"]);
runStep("Build embedded digital employee", "npm", ["run", "build:sgrobot-digital"]); runStep("Build embedded digital employee", "npm", ["run", "build:sgrobot-digital"]);
checkSgrobotDigitalAssets(); checkSgrobotDigitalAssets();
checkDigitalDiagnosticAuditProjection();
checkLocalDatabaseSchema(); checkLocalDatabaseSchema();
console.log("\n[DigitalEmployeeCheck] All checks passed"); console.log("\n[DigitalEmployeeCheck] All checks passed");
} catch (error) { } catch (error) {

View File

@@ -594,8 +594,8 @@ GET /api/digital-employee/approvals
- 建议:下一轮围绕管理端正式路由 UI 和策略下发,把入口路由从本地可观测推进到跨端治理策略。 - 建议:下一轮围绕管理端正式路由 UI 和策略下发,把入口路由从本地可观测推进到跨端治理策略。
11. **诊断 / 成本 / 审计视图** 11. **诊断 / 成本 / 审计视图**
- 已吸收:同步失败诊断较完整,部分 sandbox / memory / service 日志在 qimingclaw 其他模块中存在embedded adapter 已接管 `/api/doctor``/api/cost``/api/audit`,从 snapshot、runtime records、sync status、managed services 和 artifact delivery facts 生成只读诊断、估算成本和审计投影,只读投影已开始闭环。 - 已吸收:同步失败诊断较完整,部分 sandbox / memory / service 日志在 qimingclaw 其他模块中存在embedded adapter 已接管 `/api/doctor``/api/cost``/api/audit`,从 snapshot、runtime records、sync status、managed services 和 artifact delivery facts 生成只读诊断、估算成本和审计投影;数字员工首页已展示诊断/审计/成本摘要,`/api/digital-employee/audits` 已提供本地审计业务视图,诊断与审计风险通知联动已开始闭环。
- 缺口sandbox audit 文件、真实 token/cost 采集、CLI/tool 调用审计、诊断修复动作和管理端审计视图尚未统一进入数字员工视图和 outbox。 - 缺口sandbox audit 文件、真实 token/cost 采集、CLI/tool 调用审计、诊断修复动作和管理端正式审计视图尚未统一进入数字员工视图和 outbox。
- 建议:下一轮围绕 sandbox audit 与 token/cost 采集,把只读投影升级为可追溯的真实审计与成本统计。 - 建议:下一轮围绕 sandbox audit 与 token/cost 采集,把只读投影升级为可追溯的真实审计与成本统计。
12. **管理端业务查询模型** 12. **管理端业务查询模型**