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

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 https from 'node:https'
@@ -10,7 +8,23 @@ import { resolveCloudtentaclesConfig } from './shared.js'
const BIND_URL_PROBE_MAX_BODY_BYTES = 64 * 1024
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 key = requireKey(payload.key, 'cloudtentacles 虚拟号列表缺少 key', 'cloudtentacles_vn_list_missing_key')
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 key = requireKey(payload.key, 'cloudtentacles 申请虚拟号缺少 key', 'cloudtentacles_vn_appoint_missing_key')
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 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')
@@ -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 key = requireKey(payload.key, 'cloudtentacles 获取验证码缺少 key', 'cloudtentacles_vn_verif_code_missing_key')
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 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')
@@ -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 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')
@@ -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()
if (!bindUrl) {
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 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')
@@ -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 key = requireKey(payload.key, 'cloudtentacles 退还号码缺少 key', 'cloudtentacles_vn_back_missing_key')
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 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()
if (!token) {
throw createHttpError(message, {
@@ -318,7 +332,7 @@ function requireToken(value, message, errorCode) {
return token
}
function requireKey(value, message, errorCode) {
function requireKey(value: unknown, message: string, errorCode: string) {
const key = String(value || '').trim()
if (!key) {
throw createHttpError(message, {
@@ -330,7 +344,7 @@ function requireKey(value, message, errorCode) {
return key
}
function requireId(value, message, errorCode) {
function requireId(value: unknown, message: string, errorCode: string) {
const id = Number(value)
if (!Number.isInteger(id) || id <= 0) {
throw createHttpError(message, {
@@ -342,7 +356,7 @@ function requireId(value, message, errorCode) {
return id
}
function tryParseJson(text) {
function tryParseJson(text: string) {
try {
return JSON.parse(text)
} catch {
@@ -350,12 +364,12 @@ function tryParseJson(text) {
}
}
function parseBindInfo(value) {
function parseBindInfo(value: unknown) {
const bindInfoText = String(value || '').trim()
return tryParseJson(bindInfoText) || bindInfoText || null
}
async function requestAmsIdeProbe(endpoint, options = {}) {
async function requestAmsIdeProbe(endpoint: string, options: JsonObject = {}): Promise<AmsProbeResponse> {
const url = new URL(endpoint)
if (!['http:', 'https:'].includes(url.protocol)) {
throw new Error('unsupported_ams_probe_protocol')
@@ -376,7 +390,7 @@ async function requestAmsIdeProbe(endpoint, options = {}) {
'accept-encoding': 'identity',
},
}, (response) => {
const chunks = []
const chunks: Buffer[] = []
let receivedBytes = 0
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()
for (const [key, value] of Object.entries(headers || {})) {
@@ -429,13 +443,13 @@ function createHeaderAdapter(headers) {
}
return {
get(name) {
get(name: unknown) {
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 chartId = String(payload.chartId || config.bindUrlProbeChartId || '323794').trim()
const subChartId = String(payload.subChartId || config.bindUrlProbeSubChartId || chartId).trim()
@@ -456,7 +470,11 @@ function buildAmsIdeProbeBody(signatureParams, config, payload = {}) {
return body
}
function buildAmsIdeProbeHeaders(signatureParams, config, options = {}) {
function buildAmsIdeProbeHeaders(
signatureParams: AmsSignatureParams,
config: JsonObject,
options: JsonObject = {},
) {
const userAgent = String(options.userAgent || '').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 cookies = [
`tokenParams=${encodeURIComponent(tokenParams)}`,
@@ -490,7 +508,7 @@ function buildAmsIdeProbeCookie(signatureParams, config) {
return cookies.join('; ')
}
function extractBindUrlSignatureParams(bindUrl) {
function extractBindUrlSignatureParams(bindUrl: string): AmsSignatureParams | null {
try {
const url = new URL(bindUrl)
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 iRet = String(raw?.iRet ?? raw?.jData?.iRet ?? '').trim()
return ret === '0' || iRet === '0'
}
function isAmsSignatureExpired(raw) {
function isAmsSignatureExpired(raw: any) {
const codes = [
raw?.ret,
raw?.iRet,
@@ -559,7 +577,7 @@ function isAmsSignatureExpired(raw) {
return codes.includes(AMS_SIGNATURE_EXPIRED_CODE) || messages.some((message) => message.includes('签名已过期'))
}
function extractAmsBindInfo(raw) {
function extractAmsBindInfo(raw: any) {
const candidates = [
raw?.jData?.sBindInfo,
raw?.jData?.jData?.sBindInfo,
@@ -569,7 +587,7 @@ function extractAmsBindInfo(raw) {
return candidates.find((item) => isPlainObject(item)) || null
}
function normalizeAmsBindRoleInfo(bindInfo) {
function normalizeAmsBindRoleInfo(bindInfo: unknown) {
const source = isPlainObject(bindInfo) ? bindInfo : {}
return {
@@ -579,7 +597,7 @@ function normalizeAmsBindRoleInfo(bindInfo) {
}
}
function normalizeHeaderMap(headers) {
function normalizeHeaderMap(headers: unknown) {
if (!headers || typeof headers !== 'object') {
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)
}
@@ -666,6 +666,14 @@
- `npm run typecheck`
- `npm run build`
- `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 个用例通过
## 下一步建议