吸收数字员工风险策略远端下发

This commit is contained in:
baiyanyun
2026-06-11 09:51:14 +08:00
parent 8ab3d73fb1
commit e9c76c59ca
15 changed files with 1175 additions and 192 deletions

View File

@@ -4,9 +4,11 @@ import { getDomainTokenKey } from "@shared/utils/domain";
import { ensureDigitalEmployeeSchema, getDb, readSetting, writeSetting } from "../../db";
import {
normalizePlanStepDispatchPolicy,
normalizeRiskApprovalPolicy,
readDigitalEmployeeUiState,
saveDigitalEmployeeUiState,
type DigitalEmployeePlanStepDispatchPolicy,
type DigitalEmployeeRiskApprovalPolicy,
} from "./stateService";
import {
SKILL_POLICIES_SETTING_KEY,
@@ -18,6 +20,7 @@ import {
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_PLAN_STEP_DISPATCH_LEASE_PATH = "/api/digital-employee/leases/plan-step-dispatch";
const MANAGEMENT_SYNC_RECORD_PATH =
"/system/content/content-digital-employee-sync";
@@ -38,6 +41,8 @@ let policyPulling = false;
let lastPolicyPullAttemptMs = 0;
let skillPolicyPulling = false;
let lastSkillPolicyPullAttemptMs = 0;
let riskApprovalPolicyPulling = false;
let lastRiskApprovalPolicyPullAttemptMs = 0;
function getDigitalEmployeeDb(): ReturnType<typeof getDb> {
if (!ensureDigitalEmployeeSchema()) return null;
@@ -49,6 +54,7 @@ interface DigitalEmployeeSyncConfig {
endpoint: string | null;
policyEndpoint: string | null;
skillPolicyEndpoint: string | null;
riskApprovalPolicyEndpoint: string | null;
leaseEndpoint: string | null;
clientKey: string | null;
authToken: string | null;
@@ -73,6 +79,15 @@ export interface DigitalEmployeePlanStepDispatchPolicyPullResult {
error?: string | null;
}
export interface DigitalEmployeeRiskApprovalPolicyPullResult {
ok: boolean;
skipped?: boolean;
endpoint: string | null;
pulled_at: string | null;
policy: DigitalEmployeeRiskApprovalPolicy;
error?: string | null;
}
export interface DigitalEmployeePlanStepRemoteLease {
lease_id: string;
state: "active" | "released" | "expired";
@@ -368,6 +383,63 @@ export async function pullDigitalEmployeeSkillPolicies(
}
}
export async function pullDigitalEmployeeRiskApprovalPolicy(
options: { force?: boolean } = {},
): Promise<DigitalEmployeeRiskApprovalPolicyPullResult> {
const currentState = readDigitalEmployeeUiState();
const currentPolicy = currentState.riskApprovalPolicy ?? normalizeRiskApprovalPolicy(null);
if (riskApprovalPolicyPulling) {
return { ok: true, skipped: true, endpoint: resolveSyncConfig().riskApprovalPolicyEndpoint, pulled_at: currentPolicy.last_pulled_at ?? null, policy: currentPolicy };
}
const nowMs = Date.now();
if (!options.force && nowMs - lastRiskApprovalPolicyPullAttemptMs < POLICY_PULL_THROTTLE_MS) {
return { ok: true, skipped: true, endpoint: resolveSyncConfig().riskApprovalPolicyEndpoint, pulled_at: currentPolicy.last_pulled_at ?? null, policy: currentPolicy };
}
lastRiskApprovalPolicyPullAttemptMs = nowMs;
const config = resolveSyncConfig();
const missingCredentials = missingSyncCredentialLabels(config);
if (!config.riskApprovalPolicyEndpoint || missingCredentials.length > 0) {
const error = !config.riskApprovalPolicyEndpoint ? "management risk approval policy endpoint unavailable" : `missing credentials: ${missingCredentials.join(", ")}`;
return recordRiskApprovalPolicyPullFailure(currentState, currentPolicy, config.riskApprovalPolicyEndpoint, error);
}
riskApprovalPolicyPulling = true;
try {
const response = await fetchJsonWithAuth(config, config.riskApprovalPolicyEndpoint);
const responseError = readSyncResponseError(response as SyncResponse);
if (responseError) throw new Error(responseError);
const remotePolicy = readRemoteRiskApprovalPolicy(response);
if (!remotePolicy) throw new Error("management policy response missing risk approval policy");
const pulledAt = new Date().toISOString();
const policy = normalizeRiskApprovalPolicy({
...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_risk_approval_policy",
metadata: {
source: "management-api",
endpoint: config.riskApprovalPolicyEndpoint,
ok: true,
risk_approval_policy: policy,
},
state: {
...currentState,
riskApprovalPolicy: policy,
},
});
return { ok: true, endpoint: config.riskApprovalPolicyEndpoint, pulled_at: pulledAt, policy, error: null };
} catch (error) {
return recordRiskApprovalPolicyPullFailure(currentState, currentPolicy, config.riskApprovalPolicyEndpoint, normalizeError(error));
} finally {
riskApprovalPolicyPulling = false;
}
}
export async function acquireDigitalEmployeePlanStepRemoteLease(
input: DigitalEmployeePlanStepRemoteLeaseAcquireInput,
): Promise<DigitalEmployeePlanStepRemoteLeaseResult> {
@@ -577,6 +649,11 @@ function resolveSyncConfig(): DigitalEmployeeSyncConfig {
? config.skillPolicyEndpoint.trim()
: null;
const skillPolicyEndpoint = skillPolicyEndpointOverride || buildEndpoint(serverHost, DEFAULT_SKILL_POLICY_PATH);
const riskApprovalPolicyEndpointOverride =
typeof config.riskApprovalPolicyEndpoint === "string" && config.riskApprovalPolicyEndpoint.trim()
? config.riskApprovalPolicyEndpoint.trim()
: null;
const riskApprovalPolicyEndpoint = riskApprovalPolicyEndpointOverride || buildEndpoint(serverHost, DEFAULT_RISK_APPROVAL_POLICY_PATH);
const leaseEndpointOverride =
typeof config.planStepDispatchLeaseEndpoint === "string" && config.planStepDispatchLeaseEndpoint.trim()
? config.planStepDispatchLeaseEndpoint.trim()
@@ -593,6 +670,7 @@ function resolveSyncConfig(): DigitalEmployeeSyncConfig {
endpoint,
policyEndpoint,
skillPolicyEndpoint,
riskApprovalPolicyEndpoint,
leaseEndpoint,
clientKey: readClientKey(serverHost),
authToken: readAuthToken(serverHost),
@@ -746,6 +824,14 @@ function readRemoteSkillPolicyRecords(payload: Record<string, unknown>): Record<
?? (looksLikeSkillPolicyMap(payload) ? payload : null);
}
function readRemoteRiskApprovalPolicy(response: Record<string, unknown>): Record<string, unknown> | null {
const data = objectRecord(response.data) ?? response;
return objectRecord(data.risk_approval_policy)
?? objectRecord(data.riskApprovalPolicy)
?? objectRecord(data.policy)
?? (hasRiskApprovalPolicyFields(data) ? data : null);
}
function readPlanStepLeaseRejectionReason(response: Record<string, unknown>): string | null {
if (response.ok === false || response.success === false || response.accepted === false || response.granted === false) {
return stringField(response.reason ?? response.error ?? response.message) || "remote_lease_rejected";
@@ -813,6 +899,15 @@ function hasSkillPolicyWrapperFields(record: Record<string, unknown>): boolean {
|| "policies" in record;
}
function hasRiskApprovalPolicyFields(record: Record<string, unknown>): boolean {
return "approval_required_risk_levels" in record
|| "approvalRequiredRiskLevels" in record
|| "approval_required_actions" in record
|| "approvalRequiredActions" in record
|| "auto_reject_actions" in record
|| "autoRejectActions" in record;
}
function looksLikeSkillPolicyMap(record: Record<string, unknown>): boolean {
return Object.values(record).some((value) => {
if (typeof value === "boolean") return true;
@@ -865,6 +960,35 @@ function recordSkillPolicyPullFailure(
return { ok: false, endpoint, pulled_at: pulledAt, policies, error };
}
function recordRiskApprovalPolicyPullFailure(
state: ReturnType<typeof readDigitalEmployeeUiState>,
currentPolicy: DigitalEmployeeRiskApprovalPolicy,
endpoint: string | null,
error: string,
): DigitalEmployeeRiskApprovalPolicyPullResult {
const pulledAt = new Date().toISOString();
const policy = normalizeRiskApprovalPolicy({
...currentPolicy,
last_pulled_at: pulledAt,
last_pull_error: error,
});
saveDigitalEmployeeUiState({
action: "pull_risk_approval_policy",
metadata: {
source: "management-api",
endpoint,
ok: false,
error,
risk_approval_policy: policy,
},
state: {
...state,
riskApprovalPolicy: policy,
},
});
return { ok: false, endpoint, pulled_at: pulledAt, policy, error };
}
function buildSyncItem(row: OutboxRow): Record<string, unknown> {
const payload = parsePayload(row.payload);
const summary = readEntitySummary(row.entity_type, row.entity_id);
@@ -1058,6 +1182,7 @@ function eventSpecificBusinessView(
return artifactEventBusinessView(payload);
}
if (kind.startsWith("skill_call_")) return skillCallBusinessView(payload);
if (kind === "risk_approval_required" || kind === "risk_policy_rejected") return riskPolicyBusinessView(payload);
if (isPlanStepDependencyEvent(kind)) return planStepDependencyBusinessView(payload);
if (kind.startsWith("plan_step_dispatch_")) return planStepDispatchBusinessView(payload);
if (isPlanStepLeaseEvent(kind)) return planStepLeaseBusinessView(payload);
@@ -1132,6 +1257,24 @@ function approvalSubscriptionBusinessView(payload: Record<string, unknown>): Rec
};
}
function riskPolicyBusinessView(payload: Record<string, unknown>): Record<string, unknown> {
return {
action_kind: stringField(payload.action_kind ?? payload.actionKind) || null,
action_label: stringField(payload.action_label ?? payload.actionLabel) || null,
risk_level: stringField(payload.risk_level ?? payload.riskLevel) || null,
decision: stringField(payload.decision) || null,
status: stringField(payload.status) || null,
reason_codes: stringArrayField(payload.reason_codes ?? payload.reasonCodes),
policy_source: stringField(payload.policy_source ?? payload.policySource) || null,
approval_id: stringField(payload.approval_id ?? payload.approvalId) || null,
correlation_id: stringField(payload.correlation_id ?? payload.correlationId) || null,
actor: stringField(payload.actor) || null,
plan_id: stringField(payload.plan_id ?? payload.planId) || null,
task_id: stringField(payload.task_id ?? payload.taskId) || null,
run_id: stringField(payload.run_id ?? payload.runId) || null,
};
}
function artifactEventBusinessView(payload: Record<string, unknown>): Record<string, unknown> {
const retention = objectRecord(payload.retention_summary);
return {
@@ -1266,6 +1409,7 @@ function notificationBusinessView(payload: Record<string, unknown>): Record<stri
function eventBusinessCategory(kind: string): string {
if (kind === "route_decision") return "route";
if (kind.startsWith("approval_")) return "approval";
if (kind.startsWith("risk_")) return "approval";
if (isApprovalSubscriptionEvent(kind)) return "approval";
if (kind.startsWith("artifact_")) return "artifact";
if (kind.startsWith("skill_call_")) return "skill";