吸收数字员工入口路由干预回写

This commit is contained in:
baiyanyun
2026-06-11 18:31:41 +08:00
parent d292c34579
commit 4c862ef4b7
16 changed files with 1151 additions and 191 deletions

View File

@@ -86,6 +86,8 @@ vi.mock("../services/digitalEmployee/stateService", () => ({
syncQimingclawMemoryEntries: vi.fn(),
recordDigitalEmployeeRouteDecision: vi.fn(),
listDigitalEmployeeRouteDecisions: vi.fn(),
listDigitalEmployeeRouteInterventions: vi.fn(() => [{ route_decision_id: "route-1", intervention_status: "pending" }]),
recordDigitalEmployeeRouteInterventionResolution: vi.fn(() => ({ route_decision_id: "route-1", intervention_status: "executed" })),
}));
vi.mock("../services/digitalEmployee/schedulerService", () => ({
@@ -318,6 +320,27 @@ describe("digital employee managed service actions", () => {
expect(stateService.evaluateDigitalEmployeeRouteDecisionPolicy).toHaveBeenCalledWith({ intentKind: "schedule" });
});
it("registers route intervention list and resolution through the digital employee IPC boundary", async () => {
const electron = await import("electron");
const stateService = await import("../services/digitalEmployee/stateService");
const { registerDigitalEmployeeHandlers } = await import("./digitalEmployeeHandlers");
registerDigitalEmployeeHandlers(createCtx());
const calls = vi.mocked(electron.ipcMain.handle).mock.calls;
const listCall = calls.find(([channel]) => channel === "digitalEmployee:listRouteInterventions");
const resolutionCall = calls.find(([channel]) => channel === "digitalEmployee:recordRouteInterventionResolution");
expect(listCall).toBeTruthy();
expect(resolutionCall).toBeTruthy();
const listResult = await (listCall?.[1] as (_event: unknown, options: unknown) => Promise<unknown>)({}, { limit: 5 });
const resolutionResult = await (resolutionCall?.[1] as (_event: unknown, input: unknown) => Promise<unknown>)({}, { routeDecisionId: "route-1", decision: "approved" });
expect(listResult).toEqual([expect.objectContaining({ route_decision_id: "route-1" })]);
expect(resolutionResult).toMatchObject({ route_decision_id: "route-1", intervention_status: "executed" });
expect(stateService.listDigitalEmployeeRouteInterventions).toHaveBeenCalledWith({ limit: 5 });
expect(stateService.recordDigitalEmployeeRouteInterventionResolution).toHaveBeenCalledWith({ routeDecisionId: "route-1", decision: "approved" });
});
it("registers approval action history through the digital employee IPC boundary", async () => {
const electron = await import("electron");
const { registerDigitalEmployeeHandlers } = await import("./digitalEmployeeHandlers");

View File

@@ -39,6 +39,8 @@ import {
syncQimingclawMemoryEntries,
recordDigitalEmployeeRouteDecision,
listDigitalEmployeeRouteDecisions,
listDigitalEmployeeRouteInterventions,
recordDigitalEmployeeRouteInterventionResolution,
type DigitalEmployeeManagedService,
type DigitalEmployeeManagedServiceActionInput,
type DigitalEmployeeGovernanceCommandRecord,
@@ -47,6 +49,7 @@ import {
type DigitalEmployeeApprovalActionInput,
type DigitalEmployeeMemoryUpsertInput,
type DigitalEmployeeRouteDecisionInput,
type DigitalEmployeeRouteInterventionResolutionInput,
type DigitalEmployeeServiceStatus,
type DigitalEmployeeSnapshot,
type DigitalEmployeeUiStateUpdate,
@@ -256,10 +259,18 @@ export function registerDigitalEmployeeHandlers(ctx: HandlerContext): void {
return listDigitalEmployeeRouteDecisions(options ?? {});
});
ipcMain.handle("digitalEmployee:listRouteInterventions", async (_, options?: { limit?: number }) => {
return listDigitalEmployeeRouteInterventions(options ?? {});
});
ipcMain.handle("digitalEmployee:recordRouteDecision", async (_, input: DigitalEmployeeRouteDecisionInput) => {
return recordDigitalEmployeeRouteDecision(input);
});
ipcMain.handle("digitalEmployee:recordRouteInterventionResolution", async (_, input: DigitalEmployeeRouteInterventionResolutionInput) => {
return recordDigitalEmployeeRouteInterventionResolution(input);
});
ipcMain.handle(
"digitalEmployee:recordGovernanceCommand",
async (_, command: DigitalEmployeeGovernanceCommandRecord) => {

View File

@@ -734,6 +734,7 @@ describe("digital employee state service", () => {
occurredAt: "2026-06-07T08:12:00.000Z",
});
const approval = evaluateDigitalEmployeeRouteDecisionPolicy({
routeDecisionId: "route-decision-execute",
routeKind: "DirectRun",
intentKind: "execute",
correlationId: "corr-execute",
@@ -765,6 +766,90 @@ describe("digital employee state service", () => {
expect(mockState.db?.events.get(blocked.event_id!)).toMatchObject({ kind: "route_decision_blocked" });
expect(mockState.db?.events.get(approval.event_id!)).toMatchObject({ kind: "route_decision_approval_required" });
expect(mockState.db?.approvals.get(approval.approval_id!)).toMatchObject({ status: "pending" });
expect(JSON.parse(mockState.db?.approvals.get(approval.approval_id!)?.payload ?? "{}")).toMatchObject({
approval_kind: "route_decision_policy",
route_decision_id: "route-decision-execute",
route_kind: "DirectRun",
intent_kind: "execute",
});
});
it("records route intervention resolution once and derives intervention rows", async () => {
const {
listDigitalEmployeeRouteInterventions,
recordDigitalEmployeeRouteDecision,
recordDigitalEmployeeRouteInterventionResolution,
} = await import("./stateService");
recordDigitalEmployeeRouteDecision({
id: "route-decision-restore",
correlationId: "route-corr-restore",
routeKind: "DirectRun",
intentKind: "execute",
reasonCodes: ["route_intent_requires_approval"],
candidateSkills: ["crm-search"],
policyResult: "approval_required",
policySource: "remote",
approvalId: "route-approval-restore",
matchedIntent: "execute",
contextRefs: { task_id: "task-1" },
recordedAt: "2026-06-07T08:14:00.000Z",
});
mockState.db?.approvals.set("route-approval-restore", {
id: "route-approval-restore",
plan_id: "",
task_id: "",
run_id: "",
title: "入口路由审批",
status: "approved",
payload: JSON.stringify({ approval_kind: "route_decision_policy", route_decision_id: "route-decision-restore" }),
sync_status: "pending",
created_at: "2026-06-07T08:14:00.000Z",
updated_at: "2026-06-07T08:15:00.000Z",
});
expect(listDigitalEmployeeRouteInterventions()).toEqual([
expect.objectContaining({
route_decision_id: "route-decision-restore",
approval_id: "route-approval-restore",
intervention_status: "approved",
approval_status: "approved",
}),
]);
const first = recordDigitalEmployeeRouteInterventionResolution({
routeDecisionId: "route-decision-restore",
approvalId: "route-approval-restore",
decision: "approved",
createdCommandId: "governance-command-restore",
commandAccepted: true,
routeKind: "DirectRun",
intentKind: "execute",
policySource: "remote",
occurredAt: "2026-06-07T08:16:00.000Z",
});
const second = recordDigitalEmployeeRouteInterventionResolution({
routeDecisionId: "route-decision-restore",
approvalId: "route-approval-restore",
decision: "approved",
createdCommandId: "governance-command-duplicate",
commandAccepted: true,
occurredAt: "2026-06-07T08:17:00.000Z",
});
expect(first).toMatchObject({
route_decision_id: "route-decision-restore",
approval_id: "route-approval-restore",
intervention_status: "executed",
created_command_id: "governance-command-restore",
});
expect(second).toMatchObject({ resolution_duplicate: true, created_command_id: "governance-command-restore" });
expect(Array.from(mockState.db?.events.values() ?? []).filter((event) => event.kind === "route_decision_intervention_resolved")).toHaveLength(1);
expect(listDigitalEmployeeRouteInterventions()[0]).toMatchObject({
intervention_status: "executed",
resolved_at: "2026-06-07T08:16:00.000Z",
created_command_id: "governance-command-restore",
});
});
it("updates formal approval records when UI approval decisions are saved", async () => {

View File

@@ -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 {

View File

@@ -1024,6 +1024,19 @@ describe("digital employee sync service", () => {
matched_intent: "schedule",
},
});
insertEventEntity("event-route-intervention", "route_decision_intervention_resolved", {
route_decision_id: "route-1",
approval_id: "route-approval-1",
route_kind: "PlanStepDispatch",
intent_kind: "schedule",
decision: "approved",
status: "executed",
policy_result: "approval_required",
policy_source: "remote",
created_command_id: "governance-command-restore",
command_accepted: true,
reason_codes: ["route_intent_requires_approval"],
});
insertEventEntity("event-approval", "approval_action_recorded", {
approval_id: "approval-1",
action: "mark_risk",
@@ -1272,6 +1285,7 @@ describe("digital employee sync service", () => {
success: true,
acked: [
{ entity_type: "event", entity_id: "event-route" },
{ entity_type: "event", entity_id: "event-route-intervention" },
{ entity_type: "event", entity_id: "event-approval" },
{ entity_type: "event", entity_id: "event-approval-decision" },
{ entity_type: "event", entity_id: "event-artifact" },
@@ -1315,6 +1329,17 @@ describe("digital employee sync service", () => {
matched_intent: "schedule",
attention_level: "action",
});
expect(businessView("event-route-intervention")).toMatchObject({
category: "route",
route_decision_id: "route-1",
approval_id: "route-approval-1",
decision: "approved",
status: "executed",
created_command_id: "governance-command-restore",
policy_source: "remote",
command_accepted: true,
attention_level: "action",
});
expect(businessView("event-approval")).toMatchObject({
category: "approval",
approval_id: "approval-1",

View File

@@ -1491,6 +1491,7 @@ function eventSpecificBusinessView(
): Record<string, unknown> {
if (kind === "route_decision") return routeDecisionBusinessView(payload, eventId);
if (kind === "route_decision_blocked" || kind === "route_decision_approval_required") return routeDecisionPolicyBusinessView(payload);
if (kind === "route_decision_intervention_resolved") return routeDecisionInterventionBusinessView(payload);
if (kind === "approval_decision") return approvalDecisionBusinessView(payload);
if (kind.startsWith("approval_action_")) return approvalActionBusinessView(payload);
if (isApprovalSyncEvent(kind)) return approvalSyncBusinessView(payload);
@@ -1547,6 +1548,26 @@ function routeDecisionPolicyBusinessView(payload: Record<string, unknown>): Reco
};
}
function routeDecisionInterventionBusinessView(payload: Record<string, unknown>): Record<string, unknown> {
return {
route_decision_id: stringField(payload.route_decision_id ?? payload.routeDecisionId) || null,
approval_id: stringField(payload.approval_id ?? payload.approvalId) || null,
route_kind: stringField(payload.route_kind ?? payload.routeKind) || "ChatOnly",
intent_kind: stringField(payload.intent_kind ?? payload.intentKind) || "chat",
decision: stringField(payload.decision) || null,
status: stringField(payload.status) || null,
policy_result: stringField(payload.policy_result ?? payload.policyResult) || null,
policy_source: stringField(payload.policy_source ?? payload.policySource) || null,
blocked_reason: stringField(payload.blocked_reason ?? payload.blockedReason) || null,
matched_intent: stringField(payload.matched_intent ?? payload.matchedIntent) || null,
created_command_id: stringField(payload.created_command_id ?? payload.createdCommandId) || null,
command_accepted: payload.command_accepted === true || payload.commandAccepted === true,
command_error: stringField(payload.command_error ?? payload.commandError) || null,
reason_codes: stringArrayField(payload.reason_codes ?? payload.reasonCodes),
attention_level: stringField(payload.command_error ?? payload.commandError) ? "error" : "action",
};
}
function approvalActionBusinessView(payload: Record<string, unknown>): Record<string, unknown> {
const comment = objectRecord(payload.comment_summary);
const delegate = objectRecord(payload.delegate_summary);
@@ -1774,7 +1795,7 @@ function notificationBusinessView(payload: Record<string, unknown>): Record<stri
}
function eventBusinessCategory(kind: string): string {
if (kind === "route_decision" || kind === "route_decision_blocked" || kind === "route_decision_approval_required") return "route";
if (kind === "route_decision" || kind === "route_decision_blocked" || kind === "route_decision_approval_required" || kind === "route_decision_intervention_resolved") return "route";
if (kind.startsWith("approval_")) return "approval";
if (kind.startsWith("risk_")) return "approval";
if (isApprovalSyncEvent(kind)) return "approval";

View File

@@ -173,9 +173,15 @@ contextBridge.exposeInMainWorld("QimingClawBridge", {
async listRouteDecisions(options?: unknown) {
return ipcRenderer.invoke("digitalEmployee:listRouteDecisions", options);
},
async listRouteInterventions(options?: unknown) {
return ipcRenderer.invoke("digitalEmployee:listRouteInterventions", options);
},
async recordRouteDecision(input: unknown) {
return ipcRenderer.invoke("digitalEmployee:recordRouteDecision", input);
},
async recordRouteInterventionResolution(input: unknown) {
return ipcRenderer.invoke("digitalEmployee:recordRouteInterventionResolution", input);
},
async recordGovernanceCommand(command: unknown) {
return ipcRenderer.invoke("digitalEmployee:recordGovernanceCommand", command);
},