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.
Files changed (78) hide show
  1. package/README.md +137 -65
  2. package/bin/api-server.js +5 -0
  3. package/bin/cli.js +6 -1
  4. package/dashboard/dist/assets/{icons-rnn04CvH.js → icons-B_Xg4B-s.js} +148 -88
  5. package/dashboard/dist/assets/index-BjfUm8p9.js +197 -0
  6. package/dashboard/dist/assets/index-CkIih2CC.css +1 -0
  7. package/dashboard/dist/assets/{react-markdown-CWxUbOf4.js → react-markdown-BA6FB2NP.js} +1 -1
  8. package/dashboard/dist/assets/{syntax-highlighter-CJ2drQQb.js → syntax-highlighter-CVLHn9O5.js} +1 -1
  9. package/dashboard/dist/assets/{vendor-f83ah6cm.js → vendor-BotF760a.js} +61 -61
  10. package/dashboard/dist/index.html +6 -6
  11. package/lib/bootstrap.js +18 -1
  12. package/lib/cli/SetupService.js +86 -8
  13. package/lib/cli/UpgradeService.js +139 -2
  14. package/lib/core/ast/ProjectGraph.js +599 -0
  15. package/lib/core/gateway/GatewayActionRegistry.js +2 -2
  16. package/lib/domain/recipe/Recipe.js +3 -0
  17. package/lib/external/ai/AiProvider.js +83 -20
  18. package/lib/external/ai/providers/ClaudeProvider.js +208 -0
  19. package/lib/external/ai/providers/GoogleGeminiProvider.js +247 -1
  20. package/lib/external/ai/providers/OpenAiProvider.js +141 -0
  21. package/lib/external/mcp/McpServer.js +6 -1
  22. package/lib/external/mcp/handlers/bootstrap/pipeline/dimension-context.js +216 -0
  23. package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +657 -0
  24. package/lib/external/mcp/handlers/bootstrap/pipeline/tier-scheduler.js +160 -0
  25. package/lib/external/mcp/handlers/bootstrap/skills.js +225 -0
  26. package/lib/external/mcp/handlers/bootstrap.js +159 -1634
  27. package/lib/external/mcp/handlers/browse.js +1 -1
  28. package/lib/external/mcp/handlers/candidate.js +1 -33
  29. package/lib/external/mcp/handlers/skill.js +58 -17
  30. package/lib/external/mcp/tools.js +4 -3
  31. package/lib/http/middleware/requestLogger.js +23 -4
  32. package/lib/http/routes/ai.js +158 -2
  33. package/lib/http/routes/auth.js +3 -2
  34. package/lib/http/routes/candidates.js +49 -25
  35. package/lib/http/routes/commands.js +0 -8
  36. package/lib/http/routes/guardRules.js +1 -16
  37. package/lib/http/routes/recipes.js +4 -17
  38. package/lib/http/routes/search.js +11 -19
  39. package/lib/http/routes/skills.js +2 -0
  40. package/lib/http/routes/snippets.js +0 -33
  41. package/lib/http/routes/spm.js +37 -63
  42. package/lib/http/utils/routeHelpers.js +31 -0
  43. package/lib/infrastructure/config/Paths.js +12 -0
  44. package/lib/infrastructure/database/DatabaseConnection.js +6 -1
  45. package/lib/infrastructure/logging/Logger.js +86 -3
  46. package/lib/infrastructure/realtime/RealtimeService.js +2 -5
  47. package/lib/infrastructure/vector/JsonVectorAdapter.js +26 -1
  48. package/lib/injection/ServiceContainer.js +55 -2
  49. package/lib/service/bootstrap/BootstrapTaskManager.js +400 -0
  50. package/lib/service/candidate/CandidateFileWriter.js +72 -27
  51. package/lib/service/candidate/CandidateService.js +156 -10
  52. package/lib/service/chat/AnalystAgent.js +245 -0
  53. package/lib/service/chat/CandidateGuardrail.js +134 -0
  54. package/lib/service/chat/ChatAgent.js +1055 -167
  55. package/lib/service/chat/ContextWindow.js +730 -0
  56. package/lib/service/chat/ConversationStore.js +3 -0
  57. package/lib/service/chat/HandoffProtocol.js +181 -0
  58. package/lib/service/chat/Memory.js +3 -0
  59. package/lib/service/chat/ProducerAgent.js +293 -0
  60. package/lib/service/chat/ToolRegistry.js +149 -5
  61. package/lib/service/chat/tools.js +1404 -61
  62. package/lib/service/guard/ExclusionManager.js +2 -0
  63. package/lib/service/guard/RuleLearner.js +2 -0
  64. package/lib/service/quality/FeedbackCollector.js +2 -0
  65. package/lib/service/recipe/RecipeFileWriter.js +16 -1
  66. package/lib/service/recipe/RecipeStatsTracker.js +2 -0
  67. package/lib/service/skills/SignalCollector.js +33 -6
  68. package/lib/service/skills/SkillAdvisor.js +2 -1
  69. package/lib/service/skills/SkillHooks.js +13 -5
  70. package/lib/service/spm/SpmService.js +2 -2
  71. package/lib/shared/PathGuard.js +314 -0
  72. package/package.json +1 -1
  73. package/resources/native-ui/combined-window.swift +494 -0
  74. package/templates/copilot-instructions.md +20 -3
  75. package/templates/cursor-rules/autosnippet-conventions.mdc +21 -4
  76. package/templates/cursor-rules/autosnippet-skills.mdc +45 -0
  77. package/dashboard/dist/assets/index-BBKa3Dgi.js +0 -195
  78. 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 { /* Agent 不可用时不阻塞发布 */ }
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;
@@ -198,23 +198,24 @@ router.post('/scan-project', asyncHandler(async (req, res) => {
198
198
 
199
199
  /**
200
200
  * POST /api/v1/spm/bootstrap
201
- * 冷启动:结构收集 + 9 维度 Candidate 创建
201
+ * 冷启动:快速骨架 + 异步逐维度填充(v5)
202
202
  *
203
- * 分离式执行策略(避免前端超时):
204
- * ① 同步阶段: bootstrap_knowledge(SPM 扫描 + AST + Skill 增强 + 候选创建)→ 立即返回
205
- * ② 异步阶段: enrich + refine(AI 结构补齐 + 内容润色)→ 后台执行
203
+ * 执行策略:
204
+ * ① 同步阶段: Phase 1-4(文件收集 + AST + SPM + Guard + 骨架响应)→ 立即返回
205
+ * ② 异步阶段: Phase 5/5.5(逐维度提取 + Candidate/Skill 创建)→ 后台逐一执行
206
+ * ③ 进度推送: 通过 Socket.io 实时推送每个维度的完成状态
206
207
  *
207
- * 前端立即获得候选列表,AI 增强在后台完成后候选会自动更新。
208
+ * 前端立即获得骨架 + 任务清单,每个维度完成后通过 Socket.io 推送更新。
208
209
  */
209
210
  router.post('/bootstrap', asyncHandler(async (req, res) => {
210
- const { maxFiles, skipGuard, contentMaxLines, autoRefine } = req.body || {};
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 (split mode: sync + async AI)');
216
+ logger.info('Bootstrap cold start initiated (v5: async fill mode)');
216
217
 
217
- // ── 同步阶段: 快速执行启发式 bootstrap(~1s)──
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: responseData,
229
+ data: {
230
+ ...bootstrapResult,
231
+ asyncFill: true, // 告知前端:内容正在异步填充中
232
+ },
234
233
  });
235
234
 
236
- // ── 异步阶段: 后台 AI 增强(enrich + refine)不阻塞响应 ──
237
- const shouldRefine = (autoRefine ?? true) && chatAgent.hasAI;
238
- const created = bootstrapResult?.bootstrapCandidates?.created || 0;
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
- await chatAgent.executeTool('refine_bootstrap_candidates', {
275
- userPrompt: userPrompt || undefined,
276
- });
277
- logger.info(`[Bootstrap] Background refine done`);
278
- } catch (e) {
279
- logger.warn(`[Bootstrap] Background refine failed (non-fatal): ${e.message}`);
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
- logger.info('[Bootstrap] Background AI enhancement completed');
283
- } catch (e) {
284
- logger.error(`[Bootstrap] Background AI enhancement error: ${e.message}`);
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(dbPath);
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.colorize(),
33
- winston.format.simple()
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: config.level || 'info',
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
- this.io.to('notifications').emit(eventName, {
133
- type: eventName,
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
- await this.#load();
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
- this.singletons.vectorStore = new JsonVectorAdapter(projectRoot);
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;