autosnippet 2.0.2 → 2.4.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-B4FfLfBA.js → icons-B5rs8uNb.js} +85 -80
- package/dashboard/dist/assets/index-0YzLw2ga.css +1 -0
- package/dashboard/dist/assets/index-B9py3ybr.js +154 -0
- package/dashboard/dist/index.html +3 -3
- package/lib/bootstrap.js +5 -31
- package/lib/cli/SetupService.js +16 -14
- 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/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/middleware/roleResolver.js +1 -1
- package/lib/http/routes/auth.js +2 -2
- package/lib/http/routes/commands.js +58 -3
- package/lib/http/routes/monitoring.js +4 -4
- package/lib/http/routes/recipes.js +96 -4
- package/lib/http/routes/search.js +34 -35
- package/lib/injection/ServiceContainer.js +21 -40
- package/lib/service/candidate/CandidateService.js +12 -1
- package/lib/service/chat/ChatAgent.js +171 -30
- 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/knowledge/KnowledgeGraphService.js +20 -9
- 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-ChxJxX4B.js +0 -154
- package/dashboard/dist/assets/index-DwAp1mx5.css +0 -1
- 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
|
@@ -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/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
|
}));
|
|
@@ -121,12 +121,67 @@ router.get('/status', asyncHandler(async (req, res) => {
|
|
|
121
121
|
|
|
122
122
|
/**
|
|
123
123
|
* GET /api/v1/commands/files/tree
|
|
124
|
-
* Get project file tree
|
|
124
|
+
* Get project file tree – only .h / .m / .swift source files
|
|
125
125
|
*/
|
|
126
126
|
router.get('/files/tree', asyncHandler(async (req, res) => {
|
|
127
|
-
const
|
|
127
|
+
const fs = await import('node:fs');
|
|
128
|
+
const path = await import('node:path');
|
|
129
|
+
const container = getServiceContainer();
|
|
128
130
|
const projectRoot = container.singletons?._projectRoot || process.cwd();
|
|
129
|
-
|
|
131
|
+
|
|
132
|
+
const SOURCE_EXTS = new Set(['.h', '.m', '.swift']);
|
|
133
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', 'Pods', 'build', 'DerivedData', '.build', 'dist', 'vendor']);
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Recursively scan dir, returning FileNode or null if folder has no matching files.
|
|
137
|
+
*/
|
|
138
|
+
function scanDir(dirPath) {
|
|
139
|
+
const dirName = path.default.basename(dirPath);
|
|
140
|
+
if (SKIP_DIRS.has(dirName)) return null;
|
|
141
|
+
|
|
142
|
+
let entries;
|
|
143
|
+
try { entries = fs.default.readdirSync(dirPath, { withFileTypes: true }); } catch { return null; }
|
|
144
|
+
|
|
145
|
+
const children = [];
|
|
146
|
+
for (const entry of entries) {
|
|
147
|
+
if (entry.name.startsWith('.')) continue; // skip hidden
|
|
148
|
+
const fullPath = path.default.join(dirPath, entry.name);
|
|
149
|
+
|
|
150
|
+
if (entry.isDirectory()) {
|
|
151
|
+
const sub = scanDir(fullPath);
|
|
152
|
+
if (sub) children.push(sub);
|
|
153
|
+
} else if (entry.isFile()) {
|
|
154
|
+
const ext = path.default.extname(entry.name).toLowerCase();
|
|
155
|
+
if (SOURCE_EXTS.has(ext)) {
|
|
156
|
+
children.push({
|
|
157
|
+
type: 'file',
|
|
158
|
+
name: entry.name,
|
|
159
|
+
path: fullPath,
|
|
160
|
+
relativePath: path.default.relative(projectRoot, fullPath),
|
|
161
|
+
ext,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (children.length === 0) return null;
|
|
168
|
+
|
|
169
|
+
// Sort: folders first, then alphabetical
|
|
170
|
+
children.sort((a, b) => {
|
|
171
|
+
if (a.type !== b.type) return a.type === 'folder' ? -1 : 1;
|
|
172
|
+
return a.name.localeCompare(b.name);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
type: 'folder',
|
|
177
|
+
name: dirName,
|
|
178
|
+
path: dirPath,
|
|
179
|
+
children,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const tree = scanDir(projectRoot) || { type: 'folder', name: path.default.basename(projectRoot), path: projectRoot, children: [] };
|
|
184
|
+
res.json({ success: true, data: tree });
|
|
130
185
|
}));
|
|
131
186
|
|
|
132
187
|
/**
|
|
@@ -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: '仅管理员可重置监控统计' },
|
|
@@ -302,17 +302,109 @@ router.post('/batch-record-usage', asyncHandler(async (req, res) => {
|
|
|
302
302
|
|
|
303
303
|
/**
|
|
304
304
|
* POST /api/v1/recipes/discover-relations
|
|
305
|
-
* AI
|
|
305
|
+
* AI 批量发现知识图谱关系(异步后台任务)
|
|
306
306
|
* Body: { batchSize?: number }
|
|
307
307
|
*/
|
|
308
|
+
|
|
309
|
+
// 内存中的任务状态(单进程足够)
|
|
310
|
+
const _discoverTask = { running: false, result: null, error: null, startedAt: null, finishedAt: null };
|
|
311
|
+
|
|
312
|
+
// 10 分钟超时保护
|
|
313
|
+
const DISCOVER_TIMEOUT_MS = 10 * 60 * 1000;
|
|
314
|
+
|
|
308
315
|
router.post('/discover-relations', asyncHandler(async (req, res) => {
|
|
309
|
-
|
|
316
|
+
if (_discoverTask.running) {
|
|
317
|
+
// 检查是否已超时
|
|
318
|
+
const elapsed = Date.now() - new Date(_discoverTask.startedAt).getTime();
|
|
319
|
+
if (elapsed > DISCOVER_TIMEOUT_MS) {
|
|
320
|
+
_discoverTask.running = false;
|
|
321
|
+
_discoverTask.error = `任务超时(运行 ${Math.round(elapsed / 1000)}s 后强制结束)`;
|
|
322
|
+
_discoverTask.finishedAt = new Date().toISOString();
|
|
323
|
+
return res.json({ success: true, data: { status: 'timeout', error: _discoverTask.error, startedAt: _discoverTask.startedAt } });
|
|
324
|
+
}
|
|
325
|
+
return res.json({ success: true, data: { status: 'running', startedAt: _discoverTask.startedAt, elapsed: Math.round(elapsed / 1000) } });
|
|
326
|
+
}
|
|
310
327
|
|
|
328
|
+
const { batchSize = 20 } = req.body;
|
|
311
329
|
const container = getServiceContainer();
|
|
330
|
+
|
|
331
|
+
// 前置检查:ChatAgent 和 AI Provider 是否可用
|
|
312
332
|
const chatAgent = container.get('chatAgent');
|
|
313
|
-
|
|
333
|
+
if (!chatAgent) {
|
|
334
|
+
return res.status(503).json({ success: false, error: { message: 'ChatAgent 服务不可用,请检查服务配置' } });
|
|
335
|
+
}
|
|
314
336
|
|
|
315
|
-
|
|
337
|
+
const recipeService = container.get('recipeService');
|
|
338
|
+
if (!recipeService) {
|
|
339
|
+
return res.status(503).json({ success: false, error: { message: 'RecipeService 不可用' } });
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// 检查是否有足够 Recipe
|
|
343
|
+
try {
|
|
344
|
+
const { items = [], data = [] } = await recipeService.listRecipes({}, { page: 1, pageSize: 5 });
|
|
345
|
+
const count = (items.length > 0 ? items : data).length;
|
|
346
|
+
if (count < 2) {
|
|
347
|
+
return res.json({ success: true, data: { status: 'empty', message: `当前只有 ${count} 条 Recipe,至少需要 2 条才能分析关系` } });
|
|
348
|
+
}
|
|
349
|
+
} catch (err) {
|
|
350
|
+
return res.status(500).json({ success: false, error: { message: `检查 Recipe 失败: ${err.message}` } });
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
_discoverTask.running = true;
|
|
354
|
+
_discoverTask.result = null;
|
|
355
|
+
_discoverTask.error = null;
|
|
356
|
+
_discoverTask.finishedAt = null;
|
|
357
|
+
_discoverTask.startedAt = new Date().toISOString();
|
|
358
|
+
|
|
359
|
+
// 后台执行,不阻塞请求
|
|
360
|
+
const timeoutHandle = setTimeout(() => {
|
|
361
|
+
if (_discoverTask.running) {
|
|
362
|
+
_discoverTask.running = false;
|
|
363
|
+
_discoverTask.error = '任务超时(超过 10 分钟)';
|
|
364
|
+
_discoverTask.finishedAt = new Date().toISOString();
|
|
365
|
+
logger.warn('discover-relations timed out');
|
|
366
|
+
}
|
|
367
|
+
}, DISCOVER_TIMEOUT_MS);
|
|
368
|
+
|
|
369
|
+
chatAgent.runTask('discover_all_relations', { batchSize })
|
|
370
|
+
.then(result => { _discoverTask.result = result; })
|
|
371
|
+
.catch(err => {
|
|
372
|
+
_discoverTask.error = err.message || String(err);
|
|
373
|
+
logger.error('discover-relations failed', { error: err.message });
|
|
374
|
+
})
|
|
375
|
+
.finally(() => {
|
|
376
|
+
clearTimeout(timeoutHandle);
|
|
377
|
+
_discoverTask.running = false;
|
|
378
|
+
_discoverTask.finishedAt = new Date().toISOString();
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
res.json({ success: true, data: { status: 'started', startedAt: _discoverTask.startedAt } });
|
|
382
|
+
}));
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* GET /api/v1/recipes/discover-relations/status
|
|
386
|
+
* 查询关系发现任务进度
|
|
387
|
+
*/
|
|
388
|
+
router.get('/discover-relations/status', asyncHandler(async (req, res) => {
|
|
389
|
+
const elapsed = _discoverTask.startedAt
|
|
390
|
+
? Math.round((Date.now() - new Date(_discoverTask.startedAt).getTime()) / 1000)
|
|
391
|
+
: 0;
|
|
392
|
+
|
|
393
|
+
if (_discoverTask.running) {
|
|
394
|
+
return res.json({ success: true, data: { status: 'running', startedAt: _discoverTask.startedAt, elapsed } });
|
|
395
|
+
}
|
|
396
|
+
if (_discoverTask.error) {
|
|
397
|
+
return res.json({ success: true, data: { status: 'error', error: _discoverTask.error, startedAt: _discoverTask.startedAt, finishedAt: _discoverTask.finishedAt } });
|
|
398
|
+
}
|
|
399
|
+
if (_discoverTask.result) {
|
|
400
|
+
return res.json({ success: true, data: {
|
|
401
|
+
status: 'done',
|
|
402
|
+
...(_discoverTask.result),
|
|
403
|
+
startedAt: _discoverTask.startedAt,
|
|
404
|
+
finishedAt: _discoverTask.finishedAt,
|
|
405
|
+
} });
|
|
406
|
+
}
|
|
407
|
+
res.json({ success: true, data: { status: 'idle' } });
|
|
316
408
|
}));
|
|
317
409
|
|
|
318
410
|
export default router;
|
|
@@ -155,31 +155,46 @@ router.get('/graph/all', asyncHandler(async (req, res) => {
|
|
|
155
155
|
return res.json({ success: true, data: { edges: [], nodeLabels: {} } });
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
|
|
158
|
+
// 只返回 recipe 类型的关系边;module 依赖已由 /spm/dep-graph 提供
|
|
159
|
+
const nodeType = req.query.nodeType || 'recipe';
|
|
160
|
+
const edges = graphService.getAllEdges(limit, nodeType === 'all' ? undefined : nodeType);
|
|
159
161
|
|
|
160
|
-
// 收集节点 ID
|
|
161
|
-
const
|
|
162
|
+
// 收集节点 ID + 类型 → 按类型查标签
|
|
163
|
+
const nodeMap = new Map(); // id → Set<type>
|
|
162
164
|
for (const e of edges) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
+
if (!nodeMap.has(e.fromId)) nodeMap.set(e.fromId, new Set());
|
|
166
|
+
nodeMap.get(e.fromId).add(e.fromType);
|
|
167
|
+
if (!nodeMap.has(e.toId)) nodeMap.set(e.toId, new Set());
|
|
168
|
+
nodeMap.get(e.toId).add(e.toType);
|
|
165
169
|
}
|
|
166
170
|
|
|
167
171
|
const nodeLabels = {};
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
172
|
+
const nodeTypes = {}; // id → 主要类型(供前端区分渲染)
|
|
173
|
+
const nodeCategories = {}; // id → category/target 名(供前端分组布局)
|
|
174
|
+
if (nodeMap.size > 0) {
|
|
175
|
+
const recipeService = container.get('recipeService');
|
|
176
|
+
for (const [id, types] of nodeMap) {
|
|
177
|
+
// 记录节点主要类型
|
|
178
|
+
const primaryType = types.has('recipe') ? 'recipe' : [...types][0];
|
|
179
|
+
nodeTypes[id] = primaryType;
|
|
180
|
+
|
|
181
|
+
if (primaryType === 'recipe' && recipeService) {
|
|
182
|
+
// 仅 recipe 类型才查 recipeService
|
|
183
|
+
try {
|
|
184
|
+
const r = await recipeService.getRecipe(id);
|
|
185
|
+
if (r) {
|
|
186
|
+
nodeLabels[id] = r.title || r.name || id;
|
|
187
|
+
nodeCategories[id] = r.category || '';
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
} catch { /* not found – fall through */ }
|
|
178
191
|
}
|
|
179
|
-
|
|
192
|
+
// module / candidate 或查不到的 recipe → 直接用 ID
|
|
193
|
+
nodeLabels[id] = id;
|
|
194
|
+
}
|
|
180
195
|
}
|
|
181
196
|
|
|
182
|
-
res.json({ success: true, data: { edges, nodeLabels } });
|
|
197
|
+
res.json({ success: true, data: { edges, nodeLabels, nodeTypes, nodeCategories } });
|
|
183
198
|
}));
|
|
184
199
|
|
|
185
200
|
/**
|
|
@@ -194,27 +209,11 @@ router.get('/graph/stats', asyncHandler(async (req, res) => {
|
|
|
194
209
|
return res.json({ success: true, data: { totalEdges: 0, byRelation: {}, nodeTypes: [] } });
|
|
195
210
|
}
|
|
196
211
|
|
|
197
|
-
const
|
|
212
|
+
const nodeType = req.query.nodeType || 'recipe';
|
|
213
|
+
const stats = graphService.getStats(nodeType === 'all' ? undefined : nodeType);
|
|
198
214
|
res.json({ success: true, data: stats });
|
|
199
215
|
}));
|
|
200
216
|
|
|
201
|
-
/**
|
|
202
|
-
* GET /api/v1/search/compliance
|
|
203
|
-
* 合规评估报告
|
|
204
|
-
*/
|
|
205
|
-
router.get('/compliance', asyncHandler(async (req, res) => {
|
|
206
|
-
const { period } = req.query;
|
|
207
|
-
const container = getServiceContainer();
|
|
208
|
-
const evaluator = container.get('complianceEvaluator');
|
|
209
|
-
|
|
210
|
-
if (!evaluator) {
|
|
211
|
-
return res.json({ success: true, data: { overallScore: 0, message: 'ComplianceEvaluator not available' } });
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
const report = evaluator.evaluate({ period });
|
|
215
|
-
res.json({ success: true, data: report });
|
|
216
|
-
}));
|
|
217
|
-
|
|
218
217
|
/**
|
|
219
218
|
* POST /api/v1/search/trigger-from-code
|
|
220
219
|
* Xcode trigger 搜索模拟 (stub — 功能未完整实现)
|
|
@@ -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
|
|