|
1 | 1 | import { globals } from '../configs/globals.js'; |
2 | 2 | import { log } from './log-util.js' |
3 | 3 | import { AsyncLocalStorage } from 'node:async_hooks'; |
| 4 | +import https from 'node:https'; |
| 5 | +import http from 'node:http'; |
4 | 6 |
|
5 | 7 | // 跨异步生命周期链路的日志上下文追踪器 |
6 | 8 | export const sourceLogContext = new AsyncLocalStorage(); |
@@ -59,6 +61,31 @@ function linkSignal(externalSignal, internalController) { |
59 | 61 | }; |
60 | 62 | } |
61 | 63 |
|
| 64 | +// 旧版 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。判定仅依赖静态环境、进程内恒定,故模块加载时算一次并缓存。 |
| 65 | +function detectNodeFetchDowngrade() { |
| 66 | + if (typeof WebAssembly === 'undefined') return true; |
| 67 | + const [major, minor] = process.versions.node.split('.').map(Number); |
| 68 | + return major < 20 || (major === 20 && minor < 19); |
| 69 | +} |
| 70 | + |
| 71 | +const USE_NODE_FETCH = detectNodeFetchDowngrade(); |
| 72 | +if (USE_NODE_FETCH) { |
| 73 | + // 模块载入时 logLevel 尚未初始化,用 console.log 保证启动提示必现 |
| 74 | + console.log("[system] [http] 检测到旧版Node/iOS环境,已全局切换至 node-fetch v3 作为请求实现"); |
| 75 | +} |
| 76 | + |
| 77 | +// 降级分支共享 keep-alive Agent,复用 TCP/TLS 连接以与原生 undici 连接池达到实际等价(消除重复握手开销);按协议区分 https/http |
| 78 | +const nodeFetchHttpsAgent = USE_NODE_FETCH ? new https.Agent({ keepAlive: true, keepAliveMsecs: 1000, maxSockets: 256 }) : null; |
| 79 | +const nodeFetchHttpAgent = USE_NODE_FETCH ? new http.Agent({ keepAlive: true, keepAliveMsecs: 1000, maxSockets: 256 }) : null; |
| 80 | +function nodeFetchAgent(parsedUrl) { |
| 81 | + const protocol = parsedUrl instanceof URL ? parsedUrl.protocol : new URL(parsedUrl).protocol; |
| 82 | + return protocol === 'https:' ? nodeFetchHttpsAgent : nodeFetchHttpAgent; |
| 83 | +} |
| 84 | + |
| 85 | +function shouldUseNodeFetch() { |
| 86 | + return USE_NODE_FETCH; |
| 87 | +} |
| 88 | + |
62 | 89 | export async function httpGet(url, options = {}) { |
63 | 90 | // 单次搜索请求内 HTTP 响应复用: 若当前请求上下文已激活复用缓存且本 URL 已缓存, 直接返回克隆结果, 跳过重复网络请求 |
64 | 91 | const requestHttpCache = httpCacheContext.getStore(); |
@@ -103,17 +130,17 @@ export async function httpGet(url, options = {}) { |
103 | 130 | const cleanupSignal = linkSignal(options.signal, controller); |
104 | 131 |
|
105 | 132 | try { |
106 | | - // 兼容iOS巨魔环境:使用node-fetch替代内置fetch |
| 133 | + // 兼容iOS巨魔或旧版Node:使用node-fetch替代内置fetch |
107 | 134 | let response; |
108 | | - if (typeof WebAssembly === 'undefined') { |
109 | | - log("info", "[system] [http] iOS环境降级使用node-fetch"); |
| 135 | + if (shouldUseNodeFetch()) { |
110 | 136 | const fetch = (await import('node-fetch')).default; |
111 | 137 | response = await fetch(url, { |
112 | 138 | method: 'GET', |
113 | 139 | headers: { |
114 | 140 | ...options.headers, |
115 | 141 | }, |
116 | | - signal: controller.signal |
| 142 | + signal: controller.signal, |
| 143 | + agent: nodeFetchAgent |
117 | 144 | }); |
118 | 145 | } else { |
119 | 146 | // 现代浏览器环境 |
@@ -328,12 +355,11 @@ export async function httpPost(url, body, options = {}) { |
328 | 355 | } |
329 | 356 |
|
330 | 357 | try { |
331 | | - // 兼容iOS巨魔环境:使用node-fetch替代内置fetch |
| 358 | + // 兼容iOS巨魔或旧版Node:使用node-fetch替代内置fetch |
332 | 359 | let response; |
333 | | - if (typeof WebAssembly === 'undefined') { |
334 | | - log("info", "[system] [http] iOS环境降级使用node-fetch"); |
| 360 | + if (shouldUseNodeFetch()) { |
335 | 361 | const fetch = (await import('node-fetch')).default; |
336 | | - response = await fetch(url, fetchOptions); |
| 362 | + response = await fetch(url, { ...fetchOptions, agent: nodeFetchAgent }); |
337 | 363 | } else { |
338 | 364 | // 现代浏览器环境 |
339 | 365 | response = await fetch(url, fetchOptions); |
@@ -452,7 +478,14 @@ async function httpRequestMethod(method, url, body, options = {}) { |
452 | 478 | } |
453 | 479 |
|
454 | 480 | try { |
455 | | - const response = await fetch(url, fetchOptions); |
| 481 | + // 兼容iOS巨魔或旧版Node:使用node-fetch替代内置fetch |
| 482 | + let response; |
| 483 | + if (shouldUseNodeFetch()) { |
| 484 | + const fetch = (await import('node-fetch')).default; |
| 485 | + response = await fetch(url, { ...fetchOptions, agent: nodeFetchAgent }); |
| 486 | + } else { |
| 487 | + response = await fetch(url, fetchOptions); |
| 488 | + } |
456 | 489 | const textData = await response.text(); |
457 | 490 |
|
458 | 491 | if (!response.ok && !validStatusCodes.includes(response.status)) { |
@@ -712,11 +745,23 @@ export async function httpGetWithStreamCheck(url, options = {}, checkCallback) { |
712 | 745 | const currentSource = sourceLogContext.getStore() || "system"; |
713 | 746 | log("info", `[${currentSource}] [流式请求] HTTP GET: ${url}`); |
714 | 747 |
|
715 | | - const response = await fetch(url, { |
716 | | - method: 'GET', |
717 | | - headers: headers, |
718 | | - signal: controller.signal |
719 | | - }); |
| 748 | + // 兼容iOS巨魔或旧版Node:使用node-fetch替代内置fetch |
| 749 | + let response; |
| 750 | + if (shouldUseNodeFetch()) { |
| 751 | + const fetch = (await import('node-fetch')).default; |
| 752 | + response = await fetch(url, { |
| 753 | + method: 'GET', |
| 754 | + headers: headers, |
| 755 | + signal: controller.signal, |
| 756 | + agent: nodeFetchAgent |
| 757 | + }); |
| 758 | + } else { |
| 759 | + response = await fetch(url, { |
| 760 | + method: 'GET', |
| 761 | + headers: headers, |
| 762 | + signal: controller.signal |
| 763 | + }); |
| 764 | + } |
720 | 765 |
|
721 | 766 | if (!response.ok) { |
722 | 767 | throw new Error(`HTTP error! status: ${response.status}`); |
|
0 commit comments