tabby-ai-assistant 1.0.0
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/README.md +232 -0
- package/dist/components/chat/chat-input.component.d.ts +65 -0
- package/dist/components/chat/chat-interface.component.d.ts +71 -0
- package/dist/components/chat/chat-message.component.d.ts +53 -0
- package/dist/components/chat/chat-settings.component.d.ts +62 -0
- package/dist/components/common/error-message.component.d.ts +11 -0
- package/dist/components/common/loading-spinner.component.d.ts +4 -0
- package/dist/components/security/consent-dialog.component.d.ts +11 -0
- package/dist/components/security/password-prompt.component.d.ts +10 -0
- package/dist/components/security/risk-confirm-dialog.component.d.ts +36 -0
- package/dist/components/settings/ai-settings-tab.component.d.ts +72 -0
- package/dist/components/settings/general-settings.component.d.ts +60 -0
- package/dist/components/settings/provider-config.component.d.ts +182 -0
- package/dist/components/settings/security-settings.component.d.ts +23 -0
- package/dist/components/terminal/ai-toolbar-button.component.d.ts +10 -0
- package/dist/components/terminal/command-preview.component.d.ts +15 -0
- package/dist/components/terminal/command-suggestion.component.d.ts +16 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +2 -0
- package/dist/index.js.LICENSE.txt +18 -0
- package/dist/main.d.ts +8 -0
- package/dist/providers/tabby/ai-config.provider.d.ts +18 -0
- package/dist/providers/tabby/ai-hotkey.provider.d.ts +21 -0
- package/dist/providers/tabby/ai-settings-tab.provider.d.ts +11 -0
- package/dist/providers/tabby/ai-toolbar-button.provider.d.ts +17 -0
- package/dist/services/chat/chat-history.service.d.ts +67 -0
- package/dist/services/chat/chat-session.service.d.ts +58 -0
- package/dist/services/chat/command-generator.service.d.ts +49 -0
- package/dist/services/core/ai-assistant.service.d.ts +88 -0
- package/dist/services/core/ai-provider-manager.service.d.ts +119 -0
- package/dist/services/core/config-provider.service.d.ts +137 -0
- package/dist/services/core/logger.service.d.ts +21 -0
- package/dist/services/providers/anthropic-provider.service.d.ts +39 -0
- package/dist/services/providers/base-provider.service.d.ts +137 -0
- package/dist/services/providers/glm-provider.service.d.ts +91 -0
- package/dist/services/providers/minimax-provider.service.d.ts +93 -0
- package/dist/services/providers/openai-compatible.service.d.ts +39 -0
- package/dist/services/providers/openai-provider.service.d.ts +38 -0
- package/dist/services/security/consent-manager.service.d.ts +65 -0
- package/dist/services/security/password-manager.service.d.ts +67 -0
- package/dist/services/security/risk-assessment.service.d.ts +65 -0
- package/dist/services/security/security-validator.service.d.ts +36 -0
- package/dist/services/terminal/command-analyzer.service.d.ts +20 -0
- package/dist/services/terminal/context-menu.service.d.ts +24 -0
- package/dist/services/terminal/hotkey.service.d.ts +28 -0
- package/dist/services/terminal/terminal-context.service.d.ts +100 -0
- package/dist/types/ai.types.d.ts +107 -0
- package/dist/types/provider.types.d.ts +105 -0
- package/dist/types/security.types.d.ts +85 -0
- package/dist/types/terminal.types.d.ts +150 -0
- package/dist/utils/encryption.utils.d.ts +83 -0
- package/dist/utils/formatting.utils.d.ts +106 -0
- package/dist/utils/validation.utils.d.ts +83 -0
- package/integration-test-output.txt +50 -0
- package/integration-tests/api-integration.test.ts +183 -0
- package/jest.config.js +47 -0
- package/package.json +73 -0
- package/setup-jest.ts +37 -0
- package/src/components/chat/chat-input.component.html +61 -0
- package/src/components/chat/chat-input.component.scss +183 -0
- package/src/components/chat/chat-input.component.ts +149 -0
- package/src/components/chat/chat-interface.component.html +119 -0
- package/src/components/chat/chat-interface.component.scss +354 -0
- package/src/components/chat/chat-interface.component.ts +224 -0
- package/src/components/chat/chat-message.component.html +65 -0
- package/src/components/chat/chat-message.component.scss +178 -0
- package/src/components/chat/chat-message.component.ts +93 -0
- package/src/components/chat/chat-settings.component.html +132 -0
- package/src/components/chat/chat-settings.component.scss +172 -0
- package/src/components/chat/chat-settings.component.ts +168 -0
- package/src/components/common/error-message.component.ts +124 -0
- package/src/components/common/loading-spinner.component.ts +72 -0
- package/src/components/security/consent-dialog.component.ts +77 -0
- package/src/components/security/password-prompt.component.ts +79 -0
- package/src/components/security/risk-confirm-dialog.component.html +87 -0
- package/src/components/security/risk-confirm-dialog.component.scss +360 -0
- package/src/components/security/risk-confirm-dialog.component.ts +96 -0
- package/src/components/settings/ai-settings-tab.component.html +140 -0
- package/src/components/settings/ai-settings-tab.component.scss +371 -0
- package/src/components/settings/ai-settings-tab.component.ts +193 -0
- package/src/components/settings/general-settings.component.html +103 -0
- package/src/components/settings/general-settings.component.scss +285 -0
- package/src/components/settings/general-settings.component.ts +123 -0
- package/src/components/settings/provider-config.component.html +95 -0
- package/src/components/settings/provider-config.component.scss +60 -0
- package/src/components/settings/provider-config.component.ts +206 -0
- package/src/components/settings/security-settings.component.html +51 -0
- package/src/components/settings/security-settings.component.scss +66 -0
- package/src/components/settings/security-settings.component.ts +71 -0
- package/src/components/terminal/ai-toolbar-button.component.ts +49 -0
- package/src/components/terminal/command-preview.component.ts +185 -0
- package/src/components/terminal/command-suggestion.component.ts +128 -0
- package/src/index.ts +163 -0
- package/src/main.ts +16 -0
- package/src/providers/tabby/ai-config.provider.ts +70 -0
- package/src/providers/tabby/ai-hotkey.provider.ts +55 -0
- package/src/providers/tabby/ai-settings-tab.provider.ts +18 -0
- package/src/providers/tabby/ai-toolbar-button.provider.ts +49 -0
- package/src/services/chat/chat-history.service.ts +239 -0
- package/src/services/chat/chat-session.service.spec.ts +249 -0
- package/src/services/chat/chat-session.service.ts +180 -0
- package/src/services/chat/command-generator.service.ts +301 -0
- package/src/services/core/ai-assistant.service.ts +334 -0
- package/src/services/core/ai-provider-manager.service.ts +314 -0
- package/src/services/core/config-provider.service.ts +347 -0
- package/src/services/core/logger.service.ts +104 -0
- package/src/services/providers/anthropic-provider.service.ts +373 -0
- package/src/services/providers/base-provider.service.ts +369 -0
- package/src/services/providers/glm-provider.service.ts +467 -0
- package/src/services/providers/minimax-provider.service.ts +427 -0
- package/src/services/providers/openai-compatible.service.ts +394 -0
- package/src/services/providers/openai-provider.service.ts +376 -0
- package/src/services/security/consent-manager.service.ts +332 -0
- package/src/services/security/password-manager.service.ts +188 -0
- package/src/services/security/risk-assessment.service.ts +340 -0
- package/src/services/security/security-validator.service.ts +143 -0
- package/src/services/terminal/command-analyzer.service.ts +43 -0
- package/src/services/terminal/context-menu.service.ts +45 -0
- package/src/services/terminal/hotkey.service.ts +53 -0
- package/src/services/terminal/terminal-context.service.ts +317 -0
- package/src/styles/ai-assistant.scss +449 -0
- package/src/types/ai.types.ts +133 -0
- package/src/types/provider.types.ts +147 -0
- package/src/types/security.types.ts +103 -0
- package/src/types/terminal.types.ts +186 -0
- package/src/utils/encryption.utils.spec.ts +250 -0
- package/src/utils/encryption.utils.ts +271 -0
- package/src/utils/formatting.utils.ts +359 -0
- package/src/utils/validation.utils.spec.ts +225 -0
- package/src/utils/validation.utils.ts +314 -0
- package/tsconfig.json +45 -0
- package/webpack-docker.config.js +42 -0
- package/webpack.config.js +59 -0
- package/webpack.config.js.backup +57 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import { Injectable } from '@angular/core';
|
|
2
|
+
import axios, { AxiosInstance } from 'axios';
|
|
3
|
+
import { BaseAiProvider } from './base-provider.service';
|
|
4
|
+
import { ProviderCapability, HealthStatus, ValidationResult } from '../../types/provider.types';
|
|
5
|
+
import { ChatRequest, ChatResponse, CommandRequest, CommandResponse, ExplainRequest, ExplainResponse, AnalysisRequest, AnalysisResponse, MessageRole } from '../../types/ai.types';
|
|
6
|
+
import { LoggerService } from '../core/logger.service';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* OpenAI AI提供商
|
|
10
|
+
* 基于OpenAI API格式
|
|
11
|
+
*/
|
|
12
|
+
@Injectable()
|
|
13
|
+
export class OpenAiProviderService extends BaseAiProvider {
|
|
14
|
+
readonly name = 'openai';
|
|
15
|
+
readonly displayName = 'OpenAI (GPT-4)';
|
|
16
|
+
readonly capabilities = [
|
|
17
|
+
ProviderCapability.CHAT,
|
|
18
|
+
ProviderCapability.COMMAND_GENERATION,
|
|
19
|
+
ProviderCapability.COMMAND_EXPLANATION,
|
|
20
|
+
ProviderCapability.FUNCTION_CALL,
|
|
21
|
+
ProviderCapability.STREAMING
|
|
22
|
+
];
|
|
23
|
+
readonly authConfig = {
|
|
24
|
+
type: 'bearer' as const,
|
|
25
|
+
credentials: {
|
|
26
|
+
apiKey: ''
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
private client: AxiosInstance | null = null;
|
|
31
|
+
|
|
32
|
+
constructor(logger: LoggerService) {
|
|
33
|
+
super(logger);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
configure(config: any): void {
|
|
37
|
+
super.configure(config);
|
|
38
|
+
this.authConfig.credentials.apiKey = config.apiKey || '';
|
|
39
|
+
this.initializeClient();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private initializeClient(): void {
|
|
43
|
+
if (!this.config?.apiKey) {
|
|
44
|
+
this.logger.warn('OpenAI API key not provided');
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
this.client = axios.create({
|
|
50
|
+
baseURL: this.getBaseURL(),
|
|
51
|
+
timeout: this.getTimeout(),
|
|
52
|
+
headers: {
|
|
53
|
+
'Authorization': `Bearer ${this.config.apiKey}`,
|
|
54
|
+
'Content-Type': 'application/json'
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
this.logger.info('OpenAI client initialized', {
|
|
59
|
+
baseURL: this.getBaseURL(),
|
|
60
|
+
model: this.config.model || 'gpt-4'
|
|
61
|
+
});
|
|
62
|
+
} catch (error) {
|
|
63
|
+
this.logger.error('Failed to initialize OpenAI client', error);
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async chat(request: ChatRequest): Promise<ChatResponse> {
|
|
69
|
+
if (!this.client) {
|
|
70
|
+
throw new Error('OpenAI client not initialized');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
this.logRequest(request);
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const response = await this.withRetry(async () => {
|
|
77
|
+
const result = await this.client!.post('/chat/completions', {
|
|
78
|
+
model: this.config?.model || 'gpt-4',
|
|
79
|
+
messages: this.transformMessages(request.messages),
|
|
80
|
+
max_tokens: request.maxTokens || 1000,
|
|
81
|
+
temperature: request.temperature || 0.7,
|
|
82
|
+
stream: request.stream || false
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
this.logResponse(result.data);
|
|
86
|
+
return result.data;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return this.transformChatResponse(response);
|
|
90
|
+
|
|
91
|
+
} catch (error) {
|
|
92
|
+
this.logError(error, { request });
|
|
93
|
+
throw new Error(`OpenAI chat failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async generateCommand(request: CommandRequest): Promise<CommandResponse> {
|
|
98
|
+
const prompt = this.buildCommandPrompt(request);
|
|
99
|
+
|
|
100
|
+
const chatRequest: ChatRequest = {
|
|
101
|
+
messages: [
|
|
102
|
+
{
|
|
103
|
+
id: this.generateId(),
|
|
104
|
+
role: MessageRole.USER,
|
|
105
|
+
content: prompt,
|
|
106
|
+
timestamp: new Date()
|
|
107
|
+
}
|
|
108
|
+
],
|
|
109
|
+
maxTokens: 500,
|
|
110
|
+
temperature: 0.3
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const response = await this.chat(chatRequest);
|
|
114
|
+
return this.parseCommandResponse(response.message.content);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async explainCommand(request: ExplainRequest): Promise<ExplainResponse> {
|
|
118
|
+
const prompt = this.buildExplainPrompt(request);
|
|
119
|
+
|
|
120
|
+
const chatRequest: ChatRequest = {
|
|
121
|
+
messages: [
|
|
122
|
+
{
|
|
123
|
+
id: this.generateId(),
|
|
124
|
+
role: MessageRole.USER,
|
|
125
|
+
content: prompt,
|
|
126
|
+
timestamp: new Date()
|
|
127
|
+
}
|
|
128
|
+
],
|
|
129
|
+
maxTokens: 1000,
|
|
130
|
+
temperature: 0.5
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const response = await this.chat(chatRequest);
|
|
134
|
+
return this.parseExplainResponse(response.message.content);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async analyzeResult(request: AnalysisRequest): Promise<AnalysisResponse> {
|
|
138
|
+
const prompt = this.buildAnalysisPrompt(request);
|
|
139
|
+
|
|
140
|
+
const chatRequest: ChatRequest = {
|
|
141
|
+
messages: [
|
|
142
|
+
{
|
|
143
|
+
id: this.generateId(),
|
|
144
|
+
role: MessageRole.USER,
|
|
145
|
+
content: prompt,
|
|
146
|
+
timestamp: new Date()
|
|
147
|
+
}
|
|
148
|
+
],
|
|
149
|
+
maxTokens: 1000,
|
|
150
|
+
temperature: 0.7
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const response = await this.chat(chatRequest);
|
|
154
|
+
return this.parseAnalysisResponse(response.message.content);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async healthCheck(): Promise<HealthStatus> {
|
|
158
|
+
try {
|
|
159
|
+
if (!this.client) {
|
|
160
|
+
return HealthStatus.UNHEALTHY;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const response = await this.client.post('/chat/completions', {
|
|
164
|
+
model: this.config?.model || 'gpt-4',
|
|
165
|
+
max_tokens: 1,
|
|
166
|
+
messages: [
|
|
167
|
+
{
|
|
168
|
+
role: 'user',
|
|
169
|
+
content: 'Hi'
|
|
170
|
+
}
|
|
171
|
+
]
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
if (response.status === 200) {
|
|
175
|
+
this.lastHealthCheck = { status: HealthStatus.HEALTHY, timestamp: new Date() };
|
|
176
|
+
return HealthStatus.HEALTHY;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return HealthStatus.DEGRADED;
|
|
180
|
+
|
|
181
|
+
} catch (error) {
|
|
182
|
+
this.logger.error('OpenAI health check failed', error);
|
|
183
|
+
this.lastHealthCheck = { status: HealthStatus.UNHEALTHY, timestamp: new Date() };
|
|
184
|
+
return HealthStatus.UNHEALTHY;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
validateConfig(): ValidationResult {
|
|
189
|
+
const result = super.validateConfig();
|
|
190
|
+
|
|
191
|
+
if (!this.config?.apiKey) {
|
|
192
|
+
return {
|
|
193
|
+
valid: false,
|
|
194
|
+
errors: [...(result.errors || []), 'OpenAI API key is required']
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const supportedModels = ['gpt-4', 'gpt-4-turbo', 'gpt-3.5-turbo'];
|
|
199
|
+
if (this.config.model && !supportedModels.includes(this.config.model)) {
|
|
200
|
+
result.warnings = [
|
|
201
|
+
...(result.warnings || []),
|
|
202
|
+
`Model ${this.config.model} might not be supported. Supported models: ${supportedModels.join(', ')}`
|
|
203
|
+
];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
protected getDefaultBaseURL(): string {
|
|
210
|
+
return 'https://api.openai.com/v1';
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
protected transformMessages(messages: any[]): any[] {
|
|
214
|
+
return messages.map(msg => ({
|
|
215
|
+
role: msg.role,
|
|
216
|
+
content: msg.content
|
|
217
|
+
}));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private transformChatResponse(response: any): ChatResponse {
|
|
221
|
+
const choice = response.choices?.[0];
|
|
222
|
+
const content = choice?.message?.content || '';
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
message: {
|
|
226
|
+
id: this.generateId(),
|
|
227
|
+
role: MessageRole.ASSISTANT,
|
|
228
|
+
content,
|
|
229
|
+
timestamp: new Date()
|
|
230
|
+
},
|
|
231
|
+
usage: response.usage ? {
|
|
232
|
+
promptTokens: response.usage.prompt_tokens || 0,
|
|
233
|
+
completionTokens: response.usage.completion_tokens || 0,
|
|
234
|
+
totalTokens: response.usage.total_tokens || 0
|
|
235
|
+
} : undefined
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private buildCommandPrompt(request: CommandRequest): string {
|
|
240
|
+
let prompt = `请将以下自然语言描述转换为准确的终端命令:\n\n"${request.naturalLanguage}"\n\n`;
|
|
241
|
+
|
|
242
|
+
if (request.context) {
|
|
243
|
+
prompt += `当前环境:\n`;
|
|
244
|
+
if (request.context.currentDirectory) {
|
|
245
|
+
prompt += `- 当前目录:${request.context.currentDirectory}\n`;
|
|
246
|
+
}
|
|
247
|
+
if (request.context.operatingSystem) {
|
|
248
|
+
prompt += `- 操作系统:${request.context.operatingSystem}\n`;
|
|
249
|
+
}
|
|
250
|
+
if (request.context.shell) {
|
|
251
|
+
prompt += `- Shell:${request.context.shell}\n`;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
prompt += `\n请直接返回JSON格式:\n`;
|
|
256
|
+
prompt += `{\n`;
|
|
257
|
+
prompt += ` "command": "具体命令",\n`;
|
|
258
|
+
prompt += ` "explanation": "命令解释",\n`;
|
|
259
|
+
prompt += ` "confidence": 0.95\n`;
|
|
260
|
+
prompt += `}\n`;
|
|
261
|
+
|
|
262
|
+
return prompt;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private buildExplainPrompt(request: ExplainRequest): string {
|
|
266
|
+
let prompt = `请详细解释以下终端命令:\n\n\`${request.command}\`\n\n`;
|
|
267
|
+
|
|
268
|
+
if (request.context?.currentDirectory) {
|
|
269
|
+
prompt += `当前目录:${request.context.currentDirectory}\n`;
|
|
270
|
+
}
|
|
271
|
+
if (request.context?.operatingSystem) {
|
|
272
|
+
prompt += `操作系统:${request.context.operatingSystem}\n`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
prompt += `\n请按以下JSON格式返回:\n`;
|
|
276
|
+
prompt += `{\n`;
|
|
277
|
+
prompt += ` "explanation": "整体解释",\n`;
|
|
278
|
+
prompt += ` "breakdown": [\n`;
|
|
279
|
+
prompt += ` {"part": "命令部分", "description": "说明"}\n`;
|
|
280
|
+
prompt += ` ],\n`;
|
|
281
|
+
prompt += ` "examples": ["使用示例"]\n`;
|
|
282
|
+
prompt += `}\n`;
|
|
283
|
+
|
|
284
|
+
return prompt;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private buildAnalysisPrompt(request: AnalysisRequest): string {
|
|
288
|
+
let prompt = `请分析以下命令执行结果:\n\n`;
|
|
289
|
+
prompt += `命令:${request.command}\n`;
|
|
290
|
+
prompt += `退出码:${request.exitCode}\n`;
|
|
291
|
+
prompt += `输出:\n${request.output}\n\n`;
|
|
292
|
+
|
|
293
|
+
if (request.context?.workingDirectory) {
|
|
294
|
+
prompt += `工作目录:${request.context.workingDirectory}\n`;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
prompt += `\n请按以下JSON格式返回:\n`;
|
|
298
|
+
prompt += `{\n`;
|
|
299
|
+
prompt += ` "summary": "结果总结",\n`;
|
|
300
|
+
prompt += ` "insights": ["洞察1", "洞察2"],\n`;
|
|
301
|
+
prompt += ` "success": true/false,\n`;
|
|
302
|
+
prompt += ` "issues": [\n`;
|
|
303
|
+
prompt += ` {"severity": "warning|error|info", "message": "问题描述", "suggestion": "建议"}\n`;
|
|
304
|
+
prompt += ` ]\n`;
|
|
305
|
+
prompt += `}\n`;
|
|
306
|
+
|
|
307
|
+
return prompt;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
private parseCommandResponse(content: string): CommandResponse {
|
|
311
|
+
try {
|
|
312
|
+
const match = content.match(/\{[\s\S]*\}/);
|
|
313
|
+
if (match) {
|
|
314
|
+
const parsed = JSON.parse(match[0]);
|
|
315
|
+
return {
|
|
316
|
+
command: parsed.command || '',
|
|
317
|
+
explanation: parsed.explanation || '',
|
|
318
|
+
confidence: parsed.confidence || 0.5
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
} catch (error) {
|
|
322
|
+
this.logger.warn('Failed to parse command response as JSON', error);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const lines = content.split('\n').map(l => l.trim()).filter(l => l);
|
|
326
|
+
return {
|
|
327
|
+
command: lines[0] || '',
|
|
328
|
+
explanation: lines.slice(1).join(' ') || 'AI生成的命令',
|
|
329
|
+
confidence: 0.5
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
private parseExplainResponse(content: string): ExplainResponse {
|
|
334
|
+
try {
|
|
335
|
+
const match = content.match(/\{[\s\S]*\}/);
|
|
336
|
+
if (match) {
|
|
337
|
+
const parsed = JSON.parse(match[0]);
|
|
338
|
+
return {
|
|
339
|
+
explanation: parsed.explanation || '',
|
|
340
|
+
breakdown: parsed.breakdown || [],
|
|
341
|
+
examples: parsed.examples || []
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
} catch (error) {
|
|
345
|
+
this.logger.warn('Failed to parse explain response as JSON', error);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
explanation: content,
|
|
350
|
+
breakdown: []
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
private parseAnalysisResponse(content: string): AnalysisResponse {
|
|
355
|
+
try {
|
|
356
|
+
const match = content.match(/\{[\s\S]*\}/);
|
|
357
|
+
if (match) {
|
|
358
|
+
const parsed = JSON.parse(match[0]);
|
|
359
|
+
return {
|
|
360
|
+
summary: parsed.summary || '',
|
|
361
|
+
insights: parsed.insights || [],
|
|
362
|
+
success: parsed.success !== false,
|
|
363
|
+
issues: parsed.issues || []
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
} catch (error) {
|
|
367
|
+
this.logger.warn('Failed to parse analysis response as JSON', error);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return {
|
|
371
|
+
summary: content,
|
|
372
|
+
insights: [],
|
|
373
|
+
success: true
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
}
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { Injectable } from '@angular/core';
|
|
2
|
+
import * as CryptoJS from 'crypto-js';
|
|
3
|
+
import { RiskLevel, StoredConsent } from '../../types/security.types';
|
|
4
|
+
import { LoggerService } from '../core/logger.service';
|
|
5
|
+
|
|
6
|
+
@Injectable({ providedIn: 'root' })
|
|
7
|
+
export class ConsentManagerService {
|
|
8
|
+
private readonly CONSENT_KEY_PREFIX = 'ai-assistant-consent-';
|
|
9
|
+
private readonly DEFAULT_EXPIRY = 30 * 24 * 60 * 60 * 1000; // 30天
|
|
10
|
+
|
|
11
|
+
constructor(private logger: LoggerService) {}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 存储用户同意
|
|
15
|
+
*/
|
|
16
|
+
async storeConsent(command: string, riskLevel: RiskLevel): Promise<void> {
|
|
17
|
+
const consent: StoredConsent = {
|
|
18
|
+
commandHash: this.hashCommand(command),
|
|
19
|
+
riskLevel,
|
|
20
|
+
timestamp: Date.now(),
|
|
21
|
+
expiry: Date.now() + this.DEFAULT_EXPIRY
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
localStorage.setItem(
|
|
25
|
+
this.CONSENT_KEY_PREFIX + consent.commandHash,
|
|
26
|
+
JSON.stringify(consent)
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
this.logger.debug('User consent stored', { commandHash: consent.commandHash, riskLevel });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 检查是否有用户同意
|
|
34
|
+
*/
|
|
35
|
+
async hasConsent(command: string, riskLevel: RiskLevel): Promise<boolean> {
|
|
36
|
+
const hash = this.hashCommand(command);
|
|
37
|
+
const stored = localStorage.getItem(this.CONSENT_KEY_PREFIX + hash);
|
|
38
|
+
|
|
39
|
+
if (!stored) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const consent: StoredConsent = JSON.parse(stored);
|
|
45
|
+
|
|
46
|
+
// 检查是否过期
|
|
47
|
+
if (Date.now() > consent.expiry) {
|
|
48
|
+
localStorage.removeItem(this.CONSENT_KEY_PREFIX + hash);
|
|
49
|
+
this.logger.debug('Expired consent removed', { commandHash: hash });
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 检查风险级别是否匹配
|
|
54
|
+
const levelMatch = consent.riskLevel === riskLevel;
|
|
55
|
+
if (!levelMatch) {
|
|
56
|
+
this.logger.debug('Risk level mismatch', {
|
|
57
|
+
stored: consent.riskLevel,
|
|
58
|
+
current: riskLevel
|
|
59
|
+
});
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
this.logger.debug('Valid consent found', { commandHash: hash });
|
|
64
|
+
return true;
|
|
65
|
+
|
|
66
|
+
} catch (error) {
|
|
67
|
+
this.logger.error('Failed to parse consent', error);
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 请求用户同意
|
|
74
|
+
*/
|
|
75
|
+
async requestConsent(
|
|
76
|
+
command: string,
|
|
77
|
+
explanation: string,
|
|
78
|
+
riskLevel: RiskLevel
|
|
79
|
+
): Promise<boolean> {
|
|
80
|
+
// 显示确认对话框
|
|
81
|
+
const riskText = this.getRiskLevelText(riskLevel);
|
|
82
|
+
const message = `此命令为${riskText}风险:
|
|
83
|
+
|
|
84
|
+
命令: ${command}
|
|
85
|
+
|
|
86
|
+
解释: ${explanation}
|
|
87
|
+
|
|
88
|
+
确定要执行此命令吗?`;
|
|
89
|
+
|
|
90
|
+
const confirmed = confirm(message);
|
|
91
|
+
|
|
92
|
+
if (confirmed) {
|
|
93
|
+
this.logger.info('User granted consent', { command, riskLevel });
|
|
94
|
+
} else {
|
|
95
|
+
this.logger.info('User denied consent', { command, riskLevel });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return confirmed;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 清除所有同意
|
|
103
|
+
*/
|
|
104
|
+
async clearAllConsents(): Promise<void> {
|
|
105
|
+
const keys = Object.keys(localStorage);
|
|
106
|
+
const consentKeys = keys.filter(key => key.startsWith(this.CONSENT_KEY_PREFIX));
|
|
107
|
+
|
|
108
|
+
consentKeys.forEach(key => {
|
|
109
|
+
localStorage.removeItem(key);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
this.logger.info('All consents cleared', { count: consentKeys.length });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 获取所有同意数量
|
|
117
|
+
*/
|
|
118
|
+
async getActiveConsentsCount(): Promise<number> {
|
|
119
|
+
const keys = Object.keys(localStorage);
|
|
120
|
+
const consentKeys = keys.filter(key => key.startsWith(this.CONSENT_KEY_PREFIX));
|
|
121
|
+
|
|
122
|
+
let activeCount = 0;
|
|
123
|
+
const now = Date.now();
|
|
124
|
+
|
|
125
|
+
consentKeys.forEach(key => {
|
|
126
|
+
try {
|
|
127
|
+
const consentStr = localStorage.getItem(key);
|
|
128
|
+
if (consentStr) {
|
|
129
|
+
const consent: StoredConsent = JSON.parse(consentStr);
|
|
130
|
+
if (now <= consent.expiry) {
|
|
131
|
+
activeCount++;
|
|
132
|
+
} else {
|
|
133
|
+
// 删除过期的
|
|
134
|
+
localStorage.removeItem(key);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
} catch (error) {
|
|
138
|
+
// 删除损坏的
|
|
139
|
+
localStorage.removeItem(key);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
return activeCount;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* 获取总验证次数
|
|
148
|
+
*/
|
|
149
|
+
async getTotalValidations(): Promise<number> {
|
|
150
|
+
const keys = Object.keys(localStorage);
|
|
151
|
+
const consentKeys = keys.filter(key => key.startsWith(this.CONSENT_KEY_PREFIX));
|
|
152
|
+
return consentKeys.length;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 获取同意统计
|
|
157
|
+
*/
|
|
158
|
+
async getConsentStats(): Promise<{
|
|
159
|
+
total: number;
|
|
160
|
+
byLevel: { [key in RiskLevel]: number };
|
|
161
|
+
expired: number;
|
|
162
|
+
}> {
|
|
163
|
+
const stats = {
|
|
164
|
+
total: 0,
|
|
165
|
+
byLevel: {
|
|
166
|
+
[RiskLevel.LOW]: 0,
|
|
167
|
+
[RiskLevel.MEDIUM]: 0,
|
|
168
|
+
[RiskLevel.HIGH]: 0,
|
|
169
|
+
[RiskLevel.CRITICAL]: 0
|
|
170
|
+
},
|
|
171
|
+
expired: 0
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const keys = Object.keys(localStorage);
|
|
175
|
+
const consentKeys = keys.filter(key => key.startsWith(this.CONSENT_KEY_PREFIX));
|
|
176
|
+
const now = Date.now();
|
|
177
|
+
|
|
178
|
+
consentKeys.forEach(key => {
|
|
179
|
+
try {
|
|
180
|
+
const consentStr = localStorage.getItem(key);
|
|
181
|
+
if (consentStr) {
|
|
182
|
+
const consent: StoredConsent = JSON.parse(consentStr);
|
|
183
|
+
stats.total++;
|
|
184
|
+
|
|
185
|
+
if (now <= consent.expiry) {
|
|
186
|
+
stats.byLevel[consent.riskLevel]++;
|
|
187
|
+
} else {
|
|
188
|
+
stats.expired++;
|
|
189
|
+
// 删除过期的
|
|
190
|
+
localStorage.removeItem(key);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
} catch (error) {
|
|
194
|
+
// 删除损坏的
|
|
195
|
+
localStorage.removeItem(key);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
return stats;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* 清理过期的同意
|
|
204
|
+
*/
|
|
205
|
+
async cleanupExpiredConsents(): Promise<number> {
|
|
206
|
+
const keys = Object.keys(localStorage);
|
|
207
|
+
const consentKeys = keys.filter(key => key.startsWith(this.CONSENT_KEY_PREFIX));
|
|
208
|
+
let removedCount = 0;
|
|
209
|
+
const now = Date.now();
|
|
210
|
+
|
|
211
|
+
consentKeys.forEach(key => {
|
|
212
|
+
try {
|
|
213
|
+
const consentStr = localStorage.getItem(key);
|
|
214
|
+
if (consentStr) {
|
|
215
|
+
const consent: StoredConsent = JSON.parse(consentStr);
|
|
216
|
+
if (now > consent.expiry) {
|
|
217
|
+
localStorage.removeItem(key);
|
|
218
|
+
removedCount++;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
} catch (error) {
|
|
222
|
+
// 删除损坏的
|
|
223
|
+
localStorage.removeItem(key);
|
|
224
|
+
removedCount++;
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
if (removedCount > 0) {
|
|
229
|
+
this.logger.info('Expired consents cleaned up', { removedCount });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return removedCount;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* 获取风险级别文本
|
|
237
|
+
*/
|
|
238
|
+
private getRiskLevelText(riskLevel: RiskLevel): string {
|
|
239
|
+
switch (riskLevel) {
|
|
240
|
+
case RiskLevel.LOW:
|
|
241
|
+
return '低';
|
|
242
|
+
case RiskLevel.MEDIUM:
|
|
243
|
+
return '中';
|
|
244
|
+
case RiskLevel.HIGH:
|
|
245
|
+
return '高';
|
|
246
|
+
case RiskLevel.CRITICAL:
|
|
247
|
+
return '极';
|
|
248
|
+
default:
|
|
249
|
+
return '未知';
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* 对命令进行哈希
|
|
255
|
+
*/
|
|
256
|
+
private hashCommand(command: string): string {
|
|
257
|
+
// 规范化命令(去除多余空格,转换为小写)
|
|
258
|
+
const normalized = command.trim().toLowerCase();
|
|
259
|
+
return CryptoJS.SHA256(normalized).toString();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* 导出同意数据
|
|
264
|
+
*/
|
|
265
|
+
exportConsents(): string {
|
|
266
|
+
const consents: StoredConsent[] = [];
|
|
267
|
+
const keys = Object.keys(localStorage);
|
|
268
|
+
const consentKeys = keys.filter(key => key.startsWith(this.CONSENT_KEY_PREFIX));
|
|
269
|
+
|
|
270
|
+
consentKeys.forEach(key => {
|
|
271
|
+
try {
|
|
272
|
+
const consentStr = localStorage.getItem(key);
|
|
273
|
+
if (consentStr) {
|
|
274
|
+
consents.push(JSON.parse(consentStr));
|
|
275
|
+
}
|
|
276
|
+
} catch (error) {
|
|
277
|
+
// 跳过损坏的数据
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
return JSON.stringify({
|
|
282
|
+
exportTime: new Date().toISOString(),
|
|
283
|
+
consents
|
|
284
|
+
}, null, 2);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* 导入同意数据
|
|
289
|
+
*/
|
|
290
|
+
importConsents(data: string): Promise<{ imported: number; skipped: number }> {
|
|
291
|
+
return new Promise((resolve, reject) => {
|
|
292
|
+
try {
|
|
293
|
+
const parsed = JSON.parse(data);
|
|
294
|
+
if (!parsed.consents || !Array.isArray(parsed.consents)) {
|
|
295
|
+
throw new Error('Invalid data format');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
let imported = 0;
|
|
299
|
+
let skipped = 0;
|
|
300
|
+
|
|
301
|
+
parsed.consents.forEach((consent: StoredConsent) => {
|
|
302
|
+
try {
|
|
303
|
+
// 检查是否已存在且未过期
|
|
304
|
+
const existing = localStorage.getItem(this.CONSENT_KEY_PREFIX + consent.commandHash);
|
|
305
|
+
if (existing) {
|
|
306
|
+
const existingConsent: StoredConsent = JSON.parse(existing);
|
|
307
|
+
if (Date.now() <= existingConsent.expiry) {
|
|
308
|
+
skipped++;
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// 导入同意
|
|
314
|
+
localStorage.setItem(
|
|
315
|
+
this.CONSENT_KEY_PREFIX + consent.commandHash,
|
|
316
|
+
JSON.stringify(consent)
|
|
317
|
+
);
|
|
318
|
+
imported++;
|
|
319
|
+
} catch (error) {
|
|
320
|
+
skipped++;
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
this.logger.info('Consents imported', { imported, skipped });
|
|
325
|
+
resolve({ imported, skipped });
|
|
326
|
+
|
|
327
|
+
} catch (error) {
|
|
328
|
+
reject(error);
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|