autosnippet 2.8.3 → 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 +5 -5
- package/bin/cli.js +5 -33
- package/config/constitution.yaml +9 -2
- package/dashboard/dist/assets/{icons-B_Xg4B-s.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 +9 -27
- package/lib/core/ast/ProjectGraph.js +160 -0
- 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/ai/providers/GoogleGeminiProvider.js +12 -3
- package/lib/external/mcp/McpServer.js +7 -5
- package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +3 -2
- package/lib/external/mcp/handlers/bootstrap.js +121 -12
- 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/skill.js +4 -2
- 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/middleware/requestLogger.js +3 -3
- package/lib/http/routes/ai.js +17 -1
- 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/http/routes/skills.js +44 -1
- package/lib/infrastructure/cache/GraphCache.js +143 -0
- package/lib/infrastructure/database/migrations/015_create_token_usage.js +27 -0
- package/lib/infrastructure/database/migrations/016_unified_knowledge_entries.js +395 -0
- package/lib/infrastructure/external/XcodeAutomation.js +187 -103
- package/lib/infrastructure/realtime/RealtimeService.js +14 -2
- package/lib/injection/ServiceContainer.js +164 -63
- package/lib/repository/knowledge/KnowledgeRepository.impl.js +373 -0
- package/lib/repository/token/TokenUsageStore.js +162 -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/AnalystAgent.js +25 -14
- package/lib/service/chat/CandidateGuardrail.js +1 -1
- package/lib/service/chat/ChatAgent.js +280 -48
- package/lib/service/chat/ContextWindow.js +92 -8
- package/lib/service/chat/HandoffProtocol.js +26 -1
- package/lib/service/chat/ProducerAgent.js +11 -9
- package/lib/service/chat/tools.js +298 -194
- 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 +15 -9
- package/lib/service/skills/SkillAdvisor.js +13 -11
- package/lib/service/snippet/SnippetFactory.js +5 -5
- package/lib/service/spm/SpmService.js +119 -18
- 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-CkIih2CC.css +0 -1
- package/dashboard/dist/assets/index-Duc8Qk-c.js +0 -197
- 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 -973
- package/lib/service/recipe/RecipeFileWriter.js +0 -514
- package/lib/service/recipe/RecipeService.js +0 -786
- package/lib/service/recipe/RecipeStatsTracker.js +0 -148
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Knowledge API 路由 (V3)
|
|
3
|
+
* 统一知识条目的 CRUD + 生命周期操作
|
|
4
|
+
* 替代 recipes.js + candidates.js (旧路由继续保留用于向后兼容)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import express from 'express';
|
|
8
|
+
import { asyncHandler } from '../middleware/errorHandler.js';
|
|
9
|
+
import { getServiceContainer } from '../../injection/ServiceContainer.js';
|
|
10
|
+
import { ValidationError } from '../../shared/errors/index.js';
|
|
11
|
+
import Logger from '../../infrastructure/logging/Logger.js';
|
|
12
|
+
import { getContext, safeInt } from '../utils/routeHelpers.js';
|
|
13
|
+
|
|
14
|
+
const logger = Logger.getInstance();
|
|
15
|
+
const router = express.Router();
|
|
16
|
+
|
|
17
|
+
const MAX_BATCH_SIZE = 100;
|
|
18
|
+
|
|
19
|
+
/* ═══ 查询 ═══════════════════════════════════════════════ */
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* GET /api/v1/knowledge
|
|
23
|
+
* 获取知识条目列表(支持筛选和分页)
|
|
24
|
+
*/
|
|
25
|
+
router.get('/', asyncHandler(async (req, res) => {
|
|
26
|
+
const { lifecycle, kind, category, language, knowledgeType, scope, keyword, tag, source } = req.query;
|
|
27
|
+
const page = safeInt(req.query.page, 1);
|
|
28
|
+
const pageSize = safeInt(req.query.limit, 20, 1, 1000);
|
|
29
|
+
|
|
30
|
+
const container = getServiceContainer();
|
|
31
|
+
const knowledgeService = container.get('knowledgeService');
|
|
32
|
+
|
|
33
|
+
if (keyword) {
|
|
34
|
+
const result = await knowledgeService.search(keyword, { page, pageSize });
|
|
35
|
+
return res.json({ success: true, data: result });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const filters = {};
|
|
39
|
+
if (lifecycle) filters.lifecycle = lifecycle;
|
|
40
|
+
if (kind) filters.kind = kind;
|
|
41
|
+
if (category) filters.category = category;
|
|
42
|
+
if (language) filters.language = language;
|
|
43
|
+
if (knowledgeType) filters.knowledgeType = knowledgeType;
|
|
44
|
+
if (scope) filters.scope = scope;
|
|
45
|
+
if (tag) filters.tag = tag;
|
|
46
|
+
if (source) filters.source = source;
|
|
47
|
+
|
|
48
|
+
const result = await knowledgeService.list(filters, { page, pageSize });
|
|
49
|
+
res.json({ success: true, data: result });
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* GET /api/v1/knowledge/stats
|
|
54
|
+
* 获取统计信息
|
|
55
|
+
*/
|
|
56
|
+
router.get('/stats', asyncHandler(async (req, res) => {
|
|
57
|
+
const container = getServiceContainer();
|
|
58
|
+
const knowledgeService = container.get('knowledgeService');
|
|
59
|
+
const stats = await knowledgeService.getStats();
|
|
60
|
+
res.json({ success: true, data: stats });
|
|
61
|
+
}));
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* GET /api/v1/knowledge/:id
|
|
65
|
+
* 获取知识条目详情
|
|
66
|
+
*/
|
|
67
|
+
router.get('/:id', asyncHandler(async (req, res) => {
|
|
68
|
+
const { id } = req.params;
|
|
69
|
+
const container = getServiceContainer();
|
|
70
|
+
const knowledgeService = container.get('knowledgeService');
|
|
71
|
+
const entry = await knowledgeService.get(id);
|
|
72
|
+
res.json({ success: true, data: entry.toJSON() });
|
|
73
|
+
}));
|
|
74
|
+
|
|
75
|
+
/* ═══ CRUD ═══════════════════════════════════════════════ */
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* POST /api/v1/knowledge
|
|
79
|
+
* 创建知识条目(wire format 直通)
|
|
80
|
+
*/
|
|
81
|
+
router.post('/', asyncHandler(async (req, res) => {
|
|
82
|
+
const data = req.body;
|
|
83
|
+
|
|
84
|
+
if (!data.title || !data.content) {
|
|
85
|
+
throw new ValidationError('title and content are required');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const container = getServiceContainer();
|
|
89
|
+
const knowledgeService = container.get('knowledgeService');
|
|
90
|
+
const context = getContext(req);
|
|
91
|
+
|
|
92
|
+
const entry = await knowledgeService.create(data, context);
|
|
93
|
+
res.status(201).json({
|
|
94
|
+
success: true,
|
|
95
|
+
data: entry.toJSON(),
|
|
96
|
+
});
|
|
97
|
+
}));
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* PATCH /api/v1/knowledge/:id
|
|
101
|
+
* 更新知识条目(白名单字段)
|
|
102
|
+
*/
|
|
103
|
+
router.patch('/:id', asyncHandler(async (req, res) => {
|
|
104
|
+
const { id } = req.params;
|
|
105
|
+
const container = getServiceContainer();
|
|
106
|
+
const knowledgeService = container.get('knowledgeService');
|
|
107
|
+
const context = getContext(req);
|
|
108
|
+
|
|
109
|
+
const entry = await knowledgeService.update(id, req.body, context);
|
|
110
|
+
res.json({ success: true, data: entry.toJSON() });
|
|
111
|
+
}));
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* DELETE /api/v1/knowledge/:id
|
|
115
|
+
* 删除知识条目
|
|
116
|
+
*/
|
|
117
|
+
router.delete('/:id', asyncHandler(async (req, res) => {
|
|
118
|
+
const { id } = req.params;
|
|
119
|
+
const container = getServiceContainer();
|
|
120
|
+
const knowledgeService = container.get('knowledgeService');
|
|
121
|
+
const context = getContext(req);
|
|
122
|
+
|
|
123
|
+
const result = await knowledgeService.delete(id, context);
|
|
124
|
+
res.json({ success: true, data: result });
|
|
125
|
+
}));
|
|
126
|
+
|
|
127
|
+
/* ═══ 生命周期操作(3 状态: pending / active / deprecated)═══ */
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* PATCH /api/v1/knowledge/:id/publish
|
|
131
|
+
* 发布 (pending → active) — 仅开发者
|
|
132
|
+
*/
|
|
133
|
+
router.patch('/:id/publish', asyncHandler(async (req, res) => {
|
|
134
|
+
const { id } = req.params;
|
|
135
|
+
const container = getServiceContainer();
|
|
136
|
+
const knowledgeService = container.get('knowledgeService');
|
|
137
|
+
const context = getContext(req);
|
|
138
|
+
|
|
139
|
+
const entry = await knowledgeService.publish(id, context);
|
|
140
|
+
res.json({ success: true, data: entry.toJSON() });
|
|
141
|
+
}));
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* PATCH /api/v1/knowledge/:id/deprecate
|
|
145
|
+
* 废弃 (pending|active → deprecated)
|
|
146
|
+
*/
|
|
147
|
+
router.patch('/:id/deprecate', asyncHandler(async (req, res) => {
|
|
148
|
+
const { id } = req.params;
|
|
149
|
+
const { reason } = req.body;
|
|
150
|
+
|
|
151
|
+
if (!reason) {
|
|
152
|
+
throw new ValidationError('reason is required for deprecation');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const container = getServiceContainer();
|
|
156
|
+
const knowledgeService = container.get('knowledgeService');
|
|
157
|
+
const context = getContext(req);
|
|
158
|
+
|
|
159
|
+
const entry = await knowledgeService.deprecate(id, reason, context);
|
|
160
|
+
res.json({ success: true, data: entry.toJSON() });
|
|
161
|
+
}));
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* PATCH /api/v1/knowledge/:id/reactivate
|
|
165
|
+
* 重新激活 (deprecated → pending)
|
|
166
|
+
*/
|
|
167
|
+
router.patch('/:id/reactivate', asyncHandler(async (req, res) => {
|
|
168
|
+
const { id } = req.params;
|
|
169
|
+
const container = getServiceContainer();
|
|
170
|
+
const knowledgeService = container.get('knowledgeService');
|
|
171
|
+
const context = getContext(req);
|
|
172
|
+
|
|
173
|
+
const entry = await knowledgeService.reactivate(id, context);
|
|
174
|
+
res.json({ success: true, data: entry.toJSON() });
|
|
175
|
+
}));
|
|
176
|
+
|
|
177
|
+
/* ═══ 批量操作 ═══════════════════════════════════════════ */
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* POST /api/v1/knowledge/batch-publish
|
|
181
|
+
* 批量发布 (pending → active)
|
|
182
|
+
* 支持 autoApprovableOnly=true 参数,只发布 auto_approvable 的条目
|
|
183
|
+
*/
|
|
184
|
+
router.post('/batch-publish', asyncHandler(async (req, res) => {
|
|
185
|
+
const { ids } = req.body;
|
|
186
|
+
|
|
187
|
+
if (!Array.isArray(ids) || ids.length === 0) {
|
|
188
|
+
throw new ValidationError('ids array is required and must not be empty');
|
|
189
|
+
}
|
|
190
|
+
if (ids.length > MAX_BATCH_SIZE) {
|
|
191
|
+
throw new ValidationError(`Batch size exceeds limit of ${MAX_BATCH_SIZE}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const container = getServiceContainer();
|
|
195
|
+
const knowledgeService = container.get('knowledgeService');
|
|
196
|
+
const context = getContext(req);
|
|
197
|
+
|
|
198
|
+
const results = await Promise.allSettled(
|
|
199
|
+
ids.map(id => knowledgeService.publish(id, context)),
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const published = results.filter(r => r.status === 'fulfilled').map(r => r.value.toJSON());
|
|
203
|
+
const failed = results
|
|
204
|
+
.map((r, i) => r.status === 'rejected' ? { id: ids[i], error: r.reason?.message } : null)
|
|
205
|
+
.filter(Boolean);
|
|
206
|
+
|
|
207
|
+
res.json({
|
|
208
|
+
success: true,
|
|
209
|
+
data: { published, failed, total: ids.length, successCount: published.length, failureCount: failed.length },
|
|
210
|
+
});
|
|
211
|
+
}));
|
|
212
|
+
|
|
213
|
+
/* ═══ 使用 / 质量 ═══════════════════════════════════════ */
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* POST /api/v1/knowledge/:id/usage
|
|
217
|
+
* 记录使用(adoption / application / guard_hit / view / success)
|
|
218
|
+
*/
|
|
219
|
+
router.post('/:id/usage', asyncHandler(async (req, res) => {
|
|
220
|
+
const { id } = req.params;
|
|
221
|
+
const { type = 'adoption', feedback } = req.body;
|
|
222
|
+
const context = getContext(req);
|
|
223
|
+
|
|
224
|
+
const container = getServiceContainer();
|
|
225
|
+
const knowledgeService = container.get('knowledgeService');
|
|
226
|
+
|
|
227
|
+
await knowledgeService.incrementUsage(id, type, { actor: context.userId, feedback });
|
|
228
|
+
res.json({ success: true, message: `${type} recorded` });
|
|
229
|
+
}));
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* PATCH /api/v1/knowledge/:id/quality
|
|
233
|
+
* 重新计算质量评分
|
|
234
|
+
*/
|
|
235
|
+
router.patch('/:id/quality', asyncHandler(async (req, res) => {
|
|
236
|
+
const { id } = req.params;
|
|
237
|
+
const context = getContext(req);
|
|
238
|
+
|
|
239
|
+
const container = getServiceContainer();
|
|
240
|
+
const knowledgeService = container.get('knowledgeService');
|
|
241
|
+
|
|
242
|
+
const result = await knowledgeService.updateQuality(id, context);
|
|
243
|
+
res.json({ success: true, data: result });
|
|
244
|
+
}));
|
|
245
|
+
|
|
246
|
+
export default router;
|
|
@@ -44,13 +44,13 @@ router.get('/', asyncHandler(async (req, res) => {
|
|
|
44
44
|
const results = {};
|
|
45
45
|
const pagination = { page, pageSize: limit };
|
|
46
46
|
|
|
47
|
-
//
|
|
47
|
+
// 搜索知识条目(V3 统一模型)
|
|
48
48
|
if (type === 'all' || type === 'recipe' || type === 'solution') {
|
|
49
49
|
try {
|
|
50
|
-
const
|
|
51
|
-
results.recipes = await
|
|
50
|
+
const knowledgeService = container.get('knowledgeService');
|
|
51
|
+
results.recipes = await knowledgeService.search(q, pagination);
|
|
52
52
|
} catch (err) {
|
|
53
|
-
logger.warn('
|
|
53
|
+
logger.warn('Knowledge 搜索失败', { query: q, error: err.message });
|
|
54
54
|
results.recipes = { items: [], total: 0 };
|
|
55
55
|
}
|
|
56
56
|
}
|
|
@@ -66,11 +66,11 @@ router.get('/', asyncHandler(async (req, res) => {
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
//
|
|
69
|
+
// 搜索候选知识条目 (V3: lifecycle=draft/pending)
|
|
70
70
|
if (type === 'all' || type === 'candidate') {
|
|
71
71
|
try {
|
|
72
|
-
const
|
|
73
|
-
results.candidates = await
|
|
72
|
+
const knowledgeService = container.get('knowledgeService');
|
|
73
|
+
results.candidates = await knowledgeService.search(q, pagination);
|
|
74
74
|
} catch (err) {
|
|
75
75
|
logger.warn('Candidate 搜索失败', { query: q, error: err.message });
|
|
76
76
|
results.candidates = { items: [], total: 0 };
|
|
@@ -172,18 +172,14 @@ router.get('/graph/all', asyncHandler(async (req, res) => {
|
|
|
172
172
|
const nodeTypes = {}; // id → 主要类型(供前端区分渲染)
|
|
173
173
|
const nodeCategories = {}; // id → category/target 名(供前端分组布局)
|
|
174
174
|
if (nodeMap.size > 0) {
|
|
175
|
-
const
|
|
176
|
-
// 使用 repository.findById 避免 RecipeService.getRecipe 在找不到时记录 error 日志
|
|
177
|
-
const recipeRepo = recipeService?.recipeRepository ?? null;
|
|
175
|
+
const knowledgeRepo = container.get('knowledgeRepository');
|
|
178
176
|
for (const [id, types] of nodeMap) {
|
|
179
|
-
// 记录节点主要类型
|
|
180
177
|
const primaryType = types.has('recipe') ? 'recipe' : [...types][0];
|
|
181
178
|
nodeTypes[id] = primaryType;
|
|
182
179
|
|
|
183
|
-
if (primaryType === 'recipe' &&
|
|
184
|
-
// 仅 recipe 类型才查 DB;直接用 repo 避免 NotFoundError 日志噪音
|
|
180
|
+
if ((primaryType === 'recipe' || primaryType === 'knowledge') && knowledgeRepo) {
|
|
185
181
|
try {
|
|
186
|
-
const r = await
|
|
182
|
+
const r = await knowledgeRepo.findById(id);
|
|
187
183
|
if (r) {
|
|
188
184
|
nodeLabels[id] = r.title || r.name || id;
|
|
189
185
|
nodeCategories[id] = r.category || '';
|
|
@@ -191,7 +187,6 @@ router.get('/graph/all', asyncHandler(async (req, res) => {
|
|
|
191
187
|
}
|
|
192
188
|
} catch { /* not found – fall through */ }
|
|
193
189
|
}
|
|
194
|
-
// module / candidate 或查不到的 recipe → 直接用 ID
|
|
195
190
|
nodeLabels[id] = id;
|
|
196
191
|
}
|
|
197
192
|
}
|
|
@@ -226,9 +221,9 @@ router.post('/context-aware', asyncHandler(async (req, res) => {
|
|
|
226
221
|
throw new ValidationError('keyword is required');
|
|
227
222
|
}
|
|
228
223
|
const container = getServiceContainer();
|
|
229
|
-
const
|
|
224
|
+
const knowledgeService = container.get('knowledgeService');
|
|
230
225
|
const pageSize = Math.min(limit || 10, 100);
|
|
231
|
-
const list = await
|
|
226
|
+
const list = await knowledgeService.search(keyword, { page: 1, pageSize });
|
|
232
227
|
const items = list.data || list.items || [];
|
|
233
228
|
const results = items.map(r => ({
|
|
234
229
|
name: (r.title || r.id) + '.md',
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import express from 'express';
|
|
7
7
|
import { asyncHandler } from '../middleware/errorHandler.js';
|
|
8
|
-
import { listSkills, loadSkill, createSkill, suggestSkills } from '../../external/mcp/handlers/skill.js';
|
|
8
|
+
import { listSkills, loadSkill, createSkill, deleteSkill, updateSkill, suggestSkills } from '../../external/mcp/handlers/skill.js';
|
|
9
9
|
import { ValidationError } from '../../shared/errors/index.js';
|
|
10
10
|
|
|
11
11
|
const router = express.Router();
|
|
@@ -107,4 +107,47 @@ router.post('/', asyncHandler(async (req, res) => {
|
|
|
107
107
|
res.status(201).json({ success: true, data: parsed.data });
|
|
108
108
|
}));
|
|
109
109
|
|
|
110
|
+
/**
|
|
111
|
+
* PUT /api/v1/skills/:name
|
|
112
|
+
* 更新项目级 Skill(description / content)
|
|
113
|
+
*/
|
|
114
|
+
router.put('/:name', asyncHandler(async (req, res) => {
|
|
115
|
+
const { name } = req.params;
|
|
116
|
+
const { description, content } = req.body;
|
|
117
|
+
|
|
118
|
+
if (!description && !content) {
|
|
119
|
+
throw new ValidationError('At least one of description or content must be provided');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const raw = updateSkill(null, { name, description, content });
|
|
123
|
+
const parsed = JSON.parse(raw);
|
|
124
|
+
|
|
125
|
+
if (!parsed.success) {
|
|
126
|
+
const status = parsed.error?.code === 'SKILL_NOT_FOUND' ? 404
|
|
127
|
+
: parsed.error?.code === 'BUILTIN_PROTECTED' ? 403 : 500;
|
|
128
|
+
return res.status(status).json(parsed);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
res.json({ success: true, data: parsed.data });
|
|
132
|
+
}));
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* DELETE /api/v1/skills/:name
|
|
136
|
+
* 删除项目级 Skill
|
|
137
|
+
*/
|
|
138
|
+
router.delete('/:name', asyncHandler(async (req, res) => {
|
|
139
|
+
const { name } = req.params;
|
|
140
|
+
|
|
141
|
+
const raw = deleteSkill(null, { name });
|
|
142
|
+
const parsed = JSON.parse(raw);
|
|
143
|
+
|
|
144
|
+
if (!parsed.success) {
|
|
145
|
+
const status = parsed.error?.code === 'SKILL_NOT_FOUND' ? 404
|
|
146
|
+
: parsed.error?.code === 'BUILTIN_PROTECTED' ? 403 : 500;
|
|
147
|
+
return res.status(status).json(parsed);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
res.json({ success: true, data: parsed.data });
|
|
151
|
+
}));
|
|
152
|
+
|
|
110
153
|
export default router;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GraphCache — 基于文件的图数据持久化缓存
|
|
3
|
+
*
|
|
4
|
+
* 功能:
|
|
5
|
+
* 1. 将图数据序列化为 JSON 写入磁盘
|
|
6
|
+
* 2. 基于 contentHash 判断缓存是否有效(Package.swift / 源文件)
|
|
7
|
+
* 3. 支持 SPM 依赖图和 AST ProjectGraph 两种场景
|
|
8
|
+
*
|
|
9
|
+
* 缓存位置: {projectRoot}/.autosnippet/cache/
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from 'node:fs';
|
|
13
|
+
import { join, relative } from 'node:path';
|
|
14
|
+
import { createHash } from 'node:crypto';
|
|
15
|
+
import Logger from '../logging/Logger.js';
|
|
16
|
+
|
|
17
|
+
export class GraphCache {
|
|
18
|
+
#cacheDir;
|
|
19
|
+
#logger;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {string} projectRoot 项目根目录
|
|
23
|
+
*/
|
|
24
|
+
constructor(projectRoot) {
|
|
25
|
+
this.#cacheDir = join(projectRoot, '.autosnippet', 'cache');
|
|
26
|
+
this.#logger = Logger.getInstance();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 保存缓存
|
|
31
|
+
* @param {string} key 缓存键名(生成 {key}.json)
|
|
32
|
+
* @param {object} data 要缓存的数据
|
|
33
|
+
* @param {object} meta 元信息(含 hash、timestamp 等)
|
|
34
|
+
*/
|
|
35
|
+
save(key, data, meta = {}) {
|
|
36
|
+
try {
|
|
37
|
+
if (!existsSync(this.#cacheDir)) {
|
|
38
|
+
mkdirSync(this.#cacheDir, { recursive: true });
|
|
39
|
+
}
|
|
40
|
+
const payload = {
|
|
41
|
+
version: 1,
|
|
42
|
+
savedAt: new Date().toISOString(),
|
|
43
|
+
...meta,
|
|
44
|
+
data,
|
|
45
|
+
};
|
|
46
|
+
const filePath = join(this.#cacheDir, `${key}.json`);
|
|
47
|
+
writeFileSync(filePath, JSON.stringify(payload), 'utf-8');
|
|
48
|
+
this.#logger.debug(`[GraphCache] saved: ${key} (${JSON.stringify(payload).length} bytes)`);
|
|
49
|
+
} catch (err) {
|
|
50
|
+
this.#logger.warn(`[GraphCache] save failed for ${key}: ${err.message}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 加载缓存
|
|
56
|
+
* @param {string} key 缓存键名
|
|
57
|
+
* @returns {{ data: object, [key: string]: any } | null}
|
|
58
|
+
*/
|
|
59
|
+
load(key) {
|
|
60
|
+
try {
|
|
61
|
+
const filePath = join(this.#cacheDir, `${key}.json`);
|
|
62
|
+
if (!existsSync(filePath)) return null;
|
|
63
|
+
const raw = readFileSync(filePath, 'utf-8');
|
|
64
|
+
return JSON.parse(raw);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
this.#logger.warn(`[GraphCache] load failed for ${key}: ${err.message}`);
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 检查缓存是否有效(hash 匹配)
|
|
73
|
+
* @param {string} key 缓存键
|
|
74
|
+
* @param {string} currentHash 当前内容的 hash
|
|
75
|
+
* @returns {boolean}
|
|
76
|
+
*/
|
|
77
|
+
isValid(key, currentHash) {
|
|
78
|
+
const cached = this.load(key);
|
|
79
|
+
if (!cached) return false;
|
|
80
|
+
return cached.contentHash === currentHash;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 删除缓存
|
|
85
|
+
* @param {string} key
|
|
86
|
+
*/
|
|
87
|
+
invalidate(key) {
|
|
88
|
+
try {
|
|
89
|
+
const filePath = join(this.#cacheDir, `${key}.json`);
|
|
90
|
+
if (existsSync(filePath)) {
|
|
91
|
+
unlinkSync(filePath);
|
|
92
|
+
this.#logger.debug(`[GraphCache] invalidated: ${key}`);
|
|
93
|
+
}
|
|
94
|
+
} catch (err) {
|
|
95
|
+
this.#logger.warn(`[GraphCache] invalidate failed for ${key}: ${err.message}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 计算文件内容 hash
|
|
101
|
+
* @param {string} filePath 文件绝对路径
|
|
102
|
+
* @returns {string} sha256 hex (前 16 字符)
|
|
103
|
+
*/
|
|
104
|
+
computeFileHash(filePath) {
|
|
105
|
+
try {
|
|
106
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
107
|
+
return this.computeContentHash(content);
|
|
108
|
+
} catch {
|
|
109
|
+
return '';
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 计算字符串内容 hash
|
|
115
|
+
* @param {string} content
|
|
116
|
+
* @returns {string} sha256 hex (前 16 字符)
|
|
117
|
+
*/
|
|
118
|
+
computeContentHash(content) {
|
|
119
|
+
return createHash('sha256').update(content).digest('hex').substring(0, 16);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 批量计算文件 hash 映射
|
|
124
|
+
* @param {string[]} filePaths 文件绝对路径数组
|
|
125
|
+
* @param {string} projectRoot 项目根目录
|
|
126
|
+
* @returns {Object<string, string>} { relativePath: hash }
|
|
127
|
+
*/
|
|
128
|
+
computeFileHashes(filePaths, projectRoot) {
|
|
129
|
+
const hashes = {};
|
|
130
|
+
for (const fp of filePaths) {
|
|
131
|
+
const rel = relative(projectRoot, fp);
|
|
132
|
+
hashes[rel] = this.computeFileHash(fp);
|
|
133
|
+
}
|
|
134
|
+
return hashes;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* 获取缓存目录路径
|
|
139
|
+
*/
|
|
140
|
+
getCacheDir() {
|
|
141
|
+
return this.#cacheDir;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration 015: Create token_usage table
|
|
3
|
+
*
|
|
4
|
+
* 持久化 AI 调用的 Token 消耗记录,支持近 7 日消耗趋势查询。
|
|
5
|
+
* 每次 ChatAgent.execute() 完成后写入一条记录。
|
|
6
|
+
*/
|
|
7
|
+
export default function migrate(db) {
|
|
8
|
+
db.exec(`
|
|
9
|
+
CREATE TABLE IF NOT EXISTS token_usage (
|
|
10
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
11
|
+
timestamp INTEGER NOT NULL,
|
|
12
|
+
source TEXT NOT NULL DEFAULT 'unknown',
|
|
13
|
+
dimension TEXT,
|
|
14
|
+
provider TEXT,
|
|
15
|
+
model TEXT,
|
|
16
|
+
input_tokens INTEGER NOT NULL DEFAULT 0,
|
|
17
|
+
output_tokens INTEGER NOT NULL DEFAULT 0,
|
|
18
|
+
total_tokens INTEGER NOT NULL DEFAULT 0,
|
|
19
|
+
duration_ms INTEGER,
|
|
20
|
+
tool_calls INTEGER DEFAULT 0,
|
|
21
|
+
session_id TEXT
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
CREATE INDEX IF NOT EXISTS idx_token_usage_timestamp ON token_usage(timestamp);
|
|
25
|
+
CREATE INDEX IF NOT EXISTS idx_token_usage_source ON token_usage(source);
|
|
26
|
+
`);
|
|
27
|
+
}
|