autosnippet 3.3.6 → 3.3.7
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/dashboard/dist/assets/icons-FHns2ypa.js +1 -0
- package/dashboard/dist/assets/index-BRJv5Y3r.js +135 -0
- package/dashboard/dist/assets/index-DzoB7kxK.css +1 -0
- package/dashboard/dist/index.html +3 -3
- package/dist/bin/cli.js +1 -0
- package/dist/lib/agent/AgentRuntime.d.ts +2 -2
- package/dist/lib/agent/AgentRuntime.js +26 -18
- package/dist/lib/agent/domain/ChatAgentTasks.js +4 -0
- package/dist/lib/agent/forced-summary.js +7 -2
- package/dist/lib/cli/AiScanService.js +4 -4
- package/dist/lib/core/discovery/ConfigWatcher.d.ts +64 -0
- package/dist/lib/core/discovery/ConfigWatcher.js +336 -0
- package/dist/lib/core/discovery/CustomConfigDiscoverer.d.ts +30 -0
- package/dist/lib/core/discovery/CustomConfigDiscoverer.js +1305 -0
- package/dist/lib/core/discovery/DiscovererPreference.d.ts +44 -0
- package/dist/lib/core/discovery/DiscovererPreference.js +141 -0
- package/dist/lib/core/discovery/DiscovererRegistry.d.ts +10 -1
- package/dist/lib/core/discovery/DiscovererRegistry.js +42 -2
- package/dist/lib/core/discovery/ProjectDiscoverer.d.ts +19 -0
- package/dist/lib/core/discovery/index.d.ts +2 -0
- package/dist/lib/core/discovery/index.js +4 -0
- package/dist/lib/core/discovery/parsers/CMakeParser.d.ts +32 -0
- package/dist/lib/core/discovery/parsers/CMakeParser.js +148 -0
- package/dist/lib/core/discovery/parsers/GradleDslParser.d.ts +43 -0
- package/dist/lib/core/discovery/parsers/GradleDslParser.js +171 -0
- package/dist/lib/core/discovery/parsers/JsonConfigParser.d.ts +45 -0
- package/dist/lib/core/discovery/parsers/JsonConfigParser.js +122 -0
- package/dist/lib/core/discovery/parsers/RubyDslParser.d.ts +49 -0
- package/dist/lib/core/discovery/parsers/RubyDslParser.js +282 -0
- package/dist/lib/core/discovery/parsers/StarlarkParser.d.ts +33 -0
- package/dist/lib/core/discovery/parsers/StarlarkParser.js +229 -0
- package/dist/lib/core/discovery/parsers/YamlConfigParser.d.ts +37 -0
- package/dist/lib/core/discovery/parsers/YamlConfigParser.js +212 -0
- package/dist/lib/domain/knowledge/KnowledgeEntry.d.ts +7 -1
- package/dist/lib/domain/knowledge/KnowledgeEntry.js +17 -3
- package/dist/lib/external/ai/AiProvider.d.ts +12 -0
- package/dist/lib/external/ai/AiProvider.js +24 -0
- package/dist/lib/external/ai/AiProviderManager.d.ts +101 -0
- package/dist/lib/external/ai/AiProviderManager.js +193 -0
- package/dist/lib/external/ai/providers/ClaudeProvider.js +11 -0
- package/dist/lib/external/ai/providers/GoogleGeminiProvider.js +18 -0
- package/dist/lib/external/ai/providers/MockProvider.d.ts +21 -3
- package/dist/lib/external/ai/providers/MockProvider.js +290 -14
- package/dist/lib/external/ai/providers/OpenAiProvider.js +16 -0
- package/dist/lib/external/lark/LarkTransport.d.ts +5 -1
- package/dist/lib/external/lark/LarkTransport.js +10 -2
- package/dist/lib/external/mcp/handlers/bootstrap/pipeline/mock-pipeline.d.ts +20 -0
- package/dist/lib/external/mcp/handlers/bootstrap/pipeline/mock-pipeline.js +432 -0
- package/dist/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +16 -8
- package/dist/lib/external/mcp/handlers/bootstrap/refine.js +8 -0
- package/dist/lib/external/mcp/handlers/bootstrap-external.d.ts +9 -0
- package/dist/lib/external/mcp/handlers/bootstrap-external.js +2 -0
- package/dist/lib/external/mcp/handlers/bootstrap-internal.js +2 -0
- package/dist/lib/external/mcp/handlers/consolidated.js +2 -1
- package/dist/lib/external/mcp/handlers/dimension-complete-external.js +2 -1
- package/dist/lib/external/mcp/handlers/evolve-external.js +5 -2
- package/dist/lib/external/mcp/handlers/knowledge.js +5 -4
- package/dist/lib/http/routes/ai.js +111 -30
- package/dist/lib/http/routes/candidates.js +11 -4
- package/dist/lib/http/routes/commands.js +10 -1
- package/dist/lib/http/routes/health.js +11 -0
- package/dist/lib/http/routes/modules.js +27 -0
- package/dist/lib/http/routes/recipes.js +7 -0
- package/dist/lib/http/utils/routeHelpers.js +2 -1
- package/dist/lib/injection/ServiceContainer.d.ts +6 -5
- package/dist/lib/injection/ServiceContainer.js +11 -27
- package/dist/lib/injection/ServiceMap.d.ts +2 -0
- package/dist/lib/injection/modules/AiModule.d.ts +6 -9
- package/dist/lib/injection/modules/AiModule.js +82 -39
- package/dist/lib/injection/modules/PanoramaModule.js +1 -1
- package/dist/lib/service/cleanup/CleanupService.d.ts +54 -7
- package/dist/lib/service/cleanup/CleanupService.js +284 -37
- package/dist/lib/service/knowledge/CodeEntityGraph.d.ts +6 -0
- package/dist/lib/service/knowledge/CodeEntityGraph.js +16 -0
- package/dist/lib/service/knowledge/KnowledgeService.js +23 -10
- package/dist/lib/service/module/ModuleService.js +10 -19
- package/dist/lib/service/panorama/CouplingAnalyzer.d.ts +10 -1
- package/dist/lib/service/panorama/CouplingAnalyzer.js +44 -2
- package/dist/lib/service/panorama/DimensionAnalyzer.d.ts +1 -1
- package/dist/lib/service/panorama/DimensionAnalyzer.js +31 -17
- package/dist/lib/service/panorama/LayerInferrer.d.ts +16 -1
- package/dist/lib/service/panorama/LayerInferrer.js +118 -1
- package/dist/lib/service/panorama/ModuleDiscoverer.d.ts +9 -0
- package/dist/lib/service/panorama/ModuleDiscoverer.js +58 -2
- package/dist/lib/service/panorama/PanoramaAggregator.d.ts +6 -2
- package/dist/lib/service/panorama/PanoramaAggregator.js +84 -6
- package/dist/lib/service/panorama/PanoramaScanner.js +28 -0
- package/dist/lib/service/panorama/PanoramaService.js +10 -5
- package/dist/lib/service/panorama/PanoramaTypes.d.ts +38 -0
- package/dist/lib/service/panorama/RoleRefiner.d.ts +2 -0
- package/dist/lib/service/panorama/RoleRefiner.js +41 -0
- package/dist/lib/service/panorama/TechStackProfiler.d.ts +13 -0
- package/dist/lib/service/panorama/TechStackProfiler.js +191 -0
- package/dist/lib/service/skills/SignalCollector.d.ts +1 -0
- package/dist/lib/service/skills/SignalCollector.js +6 -5
- package/dist/lib/service/vector/ContextualEnricher.d.ts +1 -0
- package/dist/lib/service/vector/ContextualEnricher.js +4 -0
- package/dist/lib/shared/LanguageService.js +3 -0
- package/dist/lib/shared/developer-identity.d.ts +18 -0
- package/dist/lib/shared/developer-identity.js +62 -0
- package/dist/lib/shared/schemas/http-requests.d.ts +8 -17
- package/dist/lib/shared/schemas/http-requests.js +9 -6
- package/dist/lib/types/knowledge-wire.d.ts +1 -0
- package/package.json +1 -1
- package/dashboard/dist/assets/icons-D1aVZYFW.js +0 -1
- package/dashboard/dist/assets/index-CxHOu8Hd.css +0 -1
- package/dashboard/dist/assets/index-DDdAOpYT.js +0 -128
|
@@ -6,17 +6,20 @@
|
|
|
6
6
|
*
|
|
7
7
|
* 职责:
|
|
8
8
|
* - AI Provider 自动探测与创建
|
|
9
|
+
* - AiProviderManager 统一管理层
|
|
9
10
|
* - Embedding fallback provider 管理
|
|
10
11
|
* - AiFactory 实例注入
|
|
11
12
|
*
|
|
12
13
|
* @module AiModule
|
|
13
14
|
*/
|
|
15
|
+
import { AiProviderManager } from '../../external/ai/AiProviderManager.js';
|
|
14
16
|
/**
|
|
15
17
|
* 初始化 AI Provider(在模块注册前调用)
|
|
16
18
|
*
|
|
17
19
|
* 1. 动态导入 AiFactory
|
|
18
20
|
* 2. 自动探测可用 AI Provider
|
|
19
|
-
* 3. 创建
|
|
21
|
+
* 3. 创建 AiProviderManager(统一管理层)
|
|
22
|
+
* 4. 绑定 Token 追踪、Embedding fallback、DI 级联清理
|
|
20
23
|
*/
|
|
21
24
|
export async function initialize(c) {
|
|
22
25
|
const logger = c.logger;
|
|
@@ -43,56 +46,96 @@ export async function initialize(c) {
|
|
|
43
46
|
c.singletons.aiProvider = null;
|
|
44
47
|
}
|
|
45
48
|
}
|
|
46
|
-
//
|
|
47
|
-
|
|
49
|
+
// ── 创建 AiProviderManager(统一管理层)──
|
|
50
|
+
const manager = new AiProviderManager(c.singletons.aiProvider || { name: 'mock', model: 'mock-fallback' });
|
|
51
|
+
c.singletons._aiProviderManager = manager;
|
|
52
|
+
// 绑定: DI 数据管道同步(切换时更新 singletons 中的 provider 引用,供工厂函数读取)
|
|
53
|
+
manager._bindDiSync((provider, embed) => {
|
|
54
|
+
c.singletons.aiProvider = provider;
|
|
55
|
+
c.singletons._embedProvider = embed;
|
|
56
|
+
});
|
|
57
|
+
// 绑定: DI 级联清理回调
|
|
58
|
+
manager._bindDependentClearer(() => {
|
|
59
|
+
const cleared = [];
|
|
60
|
+
for (const key of c._aiDependentSingletons || []) {
|
|
61
|
+
if (c.singletons[key]) {
|
|
62
|
+
c.singletons[key] = null;
|
|
63
|
+
cleared.push(key);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return cleared;
|
|
67
|
+
});
|
|
68
|
+
// 绑定: Embedding fallback 初始化器
|
|
69
|
+
manager._bindEmbedFallbackInit((currentProvider) => {
|
|
70
|
+
return createEmbedFallback(c, currentProvider);
|
|
71
|
+
});
|
|
72
|
+
// Token 追踪 AOP(manager 自身已在构造时 wire,此处延迟注入 recorder)
|
|
73
|
+
// recorder 注入放到 register() 之后(tokenUsageStore 需先注册)
|
|
74
|
+
// Embedding fallback: manager 的 embedFallbackInit 回调已绑定,初始化时主动触发一次
|
|
75
|
+
const initialEmbed = createEmbedFallback(c, c.singletons.aiProvider);
|
|
76
|
+
if (initialEmbed) {
|
|
77
|
+
manager.setEmbedProvider(initialEmbed);
|
|
78
|
+
c.singletons._embedProvider = initialEmbed;
|
|
79
|
+
}
|
|
48
80
|
}
|
|
49
81
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* 若主 provider 不支持 embedding(如 Claude),尝试从其他可用 provider 创建备用。
|
|
82
|
+
* 纯函数: 尝试为给定 provider 创建 Embedding fallback
|
|
83
|
+
* 被 initEmbeddingFallback() 和 AiProviderManager 的 embedFallbackInit 回调共用
|
|
53
84
|
*/
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
c.logger.info('Embedding fallback provider created', { provider: fb });
|
|
74
|
-
break;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
catch {
|
|
78
|
-
/* skip */
|
|
85
|
+
function createEmbedFallback(c, currentProvider) {
|
|
86
|
+
if (!currentProvider ||
|
|
87
|
+
(typeof currentProvider.supportsEmbedding === 'function' && currentProvider.supportsEmbedding())) {
|
|
88
|
+
return null; // 主 provider 已支持 embedding,无需 fallback
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
const aiFactory = (c.singletons._aiFactory || {});
|
|
92
|
+
const providerName = (currentProvider.name || '').replace('-', '');
|
|
93
|
+
const fbCandidates = typeof aiFactory.getAvailableFallbacks === 'function'
|
|
94
|
+
? aiFactory.getAvailableFallbacks(providerName)
|
|
95
|
+
: [];
|
|
96
|
+
for (const fb of fbCandidates) {
|
|
97
|
+
try {
|
|
98
|
+
const fbProvider = aiFactory.createProvider?.({ provider: fb });
|
|
99
|
+
if (fbProvider &&
|
|
100
|
+
typeof fbProvider.supportsEmbedding === 'function' &&
|
|
101
|
+
fbProvider.supportsEmbedding()) {
|
|
102
|
+
c.logger.info('Embedding fallback provider created', { provider: fb });
|
|
103
|
+
return fbProvider;
|
|
79
104
|
}
|
|
80
105
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
106
|
+
catch {
|
|
107
|
+
/* skip */
|
|
108
|
+
}
|
|
84
109
|
}
|
|
85
110
|
}
|
|
111
|
+
catch {
|
|
112
|
+
/* no embed fallback available */
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
86
115
|
}
|
|
87
116
|
/**
|
|
88
117
|
* 注册 AI 相关的服务到容器
|
|
89
118
|
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
119
|
+
* - 标记 AI 模块就绪
|
|
120
|
+
* - 注册 aiProviderManager 服务
|
|
121
|
+
* - 延迟注入 TokenRecorder(tokenUsageStore 此时已可用)
|
|
92
122
|
*/
|
|
93
123
|
export function register(c) {
|
|
94
|
-
// aiProvider 和 _aiFactory 已通过 initialize() 写入 singletons
|
|
95
|
-
// KnowledgeModule 中已注册 'aiProvider' 的 register 工厂
|
|
96
|
-
// 此处仅标记 AI 模块已就绪
|
|
97
124
|
c.singletons._aiModuleReady = true;
|
|
125
|
+
// 注册 aiProviderManager(消费者通过 container.get('aiProviderManager') 获取)
|
|
126
|
+
c.register('aiProviderManager', () => c.singletons._aiProviderManager);
|
|
127
|
+
// 延迟注入 TokenRecorder 到 manager(tokenUsageStore 在 AppModule 中注册)
|
|
128
|
+
const manager = c.singletons._aiProviderManager;
|
|
129
|
+
const containerRef = c;
|
|
130
|
+
manager.setTokenRecorder({
|
|
131
|
+
record(r) {
|
|
132
|
+
try {
|
|
133
|
+
const store = containerRef.get('tokenUsageStore');
|
|
134
|
+
store.record(r);
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
/* tokenUsageStore not available yet */
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
});
|
|
98
141
|
}
|
|
@@ -32,7 +32,7 @@ export const PanoramaModule = {
|
|
|
32
32
|
ct.singleton('roleRefiner', () => new RoleRefiner(getDb(), getProjectRoot()));
|
|
33
33
|
ct.singleton('couplingAnalyzer', () => new CouplingAnalyzer(getDb(), getProjectRoot()));
|
|
34
34
|
ct.singleton('layerInferrer', () => new LayerInferrer());
|
|
35
|
-
ct.singleton('dimensionAnalyzer', () => new DimensionAnalyzer(getDb()));
|
|
35
|
+
ct.singleton('dimensionAnalyzer', () => new DimensionAnalyzer(getDb(), getProjectRoot()));
|
|
36
36
|
ct.singleton('panoramaAggregator', (c) => {
|
|
37
37
|
const sc = c;
|
|
38
38
|
const roleRefiner = sc.get('roleRefiner');
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CleanupService —
|
|
2
|
+
* CleanupService — 统一数据清理策略(垃圾桶模式)
|
|
3
3
|
*
|
|
4
4
|
* 提供两种清理模式:
|
|
5
|
-
* - fullReset():
|
|
6
|
-
* - rescanClean(): Rescan
|
|
5
|
+
* - fullReset(): 全量清理 — 将旧数据打包到时间戳垃圾桶文件夹,DB 表清空
|
|
6
|
+
* - rescanClean(): Rescan 清理 — 保留 Recipe,清除衍生缓存
|
|
7
7
|
* - snapshotRecipes(): 快照当前活跃 Recipe 信息
|
|
8
|
+
* - purgeExpiredTrash(): 清除超时限的垃圾桶文件夹
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
+
* 垃圾桶设计:
|
|
11
|
+
* - 位于 .autosnippet/.trash/<ISO-timestamp>/ 下
|
|
12
|
+
* - fullReset 时先将 candidates/ recipes/ skills/ wiki/ 移入垃圾桶,再清 DB
|
|
13
|
+
* - DB 数据导出为 db-snapshot.jsonl 保存在垃圾桶内
|
|
14
|
+
* - 超过保留天数(默认 7 天)的垃圾桶在下次 fullReset 或服务启动时自动清除
|
|
15
|
+
* - 暂不提供恢复功能(需要 merge 处理过于复杂)
|
|
16
|
+
*
|
|
17
|
+
* 保留原则:
|
|
10
18
|
* - 配置数据 (config.json, constitution.yaml, boxspec.json) 永不清理
|
|
11
19
|
* - IDE 集成配置 (.vscode/, .cursor/, .github/) 永不清理
|
|
12
20
|
* - 交付物 (.cursor/rules/autosnippet-*) 由 R4 重建,不在此清理
|
|
@@ -24,6 +32,22 @@ export interface CleanupResult {
|
|
|
24
32
|
clearedTables: string[];
|
|
25
33
|
preservedRecipes: number;
|
|
26
34
|
errors: string[];
|
|
35
|
+
/** 垃圾桶信息(fullReset 时填充) */
|
|
36
|
+
trash?: {
|
|
37
|
+
/** 垃圾桶文件夹路径 */
|
|
38
|
+
folder: string;
|
|
39
|
+
/** 移入垃圾桶的文件/目录数 */
|
|
40
|
+
movedItems: number;
|
|
41
|
+
/** DB 快照行数 */
|
|
42
|
+
dbSnapshotRows: number;
|
|
43
|
+
};
|
|
44
|
+
/** 本次清除的过期垃圾桶 */
|
|
45
|
+
purgedTrash?: {
|
|
46
|
+
/** 清除的垃圾桶数 */
|
|
47
|
+
count: number;
|
|
48
|
+
/** 释放的磁盘空间估算 (bytes) */
|
|
49
|
+
freedBytes: number;
|
|
50
|
+
};
|
|
27
51
|
}
|
|
28
52
|
/** Recipe 快照条目 */
|
|
29
53
|
export interface RecipeSnapshotEntry {
|
|
@@ -60,10 +84,16 @@ export declare class CleanupService {
|
|
|
60
84
|
/** 更新 DB 引用(fullReset 后重连时调用) */
|
|
61
85
|
setDb(db: unknown): void;
|
|
62
86
|
/**
|
|
63
|
-
* 全量清理 — 用于 bootstrap
|
|
87
|
+
* 全量清理 — 用于 bootstrap 冷启动(垃圾桶模式)
|
|
88
|
+
*
|
|
89
|
+
* 流程:
|
|
90
|
+
* 1. 先清除过期垃圾桶(超过 TRASH_RETENTION_DAYS)
|
|
91
|
+
* 2. 创建时间戳垃圾桶文件夹
|
|
92
|
+
* 3. 将 candidates/ recipes/ skills/ wiki/ 移入垃圾桶
|
|
93
|
+
* 4. 导出 DB 关键表数据到 db-snapshot.jsonl
|
|
94
|
+
* 5. 清空 DB 所有数据表
|
|
95
|
+
* 6. 清除向量索引、bootstrap-report、logs 等缓存
|
|
64
96
|
*
|
|
65
|
-
* 清除: DB 所有数据表、candidates/、recipes/、skills/、wiki/、
|
|
66
|
-
* 向量索引、bootstrap-report.json、logs/signals/
|
|
67
97
|
* 保留: config.json、constitution.yaml、boxspec.json、IDE 配置
|
|
68
98
|
*/
|
|
69
99
|
fullReset(): Promise<CleanupResult>;
|
|
@@ -81,5 +111,22 @@ export declare class CleanupService {
|
|
|
81
111
|
* 用于 rescan 前记录保留的知识条目
|
|
82
112
|
*/
|
|
83
113
|
snapshotRecipes(): Promise<RecipeSnapshot>;
|
|
114
|
+
/**
|
|
115
|
+
* 清除超过保留期限的垃圾桶文件夹
|
|
116
|
+
* 可在服务启动时或 fullReset 前调用
|
|
117
|
+
*/
|
|
118
|
+
purgeExpiredTrash(): {
|
|
119
|
+
count: number;
|
|
120
|
+
freedBytes: number;
|
|
121
|
+
folders: string[];
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* 列出当前所有垃圾桶(供 Dashboard 展示)
|
|
125
|
+
*/
|
|
126
|
+
listTrashFolders(): Array<{
|
|
127
|
+
name: string;
|
|
128
|
+
createdAt: Date;
|
|
129
|
+
sizeMB: number;
|
|
130
|
+
}>;
|
|
84
131
|
}
|
|
85
132
|
export {};
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CleanupService —
|
|
2
|
+
* CleanupService — 统一数据清理策略(垃圾桶模式)
|
|
3
3
|
*
|
|
4
4
|
* 提供两种清理模式:
|
|
5
|
-
* - fullReset():
|
|
6
|
-
* - rescanClean(): Rescan
|
|
5
|
+
* - fullReset(): 全量清理 — 将旧数据打包到时间戳垃圾桶文件夹,DB 表清空
|
|
6
|
+
* - rescanClean(): Rescan 清理 — 保留 Recipe,清除衍生缓存
|
|
7
7
|
* - snapshotRecipes(): 快照当前活跃 Recipe 信息
|
|
8
|
+
* - purgeExpiredTrash(): 清除超时限的垃圾桶文件夹
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
+
* 垃圾桶设计:
|
|
11
|
+
* - 位于 .autosnippet/.trash/<ISO-timestamp>/ 下
|
|
12
|
+
* - fullReset 时先将 candidates/ recipes/ skills/ wiki/ 移入垃圾桶,再清 DB
|
|
13
|
+
* - DB 数据导出为 db-snapshot.jsonl 保存在垃圾桶内
|
|
14
|
+
* - 超过保留天数(默认 7 天)的垃圾桶在下次 fullReset 或服务启动时自动清除
|
|
15
|
+
* - 暂不提供恢复功能(需要 merge 处理过于复杂)
|
|
16
|
+
*
|
|
17
|
+
* 保留原则:
|
|
10
18
|
* - 配置数据 (config.json, constitution.yaml, boxspec.json) 永不清理
|
|
11
19
|
* - IDE 集成配置 (.vscode/, .cursor/, .github/) 永不清理
|
|
12
20
|
* - 交付物 (.cursor/rules/autosnippet-*) 由 R4 重建,不在此清理
|
|
@@ -18,36 +26,52 @@ import path from 'node:path';
|
|
|
18
26
|
import { CANDIDATES_DIR } from '#infra/config/Defaults.js';
|
|
19
27
|
import { getContextIndexPath, getProjectKnowledgePath, getProjectRecipesPath, getProjectSkillsPath, } from '#infra/config/Paths.js';
|
|
20
28
|
// ── 常量 ────────────────────────────────────────────────────
|
|
21
|
-
/**
|
|
29
|
+
/** 垃圾桶根目录(相对于 .autosnippet/) */
|
|
30
|
+
const TRASH_DIR = '.trash';
|
|
31
|
+
/** 垃圾桶保留天数,超过后自动 purge */
|
|
32
|
+
const TRASH_RETENTION_DAYS = 7;
|
|
33
|
+
/** DB 快照文件名 */
|
|
34
|
+
const DB_SNAPSHOT_FILE = 'db-snapshot.jsonl';
|
|
35
|
+
/**
|
|
36
|
+
* fullReset 时清除的所有 DB 表(不含 schema_migrations)
|
|
37
|
+
*
|
|
38
|
+
* ⚠️ 顺序重要:子表必须排在父表之前,否则 FK 约束会阻止 DELETE。
|
|
39
|
+
* lifecycle_transition_events → knowledge_entries, evolution_proposals
|
|
40
|
+
* evolution_proposals → knowledge_entries
|
|
41
|
+
* recipe_source_refs → knowledge_entries (CASCADE)
|
|
42
|
+
* bootstrap_dim_files → bootstrap_snapshots (CASCADE)
|
|
43
|
+
*/
|
|
22
44
|
const ALL_DATA_TABLES = [
|
|
23
|
-
|
|
45
|
+
// ── FK 子表先删 ──
|
|
46
|
+
'lifecycle_transition_events',
|
|
47
|
+
'recipe_source_refs',
|
|
48
|
+
'evolution_proposals',
|
|
24
49
|
'knowledge_edges',
|
|
50
|
+
'bootstrap_dim_files',
|
|
51
|
+
// ── 父表后删 ──
|
|
52
|
+
'knowledge_entries',
|
|
53
|
+
'bootstrap_snapshots',
|
|
54
|
+
// ── 无 FK 依赖 ──
|
|
25
55
|
'guard_violations',
|
|
26
56
|
'audit_logs',
|
|
27
57
|
'sessions',
|
|
28
|
-
'token_usage',
|
|
29
58
|
'semantic_memories',
|
|
30
|
-
'bootstrap_snapshots',
|
|
31
|
-
'bootstrap_dim_files',
|
|
32
59
|
'code_entities',
|
|
33
60
|
'remote_commands',
|
|
34
61
|
'remote_state',
|
|
35
|
-
'evolution_proposals',
|
|
36
|
-
'recipe_source_refs',
|
|
37
62
|
];
|
|
38
63
|
/** rescanClean 时清除的 DB 表(保留知识/进化相关表) */
|
|
39
64
|
const RESCAN_CLEAN_TABLES = [
|
|
65
|
+
'bootstrap_dim_files', // FK → bootstrap_snapshots, 先删
|
|
66
|
+
'recipe_source_refs', // FK → knowledge_entries, 先删
|
|
67
|
+
'bootstrap_snapshots',
|
|
40
68
|
'code_entities',
|
|
41
69
|
'guard_violations',
|
|
42
|
-
'bootstrap_snapshots',
|
|
43
|
-
'bootstrap_dim_files',
|
|
44
70
|
'semantic_memories',
|
|
45
71
|
'sessions',
|
|
46
72
|
'audit_logs',
|
|
47
|
-
'token_usage',
|
|
48
73
|
'remote_commands',
|
|
49
74
|
'remote_state',
|
|
50
|
-
'recipe_source_refs',
|
|
51
75
|
];
|
|
52
76
|
// ── CleanupService ──────────────────────────────────────────
|
|
53
77
|
export class CleanupService {
|
|
@@ -71,12 +95,18 @@ export class CleanupService {
|
|
|
71
95
|
: db
|
|
72
96
|
: null;
|
|
73
97
|
}
|
|
74
|
-
// ─── 需求 A
|
|
98
|
+
// ─── 需求 A:全量清理(垃圾桶模式) ────────────────────
|
|
75
99
|
/**
|
|
76
|
-
* 全量清理 — 用于 bootstrap
|
|
100
|
+
* 全量清理 — 用于 bootstrap 冷启动(垃圾桶模式)
|
|
101
|
+
*
|
|
102
|
+
* 流程:
|
|
103
|
+
* 1. 先清除过期垃圾桶(超过 TRASH_RETENTION_DAYS)
|
|
104
|
+
* 2. 创建时间戳垃圾桶文件夹
|
|
105
|
+
* 3. 将 candidates/ recipes/ skills/ wiki/ 移入垃圾桶
|
|
106
|
+
* 4. 导出 DB 关键表数据到 db-snapshot.jsonl
|
|
107
|
+
* 5. 清空 DB 所有数据表
|
|
108
|
+
* 6. 清除向量索引、bootstrap-report、logs 等缓存
|
|
77
109
|
*
|
|
78
|
-
* 清除: DB 所有数据表、candidates/、recipes/、skills/、wiki/、
|
|
79
|
-
* 向量索引、bootstrap-report.json、logs/signals/
|
|
80
110
|
* 保留: config.json、constitution.yaml、boxspec.json、IDE 配置
|
|
81
111
|
*/
|
|
82
112
|
async fullReset() {
|
|
@@ -86,8 +116,34 @@ export class CleanupService {
|
|
|
86
116
|
preservedRecipes: 0,
|
|
87
117
|
errors: [],
|
|
88
118
|
};
|
|
89
|
-
this.#logger.info('[CleanupService] Starting fullReset...');
|
|
90
|
-
//
|
|
119
|
+
this.#logger.info('[CleanupService] Starting fullReset (trash-bin mode)...');
|
|
120
|
+
// 0. 清除过期垃圾桶
|
|
121
|
+
const purged = this.#purgeExpiredTrash();
|
|
122
|
+
if (purged.count > 0) {
|
|
123
|
+
result.purgedTrash = purged;
|
|
124
|
+
this.#logger.info(`[CleanupService] Purged ${purged.count} expired trash folders`);
|
|
125
|
+
}
|
|
126
|
+
// 1. 创建时间戳垃圾桶文件夹
|
|
127
|
+
const trashFolder = this.#createTrashFolder();
|
|
128
|
+
let movedItems = 0;
|
|
129
|
+
let dbSnapshotRows = 0;
|
|
130
|
+
// 2. 将知识目录移入垃圾桶(move 而非 copy,速度快)
|
|
131
|
+
const kbPath = getProjectKnowledgePath(this.#projectRoot);
|
|
132
|
+
const dirsToTrash = [
|
|
133
|
+
{ src: path.join(this.#projectRoot, CANDIDATES_DIR), name: 'candidates' },
|
|
134
|
+
{ src: getProjectRecipesPath(this.#projectRoot), name: 'recipes' },
|
|
135
|
+
{ src: getProjectSkillsPath(this.#projectRoot), name: 'skills' },
|
|
136
|
+
{ src: path.join(kbPath, 'wiki'), name: 'wiki' },
|
|
137
|
+
];
|
|
138
|
+
for (const { src, name } of dirsToTrash) {
|
|
139
|
+
const moved = this.#moveToTrash(src, path.join(trashFolder, name));
|
|
140
|
+
movedItems += moved;
|
|
141
|
+
}
|
|
142
|
+
// 3. 导出 DB 数据到垃圾桶(JSONL 格式,每行一个 {table, row})
|
|
143
|
+
if (this.#db) {
|
|
144
|
+
dbSnapshotRows = this.#exportDbToTrash(trashFolder);
|
|
145
|
+
}
|
|
146
|
+
// 4. 清空 DB 所有数据表
|
|
91
147
|
if (this.#db) {
|
|
92
148
|
for (const table of ALL_DATA_TABLES) {
|
|
93
149
|
try {
|
|
@@ -96,14 +152,14 @@ export class CleanupService {
|
|
|
96
152
|
}
|
|
97
153
|
catch (err) {
|
|
98
154
|
const msg = err instanceof Error ? err.message : String(err);
|
|
99
|
-
// 表可能不存在(未 migrate),跳过
|
|
100
155
|
if (!msg.includes('no such table')) {
|
|
101
156
|
result.errors.push(`Failed to clear ${table}: ${msg}`);
|
|
157
|
+
this.#logger.warn(`[CleanupService] DELETE FROM ${table} failed: ${msg}`);
|
|
102
158
|
}
|
|
103
159
|
}
|
|
104
160
|
}
|
|
105
|
-
//
|
|
106
|
-
for (const table of ['
|
|
161
|
+
// tasks 相关表(来自 migration 002,需先删子表)
|
|
162
|
+
for (const table of ['task_events', 'task_dependencies', 'tasks']) {
|
|
107
163
|
try {
|
|
108
164
|
this.#db.exec(`DELETE FROM ${table}`);
|
|
109
165
|
result.clearedTables.push(table);
|
|
@@ -113,23 +169,30 @@ export class CleanupService {
|
|
|
113
169
|
}
|
|
114
170
|
}
|
|
115
171
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
172
|
+
else {
|
|
173
|
+
this.#logger.warn('[CleanupService] No database reference — DB tables NOT cleared!');
|
|
174
|
+
result.errors.push('DB reference is null, database tables were not cleared');
|
|
175
|
+
}
|
|
176
|
+
// 5. 重建被移走的空目录(bootstrap 后续步骤需要)
|
|
177
|
+
for (const { src } of dirsToTrash) {
|
|
178
|
+
if (!fs.existsSync(src)) {
|
|
179
|
+
fs.mkdirSync(src, { recursive: true });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// 6. 清除向量索引
|
|
125
183
|
result.deletedFiles += this.#clearDirectory(getContextIndexPath(this.#projectRoot));
|
|
126
184
|
// 7. 删除 bootstrap-report.json
|
|
127
|
-
result.deletedFiles += this.#deleteFile(path.join(
|
|
185
|
+
result.deletedFiles += this.#deleteFile(path.join(kbPath, '.autosnippet', 'bootstrap-report.json'));
|
|
128
186
|
// 8. 清除 logs/signals/
|
|
129
|
-
result.deletedFiles += this.#clearDirectory(path.join(
|
|
130
|
-
|
|
187
|
+
result.deletedFiles += this.#clearDirectory(path.join(kbPath, '.autosnippet', 'logs', 'signals'));
|
|
188
|
+
result.deletedFiles += movedItems;
|
|
189
|
+
result.trash = { folder: trashFolder, movedItems, dbSnapshotRows };
|
|
190
|
+
this.#logger.info('[CleanupService] fullReset complete (trash-bin mode)', {
|
|
191
|
+
trashFolder: path.basename(trashFolder),
|
|
192
|
+
movedItems,
|
|
193
|
+
dbSnapshotRows,
|
|
131
194
|
tables: result.clearedTables.length,
|
|
132
|
-
|
|
195
|
+
purgedExpired: purged.count,
|
|
133
196
|
errors: result.errors.length,
|
|
134
197
|
});
|
|
135
198
|
return result;
|
|
@@ -268,7 +331,191 @@ export class CleanupService {
|
|
|
268
331
|
return { count: 0, entries: [], coverageByDimension: {} };
|
|
269
332
|
}
|
|
270
333
|
}
|
|
334
|
+
// ─── 垃圾桶管理 ───────────────────────────────────────
|
|
335
|
+
/**
|
|
336
|
+
* 清除超过保留期限的垃圾桶文件夹
|
|
337
|
+
* 可在服务启动时或 fullReset 前调用
|
|
338
|
+
*/
|
|
339
|
+
purgeExpiredTrash() {
|
|
340
|
+
return this.#purgeExpiredTrash();
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* 列出当前所有垃圾桶(供 Dashboard 展示)
|
|
344
|
+
*/
|
|
345
|
+
listTrashFolders() {
|
|
346
|
+
const trashRoot = this.#getTrashRoot();
|
|
347
|
+
if (!fs.existsSync(trashRoot)) {
|
|
348
|
+
return [];
|
|
349
|
+
}
|
|
350
|
+
const entries = fs.readdirSync(trashRoot).sort().reverse();
|
|
351
|
+
return entries
|
|
352
|
+
.filter((name) => /^\d{4}-\d{2}-\d{2}T/.test(name))
|
|
353
|
+
.map((name) => {
|
|
354
|
+
const fullPath = path.join(trashRoot, name);
|
|
355
|
+
const stat = fs.statSync(fullPath);
|
|
356
|
+
return {
|
|
357
|
+
name,
|
|
358
|
+
createdAt: stat.birthtime,
|
|
359
|
+
sizeMB: Math.round((this.#getDirSize(fullPath) / 1024 / 1024) * 100) / 100,
|
|
360
|
+
};
|
|
361
|
+
});
|
|
362
|
+
}
|
|
271
363
|
// ─── 内部工具方法 ─────────────────────────────────────
|
|
364
|
+
/** 获取垃圾桶根目录 (.autosnippet/.trash/) */
|
|
365
|
+
#getTrashRoot() {
|
|
366
|
+
return path.join(this.#projectRoot, '.autosnippet', TRASH_DIR);
|
|
367
|
+
}
|
|
368
|
+
/** 创建时间戳垃圾桶文件夹,返回绝对路径 */
|
|
369
|
+
#createTrashFolder() {
|
|
370
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
371
|
+
const trashFolder = path.join(this.#getTrashRoot(), ts);
|
|
372
|
+
fs.mkdirSync(trashFolder, { recursive: true });
|
|
373
|
+
return trashFolder;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* 将源目录内容移入垃圾桶对应子目录
|
|
377
|
+
* 使用 rename 实现(同文件系统内是原子操作,速度极快)
|
|
378
|
+
* @returns 移动的顶层条目数
|
|
379
|
+
*/
|
|
380
|
+
#moveToTrash(srcDir, trashSubDir) {
|
|
381
|
+
if (!fs.existsSync(srcDir)) {
|
|
382
|
+
return 0;
|
|
383
|
+
}
|
|
384
|
+
const entries = fs.readdirSync(srcDir);
|
|
385
|
+
if (entries.length === 0) {
|
|
386
|
+
return 0;
|
|
387
|
+
}
|
|
388
|
+
fs.mkdirSync(trashSubDir, { recursive: true });
|
|
389
|
+
let count = 0;
|
|
390
|
+
for (const entry of entries) {
|
|
391
|
+
const src = path.join(srcDir, entry);
|
|
392
|
+
const dest = path.join(trashSubDir, entry);
|
|
393
|
+
try {
|
|
394
|
+
fs.renameSync(src, dest);
|
|
395
|
+
count++;
|
|
396
|
+
}
|
|
397
|
+
catch {
|
|
398
|
+
// rename 可能跨设备失败,fallback 到 copy+delete
|
|
399
|
+
try {
|
|
400
|
+
fs.cpSync(src, dest, { recursive: true });
|
|
401
|
+
fs.rmSync(src, { recursive: true, force: true });
|
|
402
|
+
count++;
|
|
403
|
+
}
|
|
404
|
+
catch (err) {
|
|
405
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
406
|
+
this.#logger.warn(`[CleanupService] Failed to move ${entry} to trash: ${msg}`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return count;
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* 导出 DB 关键表数据到垃圾桶(JSONL 格式)
|
|
414
|
+
* 只导出有实际业务数据的表,跳过纯缓存表
|
|
415
|
+
*/
|
|
416
|
+
#exportDbToTrash(trashFolder) {
|
|
417
|
+
if (!this.#db) {
|
|
418
|
+
return 0;
|
|
419
|
+
}
|
|
420
|
+
const tablesToExport = [
|
|
421
|
+
'knowledge_entries',
|
|
422
|
+
'knowledge_edges',
|
|
423
|
+
'lifecycle_transition_events',
|
|
424
|
+
'evolution_proposals',
|
|
425
|
+
'recipe_source_refs',
|
|
426
|
+
'guard_violations',
|
|
427
|
+
];
|
|
428
|
+
const snapshotPath = path.join(trashFolder, DB_SNAPSHOT_FILE);
|
|
429
|
+
let totalRows = 0;
|
|
430
|
+
const lines = [];
|
|
431
|
+
for (const table of tablesToExport) {
|
|
432
|
+
try {
|
|
433
|
+
const rows = this.#db.prepare(`SELECT * FROM ${table}`).all();
|
|
434
|
+
for (const row of rows) {
|
|
435
|
+
lines.push(JSON.stringify({ _table: table, ...row }));
|
|
436
|
+
totalRows++;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
catch {
|
|
440
|
+
// 表可能不存在,跳过
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
if (lines.length > 0) {
|
|
444
|
+
fs.writeFileSync(snapshotPath, `${lines.join('\n')}\n`, 'utf-8');
|
|
445
|
+
this.#logger.info(`[CleanupService] DB snapshot: ${totalRows} rows → ${DB_SNAPSHOT_FILE}`);
|
|
446
|
+
}
|
|
447
|
+
return totalRows;
|
|
448
|
+
}
|
|
449
|
+
/** 清除过期垃圾桶文件夹 */
|
|
450
|
+
#purgeExpiredTrash() {
|
|
451
|
+
const trashRoot = this.#getTrashRoot();
|
|
452
|
+
if (!fs.existsSync(trashRoot)) {
|
|
453
|
+
return { count: 0, freedBytes: 0, folders: [] };
|
|
454
|
+
}
|
|
455
|
+
const now = Date.now();
|
|
456
|
+
const maxAge = TRASH_RETENTION_DAYS * 24 * 60 * 60 * 1000;
|
|
457
|
+
const entries = fs.readdirSync(trashRoot);
|
|
458
|
+
let count = 0;
|
|
459
|
+
let freedBytes = 0;
|
|
460
|
+
const folders = [];
|
|
461
|
+
for (const entry of entries) {
|
|
462
|
+
const fullPath = path.join(trashRoot, entry);
|
|
463
|
+
try {
|
|
464
|
+
const stat = fs.statSync(fullPath);
|
|
465
|
+
if (!stat.isDirectory()) {
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
// 从文件夹名解析时间戳(格式: 2026-04-09T14-30-00-000Z)
|
|
469
|
+
const ts = entry.replace(/-(\d{2})-(\d{2})-(\d{3}Z)$/, ':$1:$2.$3');
|
|
470
|
+
const created = new Date(ts).getTime();
|
|
471
|
+
const age = now - (Number.isNaN(created) ? stat.birthtimeMs : created);
|
|
472
|
+
if (age > maxAge) {
|
|
473
|
+
const size = this.#getDirSize(fullPath);
|
|
474
|
+
fs.rmSync(fullPath, { recursive: true, force: true });
|
|
475
|
+
freedBytes += size;
|
|
476
|
+
count++;
|
|
477
|
+
folders.push(entry);
|
|
478
|
+
this.#logger.info(`[CleanupService] Purged expired trash: ${entry} (${Math.round(size / 1024)}KB)`);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
catch (err) {
|
|
482
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
483
|
+
this.#logger.warn(`[CleanupService] Failed to purge trash ${entry}: ${msg}`);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
// 如果垃圾桶根目录为空,也删掉
|
|
487
|
+
try {
|
|
488
|
+
const remaining = fs.readdirSync(trashRoot);
|
|
489
|
+
if (remaining.length === 0) {
|
|
490
|
+
fs.rmdirSync(trashRoot);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
catch {
|
|
494
|
+
/* ignore */
|
|
495
|
+
}
|
|
496
|
+
return { count, freedBytes, folders };
|
|
497
|
+
}
|
|
498
|
+
/** 递归计算目录大小 (bytes) */
|
|
499
|
+
#getDirSize(dirPath) {
|
|
500
|
+
let size = 0;
|
|
501
|
+
try {
|
|
502
|
+
const entries = fs.readdirSync(dirPath);
|
|
503
|
+
for (const entry of entries) {
|
|
504
|
+
const fullPath = path.join(dirPath, entry);
|
|
505
|
+
const stat = fs.statSync(fullPath);
|
|
506
|
+
if (stat.isDirectory()) {
|
|
507
|
+
size += this.#getDirSize(fullPath);
|
|
508
|
+
}
|
|
509
|
+
else {
|
|
510
|
+
size += stat.size;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
catch {
|
|
515
|
+
/* ignore */
|
|
516
|
+
}
|
|
517
|
+
return size;
|
|
518
|
+
}
|
|
272
519
|
/**
|
|
273
520
|
* 清空目录内容(保留目录本身)
|
|
274
521
|
* @returns 删除的文件数
|
|
@@ -85,6 +85,12 @@ interface DepGraphNode {
|
|
|
85
85
|
id?: string;
|
|
86
86
|
label?: string;
|
|
87
87
|
type?: string;
|
|
88
|
+
layer?: string;
|
|
89
|
+
version?: string;
|
|
90
|
+
group?: string;
|
|
91
|
+
fullPath?: string;
|
|
92
|
+
indirect?: boolean;
|
|
93
|
+
[key: string]: unknown;
|
|
88
94
|
}
|
|
89
95
|
interface DepGraphData {
|
|
90
96
|
nodes?: (DepGraphNode | string)[];
|