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.
- package/dist/components/chat/ai-sidebar.component.d.ts +16 -3
- package/dist/components/chat/chat-input.component.d.ts +4 -0
- package/dist/components/chat/chat-interface.component.d.ts +22 -1
- package/dist/components/chat/chat-settings.component.d.ts +21 -11
- package/dist/components/settings/ai-settings-tab.component.d.ts +14 -4
- package/dist/components/settings/general-settings.component.d.ts +43 -12
- package/dist/components/settings/provider-config.component.d.ts +110 -5
- package/dist/components/settings/security-settings.component.d.ts +14 -4
- package/dist/i18n/index.d.ts +48 -0
- package/dist/i18n/translations/en-US.d.ts +5 -0
- package/dist/i18n/translations/ja-JP.d.ts +5 -0
- package/dist/i18n/translations/zh-CN.d.ts +5 -0
- package/dist/i18n/types.d.ts +198 -0
- package/dist/index.js +1 -1
- package/dist/services/chat/ai-sidebar.service.d.ts +23 -1
- package/dist/services/core/theme.service.d.ts +53 -0
- package/dist/services/core/toast.service.d.ts +15 -0
- package/package.json +1 -1
- package/src/components/chat/ai-sidebar.component.scss +468 -0
- package/src/components/chat/ai-sidebar.component.ts +47 -344
- package/src/components/chat/chat-input.component.scss +2 -2
- package/src/components/chat/chat-input.component.ts +16 -5
- package/src/components/chat/chat-interface.component.html +11 -11
- package/src/components/chat/chat-interface.component.scss +410 -4
- package/src/components/chat/chat-interface.component.ts +105 -14
- package/src/components/chat/chat-message.component.scss +3 -3
- package/src/components/chat/chat-message.component.ts +3 -2
- package/src/components/chat/chat-settings.component.html +95 -61
- package/src/components/chat/chat-settings.component.scss +224 -50
- package/src/components/chat/chat-settings.component.ts +56 -30
- package/src/components/security/risk-confirm-dialog.component.scss +7 -7
- package/src/components/settings/ai-settings-tab.component.html +27 -27
- package/src/components/settings/ai-settings-tab.component.scss +34 -20
- package/src/components/settings/ai-settings-tab.component.ts +59 -20
- package/src/components/settings/general-settings.component.html +69 -40
- package/src/components/settings/general-settings.component.scss +151 -58
- package/src/components/settings/general-settings.component.ts +168 -55
- package/src/components/settings/provider-config.component.html +183 -60
- package/src/components/settings/provider-config.component.scss +332 -153
- package/src/components/settings/provider-config.component.ts +268 -19
- package/src/components/settings/security-settings.component.html +70 -39
- package/src/components/settings/security-settings.component.scss +104 -8
- package/src/components/settings/security-settings.component.ts +48 -10
- package/src/i18n/index.ts +129 -0
- package/src/i18n/translations/en-US.ts +193 -0
- package/src/i18n/translations/ja-JP.ts +193 -0
- package/src/i18n/translations/zh-CN.ts +193 -0
- package/src/i18n/types.ts +224 -0
- package/src/index.ts +6 -0
- package/src/services/chat/ai-sidebar.service.ts +157 -5
- package/src/services/core/theme.service.ts +480 -0
- package/src/services/core/toast.service.ts +36 -0
- package/src/styles/ai-assistant.scss +8 -88
- 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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
333
|
+
this.toast.error(this.t.providers.apiKey + ': ' + this.t.providers.testError);
|
|
145
334
|
return;
|
|
146
335
|
}
|
|
147
336
|
|
|
148
|
-
const template =
|
|
337
|
+
const template = this.cloudProviderTemplates[providerName];
|
|
149
338
|
const providerDisplayName = template?.name || providerName;
|
|
150
339
|
|
|
151
340
|
// 显示测试中状态
|
|
152
|
-
const testingMessage =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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>
|
|
2
|
+
<h3>{{ t.security.title }}</h3>
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
<
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
<
|
|
32
|
-
|
|
33
|
-
<
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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>
|