token-consumer / executor.js
qwen2api's picture
Upload 11 files
8a81249 verified
Raw
History Blame Contribute Delete
30.3 kB
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 };