autosnippet 2.6.0 → 2.7.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.
Files changed (66) hide show
  1. package/bin/cli.js +1 -1
  2. package/dashboard/dist/assets/{icons-rnn04CvH.js → icons-Cq4-iQhP.js} +148 -88
  3. package/dashboard/dist/assets/index-DBxH7pVn.css +1 -0
  4. package/dashboard/dist/assets/index-Dw2F6qAS.js +197 -0
  5. package/dashboard/dist/assets/{react-markdown-CWxUbOf4.js → react-markdown-BA6FB2NP.js} +1 -1
  6. package/dashboard/dist/assets/{syntax-highlighter-CJ2drQQb.js → syntax-highlighter-CVLHn9O5.js} +1 -1
  7. package/dashboard/dist/assets/{vendor-f83ah6cm.js → vendor-BotF760a.js} +61 -61
  8. package/dashboard/dist/index.html +6 -6
  9. package/lib/bootstrap.js +1 -1
  10. package/lib/cli/SetupService.js +33 -8
  11. package/lib/cli/UpgradeService.js +139 -2
  12. package/lib/core/ast/ProjectGraph.js +599 -0
  13. package/lib/core/gateway/GatewayActionRegistry.js +2 -2
  14. package/lib/domain/recipe/Recipe.js +3 -0
  15. package/lib/external/ai/AiProvider.js +83 -20
  16. package/lib/external/ai/providers/ClaudeProvider.js +197 -0
  17. package/lib/external/ai/providers/GoogleGeminiProvider.js +235 -1
  18. package/lib/external/ai/providers/OpenAiProvider.js +131 -0
  19. package/lib/external/mcp/handlers/bootstrap/pipeline/dimension-context.js +216 -0
  20. package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +468 -0
  21. package/lib/external/mcp/handlers/bootstrap/pipeline/tier-scheduler.js +162 -0
  22. package/lib/external/mcp/handlers/bootstrap/skills.js +225 -0
  23. package/lib/external/mcp/handlers/bootstrap.js +151 -1634
  24. package/lib/external/mcp/handlers/browse.js +1 -1
  25. package/lib/external/mcp/handlers/candidate.js +1 -33
  26. package/lib/external/mcp/handlers/skill.js +54 -17
  27. package/lib/external/mcp/tools.js +4 -3
  28. package/lib/http/middleware/requestLogger.js +23 -4
  29. package/lib/http/routes/ai.js +3 -1
  30. package/lib/http/routes/auth.js +3 -2
  31. package/lib/http/routes/candidates.js +49 -25
  32. package/lib/http/routes/commands.js +0 -8
  33. package/lib/http/routes/guardRules.js +1 -16
  34. package/lib/http/routes/recipes.js +4 -17
  35. package/lib/http/routes/search.js +11 -19
  36. package/lib/http/routes/skills.js +2 -0
  37. package/lib/http/routes/snippets.js +0 -33
  38. package/lib/http/routes/spm.js +37 -63
  39. package/lib/http/utils/routeHelpers.js +31 -0
  40. package/lib/infrastructure/config/Paths.js +9 -0
  41. package/lib/infrastructure/logging/Logger.js +86 -3
  42. package/lib/infrastructure/realtime/RealtimeService.js +2 -5
  43. package/lib/infrastructure/vector/JsonVectorAdapter.js +24 -1
  44. package/lib/injection/ServiceContainer.js +55 -2
  45. package/lib/service/bootstrap/BootstrapTaskManager.js +400 -0
  46. package/lib/service/candidate/CandidateFileWriter.js +68 -27
  47. package/lib/service/candidate/CandidateService.js +156 -10
  48. package/lib/service/chat/AnalystAgent.js +216 -0
  49. package/lib/service/chat/CandidateGuardrail.js +134 -0
  50. package/lib/service/chat/ChatAgent.js +1036 -167
  51. package/lib/service/chat/ContextWindow.js +730 -0
  52. package/lib/service/chat/HandoffProtocol.js +180 -0
  53. package/lib/service/chat/ProducerAgent.js +240 -0
  54. package/lib/service/chat/ToolRegistry.js +149 -5
  55. package/lib/service/chat/tools.js +1397 -61
  56. package/lib/service/recipe/RecipeFileWriter.js +12 -1
  57. package/lib/service/skills/SignalCollector.js +31 -6
  58. package/lib/service/skills/SkillAdvisor.js +2 -1
  59. package/lib/service/skills/SkillHooks.js +13 -5
  60. package/lib/service/spm/SpmService.js +2 -2
  61. package/package.json +1 -1
  62. package/templates/copilot-instructions.md +20 -3
  63. package/templates/cursor-rules/autosnippet-conventions.mdc +21 -4
  64. package/templates/cursor-rules/autosnippet-skills.mdc +45 -0
  65. package/dashboard/dist/assets/index-BBKa3Dgi.js +0 -195
  66. package/dashboard/dist/assets/index-DLsECfzW.css +0 -1
