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,443 @@
1
+ /**
2
+ * NotificationCenter - Multi-channel notification system
3
+ *
4
+ * Design from 通知与健康检查基础设施_完整报告.md:
5
+ * - 4 channels: log, email, webhook, dingtalk
6
+ * - Priority-based routing (INFO, WARN, ERROR, CRITICAL)
7
+ * - Rate limiting per channel (prevents alert storms)
8
+ * - Deduplication by content signature
9
+ *
10
+ * Integrates with InfrastructureGuard via EventBus:
11
+ * guard:alert -> NotificationCenter.send()
12
+ */
13
+
14
+ const { bus } = require('../../core/event-bus');
15
+ const { Logger } = require('../../core/logger');
16
+
17
+ const log = new Logger('NotificationCenter');
18
+
19
+ const LEVELS = {
20
+ INFO: { value: 0, label: 'INFO', color: '\x1b[36m' },
21
+ WARN: { value: 1, label: 'WARN', color: '\x1b[33m' },
22
+ ERROR: { value: 2, label: 'ERROR', color: '\x1b[31m' },
23
+ CRITICAL: { value: 3, label: 'CRITICAL', color: '\x1b[35m' },
24
+ };
25
+
26
+ // ============ Channels ============
27
+
28
+ /**
29
+ * Base Channel class.
30
+ */
31
+ class Channel {
32
+ constructor(name, opts = {}) {
33
+ this.name = name;
34
+ this.minLevel = opts.minLevel || LEVELS.INFO;
35
+ this.rateLimitWindow = opts.rateLimitWindow || 60 * 1000; // 1 min
36
+ this.rateLimitMax = opts.rateLimitMax || 50; // 50 per window
37
+ this.enabled = opts.enabled !== false;
38
+
39
+ this._sentCount = 0;
40
+ this._rateBuckets = new Map(); // signature -> timestamps[]
41
+ }
42
+
43
+ /**
44
+ * Send a notification. Override in subclass.
45
+ */
46
+ async send(notification) {
47
+ throw new Error('Channel.send() must be overridden');
48
+ }
49
+
50
+ /**
51
+ * Check rate limit for a given signature.
52
+ */
53
+ _checkRateLimit(signature) {
54
+ const now = Date.now();
55
+ const bucket = this._rateBuckets.get(signature) || [];
56
+ const valid = bucket.filter(t => now - t < this.rateLimitWindow);
57
+
58
+ if (valid.length >= this.rateLimitMax) {
59
+ return false; // Rate limited
60
+ }
61
+
62
+ valid.push(now);
63
+ this._rateBuckets.set(signature, valid);
64
+ return true;
65
+ }
66
+
67
+ /**
68
+ * Clean up old rate limit entries.
69
+ */
70
+ _cleanup() {
71
+ const now = Date.now();
72
+ for (const [sig, times] of this._rateBuckets) {
73
+ const valid = times.filter(t => now - t < this.rateLimitWindow);
74
+ if (valid.length === 0) {
75
+ this._rateBuckets.delete(sig);
76
+ } else {
77
+ this._rateBuckets.set(sig, valid);
78
+ }
79
+ }
80
+ }
81
+
82
+ get stats() {
83
+ return { sent: this._sentCount, enabled: this.enabled };
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Log Channel — writes to console/file logger.
89
+ */
90
+ class LogChannel extends Channel {
91
+ constructor(opts = {}) {
92
+ super('log', { minLevel: LEVELS.INFO, rateLimitMax: 200, ...opts });
93
+ }
94
+
95
+ async send(notification) {
96
+ const level = LEVELS[notification.level] || LEVELS.INFO;
97
+ const color = level.color;
98
+ const reset = '\x1b[0m';
99
+ const ts = new Date(notification.timestamp).toISOString();
100
+
101
+ const line = `${color}[${ts}] ${level.label}${reset} [${notification.source}] ${notification.title}`;
102
+
103
+ if (notification.data) {
104
+ console.log(line);
105
+ console.log(` Details: ${JSON.stringify(notification.data, null, 2).split('\n').join('\n ')}`);
106
+ } else {
107
+ console.log(line);
108
+ }
109
+
110
+ if (notification.message) {
111
+ console.log(` ${notification.message}`);
112
+ }
113
+
114
+ this._sentCount++;
115
+ return true;
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Email Channel — sends email notifications (stub — implement with nodemailer).
121
+ */
122
+ class EmailChannel extends Channel {
123
+ constructor(opts = {}) {
124
+ super('email', { minLevel: LEVELS.ERROR, rateLimitMax: 20, ...opts });
125
+ this.to = opts.to || [];
126
+ this.from = opts.from || 'devassist@localhost';
127
+ this.smtpConfig = opts.smtpConfig || null;
128
+ }
129
+
130
+ async send(notification) {
131
+ if (!this.smtpConfig || this.to.length === 0) {
132
+ log.debug('Email channel not configured, skipping');
133
+ return false;
134
+ }
135
+
136
+ // In production, use nodemailer here:
137
+ // const transporter = nodemailer.createTransport(this.smtpConfig);
138
+ // await transporter.sendMail({ from, to, subject, text });
139
+
140
+ log.info(`[EMAIL] 收件人:${this.to.join(',')} | 主题:[${notification.level}] ${notification.title}`);
141
+ this._sentCount++;
142
+ return true;
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Webhook Channel — sends HTTP POST to a URL.
148
+ */
149
+ class WebhookChannel extends Channel {
150
+ constructor(opts = {}) {
151
+ super('webhook', { minLevel: LEVELS.WARN, rateLimitMax: 30, ...opts });
152
+ this.url = opts.url || null;
153
+ this.headers = opts.headers || { 'Content-Type': 'application/json' };
154
+ this.timeout = opts.timeout || 5000;
155
+ }
156
+
157
+ async send(notification) {
158
+ if (!this.url) {
159
+ log.debug('Webhook URL not configured, skipping');
160
+ return false;
161
+ }
162
+
163
+ const body = JSON.stringify({
164
+ level: notification.level,
165
+ title: notification.title,
166
+ message: notification.message,
167
+ source: notification.source,
168
+ data: notification.data,
169
+ timestamp: notification.timestamp,
170
+ });
171
+
172
+ try {
173
+ const res = await fetch(this.url, {
174
+ method: 'POST',
175
+ headers: this.headers,
176
+ body,
177
+ signal: AbortSignal.timeout ? AbortSignal.timeout(this.timeout) : undefined,
178
+ });
179
+
180
+ if (!res.ok) {
181
+ log.warn(`Webhook 返回 ${res.status}:${res.statusText}`);
182
+ return false;
183
+ }
184
+
185
+ this._sentCount++;
186
+ return true;
187
+ } catch (err) {
188
+ log.error(`Webhook 发送失败:${err.message}`);
189
+ return false;
190
+ }
191
+ }
192
+ }
193
+
194
+ /**
195
+ * DingTalk Channel — sends to DingTalk group robot.
196
+ */
197
+ class DingTalkChannel extends Channel {
198
+ constructor(opts = {}) {
199
+ super('dingtalk', { minLevel: LEVELS.WARN, rateLimitMax: 20, ...opts });
200
+ this.webhookUrl = opts.webhookUrl || null;
201
+ this.secret = opts.secret || null; // Sign key for security
202
+ }
203
+
204
+ async send(notification) {
205
+ if (!this.webhookUrl) {
206
+ log.debug('DingTalk webhook not configured, skipping');
207
+ return false;
208
+ }
209
+
210
+ const level = LEVELS[notification.level] || LEVELS.INFO;
211
+ const title = `[${level.label}] ${notification.title}`;
212
+ let text = `### ${title}\n\n`;
213
+ text += `**Source:** ${notification.source}\n\n`;
214
+ if (notification.message) {
215
+ text += `**Message:** ${notification.message}\n\n`;
216
+ }
217
+ if (notification.data) {
218
+ text += `**Details:**\n\n\`\`\`json\n${JSON.stringify(notification.data, null, 2)}\n\`\`\`\n`;
219
+ }
220
+ text += `**Time:** ${new Date(notification.timestamp).toLocaleString('zh-CN')}`;
221
+
222
+ const body = {
223
+ msgtype: 'markdown',
224
+ markdown: { title, text },
225
+ };
226
+
227
+ // Sign the request if secret is configured
228
+ let url = this.webhookUrl;
229
+ if (this.secret) {
230
+ const timestamp = Date.now();
231
+ const stringToSign = `${timestamp}\n${this.secret}`;
232
+ const crypto = require('crypto');
233
+ const sign = crypto
234
+ .createHmac('sha256', this.secret)
235
+ .update(stringToSign)
236
+ .digest('base64');
237
+ url += `&timestamp=${timestamp}&sign=${encodeURIComponent(sign)}`;
238
+ }
239
+
240
+ try {
241
+ const res = await fetch(url, {
242
+ method: 'POST',
243
+ headers: { 'Content-Type': 'application/json' },
244
+ body: JSON.stringify(body),
245
+ signal: AbortSignal.timeout ? AbortSignal.timeout(5000) : undefined,
246
+ });
247
+
248
+ const j = await res.json().catch(() => ({}));
249
+ if (j.errcode && j.errcode !== 0) {
250
+ log.warn(`钉钉错误:${j.errmsg}`);
251
+ return false;
252
+ }
253
+
254
+ this._sentCount++;
255
+ return true;
256
+ } catch (err) {
257
+ log.error(`钉钉发送失败:${err.message}`);
258
+ return false;
259
+ }
260
+ }
261
+ }
262
+
263
+ // ============ NotificationCenter ============
264
+
265
+ class NotificationCenter {
266
+ constructor() {
267
+ this._channels = new Map();
268
+ this._dedupCache = new Map(); // signature -> last sent timestamp
269
+ this._dedupTTL = 5 * 60 * 1000; // 5 minutes
270
+ this._queue = [];
271
+ this._processing = false;
272
+ this._stats = { totalSent: 0, totalDeduped: 0, totalRateLimited: 0 };
273
+
274
+ // Register default log channel
275
+ this.registerChannel(new LogChannel());
276
+
277
+ // Auto-subscribe to guard:alert events
278
+ bus.on('guard:alert', async (alert) => {
279
+ await this.send({
280
+ level: alert.severity === 'critical' ? 'CRITICAL' : 'WARN',
281
+ title: `Health Alert: ${alert.probe}`,
282
+ message: alert.message,
283
+ source: 'InfrastructureGuard',
284
+ data: alert.details,
285
+ timestamp: Date.now(),
286
+ });
287
+ });
288
+ }
289
+
290
+ /**
291
+ * Register a notification channel.
292
+ */
293
+ registerChannel(channel) {
294
+ if (!(channel instanceof Channel)) {
295
+ throw new Error('Must be a Channel instance');
296
+ }
297
+ this._channels.set(channel.name, channel);
298
+ log.info(`通道已注册:${channel.name}(最低级别:${channel.minLevel.label})`);
299
+ }
300
+
301
+ /**
302
+ * Remove a channel.
303
+ */
304
+ unregisterChannel(name) {
305
+ this._channels.delete(name);
306
+ }
307
+
308
+ /**
309
+ * Send a notification to all eligible channels.
310
+ *
311
+ * @param {object} notification - { level, title, message, source, data, timestamp }
312
+ */
313
+ async send(notification) {
314
+ // Normalize
315
+ const level = typeof notification.level === 'string'
316
+ ? LEVELS[notification.level]
317
+ : notification.level;
318
+
319
+ if (!level) {
320
+ log.warn(`未知通知级别:${notification.level}`);
321
+ return;
322
+ }
323
+
324
+ notification = {
325
+ ...notification,
326
+ level: level.label,
327
+ levelValue: level.value,
328
+ timestamp: notification.timestamp || Date.now(),
329
+ };
330
+
331
+ // Deduplication — skip if same content was sent recently
332
+ const signature = this._makeSignature(notification);
333
+ const lastSent = this._dedupCache.get(signature);
334
+ if (lastSent && Date.now() - lastSent < this._dedupTTL) {
335
+ this._stats.totalDeduped++;
336
+ return;
337
+ }
338
+
339
+ // Send to all eligible channels
340
+ const promises = [];
341
+ for (const channel of this._channels.values()) {
342
+ if (!channel.enabled) continue;
343
+ if (level.value < channel.minLevel.value) continue;
344
+
345
+ // Rate limit check
346
+ if (!channel._checkRateLimit(signature)) {
347
+ this._stats.totalRateLimited++;
348
+ log.warn(`通道 '${channel.name}' 被速率限制:${notification.title}`);
349
+ continue;
350
+ }
351
+
352
+ promises.push(
353
+ channel.send(notification).catch(err => {
354
+ log.error(`通道 '${channel.name}' 发送错误:${err.message}`);
355
+ })
356
+ );
357
+ }
358
+
359
+ await Promise.all(promises);
360
+
361
+ // Update dedup cache
362
+ this._dedupCache.set(signature, notification.timestamp);
363
+ this._stats.totalSent++;
364
+
365
+ // Emit event for monitoring
366
+ bus.emit('notification:sent', {
367
+ level: notification.level,
368
+ title: notification.title,
369
+ source: notification.source,
370
+ channels: Array.from(this._channels.keys()),
371
+ });
372
+ }
373
+
374
+ /**
375
+ * Convenience methods for each level.
376
+ */
377
+ async info(title, message, source = 'system', data = null) {
378
+ return this.send({ level: 'INFO', title, message, source, data });
379
+ }
380
+
381
+ async warn(title, message, source = 'system', data = null) {
382
+ return this.send({ level: 'WARN', title, message, source, data });
383
+ }
384
+
385
+ async error(title, message, source = 'system', data = null) {
386
+ return this.send({ level: 'ERROR', title, message, source, data });
387
+ }
388
+
389
+ async critical(title, message, source = 'system', data = null) {
390
+ return this.send({ level: 'CRITICAL', title, message, source, data });
391
+ }
392
+
393
+ /**
394
+ * Create a dedup signature from notification content.
395
+ */
396
+ _makeSignature(notification) {
397
+ const parts = [
398
+ notification.level,
399
+ notification.title,
400
+ notification.source,
401
+ notification.data ? JSON.stringify(notification.data).substring(0, 200) : '',
402
+ ];
403
+ return parts.join('|');
404
+ }
405
+
406
+ /**
407
+ * Clean up old dedup entries.
408
+ */
409
+ cleanup() {
410
+ const now = Date.now();
411
+ for (const [sig, ts] of this._dedupCache) {
412
+ if (now - ts > this._dedupTTL) {
413
+ this._dedupCache.delete(sig);
414
+ }
415
+ }
416
+ for (const channel of this._channels.values()) {
417
+ channel._cleanup();
418
+ }
419
+ }
420
+
421
+ /**
422
+ * Get stats.
423
+ */
424
+ getStats() {
425
+ const channels = {};
426
+ for (const [name, channel] of this._channels) {
427
+ channels[name] = channel.stats;
428
+ }
429
+ return { ...this._stats, channels };
430
+ }
431
+
432
+ /**
433
+ * Get registered channels.
434
+ */
435
+ getChannels() {
436
+ return Array.from(this._channels.keys());
437
+ }
438
+ }
439
+
440
+ // Singleton
441
+ const center = new NotificationCenter();
442
+
443
+ module.exports = { NotificationCenter, center, Channel, LogChannel, EmailChannel, WebhookChannel, DingTalkChannel, LEVELS };