吸收数字员工技能调用审计

This commit is contained in:
baiyanyun
2026-06-10 09:24:02 +08:00
parent 91540dec0c
commit cc36d8dcaf
15 changed files with 894 additions and 73 deletions

View File

@@ -186,6 +186,32 @@ export interface DigitalEmployeeEventRecord {
createdAt: string;
}
export type DigitalEmployeeSkillCallDecision = "allowed" | "rejected" | "observed";
export interface DigitalEmployeeSkillCallAuditInput {
callId?: string | null;
source?: string | null;
serverId?: string | null;
toolName?: string | null;
skillId?: string | null;
decision: DigitalEmployeeSkillCallDecision;
reasonCodes?: string[] | null;
policySnapshot?: Record<string, unknown> | null;
sessionId?: string | null;
taskId?: string | null;
runId?: string | null;
status?: string | null;
occurredAt?: string | null;
message?: string | null;
}
export interface DigitalEmployeeSkillCallAuditRecord {
eventId: string;
kind: "skill_call_allowed" | "skill_call_rejected" | "skill_call_observed";
decision: DigitalEmployeeSkillCallDecision;
payload: Record<string, unknown>;
}
export interface DigitalEmployeeRouteDecisionRecord {
id: string;
route_kind: string;
@@ -943,6 +969,80 @@ export function listDigitalEmployeeRouteDecisions(options: { limit?: number } =
.filter((decision): decision is DigitalEmployeeRouteDecisionRecord => Boolean(decision));
}
export function recordDigitalEmployeeSkillCallAudit(
input: DigitalEmployeeSkillCallAuditInput,
): DigitalEmployeeSkillCallAuditRecord | null {
const db = getDigitalEmployeeDb();
if (!db) return null;
const now = input.occurredAt || new Date().toISOString();
const decision: DigitalEmployeeSkillCallDecision = ["allowed", "rejected", "observed"].includes(input.decision)
? input.decision
: "observed";
const kind = `skill_call_${decision}` as DigitalEmployeeSkillCallAuditRecord["kind"];
const callId = stringValue(input.callId)
|| stringValue(input.runId)
|| stringValue(input.sessionId)
|| `${Date.now()}`;
const eventId = `skill-call:${decision}:${safeId(callId)}:${safeId(now)}`;
const source = stringValue(input.source) || "qimingclaw-acp";
const toolName = stringValue(input.toolName) || null;
const serverId = stringValue(input.serverId) || null;
const skillId = stringValue(input.skillId) || null;
const payload: Record<string, unknown> = {
call_id: callId,
source,
server_id: serverId,
tool_name: toolName,
skill_id: skillId,
decision,
reason_codes: normalizeSkillIds(input.reasonCodes ?? []),
policy_snapshot: sanitizeSkillPolicySnapshot(objectRecord(input.policySnapshot) ?? {}),
session_id: stringValue(input.sessionId) || null,
task_id: stringValue(input.taskId) || null,
run_id: stringValue(input.runId) || null,
status: stringValue(input.status) || decision,
};
insertEvent(
{
event_id: eventId,
kind,
occurred_at: now,
message: input.message || skillCallAuditMessage(decision, toolName, skillId),
plan_id: null,
task_id: stringValue(input.taskId) || null,
},
stringValue(input.runId) || null,
payload,
);
return { eventId, kind, decision, payload };
}
function sanitizeSkillPolicySnapshot(snapshot: Record<string, unknown>): Record<string, unknown> {
const allowedKeys = new Set([
"skill_enabled",
"matched_skill_id",
"server_enabled",
"global_allow_tools",
"global_deny_tools",
"server_allow_tools",
"server_deny_tools",
]);
return Object.fromEntries(
Object.entries(snapshot).filter(([key]) => allowedKeys.has(key)),
);
}
function skillCallAuditMessage(
decision: DigitalEmployeeSkillCallDecision,
toolName: string | null,
skillId: string | null,
): string {
const target = toolName || skillId || "unknown";
if (decision === "rejected") return `技能调用已拒绝:${target}`;
if (decision === "allowed") return `技能调用已允许:${target}`;
return `技能调用已观测:${target}`;
}
function routeDecisionFromEventPayload(payload: unknown): DigitalEmployeeRouteDecisionRecord | null {
const record = objectRecord(payload);
const decision = objectRecord(record?.routeDecision ?? record?.route_decision ?? payload);