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
@@ -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,26 @@
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';
17
+ import pathGuard from '../../../shared/PathGuard.js';
16
18
 
17
19
  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');
20
+ const SKILLS_DIR = path.resolve(__dirname, '../../../../skills');
21
+
22
+ /**
23
+ * 获取用户项目根目录(运行时动态解析)
24
+ */
25
+ function _getProjectRoot() {
26
+ return process.env.ASD_PROJECT_DIR || process.cwd();
27
+ }
28
+
29
+ /**
30
+ * 获取项目级 Skills 目录(运行时动态解析)
31
+ * 路径: {projectRoot}/AutoSnippet/skills/ — 跟随项目走
32
+ */
33
+ function _getProjectSkillsDir() {
34
+ return getProjectSkillsPath(_getProjectRoot());
35
+ }
21
36
 
22
37
  /**
23
38
  * 解析 SKILL.md frontmatter 全部元数据
@@ -93,10 +108,11 @@ export function listSkills() {
93
108
 
94
109
  // 项目级 Skills(覆盖同名内置)
95
110
  try {
96
- const projectDirs = fs.readdirSync(PROJECT_SKILLS_DIR, { withFileTypes: true })
111
+ const projectSkillsDir = _getProjectSkillsDir();
112
+ const projectDirs = fs.readdirSync(projectSkillsDir, { withFileTypes: true })
97
113
  .filter(d => d.isDirectory()).map(d => d.name);
98
114
  for (const name of projectDirs) {
99
- const meta = _parseSkillMeta(name, PROJECT_SKILLS_DIR);
115
+ const meta = _parseSkillMeta(name, projectSkillsDir);
100
116
  skillMap.set(name, { name, source: 'project', summary: meta.description, createdBy: meta.createdBy, createdAt: meta.createdAt, useCase: SKILL_USE_CASES[name] || null });
101
117
  }
102
118
  } catch { /* no project skills */ }
@@ -151,7 +167,8 @@ export function loadSkill(_ctx, args) {
151
167
  }
152
168
 
153
169
  // 项目级 Skills 优先
154
- const projectSkillPath = path.join(PROJECT_SKILLS_DIR, skillName, 'SKILL.md');
170
+ const projectSkillsDir = _getProjectSkillsDir();
171
+ const projectSkillPath = path.join(projectSkillsDir, skillName, 'SKILL.md');
155
172
  const builtinSkillPath = path.join(SKILLS_DIR, skillName, 'SKILL.md');
156
173
  const skillPath = fs.existsSync(projectSkillPath) ? projectSkillPath : builtinSkillPath;
157
174
  const source = skillPath === projectSkillPath ? 'project' : 'builtin';
@@ -172,7 +189,7 @@ export function loadSkill(_ctx, args) {
172
189
  }
173
190
 
174
191
  // 提取 createdBy/createdAt
175
- const meta = _parseSkillMeta(skillName, source === 'project' ? PROJECT_SKILLS_DIR : SKILLS_DIR);
192
+ const meta = _parseSkillMeta(skillName, source === 'project' ? projectSkillsDir : SKILLS_DIR);
176
193
 
