kpinquan commited on
Commit
e069b15
·
verified ·
1 Parent(s): e85d987

Upload [...path].mjs

Browse files
Files changed (1) hide show
  1. api/proxy/[...path].mjs +451 -0
api/proxy/[...path].mjs ADDED
@@ -0,0 +1,451 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // /api/proxy/[...path].mjs - Vercel Serverless Function (ES Module)
2
+
3
+ import fetch from 'node-fetch';
4
+ import { URL } from 'url'; // 使用 Node.js 内置 URL 处理
5
+
6
+ // --- 配置 (从环境变量读取) ---
7
+ const DEBUG_ENABLED = process.env.DEBUG === 'true';
8
+ const CACHE_TTL = parseInt(process.env.CACHE_TTL || '86400', 10); // 默认 24 小时
9
+ const MAX_RECURSION = parseInt(process.env.MAX_RECURSION || '5', 10); // 默认 5 层
10
+
11
+ // --- User Agent 处理 ---
12
+ // 默认 User Agent 列表
13
+ let USER_AGENTS = [
14
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
15
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15'
16
+ ];
17
+ // 尝试从环境变量读取并解析 USER_AGENTS_JSON
18
+ try {
19
+ const agentsJsonString = process.env.USER_AGENTS_JSON;
20
+ if (agentsJsonString) {
21
+ const parsedAgents = JSON.parse(agentsJsonString);
22
+ // 检查解析结果是否为非空数组
23
+ if (Array.isArray(parsedAgents) && parsedAgents.length > 0) {
24
+ USER_AGENTS = parsedAgents; // 使用环境变量中的数组
25
+ console.log(`[代理日志] 已从环境变量加载 ${USER_AGENTS.length} 个 User Agent。`);
26
+ } else {
27
+ console.warn("[代理日志] 环境变量 USER_AGENTS_JSON 不是有效的非空数组,使用默认值。");
28
+ }
29
+ } else {
30
+ console.log("[代理日志] 未设置环境变量 USER_AGENTS_JSON,使用默认 User Agent。");
31
+ }
32
+ } catch (e) {
33
+ // 如果 JSON 解析失败,记录错误并使用默认值
34
+ console.error(`[代理日志] 解析环境变量 USER_AGENTS_JSON 出错: ${e.message}。使用默认 User Agent。`);
35
+ }
36
+
37
+ // 广告过滤在代理中禁用,由播放器处理
38
+ const FILTER_DISCONTINUITY = false;
39
+
40
+
41
+ // --- 辅助函数 ---
42
+
43
+ function logDebug(message) {
44
+ if (DEBUG_ENABLED) {
45
+ console.log(`[代理日志] ${message}`);
46
+ }
47
+ }
48
+
49
+ /**
50
+ * 从代理请求路径中提取编码后的目标 URL。
51
+ * @param {string} encodedPath - URL 编码后的路径部分 (例如 "https%3A%2F%2F...")
52
+ * @returns {string|null} 解码后的目标 URL,如果无效则返回 null。
53
+ */
54
+ function getTargetUrlFromPath(encodedPath) {
55
+ if (!encodedPath) {
56
+ logDebug("getTargetUrlFromPath 收到空路径。");
57
+ return null;
58
+ }
59
+ try {
60
+ const decodedUrl = decodeURIComponent(encodedPath);
61
+ // 基础检查,看是否像一个 HTTP/HTTPS URL
62
+ if (decodedUrl.match(/^https?:\/\/.+/i)) {
63
+ return decodedUrl;
64
+ } else {
65
+ logDebug(`无效的解码 URL 格式: ${decodedUrl}`);
66
+ // 备选检查:原始路径是否未编码但看起来像 URL?
67
+ if (encodedPath.match(/^https?:\/\/.+/i)) {
68
+ logDebug(`警告: 路径未编码但看起来像 URL: ${encodedPath}`);
69
+ return encodedPath;
70
+ }
71
+ return null;
72
+ }
73
+ } catch (e) {
74
+ // 捕获解码错误 (例如格式错误的 URI)
75
+ logDebug(`解码目标 URL 出错: ${encodedPath} - ${e.message}`);
76
+ return null;
77
+ }
78
+ }
79
+
80
+ function getBaseUrl(urlStr) {
81
+ if (!urlStr) return '';
82
+ try {
83
+ const parsedUrl = new URL(urlStr);
84
+ // 处理根目录或只有文件名的情况
85
+ const pathSegments = parsedUrl.pathname.split('/').filter(Boolean); // 移除空字符串
86
+ if (pathSegments.length <= 1) {
87
+ return `${parsedUrl.origin}/`;
88
+ }
89
+ pathSegments.pop(); // 移除最后一段
90
+ return `${parsedUrl.origin}/${pathSegments.join('/')}/`;
91
+ } catch (e) {
92
+ logDebug(`获取 BaseUrl 失败: "${urlStr}": ${e.message}`);
93
+ // 备用方法:查找最后一个斜杠
94
+ const lastSlashIndex = urlStr.lastIndexOf('/');
95
+ if (lastSlashIndex > urlStr.indexOf('://') + 2) { // 确保不是协议部分的斜杠
96
+ return urlStr.substring(0, lastSlashIndex + 1);
97
+ }
98
+ return urlStr + '/'; // 如果没有路径,添加斜杠
99
+ }
100
+ }
101
+
102
+ function resolveUrl(baseUrl, relativeUrl) {
103
+ if (!relativeUrl) return ''; // 处理空的 relativeUrl
104
+ if (relativeUrl.match(/^https?:\/\/.+/i)) {
105
+ return relativeUrl; // 已经是绝对 URL
106
+ }
107
+ if (!baseUrl) return relativeUrl; // 没有基础 URL 无法解析
108
+
109
+ try {
110
+ // 使用 Node.js 的 URL 构造函数处理相对路径
111
+ return new URL(relativeUrl, baseUrl).toString();
112
+ } catch (e) {
113
+ logDebug(`URL 解析失败: base="${baseUrl}", relative="${relativeUrl}". 错误: ${e.message}`);
114
+ // 简单的备用逻辑
115
+ if (relativeUrl.startsWith('/')) {
116
+ try {
117
+ const baseOrigin = new URL(baseUrl).origin;
118
+ return `${baseOrigin}${relativeUrl}`;
119
+ } catch { return relativeUrl; } // 如果 baseUrl 也无效,返回原始相对路径
120
+ } else {
121
+ // 假设相对于包含基础 URL ���源的目录
122
+ return `${baseUrl.substring(0, baseUrl.lastIndexOf('/') + 1)}${relativeUrl}`;
123
+ }
124
+ }
125
+ }
126
+
127
+ // ** 已修正:确保生成 /proxy/ 前缀的链接 **
128
+ function rewriteUrlToProxy(targetUrl) {
129
+ if (!targetUrl || typeof targetUrl !== 'string') return '';
130
+ // 返回与 vercel.json 的 "source" 和前端 PROXY_URL 一致的路径
131
+ return `/proxy/${encodeURIComponent(targetUrl)}`;
132
+ }
133
+
134
+ function getRandomUserAgent() {
135
+ return USER_AGENTS[Math.floor(Math.random() * USER_AGENTS.length)];
136
+ }
137
+
138
+ async function fetchContentWithType(targetUrl, requestHeaders) {
139
+ // 准备请求头
140
+ const headers = {
141
+ 'User-Agent': getRandomUserAgent(),
142
+ 'Accept': requestHeaders['accept'] || '*/*', // 传递原始 Accept 头(如果有)
143
+ 'Accept-Language': requestHeaders['accept-language'] || 'zh-CN,zh;q=0.9,en;q=0.8',
144
+ // 尝试设置一个合理的 Referer
145
+ 'Referer': requestHeaders['referer'] || new URL(targetUrl).origin,
146
+ };
147
+ // 清理空值的头
148
+ Object.keys(headers).forEach(key => headers[key] === undefined || headers[key] === null || headers[key] === '' ? delete headers[key] : {});
149
+
150
+ logDebug(`准备请求目标: ${targetUrl},请求头: ${JSON.stringify(headers)}`);
151
+
152
+ try {
153
+ // 发起 fetch 请求
154
+ const response = await fetch(targetUrl, { headers, redirect: 'follow' });
155
+
156
+ // 检查响应是否成功
157
+ if (!response.ok) {
158
+ const errorBody = await response.text().catch(() => ''); // 尝试获取错误响应体
159
+ logDebug(`请求失败: ${response.status} ${response.statusText} - ${targetUrl}`);
160
+ // 创建一个包含状态码的错误对象
161
+ const err = new Error(`HTTP 错误 ${response.status}: ${response.statusText}. URL: ${targetUrl}. Body: ${errorBody.substring(0, 200)}`);
162
+ err.status = response.status; // 将状态码附加到错误对象
163
+ throw err; // 抛出错误
164
+ }
165
+
166
+ // 读取响应内容
167
+ const content = await response.text();
168
+ const contentType = response.headers.get('content-type') || '';
169
+ logDebug(`请求成功: ${targetUrl}, Content-Type: ${contentType}, 内容长度: ${content.length}`);
170
+ // 返回结果
171
+ return { content, contentType, responseHeaders: response.headers };
172
+
173
+ } catch (error) {
174
+ // 捕获 fetch 本身的错误(网络、超时等)或上面抛出的 HTTP 错误
175
+ logDebug(`请求异常 ${targetUrl}: ${error.message}`);
176
+ // 重新抛出,确保包含原始错误信息
177
+ throw new Error(`请求目标 URL 失败 ${targetUrl}: ${error.message}`);
178
+ }
179
+ }
180
+
181
+ function isM3u8Content(content, contentType) {
182
+ if (contentType && (contentType.includes('application/vnd.apple.mpegurl') || contentType.includes('application/x-mpegurl') || contentType.includes('audio/mpegurl'))) {
183
+ return true;
184
+ }
185
+ return content && typeof content === 'string' && content.trim().startsWith('#EXTM3U');
186
+ }
187
+
188
+ function processKeyLine(line, baseUrl) {
189
+ return line.replace(/URI="([^"]+)"/, (match, uri) => {
190
+ const absoluteUri = resolveUrl(baseUrl, uri);
191
+ logDebug(`处理 KEY URI: 原始='${uri}', 绝对='${absoluteUri}'`);
192
+ return `URI="${rewriteUrlToProxy(absoluteUri)}"`;
193
+ });
194
+ }
195
+
196
+ function processMapLine(line, baseUrl) {
197
+ return line.replace(/URI="([^"]+)"/, (match, uri) => {
198
+ const absoluteUri = resolveUrl(baseUrl, uri);
199
+ logDebug(`处理 MAP URI: 原始='${uri}', 绝对='${absoluteUri}'`);
200
+ return `URI="${rewriteUrlToProxy(absoluteUri)}"`;
201
+ });
202
+ }
203
+
204
+ function processMediaPlaylist(url, content) {
205
+ const baseUrl = getBaseUrl(url);
206
+ if (!baseUrl) {
207
+ logDebug(`无法确定媒体列表的 Base URL: ${url},相对路径可能无法处理。`);
208
+ }
209
+ const lines = content.split('\n');
210
+ const output = [];
211
+ for (let i = 0; i < lines.length; i++) {
212
+ const line = lines[i].trim();
213
+ // 保留最后一个空行
214
+ if (!line && i === lines.length - 1) { output.push(line); continue; }
215
+ if (!line) continue; // 跳过中间空行
216
+ // 广告过滤已禁用
217
+ if (line.startsWith('#EXT-X-KEY')) { output.push(processKeyLine(line, baseUrl)); continue; }
218
+ if (line.startsWith('#EXT-X-MAP')) { output.push(processMapLine(line, baseUrl)); continue; }
219
+ if (line.startsWith('#EXTINF')) { output.push(line); continue; }
220
+ // 处理 URL 行
221
+ if (!line.startsWith('#')) {
222
+ const absoluteUrl = resolveUrl(baseUrl, line);
223
+ logDebug(`重写媒体片段: 原始='${line}', 解析后='${absoluteUrl}'`);
224
+ output.push(rewriteUrlToProxy(absoluteUrl)); continue;
225
+ }
226
+ // 保留其他 M3U8 标签
227
+ output.push(line);
228
+ }
229
+ return output.join('\n');
230
+ }
231
+
232
+ async function processM3u8Content(targetUrl, content, recursionDepth = 0) {
233
+ // 判断是主列表还是媒体列表
234
+ if (content.includes('#EXT-X-STREAM-INF') || content.includes('#EXT-X-MEDIA:')) {
235
+ logDebug(`检测到主播放列表: ${targetUrl} (深度: ${recursionDepth})`);
236
+ return await processMasterPlaylist(targetUrl, content, recursionDepth);
237
+ }
238
+ logDebug(`检测到媒体播放列表: ${targetUrl} (深度: ${recursionDepth})`);
239
+ return processMediaPlaylist(targetUrl, content);
240
+ }
241
+
242
+ async function processMasterPlaylist(url, content, recursionDepth) {
243
+ // 检查递归深度
244
+ if (recursionDepth > MAX_RECURSION) {
245
+ throw new Error(`处理主播放列表时,递归深度超过最大限制 (${MAX_RECURSION}): ${url}`);
246
+ }
247
+ const baseUrl = getBaseUrl(url);
248
+ const lines = content.split('\n');
249
+ let highestBandwidth = -1;
250
+ let bestVariantUrl = '';
251
+
252
+ // 查找最高带宽的流
253
+ for (let i = 0; i < lines.length; i++) {
254
+ if (lines[i].startsWith('#EXT-X-STREAM-INF')) {
255
+ const bandwidthMatch = lines[i].match(/BANDWIDTH=(\d+)/);
256
+ const currentBandwidth = bandwidthMatch ? parseInt(bandwidthMatch[1], 10) : 0;
257
+ let variantUriLine = '';
258
+ // 找到下一行的 URI
259
+ for (let j = i + 1; j < lines.length; j++) {
260
+ const line = lines[j].trim();
261
+ if (line && !line.startsWith('#')) { variantUriLine = line; i = j; break; }
262
+ }
263
+ if (variantUriLine && currentBandwidth >= highestBandwidth) {
264
+ highestBandwidth = currentBandwidth;
265
+ bestVariantUrl = resolveUrl(baseUrl, variantUriLine);
266
+ }
267
+ }
268
+ }
269
+ // 如果没有找到带宽信息,尝试查找第一个 .m3u8 链接
270
+ if (!bestVariantUrl) {
271
+ logDebug(`主播放列表中未找到 BANDWIDTH 信息,尝试查找第一个 URI: ${url}`);
272
+ for (let i = 0; i < lines.length; i++) {
273
+ const line = lines[i].trim();
274
+ // 更可靠地匹配 .m3u8 链接
275
+ if (line && !line.startsWith('#') && line.match(/\.m3u8($|\?.*)/i)) {
276
+ bestVariantUrl = resolveUrl(baseUrl, line);
277
+ logDebug(`备选方案: 找到第一个子播放列表 URI: ${bestVariantUrl}`);
278
+ break;
279
+ }
280
+ }
281
+ }
282
+ // 如果仍然没有找到子列表 URL
283
+ if (!bestVariantUrl) {
284
+ logDebug(`在主播放列表 ${url} 中未找到有效的子列表 URI,将其作为媒体列表处理。`);
285
+ return processMediaPlaylist(url, content);
286
+ }
287
+
288
+ logDebug(`选择的子播放列表 (带宽: ${highestBandwidth}): ${bestVariantUrl}`);
289
+ // 请求选定的子播放列表内容 (注意:这里传递 {} 作为请求头,不传递客户端的原始请求头)
290
+ const { content: variantContent, contentType: variantContentType } = await fetchContentWithType(bestVariantUrl, {});
291
+
292
+ // 检查获取的内容是否是 M3U8
293
+ if (!isM3u8Content(variantContent, variantContentType)) {
294
+ logDebug(`获取的子播放列表 ${bestVariantUrl} 不是 M3U8 (类型: ${variantContentType}),将其作为媒体列表处理。`);
295
+ return processMediaPlaylist(bestVariantUrl, variantContent);
296
+ }
297
+
298
+ // 递归处理获取到的子 M3U8 内容
299
+ return await processM3u8Content(bestVariantUrl, variantContent, recursionDepth + 1);
300
+ }
301
+
302
+
303
+ // --- Vercel Handler 函数 ---
304
+ export default async function handler(req, res) {
305
+ // --- 记录请求开始 ---
306
+ console.info('--- Vercel 代理请求开始 ---');
307
+ console.info('时间:', new Date().toISOString());
308
+ console.info('方法:', req.method);
309
+ console.info('URL:', req.url); // 原始请求 URL (例如 /proxy/...)
310
+ console.info('查询参数:', JSON.stringify(req.query)); // Vercel 解析的查询参数
311
+
312
+ // --- 提前设置 CORS 头 ---
313
+ res.setHeader('Access-Control-Allow-Origin', '*');
314
+ res.setHeader('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS');
315
+ res.setHeader('Access-Control-Allow-Headers', '*'); // 允许所有请求头
316
+
317
+ // --- 处理 OPTIONS 预检请求 ---
318
+ if (req.method === 'OPTIONS') {
319
+ console.info("处理 OPTIONS 预检请求");
320
+ res.status(204).setHeader('Access-Control-Max-Age', '86400').end(); // 缓存预检结果 24 小时
321
+ return;
322
+ }
323
+
324
+ let targetUrl = null; // 初始化目标 URL
325
+
326
+ try { // ---- 开始主处理逻辑的 try 块 ----
327
+
328
+ // --- 提取目标 URL (主要依赖 req.query["...path"]) ---
329
+ // Vercel 将 :path* 捕获的内容(可能包含斜杠)放入 req.query["...path"] 数组
330
+ const pathData = req.query["...path"]; // 使用正确的键名
331
+ let encodedUrlPath = '';
332
+
333
+ if (pathData) {
334
+ if (Array.isArray(pathData)) {
335
+ encodedUrlPath = pathData.join('/'); // 重新组合
336
+ console.info(`从 req.query["...path"] (数组) 组合的编码路径: ${encodedUrlPath}`);
337
+ } else if (typeof pathData === 'string') {
338
+ encodedUrlPath = pathData; // 也处理 Vercel 可能只返回字符串的情况
339
+ console.info(`从 req.query["...path"] (字符串) 获取的编码路径: ${encodedUrlPath}`);
340
+ } else {
341
+ console.warn(`[代理警告] req.query["...path"] 类型未知: ${typeof pathData}`);
342
+ }
343
+ } else {
344
+ console.warn(`[代理警告] req.query["...path"] 为空或未定义。`);
345
+ // 备选:尝试从 req.url 提取(如果需要)
346
+ if (req.url && req.url.startsWith('/proxy/')) {
347
+ encodedUrlPath = req.url.substring('/proxy/'.length);
348
+ console.info(`使用备选方法从 req.url 提取的编码路径: ${encodedUrlPath}`);
349
+ }
350
+ }
351
+
352
+ // 如果仍然为空,则无法继续
353
+ if (!encodedUrlPath) {
354
+ throw new Error("无法从请求中确定编码后的目标路径。");
355
+ }
356
+
357
+ // 解析目标 URL
358
+ targetUrl = getTargetUrlFromPath(encodedUrlPath);
359
+ console.info(`解析出的目标 URL: ${targetUrl || 'null'}`); // 记录解析结果
360
+
361
+ // 检查目标 URL 是否有效
362
+ if (!targetUrl) {
363
+ // 抛出包含更多上下文的错误
364
+ throw new Error(`无效的代理请求路径。无法从组合路径 "${encodedUrlPath}" 中提取有效的目标 URL。`);
365
+ }
366
+
367
+ console.info(`开始处理目标 URL 的代理请求: ${targetUrl}`);
368
+
369
+ // --- 获取并处理目标内容 ---
370
+ const { content, contentType, responseHeaders } = await fetchContentWithType(targetUrl, req.headers);
371
+
372
+ // --- 如果是 M3U8,处理并返回 ---
373
+ if (isM3u8Content(content, contentType)) {
374
+ console.info(`正在处理 M3U8 内容: ${targetUrl}`);
375
+ const processedM3u8 = await processM3u8Content(targetUrl, content);
376
+
377
+ console.info(`成功处理 M3U8: ${targetUrl}`);
378
+ // 发送处理后的 M3U8 响应
379
+ res.status(200)
380
+ .setHeader('Content-Type', 'application/vnd.apple.mpegurl;charset=utf-8')
381
+ .setHeader('Cache-Control', `public, max-age=${CACHE_TTL}`)
382
+ // 移除可能导致问题的原始响应头
383
+ .removeHeader('content-encoding') // 很重要!node-fetch 已解压
384
+ .removeHeader('content-length') // 长度已改变
385
+ .send(processedM3u8); // 发送 M3U8 文本
386
+
387
+ } else {
388
+ // --- 如果不是 M3U8,直接返回原始内容 ---
389
+ console.info(`直接返回非 M3U8 内容: ${targetUrl}, 类型: ${contentType}`);
390
+
391
+ // 设置原始响应头,但排除有问题的头和 CORS 头(已设置)
392
+ responseHeaders.forEach((value, key) => {
393
+ const lowerKey = key.toLowerCase();
394
+ if (!lowerKey.startsWith('access-control-') &&
395
+ lowerKey !== 'content-encoding' && // 很重要!
396
+ lowerKey !== 'content-length') { // 很重要!
397
+ res.setHeader(key, value); // 设置其他原始头
398
+ }
399
+ });
400
+ // 设置我们自己的缓存策略
401
+ res.setHeader('Cache-Control', `public, max-age=${CACHE_TTL}`);
402
+
403
+ // 发送原始(已解压)内容
404
+ res.status(200).send(content);
405
+ }
406
+
407
+ // ---- 结束主处理逻辑的 try 块 ----
408
+ } catch (error) { // ---- 捕获处理过程中的任何错误 ----
409
+ // **检查这个错误是否是 "Assignment to constant variable"**
410
+ console.error(`[代理错误处理 V3] 捕获错误!目标: ${targetUrl || '解析失败'} | 错误类型: ${error.constructor.name} | 错误消息: ${error.message}`);
411
+ console.error(`[代理错误堆栈 V3] ${error.stack}`); // 记录完整的错误堆栈信息
412
+
413
+ // 特别标记 "Assignment to constant variable" 错误
414
+ if (error instanceof TypeError && error.message.includes("Assignment to constant variable")) {
415
+ console.error("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
416
+ console.error("捕获到 'Assignment to constant variable' 错误!");
417
+ console.error("请再次检查函数代码及所有辅助函数中,是否有 const 声明的变量被重新赋值。");
418
+ console.error("错误堆栈指向:", error.stack);
419
+ console.error("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
420
+ }
421
+
422
+ // 尝试从错误对象获取状态码,否则默认为 500
423
+ const statusCode = error.status || 500;
424
+
425
+ // 确保在发送错误响应前没有发送过响应头
426
+ if (!res.headersSent) {
427
+ res.setHeader('Content-Type', 'application/json');
428
+ // CORS 头应该已经在前面设置好了
429
+ res.status(statusCode).json({
430
+ success: false,
431
+ error: `代理处理错误: ${error.message}`, // 返回错误消息给前端
432
+ targetUrl: targetUrl // 包含目标 URL 以便调试
433
+ });
434
+ } else {
435
+ // 如果响应头已发送,无法��发送 JSON 错误
436
+ console.error("[代理错误处理 V3] 响应头已发送,无法发送 JSON 错误响应。");
437
+ // 尝试结束响应
438
+ if (!res.writableEnded) {
439
+ res.end();
440
+ }
441
+ }
442
+ } finally {
443
+ // 记录请求处理结束
444
+ console.info('--- Vercel 代理请求结束 ---');
445
+ }
446
+ }
447
+
448
+ // --- [确保所有辅助函数定义都在这里] ---
449
+ // getTargetUrlFromPath, getBaseUrl, resolveUrl, rewriteUrlToProxy, getRandomUserAgent,
450
+ // fetchContentWithType, isM3u8Content, processKeyLine, processMapLine,
451
+ // processMediaPlaylist, processM3u8Content, processMasterPlaylist