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,7 +1,7 @@
|
|
|
1
1
|
import { Component, OnInit, OnDestroy, ViewChild, ElementRef, AfterViewChecked, AfterViewInit, ViewEncapsulation, HostBinding } from '@angular/core';
|
|
2
2
|
import { Subject } from 'rxjs';
|
|
3
3
|
import { takeUntil } from 'rxjs/operators';
|
|
4
|
-
import { ChatMessage, MessageRole } from '../../types/ai.types';
|
|
4
|
+
import { ChatMessage, MessageRole, StreamEvent, AgentStreamEvent } from '../../types/ai.types';
|
|
5
5
|
import { AiAssistantService } from '../../services/core/ai-assistant.service';
|
|
6
6
|
import { ConfigProviderService } from '../../services/core/config-provider.service';
|
|
7
7
|
import { LoggerService } from '../../services/core/logger.service';
|
|
@@ -325,9 +325,10 @@ export class AiSidebarComponent implements OnInit, OnDestroy, AfterViewChecked,
|
|
|
325
325
|
}
|
|
326
326
|
|
|
327
327
|
/**
|
|
328
|
-
* 处理发送消息
|
|
328
|
+
* 处理发送消息 - 使用 Agent 循环模式
|
|
329
|
+
* 支持多轮工具调用自动循环
|
|
329
330
|
*/
|
|
330
|
-
async
|
|
331
|
+
async onSendMessageWithAgent(content: string): Promise<void> {
|
|
331
332
|
if (!content.trim() || this.isLoading) {
|
|
332
333
|
return;
|
|
333
334
|
}
|
|
@@ -344,42 +345,343 @@ export class AiSidebarComponent implements OnInit, OnDestroy, AfterViewChecked,
|
|
|
344
345
|
// 滚动到底部
|
|
345
346
|
setTimeout(() => this.scrollToBottom(), 0);
|
|
346
347
|
|
|
347
|
-
// 清空输入框
|
|
348
|
-
content = '';
|
|
349
|
-
|
|
350
348
|
// 显示加载状态
|
|
351
349
|
this.isLoading = true;
|
|
352
350
|
|
|
351
|
+
// 创建 AI 消息占位符用于流式更新
|
|
352
|
+
const aiMessage: ChatMessage = {
|
|
353
|
+
id: this.generateId(),
|
|
354
|
+
role: MessageRole.ASSISTANT,
|
|
355
|
+
content: '',
|
|
356
|
+
timestamp: new Date()
|
|
357
|
+
};
|
|
358
|
+
this.messages.push(aiMessage);
|
|
359
|
+
|
|
360
|
+
// 工具调用跟踪
|
|
361
|
+
const toolStatus = new Map<string, { name: string; startTime: number }>();
|
|
362
|
+
|
|
353
363
|
try {
|
|
354
|
-
//
|
|
355
|
-
|
|
356
|
-
messages: this.messages,
|
|
357
|
-
maxTokens:
|
|
364
|
+
// 使用 Agent 循环流式 API
|
|
365
|
+
this.aiService.chatStreamWithAgentLoop({
|
|
366
|
+
messages: this.messages.slice(0, -1),
|
|
367
|
+
maxTokens: 2000,
|
|
358
368
|
temperature: 0.7
|
|
369
|
+
}, {
|
|
370
|
+
maxRounds: 5
|
|
371
|
+
}).pipe(
|
|
372
|
+
takeUntil(this.destroy$)
|
|
373
|
+
).subscribe({
|
|
374
|
+
next: (event: AgentStreamEvent) => {
|
|
375
|
+
switch (event.type) {
|
|
376
|
+
case 'text_delta':
|
|
377
|
+
// 文本流式显示
|
|
378
|
+
if (event.textDelta) {
|
|
379
|
+
aiMessage.content += event.textDelta;
|
|
380
|
+
this.shouldScrollToBottom = true;
|
|
381
|
+
}
|
|
382
|
+
break;
|
|
383
|
+
|
|
384
|
+
case 'tool_use_start':
|
|
385
|
+
// 工具开始 - 使用专门的工具卡片样式
|
|
386
|
+
const toolName = event.toolCall?.name || 'unknown';
|
|
387
|
+
aiMessage.content += `
|
|
388
|
+
<div class="tool-call-card tool-executing">
|
|
389
|
+
<div class="tool-header">
|
|
390
|
+
<span class="tool-icon">🔧</span>
|
|
391
|
+
<span class="tool-name">${toolName}</span>
|
|
392
|
+
<span class="tool-status" id="tool-status-${event.toolCall?.id}">执行中...</span>
|
|
393
|
+
</div>
|
|
394
|
+
</div>`;
|
|
395
|
+
if (event.toolCall?.id) {
|
|
396
|
+
toolStatus.set(event.toolCall.id, {
|
|
397
|
+
name: toolName,
|
|
398
|
+
startTime: Date.now()
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
this.shouldScrollToBottom = true;
|
|
402
|
+
break;
|
|
403
|
+
|
|
404
|
+
case 'tool_executing':
|
|
405
|
+
// 工具正在执行(额外状态)
|
|
406
|
+
break;
|
|
407
|
+
|
|
408
|
+
case 'tool_executed':
|
|
409
|
+
// 工具完成 - 更新工具卡片
|
|
410
|
+
if (event.toolCall && event.toolResult) {
|
|
411
|
+
const name = toolStatus.get(event.toolCall.id)?.name || event.toolCall.name || 'unknown';
|
|
412
|
+
const duration = event.toolResult.duration || 0;
|
|
413
|
+
|
|
414
|
+
// 更新工具卡片状态
|
|
415
|
+
const executingCardRegex = new RegExp(
|
|
416
|
+
`<div class="tool-call-card tool-executing">\\s*<div class="tool-header">\\s*<span class="tool-icon">🔧</span>\\s*<span class="tool-name">${name}</span>[^]*?</div>\\s*</div>`,
|
|
417
|
+
'g'
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
// 构建工具结果卡片
|
|
421
|
+
let toolCard = `
|
|
422
|
+
<div class="tool-call-card tool-success">
|
|
423
|
+
<div class="tool-header">
|
|
424
|
+
<span class="tool-icon">✅</span>
|
|
425
|
+
<span class="tool-name">${name}</span>
|
|
426
|
+
<span class="tool-duration">${duration}ms</span>
|
|
427
|
+
</div>`;
|
|
428
|
+
|
|
429
|
+
// 添加工具输出
|
|
430
|
+
if (event.toolResult.content && !event.toolResult.is_error) {
|
|
431
|
+
const preview = event.toolResult.content.substring(0, 500);
|
|
432
|
+
const truncated = event.toolResult.content.length > 500 ? '...' : '';
|
|
433
|
+
toolCard += `
|
|
434
|
+
<div class="tool-output">
|
|
435
|
+
<div class="tool-output-header">输出:</div>
|
|
436
|
+
<pre class="tool-output-content">${this.escapeHtml(preview)}${truncated}</pre>
|
|
437
|
+
</div>`;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
toolCard += `</div>`;
|
|
441
|
+
|
|
442
|
+
aiMessage.content = aiMessage.content.replace(executingCardRegex, toolCard);
|
|
443
|
+
toolStatus.delete(event.toolCall.id);
|
|
444
|
+
}
|
|
445
|
+
this.shouldScrollToBottom = true;
|
|
446
|
+
break;
|
|
447
|
+
|
|
448
|
+
case 'tool_error':
|
|
449
|
+
// 工具错误 - 更新工具卡片为错误状态
|
|
450
|
+
if (event.toolCall) {
|
|
451
|
+
const name = toolStatus.get(event.toolCall.id)?.name || event.toolCall.name || 'unknown';
|
|
452
|
+
const errorMsg = event.toolResult?.content || 'Unknown error';
|
|
453
|
+
|
|
454
|
+
const executingCardRegex = new RegExp(
|
|
455
|
+
`<div class="tool-call-card tool-executing">\\s*<div class="tool-header">\\s*<span class="tool-icon">🔧</span>\\s*<span class="tool-name">${name}</span>[^]*?</div>\\s*</div>`,
|
|
456
|
+
'g'
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
const errorCard = `
|
|
460
|
+
<div class="tool-call-card tool-error">
|
|
461
|
+
<div class="tool-header">
|
|
462
|
+
<span class="tool-icon">❌</span>
|
|
463
|
+
<span class="tool-name">${name}</span>
|
|
464
|
+
<span class="tool-status">失败</span>
|
|
465
|
+
</div>
|
|
466
|
+
<div class="tool-output tool-error-message">
|
|
467
|
+
<pre class="tool-output-content">${this.escapeHtml(errorMsg)}</pre>
|
|
468
|
+
</div>
|
|
469
|
+
</div>`;
|
|
470
|
+
|
|
471
|
+
aiMessage.content = aiMessage.content.replace(executingCardRegex, errorCard);
|
|
472
|
+
toolStatus.delete(event.toolCall.id);
|
|
473
|
+
}
|
|
474
|
+
this.shouldScrollToBottom = true;
|
|
475
|
+
break;
|
|
476
|
+
|
|
477
|
+
case 'round_start':
|
|
478
|
+
// 新一轮开始
|
|
479
|
+
if (event.round && event.round > 1) {
|
|
480
|
+
aiMessage.content += '\n\n---\n\n';
|
|
481
|
+
}
|
|
482
|
+
break;
|
|
483
|
+
|
|
484
|
+
case 'round_end':
|
|
485
|
+
// 一轮结束
|
|
486
|
+
break;
|
|
487
|
+
|
|
488
|
+
case 'agent_complete':
|
|
489
|
+
// Agent 循环完成 - 显示终止原因
|
|
490
|
+
this.logger.info('Agent completed', {
|
|
491
|
+
reason: event.reason,
|
|
492
|
+
totalRounds: event.totalRounds
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
// 终止原因映射
|
|
496
|
+
const reasonText: Record<string, { icon: string; label: string }> = {
|
|
497
|
+
'task_complete': { icon: '✅', label: '任务完成' },
|
|
498
|
+
'no_tools': { icon: '✅', label: '已执行完成' },
|
|
499
|
+
'summarizing': { icon: '✅', label: '总结完成' },
|
|
500
|
+
'repeated_tool': { icon: '⚠️', label: '检测到重复操作' },
|
|
501
|
+
'high_failure_rate': { icon: '⚠️', label: '多次调用失败' },
|
|
502
|
+
'timeout': { icon: '⏱️', label: '执行超时' },
|
|
503
|
+
'max_rounds': { icon: '⚠️', label: '达到最大轮数' },
|
|
504
|
+
'user_cancel': { icon: '🛑', label: '用户取消' }
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
const reasonInfo = reasonText[event.reason || ''] || { icon: '📌', label: '完成' };
|
|
508
|
+
const roundsText = event.totalRounds ? ` (${event.totalRounds} 轮)` : '';
|
|
509
|
+
|
|
510
|
+
// 添加终止信息
|
|
511
|
+
aiMessage.content += `\n\n---\n**${reasonInfo.icon} ${reasonInfo.label}**${roundsText}`;
|
|
512
|
+
|
|
513
|
+
// 如果有终止消息,添加到内容中
|
|
514
|
+
if (event.terminationMessage) {
|
|
515
|
+
aiMessage.content += `\n${event.terminationMessage}`;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
this.shouldScrollToBottom = true;
|
|
519
|
+
break;
|
|
520
|
+
|
|
521
|
+
case 'error':
|
|
522
|
+
// 错误
|
|
523
|
+
aiMessage.content += `\n\n❌ 错误: ${event.error}`;
|
|
524
|
+
this.shouldScrollToBottom = true;
|
|
525
|
+
break;
|
|
526
|
+
}
|
|
527
|
+
},
|
|
528
|
+
error: (error) => {
|
|
529
|
+
this.logger.error('Agent stream error', error);
|
|
530
|
+
aiMessage.content += `\n\n❌ 错误: ${error instanceof Error ? error.message : 'Unknown error'}`;
|
|
531
|
+
this.isLoading = false;
|
|
532
|
+
this.shouldScrollToBottom = true;
|
|
533
|
+
this.saveChatHistory();
|
|
534
|
+
},
|
|
535
|
+
complete: () => {
|
|
536
|
+
this.isLoading = false;
|
|
537
|
+
this.updateTokenUsage();
|
|
538
|
+
this.saveChatHistory();
|
|
539
|
+
this.shouldScrollToBottom = true;
|
|
540
|
+
}
|
|
359
541
|
});
|
|
360
542
|
|
|
361
|
-
|
|
362
|
-
this.
|
|
543
|
+
} catch (error) {
|
|
544
|
+
this.logger.error('Failed to send message with agent', error);
|
|
545
|
+
aiMessage.content = `抱歉,我遇到了一些问题:${error instanceof Error ? error.message : 'Unknown error'}\n\n请稍后重试。`;
|
|
546
|
+
this.isLoading = false;
|
|
547
|
+
this.updateTokenUsage();
|
|
548
|
+
setTimeout(() => this.scrollToBottom(), 0);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* 处理发送消息 - 原有方法(保留兼容性)
|
|
554
|
+
*/
|
|
555
|
+
async onSendMessage(content: string): Promise<void> {
|
|
556
|
+
if (!content.trim() || this.isLoading) {
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// 添加用户消息
|
|
561
|
+
const userMessage: ChatMessage = {
|
|
562
|
+
id: this.generateId(),
|
|
563
|
+
role: MessageRole.USER,
|
|
564
|
+
content: content.trim(),
|
|
565
|
+
timestamp: new Date()
|
|
566
|
+
};
|
|
567
|
+
this.messages.push(userMessage);
|
|
568
|
+
|
|
569
|
+
// 滚动到底部
|
|
570
|
+
setTimeout(() => this.scrollToBottom(), 0);
|
|
571
|
+
|
|
572
|
+
// 显示加载状态
|
|
573
|
+
this.isLoading = true;
|
|
574
|
+
|
|
575
|
+
// 创建 AI 消息占位符用于流式更新
|
|
576
|
+
const aiMessage: ChatMessage = {
|
|
577
|
+
id: this.generateId(),
|
|
578
|
+
role: MessageRole.ASSISTANT,
|
|
579
|
+
content: '',
|
|
580
|
+
timestamp: new Date()
|
|
581
|
+
};
|
|
582
|
+
this.messages.push(aiMessage);
|
|
363
583
|
|
|
364
|
-
|
|
365
|
-
|
|
584
|
+
// 工具调用跟踪
|
|
585
|
+
const pendingTools = new Map<string, { name: string; startTime: number }>();
|
|
586
|
+
const toolResults: string[] = [];
|
|
587
|
+
|
|
588
|
+
try {
|
|
589
|
+
// 使用流式 API
|
|
590
|
+
this.aiService.chatStream({
|
|
591
|
+
messages: this.messages.slice(0, -1), // 排除刚添加的空 AI 消息
|
|
592
|
+
maxTokens: 2000,
|
|
593
|
+
temperature: 0.7
|
|
594
|
+
}).pipe(
|
|
595
|
+
takeUntil(this.destroy$)
|
|
596
|
+
).subscribe({
|
|
597
|
+
next: (event: StreamEvent) => {
|
|
598
|
+
switch (event.type) {
|
|
599
|
+
case 'text_delta':
|
|
600
|
+
// 文本流式显示
|
|
601
|
+
if (event.textDelta) {
|
|
602
|
+
aiMessage.content += event.textDelta;
|
|
603
|
+
this.shouldScrollToBottom = true;
|
|
604
|
+
}
|
|
605
|
+
break;
|
|
606
|
+
|
|
607
|
+
case 'tool_use_start':
|
|
608
|
+
// 工具开始 - 显示工具名称
|
|
609
|
+
const toolName = event.toolCall?.name || 'unknown';
|
|
610
|
+
aiMessage.content += `\n\n🔧 正在执行 ${toolName}...`;
|
|
611
|
+
if (event.toolCall?.id) {
|
|
612
|
+
pendingTools.set(event.toolCall.id, {
|
|
613
|
+
name: toolName,
|
|
614
|
+
startTime: Date.now()
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
this.shouldScrollToBottom = true;
|
|
618
|
+
break;
|
|
619
|
+
|
|
620
|
+
case 'tool_use_end':
|
|
621
|
+
// 工具完成 - 更新状态
|
|
622
|
+
if (event.toolCall) {
|
|
623
|
+
const toolInfo = pendingTools.get(event.toolCall.id);
|
|
624
|
+
const duration = toolInfo ? Date.now() - toolInfo.startTime : 0;
|
|
625
|
+
const name = toolInfo?.name || event.toolCall.name || 'unknown';
|
|
626
|
+
|
|
627
|
+
aiMessage.content = aiMessage.content.replace(
|
|
628
|
+
new RegExp(`🔧 正在执行 ${name}\\.\\.\\.`),
|
|
629
|
+
`✅ ${name} (${duration}ms)`
|
|
630
|
+
);
|
|
631
|
+
pendingTools.delete(event.toolCall.id);
|
|
632
|
+
}
|
|
633
|
+
this.shouldScrollToBottom = true;
|
|
634
|
+
break;
|
|
635
|
+
|
|
636
|
+
case 'tool_result':
|
|
637
|
+
// 工具结果 - 存储用于最后显示
|
|
638
|
+
if (event.result) {
|
|
639
|
+
const preview = event.result.content.substring(0, 500);
|
|
640
|
+
const truncated = event.result.content.length > 500 ? '\n...(已截断)' : '';
|
|
641
|
+
toolResults.push(`\n\n📋 **输出**:\n\`\`\`\n${preview}${truncated}\n\`\`\``);
|
|
642
|
+
}
|
|
643
|
+
break;
|
|
644
|
+
|
|
645
|
+
case 'tool_error':
|
|
646
|
+
// 工具错误
|
|
647
|
+
aiMessage.content = aiMessage.content.replace(
|
|
648
|
+
/🔧 正在执行 \w+\.\.\./,
|
|
649
|
+
`❌ 工具执行失败: ${event.error}`
|
|
650
|
+
);
|
|
651
|
+
this.shouldScrollToBottom = true;
|
|
652
|
+
break;
|
|
653
|
+
|
|
654
|
+
case 'message_end':
|
|
655
|
+
// 消息结束 - 附加工具结果
|
|
656
|
+
if (toolResults.length > 0) {
|
|
657
|
+
aiMessage.content += toolResults.join('');
|
|
658
|
+
}
|
|
659
|
+
this.logger.info('Stream completed');
|
|
660
|
+
break;
|
|
661
|
+
}
|
|
662
|
+
},
|
|
663
|
+
error: (error) => {
|
|
664
|
+
this.logger.error('Stream error', error);
|
|
665
|
+
aiMessage.content += `\n\n❌ 错误: ${error instanceof Error ? error.message : 'Unknown error'}`;
|
|
666
|
+
this.isLoading = false;
|
|
667
|
+
this.shouldScrollToBottom = true;
|
|
668
|
+
this.saveChatHistory();
|
|
669
|
+
},
|
|
670
|
+
complete: () => {
|
|
671
|
+
this.isLoading = false;
|
|
672
|
+
this.updateTokenUsage();
|
|
673
|
+
this.saveChatHistory();
|
|
674
|
+
this.shouldScrollToBottom = true;
|
|
675
|
+
}
|
|
676
|
+
});
|
|
366
677
|
|
|
367
678
|
} catch (error) {
|
|
368
679
|
this.logger.error('Failed to send message', error);
|
|
369
680
|
|
|
370
681
|
// 添加错误消息
|
|
371
|
-
|
|
372
|
-
id: this.generateId(),
|
|
373
|
-
role: MessageRole.ASSISTANT,
|
|
374
|
-
content: `抱歉,我遇到了一些问题:${error instanceof Error ? error.message : 'Unknown error'}\n\n请稍后重试。`,
|
|
375
|
-
timestamp: new Date()
|
|
376
|
-
};
|
|
377
|
-
this.messages.push(errorMessage);
|
|
378
|
-
} finally {
|
|
682
|
+
aiMessage.content = `抱歉,我遇到了一些问题:${error instanceof Error ? error.message : 'Unknown error'}\n\n请稍后重试。`;
|
|
379
683
|
this.isLoading = false;
|
|
380
|
-
// 更新 Token 使用情况
|
|
381
684
|
this.updateTokenUsage();
|
|
382
|
-
// 滚动到底部
|
|
383
685
|
setTimeout(() => this.scrollToBottom(), 0);
|
|
384
686
|
}
|
|
385
687
|
}
|
|
@@ -600,12 +902,35 @@ export class AiSidebarComponent implements OnInit, OnDestroy, AfterViewChecked,
|
|
|
600
902
|
}
|
|
601
903
|
|
|
602
904
|
/**
|
|
603
|
-
*
|
|
905
|
+
* 格式化消息内容(支持 Markdown 渲染)
|
|
604
906
|
*/
|
|
605
907
|
formatMessage(content: string): string {
|
|
606
|
-
return
|
|
607
|
-
|
|
608
|
-
|
|
908
|
+
if (!content) return '';
|
|
909
|
+
|
|
910
|
+
try {
|
|
911
|
+
// 使用 marked 库渲染 Markdown
|
|
912
|
+
const { marked } = require('marked');
|
|
913
|
+
|
|
914
|
+
// 配置 marked 选项
|
|
915
|
+
marked.setOptions({
|
|
916
|
+
breaks: true, // 支持换行
|
|
917
|
+
gfm: true, // 支持 GitHub Flavored Markdown
|
|
918
|
+
headerIds: false, // 不生成标题 ID
|
|
919
|
+
mangle: false // 不转义邮箱
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
return marked.parse(content);
|
|
923
|
+
} catch (e) {
|
|
924
|
+
// 如果 marked 失败,使用基本格式化
|
|
925
|
+
return content
|
|
926
|
+
.replace(/&/g, '&')
|
|
927
|
+
.replace(/</g, '<')
|
|
928
|
+
.replace(/>/g, '>')
|
|
929
|
+
.replace(/\n/g, '<br>')
|
|
930
|
+
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
|
931
|
+
.replace(/\*(.*?)\*/g, '<em>$1</em>')
|
|
932
|
+
.replace(/`(.*?)`/g, '<code>$1</code>');
|
|
933
|
+
}
|
|
609
934
|
}
|
|
610
935
|
|
|
611
936
|
/**
|
|
@@ -649,7 +974,8 @@ export class AiSidebarComponent implements OnInit, OnDestroy, AfterViewChecked,
|
|
|
649
974
|
submit(): void {
|
|
650
975
|
const message = this.inputValue.trim();
|
|
651
976
|
if (message && !this.isLoading) {
|
|
652
|
-
|
|
977
|
+
// 使用 Agent 循环模式发送消息(支持多轮工具调用)
|
|
978
|
+
this.onSendMessageWithAgent(message);
|
|
653
979
|
this.inputValue = '';
|
|
654
980
|
setTimeout(() => this.autoResize(), 0);
|
|
655
981
|
this.textInput?.nativeElement.focus();
|
|
@@ -687,4 +1013,13 @@ export class AiSidebarComponent implements OnInit, OnDestroy, AfterViewChecked,
|
|
|
687
1013
|
isOverLimit(): boolean {
|
|
688
1014
|
return this.getCharCount() > this.charLimit;
|
|
689
1015
|
}
|
|
1016
|
+
|
|
1017
|
+
/**
|
|
1018
|
+
* 转义 HTML 特殊字符
|
|
1019
|
+
*/
|
|
1020
|
+
private escapeHtml(text: string): string {
|
|
1021
|
+
const div = document.createElement('div');
|
|
1022
|
+
div.textContent = text;
|
|
1023
|
+
return div.innerHTML;
|
|
1024
|
+
}
|
|
690
1025
|
}
|
|
@@ -2,6 +2,7 @@ import { Component, Output, EventEmitter, Input, ViewChild, ElementRef, OnInit,
|
|
|
2
2
|
import { Subject } from 'rxjs';
|
|
3
3
|
import { debounceTime, takeUntil } from 'rxjs/operators';
|
|
4
4
|
import { ConfigProviderService } from '../../services/core/config-provider.service';
|
|
5
|
+
import { AiAssistantService } from '../../services/core/ai-assistant.service';
|
|
5
6
|
|
|
6
7
|
@Component({
|
|
7
8
|
selector: 'app-chat-input',
|
|
@@ -22,7 +23,14 @@ export class ChatInputComponent implements OnInit, OnDestroy {
|
|
|
22
23
|
isComposing = false; // 用于处理中文输入法
|
|
23
24
|
enterToSend: boolean = true; // Enter键发送
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
// 智能建议相关
|
|
27
|
+
suggestions: string[] = [];
|
|
28
|
+
showSuggestions = false;
|
|
29
|
+
|
|
30
|
+
constructor(
|
|
31
|
+
private config: ConfigProviderService,
|
|
32
|
+
private aiService: AiAssistantService
|
|
33
|
+
) {}
|
|
26
34
|
|
|
27
35
|
ngOnInit(): void {
|
|
28
36
|
// 读取 Enter 发送设置
|
|
@@ -45,10 +53,34 @@ export class ChatInputComponent implements OnInit, OnDestroy {
|
|
|
45
53
|
|
|
46
54
|
/**
|
|
47
55
|
* 处理输入变化
|
|
56
|
+
* 实现智能建议功能
|
|
57
|
+
*/
|
|
58
|
+
async onInputChange(value: string): Promise<void> {
|
|
59
|
+
if (value.length < 2) {
|
|
60
|
+
this.suggestions = [];
|
|
61
|
+
this.showSuggestions = false;
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 调用已实现的智能建议服务
|
|
66
|
+
this.suggestions = await this.aiService.getSuggestedCommands(value);
|
|
67
|
+
this.showSuggestions = this.suggestions.length > 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 选择建议
|
|
72
|
+
*/
|
|
73
|
+
selectSuggestion(suggestion: string): void {
|
|
74
|
+
this.inputValue = suggestion;
|
|
75
|
+
this.showSuggestions = false;
|
|
76
|
+
this.focus();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 关闭建议
|
|
48
81
|
*/
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
// 可以基于输入内容提供命令建议
|
|
82
|
+
dismissSuggestions(): void {
|
|
83
|
+
this.showSuggestions = false;
|
|
52
84
|
}
|
|
53
85
|
|
|
54
86
|
/**
|