吸收数字员工诊断成本审计
This commit is contained in:
@@ -2896,7 +2896,41 @@ function doctorSeverityForSync(sync: QimingclawSyncStatus | null): DiagResult['s
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildCostSummary(snapshot: QimingclawSnapshot | null): CostSummary {
|
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 {
|
return {
|
||||||
session_cost_usd: 0,
|
session_cost_usd: 0,
|
||||||
daily_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>> {
|
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 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 category = stringValue(searchParams.get('category')) || stringValue(searchParams.get('kind'));
|
||||||
@@ -2964,6 +3029,9 @@ function buildDiagnosticSummary(snapshot: QimingclawSnapshot | null): DigitalEmp
|
|||||||
},
|
},
|
||||||
cost: {
|
cost: {
|
||||||
request_count: cost.request_count,
|
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),
|
estimated: Boolean(cost.estimated),
|
||||||
updated_at: cost.updated_at ?? null,
|
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> {
|
function auditEventFromRuntimeEvent(event: QimingclawEventRecord): Record<string, unknown> {
|
||||||
const payload = asRecord(event.payload) ?? {};
|
const payload = asRecord(event.payload) ?? {};
|
||||||
const category = event.kind.startsWith('governance_')
|
const category = auditCategoryForRuntimeEvent(event.kind);
|
||||||
? 'governance'
|
const diagnosticPayload = diagnosticAuditPayload(event.kind, payload);
|
||||||
: event.kind === 'route_decision'
|
const safePayload = diagnosticPayload ?? payload;
|
||||||
? 'route'
|
|
||||||
: event.kind === 'approval_decision'
|
|
||||||
? 'approval'
|
|
||||||
: 'runtime';
|
|
||||||
const entityType = event.runId ? 'run' : event.taskId ? 'task' : event.planId ? 'plan' : 'event';
|
const entityType = event.runId ? 'run' : event.taskId ? 'task' : event.planId ? 'plan' : 'event';
|
||||||
return {
|
return {
|
||||||
audit_id: event.id,
|
audit_id: event.id,
|
||||||
category,
|
category,
|
||||||
action: event.kind,
|
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,
|
message: event.message || event.kind,
|
||||||
actor: stringValue(payload.actor) || stringValue(payload.source) || 'qimingclaw-runtime',
|
actor: stringValue(payload.actor) || stringValue(payload.source) || 'qimingclaw-runtime',
|
||||||
entity_type: entityType,
|
entity_type: entityType,
|
||||||
entity_id: event.runId ?? event.taskId ?? event.planId ?? event.id,
|
entity_id: event.runId ?? event.taskId ?? event.planId ?? event.id,
|
||||||
occurred_at: event.occurredAt,
|
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>> {
|
function auditEventsFromArtifact(artifact: ArtifactEntry): Array<Record<string, unknown>> {
|
||||||
const deliveryStatus = stringValue(artifact.delivery_status);
|
const deliveryStatus = stringValue(artifact.delivery_status);
|
||||||
if (!deliveryStatus) return [];
|
if (!deliveryStatus) return [];
|
||||||
|
|||||||
@@ -260,6 +260,17 @@ function badgeClass(tone?: string): string {
|
|||||||
return 'de-badge-info';
|
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 {
|
function approvalRiskTone(value?: string | null): string {
|
||||||
const normalized = compactText(value).toLowerCase();
|
const normalized = compactText(value).toLowerCase();
|
||||||
if (['high', 'critical', 'risk', 'marked'].includes(normalized)) return 'danger';
|
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={diagnosticIssueCount > 0 ? 'is-warning' : ''}><em>诊断异常</em><strong>{diagnosticIssueCount}</strong></span>
|
||||||
<span className={auditRiskCount > 0 ? 'is-warning' : ''}><em>审计风险</em><strong>{auditRiskCount}</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>估算请求</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>
|
<small>{latestDiagnosticIssue ? `${latestDiagnosticIssue.category} · ${latestDiagnosticIssue.message}` : '诊断、审计和成本投影正常'}</small>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1122,6 +1122,9 @@ export interface DigitalEmployeeDiagnosticSummary {
|
|||||||
};
|
};
|
||||||
cost: {
|
cost: {
|
||||||
request_count: number;
|
request_count: number;
|
||||||
|
total_tokens: number;
|
||||||
|
estimated_cost_usd: number;
|
||||||
|
by_model_count: number;
|
||||||
estimated: boolean;
|
estimated: boolean;
|
||||||
updated_at?: string | null;
|
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);
|
console.warn("[qimingclaw] digital employee auth bootstrap skipped", error);
|
||||||
}
|
}
|
||||||
</script>
|
</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">
|
<link rel="stylesheet" crossorigin href="./assets/index-CEcWFgGW.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -174,18 +174,41 @@ function checkDigitalDiagnosticAuditProjection() {
|
|||||||
const adapterPath = path.join(packageRoot, "embedded", "sgrobot-digital", "src", "lib", "qimingclawAdapter.ts");
|
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 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 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 adapter = fs.readFileSync(adapterPath, "utf8");
|
||||||
const types = fs.readFileSync(typesPath, "utf8");
|
const types = fs.readFileSync(typesPath, "utf8");
|
||||||
const page = fs.readFileSync(pagePath, "utf8");
|
const page = fs.readFileSync(pagePath, "utf8");
|
||||||
|
const syncService = fs.readFileSync(syncServicePath, "utf8");
|
||||||
const requiredSnippets = [
|
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, "'audits'", "audit business view kind"],
|
||||||
[adapter, "buildDiagnosticSummary", "diagnostic summary builder"],
|
[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, "filterAuditRows", "audit filters"],
|
||||||
[adapter, "diagnosticNotifications", "diagnostic notifications"],
|
[adapter, "diagnosticNotifications", "diagnostic notifications"],
|
||||||
[adapter, "auditNotifications", "audit notifications"],
|
[adapter, "auditNotifications", "audit notifications"],
|
||||||
[types, "DigitalEmployeeDiagnosticSummary", "diagnostic summary type"],
|
[types, "DigitalEmployeeDiagnosticSummary", "diagnostic summary type"],
|
||||||
[types, "diagnostic_summary?: DigitalEmployeeDiagnosticSummary", "workday diagnostic field"],
|
[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, "de-diagnostic-summary-strip", "dashboard diagnostic strip"],
|
||||||
|
[page, "Token", "dashboard Token metric"],
|
||||||
|
[page, "估算成本", "dashboard estimated cost metric"],
|
||||||
];
|
];
|
||||||
const missing = requiredSnippets
|
const missing = requiredSnippets
|
||||||
.filter(([content, snippet]) => !content.includes(snippet))
|
.filter(([content, snippet]) => !content.includes(snippet))
|
||||||
|
|||||||
@@ -1279,6 +1279,39 @@ describe("digital employee sync service", () => {
|
|||||||
decision: "rejected",
|
decision: "rejected",
|
||||||
reason_codes: ["skill_disabled"],
|
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", {
|
insertEventEntity("event-dispatch", "plan_step_dispatch_failed", {
|
||||||
step_id: "step-1",
|
step_id: "step-1",
|
||||||
plan_id: "plan-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-approval-decision" },
|
||||||
{ entity_type: "event", entity_id: "event-artifact" },
|
{ entity_type: "event", entity_id: "event-artifact" },
|
||||||
{ entity_type: "event", entity_id: "event-skill" },
|
{ 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-dispatch" },
|
||||||
{ entity_type: "event", entity_id: "event-lease" },
|
{ entity_type: "event", entity_id: "event-lease" },
|
||||||
{ entity_type: "event", entity_id: "event-plan-step-ready" },
|
{ entity_type: "event", entity_id: "event-plan-step-ready" },
|
||||||
@@ -1635,6 +1671,42 @@ describe("digital employee sync service", () => {
|
|||||||
decision: "rejected",
|
decision: "rejected",
|
||||||
reason_codes: ["skill_disabled"],
|
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({
|
expect(businessView("event-dispatch")).toMatchObject({
|
||||||
category: "dispatch",
|
category: "dispatch",
|
||||||
step_id: "step-1",
|
step_id: "step-1",
|
||||||
|
|||||||
@@ -1737,6 +1737,9 @@ function eventSpecificBusinessView(
|
|||||||
if (isPlanStepLeaseEvent(kind)) return planStepLeaseBusinessView(payload);
|
if (isPlanStepLeaseEvent(kind)) return planStepLeaseBusinessView(payload);
|
||||||
if (isPlanStepDispatchPolicyEvent(kind)) return planStepDispatchPolicyBusinessView(payload);
|
if (isPlanStepDispatchPolicyEvent(kind)) return planStepDispatchPolicyBusinessView(payload);
|
||||||
if (kind.startsWith("governance_")) return governanceCommandBusinessView(kind, 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);
|
if (isNotificationEvent(kind)) return notificationBusinessView(payload);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
@@ -2020,6 +2023,49 @@ function governanceCommandBusinessView(kind: string, payload: Record<string, unk
|
|||||||
return view;
|
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> {
|
function notificationBusinessView(payload: Record<string, unknown>): Record<string, unknown> {
|
||||||
const metadata = objectRecord(payload.metadata) ?? {};
|
const metadata = objectRecord(payload.metadata) ?? {};
|
||||||
const state = objectRecord(payload.state) ?? {};
|
const state = objectRecord(payload.state) ?? {};
|
||||||
@@ -2063,6 +2109,9 @@ function eventBusinessCategory(kind: string): string {
|
|||||||
if (isPlanStepDispatchPolicyEvent(kind)) return "dispatch";
|
if (isPlanStepDispatchPolicyEvent(kind)) return "dispatch";
|
||||||
if (isNotificationEvent(kind)) return "notification";
|
if (isNotificationEvent(kind)) return "notification";
|
||||||
if (kind.startsWith("governance_")) return "governance";
|
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";
|
return "runtime";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -604,9 +604,9 @@ GET /api/digital-employee/approvals
|
|||||||
- 建议:下一轮围绕管理端正式路由 UI 和跨设备冲突可视化,把入口路由从客户端可运营干预推进到管理端运营台。
|
- 建议:下一轮围绕管理端正式路由 UI 和跨设备冲突可视化,把入口路由从客户端可运营干预推进到管理端运营台。
|
||||||
|
|
||||||
11. **诊断 / 成本 / 审计视图**
|
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 / 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 文件、真实 token/cost 采集、CLI/tool 调用审计、诊断修复动作和管理端正式审计视图尚未统一进入数字员工视图和 outbox。
|
- 缺口:真实计费账单、独立 sandbox audit 文件归档、诊断修复动作和管理端正式审计视图尚未统一吸收。
|
||||||
- 建议:下一轮围绕 sandbox audit 与 token/cost 采集,把只读投影升级为可追溯的真实审计与成本统计。
|
- 建议:下一轮围绕管理端正式审计视图和诊断修复动作,把本地可追溯审计继续推进到可运营处置。
|
||||||
|
|
||||||
12. **管理端业务查询模型**
|
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 细分常用字段,本地业务视图投影已开始闭环。
|
- 已吸收:管理端已有通用 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