| |
| |
| |
|
|
|
|
| import https from 'https';
|
| import http from 'http';
|
| import { URL } from 'url';
|
| import config from './config.js';
|
|
|
| const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
|
|
| |
| |
|
|
| export async function request(url, options = {}) {
|
| const {
|
| method = 'GET',
|
| headers = {},
|
| body = null,
|
| timeout = config.timeout || 30000,
|
| followRedirect = true,
|
| maxRedirects = 5,
|
| cookies = null,
|
| } = options;
|
|
|
| const urlObj = new URL(url);
|
| const isHttps = urlObj.protocol === 'https:';
|
| const lib = isHttps ? https : http;
|
|
|
| const finalHeaders = {
|
| 'User-Agent': config.userAgent,
|
| 'Accept': 'application/json, text/html, */*',
|
| 'Accept-Language': 'en-US,en;q=0.9,ru;q=0.8,zh-CN;q=0.7',
|
| 'Accept-Encoding': 'identity',
|
| ...headers,
|
| };
|
|
|
|
|
| if (cookies && cookies.size > 0) {
|
| finalHeaders['Cookie'] = [...cookies.entries()].map(([k, v]) => `${k}=${v}`).join('; ');
|
| }
|
|
|
|
|
| let bodyStr = null;
|
| if (body) {
|
| bodyStr = typeof body === 'string' ? body : JSON.stringify(body);
|
| if (!finalHeaders['Content-Type']) {
|
| finalHeaders['Content-Type'] = 'application/json';
|
| }
|
| finalHeaders['Content-Length'] = Buffer.byteLength(bodyStr);
|
| }
|
|
|
| return new Promise((resolve, reject) => {
|
| const reqOptions = {
|
| hostname: urlObj.hostname,
|
| port: urlObj.port || (isHttps ? 443 : 80),
|
| path: urlObj.pathname + urlObj.search,
|
| method,
|
| headers: finalHeaders,
|
| timeout,
|
| };
|
|
|
| const req = lib.request(reqOptions, (res) => {
|
|
|
| const resCookies = new Map(cookies || []);
|
| const setCookieHeaders = res.headers['set-cookie'] || [];
|
| for (const sc of setCookieHeaders) {
|
| const [pair] = sc.split(';');
|
| const [name, ...valueParts] = pair.split('=');
|
| resCookies.set(name.trim(), valueParts.join('=').trim());
|
| }
|
|
|
|
|
| if (followRedirect && [301, 302, 303, 307, 308].includes(res.statusCode) && maxRedirects > 0) {
|
| const location = res.headers['location'];
|
| if (location) {
|
| const redirectUrl = location.startsWith('http') ? location : new URL(location, url).href;
|
| res.resume();
|
| resolve(request(redirectUrl, {
|
| ...options,
|
| maxRedirects: maxRedirects - 1,
|
| cookies: resCookies,
|
| method: [301, 302, 303].includes(res.statusCode) ? 'GET' : method,
|
| body: [301, 302, 303].includes(res.statusCode) ? null : body,
|
| }));
|
| return;
|
| }
|
| }
|
|
|
| let data = '';
|
| res.setEncoding('utf8');
|
| res.on('data', chunk => data += chunk);
|
| res.on('end', () => {
|
| resolve({
|
| ok: res.statusCode >= 200 && res.statusCode < 300,
|
| status: res.statusCode,
|
| statusText: res.statusMessage,
|
| headers: res.headers,
|
| cookies: resCookies,
|
| url,
|
| text: () => data,
|
| json: () => {
|
| try { return JSON.parse(data); }
|
| catch { throw new Error(`JSON parse error: ${data.substring(0, 200)}`); }
|
| },
|
| html: () => data,
|
| });
|
| });
|
| });
|
|
|
| req.on('error', reject);
|
| req.on('timeout', () => { req.destroy(); reject(new Error(`请求超时: ${url}`)); });
|
|
|
| if (bodyStr) req.write(bodyStr);
|
| req.end();
|
| });
|
| }
|
|
|
| |
| |
|
|
| export async function get(url, options = {}) {
|
| return request(url, { ...options, method: 'GET' });
|
| }
|
|
|
| |
| |
|
|
| export async function post(url, body, options = {}) {
|
| return request(url, { ...options, method: 'POST', body });
|
| }
|
|
|
| |
| |
|
|
| export async function retryRequest(url, options = {}, maxRetries = 3) {
|
| let lastError;
|
| for (let i = 0; i < maxRetries; i++) {
|
| try {
|
| const resp = await request(url, options);
|
| if (resp.ok || resp.status < 500) return resp;
|
| lastError = new Error(`HTTP ${resp.status}: ${resp.text().substring(0, 200)}`);
|
| } catch (e) {
|
| lastError = e;
|
| }
|
| if (i < maxRetries - 1) {
|
| const delay = 1000 * (i + 1) + Math.random() * 1000;
|
| console.log(` [HTTP] 重试 ${i + 1}/${maxRetries} (${Math.round(delay)}ms)...`);
|
| await sleep(delay);
|
| }
|
| }
|
| throw lastError;
|
| }
|
|
|
| export { sleep };
|
|
|
| |
| |
| |
|
|
| export async function requestStream(url, options = {}) {
|
| const {
|
| method = 'GET',
|
| headers = {},
|
| body = null,
|
| timeout = config.timeout || 120000,
|
| cookies = null,
|
| } = options;
|
|
|
| const urlObj = new URL(url);
|
| const isHttps = urlObj.protocol === 'https:';
|
| const lib = isHttps ? https : http;
|
|
|
| const finalHeaders = {
|
| 'User-Agent': config.userAgent,
|
| 'Accept': 'text/event-stream, application/json',
|
| 'Accept-Language': 'en',
|
| 'Accept-Encoding': 'identity',
|
| ...headers,
|
| };
|
|
|
| if (cookies && cookies.size > 0) {
|
| finalHeaders['Cookie'] = [...cookies.entries()].map(([k, v]) => `${k}=${v}`).join('; ');
|
| }
|
|
|
| let bodyStr = null;
|
| if (body) {
|
| bodyStr = typeof body === 'string' ? body : JSON.stringify(body);
|
| if (!finalHeaders['Content-Type']) {
|
| finalHeaders['Content-Type'] = 'application/json';
|
| }
|
| finalHeaders['Content-Length'] = Buffer.byteLength(bodyStr);
|
| }
|
|
|
| return new Promise((resolve, reject) => {
|
| const reqOptions = {
|
| hostname: urlObj.hostname,
|
| port: urlObj.port || (isHttps ? 443 : 80),
|
| path: urlObj.pathname + urlObj.search,
|
| method,
|
| headers: finalHeaders,
|
| timeout,
|
| };
|
|
|
| const req = lib.request(reqOptions, (res) => {
|
| const resCookies = new Map(cookies || []);
|
| const setCookieHeaders = res.headers['set-cookie'] || [];
|
| for (const sc of setCookieHeaders) {
|
| const [pair] = sc.split(';');
|
| const [name, ...valueParts] = pair.split('=');
|
| resCookies.set(name.trim(), valueParts.join('=').trim());
|
| }
|
|
|
| resolve({
|
| ok: res.statusCode >= 200 && res.statusCode < 300,
|
| status: res.statusCode,
|
| headers: res.headers,
|
| cookies: resCookies,
|
| stream: res,
|
| });
|
| });
|
|
|
| req.on('error', reject);
|
| req.on('timeout', () => { req.destroy(); reject(new Error(`流式请求超时: ${url}`)); });
|
|
|
| if (bodyStr) req.write(bodyStr);
|
| req.end();
|
| });
|
| }
|
|
|