tabby-ai-assistant 1.0.12 → 1.0.15
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/.editorconfig +18 -0
- package/README.md +113 -55
- package/dist/index.js +1 -1
- package/package.json +6 -4
- package/src/components/chat/ai-sidebar.component.scss +220 -9
- package/src/components/chat/ai-sidebar.component.ts +364 -29
- package/src/components/chat/chat-input.component.ts +36 -4
- package/src/components/chat/chat-interface.component.ts +225 -5
- package/src/components/chat/chat-message.component.ts +6 -1
- package/src/components/settings/context-settings.component.ts +91 -91
- package/src/components/terminal/ai-toolbar-button.component.ts +4 -2
- package/src/components/terminal/command-suggestion.component.ts +148 -6
- package/src/index.ts +0 -6
- package/src/providers/tabby/ai-toolbar-button.provider.ts +7 -3
- package/src/services/chat/ai-sidebar.service.ts +414 -410
- package/src/services/chat/chat-session.service.ts +36 -12
- package/src/services/context/compaction.ts +110 -134
- package/src/services/context/manager.ts +27 -7
- package/src/services/context/memory.ts +17 -33
- package/src/services/context/summary.service.ts +136 -0
- package/src/services/core/ai-assistant.service.ts +1060 -37
- package/src/services/core/ai-provider-manager.service.ts +154 -25
- package/src/services/core/checkpoint.service.ts +218 -18
- package/src/services/core/config-provider.service.ts +4 -12
- package/src/services/core/toast.service.ts +106 -106
- package/src/services/providers/anthropic-provider.service.ts +126 -202
- package/src/services/providers/base-provider.service.ts +315 -21
- package/src/services/providers/glm-provider.service.ts +151 -233
- package/src/services/providers/minimax-provider.service.ts +55 -238
- package/src/services/providers/ollama-provider.service.ts +117 -188
- package/src/services/providers/openai-compatible.service.ts +165 -177
- package/src/services/providers/openai-provider.service.ts +170 -177
- package/src/services/providers/vllm-provider.service.ts +116 -188
- package/src/services/terminal/terminal-context.service.ts +265 -5
- package/src/services/terminal/terminal-manager.service.ts +748 -748
- package/src/services/terminal/terminal-tools.service.ts +612 -441
- package/src/types/ai.types.ts +156 -3
- package/src/types/provider.types.ts +206 -75
- package/src/utils/cost.utils.ts +249 -0
- package/src/utils/validation.utils.ts +306 -2
- package/dist/index.js.LICENSE.txt +0 -18
- package/src/index.ts.backup +0 -165
- package/src/services/chat/chat-history.service.ts.backup +0 -239
- package/src/services/terminal/command-analyzer.service.ts +0 -43
- package/src/services/terminal/context-menu.service.ts +0 -45
- package/src/services/terminal/hotkey.service.ts +0 -53
- package/webpack.config.js.backup +0 -57
|
@@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
|
|
|
2
2
|
import { Observable, Observer } from 'rxjs';
|
|
3
3
|
import { Anthropic } from '@anthropic-ai/sdk';
|
|
4
4
|
import { BaseAiProvider } from './base-provider.service';
|
|
5
|
-
import { ProviderCapability,
|
|
5
|
+
import { ProviderCapability, ValidationResult } from '../../types/provider.types';
|
|
6
6
|
import { ChatRequest, ChatResponse, CommandRequest, CommandResponse, ExplainRequest, ExplainResponse, AnalysisRequest, AnalysisResponse, MessageRole, StreamEvent } from '../../types/ai.types';
|
|
7
7
|
import { LoggerService } from '../core/logger.service';
|
|
8
8
|
|
|
@@ -116,12 +116,16 @@ export class MinimaxProviderService extends BaseAiProvider {
|
|
|
116
116
|
chatStream(request: ChatRequest): Observable<StreamEvent> {
|
|
117
117
|
return new Observable<StreamEvent>((subscriber: Observer<StreamEvent>) => {
|
|
118
118
|
if (!this.client) {
|
|
119
|
-
|
|
119
|
+
const error = new Error('Minimax client not initialized');
|
|
120
|
+
subscriber.next({ type: 'error', error: error.message });
|
|
121
|
+
subscriber.error(error);
|
|
120
122
|
return;
|
|
121
123
|
}
|
|
122
124
|
|
|
123
125
|
this.logRequest(request);
|
|
124
126
|
|
|
127
|
+
const abortController = new AbortController();
|
|
128
|
+
|
|
125
129
|
const runStream = async () => {
|
|
126
130
|
try {
|
|
127
131
|
// 注意:SDK 类型定义可能不包含 tools,但 API 实际支持
|
|
@@ -141,6 +145,10 @@ export class MinimaxProviderService extends BaseAiProvider {
|
|
|
141
145
|
let currentToolInput = '';
|
|
142
146
|
|
|
143
147
|
for await (const event of stream) {
|
|
148
|
+
if (abortController.signal.aborted) break;
|
|
149
|
+
|
|
150
|
+
this.logger.debug('Stream event', { type: event.type });
|
|
151
|
+
|
|
144
152
|
if (event.type === 'content_block_delta') {
|
|
145
153
|
const delta = event.delta as any;
|
|
146
154
|
// 文本增量
|
|
@@ -162,7 +170,15 @@ export class MinimaxProviderService extends BaseAiProvider {
|
|
|
162
170
|
currentToolId = block.id;
|
|
163
171
|
currentToolName = block.name;
|
|
164
172
|
currentToolInput = '';
|
|
165
|
-
subscriber.next({
|
|
173
|
+
subscriber.next({
|
|
174
|
+
type: 'tool_use_start',
|
|
175
|
+
toolCall: {
|
|
176
|
+
id: currentToolId,
|
|
177
|
+
name: currentToolName,
|
|
178
|
+
input: {}
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
this.logger.debug('Stream event', { type: 'tool_use_start', name: currentToolName });
|
|
166
182
|
}
|
|
167
183
|
}
|
|
168
184
|
// 内容块结束
|
|
@@ -183,6 +199,7 @@ export class MinimaxProviderService extends BaseAiProvider {
|
|
|
183
199
|
input: parsedInput
|
|
184
200
|
}
|
|
185
201
|
});
|
|
202
|
+
this.logger.debug('Stream event', { type: 'tool_use_end', name: currentToolName });
|
|
186
203
|
// 重置
|
|
187
204
|
currentToolId = '';
|
|
188
205
|
currentToolName = '';
|
|
@@ -197,19 +214,22 @@ export class MinimaxProviderService extends BaseAiProvider {
|
|
|
197
214
|
type: 'message_end',
|
|
198
215
|
message: this.transformChatResponse(finalMessage).message
|
|
199
216
|
});
|
|
217
|
+
this.logger.debug('Stream event', { type: 'message_end' });
|
|
200
218
|
subscriber.complete();
|
|
201
219
|
} catch (error) {
|
|
202
|
-
|
|
203
|
-
|
|
220
|
+
if ((error as any).name !== 'AbortError') {
|
|
221
|
+
const errorMessage = `Minimax stream failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
222
|
+
this.logError(error, { request });
|
|
223
|
+
subscriber.next({ type: 'error', error: errorMessage });
|
|
224
|
+
subscriber.error(new Error(errorMessage));
|
|
225
|
+
}
|
|
204
226
|
}
|
|
205
227
|
};
|
|
206
228
|
|
|
207
229
|
runStream();
|
|
208
230
|
|
|
209
231
|
// 返回取消订阅的处理函数
|
|
210
|
-
return () =>
|
|
211
|
-
this.logger.debug('Stream subscription cancelled');
|
|
212
|
-
};
|
|
232
|
+
return () => abortController.abort();
|
|
213
233
|
});
|
|
214
234
|
}
|
|
215
235
|
|
|
@@ -282,35 +302,19 @@ export class MinimaxProviderService extends BaseAiProvider {
|
|
|
282
302
|
return this.parseAnalysisResponse(response.message.content);
|
|
283
303
|
}
|
|
284
304
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
try {
|
|
290
|
-
if (!this.client) {
|
|
291
|
-
return HealthStatus.UNHEALTHY;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// 简单的测试请求
|
|
295
|
-
const _response = await this.client.messages.create({
|
|
296
|
-
model: this.config?.model || 'MiniMax-M2',
|
|
297
|
-
max_tokens: 1,
|
|
298
|
-
messages: [
|
|
299
|
-
{
|
|
300
|
-
role: 'user',
|
|
301
|
-
content: 'Hi'
|
|
302
|
-
}
|
|
303
|
-
]
|
|
304
|
-
});
|
|
305
|
+
protected async sendTestRequest(request: ChatRequest): Promise<ChatResponse> {
|
|
306
|
+
if (!this.client) {
|
|
307
|
+
throw new Error('Minimax client not initialized');
|
|
308
|
+
}
|
|
305
309
|
|
|
306
|
-
|
|
307
|
-
|
|
310
|
+
const response = await this.client.messages.create({
|
|
311
|
+
model: this.config?.model || 'MiniMax-M2',
|
|
312
|
+
max_tokens: request.maxTokens || 1,
|
|
313
|
+
messages: this.transformMessages(request.messages),
|
|
314
|
+
temperature: request.temperature || 0
|
|
315
|
+
});
|
|
308
316
|
|
|
309
|
-
|
|
310
|
-
this.logger.error('Minimax health check failed', error);
|
|
311
|
-
this.lastHealthCheck = { status: HealthStatus.UNHEALTHY, timestamp: new Date() };
|
|
312
|
-
return HealthStatus.UNHEALTHY;
|
|
313
|
-
}
|
|
317
|
+
return this.transformChatResponse(response);
|
|
314
318
|
}
|
|
315
319
|
|
|
316
320
|
/**
|
|
@@ -334,14 +338,6 @@ export class MinimaxProviderService extends BaseAiProvider {
|
|
|
334
338
|
return result;
|
|
335
339
|
}
|
|
336
340
|
|
|
337
|
-
/**
|
|
338
|
-
* 获取默认基础URL
|
|
339
|
-
*/
|
|
340
|
-
protected getDefaultBaseURL(): string {
|
|
341
|
-
// 支持中国和国际端点
|
|
342
|
-
return 'https://api.minimaxi.com/anthropic';
|
|
343
|
-
}
|
|
344
|
-
|
|
345
341
|
/**
|
|
346
342
|
* 转换消息格式
|
|
347
343
|
* Anthropic API 支持两种格式:
|
|
@@ -351,8 +347,9 @@ export class MinimaxProviderService extends BaseAiProvider {
|
|
|
351
347
|
*/
|
|
352
348
|
protected transformMessages(messages: any[]): any[] {
|
|
353
349
|
// 过滤掉系统消息(system role 不应该在 messages 数组中)
|
|
350
|
+
// 保留 user, assistant, tool 角色的消息
|
|
354
351
|
const filteredMessages = messages.filter(msg =>
|
|
355
|
-
msg.role === 'user' || msg.role === 'assistant'
|
|
352
|
+
msg.role === 'user' || msg.role === 'assistant' || msg.role === 'tool'
|
|
356
353
|
);
|
|
357
354
|
|
|
358
355
|
this.logger.info('Transforming messages', {
|
|
@@ -361,10 +358,20 @@ export class MinimaxProviderService extends BaseAiProvider {
|
|
|
361
358
|
roles: messages.map(m => m.role)
|
|
362
359
|
});
|
|
363
360
|
|
|
364
|
-
return filteredMessages.map(msg =>
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
361
|
+
return filteredMessages.map(msg => {
|
|
362
|
+
// tool 角色消息转换为 user 角色(Anthropic API 不直接支持 tool 角色)
|
|
363
|
+
// 但内容保留工具结果标识,让 AI 理解这是工具执行结果
|
|
364
|
+
if (msg.role === 'tool') {
|
|
365
|
+
return {
|
|
366
|
+
role: 'user',
|
|
367
|
+
content: String(msg.content || '')
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
return {
|
|
371
|
+
role: msg.role === 'user' ? 'user' : 'assistant',
|
|
372
|
+
content: String(msg.content || '')
|
|
373
|
+
};
|
|
374
|
+
});
|
|
368
375
|
}
|
|
369
376
|
|
|
370
377
|
/**
|
|
@@ -440,194 +447,4 @@ export class MinimaxProviderService extends BaseAiProvider {
|
|
|
440
447
|
|
|
441
448
|
return result;
|
|
442
449
|
}
|
|
443
|
-
|
|
444
|
-
/**
|
|
445
|
-
* 构建命令生成提示
|
|
446
|
-
*/
|
|
447
|
-
private buildCommandPrompt(request: CommandRequest): string {
|
|
448
|
-
let prompt = `请将以下自然语言描述转换为准确的终端命令:\n\n"${request.naturalLanguage}"\n\n`;
|
|
449
|
-
|
|
450
|
-
if (request.context) {
|
|
451
|
-
prompt += `当前环境:\n`;
|
|
452
|
-
if (request.context.currentDirectory) {
|
|
453
|
-
prompt += `- 当前目录:${request.context.currentDirectory}\n`;
|
|
454
|
-
}
|
|
455
|
-
if (request.context.operatingSystem) {
|
|
456
|
-
prompt += `- 操作系统:${request.context.operatingSystem}\n`;
|
|
457
|
-
}
|
|
458
|
-
if (request.context.shell) {
|
|
459
|
-
prompt += `- Shell:${request.context.shell}\n`;
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
prompt += `\n请直接返回JSON格式:\n`;
|
|
464
|
-
prompt += `{\n`;
|
|
465
|
-
prompt += ` "command": "具体命令",\n`;
|
|
466
|
-
prompt += ` "explanation": "命令解释",\n`;
|
|
467
|
-
prompt += ` "confidence": 0.95\n`;
|
|
468
|
-
prompt += `}\n`;
|
|
469
|
-
|
|
470
|
-
return prompt;
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
/**
|
|
474
|
-
* 构建命令解释提示
|
|
475
|
-
*/
|
|
476
|
-
private buildExplainPrompt(request: ExplainRequest): string {
|
|
477
|
-
let prompt = `请详细解释以下终端命令:\n\n\`${request.command}\`\n\n`;
|
|
478
|
-
|
|
479
|
-
if (request.context?.currentDirectory) {
|
|
480
|
-
prompt += `当前目录:${request.context.currentDirectory}\n`;
|
|
481
|
-
}
|
|
482
|
-
if (request.context?.operatingSystem) {
|
|
483
|
-
prompt += `操作系统:${request.context.operatingSystem}\n`;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
prompt += `\n请按以下JSON格式返回:\n`;
|
|
487
|
-
prompt += `{\n`;
|
|
488
|
-
prompt += ` "explanation": "整体解释",\n`;
|
|
489
|
-
prompt += ` "breakdown": [\n`;
|
|
490
|
-
prompt += ` {"part": "命令部分", "description": "说明"}\n`;
|
|
491
|
-
prompt += ` ],\n`;
|
|
492
|
-
prompt += ` "examples": ["使用示例"]\n`;
|
|
493
|
-
prompt += `}\n`;
|
|
494
|
-
|
|
495
|
-
return prompt;
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
/**
|
|
499
|
-
* 构建结果分析提示
|
|
500
|
-
*/
|
|
501
|
-
private buildAnalysisPrompt(request: AnalysisRequest): string {
|
|
502
|
-
let prompt = `请分析以下命令执行结果:\n\n`;
|
|
503
|
-
prompt += `命令:${request.command}\n`;
|
|
504
|
-
prompt += `退出码:${request.exitCode}\n`;
|
|
505
|
-
prompt += `输出:\n${request.output}\n\n`;
|
|
506
|
-
|
|
507
|
-
if (request.context?.workingDirectory) {
|
|
508
|
-
prompt += `工作目录:${request.context.workingDirectory}\n`;
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
prompt += `\n请按以下JSON格式返回:\n`;
|
|
512
|
-
prompt += `{\n`;
|
|
513
|
-
prompt += ` "summary": "结果总结",\n`;
|
|
514
|
-
prompt += ` "insights": ["洞察1", "洞察2"],\n`;
|
|
515
|
-
prompt += ` "success": true/false,\n`;
|
|
516
|
-
prompt += ` "issues": [\n`;
|
|
517
|
-
prompt += ` {"severity": "warning|error|info", "message": "问题描述", "suggestion": "建议"}\n`;
|
|
518
|
-
prompt += ` ]\n`;
|
|
519
|
-
prompt += `}\n`;
|
|
520
|
-
|
|
521
|
-
return prompt;
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
/**
|
|
525
|
-
* 解析命令响应
|
|
526
|
-
*/
|
|
527
|
-
private parseCommandResponse(content: string): CommandResponse {
|
|
528
|
-
try {
|
|
529
|
-
const match = content.match(/\{[\s\S]*\}/);
|
|
530
|
-
if (match) {
|
|
531
|
-
const parsed = JSON.parse(match[0]);
|
|
532
|
-
return {
|
|
533
|
-
command: parsed.command || '',
|
|
534
|
-
explanation: parsed.explanation || '',
|
|
535
|
-
confidence: parsed.confidence || 0.5
|
|
536
|
-
};
|
|
537
|
-
}
|
|
538
|
-
} catch (error) {
|
|
539
|
-
this.logger.warn('Failed to parse command response as JSON', error);
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
// 备用解析
|
|
543
|
-
const lines = content.split('\n').map(l => l.trim()).filter(l => l);
|
|
544
|
-
return {
|
|
545
|
-
command: lines[0] || '',
|
|
546
|
-
explanation: lines.slice(1).join(' ') || 'AI生成的命令',
|
|
547
|
-
confidence: 0.5
|
|
548
|
-
};
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
/**
|
|
552
|
-
* 解析解释响应
|
|
553
|
-
*/
|
|
554
|
-
private parseExplainResponse(content: string): ExplainResponse {
|
|
555
|
-
try {
|
|
556
|
-
const match = content.match(/\{[\s\S]*\}/);
|
|
557
|
-
if (match) {
|
|
558
|
-
const parsed = JSON.parse(match[0]);
|
|
559
|
-
return {
|
|
560
|
-
explanation: parsed.explanation || '',
|
|
561
|
-
breakdown: parsed.breakdown || [],
|
|
562
|
-
examples: parsed.examples || []
|
|
563
|
-
};
|
|
564
|
-
}
|
|
565
|
-
} catch (error) {
|
|
566
|
-
this.logger.warn('Failed to parse explain response as JSON', error);
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
return {
|
|
570
|
-
explanation: content,
|
|
571
|
-
breakdown: []
|
|
572
|
-
};
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
/**
|
|
576
|
-
* 解析分析响应
|
|
577
|
-
*/
|
|
578
|
-
private parseAnalysisResponse(content: string): AnalysisResponse {
|
|
579
|
-
try {
|
|
580
|
-
const match = content.match(/\{[\s\S]*\}/);
|
|
581
|
-
if (match) {
|
|
582
|
-
const parsed = JSON.parse(match[0]);
|
|
583
|
-
return {
|
|
584
|
-
summary: parsed.summary || '',
|
|
585
|
-
insights: parsed.insights || [],
|
|
586
|
-
success: parsed.success !== false,
|
|
587
|
-
issues: parsed.issues || []
|
|
588
|
-
};
|
|
589
|
-
}
|
|
590
|
-
} catch (error) {
|
|
591
|
-
this.logger.warn('Failed to parse analysis response as JSON', error);
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
return {
|
|
595
|
-
summary: content,
|
|
596
|
-
insights: [],
|
|
597
|
-
success: true
|
|
598
|
-
};
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
/**
|
|
602
|
-
* 获取默认系统提示
|
|
603
|
-
*/
|
|
604
|
-
private getDefaultSystemPrompt(): string {
|
|
605
|
-
return `你是一个专业的终端命令助手,运行在 Tabby 终端中。
|
|
606
|
-
|
|
607
|
-
## 核心能力
|
|
608
|
-
你可以通过以下工具直接操作终端:
|
|
609
|
-
- write_to_terminal: 向终端写入并执行命令
|
|
610
|
-
- read_terminal_output: 读取终端输出
|
|
611
|
-
- get_terminal_list: 获取所有终端列表
|
|
612
|
-
- get_terminal_cwd: 获取当前工作目录
|
|
613
|
-
- focus_terminal: 切换到指定索引的终端(需要参数 terminal_index)
|
|
614
|
-
- get_terminal_selection: 获取终端中选中的文本
|
|
615
|
-
|
|
616
|
-
## 重要规则
|
|
617
|
-
1. 当用户请求执行命令(如"查看当前目录"、"列出文件"等),你必须使用 write_to_terminal 工具来执行
|
|
618
|
-
2. **当用户请求切换终端(如"切换到终端0"、"打开终端4"等),你必须使用 focus_terminal 工具**
|
|
619
|
-
3. 不要只是描述你"将要做什么",而是直接调用工具执行
|
|
620
|
-
4. 执行命令后,使用 read_terminal_output 读取结果并报告给用户
|
|
621
|
-
5. 如果不确定当前目录或终端状态,先使用 get_terminal_cwd 或 get_terminal_list 获取信息
|
|
622
|
-
6. **永远不要假装执行了操作,必须真正调用工具**
|
|
623
|
-
|
|
624
|
-
## 示例
|
|
625
|
-
用户:"查看当前目录的文件"
|
|
626
|
-
正确做法:调用 write_to_terminal 工具,参数 { "command": "dir", "execute": true }
|
|
627
|
-
错误做法:仅回复文字"我将执行 dir 命令"
|
|
628
|
-
|
|
629
|
-
用户:"切换到终端4"
|
|
630
|
-
正确做法:调用 focus_terminal 工具,参数 { "terminal_index": 4 }
|
|
631
|
-
错误做法:仅回复文字"已切换到终端4"(不调用工具)`;
|
|
632
|
-
}
|
|
633
450
|
}
|