feat(semi-auto): 支持微博半自动履约与手机号国家码登录

新增微博内置 Connector:扫码/短信登录、Cookie 获取与一次性浏览器票据;客户页支持国家码选择、短信/扫码登录及登录即授权;dev-mock 与后台任务页适配微博 Mock 单。
This commit is contained in:
2026-11-10 19:05:05 +08:00
parent 08c3834a1c
commit 90173e3bd7
11 changed files with 1980 additions and 138 deletions
@@ -68,6 +68,7 @@ export type DevMockKuaishouIndustryVoucherResult = {
export type DevMockSemiAutoResult = {
platform: 'semi_auto'
semiAutoPlatform: 'huya' | 'weibo'
orderId: number
orderNo: string
orderItemId: number
@@ -310,6 +311,7 @@ export async function createAffiliateDashMockClaim(
*/
export async function createSemiAutoMock(
input: {
platform?: unknown
orderNo?: unknown
skuName?: unknown
quantity?: unknown
@@ -321,19 +323,32 @@ export async function createSemiAutoMock(
): Promise<DevMockSemiAutoResult> {
assertDevMockEnabled()
const orderNo = String(input.orderNo || `MOCKSA${Date.now()}`).trim()
const skuKey = normalizeMockSemiAutoSku(input.skuName)
const platform = normalizeMockSemiAutoPlatform(input.platform)
const orderNo = String(
input.orderNo || `${platform === 'weibo' ? 'MOCKWB' : 'MOCKSA'}${Date.now()}`,
).trim()
const skuKey = normalizeMockSemiAutoSku(input.skuName, platform)
const quantity = normalizeMockSemiAutoQuantity(input.quantity)
const connectorMode = normalizeMockSemiAutoConnectorMode(input.connectorMode)
const pageTitle = String(input.pageTitle || '虎牙账号授权测试')
// 微博目前只支持内置 Connector,忽略其他模式,避免生成必然失败的测试单。
const connectorMode =
platform === 'weibo' ? 'embedded' : normalizeMockSemiAutoConnectorMode(input.connectorMode)
const pageTitle = String(
input.pageTitle || (platform === 'weibo' ? '微博发帖授权测试' : '虎牙账号授权测试'),
)
.trim()
.slice(0, 60)
const pageDescription = String(
input.pageDescription || '开发 Mock:完成登录和角色绑定后,客服在后台完成履约。',
input.pageDescription ||
(platform === 'weibo'
? '开发 Mock:完成微博登录即视为授权,客服在后台完成发帖履约。'
: '开发 Mock:完成登录和角色绑定后,客服在后台完成履约。'),
)
.trim()
.slice(0, 500)
const browserTargetUrl = String(input.browserTargetUrl || 'https://www.huya.com/')
const browserTargetUrl = String(
input.browserTargetUrl ||
(platform === 'weibo' ? 'https://weibo.com/' : 'https://www.huya.com/'),
)
.trim()
.slice(0, 1000)
@@ -345,19 +360,30 @@ export async function createSemiAutoMock(
note: '开发 Mock 半自动履约',
channels: [{ platformKey: 'fund_cash', creditQuantity: 100, enabled: true }],
semiAutoEnabled: true,
semiAutoPlatform: 'huya',
semiAutoPlatform: platform,
semiAutoConfig: {
pageTitle,
pageDescription,
loginInstructions:
connectorMode === 'mock'
? 'Mock 登录:点击获取二维码后,页面轮询两次会自动模拟登录成功。'
: '真实登录:点击获取二维码后,请使用虎牙 App 扫码并在手机上确认登录。',
platform === 'weibo'
? '真实登录:使用微博绑定手机号接收验证码登录,或使用微博 App 扫码登录。登录仅用于本次订单履约,不会在页面展示账号 Cookie。'
: connectorMode === 'mock'
? 'Mock 登录:点击获取二维码后,页面轮询两次会自动模拟登录成功。'
: '真实登录:点击获取二维码后,请使用虎牙 App 扫码并在手机上确认登录。',
bindingInstructions:
connectorMode === 'mock'
? 'Mock 绑定:页面轮询两次后会自动识别角色,点击确认即可。'
: '真实绑定:扫码后等待 Connector 识别角色,核对账号和区服后点击确认绑定。',
browserTargets: [{ key: 'huya', label: '虎牙', url: browserTargetUrl }],
platform === 'weibo'
? '微博登录成功即完成授权,无需再绑定游戏角色。'
: connectorMode === 'mock'
? 'Mock 绑定:页面轮询两次后会自动识别角色,点击确认即可。'
: '真实绑定:扫码后等待 Connector 识别角色,核对账号和区服后点击确认绑定。',
...(platform === 'weibo' ? { loginMethods: ['sms', 'qr'] } : {}),
browserTargets: [
{
key: platform,
label: platform === 'weibo' ? '微博' : '虎牙',
url: browserTargetUrl,
},
],
},
})
const configId = Number(config.id || 0)
@@ -386,12 +412,12 @@ export async function createSemiAutoMock(
specJson: JSON.stringify({
source: 'dev_mock',
skuNick: skuKey,
semiAuto: { platform: 'huya', mock: true },
semiAuto: { platform, mock: true },
}),
itemSnapshotJson: JSON.stringify({
source: 'dev_mock',
skuKey,
platform: 'huya',
platform,
mock: true,
}),
createdAt: now,
@@ -463,6 +489,7 @@ export async function createSemiAutoMock(
connectorMode === 'external' ? isExternalSemiAutoConnectorConfigured() : true
return {
platform: 'semi_auto',
semiAutoPlatform: platform,
orderId: Number(order.id),
orderNo,
orderItemId,
@@ -474,24 +501,40 @@ export async function createSemiAutoMock(
connectorConfigured,
customerPath: `/c/${encodeURIComponent(orderNo)}`,
adminPath: `/admin/semi-auto-tasks?keyword=${encodeURIComponent(orderNo)}`,
tips: [
'完成履约仍走真实确认流程,会写入成本快照和财务确认。',
connectorMode === 'mock'
? '当前使用内置 Mock Connector:打开客户页点击登录,轮询两次后自动登录;绑定同样自动识别角色。'
: connectorMode === 'external'
? connectorConfigured
? '当前使用远程真实 Connector:订单是 Mock 的,登录、绑定和浏览器票据都走外部服务。'
: '当前选择远程真实 Connector,但 SEMI_AUTO_CONNECTOR_BASE_URL / API_KEY 未配置,登录会失败。'
: '当前使用 order_site 内置真实 Connector:无需启动 live-hub-py,扫码登录、绑定和票据都在本进程完成。',
connectorMode === 'mock'
? '后台领取任务后生成浏览器票据;扩展消费票据后写入 Mock Cookie。'
: connectorMode === 'external'
? '请确保远程 Connector 已启动,并保证两边 SEMI_AUTO_CONNECTOR_API_KEY 一致。'
: '登录凭据只保存在当前进程内存中,重启后端会要求客户重新扫码。',
],
tips:
platform === 'weibo'
? [
'完成履约仍走真实确认流程,会写入成本快照和财务确认。',
'微博使用内置 Connector:短信验证码会真实发送到客户手机号;扫码登录需在微博 App 中确认。',
'后台领取任务后生成 weibo-ticket-* 浏览器票据,扩展消费票据后写入微博 Cookie。',
'微博登录成功即视为授权完成,任务会直接进入等待客服处理。',
]
: [
'完成履约仍走真实确认流程,会写入成本快照和财务确认。',
connectorMode === 'mock'
? '当前使用内置 Mock Connector:打开客户页点击登录,轮询两次后自动登录;绑定同样自动识别角色。'
: connectorMode === 'external'
? connectorConfigured
? '当前使用远程真实 Connector:订单是 Mock 的,登录、绑定和浏览器票据都走外部服务。'
: '当前选择远程真实 Connector,但 SEMI_AUTO_CONNECTOR_BASE_URL / API_KEY 未配置,登录会失败。'
: '当前使用 order_site 内置真实 Connector:无需启动 live-hub-py,扫码登录、绑定和票据都在本进程完成。',
connectorMode === 'mock'
? '后台领取任务后生成浏览器票据;扩展消费票据后写入 Mock Cookie。'
: connectorMode === 'external'
? '请确保远程 Connector 已启动,并保证两边 SEMI_AUTO_CONNECTOR_API_KEY 一致。'
: '登录凭据只保存在当前进程内存中,重启后端会要求客户重新扫码。',
],
}
}
function normalizeMockSemiAutoPlatform(value: unknown): 'huya' | 'weibo' {
return String(value || '')
.trim()
.toLowerCase() === 'weibo'
? 'weibo'
: 'huya'
}
function normalizeMockSemiAutoConnectorMode(value: unknown): SemiAutoConnectorMode {
const mode = String(value || '')
.trim()
@@ -500,13 +543,14 @@ function normalizeMockSemiAutoConnectorMode(value: unknown): SemiAutoConnectorMo
return 'embedded'
}
function normalizeMockSemiAutoSku(value: unknown) {
function normalizeMockSemiAutoSku(value: unknown, platform: 'huya' | 'weibo' = 'huya') {
const prefix = platform === 'weibo' ? '微博半自动' : '虎牙半自动'
const raw = String(value || '').trim()
const withCjk = raw
? containsCjk(raw)
? raw
: `虎牙半自动-${raw}`
: `虎牙半自动测试-${Date.now()}`
: `${prefix}-${raw}`
: `${prefix}测试-${Date.now()}`
return withCjk.slice(0, 48)
}