Spaces:
Sleeping
Sleeping
Upload 13 files
Browse files- data/tasks.json +0 -0
- executor.js +228 -26
- logger.js +1 -1
- package-lock.json +49 -0
- package.json +5 -2
- server.js +38 -4
- test-api.js +81 -0
- test-token-rotation.js +42 -0
- token-consumer.html +38 -8
data/tasks.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
executor.js
CHANGED
|
@@ -2,6 +2,81 @@ const https = require('https');
|
|
| 2 |
const http = require('http');
|
| 3 |
const log = require('./logger');
|
| 4 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
function buildUrl(baseUrl, endpoint) {
|
| 6 |
let url = baseUrl.trim().replace(/\/ $/, '');
|
| 7 |
if (!url) throw new Error('Base URL 不能为空');
|
|
@@ -60,13 +135,16 @@ function makeRequest(url, options, body, timeout = 60000) {
|
|
| 60 |
const isHttps = urlObj.protocol === 'https:';
|
| 61 |
const lib = isHttps ? https : http;
|
| 62 |
|
|
|
|
|
|
|
| 63 |
const reqOptions = {
|
| 64 |
hostname: urlObj.hostname,
|
| 65 |
port: urlObj.port || (isHttps ? 443 : 80),
|
| 66 |
path: urlObj.pathname + urlObj.search,
|
| 67 |
method: options.method || 'GET',
|
| 68 |
-
headers:
|
| 69 |
-
timeout
|
|
|
|
| 70 |
};
|
| 71 |
|
| 72 |
const req = lib.request(reqOptions, (res) => {
|
|
@@ -89,9 +167,14 @@ async function fetchModels(baseUrl, token) {
|
|
| 89 |
try {
|
| 90 |
log.debug('Fetching models from:', baseUrl);
|
| 91 |
const url = buildUrl(baseUrl, '/models');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
const resp = await makeRequest(url, {
|
| 93 |
method: 'GET',
|
| 94 |
-
headers: { 'Authorization': 'Bearer ' +
|
| 95 |
});
|
| 96 |
const data = JSON.parse(resp.body);
|
| 97 |
if (resp.status !== 200) {
|
|
@@ -109,9 +192,18 @@ async function fetchModels(baseUrl, token) {
|
|
| 109 |
}
|
| 110 |
}
|
| 111 |
|
| 112 |
-
async function requestOnce(config, signal) {
|
| 113 |
const { prompt, tag, marker } = buildUserPrompt(config.usr, config.randOn, config.markerLen);
|
| 114 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
const body = {
|
| 116 |
model: config.model,
|
| 117 |
messages: [
|
|
@@ -127,7 +219,7 @@ async function requestOnce(config, signal) {
|
|
| 127 |
const resp = await makeRequest(url, {
|
| 128 |
method: 'POST',
|
| 129 |
headers: {
|
| 130 |
-
'Authorization': 'Bearer ' +
|
| 131 |
'Content-Type': 'application/json'
|
| 132 |
}
|
| 133 |
}, JSON.stringify(body), config.timeout);
|
|
@@ -156,12 +248,13 @@ async function requestOnce(config, signal) {
|
|
| 156 |
}
|
| 157 |
|
| 158 |
const content = data?.choices?.[0]?.message?.content || '';
|
| 159 |
-
const
|
|
|
|
| 160 |
|
| 161 |
-
return { content, usage, finish_reason: data?.choices?.[0]?.finish_reason, tag, marker, prompt };
|
| 162 |
}
|
| 163 |
|
| 164 |
-
async function streamRequest(config, onChunk, signal) {
|
| 165 |
const { prompt, tag, marker } = buildUserPrompt(config.usr, config.randOn, config.markerLen);
|
| 166 |
|
| 167 |
const body = {
|
|
@@ -181,6 +274,15 @@ async function streamRequest(config, onChunk, signal) {
|
|
| 181 |
const isHttps = urlObj.protocol === 'https:';
|
| 182 |
const lib = isHttps ? https : http;
|
| 183 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
return new Promise((resolve, reject) => {
|
| 185 |
// Check if already aborted before starting
|
| 186 |
if (signal?.aborted) {
|
|
@@ -194,13 +296,16 @@ async function streamRequest(config, onChunk, signal) {
|
|
| 194 |
path: urlObj.pathname,
|
| 195 |
method: 'POST',
|
| 196 |
headers: {
|
| 197 |
-
'
|
|
|
|
| 198 |
'Content-Type': 'application/json'
|
| 199 |
},
|
| 200 |
-
timeout: config.timeout || 60000
|
|
|
|
| 201 |
};
|
| 202 |
|
| 203 |
let fullContent = '';
|
|
|
|
| 204 |
let usage = { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };
|
| 205 |
let finishReason = null;
|
| 206 |
let aborted = false;
|
|
@@ -248,10 +353,23 @@ async function streamRequest(config, onChunk, signal) {
|
|
| 248 |
const json = JSON.parse(payload);
|
| 249 |
if (json.error) throw new Error(json.error.message);
|
| 250 |
|
| 251 |
-
const delta = json.choices?.[0]?.delta
|
| 252 |
-
if (delta) {
|
| 253 |
-
fullContent += delta;
|
| 254 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 255 |
}
|
| 256 |
|
| 257 |
if (json.usage) usage = json.usage;
|
|
@@ -271,9 +389,10 @@ async function streamRequest(config, onChunk, signal) {
|
|
| 271 |
|
| 272 |
res.on('end', () => {
|
| 273 |
cleanup();
|
| 274 |
-
const finalUsage = usageWithFallback(usage, prompt, config.sys, fullContent);
|
| 275 |
resolve({
|
| 276 |
content: fullContent,
|
|
|
|
| 277 |
usage: finalUsage,
|
| 278 |
finish_reason: finishReason,
|
| 279 |
tag,
|
|
@@ -327,6 +446,11 @@ class TaskExecutor {
|
|
| 327 |
this.notifyTimer = null;
|
| 328 |
this.notifyInterval = 500; // ms between notifications (increased for high concurrency)
|
| 329 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 330 |
// Load from saved progress or initialize fresh
|
| 331 |
if (savedProgress) {
|
| 332 |
this.stats = savedProgress.stats || {
|
|
@@ -389,8 +513,8 @@ class TaskExecutor {
|
|
| 389 |
thread: threadId,
|
| 390 |
loop: i,
|
| 391 |
status: 'running',
|
| 392 |
-
|
| 393 |
-
|
| 394 |
};
|
| 395 |
|
| 396 |
this.threadLogs[threadId] = threadLog;
|
|
@@ -401,21 +525,24 @@ class TaskExecutor {
|
|
| 401 |
const result = this.config.streamOn
|
| 402 |
? await streamRequest(
|
| 403 |
{ ...this.config, markerLen: this.markerLen },
|
| 404 |
-
(delta,
|
| 405 |
threadLog.status = 'streaming';
|
| 406 |
-
threadLog.content =
|
| 407 |
// notifyUpdate already throttled, so this is fine
|
| 408 |
this.notifyUpdate();
|
| 409 |
},
|
| 410 |
-
this.controller.signal
|
|
|
|
| 411 |
)
|
| 412 |
: await requestOnce(
|
| 413 |
{ ...this.config, markerLen: this.markerLen },
|
| 414 |
-
this.controller.signal
|
|
|
|
| 415 |
);
|
| 416 |
|
| 417 |
this.stats.completed++;
|
| 418 |
this.stats.success++;
|
|
|
|
| 419 |
this.stats.promptTokens += result.usage.prompt_tokens;
|
| 420 |
this.stats.completionTokens += result.usage.completion_tokens;
|
| 421 |
this.stats.totalTokens += result.usage.total_tokens;
|
|
@@ -430,7 +557,15 @@ class TaskExecutor {
|
|
| 430 |
log.info('Task', this.taskId, 'reached token limit:', this.stats.totalTokens);
|
| 431 |
} else {
|
| 432 |
threadLog.status = 'success';
|
| 433 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 434 |
}
|
| 435 |
|
| 436 |
// Adjust marker for ratio
|
|
@@ -442,13 +577,16 @@ class TaskExecutor {
|
|
| 442 |
if (this.config.randOn) {
|
| 443 |
const diff = liveRatio - this.ratio.target;
|
| 444 |
if (Math.abs(diff) >= 0.02) {
|
| 445 |
-
const step = Math.max(1, Math.round(Math.abs(diff) *
|
| 446 |
-
if (diff < 0) this.markerLen = Math.min(
|
| 447 |
else this.markerLen = Math.max(0, this.markerLen - step);
|
| 448 |
}
|
| 449 |
this.ratio.markerLen = this.markerLen;
|
| 450 |
}
|
| 451 |
|
|
|
|
|
|
|
|
|
|
| 452 |
} catch (e) {
|
| 453 |
const isAborted = e.message === 'Aborted' || e.name === 'AbortError';
|
| 454 |
|
|
@@ -479,6 +617,30 @@ class TaskExecutor {
|
|
| 479 |
log.debug('Thread', threadId, 'loop', i, 'aborted');
|
| 480 |
}
|
| 481 |
} else {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 482 |
this.stats.failed++;
|
| 483 |
threadLog.status = 'error';
|
| 484 |
threadLog.error = e.message || '请求失败';
|
|
@@ -486,7 +648,6 @@ class TaskExecutor {
|
|
| 486 |
}
|
| 487 |
}
|
| 488 |
|
| 489 |
-
threadLog.endTime = Date.now();
|
| 490 |
this.notifyUpdate(true); // Force update after each request completes
|
| 491 |
|
| 492 |
// Wait between loops (not after the last loop)
|
|
@@ -531,6 +692,9 @@ class TaskExecutor {
|
|
| 531 |
}
|
| 532 |
|
| 533 |
this.notifyUpdate();
|
|
|
|
|
|
|
|
|
|
| 534 |
|
| 535 |
// Run workers in parallel
|
| 536 |
const workers = [];
|
|
@@ -541,6 +705,7 @@ class TaskExecutor {
|
|
| 541 |
await Promise.all(workers);
|
| 542 |
|
| 543 |
this.running = false;
|
|
|
|
| 544 |
log.info('TaskExecutor finished:', this.taskId, 'success:', this.stats.success, 'failed:', this.stats.failed, 'tokens:', this.stats.totalTokens);
|
| 545 |
this.notifyUpdate(true); // Force final update
|
| 546 |
|
|
@@ -552,6 +717,7 @@ class TaskExecutor {
|
|
| 552 |
if (!this.running || this.paused) return;
|
| 553 |
log.info('TaskExecutor pausing:', this.taskId);
|
| 554 |
this.paused = true;
|
|
|
|
| 555 |
if (this.startTime) {
|
| 556 |
this.elapsedBefore += Date.now() - this.startTime;
|
| 557 |
this.startTime = null;
|
|
@@ -565,6 +731,8 @@ class TaskExecutor {
|
|
| 565 |
if (!this.running || !this.paused) return;
|
| 566 |
log.info('TaskExecutor resuming:', this.taskId);
|
| 567 |
this.paused = false;
|
|
|
|
|
|
|
| 568 |
// Create new AbortController for resumed execution
|
| 569 |
this.controller = new AbortController();
|
| 570 |
this.startTime = Date.now();
|
|
@@ -575,9 +743,43 @@ class TaskExecutor {
|
|
| 575 |
log.info('TaskExecutor stopping:', this.taskId);
|
| 576 |
this.stopped = true;
|
| 577 |
this.paused = false;
|
|
|
|
| 578 |
this.controller.abort();
|
| 579 |
}
|
| 580 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 581 |
getStatus() {
|
| 582 |
let elapsed = this.elapsedBefore;
|
| 583 |
if (this.startTime && !this.paused) {
|
|
@@ -642,4 +844,4 @@ class TaskExecutor {
|
|
| 642 |
}
|
| 643 |
}
|
| 644 |
|
| 645 |
-
module.exports = { TaskExecutor, fetchModels };
|
|
|
|
| 2 |
const http = require('http');
|
| 3 |
const log = require('./logger');
|
| 4 |
|
| 5 |
+
const CHERRY_STUDIO_USER_AGENT = 'cherry studio/1.0(secure client)';
|
| 6 |
+
|
| 7 |
+
// 全局 HTTP Agent 配置 - 支持高并发连接复用
|
| 8 |
+
const httpsAgent = new https.Agent({
|
| 9 |
+
keepAlive: true, // 启用连接复用
|
| 10 |
+
maxSockets: 600, // 每个主机最大连接数(略高于线程数)
|
| 11 |
+
maxFreeSockets: 100, // 空闲连接池大小
|
| 12 |
+
keepAliveMsecs: 30000, // keepAlive 探测间隔
|
| 13 |
+
timeout: 10000, // 连接建立超时
|
| 14 |
+
});
|
| 15 |
+
|
| 16 |
+
const httpAgent = new http.Agent({
|
| 17 |
+
keepAlive: true,
|
| 18 |
+
maxSockets: 600,
|
| 19 |
+
maxFreeSockets: 100,
|
| 20 |
+
keepAliveMsecs: 30000,
|
| 21 |
+
timeout: 10000,
|
| 22 |
+
});
|
| 23 |
+
|
| 24 |
+
// 最大重试次数
|
| 25 |
+
const MAX_RETRIES = 3;
|
| 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 |
+
// Max length of content to store in thread logs (for frontend display)
|
| 48 |
+
const LOG_CONTENT_MAX_LENGTH = 20;
|
| 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 |
+
// Get next token from rotation, returns both token and index
|
| 57 |
+
function getNextToken(config) {
|
| 58 |
+
// Split tokens by comma and trim whitespace
|
| 59 |
+
const tokens = String(config.token || '').split(',').map(t => t.trim()).filter(Boolean);
|
| 60 |
+
|
| 61 |
+
// If only one token, return it directly
|
| 62 |
+
if (tokens.length <= 1) {
|
| 63 |
+
return { token: tokens[0] || config.token, index: 0, total: tokens.length || 1 };
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
// Get current counter for this config (using a hash of token string as key)
|
| 67 |
+
const tokenKey = tokens.join(',');
|
| 68 |
+
let counter = tokenCounters.get(tokenKey) || 0;
|
| 69 |
+
|
| 70 |
+
// Get current token
|
| 71 |
+
const index = counter % tokens.length;
|
| 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) {
|
| 81 |
let url = baseUrl.trim().replace(/\/ $/, '');
|
| 82 |
if (!url) throw new Error('Base URL 不能为空');
|
|
|
|
| 135 |
const isHttps = urlObj.protocol === 'https:';
|
| 136 |
const lib = isHttps ? https : http;
|
| 137 |
|
| 138 |
+
const mergedHeaders = { 'User-Agent': CHERRY_STUDIO_USER_AGENT, ...(options.headers || {}) };
|
| 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: mergedHeaders,
|
| 146 |
+
timeout,
|
| 147 |
+
agent: isHttps ? httpsAgent : httpAgent
|
| 148 |
};
|
| 149 |
|
| 150 |
const req = lib.request(reqOptions, (res) => {
|
|
|
|
| 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 ' + useToken }
|
| 178 |
});
|
| 179 |
const data = JSON.parse(resp.body);
|
| 180 |
if (resp.status !== 200) {
|
|
|
|
| 192 |
}
|
| 193 |
}
|
| 194 |
|
| 195 |
+
async function requestOnce(config, signal, threadLog) {
|
| 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 |
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 |
}
|
| 249 |
|
| 250 |
const content = data?.choices?.[0]?.message?.content || '';
|
| 251 |
+
const reasoning = data?.choices?.[0]?.message?.reasoning_content || '';
|
| 252 |
+
const usage = usageWithFallback(data?.usage, prompt, config.sys, content + reasoning);
|
| 253 |
|
| 254 |
+
return { content, reasoning, usage, finish_reason: data?.choices?.[0]?.finish_reason, tag, marker, prompt };
|
| 255 |
}
|
| 256 |
|
| 257 |
+
async function streamRequest(config, onChunk, signal, threadLog) {
|
| 258 |
const { prompt, tag, marker } = buildUserPrompt(config.usr, config.randOn, config.markerLen);
|
| 259 |
|
| 260 |
const body = {
|
|
|
|
| 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) {
|
|
|
|
| 296 |
path: urlObj.pathname,
|
| 297 |
method: 'POST',
|
| 298 |
headers: {
|
| 299 |
+
'User-Agent': CHERRY_STUDIO_USER_AGENT,
|
| 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 |
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?.content) {
|
| 358 |
+
fullContent += delta.content;
|
| 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 |
|
| 390 |
res.on('end', () => {
|
| 391 |
cleanup();
|
| 392 |
+
const finalUsage = usageWithFallback(usage, prompt, config.sys, fullContent + fullReasoning);
|
| 393 |
resolve({
|
| 394 |
content: fullContent,
|
| 395 |
+
reasoning: fullReasoning,
|
| 396 |
usage: finalUsage,
|
| 397 |
finish_reason: finishReason,
|
| 398 |
tag,
|
|
|
|
| 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 |
thread: threadId,
|
| 514 |
loop: i,
|
| 515 |
status: 'running',
|
| 516 |
+
message: '',
|
| 517 |
+
markerLen: this.markerLen
|
| 518 |
};
|
| 519 |
|
| 520 |
this.threadLogs[threadId] = threadLog;
|
|
|
|
| 525 |
const result = this.config.streamOn
|
| 526 |
? await streamRequest(
|
| 527 |
{ ...this.config, markerLen: this.markerLen },
|
| 528 |
+
(delta, fullOutput) => {
|
| 529 |
threadLog.status = 'streaming';
|
| 530 |
+
threadLog.content = fullOutput?.slice(-LOG_CONTENT_MAX_LENGTH);
|
| 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 |
log.info('Task', this.taskId, 'reached token limit:', this.stats.totalTokens);
|
| 558 |
} else {
|
| 559 |
threadLog.status = 'success';
|
| 560 |
+
// Combine reasoning and content, then slice
|
| 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 |
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) * RATIO_STEP_MULTIPLIER));
|
| 581 |
+
if (diff < 0) this.markerLen = Math.min(100000, this.markerLen + step);
|
| 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 |
|
|
|
|
| 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 |
}
|
| 649 |
}
|
| 650 |
|
|
|
|
| 651 |
this.notifyUpdate(true); // Force update after each request completes
|
| 652 |
|
| 653 |
// Wait between loops (not after the last loop)
|
|
|
|
| 692 |
}
|
| 693 |
|
| 694 |
this.notifyUpdate();
|
| 695 |
+
|
| 696 |
+
// 启动停滞检查定时器
|
| 697 |
+
this.startStallCheck();
|
| 698 |
|
| 699 |
// Run workers in parallel
|
| 700 |
const workers = [];
|
|
|
|
| 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 |
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 |
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();
|
|
|
|
| 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) {
|
|
|
|
| 844 |
}
|
| 845 |
}
|
| 846 |
|
| 847 |
+
module.exports = { TaskExecutor, fetchModels, getNextToken };
|
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.error;
|
| 6 |
|
| 7 |
const ts = () => new Date().toISOString();
|
| 8 |
|
package-lock.json
CHANGED
|
@@ -8,6 +8,7 @@
|
|
| 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,6 +187,45 @@
|
|
| 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,6 +934,15 @@
|
|
| 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",
|
|
|
|
| 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 |
"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 |
"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",
|
package.json
CHANGED
|
@@ -8,11 +8,14 @@
|
|
| 8 |
"dev": "cross-env LOG_LEVEL=debug nodemon server.js"
|
| 9 |
},
|
| 10 |
"nodemonConfig": {
|
| 11 |
-
"ignore": [
|
|
|
|
|
|
|
| 12 |
},
|
| 13 |
"dependencies": {
|
| 14 |
-
"
|
| 15 |
"cors": "^2.8.5",
|
|
|
|
| 16 |
"uuid": "^9.0.0"
|
| 17 |
},
|
| 18 |
"devDependencies": {
|
|
|
|
| 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": {
|
server.js
CHANGED
|
@@ -1,15 +1,25 @@
|
|
| 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,8 +58,9 @@ function rateLimiter(req, res, next) {
|
|
| 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,7 +126,7 @@ app.post('/api/tasks', (req, res) => {
|
|
| 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(
|
| 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,10 +171,23 @@ app.get('/api/tasks', (req, res) => {
|
|
| 160 |
if (!t.stats.total && t.config) {
|
| 161 |
t.stats.total = (t.config.loop || 10) * (t.config.threads || 3);
|
| 162 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
return {
|
| 164 |
-
...
|
| 165 |
running: executor?.running || false,
|
| 166 |
-
currentStats:
|
| 167 |
};
|
| 168 |
});
|
| 169 |
log.debug('GET /api/tasks, count:', result.length);
|
|
@@ -473,6 +497,16 @@ app.get('/', (req, res) => {
|
|
| 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();
|
|
|
|
| 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 |
}
|
| 59 |
|
| 60 |
// Middleware
|
| 61 |
+
app.use(compression());
|
| 62 |
app.use(cors());
|
| 63 |
+
app.use(express.json({ limit: '50mb' }));
|
| 64 |
app.use(rateLimiter);
|
| 65 |
|
| 66 |
// HTTP Basic Auth middleware for /api routes
|
|
|
|
| 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(MAX_THREADS, Number(config.threads) || 3)),
|
| 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 |
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 |
+
...taskCopy,
|
| 189 |
running: executor?.running || false,
|
| 190 |
+
currentStats: status
|
| 191 |
};
|
| 192 |
});
|
| 193 |
log.debug('GET /api/tasks, count:', result.length);
|
|
|
|
| 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();
|
test-api.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const https = require('https');
|
| 2 |
+
|
| 3 |
+
const BASE_URL = 'https://s2a.bbx.qzz.io';
|
| 4 |
+
const TOKEN = 'sk-b2ff52c85b4a654733d25bd7fc85f52d70fe7900613ae681a070b5e5ed880e47';
|
| 5 |
+
|
| 6 |
+
function testRequest(headers) {
|
| 7 |
+
return new Promise((resolve, reject) => {
|
| 8 |
+
const urlObj = new URL('/v1/models', BASE_URL);
|
| 9 |
+
const options = {
|
| 10 |
+
hostname: urlObj.hostname,
|
| 11 |
+
port: 443,
|
| 12 |
+
path: urlObj.pathname,
|
| 13 |
+
method: 'GET',
|
| 14 |
+
headers: {
|
| 15 |
+
'Authorization': 'Bearer ' + TOKEN,
|
| 16 |
+
...headers
|
| 17 |
+
}
|
| 18 |
+
};
|
| 19 |
+
|
| 20 |
+
console.log('Testing with headers:', JSON.stringify(headers, null, 2));
|
| 21 |
+
|
| 22 |
+
const req = https.request(options, (res) => {
|
| 23 |
+
let data = '';
|
| 24 |
+
res.on('data', chunk => data += chunk);
|
| 25 |
+
res.on('end', () => {
|
| 26 |
+
console.log(`Status: ${res.statusCode}`);
|
| 27 |
+
console.log('Response headers:', res.headers);
|
| 28 |
+
if (data) {
|
| 29 |
+
console.log('Response body:', data.slice(0, 500));
|
| 30 |
+
}
|
| 31 |
+
resolve({ status: res.statusCode, headers: res.headers, body: data });
|
| 32 |
+
});
|
| 33 |
+
});
|
| 34 |
+
|
| 35 |
+
req.on('error', reject);
|
| 36 |
+
req.end();
|
| 37 |
+
});
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
async function runTests() {
|
| 41 |
+
console.log('=== Test 1: Cherry Studio User-Agent only ===');
|
| 42 |
+
await testRequest({
|
| 43 |
+
'User-Agent': 'cherry studio/1.0(secure client)'
|
| 44 |
+
});
|
| 45 |
+
|
| 46 |
+
console.log('\n=== Test 2: Modern Chrome browser ===');
|
| 47 |
+
await testRequest({
|
| 48 |
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
|
| 49 |
+
'Accept': 'application/json, text/plain, */*',
|
| 50 |
+
'Accept-Language': 'en-US,en;q=0.9',
|
| 51 |
+
'Accept-Encoding': 'gzip, deflate, br',
|
| 52 |
+
'Referer': 'https://chat.openai.com/',
|
| 53 |
+
'Origin': 'https://chat.openai.com',
|
| 54 |
+
'Sec-Fetch-Dest': 'empty',
|
| 55 |
+
'Sec-Fetch-Mode': 'cors',
|
| 56 |
+
'Sec-Fetch-Site': 'same-origin',
|
| 57 |
+
'Connection': 'keep-alive',
|
| 58 |
+
'Cache-Control': 'no-cache'
|
| 59 |
+
});
|
| 60 |
+
|
| 61 |
+
console.log('\n=== Test 3: More complete browser headers ===');
|
| 62 |
+
await testRequest({
|
| 63 |
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
|
| 64 |
+
'Accept': 'application/json, text/plain, */*',
|
| 65 |
+
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
| 66 |
+
'Accept-Encoding': 'gzip, deflate, br',
|
| 67 |
+
'Referer': 'https://platform.openai.com/',
|
| 68 |
+
'Origin': 'https://platform.openai.com',
|
| 69 |
+
'Sec-Ch-Ua': '"Chromium";v="122", "Not(A:Brand";v="24", "Google Chrome";v="122"',
|
| 70 |
+
'Sec-Ch-Ua-Mobile': '?0',
|
| 71 |
+
'Sec-Ch-Ua-Platform': '"Windows"',
|
| 72 |
+
'Sec-Fetch-Dest': 'empty',
|
| 73 |
+
'Sec-Fetch-Mode': 'cors',
|
| 74 |
+
'Sec-Fetch-Site': 'same-site',
|
| 75 |
+
'Connection': 'keep-alive',
|
| 76 |
+
'Cache-Control': 'no-cache',
|
| 77 |
+
'Pragma': 'no-cache'
|
| 78 |
+
});
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
runTests().catch(console.error);
|
test-token-rotation.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const { getNextToken } = require('./executor');
|
| 2 |
+
|
| 3 |
+
// Test token rotation
|
| 4 |
+
function testTokenRotation() {
|
| 5 |
+
console.log('Testing token rotation...');
|
| 6 |
+
|
| 7 |
+
// Test with multiple tokens
|
| 8 |
+
const config = {
|
| 9 |
+
token: 'token1, token2, token3'
|
| 10 |
+
};
|
| 11 |
+
|
| 12 |
+
console.log('Testing with tokens:', config.token);
|
| 13 |
+
|
| 14 |
+
// Get tokens multiple times to test rotation
|
| 15 |
+
for (let i = 0; i < 10; i++) {
|
| 16 |
+
const { token, index, total } = getNextToken(config);
|
| 17 |
+
console.log(`Request ${i+1}: Using token: ${token} (Key: ${index+1}/${total})`);
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
// Test with single token
|
| 21 |
+
const singleTokenConfig = {
|
| 22 |
+
token: 'single-token'
|
| 23 |
+
};
|
| 24 |
+
|
| 25 |
+
console.log('\nTesting with single token:', singleTokenConfig.token);
|
| 26 |
+
|
| 27 |
+
for (let i = 0; i < 3; i++) {
|
| 28 |
+
const { token, index, total } = getNextToken(singleTokenConfig);
|
| 29 |
+
console.log(`Request ${i+1}: Using token: ${token} (Key: ${index+1}/${total})`);
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
// Test with empty token
|
| 33 |
+
const emptyTokenConfig = {
|
| 34 |
+
token: ''
|
| 35 |
+
};
|
| 36 |
+
|
| 37 |
+
console.log('\nTesting with empty token:');
|
| 38 |
+
const { token, index, total } = getNextToken(emptyTokenConfig);
|
| 39 |
+
console.log(`Using token: ${token} (Key: ${index+1}/${total})`);
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
testTokenRotation();
|
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}
|
|
@@ -238,6 +238,10 @@ 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"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
|
| 243 |
<span>成功 <b id="detailSuccess" style="color:#4ade80">0</b></span>
|
|
@@ -286,7 +290,7 @@ pre::-webkit-scrollbar-thumb{background:#475569;border-radius:3px}
|
|
| 286 |
</div>
|
| 287 |
<div class="g3" style="margin-top:8px">
|
| 288 |
<label>每线程循环次数<input id="loop" type="number" min="1" value="10"></label>
|
| 289 |
-
<label>并发线程数<input id="thr" type="number" min="1" value="3"></label>
|
| 290 |
<label>max_tokens<input id="max" type="number" min="1" value="1024"></label>
|
| 291 |
<label>temperature<input id="temp" type="number" min="0" max="2" step="0.1" value="1"></label>
|
| 292 |
<label>单次超时秒数<input id="to" type="number" min="5" value="600"></label>
|
|
@@ -377,9 +381,20 @@ const getLocal=()=>{try{return JSON.parse(localStorage.getItem(LOCAL_KEY)||'{}')
|
|
| 377 |
const saveLocal=d=>{try{localStorage.setItem(LOCAL_KEY,JSON.stringify(d))}catch{}};
|
| 378 |
|
| 379 |
// API functions
|
| 380 |
-
async function
|
| 381 |
-
|
| 382 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 383 |
|
| 384 |
// Load tasks
|
| 385 |
async function loadTasks(){
|
|
@@ -480,6 +495,10 @@ function renderTaskList(){
|
|
| 480 |
<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>
|
| 481 |
<span>${elapsedStr||'-'}</span>
|
| 482 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 483 |
<div class="meta-item">
|
| 484 |
<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>
|
| 485 |
<span>成功 <b style="color:#4ade80">${stats.success||0}</b></span>
|
|
@@ -577,7 +596,7 @@ function getFormData(){
|
|
| 577 |
sys:$('sys').value.trim(),
|
| 578 |
usr:$('usr').value.trim(),
|
| 579 |
loop:n($('loop').value,10,1,10000),
|
| 580 |
-
threads:n($('thr').value,3,1,
|
| 581 |
max:n($('max').value,1024,1,32768),
|
| 582 |
temp:n($('temp').value,1,0,2),
|
| 583 |
timeout:n($('to').value,600,5,3600)*1000,
|
|
@@ -767,6 +786,7 @@ function updateStats(s){
|
|
| 767 |
$('bannerProgress').textContent=`${completed}/${total}`;
|
| 768 |
$('bannerTokens').textContent=`${fmt(totalTokens)}/${fmt(maxTokens)}`;
|
| 769 |
$('bannerTime').textContent=fmtTime(s.elapsed||0);
|
|
|
|
| 770 |
$('detailSuccess').textContent=s.stats?.success||0;
|
| 771 |
$('detailFailed').textContent=s.stats?.failed||0;
|
| 772 |
$('detailAborted').textContent=s.stats?.aborted||0;
|
|
@@ -798,10 +818,18 @@ function updateLogs(threadLogs){
|
|
| 798 |
}else if(log.message){
|
| 799 |
displayContent=log.message;
|
| 800 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 801 |
return`
|
| 802 |
<div class="log ${statusClass}">
|
| 803 |
<div class="t"><span>线程 #${id}</span><span>${log.status}</span></div>
|
| 804 |
-
<div class="m">
|
| 805 |
<pre>${displayContent.slice(-500)}</pre>
|
| 806 |
</div>
|
| 807 |
`;
|
|
@@ -841,6 +869,7 @@ function updateTaskHeader(task){
|
|
| 841 |
$('detailBadge').className='task-badge '+status;
|
| 842 |
$('detailBadge').textContent=statusText;
|
| 843 |
$('bannerLabel').textContent=statusText;
|
|
|
|
| 844 |
|
| 845 |
updateButtons(task.running, task.currentStats?.paused, task.stopped);
|
| 846 |
}
|
|
@@ -1050,6 +1079,7 @@ function newTask(){
|
|
| 1050 |
$('bannerProgress').textContent='0/0';
|
| 1051 |
$('bannerTokens').textContent='0/0';
|
| 1052 |
$('bannerTime').textContent='0s';
|
|
|
|
| 1053 |
$('detailSuccess').textContent='0';
|
| 1054 |
$('detailFailed').textContent='0';
|
| 1055 |
$('detailAborted').textContent='0';
|
|
|
|
| 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;height:180px;display:flex;flex-direction:column}
|
| 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;flex:1;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}
|
|
|
|
| 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 |
</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" max="1000" value="3"></label>
|
| 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 |
const saveLocal=d=>{try{localStorage.setItem(LOCAL_KEY,JSON.stringify(d))}catch{}};
|
| 382 |
|
| 383 |
// API functions
|
| 384 |
+
async function apiRequest(url,options={}){
|
| 385 |
+
const r=await fetch(API+url,options);
|
| 386 |
+
const ct=r.headers.get('content-type')||'';
|
| 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 |
<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>
|
|
|
|
| 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,1000),
|
| 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,
|
|
|
|
| 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 |
}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">${infoLine}</div>
|
| 833 |
<pre>${displayContent.slice(-500)}</pre>
|
| 834 |
</div>
|
| 835 |
`;
|
|
|
|
| 869 |
$('detailBadge').className='task-badge '+status;
|
| 870 |
$('detailBadge').textContent=statusText;
|
| 871 |
$('bannerLabel').textContent=statusText;
|
| 872 |
+
$('detailThreads').textContent=$('thr').value||0;
|
| 873 |
|
| 874 |
updateButtons(task.running, task.currentStats?.paused, task.stopped);
|
| 875 |
}
|
|
|
|
| 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';
|