@@ -15,7 +15,7 @@ export async function listByKind(ctx, kind, args) {
15
15
  const items = (result?.data || result?.items || []).map(r => ({
16
16
  id: r.id, title: r.title || r.name, description: r.description,
17
17
  trigger: r.trigger || '', status: r.status, language: r.language, category: r.category,
18
- knowledgeType: r.knowledgeType || r.knowledge_type, kind: r.kind,
18
+ knowledgeType: r.knowledgeType, kind: r.kind,
19
19
  complexity: r.complexity, scope: r.scope, tags: r.tags || [],
20
20
  quality: r.quality || null, statistics: r.statistics || null,
21
21
  }));
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * MCP Handlers — 候选提交 & 校验 & AI 补全
3
3
  * validateCandidate, checkDuplicate, submitSingle, submitBatch, submitDrafts, enrichCandidates
4
- * + 辅助: buildReasoning, buildCandidateMetadata, _createCandidateItem
4
+ * + 辅助: buildReasoning, _createCandidateItem
5
5
  */
6
6
 
7
7
  import fs from 'node:fs';
@@ -28,38 +28,6 @@ export function buildReasoning(obj) {
28
28
  };
29
29
  }
30
30
 
31
- export function buildCandidateMetadata(obj) {
32
- const m = {};
33
- // 标识 & 描述
34
- if (obj.title) m.title = obj.title;
35
- if (obj.description) m.description = obj.description;
36
- // 中英文摘要(summary / summary_cn / summary_en)
37
- if (obj.summary_cn || obj.summary) m.summary = obj.summary_cn || obj.summary;
38
- if (obj.summary_en) m.summary_en = obj.summary_en;
39
- if (obj.trigger) m.trigger = obj.trigger;
40
- // 中英文使用指南(usageGuide / usageGuide_cn / usageGuide_en)
41
- if (obj.usageGuide_cn || obj.usageGuide) m.usageGuide = obj.usageGuide_cn || obj.usageGuide;
42
- if (obj.usageGuide_en) m.usageGuide_en = obj.usageGuide_en;
43
- // 分类
44
- if (obj.knowledgeType) m.knowledgeType = obj.knowledgeType;
45
- if (obj.complexity) m.complexity = obj.complexity;
46
- if (obj.scope) m.scope = obj.scope;
47
- if (obj.tags) m.tags = obj.tags;
48
- // 结构化内容
49
- if (obj.rationale) m.rationale = obj.rationale;
50
- if (obj.steps) m.steps = obj.steps;
51
- if (obj.codeChanges) m.codeChanges = obj.codeChanges;
52
- if (obj.verification) m.verification = obj.verification;
53
- if (obj.headers) m.headers = obj.headers;
54
- // 约束 & 关系
55
- if (obj.constraints) m.constraints = obj.constraints;
56
- if (obj.relations) m.relations = obj.relations;
57
- // 质量 & 来源
58
- if (obj.quality) m.quality = obj.quality;
59
- if (obj.sourceFile) m.sourceFile = obj.sourceFile;
60
- return m;
61
- }
62
-
63
31
  /**
64
32
  * 统一创建候选的内部方法 — 委托到 CandidateService.createFromToolParams()
65
33
  * 保留此函数作为 MCP handler 层的快捷入口,保持向后兼容。
@@ -13,11 +13,25 @@
13
13
  import fs from 'node:fs';
14
14
  import path from 'node:path';
15
15
  import { fileURLToPath } from 'node:url';
16
+ import { getProjectSkillsPath } from '../../../infrastructure/config/Paths.js';
16
17
 
17
18
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
18
- const PROJECT_ROOT = path.resolve(__dirname, '../../../..');
19
- const SKILLS_DIR = path.resolve(PROJECT_ROOT, 'skills');
20
- const PROJECT_SKILLS_DIR = path.resolve(PROJECT_ROOT, '.autosnippet', 'skills');
19
+ const SKILLS_DIR = path.resolve(__dirname, '../../../../skills');
20
+
21
+ /**
22
+ * 获取用户项目根目录(运行时动态解析)
23
+ */
24
+ function _getProjectRoot() {
25
+ return process.env.ASD_PROJECT_DIR || process.cwd();
26
+ }
27
+
28
+ /**
29
+ * 获取项目级 Skills 目录(运行时动态解析)
30
+ * 路径: {projectRoot}/AutoSnippet/skills/ — 跟随项目走
31
+ */
32
+ function _getProjectSkillsDir() {
33
+ return getProjectSkillsPath(_getProjectRoot());
34
+ }
21
35
 
