添加快手 Cloud 换绑角色功能

This commit is contained in:
yml
2026-05-30 11:23:52 +08:00
parent 681ff44cbc
commit 1c50569ea9
22 changed files with 1537 additions and 953 deletions
@@ -456,6 +456,286 @@ export async function refreshKuaishouCloudTaskBindUrl(task: TaskRow, options: Js
}
}
export async function rebindKuaishouCloudTaskRole(task: TaskRow, options: JsonObject = {}) {
if (!isKuaishouCloudTask(task)) {
throw createHttpError('当前任务不是快手 Cloud 履约任务', {
statusCode: 409,
errorCode: 'kuaishou_cloud_task_invalid',
})
}
const normalizedStatus = normalizeTaskStatus(task.task_status)
if (
![
TASK_STATUS.WAITING_BINDING,
TASK_STATUS.ROLE_CONFIRMED,
TASK_STATUS.MANUAL_REVIEW,
TASK_STATUS.RETRY_PENDING,
].includes(normalizedStatus as any)
) {
throw createHttpError('当前任务状态不可换绑角色', {
statusCode: 409,
errorCode: 'kuaishou_cloud_rebind_not_allowed',
})
}
const now = nowIso()
const actor = normalizeActor(options.actor)
const taskContext = parseTaskContext(task)
const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment)
if (
flow.dispatch.status === 'success' ||
normalizeTaskStatus(task.task_status) === TASK_STATUS.DISPATCHED_PENDING_RETURN
) {
throw createHttpError('当前任务已经发货,不能换绑角色', {
statusCode: 409,
errorCode: 'kuaishou_cloud_rebind_after_dispatch_forbidden',
})
}
if (!flow.binding.vnId || !flow.binding.vnKey) {
throw createHttpError('当前任务缺少可退还的虚拟号信息,请先准备绑定资源', {
statusCode: 409,
errorCode: 'kuaishou_cloud_rebind_missing_binding_context',
})
}
const source = String(options.source || 'system_rebind_role').trim() || 'system_rebind_role'
const cloudContext = resolvePersistedCloudtentaclesContextBySourceKeys([
flow.binding.resolvedSourceKey,
...flow.binding.cloudSourceKeys,
])
const oldBinding = {
vnKey: flow.binding.vnKey,
vnId: flow.binding.vnId,
vnPhone: flow.binding.vnPhone,
bindUrl: flow.binding.bindUrl,
bindPreparedAt: flow.binding.bindPreparedAt,
bindExpiresAt: flow.binding.bindExpiresAt,
roleName: flow.binding.roleName || flow.role.name || task.role_name || '',
roleId: flow.binding.roleId || flow.role.rid || task.role_id || '',
}
const previousRebind: JsonObject =
flow.rebind && typeof flow.rebind === 'object' ? (flow.rebind as JsonObject) : {}
const history = Array.isArray(previousRebind.history) ? previousRebind.history : []
const attempt = Math.max(1, Number(previousRebind.currentAttempt || history.length || 0) + 1)
const baseHistoryItem = {
attempt,
source,
requestedAt: now,
requestedBy: actor,
oldBinding,
}
await createTaskEvent(
task.id,
'kuaishou_cloud_rebind_requested',
{
source,
attempt,
oldVnId: oldBinding.vnId,
oldVnPhoneMasked: maskPhone(oldBinding.vnPhone),
oldRoleName: oldBinding.roleName,
oldRoleId: oldBinding.roleId,
actor,
},
now,
)
await backCloudtentaclesVirtualNumber({
...cloudContext,
key: oldBinding.vnKey,
id: oldBinding.vnId,
})
await createTaskEvent(
task.id,
'kuaishou_cloud_rebind_old_number_returned',
{
source,
attempt,
vnKey: oldBinding.vnKey,
vnId: oldBinding.vnId,
vnPhoneMasked: maskPhone(oldBinding.vnPhone),
actor,
},
now,
)
let preparedBinding
try {
preparedBinding = await prepareKuaishouCloudBindResourceWithFallback({
cloudContext,
vnKeyCandidates: resolveKuaishouCloudVnKeyCandidates({
flow,
binding: flow.binding,
}),
})
} catch (error) {
const errorMessage = error instanceof Error ? error.message : '新绑定资源准备失败'
const failedContext = {
...taskContext,
kuaishouCloudFulfillment: {
...flow,
binding: {
...flow.binding,
prepareStatus: 'pending',
vnId: 0,
vnPhone: '',
bindUrl: '',
bindPreparedAt: null,
bindExpiresAt: null,
bindProbeAt: now,
bindProbeStatus: 'rebind_failed',
bindProbeMessage: errorMessage,
roleName: '',
roleId: '',
},
role: {
status: 'pending',
name: '',
rid: '',
refreshedAt: now,
errorMessage: `旧虚拟号已退还,新绑定资源准备失败:${errorMessage}`,
rawInfo: null,
},
rebind: {
...previousRebind,
currentAttempt: attempt,
history: [
...history,
{
...baseHistoryItem,
status: 'failed',
errorMessage,
},
],
},
},
}
const failedTask = await updateTask(task.id, {
task_status: TASK_STATUS.PENDING_BINDING_PREPARE,
role_id: '',
role_name: '',
role_confirmed_at: null,
last_error: `换绑失败,旧虚拟号已退还,新绑定资源准备失败:${errorMessage}`,
context_json: JSON.stringify(failedContext),
updated_at: now,
})
await createTaskEvent(
task.id,
'kuaishou_cloud_rebind_failed',
{
source,
attempt,
errorMessage,
actor,
},
now,
)
return {
task: failedTask || task,
claimUrl: buildClaimUrl(String(task.primary_claim_token || task.claim_token || '')),
token: String(task.primary_claim_token || task.claim_token || ''),
flow: normalizeKuaishouCloudFlow(
parseTaskContext(failedTask || task).kuaishouCloudFulfillment,
),
}
}
const claimLinkState = await ensureTaskClaimLink(task)
const nextBindExpiresAt = resolveKuaishouCloudBindUrlExpiresAt(now)
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: nextBindExpiresAt,
bindProbeAt: null,
bindProbeStatus: 'pending',
bindProbeMessage: '',
roleName: '',
roleId: '',
},
role: {
status: 'pending',
name: '',
rid: '',
refreshedAt: null,
errorMessage: '',
rawInfo: null,
},
rebind: {
...previousRebind,
currentAttempt: attempt,
history: [
...history,
{
...baseHistoryItem,
newBinding: {
vnKey: preparedBinding.vnKey,
vnId: preparedBinding.vnId,
vnPhone: preparedBinding.vnPhone,
bindUrl: preparedBinding.bindUrl,
bindPreparedAt: now,
bindExpiresAt: nextBindExpiresAt,
},
status: 'success',
errorMessage: '',
},
],
},
},
}
const updatedTask = await updateTask(task.id, {
task_status: TASK_STATUS.WAITING_BINDING,
user_action_status: 'pending_claim',
claim_token: claimLinkState.token || task.claim_token || '',
claim_expires_at: claimLinkState.expiredAt || getTaskClaimExpiresAt(task),
role_id: '',
role_name: '',
role_confirmed_at: null,
last_error: '',
context_json: JSON.stringify(nextContext),
updated_at: now,
})
await createTaskEvent(
task.id,
'kuaishou_cloud_rebind_prepared',
{
source,
attempt,
oldVnId: oldBinding.vnId,
oldVnPhoneMasked: maskPhone(oldBinding.vnPhone),
vnKey: preparedBinding.vnKey,
vnId: preparedBinding.vnId,
vnPhoneMasked: maskPhone(preparedBinding.vnPhone),
bindUrl: preparedBinding.bindUrl,
actor,
},
now,
)
return {
task: updatedTask,
claimUrl: claimLinkState.claimUrl,
token: claimLinkState.token,
flow: normalizeKuaishouCloudFlow(nextContext.kuaishouCloudFulfillment),
}
}
export async function probeKuaishouCloudTaskBindUrl(task: TaskRow, options: JsonObject = {}) {
if (!isKuaishouCloudTask(task)) {
throw createHttpError('当前任务不是快手 Cloud 履约任务', {