feat: 增加多账号备选退避机制支持 cloudSourceKeyFallbacks
- kuaishou-cloud-fulfillment-config-service: 新增 cloudSourceKeyFallbacks 字段和 normalizeStringArray 辅助函数 - kuaishou-cloud-task-service: 新增 resolvePersistedCloudtentaclesContextWithFallback 函数实现备选账号自动切换 - 改造 5 个调用点(prepare/refresh/roleRefresh/dispatch/return)使用 fallback 机制 - binding 中新增 resolvedSourceKey 字段持久化首次选定的 sourceKey - admin-write-inputs: 类型定义增加 cloudSourceKeyFallbacks 可选字段 - 向后兼容:旧数据无 fallbacks 字段时等同于旧行为
This commit is contained in:
@@ -101,6 +101,10 @@ export function normalizeKuaishouCloudFlow(value) {
|
||||
String(binding.prepareStatus || "pending").trim() || "pending",
|
||||
cloudSourceKey:
|
||||
String(binding.cloudSourceKey || "default").trim() || "default",
|
||||
cloudSourceKeyFallbacks: normalizeStringArray(
|
||||
binding.cloudSourceKeyFallbacks
|
||||
),
|
||||
resolvedSourceKey: String(binding.resolvedSourceKey || "").trim(),
|
||||
skuId: Number(binding.skuId || 0) || 0,
|
||||
skuName: String(binding.skuName || "").trim(),
|
||||
vnKey:
|
||||
@@ -219,6 +223,66 @@ export function resolvePersistedCloudtentaclesContext(sourceKey = "default") {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 按优先级依次尝试 cloudSourceKey 和 fallbacks 列表中的账号,
|
||||
* 找到第一个有可用 token 的账号返回其 context。
|
||||
* 将实际使用的 resolvedSourceKey 也返回,确保后续操作(退号、发货等)
|
||||
* 使用同一个 sourceKey,防止跨账号操作导致数据不一致。
|
||||
*/
|
||||
export function resolvePersistedCloudtentaclesContextWithFallback(
|
||||
primarySourceKey = "default",
|
||||
fallbacks = []
|
||||
) {
|
||||
const candidates = [
|
||||
String(primarySourceKey || "default").trim() || "default",
|
||||
...normalizeStringArray(fallbacks),
|
||||
];
|
||||
|
||||
// 去重但保持顺序
|
||||
const uniqueCandidates = [...new Set(candidates)];
|
||||
|
||||
let lastError = null;
|
||||
|
||||
for (const sourceKey of uniqueCandidates) {
|
||||
const source = getCloudtentaclesSourceByKey(sourceKey);
|
||||
const session = getCloudtentaclesSessionStateByKey(sourceKey);
|
||||
|
||||
// 该账号不存在配置 → 跳过
|
||||
if (!source) continue;
|
||||
|
||||
// 该账号没有 token → 标记跳过但不报错
|
||||
const token = String(session?.token || "").trim();
|
||||
if (!token) {
|
||||
lastError = createHttpError(
|
||||
`cloudtentacles 账号 ${sourceKey} 没有可用 token`,
|
||||
{ statusCode: 409, errorCode: "kuaishou_cloud_missing_cloud_token" }
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 找到可用账号
|
||||
return {
|
||||
baseUrl:
|
||||
String(session.baseUrl || source.baseUrl || "").trim() ||
|
||||
"https://123.207.217.176",
|
||||
token,
|
||||
deviceId:
|
||||
String(session.deviceId || source.deviceId || "-").trim() || "-",
|
||||
deviceType: Number(session.deviceType ?? source.deviceType ?? 0),
|
||||
resolvedSourceKey: sourceKey,
|
||||
};
|
||||
}
|
||||
|
||||
// 所有备选都不可用
|
||||
throw (
|
||||
lastError ||
|
||||
createHttpError(
|
||||
"所有 cloudtentacles 备选账号均不可用,请先到平台配置完成登录校验",
|
||||
{ statusCode: 409, errorCode: "kuaishou_cloud_all_source_keys_exhausted" }
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export async function ensureTaskClaimLink(task) {
|
||||
const tokenStatus = String(task?.primary_claim_token_status || "").trim();
|
||||
const token = String(
|
||||
@@ -292,8 +356,9 @@ export async function prepareKuaishouCloudFulfillmentTask(task, options = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
const cloudContext = resolvePersistedCloudtentaclesContext(
|
||||
flow.binding.cloudSourceKey || "default"
|
||||
const cloudContext = resolvePersistedCloudtentaclesContextWithFallback(
|
||||
flow.binding.cloudSourceKey || "default",
|
||||
flow.binding.cloudSourceKeyFallbacks || []
|
||||
);
|
||||
const [knapsack, skuList] = await Promise.all([
|
||||
getCloudtentaclesKnapsack(cloudContext),
|
||||
@@ -395,6 +460,7 @@ export async function prepareKuaishouCloudFulfillmentTask(task, options = {}) {
|
||||
...flowWithResolvedBinding,
|
||||
binding: {
|
||||
...flowWithResolvedBinding.binding,
|
||||
resolvedSourceKey: cloudContext.resolvedSourceKey,
|
||||
vnKey: preparedBinding.vnKey,
|
||||
prepareStatus: "ready",
|
||||
vnId: preparedBinding.vnId,
|
||||
@@ -488,8 +554,11 @@ export async function refreshKuaishouCloudTaskBindUrl(task, options = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
const cloudContext = resolvePersistedCloudtentaclesContext(
|
||||
flow.binding.cloudSourceKey || "default"
|
||||
const effectiveSourceKey =
|
||||
flow.binding.resolvedSourceKey || flow.binding.cloudSourceKey || "default";
|
||||
const cloudContext = resolvePersistedCloudtentaclesContextWithFallback(
|
||||
effectiveSourceKey,
|
||||
flow.binding.cloudSourceKeyFallbacks || []
|
||||
);
|
||||
const oldVnKey = flow.binding.vnKey;
|
||||
const oldVnId = flow.binding.vnId;
|
||||
@@ -851,8 +920,11 @@ export async function refreshKuaishouCloudTaskRoleInfo(task, options = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
const cloudContext = resolvePersistedCloudtentaclesContext(
|
||||
flow.binding.cloudSourceKey || "default"
|
||||
const effectiveSourceKey =
|
||||
flow.binding.resolvedSourceKey || flow.binding.cloudSourceKey || "default";
|
||||
const cloudContext = resolvePersistedCloudtentaclesContextWithFallback(
|
||||
effectiveSourceKey,
|
||||
flow.binding.cloudSourceKeyFallbacks || []
|
||||
);
|
||||
const bindInfoResult = await getCloudtentaclesBindInfo({
|
||||
...cloudContext,
|
||||
@@ -925,8 +997,11 @@ export async function dispatchKuaishouCloudFulfillmentTask(task, options = {}) {
|
||||
const now = nowIso();
|
||||
const taskContext = parseTaskContext(task);
|
||||
const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment);
|
||||
const cloudContext = resolvePersistedCloudtentaclesContext(
|
||||
flow.binding.cloudSourceKey || "default"
|
||||
const effectiveSourceKey =
|
||||
flow.binding.resolvedSourceKey || flow.binding.cloudSourceKey || "default";
|
||||
const cloudContext = resolvePersistedCloudtentaclesContextWithFallback(
|
||||
effectiveSourceKey,
|
||||
flow.binding.cloudSourceKeyFallbacks || []
|
||||
);
|
||||
|
||||
if (!flow.binding.skuId || !flow.binding.vnId || !flow.binding.vnPhone) {
|
||||
@@ -1054,8 +1129,11 @@ export async function returnKuaishouCloudFulfillmentTask(task, options = {}) {
|
||||
const now = nowIso();
|
||||
const taskContext = parseTaskContext(task);
|
||||
const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment);
|
||||
const cloudContext = resolvePersistedCloudtentaclesContext(
|
||||
flow.binding.cloudSourceKey || "default"
|
||||
const effectiveSourceKey =
|
||||
flow.binding.resolvedSourceKey || flow.binding.cloudSourceKey || "default";
|
||||
const cloudContext = resolvePersistedCloudtentaclesContextWithFallback(
|
||||
effectiveSourceKey,
|
||||
flow.binding.cloudSourceKeyFallbacks || []
|
||||
);
|
||||
|
||||
if (!flow.binding.vnId || !flow.binding.vnKey) {
|
||||
@@ -1561,3 +1639,16 @@ function isClaimExpired(expiredAt) {
|
||||
const timestamp = new Date(expiredAt).getTime();
|
||||
return Number.isFinite(timestamp) && timestamp <= Date.now();
|
||||
}
|
||||
|
||||
function normalizeStringArray(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((v) => String(v || "").trim()).filter(Boolean);
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
return value
|
||||
.split(",")
|
||||
.map((v) => v.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user