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
|
@@ -27,9 +27,12 @@ import path from 'node:path';
|
|
|
27
27
|
import { fileURLToPath } from 'node:url';
|
|
28
28
|
import Logger from '../../infrastructure/logging/Logger.js';
|
|
29
29
|
import { TaskPipeline } from './TaskPipeline.js';
|
|
30
|
+
import { Memory } from './Memory.js';
|
|
30
31
|
|
|
31
32
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
32
|
-
const
|
|
33
|
+
const PROJECT_ROOT = path.resolve(__dirname, '../../..');
|
|
34
|
+
const SKILLS_DIR = path.resolve(PROJECT_ROOT, 'skills');
|
|
35
|
+
const SOUL_PATH = path.resolve(PROJECT_ROOT, 'SOUL.md');
|
|
33
36
|
const MAX_ITERATIONS = 6;
|
|
34
37
|
|
|
35
38
|
export class ChatAgent {
|
|
@@ -39,6 +42,10 @@ export class ChatAgent {
|
|
|
39
42
|
#logger;
|
|
40
43
|
/** @type {Map<string, TaskPipeline>} */
|
|
41
44
|
#pipelines = new Map();
|
|
45
|
+
/** @type {string} 缓存的项目概况(每次 execute 刷新一次) */
|
|
46
|
+
#projectBriefingCache = '';
|
|
47
|
+
/** @type {Memory|null} 跨对话轻量记忆 */
|
|
48
|
+
#memory = null;
|
|
42
49
|
|
|
43
50
|
/**
|
|
44
51
|
* @param {object} opts
|
|
@@ -55,6 +62,12 @@ export class ChatAgent {
|
|
|
55
62
|
/** 是否有 AI Provider(只读) */
|
|
56
63
|
this.hasAI = !!aiProvider;
|
|
57
64
|
|
|
65
|
+
// 初始化跨对话记忆
|
|
66
|
+
try {
|
|
67
|
+
const projectRoot = container?.singletons?._projectRoot || process.cwd();
|
|
68
|
+
this.#memory = new Memory(projectRoot);
|
|
69
|
+
} catch { /* Memory init failed, degrade silently */ }
|
|
70
|
+
|
|
58
71
|
// 注册内置 DAG 管线
|
|
59
72
|
this.#registerBuiltinPipelines();
|
|
60
73
|
}
|
|
@@ -71,6 +84,9 @@ export class ChatAgent {
|
|
|
71
84
|
* @returns {Promise<{reply: string, toolCalls: Array, hasContext: boolean}>}
|
|
72
85
|
*/
|
|
73
86
|
async execute(prompt, { history = [] } = {}) {
|
|
87
|
+
// 每次对话刷新项目概况(不是每轮 ReAct)
|
|
88
|
+
this.#projectBriefingCache = await this.#buildProjectBriefing();
|
|
89
|
+
|
|
74
90
|
const toolSchemas = this.#toolRegistry.getToolSchemas();
|
|
75
91
|
const systemPrompt = this.#buildSystemPrompt(toolSchemas);
|
|
76
92
|
|
|
@@ -97,11 +113,9 @@ export class ChatAgent {
|
|
|
97
113
|
|
|
98
114
|
if (!action) {
|
|
99
115
|
// 没有 Action → 最终回答
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
hasContext: toolCalls.length > 0,
|
|
104
|
-
};
|
|
116
|
+
const reply = this.#cleanFinalAnswer(response);
|
|
117
|
+
this.#extractMemory(prompt, reply);
|
|
118
|
+
return { reply, toolCalls, hasContext: toolCalls.length > 0 };
|
|
105
119
|
}
|
|
106
120
|
|
|
107
121
|
// 执行工具
|
|
@@ -141,8 +155,11 @@ export class ChatAgent {
|
|
|
141
155
|
systemPrompt: '直接回答用户问题,不要再调用工具。',
|
|
142
156
|
});
|
|
143
157
|
|
|
158
|
+
const finalReply = this.#cleanFinalAnswer(finalResponse);
|
|
159
|
+
this.#extractMemory(prompt, finalReply);
|
|
160
|
+
|
|
144
161
|
return {
|
|
145
|
-
reply:
|
|
162
|
+
reply: finalReply,
|
|
146
163
|
toolCalls,
|
|
147
164
|
hasContext: toolCalls.length > 0,
|
|
148
165
|
};
|
|
@@ -574,9 +591,18 @@ ${code.substring(0, 3000)}
|
|
|
574
591
|
? `\n## 可用 Skills\n通过 load_skill 工具按需加载领域知识文档,获取操作指南和最佳实践参考。\n\n| Skill | 说明 |\n|---|---|\n${skillList.map(s => `| ${s.name} | ${s.summary || '-'} |`).join('\n')}\n\n**场景 → Skill 推荐**:\n- 冷启动、初始化 → autosnippet-coldstart\n- 深度项目分析 → autosnippet-analysis\n- 候选生成 → autosnippet-candidates + autosnippet-create\n- 代码规范审计 → autosnippet-guard\n- Snippet 概念解释 → autosnippet-concepts\n- 生命周期管理 → autosnippet-lifecycle\n- Swift/ObjC/JS·TS 语言参考 → autosnippet-reference-{swift,objc,jsts}\n- 项目结构分析 → autosnippet-structure\n- 不确定该用哪个 → autosnippet-intent\n`
|
|
575
592
|
: '';
|
|
576
593
|
|
|
577
|
-
|
|
578
|
-
|
|
594
|
+
// SOUL — AI 人格注入(如果 SOUL.md 存在)
|
|
595
|
+
let soulSection = '';
|
|
596
|
+
try {
|
|
597
|
+
if (fs.existsSync(SOUL_PATH)) {
|
|
598
|
+
soulSection = '\n' + fs.readFileSync(SOUL_PATH, 'utf-8').trim() + '\n';
|
|
599
|
+
}
|
|
600
|
+
} catch { /* SOUL.md not available */ }
|
|
579
601
|
|
|
602
|
+
return `${soulSection}
|
|
603
|
+
你是 AutoSnippet 项目的统一 AI 中心。项目内所有 AI 推理和分析都通过你执行。
|
|
604
|
+
你拥有 ${toolSchemas.length} 个工具覆盖知识库管理全链路:搜索、提交、审核、质量评估、Guard 检查、知识图谱、冷启动等。
|
|
605
|
+
${this.#projectBriefingCache}${this.#memory?.toPromptSection() || ''}
|
|
580
606
|
可用工具:
|
|
581
607
|
|
|
582
608
|
${toolDescriptions}
|
|
@@ -599,7 +625,9 @@ ${skillSection}
|
|
|
599
625
|
- 候选创建/提交 → load_skill("autosnippet-candidates")
|
|
600
626
|
- 代码规范/Guard → load_skill("autosnippet-guard")
|
|
601
627
|
- 不确定做什么 → load_skill("autosnippet-intent")
|
|
602
|
-
8. 你可以组合多个工具完成复杂任务(如:查重 → 提交 → 质量评分 →
|
|
628
|
+
8. 你可以组合多个工具完成复杂任务(如:查重 → 提交 → 质量评分 → 知识图谱关联)。
|
|
629
|
+
9. 当工具返回 _meta.confidence = "none" 时,告知用户无匹配并建议下一步,不要凭空编造。当 _meta.confidence = "low" 时,明确标注结果不确定性。
|
|
630
|
+
10. 优先使用组合工具(analyze_code, knowledge_overview, submit_with_check)减少调用轮次。`;
|
|
603
631
|
}
|
|
604
632
|
|
|
605
633
|
/**
|
|
@@ -660,24 +688,39 @@ ${skillSection}
|
|
|
660
688
|
|
|
661
689
|
/**
|
|
662
690
|
* 列出可用的 Skills 及其摘要(用于系统提示词)
|
|
691
|
+
* 加载顺序: 内置 skills/ → 项目级 .autosnippet/skills/(同名覆盖)
|
|
663
692
|
* @returns {{ name: string, summary: string }[]}
|
|
664
693
|
*/
|
|
665
694
|
#listAvailableSkills() {
|
|
695
|
+
const skillMap = new Map();
|
|
696
|
+
|
|
697
|
+
// 1. 内置 Skills
|
|
698
|
+
this.#loadSkillsFromDir(SKILLS_DIR, skillMap);
|
|
699
|
+
|
|
700
|
+
// 2. 项目级 Skills(覆盖同名内置 Skill)
|
|
701
|
+
const projectSkillsDir = path.resolve(PROJECT_ROOT, '.autosnippet', 'skills');
|
|
702
|
+
this.#loadSkillsFromDir(projectSkillsDir, skillMap);
|
|
703
|
+
|
|
704
|
+
return Array.from(skillMap.values());
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* 从目录加载 Skills 到 Map
|
|
709
|
+
*/
|
|
710
|
+
#loadSkillsFromDir(dir, skillMap) {
|
|
666
711
|
try {
|
|
667
|
-
const dirs = fs.readdirSync(
|
|
712
|
+
const dirs = fs.readdirSync(dir, { withFileTypes: true })
|
|
668
713
|
.filter(d => d.isDirectory())
|
|
669
714
|
.map(d => d.name);
|
|
670
|
-
|
|
671
|
-
const skillPath = path.join(
|
|
715
|
+
for (const name of dirs) {
|
|
716
|
+
const skillPath = path.join(dir, name, 'SKILL.md');
|
|
672
717
|
let summary = '';
|
|
673
718
|
try {
|
|
674
719
|
const raw = fs.readFileSync(skillPath, 'utf-8');
|
|
675
|
-
// 从 frontmatter description 或首行 # 后提取摘要
|
|
676
720
|
const fmMatch = raw.match(/^---[\s\S]*?description:\s*["']?(.+?)["']?\s*$/m);
|
|
677
721
|
if (fmMatch) {
|
|
678
722
|
summary = fmMatch[1];
|
|
679
723
|
} else {
|
|
680
|
-
// fallback: 取首个非空非标题行
|
|
681
724
|
const lines = raw.split('\n');
|
|
682
725
|
for (const line of lines) {
|
|
683
726
|
const trimmed = line.trim();
|
|
@@ -688,10 +731,88 @@ ${skillSection}
|
|
|
688
731
|
}
|
|
689
732
|
}
|
|
690
733
|
} catch { /* SKILL.md not found */ }
|
|
691
|
-
|
|
692
|
-
}
|
|
734
|
+
skillMap.set(name, { name, summary });
|
|
735
|
+
}
|
|
736
|
+
} catch { /* directory not found */ }
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* 构建项目概况注入到系统提示词(每次 execute 刷新一次)
|
|
741
|
+
* 单次 SQL 聚合 < 5ms,静默降级
|
|
742
|
+
*/
|
|
743
|
+
async #buildProjectBriefing() {
|
|
744
|
+
try {
|
|
745
|
+
const db = this.#container?.get('database');
|
|
746
|
+
if (!db) return '';
|
|
747
|
+
// knowledge_type → kind 映射:
|
|
748
|
+
// rule: code-standard, code-style, best-practice, boundary-constraint
|
|
749
|
+
// pattern: code-pattern, architecture, solution
|
|
750
|
+
// fact: code-relation, inheritance, call-chain, data-flow, module-dependency
|
|
751
|
+
const stats = db.prepare(`
|
|
752
|
+
SELECT
|
|
753
|
+
(SELECT COUNT(*) FROM recipes) as recipeCount,
|
|
754
|
+
(SELECT COUNT(*) FROM recipes WHERE knowledge_type IN ('code-standard','code-style','best-practice','boundary-constraint')) as ruleCount,
|
|
755
|
+
(SELECT COUNT(*) FROM recipes WHERE knowledge_type IN ('code-pattern','architecture','solution')) as patternCount,
|
|
756
|
+
(SELECT COUNT(*) FROM recipes WHERE knowledge_type IN ('code-relation','inheritance','call-chain','data-flow','module-dependency')) as factCount,
|
|
757
|
+
(SELECT COUNT(*) FROM recipes WHERE knowledge_type = 'boundary-constraint') as guardRuleCount,
|
|
758
|
+
(SELECT COUNT(*) FROM candidates WHERE status='pending') as pendingCandidates,
|
|
759
|
+
(SELECT COUNT(*) FROM candidates) as totalCandidates
|
|
760
|
+
`).get();
|
|
761
|
+
if (!stats || stats.recipeCount === 0) {
|
|
762
|
+
return '\n## 项目状态\n⚠️ 知识库为空。建议先执行冷启动(bootstrap_knowledge)。\n';
|
|
763
|
+
}
|
|
764
|
+
let section = `\n## 项目状态\n- 知识库: ${stats.recipeCount} 条 Recipe(${stats.ruleCount || 0} rule / ${stats.patternCount || 0} pattern / ${stats.factCount || 0} fact)\n- Guard 规则: ${stats.guardRuleCount || 0} 条\n- 候选: ${stats.pendingCandidates} 条待审 / ${stats.totalCandidates} 条总计\n`;
|
|
765
|
+
if (stats.pendingCandidates > 10) {
|
|
766
|
+
section += `\n⚠️ 有 ${stats.pendingCandidates} 条候选积压,建议执行批量审核。\n`;
|
|
767
|
+
}
|
|
768
|
+
return section;
|
|
693
769
|
} catch {
|
|
694
|
-
return
|
|
770
|
+
return ''; // DB 不可用时静默降级
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* 从用户消息中提取偏好/决策写入 Memory
|
|
776
|
+
* 使用正则匹配,不调 AI — 零延迟
|
|
777
|
+
*/
|
|
778
|
+
#extractMemory(prompt, _reply) {
|
|
779
|
+
if (!this.#memory) return;
|
|
780
|
+
try {
|
|
781
|
+
const prefPatterns = [
|
|
782
|
+
/我们(项目|团队)?(不用|不使用|禁止|避免|偏好|习惯|规范是)/,
|
|
783
|
+
/以后(都|请|要)/,
|
|
784
|
+
/记住/,
|
|
785
|
+
];
|
|
786
|
+
if (prefPatterns.some(p => p.test(prompt))) {
|
|
787
|
+
this.#memory.append({
|
|
788
|
+
type: 'preference',
|
|
789
|
+
content: prompt.substring(0, 200),
|
|
790
|
+
ttl: 30,
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
} catch { /* memory write failure is non-critical */ }
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* 事件驱动入口(P2 预留接口)
|
|
798
|
+
* @param {{ type: string, payload: object, source?: string }} event
|
|
799
|
+
*/
|
|
800
|
+
async executeEvent(event) {
|
|
801
|
+
const { type, payload } = event;
|
|
802
|
+
const prompt = this.#eventToPrompt(type, payload);
|
|
803
|
+
return this.execute(prompt, { history: [] });
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
#eventToPrompt(type, payload) {
|
|
807
|
+
switch (type) {
|
|
808
|
+
case 'file_saved':
|
|
809
|
+
return `文件 ${payload.filePath} 刚被保存,变更了 ${payload.changedLines} 行。请分析是否有值得提取为 Recipe 的代码模式。如果有,说明原因;没有就说"无需操作"。`;
|
|
810
|
+
case 'candidate_backlog':
|
|
811
|
+
return `当前有 ${payload.count} 条候选积压(最早 ${payload.oldest})。请按质量分类:哪些值得审核、哪些可以直接拒绝、哪些需要补充信息。`;
|
|
812
|
+
case 'scheduled_health':
|
|
813
|
+
return `请执行知识库健康检查:Recipe 覆盖率、过时标记、Guard 规则有效性。给出简要报告。`;
|
|
814
|
+
default:
|
|
815
|
+
return `事件: ${type}\n${JSON.stringify(payload)}`;
|
|
695
816
|
}
|
|
696
817
|
}
|
|
697
818
|
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory — 跨对话轻量记忆
|
|
3
|
+
*
|
|
4
|
+
* 设计:
|
|
5
|
+
* - JSONL 文件存储,每行一条记忆
|
|
6
|
+
* - 支持 TTL 自动过期
|
|
7
|
+
* - 上限 maxEntries,超出时截断旧条目
|
|
8
|
+
* - 读写均做静默降级(Memory 是增强,不是核心路径)
|
|
9
|
+
*
|
|
10
|
+
* 记忆类型:
|
|
11
|
+
* - preference: 用户偏好("我们不用 singleton"、"以后用 DI")
|
|
12
|
+
* - decision: 关键决策("Network 模块审核通过")
|
|
13
|
+
* - context: 项目上下文("主语言是 Swift,使用 SPM")
|
|
14
|
+
*
|
|
15
|
+
* 文件路径: .autosnippet/memory.jsonl
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import fs from 'node:fs';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
|
|
21
|
+
export class Memory {
|
|
22
|
+
#filePath;
|
|
23
|
+
#maxEntries;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {string} projectRoot — 用户项目根目录
|
|
27
|
+
* @param {object} [opts]
|
|
28
|
+
* @param {number} [opts.maxEntries=50] — 最大记忆条数
|
|
29
|
+
*/
|
|
30
|
+
constructor(projectRoot, { maxEntries = 50 } = {}) {
|
|
31
|
+
this.#filePath = path.join(projectRoot, '.autosnippet', 'memory.jsonl');
|
|
32
|
+
this.#maxEntries = maxEntries;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 读取最近 N 条记忆,过滤过期项
|
|
37
|
+
* @param {number} [limit=20]
|
|
38
|
+
* @returns {{ ts: string, type: string, content: string, ttl?: number }[]}
|
|
39
|
+
*/
|
|
40
|
+
load(limit = 20) {
|
|
41
|
+
try {
|
|
42
|
+
if (!fs.existsSync(this.#filePath)) return [];
|
|
43
|
+
const raw = fs.readFileSync(this.#filePath, 'utf-8').trim();
|
|
44
|
+
if (!raw) return [];
|
|
45
|
+
const lines = raw.split('\n').filter(Boolean);
|
|
46
|
+
const now = Date.now();
|
|
47
|
+
return lines
|
|
48
|
+
.map(l => { try { return JSON.parse(l); } catch { return null; } })
|
|
49
|
+
.filter(Boolean)
|
|
50
|
+
.filter(m => !m.ttl || (now - new Date(m.ts).getTime()) < m.ttl * 86400000)
|
|
51
|
+
.slice(-limit);
|
|
52
|
+
} catch {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 追加一条记忆
|
|
59
|
+
* @param {{ type: string, content: string, ttl?: number }} entry
|
|
60
|
+
*/
|
|
61
|
+
append(entry) {
|
|
62
|
+
try {
|
|
63
|
+
const dir = path.dirname(this.#filePath);
|
|
64
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
65
|
+
const line = JSON.stringify({ ts: new Date().toISOString(), ...entry });
|
|
66
|
+
fs.appendFileSync(this.#filePath, line + '\n', 'utf-8');
|
|
67
|
+
this.#compact();
|
|
68
|
+
} catch { /* write failure non-critical */ }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 生成供系统提示词的记忆摘要
|
|
73
|
+
* @returns {string}
|
|
74
|
+
*/
|
|
75
|
+
toPromptSection() {
|
|
76
|
+
const memories = this.load();
|
|
77
|
+
if (memories.length === 0) return '';
|
|
78
|
+
const lines = memories.map(m => `- [${m.type}] ${m.content}`).join('\n');
|
|
79
|
+
return `\n## 历史记忆\n以下是之前对话中积累的项目偏好和决策,请参考:\n${lines}\n`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 当前记忆条数
|
|
84
|
+
*/
|
|
85
|
+
get size() {
|
|
86
|
+
return this.load(this.#maxEntries).length;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 超过 maxEntries 时截断旧条目
|
|
91
|
+
*/
|
|
92
|
+
#compact() {
|
|
93
|
+
try {
|
|
94
|
+
const raw = fs.readFileSync(this.#filePath, 'utf-8').trim();
|
|
95
|
+
if (!raw) return;
|
|
96
|
+
const lines = raw.split('\n').filter(Boolean);
|
|
97
|
+
if (lines.length > this.#maxEntries) {
|
|
98
|
+
fs.writeFileSync(this.#filePath, lines.slice(-this.#maxEntries).join('\n') + '\n', 'utf-8');
|
|
99
|
+
}
|
|
100
|
+
} catch { /* ignore */ }
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export default Memory;
|
|
@@ -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;
|