吸收数字员工诊断成本审计
This commit is contained in:
@@ -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 [];
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user