偿还 lewan 工程债:模块拆分、默认角色降级与短链收口

将 kuaishou-cloud 大入口拆为 prepare/rebind/probe/refresh 等模块;默认角色仅作诊断;补充兑换状态单测;短链明确只读兼容历史链接。
This commit is contained in:
yml2213
2026-07-10 14:01:29 +08:00
parent c70c3ff6a5
commit d6b0e45c7b
15 changed files with 1508 additions and 1190 deletions
+4
View File
@@ -1,3 +1,7 @@
/**
* 历史短链只读跳转入口(/s/:code)。
* 新 claim 交付请使用本站 /#/claim/{token},勿再为 feifei 生成 shortLink。
*/
import { Router } from 'express' import { Router } from 'express'
import { resolveShortLinkTarget } from '../services/short-links/short-link-service.js' import { resolveShortLinkTarget } from '../services/short-links/short-link-service.js'
@@ -0,0 +1,97 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import {
TASK_STATUS,
canRedeemKuaishouCloudClaimStatus,
canTaskTransition,
normalizeTaskStatus,
} from '../../domain/task-status.js'
import {
assertBoundUidMatchesExpected,
assertClaimExpectedUidReady,
isClaimUidMatched,
} from './claim-identity.js'
/**
* 一键兑换前置:waiting_binding + UID 匹配 → 可升到 role_confirmed → redeeming
*/
function canPromoteWaitingBindingToRedeem(options: {
status: unknown
expectedUid: string
boundUid: string
}) {
const status = normalizeTaskStatus(options.status)
if (!canRedeemKuaishouCloudClaimStatus(status)) {
return { ok: false, reason: 'status_not_redeemable' as const }
}
if (!options.expectedUid) {
return { ok: false, reason: 'uid_missing' as const }
}
if (!isClaimUidMatched(options.expectedUid, options.boundUid)) {
return { ok: false, reason: 'uid_mismatch' as const }
}
if (status === TASK_STATUS.WAITING_BINDING) {
if (!canTaskTransition(TASK_STATUS.WAITING_BINDING, TASK_STATUS.ROLE_CONFIRMED)) {
return { ok: false, reason: 'cannot_promote' as const }
}
}
if (!canTaskTransition(TASK_STATUS.ROLE_CONFIRMED, TASK_STATUS.REDEEMING)) {
return { ok: false, reason: 'cannot_redeem' as const }
}
return { ok: true as const, reason: 'ok' as const }
}
test('waiting_binding + uid 匹配可一键兑换', () => {
const result = canPromoteWaitingBindingToRedeem({
status: TASK_STATUS.WAITING_BINDING,
expectedUid: '10001',
boundUid: '10001',
})
assert.deepEqual(result, { ok: true, reason: 'ok' })
})
test('waiting_binding + uid 不一致不可兑换', () => {
const result = canPromoteWaitingBindingToRedeem({
status: TASK_STATUS.WAITING_BINDING,
expectedUid: '10001',
boundUid: '99999',
})
assert.equal(result.ok, false)
assert.equal(result.reason, 'uid_mismatch')
})
test('link_generated 状态不可直接 redeem', () => {
const result = canPromoteWaitingBindingToRedeem({
status: TASK_STATUS.LINK_GENERATED,
expectedUid: '10001',
boundUid: '10001',
})
assert.equal(result.ok, false)
assert.equal(result.reason, 'status_not_redeemable')
})
test('assertBoundUidMatchesExpected 在不匹配时抛错', () => {
assert.throws(
() =>
assertBoundUidMatchesExpected(
{
context_json: JSON.stringify({
claimIdentity: { expectedUid: '10001' },
}),
},
{
binding: { roleId: '999', roleName: 'x', vnPhone: '1' },
role: { rid: '999', name: 'x' },
},
),
/不一致/,
)
})
test('assertClaimExpectedUidReady 要求 claimIdentity', () => {
assert.throws(
() => assertClaimExpectedUidReady({ context_json: '{}' }),
/填写游戏 UID/,
)
})
@@ -36,7 +36,8 @@ export async function syncKuaishouCloudRoleInfo(task: TaskRow) {
source: 'claim_page_polling', source: 'claim_page_polling',
actor: { source: 'system' }, actor: { source: 'system' },
recordEvent: false, recordEvent: false,
forceProbe: flow.role.isDefaultRole === true, // 轮询阶段始终允许探测绑链,默认角色仅作诊断
forceProbe: true,
}) })
return result.task || task return result.task || task
} catch { } catch {
@@ -0,0 +1,58 @@
import type { JsonObject } from './domain.js'
export type CloudDeliveryPlanItem = {
cloudSkuId: number
cloudSkuName: string
quantity: number
skuItem: JsonObject | null
knapsackItem: JsonObject | null
knapsackCount: number
missingCount: number
}
export function resolveKuaishouCloudDeliveryPlan(
flow: JsonObject,
{ skuItems = [], knapsackItems = [] }: { skuItems?: unknown[]; knapsackItems?: unknown[] } = {},
): { items: CloudDeliveryPlanItem[] } {
const normalizedSkuItems = Array.isArray(skuItems) ? skuItems.filter(isCloudSkuLikeItem) : []
const normalizedKnapsackItems = Array.isArray(knapsackItems)
? knapsackItems.filter(isCloudSkuLikeItem)
: []
const deliveryItems = Array.isArray(flow.deliveryItems) ? flow.deliveryItems : []
return {
items: deliveryItems
.map((item: unknown) => {
const source = item && typeof item === 'object' ? (item as JsonObject) : {}
const cloudSkuId = Number(source.cloudSkuId || source.skuId || 0) || 0
const quantity = Number(source.quantity || 1) || 1
if (!cloudSkuId || !Number.isInteger(quantity) || quantity <= 0) {
return null
}
const skuItem = normalizedSkuItems.find((sku) => Number(sku.id || 0) === cloudSkuId) || null
const knapsackItem =
normalizedKnapsackItems.find((sku) => Number(sku.id || 0) === cloudSkuId) || null
const knapsackCount = Math.max(0, Number(knapsackItem?.count || 0) || 0)
const cloudSkuName = String(
source.cloudSkuName || source.skuName || skuItem?.name || knapsackItem?.name || '',
).trim()
return {
cloudSkuId,
cloudSkuName,
quantity,
skuItem,
knapsackItem,
knapsackCount,
missingCount: Math.max(0, quantity - knapsackCount),
}
})
.filter((item): item is CloudDeliveryPlanItem => Boolean(item)),
}
}
function isCloudSkuLikeItem(item: unknown): item is JsonObject {
const current = item && typeof item === 'object' ? (item as JsonObject) : {}
return Number(current.id || 0) > 0
}
@@ -1,46 +1,69 @@
import assert from 'node:assert/strict'
import test from 'node:test' import test from 'node:test'
import assert from 'node:assert/strict'
import { import {
hasKuaishouCloudCustomerRole, hasKuaishouCloudCustomerRole,
normalizeKuaishouCloudFlow, hasKuaishouCloudDefaultRoleSnapshot,
isSameKuaishouCloudRoleIdentity,
} from './domain.js' } from './domain.js'
import { buildRoleStateFromCurrentInfo } from './role-state.js'
import { normalizeKuaishouCloudFlow } from './domain.js'
test('hasKuaishouCloudCustomerRole 要求当前角色不同于虚拟机默认角色', () => { test('hasKuaishouCloudCustomerRole 要求已有绑定角色信息(UID 主闸后默认角色不再作门槛)', () => {
const defaultRoleFlow = normalizeKuaishouCloudFlow({ const defaultRoleFlow = {
binding: { binding: {
vnPhone: '13800000000', vnPhone: '13800000000',
roleName: '默认角色', roleName: '默认机位角色',
roleId: '10001', roleId: 'DEFAULT-000',
}, },
role: { role: {
name: '默认角色', defaultName: '默认机位角色',
rid: '10001', defaultRid: 'DEFAULT-000',
defaultName: '默认角色',
defaultRid: '10001',
defaultCapturedAt: '2026-05-31T00:00:00.000Z',
isDefaultRole: false,
}, },
}) }
assert.equal(defaultRoleFlow.role.isDefaultRole, true) assert.equal(hasKuaishouCloudCustomerRole(defaultRoleFlow), true)
assert.equal(hasKuaishouCloudCustomerRole(defaultRoleFlow), false) assert.equal(hasKuaishouCloudDefaultRoleSnapshot(defaultRoleFlow), true)
const customerRoleFlow = normalizeKuaishouCloudFlow({ const emptyFlow = {
...defaultRoleFlow,
binding: { binding: {
...defaultRoleFlow.binding, vnPhone: '13800000000',
roleName: '用户角色', roleName: '',
roleId: '20002', roleId: '',
}, },
}
assert.equal(hasKuaishouCloudCustomerRole(emptyFlow), false)
})
test('buildRoleStateFromCurrentInfo 即使接近默认角色也 ready 并保留 rid', () => {
const flow = normalizeKuaishouCloudFlow({
role: { role: {
...defaultRoleFlow.role, defaultName: '默认机位角色',
name: '用户角色', defaultRid: 'DEFAULT-000',
rid: '20002',
isDefaultRole: true,
}, },
binding: {},
})
const state = buildRoleStateFromCurrentInfo({
flow,
roleInfo: { name: '默认机位角色', rid: 'DEFAULT-000' },
now: '2026-07-10T00:00:00.000Z',
emptyMessage: 'empty',
}) })
assert.equal(customerRoleFlow.role.isDefaultRole, false) assert.equal(state.hasRoleInfo, true)
assert.equal(hasKuaishouCloudCustomerRole(customerRoleFlow), true) assert.equal(state.isDefaultRole, true)
assert.equal(state.role.status, 'ready')
assert.equal(state.bindingRoleId, 'DEFAULT-000')
assert.match(state.role.errorMessage, /UID/)
})
test('isSameKuaishouCloudRoleIdentity prefers rid', () => {
assert.equal(
isSameKuaishouCloudRoleIdentity({ name: 'a', rid: '1' }, { name: 'b', rid: '1' }),
true,
)
assert.equal(
isSameKuaishouCloudRoleIdentity({ name: 'a', rid: '1' }, { name: 'a', rid: '2' }),
false,
)
}) })
@@ -141,20 +141,19 @@ export function hasKuaishouCloudDefaultRoleSnapshot(flowLike: unknown) {
return Boolean(flow.role.defaultName || flow.role.defaultRid) return Boolean(flow.role.defaultName || flow.role.defaultRid)
} }
/**
* 是否已有可展示的绑定角色信息(诊断用)。
*
* 注意:履约主闸已改为 claimIdentity.expectedUid 匹配;
* 本函数不再要求「当前角色必须不同于虚拟机默认角色」。
*/
export function hasKuaishouCloudCustomerRole(flowLike: unknown) { export function hasKuaishouCloudCustomerRole(flowLike: unknown) {
const flow = normalizeKuaishouCloudFlow(flowLike) const flow = normalizeKuaishouCloudFlow(flowLike)
if (!flow.binding.vnPhone || !flow.binding.roleName || !flow.binding.roleId) { if (!flow.binding.vnPhone) {
return false return false
} }
if (!hasKuaishouCloudDefaultRoleSnapshot(flow)) { return Boolean(flow.binding.roleName || flow.binding.roleId)
return false
}
return !isSameKuaishouCloudRoleIdentity(
{ name: flow.binding.roleName, rid: flow.binding.roleId },
{ name: flow.role.defaultName, rid: flow.role.defaultRid },
)
} }
export function isSameKuaishouCloudRoleIdentity( export function isSameKuaishouCloudRoleIdentity(
@@ -0,0 +1,24 @@
import { buildClaimUrl, createTaskClaimToken } from '../../claim/claim-service.js'
import type { TaskRow } from '../../../types/repository/rows.js'
import { getTaskClaimExpiresAt, isClaimExpired } from './task-context.js'
export async function ensureTaskClaimLink(task: TaskRow) {
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,
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,480 @@
import { createTaskEvent } from '../../../repositories/task-event-repo.js'
import { updateTask } from '../../../repositories/task-repo.js'
import { buildClaimUrl } from '../../claim/claim-service.js'
import { listCloudtentaclesSku } from '../../platforms/cloudtentacles/catalog-service.js'
import { getCloudtentaclesKnapsack } from '../../platforms/cloudtentacles/knapsack-service.js'
import { backCloudtentaclesVirtualNumber } from '../../platforms/cloudtentacles/virtual-number-service.js'
import { notifyKuaishouCloudBindUrlRefreshFailed } from '../../notification/domain-notifications.js'
import { createHttpError } from '../../../utils/http.js'
import { nowIso } from '../../../utils/time.js'
import { TASK_STATUS, normalizeTaskStatus } from '../../../domain/task-status.js'
import {
KUAISHOU_CLOUD_FIXED_VN_KEY,
isKuaishouCloudBindUrlFresh,
isKuaishouCloudTask,
maskPhone,
normalizeKuaishouCloudFlow,
resolveKuaishouCloudBindUrlExpiresAt,
type JsonObject,
} from './domain.js'
import {
prepareKuaishouCloudBindResourceWithFallback,
resolveKuaishouCloudBindingResources,
resolveKuaishouCloudVnKeyCandidates,
} from './binding-resources.js'
import {
buildCloudtentaclesSourceSelectionEventDetail,
selectCloudtentaclesSourceForFulfillment,
} from './account-selector.js'
import { resolvePersistedCloudtentaclesContextBySourceKeys } from './cloudtentacles-context.js'
import {
getTaskClaimExpiresAt,
normalizeActor,
parseTaskContext,
} from './task-context.js'
import { resolveKuaishouCloudDeliveryPlan } from './delivery-plan.js'
import { ensureTaskClaimLink } from './ensure-claim-link.js'
import {
buildPendingRoleWithDefaultSnapshot,
captureKuaishouCloudDefaultRoleSnapshot,
} from './role-state.js'
import type { TaskRow } from '../../../types/repository/rows.js'
export async function prepareKuaishouCloudFulfillmentTask(task: TaskRow, options: JsonObject = {}) {
if (!isKuaishouCloudTask(task)) {
throw createHttpError('当前任务不是 kuaishou-lewan 履约任务', {
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 (normalizeTaskStatus(task.task_status) !== TASK_STATUS.WAITING_BINDING) {
readyTask =
(await updateTask(task.id, {
task_status: TASK_STATUS.WAITING_BINDING,
claim_token: claimLinkState.token || task.claim_token || '',
claim_expires_at: claimLinkState.expiredAt || getTaskClaimExpiresAt(task),
updated_at: now,
})) || task
}
return {
task: readyTask,
claimUrl: claimLinkState.claimUrl,
token: claimLinkState.token,
flow,
}
}
const selectedCloud = await selectCloudtentaclesSourceForFulfillment(flow)
try {
const cloudContext = selectedCloud.context
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 deliveryPlan = resolveKuaishouCloudDeliveryPlan(flowWithResolvedBinding, {
skuItems: Array.isArray(skuList.items) ? skuList.items : [],
knapsackItems: Array.isArray(knapsack.items) ? knapsack.items : [],
})
if (deliveryPlan.items.length === 0) {
throw createHttpError('当前任务缺少 cloud 发货物品配置', {
statusCode: 409,
errorCode: 'kuaishou_cloud_missing_delivery_items',
})
}
const usedKnapsack = deliveryPlan.items.every((item) => item.missingCount <= 0)
if (!usedKnapsack && !flowWithResolvedBinding.purchase.autoBuyEnabled) {
throw createHttpError('背包中没有现成库存,且当前配置未开启自动购买', {
statusCode: 409,
errorCode: 'kuaishou_cloud_auto_buy_disabled',
})
}
const missingSku = deliveryPlan.items.find((item) => item.missingCount > 0 && !item.skuItem)
if (missingSku) {
throw createHttpError(`cloudtentacles 未找到 SKU ${missingSku.cloudSkuId}`, {
statusCode: 404,
errorCode: 'kuaishou_cloud_sku_not_found',
})
}
const purchaseTriggered = false
const assetBefore = 0
const assetAfter = 0
const preparedBinding = await prepareKuaishouCloudBindResourceWithFallback({
cloudContext,
vnKeyCandidates,
})
const defaultRoleSnapshot = await captureKuaishouCloudDefaultRoleSnapshot({
cloudContext,
preparedBinding,
now,
})
const nextContext = {
...taskContext,
kuaishouCloudFulfillment: {
...flowWithResolvedBinding,
binding: {
...flowWithResolvedBinding.binding,
resolvedSourceKey: cloudContext.resolvedSourceKey,
vnKey: preparedBinding.vnKey,
prepareStatus: 'ready',
vnId: preparedBinding.vnId,
vnPhone: preparedBinding.vnPhone,
bindUrl: preparedBinding.bindUrl,
bindPreparedAt: now,
bindExpiresAt: resolveKuaishouCloudBindUrlExpiresAt(now),
bindProbeAt: null as null,
bindProbeStatus: 'pending',
bindProbeMessage: '',
roleName: '',
roleId: '',
},
role: buildPendingRoleWithDefaultSnapshot(defaultRoleSnapshot),
purchase: {
...flowWithResolvedBinding.purchase,
usedKnapsack,
purchaseTriggered,
assetBefore,
assetAfter,
items: deliveryPlan.items.map((item) => ({
cloudSkuId: item.cloudSkuId,
cloudSkuName: item.cloudSkuName,
requiredCount: item.quantity,
knapsackCount: item.knapsackCount,
purchasedCount: 0,
})),
purchaseAt: flowWithResolvedBinding.purchase.purchaseAt,
},
},
}
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: '',
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,
deliveryItems: deliveryPlan.items.map((item) => ({
cloudSkuId: item.cloudSkuId,
cloudSkuName: item.cloudSkuName,
quantity: item.quantity,
knapsackCount: item.knapsackCount,
purchasedCount: 0,
})),
resolvedByName: resolvedBinding.resolvedByName,
accountSelection: buildCloudtentaclesSourceSelectionEventDetail(selectedCloud),
defaultRoleName: defaultRoleSnapshot.defaultName,
defaultRoleId: defaultRoleSnapshot.defaultRid,
defaultRoleCaptureStatus: defaultRoleSnapshot.defaultCaptureStatus,
actor,
},
now,
)
return {
task: updatedTask,
claimUrl: claimLinkState.claimUrl,
token: claimLinkState.token,
flow: normalizeKuaishouCloudFlow(nextContext.kuaishouCloudFulfillment),
}
} finally {
selectedCloud.release()
}
}
export async function refreshKuaishouCloudTaskBindUrl(task: TaskRow, options: JsonObject = {}) {
if (!isKuaishouCloudTask(task)) {
throw createHttpError('当前任务不是 kuaishou-lewan 履约任务', {
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 = resolvePersistedCloudtentaclesContextBySourceKeys([
flow.binding.resolvedSourceKey,
...flow.binding.cloudSourceKeys,
])
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 defaultRoleSnapshot =
normalizeTaskStatus(task.task_status) === TASK_STATUS.ROLE_CONFIRMED
? null
: await captureKuaishouCloudDefaultRoleSnapshot({
cloudContext,
preparedBinding,
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: resolveKuaishouCloudBindUrlExpiresAt(now),
bindProbeAt: null as null,
bindProbeStatus: 'pending',
bindProbeMessage: '',
roleName:
normalizeTaskStatus(task.task_status) === TASK_STATUS.ROLE_CONFIRMED
? flow.binding.roleName
: '',
roleId:
normalizeTaskStatus(task.task_status) === TASK_STATUS.ROLE_CONFIRMED
? flow.binding.roleId
: '',
},
role:
normalizeTaskStatus(task.task_status) === TASK_STATUS.ROLE_CONFIRMED
? flow.role
: buildPendingRoleWithDefaultSnapshot(defaultRoleSnapshot!),
},
}
const claimLinkState = options.claimLinkState || (await ensureTaskClaimLink(task))
const updatedTask = await updateTask(task.id, {
task_status:
normalizeTaskStatus(task.task_status) === TASK_STATUS.ROLE_CONFIRMED
? task.task_status
: TASK_STATUS.WAITING_BINDING,
claim_token: claimLinkState.token || task.claim_token || '',
claim_expires_at: claimLinkState.expiredAt || getTaskClaimExpiresAt(task),
role_id:
normalizeTaskStatus(task.task_status) === TASK_STATUS.ROLE_CONFIRMED ? task.role_id : '',
role_name:
normalizeTaskStatus(task.task_status) === TASK_STATUS.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),
}
}
async function markKuaishouCloudBindUrlRefreshFailed(
task: TaskRow,
{ taskContext, flow, now, actor, error }: JsonObject = {},
) {
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: 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
}
@@ -0,0 +1,177 @@
import { createTaskEvent } from '../../../repositories/task-event-repo.js'
import { updateTask } from '../../../repositories/task-repo.js'
import { probeCloudtentaclesBindUrl } from '../../platforms/cloudtentacles/virtual-number-service.js'
import { resolveCloudtentaclesConfig } from '../../platforms/cloudtentacles/helpers.js'
import { notifyKuaishouCloudBindUrlRefreshFailed } from '../../notification/domain-notifications.js'
import { createHttpError } from '../../../utils/http.js'
import { nowIso } from '../../../utils/time.js'
import { TASK_STATUS } from '../../../domain/task-status.js'
import {
isKuaishouCloudBindUrlFresh,
isKuaishouCloudTask,
normalizeKuaishouCloudFlow,
normalizeKuaishouCloudRoleInfo,
type JsonObject,
} from './domain.js'
import { resolvePersistedCloudtentaclesContextBySourceKeys } from './cloudtentacles-context.js'
import { normalizeActor, parseTaskContext } from './task-context.js'
import { buildRoleStateFromCurrentInfo } from './role-state.js'
import { refreshKuaishouCloudTaskBindUrl } from './prepare-fulfillment.js'
import type { TaskRow } from '../../../types/repository/rows.js'
export async function probeKuaishouCloudTaskBindUrl(task: TaskRow, options: JsonObject = {}) {
if (!isKuaishouCloudTask(task)) {
throw createHttpError('当前任务不是 kuaishou-lewan 履约任务', {
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 as 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 as 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 roleState = hasRoleInfo
? buildRoleStateFromCurrentInfo({
flow,
roleInfo: probeRoleInfo,
now,
emptyMessage: '当前还没有查询到角色信息,请完成绑定后稍等片刻再试',
})
: null
const nextContext = {
...taskContext,
kuaishouCloudFulfillment: {
...flow,
binding: {
...flow.binding,
bindProbeAt: now,
bindProbeStatus: probe.valid ? 'valid' : 'invalid',
bindProbeMessage: String(probe.message || probe.reason || '').trim(),
roleName: roleState ? roleState.bindingRoleName : flow.binding.roleName,
roleId: roleState ? roleState.bindingRoleId : flow.binding.roleId,
},
role: roleState ? roleState.role : flow.role,
},
}
const updatedTask = await updateTask(task.id, {
role_id: roleState && !roleState.isDefaultRole ? probeRoleInfo.rid : task.role_id,
role_name: roleState && !roleState.isDefaultRole ? 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: TaskRow,
{ taskContext, flow, now, actor, error }: JsonObject = {},
) {
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: 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
}
@@ -0,0 +1,312 @@
import { createTaskEvent } from '../../../repositories/task-event-repo.js'
import { updateTask } from '../../../repositories/task-repo.js'
import { buildClaimUrl } from '../../claim/claim-service.js'
import { backCloudtentaclesVirtualNumber } from '../../platforms/cloudtentacles/virtual-number-service.js'
import { createHttpError } from '../../../utils/http.js'
import { nowIso } from '../../../utils/time.js'
import { TASK_STATUS, normalizeTaskStatus } from '../../../domain/task-status.js'
import {
isKuaishouCloudTask,
maskPhone,
normalizeKuaishouCloudFlow,
resolveKuaishouCloudBindUrlExpiresAt,
type JsonObject,
} from './domain.js'
import {
prepareKuaishouCloudBindResourceWithFallback,
resolveKuaishouCloudVnKeyCandidates,
} from './binding-resources.js'
import { resolvePersistedCloudtentaclesContextBySourceKeys } from './cloudtentacles-context.js'
import {
getTaskClaimExpiresAt,
normalizeActor,
parseTaskContext,
} from './task-context.js'
import { ensureTaskClaimLink } from './ensure-claim-link.js'
import {
buildPendingRoleWithDefaultSnapshot,
captureKuaishouCloudDefaultRoleSnapshot,
} from './role-state.js'
import type { TaskRow } from '../../../types/repository/rows.js'
export async function rebindKuaishouCloudTaskRole(task: TaskRow, options: JsonObject = {}) {
if (!isKuaishouCloudTask(task)) {
throw createHttpError('当前任务不是 kuaishou-lewan 履约任务', {
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 defaultRoleSnapshot = await captureKuaishouCloudDefaultRoleSnapshot({
cloudContext,
preparedBinding,
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: buildPendingRoleWithDefaultSnapshot(defaultRoleSnapshot),
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,
defaultRoleName: defaultRoleSnapshot.defaultName,
defaultRoleId: defaultRoleSnapshot.defaultRid,
defaultRoleCaptureStatus: defaultRoleSnapshot.defaultCaptureStatus,
actor,
},
now,
)
return {
task: updatedTask,
claimUrl: claimLinkState.claimUrl,
token: claimLinkState.token,
flow: normalizeKuaishouCloudFlow(nextContext.kuaishouCloudFulfillment),
}
}
@@ -0,0 +1,129 @@
import { createTaskEvent } from '../../../repositories/task-event-repo.js'
import { updateTask } from '../../../repositories/task-repo.js'
import { getCloudtentaclesBindInfo } from '../../platforms/cloudtentacles/virtual-number-service.js'
import { createHttpError } from '../../../utils/http.js'
import { nowIso } from '../../../utils/time.js'
import {
isKuaishouCloudTask,
normalizeKuaishouCloudFlow,
normalizeKuaishouCloudRoleInfo,
type JsonObject,
} from './domain.js'
import { resolvePersistedCloudtentaclesContextBySourceKeys } from './cloudtentacles-context.js'
import { normalizeActor, parseTaskContext } from './task-context.js'
import { buildRoleStateFromCurrentInfo } from './role-state.js'
import { probeKuaishouCloudTaskBindUrl } from './probe-bind-url.js'
import type { TaskRow } from '../../../types/repository/rows.js'
export async function refreshKuaishouCloudTaskRoleInfo(task: TaskRow, options: JsonObject = {}) {
if (!isKuaishouCloudTask(task)) {
throw createHttpError('当前任务不是 kuaishou-lewan 履约任务', {
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 = resolvePersistedCloudtentaclesContextBySourceKeys([
flow.binding.resolvedSourceKey,
...flow.binding.cloudSourceKeys,
])
const bindInfoResult = await getCloudtentaclesBindInfo({
...cloudContext,
key: flow.binding.vnKey,
id: flow.binding.vnId,
})
const bindInfo = normalizeKuaishouCloudRoleInfo(bindInfoResult.bindInfo)
const roleState = buildRoleStateFromCurrentInfo({
flow,
roleInfo: bindInfo,
now,
emptyMessage: '当前还没有查询到角色信息,请完成绑定后稍等片刻再试',
})
const nextContext = {
...taskContext,
kuaishouCloudFulfillment: {
...flow,
binding: {
...flow.binding,
roleName: roleState.bindingRoleName,
roleId: roleState.bindingRoleId,
},
role: roleState.role,
},
}
const updatedTask = await updateTask(task.id, {
// UID 主闸:有绑定角色即落库,默认角色仅诊断
role_id: roleState.hasRoleInfo ? bindInfo.rid : '',
role_name: roleState.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),
}
}
@@ -0,0 +1,119 @@
/**
* 角色状态与默认角色快照(诊断用)。
* 履约主闸已改为 claimIdentity.expectedUid;默认角色不再作为 ready 硬门槛。
*/
import { getCloudtentaclesBindInfo } from '../../platforms/cloudtentacles/virtual-number-service.js'
import {
hasKuaishouCloudDefaultRoleSnapshot,
isSameKuaishouCloudRoleIdentity,
normalizeKuaishouCloudFlow,
normalizeKuaishouCloudRoleInfo,
type JsonObject,
} from './domain.js'
export type KuaishouCloudDefaultRoleSnapshot = {
defaultName: string
defaultRid: string
defaultCapturedAt: string | null
defaultCaptureStatus: string
defaultErrorMessage: string
}
export async function captureKuaishouCloudDefaultRoleSnapshot({
cloudContext,
preparedBinding,
now,
}: {
cloudContext: JsonObject
preparedBinding: JsonObject
now: string
}): Promise<KuaishouCloudDefaultRoleSnapshot> {
try {
const bindInfoResult = await getCloudtentaclesBindInfo({
...cloudContext,
key: preparedBinding.vnKey,
id: preparedBinding.vnId,
})
const roleInfo = normalizeKuaishouCloudRoleInfo(bindInfoResult.bindInfo)
const hasRole = Boolean(roleInfo.name || roleInfo.rid)
return {
defaultName: roleInfo.name,
defaultRid: roleInfo.rid,
defaultCapturedAt: hasRole ? now : null,
defaultCaptureStatus: hasRole ? 'captured' : 'empty',
defaultErrorMessage: hasRole ? '' : '未获取到虚拟机默认角色信息,请稍后刷新',
}
} catch (error) {
return {
defaultName: '',
defaultRid: '',
defaultCapturedAt: null,
defaultCaptureStatus: 'failed',
defaultErrorMessage:
error instanceof Error ? error.message : '获取虚拟机默认角色信息失败,请稍后刷新',
}
}
}
export function buildPendingRoleWithDefaultSnapshot(snapshot: KuaishouCloudDefaultRoleSnapshot) {
return {
status: 'pending',
name: '',
rid: '',
refreshedAt: null as null,
errorMessage: snapshot.defaultErrorMessage || '',
rawInfo: null as null,
defaultName: snapshot.defaultName,
defaultRid: snapshot.defaultRid,
defaultCapturedAt: snapshot.defaultCapturedAt,
defaultCaptureStatus: snapshot.defaultCaptureStatus,
defaultErrorMessage: snapshot.defaultErrorMessage,
isDefaultRole: false,
}
}
export function buildRoleStateFromCurrentInfo({
flow,
roleInfo,
now,
emptyMessage,
}: {
flow: ReturnType<typeof normalizeKuaishouCloudFlow>
roleInfo: { name: string; rid: string; rawInfo?: unknown }
now: string
emptyMessage: string
}) {
const hasRoleInfo = Boolean(roleInfo.name || roleInfo.rid)
const hasDefaultRole = hasKuaishouCloudDefaultRoleSnapshot(flow)
const isDefaultRole =
hasRoleInfo &&
hasDefaultRole &&
isSameKuaishouCloudRoleIdentity(
{ name: roleInfo.name, rid: roleInfo.rid },
{ name: flow.role.defaultName, rid: flow.role.defaultRid },
)
// UID 主闸:只要有绑定角色信息就视为 ready 并落库 rid;
// isDefaultRole 仅作诊断文案,不再清空 roleId。
return {
hasRoleInfo,
isDefaultRole,
bindingRoleName: hasRoleInfo ? roleInfo.name : '',
bindingRoleId: hasRoleInfo ? roleInfo.rid : '',
role: {
...flow.role,
status: hasRoleInfo ? 'ready' : 'pending',
name: hasRoleInfo ? roleInfo.name : '',
rid: hasRoleInfo ? roleInfo.rid : '',
refreshedAt: now,
errorMessage: hasRoleInfo
? isDefaultRole
? '当前绑定接近虚拟机默认角色,请确认已绑定自己的角色,并与填写 UID 一致'
: ''
: emptyMessage,
rawInfo: roleInfo.rawInfo || null,
isDefaultRole,
},
}
}
@@ -44,6 +44,11 @@ export async function ensureShortLinkForTarget(
): Promise<ShortLinkPayload> { ): Promise<ShortLinkPayload> {
const normalizedTargetUrl = normalizeTargetUrl(targetUrl) const normalizedTargetUrl = normalizeTargetUrl(targetUrl)
const source = String(options.source || '').trim() const source = String(options.source || '').trim()
if (source === SHORT_LINK_SOURCE_KUAISHOU_FEIFEI_CLAIM) {
console.warn(
'[short-link] feifei claim 已不再使用 shortLink 作为对外交付;请使用本站 /#/claim/{token} 并在 H5 拼 uid',
)
}
const targetUrlHash = hashTargetUrl(normalizedTargetUrl) const targetUrlHash = hashTargetUrl(normalizedTargetUrl)
const now = nowIso() const now = nowIso()
const expiresAt = options.expiresAt !== undefined const expiresAt = options.expiresAt !== undefined
@@ -0,0 +1,32 @@
# 工程债:lewan 拆分、默认角色、短链
## lewan 模块拆分(已做)
```text
kuaishou-cloud/
index.ts # barrel 导出
prepare-fulfillment.ts # prepare + 刷新 bindUrl
rebind-role.ts
probe-bind-url.ts
refresh-role-info.ts
role-state.ts # 默认角色快照 + ready 状态(UID 主闸)
delivery-plan.ts
ensure-claim-link.ts
dispatch-role-sync.ts
task-finalization.ts # dispatch / return(仍厚,后续可再拆)
domain.ts
```
对外 import 路径仍为 `.../kuaishou-cloud/index.js`
## 默认角色
- 主闸:`claimIdentity.expectedUid` 与绑定 `rid` 一致。
- `isDefaultRole` 仅诊断文案;有角色信息即 `ready` 并落库 rid。
- `hasKuaishouCloudCustomerRole` 改为「是否已有绑定角色」,不再要求不同于默认角色。
## 短链
- `/s/:code` **只读解析**历史链接。
- feifei claim **不再**依赖 shortLink 交付;若仍调用 `ensureShortLinkForTarget` 且 source 为 feifei claim,会打 warn。
- 新业务:本站 claimUrl + H5 拼 `uid`