diff --git a/apps/backend/src/services/fulfillment/kuaishou-cloud/account-selector.ts b/apps/backend/src/services/fulfillment/kuaishou-cloud/account-selector.ts index 5165f2ed..194e3f90 100644 --- a/apps/backend/src/services/fulfillment/kuaishou-cloud/account-selector.ts +++ b/apps/backend/src/services/fulfillment/kuaishou-cloud/account-selector.ts @@ -1,11 +1,11 @@ import { listKuaishouCloudSourceLoadStats } from '../../../repositories/kuaishou-cloud-task-state-repo.js' import { normalizeKuaishouCloudFlow, normalizeStringArray, type JsonObject } from './domain.js' -import { resolvePersistedCloudtentaclesContextBySourceKeys } from './cloudtentacles-context.js' +import { resolvePersistedCloudtentaclesContextWithFallback } from './cloudtentacles-context.js' -type CloudtentaclesContext = ReturnType +type CloudtentaclesContext = ReturnType type CloudtentaclesSourceSelectionDeps = { - resolveContextBySourceKeys?: typeof resolvePersistedCloudtentaclesContextBySourceKeys + resolveContextBySourceKeys?: typeof resolvePersistedCloudtentaclesContextWithFallback listSourceLoadStats?: typeof listKuaishouCloudSourceLoadStats } @@ -30,7 +30,7 @@ export async function selectCloudtentaclesSourceForFulfillment( ): Promise { const flow = normalizeKuaishouCloudFlow(flowLike) const resolveContext = - deps.resolveContextBySourceKeys || resolvePersistedCloudtentaclesContextBySourceKeys + deps.resolveContextBySourceKeys || resolvePersistedCloudtentaclesContextWithFallback const listSourceLoadStats = deps.listSourceLoadStats || listKuaishouCloudSourceLoadStats const fixedSourceKey = String(flow.binding.resolvedSourceKey || '').trim() const candidates = normalizeCloudtentaclesSourceCandidates( diff --git a/apps/backend/src/services/fulfillment/kuaishou-cloud/cloudtentacles-context.ts b/apps/backend/src/services/fulfillment/kuaishou-cloud/cloudtentacles-context.ts index a803dd43..3d5f07dc 100644 --- a/apps/backend/src/services/fulfillment/kuaishou-cloud/cloudtentacles-context.ts +++ b/apps/backend/src/services/fulfillment/kuaishou-cloud/cloudtentacles-context.ts @@ -8,66 +8,47 @@ import { getCloudtentaclesSessionStateByKey } from "../../platforms/cloudtentacl import { normalizeStringArray } from "./domain.js"; /** - * 按优先级依次尝试 cloudSourceKeys 列表中的账号, - * 找到第一个有可用 token 的账号返回其 context。 - * 将实际使用的 resolvedSourceKey 也返回,确保后续操作(退号、发货等) - * 使用同一个 sourceKey,防止跨账号操作导致数据不一致。 + * 严格模式:只解析列表中第一个账号(调用方均把任务实际取号账号 resolvedSourceKey 放首位), + * 该账号不可用时直接报错,绝不静默 fallback 到其他账号—— + * 否则会用「非取号账号」去退号/取链,上游会因号码不归属当前会话而报权限不足。 + * 退号、取链、发货等「操作任务已有号码」的场景必须使用此函数。 + * 将实际使用的 resolvedSourceKey 也返回,确保后续操作使用同一个 sourceKey。 */ export function resolvePersistedCloudtentaclesContextBySourceKeys( sourceKeys: unknown[] = [] ) { - const candidates = normalizeStringArray(sourceKeys); + const candidates = [...new Set(normalizeStringArray(sourceKeys))]; - const uniqueCandidates = [...new Set(candidates)]; + if (candidates.length === 0) { + throw createHttpError( + "cloudtentacles 没有可用账号,请先到平台配置完成账号配置", + { statusCode: 409, errorCode: "kuaishou_cloud_no_source_keys" } + ); + } - let lastError = null; + const sourceKey = String(candidates[0] || "").trim(); - for (const sourceKey of uniqueCandidates) { - const source = getCloudtentaclesSourceByKey(sourceKey); - const session = getCloudtentaclesSessionStateByKey(sourceKey); + return resolveSingleCloudtentaclesAccountContext(sourceKey); +} - if (!source) continue; - if (source.enabled === false) { - lastError = createHttpError( - `cloudtentacles 账号 ${sourceKey} 已停用`, - { statusCode: 409, errorCode: "kuaishou_cloud_source_disabled" } - ); - continue; +/** + * 宽松模式:按顺序尝试候选账号,找到第一个有可用 token 的账号。 + * 仅用于「取新号」场景(新号码归属被选中的账号,不会产生跨账号孤儿), + * 以及 account-selector 单候选解析。严禁用于退号/取链/发货等操作已有号码的场景。 + */ +export function resolvePersistedCloudtentaclesContextWithFallback( + sourceKeys: unknown[] = [] +) { + const candidates = [...new Set(normalizeStringArray(sourceKeys))]; + + let lastError: unknown = null; + + for (const sourceKey of candidates) { + try { + return resolveSingleCloudtentaclesAccountContext(sourceKey); + } catch (error) { + lastError = error; } - - if (!session) { - lastError = createHttpError( - `cloudtentacles 账号 ${sourceKey} 没有可用 token`, - { statusCode: 409, errorCode: "kuaishou_cloud_missing_cloud_token" } - ); - continue; - } - - 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: normalizeCloudtentaclesDeviceId( - session.deviceId || source.deviceId - ), - deviceType: normalizeCloudtentaclesDeviceType( - session.deviceType ?? source.deviceType - ), - // 透传给 http-client / 错误包装,便于定位跨账号问题 - sourceKey, - resolvedSourceKey: sourceKey, - accountLabel: String(source.label || sourceKey).trim() || sourceKey, - }; } throw ( @@ -78,3 +59,54 @@ export function resolvePersistedCloudtentaclesContextBySourceKeys( ) ); } + +function resolveSingleCloudtentaclesAccountContext(sourceKey: string) { + const source = getCloudtentaclesSourceByKey(sourceKey); + const session = getCloudtentaclesSessionStateByKey(sourceKey); + + if (!source) { + throw createHttpError( + `cloudtentacles 账号 ${sourceKey} 不存在(取号账号已变更或被删除,无法继续操作其虚拟号)`, + { statusCode: 409, errorCode: "kuaishou_cloud_source_missing" } + ); + } + + if (source.enabled === false) { + throw createHttpError( + `cloudtentacles 账号 ${sourceKey} 已停用`, + { statusCode: 409, errorCode: "kuaishou_cloud_source_disabled" } + ); + } + + if (!session) { + throw createHttpError( + `cloudtentacles 账号 ${sourceKey} 没有可用 token`, + { statusCode: 409, errorCode: "kuaishou_cloud_missing_cloud_token" } + ); + } + + const token = String(session.token || "").trim(); + if (!token) { + throw createHttpError( + `cloudtentacles 账号 ${sourceKey} 没有可用 token`, + { statusCode: 409, errorCode: "kuaishou_cloud_missing_cloud_token" } + ); + } + + return { + baseUrl: + String(session.baseUrl || source.baseUrl || "").trim() || + "https://123.207.217.176", + token, + deviceId: normalizeCloudtentaclesDeviceId( + session.deviceId || source.deviceId + ), + deviceType: normalizeCloudtentaclesDeviceType( + session.deviceType ?? source.deviceType + ), + // 透传给 http-client / 错误包装,便于定位跨账号问题 + sourceKey, + resolvedSourceKey: sourceKey, + accountLabel: String(source.label || sourceKey).trim() || sourceKey, + }; +}