177
194
  return JSON.stringify({
178
195
  success: true,
@@ -191,7 +208,7 @@ export function loadSkill(_ctx, args) {
191
208
  // 列出所有可用 Skills
192
209
  const available = new Set();
193
210
  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 {}
211
+ try { fs.readdirSync(_getProjectSkillsDir(), { withFileTypes: true }).filter(d => d.isDirectory()).forEach(d => available.add(d.name)); } catch {}
195
212
 
196
213
  return JSON.stringify({
197
214
  success: false,
@@ -209,7 +226,7 @@ export function loadSkill(_ctx, args) {
209
226
  // ═══════════════════════════════════════════════════════════
210
227
 
211
228
  /**
212
- * 创建项目级 Skill — 写入 .autosnippet/skills/<name>/SKILL.md
229
+ * 创建项目级 Skill — 写入 {projectRoot}/AutoSnippet/skills/<name>/SKILL.md
213
230
  * 创建后自动 regenerate 编辑器索引(.cursor/rules/autosnippet-skills.mdc)
214
231
  *
215
232
  * @param {object} _ctx MCP context
@@ -217,7 +234,7 @@ export function loadSkill(_ctx, args) {
217
234
  * @returns {string} JSON envelope
218
235
  */
219
236
  export function createSkill(_ctx, args) {
220
- const { name, description, content, overwrite = false, createdBy = 'external-ai' } = args || {};
237
+ const { name, description, content, overwrite = false, createdBy = 'external-ai', title } = args || {};
221
238
 
222
239
  // ── 参数校验 ──
223
240
  if (!name || !description || !content) {
@@ -251,7 +268,8 @@ export function createSkill(_ctx, args) {
251
268
  }
252
269
 
253
270
  // 检查同名项目级 Skill
254
- const skillDir = path.join(PROJECT_SKILLS_DIR, name);
271
+ const projectSkillsDir = _getProjectSkillsDir();
272
+ const skillDir = path.join(projectSkillsDir, name);
255
273
  const skillPath = path.join(skillDir, 'SKILL.md');
256
274
  if (fs.existsSync(skillPath) && !overwrite) {
257
275
  return JSON.stringify({
@@ -265,17 +283,29 @@ export function createSkill(_ctx, args) {
265
283
 
266
284
  // ── 写入 SKILL.md ──
267
285
  try {
286
+ // 路径安全检查 — name 来自用户输入,可能含路径字符
287
+ pathGuard.assertProjectWriteSafe(skillDir);
268
288
  fs.mkdirSync(skillDir, { recursive: true });
269
289
 
270
- const frontmatter = [
290
+ // 自动推断 title: 优先使用传入参数,否则从 content 的第一个 # heading 提取
291
+ const resolvedTitle = title || (() => {
292
+ const m = (content || '').match(/^#\s+(.+)/m);
293
+ return m ? m[1].trim() : '';
294
+ })();
295
+
296
+ const fmLines = [
271
297
  '---',
272
298
  `name: ${name}`,
299
+ ];
300
+ if (resolvedTitle) fmLines.push(`title: "${resolvedTitle.replace(/"/g, '\\"')}"`);
301
+ fmLines.push(
273
302
  `description: ${description}`,
274
303
  `createdBy: ${createdBy}`,
275
304
  `createdAt: ${new Date().toISOString()}`,
276
305
  '---',
277
306
  '',
278
- ].join('\n');
307
+ );
308
+ const frontmatter = fmLines.join('\n');
279
309
 
280
310
  fs.writeFileSync(skillPath, frontmatter + content, 'utf8');
281
311
  } catch (err) {
@@ -288,6 +318,13 @@ export function createSkill(_ctx, args) {
288
318
  // ── regenerate 编辑器索引 ──
289
319
  const indexResult = _regenerateEditorIndex();
290
320
 
321
+ // ── 清理 SignalCollector 已创建的 pendingSuggestions ──
322
+ try {
323
+ if (global._signalCollector) {
324
+ global._signalCollector.removePendingSuggestion(name);
325
+ }
326
+ } catch { /* silent */ }
327
+
291
328
  return JSON.stringify({
292
329
  success: true,
293
330
  data: {
@@ -310,19 +347,23 @@ function _regenerateEditorIndex() {
310
347
  try {
311
348
  // 扫描项目级 Skills
312
349
  let projectSkills = [];
350
+ const projectSkillsDir = _getProjectSkillsDir();
313
351
  try {
314
- const dirs = fs.readdirSync(PROJECT_SKILLS_DIR, { withFileTypes: true })
352
+ const dirs = fs.readdirSync(projectSkillsDir, { withFileTypes: true })
315
353
  .filter(d => d.isDirectory())
316
354
  .map(d => d.name);
317
355
  for (const name of dirs) {
318
- const meta = _parseSkillMeta(name, PROJECT_SKILLS_DIR);
356
+ const meta = _parseSkillMeta(name, projectSkillsDir);
319
357
  projectSkills.push({ name, summary: meta.description });
320
358
  }
321
359
  } catch { /* no project skills dir */ }
322
360
 
361
+ const projectRoot = _getProjectRoot();
362
+ const rulesDir = path.join(projectRoot, '.cursor', 'rules');
363
+
323
364
  if (projectSkills.length === 0) {
324
365
  // 没有项目级 Skills 时,删除索引文件(如果存在)
325
- const indexPath = path.join(PROJECT_ROOT, '.cursor', 'rules', 'autosnippet-skills.mdc');
366
+ const indexPath = path.join(rulesDir, 'autosnippet-skills.mdc');
326
367
  try { fs.unlinkSync(indexPath); } catch { /* not exists */ }
327
368
  return { success: true, skillCount: 0 };
328
369
  }
@@ -347,7 +388,7 @@ function _regenerateEditorIndex() {
347
388
  ].join('\n');
348
389
 
349
390
  // 写入 .cursor/rules/
350
- const rulesDir = path.join(PROJECT_ROOT, '.cursor', 'rules');
391
+ pathGuard.assertProjectWriteSafe(rulesDir);
351
392
  fs.mkdirSync(rulesDir, { recursive: true });
352
393
  const indexPath = path.join(rulesDir, 'autosnippet-skills.mdc');
353
394
  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();
@@ -1,9 +1,11 @@
1
1
  /**
2
2
  * AI API 路由
3
- * AI 提供商管理、摘要、翻译、对话
3
+ * AI 提供商管理、摘要、翻译、对话、.env LLM 配置
4
4
  */
5
5
 
6
6
  import express from 'express';
7
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
8
+ import { join } from 'node:path';
7
9
  import { asyncHandler } from '../middleware/errorHandler.js';
8
10
  import { getServiceContainer } from '../../injection/ServiceContainer.js';
9
11
  import { createProvider } from '../../external/ai/AiFactory.js';
@@ -79,7 +81,9 @@ router.post('/config', asyncHandler(async (req, res) => {
79
81
  const container = getServiceContainer();
80
82
  container.singletons.aiProvider = newProvider;
81
83
  logger.info('AI provider synced to DI container', { provider: provider.toLowerCase(), model: newProvider.model });
82
- } catch { /* container 未初始化时不阻塞 */ }
84
+ } catch (err) {
85
+ logger.debug('DI container 同步 AI provider 失败', { error: err.message });
86
+ }
83
87
 
84
88
  res.json({
85
89
  success: true,
@@ -242,4 +246,156 @@ router.post('/format-usage-guide', asyncHandler(async (req, res) => {
242
246
  res.json({ success: true, data: { formatted } });
243
247
  }));
244
248
 
249
+ // ═══════════════════════════════════════════════════════
250
+ // .env LLM 配置读写
251
+ // ═══════════════════════════════════════════════════════
252
+
253
+ /** 获取用户项目目录下 .env 的路径 */
254
+ function _getProjectEnvPath() {
255
+ const container = getServiceContainer();
256
+ const projectRoot = container.singletons?._projectRoot || process.env.ASD_PROJECT_DIR || process.cwd();
257
+ return join(projectRoot, '.env');
258
+ }
259
+
260
+ /** LLM 相关的 env 变量名 → 标签映射 */
261
+ const LLM_ENV_KEYS = [
262
+ 'ASD_AI_PROVIDER',
263
+ 'ASD_AI_MODEL',
264
+ 'ASD_GOOGLE_API_KEY',
265
+ 'ASD_OPENAI_API_KEY',
266
+ 'ASD_CLAUDE_API_KEY',
267
+ 'ASD_DEEPSEEK_API_KEY',
268
+ 'ASD_AI_PROXY',
269
+ ];
270
+
271
+ /**
272
+ * 解析 .env 内容为 key-value(仅提取 LLM 相关变量)
273
+ * 返回 { vars, hasEnvFile, llmReady }
274
+ * llmReady: provider + 至少一个对应 API Key 已配置
275
+ */
276
+ function parseLlmEnv(envPath) {
277
+ if (!existsSync(envPath)) {
278
+ return { vars: {}, hasEnvFile: false, llmReady: false };
279
+ }
280
+
281
+ const raw = readFileSync(envPath, 'utf8');
282
+ const vars = {};
283
+
284
+ for (const line of raw.split('\n')) {
285
+ const trimmed = line.trim();
286
+ // 跳过注释和空行
287
+ if (!trimmed || trimmed.startsWith('#')) continue;
288
+ const eqIdx = trimmed.indexOf('=');
289
+ if (eqIdx === -1) continue;
290
+ const key = trimmed.slice(0, eqIdx).trim();
291
+ const val = trimmed.slice(eqIdx + 1).trim().replace(/^["']|["']$/g, '');
292
+ if (LLM_ENV_KEYS.includes(key)) {
293
+ vars[key] = val;
294
+ }
295
+ }
296
+
297
+ // 判断 LLM 是否可用:有 provider + 对应的 API Key
298
+ const provider = vars.ASD_AI_PROVIDER || '';
299
+ const keyMap = {
300
+ google: 'ASD_GOOGLE_API_KEY',
301
+ openai: 'ASD_OPENAI_API_KEY',
302
+ claude: 'ASD_CLAUDE_API_KEY',
303
+ deepseek: 'ASD_DEEPSEEK_API_KEY',
304
+ ollama: '', // ollama 不需要 key
305
+ mock: '', // mock 不需要 key
306
+ };
307
+ const neededKey = keyMap[provider] || '';
308
+ const llmReady = !!provider && (!neededKey || !!vars[neededKey]);
309
+
310
+ return { vars, hasEnvFile: true, llmReady };
311
+ }
312
+
313
+ /**
314
+ * GET /api/v1/ai/env-config
315
+ * 读取用户项目 .env 中的 LLM 配置
316
+ */
317
+ router.get('/env-config', asyncHandler(async (req, res) => {
318
+ const envPath = _getProjectEnvPath();
319
+ const result = parseLlmEnv(envPath);
320
+ res.json({ success: true, data: result });
321
+ }));
322
+
323
+ /**
324
+ * POST /api/v1/ai/env-config
325
+ * 写入 / 更新用户项目 .env 中的 LLM 配置
326
+ *
327
+ * Body: { provider, model, apiKey, proxy? }
328
+ */
329
+ router.post('/env-config', asyncHandler(async (req, res) => {
330
+ const { provider, model, apiKey, proxy } = req.body;
331
+ if (!provider || typeof provider !== 'string') {
332
+ throw new ValidationError('provider is required');
333
+ }
334
+
335
+ const envPath = _getProjectEnvPath();
336
+ let content = existsSync(envPath) ? readFileSync(envPath, 'utf8') : '';
337
+
338
+ // 构建 key-value 更新列表
339
+ const updates = {
340
+ ASD_AI_PROVIDER: provider,
341
+ };
342
+ if (model) updates.ASD_AI_MODEL = model;
343
+ if (proxy) updates.ASD_AI_PROXY = proxy;
344
+
345
+ // 根据 provider 决定写入哪个 API Key 变量
346
+ const providerKeyMap = {
347
+ google: 'ASD_GOOGLE_API_KEY',
348
+ openai: 'ASD_OPENAI_API_KEY',
349
+ claude: 'ASD_CLAUDE_API_KEY',
350
+ deepseek: 'ASD_DEEPSEEK_API_KEY',
351
+ };
352
+ const keyName = providerKeyMap[provider];
353
+ if (keyName && apiKey) {
354
+ updates[keyName] = apiKey;
355
+ }
356
+
357
+ // 逐条合并到 .env 内容
358
+ for (const [k, v] of Object.entries(updates)) {
359
+ // 匹配已有行(包括被注释的行)
360
+ const activeRe = new RegExp(`^${k}\\s*=.*$`, 'm');
361
+ const commentedRe = new RegExp(`^#\\s*${k}\\s*=.*$`, 'm');
362
+
363
+ if (activeRe.test(content)) {
364
+ // 替换已有活动行
365
+ content = content.replace(activeRe, `${k}=${v}`);
366
+ } else if (commentedRe.test(content)) {
367
+ // 取消注释并赋值
368
+ content = content.replace(commentedRe, `${k}=${v}`);
369
+ } else {
370
+ // 追加到末尾
371
+ if (!content.endsWith('\n')) content += '\n';
372
+ content += `${k}=${v}\n`;
373
+ }
374
+ }
375
+
376
+ writeFileSync(envPath, content);
377
+ logger.info('LLM env config updated', { provider, model });
378
+
379
+ // 同步到当前进程环境变量(热生效)
380
+ for (const [k, v] of Object.entries(updates)) {
381
+ process.env[k] = v;
382
+ }
383
+
384
+ // 尝试热切换 AI Provider
385
+ try {
386
+ const newProvider = createProvider({
387
+ provider: provider.toLowerCase(),
388
+ model: model || undefined,
389
+ });
390
+ const container = getServiceContainer();
391
+ container.singletons.aiProvider = newProvider;
392
+ logger.info('AI provider hot-swapped after env update', { provider, model: newProvider.model });
393
+ } catch (err) {
394
+ logger.debug('Hot-swap AI provider failed (will take effect on restart)', { error: err.message });
395
+ }
396
+
397
+ const result = parseLlmEnv(envPath);
398
+ res.json({ success: true, data: result });
399
+ }));
400
+
245
401
  export default router;
@@ -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
  */