diff --git a/danmu_api/server.js b/danmu_api/server.js index 70bf3f2..385f38b 100644 --- a/danmu_api/server.js +++ b/danmu_api/server.js @@ -23,6 +23,11 @@ import { startFavoriteScheduler, stopFavoriteScheduler } from './utils/favorite- // 导入 ES module 兼容层(始终加载,但内部会根据需要启用) import './esm-shim.cjs'; +// 预加载 node-fetch v3:仅在 Node < 20.19.0 等需兼容层的环境实际加载,其他环境为无操作;必须在首个请求前完成,否则 esm-shim 的 require 代理会拒绝同步取用 +if (typeof global.loadNodeFetch === 'function') { + await global.loadNodeFetch(); +} + // 构建 CommonJS 环境下才有的全局变量 const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); diff --git a/danmu_api/utils/http-util.js b/danmu_api/utils/http-util.js index b679bf9..bb294d0 100644 --- a/danmu_api/utils/http-util.js +++ b/danmu_api/utils/http-util.js @@ -1,6 +1,8 @@ import { globals } from '../configs/globals.js'; import { log } from './log-util.js' import { AsyncLocalStorage } from 'node:async_hooks'; +import https from 'node:https'; +import http from 'node:http'; // 跨异步生命周期链路的日志上下文追踪器 export const sourceLogContext = new AsyncLocalStorage(); @@ -59,6 +61,31 @@ function linkSignal(externalSignal, internalController) { }; } +// 旧版 Node(<20.19.0,自带 undici 解析响应头时丢弃 Set-Cookie)与 iOS 巨魔(无 WebAssembly、无原生 fetch)改用 node-fetch v3(其 Headers 正常暴露 Set-Cookie);降级边界与 esm-shim 的 20.19.0 一致,Node >= 20.19.0 仍用原生 fetch。判定仅依赖静态环境、进程内恒定,故模块加载时算一次并缓存。 +function detectNodeFetchDowngrade() { + if (typeof WebAssembly === 'undefined') return true; + const [major, minor] = process.versions.node.split('.').map(Number); + return major < 20 || (major === 20 && minor < 19); +} + +const USE_NODE_FETCH = detectNodeFetchDowngrade(); +if (USE_NODE_FETCH) { + // 模块载入时 logLevel 尚未初始化,用 console.log 保证启动提示必现 + console.log("[system] [http] 检测到旧版Node/iOS环境,已全局切换至 node-fetch v3 作为请求实现"); +} + +// 降级分支共享 keep-alive Agent,复用 TCP/TLS 连接以与原生 undici 连接池达到实际等价(消除重复握手开销);按协议区分 https/http +const nodeFetchHttpsAgent = USE_NODE_FETCH ? new https.Agent({ keepAlive: true, keepAliveMsecs: 1000, maxSockets: 256 }) : null; +const nodeFetchHttpAgent = USE_NODE_FETCH ? new http.Agent({ keepAlive: true, keepAliveMsecs: 1000, maxSockets: 256 }) : null; +function nodeFetchAgent(parsedUrl) { + const protocol = parsedUrl instanceof URL ? parsedUrl.protocol : new URL(parsedUrl).protocol; + return protocol === 'https:' ? nodeFetchHttpsAgent : nodeFetchHttpAgent; +} + +function shouldUseNodeFetch() { + return USE_NODE_FETCH; +} + export async function httpGet(url, options = {}) { // 单次搜索请求内 HTTP 响应复用: 若当前请求上下文已激活复用缓存且本 URL 已缓存, 直接返回克隆结果, 跳过重复网络请求 const requestHttpCache = httpCacheContext.getStore(); @@ -103,17 +130,17 @@ export async function httpGet(url, options = {}) { const cleanupSignal = linkSignal(options.signal, controller); try { - // 兼容iOS巨魔环境:使用node-fetch替代内置fetch + // 兼容iOS巨魔或旧版Node:使用node-fetch替代内置fetch let response; - if (typeof WebAssembly === 'undefined') { - log("info", "[system] [http] iOS环境降级使用node-fetch"); + if (shouldUseNodeFetch()) { const fetch = (await import('node-fetch')).default; response = await fetch(url, { method: 'GET', headers: { ...options.headers, }, - signal: controller.signal + signal: controller.signal, + agent: nodeFetchAgent }); } else { // 现代浏览器环境 @@ -328,12 +355,11 @@ export async function httpPost(url, body, options = {}) { } try { - // 兼容iOS巨魔环境:使用node-fetch替代内置fetch + // 兼容iOS巨魔或旧版Node:使用node-fetch替代内置fetch let response; - if (typeof WebAssembly === 'undefined') { - log("info", "[system] [http] iOS环境降级使用node-fetch"); + if (shouldUseNodeFetch()) { const fetch = (await import('node-fetch')).default; - response = await fetch(url, fetchOptions); + response = await fetch(url, { ...fetchOptions, agent: nodeFetchAgent }); } else { // 现代浏览器环境 response = await fetch(url, fetchOptions); @@ -452,7 +478,14 @@ async function httpRequestMethod(method, url, body, options = {}) { } try { - const response = await fetch(url, fetchOptions); + // 兼容iOS巨魔或旧版Node:使用node-fetch替代内置fetch + let response; + if (shouldUseNodeFetch()) { + const fetch = (await import('node-fetch')).default; + response = await fetch(url, { ...fetchOptions, agent: nodeFetchAgent }); + } else { + response = await fetch(url, fetchOptions); + } const textData = await response.text(); if (!response.ok && !validStatusCodes.includes(response.status)) { @@ -712,11 +745,23 @@ export async function httpGetWithStreamCheck(url, options = {}, checkCallback) { const currentSource = sourceLogContext.getStore() || "system"; log("info", `[${currentSource}] [流式请求] HTTP GET: ${url}`); - const response = await fetch(url, { - method: 'GET', - headers: headers, - signal: controller.signal - }); + // 兼容iOS巨魔或旧版Node:使用node-fetch替代内置fetch + let response; + if (shouldUseNodeFetch()) { + const fetch = (await import('node-fetch')).default; + response = await fetch(url, { + method: 'GET', + headers: headers, + signal: controller.signal, + agent: nodeFetchAgent + }); + } else { + response = await fetch(url, { + method: 'GET', + headers: headers, + signal: controller.signal + }); + } if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`);