tabby-ai-assistant 1.0.8 → 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.
Files changed (54) hide show
  1. package/dist/components/chat/ai-sidebar.component.d.ts +16 -3
  2. package/dist/components/chat/chat-input.component.d.ts +4 -0
  3. package/dist/components/chat/chat-interface.component.d.ts +22 -1
  4. package/dist/components/chat/chat-settings.component.d.ts +21 -11
  5. package/dist/components/settings/ai-settings-tab.component.d.ts +14 -4
  6. package/dist/components/settings/general-settings.component.d.ts +43 -12
  7. package/dist/components/settings/provider-config.component.d.ts +110 -5
  8. package/dist/components/settings/security-settings.component.d.ts +14 -4
  9. package/dist/i18n/index.d.ts +48 -0
  10. package/dist/i18n/translations/en-US.d.ts +5 -0
  11. package/dist/i18n/translations/ja-JP.d.ts +5 -0
  12. package/dist/i18n/translations/zh-CN.d.ts +5 -0
  13. package/dist/i18n/types.d.ts +198 -0
  14. package/dist/index.js +1 -1
  15. package/dist/services/chat/ai-sidebar.service.d.ts +23 -1
  16. package/dist/services/core/theme.service.d.ts +53 -0
  17. package/dist/services/core/toast.service.d.ts +15 -0
  18. package/package.json +1 -1
  19. package/src/components/chat/ai-sidebar.component.scss +468 -0
  20. package/src/components/chat/ai-sidebar.component.ts +47 -344
  21. package/src/components/chat/chat-input.component.scss +2 -2
  22. package/src/components/chat/chat-input.component.ts +16 -5
  23. package/src/components/chat/chat-interface.component.html +11 -11
  24. package/src/components/chat/chat-interface.component.scss +410 -4
  25. package/src/components/chat/chat-interface.component.ts +105 -14
  26. package/src/components/chat/chat-message.component.scss +3 -3
  27. package/src/components/chat/chat-message.component.ts +3 -2
  28. package/src/components/chat/chat-settings.component.html +95 -61
  29. package/src/components/chat/chat-settings.component.scss +224 -50
  30. package/src/components/chat/chat-settings.component.ts +56 -30
  31. package/src/components/security/risk-confirm-dialog.component.scss +7 -7
  32. package/src/components/settings/ai-settings-tab.component.html +27 -27
  33. package/src/components/settings/ai-settings-tab.component.scss +34 -20
  34. package/src/components/settings/ai-settings-tab.component.ts +59 -20
  35. package/src/components/settings/general-settings.component.html +69 -40
  36. package/src/components/settings/general-settings.component.scss +151 -58
  37. package/src/components/settings/general-settings.component.ts +168 -55
  38. package/src/components/settings/provider-config.component.html +183 -60
  39. package/src/components/settings/provider-config.component.scss +332 -153
  40. package/src/components/settings/provider-config.component.ts +268 -19
  41. package/src/components/settings/security-settings.component.html +70 -39
  42. package/src/components/settings/security-settings.component.scss +104 -8
  43. package/src/components/settings/security-settings.component.ts +48 -10
  44. package/src/i18n/index.ts +129 -0
  45. package/src/i18n/translations/en-US.ts +193 -0
  46. package/src/i18n/translations/ja-JP.ts +193 -0
  47. package/src/i18n/translations/zh-CN.ts +193 -0
  48. package/src/i18n/types.ts +224 -0
  49. package/src/index.ts +6 -0
  50. package/src/services/chat/ai-sidebar.service.ts +157 -5
  51. package/src/services/core/theme.service.ts +480 -0
  52. package/src/services/core/toast.service.ts +36 -0
  53. package/src/styles/ai-assistant.scss +8 -88
  54. package/src/styles/themes.scss +161 -0
@@ -1,13 +1,18 @@
1
- import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
1
+ import { Component, Input, Output, EventEmitter, OnInit, OnDestroy, ViewEncapsulation } from '@angular/core';
2
+ import { Subject } from 'rxjs';
3
+ import { takeUntil } from 'rxjs/operators';
2
4
  import { ConfigProviderService } from '../../services/core/config-provider.service';
