tabby-ai-assistant 1.0.9 → 1.0.11

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 (103) hide show
  1. package/dist/index.js +1 -1
  2. package/package.json +3 -3
  3. package/src/components/chat/ai-sidebar.component.scss +43 -0
  4. package/src/components/chat/ai-sidebar.component.ts +43 -1
  5. package/src/components/settings/ai-settings-tab.component.html +13 -5
  6. package/src/components/settings/ai-settings-tab.component.scss +164 -33
  7. package/src/components/settings/ai-settings-tab.component.ts +1 -0
  8. package/src/components/settings/context-settings.component.html +87 -0
  9. package/src/components/settings/context-settings.component.scss +133 -0
  10. package/src/components/settings/context-settings.component.ts +91 -0
  11. package/src/components/settings/provider-config.component.html +46 -12
  12. package/src/components/settings/provider-config.component.scss +59 -0
  13. package/src/components/settings/provider-config.component.ts +112 -15
  14. package/src/i18n/translations/en-US.ts +27 -1
  15. package/src/i18n/translations/ja-JP.ts +27 -1
  16. package/src/i18n/translations/zh-CN.ts +27 -1
  17. package/src/i18n/types.ts +28 -0
  18. package/src/index.ts +6 -0
  19. package/src/services/chat/chat-session.service.ts +91 -2
  20. package/src/services/context/manager.ts +33 -2
  21. package/src/services/core/ai-assistant.service.ts +35 -11
  22. package/src/services/core/config-provider.service.ts +66 -0
  23. package/src/services/core/toast.service.ts +36 -0
  24. package/src/services/providers/anthropic-provider.service.ts +12 -4
  25. package/src/services/providers/glm-provider.service.ts +12 -4
  26. package/src/services/providers/minimax-provider.service.ts +12 -4
  27. package/src/types/ai.types.ts +3 -3
  28. package/src/types/provider.types.ts +1 -0
  29. package/dist/components/chat/ai-sidebar.component.d.ts +0 -160
  30. package/dist/components/chat/chat-input.component.d.ts +0 -69
  31. package/dist/components/chat/chat-interface.component.d.ts +0 -124
  32. package/dist/components/chat/chat-message.component.d.ts +0 -53
  33. package/dist/components/chat/chat-settings.component.d.ts +0 -72
  34. package/dist/components/common/error-message.component.d.ts +0 -11
  35. package/dist/components/common/loading-spinner.component.d.ts +0 -4
  36. package/dist/components/security/consent-dialog.component.d.ts +0 -11
  37. package/dist/components/security/password-prompt.component.d.ts +0 -10
  38. package/dist/components/security/risk-confirm-dialog.component.d.ts +0 -36
  39. package/dist/components/settings/ai-settings-tab.component.d.ts +0 -82
  40. package/dist/components/settings/general-settings.component.d.ts +0 -94
  41. package/dist/components/settings/provider-config.component.d.ts +0 -273
  42. package/dist/components/settings/security-settings.component.d.ts +0 -33
  43. package/dist/components/terminal/ai-toolbar-button.component.d.ts +0 -10
  44. package/dist/components/terminal/command-preview.component.d.ts +0 -53
  45. package/dist/components/terminal/command-suggestion.component.d.ts +0 -16
  46. package/dist/i18n/index.d.ts +0 -48
  47. package/dist/i18n/translations/en-US.d.ts +0 -5
  48. package/dist/i18n/translations/ja-JP.d.ts +0 -5
  49. package/dist/i18n/translations/zh-CN.d.ts +0 -5
  50. package/dist/i18n/types.d.ts +0 -198
  51. package/dist/index-full.d.ts +0 -8
  52. package/dist/index-minimal.d.ts +0 -3
  53. package/dist/index.d.ts +0 -12
  54. package/dist/main.d.ts +0 -8
  55. package/dist/providers/tabby/ai-config.provider.d.ts +0 -70
  56. package/dist/providers/tabby/ai-hotkey.provider.d.ts +0 -15
  57. package/dist/providers/tabby/ai-settings-tab.provider.d.ts +0 -11
  58. package/dist/providers/tabby/ai-toolbar-button.provider.d.ts +0 -16
  59. package/dist/services/chat/ai-sidebar.service.d.ts +0 -111
  60. package/dist/services/chat/chat-history.service.d.ts +0 -145
  61. package/dist/services/chat/chat-session.service.d.ts +0 -113
  62. package/dist/services/chat/command-generator.service.d.ts +0 -49
  63. package/dist/services/context/compaction.d.ts +0 -90
  64. package/dist/services/context/manager.d.ts +0 -69
  65. package/dist/services/context/memory.d.ts +0 -116
  66. package/dist/services/context/token-budget.d.ts +0 -105
  67. package/dist/services/core/ai-assistant.service.d.ts +0 -127
  68. package/dist/services/core/ai-provider-manager.service.d.ts +0 -119
  69. package/dist/services/core/checkpoint.service.d.ts +0 -130
  70. package/dist/services/core/config-provider.service.d.ts +0 -137
  71. package/dist/services/core/logger.service.d.ts +0 -21
  72. package/dist/services/core/theme.service.d.ts +0 -53
  73. package/dist/services/platform/escape-sequence.service.d.ts +0 -132
  74. package/dist/services/platform/platform-detection.service.d.ts +0 -146
  75. package/dist/services/providers/anthropic-provider.service.d.ts +0 -44
  76. package/dist/services/providers/base-provider.service.d.ts +0 -142
  77. package/dist/services/providers/glm-provider.service.d.ts +0 -96
  78. package/dist/services/providers/minimax-provider.service.d.ts +0 -102
  79. package/dist/services/providers/ollama-provider.service.d.ts +0 -76
  80. package/dist/services/providers/openai-compatible.service.d.ts +0 -44
  81. package/dist/services/providers/openai-provider.service.d.ts +0 -43
  82. package/dist/services/providers/vllm-provider.service.d.ts +0 -82
  83. package/dist/services/security/consent-manager.service.d.ts +0 -65
  84. package/dist/services/security/password-manager.service.d.ts +0 -67
  85. package/dist/services/security/risk-assessment.service.d.ts +0 -65
  86. package/dist/services/security/security-validator.service.d.ts +0 -36
  87. package/dist/services/terminal/buffer-analyzer.service.d.ts +0 -128
  88. package/dist/services/terminal/command-analyzer.service.d.ts +0 -20
  89. package/dist/services/terminal/context-menu.service.d.ts +0 -24
  90. package/dist/services/terminal/hotkey.service.d.ts +0 -28
  91. package/dist/services/terminal/terminal-context.service.d.ts +0 -100
  92. package/dist/services/terminal/terminal-manager.service.d.ts +0 -185
  93. package/dist/services/terminal/terminal-tools.service.d.ts +0 -79
  94. package/dist/types/ai.types.d.ts +0 -199
  95. package/dist/types/provider.types.d.ts +0 -105
  96. package/dist/types/security.types.d.ts +0 -85
  97. package/dist/types/terminal.types.d.ts +0 -150
  98. package/dist/utils/encryption.utils.d.ts +0 -83
  99. package/dist/utils/formatting.utils.d.ts +0 -106
  100. package/dist/utils/validation.utils.d.ts +0 -83
  101. package/jest.config.js +0 -47
  102. package/webpack-docker.config.js +0 -42
  103. package/webpack.config.js +0 -81
