refactor: 架构整固——分层单向化、全量 TypeScript、直接运行
- 移除 bd/mg 两平台及磁盘残留产物;清理 api-test 死脚本、musicSdk/message+options 死模块、infSign vendor - musicSdk/ 改名 sdk/;src/common、src/auth、src/apis 整体移入 sdk/,依赖方向定为 src → sdk 严格单向 - src/ 手写 JS 全量转 .ts,删除 JS 类型垫片 modules.d.ts;源码 import 统一 .ts 后缀 - Node 原生类型剥离直接运行 .ts(engines >=22.18),取消 tsc 发射产物,tsc 仅类型检查(pnpm typecheck) - 删除 kwdes 备用实现与 src/index.ts 转发垫片;测试移至 test/smoke-test.ts - common/request.ts 补显式导出类型;修复 set-cookie 单值形态、ApiError 类字段等隐患 验证:tsc 零错误,smoke-test 41/41
This commit is contained in:
1 parent
c8c939aa32
commit
354df2e739
126 files changed
+413
-2942
No files matched your search
@@ -0,0 +1,204 @@
|
||||
// 网络请求模块
|
||||
// 接口与原版保持一致:
|
||||
// httpFetch(url, { method, headers, body, form, timeout, lookup, family })
|
||||
// → { promise, cancelHttp }
|
||||
// promise resolve → { statusCode, body, headers, raw }
|
||||
// - body:JSON 已解析对象,或原始字符串(HTML/文本)
|
||||
// - raw:响应体 Buffer
|
||||
// promise reject → 错误(timeout 请求超时 / canceled 主动取消)
|
||||
// 注意:promise 与 cancelHttp 均为普通可赋值属性(SDK 多处会重写 promise)
|
||||
|
||||
import http from 'node:http'
|
||||
import https from 'node:https'
|
||||
import zlib from 'node:zlib'
|
||||
import { URL } from 'node:url'
|
||||
import { requestMsg } from './message.ts'
|
||||
|
||||
const MAX_BUFFER_SIZE = 64 * 1024 * 1024 // 64MB
|
||||
|
||||
export interface HttpOptions {
|
||||
method?: string
|
||||
headers?: Record<string, any>
|
||||
body?: any
|
||||
form?: Record<string, any>
|
||||
timeout?: number
|
||||
lookup?: any
|
||||
family?: number
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface HttpFetchResponse {
|
||||
statusCode: number
|
||||
statusMessage?: string
|
||||
headers: Record<string, any>
|
||||
body: any
|
||||
raw: Buffer
|
||||
}
|
||||
|
||||
export interface HttpRequestObject {
|
||||
promise: Promise<HttpFetchResponse>
|
||||
cancelHttp(): void
|
||||
isCancelled: boolean
|
||||
requestObj?: any
|
||||
cancelFn?: ((err: any) => void) | null
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
const defaultHeaders = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36',
|
||||
}
|
||||
|
||||
const parseBody = (raw: Buffer): any => {
|
||||
if (!raw || !raw.length) return null
|
||||
const text = raw.toString('utf8')
|
||||
// 与原版一致:无论 content-type 都尝试 JSON 解析,失败则返回原始文本
|
||||
try {
|
||||
return JSON.parse(text)
|
||||
} catch {
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发起请求(返回带 promise 与 cancelHttp 的对象)
|
||||
*/
|
||||
const buildHttpPromise = (url: string, options: HttpOptions = {}): HttpRequestObject => {
|
||||
const method = (options.method || 'GET').toUpperCase()
|
||||
const obj: HttpRequestObject = {
|
||||
promise: null as any,
|
||||
isCancelled: false,
|
||||
cancelHttp: () => {
|
||||
if (!obj.requestObj) {
|
||||
obj.isCancelled = true
|
||||
return
|
||||
}
|
||||
obj.requestObj.destroy(new Error('Canceled'))
|
||||
obj.requestObj = null
|
||||
// 取消后置空是运行时事实,但公开类型保持非空(消费方总是先取 promise 再取消)
|
||||
;(obj as any).promise = (obj as any).cancelHttp = null
|
||||
obj.cancelFn?.(new Error('Canceled'))
|
||||
obj.cancelFn = null
|
||||
},
|
||||
}
|
||||
obj.promise = new Promise((resolve, reject) => {
|
||||
obj.cancelFn = reject
|
||||
|
||||
let parsedUrl: URL
|
||||
try {
|
||||
parsedUrl = new URL(url)
|
||||
} catch (err) {
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
|
||||
const isHttps = parsedUrl.protocol === 'https:'
|
||||
const client = isHttps ? https : http
|
||||
|
||||
// 数据体构造:body(对象/字符串/Buffer)与 form(对象 → urlencoded)
|
||||
let body: string | any = null
|
||||
let isForm = false
|
||||
if (options.form) {
|
||||
const formBody = new URLSearchParams()
|
||||
for (const key of Object.keys(options.form)) {
|
||||
const value = options.form[key]
|
||||
if (value == null) continue
|
||||
formBody.append(key, typeof value === 'object' ? JSON.stringify(value) : value)
|
||||
}
|
||||
body = formBody.toString()
|
||||
isForm = true
|
||||
} else if (options.body) {
|
||||
body = typeof options.body === 'object' && !Buffer.isBuffer(options.body) ? JSON.stringify(options.body) : options.body
|
||||
}
|
||||
|
||||
const headers: Record<string, any> = { ...defaultHeaders, ...(options.headers || {}) }
|
||||
if (body && !headers['content-type'] && !headers['Content-Type']) {
|
||||
headers['content-type'] = isForm ? 'application/x-www-form-urlencoded' : 'application/json'
|
||||
}
|
||||
if (body) headers['content-length'] = Buffer.byteLength(body)
|
||||
|
||||
const requestOptions: Record<string, any> = {
|
||||
method,
|
||||
headers,
|
||||
family: options.family || undefined,
|
||||
}
|
||||
if (options.lookup) requestOptions.lookup = options.lookup
|
||||
|
||||
const req = client.request(parsedUrl, requestOptions, res => {
|
||||
obj.requestObj = null
|
||||
obj.cancelFn = null
|
||||
const statusCode = res.statusCode ?? 0
|
||||
|
||||
let stream: any = res
|
||||
switch (res.headers['content-encoding']) {
|
||||
case 'gzip': stream = res.pipe(zlib.createGunzip()); break
|
||||
case 'deflate': stream = res.pipe(zlib.createInflate()); break
|
||||
case 'br': stream = res.pipe(zlib.createBrotliDecompress()); break
|
||||
}
|
||||
|
||||
const chunks: Buffer[] = []
|
||||
let size = 0
|
||||
stream.on('data', chunk => {
|
||||
size += chunk.length
|
||||
if (size > MAX_BUFFER_SIZE) {
|
||||
stream.destroy(new Error('Response too large'))
|
||||
return
|
||||
}
|
||||
chunks.push(chunk)
|
||||
})
|
||||
stream.on('error', err => reject(err))
|
||||
stream.on('end', () => {
|
||||
const raw = Buffer.concat(chunks)
|
||||
resolve({
|
||||
statusCode,
|
||||
headers: res.headers,
|
||||
body: parseBody(raw),
|
||||
raw,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
obj.requestObj = req
|
||||
|
||||
req.on('error', err => {
|
||||
const e = err as any
|
||||
if (obj.isCancelled || e.canceled) e.canceled = true
|
||||
reject(e)
|
||||
})
|
||||
|
||||
if (options.timeout) {
|
||||
req.setTimeout(options.timeout, () => {
|
||||
const err = new Error('Request timeout') as any
|
||||
err.timeout = true
|
||||
req.destroy(err)
|
||||
})
|
||||
}
|
||||
|
||||
if (body) req.write(body)
|
||||
req.end()
|
||||
})
|
||||
return obj
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求超时/断网错误统一处理(与原版行为一致)
|
||||
*/
|
||||
export const httpFetch = (url: string, options: HttpOptions = { method: 'get' }): HttpRequestObject => {
|
||||
const requestObj = buildHttpPromise(url, options)
|
||||
requestObj.promise = requestObj.promise.catch((err: any) => {
|
||||
if (err.canceled) return Promise.reject(err)
|
||||
switch (err.code) {
|
||||
case 'ETIMEDOUT':
|
||||
case 'ESOCKETTIMEDOUT':
|
||||
return Promise.reject(new Error(requestMsg.timeout))
|
||||
case 'ENOTFOUND':
|
||||
return Promise.reject(new Error(requestMsg.notConnectNetwork))
|
||||
case 'ECONNRESET':
|
||||
return Promise.reject(new Error(requestMsg.fail))
|
||||
default:
|
||||
return Promise.reject(err)
|
||||
}
|
||||
})
|
||||
return requestObj
|
||||
}
|
||||
|
||||
export default { httpFetch }
|
||||
Reference in new issue
Block a user