@zhin.js/process-monitor 1.0.1 → 1.1.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/commands/process-status.js +9 -0
- package/commands/process-status.ts +9 -0
- package/lib/index.d.ts +2 -12
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -168
- package/lib/index.js.map +1 -1
- package/lib/monitor.d.ts +43 -0
- package/lib/monitor.d.ts.map +1 -0
- package/lib/monitor.js +209 -0
- package/lib/monitor.js.map +1 -0
- package/package.json +44 -12
- package/plugin.js +20 -0
- package/schema.json +45 -0
- package/src/index.ts +11 -220
- package/src/monitor.ts +288 -0
- package/tools/process-status.js +12 -0
- package/tools/process-status.ts +12 -0
- package/plugin.yml +0 -2
- package/tools/process-status/handler.ts +0 -21
- package/tools/process-status/process-status.tool.md +0 -7
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
+
import { defineCommand } from 'zhin.js/command';
|
|
3
|
+
import { formatProcessStatus } from "../lib/monitor.js";
|
|
4
|
+
export default defineCommand({
|
|
5
|
+
description: '查看进程监控状态(PID、运行时长、重启统计)',
|
|
6
|
+
execute() {
|
|
7
|
+
return formatProcessStatus();
|
|
8
|
+
},
|
|
9
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { defineCommand } from 'zhin.js/command';
|
|
2
|
+
import { formatProcessStatus, type ProcessMonitorConfig } from '../src/monitor.js';
|
|
3
|
+
|
|
4
|
+
export default defineCommand<ProcessMonitorConfig>({
|
|
5
|
+
description: '查看进程监控状态(PID、运行时长、重启统计)',
|
|
6
|
+
execute() {
|
|
7
|
+
return formatProcessStatus();
|
|
8
|
+
},
|
|
9
|
+
});
|
package/lib/index.d.ts
CHANGED
|
@@ -1,13 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
lastPid?: number;
|
|
4
|
-
lastStartTime?: number;
|
|
5
|
-
restartCount: number;
|
|
6
|
-
crashCount: number;
|
|
7
|
-
totalUptime: number;
|
|
8
|
-
}
|
|
9
|
-
export declare let processState: ProcessState;
|
|
10
|
-
export declare const startTime: number;
|
|
11
|
-
export declare function formatUptime(ms: number): string;
|
|
12
|
-
export default plugin;
|
|
1
|
+
export { classifyStartup, formatProcessStatus, formatUptime, processState, resetProcessMonitorForTests, resolveProcessMonitorConfig, startProcessMonitor, startTime, } from './monitor.js';
|
|
2
|
+
export type { NotifyChannel, ProcessMonitorConfig, ProcessState, StartupReason } from './monitor.js';
|
|
13
3
|
//# sourceMappingURL=index.d.ts.map
|
package/lib/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,eAAe,EACf,mBAAmB,EACnB,YAAY,EACZ,YAAY,EACZ,2BAA2B,EAC3B,2BAA2B,EAC3B,mBAAmB,EACnB,SAAS,GACV,MAAM,cAAc,CAAC;AACtB,YAAY,EAAE,aAAa,EAAE,oBAAoB,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC"}
|
package/lib/index.js
CHANGED
|
@@ -1,169 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
* @zhin.js/process-monitor
|
|
3
|
-
*
|
|
4
|
-
* 进程监控与重启通知插件
|
|
5
|
-
*/
|
|
6
|
-
import { formatCompact, usePlugin } from 'zhin.js';
|
|
7
|
-
import os from 'os';
|
|
8
|
-
import fs from 'fs';
|
|
9
|
-
import path from 'path';
|
|
10
|
-
const plugin = usePlugin();
|
|
11
|
-
const { logger, root } = plugin;
|
|
12
|
-
const configService = root.inject('config');
|
|
13
|
-
const appConfig = configService?.getPrimary() || {};
|
|
14
|
-
const config = {
|
|
15
|
-
enabled: true,
|
|
16
|
-
notifyChannels: [],
|
|
17
|
-
notifyOnStart: true,
|
|
18
|
-
notifyOnRestart: true,
|
|
19
|
-
notifyOnCrash: true,
|
|
20
|
-
...appConfig['process-monitor'],
|
|
21
|
-
};
|
|
22
|
-
if (!config.enabled) {
|
|
23
|
-
logger.info(formatCompact({ op: 'load', enabled: false }));
|
|
24
|
-
}
|
|
25
|
-
// ─── 状态管理 ────────────────────────────────────────────────────────────────
|
|
26
|
-
const STATE_FILE = path.join(process.cwd(), 'data', 'process-state.json');
|
|
27
|
-
export let processState = {
|
|
28
|
-
restartCount: 0,
|
|
29
|
-
crashCount: 0,
|
|
30
|
-
totalUptime: 0,
|
|
31
|
-
};
|
|
32
|
-
export const startTime = Date.now();
|
|
33
|
-
function loadProcessState() {
|
|
34
|
-
try {
|
|
35
|
-
if (fs.existsSync(STATE_FILE)) {
|
|
36
|
-
const data = fs.readFileSync(STATE_FILE, 'utf-8');
|
|
37
|
-
processState = JSON.parse(data);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
catch (error) {
|
|
41
|
-
logger.warn(formatCompact({ op: 'load_state', ok: false, error: String(error) }));
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
function saveProcessState() {
|
|
45
|
-
try {
|
|
46
|
-
fs.mkdirSync(path.dirname(STATE_FILE), { recursive: true });
|
|
47
|
-
fs.writeFileSync(STATE_FILE, JSON.stringify(processState, null, 2));
|
|
48
|
-
}
|
|
49
|
-
catch (error) {
|
|
50
|
-
logger.warn(formatCompact({ op: 'save_state', ok: false, error: String(error) }));
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
// ─── 启动检测 ────────────────────────────────────────────────────────────────
|
|
54
|
-
async function detectStartupReason() {
|
|
55
|
-
const currentPid = process.pid;
|
|
56
|
-
const currentTime = Date.now();
|
|
57
|
-
let reason = 'start';
|
|
58
|
-
let uptime;
|
|
59
|
-
if (processState.lastPid && processState.lastStartTime) {
|
|
60
|
-
const timeSinceLastStart = currentTime - processState.lastStartTime;
|
|
61
|
-
if (timeSinceLastStart < 5 * 60 * 1000) {
|
|
62
|
-
reason = 'crash';
|
|
63
|
-
processState.crashCount++;
|
|
64
|
-
logger.warn(formatCompact({ op: 'restart', abnormal: true, since_last_s: Math.floor(timeSinceLastStart / 1000) }));
|
|
65
|
-
}
|
|
66
|
-
else {
|
|
67
|
-
reason = 'restart';
|
|
68
|
-
processState.restartCount++;
|
|
69
|
-
logger.info(formatCompact({ op: 'restart', abnormal: false }));
|
|
70
|
-
}
|
|
71
|
-
uptime = timeSinceLastStart;
|
|
72
|
-
processState.totalUptime += uptime;
|
|
73
|
-
}
|
|
74
|
-
else {
|
|
75
|
-
logger.info(formatCompact({ first: true }));
|
|
76
|
-
}
|
|
77
|
-
processState.lastPid = currentPid;
|
|
78
|
-
processState.lastStartTime = currentTime;
|
|
79
|
-
saveProcessState();
|
|
80
|
-
const record = {
|
|
81
|
-
timestamp: new Date(),
|
|
82
|
-
reason,
|
|
83
|
-
uptime,
|
|
84
|
-
pid: currentPid,
|
|
85
|
-
hostname: os.hostname(),
|
|
86
|
-
platform: `${os.platform()}-${os.arch()}`,
|
|
87
|
-
nodeVersion: process.version,
|
|
88
|
-
memory: Math.round(process.memoryUsage().heapUsed / 1024 / 1024),
|
|
89
|
-
};
|
|
90
|
-
const shouldNotify = (reason === 'start' && config.notifyOnStart) ||
|
|
91
|
-
(reason === 'restart' && config.notifyOnRestart) ||
|
|
92
|
-
(reason === 'crash' && config.notifyOnCrash);
|
|
93
|
-
if (shouldNotify && config.notifyChannels && config.notifyChannels.length > 0) {
|
|
94
|
-
await sendNotification(record);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
// ─── 通知 ────────────────────────────────────────────────────────────────────
|
|
98
|
-
function formatNotificationMessage(record) {
|
|
99
|
-
const emoji = { start: '🚀', restart: '🔄', crash: '💥' }[record.reason] || '📊';
|
|
100
|
-
const reasonText = { start: '首次启动', restart: '正常重启', crash: '异常崩溃' }[record.reason] || '未知';
|
|
101
|
-
const lines = [
|
|
102
|
-
`${emoji} 【进程监控通知】`,
|
|
103
|
-
'',
|
|
104
|
-
`📊 事件: ${reasonText}`,
|
|
105
|
-
`⏰ 时间: ${record.timestamp.toLocaleString('zh-CN')}`,
|
|
106
|
-
`🖥️ 主机: ${record.hostname}`,
|
|
107
|
-
`🔢 PID: ${record.pid}`,
|
|
108
|
-
`💻 平台: ${record.platform}`,
|
|
109
|
-
`📦 Node: ${record.nodeVersion}`,
|
|
110
|
-
];
|
|
111
|
-
if (record.uptime) {
|
|
112
|
-
lines.push(`⏱️ 运行时长: ${formatUptime(record.uptime)}`);
|
|
113
|
-
}
|
|
114
|
-
if (record.memory) {
|
|
115
|
-
lines.push(`💾 内存: ${record.memory} MB`);
|
|
116
|
-
}
|
|
117
|
-
lines.push('', `📈 统计:`);
|
|
118
|
-
lines.push(` • 总重启: ${processState.restartCount} 次`);
|
|
119
|
-
lines.push(` • 崩溃: ${processState.crashCount} 次`);
|
|
120
|
-
lines.push(` • 累计运行: ${formatUptime(processState.totalUptime)}`);
|
|
121
|
-
return lines.join('\n');
|
|
122
|
-
}
|
|
123
|
-
export function formatUptime(ms) {
|
|
124
|
-
const seconds = Math.floor(ms / 1000);
|
|
125
|
-
const minutes = Math.floor(seconds / 60);
|
|
126
|
-
const hours = Math.floor(minutes / 60);
|
|
127
|
-
const days = Math.floor(hours / 24);
|
|
128
|
-
if (days > 0)
|
|
129
|
-
return `${days}天${hours % 24}小时`;
|
|
130
|
-
if (hours > 0)
|
|
131
|
-
return `${hours}小时${minutes % 60}分钟`;
|
|
132
|
-
if (minutes > 0)
|
|
133
|
-
return `${minutes}分钟`;
|
|
134
|
-
return `${seconds}秒`;
|
|
135
|
-
}
|
|
136
|
-
async function sendNotification(record) {
|
|
137
|
-
const message = formatNotificationMessage(record);
|
|
138
|
-
for (const channel of config.notifyChannels || []) {
|
|
139
|
-
try {
|
|
140
|
-
if (channel.type === 'webhook') {
|
|
141
|
-
await fetch(channel.target, {
|
|
142
|
-
method: 'POST',
|
|
143
|
-
headers: { 'Content-Type': 'application/json' },
|
|
144
|
-
body: JSON.stringify({ event: 'process_restart', data: record, stats: processState }),
|
|
145
|
-
});
|
|
146
|
-
logger.debug(`Webhook 通知已发送: ${channel.target}`);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
catch (error) {
|
|
150
|
-
logger.error(`发送通知失败 (${channel.type}):`, error);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
// ─── 初始化 ──────────────────────────────────────────────────────────────────
|
|
155
|
-
if (config.enabled) {
|
|
156
|
-
loadProcessState();
|
|
157
|
-
detectStartupReason();
|
|
158
|
-
process.on('SIGTERM', () => {
|
|
159
|
-
logger.info(formatCompact({ op: 'shutdown', signal: 'SIGTERM' }));
|
|
160
|
-
saveProcessState();
|
|
161
|
-
});
|
|
162
|
-
process.on('SIGINT', () => {
|
|
163
|
-
logger.info(formatCompact({ op: 'shutdown', signal: 'SIGINT' }));
|
|
164
|
-
saveProcessState();
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
// AI 工具已迁移到 tools/*.tool.md,框架自动发现注册
|
|
168
|
-
export default plugin;
|
|
1
|
+
export { classifyStartup, formatProcessStatus, formatUptime, processState, resetProcessMonitorForTests, resolveProcessMonitorConfig, startProcessMonitor, startTime, } from './monitor.js';
|
|
169
2
|
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,eAAe,EACf,mBAAmB,EACnB,YAAY,EACZ,YAAY,EACZ,2BAA2B,EAC3B,2BAA2B,EAC3B,mBAAmB,EACnB,SAAS,GACV,MAAM,cAAc,CAAC"}
|
package/lib/monitor.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export interface NotifyChannel {
|
|
2
|
+
type: 'user' | 'group' | 'webhook';
|
|
3
|
+
target: string;
|
|
4
|
+
platform?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ProcessMonitorConfig {
|
|
7
|
+
enabled?: boolean;
|
|
8
|
+
notifyChannels?: NotifyChannel[];
|
|
9
|
+
notifyOnStart?: boolean;
|
|
10
|
+
notifyOnRestart?: boolean;
|
|
11
|
+
notifyOnCrash?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface ProcessState {
|
|
14
|
+
lastPid?: number;
|
|
15
|
+
lastStartTime?: number;
|
|
16
|
+
/** SIGTERM/SIGINT 正常退出时置 true,下次启动据此归为 restart 而非 crash。 */
|
|
17
|
+
cleanExit?: boolean;
|
|
18
|
+
restartCount: number;
|
|
19
|
+
crashCount: number;
|
|
20
|
+
totalUptime: number;
|
|
21
|
+
}
|
|
22
|
+
export declare let processState: ProcessState;
|
|
23
|
+
export declare const startTime: number;
|
|
24
|
+
export declare function resolveProcessMonitorConfig(raw: ProcessMonitorConfig | undefined): Required<Pick<ProcessMonitorConfig, 'enabled' | 'notifyOnStart' | 'notifyOnRestart' | 'notifyOnCrash'>> & {
|
|
25
|
+
notifyChannels: NotifyChannel[];
|
|
26
|
+
};
|
|
27
|
+
export declare function formatUptime(ms: number): string;
|
|
28
|
+
export declare function formatProcessStatus(): string;
|
|
29
|
+
export type StartupReason = 'start' | 'restart' | 'crash';
|
|
30
|
+
/**
|
|
31
|
+
* Classify this startup from persisted state.
|
|
32
|
+
* - 同 PID(热重载,进程未退出)跳过判定,不算 restart/crash。
|
|
33
|
+
* - 上次为 SIGTERM/SIGINT 正常退出(cleanExit)时归为 restart,即使间隔 <5min。
|
|
34
|
+
*/
|
|
35
|
+
export declare function classifyStartup(state: ProcessState, currentPid: number, now?: number): {
|
|
36
|
+
reason: StartupReason;
|
|
37
|
+
uptime?: number;
|
|
38
|
+
};
|
|
39
|
+
/** Start monitoring once; returns disposer for lifecycle cleanup. */
|
|
40
|
+
export declare function startProcessMonitor(rawConfig?: ProcessMonitorConfig): () => void;
|
|
41
|
+
/** Test helper: reset module state without touching disk. */
|
|
42
|
+
export declare function resetProcessMonitorForTests(): void;
|
|
43
|
+
//# sourceMappingURL=monitor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"monitor.d.ts","sourceRoot":"","sources":["../src/monitor.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,cAAc,CAAC,EAAE,aAAa,EAAE,CAAC;IACjC,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,4DAA4D;IAC5D,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;CACrB;AAID,eAAO,IAAI,YAAY,EAAE,YAIxB,CAAC;AAEF,eAAO,MAAM,SAAS,QAAa,CAAC;AAKpC,wBAAgB,2BAA2B,CACzC,GAAG,EAAE,oBAAoB,GAAG,SAAS,GACpC,QAAQ,CACT,IAAI,CACF,oBAAoB,EACpB,SAAS,GAAG,eAAe,GAAG,iBAAiB,GAAG,eAAe,CAClE,CACF,GAAG;IAAE,cAAc,EAAE,aAAa,EAAE,CAAA;CAAE,CAQtC;AAED,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAU/C;AAED,wBAAgB,mBAAmB,IAAI,MAAM,CAgB5C;AAkGD,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,SAAS,GAAG,OAAO,CAAC;AAE1D;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,YAAY,EACnB,UAAU,EAAE,MAAM,EAClB,GAAG,SAAa,GACf;IAAE,MAAM,EAAE,aAAa,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAQ5C;AAuCD,qEAAqE;AACrE,wBAAgB,mBAAmB,CAAC,SAAS,CAAC,EAAE,oBAAoB,GAAG,MAAM,IAAI,CA6BhF;AAED,6DAA6D;AAC7D,wBAAgB,2BAA2B,IAAI,IAAI,CAQlD"}
|
package/lib/monitor.js
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process monitor — module-level state + file-backed restart detection.
|
|
3
|
+
* No usePlugin; Plugin Runtime setup() calls startProcessMonitor().
|
|
4
|
+
*/
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
const STATE_FILE = path.join(process.cwd(), 'data', 'process-state.json');
|
|
9
|
+
export let processState = {
|
|
10
|
+
restartCount: 0,
|
|
11
|
+
crashCount: 0,
|
|
12
|
+
totalUptime: 0,
|
|
13
|
+
};
|
|
14
|
+
export const startTime = Date.now();
|
|
15
|
+
let started = false;
|
|
16
|
+
let signalHandlers = null;
|
|
17
|
+
export function resolveProcessMonitorConfig(raw) {
|
|
18
|
+
return {
|
|
19
|
+
enabled: raw?.enabled ?? true,
|
|
20
|
+
notifyChannels: raw?.notifyChannels ?? [],
|
|
21
|
+
notifyOnStart: raw?.notifyOnStart ?? true,
|
|
22
|
+
notifyOnRestart: raw?.notifyOnRestart ?? true,
|
|
23
|
+
notifyOnCrash: raw?.notifyOnCrash ?? true,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export function formatUptime(ms) {
|
|
27
|
+
const seconds = Math.floor(ms / 1000);
|
|
28
|
+
const minutes = Math.floor(seconds / 60);
|
|
29
|
+
const hours = Math.floor(minutes / 60);
|
|
30
|
+
const days = Math.floor(hours / 24);
|
|
31
|
+
if (days > 0)
|
|
32
|
+
return `${days}天${hours % 24}小时`;
|
|
33
|
+
if (hours > 0)
|
|
34
|
+
return `${hours}小时${minutes % 60}分钟`;
|
|
35
|
+
if (minutes > 0)
|
|
36
|
+
return `${minutes}分钟`;
|
|
37
|
+
return `${seconds}秒`;
|
|
38
|
+
}
|
|
39
|
+
export function formatProcessStatus() {
|
|
40
|
+
const uptime = Date.now() - startTime;
|
|
41
|
+
const memUsage = process.memoryUsage();
|
|
42
|
+
return [
|
|
43
|
+
'📊 进程监控状态',
|
|
44
|
+
'',
|
|
45
|
+
`🚀 当前 PID: ${process.pid}`,
|
|
46
|
+
`⏱️ 运行时长: ${formatUptime(uptime)}`,
|
|
47
|
+
`💾 内存使用: ${Math.round(memUsage.heapUsed / 1024 / 1024)} MB`,
|
|
48
|
+
`🔄 总重启: ${processState.restartCount} 次`,
|
|
49
|
+
`💥 崩溃: ${processState.crashCount} 次`,
|
|
50
|
+
`📈 累计运行: ${formatUptime(processState.totalUptime)}`,
|
|
51
|
+
`🖥️ 主机: ${os.hostname()}`,
|
|
52
|
+
`💻 平台: ${os.platform()}-${os.arch()}`,
|
|
53
|
+
`📦 Node: ${process.version}`,
|
|
54
|
+
].join('\n');
|
|
55
|
+
}
|
|
56
|
+
function loadProcessState() {
|
|
57
|
+
try {
|
|
58
|
+
if (fs.existsSync(STATE_FILE)) {
|
|
59
|
+
const data = fs.readFileSync(STATE_FILE, 'utf-8');
|
|
60
|
+
processState = JSON.parse(data);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// keep defaults
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function saveProcessState() {
|
|
68
|
+
try {
|
|
69
|
+
fs.mkdirSync(path.dirname(STATE_FILE), { recursive: true });
|
|
70
|
+
fs.writeFileSync(STATE_FILE, JSON.stringify(processState, null, 2));
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// ignore
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function formatNotificationMessage(record) {
|
|
77
|
+
const emoji = { start: '🚀', restart: '🔄', crash: '💥' }[record.reason] || '📊';
|
|
78
|
+
const reasonText = { start: '首次启动', restart: '正常重启', crash: '异常崩溃' }[record.reason] || '未知';
|
|
79
|
+
const lines = [
|
|
80
|
+
`${emoji} 【进程监控通知】`,
|
|
81
|
+
'',
|
|
82
|
+
`📊 事件: ${reasonText}`,
|
|
83
|
+
`⏰ 时间: ${record.timestamp.toLocaleString('zh-CN')}`,
|
|
84
|
+
`🖥️ 主机: ${record.hostname}`,
|
|
85
|
+
`🔢 PID: ${record.pid}`,
|
|
86
|
+
`💻 平台: ${record.platform}`,
|
|
87
|
+
`📦 Node: ${record.nodeVersion}`,
|
|
88
|
+
];
|
|
89
|
+
if (record.uptime) {
|
|
90
|
+
lines.push(`⏱️ 运行时长: ${formatUptime(record.uptime)}`);
|
|
91
|
+
}
|
|
92
|
+
if (record.memory) {
|
|
93
|
+
lines.push(`💾 内存: ${record.memory} MB`);
|
|
94
|
+
}
|
|
95
|
+
lines.push('', `📈 统计:`);
|
|
96
|
+
lines.push(` • 总重启: ${processState.restartCount} 次`);
|
|
97
|
+
lines.push(` • 崩溃: ${processState.crashCount} 次`);
|
|
98
|
+
lines.push(` • 累计运行: ${formatUptime(processState.totalUptime)}`);
|
|
99
|
+
return lines.join('\n');
|
|
100
|
+
}
|
|
101
|
+
async function sendNotification(config, record) {
|
|
102
|
+
// user/group 渠道尚未实现(需要接入 OutboundHost 出站链路),
|
|
103
|
+
// 目前仅支持 webhook;实现时用 formatNotificationMessage(record) 生成文本。
|
|
104
|
+
for (const channel of config.notifyChannels) {
|
|
105
|
+
if (channel.type !== 'webhook')
|
|
106
|
+
continue;
|
|
107
|
+
try {
|
|
108
|
+
await fetch(channel.target, {
|
|
109
|
+
method: 'POST',
|
|
110
|
+
headers: { 'Content-Type': 'application/json' },
|
|
111
|
+
body: JSON.stringify({
|
|
112
|
+
event: 'process_restart',
|
|
113
|
+
data: record,
|
|
114
|
+
stats: processState,
|
|
115
|
+
}),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// ignore webhook errors
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Classify this startup from persisted state.
|
|
125
|
+
* - 同 PID(热重载,进程未退出)跳过判定,不算 restart/crash。
|
|
126
|
+
* - 上次为 SIGTERM/SIGINT 正常退出(cleanExit)时归为 restart,即使间隔 <5min。
|
|
127
|
+
*/
|
|
128
|
+
export function classifyStartup(state, currentPid, now = Date.now()) {
|
|
129
|
+
if (!state.lastPid || !state.lastStartTime)
|
|
130
|
+
return { reason: 'start' };
|
|
131
|
+
if (state.lastPid === currentPid)
|
|
132
|
+
return { reason: 'start' };
|
|
133
|
+
const uptime = now - state.lastStartTime;
|
|
134
|
+
if (state.cleanExit === true || uptime >= 5 * 60 * 1000) {
|
|
135
|
+
return { reason: 'restart', uptime };
|
|
136
|
+
}
|
|
137
|
+
return { reason: 'crash', uptime };
|
|
138
|
+
}
|
|
139
|
+
async function detectStartupReason(config) {
|
|
140
|
+
const currentPid = process.pid;
|
|
141
|
+
const currentTime = Date.now();
|
|
142
|
+
const { reason, uptime } = classifyStartup(processState, currentPid, currentTime);
|
|
143
|
+
if (reason === 'crash')
|
|
144
|
+
processState.crashCount++;
|
|
145
|
+
else if (reason === 'restart')
|
|
146
|
+
processState.restartCount++;
|
|
147
|
+
if (uptime)
|
|
148
|
+
processState.totalUptime += uptime;
|
|
149
|
+
processState.cleanExit = false;
|
|
150
|
+
processState.lastPid = currentPid;
|
|
151
|
+
processState.lastStartTime = currentTime;
|
|
152
|
+
saveProcessState();
|
|
153
|
+
const record = {
|
|
154
|
+
timestamp: new Date(),
|
|
155
|
+
reason,
|
|
156
|
+
uptime,
|
|
157
|
+
pid: currentPid,
|
|
158
|
+
hostname: os.hostname(),
|
|
159
|
+
platform: `${os.platform()}-${os.arch()}`,
|
|
160
|
+
nodeVersion: process.version,
|
|
161
|
+
memory: Math.round(process.memoryUsage().heapUsed / 1024 / 1024),
|
|
162
|
+
};
|
|
163
|
+
const shouldNotify = (reason === 'start' && config.notifyOnStart) ||
|
|
164
|
+
(reason === 'restart' && config.notifyOnRestart) ||
|
|
165
|
+
(reason === 'crash' && config.notifyOnCrash);
|
|
166
|
+
if (shouldNotify && config.notifyChannels.length > 0) {
|
|
167
|
+
await sendNotification(config, record);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/** Start monitoring once; returns disposer for lifecycle cleanup. */
|
|
171
|
+
export function startProcessMonitor(rawConfig) {
|
|
172
|
+
const config = resolveProcessMonitorConfig(rawConfig);
|
|
173
|
+
if (!config.enabled || started) {
|
|
174
|
+
return () => undefined;
|
|
175
|
+
}
|
|
176
|
+
started = true;
|
|
177
|
+
loadProcessState();
|
|
178
|
+
void detectStartupReason(config);
|
|
179
|
+
const onSigterm = () => {
|
|
180
|
+
processState.cleanExit = true;
|
|
181
|
+
saveProcessState();
|
|
182
|
+
};
|
|
183
|
+
const onSigint = () => {
|
|
184
|
+
processState.cleanExit = true;
|
|
185
|
+
saveProcessState();
|
|
186
|
+
};
|
|
187
|
+
process.on('SIGTERM', onSigterm);
|
|
188
|
+
process.on('SIGINT', onSigint);
|
|
189
|
+
signalHandlers = { sigterm: onSigterm, sigint: onSigint };
|
|
190
|
+
return () => {
|
|
191
|
+
if (signalHandlers) {
|
|
192
|
+
process.removeListener('SIGTERM', signalHandlers.sigterm);
|
|
193
|
+
process.removeListener('SIGINT', signalHandlers.sigint);
|
|
194
|
+
signalHandlers = null;
|
|
195
|
+
}
|
|
196
|
+
started = false;
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
/** Test helper: reset module state without touching disk. */
|
|
200
|
+
export function resetProcessMonitorForTests() {
|
|
201
|
+
if (signalHandlers) {
|
|
202
|
+
process.removeListener('SIGTERM', signalHandlers.sigterm);
|
|
203
|
+
process.removeListener('SIGINT', signalHandlers.sigint);
|
|
204
|
+
signalHandlers = null;
|
|
205
|
+
}
|
|
206
|
+
processState = { restartCount: 0, crashCount: 0, totalUptime: 0 };
|
|
207
|
+
started = false;
|
|
208
|
+
}
|
|
209
|
+
//# sourceMappingURL=monitor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"monitor.js","sourceRoot":"","sources":["../src/monitor.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/process-monitor",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "进程监控与重启通知插件",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "进程监控与重启通知插件 (Plugin Runtime)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
7
7
|
"types": "./lib/index.d.ts",
|
|
@@ -14,13 +14,12 @@
|
|
|
14
14
|
"./package.json": "./package.json"
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
17
|
+
"plugin.js",
|
|
18
|
+
"schema.json",
|
|
19
|
+
"commands",
|
|
20
|
+
"tools",
|
|
17
21
|
"src",
|
|
18
22
|
"lib",
|
|
19
|
-
"client",
|
|
20
|
-
"dist",
|
|
21
|
-
"skills",
|
|
22
|
-
"tools",
|
|
23
|
-
"plugin.yml",
|
|
24
23
|
"README.md"
|
|
25
24
|
],
|
|
26
25
|
"keywords": [
|
|
@@ -39,18 +38,51 @@
|
|
|
39
38
|
"directory": "plugins/features/process-monitor"
|
|
40
39
|
},
|
|
41
40
|
"dependencies": {
|
|
42
|
-
"zhin.js": "
|
|
41
|
+
"@zhin.js/tool": "1.1.0"
|
|
43
42
|
},
|
|
44
43
|
"devDependencies": {
|
|
45
|
-
"@types/node": "^
|
|
44
|
+
"@types/node": "^26.1.2",
|
|
46
45
|
"typescript": "^6.0.3",
|
|
47
|
-
"
|
|
46
|
+
"vitest": "^4.1.10",
|
|
47
|
+
"zhin.js": "1.1.0"
|
|
48
|
+
},
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": "^20.19.0 || >=22.12.0"
|
|
51
|
+
},
|
|
52
|
+
"publishConfig": {
|
|
53
|
+
"access": "public",
|
|
54
|
+
"registry": "https://registry.npmjs.org"
|
|
55
|
+
},
|
|
56
|
+
"zhin": {
|
|
57
|
+
"protocol": 1,
|
|
58
|
+
"type": "plugin",
|
|
59
|
+
"entry": "./plugin.js",
|
|
60
|
+
"engine": "^1.0.0",
|
|
61
|
+
"runtime": "trusted",
|
|
62
|
+
"features": [
|
|
63
|
+
{
|
|
64
|
+
"package": "@zhin.js/command",
|
|
65
|
+
"api": "^1.0.0"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"package": "@zhin.js/tool",
|
|
69
|
+
"api": "^1.0.0"
|
|
70
|
+
}
|
|
71
|
+
],
|
|
72
|
+
"plugins": []
|
|
48
73
|
},
|
|
49
74
|
"peerDependencies": {
|
|
50
|
-
"zhin.js": "
|
|
75
|
+
"@zhin.js/command": "^1.1.0",
|
|
76
|
+
"zhin.js": "^1.1.0"
|
|
77
|
+
},
|
|
78
|
+
"peerDependenciesMeta": {
|
|
79
|
+
"@zhin.js/command": {
|
|
80
|
+
"optional": true
|
|
81
|
+
}
|
|
51
82
|
},
|
|
52
83
|
"scripts": {
|
|
53
84
|
"build": "tsc",
|
|
54
|
-
"
|
|
85
|
+
"clean": "rimraf lib",
|
|
86
|
+
"test": "vitest run --root ../../.. plugins/features/process-monitor/tests"
|
|
55
87
|
}
|
|
56
88
|
}
|
package/plugin.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
+
import { definePlugin } from 'zhin.js';
|
|
3
|
+
import { resolveProcessMonitorConfig, startProcessMonitor, } from "./lib/monitor.js";
|
|
4
|
+
/**
|
|
5
|
+
* Process monitor Plugin Runtime cutover:
|
|
6
|
+
* - setup() owns file-backed restart detection + signal cleanup
|
|
7
|
+
* - commands/process-status for chat status
|
|
8
|
+
* - tools/process-status kept as agent tool surface
|
|
9
|
+
*/
|
|
10
|
+
export default definePlugin({
|
|
11
|
+
name: 'process-monitor',
|
|
12
|
+
metadata: {
|
|
13
|
+
displayName: 'Process Monitor',
|
|
14
|
+
},
|
|
15
|
+
setup(context) {
|
|
16
|
+
const config = resolveProcessMonitorConfig(context.config.get());
|
|
17
|
+
const dispose = startProcessMonitor(config);
|
|
18
|
+
context.lifecycle.add(dispose);
|
|
19
|
+
},
|
|
20
|
+
});
|
package/schema.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"additionalProperties": false,
|
|
5
|
+
"properties": {
|
|
6
|
+
"enabled": {
|
|
7
|
+
"type": "boolean",
|
|
8
|
+
"default": true,
|
|
9
|
+
"description": "是否启用进程监控"
|
|
10
|
+
},
|
|
11
|
+
"notifyOnStart": {
|
|
12
|
+
"type": "boolean",
|
|
13
|
+
"default": true,
|
|
14
|
+
"description": "首次启动时通知"
|
|
15
|
+
},
|
|
16
|
+
"notifyOnRestart": {
|
|
17
|
+
"type": "boolean",
|
|
18
|
+
"default": true,
|
|
19
|
+
"description": "正常重启时通知"
|
|
20
|
+
},
|
|
21
|
+
"notifyOnCrash": {
|
|
22
|
+
"type": "boolean",
|
|
23
|
+
"default": true,
|
|
24
|
+
"description": "异常崩溃重启时通知"
|
|
25
|
+
},
|
|
26
|
+
"notifyChannels": {
|
|
27
|
+
"type": "array",
|
|
28
|
+
"default": [],
|
|
29
|
+
"description": "通知渠道(slice-1 仅 webhook 生效)",
|
|
30
|
+
"items": {
|
|
31
|
+
"type": "object",
|
|
32
|
+
"additionalProperties": false,
|
|
33
|
+
"required": ["type", "target"],
|
|
34
|
+
"properties": {
|
|
35
|
+
"type": {
|
|
36
|
+
"type": "string",
|
|
37
|
+
"enum": ["user", "group", "webhook"]
|
|
38
|
+
},
|
|
39
|
+
"target": { "type": "string" },
|
|
40
|
+
"platform": { "type": "string" }
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,220 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const { logger, root } = plugin;
|
|
13
|
-
|
|
14
|
-
// ─── 配置 ────────────────────────────────────────────────────────────────────
|
|
15
|
-
|
|
16
|
-
interface NotifyChannel {
|
|
17
|
-
type: 'user' | 'group' | 'webhook';
|
|
18
|
-
target: string;
|
|
19
|
-
platform?: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
interface Config {
|
|
23
|
-
enabled?: boolean;
|
|
24
|
-
notifyChannels?: NotifyChannel[];
|
|
25
|
-
notifyOnStart?: boolean;
|
|
26
|
-
notifyOnRestart?: boolean;
|
|
27
|
-
notifyOnCrash?: boolean;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const configService = root.inject('config');
|
|
31
|
-
const appConfig = configService?.getPrimary<{ 'process-monitor'?: Config }>() || {};
|
|
32
|
-
const config: Config = {
|
|
33
|
-
enabled: true,
|
|
34
|
-
notifyChannels: [],
|
|
35
|
-
notifyOnStart: true,
|
|
36
|
-
notifyOnRestart: true,
|
|
37
|
-
notifyOnCrash: true,
|
|
38
|
-
...appConfig['process-monitor'],
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
if (!config.enabled) {
|
|
42
|
-
logger.info(formatCompact( { op: 'load', enabled: false }));
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// ─── 状态管理 ────────────────────────────────────────────────────────────────
|
|
46
|
-
|
|
47
|
-
const STATE_FILE = path.join(process.cwd(), 'data', 'process-state.json');
|
|
48
|
-
|
|
49
|
-
interface ProcessState {
|
|
50
|
-
lastPid?: number;
|
|
51
|
-
lastStartTime?: number;
|
|
52
|
-
restartCount: number;
|
|
53
|
-
crashCount: number;
|
|
54
|
-
totalUptime: number;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export let processState: ProcessState = {
|
|
58
|
-
restartCount: 0,
|
|
59
|
-
crashCount: 0,
|
|
60
|
-
totalUptime: 0,
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
export const startTime = Date.now();
|
|
64
|
-
|
|
65
|
-
function loadProcessState() {
|
|
66
|
-
try {
|
|
67
|
-
if (fs.existsSync(STATE_FILE)) {
|
|
68
|
-
const data = fs.readFileSync(STATE_FILE, 'utf-8');
|
|
69
|
-
processState = JSON.parse(data);
|
|
70
|
-
}
|
|
71
|
-
} catch (error) {
|
|
72
|
-
logger.warn(formatCompact( { op: 'load_state', ok: false, error: String(error) }));
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function saveProcessState() {
|
|
77
|
-
try {
|
|
78
|
-
fs.mkdirSync(path.dirname(STATE_FILE), { recursive: true });
|
|
79
|
-
fs.writeFileSync(STATE_FILE, JSON.stringify(processState, null, 2));
|
|
80
|
-
} catch (error) {
|
|
81
|
-
logger.warn(formatCompact( { op: 'save_state', ok: false, error: String(error) }));
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// ─── 启动检测 ────────────────────────────────────────────────────────────────
|
|
86
|
-
|
|
87
|
-
async function detectStartupReason() {
|
|
88
|
-
const currentPid = process.pid;
|
|
89
|
-
const currentTime = Date.now();
|
|
90
|
-
let reason: 'start' | 'restart' | 'crash' = 'start';
|
|
91
|
-
let uptime: number | undefined;
|
|
92
|
-
|
|
93
|
-
if (processState.lastPid && processState.lastStartTime) {
|
|
94
|
-
const timeSinceLastStart = currentTime - processState.lastStartTime;
|
|
95
|
-
|
|
96
|
-
if (timeSinceLastStart < 5 * 60 * 1000) {
|
|
97
|
-
reason = 'crash';
|
|
98
|
-
processState.crashCount++;
|
|
99
|
-
logger.warn(formatCompact( { op: 'restart', abnormal: true, since_last_s: Math.floor(timeSinceLastStart / 1000) }));
|
|
100
|
-
} else {
|
|
101
|
-
reason = 'restart';
|
|
102
|
-
processState.restartCount++;
|
|
103
|
-
logger.info(formatCompact( { op: 'restart', abnormal: false }));
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
uptime = timeSinceLastStart;
|
|
107
|
-
processState.totalUptime += uptime;
|
|
108
|
-
} else {
|
|
109
|
-
logger.info(formatCompact({ first: true }));
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
processState.lastPid = currentPid;
|
|
113
|
-
processState.lastStartTime = currentTime;
|
|
114
|
-
saveProcessState();
|
|
115
|
-
|
|
116
|
-
const record = {
|
|
117
|
-
timestamp: new Date(),
|
|
118
|
-
reason,
|
|
119
|
-
uptime,
|
|
120
|
-
pid: currentPid,
|
|
121
|
-
hostname: os.hostname(),
|
|
122
|
-
platform: `${os.platform()}-${os.arch()}`,
|
|
123
|
-
nodeVersion: process.version,
|
|
124
|
-
memory: Math.round(process.memoryUsage().heapUsed / 1024 / 1024),
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
const shouldNotify =
|
|
128
|
-
(reason === 'start' && config.notifyOnStart) ||
|
|
129
|
-
(reason === 'restart' && config.notifyOnRestart) ||
|
|
130
|
-
(reason === 'crash' && config.notifyOnCrash);
|
|
131
|
-
|
|
132
|
-
if (shouldNotify && config.notifyChannels && config.notifyChannels.length > 0) {
|
|
133
|
-
await sendNotification(record);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// ─── 通知 ────────────────────────────────────────────────────────────────────
|
|
138
|
-
|
|
139
|
-
function formatNotificationMessage(record: any): string {
|
|
140
|
-
const emoji = { start: '🚀', restart: '🔄', crash: '💥' }[record.reason] || '📊';
|
|
141
|
-
const reasonText = { start: '首次启动', restart: '正常重启', crash: '异常崩溃' }[record.reason] || '未知';
|
|
142
|
-
|
|
143
|
-
const lines = [
|
|
144
|
-
`${emoji} 【进程监控通知】`,
|
|
145
|
-
'',
|
|
146
|
-
`📊 事件: ${reasonText}`,
|
|
147
|
-
`⏰ 时间: ${record.timestamp.toLocaleString('zh-CN')}`,
|
|
148
|
-
`🖥️ 主机: ${record.hostname}`,
|
|
149
|
-
`🔢 PID: ${record.pid}`,
|
|
150
|
-
`💻 平台: ${record.platform}`,
|
|
151
|
-
`📦 Node: ${record.nodeVersion}`,
|
|
152
|
-
];
|
|
153
|
-
|
|
154
|
-
if (record.uptime) {
|
|
155
|
-
lines.push(`⏱️ 运行时长: ${formatUptime(record.uptime)}`);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
if (record.memory) {
|
|
159
|
-
lines.push(`💾 内存: ${record.memory} MB`);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
lines.push('', `📈 统计:`);
|
|
163
|
-
lines.push(` • 总重启: ${processState.restartCount} 次`);
|
|
164
|
-
lines.push(` • 崩溃: ${processState.crashCount} 次`);
|
|
165
|
-
lines.push(` • 累计运行: ${formatUptime(processState.totalUptime)}`);
|
|
166
|
-
|
|
167
|
-
return lines.join('\n');
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
export function formatUptime(ms: number): string {
|
|
171
|
-
const seconds = Math.floor(ms / 1000);
|
|
172
|
-
const minutes = Math.floor(seconds / 60);
|
|
173
|
-
const hours = Math.floor(minutes / 60);
|
|
174
|
-
const days = Math.floor(hours / 24);
|
|
175
|
-
|
|
176
|
-
if (days > 0) return `${days}天${hours % 24}小时`;
|
|
177
|
-
if (hours > 0) return `${hours}小时${minutes % 60}分钟`;
|
|
178
|
-
if (minutes > 0) return `${minutes}分钟`;
|
|
179
|
-
return `${seconds}秒`;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
async function sendNotification(record: any) {
|
|
183
|
-
const message = formatNotificationMessage(record);
|
|
184
|
-
|
|
185
|
-
for (const channel of config.notifyChannels || []) {
|
|
186
|
-
try {
|
|
187
|
-
if (channel.type === 'webhook') {
|
|
188
|
-
await fetch(channel.target, {
|
|
189
|
-
method: 'POST',
|
|
190
|
-
headers: { 'Content-Type': 'application/json' },
|
|
191
|
-
body: JSON.stringify({ event: 'process_restart', data: record, stats: processState }),
|
|
192
|
-
});
|
|
193
|
-
logger.debug(`Webhook 通知已发送: ${channel.target}`);
|
|
194
|
-
}
|
|
195
|
-
} catch (error) {
|
|
196
|
-
logger.error(`发送通知失败 (${channel.type}):`, error);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// ─── 初始化 ──────────────────────────────────────────────────────────────────
|
|
202
|
-
|
|
203
|
-
if (config.enabled) {
|
|
204
|
-
loadProcessState();
|
|
205
|
-
detectStartupReason();
|
|
206
|
-
|
|
207
|
-
process.on('SIGTERM', () => {
|
|
208
|
-
logger.info(formatCompact( { op: 'shutdown', signal: 'SIGTERM' }));
|
|
209
|
-
saveProcessState();
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
process.on('SIGINT', () => {
|
|
213
|
-
logger.info(formatCompact( { op: 'shutdown', signal: 'SIGINT' }));
|
|
214
|
-
saveProcessState();
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// AI 工具已迁移到 tools/*.tool.md,框架自动发现注册
|
|
219
|
-
|
|
220
|
-
export default plugin;
|
|
1
|
+
export {
|
|
2
|
+
classifyStartup,
|
|
3
|
+
formatProcessStatus,
|
|
4
|
+
formatUptime,
|
|
5
|
+
processState,
|
|
6
|
+
resetProcessMonitorForTests,
|
|
7
|
+
resolveProcessMonitorConfig,
|
|
8
|
+
startProcessMonitor,
|
|
9
|
+
startTime,
|
|
10
|
+
} from './monitor.js';
|
|
11
|
+
export type { NotifyChannel, ProcessMonitorConfig, ProcessState, StartupReason } from './monitor.js';
|
package/src/monitor.ts
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process monitor — module-level state + file-backed restart detection.
|
|
3
|
+
* No usePlugin; Plugin Runtime setup() calls startProcessMonitor().
|
|
4
|
+
*/
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
|
|
9
|
+
export interface NotifyChannel {
|
|
10
|
+
type: 'user' | 'group' | 'webhook';
|
|
11
|
+
target: string;
|
|
12
|
+
platform?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ProcessMonitorConfig {
|
|
16
|
+
enabled?: boolean;
|
|
17
|
+
notifyChannels?: NotifyChannel[];
|
|
18
|
+
notifyOnStart?: boolean;
|
|
19
|
+
notifyOnRestart?: boolean;
|
|
20
|
+
notifyOnCrash?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ProcessState {
|
|
24
|
+
lastPid?: number;
|
|
25
|
+
lastStartTime?: number;
|
|
26
|
+
/** SIGTERM/SIGINT 正常退出时置 true,下次启动据此归为 restart 而非 crash。 */
|
|
27
|
+
cleanExit?: boolean;
|
|
28
|
+
restartCount: number;
|
|
29
|
+
crashCount: number;
|
|
30
|
+
totalUptime: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const STATE_FILE = path.join(process.cwd(), 'data', 'process-state.json');
|
|
34
|
+
|
|
35
|
+
export let processState: ProcessState = {
|
|
36
|
+
restartCount: 0,
|
|
37
|
+
crashCount: 0,
|
|
38
|
+
totalUptime: 0,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const startTime = Date.now();
|
|
42
|
+
|
|
43
|
+
let started = false;
|
|
44
|
+
let signalHandlers: { sigterm: () => void; sigint: () => void } | null = null;
|
|
45
|
+
|
|
46
|
+
export function resolveProcessMonitorConfig(
|
|
47
|
+
raw: ProcessMonitorConfig | undefined,
|
|
48
|
+
): Required<
|
|
49
|
+
Pick<
|
|
50
|
+
ProcessMonitorConfig,
|
|
51
|
+
'enabled' | 'notifyOnStart' | 'notifyOnRestart' | 'notifyOnCrash'
|
|
52
|
+
>
|
|
53
|
+
> & { notifyChannels: NotifyChannel[] } {
|
|
54
|
+
return {
|
|
55
|
+
enabled: raw?.enabled ?? true,
|
|
56
|
+
notifyChannels: raw?.notifyChannels ?? [],
|
|
57
|
+
notifyOnStart: raw?.notifyOnStart ?? true,
|
|
58
|
+
notifyOnRestart: raw?.notifyOnRestart ?? true,
|
|
59
|
+
notifyOnCrash: raw?.notifyOnCrash ?? true,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function formatUptime(ms: number): string {
|
|
64
|
+
const seconds = Math.floor(ms / 1000);
|
|
65
|
+
const minutes = Math.floor(seconds / 60);
|
|
66
|
+
const hours = Math.floor(minutes / 60);
|
|
67
|
+
const days = Math.floor(hours / 24);
|
|
68
|
+
|
|
69
|
+
if (days > 0) return `${days}天${hours % 24}小时`;
|
|
70
|
+
if (hours > 0) return `${hours}小时${minutes % 60}分钟`;
|
|
71
|
+
if (minutes > 0) return `${minutes}分钟`;
|
|
72
|
+
return `${seconds}秒`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function formatProcessStatus(): string {
|
|
76
|
+
const uptime = Date.now() - startTime;
|
|
77
|
+
const memUsage = process.memoryUsage();
|
|
78
|
+
return [
|
|
79
|
+
'📊 进程监控状态',
|
|
80
|
+
'',
|
|
81
|
+
`🚀 当前 PID: ${process.pid}`,
|
|
82
|
+
`⏱️ 运行时长: ${formatUptime(uptime)}`,
|
|
83
|
+
`💾 内存使用: ${Math.round(memUsage.heapUsed / 1024 / 1024)} MB`,
|
|
84
|
+
`🔄 总重启: ${processState.restartCount} 次`,
|
|
85
|
+
`💥 崩溃: ${processState.crashCount} 次`,
|
|
86
|
+
`📈 累计运行: ${formatUptime(processState.totalUptime)}`,
|
|
87
|
+
`🖥️ 主机: ${os.hostname()}`,
|
|
88
|
+
`💻 平台: ${os.platform()}-${os.arch()}`,
|
|
89
|
+
`📦 Node: ${process.version}`,
|
|
90
|
+
].join('\n');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function loadProcessState(): void {
|
|
94
|
+
try {
|
|
95
|
+
if (fs.existsSync(STATE_FILE)) {
|
|
96
|
+
const data = fs.readFileSync(STATE_FILE, 'utf-8');
|
|
97
|
+
processState = JSON.parse(data);
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
// keep defaults
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function saveProcessState(): void {
|
|
105
|
+
try {
|
|
106
|
+
fs.mkdirSync(path.dirname(STATE_FILE), { recursive: true });
|
|
107
|
+
fs.writeFileSync(STATE_FILE, JSON.stringify(processState, null, 2));
|
|
108
|
+
} catch {
|
|
109
|
+
// ignore
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function formatNotificationMessage(record: {
|
|
114
|
+
reason: string;
|
|
115
|
+
timestamp: Date;
|
|
116
|
+
hostname: string;
|
|
117
|
+
pid: number;
|
|
118
|
+
platform: string;
|
|
119
|
+
nodeVersion: string;
|
|
120
|
+
uptime?: number;
|
|
121
|
+
memory?: number;
|
|
122
|
+
}): string {
|
|
123
|
+
const emoji = ({ start: '🚀', restart: '🔄', crash: '💥' } as Record<string, string>)[
|
|
124
|
+
record.reason
|
|
125
|
+
] || '📊';
|
|
126
|
+
const reasonText = (
|
|
127
|
+
{ start: '首次启动', restart: '正常重启', crash: '异常崩溃' } as Record<string, string>
|
|
128
|
+
)[record.reason] || '未知';
|
|
129
|
+
|
|
130
|
+
const lines = [
|
|
131
|
+
`${emoji} 【进程监控通知】`,
|
|
132
|
+
'',
|
|
133
|
+
`📊 事件: ${reasonText}`,
|
|
134
|
+
`⏰ 时间: ${record.timestamp.toLocaleString('zh-CN')}`,
|
|
135
|
+
`🖥️ 主机: ${record.hostname}`,
|
|
136
|
+
`🔢 PID: ${record.pid}`,
|
|
137
|
+
`💻 平台: ${record.platform}`,
|
|
138
|
+
`📦 Node: ${record.nodeVersion}`,
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
if (record.uptime) {
|
|
142
|
+
lines.push(`⏱️ 运行时长: ${formatUptime(record.uptime)}`);
|
|
143
|
+
}
|
|
144
|
+
if (record.memory) {
|
|
145
|
+
lines.push(`💾 内存: ${record.memory} MB`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
lines.push('', `📈 统计:`);
|
|
149
|
+
lines.push(` • 总重启: ${processState.restartCount} 次`);
|
|
150
|
+
lines.push(` • 崩溃: ${processState.crashCount} 次`);
|
|
151
|
+
lines.push(` • 累计运行: ${formatUptime(processState.totalUptime)}`);
|
|
152
|
+
|
|
153
|
+
return lines.join('\n');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function sendNotification(
|
|
157
|
+
config: ReturnType<typeof resolveProcessMonitorConfig>,
|
|
158
|
+
record: {
|
|
159
|
+
reason: string;
|
|
160
|
+
timestamp: Date;
|
|
161
|
+
hostname: string;
|
|
162
|
+
pid: number;
|
|
163
|
+
platform: string;
|
|
164
|
+
nodeVersion: string;
|
|
165
|
+
uptime?: number;
|
|
166
|
+
memory?: number;
|
|
167
|
+
},
|
|
168
|
+
): Promise<void> {
|
|
169
|
+
// user/group 渠道尚未实现(需要接入 OutboundHost 出站链路),
|
|
170
|
+
// 目前仅支持 webhook;实现时用 formatNotificationMessage(record) 生成文本。
|
|
171
|
+
for (const channel of config.notifyChannels) {
|
|
172
|
+
if (channel.type !== 'webhook') continue;
|
|
173
|
+
try {
|
|
174
|
+
await fetch(channel.target, {
|
|
175
|
+
method: 'POST',
|
|
176
|
+
headers: { 'Content-Type': 'application/json' },
|
|
177
|
+
body: JSON.stringify({
|
|
178
|
+
event: 'process_restart',
|
|
179
|
+
data: record,
|
|
180
|
+
stats: processState,
|
|
181
|
+
}),
|
|
182
|
+
});
|
|
183
|
+
} catch {
|
|
184
|
+
// ignore webhook errors
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export type StartupReason = 'start' | 'restart' | 'crash';
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Classify this startup from persisted state.
|
|
193
|
+
* - 同 PID(热重载,进程未退出)跳过判定,不算 restart/crash。
|
|
194
|
+
* - 上次为 SIGTERM/SIGINT 正常退出(cleanExit)时归为 restart,即使间隔 <5min。
|
|
195
|
+
*/
|
|
196
|
+
export function classifyStartup(
|
|
197
|
+
state: ProcessState,
|
|
198
|
+
currentPid: number,
|
|
199
|
+
now = Date.now(),
|
|
200
|
+
): { reason: StartupReason; uptime?: number } {
|
|
201
|
+
if (!state.lastPid || !state.lastStartTime) return { reason: 'start' };
|
|
202
|
+
if (state.lastPid === currentPid) return { reason: 'start' };
|
|
203
|
+
const uptime = now - state.lastStartTime;
|
|
204
|
+
if (state.cleanExit === true || uptime >= 5 * 60 * 1000) {
|
|
205
|
+
return { reason: 'restart', uptime };
|
|
206
|
+
}
|
|
207
|
+
return { reason: 'crash', uptime };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function detectStartupReason(
|
|
211
|
+
config: ReturnType<typeof resolveProcessMonitorConfig>,
|
|
212
|
+
): Promise<void> {
|
|
213
|
+
const currentPid = process.pid;
|
|
214
|
+
const currentTime = Date.now();
|
|
215
|
+
|
|
216
|
+
const { reason, uptime } = classifyStartup(processState, currentPid, currentTime);
|
|
217
|
+
if (reason === 'crash') processState.crashCount++;
|
|
218
|
+
else if (reason === 'restart') processState.restartCount++;
|
|
219
|
+
if (uptime) processState.totalUptime += uptime;
|
|
220
|
+
|
|
221
|
+
processState.cleanExit = false;
|
|
222
|
+
processState.lastPid = currentPid;
|
|
223
|
+
processState.lastStartTime = currentTime;
|
|
224
|
+
saveProcessState();
|
|
225
|
+
|
|
226
|
+
const record = {
|
|
227
|
+
timestamp: new Date(),
|
|
228
|
+
reason,
|
|
229
|
+
uptime,
|
|
230
|
+
pid: currentPid,
|
|
231
|
+
hostname: os.hostname(),
|
|
232
|
+
platform: `${os.platform()}-${os.arch()}`,
|
|
233
|
+
nodeVersion: process.version,
|
|
234
|
+
memory: Math.round(process.memoryUsage().heapUsed / 1024 / 1024),
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const shouldNotify =
|
|
238
|
+
(reason === 'start' && config.notifyOnStart) ||
|
|
239
|
+
(reason === 'restart' && config.notifyOnRestart) ||
|
|
240
|
+
(reason === 'crash' && config.notifyOnCrash);
|
|
241
|
+
|
|
242
|
+
if (shouldNotify && config.notifyChannels.length > 0) {
|
|
243
|
+
await sendNotification(config, record);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Start monitoring once; returns disposer for lifecycle cleanup. */
|
|
248
|
+
export function startProcessMonitor(rawConfig?: ProcessMonitorConfig): () => void {
|
|
249
|
+
const config = resolveProcessMonitorConfig(rawConfig);
|
|
250
|
+
if (!config.enabled || started) {
|
|
251
|
+
return () => undefined;
|
|
252
|
+
}
|
|
253
|
+
started = true;
|
|
254
|
+
loadProcessState();
|
|
255
|
+
void detectStartupReason(config);
|
|
256
|
+
|
|
257
|
+
const onSigterm = () => {
|
|
258
|
+
processState.cleanExit = true;
|
|
259
|
+
saveProcessState();
|
|
260
|
+
};
|
|
261
|
+
const onSigint = () => {
|
|
262
|
+
processState.cleanExit = true;
|
|
263
|
+
saveProcessState();
|
|
264
|
+
};
|
|
265
|
+
process.on('SIGTERM', onSigterm);
|
|
266
|
+
process.on('SIGINT', onSigint);
|
|
267
|
+
signalHandlers = { sigterm: onSigterm, sigint: onSigint };
|
|
268
|
+
|
|
269
|
+
return () => {
|
|
270
|
+
if (signalHandlers) {
|
|
271
|
+
process.removeListener('SIGTERM', signalHandlers.sigterm);
|
|
272
|
+
process.removeListener('SIGINT', signalHandlers.sigint);
|
|
273
|
+
signalHandlers = null;
|
|
274
|
+
}
|
|
275
|
+
started = false;
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Test helper: reset module state without touching disk. */
|
|
280
|
+
export function resetProcessMonitorForTests(): void {
|
|
281
|
+
if (signalHandlers) {
|
|
282
|
+
process.removeListener('SIGTERM', signalHandlers.sigterm);
|
|
283
|
+
process.removeListener('SIGINT', signalHandlers.sigint);
|
|
284
|
+
signalHandlers = null;
|
|
285
|
+
}
|
|
286
|
+
processState = { restartCount: 0, crashCount: 0, totalUptime: 0 };
|
|
287
|
+
started = false;
|
|
288
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
+
import { defineAgentTool } from '@zhin.js/tool';
|
|
3
|
+
import { formatProcessStatus } from "../lib/monitor.js";
|
|
4
|
+
export default defineAgentTool({
|
|
5
|
+
description: '查看进程监控状态,包括 PID、运行时长、内存、重启和崩溃统计',
|
|
6
|
+
inputSchema: {
|
|
7
|
+
type: 'object',
|
|
8
|
+
properties: {},
|
|
9
|
+
},
|
|
10
|
+
approval: 'never',
|
|
11
|
+
execute: () => formatProcessStatus(),
|
|
12
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { defineAgentTool } from '@zhin.js/tool';
|
|
2
|
+
import { formatProcessStatus } from '../src/monitor.js';
|
|
3
|
+
|
|
4
|
+
export default defineAgentTool({
|
|
5
|
+
description: '查看进程监控状态,包括 PID、运行时长、内存、重启和崩溃统计',
|
|
6
|
+
inputSchema: {
|
|
7
|
+
type: 'object',
|
|
8
|
+
properties: {},
|
|
9
|
+
},
|
|
10
|
+
approval: 'never',
|
|
11
|
+
execute: () => formatProcessStatus(),
|
|
12
|
+
});
|
package/plugin.yml
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import os from 'os';
|
|
2
|
-
import { processState, startTime, formatUptime } from '../../src/index.js';
|
|
3
|
-
|
|
4
|
-
export default async function processStatus(): Promise<string> {
|
|
5
|
-
const uptime = Date.now() - startTime;
|
|
6
|
-
const memUsage = process.memoryUsage();
|
|
7
|
-
|
|
8
|
-
return [
|
|
9
|
-
'📊 进程监控状态',
|
|
10
|
-
'',
|
|
11
|
-
`🚀 当前 PID: ${process.pid}`,
|
|
12
|
-
`⏱️ 运行时长: ${formatUptime(uptime)}`,
|
|
13
|
-
`💾 内存使用: ${Math.round(memUsage.heapUsed / 1024 / 1024)} MB`,
|
|
14
|
-
`🔄 总重启: ${processState.restartCount} 次`,
|
|
15
|
-
`💥 崩溃: ${processState.crashCount} 次`,
|
|
16
|
-
`📈 累计运行: ${formatUptime(processState.totalUptime)}`,
|
|
17
|
-
`🖥️ 主机: ${os.hostname()}`,
|
|
18
|
-
`💻 平台: ${os.platform()}-${os.arch()}`,
|
|
19
|
-
`📦 Node: ${process.version}`,
|
|
20
|
-
].join('\n');
|
|
21
|
-
}
|