tabby-ai-assistant 1.0.13 → 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/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/toast.service.ts +106 -106
- package/src/services/providers/anthropic-provider.service.ts +126 -30
- package/src/services/providers/base-provider.service.ts +90 -7
- package/src/services/providers/glm-provider.service.ts +151 -38
- package/src/services/providers/minimax-provider.service.ts +55 -40
- package/src/services/providers/ollama-provider.service.ts +117 -28
- package/src/services/providers/openai-compatible.service.ts +164 -34
- package/src/services/providers/openai-provider.service.ts +169 -34
- package/src/services/providers/vllm-provider.service.ts +116 -28
- 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/utils/cost.utils.ts +249 -0
- package/src/utils/validation.utils.ts +306 -2
- package/dist/index.js.LICENSE.txt +0 -18
- 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
|
@@ -1,11 +1,19 @@
|
|
|
1
|
-
import { Injectable, Inject, Optional } from '@angular/core';
|
|
2
|
-
import { Observable, from, throwError } from 'rxjs';
|
|
3
|
-
import { map, catchError, tap } from 'rxjs/operators';
|
|
4
|
-
import {
|
|
1
|
+
import { Injectable, Inject, Optional, Injector } from '@angular/core';
|
|
2
|
+
import { Observable, from, throwError, Subject, merge } from 'rxjs';
|
|
3
|
+
import { map, catchError, tap, takeUntil, finalize } from 'rxjs/operators';
|
|
4
|
+
import {
|
|
5
|
+
ChatMessage, MessageRole, ChatRequest, ChatResponse, CommandRequest, CommandResponse,
|
|
6
|
+
ExplainRequest, ExplainResponse, StreamEvent, ToolCall, ToolResult,
|
|
7
|
+
AgentStreamEvent, AgentLoopConfig, TerminationReason, AgentState, ToolCallRecord,
|
|
8
|
+
TerminationResult
|
|
9
|
+
} from '../../types/ai.types';
|
|
5
10
|
import { AiProviderManagerService } from './ai-provider-manager.service';
|
|
6
11
|
import { ConfigProviderService } from './config-provider.service';
|
|
7
12
|
import { TerminalContextService } from '../terminal/terminal-context.service';
|
|
8
|
-
import { TerminalToolsService
|
|
13
|
+
import { TerminalToolsService } from '../terminal/terminal-tools.service';
|
|
14
|
+
import { TerminalManagerService } from '../terminal/terminal-manager.service';
|
|
15
|
+
// 使用延迟注入获取 AiSidebarService 以打破循环依赖
|
|
16
|
+
import type { AiSidebarService } from '../chat/ai-sidebar.service';
|
|
9
17
|
import { LoggerService } from './logger.service';
|
|
10
18
|
import { BaseAiProvider } from '../../types/provider.types';
|
|
11
19
|
|
|
@@ -28,6 +36,8 @@ export class AiAssistantService {
|
|
|
28
36
|
private config: ConfigProviderService,
|
|
29
37
|
private terminalContext: TerminalContextService,
|
|
30
38
|
private terminalTools: TerminalToolsService,
|
|
39
|
+
private terminalManager: TerminalManagerService,
|
|
40
|
+
private injector: Injector,
|
|
31
41
|
private logger: LoggerService,
|
|
32
42
|
// 注入所有提供商服务
|
|
33
43
|
@Optional() private openaiProvider: OpenAiProviderService,
|
|
@@ -169,14 +179,16 @@ export class AiAssistantService {
|
|
|
169
179
|
|
|
170
180
|
let response = await activeProvider.chat(request);
|
|
171
181
|
|
|
172
|
-
//
|
|
173
|
-
|
|
182
|
+
// 处理工具调用(返回值包含工具调用统计)
|
|
183
|
+
const { finalResponse, totalToolCallsExecuted } = await this.handleToolCallsWithStats(
|
|
184
|
+
request, response, activeProvider
|
|
185
|
+
);
|
|
186
|
+
response = finalResponse;
|
|
174
187
|
|
|
175
|
-
//
|
|
176
|
-
const toolCalls = (response as any).toolCalls;
|
|
188
|
+
// 使用累计的工具调用次数进行幻觉检测
|
|
177
189
|
const hallucinationDetected = this.detectHallucination({
|
|
178
190
|
text: response.message.content,
|
|
179
|
-
toolCallCount:
|
|
191
|
+
toolCallCount: totalToolCallsExecuted
|
|
180
192
|
});
|
|
181
193
|
|
|
182
194
|
if (hallucinationDetected) {
|
|
@@ -213,38 +225,72 @@ export class AiAssistantService {
|
|
|
213
225
|
request.tools = this.terminalTools.getToolDefinitions();
|
|
214
226
|
}
|
|
215
227
|
|
|
228
|
+
// 使用 Subject 发送额外的工具结果事件
|
|
229
|
+
const toolResultSubject = new Subject<StreamEvent>();
|
|
230
|
+
|
|
216
231
|
// 调用流式方法
|
|
217
|
-
return
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
232
|
+
return merge(
|
|
233
|
+
activeProvider.chatStream(request).pipe(
|
|
234
|
+
tap(async (event: StreamEvent) => {
|
|
235
|
+
// 工具调用完成时执行
|
|
236
|
+
if (event.type === 'tool_use_end' && event.toolCall) {
|
|
237
|
+
await this.executeToolAndEmit(event.toolCall, toolResultSubject);
|
|
238
|
+
}
|
|
239
|
+
}),
|
|
240
|
+
catchError(error => {
|
|
241
|
+
this.logger.error('Stream error', error);
|
|
242
|
+
toolResultSubject.error(error);
|
|
243
|
+
return throwError(() => error);
|
|
244
|
+
}),
|
|
245
|
+
// 主流完成时,同时完成 toolResultSubject
|
|
246
|
+
finalize(() => {
|
|
247
|
+
this.logger.info('Main stream finalized, completing toolResultSubject');
|
|
248
|
+
toolResultSubject.complete();
|
|
249
|
+
})
|
|
250
|
+
),
|
|
251
|
+
toolResultSubject.asObservable()
|
|
228
252
|
);
|
|
229
253
|
}
|
|
230
254
|
|
|
231
255
|
/**
|
|
232
|
-
*
|
|
256
|
+
* 执行工具调用并发送结果事件
|
|
233
257
|
*/
|
|
234
|
-
private async
|
|
258
|
+
private async executeToolAndEmit(
|
|
235
259
|
toolCall: { id: string; name: string; input: any },
|
|
236
|
-
|
|
237
|
-
provider: any
|
|
260
|
+
resultSubject: Subject<StreamEvent>
|
|
238
261
|
): Promise<void> {
|
|
239
262
|
try {
|
|
263
|
+
const startTime = Date.now();
|
|
240
264
|
const result = await this.terminalTools.executeToolCall({
|
|
241
265
|
id: toolCall.id,
|
|
242
266
|
name: toolCall.name,
|
|
243
267
|
input: toolCall.input
|
|
244
268
|
});
|
|
245
|
-
|
|
269
|
+
const duration = Date.now() - startTime;
|
|
270
|
+
|
|
271
|
+
// 发送工具结果事件
|
|
272
|
+
resultSubject.next({
|
|
273
|
+
type: 'tool_result',
|
|
274
|
+
result: {
|
|
275
|
+
tool_use_id: result.tool_use_id,
|
|
276
|
+
content: result.content,
|
|
277
|
+
is_error: result.is_error
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
this.logger.info('Tool executed in stream', {
|
|
282
|
+
name: toolCall.name,
|
|
283
|
+
duration,
|
|
284
|
+
success: !result.is_error,
|
|
285
|
+
resultPreview: result.content.substring(0, 100)
|
|
286
|
+
});
|
|
246
287
|
} catch (error) {
|
|
247
|
-
|
|
288
|
+
// 发送工具错误事件
|
|
289
|
+
resultSubject.next({
|
|
290
|
+
type: 'tool_error',
|
|
291
|
+
error: error instanceof Error ? error.message : String(error)
|
|
292
|
+
});
|
|
293
|
+
this.logger.error('Tool execution failed in stream', { name: toolCall.name, error });
|
|
248
294
|
}
|
|
249
295
|
}
|
|
250
296
|
|
|
@@ -337,6 +383,113 @@ export class AiAssistantService {
|
|
|
337
383
|
return this.handleToolCalls(followUpRequest, followUpResponse, provider, depth + 1, maxDepth);
|
|
338
384
|
}
|
|
339
385
|
|
|
386
|
+
/**
|
|
387
|
+
* 处理工具调用(带统计)
|
|
388
|
+
* 返回最终响应和累计的工具调用次数
|
|
389
|
+
*/
|
|
390
|
+
private async handleToolCallsWithStats(
|
|
391
|
+
originalRequest: ChatRequest,
|
|
392
|
+
response: ChatResponse,
|
|
393
|
+
provider: BaseAiProvider,
|
|
394
|
+
depth: number = 0,
|
|
395
|
+
maxDepth: number = 10,
|
|
396
|
+
accumulatedToolCalls: number = 0
|
|
397
|
+
): Promise<{ finalResponse: ChatResponse; totalToolCallsExecuted: number }> {
|
|
398
|
+
// 检查响应中是否有工具调用
|
|
399
|
+
const toolCalls = (response as any).toolCalls as ToolCall[] | undefined;
|
|
400
|
+
|
|
401
|
+
if (!toolCalls || toolCalls.length === 0) {
|
|
402
|
+
return {
|
|
403
|
+
finalResponse: response,
|
|
404
|
+
totalToolCallsExecuted: accumulatedToolCalls
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// 检查递归深度
|
|
409
|
+
if (depth >= maxDepth) {
|
|
410
|
+
this.logger.warn('Max tool call depth reached', { depth, maxDepth });
|
|
411
|
+
return {
|
|
412
|
+
finalResponse: response,
|
|
413
|
+
totalToolCallsExecuted: accumulatedToolCalls
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// 累计工具调用次数
|
|
418
|
+
const newTotal = accumulatedToolCalls + toolCalls.length;
|
|
419
|
+
this.logger.info('Tool calls executed', {
|
|
420
|
+
thisRound: toolCalls.length,
|
|
421
|
+
total: newTotal,
|
|
422
|
+
depth
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
// 执行所有工具调用
|
|
426
|
+
const toolResults: ToolResult[] = [];
|
|
427
|
+
for (const toolCall of toolCalls) {
|
|
428
|
+
this.logger.info('Executing tool in handleToolCalls', { name: toolCall.name, depth });
|
|
429
|
+
const result = await this.terminalTools.executeToolCall(toolCall);
|
|
430
|
+
toolResults.push(result);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// 构建包含工具结果的新请求
|
|
434
|
+
const toolResultsMessage: ChatMessage = {
|
|
435
|
+
id: `tool_result_${Date.now()}`,
|
|
436
|
+
role: MessageRole.USER,
|
|
437
|
+
content: toolResults.map(r =>
|
|
438
|
+
`工具 ${r.tool_use_id} 结果:\n${r.content}`
|
|
439
|
+
).join('\n\n'),
|
|
440
|
+
timestamp: new Date(),
|
|
441
|
+
metadata: { toolResults }
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
// 继续对话 - 仍然允许工具调用但递归处理
|
|
445
|
+
const followUpRequest: ChatRequest = {
|
|
446
|
+
...originalRequest,
|
|
447
|
+
messages: [
|
|
448
|
+
...originalRequest.messages,
|
|
449
|
+
response.message,
|
|
450
|
+
toolResultsMessage
|
|
451
|
+
],
|
|
452
|
+
tools: this.terminalTools.getToolDefinitions()
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
// 发送后续请求
|
|
456
|
+
const followUpResponse = await provider.chat(followUpRequest);
|
|
457
|
+
|
|
458
|
+
// 如果 AI 回复太短,直接附加工具结果
|
|
459
|
+
const minResponseLength = 50;
|
|
460
|
+
const toolResultsText = toolResults.map(r => r.content).join('\n\n');
|
|
461
|
+
|
|
462
|
+
if (followUpResponse.message.content.length < minResponseLength && toolResultsText.length > 0) {
|
|
463
|
+
this.logger.info('AI response too short, appending tool results directly', {
|
|
464
|
+
responseLength: followUpResponse.message.content.length,
|
|
465
|
+
toolResultsLength: toolResultsText.length
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
const terminalOutput = toolResults.find(r =>
|
|
469
|
+
r.content.includes('=== 终端输出 ===') ||
|
|
470
|
+
r.content.includes('✅ 命令已执行')
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
if (terminalOutput) {
|
|
474
|
+
followUpResponse.message.content =
|
|
475
|
+
followUpResponse.message.content + '\n\n' + terminalOutput.content;
|
|
476
|
+
} else {
|
|
477
|
+
followUpResponse.message.content =
|
|
478
|
+
followUpResponse.message.content + '\n\n' + toolResultsText;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// 递归处理后续响应中的工具调用,传递累计值
|
|
483
|
+
return this.handleToolCallsWithStats(
|
|
484
|
+
followUpRequest,
|
|
485
|
+
followUpResponse,
|
|
486
|
+
provider,
|
|
487
|
+
depth + 1,
|
|
488
|
+
maxDepth,
|
|
489
|
+
newTotal
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
|
|
340
493
|
/**
|
|
341
494
|
* 生成命令
|
|
342
495
|
*/
|
|
@@ -418,8 +571,12 @@ export class AiAssistantService {
|
|
|
418
571
|
*/
|
|
419
572
|
async generateCommandFromSelection(): Promise<CommandResponse | null> {
|
|
420
573
|
try {
|
|
421
|
-
//
|
|
422
|
-
const selection =
|
|
574
|
+
// 从当前终端获取选中文本
|
|
575
|
+
const selection = await this.terminalManager.getSelection();
|
|
576
|
+
if (!selection) {
|
|
577
|
+
this.logger.warn('No text selected in terminal');
|
|
578
|
+
return null;
|
|
579
|
+
}
|
|
423
580
|
const context = this.terminalContext.getCurrentContext();
|
|
424
581
|
|
|
425
582
|
const request: CommandRequest = {
|
|
@@ -444,9 +601,10 @@ export class AiAssistantService {
|
|
|
444
601
|
*/
|
|
445
602
|
async explainCommandFromSelection(): Promise<ExplainResponse | null> {
|
|
446
603
|
try {
|
|
447
|
-
//
|
|
448
|
-
const selection =
|
|
604
|
+
// 从当前终端获取选中文本
|
|
605
|
+
const selection = await this.terminalManager.getSelection();
|
|
449
606
|
if (!selection) {
|
|
607
|
+
this.logger.warn('No text selected in terminal');
|
|
450
608
|
return null;
|
|
451
609
|
}
|
|
452
610
|
|
|
@@ -468,11 +626,14 @@ export class AiAssistantService {
|
|
|
468
626
|
|
|
469
627
|
/**
|
|
470
628
|
* 打开聊天界面
|
|
629
|
+
* 使用延迟注入获取 AiSidebarService 以避免循环依赖
|
|
471
630
|
*/
|
|
472
631
|
openChatInterface(): void {
|
|
473
632
|
this.logger.info('Opening chat interface');
|
|
474
|
-
//
|
|
475
|
-
|
|
633
|
+
// 延迟获取 AiSidebarService 以打破循环依赖
|
|
634
|
+
const { AiSidebarService } = require('../chat/ai-sidebar.service');
|
|
635
|
+
const sidebarService = this.injector.get(AiSidebarService) as AiSidebarService;
|
|
636
|
+
sidebarService.show();
|
|
476
637
|
}
|
|
477
638
|
|
|
478
639
|
/**
|
|
@@ -565,22 +726,272 @@ export class AiAssistantService {
|
|
|
565
726
|
/**
|
|
566
727
|
* 获取建议命令
|
|
567
728
|
*/
|
|
568
|
-
async getSuggestedCommands(
|
|
729
|
+
async getSuggestedCommands(input: string): Promise<string[]> {
|
|
569
730
|
const activeProvider = this.providerManager.getActiveProvider();
|
|
570
731
|
if (!activeProvider) {
|
|
571
732
|
return [];
|
|
572
733
|
}
|
|
573
734
|
|
|
574
735
|
try {
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
736
|
+
const context = this.terminalContext.getCurrentContext();
|
|
737
|
+
const suggestions: string[] = [];
|
|
738
|
+
|
|
739
|
+
// 1. 基于当前目录的智能建议
|
|
740
|
+
if (context?.session.cwd) {
|
|
741
|
+
const dirSuggestions = this.getDirectoryBasedSuggestions(context.session.cwd);
|
|
742
|
+
suggestions.push(...dirSuggestions);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// 2. 基于最近命令的建议
|
|
746
|
+
if (context?.recentCommands) {
|
|
747
|
+
const historySuggestions = this.getHistoryBasedSuggestions(context.recentCommands, input);
|
|
748
|
+
suggestions.push(...historySuggestions);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// 3. 基于当前输入的模糊匹配建议
|
|
752
|
+
if (input.length > 0) {
|
|
753
|
+
const inputSuggestions = this.getInputBasedSuggestions(input, suggestions);
|
|
754
|
+
suggestions.push(...inputSuggestions);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// 去重并限制数量
|
|
758
|
+
const uniqueSuggestions = [...new Set(suggestions)].slice(0, 8);
|
|
759
|
+
|
|
760
|
+
return uniqueSuggestions;
|
|
761
|
+
|
|
578
762
|
} catch (error) {
|
|
579
763
|
this.logger.error('Failed to get suggested commands', error);
|
|
580
764
|
return [];
|
|
581
765
|
}
|
|
582
766
|
}
|
|
583
767
|
|
|
768
|
+
/**
|
|
769
|
+
* 基于当前目录的智能建议
|
|
770
|
+
*/
|
|
771
|
+
private getDirectoryBasedSuggestions(cwd: string): string[] {
|
|
772
|
+
const suggestions: string[] = [];
|
|
773
|
+
|
|
774
|
+
// Git相关建议
|
|
775
|
+
if (cwd.includes('.git') || this.isGitRepository(cwd)) {
|
|
776
|
+
suggestions.push(
|
|
777
|
+
'git status',
|
|
778
|
+
'git pull',
|
|
779
|
+
'git add .',
|
|
780
|
+
'git commit -m ""',
|
|
781
|
+
'git log --oneline',
|
|
782
|
+
'git checkout -b '
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// Node.js项目建议
|
|
787
|
+
if (this.isNodeProject(cwd)) {
|
|
788
|
+
suggestions.push(
|
|
789
|
+
'npm install',
|
|
790
|
+
'npm run dev',
|
|
791
|
+
'npm run build',
|
|
792
|
+
'npm test',
|
|
793
|
+
'npm run lint',
|
|
794
|
+
'yarn install',
|
|
795
|
+
'pnpm install'
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// Python项目建议
|
|
800
|
+
if (this.isPythonProject(cwd)) {
|
|
801
|
+
suggestions.push(
|
|
802
|
+
'python -m venv venv',
|
|
803
|
+
'pip install -r requirements.txt',
|
|
804
|
+
'python main.py',
|
|
805
|
+
'pytest',
|
|
806
|
+
'python -m pip install --upgrade pip'
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// Docker项目建议
|
|
811
|
+
if (this.hasDockerFiles(cwd)) {
|
|
812
|
+
suggestions.push(
|
|
813
|
+
'docker build -t .',
|
|
814
|
+
'docker-compose up',
|
|
815
|
+
'docker-compose down',
|
|
816
|
+
'docker ps',
|
|
817
|
+
'docker images'
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// Kubernetes项目建议
|
|
822
|
+
if (this.hasK8sFiles(cwd)) {
|
|
823
|
+
suggestions.push(
|
|
824
|
+
'kubectl get pods',
|
|
825
|
+
'kubectl get svc',
|
|
826
|
+
'kubectl apply -f ',
|
|
827
|
+
'kubectl describe pod ',
|
|
828
|
+
'kubectl logs -f '
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
return suggestions;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* 基于历史的智能建议
|
|
837
|
+
*/
|
|
838
|
+
private getHistoryBasedSuggestions(recentCommands: string[], input: string): string[] {
|
|
839
|
+
const suggestions: string[] = [];
|
|
840
|
+
|
|
841
|
+
// 提取最近使用过的相关命令
|
|
842
|
+
for (const cmd of recentCommands.slice(0, 10)) {
|
|
843
|
+
// 如果输入与历史命令开头匹配,添加完整命令
|
|
844
|
+
if (cmd.toLowerCase().startsWith(input.toLowerCase()) && cmd !== input) {
|
|
845
|
+
suggestions.push(cmd);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// 添加相似类别的新命令
|
|
849
|
+
if (input.length > 2 && cmd.toLowerCase().includes(input.toLowerCase())) {
|
|
850
|
+
const baseCmd = cmd.split(' ')[0];
|
|
851
|
+
if (!suggestions.includes(baseCmd)) {
|
|
852
|
+
suggestions.push(baseCmd);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
return suggestions;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/**
|
|
861
|
+
* 基于输入的模糊建议
|
|
862
|
+
*/
|
|
863
|
+
private getInputBasedSuggestions(input: string, existingSuggestions: string[]): string[] {
|
|
864
|
+
const suggestions: string[] = [];
|
|
865
|
+
const lowerInput = input.toLowerCase();
|
|
866
|
+
|
|
867
|
+
// 常用命令模板
|
|
868
|
+
const commandTemplates: { [key: string]: string[] } = {
|
|
869
|
+
'git': [
|
|
870
|
+
'git status',
|
|
871
|
+
'git add .',
|
|
872
|
+
'git commit -m ""',
|
|
873
|
+
'git checkout -b ',
|
|
874
|
+
'git merge ',
|
|
875
|
+
'git rebase ',
|
|
876
|
+
'git stash',
|
|
877
|
+
'git stash pop',
|
|
878
|
+
'git diff',
|
|
879
|
+
'git log --oneline'
|
|
880
|
+
],
|
|
881
|
+
'npm': [
|
|
882
|
+
'npm install ',
|
|
883
|
+
'npm run ',
|
|
884
|
+
'npm list',
|
|
885
|
+
'npm outdated',
|
|
886
|
+
'npm update',
|
|
887
|
+
'npm run dev',
|
|
888
|
+
'npm run build'
|
|
889
|
+
],
|
|
890
|
+
'docker': [
|
|
891
|
+
'docker build -t ',
|
|
892
|
+
'docker run -it ',
|
|
893
|
+
'docker-compose up',
|
|
894
|
+
'docker-compose down',
|
|
895
|
+
'docker ps',
|
|
896
|
+
'docker images'
|
|
897
|
+
],
|
|
898
|
+
'kubectl': [
|
|
899
|
+
'kubectl get ',
|
|
900
|
+
'kubectl describe ',
|
|
901
|
+
'kubectl apply -f ',
|
|
902
|
+
'kubectl delete -f ',
|
|
903
|
+
'kubectl logs '
|
|
904
|
+
],
|
|
905
|
+
'ls': [
|
|
906
|
+
'ls -la',
|
|
907
|
+
'ls -lh',
|
|
908
|
+
'ls -R'
|
|
909
|
+
],
|
|
910
|
+
'cd': [
|
|
911
|
+
'cd ..',
|
|
912
|
+
'cd /',
|
|
913
|
+
'cd ~'
|
|
914
|
+
],
|
|
915
|
+
'grep': [
|
|
916
|
+
'grep -r "" .',
|
|
917
|
+
'grep -n "" .',
|
|
918
|
+
'grep -E "" .'
|
|
919
|
+
],
|
|
920
|
+
'find': [
|
|
921
|
+
'find . -name ""',
|
|
922
|
+
'find . -type f -name ""'
|
|
923
|
+
]
|
|
924
|
+
};
|
|
925
|
+
|
|
926
|
+
// 查找匹配的命令模板
|
|
927
|
+
for (const [prefix, templates] of Object.entries(commandTemplates)) {
|
|
928
|
+
if (lowerInput.startsWith(prefix) || lowerInput.includes(prefix)) {
|
|
929
|
+
for (const template of templates) {
|
|
930
|
+
if (!existingSuggestions.includes(template)) {
|
|
931
|
+
suggestions.push(template);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
return suggestions;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/**
|
|
941
|
+
* 检查是否为Git仓库
|
|
942
|
+
*/
|
|
943
|
+
private isGitRepository(path: string): boolean {
|
|
944
|
+
return path.includes('.git') ||
|
|
945
|
+
this.hasFile(path, '.git');
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
/**
|
|
949
|
+
* 检查是否为Node.js项目
|
|
950
|
+
*/
|
|
951
|
+
private isNodeProject(path: string): boolean {
|
|
952
|
+
return this.hasFile(path, 'package.json') ||
|
|
953
|
+
this.hasFile(path, 'node_modules');
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
/**
|
|
957
|
+
* 检查是否为Python项目
|
|
958
|
+
*/
|
|
959
|
+
private isPythonProject(path: string): boolean {
|
|
960
|
+
return this.hasFile(path, 'requirements.txt') ||
|
|
961
|
+
this.hasFile(path, 'pyproject.toml') ||
|
|
962
|
+
this.hasFile(path, 'setup.py') ||
|
|
963
|
+
this.hasFile(path, 'venv');
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
/**
|
|
967
|
+
* 检查是否有Docker文件
|
|
968
|
+
*/
|
|
969
|
+
private hasDockerFiles(path: string): boolean {
|
|
970
|
+
return this.hasFile(path, 'Dockerfile') ||
|
|
971
|
+
this.hasFile(path, 'docker-compose.yml') ||
|
|
972
|
+
this.hasFile(path, 'docker-compose.yaml');
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/**
|
|
976
|
+
* 检查是否有Kubernetes文件
|
|
977
|
+
*/
|
|
978
|
+
private hasK8sFiles(path: string): boolean {
|
|
979
|
+
return this.hasFile(path, 'k8s') ||
|
|
980
|
+
this.hasFile(path, 'kubernetes') ||
|
|
981
|
+
path.includes('k8s') ||
|
|
982
|
+
path.includes('kubernetes');
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
/**
|
|
986
|
+
* 检查文件是否存在(简化版)
|
|
987
|
+
*/
|
|
988
|
+
private hasFile(path: string, filename: string): boolean {
|
|
989
|
+
// 这里应该是实际的文件系统检查
|
|
990
|
+
// 由于无法直接访问文件系统,返回false
|
|
991
|
+
// 实际实现应该使用Node.js的fs模块
|
|
992
|
+
return path.includes(filename);
|
|
993
|
+
}
|
|
994
|
+
|
|
584
995
|
/**
|
|
585
996
|
* 分析终端错误并提供修复建议
|
|
586
997
|
*/
|
|
@@ -630,4 +1041,616 @@ export class AiAssistantService {
|
|
|
630
1041
|
|
|
631
1042
|
return false;
|
|
632
1043
|
}
|
|
1044
|
+
|
|
1045
|
+
// ============================================================================
|
|
1046
|
+
// Agent 循环相关方法
|
|
1047
|
+
// ============================================================================
|
|
1048
|
+
|
|
1049
|
+
/**
|
|
1050
|
+
* 完整的 Agent 对话循环
|
|
1051
|
+
* 自动处理:工具调用 → 执行工具 → 工具结果发回 AI → 多轮循环
|
|
1052
|
+
* 包含智能终止检测
|
|
1053
|
+
*/
|
|
1054
|
+
chatStreamWithAgentLoop(
|
|
1055
|
+
request: ChatRequest,
|
|
1056
|
+
config: AgentLoopConfig = {}
|
|
1057
|
+
): Observable<AgentStreamEvent> {
|
|
1058
|
+
// 🔥 入口日志 - 确认方法被调用
|
|
1059
|
+
this.logger.info('🔥 chatStreamWithAgentLoop CALLED', {
|
|
1060
|
+
messagesCount: request.messages?.length,
|
|
1061
|
+
maxRounds: config.maxRounds,
|
|
1062
|
+
timeoutMs: config.timeoutMs
|
|
1063
|
+
});
|
|
1064
|
+
|
|
1065
|
+
// 配置参数
|
|
1066
|
+
const maxRounds = config.maxRounds || 15;
|
|
1067
|
+
const timeoutMs = config.timeoutMs || 120000; // 默认 2 分钟
|
|
1068
|
+
const repeatThreshold = config.repeatThreshold || 3; // 重复调用阈值
|
|
1069
|
+
const failureThreshold = config.failureThreshold || 2; // 连续失败阈值
|
|
1070
|
+
|
|
1071
|
+
const callbacks = {
|
|
1072
|
+
onRoundStart: config.onRoundStart,
|
|
1073
|
+
onRoundEnd: config.onRoundEnd,
|
|
1074
|
+
onAgentComplete: config.onAgentComplete
|
|
1075
|
+
};
|
|
1076
|
+
|
|
1077
|
+
// Agent 状态追踪
|
|
1078
|
+
const agentState: AgentState = {
|
|
1079
|
+
currentRound: 0,
|
|
1080
|
+
startTime: Date.now(),
|
|
1081
|
+
toolCallHistory: [],
|
|
1082
|
+
lastAiResponse: '',
|
|
1083
|
+
isActive: true
|
|
1084
|
+
};
|
|
1085
|
+
|
|
1086
|
+
return new Observable<AgentStreamEvent>((subscriber) => {
|
|
1087
|
+
// 消息历史副本(用于多轮对话)
|
|
1088
|
+
const conversationMessages: ChatMessage[] = [...(request.messages || [])];
|
|
1089
|
+
|
|
1090
|
+
// 递归执行单轮对话
|
|
1091
|
+
const runSingleRound = async (): Promise<void> => {
|
|
1092
|
+
if (!agentState.isActive) return;
|
|
1093
|
+
|
|
1094
|
+
agentState.currentRound++;
|
|
1095
|
+
|
|
1096
|
+
// 发送 round_start 事件
|
|
1097
|
+
subscriber.next({ type: 'round_start', round: agentState.currentRound });
|
|
1098
|
+
callbacks.onRoundStart?.(agentState.currentRound);
|
|
1099
|
+
this.logger.info(`Agent round ${agentState.currentRound} started`);
|
|
1100
|
+
|
|
1101
|
+
// 本轮收集的工具调用
|
|
1102
|
+
const pendingToolCalls: ToolCall[] = [];
|
|
1103
|
+
let roundTextContent = '';
|
|
1104
|
+
|
|
1105
|
+
return new Promise<void>((resolve, reject) => {
|
|
1106
|
+
// 构建当前轮次的请求
|
|
1107
|
+
const roundRequest: ChatRequest = {
|
|
1108
|
+
...request,
|
|
1109
|
+
messages: conversationMessages,
|
|
1110
|
+
enableTools: true
|
|
1111
|
+
};
|
|
1112
|
+
|
|
1113
|
+
// 调用流式 API
|
|
1114
|
+
const activeProvider = this.providerManager.getActiveProvider() as any;
|
|
1115
|
+
if (!activeProvider) {
|
|
1116
|
+
const error = new Error('No active AI provider available');
|
|
1117
|
+
subscriber.next({ type: 'error', error: error.message });
|
|
1118
|
+
reject(error);
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// 添加工具定义
|
|
1123
|
+
roundRequest.tools = this.terminalTools.getToolDefinitions();
|
|
1124
|
+
|
|
1125
|
+
// 直接订阅 provider 的流(不使用 merge,否则需要所有源都 complete)
|
|
1126
|
+
activeProvider.chatStream(roundRequest).subscribe({
|
|
1127
|
+
next: (event: any) => {
|
|
1128
|
+
switch (event.type) {
|
|
1129
|
+
case 'text_delta':
|
|
1130
|
+
// 转发文本增量
|
|
1131
|
+
if (event.textDelta) {
|
|
1132
|
+
roundTextContent += event.textDelta;
|
|
1133
|
+
subscriber.next({
|
|
1134
|
+
type: 'text_delta',
|
|
1135
|
+
textDelta: event.textDelta
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
break;
|
|
1139
|
+
|
|
1140
|
+
case 'tool_use_start':
|
|
1141
|
+
// 转发工具开始
|
|
1142
|
+
subscriber.next({
|
|
1143
|
+
type: 'tool_use_start',
|
|
1144
|
+
toolCall: event.toolCall
|
|
1145
|
+
});
|
|
1146
|
+
break;
|
|
1147
|
+
|
|
1148
|
+
case 'tool_use_end':
|
|
1149
|
+
// 收集工具调用
|
|
1150
|
+
if (event.toolCall) {
|
|
1151
|
+
pendingToolCalls.push(event.toolCall as ToolCall);
|
|
1152
|
+
subscriber.next({
|
|
1153
|
+
type: 'tool_use_end',
|
|
1154
|
+
toolCall: event.toolCall
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
break;
|
|
1158
|
+
|
|
1159
|
+
case 'error':
|
|
1160
|
+
subscriber.next({ type: 'error', error: event.error });
|
|
1161
|
+
break;
|
|
1162
|
+
}
|
|
1163
|
+
},
|
|
1164
|
+
error: (error) => {
|
|
1165
|
+
subscriber.next({
|
|
1166
|
+
type: 'error',
|
|
1167
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1168
|
+
});
|
|
1169
|
+
reject(error);
|
|
1170
|
+
},
|
|
1171
|
+
complete: () => {
|
|
1172
|
+
// 使用 IIFE 确保异步操作被正确执行
|
|
1173
|
+
(async () => {
|
|
1174
|
+
// 发送 round_end 事件
|
|
1175
|
+
subscriber.next({ type: 'round_end', round: agentState.currentRound });
|
|
1176
|
+
callbacks.onRoundEnd?.(agentState.currentRound);
|
|
1177
|
+
this.logger.debug(`Round ${agentState.currentRound} ended, messages in conversation: ${conversationMessages.length}`);
|
|
1178
|
+
|
|
1179
|
+
// 将本轮 AI 回复添加到消息历史
|
|
1180
|
+
if (roundTextContent) {
|
|
1181
|
+
conversationMessages.push({
|
|
1182
|
+
id: this.generateId(),
|
|
1183
|
+
role: MessageRole.ASSISTANT,
|
|
1184
|
+
content: roundTextContent,
|
|
1185
|
+
timestamp: new Date()
|
|
1186
|
+
});
|
|
1187
|
+
// 更新 Agent 状态的 lastAiResponse
|
|
1188
|
+
agentState.lastAiResponse = roundTextContent;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// 执行智能终止检测 (AI 响应后)
|
|
1192
|
+
const termination = this.checkTermination(
|
|
1193
|
+
agentState,
|
|
1194
|
+
pendingToolCalls,
|
|
1195
|
+
[],
|
|
1196
|
+
{ maxRounds, timeoutMs, repeatThreshold, failureThreshold },
|
|
1197
|
+
'after_ai_response'
|
|
1198
|
+
);
|
|
1199
|
+
|
|
1200
|
+
if (termination.shouldTerminate) {
|
|
1201
|
+
this.logger.info('Agent terminated by smart detector', { reason: termination.reason });
|
|
1202
|
+
subscriber.next({
|
|
1203
|
+
type: 'agent_complete',
|
|
1204
|
+
reason: termination.reason,
|
|
1205
|
+
totalRounds: agentState.currentRound,
|
|
1206
|
+
terminationMessage: termination.message
|
|
1207
|
+
});
|
|
1208
|
+
callbacks.onAgentComplete?.(termination.reason, agentState.currentRound);
|
|
1209
|
+
subscriber.complete();
|
|
1210
|
+
resolve();
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
// 检查是否有待执行的工具
|
|
1215
|
+
if (pendingToolCalls.length > 0) {
|
|
1216
|
+
this.logger.info(`Round ${agentState.currentRound}: ${pendingToolCalls.length} tools to execute`);
|
|
1217
|
+
|
|
1218
|
+
// 执行所有工具
|
|
1219
|
+
const toolResults = await this.executeToolsSequentially(
|
|
1220
|
+
pendingToolCalls,
|
|
1221
|
+
subscriber,
|
|
1222
|
+
agentState
|
|
1223
|
+
);
|
|
1224
|
+
|
|
1225
|
+
// 将工具结果添加到消息历史
|
|
1226
|
+
const toolResultMessage = this.buildToolResultMessage(toolResults);
|
|
1227
|
+
conversationMessages.push(toolResultMessage);
|
|
1228
|
+
|
|
1229
|
+
this.logger.info('Tool results added to conversation, starting next round', {
|
|
1230
|
+
round: agentState.currentRound,
|
|
1231
|
+
totalMessages: conversationMessages.length
|
|
1232
|
+
});
|
|
1233
|
+
|
|
1234
|
+
// 执行工具后的终止检测 (不检查 no_tools)
|
|
1235
|
+
const postToolTermination = this.checkTermination(
|
|
1236
|
+
agentState,
|
|
1237
|
+
[],
|
|
1238
|
+
toolResults,
|
|
1239
|
+
{ maxRounds, timeoutMs, repeatThreshold, failureThreshold },
|
|
1240
|
+
'after_tool_execution'
|
|
1241
|
+
);
|
|
1242
|
+
|
|
1243
|
+
if (postToolTermination.shouldTerminate) {
|
|
1244
|
+
this.logger.info('Agent terminated after tool execution', { reason: postToolTermination.reason });
|
|
1245
|
+
subscriber.next({
|
|
1246
|
+
type: 'agent_complete',
|
|
1247
|
+
reason: postToolTermination.reason,
|
|
1248
|
+
totalRounds: agentState.currentRound,
|
|
1249
|
+
terminationMessage: postToolTermination.message
|
|
1250
|
+
});
|
|
1251
|
+
callbacks.onAgentComplete?.(postToolTermination.reason, agentState.currentRound);
|
|
1252
|
+
subscriber.complete();
|
|
1253
|
+
resolve();
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
// 继续下一轮(添加递归安全保护)
|
|
1258
|
+
try {
|
|
1259
|
+
await runSingleRound();
|
|
1260
|
+
} catch (recursionError) {
|
|
1261
|
+
this.logger.error('Recursive round error', recursionError);
|
|
1262
|
+
subscriber.next({
|
|
1263
|
+
type: 'error',
|
|
1264
|
+
error: `执行循环中断: ${recursionError instanceof Error ? recursionError.message : 'Unknown error'}`
|
|
1265
|
+
});
|
|
1266
|
+
subscriber.error(recursionError);
|
|
1267
|
+
}
|
|
1268
|
+
} else {
|
|
1269
|
+
// 没有工具调用,Agent 循环完成
|
|
1270
|
+
this.logger.info(`Agent completed: ${agentState.currentRound} rounds, reason: no_tools`);
|
|
1271
|
+
subscriber.next({
|
|
1272
|
+
type: 'agent_complete',
|
|
1273
|
+
reason: 'no_tools',
|
|
1274
|
+
totalRounds: agentState.currentRound
|
|
1275
|
+
});
|
|
1276
|
+
callbacks.onAgentComplete?.('no_tools', agentState.currentRound);
|
|
1277
|
+
subscriber.complete();
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
resolve();
|
|
1281
|
+
})().catch(error => {
|
|
1282
|
+
this.logger.error('Error in complete handler', error);
|
|
1283
|
+
subscriber.next({ type: 'error', error: error.message });
|
|
1284
|
+
reject(error);
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
1287
|
+
});
|
|
1288
|
+
});
|
|
1289
|
+
};
|
|
1290
|
+
|
|
1291
|
+
// 开始第一轮
|
|
1292
|
+
runSingleRound().catch(error => {
|
|
1293
|
+
subscriber.error(error);
|
|
1294
|
+
});
|
|
1295
|
+
|
|
1296
|
+
// 返回取消函数
|
|
1297
|
+
return () => {
|
|
1298
|
+
agentState.isActive = false;
|
|
1299
|
+
this.logger.info('Agent loop cancelled by subscriber');
|
|
1300
|
+
};
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
/**
|
|
1305
|
+
* 顺序执行工具并发送事件
|
|
1306
|
+
* @param toolCalls 工具调用列表
|
|
1307
|
+
* @param subscriber 事件订阅者
|
|
1308
|
+
* @param agentState Agent 状态(用于追踪工具调用历史)
|
|
1309
|
+
*/
|
|
1310
|
+
private async executeToolsSequentially(
|
|
1311
|
+
toolCalls: ToolCall[],
|
|
1312
|
+
subscriber: { next: (event: AgentStreamEvent) => void },
|
|
1313
|
+
agentState?: AgentState
|
|
1314
|
+
): Promise<ToolResult[]> {
|
|
1315
|
+
const results: ToolResult[] = [];
|
|
1316
|
+
|
|
1317
|
+
for (const toolCall of toolCalls) {
|
|
1318
|
+
// 发送 tool_executing 事件
|
|
1319
|
+
subscriber.next({
|
|
1320
|
+
type: 'tool_executing',
|
|
1321
|
+
toolCall: {
|
|
1322
|
+
id: toolCall.id,
|
|
1323
|
+
name: toolCall.name,
|
|
1324
|
+
input: toolCall.input
|
|
1325
|
+
}
|
|
1326
|
+
});
|
|
1327
|
+
|
|
1328
|
+
const startTime = Date.now();
|
|
1329
|
+
|
|
1330
|
+
try {
|
|
1331
|
+
const result = await this.terminalTools.executeToolCall(toolCall);
|
|
1332
|
+
const duration = Date.now() - startTime;
|
|
1333
|
+
|
|
1334
|
+
// 添加工具名称到结果中
|
|
1335
|
+
results.push({
|
|
1336
|
+
...result,
|
|
1337
|
+
name: toolCall.name // 添加工具名称
|
|
1338
|
+
});
|
|
1339
|
+
|
|
1340
|
+
// 记录到 Agent 状态历史
|
|
1341
|
+
if (agentState) {
|
|
1342
|
+
agentState.toolCallHistory.push({
|
|
1343
|
+
name: toolCall.name,
|
|
1344
|
+
input: toolCall.input,
|
|
1345
|
+
inputHash: this.hashInput(toolCall.input),
|
|
1346
|
+
success: !result.is_error,
|
|
1347
|
+
timestamp: Date.now()
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
// 发送 tool_executed 事件
|
|
1352
|
+
subscriber.next({
|
|
1353
|
+
type: 'tool_executed',
|
|
1354
|
+
toolCall: {
|
|
1355
|
+
id: toolCall.id,
|
|
1356
|
+
name: toolCall.name,
|
|
1357
|
+
input: toolCall.input
|
|
1358
|
+
},
|
|
1359
|
+
toolResult: {
|
|
1360
|
+
tool_use_id: result.tool_use_id,
|
|
1361
|
+
content: result.content,
|
|
1362
|
+
is_error: !!result.is_error,
|
|
1363
|
+
duration
|
|
1364
|
+
}
|
|
1365
|
+
});
|
|
1366
|
+
|
|
1367
|
+
this.logger.info('Tool executed', {
|
|
1368
|
+
name: toolCall.name,
|
|
1369
|
+
duration,
|
|
1370
|
+
success: !result.is_error
|
|
1371
|
+
});
|
|
1372
|
+
|
|
1373
|
+
} catch (error) {
|
|
1374
|
+
const duration = Date.now() - startTime;
|
|
1375
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1376
|
+
|
|
1377
|
+
// 发送 tool_error 事件
|
|
1378
|
+
subscriber.next({
|
|
1379
|
+
type: 'tool_error',
|
|
1380
|
+
toolCall: {
|
|
1381
|
+
id: toolCall.id,
|
|
1382
|
+
name: toolCall.name,
|
|
1383
|
+
input: toolCall.input
|
|
1384
|
+
},
|
|
1385
|
+
toolResult: {
|
|
1386
|
+
tool_use_id: toolCall.id,
|
|
1387
|
+
content: `工具执行失败: ${errorMessage}`,
|
|
1388
|
+
is_error: true,
|
|
1389
|
+
duration
|
|
1390
|
+
}
|
|
1391
|
+
});
|
|
1392
|
+
|
|
1393
|
+
// 添加错误结果以便 AI 知道
|
|
1394
|
+
results.push({
|
|
1395
|
+
tool_use_id: toolCall.id,
|
|
1396
|
+
content: `工具执行失败: ${errorMessage}`,
|
|
1397
|
+
is_error: true
|
|
1398
|
+
});
|
|
1399
|
+
|
|
1400
|
+
// 记录失败的调用到历史
|
|
1401
|
+
if (agentState) {
|
|
1402
|
+
agentState.toolCallHistory.push({
|
|
1403
|
+
name: toolCall.name,
|
|
1404
|
+
input: toolCall.input,
|
|
1405
|
+
inputHash: this.hashInput(toolCall.input),
|
|
1406
|
+
success: false,
|
|
1407
|
+
timestamp: Date.now()
|
|
1408
|
+
});
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
this.logger.error('Tool execution failed', { name: toolCall.name, error });
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
return results;
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
/**
|
|
1419
|
+
* 构建工具结果消息
|
|
1420
|
+
* 使用清晰的格式让 AI 理解工具已执行完成
|
|
1421
|
+
*/
|
|
1422
|
+
private buildToolResultMessage(results: ToolResult[]): ChatMessage {
|
|
1423
|
+
const content = results.map(r => {
|
|
1424
|
+
const toolName = r.name || r.tool_use_id;
|
|
1425
|
+
const status = r.is_error ? '执行失败' : '执行成功';
|
|
1426
|
+
return `【${toolName}】${status}。\n返回结果:${r.content}`;
|
|
1427
|
+
}).join('\n\n');
|
|
1428
|
+
|
|
1429
|
+
return {
|
|
1430
|
+
id: this.generateId(),
|
|
1431
|
+
role: MessageRole.TOOL,
|
|
1432
|
+
// 添加提示让 AI 继续完成用户的其他请求
|
|
1433
|
+
content: `工具已执行完成:\n\n${content}\n\n请检查用户的原始请求,如果还有未完成的任务,请继续调用相应工具完成。如果所有任务都已完成,请总结结果回复用户。`,
|
|
1434
|
+
timestamp: new Date()
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
/**
|
|
1439
|
+
* 生成唯一 ID
|
|
1440
|
+
*/
|
|
1441
|
+
private generateId(): string {
|
|
1442
|
+
return `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
// ============================================================================
|
|
1446
|
+
// 智能终止检测相关方法
|
|
1447
|
+
// ============================================================================
|
|
1448
|
+
|
|
1449
|
+
/**
|
|
1450
|
+
* 智能终止检测器
|
|
1451
|
+
* @param state Agent 状态
|
|
1452
|
+
* @param currentToolCalls 当前工具调用列表
|
|
1453
|
+
* @param toolResults 工具执行结果列表
|
|
1454
|
+
* @param config 配置参数
|
|
1455
|
+
* @param phase 检测场景:'after_ai_response'(AI响应后) | 'after_tool_execution'(工具执行后)
|
|
1456
|
+
*/
|
|
1457
|
+
private checkTermination(
|
|
1458
|
+
state: AgentState,
|
|
1459
|
+
currentToolCalls: ToolCall[],
|
|
1460
|
+
toolResults: ToolResult[],
|
|
1461
|
+
config: {
|
|
1462
|
+
maxRounds: number;
|
|
1463
|
+
timeoutMs: number;
|
|
1464
|
+
repeatThreshold: number;
|
|
1465
|
+
failureThreshold: number;
|
|
1466
|
+
},
|
|
1467
|
+
phase: 'after_ai_response' | 'after_tool_execution' = 'after_ai_response'
|
|
1468
|
+
): TerminationResult {
|
|
1469
|
+
this.logger.debug('Checking termination conditions', {
|
|
1470
|
+
currentRound: state.currentRound,
|
|
1471
|
+
maxRounds: config.maxRounds,
|
|
1472
|
+
toolCallsCount: currentToolCalls.length,
|
|
1473
|
+
historyCount: state.toolCallHistory.length,
|
|
1474
|
+
phase
|
|
1475
|
+
});
|
|
1476
|
+
|
|
1477
|
+
// 1. 检查 task_complete 工具调用 (两个场景都检查)
|
|
1478
|
+
const taskCompleteResult = toolResults.find(r => (r as any).isTaskComplete);
|
|
1479
|
+
if (taskCompleteResult) {
|
|
1480
|
+
const terminationMessage = (taskCompleteResult as any).content || '任务完成';
|
|
1481
|
+
return {
|
|
1482
|
+
shouldTerminate: true,
|
|
1483
|
+
reason: 'task_complete',
|
|
1484
|
+
message: terminationMessage
|
|
1485
|
+
};
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
// 2. 无工具调用检测 (只在 AI 响应后检查)
|
|
1489
|
+
if (phase === 'after_ai_response') {
|
|
1490
|
+
if (currentToolCalls.length === 0 && state.lastAiResponse) {
|
|
1491
|
+
// 先检查「未完成暗示」
|
|
1492
|
+
if (this.hasIncompleteHint(state.lastAiResponse)) {
|
|
1493
|
+
this.logger.warn('AI indicated incomplete task but no tools called', {
|
|
1494
|
+
response: state.lastAiResponse.substring(0, 100)
|
|
1495
|
+
});
|
|
1496
|
+
return { shouldTerminate: false, reason: 'no_tools' };
|
|
1497
|
+
}
|
|
1498
|
+
// 检查总结关键词
|
|
1499
|
+
if (this.hasSummaryHint(state.lastAiResponse)) {
|
|
1500
|
+
return {
|
|
1501
|
+
shouldTerminate: true,
|
|
1502
|
+
reason: 'summarizing',
|
|
1503
|
+
message: '检测到 AI 正在总结,任务已完成'
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
// 默认无工具调用结束
|
|
1507
|
+
return {
|
|
1508
|
+
shouldTerminate: true,
|
|
1509
|
+
reason: 'no_tools',
|
|
1510
|
+
message: '本轮无工具调用,任务完成'
|
|
1511
|
+
};
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
// 3. 重复工具调用检测 (两个场景都检查)
|
|
1516
|
+
if (currentToolCalls.length > 0) {
|
|
1517
|
+
const recentHistory = state.toolCallHistory.slice(-config.repeatThreshold * 2);
|
|
1518
|
+
|
|
1519
|
+
for (const tc of currentToolCalls) {
|
|
1520
|
+
const inputHash = this.hashInput(tc.input);
|
|
1521
|
+
const repeatCount = recentHistory.filter(h =>
|
|
1522
|
+
h.name === tc.name && h.inputHash === inputHash
|
|
1523
|
+
).length;
|
|
1524
|
+
|
|
1525
|
+
if (repeatCount >= config.repeatThreshold - 1) { // 加上本次
|
|
1526
|
+
return {
|
|
1527
|
+
shouldTerminate: true,
|
|
1528
|
+
reason: 'repeated_tool',
|
|
1529
|
+
message: `工具 ${tc.name} 被重复调用 ${repeatCount + 1} 次,可能陷入循环`
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
// 4. 连续失败检测 (两个场景都检查)
|
|
1536
|
+
const recentResults = state.toolCallHistory.slice(-config.failureThreshold * 2);
|
|
1537
|
+
const failureCount = recentResults.filter(r => !r.success).length;
|
|
1538
|
+
if (failureCount >= config.failureThreshold) {
|
|
1539
|
+
return {
|
|
1540
|
+
shouldTerminate: true,
|
|
1541
|
+
reason: 'high_failure_rate',
|
|
1542
|
+
message: `连续 ${failureCount} 次工具调用失败,停止执行`
|
|
1543
|
+
};
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
// 5. 超时检测 (两个场景都检查)
|
|
1547
|
+
const elapsedTime = Date.now() - state.startTime;
|
|
1548
|
+
if (elapsedTime > config.timeoutMs) {
|
|
1549
|
+
return {
|
|
1550
|
+
shouldTerminate: true,
|
|
1551
|
+
reason: 'timeout',
|
|
1552
|
+
message: `任务执行超时 (${Math.round(elapsedTime / 1000)}s)`
|
|
1553
|
+
};
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
// 6. 安全保底 - 最大轮数检测 (两个场景都检查)
|
|
1557
|
+
if (state.currentRound >= config.maxRounds) {
|
|
1558
|
+
return {
|
|
1559
|
+
shouldTerminate: true,
|
|
1560
|
+
reason: 'max_rounds',
|
|
1561
|
+
message: `达到最大执行轮数 (${config.maxRounds}轮)`
|
|
1562
|
+
};
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
return { shouldTerminate: false, reason: 'no_tools' };
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
/**
|
|
1569
|
+
* 检测 AI 回复中的「未完成暗示」
|
|
1570
|
+
* 使用正则表达式匹配更多变体
|
|
1571
|
+
*/
|
|
1572
|
+
private hasIncompleteHint(text: string): boolean {
|
|
1573
|
+
if (!text || text.length < 2) return false; // 边界情况检查
|
|
1574
|
+
|
|
1575
|
+
return AiAssistantService.INCOMPLETE_PATTERNS.some(p => p.test(text));
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
/**
|
|
1579
|
+
* 检测 AI 回复中的「总结暗示」
|
|
1580
|
+
*/
|
|
1581
|
+
private hasSummaryHint(text: string): boolean {
|
|
1582
|
+
if (!text || text.length < 2) return false; // 边界情况检查
|
|
1583
|
+
|
|
1584
|
+
return AiAssistantService.SUMMARY_PATTERNS.some(p => p.test(text));
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
// ============================================================================
|
|
1588
|
+
// 预编译的正则表达式(静态缓存)
|
|
1589
|
+
// ============================================================================
|
|
1590
|
+
|
|
1591
|
+
// 未完成暗示模式
|
|
1592
|
+
private static readonly INCOMPLETE_PATTERNS: RegExp[] = [
|
|
1593
|
+
// 中文模式
|
|
1594
|
+
/现在.{0,6}(为您|帮您|给您|查看|执行|检查)/, // 现在为您、现在继续为您
|
|
1595
|
+
/继续.{0,4}(为您|帮您|查看|执行|检查|获取)/, // 继续为您、继续查看
|
|
1596
|
+
/(让我|我来|我将|我会).{0,6}(查看|执行|检查|获取)/, // 让我查看、我来执行
|
|
1597
|
+
/(正在|开始|准备).{0,4}(执行|查看|检查|获取)/, // 正在执行、开始查看
|
|
1598
|
+
/(接下来|然后|之后|随后).{0,4}(将|会|要)/, // 接下来将、然后会
|
|
1599
|
+
/(马上|立即|即将|稍后|待会).{0,4}(为您|执行|查看)/, // 马上为您、即将执行
|
|
1600
|
+
/首先.{0,8}(然后|接着|之后)/, // 首先...然后
|
|
1601
|
+
/(第一步|下一步|接下来)/, // 步骤指示
|
|
1602
|
+
/(帮您|为您|给您).{0,4}(查看|执行|检查|获取|操作)/, // 帮您查看、为您执行
|
|
1603
|
+
/(我需要|需要).{0,4}(查看|执行|检查|获取)/, // 我需要查看
|
|
1604
|
+
/(先|首先|第一).{0,4}(看看|检查|执行)/, // 先看看、首先检查
|
|
1605
|
+
/下面.{0,4}(将|会|要|是)/, // 下面将、下面是
|
|
1606
|
+
/(等一下|稍等|请稍候)/, // 等待提示
|
|
1607
|
+
// 英文模式
|
|
1608
|
+
/\b(let me|i('ll| will| am going to))\b/i,
|
|
1609
|
+
/\b(now i|first i|next i)\b/i,
|
|
1610
|
+
/\b(going to|about to|starting to)\b/i,
|
|
1611
|
+
/\b(will now|shall now|let's)\b/i,
|
|
1612
|
+
/\b(proceed(ing)? to|continu(e|ing) to)\b/i,
|
|
1613
|
+
/\b(executing|running|checking|fetching)\b/i,
|
|
1614
|
+
/\b(step \d|first,?|next,?|then,?)\b/i,
|
|
1615
|
+
/\b(wait(ing)?|hold on)\b/i, // waiting, hold on
|
|
1616
|
+
/\b(i need to|i have to)\b/i, // I need to, I have to
|
|
1617
|
+
/\b(looking (at|into|for))\b/i, // looking at/into/for
|
|
1618
|
+
];
|
|
1619
|
+
|
|
1620
|
+
// 总结暗示模式
|
|
1621
|
+
private static readonly SUMMARY_PATTERNS: RegExp[] = [
|
|
1622
|
+
// 中文模式
|
|
1623
|
+
/(已经|已|均已).{0,4}(完成|结束|执行完)/,
|
|
1624
|
+
/(总结|汇总|综上|以上是|如上)/,
|
|
1625
|
+
/任务.{0,4}(完成|结束)/,
|
|
1626
|
+
/操作.{0,4}(完成|成功)/,
|
|
1627
|
+
/(至此|到此|至今|目前).{0,4}(完成|结束)/, // 至此完成
|
|
1628
|
+
/(全部|所有|均).{0,4}(完成|执行完|结束)/, // 全部完成
|
|
1629
|
+
/以上.{0,4}(就是|便是|为)/, // 以上就是
|
|
1630
|
+
/这.{0,4}(就是|便是).*结果/, // 这就是结果
|
|
1631
|
+
// 英文模式
|
|
1632
|
+
/\b(completed?|finished|done|all set)\b/i,
|
|
1633
|
+
/\b(in summary|to summarize|here('s| is) (the|a) summary)\b/i,
|
|
1634
|
+
/\b(task (is )?completed?|successfully (completed?|executed?))\b/i,
|
|
1635
|
+
/\b(that's (all|it)|we('re| are) done)\b/i, // that's all, we're done
|
|
1636
|
+
/\b(above (is|are)|here (is|are) the result)\b/i,
|
|
1637
|
+
];
|
|
1638
|
+
|
|
1639
|
+
/**
|
|
1640
|
+
* 计算输入的哈希值(用于重复检测)
|
|
1641
|
+
*/
|
|
1642
|
+
private hashInput(input: any): string {
|
|
1643
|
+
try {
|
|
1644
|
+
const str = JSON.stringify(input);
|
|
1645
|
+
let hash = 0;
|
|
1646
|
+
for (let i = 0; i < str.length; i++) {
|
|
1647
|
+
const char = str.charCodeAt(i);
|
|
1648
|
+
hash = ((hash << 5) - hash) + char;
|
|
1649
|
+
hash = hash & hash; // 转换为 32 位整数
|
|
1650
|
+
}
|
|
1651
|
+
return hash.toString(36);
|
|
1652
|
+
} catch {
|
|
1653
|
+
return Math.random().toString(36);
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
633
1656
|
}
|