tabby-ai-assistant 1.0.13 → 1.0.16
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/.editorconfig +18 -0
- package/README.md +40 -10
- package/dist/index.js +1 -1
- package/package.json +5 -3
- package/src/components/chat/ai-sidebar.component.scss +220 -9
- package/src/components/chat/ai-sidebar.component.ts +379 -29
- package/src/components/chat/chat-input.component.ts +36 -4
- package/src/components/chat/chat-interface.component.ts +225 -5
- package/src/components/chat/chat-message.component.ts +6 -1
- package/src/components/settings/context-settings.component.ts +91 -91
- package/src/components/terminal/ai-toolbar-button.component.ts +4 -2
- package/src/components/terminal/command-suggestion.component.ts +148 -6
- package/src/index.ts +81 -19
- package/src/providers/tabby/ai-toolbar-button.provider.ts +7 -3
- package/src/services/chat/ai-sidebar.service.ts +448 -410
- package/src/services/chat/chat-session.service.ts +36 -12
- package/src/services/context/compaction.ts +110 -134
- package/src/services/context/manager.ts +27 -7
- package/src/services/context/memory.ts +17 -33
- package/src/services/context/summary.service.ts +136 -0
- package/src/services/core/ai-assistant.service.ts +1060 -37
- package/src/services/core/ai-provider-manager.service.ts +154 -25
- package/src/services/core/checkpoint.service.ts +218 -18
- package/src/services/core/toast.service.ts +106 -106
- package/src/services/providers/anthropic-provider.service.ts +126 -30
- package/src/services/providers/base-provider.service.ts +90 -7
- package/src/services/providers/glm-provider.service.ts +151 -38
- package/src/services/providers/minimax-provider.service.ts +55 -40
- package/src/services/providers/ollama-provider.service.ts +117 -28
- package/src/services/providers/openai-compatible.service.ts +164 -34
- package/src/services/providers/openai-provider.service.ts +169 -34
- package/src/services/providers/vllm-provider.service.ts +116 -28
- package/src/services/terminal/terminal-context.service.ts +265 -5
- package/src/services/terminal/terminal-manager.service.ts +845 -748
- package/src/services/terminal/terminal-tools.service.ts +612 -441
- package/src/types/ai.types.ts +156 -3
- package/src/utils/cost.utils.ts +249 -0
- package/src/utils/validation.utils.ts +306 -2
- package/dist/index.js.LICENSE.txt +0 -18
- package/src/services/terminal/command-analyzer.service.ts +0 -43
- package/src/services/terminal/context-menu.service.ts +0 -45
- package/src/services/terminal/hotkey.service.ts +0 -53
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import { Injectable } from '@angular/core';
|
|
2
2
|
import { Subject, Observable } from 'rxjs';
|
|
3
|
-
import { BaseAiProvider, ProviderManager, ProviderInfo, ProviderEvent } from '../../types/provider.types';
|
|
3
|
+
import { BaseAiProvider, ProviderManager, ProviderInfo, ProviderEvent, HealthStatus } from '../../types/provider.types';
|
|
4
4
|
import { LoggerService } from './logger.service';
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* 健康检查缓存项
|
|
8
|
+
*/
|
|
9
|
+
interface HealthCheckCacheItem {
|
|
10
|
+
status: HealthStatus;
|
|
11
|
+
latency: number;
|
|
12
|
+
timestamp: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
6
15
|
/**
|
|
7
16
|
* AI提供商管理器
|
|
8
17
|
* 负责注册、管理和切换不同的AI提供商
|
|
@@ -13,6 +22,14 @@ export class AiProviderManagerService implements ProviderManager {
|
|
|
13
22
|
private activeProvider: string | null = null;
|
|
14
23
|
private eventSubject = new Subject<ProviderEvent>();
|
|
15
24
|
|
|
25
|
+
// 健康检查缓存
|
|
26
|
+
private healthCache = new Map<string, HealthCheckCacheItem>();
|
|
27
|
+
private readonly HEALTH_CACHE_TTL = 60000; // 缓存有效期:60秒
|
|
28
|
+
private readonly HEALTH_CHECK_TIMEOUT = 10000; // 健康检查超时:10秒
|
|
29
|
+
|
|
30
|
+
// 正在进行的健康检查(防止并发重复检查)
|
|
31
|
+
private pendingHealthChecks = new Map<string, Promise<HealthStatus>>();
|
|
32
|
+
|
|
16
33
|
constructor(private logger: LoggerService) {}
|
|
17
34
|
|
|
18
35
|
/**
|
|
@@ -174,33 +191,139 @@ export class AiProviderManagerService implements ProviderManager {
|
|
|
174
191
|
}
|
|
175
192
|
|
|
176
193
|
/**
|
|
177
|
-
*
|
|
194
|
+
* 检查所有提供商健康状态(使用缓存)
|
|
178
195
|
*/
|
|
179
|
-
async checkAllProvidersHealth(): Promise<{ provider: string; status:
|
|
180
|
-
const results: { provider: string; status:
|
|
196
|
+
async checkAllProvidersHealth(forceRefresh: boolean = false): Promise<{ provider: string; status: HealthStatus; latency?: number; cached?: boolean }[]> {
|
|
197
|
+
const results: { provider: string; status: HealthStatus; latency?: number; cached?: boolean }[] = [];
|
|
198
|
+
|
|
199
|
+
// 并行执行所有健康检查
|
|
200
|
+
const checks = Array.from(this.providers.entries()).map(async ([name, provider]) => {
|
|
201
|
+
const healthStatus = await this.getProviderHealthStatus(name, forceRefresh);
|
|
202
|
+
const cachedItem = this.healthCache.get(name);
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
provider: name,
|
|
206
|
+
status: healthStatus,
|
|
207
|
+
latency: cachedItem?.latency,
|
|
208
|
+
cached: cachedItem && !forceRefresh && (Date.now() - cachedItem.timestamp) < this.HEALTH_CACHE_TTL
|
|
209
|
+
};
|
|
210
|
+
});
|
|
181
211
|
|
|
182
|
-
|
|
183
|
-
try {
|
|
184
|
-
const start = Date.now();
|
|
185
|
-
const health = await provider.healthCheck();
|
|
186
|
-
const latency = Date.now() - start;
|
|
212
|
+
const settledResults = await Promise.allSettled(checks);
|
|
187
213
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
latency
|
|
192
|
-
});
|
|
193
|
-
} catch (error) {
|
|
194
|
-
results.push({
|
|
195
|
-
provider: name,
|
|
196
|
-
status: 'unhealthy'
|
|
197
|
-
});
|
|
214
|
+
for (const result of settledResults) {
|
|
215
|
+
if (result.status === 'fulfilled') {
|
|
216
|
+
results.push(result.value);
|
|
198
217
|
}
|
|
199
218
|
}
|
|
200
219
|
|
|
201
220
|
return results;
|
|
202
221
|
}
|
|
203
222
|
|
|
223
|
+
/**
|
|
224
|
+
* 获取单个提供商的健康状态(使用缓存)
|
|
225
|
+
*/
|
|
226
|
+
async getProviderHealthStatus(providerName: string, forceRefresh: boolean = false): Promise<HealthStatus> {
|
|
227
|
+
const provider = this.providers.get(providerName);
|
|
228
|
+
if (!provider) {
|
|
229
|
+
return HealthStatus.UNHEALTHY;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// 检查缓存是否有效
|
|
233
|
+
if (!forceRefresh) {
|
|
234
|
+
const cached = this.healthCache.get(providerName);
|
|
235
|
+
if (cached && (Date.now() - cached.timestamp) < this.HEALTH_CACHE_TTL) {
|
|
236
|
+
this.logger.debug(`Health check cache hit for ${providerName}`);
|
|
237
|
+
return cached.status;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// 检查是否已有正在进行的健康检查
|
|
242
|
+
if (this.pendingHealthChecks.has(providerName)) {
|
|
243
|
+
this.logger.debug(`Health check already in progress for ${providerName}`);
|
|
244
|
+
return this.pendingHealthChecks.get(providerName)!;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// 执行新的健康检查
|
|
248
|
+
const healthCheckPromise = this.executeHealthCheck(provider);
|
|
249
|
+
this.pendingHealthChecks.set(providerName, healthCheckPromise);
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
const status = await Promise.race([
|
|
253
|
+
healthCheckPromise,
|
|
254
|
+
new Promise<HealthStatus>(resolve => {
|
|
255
|
+
setTimeout(() => resolve(HealthStatus.DEGRADED), this.HEALTH_CHECK_TIMEOUT);
|
|
256
|
+
})
|
|
257
|
+
]);
|
|
258
|
+
|
|
259
|
+
// 更新缓存
|
|
260
|
+
this.updateHealthCache(providerName, status);
|
|
261
|
+
|
|
262
|
+
return status;
|
|
263
|
+
} finally {
|
|
264
|
+
this.pendingHealthChecks.delete(providerName);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* 执行健康检查(内部方法)
|
|
270
|
+
*/
|
|
271
|
+
private async executeHealthCheck(provider: BaseAiProvider): Promise<HealthStatus> {
|
|
272
|
+
const start = Date.now();
|
|
273
|
+
try {
|
|
274
|
+
const status = await provider.healthCheck();
|
|
275
|
+
const latency = Date.now() - start;
|
|
276
|
+
this.logger.debug(`Health check completed for ${provider.name}`, { status, latency });
|
|
277
|
+
return status;
|
|
278
|
+
} catch (error) {
|
|
279
|
+
this.logger.warn(`Health check failed for ${provider.name}`, error);
|
|
280
|
+
return HealthStatus.UNHEALTHY;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* 更新健康检查缓存
|
|
286
|
+
*/
|
|
287
|
+
private updateHealthCache(providerName: string, status: HealthStatus): void {
|
|
288
|
+
const provider = this.providers.get(providerName);
|
|
289
|
+
if (!provider) return;
|
|
290
|
+
|
|
291
|
+
const cachedItem = this.healthCache.get(providerName);
|
|
292
|
+
this.healthCache.set(providerName, {
|
|
293
|
+
status,
|
|
294
|
+
latency: cachedItem?.latency || 0,
|
|
295
|
+
timestamp: Date.now()
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* 清除健康检查缓存
|
|
301
|
+
*/
|
|
302
|
+
clearHealthCache(): void {
|
|
303
|
+
this.healthCache.clear();
|
|
304
|
+
this.logger.info('Health check cache cleared');
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* 清除指定提供商的健康检查缓存
|
|
309
|
+
*/
|
|
310
|
+
clearProviderHealthCache(providerName: string): void {
|
|
311
|
+
this.healthCache.delete(providerName);
|
|
312
|
+
this.logger.debug(`Health check cache cleared for ${providerName}`);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* 获取健康检查缓存状态
|
|
317
|
+
*/
|
|
318
|
+
getHealthCacheStatus(): { provider: string; cached: boolean; age: number }[] {
|
|
319
|
+
const now = Date.now();
|
|
320
|
+
return Array.from(this.healthCache.entries()).map(([name, item]) => ({
|
|
321
|
+
provider: name,
|
|
322
|
+
cached: true,
|
|
323
|
+
age: now - item.timestamp
|
|
324
|
+
}));
|
|
325
|
+
}
|
|
326
|
+
|
|
204
327
|
/**
|
|
205
328
|
* 获取启用的提供商
|
|
206
329
|
*/
|
|
@@ -288,18 +411,22 @@ export class AiProviderManagerService implements ProviderManager {
|
|
|
288
411
|
totalProviders: number;
|
|
289
412
|
enabledProviders: number;
|
|
290
413
|
activeProvider: string | null;
|
|
291
|
-
providers: { name: string; enabled: boolean; healthy: boolean }[];
|
|
414
|
+
providers: { name: string; enabled: boolean; healthy: boolean; cached?: boolean }[];
|
|
292
415
|
} {
|
|
293
416
|
const providers = this.getAllProviders();
|
|
294
417
|
return {
|
|
295
418
|
totalProviders: providers.length,
|
|
296
419
|
enabledProviders: this.getEnabledProviders().length,
|
|
297
420
|
activeProvider: this.activeProvider,
|
|
298
|
-
providers: providers.map(p =>
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
421
|
+
providers: providers.map(p => {
|
|
422
|
+
const cached = this.healthCache.get(p.name);
|
|
423
|
+
return {
|
|
424
|
+
name: p.name,
|
|
425
|
+
enabled: p.getConfig()?.enabled !== false,
|
|
426
|
+
healthy: cached?.status === HealthStatus.HEALTHY,
|
|
427
|
+
cached: !!cached
|
|
428
|
+
};
|
|
429
|
+
})
|
|
303
430
|
};
|
|
304
431
|
}
|
|
305
432
|
|
|
@@ -309,6 +436,8 @@ export class AiProviderManagerService implements ProviderManager {
|
|
|
309
436
|
reset(): void {
|
|
310
437
|
this.providers.clear();
|
|
311
438
|
this.activeProvider = null;
|
|
439
|
+
this.healthCache.clear();
|
|
440
|
+
this.pendingHealthChecks.clear();
|
|
312
441
|
this.logger.info('All providers reset');
|
|
313
442
|
}
|
|
314
443
|
}
|
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
import { Injectable } from '@angular/core';
|
|
2
2
|
import { BehaviorSubject, Observable } from 'rxjs';
|
|
3
|
+
import * as pako from 'pako';
|
|
3
4
|
import { LoggerService } from './logger.service';
|
|
4
5
|
import { ChatHistoryService } from '../chat/chat-history.service';
|
|
5
6
|
import { Checkpoint, ApiMessage } from '../../types/ai.types';
|
|
6
7
|
|
|
8
|
+
/**
|
|
9
|
+
* 压缩后的检查点数据接口
|
|
10
|
+
*/
|
|
11
|
+
export interface CompressedCheckpointData {
|
|
12
|
+
compressed: boolean;
|
|
13
|
+
compressionRatio: number;
|
|
14
|
+
originalSize: number;
|
|
15
|
+
compressedSize: number;
|
|
16
|
+
messages: ApiMessage[];
|
|
17
|
+
messagesJson: string; // 压缩后的JSON字符串
|
|
18
|
+
}
|
|
19
|
+
|
|
7
20
|
/**
|
|
8
21
|
* 检查点状态
|
|
9
22
|
*/
|
|
@@ -63,6 +76,13 @@ export class CheckpointManager {
|
|
|
63
76
|
private readonly MAX_CHECKPOINTS_PER_SESSION = 20;
|
|
64
77
|
private readonly AUTO_CLEANUP_DAYS = 30;
|
|
65
78
|
private readonly COMPRESSION_THRESHOLD = 1000; // 消息数量阈值
|
|
79
|
+
private readonly MIN_COMPRESSION_SIZE = 1024; // 最小压缩大小(字节)
|
|
80
|
+
private readonly COMPRESSION_LEVEL = 6; // pako压缩级别 (1-9)
|
|
81
|
+
|
|
82
|
+
// 压缩统计
|
|
83
|
+
private totalOriginalSize = 0;
|
|
84
|
+
private totalCompressedSize = 0;
|
|
85
|
+
private compressionCount = 0;
|
|
66
86
|
|
|
67
87
|
constructor(
|
|
68
88
|
private logger: LoggerService,
|
|
@@ -243,6 +263,7 @@ export class CheckpointManager {
|
|
|
243
263
|
|
|
244
264
|
/**
|
|
245
265
|
* 压缩存储检查点
|
|
266
|
+
* 使用 pako DEFLATE 算法压缩消息数据
|
|
246
267
|
*/
|
|
247
268
|
async compressForCheckpoint(checkpointId: string): Promise<Checkpoint> {
|
|
248
269
|
const checkpoint = this.getCheckpoint(checkpointId);
|
|
@@ -250,25 +271,185 @@ export class CheckpointManager {
|
|
|
250
271
|
throw new Error(`Checkpoint not found: ${checkpointId}`);
|
|
251
272
|
}
|
|
252
273
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
274
|
+
try {
|
|
275
|
+
// 1. 将消息转换为 JSON 字符串
|
|
276
|
+
const messagesJson = JSON.stringify(checkpoint.messages);
|
|
277
|
+
const originalSize = messagesJson.length;
|
|
278
|
+
|
|
279
|
+
// 2. 如果数据太小,不进行压缩
|
|
280
|
+
if (originalSize < this.MIN_COMPRESSION_SIZE) {
|
|
281
|
+
this.logger.info('Checkpoint too small for compression', {
|
|
282
|
+
checkpointId,
|
|
283
|
+
size: originalSize
|
|
284
|
+
});
|
|
285
|
+
return checkpoint;
|
|
286
|
+
}
|
|
256
287
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
288
|
+
// 3. 使用 pako 进行 DEFLATE 压缩
|
|
289
|
+
const compressedData = pako.deflate(messagesJson, {
|
|
290
|
+
level: this.COMPRESSION_LEVEL
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// 4. 将压缩后的数据转换为 base64 字符串存储
|
|
294
|
+
const compressedJson = this.arrayBufferToBase64(compressedData);
|
|
295
|
+
|
|
296
|
+
// 5. 计算压缩比
|
|
297
|
+
const compressedSize = compressedData.length;
|
|
298
|
+
const compressionRatioValue = originalSize > 0
|
|
299
|
+
? parseFloat(((originalSize - compressedSize) / originalSize * 100).toFixed(2))
|
|
300
|
+
: 0;
|
|
301
|
+
|
|
302
|
+
// 6. 更新统计
|
|
303
|
+
this.totalOriginalSize += originalSize;
|
|
304
|
+
this.totalCompressedSize += compressedSize;
|
|
305
|
+
this.compressionCount++;
|
|
306
|
+
|
|
307
|
+
// 7. 创建压缩后的检查点(保留原始消息用于恢复)
|
|
308
|
+
const compressedCheckpoint: Checkpoint = {
|
|
309
|
+
...checkpoint,
|
|
310
|
+
messages: checkpoint.messages, // 保留原始数据用于即时访问
|
|
311
|
+
compressedData: {
|
|
312
|
+
compressed: true,
|
|
313
|
+
compressionRatio: compressionRatioValue,
|
|
314
|
+
originalSize,
|
|
315
|
+
compressedSize,
|
|
316
|
+
messagesJson: compressedJson
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
this.updateCheckpoint(compressedCheckpoint);
|
|
321
|
+
|
|
322
|
+
this.logger.info('Checkpoint compressed successfully', {
|
|
323
|
+
checkpointId,
|
|
324
|
+
originalSize,
|
|
325
|
+
compressedSize,
|
|
326
|
+
compressionRatio: `${compressionRatioValue}%`,
|
|
327
|
+
overallRatio: this.getOverallCompressionRatio()
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
return compressedCheckpoint;
|
|
331
|
+
} catch (error) {
|
|
332
|
+
this.logger.error('Failed to compress checkpoint', {
|
|
333
|
+
checkpointId,
|
|
334
|
+
error
|
|
335
|
+
});
|
|
336
|
+
throw error;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* 解压缩检查点
|
|
342
|
+
*/
|
|
343
|
+
decompressCheckpoint(checkpointId: string): ApiMessage[] {
|
|
344
|
+
const checkpoint = this.getCheckpoint(checkpointId);
|
|
345
|
+
if (!checkpoint) {
|
|
346
|
+
throw new Error(`Checkpoint not found: ${checkpointId}`);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// 如果有压缩数据,解压缩
|
|
350
|
+
if (checkpoint.compressedData?.compressed) {
|
|
351
|
+
try {
|
|
352
|
+
const compressedData = this.base64ToArrayBuffer(checkpoint.compressedData.messagesJson);
|
|
353
|
+
const decompressedJson = pako.inflate(compressedData, { to: 'string' });
|
|
354
|
+
return JSON.parse(decompressedJson) as ApiMessage[];
|
|
355
|
+
} catch (error) {
|
|
356
|
+
this.logger.error('Failed to decompress checkpoint', {
|
|
357
|
+
checkpointId,
|
|
358
|
+
error
|
|
359
|
+
});
|
|
360
|
+
throw new Error('Failed to decompress checkpoint');
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// 否则返回原始消息
|
|
365
|
+
return checkpoint.messages;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* 自动压缩符合条件的检查点
|
|
370
|
+
*/
|
|
371
|
+
async autoCompressLargeCheckpoints(): Promise<number> {
|
|
372
|
+
const allCheckpoints = this.checkpointsSubject.value;
|
|
373
|
+
let compressedCount = 0;
|
|
374
|
+
|
|
375
|
+
for (const checkpoint of allCheckpoints) {
|
|
376
|
+
// 只压缩消息数量超过阈值的检查点
|
|
377
|
+
if (checkpoint.messages.length >= this.COMPRESSION_THRESHOLD) {
|
|
378
|
+
if (!checkpoint.compressedData?.compressed) {
|
|
379
|
+
try {
|
|
380
|
+
await this.compressForCheckpoint(checkpoint.id);
|
|
381
|
+
compressedCount++;
|
|
382
|
+
} catch (error) {
|
|
383
|
+
this.logger.warn('Failed to auto-compress checkpoint', {
|
|
384
|
+
checkpointId: checkpoint.id,
|
|
385
|
+
error
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (compressedCount > 0) {
|
|
393
|
+
this.logger.info('Auto-compression completed', {
|
|
394
|
+
compressedCount,
|
|
395
|
+
overallRatio: this.getOverallCompressionRatio()
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return compressedCount;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* 获取压缩统计信息
|
|
404
|
+
*/
|
|
405
|
+
getCompressionStatistics(): {
|
|
406
|
+
totalOriginalSize: number;
|
|
407
|
+
totalCompressedSize: number;
|
|
408
|
+
compressionCount: number;
|
|
409
|
+
overallRatio: string;
|
|
410
|
+
averageRatio: number;
|
|
411
|
+
} {
|
|
412
|
+
return {
|
|
413
|
+
totalOriginalSize: this.totalOriginalSize,
|
|
414
|
+
totalCompressedSize: this.totalCompressedSize,
|
|
415
|
+
compressionCount: this.compressionCount,
|
|
416
|
+
overallRatio: this.getOverallCompressionRatio(),
|
|
417
|
+
averageRatio: this.compressionCount > 0
|
|
418
|
+
? ((this.totalOriginalSize - this.totalCompressedSize) / this.totalOriginalSize * 100)
|
|
419
|
+
: 0
|
|
261
420
|
};
|
|
421
|
+
}
|
|
262
422
|
|
|
263
|
-
|
|
423
|
+
/**
|
|
424
|
+
* 获取整体压缩比
|
|
425
|
+
*/
|
|
426
|
+
private getOverallCompressionRatio(): string {
|
|
427
|
+
if (this.totalOriginalSize === 0) return '0%';
|
|
428
|
+
const ratio = ((this.totalOriginalSize - this.totalCompressedSize) / this.totalOriginalSize * 100);
|
|
429
|
+
return `${ratio.toFixed(2)}%`;
|
|
430
|
+
}
|
|
264
431
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
432
|
+
/**
|
|
433
|
+
* 将 ArrayBuffer 转换为 Base64 字符串
|
|
434
|
+
*/
|
|
435
|
+
private arrayBufferToBase64(buffer: Uint8Array): string {
|
|
436
|
+
let binary = '';
|
|
437
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
438
|
+
binary += String.fromCharCode(buffer[i]);
|
|
439
|
+
}
|
|
440
|
+
return btoa(binary);
|
|
441
|
+
}
|
|
270
442
|
|
|
271
|
-
|
|
443
|
+
/**
|
|
444
|
+
* 将 Base64 字符串转换为 Uint8Array
|
|
445
|
+
*/
|
|
446
|
+
private base64ToArrayBuffer(base64: string): Uint8Array {
|
|
447
|
+
const binary = atob(base64);
|
|
448
|
+
const buffer = new Uint8Array(binary.length);
|
|
449
|
+
for (let i = 0; i < binary.length; i++) {
|
|
450
|
+
buffer[i] = binary.charCodeAt(i);
|
|
451
|
+
}
|
|
452
|
+
return buffer;
|
|
272
453
|
}
|
|
273
454
|
|
|
274
455
|
/**
|
|
@@ -327,9 +508,24 @@ export class CheckpointManager {
|
|
|
327
508
|
const allCheckpoints = this.checkpointsSubject.value;
|
|
328
509
|
|
|
329
510
|
const totalCheckpoints = allCheckpoints.length;
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
511
|
+
|
|
512
|
+
// 统计归档和压缩的检查点
|
|
513
|
+
let archivedCount = 0;
|
|
514
|
+
let compressedCount = 0;
|
|
515
|
+
allCheckpoints.forEach(cp => {
|
|
516
|
+
// 检查是否已归档
|
|
517
|
+
if (cp.isArchived) {
|
|
518
|
+
archivedCount++;
|
|
519
|
+
}
|
|
520
|
+
// 检查是否已压缩
|
|
521
|
+
if (cp.compressedData?.compressed) {
|
|
522
|
+
compressedCount++;
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
const activeCheckpoints = totalCheckpoints - archivedCount;
|
|
527
|
+
const archivedCheckpoints = archivedCount;
|
|
528
|
+
const compressedCheckpoints = compressedCount;
|
|
333
529
|
|
|
334
530
|
const totalMessages = allCheckpoints.reduce((sum, cp) => sum + cp.messages.length, 0);
|
|
335
531
|
const averageMessagesPerCheckpoint = totalCheckpoints > 0 ? totalMessages / totalCheckpoints : 0;
|
|
@@ -516,7 +712,11 @@ export class CheckpointManager {
|
|
|
516
712
|
}
|
|
517
713
|
|
|
518
714
|
if (filter.tags && filter.tags.length > 0) {
|
|
519
|
-
|
|
715
|
+
filtered = filtered.filter(cp => {
|
|
716
|
+
// 检查点需要至少有一个匹配的标签
|
|
717
|
+
const cpTags = cp.tags || [];
|
|
718
|
+
return filter.tags!.some(tag => cpTags.includes(tag));
|
|
719
|
+
});
|
|
520
720
|
}
|
|
521
721
|
|
|
522
722
|
return filtered;
|