后端迁移云触手虚拟号服务
This commit is contained in:
+49
-31
@@ -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}×tamp=${signatureParams.timestamp}&nonce=${signatureParams.nonce}&sign=${signatureParams.sign}`
|
const tokenParams = `?userId=${signatureParams.userId}×tamp=${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 个用例通过
|
||||||
|
|
||||||
## 下一步建议
|
## 下一步建议
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user