Spaces:
Paused
Paused
Upload 11 files
Browse files- executor.js +72 -233
- logger.js +1 -1
- package-lock.json +0 -49
- package.json +2 -5
- server.js +4 -38
- token-consumer.html +90 -40
executor.js
CHANGED
|
@@ -2,79 +2,49 @@ const https = require('https');
|
|
| 2 |
const http = require('http');
|
| 3 |
const log = require('./logger');
|
| 4 |
|
| 5 |
-
const
|
| 6 |
-
|
| 7 |
-
/
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
const
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
// 停滞自动暂停配置:连续多久没有成功就自动暂停(毫秒)
|
| 28 |
-
// 默认 1 小时 = 3600000ms,可通过环境变量 STALL_TIMEOUT_MS 配置
|
| 29 |
-
const STALL_TIMEOUT_MS = Number(process.env.STALL_TIMEOUT_MS) || 3600000;
|
| 30 |
-
|
| 31 |
-
// 判断是否是可重试的网络错误(TLS 握手失败、连接重置等)
|
| 32 |
-
function isRetryableError(err) {
|
| 33 |
-
if (!err) return false;
|
| 34 |
-
const msg = err.message || '';
|
| 35 |
-
const code = err.code || '';
|
| 36 |
-
|
| 37 |
-
// TLS 握手错误
|
| 38 |
-
if (msg.includes('TLS') || msg.includes('socket disconnected before secure')) return true;
|
| 39 |
-
|
| 40 |
-
// 连接相关错误
|
| 41 |
-
if (['ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'ENOTFOUND', 'EHOSTUNREACH'].includes(code)) return true;
|
| 42 |
-
if (msg.includes('ECONNRESET') || msg.includes('ETIMEDOUT') || msg.includes('socket hang up')) return true;
|
| 43 |
-
|
| 44 |
-
return false;
|
| 45 |
}
|
| 46 |
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
// Multiplier for marker length step calculation
|
| 51 |
-
const RATIO_STEP_MULTIPLIER = 100;
|
| 52 |
-
|
| 53 |
-
// Token rotation counter
|
| 54 |
-
const tokenCounters = new Map();
|
| 55 |
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
const token = tokens[index];
|
| 73 |
-
|
| 74 |
-
// Increment counter
|
| 75 |
-
tokenCounters.set(tokenKey, counter + 1);
|
| 76 |
-
|
| 77 |
-
return { token, index, total: tokens.length };
|
| 78 |
}
|
| 79 |
|
| 80 |
function buildUrl(baseUrl, endpoint) {
|
|
@@ -135,16 +105,14 @@ function makeRequest(url, options, body, timeout = 60000) {
|
|
| 135 |
const isHttps = urlObj.protocol === 'https:';
|
| 136 |
const lib = isHttps ? https : http;
|
| 137 |
|
| 138 |
-
const
|
| 139 |
-
|
| 140 |
const reqOptions = {
|
| 141 |
hostname: urlObj.hostname,
|
| 142 |
port: urlObj.port || (isHttps ? 443 : 80),
|
| 143 |
path: urlObj.pathname + urlObj.search,
|
| 144 |
method: options.method || 'GET',
|
| 145 |
-
headers:
|
| 146 |
-
timeout
|
| 147 |
-
agent: isHttps ? httpsAgent : httpAgent
|
| 148 |
};
|
| 149 |
|
| 150 |
const req = lib.request(reqOptions, (res) => {
|
|
@@ -167,14 +135,9 @@ async function fetchModels(baseUrl, token) {
|
|
| 167 |
try {
|
| 168 |
log.debug('Fetching models from:', baseUrl);
|
| 169 |
const url = buildUrl(baseUrl, '/models');
|
| 170 |
-
|
| 171 |
-
// Handle token rotation for model fetching
|
| 172 |
-
const tokens = String(token || '').split(',').map(t => t.trim()).filter(Boolean);
|
| 173 |
-
const useToken = tokens.length > 0 ? tokens[0] : token;
|
| 174 |
-
|
| 175 |
const resp = await makeRequest(url, {
|
| 176 |
method: 'GET',
|
| 177 |
-
headers: { 'Authorization': 'Bearer ' +
|
| 178 |
});
|
| 179 |
const data = JSON.parse(resp.body);
|
| 180 |
if (resp.status !== 200) {
|
|
@@ -192,18 +155,9 @@ async function fetchModels(baseUrl, token) {
|
|
| 192 |
}
|
| 193 |
}
|
| 194 |
|
| 195 |
-
async function requestOnce(config, signal
|
| 196 |
const { prompt, tag, marker } = buildUserPrompt(config.usr, config.randOn, config.markerLen);
|
| 197 |
|
| 198 |
-
// Get next token from rotation
|
| 199 |
-
const { token, index, total } = getNextToken(config);
|
| 200 |
-
|
| 201 |
-
// Record token index in thread log
|
| 202 |
-
if (threadLog) {
|
| 203 |
-
threadLog.tokenIndex = index;
|
| 204 |
-
threadLog.tokenTotal = total;
|
| 205 |
-
}
|
| 206 |
-
|
| 207 |
const body = {
|
| 208 |
model: config.model,
|
| 209 |
messages: [
|
|
@@ -219,7 +173,7 @@ async function requestOnce(config, signal, threadLog) {
|
|
| 219 |
const resp = await makeRequest(url, {
|
| 220 |
method: 'POST',
|
| 221 |
headers: {
|
| 222 |
-
'Authorization': 'Bearer ' + token,
|
| 223 |
'Content-Type': 'application/json'
|
| 224 |
}
|
| 225 |
}, JSON.stringify(body), config.timeout);
|
|
@@ -248,13 +202,12 @@ async function requestOnce(config, signal, threadLog) {
|
|
| 248 |
}
|
| 249 |
|
| 250 |
const content = data?.choices?.[0]?.message?.content || '';
|
| 251 |
-
const
|
| 252 |
-
const usage = usageWithFallback(data?.usage, prompt, config.sys, content + reasoning);
|
| 253 |
|
| 254 |
-
return { content,
|
| 255 |
}
|
| 256 |
|
| 257 |
-
async function streamRequest(config, onChunk, signal
|
| 258 |
const { prompt, tag, marker } = buildUserPrompt(config.usr, config.randOn, config.markerLen);
|
| 259 |
|
| 260 |
const body = {
|
|
@@ -274,38 +227,27 @@ async function streamRequest(config, onChunk, signal, threadLog) {
|
|
| 274 |
const isHttps = urlObj.protocol === 'https:';
|
| 275 |
const lib = isHttps ? https : http;
|
| 276 |
|
| 277 |
-
// Get next token from rotation
|
| 278 |
-
const { token, index, total } = getNextToken(config);
|
| 279 |
-
|
| 280 |
-
// Record token index in thread log
|
| 281 |
-
if (threadLog) {
|
| 282 |
-
threadLog.tokenIndex = index;
|
| 283 |
-
threadLog.tokenTotal = total;
|
| 284 |
-
}
|
| 285 |
-
|
| 286 |
return new Promise((resolve, reject) => {
|
| 287 |
-
// Check if already aborted before starting
|
| 288 |
if (signal?.aborted) {
|
| 289 |
reject(new Error('Aborted'));
|
| 290 |
return;
|
| 291 |
}
|
| 292 |
|
|
|
|
| 293 |
const reqOptions = {
|
| 294 |
hostname: urlObj.hostname,
|
| 295 |
port: urlObj.port || (isHttps ? 443 : 80),
|
| 296 |
path: urlObj.pathname,
|
| 297 |
method: 'POST',
|
| 298 |
headers: {
|
| 299 |
-
|
| 300 |
-
'Authorization': 'Bearer ' + token,
|
| 301 |
'Content-Type': 'application/json'
|
| 302 |
},
|
| 303 |
-
timeout: config.timeout || 60000
|
| 304 |
-
agent: isHttps ? httpsAgent : httpAgent
|
| 305 |
};
|
| 306 |
|
| 307 |
let fullContent = '';
|
| 308 |
-
let fullReasoning = '';
|
| 309 |
let usage = { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };
|
| 310 |
let finishReason = null;
|
| 311 |
let aborted = false;
|
|
@@ -353,23 +295,10 @@ async function streamRequest(config, onChunk, signal, threadLog) {
|
|
| 353 |
const json = JSON.parse(payload);
|
| 354 |
if (json.error) throw new Error(json.error.message);
|
| 355 |
|
| 356 |
-
const delta = json.choices?.[0]?.delta;
|
| 357 |
-
if (delta
|
| 358 |
-
fullContent += delta
|
| 359 |
-
|
| 360 |
-
if (delta?.reasoning_content) {
|
| 361 |
-
fullReasoning += delta.reasoning_content;
|
| 362 |
-
}
|
| 363 |
-
// Call onChunk with combined output
|
| 364 |
-
if (onChunk && (delta?.content || delta?.reasoning_content)) {
|
| 365 |
-
let fullOutput = '';
|
| 366 |
-
if (fullReasoning) {
|
| 367 |
-
fullOutput += '💭 ' + fullReasoning + '\n---\n';
|
| 368 |
-
}
|
| 369 |
-
if (fullContent) {
|
| 370 |
-
fullOutput += fullContent;
|
| 371 |
-
}
|
| 372 |
-
onChunk(delta?.content || delta?.reasoning_content, fullOutput);
|
| 373 |
}
|
| 374 |
|
| 375 |
if (json.usage) usage = json.usage;
|
|
@@ -389,10 +318,9 @@ async function streamRequest(config, onChunk, signal, threadLog) {
|
|
| 389 |
|
| 390 |
res.on('end', () => {
|
| 391 |
cleanup();
|
| 392 |
-
const finalUsage = usageWithFallback(usage, prompt, config.sys, fullContent
|
| 393 |
resolve({
|
| 394 |
content: fullContent,
|
| 395 |
-
reasoning: fullReasoning,
|
| 396 |
usage: finalUsage,
|
| 397 |
finish_reason: finishReason,
|
| 398 |
tag,
|
|
@@ -446,11 +374,6 @@ class TaskExecutor {
|
|
| 446 |
this.notifyTimer = null;
|
| 447 |
this.notifyInterval = 500; // ms between notifications (increased for high concurrency)
|
| 448 |
|
| 449 |
-
// 停滞自动暂停:跟踪最后一次成功时间
|
| 450 |
-
this.lastSuccessTime = Date.now();
|
| 451 |
-
this.stallCheckTimer = null;
|
| 452 |
-
this.stallCheckInterval = 60000; // 每分钟检查一次
|
| 453 |
-
|
| 454 |
// Load from saved progress or initialize fresh
|
| 455 |
if (savedProgress) {
|
| 456 |
this.stats = savedProgress.stats || {
|
|
@@ -513,8 +436,8 @@ class TaskExecutor {
|
|
| 513 |
thread: threadId,
|
| 514 |
loop: i,
|
| 515 |
status: 'running',
|
| 516 |
-
|
| 517 |
-
|
| 518 |
};
|
| 519 |
|
| 520 |
this.threadLogs[threadId] = threadLog;
|
|
@@ -525,24 +448,21 @@ class TaskExecutor {
|
|
| 525 |
const result = this.config.streamOn
|
| 526 |
? await streamRequest(
|
| 527 |
{ ...this.config, markerLen: this.markerLen },
|
| 528 |
-
(delta,
|
| 529 |
threadLog.status = 'streaming';
|
| 530 |
-
threadLog.content =
|
| 531 |
// notifyUpdate already throttled, so this is fine
|
| 532 |
this.notifyUpdate();
|
| 533 |
},
|
| 534 |
-
this.controller.signal
|
| 535 |
-
threadLog
|
| 536 |
)
|
| 537 |
: await requestOnce(
|
| 538 |
{ ...this.config, markerLen: this.markerLen },
|
| 539 |
-
this.controller.signal
|
| 540 |
-
threadLog
|
| 541 |
);
|
| 542 |
|
| 543 |
this.stats.completed++;
|
| 544 |
this.stats.success++;
|
| 545 |
-
this.lastSuccessTime = Date.now(); // 更新最后成功时间
|
| 546 |
this.stats.promptTokens += result.usage.prompt_tokens;
|
| 547 |
this.stats.completionTokens += result.usage.completion_tokens;
|
| 548 |
this.stats.totalTokens += result.usage.total_tokens;
|
|
@@ -557,15 +477,7 @@ class TaskExecutor {
|
|
| 557 |
log.info('Task', this.taskId, 'reached token limit:', this.stats.totalTokens);
|
| 558 |
} else {
|
| 559 |
threadLog.status = 'success';
|
| 560 |
-
|
| 561 |
-
let fullOutput = '';
|
| 562 |
-
if (result.reasoning) {
|
| 563 |
-
fullOutput += '💭 ' + result.reasoning + '\n---\n';
|
| 564 |
-
}
|
| 565 |
-
if (result.content) {
|
| 566 |
-
fullOutput += result.content;
|
| 567 |
-
}
|
| 568 |
-
threadLog.content = fullOutput.slice(-LOG_CONTENT_MAX_LENGTH);
|
| 569 |
}
|
| 570 |
|
| 571 |
// Adjust marker for ratio
|
|
@@ -577,39 +489,31 @@ class TaskExecutor {
|
|
| 577 |
if (this.config.randOn) {
|
| 578 |
const diff = liveRatio - this.ratio.target;
|
| 579 |
if (Math.abs(diff) >= 0.02) {
|
| 580 |
-
const step = Math.max(1, Math.round(Math.abs(diff) *
|
| 581 |
-
if (diff < 0) this.markerLen = Math.min(
|
| 582 |
else this.markerLen = Math.max(0, this.markerLen - step);
|
| 583 |
}
|
| 584 |
this.ratio.markerLen = this.markerLen;
|
| 585 |
}
|
| 586 |
|
| 587 |
-
// Add markerLen info to thread log
|
| 588 |
-
threadLog.markerLen = this.markerLen;
|
| 589 |
-
|
| 590 |
} catch (e) {
|
| 591 |
const isAborted = e.message === 'Aborted' || e.name === 'AbortError';
|
| 592 |
|
| 593 |
if (isAborted) {
|
| 594 |
-
// Distinguish between pause and stop
|
| 595 |
if (this.paused) {
|
| 596 |
-
// Paused - don't count as aborted, keep current progress
|
| 597 |
threadLog.status = 'paused';
|
| 598 |
threadLog.message = '已暂停,等待继续...';
|
| 599 |
log.debug('Thread', threadId, 'loop', i, 'paused');
|
| 600 |
-
this.notifyUpdate(true);
|
| 601 |
-
// Wait for resume
|
| 602 |
while (this.paused && !this.stopped) {
|
| 603 |
await new Promise(resolve => setTimeout(resolve, 100));
|
| 604 |
}
|
| 605 |
-
// If resumed and not stopped, retry current loop with new controller
|
| 606 |
if (!this.stopped) {
|
| 607 |
-
i--;
|
| 608 |
threadLog.status = 'running';
|
| 609 |
continue;
|
| 610 |
}
|
| 611 |
}
|
| 612 |
-
// Stopped - count as aborted
|
| 613 |
if (this.stopped) {
|
| 614 |
this.stats.completed++;
|
| 615 |
this.stats.aborted++;
|
|
@@ -617,30 +521,6 @@ class TaskExecutor {
|
|
| 617 |
log.debug('Thread', threadId, 'loop', i, 'aborted');
|
| 618 |
}
|
| 619 |
} else {
|
| 620 |
-
// 检查是否是可重试的网络错误(TLS 握手失败等)
|
| 621 |
-
if (isRetryableError(e)) {
|
| 622 |
-
// 获取或初始化重试计数
|
| 623 |
-
const retryKey = `${threadId}-${i}`;
|
| 624 |
-
if (!this._retryCount) this._retryCount = {};
|
| 625 |
-
this._retryCount[retryKey] = (this._retryCount[retryKey] || 0) + 1;
|
| 626 |
-
|
| 627 |
-
if (this._retryCount[retryKey] <= MAX_RETRIES) {
|
| 628 |
-
threadLog.status = 'retrying';
|
| 629 |
-
threadLog.message = `网络错误,重试 ${this._retryCount[retryKey]}/${MAX_RETRIES}...`;
|
| 630 |
-
log.warn('Thread', threadId, 'loop', i, 'retryable error:', e.message, 'retry', this._retryCount[retryKey]);
|
| 631 |
-
this.notifyUpdate(true);
|
| 632 |
-
|
| 633 |
-
// 指数退避等待
|
| 634 |
-
await new Promise(resolve => setTimeout(resolve, 100 * this._retryCount[retryKey]));
|
| 635 |
-
|
| 636 |
-
// 重试当前循环
|
| 637 |
-
i--;
|
| 638 |
-
continue;
|
| 639 |
-
}
|
| 640 |
-
// 重试次数用尽,清理计数
|
| 641 |
-
delete this._retryCount[retryKey];
|
| 642 |
-
}
|
| 643 |
-
|
| 644 |
this.stats.failed++;
|
| 645 |
threadLog.status = 'error';
|
| 646 |
threadLog.error = e.message || '请求失败';
|
|
@@ -648,6 +528,7 @@ class TaskExecutor {
|
|
| 648 |
}
|
| 649 |
}
|
| 650 |
|
|
|
|
| 651 |
this.notifyUpdate(true); // Force update after each request completes
|
| 652 |
|
| 653 |
// Wait between loops (not after the last loop)
|
|
@@ -692,9 +573,6 @@ class TaskExecutor {
|
|
| 692 |
}
|
| 693 |
|
| 694 |
this.notifyUpdate();
|
| 695 |
-
|
| 696 |
-
// 启动停滞检查定时器
|
| 697 |
-
this.startStallCheck();
|
| 698 |
|
| 699 |
// Run workers in parallel
|
| 700 |
const workers = [];
|
|
@@ -705,7 +583,6 @@ class TaskExecutor {
|
|
| 705 |
await Promise.all(workers);
|
| 706 |
|
| 707 |
this.running = false;
|
| 708 |
-
this.stopStallCheck(); // 停止停滞检查
|
| 709 |
log.info('TaskExecutor finished:', this.taskId, 'success:', this.stats.success, 'failed:', this.stats.failed, 'tokens:', this.stats.totalTokens);
|
| 710 |
this.notifyUpdate(true); // Force final update
|
| 711 |
|
|
@@ -717,7 +594,6 @@ class TaskExecutor {
|
|
| 717 |
if (!this.running || this.paused) return;
|
| 718 |
log.info('TaskExecutor pausing:', this.taskId);
|
| 719 |
this.paused = true;
|
| 720 |
-
this.stopStallCheck(); // 停止停滞检查
|
| 721 |
if (this.startTime) {
|
| 722 |
this.elapsedBefore += Date.now() - this.startTime;
|
| 723 |
this.startTime = null;
|
|
@@ -731,9 +607,6 @@ class TaskExecutor {
|
|
| 731 |
if (!this.running || !this.paused) return;
|
| 732 |
log.info('TaskExecutor resuming:', this.taskId);
|
| 733 |
this.paused = false;
|
| 734 |
-
this.lastSuccessTime = Date.now(); // 重置成功时间
|
| 735 |
-
this.startStallCheck(); // 重启停滞检查
|
| 736 |
-
// Create new AbortController for resumed execution
|
| 737 |
this.controller = new AbortController();
|
| 738 |
this.startTime = Date.now();
|
| 739 |
this.notifyUpdate();
|
|
@@ -743,43 +616,9 @@ class TaskExecutor {
|
|
| 743 |
log.info('TaskExecutor stopping:', this.taskId);
|
| 744 |
this.stopped = true;
|
| 745 |
this.paused = false;
|
| 746 |
-
this.stopStallCheck(); // 停止停滞检查
|
| 747 |
this.controller.abort();
|
| 748 |
}
|
| 749 |
|
| 750 |
-
// 启动停滞检查定时器
|
| 751 |
-
startStallCheck() {
|
| 752 |
-
if (this.stallCheckTimer) return;
|
| 753 |
-
this.lastSuccessTime = Date.now();
|
| 754 |
-
this.stallCheckTimer = setInterval(() => this.checkStall(), this.stallCheckInterval);
|
| 755 |
-
}
|
| 756 |
-
|
| 757 |
-
// 停止停滞检查定时器
|
| 758 |
-
stopStallCheck() {
|
| 759 |
-
if (this.stallCheckTimer) {
|
| 760 |
-
clearInterval(this.stallCheckTimer);
|
| 761 |
-
this.stallCheckTimer = null;
|
| 762 |
-
}
|
| 763 |
-
}
|
| 764 |
-
|
| 765 |
-
// 检查是否停滞,如果超时则自动暂停
|
| 766 |
-
checkStall() {
|
| 767 |
-
if (this.paused || this.stopped) return;
|
| 768 |
-
|
| 769 |
-
const timeSinceLastSuccess = Date.now() - this.lastSuccessTime;
|
| 770 |
-
if (timeSinceLastSuccess >= STALL_TIMEOUT_MS) {
|
| 771 |
-
log.warn('Task', this.taskId, '停滞超时,自动暂停。最后成功时间:', new Date(this.lastSuccessTime).toISOString());
|
| 772 |
-
this.pause();
|
| 773 |
-
// 通知前端
|
| 774 |
-
if (this.onUpdate) {
|
| 775 |
-
const status = this.getStatus();
|
| 776 |
-
status.stallAutoPaused = true;
|
| 777 |
-
status.stallMessage = `连续 ${Math.round(STALL_TIMEOUT_MS / 60000)} 分钟无成功请求,已自动暂停`;
|
| 778 |
-
this.onUpdate(status);
|
| 779 |
-
}
|
| 780 |
-
}
|
| 781 |
-
}
|
| 782 |
-
|
| 783 |
getStatus() {
|
| 784 |
let elapsed = this.elapsedBefore;
|
| 785 |
if (this.startTime && !this.paused) {
|
|
@@ -794,7 +633,7 @@ class TaskExecutor {
|
|
| 794 |
ratio: this.ratio,
|
| 795 |
elapsed: elapsed,
|
| 796 |
elapsedBefore: this.elapsedBefore,
|
| 797 |
-
threadLogs: { ...this.threadLogs },
|
| 798 |
threadProgress: { ...this.threadProgress },
|
| 799 |
markerLen: this.markerLen
|
| 800 |
};
|
|
@@ -844,4 +683,4 @@ class TaskExecutor {
|
|
| 844 |
}
|
| 845 |
}
|
| 846 |
|
| 847 |
-
module.exports = { TaskExecutor, fetchModels
|
|
|
|
| 2 |
const http = require('http');
|
| 3 |
const log = require('./logger');
|
| 4 |
|
| 5 |
+
const USER_AGENTS = [
|
| 6 |
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
|
| 7 |
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36',
|
| 8 |
+
'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',
|
| 9 |
+
'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',
|
| 10 |
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:132.0) Gecko/20100101 Firefox/132.0',
|
| 11 |
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:132.0) Gecko/20100101 Firefox/132.0',
|
| 12 |
+
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
|
| 13 |
+
'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'
|
| 14 |
+
];
|
| 15 |
+
|
| 16 |
+
const ACCEPT_LANGUAGES = [
|
| 17 |
+
'en-US,en;q=0.9',
|
| 18 |
+
'en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7',
|
| 19 |
+
'zh-CN,zh;q=0.9,en;q=0.8',
|
| 20 |
+
'en-GB,en;q=0.9,en-US;q=0.8',
|
| 21 |
+
'zh-TW,zh;q=0.9,en-US;q=0.8,en;q=0.7'
|
| 22 |
+
];
|
| 23 |
+
|
| 24 |
+
function getRandomUserAgent() {
|
| 25 |
+
return USER_AGENTS[Math.floor(Math.random() * USER_AGENTS.length)];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
}
|
| 27 |
|
| 28 |
+
function getRandomAcceptLanguage() {
|
| 29 |
+
return ACCEPT_LANGUAGES[Math.floor(Math.random() * ACCEPT_LANGUAGES.length)];
|
| 30 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
|
| 32 |
+
function buildDisguiseHeaders() {
|
| 33 |
+
return {
|
| 34 |
+
'User-Agent': getRandomUserAgent(),
|
| 35 |
+
'Accept': 'application/json, text/event-stream, */*',
|
| 36 |
+
'Accept-Language': getRandomAcceptLanguage(),
|
| 37 |
+
'Accept-Encoding': 'gzip, deflate, br',
|
| 38 |
+
'Cache-Control': 'no-cache',
|
| 39 |
+
'Pragma': 'no-cache',
|
| 40 |
+
'Sec-Ch-Ua': '"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"',
|
| 41 |
+
'Sec-Ch-Ua-Mobile': '?0',
|
| 42 |
+
'Sec-Ch-Ua-Platform': '"Windows"',
|
| 43 |
+
'Sec-Fetch-Dest': 'empty',
|
| 44 |
+
'Sec-Fetch-Mode': 'cors',
|
| 45 |
+
'Sec-Fetch-Site': 'cross-site',
|
| 46 |
+
'Connection': 'keep-alive'
|
| 47 |
+
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
}
|
| 49 |
|
| 50 |
function buildUrl(baseUrl, endpoint) {
|
|
|
|
| 105 |
const isHttps = urlObj.protocol === 'https:';
|
| 106 |
const lib = isHttps ? https : http;
|
| 107 |
|
| 108 |
+
const disguiseHeaders = buildDisguiseHeaders();
|
|
|
|
| 109 |
const reqOptions = {
|
| 110 |
hostname: urlObj.hostname,
|
| 111 |
port: urlObj.port || (isHttps ? 443 : 80),
|
| 112 |
path: urlObj.pathname + urlObj.search,
|
| 113 |
method: options.method || 'GET',
|
| 114 |
+
headers: { ...disguiseHeaders, ...options.headers },
|
| 115 |
+
timeout
|
|
|
|
| 116 |
};
|
| 117 |
|
| 118 |
const req = lib.request(reqOptions, (res) => {
|
|
|
|
| 135 |
try {
|
| 136 |
log.debug('Fetching models from:', baseUrl);
|
| 137 |
const url = buildUrl(baseUrl, '/models');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
const resp = await makeRequest(url, {
|
| 139 |
method: 'GET',
|
| 140 |
+
headers: { 'Authorization': 'Bearer ' + token }
|
| 141 |
});
|
| 142 |
const data = JSON.parse(resp.body);
|
| 143 |
if (resp.status !== 200) {
|
|
|
|
| 155 |
}
|
| 156 |
}
|
| 157 |
|
| 158 |
+
async function requestOnce(config, signal) {
|
| 159 |
const { prompt, tag, marker } = buildUserPrompt(config.usr, config.randOn, config.markerLen);
|
| 160 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
const body = {
|
| 162 |
model: config.model,
|
| 163 |
messages: [
|
|
|
|
| 173 |
const resp = await makeRequest(url, {
|
| 174 |
method: 'POST',
|
| 175 |
headers: {
|
| 176 |
+
'Authorization': 'Bearer ' + config.token,
|
| 177 |
'Content-Type': 'application/json'
|
| 178 |
}
|
| 179 |
}, JSON.stringify(body), config.timeout);
|
|
|
|
| 202 |
}
|
| 203 |
|
| 204 |
const content = data?.choices?.[0]?.message?.content || '';
|
| 205 |
+
const usage = usageWithFallback(data?.usage, prompt, config.sys, content);
|
|
|
|
| 206 |
|
| 207 |
+
return { content, usage, finish_reason: data?.choices?.[0]?.finish_reason, tag, marker, prompt };
|
| 208 |
}
|
| 209 |
|
| 210 |
+
async function streamRequest(config, onChunk, signal) {
|
| 211 |
const { prompt, tag, marker } = buildUserPrompt(config.usr, config.randOn, config.markerLen);
|
| 212 |
|
| 213 |
const body = {
|
|
|
|
| 227 |
const isHttps = urlObj.protocol === 'https:';
|
| 228 |
const lib = isHttps ? https : http;
|
| 229 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
return new Promise((resolve, reject) => {
|
|
|
|
| 231 |
if (signal?.aborted) {
|
| 232 |
reject(new Error('Aborted'));
|
| 233 |
return;
|
| 234 |
}
|
| 235 |
|
| 236 |
+
const disguiseHeaders = buildDisguiseHeaders();
|
| 237 |
const reqOptions = {
|
| 238 |
hostname: urlObj.hostname,
|
| 239 |
port: urlObj.port || (isHttps ? 443 : 80),
|
| 240 |
path: urlObj.pathname,
|
| 241 |
method: 'POST',
|
| 242 |
headers: {
|
| 243 |
+
...disguiseHeaders,
|
| 244 |
+
'Authorization': 'Bearer ' + config.token,
|
| 245 |
'Content-Type': 'application/json'
|
| 246 |
},
|
| 247 |
+
timeout: config.timeout || 60000
|
|
|
|
| 248 |
};
|
| 249 |
|
| 250 |
let fullContent = '';
|
|
|
|
| 251 |
let usage = { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };
|
| 252 |
let finishReason = null;
|
| 253 |
let aborted = false;
|
|
|
|
| 295 |
const json = JSON.parse(payload);
|
| 296 |
if (json.error) throw new Error(json.error.message);
|
| 297 |
|
| 298 |
+
const delta = json.choices?.[0]?.delta?.content;
|
| 299 |
+
if (delta) {
|
| 300 |
+
fullContent += delta;
|
| 301 |
+
if (onChunk) onChunk(delta, fullContent);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 302 |
}
|
| 303 |
|
| 304 |
if (json.usage) usage = json.usage;
|
|
|
|
| 318 |
|
| 319 |
res.on('end', () => {
|
| 320 |
cleanup();
|
| 321 |
+
const finalUsage = usageWithFallback(usage, prompt, config.sys, fullContent);
|
| 322 |
resolve({
|
| 323 |
content: fullContent,
|
|
|
|
| 324 |
usage: finalUsage,
|
| 325 |
finish_reason: finishReason,
|
| 326 |
tag,
|
|
|
|
| 374 |
this.notifyTimer = null;
|
| 375 |
this.notifyInterval = 500; // ms between notifications (increased for high concurrency)
|
| 376 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 377 |
// Load from saved progress or initialize fresh
|
| 378 |
if (savedProgress) {
|
| 379 |
this.stats = savedProgress.stats || {
|
|
|
|
| 436 |
thread: threadId,
|
| 437 |
loop: i,
|
| 438 |
status: 'running',
|
| 439 |
+
startTime: Date.now(),
|
| 440 |
+
message: ''
|
| 441 |
};
|
| 442 |
|
| 443 |
this.threadLogs[threadId] = threadLog;
|
|
|
|
| 448 |
const result = this.config.streamOn
|
| 449 |
? await streamRequest(
|
| 450 |
{ ...this.config, markerLen: this.markerLen },
|
| 451 |
+
(delta, full) => {
|
| 452 |
threadLog.status = 'streaming';
|
| 453 |
+
threadLog.content = full;
|
| 454 |
// notifyUpdate already throttled, so this is fine
|
| 455 |
this.notifyUpdate();
|
| 456 |
},
|
| 457 |
+
this.controller.signal
|
|
|
|
| 458 |
)
|
| 459 |
: await requestOnce(
|
| 460 |
{ ...this.config, markerLen: this.markerLen },
|
| 461 |
+
this.controller.signal
|
|
|
|
| 462 |
);
|
| 463 |
|
| 464 |
this.stats.completed++;
|
| 465 |
this.stats.success++;
|
|
|
|
| 466 |
this.stats.promptTokens += result.usage.prompt_tokens;
|
| 467 |
this.stats.completionTokens += result.usage.completion_tokens;
|
| 468 |
this.stats.totalTokens += result.usage.total_tokens;
|
|
|
|
| 477 |
log.info('Task', this.taskId, 'reached token limit:', this.stats.totalTokens);
|
| 478 |
} else {
|
| 479 |
threadLog.status = 'success';
|
| 480 |
+
threadLog.content = result.content?.slice(-500);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 481 |
}
|
| 482 |
|
| 483 |
// Adjust marker for ratio
|
|
|
|
| 489 |
if (this.config.randOn) {
|
| 490 |
const diff = liveRatio - this.ratio.target;
|
| 491 |
if (Math.abs(diff) >= 0.02) {
|
| 492 |
+
const step = Math.max(1, Math.round(Math.abs(diff) * 50));
|
| 493 |
+
if (diff < 0) this.markerLen = Math.min(4000, this.markerLen + step);
|
| 494 |
else this.markerLen = Math.max(0, this.markerLen - step);
|
| 495 |
}
|
| 496 |
this.ratio.markerLen = this.markerLen;
|
| 497 |
}
|
| 498 |
|
|
|
|
|
|
|
|
|
|
| 499 |
} catch (e) {
|
| 500 |
const isAborted = e.message === 'Aborted' || e.name === 'AbortError';
|
| 501 |
|
| 502 |
if (isAborted) {
|
|
|
|
| 503 |
if (this.paused) {
|
|
|
|
| 504 |
threadLog.status = 'paused';
|
| 505 |
threadLog.message = '已暂停,等待继续...';
|
| 506 |
log.debug('Thread', threadId, 'loop', i, 'paused');
|
| 507 |
+
this.notifyUpdate(true);
|
|
|
|
| 508 |
while (this.paused && !this.stopped) {
|
| 509 |
await new Promise(resolve => setTimeout(resolve, 100));
|
| 510 |
}
|
|
|
|
| 511 |
if (!this.stopped) {
|
| 512 |
+
i--;
|
| 513 |
threadLog.status = 'running';
|
| 514 |
continue;
|
| 515 |
}
|
| 516 |
}
|
|
|
|
| 517 |
if (this.stopped) {
|
| 518 |
this.stats.completed++;
|
| 519 |
this.stats.aborted++;
|
|
|
|
| 521 |
log.debug('Thread', threadId, 'loop', i, 'aborted');
|
| 522 |
}
|
| 523 |
} else {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 524 |
this.stats.failed++;
|
| 525 |
threadLog.status = 'error';
|
| 526 |
threadLog.error = e.message || '请求失败';
|
|
|
|
| 528 |
}
|
| 529 |
}
|
| 530 |
|
| 531 |
+
threadLog.endTime = Date.now();
|
| 532 |
this.notifyUpdate(true); // Force update after each request completes
|
| 533 |
|
| 534 |
// Wait between loops (not after the last loop)
|
|
|
|
| 573 |
}
|
| 574 |
|
| 575 |
this.notifyUpdate();
|
|
|
|
|
|
|
|
|
|
| 576 |
|
| 577 |
// Run workers in parallel
|
| 578 |
const workers = [];
|
|
|
|
| 583 |
await Promise.all(workers);
|
| 584 |
|
| 585 |
this.running = false;
|
|
|
|
| 586 |
log.info('TaskExecutor finished:', this.taskId, 'success:', this.stats.success, 'failed:', this.stats.failed, 'tokens:', this.stats.totalTokens);
|
| 587 |
this.notifyUpdate(true); // Force final update
|
| 588 |
|
|
|
|
| 594 |
if (!this.running || this.paused) return;
|
| 595 |
log.info('TaskExecutor pausing:', this.taskId);
|
| 596 |
this.paused = true;
|
|
|
|
| 597 |
if (this.startTime) {
|
| 598 |
this.elapsedBefore += Date.now() - this.startTime;
|
| 599 |
this.startTime = null;
|
|
|
|
| 607 |
if (!this.running || !this.paused) return;
|
| 608 |
log.info('TaskExecutor resuming:', this.taskId);
|
| 609 |
this.paused = false;
|
|
|
|
|
|
|
|
|
|
| 610 |
this.controller = new AbortController();
|
| 611 |
this.startTime = Date.now();
|
| 612 |
this.notifyUpdate();
|
|
|
|
| 616 |
log.info('TaskExecutor stopping:', this.taskId);
|
| 617 |
this.stopped = true;
|
| 618 |
this.paused = false;
|
|
|
|
| 619 |
this.controller.abort();
|
| 620 |
}
|
| 621 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 622 |
getStatus() {
|
| 623 |
let elapsed = this.elapsedBefore;
|
| 624 |
if (this.startTime && !this.paused) {
|
|
|
|
| 633 |
ratio: this.ratio,
|
| 634 |
elapsed: elapsed,
|
| 635 |
elapsedBefore: this.elapsedBefore,
|
| 636 |
+
threadLogs: { ...this.threadLogs },
|
| 637 |
threadProgress: { ...this.threadProgress },
|
| 638 |
markerLen: this.markerLen
|
| 639 |
};
|
|
|
|
| 683 |
}
|
| 684 |
}
|
| 685 |
|
| 686 |
+
module.exports = { TaskExecutor, fetchModels };
|
logger.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
| 2 |
// LOG_LEVEL: debug, info, warn, error, none (default: info)
|
| 3 |
|
| 4 |
const LOG_LEVELS = { debug: 0, info: 1, warn: 2, error: 3, none: 4 };
|
| 5 |
-
const currentLevel = LOG_LEVELS[process.env.LOG_LEVEL?.toLowerCase()] ?? LOG_LEVELS.
|
| 6 |
|
| 7 |
const ts = () => new Date().toISOString();
|
| 8 |
|
|
|
|
| 2 |
// LOG_LEVEL: debug, info, warn, error, none (default: info)
|
| 3 |
|
| 4 |
const LOG_LEVELS = { debug: 0, info: 1, warn: 2, error: 3, none: 4 };
|
| 5 |
+
const currentLevel = LOG_LEVELS[process.env.LOG_LEVEL?.toLowerCase()] ?? LOG_LEVELS.info;
|
| 6 |
|
| 7 |
const ts = () => new Date().toISOString();
|
| 8 |
|
package-lock.json
CHANGED
|
@@ -8,7 +8,6 @@
|
|
| 8 |
"name": "token-consumer",
|
| 9 |
"version": "1.0.0",
|
| 10 |
"dependencies": {
|
| 11 |
-
"compression": "^1.8.1",
|
| 12 |
"cors": "^2.8.5",
|
| 13 |
"express": "^4.18.2",
|
| 14 |
"uuid": "^9.0.0"
|
|
@@ -187,45 +186,6 @@
|
|
| 187 |
"fsevents": "~2.3.2"
|
| 188 |
}
|
| 189 |
},
|
| 190 |
-
"node_modules/compressible": {
|
| 191 |
-
"version": "2.0.18",
|
| 192 |
-
"resolved": "https://registry.npmmirror.com/compressible/-/compressible-2.0.18.tgz",
|
| 193 |
-
"integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
|
| 194 |
-
"license": "MIT",
|
| 195 |
-
"dependencies": {
|
| 196 |
-
"mime-db": ">= 1.43.0 < 2"
|
| 197 |
-
},
|
| 198 |
-
"engines": {
|
| 199 |
-
"node": ">= 0.6"
|
| 200 |
-
}
|
| 201 |
-
},
|
| 202 |
-
"node_modules/compression": {
|
| 203 |
-
"version": "1.8.1",
|
| 204 |
-
"resolved": "https://registry.npmmirror.com/compression/-/compression-1.8.1.tgz",
|
| 205 |
-
"integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==",
|
| 206 |
-
"license": "MIT",
|
| 207 |
-
"dependencies": {
|
| 208 |
-
"bytes": "3.1.2",
|
| 209 |
-
"compressible": "~2.0.18",
|
| 210 |
-
"debug": "2.6.9",
|
| 211 |
-
"negotiator": "~0.6.4",
|
| 212 |
-
"on-headers": "~1.1.0",
|
| 213 |
-
"safe-buffer": "5.2.1",
|
| 214 |
-
"vary": "~1.1.2"
|
| 215 |
-
},
|
| 216 |
-
"engines": {
|
| 217 |
-
"node": ">= 0.8.0"
|
| 218 |
-
}
|
| 219 |
-
},
|
| 220 |
-
"node_modules/compression/node_modules/negotiator": {
|
| 221 |
-
"version": "0.6.4",
|
| 222 |
-
"resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.4.tgz",
|
| 223 |
-
"integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==",
|
| 224 |
-
"license": "MIT",
|
| 225 |
-
"engines": {
|
| 226 |
-
"node": ">= 0.6"
|
| 227 |
-
}
|
| 228 |
-
},
|
| 229 |
"node_modules/content-disposition": {
|
| 230 |
"version": "0.5.4",
|
| 231 |
"resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-0.5.4.tgz",
|
|
@@ -934,15 +894,6 @@
|
|
| 934 |
"node": ">= 0.8"
|
| 935 |
}
|
| 936 |
},
|
| 937 |
-
"node_modules/on-headers": {
|
| 938 |
-
"version": "1.1.0",
|
| 939 |
-
"resolved": "https://registry.npmmirror.com/on-headers/-/on-headers-1.1.0.tgz",
|
| 940 |
-
"integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
|
| 941 |
-
"license": "MIT",
|
| 942 |
-
"engines": {
|
| 943 |
-
"node": ">= 0.8"
|
| 944 |
-
}
|
| 945 |
-
},
|
| 946 |
"node_modules/parseurl": {
|
| 947 |
"version": "1.3.3",
|
| 948 |
"resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz",
|
|
|
|
| 8 |
"name": "token-consumer",
|
| 9 |
"version": "1.0.0",
|
| 10 |
"dependencies": {
|
|
|
|
| 11 |
"cors": "^2.8.5",
|
| 12 |
"express": "^4.18.2",
|
| 13 |
"uuid": "^9.0.0"
|
|
|
|
| 186 |
"fsevents": "~2.3.2"
|
| 187 |
}
|
| 188 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
"node_modules/content-disposition": {
|
| 190 |
"version": "0.5.4",
|
| 191 |
"resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-0.5.4.tgz",
|
|
|
|
| 894 |
"node": ">= 0.8"
|
| 895 |
}
|
| 896 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 897 |
"node_modules/parseurl": {
|
| 898 |
"version": "1.3.3",
|
| 899 |
"resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz",
|
package.json
CHANGED
|
@@ -8,14 +8,11 @@
|
|
| 8 |
"dev": "cross-env LOG_LEVEL=debug nodemon server.js"
|
| 9 |
},
|
| 10 |
"nodemonConfig": {
|
| 11 |
-
"ignore": [
|
| 12 |
-
"data/*"
|
| 13 |
-
]
|
| 14 |
},
|
| 15 |
"dependencies": {
|
| 16 |
-
"compression": "^1.8.1",
|
| 17 |
-
"cors": "^2.8.5",
|
| 18 |
"express": "^4.18.2",
|
|
|
|
| 19 |
"uuid": "^9.0.0"
|
| 20 |
},
|
| 21 |
"devDependencies": {
|
|
|
|
| 8 |
"dev": "cross-env LOG_LEVEL=debug nodemon server.js"
|
| 9 |
},
|
| 10 |
"nodemonConfig": {
|
| 11 |
+
"ignore": ["data/*"]
|
|
|
|
|
|
|
| 12 |
},
|
| 13 |
"dependencies": {
|
|
|
|
|
|
|
| 14 |
"express": "^4.18.2",
|
| 15 |
+
"cors": "^2.8.5",
|
| 16 |
"uuid": "^9.0.0"
|
| 17 |
},
|
| 18 |
"devDependencies": {
|
server.js
CHANGED
|
@@ -1,25 +1,15 @@
|
|
| 1 |
const express = require('express');
|
| 2 |
const cors = require('cors');
|
| 3 |
-
const compression = require('compression');
|
| 4 |
const { v4: uuidv4 } = require('uuid');
|
| 5 |
const path = require('path');
|
| 6 |
const os = require('os');
|
| 7 |
-
const http = require('http');
|
| 8 |
-
const https = require('https');
|
| 9 |
const storage = require('./storage');
|
| 10 |
const { TaskExecutor, fetchModels } = require('./executor');
|
| 11 |
const log = require('./logger');
|
| 12 |
|
| 13 |
-
// 设置全局 HTTP Agent 的最大连接数,支持高并发
|
| 14 |
-
http.globalAgent.maxSockets = 1000;
|
| 15 |
-
https.globalAgent.maxSockets = 1000;
|
| 16 |
-
|
| 17 |
const app = express();
|
| 18 |
const PORT = process.env.PORT || 51730;
|
| 19 |
|
| 20 |
-
// Configuration
|
| 21 |
-
const MAX_THREADS = 1000;
|
| 22 |
-
|
| 23 |
// Auth configuration
|
| 24 |
const AUTH_USER = process.env.AUTH_USER || 'admin';
|
| 25 |
const AUTH_PASS = process.env.AUTH_PASS || 'admin';
|
|
@@ -58,9 +48,8 @@ function rateLimiter(req, res, next) {
|
|
| 58 |
}
|
| 59 |
|
| 60 |
// Middleware
|
| 61 |
-
app.use(compression());
|
| 62 |
app.use(cors());
|
| 63 |
-
app.use(express.json(
|
| 64 |
app.use(rateLimiter);
|
| 65 |
|
| 66 |
// HTTP Basic Auth middleware for /api routes
|
|
@@ -126,7 +115,7 @@ app.post('/api/tasks', (req, res) => {
|
|
| 126 |
sys: config.sys || '',
|
| 127 |
usr: config.usr,
|
| 128 |
loop: Math.max(1, Math.min(10000, Number(config.loop) || 10)),
|
| 129 |
-
threads: Math.max(1, Math.min(
|
| 130 |
max: Math.max(1, Math.min(32768, Number(config.max) || 1024)),
|
| 131 |
temp: Math.max(0, Math.min(2, Number(config.temp) || 1)),
|
| 132 |
timeout: Math.max(5000, Number(config.timeout) || 60000),
|
|
@@ -171,23 +160,10 @@ app.get('/api/tasks', (req, res) => {
|
|
| 171 |
if (!t.stats.total && t.config) {
|
| 172 |
t.stats.total = (t.config.loop || 10) * (t.config.threads || 3);
|
| 173 |
}
|
| 174 |
-
const status = executor?.getStatus() || null;
|
| 175 |
-
// Remove threadLogs and threadProgress from list view to reduce payload
|
| 176 |
-
if (status) {
|
| 177 |
-
delete status.threadLogs;
|
| 178 |
-
delete status.threadProgress;
|
| 179 |
-
}
|
| 180 |
-
// Create shallow copy and remove threadLogs/threadProgress from progress
|
| 181 |
-
const taskCopy = { ...t };
|
| 182 |
-
if (taskCopy.progress) {
|
| 183 |
-
taskCopy.progress = { ...taskCopy.progress };
|
| 184 |
-
delete taskCopy.progress.threadLogs;
|
| 185 |
-
delete taskCopy.progress.threadProgress;
|
| 186 |
-
}
|
| 187 |
return {
|
| 188 |
-
...
|
| 189 |
running: executor?.running || false,
|
| 190 |
-
currentStats:
|
| 191 |
};
|
| 192 |
});
|
| 193 |
log.debug('GET /api/tasks, count:', result.length);
|
|
@@ -497,16 +473,6 @@ app.get('/', (req, res) => {
|
|
| 497 |
res.sendFile(path.join(__dirname, 'token-consumer.html'));
|
| 498 |
});
|
| 499 |
|
| 500 |
-
// Global error handler - must be after all routes
|
| 501 |
-
app.use((err, req, res, next) => {
|
| 502 |
-
log.error('Unhandled error:', err.message);
|
| 503 |
-
// Ensure JSON response for API routes
|
| 504 |
-
if (req.path.startsWith('/api')) {
|
| 505 |
-
return res.status(500).json({ error: err.message || '服务器内部错误' });
|
| 506 |
-
}
|
| 507 |
-
res.status(500).send('服务器内部错误');
|
| 508 |
-
});
|
| 509 |
-
|
| 510 |
// Resume running tasks on startup
|
| 511 |
function resumeRunningTasks() {
|
| 512 |
const tasks = storage.getAllTasks();
|
|
|
|
| 1 |
const express = require('express');
|
| 2 |
const cors = require('cors');
|
|
|
|
| 3 |
const { v4: uuidv4 } = require('uuid');
|
| 4 |
const path = require('path');
|
| 5 |
const os = require('os');
|
|
|
|
|
|
|
| 6 |
const storage = require('./storage');
|
| 7 |
const { TaskExecutor, fetchModels } = require('./executor');
|
| 8 |
const log = require('./logger');
|
| 9 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
const app = express();
|
| 11 |
const PORT = process.env.PORT || 51730;
|
| 12 |
|
|
|
|
|
|
|
|
|
|
| 13 |
// Auth configuration
|
| 14 |
const AUTH_USER = process.env.AUTH_USER || 'admin';
|
| 15 |
const AUTH_PASS = process.env.AUTH_PASS || 'admin';
|
|
|
|
| 48 |
}
|
| 49 |
|
| 50 |
// Middleware
|
|
|
|
| 51 |
app.use(cors());
|
| 52 |
+
app.use(express.json());
|
| 53 |
app.use(rateLimiter);
|
| 54 |
|
| 55 |
// HTTP Basic Auth middleware for /api routes
|
|
|
|
| 115 |
sys: config.sys || '',
|
| 116 |
usr: config.usr,
|
| 117 |
loop: Math.max(1, Math.min(10000, Number(config.loop) || 10)),
|
| 118 |
+
threads: Math.max(1, Math.min(200, Number(config.threads) || 3)),
|
| 119 |
max: Math.max(1, Math.min(32768, Number(config.max) || 1024)),
|
| 120 |
temp: Math.max(0, Math.min(2, Number(config.temp) || 1)),
|
| 121 |
timeout: Math.max(5000, Number(config.timeout) || 60000),
|
|
|
|
| 160 |
if (!t.stats.total && t.config) {
|
| 161 |
t.stats.total = (t.config.loop || 10) * (t.config.threads || 3);
|
| 162 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
return {
|
| 164 |
+
...t,
|
| 165 |
running: executor?.running || false,
|
| 166 |
+
currentStats: executor?.getStatus() || null
|
| 167 |
};
|
| 168 |
});
|
| 169 |
log.debug('GET /api/tasks, count:', result.length);
|
|
|
|
| 473 |
res.sendFile(path.join(__dirname, 'token-consumer.html'));
|
| 474 |
});
|
| 475 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 476 |
// Resume running tasks on startup
|
| 477 |
function resumeRunningTasks() {
|
| 478 |
const tasks = storage.getAllTasks();
|
token-consumer.html
CHANGED
|
@@ -38,7 +38,7 @@ button:disabled{opacity:.5;cursor:not-allowed;transform:none}
|
|
| 38 |
.box label{font-size:11px;color:#64748b;margin-bottom:4px}
|
| 39 |
.box b{display:block;font-size:22px;color:#60a5fa;margin-top:4px}
|
| 40 |
.logs{grid-template-columns:repeat(auto-fill,minmax(280px,1fr))}
|
| 41 |
-
.log{border:1px solid #334155;border-radius:10px;background:linear-gradient(145deg,#1e293b,#0f172a);padding:10px;transition:all 0.2s
|
| 42 |
.log.waiting{border-color:#475569}
|
| 43 |
.log.running{border-color:#3b82f6;border-width:2px;box-shadow:0 0 15px rgba(59,130,246,0.3)}
|
| 44 |
.log.success{border-color:#22c55e;box-shadow:0 0 10px rgba(34,197,94,0.2)}
|
|
@@ -48,7 +48,7 @@ button:disabled{opacity:.5;cursor:not-allowed;transform:none}
|
|
| 48 |
.log.paused{border-color:#f59e0b;box-shadow:0 0 10px rgba(245,158,11,0.2)}
|
| 49 |
.t{display:flex;justify-content:space-between;font-size:13px;font-weight:600;margin-bottom:6px;color:#e2e8f0}
|
| 50 |
.m{font-size:11px;color:#64748b;margin-bottom:6px}
|
| 51 |
-
pre{margin:0;background:#0f172a;color:#93c5fd;border-radius:6px;padding:8px;
|
| 52 |
pre::-webkit-scrollbar{width:5px;height:5px}
|
| 53 |
pre::-webkit-scrollbar-track{background:#1e293b;border-radius:3px}
|
| 54 |
pre::-webkit-scrollbar-thumb{background:#475569;border-radius:3px}
|
|
@@ -217,6 +217,16 @@ pre::-webkit-scrollbar-thumb{background:#475569;border-radius:3px}
|
|
| 217 |
</div>
|
| 218 |
<span class="task-badge idle" id="detailBadge">空闲</span>
|
| 219 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
<div class="task-progress">
|
| 221 |
<div class="progress-row">
|
| 222 |
<span class="progress-label">任务</span>
|
|
@@ -238,10 +248,6 @@ pre::-webkit-scrollbar-thumb{background:#475569;border-radius:3px}
|
|
| 238 |
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>
|
| 239 |
<span id="bannerTime">0s</span>
|
| 240 |
</div>
|
| 241 |
-
<div class="meta-item">
|
| 242 |
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
|
| 243 |
-
<span>线程 <b id="detailThreads">0</b></span>
|
| 244 |
-
</div>
|
| 245 |
<div class="meta-item">
|
| 246 |
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
|
| 247 |
<span>成功 <b id="detailSuccess" style="color:#4ade80">0</b></span>
|
|
@@ -290,7 +296,7 @@ pre::-webkit-scrollbar-thumb{background:#475569;border-radius:3px}
|
|
| 290 |
</div>
|
| 291 |
<div class="g3" style="margin-top:8px">
|
| 292 |
<label>每线程循环次数<input id="loop" type="number" min="1" value="10"></label>
|
| 293 |
-
<label>并发线程数<input id="thr" type="number" min="1"
|
| 294 |
<label>max_tokens<input id="max" type="number" min="1" value="1024"></label>
|
| 295 |
<label>temperature<input id="temp" type="number" min="0" max="2" step="0.1" value="1"></label>
|
| 296 |
<label>单次超时秒数<input id="to" type="number" min="5" value="600"></label>
|
|
@@ -381,20 +387,9 @@ const getLocal=()=>{try{return JSON.parse(localStorage.getItem(LOCAL_KEY)||'{}')
|
|
| 381 |
const saveLocal=d=>{try{localStorage.setItem(LOCAL_KEY,JSON.stringify(d))}catch{}};
|
| 382 |
|
| 383 |
// API functions
|
| 384 |
-
async function
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
if(!ct.includes('application/json')){
|
| 388 |
-
const text=await r.text();
|
| 389 |
-
throw new Error(r.status===401?'需要认证,请刷新页面重新登录':'服务器返回非JSON响应: '+text.slice(0,100));
|
| 390 |
-
}
|
| 391 |
-
const data=await r.json();
|
| 392 |
-
if(!r.ok)throw new Error(data.error||'请求失败');
|
| 393 |
-
return data;
|
| 394 |
-
}
|
| 395 |
-
async function apiGet(url){return apiRequest(url)}
|
| 396 |
-
async function apiPost(url,data){return apiRequest(url,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)})}
|
| 397 |
-
async function apiDelete(url){return apiRequest(url,{method:'DELETE'})}
|
| 398 |
|
| 399 |
// Load tasks
|
| 400 |
async function loadTasks(){
|
|
@@ -495,10 +490,6 @@ function renderTaskList(){
|
|
| 495 |
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>
|
| 496 |
<span>${elapsedStr||'-'}</span>
|
| 497 |
</div>
|
| 498 |
-
<div class="meta-item">
|
| 499 |
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
|
| 500 |
-
<span>线程 <b>${t.config?.threads||0}</b></span>
|
| 501 |
-
</div>
|
| 502 |
<div class="meta-item">
|
| 503 |
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
|
| 504 |
<span>成功 <b style="color:#4ade80">${stats.success||0}</b></span>
|
|
@@ -582,6 +573,7 @@ function fillForm(d){
|
|
| 582 |
$('randOn').checked=!!d.randOn;
|
| 583 |
$('streamOn').checked=d.streamOn!==false;
|
| 584 |
$('ratioTarget').value=d.ratioTarget||1;
|
|
|
|
| 585 |
}
|
| 586 |
|
| 587 |
// Get form data
|
|
@@ -596,7 +588,7 @@ function getFormData(){
|
|
| 596 |
sys:$('sys').value.trim(),
|
| 597 |
usr:$('usr').value.trim(),
|
| 598 |
loop:n($('loop').value,10,1,10000),
|
| 599 |
-
threads:n($('thr').value,3,1,
|
| 600 |
max:n($('max').value,1024,1,32768),
|
| 601 |
temp:n($('temp').value,1,0,2),
|
| 602 |
timeout:n($('to').value,600,5,3600)*1000,
|
|
@@ -609,6 +601,16 @@ function getFormData(){
|
|
| 609 |
};
|
| 610 |
}
|
| 611 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 612 |
// Save task
|
| 613 |
async function saveTask(){
|
| 614 |
const data=getFormData();
|
|
@@ -786,7 +788,6 @@ function updateStats(s){
|
|
| 786 |
$('bannerProgress').textContent=`${completed}/${total}`;
|
| 787 |
$('bannerTokens').textContent=`${fmt(totalTokens)}/${fmt(maxTokens)}`;
|
| 788 |
$('bannerTime').textContent=fmtTime(s.elapsed||0);
|
| 789 |
-
$('detailThreads').textContent=$('thr').value||0;
|
| 790 |
$('detailSuccess').textContent=s.stats?.success||0;
|
| 791 |
$('detailFailed').textContent=s.stats?.failed||0;
|
| 792 |
$('detailAborted').textContent=s.stats?.aborted||0;
|
|
@@ -818,18 +819,10 @@ function updateLogs(threadLogs){
|
|
| 818 |
}else if(log.message){
|
| 819 |
displayContent=log.message;
|
| 820 |
}
|
| 821 |
-
// Build info line
|
| 822 |
-
let infoLine=`循环:${log.loop||'-'}`;
|
| 823 |
-
if(log.markerLen!==undefined){
|
| 824 |
-
infoLine+=` | 标记:${log.markerLen}`;
|
| 825 |
-
}
|
| 826 |
-
if(log.tokenTotal!==undefined && log.tokenTotal>1){
|
| 827 |
-
infoLine+=` | Key:${log.tokenIndex+1}/${log.tokenTotal}`;
|
| 828 |
-
}
|
| 829 |
return`
|
| 830 |
<div class="log ${statusClass}">
|
| 831 |
<div class="t"><span>线程 #${id}</span><span>${log.status}</span></div>
|
| 832 |
-
<div class="m">${
|
| 833 |
<pre>${displayContent.slice(-500)}</pre>
|
| 834 |
</div>
|
| 835 |
`;
|
|
@@ -862,14 +855,21 @@ function updateTaskHeader(task){
|
|
| 862 |
badge.textContent=statusText;
|
| 863 |
badge.className='badge '+status;
|
| 864 |
|
| 865 |
-
// Update status card
|
| 866 |
const statusCard=$('statusCard');
|
| 867 |
statusCard.className='card status-card '+status;
|
| 868 |
$('detailStatusDot').className='status-dot '+status;
|
| 869 |
$('detailBadge').className='task-badge '+status;
|
| 870 |
$('detailBadge').textContent=statusText;
|
| 871 |
$('bannerLabel').textContent=statusText;
|
| 872 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 873 |
|
| 874 |
updateButtons(task.running, task.currentStats?.paused, task.stopped);
|
| 875 |
}
|
|
@@ -884,7 +884,6 @@ function startPolling(taskId){
|
|
| 884 |
updateLogs(s.threadLogs);
|
| 885 |
updateButtons(s.running, s.paused, s.stopped);
|
| 886 |
|
| 887 |
-
// Update status card
|
| 888 |
let status='idle', statusText='空闲';
|
| 889 |
if(s.running && s.paused){
|
| 890 |
status='paused';
|
|
@@ -903,6 +902,16 @@ function startPolling(taskId){
|
|
| 903 |
$('detailBadge').textContent=statusText;
|
| 904 |
$('bannerLabel').textContent=statusText;
|
| 905 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 906 |
if(!s.running){
|
| 907 |
$('start').textContent='启动任务';
|
| 908 |
await loadTasks();
|
|
@@ -1079,11 +1088,18 @@ function newTask(){
|
|
| 1079 |
$('bannerProgress').textContent='0/0';
|
| 1080 |
$('bannerTokens').textContent='0/0';
|
| 1081 |
$('bannerTime').textContent='0s';
|
| 1082 |
-
$('detailThreads').textContent=$('thr').value||'0';
|
| 1083 |
$('detailSuccess').textContent='0';
|
| 1084 |
$('detailFailed').textContent='0';
|
| 1085 |
$('detailAborted').textContent='0';
|
| 1086 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1087 |
updateButtons(false, false, false);
|
| 1088 |
showEditView();
|
| 1089 |
stopPolling();
|
|
@@ -1111,6 +1127,11 @@ $('detailStartBtn').onclick=startTask;
|
|
| 1111 |
$('detailPauseBtn').onclick=pauseTask;
|
| 1112 |
$('detailStopBtn').onclick=stopTask;
|
| 1113 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1114 |
// Memory status update
|
| 1115 |
function formatBytes(bytes){
|
| 1116 |
if(bytes>=1e9)return(bytes/1e9).toFixed(1)+' GB';
|
|
@@ -1163,11 +1184,40 @@ loadTasks();
|
|
| 1163 |
$('base').value=local.base||'https://api.openai.com';
|
| 1164 |
$('token').value=local.token||'';
|
| 1165 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1166 |
// Load server config
|
| 1167 |
try{
|
| 1168 |
const serverConfig=await apiGet('/api/config');
|
| 1169 |
if(Object.keys(serverConfig).length){
|
| 1170 |
saveLocal({...local,...serverConfig});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1171 |
}
|
| 1172 |
}catch(e){}
|
| 1173 |
|
|
|
|
| 38 |
.box label{font-size:11px;color:#64748b;margin-bottom:4px}
|
| 39 |
.box b{display:block;font-size:22px;color:#60a5fa;margin-top:4px}
|
| 40 |
.logs{grid-template-columns:repeat(auto-fill,minmax(280px,1fr))}
|
| 41 |
+
.log{border:1px solid #334155;border-radius:10px;background:linear-gradient(145deg,#1e293b,#0f172a);padding:10px;transition:all 0.2s}
|
| 42 |
.log.waiting{border-color:#475569}
|
| 43 |
.log.running{border-color:#3b82f6;border-width:2px;box-shadow:0 0 15px rgba(59,130,246,0.3)}
|
| 44 |
.log.success{border-color:#22c55e;box-shadow:0 0 10px rgba(34,197,94,0.2)}
|
|
|
|
| 48 |
.log.paused{border-color:#f59e0b;box-shadow:0 0 10px rgba(245,158,11,0.2)}
|
| 49 |
.t{display:flex;justify-content:space-between;font-size:13px;font-weight:600;margin-bottom:6px;color:#e2e8f0}
|
| 50 |
.m{font-size:11px;color:#64748b;margin-bottom:6px}
|
| 51 |
+
pre{margin:0;background:#0f172a;color:#93c5fd;border-radius:6px;padding:8px;min-height:80px;max-height:180px;overflow:auto;white-space:pre-wrap;word-break:break-word;font-size:11px}
|
| 52 |
pre::-webkit-scrollbar{width:5px;height:5px}
|
| 53 |
pre::-webkit-scrollbar-track{background:#1e293b;border-radius:3px}
|
| 54 |
pre::-webkit-scrollbar-thumb{background:#475569;border-radius:3px}
|
|
|
|
| 217 |
</div>
|
| 218 |
<span class="task-badge idle" id="detailBadge">空闲</span>
|
| 219 |
</div>
|
| 220 |
+
<div id="configInfo" style="background:rgba(100,116,139,0.1);border:1px solid rgba(100,116,139,0.2);border-radius:8px;padding:10px;margin:10px 0;font-size:12px;color:#94a3b8">
|
| 221 |
+
<div style="display:flex;flex-wrap:wrap;gap:8px 16px">
|
| 222 |
+
<span>模型: <b id="infoModel" style="color:#60a5fa">-</b></span>
|
| 223 |
+
<span>max_tokens: <b id="infoMaxTokens" style="color:#60a5fa">-</b></span>
|
| 224 |
+
<span>累计上限: <b id="infoMaxLimit" style="color:#60a5fa">-</b></span>
|
| 225 |
+
<span>线程: <b id="infoThreads" style="color:#60a5fa">-</b></span>
|
| 226 |
+
<span>循环: <b id="infoLoop" style="color:#60a5fa">-</b></span>
|
| 227 |
+
<span>温度: <b id="infoTemp" style="color:#60a5fa">-</b></span>
|
| 228 |
+
</div>
|
| 229 |
+
</div>
|
| 230 |
<div class="task-progress">
|
| 231 |
<div class="progress-row">
|
| 232 |
<span class="progress-label">任务</span>
|
|
|
|
| 248 |
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>
|
| 249 |
<span id="bannerTime">0s</span>
|
| 250 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
<div class="meta-item">
|
| 252 |
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
|
| 253 |
<span>成功 <b id="detailSuccess" style="color:#4ade80">0</b></span>
|
|
|
|
| 296 |
</div>
|
| 297 |
<div class="g3" style="margin-top:8px">
|
| 298 |
<label>每线程循环次数<input id="loop" type="number" min="1" value="10"></label>
|
| 299 |
+
<label>并发线程数<input id="thr" type="number" min="1" value="3"></label>
|
| 300 |
<label>max_tokens<input id="max" type="number" min="1" value="1024"></label>
|
| 301 |
<label>temperature<input id="temp" type="number" min="0" max="2" step="0.1" value="1"></label>
|
| 302 |
<label>单次超时秒数<input id="to" type="number" min="5" value="600"></label>
|
|
|
|
| 387 |
const saveLocal=d=>{try{localStorage.setItem(LOCAL_KEY,JSON.stringify(d))}catch{}};
|
| 388 |
|
| 389 |
// API functions
|
| 390 |
+
async function apiGet(url){const r=await fetch(API+url);return r.json()}
|
| 391 |
+
async function apiPost(url,data){const r=await fetch(API+url,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)});return r.json()}
|
| 392 |
+
async function apiDelete(url){const r=await fetch(API+url,{method:'DELETE'});return r.json()}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 393 |
|
| 394 |
// Load tasks
|
| 395 |
async function loadTasks(){
|
|
|
|
| 490 |
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>
|
| 491 |
<span>${elapsedStr||'-'}</span>
|
| 492 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 493 |
<div class="meta-item">
|
| 494 |
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
|
| 495 |
<span>成功 <b style="color:#4ade80">${stats.success||0}</b></span>
|
|
|
|
| 573 |
$('randOn').checked=!!d.randOn;
|
| 574 |
$('streamOn').checked=d.streamOn!==false;
|
| 575 |
$('ratioTarget').value=d.ratioTarget||1;
|
| 576 |
+
updateConfigInfo();
|
| 577 |
}
|
| 578 |
|
| 579 |
// Get form data
|
|
|
|
| 588 |
sys:$('sys').value.trim(),
|
| 589 |
usr:$('usr').value.trim(),
|
| 590 |
loop:n($('loop').value,10,1,10000),
|
| 591 |
+
threads:n($('thr').value,3,1,200),
|
| 592 |
max:n($('max').value,1024,1,32768),
|
| 593 |
temp:n($('temp').value,1,0,2),
|
| 594 |
timeout:n($('to').value,600,5,3600)*1000,
|
|
|
|
| 601 |
};
|
| 602 |
}
|
| 603 |
|
| 604 |
+
// Update config info display
|
| 605 |
+
function updateConfigInfo(){
|
| 606 |
+
$('infoModel').textContent=$('modelAuto').value||'-';
|
| 607 |
+
$('infoMaxTokens').textContent=$('max').value||1024;
|
| 608 |
+
$('infoMaxLimit').textContent=fmt(Number($('maxTokens').value)||1000000000);
|
| 609 |
+
$('infoThreads').textContent=$('thr').value||3;
|
| 610 |
+
$('infoLoop').textContent=$('loop').value||10;
|
| 611 |
+
$('infoTemp').textContent=$('temp').value||1;
|
| 612 |
+
}
|
| 613 |
+
|
| 614 |
// Save task
|
| 615 |
async function saveTask(){
|
| 616 |
const data=getFormData();
|
|
|
|
| 788 |
$('bannerProgress').textContent=`${completed}/${total}`;
|
| 789 |
$('bannerTokens').textContent=`${fmt(totalTokens)}/${fmt(maxTokens)}`;
|
| 790 |
$('bannerTime').textContent=fmtTime(s.elapsed||0);
|
|
|
|
| 791 |
$('detailSuccess').textContent=s.stats?.success||0;
|
| 792 |
$('detailFailed').textContent=s.stats?.failed||0;
|
| 793 |
$('detailAborted').textContent=s.stats?.aborted||0;
|
|
|
|
| 819 |
}else if(log.message){
|
| 820 |
displayContent=log.message;
|
| 821 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 822 |
return`
|
| 823 |
<div class="log ${statusClass}">
|
| 824 |
<div class="t"><span>线程 #${id}</span><span>${log.status}</span></div>
|
| 825 |
+
<div class="m">循环:${log.loop||'-'}</div>
|
| 826 |
<pre>${displayContent.slice(-500)}</pre>
|
| 827 |
</div>
|
| 828 |
`;
|
|
|
|
| 855 |
badge.textContent=statusText;
|
| 856 |
badge.className='badge '+status;
|
| 857 |
|
|
|
|
| 858 |
const statusCard=$('statusCard');
|
| 859 |
statusCard.className='card status-card '+status;
|
| 860 |
$('detailStatusDot').className='status-dot '+status;
|
| 861 |
$('detailBadge').className='task-badge '+status;
|
| 862 |
$('detailBadge').textContent=statusText;
|
| 863 |
$('bannerLabel').textContent=statusText;
|
| 864 |
+
|
| 865 |
+
if(task.config){
|
| 866 |
+
$('infoModel').textContent=task.config.model||'-';
|
| 867 |
+
$('infoMaxTokens').textContent=task.config.max||'-';
|
| 868 |
+
$('infoMaxLimit').textContent=fmt(task.config.maxTokens||0);
|
| 869 |
+
$('infoThreads').textContent=task.config.threads||'-';
|
| 870 |
+
$('infoLoop').textContent=task.config.loop||'-';
|
| 871 |
+
$('infoTemp').textContent=task.config.temp||'-';
|
| 872 |
+
}
|
| 873 |
|
| 874 |
updateButtons(task.running, task.currentStats?.paused, task.stopped);
|
| 875 |
}
|
|
|
|
| 884 |
updateLogs(s.threadLogs);
|
| 885 |
updateButtons(s.running, s.paused, s.stopped);
|
| 886 |
|
|
|
|
| 887 |
let status='idle', statusText='空闲';
|
| 888 |
if(s.running && s.paused){
|
| 889 |
status='paused';
|
|
|
|
| 902 |
$('detailBadge').textContent=statusText;
|
| 903 |
$('bannerLabel').textContent=statusText;
|
| 904 |
|
| 905 |
+
const task=tasks.find(t=>t.id===taskId);
|
| 906 |
+
if(task?.config){
|
| 907 |
+
$('infoModel').textContent=task.config.model||'-';
|
| 908 |
+
$('infoMaxTokens').textContent=task.config.max||'-';
|
| 909 |
+
$('infoMaxLimit').textContent=fmt(task.config.maxTokens||0);
|
| 910 |
+
$('infoThreads').textContent=task.config.threads||'-';
|
| 911 |
+
$('infoLoop').textContent=task.config.loop||'-';
|
| 912 |
+
$('infoTemp').textContent=task.config.temp||'-';
|
| 913 |
+
}
|
| 914 |
+
|
| 915 |
if(!s.running){
|
| 916 |
$('start').textContent='启动任务';
|
| 917 |
await loadTasks();
|
|
|
|
| 1088 |
$('bannerProgress').textContent='0/0';
|
| 1089 |
$('bannerTokens').textContent='0/0';
|
| 1090 |
$('bannerTime').textContent='0s';
|
|
|
|
| 1091 |
$('detailSuccess').textContent='0';
|
| 1092 |
$('detailFailed').textContent='0';
|
| 1093 |
$('detailAborted').textContent='0';
|
| 1094 |
|
| 1095 |
+
// Update config info
|
| 1096 |
+
$('infoModel').textContent=local.model||'-';
|
| 1097 |
+
$('infoMaxTokens').textContent=local.max||1024;
|
| 1098 |
+
$('infoMaxLimit').textContent=fmt(local.maxTokens||1000000000);
|
| 1099 |
+
$('infoThreads').textContent=local.threads||3;
|
| 1100 |
+
$('infoLoop').textContent=local.loop||10;
|
| 1101 |
+
$('infoTemp').textContent=local.temp||1;
|
| 1102 |
+
|
| 1103 |
updateButtons(false, false, false);
|
| 1104 |
showEditView();
|
| 1105 |
stopPolling();
|
|
|
|
| 1127 |
$('detailPauseBtn').onclick=pauseTask;
|
| 1128 |
$('detailStopBtn').onclick=stopTask;
|
| 1129 |
|
| 1130 |
+
// Config info update on input change
|
| 1131 |
+
['modelAuto','max','maxTokens','thr','loop','temp'].forEach(id=>{
|
| 1132 |
+
$(id).addEventListener('input',updateConfigInfo);
|
| 1133 |
+
});
|
| 1134 |
+
|
| 1135 |
// Memory status update
|
| 1136 |
function formatBytes(bytes){
|
| 1137 |
if(bytes>=1e9)return(bytes/1e9).toFixed(1)+' GB';
|
|
|
|
| 1184 |
$('base').value=local.base||'https://api.openai.com';
|
| 1185 |
$('token').value=local.token||'';
|
| 1186 |
}
|
| 1187 |
+
if(local.model)$('modelAuto').value=local.model;
|
| 1188 |
+
if(local.sys)$('sys').value=local.sys;
|
| 1189 |
+
if(local.usr)$('usr').value=local.usr;
|
| 1190 |
+
if(local.loop)$('loop').value=local.loop;
|
| 1191 |
+
if(local.threads)$('thr').value=local.threads;
|
| 1192 |
+
if(local.max)$('max').value=local.max;
|
| 1193 |
+
if(local.temp)$('temp').value=local.temp;
|
| 1194 |
+
if(local.timeout)$('to').value=local.timeout/1000;
|
| 1195 |
+
if(local.waitBetween)$('waitBetween').value=local.waitBetween;
|
| 1196 |
+
if(local.maxTokens)$('maxTokens').value=local.maxTokens;
|
| 1197 |
+
if(local.ratioTarget)$('ratioTarget').value=local.ratioTarget;
|
| 1198 |
+
if(local.randOn!==undefined)$('randOn').checked=local.randOn;
|
| 1199 |
+
if(local.streamOn!==undefined)$('streamOn').checked=local.streamOn;
|
| 1200 |
+
|
| 1201 |
// Load server config
|
| 1202 |
try{
|
| 1203 |
const serverConfig=await apiGet('/api/config');
|
| 1204 |
if(Object.keys(serverConfig).length){
|
| 1205 |
saveLocal({...local,...serverConfig});
|
| 1206 |
+
if(serverConfig.base)$('base').value=serverConfig.base;
|
| 1207 |
+
if(serverConfig.token)$('token').value=serverConfig.token;
|
| 1208 |
+
if(serverConfig.model)$('modelAuto').value=serverConfig.model;
|
| 1209 |
+
if(serverConfig.sys)$('sys').value=serverConfig.sys;
|
| 1210 |
+
if(serverConfig.usr)$('usr').value=serverConfig.usr;
|
| 1211 |
+
if(serverConfig.loop)$('loop').value=serverConfig.loop;
|
| 1212 |
+
if(serverConfig.threads)$('thr').value=serverConfig.threads;
|
| 1213 |
+
if(serverConfig.max)$('max').value=serverConfig.max;
|
| 1214 |
+
if(serverConfig.temp)$('temp').value=serverConfig.temp;
|
| 1215 |
+
if(serverConfig.timeout)$('to').value=serverConfig.timeout/1000;
|
| 1216 |
+
if(serverConfig.waitBetween)$('waitBetween').value=serverConfig.waitBetween;
|
| 1217 |
+
if(serverConfig.maxTokens)$('maxTokens').value=serverConfig.maxTokens;
|
| 1218 |
+
if(serverConfig.ratioTarget)$('ratioTarget').value=serverConfig.ratioTarget;
|
| 1219 |
+
if(serverConfig.randOn!==undefined)$('randOn').checked=serverConfig.randOn;
|
| 1220 |
+
if(serverConfig.streamOn!==undefined)$('streamOn').checked=serverConfig.streamOn;
|
| 1221 |
}
|
| 1222 |
}catch(e){}
|
| 1223 |
|