autosnippet 2.1.0 → 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/{index-DbkbX1c-.js → index-B9py3ybr.js} +32 -32
- package/dashboard/dist/index.html +1 -1
- 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/monitoring.js +4 -4
- package/lib/http/routes/search.js +0 -17
- 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/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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* tools.js — ChatAgent 全部工具定义
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 37 个工具覆盖项目全部 AI 能力:
|
|
5
5
|
*
|
|
6
6
|
* ┌─── 查询类 (8) ─────────────────────────────────┐
|
|
7
7
|
* │ 1. search_recipes 搜索 Recipe │
|
|
@@ -53,6 +53,11 @@
|
|
|
53
53
|
* │ 32. load_skill 加载 Agent Skill 文档 │
|
|
54
54
|
* │ 33. bootstrap_knowledge 冷启动知识库初始化 │
|
|
55
55
|
* └─────────────────────────────────────────────────────┘
|
|
56
|
+
* ┌─── 组合工具 (3) ───────────────────────────────────┐
|
|
57
|
+
* │ 34. analyze_code Guard + Recipe 搜索 │
|
|
58
|
+
* │ 35. knowledge_overview 全局知识库概览 │
|
|
59
|
+
* │ 36. submit_with_check 查重 + 提交 │
|
|
60
|
+
* └─────────────────────────────────────────────────────┘
|
|
56
61
|
*/
|
|
57
62
|
|
|
58
63
|
import fs from 'node:fs';
|
|
@@ -63,8 +68,11 @@ import Logger from '../../infrastructure/logging/Logger.js';
|
|
|
63
68
|
|
|
64
69
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
65
70
|
|
|
71
|
+
const PROJECT_ROOT = path.resolve(__dirname, '../../..');
|
|
66
72
|
/** skills/ 目录绝对路径 */
|
|
67
|
-
const SKILLS_DIR = path.resolve(
|
|
73
|
+
const SKILLS_DIR = path.resolve(PROJECT_ROOT, 'skills');
|
|
74
|
+
/** 项目级 skills 目录 */
|
|
75
|
+
const PROJECT_SKILLS_DIR = path.resolve(PROJECT_ROOT, '.autosnippet', 'skills');
|
|
68
76
|
|
|
69
77
|
// ────────────────────────────────────────────────────────────
|
|
70
78
|
// 1. search_recipes
|
|
@@ -206,7 +214,24 @@ const searchKnowledge = {
|
|
|
206
214
|
const searchEngine = ctx.container.get('searchEngine');
|
|
207
215
|
const results = await searchEngine.search(query, { limit: topK });
|
|
208
216
|
if (results && results.length > 0) {
|
|
209
|
-
|
|
217
|
+
const enriched = results.slice(0, topK).map((r, i) => ({
|
|
218
|
+
...r,
|
|
219
|
+
reasoning: {
|
|
220
|
+
whyRelevant: r.score != null
|
|
221
|
+
? `匹配分 ${(r.score * 100).toFixed(0)}%` + (r.matchType ? ` (${r.matchType})` : '')
|
|
222
|
+
: '语义相关',
|
|
223
|
+
rank: i + 1,
|
|
224
|
+
},
|
|
225
|
+
}));
|
|
226
|
+
const topScore = enriched[0]?.score ?? 0;
|
|
227
|
+
return {
|
|
228
|
+
source: 'searchEngine',
|
|
229
|
+
results: enriched,
|
|
230
|
+
_meta: {
|
|
231
|
+
confidence: topScore > 0.7 ? 'high' : topScore > 0.3 ? 'medium' : 'low',
|
|
232
|
+
hint: topScore < 0.3 ? '匹配度较低,结果可能不够相关。建议尝试更具体的查询词。' : null,
|
|
233
|
+
},
|
|
234
|
+
};
|
|
210
235
|
}
|
|
211
236
|
} catch { /* SearchEngine not available */ }
|
|
212
237
|
|
|
@@ -233,7 +258,7 @@ const searchKnowledge = {
|
|
|
233
258
|
}
|
|
234
259
|
} catch { /* RetrievalFunnel not available */ }
|
|
235
260
|
|
|
236
|
-
return { source: 'none', results: [], message: 'No search engine available' };
|
|
261
|
+
return { source: 'none', results: [], message: 'No search engine available', _meta: { confidence: 'none', hint: '搜索引擎不可用。请确认向量索引已构建(rebuild_index)。' } };
|
|
237
262
|
},
|
|
238
263
|
};
|
|
239
264
|
|
|
@@ -464,6 +489,13 @@ const checkDuplicate = {
|
|
|
464
489
|
similar,
|
|
465
490
|
hasDuplicate: similar.some(s => s.similarity >= 0.7),
|
|
466
491
|
highestSimilarity: similar.length > 0 ? similar[0].similarity : 0,
|
|
492
|
+
_meta: {
|
|
493
|
+
confidence: similar.length === 0 ? 'none'
|
|
494
|
+
: similar[0].similarity >= 0.7 ? 'high' : 'low',
|
|
495
|
+
hint: similar.length === 0 ? '未发现相似 Recipe,可放心提交。'
|
|
496
|
+
: similar[0].similarity >= 0.7 ? '发现高度相似 Recipe,建议人工审核是否重复。'
|
|
497
|
+
: '有低相似度匹配,大概率不是重复。',
|
|
498
|
+
},
|
|
467
499
|
};
|
|
468
500
|
},
|
|
469
501
|
};
|
|
@@ -696,6 +728,7 @@ const guardCheckCode = {
|
|
|
696
728
|
try {
|
|
697
729
|
const engine = ctx.container.get('guardCheckEngine');
|
|
698
730
|
const violations = engine.checkCode(code, language || 'unknown', { scope });
|
|
731
|
+
// reasoning 已由 GuardCheckEngine.checkCode() 内置附加
|
|
699
732
|
return { violationCount: violations.length, violations };
|
|
700
733
|
} catch { /* not available */ }
|
|
701
734
|
|
|
@@ -1116,15 +1149,19 @@ const loadSkill = {
|
|
|
1116
1149
|
required: ['skillName'],
|
|
1117
1150
|
},
|
|
1118
1151
|
handler: async (params) => {
|
|
1119
|
-
|
|
1152
|
+
// 项目级 Skills 优先(覆盖同名内置 Skill)
|
|
1153
|
+
const projectSkillPath = path.join(PROJECT_SKILLS_DIR, params.skillName, 'SKILL.md');
|
|
1154
|
+
const builtinSkillPath = path.join(SKILLS_DIR, params.skillName, 'SKILL.md');
|
|
1155
|
+
const skillPath = fs.existsSync(projectSkillPath) ? projectSkillPath : builtinSkillPath;
|
|
1120
1156
|
try {
|
|
1121
1157
|
const content = fs.readFileSync(skillPath, 'utf8');
|
|
1122
|
-
|
|
1158
|
+
const source = skillPath === projectSkillPath ? 'project' : 'builtin';
|
|
1159
|
+
return { skillName: params.skillName, source, content };
|
|
1123
1160
|
} catch {
|
|
1124
|
-
const available =
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
return { error: `Skill "${params.skillName}" not found`, availableSkills: available };
|
|
1161
|
+
const available = new Set();
|
|
1162
|
+
try { fs.readdirSync(SKILLS_DIR, { withFileTypes: true }).filter(d => d.isDirectory()).forEach(d => available.add(d.name)); } catch {}
|
|
1163
|
+
try { fs.readdirSync(PROJECT_SKILLS_DIR, { withFileTypes: true }).filter(d => d.isDirectory()).forEach(d => available.add(d.name)); } catch {}
|
|
1164
|
+
return { error: `Skill "${params.skillName}" not found`, availableSkills: [...available] };
|
|
1128
1165
|
}
|
|
1129
1166
|
},
|
|
1130
1167
|
};
|
|
@@ -1166,6 +1203,199 @@ const bootstrapKnowledgeTool = {
|
|
|
1166
1203
|
// 导出全部工具
|
|
1167
1204
|
// ────────────────────────────────────────────────────────────
|
|
1168
1205
|
|
|
1206
|
+
// ────────────────────────────────────────────────────────────
|
|
1207
|
+
// 34. analyze_code — 组合工具 (Guard + Recipe 搜索)
|
|
1208
|
+
// ────────────────────────────────────────────────────────────
|
|
1209
|
+
const analyzeCode = {
|
|
1210
|
+
name: 'analyze_code',
|
|
1211
|
+
description: '综合分析一段代码:Guard 规范检查 + 相关 Recipe 搜索。一次调用完成完整分析,减少多轮工具调用。',
|
|
1212
|
+
parameters: {
|
|
1213
|
+
type: 'object',
|
|
1214
|
+
properties: {
|
|
1215
|
+
code: { type: 'string', description: '待分析的源码' },
|
|
1216
|
+
language: { type: 'string', description: '编程语言 (swift/objc/javascript 等)' },
|
|
1217
|
+
filePath: { type: 'string', description: '文件路径(可选,用于上下文)' },
|
|
1218
|
+
},
|
|
1219
|
+
required: ['code'],
|
|
1220
|
+
},
|
|
1221
|
+
handler: async (params, ctx) => {
|
|
1222
|
+
const { code, language, filePath } = params;
|
|
1223
|
+
const results = {};
|
|
1224
|
+
|
|
1225
|
+
// 并行执行 Guard 检查 + Recipe 搜索
|
|
1226
|
+
const [guardResult, searchResult] = await Promise.all([
|
|
1227
|
+
(async () => {
|
|
1228
|
+
try {
|
|
1229
|
+
const engine = ctx.container.get('guardCheckEngine');
|
|
1230
|
+
const violations = engine.checkCode(code, language || 'unknown', { scope: 'file' });
|
|
1231
|
+
return { violationCount: violations.length, violations };
|
|
1232
|
+
} catch {
|
|
1233
|
+
try {
|
|
1234
|
+
const guardService = ctx.container.get('guardService');
|
|
1235
|
+
const matches = await guardService.checkCode(code, { language });
|
|
1236
|
+
return { violationCount: matches.length, violations: matches };
|
|
1237
|
+
} catch { return { violationCount: 0, violations: [] }; }
|
|
1238
|
+
}
|
|
1239
|
+
})(),
|
|
1240
|
+
(async () => {
|
|
1241
|
+
try {
|
|
1242
|
+
const searchEngine = ctx.container.get('searchEngine');
|
|
1243
|
+
// 取代码首段作为搜索词
|
|
1244
|
+
const query = code.substring(0, 200).replace(/\n/g, ' ');
|
|
1245
|
+
const rawResults = await searchEngine.search(query, { limit: 5 });
|
|
1246
|
+
return { results: rawResults || [], total: rawResults?.length || 0 };
|
|
1247
|
+
} catch { return { results: [], total: 0 }; }
|
|
1248
|
+
})(),
|
|
1249
|
+
]);
|
|
1250
|
+
|
|
1251
|
+
results.guard = guardResult;
|
|
1252
|
+
results.relatedRecipes = searchResult;
|
|
1253
|
+
results.filePath = filePath || '(inline)';
|
|
1254
|
+
|
|
1255
|
+
const hasFindings = guardResult.violationCount > 0 || searchResult.total > 0;
|
|
1256
|
+
results._meta = {
|
|
1257
|
+
confidence: hasFindings ? 'high' : 'low',
|
|
1258
|
+
hint: hasFindings
|
|
1259
|
+
? `已完成 Guard 检查(${guardResult.violationCount} 个违规)+ Recipe 搜索(${searchResult.total} 条匹配)。`
|
|
1260
|
+
: '未发现 Guard 违规,也未找到相关 Recipe。可能需要先冷启动知识库。',
|
|
1261
|
+
};
|
|
1262
|
+
|
|
1263
|
+
return results;
|
|
1264
|
+
},
|
|
1265
|
+
};
|
|
1266
|
+
|
|
1267
|
+
// ────────────────────────────────────────────────────────────
|
|
1268
|
+
// 35. knowledge_overview — 组合工具 (一次获取全部类型的 Recipe 统计)
|
|
1269
|
+
// ────────────────────────────────────────────────────────────
|
|
1270
|
+
const knowledgeOverview = {
|
|
1271
|
+
name: 'knowledge_overview',
|
|
1272
|
+
description: '一次性获取知识库全貌:各类型 Recipe 分布 + 候选状态 + 知识图谱概况 + 质量概览。比分别调用 get_project_stats + search_recipes 更高效。',
|
|
1273
|
+
parameters: {
|
|
1274
|
+
type: 'object',
|
|
1275
|
+
properties: {
|
|
1276
|
+
includeTopRecipes: { type: 'boolean', description: '是否包含热门 Recipe 列表,默认 true' },
|
|
1277
|
+
limit: { type: 'number', description: '每类返回数量,默认 5' },
|
|
1278
|
+
},
|
|
1279
|
+
},
|
|
1280
|
+
handler: async (params, ctx) => {
|
|
1281
|
+
const { includeTopRecipes = true, limit = 5 } = params;
|
|
1282
|
+
const result = {};
|
|
1283
|
+
|
|
1284
|
+
// 并行获取统计 + 可选的热门列表
|
|
1285
|
+
const [statsResult, feedbackResult] = await Promise.all([
|
|
1286
|
+
(async () => {
|
|
1287
|
+
try {
|
|
1288
|
+
const recipeService = ctx.container.get('recipeService');
|
|
1289
|
+
const candidateService = ctx.container.get('candidateService');
|
|
1290
|
+
const [rs, cs] = await Promise.all([
|
|
1291
|
+
recipeService.getRecipeStats(),
|
|
1292
|
+
candidateService.getCandidateStats(),
|
|
1293
|
+
]);
|
|
1294
|
+
return { recipes: rs, candidates: cs };
|
|
1295
|
+
} catch { return null; }
|
|
1296
|
+
})(),
|
|
1297
|
+
(async () => {
|
|
1298
|
+
if (!includeTopRecipes) return null;
|
|
1299
|
+
try {
|
|
1300
|
+
const feedbackCollector = ctx.container.get('feedbackCollector');
|
|
1301
|
+
return feedbackCollector.getTopRecipes(limit);
|
|
1302
|
+
} catch { return null; }
|
|
1303
|
+
})(),
|
|
1304
|
+
]);
|
|
1305
|
+
|
|
1306
|
+
if (statsResult) {
|
|
1307
|
+
result.recipes = statsResult.recipes;
|
|
1308
|
+
result.candidates = statsResult.candidates;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
// 知识图谱统计
|
|
1312
|
+
try {
|
|
1313
|
+
const kgService = ctx.container.get('knowledgeGraphService');
|
|
1314
|
+
result.knowledgeGraph = kgService.getStats();
|
|
1315
|
+
} catch { /* KG not available */ }
|
|
1316
|
+
|
|
1317
|
+
if (feedbackResult) result.topRecipes = feedbackResult;
|
|
1318
|
+
|
|
1319
|
+
const recipeCount = result.recipes?.total || result.recipes?.count || 0;
|
|
1320
|
+
result._meta = {
|
|
1321
|
+
confidence: recipeCount > 0 ? 'high' : 'none',
|
|
1322
|
+
hint: recipeCount === 0 ? '知识库为空,建议先执行冷启动(bootstrap_knowledge)。' : null,
|
|
1323
|
+
};
|
|
1324
|
+
|
|
1325
|
+
return result;
|
|
1326
|
+
},
|
|
1327
|
+
};
|
|
1328
|
+
|
|
1329
|
+
// ────────────────────────────────────────────────────────────
|
|
1330
|
+
// 36. submit_with_check — 组合工具 (查重 + 提交)
|
|
1331
|
+
// ────────────────────────────────────────────────────────────
|
|
1332
|
+
const submitWithCheck = {
|
|
1333
|
+
name: 'submit_with_check',
|
|
1334
|
+
description: '安全提交候选:先执行查重检测,无重复则自动提交。如果发现高度相似 Recipe 则阻止并返回相似列表。一次调用完成 check_duplicate + submit_candidate。',
|
|
1335
|
+
parameters: {
|
|
1336
|
+
type: 'object',
|
|
1337
|
+
properties: {
|
|
1338
|
+
code: { type: 'string', description: '代码内容' },
|
|
1339
|
+
language: { type: 'string', description: '编程语言' },
|
|
1340
|
+
category: { type: 'string', description: '分类 (View/Service/Tool/Model 等)' },
|
|
1341
|
+
title: { type: 'string', description: '候选标题' },
|
|
1342
|
+
summary: { type: 'string', description: '摘要' },
|
|
1343
|
+
threshold: { type: 'number', description: '相似度阈值,默认 0.7' },
|
|
1344
|
+
},
|
|
1345
|
+
required: ['code', 'language', 'category'],
|
|
1346
|
+
},
|
|
1347
|
+
handler: async (params, ctx) => {
|
|
1348
|
+
const { code, language, category, title, summary, threshold = 0.7 } = params;
|
|
1349
|
+
const projectRoot = ctx.projectRoot;
|
|
1350
|
+
|
|
1351
|
+
// Step 1: 查重
|
|
1352
|
+
const cand = { title: title || '', summary: summary || '', code };
|
|
1353
|
+
const similar = findSimilarRecipes(projectRoot, cand, { threshold: 0.5, topK: 5 });
|
|
1354
|
+
const hasDuplicate = similar.some(s => s.similarity >= threshold);
|
|
1355
|
+
|
|
1356
|
+
if (hasDuplicate) {
|
|
1357
|
+
return {
|
|
1358
|
+
submitted: false,
|
|
1359
|
+
reason: 'duplicate_blocked',
|
|
1360
|
+
similar,
|
|
1361
|
+
highestSimilarity: similar[0]?.similarity || 0,
|
|
1362
|
+
_meta: {
|
|
1363
|
+
confidence: 'high',
|
|
1364
|
+
hint: `发现高度相似 Recipe(相似度 ${(similar[0]?.similarity * 100).toFixed(0)}%),已阻止提交。请人工审核。`,
|
|
1365
|
+
},
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
// Step 2: 提交
|
|
1370
|
+
try {
|
|
1371
|
+
const candidateService = ctx.container.get('candidateService');
|
|
1372
|
+
const item = {
|
|
1373
|
+
code,
|
|
1374
|
+
language,
|
|
1375
|
+
category,
|
|
1376
|
+
title: title || '',
|
|
1377
|
+
summary_cn: summary || '',
|
|
1378
|
+
reasoning: { whyStandard: 'Submitted via submit_with_check', sources: ['agent'], confidence: 0.7 },
|
|
1379
|
+
};
|
|
1380
|
+
const created = await candidateService.createFromToolParams(item, 'agent', {}, { userId: 'agent' });
|
|
1381
|
+
|
|
1382
|
+
return {
|
|
1383
|
+
submitted: true,
|
|
1384
|
+
candidate: created,
|
|
1385
|
+
similar: similar.length > 0 ? similar : [],
|
|
1386
|
+
_meta: {
|
|
1387
|
+
confidence: 'high',
|
|
1388
|
+
hint: similar.length > 0
|
|
1389
|
+
? `已提交,但有 ${similar.length} 个低相似度匹配,大概率不是重复。`
|
|
1390
|
+
: '已提交,无重复风险。',
|
|
1391
|
+
},
|
|
1392
|
+
};
|
|
1393
|
+
} catch (err) {
|
|
1394
|
+
return { submitted: false, reason: 'submit_error', error: err.message };
|
|
1395
|
+
}
|
|
1396
|
+
},
|
|
1397
|
+
};
|
|
1398
|
+
|
|
1169
1399
|
export const ALL_TOOLS = [
|
|
1170
1400
|
// 查询类 (8)
|
|
1171
1401
|
searchRecipes,
|
|
@@ -1209,6 +1439,10 @@ export const ALL_TOOLS = [
|
|
|
1209
1439
|
// Skills & Bootstrap (2)
|
|
1210
1440
|
loadSkill,
|
|
1211
1441
|
bootstrapKnowledgeTool,
|
|
1442
|
+
// 组合工具 (3) — 减少 ReAct 轮次
|
|
1443
|
+
analyzeCode,
|
|
1444
|
+
knowledgeOverview,
|
|
1445
|
+
submitWithCheck,
|
|
1212
1446
|
];
|
|
1213
1447
|
|
|
1214
1448
|
export default ALL_TOOLS;
|
|
@@ -351,7 +351,15 @@ export class GuardCheckEngine {
|
|
|
351
351
|
// 跟踪 Guard 命中次数(回写 Recipe 统计)
|
|
352
352
|
this.trackGuardHits(violations);
|
|
353
353
|
|
|
354
|
-
|
|
354
|
+
// ── Reasoning Enrichment: 推理信息跟随数据流动 ──
|
|
355
|
+
return violations.map(v => ({
|
|
356
|
+
...v,
|
|
357
|
+
reasoning: {
|
|
358
|
+
whatViolated: v.ruleId,
|
|
359
|
+
whyItMatters: v.message,
|
|
360
|
+
suggestedFix: v.suggestedFix || null,
|
|
361
|
+
},
|
|
362
|
+
}));
|
|
355
363
|
}
|
|
356
364
|
|
|
357
365
|
/**
|
|
@@ -27,6 +27,7 @@ export class RecipeService {
|
|
|
27
27
|
this.gateway = gateway;
|
|
28
28
|
this._knowledgeGraphService = knowledgeGraphService || null;
|
|
29
29
|
this._fileWriter = options.fileWriter || null;
|
|
30
|
+
this._skillHooks = options.skillHooks || null;
|
|
30
31
|
this.logger = Logger.getInstance();
|
|
31
32
|
}
|
|
32
33
|
|
|
@@ -110,6 +111,13 @@ export class RecipeService {
|
|
|
110
111
|
title: created.title,
|
|
111
112
|
});
|
|
112
113
|
|
|
114
|
+
// ── SkillHooks: onRecipeCreated (fire-and-forget) ──
|
|
115
|
+
if (this._skillHooks) {
|
|
116
|
+
this._skillHooks.run('onRecipeCreated', created, {
|
|
117
|
+
userId: context.userId,
|
|
118
|
+
}).catch(err => this.logger.warn('SkillHook onRecipeCreated error', { error: err.message }));
|
|
119
|
+
}
|
|
120
|
+
|
|
113
121
|
return created;
|
|
114
122
|
} catch (error) {
|
|
115
123
|
this.logger.error('Error creating recipe', {
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SkillHooks — Skill 生命周期钩子管理器
|
|
3
|
+
*
|
|
4
|
+
* 每个 Skill 目录可以包含一个 hooks.js 文件,导出生命周期回调。
|
|
5
|
+
* SkillHooks 在启动时扫描并注册所有钩子,在特定事件发生时按序调用。
|
|
6
|
+
*
|
|
7
|
+
* 支持的钩子:
|
|
8
|
+
* - onCandidateSubmit(candidate, ctx) → { block?: boolean, reason?: string }
|
|
9
|
+
* - onRecipeCreated(recipe, ctx) → void
|
|
10
|
+
* - onGuardCheck(violation, ctx) → violation (可修改)
|
|
11
|
+
* - onBootstrapComplete(stats, ctx) → void
|
|
12
|
+
*
|
|
13
|
+
* 加载顺序: 内置 skills/ → 项目级 .autosnippet/skills/(同名覆盖)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
import Logger from '../../infrastructure/logging/Logger.js';
|
|
20
|
+
|
|
21
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
const PROJECT_ROOT = path.resolve(__dirname, '../../..');
|
|
23
|
+
const SKILLS_DIR = path.resolve(PROJECT_ROOT, 'skills');
|
|
24
|
+
const PROJECT_SKILLS_DIR = path.resolve(PROJECT_ROOT, '.autosnippet', 'skills');
|
|
25
|
+
|
|
26
|
+
const HOOK_NAMES = [
|
|
27
|
+
'onCandidateSubmit',
|
|
28
|
+
'onRecipeCreated',
|
|
29
|
+
'onGuardCheck',
|
|
30
|
+
'onBootstrapComplete',
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
export class SkillHooks {
|
|
34
|
+
constructor() {
|
|
35
|
+
this.logger = Logger.getInstance();
|
|
36
|
+
/** @type {Map<string, Function[]>} hookName → [handler, ...] */
|
|
37
|
+
this.hooks = new Map(HOOK_NAMES.map(n => [n, []]));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 扫描 skills 目录,加载所有 hooks.js
|
|
42
|
+
* 项目级 hooks 覆盖同名内置 hooks
|
|
43
|
+
*/
|
|
44
|
+
async load() {
|
|
45
|
+
const loaded = new Map(); // skillName → hooks module
|
|
46
|
+
|
|
47
|
+
// 1. 内置 skills
|
|
48
|
+
await this.#loadFromDir(SKILLS_DIR, loaded);
|
|
49
|
+
|
|
50
|
+
// 2. 项目级 skills(覆盖同名)
|
|
51
|
+
await this.#loadFromDir(PROJECT_SKILLS_DIR, loaded);
|
|
52
|
+
|
|
53
|
+
// 3. 注册所有钩子
|
|
54
|
+
for (const [skillName, mod] of loaded) {
|
|
55
|
+
for (const hookName of HOOK_NAMES) {
|
|
56
|
+
if (typeof mod[hookName] === 'function') {
|
|
57
|
+
this.hooks.get(hookName).push(mod[hookName]);
|
|
58
|
+
this.logger.debug(`SkillHook registered: ${skillName}.${hookName}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const totalHooks = [...this.hooks.values()].reduce((s, a) => s + a.length, 0);
|
|
64
|
+
if (totalHooks > 0) {
|
|
65
|
+
this.logger.info(`SkillHooks: loaded ${totalHooks} hooks from ${loaded.size} skills`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 触发钩子 — 按注册顺序调用
|
|
71
|
+
*
|
|
72
|
+
* @param {string} hookName
|
|
73
|
+
* @param {...any} args
|
|
74
|
+
* @returns {Promise<any>} 最后一个返回值(用于 blocking hooks 如 onCandidateSubmit)
|
|
75
|
+
*/
|
|
76
|
+
async run(hookName, ...args) {
|
|
77
|
+
const handlers = this.hooks.get(hookName);
|
|
78
|
+
if (!handlers || handlers.length === 0) return undefined;
|
|
79
|
+
|
|
80
|
+
let result;
|
|
81
|
+
for (const handler of handlers) {
|
|
82
|
+
try {
|
|
83
|
+
result = await handler(...args);
|
|
84
|
+
// 如果是 blocking hook 且返回 block=true,立即中断
|
|
85
|
+
if (result?.block) return result;
|
|
86
|
+
} catch (err) {
|
|
87
|
+
this.logger.warn(`SkillHook error in ${hookName}`, { error: err.message });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 检查是否有任何钩子注册
|
|
95
|
+
*/
|
|
96
|
+
has(hookName) {
|
|
97
|
+
const handlers = this.hooks.get(hookName);
|
|
98
|
+
return handlers && handlers.length > 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ─── Internal ──────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
async #loadFromDir(dir, loaded) {
|
|
104
|
+
let dirs;
|
|
105
|
+
try {
|
|
106
|
+
dirs = fs.readdirSync(dir, { withFileTypes: true })
|
|
107
|
+
.filter(d => d.isDirectory())
|
|
108
|
+
.map(d => d.name);
|
|
109
|
+
} catch {
|
|
110
|
+
return; // 目录不存在
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
for (const name of dirs) {
|
|
114
|
+
const hooksPath = path.join(dir, name, 'hooks.js');
|
|
115
|
+
if (!fs.existsSync(hooksPath)) continue;
|
|
116
|
+
try {
|
|
117
|
+
const mod = await import(hooksPath);
|
|
118
|
+
loaded.set(name, mod.default || mod);
|
|
119
|
+
} catch (err) {
|
|
120
|
+
this.logger.warn(`SkillHooks: failed to load ${name}/hooks.js`, { error: err.message });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export default SkillHooks;
|
package/package.json
CHANGED
package/scripts/init-db.js
CHANGED
|
@@ -25,14 +25,13 @@ async function main() {
|
|
|
25
25
|
console.log(' - Gateway:', components.gateway ? '✓' : '✗');
|
|
26
26
|
console.log(' - Permission Manager:', components.permissionManager ? '✓' : '✗');
|
|
27
27
|
console.log(' - Audit Logger:', components.auditLogger ? '✓' : '✗');
|
|
28
|
-
console.log(' - Session Manager:', components.sessionManager ? '✓' : '✗');
|
|
29
28
|
|
|
30
29
|
// 显示宪法信息
|
|
31
30
|
const constitutionInfo = components.constitution.toJSON();
|
|
32
31
|
console.log('\n📜 Constitution:');
|
|
33
32
|
console.log(' - Version:', constitutionInfo.version);
|
|
34
33
|
console.log(' - Effective Date:', constitutionInfo.effectiveDate);
|
|
35
|
-
console.log(' -
|
|
34
|
+
console.log(' - Rules:', (constitutionInfo.rules || []).length);
|
|
36
35
|
console.log(' - Roles:', constitutionInfo.roles.length);
|
|
37
36
|
|
|
38
37
|
await bootstrap.shutdown();
|
|
@@ -5,65 +5,45 @@
|
|
|
5
5
|
# 三层权限架构:
|
|
6
6
|
# ① 能力层 (capabilities) — git push --dry-run 探测物理写权限
|
|
7
7
|
# ② 角色层 (roles) — 角色权限矩阵 (action:resource)
|
|
8
|
-
# ③ 治理层 (
|
|
8
|
+
# ③ 治理层 (rules) — 扁平规则引擎
|
|
9
9
|
#
|
|
10
10
|
# 双路径模式:
|
|
11
11
|
# AUTH_ENABLED=false → 子仓库探针自动决定角色(能力层驱动)
|
|
12
12
|
# AUTH_ENABLED=true → 登录后根据用户配置角色(角色层驱动)
|
|
13
13
|
# ═══════════════════════════════════════════════════════════
|
|
14
14
|
|
|
15
|
-
version: "
|
|
16
|
-
effective_date: "2026-02-
|
|
15
|
+
version: "3.0"
|
|
16
|
+
effective_date: "2026-02-13"
|
|
17
17
|
|
|
18
18
|
# ─── 能力探测 ─────────────────────────────────────────────
|
|
19
19
|
capabilities:
|
|
20
20
|
git_write:
|
|
21
21
|
description: "子仓库 git push 权限"
|
|
22
22
|
probe: "git push --dry-run"
|
|
23
|
-
# 无子仓库 → 个人项目, 视为 admin (全权限)
|
|
24
23
|
no_subrepo: "allow"
|
|
25
|
-
# 有子仓库但无 remote → 本地开发, 视为 admin
|
|
26
24
|
no_remote: "allow"
|
|
27
25
|
cache_ttl: 86400
|
|
28
26
|
|
|
29
|
-
# ───
|
|
30
|
-
|
|
31
|
-
- id:
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
description: "
|
|
42
|
-
|
|
43
|
-
- "AI 生成的 Candidate 必须经人工审核"
|
|
44
|
-
- "Guard 规则修改需要人工批准"
|
|
45
|
-
- "批量操作需要明确授权"
|
|
46
|
-
|
|
47
|
-
- id: 3
|
|
48
|
-
name: "AI Transparency"
|
|
49
|
-
description: "AI 决策过程必须可追溯"
|
|
50
|
-
rules:
|
|
51
|
-
- "Candidate 必须包含 Reasoning 信息"
|
|
52
|
-
- "Guard 规则必须关联来源 Recipe"
|
|
53
|
-
- "所有 AI 操作记录到审计日志"
|
|
54
|
-
|
|
55
|
-
- id: 4
|
|
56
|
-
name: "Helpfulness"
|
|
57
|
-
description: "在保证安全前提下尽可能帮助用户"
|
|
58
|
-
rules:
|
|
59
|
-
- "提供充分的错误信息和修复建议"
|
|
60
|
-
- "优先推荐高质量 Recipe"
|
|
61
|
-
- "自动优化常见操作"
|
|
27
|
+
# ─── 治理规则(扁平规则替代优先级层级) ─────────────────
|
|
28
|
+
rules:
|
|
29
|
+
- id: "destructive_confirm"
|
|
30
|
+
description: "删除操作需要确认"
|
|
31
|
+
check: "destructive_needs_confirmation"
|
|
32
|
+
- id: "content_required"
|
|
33
|
+
description: "创建 candidate/recipe 需要内容"
|
|
34
|
+
check: "creation_needs_content"
|
|
35
|
+
- id: "ai_no_direct_recipe"
|
|
36
|
+
description: "AI 不能直接创建/批准 recipe"
|
|
37
|
+
check: "ai_cannot_approve_recipe"
|
|
38
|
+
- id: "batch_authorized"
|
|
39
|
+
description: "批量操作需要授权"
|
|
40
|
+
check: "batch_needs_authorization"
|
|
62
41
|
|
|
42
|
+
# ─── 角色定义 ─────────────────────────────────────────────
|
|
63
43
|
roles:
|
|
64
|
-
- id: "
|
|
65
|
-
name: "
|
|
66
|
-
description: "
|
|
44
|
+
- id: "external_agent"
|
|
45
|
+
name: "External Agent"
|
|
46
|
+
description: "IDE 中的外部 AI Agent(Cursor / Copilot / Claude Code)"
|
|
67
47
|
permissions:
|
|
68
48
|
- "read:recipes"
|
|
69
49
|
- "read:guard_rules"
|
|
@@ -71,14 +51,15 @@ roles:
|
|
|
71
51
|
- "submit:candidates"
|
|
72
52
|
- "read:audit_logs:self"
|
|
73
53
|
- "knowledge:bootstrap"
|
|
54
|
+
- "create:skills"
|
|
74
55
|
constraints:
|
|
75
56
|
- "不能直接修改 Recipe"
|
|
76
57
|
- "不能修改 Guard 规则"
|
|
77
58
|
- "不能删除任何数据"
|
|
78
59
|
|
|
79
|
-
- id: "
|
|
80
|
-
name: "
|
|
81
|
-
description: "AutoSnippet
|
|
60
|
+
- id: "chat_agent"
|
|
61
|
+
name: "ChatAgent"
|
|
62
|
+
description: "AutoSnippet 内置 AI Agent(Dashboard 对话 / 程序化调用)"
|
|
82
63
|
permissions:
|
|
83
64
|
- "read:recipes"
|
|
84
65
|
- "read:candidates"
|
|
@@ -88,47 +69,10 @@ roles:
|
|
|
88
69
|
- "生成的 Candidate 必须包含完整 Reasoning"
|
|
89
70
|
- "不能绕过 Guard 检查"
|
|
90
71
|
|
|
91
|
-
- id: "
|
|
92
|
-
name: "
|
|
93
|
-
description: "
|
|
94
|
-
permissions:
|
|
95
|
-
- "read:candidates"
|
|
96
|
-
- "read:guard_rules"
|
|
97
|
-
- "write:audit_logs"
|
|
98
|
-
constraints:
|
|
99
|
-
- "只能读取,不能修改数据"
|
|
100
|
-
|
|
101
|
-
- id: "developer_admin"
|
|
102
|
-
name: "开发者(管理员)"
|
|
103
|
-
description: "项目管理员"
|
|
72
|
+
- id: "developer"
|
|
73
|
+
name: "开发者"
|
|
74
|
+
description: "项目 Owner,完整权限"
|
|
104
75
|
permissions:
|
|
105
|
-
- "*"
|
|
76
|
+
- "*"
|
|
106
77
|
requires_capability:
|
|
107
78
|
- "git_write"
|
|
108
|
-
|
|
109
|
-
- id: "developer_contributor"
|
|
110
|
-
name: "开发者(贡献者)"
|
|
111
|
-
description: "项目贡献者"
|
|
112
|
-
permissions:
|
|
113
|
-
- "read:*"
|
|
114
|
-
- "approve:candidates"
|
|
115
|
-
- "reject:candidates"
|
|
116
|
-
- "create:recipes"
|
|
117
|
-
constraints:
|
|
118
|
-
- "不能删除 Recipe"
|
|
119
|
-
- "不能禁用 Guard 规则"
|
|
120
|
-
requires_capability:
|
|
121
|
-
- "git_write"
|
|
122
|
-
|
|
123
|
-
- id: "visitor"
|
|
124
|
-
name: "访问者"
|
|
125
|
-
description: "只读访问(无子仓库写权限或未登录)"
|
|
126
|
-
permissions:
|
|
127
|
-
- "read:recipes"
|
|
128
|
-
- "read:snippets"
|
|
129
|
-
- "read:candidates"
|
|
130
|
-
- "read:guard_rules"
|
|
131
|
-
- "search:query"
|
|
132
|
-
constraints:
|
|
133
|
-
- "只能查看,不能修改任何数据"
|
|
134
|
-
- "不能创建 Candidate 或 Recipe"
|