autosnippet 2.9.0 → 2.10.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 +4 -4
- package/bin/cli.js +5 -33
- package/config/constitution.yaml +9 -2
- package/dashboard/dist/assets/{icons-CH-H9x0E.js → icons-BkT3XrKf.js} +105 -100
- package/dashboard/dist/assets/index-BsB7DzW4.css +1 -0
- package/dashboard/dist/assets/index-DdmQMrJJ.js +155 -0
- package/dashboard/dist/index.html +3 -3
- package/lib/cli/AiScanService.js +13 -11
- package/lib/cli/KnowledgeSyncService.js +343 -0
- package/lib/cli/SetupService.js +8 -26
- package/lib/core/gateway/GatewayActionRegistry.js +48 -58
- package/lib/domain/index.js +16 -11
- package/lib/domain/knowledge/KnowledgeEntry.js +351 -0
- package/lib/domain/knowledge/KnowledgeRepository.js +123 -0
- package/lib/domain/knowledge/Lifecycle.js +109 -0
- package/lib/domain/knowledge/index.js +27 -0
- package/lib/domain/knowledge/values/Constraints.js +125 -0
- package/lib/domain/knowledge/values/Content.js +86 -0
- package/lib/domain/knowledge/values/Quality.js +93 -0
- package/lib/domain/knowledge/values/Reasoning.js +69 -0
- package/lib/domain/knowledge/values/Relations.js +168 -0
- package/lib/domain/knowledge/values/Stats.js +87 -0
- package/lib/domain/knowledge/values/index.js +9 -0
- package/lib/external/ai/AiProvider.js +48 -0
- package/lib/external/mcp/McpServer.js +7 -5
- package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +2 -2
- package/lib/external/mcp/handlers/bootstrap.js +116 -11
- package/lib/external/mcp/handlers/browse.js +77 -73
- package/lib/external/mcp/handlers/candidate.js +29 -276
- package/lib/external/mcp/handlers/guard.js +2 -0
- package/lib/external/mcp/handlers/knowledge.js +205 -0
- package/lib/external/mcp/handlers/structure.js +25 -23
- package/lib/external/mcp/handlers/system.js +10 -12
- package/lib/external/mcp/tools.js +125 -138
- package/lib/http/HttpServer.js +4 -8
- package/lib/http/routes/extract.js +48 -4
- package/lib/http/routes/knowledge.js +246 -0
- package/lib/http/routes/search.js +12 -17
- package/lib/infrastructure/database/migrations/016_unified_knowledge_entries.js +395 -0
- package/lib/infrastructure/external/XcodeAutomation.js +187 -103
- package/lib/injection/ServiceContainer.js +49 -60
- package/lib/repository/knowledge/KnowledgeRepository.impl.js +373 -0
- package/lib/service/automation/DirectiveDetector.js +2 -3
- package/lib/service/automation/FileWatcher.js +67 -28
- package/lib/service/automation/XcodeIntegration.js +931 -156
- package/lib/service/automation/handlers/AlinkHandler.js +6 -4
- package/lib/service/automation/handlers/CreateHandler.js +53 -18
- package/lib/service/automation/handlers/GuardHandler.js +183 -20
- package/lib/service/automation/handlers/SearchHandler.js +35 -17
- package/lib/service/chat/CandidateGuardrail.js +1 -1
- package/lib/service/chat/ChatAgent.js +46 -45
- package/lib/service/chat/ContextWindow.js +5 -5
- package/lib/service/chat/ProducerAgent.js +7 -7
- package/lib/service/chat/tools.js +130 -123
- package/lib/service/guard/GuardCheckEngine.js +114 -10
- package/lib/service/guard/GuardService.js +59 -48
- package/lib/service/knowledge/ConfidenceRouter.js +159 -0
- package/lib/service/knowledge/KnowledgeFileWriter.js +602 -0
- package/lib/service/knowledge/KnowledgeService.js +725 -0
- package/lib/service/search/SearchEngine.js +92 -19
- package/lib/service/skills/SignalCollector.js +12 -7
- package/lib/service/skills/SkillAdvisor.js +13 -11
- package/lib/service/snippet/SnippetFactory.js +5 -5
- package/package.json +1 -1
- package/scripts/install-cursor-skill.js +0 -6
- package/scripts/migrate-md-to-knowledge.mjs +364 -0
- package/skills/autosnippet-analysis/SKILL.md +15 -7
- package/skills/autosnippet-candidates/SKILL.md +6 -6
- package/skills/autosnippet-coldstart/SKILL.md +7 -3
- package/skills/autosnippet-concepts/SKILL.md +7 -6
- package/skills/autosnippet-create/SKILL.md +13 -13
- package/skills/autosnippet-intent/SKILL.md +3 -2
- package/skills/autosnippet-lifecycle/SKILL.md +5 -5
- package/skills/autosnippet-recipes/SKILL.md +16 -4
- package/templates/constitution.yaml +1 -1
- package/templates/copilot-instructions.md +6 -6
- package/templates/recipes-setup/README.md +3 -3
- package/dashboard/dist/assets/index-CqJRvYRL.js +0 -197
- package/dashboard/dist/assets/index-DICm9PNa.css +0 -1
- package/lib/cli/CandidateSyncService.js +0 -261
- package/lib/cli/SyncService.js +0 -356
- package/lib/domain/candidate/Candidate.js +0 -196
- package/lib/domain/candidate/CandidateRepository.js +0 -107
- package/lib/domain/candidate/Reasoning.js +0 -52
- package/lib/domain/recipe/Recipe.js +0 -421
- package/lib/domain/recipe/RecipeRepository.js +0 -54
- package/lib/domain/types/CandidateStatus.js +0 -52
- package/lib/http/routes/candidates.js +0 -559
- package/lib/http/routes/recipes.js +0 -397
- package/lib/repository/candidate/CandidateRepository.impl.js +0 -230
- package/lib/repository/recipe/RecipeRepository.impl.js +0 -498
- package/lib/service/candidate/CandidateAggregator.js +0 -52
- package/lib/service/candidate/CandidateFileWriter.js +0 -383
- package/lib/service/candidate/CandidateService.js +0 -1001
- package/lib/service/recipe/RecipeFileWriter.js +0 -514
- package/lib/service/recipe/RecipeService.js +0 -786
- package/lib/service/recipe/RecipeStatsTracker.js +0 -148
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<title>AutoSnippet Dashboard</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-DdmQMrJJ.js"></script>
|
|
9
9
|
<link rel="modulepreload" crossorigin href="/assets/yaml-qRaU8Ldn.js">
|
|
10
10
|
<link rel="modulepreload" crossorigin href="/assets/vendor-BotF760a.js">
|
|
11
11
|
<link rel="modulepreload" crossorigin href="/assets/axios-C0Zqfgkc.js">
|
|
12
|
-
<link rel="modulepreload" crossorigin href="/assets/icons-
|
|
12
|
+
<link rel="modulepreload" crossorigin href="/assets/icons-BkT3XrKf.js">
|
|
13
13
|
<link rel="modulepreload" crossorigin href="/assets/syntax-highlighter-CVLHn9O5.js">
|
|
14
14
|
<link rel="modulepreload" crossorigin href="/assets/react-markdown-BA6FB2NP.js">
|
|
15
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
15
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BsB7DzW4.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body>
|
|
18
18
|
<div id="root"></div>
|
package/lib/cli/AiScanService.js
CHANGED
|
@@ -56,7 +56,7 @@ export class AiScanService {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
report.files = files.length;
|
|
59
|
-
const
|
|
59
|
+
const knowledgeService = this.container.get('knowledgeService');
|
|
60
60
|
|
|
61
61
|
// 3. 按文件调用 AI 提取
|
|
62
62
|
for (const file of files) {
|
|
@@ -95,29 +95,31 @@ export class AiScanService {
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
try {
|
|
98
|
-
await
|
|
99
|
-
|
|
98
|
+
await knowledgeService.create({
|
|
99
|
+
content: {
|
|
100
|
+
pattern: recipe.code,
|
|
101
|
+
},
|
|
100
102
|
language: recipe.language || this._inferLanguage(file.name),
|
|
101
103
|
category: recipe.category || 'ai-scan',
|
|
102
104
|
source: 'ai-scan',
|
|
105
|
+
knowledge_type: recipe.knowledgeType || 'code-pattern',
|
|
106
|
+
title: recipe.title || `[AI Scan] ${file.name}`,
|
|
107
|
+
summary_cn: recipe.summary_cn || recipe.summary_en || '',
|
|
108
|
+
tags: [...(recipe.tags || []), 'ai-scan', file.targetName],
|
|
109
|
+
scope: 'project-specific',
|
|
103
110
|
reasoning: {
|
|
104
|
-
|
|
111
|
+
why_standard: recipe.summary_cn || recipe.summary_en || recipe.title || '',
|
|
105
112
|
sources: [file.relativePath || file.name],
|
|
106
113
|
confidence: 0.7,
|
|
107
|
-
|
|
114
|
+
quality_signals: { origin: 'ai-scan', completeness: 'full' },
|
|
108
115
|
},
|
|
109
116
|
metadata: {
|
|
110
|
-
title: recipe.title || `[AI Scan] ${file.name}`,
|
|
111
|
-
description: recipe.summary_cn || recipe.summary_en || '',
|
|
112
|
-
knowledgeType: recipe.knowledgeType || 'code-pattern',
|
|
113
|
-
tags: [...(recipe.tags || []), 'ai-scan', file.targetName],
|
|
114
117
|
trigger: recipe.trigger || '',
|
|
115
|
-
scope: 'project-specific',
|
|
116
118
|
usageGuideCn: recipe.usageGuide_cn || '',
|
|
117
119
|
usageGuideEn: recipe.usageGuide_en || '',
|
|
118
120
|
headers: recipe.headers || [],
|
|
119
121
|
},
|
|
120
|
-
}
|
|
122
|
+
});
|
|
121
123
|
|
|
122
124
|
report.candidates++;
|
|
123
125
|
} catch (err) {
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KnowledgeSyncService — 将 .md 文件增量同步到 SQLite DB(knowledge_entries 表)
|
|
3
|
+
*
|
|
4
|
+
* 统一替代 SyncService (Recipe) + CandidateSyncService。
|
|
5
|
+
*
|
|
6
|
+
* 设计原则:
|
|
7
|
+
* - .md 文件 = 完整唯一数据源(Source of Truth),DB = 索引缓存
|
|
8
|
+
* - 通过 _content_hash 检测手写/手改 .md → 进入违规统计(audit_logs)
|
|
9
|
+
* - 孤儿 Entry(DB 有但 .md 不存在)→ 自动标记 deprecated
|
|
10
|
+
* - 同时扫描 AutoSnippet/candidates/ 和 AutoSnippet/recipes/ 两个目录
|
|
11
|
+
*
|
|
12
|
+
* 使用方式:
|
|
13
|
+
* - CLI: `asd sync` 委托调用
|
|
14
|
+
* - 内部: SetupService.stepDatabase() 委托调用(skipViolations = true)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { randomUUID } from 'node:crypto';
|
|
20
|
+
import { RECIPES_DIR, CANDIDATES_DIR } from '../infrastructure/config/Defaults.js';
|
|
21
|
+
import { parseKnowledgeMarkdown, computeKnowledgeHash } from '../service/knowledge/KnowledgeFileWriter.js';
|
|
22
|
+
import { KnowledgeEntry } from '../domain/knowledge/KnowledgeEntry.js';
|
|
23
|
+
import Logger from '../infrastructure/logging/Logger.js';
|
|
24
|
+
|
|
25
|
+
export class KnowledgeSyncService {
|
|
26
|
+
/**
|
|
27
|
+
* @param {string} projectRoot
|
|
28
|
+
*/
|
|
29
|
+
constructor(projectRoot) {
|
|
30
|
+
this.projectRoot = projectRoot;
|
|
31
|
+
this.recipesDir = path.join(projectRoot, RECIPES_DIR);
|
|
32
|
+
this.candidatesDir = path.join(projectRoot, CANDIDATES_DIR);
|
|
33
|
+
this.logger = Logger.getInstance();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 执行增量同步:.md → DB(knowledge_entries 表)
|
|
38
|
+
*
|
|
39
|
+
* 同时扫描 candidates/ 和 recipes/ 两个目录。
|
|
40
|
+
*
|
|
41
|
+
* @param {import('better-sqlite3').Database} db better-sqlite3 原始句柄
|
|
42
|
+
* @param {Object} [opts={}]
|
|
43
|
+
* @param {boolean} [opts.dryRun=false] 只报告不写入
|
|
44
|
+
* @param {boolean} [opts.force=false] 忽略 hash,强制覆盖
|
|
45
|
+
* @param {boolean} [opts.skipViolations=false] 跳过违规记录(setup 场景)
|
|
46
|
+
* @returns {{ synced: number, created: number, updated: number, violations: string[], orphaned: string[], skipped: number }}
|
|
47
|
+
*/
|
|
48
|
+
sync(db, opts = {}) {
|
|
49
|
+
const { dryRun = false, force = false, skipViolations = false } = opts;
|
|
50
|
+
|
|
51
|
+
const report = {
|
|
52
|
+
synced: 0,
|
|
53
|
+
created: 0,
|
|
54
|
+
updated: 0,
|
|
55
|
+
violations: [], // 手动编辑的文件列表
|
|
56
|
+
orphaned: [], // DB 有但 .md 不存在
|
|
57
|
+
skipped: 0,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// ── 1. 收集 .md 文件(两个目录) ──
|
|
61
|
+
const mdFiles = [
|
|
62
|
+
...this._collectMdFiles(this.candidatesDir, CANDIDATES_DIR),
|
|
63
|
+
...this._collectMdFiles(this.recipesDir, RECIPES_DIR),
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
if (mdFiles.length === 0) {
|
|
67
|
+
this.logger.info('KnowledgeSyncService: no .md files found');
|
|
68
|
+
return report;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── 2. 准备 upsert 语句 ──
|
|
72
|
+
const upsertStmt = dryRun ? null : this._prepareUpsert(db);
|
|
73
|
+
const auditStmt = (dryRun || skipViolations) ? null : this._prepareAuditInsert(db);
|
|
74
|
+
|
|
75
|
+
// ── 3. 逐文件同步 ──
|
|
76
|
+
const syncedIds = new Set();
|
|
77
|
+
|
|
78
|
+
for (const { absPath, relPath } of mdFiles) {
|
|
79
|
+
try {
|
|
80
|
+
const content = fs.readFileSync(absPath, 'utf8');
|
|
81
|
+
const parsed = parseKnowledgeMarkdown(content, relPath);
|
|
82
|
+
|
|
83
|
+
if (!parsed.id) {
|
|
84
|
+
this.logger.warn(`KnowledgeSyncService: skip file without id — ${relPath}`);
|
|
85
|
+
report.skipped++;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
syncedIds.add(parsed.id);
|
|
90
|
+
|
|
91
|
+
// ── 检测手动编辑 ──
|
|
92
|
+
const actualHash = computeKnowledgeHash(content);
|
|
93
|
+
const storedHash = parsed.content_hash;
|
|
94
|
+
const isManualEdit = storedHash && storedHash !== actualHash && !force;
|
|
95
|
+
|
|
96
|
+
if (isManualEdit) {
|
|
97
|
+
report.violations.push(relPath);
|
|
98
|
+
if (auditStmt) {
|
|
99
|
+
this._logViolation(auditStmt, parsed.id, relPath, storedHash, actualHash);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ── upsert ──
|
|
104
|
+
if (!dryRun) {
|
|
105
|
+
const existed = this._entryExists(db, parsed.id);
|
|
106
|
+
const row = this._buildDbRow(parsed, relPath, content);
|
|
107
|
+
upsertStmt.run(...Object.values(row));
|
|
108
|
+
|
|
109
|
+
if (existed) {
|
|
110
|
+
report.updated++;
|
|
111
|
+
} else {
|
|
112
|
+
report.created++;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
report.synced++;
|
|
117
|
+
} catch (err) {
|
|
118
|
+
this.logger.error(`KnowledgeSyncService: failed to sync ${relPath}`, { error: err.message });
|
|
119
|
+
report.skipped++;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ── 4. 检测孤儿 ──
|
|
124
|
+
report.orphaned = this._detectOrphans(db, syncedIds, dryRun);
|
|
125
|
+
|
|
126
|
+
this.logger.info('KnowledgeSyncService: sync complete', {
|
|
127
|
+
synced: report.synced,
|
|
128
|
+
created: report.created,
|
|
129
|
+
updated: report.updated,
|
|
130
|
+
violations: report.violations.length,
|
|
131
|
+
orphaned: report.orphaned.length,
|
|
132
|
+
skipped: report.skipped,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
return report;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/* ═══ 文件收集 ═══════════════════════════════════════════ */
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 递归收集指定目录下所有 .md 文件(跳过 _ 前缀模板)
|
|
142
|
+
* @param {string} dir 绝对目录路径
|
|
143
|
+
* @param {string} prefix 相对路径前缀 (e.g. 'AutoSnippet/candidates')
|
|
144
|
+
* @returns {{ absPath: string, relPath: string }[]}
|
|
145
|
+
*/
|
|
146
|
+
_collectMdFiles(dir, prefix) {
|
|
147
|
+
if (!fs.existsSync(dir)) return [];
|
|
148
|
+
|
|
149
|
+
const results = [];
|
|
150
|
+
const walk = (curDir, base) => {
|
|
151
|
+
for (const entry of fs.readdirSync(curDir, { withFileTypes: true })) {
|
|
152
|
+
const full = path.join(curDir, entry.name);
|
|
153
|
+
const rel = base ? `${base}/${entry.name}` : entry.name;
|
|
154
|
+
|
|
155
|
+
if (entry.isDirectory()) {
|
|
156
|
+
walk(full, rel);
|
|
157
|
+
} else if (entry.name.endsWith('.md') && !entry.name.startsWith('_')) {
|
|
158
|
+
results.push({
|
|
159
|
+
absPath: full,
|
|
160
|
+
relPath: `${prefix}/${rel}`,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
walk(dir, '');
|
|
166
|
+
return results;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/* ═══ DB 操作 ═══════════════════════════════════════════ */
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 从 parseKnowledgeMarkdown 的结果构建 DB row
|
|
173
|
+
* wire format → DB 列映射(与 KnowledgeRepository.impl 对齐)
|
|
174
|
+
*/
|
|
175
|
+
_buildDbRow(parsed, relPath, rawContent) {
|
|
176
|
+
const now = Math.floor(Date.now() / 1000);
|
|
177
|
+
|
|
178
|
+
// 内容 hash
|
|
179
|
+
const contentHash = computeKnowledgeHash(rawContent);
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
id: parsed.id,
|
|
183
|
+
title: parsed.title || '',
|
|
184
|
+
trigger_key: parsed.trigger || '',
|
|
185
|
+
description: parsed.description || '',
|
|
186
|
+
lifecycle: parsed.lifecycle || 'pending',
|
|
187
|
+
lifecycle_history: JSON.stringify(parsed.lifecycle_history || []),
|
|
188
|
+
probation: (parsed.auto_approvable ?? parsed.probation) ? 1 : 0,
|
|
189
|
+
language: parsed.language || 'swift',
|
|
190
|
+
category: parsed.category || 'general',
|
|
191
|
+
kind: parsed.kind || 'pattern',
|
|
192
|
+
knowledge_type: parsed.knowledge_type || 'code-pattern',
|
|
193
|
+
complexity: parsed.complexity || 'intermediate',
|
|
194
|
+
scope: parsed.scope || 'universal',
|
|
195
|
+
difficulty: parsed.difficulty || null,
|
|
196
|
+
tags: JSON.stringify(parsed.tags || []),
|
|
197
|
+
summary_cn: parsed.summary_cn || '',
|
|
198
|
+
summary_en: parsed.summary_en || '',
|
|
199
|
+
usage_guide_cn: parsed.usage_guide_cn || '',
|
|
200
|
+
usage_guide_en: parsed.usage_guide_en || '',
|
|
201
|
+
content: JSON.stringify(parsed.content || {}),
|
|
202
|
+
relations: JSON.stringify(parsed.relations || {}),
|
|
203
|
+
constraints: JSON.stringify(parsed.constraints || {}),
|
|
204
|
+
reasoning: JSON.stringify(parsed.reasoning || {}),
|
|
205
|
+
quality: JSON.stringify(parsed.quality || {}),
|
|
206
|
+
stats: JSON.stringify(parsed.stats || {}),
|
|
207
|
+
headers: JSON.stringify(parsed.headers || []),
|
|
208
|
+
header_paths: JSON.stringify(parsed.header_paths || []),
|
|
209
|
+
module_name: parsed.module_name || '',
|
|
210
|
+
include_headers: parsed.include_headers ? 1 : 0,
|
|
211
|
+
agent_notes: parsed.agent_notes ? JSON.stringify(parsed.agent_notes) : null,
|
|
212
|
+
ai_insight: parsed.ai_insight || null,
|
|
213
|
+
reviewed_by: parsed.reviewed_by || null,
|
|
214
|
+
reviewed_at: parsed.reviewed_at || null,
|
|
215
|
+
rejection_reason: parsed.rejection_reason || null,
|
|
216
|
+
source: parsed.source || 'file-sync',
|
|
217
|
+
source_file: relPath,
|
|
218
|
+
source_candidate_id: parsed.source_candidate_id || null,
|
|
219
|
+
created_by: parsed.created_by || 'file-sync',
|
|
220
|
+
created_at: parsed.created_at || now,
|
|
221
|
+
updated_at: parsed.updated_at || now,
|
|
222
|
+
published_at: parsed.published_at || null,
|
|
223
|
+
published_by: parsed.published_by || null,
|
|
224
|
+
content_hash: contentHash,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* 准备 upsert 语句(INSERT ... ON CONFLICT DO UPDATE 全字段)
|
|
230
|
+
*/
|
|
231
|
+
_prepareUpsert(db) {
|
|
232
|
+
const cols = [
|
|
233
|
+
'id', 'title', 'trigger_key', 'description',
|
|
234
|
+
'lifecycle', 'lifecycle_history', 'probation',
|
|
235
|
+
'language', 'category', 'kind', 'knowledge_type', 'complexity', 'scope', 'difficulty',
|
|
236
|
+
'tags', 'summary_cn', 'summary_en', 'usage_guide_cn', 'usage_guide_en',
|
|
237
|
+
'content', 'relations', 'constraints', 'reasoning', 'quality', 'stats',
|
|
238
|
+
'headers', 'header_paths', 'module_name', 'include_headers',
|
|
239
|
+
'agent_notes', 'ai_insight',
|
|
240
|
+
'reviewed_by', 'reviewed_at', 'rejection_reason',
|
|
241
|
+
'source', 'source_file', 'source_candidate_id',
|
|
242
|
+
'created_by', 'created_at', 'updated_at',
|
|
243
|
+
'published_at', 'published_by',
|
|
244
|
+
'content_hash',
|
|
245
|
+
];
|
|
246
|
+
|
|
247
|
+
// ON CONFLICT 更新除 id, created_by, created_at 以外的所有列
|
|
248
|
+
const updateCols = cols.filter(c => !['id', 'created_by', 'created_at'].includes(c));
|
|
249
|
+
const setClauses = updateCols.map(c => `${c} = excluded.${c}`).join(',\n ');
|
|
250
|
+
|
|
251
|
+
const sql = `
|
|
252
|
+
INSERT INTO knowledge_entries (${cols.join(', ')})
|
|
253
|
+
VALUES (${cols.map(() => '?').join(', ')})
|
|
254
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
255
|
+
${setClauses}
|
|
256
|
+
`;
|
|
257
|
+
|
|
258
|
+
return db.prepare(sql);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* 检查 entry 是否已存在于 DB
|
|
263
|
+
*/
|
|
264
|
+
_entryExists(db, id) {
|
|
265
|
+
const row = db.prepare('SELECT 1 FROM knowledge_entries WHERE id = ?').get(id);
|
|
266
|
+
return !!row;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/* ═══ 违规记录 ═══════════════════════════════════════════ */
|
|
270
|
+
|
|
271
|
+
_prepareAuditInsert(db) {
|
|
272
|
+
try {
|
|
273
|
+
return db.prepare(`
|
|
274
|
+
INSERT INTO audit_logs (id, timestamp, actor, actor_context, action, resource, operation_data, result, error_message, duration)
|
|
275
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
276
|
+
`);
|
|
277
|
+
} catch {
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
_logViolation(stmt, entryId, filePath, expectedHash, actualHash) {
|
|
283
|
+
try {
|
|
284
|
+
stmt.run(
|
|
285
|
+
randomUUID(),
|
|
286
|
+
Math.floor(Date.now() / 1000),
|
|
287
|
+
'sync',
|
|
288
|
+
JSON.stringify({ source: 'cli' }),
|
|
289
|
+
'manual_knowledge_edit',
|
|
290
|
+
entryId,
|
|
291
|
+
JSON.stringify({ file: filePath, expectedHash, actualHash }),
|
|
292
|
+
'violation_detected',
|
|
293
|
+
null,
|
|
294
|
+
0,
|
|
295
|
+
);
|
|
296
|
+
} catch (err) {
|
|
297
|
+
this.logger.warn('KnowledgeSyncService: failed to log violation', {
|
|
298
|
+
entryId,
|
|
299
|
+
error: err.message,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/* ═══ 孤儿检测 ═══════════════════════════════════════════ */
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* 检测 DB 中存在但 .md 已删除的 Entry → 标记 deprecated
|
|
308
|
+
* @returns {string[]} 孤儿 entry id 列表
|
|
309
|
+
*/
|
|
310
|
+
_detectOrphans(db, syncedIds, dryRun) {
|
|
311
|
+
const orphanIds = [];
|
|
312
|
+
try {
|
|
313
|
+
const rows = db.prepare(
|
|
314
|
+
`SELECT id, source_file FROM knowledge_entries
|
|
315
|
+
WHERE lifecycle NOT IN ('deprecated')
|
|
316
|
+
AND source_file IS NOT NULL`
|
|
317
|
+
).all();
|
|
318
|
+
|
|
319
|
+
for (const row of rows) {
|
|
320
|
+
if (!syncedIds.has(row.id)) {
|
|
321
|
+
orphanIds.push(row.id);
|
|
322
|
+
if (!dryRun) {
|
|
323
|
+
const now = Math.floor(Date.now() / 1000);
|
|
324
|
+
db.prepare(
|
|
325
|
+
`UPDATE knowledge_entries
|
|
326
|
+
SET lifecycle = 'deprecated',
|
|
327
|
+
rejection_reason = ?,
|
|
328
|
+
updated_at = ?
|
|
329
|
+
WHERE id = ?`
|
|
330
|
+
).run('source file deleted (orphan)', now, row.id);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
} catch (err) {
|
|
335
|
+
this.logger.warn('KnowledgeSyncService: orphan detection failed', {
|
|
336
|
+
error: err.message,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
return orphanIds;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export default KnowledgeSyncService;
|
package/lib/cli/SetupService.js
CHANGED
|
@@ -288,7 +288,7 @@ export class SetupService {
|
|
|
288
288
|
' requires_capability: ["git_write"]',
|
|
289
289
|
' - id: "external_agent"',
|
|
290
290
|
' name: "External Agent"',
|
|
291
|
-
' permissions: ["read:recipes", "read:guard_rules", "create:candidates", "submit:
|
|
291
|
+
' permissions: ["read:recipes", "read:guard_rules", "create:candidates", "submit:knowledge"]',
|
|
292
292
|
' - id: "chat_agent"',
|
|
293
293
|
' name: "ChatAgent"',
|
|
294
294
|
' permissions: ["read:recipes", "read:candidates", "create:candidates", "read:guard_rules"]',
|
|
@@ -571,40 +571,22 @@ export class SetupService {
|
|
|
571
571
|
}
|
|
572
572
|
|
|
573
573
|
/**
|
|
574
|
-
* @private 从 AutoSnippet/recipes/*.md 同步到 DB 缓存
|
|
575
|
-
* 委托
|
|
574
|
+
* @private 从 AutoSnippet/recipes/*.md + candidates/*.md 同步到 DB 缓存
|
|
575
|
+
* 委托 KnowledgeSyncService 执行全字段同步(setup 场景跳过违规记录)
|
|
576
576
|
*/
|
|
577
577
|
async _syncRecipesToDB(db) {
|
|
578
|
-
const {
|
|
579
|
-
const syncService = new
|
|
578
|
+
const { KnowledgeSyncService } = await import('./KnowledgeSyncService.js');
|
|
579
|
+
const syncService = new KnowledgeSyncService(this.projectRoot);
|
|
580
580
|
const report = syncService.sync(db, { skipViolations: true });
|
|
581
581
|
|
|
582
582
|
if (report.synced > 0) {
|
|
583
|
-
console.log(` ✅ 已同步 ${report.synced}
|
|
583
|
+
console.log(` ✅ 已同步 ${report.synced} 个知识文件到 DB 缓存(新增 ${report.created},更新 ${report.updated})`);
|
|
584
584
|
} else {
|
|
585
|
-
console.log(' ℹ️
|
|
585
|
+
console.log(' ℹ️ 暂无 .md 文件,跳过同步');
|
|
586
586
|
}
|
|
587
587
|
|
|
588
588
|
if (report.orphaned.length > 0) {
|
|
589
|
-
console.log(` ℹ️ ${report.orphaned.length}
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
// ── Candidate 文件同步 ──
|
|
593
|
-
await this._syncCandidatesToDB(db);
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
/**
|
|
597
|
-
* @private 从 AutoSnippet/candidates/*.md 同步到 DB 缓存
|
|
598
|
-
*/
|
|
599
|
-
async _syncCandidatesToDB(db) {
|
|
600
|
-
const { CandidateSyncService } = await import('./CandidateSyncService.js');
|
|
601
|
-
const syncService = new CandidateSyncService(this.projectRoot);
|
|
602
|
-
const report = syncService.sync(db, { skipViolations: true });
|
|
603
|
-
|
|
604
|
-
if (report.synced > 0) {
|
|
605
|
-
console.log(` ✅ 已同步 ${report.synced} 个 Candidate 文件到 DB 缓存(新增 ${report.created},更新 ${report.updated})`);
|
|
606
|
-
} else {
|
|
607
|
-
console.log(' ℹ️ candidates/ 暂无 .md 文件,跳过同步');
|
|
589
|
+
console.log(` ℹ️ ${report.orphaned.length} 个孤儿条目已标记 deprecated`);
|
|
608
590
|
}
|
|
609
591
|
}
|
|
610
592
|
|
|
@@ -17,133 +17,125 @@ const logger = Logger.getInstance();
|
|
|
17
17
|
* @param {import('../../injection/ServiceContainer.js').ServiceContainer} container
|
|
18
18
|
*/
|
|
19
19
|
export function registerGatewayActions(gateway, container) {
|
|
20
|
-
// ==========
|
|
20
|
+
// ========== Knowledge Actions (V3: replaces Candidate + Recipe) ==========
|
|
21
21
|
|
|
22
22
|
gateway.register('candidate:create', async (ctx) => {
|
|
23
|
-
const service = container.get('
|
|
24
|
-
return service.
|
|
23
|
+
const service = container.get('knowledgeService');
|
|
24
|
+
return service.create(ctx.data, {
|
|
25
25
|
userId: ctx.actor,
|
|
26
|
-
ip: ctx.data._ip,
|
|
27
|
-
userAgent: ctx.data._userAgent,
|
|
28
26
|
});
|
|
29
27
|
});
|
|
30
28
|
|
|
31
29
|
gateway.register('candidate:approve', async (ctx) => {
|
|
32
|
-
const service = container.get('
|
|
33
|
-
return service.
|
|
30
|
+
const service = container.get('knowledgeService');
|
|
31
|
+
return service.approve(ctx.data.candidateId, {
|
|
34
32
|
userId: ctx.actor,
|
|
35
33
|
});
|
|
36
34
|
});
|
|
37
35
|
|
|
38
36
|
gateway.register('candidate:reject', async (ctx) => {
|
|
39
|
-
const service = container.get('
|
|
40
|
-
return service.
|
|
37
|
+
const service = container.get('knowledgeService');
|
|
38
|
+
return service.reject(ctx.data.candidateId, ctx.data.reason, {
|
|
41
39
|
userId: ctx.actor,
|
|
42
40
|
});
|
|
43
41
|
});
|
|
44
42
|
|
|
45
43
|
gateway.register('candidate:apply_to_recipe', async (ctx) => {
|
|
46
|
-
const service = container.get('
|
|
47
|
-
return service.
|
|
48
|
-
userId: ctx.actor,
|
|
49
|
-
});
|
|
44
|
+
const service = container.get('knowledgeService');
|
|
45
|
+
return service.publish(ctx.data.candidateId, { userId: ctx.actor });
|
|
50
46
|
});
|
|
51
47
|
|
|
52
48
|
gateway.register('candidate:list', async (ctx) => {
|
|
53
|
-
const service = container.get('
|
|
54
|
-
return service.
|
|
49
|
+
const service = container.get('knowledgeService');
|
|
50
|
+
return service.list(ctx.data.filters, ctx.data.pagination);
|
|
55
51
|
});
|
|
56
52
|
|
|
57
53
|
gateway.register('candidate:search', async (ctx) => {
|
|
58
|
-
const service = container.get('
|
|
59
|
-
return service.
|
|
54
|
+
const service = container.get('knowledgeService');
|
|
55
|
+
return service.search(ctx.data.keyword, ctx.data.pagination);
|
|
60
56
|
});
|
|
61
57
|
|
|
62
58
|
gateway.register('candidate:get_stats', async (ctx) => {
|
|
63
|
-
const service = container.get('
|
|
64
|
-
return service.
|
|
59
|
+
const service = container.get('knowledgeService');
|
|
60
|
+
return service.getStats();
|
|
65
61
|
});
|
|
66
62
|
|
|
67
63
|
gateway.register('candidate:get', async (ctx) => {
|
|
68
|
-
const
|
|
69
|
-
return
|
|
64
|
+
const service = container.get('knowledgeService');
|
|
65
|
+
return service.get(ctx.data.id);
|
|
70
66
|
});
|
|
71
67
|
|
|
72
68
|
gateway.register('candidate:delete', async (ctx) => {
|
|
73
|
-
const service = container.get('
|
|
74
|
-
return service.
|
|
69
|
+
const service = container.get('knowledgeService');
|
|
70
|
+
return service.delete(ctx.data.candidateId, { userId: ctx.actor });
|
|
75
71
|
});
|
|
76
72
|
|
|
77
|
-
// ========== Recipe Actions ==========
|
|
73
|
+
// ========== Recipe Actions (V3: routed to knowledgeService) ==========
|
|
78
74
|
|
|
79
75
|
gateway.register('recipe:create', async (ctx) => {
|
|
80
|
-
const service = container.get('
|
|
81
|
-
return service.
|
|
76
|
+
const service = container.get('knowledgeService');
|
|
77
|
+
return service.create(ctx.data, {
|
|
82
78
|
userId: ctx.actor,
|
|
83
|
-
ip: ctx.data._ip,
|
|
84
|
-
userAgent: ctx.data._userAgent,
|
|
85
79
|
});
|
|
86
80
|
});
|
|
87
81
|
|
|
88
82
|
gateway.register('recipe:publish', async (ctx) => {
|
|
89
|
-
const service = container.get('
|
|
90
|
-
return service.
|
|
83
|
+
const service = container.get('knowledgeService');
|
|
84
|
+
return service.publish(ctx.data.recipeId, {
|
|
91
85
|
userId: ctx.actor,
|
|
92
86
|
});
|
|
93
87
|
});
|
|
94
88
|
|
|
95
89
|
gateway.register('recipe:deprecate', async (ctx) => {
|
|
96
|
-
const service = container.get('
|
|
97
|
-
return service.
|
|
90
|
+
const service = container.get('knowledgeService');
|
|
91
|
+
return service.deprecate(ctx.data.recipeId, ctx.data.reason, {
|
|
98
92
|
userId: ctx.actor,
|
|
99
93
|
});
|
|
100
94
|
});
|
|
101
95
|
|
|
102
96
|
gateway.register('recipe:update_quality', async (ctx) => {
|
|
103
|
-
const service = container.get('
|
|
104
|
-
return service.updateQuality(ctx.data.recipeId, ctx.data.metrics
|
|
105
|
-
userId: ctx.actor,
|
|
106
|
-
});
|
|
97
|
+
const service = container.get('knowledgeService');
|
|
98
|
+
return service.updateQuality(ctx.data.recipeId, ctx.data.metrics);
|
|
107
99
|
});
|
|
108
100
|
|
|
109
101
|
gateway.register('recipe:adopt', async (ctx) => {
|
|
110
|
-
const service = container.get('
|
|
111
|
-
return service.
|
|
102
|
+
const service = container.get('knowledgeService');
|
|
103
|
+
return service.incrementUsage(ctx.data.recipeId, 'adoption');
|
|
112
104
|
});
|
|
113
105
|
|
|
114
106
|
gateway.register('recipe:apply', async (ctx) => {
|
|
115
|
-
const service = container.get('
|
|
116
|
-
return service.
|
|
107
|
+
const service = container.get('knowledgeService');
|
|
108
|
+
return service.incrementUsage(ctx.data.recipeId, 'application');
|
|
117
109
|
});
|
|
118
110
|
|
|
119
111
|
gateway.register('recipe:list', async (ctx) => {
|
|
120
|
-
const service = container.get('
|
|
121
|
-
return service.
|
|
112
|
+
const service = container.get('knowledgeService');
|
|
113
|
+
return service.list(ctx.data.filters, ctx.data.pagination);
|
|
122
114
|
});
|
|
123
115
|
|
|
124
116
|
gateway.register('recipe:search', async (ctx) => {
|
|
125
|
-
const service = container.get('
|
|
126
|
-
return service.
|
|
117
|
+
const service = container.get('knowledgeService');
|
|
118
|
+
return service.search(ctx.data.keyword, ctx.data.pagination);
|
|
127
119
|
});
|
|
128
120
|
|
|
129
121
|
gateway.register('recipe:get_stats', async (ctx) => {
|
|
130
|
-
const service = container.get('
|
|
131
|
-
return service.
|
|
122
|
+
const service = container.get('knowledgeService');
|
|
123
|
+
return service.getStats();
|
|
132
124
|
});
|
|
133
125
|
|
|
134
126
|
gateway.register('recipe:get', async (ctx) => {
|
|
135
|
-
const
|
|
136
|
-
return
|
|
127
|
+
const service = container.get('knowledgeService');
|
|
128
|
+
return service.get(ctx.data.id);
|
|
137
129
|
});
|
|
138
130
|
|
|
139
131
|
gateway.register('recipe:get_recommendations', async (ctx) => {
|
|
140
|
-
const service = container.get('
|
|
141
|
-
return service.
|
|
132
|
+
const service = container.get('knowledgeService');
|
|
133
|
+
return service.list({ lifecycle: 'active' }, { page: 1, pageSize: ctx.data.limit || 10 });
|
|
142
134
|
});
|
|
143
135
|
|
|
144
136
|
gateway.register('recipe:delete', async (ctx) => {
|
|
145
|
-
const service = container.get('
|
|
146
|
-
return service.
|
|
137
|
+
const service = container.get('knowledgeService');
|
|
138
|
+
return service.delete(ctx.data.recipeId, {
|
|
147
139
|
userId: ctx.actor,
|
|
148
140
|
});
|
|
149
141
|
});
|
|
@@ -200,19 +192,17 @@ export function registerGatewayActions(gateway, container) {
|
|
|
200
192
|
});
|
|
201
193
|
|
|
202
194
|
gateway.register('guard_rule:get', async (ctx) => {
|
|
203
|
-
const repo = container.get('
|
|
195
|
+
const repo = container.get('knowledgeRepository');
|
|
204
196
|
return repo.findById(ctx.data.id);
|
|
205
197
|
});
|
|
206
198
|
|
|
207
199
|
// ========== Search Actions ==========
|
|
208
200
|
|
|
209
|
-
// ==========
|
|
201
|
+
// ========== Knowledge Update (enrich/refine) ==========
|
|
210
202
|
|
|
211
203
|
gateway.register('candidate:update', async (ctx) => {
|
|
212
|
-
const service = container.get('
|
|
213
|
-
return service.
|
|
214
|
-
? service.updateCandidate(ctx.data.id, ctx.data, { userId: ctx.actor })
|
|
215
|
-
: service.createCandidate(ctx.data, { userId: ctx.actor });
|
|
204
|
+
const service = container.get('knowledgeService');
|
|
205
|
+
return service.update(ctx.data.id, ctx.data, { userId: ctx.actor });
|
|
216
206
|
});
|
|
217
207
|
|
|
218
208
|
// ========== Search ==========
|