feat: add dispatch retry and import recovery flows
This commit is contained in:
@@ -5564,42 +5564,86 @@ function upsertDeviceImportDraftFromHeartbeat(
|
||||
candidates: DeviceImportCandidate[];
|
||||
},
|
||||
) {
|
||||
const existing = state.deviceImportDrafts.find((item) => item.deviceId === payload.deviceId);
|
||||
if (payload.candidates.length === 0) {
|
||||
return null;
|
||||
if (existing?.status === "applied" && existing.appliedProjectNames.length > 0) {
|
||||
return existing;
|
||||
}
|
||||
const waitingDraft = normalizeDeviceImportDraft({
|
||||
draftId: existing?.draftId ?? randomToken("import-draft"),
|
||||
deviceId: payload.deviceId,
|
||||
enrollmentId: payload.enrollmentId ?? existing?.enrollmentId,
|
||||
status: "pending_candidates",
|
||||
candidates: [],
|
||||
selectedCandidateIds: [],
|
||||
appliedProjectNames: [],
|
||||
createdAt: existing?.createdAt ?? nowIso(),
|
||||
updatedAt: nowIso(),
|
||||
}, existing);
|
||||
waitingDraft.reviewedAt = undefined;
|
||||
waitingDraft.reviewedBy = undefined;
|
||||
waitingDraft.resolutionId = undefined;
|
||||
state.deviceImportResolutions = state.deviceImportResolutions.filter(
|
||||
(item) => item.draftId !== waitingDraft.draftId,
|
||||
);
|
||||
state.deviceImportDrafts = [
|
||||
waitingDraft,
|
||||
...state.deviceImportDrafts.filter((item) => item.draftId !== waitingDraft.draftId),
|
||||
];
|
||||
return waitingDraft;
|
||||
}
|
||||
|
||||
const existing = state.deviceImportDrafts.find((item) => item.deviceId === payload.deviceId);
|
||||
const selectedCandidateIds = dedupeStrings(
|
||||
(existing?.selectedCandidateIds ?? []).filter((candidateId) =>
|
||||
payload.candidates.some((candidate) => candidate.candidateId === candidateId),
|
||||
),
|
||||
);
|
||||
const previousCandidateIds = existing?.candidates.map((candidate) => candidate.candidateId) ?? [];
|
||||
const nextCandidateIds = payload.candidates.map((candidate) => candidate.candidateId);
|
||||
const selectionChanged =
|
||||
!sameStringSet(existing?.selectedCandidateIds ?? [], selectedCandidateIds) ||
|
||||
!sameStringSet(previousCandidateIds, nextCandidateIds);
|
||||
const keepAppliedState =
|
||||
!selectionChanged &&
|
||||
existing?.status === "applied" &&
|
||||
Boolean(existing.resolutionId) &&
|
||||
selectedCandidateIds.length > 0;
|
||||
const keepResolvedState =
|
||||
!selectionChanged &&
|
||||
selectedCandidateIds.length > 0 &&
|
||||
Boolean(existing?.resolutionId);
|
||||
|
||||
const nextDraft = normalizeDeviceImportDraft({
|
||||
draftId: existing?.draftId ?? randomToken("import-draft"),
|
||||
deviceId: payload.deviceId,
|
||||
enrollmentId: payload.enrollmentId ?? existing?.enrollmentId,
|
||||
status:
|
||||
existing?.status === "applied" && existing.resolutionId && selectedCandidateIds.length > 0
|
||||
keepAppliedState
|
||||
? "applied"
|
||||
: selectedCandidateIds.length > 0
|
||||
? existing?.resolutionId
|
||||
? keepResolvedState
|
||||
? "resolved"
|
||||
: "pending_resolution"
|
||||
: "pending_selection",
|
||||
candidates: payload.candidates,
|
||||
selectedCandidateIds,
|
||||
appliedProjectNames:
|
||||
existing?.status === "applied" && selectedCandidateIds.length > 0
|
||||
keepAppliedState
|
||||
? existing.appliedProjectNames
|
||||
: [],
|
||||
createdAt: existing?.createdAt ?? nowIso(),
|
||||
updatedAt: nowIso(),
|
||||
reviewedAt: existing?.reviewedAt,
|
||||
reviewedBy: existing?.reviewedBy,
|
||||
resolutionId: existing?.resolutionId,
|
||||
reviewedAt: keepResolvedState || keepAppliedState ? existing?.reviewedAt : undefined,
|
||||
reviewedBy: keepResolvedState || keepAppliedState ? existing?.reviewedBy : undefined,
|
||||
resolutionId: keepResolvedState || keepAppliedState ? existing?.resolutionId : undefined,
|
||||
}, existing);
|
||||
|
||||
if (!keepResolvedState && !keepAppliedState) {
|
||||
state.deviceImportResolutions = state.deviceImportResolutions.filter(
|
||||
(item) => item.draftId !== nextDraft.draftId,
|
||||
);
|
||||
}
|
||||
|
||||
state.deviceImportDrafts = [
|
||||
nextDraft,
|
||||
...state.deviceImportDrafts.filter((item) => item.draftId !== nextDraft.draftId),
|
||||
@@ -5652,7 +5696,8 @@ export async function upsertDeviceHeartbeat(payload: {
|
||||
suggestedImport: candidate.suggestedImport ?? true,
|
||||
}),
|
||||
);
|
||||
const shouldAutoImportLegacyProjects = normalizedCandidates.length === 0;
|
||||
const reportedProjectCandidates = Array.isArray(payload.projectCandidates);
|
||||
const shouldAutoImportLegacyProjects = !reportedProjectCandidates && normalizedCandidates.length === 0;
|
||||
|
||||
let device = existingDevice;
|
||||
if (!device) {
|
||||
@@ -5969,11 +6014,8 @@ export async function selectDeviceImportCandidates(input: {
|
||||
const nextSelected = dedupeStrings(input.selectedCandidateIds).filter((candidateId) =>
|
||||
availableCandidateIds.has(candidateId),
|
||||
);
|
||||
if (nextSelected.length === 0) {
|
||||
throw new Error("DEVICE_IMPORT_SELECTION_REQUIRED");
|
||||
}
|
||||
draft.selectedCandidateIds = nextSelected;
|
||||
draft.status = "pending_resolution";
|
||||
draft.status = nextSelected.length > 0 ? "pending_resolution" : "pending_selection";
|
||||
draft.appliedProjectNames = [];
|
||||
draft.updatedAt = nowIso();
|
||||
draft.reviewedBy = input.selectedBy;
|
||||
@@ -6202,6 +6244,23 @@ function applyDeviceImportResolutionInState(
|
||||
const device = state.devices.find((item) => item.id === input.deviceId);
|
||||
if (!device) throw new Error("DEVICE_NOT_FOUND");
|
||||
|
||||
if (draft.status === "applied" && resolution.status === "applied") {
|
||||
const importedProjects = state.projects.filter(
|
||||
(project) =>
|
||||
!project.isGroup &&
|
||||
project.deviceIds.includes(device.id) &&
|
||||
draft.appliedProjectNames.includes(project.name),
|
||||
);
|
||||
return {
|
||||
draft: { ...draft },
|
||||
resolution: { ...resolution },
|
||||
importedProjects: importedProjects.map((project) => ({ ...project })),
|
||||
};
|
||||
}
|
||||
if (draft.status !== "resolved") {
|
||||
throw new Error("DEVICE_IMPORT_RESOLUTION_STALE");
|
||||
}
|
||||
|
||||
const selectedCandidates = draft.candidates.filter((candidate) =>
|
||||
draft.selectedCandidateIds.includes(candidate.candidateId),
|
||||
);
|
||||
|
||||
@@ -14,6 +14,10 @@ export function latestPendingDispatchPlan(plans: DispatchPlanUiPayload[] | null
|
||||
return (plans ?? []).find((plan) => plan.status === "pending_user_confirmation") ?? null;
|
||||
}
|
||||
|
||||
export function latestRejectedDispatchPlan(plans: DispatchPlanUiPayload[] | null | undefined) {
|
||||
return (plans ?? []).find((plan) => plan.status === "rejected") ?? null;
|
||||
}
|
||||
|
||||
export function summarizeDispatchPlan(plan: DispatchPlanUiPayload | null | undefined) {
|
||||
if (!plan) {
|
||||
return "主 Agent 暂未生成推荐线程。";
|
||||
|
||||
Reference in New Issue
Block a user