吸收数字员工技能远端策略下发

This commit is contained in:
baiyanyun
2026-06-11 09:04:08 +08:00
parent 751ce45619
commit 8ab3d73fb1
18 changed files with 876 additions and 207 deletions

View File

@@ -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);