实现内置虎牙扫码登录与绑定 Connector

This commit is contained in:
yml2213
2026-11-09 17:11:01 +08:00
parent b0b8e983ba
commit a9937ef8c6
41 changed files with 4246 additions and 152 deletions
@@ -0,0 +1,20 @@
import { runtimeConfig } from '../../config/runtime.js'
import { createHttpError } from '../../utils/http.js'
import { isDevMockEnabled } from './dev-mock-enabled.js'
export const DEV_MOCK_CONNECTOR_PATH = '/api/v1/dev-mock/connector'
export const DEV_MOCK_CONNECTOR_API_KEY = 'local-semi-auto-connector-key'
export function devMockConnectorBaseUrl() {
const port = Math.trunc(Number(runtimeConfig.server.port || 3000))
return `http://127.0.0.1:${port}${DEV_MOCK_CONNECTOR_PATH}`
}
export function assertDevMockConnectorEnabled() {
if (!isDevMockEnabled()) {
throw createHttpError('内置 Mock Connector 仅在开发环境可用', {
statusCode: 503,
errorCode: 'dev_mock_connector_disabled',
})
}
}
@@ -0,0 +1,21 @@
import { isProductionLike } from '../../config/runtime-validation.js'
import { createHttpError } from '../../utils/http.js'
export function isDevMockEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
const flag = String(env.ENABLE_DEV_MOCK || '')
.trim()
.toLowerCase()
if (flag === '1' || flag === 'true') {
return true
}
return !isProductionLike(env)
}
export function assertDevMockEnabled() {
if (!isDevMockEnabled()) {
throw createHttpError('当前环境未开启开发 Mock(仅非 production 或 ENABLE_DEV_MOCK=1)', {
statusCode: 403,
errorCode: 'dev_mock_disabled',
})
}
}
@@ -26,13 +26,19 @@ import {
getKuaishouIndustrySourceConfig,
listKuaishouIndustryShopConfigs,
} from '../platforms/kuaishou-industry/source-config-service.js'
import { isProductionLike } from '../../config/runtime-validation.js'
import { runtimeConfig } from '../../config/runtime.js'
import { createHttpError } from '../../utils/http.js'
import { nowIso } from '../../utils/time.js'
import { randomId } from '../../utils/random.js'
import { TASK_STATUS } from '../../domain/task-status.js'
import { ensureSemiAutoTasksForOrder } from '../semi-auto/semi-auto-task-service.js'
import {
defaultSemiAutoConnectorMode,
isExternalSemiAutoConnectorConfigured,
type SemiAutoConnectorMode,
} from '../semi-auto/huya-connector-client.js'
import { assertDevMockEnabled, isDevMockEnabled } from './dev-mock-enabled.js'
export { assertDevMockEnabled, isDevMockEnabled }
export type DevMockCreateResult = {
platform: 'affiliate_dash'
@@ -69,35 +75,13 @@ export type DevMockSemiAutoResult = {
configId: number
taskId: number
skuKey: string
connectorMode: 'mock' | 'external'
connectorMode: SemiAutoConnectorMode
connectorConfigured: boolean
customerPath: string
adminPath: string
tips: string[]
}
export function isDevMockEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
if (String(env.ENABLE_DEV_MOCK || '').trim() === '1') {
return true
}
if (
String(env.ENABLE_DEV_MOCK || '')
.trim()
.toLowerCase() === 'true'
) {
return true
}
return !isProductionLike(env)
}
export function assertDevMockEnabled() {
if (!isDevMockEnabled()) {
throw createHttpError('当前环境未开启开发 Mock(仅非 production 或 ENABLE_DEV_MOCK=1)', {
statusCode: 403,
errorCode: 'dev_mock_disabled',
})
}
}
export function getDevMockStatus() {
const enabled = isDevMockEnabled()
return {
@@ -115,28 +99,21 @@ export function getDevMockStatus() {
key: 'semi_auto',
name: '半自动履约',
description:
'创建人工渠道半自动测试单;登录、绑定和浏览器票据由内置 Mock Connector 模拟',
'创建人工渠道半自动测试单;可选择仅 Mock 订单,登录、绑定和票据注入走真实 Connector',
steps: ['create_order', 'login', 'binding', 'browser_ticket', 'complete'],
},
]
: [],
defaultUid: '10001',
semiAuto: {
connectorMode: semiAutoConnectorMode(),
connectorConfigured: Boolean(
String(runtimeConfig.semiAuto?.connectorBaseUrl || '').trim() &&
String(runtimeConfig.semiAuto?.connectorApiKey || '').trim(),
),
defaultConnectorMode: defaultSemiAutoConnectorMode(),
externalConnectorConfigured: isExternalSemiAutoConnectorConfigured(),
mockConnectorAvailable: enabled,
platform: 'huya',
},
}
}
function semiAutoConnectorMode(): 'mock' | 'external' {
const baseUrl = String(runtimeConfig.semiAuto?.connectorBaseUrl || '').trim()
return baseUrl.includes('/dev-mock/connector') ? 'mock' : 'external'
}
type AffiliateDashMockStep = 'uid' | 'bind' | 'submitted' | 'completed' | 'failed'
/**
@@ -329,7 +306,7 @@ export async function createAffiliateDashMockClaim(
/**
* 生成半自动履约测试单:人工 SKU 配置、订单、履约和半自动任务都走真实领域服务。
* 平台登录、绑定和浏览器票据由内置 Mock Connector 模拟。
* 平台登录、绑定和浏览器票据按 connectorMode 选择真实 Connector 或内置 Mock。
*/
export async function createSemiAutoMock(
input: {
@@ -339,6 +316,7 @@ export async function createSemiAutoMock(
pageTitle?: unknown
pageDescription?: unknown
browserTargetUrl?: unknown
connectorMode?: unknown
} = {},
): Promise<DevMockSemiAutoResult> {
assertDevMockEnabled()
@@ -346,6 +324,7 @@ export async function createSemiAutoMock(
const orderNo = String(input.orderNo || `MOCKSA${Date.now()}`).trim()
const skuKey = normalizeMockSemiAutoSku(input.skuName)
const quantity = normalizeMockSemiAutoQuantity(input.quantity)
const connectorMode = normalizeMockSemiAutoConnectorMode(input.connectorMode)
const pageTitle = String(input.pageTitle || '虎牙账号授权测试')
.trim()
.slice(0, 60)
@@ -370,8 +349,14 @@ export async function createSemiAutoMock(
semiAutoConfig: {
pageTitle,
pageDescription,
loginInstructions: 'Mock 登录:点击获取二维码后,页面轮询两次会自动模拟登录成功。',
bindingInstructions: 'Mock 绑定:页面轮询两次后会自动识别角色,点击确认即可。',
loginInstructions:
connectorMode === 'mock'
? 'Mock 登录:点击获取二维码后,页面轮询两次会自动模拟登录成功。'
: '真实登录:点击获取二维码后,请使用虎牙 App 扫码并在手机上确认登录。',
bindingInstructions:
connectorMode === 'mock'
? 'Mock 绑定:页面轮询两次后会自动识别角色,点击确认即可。'
: '真实绑定:扫码后等待 Connector 识别角色,核对账号和区服后点击确认绑定。',
browserTargets: [{ key: 'huya', label: '虎牙', url: browserTargetUrl }],
},
})
@@ -469,7 +454,13 @@ export async function createSemiAutoMock(
})
}
const connectorMode = semiAutoConnectorMode()
await query('UPDATE semi_auto_tasks SET connector_mode = $2, updated_at = NOW() WHERE id = $1', [
taskId,
connectorMode,
])
const connectorConfigured =
connectorMode === 'external' ? isExternalSemiAutoConnectorConfigured() : true
return {
platform: 'semi_auto',
orderId: Number(order.id),
@@ -480,20 +471,35 @@ export async function createSemiAutoMock(
taskId,
skuKey,
connectorMode,
connectorConfigured,
customerPath: `/c/${encodeURIComponent(orderNo)}`,
adminPath: `/admin/semi-auto-tasks?keyword=${encodeURIComponent(orderNo)}`,
tips: [
'打开客户页,点击“获取登录二维码”;Mock Connector 会在两次轮询后自动模拟登录成功。',
'登录后页面会自动创建绑定二维码,轮询两次后识别角色,点击“确认绑定角色”。',
'后台“任务 → 半自动履约”领取任务后生成浏览器票据;扩展消费票据后写入 Mock Cookie。',
'完成履约仍走真实确认流程,会写入成本快照和财务确认。',
connectorMode === 'mock'
? '当前使用内置 Mock Connector,不需要启动 live-hub-py。'
: '当前配置的是外部 Connector;请确认 Connector 已启动,否则登录和绑定会失败。',
? '当前使用内置 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 normalizeMockSemiAutoConnectorMode(value: unknown): SemiAutoConnectorMode {
const mode = String(value || '')
.trim()
.toLowerCase()
if (mode === 'external' || mode === 'mock') return mode
return 'embedded'
}
function normalizeMockSemiAutoSku(value: unknown) {
const raw = String(value || '').trim()
const withCjk = raw