彻底修复 lewan 自动发货问题-8-8

This commit is contained in:
yml2213
2026-08-08 23:08:05 +08:00
parent 7196752d44
commit 6e41d7e0db
10 changed files with 260 additions and 37 deletions
@@ -18,6 +18,7 @@ import {
isKuaishouCloudTask,
maskPhone,
normalizeKuaishouCloudFlow,
normalizeStringArray,
resolveKuaishouCloudBindUrlExpiresAt,
type JsonObject,
} from './domain.js'
@@ -123,7 +124,16 @@ export async function prepareKuaishouCloudFulfillmentTask(task: TaskRow, options
})
}
const selectedCloud = await selectCloudtentaclesSourceForFulfillment(flow)
const excludedSourceKeys = normalizeStringArray(options.excludedSourceKeys)
const sourceSelectionAttempts = normalizeStringArray(options.sourceSelectionAttempts)
const sourceCandidateCount = new Set(
normalizeStringArray([...flow.binding.cloudSourceKeys, flow.binding.resolvedSourceKey]),
).size
const selectedCloud = await selectCloudtentaclesSourceForFulfillment({
...flow,
excludedSourceKeys,
})
let retriedOnAnotherSource = false
try {
const cloudContext = selectedCloud.context
const [knapsack, skuList] = await Promise.all([
@@ -277,7 +287,10 @@ export async function prepareKuaishouCloudFulfillmentTask(task: TaskRow, options
purchasedCount: 0,
})),
resolvedByName: resolvedBinding.resolvedByName,
accountSelection: buildCloudtentaclesSourceSelectionEventDetail(selectedCloud),
accountSelection: {
...buildCloudtentaclesSourceSelectionEventDetail(selectedCloud),
attemptedSourceKeys: [...sourceSelectionAttempts, selectedCloud.sourceKey],
},
defaultRoleName: defaultRoleSnapshot.defaultName,
defaultRoleId: defaultRoleSnapshot.defaultRid,
defaultRoleCaptureStatus: defaultRoleSnapshot.defaultCaptureStatus,
@@ -293,11 +306,43 @@ export async function prepareKuaishouCloudFulfillmentTask(task: TaskRow, options
flow: normalizeKuaishouCloudFlow(nextContext.kuaishouCloudFulfillment),
previousVnRelease,
}
} catch (error) {
// Before a VN exists, account-scoped failures are safe to retry on another
// candidate. Once a VN is acquired, switching accounts would orphan it.
if (
flow.binding.vnId <= 0 &&
isRetryableCloudtentaclesSourceError(error) &&
excludedSourceKeys.length + 1 < sourceCandidateCount
) {
retriedOnAnotherSource = true
selectedCloud.release()
return prepareKuaishouCloudFulfillmentTask(task, {
...options,
excludedSourceKeys: [...excludedSourceKeys, selectedCloud.sourceKey],
sourceSelectionAttempts: [...sourceSelectionAttempts, selectedCloud.sourceKey],
})
}
throw error
} finally {
selectedCloud.release()
if (!retriedOnAnotherSource) {
selectedCloud.release()
}
}
}
function isRetryableCloudtentaclesSourceError(error: unknown) {
const current = error && typeof error === 'object' ? error as JsonObject : {}
const code = String(current.errorCode || current.code || '').trim()
return (
code === 'cloudtentacles_vn_quota_cooldown' ||
(code === 'cloudtentacles_vn_appoint_failed' && String(current.message || '').trim().includes('最多同时占用')) ||
code === 'cloudtentacles_vn_list_failed' ||
code === 'cloudtentacles_sku_list_failed' ||
code === 'cloudtentacles_knapsack_failed' ||
code === 'cloudtentacles_asset_failed'
)
}
/** 旧虚拟号 best-effort 释放:已被回收/权限不足时不算失败 */
async function tryReleasePreviousVirtualNumber({
task,