tabby-ai-assistant 1.0.5 → 1.0.6
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/dist/components/chat/ai-sidebar.component.d.ts +147 -0
- package/dist/components/chat/chat-interface.component.d.ts +38 -6
- package/dist/components/settings/general-settings.component.d.ts +6 -3
- package/dist/components/settings/provider-config.component.d.ts +25 -12
- package/dist/components/terminal/command-preview.component.d.ts +38 -0
- package/dist/index-full.d.ts +8 -0
- package/dist/index-minimal.d.ts +3 -0
- package/dist/index.d.ts +7 -3
- package/dist/index.js +1 -2
- package/dist/providers/tabby/ai-config.provider.d.ts +57 -5
- package/dist/providers/tabby/ai-hotkey.provider.d.ts +8 -14
- package/dist/providers/tabby/ai-toolbar-button.provider.d.ts +8 -9
- package/dist/services/chat/ai-sidebar.service.d.ts +89 -0
- package/dist/services/chat/chat-history.service.d.ts +78 -0
- package/dist/services/chat/chat-session.service.d.ts +57 -2
- package/dist/services/context/compaction.d.ts +90 -0
- package/dist/services/context/manager.d.ts +69 -0
- package/dist/services/context/memory.d.ts +116 -0
- package/dist/services/context/token-budget.d.ts +105 -0
- package/dist/services/core/ai-assistant.service.d.ts +40 -1
- package/dist/services/core/checkpoint.service.d.ts +130 -0
- package/dist/services/platform/escape-sequence.service.d.ts +132 -0
- package/dist/services/platform/platform-detection.service.d.ts +146 -0
- package/dist/services/providers/anthropic-provider.service.d.ts +5 -0
- package/dist/services/providers/base-provider.service.d.ts +6 -1
- package/dist/services/providers/glm-provider.service.d.ts +5 -0
- package/dist/services/providers/minimax-provider.service.d.ts +10 -1
- package/dist/services/providers/ollama-provider.service.d.ts +76 -0
- package/dist/services/providers/openai-compatible.service.d.ts +5 -0
- package/dist/services/providers/openai-provider.service.d.ts +5 -0
- package/dist/services/providers/vllm-provider.service.d.ts +82 -0
- package/dist/services/terminal/buffer-analyzer.service.d.ts +128 -0
- package/dist/services/terminal/terminal-manager.service.d.ts +185 -0
- package/dist/services/terminal/terminal-tools.service.d.ts +79 -0
- package/dist/types/ai.types.d.ts +92 -0
- package/dist/types/provider.types.d.ts +1 -1
- package/package.json +7 -10
- package/src/components/chat/ai-sidebar.component.ts +945 -0
- package/src/components/chat/chat-input.component.html +9 -24
- package/src/components/chat/chat-input.component.scss +3 -2
- package/src/components/chat/chat-interface.component.html +77 -69
- package/src/components/chat/chat-interface.component.scss +54 -4
- package/src/components/chat/chat-interface.component.ts +250 -34
- package/src/components/chat/chat-settings.component.scss +4 -4
- package/src/components/chat/chat-settings.component.ts +22 -11
- package/src/components/common/error-message.component.html +15 -0
- package/src/components/common/error-message.component.scss +77 -0
- package/src/components/common/error-message.component.ts +2 -96
- package/src/components/common/loading-spinner.component.html +4 -0
- package/src/components/common/loading-spinner.component.scss +57 -0
- package/src/components/common/loading-spinner.component.ts +2 -63
- package/src/components/security/consent-dialog.component.html +22 -0
- package/src/components/security/consent-dialog.component.scss +34 -0
- package/src/components/security/consent-dialog.component.ts +2 -55
- package/src/components/security/password-prompt.component.html +19 -0
- package/src/components/security/password-prompt.component.scss +30 -0
- package/src/components/security/password-prompt.component.ts +2 -54
- package/src/components/security/risk-confirm-dialog.component.html +8 -12
- package/src/components/security/risk-confirm-dialog.component.scss +8 -5
- package/src/components/security/risk-confirm-dialog.component.ts +6 -6
- package/src/components/settings/ai-settings-tab.component.html +16 -20
- package/src/components/settings/ai-settings-tab.component.scss +8 -5
- package/src/components/settings/ai-settings-tab.component.ts +12 -12
- package/src/components/settings/general-settings.component.html +8 -17
- package/src/components/settings/general-settings.component.scss +6 -3
- package/src/components/settings/general-settings.component.ts +62 -22
- package/src/components/settings/provider-config.component.html +19 -39
- package/src/components/settings/provider-config.component.scss +182 -39
- package/src/components/settings/provider-config.component.ts +119 -7
- package/src/components/settings/security-settings.component.scss +1 -1
- package/src/components/terminal/ai-toolbar-button.component.html +8 -0
- package/src/components/terminal/ai-toolbar-button.component.scss +20 -0
- package/src/components/terminal/ai-toolbar-button.component.ts +2 -30
- package/src/components/terminal/command-preview.component.html +61 -0
- package/src/components/terminal/command-preview.component.scss +72 -0
- package/src/components/terminal/command-preview.component.ts +127 -140
- package/src/components/terminal/command-suggestion.component.html +23 -0
- package/src/components/terminal/command-suggestion.component.scss +55 -0
- package/src/components/terminal/command-suggestion.component.ts +2 -77
- package/src/index-minimal.ts +32 -0
- package/src/index.ts +94 -11
- package/src/index.ts.backup +165 -0
- package/src/providers/tabby/ai-config.provider.ts +60 -51
- package/src/providers/tabby/ai-hotkey.provider.ts +23 -39
- package/src/providers/tabby/ai-settings-tab.provider.ts +2 -2
- package/src/providers/tabby/ai-toolbar-button.provider.ts +29 -24
- package/src/services/chat/ai-sidebar.service.ts +258 -0
- package/src/services/chat/chat-history.service.ts +308 -0
- package/src/services/chat/chat-history.service.ts.backup +239 -0
- package/src/services/chat/chat-session.service.ts +276 -3
- package/src/services/context/compaction.ts +483 -0
- package/src/services/context/manager.ts +442 -0
- package/src/services/context/memory.ts +519 -0
- package/src/services/context/token-budget.ts +422 -0
- package/src/services/core/ai-assistant.service.ts +280 -5
- package/src/services/core/ai-provider-manager.service.ts +2 -2
- package/src/services/core/checkpoint.service.ts +619 -0
- package/src/services/platform/escape-sequence.service.ts +499 -0
- package/src/services/platform/platform-detection.service.ts +494 -0
- package/src/services/providers/anthropic-provider.service.ts +28 -1
- package/src/services/providers/base-provider.service.ts +7 -1
- package/src/services/providers/glm-provider.service.ts +28 -1
- package/src/services/providers/minimax-provider.service.ts +209 -11
- package/src/services/providers/ollama-provider.service.ts +445 -0
- package/src/services/providers/openai-compatible.service.ts +9 -0
- package/src/services/providers/openai-provider.service.ts +9 -0
- package/src/services/providers/vllm-provider.service.ts +463 -0
- package/src/services/security/risk-assessment.service.ts +6 -2
- package/src/services/terminal/buffer-analyzer.service.ts +594 -0
- package/src/services/terminal/terminal-manager.service.ts +748 -0
- package/src/services/terminal/terminal-tools.service.ts +441 -0
- package/src/styles/ai-assistant.scss +78 -6
- package/src/types/ai.types.ts +144 -0
- package/src/types/provider.types.ts +1 -1
- package/tsconfig.json +9 -9
- package/webpack.config.js +28 -6
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
import { Injectable } from '@angular/core';
|
|
2
|
+
import { BehaviorSubject, Observable } from 'rxjs';
|
|
3
|
+
import { LoggerService } from './logger.service';
|
|
4
|
+
import { ChatHistoryService } from '../chat/chat-history.service';
|
|
5
|
+
import { Checkpoint, ApiMessage } from '../../types/ai.types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 检查点状态
|
|
9
|
+
*/
|
|
10
|
+
export enum CheckpointStatus {
|
|
11
|
+
ACTIVE = 'active', // 活跃检查点
|
|
12
|
+
ARCHIVED = 'archived', // 已归档
|
|
13
|
+
COMPRESSED = 'compressed', // 已压缩
|
|
14
|
+
CORRUPTED = 'corrupted' // 已损坏
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 检查点过滤器
|
|
19
|
+
*/
|
|
20
|
+
export interface CheckpointFilter {
|
|
21
|
+
sessionId?: string;
|
|
22
|
+
status?: CheckpointStatus;
|
|
23
|
+
dateFrom?: Date;
|
|
24
|
+
dateTo?: Date;
|
|
25
|
+
tags?: string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 检查点搜索结果
|
|
30
|
+
*/
|
|
31
|
+
export interface CheckpointSearchResult {
|
|
32
|
+
checkpoint: Checkpoint;
|
|
33
|
+
relevanceScore: number;
|
|
34
|
+
matchedFields: string[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 检查点统计
|
|
39
|
+
*/
|
|
40
|
+
export interface CheckpointStatistics {
|
|
41
|
+
totalCheckpoints: number;
|
|
42
|
+
activeCheckpoints: number;
|
|
43
|
+
archivedCheckpoints: number;
|
|
44
|
+
compressedCheckpoints: number;
|
|
45
|
+
averageMessagesPerCheckpoint: number;
|
|
46
|
+
totalTokenUsage: number;
|
|
47
|
+
oldestCheckpoint?: Date;
|
|
48
|
+
newestCheckpoint?: Date;
|
|
49
|
+
mostActiveSession?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 检查点管理器
|
|
54
|
+
* 负责检查点的创建、恢复、归档和清理
|
|
55
|
+
*/
|
|
56
|
+
@Injectable({
|
|
57
|
+
providedIn: 'root'
|
|
58
|
+
})
|
|
59
|
+
export class CheckpointManager {
|
|
60
|
+
private checkpointsSubject = new BehaviorSubject<Checkpoint[]>([]);
|
|
61
|
+
public checkpoints$ = this.checkpointsSubject.asObservable();
|
|
62
|
+
|
|
63
|
+
private readonly MAX_CHECKPOINTS_PER_SESSION = 20;
|
|
64
|
+
private readonly AUTO_CLEANUP_DAYS = 30;
|
|
65
|
+
private readonly COMPRESSION_THRESHOLD = 1000; // 消息数量阈值
|
|
66
|
+
|
|
67
|
+
constructor(
|
|
68
|
+
private logger: LoggerService,
|
|
69
|
+
private chatHistoryService: ChatHistoryService
|
|
70
|
+
) {
|
|
71
|
+
this.loadCheckpoints();
|
|
72
|
+
this.logger.info('CheckpointManager initialized');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 创建检查点
|
|
77
|
+
*/
|
|
78
|
+
create(
|
|
79
|
+
sessionId: string,
|
|
80
|
+
messages: ApiMessage[],
|
|
81
|
+
summary?: string,
|
|
82
|
+
metadata: Partial<Checkpoint> = {}
|
|
83
|
+
): Checkpoint {
|
|
84
|
+
const checkpointId = this.generateCheckpointId();
|
|
85
|
+
const now = Date.now();
|
|
86
|
+
|
|
87
|
+
// 计算Token使用量
|
|
88
|
+
const tokenUsage = this.calculateTokenUsage(messages);
|
|
89
|
+
|
|
90
|
+
// 生成智能摘要
|
|
91
|
+
const autoSummary = summary || this.generateSummary(messages);
|
|
92
|
+
|
|
93
|
+
const checkpoint: Checkpoint = {
|
|
94
|
+
id: checkpointId,
|
|
95
|
+
sessionId,
|
|
96
|
+
messages: [...messages], // 深拷贝
|
|
97
|
+
summary: autoSummary,
|
|
98
|
+
createdAt: now,
|
|
99
|
+
tokenUsage,
|
|
100
|
+
...metadata
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// 保存到存储
|
|
104
|
+
this.saveCheckpoint(checkpoint);
|
|
105
|
+
|
|
106
|
+
// 更新本地列表
|
|
107
|
+
const currentCheckpoints = this.checkpointsSubject.value;
|
|
108
|
+
this.checkpointsSubject.next([...currentCheckpoints, checkpoint]);
|
|
109
|
+
|
|
110
|
+
// 强制执行限制
|
|
111
|
+
this.enforceSessionLimit(sessionId);
|
|
112
|
+
|
|
113
|
+
this.logger.info('Checkpoint created', {
|
|
114
|
+
checkpointId,
|
|
115
|
+
sessionId,
|
|
116
|
+
messageCount: messages.length,
|
|
117
|
+
tokenUsage: tokenUsage.input + tokenUsage.output
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
return checkpoint;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 恢复检查点
|
|
125
|
+
*/
|
|
126
|
+
restore(checkpointId: string): ApiMessage[] {
|
|
127
|
+
const checkpoint = this.getCheckpoint(checkpointId);
|
|
128
|
+
if (!checkpoint) {
|
|
129
|
+
throw new Error(`Checkpoint not found: ${checkpointId}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 验证检查点完整性
|
|
133
|
+
this.validateCheckpoint(checkpoint);
|
|
134
|
+
|
|
135
|
+
this.logger.info('Checkpoint restored', {
|
|
136
|
+
checkpointId,
|
|
137
|
+
sessionId: checkpoint.sessionId,
|
|
138
|
+
messageCount: checkpoint.messages.length
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
return [...checkpoint.messages];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* 获取检查点
|
|
146
|
+
*/
|
|
147
|
+
getCheckpoint(checkpointId: string): Checkpoint | undefined {
|
|
148
|
+
return this.checkpointsSubject.value.find(cp => cp.id === checkpointId);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* 列出检查点
|
|
153
|
+
*/
|
|
154
|
+
listBySession(sessionId: string, filter?: CheckpointFilter): Checkpoint[] {
|
|
155
|
+
let checkpoints = this.checkpointsSubject.value.filter(cp => cp.sessionId === sessionId);
|
|
156
|
+
|
|
157
|
+
if (filter) {
|
|
158
|
+
checkpoints = this.applyFilter(checkpoints, filter);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 按创建时间倒序排列
|
|
162
|
+
return checkpoints.sort((a, b) => b.createdAt - a.createdAt);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* 搜索检查点
|
|
167
|
+
*/
|
|
168
|
+
search(query: string, filter?: CheckpointFilter): CheckpointSearchResult[] {
|
|
169
|
+
const searchTerms = query.toLowerCase().split(' ');
|
|
170
|
+
let checkpoints = this.checkpointsSubject.value;
|
|
171
|
+
|
|
172
|
+
if (filter) {
|
|
173
|
+
checkpoints = this.applyFilter(checkpoints, filter);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const results: CheckpointSearchResult[] = [];
|
|
177
|
+
|
|
178
|
+
checkpoints.forEach(checkpoint => {
|
|
179
|
+
const searchableText = `${checkpoint.summary} ${checkpoint.messages.map(m =>
|
|
180
|
+
typeof m.content === 'string' ? m.content : JSON.stringify(m.content)
|
|
181
|
+
).join(' ')}`.toLowerCase();
|
|
182
|
+
|
|
183
|
+
let relevanceScore = 0;
|
|
184
|
+
const matchedFields: string[] = [];
|
|
185
|
+
|
|
186
|
+
searchTerms.forEach(term => {
|
|
187
|
+
if (checkpoint.summary.toLowerCase().includes(term)) {
|
|
188
|
+
relevanceScore += 2;
|
|
189
|
+
matchedFields.push('summary');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (searchableText.includes(term)) {
|
|
193
|
+
relevanceScore += 1;
|
|
194
|
+
matchedFields.push('content');
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
if (relevanceScore > 0) {
|
|
199
|
+
results.push({
|
|
200
|
+
checkpoint,
|
|
201
|
+
relevanceScore,
|
|
202
|
+
matchedFields: [...new Set(matchedFields)]
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// 按相关性排序
|
|
208
|
+
return results.sort((a, b) => b.relevanceScore - a.relevanceScore);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* 归档检查点
|
|
213
|
+
*/
|
|
214
|
+
archive(checkpointId: string): void {
|
|
215
|
+
const checkpoint = this.getCheckpoint(checkpointId);
|
|
216
|
+
if (!checkpoint) {
|
|
217
|
+
throw new Error(`Checkpoint not found: ${checkpointId}`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// 标记为已归档
|
|
221
|
+
const archivedCheckpoint = {
|
|
222
|
+
...checkpoint,
|
|
223
|
+
// 可以添加状态字段
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
this.updateCheckpoint(archivedCheckpoint);
|
|
227
|
+
|
|
228
|
+
this.logger.info('Checkpoint archived', { checkpointId });
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* 删除检查点
|
|
233
|
+
*/
|
|
234
|
+
delete(checkpointId: string): void {
|
|
235
|
+
const currentCheckpoints = this.checkpointsSubject.value;
|
|
236
|
+
const filteredCheckpoints = currentCheckpoints.filter(cp => cp.id !== checkpointId);
|
|
237
|
+
|
|
238
|
+
this.checkpointsSubject.next(filteredCheckpoints);
|
|
239
|
+
this.removeFromStorage(checkpointId);
|
|
240
|
+
|
|
241
|
+
this.logger.info('Checkpoint deleted', { checkpointId });
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* 压缩存储检查点
|
|
246
|
+
*/
|
|
247
|
+
async compressForCheckpoint(checkpointId: string): Promise<Checkpoint> {
|
|
248
|
+
const checkpoint = this.getCheckpoint(checkpointId);
|
|
249
|
+
if (!checkpoint) {
|
|
250
|
+
throw new Error(`Checkpoint not found: ${checkpointId}`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// TODO: 实现压缩逻辑
|
|
254
|
+
// 这里应该使用 ContextManager 进行压缩
|
|
255
|
+
const compressedMessages = checkpoint.messages; // 临时实现
|
|
256
|
+
|
|
257
|
+
const compressedCheckpoint: Checkpoint = {
|
|
258
|
+
...checkpoint,
|
|
259
|
+
messages: compressedMessages,
|
|
260
|
+
// 可以添加压缩相关的元数据
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
this.updateCheckpoint(compressedCheckpoint);
|
|
264
|
+
|
|
265
|
+
this.logger.info('Checkpoint compressed', {
|
|
266
|
+
checkpointId,
|
|
267
|
+
originalSize: checkpoint.messages.length,
|
|
268
|
+
compressedSize: compressedMessages.length
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
return compressedCheckpoint;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* 清理孤儿检查点
|
|
276
|
+
*/
|
|
277
|
+
cleanupOrphaned(): number {
|
|
278
|
+
const allCheckpoints = this.checkpointsSubject.value;
|
|
279
|
+
const sessions = this.chatHistoryService.getRecentSessions(1000);
|
|
280
|
+
|
|
281
|
+
const validSessionIds = new Set(sessions.map(s => s.sessionId));
|
|
282
|
+
const orphanedCheckpoints = allCheckpoints.filter(cp => !validSessionIds.has(cp.sessionId));
|
|
283
|
+
|
|
284
|
+
orphanedCheckpoints.forEach(cp => {
|
|
285
|
+
this.removeFromStorage(cp.id);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
const remainingCheckpoints = allCheckpoints.filter(cp => validSessionIds.has(cp.sessionId));
|
|
289
|
+
this.checkpointsSubject.next(remainingCheckpoints);
|
|
290
|
+
|
|
291
|
+
this.logger.info('Cleaned up orphaned checkpoints', {
|
|
292
|
+
removedCount: orphanedCheckpoints.length
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
return orphanedCheckpoints.length;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* 自动清理过期检查点
|
|
300
|
+
*/
|
|
301
|
+
autoCleanup(): number {
|
|
302
|
+
const allCheckpoints = this.checkpointsSubject.value;
|
|
303
|
+
const now = Date.now();
|
|
304
|
+
const cutoffTime = now - (this.AUTO_CLEANUP_DAYS * 24 * 60 * 60 * 1000);
|
|
305
|
+
|
|
306
|
+
const expiredCheckpoints = allCheckpoints.filter(cp => cp.createdAt < cutoffTime);
|
|
307
|
+
|
|
308
|
+
expiredCheckpoints.forEach(cp => {
|
|
309
|
+
this.removeFromStorage(cp.id);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
const remainingCheckpoints = allCheckpoints.filter(cp => cp.createdAt >= cutoffTime);
|
|
313
|
+
this.checkpointsSubject.next(remainingCheckpoints);
|
|
314
|
+
|
|
315
|
+
this.logger.info('Auto cleanup completed', {
|
|
316
|
+
removedCount: expiredCheckpoints.length,
|
|
317
|
+
remainingCount: remainingCheckpoints.length
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
return expiredCheckpoints.length;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* 获取统计信息
|
|
325
|
+
*/
|
|
326
|
+
getStatistics(): CheckpointStatistics {
|
|
327
|
+
const allCheckpoints = this.checkpointsSubject.value;
|
|
328
|
+
|
|
329
|
+
const totalCheckpoints = allCheckpoints.length;
|
|
330
|
+
const activeCheckpoints = totalCheckpoints; // 简化实现
|
|
331
|
+
const archivedCheckpoints = 0; // TODO: 实现状态跟踪
|
|
332
|
+
const compressedCheckpoints = 0; // TODO: 实现压缩状态跟踪
|
|
333
|
+
|
|
334
|
+
const totalMessages = allCheckpoints.reduce((sum, cp) => sum + cp.messages.length, 0);
|
|
335
|
+
const averageMessagesPerCheckpoint = totalCheckpoints > 0 ? totalMessages / totalCheckpoints : 0;
|
|
336
|
+
|
|
337
|
+
const totalTokenUsage = allCheckpoints.reduce((sum, cp) => {
|
|
338
|
+
return sum + cp.tokenUsage.input + cp.tokenUsage.output;
|
|
339
|
+
}, 0);
|
|
340
|
+
|
|
341
|
+
const timestamps = allCheckpoints.map(cp => cp.createdAt);
|
|
342
|
+
const oldestCheckpoint = timestamps.length > 0 ? new Date(Math.min(...timestamps)) : undefined;
|
|
343
|
+
const newestCheckpoint = timestamps.length > 0 ? new Date(Math.max(...timestamps)) : undefined;
|
|
344
|
+
|
|
345
|
+
// 找出最活跃的会话
|
|
346
|
+
const sessionCounts = new Map<string, number>();
|
|
347
|
+
allCheckpoints.forEach(cp => {
|
|
348
|
+
const count = sessionCounts.get(cp.sessionId) || 0;
|
|
349
|
+
sessionCounts.set(cp.sessionId, count + 1);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
let mostActiveSession: string | undefined;
|
|
353
|
+
let maxCount = 0;
|
|
354
|
+
sessionCounts.forEach((count, sessionId) => {
|
|
355
|
+
if (count > maxCount) {
|
|
356
|
+
maxCount = count;
|
|
357
|
+
mostActiveSession = sessionId;
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
return {
|
|
362
|
+
totalCheckpoints,
|
|
363
|
+
activeCheckpoints,
|
|
364
|
+
archivedCheckpoints,
|
|
365
|
+
compressedCheckpoints,
|
|
366
|
+
averageMessagesPerCheckpoint: Math.round(averageMessagesPerCheckpoint * 100) / 100,
|
|
367
|
+
totalTokenUsage,
|
|
368
|
+
oldestCheckpoint,
|
|
369
|
+
newestCheckpoint,
|
|
370
|
+
mostActiveSession
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* 导出检查点
|
|
376
|
+
*/
|
|
377
|
+
exportCheckpoint(checkpointId: string): string {
|
|
378
|
+
const checkpoint = this.getCheckpoint(checkpointId);
|
|
379
|
+
if (!checkpoint) {
|
|
380
|
+
throw new Error(`Checkpoint not found: ${checkpointId}`);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const exportData = {
|
|
384
|
+
checkpoint,
|
|
385
|
+
exportedAt: new Date().toISOString(),
|
|
386
|
+
version: '1.0'
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
return JSON.stringify(exportData, null, 2);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* 导入检查点
|
|
394
|
+
*/
|
|
395
|
+
importCheckpoint(data: string): Checkpoint {
|
|
396
|
+
try {
|
|
397
|
+
const importData = JSON.parse(data);
|
|
398
|
+
|
|
399
|
+
if (!importData.checkpoint) {
|
|
400
|
+
throw new Error('Invalid checkpoint format');
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const checkpoint = importData.checkpoint;
|
|
404
|
+
|
|
405
|
+
// 验证检查点
|
|
406
|
+
this.validateCheckpoint(checkpoint);
|
|
407
|
+
|
|
408
|
+
// 添加到列表
|
|
409
|
+
const currentCheckpoints = this.checkpointsSubject.value;
|
|
410
|
+
this.checkpointsSubject.next([...currentCheckpoints, checkpoint]);
|
|
411
|
+
|
|
412
|
+
// 保存到存储
|
|
413
|
+
this.saveCheckpoint(checkpoint);
|
|
414
|
+
|
|
415
|
+
this.logger.info('Checkpoint imported', {
|
|
416
|
+
checkpointId: checkpoint.id,
|
|
417
|
+
sessionId: checkpoint.sessionId
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
return checkpoint;
|
|
421
|
+
} catch (error) {
|
|
422
|
+
this.logger.error('Failed to import checkpoint', error);
|
|
423
|
+
throw new Error('Invalid checkpoint file format');
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* 比较检查点
|
|
429
|
+
*/
|
|
430
|
+
compare(checkpointId1: string, checkpointId2: string): {
|
|
431
|
+
messageDiff: number;
|
|
432
|
+
tokenDiff: number;
|
|
433
|
+
timeDiff: number;
|
|
434
|
+
summaryDiff: string;
|
|
435
|
+
} {
|
|
436
|
+
const cp1 = this.getCheckpoint(checkpointId1);
|
|
437
|
+
const cp2 = this.getCheckpoint(checkpointId2);
|
|
438
|
+
|
|
439
|
+
if (!cp1 || !cp2) {
|
|
440
|
+
throw new Error('One or both checkpoints not found');
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return {
|
|
444
|
+
messageDiff: cp2.messages.length - cp1.messages.length,
|
|
445
|
+
tokenDiff: (cp2.tokenUsage.input + cp2.tokenUsage.output) -
|
|
446
|
+
(cp1.tokenUsage.input + cp1.tokenUsage.output),
|
|
447
|
+
timeDiff: cp2.createdAt - cp1.createdAt,
|
|
448
|
+
summaryDiff: cp2.summary !== cp1.summary ? 'Different' : 'Same'
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// ==================== 私有方法 ====================
|
|
453
|
+
|
|
454
|
+
private generateCheckpointId(): string {
|
|
455
|
+
return `checkpoint_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
private calculateTokenUsage(messages: ApiMessage[]): Checkpoint['tokenUsage'] {
|
|
459
|
+
let input = 0;
|
|
460
|
+
let output = 0;
|
|
461
|
+
|
|
462
|
+
messages.forEach(msg => {
|
|
463
|
+
const content = typeof msg.content === 'string'
|
|
464
|
+
? msg.content
|
|
465
|
+
: JSON.stringify(msg.content);
|
|
466
|
+
|
|
467
|
+
const tokens = Math.ceil(content.length / 4);
|
|
468
|
+
|
|
469
|
+
if (msg.role === 'user' || msg.role === 'system') {
|
|
470
|
+
input += tokens;
|
|
471
|
+
} else if (msg.role === 'assistant') {
|
|
472
|
+
output += tokens;
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
return {
|
|
477
|
+
input,
|
|
478
|
+
output,
|
|
479
|
+
cacheRead: 0,
|
|
480
|
+
cacheWrite: 0
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
private generateSummary(messages: ApiMessage[]): string {
|
|
485
|
+
if (messages.length === 0) {
|
|
486
|
+
return '空检查点';
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const firstMessage = messages[0];
|
|
490
|
+
const lastMessage = messages[messages.length - 1];
|
|
491
|
+
|
|
492
|
+
const firstContent = typeof firstMessage.content === 'string'
|
|
493
|
+
? firstMessage.content
|
|
494
|
+
: '[复杂内容]';
|
|
495
|
+
|
|
496
|
+
const lastContent = typeof lastMessage.content === 'string'
|
|
497
|
+
? lastMessage.content
|
|
498
|
+
: '[复杂内容]';
|
|
499
|
+
|
|
500
|
+
return `检查点:${messages.length}条消息 | 从 "${firstContent.substring(0, 50)}..." 到 "${lastContent.substring(0, 50)}..."`;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
private applyFilter(checkpoints: Checkpoint[], filter: CheckpointFilter): Checkpoint[] {
|
|
504
|
+
let filtered = [...checkpoints];
|
|
505
|
+
|
|
506
|
+
if (filter.sessionId) {
|
|
507
|
+
filtered = filtered.filter(cp => cp.sessionId === filter.sessionId);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (filter.dateFrom) {
|
|
511
|
+
filtered = filtered.filter(cp => cp.createdAt >= filter.dateFrom!.getTime());
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (filter.dateTo) {
|
|
515
|
+
filtered = filtered.filter(cp => cp.createdAt <= filter.dateTo!.getTime());
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (filter.tags && filter.tags.length > 0) {
|
|
519
|
+
// TODO: 实现标签过滤
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return filtered;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
private validateCheckpoint(checkpoint: Checkpoint): void {
|
|
526
|
+
if (!checkpoint.id) {
|
|
527
|
+
throw new Error('Invalid checkpoint: missing id');
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (!checkpoint.sessionId) {
|
|
531
|
+
throw new Error('Invalid checkpoint: missing sessionId');
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (!Array.isArray(checkpoint.messages)) {
|
|
535
|
+
throw new Error('Invalid checkpoint: messages must be an array');
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (checkpoint.tokenUsage.input < 0 || checkpoint.tokenUsage.output < 0) {
|
|
539
|
+
throw new Error('Invalid checkpoint: negative token usage');
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
private enforceSessionLimit(sessionId: string): void {
|
|
544
|
+
const sessionCheckpoints = this.listBySession(sessionId);
|
|
545
|
+
|
|
546
|
+
if (sessionCheckpoints.length > this.MAX_CHECKPOINTS_PER_SESSION) {
|
|
547
|
+
// 删除最旧的检查点
|
|
548
|
+
const toDelete = sessionCheckpoints
|
|
549
|
+
.sort((a, b) => a.createdAt - b.createdAt)
|
|
550
|
+
.slice(0, sessionCheckpoints.length - this.MAX_CHECKPOINTS_PER_SESSION);
|
|
551
|
+
|
|
552
|
+
toDelete.forEach(cp => {
|
|
553
|
+
this.delete(cp.id);
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
this.logger.info('Enforced checkpoint limit', {
|
|
557
|
+
sessionId,
|
|
558
|
+
deletedCount: toDelete.length
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
private saveCheckpoint(checkpoint: Checkpoint): void {
|
|
564
|
+
try {
|
|
565
|
+
const key = `checkpoint_${checkpoint.id}`;
|
|
566
|
+
localStorage.setItem(key, JSON.stringify(checkpoint));
|
|
567
|
+
} catch (error) {
|
|
568
|
+
this.logger.error('Failed to save checkpoint', error);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
private removeFromStorage(checkpointId: string): void {
|
|
573
|
+
try {
|
|
574
|
+
const key = `checkpoint_${checkpointId}`;
|
|
575
|
+
localStorage.removeItem(key);
|
|
576
|
+
} catch (error) {
|
|
577
|
+
this.logger.error('Failed to remove checkpoint from storage', error);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
private updateCheckpoint(checkpoint: Checkpoint): void {
|
|
582
|
+
const currentCheckpoints = this.checkpointsSubject.value;
|
|
583
|
+
const index = currentCheckpoints.findIndex(cp => cp.id === checkpoint.id);
|
|
584
|
+
|
|
585
|
+
if (index >= 0) {
|
|
586
|
+
currentCheckpoints[index] = checkpoint;
|
|
587
|
+
this.checkpointsSubject.next([...currentCheckpoints]);
|
|
588
|
+
this.saveCheckpoint(checkpoint);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
private loadCheckpoints(): void {
|
|
593
|
+
try {
|
|
594
|
+
// 从 localStorage 加载所有检查点
|
|
595
|
+
const checkpoints: Checkpoint[] = [];
|
|
596
|
+
|
|
597
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
598
|
+
const key = localStorage.key(i);
|
|
599
|
+
if (key && key.startsWith('checkpoint_')) {
|
|
600
|
+
try {
|
|
601
|
+
const checkpoint = JSON.parse(localStorage.getItem(key)!);
|
|
602
|
+
checkpoints.push(checkpoint);
|
|
603
|
+
} catch (error) {
|
|
604
|
+
this.logger.warn('Failed to parse checkpoint', { key, error });
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
this.checkpointsSubject.next(checkpoints);
|
|
610
|
+
|
|
611
|
+
this.logger.info('Loaded checkpoints from storage', {
|
|
612
|
+
count: checkpoints.length
|
|
613
|
+
});
|
|
614
|
+
} catch (error) {
|
|
615
|
+
this.logger.error('Failed to load checkpoints', error);
|
|
616
|
+
this.checkpointsSubject.next([]);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|