autosnippet 2.6.0 → 2.7.1
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 +137 -65
- package/bin/api-server.js +5 -0
- package/bin/cli.js +6 -1
- package/dashboard/dist/assets/{icons-rnn04CvH.js → icons-B_Xg4B-s.js} +148 -88
- package/dashboard/dist/assets/index-BjfUm8p9.js +197 -0
- package/dashboard/dist/assets/index-CkIih2CC.css +1 -0
- package/dashboard/dist/assets/{react-markdown-CWxUbOf4.js → react-markdown-BA6FB2NP.js} +1 -1
- package/dashboard/dist/assets/{syntax-highlighter-CJ2drQQb.js → syntax-highlighter-CVLHn9O5.js} +1 -1
- package/dashboard/dist/assets/{vendor-f83ah6cm.js → vendor-BotF760a.js} +61 -61
- package/dashboard/dist/index.html +6 -6
- package/lib/bootstrap.js +18 -1
- package/lib/cli/SetupService.js +86 -8
- package/lib/cli/UpgradeService.js +139 -2
- package/lib/core/ast/ProjectGraph.js +599 -0
- package/lib/core/gateway/GatewayActionRegistry.js +2 -2
- package/lib/domain/recipe/Recipe.js +3 -0
- package/lib/external/ai/AiProvider.js +83 -20
- package/lib/external/ai/providers/ClaudeProvider.js +208 -0
- package/lib/external/ai/providers/GoogleGeminiProvider.js +247 -1
- package/lib/external/ai/providers/OpenAiProvider.js +141 -0
- package/lib/external/mcp/McpServer.js +6 -1
- package/lib/external/mcp/handlers/bootstrap/pipeline/dimension-context.js +216 -0
- package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +657 -0
- package/lib/external/mcp/handlers/bootstrap/pipeline/tier-scheduler.js +160 -0
- package/lib/external/mcp/handlers/bootstrap/skills.js +225 -0
- package/lib/external/mcp/handlers/bootstrap.js +159 -1634
- package/lib/external/mcp/handlers/browse.js +1 -1
- package/lib/external/mcp/handlers/candidate.js +1 -33
- package/lib/external/mcp/handlers/skill.js +58 -17
- package/lib/external/mcp/tools.js +4 -3
- package/lib/http/middleware/requestLogger.js +23 -4
- package/lib/http/routes/ai.js +158 -2
- package/lib/http/routes/auth.js +3 -2
- package/lib/http/routes/candidates.js +49 -25
- package/lib/http/routes/commands.js +0 -8
- package/lib/http/routes/guardRules.js +1 -16
- package/lib/http/routes/recipes.js +4 -17
- package/lib/http/routes/search.js +11 -19
- package/lib/http/routes/skills.js +2 -0
- package/lib/http/routes/snippets.js +0 -33
- package/lib/http/routes/spm.js +37 -63
- package/lib/http/utils/routeHelpers.js +31 -0
- package/lib/infrastructure/config/Paths.js +12 -0
- package/lib/infrastructure/database/DatabaseConnection.js +6 -1
- package/lib/infrastructure/logging/Logger.js +86 -3
- package/lib/infrastructure/realtime/RealtimeService.js +2 -5
- package/lib/infrastructure/vector/JsonVectorAdapter.js +26 -1
- package/lib/injection/ServiceContainer.js +55 -2
- package/lib/service/bootstrap/BootstrapTaskManager.js +400 -0
- package/lib/service/candidate/CandidateFileWriter.js +72 -27
- package/lib/service/candidate/CandidateService.js +156 -10
- package/lib/service/chat/AnalystAgent.js +245 -0
- package/lib/service/chat/CandidateGuardrail.js +134 -0
- package/lib/service/chat/ChatAgent.js +1055 -167
- package/lib/service/chat/ContextWindow.js +730 -0
- package/lib/service/chat/ConversationStore.js +3 -0
- package/lib/service/chat/HandoffProtocol.js +181 -0
- package/lib/service/chat/Memory.js +3 -0
- package/lib/service/chat/ProducerAgent.js +293 -0
- package/lib/service/chat/ToolRegistry.js +149 -5
- package/lib/service/chat/tools.js +1404 -61
- package/lib/service/guard/ExclusionManager.js +2 -0
- package/lib/service/guard/RuleLearner.js +2 -0
- package/lib/service/quality/FeedbackCollector.js +2 -0
- package/lib/service/recipe/RecipeFileWriter.js +16 -1
- package/lib/service/recipe/RecipeStatsTracker.js +2 -0
- package/lib/service/skills/SignalCollector.js +33 -6
- package/lib/service/skills/SkillAdvisor.js +2 -1
- package/lib/service/skills/SkillHooks.js +13 -5
- package/lib/service/spm/SpmService.js +2 -2
- package/lib/shared/PathGuard.js +314 -0
- package/package.json +1 -1
- package/resources/native-ui/combined-window.swift +494 -0
- package/templates/copilot-instructions.md +20 -3
- package/templates/cursor-rules/autosnippet-conventions.mdc +21 -4
- package/templates/cursor-rules/autosnippet-skills.mdc +45 -0
- package/dashboard/dist/assets/index-BBKa3Dgi.js +0 -195
- package/dashboard/dist/assets/index-DLsECfzW.css +0 -1
|
@@ -8,27 +8,12 @@ import { asyncHandler } from '../middleware/errorHandler.js';
|
|
|
8
8
|
import { getServiceContainer } from '../../injection/ServiceContainer.js';
|
|
9
9
|
import { NotFoundError, ValidationError } from '../../shared/errors/index.js';
|
|
10
10
|
import Logger from '../../infrastructure/logging/Logger.js';
|
|
11
|
+
import { getContext, safeInt } from '../utils/routeHelpers.js';
|
|
11
12
|
|
|
12
13
|
const logger = Logger.getInstance();
|
|
13
14
|
|
|
14
15
|
const router = express.Router();
|
|
15
16
|
|
|
16
|
-
/** 从请求中提取操作上下文 */
|
|
17
|
-
function getContext(req) {
|
|
18
|
-
return {
|
|
19
|
-
userId: req.headers['x-user-id'] || 'anonymous',
|
|
20
|
-
ip: req.ip,
|
|
21
|
-
userAgent: req.headers['user-agent'] || '',
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** 安全的整数解析 */
|
|
26
|
-
function safeInt(value, defaultValue, min = 1, max = 1000) {
|
|
27
|
-
const parsed = parseInt(value, 10);
|
|
28
|
-
if (Number.isNaN(parsed)) return defaultValue;
|
|
29
|
-
return Math.max(min, Math.min(max, parsed));
|
|
30
|
-
}
|
|
31
|
-
|
|
32
17
|
/**
|
|
33
18
|
* GET /api/v1/recipes
|
|
34
19
|
* 获取 Recipe 列表(支持筛选和分页)
|
|
@@ -188,7 +173,9 @@ router.patch('/:id/publish', asyncHandler(async (req, res) => {
|
|
|
188
173
|
});
|
|
189
174
|
}
|
|
190
175
|
}
|
|
191
|
-
} catch {
|
|
176
|
+
} catch (err) {
|
|
177
|
+
logger.warn('Agent 不可用,跳过 discover_relations', { error: err.message });
|
|
178
|
+
}
|
|
192
179
|
|
|
193
180
|
res.json({ success: true, data: result.data, requestId: result.requestId });
|
|
194
181
|
}));
|
|
@@ -7,14 +7,11 @@ import express from 'express';
|
|
|
7
7
|
import { asyncHandler } from '../middleware/errorHandler.js';
|
|
8
8
|
import { getServiceContainer } from '../../injection/ServiceContainer.js';
|
|
9
9
|
import { ValidationError } from '../../shared/errors/index.js';
|
|
10
|
+
import Logger from '../../infrastructure/logging/Logger.js';
|
|
11
|
+
import { safeInt } from '../utils/routeHelpers.js';
|
|
10
12
|
|
|
11
13
|
const router = express.Router();
|
|
12
|
-
|
|
13
|
-
function safeInt(value, defaultValue, min = 1, max = 1000) {
|
|
14
|
-
const parsed = parseInt(value, 10);
|
|
15
|
-
if (Number.isNaN(parsed)) return defaultValue;
|
|
16
|
-
return Math.max(min, Math.min(max, parsed));
|
|
17
|
-
}
|
|
14
|
+
const logger = Logger.getInstance();
|
|
18
15
|
|
|
19
16
|
/**
|
|
20
17
|
* GET /api/v1/search
|
|
@@ -39,8 +36,8 @@ router.get('/', asyncHandler(async (req, res) => {
|
|
|
39
36
|
const searchEngine = container.get('searchEngine');
|
|
40
37
|
const result = await searchEngine.search(q, { type, limit, mode, groupByKind });
|
|
41
38
|
return res.json({ success: true, data: result });
|
|
42
|
-
} catch {
|
|
43
|
-
|
|
39
|
+
} catch (err) {
|
|
40
|
+
logger.warn('SearchEngine 搜索失败,降级到传统搜索', { mode, error: err.message });
|
|
44
41
|
}
|
|
45
42
|
}
|
|
46
43
|
|
|
@@ -52,7 +49,8 @@ router.get('/', asyncHandler(async (req, res) => {
|
|
|
52
49
|
try {
|
|
53
50
|
const recipeService = container.get('recipeService');
|
|
54
51
|
results.recipes = await recipeService.searchRecipes(q, pagination);
|
|
55
|
-
} catch {
|
|
52
|
+
} catch (err) {
|
|
53
|
+
logger.warn('Recipe 搜索失败', { query: q, error: err.message });
|
|
56
54
|
results.recipes = { items: [], total: 0 };
|
|
57
55
|
}
|
|
58
56
|
}
|
|
@@ -62,7 +60,8 @@ router.get('/', asyncHandler(async (req, res) => {
|
|
|
62
60
|
try {
|
|
63
61
|
const guardService = container.get('guardService');
|
|
64
62
|
results.rules = await guardService.searchRules(q, pagination);
|
|
65
|
-
} catch {
|
|
63
|
+
} catch (err) {
|
|
64
|
+
logger.warn('Guard Rule 搜索失败', { query: q, error: err.message });
|
|
66
65
|
results.rules = { items: [], total: 0 };
|
|
67
66
|
}
|
|
68
67
|
}
|
|
@@ -72,7 +71,8 @@ router.get('/', asyncHandler(async (req, res) => {
|
|
|
72
71
|
try {
|
|
73
72
|
const candidateService = container.get('candidateService');
|
|
74
73
|
results.candidates = await candidateService.searchCandidates(q, pagination);
|
|
75
|
-
} catch {
|
|
74
|
+
} catch (err) {
|
|
75
|
+
logger.warn('Candidate 搜索失败', { query: q, error: err.message });
|
|
76
76
|
results.candidates = { items: [], total: 0 };
|
|
77
77
|
}
|
|
78
78
|
}
|
|
@@ -216,14 +216,6 @@ router.get('/graph/stats', asyncHandler(async (req, res) => {
|
|
|
216
216
|
res.json({ success: true, data: stats });
|
|
217
217
|
}));
|
|
218
218
|
|
|
219
|
-
/**
|
|
220
|
-
* POST /api/v1/search/trigger-from-code
|
|
221
|
-
* Xcode trigger 搜索模拟 (stub — 功能未完整实现)
|
|
222
|
-
*/
|
|
223
|
-
router.post('/trigger-from-code', asyncHandler(async (req, res) => {
|
|
224
|
-
res.json({ success: true, data: { results: [], total: 0, triggered: false } });
|
|
225
|
-
}));
|
|
226
|
-
|
|
227
219
|
/**
|
|
228
220
|
* POST /api/v1/search/context-aware
|
|
229
221
|
* 上下文感知搜索
|
|
@@ -40,6 +40,8 @@ router.get('/signal-status', asyncHandler(async (_req, res) => {
|
|
|
40
40
|
running: true,
|
|
41
41
|
mode: _signalCollector.getMode(),
|
|
42
42
|
snapshot: _signalCollector.getSnapshot(),
|
|
43
|
+
// 返回 AI 的待处理建议,前端可直接展示
|
|
44
|
+
suggestions: (_signalCollector.getSnapshot().pendingSuggestions || []),
|
|
43
45
|
},
|
|
44
46
|
});
|
|
45
47
|
}));
|
|
@@ -51,37 +51,4 @@ router.get('/:id', asyncHandler(async (req, res) => {
|
|
|
51
51
|
res.json({ success: true, data: snippet });
|
|
52
52
|
}));
|
|
53
53
|
|
|
54
|
-
/**
|
|
55
|
-
* POST /api/v1/snippets/install
|
|
56
|
-
* 从全部 Recipe 生成并同步到 Xcode
|
|
57
|
-
*/
|
|
58
|
-
router.post('/install', asyncHandler(async (req, res) => {
|
|
59
|
-
const container = getServiceContainer();
|
|
60
|
-
const snippetFactory = container.get('snippetFactory');
|
|
61
|
-
const snippetInstaller = container.get('snippetInstaller');
|
|
62
|
-
const recipeRepository = container.get('recipeRepository');
|
|
63
|
-
|
|
64
|
-
// 获取所有活跃 Recipe
|
|
65
|
-
const result = await recipeRepository.findWithPagination(
|
|
66
|
-
{ status: 'active' },
|
|
67
|
-
{ page: 1, pageSize: 9999 },
|
|
68
|
-
);
|
|
69
|
-
const recipes = (result?.data || result?.items || []).map(r => ({
|
|
70
|
-
id: r.id,
|
|
71
|
-
title: r.title,
|
|
72
|
-
trigger: r.trigger,
|
|
73
|
-
code: r.content?.pattern || '',
|
|
74
|
-
description: r.description || r.summaryCn || '',
|
|
75
|
-
language: r.language || 'swift',
|
|
76
|
-
}));
|
|
77
|
-
|
|
78
|
-
// 批量安装
|
|
79
|
-
const installResult = snippetInstaller.installFromRecipes(recipes);
|
|
80
|
-
|
|
81
|
-
res.json({
|
|
82
|
-
success: true,
|
|
83
|
-
data: installResult,
|
|
84
|
-
});
|
|
85
|
-
}));
|
|
86
|
-
|
|
87
54
|
export default router;
|
package/lib/http/routes/spm.js
CHANGED
|
@@ -198,23 +198,24 @@ router.post('/scan-project', asyncHandler(async (req, res) => {
|
|
|
198
198
|
|
|
199
199
|
/**
|
|
200
200
|
* POST /api/v1/spm/bootstrap
|
|
201
|
-
*
|
|
201
|
+
* 冷启动:快速骨架 + 异步逐维度填充(v5)
|
|
202
202
|
*
|
|
203
|
-
*
|
|
204
|
-
* ① 同步阶段:
|
|
205
|
-
* ② 异步阶段:
|
|
203
|
+
* 执行策略:
|
|
204
|
+
* ① 同步阶段: Phase 1-4(文件收集 + AST + SPM + Guard + 骨架响应)→ 立即返回
|
|
205
|
+
* ② 异步阶段: Phase 5/5.5(逐维度提取 + Candidate/Skill 创建)→ 后台逐一执行
|
|
206
|
+
* ③ 进度推送: 通过 Socket.io 实时推送每个维度的完成状态
|
|
206
207
|
*
|
|
207
|
-
*
|
|
208
|
+
* 前端立即获得骨架 + 任务清单,每个维度完成后通过 Socket.io 推送更新。
|
|
208
209
|
*/
|
|
209
210
|
router.post('/bootstrap', asyncHandler(async (req, res) => {
|
|
210
|
-
const { maxFiles, skipGuard, contentMaxLines
|
|
211
|
+
const { maxFiles, skipGuard, contentMaxLines } = req.body || {};
|
|
211
212
|
|
|
212
213
|
const container = getServiceContainer();
|
|
213
214
|
const chatAgent = container.get('chatAgent');
|
|
214
215
|
|
|
215
|
-
logger.info('Bootstrap cold start initiated (
|
|
216
|
+
logger.info('Bootstrap cold start initiated (v5: async fill mode)');
|
|
216
217
|
|
|
217
|
-
// ── 同步阶段:
|
|
218
|
+
// ── 同步阶段: 快速执行 Phase 1-4 → 返回骨架 ──
|
|
218
219
|
const bootstrapResult = await chatAgent.executeTool('bootstrap_knowledge', {
|
|
219
220
|
maxFiles: maxFiles || 500,
|
|
220
221
|
skipGuard: skipGuard || false,
|
|
@@ -222,69 +223,42 @@ router.post('/bootstrap', asyncHandler(async (req, res) => {
|
|
|
222
223
|
loadSkills: true,
|
|
223
224
|
});
|
|
224
225
|
|
|
225
|
-
//
|
|
226
|
-
const responseData = {
|
|
227
|
-
...bootstrapResult,
|
|
228
|
-
aiEnhancement: 'pending', // 告知前端 AI 增强正在后台进行
|
|
229
|
-
};
|
|
230
|
-
|
|
226
|
+
// 立即返回骨架结果给前端
|
|
231
227
|
res.json({
|
|
232
228
|
success: true,
|
|
233
|
-
data:
|
|
229
|
+
data: {
|
|
230
|
+
...bootstrapResult,
|
|
231
|
+
asyncFill: true, // 告知前端:内容正在异步填充中
|
|
232
|
+
},
|
|
234
233
|
});
|
|
235
234
|
|
|
236
|
-
//
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
if (shouldRefine && created > 0) {
|
|
240
|
-
setImmediate(async () => {
|
|
241
|
-
try {
|
|
242
|
-
logger.info(`[Bootstrap] Background AI enhancement starting for ${created} candidates`);
|
|
243
|
-
|
|
244
|
-
// enrich: 结构补齐
|
|
245
|
-
const candidateIds = bootstrapResult?.bootstrapCandidates?.ids
|
|
246
|
-
|| bootstrapResult?.bootstrapCandidates?.candidateIds;
|
|
247
|
-
if (Array.isArray(candidateIds) && candidateIds.length > 0) {
|
|
248
|
-
try {
|
|
249
|
-
await chatAgent.executeTool('enrich_candidate', { candidateIds });
|
|
250
|
-
logger.info(`[Bootstrap] Background enrich done for ${candidateIds.length} candidates`);
|
|
251
|
-
} catch (e) {
|
|
252
|
-
logger.warn(`[Bootstrap] Background enrich failed (non-fatal): ${e.message}`);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// loadSkill + refine: AI 内容润色
|
|
257
|
-
try {
|
|
258
|
-
let userPrompt = '';
|
|
259
|
-
// 加载 Skill 内容
|
|
260
|
-
const loaded = bootstrapResult?.skillsLoaded || [];
|
|
261
|
-
const skillName = loaded.find(s => s.startsWith('autosnippet-reference-')) || 'autosnippet-coldstart';
|
|
262
|
-
try {
|
|
263
|
-
const skillResult = await chatAgent.executeTool('load_skill', { skillName });
|
|
264
|
-
if (skillResult?.content) {
|
|
265
|
-
userPrompt = `请参考以下业界最佳实践标准润色候选,确保 summary 精准、tags 丰富、confidence 合理:\n${skillResult.content.substring(0, 3000)}`;
|
|
266
|
-
}
|
|
267
|
-
} catch { /* skill load failed, proceed without */ }
|
|
268
|
-
|
|
269
|
-
// AST 上下文注入
|
|
270
|
-
if (bootstrapResult?.astContext) {
|
|
271
|
-
userPrompt += `\n\n# 项目代码结构分析 (Tree-sitter AST)\n${bootstrapResult.astContext.substring(0, 2000)}`;
|
|
272
|
-
}
|
|
235
|
+
// 注意:Phase 5/5.5 异步填充已在 bootstrapKnowledge() 内部通过 setImmediate 启动
|
|
236
|
+
// 进度通过 BootstrapTaskManager → Socket.io 推送到前端
|
|
237
|
+
}));
|
|
273
238
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
239
|
+
/**
|
|
240
|
+
* GET /api/v1/spm/bootstrap/status
|
|
241
|
+
* 查询当前 bootstrap 异步填充进度
|
|
242
|
+
*
|
|
243
|
+
* 返回当前 session 的任务状态列表,供前端轮询(Socket.io 不可用时的 fallback)
|
|
244
|
+
*/
|
|
245
|
+
router.get('/bootstrap/status', asyncHandler(async (req, res) => {
|
|
246
|
+
const container = getServiceContainer();
|
|
281
247
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
248
|
+
// 从容器获取 BootstrapTaskManager(正式 DI 注册)
|
|
249
|
+
let taskManager = null;
|
|
250
|
+
try { taskManager = container.get('bootstrapTaskManager'); } catch { /* not registered */ }
|
|
251
|
+
if (!taskManager) {
|
|
252
|
+
return res.json({
|
|
253
|
+
success: true,
|
|
254
|
+
data: { status: 'idle', message: 'No bootstrap task manager initialized' },
|
|
286
255
|
});
|
|
287
256
|
}
|
|
257
|
+
|
|
258
|
+
res.json({
|
|
259
|
+
success: true,
|
|
260
|
+
data: taskManager.getSessionStatus(),
|
|
261
|
+
});
|
|
288
262
|
}));
|
|
289
263
|
|
|
290
264
|
export default router;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 路由共用工具函数
|
|
3
|
+
* 提取自各路由文件中的重复实现
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 从请求中提取操作上下文(用户身份、IP、UA)
|
|
8
|
+
* @param {import('express').Request} req
|
|
9
|
+
* @returns {{ userId: string, ip: string, userAgent: string }}
|
|
10
|
+
*/
|
|
11
|
+
export function getContext(req) {
|
|
12
|
+
return {
|
|
13
|
+
userId: req.headers['x-user-id'] || 'anonymous',
|
|
14
|
+
ip: req.ip,
|
|
15
|
+
userAgent: req.headers['user-agent'] || '',
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 安全的整数解析(带范围约束)
|
|
21
|
+
* @param {*} value 待解析值
|
|
22
|
+
* @param {number} defaultValue 解析失败时的默认值
|
|
23
|
+
* @param {number} [min=1] 最小值
|
|
24
|
+
* @param {number} [max=1000] 最大值
|
|
25
|
+
* @returns {number}
|
|
26
|
+
*/
|
|
27
|
+
export function safeInt(value, defaultValue, min = 1, max = 1000) {
|
|
28
|
+
const parsed = parseInt(value, 10);
|
|
29
|
+
if (Number.isNaN(parsed)) return defaultValue;
|
|
30
|
+
return Math.max(min, Math.min(max, parsed));
|
|
31
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
|
+
import pathGuard from '../../shared/PathGuard.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Paths — 项目路径解析工具
|
|
@@ -14,6 +15,8 @@ const USER_HOME = process.env.HOME || process.env.USERPROFILE || '';
|
|
|
14
15
|
/** 确保目录存在(静默处理异常) */
|
|
15
16
|
function ensureDir(dirPath) {
|
|
16
17
|
try {
|
|
18
|
+
// 双层路径安全检查 — 阻止在项目允许范围外创建文件夹
|
|
19
|
+
pathGuard.assertProjectWriteSafe(dirPath);
|
|
17
20
|
if (!fs.existsSync(dirPath)) {
|
|
18
21
|
fs.mkdirSync(dirPath, { recursive: true });
|
|
19
22
|
}
|
|
@@ -94,6 +97,14 @@ export function getContextIndexPath(projectRoot) {
|
|
|
94
97
|
return ensureDir(path.join(getContextStoragePath(projectRoot), 'index'));
|
|
95
98
|
}
|
|
96
99
|
|
|
100
|
+
/**
|
|
101
|
+
* 项目级 Skills 目录 = knowledgePath/skills
|
|
102
|
+
* Skills 放在知识库目录下跟随项目走(Git-tracked,用户可见)
|
|
103
|
+
*/
|
|
104
|
+
export function getProjectSkillsPath(projectRoot) {
|
|
105
|
+
return ensureDir(path.join(getProjectKnowledgePath(projectRoot), 'skills'));
|
|
106
|
+
}
|
|
107
|
+
|
|
97
108
|
/**
|
|
98
109
|
* Recipes 目录
|
|
99
110
|
* 优先使用 rootSpec.recipes.dir / rootSpec.skills.dir(兼容旧配置)
|
|
@@ -114,6 +125,7 @@ export default {
|
|
|
114
125
|
getProjectKnowledgePath,
|
|
115
126
|
getProjectSpecPath,
|
|
116
127
|
getProjectInternalDataPath,
|
|
128
|
+
getProjectSkillsPath,
|
|
117
129
|
getContextStoragePath,
|
|
118
130
|
getContextIndexPath,
|
|
119
131
|
getProjectRecipesPath,
|
|
@@ -2,6 +2,7 @@ import Database from 'better-sqlite3';
|
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
|
+
import pathGuard from '../../shared/PathGuard.js';
|
|
5
6
|
|
|
6
7
|
const __filename = fileURLToPath(import.meta.url);
|
|
7
8
|
const __dirname = path.dirname(__filename);
|
|
@@ -20,9 +21,13 @@ export class DatabaseConnection {
|
|
|
20
21
|
*/
|
|
21
22
|
async connect() {
|
|
22
23
|
const dbPath = this.config.path;
|
|
24
|
+
|
|
25
|
+
// 路径安全检查 — 防止 DB 文件创建到项目允许范围外
|
|
26
|
+
const resolvedDbPath = path.resolve(dbPath);
|
|
27
|
+
pathGuard.assertProjectWriteSafe(resolvedDbPath);
|
|
23
28
|
|
|
24
29
|
// 确保数据目录存在
|
|
25
|
-
const dbDir = path.dirname(
|
|
30
|
+
const dbDir = path.dirname(resolvedDbPath);
|
|
26
31
|
if (!fs.existsSync(dbDir)) {
|
|
27
32
|
fs.mkdirSync(dbDir, { recursive: true });
|
|
28
33
|
}
|
|
@@ -2,9 +2,91 @@ import winston from 'winston';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
|
|
5
|
+
// ChatAgent 相关标签 — 终端高亮显示
|
|
6
|
+
const AGENT_TAGS = ['ChatAgent', 'ToolRegistry', 'SignalCollector', 'SkillAdvisor', 'CircuitBreaker', 'EventAggregator'];
|
|
7
|
+
const MUTED_PREFIXES = ['HTTP Request', 'Tool registered:', '📊 性能统计已更新'];
|
|
8
|
+
|
|
9
|
+
// ANSI 颜色常量 — 保证深色终端可读性
|
|
10
|
+
const C = {
|
|
11
|
+
reset: '\x1b[0m',
|
|
12
|
+
dim: '\x1b[2m', // 真正的 dim(用于次要信息)
|
|
13
|
+
bold: '\x1b[1m',
|
|
14
|
+
// 前景色 — 使用亮色变体,深色终端更清晰
|
|
15
|
+
gray: '\x1b[37m', // 白色(替代 90 暗灰)
|
|
16
|
+
cyan: '\x1b[96m', // 亮青
|
|
17
|
+
green: '\x1b[92m', // 亮绿
|
|
18
|
+
yellow: '\x1b[93m', // 亮黄
|
|
19
|
+
red: '\x1b[91m', // 亮红
|
|
20
|
+
magenta: '\x1b[95m', // 亮洋红
|
|
21
|
+
blue: '\x1b[94m', // 亮蓝
|
|
22
|
+
dimGray: '\x1b[2;37m', // dim 白色 — 比 90 在深色背景上更可读
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const LEVEL_COLORS = {
|
|
26
|
+
error: C.red,
|
|
27
|
+
warn: C.yellow,
|
|
28
|
+
info: C.green,
|
|
29
|
+
debug: C.blue,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 精简 Console 格式
|
|
34
|
+
* - ChatAgent 相关日志: 高亮 cyan/magenta,显示完整信息
|
|
35
|
+
* - warn/error: 醒目颜色完整显示
|
|
36
|
+
* - HTTP 日志: 精简并降低视觉权重
|
|
37
|
+
* - 其他 info/debug: 一行精简格式
|
|
38
|
+
*/
|
|
39
|
+
const compactConsoleFormat = winston.format.printf(({ level, message, timestamp, ...meta }) => {
|
|
40
|
+
const ts = new Date(timestamp).toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
41
|
+
const rawLevel = level.replace(/\u001b\[\d+m/g, ''); // 去 ANSI
|
|
42
|
+
const lc = LEVEL_COLORS[rawLevel] || C.gray;
|
|
43
|
+
|
|
44
|
+
// 静音高频噪音日志
|
|
45
|
+
if (rawLevel === 'info' && MUTED_PREFIXES.some(p => message.startsWith(p))) {
|
|
46
|
+
return ''; // 返回空字符串会被 winston 跳过
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 判断是否为 Agent 相关日志
|
|
50
|
+
const isAgentLog = AGENT_TAGS.some(tag => message.includes(tag) || message.startsWith(`[${tag}]`));
|
|
51
|
+
|
|
52
|
+
if (isAgentLog) {
|
|
53
|
+
// ChatAgent 日志 — 高亮显示
|
|
54
|
+
const metaStr = Object.keys(meta).length > 0
|
|
55
|
+
? ' ' + JSON.stringify(meta, null, 0).replace(/"/g, '').replace(/,/g, ', ')
|
|
56
|
+
: '';
|
|
57
|
+
return `${C.cyan}${ts}${C.reset} ${C.magenta}⚡ ${message}${C.reset}${metaStr ? `${C.dimGray}${metaStr}${C.reset}` : ''}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// HTTP 请求日志 — 精简格式,降低视觉权重
|
|
61
|
+
if (message === 'HTTP' && meta.method) {
|
|
62
|
+
const { method, path: reqPath, statusCode, duration } = meta;
|
|
63
|
+
const status = Number(statusCode);
|
|
64
|
+
const sc = status >= 500 ? C.red : status >= 400 ? C.yellow : C.dimGray;
|
|
65
|
+
const dur = parseInt(duration) > 1000 ? `${C.yellow}${duration}${C.reset}` : `${C.dimGray}${duration}${C.reset}`;
|
|
66
|
+
return `${C.dimGray}${ts}${C.reset} ${lc}${rawLevel}${C.reset} ${C.dimGray}${method}${C.reset} ${C.gray}${reqPath}${C.reset} ${sc}${statusCode}${C.reset} ${dur}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (rawLevel === 'warn') {
|
|
70
|
+
const metaStr = Object.keys(meta).length > 0 ? ' ' + JSON.stringify(meta) : '';
|
|
71
|
+
return `${C.gray}${ts}${C.reset} ${C.yellow}${C.bold}warn${C.reset} ${C.yellow}${message}${C.reset}${metaStr ? `${C.dimGray}${metaStr}${C.reset}` : ''}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (rawLevel === 'error') {
|
|
75
|
+
const metaStr = Object.keys(meta).length > 0 ? ' ' + JSON.stringify(meta) : '';
|
|
76
|
+
return `${C.gray}${ts}${C.reset} ${C.red}${C.bold}error${C.reset} ${C.red}${message}${C.reset}${metaStr ? `${C.dimGray}${metaStr}${C.reset}` : ''}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 普通 info/debug — 精简一行,但保证可读
|
|
80
|
+
return `${C.dimGray}${ts}${C.reset} ${lc}${rawLevel}${C.reset} ${C.gray}${message}${C.reset}`;
|
|
81
|
+
});
|
|
82
|
+
|
|
5
83
|
/**
|
|
6
84
|
* Logger - 统一日志系统
|
|
7
85
|
*
|
|
86
|
+
* 环境变量:
|
|
87
|
+
* ASD_LOG_LEVEL — 覆盖日志级别 (debug/info/warn/error)
|
|
88
|
+
* ASD_MCP_MODE=1 — MCP 模式下禁用 Console transport
|
|
89
|
+
*
|
|
8
90
|
* MCP 模式(ASD_MCP_MODE=1)下 Console transport 输出到 stderr 并禁用彩色,
|
|
9
91
|
* 避免污染 stdout JSON-RPC 通道。
|
|
10
92
|
*/
|
|
@@ -21,6 +103,7 @@ export class Logger {
|
|
|
21
103
|
}
|
|
22
104
|
|
|
23
105
|
const isMcpMode = process.env.ASD_MCP_MODE === '1';
|
|
106
|
+
const logLevel = process.env.ASD_LOG_LEVEL || config.level || 'info';
|
|
24
107
|
const transports = [];
|
|
25
108
|
|
|
26
109
|
// Console transport — MCP 模式下完全禁用(任何 stderr 输出都会被 Cursor 标记为 [error])
|
|
@@ -29,8 +112,8 @@ export class Logger {
|
|
|
29
112
|
new winston.transports.Console({
|
|
30
113
|
stderrLevels: ['error', 'warn', 'info', 'debug'],
|
|
31
114
|
format: winston.format.combine(
|
|
32
|
-
winston.format.
|
|
33
|
-
|
|
115
|
+
winston.format.timestamp(),
|
|
116
|
+
compactConsoleFormat,
|
|
34
117
|
),
|
|
35
118
|
})
|
|
36
119
|
);
|
|
@@ -55,7 +138,7 @@ export class Logger {
|
|
|
55
138
|
}
|
|
56
139
|
|
|
57
140
|
this.instance = winston.createLogger({
|
|
58
|
-
level:
|
|
141
|
+
level: logLevel,
|
|
59
142
|
format: winston.format.combine(winston.format.timestamp(), winston.format.json()),
|
|
60
143
|
transports,
|
|
61
144
|
});
|
|
@@ -129,11 +129,8 @@ export class RealtimeService {
|
|
|
129
129
|
* 广播通用事件
|
|
130
130
|
*/
|
|
131
131
|
broadcastEvent(eventName, data) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
...data,
|
|
135
|
-
timestamp: Date.now(),
|
|
136
|
-
});
|
|
132
|
+
// 直接透传 data(不包装 type/timestamp),保持与前端 hook 期望的数据结构一致
|
|
133
|
+
this.io.to('notifications').emit(eventName, data);
|
|
137
134
|
}
|
|
138
135
|
|
|
139
136
|
/**
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { VectorStore } from './VectorStore.js';
|
|
8
8
|
import { writeFileSync, readFileSync, mkdirSync, existsSync, statSync } from 'node:fs';
|
|
9
9
|
import { join, dirname } from 'node:path';
|
|
10
|
+
import pathGuard from '../../shared/PathGuard.js';
|
|
10
11
|
|
|
11
12
|
export class JsonVectorAdapter extends VectorStore {
|
|
12
13
|
#indexPath;
|
|
@@ -22,7 +23,15 @@ export class JsonVectorAdapter extends VectorStore {
|
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
async init() {
|
|
25
|
-
|
|
26
|
+
this.#load();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 同步初始化 — 供 ServiceContainer 懒加载工厂使用
|
|
31
|
+
* (#load 本身就是同步的 readFileSync,无需 await)
|
|
32
|
+
*/
|
|
33
|
+
initSync() {
|
|
34
|
+
this.#load();
|
|
26
35
|
}
|
|
27
36
|
|
|
28
37
|
async upsert(item) {
|
|
@@ -132,6 +141,21 @@ export class JsonVectorAdapter extends VectorStore {
|
|
|
132
141
|
return scored;
|
|
133
142
|
}
|
|
134
143
|
|
|
144
|
+
/**
|
|
145
|
+
* query() — SearchEngine / RetrievalFunnel 使用的向量搜索别名
|
|
146
|
+
* 接口: query(vector, topK) → Array<{ id, similarity, metadata }>
|
|
147
|
+
*/
|
|
148
|
+
async query(queryVector, topK = 10) {
|
|
149
|
+
const results = await this.searchVector(queryVector, { topK });
|
|
150
|
+
return results.map(r => ({
|
|
151
|
+
id: r.item.id,
|
|
152
|
+
similarity: r.score,
|
|
153
|
+
score: r.score,
|
|
154
|
+
content: r.item.content,
|
|
155
|
+
metadata: r.item.metadata || {},
|
|
156
|
+
}));
|
|
157
|
+
}
|
|
158
|
+
|
|
135
159
|
async searchByFilter(filter) {
|
|
136
160
|
return this.#applyFilter([...this.#data.values()], filter);
|
|
137
161
|
}
|
|
@@ -215,6 +239,7 @@ export class JsonVectorAdapter extends VectorStore {
|
|
|
215
239
|
if (!this.#dirty) return;
|
|
216
240
|
try {
|
|
217
241
|
const dir = dirname(this.#indexPath);
|
|
242
|
+
pathGuard.assertProjectWriteSafe(dir);
|
|
218
243
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
219
244
|
const items = [...this.#data.values()];
|
|
220
245
|
writeFileSync(this.#indexPath, JSON.stringify(items, null, 2));
|
|
@@ -51,8 +51,13 @@ import { ChatAgent } from '../service/chat/ChatAgent.js';
|
|
|
51
51
|
import { ALL_TOOLS } from '../service/chat/tools.js';
|
|
52
52
|
import { SkillHooks } from '../service/skills/SkillHooks.js';
|
|
53
53
|
|
|
54
|
+
// ─── v3.0: AST ProjectGraph ──────────────────────────
|
|
55
|
+
import ProjectGraph from '../core/ast/ProjectGraph.js';
|
|
56
|
+
|
|
54
57
|
// ─── P3: Infrastructure ──────────────────────────────
|
|
55
58
|
import { EventBus } from '../infrastructure/event/EventBus.js';
|
|
59
|
+
import { BootstrapTaskManager } from '../service/bootstrap/BootstrapTaskManager.js';
|
|
60
|
+
import { getRealtimeService as _getRealtimeService } from '../infrastructure/realtime/RealtimeService.js';
|
|
56
61
|
|
|
57
62
|
/**
|
|
58
63
|
* DependencyInjection 容器
|
|
@@ -208,6 +213,19 @@ export class ServiceContainer {
|
|
|
208
213
|
}
|
|
209
214
|
return this.singletons.eventBus;
|
|
210
215
|
});
|
|
216
|
+
|
|
217
|
+
// BootstrapTaskManager(冷启动异步任务管理器 — 单例)
|
|
218
|
+
this.register('bootstrapTaskManager', () => {
|
|
219
|
+
if (!this.singletons.bootstrapTaskManager) {
|
|
220
|
+
const eventBus = this.get('eventBus');
|
|
221
|
+
// 延迟 getter: RealtimeService 在 HTTP server 启动后才可用,CLI 模式下不可用
|
|
222
|
+
const getRS = () => {
|
|
223
|
+
try { return _getRealtimeService(); } catch { return null; }
|
|
224
|
+
};
|
|
225
|
+
this.singletons.bootstrapTaskManager = new BootstrapTaskManager({ eventBus, getRealtimeService: getRS });
|
|
226
|
+
}
|
|
227
|
+
return this.singletons.bootstrapTaskManager;
|
|
228
|
+
});
|
|
211
229
|
}
|
|
212
230
|
|
|
213
231
|
/**
|
|
@@ -340,11 +358,13 @@ export class ServiceContainer {
|
|
|
340
358
|
return this.singletons.retrievalFunnel;
|
|
341
359
|
});
|
|
342
360
|
|
|
343
|
-
// JsonVectorAdapter
|
|
361
|
+
// JsonVectorAdapter(同步构造 + 同步 init — 从磁盘 JSON 加载历史向量数据)
|
|
344
362
|
this.register('vectorStore', () => {
|
|
345
363
|
if (!this.singletons.vectorStore) {
|
|
346
364
|
const projectRoot = this.singletons._projectRoot || process.cwd();
|
|
347
|
-
|
|
365
|
+
const store = new JsonVectorAdapter(projectRoot);
|
|
366
|
+
store.initSync(); // 从磁盘加载已有 vector_index.json
|
|
367
|
+
this.singletons.vectorStore = store;
|
|
348
368
|
}
|
|
349
369
|
return this.singletons.vectorStore;
|
|
350
370
|
});
|
|
@@ -481,6 +501,12 @@ export class ServiceContainer {
|
|
|
481
501
|
return this.singletons.toolRegistry;
|
|
482
502
|
});
|
|
483
503
|
|
|
504
|
+
// ProjectGraph (v3.0 AST 结构图 — 懒初始化,首次 get 时构建)
|
|
505
|
+
this.register('projectGraph', () => {
|
|
506
|
+
// 返回已构建的实例;需要外部先调用 buildProjectGraph() 构建
|
|
507
|
+
return this.singletons.projectGraph || null;
|
|
508
|
+
});
|
|
509
|
+
|
|
484
510
|
// AI Provider(供 MCP handler / ChatAgent / 任意服务层使用)
|
|
485
511
|
this.register('aiProvider', () => this.singletons.aiProvider || null);
|
|
486
512
|
|
|
@@ -537,6 +563,33 @@ export class ServiceContainer {
|
|
|
537
563
|
getServiceNames() {
|
|
538
564
|
return Object.keys(this.services);
|
|
539
565
|
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* 构建 ProjectGraph (v3.0 AST 结构图)
|
|
569
|
+
* 应在 bootstrap 流程开始前调用一次
|
|
570
|
+
* @param {string} projectRoot 项目根目录
|
|
571
|
+
* @param {object} [options] 传递给 ProjectGraph.build() 的选项
|
|
572
|
+
* @returns {Promise<import('../core/ast/ProjectGraph.js').default|null>}
|
|
573
|
+
*/
|
|
574
|
+
async buildProjectGraph(projectRoot, options = {}) {
|
|
575
|
+
if (this.singletons.projectGraph) {
|
|
576
|
+
return this.singletons.projectGraph;
|
|
577
|
+
}
|
|
578
|
+
try {
|
|
579
|
+
const graph = await ProjectGraph.build(projectRoot, options);
|
|
580
|
+
this.singletons.projectGraph = graph;
|
|
581
|
+
const overview = graph.getOverview();
|
|
582
|
+
this.logger.info(
|
|
583
|
+
`[ServiceContainer] ProjectGraph built: ${overview.totalClasses} classes, ` +
|
|
584
|
+
`${overview.totalProtocols} protocols, ${overview.totalCategories} categories ` +
|
|
585
|
+
`(${overview.buildTimeMs}ms)`
|
|
586
|
+
);
|
|
587
|
+
return graph;
|
|
588
|
+
} catch (err) {
|
|
589
|
+
this.logger.warn(`[ServiceContainer] ProjectGraph build failed: ${err.message}`);
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
540
593
|
}
|
|
541
594
|
|
|
542
595
|
let containerInstance = null;
|