吸收数字员工诊断成本审计

This commit is contained in:
baiyanyun
2026-06-12 10:52:35 +08:00
parent 3a159e2f5a
commit 9011b91dba
9 changed files with 343 additions and 57 deletions

View File

@@ -2896,7 +2896,41 @@ function doctorSeverityForSync(sync: QimingclawSyncStatus | null): DiagResult['s
}
function buildCostSummary(snapshot: QimingclawSnapshot | null): CostSummary {
const requestCount = runtimeEvents(snapshot).length + runtimeRuns(snapshot).length + runtimeTasks(snapshot).length;
const samples = [
...runtimeEvents(snapshot).map((event) => costSampleFromPayload(asRecord(event.payload), event.occurredAt)),
...runtimeRuns(snapshot).map((run) => costSampleFromPayload(asRecord(run.payload), run.finishedAt ?? run.updatedAt ?? run.createdAt)),
].filter((sample): sample is TokenCostSample => Boolean(sample));
const requestCount = samples.length || runtimeEvents(snapshot).length + runtimeRuns(snapshot).length + runtimeTasks(snapshot).length;
if (samples.length > 0) {
const byModel: CostSummary['by_model'] = {};
let totalTokens = 0;
let totalCostUsd = 0;
let estimated = false;
let updatedAt = snapshot?.generatedAt ?? new Date().toISOString();
for (const sample of samples) {
const model = sample.model || 'unknown';
const current = byModel[model] ?? { model, cost_usd: 0, total_tokens: 0, request_count: 0 };
current.cost_usd += sample.estimated_cost_usd ?? 0;
current.total_tokens += sample.total_tokens ?? 0;
current.request_count += 1;
byModel[model] = current;
totalTokens += sample.total_tokens ?? 0;
totalCostUsd += sample.estimated_cost_usd ?? 0;
estimated = estimated || sample.estimated;
if (sample.updated_at && sample.updated_at > updatedAt) updatedAt = sample.updated_at;
}
return {
session_cost_usd: totalCostUsd,
daily_cost_usd: totalCostUsd,
monthly_cost_usd: totalCostUsd,
total_tokens: totalTokens,
request_count: samples.length,
by_model: byModel,
estimated,
source: 'qimingclaw-runtime-projection',
updated_at: updatedAt,
};
}
return {
session_cost_usd: 0,
daily_cost_usd: 0,
@@ -2917,6 +2951,37 @@ function buildCostSummary(snapshot: QimingclawSnapshot | null): CostSummary {
};
}
interface TokenCostSample {
model: string;
input_tokens: number | null;
output_tokens: number | null;
total_tokens: number | null;
estimated_cost_usd: number | null;
estimated: boolean;
updated_at: string | null;
}
function costSampleFromPayload(payload: Record<string, unknown> | null, updatedAt?: string | null): TokenCostSample | null {
if (!payload) return null;
const usage = asRecord(payload.usage) ?? asRecord(payload.token_usage) ?? asRecord(payload.tokenUsage) ?? payload;
const cost = asRecord(payload.cost) ?? payload;
const inputTokens = numberValue(usage.input_tokens ?? usage.inputTokens ?? usage.prompt_tokens ?? usage.promptTokens);
const outputTokens = numberValue(usage.output_tokens ?? usage.outputTokens ?? usage.completion_tokens ?? usage.completionTokens);
const totalTokens = numberValue(usage.total_tokens ?? usage.totalTokens) ?? ((inputTokens ?? 0) + (outputTokens ?? 0) || null);
const estimatedCostUsd = numberValue(cost.estimated_cost_usd ?? cost.estimatedCostUsd ?? cost.cost_usd ?? cost.costUsd ?? cost.usd);
const model = stringValue(payload.model ?? payload.model_name ?? payload.modelName ?? usage.model) || 'unknown';
if (inputTokens === null && outputTokens === null && totalTokens === null && estimatedCostUsd === null) return null;
return {
model,
input_tokens: inputTokens,
output_tokens: outputTokens,
total_tokens: totalTokens,
estimated_cost_usd: estimatedCostUsd,
estimated: typeof payload.estimated === 'boolean' ? payload.estimated : estimatedCostUsd === null,
updated_at: stringValue(payload.updated_at ?? payload.updatedAt) || updatedAt || null,
};
}
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'));
@@ -2964,6 +3029,9 @@ function buildDiagnosticSummary(snapshot: QimingclawSnapshot | null): DigitalEmp
},
cost: {
request_count: cost.request_count,
total_tokens: cost.total_tokens,
estimated_cost_usd: cost.session_cost_usd,
by_model_count: Object.keys(cost.by_model ?? {}).length,
estimated: Boolean(cost.estimated),
updated_at: cost.updated_at ?? null,
},
@@ -4169,28 +4237,86 @@ function durationMs(startedAt?: string | null, finishedAt?: string | null): numb
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 category = auditCategoryForRuntimeEvent(event.kind);
const diagnosticPayload = diagnosticAuditPayload(event.kind, payload);
const safePayload = diagnosticPayload ?? payload;
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',
level: auditLevelForRuntimeEvent(event, safePayload),
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,
...safePayload,
payload: safePayload,
};
}
function auditCategoryForRuntimeEvent(kind: string): string {
if (kind === 'sandbox_audit') return 'sandbox_audit';
if (kind === 'token_cost') return 'token_cost';
if (kind === 'tool_call_audit') return 'tool_call_audit';
if (kind.startsWith('governance_')) return 'governance';
if (kind === 'route_decision') return 'route';
if (kind === 'approval_decision') return 'approval';
return 'runtime';
}
function auditLevelForRuntimeEvent(event: QimingclawEventRecord, payload: Record<string, unknown>): 'info' | 'warn' | 'error' {
const status = stringValue(payload.status).toLowerCase();
const text = `${event.kind} ${event.message} ${status}`;
if (/fail|error|reject|denied|blocked|cancel/i.test(text)) return 'error';
if (event.kind === 'sandbox_audit' || event.kind === 'tool_call_audit') {
if (/warn|timeout|retry/i.test(text)) return 'warn';
}
return 'info';
}
function diagnosticAuditPayload(kind: string, payload: Record<string, unknown>): Record<string, unknown> | null {
if (kind === 'sandbox_audit') {
return {
session_id: stringValue(payload.session_id ?? payload.sessionId) || null,
operation: stringValue(payload.operation ?? payload.action) || null,
status: stringValue(payload.status) || null,
exit_code: numberValue(payload.exit_code ?? payload.exitCode),
duration_ms: numberValue(payload.duration_ms ?? payload.durationMs),
command_preview: stringValue(payload.command_preview ?? payload.commandPreview) || null,
reason_codes: uniqueStrings(arrayValue(payload.reason_codes ?? payload.reasonCodes).map(stringValue)),
};
}
if (kind === 'token_cost') {
const sample = costSampleFromPayload(payload, stringValue(payload.updated_at ?? payload.updatedAt) || null);
return {
session_id: stringValue(payload.session_id ?? payload.sessionId) || null,
model: sample?.model ?? (stringValue(payload.model) || null),
status: stringValue(payload.status) || null,
input_tokens: sample?.input_tokens ?? null,
output_tokens: sample?.output_tokens ?? null,
total_tokens: sample?.total_tokens ?? null,
estimated_cost_usd: sample?.estimated_cost_usd ?? null,
estimated: sample?.estimated ?? true,
};
}
if (kind === 'tool_call_audit') {
return {
session_id: stringValue(payload.session_id ?? payload.sessionId) || null,
server_id: stringValue(payload.server_id ?? payload.serverId) || null,
tool_name: stringValue(payload.tool_name ?? payload.toolName) || null,
operation: stringValue(payload.operation ?? payload.action) || null,
status: stringValue(payload.status) || null,
exit_code: numberValue(payload.exit_code ?? payload.exitCode),
duration_ms: numberValue(payload.duration_ms ?? payload.durationMs),
input_length: numberValue(payload.input_length ?? payload.inputLength),
reason_codes: uniqueStrings(arrayValue(payload.reason_codes ?? payload.reasonCodes).map(stringValue)),
};
}
return null;
}
function auditEventsFromArtifact(artifact: ArtifactEntry): Array<Record<string, unknown>> {
const deliveryStatus = stringValue(artifact.delivery_status);
if (!deliveryStatus) return [];

View File

@@ -260,6 +260,17 @@ function badgeClass(tone?: string): string {
return 'de-badge-info';
}
function formatCompactNumber(value?: number | null): string {
const numeric = typeof value === 'number' && Number.isFinite(value) ? value : 0;
return new Intl.NumberFormat('zh-CN', { notation: 'compact', maximumFractionDigits: 1 }).format(numeric);
}
function formatEstimatedCostUsd(value?: number | null): string {
const numeric = typeof value === 'number' && Number.isFinite(value) ? value : 0;
if (numeric > 0 && numeric < 0.01) return `$${numeric.toFixed(4)}`;
return `$${numeric.toFixed(2)}`;
}
function approvalRiskTone(value?: string | null): string {
const normalized = compactText(value).toLowerCase();
if (['high', 'critical', 'risk', 'marked'].includes(normalized)) return 'danger';
@@ -2478,6 +2489,8 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
<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>
<span><em>Token</em><strong>{formatCompactNumber(diagnosticSummary.cost.total_tokens)}</strong></span>
<span><em></em><strong>{formatEstimatedCostUsd(diagnosticSummary.cost.estimated_cost_usd)}</strong></span>
<small>{latestDiagnosticIssue ? `${latestDiagnosticIssue.category} · ${latestDiagnosticIssue.message}` : '诊断、审计和成本投影正常'}</small>
</div>
)}

View File

@@ -1122,6 +1122,9 @@ export interface DigitalEmployeeDiagnosticSummary {
};
cost: {
request_count: number;
total_tokens: number;
estimated_cost_usd: number;
by_model_count: number;
estimated: boolean;
updated_at?: string | null;
};