Files
snowy-yang 354df2e739 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
2026-09-25 23:06:23 +08:00

205 lines
6.0 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 网络请求模块
// 接口与原版保持一致:
// 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 }