吸收数字员工入口路由策略下发
This commit is contained in:
@@ -5,12 +5,14 @@ import { ensureDigitalEmployeeSchema, getDb, readSetting, writeSetting } from ".
|
||||
import {
|
||||
normalizePlanStepDispatchPolicy,
|
||||
normalizeRiskApprovalPolicy,
|
||||
normalizeRouteDecisionPolicy,
|
||||
readDigitalEmployeeUiState,
|
||||
saveDigitalEmployeeUiState,
|
||||
upsertDigitalEmployeeApprovalFromRemote,
|
||||
type DigitalEmployeeApprovalUpdateInput,
|
||||
type DigitalEmployeePlanStepDispatchPolicy,
|
||||
type DigitalEmployeeRiskApprovalPolicy,
|
||||
type DigitalEmployeeRouteDecisionPolicy,
|
||||
} from "./stateService";
|
||||
import {
|
||||
SKILL_POLICIES_SETTING_KEY,
|
||||
@@ -23,6 +25,7 @@ const DEFAULT_SYNC_PATH = "/api/digital-employee/sync/outbox";
|
||||
const DEFAULT_PLAN_STEP_DISPATCH_POLICY_PATH = "/api/digital-employee/policies/plan-step-dispatch";
|
||||
const DEFAULT_SKILL_POLICY_PATH = "/api/digital-employee/policies/skills";
|
||||
const DEFAULT_RISK_APPROVAL_POLICY_PATH = "/api/digital-employee/policies/risk-approval";
|
||||
const DEFAULT_ROUTE_DECISION_POLICY_PATH = "/api/digital-employee/policies/route-decision";
|
||||
const DEFAULT_APPROVAL_UPDATES_PATH = "/api/digital-employee/approvals/updates";
|
||||
const DEFAULT_APPROVAL_DECISIONS_PATH = "/api/digital-employee/approvals/decisions";
|
||||
const DEFAULT_PLAN_STEP_DISPATCH_LEASE_PATH = "/api/digital-employee/leases/plan-step-dispatch";
|
||||
@@ -47,6 +50,8 @@ let skillPolicyPulling = false;
|
||||
let lastSkillPolicyPullAttemptMs = 0;
|
||||
let riskApprovalPolicyPulling = false;
|
||||
let lastRiskApprovalPolicyPullAttemptMs = 0;
|
||||
let routeDecisionPolicyPulling = false;
|
||||
let lastRouteDecisionPolicyPullAttemptMs = 0;
|
||||
let approvalUpdatesPulling = false;
|
||||
let lastApprovalUpdatesPullAttemptMs = 0;
|
||||
|
||||
@@ -61,6 +66,7 @@ interface DigitalEmployeeSyncConfig {
|
||||
policyEndpoint: string | null;
|
||||
skillPolicyEndpoint: string | null;
|
||||
riskApprovalPolicyEndpoint: string | null;
|
||||
routeDecisionPolicyEndpoint: string | null;
|
||||
approvalUpdatesEndpoint: string | null;
|
||||
approvalDecisionsEndpoint: string | null;
|
||||
leaseEndpoint: string | null;
|
||||
@@ -96,6 +102,15 @@ export interface DigitalEmployeeRiskApprovalPolicyPullResult {
|
||||
error?: string | null;
|
||||
}
|
||||
|
||||
export interface DigitalEmployeeRouteDecisionPolicyPullResult {
|
||||
ok: boolean;
|
||||
skipped?: boolean;
|
||||
endpoint: string | null;
|
||||
pulled_at: string | null;
|
||||
policy: DigitalEmployeeRouteDecisionPolicy;
|
||||
error?: string | null;
|
||||
}
|
||||
|
||||
export interface DigitalEmployeeApprovalUpdatesPullResult {
|
||||
ok: boolean;
|
||||
skipped?: boolean;
|
||||
@@ -465,6 +480,63 @@ export async function pullDigitalEmployeeRiskApprovalPolicy(
|
||||
}
|
||||
}
|
||||
|
||||
export async function pullDigitalEmployeeRouteDecisionPolicy(
|
||||
options: { force?: boolean } = {},
|
||||
): Promise<DigitalEmployeeRouteDecisionPolicyPullResult> {
|
||||
const currentState = readDigitalEmployeeUiState();
|
||||
const currentPolicy = currentState.routeDecisionPolicy ?? normalizeRouteDecisionPolicy(null);
|
||||
if (routeDecisionPolicyPulling) {
|
||||
return { ok: true, skipped: true, endpoint: resolveSyncConfig().routeDecisionPolicyEndpoint, pulled_at: currentPolicy.last_pulled_at ?? null, policy: currentPolicy };
|
||||
}
|
||||
const nowMs = Date.now();
|
||||
if (!options.force && nowMs - lastRouteDecisionPolicyPullAttemptMs < POLICY_PULL_THROTTLE_MS) {
|
||||
return { ok: true, skipped: true, endpoint: resolveSyncConfig().routeDecisionPolicyEndpoint, pulled_at: currentPolicy.last_pulled_at ?? null, policy: currentPolicy };
|
||||
}
|
||||
lastRouteDecisionPolicyPullAttemptMs = nowMs;
|
||||
|
||||
const config = resolveSyncConfig();
|
||||
const missingCredentials = missingSyncCredentialLabels(config);
|
||||
if (!config.routeDecisionPolicyEndpoint || missingCredentials.length > 0) {
|
||||
const error = !config.routeDecisionPolicyEndpoint ? "management route decision policy endpoint unavailable" : `missing credentials: ${missingCredentials.join(", ")}`;
|
||||
return recordRouteDecisionPolicyPullFailure(currentState, currentPolicy, config.routeDecisionPolicyEndpoint, error);
|
||||
}
|
||||
|
||||
routeDecisionPolicyPulling = true;
|
||||
try {
|
||||
const response = await fetchJsonWithAuth(config, config.routeDecisionPolicyEndpoint);
|
||||
const responseError = readSyncResponseError(response as SyncResponse);
|
||||
if (responseError) throw new Error(responseError);
|
||||
const remotePolicy = readRemoteRouteDecisionPolicy(response);
|
||||
if (!remotePolicy) throw new Error("management policy response missing route decision policy");
|
||||
const pulledAt = new Date().toISOString();
|
||||
const policy = normalizeRouteDecisionPolicy({
|
||||
...remotePolicy,
|
||||
source: "remote",
|
||||
remote_updated_at: stringField(remotePolicy.updated_at) || stringField(remotePolicy.updatedAt) || stringField(remotePolicy.remote_updated_at) || null,
|
||||
last_pulled_at: pulledAt,
|
||||
last_pull_error: null,
|
||||
});
|
||||
saveDigitalEmployeeUiState({
|
||||
action: "pull_route_decision_policy",
|
||||
metadata: {
|
||||
source: "management-api",
|
||||
endpoint: config.routeDecisionPolicyEndpoint,
|
||||
ok: true,
|
||||
route_decision_policy: policy,
|
||||
},
|
||||
state: {
|
||||
...currentState,
|
||||
routeDecisionPolicy: policy,
|
||||
},
|
||||
});
|
||||
return { ok: true, endpoint: config.routeDecisionPolicyEndpoint, pulled_at: pulledAt, policy, error: null };
|
||||
} catch (error) {
|
||||
return recordRouteDecisionPolicyPullFailure(currentState, currentPolicy, config.routeDecisionPolicyEndpoint, normalizeError(error));
|
||||
} finally {
|
||||
routeDecisionPolicyPulling = false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function pullDigitalEmployeeApprovalUpdates(
|
||||
options: { force?: boolean } = {},
|
||||
): Promise<DigitalEmployeeApprovalUpdatesPullResult> {
|
||||
@@ -794,6 +866,11 @@ function resolveSyncConfig(): DigitalEmployeeSyncConfig {
|
||||
? config.riskApprovalPolicyEndpoint.trim()
|
||||
: null;
|
||||
const riskApprovalPolicyEndpoint = riskApprovalPolicyEndpointOverride || buildEndpoint(serverHost, DEFAULT_RISK_APPROVAL_POLICY_PATH);
|
||||
const routeDecisionPolicyEndpointOverride =
|
||||
typeof config.routeDecisionPolicyEndpoint === "string" && config.routeDecisionPolicyEndpoint.trim()
|
||||
? config.routeDecisionPolicyEndpoint.trim()
|
||||
: null;
|
||||
const routeDecisionPolicyEndpoint = routeDecisionPolicyEndpointOverride || buildEndpoint(serverHost, DEFAULT_ROUTE_DECISION_POLICY_PATH);
|
||||
const approvalUpdatesEndpointOverride =
|
||||
typeof config.approvalUpdatesEndpoint === "string" && config.approvalUpdatesEndpoint.trim()
|
||||
? config.approvalUpdatesEndpoint.trim()
|
||||
@@ -821,6 +898,7 @@ function resolveSyncConfig(): DigitalEmployeeSyncConfig {
|
||||
policyEndpoint,
|
||||
skillPolicyEndpoint,
|
||||
riskApprovalPolicyEndpoint,
|
||||
routeDecisionPolicyEndpoint,
|
||||
approvalUpdatesEndpoint,
|
||||
approvalDecisionsEndpoint,
|
||||
leaseEndpoint,
|
||||
@@ -984,6 +1062,14 @@ function readRemoteRiskApprovalPolicy(response: Record<string, unknown>): Record
|
||||
?? (hasRiskApprovalPolicyFields(data) ? data : null);
|
||||
}
|
||||
|
||||
function readRemoteRouteDecisionPolicy(response: Record<string, unknown>): Record<string, unknown> | null {
|
||||
const data = objectRecord(response.data) ?? response;
|
||||
return objectRecord(data.route_decision_policy)
|
||||
?? objectRecord(data.routeDecisionPolicy)
|
||||
?? objectRecord(data.policy)
|
||||
?? (hasRouteDecisionPolicyFields(data) ? data : null);
|
||||
}
|
||||
|
||||
function readRemoteApprovalUpdates(response: Record<string, unknown>): DigitalEmployeeApprovalUpdateInput[] {
|
||||
const data = objectRecord(response.data) ?? response;
|
||||
const raw = Array.isArray(data.approvals)
|
||||
@@ -1088,6 +1174,17 @@ function hasRiskApprovalPolicyFields(record: Record<string, unknown>): boolean {
|
||||
|| "autoRejectActions" in record;
|
||||
}
|
||||
|
||||
function hasRouteDecisionPolicyFields(record: Record<string, unknown>): boolean {
|
||||
return "approval_required_intents" in record
|
||||
|| "approvalRequiredIntents" in record
|
||||
|| "blocked_intents" in record
|
||||
|| "blockedIntents" in record
|
||||
|| "preferred_route_kinds" in record
|
||||
|| "preferredRouteKinds" in record
|
||||
|| "auto_create_governance_commands" in record
|
||||
|| "autoCreateGovernanceCommands" in record;
|
||||
}
|
||||
|
||||
function looksLikeSkillPolicyMap(record: Record<string, unknown>): boolean {
|
||||
return Object.values(record).some((value) => {
|
||||
if (typeof value === "boolean") return true;
|
||||
@@ -1169,6 +1266,35 @@ function recordRiskApprovalPolicyPullFailure(
|
||||
return { ok: false, endpoint, pulled_at: pulledAt, policy, error };
|
||||
}
|
||||
|
||||
function recordRouteDecisionPolicyPullFailure(
|
||||
state: ReturnType<typeof readDigitalEmployeeUiState>,
|
||||
currentPolicy: DigitalEmployeeRouteDecisionPolicy,
|
||||
endpoint: string | null,
|
||||
error: string,
|
||||
): DigitalEmployeeRouteDecisionPolicyPullResult {
|
||||
const pulledAt = new Date().toISOString();
|
||||
const policy = normalizeRouteDecisionPolicy({
|
||||
...currentPolicy,
|
||||
last_pulled_at: pulledAt,
|
||||
last_pull_error: error,
|
||||
});
|
||||
saveDigitalEmployeeUiState({
|
||||
action: "pull_route_decision_policy",
|
||||
metadata: {
|
||||
source: "management-api",
|
||||
endpoint,
|
||||
ok: false,
|
||||
error,
|
||||
route_decision_policy: policy,
|
||||
},
|
||||
state: {
|
||||
...state,
|
||||
routeDecisionPolicy: policy,
|
||||
},
|
||||
});
|
||||
return { ok: false, endpoint, pulled_at: pulledAt, policy, error };
|
||||
}
|
||||
|
||||
function recordApprovalSyncEvent(action: string, occurredAt: string, metadata: Record<string, unknown>): void {
|
||||
const state = readDigitalEmployeeUiState();
|
||||
saveDigitalEmployeeUiState({
|
||||
@@ -1364,6 +1490,7 @@ function eventSpecificBusinessView(
|
||||
eventId: string,
|
||||
): 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 === "approval_decision") return approvalDecisionBusinessView(payload);
|
||||
if (kind.startsWith("approval_action_")) return approvalActionBusinessView(payload);
|
||||
if (isApprovalSyncEvent(kind)) return approvalSyncBusinessView(payload);
|
||||
@@ -1395,10 +1522,31 @@ function routeDecisionBusinessView(payload: Record<string, unknown>, eventId: st
|
||||
reason_codes: reasonCodes,
|
||||
candidate_skills: stringArrayField(decision.candidate_skills ?? decision.candidateSkills),
|
||||
created_command_id: createdCommandId,
|
||||
policy_source: stringField(decision.policy_source ?? decision.policySource) || null,
|
||||
blocked_reason: stringField(decision.blocked_reason ?? decision.blockedReason) || null,
|
||||
approval_id: stringField(decision.approval_id ?? decision.approvalId) || null,
|
||||
matched_intent: stringField(decision.matched_intent ?? decision.matchedIntent) || null,
|
||||
attention_level: routeDecisionAttentionLevel(policyResult, reasonCodes, createdCommandId),
|
||||
};
|
||||
}
|
||||
|
||||
function routeDecisionPolicyBusinessView(payload: Record<string, unknown>): Record<string, unknown> {
|
||||
const policyResult = stringField(payload.policy_result ?? payload.decision) || stringField(payload.status) || "blocked";
|
||||
const reasonCodes = stringArrayField(payload.reason_codes ?? payload.reasonCodes);
|
||||
return {
|
||||
route_kind: stringField(payload.route_kind ?? payload.routeKind) || "ChatOnly",
|
||||
intent_kind: stringField(payload.intent_kind ?? payload.intentKind) || "chat",
|
||||
policy_result: policyResult,
|
||||
policy_source: stringField(payload.policy_source ?? payload.policySource) || null,
|
||||
blocked_reason: stringField(payload.blocked_reason ?? payload.blockedReason) || null,
|
||||
approval_id: stringField(payload.approval_id ?? payload.approvalId) || null,
|
||||
matched_intent: stringField(payload.matched_intent ?? payload.matchedIntent) || null,
|
||||
reason_codes: reasonCodes,
|
||||
candidate_skills: stringArrayField(payload.candidate_skills ?? payload.candidateSkills),
|
||||
attention_level: policyResult === "approval_required" ? "action" : "error",
|
||||
};
|
||||
}
|
||||
|
||||
function approvalActionBusinessView(payload: Record<string, unknown>): Record<string, unknown> {
|
||||
const comment = objectRecord(payload.comment_summary);
|
||||
const delegate = objectRecord(payload.delegate_summary);
|
||||
@@ -1626,7 +1774,7 @@ function notificationBusinessView(payload: Record<string, unknown>): Record<stri
|
||||
}
|
||||
|
||||
function eventBusinessCategory(kind: string): string {
|
||||
if (kind === "route_decision") return "route";
|
||||
if (kind === "route_decision" || kind === "route_decision_blocked" || kind === "route_decision_approval_required") return "route";
|
||||
if (kind.startsWith("approval_")) return "approval";
|
||||
if (kind.startsWith("risk_")) return "approval";
|
||||
if (isApprovalSyncEvent(kind)) return "approval";
|
||||
|
||||
Reference in New Issue
Block a user