吸收数字员工技能远端策略下发
This commit is contained in:
@@ -1,16 +1,23 @@
|
||||
import log from "electron-log";
|
||||
import { getDeviceId } from "../system/deviceId";
|
||||
import { getDomainTokenKey } from "@shared/utils/domain";
|
||||
import { ensureDigitalEmployeeSchema, getDb, readSetting } from "../../db";
|
||||
import { ensureDigitalEmployeeSchema, getDb, readSetting, writeSetting } from "../../db";
|
||||
import {
|
||||
normalizePlanStepDispatchPolicy,
|
||||
readDigitalEmployeeUiState,
|
||||
saveDigitalEmployeeUiState,
|
||||
type DigitalEmployeePlanStepDispatchPolicy,
|
||||
} from "./stateService";
|
||||
import {
|
||||
SKILL_POLICIES_SETTING_KEY,
|
||||
normalizeDigitalEmployeeSkillPolicies,
|
||||
readDigitalEmployeeSkillPolicies,
|
||||
type DigitalEmployeeSkillPolicies,
|
||||
} from "./skillExecutionPolicy";
|
||||
|
||||
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_PLAN_STEP_DISPATCH_LEASE_PATH = "/api/digital-employee/leases/plan-step-dispatch";
|
||||
const MANAGEMENT_SYNC_RECORD_PATH =
|
||||
"/system/content/content-digital-employee-sync";
|
||||
@@ -29,6 +36,8 @@ let lastSyncAt: string | null = null;
|
||||
let lastSyncError: string | null = null;
|
||||
let policyPulling = false;
|
||||
let lastPolicyPullAttemptMs = 0;
|
||||
let skillPolicyPulling = false;
|
||||
let lastSkillPolicyPullAttemptMs = 0;
|
||||
|
||||
function getDigitalEmployeeDb(): ReturnType<typeof getDb> {
|
||||
if (!ensureDigitalEmployeeSchema()) return null;
|
||||
@@ -39,12 +48,22 @@ interface DigitalEmployeeSyncConfig {
|
||||
enabled: boolean;
|
||||
endpoint: string | null;
|
||||
policyEndpoint: string | null;
|
||||
skillPolicyEndpoint: string | null;
|
||||
leaseEndpoint: string | null;
|
||||
clientKey: string | null;
|
||||
authToken: string | null;
|
||||
batchSize: number;
|
||||
}
|
||||
|
||||
export interface DigitalEmployeeSkillPolicyPullResult {
|
||||
ok: boolean;
|
||||
skipped?: boolean;
|
||||
endpoint: string | null;
|
||||
pulled_at: string | null;
|
||||
policies: DigitalEmployeeSkillPolicies;
|
||||
error?: string | null;
|
||||
}
|
||||
|
||||
export interface DigitalEmployeePlanStepDispatchPolicyPullResult {
|
||||
ok: boolean;
|
||||
skipped?: boolean;
|
||||
@@ -289,6 +308,66 @@ export async function pullDigitalEmployeePlanStepDispatchPolicy(
|
||||
}
|
||||
}
|
||||
|
||||
export async function pullDigitalEmployeeSkillPolicies(
|
||||
options: { force?: boolean } = {},
|
||||
): Promise<DigitalEmployeeSkillPolicyPullResult> {
|
||||
const currentPolicies = readDigitalEmployeeSkillPolicies();
|
||||
if (skillPolicyPulling) {
|
||||
return {
|
||||
ok: true,
|
||||
skipped: true,
|
||||
endpoint: resolveSyncConfig().skillPolicyEndpoint,
|
||||
pulled_at: currentPolicies.last_pulled_at ?? null,
|
||||
policies: currentPolicies,
|
||||
};
|
||||
}
|
||||
const nowMs = Date.now();
|
||||
if (!options.force && nowMs - lastSkillPolicyPullAttemptMs < POLICY_PULL_THROTTLE_MS) {
|
||||
return {
|
||||
ok: true,
|
||||
skipped: true,
|
||||
endpoint: resolveSyncConfig().skillPolicyEndpoint,
|
||||
pulled_at: currentPolicies.last_pulled_at ?? null,
|
||||
policies: currentPolicies,
|
||||
};
|
||||
}
|
||||
lastSkillPolicyPullAttemptMs = nowMs;
|
||||
|
||||
const config = resolveSyncConfig();
|
||||
const missingCredentials = missingSyncCredentialLabels(config);
|
||||
if (!config.skillPolicyEndpoint || missingCredentials.length > 0) {
|
||||
const error = !config.skillPolicyEndpoint ? "management skill policy endpoint unavailable" : `missing credentials: ${missingCredentials.join(", ")}`;
|
||||
return recordSkillPolicyPullFailure(currentPolicies, config.skillPolicyEndpoint, error);
|
||||
}
|
||||
|
||||
skillPolicyPulling = true;
|
||||
try {
|
||||
const response = await fetchJsonWithAuth(config, config.skillPolicyEndpoint);
|
||||
const responseError = readSyncResponseError(response as SyncResponse);
|
||||
if (responseError) throw new Error(responseError);
|
||||
const remotePayload = readRemoteSkillPolicyPayload(response);
|
||||
if (!remotePayload) throw new Error("management policy response missing skill policies");
|
||||
const remoteSkills = readRemoteSkillPolicyRecords(remotePayload);
|
||||
if (!remoteSkills) throw new Error("management policy response missing skill policy records");
|
||||
const pulledAt = new Date().toISOString();
|
||||
const policies = normalizeDigitalEmployeeSkillPolicies({
|
||||
version: 1,
|
||||
local_skills: currentPolicies.local_skills,
|
||||
remote_skills: remoteSkills,
|
||||
source: "remote",
|
||||
remote_updated_at: stringField(remotePayload.updated_at) || stringField(remotePayload.updatedAt) || stringField(remotePayload.remote_updated_at) || null,
|
||||
last_pulled_at: pulledAt,
|
||||
last_pull_error: null,
|
||||
});
|
||||
writeSetting(SKILL_POLICIES_SETTING_KEY, policies);
|
||||
return { ok: true, endpoint: config.skillPolicyEndpoint, pulled_at: pulledAt, policies, error: null };
|
||||
} catch (error) {
|
||||
return recordSkillPolicyPullFailure(currentPolicies, config.skillPolicyEndpoint, normalizeError(error));
|
||||
} finally {
|
||||
skillPolicyPulling = false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function acquireDigitalEmployeePlanStepRemoteLease(
|
||||
input: DigitalEmployeePlanStepRemoteLeaseAcquireInput,
|
||||
): Promise<DigitalEmployeePlanStepRemoteLeaseResult> {
|
||||
@@ -493,6 +572,11 @@ function resolveSyncConfig(): DigitalEmployeeSyncConfig {
|
||||
? config.policyEndpoint.trim()
|
||||
: null;
|
||||
const policyEndpoint = policyEndpointOverride || buildEndpoint(serverHost, DEFAULT_PLAN_STEP_DISPATCH_POLICY_PATH);
|
||||
const skillPolicyEndpointOverride =
|
||||
typeof config.skillPolicyEndpoint === "string" && config.skillPolicyEndpoint.trim()
|
||||
? config.skillPolicyEndpoint.trim()
|
||||
: null;
|
||||
const skillPolicyEndpoint = skillPolicyEndpointOverride || buildEndpoint(serverHost, DEFAULT_SKILL_POLICY_PATH);
|
||||
const leaseEndpointOverride =
|
||||
typeof config.planStepDispatchLeaseEndpoint === "string" && config.planStepDispatchLeaseEndpoint.trim()
|
||||
? config.planStepDispatchLeaseEndpoint.trim()
|
||||
@@ -508,6 +592,7 @@ function resolveSyncConfig(): DigitalEmployeeSyncConfig {
|
||||
enabled: !disabled,
|
||||
endpoint,
|
||||
policyEndpoint,
|
||||
skillPolicyEndpoint,
|
||||
leaseEndpoint,
|
||||
clientKey: readClientKey(serverHost),
|
||||
authToken: readAuthToken(serverHost),
|
||||
@@ -645,6 +730,22 @@ function readRemotePlanStepDispatchPolicy(response: Record<string, unknown>): Re
|
||||
?? (hasPlanStepDispatchPolicyFields(data) ? data : null);
|
||||
}
|
||||
|
||||
function readRemoteSkillPolicyPayload(response: Record<string, unknown>): Record<string, unknown> | null {
|
||||
const data = objectRecord(response.data) ?? response;
|
||||
return objectRecord(data.skill_policies)
|
||||
?? objectRecord(data.skillPolicies)
|
||||
?? objectRecord(data.policies)
|
||||
?? objectRecord(data.policy)
|
||||
?? (hasSkillPolicyWrapperFields(data) || looksLikeSkillPolicyMap(data) ? data : null);
|
||||
}
|
||||
|
||||
function readRemoteSkillPolicyRecords(payload: Record<string, unknown>): Record<string, unknown> | null {
|
||||
return objectRecord(payload.remote_skills)
|
||||
?? objectRecord(payload.remoteSkills)
|
||||
?? objectRecord(payload.skills)
|
||||
?? (looksLikeSkillPolicyMap(payload) ? payload : 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";
|
||||
@@ -703,6 +804,23 @@ function hasPlanStepDispatchPolicyFields(record: Record<string, unknown>): boole
|
||||
return "enabled" in record || "max_per_sweep" in record || "auto_dispatch_ready_steps" in record;
|
||||
}
|
||||
|
||||
function hasSkillPolicyWrapperFields(record: Record<string, unknown>): boolean {
|
||||
return "remote_skills" in record
|
||||
|| "remoteSkills" in record
|
||||
|| "skills" in record
|
||||
|| "skill_policies" in record
|
||||
|| "skillPolicies" in record
|
||||
|| "policies" in record;
|
||||
}
|
||||
|
||||
function looksLikeSkillPolicyMap(record: Record<string, unknown>): boolean {
|
||||
return Object.values(record).some((value) => {
|
||||
if (typeof value === "boolean") return true;
|
||||
const policy = objectRecord(value);
|
||||
return Boolean(policy && typeof policy.enabled === "boolean");
|
||||
});
|
||||
}
|
||||
|
||||
function recordPlanStepDispatchPolicyPullFailure(
|
||||
state: ReturnType<typeof readDigitalEmployeeUiState>,
|
||||
currentPolicy: DigitalEmployeePlanStepDispatchPolicy,
|
||||
@@ -732,6 +850,21 @@ function recordPlanStepDispatchPolicyPullFailure(
|
||||
return { ok: false, endpoint, pulled_at: pulledAt, policy, error };
|
||||
}
|
||||
|
||||
function recordSkillPolicyPullFailure(
|
||||
currentPolicies: DigitalEmployeeSkillPolicies,
|
||||
endpoint: string | null,
|
||||
error: string,
|
||||
): DigitalEmployeeSkillPolicyPullResult {
|
||||
const pulledAt = new Date().toISOString();
|
||||
const policies = normalizeDigitalEmployeeSkillPolicies({
|
||||
...currentPolicies,
|
||||
last_pulled_at: pulledAt,
|
||||
last_pull_error: error,
|
||||
});
|
||||
writeSetting(SKILL_POLICIES_SETTING_KEY, policies);
|
||||
return { ok: false, endpoint, pulled_at: pulledAt, policies, error };
|
||||
}
|
||||
|
||||
function buildSyncItem(row: OutboxRow): Record<string, unknown> {
|
||||
const payload = parsePayload(row.payload);
|
||||
const summary = readEntitySummary(row.entity_type, row.entity_id);
|
||||
|
||||
Reference in New Issue
Block a user