autosnippet 2.6.0 → 2.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +137 -65
- package/bin/api-server.js +5 -0
- package/bin/cli.js +6 -1
- package/dashboard/dist/assets/{icons-rnn04CvH.js → icons-B_Xg4B-s.js} +148 -88
- package/dashboard/dist/assets/index-BjfUm8p9.js +197 -0
- package/dashboard/dist/assets/index-CkIih2CC.css +1 -0
- package/dashboard/dist/assets/{react-markdown-CWxUbOf4.js → react-markdown-BA6FB2NP.js} +1 -1
- package/dashboard/dist/assets/{syntax-highlighter-CJ2drQQb.js → syntax-highlighter-CVLHn9O5.js} +1 -1
- package/dashboard/dist/assets/{vendor-f83ah6cm.js → vendor-BotF760a.js} +61 -61
- package/dashboard/dist/index.html +6 -6
- package/lib/bootstrap.js +18 -1
- package/lib/cli/SetupService.js +86 -8
- package/lib/cli/UpgradeService.js +139 -2
- package/lib/core/ast/ProjectGraph.js +599 -0
- package/lib/core/gateway/GatewayActionRegistry.js +2 -2
- package/lib/domain/recipe/Recipe.js +3 -0
- package/lib/external/ai/AiProvider.js +83 -20
- package/lib/external/ai/providers/ClaudeProvider.js +208 -0
- package/lib/external/ai/providers/GoogleGeminiProvider.js +247 -1
- package/lib/external/ai/providers/OpenAiProvider.js +141 -0
- package/lib/external/mcp/McpServer.js +6 -1
- package/lib/external/mcp/handlers/bootstrap/pipeline/dimension-context.js +216 -0
- package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +657 -0
- package/lib/external/mcp/handlers/bootstrap/pipeline/tier-scheduler.js +160 -0
- package/lib/external/mcp/handlers/bootstrap/skills.js +225 -0
- package/lib/external/mcp/handlers/bootstrap.js +159 -1634
- package/lib/external/mcp/handlers/browse.js +1 -1
- package/lib/external/mcp/handlers/candidate.js +1 -33
- package/lib/external/mcp/handlers/skill.js +58 -17
- package/lib/external/mcp/tools.js +4 -3
- package/lib/http/middleware/requestLogger.js +23 -4
- package/lib/http/routes/ai.js +158 -2
- package/lib/http/routes/auth.js +3 -2
- package/lib/http/routes/candidates.js +49 -25
- package/lib/http/routes/commands.js +0 -8
- package/lib/http/routes/guardRules.js +1 -16
- package/lib/http/routes/recipes.js +4 -17
- package/lib/http/routes/search.js +11 -19
- package/lib/http/routes/skills.js +2 -0
- package/lib/http/routes/snippets.js +0 -33
- package/lib/http/routes/spm.js +37 -63
- package/lib/http/utils/routeHelpers.js +31 -0
- package/lib/infrastructure/config/Paths.js +12 -0
- package/lib/infrastructure/database/DatabaseConnection.js +6 -1
- package/lib/infrastructure/logging/Logger.js +86 -3
- package/lib/infrastructure/realtime/RealtimeService.js +2 -5
- package/lib/infrastructure/vector/JsonVectorAdapter.js +26 -1
- package/lib/injection/ServiceContainer.js +55 -2
- package/lib/service/bootstrap/BootstrapTaskManager.js +400 -0
- package/lib/service/candidate/CandidateFileWriter.js +72 -27
- package/lib/service/candidate/CandidateService.js +156 -10
- package/lib/service/chat/AnalystAgent.js +245 -0
- package/lib/service/chat/CandidateGuardrail.js +134 -0
- package/lib/service/chat/ChatAgent.js +1055 -167
- package/lib/service/chat/ContextWindow.js +730 -0
- package/lib/service/chat/ConversationStore.js +3 -0
- package/lib/service/chat/HandoffProtocol.js +181 -0
- package/lib/service/chat/Memory.js +3 -0
- package/lib/service/chat/ProducerAgent.js +293 -0
- package/lib/service/chat/ToolRegistry.js +149 -5
- package/lib/service/chat/tools.js +1404 -61
- package/lib/service/guard/ExclusionManager.js +2 -0
- package/lib/service/guard/RuleLearner.js +2 -0
- package/lib/service/quality/FeedbackCollector.js +2 -0
- package/lib/service/recipe/RecipeFileWriter.js +16 -1
- package/lib/service/recipe/RecipeStatsTracker.js +2 -0
- package/lib/service/skills/SignalCollector.js +33 -6
- package/lib/service/skills/SkillAdvisor.js +2 -1
- package/lib/service/skills/SkillHooks.js +13 -5
- package/lib/service/spm/SpmService.js +2 -2
- package/lib/shared/PathGuard.js +314 -0
- package/package.json +1 -1
- package/resources/native-ui/combined-window.swift +494 -0
- package/templates/copilot-instructions.md +20 -3
- package/templates/cursor-rules/autosnippet-conventions.mdc +21 -4
- package/templates/cursor-rules/autosnippet-skills.mdc +45 -0
- package/dashboard/dist/assets/index-BBKa3Dgi.js +0 -195
- package/dashboard/dist/assets/index-DLsECfzW.css +0 -1
|
@@ -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
|
|
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,
|
|
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
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
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,
|
|
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
|
|
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' ?
|
|
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(
|
|
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 — 写入
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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,
|
|
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(
|
|
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
|
-
|
|
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
|
-
'
|
|
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 文档,写入
|
|
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
|
-
|
|
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
|
-
|
|
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();
|
package/lib/http/routes/ai.js
CHANGED
|
@@ -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 {
|
|
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;
|
package/lib/http/routes/auth.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
|
257
|
-
try { await
|
|
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 {
|
|
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:
|
|
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
|
*/
|