后端迁移云触手虚拟号服务

This commit is contained in:
yml
2026-05-21 16:26:53 +08:00
parent 89c2bd5273
commit fb95f7dc55
2 changed files with 57 additions and 31 deletions
@@ -1,5 +1,3 @@
// @ts-check
import http from 'node:http' import http from 'node:http'
import https from 'node:https' import https from 'node:https'
@@ -10,7 +8,23 @@ import { resolveCloudtentaclesConfig } from './shared.js'
const BIND_URL_PROBE_MAX_BODY_BYTES = 64 * 1024 const BIND_URL_PROBE_MAX_BODY_BYTES = 64 * 1024
const AMS_SIGNATURE_EXPIRED_CODE = '99998' const AMS_SIGNATURE_EXPIRED_CODE = '99998'
export async function listCloudtentaclesVirtualNumbers(payload = {}) { type JsonObject = Record<string, any>
type HeaderAdapter = {
get(name: unknown): string | null
}
type AmsProbeResponse = {
status: number
headers: HeaderAdapter
bodyText: string
}
type AmsSignatureParams = {
userId: string
timestamp: string
nonce: string
sign: string
}
export async function listCloudtentaclesVirtualNumbers(payload: JsonObject = {}) {
const token = requireToken(payload.token, 'cloudtentacles 虚拟号列表缺少 token', 'cloudtentacles_vn_list_missing_token') const token = requireToken(payload.token, 'cloudtentacles 虚拟号列表缺少 token', 'cloudtentacles_vn_list_missing_token')
const key = requireKey(payload.key, 'cloudtentacles 虚拟号列表缺少 key', 'cloudtentacles_vn_list_missing_key') const key = requireKey(payload.key, 'cloudtentacles 虚拟号列表缺少 key', 'cloudtentacles_vn_list_missing_key')
const config = resolveCloudtentaclesConfig(payload) const config = resolveCloudtentaclesConfig(payload)
@@ -35,7 +49,7 @@ export async function listCloudtentaclesVirtualNumbers(payload = {}) {
} }
} }
export async function appointCloudtentaclesVirtualNumber(payload = {}) { export async function appointCloudtentaclesVirtualNumber(payload: JsonObject = {}) {
const token = requireToken(payload.token, 'cloudtentacles 申请虚拟号缺少 token', 'cloudtentacles_vn_appoint_missing_token') const token = requireToken(payload.token, 'cloudtentacles 申请虚拟号缺少 token', 'cloudtentacles_vn_appoint_missing_token')
const key = requireKey(payload.key, 'cloudtentacles 申请虚拟号缺少 key', 'cloudtentacles_vn_appoint_missing_key') const key = requireKey(payload.key, 'cloudtentacles 申请虚拟号缺少 key', 'cloudtentacles_vn_appoint_missing_key')
const config = resolveCloudtentaclesConfig(payload) const config = resolveCloudtentaclesConfig(payload)
@@ -57,7 +71,7 @@ export async function appointCloudtentaclesVirtualNumber(payload = {}) {
} }
} }
export async function generateCloudtentaclesLoginCode(payload = {}) { export async function generateCloudtentaclesLoginCode(payload: JsonObject = {}) {
const token = requireToken(payload.token, 'cloudtentacles 生成登录码缺少 token', 'cloudtentacles_vn_generate_code_missing_token') const token = requireToken(payload.token, 'cloudtentacles 生成登录码缺少 token', 'cloudtentacles_vn_generate_code_missing_token')
const key = requireKey(payload.key, 'cloudtentacles 生成登录码缺少 key', 'cloudtentacles_vn_generate_code_missing_key') const key = requireKey(payload.key, 'cloudtentacles 生成登录码缺少 key', 'cloudtentacles_vn_generate_code_missing_key')
const id = requireId(payload.id, 'cloudtentacles 生成登录码缺少 id', 'cloudtentacles_vn_generate_code_missing_id') const id = requireId(payload.id, 'cloudtentacles 生成登录码缺少 id', 'cloudtentacles_vn_generate_code_missing_id')
@@ -80,7 +94,7 @@ export async function generateCloudtentaclesLoginCode(payload = {}) {
} }
} }
export async function fetchCloudtentaclesVirtualNumberCode(payload = {}) { export async function fetchCloudtentaclesVirtualNumberCode(payload: JsonObject = {}) {
const token = requireToken(payload.token, 'cloudtentacles 获取验证码缺少 token', 'cloudtentacles_vn_verif_code_missing_token') const token = requireToken(payload.token, 'cloudtentacles 获取验证码缺少 token', 'cloudtentacles_vn_verif_code_missing_token')
const key = requireKey(payload.key, 'cloudtentacles 获取验证码缺少 key', 'cloudtentacles_vn_verif_code_missing_key') const key = requireKey(payload.key, 'cloudtentacles 获取验证码缺少 key', 'cloudtentacles_vn_verif_code_missing_key')
const phone = String(payload.phone || '').trim() const phone = String(payload.phone || '').trim()
@@ -110,7 +124,7 @@ export async function fetchCloudtentaclesVirtualNumberCode(payload = {}) {
} }
} }
export async function verifyCloudtentaclesLoginCode(payload = {}) { export async function verifyCloudtentaclesLoginCode(payload: JsonObject = {}) {
const token = requireToken(payload.token, 'cloudtentacles 校验登录码缺少 token', 'cloudtentacles_vn_verify_code_missing_token') const token = requireToken(payload.token, 'cloudtentacles 校验登录码缺少 token', 'cloudtentacles_vn_verify_code_missing_token')
const key = requireKey(payload.key, 'cloudtentacles 校验登录码缺少 key', 'cloudtentacles_vn_verify_code_missing_key') const key = requireKey(payload.key, 'cloudtentacles 校验登录码缺少 key', 'cloudtentacles_vn_verify_code_missing_key')
const id = requireId(payload.id, 'cloudtentacles 校验登录码缺少 id', 'cloudtentacles_vn_verify_code_missing_id') const id = requireId(payload.id, 'cloudtentacles 校验登录码缺少 id', 'cloudtentacles_vn_verify_code_missing_id')
@@ -141,7 +155,7 @@ export async function verifyCloudtentaclesLoginCode(payload = {}) {
} }
} }
export async function getCloudtentaclesBindUrl(payload = {}) { export async function getCloudtentaclesBindUrl(payload: JsonObject = {}) {
const token = requireToken(payload.token, 'cloudtentacles 获取兑换链接缺少 token', 'cloudtentacles_vn_bind_url_missing_token') const token = requireToken(payload.token, 'cloudtentacles 获取兑换链接缺少 token', 'cloudtentacles_vn_bind_url_missing_token')
const key = requireKey(payload.key, 'cloudtentacles 获取兑换链接缺少 key', 'cloudtentacles_vn_bind_url_missing_key') const key = requireKey(payload.key, 'cloudtentacles 获取兑换链接缺少 key', 'cloudtentacles_vn_bind_url_missing_key')
const id = requireId(payload.id, 'cloudtentacles 获取兑换链接缺少 id', 'cloudtentacles_vn_bind_url_missing_id') const id = requireId(payload.id, 'cloudtentacles 获取兑换链接缺少 id', 'cloudtentacles_vn_bind_url_missing_id')
@@ -165,7 +179,7 @@ export async function getCloudtentaclesBindUrl(payload = {}) {
} }
} }
export async function probeCloudtentaclesBindUrl(payload = {}) { export async function probeCloudtentaclesBindUrl(payload: JsonObject = {}) {
const bindUrl = String(payload.bindUrl || '').trim() const bindUrl = String(payload.bindUrl || '').trim()
if (!bindUrl) { if (!bindUrl) {
return { return {
@@ -239,7 +253,7 @@ export async function probeCloudtentaclesBindUrl(payload = {}) {
} }
} }
export async function getCloudtentaclesBindInfo(payload = {}) { export async function getCloudtentaclesBindInfo(payload: JsonObject = {}) {
const token = requireToken(payload.token, 'cloudtentacles 获取绑定信息缺少 token', 'cloudtentacles_vn_bind_info_missing_token') const token = requireToken(payload.token, 'cloudtentacles 获取绑定信息缺少 token', 'cloudtentacles_vn_bind_info_missing_token')
const key = requireKey(payload.key, 'cloudtentacles 获取绑定信息缺少 key', 'cloudtentacles_vn_bind_info_missing_key') const key = requireKey(payload.key, 'cloudtentacles 获取绑定信息缺少 key', 'cloudtentacles_vn_bind_info_missing_key')
const id = requireId(payload.id, 'cloudtentacles 获取绑定信息缺少 id', 'cloudtentacles_vn_bind_info_missing_id') const id = requireId(payload.id, 'cloudtentacles 获取绑定信息缺少 id', 'cloudtentacles_vn_bind_info_missing_id')
@@ -268,7 +282,7 @@ export async function getCloudtentaclesBindInfo(payload = {}) {
} }
} }
export async function backCloudtentaclesVirtualNumber(payload = {}) { export async function backCloudtentaclesVirtualNumber(payload: JsonObject = {}) {
const token = requireToken(payload.token, 'cloudtentacles 退还号码缺少 token', 'cloudtentacles_vn_back_missing_token') const token = requireToken(payload.token, 'cloudtentacles 退还号码缺少 token', 'cloudtentacles_vn_back_missing_token')
const key = requireKey(payload.key, 'cloudtentacles 退还号码缺少 key', 'cloudtentacles_vn_back_missing_key') const key = requireKey(payload.key, 'cloudtentacles 退还号码缺少 key', 'cloudtentacles_vn_back_missing_key')
const id = requireId(payload.id, 'cloudtentacles 退还号码缺少 id', 'cloudtentacles_vn_back_missing_id') const id = requireId(payload.id, 'cloudtentacles 退还号码缺少 id', 'cloudtentacles_vn_back_missing_id')
@@ -292,7 +306,7 @@ export async function backCloudtentaclesVirtualNumber(payload = {}) {
} }
} }
function mapVirtualNumberItem(item) { function mapVirtualNumberItem(item: unknown) {
const source = isPlainObject(item) ? item : {} const source = isPlainObject(item) ? item : {}
const bindInfo = parseBindInfo(source.bind_info) const bindInfo = parseBindInfo(source.bind_info)
@@ -306,7 +320,7 @@ function mapVirtualNumberItem(item) {
} }
} }
function requireToken(value, message, errorCode) { function requireToken(value: unknown, message: string, errorCode: string) {
const token = String(value || '').trim() const token = String(value || '').trim()
if (!token) { if (!token) {
throw createHttpError(message, { throw createHttpError(message, {
@@ -318,7 +332,7 @@ function requireToken(value, message, errorCode) {
return token return token
} }
function requireKey(value, message, errorCode) { function requireKey(value: unknown, message: string, errorCode: string) {
const key = String(value || '').trim() const key = String(value || '').trim()
if (!key) { if (!key) {
throw createHttpError(message, { throw createHttpError(message, {
@@ -330,7 +344,7 @@ function requireKey(value, message, errorCode) {
return key return key
} }
function requireId(value, message, errorCode) { function requireId(value: unknown, message: string, errorCode: string) {
const id = Number(value) const id = Number(value)
if (!Number.isInteger(id) || id <= 0) { if (!Number.isInteger(id) || id <= 0) {
throw createHttpError(message, { throw createHttpError(message, {
@@ -342,7 +356,7 @@ function requireId(value, message, errorCode) {
return id return id
} }
function tryParseJson(text) { function tryParseJson(text: string) {
try { try {
return JSON.parse(text) return JSON.parse(text)
} catch { } catch {
@@ -350,12 +364,12 @@ function tryParseJson(text) {
} }
} }
function parseBindInfo(value) { function parseBindInfo(value: unknown) {
const bindInfoText = String(value || '').trim() const bindInfoText = String(value || '').trim()
return tryParseJson(bindInfoText) || bindInfoText || null return tryParseJson(bindInfoText) || bindInfoText || null
} }
async function requestAmsIdeProbe(endpoint, options = {}) { async function requestAmsIdeProbe(endpoint: string, options: JsonObject = {}): Promise<AmsProbeResponse> {
const url = new URL(endpoint) const url = new URL(endpoint)
if (!['http:', 'https:'].includes(url.protocol)) { if (!['http:', 'https:'].includes(url.protocol)) {
throw new Error('unsupported_ams_probe_protocol') throw new Error('unsupported_ams_probe_protocol')
@@ -376,7 +390,7 @@ async function requestAmsIdeProbe(endpoint, options = {}) {
'accept-encoding': 'identity', 'accept-encoding': 'identity',
}, },
}, (response) => { }, (response) => {
const chunks = [] const chunks: Buffer[] = []
let receivedBytes = 0 let receivedBytes = 0
response.on('data', (chunk) => { response.on('data', (chunk) => {
@@ -414,7 +428,7 @@ async function requestAmsIdeProbe(endpoint, options = {}) {
}) })
} }
function createHeaderAdapter(headers) { function createHeaderAdapter(headers: http.IncomingHttpHeaders): HeaderAdapter {
const normalized = new Map() const normalized = new Map()
for (const [key, value] of Object.entries(headers || {})) { for (const [key, value] of Object.entries(headers || {})) {
@@ -429,13 +443,13 @@ function createHeaderAdapter(headers) {
} }
return { return {
get(name) { get(name: unknown) {
return normalized.get(String(name || '').toLowerCase()) || null return normalized.get(String(name || '').toLowerCase()) || null
}, },
} }
} }
function buildAmsIdeProbeBody(signatureParams, config, payload = {}) { function buildAmsIdeProbeBody(signatureParams: AmsSignatureParams, config: JsonObject, payload: JsonObject = {}) {
const body = new URLSearchParams() const body = new URLSearchParams()
const chartId = String(payload.chartId || config.bindUrlProbeChartId || '323794').trim() const chartId = String(payload.chartId || config.bindUrlProbeChartId || '323794').trim()
const subChartId = String(payload.subChartId || config.bindUrlProbeSubChartId || chartId).trim() const subChartId = String(payload.subChartId || config.bindUrlProbeSubChartId || chartId).trim()
@@ -456,7 +470,11 @@ function buildAmsIdeProbeBody(signatureParams, config, payload = {}) {
return body return body
} }
function buildAmsIdeProbeHeaders(signatureParams, config, options = {}) { function buildAmsIdeProbeHeaders(
signatureParams: AmsSignatureParams,
config: JsonObject,
options: JsonObject = {},
) {
const userAgent = String(options.userAgent || '').trim() const userAgent = String(options.userAgent || '').trim()
const referer = String(config.bindUrlProbeReferer || 'https://gp.qq.com/').trim() const referer = String(config.bindUrlProbeReferer || 'https://gp.qq.com/').trim()
@@ -479,7 +497,7 @@ function buildAmsIdeProbeHeaders(signatureParams, config, options = {}) {
} }
} }
function buildAmsIdeProbeCookie(signatureParams, config) { function buildAmsIdeProbeCookie(signatureParams: AmsSignatureParams, config: JsonObject) {
const tokenParams = `?userId=${signatureParams.userId}&timestamp=${signatureParams.timestamp}&nonce=${signatureParams.nonce}&sign=${signatureParams.sign}` const tokenParams = `?userId=${signatureParams.userId}&timestamp=${signatureParams.timestamp}&nonce=${signatureParams.nonce}&sign=${signatureParams.sign}`
const cookies = [ const cookies = [
`tokenParams=${encodeURIComponent(tokenParams)}`, `tokenParams=${encodeURIComponent(tokenParams)}`,
@@ -490,7 +508,7 @@ function buildAmsIdeProbeCookie(signatureParams, config) {
return cookies.join('; ') return cookies.join('; ')
} }
function extractBindUrlSignatureParams(bindUrl) { function extractBindUrlSignatureParams(bindUrl: string): AmsSignatureParams | null {
try { try {
const url = new URL(bindUrl) const url = new URL(bindUrl)
const params = new URLSearchParams(url.search) const params = new URLSearchParams(url.search)
@@ -533,13 +551,13 @@ function extractBindUrlSignatureParams(bindUrl) {
} }
} }
function isAmsBusinessOk(raw) { function isAmsBusinessOk(raw: any) {
const ret = String(raw?.ret ?? '').trim() const ret = String(raw?.ret ?? '').trim()
const iRet = String(raw?.iRet ?? raw?.jData?.iRet ?? '').trim() const iRet = String(raw?.iRet ?? raw?.jData?.iRet ?? '').trim()
return ret === '0' || iRet === '0' return ret === '0' || iRet === '0'
} }
function isAmsSignatureExpired(raw) { function isAmsSignatureExpired(raw: any) {
const codes = [ const codes = [
raw?.ret, raw?.ret,
raw?.iRet, raw?.iRet,
@@ -559,7 +577,7 @@ function isAmsSignatureExpired(raw) {
return codes.includes(AMS_SIGNATURE_EXPIRED_CODE) || messages.some((message) => message.includes('签名已过期')) return codes.includes(AMS_SIGNATURE_EXPIRED_CODE) || messages.some((message) => message.includes('签名已过期'))
} }
function extractAmsBindInfo(raw) { function extractAmsBindInfo(raw: any) {
const candidates = [ const candidates = [
raw?.jData?.sBindInfo, raw?.jData?.sBindInfo,
raw?.jData?.jData?.sBindInfo, raw?.jData?.jData?.sBindInfo,
@@ -569,7 +587,7 @@ function extractAmsBindInfo(raw) {
return candidates.find((item) => isPlainObject(item)) || null return candidates.find((item) => isPlainObject(item)) || null
} }
function normalizeAmsBindRoleInfo(bindInfo) { function normalizeAmsBindRoleInfo(bindInfo: unknown) {
const source = isPlainObject(bindInfo) ? bindInfo : {} const source = isPlainObject(bindInfo) ? bindInfo : {}
return { return {
@@ -579,7 +597,7 @@ function normalizeAmsBindRoleInfo(bindInfo) {
} }
} }
function normalizeHeaderMap(headers) { function normalizeHeaderMap(headers: unknown) {
if (!headers || typeof headers !== 'object') { if (!headers || typeof headers !== 'object') {
return {} return {}
} }
@@ -591,6 +609,6 @@ function normalizeHeaderMap(headers) {
) )
} }
function isPlainObject(value) { function isPlainObject(value: unknown): value is JsonObject {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value) return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
} }
@@ -666,6 +666,14 @@
- `npm run typecheck` - `npm run typecheck`
- `npm run build` - `npm run build`
- `npm test` 共 139 个用例通过 - `npm test` 共 139 个用例通过
162. Cloudtentacles 虚拟号服务迁移到 `.ts`
- `src/services/platforms/cloudtentacles/virtual-number-service.ts`
163. Cloudtentacles 虚拟号列表、申请、生成登录码、获取验证码、校验验证码、兑换链接、绑定信息、退还号码、AMS 兑换链接探测已进入 TS 编译链路;虚拟号 payload、AMS 签名参数、探测 response、header adapter 与动态响应对象补齐类型
164. Docker 内验证通过:
- `src/services/admin/platform-config/*.test.js` 共 37 个用例通过
- `npm run typecheck`
- `npm run build`
- `npm test` 共 139 个用例通过
## 下一步建议 ## 下一步建议