From 6e41d7e0db24044e26aa7187e5e442621a8b0d3b Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sat, 8 Aug 2026 23:08:05 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BD=BB=E5=BA=95=E4=BF=AE=E5=A4=8D=20lewan=20?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E5=8F=91=E8=B4=A7=E9=97=AE=E9=A2=98-8-8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kuaishou-cloud/account-selector.test.ts | 68 +++++++++++ .../kuaishou-cloud/account-selector.ts | 113 +++++++++++++----- .../kuaishou-cloud/prepare-fulfillment.ts | 51 +++++++- .../cloudtentacles-name-match-service.test.ts | 20 ++++ .../cloudtentacles-name-match-service.ts | 7 +- .../cloudtentacles/catalog-service.ts | 11 ++ .../platforms/cloudtentacles/http-client.ts | 3 + .../cloudtentacles/knapsack-service.ts | 3 + .../cloudtentacles/virtual-number-service.ts | 19 +++ .../cloudtentacles-number-recycle-service.ts | 2 + 10 files changed, 260 insertions(+), 37 deletions(-) diff --git a/apps/backend/src/services/fulfillment/kuaishou-cloud/account-selector.test.ts b/apps/backend/src/services/fulfillment/kuaishou-cloud/account-selector.test.ts index 7ead69ee..d403644c 100644 --- a/apps/backend/src/services/fulfillment/kuaishou-cloud/account-selector.test.ts +++ b/apps/backend/src/services/fulfillment/kuaishou-cloud/account-selector.test.ts @@ -20,12 +20,21 @@ const contexts = { }, } +const listEmptyVirtualNumbers = async () => ({ + baseUrl: 'https://cloud.example.com', + key: '1', + itemCount: 0, + items: [], + rawItems: [], +}) + test('selectCloudtentaclesSourceForFulfillment 已固定账号时不按压力切换', async () => { const selection = await selectCloudtentaclesSourceForFulfillment( { binding: { resolvedSourceKey: 'account-a', cloudSourceKeys: ['account-a', 'account-b'], + vnId: 123, }, }, { @@ -37,6 +46,7 @@ test('selectCloudtentaclesSourceForFulfillment 已固定账号时不按压力切 { sourceKey: 'account-a', activeCount: 99, redeemingCount: 99 }, { sourceKey: 'account-b', activeCount: 0, redeemingCount: 0 }, ], + listVirtualNumbers: listEmptyVirtualNumbers, }, ) @@ -44,6 +54,61 @@ test('selectCloudtentaclesSourceForFulfillment 已固定账号时不按压力切 assert.equal(selection.fixed, true) }) +test('selectCloudtentaclesSourceForFulfillment 无号码时忽略残留固定账号', async () => { + const selection = await selectCloudtentaclesSourceForFulfillment( + { + binding: { + resolvedSourceKey: 'account-a', + cloudSourceKeys: ['account-a', 'account-b'], + vnId: 0, + }, + }, + { + resolveContextBySourceKeys: (sourceKeys) => contexts[String(sourceKeys[0]) as keyof typeof contexts], + listSourceLoadStats: async () => [ + { sourceKey: 'account-a', activeCount: 20, redeemingCount: 0 }, + { sourceKey: 'account-b', activeCount: 1, redeemingCount: 0 }, + ], + listVirtualNumbers: listEmptyVirtualNumbers, + }, + ) + + try { + assert.equal(selection.sourceKey, 'account-b') + assert.equal(selection.fixed, false) + } finally { + selection.release() + } +}) + +test('selectCloudtentaclesSourceForFulfillment 跳过真实占用已达20的账号', async () => { + const selection = await selectCloudtentaclesSourceForFulfillment( + { binding: { cloudSourceKeys: ['account-a', 'account-b'] } }, + { + resolveContextBySourceKeys: (sourceKeys) => contexts[String(sourceKeys[0]) as keyof typeof contexts], + listSourceLoadStats: async () => [], + listVirtualNumbers: async (payload) => { + const count = payload.sourceKey === 'account-a' ? 20 : 3 + return { + baseUrl: 'https://cloud.example.com', + key: '1', + itemCount: count, + items: Array.from({ length: count }, (_, id) => ({ id: id + 1 })), + rawItems: [], + } + }, + }, + ) + + try { + assert.equal(selection.sourceKey, 'account-b') + assert.equal(selection.occupiedCount, 3) + assert.equal(selection.availableCount, 17) + } finally { + selection.release() + } +}) + test('selectCloudtentaclesSourceForFulfillment 多候选时选择压力最低账号', async () => { const selection = await selectCloudtentaclesSourceForFulfillment( { @@ -60,6 +125,7 @@ test('selectCloudtentaclesSourceForFulfillment 多候选时选择压力最低账 { sourceKey: 'account-a', activeCount: 3, redeemingCount: 1 }, { sourceKey: 'account-b', activeCount: 0, redeemingCount: 0 }, ], + listVirtualNumbers: listEmptyVirtualNumbers, }, ) @@ -87,6 +153,7 @@ test('selectCloudtentaclesSourceForFulfillment 会跳过不可用账号', async return contexts[key as keyof typeof contexts] }, listSourceLoadStats: async () => [], + listVirtualNumbers: listEmptyVirtualNumbers, }, ) @@ -105,6 +172,7 @@ test('selectCloudtentaclesSourceForFulfillment 用进程内占位分散并发准 return contexts[key as keyof typeof contexts] }, listSourceLoadStats: async () => [], + listVirtualNumbers: listEmptyVirtualNumbers, } const flow = { binding: { 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 194e3f90..d80ea6a2 100644 --- a/apps/backend/src/services/fulfillment/kuaishou-cloud/account-selector.ts +++ b/apps/backend/src/services/fulfillment/kuaishou-cloud/account-selector.ts @@ -1,12 +1,20 @@ import { listKuaishouCloudSourceLoadStats } from '../../../repositories/kuaishou-cloud-task-state-repo.js' -import { normalizeKuaishouCloudFlow, normalizeStringArray, type JsonObject } from './domain.js' +import { + KUAISHOU_CLOUD_FIXED_VN_KEY, + normalizeKuaishouCloudFlow, + normalizeStringArray, + type JsonObject, +} from './domain.js' import { resolvePersistedCloudtentaclesContextWithFallback } from './cloudtentacles-context.js' +import { listCloudtentaclesVirtualNumbers } from '../../platforms/cloudtentacles/virtual-number-service.js' +import { createHttpError } from '../../../utils/http.js' type CloudtentaclesContext = ReturnType type CloudtentaclesSourceSelectionDeps = { resolveContextBySourceKeys?: typeof resolvePersistedCloudtentaclesContextWithFallback listSourceLoadStats?: typeof listKuaishouCloudSourceLoadStats + listVirtualNumbers?: typeof listCloudtentaclesVirtualNumbers } export type CloudtentaclesSourceSelection = { @@ -17,6 +25,9 @@ export type CloudtentaclesSourceSelection = { redeemingCount: number inProcessCount: number score: number + occupiedCount: number + capacity: number + availableCount: number fixed: boolean release: () => void } @@ -32,12 +43,15 @@ export async function selectCloudtentaclesSourceForFulfillment( const resolveContext = deps.resolveContextBySourceKeys || resolvePersistedCloudtentaclesContextWithFallback const listSourceLoadStats = deps.listSourceLoadStats || listKuaishouCloudSourceLoadStats - const fixedSourceKey = String(flow.binding.resolvedSourceKey || '').trim() + const listVirtualNumbers = deps.listVirtualNumbers || listCloudtentaclesVirtualNumbers + const persistedSourceKey = String(flow.binding.resolvedSourceKey || '').trim() + const fixedSourceKey = flow.binding.vnId > 0 ? persistedSourceKey : '' + const excludedSourceKeys = new Set(normalizeStringArray((flow as JsonObject).excludedSourceKeys)) const candidates = normalizeCloudtentaclesSourceCandidates( fixedSourceKey ? [fixedSourceKey, ...flow.binding.cloudSourceKeys] - : flow.binding.cloudSourceKeys, - ) + : [...flow.binding.cloudSourceKeys, persistedSourceKey], + ).filter((key) => !excludedSourceKeys.has(key)) if (fixedSourceKey) { const context = resolveContext([fixedSourceKey, ...flow.binding.cloudSourceKeys]) @@ -49,6 +63,9 @@ export async function selectCloudtentaclesSourceForFulfillment( redeemingCount: 0, inProcessCount: 0, score: 0, + occupiedCount: 0, + capacity: 20, + availableCount: 20, fixed: true, release: () => {}, } @@ -76,40 +93,66 @@ export async function selectCloudtentaclesSourceForFulfillment( const stats = await listSourceLoadStats(contexts.map((context) => context.resolvedSourceKey)) const statsBySourceKey = new Map(stats.map((stat) => [stat.sourceKey, stat])) - const ranked = contexts - .map((context, index) => { - const stat = statsBySourceKey.get(context.resolvedSourceKey) || { + const ranked = [] + let lastCapacityError: unknown = null + for (const [index, context] of contexts.entries()) { + let occupiedCount = 0 + try { + // CT's VN list is the authoritative count; DB counters can lag after crashes/restarts. + const listed = await listVirtualNumbers({ + ...context, + key: KUAISHOU_CLOUD_FIXED_VN_KEY, sourceKey: context.resolvedSourceKey, - activeCount: 0, - redeemingCount: 0, - } - const activeCount = normalizeCount(stat.activeCount) - const redeemingCount = normalizeCount(stat.redeemingCount) - const inProcessCount = normalizeCount(inProcessSelections.get(context.resolvedSourceKey)) - const score = (activeCount + inProcessCount) * 10 + redeemingCount * 20 + accountLabel: context.accountLabel, + }) + occupiedCount = Array.isArray(listed.items) ? listed.items.length : Number(listed.itemCount || 0) || 0 + } catch (error) { + lastCapacityError = error + continue + } + const capacity = 20 + const inProcessCount = normalizeCount(inProcessSelections.get(context.resolvedSourceKey)) + if (occupiedCount + inProcessCount >= capacity) continue + const stat = statsBySourceKey.get(context.resolvedSourceKey) || { + sourceKey: context.resolvedSourceKey, + activeCount: 0, + redeemingCount: 0, + } + const activeCount = normalizeCount(stat.activeCount) + const redeemingCount = normalizeCount(stat.redeemingCount) + // Real upstream occupancy is the primary balancing signal. DB state is + // retained as a secondary tie-breaker for tasks still being processed. + const score = (occupiedCount + inProcessCount) * 100 + activeCount * 10 + redeemingCount * 20 - return { - context, - sourceKey: context.resolvedSourceKey, - index, - activeCount, - redeemingCount, - inProcessCount, - score, - } + ranked.push({ + context, + sourceKey: context.resolvedSourceKey, + index, + activeCount, + redeemingCount, + inProcessCount, + score, + occupiedCount, + capacity, + availableCount: Math.max(0, capacity - occupiedCount - inProcessCount), }) - .sort( - (left, right) => - left.score - right.score || - left.activeCount - right.activeCount || - left.redeemingCount - right.redeemingCount || - left.index - right.index || - left.sourceKey.localeCompare(right.sourceKey), - ) + } + if (ranked.length === 0 && lastCapacityError && contexts.length > 0) throw lastCapacityError + ranked.sort( + (left, right) => + left.score - right.score || + left.activeCount - right.activeCount || + left.redeemingCount - right.redeemingCount || + left.index - right.index || + left.sourceKey.localeCompare(right.sourceKey), + ) const selected = ranked[0] if (!selected) { - throw new Error('所有 cloudtentacles 账号均不可用') + throw createHttpError('所有 cloudtentacles 账号虚拟号配额均已满', { + statusCode: 409, + errorCode: 'cloudtentacles_all_sources_at_capacity', + }) } reserveCloudtentaclesSource(selected.sourceKey) @@ -122,6 +165,9 @@ export async function selectCloudtentaclesSourceForFulfillment( redeemingCount: selected.redeemingCount, inProcessCount: selected.inProcessCount, score: selected.score, + occupiedCount: selected.occupiedCount, + capacity: selected.capacity, + availableCount: selected.availableCount, fixed: false, release: () => releaseCloudtentaclesSource(selected.sourceKey), } @@ -138,6 +184,9 @@ export function buildCloudtentaclesSourceSelectionEventDetail( redeemingCount: selection.redeemingCount, inProcessCount: selection.inProcessCount, score: selection.score, + occupiedCount: selection.occupiedCount, + capacity: selection.capacity, + availableCount: selection.availableCount, fixed: selection.fixed, } } diff --git a/apps/backend/src/services/fulfillment/kuaishou-cloud/prepare-fulfillment.ts b/apps/backend/src/services/fulfillment/kuaishou-cloud/prepare-fulfillment.ts index 2d0f173c..e54ca576 100644 --- a/apps/backend/src/services/fulfillment/kuaishou-cloud/prepare-fulfillment.ts +++ b/apps/backend/src/services/fulfillment/kuaishou-cloud/prepare-fulfillment.ts @@ -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, diff --git a/apps/backend/src/services/order/cloudtentacles-name-match-service.test.ts b/apps/backend/src/services/order/cloudtentacles-name-match-service.test.ts index 04d24d70..7cad45b5 100644 --- a/apps/backend/src/services/order/cloudtentacles-name-match-service.test.ts +++ b/apps/backend/src/services/order/cloudtentacles-name-match-service.test.ts @@ -58,6 +58,25 @@ test('resolveCloudtentaclesSkuByProductName matches SKU name with logged-in sour assert.equal(result?.matchMode, 'cloudtentacles_name') }) +test('resolveCloudtentaclesSkuByProductName 多账号匹配不提前固定账号', async () => { + const result = await resolveCloudtentaclesSkuByProductName('商品A', { + listCloudtentaclesSources: () => ({ + enabled: true, + sources: [ + { key: 'account-a', enabled: true }, + { key: 'account-b', enabled: true }, + ], + }), + getCloudtentaclesSessionStateByKey: (key) => ({ token: `token-${key}` }), + listCloudtentaclesSku: async () => ({ + items: [{ id: 28, name: '商品A', inventory: 10, price: 100 }], + }), + }) + + assert.deepEqual(result?.cloudSourceKeys, ['account-a', 'account-b']) + assert.equal(result?.resolvedSourceKey, '') +}) + test('resolveCloudtentaclesSkuByProductName prefers override rule with multiple delivery items', async () => { const result = await resolveCloudtentaclesSkuByProductName('套装1', { listCloudtentaclesSources: () => ({ @@ -132,6 +151,7 @@ test('resolveCloudtentaclesSkuByProductName prefers override rule with multiple assert.equal(result?.matchMode, 'cloudtentacles_override') assert.equal(result?.cloudSkuId, 28) assert.equal(result?.cloudSkuName, '商品A') + assert.equal(result?.resolvedSourceKey, '') assert.deepEqual(result?.deliveryItems, [ { cloudSkuId: 28, diff --git a/apps/backend/src/services/order/cloudtentacles-name-match-service.ts b/apps/backend/src/services/order/cloudtentacles-name-match-service.ts index 7ce13bac..5afa9959 100644 --- a/apps/backend/src/services/order/cloudtentacles-name-match-service.ts +++ b/apps/backend/src/services/order/cloudtentacles-name-match-service.ts @@ -133,7 +133,9 @@ export async function resolveCloudtentaclesSkuByProductName( cloudSkuPrice: Number(matchedSku.price || 0) || 0, cloudSkuInventory: Number(matchedSku.inventory || 0) || 0, cloudSourceKeys, - resolvedSourceKey: context.sourceKey, + // Matching only discovers compatible accounts. The fulfillment selector + // chooses the least-loaded account immediately before taking a number. + resolvedSourceKey: '', deliveryItems: [ { cloudSkuId, @@ -168,6 +170,7 @@ function buildCloudtentaclesSourceContext( return { sourceKey, + accountLabel: String(source.label || sourceKey).trim() || sourceKey, baseUrl: String(session?.baseUrl || source.baseUrl || '').trim(), token, deviceId: normalizeCloudtentaclesDeviceId(session?.deviceId || source.deviceId), @@ -226,7 +229,7 @@ async function resolveOverrideMatch( cloudSkuPrice: Number(firstSku.price || 0) || 0, cloudSkuInventory: Number(firstSku.inventory || 0) || 0, cloudSourceKeys, - resolvedSourceKey: context.sourceKey, + resolvedSourceKey: rule.sourceKey ? context.sourceKey : '', deliveryItems, skuSnapshot: { overrideRuleId: rule.id, diff --git a/apps/backend/src/services/platforms/cloudtentacles/catalog-service.ts b/apps/backend/src/services/platforms/cloudtentacles/catalog-service.ts index c51433bc..792460c5 100644 --- a/apps/backend/src/services/platforms/cloudtentacles/catalog-service.ts +++ b/apps/backend/src/services/platforms/cloudtentacles/catalog-service.ts @@ -12,6 +12,9 @@ export async function getCloudtentaclesAsset(payload: JsonObject = {}) { method: 'GET', token, contentType: 'application/json', + sourceKey: payload.sourceKey || payload.resolvedSourceKey, + accountLabel: payload.accountLabel, + context: { operation: 'asset_get', sourceKey: payload.sourceKey || payload.resolvedSourceKey || '' }, businessErrorStatusCode: 401, businessErrorCode: 'cloudtentacles_asset_failed', }) @@ -32,6 +35,9 @@ export async function getCloudtentaclesCategories(payload: JsonObject = {}) { method: 'GET', token, contentType: 'application/json', + sourceKey: payload.sourceKey || payload.resolvedSourceKey, + accountLabel: payload.accountLabel, + context: { operation: 'categories_list', sourceKey: payload.sourceKey || payload.resolvedSourceKey || '' }, businessErrorStatusCode: 401, businessErrorCode: 'cloudtentacles_categories_failed', }) @@ -55,6 +61,9 @@ export async function listCloudtentaclesSku(payload: JsonObject = {}) { method: 'GET', token, contentType: 'application/json', + sourceKey: payload.sourceKey || payload.resolvedSourceKey, + accountLabel: payload.accountLabel, + context: { operation: 'sku_list', sourceKey: payload.sourceKey || payload.resolvedSourceKey || '' }, businessErrorStatusCode: 401, businessErrorCode: 'cloudtentacles_sku_list_failed', }) @@ -85,6 +94,8 @@ export async function buyCloudtentaclesSku(payload: JsonObject = {}) { }, businessErrorStatusCode: 401, businessErrorCode: 'cloudtentacles_sku_buy_failed', + sourceKey: payload.sourceKey || payload.resolvedSourceKey, + accountLabel: payload.accountLabel, context: { operation: 'sku_buy', skuId, diff --git a/apps/backend/src/services/platforms/cloudtentacles/http-client.ts b/apps/backend/src/services/platforms/cloudtentacles/http-client.ts index 9b94c38c..87bd49a8 100644 --- a/apps/backend/src/services/platforms/cloudtentacles/http-client.ts +++ b/apps/backend/src/services/platforms/cloudtentacles/http-client.ts @@ -187,6 +187,9 @@ export async function cloudtentaclesRequest(pathname: unknown, options: JsonObje pathname: normalizedPathname, status: response.status, attempt: attempt + 1, + sourceKey: options.sourceKey, + accountLabel: options.accountLabel, + context: options.context, }) return { diff --git a/apps/backend/src/services/platforms/cloudtentacles/knapsack-service.ts b/apps/backend/src/services/platforms/cloudtentacles/knapsack-service.ts index 889cc770..7a06864c 100644 --- a/apps/backend/src/services/platforms/cloudtentacles/knapsack-service.ts +++ b/apps/backend/src/services/platforms/cloudtentacles/knapsack-service.ts @@ -18,6 +18,9 @@ export async function getCloudtentaclesKnapsack(payload: JsonObject = {}) { method: 'GET', token, contentType: 'application/json', + sourceKey: payload.sourceKey || payload.resolvedSourceKey, + accountLabel: payload.accountLabel, + context: { operation: 'knapsack_get', sourceKey: payload.sourceKey || payload.resolvedSourceKey || '' }, businessErrorStatusCode: 401, businessErrorCode: 'cloudtentacles_knapsack_failed', }) diff --git a/apps/backend/src/services/platforms/cloudtentacles/virtual-number-service.ts b/apps/backend/src/services/platforms/cloudtentacles/virtual-number-service.ts index 63503fcb..bd816cf5 100644 --- a/apps/backend/src/services/platforms/cloudtentacles/virtual-number-service.ts +++ b/apps/backend/src/services/platforms/cloudtentacles/virtual-number-service.ts @@ -5,6 +5,7 @@ import type { JsonObject } from '../../../types/json.js' import { createHttpError } from '../../../utils/http.js' import { cloudtentaclesRequest } from './http-client.js' import { resolveCloudtentaclesConfig } from './helpers.js' +import { maskPhone } from '../../../utils/masking.js' const BIND_URL_PROBE_MAX_BODY_BYTES = 64 * 1024 const AMS_SIGNATURE_EXPIRED_CODE = '99998' @@ -49,6 +50,9 @@ export async function listCloudtentaclesVirtualNumbers(payload: JsonObject = {}) method: 'POST', token, body: { key }, + sourceKey: payload.sourceKey || payload.resolvedSourceKey, + accountLabel: payload.accountLabel, + context: { operation: 'vn_list', vnKey: key, sourceKey: payload.sourceKey || payload.resolvedSourceKey || '' }, businessErrorStatusCode: 401, businessErrorCode: 'cloudtentacles_vn_list_failed', }) @@ -74,6 +78,9 @@ export async function appointCloudtentaclesVirtualNumber(payload: JsonObject = { method: 'POST', token, body: { key }, + sourceKey: payload.sourceKey || payload.resolvedSourceKey, + accountLabel: payload.accountLabel, + context: { operation: 'vn_appoint', vnKey: key, sourceKey: payload.sourceKey || payload.resolvedSourceKey || '' }, businessErrorStatusCode: 401, businessErrorCode: 'cloudtentacles_vn_appoint_failed', }) @@ -97,6 +104,9 @@ export async function generateCloudtentaclesLoginCode(payload: JsonObject = {}) method: 'POST', token, body: { key, id }, + sourceKey: payload.sourceKey || payload.resolvedSourceKey, + accountLabel: payload.accountLabel, + context: { operation: 'vn_generate_code', vnKey: key, vnId: id, sourceKey: payload.sourceKey || payload.resolvedSourceKey || '' }, businessErrorStatusCode: 401, businessErrorCode: 'cloudtentacles_vn_generate_code_failed', }) @@ -126,6 +136,9 @@ export async function fetchCloudtentaclesVirtualNumberCode(payload: JsonObject = method: 'POST', token, body: { key, phone }, + sourceKey: payload.sourceKey || payload.resolvedSourceKey, + accountLabel: payload.accountLabel, + context: { operation: 'vn_fetch_code', vnKey: key, phoneMasked: maskPhone(phone), sourceKey: payload.sourceKey || payload.resolvedSourceKey || '' }, businessErrorStatusCode: 401, businessErrorCode: 'cloudtentacles_vn_verif_code_failed', }) @@ -157,6 +170,9 @@ export async function verifyCloudtentaclesLoginCode(payload: JsonObject = {}) { method: 'POST', token, body: { key, id, code }, + sourceKey: payload.sourceKey || payload.resolvedSourceKey, + accountLabel: payload.accountLabel, + context: { operation: 'vn_verify_code', vnKey: key, vnId: id, sourceKey: payload.sourceKey || payload.resolvedSourceKey || '' }, businessErrorStatusCode: 401, businessErrorCode: 'cloudtentacles_vn_verify_code_failed', }) @@ -305,6 +321,9 @@ export async function getCloudtentaclesBindInfo(payload: JsonObject = {}) { rateLimitRetryDelayMs: BIND_INFO_RATE_LIMIT_RETRY_DELAY_MS, rateLimitRetries: 2, rateLimitIsolatePath: true, + sourceKey: payload.sourceKey || payload.resolvedSourceKey, + accountLabel: payload.accountLabel, + context: { operation: 'vn_bind_info', vnKey: key, vnId: id, sourceKey: payload.sourceKey || payload.resolvedSourceKey || '' }, }) const items = Array.isArray(result.payload?.data) ? result.payload.data : [] diff --git a/apps/backend/src/services/scheduler/cloudtentacles-number-recycle-service.ts b/apps/backend/src/services/scheduler/cloudtentacles-number-recycle-service.ts index 2f9d9237..d6c99996 100644 --- a/apps/backend/src/services/scheduler/cloudtentacles-number-recycle-service.ts +++ b/apps/backend/src/services/scheduler/cloudtentacles-number-recycle-service.ts @@ -120,6 +120,8 @@ export async function recycleCloudtentaclesStaleNumbers(): Promise> '{kuaishouCloudFulfillment,dispatch,status}', '') <> 'success' AND COALESCE(ft.context_json #>> '{kuaishouCloudFulfillment,binding,prepareStatus}', '') = 'ready' AND COALESCE(ft.context_json #>> '{kuaishouCloudFulfillment,binding,bindExpiresAt}', '') <> '' AND (ft.context_json #>> '{kuaishouCloudFulfillment,binding,bindExpiresAt}')::timestamptz < $1