22
36
  /**
23
37
  * 解析 SKILL.md frontmatter 全部元数据
@@ -93,10 +107,11 @@ export function listSkills() {
93
107
 
94
108
  // 项目级 Skills(覆盖同名内置)
95
109
  try {
96
- const projectDirs = fs.readdirSync(PROJECT_SKILLS_DIR, { withFileTypes: true })
110
+ const projectSkillsDir = _getProjectSkillsDir();
111
+ const projectDirs = fs.readdirSync(projectSkillsDir, { withFileTypes: true })
97
112
  .filter(d => d.isDirectory()).map(d => d.name);
98
113
  for (const name of projectDirs) {
99
- const meta = _parseSkillMeta(name, PROJECT_SKILLS_DIR);
114
+ const meta = _parseSkillMeta(name, projectSkillsDir);
100
115
  skillMap.set(name, { name, source: 'project', summary: meta.description, createdBy: meta.createdBy, createdAt: meta.createdAt, useCase: SKILL_USE_CASES[name] || null });
101
116
  }
102
117
  } catch { /* no project skills */ }
@@ -151,7 +166,8 @@ export function loadSkill(_ctx, args) {
151
166
  }
152
167
 
153
168
  // 项目级 Skills 优先
154
- const projectSkillPath = path.join(PROJECT_SKILLS_DIR, skillName, 'SKILL.md');
169
+ const projectSkillsDir = _getProjectSkillsDir();
170
+ const projectSkillPath = path.join(projectSkillsDir, skillName, 'SKILL.md');
155
171
  const builtinSkillPath = path.join(SKILLS_DIR, skillName, 'SKILL.md');
156
172
  const skillPath = fs.existsSync(projectSkillPath) ? projectSkillPath : builtinSkillPath;
157
173
  const source = skillPath === projectSkillPath ? 'project' : 'builtin';
@@ -172,7 +188,7 @@ export function loadSkill(_ctx, args) {
172
188
  }
173
189
 
174
190
  // 提取 createdBy/createdAt
175
- const meta = _parseSkillMeta(skillName, source === 'project' ? PROJECT_SKILLS_DIR : SKILLS_DIR);
191
+ const meta = _parseSkillMeta(skillName, source === 'project' ? projectSkillsDir : SKILLS_DIR);
176
192
 
