tabby-ai-assistant 1.0.12 → 1.0.15

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 (47) hide show
  1. package/.editorconfig +18 -0
  2. package/README.md +113 -55
  3. package/dist/index.js +1 -1
  4. package/package.json +6 -4
  5. package/src/components/chat/ai-sidebar.component.scss +220 -9
  6. package/src/components/chat/ai-sidebar.component.ts +364 -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 +0 -6
  14. package/src/providers/tabby/ai-toolbar-button.provider.ts +7 -3
  15. package/src/services/chat/ai-sidebar.service.ts +414 -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/config-provider.service.ts +4 -12
  25. package/src/services/core/toast.service.ts +106 -106
  26. package/src/services/providers/anthropic-provider.service.ts +126 -202
  27. package/src/services/providers/base-provider.service.ts +315 -21
  28. package/src/services/providers/glm-provider.service.ts +151 -233
  29. package/src/services/providers/minimax-provider.service.ts +55 -238
  30. package/src/services/providers/ollama-provider.service.ts +117 -188
  31. package/src/services/providers/openai-compatible.service.ts +165 -177
  32. package/src/services/providers/openai-provider.service.ts +170 -177
  33. package/src/services/providers/vllm-provider.service.ts +116 -188
  34. package/src/services/terminal/terminal-context.service.ts +265 -5
  35. package/src/services/terminal/terminal-manager.service.ts +748 -748
  36. package/src/services/terminal/terminal-tools.service.ts +612 -441
  37. package/src/types/ai.types.ts +156 -3
  38. package/src/types/provider.types.ts +206 -75
  39. package/src/utils/cost.utils.ts +249 -0
  40. package/src/utils/validation.utils.ts +306 -2
  41. package/dist/index.js.LICENSE.txt +0 -18
  42. package/src/index.ts.backup +0 -165
  43. package/src/services/chat/chat-history.service.ts.backup +0 -239
  44. package/src/services/terminal/command-analyzer.service.ts +0 -43
  45. package/src/services/terminal/context-menu.service.ts +0 -45
  46. package/src/services/terminal/hotkey.service.ts +0 -53
  47. package/webpack.config.js.backup +0 -57
@@ -1,15 +1,15 @@
1
1
  import { Injectable } from '@angular/core';
2
- import { Observable } from 'rxjs';
3
- import { BaseAiProvider as IBaseAiProvider, ProviderConfig, AuthConfig, ProviderCapability, HealthStatus, ValidationResult } from '../../types/provider.types';
4
- import { ChatRequest, ChatResponse, CommandRequest, CommandResponse, ExplainRequest, ExplainResponse, AnalysisRequest, AnalysisResponse, StreamEvent } from '../../types/ai.types';
2
+ import { Observable, of } from 'rxjs';
3
+ import { IBaseAiProvider, ProviderConfig, AuthConfig, ProviderCapability, HealthStatus, ValidationResult, ProviderInfo, PROVIDER_DEFAULTS } from '../../types/provider.types';
4
+ import { ChatRequest, ChatResponse, CommandRequest, CommandResponse, ExplainRequest, ExplainResponse, AnalysisRequest, AnalysisResponse, StreamEvent, MessageRole } from '../../types/ai.types';
5
5
  import { LoggerService } from '../core/logger.service';
6
6
 
7
7
  /**
8
8
  * 基础AI提供商抽象类
9
- * 所有AI提供商都应该继承此类
9
+ * 所有AI提供商都应该实现此接口
10
10
  */
11
11
  @Injectable()