3
5
  import { LoggerService } from '../../services/core/logger.service';
6
+ import { ToastService } from '../../services/core/toast.service';
7
+ import { TranslateService } from '../../i18n';
4
8
 
5
9
  @Component({
6
10
  selector: 'app-provider-config',
7
11
  templateUrl: './provider-config.component.html',
8
- styleUrls: ['./provider-config.component.scss']
12
+ styleUrls: ['./provider-config.component.scss'],
13
+ encapsulation: ViewEncapsulation.None
9
14
  })
10
- export class ProviderConfigComponent implements OnInit {
15
+ export class ProviderConfigComponent implements OnInit, OnDestroy {
11
16
  @Input() providerStatus: any = {};
12
17
  @Output() refreshStatus = new EventEmitter<void>();
13
18
  @Output() switchProvider = new EventEmitter<string>();
@@ -17,12 +22,29 @@ export class ProviderConfigComponent implements OnInit {
17
22
 
18
23
  selectedProvider = '';
19
24
  configs: { [key: string]: any } = {};
25
+ expandedProvider: string = '';
26
+ localStatus: { [key: string]: boolean } = {};
27
+ passwordVisibility: { [key: string]: { [fieldKey: string]: boolean } } = {};
28
+
29
+ // 翻译对象
30
+ t: any;
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
+
40
+ private destroy$ = new Subject<void>();
20
41
 
21
- // 预定义的提供商模板
22
- providerTemplates = {
42
+ // 云端提供商模板
43
+ cloudProviderTemplates = {
23
44
  'openai': {
24
45
  name: 'OpenAI',
25
46
  description: 'OpenAI GPT模型',
47
+ icon: 'fa-robot',
26
48
  fields: [
27
49
  { key: 'apiKey', label: 'API Key', type: 'password', required: true },
28
50
  { key: 'baseURL', label: 'Base URL', type: 'text', default: 'https://api.openai.com/v1', required: false },
@@ -32,6 +54,7 @@ export class ProviderConfigComponent implements OnInit {
32
54
  'anthropic': {
33
55
  name: 'Anthropic Claude',
34
56
  description: 'Anthropic Claude模型',
57
+ icon: 'fa-comments',
35
58
  fields: [
36
59
  { key: 'apiKey', label: 'API Key', type: 'password', required: true },
37
60
  { key: 'baseURL', label: 'Base URL', type: 'text', default: 'https://api.anthropic.com', required: false },
@@ -41,6 +64,7 @@ export class ProviderConfigComponent implements OnInit {
41
64
  'minimax': {
42
65
  name: 'Minimax',
43
66
  description: 'Minimax AI模型',
67
+ icon: 'fa-brain',
44
68
  fields: [
45
69
  { key: 'apiKey', label: 'API Key', type: 'password', required: true },
46
70
  { key: 'baseURL', label: 'Base URL', type: 'text', default: 'https://api.minimaxi.com/anthropic', required: false },
@@ -50,6 +74,7 @@ export class ProviderConfigComponent implements OnInit {
50
74
  'glm': {
51
75
  name: 'GLM (ChatGLM)',
52
76
  description: '智谱AI ChatGLM模型',
77
+ icon: 'fa-network-wired',
53
78
  fields: [
54
79
  { key: 'apiKey', label: 'API Key', type: 'password', required: true },
55
80
  { key: 'baseURL', label: 'Base URL', type: 'text', default: 'https://open.bigmodel.cn/api/paas/v4', required: false },
@@ -58,13 +83,56 @@ export class ProviderConfigComponent implements OnInit {
58
83
  }
59
84
  };
60
85
 
86
+ // 本地提供商模板(不需要 API Key)
87
+ localProviderTemplates = {
88
+ 'ollama': {
89
+ name: 'Ollama (本地)',
90
+ description: '本地运行的 Ollama 服务,支持 Llama、Qwen 等模型',
91
+ icon: 'fa-server',
92
+ defaultURL: 'http://localhost:11434/v1',
93
+ fields: [
94
+ { key: 'baseURL', label: 'Base URL', type: 'text', default: 'http://localhost:11434/v1', required: true, placeholder: '例如: http://localhost:11434/v1' },
95
+ { key: 'model', label: 'Model', type: 'text', default: 'llama3.1', required: false, placeholder: '例如: llama3.1, qwen2.5, mistral' }
96
+ ]
97
+ },
98
+ 'vllm': {
99
+ name: 'vLLM (本地)',
100
+ description: '本地运行的 vLLM 服务,适合生产部署',
101
+ icon: 'fa-database',
102
+ defaultURL: 'http://localhost:8000/v1',
103
+ fields: [
104
+ { key: 'baseURL', label: 'Base URL', type: 'text', default: 'http://localhost:8000/v1', required: true, placeholder: '例如: http://localhost:8000/v1' },
105
+ { key: 'apiKey', label: 'API Key (可选)', type: 'password', required: false },
106
+ { key: 'model', label: 'Model', type: 'text', default: 'meta-llama/Llama-3.1-8B', required: false, placeholder: 'HuggingFace 模型路径' }
107
+ ]
108
+ }
109
+ };
110
+
61
111
  constructor(
62
112
  private config: ConfigProviderService,
63
- private logger: LoggerService
64
- ) { }
113
+ private logger: LoggerService,
114
+ private toast: ToastService,
115
+ private translate: TranslateService
116
+ ) {
117
+ this.t = this.translate.t;
118
+ }
65
119
 
66
120
  ngOnInit(): void {
121
+ // 监听语言变化
122
+ this.translate.translation$.pipe(
123
+ takeUntil(this.destroy$)
124
+ ).subscribe(translation => {
125
+ this.t = translation;
126
+ });
127
+
67
128
  this.loadConfigs();
129
+ // 检测本地供应商状态
130
+ this.checkLocalProviderStatus();
131
+ }
132
+
133
+ ngOnDestroy(): void {
134
+ this.destroy$.next();
135
+ this.destroy$.complete();
68
136
  }
69
137
 
70
138
  /**
@@ -72,10 +140,120 @@ export class ProviderConfigComponent implements OnInit {
72
140
  */
73
141
  private loadConfigs(): void {
74
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
+
75
170
  this.configs = allConfigs;
76
171
  this.selectedProvider = this.config.getDefaultProvider();
77
172
  }
78
173
 
174
+ /**
175
+ * 切换展开/折叠
176
+ */
177
+ toggleExpand(providerName: string): void {
178
+ this.expandedProvider = this.expandedProvider === providerName ? '' : providerName;
179
+ }
180
+
181
+ /**
182
+ * 检查是否是本地提供商
183
+ */
184
+ isLocalProvider(providerName: string): boolean {
185
+ return providerName in this.localProviderTemplates;
186
+ }
187
+
188
+ /**
189
+ * 检测本地供应商状态
190
+ */
191
+ private async checkLocalProviderStatus(): Promise<void> {
192
+ const localUrls: { [key: string]: string } = {
193
+ 'ollama': 'http://localhost:11434/v1/models',
194
+ 'vllm': 'http://localhost:8000/v1/models'
195
+ };
196
+
197
+ for (const [name, url] of Object.entries(localUrls)) {
198
+ try {
199
+ const controller = new AbortController();
200
+ setTimeout(() => controller.abort(), 2000);
201
+
202
+ const response = await fetch(url, { signal: controller.signal });
203
+ this.localStatus[name] = response.ok;
204
+ } catch {
205
+ this.localStatus[name] = false;
206
+ }
207
+ }
208
+ }
209
+
210
+ /**
211
+ * 获取本地供应商在线状态
212
+ */
213
+ getLocalStatus(providerName: string): { text: string; color: string; icon: string } {
214
+ const isOnline = this.localStatus[providerName];
215
+ return isOnline
216
+ ? { text: '在线', color: '#4caf50', icon: 'fa-check-circle' }
217
+ : { text: '离线', color: '#f44336', icon: 'fa-times-circle' };
218
+ }
219
+
220
+ /**
221
+ * 测试本地提供商连接
222
+ */
223
+ async testLocalProvider(providerName: string): Promise<void> {
224
+ const template = this.localProviderTemplates[providerName];
225
+ const baseURL = this.configs[providerName]?.baseURL || template?.defaultURL;
226
+
227
+ if (!baseURL) {
228
+ this.toast.error(this.t.providers.baseURL + ': ' + this.t.providers.testError);
229
+ return;
230
+ }
231
+
232
+ const testingMessage = `${this.t.providers.testConnection} ${template.name}...`;
233
+ this.logger.info(testingMessage);
234
+
235
+ try {
236
+ const response = await fetch(`${baseURL}/models`, {
237
+ method: 'GET',
238
+ signal: AbortSignal.timeout(5000)
239
+ });
240
+
241
+ if (response.ok) {
242
+ this.toast.success(`${template.name}: ${this.t.providers.testSuccess}`);
243
+ this.localStatus[providerName] = true;
244
+ this.logger.info('Local provider test successful', { provider: providerName });
245
+ } else {
246
+ this.toast.error(`${this.t.providers.testFail}: ${response.status}`);
247
+ this.localStatus[providerName] = false;
248
+ }
249
+ } catch (error) {
250
+ const errorMessage = error instanceof Error ? error.message : this.t.providers.testError;
251
+ this.toast.error(`${template.name}\n\n${this.t.providers.testError}\n${errorMessage}`);
252
+ this.localStatus[providerName] = false;
253
+ this.logger.error('Local provider test failed', { provider: providerName, error: errorMessage });
254
+ }
255
+ }
256
+
79
257
  /**
80
258
  * 保存配置
81
259
  */
@@ -84,6 +262,7 @@ export class ProviderConfigComponent implements OnInit {
84
262
  if (providerConfig) {
85
263
  this.config.setProviderConfig(providerName, providerConfig);
86
264
  this.logger.info('Provider config saved', { provider: providerName });
265
+ this.toast.success(`${this.getProviderTemplate(providerName)?.name || providerName} ${this.t.providers.configSaved || '配置已保存'}`);
87
266
  }
88
267
  }
89
268
 
@@ -92,7 +271,11 @@ export class ProviderConfigComponent implements OnInit {
92
271
  */
93
272
  addProvider(providerName: string): void {
94
273
  if (!this.configs[providerName]) {
95
- const template = this.providerTemplates[providerName];
274
+ // 检查是云端还是本地提供商
275
+ let template = this.cloudProviderTemplates[providerName];
276
+ if (!template) {
277
+ template = this.localProviderTemplates[providerName];
278
+ }
96
279
  if (template) {
97
280
  const newConfig = {
98
281
  name: providerName,
@@ -110,7 +293,7 @@ export class ProviderConfigComponent implements OnInit {
110
293
  * 删除提供商
111
294
  */
112
295
  removeProvider(providerName: string): void {
113
- if (confirm(`确定要删除 ${this.providerTemplates[providerName]?.name} 的配置吗?`)) {
296
+ if (confirm(this.t.providers.deleteConfirm)) {
114
297
  delete this.configs[providerName];
115
298
  this.config.deleteProviderConfig(providerName);
116
299
  this.logger.info('Provider config removed', { provider: providerName });
@@ -133,7 +316,13 @@ export class ProviderConfigComponent implements OnInit {
133
316
  async testConnection(providerName: string): Promise<void> {
134
317
  const providerConfig = this.configs[providerName];
135
318
  if (!providerConfig) {
136
- alert('请先配置该提供商');
319
+ this.toast.error(this.t.providers.testError);
320
+ return;
321
+ }
322
+
323
+ // 本地提供商使用不同的测试方法
324
+ if (this.isLocalProvider(providerName)) {
325
+ await this.testLocalProvider(providerName);
137
326
  return;
138
327
  }
139
328
 
@@ -141,15 +330,15 @@ export class ProviderConfigComponent implements OnInit {
141
330
  const baseURL = providerConfig.baseURL;
142
331
 
143
332
  if (!apiKey) {
144
- alert('请先填写 API Key');
333
+ this.toast.error(this.t.providers.apiKey + ': ' + this.t.providers.testError);
145
334
  return;
146
335
  }
147
336
 
148
- const template = (this.providerTemplates as any)[providerName];
337
+ const template = this.cloudProviderTemplates[providerName];
149
338
  const providerDisplayName = template?.name || providerName;
150
339
 
151
340
  // 显示测试中状态
152
- const testingMessage = `正在测试 ${providerDisplayName} 的连接...`;
341
+ const testingMessage = `${this.t.providers.testConnection} ${providerDisplayName}...`;
153
342
  this.logger.info(testingMessage);
154
343
 
155
344
  try {
@@ -164,16 +353,16 @@ export class ProviderConfigComponent implements OnInit {
164
353
  });
165
354
 
166
355
  if (response.ok) {
167
- alert(`✅ ${providerDisplayName} 连接测试成功!`);
356
+ this.toast.success(this.t.providers.testSuccess);
168
357
  this.logger.info('Connection test successful', { provider: providerName });
169
358
  } else {
170
359
  const errorData = await response.text();
171
- alert(`❌ ${providerDisplayName} 连接测试失败\n\n状态码: ${response.status}\n错误信息: ${errorData.substring(0, 200)}`);
360
+ this.toast.error(`${this.t.providers.testFail}\n\nStatus: ${response.status}\n${errorData.substring(0, 200)}`);
172
361
  this.logger.error('Connection test failed', { provider: providerName, status: response.status });
173
362
  }
174
363
  } catch (error) {
175
- const errorMessage = error instanceof Error ? error.message : '未知错误';
176
- alert(`❌ ${providerDisplayName} 连接测试失败\n\n错误: ${errorMessage}`);
364
+ const errorMessage = error instanceof Error ? error.message : this.t.providers.testError;
365
+ this.toast.error(`${this.t.providers.testFail}\n\n${errorMessage}`);
177
366
  this.logger.error('Connection test error', { provider: providerName, error: errorMessage });
178
367
  }
179
368
  }
@@ -286,10 +475,18 @@ export class ProviderConfigComponent implements OnInit {
286
475
  }
287
476
 
288
477
  /**
289
- * 获取提供商模板
478
+ * 获取提供商模板(支持云端和本地)
290
479
  */
291
480
  getProviderTemplate(providerName: string): any {
292
- return this.providerTemplates[providerName];
481
+ return this.cloudProviderTemplates[providerName] || this.localProviderTemplates[providerName];
482
+ }
483
+
484
+ /**
485
+ * 获取提供商图标
486
+ */
487
+ getProviderIcon(providerName: string): string {
488
+ const template = this.getProviderTemplate(providerName);
489
+ return template?.icon || 'fa-cog';
293
490
  }
294
491
 
295
492
  /**
@@ -315,4 +512,56 @@ export class ProviderConfigComponent implements OnInit {
315
512
  }
316
513
  this.configs[providerName][key] = value;
317
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
+ }
318
567
  }
@@ -1,51 +1,82 @@
1
1
  <div class="security-settings">
2
- <h3>Security Settings</h3>
2
+ <h3>{{ t.security.title }}</h3>
3
3
 
4
- <div class="form-group">
5
- <label class="form-label">
6
- <input type="checkbox" [(ngModel)]="enablePasswordProtection">
7
- Enable Password Protection
8
- </label>
9
- <small class="form-text text-muted">
10
- Require password for high-risk commands
11
- </small>
4
+ <!-- 密码保护 -->
5
+ <div class="settings-section">
6
+ <h4>{{ t.security.accessControl }}</h4>
7
+ <div class="form-group">
8
+ <label class="form-label">
9
+ <input type="checkbox" [(ngModel)]="settings.enablePasswordProtection">
10
+ {{ t.security.passwordProtection }}
11
+ </label>
12
+ <small class="form-text text-muted">
13
+ {{ t.security.passwordProtectionDesc }}
14
+ </small>
15
+ </div>
16
+ <div class="form-group" *ngIf="settings.enablePasswordProtection">
17
+ <label class="form-label">{{ t.security.setPassword }}</label>
18
+ <input type="password" class="form-control" [(ngModel)]="password" [placeholder]="t.security.passwordPlaceholder">
19
+ </div>
12
20
  </div>
13
21
 
14
- <div class="form-group" *ngIf="enablePasswordProtection">
15
- <label class="form-label">Set Password</label>
16
- <input type="password" class="form-control" [(ngModel)]="password" placeholder="Enter password">
22
+ <!-- 风险评估 -->
23
+ <div class="settings-section">
24
+ <h4>{{ t.security.riskAssessment }}</h4>
25
+ <div class="form-group">
26
+ <label class="form-label">
27
+ <input type="checkbox" [(ngModel)]="settings.enableRiskAssessment">
28
+ {{ t.security.riskAssessment }}
29
+ </label>
30
+ <small class="form-text text-muted">
31
+ {{ t.security.riskAssessmentDesc }}
32
+ </small>
33
+ </div>
34
+ <div class="form-group">
35
+ <label class="form-label">{{ t.security.defaultRiskLevel }}</label>
36
+ <select class="form-control" [(ngModel)]="settings.defaultRiskLevel">
37
+ <option value="low">{{ t.security.riskLow }}</option>
38
+ <option value="medium">{{ t.security.riskMedium }}</option>
39
+ <option value="high">{{ t.security.riskHigh }}</option>
40
+ </select>
41
+ </div>
17
42
  </div>
18
43
 
19
- <div class="form-group">
20
- <label class="form-label">
21
- <input type="checkbox" [(ngModel)]="enableRiskAssessment">
22
- Enable Risk Assessment
23
- </label>
24
- <small class="form-text text-muted">
25
- Automatically assess command risk levels
26
- </small>
44
+ <!-- 用户授权 -->
45
+ <div class="settings-section">
46
+ <h4>{{ t.security.userConsent }}</h4>
47
+ <div class="form-group">
48
+ <label class="form-label">
49
+ <input type="checkbox" [(ngModel)]="settings.enableConsentPersistence">
50
+ {{ t.security.rememberConsent }}
51
+ </label>
52
+ <small class="form-text text-muted">
53
+ {{ t.security.rememberConsentDesc }} {{ settings.consentExpiryDays }} {{ t.security.consentExpiryDays }}
54
+ </small>
55
+ </div>
27
56
  </div>
28
57
 
29
- <div class="form-group">
30
- <label class="form-label">Default Risk Level</label>
31
- <select class="form-control" [(ngModel)]="defaultRiskLevel">
32
- <option value="low">Low</option>
33
- <option value="medium">Medium</option>
34
- <option value="high">High</option>
35
- </select>
58
+ <!-- 危险命令模式 -->
59
+ <div class="settings-section">
60
+ <h4>{{ t.security.dangerousPatterns }}</h4>
61
+ <div class="pattern-list">
62
+ <div *ngFor="let pattern of dangerousPatterns; let i = index" class="pattern-item">
63
+ <code>{{ pattern }}</code>
64
+ <button type="button" class="btn-icon" (click)="removeDangerousPattern(i)">
65
+ <i class="fa fa-times"></i>
66
+ </button>
67
+ </div>
68
+ </div>
69
+ <div class="form-group add-pattern">
70
+ <input type="text" class="form-control" [(ngModel)]="newPattern" [placeholder]="t.security.patternPlaceholder">
71
+ <button type="button" class="btn btn-secondary" (click)="addDangerousPattern(newPattern)">
72
+ {{ t.security.addPattern }}
73
+ </button>
74
+ </div>
36
75
  </div>
37
76
 
38
- <div class="form-group">
39
- <label class="form-label">
40
- <input type="checkbox" [(ngModel)]="enableConsentPersistence">
41
- Persist User Consent
42
- </label>
43
- <small class="form-text text-muted">
44
- Remember user consent for 30 days
45
- </small>
77
+ <!-- 操作按钮 -->
78
+ <div class="settings-actions">
79
+ <button class="btn btn-primary" (click)="saveSettings()">{{ t.common.save }}</button>
80
+ <button class="btn btn-outline" (click)="resetToDefaults()">{{ t.security.resetDefaults }}</button>
46
81
  </div>
47
-
48
- <button class="btn btn-primary" (click)="saveSettings()">
49
- Save Settings
50
- </button>
51
82
  </div>