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.
- package/.editorconfig +18 -0
- package/README.md +40 -10
- package/dist/index.js +1 -1
- package/package.json +5 -3
- package/src/components/chat/ai-sidebar.component.scss +220 -9
- package/src/components/chat/ai-sidebar.component.ts +379 -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 +81 -19
- package/src/providers/tabby/ai-toolbar-button.provider.ts +7 -3
- package/src/services/chat/ai-sidebar.service.ts +448 -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 +845 -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';
|
|
@@ -220,6 +220,21 @@ export class AiSidebarComponent implements OnInit, OnDestroy, AfterViewChecked,
|
|
|
220
220
|
this.sendWelcomeMessage();
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
+
// 订阅预设消息(快捷键功能)
|
|
224
|
+
this.sidebarService.presetMessage$.pipe(
|
|
225
|
+
takeUntil(this.destroy$)
|
|
226
|
+
).subscribe(({ message, autoSend }) => {
|
|
227
|
+
this.inputValue = message;
|
|
228
|
+
|
|
229
|
+
if (autoSend) {
|
|
230
|
+
// 延迟一点确保 UI 更新
|
|
231
|
+
setTimeout(() => this.submit(), 100);
|
|
232
|
+
} else {
|
|
233
|
+
// 聚焦输入框
|
|
234
|
+
this.textInput?.nativeElement?.focus();
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
223
238
|
// 延迟检查滚动状态(等待 DOM 渲染)
|
|
224
239
|
setTimeout(() => this.checkScrollState(), 100);
|
|
225
240
|
}
|
|
@@ -325,9 +340,10 @@ export class AiSidebarComponent implements OnInit, OnDestroy, AfterViewChecked,
|
|
|
325
340
|
}
|
|
326
341
|
|
|
327
342
|
/**
|
|
328
|
-
* 处理发送消息
|
|
343
|
+
* 处理发送消息 - 使用 Agent 循环模式
|
|
344
|
+
* 支持多轮工具调用自动循环
|
|
329
345
|
*/
|
|
330
|
-
async
|
|
346
|
+
async onSendMessageWithAgent(content: string): Promise<void> {
|
|
331
347
|
if (!content.trim() || this.isLoading) {
|
|
332
348
|
return;
|
|
333
349
|
}
|
|
@@ -344,42 +360,343 @@ export class AiSidebarComponent implements OnInit, OnDestroy, AfterViewChecked,
|
|
|
344
360
|
// 滚动到底部
|
|
345
361
|
setTimeout(() => this.scrollToBottom(), 0);
|
|
346
362
|
|
|
347
|
-
// 清空输入框
|
|
348
|
-
content = '';
|
|
349
|
-
|
|
350
363
|
// 显示加载状态
|
|
351
364
|
this.isLoading = true;
|
|
352
365
|
|
|
366
|
+
// 创建 AI 消息占位符用于流式更新
|
|
367
|
+
const aiMessage: ChatMessage = {
|
|
368
|
+
id: this.generateId(),
|
|
369
|
+
role: MessageRole.ASSISTANT,
|
|
370
|
+
content: '',
|
|
371
|
+
timestamp: new Date()
|
|
372
|
+
};
|
|
373
|
+
this.messages.push(aiMessage);
|
|
374
|
+
|
|
375
|
+
// 工具调用跟踪
|
|
376
|
+
const toolStatus = new Map<string, { name: string; startTime: number }>();
|
|
377
|
+
|
|
353
378
|
try {
|
|
354
|
-
//
|
|
355
|
-
|
|
356
|
-
messages: this.messages,
|
|
357
|
-
maxTokens:
|
|
379
|
+
// 使用 Agent 循环流式 API
|
|
380
|
+
this.aiService.chatStreamWithAgentLoop({
|
|
381
|
+
messages: this.messages.slice(0, -1),
|
|
382
|
+
maxTokens: 2000,
|
|
358
383
|
temperature: 0.7
|
|
384
|
+
}, {
|
|
385
|
+
maxRounds: 5
|
|
386
|
+
}).pipe(
|
|
387
|
+
takeUntil(this.destroy$)
|
|
388
|
+
).subscribe({
|
|
389
|
+
next: (event: AgentStreamEvent) => {
|
|
390
|
+
switch (event.type) {
|
|
391
|
+
case 'text_delta':
|
|
392
|
+
// 文本流式显示
|
|
393
|
+
if (event.textDelta) {
|
|
394
|
+
aiMessage.content += event.textDelta;
|
|
395
|
+
this.shouldScrollToBottom = true;
|
|
396
|
+
}
|
|
397
|
+
break;
|
|
398
|
+
|
|
399
|
+
case 'tool_use_start':
|
|
400
|
+
// 工具开始 - 使用专门的工具卡片样式
|
|
401
|
+
const toolName = event.toolCall?.name || 'unknown';
|
|
402
|
+
aiMessage.content += `
|
|
403
|
+
<div class="tool-call-card tool-executing">
|
|
404
|
+
<div class="tool-header">
|
|
405
|
+
<span class="tool-icon">🔧</span>
|
|
406
|
+
<span class="tool-name">${toolName}</span>
|
|
407
|
+
<span class="tool-status" id="tool-status-${event.toolCall?.id}">执行中...</span>
|
|
408
|
+
</div>
|
|
409
|
+
</div>`;
|
|
410
|
+
if (event.toolCall?.id) {
|
|
411
|
+
toolStatus.set(event.toolCall.id, {
|
|
412
|
+
name: toolName,
|
|
413
|
+
startTime: Date.now()
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
this.shouldScrollToBottom = true;
|
|
417
|
+
break;
|
|
418
|
+
|
|
419
|
+
case 'tool_executing':
|
|
420
|
+
// 工具正在执行(额外状态)
|
|
421
|
+
break;
|
|
422
|
+
|
|
423
|
+
case 'tool_executed':
|
|
424
|
+
// 工具完成 - 更新工具卡片
|
|
425
|
+
if (event.toolCall && event.toolResult) {
|
|
426
|
+
const name = toolStatus.get(event.toolCall.id)?.name || event.toolCall.name || 'unknown';
|
|
427
|
+
const duration = event.toolResult.duration || 0;
|
|
428
|
+
|
|
429
|
+
// 更新工具卡片状态
|
|
430
|
+
const executingCardRegex = new RegExp(
|
|
431
|
+
`<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>`,
|
|
432
|
+
'g'
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
// 构建工具结果卡片
|
|
436
|
+
let toolCard = `
|
|
437
|
+
<div class="tool-call-card tool-success">
|
|
438
|
+
<div class="tool-header">
|
|
439
|
+
<span class="tool-icon">✅</span>
|
|
440
|
+
<span class="tool-name">${name}</span>
|
|
441
|
+
<span class="tool-duration">${duration}ms</span>
|
|
442
|
+
</div>`;
|
|
443
|
+
|
|
444
|
+
// 添加工具输出
|
|
445
|
+
if (event.toolResult.content && !event.toolResult.is_error) {
|
|
446
|
+
const preview = event.toolResult.content.substring(0, 500);
|
|
447
|
+
const truncated = event.toolResult.content.length > 500 ? '...' : '';
|
|
448
|
+
toolCard += `
|
|
449
|
+
<div class="tool-output">
|
|
450
|
+
<div class="tool-output-header">输出:</div>
|
|
451
|
+
<pre class="tool-output-content">${this.escapeHtml(preview)}${truncated}</pre>
|
|
452
|
+
</div>`;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
toolCard += `</div>`;
|
|
456
|
+
|
|
457
|
+
aiMessage.content = aiMessage.content.replace(executingCardRegex, toolCard);
|
|
458
|
+
toolStatus.delete(event.toolCall.id);
|
|
459
|
+
}
|
|
460
|
+
this.shouldScrollToBottom = true;
|
|
461
|
+
break;
|
|
462
|
+
|
|
463
|
+
case 'tool_error':
|
|
464
|
+
// 工具错误 - 更新工具卡片为错误状态
|
|
465
|
+
if (event.toolCall) {
|
|
466
|
+
const name = toolStatus.get(event.toolCall.id)?.name || event.toolCall.name || 'unknown';
|
|
467
|
+
const errorMsg = event.toolResult?.content || 'Unknown error';
|
|
468
|
+
|
|
469
|
+
const executingCardRegex = new RegExp(
|
|
470
|
+
`<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>`,
|
|
471
|
+
'g'
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
const errorCard = `
|
|
475
|
+
<div class="tool-call-card tool-error">
|
|
476
|
+
<div class="tool-header">
|
|
477
|
+
<span class="tool-icon">❌</span>
|
|
478
|
+
<span class="tool-name">${name}</span>
|
|
479
|
+
<span class="tool-status">失败</span>
|
|
480
|
+
</div>
|
|
481
|
+
<div class="tool-output tool-error-message">
|
|
482
|
+
<pre class="tool-output-content">${this.escapeHtml(errorMsg)}</pre>
|
|
483
|
+
</div>
|
|
484
|
+
</div>`;
|
|
485
|
+
|
|
486
|
+
aiMessage.content = aiMessage.content.replace(executingCardRegex, errorCard);
|
|
487
|
+
toolStatus.delete(event.toolCall.id);
|
|
488
|
+
}
|
|
489
|
+
this.shouldScrollToBottom = true;
|
|
490
|
+
break;
|
|
491
|
+
|
|
492
|
+
case 'round_start':
|
|
493
|
+
// 新一轮开始
|
|
494
|
+
if (event.round && event.round > 1) {
|
|
495
|
+
aiMessage.content += '\n\n---\n\n';
|
|
496
|
+
}
|
|
497
|
+
break;
|
|
498
|
+
|
|
499
|
+
case 'round_end':
|
|
500
|
+
// 一轮结束
|
|
501
|
+
break;
|
|
502
|
+
|
|
503
|
+
case 'agent_complete':
|
|
504
|
+
// Agent 循环完成 - 显示终止原因
|
|
505
|
+
this.logger.info('Agent completed', {
|
|
506
|
+
reason: event.reason,
|
|
507
|
+
totalRounds: event.totalRounds
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
// 终止原因映射
|
|
511
|
+
const reasonText: Record<string, { icon: string; label: string }> = {
|
|
512
|
+
'task_complete': { icon: '✅', label: '任务完成' },
|
|
513
|
+
'no_tools': { icon: '✅', label: '已执行完成' },
|
|
514
|
+
'summarizing': { icon: '✅', label: '总结完成' },
|
|
515
|
+
'repeated_tool': { icon: '⚠️', label: '检测到重复操作' },
|
|
516
|
+
'high_failure_rate': { icon: '⚠️', label: '多次调用失败' },
|
|
517
|
+
'timeout': { icon: '⏱️', label: '执行超时' },
|
|
518
|
+
'max_rounds': { icon: '⚠️', label: '达到最大轮数' },
|
|
519
|
+
'user_cancel': { icon: '🛑', label: '用户取消' }
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
const reasonInfo = reasonText[event.reason || ''] || { icon: '📌', label: '完成' };
|
|
523
|
+
const roundsText = event.totalRounds ? ` (${event.totalRounds} 轮)` : '';
|
|
524
|
+
|
|
525
|
+
// 添加终止信息
|
|
526
|
+
aiMessage.content += `\n\n---\n**${reasonInfo.icon} ${reasonInfo.label}**${roundsText}`;
|
|
527
|
+
|
|
528
|
+
// 如果有终止消息,添加到内容中
|
|
529
|
+
if (event.terminationMessage) {
|
|
530
|
+
aiMessage.content += `\n${event.terminationMessage}`;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
this.shouldScrollToBottom = true;
|
|
534
|
+
break;
|
|
535
|
+
|
|
536
|
+
case 'error':
|
|
537
|
+
// 错误
|
|
538
|
+
aiMessage.content += `\n\n❌ 错误: ${event.error}`;
|
|
539
|
+
this.shouldScrollToBottom = true;
|
|
540
|
+
break;
|
|
541
|
+
}
|
|
542
|
+
},
|
|
543
|
+
error: (error) => {
|
|
544
|
+
this.logger.error('Agent stream error', error);
|
|
545
|
+
aiMessage.content += `\n\n❌ 错误: ${error instanceof Error ? error.message : 'Unknown error'}`;
|
|
546
|
+
this.isLoading = false;
|
|
547
|
+
this.shouldScrollToBottom = true;
|
|
548
|
+
this.saveChatHistory();
|
|
549
|
+
},
|
|
550
|
+
complete: () => {
|
|
551
|
+
this.isLoading = false;
|
|
552
|
+
this.updateTokenUsage();
|
|
553
|
+
this.saveChatHistory();
|
|
554
|
+
this.shouldScrollToBottom = true;
|
|
555
|
+
}
|
|
359
556
|
});
|
|
360
557
|
|
|
361
|
-
|
|
362
|
-
this.
|
|
558
|
+
} catch (error) {
|
|
559
|
+
this.logger.error('Failed to send message with agent', error);
|
|
560
|
+
aiMessage.content = `抱歉,我遇到了一些问题:${error instanceof Error ? error.message : 'Unknown error'}\n\n请稍后重试。`;
|
|
561
|
+
this.isLoading = false;
|
|
562
|
+
this.updateTokenUsage();
|
|
563
|
+
setTimeout(() => this.scrollToBottom(), 0);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* 处理发送消息 - 原有方法(保留兼容性)
|
|
569
|
+
*/
|
|
570
|
+
async onSendMessage(content: string): Promise<void> {
|
|
571
|
+
if (!content.trim() || this.isLoading) {
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// 添加用户消息
|
|
576
|
+
const userMessage: ChatMessage = {
|
|
577
|
+
id: this.generateId(),
|
|
578
|
+
role: MessageRole.USER,
|
|
579
|
+
content: content.trim(),
|
|
580
|
+
timestamp: new Date()
|
|
581
|
+
};
|
|
582
|
+
this.messages.push(userMessage);
|
|
583
|
+
|
|
584
|
+
// 滚动到底部
|
|
585
|
+
setTimeout(() => this.scrollToBottom(), 0);
|
|
586
|
+
|
|
587
|
+
// 显示加载状态
|
|
588
|
+
this.isLoading = true;
|
|
589
|
+
|
|
590
|
+
// 创建 AI 消息占位符用于流式更新
|
|
591
|
+
const aiMessage: ChatMessage = {
|
|
592
|
+
id: this.generateId(),
|
|
593
|
+
role: MessageRole.ASSISTANT,
|
|
594
|
+
content: '',
|
|
595
|
+
timestamp: new Date()
|
|
596
|
+
};
|
|
597
|
+
this.messages.push(aiMessage);
|
|
598
|
+
|
|
599
|
+
// 工具调用跟踪
|
|
600
|
+
const pendingTools = new Map<string, { name: string; startTime: number }>();
|
|
601
|
+
const toolResults: string[] = [];
|
|
363
602
|
|
|
364
|
-
|
|
365
|
-
|
|
603
|
+
try {
|
|
604
|
+
// 使用流式 API
|
|
605
|
+
this.aiService.chatStream({
|
|
606
|
+
messages: this.messages.slice(0, -1), // 排除刚添加的空 AI 消息
|
|
607
|
+
maxTokens: 2000,
|
|
608
|
+
temperature: 0.7
|
|
609
|
+
}).pipe(
|
|
610
|
+
takeUntil(this.destroy$)
|
|
611
|
+
).subscribe({
|
|
612
|
+
next: (event: StreamEvent) => {
|
|
613
|
+
switch (event.type) {
|
|
614
|
+
case 'text_delta':
|
|
615
|
+
// 文本流式显示
|
|
616
|
+
if (event.textDelta) {
|
|
617
|
+
aiMessage.content += event.textDelta;
|
|
618
|
+
this.shouldScrollToBottom = true;
|
|
619
|
+
}
|
|
620
|
+
break;
|
|
621
|
+
|
|
622
|
+
case 'tool_use_start':
|
|
623
|
+
// 工具开始 - 显示工具名称
|
|
624
|
+
const toolName = event.toolCall?.name || 'unknown';
|
|
625
|
+
aiMessage.content += `\n\n🔧 正在执行 ${toolName}...`;
|
|
626
|
+
if (event.toolCall?.id) {
|
|
627
|
+
pendingTools.set(event.toolCall.id, {
|
|
628
|
+
name: toolName,
|
|
629
|
+
startTime: Date.now()
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
this.shouldScrollToBottom = true;
|
|
633
|
+
break;
|
|
634
|
+
|
|
635
|
+
case 'tool_use_end':
|
|
636
|
+
// 工具完成 - 更新状态
|
|
637
|
+
if (event.toolCall) {
|
|
638
|
+
const toolInfo = pendingTools.get(event.toolCall.id);
|
|
639
|
+
const duration = toolInfo ? Date.now() - toolInfo.startTime : 0;
|
|
640
|
+
const name = toolInfo?.name || event.toolCall.name || 'unknown';
|
|
641
|
+
|
|
642
|
+
aiMessage.content = aiMessage.content.replace(
|
|
643
|
+
new RegExp(`🔧 正在执行 ${name}\\.\\.\\.`),
|
|
644
|
+
`✅ ${name} (${duration}ms)`
|
|
645
|
+
);
|
|
646
|
+
pendingTools.delete(event.toolCall.id);
|
|
647
|
+
}
|
|
648
|
+
this.shouldScrollToBottom = true;
|
|
649
|
+
break;
|
|
650
|
+
|
|
651
|
+
case 'tool_result':
|
|
652
|
+
// 工具结果 - 存储用于最后显示
|
|
653
|
+
if (event.result) {
|
|
654
|
+
const preview = event.result.content.substring(0, 500);
|
|
655
|
+
const truncated = event.result.content.length > 500 ? '\n...(已截断)' : '';
|
|
656
|
+
toolResults.push(`\n\n📋 **输出**:\n\`\`\`\n${preview}${truncated}\n\`\`\``);
|
|
657
|
+
}
|
|
658
|
+
break;
|
|
659
|
+
|
|
660
|
+
case 'tool_error':
|
|
661
|
+
// 工具错误
|
|
662
|
+
aiMessage.content = aiMessage.content.replace(
|
|
663
|
+
/🔧 正在执行 \w+\.\.\./,
|
|
664
|
+
`❌ 工具执行失败: ${event.error}`
|
|
665
|
+
);
|
|
666
|
+
this.shouldScrollToBottom = true;
|
|
667
|
+
break;
|
|
668
|
+
|
|
669
|
+
case 'message_end':
|
|
670
|
+
// 消息结束 - 附加工具结果
|
|
671
|
+
if (toolResults.length > 0) {
|
|
672
|
+
aiMessage.content += toolResults.join('');
|
|
673
|
+
}
|
|
674
|
+
this.logger.info('Stream completed');
|
|
675
|
+
break;
|
|
676
|
+
}
|
|
677
|
+
},
|
|
678
|
+
error: (error) => {
|
|
679
|
+
this.logger.error('Stream error', error);
|
|
680
|
+
aiMessage.content += `\n\n❌ 错误: ${error instanceof Error ? error.message : 'Unknown error'}`;
|
|
681
|
+
this.isLoading = false;
|
|
682
|
+
this.shouldScrollToBottom = true;
|
|
683
|
+
this.saveChatHistory();
|
|
684
|
+
},
|
|
685
|
+
complete: () => {
|
|
686
|
+
this.isLoading = false;
|
|
687
|
+
this.updateTokenUsage();
|
|
688
|
+
this.saveChatHistory();
|
|
689
|
+
this.shouldScrollToBottom = true;
|
|
690
|
+
}
|
|
691
|
+
});
|
|
366
692
|
|
|
367
693
|
} catch (error) {
|
|
368
694
|
this.logger.error('Failed to send message', error);
|
|
369
695
|
|
|
370
696
|
// 添加错误消息
|
|
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 {
|
|
697
|
+
aiMessage.content = `抱歉,我遇到了一些问题:${error instanceof Error ? error.message : 'Unknown error'}\n\n请稍后重试。`;
|
|
379
698
|
this.isLoading = false;
|
|
380
|
-
// 更新 Token 使用情况
|
|
381
699
|
this.updateTokenUsage();
|
|
382
|
-
// 滚动到底部
|
|
383
700
|
setTimeout(() => this.scrollToBottom(), 0);
|
|
384
701
|
}
|
|
385
702
|
}
|
|
@@ -600,12 +917,35 @@ export class AiSidebarComponent implements OnInit, OnDestroy, AfterViewChecked,
|
|
|
600
917
|
}
|
|
601
918
|
|
|
602
919
|
/**
|
|
603
|
-
*
|
|
920
|
+
* 格式化消息内容(支持 Markdown 渲染)
|
|
604
921
|
*/
|
|
605
922
|
formatMessage(content: string): string {
|
|
606
|
-
return
|
|
607
|
-
|
|
608
|
-
|
|
923
|
+
if (!content) return '';
|
|
924
|
+
|
|
925
|
+
try {
|
|
926
|
+
// 使用 marked 库渲染 Markdown
|
|
927
|
+
const { marked } = require('marked');
|
|
928
|
+
|
|
929
|
+
// 配置 marked 选项
|
|
930
|
+
marked.setOptions({
|
|
931
|
+
breaks: true, // 支持换行
|
|
932
|
+
gfm: true, // 支持 GitHub Flavored Markdown
|
|
933
|
+
headerIds: false, // 不生成标题 ID
|
|
934
|
+
mangle: false // 不转义邮箱
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
return marked.parse(content);
|
|
938
|
+
} catch (e) {
|
|
939
|
+
// 如果 marked 失败,使用基本格式化
|
|
940
|
+
return content
|
|
941
|
+
.replace(/&/g, '&')
|
|
942
|
+
.replace(/</g, '<')
|
|
943
|
+
.replace(/>/g, '>')
|
|
944
|
+
.replace(/\n/g, '<br>')
|
|
945
|
+
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
|
946
|
+
.replace(/\*(.*?)\*/g, '<em>$1</em>')
|
|
947
|
+
.replace(/`(.*?)`/g, '<code>$1</code>');
|
|
948
|
+
}
|
|
609
949
|
}
|
|
610
950
|
|
|
611
951
|
/**
|
|
@@ -649,7 +989,8 @@ export class AiSidebarComponent implements OnInit, OnDestroy, AfterViewChecked,
|
|
|
649
989
|
submit(): void {
|
|
650
990
|
const message = this.inputValue.trim();
|
|
651
991
|
if (message && !this.isLoading) {
|
|
652
|
-
|
|
992
|
+
// 使用 Agent 循环模式发送消息(支持多轮工具调用)
|
|
993
|
+
this.onSendMessageWithAgent(message);
|
|
653
994
|
this.inputValue = '';
|
|
654
995
|
setTimeout(() => this.autoResize(), 0);
|
|
655
996
|
this.textInput?.nativeElement.focus();
|
|
@@ -687,4 +1028,13 @@ export class AiSidebarComponent implements OnInit, OnDestroy, AfterViewChecked,
|
|
|
687
1028
|
isOverLimit(): boolean {
|
|
688
1029
|
return this.getCharCount() > this.charLimit;
|
|
689
1030
|
}
|
|
1031
|
+
|
|
1032
|
+
/**
|
|
1033
|
+
* 转义 HTML 特殊字符
|
|
1034
|
+
*/
|
|
1035
|
+
private escapeHtml(text: string): string {
|
|
1036
|
+
const div = document.createElement('div');
|
|
1037
|
+
div.textContent = text;
|
|
1038
|
+
return div.innerHTML;
|
|
1039
|
+
}
|
|
690
1040
|
}
|
|
@@ -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
|
/**
|