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
package/README.md
CHANGED
|
@@ -240,7 +240,7 @@ asd install:vscode-copilot # 配置 MCP 和 Copilot 指令
|
|
|
240
240
|
|
|
241
241
|
## MCP 工具一览
|
|
242
242
|
|
|
243
|
-
|
|
243
|
+
39 个 MCP 工具按功能分组(省略了 **autosnippet_** 前缀):
|
|
244
244
|
|
|
245
245
|
| 分类 | 工具 |
|
|
246
246
|
|------|------|
|
|
@@ -248,6 +248,7 @@ asd install:vscode-copilot # 配置 MCP 和 Copilot 指令
|
|
|
248
248
|
| **搜索** | `search`(统合入口)、`context_search`(4 层漏斗)、`keyword_search`、`semantic_search` |
|
|
249
249
|
| **Recipe 浏览** | `list_recipes`、`get_recipe`、`list_rules`、`patterns`、`list_facts`、`recipe_insights`、`confirm_usage` |
|
|
250
250
|
| **候选管理** | `validate_candidate`、`check_duplicate`、`submit_knowledge`、`submit_knowledge_batch`、`enrich_candidates` |
|
|
251
|
+
| **开发文档** | `save_document` |
|
|
251
252
|
| **知识图谱** | `graph_query`、`graph_impact`、`graph_path`、`graph_stats` |
|
|
252
253
|
| **项目结构** | `get_targets`、`get_target_files`、`get_target_metadata` |
|
|
253
254
|
| **Guard** | `guard_check`、`guard_audit_files`、`scan_project` |
|
package/bin/cli.js
CHANGED
|
@@ -658,6 +658,7 @@ program
|
|
|
658
658
|
console.log(` Channel A (Always-On Rules): ${result.channelA.rulesCount} 条规则 (${result.channelA.tokensUsed} tokens)`);
|
|
659
659
|
console.log(` Channel B (Smart Rules): ${result.channelB.topicCount} 个主题, ${result.channelB.patternsCount} 个模式 (${result.channelB.totalTokens} tokens)`);
|
|
660
660
|
console.log(` Channel C (Agent Skills): ${result.channelC.synced} 个 Skills 已同步`);
|
|
661
|
+
console.log(` Channel D (Dev Documents): ${result.channelD?.documentsCount || 0} 篇文档`);
|
|
661
662
|
if (result.channelC.errors > 0) {
|
|
662
663
|
console.log(` ⚠️ ${result.channelC.errors} 个错误`);
|
|
663
664
|
}
|
|
@@ -181,7 +181,8 @@ export class UpgradeService {
|
|
|
181
181
|
pipeline.deliver()
|
|
182
182
|
.then(result => {
|
|
183
183
|
console.log(` ✅ Cursor Delivery: ${result.channelA.rulesCount} rules, ` +
|
|
184
|
-
`${result.channelB.topicCount} topics, ${result.channelC.synced} skills`
|
|
184
|
+
`${result.channelB.topicCount} topics, ${result.channelC.synced} skills, ` +
|
|
185
|
+
`${result.channelD?.documentsCount || 0} documents`);
|
|
185
186
|
})
|
|
186
187
|
.catch(err => {
|
|
187
188
|
console.log(` ⚠️ Cursor Delivery 跳过: ${err.message}`);
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
import Logger from '../../infrastructure/logging/Logger.js';
|
|
21
21
|
import { envelope } from './envelope.js';
|
|
22
22
|
import { TOOLS, TOOL_GATEWAY_MAP } from './tools.js';
|
|
23
|
+
import { wrapHandler } from './errorHandler.js';
|
|
23
24
|
|
|
24
25
|
// ─── Handler 模块 ─────────────────────────────────────────────
|
|
25
26
|
|
|
@@ -116,55 +117,70 @@ export class McpServer {
|
|
|
116
117
|
await this._gatewayGate(name, args);
|
|
117
118
|
|
|
118
119
|
const ctx = this._ctx;
|
|
119
|
-
|
|
120
|
+
|
|
121
|
+
// 查找 handler 并通过 wrapHandler 统一错误处理
|
|
122
|
+
const handler = this._resolveHandler(name);
|
|
123
|
+
if (!handler) throw new Error(`Unknown tool: ${name}`);
|
|
124
|
+
|
|
125
|
+
const wrapped = wrapHandler(name, handler);
|
|
126
|
+
return wrapped(ctx, args);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* 解析工具名到 handler 函数
|
|
131
|
+
* @private
|
|
132
|
+
*/
|
|
133
|
+
_resolveHandler(name) {
|
|
134
|
+
const HANDLER_MAP = {
|
|
120
135
|
// 系统
|
|
121
|
-
|
|
122
|
-
|
|
136
|
+
autosnippet_health: (ctx) => systemHandlers.health(ctx),
|
|
137
|
+
autosnippet_capabilities: () => systemHandlers.capabilities(),
|
|
123
138
|
// 搜索
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
139
|
+
autosnippet_search: (ctx, args) => searchHandlers.search(ctx, args),
|
|
140
|
+
autosnippet_context_search: (ctx, args) => searchHandlers.contextSearch(ctx, args),
|
|
141
|
+
autosnippet_keyword_search: (ctx, args) => searchHandlers.keywordSearch(ctx, args),
|
|
142
|
+
autosnippet_semantic_search: (ctx, args) => searchHandlers.semanticSearch(ctx, args),
|
|
128
143
|
// 知识浏览
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
144
|
+
autosnippet_list_rules: (ctx, args) => browseHandlers.listByKind(ctx, 'rule', args),
|
|
145
|
+
autosnippet_list_patterns: (ctx, args) => browseHandlers.listByKind(ctx, 'pattern', args),
|
|
146
|
+
autosnippet_list_facts: (ctx, args) => browseHandlers.listByKind(ctx, 'fact', args),
|
|
147
|
+
autosnippet_list_recipes: (ctx, args) => browseHandlers.listRecipes(ctx, args),
|
|
148
|
+
autosnippet_get_recipe: (ctx, args) => browseHandlers.getRecipe(ctx, args),
|
|
149
|
+
autosnippet_recipe_insights: (ctx, args) => browseHandlers.recipeInsights(ctx, args),
|
|
150
|
+
autosnippet_confirm_usage: (ctx, args) => browseHandlers.confirmUsage(ctx, args),
|
|
136
151
|
// 项目结构 & 图谱
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
// 候选校验 & AI
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
152
|
+
autosnippet_get_targets: (ctx) => structureHandlers.getTargets(ctx),
|
|
153
|
+
autosnippet_get_target_files: (ctx, args) => structureHandlers.getTargetFiles(ctx, args),
|
|
154
|
+
autosnippet_get_target_metadata: (ctx, args) => structureHandlers.getTargetMetadata(ctx, args),
|
|
155
|
+
autosnippet_graph_query: (ctx, args) => structureHandlers.graphQuery(ctx, args),
|
|
156
|
+
autosnippet_graph_impact: (ctx, args) => structureHandlers.graphImpact(ctx, args),
|
|
157
|
+
autosnippet_graph_path: (ctx, args) => structureHandlers.graphPath(ctx, args),
|
|
158
|
+
autosnippet_graph_stats: (ctx) => structureHandlers.graphStats(ctx),
|
|
159
|
+
// 候选校验 & AI 补全
|
|
160
|
+
autosnippet_validate_candidate: (ctx, args) => candidateHandlers.validateCandidate(ctx, args),
|
|
161
|
+
autosnippet_check_duplicate: (ctx, args) => candidateHandlers.checkDuplicate(ctx, args),
|
|
162
|
+
autosnippet_enrich_candidates: (ctx, args) => candidateHandlers.enrichCandidates(ctx, args),
|
|
148
163
|
// Guard & 扫描
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
164
|
+
autosnippet_guard_check: (ctx, args) => guardHandlers.guardCheck(ctx, args),
|
|
165
|
+
autosnippet_guard_audit_files: (ctx, args) => guardHandlers.guardAuditFiles(ctx, args),
|
|
166
|
+
autosnippet_scan_project: (ctx, args) => guardHandlers.scanProject(ctx, args),
|
|
152
167
|
// Bootstrap 冷启动
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
// Skills
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
168
|
+
autosnippet_bootstrap_knowledge: (ctx, args) => bootstrapHandlers.bootstrapKnowledge(ctx, args),
|
|
169
|
+
autosnippet_bootstrap_refine: (ctx, args) => bootstrapHandlers.bootstrapRefine(ctx, args),
|
|
170
|
+
// Skills
|
|
171
|
+
autosnippet_list_skills: () => skillHandlers.listSkills(),
|
|
172
|
+
autosnippet_load_skill: (ctx, args) => skillHandlers.loadSkill(ctx, args),
|
|
173
|
+
autosnippet_create_skill: (ctx, args) => skillHandlers.createSkill(ctx, args),
|
|
174
|
+
autosnippet_delete_skill: (ctx, args) => skillHandlers.deleteSkill(ctx, args),
|
|
175
|
+
autosnippet_update_skill: (ctx, args) => skillHandlers.updateSkill(ctx, args),
|
|
176
|
+
autosnippet_suggest_skills: (ctx) => skillHandlers.suggestSkills(ctx),
|
|
162
177
|
// V3 知识条目
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
178
|
+
autosnippet_submit_knowledge: (ctx, args) => knowledgeHandlers.submitKnowledge(ctx, args),
|
|
179
|
+
autosnippet_submit_knowledge_batch: (ctx, args) => knowledgeHandlers.submitKnowledgeBatch(ctx, args),
|
|
180
|
+
autosnippet_knowledge_lifecycle: (ctx, args) => knowledgeHandlers.knowledgeLifecycle(ctx, args),
|
|
181
|
+
autosnippet_save_document: (ctx, args) => knowledgeHandlers.saveDocument(ctx, args),
|
|
182
|
+
};
|
|
183
|
+
return HANDLER_MAP[name] || null;
|
|
168
184
|
}
|
|
169
185
|
|
|
170
186
|
/**
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP 工具统一错误处理
|
|
3
|
+
*
|
|
4
|
+
* 提供 wrapHandler() 包装函数,将所有 handler 的异常统一转换为
|
|
5
|
+
* envelope 格式的错误响应,确保:
|
|
6
|
+
* 1. 已知业务错误 → 结构化 errorCode + message
|
|
7
|
+
* 2. 未知异常 → 通用 INTERNAL_ERROR + 原始 message
|
|
8
|
+
* 3. 一致的 meta.tool + meta.responseTimeMs
|
|
9
|
+
*
|
|
10
|
+
* @module external/mcp/errorHandler
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { envelope } from './envelope.js';
|
|
14
|
+
import {
|
|
15
|
+
ValidationError,
|
|
16
|
+
NotFoundError,
|
|
17
|
+
ConflictError,
|
|
18
|
+
PermissionDenied,
|
|
19
|
+
ConstitutionViolation,
|
|
20
|
+
} from '../../shared/errors/index.js';
|
|
21
|
+
import Logger from '../../infrastructure/logging/Logger.js';
|
|
22
|
+
|
|
23
|
+
const logger = Logger.getInstance();
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 从已知错误类型推断 errorCode
|
|
27
|
+
* @param {Error} err
|
|
28
|
+
* @returns {string}
|
|
29
|
+
*/
|
|
30
|
+
function inferErrorCode(err) {
|
|
31
|
+
if (err instanceof ValidationError) return 'VALIDATION_ERROR';
|
|
32
|
+
if (err instanceof NotFoundError) return 'NOT_FOUND';
|
|
33
|
+
if (err instanceof ConflictError) return 'CONFLICT';
|
|
34
|
+
if (err instanceof PermissionDenied) return 'PERMISSION_DENIED';
|
|
35
|
+
if (err instanceof ConstitutionViolation) return 'CONSTITUTION_VIOLATION';
|
|
36
|
+
if (err.code) return err.code;
|
|
37
|
+
return 'INTERNAL_ERROR';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 包装 MCP handler 函数,提供统一错误处理
|
|
42
|
+
*
|
|
43
|
+
* @param {string} toolName — 工具名(用于 meta.tool)
|
|
44
|
+
* @param {Function} handlerFn — 原始 handler: (ctx, args) => Promise<any>
|
|
45
|
+
* @returns {Function} — 包装后的 handler,保证 *不会* throw
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* import { wrapHandler } from '../errorHandler.js';
|
|
49
|
+
* export const search = wrapHandler('autosnippet_search', async (ctx, args) => {
|
|
50
|
+
* // ... 正常返回 envelope(...)
|
|
51
|
+
* });
|
|
52
|
+
*/
|
|
53
|
+
export function wrapHandler(toolName, handlerFn) {
|
|
54
|
+
return async function wrappedHandler(ctx, args) {
|
|
55
|
+
const t0 = Date.now();
|
|
56
|
+
try {
|
|
57
|
+
return await handlerFn(ctx, args);
|
|
58
|
+
} catch (err) {
|
|
59
|
+
const elapsed = Date.now() - t0;
|
|
60
|
+
const errorCode = inferErrorCode(err);
|
|
61
|
+
const message = err.message || 'Unknown error';
|
|
62
|
+
|
|
63
|
+
logger.error(`[MCP:${toolName}] ${errorCode}: ${message}`, {
|
|
64
|
+
tool: toolName,
|
|
65
|
+
errorCode,
|
|
66
|
+
durationMs: elapsed,
|
|
67
|
+
...(err.details ? { details: err.details } : {}),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return envelope({
|
|
71
|
+
success: false,
|
|
72
|
+
message,
|
|
73
|
+
errorCode,
|
|
74
|
+
meta: {
|
|
75
|
+
tool: toolName,
|
|
76
|
+
responseTimeMs: elapsed,
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 批量包装一个模块的所有 handler 函数
|
|
85
|
+
*
|
|
86
|
+
* @param {string} prefix — 工具名前缀(如 'autosnippet_search')
|
|
87
|
+
* @param {Record<string, Function>} handlersModule — handler 模块 exports
|
|
88
|
+
* @returns {Record<string, Function>} — 包装后的 handlers
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* import * as rawSearchHandlers from './handlers/search.js';
|
|
92
|
+
* const searchHandlers = wrapHandlers('autosnippet', rawSearchHandlers);
|
|
93
|
+
*/
|
|
94
|
+
export function wrapHandlers(prefix, handlersModule) {
|
|
95
|
+
const wrapped = {};
|
|
96
|
+
for (const [key, fn] of Object.entries(handlersModule)) {
|
|
97
|
+
if (typeof fn === 'function') {
|
|
98
|
+
wrapped[key] = wrapHandler(`${prefix}_${key}`, fn);
|
|
99
|
+
} else {
|
|
100
|
+
wrapped[key] = fn; // 非函数属性原样透传
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return wrapped;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export default { wrapHandler, wrapHandlers };
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* 设计:
|
|
8
8
|
* - 缓存粒度: 完整工具调用 (key = toolName + normalized args)
|
|
9
|
-
* - 失效策略:
|
|
9
|
+
* - 失效策略: TTL 过期 + 手动清理 (默认 30 分钟)
|
|
10
10
|
* - 内存管理: 文件内容缓存上限 200 个,搜索结果上限 500 个
|
|
11
11
|
*
|
|
12
12
|
* 使用方式:
|
|
@@ -17,10 +17,13 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import Logger from '../../../../../infrastructure/logging/Logger.js';
|
|
20
|
+
import { CACHE } from '../../../../../shared/constants.js';
|
|
20
21
|
|
|
21
22
|
/** 最大缓存条目 */
|
|
22
|
-
const MAX_FILE_CACHE =
|
|
23
|
-
const MAX_SEARCH_CACHE =
|
|
23
|
+
const MAX_FILE_CACHE = CACHE.MAX_FILE_ENTRIES;
|
|
24
|
+
const MAX_SEARCH_CACHE = CACHE.MAX_SEARCH_ENTRIES;
|
|
25
|
+
/** 缓存 TTL(毫秒),超过此时间的条目视为过期 */
|
|
26
|
+
const DEFAULT_TTL_MS = CACHE.DEFAULT_TTL_MS;
|
|
24
27
|
|
|
25
28
|
export class ToolResultCache {
|
|
26
29
|
/** @type {Map<string, {result: any, cachedAt: number, hitCount: number}>} */
|
|
@@ -32,11 +35,31 @@ export class ToolResultCache {
|
|
|
32
35
|
/** @type {import('../../../../../lib/infrastructure/logging/Logger.js').default} */
|
|
33
36
|
#logger;
|
|
34
37
|
|
|
35
|
-
/** @type {{hits: number, misses: number}} */
|
|
36
|
-
#stats = { hits: 0, misses: 0 };
|
|
38
|
+
/** @type {{hits: number, misses: number, evictions: number}} */
|
|
39
|
+
#stats = { hits: 0, misses: 0, evictions: 0 };
|
|
37
40
|
|
|
38
|
-
|
|
41
|
+
/** @type {number} TTL in ms */
|
|
42
|
+
#ttlMs;
|
|
43
|
+
|
|
44
|
+
/** @type {ReturnType<typeof setInterval>|null} cleanup timer */
|
|
45
|
+
#cleanupTimer = null;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {object} [options]
|
|
49
|
+
* @param {number} [options.ttlMs] — 缓存 TTL(毫秒),默认取 CACHE.DEFAULT_TTL_MS
|
|
50
|
+
* @param {number} [options.cleanupIntervalMs] — 清理周期(毫秒),默认 5 分钟
|
|
51
|
+
*/
|
|
52
|
+
constructor(options = {}) {
|
|
39
53
|
this.#logger = Logger.getInstance();
|
|
54
|
+
this.#ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
|
|
55
|
+
|
|
56
|
+
// 定期清理过期条目,避免内存膨胀
|
|
57
|
+
const cleanupInterval = options.cleanupIntervalMs ?? 5 * 60 * 1000;
|
|
58
|
+
if (this.#ttlMs > 0 && cleanupInterval > 0) {
|
|
59
|
+
this.#cleanupTimer = setInterval(() => this.#evictExpired(), cleanupInterval);
|
|
60
|
+
// 允许进程退出时不被 timer 阻塞
|
|
61
|
+
if (this.#cleanupTimer.unref) this.#cleanupTimer.unref();
|
|
62
|
+
}
|
|
40
63
|
}
|
|
41
64
|
|
|
42
65
|
// ─── 搜索结果缓存 ────────────────────────────────────
|
|
@@ -67,6 +90,13 @@ export class ToolResultCache {
|
|
|
67
90
|
getCachedSearch(pattern) {
|
|
68
91
|
const entry = this.#searchCache.get(pattern);
|
|
69
92
|
if (entry) {
|
|
93
|
+
// TTL 检查
|
|
94
|
+
if (this.#ttlMs > 0 && (Date.now() - entry.cachedAt) > this.#ttlMs) {
|
|
95
|
+
this.#searchCache.delete(pattern);
|
|
96
|
+
this.#stats.evictions++;
|
|
97
|
+
this.#stats.misses++;
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
70
100
|
entry.hitCount++;
|
|
71
101
|
this.#stats.hits++;
|
|
72
102
|
return entry.result;
|
|
@@ -102,6 +132,13 @@ export class ToolResultCache {
|
|
|
102
132
|
getCachedFile(filePath) {
|
|
103
133
|
const entry = this.#fileCache.get(filePath);
|
|
104
134
|
if (entry) {
|
|
135
|
+
// TTL 检查
|
|
136
|
+
if (this.#ttlMs > 0 && (Date.now() - entry.cachedAt) > this.#ttlMs) {
|
|
137
|
+
this.#fileCache.delete(filePath);
|
|
138
|
+
this.#stats.evictions++;
|
|
139
|
+
this.#stats.misses++;
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
105
142
|
entry.hitCount++;
|
|
106
143
|
this.#stats.hits++;
|
|
107
144
|
return entry.content;
|
|
@@ -182,7 +219,49 @@ export class ToolResultCache {
|
|
|
182
219
|
clear() {
|
|
183
220
|
this.#searchCache.clear();
|
|
184
221
|
this.#fileCache.clear();
|
|
185
|
-
this.#stats = { hits: 0, misses: 0 };
|
|
222
|
+
this.#stats = { hits: 0, misses: 0, evictions: 0 };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* 销毁缓存实例,释放定时器
|
|
227
|
+
* 应在 Bootstrap 会话结束时调用
|
|
228
|
+
*/
|
|
229
|
+
dispose() {
|
|
230
|
+
this.clear();
|
|
231
|
+
if (this.#cleanupTimer) {
|
|
232
|
+
clearInterval(this.#cleanupTimer);
|
|
233
|
+
this.#cleanupTimer = null;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ─── 内部 ─────────────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* 清理两个 Map 中的过期条目
|
|
241
|
+
* @private
|
|
242
|
+
*/
|
|
243
|
+
#evictExpired() {
|
|
244
|
+
if (this.#ttlMs <= 0) return;
|
|
245
|
+
const now = Date.now();
|
|
246
|
+
let evicted = 0;
|
|
247
|
+
|
|
248
|
+
for (const [key, entry] of this.#searchCache) {
|
|
249
|
+
if ((now - entry.cachedAt) > this.#ttlMs) {
|
|
250
|
+
this.#searchCache.delete(key);
|
|
251
|
+
evicted++;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
for (const [key, entry] of this.#fileCache) {
|
|
255
|
+
if ((now - entry.cachedAt) > this.#ttlMs) {
|
|
256
|
+
this.#fileCache.delete(key);
|
|
257
|
+
evicted++;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (evicted > 0) {
|
|
262
|
+
this.#stats.evictions += evicted;
|
|
263
|
+
this.#logger.debug(`[ToolResultCache] evicted ${evicted} expired entries`);
|
|
264
|
+
}
|
|
186
265
|
}
|
|
187
266
|
}
|
|
188
267
|
|
|
@@ -1108,7 +1108,8 @@ export async function fillDimensionsV3(fillContext) {
|
|
|
1108
1108
|
logger.info(`[Bootstrap-v3] 🚀 Cursor Delivery complete — ` +
|
|
1109
1109
|
`A: ${deliveryResult.channelA.rulesCount} rules, ` +
|
|
1110
1110
|
`B: ${deliveryResult.channelB.topicCount} topics, ` +
|
|
1111
|
-
`C: ${deliveryResult.channelC.synced} skills`
|
|
1111
|
+
`C: ${deliveryResult.channelC.synced} skills, ` +
|
|
1112
|
+
`D: ${deliveryResult.channelD?.documentsCount || 0} documents`);
|
|
1112
1113
|
}
|
|
1113
1114
|
} catch (deliveryErr) {
|
|
1114
1115
|
logger.warn(`[Bootstrap-v3] Cursor Delivery failed (non-blocking): ${deliveryErr.message}`);
|
|
@@ -237,6 +237,80 @@ export async function knowledgeLifecycle(ctx, args) {
|
|
|
237
237
|
|
|
238
238
|
// ─── 内部辅助 ──────────────────────────────────────────────
|
|
239
239
|
|
|
240
|
+
/**
|
|
241
|
+
* 保存开发文档 (autosnippet_save_document)
|
|
242
|
+
*
|
|
243
|
+
* 精简入口:仅需 title + markdown。
|
|
244
|
+
* 自动设置 knowledgeType='dev-document', kind='fact', source='agent'。
|
|
245
|
+
* 不走 RecipeReadiness 检查(文档无需 doClause/trigger)。
|
|
246
|
+
* 支持 autoApprove — 文档直接进入 active 状态。
|
|
247
|
+
*/
|
|
248
|
+
export async function saveDocument(ctx, args) {
|
|
249
|
+
if (!args.title || !args.title.trim()) {
|
|
250
|
+
throw new Error('title 必填');
|
|
251
|
+
}
|
|
252
|
+
if (!args.markdown || !args.markdown.trim()) {
|
|
253
|
+
throw new Error('markdown 必填');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// 限流
|
|
257
|
+
const blocked = await _checkRateLimit('autosnippet_save_document', args.client_id);
|
|
258
|
+
if (blocked) return blocked;
|
|
259
|
+
|
|
260
|
+
const service = ctx.container.get('knowledgeService');
|
|
261
|
+
|
|
262
|
+
const data = {
|
|
263
|
+
title: args.title.trim(),
|
|
264
|
+
description: args.description || '',
|
|
265
|
+
knowledgeType: 'dev-document',
|
|
266
|
+
kind: 'fact',
|
|
267
|
+
source: args.source || 'agent',
|
|
268
|
+
scope: args.scope || 'project-specific',
|
|
269
|
+
tags: args.tags || [],
|
|
270
|
+
content: {
|
|
271
|
+
markdown: args.markdown,
|
|
272
|
+
pattern: '',
|
|
273
|
+
},
|
|
274
|
+
// 文档不需要 Cursor Delivery 字段
|
|
275
|
+
trigger: '',
|
|
276
|
+
doClause: '',
|
|
277
|
+
dontClause: '',
|
|
278
|
+
whenClause: '',
|
|
279
|
+
topicHint: '',
|
|
280
|
+
coreCode: '',
|
|
281
|
+
// 基础推理
|
|
282
|
+
reasoning: {
|
|
283
|
+
whyStandard: 'Agent development document — preserved for team knowledge',
|
|
284
|
+
sources: ['agent'],
|
|
285
|
+
confidence: 0.8,
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const entry = await service.create(data, { userId: 'mcp' });
|
|
290
|
+
|
|
291
|
+
// 自动发布(dev-document 不需要人工审核)
|
|
292
|
+
try {
|
|
293
|
+
await service.publish(entry.id, { userId: 'mcp' });
|
|
294
|
+
} catch {
|
|
295
|
+
// 发布失败保持 pending — 非阻塞
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return envelope({
|
|
299
|
+
success: true,
|
|
300
|
+
data: {
|
|
301
|
+
id: entry.id,
|
|
302
|
+
lifecycle: 'active',
|
|
303
|
+
title: entry.title,
|
|
304
|
+
kind: 'fact',
|
|
305
|
+
knowledgeType: 'dev-document',
|
|
306
|
+
},
|
|
307
|
+
message: `文档「${entry.title}」已保存到知识库。`,
|
|
308
|
+
meta: { tool: 'autosnippet_save_document' },
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ─── 内部辅助 ──────────────────────────────────────────────
|
|
313
|
+
|
|
240
314
|
/**
|
|
241
315
|
* V3 wire format → RecipeReadinessChecker 兼容格式
|
|
242
316
|
*/
|
|
@@ -23,6 +23,7 @@ export const TOOL_GATEWAY_MAP = {
|
|
|
23
23
|
autosnippet_submit_knowledge: { action: 'knowledge:create', resource: 'knowledge' },
|
|
24
24
|
autosnippet_submit_knowledge_batch: { action: 'knowledge:create', resource: 'knowledge' },
|
|
25
25
|
autosnippet_knowledge_lifecycle: { action: 'knowledge:update', resource: 'knowledge' },
|
|
26
|
+
autosnippet_save_document: { action: 'knowledge:create', resource: 'knowledge' },
|
|
26
27
|
};
|
|
27
28
|
|
|
28
29
|
export const TOOLS = [
|
|
@@ -751,4 +752,26 @@ export const TOOLS = [
|
|
|
751
752
|
required: ['id', 'action'],
|
|
752
753
|
},
|
|
753
754
|
},
|
|
755
|
+
// 39. 保存开发文档
|
|
756
|
+
{
|
|
757
|
+
name: 'autosnippet_save_document',
|
|
758
|
+
description:
|
|
759
|
+
'保存开发文档到知识库(架构设计、排查报告、决策记录、调研笔记等)。\n' +
|
|
760
|
+
'仅需 title + markdown,无需填写 kind/doClause/trigger 等 Cursor Delivery 字段。\n' +
|
|
761
|
+
'文档自动以 dev-document 类型存储,不参与 Cursor Rules 压缩,保持原文完整性。\n' +
|
|
762
|
+
'交付路径: Channel D → .cursor/skills/autosnippet-devdocs/references/*.md。\n' +
|
|
763
|
+
'Agent 可通过 autosnippet_search 全文检索已保存的文档。',
|
|
764
|
+
inputSchema: {
|
|
765
|
+
type: 'object',
|
|
766
|
+
properties: {
|
|
767
|
+
title: { type: 'string', description: '文档标题(中英文皆可)' },
|
|
768
|
+
markdown: { type: 'string', description: '文档 Markdown 全文' },
|
|
769
|
+
description: { type: 'string', description: '一句话摘要(可选)' },
|
|
770
|
+
tags: { type: 'array', items: { type: 'string' }, description: '标签列表,如 ["adr", "debug-report", "design-doc", "research"]' },
|
|
771
|
+
scope: { type: 'string', enum: ['universal', 'project-specific'], default: 'project-specific', description: '适用范围' },
|
|
772
|
+
source: { type: 'string', description: '来源标识(默认 agent)' },
|
|
773
|
+
},
|
|
774
|
+
required: ['title', 'markdown'],
|
|
775
|
+
},
|
|
776
|
+
},
|
|
754
777
|
];
|
package/lib/http/HttpServer.js
CHANGED
|
@@ -26,6 +26,7 @@ import violationsRouter from './routes/violations.js';
|
|
|
26
26
|
import authRouter from './routes/auth.js';
|
|
27
27
|
import skillsRouter from './routes/skills.js';
|
|
28
28
|
import knowledgeRouter from './routes/knowledge.js';
|
|
29
|
+
import recipesRouter from './routes/recipes.js';
|
|
29
30
|
import apiSpec from './api-spec.js';
|
|
30
31
|
import { initRedisService } from '../infrastructure/cache/RedisService.js';
|
|
31
32
|
import { initCacheAdapter } from '../infrastructure/cache/UnifiedCacheAdapter.js';
|
|
@@ -264,6 +265,9 @@ export class HttpServer {
|
|
|
264
265
|
// 知识条目路由 (V3)
|
|
265
266
|
this.app.use(`${apiPrefix}/knowledge`, knowledgeRouter);
|
|
266
267
|
|
|
268
|
+
// Recipe 操作路由(关系发现等)
|
|
269
|
+
this.app.use(`${apiPrefix}/recipes`, recipesRouter);
|
|
270
|
+
|
|
267
271
|
// 根路径 — 返回 API 元信息(避免外部探测产生无意义 404)
|
|
268
272
|
this.app.all('/', (req, res) => {
|
|
269
273
|
res.json({
|
|
@@ -5,24 +5,41 @@
|
|
|
5
5
|
* 精简策略:
|
|
6
6
|
* - GET 请求 + 2xx 状态码: 降为 debug(Dashboard 轮询高频噪音)
|
|
7
7
|
* - 非 GET / 非 2xx / 慢请求(>2s): 保留 info 级别
|
|
8
|
+
*
|
|
9
|
+
* ⚠️ 重要: 使用 req.originalUrl 而非 req.path。
|
|
10
|
+
* Express 4 子路由器 (app.use('/api/v1/x', router)) 会在执行
|
|
11
|
+
* handler 期间临时修改 req.url / req.path 为相对路径 (e.g. '/')。
|
|
12
|
+
* 当 res.on('finish') 同步触发时 req.url 尚未恢复,导致日志中
|
|
13
|
+
* 所有子路由请求都显示为 'GET / ...',SILENT_PATHS 匹配也失效。
|
|
14
|
+
* req.originalUrl 始终保持请求的原始 URL,不受路由挂载影响。
|
|
8
15
|
*/
|
|
9
16
|
|
|
10
17
|
// 轮询/心跳路径 — 完全静默
|
|
11
|
-
const SILENT_PATHS = ['/api/health', '/api/realtime/events', '/api/sse', '/socket.io'];
|
|
18
|
+
const SILENT_PATHS = ['/api/v1/health', '/api/health', '/api/realtime/events', '/api/sse', '/socket.io'];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 从 originalUrl 中提取 pathname(去除 query string)
|
|
22
|
+
*/
|
|
23
|
+
function extractPath(originalUrl) {
|
|
24
|
+
const idx = originalUrl.indexOf('?');
|
|
25
|
+
return idx === -1 ? originalUrl : originalUrl.slice(0, idx);
|
|
26
|
+
}
|
|
12
27
|
|
|
13
28
|
export function requestLogger(logger) {
|
|
14
29
|
return (req, res, next) => {
|
|
15
30
|
const startTime = Date.now();
|
|
31
|
+
// 在中间件进入时捕获 originalUrl — 此值不会被 Express 路由修改
|
|
32
|
+
const originalPath = extractPath(req.originalUrl);
|
|
16
33
|
|
|
17
34
|
res.on('finish', () => {
|
|
18
35
|
const duration = Date.now() - startTime;
|
|
19
36
|
|
|
20
37
|
// 完全静默的路径
|
|
21
|
-
if (SILENT_PATHS.some(p =>
|
|
38
|
+
if (SILENT_PATHS.some(p => originalPath.startsWith(p))) return;
|
|
22
39
|
|
|
23
40
|
const logData = {
|
|
24
41
|
method: req.method,
|
|
25
|
-
path:
|
|
42
|
+
path: originalPath,
|
|
26
43
|
statusCode: res.statusCode,
|
|
27
44
|
duration: `${duration}ms`,
|
|
28
45
|
};
|
|
@@ -31,7 +48,7 @@ export function requestLogger(logger) {
|
|
|
31
48
|
const isNoisy = req.method === 'GET' && (res.statusCode >= 200 && res.statusCode < 300 || res.statusCode === 304) && duration < 2000;
|
|
32
49
|
const isSlow = duration >= 1000;
|
|
33
50
|
if (isSlow) {
|
|
34
|
-
logger.warn(`🐌慢请求: ${req.method} ${
|
|
51
|
+
logger.warn(`🐌慢请求: ${req.method} ${originalPath} - ${duration}ms`, logData);
|
|
35
52
|
} else if (isNoisy) {
|
|
36
53
|
logger.debug('HTTP', logData);
|
|
37
54
|
} else {
|