Aucune description
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

conversation.component.ts 28KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779
  1. import { Component, OnInit, OnDestroy, ViewChild, ElementRef, AfterViewChecked, Input, OnChanges, SimpleChanges } from '@angular/core';
  2. import { CommonModule } from '@angular/common';
  3. import { MatCardModule } from '@angular/material/card';
  4. import { MatButtonModule } from '@angular/material/button';
  5. import { MatIconModule } from '@angular/material/icon';
  6. import { MatInputModule } from '@angular/material/input';
  7. import { MatFormFieldModule } from '@angular/material/form-field';
  8. import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
  9. import { FormsModule } from '@angular/forms';
  10. import { Subject, Subscription, of } from 'rxjs';
  11. import { distinctUntilChanged, switchMap, takeUntil, tap, catchError } from 'rxjs/operators';
  12. import { ConversationService } from '../services/conversation.service';
  13. import { SessionService } from '../services/session.service';
  14. import { EventService } from '../services/event.service';
  15. import { IndependentEventService } from '../services/independent-event.service';
  16. import { ChatMessage } from '../models/conversation.model';
  17. import { Session } from '../models/session.model';
  18. import { GlobalEvent, MessageUpdatedEvent, MessagePartUpdatedEvent, SessionUpdatedEvent } from '../models/event.model';
  19. import { MarkdownPipe } from '../shared/pipes/markdown.pipe';
  20. @Component({
  21. selector: 'app-conversation',
  22. standalone: true,
  23. imports: [
  24. CommonModule,
  25. MatCardModule,
  26. MatButtonModule,
  27. MatIconModule,
  28. MatInputModule,
  29. MatFormFieldModule,
  30. MatProgressSpinnerModule,
  31. FormsModule,
  32. MarkdownPipe
  33. ],
  34. templateUrl: './conversation.component.html',
  35. styleUrl: './conversation.component.scss',
  36. })
  37. export class ConversationComponent implements OnInit, OnDestroy, AfterViewChecked, OnChanges {
  38. @ViewChild('messagesContainer') private messagesContainer!: ElementRef;
  39. @ViewChild('messageInput') private messageInput!: ElementRef;
  40. @Input() instanceId: string | undefined;
  41. @Input() sessionId: string | undefined;
  42. @Input() independentEventService?: IndependentEventService;
  43. messages: ChatMessage[] = [];
  44. userInput = '';
  45. isLoading = false;
  46. activeSession: Session | null = null;
  47. private subscriptions: Subscription = new Subscription();
  48. private currentStreamSubscription: Subscription | null = null;
  49. private sessionEventSubscription: Subscription | null = null;
  50. private backendEventSubscription: Subscription | null = null;
  51. private shouldScroll = false;
  52. private isUserScrolling = false;
  53. private scrollDebounceTimeout: any = null;
  54. private isProgrammaticScroll = false; // 是否程序触发的滚动
  55. private isInThinking = false; // 是否正在思考过程中
  56. private thinkingStartIndex = 0; // 思考内容的起始位置(在消息内容中的索引)
  57. // RxJS Subjects for reactive session management
  58. private destroy$ = new Subject<void>();
  59. private sessionChanged$ = new Subject<string>();
  60. private currentLoadingSession: string | null = null;
  61. private isLoadingMessages = false;
  62. // RouteReuseStrategy生命周期方法
  63. private isDetached = false;
  64. constructor(
  65. private conversationService: ConversationService,
  66. private sessionService: SessionService,
  67. private eventService: EventService
  68. ) {}
  69. ngOnInit() {
  70. console.log('🔍 [ConversationComponent] 初始化,instanceId:', this.instanceId, 'sessionId:', this.sessionId, 'activeSession初始值:', this.activeSession?.id);
  71. console.log('🔍 [ConversationComponent] independentEventService:', this.independentEventService ? '已提供' : '未提供');
  72. // 如果提供了sessionId,直接使用该会话(独立模式)
  73. if (this.sessionId) {
  74. console.log('🔍 [ConversationComponent] 独立模式,直接加载会话:', this.sessionId);
  75. this.activeSession = { id: this.sessionId } as Session; // 临时会话对象
  76. console.log('🔍 [ConversationComponent] 设置activeSession:', this.activeSession.id);
  77. this.loadMessages(this.sessionId);
  78. // 设置后端事件订阅
  79. this.setupBackendEventSubscription();
  80. // 独立模式下不订阅全局活动会话变化
  81. console.log('🔍 [ConversationComponent] 独立模式,跳过全局活动会话订阅');
  82. } else {
  83. // 原有逻辑:依赖全局活动会话
  84. console.log('🔍 [ConversationComponent] 使用全局活动会话模式');
  85. // 立即检查当前是否有活动会话(处理组件在会话设置后初始化的情况)
  86. const currentSession = this.sessionService.getActiveSession();
  87. if (currentSession) {
  88. console.log('🔍 [ConversationComponent] 初始化时已有活动会话:', currentSession.id);
  89. this.activeSession = currentSession;
  90. this.loadMessages(currentSession.id);
  91. this.subscribeToSessionEvents(currentSession.id);
  92. }
  93. // 订阅活动会话变化 - 使用响应式管道避免重复加载
  94. this.subscriptions.add(
  95. this.sessionService.activeSession$.pipe(
  96. takeUntil(this.destroy$),
  97. // 避免相同会话重复触发
  98. distinctUntilChanged((prev, curr) => {
  99. const prevId = prev?.id;
  100. const currId = curr?.id;
  101. return prevId === currId;
  102. })
  103. ).subscribe(session => {
  104. console.log('🔍 [ConversationComponent] 活动会话变化:', session?.id);
  105. console.log('🔍 [ConversationComponent] 当前instanceId:', this.instanceId);
  106. console.log('🔍 [ConversationComponent] 当前sessionId输入:', this.sessionId);
  107. // 会话切换时取消当前正在进行的流式请求
  108. if (this.activeSession && this.activeSession.id !== session?.id) {
  109. console.log('🔍 [ConversationComponent] 会话切换,取消当前流式请求');
  110. this.cancelCurrentStream();
  111. }
  112. this.activeSession = session;
  113. console.log('🔍 [ConversationComponent] 设置activeSession为:', this.activeSession?.id || 'null');
  114. if (session) {
  115. console.log('🔍 [ConversationComponent] 加载会话消息:', session.id);
  116. this.loadMessages(session.id);
  117. // 订阅该会话的事件
  118. this.subscribeToSessionEvents(session.id);
  119. } else {
  120. console.log('🔍 [ConversationComponent] 无活动会话,清空消息');
  121. this.messages = [];
  122. // 取消会话事件订阅
  123. this.unsubscribeFromSessionEvents();
  124. }
  125. })
  126. );
  127. }
  128. // 设置后端事件订阅
  129. this.setupBackendEventSubscription();
  130. // 订阅新消息,过滤当前会话
  131. this.subscriptions.add(
  132. this.conversationService.newMessage$.subscribe(message => {
  133. if (message && message.sessionID) {
  134. // 检查消息是否属于当前会话
  135. const currentSessionId = this.sessionId || this.activeSession?.id;
  136. if (currentSessionId && message.sessionID === currentSessionId) {
  137. this.messages.push(message);
  138. this.shouldScroll = true;
  139. }
  140. }
  141. })
  142. );
  143. // 订阅流式更新,仅在当前组件处于加载状态时处理(避免跨会话干扰)
  144. this.subscriptions.add(
  145. this.conversationService.streamUpdate$.subscribe(update => {
  146. // 如果当前没有活动的流订阅,忽略(防止跨会话干扰)
  147. if (!this.currentStreamSubscription) return;
  148. console.log('🔍 [ConversationComponent] 收到流式更新:', update.type, 'data:', update.data?.substring(0, 50));
  149. // 找到最后一个AI消息
  150. let lastAIMessage = null;
  151. for (let i = this.messages.length - 1; i >= 0; i--) {
  152. if (this.messages[i].role === 'assistant') {
  153. lastAIMessage = this.messages[i];
  154. break;
  155. }
  156. }
  157. if (!lastAIMessage) return;
  158. switch (update.type) {
  159. case 'thinking':
  160. // 思考过程 - 只在开始时添加标签
  161. if (!this.isInThinking) {
  162. // 第一次思考,添加标签和换行
  163. lastAIMessage.content += '\n[思考] ' + update.data;
  164. this.isInThinking = true;
  165. this.thinkingStartIndex = lastAIMessage.content.length - update.data.length - 4; // 减去"[思考] "的长度
  166. } else {
  167. // 继续思考,直接追加内容
  168. lastAIMessage.content += update.data;
  169. }
  170. this.shouldScroll = true;
  171. break;
  172. case 'tool':
  173. // 工具调用 - 添加标签(每个工具调用独立)
  174. lastAIMessage.content += '\n[工具] ' + update.data;
  175. this.shouldScroll = true;
  176. break;
  177. case 'reply':
  178. // 最终回复 - 直接追加,清除思考状态
  179. this.isInThinking = false;
  180. lastAIMessage.content += update.data;
  181. this.shouldScroll = true;
  182. break;
  183. case 'error':
  184. // 错误信息 - 添加标签,清除思考状态
  185. this.isInThinking = false;
  186. console.log('🔍 [ConversationComponent] 错误,设置AI消息loading为false');
  187. lastAIMessage.content += '\n\n[错误] ' + update.data;
  188. lastAIMessage.loading = false;
  189. this.isLoading = false;
  190. this.focusInput();
  191. break;
  192. case 'done':
  193. // 完成标记 - 停止加载,清除思考状态
  194. this.isInThinking = false;
  195. console.log('🔍 [ConversationComponent] 设置AI消息loading为false');
  196. lastAIMessage.loading = false;
  197. this.isLoading = false;
  198. this.focusInput();
  199. break;
  200. }
  201. })
  202. );
  203. }
  204. ngOnChanges(changes: SimpleChanges) {
  205. console.log('🔍 [ConversationComponent] 输入属性变化:', changes);
  206. console.log('🔍 [ConversationComponent] 当前activeSession:', this.activeSession?.id);
  207. // 处理sessionId输入变化(独立模式)
  208. if (changes['sessionId'] && !changes['sessionId'].firstChange) {
  209. console.log('🔍 [ConversationComponent] sessionId变化,新值:', this.sessionId, '旧值:', changes['sessionId'].previousValue);
  210. // 取消当前流式请求
  211. this.cancelCurrentStream();
  212. // 取消会话事件订阅
  213. this.unsubscribeFromSessionEvents();
  214. if (this.sessionId) {
  215. // 独立模式:直接使用新的sessionId
  216. console.log('🔍 [ConversationComponent] 独立模式,重新加载会话消息:', this.sessionId);
  217. this.activeSession = { id: this.sessionId } as Session;
  218. console.log('🔍 [ConversationComponent] 设置activeSession为:', this.activeSession.id);
  219. this.loadMessages(this.sessionId);
  220. // 更新后端事件订阅
  221. this.setupBackendEventSubscription();
  222. } else {
  223. // 切换到全局模式
  224. console.log('🔍 [ConversationComponent] 切换到全局模式');
  225. this.activeSession = null;
  226. this.messages = [];
  227. console.log('🔍 [ConversationComponent] 设置activeSession为null');
  228. }
  229. }
  230. // 处理instanceId输入变化
  231. if (changes['instanceId'] && this.sessionId && this.instanceId) {
  232. // 更新后端事件订阅
  233. this.setupBackendEventSubscription();
  234. }
  235. }
  236. // 订阅后端推送的事件
  237. private setupBackendEventSubscription() {
  238. // 清理现有的后端事件订阅
  239. if (this.backendEventSubscription) {
  240. this.backendEventSubscription.unsubscribe();
  241. this.backendEventSubscription = null;
  242. }
  243. // 优先使用独立事件服务
  244. if (this.independentEventService) {
  245. console.log('🔍 [ConversationComponent] 使用独立事件服务订阅事件');
  246. this.backendEventSubscription = this.independentEventService.events$.subscribe(event => {
  247. this.handleBackendEvent(event);
  248. });
  249. } else {
  250. // 回退到全局事件服务
  251. console.log('🔍 [ConversationComponent] 使用全局事件服务订阅事件');
  252. this.backendEventSubscription = this.eventService.allEvents$.subscribe(event => {
  253. this.handleBackendEvent(event);
  254. });
  255. }
  256. }
  257. private unsubscribeBackendEvents() {
  258. if (this.backendEventSubscription) {
  259. this.backendEventSubscription.unsubscribe();
  260. this.backendEventSubscription = null;
  261. console.log('🔍 [ConversationComponent] 取消后端事件订阅');
  262. }
  263. }
  264. // 处理后端推送的事件
  265. private handleBackendEvent(event: GlobalEvent) {
  266. const payload = event.payload;
  267. console.log('🔍 [ConversationComponent] 收到后端事件:', payload.type);
  268. // 处理不同类型的后端事件
  269. switch (payload.type) {
  270. case 'message.updated':
  271. // 处理消息更新事件
  272. const messageEvent = payload as MessageUpdatedEvent;
  273. this.handleMessageUpdated(messageEvent);
  274. break;
  275. case 'message.part.updated':
  276. // 处理消息部分更新事件
  277. const partEvent = payload as MessagePartUpdatedEvent;
  278. this.handleMessagePartUpdated(partEvent);
  279. break;
  280. case 'session.updated':
  281. // 处理会话更新事件
  282. const sessionEvent = payload as SessionUpdatedEvent;
  283. this.handleSessionUpdated(sessionEvent);
  284. break;
  285. // 可以根据需要处理其他事件类型
  286. }
  287. }
  288. // 处理消息更新事件
  289. private handleMessageUpdated(event: MessageUpdatedEvent) {
  290. const messageInfo = event.properties.info;
  291. // 根据消息信息更新UI
  292. console.log('🔍 [ConversationComponent] 处理消息更新事件:', messageInfo.id, 'role:', messageInfo.role);
  293. // 找到对应的消息并更新
  294. const messageIndex = this.messages.findIndex(msg => msg.id === messageInfo.id);
  295. if (messageIndex !== -1) {
  296. // 更新现有消息
  297. if (messageInfo.content !== undefined) {
  298. this.messages[messageIndex].content = messageInfo.content;
  299. }
  300. this.shouldScroll = true;
  301. } else {
  302. // 创建新消息(处理从后端推送的新消息)
  303. const currentSessionId = this.sessionId || this.activeSession?.id;
  304. if (currentSessionId && messageInfo.role) {
  305. const newMessage: ChatMessage = {
  306. id: messageInfo.id,
  307. role: messageInfo.role as 'user' | 'assistant' | 'system',
  308. content: messageInfo.content || '',
  309. timestamp: new Date(),
  310. sessionID: currentSessionId,
  311. loading: false
  312. };
  313. this.messages.push(newMessage);
  314. this.shouldScroll = true;
  315. console.log('🔍 [ConversationComponent] 添加新消息:', newMessage.id, 'role:', newMessage.role);
  316. }
  317. }
  318. }
  319. // 处理消息部分更新事件
  320. private handleMessagePartUpdated(event: MessagePartUpdatedEvent) {
  321. const part = event.properties.part;
  322. const delta = event.properties.delta;
  323. console.log('🔍 [ConversationComponent] 处理消息部分更新事件:', part.type);
  324. // 这里需要根据具体业务逻辑处理消息部分更新
  325. // 例如,如果是文本部分更新,可以更新最后一条AI消息的内容
  326. if (part.type === 'text' && delta && this.messages.length > 0) {
  327. const lastMessage = this.messages[this.messages.length - 1];
  328. if (lastMessage.role === 'assistant') {
  329. lastMessage.content += delta;
  330. this.shouldScroll = true;
  331. }
  332. }
  333. }
  334. // 处理会话更新事件
  335. private handleSessionUpdated(event: SessionUpdatedEvent) {
  336. const sessionInfo = event.properties.info;
  337. console.log('🔍 [ConversationComponent] 处理会话更新事件:', sessionInfo.id);
  338. // 如果更新的是当前活动会话,更新会话标题等
  339. if (this.activeSession && this.activeSession.id === sessionInfo.id) {
  340. // 可以更新会话信息
  341. }
  342. }
  343. ngAfterViewChecked() {
  344. if (this.shouldScroll && !this.isUserScrolling) {
  345. this.scrollToBottom();
  346. this.shouldScroll = false;
  347. }
  348. }
  349. ngOnDestroy() {
  350. console.log('🔍 [ConversationComponent] 组件销毁');
  351. this.cleanupAllSubscriptions();
  352. // 清理消息加载订阅
  353. if (this.loadMessagesSubscription) {
  354. this.loadMessagesSubscription.unsubscribe();
  355. this.loadMessagesSubscription = null;
  356. }
  357. }
  358. // 取消当前的流式请求
  359. private cancelCurrentStream() {
  360. if (this.currentStreamSubscription) {
  361. console.log('🔍 [ConversationComponent] 取消当前的流式请求');
  362. this.currentStreamSubscription.unsubscribe();
  363. this.currentStreamSubscription = null;
  364. }
  365. // 重置加载状态
  366. if (this.isLoading) {
  367. this.isLoading = false;
  368. }
  369. // 重置所有AI消息的loading状态(如果存在)
  370. let resetCount = 0;
  371. for (let i = this.messages.length - 1; i >= 0; i--) {
  372. const message = this.messages[i];
  373. if (message.role === 'assistant' && message.loading) {
  374. message.loading = false;
  375. resetCount++;
  376. }
  377. }
  378. if (resetCount > 0) {
  379. console.log(`🔍 [ConversationComponent] 取消时重置${resetCount}个AI消息的loading状态`);
  380. }
  381. // 重置思考状态
  382. this.isInThinking = false;
  383. this.thinkingStartIndex = 0;
  384. }
  385. // 安全的计时器函数,避免重复计时器错误
  386. private safeTimeStart(label: string) {
  387. try {
  388. console.timeEnd(label);
  389. } catch(e) {
  390. // 忽略计时器不存在的错误
  391. }
  392. console.time(label);
  393. }
  394. private safeTimeEnd(label: string) {
  395. try {
  396. console.timeEnd(label);
  397. } catch(e) {
  398. // 忽略计时器不存在的错误
  399. }
  400. }
  401. private loadMessagesSubscription: Subscription | null = null;
  402. loadMessages(sessionId: string) {
  403. console.log('🔍 [ConversationComponent] loadMessages 被调用,sessionId:', sessionId);
  404. console.log('🔍 [ConversationComponent] 当前messages长度:', this.messages.length);
  405. console.log('🔍 [ConversationComponent] 当前activeSession:', this.activeSession?.id);
  406. console.log('🔍 [ConversationComponent] independentEventService:', this.independentEventService ? '已提供' : '未提供');
  407. console.log('🔍 [ConversationComponent] 缓存键:', this.sessionId || this.activeSession?.id);
  408. // 检查是否已有消息(组件复用场景)
  409. // 使用当前输入参数作为缓存键,避免路由复用导致的错误缓存
  410. const cacheKey = this.sessionId || this.activeSession?.id;
  411. if (this.messages.length > 0 && cacheKey === sessionId) {
  412. console.log('🔍 [ConversationComponent] 已有消息,会话未变化,跳过重新加载');
  413. this.shouldScroll = true;
  414. // 独立实例页面:滚动到底部并聚焦输入框
  415. if (this.sessionId) { // 独立模式
  416. setTimeout(() => {
  417. this.scrollToBottom();
  418. this.focusInput();
  419. }, 100);
  420. }
  421. return;
  422. }
  423. // 取消之前的加载请求
  424. if (this.loadMessagesSubscription) {
  425. console.log('🔍 [ConversationComponent] 取消之前的消息加载请求');
  426. this.loadMessagesSubscription.unsubscribe();
  427. this.loadMessagesSubscription = null;
  428. }
  429. // 安全地开始计时器
  430. this.safeTimeStart(`[对话消息-加载] ${sessionId}`);
  431. console.log('📊 [对话消息] 开始加载会话消息,sessionId:', sessionId);
  432. this.messages = [];
  433. // 从服务加载历史消息
  434. this.loadMessagesSubscription = this.conversationService.getHistory(sessionId, 50).subscribe({
  435. next: (historyMessages) => {
  436. this.safeTimeEnd(`[对话消息-加载] ${sessionId}`);
  437. console.log('📊 [对话消息] 收到历史消息数量:', historyMessages.length);
  438. // 开始消息处理和渲染计时
  439. this.safeTimeStart(`[对话消息-处理] ${sessionId}`);
  440. this.messages = historyMessages;
  441. // 按时间排序(最早的在前)
  442. this.messages.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
  443. this.shouldScroll = true;
  444. // 独立实例页面:加载历史后滚动到底部并聚焦输入框
  445. if (this.sessionId) { // 独立模式
  446. setTimeout(() => {
  447. this.scrollToBottom();
  448. this.focusInput();
  449. console.log('🔍 [ConversationComponent] 独立实例页面:历史加载完成,滚动到底部并聚焦输入框');
  450. }, 100);
  451. }
  452. // 使用setTimeout估算渲染完成时间
  453. setTimeout(() => {
  454. this.safeTimeEnd(`[对话消息-处理] ${sessionId}`);
  455. console.log('📊 [对话消息] 消息处理和渲染完成,会话:', sessionId);
  456. }, 0);
  457. // 清理订阅引用
  458. this.loadMessagesSubscription = null;
  459. },
  460. error: (error) => {
  461. this.safeTimeEnd(`[对话消息-加载] ${sessionId}`);
  462. console.error('📊 [对话消息] 加载历史消息失败:', error);
  463. // 历史消息加载失败,但页面仍然可以正常工作
  464. // 用户仍然可以发送新消息
  465. this.loadMessagesSubscription = null;
  466. }
  467. });
  468. }
  469. // 订阅会话事件
  470. private subscribeToSessionEvents(sessionId: string) {
  471. // 取消现有订阅
  472. this.unsubscribeFromSessionEvents();
  473. if (!sessionId) return;
  474. // 如果有独立事件服务,则跳过会话事件订阅(独立服务已处理)
  475. if (this.independentEventService) {
  476. console.log(`🔍 [ConversationComponent] 使用独立事件服务,跳过单独会话订阅`);
  477. return;
  478. }
  479. // 使用全局事件服务订阅该会话的事件
  480. this.sessionEventSubscription = this.eventService.subscribeToSessionEvents(sessionId)
  481. .subscribe(event => {
  482. console.log(`收到会话 ${sessionId} 的事件:`, event.payload.type);
  483. // 处理消息更新事件
  484. if (event.payload.type === 'message.updated') {
  485. const messageEvent = event.payload as any;
  486. const messageInfo = messageEvent.properties?.info;
  487. if (messageInfo) {
  488. // 处理新消息
  489. const newMessage: ChatMessage = {
  490. id: messageInfo.id || Date.now().toString(),
  491. role: messageInfo.role || 'assistant',
  492. content: messageInfo.content || '',
  493. timestamp: new Date(),
  494. sessionID: sessionId, // 添加sessionID字段
  495. loading: false
  496. };
  497. // 添加到消息列表
  498. this.messages.push(newMessage);
  499. this.shouldScroll = true;
  500. }
  501. }
  502. // 可以添加其他事件类型的处理
  503. });
  504. console.log(`已订阅会话 ${sessionId} 的事件`);
  505. }
  506. // 取消会话事件订阅
  507. private unsubscribeFromSessionEvents() {
  508. if (this.sessionEventSubscription) {
  509. this.sessionEventSubscription.unsubscribe();
  510. this.sessionEventSubscription = null;
  511. console.log('已取消会话事件订阅');
  512. }
  513. }
  514. onSendMessage(event: any) {
  515. if (event.key === 'Enter' && !event.shiftKey) {
  516. event.preventDefault();
  517. this.sendMessage();
  518. }
  519. }
  520. sendMessage() {
  521. if (!this.userInput.trim() || !this.activeSession) return;
  522. // 取消之前可能仍在进行的流式请求(允许打断之前的回答)
  523. this.cancelCurrentStream();
  524. const userMessage: ChatMessage = {
  525. id: Date.now().toString(),
  526. role: 'user',
  527. content: this.userInput,
  528. timestamp: new Date(),
  529. sessionID: this.activeSession.id
  530. };
  531. // 添加用户消息
  532. this.messages.push(userMessage);
  533. this.shouldScroll = true;
  534. // 用户发送新消息时重置手动滚动标志
  535. this.isUserScrolling = false;
  536. if (this.scrollDebounceTimeout) {
  537. clearTimeout(this.scrollDebounceTimeout);
  538. this.scrollDebounceTimeout = null;
  539. }
  540. // 添加AI响应占位
  541. const aiMessage: ChatMessage = {
  542. id: (Date.now() + 1).toString(),
  543. role: 'assistant',
  544. content: '',
  545. timestamp: new Date(),
  546. sessionID: this.activeSession.id,
  547. loading: true
  548. };
  549. console.log('🔍 [ConversationComponent] 创建AI消息,loading=true');
  550. this.messages.push(aiMessage);
  551. this.shouldScroll = true;
  552. // 重置思考状态
  553. this.isInThinking = false;
  554. this.thinkingStartIndex = 0;
  555. // 发送到服务
  556. this.isLoading = true;
  557. this.currentStreamSubscription = this.conversationService.sendMessage(
  558. this.activeSession.id,
  559. this.userInput
  560. ).subscribe({
  561. next: () => {
  562. // 流式连接已建立,但传输仍在继续
  563. // 消息已成功发送,立即恢复发送按钮状态,允许连续发送
  564. this.isLoading = false;
  565. },
  566. error: (error) => {
  567. console.error('发送消息失败:', error);
  568. aiMessage.content = '抱歉,发送消息时出现错误: ' + error.message;
  569. aiMessage.loading = false;
  570. this.isLoading = false;
  571. this.currentStreamSubscription = null;
  572. this.focusInput();
  573. },
  574. complete: () => {
  575. // 流式完成已在streamUpdate$中处理
  576. this.currentStreamSubscription = null;
  577. }
  578. });
  579. // 清空输入框
  580. this.userInput = '';
  581. // 聚焦输入框
  582. this.focusInput();
  583. }
  584. onMessagesContainerScroll() {
  585. // 如果是程序滚动,跳过用户滚动检测
  586. if (this.isProgrammaticScroll) {
  587. this.isProgrammaticScroll = false;
  588. return;
  589. }
  590. // 用户手动滚动时触发
  591. this.isUserScrolling = true;
  592. // 清除之前的超时
  593. if (this.scrollDebounceTimeout) {
  594. clearTimeout(this.scrollDebounceTimeout);
  595. }
  596. // 设置超时,2秒后重置滚动标志
  597. this.scrollDebounceTimeout = setTimeout(() => {
  598. this.isUserScrolling = false;
  599. }, 2000);
  600. }
  601. private scrollToBottom(retryCount = 0) {
  602. try {
  603. this.isProgrammaticScroll = true;
  604. const container = this.messagesContainer.nativeElement;
  605. // 强制回流,确保scrollHeight包含完整布局
  606. container.offsetHeight;
  607. const targetScrollTop = container.scrollHeight;
  608. container.scrollTop = targetScrollTop;
  609. // 如有误差,下一帧立即重试(16ms内完成)
  610. if (retryCount < 2 && Math.abs(container.scrollTop - targetScrollTop) > 1) {
  611. requestAnimationFrame(() => this.scrollToBottom(retryCount + 1));
  612. }
  613. } catch (err) {
  614. console.error('滚动失败:', err);
  615. }
  616. }
  617. private focusInput() {
  618. // 延迟聚焦以确保DOM已更新
  619. setTimeout(() => {
  620. if (this.messageInput?.nativeElement) {
  621. this.messageInput.nativeElement.focus();
  622. }
  623. }, 100);
  624. }
  625. /**
  626. * RouteReuseStrategy生命周期方法:组件被分离时调用
  627. * 组件分离期间,SSE订阅继续接收事件,但变更检测暂停
  628. */
  629. pauseEventProcessing(): void {
  630. this.isDetached = true;
  631. console.log('🔍 [ConversationComponent] 组件分离,暂停变更检测,SSE订阅保持');
  632. }
  633. /**
  634. * RouteReuseStrategy生命周期方法:组件被重新附加时调用
  635. */
  636. resumeEventProcessing(): void {
  637. this.isDetached = false;
  638. console.log('🔍 [ConversationComponent] 组件重新附加,恢复变更检测');
  639. // 触发变更检测以确保UI更新
  640. this.shouldScroll = true;
  641. }
  642. /**
  643. * RouteReuseStrategy生命周期方法:组件缓存被清理前调用
  644. * 这里取消所有订阅,因为组件即将被销毁
  645. */
  646. cleanupBeforeCacheRemoval(): void {
  647. console.log('🔍 [ConversationComponent] 清理缓存前的订阅');
  648. this.cleanupAllSubscriptions();
  649. }
  650. /**
  651. * 清理所有订阅(仅在组件真正销毁或缓存清理时调用)
  652. */
  653. private cleanupAllSubscriptions(): void {
  654. // 取消所有RxJS订阅
  655. this.subscriptions.unsubscribe();
  656. this.subscriptions = new Subscription();
  657. // 取消当前流式请求
  658. this.cancelCurrentStream();
  659. // 取消会话事件订阅
  660. this.unsubscribeFromSessionEvents();
  661. // 取消后端事件订阅
  662. this.unsubscribeBackendEvents();
  663. // 清理RxJS Subjects
  664. this.destroy$.next();
  665. this.destroy$.complete();
  666. this.sessionChanged$.complete();
  667. }
  668. }