// @ts-check import { createTaskEvent } from "../../repositories/task-event-repo.js"; import { getOrderById } from "../../repositories/order-repo.js"; import { updateTask } from "../../repositories/task-repo.js"; import { buildClaimUrl, createTaskClaimToken } from "../claim/claim-service.js"; import { normalizeProductName } from "../order/product-match-service.js"; import { getCloudtentaclesSourceConfig, getCloudtentaclesSourceByKey, } from "../platforms/cloudtentacles/source-config-service.js"; import { getCloudtentaclesSessionState, getCloudtentaclesSessionStateByKey, } from "../platforms/cloudtentacles/session-state-service.js"; import { buyCloudtentaclesSku, getCloudtentaclesAsset, listCloudtentaclesSku, useCloudtentaclesSku, } from "../platforms/cloudtentacles/catalog-service.js"; import { getCloudtentaclesKnapsack } from "../platforms/cloudtentacles/knapsack-service.js"; import { appointCloudtentaclesVirtualNumber, backCloudtentaclesVirtualNumber, fetchCloudtentaclesVirtualNumberCode, generateCloudtentaclesLoginCode, getCloudtentaclesBindInfo, getCloudtentaclesBindUrl, probeCloudtentaclesBindUrl, verifyCloudtentaclesLoginCode, } from "../platforms/cloudtentacles/virtual-number-service.js"; import { resolveCloudtentaclesConfig } from "../platforms/cloudtentacles/shared.js"; import { consumeKuaishouEticket } from "../platforms/kuaishou-eticket/consume-service.js"; import { getKuaishouEticketSourceConfig, resolveKuaishouEticketShopConfig, } from "../platforms/kuaishou-eticket/source-config-service.js"; import { notifyKuaishouCloudAssetNotEnough, notifyKuaishouCloudBindUrlRefreshFailed, notifyKuaishouCloudConsumeFailed, } from "../notification/domain-notifications.js"; import { createHttpError } from "../../utils/http.js"; import { nowIso } from "../../utils/time.js"; const KUAISHOU_CLOUD_FIXED_VN_KEY = "1"; export function isKuaishouCloudTask(task) { return String(task?.executor_key || "").trim() === "kuaishou_ct_assisted"; } export function normalizeKuaishouCloudFlow(value) { const source = value && typeof value === "object" ? value : {}; const binding = source.binding && typeof source.binding === "object" ? source.binding : {}; const role = source.role && typeof source.role === "object" ? source.role : {}; const purchase = source.purchase && typeof source.purchase === "object" ? source.purchase : {}; const dispatch = source.dispatch && typeof source.dispatch === "object" ? source.dispatch : {}; const returnNumber = source.returnNumber && typeof source.returnNumber === "object" ? source.returnNumber : {}; const consume = source.consume && typeof source.consume === "object" ? source.consume : {}; const ticket = source.ticket && typeof source.ticket === "object" ? source.ticket : {}; const roleName = String(role.name || binding.roleName || "").trim(); const roleId = String(role.rid || binding.roleId || "").trim(); const bindPreparedAt = binding.bindPreparedAt || null; const bindExpiresAt = binding.bindExpiresAt || resolveKuaishouCloudBindUrlExpiresAt(bindPreparedAt); return { ...source, configId: String(source.configId || "").trim(), internalSkuCode: String(source.internalSkuCode || "").trim(), internalSkuName: String(source.internalSkuName || "").trim(), ticket: { code: String(ticket.code || "").trim(), status: String(ticket.status || "pending").trim() || "pending", capturedAt: ticket.capturedAt || null, capturedBy: ticket.capturedBy || null, verifiedAt: ticket.verifiedAt || null, oid: String(ticket.oid || "").trim(), formToken: String(ticket.formToken || "").trim(), leftCount: Number(ticket.leftCount || 0) || 0, goodsTitle: String(ticket.goodsTitle || "").trim(), }, binding: { prepareStatus: String(binding.prepareStatus || "pending").trim() || "pending", cloudSourceKey: String(binding.cloudSourceKey || "default").trim() || "default", skuId: Number(binding.skuId || 0) || 0, skuName: String(binding.skuName || "").trim(), vnKey: String(binding.vnKey || KUAISHOU_CLOUD_FIXED_VN_KEY).trim() || KUAISHOU_CLOUD_FIXED_VN_KEY, vnId: Number(binding.vnId || 0) || 0, vnPhone: String(binding.vnPhone || "").trim(), bindUrl: String(binding.bindUrl || "").trim(), bindPreparedAt, bindExpiresAt, bindProbeAt: binding.bindProbeAt || null, bindProbeStatus: String(binding.bindProbeStatus || "").trim(), bindProbeMessage: String(binding.bindProbeMessage || "").trim(), roleName, roleId, }, role: { status: String( role.status || (roleName || roleId ? "ready" : "pending") ).trim() || "pending", name: roleName, rid: roleId, refreshedAt: role.refreshedAt || null, errorMessage: String(role.errorMessage || "").trim(), rawInfo: role.rawInfo && typeof role.rawInfo === "object" ? role.rawInfo : null, }, purchase: { autoBuyEnabled: purchase.autoBuyEnabled !== false, minAssetReserve: Number(purchase.minAssetReserve || 0) || 0, usedKnapsack: purchase.usedKnapsack === true, purchaseTriggered: purchase.purchaseTriggered === true, assetBefore: Number(purchase.assetBefore || 0) || 0, assetAfter: Number(purchase.assetAfter || 0) || 0, purchaseAt: purchase.purchaseAt || null, }, dispatch: { status: String(dispatch.status || "pending").trim() || "pending", dispatchAt: dispatch.dispatchAt || null, dispatchBy: dispatch.dispatchBy || null, sendType: Number(dispatch.sendType || 0) || 0, note: String(dispatch.note || "").trim(), }, returnNumber: { status: String(returnNumber.status || "pending").trim() || "pending", returnedAt: returnNumber.returnedAt || null, returnedBy: returnNumber.returnedBy || null, autoReturnEnabled: returnNumber.autoReturnEnabled === true, }, consume: { status: String(consume.status || "pending").trim() || "pending", shopId: String(consume.shopId || "").trim(), shopName: String(consume.shopName || "").trim(), autoConsumeEnabled: consume.autoConsumeEnabled === true, consumedAt: consume.consumedAt || null, errorMessage: String(consume.errorMessage || "").trim(), }, notes: String(source.notes || "").trim(), }; } export function normalizeKuaishouCloudRoleInfo(value) { const rawInfo = value && typeof value === "object" ? value : null; const nestedBindInfo = rawInfo?.sBindInfo && typeof rawInfo.sBindInfo === "object" ? rawInfo.sBindInfo : null; const source = nestedBindInfo || rawInfo; return { name: String( source?.name || source?.roleName || source?.nickname || source?.sRoleName || "" ).trim(), rid: String( source?.rid || source?.roleId || source?.uid || source?.sRoleId || source?.sUserId || "" ).trim(), rawInfo, }; } export function resolvePersistedCloudtentaclesContext(sourceKey = "default") { const source = getCloudtentaclesSourceByKey(sourceKey) || getCloudtentaclesSourceConfig(); const session = getCloudtentaclesSessionStateByKey(sourceKey) || getCloudtentaclesSessionState(); const token = String(session.token || "").trim(); if (!token) { throw createHttpError( "当前 cloudtentacles 没有可用 token,请先到平台配置完成登录校验", { statusCode: 409, errorCode: "kuaishou_cloud_missing_cloud_token", } ); } 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), }; } export async function ensureTaskClaimLink(task) { const tokenStatus = String(task?.primary_claim_token_status || "").trim(); const token = String( task?.primary_claim_token || task?.claim_token || "" ).trim(); const expiredAt = getTaskClaimExpiresAt(task); if (tokenStatus === "active" && token && !isClaimExpired(expiredAt)) { return { token, expiredAt, claimUrl: buildClaimUrl(token), }; } const claimToken = await createTaskClaimToken(task.id); return { token: claimToken.token, expiredAt: claimToken.expired_at, claimUrl: claimToken.claimUrl, }; } export async function prepareKuaishouCloudFulfillmentTask(task, options = {}) { if (!isKuaishouCloudTask(task)) { throw createHttpError("当前任务不是快手 Cloud 履约任务", { statusCode: 409, errorCode: "kuaishou_cloud_task_invalid", }); } const now = nowIso(); const actor = normalizeActor(options.actor); const force = options.force === true; const taskContext = parseTaskContext(task); const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment); const claimLinkState = await ensureTaskClaimLink(task); if ( !force && flow.binding.prepareStatus === "ready" && flow.binding.vnId > 0 && flow.binding.vnPhone && flow.binding.bindUrl ) { if (!isKuaishouCloudBindUrlFresh(flow)) { return refreshKuaishouCloudTaskBindUrl(task, { source: options.source || "system_refresh_expired_bind_url", actor, claimLinkState, }); } let readyTask = task; if (String(task.task_status || "").trim() !== "waiting_binding") { readyTask = await updateTask(task.id, { task_status: "waiting_binding", claim_token: claimLinkState.token || task.claim_token || "", claim_expires_at: claimLinkState.expiredAt || getTaskClaimExpiresAt(task), updated_at: now, }); } return { task: readyTask, claimUrl: claimLinkState.claimUrl, token: claimLinkState.token, flow, }; } const cloudContext = resolvePersistedCloudtentaclesContext( flow.binding.cloudSourceKey || "default" ); const [knapsack, skuList] = await Promise.all([ getCloudtentaclesKnapsack(cloudContext), listCloudtentaclesSku(cloudContext), ]); const resolvedBinding = resolveKuaishouCloudBindingResources(flow, { skuItems: Array.isArray(skuList.items) ? skuList.items : [], knapsackItems: Array.isArray(knapsack.items) ? knapsack.items : [], }); const vnKeyCandidates = resolveKuaishouCloudVnKeyCandidates({ flow, binding: resolvedBinding, }); if (!resolvedBinding.skuId || vnKeyCandidates.length === 0) { throw createHttpError("当前任务缺少可用 cloud 资源,无法自动准备绑定", { statusCode: 409, errorCode: "kuaishou_cloud_missing_binding_config", }); } const flowWithResolvedBinding = { ...flow, binding: { ...flow.binding, skuId: resolvedBinding.skuId, skuName: resolvedBinding.skuName, vnKey: KUAISHOU_CLOUD_FIXED_VN_KEY, }, }; const knapsackItem = resolvedBinding.knapsackItem; let usedKnapsack = Number(knapsackItem?.count || 0) > 0; let purchaseTriggered = false; let assetBefore = 0; let assetAfter = 0; if (!usedKnapsack) { if (!flowWithResolvedBinding.purchase.autoBuyEnabled) { throw createHttpError("背包中没有现成库存,且当前配置未开启自动购买", { statusCode: 409, errorCode: "kuaishou_cloud_auto_buy_disabled", }); } const asset = await getCloudtentaclesAsset(cloudContext); const targetSku = resolvedBinding.skuItem; if (!targetSku) { throw createHttpError( `cloudtentacles 未找到 SKU ${flowWithResolvedBinding.binding.skuId}`, { statusCode: 404, errorCode: "kuaishou_cloud_sku_not_found", } ); } assetBefore = Number(asset.asset || 0) || 0; const targetPrice = Number(targetSku.price || 0) || 0; const requiredAsset = targetPrice + flowWithResolvedBinding.purchase.minAssetReserve; if (assetBefore < requiredAsset) { await notifyKuaishouCloudAssetNotEnough({ task, flow: flowWithResolvedBinding, assetBefore, requiredAsset, skuName: targetSku.name, }); throw createHttpError( `余额不足,当前 ${assetBefore},至少需要 ${requiredAsset}`, { statusCode: 409, errorCode: "kuaishou_cloud_asset_not_enough", } ); } await buyCloudtentaclesSku({ ...cloudContext, id: flowWithResolvedBinding.binding.skuId, count: 1, }); purchaseTriggered = true; const assetResult = await getCloudtentaclesAsset(cloudContext); assetAfter = Number(assetResult.asset || 0) || 0; } const preparedBinding = await prepareKuaishouCloudBindResourceWithFallback({ cloudContext, vnKeyCandidates, }); const nextContext = { ...taskContext, kuaishouCloudFulfillment: { ...flowWithResolvedBinding, binding: { ...flowWithResolvedBinding.binding, vnKey: preparedBinding.vnKey, prepareStatus: "ready", vnId: preparedBinding.vnId, vnPhone: preparedBinding.vnPhone, bindUrl: preparedBinding.bindUrl, bindPreparedAt: now, bindExpiresAt: resolveKuaishouCloudBindUrlExpiresAt(now), bindProbeAt: null, bindProbeStatus: "pending", bindProbeMessage: "", roleName: "", roleId: "", }, role: { status: "pending", name: "", rid: "", refreshedAt: null, errorMessage: "", rawInfo: null, }, purchase: { ...flowWithResolvedBinding.purchase, usedKnapsack, purchaseTriggered, assetBefore, assetAfter, purchaseAt: purchaseTriggered ? now : flowWithResolvedBinding.purchase.purchaseAt, }, }, }; const updatedTask = await updateTask(task.id, { task_status: "waiting_binding", inventory_status: "not_required", user_action_status: "pending_claim", claim_token: claimLinkState.token || task.claim_token || "", claim_expires_at: claimLinkState.expiredAt || getTaskClaimExpiresAt(task), role_id: "", role_name: "", last_error: "", context_json: JSON.stringify(nextContext), updated_at: now, }); await createTaskEvent( task.id, "kuaishou_cloud_binding_prepared", { source: String(options.source || "system").trim() || "system", skuId: flowWithResolvedBinding.binding.skuId, skuName: flowWithResolvedBinding.binding.skuName, vnKey: preparedBinding.vnKey, vnId: preparedBinding.vnId, vnPhoneMasked: maskPhone(preparedBinding.vnPhone), purchaseTriggered, usedKnapsack, resolvedByName: resolvedBinding.resolvedByName, actor, }, now ); return { task: updatedTask, claimUrl: claimLinkState.claimUrl, token: claimLinkState.token, flow: normalizeKuaishouCloudFlow(nextContext.kuaishouCloudFulfillment), }; } export async function refreshKuaishouCloudTaskBindUrl(task, options = {}) { if (!isKuaishouCloudTask(task)) { throw createHttpError("当前任务不是快手 Cloud 履约任务", { statusCode: 409, errorCode: "kuaishou_cloud_task_invalid", }); } const now = nowIso(); const actor = normalizeActor(options.actor); const taskContext = parseTaskContext(task); const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment); if (!flow.binding.vnId || !flow.binding.vnKey) { throw createHttpError("当前任务缺少可刷新绑定链接的虚拟号信息", { statusCode: 409, errorCode: "kuaishou_cloud_missing_bind_url_context", }); } const cloudContext = resolvePersistedCloudtentaclesContext( flow.binding.cloudSourceKey || "default" ); const oldVnKey = flow.binding.vnKey; const oldVnId = flow.binding.vnId; const oldVnPhone = flow.binding.vnPhone; 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 = { ...taskContext, kuaishouCloudFulfillment: { ...flow, binding: { ...flow.binding, prepareStatus: "ready", vnKey: preparedBinding.vnKey, vnId: preparedBinding.vnId, vnPhone: preparedBinding.vnPhone, bindUrl: preparedBinding.bindUrl, bindPreparedAt: now, bindExpiresAt: resolveKuaishouCloudBindUrlExpiresAt(now), bindProbeAt: null, bindProbeStatus: "pending", bindProbeMessage: "", roleName: String(task.task_status || "").trim() === "role_confirmed" ? flow.binding.roleName : "", roleId: String(task.task_status || "").trim() === "role_confirmed" ? flow.binding.roleId : "", }, role: String(task.task_status || "").trim() === "role_confirmed" ? flow.role : { status: "pending", name: "", rid: "", refreshedAt: null, errorMessage: "", rawInfo: null, }, }, }; const claimLinkState = options.claimLinkState || (await ensureTaskClaimLink(task)); const updatedTask = await updateTask(task.id, { task_status: String(task.task_status || "").trim() === "role_confirmed" ? task.task_status : "waiting_binding", claim_token: claimLinkState.token || task.claim_token || "", claim_expires_at: claimLinkState.expiredAt || getTaskClaimExpiresAt(task), role_id: String(task.task_status || "").trim() === "role_confirmed" ? task.role_id : "", role_name: String(task.task_status || "").trim() === "role_confirmed" ? task.role_name : "", last_error: "", context_json: JSON.stringify(nextContext), updated_at: now, }); await createTaskEvent( task.id, "kuaishou_cloud_bind_url_refreshed", { source: String(options.source || "system").trim() || "system", oldVnId, oldVnPhoneMasked: maskPhone(oldVnPhone), vnKey: preparedBinding.vnKey, vnId: preparedBinding.vnId, vnPhoneMasked: maskPhone(preparedBinding.vnPhone), actor, }, now ); return { task: updatedTask, claimUrl: claimLinkState.claimUrl, token: claimLinkState.token, flow: normalizeKuaishouCloudFlow(nextContext.kuaishouCloudFulfillment), }; } export async function probeKuaishouCloudTaskBindUrl(task, options = {}) { if (!isKuaishouCloudTask(task)) { throw createHttpError("当前任务不是快手 Cloud 履约任务", { statusCode: 409, errorCode: "kuaishou_cloud_task_invalid", }); } const now = nowIso(); const taskContext = parseTaskContext(task); const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment); if (!flow.binding.bindUrl) { return { task, flow, probe: null, }; } const probeIntervalMs = Number(resolveCloudtentaclesConfig().bindUrlProbeIntervalSeconds || 30) * 1000; if ( !options.force && flow.binding.bindProbeAt && Date.now() - Date.parse(flow.binding.bindProbeAt) < probeIntervalMs ) { return { task, flow, probe: null, }; } const probe = await probeCloudtentaclesBindUrl({ bindUrl: flow.binding.bindUrl, }); if (probe.expired || !isKuaishouCloudBindUrlFresh(flow)) { const refreshed = await refreshKuaishouCloudTaskBindUrl(task, { source: options.source || "claim_page_bind_url_expired", actor: options.actor || { source: "system" }, }); return { ...refreshed, probe, }; } const probeRoleInfo = normalizeKuaishouCloudRoleInfo(probe.roleInfo); const hasRoleInfo = Boolean(probeRoleInfo.name || probeRoleInfo.rid); const nextContext = { ...taskContext, kuaishouCloudFulfillment: { ...flow, binding: { ...flow.binding, bindProbeAt: now, bindProbeStatus: probe.valid ? "valid" : "invalid", bindProbeMessage: String(probe.message || probe.reason || "").trim(), roleName: hasRoleInfo ? probeRoleInfo.name : flow.binding.roleName, roleId: hasRoleInfo ? probeRoleInfo.rid : flow.binding.roleId, }, role: { ...flow.role, status: hasRoleInfo ? "ready" : flow.role.status, name: hasRoleInfo ? probeRoleInfo.name : flow.role.name, rid: hasRoleInfo ? probeRoleInfo.rid : flow.role.rid, refreshedAt: hasRoleInfo ? now : flow.role.refreshedAt, errorMessage: hasRoleInfo ? "" : flow.role.errorMessage, rawInfo: hasRoleInfo ? probeRoleInfo.rawInfo : flow.role.rawInfo, }, }, }; const updatedTask = await updateTask(task.id, { role_id: hasRoleInfo ? probeRoleInfo.rid : task.role_id, role_name: hasRoleInfo ? probeRoleInfo.name : task.role_name, context_json: JSON.stringify(nextContext), updated_at: now, }); return { task: updatedTask, flow: normalizeKuaishouCloudFlow(nextContext.kuaishouCloudFulfillment), probe, }; } /** * @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 ); await notifyKuaishouCloudBindUrlRefreshFailed({ task: updatedTask, errorMessage, }); return updatedTask; } export async function refreshKuaishouCloudTaskRoleInfo(task, options = {}) { if (!isKuaishouCloudTask(task)) { throw createHttpError("当前任务不是快手 Cloud 履约任务", { statusCode: 409, errorCode: "kuaishou_cloud_task_invalid", }); } const now = nowIso(); const actor = normalizeActor(options.actor); const taskContext = parseTaskContext(task); const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment); if (!flow.binding.vnId || !flow.binding.vnKey) { throw createHttpError( "当前任务还没有可查询的绑定角色信息,请先准备绑定资源", { statusCode: 409, errorCode: "kuaishou_cloud_missing_bind_info_context", } ); } if (flow.binding.bindUrl) { const probed = await probeKuaishouCloudTaskBindUrl(task, { source: options.source || "system_role_refresh_bind_url_probe", actor, force: options.forceProbe === true, }); const probedFlow = normalizeKuaishouCloudFlow( parseTaskContext(probed.task).kuaishouCloudFulfillment ); if (probed.probe?.expired) { return { task: probed.task, roleInfo: normalizeKuaishouCloudRoleInfo(null), flow: probedFlow, }; } if ( (probed.probe === null || probed.probe?.valid) && (probedFlow.binding.roleName || probedFlow.binding.roleId) ) { return { task: probed.task, roleInfo: { name: probedFlow.binding.roleName, rid: probedFlow.binding.roleId, rawInfo: probedFlow.role.rawInfo, }, flow: probedFlow, }; } } const cloudContext = resolvePersistedCloudtentaclesContext( flow.binding.cloudSourceKey || "default" ); const bindInfoResult = await getCloudtentaclesBindInfo({ ...cloudContext, key: flow.binding.vnKey, id: flow.binding.vnId, }); const bindInfo = normalizeKuaishouCloudRoleInfo(bindInfoResult.bindInfo); const hasRoleInfo = Boolean(bindInfo.name || bindInfo.rid); const nextContext = { ...taskContext, kuaishouCloudFulfillment: { ...flow, binding: { ...flow.binding, roleName: hasRoleInfo ? bindInfo.name : "", roleId: hasRoleInfo ? bindInfo.rid : "", }, role: { status: hasRoleInfo ? "ready" : "pending", name: hasRoleInfo ? bindInfo.name : "", rid: hasRoleInfo ? bindInfo.rid : "", refreshedAt: now, errorMessage: hasRoleInfo ? "" : "当前还没有查询到角色信息,请完成绑定后稍等片刻再试", rawInfo: bindInfo.rawInfo, }, }, }; const updatedTask = await updateTask(task.id, { role_id: hasRoleInfo ? bindInfo.rid : "", role_name: hasRoleInfo ? bindInfo.name : "", context_json: JSON.stringify(nextContext), updated_at: now, }); if (options.recordEvent !== false) { await createTaskEvent( task.id, "kuaishou_cloud_role_info_refreshed", { source: String(options.source || "system").trim() || "system", roleName: bindInfo.name, roleId: bindInfo.rid, vnId: flow.binding.vnId, actor, }, now ); } return { task: updatedTask, roleInfo: bindInfo, flow: normalizeKuaishouCloudFlow(nextContext.kuaishouCloudFulfillment), }; } export async function dispatchKuaishouCloudFulfillmentTask(task, options = {}) { if (!isKuaishouCloudTask(task)) { throw createHttpError("当前任务不是快手 Cloud 履约任务", { statusCode: 409, errorCode: "kuaishou_cloud_task_invalid", }); } const actor = normalizeActor(options.actor); const now = nowIso(); const taskContext = parseTaskContext(task); const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment); const cloudContext = resolvePersistedCloudtentaclesContext( flow.binding.cloudSourceKey || "default" ); if (!flow.binding.skuId || !flow.binding.vnId || !flow.binding.vnPhone) { throw createHttpError( "当前任务还没有准备好绑定资源,请先完成绑定资源准备", { statusCode: 409, errorCode: "kuaishou_cloud_not_prepared", } ); } const ticketCode = String(options.ticketCode || "").trim(); const persistedTicketCode = String(flow.ticket.code || "").trim(); if (!persistedTicketCode && !ticketCode) { throw createHttpError("客户还没有提交有效核销码,暂时不能继续兑换", { statusCode: 409, errorCode: "kuaishou_cloud_missing_ticket_code", }); } if ( flow.dispatch.status === "success" && String(task.task_status || "").trim() === "dispatched_pending_return" ) { return { task, flow }; } const dispatchResult = await useCloudtentaclesSku({ ...cloudContext, id: flow.binding.skuId, virtualNumberId: flow.binding.vnId, phone: flow.binding.vnPhone, }); const nextContext = { ...taskContext, kuaishouCloudFulfillment: { ...flow, ticket: { ...flow.ticket, code: ticketCode || persistedTicketCode, capturedAt: ticketCode ? now : flow.ticket.capturedAt, capturedBy: ticketCode && actor ? actor : flow.ticket.capturedBy, }, dispatch: { ...flow.dispatch, status: "success", dispatchAt: now, dispatchBy: actor, sendType: Number(dispatchResult.sendType || 0) || 0, note: String( dispatchResult.note || dispatchResult.responseMessage || "cloudtentacles 发货成功" ).trim(), }, }, }; let updatedTask = await updateTask(task.id, { task_status: "dispatched_pending_return", delivery_status: "delivered", result_code: "kuaishou_cloud_dispatched", result_message: String( dispatchResult.responseMessage || dispatchResult.note || "cloudtentacles 发货成功" ).trim(), user_action_status: "not_required", last_error: "", context_json: JSON.stringify(nextContext), updated_at: now, }); await createTaskEvent( task.id, "kuaishou_cloud_dispatched", { source: String(options.source || "system").trim() || "system", ticketCodeMasked: maskCode(ticketCode || persistedTicketCode), skuId: flow.binding.skuId, vnId: flow.binding.vnId, vnPhoneMasked: maskPhone(flow.binding.vnPhone), sendType: dispatchResult.sendType, note: dispatchResult.note, actor, }, now ); const shouldAutoFinalize = options.autoFinalize === true && normalizeKuaishouCloudFlow(nextContext.kuaishouCloudFulfillment) .returnNumber.autoReturnEnabled === true; if (shouldAutoFinalize) { const finalizeResult = await returnKuaishouCloudFulfillmentTask( updatedTask, { actor, source: options.source || "system_auto_finalize", } ); updatedTask = finalizeResult.task; } return { task: updatedTask, flow: normalizeKuaishouCloudFlow( parseTaskContext(updatedTask).kuaishouCloudFulfillment ), }; } export async function returnKuaishouCloudFulfillmentTask(task, options = {}) { if (!isKuaishouCloudTask(task)) { throw createHttpError("当前任务不是快手 Cloud 履约任务", { statusCode: 409, errorCode: "kuaishou_cloud_task_invalid", }); } const actor = normalizeActor(options.actor); const now = nowIso(); const taskContext = parseTaskContext(task); const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment); const cloudContext = resolvePersistedCloudtentaclesContext( flow.binding.cloudSourceKey || "default" ); if (!flow.binding.vnId || !flow.binding.vnKey) { throw createHttpError("当前任务缺少可退还的虚拟号信息", { statusCode: 409, errorCode: "kuaishou_cloud_missing_return_context", }); } if ( flow.returnNumber.status === "success" && ["completed", "manual_review"].includes( String(task.task_status || "").trim() ) ) { return { task, flow }; } await backCloudtentaclesVirtualNumber({ ...cloudContext, key: flow.binding.vnKey, id: flow.binding.vnId, }); const order = await getOrderById(task.order_id); const ticketCode = String(flow.ticket.code || "").trim(); const shopId = String(flow.consume.shopId || order?.shop_id || "").trim(); const shopName = String( flow.consume.shopName || order?.shop_name || "" ).trim(); const eticketSource = getKuaishouEticketSourceConfig(); const shopConfig = resolveKuaishouEticketShopConfig({ shopId, shopName, }); let consumeStatus = "pending"; let consumeErrorMessage = ""; let consumedAt = null; let nextTaskStatus = "completed"; let nextResultCode = "kuaishou_cloud_completed"; let nextResultMessage = "cloudtentacles 发货、退号并完成快手核销"; if (!order) { consumeStatus = "failed"; consumeErrorMessage = "任务关联订单不存在,无法执行快手核销"; } else if (!ticketCode) { consumeStatus = "failed"; consumeErrorMessage = "客户未提交有效核销码,无法执行快手核销"; } else if ( !shopConfig || shopConfig.enabled === false || !String(shopConfig.cookie || "").trim() ) { consumeStatus = "failed"; consumeErrorMessage = "订单对应快手小店缺少可用 Cookie,无法执行快手核销"; } else { try { const consumeResult = await consumeKuaishouEticket({ baseUrl: eticketSource.baseUrl, cookie: shopConfig.cookie, eTicketId: ticketCode, oid: String(flow.ticket.oid || "").trim(), formToken: String(flow.ticket.formToken || "").trim(), }); if (consumeResult.consumed) { consumeStatus = "success"; consumedAt = now; } else { consumeStatus = "failed"; consumeErrorMessage = String( consumeResult.errorMessage || "快手核销失败" ).trim(); } } catch (error) { consumeStatus = "failed"; consumeErrorMessage = error instanceof Error ? error.message : "快手核销失败"; } } if (consumeStatus !== "success") { nextTaskStatus = "manual_review"; nextResultCode = "kuaishou_cloud_consume_failed"; nextResultMessage = consumeErrorMessage || "号码已退还,但快手核销未完成,请人工处理"; } const nextContext = { ...taskContext, kuaishouCloudFulfillment: { ...flow, returnNumber: { ...flow.returnNumber, status: "success", returnedAt: now, returnedBy: actor, }, consume: { ...flow.consume, status: consumeStatus, shopId: shopId || flow.consume.shopId, shopName, autoConsumeEnabled: flow.consume.autoConsumeEnabled === true, consumedAt, errorMessage: consumeErrorMessage, }, }, }; const updatedTask = await updateTask(task.id, { task_status: nextTaskStatus, delivery_status: "delivered", result_code: nextResultCode, result_message: nextResultMessage, redeemed_at: consumeStatus === "success" ? now : task.redeemed_at, last_error: consumeErrorMessage, context_json: JSON.stringify(nextContext), updated_at: now, }); await createTaskEvent( task.id, "kuaishou_cloud_number_returned", { source: String(options.source || "system").trim() || "system", vnId: flow.binding.vnId, vnPhoneMasked: maskPhone(flow.binding.vnPhone), actor, }, now ); await createTaskEvent( task.id, consumeStatus === "success" ? "kuaishou_cloud_consumed" : "kuaishou_cloud_consume_failed", { source: String(options.source || "system").trim() || "system", ticketCodeMasked: maskCode(ticketCode), shopId, shopName, consumeStatus, errorMessage: consumeErrorMessage, actor, }, now ); if (consumeStatus !== "success") { await notifyKuaishouCloudConsumeFailed({ task: updatedTask, order, ticketCodeMasked: maskCode(ticketCode), shopId, shopName, errorMessage: consumeErrorMessage, }); } return { task: updatedTask, flow: normalizeKuaishouCloudFlow( parseTaskContext(updatedTask).kuaishouCloudFulfillment ), }; } export function maskPhone(value) { const text = String(value || "").trim(); if (!text) { return ""; } if (text.length <= 7) { return `${text.slice(0, 2)}***${text.slice(-2)}`; } return `${text.slice(0, 3)}****${text.slice(-4)}`; } export function maskCode(value) { const text = String(value || "").trim(); if (!text) { return ""; } if (text.length <= 8) { return `${text.slice(0, 2)}***${text.slice(-2)}`; } return `${text.slice(0, 4)}****${text.slice(-4)}`; } export function resolveKuaishouCloudBindUrlExpiresAt(preparedAt) { const preparedTime = Date.parse(String(preparedAt || "")); if (!Number.isFinite(preparedTime)) { return null; } const config = resolveCloudtentaclesConfig(); const ttlSeconds = Number(config.bindUrlTtlSeconds || 600); return new Date(preparedTime + Math.max(1, ttlSeconds) * 1000).toISOString(); } export function isKuaishouCloudBindUrlFresh(flow, now = new Date()) { const normalizedFlow = normalizeKuaishouCloudFlow(flow); if (!normalizedFlow.binding.bindUrl) { return false; } const expiresAt = normalizedFlow.binding.bindExpiresAt; if (!expiresAt) { return false; } const expiresTime = Date.parse(String(expiresAt || "")); if (!Number.isFinite(expiresTime)) { return false; } return expiresTime > now.getTime(); } function resolveKuaishouCloudBindingResources( flow, { skuItems = [], knapsackItems = [] } = {} ) { const normalizedSkuItems = Array.isArray(skuItems) ? skuItems.filter(isCloudSkuLikeItem) : []; const normalizedKnapsackItems = Array.isArray(knapsackItems) ? knapsackItems.filter(isCloudSkuLikeItem) : []; const currentSkuId = Number(flow?.binding?.skuId || 0) || 0; const currentSkuName = String(flow?.binding?.skuName || "").trim(); const nameCandidates = collectKuaishouCloudNameCandidates(flow); const skuItemById = currentSkuId > 0 ? normalizedSkuItems.find( (item) => Number(item.id || 0) === currentSkuId ) || null : null; const knapsackItemById = currentSkuId > 0 ? normalizedKnapsackItems.find( (item) => Number(item.id || 0) === currentSkuId ) || null : null; if (skuItemById || knapsackItemById) { const matchedItem = skuItemById || knapsackItemById; return { skuId: Number(matchedItem?.id || 0) || 0, skuName: String(currentSkuName || matchedItem?.name || "").trim(), vnKey: KUAISHOU_CLOUD_FIXED_VN_KEY, skuItem: skuItemById, knapsackItem: knapsackItemById, resolvedByName: false, }; } const matchedSkuItem = findCloudItemByNames( normalizedSkuItems, nameCandidates ); const matchedKnapsackItem = findCloudItemByNames( normalizedKnapsackItems, nameCandidates, matchedSkuItem ? Number(matchedSkuItem.id || 0) : 0 ); const matchedItem = matchedSkuItem || matchedKnapsackItem; return { skuId: Number(matchedItem?.id || 0) || 0, skuName: String(currentSkuName || matchedItem?.name || "").trim(), vnKey: KUAISHOU_CLOUD_FIXED_VN_KEY, skuItem: matchedSkuItem, knapsackItem: matchedKnapsackItem, resolvedByName: Boolean(matchedItem), }; } function resolveKuaishouCloudVnKeyCandidates(_input = {}) { return [KUAISHOU_CLOUD_FIXED_VN_KEY]; } async function prepareKuaishouCloudBindResourceWithFallback(input = {}) { const { cloudContext = {}, vnKeyCandidates = [] } = input; const candidates = Array.isArray(vnKeyCandidates) ? vnKeyCandidates : []; let lastError = null; for (const vnKey of candidates) { let vnId = 0; let vnPhone = ""; try { const appointed = await appointCloudtentaclesVirtualNumber({ ...cloudContext, key: vnKey, }); vnId = Number(appointed.item?.id || 0); vnPhone = String(appointed.item?.phone || "").trim(); if (!vnId || !vnPhone) { throw createHttpError("申请虚拟号成功但返回数据不完整", { statusCode: 502, errorCode: "kuaishou_cloud_invalid_vn", }); } await generateCloudtentaclesLoginCode({ ...cloudContext, key: vnKey, id: vnId, }); const fetchedCode = await fetchCloudtentaclesVirtualNumberCode({ ...cloudContext, key: vnKey, phone: vnPhone, }); await verifyCloudtentaclesLoginCode({ ...cloudContext, key: vnKey, id: vnId, code: fetchedCode.code, }); const bindUrlResult = await getCloudtentaclesBindUrl({ ...cloudContext, key: vnKey, id: vnId, }); return { vnKey, vnId, vnPhone, bindUrl: String(bindUrlResult.bindUrl || "").trim(), }; } catch (error) { lastError = error; if (vnId > 0) { try { await backCloudtentaclesVirtualNumber({ ...cloudContext, key: vnKey, id: vnId, }); } catch { // 退号失败保留主错误 } } if (!isRecoverableKuaishouCloudVnKeyError(error)) { throw error; } } } throw ( lastError || createHttpError("没有找到可用的 VN Key", { statusCode: 409, errorCode: "kuaishou_cloud_missing_binding_config", }) ); } function collectKuaishouCloudNameCandidates(flow) { return Array.from( new Set( [ String(flow?.binding?.skuName || "").trim(), String(flow?.internalSkuName || "").trim(), String(flow?.internalSkuCode || "").trim(), ].filter(Boolean) ) ); } function findCloudItemByNames(items, nameCandidates, preferredId = 0) { const normalizedItems = Array.isArray(items) ? items : []; const normalizedNames = nameCandidates .map((item) => ({ raw: String(item || "").trim(), normalized: normalizeProductName(item), })) .filter((item) => item.raw && item.normalized); if (normalizedNames.length === 0 || normalizedItems.length === 0) { return preferredId > 0 ? normalizedItems.find((item) => Number(item.id || 0) === preferredId) || null : null; } if (preferredId > 0) { const preferred = normalizedItems.find((item) => Number(item.id || 0) === preferredId) || null; if (preferred) { return preferred; } } const exactMatches = normalizedItems.filter((item) => { const itemName = normalizeProductName(item.name); return normalizedNames.some( (candidate) => candidate.normalized === itemName ); }); if (exactMatches.length > 0) { return exactMatches[0]; } const partialMatches = normalizedItems.filter((item) => { const itemName = normalizeProductName(item.name); return normalizedNames.some( (candidate) => itemName.includes(candidate.normalized) || candidate.normalized.includes(itemName) ); }); if (partialMatches.length > 0) { return partialMatches.sort( (left, right) => String(left.name || "").length - String(right.name || "").length )[0]; } return null; } function isCloudSkuLikeItem(item) { return Boolean(item) && typeof item === "object" && Number(item.id || 0) > 0; } function isRecoverableKuaishouCloudVnKeyError(error) { const errorCode = String(error?.errorCode || error?.code || "").trim(); const errorMessage = String(error?.message || "").trim(); return ( errorCode === "cloudtentacles_vn_bind_url_failed" && errorMessage.includes("不支持的游戏类型") ); } function normalizeActor(actor) { if (!actor || typeof actor !== "object") { return null; } const source = String(actor.source || "").trim(); const userId = Number(actor.userId || 0) || 0; const username = String(actor.username || "").trim(); const role = String(actor.role || "").trim(); if (!source && !userId && !username && !role) { return null; } return { source, userId, username, role, }; } function parseTaskContext(task) { const value = task?.context_json; if (!value) { return {}; } if (typeof value === "object") { return value; } try { return JSON.parse(String(value || "{}")); } catch { return {}; } } function getTaskClaimExpiresAt(task) { return task?.claim_expires_at || task?.primary_claim_expires_at || null; } function isClaimExpired(expiredAt) { if (!expiredAt) { return false; } const timestamp = new Date(expiredAt).getTime(); return Number.isFinite(timestamp) && timestamp <= Date.now(); }