079project 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/GroupStarter.cjs +647 -0
- package/LICENSE +165 -0
- package/PropagateSignalUseJsWorker.js +92 -0
- package/README.md +102 -0
- package/Redis-8.0.3-Windows-x64-cygwin-with-Service/README.md +52 -0
- package/Redis-8.0.3-Windows-x64-cygwin-with-Service/README.zh_CN.md +59 -0
- package/Redis-8.0.3-Windows-x64-cygwin-with-Service/RedisService.exe +0 -0
- package/Redis-8.0.3-Windows-x64-cygwin-with-Service/cygcrypto-3.dll +0 -0
- package/Redis-8.0.3-Windows-x64-cygwin-with-Service/cyggcc_s-seh-1.dll +0 -0
- package/Redis-8.0.3-Windows-x64-cygwin-with-Service/cygssl-3.dll +0 -0
- package/Redis-8.0.3-Windows-x64-cygwin-with-Service/cygstdc++-6.dll +0 -0
- package/Redis-8.0.3-Windows-x64-cygwin-with-Service/cygwin1.dll +0 -0
- package/Redis-8.0.3-Windows-x64-cygwin-with-Service/cygz.dll +0 -0
- package/Redis-8.0.3-Windows-x64-cygwin-with-Service/dump.rdb +0 -0
- package/Redis-8.0.3-Windows-x64-cygwin-with-Service/install_redis_service.bat +100 -0
- package/Redis-8.0.3-Windows-x64-cygwin-with-Service/redis-benchmark.exe +0 -0
- package/Redis-8.0.3-Windows-x64-cygwin-with-Service/redis-check-aof.exe +0 -0
- package/Redis-8.0.3-Windows-x64-cygwin-with-Service/redis-check-rdb.exe +0 -0
- package/Redis-8.0.3-Windows-x64-cygwin-with-Service/redis-cli.exe +0 -0
- package/Redis-8.0.3-Windows-x64-cygwin-with-Service/redis-full.conf +376 -0
- package/Redis-8.0.3-Windows-x64-cygwin-with-Service/redis-sentinel.exe +0 -0
- package/Redis-8.0.3-Windows-x64-cygwin-with-Service/redis-server.exe +0 -0
- package/Redis-8.0.3-Windows-x64-cygwin-with-Service/redis.conf +2348 -0
- package/Redis-8.0.3-Windows-x64-cygwin-with-Service/sentinel.conf +361 -0
- package/Redis-8.0.3-Windows-x64-cygwin-with-Service/start.bat +4 -0
- package/Redis-8.0.3-Windows-x64-cygwin-with-Service/uninstall_redis_service.bat +30 -0
- package/boot.py +51 -0
- package/chat_Client.js +29 -0
- package/controller.cjs +118 -0
- package/enhancedForwarder.js +378 -0
- package/forwarder.js +1456 -0
- package/groupmanager.cjs +143 -0
- package/howToStart.txt +8 -0
- package/lemma.csv +210 -0
- package/load.py +35 -0
- package/mainManager.cjs +81 -0
- package/mainStarter.cjs +535 -0
- package/main_Serve.cjs +2745 -0
- package/main_Study.cjs +3230 -0
- package/memeMergeWorker.cjs +55 -0
- package/model_RNN.py +117 -0
- package/note.txt +5 -0
- package/notebook.txt +8 -0
- package/npminstall-debug.log +206 -0
- package/package.json +48 -0
- package/public/chat_straight.html +90 -0
- package/public/index.html +247 -0
- package/public/indexmain.html +136 -0
- package/public/monitor.html +194 -0
- package/robots/wikitext-something.txt +25 -0
- package/runtime.proto +24 -0
- package/runtime_data.json +766294 -0
- package/serializer_seq2seq.h5 +0 -0
- package/start.js +46 -0
- package/tests/test_FIrststep1.txt +1224 -0
- package/tests/test_FIrststep2.txt +2956 -0
- package/tests/test_FIrststep3.txt +1224 -0
- package/tests/test_FIrststep4.txt +1396 -0
- package/tests/test_FIrststep5.txt +2852 -0
- package/tests/test_FIrststep6.txt +1516 -0
- package/tests/test_FirstStep7.txt +1748 -0
- package/tests/test_Firstsetp8.txt +2672 -0
- package/tokenizer.json +1 -0
- package/vocabularySplitter.js +253 -0
- package/wikitext/.gitattributes +27 -0
- package/wikitext/README.md +344 -0
- package/wikitext/describtion.txt +1 -0
package/GroupStarter.cjs
ADDED
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
// ...existing code...
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { spawn } = require('child_process');
|
|
5
|
+
const express = require('express');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const { exec } = require('child_process');
|
|
8
|
+
const axios = require('axios');
|
|
9
|
+
|
|
10
|
+
// 轻量参数解析(不引入依赖)
|
|
11
|
+
function parseArgs(argv) {
|
|
12
|
+
const out = {};
|
|
13
|
+
for (let i = 0; i < argv.length; i++) {
|
|
14
|
+
const a = argv[i];
|
|
15
|
+
if (a.startsWith('--')) {
|
|
16
|
+
const key = a.slice(2);
|
|
17
|
+
const val = argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[++i] : true;
|
|
18
|
+
out[key] = val;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
const args = parseArgs(process.argv.slice(2));
|
|
24
|
+
|
|
25
|
+
// 配置(可被 CLI 参数或环境变量覆盖)
|
|
26
|
+
let GROUPS_DIR = path.resolve(args['groups-dir'] || process.env.GROUPS_DIR || path.join(__dirname, 'groups'));
|
|
27
|
+
let SERVE_COUNT = Number(args['serve-count'] || process.env.SERVE_COUNT || 4);
|
|
28
|
+
let PORT_BASE = Number(args['port-base'] || process.env.PORT_BASE || 12001);
|
|
29
|
+
let MASTER_PORT = Number(args['master-port'] || process.env.MASTER_PORT || 9100);
|
|
30
|
+
let GROUP_START_DELAY = Number(args['group-start-delay'] || process.env.GROUP_START_DELAY || 3000);
|
|
31
|
+
let ROBOTS_DIR_OVERRIDE = args['robots-dir'] ? path.resolve(args['robots-dir']) : '';
|
|
32
|
+
let TESTS_DIR_OVERRIDE = args['tests-dir'] ? path.resolve(args['tests-dir']) : '';
|
|
33
|
+
let PUBLIC_DIR_OVERRIDE = args['public-dir'] ? path.resolve(args['public-dir']) : '';
|
|
34
|
+
let SNAPSHOTS_DIR_OVERRIDE = args['snapshots-dir'] ? path.resolve(args['snapshots-dir']) : '';
|
|
35
|
+
const FORWARDER_OFFSET = SERVE_COUNT; // forwarder端口偏移
|
|
36
|
+
|
|
37
|
+
console.log('[CFG] GROUPS_DIR=', GROUPS_DIR);
|
|
38
|
+
console.log('[CFG] SERVE_COUNT=', SERVE_COUNT, ' PORT_BASE=', PORT_BASE, ' MASTER_PORT=', MASTER_PORT);
|
|
39
|
+
if (ROBOTS_DIR_OVERRIDE) console.log('[CFG] ROBOTS_DIR_OVERRIDE=', ROBOTS_DIR_OVERRIDE);
|
|
40
|
+
if (TESTS_DIR_OVERRIDE) console.log('[CFG] TESTS_DIR_OVERRIDE=', TESTS_DIR_OVERRIDE);
|
|
41
|
+
if (PUBLIC_DIR_OVERRIDE) console.log('[CFG] PUBLIC_DIR_OVERRIDE=', PUBLIC_DIR_OVERRIDE);
|
|
42
|
+
if (SNAPSHOTS_DIR_OVERRIDE) console.log('[CFG] SNAPSHOTS_DIR_OVERRIDE=', SNAPSHOTS_DIR_OVERRIDE);
|
|
43
|
+
|
|
44
|
+
// 读取所有 group_x 文件夹
|
|
45
|
+
const groupFolders = fs.existsSync(GROUPS_DIR)
|
|
46
|
+
? fs.readdirSync(GROUPS_DIR).filter((f) => /^group_\d+$/.test(f)).map((f) => path.join(GROUPS_DIR, f))
|
|
47
|
+
: [];
|
|
48
|
+
// ...existing code...
|
|
49
|
+
// 记录所有 group 的 forwarder 端口与子进程
|
|
50
|
+
const groupForwarderPorts = [];
|
|
51
|
+
const groupProcesses = [];
|
|
52
|
+
|
|
53
|
+
// 运行时注册表,便于管理与重启
|
|
54
|
+
const groupRegistry = []; // { id, dir, servePorts: [{pid,port}], study: {pid,port}, forwarder: {pid,port} }
|
|
55
|
+
|
|
56
|
+
// 资源监控与自适应节流
|
|
57
|
+
const METRICS_WINDOW = 300; // 保留最近300个点
|
|
58
|
+
const metrics = {
|
|
59
|
+
ts: [],
|
|
60
|
+
cpu: [], // 0..1
|
|
61
|
+
mem: [], // 使用率 0..1
|
|
62
|
+
disk: [], // [{ drive, free, total }]
|
|
63
|
+
gpu: [], // 简化 [{ util, memUsed, memTotal }]
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
let lastCpuSample = os.cpus();
|
|
67
|
+
let emergencyBuffer = null; // 应急释放缓冲,避免立刻 OOM
|
|
68
|
+
let swapActive = false; // 是否处于“内存应急”状态
|
|
69
|
+
|
|
70
|
+
// 计算CPU占用(基于两次 /os.cpus 快照)
|
|
71
|
+
function sampleCpu() {
|
|
72
|
+
const now = os.cpus();
|
|
73
|
+
const util = now.map((core, idx) => {
|
|
74
|
+
const p = lastCpuSample[idx];
|
|
75
|
+
const idle = core.times.idle - p.times.idle;
|
|
76
|
+
const total = Object.keys(core.times).reduce((s, k) => s + (core.times[k] - p.times[k]), 0);
|
|
77
|
+
const busy = Math.max(0, total - idle);
|
|
78
|
+
return total > 0 ? busy / total : 0;
|
|
79
|
+
});
|
|
80
|
+
lastCpuSample = now;
|
|
81
|
+
return util.reduce((a, b) => a + b, 0) / Math.max(1, util.length);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function sampleMem() {
|
|
85
|
+
const total = os.totalmem();
|
|
86
|
+
const free = os.freemem();
|
|
87
|
+
const usedRatio = (total - free) / total;
|
|
88
|
+
return { usedRatio, total, free };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function pushMetric(key, value) {
|
|
92
|
+
const ts = Date.now();
|
|
93
|
+
metrics.ts.push(ts);
|
|
94
|
+
if (metrics.ts.length > METRICS_WINDOW) metrics.ts.shift();
|
|
95
|
+
const pushArr = (arr, v) => {
|
|
96
|
+
arr.push(v);
|
|
97
|
+
if (arr.length > METRICS_WINDOW) arr.shift();
|
|
98
|
+
};
|
|
99
|
+
if (key === 'cpu') pushArr(metrics.cpu, value);
|
|
100
|
+
if (key === 'mem') pushArr(metrics.mem, value);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 尝试采样磁盘(Windows: wmic)
|
|
104
|
+
async function sampleDiskOnce() {
|
|
105
|
+
return new Promise((resolve) => {
|
|
106
|
+
exec(
|
|
107
|
+
'wmic logicaldisk get size,freespace,caption',
|
|
108
|
+
{ windowsHide: true },
|
|
109
|
+
(err, stdout) => {
|
|
110
|
+
if (err || !stdout) return resolve([]);
|
|
111
|
+
const lines = stdout.split(/\r?\n/).filter(Boolean).slice(1);
|
|
112
|
+
const out = [];
|
|
113
|
+
for (const line of lines) {
|
|
114
|
+
const parts = line.trim().split(/\s+/);
|
|
115
|
+
if (parts.length >= 3) {
|
|
116
|
+
const [caption, free, size] = [
|
|
117
|
+
parts[0],
|
|
118
|
+
Number(parts[1] || 0),
|
|
119
|
+
Number(parts[2] || 0),
|
|
120
|
+
];
|
|
121
|
+
out.push({ drive: caption, free, total: size });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
resolve(out);
|
|
125
|
+
}
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 尝试采样GPU(若有NVIDIA)
|
|
131
|
+
async function sampleGpuOnce() {
|
|
132
|
+
return new Promise((resolve) => {
|
|
133
|
+
exec(
|
|
134
|
+
'nvidia-smi --query-gpu=utilization.gpu,memory.used,memory.total --format=csv,noheader,nounits',
|
|
135
|
+
{ windowsHide: true },
|
|
136
|
+
(err, stdout) => {
|
|
137
|
+
if (err || !stdout) return resolve([]);
|
|
138
|
+
const lines = stdout.split(/\r?\n/).filter(Boolean);
|
|
139
|
+
const out = lines.map((l) => {
|
|
140
|
+
const [util, memUsed, memTotal] = l
|
|
141
|
+
.split(',')
|
|
142
|
+
.map((s) => Number(s.trim()));
|
|
143
|
+
return {
|
|
144
|
+
util: util / 100,
|
|
145
|
+
memUsed: memUsed * 1024 * 1024,
|
|
146
|
+
memTotal: memTotal * 1024 * 1024,
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
resolve(out);
|
|
150
|
+
}
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function tryAllocEmergencyBuffer() {
|
|
156
|
+
if (!emergencyBuffer) {
|
|
157
|
+
try {
|
|
158
|
+
// 预留一块可释放内存(64MB,不至于影响正常运行)
|
|
159
|
+
emergencyBuffer = Buffer.allocUnsafe(64 * 1024 * 1024);
|
|
160
|
+
} catch (_) {
|
|
161
|
+
emergencyBuffer = null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function releaseEmergencyBuffer() {
|
|
167
|
+
if (emergencyBuffer) {
|
|
168
|
+
emergencyBuffer = null;
|
|
169
|
+
global.gc && global.gc(); // 如 --expose-gc 则主动GC
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function startResourceMonitor() {
|
|
174
|
+
tryAllocEmergencyBuffer();
|
|
175
|
+
setInterval(async () => {
|
|
176
|
+
const cpu = sampleCpu();
|
|
177
|
+
pushMetric('cpu', cpu);
|
|
178
|
+
const { usedRatio } = sampleMem();
|
|
179
|
+
pushMetric('mem', usedRatio);
|
|
180
|
+
// 每30秒采样一次磁盘与GPU
|
|
181
|
+
if (metrics.ts.length % 30 === 0) {
|
|
182
|
+
try {
|
|
183
|
+
metrics.disk = await sampleDiskOnce();
|
|
184
|
+
} catch (_) {}
|
|
185
|
+
try {
|
|
186
|
+
metrics.gpu = await sampleGpuOnce();
|
|
187
|
+
} catch (_) {}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// 自适应:高内存时进入应急模式,释放预留;恢复后重新预留
|
|
191
|
+
if (usedRatio >= 0.92 && !swapActive) {
|
|
192
|
+
swapActive = true;
|
|
193
|
+
releaseEmergencyBuffer();
|
|
194
|
+
} else if (swapActive && usedRatio <= 0.8) {
|
|
195
|
+
swapActive = false;
|
|
196
|
+
tryAllocEmergencyBuffer();
|
|
197
|
+
}
|
|
198
|
+
}, 1000);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function startAllGroups() {
|
|
202
|
+
for (let i = 0; i < groupFolders.length; i++) {
|
|
203
|
+
const groupDir = groupFolders[i];
|
|
204
|
+
const reg = { id: i, dir: groupDir, servePorts: [], study: null, forwarder: null };
|
|
205
|
+
const servePorts = [];
|
|
206
|
+
|
|
207
|
+
for (let j = 0; j < SERVE_COUNT; j++) {
|
|
208
|
+
const port = PORT_BASE + i * (SERVE_COUNT + 2) + j;
|
|
209
|
+
servePorts.push(port);
|
|
210
|
+
const serveScript = path.join(groupDir, 'main_Serve.cjs');
|
|
211
|
+
if (fs.existsSync(serveScript)) {
|
|
212
|
+
let attempts = 0;
|
|
213
|
+
while (attempts < 3) {
|
|
214
|
+
try {
|
|
215
|
+
const proc = spawn('node', [serveScript, port.toString(),'--expose-gc'], {
|
|
216
|
+
cwd: groupDir,
|
|
217
|
+
stdio: 'inherit',
|
|
218
|
+
});
|
|
219
|
+
groupProcesses.push(proc);
|
|
220
|
+
reg.servePorts.push({ pid: null, port });
|
|
221
|
+
proc.on('spawn', () => {
|
|
222
|
+
const idx = reg.servePorts.findIndex((p) => p.port === port);
|
|
223
|
+
if (idx >= 0) reg.servePorts[idx].pid = proc.pid;
|
|
224
|
+
});
|
|
225
|
+
break;
|
|
226
|
+
} catch (error) {
|
|
227
|
+
attempts++;
|
|
228
|
+
console.warn(
|
|
229
|
+
`[WARN] 启动服务 ${serveScript} 失败 (尝试 ${attempts}/3): ${error.message}`
|
|
230
|
+
);
|
|
231
|
+
if (attempts === 3)
|
|
232
|
+
console.error(`[ERROR] 无法启动服务 ${serveScript}`);
|
|
233
|
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// study端口
|
|
240
|
+
const studyPort = PORT_BASE + i * (SERVE_COUNT + 2) + SERVE_COUNT;
|
|
241
|
+
const studyScript = path.join(groupDir, 'main_Study.cjs');
|
|
242
|
+
if (fs.existsSync(studyScript)) {
|
|
243
|
+
const proc = spawn(
|
|
244
|
+
'node',
|
|
245
|
+
[studyScript, studyPort.toString(), '--max-old-space-size=12288', '--expose-gc'],
|
|
246
|
+
{ cwd: groupDir, stdio: 'inherit' }
|
|
247
|
+
);
|
|
248
|
+
groupProcesses.push(proc);
|
|
249
|
+
reg.study = { pid: null, port: studyPort };
|
|
250
|
+
proc.on('spawn', () => {
|
|
251
|
+
reg.study.pid = proc.pid;
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// forwarder端口
|
|
256
|
+
const forwarderPort = PORT_BASE + i * (SERVE_COUNT + 2) + FORWARDER_OFFSET + 1;
|
|
257
|
+
groupForwarderPorts.push(forwarderPort);
|
|
258
|
+
const forwarderScript = path.join(groupDir, 'forwarder.js');
|
|
259
|
+
if (fs.existsSync(forwarderScript)) {
|
|
260
|
+
const proc = spawn(
|
|
261
|
+
'node',
|
|
262
|
+
[
|
|
263
|
+
forwarderScript,
|
|
264
|
+
forwarderPort.toString(),
|
|
265
|
+
...servePorts.map((p) => p.toString()),
|
|
266
|
+
studyPort.toString(),
|
|
267
|
+
'--expose-gc'
|
|
268
|
+
],
|
|
269
|
+
{ cwd: groupDir, stdio: 'inherit' }
|
|
270
|
+
);
|
|
271
|
+
groupProcesses.push(proc);
|
|
272
|
+
reg.forwarder = { pid: null, port: forwarderPort };
|
|
273
|
+
proc.on('spawn', () => {
|
|
274
|
+
reg.forwarder.pid = proc.pid;
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// 写入组配置文件
|
|
279
|
+
const config = { servePorts, studyPort, forwarderPort };
|
|
280
|
+
fs.writeFileSync(
|
|
281
|
+
path.join(groupDir, 'group_ports.json'),
|
|
282
|
+
JSON.stringify(config, null, 2)
|
|
283
|
+
);
|
|
284
|
+
console.log(
|
|
285
|
+
`[START] group_${i} 端口分配: serve=${servePorts.join(',')}, study=${studyPort}, forwarder=${forwarderPort}`
|
|
286
|
+
);
|
|
287
|
+
groupRegistry.push(reg);
|
|
288
|
+
await new Promise((r) => setTimeout(r, GROUP_START_DELAY));
|
|
289
|
+
}
|
|
290
|
+
console.log(`[INFO] 共启动 ${groupFolders.length} 个 group`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function startMasterForwarder() {
|
|
294
|
+
const app = express();
|
|
295
|
+
app.use(express.json({ limit: '800mb' }));
|
|
296
|
+
// 新增:托管前端静态资源
|
|
297
|
+
app.use(express.static(path.join(__dirname, 'public')));
|
|
298
|
+
|
|
299
|
+
// 启动资源监控
|
|
300
|
+
startResourceMonitor();
|
|
301
|
+
|
|
302
|
+
// 新增:语料流接口(从各组 robots/*.txt 逐行推送至前端)
|
|
303
|
+
app.get('/api/corpus', async (req, res) => {
|
|
304
|
+
try {
|
|
305
|
+
const max = Math.min(parseInt(req.query.max || '2000', 10), 200000);
|
|
306
|
+
const gStart = Math.max(parseInt(req.query.groupStart || '0', 10), 0);
|
|
307
|
+
const gEnd = Math.min(
|
|
308
|
+
parseInt(req.query.groupEnd || String(groupFolders.length - 1), 10),
|
|
309
|
+
groupFolders.length - 1
|
|
310
|
+
);
|
|
311
|
+
const format = (req.query.format || 'ndjson').toLowerCase(); // ndjson | text
|
|
312
|
+
|
|
313
|
+
if (format === 'ndjson') {
|
|
314
|
+
res.setHeader('Content-Type', 'application/x-ndjson; charset=utf-8');
|
|
315
|
+
} else {
|
|
316
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const { createInterface } = require('readline');
|
|
320
|
+
let count = 0;
|
|
321
|
+
|
|
322
|
+
for (let i = gStart; i <= gEnd && count < max; i++) {
|
|
323
|
+
const robotsDir = ROBOTS_DIR_OVERRIDE || path.join(groupFolders[i], 'robots');
|
|
324
|
+
if (!fs.existsSync(robotsDir)) continue;
|
|
325
|
+
const files = fs
|
|
326
|
+
.readdirSync(robotsDir)
|
|
327
|
+
.filter((f) => f.toLowerCase().endsWith('.txt'));
|
|
328
|
+
|
|
329
|
+
for (const f of files) {
|
|
330
|
+
if (count >= max) break;
|
|
331
|
+
const full = path.join(robotsDir, f);
|
|
332
|
+
const stream = fs.createReadStream(full, { encoding: 'utf-8' });
|
|
333
|
+
const rl = createInterface({ input: stream, crlfDelay: Infinity });
|
|
334
|
+
|
|
335
|
+
for await (const line of rl) {
|
|
336
|
+
const t = (line || '').trim();
|
|
337
|
+
if (!t) continue;
|
|
338
|
+
if (format === 'ndjson') {
|
|
339
|
+
res.write(JSON.stringify({ g: i, t }) + '\n');
|
|
340
|
+
} else {
|
|
341
|
+
res.write(t + '\n');
|
|
342
|
+
}
|
|
343
|
+
count++;
|
|
344
|
+
if (count >= max) {
|
|
345
|
+
rl.close();
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
res.end();
|
|
352
|
+
} catch (e) {
|
|
353
|
+
res.status(500).json({ error: e.message });
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// 指标接口:返回最近窗口的时序数据
|
|
358
|
+
app.get('/api/metrics', (req, res) => {
|
|
359
|
+
res.json({
|
|
360
|
+
ts: metrics.ts,
|
|
361
|
+
cpu: metrics.cpu,
|
|
362
|
+
mem: metrics.mem,
|
|
363
|
+
disk: metrics.disk,
|
|
364
|
+
gpu: metrics.gpu,
|
|
365
|
+
swapActive,
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
// 管理:重启指定组或类型
|
|
370
|
+
app.post('/api/admin/restart', async (req, res) => {
|
|
371
|
+
try {
|
|
372
|
+
const { groupId, type } = req.body; // type: 'serve'|'study'|'forwarder'|'all'
|
|
373
|
+
if (
|
|
374
|
+
typeof groupId !== 'number' ||
|
|
375
|
+
groupId < 0 ||
|
|
376
|
+
groupId >= groupRegistry.length
|
|
377
|
+
) {
|
|
378
|
+
return res.status(400).json({ error: 'invalid groupId' });
|
|
379
|
+
}
|
|
380
|
+
const reg = groupRegistry[groupId];
|
|
381
|
+
const spawnServe = () => {
|
|
382
|
+
const serveScript = path.join(reg.dir, 'main_Serve.cjs');
|
|
383
|
+
for (const sp of reg.servePorts) {
|
|
384
|
+
const p = spawn('node', [serveScript, sp.port.toString(), '--expose-gc','--max-old-space-size=16384'], {
|
|
385
|
+
cwd: reg.dir,
|
|
386
|
+
stdio: 'inherit',
|
|
387
|
+
});
|
|
388
|
+
p.on('spawn', () => {
|
|
389
|
+
sp.pid = p.pid;
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
const spawnStudy = () => {
|
|
394
|
+
const studyScript = path.join(reg.dir, 'main_Study.cjs');
|
|
395
|
+
const p = spawn(
|
|
396
|
+
'node',
|
|
397
|
+
[studyScript, reg.study.port.toString(), '--max-old-space-size=16384', '--expose-gc'],
|
|
398
|
+
{ cwd: reg.dir, stdio: 'inherit' }
|
|
399
|
+
);
|
|
400
|
+
p.on('spawn', () => {
|
|
401
|
+
reg.study.pid = p.pid;
|
|
402
|
+
});
|
|
403
|
+
};
|
|
404
|
+
const spawnForwarder = () => {
|
|
405
|
+
const forwarderScript = path.join(reg.dir, 'forwarder.js');
|
|
406
|
+
const p = spawn(
|
|
407
|
+
'node',
|
|
408
|
+
[
|
|
409
|
+
forwarderScript,
|
|
410
|
+
reg.forwarder.port.toString(),
|
|
411
|
+
...reg.servePorts.map((s) => s.port.toString()),
|
|
412
|
+
reg.study.port.toString(),
|
|
413
|
+
'--expose-gc',
|
|
414
|
+
'--max-old-space-size=16384'
|
|
415
|
+
],
|
|
416
|
+
{ cwd: reg.dir, stdio: 'inherit' }
|
|
417
|
+
);
|
|
418
|
+
p.on('spawn', () => {
|
|
419
|
+
reg.forwarder.pid = p.pid;
|
|
420
|
+
});
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
const killIf = (pid) => {
|
|
424
|
+
try {
|
|
425
|
+
process.kill(pid);
|
|
426
|
+
} catch (_) {}
|
|
427
|
+
};
|
|
428
|
+
if (type === 'serve' || type === 'all') {
|
|
429
|
+
reg.servePorts.forEach((sp) => sp.pid && killIf(sp.pid));
|
|
430
|
+
spawnServe();
|
|
431
|
+
}
|
|
432
|
+
if (type === 'study' || type === 'all') {
|
|
433
|
+
reg.study?.pid && killIf(reg.study.pid);
|
|
434
|
+
spawnStudy();
|
|
435
|
+
}
|
|
436
|
+
if (type === 'forwarder' || type === 'all') {
|
|
437
|
+
reg.forwarder?.pid && killIf(reg.forwarder.pid);
|
|
438
|
+
spawnForwarder();
|
|
439
|
+
}
|
|
440
|
+
res.json({ ok: true });
|
|
441
|
+
} catch (e) {
|
|
442
|
+
res.status(500).json({ error: e.message });
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
// 管理:批量更新某一类文件到所有组(如 robots 资源或脚本)
|
|
447
|
+
app.post('/api/admin/batch-update', async (req, res) => {
|
|
448
|
+
try {
|
|
449
|
+
const { relativePath, content, sourcePath } = req.body; // 二选一:直接内容或从源文件复制
|
|
450
|
+
if (!relativePath)
|
|
451
|
+
return res.status(400).json({ error: 'relativePath required' });
|
|
452
|
+
const updated = [];
|
|
453
|
+
for (const reg of groupRegistry) {
|
|
454
|
+
const dst = path.join(reg.dir, relativePath);
|
|
455
|
+
fs.mkdirSync(path.dirname(dst), { recursive: true });
|
|
456
|
+
if (typeof content === 'string') {
|
|
457
|
+
fs.writeFileSync(dst, content, 'utf-8');
|
|
458
|
+
} else if (typeof sourcePath === 'string' && fs.existsSync(sourcePath)) {
|
|
459
|
+
fs.copyFileSync(sourcePath, dst);
|
|
460
|
+
} else {
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
updated.push(dst);
|
|
464
|
+
}
|
|
465
|
+
res.json({ ok: true, updatedCount: updated.length });
|
|
466
|
+
} catch (e) {
|
|
467
|
+
res.status(500).json({ error: e.message });
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
// 聚合器:自适应采样与节流
|
|
472
|
+
app.post('/api/chat', async (req, res) => {
|
|
473
|
+
const { message } = req.body || {};
|
|
474
|
+
if (!message || typeof message !== 'string') {
|
|
475
|
+
return res.status(400).json({ error: '消息不能为空' });
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// 轻微节流:高负载时延迟处理,避免CPU尖峰
|
|
479
|
+
const curCpu = metrics.cpu[metrics.cpu.length - 1] || 0;
|
|
480
|
+
const curMem = metrics.mem[metrics.mem.length - 1] || 0;
|
|
481
|
+
const throttleMs = curCpu > 0.9 ? 150 : curCpu > 0.8 ? 60 : 0;
|
|
482
|
+
if (throttleMs) await new Promise((r) => setTimeout(r, throttleMs));
|
|
483
|
+
|
|
484
|
+
// 采样规模自适应
|
|
485
|
+
const ALL = groupForwarderPorts.slice();
|
|
486
|
+
let baseSize = Math.min(
|
|
487
|
+
Math.max(8, Math.floor(Math.sqrt(ALL.length || 100))),
|
|
488
|
+
64
|
|
489
|
+
);
|
|
490
|
+
let factor = 1.0;
|
|
491
|
+
if (curCpu > 0.85) factor *= 0.6;
|
|
492
|
+
if (curMem > 0.9 || swapActive) factor *= 0.6;
|
|
493
|
+
const SAMPLE_SIZE = Math.max(2, Math.floor(baseSize * factor));
|
|
494
|
+
|
|
495
|
+
const samplePorts = (allPorts, m) => {
|
|
496
|
+
const arr = [...allPorts];
|
|
497
|
+
const out = [];
|
|
498
|
+
for (let i = 0; i < Math.min(m, arr.length); i++) {
|
|
499
|
+
const idx = Math.floor(Math.random() * arr.length);
|
|
500
|
+
out.push(arr.splice(idx, 1)[0]);
|
|
501
|
+
}
|
|
502
|
+
return out;
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
const SELECTED = samplePorts(ALL, SAMPLE_SIZE);
|
|
506
|
+
|
|
507
|
+
// 历史统计持久化
|
|
508
|
+
const STATS_PATH = path.join(__dirname, 'group_stats.json');
|
|
509
|
+
const loadStats = () => {
|
|
510
|
+
try {
|
|
511
|
+
return JSON.parse(fs.readFileSync(STATS_PATH, 'utf-8'));
|
|
512
|
+
} catch {
|
|
513
|
+
return {};
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
const saveStats = (s) => {
|
|
517
|
+
try {
|
|
518
|
+
fs.writeFileSync(STATS_PATH, JSON.stringify(s, null, 2));
|
|
519
|
+
} catch {}
|
|
520
|
+
};
|
|
521
|
+
const stats = loadStats();
|
|
522
|
+
|
|
523
|
+
async function queryPorts(ports) {
|
|
524
|
+
const promises = ports.map((port) => {
|
|
525
|
+
const t0 = Date.now();
|
|
526
|
+
return axios
|
|
527
|
+
.post(`http://localhost:${port}/api/chat`, { message }, { timeout: 20000 })
|
|
528
|
+
.then((r) => ({
|
|
529
|
+
port,
|
|
530
|
+
ok: true,
|
|
531
|
+
text: r.data && r.data.response ? r.data.response : '',
|
|
532
|
+
latency: Date.now() - t0,
|
|
533
|
+
}))
|
|
534
|
+
.catch(() => ({ port, ok: false, text: '', latency: Date.now() - t0 }));
|
|
535
|
+
});
|
|
536
|
+
return Promise.all(promises);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// 中间层“周期拆分”:CPU>80%时将一次查询分两批执行,批间让出事件循环
|
|
540
|
+
let settled = [];
|
|
541
|
+
if (curCpu > 0.8 && SELECTED.length > 2) {
|
|
542
|
+
const mid = Math.ceil(SELECTED.length / 2);
|
|
543
|
+
const first = SELECTED.slice(0, mid);
|
|
544
|
+
const second = SELECTED.slice(mid);
|
|
545
|
+
const r1 = await queryPorts(first);
|
|
546
|
+
settled.push(...r1);
|
|
547
|
+
await new Promise((r) => setImmediate(r));
|
|
548
|
+
const r2 = await queryPorts(second);
|
|
549
|
+
settled.push(...r2);
|
|
550
|
+
} else {
|
|
551
|
+
settled = await queryPorts(SELECTED);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// 更新统计(简单EWMA/计数)
|
|
555
|
+
settled.forEach((r) => {
|
|
556
|
+
if (!stats[r.port])
|
|
557
|
+
stats[r.port] = { score: 1, requests: 0, success: 0, avgLatency: r.latency || 0 };
|
|
558
|
+
const s = stats[r.port];
|
|
559
|
+
s.requests = (s.requests || 0) + 1;
|
|
560
|
+
if (r.ok && r.text) s.success = (s.success || 0) + 1;
|
|
561
|
+
s.avgLatency = ((s.avgLatency * (s.requests - 1)) + r.latency) / s.requests;
|
|
562
|
+
s.score = Math.max(
|
|
563
|
+
0.1,
|
|
564
|
+
Math.min(10, ((s.success || 0) / s.requests) * 2 + (s.score || 1) * 0.8)
|
|
565
|
+
);
|
|
566
|
+
});
|
|
567
|
+
saveStats(stats);
|
|
568
|
+
|
|
569
|
+
const scoreResp = (text, port) => {
|
|
570
|
+
const s = stats[port] || { score: 1, avgLatency: 1000, requests: 0, success: 0 };
|
|
571
|
+
const lenScore = Math.min(1, (text.length || 0) / 200);
|
|
572
|
+
const latencyScore = 1 / (1 + (s.avgLatency || 1000) / 500);
|
|
573
|
+
return 0.6 * (s.score || 1) + 0.3 * lenScore + 0.1 * latencyScore;
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
// 只保留有效响应并打分
|
|
577
|
+
const scored = settled
|
|
578
|
+
.filter((r) => r.ok && r.text && r.text.length > 0)
|
|
579
|
+
.map((r) => ({ ...r, score: scoreResp(r.text, r.port) }))
|
|
580
|
+
.sort((a, b) => b.score - a.score);
|
|
581
|
+
|
|
582
|
+
if (scored.length === 0) {
|
|
583
|
+
return res.status(502).json({ error: '所有组无有效响应' });
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// 去重并返回 top K
|
|
587
|
+
const seen = new Set();
|
|
588
|
+
const unique = [];
|
|
589
|
+
for (const item of scored) {
|
|
590
|
+
const key = item.text.trim().slice(0, 200);
|
|
591
|
+
if (!seen.has(key)) {
|
|
592
|
+
seen.add(key);
|
|
593
|
+
unique.push(item);
|
|
594
|
+
}
|
|
595
|
+
if (unique.length >= 10) break;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
res.json({
|
|
599
|
+
top: unique[0].text,
|
|
600
|
+
responses: unique.map((u) => ({ port: u.port, response: u.text, score: u.score })),
|
|
601
|
+
sampled: SELECTED.length,
|
|
602
|
+
totalCandidates: ALL.length,
|
|
603
|
+
cpu: curCpu,
|
|
604
|
+
mem: curMem,
|
|
605
|
+
throttledMs: throttleMs,
|
|
606
|
+
});
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
// 健康检查
|
|
610
|
+
app.get('/health', (req, res) => {
|
|
611
|
+
res.json({ status: 'ok', groups: groupForwarderPorts.length });
|
|
612
|
+
});
|
|
613
|
+
app.get('/', (req, res) => {
|
|
614
|
+
res.sendFile(path.join(__dirname, 'public', 'index.html'));
|
|
615
|
+
});
|
|
616
|
+
// 监控页面
|
|
617
|
+
app.get('/monitor', (req, res) => {
|
|
618
|
+
res.sendFile(path.join(__dirname, 'public', 'monitor.html'));
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
// 启动服务
|
|
622
|
+
app.listen(MASTER_PORT, () => {
|
|
623
|
+
console.log(`[MASTER] GroupStarter master forwarder已启动,端口 ${MASTER_PORT}`);
|
|
624
|
+
console.log(`[MASTER] 静态前端: http://localhost:${MASTER_PORT}/`);
|
|
625
|
+
console.log(
|
|
626
|
+
`[MASTER] 语料流: GET /api/corpus?max=1000&groupStart=0&groupEnd=10&format=ndjson`
|
|
627
|
+
);
|
|
628
|
+
console.log(`[MASTER] 对外API: POST /api/chat(前端不会调用,可留作他用)`);
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// 启动所有组和总控 forwarder
|
|
633
|
+
(async () => {
|
|
634
|
+
await startAllGroups();
|
|
635
|
+
startMasterForwarder();
|
|
636
|
+
})();
|
|
637
|
+
|
|
638
|
+
// 进程退出处理
|
|
639
|
+
process.on('SIGINT', () => {
|
|
640
|
+
console.log('收到退出信号,正在关闭所有工作组进程...');
|
|
641
|
+
for (const proc of groupProcesses) {
|
|
642
|
+
try {
|
|
643
|
+
proc.kill();
|
|
644
|
+
} catch (e) {}
|
|
645
|
+
}
|
|
646
|
+
process.exit(0);
|
|
647
|
+
});
|