吸收数字员工诊断审计投影

This commit is contained in:
baiyanyun
2026-06-09 19:24:28 +08:00
parent 755a8f041d
commit d1ae9421c4
6 changed files with 361 additions and 152 deletions

View File

@@ -5,11 +5,13 @@ import type {
CanonicalEvent,
CronJob,
CronRun,
CostSummary,
DashboardResponse,
DebugPlan,
DebugRun,
DebugStoreResponse,
DebugTask,
DiagResult,
DigitalEmployeeDecision,
DigitalEmployeeDuty,
DigitalEmployeeManagedService,
@@ -647,6 +649,15 @@ export async function handleQimingclawDigitalApi<T>(
if (method === 'GET' && pathname === '/api/dashboard') {
return buildDashboard(await readQimingclawSnapshot()) as T;
}
if ((method === 'GET' || method === 'POST') && pathname === '/api/doctor') {
return buildDoctor(await readQimingclawSnapshot()) as T;
}
if (method === 'GET' && pathname === '/api/cost') {
return { cost: buildCostSummary(await readQimingclawSnapshot()) } as T;
}
if (method === 'GET' && pathname === '/api/audit') {
return { events: buildAuditEvents(await readQimingclawSnapshot(), url.searchParams) } as T;
}
if (method === 'GET' && pathname === '/api/artifact') {
return { artifacts: buildArtifacts(await readQimingclawSnapshot()) } as T;
}
@@ -1889,6 +1900,197 @@ function buildDashboard(snapshot: QimingclawSnapshot | null): DashboardResponse
};
}
function buildDoctor(snapshot: QimingclawSnapshot | null): { results: DiagResult[]; summary: Record<string, unknown> } {
const now = new Date().toISOString();
const managedServices = buildManagedServices(snapshot);
const tasks = buildTasks(null, snapshot);
const approvals = buildApprovals(snapshot);
const runtimeErrors = runtimeEvents(snapshot).filter((event) => /fail|error|reject|cancel/i.test(`${event.kind} ${event.message}`));
const results: DiagResult[] = managedServices.map((service) => ({
id: `service:${service.service_id}`,
severity: service.requires_human_action || service.status === 'stopped' ? 'error' : service.status === 'degraded' ? 'warn' : 'ok',
category: 'service',
message: `${service.name}${service.status_label || service.status}`,
details: compactRecord({ error: service.error, kind: service.kind, running: service.running, dependent_tasks: service.dependent_tasks }),
source: 'qimingclaw-managed-service',
created_at: service.last_seen_at || now,
}));
const sync = snapshot?.sync;
results.push({
id: 'sync:management',
severity: doctorSeverityForSync(sync ?? null),
category: 'sync',
message: syncLine(snapshot),
details: sync ? compactRecord({ pending: sync.pending, failed: sync.failed, endpoint: sync.endpoint, missing_credentials: sync.missingCredentials, last_error: sync.lastSyncError }) : null,
source: 'qimingclaw-sync-status',
created_at: sync?.lastSyncAt ?? now,
});
for (const failure of sync?.recentFailures ?? []) {
results.push({
id: `sync_failure:${failure.id}`,
severity: failure.dueForRetry === false ? 'error' : 'warn',
category: 'sync',
message: syncFailureLine(failure),
details: compactRecord({ entity_type: failure.entityType, entity_id: failure.entityId, operation: failure.operation, attempts: failure.attempts, next_retry_at: failure.nextRetryAt }),
source: 'qimingclaw-sync-failure',
created_at: failure.lastAttemptAt ?? failure.updatedAt ?? failure.createdAt,
});
}
const failedTasks = tasks.filter((task) => taskStatusTone(task.status) === 'danger');
results.push({
id: 'runtime:tasks',
severity: failedTasks.length > 0 ? 'error' : tasks.length === 0 ? 'warn' : 'ok',
category: 'runtime',
message: tasks.length === 0 ? '尚未读取到 qimingclaw runtime task。' : `任务 ${tasks.length} 个,异常 ${failedTasks.length} 个。`,
details: compactRecord({ task_count: tasks.length, failed_task_count: failedTasks.length }),
source: 'qimingclaw-runtime-records',
created_at: now,
});
results.push({
id: 'runtime:approvals',
severity: approvals.some((approval) => approval.status === 'pending') ? 'warn' : 'ok',
category: 'approval',
message: `审批 ${approvals.length} 个,待处理 ${approvals.filter((approval) => approval.status === 'pending').length} 个。`,
details: compactRecord({ approval_count: approvals.length, pending_count: approvals.filter((approval) => approval.status === 'pending').length }),
source: 'qimingclaw-approval-records',
created_at: now,
});
if (runtimeErrors.length > 0) {
results.push({
id: 'runtime:events:error',
severity: 'error',
category: 'runtime',
message: `最近运行事件中发现 ${runtimeErrors.length} 条异常事实。`,
details: { event_ids: runtimeErrors.slice(0, 8).map((event) => event.id) },
source: 'qimingclaw-runtime-events',
created_at: runtimeErrors[0]?.occurredAt ?? now,
});
}
if (!hasRuntimeRecords(snapshot)) {
results.push({
id: 'runtime:data:empty',
severity: 'warn',
category: 'data',
message: '本地数字员工 runtime 记录为空,页面将只显示兼容投影。',
source: 'qimingclaw-runtime-records',
created_at: now,
});
}
return {
results,
summary: {
ok: results.filter((result) => result.severity === 'ok').length,
warn: results.filter((result) => result.severity === 'warn').length,
error: results.filter((result) => result.severity === 'error').length,
generated_at: now,
source: 'qimingclaw-runtime-projection',
},
};
}
function doctorSeverityForSync(sync: QimingclawSyncStatus | null): DiagResult['severity'] {
if (!sync) return 'warn';
if (sync.failed > 0 || sync.lastSyncError || (sync.missingCredentials?.length ?? 0) > 0) return 'error';
if (!sync.enabled || !sync.endpoint || sync.pending > 0) return 'warn';
return 'ok';
}
function buildCostSummary(snapshot: QimingclawSnapshot | null): CostSummary {
const requestCount = runtimeEvents(snapshot).length + runtimeRuns(snapshot).length + runtimeTasks(snapshot).length;
return {
session_cost_usd: 0,
daily_cost_usd: 0,
monthly_cost_usd: 0,
total_tokens: 0,
request_count: requestCount,
by_model: {
'qimingclaw-local': {
model: 'qimingclaw-local',
cost_usd: 0,
total_tokens: 0,
request_count: requestCount,
},
},
estimated: true,
source: 'qimingclaw-runtime-projection',
updated_at: snapshot?.generatedAt ?? new Date().toISOString(),
};
}
function buildAuditEvents(snapshot: QimingclawSnapshot | null, searchParams: URLSearchParams): Array<Record<string, unknown>> {
const limit = Math.max(1, Math.min(Math.floor(numberValue(searchParams.get('limit')) ?? 100), 300));
const category = stringValue(searchParams.get('category')) || stringValue(searchParams.get('kind'));
const level = stringValue(searchParams.get('level'));
const entityId = stringValue(searchParams.get('id'));
const events = [
...runtimeEvents(snapshot).map(auditEventFromRuntimeEvent),
...(snapshot?.sync?.recentFailures ?? []).map((failure) => ({
audit_id: `sync_failure:${failure.id}`,
category: 'sync',
action: 'sync_failed',
level: failure.dueForRetry === false ? 'error' : 'warn',
message: syncFailureLine(failure),
actor: 'qimingclaw-sync',
entity_type: failure.entityType,
entity_id: failure.entityId,
occurred_at: failure.lastAttemptAt ?? failure.updatedAt ?? failure.createdAt,
payload: failure,
})),
...buildArtifacts(snapshot).flatMap((artifact) => auditEventsFromArtifact(artifact)),
];
return events
.filter((event) => !category || stringValue(event.category) === category || stringValue(event.entity_type) === category)
.filter((event) => !level || stringValue(event.level) === level)
.filter((event) => !entityId || stringValue(event.entity_id) === entityId || stringValue(event.audit_id).includes(entityId))
.sort((left, right) => stringValue(right.occurred_at).localeCompare(stringValue(left.occurred_at)))
.slice(0, limit);
}
function auditEventFromRuntimeEvent(event: QimingclawEventRecord): Record<string, unknown> {
const payload = asRecord(event.payload) ?? {};
const category = event.kind.startsWith('governance_')
? 'governance'
: event.kind === 'route_decision'
? 'route'
: event.kind === 'approval_decision'
? 'approval'
: 'runtime';
const entityType = event.runId ? 'run' : event.taskId ? 'task' : event.planId ? 'plan' : 'event';
return {
audit_id: event.id,
category,
action: event.kind,
level: /fail|error|reject|cancel/i.test(`${event.kind} ${event.message}`) ? 'error' : 'info',
message: event.message || event.kind,
actor: stringValue(payload.actor) || stringValue(payload.source) || 'qimingclaw-runtime',
entity_type: entityType,
entity_id: event.runId ?? event.taskId ?? event.planId ?? event.id,
occurred_at: event.occurredAt,
payload,
};
}
function auditEventsFromArtifact(artifact: ArtifactEntry): Array<Record<string, unknown>> {
const deliveryStatus = stringValue(artifact.delivery_status);
if (!deliveryStatus) return [];
return [{
audit_id: `artifact_delivery:${artifact.artifact_id ?? artifact.subject_id}`,
category: 'artifact',
action: stringValue(artifact.delivery_stage) || 'artifact_delivery',
level: deliveryStatus === 'failed' || deliveryStatus === 'error' ? 'error' : 'info',
message: `${artifact.name || artifact.kind || 'Artifact'}${deliveryStatus}`,
actor: stringValue(artifact.source_tool) || 'qimingclaw-file-server',
entity_type: 'artifact',
entity_id: artifact.artifact_id ?? artifact.subject_id,
occurred_at: stringValue(artifact.created_at) || new Date().toISOString(),
payload: artifact,
}];
}
function buildCanonicalEvents(
snapshot: QimingclawSnapshot | null,
tasks: DebugTask[],

View File

@@ -72,9 +72,13 @@ export interface Integration {
}
export interface DiagResult {
id?: string;
severity: 'ok' | 'warn' | 'error';
category: string;
message: string;
details?: string | Record<string, unknown> | null;
source?: string;
created_at?: string;
}
export interface MemoryEntry {
@@ -94,6 +98,9 @@ export interface CostSummary {
total_tokens: number;
request_count: number;
by_model: Record<string, ModelStats>;
estimated?: boolean;
source?: string;
updated_at?: string;
}
export interface ModelStats {

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,7 +16,7 @@
console.warn("[qimingclaw] digital employee auth bootstrap skipped", error);
}
</script>
<script type="module" crossorigin src="./assets/index-2bfLPJ8G.js"></script>
<script type="module" crossorigin src="./assets/index-BgNhwaUy.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-BarRiSjT.css">
</head>
<body>

View File

@@ -594,9 +594,9 @@ GET /api/digital-employee/approvals
- 建议:下一轮围绕 ready PlanStep 安全触发和管理端路由视图,把入口路由从单次 action 映射推进到可观测调度策略。
11. **诊断 / 成本 / 审计视图**
- 已吸收:同步失败诊断较完整,部分 sandbox / memory / service 日志在 qimingclaw 其他模块中存在。
- 缺口:`/api/doctor``/api/cost``/api/audit`sandbox audittoken/cost、CLI/tool 调用审计尚未统一进入数字员工视图和 outbox。
- 建议:从只读 dashboard 投影开始,避免先做复杂写操作
- 已吸收:同步失败诊断较完整,部分 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 audit 与 token/cost 采集,把只读投影升级为可追溯的真实审计与成本统计
12. **管理端业务查询模型**
- 已吸收:管理端已有通用 sync record 接收、查询、payload 摘要和深链。