tabby-ai-assistant 1.0.13 → 1.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/.editorconfig +18 -0
  2. package/README.md +40 -10
  3. package/dist/index.js +1 -1
  4. package/package.json +5 -3
  5. package/src/components/chat/ai-sidebar.component.scss +220 -9
  6. package/src/components/chat/ai-sidebar.component.ts +379 -29
  7. package/src/components/chat/chat-input.component.ts +36 -4
  8. package/src/components/chat/chat-interface.component.ts +225 -5
  9. package/src/components/chat/chat-message.component.ts +6 -1
  10. package/src/components/settings/context-settings.component.ts +91 -91
  11. package/src/components/terminal/ai-toolbar-button.component.ts +4 -2
  12. package/src/components/terminal/command-suggestion.component.ts +148 -6
  13. package/src/index.ts +81 -19
  14. package/src/providers/tabby/ai-toolbar-button.provider.ts +7 -3
  15. package/src/services/chat/ai-sidebar.service.ts +448 -410
  16. package/src/services/chat/chat-session.service.ts +36 -12
  17. package/src/services/context/compaction.ts +110 -134
  18. package/src/services/context/manager.ts +27 -7
  19. package/src/services/context/memory.ts +17 -33
  20. package/src/services/context/summary.service.ts +136 -0
  21. package/src/services/core/ai-assistant.service.ts +1060 -37
  22. package/src/services/core/ai-provider-manager.service.ts +154 -25
  23. package/src/services/core/checkpoint.service.ts +218 -18
  24. package/src/services/core/toast.service.ts +106 -106
  25. package/src/services/providers/anthropic-provider.service.ts +126 -30
  26. package/src/services/providers/base-provider.service.ts +90 -7
  27. package/src/services/providers/glm-provider.service.ts +151 -38
  28. package/src/services/providers/minimax-provider.service.ts +55 -40
  29. package/src/services/providers/ollama-provider.service.ts +117 -28
  30. package/src/services/providers/openai-compatible.service.ts +164 -34
  31. package/src/services/providers/openai-provider.service.ts +169 -34
  32. package/src/services/providers/vllm-provider.service.ts +116 -28
  33. package/src/services/terminal/terminal-context.service.ts +265 -5
  34. package/src/services/terminal/terminal-manager.service.ts +845 -748
  35. package/src/services/terminal/terminal-tools.service.ts +612 -441
  36. package/src/types/ai.types.ts +156 -3
  37. package/src/utils/cost.utils.ts +249 -0
  38. package/src/utils/validation.utils.ts +306 -2
  39. package/dist/index.js.LICENSE.txt +0 -18
  40. package/src/services/terminal/command-analyzer.service.ts +0 -43
  41. package/src/services/terminal/context-menu.service.ts +0 -45
  42. 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 { ChatMessage, MessageRole, ChatRequest, ChatResponse, CommandRequest, CommandResponse, ExplainRequest, ExplainResponse, StreamEvent } from '../../types/ai.types';
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, ToolCall, ToolResult } from '../terminal/terminal-tools.service';
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
- response = await this.handleToolCalls(request, response, activeProvider);
182
+ // 处理工具调用(返回值包含工具调用统计)
183
+ const { finalResponse, totalToolCallsExecuted } = await this.handleToolCallsWithStats(
184
+ request, response, activeProvider
185
+ );
186
+ response = finalResponse;
174
187
 
175
- // 检测幻觉:AI声称执行了操作但未调用工具
176
- const toolCalls = (response as any).toolCalls;
188
+ // 使用累计的工具调用次数进行幻觉检测
177
189
  const hallucinationDetected = this.detectHallucination({
178
190
  text: response.message.content,
179
- toolCallCount: toolCalls?.length || 0
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 activeProvider.chatStream(request).pipe(
218
- tap((event: StreamEvent) => {
219
- // 工具调用完成时执行
220
- if (event.type === 'tool_use_end' && event.toolCall) {
221
- this.executeToolAndContinue(event.toolCall, request, activeProvider);
222
- }
223
- }),
224
- catchError(error => {
225
- this.logger.error('Stream error', error);
226
- return throwError(() => error);
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 executeToolAndContinue(
258
+ private async executeToolAndEmit(
235
259
  toolCall: { id: string; name: string; input: any },
236
- request: ChatRequest,
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
- this.logger.info('Tool executed in stream', { name: toolCall.name, result: result.content.substring(0, 100) });
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
- this.logger.error('Tool execution failed in stream', error);
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
- // TODO: 从当前终端获取选中文本
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
- // TODO: 从当前终端获取选中文本
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
- // TODO: 实现打开聊天界面的逻辑
475
- // 可以使用Tabby的窗口API或者Angular路由
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(_input: string): Promise<string[]> {
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
- // TODO: 实现智能建议逻辑
576
- // 可以基于历史命令、当前上下文等生成建议
577
- return [];
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
  }