吸收数字员工PlanStep管理端强租约
This commit is contained in:
@@ -11,6 +11,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_PLAN_STEP_DISPATCH_LEASE_PATH = "/api/digital-employee/leases/plan-step-dispatch";
|
||||
const MANAGEMENT_SYNC_RECORD_PATH =
|
||||
"/system/content/content-digital-employee-sync";
|
||||
const DIGITAL_EMPLOYEE_SYNC_CONTRACT_VERSION = "qimingclaw.digital_employee.sync.v1";
|
||||
@@ -38,6 +39,7 @@ interface DigitalEmployeeSyncConfig {
|
||||
enabled: boolean;
|
||||
endpoint: string | null;
|
||||
policyEndpoint: string | null;
|
||||
leaseEndpoint: string | null;
|
||||
clientKey: string | null;
|
||||
authToken: string | null;
|
||||
batchSize: number;
|
||||
@@ -52,6 +54,48 @@ export interface DigitalEmployeePlanStepDispatchPolicyPullResult {
|
||||
error?: string | null;
|
||||
}
|
||||
|
||||
export interface DigitalEmployeePlanStepRemoteLease {
|
||||
lease_id: string;
|
||||
state: "active" | "released" | "expired";
|
||||
owner_device_id: string;
|
||||
acquired_at: string;
|
||||
expires_at: string;
|
||||
released_at?: string | null;
|
||||
release_reason?: string | null;
|
||||
remote_lease_id?: string | null;
|
||||
remote_owner_device_id?: string | null;
|
||||
}
|
||||
|
||||
export interface DigitalEmployeePlanStepRemoteLeaseResult {
|
||||
ok: boolean;
|
||||
rejected?: boolean;
|
||||
fallback?: boolean;
|
||||
endpoint: string | null;
|
||||
lease?: DigitalEmployeePlanStepRemoteLease | null;
|
||||
reason?: string | null;
|
||||
error?: string | null;
|
||||
}
|
||||
|
||||
export interface DigitalEmployeePlanStepRemoteLeaseAcquireInput {
|
||||
planId?: string | null;
|
||||
stepId: string;
|
||||
taskId?: string | null;
|
||||
triggerSource?: string | null;
|
||||
ttlMs?: number | null;
|
||||
occurredAt?: string | null;
|
||||
}
|
||||
|
||||
export interface DigitalEmployeePlanStepRemoteLeaseReleaseInput {
|
||||
planId?: string | null;
|
||||
stepId: string;
|
||||
taskId?: string | null;
|
||||
runId?: string | null;
|
||||
leaseId?: string | null;
|
||||
remoteLeaseId?: string | null;
|
||||
reason?: string | null;
|
||||
occurredAt?: string | null;
|
||||
}
|
||||
|
||||
interface OutboxRow {
|
||||
id: string;
|
||||
entity_type: string;
|
||||
@@ -245,6 +289,80 @@ export async function pullDigitalEmployeePlanStepDispatchPolicy(
|
||||
}
|
||||
}
|
||||
|
||||
export async function acquireDigitalEmployeePlanStepRemoteLease(
|
||||
input: DigitalEmployeePlanStepRemoteLeaseAcquireInput,
|
||||
): Promise<DigitalEmployeePlanStepRemoteLeaseResult> {
|
||||
const config = resolveSyncConfig();
|
||||
const endpoint = buildLeaseOperationEndpoint(config.leaseEndpoint, "acquire");
|
||||
const missingCredentials = missingSyncCredentialLabels(config);
|
||||
if (!endpoint || missingCredentials.length > 0) {
|
||||
return {
|
||||
ok: false,
|
||||
fallback: true,
|
||||
endpoint,
|
||||
error: !endpoint ? "management lease endpoint unavailable" : `missing credentials: ${missingCredentials.join(", ")}`,
|
||||
};
|
||||
}
|
||||
const occurredAt = input.occurredAt || new Date().toISOString();
|
||||
const ttlMs = Math.max(60_000, Math.min(Number(input.ttlMs) || 5 * 60_000, 30 * 60_000));
|
||||
try {
|
||||
const response = await fetchJsonWithAuth(config, endpoint, {
|
||||
client_key: config.clientKey,
|
||||
device_id: getDeviceId(),
|
||||
plan_id: stringField(input.planId) || null,
|
||||
step_id: input.stepId,
|
||||
task_id: stringField(input.taskId) || null,
|
||||
trigger_source: stringField(input.triggerSource) || null,
|
||||
ttl_ms: ttlMs,
|
||||
occurred_at: occurredAt,
|
||||
});
|
||||
const rejectedReason = readPlanStepLeaseRejectionReason(response);
|
||||
if (rejectedReason) return { ok: false, rejected: true, endpoint, reason: rejectedReason, lease: readRemotePlanStepLease(response, occurredAt, ttlMs) };
|
||||
const responseError = readSyncResponseError(response as SyncResponse);
|
||||
if (responseError) throw new Error(responseError);
|
||||
const lease = readRemotePlanStepLease(response, occurredAt, ttlMs);
|
||||
if (!lease) throw new Error("management lease response missing plan step lease");
|
||||
return { ok: true, endpoint, lease };
|
||||
} catch (error) {
|
||||
return { ok: false, fallback: true, endpoint, error: normalizeError(error) };
|
||||
}
|
||||
}
|
||||
|
||||
export async function releaseDigitalEmployeePlanStepRemoteLease(
|
||||
input: DigitalEmployeePlanStepRemoteLeaseReleaseInput,
|
||||
): Promise<DigitalEmployeePlanStepRemoteLeaseResult> {
|
||||
const config = resolveSyncConfig();
|
||||
const endpoint = buildLeaseOperationEndpoint(config.leaseEndpoint, "release");
|
||||
const missingCredentials = missingSyncCredentialLabels(config);
|
||||
if (!endpoint || missingCredentials.length > 0) {
|
||||
return {
|
||||
ok: false,
|
||||
endpoint,
|
||||
error: !endpoint ? "management lease endpoint unavailable" : `missing credentials: ${missingCredentials.join(", ")}`,
|
||||
};
|
||||
}
|
||||
const occurredAt = input.occurredAt || new Date().toISOString();
|
||||
try {
|
||||
const response = await fetchJsonWithAuth(config, endpoint, {
|
||||
client_key: config.clientKey,
|
||||
device_id: getDeviceId(),
|
||||
plan_id: stringField(input.planId) || null,
|
||||
step_id: input.stepId,
|
||||
task_id: stringField(input.taskId) || null,
|
||||
run_id: stringField(input.runId) || null,
|
||||
lease_id: stringField(input.leaseId) || null,
|
||||
remote_lease_id: stringField(input.remoteLeaseId) || stringField(input.leaseId) || null,
|
||||
reason: stringField(input.reason) || null,
|
||||
occurred_at: occurredAt,
|
||||
});
|
||||
const responseError = readSyncResponseError(response as SyncResponse);
|
||||
if (responseError) throw new Error(responseError);
|
||||
return { ok: true, endpoint, lease: readRemotePlanStepLease(response, occurredAt, 0) };
|
||||
} catch (error) {
|
||||
return { ok: false, endpoint, error: normalizeError(error) };
|
||||
}
|
||||
}
|
||||
|
||||
export async function flushDigitalEmployeeSyncOutbox(
|
||||
options: { force?: boolean } = {},
|
||||
): Promise<DigitalEmployeeSyncStatus> {
|
||||
@@ -375,6 +493,11 @@ function resolveSyncConfig(): DigitalEmployeeSyncConfig {
|
||||
? config.policyEndpoint.trim()
|
||||
: null;
|
||||
const policyEndpoint = policyEndpointOverride || buildEndpoint(serverHost, DEFAULT_PLAN_STEP_DISPATCH_POLICY_PATH);
|
||||
const leaseEndpointOverride =
|
||||
typeof config.planStepDispatchLeaseEndpoint === "string" && config.planStepDispatchLeaseEndpoint.trim()
|
||||
? config.planStepDispatchLeaseEndpoint.trim()
|
||||
: null;
|
||||
const leaseEndpoint = leaseEndpointOverride || buildEndpoint(serverHost, DEFAULT_PLAN_STEP_DISPATCH_LEASE_PATH);
|
||||
const disabled = config.enabled === false;
|
||||
const batchSize =
|
||||
typeof config.batchSize === "number" && config.batchSize > 0
|
||||
@@ -385,12 +508,18 @@ function resolveSyncConfig(): DigitalEmployeeSyncConfig {
|
||||
enabled: !disabled,
|
||||
endpoint,
|
||||
policyEndpoint,
|
||||
leaseEndpoint,
|
||||
clientKey: readClientKey(serverHost),
|
||||
authToken: readAuthToken(serverHost),
|
||||
batchSize,
|
||||
};
|
||||
}
|
||||
|
||||
function buildLeaseOperationEndpoint(baseEndpoint: string | null, operation: "acquire" | "release"): string | null {
|
||||
if (!baseEndpoint) return null;
|
||||
return `${baseEndpoint.replace(/\/+$/, "")}/${operation}`;
|
||||
}
|
||||
|
||||
function buildEndpoint(serverHost: string | null, path = DEFAULT_SYNC_PATH): string | null {
|
||||
if (!serverHost) return null;
|
||||
const normalized = serverHost.startsWith("http")
|
||||
@@ -516,6 +645,60 @@ function readRemotePlanStepDispatchPolicy(response: Record<string, unknown>): Re
|
||||
?? (hasPlanStepDispatchPolicyFields(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";
|
||||
}
|
||||
const data = objectRecord(response.data);
|
||||
if (data && (data.ok === false || data.success === false || data.accepted === false || data.granted === false)) {
|
||||
return stringField(data.reason ?? data.error ?? data.message) || "remote_lease_rejected";
|
||||
}
|
||||
const status = normalizeResponseCode(response.status ?? data?.status);
|
||||
if (status && ["rejected", "denied", "conflict", "locked"].includes(status.toLowerCase())) {
|
||||
return stringField(response.reason ?? response.message ?? data?.reason ?? data?.message) || "remote_lease_rejected";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function readRemotePlanStepLease(
|
||||
response: Record<string, unknown>,
|
||||
occurredAt: string,
|
||||
ttlMs: number,
|
||||
): DigitalEmployeePlanStepRemoteLease | null {
|
||||
const data = objectRecord(response.data) ?? response;
|
||||
const lease = objectRecord(data.lease)
|
||||
?? objectRecord(data.plan_step_dispatch_lease)
|
||||
?? objectRecord(data.planStepDispatchLease)
|
||||
?? (hasPlanStepLeaseFields(data) ? data : null);
|
||||
if (!lease) return null;
|
||||
const remoteLeaseId = stringField(lease.remote_lease_id ?? lease.remoteLeaseId ?? lease.lease_id ?? lease.leaseId);
|
||||
const leaseId = stringField(lease.lease_id ?? lease.leaseId) || remoteLeaseId;
|
||||
const ownerDeviceId = stringField(lease.owner_device_id ?? lease.ownerDeviceId) || getDeviceId();
|
||||
const expiresAt = stringField(lease.expires_at ?? lease.expiresAt)
|
||||
|| (ttlMs > 0 ? new Date(new Date(occurredAt).getTime() + ttlMs).toISOString() : occurredAt);
|
||||
if (!leaseId || !ownerDeviceId || !expiresAt) return null;
|
||||
return {
|
||||
lease_id: leaseId,
|
||||
state: normalizePlanStepRemoteLeaseState(lease.state),
|
||||
owner_device_id: ownerDeviceId,
|
||||
acquired_at: stringField(lease.acquired_at ?? lease.acquiredAt) || occurredAt,
|
||||
expires_at: expiresAt,
|
||||
released_at: stringField(lease.released_at ?? lease.releasedAt) || null,
|
||||
release_reason: stringField(lease.release_reason ?? lease.releaseReason) || null,
|
||||
remote_lease_id: remoteLeaseId || leaseId,
|
||||
remote_owner_device_id: stringField(lease.remote_owner_device_id ?? lease.remoteOwnerDeviceId ?? lease.owner_device_id ?? lease.ownerDeviceId) || null,
|
||||
};
|
||||
}
|
||||
|
||||
function hasPlanStepLeaseFields(record: Record<string, unknown>): boolean {
|
||||
return "lease_id" in record || "leaseId" in record || "remote_lease_id" in record || "remoteLeaseId" in record;
|
||||
}
|
||||
|
||||
function normalizePlanStepRemoteLeaseState(value: unknown): DigitalEmployeePlanStepRemoteLease["state"] {
|
||||
if (value === "released" || value === "expired") return value;
|
||||
return "active";
|
||||
}
|
||||
|
||||
function hasPlanStepDispatchPolicyFields(record: Record<string, unknown>): boolean {
|
||||
return "enabled" in record || "max_per_sweep" in record || "auto_dispatch_ready_steps" in record;
|
||||
}
|
||||
@@ -675,6 +858,11 @@ function planStepBusinessView(
|
||||
lease_expires_at: leaseExpiresAt,
|
||||
lease_released_at: stringField(dispatchLease.released_at ?? dispatchLease.releasedAt) || null,
|
||||
lease_release_reason: stringField(dispatchLease.release_reason ?? dispatchLease.releaseReason) || null,
|
||||
lease_source: stringField(dispatchLease.source) || null,
|
||||
remote_lease_id: stringField(dispatchLease.remote_lease_id ?? dispatchLease.remoteLeaseId) || null,
|
||||
remote_lease_error: stringField(dispatchLease.remote_error ?? dispatchLease.remoteError) || null,
|
||||
remote_release_error: stringField(dispatchLease.remote_release_error ?? dispatchLease.remoteReleaseError) || null,
|
||||
remote_lease_rejected: skipReason === "remote_lease_rejected",
|
||||
lease_active: leaseActive,
|
||||
};
|
||||
}
|
||||
@@ -862,6 +1050,11 @@ function planStepLeaseBusinessView(payload: Record<string, unknown>): Record<str
|
||||
lease_expires_at: stringField(payload.lease_expires_at ?? payload.leaseExpiresAt ?? lease.expires_at ?? lease.expiresAt) || null,
|
||||
lease_released_at: stringField(lease.released_at ?? lease.releasedAt) || null,
|
||||
lease_release_reason: stringField(lease.release_reason ?? lease.releaseReason) || null,
|
||||
lease_source: stringField(payload.lease_source ?? payload.leaseSource ?? lease.source) || null,
|
||||
remote_lease_id: stringField(payload.remote_lease_id ?? payload.remoteLeaseId ?? lease.remote_lease_id ?? lease.remoteLeaseId) || null,
|
||||
remote_lease_error: stringField(payload.remote_lease_error ?? payload.remoteLeaseError ?? lease.remote_error ?? lease.remoteError) || null,
|
||||
remote_release_error: stringField(payload.remote_release_error ?? payload.remoteReleaseError ?? lease.remote_release_error ?? lease.remoteReleaseError) || null,
|
||||
remote_lease_rejected: payload.remote_lease_rejected === true || stringField(payload.reason) === "remote_lease_rejected",
|
||||
reason: stringField(payload.reason) || null,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user