Spaces:
Sleeping
Sleeping
| const https = require('https'); | |
| const http = require('http'); | |
| const log = require('./logger'); | |
| const USER_AGENTS = [ | |
| 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', | |
| 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36', | |
| 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', | |
| 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.1 Safari/605.1.15', | |
| 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:132.0) Gecko/20100101 Firefox/132.0', | |
| 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:132.0) Gecko/20100101 Firefox/132.0', | |
| 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', | |
| 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0' | |
| ]; | |
| const ACCEPT_LANGUAGES = [ | |
| 'en-US,en;q=0.9', | |
| 'en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7', | |
| 'zh-CN,zh;q=0.9,en;q=0.8', | |
| 'en-GB,en;q=0.9,en-US;q=0.8', | |
| 'zh-TW,zh;q=0.9,en-US;q=0.8,en;q=0.7' | |
| ]; | |
| function getRandomUserAgent() { | |
| return USER_AGENTS[Math.floor(Math.random() * USER_AGENTS.length)]; | |
| } | |
| function getRandomAcceptLanguage() { | |
| return ACCEPT_LANGUAGES[Math.floor(Math.random() * ACCEPT_LANGUAGES.length)]; | |
| } | |
| function buildDisguiseHeaders() { | |
| return { | |
| 'User-Agent': getRandomUserAgent(), | |
| 'Accept': 'application/json, text/event-stream, */*', | |
| 'Accept-Language': getRandomAcceptLanguage(), | |
| 'Accept-Encoding': 'gzip, deflate, br', | |
| 'Cache-Control': 'no-cache', | |
| 'Pragma': 'no-cache', | |
| 'Sec-Ch-Ua': '"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"', | |
| 'Sec-Ch-Ua-Mobile': '?0', | |
| 'Sec-Ch-Ua-Platform': '"Windows"', | |
| 'Sec-Fetch-Dest': 'empty', | |
| 'Sec-Fetch-Mode': 'cors', | |
| 'Sec-Fetch-Site': 'cross-site', | |
| 'Connection': 'keep-alive' | |
| }; | |
| } | |
| function buildUrl(baseUrl, endpoint) { | |
| let url = baseUrl.trim().replace(/\/ $/, ''); | |
| if (!url) throw new Error('Base URL 不能为空'); | |
| if (!url.endsWith('/v1')) url += '/v1'; | |
| return url + endpoint; | |
| } | |
| function randHex(n) { | |
| const chars = '0123456789abcdef'; | |
| let result = ''; | |
| for (let i = 0; i < n; i++) { | |
| result += chars[Math.floor(Math.random() * 16)]; | |
| } | |
| return result; | |
| } | |
| function randText(n) { | |
| const len = Math.max(0, Math.min(4000, Math.floor(Number(n) || 0))); | |
| if (len <= 0) return ''; | |
| return randHex(Math.ceil(len / 2)).slice(0, len); | |
| } | |
| function buildUserPrompt(text, useRand, markerLen) { | |
| if (!useRand) return { prompt: text, tag: null, marker: null }; | |
| const tag = 'r' + randText(markerLen); | |
| const marker = '[RANDOM_MARKER:' + tag + ']'; | |
| return { prompt: marker + text, tag, marker }; | |
| } | |
| function estimateTokens(text) { | |
| return Math.max(1, Math.ceil(String(text || '').length / 4)); | |
| } | |
| function usageWithFallback(usage, reqPrompt, sysPrompt, respText) { | |
| let p = Number(usage?.prompt_tokens || 0); | |
| let c = Number(usage?.completion_tokens || 0); | |
| let t = Number(usage?.total_tokens || 0); | |
| let estimated = false; | |
| if (p <= 0 && c <= 0 && t <= 0) { | |
| p = estimateTokens((sysPrompt ? sysPrompt + '\n' : '') + (reqPrompt || '')); | |
| c = estimateTokens(respText || ''); | |
| t = p + c; | |
| estimated = true; | |
| } else { | |
| if (p <= 0) { p = estimateTokens((sysPrompt ? sysPrompt + '\n' : '') + (reqPrompt || '')); estimated = true; } | |
| if (c <= 0) { c = estimateTokens(respText || ''); estimated = true; } | |
| if (t <= 0) { t = p + c; estimated = true; } | |
| } | |
| return { prompt_tokens: p, completion_tokens: c, total_tokens: t, estimated }; | |
| } | |
| function makeRequest(url, options, body, timeout = 60000) { | |
| return new Promise((resolve, reject) => { | |
| const urlObj = new URL(url); | |
| const isHttps = urlObj.protocol === 'https:'; | |
| const lib = isHttps ? https : http; | |
| const disguiseHeaders = buildDisguiseHeaders(); | |
| const reqOptions = { | |
| hostname: urlObj.hostname, | |
| port: urlObj.port || (isHttps ? 443 : 80), | |
| path: urlObj.pathname + urlObj.search, | |
| method: options.method || 'GET', | |
| headers: { ...disguiseHeaders, ...options.headers }, | |
| timeout | |
| }; | |
| const req = lib.request(reqOptions, (res) => { | |
| let data = ''; | |
| res.on('data', chunk => data += chunk); | |
| res.on('end', () => { | |
| resolve({ status: res.statusCode, headers: res.headers, body: data }); | |
| }); | |
| }); | |
| req.on('error', reject); | |
| req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); }); | |
| if (body) req.write(body); | |
| req.end(); | |
| }); | |
| } | |
| async function fetchModels(baseUrl, token) { | |
| try { | |
| log.debug('Fetching models from:', baseUrl); | |
| const url = buildUrl(baseUrl, '/models'); | |
| const resp = await makeRequest(url, { | |
| method: 'GET', | |
| headers: { 'Authorization': 'Bearer ' + token } | |
| }); | |
| const data = JSON.parse(resp.body); | |
| if (resp.status !== 200) { | |
| throw new Error(data?.error?.message || 'HTTP ' + resp.status); | |
| } | |
| const ids = (Array.isArray(data?.data) ? data.data : []) | |
| .map(x => String(x.id || '').trim()) | |
| .filter(Boolean) | |
| .sort(); | |
| log.debug('Fetched', ids.length, 'models'); | |
| return ids; | |
| } catch (e) { | |
| log.error('Failed to fetch models:', e.message); | |
| throw new Error('加载模型失败: ' + e.message); | |
| } | |
| } | |
| async function requestOnce(config, signal) { | |
| const { prompt, tag, marker } = buildUserPrompt(config.usr, config.randOn, config.markerLen); | |
| const body = { | |
| model: config.model, | |
| messages: [ | |
| ...(config.sys ? [{ role: 'system', content: config.sys }] : []), | |
| { role: 'user', content: prompt } | |
| ], | |
| max_tokens: config.max, | |
| temperature: config.temp, | |
| stream: false | |
| }; | |
| const url = buildUrl(config.base, '/chat/completions'); | |
| const resp = await makeRequest(url, { | |
| method: 'POST', | |
| headers: { | |
| 'Authorization': 'Bearer ' + config.token, | |
| 'Content-Type': 'application/json' | |
| } | |
| }, JSON.stringify(body), config.timeout); | |
| if (signal?.aborted) throw new Error('Aborted'); | |
| // Check HTTP status first | |
| if (resp.status !== 200) { | |
| let errorMsg = 'HTTP ' + resp.status; | |
| try { | |
| const errorData = JSON.parse(resp.body); | |
| errorMsg = errorData?.error?.message || errorMsg; | |
| } catch (e) { | |
| // Response body is not valid JSON, use raw body | |
| if (resp.body) errorMsg += ': ' + resp.body.slice(0, 200); | |
| } | |
| throw new Error(errorMsg); | |
| } | |
| // Parse successful response | |
| let data; | |
| try { | |
| data = JSON.parse(resp.body); | |
| } catch (e) { | |
| throw new Error('响应解析失败: ' + resp.body.slice(0, 200)); | |
| } | |
| const content = data?.choices?.[0]?.message?.content || ''; | |
| const usage = usageWithFallback(data?.usage, prompt, config.sys, content); | |
| return { content, usage, finish_reason: data?.choices?.[0]?.finish_reason, tag, marker, prompt }; | |
| } | |
| async function streamRequest(config, onChunk, signal) { | |
| const { prompt, tag, marker } = buildUserPrompt(config.usr, config.randOn, config.markerLen); | |
| const body = { | |
| model: config.model, | |
| messages: [ | |
| ...(config.sys ? [{ role: 'system', content: config.sys }] : []), | |
| { role: 'user', content: prompt } | |
| ], | |
| max_tokens: config.max, | |
| temperature: config.temp, | |
| stream: true, | |
| stream_options: { include_usage: true } | |
| }; | |
| const url = buildUrl(config.base, '/chat/completions'); | |
| const urlObj = new URL(url); | |
| const isHttps = urlObj.protocol === 'https:'; | |
| const lib = isHttps ? https : http; | |
| return new Promise((resolve, reject) => { | |
| if (signal?.aborted) { | |
| reject(new Error('Aborted')); | |
| return; | |
| } | |
| const disguiseHeaders = buildDisguiseHeaders(); | |
| const reqOptions = { | |
| hostname: urlObj.hostname, | |
| port: urlObj.port || (isHttps ? 443 : 80), | |
| path: urlObj.pathname, | |
| method: 'POST', | |
| headers: { | |
| ...disguiseHeaders, | |
| 'Authorization': 'Bearer ' + config.token, | |
| 'Content-Type': 'application/json' | |
| }, | |
| timeout: config.timeout || 60000 | |
| }; | |
| let fullContent = ''; | |
| let usage = { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }; | |
| let finishReason = null; | |
| let aborted = false; | |
| let abortHandler = null; // Define early for cleanup | |
| const cleanup = () => { | |
| if (abortHandler && signal) { | |
| signal.removeEventListener('abort', abortHandler); | |
| } | |
| }; | |
| const req = lib.request(reqOptions, (res) => { | |
| let buffer = ''; | |
| // Check HTTP status code for non-streaming errors | |
| if (res.statusCode !== 200) { | |
| res.on('data', chunk => buffer += chunk.toString()); | |
| res.on('end', () => { | |
| cleanup(); | |
| try { | |
| const data = JSON.parse(buffer); | |
| reject(new Error(data?.error?.message || 'HTTP ' + res.statusCode)); | |
| } catch (e) { | |
| reject(new Error('HTTP ' + res.statusCode + ': ' + buffer.slice(0, 200))); | |
| } | |
| }); | |
| return; | |
| } | |
| res.on('data', chunk => { | |
| if (aborted) { req.destroy(); return; } | |
| buffer += chunk.toString(); | |
| const lines = buffer.split('\n'); | |
| buffer = lines.pop() || ''; | |
| for (const line of lines) { | |
| const trimmed = line.trim(); | |
| if (!trimmed || !trimmed.startsWith('data:')) continue; | |
| const payload = trimmed.slice(5).trim(); | |
| if (payload === '[DONE]') continue; | |
| try { | |
| const json = JSON.parse(payload); | |
| if (json.error) throw new Error(json.error.message); | |
| const delta = json.choices?.[0]?.delta?.content; | |
| if (delta) { | |
| fullContent += delta; | |
| if (onChunk) onChunk(delta, fullContent); | |
| } | |
| if (json.usage) usage = json.usage; | |
| if (json.choices?.[0]?.finish_reason) { | |
| finishReason = json.choices[0].finish_reason; | |
| } | |
| } catch (e) { | |
| if (e.message && !e.message.includes('JSON')) { | |
| req.destroy(); | |
| cleanup(); | |
| reject(e); | |
| return; | |
| } | |
| } | |
| } | |
| }); | |
| res.on('end', () => { | |
| cleanup(); | |
| const finalUsage = usageWithFallback(usage, prompt, config.sys, fullContent); | |
| resolve({ | |
| content: fullContent, | |
| usage: finalUsage, | |
| finish_reason: finishReason, | |
| tag, | |
| marker, | |
| prompt | |
| }); | |
| }); | |
| }); | |
| req.on('error', (err) => { | |
| cleanup(); | |
| if (aborted) reject(new Error('Aborted')); | |
| else reject(err); | |
| }); | |
| req.on('timeout', () => { | |
| cleanup(); | |
| aborted = true; | |
| req.destroy(); | |
| reject(new Error('请求超时')); | |
| }); | |
| req.write(JSON.stringify(body)); | |
| req.end(); | |
| // Set up abort handler | |
| if (signal) { | |
| abortHandler = () => { | |
| aborted = true; | |
| req.destroy(); | |
| reject(new Error('Aborted')); | |
| }; | |
| signal.addEventListener('abort', abortHandler); | |
| } | |
| }); | |
| } | |
| class TaskExecutor { | |
| constructor(taskId, config, onUpdate, savedProgress = null) { | |
| this.taskId = taskId; | |
| this.config = config; | |
| this.onUpdate = onUpdate; | |
| this.running = false; | |
| this.stopped = false; | |
| this.paused = false; | |
| this.controller = new AbortController(); | |
| // Update throttling - limit notification frequency | |
| this.lastNotifyTime = 0; | |
| this.notifyTimer = null; | |
| this.notifyInterval = 500; // ms between notifications (increased for high concurrency) | |
| // Load from saved progress or initialize fresh | |
| if (savedProgress) { | |
| this.stats = savedProgress.stats || { | |
| total: config.loop * config.threads, | |
| completed: 0, | |
| success: 0, | |
| failed: 0, | |
| aborted: 0, | |
| promptTokens: 0, | |
| completionTokens: 0, | |
| totalTokens: 0 | |
| }; | |
| this.threadProgress = savedProgress.threadProgress || {}; | |
| this.markerLen = savedProgress.markerLen || config.markerLen || 24; | |
| this.ratio = savedProgress.ratio || { live: 0, target: config.ratioTarget || 1, markerLen: this.markerLen }; | |
| this.elapsedBefore = savedProgress.elapsedBefore || 0; | |
| // Resume from paused state if saved | |
| if (savedProgress.paused) { | |
| this.paused = true; | |
| log.info('Resuming from paused state:', taskId); | |
| } | |
| } else { | |
| this.stats = { | |
| total: config.loop * config.threads, | |
| completed: 0, | |
| success: 0, | |
| failed: 0, | |
| aborted: 0, | |
| promptTokens: 0, | |
| completionTokens: 0, | |
| totalTokens: 0 | |
| }; | |
| this.threadProgress = {}; | |
| this.markerLen = config.markerLen || 24; | |
| this.ratio = { live: 0, target: config.ratioTarget || 1, markerLen: this.markerLen }; | |
| this.elapsedBefore = 0; | |
| } | |
| this.startTime = null; | |
| this.threadLogs = {}; | |
| log.debug('TaskExecutor created:', taskId, 'threads:', config.threads, 'loop:', config.loop, 'resumed:', !!savedProgress); | |
| } | |
| async runWorker(threadId) { | |
| const startLoop = (this.threadProgress[threadId] || 0) + 1; | |
| for (let i = startLoop; i <= this.config.loop; i++) { | |
| // Check for pause | |
| while (this.paused && !this.stopped) { | |
| await new Promise(resolve => setTimeout(resolve, 100)); | |
| } | |
| if (this.stopped) { | |
| log.debug('Thread', threadId, 'stopped at loop', i); | |
| break; | |
| } | |
| const threadLog = { | |
| thread: threadId, | |
| loop: i, | |
| status: 'running', | |
| startTime: Date.now(), | |
| message: '' | |
| }; | |
| this.threadLogs[threadId] = threadLog; | |
| this.threadProgress[threadId] = i; | |
| this.notifyUpdate(); | |
| try { | |
| const result = this.config.streamOn | |
| ? await streamRequest( | |
| { ...this.config, markerLen: this.markerLen }, | |
| (delta, full) => { | |
| threadLog.status = 'streaming'; | |
| threadLog.content = full; | |
| // notifyUpdate already throttled, so this is fine | |
| this.notifyUpdate(); | |
| }, | |
| this.controller.signal | |
| ) | |
| : await requestOnce( | |
| { ...this.config, markerLen: this.markerLen }, | |
| this.controller.signal | |
| ); | |
| this.stats.completed++; | |
| this.stats.success++; | |
| this.stats.promptTokens += result.usage.prompt_tokens; | |
| this.stats.completionTokens += result.usage.completion_tokens; | |
| this.stats.totalTokens += result.usage.total_tokens; | |
| log.debug('Thread', threadId, 'loop', i, 'success, tokens:', result.usage.total_tokens); | |
| // Check token limit | |
| if (this.stats.totalTokens >= this.config.maxTokens) { | |
| this.stopped = true; | |
| threadLog.status = 'limit_reached'; | |
| threadLog.message = '达到 tokens 上限'; | |
| log.info('Task', this.taskId, 'reached token limit:', this.stats.totalTokens); | |
| } else { | |
| threadLog.status = 'success'; | |
| threadLog.content = result.content?.slice(-500); | |
| } | |
| // Adjust marker for ratio | |
| const liveRatio = this.stats.completionTokens > 0 | |
| ? this.stats.promptTokens / this.stats.completionTokens | |
| : 0; | |
| this.ratio.live = liveRatio; | |
| if (this.config.randOn) { | |
| const diff = liveRatio - this.ratio.target; | |
| if (Math.abs(diff) >= 0.02) { | |
| const step = Math.max(1, Math.round(Math.abs(diff) * 50)); | |
| if (diff < 0) this.markerLen = Math.min(4000, this.markerLen + step); | |
| else this.markerLen = Math.max(0, this.markerLen - step); | |
| } | |
| this.ratio.markerLen = this.markerLen; | |
| } | |
| } catch (e) { | |
| const isAborted = e.message === 'Aborted' || e.name === 'AbortError'; | |
| if (isAborted) { | |
| if (this.paused) { | |
| threadLog.status = 'paused'; | |
| threadLog.message = '已暂停,等待继续...'; | |
| log.debug('Thread', threadId, 'loop', i, 'paused'); | |
| this.notifyUpdate(true); | |
| while (this.paused && !this.stopped) { | |
| await new Promise(resolve => setTimeout(resolve, 100)); | |
| } | |
| if (!this.stopped) { | |
| i--; | |
| threadLog.status = 'running'; | |
| continue; | |
| } | |
| } | |
| if (this.stopped) { | |
| this.stats.completed++; | |
| this.stats.aborted++; | |
| threadLog.status = 'aborted'; | |
| log.debug('Thread', threadId, 'loop', i, 'aborted'); | |
| } | |
| } else { | |
| this.stats.failed++; | |
| threadLog.status = 'error'; | |
| threadLog.error = e.message || '请求失败'; | |
| log.error('Thread', threadId, 'loop', i, 'error:', e.message); | |
| } | |
| } | |
| threadLog.endTime = Date.now(); | |
| this.notifyUpdate(true); // Force update after each request completes | |
| // Wait between loops (not after the last loop) | |
| if (i < this.config.loop && this.config.waitBetween > 0 && !this.stopped && !this.paused) { | |
| await new Promise(resolve => setTimeout(resolve, this.config.waitBetween)); | |
| } | |
| } | |
| // Clear progress when thread completes all loops | |
| if (!this.stopped && !this.paused) { | |
| delete this.threadProgress[threadId]; | |
| } | |
| } | |
| async start() { | |
| log.info('TaskExecutor starting:', this.taskId, 'threads:', this.config.threads, 'loops:', this.config.loop, 'paused:', this.paused); | |
| this.running = true; | |
| this.stopped = false; | |
| // Always ensure controller exists | |
| if (!this.controller) { | |
| this.controller = new AbortController(); | |
| } | |
| // Only reset startTime if not paused (will be set by resume()) | |
| if (!this.paused) { | |
| this.startTime = Date.now(); | |
| // Initialize thread logs for fresh start | |
| for (let i = 1; i <= this.config.threads; i++) { | |
| this.threadLogs[i] = { thread: i, status: 'waiting', message: '等待开始...' }; | |
| } | |
| } else { | |
| // Resuming from paused state - restore thread logs | |
| for (let i = 1; i <= this.config.threads; i++) { | |
| if (!this.threadLogs[i]) { | |
| this.threadLogs[i] = { thread: i, status: 'paused', message: '等待继续...' }; | |
| } else { | |
| this.threadLogs[i].status = 'paused'; | |
| this.threadLogs[i].message = '等待继续...'; | |
| } | |
| } | |
| } | |
| this.notifyUpdate(); | |
| // Run workers in parallel | |
| const workers = []; | |
| for (let i = 1; i <= this.config.threads; i++) { | |
| workers.push(this.runWorker(i)); | |
| } | |
| await Promise.all(workers); | |
| this.running = false; | |
| log.info('TaskExecutor finished:', this.taskId, 'success:', this.stats.success, 'failed:', this.stats.failed, 'tokens:', this.stats.totalTokens); | |
| this.notifyUpdate(true); // Force final update | |
| // Clean up callback reference to allow garbage collection | |
| this.onUpdate = null; | |
| } | |
| pause() { | |
| if (!this.running || this.paused) return; | |
| log.info('TaskExecutor pausing:', this.taskId); | |
| this.paused = true; | |
| if (this.startTime) { | |
| this.elapsedBefore += Date.now() - this.startTime; | |
| this.startTime = null; | |
| } | |
| // Abort current request to pause immediately | |
| this.controller.abort(); | |
| this.notifyUpdate(); | |
| } | |
| resume() { | |
| if (!this.running || !this.paused) return; | |
| log.info('TaskExecutor resuming:', this.taskId); | |
| this.paused = false; | |
| this.controller = new AbortController(); | |
| this.startTime = Date.now(); | |
| this.notifyUpdate(); | |
| } | |
| stop() { | |
| log.info('TaskExecutor stopping:', this.taskId); | |
| this.stopped = true; | |
| this.paused = false; | |
| this.controller.abort(); | |
| } | |
| getStatus() { | |
| let elapsed = this.elapsedBefore; | |
| if (this.startTime && !this.paused) { | |
| elapsed += Date.now() - this.startTime; | |
| } | |
| return { | |
| taskId: this.taskId, | |
| running: this.running, | |
| stopped: this.stopped, | |
| paused: this.paused, | |
| stats: this.stats, | |
| ratio: this.ratio, | |
| elapsed: elapsed, | |
| elapsedBefore: this.elapsedBefore, | |
| threadLogs: { ...this.threadLogs }, | |
| threadProgress: { ...this.threadProgress }, | |
| markerLen: this.markerLen | |
| }; | |
| } | |
| getProgress() { | |
| let elapsed = this.elapsedBefore; | |
| if (this.startTime && !this.paused) { | |
| elapsed += Date.now() - this.startTime; | |
| } | |
| return { | |
| stats: this.stats, | |
| threadProgress: this.threadProgress, | |
| ratio: this.ratio, | |
| markerLen: this.markerLen, | |
| elapsedBefore: elapsed | |
| }; | |
| } | |
| notifyUpdate(force = false) { | |
| // Throttle updates to reduce API load | |
| // force = true means immediate update (for errors, completion, etc.) | |
| const now = Date.now(); | |
| const timeSinceLastNotify = now - this.lastNotifyTime; | |
| if (force || timeSinceLastNotify >= this.notifyInterval) { | |
| // Enough time passed or forced update, notify immediately | |
| if (this.notifyTimer) { | |
| clearTimeout(this.notifyTimer); | |
| this.notifyTimer = null; | |
| } | |
| this.lastNotifyTime = now; | |
| if (this.onUpdate) { | |
| this.onUpdate(this.getStatus()); | |
| } | |
| } else if (!this.notifyTimer) { | |
| // Schedule a delayed notification - only one timer at a time | |
| this.notifyTimer = setTimeout(() => { | |
| this.notifyTimer = null; | |
| this.lastNotifyTime = Date.now(); | |
| if (this.onUpdate) { | |
| this.onUpdate(this.getStatus()); | |
| } | |
| }, this.notifyInterval - timeSinceLastNotify); | |
| } | |
| // If timer already exists, the scheduled notification will include latest state | |
| } | |
| } | |
| class HttpTaskExecutor { | |
| constructor(taskId, config, onUpdate, savedProgress = null) { | |
| this.taskId = taskId; | |
| this.config = config; | |
| this.onUpdate = onUpdate; | |
| this.running = false; | |
| this.stopped = false; | |
| this.paused = false; | |
| this.controller = new AbortController(); | |
| this.lastNotifyTime = 0; | |
| this.notifyTimer = null; | |
| this.notifyInterval = 500; | |
| if (savedProgress) { | |
| this.stats = savedProgress.stats || { | |
| total: config.loop * config.threads, | |
| completed: 0, | |
| success: 0, | |
| failed: 0, | |
| aborted: 0, | |
| totalRequests: 0 | |
| }; | |
| this.threadProgress = savedProgress.threadProgress || {}; | |
| this.elapsedBefore = savedProgress.elapsedBefore || 0; | |
| if (savedProgress.paused) { | |
| this.paused = true; | |
| log.info('Resuming HTTP task from paused state:', taskId); | |
| } | |
| } else { | |
| this.stats = { | |
| total: config.loop * config.threads, | |
| completed: 0, | |
| success: 0, | |
| failed: 0, | |
| aborted: 0, | |
| totalRequests: 0 | |
| }; | |
| this.threadProgress = {}; | |
| this.elapsedBefore = 0; | |
| } | |
| this.startTime = null; | |
| this.threadLogs = {}; | |
| log.debug('HttpTaskExecutor created:', taskId, 'threads:', config.threads, 'loop:', config.loop); | |
| } | |
| async executeHttpRequest(threadId, loopNum) { | |
| const { url, method, headers: headersStr, body: bodyStr, timeout } = this.config; | |
| let headers = {}; | |
| try { | |
| if (headersStr) { | |
| headers = JSON.parse(headersStr); | |
| } | |
| } catch (e) { | |
| throw new Error('Headers JSON格式错误: ' + e.message); | |
| } | |
| let body = null; | |
| if (method.toUpperCase() !== 'GET' && bodyStr) { | |
| body = bodyStr; | |
| if (!headers['Content-Type']) { | |
| try { | |
| JSON.parse(bodyStr); | |
| headers['Content-Type'] = 'application/json'; | |
| } catch { | |
| headers['Content-Type'] = 'application/x-www-form-urlencoded'; | |
| } | |
| } | |
| } | |
| const urlObj = new URL(url); | |
| const isHttps = urlObj.protocol === 'https:'; | |
| const lib = isHttps ? https : http; | |
| return new Promise((resolve, reject) => { | |
| const reqOptions = { | |
| hostname: urlObj.hostname, | |
| port: urlObj.port || (isHttps ? 443 : 80), | |
| path: urlObj.pathname + urlObj.search, | |
| method: method.toUpperCase(), | |
| headers: { ...headers }, | |
| timeout: timeout || 60000 | |
| }; | |
| const req = lib.request(reqOptions, (res) => { | |
| let data = ''; | |
| res.on('data', chunk => data += chunk); | |
| res.on('end', () => { | |
| resolve({ | |
| statusCode: res.statusCode, | |
| headers: res.headers, | |
| body: data, | |
| success: res.statusCode >= 200 && res.statusCode < 300 | |
| }); | |
| }); | |
| }); | |
| req.on('error', err => reject(err)); | |
| req.on('timeout', () => { | |
| req.destroy(); | |
| reject(new Error('请求超时')); | |
| }); | |
| if (body) req.write(body); | |
| req.end(); | |
| }); | |
| } | |
| async runWorker(threadId) { | |
| const startLoop = (this.threadProgress[threadId] || 0) + 1; | |
| for (let i = startLoop; i <= this.config.loop; i++) { | |
| while (this.paused && !this.stopped) { | |
| await new Promise(resolve => setTimeout(resolve, 100)); | |
| } | |
| if (this.stopped) { | |
| break; | |
| } | |
| const threadLog = { | |
| thread: threadId, | |
| loop: i, | |
| status: 'running', | |
| startTime: Date.now(), | |
| message: '', | |
| responseData: null, | |
| errorCode: null, | |
| errorMessage: null | |
| }; | |
| this.threadLogs[threadId] = threadLog; | |
| this.threadProgress[threadId] = i; | |
| this.notifyUpdate(); | |
| try { | |
| const result = await this.executeHttpRequest(threadId, i); | |
| this.stats.completed++; | |
| this.stats.totalRequests++; | |
| if (result.success) { | |
| this.stats.success++; | |
| threadLog.status = 'success'; | |
| threadLog.responseData = result.body; | |
| threadLog.message = `HTTP ${result.statusCode}`; | |
| } else { | |
| this.stats.failed++; | |
| threadLog.status = 'error'; | |
| threadLog.errorCode = result.statusCode; | |
| threadLog.errorMessage = result.body.slice(0, 500); | |
| threadLog.message = `HTTP ${result.statusCode}`; | |
| } | |
| } catch (e) { | |
| this.stats.completed++; | |
| this.stats.failed++; | |
| threadLog.status = 'error'; | |
| threadLog.errorCode = 'ERR'; | |
| threadLog.errorMessage = e.message; | |
| threadLog.message = '请求失败: ' + e.message; | |
| log.error('Thread', threadId, 'loop', i, 'error:', e.message); | |
| } | |
| threadLog.endTime = Date.now(); | |
| this.notifyUpdate(true); | |
| if (i < this.config.loop && this.config.waitBetween > 0 && !this.stopped && !this.paused) { | |
| await new Promise(resolve => setTimeout(resolve, this.config.waitBetween)); | |
| } | |
| } | |
| if (!this.stopped && !this.paused) { | |
| delete this.threadProgress[threadId]; | |
| } | |
| } | |
| async start() { | |
| log.info('HttpTaskExecutor starting:', this.taskId); | |
| this.running = true; | |
| this.stopped = false; | |
| if (!this.controller) { | |
| this.controller = new AbortController(); | |
| } | |
| if (!this.paused) { | |
| this.startTime = Date.now(); | |
| for (let i = 1; i <= this.config.threads; i++) { | |
| this.threadLogs[i] = { thread: i, status: 'waiting', message: '等待开始...' }; | |
| } | |
| } else { | |
| for (let i = 1; i <= this.config.threads; i++) { | |
| if (!this.threadLogs[i]) { | |
| this.threadLogs[i] = { thread: i, status: 'paused', message: '等待继续...' }; | |
| } else { | |
| this.threadLogs[i].status = 'paused'; | |
| this.threadLogs[i].message = '等待继续...'; | |
| } | |
| } | |
| } | |
| this.notifyUpdate(); | |
| const workers = []; | |
| for (let i = 1; i <= this.config.threads; i++) { | |
| workers.push(this.runWorker(i)); | |
| } | |
| await Promise.all(workers); | |
| this.running = false; | |
| log.info('HttpTaskExecutor finished:', this.taskId, 'success:', this.stats.success, 'failed:', this.stats.failed); | |
| this.notifyUpdate(true); | |
| this.onUpdate = null; | |
| } | |
| pause() { | |
| if (!this.running || this.paused) return; | |
| log.info('HttpTaskExecutor pausing:', this.taskId); | |
| this.paused = true; | |
| if (this.startTime) { | |
| this.elapsedBefore += Date.now() - this.startTime; | |
| this.startTime = null; | |
| } | |
| this.controller.abort(); | |
| this.notifyUpdate(); | |
| } | |
| resume() { | |
| if (!this.running || !this.paused) return; | |
| log.info('HttpTaskExecutor resuming:', this.taskId); | |
| this.paused = false; | |
| this.controller = new AbortController(); | |
| this.startTime = Date.now(); | |
| this.notifyUpdate(); | |
| } | |
| stop() { | |
| log.info('HttpTaskExecutor stopping:', this.taskId); | |
| this.stopped = true; | |
| this.paused = false; | |
| this.controller.abort(); | |
| } | |
| getStatus() { | |
| let elapsed = this.elapsedBefore; | |
| if (this.startTime && !this.paused) { | |
| elapsed += Date.now() - this.startTime; | |
| } | |
| return { | |
| taskId: this.taskId, | |
| running: this.running, | |
| stopped: this.stopped, | |
| paused: this.paused, | |
| stats: this.stats, | |
| elapsed: elapsed, | |
| elapsedBefore: this.elapsedBefore, | |
| threadLogs: { ...this.threadLogs }, | |
| threadProgress: { ...this.threadProgress } | |
| }; | |
| } | |
| getProgress() { | |
| let elapsed = this.elapsedBefore; | |
| if (this.startTime && !this.paused) { | |
| elapsed += Date.now() - this.startTime; | |
| } | |
| return { | |
| stats: this.stats, | |
| threadProgress: this.threadProgress, | |
| elapsedBefore: elapsed | |
| }; | |
| } | |
| notifyUpdate(force = false) { | |
| const now = Date.now(); | |
| const timeSinceLastNotify = now - this.lastNotifyTime; | |
| if (force || timeSinceLastNotify >= this.notifyInterval) { | |
| if (this.notifyTimer) { | |
| clearTimeout(this.notifyTimer); | |
| this.notifyTimer = null; | |
| } | |
| this.lastNotifyTime = now; | |
| if (this.onUpdate) { | |
| this.onUpdate(this.getStatus()); | |
| } | |
| } else if (!this.notifyTimer) { | |
| this.notifyTimer = setTimeout(() => { | |
| this.notifyTimer = null; | |
| this.lastNotifyTime = Date.now(); | |
| if (this.onUpdate) { | |
| this.onUpdate(this.getStatus()); | |
| } | |
| }, this.notifyInterval - timeSinceLastNotify); | |
| } | |
| } | |
| } | |
| module.exports = { TaskExecutor, HttpTaskExecutor, fetchModels }; | |