autosnippet 2.1.0 → 2.5.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/README.md +189 -113
- package/bin/api-server.js +1 -4
- package/bin/cli.js +1 -50
- package/config/constitution.yaml +33 -107
- package/dashboard/dist/assets/{icons-B5rs8uNb.js → icons-Dtm0E6DS.js} +95 -85
- package/dashboard/dist/assets/index-B7VpZOCz.css +1 -0
- package/dashboard/dist/assets/index-D87IZTmZ.js +187 -0
- package/dashboard/dist/assets/{react-markdown-Bp8u1wRC.js → react-markdown-CWxUbOf4.js} +1 -1
- package/dashboard/dist/assets/{syntax-highlighter-C6bvFtpx.js → syntax-highlighter-CJ2drQQb.js} +1 -1
- package/dashboard/dist/assets/{vendor-Cky7Jynh.js → vendor-f83ah6cm.js} +13 -13
- package/dashboard/dist/index.html +6 -6
- package/lib/bootstrap.js +5 -31
- package/lib/cli/SetupService.js +46 -18
- package/lib/core/capability/CapabilityProbe.js +8 -6
- package/lib/core/constitution/Constitution.js +13 -4
- package/lib/core/constitution/ConstitutionValidator.js +106 -211
- package/lib/core/gateway/Gateway.js +34 -98
- package/lib/core/gateway/GatewayActionRegistry.js +12 -1
- package/lib/core/permission/PermissionManager.js +2 -2
- package/lib/external/ai/AiProvider.js +47 -7
- package/lib/external/mcp/McpServer.js +4 -7
- package/lib/external/mcp/handlers/bootstrap.js +13 -1
- package/lib/external/mcp/handlers/browse.js +0 -7
- package/lib/external/mcp/handlers/candidate.js +1 -1
- package/lib/external/mcp/handlers/guard.js +11 -0
- package/lib/external/mcp/handlers/skill.js +186 -18
- package/lib/external/mcp/tools.js +40 -1
- package/lib/http/HttpServer.js +4 -0
- package/lib/http/middleware/roleResolver.js +1 -1
- package/lib/http/routes/auth.js +2 -2
- package/lib/http/routes/monitoring.js +4 -4
- package/lib/http/routes/search.js +0 -17
- package/lib/http/routes/skills.js +73 -0
- package/lib/injection/ServiceContainer.js +21 -40
- package/lib/service/candidate/CandidateService.js +12 -1
- package/lib/service/chat/ChatAgent.js +139 -18
- package/lib/service/chat/Memory.js +104 -0
- package/lib/service/chat/tools.js +244 -10
- package/lib/service/guard/GuardCheckEngine.js +9 -1
- package/lib/service/recipe/RecipeService.js +8 -0
- package/lib/service/skills/SkillHooks.js +126 -0
- package/package.json +1 -1
- package/scripts/init-db.js +1 -2
- package/templates/constitution.yaml +29 -85
- package/dashboard/dist/assets/index-0YzLw2ga.css +0 -1
- package/dashboard/dist/assets/index-DbkbX1c-.js +0 -154
- package/lib/core/session/SessionManager.js +0 -232
- package/lib/infrastructure/logging/ReasoningLogger.js +0 -269
- package/lib/infrastructure/monitoring/RoleDriftMonitor.js +0 -259
- package/lib/infrastructure/quality/ComplianceEvaluator.js +0 -326
|
@@ -15,7 +15,9 @@ import path from 'node:path';
|
|
|
15
15
|
import { fileURLToPath } from 'node:url';
|
|
16
16
|
|
|
17
17
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
18
|
-
const
|
|
18
|
+
const PROJECT_ROOT = path.resolve(__dirname, '../../../..');
|
|
19
|
+
const SKILLS_DIR = path.resolve(PROJECT_ROOT, 'skills');
|
|
20
|
+
const PROJECT_SKILLS_DIR = path.resolve(PROJECT_ROOT, '.autosnippet', 'skills');
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
23
|
* Skill 名称 → 摘要描述映射(用于 list_skills 返回)
|
|
@@ -23,10 +25,10 @@ const SKILLS_DIR = path.resolve(__dirname, '../../../../skills');
|
|
|
23
25
|
* 从 SKILL.md 的 frontmatter description 提取。
|
|
24
26
|
* 如果解析失败,返回 Skill 名称本身。
|
|
25
27
|
*/
|
|
26
|
-
function _parseSkillSummary(skillName) {
|
|
28
|
+
function _parseSkillSummary(skillName, baseDir = SKILLS_DIR) {
|
|
27
29
|
try {
|
|
28
30
|
const content = fs.readFileSync(
|
|
29
|
-
path.join(
|
|
31
|
+
path.join(baseDir, skillName, 'SKILL.md'), 'utf8',
|
|
30
32
|
);
|
|
31
33
|
// 提取 frontmatter 的 description 字段
|
|
32
34
|
const descMatch = content.match(/^description:\s*(.+?)(?:\n|$)/m);
|
|
@@ -72,16 +74,25 @@ const SKILL_USE_CASES = {
|
|
|
72
74
|
*/
|
|
73
75
|
export function listSkills() {
|
|
74
76
|
try {
|
|
75
|
-
const
|
|
76
|
-
.filter(d => d.isDirectory())
|
|
77
|
-
.map(d => d.name)
|
|
78
|
-
.sort();
|
|
77
|
+
const skillMap = new Map();
|
|
79
78
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
79
|
+
// 内置 Skills
|
|
80
|
+
const builtinDirs = fs.readdirSync(SKILLS_DIR, { withFileTypes: true })
|
|
81
|
+
.filter(d => d.isDirectory()).map(d => d.name);
|
|
82
|
+
for (const name of builtinDirs) {
|
|
83
|
+
skillMap.set(name, { name, source: 'builtin', summary: _parseSkillSummary(name, SKILLS_DIR), useCase: SKILL_USE_CASES[name] || null });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 项目级 Skills(覆盖同名内置)
|
|
87
|
+
try {
|
|
88
|
+
const projectDirs = fs.readdirSync(PROJECT_SKILLS_DIR, { withFileTypes: true })
|
|
89
|
+
.filter(d => d.isDirectory()).map(d => d.name);
|
|
90
|
+
for (const name of projectDirs) {
|
|
91
|
+
skillMap.set(name, { name, source: 'project', summary: _parseSkillSummary(name, PROJECT_SKILLS_DIR), useCase: SKILL_USE_CASES[name] || null });
|
|
92
|
+
}
|
|
93
|
+
} catch { /* no project skills */ }
|
|
94
|
+
|
|
95
|
+
const skills = [...skillMap.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
85
96
|
|
|
86
97
|
return JSON.stringify({
|
|
87
98
|
success: true,
|
|
@@ -120,7 +131,11 @@ export function loadSkill(_ctx, args) {
|
|
|
120
131
|
});
|
|
121
132
|
}
|
|
122
133
|
|
|
123
|
-
|
|
134
|
+
// 项目级 Skills 优先
|
|
135
|
+
const projectSkillPath = path.join(PROJECT_SKILLS_DIR, skillName, 'SKILL.md');
|
|
136
|
+
const builtinSkillPath = path.join(SKILLS_DIR, skillName, 'SKILL.md');
|
|
137
|
+
const skillPath = fs.existsSync(projectSkillPath) ? projectSkillPath : builtinSkillPath;
|
|
138
|
+
const source = skillPath === projectSkillPath ? 'project' : 'builtin';
|
|
124
139
|
|
|
125
140
|
try {
|
|
126
141
|
let content = fs.readFileSync(skillPath, 'utf8');
|
|
@@ -141,6 +156,7 @@ export function loadSkill(_ctx, args) {
|
|
|
141
156
|
success: true,
|
|
142
157
|
data: {
|
|
143
158
|
skillName,
|
|
159
|
+
source,
|
|
144
160
|
content,
|
|
145
161
|
charCount: content.length,
|
|
146
162
|
useCase: SKILL_USE_CASES[skillName] || null,
|
|
@@ -148,22 +164,174 @@ export function loadSkill(_ctx, args) {
|
|
|
148
164
|
},
|
|
149
165
|
});
|
|
150
166
|
} catch {
|
|
151
|
-
//
|
|
152
|
-
const available =
|
|
153
|
-
|
|
154
|
-
|
|
167
|
+
// 列出所有可用 Skills
|
|
168
|
+
const available = new Set();
|
|
169
|
+
try { fs.readdirSync(SKILLS_DIR, { withFileTypes: true }).filter(d => d.isDirectory()).forEach(d => available.add(d.name)); } catch {}
|
|
170
|
+
try { fs.readdirSync(PROJECT_SKILLS_DIR, { withFileTypes: true }).filter(d => d.isDirectory()).forEach(d => available.add(d.name)); } catch {}
|
|
155
171
|
|
|
156
172
|
return JSON.stringify({
|
|
157
173
|
success: false,
|
|
158
174
|
error: {
|
|
159
175
|
code: 'SKILL_NOT_FOUND',
|
|
160
176
|
message: `Skill "${skillName}" not found`,
|
|
161
|
-
availableSkills: available,
|
|
177
|
+
availableSkills: [...available],
|
|
162
178
|
},
|
|
163
179
|
});
|
|
164
180
|
}
|
|
165
181
|
}
|
|
166
182
|
|
|
183
|
+
// ═══════════════════════════════════════════════════════════
|
|
184
|
+
// Handler: createSkill
|
|
185
|
+
// ═══════════════════════════════════════════════════════════
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* 创建项目级 Skill — 写入 .autosnippet/skills/<name>/SKILL.md
|
|
189
|
+
* 创建后自动 regenerate 编辑器索引(.cursor/rules/autosnippet-skills.mdc)
|
|
190
|
+
*
|
|
191
|
+
* @param {object} _ctx MCP context
|
|
192
|
+
* @param {object} args { name, description, content, overwrite? }
|
|
193
|
+
* @returns {string} JSON envelope
|
|
194
|
+
*/
|
|
195
|
+
export function createSkill(_ctx, args) {
|
|
196
|
+
const { name, description, content, overwrite = false } = args || {};
|
|
197
|
+
|
|
198
|
+
// ── 参数校验 ──
|
|
199
|
+
if (!name || !description || !content) {
|
|
200
|
+
return JSON.stringify({
|
|
201
|
+
success: false,
|
|
202
|
+
error: { code: 'MISSING_PARAM', message: 'name, description, content are all required' },
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// 名称格式校验:kebab-case(允许字母、数字、连字符)
|
|
207
|
+
if (!/^[a-z][a-z0-9-]*[a-z0-9]$/.test(name) || name.length < 3 || name.length > 64) {
|
|
208
|
+
return JSON.stringify({
|
|
209
|
+
success: false,
|
|
210
|
+
error: {
|
|
211
|
+
code: 'INVALID_NAME',
|
|
212
|
+
message: `Skill name must be kebab-case (a-z, 0-9, -), 3-64 chars. Got: "${name}"`,
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// 不允许覆盖内置 Skill
|
|
218
|
+
const builtinSkillPath = path.join(SKILLS_DIR, name, 'SKILL.md');
|
|
219
|
+
if (fs.existsSync(builtinSkillPath)) {
|
|
220
|
+
return JSON.stringify({
|
|
221
|
+
success: false,
|
|
222
|
+
error: {
|
|
223
|
+
code: 'BUILTIN_CONFLICT',
|
|
224
|
+
message: `"${name}" is a built-in Skill and cannot be overwritten. Choose a different name.`,
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// 检查同名项目级 Skill
|
|
230
|
+
const skillDir = path.join(PROJECT_SKILLS_DIR, name);
|
|
231
|
+
const skillPath = path.join(skillDir, 'SKILL.md');
|
|
232
|
+
if (fs.existsSync(skillPath) && !overwrite) {
|
|
233
|
+
return JSON.stringify({
|
|
234
|
+
success: false,
|
|
235
|
+
error: {
|
|
236
|
+
code: 'ALREADY_EXISTS',
|
|
237
|
+
message: `Project skill "${name}" already exists. Set overwrite=true to replace.`,
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ── 写入 SKILL.md ──
|
|
243
|
+
try {
|
|
244
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
245
|
+
|
|
246
|
+
const frontmatter = [
|
|
247
|
+
'---',
|
|
248
|
+
`name: ${name}`,
|
|
249
|
+
`description: ${description}`,
|
|
250
|
+
'---',
|
|
251
|
+
'',
|
|
252
|
+
].join('\n');
|
|
253
|
+
|
|
254
|
+
fs.writeFileSync(skillPath, frontmatter + content, 'utf8');
|
|
255
|
+
} catch (err) {
|
|
256
|
+
return JSON.stringify({
|
|
257
|
+
success: false,
|
|
258
|
+
error: { code: 'WRITE_ERROR', message: `Failed to write SKILL.md: ${err.message}` },
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ── regenerate 编辑器索引 ──
|
|
263
|
+
const indexResult = _regenerateEditorIndex();
|
|
264
|
+
|
|
265
|
+
return JSON.stringify({
|
|
266
|
+
success: true,
|
|
267
|
+
data: {
|
|
268
|
+
skillName: name,
|
|
269
|
+
path: skillPath,
|
|
270
|
+
overwritten: fs.existsSync(skillPath) && overwrite,
|
|
271
|
+
editorIndex: indexResult,
|
|
272
|
+
hint: `Skill "${name}" created. Use autosnippet_load_skill to verify content.`,
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Regenerate .cursor/rules/autosnippet-skills.mdc 索引文件
|
|
279
|
+
* 扫描所有项目级 Skills,生成摘要索引供 External Agent 被动发现
|
|
280
|
+
*
|
|
281
|
+
* @returns {{ success: boolean, path?: string, skillCount?: number, error?: string }}
|
|
282
|
+
*/
|
|
283
|
+
function _regenerateEditorIndex() {
|
|
284
|
+
try {
|
|
285
|
+
// 扫描项目级 Skills
|
|
286
|
+
let projectSkills = [];
|
|
287
|
+
try {
|
|
288
|
+
const dirs = fs.readdirSync(PROJECT_SKILLS_DIR, { withFileTypes: true })
|
|
289
|
+
.filter(d => d.isDirectory())
|
|
290
|
+
.map(d => d.name);
|
|
291
|
+
for (const name of dirs) {
|
|
292
|
+
const summary = _parseSkillSummary(name, PROJECT_SKILLS_DIR);
|
|
293
|
+
projectSkills.push({ name, summary });
|
|
294
|
+
}
|
|
295
|
+
} catch { /* no project skills dir */ }
|
|
296
|
+
|
|
297
|
+
if (projectSkills.length === 0) {
|
|
298
|
+
// 没有项目级 Skills 时,删除索引文件(如果存在)
|
|
299
|
+
const indexPath = path.join(PROJECT_ROOT, '.cursor', 'rules', 'autosnippet-skills.mdc');
|
|
300
|
+
try { fs.unlinkSync(indexPath); } catch { /* not exists */ }
|
|
301
|
+
return { success: true, skillCount: 0 };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// 生成 .mdc 内容
|
|
305
|
+
const skillLines = projectSkills
|
|
306
|
+
.map(s => `- **${s.name}**: ${s.summary}`)
|
|
307
|
+
.join('\n');
|
|
308
|
+
|
|
309
|
+
const mdcContent = [
|
|
310
|
+
'---',
|
|
311
|
+
'description: AutoSnippet 项目级 Skills 索引(自动生成,请勿手动编辑)',
|
|
312
|
+
'alwaysApply: true',
|
|
313
|
+
'---',
|
|
314
|
+
'',
|
|
315
|
+
'# AutoSnippet Project Skills',
|
|
316
|
+
'',
|
|
317
|
+
`本项目已注册 ${projectSkills.length} 个自定义 Skill。使用 \`autosnippet_load_skill\` 工具加载完整内容。`,
|
|
318
|
+
'',
|
|
319
|
+
skillLines,
|
|
320
|
+
'',
|
|
321
|
+
].join('\n');
|
|
322
|
+
|
|
323
|
+
// 写入 .cursor/rules/
|
|
324
|
+
const rulesDir = path.join(PROJECT_ROOT, '.cursor', 'rules');
|
|
325
|
+
fs.mkdirSync(rulesDir, { recursive: true });
|
|
326
|
+
const indexPath = path.join(rulesDir, 'autosnippet-skills.mdc');
|
|
327
|
+
fs.writeFileSync(indexPath, mdcContent, 'utf8');
|
|
328
|
+
|
|
329
|
+
return { success: true, path: indexPath, skillCount: projectSkills.length };
|
|
330
|
+
} catch (err) {
|
|
331
|
+
return { success: false, error: err.message };
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
167
335
|
/**
|
|
168
336
|
* 推荐相关 Skills(基于静态映射)
|
|
169
337
|
*/
|
|
@@ -17,6 +17,7 @@ export const TOOL_GATEWAY_MAP = {
|
|
|
17
17
|
autosnippet_enrich_candidates: { action: 'candidate:update', resource: 'candidates' },
|
|
18
18
|
autosnippet_bootstrap_knowledge: { action: 'knowledge:bootstrap', resource: 'knowledge' },
|
|
19
19
|
autosnippet_bootstrap_refine: { action: 'candidate:update', resource: 'candidates' },
|
|
20
|
+
autosnippet_create_skill: { action: 'create:skills', resource: 'skills' },
|
|
20
21
|
};
|
|
21
22
|
|
|
22
23
|
export const TOOLS = [
|
|
@@ -623,7 +624,45 @@ export const TOOLS = [
|
|
|
623
624
|
required: ['skillName'],
|
|
624
625
|
},
|
|
625
626
|
},
|
|
626
|
-
// 35.
|
|
627
|
+
// 35. 创建项目级 Skill
|
|
628
|
+
{
|
|
629
|
+
name: 'autosnippet_create_skill',
|
|
630
|
+
description:
|
|
631
|
+
'创建一个项目级 Skill 文档,写入 .autosnippet/skills/<name>/SKILL.md。\n' +
|
|
632
|
+
'Skill 是 Agent 的领域知识增强文档,帮助 Agent 正确执行特定任务。\n' +
|
|
633
|
+
'创建后自动更新编辑器索引(.cursor/rules/autosnippet-skills.mdc),使 Skill 被 AI Agent 被动发现。\n' +
|
|
634
|
+
'\n' +
|
|
635
|
+
'使用场景:\n' +
|
|
636
|
+
' • 将反复出现的操作指南/架构决策/编码规范固化为 Skill\n' +
|
|
637
|
+
' • 为特定 Target/模块创建定制化开发指南\n' +
|
|
638
|
+
' • 记录项目私有的最佳实践(不适合放入通用知识库)\n' +
|
|
639
|
+
'\n' +
|
|
640
|
+
'⚠️ 注意:Skill 名称建议使用 kebab-case,如 my-auth-guide',
|
|
641
|
+
inputSchema: {
|
|
642
|
+
type: 'object',
|
|
643
|
+
properties: {
|
|
644
|
+
name: {
|
|
645
|
+
type: 'string',
|
|
646
|
+
description: 'Skill 名称(kebab-case,如 my-auth-guide)。将作为目录名。',
|
|
647
|
+
},
|
|
648
|
+
description: {
|
|
649
|
+
type: 'string',
|
|
650
|
+
description: 'Skill 一句话描述(写入 SKILL.md frontmatter)',
|
|
651
|
+
},
|
|
652
|
+
content: {
|
|
653
|
+
type: 'string',
|
|
654
|
+
description: 'Skill 正文内容(Markdown 格式,不含 frontmatter)',
|
|
655
|
+
},
|
|
656
|
+
overwrite: {
|
|
657
|
+
type: 'boolean',
|
|
658
|
+
default: false,
|
|
659
|
+
description: '如果同名 Skill 已存在,是否覆盖(默认 false)',
|
|
660
|
+
},
|
|
661
|
+
},
|
|
662
|
+
required: ['name', 'description', 'content'],
|
|
663
|
+
},
|
|
664
|
+
},
|
|
665
|
+
// 36. ② 内容润色:Bootstrap 候选 AI 精炼(Phase 6)
|
|
627
666
|
{
|
|
628
667
|
name: 'autosnippet_bootstrap_refine',
|
|
629
668
|
description:
|
package/lib/http/HttpServer.js
CHANGED
|
@@ -26,6 +26,7 @@ import commandsRouter from './routes/commands.js';
|
|
|
26
26
|
import spmRouter from './routes/spm.js';
|
|
27
27
|
import violationsRouter from './routes/violations.js';
|
|
28
28
|
import authRouter from './routes/auth.js';
|
|
29
|
+
import skillsRouter from './routes/skills.js';
|
|
29
30
|
import apiSpec from './api-spec.js';
|
|
30
31
|
import { initRedisService } from '../infrastructure/cache/RedisService.js';
|
|
31
32
|
import { initCacheAdapter } from '../infrastructure/cache/UnifiedCacheAdapter.js';
|
|
@@ -258,6 +259,9 @@ export class HttpServer {
|
|
|
258
259
|
// 命令路由
|
|
259
260
|
this.app.use(`${apiPrefix}/commands`, commandsRouter);
|
|
260
261
|
|
|
262
|
+
// Skills 路由
|
|
263
|
+
this.app.use(`${apiPrefix}/skills`, skillsRouter);
|
|
264
|
+
|
|
261
265
|
// SPM 路由
|
|
262
266
|
this.app.use(`${apiPrefix}/spm`, spmRouter);
|
|
263
267
|
|
package/lib/http/routes/auth.js
CHANGED
|
@@ -44,7 +44,7 @@ if (!process.env.ASD_AUTH_SECRET) {
|
|
|
44
44
|
function createToken(username) {
|
|
45
45
|
const payload = {
|
|
46
46
|
sub: username,
|
|
47
|
-
role: '
|
|
47
|
+
role: 'developer',
|
|
48
48
|
iat: Date.now(),
|
|
49
49
|
exp: Date.now() + TOKEN_TTL,
|
|
50
50
|
};
|
|
@@ -107,7 +107,7 @@ router.post('/login', asyncHandler(async (req, res) => {
|
|
|
107
107
|
success: true,
|
|
108
108
|
data: {
|
|
109
109
|
token,
|
|
110
|
-
user: { username, role: '
|
|
110
|
+
user: { username, role: 'developer' },
|
|
111
111
|
},
|
|
112
112
|
});
|
|
113
113
|
}));
|
|
@@ -160,12 +160,12 @@ router.get('/cache', (req, res) => {
|
|
|
160
160
|
|
|
161
161
|
/**
|
|
162
162
|
* POST /api/v1/monitoring/cache/clear
|
|
163
|
-
* 清空缓存(仅限
|
|
163
|
+
* 清空缓存(仅限 developer)
|
|
164
164
|
*/
|
|
165
165
|
router.post('/cache/clear', async (req, res) => {
|
|
166
166
|
// 角色检查:仅 admin 可操作
|
|
167
167
|
const role = req.resolvedRole || 'visitor';
|
|
168
|
-
if (role !== '
|
|
168
|
+
if (role !== 'developer') {
|
|
169
169
|
return res.status(403).json({
|
|
170
170
|
success: false,
|
|
171
171
|
error: { message: '仅管理员可清空缓存' },
|
|
@@ -285,7 +285,7 @@ router.get('/dashboard', async (req, res) => {
|
|
|
285
285
|
|
|
286
286
|
/**
|
|
287
287
|
* POST /api/v1/monitoring/reset
|
|
288
|
-
* 重置监控统计(仅限开发环境 +
|
|
288
|
+
* 重置监控统计(仅限开发环境 + developer)
|
|
289
289
|
*/
|
|
290
290
|
router.post('/reset', (req, res) => {
|
|
291
291
|
if (process.env.NODE_ENV === 'production') {
|
|
@@ -297,7 +297,7 @@ router.post('/reset', (req, res) => {
|
|
|
297
297
|
|
|
298
298
|
// 角色检查:仅 admin 可操作
|
|
299
299
|
const role = req.resolvedRole || 'visitor';
|
|
300
|
-
if (role !== '
|
|
300
|
+
if (role !== 'developer') {
|
|
301
301
|
return res.status(403).json({
|
|
302
302
|
success: false,
|
|
303
303
|
error: { message: '仅管理员可重置监控统计' },
|
|
@@ -214,23 +214,6 @@ router.get('/graph/stats', asyncHandler(async (req, res) => {
|
|
|
214
214
|
res.json({ success: true, data: stats });
|
|
215
215
|
}));
|
|
216
216
|
|
|
217
|
-
/**
|
|
218
|
-
* GET /api/v1/search/compliance
|
|
219
|
-
* 合规评估报告
|
|
220
|
-
*/
|
|
221
|
-
router.get('/compliance', asyncHandler(async (req, res) => {
|
|
222
|
-
const { period } = req.query;
|
|
223
|
-
const container = getServiceContainer();
|
|
224
|
-
const evaluator = container.get('complianceEvaluator');
|
|
225
|
-
|
|
226
|
-
if (!evaluator) {
|
|
227
|
-
return res.json({ success: true, data: { overallScore: 0, message: 'ComplianceEvaluator not available' } });
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
const report = evaluator.evaluate({ period });
|
|
231
|
-
res.json({ success: true, data: report });
|
|
232
|
-
}));
|
|
233
|
-
|
|
234
217
|
/**
|
|
235
218
|
* POST /api/v1/search/trigger-from-code
|
|
236
219
|
* Xcode trigger 搜索模拟 (stub — 功能未完整实现)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills API 路由
|
|
3
|
+
* 管理 Agent Skills 的查询、加载和创建(项目级)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import express from 'express';
|
|
7
|
+
import { asyncHandler } from '../middleware/errorHandler.js';
|
|
8
|
+
import { listSkills, loadSkill, createSkill } from '../../external/mcp/handlers/skill.js';
|
|
9
|
+
import { ValidationError } from '../../shared/errors/index.js';
|
|
10
|
+
|
|
11
|
+
const router = express.Router();
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* GET /api/v1/skills
|
|
15
|
+
* 列出所有可用 Skills(内置 + 项目级)
|
|
16
|
+
*/
|
|
17
|
+
router.get('/', asyncHandler(async (_req, res) => {
|
|
18
|
+
const raw = listSkills();
|
|
19
|
+
const parsed = JSON.parse(raw);
|
|
20
|
+
|
|
21
|
+
if (!parsed.success) {
|
|
22
|
+
return res.status(500).json(parsed);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
res.json({ success: true, data: parsed.data });
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* GET /api/v1/skills/:name
|
|
30
|
+
* 加载指定 Skill 的完整文档
|
|
31
|
+
* Query: ?section=xxx 可只返回指定章节
|
|
32
|
+
*/
|
|
33
|
+
router.get('/:name', asyncHandler(async (req, res) => {
|
|
34
|
+
const { name } = req.params;
|
|
35
|
+
const { section } = req.query;
|
|
36
|
+
|
|
37
|
+
const raw = loadSkill(null, { skillName: name, section });
|
|
38
|
+
const parsed = JSON.parse(raw);
|
|
39
|
+
|
|
40
|
+
if (!parsed.success) {
|
|
41
|
+
const status = parsed.error?.code === 'SKILL_NOT_FOUND' ? 404 : 400;
|
|
42
|
+
return res.status(status).json(parsed);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
res.json({ success: true, data: parsed.data });
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* POST /api/v1/skills
|
|
50
|
+
* 创建项目级 Skill
|
|
51
|
+
* Body: { name, description, content, overwrite? }
|
|
52
|
+
*/
|
|
53
|
+
router.post('/', asyncHandler(async (req, res) => {
|
|
54
|
+
const { name, description, content, overwrite } = req.body;
|
|
55
|
+
|
|
56
|
+
if (!name || !description || !content) {
|
|
57
|
+
throw new ValidationError('name, description, content are all required');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const raw = createSkill(null, { name, description, content, overwrite });
|
|
61
|
+
const parsed = JSON.parse(raw);
|
|
62
|
+
|
|
63
|
+
if (!parsed.success) {
|
|
64
|
+
const status = parsed.error?.code === 'BUILTIN_CONFLICT' ? 409
|
|
65
|
+
: parsed.error?.code === 'ALREADY_EXISTS' ? 409
|
|
66
|
+
: parsed.error?.code === 'INVALID_NAME' ? 400 : 500;
|
|
67
|
+
return res.status(status).json(parsed);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
res.status(201).json({ success: true, data: parsed.data });
|
|
71
|
+
}));
|
|
72
|
+
|
|
73
|
+
export default router;
|
|
@@ -49,10 +49,10 @@ import { AutomationOrchestrator } from '../service/automation/AutomationOrchestr
|
|
|
49
49
|
import { ToolRegistry } from '../service/chat/ToolRegistry.js';
|
|
50
50
|
import { ChatAgent } from '../service/chat/ChatAgent.js';
|
|
51
51
|
import { ALL_TOOLS } from '../service/chat/tools.js';
|
|
52
|
+
import { SkillHooks } from '../service/skills/SkillHooks.js';
|
|
52
53
|
|
|
53
54
|
// ─── P3: Infrastructure ──────────────────────────────
|
|
54
|
-
|
|
55
|
-
import { PluginManager } from '../infrastructure/plugin/PluginManager.js';
|
|
55
|
+
// EventBus / PluginManager imports removed — source files retained for future use
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
58
|
* DependencyInjection 容器
|
|
@@ -88,18 +88,10 @@ export class ServiceContainer {
|
|
|
88
88
|
if (bootstrapComponents.gateway) {
|
|
89
89
|
this.singletons.gateway = bootstrapComponents.gateway;
|
|
90
90
|
}
|
|
91
|
-
if (bootstrapComponents.
|
|
92
|
-
this.singletons.
|
|
93
|
-
}
|
|
94
|
-
if (bootstrapComponents.roleDriftMonitor) {
|
|
95
|
-
this.singletons.roleDriftMonitor = bootstrapComponents.roleDriftMonitor;
|
|
96
|
-
}
|
|
97
|
-
if (bootstrapComponents.complianceEvaluator) {
|
|
98
|
-
this.singletons.complianceEvaluator = bootstrapComponents.complianceEvaluator;
|
|
99
|
-
}
|
|
100
|
-
if (bootstrapComponents.sessionManager) {
|
|
101
|
-
this.singletons.sessionManager = bootstrapComponents.sessionManager;
|
|
91
|
+
if (bootstrapComponents.constitution) {
|
|
92
|
+
this.singletons.constitution = bootstrapComponents.constitution;
|
|
102
93
|
}
|
|
94
|
+
|
|
103
95
|
if (bootstrapComponents.projectRoot) {
|
|
104
96
|
this.singletons._projectRoot = bootstrapComponents.projectRoot;
|
|
105
97
|
}
|
|
@@ -209,17 +201,7 @@ export class ServiceContainer {
|
|
|
209
201
|
return this.singletons.gateway;
|
|
210
202
|
});
|
|
211
203
|
|
|
212
|
-
// ReasoningLogger
|
|
213
|
-
this.register('reasoningLogger', () => this.singletons.reasoningLogger || null);
|
|
214
204
|
|
|
215
|
-
// RoleDriftMonitor
|
|
216
|
-
this.register('roleDriftMonitor', () => this.singletons.roleDriftMonitor || null);
|
|
217
|
-
|
|
218
|
-
// ComplianceEvaluator
|
|
219
|
-
this.register('complianceEvaluator', () => this.singletons.complianceEvaluator || null);
|
|
220
|
-
|
|
221
|
-
// SessionManager
|
|
222
|
-
this.register('sessionManager', () => this.singletons.sessionManager || null);
|
|
223
205
|
}
|
|
224
206
|
|
|
225
207
|
/**
|
|
@@ -257,11 +239,12 @@ export class ServiceContainer {
|
|
|
257
239
|
const gateway = this.get('gateway');
|
|
258
240
|
const projectRoot = this.singletons._projectRoot || process.cwd();
|
|
259
241
|
const candidateFileWriter = new CandidateFileWriter(projectRoot);
|
|
242
|
+
const skillHooks = this.get('skillHooks');
|
|
260
243
|
this.singletons.candidateService = new CandidateService(
|
|
261
244
|
candidateRepository,
|
|
262
245
|
auditLogger,
|
|
263
246
|
gateway,
|
|
264
|
-
{ fileWriter: candidateFileWriter }
|
|
247
|
+
{ fileWriter: candidateFileWriter, skillHooks }
|
|
265
248
|
);
|
|
266
249
|
}
|
|
267
250
|
return this.singletons.candidateService;
|
|
@@ -276,12 +259,13 @@ export class ServiceContainer {
|
|
|
276
259
|
const knowledgeGraphService = this.get('knowledgeGraphService');
|
|
277
260
|
const projectRoot = this.singletons._projectRoot || process.cwd();
|
|
278
261
|
const fileWriter = new RecipeFileWriter(projectRoot);
|
|
262
|
+
const skillHooks = this.get('skillHooks');
|
|
279
263
|
this.singletons.recipeService = new RecipeService(
|
|
280
264
|
recipeRepository,
|
|
281
265
|
auditLogger,
|
|
282
266
|
gateway,
|
|
283
267
|
knowledgeGraphService,
|
|
284
|
-
{ fileWriter }
|
|
268
|
+
{ fileWriter, skillHooks }
|
|
285
269
|
);
|
|
286
270
|
}
|
|
287
271
|
return this.singletons.recipeService;
|
|
@@ -332,23 +316,12 @@ export class ServiceContainer {
|
|
|
332
316
|
return this.singletons.guardCheckEngine;
|
|
333
317
|
});
|
|
334
318
|
|
|
335
|
-
// ───
|
|
319
|
+
// ─── Constitution ────────────────────────────────────
|
|
320
|
+
this.register('constitution', () => this.singletons.constitution || null);
|
|
336
321
|
|
|
337
|
-
//
|
|
338
|
-
this.register('eventBus', () => {
|
|
339
|
-
if (!this.singletons.eventBus) {
|
|
340
|
-
this.singletons.eventBus = new EventBus();
|
|
341
|
-
}
|
|
342
|
-
return this.singletons.eventBus;
|
|
343
|
-
});
|
|
322
|
+
// ─── 新迁移的服务 ────────────────────────────────────
|
|
344
323
|
|
|
345
|
-
// PluginManager
|
|
346
|
-
this.register('pluginManager', () => {
|
|
347
|
-
if (!this.singletons.pluginManager) {
|
|
348
|
-
this.singletons.pluginManager = new PluginManager();
|
|
349
|
-
}
|
|
350
|
-
return this.singletons.pluginManager;
|
|
351
|
-
});
|
|
324
|
+
// EventBus / PluginManager — 已移除注册(源文件保留,未来可恢复)
|
|
352
325
|
|
|
353
326
|
// RetrievalFunnel (Advanced Search)
|
|
354
327
|
this.register('retrievalFunnel', () => {
|
|
@@ -518,6 +491,14 @@ export class ServiceContainer {
|
|
|
518
491
|
}
|
|
519
492
|
return this.singletons.chatAgent;
|
|
520
493
|
});
|
|
494
|
+
|
|
495
|
+
// SkillHooks (Skill 生命周期钩子 — 加载 skills/*/hooks.js)
|
|
496
|
+
this.register('skillHooks', () => {
|
|
497
|
+
if (!this.singletons.skillHooks) {
|
|
498
|
+
this.singletons.skillHooks = new SkillHooks();
|
|
499
|
+
}
|
|
500
|
+
return this.singletons.skillHooks;
|
|
501
|
+
});
|
|
521
502
|
}
|
|
522
503
|
|
|
523
504
|
/**
|
|
@@ -11,11 +11,12 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
11
11
|
* 包括创建、批准、驳回和应用到 Recipe 的业务逻辑
|
|
12
12
|
*/
|
|
13
13
|
export class CandidateService {
|
|
14
|
-
constructor(candidateRepository, auditLogger, gateway, { fileWriter } = {}) {
|
|
14
|
+
constructor(candidateRepository, auditLogger, gateway, { fileWriter, skillHooks } = {}) {
|
|
15
15
|
this.candidateRepository = candidateRepository;
|
|
16
16
|
this.auditLogger = auditLogger;
|
|
17
17
|
this.gateway = gateway;
|
|
18
18
|
this.fileWriter = fileWriter || null;
|
|
19
|
+
this.skillHooks = skillHooks || null;
|
|
19
20
|
this.logger = Logger.getInstance();
|
|
20
21
|
}
|
|
21
22
|
|
|
@@ -52,6 +53,16 @@ export class CandidateService {
|
|
|
52
53
|
throw new ValidationError('Invalid candidate data');
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
// ── SkillHooks: onCandidateSubmit ──
|
|
57
|
+
if (this.skillHooks) {
|
|
58
|
+
const hookResult = await this.skillHooks.run('onCandidateSubmit', candidate, {
|
|
59
|
+
userId: context.userId,
|
|
60
|
+
});
|
|
61
|
+
if (hookResult?.block) {
|
|
62
|
+
throw new ValidationError(`SkillHook blocked: ${hookResult.reason || 'unknown'}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
55
66
|
// 保存到数据库
|
|
56
67
|
const created = await this.candidateRepository.create(candidate);
|
|
57
68
|
|