From 89ffc9b087fd1d22be0480468f83238b3f6ce3c4 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Thu, 14 May 2026 16:07:17 +0800 Subject: [PATCH] =?UTF-8?q?=E5=86=8D=E8=B7=B3=E5=9B=9E=E7=AC=AC=201=20?= =?UTF-8?q?=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.server.example | 1 + apps/backend/config/default.cjs | 1 + apps/backend/src/config/runtime.js | 5 + .../claim/kuaishou-cloud-sync-service.js | 13 ++ .../kuaishou-cloud-task-service.js | 131 ++++++++++++++++-- .../platforms/cloudtentacles/shared.js | 5 + apps/backend/src/types/runtime-config.js | 1 + .../views/claim/KuaishouCloudClaimView.vue | 4 +- 8 files changed, 145 insertions(+), 16 deletions(-) diff --git a/.env.server.example b/.env.server.example index 2c8d43ae..79cb7327 100644 --- a/.env.server.example +++ b/.env.server.example @@ -26,6 +26,7 @@ TENCENT_BROWSER_KEEP_ALIVE=true TENCENT_BROWSER_SLOW_MO=0 TENCENT_SESSION_DEBUG=false CLOUDTENTACLES_BIND_URL_TTL_SECONDS=300 +CLOUDTENTACLES_BIND_URL_PROBE_INTERVAL_SECONDS=30 CLOUDTENTACLES_BIND_URL_PROBE_TIMEOUT_MS=5000 CLOUDTENTACLES_BIND_URL_PROBE_ENDPOINT=https://comm.ams.game.qq.com/ide/ CLOUDTENTACLES_BIND_URL_PROBE_CHART_ID=323794 diff --git a/apps/backend/config/default.cjs b/apps/backend/config/default.cjs index 3374e758..7e9ecd30 100644 --- a/apps/backend/config/default.cjs +++ b/apps/backend/config/default.cjs @@ -100,6 +100,7 @@ module.exports = { vnBindInfoPath: '/vn/bind_info', vnBackPath: '/vn/back', bindUrlTtlSeconds: 300, + bindUrlProbeIntervalSeconds: 30, bindUrlProbeTimeoutMs: 5000, bindUrlProbeUserAgent: 'Mozilla/5.0 (Linux; Android 12; M2012K11AC Build/SKQ1.211006.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/98.0.4758.102 MQQBrowser/6.2 TBS/046141 Mobile Safari/537.36 V1_AND_SQ_8.8.85_2674_YYB_D QQ/8.8.85.7685 NetType/WIFI WebP/0.3.0 Pixel/1080 StatusBarHeight/108 SimpleUISwitch/0 QQTheme/1000', bindUrlProbeEndpoint: 'https://comm.ams.game.qq.com/ide/', diff --git a/apps/backend/src/config/runtime.js b/apps/backend/src/config/runtime.js index 0dc0e288..1e59876d 100644 --- a/apps/backend/src/config/runtime.js +++ b/apps/backend/src/config/runtime.js @@ -370,6 +370,11 @@ function applyEnvOverrides(baseConfig) { nextConfig.platforms.cloudtentacles.bindUrlTtlSeconds = cloudtentaclesBindUrlTtlSeconds } + const cloudtentaclesBindUrlProbeIntervalSeconds = parseInteger(process.env.CLOUDTENTACLES_BIND_URL_PROBE_INTERVAL_SECONDS) + if (cloudtentaclesBindUrlProbeIntervalSeconds !== null) { + nextConfig.platforms.cloudtentacles.bindUrlProbeIntervalSeconds = cloudtentaclesBindUrlProbeIntervalSeconds + } + const cloudtentaclesBindUrlProbeTimeoutMs = parseInteger(process.env.CLOUDTENTACLES_BIND_URL_PROBE_TIMEOUT_MS) if (cloudtentaclesBindUrlProbeTimeoutMs !== null) { nextConfig.platforms.cloudtentacles.bindUrlProbeTimeoutMs = cloudtentaclesBindUrlProbeTimeoutMs diff --git a/apps/backend/src/services/claim/kuaishou-cloud-sync-service.js b/apps/backend/src/services/claim/kuaishou-cloud-sync-service.js index 4e1c0992..f9290902 100644 --- a/apps/backend/src/services/claim/kuaishou-cloud-sync-service.js +++ b/apps/backend/src/services/claim/kuaishou-cloud-sync-service.js @@ -1,5 +1,6 @@ import { normalizeKuaishouCloudFlow, + prepareKuaishouCloudFulfillmentTask, probeKuaishouCloudTaskBindUrl, refreshKuaishouCloudTaskRoleInfo, } from '../fulfillment/kuaishou-cloud-task-service.js' @@ -8,6 +9,18 @@ export async function syncKuaishouCloudRoleInfo(task) { const flow = normalizeKuaishouCloudFlow(parseTaskContext(task).kuaishouCloudFulfillment) if (!flow.binding.vnId || !flow.binding.vnKey || !flow.binding.vnPhone) { + if (flow.ticket.status === 'verified' && flow.binding.prepareStatus !== 'ready') { + try { + const prepared = await prepareKuaishouCloudFulfillmentTask(task, { + source: 'claim_page_retry_binding_prepare', + actor: { source: 'system' }, + }) + return prepared.task + } catch { + return task + } + } + return task } diff --git a/apps/backend/src/services/fulfillment/kuaishou-cloud-task-service.js b/apps/backend/src/services/fulfillment/kuaishou-cloud-task-service.js index 33dad741..8303ecf1 100644 --- a/apps/backend/src/services/fulfillment/kuaishou-cloud-task-service.js +++ b/apps/backend/src/services/fulfillment/kuaishou-cloud-task-service.js @@ -399,18 +399,55 @@ export async function refreshKuaishouCloudTaskBindUrl(task, options = {}) { } const cloudContext = resolvePersistedCloudtentaclesContext() - const bindUrlResult = await getCloudtentaclesBindUrl({ - ...cloudContext, - key: flow.binding.vnKey, - id: flow.binding.vnId, - }) - const bindUrl = String(bindUrlResult.bindUrl || '').trim() + const oldVnKey = flow.binding.vnKey + const oldVnId = flow.binding.vnId + const oldVnPhone = flow.binding.vnPhone - if (!bindUrl) { - throw createHttpError('cloudtentacles 未返回新的绑定链接', { - statusCode: 502, - errorCode: 'kuaishou_cloud_empty_bind_url', + await backCloudtentaclesVirtualNumber({ + ...cloudContext, + key: oldVnKey, + id: oldVnId, + }) + + await createTaskEvent(task.id, 'kuaishou_cloud_expired_bind_number_returned', { + source: String(options.source || 'system').trim() || 'system', + vnKey: oldVnKey, + vnId: oldVnId, + vnPhoneMasked: maskPhone(oldVnPhone), + actor, + }, now) + + let preparedBinding + try { + preparedBinding = await prepareKuaishouCloudBindResourceWithFallback({ + cloudContext, + vnKeyCandidates: resolveKuaishouCloudVnKeyCandidates({ + flow, + binding: flow.binding, + }), }) + + if (!String(preparedBinding.bindUrl || '').trim()) { + throw createHttpError('cloudtentacles 未返回新的绑定链接', { + statusCode: 502, + errorCode: 'kuaishou_cloud_empty_bind_url', + }) + } + } catch (error) { + const failedTask = await markKuaishouCloudBindUrlRefreshFailed(task, { + taskContext, + flow, + now, + actor, + error, + }) + + return { + task: failedTask, + claimUrl: buildClaimUrl(String(task.primary_claim_token || task.claim_token || '')), + token: String(task.primary_claim_token || task.claim_token || ''), + flow: normalizeKuaishouCloudFlow(parseTaskContext(failedTask).kuaishouCloudFulfillment), + } } const nextContext = { @@ -420,7 +457,10 @@ export async function refreshKuaishouCloudTaskBindUrl(task, options = {}) { binding: { ...flow.binding, prepareStatus: 'ready', - bindUrl, + vnKey: preparedBinding.vnKey, + vnId: preparedBinding.vnId, + vnPhone: preparedBinding.vnPhone, + bindUrl: preparedBinding.bindUrl, bindPreparedAt: now, bindExpiresAt: resolveKuaishouCloudBindUrlExpiresAt(now), bindProbeAt: null, @@ -456,8 +496,11 @@ export async function refreshKuaishouCloudTaskBindUrl(task, options = {}) { await createTaskEvent(task.id, 'kuaishou_cloud_bind_url_refreshed', { source: String(options.source || 'system').trim() || 'system', - vnId: flow.binding.vnId, - vnPhoneMasked: maskPhone(flow.binding.vnPhone), + oldVnId, + oldVnPhoneMasked: maskPhone(oldVnPhone), + vnKey: preparedBinding.vnKey, + vnId: preparedBinding.vnId, + vnPhoneMasked: maskPhone(preparedBinding.vnPhone), actor, }, now) @@ -489,7 +532,8 @@ export async function probeKuaishouCloudTaskBindUrl(task, options = {}) { } } - if (!options.force && flow.binding.bindProbeAt && Date.now() - Date.parse(flow.binding.bindProbeAt) < 15000) { + const probeIntervalMs = Number(resolveCloudtentaclesConfig().bindUrlProbeIntervalSeconds || 30) * 1000 + if (!options.force && flow.binding.bindProbeAt && Date.now() - Date.parse(flow.binding.bindProbeAt) < probeIntervalMs) { return { task, flow, @@ -552,6 +596,65 @@ export async function probeKuaishouCloudTaskBindUrl(task, options = {}) { } } +/** + * @param {any} task + * @param {{ taskContext?: any, flow?: any, now?: string, actor?: any, error?: unknown }} [input] + */ +async function markKuaishouCloudBindUrlRefreshFailed(task, { + taskContext, + flow, + now, + actor, + error, +} = {}) { + const errorMessage = error instanceof Error ? error.message : String(error || '新绑定链接准备失败') + const nextContext = { + ...taskContext, + kuaishouCloudFulfillment: { + ...flow, + binding: { + ...flow.binding, + prepareStatus: 'pending', + vnId: 0, + vnPhone: '', + bindUrl: '', + bindPreparedAt: null, + bindExpiresAt: null, + bindProbeAt: now, + bindProbeStatus: 'refresh_failed', + bindProbeMessage: errorMessage, + roleName: '', + roleId: '', + }, + role: { + status: 'pending', + name: '', + rid: '', + refreshedAt: now, + errorMessage: '绑定链接已过期,旧号码已退还,新链接准备失败,请稍后刷新或联系客服处理', + rawInfo: null, + }, + }, + } + + const updatedTask = await updateTask(task.id, { + task_status: 'pending_binding_prepare', + role_id: '', + role_name: '', + last_error: `绑定链接过期,旧号码已退还,新链接准备失败:${errorMessage}`, + context_json: JSON.stringify(nextContext), + updated_at: now, + }) + + await createTaskEvent(task.id, 'kuaishou_cloud_bind_url_refresh_failed', { + source: 'system_refresh_expired_bind_url', + errorMessage, + actor, + }, now) + + return updatedTask +} + export async function refreshKuaishouCloudTaskRoleInfo(task, options = {}) { if (!isKuaishouCloudTask(task)) { throw createHttpError('当前任务不是快手 Cloud 履约任务', { diff --git a/apps/backend/src/services/platforms/cloudtentacles/shared.js b/apps/backend/src/services/platforms/cloudtentacles/shared.js index 24ed4ee1..3896affb 100644 --- a/apps/backend/src/services/platforms/cloudtentacles/shared.js +++ b/apps/backend/src/services/platforms/cloudtentacles/shared.js @@ -28,6 +28,7 @@ export function resolveCloudtentaclesConfig(overrides = {}) { vnBindInfoPath: '/vn/bind_info', vnBackPath: '/vn/back', bindUrlTtlSeconds: 300, + bindUrlProbeIntervalSeconds: 30, bindUrlProbeTimeoutMs: 5000, bindUrlProbeUserAgent: '', bindUrlProbeEndpoint: 'https://comm.ams.game.qq.com/ide/', @@ -71,6 +72,10 @@ export function resolveCloudtentaclesConfig(overrides = {}) { vnBindInfoPath: normalizePath(overrides.vnBindInfoPath || baseConfig.vnBindInfoPath, '/vn/bind_info'), vnBackPath: normalizePath(overrides.vnBackPath || baseConfig.vnBackPath, '/vn/back'), bindUrlTtlSeconds: normalizePositiveInteger(overrides.bindUrlTtlSeconds || baseConfig.bindUrlTtlSeconds, 300), + bindUrlProbeIntervalSeconds: normalizePositiveInteger( + overrides.bindUrlProbeIntervalSeconds || baseConfig.bindUrlProbeIntervalSeconds, + 30, + ), bindUrlProbeTimeoutMs: normalizePositiveInteger(overrides.bindUrlProbeTimeoutMs || baseConfig.bindUrlProbeTimeoutMs, 5000), bindUrlProbeUserAgent: String(overrides.bindUrlProbeUserAgent || baseConfig.bindUrlProbeUserAgent || '').trim(), bindUrlProbeEndpoint: String(overrides.bindUrlProbeEndpoint || baseConfig.bindUrlProbeEndpoint || 'https://comm.ams.game.qq.com/ide/').trim(), diff --git a/apps/backend/src/types/runtime-config.js b/apps/backend/src/types/runtime-config.js index 1a9f5a3c..b16e3c8e 100644 --- a/apps/backend/src/types/runtime-config.js +++ b/apps/backend/src/types/runtime-config.js @@ -120,6 +120,7 @@ export {} * vnBindInfoPath: string * vnBackPath: string * bindUrlTtlSeconds: number + * bindUrlProbeIntervalSeconds: number * bindUrlProbeTimeoutMs: number * bindUrlProbeUserAgent: string * bindUrlProbeEndpoint: string diff --git a/apps/frontend/src/views/claim/KuaishouCloudClaimView.vue b/apps/frontend/src/views/claim/KuaishouCloudClaimView.vue index f4cd0916..aaf45d55 100644 --- a/apps/frontend/src/views/claim/KuaishouCloudClaimView.vue +++ b/apps/frontend/src/views/claim/KuaishouCloudClaimView.vue @@ -51,7 +51,7 @@ const isBindingPrepared = computed(() => ( && !isBindUrlExpired.value )) const isBindingPreparing = computed(() => flow.value?.binding.prepareStatus === 'pending') -const canEnterBindingStep = computed(() => isTicketVerified.value && isBindingPrepared.value) +const canEnterBindingStep = computed(() => isTicketVerified.value) const isRoleReady = computed(() => Boolean(roleName.value || roleId.value)) const isRoleConfirmed = computed(() => String(task.value?.status || '').trim() === 'role_confirmed') const isDispatched = computed(() => String(flow.value?.dispatch.status || '').trim() === 'success') @@ -85,7 +85,7 @@ const progressText = computed(() => { return '角色已确认,等待兑换' } if (isTicketVerified.value) { - return isBindingPrepared.value ? '请完成扫码绑定' : '绑定资源未准备完成,请重新验证核销码' + return isBindingPrepared.value ? '请完成扫码绑定' : '绑定链接刷新中,请稍候' } return '等待提交核销码' })