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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +467 -0
  3. package/bin/devassist.js +220 -0
  4. package/package.json +44 -0
  5. package/src/ai/adapter.js +464 -0
  6. package/src/ai/providers/claude.js +80 -0
  7. package/src/ai/providers/hunyuan.js +87 -0
  8. package/src/ai/providers/openai.js +74 -0
  9. package/src/ai/providers/qwen.js +81 -0
  10. package/src/cli/commands/ai.js +944 -0
  11. package/src/cli/commands/ask.js +79 -0
  12. package/src/cli/commands/backup.js +30 -0
  13. package/src/cli/commands/check.js +327 -0
  14. package/src/cli/commands/clean.js +130 -0
  15. package/src/cli/commands/comparison-report.js +326 -0
  16. package/src/cli/commands/convention.js +91 -0
  17. package/src/cli/commands/debt.js +49 -0
  18. package/src/cli/commands/deploy.js +88 -0
  19. package/src/cli/commands/diff.js +193 -0
  20. package/src/cli/commands/doctor.js +186 -0
  21. package/src/cli/commands/fix.js +195 -0
  22. package/src/cli/commands/init.js +431 -0
  23. package/src/cli/commands/inject.js +254 -0
  24. package/src/cli/commands/report.js +310 -0
  25. package/src/cli/commands/restore.js +78 -0
  26. package/src/cli/commands/schema.js +93 -0
  27. package/src/cli/commands/watch.js +212 -0
  28. package/src/cli/shared/code-context.js +51 -0
  29. package/src/cli/shared/config-loader.js +89 -0
  30. package/src/cli/shared/file-collector.js +116 -0
  31. package/src/cli/shared/inline-ignore.js +142 -0
  32. package/src/cli/shared/watch-list.js +281 -0
  33. package/src/core/event-bus.js +83 -0
  34. package/src/core/fsm.js +103 -0
  35. package/src/core/logger.js +54 -0
  36. package/src/core/rule-engine.js +117 -0
  37. package/src/index.js +64 -0
  38. package/src/modules/dev-time/arch-risk-assessor.js +250 -0
  39. package/src/modules/dev-time/code-quality-guard.js +1340 -0
  40. package/src/modules/dev-time/convention-store.js +201 -0
  41. package/src/modules/dev-time/impact-analyzer.js +292 -0
  42. package/src/modules/dev-time/pre-deploy-guard.js +284 -0
  43. package/src/modules/dev-time/schema-registry.js +284 -0
  44. package/src/modules/dev-time/tech-debt-tracker.js +225 -0
  45. package/src/modules/dev-time/version-manager.js +280 -0
  46. package/src/modules/runtime/channel-agent.js +404 -0
  47. package/src/modules/runtime/channel-middleware.js +316 -0
  48. package/src/modules/runtime/index.js +64 -0
  49. package/src/modules/runtime/infrastructure-guard.js +582 -0
  50. package/src/modules/runtime/notification-center.js +443 -0
  51. package/src/modules/runtime/schema-gatekeeper.js +664 -0
  52. package/src/modules/runtime/tool-registry.js +329 -0
  53. package/templates/ci/github-actions.yml +60 -0
  54. package/templates/ci/gitlab-ci.yml +30 -0
  55. package/templates/ci/pre-commit-hook.sh +18 -0
  56. package/templates/git-hooks/pre-commit +61 -0
  57. package/tests/run-all.js +434 -0
  58. package/tests/test-layer2.js +461 -0
  59. 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
+ };