@@ -0,0 +1,91 @@
1
+ import { Component, OnInit, OnDestroy, ViewEncapsulation } from '@angular/core';
2
+ import { Subject } from 'rxjs';
3
+ import { takeUntil } from 'rxjs/operators';
4
+ import { ConfigProviderService } from '../../services/core/config-provider.service';
5
+ import { ContextManager } from '../../services/context/manager';
6
+ import { ToastService } from '../../services/core/toast.service';
7
+ import { TranslateService } from '../../i18n';
8
+ import { ContextConfig, DEFAULT_CONTEXT_CONFIG } from '../../types/ai.types';
9
+
10
+ @Component({
11
+ selector: 'app-context-settings',
12
+ templateUrl: './context-settings.component.html',
13
+ styleUrls: ['./context-settings.component.scss'],
14
+ encapsulation: ViewEncapsulation.None
15
+ })
16
+ export class ContextSettingsComponent implements OnInit, OnDestroy {
17
+ // 配置项
18
+ config: ContextConfig = { ...DEFAULT_CONTEXT_CONFIG };
19
+ autoCompactEnabled = true;
20
+
21
+ // 当前供应商的上下文限制
22
+ activeProviderContextWindow: number = 200000;
23
+
24
+ // 翻译对象
25
+ t: any;
26
+
27
+ private destroy$ = new Subject<void>();
28
+
29
+ constructor(
30
+ private configService: ConfigProviderService,
31
+ private contextManager: ContextManager,
32
+ private toast: ToastService,
33
+ private translate: TranslateService
34
+ ) {
35
+ this.t = this.translate.t;
36
+ }
37
+
38
+ ngOnInit(): void {
39
+ this.translate.translation$.pipe(
40
+ takeUntil(this.destroy$)
41
+ ).subscribe(translation => {
42
+ this.t = translation;
43
+ });
44
+
45
+ this.loadConfig();
46
+ }
47
+
48
+ ngOnDestroy(): void {
49
+ this.destroy$.next();
50
+ this.destroy$.complete();
51
+ }
52
+
53
+ loadConfig(): void {
54
+ const savedConfig = this.configService.getContextConfig();
55
+ if (savedConfig) {
56
+ this.config = { ...DEFAULT_CONTEXT_CONFIG, ...savedConfig };
57
+ }
58
+ this.autoCompactEnabled = this.configService.isAutoCompactEnabled();
59
+
60
+ // 获取当前供应商的上下文限制
61
+ this.activeProviderContextWindow = this.configService.getActiveProviderContextWindow();
62
+
63
+ // 确保配置的 maxContextTokens 不超过供应商限制
64
+ if (this.config.maxContextTokens > this.activeProviderContextWindow) {
65
+ this.config.maxContextTokens = this.activeProviderContextWindow;
66
+ }
67
+ }
68
+
69
+ saveConfig(): void {
70
+ this.configService.setContextConfig(this.config);
71
+ this.contextManager.updateConfig(this.config);
72
+ this.toast.success(this.t?.contextSettings?.configSaved || '上下文配置已保存');
73
+ }
74
+
75
+ toggleAutoCompact(): void {
76
+ this.autoCompactEnabled = !this.autoCompactEnabled;
77
+ this.configService.setAutoCompactEnabled(this.autoCompactEnabled);
78
+ this.toast.info(
79
+ this.autoCompactEnabled
80
+ ? (this.t?.contextSettings?.autoCompactEnabled || '自动压缩已启用')
81
+ : (this.t?.contextSettings?.autoCompactDisabled || '自动压缩已禁用')
82
+ );
83
+ }
84
+
85
+ resetToDefaults(): void {
86
+ this.config = { ...DEFAULT_CONTEXT_CONFIG };
87
+ this.autoCompactEnabled = true;
88
+ this.saveConfig();
89
+ this.toast.info(this.t?.common?.resetToDefault || '已重置为默认配置');
90
+ }
91
+ }
@@ -37,7 +37,7 @@
37
37
  <div class="form-row">
