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