吸收数字员工诊断成本审计
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;
|
||||
};
|
||||
|
||||
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-DIYwF-B5.js"></script>
|
||||
<script type="module" crossorigin src="./assets/index-v4rEdqGS.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-CEcWFgGW.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -174,18 +174,41 @@ function checkDigitalDiagnosticAuditProjection() {
|
||||
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 syncServicePath = path.join(packageRoot, "src", "main", "services", "digitalEmployee", "syncService.ts");
|
||||
const adapter = fs.readFileSync(adapterPath, "utf8");
|
||||
const types = fs.readFileSync(typesPath, "utf8");
|
||||
const page = fs.readFileSync(pagePath, "utf8");
|
||||
const syncService = fs.readFileSync(syncServicePath, "utf8");
|
||||
const requiredSnippets = [
|
||||
[syncService, "sandboxAuditBusinessView", "sandbox audit sync business view"],
|
||||
[syncService, "tokenCostBusinessView", "token/cost sync business view"],
|
||||
[syncService, "toolCallAuditBusinessView", "tool call sync business view"],
|
||||
[syncService, "sandbox_audit", "sandbox audit business category"],
|
||||
[syncService, "token_cost", "token cost business category"],
|
||||
[syncService, "tool_call_audit", "tool call business category"],
|
||||
[syncService, "estimated_cost_usd", "safe estimated cost field"],
|
||||
[syncService, "total_tokens", "safe total token field"],
|
||||
[syncService, "input_length", "sensitive input length redaction"],
|
||||
[adapter, "'audits'", "audit business view kind"],
|
||||
[adapter, "buildDiagnosticSummary", "diagnostic summary builder"],
|
||||
[adapter, "buildCostSummary", "cost summary builder"],
|
||||
[adapter, "costSampleFromPayload", "runtime token/cost aggregation"],
|
||||
[adapter, "diagnosticAuditPayload", "diagnostic audit payload redaction"],
|
||||
[adapter, "sandbox_audit", "sandbox audit row category"],
|
||||
[adapter, "token_cost", "token cost row category"],
|
||||
[adapter, "tool_call_audit", "tool call row category"],
|
||||
[adapter, "estimated_cost_usd", "audit estimated cost field"],
|
||||
[adapter, "total_tokens", "audit total token field"],
|
||||
[adapter, "filterAuditRows", "audit filters"],
|
||||
[adapter, "diagnosticNotifications", "diagnostic notifications"],
|
||||
[adapter, "auditNotifications", "audit notifications"],
|
||||
[types, "DigitalEmployeeDiagnosticSummary", "diagnostic summary type"],
|
||||
[types, "diagnostic_summary?: DigitalEmployeeDiagnosticSummary", "workday diagnostic field"],
|
||||
[types, "estimated_cost_usd", "diagnostic summary estimated cost type"],
|
||||
[types, "total_tokens", "diagnostic summary token type"],
|
||||
[page, "de-diagnostic-summary-strip", "dashboard diagnostic strip"],
|
||||
[page, "Token", "dashboard Token metric"],
|
||||
[page, "估算成本", "dashboard estimated cost metric"],
|
||||
];
|
||||
const missing = requiredSnippets
|
||||
.filter(([content, snippet]) => !content.includes(snippet))
|
||||
|
||||
@@ -1279,6 +1279,39 @@ describe("digital employee sync service", () => {
|
||||
decision: "rejected",
|
||||
reason_codes: ["skill_disabled"],
|
||||
});
|
||||
insertEventEntity("event-sandbox-audit", "sandbox_audit", {
|
||||
session_id: "sandbox-session-1",
|
||||
operation: "execute",
|
||||
status: "failed",
|
||||
exit_code: 126,
|
||||
duration_ms: 2400,
|
||||
command: "curl https://example.com --header Authorization: Bearer secret-token",
|
||||
command_preview: "curl https://example.com",
|
||||
reason_codes: ["permission_denied"],
|
||||
authorization: "Bearer secret-token",
|
||||
});
|
||||
insertEventEntity("event-token-cost", "token_cost", {
|
||||
session_id: "session-cost-1",
|
||||
model: "gpt-4.1-mini",
|
||||
input_tokens: 120,
|
||||
output_tokens: 80,
|
||||
total_tokens: 200,
|
||||
estimated_cost_usd: 0.0125,
|
||||
prompt: "完整客户资料和敏感输入不应同步",
|
||||
raw_input: { token: "raw-secret" },
|
||||
token: "secret-token",
|
||||
});
|
||||
insertEventEntity("event-tool-audit", "tool_call_audit", {
|
||||
session_id: "session-tool-1",
|
||||
server_id: "crm-server",
|
||||
tool_name: "crm.search",
|
||||
operation: "tool_call",
|
||||
status: "allowed",
|
||||
duration_ms: 640,
|
||||
input_length: 36,
|
||||
raw_input: { customer_phone: "13800000000" },
|
||||
authorization: "Bearer tool-secret",
|
||||
});
|
||||
insertEventEntity("event-dispatch", "plan_step_dispatch_failed", {
|
||||
step_id: "step-1",
|
||||
plan_id: "plan-1",
|
||||
@@ -1538,6 +1571,9 @@ describe("digital employee sync service", () => {
|
||||
{ entity_type: "event", entity_id: "event-approval-decision" },
|
||||
{ entity_type: "event", entity_id: "event-artifact" },
|
||||
{ entity_type: "event", entity_id: "event-skill" },
|
||||
{ entity_type: "event", entity_id: "event-sandbox-audit" },
|
||||
{ entity_type: "event", entity_id: "event-token-cost" },
|
||||
{ entity_type: "event", entity_id: "event-tool-audit" },
|
||||
{ entity_type: "event", entity_id: "event-dispatch" },
|
||||
{ entity_type: "event", entity_id: "event-lease" },
|
||||
{ entity_type: "event", entity_id: "event-plan-step-ready" },
|
||||
@@ -1635,6 +1671,42 @@ describe("digital employee sync service", () => {
|
||||
decision: "rejected",
|
||||
reason_codes: ["skill_disabled"],
|
||||
});
|
||||
expect(businessView("event-sandbox-audit")).toMatchObject({
|
||||
category: "sandbox_audit",
|
||||
session_id: "sandbox-session-1",
|
||||
operation: "execute",
|
||||
status: "failed",
|
||||
exit_code: 126,
|
||||
duration_ms: 2400,
|
||||
command_preview: "curl https://example.com",
|
||||
reason_codes: ["permission_denied"],
|
||||
});
|
||||
expect(JSON.stringify(businessView("event-sandbox-audit"))).not.toContain("secret-token");
|
||||
expect(JSON.stringify(businessView("event-sandbox-audit"))).not.toContain("Authorization");
|
||||
expect(businessView("event-token-cost")).toMatchObject({
|
||||
category: "token_cost",
|
||||
session_id: "session-cost-1",
|
||||
model: "gpt-4.1-mini",
|
||||
input_tokens: 120,
|
||||
output_tokens: 80,
|
||||
total_tokens: 200,
|
||||
estimated_cost_usd: 0.0125,
|
||||
estimated: false,
|
||||
});
|
||||
expect(JSON.stringify(businessView("event-token-cost"))).not.toContain("完整客户资料");
|
||||
expect(JSON.stringify(businessView("event-token-cost"))).not.toContain("raw-secret");
|
||||
expect(businessView("event-tool-audit")).toMatchObject({
|
||||
category: "tool_call_audit",
|
||||
session_id: "session-tool-1",
|
||||
server_id: "crm-server",
|
||||
tool_name: "crm.search",
|
||||
operation: "tool_call",
|
||||
status: "allowed",
|
||||
duration_ms: 640,
|
||||
input_length: 36,
|
||||
});
|
||||
expect(JSON.stringify(businessView("event-tool-audit"))).not.toContain("13800000000");
|
||||
expect(JSON.stringify(businessView("event-tool-audit"))).not.toContain("tool-secret");
|
||||
expect(businessView("event-dispatch")).toMatchObject({
|
||||
category: "dispatch",
|
||||
step_id: "step-1",
|
||||
|
||||
@@ -1737,6 +1737,9 @@ function eventSpecificBusinessView(
|
||||
if (isPlanStepLeaseEvent(kind)) return planStepLeaseBusinessView(payload);
|
||||
if (isPlanStepDispatchPolicyEvent(kind)) return planStepDispatchPolicyBusinessView(payload);
|
||||
if (kind.startsWith("governance_")) return governanceCommandBusinessView(kind, payload);
|
||||
if (kind === "sandbox_audit") return sandboxAuditBusinessView(payload);
|
||||
if (kind === "token_cost") return tokenCostBusinessView(payload);
|
||||
if (kind === "tool_call_audit") return toolCallAuditBusinessView(payload);
|
||||
if (isNotificationEvent(kind)) return notificationBusinessView(payload);
|
||||
return {};
|
||||
}
|
||||
@@ -2020,6 +2023,49 @@ function governanceCommandBusinessView(kind: string, payload: Record<string, unk
|
||||
return view;
|
||||
}
|
||||
|
||||
function sandboxAuditBusinessView(payload: Record<string, unknown>): Record<string, unknown> {
|
||||
return {
|
||||
session_id: stringField(payload.session_id ?? payload.sessionId) || null,
|
||||
operation: stringField(payload.operation ?? payload.action) || null,
|
||||
status: stringField(payload.status) || null,
|
||||
exit_code: numberField(payload.exit_code ?? payload.exitCode),
|
||||
duration_ms: numberField(payload.duration_ms ?? payload.durationMs),
|
||||
command_preview: compactPreview(stringField(payload.command_preview ?? payload.commandPreview) || ""),
|
||||
reason_codes: stringArrayField(payload.reason_codes ?? payload.reasonCodes),
|
||||
};
|
||||
}
|
||||
|
||||
function tokenCostBusinessView(payload: Record<string, unknown>): Record<string, unknown> {
|
||||
const inputTokens = numberField(payload.input_tokens ?? payload.inputTokens);
|
||||
const outputTokens = numberField(payload.output_tokens ?? payload.outputTokens);
|
||||
const totalTokens = numberField(payload.total_tokens ?? payload.totalTokens) ?? ((inputTokens ?? 0) + (outputTokens ?? 0) || null);
|
||||
const estimatedCostUsd = numberField(payload.estimated_cost_usd ?? payload.estimatedCostUsd ?? payload.cost_usd ?? payload.costUsd);
|
||||
return {
|
||||
session_id: stringField(payload.session_id ?? payload.sessionId) || null,
|
||||
model: stringField(payload.model) || null,
|
||||
status: stringField(payload.status) || null,
|
||||
input_tokens: inputTokens,
|
||||
output_tokens: outputTokens,
|
||||
total_tokens: totalTokens,
|
||||
estimated_cost_usd: estimatedCostUsd,
|
||||
estimated: typeof payload.estimated === "boolean" ? payload.estimated : estimatedCostUsd === null,
|
||||
};
|
||||
}
|
||||
|
||||
function toolCallAuditBusinessView(payload: Record<string, unknown>): Record<string, unknown> {
|
||||
return {
|
||||
session_id: stringField(payload.session_id ?? payload.sessionId) || null,
|
||||
server_id: stringField(payload.server_id ?? payload.serverId) || null,
|
||||
tool_name: stringField(payload.tool_name ?? payload.toolName) || null,
|
||||
operation: stringField(payload.operation ?? payload.action) || null,
|
||||
status: stringField(payload.status) || null,
|
||||
exit_code: numberField(payload.exit_code ?? payload.exitCode),
|
||||
duration_ms: numberField(payload.duration_ms ?? payload.durationMs),
|
||||
input_length: numberField(payload.input_length ?? payload.inputLength),
|
||||
reason_codes: stringArrayField(payload.reason_codes ?? payload.reasonCodes),
|
||||
};
|
||||
}
|
||||
|
||||
function notificationBusinessView(payload: Record<string, unknown>): Record<string, unknown> {
|
||||
const metadata = objectRecord(payload.metadata) ?? {};
|
||||
const state = objectRecord(payload.state) ?? {};
|
||||
@@ -2063,6 +2109,9 @@ function eventBusinessCategory(kind: string): string {
|
||||
if (isPlanStepDispatchPolicyEvent(kind)) return "dispatch";
|
||||
if (isNotificationEvent(kind)) return "notification";
|
||||
if (kind.startsWith("governance_")) return "governance";
|
||||
if (kind === "sandbox_audit") return "sandbox_audit";
|
||||
if (kind === "token_cost") return "token_cost";
|
||||
if (kind === "tool_call_audit") return "tool_call_audit";
|
||||
return "runtime";
|
||||
}
|
||||
|
||||
|
||||
@@ -604,9 +604,9 @@ 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 生成只读诊断、估算成本和审计投影;数字员工首页已展示诊断/审计/成本摘要,`/api/digital-employee/audits` 已提供本地审计业务视图,诊断与审计风险通知联动已开始闭环。
|
||||
- 缺口:sandbox audit 文件、真实 token/cost 采集、CLI/tool 调用审计、诊断修复动作和管理端正式审计视图尚未统一进入数字员工视图和 outbox。
|
||||
- 建议:下一轮围绕 sandbox audit 与 token/cost 采集,把只读投影升级为可追溯的真实审计与成本统计。
|
||||
- 已吸收:同步失败诊断较完整,部分 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 调用事件已开始通过 `digital_events` 与 outbox `business_view` 提炼为 `sandbox_audit`、`token_cost`、`tool_call_audit` 分类,并只同步 session、tool、server、operation、status、exit code、duration、model、token 计数和估算成本等安全字段;数字员工首页已展示诊断/审计/Token/估算成本摘要,`/api/digital-employee/audits` 已提供本地审计业务视图,诊断与审计风险通知联动已开始闭环。
|
||||
- 缺口:真实计费账单、独立 sandbox audit 文件归档、诊断修复动作和管理端正式审计视图尚未统一吸收。
|
||||
- 建议:下一轮围绕管理端正式审计视图和诊断修复动作,把本地可追溯审计继续推进到可运营处置。
|
||||
|
||||
12. **管理端业务查询模型**
|
||||
- 已吸收:管理端已有通用 sync record 接收、查询、payload 摘要和深链;embedded adapter 已拆出 `/api/digital-employee/plans`、`/tasks`、`/runs`、`/events`、`/artifacts`、`/approvals` 与 `/plans/:planId` 只读业务查询接口,从 qimingclaw runtime、artifact、approval 和 canonical event 投影生成统一分页视图;客户端 outbox 的 event `business_view` 已开始按 route decision、governance command、approval action、artifact lifecycle/access、skill call audit 和 ready PlanStep dispatch 细分常用字段,本地业务视图投影已开始闭环。
|
||||
|
||||
Reference in New Issue
Block a user