38
38
  <div class="form-group">
39
39
  <label>{{ t.providers.displayName }}</label>
40
- <input type="text" class="form-control" [(ngModel)]="configs[providerName].displayName">
40
+ <input type="text" class="form-control" [(ngModel)]="configs[providerName].displayName" *ngIf="configs[providerName]">
41
41
  </div>
42
42
  <div class="form-group">
43
43
  <label>{{ t.providers.status }}</label>
@@ -57,11 +57,28 @@
57
57
  {{ t.providers[field.key] || field.label }}
58
58
  <span *ngIf="isRequired(field)" class="required">*</span>
59
59
  </label>
60
- <input *ngIf="getFieldType(field) === 'text' || getFieldType(field) === 'password'"
61
- [type]="isPasswordField(field) ? 'password' : 'text'"
62
- class="form-control"
63
- [placeholder]="field.placeholder || ''"
64
- [(ngModel)]="configs[providerName][field.key]">
60
+ <div class="input-with-toggle" *ngIf="configs[providerName] && (getFieldType(field) === 'text' || getFieldType(field) === 'password')">
61
+ <input
62
+ [type]="isPasswordField(field) && !isPasswordVisible(providerName, field.key) ? 'password' : 'text'"
63
+ class="form-control"
64
+ [class]="getInputValidationClass(providerName, field.key)"
65
+ [placeholder]="field.placeholder || ''"
66
+ [(ngModel)]="configs[providerName][field.key]">
67
+ <button *ngIf="isPasswordField(field)"
68
+ type="button"
69
+ class="btn-toggle-visibility"
70
+ (click)="togglePasswordVisibility(providerName, field.key)">
71
+ <i class="fa" [ngClass]="isPasswordVisible(providerName, field.key) ? 'fa-eye-slash' : 'fa-eye'"></i>
72
+ </button>
73
+ </div>
74
+ <!-- API Key 验证提示 -->
75
+ <div *ngIf="field.key === 'apiKey' && configs[providerName]?.[field.key]"
76
+ class="validation-feedback"
77
+ [class.valid]="validateApiKeyFormat(providerName, configs[providerName][field.key]).valid"
78
+ [class.invalid]="!validateApiKeyFormat(providerName, configs[providerName][field.key]).valid">
79
+ <i class="fa" [ngClass]="validateApiKeyFormat(providerName, configs[providerName][field.key]).valid ? 'fa-check-circle' : 'fa-exclamation-circle'"></i>
80
+ {{ validateApiKeyFormat(providerName, configs[providerName][field.key]).message || '格式正确' }}
81
+ </div>
65
82
  </div>
66
83
 
67
84
  <div class="form-actions">
@@ -121,7 +138,7 @@
121
138
  <div class="form-row">
122
139
  <div class="form-group">
123
140
  <label>{{ t.providers.displayName }}</label>
124
- <input type="text" class="form-control" [(ngModel)]="configs[providerName].displayName">
141
+ <input type="text" class="form-control" [(ngModel)]="configs[providerName].displayName" *ngIf="configs[providerName]">
125
142
  </div>
126
143
  <div class="form-group">
127
144
  <label>{{ t.providers.status }}</label>
@@ -141,11 +158,28 @@
141
158
  {{ t.providers[field.key] || field.label }}
142
159
  <span *ngIf="isRequired(field)" class="required">*</span>
143
160
  </label>
144
- <input *ngIf="getFieldType(field) === 'text' || getFieldType(field) === 'password'"
145
- [type]="isPasswordField(field) ? 'password' : 'text'"
146
- class="form-control"
147
- [placeholder]="field.placeholder || ''"
148
- [(ngModel)]="configs[providerName][field.key]">
161
+ <div class="input-with-toggle" *ngIf="configs[providerName] && (getFieldType(field) === 'text' || getFieldType(field) === 'password')">
162
+ <input
163
+ [type]="isPasswordField(field) && !isPasswordVisible(providerName, field.key) ? 'password' : 'text'"
164
+ class="form-control"
165
+ [class]="getInputValidationClass(providerName, field.key)"
166
+ [placeholder]="field.placeholder || ''"
167
+ [(ngModel)]="configs[providerName][field.key]">
168
+ <button *ngIf="isPasswordField(field)"
169
+ type="button"
170
+ class="btn-toggle-visibility"
171
+ (click)="togglePasswordVisibility(providerName, field.key)">
172
+ <i class="fa" [ngClass]="isPasswordVisible(providerName, field.key) ? 'fa-eye-slash' : 'fa-eye'"></i>
173
+ </button>
174
+ </div>
175
+ <!-- API Key 验证提示 -->
176
+ <div *ngIf="field.key === 'apiKey' && configs[providerName]?.[field.key]"
177
+ class="validation-feedback"
178
+ [class.valid]="validateApiKeyFormat(providerName, configs[providerName][field.key]).valid"
179
+ [class.invalid]="!validateApiKeyFormat(providerName, configs[providerName][field.key]).valid">
180
+ <i class="fa" [ngClass]="validateApiKeyFormat(providerName, configs[providerName][field.key]).valid ? 'fa-check-circle' : 'fa-exclamation-circle'"></i>
181
+ {{ validateApiKeyFormat(providerName, configs[providerName][field.key]).message || '格式正确' }}
182
+ </div>
149
183
  </div>
