qwen2api commited on
Commit
e5f68c9
·
verified ·
1 Parent(s): f9b603f

Upload 11 files

Browse files
Files changed (11) hide show
  1. .dockerignore +7 -0
  2. .gitignore +2 -0
  3. Dockerfile +33 -0
  4. executor.js +645 -0
  5. logger.js +16 -0
  6. package-lock.json +1336 -0
  7. package.json +22 -0
  8. server.js +576 -0
  9. storage.js +190 -0
  10. task.md +8 -0
  11. token-consumer.html +1150 -0
.dockerignore ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ node_modules
2
+ npm-debug.log
3
+ .git
4
+ .gitignore
5
+ *.md
6
+ data
7
+ .env
.gitignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ node_modules/
2
+ data/
Dockerfile ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:20-alpine
2
+
3
+ WORKDIR /app
4
+
5
+ # Copy package files
6
+ COPY package*.json ./
7
+
8
+ # Install production dependencies only
9
+ RUN npm ci --only=production
10
+
11
+ # Copy application files
12
+ COPY server.js .
13
+ COPY executor.js .
14
+ COPY storage.js .
15
+ COPY logger.js .
16
+ COPY token-consumer.html .
17
+
18
+ # Create data directory
19
+ RUN mkdir -p /app/data
20
+
21
+ # Expose port
22
+ EXPOSE 7860
23
+
24
+ # Set environment
25
+ ENV NODE_ENV=production
26
+ ENV PORT=7860
27
+
28
+ # Health check
29
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
30
+ CMD wget --no-verbose --tries=1 --spider http://localhost:7860/health || exit 1
31
+
32
+ # Start server
33
+ CMD ["node", "server.js"]
executor.js ADDED
@@ -0,0 +1,645 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 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 不能为空');
8
+ if (!url.endsWith('/v1')) url += '/v1';
9
+ return url + endpoint;
10
+ }
11
+
12
+ function randHex(n) {
13
+ const chars = '0123456789abcdef';
14
+ let result = '';
15
+ for (let i = 0; i < n; i++) {
16
+ result += chars[Math.floor(Math.random() * 16)];
17
+ }
18
+ return result;
19
+ }
20
+
21
+ function randText(n) {
22
+ const len = Math.max(0, Math.min(4000, Math.floor(Number(n) || 0)));
23
+ if (len <= 0) return '';
24
+ return randHex(Math.ceil(len / 2)).slice(0, len);
25
+ }
26
+
27
+ function buildUserPrompt(text, useRand, markerLen) {
28
+ if (!useRand) return { prompt: text, tag: null, marker: null };
29
+ const tag = 'r' + randText(markerLen);
30
+ const marker = '[RANDOM_MARKER:' + tag + ']';
31
+ return { prompt: marker + text, tag, marker };
32
+ }
33
+
34
+ function estimateTokens(text) {
35
+ return Math.max(1, Math.ceil(String(text || '').length / 4));
36
+ }
37
+
38
+ function usageWithFallback(usage, reqPrompt, sysPrompt, respText) {
39
+ let p = Number(usage?.prompt_tokens || 0);
40
+ let c = Number(usage?.completion_tokens || 0);
41
+ let t = Number(usage?.total_tokens || 0);
42
+ let estimated = false;
43
+
44
+ if (p <= 0 && c <= 0 && t <= 0) {
45
+ p = estimateTokens((sysPrompt ? sysPrompt + '\n' : '') + (reqPrompt || ''));
46
+ c = estimateTokens(respText || '');
47
+ t = p + c;
48
+ estimated = true;
49
+ } else {
50
+ if (p <= 0) { p = estimateTokens((sysPrompt ? sysPrompt + '\n' : '') + (reqPrompt || '')); estimated = true; }
51
+ if (c <= 0) { c = estimateTokens(respText || ''); estimated = true; }
52
+ if (t <= 0) { t = p + c; estimated = true; }
53
+ }
54
+ return { prompt_tokens: p, completion_tokens: c, total_tokens: t, estimated };
55
+ }
56
+
57
+ function makeRequest(url, options, body, timeout = 60000) {
58
+ return new Promise((resolve, reject) => {
59
+ const urlObj = new URL(url);
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: options.headers || {},
69
+ timeout
70
+ };
71
+
72
+ const req = lib.request(reqOptions, (res) => {
73
+ let data = '';
74
+ res.on('data', chunk => data += chunk);
75
+ res.on('end', () => {
76
+ resolve({ status: res.statusCode, headers: res.headers, body: data });
77
+ });
78
+ });
79
+
80
+ req.on('error', reject);
81
+ req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); });
82
+
83
+ if (body) req.write(body);
84
+ req.end();
85
+ });
86
+ }
87
+
88
+ 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 ' + token }
95
+ });
96
+ const data = JSON.parse(resp.body);
97
+ if (resp.status !== 200) {
98
+ throw new Error(data?.error?.message || 'HTTP ' + resp.status);
99
+ }
100
+ const ids = (Array.isArray(data?.data) ? data.data : [])
101
+ .map(x => String(x.id || '').trim())
102
+ .filter(Boolean)
103
+ .sort();
104
+ log.debug('Fetched', ids.length, 'models');
105
+ return ids;
106
+ } catch (e) {
107
+ log.error('Failed to fetch models:', e.message);
108
+ throw new Error('加载模型失败: ' + e.message);
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: [
118
+ ...(config.sys ? [{ role: 'system', content: config.sys }] : []),
119
+ { role: 'user', content: prompt }
120
+ ],
121
+ max_tokens: config.max,
122
+ temperature: config.temp,
123
+ stream: false
124
+ };
125
+
126
+ const url = buildUrl(config.base, '/chat/completions');
127
+ const resp = await makeRequest(url, {
128
+ method: 'POST',
129
+ headers: {
130
+ 'Authorization': 'Bearer ' + config.token,
131
+ 'Content-Type': 'application/json'
132
+ }
133
+ }, JSON.stringify(body), config.timeout);
134
+
135
+ if (signal?.aborted) throw new Error('Aborted');
136
+
137
+ // Check HTTP status first
138
+ if (resp.status !== 200) {
139
+ let errorMsg = 'HTTP ' + resp.status;
140
+ try {
141
+ const errorData = JSON.parse(resp.body);
142
+ errorMsg = errorData?.error?.message || errorMsg;
143
+ } catch (e) {
144
+ // Response body is not valid JSON, use raw body
145
+ if (resp.body) errorMsg += ': ' + resp.body.slice(0, 200);
146
+ }
147
+ throw new Error(errorMsg);
148
+ }
149
+
150
+ // Parse successful response
151
+ let data;
152
+ try {
153
+ data = JSON.parse(resp.body);
154
+ } catch (e) {
155
+ throw new Error('响应解析失败: ' + resp.body.slice(0, 200));
156
+ }
157
+
158
+ const content = data?.choices?.[0]?.message?.content || '';
159
+ const usage = usageWithFallback(data?.usage, prompt, config.sys, content);
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 = {
168
+ model: config.model,
169
+ messages: [
170
+ ...(config.sys ? [{ role: 'system', content: config.sys }] : []),
171
+ { role: 'user', content: prompt }
172
+ ],
173
+ max_tokens: config.max,
174
+ temperature: config.temp,
175
+ stream: true,
176
+ stream_options: { include_usage: true }
177
+ };
178
+
179
+ const url = buildUrl(config.base, '/chat/completions');
180
+ const urlObj = new URL(url);
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) {
187
+ reject(new Error('Aborted'));
188
+ return;
189
+ }
190
+
191
+ const reqOptions = {
192
+ hostname: urlObj.hostname,
193
+ port: urlObj.port || (isHttps ? 443 : 80),
194
+ path: urlObj.pathname,
195
+ method: 'POST',
196
+ headers: {
197
+ 'Authorization': 'Bearer ' + config.token,
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;
207
+ let abortHandler = null; // Define early for cleanup
208
+
209
+ const cleanup = () => {
210
+ if (abortHandler && signal) {
211
+ signal.removeEventListener('abort', abortHandler);
212
+ }
213
+ };
214
+
215
+ const req = lib.request(reqOptions, (res) => {
216
+ let buffer = '';
217
+
218
+ // Check HTTP status code for non-streaming errors
219
+ if (res.statusCode !== 200) {
220
+ res.on('data', chunk => buffer += chunk.toString());
221
+ res.on('end', () => {
222
+ cleanup();
223
+ try {
224
+ const data = JSON.parse(buffer);
225
+ reject(new Error(data?.error?.message || 'HTTP ' + res.statusCode));
226
+ } catch (e) {
227
+ reject(new Error('HTTP ' + res.statusCode + ': ' + buffer.slice(0, 200)));
228
+ }
229
+ });
230
+ return;
231
+ }
232
+
233
+ res.on('data', chunk => {
234
+ if (aborted) { req.destroy(); return; }
235
+
236
+ buffer += chunk.toString();
237
+ const lines = buffer.split('\n');
238
+ buffer = lines.pop() || '';
239
+
240
+ for (const line of lines) {
241
+ const trimmed = line.trim();
242
+ if (!trimmed || !trimmed.startsWith('data:')) continue;
243
+
244
+ const payload = trimmed.slice(5).trim();
245
+ if (payload === '[DONE]') continue;
246
+
247
+ try {
248
+ const json = JSON.parse(payload);
249
+ if (json.error) throw new Error(json.error.message);
250
+
251
+ const delta = json.choices?.[0]?.delta?.content;
252
+ if (delta) {
253
+ fullContent += delta;
254
+ if (onChunk) onChunk(delta, fullContent);
255
+ }
256
+
257
+ if (json.usage) usage = json.usage;
258
+ if (json.choices?.[0]?.finish_reason) {
259
+ finishReason = json.choices[0].finish_reason;
260
+ }
261
+ } catch (e) {
262
+ if (e.message && !e.message.includes('JSON')) {
263
+ req.destroy();
264
+ cleanup();
265
+ reject(e);
266
+ return;
267
+ }
268
+ }
269
+ }
270
+ });
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,
280
+ marker,
281
+ prompt
282
+ });
283
+ });
284
+ });
285
+
286
+ req.on('error', (err) => {
287
+ cleanup();
288
+ if (aborted) reject(new Error('Aborted'));
289
+ else reject(err);
290
+ });
291
+
292
+ req.on('timeout', () => {
293
+ cleanup();
294
+ aborted = true;
295
+ req.destroy();
296
+ reject(new Error('请求超时'));
297
+ });
298
+
299
+ req.write(JSON.stringify(body));
300
+ req.end();
301
+
302
+ // Set up abort handler
303
+ if (signal) {
304
+ abortHandler = () => {
305
+ aborted = true;
306
+ req.destroy();
307
+ reject(new Error('Aborted'));
308
+ };
309
+ signal.addEventListener('abort', abortHandler);
310
+ }
311
+ });
312
+ }
313
+
314
+ class TaskExecutor {
315
+ constructor(taskId, config, onUpdate, savedProgress = null) {
316
+ this.taskId = taskId;
317
+ this.config = config;
318
+ this.onUpdate = onUpdate;
319
+
320
+ this.running = false;
321
+ this.stopped = false;
322
+ this.paused = false;
323
+ this.controller = new AbortController();
324
+
325
+ // Update throttling - limit notification frequency
326
+ this.lastNotifyTime = 0;
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 || {
333
+ total: config.loop * config.threads,
334
+ completed: 0,
335
+ success: 0,
336
+ failed: 0,
337
+ aborted: 0,
338
+ promptTokens: 0,
339
+ completionTokens: 0,
340
+ totalTokens: 0
341
+ };
342
+ this.threadProgress = savedProgress.threadProgress || {};
343
+ this.markerLen = savedProgress.markerLen || config.markerLen || 24;
344
+ this.ratio = savedProgress.ratio || { live: 0, target: config.ratioTarget || 1, markerLen: this.markerLen };
345
+ this.elapsedBefore = savedProgress.elapsedBefore || 0;
346
+ // Resume from paused state if saved
347
+ if (savedProgress.paused) {
348
+ this.paused = true;
349
+ log.info('Resuming from paused state:', taskId);
350
+ }
351
+ } else {
352
+ this.stats = {
353
+ total: config.loop * config.threads,
354
+ completed: 0,
355
+ success: 0,
356
+ failed: 0,
357
+ aborted: 0,
358
+ promptTokens: 0,
359
+ completionTokens: 0,
360
+ totalTokens: 0
361
+ };
362
+ this.threadProgress = {};
363
+ this.markerLen = config.markerLen || 24;
364
+ this.ratio = { live: 0, target: config.ratioTarget || 1, markerLen: this.markerLen };
365
+ this.elapsedBefore = 0;
366
+ }
367
+
368
+ this.startTime = null;
369
+ this.threadLogs = {};
370
+
371
+ log.debug('TaskExecutor created:', taskId, 'threads:', config.threads, 'loop:', config.loop, 'resumed:', !!savedProgress);
372
+ }
373
+
374
+ async runWorker(threadId) {
375
+ const startLoop = (this.threadProgress[threadId] || 0) + 1;
376
+
377
+ for (let i = startLoop; i <= this.config.loop; i++) {
378
+ // Check for pause
379
+ while (this.paused && !this.stopped) {
380
+ await new Promise(resolve => setTimeout(resolve, 100));
381
+ }
382
+
383
+ if (this.stopped) {
384
+ log.debug('Thread', threadId, 'stopped at loop', i);
385
+ break;
386
+ }
387
+
388
+ const threadLog = {
389
+ thread: threadId,
390
+ loop: i,
391
+ status: 'running',
392
+ startTime: Date.now(),
393
+ message: ''
394
+ };
395
+
396
+ this.threadLogs[threadId] = threadLog;
397
+ this.threadProgress[threadId] = i;
398
+ this.notifyUpdate();
399
+
400
+ try {
401
+ const result = this.config.streamOn
402
+ ? await streamRequest(
403
+ { ...this.config, markerLen: this.markerLen },
404
+ (delta, full) => {
405
+ threadLog.status = 'streaming';
406
+ threadLog.content = full;
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;
422
+
423
+ log.debug('Thread', threadId, 'loop', i, 'success, tokens:', result.usage.total_tokens);
424
+
425
+ // Check token limit
426
+ if (this.stats.totalTokens >= this.config.maxTokens) {
427
+ this.stopped = true;
428
+ threadLog.status = 'limit_reached';
429
+ threadLog.message = '达到 tokens 上限';
430
+ log.info('Task', this.taskId, 'reached token limit:', this.stats.totalTokens);
431
+ } else {
432
+ threadLog.status = 'success';
433
+ threadLog.content = result.content?.slice(-500);
434
+ }
435
+
436
+ // Adjust marker for ratio
437
+ const liveRatio = this.stats.completionTokens > 0
438
+ ? this.stats.promptTokens / this.stats.completionTokens
439
+ : 0;
440
+ this.ratio.live = liveRatio;
441
+
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) * 50));
446
+ if (diff < 0) this.markerLen = Math.min(4000, this.markerLen + step);
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
+
455
+ if (isAborted) {
456
+ // Distinguish between pause and stop
457
+ if (this.paused) {
458
+ // Paused - don't count as aborted, keep current progress
459
+ threadLog.status = 'paused';
460
+ threadLog.message = '已暂停,等待继续...';
461
+ log.debug('Thread', threadId, 'loop', i, 'paused');
462
+ this.notifyUpdate(true); // Force update for pause
463
+ // Wait for resume
464
+ while (this.paused && !this.stopped) {
465
+ await new Promise(resolve => setTimeout(resolve, 100));
466
+ }
467
+ // If resumed and not stopped, retry current loop with new controller
468
+ if (!this.stopped) {
469
+ i--; // Retry current loop
470
+ threadLog.status = 'running';
471
+ continue;
472
+ }
473
+ }
474
+ // Stopped - count as aborted
475
+ if (this.stopped) {
476
+ this.stats.completed++;
477
+ this.stats.aborted++;
478
+ threadLog.status = 'aborted';
479
+ log.debug('Thread', threadId, 'loop', i, 'aborted');
480
+ }
481
+ } else {
482
+ this.stats.failed++;
483
+ threadLog.status = 'error';
484
+ threadLog.error = e.message || '请求失败';
485
+ log.error('Thread', threadId, 'loop', i, 'error:', e.message);
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)
493
+ if (i < this.config.loop && this.config.waitBetween > 0 && !this.stopped && !this.paused) {
494
+ await new Promise(resolve => setTimeout(resolve, this.config.waitBetween));
495
+ }
496
+ }
497
+
498
+ // Clear progress when thread completes all loops
499
+ if (!this.stopped && !this.paused) {
500
+ delete this.threadProgress[threadId];
501
+ }
502
+ }
503
+
504
+ async start() {
505
+ log.info('TaskExecutor starting:', this.taskId, 'threads:', this.config.threads, 'loops:', this.config.loop, 'paused:', this.paused);
506
+ this.running = true;
507
+ this.stopped = false;
508
+
509
+ // Always ensure controller exists
510
+ if (!this.controller) {
511
+ this.controller = new AbortController();
512
+ }
513
+
514
+ // Only reset startTime if not paused (will be set by resume())
515
+ if (!this.paused) {
516
+ this.startTime = Date.now();
517
+ // Initialize thread logs for fresh start
518
+ for (let i = 1; i <= this.config.threads; i++) {
519
+ this.threadLogs[i] = { thread: i, status: 'waiting', message: '等待开始...' };
520
+ }
521
+ } else {
522
+ // Resuming from paused state - restore thread logs
523
+ for (let i = 1; i <= this.config.threads; i++) {
524
+ if (!this.threadLogs[i]) {
525
+ this.threadLogs[i] = { thread: i, status: 'paused', message: '等待继续...' };
526
+ } else {
527
+ this.threadLogs[i].status = 'paused';
528
+ this.threadLogs[i].message = '等待继续...';
529
+ }
530
+ }
531
+ }
532
+
533
+ this.notifyUpdate();
534
+
535
+ // Run workers in parallel
536
+ const workers = [];
537
+ for (let i = 1; i <= this.config.threads; i++) {
538
+ workers.push(this.runWorker(i));
539
+ }
540
+
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
+
547
+ // Clean up callback reference to allow garbage collection
548
+ this.onUpdate = null;
549
+ }
550
+
551
+ pause() {
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;
558
+ }
559
+ // Abort current request to pause immediately
560
+ this.controller.abort();
561
+ this.notifyUpdate();
562
+ }
563
+
564
+ resume() {
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();
571
+ this.notifyUpdate();
572
+ }
573
+
574
+ stop() {
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) {
584
+ elapsed += Date.now() - this.startTime;
585
+ }
586
+ return {
587
+ taskId: this.taskId,
588
+ running: this.running,
589
+ stopped: this.stopped,
590
+ paused: this.paused,
591
+ stats: this.stats,
592
+ ratio: this.ratio,
593
+ elapsed: elapsed,
594
+ elapsedBefore: this.elapsedBefore,
595
+ threadLogs: { ...this.threadLogs }, // Shallow copy for thread safety
596
+ threadProgress: { ...this.threadProgress },
597
+ markerLen: this.markerLen
598
+ };
599
+ }
600
+
601
+ getProgress() {
602
+ let elapsed = this.elapsedBefore;
603
+ if (this.startTime && !this.paused) {
604
+ elapsed += Date.now() - this.startTime;
605
+ }
606
+ return {
607
+ stats: this.stats,
608
+ threadProgress: this.threadProgress,
609
+ ratio: this.ratio,
610
+ markerLen: this.markerLen,
611
+ elapsedBefore: elapsed
612
+ };
613
+ }
614
+
615
+ notifyUpdate(force = false) {
616
+ // Throttle updates to reduce API load
617
+ // force = true means immediate update (for errors, completion, etc.)
618
+ const now = Date.now();
619
+ const timeSinceLastNotify = now - this.lastNotifyTime;
620
+
621
+ if (force || timeSinceLastNotify >= this.notifyInterval) {
622
+ // Enough time passed or forced update, notify immediately
623
+ if (this.notifyTimer) {
624
+ clearTimeout(this.notifyTimer);
625
+ this.notifyTimer = null;
626
+ }
627
+ this.lastNotifyTime = now;
628
+ if (this.onUpdate) {
629
+ this.onUpdate(this.getStatus());
630
+ }
631
+ } else if (!this.notifyTimer) {
632
+ // Schedule a delayed notification - only one timer at a time
633
+ this.notifyTimer = setTimeout(() => {
634
+ this.notifyTimer = null;
635
+ this.lastNotifyTime = Date.now();
636
+ if (this.onUpdate) {
637
+ this.onUpdate(this.getStatus());
638
+ }
639
+ }, this.notifyInterval - timeSinceLastNotify);
640
+ }
641
+ // If timer already exists, the scheduled notification will include latest state
642
+ }
643
+ }
644
+
645
+ module.exports = { TaskExecutor, fetchModels };
logger.js ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Logger with environment variable control
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
+
9
+ const log = {
10
+ debug: (...args) => currentLevel <= LOG_LEVELS.debug && console.log(`[${ts()}] [DEBUG]`, ...args),
11
+ info: (...args) => currentLevel <= LOG_LEVELS.info && console.log(`[${ts()}] [INFO]`, ...args),
12
+ warn: (...args) => currentLevel <= LOG_LEVELS.warn && console.warn(`[${ts()}] [WARN]`, ...args),
13
+ error: (...args) => currentLevel <= LOG_LEVELS.error && console.error(`[${ts()}] [ERROR]`, ...args),
14
+ };
15
+
16
+ module.exports = log;
package-lock.json ADDED
@@ -0,0 +1,1336 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "token-consumer",
3
+ "version": "1.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
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"
14
+ },
15
+ "devDependencies": {
16
+ "cross-env": "^7.0.3",
17
+ "nodemon": "^3.0.2"
18
+ }
19
+ },
20
+ "node_modules/accepts": {
21
+ "version": "1.3.8",
22
+ "resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz",
23
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
24
+ "license": "MIT",
25
+ "dependencies": {
26
+ "mime-types": "~2.1.34",
27
+ "negotiator": "0.6.3"
28
+ },
29
+ "engines": {
30
+ "node": ">= 0.6"
31
+ }
32
+ },
33
+ "node_modules/anymatch": {
34
+ "version": "3.1.3",
35
+ "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz",
36
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
37
+ "dev": true,
38
+ "license": "ISC",
39
+ "dependencies": {
40
+ "normalize-path": "^3.0.0",
41
+ "picomatch": "^2.0.4"
42
+ },
43
+ "engines": {
44
+ "node": ">= 8"
45
+ }
46
+ },
47
+ "node_modules/array-flatten": {
48
+ "version": "1.1.1",
49
+ "resolved": "https://registry.npmmirror.com/array-flatten/-/array-flatten-1.1.1.tgz",
50
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
51
+ "license": "MIT"
52
+ },
53
+ "node_modules/balanced-match": {
54
+ "version": "4.0.4",
55
+ "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz",
56
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
57
+ "dev": true,
58
+ "license": "MIT",
59
+ "engines": {
60
+ "node": "18 || 20 || >=22"
61
+ }
62
+ },
63
+ "node_modules/binary-extensions": {
64
+ "version": "2.3.0",
65
+ "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz",
66
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
67
+ "dev": true,
68
+ "license": "MIT",
69
+ "engines": {
70
+ "node": ">=8"
71
+ },
72
+ "funding": {
73
+ "url": "https://github.com/sponsors/sindresorhus"
74
+ }
75
+ },
76
+ "node_modules/body-parser": {
77
+ "version": "1.20.4",
78
+ "resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-1.20.4.tgz",
79
+ "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
80
+ "license": "MIT",
81
+ "dependencies": {
82
+ "bytes": "~3.1.2",
83
+ "content-type": "~1.0.5",
84
+ "debug": "2.6.9",
85
+ "depd": "2.0.0",
86
+ "destroy": "~1.2.0",
87
+ "http-errors": "~2.0.1",
88
+ "iconv-lite": "~0.4.24",
89
+ "on-finished": "~2.4.1",
90
+ "qs": "~6.14.0",
91
+ "raw-body": "~2.5.3",
92
+ "type-is": "~1.6.18",
93
+ "unpipe": "~1.0.0"
94
+ },
95
+ "engines": {
96
+ "node": ">= 0.8",
97
+ "npm": "1.2.8000 || >= 1.4.16"
98
+ }
99
+ },
100
+ "node_modules/brace-expansion": {
101
+ "version": "5.0.4",
102
+ "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.4.tgz",
103
+ "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==",
104
+ "dev": true,
105
+ "license": "MIT",
106
+ "dependencies": {
107
+ "balanced-match": "^4.0.2"
108
+ },
109
+ "engines": {
110
+ "node": "18 || 20 || >=22"
111
+ }
112
+ },
113
+ "node_modules/braces": {
114
+ "version": "3.0.3",
115
+ "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz",
116
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
117
+ "dev": true,
118
+ "license": "MIT",
119
+ "dependencies": {
120
+ "fill-range": "^7.1.1"
121
+ },
122
+ "engines": {
123
+ "node": ">=8"
124
+ }
125
+ },
126
+ "node_modules/bytes": {
127
+ "version": "3.1.2",
128
+ "resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz",
129
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
130
+ "license": "MIT",
131
+ "engines": {
132
+ "node": ">= 0.8"
133
+ }
134
+ },
135
+ "node_modules/call-bind-apply-helpers": {
136
+ "version": "1.0.2",
137
+ "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
138
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
139
+ "license": "MIT",
140
+ "dependencies": {
141
+ "es-errors": "^1.3.0",
142
+ "function-bind": "^1.1.2"
143
+ },
144
+ "engines": {
145
+ "node": ">= 0.4"
146
+ }
147
+ },
148
+ "node_modules/call-bound": {
149
+ "version": "1.0.4",
150
+ "resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz",
151
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
152
+ "license": "MIT",
153
+ "dependencies": {
154
+ "call-bind-apply-helpers": "^1.0.2",
155
+ "get-intrinsic": "^1.3.0"
156
+ },
157
+ "engines": {
158
+ "node": ">= 0.4"
159
+ },
160
+ "funding": {
161
+ "url": "https://github.com/sponsors/ljharb"
162
+ }
163
+ },
164
+ "node_modules/chokidar": {
165
+ "version": "3.6.0",
166
+ "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz",
167
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
168
+ "dev": true,
169
+ "license": "MIT",
170
+ "dependencies": {
171
+ "anymatch": "~3.1.2",
172
+ "braces": "~3.0.2",
173
+ "glob-parent": "~5.1.2",
174
+ "is-binary-path": "~2.1.0",
175
+ "is-glob": "~4.0.1",
176
+ "normalize-path": "~3.0.0",
177
+ "readdirp": "~3.6.0"
178
+ },
179
+ "engines": {
180
+ "node": ">= 8.10.0"
181
+ },
182
+ "funding": {
183
+ "url": "https://paulmillr.com/funding/"
184
+ },
185
+ "optionalDependencies": {
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",
192
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
193
+ "license": "MIT",
194
+ "dependencies": {
195
+ "safe-buffer": "5.2.1"
196
+ },
197
+ "engines": {
198
+ "node": ">= 0.6"
199
+ }
200
+ },
201
+ "node_modules/content-type": {
202
+ "version": "1.0.5",
203
+ "resolved": "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz",
204
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
205
+ "license": "MIT",
206
+ "engines": {
207
+ "node": ">= 0.6"
208
+ }
209
+ },
210
+ "node_modules/cookie": {
211
+ "version": "0.7.2",
212
+ "resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.7.2.tgz",
213
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
214
+ "license": "MIT",
215
+ "engines": {
216
+ "node": ">= 0.6"
217
+ }
218
+ },
219
+ "node_modules/cookie-signature": {
220
+ "version": "1.0.7",
221
+ "resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.0.7.tgz",
222
+ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
223
+ "license": "MIT"
224
+ },
225
+ "node_modules/cors": {
226
+ "version": "2.8.6",
227
+ "resolved": "https://registry.npmmirror.com/cors/-/cors-2.8.6.tgz",
228
+ "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
229
+ "license": "MIT",
230
+ "dependencies": {
231
+ "object-assign": "^4",
232
+ "vary": "^1"
233
+ },
234
+ "engines": {
235
+ "node": ">= 0.10"
236
+ },
237
+ "funding": {
238
+ "type": "opencollective",
239
+ "url": "https://opencollective.com/express"
240
+ }
241
+ },
242
+ "node_modules/cross-env": {
243
+ "version": "7.0.3",
244
+ "resolved": "https://registry.npmmirror.com/cross-env/-/cross-env-7.0.3.tgz",
245
+ "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
246
+ "dev": true,
247
+ "license": "MIT",
248
+ "dependencies": {
249
+ "cross-spawn": "^7.0.1"
250
+ },
251
+ "bin": {
252
+ "cross-env": "src/bin/cross-env.js",
253
+ "cross-env-shell": "src/bin/cross-env-shell.js"
254
+ },
255
+ "engines": {
256
+ "node": ">=10.14",
257
+ "npm": ">=6",
258
+ "yarn": ">=1"
259
+ }
260
+ },
261
+ "node_modules/cross-spawn": {
262
+ "version": "7.0.6",
263
+ "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz",
264
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
265
+ "dev": true,
266
+ "license": "MIT",
267
+ "dependencies": {
268
+ "path-key": "^3.1.0",
269
+ "shebang-command": "^2.0.0",
270
+ "which": "^2.0.1"
271
+ },
272
+ "engines": {
273
+ "node": ">= 8"
274
+ }
275
+ },
276
+ "node_modules/debug": {
277
+ "version": "2.6.9",
278
+ "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz",
279
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
280
+ "license": "MIT",
281
+ "dependencies": {
282
+ "ms": "2.0.0"
283
+ }
284
+ },
285
+ "node_modules/depd": {
286
+ "version": "2.0.0",
287
+ "resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz",
288
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
289
+ "license": "MIT",
290
+ "engines": {
291
+ "node": ">= 0.8"
292
+ }
293
+ },
294
+ "node_modules/destroy": {
295
+ "version": "1.2.0",
296
+ "resolved": "https://registry.npmmirror.com/destroy/-/destroy-1.2.0.tgz",
297
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
298
+ "license": "MIT",
299
+ "engines": {
300
+ "node": ">= 0.8",
301
+ "npm": "1.2.8000 || >= 1.4.16"
302
+ }
303
+ },
304
+ "node_modules/dunder-proto": {
305
+ "version": "1.0.1",
306
+ "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
307
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
308
+ "license": "MIT",
309
+ "dependencies": {
310
+ "call-bind-apply-helpers": "^1.0.1",
311
+ "es-errors": "^1.3.0",
312
+ "gopd": "^1.2.0"
313
+ },
314
+ "engines": {
315
+ "node": ">= 0.4"
316
+ }
317
+ },
318
+ "node_modules/ee-first": {
319
+ "version": "1.1.1",
320
+ "resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz",
321
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
322
+ "license": "MIT"
323
+ },
324
+ "node_modules/encodeurl": {
325
+ "version": "2.0.0",
326
+ "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz",
327
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
328
+ "license": "MIT",
329
+ "engines": {
330
+ "node": ">= 0.8"
331
+ }
332
+ },
333
+ "node_modules/es-define-property": {
334
+ "version": "1.0.1",
335
+ "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
336
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
337
+ "license": "MIT",
338
+ "engines": {
339
+ "node": ">= 0.4"
340
+ }
341
+ },
342
+ "node_modules/es-errors": {
343
+ "version": "1.3.0",
344
+ "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
345
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
346
+ "license": "MIT",
347
+ "engines": {
348
+ "node": ">= 0.4"
349
+ }
350
+ },
351
+ "node_modules/es-object-atoms": {
352
+ "version": "1.1.1",
353
+ "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
354
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
355
+ "license": "MIT",
356
+ "dependencies": {
357
+ "es-errors": "^1.3.0"
358
+ },
359
+ "engines": {
360
+ "node": ">= 0.4"
361
+ }
362
+ },
363
+ "node_modules/escape-html": {
364
+ "version": "1.0.3",
365
+ "resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz",
366
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
367
+ "license": "MIT"
368
+ },
369
+ "node_modules/etag": {
370
+ "version": "1.8.1",
371
+ "resolved": "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz",
372
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
373
+ "license": "MIT",
374
+ "engines": {
375
+ "node": ">= 0.6"
376
+ }
377
+ },
378
+ "node_modules/express": {
379
+ "version": "4.22.1",
380
+ "resolved": "https://registry.npmmirror.com/express/-/express-4.22.1.tgz",
381
+ "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
382
+ "license": "MIT",
383
+ "dependencies": {
384
+ "accepts": "~1.3.8",
385
+ "array-flatten": "1.1.1",
386
+ "body-parser": "~1.20.3",
387
+ "content-disposition": "~0.5.4",
388
+ "content-type": "~1.0.4",
389
+ "cookie": "~0.7.1",
390
+ "cookie-signature": "~1.0.6",
391
+ "debug": "2.6.9",
392
+ "depd": "2.0.0",
393
+ "encodeurl": "~2.0.0",
394
+ "escape-html": "~1.0.3",
395
+ "etag": "~1.8.1",
396
+ "finalhandler": "~1.3.1",
397
+ "fresh": "~0.5.2",
398
+ "http-errors": "~2.0.0",
399
+ "merge-descriptors": "1.0.3",
400
+ "methods": "~1.1.2",
401
+ "on-finished": "~2.4.1",
402
+ "parseurl": "~1.3.3",
403
+ "path-to-regexp": "~0.1.12",
404
+ "proxy-addr": "~2.0.7",
405
+ "qs": "~6.14.0",
406
+ "range-parser": "~1.2.1",
407
+ "safe-buffer": "5.2.1",
408
+ "send": "~0.19.0",
409
+ "serve-static": "~1.16.2",
410
+ "setprototypeof": "1.2.0",
411
+ "statuses": "~2.0.1",
412
+ "type-is": "~1.6.18",
413
+ "utils-merge": "1.0.1",
414
+ "vary": "~1.1.2"
415
+ },
416
+ "engines": {
417
+ "node": ">= 0.10.0"
418
+ },
419
+ "funding": {
420
+ "type": "opencollective",
421
+ "url": "https://opencollective.com/express"
422
+ }
423
+ },
424
+ "node_modules/fill-range": {
425
+ "version": "7.1.1",
426
+ "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz",
427
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
428
+ "dev": true,
429
+ "license": "MIT",
430
+ "dependencies": {
431
+ "to-regex-range": "^5.0.1"
432
+ },
433
+ "engines": {
434
+ "node": ">=8"
435
+ }
436
+ },
437
+ "node_modules/finalhandler": {
438
+ "version": "1.3.2",
439
+ "resolved": "https://registry.npmmirror.com/finalhandler/-/finalhandler-1.3.2.tgz",
440
+ "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
441
+ "license": "MIT",
442
+ "dependencies": {
443
+ "debug": "2.6.9",
444
+ "encodeurl": "~2.0.0",
445
+ "escape-html": "~1.0.3",
446
+ "on-finished": "~2.4.1",
447
+ "parseurl": "~1.3.3",
448
+ "statuses": "~2.0.2",
449
+ "unpipe": "~1.0.0"
450
+ },
451
+ "engines": {
452
+ "node": ">= 0.8"
453
+ }
454
+ },
455
+ "node_modules/forwarded": {
456
+ "version": "0.2.0",
457
+ "resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz",
458
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
459
+ "license": "MIT",
460
+ "engines": {
461
+ "node": ">= 0.6"
462
+ }
463
+ },
464
+ "node_modules/fresh": {
465
+ "version": "0.5.2",
466
+ "resolved": "https://registry.npmmirror.com/fresh/-/fresh-0.5.2.tgz",
467
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
468
+ "license": "MIT",
469
+ "engines": {
470
+ "node": ">= 0.6"
471
+ }
472
+ },
473
+ "node_modules/fsevents": {
474
+ "version": "2.3.3",
475
+ "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
476
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
477
+ "dev": true,
478
+ "hasInstallScript": true,
479
+ "license": "MIT",
480
+ "optional": true,
481
+ "os": [
482
+ "darwin"
483
+ ],
484
+ "engines": {
485
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
486
+ }
487
+ },
488
+ "node_modules/function-bind": {
489
+ "version": "1.1.2",
490
+ "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
491
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
492
+ "license": "MIT",
493
+ "funding": {
494
+ "url": "https://github.com/sponsors/ljharb"
495
+ }
496
+ },
497
+ "node_modules/get-intrinsic": {
498
+ "version": "1.3.0",
499
+ "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
500
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
501
+ "license": "MIT",
502
+ "dependencies": {
503
+ "call-bind-apply-helpers": "^1.0.2",
504
+ "es-define-property": "^1.0.1",
505
+ "es-errors": "^1.3.0",
506
+ "es-object-atoms": "^1.1.1",
507
+ "function-bind": "^1.1.2",
508
+ "get-proto": "^1.0.1",
509
+ "gopd": "^1.2.0",
510
+ "has-symbols": "^1.1.0",
511
+ "hasown": "^2.0.2",
512
+ "math-intrinsics": "^1.1.0"
513
+ },
514
+ "engines": {
515
+ "node": ">= 0.4"
516
+ },
517
+ "funding": {
518
+ "url": "https://github.com/sponsors/ljharb"
519
+ }
520
+ },
521
+ "node_modules/get-proto": {
522
+ "version": "1.0.1",
523
+ "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
524
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
525
+ "license": "MIT",
526
+ "dependencies": {
527
+ "dunder-proto": "^1.0.1",
528
+ "es-object-atoms": "^1.0.0"
529
+ },
530
+ "engines": {
531
+ "node": ">= 0.4"
532
+ }
533
+ },
534
+ "node_modules/glob-parent": {
535
+ "version": "5.1.2",
536
+ "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz",
537
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
538
+ "dev": true,
539
+ "license": "ISC",
540
+ "dependencies": {
541
+ "is-glob": "^4.0.1"
542
+ },
543
+ "engines": {
544
+ "node": ">= 6"
545
+ }
546
+ },
547
+ "node_modules/gopd": {
548
+ "version": "1.2.0",
549
+ "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
550
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
551
+ "license": "MIT",
552
+ "engines": {
553
+ "node": ">= 0.4"
554
+ },
555
+ "funding": {
556
+ "url": "https://github.com/sponsors/ljharb"
557
+ }
558
+ },
559
+ "node_modules/has-flag": {
560
+ "version": "3.0.0",
561
+ "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-3.0.0.tgz",
562
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
563
+ "dev": true,
564
+ "license": "MIT",
565
+ "engines": {
566
+ "node": ">=4"
567
+ }
568
+ },
569
+ "node_modules/has-symbols": {
570
+ "version": "1.1.0",
571
+ "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
572
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
573
+ "license": "MIT",
574
+ "engines": {
575
+ "node": ">= 0.4"
576
+ },
577
+ "funding": {
578
+ "url": "https://github.com/sponsors/ljharb"
579
+ }
580
+ },
581
+ "node_modules/hasown": {
582
+ "version": "2.0.2",
583
+ "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz",
584
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
585
+ "license": "MIT",
586
+ "dependencies": {
587
+ "function-bind": "^1.1.2"
588
+ },
589
+ "engines": {
590
+ "node": ">= 0.4"
591
+ }
592
+ },
593
+ "node_modules/http-errors": {
594
+ "version": "2.0.1",
595
+ "resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.1.tgz",
596
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
597
+ "license": "MIT",
598
+ "dependencies": {
599
+ "depd": "~2.0.0",
600
+ "inherits": "~2.0.4",
601
+ "setprototypeof": "~1.2.0",
602
+ "statuses": "~2.0.2",
603
+ "toidentifier": "~1.0.1"
604
+ },
605
+ "engines": {
606
+ "node": ">= 0.8"
607
+ },
608
+ "funding": {
609
+ "type": "opencollective",
610
+ "url": "https://opencollective.com/express"
611
+ }
612
+ },
613
+ "node_modules/iconv-lite": {
614
+ "version": "0.4.24",
615
+ "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz",
616
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
617
+ "license": "MIT",
618
+ "dependencies": {
619
+ "safer-buffer": ">= 2.1.2 < 3"
620
+ },
621
+ "engines": {
622
+ "node": ">=0.10.0"
623
+ }
624
+ },
625
+ "node_modules/ignore-by-default": {
626
+ "version": "1.0.1",
627
+ "resolved": "https://registry.npmmirror.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
628
+ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
629
+ "dev": true,
630
+ "license": "ISC"
631
+ },
632
+ "node_modules/inherits": {
633
+ "version": "2.0.4",
634
+ "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz",
635
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
636
+ "license": "ISC"
637
+ },
638
+ "node_modules/ipaddr.js": {
639
+ "version": "1.9.1",
640
+ "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
641
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
642
+ "license": "MIT",
643
+ "engines": {
644
+ "node": ">= 0.10"
645
+ }
646
+ },
647
+ "node_modules/is-binary-path": {
648
+ "version": "2.1.0",
649
+ "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz",
650
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
651
+ "dev": true,
652
+ "license": "MIT",
653
+ "dependencies": {
654
+ "binary-extensions": "^2.0.0"
655
+ },
656
+ "engines": {
657
+ "node": ">=8"
658
+ }
659
+ },
660
+ "node_modules/is-extglob": {
661
+ "version": "2.1.1",
662
+ "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz",
663
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
664
+ "dev": true,
665
+ "license": "MIT",
666
+ "engines": {
667
+ "node": ">=0.10.0"
668
+ }
669
+ },
670
+ "node_modules/is-glob": {
671
+ "version": "4.0.3",
672
+ "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz",
673
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
674
+ "dev": true,
675
+ "license": "MIT",
676
+ "dependencies": {
677
+ "is-extglob": "^2.1.1"
678
+ },
679
+ "engines": {
680
+ "node": ">=0.10.0"
681
+ }
682
+ },
683
+ "node_modules/is-number": {
684
+ "version": "7.0.0",
685
+ "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz",
686
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
687
+ "dev": true,
688
+ "license": "MIT",
689
+ "engines": {
690
+ "node": ">=0.12.0"
691
+ }
692
+ },
693
+ "node_modules/isexe": {
694
+ "version": "2.0.0",
695
+ "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz",
696
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
697
+ "dev": true,
698
+ "license": "ISC"
699
+ },
700
+ "node_modules/math-intrinsics": {
701
+ "version": "1.1.0",
702
+ "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
703
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
704
+ "license": "MIT",
705
+ "engines": {
706
+ "node": ">= 0.4"
707
+ }
708
+ },
709
+ "node_modules/media-typer": {
710
+ "version": "0.3.0",
711
+ "resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-0.3.0.tgz",
712
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
713
+ "license": "MIT",
714
+ "engines": {
715
+ "node": ">= 0.6"
716
+ }
717
+ },
718
+ "node_modules/merge-descriptors": {
719
+ "version": "1.0.3",
720
+ "resolved": "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
721
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
722
+ "license": "MIT",
723
+ "funding": {
724
+ "url": "https://github.com/sponsors/sindresorhus"
725
+ }
726
+ },
727
+ "node_modules/methods": {
728
+ "version": "1.1.2",
729
+ "resolved": "https://registry.npmmirror.com/methods/-/methods-1.1.2.tgz",
730
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
731
+ "license": "MIT",
732
+ "engines": {
733
+ "node": ">= 0.6"
734
+ }
735
+ },
736
+ "node_modules/mime": {
737
+ "version": "1.6.0",
738
+ "resolved": "https://registry.npmmirror.com/mime/-/mime-1.6.0.tgz",
739
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
740
+ "license": "MIT",
741
+ "bin": {
742
+ "mime": "cli.js"
743
+ },
744
+ "engines": {
745
+ "node": ">=4"
746
+ }
747
+ },
748
+ "node_modules/mime-db": {
749
+ "version": "1.52.0",
750
+ "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
751
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
752
+ "license": "MIT",
753
+ "engines": {
754
+ "node": ">= 0.6"
755
+ }
756
+ },
757
+ "node_modules/mime-types": {
758
+ "version": "2.1.35",
759
+ "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
760
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
761
+ "license": "MIT",
762
+ "dependencies": {
763
+ "mime-db": "1.52.0"
764
+ },
765
+ "engines": {
766
+ "node": ">= 0.6"
767
+ }
768
+ },
769
+ "node_modules/minimatch": {
770
+ "version": "10.2.4",
771
+ "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-10.2.4.tgz",
772
+ "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
773
+ "dev": true,
774
+ "license": "BlueOak-1.0.0",
775
+ "dependencies": {
776
+ "brace-expansion": "^5.0.2"
777
+ },
778
+ "engines": {
779
+ "node": "18 || 20 || >=22"
780
+ },
781
+ "funding": {
782
+ "url": "https://github.com/sponsors/isaacs"
783
+ }
784
+ },
785
+ "node_modules/ms": {
786
+ "version": "2.0.0",
787
+ "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz",
788
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
789
+ "license": "MIT"
790
+ },
791
+ "node_modules/negotiator": {
792
+ "version": "0.6.3",
793
+ "resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.3.tgz",
794
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
795
+ "license": "MIT",
796
+ "engines": {
797
+ "node": ">= 0.6"
798
+ }
799
+ },
800
+ "node_modules/nodemon": {
801
+ "version": "3.1.14",
802
+ "resolved": "https://registry.npmmirror.com/nodemon/-/nodemon-3.1.14.tgz",
803
+ "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==",
804
+ "dev": true,
805
+ "license": "MIT",
806
+ "dependencies": {
807
+ "chokidar": "^3.5.2",
808
+ "debug": "^4",
809
+ "ignore-by-default": "^1.0.1",
810
+ "minimatch": "^10.2.1",
811
+ "pstree.remy": "^1.1.8",
812
+ "semver": "^7.5.3",
813
+ "simple-update-notifier": "^2.0.0",
814
+ "supports-color": "^5.5.0",
815
+ "touch": "^3.1.0",
816
+ "undefsafe": "^2.0.5"
817
+ },
818
+ "bin": {
819
+ "nodemon": "bin/nodemon.js"
820
+ },
821
+ "engines": {
822
+ "node": ">=10"
823
+ },
824
+ "funding": {
825
+ "type": "opencollective",
826
+ "url": "https://opencollective.com/nodemon"
827
+ }
828
+ },
829
+ "node_modules/nodemon/node_modules/debug": {
830
+ "version": "4.4.3",
831
+ "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
832
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
833
+ "dev": true,
834
+ "license": "MIT",
835
+ "dependencies": {
836
+ "ms": "^2.1.3"
837
+ },
838
+ "engines": {
839
+ "node": ">=6.0"
840
+ },
841
+ "peerDependenciesMeta": {
842
+ "supports-color": {
843
+ "optional": true
844
+ }
845
+ }
846
+ },
847
+ "node_modules/nodemon/node_modules/ms": {
848
+ "version": "2.1.3",
849
+ "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
850
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
851
+ "dev": true,
852
+ "license": "MIT"
853
+ },
854
+ "node_modules/normalize-path": {
855
+ "version": "3.0.0",
856
+ "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz",
857
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
858
+ "dev": true,
859
+ "license": "MIT",
860
+ "engines": {
861
+ "node": ">=0.10.0"
862
+ }
863
+ },
864
+ "node_modules/object-assign": {
865
+ "version": "4.1.1",
866
+ "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz",
867
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
868
+ "license": "MIT",
869
+ "engines": {
870
+ "node": ">=0.10.0"
871
+ }
872
+ },
873
+ "node_modules/object-inspect": {
874
+ "version": "1.13.4",
875
+ "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz",
876
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
877
+ "license": "MIT",
878
+ "engines": {
879
+ "node": ">= 0.4"
880
+ },
881
+ "funding": {
882
+ "url": "https://github.com/sponsors/ljharb"
883
+ }
884
+ },
885
+ "node_modules/on-finished": {
886
+ "version": "2.4.1",
887
+ "resolved": "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz",
888
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
889
+ "license": "MIT",
890
+ "dependencies": {
891
+ "ee-first": "1.1.1"
892
+ },
893
+ "engines": {
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",
900
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
901
+ "license": "MIT",
902
+ "engines": {
903
+ "node": ">= 0.8"
904
+ }
905
+ },
906
+ "node_modules/path-key": {
907
+ "version": "3.1.1",
908
+ "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz",
909
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
910
+ "dev": true,
911
+ "license": "MIT",
912
+ "engines": {
913
+ "node": ">=8"
914
+ }
915
+ },
916
+ "node_modules/path-to-regexp": {
917
+ "version": "0.1.12",
918
+ "resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
919
+ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
920
+ "license": "MIT"
921
+ },
922
+ "node_modules/picomatch": {
923
+ "version": "2.3.1",
924
+ "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz",
925
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
926
+ "dev": true,
927
+ "license": "MIT",
928
+ "engines": {
929
+ "node": ">=8.6"
930
+ },
931
+ "funding": {
932
+ "url": "https://github.com/sponsors/jonschlinkert"
933
+ }
934
+ },
935
+ "node_modules/proxy-addr": {
936
+ "version": "2.0.7",
937
+ "resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz",
938
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
939
+ "license": "MIT",
940
+ "dependencies": {
941
+ "forwarded": "0.2.0",
942
+ "ipaddr.js": "1.9.1"
943
+ },
944
+ "engines": {
945
+ "node": ">= 0.10"
946
+ }
947
+ },
948
+ "node_modules/pstree.remy": {
949
+ "version": "1.1.8",
950
+ "resolved": "https://registry.npmmirror.com/pstree.remy/-/pstree.remy-1.1.8.tgz",
951
+ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
952
+ "dev": true,
953
+ "license": "MIT"
954
+ },
955
+ "node_modules/qs": {
956
+ "version": "6.14.2",
957
+ "resolved": "https://registry.npmmirror.com/qs/-/qs-6.14.2.tgz",
958
+ "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
959
+ "license": "BSD-3-Clause",
960
+ "dependencies": {
961
+ "side-channel": "^1.1.0"
962
+ },
963
+ "engines": {
964
+ "node": ">=0.6"
965
+ },
966
+ "funding": {
967
+ "url": "https://github.com/sponsors/ljharb"
968
+ }
969
+ },
970
+ "node_modules/range-parser": {
971
+ "version": "1.2.1",
972
+ "resolved": "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.1.tgz",
973
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
974
+ "license": "MIT",
975
+ "engines": {
976
+ "node": ">= 0.6"
977
+ }
978
+ },
979
+ "node_modules/raw-body": {
980
+ "version": "2.5.3",
981
+ "resolved": "https://registry.npmmirror.com/raw-body/-/raw-body-2.5.3.tgz",
982
+ "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
983
+ "license": "MIT",
984
+ "dependencies": {
985
+ "bytes": "~3.1.2",
986
+ "http-errors": "~2.0.1",
987
+ "iconv-lite": "~0.4.24",
988
+ "unpipe": "~1.0.0"
989
+ },
990
+ "engines": {
991
+ "node": ">= 0.8"
992
+ }
993
+ },
994
+ "node_modules/readdirp": {
995
+ "version": "3.6.0",
996
+ "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz",
997
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
998
+ "dev": true,
999
+ "license": "MIT",
1000
+ "dependencies": {
1001
+ "picomatch": "^2.2.1"
1002
+ },
1003
+ "engines": {
1004
+ "node": ">=8.10.0"
1005
+ }
1006
+ },
1007
+ "node_modules/safe-buffer": {
1008
+ "version": "5.2.1",
1009
+ "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz",
1010
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
1011
+ "funding": [
1012
+ {
1013
+ "type": "github",
1014
+ "url": "https://github.com/sponsors/feross"
1015
+ },
1016
+ {
1017
+ "type": "patreon",
1018
+ "url": "https://www.patreon.com/feross"
1019
+ },
1020
+ {
1021
+ "type": "consulting",
1022
+ "url": "https://feross.org/support"
1023
+ }
1024
+ ],
1025
+ "license": "MIT"
1026
+ },
1027
+ "node_modules/safer-buffer": {
1028
+ "version": "2.1.2",
1029
+ "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz",
1030
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
1031
+ "license": "MIT"
1032
+ },
1033
+ "node_modules/semver": {
1034
+ "version": "7.7.4",
1035
+ "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz",
1036
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
1037
+ "dev": true,
1038
+ "license": "ISC",
1039
+ "bin": {
1040
+ "semver": "bin/semver.js"
1041
+ },
1042
+ "engines": {
1043
+ "node": ">=10"
1044
+ }
1045
+ },
1046
+ "node_modules/send": {
1047
+ "version": "0.19.2",
1048
+ "resolved": "https://registry.npmmirror.com/send/-/send-0.19.2.tgz",
1049
+ "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
1050
+ "license": "MIT",
1051
+ "dependencies": {
1052
+ "debug": "2.6.9",
1053
+ "depd": "2.0.0",
1054
+ "destroy": "1.2.0",
1055
+ "encodeurl": "~2.0.0",
1056
+ "escape-html": "~1.0.3",
1057
+ "etag": "~1.8.1",
1058
+ "fresh": "~0.5.2",
1059
+ "http-errors": "~2.0.1",
1060
+ "mime": "1.6.0",
1061
+ "ms": "2.1.3",
1062
+ "on-finished": "~2.4.1",
1063
+ "range-parser": "~1.2.1",
1064
+ "statuses": "~2.0.2"
1065
+ },
1066
+ "engines": {
1067
+ "node": ">= 0.8.0"
1068
+ }
1069
+ },
1070
+ "node_modules/send/node_modules/ms": {
1071
+ "version": "2.1.3",
1072
+ "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
1073
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1074
+ "license": "MIT"
1075
+ },
1076
+ "node_modules/serve-static": {
1077
+ "version": "1.16.3",
1078
+ "resolved": "https://registry.npmmirror.com/serve-static/-/serve-static-1.16.3.tgz",
1079
+ "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
1080
+ "license": "MIT",
1081
+ "dependencies": {
1082
+ "encodeurl": "~2.0.0",
1083
+ "escape-html": "~1.0.3",
1084
+ "parseurl": "~1.3.3",
1085
+ "send": "~0.19.1"
1086
+ },
1087
+ "engines": {
1088
+ "node": ">= 0.8.0"
1089
+ }
1090
+ },
1091
+ "node_modules/setprototypeof": {
1092
+ "version": "1.2.0",
1093
+ "resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz",
1094
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
1095
+ "license": "ISC"
1096
+ },
1097
+ "node_modules/shebang-command": {
1098
+ "version": "2.0.0",
1099
+ "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz",
1100
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
1101
+ "dev": true,
1102
+ "license": "MIT",
1103
+ "dependencies": {
1104
+ "shebang-regex": "^3.0.0"
1105
+ },
1106
+ "engines": {
1107
+ "node": ">=8"
1108
+ }
1109
+ },
1110
+ "node_modules/shebang-regex": {
1111
+ "version": "3.0.0",
1112
+ "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz",
1113
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
1114
+ "dev": true,
1115
+ "license": "MIT",
1116
+ "engines": {
1117
+ "node": ">=8"
1118
+ }
1119
+ },
1120
+ "node_modules/side-channel": {
1121
+ "version": "1.1.0",
1122
+ "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz",
1123
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
1124
+ "license": "MIT",
1125
+ "dependencies": {
1126
+ "es-errors": "^1.3.0",
1127
+ "object-inspect": "^1.13.3",
1128
+ "side-channel-list": "^1.0.0",
1129
+ "side-channel-map": "^1.0.1",
1130
+ "side-channel-weakmap": "^1.0.2"
1131
+ },
1132
+ "engines": {
1133
+ "node": ">= 0.4"
1134
+ },
1135
+ "funding": {
1136
+ "url": "https://github.com/sponsors/ljharb"
1137
+ }
1138
+ },
1139
+ "node_modules/side-channel-list": {
1140
+ "version": "1.0.0",
1141
+ "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.0.tgz",
1142
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
1143
+ "license": "MIT",
1144
+ "dependencies": {
1145
+ "es-errors": "^1.3.0",
1146
+ "object-inspect": "^1.13.3"
1147
+ },
1148
+ "engines": {
1149
+ "node": ">= 0.4"
1150
+ },
1151
+ "funding": {
1152
+ "url": "https://github.com/sponsors/ljharb"
1153
+ }
1154
+ },
1155
+ "node_modules/side-channel-map": {
1156
+ "version": "1.0.1",
1157
+ "resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz",
1158
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
1159
+ "license": "MIT",
1160
+ "dependencies": {
1161
+ "call-bound": "^1.0.2",
1162
+ "es-errors": "^1.3.0",
1163
+ "get-intrinsic": "^1.2.5",
1164
+ "object-inspect": "^1.13.3"
1165
+ },
1166
+ "engines": {
1167
+ "node": ">= 0.4"
1168
+ },
1169
+ "funding": {
1170
+ "url": "https://github.com/sponsors/ljharb"
1171
+ }
1172
+ },
1173
+ "node_modules/side-channel-weakmap": {
1174
+ "version": "1.0.2",
1175
+ "resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
1176
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
1177
+ "license": "MIT",
1178
+ "dependencies": {
1179
+ "call-bound": "^1.0.2",
1180
+ "es-errors": "^1.3.0",
1181
+ "get-intrinsic": "^1.2.5",
1182
+ "object-inspect": "^1.13.3",
1183
+ "side-channel-map": "^1.0.1"
1184
+ },
1185
+ "engines": {
1186
+ "node": ">= 0.4"
1187
+ },
1188
+ "funding": {
1189
+ "url": "https://github.com/sponsors/ljharb"
1190
+ }
1191
+ },
1192
+ "node_modules/simple-update-notifier": {
1193
+ "version": "2.0.0",
1194
+ "resolved": "https://registry.npmmirror.com/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
1195
+ "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
1196
+ "dev": true,
1197
+ "license": "MIT",
1198
+ "dependencies": {
1199
+ "semver": "^7.5.3"
1200
+ },
1201
+ "engines": {
1202
+ "node": ">=10"
1203
+ }
1204
+ },
1205
+ "node_modules/statuses": {
1206
+ "version": "2.0.2",
1207
+ "resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.2.tgz",
1208
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
1209
+ "license": "MIT",
1210
+ "engines": {
1211
+ "node": ">= 0.8"
1212
+ }
1213
+ },
1214
+ "node_modules/supports-color": {
1215
+ "version": "5.5.0",
1216
+ "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-5.5.0.tgz",
1217
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
1218
+ "dev": true,
1219
+ "license": "MIT",
1220
+ "dependencies": {
1221
+ "has-flag": "^3.0.0"
1222
+ },
1223
+ "engines": {
1224
+ "node": ">=4"
1225
+ }
1226
+ },
1227
+ "node_modules/to-regex-range": {
1228
+ "version": "5.0.1",
1229
+ "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz",
1230
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
1231
+ "dev": true,
1232
+ "license": "MIT",
1233
+ "dependencies": {
1234
+ "is-number": "^7.0.0"
1235
+ },
1236
+ "engines": {
1237
+ "node": ">=8.0"
1238
+ }
1239
+ },
1240
+ "node_modules/toidentifier": {
1241
+ "version": "1.0.1",
1242
+ "resolved": "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz",
1243
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
1244
+ "license": "MIT",
1245
+ "engines": {
1246
+ "node": ">=0.6"
1247
+ }
1248
+ },
1249
+ "node_modules/touch": {
1250
+ "version": "3.1.1",
1251
+ "resolved": "https://registry.npmmirror.com/touch/-/touch-3.1.1.tgz",
1252
+ "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
1253
+ "dev": true,
1254
+ "license": "ISC",
1255
+ "bin": {
1256
+ "nodetouch": "bin/nodetouch.js"
1257
+ }
1258
+ },
1259
+ "node_modules/type-is": {
1260
+ "version": "1.6.18",
1261
+ "resolved": "https://registry.npmmirror.com/type-is/-/type-is-1.6.18.tgz",
1262
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
1263
+ "license": "MIT",
1264
+ "dependencies": {
1265
+ "media-typer": "0.3.0",
1266
+ "mime-types": "~2.1.24"
1267
+ },
1268
+ "engines": {
1269
+ "node": ">= 0.6"
1270
+ }
1271
+ },
1272
+ "node_modules/undefsafe": {
1273
+ "version": "2.0.5",
1274
+ "resolved": "https://registry.npmmirror.com/undefsafe/-/undefsafe-2.0.5.tgz",
1275
+ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
1276
+ "dev": true,
1277
+ "license": "MIT"
1278
+ },
1279
+ "node_modules/unpipe": {
1280
+ "version": "1.0.0",
1281
+ "resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz",
1282
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
1283
+ "license": "MIT",
1284
+ "engines": {
1285
+ "node": ">= 0.8"
1286
+ }
1287
+ },
1288
+ "node_modules/utils-merge": {
1289
+ "version": "1.0.1",
1290
+ "resolved": "https://registry.npmmirror.com/utils-merge/-/utils-merge-1.0.1.tgz",
1291
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
1292
+ "license": "MIT",
1293
+ "engines": {
1294
+ "node": ">= 0.4.0"
1295
+ }
1296
+ },
1297
+ "node_modules/uuid": {
1298
+ "version": "9.0.1",
1299
+ "resolved": "https://registry.npmmirror.com/uuid/-/uuid-9.0.1.tgz",
1300
+ "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
1301
+ "funding": [
1302
+ "https://github.com/sponsors/broofa",
1303
+ "https://github.com/sponsors/ctavan"
1304
+ ],
1305
+ "license": "MIT",
1306
+ "bin": {
1307
+ "uuid": "dist/bin/uuid"
1308
+ }
1309
+ },
1310
+ "node_modules/vary": {
1311
+ "version": "1.1.2",
1312
+ "resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz",
1313
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
1314
+ "license": "MIT",
1315
+ "engines": {
1316
+ "node": ">= 0.8"
1317
+ }
1318
+ },
1319
+ "node_modules/which": {
1320
+ "version": "2.0.2",
1321
+ "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",
1322
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
1323
+ "dev": true,
1324
+ "license": "ISC",
1325
+ "dependencies": {
1326
+ "isexe": "^2.0.0"
1327
+ },
1328
+ "bin": {
1329
+ "node-which": "bin/node-which"
1330
+ },
1331
+ "engines": {
1332
+ "node": ">= 8"
1333
+ }
1334
+ }
1335
+ }
1336
+ }
package.json ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "token-consumer",
3
+ "version": "1.0.0",
4
+ "description": "OpenAI Token Consumer with backend task management",
5
+ "main": "server.js",
6
+ "scripts": {
7
+ "start": "node server.js",
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": {
19
+ "cross-env": "^7.0.3",
20
+ "nodemon": "^3.0.2"
21
+ }
22
+ }
server.js ADDED
@@ -0,0 +1,576 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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';
16
+
17
+ // Rate limiter - simple in-memory implementation
18
+ const rateLimitMap = new Map();
19
+ const RATE_LIMIT_WINDOW = 60000; // 1 minute
20
+ const RATE_LIMIT_MAX = 300; // max requests per window
21
+
22
+ function rateLimiter(req, res, next) {
23
+ const ip = req.ip || req.connection.remoteAddress || 'unknown';
24
+ const now = Date.now();
25
+
26
+ let entry = rateLimitMap.get(ip);
27
+ if (!entry || now - entry.startTime > RATE_LIMIT_WINDOW) {
28
+ entry = { count: 1, startTime: now };
29
+ rateLimitMap.set(ip, entry);
30
+ } else {
31
+ entry.count++;
32
+ }
33
+
34
+ if (entry.count > RATE_LIMIT_MAX) {
35
+ return res.status(429).json({ error: '请求过于频繁,请稍后再试' });
36
+ }
37
+
38
+ // Clean up old entries periodically
39
+ if (rateLimitMap.size > 1000) {
40
+ for (const [key, val] of rateLimitMap) {
41
+ if (now - val.startTime > RATE_LIMIT_WINDOW) {
42
+ rateLimitMap.delete(key);
43
+ }
44
+ }
45
+ }
46
+
47
+ 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
56
+ const basicAuth = (req, res, next) => {
57
+ const authHeader = req.headers.authorization;
58
+
59
+ if (!authHeader || !authHeader.startsWith('Basic ')) {
60
+ res.setHeader('WWW-Authenticate', 'Basic realm="Token Consumer"');
61
+ return res.status(401).json({ error: '需要认证' });
62
+ }
63
+
64
+ const base64Credentials = authHeader.split(' ')[1];
65
+ const credentials = Buffer.from(base64Credentials, 'base64').toString('utf8');
66
+ const [username, password] = credentials.split(':');
67
+
68
+ if (username === AUTH_USER && password === AUTH_PASS) {
69
+ return next();
70
+ }
71
+
72
+ res.setHeader('WWW-Authenticate', 'Basic realm="Token Consumer"');
73
+ return res.status(401).json({ error: '认证失败' });
74
+ };
75
+
76
+ // Apply auth to all /api routes
77
+ app.use('/api', basicAuth);
78
+
79
+ // Health check endpoint (no auth required)
80
+ app.get('/health', (req, res) => {
81
+ res.json({
82
+ status: 'ok',
83
+ timestamp: new Date().toISOString(),
84
+ uptime: process.uptime()
85
+ });
86
+ });
87
+
88
+ // Serve static files
89
+ app.use(express.static(__dirname));
90
+
91
+ // In-memory task executors
92
+ const executors = new Map();
93
+
94
+ // Create or update task
95
+ app.post('/api/tasks', (req, res) => {
96
+ try {
97
+ const { id, label, config } = req.body;
98
+ log.debug('POST /api/tasks', { id, label, model: config?.model });
99
+
100
+ if (!config?.base || !config?.token || !config?.model || !config?.usr) {
101
+ log.warn('缺少必要参数');
102
+ return res.status(400).json({ error: '缺少必要参数' });
103
+ }
104
+
105
+ const taskId = id || uuidv4();
106
+ const now = Date.now();
107
+
108
+ const task = {
109
+ id: taskId,
110
+ label: label || `任务-${taskId.slice(0, 6)}`,
111
+ config: {
112
+ base: config.base,
113
+ token: config.token,
114
+ model: config.model,
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),
122
+ maxTokens: Math.max(1000, Number(config.maxTokens) || 1000000000),
123
+ randOn: !!config.randOn,
124
+ streamOn: config.streamOn !== false,
125
+ ratioTarget: Math.max(0.1, Math.min(20, Number(config.ratioTarget) || 1)),
126
+ markerLen: Math.max(0, Number(config.markerLen) || 24),
127
+ waitBetween: Math.max(0, Number(config.waitBetween) || 1000)
128
+ },
129
+ status: 'idle',
130
+ stats: {
131
+ total: Math.max(1, Math.min(10000, Number(config.loop) || 10)) * Math.max(1, Math.min(200, Number(config.threads) || 3)),
132
+ completed: 0,
133
+ success: 0,
134
+ failed: 0,
135
+ aborted: 0,
136
+ promptTokens: 0,
137
+ completionTokens: 0,
138
+ totalTokens: 0
139
+ },
140
+ createdAt: storage.getTask(taskId)?.createdAt || now,
141
+ updatedAt: now
142
+ };
143
+
144
+ storage.saveTask(taskId, task);
145
+ log.info('任务已保存:', taskId, label || task.label);
146
+ res.json(task);
147
+ } catch (e) {
148
+ log.error('保存任务失败:', e.message);
149
+ res.status(500).json({ error: e.message });
150
+ }
151
+ });
152
+
153
+ // Get all tasks
154
+ app.get('/api/tasks', (req, res) => {
155
+ try {
156
+ const tasks = storage.getAllTasks();
157
+ const result = Object.values(tasks).map(t => {
158
+ const executor = executors.get(t.id);
159
+ // Fix stats.total for old tasks
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);
170
+ res.json(result);
171
+ } catch (e) {
172
+ log.error('获取任务列表失败:', e.message);
173
+ res.status(500).json({ error: e.message });
174
+ }
175
+ });
176
+
177
+ // Get single task
178
+ app.get('/api/tasks/:id', (req, res) => {
179
+ try {
180
+ const task = storage.getTask(req.params.id);
181
+ if (!task) {
182
+ return res.status(404).json({ error: '任务不存在' });
183
+ }
184
+
185
+ const executor = executors.get(req.params.id);
186
+ // Fix stats.total for old tasks
187
+ if (!task.stats.total && task.config) {
188
+ task.stats.total = (task.config.loop || 10) * (task.config.threads || 3);
189
+ }
190
+ res.json({
191
+ ...task,
192
+ running: executor?.running || false,
193
+ currentStats: executor?.getStatus() || null
194
+ });
195
+ } catch (e) {
196
+ res.status(500).json({ error: e.message });
197
+ }
198
+ });
199
+
200
+ // Delete task
201
+ app.delete('/api/tasks/:id', (req, res) => {
202
+ try {
203
+ log.info('删除任务:', req.params.id);
204
+ const executor = executors.get(req.params.id);
205
+ if (executor?.running) {
206
+ log.warn('任务正在运行,先停止:', req.params.id);
207
+ executor.stop();
208
+ }
209
+ executors.delete(req.params.id);
210
+ storage.deleteTask(req.params.id);
211
+ log.info('任务已删除:', req.params.id);
212
+ res.json({ success: true });
213
+ } catch (e) {
214
+ log.error('删除任务失败:', e.message);
215
+ res.status(500).json({ error: e.message });
216
+ }
217
+ });
218
+
219
+ // Start task
220
+ app.post('/api/tasks/:id/start', (req, res) => {
221
+ try {
222
+ const task = storage.getTask(req.params.id);
223
+ if (!task) {
224
+ log.warn('任务不存在:', req.params.id);
225
+ return res.status(404).json({ error: '任务不存在' });
226
+ }
227
+
228
+ let executor = executors.get(req.params.id);
229
+ if (executor?.running && !executor.paused) {
230
+ log.warn('任务已在运行:', req.params.id);
231
+ return res.status(400).json({ error: '任务已在运行' });
232
+ }
233
+
234
+ // If paused, resume
235
+ if (executor?.paused) {
236
+ log.info('恢复暂停的任务:', req.params.id);
237
+ executor.resume();
238
+ return res.json({ success: true, message: '任务已恢复' });
239
+ }
240
+
241
+ log.info('启动任务:', req.params.id, task.label);
242
+
243
+ // Check if we have saved progress to resume
244
+ const savedProgress = task.progress || null;
245
+ if (savedProgress) {
246
+ log.info('从保存的进度恢复:', req.params.id);
247
+ }
248
+
249
+ // Create new executor
250
+ executor = new TaskExecutor(req.params.id, task.config, (status) => {
251
+ // Update task in storage
252
+ const updatedTask = storage.getTask(req.params.id);
253
+ if (updatedTask) {
254
+ updatedTask.stats = status.stats;
255
+ updatedTask.status = status.running ? (status.paused ? 'paused' : 'running') : (status.stopped ? 'stopped' : 'completed');
256
+ // Always save progress with threadLogs for display, even after completion
257
+ updatedTask.progress = status;
258
+ updatedTask.updatedAt = Date.now();
259
+ storage.saveTask(req.params.id, updatedTask);
260
+ }
261
+
262
+ // Remove from memory if stopped/completed
263
+ if (!status.running) {
264
+ executors.delete(req.params.id);
265
+ log.info('任务结束:', req.params.id, '总tokens:', status.stats?.totalTokens || 0);
266
+ }
267
+ }, savedProgress);
268
+
269
+ executors.set(req.params.id, executor);
270
+ log.debug('Executor created and set in map:', req.params.id, 'map size:', executors.size);
271
+
272
+ // Start in background
273
+ executor.start().then(() => {
274
+ log.debug('Executor start() resolved:', req.params.id);
275
+ }).catch(e => log.error('任务执行错误:', e.message));
276
+
277
+ res.json({ success: true, message: savedProgress ? '任务已恢复' : '任务已启动' });
278
+ } catch (e) {
279
+ log.error('启动任务失败:', e.message);
280
+ res.status(500).json({ error: e.message });
281
+ }
282
+ });
283
+
284
+ // Pause task
285
+ app.post('/api/tasks/:id/pause', (req, res) => {
286
+ try {
287
+ const executor = executors.get(req.params.id);
288
+ if (!executor || !executor.running || executor.paused) {
289
+ log.warn('任务未在运行或已暂停:', req.params.id);
290
+ return res.status(400).json({ error: '任务未在运行或已暂停' });
291
+ }
292
+
293
+ log.info('暂停任务:', req.params.id);
294
+ executor.pause();
295
+ res.json({ success: true, message: '任务已暂停' });
296
+ } catch (e) {
297
+ log.error('暂停任务失败:', e.message);
298
+ res.status(500).json({ error: e.message });
299
+ }
300
+ });
301
+
302
+ // Stop task (reset progress)
303
+ app.post('/api/tasks/:id/stop', (req, res) => {
304
+ try {
305
+ const executor = executors.get(req.params.id);
306
+ if (!executor || !executor.running) {
307
+ log.warn('任务未在运行:', req.params.id);
308
+ return res.status(400).json({ error: '任务未在运行' });
309
+ }
310
+
311
+ log.info('停止任务:', req.params.id);
312
+ executor.stop();
313
+
314
+ // Update task status (progress will be saved by executor callback)
315
+ const task = storage.getTask(req.params.id);
316
+ if (task) {
317
+ task.status = 'stopped';
318
+ storage.saveTask(req.params.id, task);
319
+ }
320
+
321
+ res.json({ success: true, message: '任务已停止' });
322
+ } catch (e) {
323
+ log.error('停止任务失败:', e.message);
324
+ res.status(500).json({ error: e.message });
325
+ }
326
+ });
327
+
328
+ // Get task status (for polling)
329
+ app.get('/api/tasks/:id/status', (req, res) => {
330
+ try {
331
+ const executor = executors.get(req.params.id);
332
+ const task = storage.getTask(req.params.id);
333
+
334
+ if (!task) {
335
+ return res.status(404).json({ error: '任务不存在' });
336
+ }
337
+
338
+ // Use getStatus() for running tasks, or progress from storage for completed tasks
339
+ const status = executor?.getStatus();
340
+ const progress = status || task.progress;
341
+
342
+ res.json({
343
+ taskId: req.params.id,
344
+ running: status?.running || false,
345
+ stopped: status?.stopped || task.status === 'stopped' || false,
346
+ paused: status?.paused || false,
347
+ stats: progress?.stats || task.stats,
348
+ ratio: progress?.ratio || null,
349
+ elapsed: progress?.elapsed || 0,
350
+ threadLogs: progress?.threadLogs || {}
351
+ });
352
+ } catch (e) {
353
+ log.error('获取任务状态失败:', e.message);
354
+ res.status(500).json({ error: e.message });
355
+ }
356
+ });
357
+
358
+ // Load models
359
+ app.post('/api/models', async (req, res) => {
360
+ try {
361
+ const { base, token } = req.body;
362
+ log.debug('加载模型列表, base:', base);
363
+ if (!base || !token) {
364
+ log.warn('缺少 base 或 token');
365
+ return res.status(400).json({ error: '缺少 base 或 token' });
366
+ }
367
+
368
+ const models = await fetchModels(base, token);
369
+ log.info('模型列表加载成功, 数量:', models.length);
370
+ res.json({ models });
371
+ } catch (e) {
372
+ log.error('加载模型失败:', e.message);
373
+ res.status(500).json({ error: e.message });
374
+ }
375
+ });
376
+
377
+ // Save config to server
378
+ app.post('/api/config', (req, res) => {
379
+ try {
380
+ storage.saveConfig(req.body);
381
+ res.json({ success: true });
382
+ } catch (e) {
383
+ res.status(500).json({ error: e.message });
384
+ }
385
+ });
386
+
387
+ // Get config from server
388
+ app.get('/api/config', (req, res) => {
389
+ try {
390
+ const config = storage.loadConfig();
391
+ res.json(config);
392
+ } catch (e) {
393
+ res.status(500).json({ error: e.message });
394
+ }
395
+ });
396
+
397
+ // Get memory usage
398
+ app.get('/api/memory', (req, res) => {
399
+ try {
400
+ // Process memory
401
+ const processMem = process.memoryUsage();
402
+
403
+ // System memory
404
+ const totalMem = os.totalmem();
405
+ const freeMem = os.freemem();
406
+ const usedMem = totalMem - freeMem;
407
+
408
+ res.json({
409
+ process: {
410
+ rss: processMem.rss, // Resident Set Size
411
+ heapTotal: processMem.heapTotal,
412
+ heapUsed: processMem.heapUsed,
413
+ external: processMem.external,
414
+ arrayBuffers: processMem.arrayBuffers
415
+ },
416
+ system: {
417
+ total: totalMem,
418
+ free: freeMem,
419
+ used: usedMem,
420
+ usagePercent: Math.round(usedMem / totalMem * 100)
421
+ }
422
+ });
423
+ } catch (e) {
424
+ res.status(500).json({ error: e.message });
425
+ }
426
+ });
427
+
428
+ // Get raw tasks.json content
429
+ app.get('/api/tasks-raw', (req, res) => {
430
+ try {
431
+ const data = storage.getTasksRaw();
432
+ res.json({ content: JSON.stringify(data, null, 2) });
433
+ } catch (e) {
434
+ log.error('读取 tasks.json 失败:', e.message);
435
+ res.status(500).json({ error: e.message });
436
+ }
437
+ });
438
+
439
+ // Save raw tasks.json content
440
+ app.post('/api/tasks-raw', async (req, res) => {
441
+ try {
442
+ // Check if any task is running (not paused - paused tasks don't write)
443
+ const runningCount = [...executors.values()].filter(e => e.running && !e.paused).length;
444
+ if (runningCount > 0) {
445
+ return res.status(409).json({
446
+ error: `有 ${runningCount} 个任务正在运行,请先暂停或停止所有运行中的任务后再保存配置`
447
+ });
448
+ }
449
+
450
+ const { content } = req.body;
451
+ if (!content) {
452
+ return res.status(400).json({ error: '内容不能为空' });
453
+ }
454
+
455
+ // Validate JSON
456
+ try {
457
+ JSON.parse(content);
458
+ } catch (e) {
459
+ return res.status(400).json({ error: 'JSON 格式无效: ' + e.message });
460
+ }
461
+
462
+ storage.saveTasksRaw(content);
463
+ log.info('tasks.json 已更新');
464
+ res.json({ success: true, message: '保存成功' });
465
+ } catch (e) {
466
+ log.error('保存 tasks.json 失败:', e.message);
467
+ res.status(500).json({ error: e.message });
468
+ }
469
+ });
470
+
471
+ // Serve index.html
472
+ 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();
479
+ const runningTasks = Object.values(tasks).filter(t => t.status === 'running' || t.status === 'paused');
480
+
481
+ if (runningTasks.length === 0) {
482
+ log.info('没有需要恢复的运行中任务');
483
+ return;
484
+ }
485
+
486
+ log.info(`发现 ${runningTasks.length} 个运行中任务,正在恢复...`);
487
+
488
+ for (const task of runningTasks) {
489
+ try {
490
+ const savedProgress = task.progress || null;
491
+
492
+ const executor = new TaskExecutor(task.id, task.config, (status) => {
493
+ const updatedTask = storage.getTask(task.id);
494
+ if (updatedTask) {
495
+ updatedTask.stats = status.stats;
496
+ updatedTask.status = status.running ? (status.paused ? 'paused' : 'running') : (status.stopped ? 'stopped' : 'completed');
497
+ // Always save progress with threadLogs for display
498
+ updatedTask.progress = status;
499
+ updatedTask.updatedAt = Date.now();
500
+ storage.saveTask(task.id, updatedTask);
501
+ }
502
+
503
+ if (!status.running) {
504
+ executors.delete(task.id);
505
+ log.info('任务结束:', task.id, '总tokens:', status.stats?.totalTokens || 0);
506
+ }
507
+ }, savedProgress);
508
+
509
+ executors.set(task.id, executor);
510
+ executor.start().catch(e => log.error('恢复任务失败:', task.id, e.message));
511
+ log.info('任务已恢复:', task.id, task.label);
512
+ } catch (e) {
513
+ log.error('恢复任务失败:', task.id, e.message);
514
+ }
515
+ }
516
+ }
517
+
518
+ const HOST = process.env.HOST || '0.0.0.0';
519
+ app.listen(PORT, HOST, () => {
520
+ console.log(`Server running at http://${HOST}:${PORT}`);
521
+ console.log(`Log level: ${(process.env.LOG_LEVEL || 'info').toUpperCase()} (set LOG_LEVEL env: debug|info|warn|error|none)`);
522
+ console.log(`API endpoints:`);
523
+ console.log(` GET /health - 健康检查`);
524
+ console.log(` GET /api/tasks - 获取所有任务`);
525
+ console.log(` POST /api/tasks - 创建/更新任务`);
526
+ console.log(` GET /api/tasks/:id - 获取单个任务`);
527
+ console.log(` DELETE /api/tasks/:id - 删除任务`);
528
+ console.log(` POST /api/tasks/:id/start - 启动/恢复任务`);
529
+ console.log(` POST /api/tasks/:id/pause - 暂停任务`);
530
+ console.log(` POST /api/tasks/:id/stop - 停止任务`);
531
+ console.log(` GET /api/tasks/:id/status - 获取任务状态`);
532
+ console.log(` POST /api/models - 加载模型列表`);
533
+ console.log(` GET /api/config - 获取配置`);
534
+ console.log(` POST /api/config - 保存配置`);
535
+ console.log(` GET /api/memory - 获取内存使用`);
536
+
537
+ // Resume running tasks after server starts
538
+ resumeRunningTasks();
539
+ });
540
+
541
+ // Graceful shutdown handler
542
+ function gracefulShutdown(signal) {
543
+ console.log(`\n[${signal}] Graceful shutdown initiated...`);
544
+
545
+ // Stop all running executors
546
+ let runningCount = 0;
547
+ for (const [id, executor] of executors) {
548
+ if (executor.running) {
549
+ executor.stop();
550
+ runningCount++;
551
+ }
552
+ }
553
+ if (runningCount > 0) {
554
+ console.log(`[Shutdown] Stopped ${runningCount} running task(s)`);
555
+ }
556
+
557
+ // Flush any pending data
558
+ storage.shutdown();
559
+
560
+ console.log('[Shutdown] Complete');
561
+ process.exit(0);
562
+ }
563
+
564
+ // Handle termination signals
565
+ process.on('SIGINT', () => gracefulShutdown('SIGINT'));
566
+ process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
567
+
568
+ // Handle Windows CTRL+C (SIGINT not always reliable on Windows)
569
+ if (process.platform === 'win32') {
570
+ const readline = require('readline');
571
+ const rl = readline.createInterface({
572
+ input: process.stdin,
573
+ output: process.stdout
574
+ });
575
+ rl.on('SIGINT', () => gracefulShutdown('SIGINT'));
576
+ }
storage.js ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const DATA_DIR = path.join(__dirname, 'data');
5
+ const TASKS_FILE = path.join(DATA_DIR, 'tasks.json');
6
+ const CONFIG_FILE = path.join(DATA_DIR, 'config.json');
7
+
8
+ // In-memory cache to avoid read-modify-write race conditions
9
+ let tasksCache = null;
10
+
11
+ // Write throttling
12
+ let writePending = false;
13
+ let writeTimer = null;
14
+
15
+ function ensureDir() {
16
+ if (!fs.existsSync(DATA_DIR)) {
17
+ fs.mkdirSync(DATA_DIR, { recursive: true });
18
+ }
19
+ }
20
+
21
+ // Load tasks into memory cache (lazy load)
22
+ function loadTasksCache() {
23
+ if (tasksCache === null) {
24
+ try {
25
+ if (fs.existsSync(TASKS_FILE)) {
26
+ const content = fs.readFileSync(TASKS_FILE, 'utf-8');
27
+ const data = JSON.parse(content);
28
+ tasksCache = data.tasks || {};
29
+ } else {
30
+ tasksCache = {};
31
+ }
32
+ } catch (e) {
33
+ console.error('Error loading tasks:', e.message);
34
+ tasksCache = {};
35
+ }
36
+ }
37
+ return tasksCache;
38
+ }
39
+
40
+ // Throttled write - batch writes within 100ms
41
+ function scheduleWrite() {
42
+ if (!writePending) {
43
+ writePending = true;
44
+ if (writeTimer) {
45
+ clearTimeout(writeTimer);
46
+ }
47
+ writeTimer = setTimeout(() => {
48
+ writePending = false;
49
+ writeTimer = null;
50
+ try {
51
+ ensureDir();
52
+ fs.writeFileSync(TASKS_FILE, JSON.stringify({ tasks: tasksCache }, null, 2), 'utf-8');
53
+ } catch (e) {
54
+ console.error('Write error:', e.message);
55
+ }
56
+ }, 100);
57
+ }
58
+ }
59
+
60
+ // Immediate write (for critical operations)
61
+ function flushWrite() {
62
+ if (writeTimer) {
63
+ clearTimeout(writeTimer);
64
+ writeTimer = null;
65
+ }
66
+ if (tasksCache !== null) {
67
+ try {
68
+ ensureDir();
69
+ fs.writeFileSync(TASKS_FILE, JSON.stringify({ tasks: tasksCache }, null, 2), 'utf-8');
70
+ } catch (e) {
71
+ console.error('Flush write error:', e.message);
72
+ }
73
+ }
74
+ writePending = false;
75
+ }
76
+
77
+ // Read JSON helper for other files
78
+ function readJSON(file, defaultValue = {}) {
79
+ try {
80
+ if (fs.existsSync(file)) {
81
+ const content = fs.readFileSync(file, 'utf-8');
82
+ return JSON.parse(content);
83
+ }
84
+ } catch (e) {
85
+ console.error(`Error reading ${file}:`, e.message);
86
+ }
87
+ return defaultValue;
88
+ }
89
+
90
+ // Task persistence - all operations use in-memory cache
91
+ function loadTasks() {
92
+ return { tasks: loadTasksCache() };
93
+ }
94
+
95
+ function saveTasks(tasks) {
96
+ tasksCache = tasks;
97
+ scheduleWrite();
98
+ }
99
+
100
+ function getTask(taskId) {
101
+ const cache = loadTasksCache();
102
+ return cache[taskId] || null;
103
+ }
104
+
105
+ function saveTask(taskId, taskData) {
106
+ const cache = loadTasksCache();
107
+ cache[taskId] = taskData;
108
+ scheduleWrite();
109
+ }
110
+
111
+ function deleteTask(taskId) {
112
+ const cache = loadTasksCache();
113
+ delete cache[taskId];
114
+ scheduleWrite();
115
+ }
116
+
117
+ function getAllTasks() {
118
+ return loadTasksCache();
119
+ }
120
+
121
+ // Config persistence
122
+ function loadConfig() {
123
+ return readJSON(CONFIG_FILE, {});
124
+ }
125
+
126
+ function saveConfig(config) {
127
+ ensureDir();
128
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8');
129
+ }
130
+
131
+ // Raw tasks.json operations (for config editor)
132
+ function getTasksRaw() {
133
+ flushWrite(); // Ensure latest data is written
134
+ return readJSON(TASKS_FILE, { tasks: {} });
135
+ }
136
+
137
+ function saveTasksRaw(content) {
138
+ // Parse and validate
139
+ const data = JSON.parse(content);
140
+
141
+ // Update cache and write immediately
142
+ tasksCache = data.tasks || {};
143
+ flushWrite();
144
+ }
145
+
146
+ // Check if any task is running
147
+ function hasRunningTasks() {
148
+ const tasks = getAllTasks();
149
+ return Object.values(tasks).some(t => t.status === 'running' || t.status === 'paused');
150
+ }
151
+
152
+ // Check if there's pending data to write
153
+ function hasPendingWrite() {
154
+ return writePending || writeTimer !== null;
155
+ }
156
+
157
+ // Graceful shutdown - flush all pending data
158
+ function shutdown() {
159
+ if (writeTimer) {
160
+ clearTimeout(writeTimer);
161
+ writeTimer = null;
162
+ }
163
+ if (tasksCache !== null && writePending) {
164
+ try {
165
+ ensureDir();
166
+ fs.writeFileSync(TASKS_FILE, JSON.stringify({ tasks: tasksCache }, null, 2), 'utf-8');
167
+ console.log('[Storage] Data flushed on shutdown');
168
+ } catch (e) {
169
+ console.error('[Storage] Shutdown flush error:', e.message);
170
+ }
171
+ }
172
+ writePending = false;
173
+ }
174
+
175
+ module.exports = {
176
+ loadTasks,
177
+ saveTasks,
178
+ getTask,
179
+ saveTask,
180
+ deleteTask,
181
+ getAllTasks,
182
+ loadConfig,
183
+ saveConfig,
184
+ getTasksRaw,
185
+ saveTasksRaw,
186
+ hasRunningTasks,
187
+ flushTasks: flushWrite,
188
+ hasPendingWrite,
189
+ shutdown
190
+ };
task.md ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ 分析token-consumer.html的功能开发一个后端的版本
2
+ 循环并发任务放在后端,
3
+ 支持页面关闭后打开可以恢复任务
4
+ 可以支持多任务,每个任务都有一个标签
5
+ 页面上会显示当前任务的列表
6
+ 点击其中一个进入对应的参数设置
7
+ 输入的参数前后端后保存一份,打开页面先加载本地的数据,然后在加载远程的数据覆盖
8
+ 项目使用node开发
token-consumer.html ADDED
@@ -0,0 +1,1150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <title>OpenAI Token 消耗器</title>
7
+ <style>
8
+ *{box-sizing:border-box}
9
+ body{margin:0;background:#0f172a;font:14px/1.5 "Segoe UI","Microsoft YaHei",sans-serif;color:#e2e8f0}
10
+ .wrap{max-width:1400px;margin:0 auto;padding:18px 16px}
11
+ h1{margin:0 0 16px;font-size:26px;background:linear-gradient(135deg,#60a5fa,#a78bfa);-webkit-background-clip:text;-webkit-text-fill-color:transparent}
12
+ .card{background:linear-gradient(145deg,#1e293b,#0f172a);border:1px solid #334155;border-radius:12px;padding:20px;margin-bottom:16px;box-shadow:0 4px 20px rgba(0,0,0,0.3)}
13
+ .g2,.g3,.stats,.logs{display:grid;gap:16px}
14
+ .g2{grid-template-columns:repeat(auto-fit,minmax(280px,1fr))}
15
+ .g3{grid-template-columns:repeat(auto-fit,minmax(200px,1fr))}
16
+ label{display:flex;flex-direction:column;gap:6px;font-size:12px;color:#94a3b8}
17
+ input,textarea,select,button{font:inherit}
18
+ input,textarea,select{margin-top:0;border:1px solid #475569;border-radius:8px;padding:10px 12px;background:#1e293b;color:#e2e8f0;transition:border-color 0.2s,box-shadow 0.2s}
19
+ input:focus,textarea:focus,select:focus{outline:none;border-color:#60a5fa;box-shadow:0 0 0 3px rgba(96,165,250,0.2)}
20
+ input::placeholder,textarea::placeholder{color:#64748b}
21
+ input[type=checkbox]{width:18px;height:18px;accent-color:#60a5fa}
22
+ .toggle{display:flex;flex-direction:row;align-items:center;gap:8px;font-size:13px;color:#cbd5e1}
23
+ .toggle span{line-height:1.35}
24
+ textarea{min-height:80px;resize:vertical}
25
+ .row{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-top:12px}
26
+ button{padding:10px 18px;border:0;border-radius:8px;cursor:pointer;font-weight:500;transition:all 0.2s}
27
+ button:hover{transform:translateY(-1px)}
28
+ button:active{transform:translateY(0)}
29
+ .p{background:linear-gradient(135deg,#3b82f6,#2563eb);color:#fff;box-shadow:0 2px 10px rgba(59,130,246,0.4)}
30
+ .p:hover{box-shadow:0 4px 15px rgba(59,130,246,0.5)}
31
+ .d{background:linear-gradient(135deg,#ef4444,#dc2626);color:#fff;box-shadow:0 2px 10px rgba(239,68,68,0.4)}
32
+ .s{background:#1e293b;border:1px solid #475569;color:#e2e8f0}
33
+ .s:hover{border-color:#60a5fa}
34
+ button:disabled{opacity:.5;cursor:not-allowed;transform:none}
35
+ #ms,#rs{font-size:12px;color:#64748b}
36
+ .stats{grid-template-columns:repeat(auto-fit,minmax(120px,1fr))}
37
+ .box{border:1px solid #334155;border-radius:10px;background:linear-gradient(145deg,#1e293b,#0f172a);padding:12px;text-align:center}
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)}
45
+ .log.done{border-color:#22c55e;border-style:dashed}
46
+ .log.error{border-color:#ef4444;box-shadow:0 0 10px rgba(239,68,68,0.2)}
47
+ .log.stopped{border-color:#f59e0b;box-shadow:0 0 10px rgba(245,158,11,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}
55
+
56
+ /* Config Editor Scrollbar */
57
+ #configEditor::-webkit-scrollbar{width:10px;height:10px}
58
+ #configEditor::-webkit-scrollbar-track{background:#0f172a;border-radius:5px}
59
+ #configEditor::-webkit-scrollbar-thumb{background:linear-gradient(180deg,#475569,#334155);border-radius:5px;border:2px solid #0f172a}
60
+ #configEditor::-webkit-scrollbar-thumb:hover{background:linear-gradient(180deg,#60a5fa,#3b82f6)}
61
+ #configEditor::-webkit-scrollbar-corner{background:#0f172a}
62
+
63
+ /* Task list styles */
64
+ .task-list{display:flex;flex-direction:column;gap:12px}
65
+ .task-item{display:flex;flex-direction:column;padding:16px 18px;border:1px solid #334155;border-radius:12px;background:linear-gradient(145deg,#1e293b,#0f172a);cursor:pointer;transition:all 0.2s;position:relative;overflow:hidden}
66
+ .task-item:hover{transform:translateX(4px)}
67
+ .task-item.running{border-color:#3b82f6;box-shadow:0 0 20px rgba(59,130,246,0.15)}
68
+ .task-item.running::before{content:'';position:absolute;top:0;left:0;right:0;height:2px;background:linear-gradient(90deg,#3b82f6,#60a5fa,#3b82f6);animation:shimmer 2s infinite}
69
+ .task-item.paused{border-color:#f59e0b;box-shadow:0 0 20px rgba(245,158,11,0.15)}
70
+ .task-item.paused::before{content:'';position:absolute;top:0;left:0;right:0;height:2px;background:#f59e0b}
71
+ .task-item.selected{transform:translateX(4px)}
72
+ @keyframes shimmer{0%{background-position:-200% 0}100%{background-position:200% 0}}
73
+ .task-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}
74
+ .task-title{display:flex;align-items:center;gap:10px}
75
+ .task-label{font-weight:600;font-size:15px;color:#e2e8f0}
76
+ .status-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
77
+ .status-dot.running{background:#3b82f6;box-shadow:0 0 8px #3b82f6;animation:pulse 1.5s infinite}
78
+ .status-dot.paused{background:#f59e0b;box-shadow:0 0 8px #f59e0b}
79
+ .status-dot.completed{background:#22c55e}
80
+ .status-dot.stopped{background:#ef4444}
81
+ .status-dot.idle{background:#64748b}
82
+ @keyframes pulse{0%,100%{opacity:1}50%{opacity:0.5}}
83
+ .task-badge{padding:3px 10px;border-radius:6px;font-size:11px;font-weight:600}
84
+ .task-badge.running{background:rgba(59,130,246,0.15);color:#60a5fa;border:1px solid rgba(59,130,246,0.3)}
85
+ .task-badge.paused{background:rgba(245,158,11,0.15);color:#fbbf24;border:1px solid rgba(245,158,11,0.3)}
86
+ .task-badge.completed{background:rgba(34,197,94,0.15);color:#4ade80;border:1px solid rgba(34,197,94,0.3)}
87
+ .task-badge.stopped{background:rgba(239,68,68,0.15);color:#f87171;border:1px solid rgba(239,68,68,0.3)}
88
+ .task-badge.idle{background:rgba(100,116,139,0.15);color:#94a3b8;border:1px solid rgba(100,116,139,0.3)}
89
+ .task-progress{display:flex;flex-direction:column;gap:8px}
90
+ .progress-row{display:flex;align-items:center;gap:10px}
91
+ .progress-label{font-size:11px;color:#64748b;width:50px;flex-shrink:0}
92
+ .progress-bar{flex:1;height:6px;background:#1e293b;border-radius:3px;overflow:hidden}
93
+ .progress-fill{height:100%;border-radius:3px;transition:width 0.3s ease}
94
+ .progress-fill.task{background:linear-gradient(90deg,#3b82f6,#60a5fa)}
95
+ .progress-fill.token{background:linear-gradient(90deg,#8b5cf6,#a78bfa)}
96
+ .progress-text{font-size:11px;color:#94a3b8;width:80px;text-align:right;flex-shrink:0}
97
+ .task-meta{display:flex;align-items:center;gap:16px;margin-top:12px;padding-top:12px;border-top:1px solid #334155;flex-wrap:wrap}
98
+
99
+ /* Task Actions Bar */
100
+ .task-actions-bar{display:flex;gap:8px;margin-left:auto;flex-shrink:0;padding-top:2px}
101
+ .action-btn{padding:6px 16px;border-radius:6px;font-size:12px;font-weight:500;cursor:pointer;transition:all 0.2s;border:1px solid transparent}
102
+ .action-btn.disabled{opacity:0.4;cursor:not-allowed}
103
+ .start-btn{background:linear-gradient(135deg,#22c55e,#16a34a);color:#fff}
104
+ .start-btn:hover:not(.disabled){transform:translateY(-1px);box-shadow:0 2px 8px rgba(34,197,94,0.4)}
105
+ .pause-btn{background:linear-gradient(135deg,#f59e0b,#d97706);color:#fff}
106
+ .pause-btn:hover:not(.disabled){transform:translateY(-1px);box-shadow:0 2px 8px rgba(245,158,11,0.4)}
107
+ .stop-btn{background:linear-gradient(135deg,#ef4444,#dc2626);color:#fff}
108
+ .stop-btn:hover:not(.disabled){transform:translateY(-1px);box-shadow:0 2px 8px rgba(239,68,68,0.4)}
109
+ .meta-item{display:flex;align-items:center;gap:4px;font-size:11px;color:#64748b}
110
+ .meta-item svg{width:12px;height:12px;opacity:0.7}
111
+ .task-actions{display:flex;gap:8px}
112
+ .task-actions button{padding:6px 12px;font-size:12px}
113
+ .badge{display:inline-block;padding:2px 8px;border-radius:999px;font-size:10px;font-weight:600}
114
+ .badge.running{background:rgba(59,130,246,0.2);color:#60a5fa}
115
+ .badge.idle{background:rgba(100,116,139,0.3);color:#94a3b8}
116
+ .badge.completed{background:rgba(34,197,94,0.2);color:#4ade80}
117
+ .badge.stopped{background:rgba(245,158,11,0.2);color:#fbbf24}
118
+ .badge.paused{background:rgba(251,191,36,0.2);color:#fbbf24}
119
+
120
+ /* Navigation */
121
+ .nav{display:flex;gap:10px;margin-bottom:16px}
122
+ .nav-btn{padding:10px 20px;background:#1e293b;border:1px solid #475569;color:#94a3b8;border-radius:8px;cursor:pointer;transition:all 0.2s}
123
+ .nav-btn:hover{border-color:#60a5fa;color:#e2e8f0}
124
+ .nav-btn.active{background:linear-gradient(135deg,#3b82f6,#2563eb);border-color:#3b82f6;color:#fff}
125
+ .header{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px}
126
+ .header h1{margin:0}
127
+
128
+ /* Empty state */
129
+ .empty{text-align:center;padding:40px 20px;color:#64748b}
130
+ .empty-icon{font-size:48px;margin-bottom:12px;opacity:0.5}
131
+
132
+ /* Responsive */
133
+ @media(max-width:768px){
134
+ .stats{grid-template-columns:repeat(2,1fr)}
135
+ .g3{grid-template-columns:1fr}
136
+ .task-actions{flex-direction:column}
137
+ }
138
+
139
+ /* Status Card for Detail Page */
140
+ .status-card{display:flex;flex-direction:column;gap:0;padding:16px 18px;border-radius:12px;transition:all 0.3s;position:relative;overflow:hidden;background:linear-gradient(145deg,#1e293b,#0f172a)}
141
+ .status-card.idle{border-color:#334155}
142
+ .status-card.running{border-color:#3b82f6;box-shadow:0 0 20px rgba(59,130,246,0.15)}
143
+ .status-card.running::before{content:'';position:absolute;top:0;left:0;right:0;height:2px;background:linear-gradient(90deg,#3b82f6,#60a5fa,#3b82f6);animation:shimmer 2s infinite;background-size:200% 100%}
144
+ .status-card.paused{border-color:#f59e0b;box-shadow:0 0 20px rgba(245,158,11,0.15)}
145
+ .status-card.paused::before{content:'';position:absolute;top:0;left:0;right:0;height:2px;background:#f59e0b}
146
+ .status-card.completed{border-color:#22c55e}
147
+ .status-card.stopped{border-color:#ef4444}
148
+
149
+ /* Status Bar */
150
+ .status-bar{display:flex;align-items:center;gap:24px;padding:10px 18px;background:linear-gradient(145deg,#1e293b,#0f172a);border:1px solid #334155;border-radius:10px;margin-bottom:16px;font-size:12px}
151
+ .status-item{display:flex;align-items:center;gap:6px;color:#94a3b8}
152
+ .status-label{color:#64748b}
153
+ .status-item span:last-child{color:#60a5fa;font-weight:500}
154
+
155
+ /* Config Editor */
156
+ #configEditor{background:#0f172a;color:#e2e8f0;border:1px solid #475569;border-radius:8px;resize:vertical}
157
+ #configEditor:focus{outline:none;border-color:#60a5fa;box-shadow:0 0 0 3px rgba(96,165,250,0.2)}
158
+ </style>
159
+ </head>
160
+ <body>
161
+ <div class="wrap">
162
+ <div class="header">
163
+ <h1>OpenAI Token 消耗器</h1>
164
+ <div class="nav">
165
+ <button class="nav-btn active" id="navList">任务列表</button>
166
+ <button class="nav-btn" id="navNew">新建任务</button>
167
+ <button class="nav-btn" id="navConfig">配置编辑</button>
168
+ </div>
169
+ </div>
170
+
171
+ <!-- Status Bar -->
172
+ <div class="status-bar" id="statusBar">
173
+ <div class="status-item">
174
+ <span class="status-label">进程内存:</span>
175
+ <span id="processMem">--</span>
176
+ </div>
177
+ <div class="status-item">
178
+ <span class="status-label">系统内存:</span>
179
+ <span id="systemMem">--</span>
180
+ </div>
181
+ <div class="status-item">
182
+ <span class="status-label">系统使用率:</span>
183
+ <span id="systemMemPercent">--</span>
184
+ </div>
185
+ </div>
186
+
187
+ <!-- Task List View -->
188
+ <div id="listView">
189
+ <div class="card">
190
+ <div class="row" style="justify-content:space-between;margin-top:0">
191
+ <span style="color:#94a3b8;font-size:13px">点击任务进入详情,或创建新任务</span>
192
+ <button class="p" id="refreshBtn">刷新列表</button>
193
+ </div>
194
+ </div>
195
+ <div class="card">
196
+ <div class="task-list" id="taskList"></div>
197
+ </div>
198
+ </div>
199
+
200
+ <!-- Task Edit View -->
201
+ <div id="editView" style="display:none">
202
+ <div class="card">
203
+ <div class="row" style="justify-content:space-between;margin-top:0">
204
+ <button class="s" id="backBtn">← 返回列表</button>
205
+ <div id="taskHeader">
206
+ <span id="currentTaskLabel" style="font-weight:600;font-size:16px"></span>
207
+ <span id="currentTaskStatus" class="badge idle" style="margin-left:8px">空闲</span>
208
+ </div>
209
+ </div>
210
+ </div>
211
+
212
+ <div class="card status-card idle" id="statusCard">
213
+ <div class="task-header">
214
+ <div class="task-title">
215
+ <span class="status-dot idle" id="detailStatusDot"></span>
216
+ <span class="task-label" id="bannerLabel">空闲</span>
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>
223
+ <div class="progress-bar">
224
+ <div class="progress-fill task" id="detailTaskProgress" style="width:0%"></div>
225
+ </div>
226
+ <span class="progress-text" id="bannerProgress">0/0</span>
227
+ </div>
228
+ <div class="progress-row">
229
+ <span class="progress-label">Tokens</span>
230
+ <div class="progress-bar">
231
+ <div class="progress-fill token" id="detailTokenProgress" style="width:0%"></div>
232
+ </div>
233
+ <span class="progress-text" id="bannerTokens">0/0</span>
234
+ </div>
235
+ </div>
236
+ <div class="task-meta">
237
+ <div class="meta-item">
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>
244
+ </div>
245
+ <div class="meta-item">
246
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>
247
+ <span>失败 <b id="detailFailed" style="color:#f87171">0</b></span>
248
+ </div>
249
+ <div class="meta-item">
250
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="8" y1="12" x2="16" y2="12"/></svg>
251
+ <span>中止 <b id="detailAborted">0</b></span>
252
+ </div>
253
+ <div class="task-actions-bar" id="detailActions">
254
+ <button class="action-btn start-btn" id="detailStartBtn">启动</button>
255
+ <button class="action-btn pause-btn disabled" id="detailPauseBtn" disabled>暂停</button>
256
+ <button class="action-btn stop-btn disabled" id="detailStopBtn" disabled>停止</button>
257
+ </div>
258
+ </div>
259
+ </div>
260
+
261
+ <div class="card">
262
+ <div class="g2">
263
+ <label>任务标签<input id="label" placeholder="给任务起个名字"></label>
264
+ </div>
265
+ </div>
266
+
267
+ <div class="card">
268
+ <div class="g2">
269
+ <label>Base URL<input id="base" value="https://api.openai.com" placeholder="https://api.openai.com 或代理地址/v1"></label>
270
+ <label>Token<input id="token" type="password" placeholder="sk-..."></label>
271
+ <label>Model<input id="modelAuto" list="modelList" placeholder="Load models first"><datalist id="modelList"></datalist></label>
272
+ </div>
273
+ <div class="row">
274
+ <label class="toggle" style="margin-top:0"><input id="tokenShow" type="checkbox"><span>Show Token</span></label>
275
+ <button class="s" id="load">Load Models</button>
276
+ <span id="ms">Enter baseurl + token to auto load models.</span>
277
+ </div>
278
+ </div>
279
+
280
+ <div class="card">
281
+ <label>System Prompt(可选)<textarea id="sys">You are a concise assistant.</textarea></label>
282
+ <label style="margin-top:8px">User Prompt<textarea id="usr" placeholder="输入要重复调用的内容">写一个1000字的小说</textarea></label>
283
+ <div class="row">
284
+ <label class="toggle"><input id="randOn" type="checkbox"><span>添加随机标记</span></label>
285
+ <label class="toggle"><input id="streamOn" type="checkbox" checked><span>流式输出</span></label>
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>
293
+ <label>循环间隔毫秒<input id="waitBetween" type="number" min="0" value="1000"></label>
294
+ <label>累计tokens上限<input id="maxTokens" type="number" min="1000" value="1000000000"></label>
295
+ <label>目标输入/输出比例<input id="ratioTarget" type="number" min="0.1" max="20" step="0.1" value="1.0"></label>
296
+ </div>
297
+ <div class="row">
298
+ <button class="p" id="saveTask">保存任务</button>
299
+ <button class="p" id="start">启动任务</button>
300
+ <button class="s" id="pause" disabled style="background:#f59e0b;color:#fff">暂停</button>
301
+ <button class="d" id="stop" disabled>停止任务</button>
302
+ <button class="d" id="deleteTask">删除任务</button>
303
+ <span id="rs">尚未开始</span>
304
+ </div>
305
+ </div>
306
+
307
+ <div class="card">
308
+ <div class="stats">
309
+ <div class="box"><label>目标请求</label><b id="stt">0</b></div>
310
+ <div class="box"><label>已完成</label><b id="std">0</b></div>
311
+ <div class="box"><label>成功/失败/中止</label><b id="str">0/0/0</b></div>
312
+ <div class="box"><label>累计 tokens</label><b id="stk">0</b></div>
313
+ <div class="box"><label>输入/输出</label><b id="stp">0/0</b></div>
314
+ <div class="box"><label>耗时</label><b id="sec">0s</b></div>
315
+ </div>
316
+ </div>
317
+
318
+ <div class="card">
319
+ <div class="logs" id="logs"><div style="color:#64748b;text-align:center">启动后显示线程日志</div></div>
320
+ </div>
321
+ </div>
322
+
323
+ <!-- Config Edit View -->
324
+ <div id="configView" style="display:none">
325
+ <div class="card" id="configWarning" style="display:none;background:linear-gradient(145deg,#422006,#1e293b);border-color:#f59e0b">
326
+ <div style="display:flex;align-items:center;gap:10px">
327
+ <span style="font-size:18px">⚠️</span>
328
+ <span style="color:#fbbf24">有 <b id="runningTaskCount">0</b> 个任务正在运行,保存配置被禁用。请先暂停或停止运行中的任务(暂停状态可保存)。</span>
329
+ </div>
330
+ </div>
331
+ <div class="card">
332
+ <div class="row" style="justify-content:space-between;margin-top:0">
333
+ <span style="color:#94a3b8;font-size:13px">编辑 data/tasks.json 文件内容,或上传文件覆盖</span>
334
+ <div style="display:flex;gap:10px">
335
+ <label class="s" style="padding:10px 18px;border-radius:8px;cursor:pointer;display:flex;align-items:center;gap:6px">
336
+ <input type="file" id="configFileInput" accept=".json" style="display:none">
337
+ 上传文件
338
+ </label>
339
+ <button class="p" id="saveConfigBtn">保存配置</button>
340
+ <button class="s" id="reloadConfigBtn">重新加载</button>
341
+ </div>
342
+ </div>
343
+ </div>
344
+ <div class="card">
345
+ <textarea id="configEditor" style="width:100%;min-height:500px;font-family:Consolas,Monaco,monospace;font-size:13px;line-height:1.5;tab-size:2" placeholder="加载中..."></textarea>
346
+ </div>
347
+ </div>
348
+ </div>
349
+ </div>
350
+
351
+ <script>
352
+ const $=id=>document.getElementById(id);
353
+ const API=location.origin;
354
+ let currentTaskId=null;
355
+ let pollTimer=null;
356
+ let listPollTimer=null;
357
+ let tasks=[];
358
+
359
+ // Utility functions
360
+ const n=(v,d,min,max)=>{v=Number(v);if(!Number.isFinite(v))return d;return Math.min(max,Math.max(min,v))};
361
+ const fmt=n=>{if(n>=1e9)return(n/1e9).toFixed(2)+'B';if(n>=1e6)return(n/1e6).toFixed(2)+'M';if(n>=1e3)return(n/1e3).toFixed(1)+'K';return String(n)};
362
+ const fmtTime=ms=>{
363
+ const s=Math.floor(ms/1000);
364
+ const h=Math.floor(s/3600);
365
+ const m=Math.floor((s%3600)/60);
366
+ const sec=s%60;
367
+ if(h>0)return`${h}h${m}m${sec}s`;
368
+ if(m>0)return`${m}m${sec}s`;
369
+ return`${sec}s`;
370
+ };
371
+ const ms=(t,c='')=>{$('ms').style.color=c||'#64748b';$('ms').textContent=t};
372
+ const rs=(t,c='')=>{$('rs').style.color=c||'#64748b';$('rs').textContent=t};
373
+
374
+ // Local storage for config
375
+ const LOCAL_KEY='cpac.tokenConsumer.v2';
376
+ const getLocal=()=>{try{return JSON.parse(localStorage.getItem(LOCAL_KEY)||'{}')}catch{return{}}};
377
+ const saveLocal=d=>{try{localStorage.setItem(LOCAL_KEY,JSON.stringify(d))}catch{}};
378
+
379
+ // API functions
380
+ async function apiGet(url){const r=await fetch(API+url);return r.json()}
381
+ 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()}
382
+ async function apiDelete(url){const r=await fetch(API+url,{method:'DELETE'});return r.json()}
383
+
384
+ // Load tasks
385
+ async function loadTasks(){
386
+ try{
387
+ tasks=await apiGet('/api/tasks');
388
+ renderTaskList();
389
+ // Start list polling if any task is running
390
+ const hasRunning=tasks.some(t=>t.running);
391
+ if(hasRunning && !listPollTimer){
392
+ startListPolling();
393
+ }else if(!hasRunning && listPollTimer){
394
+ stopListPolling();
395
+ }
396
+ }catch(e){
397
+ console.error('加载任务失败:',e);
398
+ renderTaskList();
399
+ }
400
+ }
401
+
402
+ // List polling for task status updates
403
+ function startListPolling(){
404
+ if(listPollTimer)return;
405
+ listPollTimer=setInterval(loadTasks,2000);
406
+ }
407
+
408
+ function stopListPolling(){
409
+ if(listPollTimer){
410
+ clearInterval(listPollTimer);
411
+ listPollTimer=null;
412
+ }
413
+ }
414
+
415
+ // Render task list
416
+ function renderTaskList(){
417
+ const box=$('taskList');
418
+ if(!tasks.length){
419
+ box.innerHTML='<div class="empty"><div class="empty-icon">📋</div><div>暂无任务,点击"新建任务"创建</div></div>';
420
+ return;
421
+ }
422
+ box.innerHTML=tasks.map(t=>{
423
+ // currentStats has nested stats, flatten it
424
+ const currentStats=t.currentStats||{};
425
+ const stats=currentStats.stats||t.stats||{};
426
+ const taskProgress=stats.total>0?Math.round((stats.completed||0)/stats.total*100):0;
427
+ const tokenProgress=t.config?.maxTokens>0?Math.round((stats.totalTokens||0)/t.config.maxTokens*100):0;
428
+ const elapsed=currentStats.elapsed||0;
429
+ const elapsedStr=elapsed>0?fmtTime(elapsed):'';
430
+
431
+ let status, statusText;
432
+ const isPaused=t.running && t.currentStats?.paused;
433
+ if(isPaused){
434
+ status='paused';
435
+ statusText='已暂停';
436
+ }else if(t.running){
437
+ status='running';
438
+ statusText='运行中';
439
+ }else if(t.status==='completed'){
440
+ status='completed';
441
+ statusText='已完成';
442
+ }else if(t.status==='stopped'){
443
+ status='stopped';
444
+ statusText='已停止';
445
+ }else{
446
+ status='idle';
447
+ statusText='空闲';
448
+ }
449
+
450
+ // Button states (unified logic)
451
+ const btnStates=getButtonStates(t.running, isPaused);
452
+
453
+ return`
454
+ <div class="task-item ${t.id===currentTaskId?'selected':''} ${t.running?'running':''} ${isPaused?'paused':''}" data-id="${t.id}">
455
+ <div class="task-header">
456
+ <div class="task-title">
457
+ <span class="status-dot ${status}"></span>
458
+ <span class="task-label">${t.label||'未命名任务'}</span>
459
+ </div>
460
+ <span class="task-badge ${status}">${statusText}</span>
461
+ </div>
462
+ <div class="task-progress">
463
+ <div class="progress-row">
464
+ <span class="progress-label">任务</span>
465
+ <div class="progress-bar">
466
+ <div class="progress-fill task" style="width:${taskProgress}%"></div>
467
+ </div>
468
+ <span class="progress-text">${stats.completed||0}/${stats.total||0}</span>
469
+ </div>
470
+ <div class="progress-row">
471
+ <span class="progress-label">Tokens</span>
472
+ <div class="progress-bar">
473
+ <div class="progress-fill token" style="width:${Math.min(tokenProgress,100)}%"></div>
474
+ </div>
475
+ <span class="progress-text">${fmt(stats.totalTokens||0)}/${fmt(t.config?.maxTokens||0)}</span>
476
+ </div>
477
+ </div>
478
+ <div class="task-meta">
479
+ <div class="meta-item">
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>
486
+ </div>
487
+ <div class="meta-item">
488
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>
489
+ <span>失败 <b style="color:#f87171">${stats.failed||0}</b></span>
490
+ </div>
491
+ <div class="meta-item">
492
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="8" y1="12" x2="16" y2="12"/></svg>
493
+ <span>中止 ${stats.aborted||0}</span>
494
+ </div>
495
+ <div class="task-actions-bar" data-task-id="${t.id}">
496
+ <button class="action-btn start-btn ${btnStates.canStart?'':'disabled'}" data-action="start" ${btnStates.canStart?'':'disabled'}>${btnStates.isPaused?'继续':'启动'}</button>
497
+ <button class="action-btn pause-btn ${btnStates.canPause?'':'disabled'}" data-action="pause" ${btnStates.canPause?'':'disabled'}>暂停</button>
498
+ <button class="action-btn stop-btn ${btnStates.canStop?'':'disabled'}" data-action="stop" ${btnStates.canStop?'':'disabled'}>停止</button>
499
+ </div>
500
+ </div>
501
+ </div>
502
+ `;
503
+ }).join('');
504
+
505
+ // Add click handlers
506
+ box.querySelectorAll('.task-item').forEach(el=>{
507
+ el.addEventListener('click',()=>openTask(el.dataset.id));
508
+ });
509
+
510
+ // Add action button handlers
511
+ box.querySelectorAll('.action-btn').forEach(btn=>{
512
+ btn.addEventListener('click',async(e)=>{
513
+ e.stopPropagation(); // Prevent card click
514
+ const action=btn.dataset.action;
515
+ const taskId=btn.closest('.task-actions-bar').dataset.taskId;
516
+
517
+ if(action==='start'){
518
+ await apiPost('/api/tasks/'+taskId+'/start',{});
519
+ }else if(action==='pause'){
520
+ await apiPost('/api/tasks/'+taskId+'/pause',{});
521
+ }else if(action==='stop'){
522
+ await apiPost('/api/tasks/'+taskId+'/stop',{});
523
+ }
524
+ await loadTasks();
525
+ });
526
+ });
527
+ }
528
+
529
+ // Open task for editing
530
+ async function openTask(id){
531
+ currentTaskId=id;
532
+ const task=await apiGet('/api/tasks/'+id);
533
+ if(task.error){
534
+ alert('加载任务失败:'+task.error);
535
+ return;
536
+ }
537
+
538
+ // Load local config first, then merge with server
539
+ const local=getLocal();
540
+ const merged={...local,...task.config,id:task.id,label:task.label};
541
+
542
+ fillForm(merged);
543
+ updateTaskHeader(task);
544
+ showEditView();
545
+ startPolling(id);
546
+ }
547
+
548
+ // Fill form with task data
549
+ function fillForm(d){
550
+ $('label').value=d.label||'';
551
+ $('base').value=d.base||'https://api.openai.com';
552
+ $('token').value=d.token||'';
553
+ $('modelAuto').value=d.model||'';
554
+ $('sys').value=d.sys||'You are a concise assistant.';
555
+ $('usr').value=d.usr||'';
556
+ $('loop').value=d.loop||10;
557
+ $('thr').value=d.threads||3;
558
+ $('max').value=d.max||1024;
559
+ $('temp').value=d.temp||1;
560
+ $('to').value=(d.timeout||60000)/1000;
561
+ $('waitBetween').value=d.waitBetween||1000;
562
+ $('maxTokens').value=d.maxTokens||1000000000;
563
+ $('randOn').checked=!!d.randOn;
564
+ $('streamOn').checked=d.streamOn!==false;
565
+ $('ratioTarget').value=d.ratioTarget||1;
566
+ }
567
+
568
+ // Get form data
569
+ function getFormData(){
570
+ return{
571
+ id:currentTaskId,
572
+ label:$('label').value.trim()||`任务-${Date.now().toString(36)}`,
573
+ config:{
574
+ base:$('base').value.trim(),
575
+ token:$('token').value.trim(),
576
+ model:$('modelAuto').value.trim(),
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,200),
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,
584
+ waitBetween:n($('waitBetween').value,1000,0,60000),
585
+ maxTokens:n($('maxTokens').value,1e9,1000,1e12),
586
+ randOn:$('randOn').checked,
587
+ streamOn:$('streamOn').checked,
588
+ ratioTarget:n($('ratioTarget').value,1,0.1,20)
589
+ }
590
+ };
591
+ }
592
+
593
+ // Save task
594
+ async function saveTask(){
595
+ const data=getFormData();
596
+ try{
597
+ const result=await apiPost('/api/tasks',data);
598
+ if(result.error){
599
+ alert('保存失败:'+result.error);
600
+ return;
601
+ }
602
+ currentTaskId=result.id;
603
+
604
+ // Also save to local
605
+ saveLocal({...data.config,id:result.id,label:data.label});
606
+
607
+ rs('已保存','#22c55e');
608
+ await loadTasks();
609
+ }catch(e){
610
+ alert('保存失败:'+e.message);
611
+ }
612
+ }
613
+
614
+ // Start task
615
+ async function startTask(){
616
+ await saveTask();
617
+ try{
618
+ const result=await apiPost('/api/tasks/'+currentTaskId+'/start',{});
619
+ if(result.error){
620
+ alert('启动失败:'+result.error);
621
+ return;
622
+ }
623
+ rs(result.message||'任务已启动','#3b82f6');
624
+ startPolling(currentTaskId);
625
+ await loadTasks();
626
+ }catch(e){
627
+ alert('启动失败:'+e.message);
628
+ }
629
+ }
630
+
631
+ // Pause task
632
+ async function pauseTask(){
633
+ try{
634
+ const result=await apiPost('/api/tasks/'+currentTaskId+'/pause',{});
635
+ if(result.error){
636
+ alert('暂停失败:'+result.error);
637
+ return;
638
+ }
639
+ rs('任务已暂停','#f59e0b');
640
+ updateButtons(true, false, true); // running, paused
641
+ await loadTasks();
642
+ }catch(e){
643
+ alert('暂停失败:'+e.message);
644
+ }
645
+ }
646
+
647
+ // Stop task
648
+ async function stopTask(){
649
+ try{
650
+ const result=await apiPost('/api/tasks/'+currentTaskId+'/stop',{});
651
+ if(result.error){
652
+ alert('停止失败:'+result.error);
653
+ return;
654
+ }
655
+ rs('任务已停止','#f59e0b');
656
+ await loadTasks();
657
+ }catch(e){
658
+ alert('停止失败:'+e.message);
659
+ }
660
+ }
661
+
662
+ // Update button states
663
+ // Calculate button states (unified logic)
664
+ function getButtonStates(running, paused){
665
+ const isRunning=running && !paused;
666
+ return{
667
+ canStart:!running || paused, // can start if idle or paused
668
+ canPause:isRunning, // can pause if running (not paused)
669
+ canStop:running, // can stop if running or paused
670
+ isPaused:paused
671
+ };
672
+ }
673
+
674
+ // Update button states (used by both list cards and detail page)
675
+ function applyButtonStates(btns, states){
676
+ if(btns.start){
677
+ btns.start.disabled=!states.canStart;
678
+ btns.start.textContent=states.isPaused?'继续':'启动';
679
+ btns.start.classList.toggle('disabled', !states.canStart);
680
+ }
681
+ if(btns.pause){
682
+ btns.pause.disabled=!states.canPause;
683
+ btns.pause.classList.toggle('disabled', !states.canPause);
684
+ }
685
+ if(btns.stop){
686
+ btns.stop.disabled=!states.canStop;
687
+ btns.stop.classList.toggle('disabled', !states.canStop);
688
+ }
689
+ }
690
+
691
+ function updateButtons(running, paused, stopped){
692
+ const states=getButtonStates(running, paused);
693
+
694
+ // Bottom config buttons
695
+ $('start').disabled=!states.canStart;
696
+ $('start').textContent=states.isPaused?'继续':'启动任务';
697
+ $('pause').disabled=!states.canPause;
698
+ $('stop').disabled=!states.canStop;
699
+
700
+ // Detail page action buttons
701
+ applyButtonStates({
702
+ start:$('detailStartBtn'),
703
+ pause:$('detailPauseBtn'),
704
+ stop:$('detailStopBtn')
705
+ }, states);
706
+ }
707
+
708
+ // Delete task
709
+ async function deleteTask(){
710
+ if(!confirm('确定删除此任务?'))return;
711
+ try{
712
+ await apiDelete('/api/tasks/'+currentTaskId);
713
+ currentTaskId=null;
714
+ stopPolling();
715
+ showListView();
716
+ await loadTasks();
717
+ }catch(e){
718
+ alert('删除失败:'+e.message);
719
+ }
720
+ }
721
+
722
+ // Load models
723
+ async function loadModels(){
724
+ const base=$('base').value.trim();
725
+ const token=$('token').value.trim();
726
+ if(!base||!token){
727
+ ms('请输入 baseurl + token','#f59e0b');
728
+ return;
729
+ }
730
+ ms('正在加载模型...','#3b82f6');
731
+ try{
732
+ const result=await apiPost('/api/models',{base,token});
733
+ if(result.error){
734
+ ms('加载失败:'+result.error,'#ef4444');
735
+ return;
736
+ }
737
+ const models=result.models||[];
738
+ $('modelList').innerHTML=models.map(m=>`<option value="${m}">`).join('');
739
+ if(models.length&&!$('modelAuto').value)$('modelAuto').value=models[0];
740
+ ms('已加载 '+models.length+' 个模型','#22c55e');
741
+ }catch(e){
742
+ ms('加载失败:'+e.message,'#ef4444');
743
+ }
744
+ }
745
+
746
+ // Update stats display
747
+ function updateStats(s){
748
+ $('stt').textContent=s.stats?.total||0;
749
+ $('std').textContent=s.stats?.completed||0;
750
+ $('str').textContent=`${s.stats?.success||0}/${s.stats?.failed||0}/${s.stats?.aborted||0}`;
751
+ $('stk').textContent=fmt(s.stats?.totalTokens||0);
752
+ const pct=s.stats?.completionTokens>0?(s.stats?.promptTokens/s.stats?.completionTokens*100).toFixed(1):'0';
753
+ $('stp').textContent=`${fmt(s.stats?.promptTokens||0)}/${fmt(s.stats?.completionTokens||0)} (${pct}%)`;
754
+ $('sec').textContent=fmtTime(s.elapsed||0);
755
+
756
+ // Update status card
757
+ const total=s.stats?.total||0;
758
+ const completed=s.stats?.completed||0;
759
+ const totalTokens=s.stats?.totalTokens||0;
760
+ const maxTokens=Number($('maxTokens').value)||1000000000;
761
+
762
+ const taskProgress=total>0?Math.round(completed/total*100):0;
763
+ const tokenProgress=maxTokens>0?Math.min(Math.round(totalTokens/maxTokens*100),100):0;
764
+
765
+ $('detailTaskProgress').style.width=taskProgress+'%';
766
+ $('detailTokenProgress').style.width=tokenProgress+'%';
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;
773
+ }
774
+
775
+ // Update thread logs
776
+ function updateLogs(threadLogs){
777
+ const box=$('logs');
778
+ if(!threadLogs||!Object.keys(threadLogs).length){
779
+ box.innerHTML='<div style="color:#64748b;text-align:center">启动后显示线程日志</div>';
780
+ return;
781
+ }
782
+ box.innerHTML=Object.entries(threadLogs).map(([id,log])=>{
783
+ const statusClass=log.status==='running'||log.status==='streaming'?'running':
784
+ log.status==='success'?'success':
785
+ log.status==='error'?'error':
786
+ log.status==='aborted'?'stopped':
787
+ log.status==='paused'?'paused':'waiting';
788
+ // Display error first if status is error, otherwise show content/message
789
+ let displayContent='等待中...';
790
+ if(log.status==='error'){
791
+ displayContent='❌ '+(log.error||'请求失败');
792
+ }else if(log.status==='paused'){
793
+ displayContent='⏸️ '+(log.message||'已暂停');
794
+ }else if(log.status==='aborted'){
795
+ displayContent='⏹️ '+(log.message||'已中止');
796
+ }else if(log.content){
797
+ displayContent=log.content;
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">循环:${log.loop||'-'}</div>
805
+ <pre>${displayContent.slice(-500)}</pre>
806
+ </div>
807
+ `;
808
+ }).join('');
809
+ }
810
+
811
+ // Update task header
812
+ function updateTaskHeader(task){
813
+ $('currentTaskLabel').textContent=task.label||'未命名任务';
814
+
815
+ let status, statusText;
816
+ if(task.running && task.currentStats?.paused){
817
+ status='paused';
818
+ statusText='已暂停';
819
+ }else if(task.running){
820
+ status='running';
821
+ statusText='运行中';
822
+ }else if(task.status==='completed'){
823
+ status='completed';
824
+ statusText='已完成';
825
+ }else if(task.status==='stopped'){
826
+ status='stopped';
827
+ statusText='已停止';
828
+ }else{
829
+ status='idle';
830
+ statusText='空闲';
831
+ }
832
+
833
+ const badge=$('currentTaskStatus');
834
+ badge.textContent=statusText;
835
+ badge.className='badge '+status;
836
+
837
+ // Update status card
838
+ const statusCard=$('statusCard');
839
+ statusCard.className='card status-card '+status;
840
+ $('detailStatusDot').className='status-dot '+status;
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
+ }
847
+
848
+ // Start polling for task status
849
+ function startPolling(taskId){
850
+ stopPolling();
851
+ pollTimer=setInterval(async()=>{
852
+ try{
853
+ const s=await apiGet('/api/tasks/'+taskId+'/status');
854
+ updateStats(s);
855
+ updateLogs(s.threadLogs);
856
+ updateButtons(s.running, s.paused, s.stopped);
857
+
858
+ // Update status card
859
+ let status='idle', statusText='空闲';
860
+ if(s.running && s.paused){
861
+ status='paused';
862
+ statusText='已暂停';
863
+ }else if(s.running){
864
+ status='running';
865
+ statusText='运行中';
866
+ }else if(s.stopped){
867
+ status='stopped';
868
+ statusText='已停止';
869
+ }
870
+ const statusCard=$('statusCard');
871
+ statusCard.className='card status-card '+status;
872
+ $('detailStatusDot').className='status-dot '+status;
873
+ $('detailBadge').className='task-badge '+status;
874
+ $('detailBadge').textContent=statusText;
875
+ $('bannerLabel').textContent=statusText;
876
+
877
+ if(!s.running){
878
+ $('start').textContent='启动任务';
879
+ await loadTasks();
880
+ }else{
881
+ $('start').textContent=s.paused?'继续':'启动任务';
882
+ }
883
+ }catch(e){
884
+ console.error('轮询失败:',e);
885
+ }
886
+ },1000);
887
+ }
888
+
889
+ // Stop polling
890
+ function stopPolling(){
891
+ if(pollTimer){
892
+ clearInterval(pollTimer);
893
+ pollTimer=null;
894
+ }
895
+ }
896
+
897
+ // View navigation
898
+ function showListView(){
899
+ stopPolling();
900
+ currentTaskId=null;
901
+ $('listView').style.display='block';
902
+ $('editView').style.display='none';
903
+ $('configView').style.display='none';
904
+ $('navList').classList.add('active');
905
+ $('navNew').classList.remove('active');
906
+ $('navConfig').classList.remove('active');
907
+ loadTasks(); // This will start list polling if needed
908
+ }
909
+
910
+ function showEditView(){
911
+ stopListPolling();
912
+ $('listView').style.display='none';
913
+ $('editView').style.display='block';
914
+ $('configView').style.display='none';
915
+ $('navList').classList.remove('active');
916
+ $('navNew').classList.add('active');
917
+ $('navConfig').classList.remove('active');
918
+ }
919
+
920
+ function showConfigView(){
921
+ stopPolling();
922
+ stopListPolling();
923
+ $('listView').style.display='none';
924
+ $('editView').style.display='none';
925
+ $('configView').style.display='block';
926
+ $('navList').classList.remove('active');
927
+ $('navNew').classList.remove('active');
928
+ $('navConfig').classList.add('active');
929
+ // Load tasks first to check running status, then load config
930
+ loadTasks().then(()=>{
931
+ updateConfigWarning();
932
+ loadConfigRaw();
933
+ });
934
+ }
935
+
936
+ // Config Editor functions
937
+ async function loadConfigRaw(){
938
+ try{
939
+ const result=await apiGet('/api/tasks-raw');
940
+ if(result.error){
941
+ alert('加载失败: '+result.error);
942
+ return;
943
+ }
944
+ // Format JSON
945
+ const parsed=JSON.parse(result.content);
946
+ $('configEditor').value=JSON.stringify(parsed, null, 2);
947
+ // Update running status
948
+ updateConfigWarning();
949
+ }catch(e){
950
+ alert('加载失败: '+e.message);
951
+ }
952
+ }
953
+
954
+ function updateConfigWarning(){
955
+ // Only count tasks that are running AND not paused (paused tasks don't write to file)
956
+ const runningCount=tasks.filter(t=>t.running && !t.currentStats?.paused).length;
957
+ const warning=$('configWarning');
958
+ const count=$('runningTaskCount');
959
+ const saveBtn=$('saveConfigBtn');
960
+
961
+ count.textContent=runningCount;
962
+
963
+ if(runningCount>0){
964
+ warning.style.display='block';
965
+ saveBtn.disabled=true;
966
+ saveBtn.classList.remove('p');
967
+ saveBtn.classList.add('s');
968
+ saveBtn.style.opacity='0.5';
969
+ }else{
970
+ warning.style.display='none';
971
+ saveBtn.disabled=false;
972
+ saveBtn.classList.remove('s');
973
+ saveBtn.classList.add('p');
974
+ saveBtn.style.opacity='1';
975
+ }
976
+ }
977
+
978
+ async function saveConfigRaw(){
979
+ const content=$('configEditor').value;
980
+ if(!content.trim()){
981
+ alert('内容不能为空');
982
+ return;
983
+ }
984
+
985
+ // Validate JSON first
986
+ try{
987
+ JSON.parse(content);
988
+ }catch(e){
989
+ alert('JSON 格式无效: '+e.message);
990
+ return;
991
+ }
992
+
993
+ if(!confirm('确定要保存配置吗?这将覆盖现有的 tasks.json 文件。'))return;
994
+
995
+ try{
996
+ const result=await apiPost('/api/tasks-raw', {content});
997
+ if(result.error){
998
+ // Check if it's a conflict error (task running)
999
+ if(result.error.includes('正在运行')){
1000
+ alert('无法保存:' + result.error + '\n\n提示:暂停的任务可以保存配置,请先暂停或停止运行中的任务。');
1001
+ }else{
1002
+ alert('保存失败: '+result.error);
1003
+ }
1004
+ return;
1005
+ }
1006
+ alert('保存成功');
1007
+ await loadTasks();
1008
+ }catch(e){
1009
+ alert('保存失败: '+e.message);
1010
+ }
1011
+ }
1012
+
1013
+ function handleConfigFileUpload(e){
1014
+ const file=e.target.files[0];
1015
+ if(!file)return;
1016
+
1017
+ const reader=new FileReader();
1018
+ reader.onload=(ev)=>{
1019
+ try{
1020
+ const content=ev.target.result;
1021
+ const parsed=JSON.parse(content);
1022
+ $('configEditor').value=JSON.stringify(parsed, null, 2);
1023
+ alert('文件已加载,点击"保存配置"按钮保存');
1024
+ }catch(err){
1025
+ alert('文件不是有效的 JSON 格式: '+err.message);
1026
+ }
1027
+ };
1028
+ reader.readAsText(file);
1029
+ e.target.value=''; // Reset input
1030
+ }
1031
+
1032
+ // Create new task
1033
+ function newTask(){
1034
+ currentTaskId=null;
1035
+ const local=getLocal();
1036
+ fillForm(local);
1037
+ $('currentTaskLabel').textContent='新任务';
1038
+ $('currentTaskStatus').textContent='新建';
1039
+ $('currentTaskStatus').className='badge idle';
1040
+
1041
+ // Reset status card
1042
+ const statusCard=$('statusCard');
1043
+ statusCard.className='card status-card idle';
1044
+ $('detailStatusDot').className='status-dot idle';
1045
+ $('detailBadge').className='task-badge idle';
1046
+ $('detailBadge').textContent='新建';
1047
+ $('bannerLabel').textContent='新建';
1048
+ $('detailTaskProgress').style.width='0%';
1049
+ $('detailTokenProgress').style.width='0%';
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';
1056
+
1057
+ updateButtons(false, false, false);
1058
+ showEditView();
1059
+ stopPolling();
1060
+ }
1061
+
1062
+ // Event handlers
1063
+ $('navList').onclick=showListView;
1064
+ $('navNew').onclick=newTask;
1065
+ $('navConfig').onclick=showConfigView;
1066
+ $('backBtn').onclick=showListView;
1067
+ $('refreshBtn').onclick=loadTasks;
1068
+ $('load').onclick=loadModels;
1069
+ $('saveTask').onclick=saveTask;
1070
+ $('start').onclick=startTask;
1071
+ $('pause').onclick=pauseTask;
1072
+ $('stop').onclick=stopTask;
1073
+ $('deleteTask').onclick=deleteTask;
1074
+ $('tokenShow').onchange=()=>$('token').type=$('tokenShow').checked?'text':'password';
1075
+ $('saveConfigBtn').onclick=saveConfigRaw;
1076
+ $('reloadConfigBtn').onclick=loadConfigRaw;
1077
+ $('configFileInput').onchange=handleConfigFileUpload;
1078
+
1079
+ // Detail page action buttons
1080
+ $('detailStartBtn').onclick=startTask;
1081
+ $('detailPauseBtn').onclick=pauseTask;
1082
+ $('detailStopBtn').onclick=stopTask;
1083
+
1084
+ // Memory status update
1085
+ function formatBytes(bytes){
1086
+ if(bytes>=1e9)return(bytes/1e9).toFixed(1)+' GB';
1087
+ if(bytes>=1e6)return(bytes/1e6).toFixed(0)+' MB';
1088
+ if(bytes>=1e3)return(bytes/1e3).toFixed(0)+' KB';
1089
+ return bytes+' B';
1090
+ }
1091
+
1092
+ async function updateMemoryStatus(){
1093
+ try{
1094
+ const mem=await apiGet('/api/memory');
1095
+ $('processMem').textContent=formatBytes(mem.process.rss);
1096
+ $('systemMem').textContent=formatBytes(mem.system.used)+' / '+formatBytes(mem.system.total);
1097
+ $('systemMemPercent').textContent=mem.system.usagePercent+'%';
1098
+
1099
+ // Color based on usage
1100
+ const percent=mem.system.usagePercent;
1101
+ const el=$('systemMemPercent');
1102
+ if(percent>90)el.style.color='#ef4444';
1103
+ else if(percent>70)el.style.color='#f59e0b';
1104
+ else el.style.color='#60a5fa';
1105
+ }catch(e){
1106
+ console.error('Memory status error:',e);
1107
+ }
1108
+ }
1109
+
1110
+ // Update memory every 5 seconds
1111
+ setInterval(updateMemoryStatus,5000);
1112
+ updateMemoryStatus();
1113
+
1114
+ // Auto-load models on base/token change
1115
+ let loadTimer=null;
1116
+ const autoLoad=()=>{
1117
+ if(loadTimer)clearTimeout(loadTimer);
1118
+ loadTimer=setTimeout(()=>{
1119
+ if($('base').value.trim()&&$('token').value.trim())loadModels();
1120
+ },500);
1121
+ };
1122
+ $('base').oninput=autoLoad;
1123
+ $('token').oninput=autoLoad;
1124
+
1125
+ // Init
1126
+ showListView();
1127
+ loadTasks();
1128
+
1129
+ // Load local config and try to merge with server
1130
+ (async()=>{
1131
+ const local=getLocal();
1132
+ if(local.base||local.token){
1133
+ $('base').value=local.base||'https://api.openai.com';
1134
+ $('token').value=local.token||'';
1135
+ }
1136
+ // Load server config
1137
+ try{
1138
+ const serverConfig=await apiGet('/api/config');
1139
+ if(Object.keys(serverConfig).length){
1140
+ saveLocal({...local,...serverConfig});
1141
+ }
1142
+ }catch(e){}
1143
+
1144
+ if($('base').value.trim()&&$('token').value.trim()){
1145
+ loadModels();
1146
+ }
1147
+ })();
1148
+ </script>
1149
+ </body>
1150
+ </html>