079project 1.0.0 → 2.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 +211 -35
- package/README.md +3 -3
- package/loggerworker.cjs +202 -0
- package/main_Serve.cjs +157 -66
- package/main_Study.cjs +217 -68
- package/package.json +1 -1
- package/todo-list.txt +86 -0
- package/wikitext/wikitext-103-all.txt +0 -0
- package/wikitext/.gitattributes +0 -27
- package/wikitext/README.md +0 -344
- package/wikitext/describtion.txt +0 -1
package/GroupStarter.cjs
CHANGED
|
@@ -6,6 +6,24 @@ const express = require('express');
|
|
|
6
6
|
const os = require('os');
|
|
7
7
|
const { exec } = require('child_process');
|
|
8
8
|
const axios = require('axios');
|
|
9
|
+
// 新增:日志子进程
|
|
10
|
+
let loggerProc = null;
|
|
11
|
+
function startLogger() {
|
|
12
|
+
if (loggerProc && !loggerProc.killed) return;
|
|
13
|
+
const script = path.join(__dirname, 'loggerWorker.cjs');
|
|
14
|
+
if (!fs.existsSync(script)) {
|
|
15
|
+
console.warn('[LOGGER] 缺少 loggerWorker.cjs,跳过启动');
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
loggerProc = spawn('node', [script], { cwd: __dirname, stdio: ['inherit', 'inherit', 'inherit', 'ipc'] });
|
|
19
|
+
loggerProc.on('exit', () => { loggerProc = null; setTimeout(startLogger, 2000); });
|
|
20
|
+
}
|
|
21
|
+
function loggerSend(msg) {
|
|
22
|
+
try { loggerProc && loggerProc.connected && loggerProc.send(msg); } catch (_) {}
|
|
23
|
+
}
|
|
24
|
+
// ...existing code...
|
|
25
|
+
|
|
26
|
+
|
|
9
27
|
|
|
10
28
|
// 轻量参数解析(不引入依赖)
|
|
11
29
|
function parseArgs(argv) {
|
|
@@ -34,12 +52,30 @@ let PUBLIC_DIR_OVERRIDE = args['public-dir'] ? path.resolve(args['public-dir'])
|
|
|
34
52
|
let SNAPSHOTS_DIR_OVERRIDE = args['snapshots-dir'] ? path.resolve(args['snapshots-dir']) : '';
|
|
35
53
|
const FORWARDER_OFFSET = SERVE_COUNT; // forwarder端口偏移
|
|
36
54
|
|
|
55
|
+
let USE_PM2 = !!(args['use-pm2'] || process.env.USE_PM2);
|
|
37
56
|
console.log('[CFG] GROUPS_DIR=', GROUPS_DIR);
|
|
38
57
|
console.log('[CFG] SERVE_COUNT=', SERVE_COUNT, ' PORT_BASE=', PORT_BASE, ' MASTER_PORT=', MASTER_PORT);
|
|
39
58
|
if (ROBOTS_DIR_OVERRIDE) console.log('[CFG] ROBOTS_DIR_OVERRIDE=', ROBOTS_DIR_OVERRIDE);
|
|
40
59
|
if (TESTS_DIR_OVERRIDE) console.log('[CFG] TESTS_DIR_OVERRIDE=', TESTS_DIR_OVERRIDE);
|
|
41
60
|
if (PUBLIC_DIR_OVERRIDE) console.log('[CFG] PUBLIC_DIR_OVERRIDE=', PUBLIC_DIR_OVERRIDE);
|
|
42
61
|
if (SNAPSHOTS_DIR_OVERRIDE) console.log('[CFG] SNAPSHOTS_DIR_OVERRIDE=', SNAPSHOTS_DIR_OVERRIDE);
|
|
62
|
+
function hasPm2() {
|
|
63
|
+
return new Promise((resolve) => {
|
|
64
|
+
exec('pm2 -v', { windowsHide: true }, (e) => resolve(!e));
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
async function ensurePm2(name, script, argsArr, cwd) {
|
|
68
|
+
const ok = await hasPm2();
|
|
69
|
+
if (!ok) return false;
|
|
70
|
+
return new Promise((resolve) => {
|
|
71
|
+
const cmd = `pm2 start "${process.execPath}" --name "${name}" -- ${['"' + script + '"', ...argsArr.map(a => `"${a}"`)].join(' ')}`;
|
|
72
|
+
exec(cmd, { cwd, windowsHide: true }, (e) => resolve(!e));
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
function killPid(pid) { try { process.kill(pid); } catch (_) {} }
|
|
76
|
+
|
|
77
|
+
// 维护端口->组/角色映射,便于僵尸检测和重启
|
|
78
|
+
const portIndex = new Map(); // port -> { groupId, role: 'serve'|'study'|'forwarder' }
|
|
43
79
|
|
|
44
80
|
// 读取所有 group_x 文件夹
|
|
45
81
|
const groupFolders = fs.existsSync(GROUPS_DIR)
|
|
@@ -198,62 +234,79 @@ function startResourceMonitor() {
|
|
|
198
234
|
}, 1000);
|
|
199
235
|
}
|
|
200
236
|
|
|
237
|
+
|
|
201
238
|
async function startAllGroups() {
|
|
239
|
+
startLogger();
|
|
202
240
|
for (let i = 0; i < groupFolders.length; i++) {
|
|
203
241
|
const groupDir = groupFolders[i];
|
|
204
242
|
const reg = { id: i, dir: groupDir, servePorts: [], study: null, forwarder: null };
|
|
205
|
-
const servePorts = [];
|
|
206
243
|
|
|
244
|
+
// 预计算本组端口
|
|
245
|
+
const groupServePorts = Array.from({ length: SERVE_COUNT }, (_, j) => PORT_BASE + i * (SERVE_COUNT + 2) + j);
|
|
246
|
+
const studyPort = PORT_BASE + i * (SERVE_COUNT + 2) + SERVE_COUNT;
|
|
247
|
+
const forwarderPort = PORT_BASE + i * (SERVE_COUNT + 2) + FORWARDER_OFFSET + 1;
|
|
248
|
+
|
|
249
|
+
const servePorts = [];
|
|
207
250
|
for (let j = 0; j < SERVE_COUNT; j++) {
|
|
208
|
-
const port =
|
|
251
|
+
const port = groupServePorts[j];
|
|
209
252
|
servePorts.push(port);
|
|
210
253
|
const serveScript = path.join(groupDir, 'main_Serve.cjs');
|
|
211
254
|
if (fs.existsSync(serveScript)) {
|
|
212
255
|
let attempts = 0;
|
|
256
|
+
const peers = groupServePorts.filter(p => p !== port).join(',');
|
|
213
257
|
while (attempts < 3) {
|
|
214
258
|
try {
|
|
215
|
-
const proc = spawn('node', [
|
|
259
|
+
const proc = spawn('node', [
|
|
260
|
+
serveScript,
|
|
261
|
+
port.toString(),
|
|
262
|
+
'--expose-gc',
|
|
263
|
+
'--group-id', String(i),
|
|
264
|
+
'--forwarder-port', String(forwarderPort),
|
|
265
|
+
'--study-port', String(studyPort),
|
|
266
|
+
'--peers', peers
|
|
267
|
+
], {
|
|
216
268
|
cwd: groupDir,
|
|
217
269
|
stdio: 'inherit',
|
|
218
270
|
});
|
|
271
|
+
// 注册到日志子进程
|
|
272
|
+
portIndex.set(port, { groupId: i, role: 'serve' });
|
|
273
|
+
loggerSend({ type: 'register', processes: [{ groupId: i, role: 'serve', port, pid: null }] });
|
|
219
274
|
groupProcesses.push(proc);
|
|
220
275
|
reg.servePorts.push({ pid: null, port });
|
|
221
276
|
proc.on('spawn', () => {
|
|
222
277
|
const idx = reg.servePorts.findIndex((p) => p.port === port);
|
|
223
278
|
if (idx >= 0) reg.servePorts[idx].pid = proc.pid;
|
|
279
|
+
loggerSend({ type: 'update', port, pid: proc.pid, role: 'serve', groupId: i });
|
|
224
280
|
});
|
|
225
281
|
break;
|
|
226
282
|
} catch (error) {
|
|
227
283
|
attempts++;
|
|
228
|
-
console.warn(
|
|
229
|
-
|
|
230
|
-
);
|
|
231
|
-
if (attempts === 3)
|
|
232
|
-
console.error(`[ERROR] 无法启动服务 ${serveScript}`);
|
|
284
|
+
console.warn(`[WARN] 启动服务 ${serveScript} 失败 (尝试 ${attempts}/3): ${error.message}`);
|
|
285
|
+
if (attempts === 3) console.error(`[ERROR] 无法启动服务 ${serveScript}`);
|
|
233
286
|
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
234
287
|
}
|
|
235
288
|
}
|
|
236
289
|
}
|
|
237
290
|
}
|
|
238
291
|
|
|
239
|
-
// study
|
|
240
|
-
|
|
241
|
-
const studyScript = path.join(groupDir, 'main_Study.cjs');
|
|
242
|
-
if (fs.existsSync(studyScript)) {
|
|
292
|
+
// study 进程(保持不变)
|
|
293
|
+
if (fs.existsSync(path.join(groupDir, 'main_Study.cjs'))) {
|
|
243
294
|
const proc = spawn(
|
|
244
295
|
'node',
|
|
245
|
-
[
|
|
296
|
+
[path.join(groupDir, 'main_Study.cjs'), studyPort.toString(), '--max-old-space-size=16384', '--expose-gc'],
|
|
246
297
|
{ cwd: groupDir, stdio: 'inherit' }
|
|
247
298
|
);
|
|
299
|
+
portIndex.set(studyPort, { groupId: i, role: 'study' });
|
|
300
|
+
loggerSend({ type: 'register', processes: [{ groupId: i, role: 'study', port: studyPort, pid: null }] });
|
|
248
301
|
groupProcesses.push(proc);
|
|
249
302
|
reg.study = { pid: null, port: studyPort };
|
|
250
303
|
proc.on('spawn', () => {
|
|
251
304
|
reg.study.pid = proc.pid;
|
|
305
|
+
loggerSend({ type: 'update', port: studyPort, pid: proc.pid, role: 'study', groupId: i });
|
|
252
306
|
});
|
|
253
307
|
}
|
|
254
308
|
|
|
255
|
-
// forwarder
|
|
256
|
-
const forwarderPort = PORT_BASE + i * (SERVE_COUNT + 2) + FORWARDER_OFFSET + 1;
|
|
309
|
+
// forwarder 进程(保持不变)
|
|
257
310
|
groupForwarderPorts.push(forwarderPort);
|
|
258
311
|
const forwarderScript = path.join(groupDir, 'forwarder.js');
|
|
259
312
|
if (fs.existsSync(forwarderScript)) {
|
|
@@ -268,22 +321,20 @@ async function startAllGroups() {
|
|
|
268
321
|
],
|
|
269
322
|
{ cwd: groupDir, stdio: 'inherit' }
|
|
270
323
|
);
|
|
324
|
+
portIndex.set(forwarderPort, { groupId: i, role: 'forwarder' });
|
|
325
|
+
loggerSend({ type: 'register', processes: [{ groupId: i, role: 'forwarder', port: forwarderPort, pid: null }] });
|
|
271
326
|
groupProcesses.push(proc);
|
|
272
327
|
reg.forwarder = { pid: null, port: forwarderPort };
|
|
273
328
|
proc.on('spawn', () => {
|
|
274
329
|
reg.forwarder.pid = proc.pid;
|
|
330
|
+
loggerSend({ type: 'update', port: forwarderPort, pid: proc.pid, role: 'forwarder', groupId: i });
|
|
275
331
|
});
|
|
276
332
|
}
|
|
277
333
|
|
|
278
334
|
// 写入组配置文件
|
|
279
335
|
const config = { servePorts, studyPort, forwarderPort };
|
|
280
|
-
fs.writeFileSync(
|
|
281
|
-
|
|
282
|
-
JSON.stringify(config, null, 2)
|
|
283
|
-
);
|
|
284
|
-
console.log(
|
|
285
|
-
`[START] group_${i} 端口分配: serve=${servePorts.join(',')}, study=${studyPort}, forwarder=${forwarderPort}`
|
|
286
|
-
);
|
|
336
|
+
fs.writeFileSync(path.join(groupDir, 'group_ports.json'), JSON.stringify(config, null, 2));
|
|
337
|
+
console.log(`[START] group_${i} 端口分配: serve=${servePorts.join(',')}, study=${studyPort}, forwarder=${forwarderPort}`);
|
|
287
338
|
groupRegistry.push(reg);
|
|
288
339
|
await new Promise((r) => setTimeout(r, GROUP_START_DELAY));
|
|
289
340
|
}
|
|
@@ -378,18 +429,26 @@ function startMasterForwarder() {
|
|
|
378
429
|
return res.status(400).json({ error: 'invalid groupId' });
|
|
379
430
|
}
|
|
380
431
|
const reg = groupRegistry[groupId];
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
432
|
+
const spawnServe = () => {
|
|
433
|
+
const serveScript = path.join(reg.dir, 'main_Serve.cjs');
|
|
434
|
+
const groupServePorts = reg.servePorts.map(s => s.port);
|
|
435
|
+
for (const sp of reg.servePorts) {
|
|
436
|
+
const peers = groupServePorts.filter(p => p !== sp.port).join(',');
|
|
437
|
+
const p = spawn('node', [
|
|
438
|
+
serveScript,
|
|
439
|
+
sp.port.toString(),
|
|
440
|
+
'--expose-gc',
|
|
441
|
+
'--group-id', String(groupId),
|
|
442
|
+
'--forwarder-port', String(reg.forwarder.port),
|
|
443
|
+
'--study-port', String(reg.study.port),
|
|
444
|
+
'--peers', peers
|
|
445
|
+
], {
|
|
446
|
+
cwd: reg.dir,
|
|
447
|
+
stdio: 'inherit',
|
|
448
|
+
});
|
|
449
|
+
p.on('spawn', () => { sp.pid = p.pid; });
|
|
450
|
+
}
|
|
451
|
+
};
|
|
393
452
|
const spawnStudy = () => {
|
|
394
453
|
const studyScript = path.join(reg.dir, 'main_Study.cjs');
|
|
395
454
|
const p = spawn(
|
|
@@ -474,7 +533,9 @@ function startMasterForwarder() {
|
|
|
474
533
|
if (!message || typeof message !== 'string') {
|
|
475
534
|
return res.status(400).json({ error: '消息不能为空' });
|
|
476
535
|
}
|
|
477
|
-
|
|
536
|
+
const reqId = `${Date.now()}_${Math.floor(Math.random() * 1e6)}`;
|
|
537
|
+
// 记录输入
|
|
538
|
+
loggerSend({ type: 'io', ts: Date.now(), reqId, phase: 'in', input: message });
|
|
478
539
|
// 轻微节流:高负载时延迟处理,避免CPU尖峰
|
|
479
540
|
const curCpu = metrics.cpu[metrics.cpu.length - 1] || 0;
|
|
480
541
|
const curMem = metrics.mem[metrics.mem.length - 1] || 0;
|
|
@@ -604,6 +665,17 @@ function startMasterForwarder() {
|
|
|
604
665
|
mem: curMem,
|
|
605
666
|
throttledMs: throttleMs,
|
|
606
667
|
});
|
|
668
|
+
try {
|
|
669
|
+
loggerSend({
|
|
670
|
+
type: 'io',
|
|
671
|
+
ts: Date.now(),
|
|
672
|
+
reqId,
|
|
673
|
+
phase: 'out',
|
|
674
|
+
sampled: SELECTED,
|
|
675
|
+
top: unique[0]?.text || '',
|
|
676
|
+
responses: unique.slice(0, 10).map(u => ({ port: u.port, len: (u.text || '').length, score: u.score }))
|
|
677
|
+
});
|
|
678
|
+
} catch (_) {}
|
|
607
679
|
});
|
|
608
680
|
|
|
609
681
|
// 健康检查
|
|
@@ -628,13 +700,117 @@ function startMasterForwarder() {
|
|
|
628
700
|
console.log(`[MASTER] 对外API: POST /api/chat(前端不会调用,可留作他用)`);
|
|
629
701
|
});
|
|
630
702
|
}
|
|
703
|
+
// 僵尸端口检测 + 自动重启
|
|
704
|
+
async function probePort(port) {
|
|
705
|
+
// 先 HTTP 探活
|
|
706
|
+
try {
|
|
707
|
+
const url = `http://localhost:${port}/health`;
|
|
708
|
+
const r = await axios.get(url, { timeout: 1500 });
|
|
709
|
+
if (r && r.status === 200) return true;
|
|
710
|
+
} catch (_) {}
|
|
711
|
+
// 回退:netstat 检查 LISTEN
|
|
712
|
+
return new Promise((resolve) => {
|
|
713
|
+
exec('netstat -ano -p TCP', { windowsHide: true }, (e, stdout) => {
|
|
714
|
+
if (e || !stdout) return resolve(false);
|
|
715
|
+
const re = new RegExp(`:${port}\\s+.*LISTENING\\s+(\\d+)`, 'i');
|
|
716
|
+
resolve(re.test(stdout));
|
|
717
|
+
});
|
|
718
|
+
});
|
|
719
|
+
}
|
|
631
720
|
|
|
632
721
|
// 启动所有组和总控 forwarder
|
|
633
722
|
(async () => {
|
|
634
723
|
await startAllGroups();
|
|
635
724
|
startMasterForwarder();
|
|
636
725
|
})();
|
|
726
|
+
async function restartByPort(groupId, role, port) {
|
|
727
|
+
try {
|
|
728
|
+
const reg = groupRegistry[groupId];
|
|
729
|
+
if (!reg) return false;
|
|
730
|
+
const groupDir = reg.dir;
|
|
731
|
+
|
|
732
|
+
if (USE_PM2 && await hasPm2()) {
|
|
733
|
+
const name = `g${groupId}-${role}-${port}`;
|
|
734
|
+
const script = path.join(groupDir, role === 'study' ? 'main_Study.cjs' :
|
|
735
|
+
(role === 'forwarder' ? 'forwarder.js' : 'main_Serve.cjs'));
|
|
736
|
+
const args = role === 'forwarder'
|
|
737
|
+
? [port.toString(), ...reg.servePorts.map(s => s.port.toString()), reg.study.port.toString(), '--expose-gc']
|
|
738
|
+
: [
|
|
739
|
+
port.toString(),
|
|
740
|
+
'--expose-gc',
|
|
741
|
+
'--group-id', String(groupId),
|
|
742
|
+
'--forwarder-port', String(reg.forwarder.port),
|
|
743
|
+
'--study-port', String(reg.study.port),
|
|
744
|
+
'--peers', reg.servePorts.map(s => s.port).filter(p => p !== port).join(',')
|
|
745
|
+
];
|
|
746
|
+
await ensurePm2(name, script, args, groupDir);
|
|
747
|
+
console.log(`[PM2] 已重启 ${name}`);
|
|
748
|
+
return true;
|
|
749
|
+
}
|
|
637
750
|
|
|
751
|
+
if (role === 'serve') {
|
|
752
|
+
const found = reg.servePorts.find(s => s.port === port);
|
|
753
|
+
if (found?.pid) killPid(found.pid);
|
|
754
|
+
const serveScript = path.join(groupDir, 'main_Serve.cjs');
|
|
755
|
+
const peers = reg.servePorts.map(s => s.port).filter(p => p !== port).join(',');
|
|
756
|
+
const p = spawn('node', [
|
|
757
|
+
serveScript,
|
|
758
|
+
port.toString(),
|
|
759
|
+
'--expose-gc',
|
|
760
|
+
'--group-id', String(groupId),
|
|
761
|
+
'--forwarder-port', String(reg.forwarder.port),
|
|
762
|
+
'--study-port', String(reg.study.port),
|
|
763
|
+
'--peers', peers
|
|
764
|
+
], { cwd: groupDir, stdio: 'inherit' });
|
|
765
|
+
p.on('spawn', () => {
|
|
766
|
+
if (found) found.pid = p.pid;
|
|
767
|
+
loggerSend({ type: 'update', port, pid: p.pid, role, groupId });
|
|
768
|
+
});
|
|
769
|
+
return true;
|
|
770
|
+
} else if (role === 'study') {
|
|
771
|
+
if (reg.study?.pid) killPid(reg.study.pid);
|
|
772
|
+
const studyScript = path.join(groupDir, 'main_Study.cjs');
|
|
773
|
+
const p = spawn('node', [studyScript, port.toString(), '--max-old-space-size=16384', '--expose-gc'], { cwd: groupDir, stdio: 'inherit' });
|
|
774
|
+
p.on('spawn', () => {
|
|
775
|
+
reg.study.pid = p.pid;
|
|
776
|
+
loggerSend({ type: 'update', port, pid: p.pid, role, groupId });
|
|
777
|
+
});
|
|
778
|
+
return true;
|
|
779
|
+
} else if (role === 'forwarder') {
|
|
780
|
+
if (reg.forwarder?.pid) killPid(reg.forwarder.pid);
|
|
781
|
+
const forwarderScript = path.join(groupDir, 'forwarder.js');
|
|
782
|
+
const args = [port.toString(), ...reg.servePorts.map(s => s.port.toString()), reg.study.port.toString(), '--expose-gc'];
|
|
783
|
+
const p = spawn('node', [forwarderScript, ...args], { cwd: groupDir, stdio: 'inherit' });
|
|
784
|
+
p.on('spawn', () => {
|
|
785
|
+
reg.forwarder.pid = p.pid;
|
|
786
|
+
loggerSend({ type: 'update', port, pid: p.pid, role, groupId });
|
|
787
|
+
});
|
|
788
|
+
return true;
|
|
789
|
+
}
|
|
790
|
+
} catch (e) {
|
|
791
|
+
console.warn('[RESTART] 重启失败:', e.message);
|
|
792
|
+
}
|
|
793
|
+
return false;
|
|
794
|
+
}
|
|
795
|
+
// 反触发/僵尸扫描:定期检测端口状态,异常则报警并重启
|
|
796
|
+
setInterval(async () => {
|
|
797
|
+
const checks = [];
|
|
798
|
+
for (const [port, meta] of portIndex.entries()) {
|
|
799
|
+
checks.push({ port, meta, p: probePort(port) });
|
|
800
|
+
}
|
|
801
|
+
const results = await Promise.all(checks.map(c => c.p.catch(() => false)));
|
|
802
|
+
for (let i = 0; i < checks.length; i++) {
|
|
803
|
+
const ok = results[i];
|
|
804
|
+
const { port, meta } = checks[i];
|
|
805
|
+
const alive = ok ? 'OK' : 'ZOMBIE';
|
|
806
|
+
if (!ok) {
|
|
807
|
+
console.warn(`[ANTI-TRIGGER] 发现僵尸端口: ${port} (${meta.role} @ group ${meta.groupId}),自动重启`);
|
|
808
|
+
// 记录事件到 io 日志(子进程)
|
|
809
|
+
loggerSend({ type: 'io', ts: Date.now(), phase: 'event', event: 'zombie', port, role: meta.role, groupId: meta.groupId });
|
|
810
|
+
await restartByPort(meta.groupId, meta.role, port);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}, 15000);
|
|
638
814
|
// 进程退出处理
|
|
639
815
|
process.on('SIGINT', () => {
|
|
640
816
|
console.log('收到退出信号,正在关闭所有工作组进程...');
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## 项目简介
|
|
4
4
|
Phoenix AI 是一个基于多模态架构的人工智能系统,支持文本、图片、语音三种数据类型的处理。系统通过分布式工作组架构实现高效的任务分片和并行处理,适用于大规模数据集和复杂任务场景。
|
|
5
|
-
|
|
5
|
+
目前,大多数模块仍然可能不那么稳定,如果有问题或者提交bug,请联系3873636760@qq.com
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
## 文件结构
|
|
@@ -62,8 +62,8 @@ Phoenix AI 是一个基于多模态架构的人工智能系统,支持文本、
|
|
|
62
62
|
|
|
63
63
|
3. **访问服务**
|
|
64
64
|
- 文本组入口: http://localhost:9100
|
|
65
|
-
- 图片组入口: http://localhost:9200
|
|
66
|
-
- 语音组入口: http://localhost:9300
|
|
65
|
+
- 图片组入口: http://localhost:9200(未测试,谨慎使用)
|
|
66
|
+
- 语音组入口: http://localhost:9300(未测试,谨慎使用)
|
|
67
67
|
- 多模态管理器: http://localhost:9050
|
|
68
68
|
*** 注意,目前python的部分-前端序列化部分尚未完成,所以python可能没有用处 ***
|
|
69
69
|
---
|
package/loggerworker.cjs
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/* 日志子进程:接收主进程发来的 IO 记录与进程注册信息,按周期采样资源并输出文本表格 */
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const { exec } = require('child_process');
|
|
6
|
+
|
|
7
|
+
const LOG_DIR = path.join(__dirname, 'logs');
|
|
8
|
+
fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
9
|
+
|
|
10
|
+
let watch = new Map(); // key: port -> { pid, groupId, role, port }
|
|
11
|
+
let prevSample = new Map(); // pid -> { cpuSec, readBytes, writeBytes, ts }
|
|
12
|
+
let lastMetricsWriteHour = '';
|
|
13
|
+
let lastIoWriteHour = '';
|
|
14
|
+
let sampleIntervalMs = 5000;
|
|
15
|
+
let cores = os.cpus().length;
|
|
16
|
+
|
|
17
|
+
// 工具:获取当前小时文件名
|
|
18
|
+
function hourSuffix() {
|
|
19
|
+
const d = new Date();
|
|
20
|
+
const y = d.getFullYear();
|
|
21
|
+
const m = String(d.getMonth() + 1).padStart(2, '0');
|
|
22
|
+
const dd = String(d.getDate()).padStart(2, '0');
|
|
23
|
+
const hh = String(d.getHours()).padStart(2, '0');
|
|
24
|
+
return `${y}${m}${dd}_${hh}`;
|
|
25
|
+
}
|
|
26
|
+
function ioLogPath() { return path.join(LOG_DIR, `io_${hourSuffix()}.log`); }
|
|
27
|
+
function metricsLogPath() { return path.join(LOG_DIR, `metrics_${hourSuffix()}.txt`); }
|
|
28
|
+
|
|
29
|
+
function appendLine(file, line) {
|
|
30
|
+
fs.appendFile(file, line + '\n', () => {});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function writeIo(record) {
|
|
34
|
+
const nowHour = hourSuffix();
|
|
35
|
+
if (lastIoWriteHour !== nowHour) lastIoWriteHour = nowHour;
|
|
36
|
+
const file = ioLogPath();
|
|
37
|
+
appendLine(file, JSON.stringify(record));
|
|
38
|
+
}
|
|
39
|
+
function getProcInfoPowershell(pids) {
|
|
40
|
+
return new Promise((resolve) => {
|
|
41
|
+
if (!pids.length) return resolve([]);
|
|
42
|
+
const pidList = pids.join(',');
|
|
43
|
+
const cmd = [
|
|
44
|
+
'powershell.exe -NoProfile -Command',
|
|
45
|
+
`"Get-Process -Id ${pidList} | Select-Object Id,CPU,WorkingSet64,Name,`,
|
|
46
|
+
'IOReadBytes,IOWriteBytes | ConvertTo-Json -Compress"'
|
|
47
|
+
].join(' ');
|
|
48
|
+
exec(cmd, { windowsHide: true, maxBuffer: 10 * 1024 * 1024 }, (err, stdout) => {
|
|
49
|
+
if (err || !stdout) return resolve([]);
|
|
50
|
+
try {
|
|
51
|
+
const data = JSON.parse(stdout);
|
|
52
|
+
resolve(Array.isArray(data) ? data : [data]);
|
|
53
|
+
} catch {
|
|
54
|
+
resolve([]);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function getTcpConnCounts() {
|
|
61
|
+
return new Promise((resolve) => {
|
|
62
|
+
// 使用 netstat -ano 统计 Owning PID 的 TCP 连接数量
|
|
63
|
+
exec('netstat -ano', { windowsHide: true, maxBuffer: 10 * 1024 * 1024 }, (err, stdout) => {
|
|
64
|
+
if (err || !stdout) return resolve(new Map());
|
|
65
|
+
const lines = stdout.split(/\r?\n/).filter(Boolean);
|
|
66
|
+
const map = new Map(); // pid -> count
|
|
67
|
+
for (const line of lines) {
|
|
68
|
+
// Proto Local Address Foreign Address State PID
|
|
69
|
+
const parts = line.trim().split(/\s+/);
|
|
70
|
+
if (parts.length < 5) continue;
|
|
71
|
+
const pid = Number(parts[parts.length - 1]);
|
|
72
|
+
if (!Number.isFinite(pid)) continue;
|
|
73
|
+
map.set(pid, (map.get(pid) || 0) + 1);
|
|
74
|
+
}
|
|
75
|
+
resolve(map);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function getListeningPortsByPid() {
|
|
81
|
+
return new Promise((resolve) => {
|
|
82
|
+
exec('netstat -ano -p TCP', { windowsHide: true, maxBuffer: 10 * 1024 * 1024 }, (err, stdout) => {
|
|
83
|
+
const map = new Map(); // pid -> Set(ports)
|
|
84
|
+
if (err || !stdout) return resolve(map);
|
|
85
|
+
const lines = stdout.split(/\r?\n/).filter(Boolean);
|
|
86
|
+
for (const line of lines) {
|
|
87
|
+
const parts = line.trim().split(/\s+/);
|
|
88
|
+
if (parts.length < 5) continue;
|
|
89
|
+
const state = parts[3] || parts[4] || '';
|
|
90
|
+
const pid = Number(parts[parts.length - 1]);
|
|
91
|
+
if (!Number.isFinite(pid)) continue;
|
|
92
|
+
const local = parts[1] || '';
|
|
93
|
+
const port = Number(local.split(':').pop());
|
|
94
|
+
if (!Number.isFinite(port)) continue;
|
|
95
|
+
if (!map.has(pid)) map.set(pid, new Set());
|
|
96
|
+
if (state.toUpperCase().includes('LISTEN')) {
|
|
97
|
+
map.get(pid).add(port);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
resolve(map);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function sampleAndWrite() {
|
|
106
|
+
try {
|
|
107
|
+
const ports = Array.from(watch.keys());
|
|
108
|
+
const procs = Array.from(watch.values());
|
|
109
|
+
const pids = Array.from(new Set(procs.map(p => p.pid))).filter(Boolean);
|
|
110
|
+
|
|
111
|
+
const [procInfo, connCounts, listenMap] = await Promise.all([
|
|
112
|
+
getProcInfoPowershell(pids),
|
|
113
|
+
getTcpConnCounts(),
|
|
114
|
+
getListeningPortsByPid()
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
const infoByPid = new Map();
|
|
118
|
+
for (const it of procInfo) {
|
|
119
|
+
infoByPid.set(Number(it.Id), {
|
|
120
|
+
cpuSec: Number(it.CPU || 0),
|
|
121
|
+
rss: Number(it.WorkingSet64 || 0),
|
|
122
|
+
ioRead: Number(it.IOReadBytes || 0),
|
|
123
|
+
ioWrite: Number(it.IOWriteBytes || 0),
|
|
124
|
+
name: it.Name || ''
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const nowTs = Date.now();
|
|
129
|
+
const rows = [];
|
|
130
|
+
let aliveCount = 0;
|
|
131
|
+
|
|
132
|
+
for (const port of ports) {
|
|
133
|
+
const { pid, role, groupId } = watch.get(port);
|
|
134
|
+
const info = infoByPid.get(pid) || { cpuSec: 0, rss: 0, ioRead: 0, ioWrite: 0, name: '' };
|
|
135
|
+
const prev = prevSample.get(pid) || { cpuSec: info.cpuSec, readBytes: info.ioRead, writeBytes: info.ioWrite, ts: nowTs };
|
|
136
|
+
const dt = Math.max(1, (nowTs - prev.ts) / 1000); // sec
|
|
137
|
+
const dCpu = Math.max(0, info.cpuSec - prev.cpuSec); // sec
|
|
138
|
+
const cpuPct = Math.min(100, (dCpu / dt) * (100 / Math.max(1, 1 / cores))); // 近似
|
|
139
|
+
const dRead = Math.max(0, info.ioRead - prev.readBytes);
|
|
140
|
+
const dWrite = Math.max(0, info.ioWrite - prev.writeBytes);
|
|
141
|
+
const conn = connCounts.get(pid) || 0;
|
|
142
|
+
const rssMb = (info.rss / 1024 / 1024).toFixed(1);
|
|
143
|
+
const rps = (dRead / dt) | 0;
|
|
144
|
+
const wps = (dWrite / dt) | 0;
|
|
145
|
+
const listeningPorts = listenMap.get(pid) || new Set();
|
|
146
|
+
const up = listeningPorts.has(port) ? 'Y' : 'N';
|
|
147
|
+
if (up === 'Y') aliveCount++;
|
|
148
|
+
|
|
149
|
+
rows.push({
|
|
150
|
+
port,
|
|
151
|
+
pid,
|
|
152
|
+
role,
|
|
153
|
+
groupId,
|
|
154
|
+
cpu: cpuPct.toFixed(1),
|
|
155
|
+
mem: rssMb,
|
|
156
|
+
conn,
|
|
157
|
+
rps,
|
|
158
|
+
wps,
|
|
159
|
+
up
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
prevSample.set(pid, { cpuSec: info.cpuSec, readBytes: info.ioRead, writeBytes: info.ioWrite, ts: nowTs });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// 输出文本表
|
|
166
|
+
const nowHour = hourSuffix();
|
|
167
|
+
if (lastMetricsWriteHour !== nowHour) lastMetricsWriteHour = nowHour;
|
|
168
|
+
const file = metricsLogPath();
|
|
169
|
+
const header = `# ${new Date().toISOString()} AlivePorts=${aliveCount}/${ports.length}\n` +
|
|
170
|
+
'PORT PID ROLE GID CPU% MEM(MB) CONN DiskR/s DiskW/s UP\n' +
|
|
171
|
+
'----- ------- --------- --- ---- ------- ---- ------- ------- --';
|
|
172
|
+
const lines = rows
|
|
173
|
+
.sort((a, b) => a.port - b.port)
|
|
174
|
+
.map(r =>
|
|
175
|
+
`${String(r.port).padEnd(5)} ${String(r.pid).padEnd(7)} ${String(r.role).padEnd(9)} ${String(r.groupId).padEnd(3)} ${String(r.cpu).padStart(4)} ${String(r.mem).padStart(7)} ${String(r.conn).padStart(4)} ${String(r.rps).padStart(7)} ${String(r.wps).padStart(7)} ${r.up}`
|
|
176
|
+
);
|
|
177
|
+
appendLine(file, [header, ...lines, ''].join('\n'));
|
|
178
|
+
} catch (e) {
|
|
179
|
+
// 忽略单次采样错误
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
process.on('message', (msg) => {
|
|
184
|
+
if (!msg || typeof msg !== 'object') return;
|
|
185
|
+
if (msg.type === 'register' && Array.isArray(msg.processes)) {
|
|
186
|
+
for (const p of msg.processes) {
|
|
187
|
+
if (p && p.port) {
|
|
188
|
+
watch.set(p.port, { pid: p.pid, role: p.role, groupId: p.groupId, port: p.port });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
} else if (msg.type === 'update' && msg.port) {
|
|
192
|
+
const old = watch.get(msg.port) || {};
|
|
193
|
+
watch.set(msg.port, { ...old, ...msg });
|
|
194
|
+
} else if (msg.type === 'io') {
|
|
195
|
+
// { ts, reqId, input, selectedPorts, responses[] }
|
|
196
|
+
writeIo(msg);
|
|
197
|
+
} else if (msg.type === 'config' && msg.sampleIntervalMs) {
|
|
198
|
+
sampleIntervalMs = Math.max(1000, Number(msg.sampleIntervalMs) || sampleIntervalMs);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
setInterval(sampleAndWrite, sampleIntervalMs);
|