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:
snowy-yang committed 2026-09-25 23:06:23 +08:00
1 parent c8c939aa32
commit 354df2e739
126 files changed
+413 -2942

No files matched your search

+204
View File
@@ -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 }