import { Component, OnInit, OnDestroy, ViewChild, ElementRef, AfterViewChecked, Input, OnChanges, SimpleChanges } from '@angular/core'; import { CommonModule } from '@angular/common'; import { MatCardModule } from '@angular/material/card'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { FormsModule } from '@angular/forms'; import { Subject, Subscription, of } from 'rxjs'; import { distinctUntilChanged, switchMap, takeUntil, tap, catchError } from 'rxjs/operators'; import { ConversationService } from '../services/conversation.service'; import { SessionService } from '../services/session.service'; import { EventService } from '../services/event.service'; import { IndependentEventService } from '../services/independent-event.service'; import { ChatMessage } from '../models/conversation.model'; import { Session } from '../models/session.model'; import { GlobalEvent, MessageUpdatedEvent, MessagePartUpdatedEvent, SessionUpdatedEvent } from '../models/event.model'; import { MarkdownPipe } from '../shared/pipes/markdown.pipe'; @Component({ selector: 'app-conversation', standalone: true, imports: [ CommonModule, MatCardModule, MatButtonModule, MatIconModule, MatInputModule, MatFormFieldModule, MatProgressSpinnerModule, FormsModule, MarkdownPipe ], templateUrl: './conversation.component.html', styleUrl: './conversation.component.scss', }) export class ConversationComponent implements OnInit, OnDestroy, AfterViewChecked, OnChanges { @ViewChild('messagesContainer') private messagesContainer!: ElementRef; @ViewChild('messageInput') private messageInput!: ElementRef; @Input() instanceId: string | undefined; @Input() sessionId: string | undefined; @Input() independentEventService?: IndependentEventService; messages: ChatMessage[] = []; userInput = ''; isLoading = false; activeSession: Session | null = null; private subscriptions: Subscription = new Subscription(); private currentStreamSubscription: Subscription | null = null; private sessionEventSubscription: Subscription | null = null; private backendEventSubscription: Subscription | null = null; private shouldScroll = false; private isUserScrolling = false; private scrollDebounceTimeout: any = null; private isProgrammaticScroll = false; // 是否程序触发的滚动 private isInThinking = false; // 是否正在思考过程中 private thinkingStartIndex = 0; // 思考内容的起始位置(在消息内容中的索引) // RxJS Subjects for reactive session management private destroy$ = new Subject(); private sessionChanged$ = new Subject(); private currentLoadingSession: string | null = null; private isLoadingMessages = false; // RouteReuseStrategy生命周期方法 private isDetached = false; constructor( private conversationService: ConversationService, private sessionService: SessionService, private eventService: EventService ) {} ngOnInit() { console.log('🔍 [ConversationComponent] 初始化,instanceId:', this.instanceId, 'sessionId:', this.sessionId, 'activeSession初始值:', this.activeSession?.id); console.log('🔍 [ConversationComponent] independentEventService:', this.independentEventService ? '已提供' : '未提供'); // 如果提供了sessionId,直接使用该会话(独立模式) if (this.sessionId) { console.log('🔍 [ConversationComponent] 独立模式,直接加载会话:', this.sessionId); this.activeSession = { id: this.sessionId } as Session; // 临时会话对象 console.log('🔍 [ConversationComponent] 设置activeSession:', this.activeSession.id); this.loadMessages(this.sessionId); // 设置后端事件订阅 this.setupBackendEventSubscription(); // 独立模式下不订阅全局活动会话变化 console.log('🔍 [ConversationComponent] 独立模式,跳过全局活动会话订阅'); } else { // 原有逻辑:依赖全局活动会话 console.log('🔍 [ConversationComponent] 使用全局活动会话模式'); // 立即检查当前是否有活动会话(处理组件在会话设置后初始化的情况) const currentSession = this.sessionService.getActiveSession(); if (currentSession) { console.log('🔍 [ConversationComponent] 初始化时已有活动会话:', currentSession.id); this.activeSession = currentSession; this.loadMessages(currentSession.id); this.subscribeToSessionEvents(currentSession.id); } // 订阅活动会话变化 - 使用响应式管道避免重复加载 this.subscriptions.add( this.sessionService.activeSession$.pipe( takeUntil(this.destroy$), // 避免相同会话重复触发 distinctUntilChanged((prev, curr) => { const prevId = prev?.id; const currId = curr?.id; return prevId === currId; }) ).subscribe(session => { console.log('🔍 [ConversationComponent] 活动会话变化:', session?.id); console.log('🔍 [ConversationComponent] 当前instanceId:', this.instanceId); console.log('🔍 [ConversationComponent] 当前sessionId输入:', this.sessionId); // 会话切换时取消当前正在进行的流式请求 if (this.activeSession && this.activeSession.id !== session?.id) { console.log('🔍 [ConversationComponent] 会话切换,取消当前流式请求'); this.cancelCurrentStream(); } this.activeSession = session; console.log('🔍 [ConversationComponent] 设置activeSession为:', this.activeSession?.id || 'null'); if (session) { console.log('🔍 [ConversationComponent] 加载会话消息:', session.id); this.loadMessages(session.id); // 订阅该会话的事件 this.subscribeToSessionEvents(session.id); } else { console.log('🔍 [ConversationComponent] 无活动会话,清空消息'); this.messages = []; // 取消会话事件订阅 this.unsubscribeFromSessionEvents(); } }) ); } // 设置后端事件订阅 this.setupBackendEventSubscription(); // 订阅新消息,过滤当前会话 this.subscriptions.add( this.conversationService.newMessage$.subscribe(message => { if (message && message.sessionID) { // 检查消息是否属于当前会话 const currentSessionId = this.sessionId || this.activeSession?.id; if (currentSessionId && message.sessionID === currentSessionId) { this.messages.push(message); this.shouldScroll = true; } } }) ); // 订阅流式更新,仅在当前组件处于加载状态时处理(避免跨会话干扰) this.subscriptions.add( this.conversationService.streamUpdate$.subscribe(update => { // 如果当前没有活动的流订阅,忽略(防止跨会话干扰) if (!this.currentStreamSubscription) return; console.log('🔍 [ConversationComponent] 收到流式更新:', update.type, 'data:', update.data?.substring(0, 50)); // 找到最后一个AI消息 let lastAIMessage = null; for (let i = this.messages.length - 1; i >= 0; i--) { if (this.messages[i].role === 'assistant') { lastAIMessage = this.messages[i]; break; } } if (!lastAIMessage) return; switch (update.type) { case 'thinking': // 思考过程 - 只在开始时添加标签 if (!this.isInThinking) { // 第一次思考,添加标签和换行 lastAIMessage.content += '\n[思考] ' + update.data; this.isInThinking = true; this.thinkingStartIndex = lastAIMessage.content.length - update.data.length - 4; // 减去"[思考] "的长度 } else { // 继续思考,直接追加内容 lastAIMessage.content += update.data; } this.shouldScroll = true; break; case 'tool': // 工具调用 - 添加标签(每个工具调用独立) lastAIMessage.content += '\n[工具] ' + update.data; this.shouldScroll = true; break; case 'reply': // 最终回复 - 直接追加,清除思考状态 this.isInThinking = false; lastAIMessage.content += update.data; this.shouldScroll = true; break; case 'error': // 错误信息 - 添加标签,清除思考状态 this.isInThinking = false; console.log('🔍 [ConversationComponent] 错误,设置AI消息loading为false'); lastAIMessage.content += '\n\n[错误] ' + update.data; lastAIMessage.loading = false; this.isLoading = false; this.focusInput(); break; case 'done': // 完成标记 - 停止加载,清除思考状态 this.isInThinking = false; console.log('🔍 [ConversationComponent] 设置AI消息loading为false'); lastAIMessage.loading = false; this.isLoading = false; this.focusInput(); break; } }) ); } ngOnChanges(changes: SimpleChanges) { console.log('🔍 [ConversationComponent] 输入属性变化:', changes); console.log('🔍 [ConversationComponent] 当前activeSession:', this.activeSession?.id); // 处理sessionId输入变化(独立模式) if (changes['sessionId'] && !changes['sessionId'].firstChange) { console.log('🔍 [ConversationComponent] sessionId变化,新值:', this.sessionId, '旧值:', changes['sessionId'].previousValue); // 取消当前流式请求 this.cancelCurrentStream(); // 取消会话事件订阅 this.unsubscribeFromSessionEvents(); if (this.sessionId) { // 独立模式:直接使用新的sessionId console.log('🔍 [ConversationComponent] 独立模式,重新加载会话消息:', this.sessionId); this.activeSession = { id: this.sessionId } as Session; console.log('🔍 [ConversationComponent] 设置activeSession为:', this.activeSession.id); this.loadMessages(this.sessionId); // 更新后端事件订阅 this.setupBackendEventSubscription(); } else { // 切换到全局模式 console.log('🔍 [ConversationComponent] 切换到全局模式'); this.activeSession = null; this.messages = []; console.log('🔍 [ConversationComponent] 设置activeSession为null'); } } // 处理instanceId输入变化 if (changes['instanceId'] && this.sessionId && this.instanceId) { // 更新后端事件订阅 this.setupBackendEventSubscription(); } } // 订阅后端推送的事件 private setupBackendEventSubscription() { // 清理现有的后端事件订阅 if (this.backendEventSubscription) { this.backendEventSubscription.unsubscribe(); this.backendEventSubscription = null; } // 优先使用独立事件服务 if (this.independentEventService) { console.log('🔍 [ConversationComponent] 使用独立事件服务订阅事件'); this.backendEventSubscription = this.independentEventService.events$.subscribe(event => { this.handleBackendEvent(event); }); } else { // 回退到全局事件服务 console.log('🔍 [ConversationComponent] 使用全局事件服务订阅事件'); this.backendEventSubscription = this.eventService.allEvents$.subscribe(event => { this.handleBackendEvent(event); }); } } private unsubscribeBackendEvents() { if (this.backendEventSubscription) { this.backendEventSubscription.unsubscribe(); this.backendEventSubscription = null; console.log('🔍 [ConversationComponent] 取消后端事件订阅'); } } // 处理后端推送的事件 private handleBackendEvent(event: GlobalEvent) { const payload = event.payload; console.log('🔍 [ConversationComponent] 收到后端事件:', payload.type); // 处理不同类型的后端事件 switch (payload.type) { case 'message.updated': // 处理消息更新事件 const messageEvent = payload as MessageUpdatedEvent; this.handleMessageUpdated(messageEvent); break; case 'message.part.updated': // 处理消息部分更新事件 const partEvent = payload as MessagePartUpdatedEvent; this.handleMessagePartUpdated(partEvent); break; case 'session.updated': // 处理会话更新事件 const sessionEvent = payload as SessionUpdatedEvent; this.handleSessionUpdated(sessionEvent); break; // 可以根据需要处理其他事件类型 } } // 处理消息更新事件 private handleMessageUpdated(event: MessageUpdatedEvent) { const messageInfo = event.properties.info; // 根据消息信息更新UI console.log('🔍 [ConversationComponent] 处理消息更新事件:', messageInfo.id, 'role:', messageInfo.role); // 找到对应的消息并更新 const messageIndex = this.messages.findIndex(msg => msg.id === messageInfo.id); if (messageIndex !== -1) { // 更新现有消息 if (messageInfo.content !== undefined) { this.messages[messageIndex].content = messageInfo.content; } this.shouldScroll = true; } else { // 创建新消息(处理从后端推送的新消息) const currentSessionId = this.sessionId || this.activeSession?.id; if (currentSessionId && messageInfo.role) { const newMessage: ChatMessage = { id: messageInfo.id, role: messageInfo.role as 'user' | 'assistant' | 'system', content: messageInfo.content || '', timestamp: new Date(), sessionID: currentSessionId, loading: false }; this.messages.push(newMessage); this.shouldScroll = true; console.log('🔍 [ConversationComponent] 添加新消息:', newMessage.id, 'role:', newMessage.role); } } } // 处理消息部分更新事件 private handleMessagePartUpdated(event: MessagePartUpdatedEvent) { const part = event.properties.part; const delta = event.properties.delta; console.log('🔍 [ConversationComponent] 处理消息部分更新事件:', part.type); // 这里需要根据具体业务逻辑处理消息部分更新 // 例如,如果是文本部分更新,可以更新最后一条AI消息的内容 if (part.type === 'text' && delta && this.messages.length > 0) { const lastMessage = this.messages[this.messages.length - 1]; if (lastMessage.role === 'assistant') { lastMessage.content += delta; this.shouldScroll = true; } } } // 处理会话更新事件 private handleSessionUpdated(event: SessionUpdatedEvent) { const sessionInfo = event.properties.info; console.log('🔍 [ConversationComponent] 处理会话更新事件:', sessionInfo.id); // 如果更新的是当前活动会话,更新会话标题等 if (this.activeSession && this.activeSession.id === sessionInfo.id) { // 可以更新会话信息 } } ngAfterViewChecked() { if (this.shouldScroll && !this.isUserScrolling) { this.scrollToBottom(); this.shouldScroll = false; } } ngOnDestroy() { console.log('🔍 [ConversationComponent] 组件销毁'); this.cleanupAllSubscriptions(); // 清理消息加载订阅 if (this.loadMessagesSubscription) { this.loadMessagesSubscription.unsubscribe(); this.loadMessagesSubscription = null; } } // 取消当前的流式请求 private cancelCurrentStream() { if (this.currentStreamSubscription) { console.log('🔍 [ConversationComponent] 取消当前的流式请求'); this.currentStreamSubscription.unsubscribe(); this.currentStreamSubscription = null; } // 重置加载状态 if (this.isLoading) { this.isLoading = false; } // 重置所有AI消息的loading状态(如果存在) let resetCount = 0; for (let i = this.messages.length - 1; i >= 0; i--) { const message = this.messages[i]; if (message.role === 'assistant' && message.loading) { message.loading = false; resetCount++; } } if (resetCount > 0) { console.log(`🔍 [ConversationComponent] 取消时重置${resetCount}个AI消息的loading状态`); } // 重置思考状态 this.isInThinking = false; this.thinkingStartIndex = 0; } // 安全的计时器函数,避免重复计时器错误 private safeTimeStart(label: string) { try { console.timeEnd(label); } catch(e) { // 忽略计时器不存在的错误 } console.time(label); } private safeTimeEnd(label: string) { try { console.timeEnd(label); } catch(e) { // 忽略计时器不存在的错误 } } private loadMessagesSubscription: Subscription | null = null; loadMessages(sessionId: string) { console.log('🔍 [ConversationComponent] loadMessages 被调用,sessionId:', sessionId); console.log('🔍 [ConversationComponent] 当前messages长度:', this.messages.length); console.log('🔍 [ConversationComponent] 当前activeSession:', this.activeSession?.id); console.log('🔍 [ConversationComponent] independentEventService:', this.independentEventService ? '已提供' : '未提供'); console.log('🔍 [ConversationComponent] 缓存键:', this.sessionId || this.activeSession?.id); // 检查是否已有消息(组件复用场景) // 使用当前输入参数作为缓存键,避免路由复用导致的错误缓存 const cacheKey = this.sessionId || this.activeSession?.id; if (this.messages.length > 0 && cacheKey === sessionId) { console.log('🔍 [ConversationComponent] 已有消息,会话未变化,跳过重新加载'); this.shouldScroll = true; // 独立实例页面:滚动到底部并聚焦输入框 if (this.sessionId) { // 独立模式 setTimeout(() => { this.scrollToBottom(); this.focusInput(); }, 100); } return; } // 取消之前的加载请求 if (this.loadMessagesSubscription) { console.log('🔍 [ConversationComponent] 取消之前的消息加载请求'); this.loadMessagesSubscription.unsubscribe(); this.loadMessagesSubscription = null; } // 安全地开始计时器 this.safeTimeStart(`[对话消息-加载] ${sessionId}`); console.log('📊 [对话消息] 开始加载会话消息,sessionId:', sessionId); this.messages = []; // 从服务加载历史消息 this.loadMessagesSubscription = this.conversationService.getHistory(sessionId, 50).subscribe({ next: (historyMessages) => { this.safeTimeEnd(`[对话消息-加载] ${sessionId}`); console.log('📊 [对话消息] 收到历史消息数量:', historyMessages.length); // 开始消息处理和渲染计时 this.safeTimeStart(`[对话消息-处理] ${sessionId}`); this.messages = historyMessages; // 按时间排序(最早的在前) this.messages.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime()); this.shouldScroll = true; // 独立实例页面:加载历史后滚动到底部并聚焦输入框 if (this.sessionId) { // 独立模式 setTimeout(() => { this.scrollToBottom(); this.focusInput(); console.log('🔍 [ConversationComponent] 独立实例页面:历史加载完成,滚动到底部并聚焦输入框'); }, 100); } // 使用setTimeout估算渲染完成时间 setTimeout(() => { this.safeTimeEnd(`[对话消息-处理] ${sessionId}`); console.log('📊 [对话消息] 消息处理和渲染完成,会话:', sessionId); }, 0); // 清理订阅引用 this.loadMessagesSubscription = null; }, error: (error) => { this.safeTimeEnd(`[对话消息-加载] ${sessionId}`); console.error('📊 [对话消息] 加载历史消息失败:', error); // 历史消息加载失败,但页面仍然可以正常工作 // 用户仍然可以发送新消息 this.loadMessagesSubscription = null; } }); } // 订阅会话事件 private subscribeToSessionEvents(sessionId: string) { // 取消现有订阅 this.unsubscribeFromSessionEvents(); if (!sessionId) return; // 如果有独立事件服务,则跳过会话事件订阅(独立服务已处理) if (this.independentEventService) { console.log(`🔍 [ConversationComponent] 使用独立事件服务,跳过单独会话订阅`); return; } // 使用全局事件服务订阅该会话的事件 this.sessionEventSubscription = this.eventService.subscribeToSessionEvents(sessionId) .subscribe(event => { console.log(`收到会话 ${sessionId} 的事件:`, event.payload.type); // 处理消息更新事件 if (event.payload.type === 'message.updated') { const messageEvent = event.payload as any; const messageInfo = messageEvent.properties?.info; if (messageInfo) { // 处理新消息 const newMessage: ChatMessage = { id: messageInfo.id || Date.now().toString(), role: messageInfo.role || 'assistant', content: messageInfo.content || '', timestamp: new Date(), sessionID: sessionId, // 添加sessionID字段 loading: false }; // 添加到消息列表 this.messages.push(newMessage); this.shouldScroll = true; } } // 可以添加其他事件类型的处理 }); console.log(`已订阅会话 ${sessionId} 的事件`); } // 取消会话事件订阅 private unsubscribeFromSessionEvents() { if (this.sessionEventSubscription) { this.sessionEventSubscription.unsubscribe(); this.sessionEventSubscription = null; console.log('已取消会话事件订阅'); } } onSendMessage(event: any) { if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); this.sendMessage(); } } sendMessage() { if (!this.userInput.trim() || !this.activeSession) return; // 取消之前可能仍在进行的流式请求(允许打断之前的回答) this.cancelCurrentStream(); const userMessage: ChatMessage = { id: Date.now().toString(), role: 'user', content: this.userInput, timestamp: new Date(), sessionID: this.activeSession.id }; // 添加用户消息 this.messages.push(userMessage); this.shouldScroll = true; // 用户发送新消息时重置手动滚动标志 this.isUserScrolling = false; if (this.scrollDebounceTimeout) { clearTimeout(this.scrollDebounceTimeout); this.scrollDebounceTimeout = null; } // 添加AI响应占位 const aiMessage: ChatMessage = { id: (Date.now() + 1).toString(), role: 'assistant', content: '', timestamp: new Date(), sessionID: this.activeSession.id, loading: true }; console.log('🔍 [ConversationComponent] 创建AI消息,loading=true'); this.messages.push(aiMessage); this.shouldScroll = true; // 重置思考状态 this.isInThinking = false; this.thinkingStartIndex = 0; // 发送到服务 this.isLoading = true; this.currentStreamSubscription = this.conversationService.sendMessage( this.activeSession.id, this.userInput ).subscribe({ next: () => { // 流式连接已建立,但传输仍在继续 // 消息已成功发送,立即恢复发送按钮状态,允许连续发送 this.isLoading = false; }, error: (error) => { console.error('发送消息失败:', error); aiMessage.content = '抱歉,发送消息时出现错误: ' + error.message; aiMessage.loading = false; this.isLoading = false; this.currentStreamSubscription = null; this.focusInput(); }, complete: () => { // 流式完成已在streamUpdate$中处理 this.currentStreamSubscription = null; } }); // 清空输入框 this.userInput = ''; // 聚焦输入框 this.focusInput(); } onMessagesContainerScroll() { // 如果是程序滚动,跳过用户滚动检测 if (this.isProgrammaticScroll) { this.isProgrammaticScroll = false; return; } // 用户手动滚动时触发 this.isUserScrolling = true; // 清除之前的超时 if (this.scrollDebounceTimeout) { clearTimeout(this.scrollDebounceTimeout); } // 设置超时,2秒后重置滚动标志 this.scrollDebounceTimeout = setTimeout(() => { this.isUserScrolling = false; }, 2000); } private scrollToBottom(retryCount = 0) { try { this.isProgrammaticScroll = true; const container = this.messagesContainer.nativeElement; // 强制回流,确保scrollHeight包含完整布局 container.offsetHeight; const targetScrollTop = container.scrollHeight; container.scrollTop = targetScrollTop; // 如有误差,下一帧立即重试(16ms内完成) if (retryCount < 2 && Math.abs(container.scrollTop - targetScrollTop) > 1) { requestAnimationFrame(() => this.scrollToBottom(retryCount + 1)); } } catch (err) { console.error('滚动失败:', err); } } private focusInput() { // 延迟聚焦以确保DOM已更新 setTimeout(() => { if (this.messageInput?.nativeElement) { this.messageInput.nativeElement.focus(); } }, 100); } /** * RouteReuseStrategy生命周期方法:组件被分离时调用 * 组件分离期间,SSE订阅继续接收事件,但变更检测暂停 */ pauseEventProcessing(): void { this.isDetached = true; console.log('🔍 [ConversationComponent] 组件分离,暂停变更检测,SSE订阅保持'); } /** * RouteReuseStrategy生命周期方法:组件被重新附加时调用 */ resumeEventProcessing(): void { this.isDetached = false; console.log('🔍 [ConversationComponent] 组件重新附加,恢复变更检测'); // 触发变更检测以确保UI更新 this.shouldScroll = true; } /** * RouteReuseStrategy生命周期方法:组件缓存被清理前调用 * 这里取消所有订阅,因为组件即将被销毁 */ cleanupBeforeCacheRemoval(): void { console.log('🔍 [ConversationComponent] 清理缓存前的订阅'); this.cleanupAllSubscriptions(); } /** * 清理所有订阅(仅在组件真正销毁或缓存清理时调用) */ private cleanupAllSubscriptions(): void { // 取消所有RxJS订阅 this.subscriptions.unsubscribe(); this.subscriptions = new Subscription(); // 取消当前流式请求 this.cancelCurrentStream(); // 取消会话事件订阅 this.unsubscribeFromSessionEvents(); // 取消后端事件订阅 this.unsubscribeBackendEvents(); // 清理RxJS Subjects this.destroy$.next(); this.destroy$.complete(); this.sessionChanged$.complete(); } }