autosnippet 2.5.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.
- package/bin/cli.js +35 -0
- package/dashboard/dist/assets/{icons-Dtm0E6DS.js → icons-Cq4-iQhP.js} +152 -87
- package/dashboard/dist/assets/index-DBxH7pVn.css +1 -0
- package/dashboard/dist/assets/index-Dw2F6qAS.js +197 -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 +1 -1
- package/lib/cli/SetupService.js +33 -8
- package/lib/cli/UpgradeService.js +139 -2
- package/lib/core/ast/ProjectGraph.js +599 -0
- package/lib/core/gateway/Gateway.js +19 -4
- package/lib/core/gateway/GatewayActionRegistry.js +2 -2
- package/lib/domain/recipe/Recipe.js +3 -0
- package/lib/external/ai/AiProvider.js +117 -10
- package/lib/external/ai/providers/ClaudeProvider.js +197 -0
- package/lib/external/ai/providers/GoogleGeminiProvider.js +235 -1
- package/lib/external/ai/providers/OpenAiProvider.js +131 -0
- package/lib/external/mcp/McpServer.js +2 -1
- package/lib/external/mcp/handlers/bootstrap/pipeline/dimension-context.js +216 -0
- package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +468 -0
- package/lib/external/mcp/handlers/bootstrap/pipeline/tier-scheduler.js +162 -0
- package/lib/external/mcp/handlers/bootstrap/skills.js +225 -0
- package/lib/external/mcp/handlers/bootstrap.js +151 -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 +126 -31
- package/lib/external/mcp/tools.js +25 -3
- package/lib/http/middleware/requestLogger.js +23 -4
- package/lib/http/routes/ai.js +3 -1
- 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 +16 -22
- package/lib/http/routes/skills.js +40 -3
- 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/audit/AuditStore.js +18 -0
- package/lib/infrastructure/config/Paths.js +9 -0
- package/lib/infrastructure/logging/Logger.js +86 -3
- package/lib/infrastructure/realtime/RealtimeService.js +2 -5
- package/lib/infrastructure/vector/JsonVectorAdapter.js +24 -1
- package/lib/injection/ServiceContainer.js +62 -3
- package/lib/service/bootstrap/BootstrapTaskManager.js +400 -0
- package/lib/service/candidate/CandidateFileWriter.js +68 -27
- package/lib/service/candidate/CandidateService.js +156 -10
- package/lib/service/chat/AnalystAgent.js +216 -0
- package/lib/service/chat/CandidateGuardrail.js +134 -0
- package/lib/service/chat/ChatAgent.js +1272 -155
- package/lib/service/chat/ContextWindow.js +730 -0
- package/lib/service/chat/ConversationStore.js +377 -0
- package/lib/service/chat/HandoffProtocol.js +180 -0
- package/lib/service/chat/Memory.js +40 -10
- package/lib/service/chat/ProducerAgent.js +240 -0
- package/lib/service/chat/ToolRegistry.js +149 -5
- package/lib/service/chat/tools.js +1493 -60
- package/lib/service/recipe/RecipeFileWriter.js +12 -1
- package/lib/service/skills/EventAggregator.js +187 -0
- package/lib/service/skills/SignalCollector.js +549 -0
- package/lib/service/skills/SkillAdvisor.js +324 -0
- package/lib/service/skills/SkillHooks.js +13 -5
- package/lib/service/spm/SpmService.js +2 -2
- package/package.json +1 -1
- 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-B7VpZOCz.css +0 -1
- package/dashboard/dist/assets/index-D87IZTmZ.js +0 -187
|
@@ -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,34 +13,55 @@
|
|
|
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
|
|
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');
|
|
21
20
|
|
|
22
21
|
/**
|
|
23
|
-
*
|
|
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
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 解析 SKILL.md frontmatter 全部元数据
|
|
24
38
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
39
|
+
* 返回 { description, createdBy, createdAt },缺失字段为 null。
|
|
40
|
+
* 同时兼容旧格式(无 createdBy 的 SKILL.md)。
|
|
27
41
|
*/
|
|
28
|
-
function
|
|
42
|
+
function _parseSkillMeta(skillName, baseDir = SKILLS_DIR) {
|
|
29
43
|
try {
|
|
30
44
|
const content = fs.readFileSync(
|
|
31
45
|
path.join(baseDir, skillName, 'SKILL.md'), 'utf8',
|
|
32
46
|
);
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
if (
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
47
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
48
|
+
const meta = { description: skillName, createdBy: null, createdAt: null };
|
|
49
|
+
if (fmMatch) {
|
|
50
|
+
const fm = fmMatch[1];
|
|
51
|
+
const descMatch = fm.match(/^description:\s*(.+?)$/m);
|
|
52
|
+
if (descMatch) {
|
|
53
|
+
const desc = descMatch[1].trim();
|
|
54
|
+
const firstSentence = desc.split(/\.\s/)[0];
|
|
55
|
+
meta.description = firstSentence.length < desc.length ? `${firstSentence}.` : desc.substring(0, 120);
|
|
56
|
+
}
|
|
57
|
+
const cbMatch = fm.match(/^createdBy:\s*(.+?)$/m);
|
|
58
|
+
if (cbMatch) meta.createdBy = cbMatch[1].trim();
|
|
59
|
+
const caMatch = fm.match(/^createdAt:\s*(.+?)$/m);
|
|
60
|
+
if (caMatch) meta.createdAt = caMatch[1].trim();
|
|
40
61
|
}
|
|
41
|
-
return
|
|
62
|
+
return meta;
|
|
42
63
|
} catch {
|
|
43
|
-
return skillName;
|
|
64
|
+
return { description: skillName, createdBy: null, createdAt: null };
|
|
44
65
|
}
|
|
45
66
|
}
|
|
46
67
|
|
|
@@ -80,26 +101,39 @@ export function listSkills() {
|
|
|
80
101
|
const builtinDirs = fs.readdirSync(SKILLS_DIR, { withFileTypes: true })
|
|
81
102
|
.filter(d => d.isDirectory()).map(d => d.name);
|
|
82
103
|
for (const name of builtinDirs) {
|
|
83
|
-
|
|
104
|
+
const meta = _parseSkillMeta(name, SKILLS_DIR);
|
|
105
|
+
skillMap.set(name, { name, source: 'builtin', summary: meta.description, createdBy: null, createdAt: null, useCase: SKILL_USE_CASES[name] || null });
|
|
84
106
|
}
|
|
85
107
|
|
|
86
108
|
// 项目级 Skills(覆盖同名内置)
|
|
87
109
|
try {
|
|
88
|
-
const
|
|
110
|
+
const projectSkillsDir = _getProjectSkillsDir();
|
|
111
|
+
const projectDirs = fs.readdirSync(projectSkillsDir, { withFileTypes: true })
|
|
89
112
|
.filter(d => d.isDirectory()).map(d => d.name);
|
|
90
113
|
for (const name of projectDirs) {
|
|
91
|
-
|
|
114
|
+
const meta = _parseSkillMeta(name, projectSkillsDir);
|
|
115
|
+
skillMap.set(name, { name, source: 'project', summary: meta.description, createdBy: meta.createdBy, createdAt: meta.createdAt, useCase: SKILL_USE_CASES[name] || null });
|
|
92
116
|
}
|
|
93
117
|
} catch { /* no project skills */ }
|
|
94
118
|
|
|
95
119
|
const skills = [...skillMap.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
96
120
|
|
|
121
|
+
// _meta:附带 SignalCollector 推荐计数(如果后台服务可用)
|
|
122
|
+
let suggestionCount = 0;
|
|
123
|
+
try {
|
|
124
|
+
if (global._signalCollector) {
|
|
125
|
+
const snapshot = global._signalCollector.getSnapshot();
|
|
126
|
+
suggestionCount = snapshot?.lastResult?.newSuggestions || 0;
|
|
127
|
+
}
|
|
128
|
+
} catch { /* silent */ }
|
|
129
|
+
|
|
97
130
|
return JSON.stringify({
|
|
98
131
|
success: true,
|
|
99
132
|
data: {
|
|
100
133
|
skills,
|
|
101
134
|
total: skills.length,
|
|
102
135
|
hint: '根据当前任务选择合适的 Skill 加载(load_skill)。不确定时先加载 autosnippet-intent 做意图路由。',
|
|
136
|
+
_meta: { signalSuggestions: suggestionCount },
|
|
103
137
|
},
|
|
104
138
|
});
|
|
105
139
|
} catch (err) {
|
|
@@ -132,7 +166,8 @@ export function loadSkill(_ctx, args) {
|
|
|
132
166
|
}
|
|
133
167
|
|
|
134
168
|
// 项目级 Skills 优先
|
|
135
|
-
const
|
|
169
|
+
const projectSkillsDir = _getProjectSkillsDir();
|
|
170
|
+
const projectSkillPath = path.join(projectSkillsDir, skillName, 'SKILL.md');
|
|
136
171
|
const builtinSkillPath = path.join(SKILLS_DIR, skillName, 'SKILL.md');
|
|
137
172
|
const skillPath = fs.existsSync(projectSkillPath) ? projectSkillPath : builtinSkillPath;
|
|
138
173
|
const source = skillPath === projectSkillPath ? 'project' : 'builtin';
|
|
@@ -152,6 +187,9 @@ export function loadSkill(_ctx, args) {
|
|
|
152
187
|
}
|
|
153
188
|
}
|
|
154
189
|
|
|
190
|
+
// 提取 createdBy/createdAt
|
|
191
|
+
const meta = _parseSkillMeta(skillName, source === 'project' ? projectSkillsDir : SKILLS_DIR);
|
|
192
|
+
|
|
155
193
|
return JSON.stringify({
|
|
156
194
|
success: true,
|
|
157
195
|
data: {
|
|
@@ -159,6 +197,8 @@ export function loadSkill(_ctx, args) {
|
|
|
159
197
|
source,
|
|
160
198
|
content,
|
|
161
199
|
charCount: content.length,
|
|
200
|
+
createdBy: source === 'project' ? meta.createdBy : null,
|
|
201
|
+
createdAt: source === 'project' ? meta.createdAt : null,
|
|
162
202
|
useCase: SKILL_USE_CASES[skillName] || null,
|
|
163
203
|
relatedSkills: _getRelatedSkills(skillName),
|
|
164
204
|
},
|
|
@@ -167,7 +207,7 @@ export function loadSkill(_ctx, args) {
|
|
|
167
207
|
// 列出所有可用 Skills
|
|
168
208
|
const available = new Set();
|
|
169
209
|
try { fs.readdirSync(SKILLS_DIR, { withFileTypes: true }).filter(d => d.isDirectory()).forEach(d => available.add(d.name)); } catch {}
|
|
170
|
-
try { fs.readdirSync(
|
|
210
|
+
try { fs.readdirSync(_getProjectSkillsDir(), { withFileTypes: true }).filter(d => d.isDirectory()).forEach(d => available.add(d.name)); } catch {}
|
|
171
211
|
|
|
172
212
|
return JSON.stringify({
|
|
173
213
|
success: false,
|
|
@@ -185,7 +225,7 @@ export function loadSkill(_ctx, args) {
|
|
|
185
225
|
// ═══════════════════════════════════════════════════════════
|
|
186
226
|
|
|
187
227
|
/**
|
|
188
|
-
* 创建项目级 Skill — 写入
|
|
228
|
+
* 创建项目级 Skill — 写入 {projectRoot}/AutoSnippet/skills/<name>/SKILL.md
|
|
189
229
|
* 创建后自动 regenerate 编辑器索引(.cursor/rules/autosnippet-skills.mdc)
|
|
190
230
|
*
|
|
191
231
|
* @param {object} _ctx MCP context
|
|
@@ -193,7 +233,7 @@ export function loadSkill(_ctx, args) {
|
|
|
193
233
|
* @returns {string} JSON envelope
|
|
194
234
|
*/
|
|
195
235
|
export function createSkill(_ctx, args) {
|
|
196
|
-
const { name, description, content, overwrite = false } = args || {};
|
|
236
|
+
const { name, description, content, overwrite = false, createdBy = 'external-ai', title } = args || {};
|
|
197
237
|
|
|
198
238
|
// ── 参数校验 ──
|
|
199
239
|
if (!name || !description || !content) {
|
|
@@ -227,7 +267,8 @@ export function createSkill(_ctx, args) {
|
|
|
227
267
|
}
|
|
228
268
|
|
|
229
269
|
// 检查同名项目级 Skill
|
|
230
|
-
const
|
|
270
|
+
const projectSkillsDir = _getProjectSkillsDir();
|
|
271
|
+
const skillDir = path.join(projectSkillsDir, name);
|
|
231
272
|
const skillPath = path.join(skillDir, 'SKILL.md');
|
|
232
273
|
if (fs.existsSync(skillPath) && !overwrite) {
|
|
233
274
|
return JSON.stringify({
|
|
@@ -243,13 +284,25 @@ export function createSkill(_ctx, args) {
|
|
|
243
284
|
try {
|
|
244
285
|
fs.mkdirSync(skillDir, { recursive: true });
|
|
245
286
|
|
|
246
|
-
|
|
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 = [
|
|
247
294
|
'---',
|
|
248
295
|
`name: ${name}`,
|
|
296
|
+
];
|
|
297
|
+
if (resolvedTitle) fmLines.push(`title: "${resolvedTitle.replace(/"/g, '\\"')}"`);
|
|
298
|
+
fmLines.push(
|
|
249
299
|
`description: ${description}`,
|
|
300
|
+
`createdBy: ${createdBy}`,
|
|
301
|
+
`createdAt: ${new Date().toISOString()}`,
|
|
250
302
|
'---',
|
|
251
303
|
'',
|
|
252
|
-
|
|
304
|
+
);
|
|
305
|
+
const frontmatter = fmLines.join('\n');
|
|
253
306
|
|
|
254
307
|
fs.writeFileSync(skillPath, frontmatter + content, 'utf8');
|
|
255
308
|
} catch (err) {
|
|
@@ -262,6 +315,13 @@ export function createSkill(_ctx, args) {
|
|
|
262
315
|
// ── regenerate 编辑器索引 ──
|
|
263
316
|
const indexResult = _regenerateEditorIndex();
|
|
264
317
|
|
|
318
|
+
// ── 清理 SignalCollector 已创建的 pendingSuggestions ──
|
|
319
|
+
try {
|
|
320
|
+
if (global._signalCollector) {
|
|
321
|
+
global._signalCollector.removePendingSuggestion(name);
|
|
322
|
+
}
|
|
323
|
+
} catch { /* silent */ }
|
|
324
|
+
|
|
265
325
|
return JSON.stringify({
|
|
266
326
|
success: true,
|
|
267
327
|
data: {
|
|
@@ -284,19 +344,23 @@ function _regenerateEditorIndex() {
|
|
|
284
344
|
try {
|
|
285
345
|
// 扫描项目级 Skills
|
|
286
346
|
let projectSkills = [];
|
|
347
|
+
const projectSkillsDir = _getProjectSkillsDir();
|
|
287
348
|
try {
|
|
288
|
-
const dirs = fs.readdirSync(
|
|
349
|
+
const dirs = fs.readdirSync(projectSkillsDir, { withFileTypes: true })
|
|
289
350
|
.filter(d => d.isDirectory())
|
|
290
351
|
.map(d => d.name);
|
|
291
352
|
for (const name of dirs) {
|
|
292
|
-
const
|
|
293
|
-
projectSkills.push({ name, summary });
|
|
353
|
+
const meta = _parseSkillMeta(name, projectSkillsDir);
|
|
354
|
+
projectSkills.push({ name, summary: meta.description });
|
|
294
355
|
}
|
|
295
356
|
} catch { /* no project skills dir */ }
|
|
296
357
|
|
|
358
|
+
const projectRoot = _getProjectRoot();
|
|
359
|
+
const rulesDir = path.join(projectRoot, '.cursor', 'rules');
|
|
360
|
+
|
|
297
361
|
if (projectSkills.length === 0) {
|
|
298
362
|
// 没有项目级 Skills 时,删除索引文件(如果存在)
|
|
299
|
-
const indexPath = path.join(
|
|
363
|
+
const indexPath = path.join(rulesDir, 'autosnippet-skills.mdc');
|
|
300
364
|
try { fs.unlinkSync(indexPath); } catch { /* not exists */ }
|
|
301
365
|
return { success: true, skillCount: 0 };
|
|
302
366
|
}
|
|
@@ -321,7 +385,6 @@ function _regenerateEditorIndex() {
|
|
|
321
385
|
].join('\n');
|
|
322
386
|
|
|
323
387
|
// 写入 .cursor/rules/
|
|
324
|
-
const rulesDir = path.join(PROJECT_ROOT, '.cursor', 'rules');
|
|
325
388
|
fs.mkdirSync(rulesDir, { recursive: true });
|
|
326
389
|
const indexPath = path.join(rulesDir, 'autosnippet-skills.mdc');
|
|
327
390
|
fs.writeFileSync(indexPath, mdcContent, 'utf8');
|
|
@@ -332,6 +395,38 @@ function _regenerateEditorIndex() {
|
|
|
332
395
|
}
|
|
333
396
|
}
|
|
334
397
|
|
|
398
|
+
// ═══════════════════════════════════════════════════════════
|
|
399
|
+
// Handler: suggestSkills
|
|
400
|
+
// ═══════════════════════════════════════════════════════════
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* 基于项目使用模式分析,推荐创建 Skill
|
|
404
|
+
*
|
|
405
|
+
* 分析维度:Guard 违规模式、Memory 偏好积累、Recipe 分布缺口、候选积压
|
|
406
|
+
* Agent 可根据推荐结果自行决定是否调用 createSkill 创建
|
|
407
|
+
*
|
|
408
|
+
* @param {object} ctx MCP context(含 container)
|
|
409
|
+
* @returns {string} JSON envelope
|
|
410
|
+
*/
|
|
411
|
+
export async function suggestSkills(ctx) {
|
|
412
|
+
try {
|
|
413
|
+
const { SkillAdvisor } = await import('../../../service/skills/SkillAdvisor.js');
|
|
414
|
+
const database = ctx?.container?.get?.('database') || null;
|
|
415
|
+
const advisor = new SkillAdvisor(PROJECT_ROOT, { database });
|
|
416
|
+
const result = advisor.suggest();
|
|
417
|
+
|
|
418
|
+
return JSON.stringify({
|
|
419
|
+
success: true,
|
|
420
|
+
data: result,
|
|
421
|
+
});
|
|
422
|
+
} catch (err) {
|
|
423
|
+
return JSON.stringify({
|
|
424
|
+
success: false,
|
|
425
|
+
error: { code: 'SUGGEST_ERROR', message: err.message },
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
335
430
|
/**
|
|
336
431
|
* 推荐相关 Skills(基于静态映射)
|
|
337
432
|
*/
|
|
@@ -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' +
|
|
@@ -658,10 +659,31 @@ export const TOOLS = [
|
|
|
658
659
|
default: false,
|
|
659
660
|
description: '如果同名 Skill 已存在,是否覆盖(默认 false)',
|
|
660
661
|
},
|
|
662
|
+
createdBy: {
|
|
663
|
+
type: 'string',
|
|
664
|
+
enum: ['manual', 'user-ai', 'system-ai', 'external-ai'],
|
|
665
|
+
default: 'external-ai',
|
|
666
|
+
description: '创建者类型:manual=用户手动 | user-ai=用户调用AI | system-ai=系统自动 | external-ai=外部AI Agent',
|
|
667
|
+
},
|
|
661
668
|
},
|
|
662
669
|
required: ['name', 'description', 'content'],
|
|
663
670
|
},
|
|
664
671
|
},
|
|
672
|
+
// 36. Skill 推荐:基于使用模式分析,推荐创建 Skill
|
|
673
|
+
{
|
|
674
|
+
name: 'autosnippet_suggest_skills',
|
|
675
|
+
description:
|
|
676
|
+
'基于项目使用模式分析,推荐创建 Skill。\n' +
|
|
677
|
+
'分析 4 个维度:Guard 违规模式、Memory 偏好积累、Recipe 分布缺口、候选积压率。\n' +
|
|
678
|
+
'返回推荐列表(含 name / description / rationale / priority),Agent 可据此直接调用 autosnippet_create_skill 创建。\n' +
|
|
679
|
+
'\n' +
|
|
680
|
+
'使用时机:\n' +
|
|
681
|
+
' • 项目使用一段时间后,定期调用检查是否有新的 Skill 需求\n' +
|
|
682
|
+
' • 用户反复说"我们项目不用…"、"以后都…"等偏好表述时\n' +
|
|
683
|
+
' • Guard 违规频繁出现同一规则时\n' +
|
|
684
|
+
' • 候选被大量驳回时',
|
|
685
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
686
|
+
},
|
|
665
687
|
// 36. ② 内容润色:Bootstrap 候选 AI 精炼(Phase 6)
|
|
666
688
|
{
|
|
667
689
|
name: 'autosnippet_bootstrap_refine',
|
|
@@ -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
|
@@ -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 {
|
|
82
|
+
} catch (err) {
|
|
83
|
+
logger.debug('DI container 同步 AI provider 失败', { error: err.message });
|
|
84
|
+
}
|
|
83
85
|
|
|
84
86
|
res.json({
|
|
85
87
|
success: true,
|
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
|
*/
|