autosnippet 2.13.0 → 2.15.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/README.md +2 -1
- package/bin/cli.js +1 -0
- package/lib/cli/UpgradeService.js +2 -1
- package/lib/domain/knowledge/Lifecycle.js +1 -0
- package/lib/external/mcp/McpServer.js +58 -42
- package/lib/external/mcp/errorHandler.js +106 -0
- package/lib/external/mcp/handlers/bootstrap/pipeline/ToolResultCache.js +86 -7
- package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +2 -1
- package/lib/external/mcp/handlers/knowledge.js +74 -0
- package/lib/external/mcp/tools.js +23 -0
- package/lib/http/HttpServer.js +4 -0
- package/lib/http/middleware/requestLogger.js +21 -4
- package/lib/http/routes/recipes.js +163 -0
- package/lib/repository/base/BaseRepository.js +2 -1
- package/lib/repository/knowledge/KnowledgeRepository.impl.js +26 -32
- package/lib/service/automation/FileWatcher.js +19 -6
- package/lib/service/chat/WorkingMemory.js +11 -0
- package/lib/service/chat/tools.js +63 -0
- package/lib/service/cursor/CursorDeliveryPipeline.js +123 -16
- package/lib/service/guard/ComplianceReporter.js +14 -13
- package/lib/service/guard/GuardService.js +4 -3
- package/lib/service/guard/RuleLearner.js +13 -9
- package/lib/service/quality/QualityScorer.js +10 -14
- package/lib/shared/constants.js +161 -0
- package/lib/shared/utils/common.js +68 -0
- package/package.json +1 -1
- package/skills/autosnippet-candidates/SKILL.md +1 -1
- package/skills/autosnippet-create/SKILL.md +1 -0
- package/skills/autosnippet-devdocs/SKILL.md +90 -0
- package/skills/autosnippet-recipes/SKILL.md +1 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recipes API 路由
|
|
3
|
+
* 提供 Recipe 知识图谱关系发现等操作
|
|
4
|
+
*
|
|
5
|
+
* 说明: Recipe 的 CRUD 已由 knowledge.js 统一提供,
|
|
6
|
+
* 此路由仅处理 Recipe 特有的批量 AI 操作。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import express from 'express';
|
|
10
|
+
import { asyncHandler } from '../middleware/errorHandler.js';
|
|
11
|
+
import { getServiceContainer } from '../../injection/ServiceContainer.js';
|
|
12
|
+
import Logger from '../../infrastructure/logging/Logger.js';
|
|
13
|
+
|
|
14
|
+
const router = express.Router();
|
|
15
|
+
const logger = Logger.getInstance();
|
|
16
|
+
|
|
17
|
+
/* ═══ 进程内任务状态(单实例足够) ═══════════════════════ */
|
|
18
|
+
|
|
19
|
+
let discoverTask = {
|
|
20
|
+
status: 'idle', // idle | running | done | error
|
|
21
|
+
startedAt: null,
|
|
22
|
+
finishedAt: null,
|
|
23
|
+
discovered: 0,
|
|
24
|
+
totalPairs: 0,
|
|
25
|
+
batchErrors: 0,
|
|
26
|
+
error: null,
|
|
27
|
+
elapsed: 0,
|
|
28
|
+
message: null,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function resetTask() {
|
|
32
|
+
discoverTask = {
|
|
33
|
+
status: 'idle',
|
|
34
|
+
startedAt: null,
|
|
35
|
+
finishedAt: null,
|
|
36
|
+
discovered: 0,
|
|
37
|
+
totalPairs: 0,
|
|
38
|
+
batchErrors: 0,
|
|
39
|
+
error: null,
|
|
40
|
+
elapsed: 0,
|
|
41
|
+
message: null,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* ═══ POST /api/v1/recipes/discover-relations ═══════════ */
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 异步启动 AI 批量发现 Recipe 知识图谱关系
|
|
49
|
+
* Body: { batchSize?: number }
|
|
50
|
+
*
|
|
51
|
+
* 立即返回 { status: 'started' },后台执行。
|
|
52
|
+
* Dashboard 通过 GET /discover-relations/status 轮询进度。
|
|
53
|
+
*/
|
|
54
|
+
router.post('/discover-relations', asyncHandler(async (req, res) => {
|
|
55
|
+
const { batchSize = 20 } = req.body;
|
|
56
|
+
|
|
57
|
+
// 如果已有任务在运行,返回当前状态
|
|
58
|
+
if (discoverTask.status === 'running') {
|
|
59
|
+
const elapsed = Math.round((Date.now() - new Date(discoverTask.startedAt).getTime()) / 1000);
|
|
60
|
+
return res.json({
|
|
61
|
+
success: true,
|
|
62
|
+
data: {
|
|
63
|
+
status: 'running',
|
|
64
|
+
startedAt: discoverTask.startedAt,
|
|
65
|
+
elapsed,
|
|
66
|
+
message: 'AI 分析仍在进行中',
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 检查 ChatAgent 是否可用
|
|
72
|
+
const container = getServiceContainer();
|
|
73
|
+
let chatAgent;
|
|
74
|
+
try {
|
|
75
|
+
chatAgent = container.get('chatAgent');
|
|
76
|
+
} catch {
|
|
77
|
+
return res.json({
|
|
78
|
+
success: true,
|
|
79
|
+
data: { status: 'error', error: 'ChatAgent 不可用,请检查 AI Provider 配置' },
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 快速检查:至少需要 2 条活跃 Recipe
|
|
84
|
+
try {
|
|
85
|
+
const knowledgeService = container.get('knowledgeService');
|
|
86
|
+
const { items = [], data = [] } = await knowledgeService.list(
|
|
87
|
+
{ lifecycle: 'active' }, { page: 1, pageSize: 5 },
|
|
88
|
+
);
|
|
89
|
+
const count = items.length || data.length;
|
|
90
|
+
if (count < 2) {
|
|
91
|
+
return res.json({
|
|
92
|
+
success: true,
|
|
93
|
+
data: {
|
|
94
|
+
status: 'empty',
|
|
95
|
+
message: `只有 ${count} 条活跃 Recipe,至少需要 2 条才能分析关系`,
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
// 如果 list 失败,继续尝试(让 runTask 给出具体错误)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 重置并启动后台任务
|
|
104
|
+
resetTask();
|
|
105
|
+
discoverTask.status = 'running';
|
|
106
|
+
discoverTask.startedAt = new Date().toISOString();
|
|
107
|
+
|
|
108
|
+
// 异步执行,不 await
|
|
109
|
+
(async () => {
|
|
110
|
+
try {
|
|
111
|
+
const result = await chatAgent.runTask('discover_all_relations', { batchSize });
|
|
112
|
+
discoverTask.status = 'done';
|
|
113
|
+
discoverTask.finishedAt = new Date().toISOString();
|
|
114
|
+
discoverTask.discovered = result.discovered || 0;
|
|
115
|
+
discoverTask.totalPairs = result.totalPairs || 0;
|
|
116
|
+
discoverTask.batchErrors = result.batchErrors || 0;
|
|
117
|
+
discoverTask.elapsed = Math.round(
|
|
118
|
+
(new Date(discoverTask.finishedAt).getTime() - new Date(discoverTask.startedAt).getTime()) / 1000,
|
|
119
|
+
);
|
|
120
|
+
logger.info('Discover relations completed', {
|
|
121
|
+
discovered: discoverTask.discovered,
|
|
122
|
+
totalPairs: discoverTask.totalPairs,
|
|
123
|
+
batchErrors: discoverTask.batchErrors,
|
|
124
|
+
elapsed: discoverTask.elapsed,
|
|
125
|
+
});
|
|
126
|
+
} catch (err) {
|
|
127
|
+
discoverTask.status = 'error';
|
|
128
|
+
discoverTask.finishedAt = new Date().toISOString();
|
|
129
|
+
discoverTask.error = err.message;
|
|
130
|
+
discoverTask.elapsed = Math.round(
|
|
131
|
+
(new Date(discoverTask.finishedAt).getTime() - new Date(discoverTask.startedAt).getTime()) / 1000,
|
|
132
|
+
);
|
|
133
|
+
logger.error('Discover relations failed', { error: err.message });
|
|
134
|
+
}
|
|
135
|
+
})();
|
|
136
|
+
|
|
137
|
+
res.json({
|
|
138
|
+
success: true,
|
|
139
|
+
data: {
|
|
140
|
+
status: 'started',
|
|
141
|
+
startedAt: discoverTask.startedAt,
|
|
142
|
+
message: 'AI 分析已启动,正在后台运行',
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
}));
|
|
146
|
+
|
|
147
|
+
/* ═══ GET /api/v1/recipes/discover-relations/status ═════ */
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* 查询关系发现任务状态
|
|
151
|
+
*/
|
|
152
|
+
router.get('/discover-relations/status', asyncHandler(async (req, res) => {
|
|
153
|
+
const data = { ...discoverTask };
|
|
154
|
+
|
|
155
|
+
// 计算实时 elapsed
|
|
156
|
+
if (data.status === 'running' && data.startedAt) {
|
|
157
|
+
data.elapsed = Math.round((Date.now() - new Date(data.startedAt).getTime()) / 1000);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
res.json({ success: true, data });
|
|
161
|
+
}));
|
|
162
|
+
|
|
163
|
+
export default router;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Logger from '../../infrastructure/logging/Logger.js';
|
|
2
|
+
import { unixNow } from '../../shared/utils/common.js';
|
|
2
3
|
|
|
3
4
|
/** Only allow safe SQL identifier characters: letters, digits, underscore */
|
|
4
5
|
const SAFE_IDENTIFIER_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
@@ -180,7 +181,7 @@ export class BaseRepository {
|
|
|
180
181
|
`;
|
|
181
182
|
|
|
182
183
|
const stmt = this.db.prepare(query);
|
|
183
|
-
stmt.run(...updateValues,
|
|
184
|
+
stmt.run(...updateValues, unixNow(), id);
|
|
184
185
|
|
|
185
186
|
return this.findById(id);
|
|
186
187
|
} catch (error) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { BaseRepository } from '../base/BaseRepository.js';
|
|
2
2
|
import { KnowledgeEntry, Lifecycle, inferKind } from '../../domain/knowledge/index.js';
|
|
3
3
|
import Logger from '../../infrastructure/logging/Logger.js';
|
|
4
|
+
import { safeJsonParse, safeJsonStringify, unixNow } from '../../shared/utils/common.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* KnowledgeRepositoryImpl — 统一知识实体仓储实现
|
|
@@ -57,7 +58,7 @@ export class KnowledgeRepositoryImpl extends BaseRepository {
|
|
|
57
58
|
const row = this._entityToRow(updates);
|
|
58
59
|
delete row.id;
|
|
59
60
|
delete row.createdAt;
|
|
60
|
-
row.updatedAt =
|
|
61
|
+
row.updatedAt = unixNow();
|
|
61
62
|
|
|
62
63
|
const setClauses = Object.keys(row).map(k => `${k} = ?`).join(', ');
|
|
63
64
|
this.db.prepare(`UPDATE knowledge_entries SET ${setClauses} WHERE id = ?`)
|
|
@@ -69,7 +70,7 @@ export class KnowledgeRepositoryImpl extends BaseRepository {
|
|
|
69
70
|
const merged = KnowledgeEntry.fromJSON({
|
|
70
71
|
...existing.toJSON(),
|
|
71
72
|
...updates,
|
|
72
|
-
updatedAt:
|
|
73
|
+
updatedAt: unixNow(),
|
|
73
74
|
});
|
|
74
75
|
const row = this._entityToRow(merged);
|
|
75
76
|
delete row.id;
|
|
@@ -248,17 +249,17 @@ export class KnowledgeRepositoryImpl extends BaseRepository {
|
|
|
248
249
|
return new KnowledgeEntry({
|
|
249
250
|
...row,
|
|
250
251
|
// JSON 列需要 parse
|
|
251
|
-
lifecycleHistory:
|
|
252
|
-
tags:
|
|
253
|
-
content:
|
|
254
|
-
relations:
|
|
255
|
-
constraints:
|
|
256
|
-
reasoning:
|
|
257
|
-
quality:
|
|
258
|
-
stats:
|
|
259
|
-
headers:
|
|
260
|
-
headerPaths:
|
|
261
|
-
agentNotes:
|
|
252
|
+
lifecycleHistory: safeJsonParse(row.lifecycleHistory, []),
|
|
253
|
+
tags: safeJsonParse(row.tags, []),
|
|
254
|
+
content: safeJsonParse(row.content, {}),
|
|
255
|
+
relations: safeJsonParse(row.relations, {}),
|
|
256
|
+
constraints: safeJsonParse(row.constraints, {}),
|
|
257
|
+
reasoning: safeJsonParse(row.reasoning, {}),
|
|
258
|
+
quality: safeJsonParse(row.quality, {}),
|
|
259
|
+
stats: safeJsonParse(row.stats, {}),
|
|
260
|
+
headers: safeJsonParse(row.headers, []),
|
|
261
|
+
headerPaths: safeJsonParse(row.headerPaths, []),
|
|
262
|
+
agentNotes: safeJsonParse(row.agentNotes, null),
|
|
262
263
|
// SQLite INTEGER → boolean
|
|
263
264
|
autoApprovable: !!row.autoApprovable,
|
|
264
265
|
includeHeaders: !!row.includeHeaders,
|
|
@@ -271,13 +272,13 @@ export class KnowledgeRepositoryImpl extends BaseRepository {
|
|
|
271
272
|
* @returns {Object}
|
|
272
273
|
*/
|
|
273
274
|
_entityToRow(e) {
|
|
274
|
-
const now =
|
|
275
|
+
const now = unixNow();
|
|
275
276
|
return {
|
|
276
277
|
id: e.id,
|
|
277
278
|
title: e.title,
|
|
278
279
|
description: e.description || '',
|
|
279
280
|
lifecycle: e.lifecycle,
|
|
280
|
-
lifecycleHistory:
|
|
281
|
+
lifecycleHistory: safeJsonStringify(e.lifecycleHistory || [], '[]'),
|
|
281
282
|
autoApprovable: e.autoApprovable ? 1 : 0,
|
|
282
283
|
language: e.language,
|
|
283
284
|
category: e.category,
|
|
@@ -286,24 +287,24 @@ export class KnowledgeRepositoryImpl extends BaseRepository {
|
|
|
286
287
|
complexity: e.complexity || 'intermediate',
|
|
287
288
|
scope: e.scope || null,
|
|
288
289
|
difficulty: e.difficulty || null,
|
|
289
|
-
tags:
|
|
290
|
+
tags: safeJsonStringify(e.tags || [], '[]'),
|
|
290
291
|
trigger: e.trigger || '',
|
|
291
292
|
topicHint: e.topicHint || '',
|
|
292
293
|
whenClause: e.whenClause || '',
|
|
293
294
|
doClause: e.doClause || '',
|
|
294
295
|
dontClause: e.dontClause || '',
|
|
295
296
|
coreCode: e.coreCode || '',
|
|
296
|
-
content:
|
|
297
|
-
relations:
|
|
298
|
-
constraints:
|
|
299
|
-
reasoning:
|
|
300
|
-
quality:
|
|
301
|
-
stats:
|
|
302
|
-
headers:
|
|
303
|
-
headerPaths:
|
|
297
|
+
content: safeJsonStringify(e.content || {}),
|
|
298
|
+
relations: safeJsonStringify(e.relations || {}),
|
|
299
|
+
constraints: safeJsonStringify(e.constraints || {}),
|
|
300
|
+
reasoning: safeJsonStringify(e.reasoning || {}),
|
|
301
|
+
quality: safeJsonStringify(e.quality || {}),
|
|
302
|
+
stats: safeJsonStringify(e.stats || {}),
|
|
303
|
+
headers: safeJsonStringify(e.headers || [], '[]'),
|
|
304
|
+
headerPaths: safeJsonStringify(e.headerPaths || [], '[]'),
|
|
304
305
|
moduleName: e.moduleName || null,
|
|
305
306
|
includeHeaders: e.includeHeaders ? 1 : 0,
|
|
306
|
-
agentNotes: e.agentNotes ?
|
|
307
|
+
agentNotes: e.agentNotes ? safeJsonStringify(e.agentNotes) : null,
|
|
307
308
|
aiInsight: e.aiInsight || null,
|
|
308
309
|
reviewedBy: e.reviewedBy || null,
|
|
309
310
|
reviewedAt: e.reviewedAt || null,
|
|
@@ -326,13 +327,6 @@ export class KnowledgeRepositoryImpl extends BaseRepository {
|
|
|
326
327
|
_mapRowToEntity(row) {
|
|
327
328
|
return this._rowToEntity(row);
|
|
328
329
|
}
|
|
329
|
-
|
|
330
|
-
/** @private 安全解析 JSON */
|
|
331
|
-
_parseJson(value, fallback) {
|
|
332
|
-
if (!value || value === 'null') return fallback;
|
|
333
|
-
if (typeof value === 'object') return value;
|
|
334
|
-
try { return JSON.parse(value); } catch { return fallback; }
|
|
335
|
-
}
|
|
336
330
|
}
|
|
337
331
|
|
|
338
332
|
export default KnowledgeRepositoryImpl;
|
|
@@ -14,6 +14,7 @@ import { readFileSync, accessSync, statSync } from 'node:fs';
|
|
|
14
14
|
import { basename, join, normalize } from 'node:path';
|
|
15
15
|
import { detectTriggers, REGEX } from './DirectiveDetector.js';
|
|
16
16
|
import { saveEventFilter } from './SaveEventFilter.js';
|
|
17
|
+
import { FILE_WATCHER } from '../../shared/constants.js';
|
|
17
18
|
|
|
18
19
|
/* ── Handler imports ── */
|
|
19
20
|
import { handleCreate } from './handlers/CreateHandler.js';
|
|
@@ -31,7 +32,7 @@ const IGNORED = [
|
|
|
31
32
|
'**/xcuserdata/**', '**/.build/**', '**/*.swp', '**/*.tmp', '**/*~.m', '**/*~.h',
|
|
32
33
|
'**/DerivedData/**', '**/Pods/**', '**/Carthage/**',
|
|
33
34
|
];
|
|
34
|
-
const DEBOUNCE_DELAY =
|
|
35
|
+
const DEBOUNCE_DELAY = FILE_WATCHER.DEBOUNCE_DELAY_MS;
|
|
35
36
|
|
|
36
37
|
/* ────────── FileWatcher ────────── */
|
|
37
38
|
|
|
@@ -76,10 +77,10 @@ export class FileWatcher {
|
|
|
76
77
|
ignored: IGNORED,
|
|
77
78
|
ignoreInitial: true,
|
|
78
79
|
persistent: true,
|
|
79
|
-
awaitWriteFinish: { stabilityThreshold:
|
|
80
|
+
awaitWriteFinish: { stabilityThreshold: FILE_WATCHER.STABILITY_THRESHOLD_MS, pollInterval: FILE_WATCHER.POLL_INTERVAL_MS },
|
|
80
81
|
usePolling: process.env.ASD_WATCH_POLLING === 'true',
|
|
81
|
-
interval:
|
|
82
|
-
binaryInterval:
|
|
82
|
+
interval: FILE_WATCHER.POLL_INTERVAL_MS,
|
|
83
|
+
binaryInterval: FILE_WATCHER.BINARY_INTERVAL_MS,
|
|
83
84
|
});
|
|
84
85
|
|
|
85
86
|
const handleEvent = (relativePath) => {
|
|
@@ -115,17 +116,29 @@ export class FileWatcher {
|
|
|
115
116
|
}
|
|
116
117
|
|
|
117
118
|
/**
|
|
118
|
-
*
|
|
119
|
+
* 停止监听,释放所有资源
|
|
119
120
|
*/
|
|
120
121
|
async stop() {
|
|
121
122
|
if (this._watcher) {
|
|
123
|
+
// 移除所有事件监听器,避免泄漏
|
|
124
|
+
this._watcher.removeAllListeners();
|
|
122
125
|
await this._watcher.close();
|
|
123
126
|
this._watcher = null;
|
|
124
127
|
}
|
|
128
|
+
// 清理所有防抖定时器
|
|
125
129
|
for (const timer of this._debounceTimers.values()) {
|
|
126
130
|
clearTimeout(timer);
|
|
127
131
|
}
|
|
128
132
|
this._debounceTimers.clear();
|
|
133
|
+
// 清理 handler 级别的延时定时器
|
|
134
|
+
if (this._timeoutLink) {
|
|
135
|
+
clearTimeout(this._timeoutLink);
|
|
136
|
+
this._timeoutLink = null;
|
|
137
|
+
}
|
|
138
|
+
if (this._timeoutHead) {
|
|
139
|
+
clearTimeout(this._timeoutHead);
|
|
140
|
+
this._timeoutHead = null;
|
|
141
|
+
}
|
|
129
142
|
}
|
|
130
143
|
|
|
131
144
|
/* ────────── 内部:文件处理(分派到 handlers) ────────── */
|
|
@@ -134,7 +147,7 @@ export class FileWatcher {
|
|
|
134
147
|
try {
|
|
135
148
|
accessSync(fullPath);
|
|
136
149
|
const stat = statSync(fullPath);
|
|
137
|
-
if (stat.isDirectory() || stat.size >
|
|
150
|
+
if (stat.isDirectory() || stat.size > FILE_WATCHER.MAX_FILE_SIZE_BYTES) return;
|
|
138
151
|
} catch {
|
|
139
152
|
return;
|
|
140
153
|
}
|
|
@@ -284,6 +284,17 @@ export class WorkingMemory {
|
|
|
284
284
|
.sort((a, b) => b.importance - a.importance);
|
|
285
285
|
}
|
|
286
286
|
|
|
287
|
+
/**
|
|
288
|
+
* 清空 WorkingMemory — 释放内存
|
|
289
|
+
* 在 Agent execute 结束后调用,避免残留引用导致内存泄漏
|
|
290
|
+
*/
|
|
291
|
+
clear() {
|
|
292
|
+
this.#recentObservations.length = 0;
|
|
293
|
+
this.#compressedObservations.length = 0;
|
|
294
|
+
this.#scratchpad.length = 0;
|
|
295
|
+
this.#totalObservations = 0;
|
|
296
|
+
}
|
|
297
|
+
|
|
287
298
|
// ─── 内部 ─────────────────────────────────────────────
|
|
288
299
|
|
|
289
300
|
/**
|
|
@@ -1844,6 +1844,68 @@ const submitCandidate = {
|
|
|
1844
1844
|
},
|
|
1845
1845
|
};
|
|
1846
1846
|
|
|
1847
|
+
// ────────────────────────────────────────────────────────────
|
|
1848
|
+
// 16b. save_document — 保存开发文档到知识库
|
|
1849
|
+
// ────────────────────────────────────────────────────────────
|
|
1850
|
+
const saveDocument = {
|
|
1851
|
+
name: 'save_document',
|
|
1852
|
+
description: '保存开发文档到知识库(架构设计、排查报告、决策记录、调研笔记等)。仅需 title + markdown,无需 Cursor Delivery 字段。文档自动发布,可通过 autosnippet_search 检索。',
|
|
1853
|
+
parameters: {
|
|
1854
|
+
type: 'object',
|
|
1855
|
+
properties: {
|
|
1856
|
+
title: { type: 'string', description: '文档标题' },
|
|
1857
|
+
markdown: { type: 'string', description: '文档 Markdown 全文' },
|
|
1858
|
+
description: { type: 'string', description: '一句话摘要(可选)' },
|
|
1859
|
+
tags: { type: 'array', items: { type: 'string' }, description: '标签: adr, debug-report, design-doc, research, performance 等' },
|
|
1860
|
+
scope: { type: 'string', enum: ['universal', 'project-specific'], description: '适用范围(默认 project-specific)' },
|
|
1861
|
+
},
|
|
1862
|
+
required: ['title', 'markdown'],
|
|
1863
|
+
},
|
|
1864
|
+
handler: async (params, ctx) => {
|
|
1865
|
+
const knowledgeService = ctx.container.get('knowledgeService');
|
|
1866
|
+
|
|
1867
|
+
const data = {
|
|
1868
|
+
title: params.title.trim(),
|
|
1869
|
+
description: params.description || '',
|
|
1870
|
+
knowledgeType: 'dev-document',
|
|
1871
|
+
kind: 'fact',
|
|
1872
|
+
source: 'agent',
|
|
1873
|
+
scope: params.scope || 'project-specific',
|
|
1874
|
+
tags: params.tags || [],
|
|
1875
|
+
content: {
|
|
1876
|
+
markdown: params.markdown,
|
|
1877
|
+
pattern: '',
|
|
1878
|
+
},
|
|
1879
|
+
trigger: '',
|
|
1880
|
+
doClause: '',
|
|
1881
|
+
dontClause: '',
|
|
1882
|
+
whenClause: '',
|
|
1883
|
+
topicHint: '',
|
|
1884
|
+
coreCode: '',
|
|
1885
|
+
reasoning: {
|
|
1886
|
+
whyStandard: 'Agent development document',
|
|
1887
|
+
sources: ['agent'],
|
|
1888
|
+
confidence: 0.8,
|
|
1889
|
+
},
|
|
1890
|
+
};
|
|
1891
|
+
|
|
1892
|
+
const saved = await knowledgeService.create(data, { userId: 'agent' });
|
|
1893
|
+
|
|
1894
|
+
// 自动发布(文档不需要人工审核)
|
|
1895
|
+
try {
|
|
1896
|
+
await knowledgeService.publish(saved.id, { userId: 'agent' });
|
|
1897
|
+
} catch { /* best effort */ }
|
|
1898
|
+
|
|
1899
|
+
return {
|
|
1900
|
+
id: saved.id,
|
|
1901
|
+
title: saved.title,
|
|
1902
|
+
lifecycle: 'active',
|
|
1903
|
+
knowledgeType: 'dev-document',
|
|
1904
|
+
message: `文档「${saved.title}」已保存到知识库`,
|
|
1905
|
+
};
|
|
1906
|
+
},
|
|
1907
|
+
};
|
|
1908
|
+
|
|
1847
1909
|
// ────────────────────────────────────────────────────────────
|
|
1848
1910
|
// 17. approve_candidate
|
|
1849
1911
|
// ────────────────────────────────────────────────────────────
|
|
@@ -3273,6 +3335,7 @@ export const ALL_TOOLS = [
|
|
|
3273
3335
|
generateGuardRule,
|
|
3274
3336
|
// 生命周期操作类 (7)
|
|
3275
3337
|
submitCandidate,
|
|
3338
|
+
saveDocument,
|
|
3276
3339
|
approveCandidate,
|
|
3277
3340
|
rejectCandidate,
|
|
3278
3341
|
publishRecipe,
|
|
@@ -15,7 +15,9 @@ import { TopicClassifier } from './TopicClassifier.js';
|
|
|
15
15
|
import { RulesGenerator } from './RulesGenerator.js';
|
|
16
16
|
import { SkillsSyncer } from './SkillsSyncer.js';
|
|
17
17
|
import { estimateTokens, BUDGET } from './TokenBudget.js';
|
|
18
|
+
import { KNOWLEDGE_CONFIDENCE, DELIVERY_RANK } from '../../shared/constants.js';
|
|
18
19
|
import path from 'node:path';
|
|
20
|
+
import fs from 'node:fs';
|
|
19
21
|
|
|
20
22
|
export class CursorDeliveryPipeline {
|
|
21
23
|
/**
|
|
@@ -48,6 +50,7 @@ export class CursorDeliveryPipeline {
|
|
|
48
50
|
channelA: { rulesCount: 0, tokensUsed: 0 },
|
|
49
51
|
channelB: { topicCount: 0, patternsCount: 0, totalTokens: 0 },
|
|
50
52
|
channelC: { synced: 0, skipped: 0, errors: 0 },
|
|
53
|
+
channelD: { documentsCount: 0, filesWritten: 0 },
|
|
51
54
|
totalTokensUsed: 0,
|
|
52
55
|
duration: 0,
|
|
53
56
|
};
|
|
@@ -57,9 +60,9 @@ export class CursorDeliveryPipeline {
|
|
|
57
60
|
const entries = await this._loadEntries();
|
|
58
61
|
this.logger.info?.(`[CursorDelivery] Loaded ${entries.length} knowledge entries`);
|
|
59
62
|
|
|
60
|
-
// 2. 分类:rules vs patterns vs facts
|
|
61
|
-
const { rules, patterns } = this._classify(entries);
|
|
62
|
-
this.logger.info?.(`[CursorDelivery] Classified: ${rules.length} rules, ${patterns.length} patterns`);
|
|
63
|
+
// 2. 分类:rules vs patterns vs facts vs documents
|
|
64
|
+
const { rules, patterns, documents } = this._classify(entries);
|
|
65
|
+
this.logger.info?.(`[CursorDelivery] Classified: ${rules.length} rules, ${patterns.length} patterns, ${documents.length} documents`);
|
|
63
66
|
|
|
64
67
|
// 3. 清理旧的动态生成文件
|
|
65
68
|
this.rulesGenerator.cleanDynamicFiles();
|
|
@@ -76,6 +79,10 @@ export class CursorDeliveryPipeline {
|
|
|
76
79
|
const channelC = await this._generateChannelC();
|
|
77
80
|
stats.channelC = channelC;
|
|
78
81
|
|
|
82
|
+
// ── Channel D: Dev Documents → references ──
|
|
83
|
+
const channelD = this._generateChannelD(documents);
|
|
84
|
+
stats.channelD = channelD;
|
|
85
|
+
|
|
79
86
|
// 统计
|
|
80
87
|
stats.totalTokensUsed = channelA.tokensUsed + channelB.totalTokens;
|
|
81
88
|
stats.duration = Date.now() - startTime;
|
|
@@ -83,9 +90,10 @@ export class CursorDeliveryPipeline {
|
|
|
83
90
|
this.logger.info?.(`[CursorDelivery] Done in ${stats.duration}ms — ` +
|
|
84
91
|
`A: ${channelA.rulesCount} rules (${channelA.tokensUsed} tokens), ` +
|
|
85
92
|
`B: ${channelB.topicCount} topics (${channelB.totalTokens} tokens), ` +
|
|
86
|
-
`C: ${channelC.synced} skills synced`
|
|
93
|
+
`C: ${channelC.synced} skills synced, ` +
|
|
94
|
+
`D: ${channelD.documentsCount} documents`);
|
|
87
95
|
|
|
88
|
-
return { channelA, channelB, channelC, stats };
|
|
96
|
+
return { channelA, channelB, channelC, channelD, stats };
|
|
89
97
|
} catch (error) {
|
|
90
98
|
this.logger.error?.(`[CursorDelivery] Error: ${error.message}`);
|
|
91
99
|
throw error;
|
|
@@ -120,10 +128,10 @@ export class CursorDeliveryPipeline {
|
|
|
120
128
|
{ page: 1, pageSize: 200 }
|
|
121
129
|
);
|
|
122
130
|
const pendingItems = this._extractItems(pending);
|
|
123
|
-
// 过滤高置信度 pending(quality.confidence >=
|
|
131
|
+
// 过滤高置信度 pending(quality.confidence >= PENDING_MIN 或无 quality 字段)
|
|
124
132
|
const highConfPending = pendingItems.filter(e => {
|
|
125
133
|
const conf = e.quality?.confidence;
|
|
126
|
-
return conf === undefined || conf === null || conf >=
|
|
134
|
+
return conf === undefined || conf === null || conf >= KNOWLEDGE_CONFIDENCE.PENDING_MIN;
|
|
127
135
|
});
|
|
128
136
|
allEntries.push(...highConfPending);
|
|
129
137
|
} catch (e) {
|
|
@@ -146,16 +154,23 @@ export class CursorDeliveryPipeline {
|
|
|
146
154
|
|
|
147
155
|
/**
|
|
148
156
|
* 按 kind 分类知识条目
|
|
157
|
+
* dev-document 类型单独分流,不进入 Channel A/B 压缩
|
|
149
158
|
* @private
|
|
150
159
|
*/
|
|
151
160
|
_classify(entries) {
|
|
152
|
-
const rules = [], patterns = [], facts = [];
|
|
161
|
+
const rules = [], patterns = [], facts = [], documents = [];
|
|
153
162
|
for (const entry of entries) {
|
|
154
|
-
if (entry.
|
|
155
|
-
|
|
156
|
-
else
|
|
163
|
+
if (entry.knowledgeType === 'dev-document') {
|
|
164
|
+
documents.push(entry);
|
|
165
|
+
} else if (entry.kind === 'rule') {
|
|
166
|
+
rules.push(entry);
|
|
167
|
+
} else if (entry.kind === 'fact') {
|
|
168
|
+
facts.push(entry);
|
|
169
|
+
} else {
|
|
170
|
+
patterns.push(entry); // 无 kind 或 kind='pattern' → pattern
|
|
171
|
+
}
|
|
157
172
|
}
|
|
158
|
-
return { rules, patterns, facts };
|
|
173
|
+
return { rules, patterns, facts, documents };
|
|
159
174
|
}
|
|
160
175
|
|
|
161
176
|
/**
|
|
@@ -176,10 +191,10 @@ export class CursorDeliveryPipeline {
|
|
|
176
191
|
*/
|
|
177
192
|
_rankScore(entry) {
|
|
178
193
|
let score = 0;
|
|
179
|
-
score += (entry.quality?.confidence ||
|
|
180
|
-
score += (entry.quality?.authorityScore || 0) *
|
|
181
|
-
score += Math.min(entry.stats?.useCount || 0,
|
|
182
|
-
if (entry.lifecycle === 'active') score +=
|
|
194
|
+
score += (entry.quality?.confidence || KNOWLEDGE_CONFIDENCE.RANK_DEFAULT) * DELIVERY_RANK.CONFIDENCE_WEIGHT;
|
|
195
|
+
score += (entry.quality?.authorityScore || 0) * DELIVERY_RANK.AUTHORITY_WEIGHT;
|
|
196
|
+
score += Math.min(entry.stats?.useCount || 0, DELIVERY_RANK.USE_COUNT_MAX) * DELIVERY_RANK.USE_COUNT_WEIGHT;
|
|
197
|
+
if (entry.lifecycle === 'active') score += DELIVERY_RANK.ACTIVE_BONUS;
|
|
183
198
|
return score;
|
|
184
199
|
}
|
|
185
200
|
|
|
@@ -267,6 +282,98 @@ export class CursorDeliveryPipeline {
|
|
|
267
282
|
}
|
|
268
283
|
}
|
|
269
284
|
|
|
285
|
+
/**
|
|
286
|
+
* Channel D — Dev Documents 生成
|
|
287
|
+
* 将 knowledgeType='dev-document' 的条目以原始 MD 写入
|
|
288
|
+
* .cursor/skills/autosnippet-devdocs/references/ 目录
|
|
289
|
+
* @private
|
|
290
|
+
*/
|
|
291
|
+
_generateChannelD(documents) {
|
|
292
|
+
const result = { documentsCount: 0, filesWritten: 0, filePaths: [] };
|
|
293
|
+
if (!documents || documents.length === 0) {
|
|
294
|
+
return result;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const devdocsDir = path.join(this.projectRoot, '.cursor', 'skills', 'autosnippet-devdocs');
|
|
298
|
+
const refsDir = path.join(devdocsDir, 'references');
|
|
299
|
+
fs.mkdirSync(refsDir, { recursive: true });
|
|
300
|
+
|
|
301
|
+
// 生成 SKILL.md(索引页)
|
|
302
|
+
const skillLines = [
|
|
303
|
+
'---',
|
|
304
|
+
'name: autosnippet-devdocs',
|
|
305
|
+
`description: "Development documents and knowledge artifacts for ${this.projectName}. Use when looking up architecture decisions, debug reports, design docs, or analysis notes."`,
|
|
306
|
+
'---',
|
|
307
|
+
'',
|
|
308
|
+
`# Dev Documents — ${this.projectName}`,
|
|
309
|
+
'',
|
|
310
|
+
'Use this skill when:',
|
|
311
|
+
'- Looking up architecture decisions or design rationale',
|
|
312
|
+
'- Reviewing debug reports or performance analysis',
|
|
313
|
+
'- Finding previous research or investigation notes',
|
|
314
|
+
'- Understanding project-specific decisions and trade-offs',
|
|
315
|
+
'',
|
|
316
|
+
'## Document Index',
|
|
317
|
+
'',
|
|
318
|
+
'| Title | Tags | Updated |',
|
|
319
|
+
'|-------|------|---------|',
|
|
320
|
+
];
|
|
321
|
+
|
|
322
|
+
for (const doc of documents) {
|
|
323
|
+
const tags = (doc.tags || []).join(', ') || '-';
|
|
324
|
+
const updated = doc.updatedAt
|
|
325
|
+
? new Date(doc.updatedAt * 1000).toISOString().split('T')[0]
|
|
326
|
+
: '-';
|
|
327
|
+
const slug = this._slugify(doc.title || doc.id);
|
|
328
|
+
skillLines.push(`| [${doc.title}](references/${slug}.md) | ${tags} | ${updated} |`);
|
|
329
|
+
|
|
330
|
+
// 写入单个文档 MD
|
|
331
|
+
const markdown = doc.content?.markdown || doc.description || '';
|
|
332
|
+
const docContent = [
|
|
333
|
+
`# ${doc.title || 'Untitled'}`,
|
|
334
|
+
'',
|
|
335
|
+
doc.description ? `> ${doc.description}` : '',
|
|
336
|
+
'',
|
|
337
|
+
`**Tags:** ${tags} `,
|
|
338
|
+
`**Scope:** ${doc.scope || 'universal'} `,
|
|
339
|
+
`**Created:** ${doc.createdAt ? new Date(doc.createdAt * 1000).toISOString().split('T')[0] : '-'}`,
|
|
340
|
+
'',
|
|
341
|
+
'---',
|
|
342
|
+
'',
|
|
343
|
+
markdown,
|
|
344
|
+
].filter(Boolean).join('\n');
|
|
345
|
+
|
|
346
|
+
const docPath = path.join(refsDir, `${slug}.md`);
|
|
347
|
+
fs.writeFileSync(docPath, docContent, 'utf8');
|
|
348
|
+
result.filePaths.push(docPath);
|
|
349
|
+
result.filesWritten++;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
skillLines.push('');
|
|
353
|
+
skillLines.push('## Deeper Knowledge');
|
|
354
|
+
skillLines.push('');
|
|
355
|
+
skillLines.push('For full-text search across all documents:');
|
|
356
|
+
skillLines.push('- `autosnippet_search("your query")`');
|
|
357
|
+
|
|
358
|
+
fs.writeFileSync(path.join(devdocsDir, 'SKILL.md'), skillLines.join('\n') + '\n', 'utf8');
|
|
359
|
+
result.documentsCount = documents.length;
|
|
360
|
+
|
|
361
|
+
this.logger.info?.(`[CursorDelivery] Channel D: ${result.documentsCount} documents → ${refsDir}`);
|
|
362
|
+
return result;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* 文件名安全 slug 化
|
|
367
|
+
* @private
|
|
368
|
+
*/
|
|
369
|
+
_slugify(text) {
|
|
370
|
+
return text
|
|
371
|
+
.toLowerCase()
|
|
372
|
+
.replace(/[^a-z0-9\u4e00-\u9fff]+/g, '-')
|
|
373
|
+
.replace(/^-+|-+$/g, '')
|
|
374
|
+
.substring(0, 80) || 'untitled';
|
|
375
|
+
}
|
|
376
|
+
|
|
270
377
|
/**
|
|
271
378
|
* 从项目路径推断项目名称
|
|
272
379
|
* @private
|