吸收数字员工诊断审计投影
This commit is contained in:
@@ -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[],
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -594,9 +594,9 @@ GET /api/digital-employee/approvals
|
||||
- 建议:下一轮围绕 ready PlanStep 安全触发和管理端路由视图,把入口路由从单次 action 映射推进到可观测调度策略。
|
||||
|
||||
11. **诊断 / 成本 / 审计视图**
|
||||
- 已吸收:同步失败诊断较完整,部分 sandbox / memory / service 日志在 qimingclaw 其他模块中存在。
|
||||
- 缺口:`/api/doctor`、`/api/cost`、`/api/audit`、sandbox audit、token/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 摘要和深链。
|
||||
|
||||
Reference in New Issue
Block a user