autosnippet 2.5.0 → 2.6.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/bin/cli.js +35 -0
- package/dashboard/dist/assets/{icons-Dtm0E6DS.js → icons-rnn04CvH.js} +89 -84
- package/dashboard/dist/assets/index-BBKa3Dgi.js +195 -0
- package/dashboard/dist/assets/index-DLsECfzW.css +1 -0
- package/dashboard/dist/index.html +3 -3
- package/lib/core/gateway/Gateway.js +19 -4
- package/lib/external/ai/AiProvider.js +47 -3
- package/lib/external/mcp/McpServer.js +2 -1
- package/lib/external/mcp/handlers/skill.js +76 -18
- package/lib/external/mcp/tools.js +21 -0
- package/lib/http/routes/search.js +5 -3
- package/lib/http/routes/skills.js +38 -3
- package/lib/infrastructure/audit/AuditStore.js +18 -0
- package/lib/injection/ServiceContainer.js +8 -2
- package/lib/service/chat/ChatAgent.js +281 -33
- package/lib/service/chat/ConversationStore.js +377 -0
- package/lib/service/chat/Memory.js +40 -10
- package/lib/service/chat/tools.js +104 -7
- package/lib/service/skills/EventAggregator.js +187 -0
- package/lib/service/skills/SignalCollector.js +524 -0
- package/lib/service/skills/SkillAdvisor.js +323 -0
- package/package.json +1 -1
- package/dashboard/dist/assets/index-B7VpZOCz.css +0 -1
- package/dashboard/dist/assets/index-D87IZTmZ.js +0 -187
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EventAggregator — 信号聚类引擎(受 Continue EditAggregator 启发)
|
|
3
|
+
*
|
|
4
|
+
* 在短时间窗口内将多个同类事件聚合为一个 batch 事件,避免:
|
|
5
|
+
* 1. SignalCollector 对相同类型的信号重复触发 AI 分析
|
|
6
|
+
* 2. 高频操作(如连续文件保存)产生大量冗余事件
|
|
7
|
+
* 3. 多个 Guard 违规在同一编辑中重复推送
|
|
8
|
+
*
|
|
9
|
+
* 聚合策略:
|
|
10
|
+
* 时间窗口 — 同 key 事件在 windowMs 内合并为一个 batch
|
|
11
|
+
* 空间聚类 — 支持自定义 key 提取函数(如按文件路径/违规规则分组)
|
|
12
|
+
* 去重 —— 已处理的事件在 dedupeWindowMs 内不重复触发
|
|
13
|
+
*
|
|
14
|
+
* 用法:
|
|
15
|
+
* const agg = new EventAggregator({ windowMs: 5000 });
|
|
16
|
+
* agg.on('batch', (key, events) => { ... });
|
|
17
|
+
* agg.push('file_change', { filePath: 'a.js' });
|
|
18
|
+
* // 5 秒内的多次 push 会合并为一次 batch 回调
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import Logger from '../../infrastructure/logging/Logger.js';
|
|
22
|
+
|
|
23
|
+
const DEFAULT_WINDOW_MS = 5000; // 5 秒聚合窗口
|
|
24
|
+
const DEFAULT_MAX_BATCH = 50; // 单次 batch 最大事件数
|
|
25
|
+
const DEFAULT_DEDUPE_MS = 60_000; // 60 秒去重窗口
|
|
26
|
+
|
|
27
|
+
export class EventAggregator {
|
|
28
|
+
/** @type {Map<string, { events: any[], timer: ReturnType<typeof setTimeout> }>} */
|
|
29
|
+
#buckets = new Map();
|
|
30
|
+
/** @type {Map<string, number>} 已处理事件的 hash → 最后处理时间 */
|
|
31
|
+
#dedupeMap = new Map();
|
|
32
|
+
#listeners = new Map();
|
|
33
|
+
#windowMs;
|
|
34
|
+
#maxBatch;
|
|
35
|
+
#dedupeMs;
|
|
36
|
+
#logger;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {object} [opts]
|
|
40
|
+
* @param {number} [opts.windowMs=5000] — 聚合时间窗口(毫秒)
|
|
41
|
+
* @param {number} [opts.maxBatch=50] — 单次 batch 最大事件数
|
|
42
|
+
* @param {number} [opts.dedupeMs=60000] — 相同事件去重窗口(毫秒)
|
|
43
|
+
*/
|
|
44
|
+
constructor({
|
|
45
|
+
windowMs = DEFAULT_WINDOW_MS,
|
|
46
|
+
maxBatch = DEFAULT_MAX_BATCH,
|
|
47
|
+
dedupeMs = DEFAULT_DEDUPE_MS,
|
|
48
|
+
} = {}) {
|
|
49
|
+
this.#windowMs = windowMs;
|
|
50
|
+
this.#maxBatch = maxBatch;
|
|
51
|
+
this.#dedupeMs = dedupeMs;
|
|
52
|
+
this.#logger = Logger.getInstance();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 推送一个事件到聚合器
|
|
57
|
+
* @param {string} key — 聚合键(如 'file_change', 'guard_violation')
|
|
58
|
+
* @param {object} event — 事件数据
|
|
59
|
+
* @param {object} [opts]
|
|
60
|
+
* @param {string} [opts.dedupeId] — 去重标识(默认为 JSON hash)
|
|
61
|
+
*/
|
|
62
|
+
push(key, event, { dedupeId } = {}) {
|
|
63
|
+
// 去重检查
|
|
64
|
+
const dedupe = dedupeId || this.#hashEvent(key, event);
|
|
65
|
+
const lastSeen = this.#dedupeMap.get(dedupe);
|
|
66
|
+
if (lastSeen && (Date.now() - lastSeen) < this.#dedupeMs) {
|
|
67
|
+
this.#logger.debug(`[EventAggregator] dedup skip: ${key}/${dedupe}`);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let bucket = this.#buckets.get(key);
|
|
72
|
+
if (!bucket) {
|
|
73
|
+
bucket = { events: [], timer: null };
|
|
74
|
+
this.#buckets.set(key, bucket);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
bucket.events.push({ ...event, _ts: Date.now() });
|
|
78
|
+
|
|
79
|
+
// 达到最大 batch 立即触发
|
|
80
|
+
if (bucket.events.length >= this.#maxBatch) {
|
|
81
|
+
this.#flush(key);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 重置窗口计时器
|
|
86
|
+
if (bucket.timer) clearTimeout(bucket.timer);
|
|
87
|
+
bucket.timer = setTimeout(() => this.#flush(key), this.#windowMs);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 注册 batch 事件监听器
|
|
92
|
+
* @param {'batch'} eventName
|
|
93
|
+
* @param {(key: string, events: any[]) => void} fn
|
|
94
|
+
*/
|
|
95
|
+
on(eventName, fn) {
|
|
96
|
+
if (!this.#listeners.has(eventName)) {
|
|
97
|
+
this.#listeners.set(eventName, []);
|
|
98
|
+
}
|
|
99
|
+
this.#listeners.get(eventName).push(fn);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 立即刷新所有待处理 bucket
|
|
104
|
+
*/
|
|
105
|
+
flushAll() {
|
|
106
|
+
for (const key of this.#buckets.keys()) {
|
|
107
|
+
this.#flush(key);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 停止所有计时器
|
|
113
|
+
*/
|
|
114
|
+
destroy() {
|
|
115
|
+
for (const [, bucket] of this.#buckets) {
|
|
116
|
+
if (bucket.timer) clearTimeout(bucket.timer);
|
|
117
|
+
}
|
|
118
|
+
this.#buckets.clear();
|
|
119
|
+
this.#dedupeMap.clear();
|
|
120
|
+
this.#listeners.clear();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 获取待处理事件数
|
|
125
|
+
*/
|
|
126
|
+
get pendingCount() {
|
|
127
|
+
let count = 0;
|
|
128
|
+
for (const [, bucket] of this.#buckets) count += bucket.events.length;
|
|
129
|
+
return count;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── 内部方法 ──
|
|
133
|
+
|
|
134
|
+
#flush(key) {
|
|
135
|
+
const bucket = this.#buckets.get(key);
|
|
136
|
+
if (!bucket || bucket.events.length === 0) return;
|
|
137
|
+
|
|
138
|
+
if (bucket.timer) {
|
|
139
|
+
clearTimeout(bucket.timer);
|
|
140
|
+
bucket.timer = null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const events = bucket.events.splice(0);
|
|
144
|
+
this.#buckets.delete(key);
|
|
145
|
+
|
|
146
|
+
// 标记去重
|
|
147
|
+
for (const evt of events) {
|
|
148
|
+
const dedupe = this.#hashEvent(key, evt);
|
|
149
|
+
this.#dedupeMap.set(dedupe, Date.now());
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 清理过期去重记录
|
|
153
|
+
this.#cleanupDedupe();
|
|
154
|
+
|
|
155
|
+
// 通知监听器
|
|
156
|
+
const listeners = this.#listeners.get('batch') || [];
|
|
157
|
+
for (const fn of listeners) {
|
|
158
|
+
try { fn(key, events); }
|
|
159
|
+
catch (err) {
|
|
160
|
+
this.#logger.warn(`[EventAggregator] listener error: ${err.message}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
this.#logger.debug(`[EventAggregator] flushed ${events.length} events for key "${key}"`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
#hashEvent(key, event) {
|
|
168
|
+
// 简单 hash: key + 事件关键字段
|
|
169
|
+
const significant = { key };
|
|
170
|
+
if (event.filePath) significant.f = event.filePath;
|
|
171
|
+
if (event.ruleName) significant.r = event.ruleName;
|
|
172
|
+
if (event.action) significant.a = event.action;
|
|
173
|
+
if (event.id) significant.i = event.id;
|
|
174
|
+
return JSON.stringify(significant);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
#cleanupDedupe() {
|
|
178
|
+
const now = Date.now();
|
|
179
|
+
for (const [hash, ts] of this.#dedupeMap) {
|
|
180
|
+
if ((now - ts) > this.#dedupeMs) {
|
|
181
|
+
this.#dedupeMap.delete(hash);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export default EventAggregator;
|