12
- export abstract class BaseAiProvider extends IBaseAiProvider {
12
+ export abstract class BaseAiProvider implements IBaseAiProvider {
13
13
  abstract readonly name: string;
14
14
  abstract readonly displayName: string;
15
15
  abstract readonly capabilities: ProviderCapability[];
@@ -19,9 +19,7 @@ export abstract class BaseAiProvider extends IBaseAiProvider {
19
19
  protected isInitialized = false;
20
20
  protected lastHealthCheck: { status: HealthStatus; timestamp: Date } | null = null;
21
21
 
22
- constructor(protected logger: LoggerService) {
23
- super();
24
- }
22
+ constructor(protected logger: LoggerService) {}
25
23
 
26
24
  /**
27
25
  * 配置提供商
@@ -62,16 +60,22 @@ export abstract class BaseAiProvider extends IBaseAiProvider {
62
60
  */
63
61
  async healthCheck(): Promise<HealthStatus> {
64
62
  try {
65
- // 简单的健康检查:验证配置和连接
63
+ // 1. 验证配置
66
64
  const validation = this.validateConfig();
67
65
  if (!validation.valid) {
68
66
  this.lastHealthCheck = { status: HealthStatus.UNHEALTHY, timestamp: new Date() };
69
67
  return HealthStatus.UNHEALTHY;
70
68
  }
71
69
 
72
- // TODO: 实际的网络健康检查
73
- this.lastHealthCheck = { status: HealthStatus.HEALTHY, timestamp: new Date() };
74
- return HealthStatus.HEALTHY;
70
+ // 2. 执行网络健康检查
71
+ const networkStatus = await this.performNetworkHealthCheck();
72
+
73
+ this.lastHealthCheck = {
74
+ status: networkStatus,
75
+ timestamp: new Date()
76
+ };
77
+
78
+ return networkStatus;
75
79
 
76
80
  } catch (error) {
77
81
  this.logger.error(`Health check failed for ${this.name}`, error);
@@ -80,6 +84,64 @@ export abstract class BaseAiProvider extends IBaseAiProvider {
80
84
  }
81
85
  }
82
86
 
87
+ /**
88
+ * 执行网络健康检查 - 发送测试请求
89
+ */
90
+ protected async performNetworkHealthCheck(): Promise<HealthStatus> {
91
+ try {
92
+ // 发送一个简单的测试请求
93
+ const testRequest: ChatRequest = {
94
+ messages: [{
95
+ id: 'health-check',
96
+ role: MessageRole.USER,
97
+ content: 'Hi',
98
+ timestamp: new Date()
99
+ }],
100
+ maxTokens: 1,
101
+ temperature: 0
102
+ };
103
+
104
+ const response = await this.withRetry(() => this.sendTestRequest(testRequest));
105
+
106
+ if (this.isSuccessfulResponse(response)) {
107
+ return HealthStatus.HEALTHY;
108
+ } else {
109
+ this.logger.warn(`Health check returned unsuccessful response for ${this.name}`, {
110
+ response: this.sanitizeResponse(response)
111
+ });
112
+ return HealthStatus.DEGRADED;
113
+ }
114
+
115
+ } catch (error) {
116
+ const errorMessage = error instanceof Error ? error.message : String(error);
117
+
118
+ // 根据错误类型判断状态
119
+ if (errorMessage.includes('timeout') || errorMessage.includes('Timed out')) {
120
+ this.logger.warn(`Health check timed out for ${this.name}`);
121
+ return HealthStatus.DEGRADED;
122
+ }
123
+
124
+ if (errorMessage.includes('401') || errorMessage.includes('Unauthorized') ||
125
+ errorMessage.includes('invalid') || errorMessage.includes('API key')) {
126
+ this.logger.warn(`Health check authentication failed for ${this.name}`);
127
+ return HealthStatus.UNHEALTHY;
128
+ }
129
+
130
+ if (errorMessage.includes('429') || errorMessage.includes('rate limit')) {
131
+ this.logger.warn(`Health check rate limited for ${this.name}`);
132
+ return HealthStatus.DEGRADED;
133
+ }
134
+
135
+ this.logger.error(`Health check network error for ${this.name}`, error);
136
+ return HealthStatus.UNHEALTHY;
137
+ }
138
+ }
139
+
140
+ /**
141
+ * 发送测试请求 - 子类必须实现
142
+ */
143
+ protected abstract sendTestRequest(request: ChatRequest): Promise<ChatResponse>;
144
+
83
145
  /**
84
146
  * 验证配置 - 默认实现,子类可以重写
85
147
  */
@@ -135,7 +197,7 @@ export abstract class BaseAiProvider extends IBaseAiProvider {
135
197
  /**
136
198
  * 获取提供商信息
137
199
  */
138
- getInfo(): any {
200
+ getInfo(): ProviderInfo {
139
201
  return {
140
202
  name: this.name,
141
203
  displayName: this.displayName,
@@ -145,7 +207,7 @@ export abstract class BaseAiProvider extends IBaseAiProvider {
145
207
  authConfig: this.authConfig,
146
208
  supportedModels: this.config?.model ? [this.config.model] : [],
147
209
  configured: this.isInitialized,
148
- lastHealthCheck: this.lastHealthCheck
210
+ lastHealthCheck: this.lastHealthCheck ?? undefined
149
211
  };
150
212
  }
151
213
 
@@ -196,7 +258,10 @@ export abstract class BaseAiProvider extends IBaseAiProvider {
196
258
  break;
197
259
 
198
260
  case 'oauth':
199
- // TODO: 实现OAuth认证
261
+ // OAuth 认证预留
262
+ // 当前无提供商使用此认证方式
263
+ // 如需实现,可参考 OAuth 2.0 Authorization Code Flow
264
+ this.logger.debug('OAuth authentication not implemented');
200
265
  break;
201
266
  }
202
267
 
@@ -306,19 +371,40 @@ export abstract class BaseAiProvider extends IBaseAiProvider {
306
371
  * 获取基础URL
307
372
  */
308
373
  protected getBaseURL(): string {
309
- return this.config?.baseURL || this.getDefaultBaseURL();
374
+ if (this.config?.baseURL) {
375
+ return this.config.baseURL;
376
+ }
377
+ // 从统一默认值获取
378
+ const defaults = PROVIDER_DEFAULTS[this.name];
379
+ return defaults?.baseURL || '';
310
380
  }
311
381
 
312
382
  /**
313
- * 获取默认基础URL - 子类必须实现
383
+ * 获取默认模型
314
384
  */
315
- protected abstract getDefaultBaseURL(): string;
385
+ protected getDefaultModel(): string {
386
+ if (this.config?.model) {
387
+ return this.config.model;
388
+ }
389
+ // 从统一默认值获取
390
+ const defaults = PROVIDER_DEFAULTS[this.name];
391
+ return defaults?.model || 'default';
392
+ }
316
393
 
317
394
  /**
318
- * 获取默认模型 - 子类可以重写
395
+ * 获取默认超时时间
319
396
  */
320
- protected getDefaultModel(): string {
321
- return this.config?.model || 'default';
397
+ protected getDefaultTimeout(): number {
398
+ const defaults = PROVIDER_DEFAULTS[this.name];
399
+ return defaults?.timeout || 30000;
400
+ }
401
+
402
+ /**
403
+ * 获取默认重试次数
404
+ */
405
+ protected getDefaultRetries(): number {
406
+ const defaults = PROVIDER_DEFAULTS[this.name];
407
+ return defaults?.retries || 3;
322
408
  }
323
409
 
324
410
  /**
@@ -372,4 +458,212 @@ export abstract class BaseAiProvider extends IBaseAiProvider {
372
458
  }
373
459
  return 'Unknown error';
374
460
  }
461
+
462
+ // ==================== 通用命令处理方法 ====================
463
+
464
+ /**
465
+ * 构建命令生成提示 - 通用实现
466
+ */
467
+ protected buildCommandPrompt(request: CommandRequest): string {
468
+ let prompt = `请将以下自然语言描述转换为准确的终端命令:\n\n"${request.naturalLanguage}"\n\n`;
469
+
470
+ if (request.context) {
471
+ prompt += `当前环境:\n`;
472
+ if (request.context.currentDirectory) {
473
+ prompt += `- 当前目录:${request.context.currentDirectory}\n`;
474
+ }
475
+ if (request.context.operatingSystem) {
476
+ prompt += `- 操作系统:${request.context.operatingSystem}\n`;
477
+ }
478
+ if (request.context.shell) {
479
+ prompt += `- Shell:${request.context.shell}\n`;
480
+ }
481
+ }
482
+
483
+ prompt += `\n请直接返回JSON格式:\n`;
484
+ prompt += `{\n`;
485
+ prompt += ` "command": "具体命令",\n`;
486
+ prompt += ` "explanation": "命令解释",\n`;
487
+ prompt += ` "confidence": 0.95\n`;
488
+ prompt += `}\n`;
489
+
490
+ return prompt;
491
+ }
492
+
493
+ /**
494
+ * 构建命令解释提示 - 通用实现
495
+ */
496
+ protected buildExplainPrompt(request: ExplainRequest): string {
497
+ let prompt = `请详细解释以下终端命令:\n\n\`${request.command}\`\n\n`;
498
+
499
+ if (request.context?.currentDirectory) {
500
+ prompt += `当前目录:${request.context.currentDirectory}\n`;
501
+ }
502
+ if (request.context?.operatingSystem) {
503
+ prompt += `操作系统:${request.context.operatingSystem}\n`;
504
+ }
505
+
506
+ prompt += `\n请按以下JSON格式返回:\n`;
507
+ prompt += `{\n`;
508
+ prompt += ` "explanation": "整体解释",\n`;
509
+ prompt += ` "breakdown": [\n`;
510
+ prompt += ` {"part": "命令部分", "description": "说明"}\n`;
511
+ prompt += ` ],\n`;
512
+ prompt += ` "examples": ["使用示例"]\n`;
513
+ prompt += `}\n`;
514
+
515
+ return prompt;
516
+ }
517
+
518
+ /**
519
+ * 构建结果分析提示 - 通用实现
520
+ */
521
+ protected buildAnalysisPrompt(request: AnalysisRequest): string {
522
+ let prompt = `请分析以下命令执行结果:\n\n`;
523
+ prompt += `命令:${request.command}\n`;
524
+ prompt += `退出码:${request.exitCode}\n`;
525
+ prompt += `输出:\n${request.output}\n\n`;
526
+
527
+ if (request.context?.workingDirectory) {
528
+ prompt += `工作目录:${request.context.workingDirectory}\n`;
529
+ }
530
+
531
+ prompt += `\n请按以下JSON格式返回:\n`;
532
+ prompt += `{\n`;
533
+ prompt += ` "summary": "结果总结",\n`;
534
+ prompt += ` "insights": ["洞察1", "洞察2"],\n`;
535
+ prompt += ` "success": true/false,\n`;
536
+ prompt += ` "issues": [\n`;
537
+ prompt += ` {"severity": "warning|error|info", "message": "问题描述", "suggestion": "建议"}\n`;
538
+ prompt += ` ]\n`;
539
+ prompt += `}\n`;
540
+
541
+ return prompt;
542
+ }
543
+
544
+ /**
545
+ * 解析命令响应 - 通用实现
546
+ */
547
+ protected parseCommandResponse(content: string): CommandResponse {
548
+ try {
549
+ const match = content.match(/\{[\s\S]*\}/);
550
+ if (match) {
551
+ const parsed = JSON.parse(match[0]);
552
+ return {
553
+ command: parsed.command || '',
554
+ explanation: parsed.explanation || '',
555
+ confidence: parsed.confidence || 0.5
556
+ };
557
+ }
558
+ } catch (error) {
559
+ this.logger.warn('Failed to parse command response as JSON', error);
560
+ }
561
+
562
+ // 备用解析
563
+ const lines = content.split('\n').map(l => l.trim()).filter(l => l);
564
+ return {
565
+ command: lines[0] || '',
566
+ explanation: lines.slice(1).join(' ') || 'AI生成的命令',
567
+ confidence: 0.5
568
+ };
569
+ }
570
+
571
+ /**
572
+ * 解析解释响应 - 通用实现
573
+ */
574
+ protected parseExplainResponse(content: string): ExplainResponse {
575
+ try {
576
+ const match = content.match(/\{[\s\S]*\}/);
577
+ if (match) {
578
+ const parsed = JSON.parse(match[0]);
579
+ return {
580
+ explanation: parsed.explanation || '',
581
+ breakdown: parsed.breakdown || [],
582
+ examples: parsed.examples || []
583
+ };
584
+ }
585
+ } catch (error) {
586
+ this.logger.warn('Failed to parse explain response as JSON', error);
587
+ }
588
+
589
+ return {
590
+ explanation: content,
591
+ breakdown: []
592
+ };
593
+ }
594
+
595
+ /**
596
+ * 解析分析响应 - 通用实现
597
+ */
598
+ protected parseAnalysisResponse(content: string): AnalysisResponse {
599
+ try {
600
+ const match = content.match(/\{[\s\S]*\}/);
601
+ if (match) {
602
+ const parsed = JSON.parse(match[0]);
603
+ return {
604
+ summary: parsed.summary || '',
605
+ insights: parsed.insights || [],
606
+ success: parsed.success !== false,
607
+ issues: parsed.issues || []
608
+ };
609
+ }
610
+ } catch (error) {
611
+ this.logger.warn('Failed to parse analysis response as JSON', error);
612
+ }
613
+
614
+ return {
615
+ summary: content,
616
+ insights: [],
617
+ success: true
618
+ };
619
+ }
620
+
621
+ /**
622
+ * 获取默认系统提示 - 子类可重写
623
+ */
624
+ protected getDefaultSystemPrompt(): string {
625
+ return `你是一个专业的终端命令助手,运行在 Tabby 终端中。
626
+
627
+ ## 核心能力
628
+ 你可以通过以下工具直接操作终端:
629
+ - write_to_terminal: 向终端写入并执行命令
630
+ - read_terminal_output: 读取终端输出
631
+ - get_terminal_list: 获取所有终端列表
632
+ - get_terminal_cwd: 获取当前工作目录
633
+ - focus_terminal: 切换到指定索引的终端(需要参数 terminal_index)
634
+ - get_terminal_selection: 获取终端中选中的文本
635
+
636
+ ## 重要规则
637
+ 1. 当用户请求执行命令(如"查看当前目录"、"列出文件"等),你必须使用 write_to_terminal 工具来执行
638
+ 2. **当用户请求切换终端(如"切换到终端0"、"打开终端4"等),你必须使用 focus_terminal 工具**
639
+ 3. 不要只是描述你"将要做什么",而是直接调用工具执行
640
+ 4. 执行命令后,使用 read_terminal_output 读取结果并报告给用户
641
+ 5. 如果不确定当前目录或终端状态,先使用 get_terminal_cwd 或 get_terminal_list 获取信息
642
+ 6. **永远不要假装执行了操作,必须真正调用工具**
643
+
644
+ ## 命令执行策略
645
+ ### 快速命令(无需额外等待)
646
+ - dir, ls, cd, pwd, echo, cat, type, mkdir, rm, copy, move
647
+ - 这些命令通常在 500ms 内完成
648
+
649
+ ### 慢速命令(需要等待完整输出)
650
+ - systeminfo, ipconfig, netstat: 等待 3-8 秒
651
+ - npm, yarn, pip, docker: 等待 5-10 秒
652
+ - git: 等待 3 秒以上
653
+ - ping, tracert: 可能需要 10+ 秒
654
+
655
+ **对于慢速命令**:
656
+ 1. 执行命令后,系统会自动等待
657
+ 2. 如果输出不完整,可以再次调用 read_terminal_output 获取更新的内容
658
+ 3. **不要猜测或假设命令输出,始终以实际读取到的输出为准**
659
+
660
+ ## 示例
661
+ 用户:"查看当前目录的文件"
662
+ 正确做法:调用 write_to_terminal 工具,参数 { "command": "dir", "execute": true }
663
+ 错误做法:仅回复文字"我将执行 dir 命令"
664
+
665
+ 用户:"切换到终端4"
666
+ 正确做法:调用 focus_terminal 工具,参数 { "terminal_index": 4 }
667
+ 错误做法:仅回复文字"已切换到终端4"(不调用工具)`;
668
+ }
375
669
  }