From fb95f7dc55661efa90b8773a1906f826e480f438 Mon Sep 17 00:00:00 2001 From: yml Date: Thu, 21 May 2026 16:26:53 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=8E=E7=AB=AF=E8=BF=81=E7=A7=BB=E4=BA=91?= =?UTF-8?q?=E8=A7=A6=E6=89=8B=E8=99=9A=E6=8B=9F=E5=8F=B7=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...r-service.js => virtual-number-service.ts} | 80 ++++++++++++------- docs/backend-typescript-migration-plan.md | 8 ++ 2 files changed, 57 insertions(+), 31 deletions(-) rename apps/backend/src/services/platforms/cloudtentacles/{virtual-number-service.js => virtual-number-service.ts} (89%) diff --git a/apps/backend/src/services/platforms/cloudtentacles/virtual-number-service.js b/apps/backend/src/services/platforms/cloudtentacles/virtual-number-service.ts similarity index 89% rename from apps/backend/src/services/platforms/cloudtentacles/virtual-number-service.js rename to apps/backend/src/services/platforms/cloudtentacles/virtual-number-service.ts index 70c4d353..9d2c3263 100644 --- a/apps/backend/src/services/platforms/cloudtentacles/virtual-number-service.js +++ b/apps/backend/src/services/platforms/cloudtentacles/virtual-number-service.ts @@ -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 +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 { 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}×tamp=${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) } diff --git a/docs/backend-typescript-migration-plan.md b/docs/backend-typescript-migration-plan.md index 1dd840ec..d74c1bdf 100644 --- a/docs/backend-typescript-migration-plan.md +++ b/docs/backend-typescript-migration-plan.md @@ -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 个用例通过 ## 下一步建议