devassist-agent 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +467 -0
- package/bin/devassist.js +220 -0
- package/package.json +44 -0
- package/src/ai/adapter.js +464 -0
- package/src/ai/providers/claude.js +80 -0
- package/src/ai/providers/hunyuan.js +87 -0
- package/src/ai/providers/openai.js +74 -0
- package/src/ai/providers/qwen.js +81 -0
- package/src/cli/commands/ai.js +944 -0
- package/src/cli/commands/ask.js +79 -0
- package/src/cli/commands/backup.js +30 -0
- package/src/cli/commands/check.js +327 -0
- package/src/cli/commands/clean.js +130 -0
- package/src/cli/commands/comparison-report.js +326 -0
- package/src/cli/commands/convention.js +91 -0
- package/src/cli/commands/debt.js +49 -0
- package/src/cli/commands/deploy.js +88 -0
- package/src/cli/commands/diff.js +193 -0
- package/src/cli/commands/doctor.js +186 -0
- package/src/cli/commands/fix.js +195 -0
- package/src/cli/commands/init.js +431 -0
- package/src/cli/commands/inject.js +254 -0
- package/src/cli/commands/report.js +310 -0
- package/src/cli/commands/restore.js +78 -0
- package/src/cli/commands/schema.js +93 -0
- package/src/cli/commands/watch.js +212 -0
- package/src/cli/shared/code-context.js +51 -0
- package/src/cli/shared/config-loader.js +89 -0
- package/src/cli/shared/file-collector.js +116 -0
- package/src/cli/shared/inline-ignore.js +142 -0
- package/src/cli/shared/watch-list.js +281 -0
- package/src/core/event-bus.js +83 -0
- package/src/core/fsm.js +103 -0
- package/src/core/logger.js +54 -0
- package/src/core/rule-engine.js +117 -0
- package/src/index.js +64 -0
- package/src/modules/dev-time/arch-risk-assessor.js +250 -0
- package/src/modules/dev-time/code-quality-guard.js +1340 -0
- package/src/modules/dev-time/convention-store.js +201 -0
- package/src/modules/dev-time/impact-analyzer.js +292 -0
- package/src/modules/dev-time/pre-deploy-guard.js +284 -0
- package/src/modules/dev-time/schema-registry.js +284 -0
- package/src/modules/dev-time/tech-debt-tracker.js +225 -0
- package/src/modules/dev-time/version-manager.js +280 -0
- package/src/modules/runtime/channel-agent.js +404 -0
- package/src/modules/runtime/channel-middleware.js +316 -0
- package/src/modules/runtime/index.js +64 -0
- package/src/modules/runtime/infrastructure-guard.js +582 -0
- package/src/modules/runtime/notification-center.js +443 -0
- package/src/modules/runtime/schema-gatekeeper.js +664 -0
- package/src/modules/runtime/tool-registry.js +329 -0
- package/templates/ci/github-actions.yml +60 -0
- package/templates/ci/gitlab-ci.yml +30 -0
- package/templates/ci/pre-commit-hook.sh +18 -0
- package/templates/git-hooks/pre-commit +61 -0
- package/tests/run-all.js +434 -0
- package/tests/test-layer2.js +461 -0
- package/tests/test-new-rules.js +157 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime module index — exports all Layer 2 base code modules.
|
|
3
|
+
*
|
|
4
|
+
* This file is used both:
|
|
5
|
+
* 1. Internally by devassist for testing
|
|
6
|
+
* 2. As the entry point for installed base code in projects
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { ToolRegistry, registry, MODULE_TYPES, TOOL_INTERFACE } = require('./tool-registry');
|
|
10
|
+
const { ChannelAgent, ActionRouter, PROTOCOL_VERSION, STATE, PRIORITY } = require('./channel-agent');
|
|
11
|
+
const { compose, requestLogger, authGuard, rateLimiter, schemaValidator, errorHandler, corsHandler } = require('./channel-middleware');
|
|
12
|
+
const { InfrastructureGuard, Probe, HttpProbe, DatabaseProbe, MemoryProbe, DiskProbe, ProcessProbe, HEALTH_STATE } = require('./infrastructure-guard');
|
|
13
|
+
const { NotificationCenter, center, Channel, LogChannel, EmailChannel, WebhookChannel, DingTalkChannel, LEVELS } = require('./notification-center');
|
|
14
|
+
const { SchemaGatekeeper, gatekeeper, LAYERS, SEVERITY } = require('./schema-gatekeeper');
|
|
15
|
+
|
|
16
|
+
module.exports = {
|
|
17
|
+
// ToolRegistry
|
|
18
|
+
ToolRegistry,
|
|
19
|
+
registry,
|
|
20
|
+
MODULE_TYPES,
|
|
21
|
+
TOOL_INTERFACE,
|
|
22
|
+
|
|
23
|
+
// ChannelAgent
|
|
24
|
+
ChannelAgent,
|
|
25
|
+
ActionRouter,
|
|
26
|
+
PROTOCOL_VERSION,
|
|
27
|
+
CHANNEL_STATE: STATE,
|
|
28
|
+
PRIORITY,
|
|
29
|
+
|
|
30
|
+
// ChannelMiddleware
|
|
31
|
+
compose,
|
|
32
|
+
requestLogger,
|
|
33
|
+
authGuard,
|
|
34
|
+
rateLimiter,
|
|
35
|
+
schemaValidator,
|
|
36
|
+
errorHandler,
|
|
37
|
+
corsHandler,
|
|
38
|
+
|
|
39
|
+
// InfrastructureGuard
|
|
40
|
+
InfrastructureGuard,
|
|
41
|
+
Probe,
|
|
42
|
+
HttpProbe,
|
|
43
|
+
DatabaseProbe,
|
|
44
|
+
MemoryProbe,
|
|
45
|
+
DiskProbe,
|
|
46
|
+
ProcessProbe,
|
|
47
|
+
HEALTH_STATE,
|
|
48
|
+
|
|
49
|
+
// NotificationCenter
|
|
50
|
+
NotificationCenter,
|
|
51
|
+
center,
|
|
52
|
+
Channel,
|
|
53
|
+
LogChannel,
|
|
54
|
+
EmailChannel,
|
|
55
|
+
WebhookChannel,
|
|
56
|
+
DingTalkChannel,
|
|
57
|
+
NOTIF_LEVELS: LEVELS,
|
|
58
|
+
|
|
59
|
+
// SchemaGatekeeper
|
|
60
|
+
SchemaGatekeeper,
|
|
61
|
+
gatekeeper,
|
|
62
|
+
GATEKEEPER_LAYERS: LAYERS,
|
|
63
|
+
SEVERITY,
|
|
64
|
+
};
|
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InfrastructureGuard - Runtime health monitoring (5 probes)
|
|
3
|
+
*
|
|
4
|
+
* Design from 通知与健康检查基础设施_完整报告.md:
|
|
5
|
+
* - 5 probes: HTTP, Database, Memory, Disk, Process
|
|
6
|
+
* - Adaptive frequency based on stability (EMA learning)
|
|
7
|
+
* - FSM: HEALTHY -> DEGRADED -> UNHEALTHY -> RECOVERING -> HEALTHY
|
|
8
|
+
* - Alerting via EventBus -> NotificationCenter
|
|
9
|
+
*
|
|
10
|
+
* This is the runtime "always-on" guard that catches production issues
|
|
11
|
+
* (failure #1 port conflicts, #4 memory leaks, #10 process crashes, etc.)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const { bus } = require('../../core/event-bus');
|
|
15
|
+
const { Logger } = require('../../core/logger');
|
|
16
|
+
const os = require('os');
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
|
|
20
|
+
const log = new Logger('InfrastructureGuard');
|
|
21
|
+
|
|
22
|
+
const HEALTH_STATE = {
|
|
23
|
+
HEALTHY: 'healthy',
|
|
24
|
+
DEGRADED: 'degraded',
|
|
25
|
+
UNHEALTHY: 'unhealthy',
|
|
26
|
+
RECOVERING: 'recovering',
|
|
27
|
+
UNKNOWN: 'unknown',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const TRANSITIONS = {
|
|
31
|
+
[HEALTH_STATE.HEALTHY]: [HEALTH_STATE.DEGRADED, HEALTH_STATE.UNHEALTHY],
|
|
32
|
+
[HEALTH_STATE.DEGRADED]: [HEALTH_STATE.HEALTHY, HEALTH_STATE.UNHEALTHY],
|
|
33
|
+
[HEALTH_STATE.UNHEALTHY]: [HEALTH_STATE.RECOVERING, HEALTH_STATE.DEGRADED],
|
|
34
|
+
[HEALTH_STATE.RECOVERING]: [HEALTH_STATE.HEALTHY, HEALTH_STATE.UNHEALTHY],
|
|
35
|
+
[HEALTH_STATE.UNKNOWN]: [HEALTH_STATE.HEALTHY, HEALTH_STATE.UNHEALTHY],
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// ============ Probes ============
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Base Probe class.
|
|
42
|
+
*/
|
|
43
|
+
class Probe {
|
|
44
|
+
constructor(name, opts = {}) {
|
|
45
|
+
this.name = name;
|
|
46
|
+
this.interval = opts.interval || 30000;
|
|
47
|
+
this.timeout = opts.timeout || 5000;
|
|
48
|
+
this.enabled = opts.enabled !== false;
|
|
49
|
+
this._timer = null;
|
|
50
|
+
this._lastResult = null;
|
|
51
|
+
this._consecutiveFailures = 0;
|
|
52
|
+
this._consecutiveSuccesses = 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Run the probe check. Override in subclass.
|
|
57
|
+
* @returns {Promise<{ healthy: boolean, details: object, latency: number }>}
|
|
58
|
+
*/
|
|
59
|
+
async check() {
|
|
60
|
+
throw new Error('Probe.check() must be overridden');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Start periodic checking.
|
|
65
|
+
*/
|
|
66
|
+
start(onResult) {
|
|
67
|
+
if (this._timer) return;
|
|
68
|
+
this.enabled = true;
|
|
69
|
+
|
|
70
|
+
const run = async () => {
|
|
71
|
+
const result = await this._safeCheck();
|
|
72
|
+
this._lastResult = result;
|
|
73
|
+
|
|
74
|
+
if (result.healthy) {
|
|
75
|
+
this._consecutiveSuccesses++;
|
|
76
|
+
this._consecutiveFailures = 0;
|
|
77
|
+
} else {
|
|
78
|
+
this._consecutiveFailures++;
|
|
79
|
+
this._consecutiveSuccesses = 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (typeof onResult === 'function') {
|
|
83
|
+
onResult(this, result);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Adaptive interval: check more frequently when unhealthy
|
|
87
|
+
const nextInterval = result.healthy ? this.interval : Math.max(this.interval / 3, 5000);
|
|
88
|
+
this._timer = setTimeout(run, nextInterval);
|
|
89
|
+
this._timer.unref?.();
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
run();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
stop() {
|
|
96
|
+
if (this._timer) {
|
|
97
|
+
clearTimeout(this._timer);
|
|
98
|
+
this._timer = null;
|
|
99
|
+
}
|
|
100
|
+
this.enabled = false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async _safeCheck() {
|
|
104
|
+
const start = Date.now();
|
|
105
|
+
try {
|
|
106
|
+
const timeoutPromise = new Promise((_, reject) =>
|
|
107
|
+
setTimeout(() => reject(new Error('Probe timeout')), this.timeout)
|
|
108
|
+
);
|
|
109
|
+
const result = await Promise.race([this.check(), timeoutPromise]);
|
|
110
|
+
return { ...result, latency: Date.now() - start };
|
|
111
|
+
} catch (err) {
|
|
112
|
+
return {
|
|
113
|
+
healthy: false,
|
|
114
|
+
details: { error: err.message },
|
|
115
|
+
latency: Date.now() - start,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
get lastResult() {
|
|
121
|
+
return this._lastResult;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
get consecutiveFailures() {
|
|
125
|
+
return this._consecutiveFailures;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
get consecutiveSuccesses() {
|
|
129
|
+
return this._consecutiveSuccesses;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* HTTP Probe — checks if an HTTP endpoint responds with 2xx.
|
|
135
|
+
*/
|
|
136
|
+
class HttpProbe extends Probe {
|
|
137
|
+
constructor(opts = {}) {
|
|
138
|
+
super('http', opts);
|
|
139
|
+
this.url = opts.url || 'http://localhost:8080/health';
|
|
140
|
+
this.expectedStatus = opts.expectedStatus || 200;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async check() {
|
|
144
|
+
const res = await fetch(this.url, {
|
|
145
|
+
method: 'GET',
|
|
146
|
+
signal: AbortSignal.timeout ? AbortSignal.timeout(this.timeout) : undefined,
|
|
147
|
+
});
|
|
148
|
+
return {
|
|
149
|
+
healthy: res.status === this.expectedStatus,
|
|
150
|
+
details: {
|
|
151
|
+
url: this.url,
|
|
152
|
+
status: res.status,
|
|
153
|
+
statusText: res.statusText,
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Database Probe — checks if a database query succeeds.
|
|
161
|
+
*/
|
|
162
|
+
class DatabaseProbe extends Probe {
|
|
163
|
+
constructor(opts = {}) {
|
|
164
|
+
super('database', opts);
|
|
165
|
+
this.queryFn = opts.queryFn || (async () => true);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async check() {
|
|
169
|
+
const result = await this.queryFn();
|
|
170
|
+
return {
|
|
171
|
+
healthy: result === true || (result && result.healthy !== false),
|
|
172
|
+
details: typeof result === 'object' ? result : { connected: result },
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Memory Probe — checks process memory usage.
|
|
179
|
+
*/
|
|
180
|
+
class MemoryProbe extends Probe {
|
|
181
|
+
constructor(opts = {}) {
|
|
182
|
+
super('memory', opts);
|
|
183
|
+
this.maxHeapMB = opts.maxHeapMB || 512; // 512MB default
|
|
184
|
+
this.maxRssMB = opts.maxRssMB || 1024; // 1GB default
|
|
185
|
+
this.warnHeapMB = opts.warnHeapMB || 384; // 75% of max
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async check() {
|
|
189
|
+
const mem = process.memoryUsage();
|
|
190
|
+
const heapMB = Math.round(mem.heapUsed / 1024 / 1024);
|
|
191
|
+
const rssMB = Math.round(mem.rss / 1024 / 1024);
|
|
192
|
+
const systemFreeMB = Math.round(os.freemem() / 1024 / 1024);
|
|
193
|
+
const systemTotalMB = Math.round(os.totalmem() / 1024 / 1024);
|
|
194
|
+
|
|
195
|
+
let healthy = true;
|
|
196
|
+
let warning = null;
|
|
197
|
+
|
|
198
|
+
if (heapMB > this.maxHeapMB) {
|
|
199
|
+
healthy = false;
|
|
200
|
+
warning = `Heap usage ${heapMB}MB exceeds limit ${this.maxHeapMB}MB`;
|
|
201
|
+
} else if (heapMB > this.warnHeapMB) {
|
|
202
|
+
warning = `Heap usage ${heapMB}MB approaching limit ${this.maxHeapMB}MB`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (rssMB > this.maxRssMB) {
|
|
206
|
+
healthy = false;
|
|
207
|
+
warning = `RSS ${rssMB}MB exceeds limit ${this.maxRssMB}MB`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
healthy,
|
|
212
|
+
details: {
|
|
213
|
+
heapUsedMB: heapMB,
|
|
214
|
+
heapLimitMB: this.maxHeapMB,
|
|
215
|
+
rssMB,
|
|
216
|
+
systemFreeMB,
|
|
217
|
+
systemTotalMB,
|
|
218
|
+
warning,
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Disk Probe — checks available disk space on a path.
|
|
226
|
+
*/
|
|
227
|
+
class DiskProbe extends Probe {
|
|
228
|
+
constructor(opts = {}) {
|
|
229
|
+
super('disk', opts);
|
|
230
|
+
this.path = opts.path || process.cwd();
|
|
231
|
+
this.minFreeMB = opts.minFreeMB || 500; // 500MB minimum
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async check() {
|
|
235
|
+
try {
|
|
236
|
+
const stats = fs.statSync(this.path);
|
|
237
|
+
// Use fs.statfs if available (Node 18.15+)
|
|
238
|
+
if (fs.statfs) {
|
|
239
|
+
const fsStats = fs.statfsSync(this.path);
|
|
240
|
+
const freeMB = Math.round(fsStats.bavail * fsStats.bsize / 1024 / 1024);
|
|
241
|
+
const totalMB = Math.round(fsStats.blocks * fsStats.bsize / 1024 / 1024);
|
|
242
|
+
return {
|
|
243
|
+
healthy: freeMB >= this.minFreeMB,
|
|
244
|
+
details: {
|
|
245
|
+
path: this.path,
|
|
246
|
+
freeMB,
|
|
247
|
+
totalMB,
|
|
248
|
+
minFreeMB: this.minFreeMB,
|
|
249
|
+
usagePercent: Math.round((1 - freeMB / totalMB) * 100),
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
// Fallback: just check the path exists
|
|
254
|
+
return {
|
|
255
|
+
healthy: true,
|
|
256
|
+
details: { path: this.path, note: 'statfs not available, existence check only' },
|
|
257
|
+
};
|
|
258
|
+
} catch (err) {
|
|
259
|
+
return {
|
|
260
|
+
healthy: false,
|
|
261
|
+
details: { path: this.path, error: err.message },
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Process Probe — checks if child processes are alive.
|
|
269
|
+
*/
|
|
270
|
+
class ProcessProbe extends Probe {
|
|
271
|
+
constructor(opts = {}) {
|
|
272
|
+
super('process', opts);
|
|
273
|
+
this.checks = opts.checks || []; // [{ name, pid | checkFn }]
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async check() {
|
|
277
|
+
const results = [];
|
|
278
|
+
let allHealthy = true;
|
|
279
|
+
|
|
280
|
+
for (const check of this.checks) {
|
|
281
|
+
try {
|
|
282
|
+
if (check.pid) {
|
|
283
|
+
// Check if process with given PID is alive
|
|
284
|
+
process.kill(check.pid, 0); // signal 0 = check existence
|
|
285
|
+
results.push({ name: check.name, pid: check.pid, alive: true });
|
|
286
|
+
} else if (typeof check.checkFn === 'function') {
|
|
287
|
+
const alive = await check.checkFn();
|
|
288
|
+
results.push({ name: check.name, alive });
|
|
289
|
+
if (!alive) allHealthy = false;
|
|
290
|
+
}
|
|
291
|
+
} catch (err) {
|
|
292
|
+
results.push({ name: check.name, pid: check.pid, alive: false, error: err.message });
|
|
293
|
+
allHealthy = false;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
healthy: allHealthy,
|
|
299
|
+
details: { processes: results },
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ============ InfrastructureGuard ============
|
|
305
|
+
|
|
306
|
+
class InfrastructureGuard {
|
|
307
|
+
constructor(opts = {}) {
|
|
308
|
+
this._probes = new Map();
|
|
309
|
+
this._state = HEALTH_STATE.UNKNOWN;
|
|
310
|
+
this._listeners = [];
|
|
311
|
+
this._history = [];
|
|
312
|
+
this._maxHistory = 100;
|
|
313
|
+
this._checkInterval = opts.checkInterval || 30000;
|
|
314
|
+
|
|
315
|
+
// Learning engine — adaptive probe frequency
|
|
316
|
+
this._learning = {
|
|
317
|
+
totalChecks: 0,
|
|
318
|
+
totalFailures: 0,
|
|
319
|
+
stability: 1.0,
|
|
320
|
+
lastHealthyAt: Date.now(),
|
|
321
|
+
|
|
322
|
+
record(success) {
|
|
323
|
+
this.totalChecks++;
|
|
324
|
+
if (!success) this.totalFailures++;
|
|
325
|
+
if (success) this.lastHealthyAt = Date.now();
|
|
326
|
+
const total = Math.max(this.totalChecks, 1);
|
|
327
|
+
this.stability = 1 - (this.totalFailures / total);
|
|
328
|
+
},
|
|
329
|
+
|
|
330
|
+
getAdaptiveInterval() {
|
|
331
|
+
if (this.stability > 0.95) return 45000; // Very stable → 45s
|
|
332
|
+
if (this.stability > 0.85) return 30000; // Stable → 30s
|
|
333
|
+
if (this.stability > 0.6) return 20000; // Fair → 20s
|
|
334
|
+
return 10000; // Unstable → 10s
|
|
335
|
+
},
|
|
336
|
+
|
|
337
|
+
getReport() {
|
|
338
|
+
return {
|
|
339
|
+
totalChecks: this.totalChecks,
|
|
340
|
+
totalFailures: this.totalFailures,
|
|
341
|
+
stability: Math.round(this.stability * 100) / 100,
|
|
342
|
+
adaptiveIntervalMs: this.getAdaptiveInterval(),
|
|
343
|
+
lastHealthyAt: this.lastHealthyAt,
|
|
344
|
+
};
|
|
345
|
+
},
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Register a probe.
|
|
351
|
+
*/
|
|
352
|
+
registerProbe(probe) {
|
|
353
|
+
if (!(probe instanceof Probe)) {
|
|
354
|
+
throw new Error('Must be a Probe instance');
|
|
355
|
+
}
|
|
356
|
+
this._probes.set(probe.name, probe);
|
|
357
|
+
log.info(`探针已注册:${probe.name}(间隔:${probe.interval}ms)`);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Add an HTTP probe.
|
|
362
|
+
*/
|
|
363
|
+
addHttpProbe(opts) {
|
|
364
|
+
this.registerProbe(new HttpProbe(opts));
|
|
365
|
+
return this;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Add a database probe.
|
|
370
|
+
*/
|
|
371
|
+
addDatabaseProbe(opts) {
|
|
372
|
+
this.registerProbe(new DatabaseProbe(opts));
|
|
373
|
+
return this;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Add a memory probe.
|
|
378
|
+
*/
|
|
379
|
+
addMemoryProbe(opts) {
|
|
380
|
+
this.registerProbe(new MemoryProbe(opts));
|
|
381
|
+
return this;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Add a disk probe.
|
|
386
|
+
*/
|
|
387
|
+
addDiskProbe(opts) {
|
|
388
|
+
this.registerProbe(new DiskProbe(opts));
|
|
389
|
+
return this;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Add a process probe.
|
|
394
|
+
*/
|
|
395
|
+
addProcessProbe(opts) {
|
|
396
|
+
this.registerProbe(new ProcessProbe(opts));
|
|
397
|
+
return this;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Start all probes.
|
|
402
|
+
*/
|
|
403
|
+
start() {
|
|
404
|
+
for (const probe of this._probes.values()) {
|
|
405
|
+
probe.start((p, result) => this._onProbeResult(p, result));
|
|
406
|
+
}
|
|
407
|
+
this._transition(HEALTH_STATE.HEALTHY);
|
|
408
|
+
log.info(`InfrastructureGuard 已启动,共 ${this._probes.size} 个探针`);
|
|
409
|
+
bus.emit('guard:started', { probes: Array.from(this._probes.keys()) });
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Stop all probes.
|
|
414
|
+
*/
|
|
415
|
+
stop() {
|
|
416
|
+
for (const probe of this._probes.values()) {
|
|
417
|
+
probe.stop();
|
|
418
|
+
}
|
|
419
|
+
log.info('InfrastructureGuard 已停止');
|
|
420
|
+
bus.emit('guard:stopped', {});
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Run a single health check across all probes.
|
|
425
|
+
*/
|
|
426
|
+
async checkNow() {
|
|
427
|
+
const results = {};
|
|
428
|
+
let allHealthy = true;
|
|
429
|
+
let anyDegraded = false;
|
|
430
|
+
|
|
431
|
+
for (const [name, probe] of this._probes) {
|
|
432
|
+
const result = await probe._safeCheck();
|
|
433
|
+
probe._lastResult = result;
|
|
434
|
+
results[name] = result;
|
|
435
|
+
if (!result.healthy) {
|
|
436
|
+
allHealthy = false;
|
|
437
|
+
if (probe.consecutiveFailures < 3) {
|
|
438
|
+
anyDegraded = true;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const newState = allHealthy ? HEALTH_STATE.HEALTHY
|
|
444
|
+
: anyDegraded ? HEALTH_STATE.DEGRADED
|
|
445
|
+
: HEALTH_STATE.UNHEALTHY;
|
|
446
|
+
|
|
447
|
+
if (newState !== this._state) {
|
|
448
|
+
this._transition(newState);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
this._learning.record(allHealthy);
|
|
452
|
+
|
|
453
|
+
return {
|
|
454
|
+
state: this._state,
|
|
455
|
+
healthy: allHealthy,
|
|
456
|
+
probes: results,
|
|
457
|
+
learning: this._learning.getReport(),
|
|
458
|
+
timestamp: Date.now(),
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Get current health status.
|
|
464
|
+
*/
|
|
465
|
+
getStatus() {
|
|
466
|
+
const probes = {};
|
|
467
|
+
for (const [name, probe] of this._probes) {
|
|
468
|
+
probes[name] = {
|
|
469
|
+
enabled: probe.enabled,
|
|
470
|
+
lastResult: probe.lastResult,
|
|
471
|
+
consecutiveFailures: probe.consecutiveFailures,
|
|
472
|
+
consecutiveSuccesses: probe.consecutiveSuccesses,
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
return {
|
|
476
|
+
state: this._state,
|
|
477
|
+
probes,
|
|
478
|
+
learning: this._learning.getReport(),
|
|
479
|
+
uptime: process.uptime(),
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Handle individual probe results.
|
|
485
|
+
*/
|
|
486
|
+
_onProbeResult(probe, result) {
|
|
487
|
+
this._learning.record(result.healthy);
|
|
488
|
+
|
|
489
|
+
// Record in history
|
|
490
|
+
this._history.push({
|
|
491
|
+
probe: probe.name,
|
|
492
|
+
healthy: result.healthy,
|
|
493
|
+
latency: result.latency,
|
|
494
|
+
details: result.details,
|
|
495
|
+
ts: Date.now(),
|
|
496
|
+
});
|
|
497
|
+
if (this._history.length > this._maxHistory) {
|
|
498
|
+
this._history.shift();
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Emit event
|
|
502
|
+
bus.emit('guard:probe', {
|
|
503
|
+
probe: probe.name,
|
|
504
|
+
healthy: result.healthy,
|
|
505
|
+
latency: result.latency,
|
|
506
|
+
details: result.details,
|
|
507
|
+
consecutiveFailures: probe.consecutiveFailures,
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
// State transition logic
|
|
511
|
+
if (!result.healthy && probe.consecutiveFailures >= 3) {
|
|
512
|
+
if (this._state === HEALTH_STATE.HEALTHY) {
|
|
513
|
+
this._transition(HEALTH_STATE.DEGRADED);
|
|
514
|
+
} else if (this._state === HEALTH_STATE.DEGRADED && probe.consecutiveFailures >= 5) {
|
|
515
|
+
this._transition(HEALTH_STATE.UNHEALTHY);
|
|
516
|
+
}
|
|
517
|
+
} else if (result.healthy && probe.consecutiveSuccesses >= 3) {
|
|
518
|
+
if (this._state === HEALTH_STATE.UNHEALTHY) {
|
|
519
|
+
this._transition(HEALTH_STATE.RECOVERING);
|
|
520
|
+
} else if (this._state === HEALTH_STATE.RECOVERING || this._state === HEALTH_STATE.DEGRADED) {
|
|
521
|
+
// Check if all probes are now healthy
|
|
522
|
+
const allHealthy = Array.from(this._probes.values())
|
|
523
|
+
.every(p => !p.lastResult || p.lastResult.healthy);
|
|
524
|
+
if (allHealthy) {
|
|
525
|
+
this._transition(HEALTH_STATE.HEALTHY);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Emit alert if unhealthy
|
|
531
|
+
if (!result.healthy && probe.consecutiveFailures === 1) {
|
|
532
|
+
bus.emit('guard:alert', {
|
|
533
|
+
probe: probe.name,
|
|
534
|
+
severity: probe.consecutiveFailures >= 5 ? 'critical' : 'warning',
|
|
535
|
+
message: `Probe '${probe.name}' is unhealthy: ${JSON.stringify(result.details)}`,
|
|
536
|
+
details: result.details,
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
_transition(newState) {
|
|
542
|
+
if (this._state === newState) return;
|
|
543
|
+
if (!TRANSITIONS[this._state]?.includes(newState)) return;
|
|
544
|
+
|
|
545
|
+
const old = this._state;
|
|
546
|
+
this._state = newState;
|
|
547
|
+
log.info(`健康状态转换:${old} -> ${newState}`);
|
|
548
|
+
|
|
549
|
+
this._listeners.forEach(fn => {
|
|
550
|
+
try { fn(newState, old); } catch (_) {}
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
bus.emit('guard:state_change', { from: old, to: newState });
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
onStateChange(fn) {
|
|
557
|
+
this._listeners.push(fn);
|
|
558
|
+
return () => {
|
|
559
|
+
this._listeners = this._listeners.filter(f => f !== fn);
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
getHistory(limit) {
|
|
564
|
+
if (limit) return this._history.slice(-limit);
|
|
565
|
+
return this._history.slice();
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
get state() {
|
|
569
|
+
return this._state;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
module.exports = {
|
|
574
|
+
InfrastructureGuard,
|
|
575
|
+
Probe,
|
|
576
|
+
HttpProbe,
|
|
577
|
+
DatabaseProbe,
|
|
578
|
+
MemoryProbe,
|
|
579
|
+
DiskProbe,
|
|
580
|
+
ProcessProbe,
|
|
581
|
+
HEALTH_STATE,
|
|
582
|
+
};
|