177
193
  return JSON.stringify({
178
194
  success: true,
@@ -191,7 +207,7 @@ export function loadSkill(_ctx, args) {
191
207
  // 列出所有可用 Skills
192
208
  const available = new Set();
193
209
  try { fs.readdirSync(SKILLS_DIR, { withFileTypes: true }).filter(d => d.isDirectory()).forEach(d => available.add(d.name)); } catch {}
194
- try { fs.readdirSync(PROJECT_SKILLS_DIR, { withFileTypes: true }).filter(d => d.isDirectory()).forEach(d => available.add(d.name)); } catch {}
210
+ try { fs.readdirSync(_getProjectSkillsDir(), { withFileTypes: true }).filter(d => d.isDirectory()).forEach(d => available.add(d.name)); } catch {}
195
211
 
196
212
  return JSON.stringify({
197
213
  success: false,
@@ -209,7 +225,7 @@ export function loadSkill(_ctx, args) {
209
225
  // ═══════════════════════════════════════════════════════════
210
226
 
211
227
  /**
212
- * 创建项目级 Skill — 写入 .autosnippet/skills/<name>/SKILL.md
228
+ * 创建项目级 Skill — 写入 {projectRoot}/AutoSnippet/skills/<name>/SKILL.md
213
229
  * 创建后自动 regenerate 编辑器索引(.cursor/rules/autosnippet-skills.mdc)
214
230
  *
215
231
  * @param {object} _ctx MCP context
@@ -217,7 +233,7 @@ export function loadSkill(_ctx, args) {
217
233
  * @returns {string} JSON envelope
218
234
  */
219
235
  export function createSkill(_ctx, args) {
220
- const { name, description, content, overwrite = false, createdBy = 'external-ai' } = args || {};
236
+ const { name, description, content, overwrite = false, createdBy = 'external-ai', title } = args || {};
221
237
 
222
238
  // ── 参数校验 ──
223
239
  if (!name || !description || !content) {
@@ -251,7 +267,8 @@ export function createSkill(_ctx, args) {
251
267
  }
252
268
 
253
269
  // 检查同名项目级 Skill
254
- const skillDir = path.join(PROJECT_SKILLS_DIR, name);
270
+ const projectSkillsDir = _getProjectSkillsDir();
271
+ const skillDir = path.join(projectSkillsDir, name);
255
272
  const skillPath = path.join(skillDir, 'SKILL.md');
256
273
  if (fs.existsSync(skillPath) && !overwrite) {
257
274
  return JSON.stringify({
@@ -267,15 +284,25 @@ export function createSkill(_ctx, args) {
267
284
  try {
268
285
  fs.mkdirSync(skillDir, { recursive: true });
269
286
 
270
- const frontmatter = [
287
+ // 自动推断 title: 优先使用传入参数,否则从 content 的第一个 # heading 提取
288
+ const resolvedTitle = title || (() => {
289
+ const m = (content || '').match(/^#\s+(.+)/m);
290
+ return m ? m[1].trim() : '';
291
+ })();
292
+
293
+ const fmLines = [
271
294
  '---',
272
295
  `name: ${name}`,
296
+ ];
297
+ if (resolvedTitle) fmLines.push(`title: "${resolvedTitle.replace(/"/g, '\\"')}"`);
298
+ fmLines.push(
273
299
  `description: ${description}`,
274
300
  `createdBy: ${createdBy}`,
275
301
  `createdAt: ${new Date().toISOString()}`,
276
302
  '---',
277
303
  '',
278
- ].join('\n');
304
+ );
305
+ const frontmatter = fmLines.join('\n');
279
306
 
280
307
  fs.writeFileSync(skillPath, frontmatter + content, 'utf8');
281
308
  } catch (err) {
@@ -288,6 +315,13 @@ export function createSkill(_ctx, args) {
288
315
  // ── regenerate 编辑器索引 ──
289
316
  const indexResult = _regenerateEditorIndex();
290
317
 
318
+ // ── 清理 SignalCollector 已创建的 pendingSuggestions ──
319
+ try {
320
+ if (global._signalCollector) {
321
+ global._signalCollector.removePendingSuggestion(name);
322
+ }
323
+ } catch { /* silent */ }
324
+
291
325
  return JSON.stringify({
292
326
  success: true,
293
327
  data: {
@@ -310,19 +344,23 @@ function _regenerateEditorIndex() {
310
344
  try {
311
345
  // 扫描项目级 Skills
312
346
  let projectSkills = [];
347
+ const projectSkillsDir = _getProjectSkillsDir();
313
348
  try {
314
- const dirs = fs.readdirSync(PROJECT_SKILLS_DIR, { withFileTypes: true })
349
+ const dirs = fs.readdirSync(projectSkillsDir, { withFileTypes: true })
315
350
  .filter(d => d.isDirectory())
316
351
  .map(d => d.name);
317
352
  for (const name of dirs) {
318
- const meta = _parseSkillMeta(name, PROJECT_SKILLS_DIR);
353
+ const meta = _parseSkillMeta(name, projectSkillsDir);
319
354
  projectSkills.push({ name, summary: meta.description });
320
355
  }
321
356
  } catch { /* no project skills dir */ }
322
357
 
358
+ const projectRoot = _getProjectRoot();
359
+ const rulesDir = path.join(projectRoot, '.cursor', 'rules');
360
+
323
361
  if (projectSkills.length === 0) {
324
362
  // 没有项目级 Skills 时,删除索引文件(如果存在)
325
- const indexPath = path.join(PROJECT_ROOT, '.cursor', 'rules', 'autosnippet-skills.mdc');
363
+ const indexPath = path.join(rulesDir, 'autosnippet-skills.mdc');
326
364
  try { fs.unlinkSync(indexPath); } catch { /* not exists */ }
327
365
  return { success: true, skillCount: 0 };
328
366
  }
@@ -347,7 +385,6 @@ function _regenerateEditorIndex() {
347
385
  ].join('\n');
348
386
 
349
387
  // 写入 .cursor/rules/
350
- const rulesDir = path.join(PROJECT_ROOT, '.cursor', 'rules');
351
388
  fs.mkdirSync(rulesDir, { recursive: true });
352
389
  const indexPath = path.join(rulesDir, 'autosnippet-skills.mdc');
353
390
  fs.writeFileSync(indexPath, mdcContent, 'utf8');
@@ -558,13 +558,14 @@ export const TOOLS = [
558
558
  required: ['candidateIds'],
559
559
  },
560
560
  },
561
- // 31. 冷启动知识库初始化(自动创建 9 维度 Candidate)
561
+ // 31. 冷启动知识库初始化(自动创建 9 维度 Candidate + 4 个 Project Skills
562
562
  {
563
563
  name: 'autosnippet_bootstrap_knowledge',
564
564
  description:
565
565
  '项目冷启动:一键初始化知识库(纯启发式,不使用 AI)。覆盖 9 大知识维度。\n' +
566
566
  '自动为每个维度创建 N 条 Candidate(PENDING 状态),基于启发式规则从扫描文件中提取代表性代码。\n' +
567
- '返回 filesByTarget、dependencyGraph、bootstrapCandidates、analysisFramework。\n' +
567
+ 'Phase 5.5 自动为 4 个宏观维度(code-standard, architecture, project-profile, agent-guidelines)生成 Project Skills,写入 AutoSnippet/skills/。\n' +
568
+ '返回 filesByTarget、dependencyGraph、bootstrapCandidates、projectSkills、analysisFramework。\n' +
568
569
  '\n' +
569
570
  '💡 建议:调用前先加载 autosnippet-coldstart Skill(autosnippet_load_skill),获取完整的 9 维度分析指南和最佳实践。\n' +
570
571
  '\n' +
@@ -628,7 +629,7 @@ export const TOOLS = [
628
629
  {
629
630
  name: 'autosnippet_create_skill',
630
631
  description:
631
- '创建一个项目级 Skill 文档,写入 .autosnippet/skills/<name>/SKILL.md。\n' +
632
+ '创建一个项目级 Skill 文档,写入 AutoSnippet/skills/<name>/SKILL.md。\n' +
632
633
  'Skill 是 Agent 的领域知识增强文档,帮助 Agent 正确执行特定任务。\n' +
633
634
  '创建后自动更新编辑器索引(.cursor/rules/autosnippet-skills.mdc),使 Skill 被 AI Agent 被动发现。\n' +
634
635
  '\n' +
@@ -1,8 +1,15 @@
1
1
  /**
2
2
  * 请求日志中间件
3
3
  * 使用 res.on('finish') 替代猴子补丁 res.send
4
+ *
5
+ * 精简策略:
6
+ * - GET 请求 + 2xx 状态码: 降为 debug(Dashboard 轮询高频噪音)
7
+ * - 非 GET / 非 2xx / 慢请求(>2s): 保留 info 级别
4
8
  */
5
9
 
10
+ // 轮询/心跳路径 — 完全静默
11
+ const SILENT_PATHS = ['/api/health', '/api/realtime/events', '/api/sse'];
12
+
6
13
  export function requestLogger(logger) {
7
14
  return (req, res, next) => {
8
15
  const startTime = Date.now();
@@ -10,14 +17,26 @@ export function requestLogger(logger) {
10
17
  res.on('finish', () => {
11
18
  const duration = Date.now() - startTime;
12
19
 
13
- logger.info('HTTP Request', {
20
+ // 完全静默的路径
21
+ if (SILENT_PATHS.some(p => req.path.startsWith(p))) return;
22
+
23
+ const logData = {
14
24
  method: req.method,
15
25
  path: req.path,
16
- query: Object.keys(req.query).length > 0 ? req.query : undefined,
17
26
  statusCode: res.statusCode,
18
27
  duration: `${duration}ms`,
19
- timestamp: new Date().toISOString(),
20
- });
28
+ };
29
+
30
+ // 非 GET / 非 2xx / 慢请求 → info; 其余 → debug
31
+ const isNoisy = req.method === 'GET' && res.statusCode >= 200 && res.statusCode < 300 && duration < 2000;
32
+ const isSlow = duration >= 1000;
33
+ if (isSlow) {
34
+ logger.warn(`🐌慢请求: ${req.method} ${req.path} - ${duration}ms`, logData);
35
+ } else if (isNoisy) {
36
+ logger.debug('HTTP', logData);
37
+ } else {
38
+ logger.info('HTTP', logData);
39
+ }
21
40
  });
22
41
 
23
42
  next();
@@ -79,7 +79,9 @@ router.post('/config', asyncHandler(async (req, res) => {
79
79
  const container = getServiceContainer();
80
80
  container.singletons.aiProvider = newProvider;
81
81
  logger.info('AI provider synced to DI container', { provider: provider.toLowerCase(), model: newProvider.model });
82
- } catch { /* container 未初始化时不阻塞 */ }
82
+ } catch (err) {
83
+ logger.debug('DI container 同步 AI provider 失败', { error: err.message });
84
+ }
83
85
 
84
86
  res.json({
85
87
  success: true,
@@ -23,8 +23,9 @@ const AUTH_USERNAME = process.env.ASD_AUTH_USERNAME || 'admin';
23
23
  const AUTH_PASSWORD = process.env.ASD_AUTH_PASSWORD || 'autosnippet';
24
24
  const TOKEN_SECRET = process.env.ASD_AUTH_SECRET || crypto.randomBytes(32).toString('hex');
25
25
 
26
- // 安全警告:使用默认凭据时提示
27
- if (!process.env.ASD_AUTH_USERNAME || !process.env.ASD_AUTH_PASSWORD) {
26
+ // 安全警告:仅在认证启用且使用默认凭据时提示
27
+ const authEnabled = process.env.VITE_AUTH_ENABLED === 'true' || process.env.ASD_AUTH_ENABLED === 'true';
28
+ if (authEnabled && (!process.env.ASD_AUTH_USERNAME || !process.env.ASD_AUTH_PASSWORD)) {
28
29
  console.warn(
29
30
  '[auth] WARNING: Using default credentials (admin/autosnippet). '
30
31
  + 'Set ASD_AUTH_USERNAME and ASD_AUTH_PASSWORD environment variables for production.',
@@ -7,27 +7,14 @@ import express from 'express';
7
7
  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
+ import Logger from '../../infrastructure/logging/Logger.js';
11
+ import { getContext, safeInt } from '../utils/routeHelpers.js';
10
12
 
11
13
  const router = express.Router();
14
+ const logger = Logger.getInstance();
12
15
 
13
16
  const MAX_BATCH_SIZE = 100;
14
17
 
15
- /** 从请求中提取操作上下文 */
16
- function getContext(req) {
17
- return {
18
- userId: req.headers['x-user-id'] || 'anonymous',
19
- ip: req.ip,
20
- userAgent: req.headers['user-agent'] || '',
21
- };
22
- }
23
-
24
- /** 安全的整数解析 */
25
- function safeInt(value, defaultValue, min = 1, max = 1000) {
26
- const parsed = parseInt(value, 10);
27
- if (Number.isNaN(parsed)) return defaultValue;
28
- return Math.max(min, Math.min(max, parsed));
29
- }
30
-
31
18
  /**
32
19
  * GET /api/v1/candidates
33
20
  * 获取候选项列表(支持筛选和分页)
@@ -112,7 +99,9 @@ router.post('/', asyncHandler(async (req, res) => {
112
99
  duplicateCheck = await chatAgent.executeTool('check_duplicate', {
113
100
  candidate: candidateForCheck,
114
101
  });
115
- } catch { /* 查重失败不阻塞创建 */ }
102
+ } catch (err) {
103
+ logger.warn('自动查重失败,不阻塞创建', { error: err.message });
104
+ }
116
105
 
117
106
  res.status(201).json({
118
107
  success: true,
@@ -249,12 +238,35 @@ router.post('/batch-delete', asyncHandler(async (req, res) => {
249
238
  }
250
239
  const container = getServiceContainer();
251
240
  const candidateService = container.get('candidateService');
252
- const candidateRepo = container.get('candidateRepository');
253
- const list = await candidateService.listCandidates({ category: targetName }, { page: 1, pageSize: 2000 });
254
- const items = list.data || list.items || [];
241
+
242
+ // 查两次:按 category 字段 + 全量扫描 metadata.targetName
243
+ // (前端分组 key = metadata.targetName || category,两者可能不同)
244
+ const byCategory = await candidateService.listCandidates({ category: targetName }, { page: 1, pageSize: 2000 });
245
+ const byCategoryItems = byCategory.data || byCategory.items || [];
246
+
247
+ // 全量扫描 metadata.targetName 匹配(避免 category 不一致导致漏删)
248
+ const allList = await candidateService.listCandidates({}, { page: 1, pageSize: 5000 });
249
+ const allItems = allList.data || allList.items || [];
250
+ const byTargetName = allItems.filter(c => {
251
+ const meta = c.metadata || {};
252
+ return meta.targetName === targetName;
253
+ });
254
+
255
+ // 合并去重
256
+ const seen = new Set();
257
+ const merged = [];
258
+ for (const item of [...byCategoryItems, ...byTargetName]) {
259
+ if (!seen.has(item.id)) {
260
+ seen.add(item.id);
261
+ merged.push(item);
262
+ }
263
+ }
264
+
255
265
  let deleted = 0;
256
- for (const item of items) {
257
- try { await candidateRepo.delete(item.id); deleted++; } catch { /* skip */ }
266
+ for (const item of merged) {
267
+ try { await candidateService.deleteCandidate(item.id, { userId: 'batch-delete' }); deleted++; } catch (err) {
268
+ logger.debug('批量删除: 单条失败', { id: item.id, error: err.message });
269
+ }
258
270
  }
259
271
  res.json({ success: true, data: { deleted } });
260
272
  }));
@@ -298,7 +310,9 @@ router.post('/similarity', asyncHandler(async (req, res) => {
298
310
  if (result && result.length > 0) {
299
311
  return res.json({ success: true, data: { similar: result } });
300
312
  }
301
- } catch { /* fallback to text similarity */ }
313
+ } catch (err) {
314
+ logger.debug('SimilarityService 不可用,降级到文本相似度', { error: err.message });
315
+ }
302
316
 
303
317
  // Fallback: text-based similarity
304
318
  const allRecipes = await recipeService.listRecipes({}, { page: 1, pageSize: 500 });
@@ -490,11 +504,21 @@ router.post('/refine-preview', asyncHandler(async (req, res) => {
490
504
 
491
505
  const preview = result.results?.[0]?.preview || {};
492
506
 
493
- // 构建 after 对象
507
+ // 构建 after 对象(增强 code 字段校验:防止 AI 返回截断/片段代码或类型变更)
508
+ const origCode = before.code || '';
509
+ const isOrigMarkdown = /^---\s*\n/.test(origCode) || /^#\s+/.test(origCode) || (origCode.match(/^#{1,3}\s+/gm) || []).length >= 2;
510
+ const isPreviewMarkdown = preview.code && (/^---\s*\n/.test(preview.code) || /^#\s+/.test(preview.code) || (preview.code.match(/^#{1,3}\s+/gm) || []).length >= 2);
511
+ // 防止源代码被转成 Markdown 文档
512
+ const codeTypeChanged = !isOrigMarkdown && isPreviewMarkdown;
513
+ const isCodeValid = preview.code
514
+ && preview.code.length > 50
515
+ && preview.code !== before.code
516
+ && preview.code.length >= before.code.length * 0.4 // 不能太短(防止截断)
517
+ && !codeTypeChanged; // 不允许类型变更
494
518
  const after = {
495
519
  title: preview.title || before.title,
496
520
  summary: preview.summary || before.summary,
497
- code: (preview.code && preview.code.length > 50 && preview.code !== before.code) ? preview.code : before.code,
521
+ code: isCodeValid ? preview.code : before.code,
498
522
  tags: preview.tags ? [...new Set([...(before.tags || []), ...preview.tags])] : before.tags,
499
523
  confidence: (typeof preview.confidence === 'number' && preview.confidence !== 0.6) ? preview.confidence : before.confidence,
500
524
  relations: (preview.relations && Array.isArray(preview.relations) && preview.relations.length > 0) ? preview.relations : before.relations,
@@ -238,12 +238,4 @@ router.post('/files/save', asyncHandler(async (req, res) => {
238
238
  }
239
239
  }));
240
240
 
241
- /**
242
- * POST /api/v1/commands/execute
243
- * Execute command (stub - not supported for security)
244
- */
245
- router.post('/execute', asyncHandler(async (req, res) => {
246
- res.json({ success: false, error: 'Execute not supported' });
247
- }));
248
-
249
241
  export default router;
@@ -7,27 +7,12 @@ import express from 'express';
7
7
  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
+ import { getContext, safeInt } from '../utils/routeHelpers.js';
10
11
 
11
12
  const router = express.Router();
12
13
 
13
14
  const MAX_BATCH_SIZE = 100;
14
15
 
15
- /** 从请求中提取操作上下文 */
16
- function getContext(req) {
17
- return {
18
- userId: req.headers['x-user-id'] || 'anonymous',
19
- ip: req.ip,
20
- userAgent: req.headers['user-agent'] || '',
21
- };
22
- }
23
-
24
- /** 安全的整数解析 */
25
- function safeInt(value, defaultValue, min = 1, max = 1000) {
26
- const parsed = parseInt(value, 10);
27
- if (Number.isNaN(parsed)) return defaultValue;
28
- return Math.max(min, Math.min(max, parsed));
29
- }
30
-
31
16
  /**
32
17
  * 将 Recipe 实体 → Guard 规则扁平格式(Dashboard GuardView 期望)
33
18
  */
@@ -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;