150
184
 
151
185
  <div class="form-actions">
@@ -302,6 +302,65 @@
302
302
  }
303
303
  }
304
304
 
305
+ /* 输入框密码显示切换 */
306
+ .input-with-toggle {
307
+ position: relative;
308
+ display: flex;
309
+ align-items: center;
310
+
311
+ .form-control {
312
+ padding-right: 40px;
313
+ }
314
+
315
+ .btn-toggle-visibility {
316
+ position: absolute;
317
+ right: 8px;
318
+ background: transparent;
319
+ border: none;
320
+ color: var(--ai-text-secondary);
321
+ cursor: pointer;
322
+ padding: 4px 8px;
323
+ display: flex;
324
+ align-items: center;
325
+ justify-content: center;
326
+
327
+ &:hover {
328
+ color: var(--ai-text-primary);
329
+ }
330
+ }
331
+ }
332
+
333
+ /* 表单验证状态 */
334
+ .form-control {
335
+ &.is-valid {
336
+ border-color: var(--ai-success, #4caf50);
337
+ }
338
+
339
+ &.is-invalid {
340
+ border-color: var(--ai-warning, #ff9800);
341
+ }
342
+ }
343
+
344
+ .validation-feedback {
345
+ font-size: 0.85em;
346
+ margin-top: 4px;
347
+ display: flex;
348
+ align-items: center;
349
+ gap: 4px;
350
+
351
+ &.valid {
352
+ color: var(--ai-success, #4caf50);
353
+ }
354
+
355
+ &.invalid {
356
+ color: var(--ai-warning, #ff9800);
357
+ }
358
+
359
+ i {
360
+ font-size: 0.9em;
361
+ }
362
+ }
363
+
305
364
  /* 响应式设计 */
306
365
  @media (max-width: 768px) {
307
366
  .providers-grid {
@@ -3,6 +3,7 @@ import { Subject } from 'rxjs';
3
3
  import { takeUntil } from 'rxjs/operators';
4
4
  import { ConfigProviderService } from '../../services/core/config-provider.service';
5
5
  import { LoggerService } from '../../services/core/logger.service';
6
+ import { ToastService } from '../../services/core/toast.service';
6
7
  import { TranslateService } from '../../i18n';
7
8
 
8
9
  @Component({
@@ -23,10 +24,19 @@ export class ProviderConfigComponent implements OnInit, OnDestroy {
23
24
  configs: { [key: string]: any } = {};
24
25
  expandedProvider: string = '';
25
26
  localStatus: { [key: string]: boolean } = {};
27
+ passwordVisibility: { [key: string]: { [fieldKey: string]: boolean } } = {};
26
28
 
27
29
  // 翻译对象
28
30
  t: any;
29
31
 
32
+ // API Key 格式校验规则
33
+ private apiKeyPatterns: { [key: string]: RegExp } = {
34
+ 'openai': /^sk-[a-zA-Z0-9]{32,}$/,
35
+ 'anthropic': /^sk-ant-[a-zA-Z0-9-]+$/,
36
+ 'minimax': /^[a-zA-Z0-9]{32,}$/,
37
+ 'glm': /^[a-zA-Z0-9._-]+$/
38
+ };
39
+
30
40
  private destroy$ = new Subject<void>();
31
41
 
32
42
  // 云端提供商模板
@@ -38,7 +48,8 @@ export class ProviderConfigComponent implements OnInit, OnDestroy {
38
48
  fields: [
39
49
  { key: 'apiKey', label: 'API Key', type: 'password', required: true },
40
50
  { key: 'baseURL', label: 'Base URL', type: 'text', default: 'https://api.openai.com/v1', required: false },
41
- { key: 'model', label: 'Model', type: 'text', default: 'gpt-4', required: false, placeholder: '例如: gpt-4, gpt-4-turbo, gpt-3.5-turbo' }
51
+ { key: 'model', label: 'Model', type: 'text', default: 'gpt-4', required: false, placeholder: '例如: gpt-4, gpt-4-turbo, gpt-3.5-turbo' },
52
+ { key: 'contextWindow', label: '上下文限制', type: 'number', default: 128000, required: false, placeholder: 'GPT-4: 128000, GPT-3.5: 16385' }
42
53
  ]
43
54
  },
44
55
  'anthropic': {
@@ -48,7 +59,8 @@ export class ProviderConfigComponent implements OnInit, OnDestroy {
48
59
  fields: [
49
60
  { key: 'apiKey', label: 'API Key', type: 'password', required: true },
50
61
  { key: 'baseURL', label: 'Base URL', type: 'text', default: 'https://api.anthropic.com', required: false },
51
- { key: 'model', label: 'Model', type: 'text', default: 'claude-3-sonnet-20240229', required: false, placeholder: '例如: claude-3-opus, claude-3-sonnet' }
62
+ { key: 'model', label: 'Model', type: 'text', default: 'claude-3-sonnet-20240229', required: false, placeholder: '例如: claude-3-opus, claude-3-sonnet' },
63
+ { key: 'contextWindow', label: '上下文限制', type: 'number', default: 200000, required: false, placeholder: 'Claude 3: 200000' }
52
64
  ]
53
65
  },
54
66
  'minimax': {
@@ -58,7 +70,8 @@ export class ProviderConfigComponent implements OnInit, OnDestroy {
58
70
  fields: [
59
71
  { key: 'apiKey', label: 'API Key', type: 'password', required: true },
60
72
  { key: 'baseURL', label: 'Base URL', type: 'text', default: 'https://api.minimaxi.com/anthropic', required: false },
61
- { key: 'model', label: 'Model', type: 'text', default: 'MiniMax-M2', required: false, placeholder: '例如: MiniMax-M2, MiniMax-M2.1' }
73
+ { key: 'model', label: 'Model', type: 'text', default: 'MiniMax-M2', required: false, placeholder: '例如: MiniMax-M2, MiniMax-M2.1' },
74
+ { key: 'contextWindow', label: '上下文限制', type: 'number', default: 128000, required: false, placeholder: 'MiniMax-M2: 128000' }
62
75
  ]
63
76
  },
64
77
  'glm': {
@@ -68,7 +81,8 @@ export class ProviderConfigComponent implements OnInit, OnDestroy {
68
81
  fields: [
69
82
  { key: 'apiKey', label: 'API Key', type: 'password', required: true },
70
83
  { key: 'baseURL', label: 'Base URL', type: 'text', default: 'https://open.bigmodel.cn/api/paas/v4', required: false },
71
- { key: 'model', label: 'Model', type: 'text', default: 'glm-4', required: false, placeholder: '例如: glm-4, glm-4-air, glm-4-flash' }
84
+ { key: 'model', label: 'Model', type: 'text', default: 'glm-4', required: false, placeholder: '例如: glm-4, glm-4-air, glm-4-flash' },
85
+ { key: 'contextWindow', label: '上下文限制', type: 'number', default: 128000, required: false, placeholder: 'GLM-4: 128000' }
72
86
  ]
73
87
  }
74
88
  };
@@ -82,7 +96,8 @@ export class ProviderConfigComponent implements OnInit, OnDestroy {
82
96
  defaultURL: 'http://localhost:11434/v1',
83
97
  fields: [
84
98
  { key: 'baseURL', label: 'Base URL', type: 'text', default: 'http://localhost:11434/v1', required: true, placeholder: '例如: http://localhost:11434/v1' },
85
- { key: 'model', label: 'Model', type: 'text', default: 'llama3.1', required: false, placeholder: '例如: llama3.1, qwen2.5, mistral' }
99
+ { key: 'model', label: 'Model', type: 'text', default: 'llama3.1', required: false, placeholder: '例如: llama3.1, qwen2.5, mistral' },
100
+ { key: 'contextWindow', label: '上下文限制', type: 'number', default: 8192, required: false, placeholder: 'Llama 3.1: 8192' }
86
101
  ]
87
102
  },
88
103
  'vllm': {
@@ -93,7 +108,8 @@ export class ProviderConfigComponent implements OnInit, OnDestroy {
93
108
  fields: [
94
109
  { key: 'baseURL', label: 'Base URL', type: 'text', default: 'http://localhost:8000/v1', required: true, placeholder: '例如: http://localhost:8000/v1' },
95
110
  { key: 'apiKey', label: 'API Key (可选)', type: 'password', required: false },
96
- { key: 'model', label: 'Model', type: 'text', default: 'meta-llama/Llama-3.1-8B', required: false, placeholder: 'HuggingFace 模型路径' }
111
+ { key: 'model', label: 'Model', type: 'text', default: 'meta-llama/Llama-3.1-8B', required: false, placeholder: 'HuggingFace 模型路径' },
112
+ { key: 'contextWindow', label: '上下文限制', type: 'number', default: 8192, required: false, placeholder: '根据模型实际配置设置' }
97
113
  ]
98
114
  }
99
115
  };
@@ -101,6 +117,7 @@ export class ProviderConfigComponent implements OnInit, OnDestroy {
101
117
  constructor(
102
118
  private config: ConfigProviderService,
103
119
  private logger: LoggerService,
120
+ private toast: ToastService,
104
121
  private translate: TranslateService
105
122
  ) {
106
123
  this.t = this.translate.t;
@@ -129,6 +146,33 @@ export class ProviderConfigComponent implements OnInit, OnDestroy {
129
146
  */
130
147
  private loadConfigs(): void {
131
148
  const allConfigs = this.config.getAllProviderConfigs();
149
+
150
+ // 为所有云端供应商初始化默认配置
151
+ for (const providerName of Object.keys(this.cloudProviderTemplates)) {
152
+ if (!allConfigs[providerName]) {
153
+ const template = this.cloudProviderTemplates[providerName];
154
+ allConfigs[providerName] = {
155
+ name: providerName,
156
+ displayName: template.name,
157
+ enabled: false,
158
+ ...this.createDefaultConfig(template.fields)
159
+ };
160
+ }
161
+ }
162
+
163
+ // 为所有本地供应商初始化默认配置
164
+ for (const providerName of Object.keys(this.localProviderTemplates)) {
165
+ if (!allConfigs[providerName]) {
166
+ const template = this.localProviderTemplates[providerName];
167
+ allConfigs[providerName] = {
168
+ name: providerName,
169
+ displayName: template.name,
170
+ enabled: false,
171
+ ...this.createDefaultConfig(template.fields)
172
+ };
173
+ }
174
+ }
175
+
132
176
  this.configs = allConfigs;
133
177
  this.selectedProvider = this.config.getDefaultProvider();
134
178
  }
@@ -187,7 +231,7 @@ export class ProviderConfigComponent implements OnInit, OnDestroy {
187
231
  const baseURL = this.configs[providerName]?.baseURL || template?.defaultURL;
188
232
 
189
233
  if (!baseURL) {
190
- alert(this.t.providers.baseURL + ': ' + this.t.providers.testError);
234
+ this.toast.error(this.t.providers.baseURL + ': ' + this.t.providers.testError);
191
235
  return;
192
236
  }
193
237
 
@@ -201,16 +245,16 @@ export class ProviderConfigComponent implements OnInit, OnDestroy {
201
245
  });
202
246
 
203
247
  if (response.ok) {
204
- alert(`✅ ${template.name}: ${this.t.providers.testSuccess}`);
248
+ this.toast.success(`${template.name}: ${this.t.providers.testSuccess}`);
205
249
  this.localStatus[providerName] = true;
206
250
  this.logger.info('Local provider test successful', { provider: providerName });
207
251
  } else {
208
- alert(`❌ ${this.t.providers.testFail}: ${response.status}`);
252
+ this.toast.error(`${this.t.providers.testFail}: ${response.status}`);
209
253
  this.localStatus[providerName] = false;
210
254
  }
211
255
  } catch (error) {
212
256
  const errorMessage = error instanceof Error ? error.message : this.t.providers.testError;
213
- alert(`❌ ${template.name}\n\n${this.t.providers.testError}\n${errorMessage}`);
257
+ this.toast.error(`${template.name}\n\n${this.t.providers.testError}\n${errorMessage}`);
214
258
  this.localStatus[providerName] = false;
215
259
  this.logger.error('Local provider test failed', { provider: providerName, error: errorMessage });
216
260
  }
@@ -224,6 +268,7 @@ export class ProviderConfigComponent implements OnInit, OnDestroy {
224
268
  if (providerConfig) {
225
269
  this.config.setProviderConfig(providerName, providerConfig);
226
270
  this.logger.info('Provider config saved', { provider: providerName });
271
+ this.toast.success(`${this.getProviderTemplate(providerName)?.name || providerName} ${this.t.providers.configSaved || '配置已保存'}`);
227
272
  }
228
273
  }
229
274
 
@@ -277,7 +322,7 @@ export class ProviderConfigComponent implements OnInit, OnDestroy {
277
322
  async testConnection(providerName: string): Promise<void> {
278
323
  const providerConfig = this.configs[providerName];
279
324
  if (!providerConfig) {
280
- alert(this.t.providers.testError);
325
+ this.toast.error(this.t.providers.testError);
281
326
  return;
282
327
  }
283
328
 
@@ -291,7 +336,7 @@ export class ProviderConfigComponent implements OnInit, OnDestroy {
291
336
  const baseURL = providerConfig.baseURL;
292
337
 
293
338
  if (!apiKey) {
294
- alert(this.t.providers.apiKey + ': ' + this.t.providers.testError);
339
+ this.toast.error(this.t.providers.apiKey + ': ' + this.t.providers.testError);
295
340
  return;
296
341
  }
297
342
 
@@ -314,16 +359,16 @@ export class ProviderConfigComponent implements OnInit, OnDestroy {
314
359
  });
315
360
 
316
361
  if (response.ok) {
317
- alert(`✅ ${this.t.providers.testSuccess}`);
362
+ this.toast.success(this.t.providers.testSuccess);
318
363
  this.logger.info('Connection test successful', { provider: providerName });
319
364
  } else {
320
365
  const errorData = await response.text();
321
- alert(`❌ ${this.t.providers.testFail}\n\nStatus: ${response.status}\n${errorData.substring(0, 200)}`);
366
+ this.toast.error(`${this.t.providers.testFail}\n\nStatus: ${response.status}\n${errorData.substring(0, 200)}`);
322
367
  this.logger.error('Connection test failed', { provider: providerName, status: response.status });
323
368
  }
324
369
  } catch (error) {
325
370
  const errorMessage = error instanceof Error ? error.message : this.t.providers.testError;
326
- alert(`❌ ${this.t.providers.testFail}\n\n${errorMessage}`);
371
+ this.toast.error(`${this.t.providers.testFail}\n\n${errorMessage}`);
327
372
  this.logger.error('Connection test error', { provider: providerName, error: errorMessage });
328
373
  }
329
374
  }
@@ -473,4 +518,56 @@ export class ProviderConfigComponent implements OnInit, OnDestroy {
473
518
  }
474
519
  this.configs[providerName][key] = value;
475
520
  }
521
+
522
+ /**
523
+ * 切换密码字段可见性
524
+ */
525
+ togglePasswordVisibility(providerName: string, fieldKey: string): void {
526
+ if (!this.passwordVisibility[providerName]) {
527
+ this.passwordVisibility[providerName] = {};
528
+ }
529
+ this.passwordVisibility[providerName][fieldKey] = !this.passwordVisibility[providerName][fieldKey];
530
+ }
531
+
532
+ /**
533
+ * 获取密码字段可见性状态
534
+ */
535
+ isPasswordVisible(providerName: string, fieldKey: string): boolean {
536
+ return this.passwordVisibility[providerName]?.[fieldKey] ?? false;
537
+ }
538
+
539
+ /**
540
+ * 验证 API Key 格式
541
+ */
542
+ validateApiKeyFormat(providerName: string, apiKey: string): { valid: boolean; message: string } {
543
+ if (!apiKey || apiKey.trim().length === 0) {
544
+ return { valid: false, message: this.t?.providers?.apiKeyRequired || 'API Key 不能为空' };
545
+ }
546
+
547
+ const pattern = this.apiKeyPatterns[providerName];
548
+ if (pattern && !pattern.test(apiKey)) {
549
+ const hints: { [key: string]: string } = {
550
+ 'openai': 'OpenAI API Key 应以 sk- 开头',
551
+ 'anthropic': 'Anthropic API Key 应以 sk-ant- 开头',
552
+ 'minimax': 'Minimax API Key 应为 32 位以上的字母数字',
553
+ 'glm': 'GLM API Key 格式不正确'
554
+ };
555
+ return { valid: false, message: hints[providerName] || 'API Key 格式可能不正确' };
556
+ }
557
+
558
+ return { valid: true, message: '' };
559
+ }
560
+
561
+ /**
562
+ * 获取输入框的验证状态类
563
+ */
564
+ getInputValidationClass(providerName: string, fieldKey: string): string {
565
+ if (fieldKey !== 'apiKey') return '';
566
+
567
+ const value = this.configs[providerName]?.[fieldKey];
568
+ if (!value || value.trim().length === 0) return '';
569
+
570
+ const result = this.validateApiKeyFormat(providerName, value);
571
+ return result.valid ? 'is-valid' : 'is-invalid';
572
+ }
476
573
  }
@@ -31,6 +31,7 @@ export const enUS: TranslationKeys = {
31
31
  title: 'Settings',
32
32
  generalTab: 'General',
33
33
  providersTab: 'AI Providers',
34
+ contextTab: 'Context',
34
35
  securityTab: 'Security',
35
36
  chatTab: 'Chat',
36
37
  advancedTab: 'Advanced'
@@ -135,7 +136,9 @@ export const enUS: TranslationKeys = {
135
136
  testFail: 'Connection test failed',
136
137
  testError: 'Cannot connect to service, please ensure it is running',
137
138
  configSaved: 'Configuration saved',
138
- configDeleted: 'Configuration deleted'
139
+ configDeleted: 'Configuration deleted',
140
+ contextWindow: 'Context Window',
141
+ contextWindowDesc: 'Maximum context tokens for this model'
139
142
  },
140
143
 
141
144
  providerNames: {
@@ -189,5 +192,28 @@ export const enUS: TranslationKeys = {
189
192
  pluginVersion: 'Plugin Version',
190
193
  supportedProviders: 'Supported Providers',
191
194
  currentProvider: 'Current Provider'
195
+ },
196
+
197
+ contextSettings: {
198
+ title: 'Context Management',
199
+ autoCompact: 'Auto Compact',
200
+ enableAutoCompact: 'Enable Auto Compact',
201
+ autoCompactDesc: 'Automatically compress history when context exceeds threshold',
202
+ autoCompactEnabled: 'Auto Compact Enabled',
203
+ autoCompactDisabled: 'Auto Compact Disabled',
204
+ tokenConfig: 'Token Configuration',
205
+ currentProviderLimit: 'Current Provider Context Limit',
206
+ maxContextTokens: 'Max Context Tokens',
207
+ maxContextTokensDesc: 'Cannot exceed current provider context limit',
208
+ reservedOutputTokens: 'Reserved Output Tokens',
209
+ reservedOutputTokensDesc: 'Token count reserved for AI response',
210
+ thresholdConfig: 'Compression Thresholds',
211
+ pruneThreshold: 'Prune Threshold',
212
+ pruneThresholdDesc: 'Prune redundant content when usage exceeds this (default: 70%)',
213
+ compactThreshold: 'Compact Threshold',
214
+ compactThresholdDesc: 'Generate summary when usage exceeds this (default: 85%)',
215
+ messagesToKeep: 'Messages to Keep',
216
+ messagesToKeepDesc: 'Recent messages to always preserve (default: 3)',
217
+ configSaved: 'Context config saved'
192
218
  }
193
219
  };
@@ -31,6 +31,7 @@ export const jaJP: TranslationKeys = {
31
31
  title: '設定',
32
32
  generalTab: '基本設定',
33
33
  providersTab: 'AIプロバイダー',
34
+ contextTab: 'コンテキスト',
34
35
  securityTab: 'セキュリティ',
35
36
  chatTab: 'チャット',
36
37
  advancedTab: '詳細設定'
@@ -135,7 +136,9 @@ export const jaJP: TranslationKeys = {
135
136
  testFail: '接続テスト失敗',
136
137
  testError: 'サービスに接続できません。サービスが起動していることを確認してください。',
137
138
  configSaved: '設定が保存されました',
138
- configDeleted: '設定が削除されました'
139
+ configDeleted: '設定が削除されました',
140
+ contextWindow: 'コンテキストウィンドウ',
141
+ contextWindowDesc: 'モデルの最大コンテキストトークン数'
139
142
  },
140
143
 
141
144
  providerNames: {
@@ -189,5 +192,28 @@ export const jaJP: TranslationKeys = {
189
192
  pluginVersion: 'プラグインVersion',
190
193
  supportedProviders: 'サポートされているプロバイダー',
191
194
  currentProvider: '現在のリプロバイダー'
195
+ },
196
+
197
+ contextSettings: {
198
+ title: 'コンテキスト管理',
199
+ autoCompact: '自動圧縮',
200
+ enableAutoCompact: '自動圧縮を有効にする',
201
+ autoCompactDesc: 'コンテキストがしきい値を超えた場合に履歴を自動的に圧縮',
202
+ autoCompactEnabled: '自動圧縮が有効',
203
+ autoCompactDisabled: '自動圧縮が無効',
204
+ tokenConfig: 'トークン設定',
205
+ currentProviderLimit: '現在のリプロバイダーのコンテキスト制限',
206
+ maxContextTokens: '最大コンテキストトークン数',
207
+ maxContextTokensDesc: '現在のリプロバイダーのコンテキスト制限を超えないこと',
208
+ reservedOutputTokens: '出力予約トークン数',
209
+ reservedOutputTokensDesc: 'AI応答に予約するトークン数',
210
+ thresholdConfig: '圧縮しきい値',
211
+ pruneThreshold: 'プルーンしきい値',
212
+ pruneThresholdDesc: '使用率がこれを超えた場合に冗長コンテンツを削除(デフォルト:70%)',
213
+ compactThreshold: '圧縮しきい値',
214
+ compactThresholdDesc: '使用率がこれを超えた場合にサマリーを生成(デフォルト:85%)',
215
+ messagesToKeep: '保持するメッセージ数',
216
+ messagesToKeepDesc: '圧縮時に常に保持する最新メッセージ数(デフォルト:3)',
217
+ configSaved: 'コンテキスト設定が保存されました'
192
218
  }
193
219
  };
@@ -31,6 +31,7 @@ export const zhCN: TranslationKeys = {
31
31
  title: '设置',
32
32
  generalTab: '基本设置',
33
33
  providersTab: 'AI提供商',
34
+ contextTab: '上下文设置',
34
35
  securityTab: '安全设置',
35
36
  chatTab: '聊天设置',
36
37
  advancedTab: '高级设置'
@@ -135,7 +136,9 @@ export const zhCN: TranslationKeys = {
135
136
  testFail: '连接测试失败',
136
137
  testError: '无法连接到服务,请确保服务已启动',
137
138
  configSaved: '配置已保存',
138
- configDeleted: '配置已删除'
139
+ configDeleted: '配置已删除',
140
+ contextWindow: '上下文限制',
141
+ contextWindowDesc: '模型的最大上下文Token数'
139
142
  },
140
143
 
141
144
  providerNames: {
@@ -189,5 +192,28 @@ export const zhCN: TranslationKeys = {
189
192
  pluginVersion: '插件版本',
190
193
  supportedProviders: '支持的提供商',
191
194
  currentProvider: '当前提供商'
195
+ },
196
+
197
+ contextSettings: {
198
+ title: '上下文管理',
199
+ autoCompact: '自动压缩',
200
+ enableAutoCompact: '启用自动压缩',
201
+ autoCompactDesc: '当上下文超过阈值时自动压缩历史消息',
202
+ autoCompactEnabled: '自动压缩已启用',
203
+ autoCompactDisabled: '自动压缩已禁用',
204
+ tokenConfig: 'Token 配置',
205
+ currentProviderLimit: '当前供应商上下文限制',
206
+ maxContextTokens: '最大上下文 Token 数',
207
+ maxContextTokensDesc: '不能超过当前供应商的上下文限制',
208
+ reservedOutputTokens: '输出预留 Token 数',
209
+ reservedOutputTokensDesc: '为 AI 回复预留的 Token 数量',
210
+ thresholdConfig: '压缩阈值',
211
+ pruneThreshold: '裁剪阈值',
212
+ pruneThresholdDesc: '使用率超过此阈值时裁剪冗余内容(默认:70%)',
213
+ compactThreshold: '压缩阈值',
214
+ compactThresholdDesc: '使用率超过此阈值时生成摘要压缩(默认:85%)',
215
+ messagesToKeep: '保留消息数',
216
+ messagesToKeepDesc: '压缩时始终保留的最近消息数量(默认:3)',
217
+ configSaved: '上下文配置已保存'
192
218
  }
193
219
  };