autosnippet 2.14.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/lib/external/mcp/McpServer.js +58 -43
- package/lib/external/mcp/errorHandler.js +106 -0
- package/lib/external/mcp/handlers/bootstrap/pipeline/ToolResultCache.js +86 -7
- 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/cursor/CursorDeliveryPipeline.js +7 -6
- 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
|
@@ -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,56 +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
|
-
|
|
168
|
-
|
|
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;
|
|
169
184
|
}
|
|
170
185
|
|
|
171
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
|
|
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 {
|
|
@@ -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
|
/**
|
|
@@ -15,6 +15,7 @@ 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';
|
|
19
20
|
import fs from 'node:fs';
|
|
20
21
|
|
|
@@ -127,10 +128,10 @@ export class CursorDeliveryPipeline {
|
|
|
127
128
|
{ page: 1, pageSize: 200 }
|
|
128
129
|
);
|
|
129
130
|
const pendingItems = this._extractItems(pending);
|
|
130
|
-
// 过滤高置信度 pending(quality.confidence >=
|
|
131
|
+
// 过滤高置信度 pending(quality.confidence >= PENDING_MIN 或无 quality 字段)
|
|
131
132
|
const highConfPending = pendingItems.filter(e => {
|
|
132
133
|
const conf = e.quality?.confidence;
|
|
133
|
-
return conf === undefined || conf === null || conf >=
|
|
134
|
+
return conf === undefined || conf === null || conf >= KNOWLEDGE_CONFIDENCE.PENDING_MIN;
|
|
134
135
|
});
|
|
135
136
|
allEntries.push(...highConfPending);
|
|
136
137
|
} catch (e) {
|
|
@@ -190,10 +191,10 @@ export class CursorDeliveryPipeline {
|
|
|
190
191
|
*/
|
|
191
192
|
_rankScore(entry) {
|
|
192
193
|
let score = 0;
|
|
193
|
-
score += (entry.quality?.confidence ||
|
|
194
|
-
score += (entry.quality?.authorityScore || 0) *
|
|
195
|
-
score += Math.min(entry.stats?.useCount || 0,
|
|
196
|
-
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;
|
|
197
198
|
return score;
|
|
198
199
|
}
|
|
199
200
|
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import Logger from '../../infrastructure/logging/Logger.js';
|
|
16
16
|
import { collectSourceFilesWithContent } from './SourceFileCollector.js';
|
|
17
|
+
import { COMPLIANCE_SCORING, QUALITY_GATE } from '../../shared/constants.js';
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Quality Gate 评分算法
|
|
@@ -21,20 +22,20 @@ import { collectSourceFilesWithContent } from './SourceFileCollector.js';
|
|
|
21
22
|
function computeScore(summary, ruleHealth = []) {
|
|
22
23
|
let score = 100;
|
|
23
24
|
|
|
24
|
-
// 扣分:每个 error
|
|
25
|
-
score -= summary.errors *
|
|
26
|
-
score -= summary.warnings *
|
|
27
|
-
score -= (summary.infos || 0) *
|
|
25
|
+
// 扣分:每个 error/warning/info 按常量权重扣分
|
|
26
|
+
score -= summary.errors * COMPLIANCE_SCORING.ERROR_PENALTY;
|
|
27
|
+
score -= summary.warnings * COMPLIANCE_SCORING.WARNING_PENALTY;
|
|
28
|
+
score -= (summary.infos || 0) * COMPLIANCE_SCORING.INFO_PENALTY;
|
|
28
29
|
|
|
29
|
-
// 加分:规则平均 F1 >
|
|
30
|
+
// 加分:规则平均 F1 > 阈值加分
|
|
30
31
|
if (ruleHealth.length > 0) {
|
|
31
32
|
const avgF1 = ruleHealth.reduce((s, r) => s + (r.f1 || 0), 0) / ruleHealth.length;
|
|
32
|
-
if (avgF1 >
|
|
33
|
+
if (avgF1 > COMPLIANCE_SCORING.HIGH_F1_THRESHOLD) score += COMPLIANCE_SCORING.HIGH_F1_BONUS;
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
//
|
|
36
|
-
const problematic = ruleHealth.filter(r => (r.precision || 1) <
|
|
37
|
-
score -= problematic.length *
|
|
36
|
+
// 扣分:高误报规则每条扣分
|
|
37
|
+
const problematic = ruleHealth.filter(r => (r.precision || 1) < COMPLIANCE_SCORING.LOW_PRECISION_THRESHOLD);
|
|
38
|
+
score -= problematic.length * COMPLIANCE_SCORING.PROBLEMATIC_RULE_PENALTY;
|
|
38
39
|
|
|
39
40
|
return Math.max(0, Math.min(100, Math.round(score)));
|
|
40
41
|
}
|
|
@@ -43,7 +44,7 @@ function computeScore(summary, ruleHealth = []) {
|
|
|
43
44
|
* 判定 Quality Gate 状态
|
|
44
45
|
*/
|
|
45
46
|
function evaluateGate(summary, score, thresholds) {
|
|
46
|
-
const { maxErrors =
|
|
47
|
+
const { maxErrors = QUALITY_GATE.MAX_ERRORS, maxWarnings = QUALITY_GATE.MAX_WARNINGS, minScore = QUALITY_GATE.MIN_SCORE } = thresholds;
|
|
47
48
|
|
|
48
49
|
if (summary.errors > maxErrors) return 'FAIL';
|
|
49
50
|
if (score < minScore) return 'FAIL';
|
|
@@ -65,9 +66,9 @@ export class ComplianceReporter {
|
|
|
65
66
|
this.ruleLearner = ruleLearner;
|
|
66
67
|
this.exclusionManager = exclusionManager;
|
|
67
68
|
this.qualityGateConfig = {
|
|
68
|
-
maxErrors:
|
|
69
|
-
maxWarnings:
|
|
70
|
-
minScore:
|
|
69
|
+
maxErrors: QUALITY_GATE.MAX_ERRORS,
|
|
70
|
+
maxWarnings: QUALITY_GATE.MAX_WARNINGS,
|
|
71
|
+
minScore: QUALITY_GATE.MIN_SCORE,
|
|
71
72
|
...qualityGateConfig,
|
|
72
73
|
};
|
|
73
74
|
this.logger = Logger.getInstance();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Logger from '../../infrastructure/logging/Logger.js';
|
|
2
2
|
import { ValidationError, ConflictError, NotFoundError } from '../../shared/errors/index.js';
|
|
3
|
+
import { unixNow } from '../../shared/utils/common.js';
|
|
3
4
|
import { v4 as uuidv4 } from 'uuid';
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -65,7 +66,7 @@ export class GuardService {
|
|
|
65
66
|
resourceId: created.id,
|
|
66
67
|
actor: context.userId,
|
|
67
68
|
details: `Created guard rule: ${data.name}`,
|
|
68
|
-
timestamp:
|
|
69
|
+
timestamp: unixNow(),
|
|
69
70
|
});
|
|
70
71
|
|
|
71
72
|
return created;
|
|
@@ -94,7 +95,7 @@ export class GuardService {
|
|
|
94
95
|
resourceId: ruleId,
|
|
95
96
|
actor: context.userId,
|
|
96
97
|
details: `Enabled guard rule: ${entry.title}`,
|
|
97
|
-
timestamp:
|
|
98
|
+
timestamp: unixNow(),
|
|
98
99
|
});
|
|
99
100
|
|
|
100
101
|
return this.knowledgeRepository.findById(ruleId);
|
|
@@ -130,7 +131,7 @@ export class GuardService {
|
|
|
130
131
|
resourceId: ruleId,
|
|
131
132
|
actor: context.userId,
|
|
132
133
|
details: `Disabled guard rule: ${reason}`,
|
|
133
|
-
timestamp:
|
|
134
|
+
timestamp: unixNow(),
|
|
134
135
|
});
|
|
135
136
|
|
|
136
137
|
return this.knowledgeRepository.findById(ruleId);
|
|
@@ -8,8 +8,12 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from '
|
|
|
8
8
|
import { join, dirname } from 'node:path';
|
|
9
9
|
import Logger from '../../infrastructure/logging/Logger.js';
|
|
10
10
|
import pathGuard from '../../shared/PathGuard.js';
|
|
11
|
+
import { RULE_LEARNER } from '../../shared/constants.js';
|
|
11
12
|
|
|
12
|
-
const PROBLEMATIC_THRESHOLD = {
|
|
13
|
+
const PROBLEMATIC_THRESHOLD = {
|
|
14
|
+
falsePositiveRate: RULE_LEARNER.PROBLEMATIC_FALSE_POSITIVE_RATE,
|
|
15
|
+
minTriggers: RULE_LEARNER.PROBLEMATIC_MIN_TRIGGERS,
|
|
16
|
+
};
|
|
13
17
|
|
|
14
18
|
export class RuleLearner {
|
|
15
19
|
#learnerPath;
|
|
@@ -142,7 +146,7 @@ export class RuleLearner {
|
|
|
142
146
|
type: 'tune_existing',
|
|
143
147
|
ruleId: p.ruleId,
|
|
144
148
|
message: `规则 ${p.ruleId} 误报率 ${(p.metrics.falsePositiveRate * 100).toFixed(0)}%,建议调整正则或收窄语言范围`,
|
|
145
|
-
confidence:
|
|
149
|
+
confidence: RULE_LEARNER.CONFIDENCE_TUNE,
|
|
146
150
|
evidence: p.metrics,
|
|
147
151
|
});
|
|
148
152
|
} else if (p.recommendation === 'disable') {
|
|
@@ -150,7 +154,7 @@ export class RuleLearner {
|
|
|
150
154
|
type: 'disable',
|
|
151
155
|
ruleId: p.ruleId,
|
|
152
156
|
message: `规则 ${p.ruleId} 误报率 ${(p.metrics.falsePositiveRate * 100).toFixed(0)}%,建议禁用`,
|
|
153
|
-
confidence:
|
|
157
|
+
confidence: RULE_LEARNER.CONFIDENCE_DISABLE,
|
|
154
158
|
evidence: p.metrics,
|
|
155
159
|
});
|
|
156
160
|
}
|
|
@@ -159,12 +163,12 @@ export class RuleLearner {
|
|
|
159
163
|
// 策略 2: 高触发 + 高精度内置规则 → 建议创建项目定制版
|
|
160
164
|
const allStats = this.getAllStats();
|
|
161
165
|
for (const [ruleId, stat] of Object.entries(allStats)) {
|
|
162
|
-
if (stat.triggers >
|
|
166
|
+
if (stat.triggers > RULE_LEARNER.HIGH_TRIGGER_COUNT && (stat.metrics?.precision ?? 1) > RULE_LEARNER.HIGH_PRECISION) {
|
|
163
167
|
suggestions.push({
|
|
164
168
|
type: 'specialize',
|
|
165
169
|
ruleId,
|
|
166
170
|
message: `规则 ${ruleId} 触发 ${stat.triggers} 次且精准度高 (${((stat.metrics?.precision ?? 1) * 100).toFixed(0)}%),建议创建项目定制版本`,
|
|
167
|
-
confidence:
|
|
171
|
+
confidence: RULE_LEARNER.CONFIDENCE_SPECIALIZE,
|
|
168
172
|
evidence: stat.metrics,
|
|
169
173
|
});
|
|
170
174
|
}
|
|
@@ -174,12 +178,12 @@ export class RuleLearner {
|
|
|
174
178
|
for (const [ruleId, stat] of Object.entries(allStats)) {
|
|
175
179
|
if (stat.triggers === 0 && stat.lastTriggered) {
|
|
176
180
|
const daysSinceLastTrigger = (Date.now() - new Date(stat.lastTriggered).getTime()) / 86400000;
|
|
177
|
-
if (daysSinceLastTrigger >
|
|
181
|
+
if (daysSinceLastTrigger > RULE_LEARNER.UNUSED_DAYS_THRESHOLD) {
|
|
178
182
|
suggestions.push({
|
|
179
183
|
type: 'review_unused',
|
|
180
184
|
ruleId,
|
|
181
|
-
message: `规则 ${ruleId} 超过
|
|
182
|
-
confidence:
|
|
185
|
+
message: `规则 ${ruleId} 超过 ${RULE_LEARNER.UNUSED_DAYS_THRESHOLD} 天未触发,建议审查是否仍需保留`,
|
|
186
|
+
confidence: RULE_LEARNER.CONFIDENCE_REVIEW,
|
|
183
187
|
evidence: { daysSinceLastTrigger: Math.round(daysSinceLastTrigger), triggers: stat.triggers },
|
|
184
188
|
});
|
|
185
189
|
}
|
|
@@ -218,7 +222,7 @@ export class RuleLearner {
|
|
|
218
222
|
const metrics = this.getMetrics(ruleId);
|
|
219
223
|
|
|
220
224
|
// 14 天后判定
|
|
221
|
-
if (metrics.precision <
|
|
225
|
+
if (metrics.precision < RULE_LEARNER.LOW_PRECISION && stat.triggers >= PROBLEMATIC_THRESHOLD.minTriggers) {
|
|
222
226
|
return {
|
|
223
227
|
status: 'ineffective',
|
|
224
228
|
triggers: stat.triggers,
|
|
@@ -4,13 +4,9 @@
|
|
|
4
4
|
* 每个维度评分 0-1,加权求和
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
codeQuality: 0.25,
|
|
11
|
-
metadata: 0.15,
|
|
12
|
-
engagement: 0.00,
|
|
13
|
-
};
|
|
7
|
+
import { QUALITY_WEIGHTS, QUALITY_GRADES, CODE_LENGTH } from '../../shared/constants.js';
|
|
8
|
+
|
|
9
|
+
const DEFAULT_WEIGHTS = QUALITY_WEIGHTS;
|
|
14
10
|
|
|
15
11
|
export class QualityScorer {
|
|
16
12
|
#weights;
|
|
@@ -102,9 +98,9 @@ export class QualityScorer {
|
|
|
102
98
|
let s = 0;
|
|
103
99
|
const code = r.code;
|
|
104
100
|
|
|
105
|
-
// 长度适中
|
|
106
|
-
if (code.length >=
|
|
107
|
-
else if (code.length >
|
|
101
|
+
// 长度适中
|
|
102
|
+
if (code.length >= CODE_LENGTH.MIN && code.length <= CODE_LENGTH.MAX) s += 0.3;
|
|
103
|
+
else if (code.length > CODE_LENGTH.MAX) s += 0.15;
|
|
108
104
|
|
|
109
105
|
// 无 TODO/FIXME/HACK
|
|
110
106
|
if (!/\b(TODO|FIXME|HACK|XXX)\b/.test(code)) s += 0.2;
|
|
@@ -144,10 +140,10 @@ export class QualityScorer {
|
|
|
144
140
|
* 分数转等级
|
|
145
141
|
*/
|
|
146
142
|
#toGrade(score) {
|
|
147
|
-
if (score >=
|
|
148
|
-
if (score >=
|
|
149
|
-
if (score >=
|
|
150
|
-
if (score >=
|
|
143
|
+
if (score >= QUALITY_GRADES.A) return 'A';
|
|
144
|
+
if (score >= QUALITY_GRADES.B) return 'B';
|
|
145
|
+
if (score >= QUALITY_GRADES.C) return 'C';
|
|
146
|
+
if (score >= QUALITY_GRADES.D) return 'D';
|
|
151
147
|
return 'F';
|
|
152
148
|
}
|
|
153
149
|
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 全局常量注册表 — 集中管理所有魔法数字和阈值
|
|
3
|
+
*
|
|
4
|
+
* 取代散落在各模块中的硬编码数字,方便统一调参和文档化。
|
|
5
|
+
*
|
|
6
|
+
* @module shared/constants
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// ─── 质量评分 ───────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
/** QualityScorer 维度权重 */
|
|
12
|
+
export const QUALITY_WEIGHTS = Object.freeze({
|
|
13
|
+
completeness: 0.35,
|
|
14
|
+
format: 0.25,
|
|
15
|
+
codeQuality: 0.25,
|
|
16
|
+
metadata: 0.15,
|
|
17
|
+
engagement: 0.00,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
/** QualityScorer 等级分界线 */
|
|
21
|
+
export const QUALITY_GRADES = Object.freeze({
|
|
22
|
+
A: 0.9,
|
|
23
|
+
B: 0.75,
|
|
24
|
+
C: 0.6,
|
|
25
|
+
D: 0.4,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
/** 代码质量评估 — 合理长度范围 */
|
|
29
|
+
export const CODE_LENGTH = Object.freeze({
|
|
30
|
+
MIN: 10,
|
|
31
|
+
MAX: 5000,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// ─── Guard 规则学习 ──────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
/** RuleLearner 规则健康阈值 */
|
|
37
|
+
export const RULE_LEARNER = Object.freeze({
|
|
38
|
+
/** 触发高误报规则的条件 */
|
|
39
|
+
PROBLEMATIC_FALSE_POSITIVE_RATE: 0.3,
|
|
40
|
+
PROBLEMATIC_MIN_TRIGGERS: 5,
|
|
41
|
+
/** 规则建议置信度 */
|
|
42
|
+
CONFIDENCE_TUNE: 0.7,
|
|
43
|
+
CONFIDENCE_DISABLE: 0.8,
|
|
44
|
+
CONFIDENCE_SPECIALIZE: 0.6,
|
|
45
|
+
CONFIDENCE_REVIEW: 0.4,
|
|
46
|
+
/** 触发数阈值 */
|
|
47
|
+
HIGH_TRIGGER_COUNT: 50,
|
|
48
|
+
HIGH_PRECISION: 0.8,
|
|
49
|
+
/** 闲置天数阈值 */
|
|
50
|
+
UNUSED_DAYS_THRESHOLD: 30,
|
|
51
|
+
/** 精度下限 */
|
|
52
|
+
LOW_PRECISION: 0.5,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// ─── 合规报告 ────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
/** ComplianceReporter 默认 Quality Gate */
|
|
58
|
+
export const QUALITY_GATE = Object.freeze({
|
|
59
|
+
MAX_ERRORS: 0,
|
|
60
|
+
MAX_WARNINGS: 20,
|
|
61
|
+
MIN_SCORE: 70,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
/** ComplianceReporter 扣分权重 */
|
|
65
|
+
export const COMPLIANCE_SCORING = Object.freeze({
|
|
66
|
+
ERROR_PENALTY: 5,
|
|
67
|
+
WARNING_PENALTY: 1,
|
|
68
|
+
INFO_PENALTY: 0.2,
|
|
69
|
+
PROBLEMATIC_RULE_PENALTY: 3,
|
|
70
|
+
HIGH_F1_BONUS: 5,
|
|
71
|
+
HIGH_F1_THRESHOLD: 0.8,
|
|
72
|
+
LOW_PRECISION_THRESHOLD: 0.5,
|
|
73
|
+
MAX_FILES_DEFAULT: 500,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// ─── 知识置信度 ──────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
/** 知识条目默认置信度和阈值 */
|
|
79
|
+
export const KNOWLEDGE_CONFIDENCE = Object.freeze({
|
|
80
|
+
/** 默认 confidence(Reasoning VO) */
|
|
81
|
+
DEFAULT: 0.7,
|
|
82
|
+
/** pending 条目纳入交付的最低 confidence */
|
|
83
|
+
PENDING_MIN: 0.7,
|
|
84
|
+
/** rankScore 中 confidence 缺省值 */
|
|
85
|
+
RANK_DEFAULT: 0.5,
|
|
86
|
+
/** Bootstrap refine 时的 AI 默认 confidence */
|
|
87
|
+
BOOTSTRAP_DEFAULT: 0.6,
|
|
88
|
+
/** 自动提交时的 bootstrap confidence */
|
|
89
|
+
BOOTSTRAP_SUBMIT: 0.8,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// ─── 搜索管线 ────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
/** SearchEngine 配置 */
|
|
95
|
+
export const SEARCH = Object.freeze({
|
|
96
|
+
DEFAULT_LIMIT: 10,
|
|
97
|
+
MAX_RESULTS: 100,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// ─── 文件监听器 ──────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
/** FileWatcher 配置 */
|
|
103
|
+
export const FILE_WATCHER = Object.freeze({
|
|
104
|
+
DEBOUNCE_DELAY_MS: 300,
|
|
105
|
+
STABILITY_THRESHOLD_MS: 500,
|
|
106
|
+
POLL_INTERVAL_MS: 100,
|
|
107
|
+
BINARY_INTERVAL_MS: 300,
|
|
108
|
+
MAX_FILE_SIZE_BYTES: 1024 * 1024,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// ─── AI Provider ─────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
/** AiProvider 熔断配置 */
|
|
114
|
+
export const AI_CIRCUIT_BREAKER = Object.freeze({
|
|
115
|
+
FAILURE_THRESHOLD: 5,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// ─── 缓存 ────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
/** ToolResultCache 配置 */
|
|
121
|
+
export const CACHE = Object.freeze({
|
|
122
|
+
MAX_FILE_ENTRIES: 200,
|
|
123
|
+
MAX_SEARCH_ENTRIES: 500,
|
|
124
|
+
/** 缓存条目默认 TTL(毫秒),0 = 不过期 */
|
|
125
|
+
DEFAULT_TTL_MS: 30 * 60 * 1000, // 30 分钟
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// ─── 性能监控 ────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
/** PerformanceMonitor 配置 */
|
|
131
|
+
export const MONITORING = Object.freeze({
|
|
132
|
+
SLOW_REQUEST_THRESHOLD_MS: 1000,
|
|
133
|
+
ERROR_ALERT_THRESHOLD: 10,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// ─── Cursor 交付 ─────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
/** CursorDeliveryPipeline 排序权重 */
|
|
139
|
+
export const DELIVERY_RANK = Object.freeze({
|
|
140
|
+
CONFIDENCE_WEIGHT: 50,
|
|
141
|
+
AUTHORITY_WEIGHT: 30,
|
|
142
|
+
USE_COUNT_MAX: 10,
|
|
143
|
+
USE_COUNT_WEIGHT: 2,
|
|
144
|
+
ACTIVE_BONUS: 10,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
export default {
|
|
148
|
+
QUALITY_WEIGHTS,
|
|
149
|
+
QUALITY_GRADES,
|
|
150
|
+
CODE_LENGTH,
|
|
151
|
+
RULE_LEARNER,
|
|
152
|
+
QUALITY_GATE,
|
|
153
|
+
COMPLIANCE_SCORING,
|
|
154
|
+
KNOWLEDGE_CONFIDENCE,
|
|
155
|
+
SEARCH,
|
|
156
|
+
FILE_WATCHER,
|
|
157
|
+
AI_CIRCUIT_BREAKER,
|
|
158
|
+
CACHE,
|
|
159
|
+
MONITORING,
|
|
160
|
+
DELIVERY_RANK,
|
|
161
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 通用工具函数 — 消除跨模块重复代码
|
|
3
|
+
*
|
|
4
|
+
* @module shared/utils/common
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ─── JSON 安全解析 ──────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 安全解析 JSON 字符串,失败时返回 fallback
|
|
11
|
+
* 消除各 Repository / handler 中重复的 try-catch JSON.parse
|
|
12
|
+
*
|
|
13
|
+
* @param {string|object|null|undefined} value — 值(可能是 JSON 字符串、对象或空值)
|
|
14
|
+
* @param {*} [fallback=null] — 解析失败时的回退值
|
|
15
|
+
* @returns {*}
|
|
16
|
+
*/
|
|
17
|
+
export function safeJsonParse(value, fallback = null) {
|
|
18
|
+
if (value == null || value === 'null' || value === '') return fallback;
|
|
19
|
+
if (typeof value === 'object') return value;
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(value);
|
|
22
|
+
} catch {
|
|
23
|
+
return fallback;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 安全序列化到 JSON,处理 toJSON() 方法和空值
|
|
29
|
+
* 消除 _entityToRow 中重复的 JSON.stringify 逻辑
|
|
30
|
+
*
|
|
31
|
+
* @param {*} value — 需要序列化的值
|
|
32
|
+
* @param {*} [fallback='{}'] — 值为空时的回退
|
|
33
|
+
* @returns {string}
|
|
34
|
+
*/
|
|
35
|
+
export function safeJsonStringify(value, fallback = '{}') {
|
|
36
|
+
if (value == null) return fallback;
|
|
37
|
+
if (typeof value?.toJSON === 'function') {
|
|
38
|
+
return JSON.stringify(value.toJSON());
|
|
39
|
+
}
|
|
40
|
+
return JSON.stringify(value);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ─── 时间 ────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 返回当前 Unix 时间戳(秒)
|
|
47
|
+
* 消除各处 Math.floor(Date.now() / 1000) 的重复
|
|
48
|
+
*
|
|
49
|
+
* @returns {number}
|
|
50
|
+
*/
|
|
51
|
+
export function unixNow() {
|
|
52
|
+
return Math.floor(Date.now() / 1000);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ─── 安全默认值 ──────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 返回非空字符串值或 fallback
|
|
59
|
+
* @param {*} value
|
|
60
|
+
* @param {string} fallback
|
|
61
|
+
* @returns {string}
|
|
62
|
+
*/
|
|
63
|
+
export function strOr(value, fallback = '') {
|
|
64
|
+
if (typeof value === 'string' && value.trim()) return value;
|
|
65
|
+
return fallback;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export default { safeJsonParse, safeJsonStringify, unixNow, strOr };
|