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.
Files changed (42) hide show
  1. package/.editorconfig +18 -0
  2. package/README.md +40 -10
  3. package/dist/index.js +1 -1
  4. package/package.json +5 -3
  5. package/src/components/chat/ai-sidebar.component.scss +220 -9
  6. package/src/components/chat/ai-sidebar.component.ts +379 -29
  7. package/src/components/chat/chat-input.component.ts +36 -4
  8. package/src/components/chat/chat-interface.component.ts +225 -5
  9. package/src/components/chat/chat-message.component.ts +6 -1
  10. package/src/components/settings/context-settings.component.ts +91 -91
  11. package/src/components/terminal/ai-toolbar-button.component.ts +4 -2
  12. package/src/components/terminal/command-suggestion.component.ts +148 -6
  13. package/src/index.ts +81 -19
  14. package/src/providers/tabby/ai-toolbar-button.provider.ts +7 -3
  15. package/src/services/chat/ai-sidebar.service.ts +448 -410
  16. package/src/services/chat/chat-session.service.ts +36 -12
  17. package/src/services/context/compaction.ts +110 -134
  18. package/src/services/context/manager.ts +27 -7
  19. package/src/services/context/memory.ts +17 -33
  20. package/src/services/context/summary.service.ts +136 -0
  21. package/src/services/core/ai-assistant.service.ts +1060 -37
  22. package/src/services/core/ai-provider-manager.service.ts +154 -25
  23. package/src/services/core/checkpoint.service.ts +218 -18
  24. package/src/services/core/toast.service.ts +106 -106
  25. package/src/services/providers/anthropic-provider.service.ts +126 -30
  26. package/src/services/providers/base-provider.service.ts +90 -7
  27. package/src/services/providers/glm-provider.service.ts +151 -38
  28. package/src/services/providers/minimax-provider.service.ts +55 -40
  29. package/src/services/providers/ollama-provider.service.ts +117 -28
  30. package/src/services/providers/openai-compatible.service.ts +164 -34
  31. package/src/services/providers/openai-provider.service.ts +169 -34
  32. package/src/services/providers/vllm-provider.service.ts +116 -28
  33. package/src/services/terminal/terminal-context.service.ts +265 -5
  34. package/src/services/terminal/terminal-manager.service.ts +845 -748
  35. package/src/services/terminal/terminal-tools.service.ts +612 -441
  36. package/src/types/ai.types.ts +156 -3
  37. package/src/utils/cost.utils.ts +249 -0
  38. package/src/utils/validation.utils.ts +306 -2
  39. package/dist/index.js.LICENSE.txt +0 -18
  40. package/src/services/terminal/command-analyzer.service.ts +0 -43
  41. package/src/services/terminal/context-menu.service.ts +0 -45
  42. 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: string; latency?: number }[]> {
180
- const results: { provider: string; status: string; latency?: number }[] = [];
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
- for (const [name, provider] of this.providers) {
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
- results.push({
189
- provider: name,
190
- status: health,
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
- name: p.name,
300
- enabled: p.getConfig()?.enabled !== false,
301
- healthy: true // TODO: 实现健康检查缓存
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
- // TODO: 实现压缩逻辑
254
- // 这里应该使用 ContextManager 进行压缩
255
- const compressedMessages = checkpoint.messages; // 临时实现
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
- const compressedCheckpoint: Checkpoint = {
258
- ...checkpoint,
259
- messages: compressedMessages,
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
- this.updateCheckpoint(compressedCheckpoint);
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
- this.logger.info('Checkpoint compressed', {
266
- checkpointId,
267
- originalSize: checkpoint.messages.length,
268
- compressedSize: compressedMessages.length
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
- return compressedCheckpoint;
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
- const activeCheckpoints = totalCheckpoints; // 简化实现
331
- const archivedCheckpoints = 0; // TODO: 实现状态跟踪
332
- const compressedCheckpoints = 0; // TODO: 实现压缩状态跟踪
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
- // TODO: 实现标签过滤
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;