tabby-ai-assistant 1.0.9 → 1.0.10

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.
@@ -0,0 +1,15 @@
1
+ export interface ToastMessage {
2
+ id: string;
3
+ type: 'success' | 'error' | 'warning' | 'info';
4
+ message: string;
5
+ duration?: number;
6
+ }
7
+ export declare class ToastService {
8
+ private toastSubject;
9
+ toast$: import("rxjs").Observable<ToastMessage>;
10
+ show(type: ToastMessage['type'], message: string, duration?: number): void;
11
+ success(message: string, duration?: number): void;
12
+ error(message: string, duration?: number): void;
13
+ warning(message: string, duration?: number): void;
14
+ info(message: string, duration?: number): void;
15
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tabby-ai-assistant",
3
- "version": "1.0.9",
3
+ "version": "1.0.10",
4
4
  "description": "Tabby终端AI助手插件 - 支持多AI提供商(OpenAI、Anthropic、Minimax、GLM、Ollama、vLLM)",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
@@ -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
  // 云端提供商模板
@@ -101,6 +111,7 @@ export class ProviderConfigComponent implements OnInit, OnDestroy {
101
111
  constructor(
102
112
  private config: ConfigProviderService,
103
113
  private logger: LoggerService,
114
+ private toast: ToastService,
104
115
  private translate: TranslateService
105
116
  ) {
106
117
  this.t = this.translate.t;
@@ -129,6 +140,33 @@ export class ProviderConfigComponent implements OnInit, OnDestroy {
129
140
  */
130
141
  private loadConfigs(): void {
131
142
  const allConfigs = this.config.getAllProviderConfigs();
143
+
144
+ // 为所有云端供应商初始化默认配置
145
+ for (const providerName of Object.keys(this.cloudProviderTemplates)) {
146
+ if (!allConfigs[providerName]) {
147
+ const template = this.cloudProviderTemplates[providerName];
148
+ allConfigs[providerName] = {
149
+ name: providerName,
150
+ displayName: template.name,
151
+ enabled: false,
152
+ ...this.createDefaultConfig(template.fields)
153
+ };
154
+ }
155
+ }
156
+
157
+ // 为所有本地供应商初始化默认配置
158
+ for (const providerName of Object.keys(this.localProviderTemplates)) {
159
+ if (!allConfigs[providerName]) {
160
+ const template = this.localProviderTemplates[providerName];
161
+ allConfigs[providerName] = {
162
+ name: providerName,
163
+ displayName: template.name,
164
+ enabled: false,
165
+ ...this.createDefaultConfig(template.fields)
166
+ };
167
+ }
168
+ }
169
+
132
170
  this.configs = allConfigs;
133
171
  this.selectedProvider = this.config.getDefaultProvider();
134
172
  }
@@ -187,7 +225,7 @@ export class ProviderConfigComponent implements OnInit, OnDestroy {
187
225
  const baseURL = this.configs[providerName]?.baseURL || template?.defaultURL;
188
226
 
189
227
  if (!baseURL) {
190
- alert(this.t.providers.baseURL + ': ' + this.t.providers.testError);
228
+ this.toast.error(this.t.providers.baseURL + ': ' + this.t.providers.testError);
191
229
  return;
192
230
  }
193
231
 
@@ -201,16 +239,16 @@ export class ProviderConfigComponent implements OnInit, OnDestroy {
201
239
  });
202
240
 
203
241
  if (response.ok) {
204
- alert(`✅ ${template.name}: ${this.t.providers.testSuccess}`);
242
+ this.toast.success(`${template.name}: ${this.t.providers.testSuccess}`);
205
243
  this.localStatus[providerName] = true;
206
244
  this.logger.info('Local provider test successful', { provider: providerName });
207
245
  } else {
208
- alert(`❌ ${this.t.providers.testFail}: ${response.status}`);
246
+ this.toast.error(`${this.t.providers.testFail}: ${response.status}`);
209
247
  this.localStatus[providerName] = false;
210
248
  }
211
249
  } catch (error) {
212
250
  const errorMessage = error instanceof Error ? error.message : this.t.providers.testError;
213
- alert(`❌ ${template.name}\n\n${this.t.providers.testError}\n${errorMessage}`);
251
+ this.toast.error(`${template.name}\n\n${this.t.providers.testError}\n${errorMessage}`);
214
252
  this.localStatus[providerName] = false;
215
253
  this.logger.error('Local provider test failed', { provider: providerName, error: errorMessage });
216
254
  }
@@ -224,6 +262,7 @@ export class ProviderConfigComponent implements OnInit, OnDestroy {
224
262
  if (providerConfig) {
225
263
  this.config.setProviderConfig(providerName, providerConfig);
226
264
  this.logger.info('Provider config saved', { provider: providerName });
265
+ this.toast.success(`${this.getProviderTemplate(providerName)?.name || providerName} ${this.t.providers.configSaved || '配置已保存'}`);
227
266
  }
228
267
  }
229
268
 
@@ -277,7 +316,7 @@ export class ProviderConfigComponent implements OnInit, OnDestroy {
277
316
  async testConnection(providerName: string): Promise<void> {
278
317
  const providerConfig = this.configs[providerName];
279
318
  if (!providerConfig) {
280
- alert(this.t.providers.testError);
319
+ this.toast.error(this.t.providers.testError);
281
320
  return;
282
321
  }
283
322
 
@@ -291,7 +330,7 @@ export class ProviderConfigComponent implements OnInit, OnDestroy {
291
330
  const baseURL = providerConfig.baseURL;
292
331
 
293
332
  if (!apiKey) {
294
- alert(this.t.providers.apiKey + ': ' + this.t.providers.testError);
333
+ this.toast.error(this.t.providers.apiKey + ': ' + this.t.providers.testError);
295
334
  return;
296
335
  }
297
336
 
@@ -314,16 +353,16 @@ export class ProviderConfigComponent implements OnInit, OnDestroy {
314
353
  });
315
354
 
316
355
  if (response.ok) {
317
- alert(`✅ ${this.t.providers.testSuccess}`);
356
+ this.toast.success(this.t.providers.testSuccess);
318
357
  this.logger.info('Connection test successful', { provider: providerName });
319
358
  } else {
320
359
  const errorData = await response.text();
321
- alert(`❌ ${this.t.providers.testFail}\n\nStatus: ${response.status}\n${errorData.substring(0, 200)}`);
360
+ this.toast.error(`${this.t.providers.testFail}\n\nStatus: ${response.status}\n${errorData.substring(0, 200)}`);
322
361
  this.logger.error('Connection test failed', { provider: providerName, status: response.status });
323
362
  }
324
363
  } catch (error) {
325
364
  const errorMessage = error instanceof Error ? error.message : this.t.providers.testError;
326
- alert(`❌ ${this.t.providers.testFail}\n\n${errorMessage}`);
365
+ this.toast.error(`${this.t.providers.testFail}\n\n${errorMessage}`);
327
366
  this.logger.error('Connection test error', { provider: providerName, error: errorMessage });
328
367
  }
329
368
  }
@@ -473,4 +512,56 @@ export class ProviderConfigComponent implements OnInit, OnDestroy {
473
512
  }
474
513
  this.configs[providerName][key] = value;
475
514
  }
515
+
516
+ /**
517
+ * 切换密码字段可见性
518
+ */
519
+ togglePasswordVisibility(providerName: string, fieldKey: string): void {
520
+ if (!this.passwordVisibility[providerName]) {
521
+ this.passwordVisibility[providerName] = {};
522
+ }
523
+ this.passwordVisibility[providerName][fieldKey] = !this.passwordVisibility[providerName][fieldKey];
524
+ }
525
+
526
+ /**
527
+ * 获取密码字段可见性状态
528
+ */
529
+ isPasswordVisible(providerName: string, fieldKey: string): boolean {
530
+ return this.passwordVisibility[providerName]?.[fieldKey] ?? false;
531
+ }
532
+
533
+ /**
534
+ * 验证 API Key 格式
535
+ */
536
+ validateApiKeyFormat(providerName: string, apiKey: string): { valid: boolean; message: string } {
537
+ if (!apiKey || apiKey.trim().length === 0) {
538
+ return { valid: false, message: this.t?.providers?.apiKeyRequired || 'API Key 不能为空' };
539
+ }
540
+
541
+ const pattern = this.apiKeyPatterns[providerName];
542
+ if (pattern && !pattern.test(apiKey)) {
543
+ const hints: { [key: string]: string } = {
544
+ 'openai': 'OpenAI API Key 应以 sk- 开头',
545
+ 'anthropic': 'Anthropic API Key 应以 sk-ant- 开头',
546
+ 'minimax': 'Minimax API Key 应为 32 位以上的字母数字',
547
+ 'glm': 'GLM API Key 格式不正确'
548
+ };
549
+ return { valid: false, message: hints[providerName] || 'API Key 格式可能不正确' };
550
+ }
551
+
552
+ return { valid: true, message: '' };
553
+ }
554
+
555
+ /**
556
+ * 获取输入框的验证状态类
557
+ */
558
+ getInputValidationClass(providerName: string, fieldKey: string): string {
559
+ if (fieldKey !== 'apiKey') return '';
560
+
561
+ const value = this.configs[providerName]?.[fieldKey];
562
+ if (!value || value.trim().length === 0) return '';
563
+
564
+ const result = this.validateApiKeyFormat(providerName, value);
565
+ return result.valid ? 'is-valid' : 'is-invalid';
566
+ }
476
567
  }
@@ -0,0 +1,36 @@
1
+ import { Injectable } from '@angular/core';
2
+ import { Subject } from 'rxjs';
3
+
4
+ export interface ToastMessage {
5
+ id: string;
6
+ type: 'success' | 'error' | 'warning' | 'info';
7
+ message: string;
8
+ duration?: number;
9
+ }
10
+
11
+ @Injectable({ providedIn: 'root' })
12
+ export class ToastService {
13
+ private toastSubject = new Subject<ToastMessage>();
14
+ toast$ = this.toastSubject.asObservable();
15
+
16
+ show(type: ToastMessage['type'], message: string, duration = 3000): void {
17
+ const id = `toast-${Date.now()}`;
18
+ this.toastSubject.next({ id, type, message, duration });
19
+ }
20
+
21
+ success(message: string, duration = 3000): void {
22
+ this.show('success', message, duration);
23
+ }
24
+
25
+ error(message: string, duration = 5000): void {
26
+ this.show('error', message, duration);
27
+ }
28
+
29
+ warning(message: string, duration = 4000): void {
30
+ this.show('warning', message, duration);
31
+ }
32
+
33
+ info(message: string, duration = 3000): void {
34
+ this.show('info', message, duration);
35
+ }
36
+ }