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 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 = PORT_BASE + i * (SERVE_COUNT + 2) + j;
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', [serveScript, port.toString(),'--expose-gc'], {
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
- `[WARN] 启动服务 ${serveScript} 失败 (尝试 ${attempts}/3): ${error.message}`
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
- const studyPort = PORT_BASE + i * (SERVE_COUNT + 2) + SERVE_COUNT;
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
- [studyScript, studyPort.toString(), '--max-old-space-size=12288', '--expose-gc'],
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
- 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
- );
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
- 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
- };
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
  ---
@@ -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);