qwen2api commited on
Commit
39af208
·
verified ·
1 Parent(s): 5815428

Upload 11 files

Browse files
Files changed (6) hide show
  1. executor.js +72 -233
  2. logger.js +1 -1
  3. package-lock.json +0 -49
  4. package.json +2 -5
  5. server.js +4 -38
  6. 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 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) {
@@ -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 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,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 ' + useToken }
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, 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,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 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,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
- '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,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?.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,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 + fullReasoning);
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
- message: '',
517
- markerLen: this.markerLen
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, 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,15 +477,7 @@ class TaskExecutor {
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,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) * 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
 
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); // Force update for pause
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--; // Retry current loop
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 }, // Shallow copy for thread safety
798
  threadProgress: { ...this.threadProgress },
799
  markerLen: this.markerLen
800
  };
@@ -844,4 +683,4 @@ class TaskExecutor {
844
  }
845
  }
846
 
847
- module.exports = { TaskExecutor, fetchModels, getNextToken };
 
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.error;
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({ limit: '50mb' }));
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(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,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
- ...taskCopy,
189
  running: executor?.running || false,
190
- currentStats: status
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;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,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;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}
@@ -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" 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,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 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,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,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,
@@ -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">${infoLine}</div>
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
- $('detailThreads').textContent=$('thr').value||0;
 
 
 
 
 
 
 
 
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