|
|
|
|
@@ -456,6 +456,7 @@ export interface DigitalEmployeeRouteDecisionPolicy {
|
|
|
|
|
export type DigitalEmployeeRouteDecisionPolicyDecision = "allowed" | "approval_required" | "blocked";
|
|
|
|
|
|
|
|
|
|
export interface DigitalEmployeeRouteDecisionPolicyInput {
|
|
|
|
|
routeDecisionId?: string | null;
|
|
|
|
|
routeKind?: string | null;
|
|
|
|
|
intentKind?: string | null;
|
|
|
|
|
reasonCodes?: string[] | null;
|
|
|
|
|
@@ -477,6 +478,7 @@ export interface DigitalEmployeeRouteDecisionPolicyResult {
|
|
|
|
|
policy_result: "accepted" | "approval_required" | "blocked";
|
|
|
|
|
route_kind: string;
|
|
|
|
|
intent_kind: string;
|
|
|
|
|
route_decision_id?: string | null;
|
|
|
|
|
reason_codes: string[];
|
|
|
|
|
matched_intent?: string | null;
|
|
|
|
|
blocked_reason?: string | null;
|
|
|
|
|
@@ -562,6 +564,55 @@ export interface DigitalEmployeeRouteDecisionInput {
|
|
|
|
|
recordedAt?: string | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface DigitalEmployeeRouteInterventionRecord {
|
|
|
|
|
route_decision_id: string;
|
|
|
|
|
approval_id: string | null;
|
|
|
|
|
route_kind: string;
|
|
|
|
|
intent_kind: string;
|
|
|
|
|
policy_result: string | null;
|
|
|
|
|
intervention_status: "pending" | "blocked" | "approved" | "rejected" | "executed" | "failed" | "resolved";
|
|
|
|
|
decision: string | null;
|
|
|
|
|
created_command_id: string | null;
|
|
|
|
|
blocked_reason: string | null;
|
|
|
|
|
matched_intent: string | null;
|
|
|
|
|
policy_source: string | null;
|
|
|
|
|
reason_codes: string[];
|
|
|
|
|
candidate_skills: string[];
|
|
|
|
|
context_refs: Record<string, unknown>;
|
|
|
|
|
entrypoint: string | null;
|
|
|
|
|
actor: string | null;
|
|
|
|
|
correlation_id: string | null;
|
|
|
|
|
approval_status: string | null;
|
|
|
|
|
recorded_at: string;
|
|
|
|
|
resolved_at: string | null;
|
|
|
|
|
resolution_event_id?: string | null;
|
|
|
|
|
resolution_duplicate?: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface DigitalEmployeeRouteInterventionResolutionInput {
|
|
|
|
|
routeDecisionId: string;
|
|
|
|
|
approvalId?: string | null;
|
|
|
|
|
decision: string;
|
|
|
|
|
createdCommandId?: string | null;
|
|
|
|
|
commandAccepted?: boolean | null;
|
|
|
|
|
commandError?: string | null;
|
|
|
|
|
blockedReason?: string | null;
|
|
|
|
|
policyResult?: string | null;
|
|
|
|
|
policySource?: string | null;
|
|
|
|
|
matchedIntent?: string | null;
|
|
|
|
|
routeKind?: string | null;
|
|
|
|
|
intentKind?: string | null;
|
|
|
|
|
reasonCodes?: string[] | null;
|
|
|
|
|
candidateSkills?: string[] | null;
|
|
|
|
|
contextRefs?: Record<string, unknown> | null;
|
|
|
|
|
entrypoint?: string | null;
|
|
|
|
|
actor?: string | null;
|
|
|
|
|
correlationId?: string | null;
|
|
|
|
|
message?: string | null;
|
|
|
|
|
payload?: Record<string, unknown> | null;
|
|
|
|
|
occurredAt?: string | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface DigitalEmployeeArtifactRecord {
|
|
|
|
|
id: string;
|
|
|
|
|
remoteId?: string | null;
|
|
|
|
|
@@ -1743,12 +1794,134 @@ export function listDigitalEmployeeRouteDecisions(options: { limit?: number } =
|
|
|
|
|
.filter((decision): decision is DigitalEmployeeRouteDecisionRecord => Boolean(decision));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function listDigitalEmployeeRouteInterventions(options: { limit?: number } = {}): DigitalEmployeeRouteInterventionRecord[] {
|
|
|
|
|
const db = getDigitalEmployeeDb();
|
|
|
|
|
if (!db) return [];
|
|
|
|
|
const safeLimit = Math.max(1, Math.min(Math.floor(options.limit ?? 80), 300));
|
|
|
|
|
const eventRows = db.prepare(`
|
|
|
|
|
SELECT id, kind, payload, occurred_at, created_at
|
|
|
|
|
FROM digital_events
|
|
|
|
|
WHERE kind IN ('route_decision', 'route_decision_blocked', 'route_decision_approval_required', 'route_decision_intervention_resolved')
|
|
|
|
|
ORDER BY occurred_at DESC, created_at DESC
|
|
|
|
|
LIMIT ?
|
|
|
|
|
`).all(Math.max(safeLimit * 4, 80)) as Array<Record<string, unknown>>;
|
|
|
|
|
const approvalRows = db.prepare(`
|
|
|
|
|
SELECT id, status, payload, updated_at, created_at
|
|
|
|
|
FROM digital_approvals
|
|
|
|
|
ORDER BY updated_at DESC, created_at DESC
|
|
|
|
|
LIMIT ?
|
|
|
|
|
`).all(Math.max(safeLimit * 4, 80)) as Array<Record<string, unknown>>;
|
|
|
|
|
const approvalsById = new Map<string, { status: string | null; payload: Record<string, unknown> | null }>();
|
|
|
|
|
for (const row of approvalRows) {
|
|
|
|
|
const id = stringValue(row.id);
|
|
|
|
|
if (!id) continue;
|
|
|
|
|
approvalsById.set(id, {
|
|
|
|
|
status: stringValue(row.status) || null,
|
|
|
|
|
payload: objectRecord(parseStoredPayload(row.payload)) ?? null,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
const records = new Map<string, DigitalEmployeeRouteInterventionRecord>();
|
|
|
|
|
const resolutions = new Map<string, Record<string, unknown>>();
|
|
|
|
|
for (const row of eventRows) {
|
|
|
|
|
const kind = stringValue(row.kind);
|
|
|
|
|
const payload = objectRecord(parseStoredPayload(row.payload)) ?? {};
|
|
|
|
|
if (kind === "route_decision_intervention_resolved") {
|
|
|
|
|
const routeDecisionId = stringValue(payload.route_decision_id ?? payload.routeDecisionId);
|
|
|
|
|
if (routeDecisionId) resolutions.set(routeDecisionId, { ...payload, event_id: row.id, occurred_at: row.occurred_at });
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const decision = kind === "route_decision" ? routeDecisionFromEventPayload(payload) : null;
|
|
|
|
|
const source = decision ? routeInterventionSourceFromDecision(decision) : routeInterventionSourceFromPayload(payload, row);
|
|
|
|
|
if (!source) continue;
|
|
|
|
|
records.set(source.route_decision_id, source);
|
|
|
|
|
}
|
|
|
|
|
for (const approval of approvalsById.values()) {
|
|
|
|
|
const payload = approval.payload;
|
|
|
|
|
if (!payload || stringValue(payload.approval_kind) !== "route_decision_policy") continue;
|
|
|
|
|
const routeDecisionId = stringValue(payload.route_decision_id ?? payload.routeDecisionId);
|
|
|
|
|
if (!routeDecisionId || records.has(routeDecisionId)) continue;
|
|
|
|
|
records.set(routeDecisionId, routeInterventionSourceFromPayload({
|
|
|
|
|
...payload,
|
|
|
|
|
approval_id: stringValue(payload.approval_id ?? payload.approvalId) || null,
|
|
|
|
|
policy_result: "approval_required",
|
|
|
|
|
}, { occurred_at: stringValue(payload.created_at ?? payload.createdAt) || new Date().toISOString() })!);
|
|
|
|
|
}
|
|
|
|
|
for (const record of records.values()) {
|
|
|
|
|
const approval = record.approval_id ? approvalsById.get(record.approval_id) : null;
|
|
|
|
|
if (approval?.status) record.approval_status = approval.status;
|
|
|
|
|
const resolution = resolutions.get(record.route_decision_id);
|
|
|
|
|
if (resolution) applyRouteInterventionResolution(record, resolution);
|
|
|
|
|
else if (record.approval_status && isApprovedRouteInterventionDecision(record.approval_status)) record.intervention_status = "approved";
|
|
|
|
|
else if (record.approval_status && isRejectedRouteInterventionDecision(record.approval_status)) record.intervention_status = "rejected";
|
|
|
|
|
}
|
|
|
|
|
return Array.from(records.values())
|
|
|
|
|
.sort((left, right) => (right.resolved_at || right.recorded_at).localeCompare(left.resolved_at || left.recorded_at))
|
|
|
|
|
.slice(0, safeLimit);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function recordDigitalEmployeeRouteInterventionResolution(
|
|
|
|
|
input: DigitalEmployeeRouteInterventionResolutionInput,
|
|
|
|
|
): DigitalEmployeeRouteInterventionRecord {
|
|
|
|
|
const db = getDigitalEmployeeDb();
|
|
|
|
|
const now = input.occurredAt || new Date().toISOString();
|
|
|
|
|
const routeDecisionId = stringValue(input.routeDecisionId);
|
|
|
|
|
if (!routeDecisionId) throw new Error("route decision id is required");
|
|
|
|
|
const approvalId = stringValue(input.approvalId) || null;
|
|
|
|
|
if (db) {
|
|
|
|
|
const existing = findRouteInterventionResolution(routeDecisionId, approvalId);
|
|
|
|
|
if (existing) {
|
|
|
|
|
return routeInterventionRecordFromResolution(input, existing, true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const decision = normalizeRouteInterventionDecision(input.decision);
|
|
|
|
|
const commandAccepted = input.commandAccepted === true;
|
|
|
|
|
const commandError = stringValue(input.commandError) || null;
|
|
|
|
|
const eventId = `route-intervention:${safeId(routeDecisionId)}:${safeId(approvalId || "no-approval")}`;
|
|
|
|
|
const payload: Record<string, unknown> = {
|
|
|
|
|
source: "qimingclaw-route-intervention",
|
|
|
|
|
route_decision_id: routeDecisionId,
|
|
|
|
|
approval_id: approvalId,
|
|
|
|
|
decision,
|
|
|
|
|
status: routeInterventionStatusFromResolution(decision, commandAccepted, commandError),
|
|
|
|
|
created_command_id: stringValue(input.createdCommandId) || null,
|
|
|
|
|
command_accepted: commandAccepted,
|
|
|
|
|
command_error: commandError,
|
|
|
|
|
blocked_reason: stringValue(input.blockedReason) || null,
|
|
|
|
|
policy_result: stringValue(input.policyResult) || null,
|
|
|
|
|
policy_source: stringValue(input.policySource) || null,
|
|
|
|
|
matched_intent: stringValue(input.matchedIntent) || null,
|
|
|
|
|
route_kind: normalizeRouteKind(input.routeKind),
|
|
|
|
|
intent_kind: normalizeRouteIntentKind(input.intentKind),
|
|
|
|
|
reason_codes: normalizeSkillIds(input.reasonCodes ?? []),
|
|
|
|
|
candidate_skills: normalizeSkillIds(input.candidateSkills ?? []),
|
|
|
|
|
context_refs: objectRecord(input.contextRefs) ?? {},
|
|
|
|
|
entrypoint: stringValue(input.entrypoint) || null,
|
|
|
|
|
actor: stringValue(input.actor) || "digital_employee_operator",
|
|
|
|
|
correlation_id: stringValue(input.correlationId) || null,
|
|
|
|
|
resolution_payload: sanitizeRouteDecisionPolicyPayload(input.payload),
|
|
|
|
|
};
|
|
|
|
|
insertEvent(
|
|
|
|
|
{
|
|
|
|
|
event_id: eventId,
|
|
|
|
|
kind: "route_decision_intervention_resolved",
|
|
|
|
|
occurred_at: now,
|
|
|
|
|
message: input.message || routeInterventionResolutionMessage(decision, routeDecisionId),
|
|
|
|
|
plan_id: null,
|
|
|
|
|
task_id: null,
|
|
|
|
|
},
|
|
|
|
|
null,
|
|
|
|
|
payload,
|
|
|
|
|
);
|
|
|
|
|
return routeInterventionRecordFromResolution(input, { ...payload, event_id: eventId, occurred_at: now }, false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function evaluateDigitalEmployeeRouteDecisionPolicy(
|
|
|
|
|
input: DigitalEmployeeRouteDecisionPolicyInput,
|
|
|
|
|
): DigitalEmployeeRouteDecisionPolicyResult {
|
|
|
|
|
const policy = readDigitalEmployeeRouteDecisionPolicy();
|
|
|
|
|
const routeKind = normalizeRouteKind(input.routeKind);
|
|
|
|
|
const intentKind = normalizeRouteIntentKind(input.intentKind);
|
|
|
|
|
const routeDecisionId = stringValue(input.routeDecisionId) || null;
|
|
|
|
|
const baseReasonCodes = normalizeSkillIds(input.reasonCodes ?? []);
|
|
|
|
|
const source = policy.source || "local";
|
|
|
|
|
if (!policy.enabled) {
|
|
|
|
|
@@ -1759,6 +1932,7 @@ export function evaluateDigitalEmployeeRouteDecisionPolicy(
|
|
|
|
|
policy_result: "accepted",
|
|
|
|
|
route_kind: routeKind,
|
|
|
|
|
intent_kind: intentKind,
|
|
|
|
|
route_decision_id: routeDecisionId,
|
|
|
|
|
reason_codes: [...baseReasonCodes, "route_policy_disabled"],
|
|
|
|
|
policy_source: source,
|
|
|
|
|
policy,
|
|
|
|
|
@@ -1787,6 +1961,7 @@ export function evaluateDigitalEmployeeRouteDecisionPolicy(
|
|
|
|
|
policy_result: "accepted",
|
|
|
|
|
route_kind: routeKind,
|
|
|
|
|
intent_kind: intentKind,
|
|
|
|
|
route_decision_id: routeDecisionId,
|
|
|
|
|
reason_codes: baseReasonCodes.length > 0 ? baseReasonCodes : ["route_policy_allowed"],
|
|
|
|
|
policy_source: source,
|
|
|
|
|
policy,
|
|
|
|
|
@@ -1803,6 +1978,7 @@ function recordDigitalEmployeeRouteDecisionPolicyIntervention(
|
|
|
|
|
const now = input.occurredAt || new Date().toISOString();
|
|
|
|
|
const routeKind = normalizeRouteKind(input.routeKind);
|
|
|
|
|
const intentKind = normalizeRouteIntentKind(input.intentKind);
|
|
|
|
|
const routeDecisionId = stringValue(input.routeDecisionId) || null;
|
|
|
|
|
const source = stringValue(input.source) || "qimingclaw-route-policy";
|
|
|
|
|
const correlationId = stringValue(input.correlationId) || stringValue(input.idempotencyKey) || `${routeKind}:${intentKind}:${now}`;
|
|
|
|
|
const eventId = `route-policy:${safeId(decision)}:${safeId(intentKind)}:${safeId(correlationId)}:${safeId(now)}`;
|
|
|
|
|
@@ -1818,6 +1994,7 @@ function recordDigitalEmployeeRouteDecisionPolicyIntervention(
|
|
|
|
|
policy_result: "blocked",
|
|
|
|
|
route_kind: routeKind,
|
|
|
|
|
intent_kind: intentKind,
|
|
|
|
|
route_decision_id: routeDecisionId,
|
|
|
|
|
reason_codes: [...reasonCodes, "database_unavailable"],
|
|
|
|
|
matched_intent: intentKind,
|
|
|
|
|
blocked_reason: "database_unavailable",
|
|
|
|
|
@@ -1838,6 +2015,7 @@ function recordDigitalEmployeeRouteDecisionPolicyIntervention(
|
|
|
|
|
payload: {
|
|
|
|
|
source,
|
|
|
|
|
approval_kind: "route_decision_policy",
|
|
|
|
|
route_decision_id: routeDecisionId,
|
|
|
|
|
route_kind: routeKind,
|
|
|
|
|
intent_kind: intentKind,
|
|
|
|
|
reason_codes: reasonCodes,
|
|
|
|
|
@@ -1854,6 +2032,7 @@ function recordDigitalEmployeeRouteDecisionPolicyIntervention(
|
|
|
|
|
source,
|
|
|
|
|
route_kind: routeKind,
|
|
|
|
|
intent_kind: intentKind,
|
|
|
|
|
route_decision_id: routeDecisionId,
|
|
|
|
|
decision,
|
|
|
|
|
status: decision,
|
|
|
|
|
policy_result: decision,
|
|
|
|
|
@@ -1889,6 +2068,7 @@ function recordDigitalEmployeeRouteDecisionPolicyIntervention(
|
|
|
|
|
policy_result: decision,
|
|
|
|
|
route_kind: routeKind,
|
|
|
|
|
intent_kind: intentKind,
|
|
|
|
|
route_decision_id: routeDecisionId,
|
|
|
|
|
reason_codes: reasonCodes,
|
|
|
|
|
matched_intent: intentKind,
|
|
|
|
|
blocked_reason: blockedReason,
|
|
|
|
|
@@ -2112,6 +2292,164 @@ function routeDecisionPolicyMessage(
|
|
|
|
|
: `入口路由已被策略阻断:${routeKind} / ${intentKind}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function routeInterventionSourceFromDecision(decision: DigitalEmployeeRouteDecisionRecord): DigitalEmployeeRouteInterventionRecord | null {
|
|
|
|
|
if (!decision.approval_id && !decision.blocked_reason && decision.policy_result !== "blocked" && decision.policy_result !== "approval_required") return null;
|
|
|
|
|
return {
|
|
|
|
|
route_decision_id: decision.id,
|
|
|
|
|
approval_id: decision.approval_id ?? null,
|
|
|
|
|
route_kind: decision.route_kind,
|
|
|
|
|
intent_kind: decision.intent_kind,
|
|
|
|
|
policy_result: decision.policy_result,
|
|
|
|
|
intervention_status: decision.policy_result === "approval_required" ? "pending" : "blocked",
|
|
|
|
|
decision: null,
|
|
|
|
|
created_command_id: decision.created_command_id,
|
|
|
|
|
blocked_reason: decision.blocked_reason ?? null,
|
|
|
|
|
matched_intent: decision.matched_intent ?? null,
|
|
|
|
|
policy_source: decision.policy_source ?? null,
|
|
|
|
|
reason_codes: decision.reason_codes,
|
|
|
|
|
candidate_skills: decision.candidate_skills,
|
|
|
|
|
context_refs: decision.context_refs,
|
|
|
|
|
entrypoint: decision.entrypoint ?? null,
|
|
|
|
|
actor: decision.actor ?? null,
|
|
|
|
|
correlation_id: decision.correlation_id,
|
|
|
|
|
approval_status: null,
|
|
|
|
|
recorded_at: decision.recorded_at,
|
|
|
|
|
resolved_at: null,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function routeInterventionSourceFromPayload(
|
|
|
|
|
payload: Record<string, unknown>,
|
|
|
|
|
row: Record<string, unknown>,
|
|
|
|
|
): DigitalEmployeeRouteInterventionRecord | null {
|
|
|
|
|
const routeDecisionId = stringValue(payload.route_decision_id ?? payload.routeDecisionId);
|
|
|
|
|
if (!routeDecisionId) return null;
|
|
|
|
|
const policyResult = stringValue(payload.policy_result ?? payload.policyResult ?? payload.decision) || null;
|
|
|
|
|
return {
|
|
|
|
|
route_decision_id: routeDecisionId,
|
|
|
|
|
approval_id: stringValue(payload.approval_id ?? payload.approvalId) || null,
|
|
|
|
|
route_kind: normalizeRouteKind(payload.route_kind ?? payload.routeKind),
|
|
|
|
|
intent_kind: normalizeRouteIntentKind(payload.intent_kind ?? payload.intentKind),
|
|
|
|
|
policy_result: policyResult,
|
|
|
|
|
intervention_status: policyResult === "approval_required" ? "pending" : "blocked",
|
|
|
|
|
decision: null,
|
|
|
|
|
created_command_id: null,
|
|
|
|
|
blocked_reason: stringValue(payload.blocked_reason ?? payload.blockedReason) || null,
|
|
|
|
|
matched_intent: stringValue(payload.matched_intent ?? payload.matchedIntent) || null,
|
|
|
|
|
policy_source: stringValue(payload.policy_source ?? payload.policySource) || null,
|
|
|
|
|
reason_codes: parseRouteDecisionStringArray(payload.reason_codes ?? payload.reasonCodes),
|
|
|
|
|
candidate_skills: parseRouteDecisionStringArray(payload.candidate_skills ?? payload.candidateSkills),
|
|
|
|
|
context_refs: objectRecord(payload.context_refs ?? payload.contextRefs) ?? {},
|
|
|
|
|
entrypoint: stringValue(payload.entrypoint) || null,
|
|
|
|
|
actor: stringValue(payload.actor) || null,
|
|
|
|
|
correlation_id: stringValue(payload.correlation_id ?? payload.correlationId) || null,
|
|
|
|
|
approval_status: null,
|
|
|
|
|
recorded_at: stringValue(row.occurred_at ?? row.occurredAt) || new Date().toISOString(),
|
|
|
|
|
resolved_at: null,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function applyRouteInterventionResolution(record: DigitalEmployeeRouteInterventionRecord, resolution: Record<string, unknown>): void {
|
|
|
|
|
record.decision = stringValue(resolution.decision) || null;
|
|
|
|
|
record.created_command_id = stringValue(resolution.created_command_id ?? resolution.createdCommandId) || record.created_command_id;
|
|
|
|
|
record.intervention_status = routeInterventionStatusFromResolution(
|
|
|
|
|
record.decision || "resolved",
|
|
|
|
|
resolution.command_accepted === true || resolution.commandAccepted === true,
|
|
|
|
|
stringValue(resolution.command_error ?? resolution.commandError) || null,
|
|
|
|
|
);
|
|
|
|
|
record.resolved_at = stringValue(resolution.occurred_at ?? resolution.occurredAt) || null;
|
|
|
|
|
record.resolution_event_id = stringValue(resolution.event_id ?? resolution.eventId) || null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function routeInterventionRecordFromResolution(
|
|
|
|
|
input: DigitalEmployeeRouteInterventionResolutionInput,
|
|
|
|
|
resolution: Record<string, unknown>,
|
|
|
|
|
duplicate: boolean,
|
|
|
|
|
): DigitalEmployeeRouteInterventionRecord {
|
|
|
|
|
const decision = stringValue(resolution.decision) || normalizeRouteInterventionDecision(input.decision);
|
|
|
|
|
const record: DigitalEmployeeRouteInterventionRecord = {
|
|
|
|
|
route_decision_id: stringValue(resolution.route_decision_id ?? resolution.routeDecisionId) || input.routeDecisionId,
|
|
|
|
|
approval_id: stringValue(resolution.approval_id ?? resolution.approvalId ?? input.approvalId) || null,
|
|
|
|
|
route_kind: normalizeRouteKind(resolution.route_kind ?? resolution.routeKind ?? input.routeKind),
|
|
|
|
|
intent_kind: normalizeRouteIntentKind(resolution.intent_kind ?? resolution.intentKind ?? input.intentKind),
|
|
|
|
|
policy_result: stringValue(resolution.policy_result ?? resolution.policyResult ?? input.policyResult) || null,
|
|
|
|
|
intervention_status: routeInterventionStatusFromResolution(
|
|
|
|
|
decision,
|
|
|
|
|
resolution.command_accepted === true || input.commandAccepted === true,
|
|
|
|
|
stringValue(resolution.command_error ?? resolution.commandError ?? input.commandError) || null,
|
|
|
|
|
),
|
|
|
|
|
decision,
|
|
|
|
|
created_command_id: stringValue(resolution.created_command_id ?? resolution.createdCommandId ?? input.createdCommandId) || null,
|
|
|
|
|
blocked_reason: stringValue(resolution.blocked_reason ?? resolution.blockedReason ?? input.blockedReason) || null,
|
|
|
|
|
matched_intent: stringValue(resolution.matched_intent ?? resolution.matchedIntent ?? input.matchedIntent) || null,
|
|
|
|
|
policy_source: stringValue(resolution.policy_source ?? resolution.policySource ?? input.policySource) || null,
|
|
|
|
|
reason_codes: parseRouteDecisionStringArray(resolution.reason_codes ?? resolution.reasonCodes ?? input.reasonCodes),
|
|
|
|
|
candidate_skills: parseRouteDecisionStringArray(resolution.candidate_skills ?? resolution.candidateSkills ?? input.candidateSkills),
|
|
|
|
|
context_refs: objectRecord(resolution.context_refs ?? resolution.contextRefs ?? input.contextRefs) ?? {},
|
|
|
|
|
entrypoint: stringValue(resolution.entrypoint ?? input.entrypoint) || null,
|
|
|
|
|
actor: stringValue(resolution.actor ?? input.actor) || null,
|
|
|
|
|
correlation_id: stringValue(resolution.correlation_id ?? resolution.correlationId ?? input.correlationId) || null,
|
|
|
|
|
approval_status: null,
|
|
|
|
|
recorded_at: stringValue(resolution.occurred_at ?? resolution.occurredAt ?? input.occurredAt) || new Date().toISOString(),
|
|
|
|
|
resolved_at: stringValue(resolution.occurred_at ?? resolution.occurredAt ?? input.occurredAt) || new Date().toISOString(),
|
|
|
|
|
resolution_event_id: stringValue(resolution.event_id ?? resolution.eventId) || null,
|
|
|
|
|
resolution_duplicate: duplicate,
|
|
|
|
|
};
|
|
|
|
|
return record;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function findRouteInterventionResolution(routeDecisionId: string, approvalId: string | null): Record<string, unknown> | null {
|
|
|
|
|
const db = getDigitalEmployeeDb();
|
|
|
|
|
if (!db) return null;
|
|
|
|
|
const rows = db.prepare(`
|
|
|
|
|
SELECT id, payload, occurred_at, created_at
|
|
|
|
|
FROM digital_events
|
|
|
|
|
WHERE kind = 'route_decision_intervention_resolved'
|
|
|
|
|
ORDER BY occurred_at DESC, created_at DESC
|
|
|
|
|
LIMIT 300
|
|
|
|
|
`).all() as Array<Record<string, unknown>>;
|
|
|
|
|
for (const row of rows) {
|
|
|
|
|
const payload = objectRecord(parseStoredPayload(row.payload));
|
|
|
|
|
if (!payload) continue;
|
|
|
|
|
const payloadRouteDecisionId = stringValue(payload.route_decision_id ?? payload.routeDecisionId);
|
|
|
|
|
const payloadApprovalId = stringValue(payload.approval_id ?? payload.approvalId) || null;
|
|
|
|
|
if (payloadRouteDecisionId === routeDecisionId && payloadApprovalId === approvalId) {
|
|
|
|
|
return { ...payload, event_id: row.id, occurred_at: row.occurred_at };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeRouteInterventionDecision(value: unknown): string {
|
|
|
|
|
return stringValue(value).trim().toLowerCase() || "resolved";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isApprovedRouteInterventionDecision(value: string): boolean {
|
|
|
|
|
return ["approve", "approved", "accept", "accepted"].includes(value.toLowerCase());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isRejectedRouteInterventionDecision(value: string): boolean {
|
|
|
|
|
return ["reject", "rejected", "deny", "denied", "dismiss", "dismissed"].includes(value.toLowerCase());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function routeInterventionStatusFromResolution(
|
|
|
|
|
decision: string,
|
|
|
|
|
commandAccepted: boolean,
|
|
|
|
|
commandError: string | null,
|
|
|
|
|
): DigitalEmployeeRouteInterventionRecord["intervention_status"] {
|
|
|
|
|
if (commandError) return "failed";
|
|
|
|
|
if (commandAccepted) return "executed";
|
|
|
|
|
if (isRejectedRouteInterventionDecision(decision)) return "rejected";
|
|
|
|
|
if (isApprovedRouteInterventionDecision(decision)) return "approved";
|
|
|
|
|
return "resolved";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function routeInterventionResolutionMessage(decision: string, routeDecisionId: string): string {
|
|
|
|
|
if (isRejectedRouteInterventionDecision(decision)) return `入口路由干预已拒绝:${routeDecisionId}`;
|
|
|
|
|
if (isApprovedRouteInterventionDecision(decision)) return `入口路由干预已通过:${routeDecisionId}`;
|
|
|
|
|
return `入口路由干预已处理:${routeDecisionId}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function recordDigitalEmployeeSkillCallAudit(
|
|
|
|
|
input: DigitalEmployeeSkillCallAuditInput,
|
|
|
|
|
): DigitalEmployeeSkillCallAuditRecord | null {
|
|
|
|
|
|