Geen omschrijving
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

conversation.service.ts 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461
  1. import { Injectable } from '@angular/core';
  2. import { HttpClient } from '@angular/common/http';
  3. import { BehaviorSubject, Observable, Subject, of } from 'rxjs';
  4. import { map, catchError } from 'rxjs/operators';
  5. import { ChatMessage, StreamChunk, PromptStreamRequest, TextPart } from '../models/conversation.model';
  6. import { AuthService } from './auth.service';
  7. @Injectable({
  8. providedIn: 'root'
  9. })
  10. export class ConversationService {
  11. private newMessageSubject = new BehaviorSubject<ChatMessage | null>(null);
  12. newMessage$ = this.newMessageSubject.asObservable();
  13. private streamUpdateSubject = new Subject<StreamChunk>();
  14. streamUpdate$ = this.streamUpdateSubject.asObservable();
  15. private messageCache = new Map<string, ChatMessage[]>();
  16. constructor(
  17. private http: HttpClient,
  18. private authService: AuthService
  19. ) {}
  20. // 发送消息(流式响应)
  21. sendMessage(sessionId: string, message: string): Observable<void> {
  22. const request: PromptStreamRequest = {
  23. sessionID: sessionId,
  24. parts: [
  25. {
  26. type: 'text',
  27. text: message
  28. }
  29. ]
  30. };
  31. return new Observable<void>(observer => {
  32. // 获取认证token
  33. const token = this.authService.getToken();
  34. console.log('🔍 [ConversationService] 发送消息,sessionId:', sessionId);
  35. console.log('🔍 [ConversationService] 获取的token长度:', token?.length);
  36. console.log('🔍 [ConversationService] 当前认证状态:', this.authService.isAuthenticated());
  37. if (!token) {
  38. console.error('🔍 [ConversationService] 错误: 用户未认证,无法建立流式连接');
  39. observer.error(new Error('用户未认证,无法建立流式连接'));
  40. return;
  41. }
  42. console.log('🔍 [ConversationService] 发送POST请求启动流:', request);
  43. // 创建AbortController用于取消请求
  44. const abortController = new AbortController();
  45. // 发送超时定时器 - 控制发送消息到后端的超时(30秒)
  46. let sendTimeout: any = null;
  47. // 使用fetch API以便流式读取SSE响应
  48. fetch('/api/prompt/stream', {
  49. method: 'POST',
  50. headers: {
  51. 'Content-Type': 'application/json',
  52. 'Authorization': `Bearer ${token}`
  53. },
  54. body: JSON.stringify(request),
  55. signal: abortController.signal // 添加取消信号
  56. }).then(response => {
  57. console.log('🔍 [ConversationService] POST响应状态:', response.status, response.statusText);
  58. if (!response.ok) {
  59. throw new Error(`HTTP ${response.status}: ${response.statusText}`);
  60. }
  61. if (!response.body) {
  62. throw new Error('响应体为空');
  63. }
  64. // 检查Content-Type是否为text/event-stream
  65. const contentType = response.headers.get('content-type');
  66. console.log('🔍 [ConversationService] 响应Content-Type:', contentType);
  67. // 消息发送成功,立即通知组件可以恢复发送按钮状态
  68. observer.next();
  69. // 创建SSE解析器
  70. const reader = response.body.getReader();
  71. const decoder = new TextDecoder();
  72. let buffer = '';
  73. // 发送请求超时(30秒)- 控制发送消息到后端的超时
  74. const SEND_TIMEOUT_MS = 30 * 1000; // 30秒
  75. sendTimeout = setTimeout(() => {
  76. console.error('🔍 [ConversationService] 发送请求超时(30秒未收到初始响应)');
  77. abortController.abort();
  78. this.streamUpdateSubject.next({ type: 'error', data: '发送请求超时,请重试' });
  79. observer.error(new Error('发送请求超时,请重试'));
  80. }, SEND_TIMEOUT_MS);
  81. // 请求成功后清除发送超时
  82. clearTimeout(sendTimeout);
  83. const readStream = () => {
  84. reader.read().then(({ done, value }) => {
  85. if (done) {
  86. console.log('🔍 [ConversationService] 流结束');
  87. // 清除发送超时定时器
  88. clearTimeout(sendTimeout);
  89. this.streamUpdateSubject.next({ type: 'done', data: '' });
  90. observer.complete();
  91. return;
  92. }
  93. // 解码数据
  94. buffer += decoder.decode(value, { stream: true });
  95. // 解析SSE格式:每个事件以"data: "开头,以两个换行符结束
  96. let eventEnd = buffer.indexOf('\n\n');
  97. while (eventEnd !== -1) {
  98. const event = buffer.substring(0, eventEnd);
  99. buffer = buffer.substring(eventEnd + 2); // 移除已处理的事件和两个换行符
  100. // 收到事件,继续处理
  101. // 检查是否为注释行(以冒号开头)
  102. if (event.startsWith(':')) {
  103. console.log('🔍 [ConversationService] 收到SSE注释:', event.substring(0, 50));
  104. // 心跳注释,继续处理下一个事件
  105. eventEnd = buffer.indexOf('\n\n');
  106. continue;
  107. }
  108. // 查找数据行
  109. const dataLineStart = event.indexOf('data: ');
  110. if (dataLineStart !== -1) {
  111. const data = event.substring(dataLineStart + 6); // 移除"data: "
  112. console.log('🔍 [ConversationService] 收到SSE数据:', data.substring(0, 100));
  113. if (data === '[DONE]') {
  114. console.log('🔍 [ConversationService] 收到DONE标记');
  115. // 清除发送超时定时器
  116. clearTimeout(sendTimeout);
  117. this.streamUpdateSubject.next({ type: 'done', data: '' });
  118. observer.complete();
  119. return;
  120. } else {
  121. try {
  122. // 解析JSON格式的SSE数据
  123. const jsonData = JSON.parse(data);
  124. console.log('🔍 [ConversationService] 解析JSON数据:', jsonData);
  125. // 根据payload类型处理不同事件
  126. // 支持两种数据格式:直接 {payload: ...} 或 {directory: ..., payload: ...}
  127. const payload = jsonData.payload || jsonData;
  128. console.log('🔍 [ConversationService] payload类型:', payload.type);
  129. // 处理消息部分更新事件(包含文本内容)
  130. if (payload.type === 'message.part.updated' && payload.properties?.part) {
  131. const part = payload.properties.part;
  132. // 支持 text、reasoning 和 tool 类型
  133. if ((part.type === 'text' || part.type === 'reasoning' || part.type === 'tool') && part.text) {
  134. // 优先使用 delta 字段(增量),如果没有则使用完整文本
  135. const delta = payload.properties.delta || part.text;
  136. // 映射事件类型到前端消息类型
  137. let frontendType: 'thinking' | 'tool' | 'reply' | 'error';
  138. if (part.type === 'reasoning') {
  139. frontendType = 'thinking';
  140. } else if (part.type === 'tool') {
  141. frontendType = 'tool';
  142. } else {
  143. frontendType = 'reply'; // text 类型
  144. }
  145. console.log('🔍 [ConversationService] 收到部分内容 (类型:', part.type, '=>', frontendType, 'delta:', delta, '):', part.text.substring(0, 50));
  146. this.streamUpdateSubject.next({ type: frontendType, data: delta });
  147. }
  148. }
  149. // 处理消息更新事件(包含完整消息信息)
  150. else if (payload.type === 'message.updated' && payload.properties?.info) {
  151. const info = payload.properties.info;
  152. if (info.role === 'assistant') {
  153. console.log('🔍 [ConversationService] 收到助理消息更新,角色:', info.role, '消息ID:', info.id);
  154. // 不发送文本内容,避免与message.part.updated重复
  155. // 文本内容已通过message.part.updated事件流式传输
  156. }
  157. }
  158. // 处理其他事件类型
  159. else if (payload.type === 'session.updated') {
  160. console.log('🔍 [ConversationService] 会话更新事件');
  161. }
  162. else if (payload.type === 'server.connected') {
  163. console.log('🔍 [ConversationService] 服务器连接成功');
  164. }
  165. else if (payload.type === 'session.status') {
  166. console.log('🔍 [ConversationService] 会话状态更新:', payload.properties?.status?.type);
  167. }
  168. else if (payload.type === 'session.idle') {
  169. console.log('🔍 [ConversationService] AI进入空闲状态,保持连接可继续交互');
  170. }
  171. } catch (e) {
  172. console.error('🔍 [ConversationService] 解析SSE JSON数据失败:', e, '原始数据:', data);
  173. // 如果不是JSON,按纯文本处理
  174. this.streamUpdateSubject.next({ type: 'reply', data });
  175. }
  176. }
  177. }
  178. eventEnd = buffer.indexOf('\n\n');
  179. }
  180. // 继续读取
  181. readStream();
  182. }).catch(error => {
  183. // 检查是否是取消错误
  184. if (error.name === 'AbortError') {
  185. console.log('🔍 [ConversationService] 流式请求被取消');
  186. this.streamUpdateSubject.next({ type: 'done', data: '请求已取消' });
  187. observer.complete();
  188. } else {
  189. console.error('🔍 [ConversationService] 读取流错误:', error);
  190. this.streamUpdateSubject.next({ type: 'error', data: '流读取错误' });
  191. observer.error(error);
  192. }
  193. });
  194. };
  195. // 开始读取流
  196. readStream();
  197. }).catch(error => {
  198. // 检查是否是取消错误
  199. if (error.name === 'AbortError') {
  200. console.log('🔍 [ConversationService] 请求被取消');
  201. observer.complete();
  202. } else {
  203. console.error('🔍 [ConversationService] 启动流式请求失败:', error);
  204. this.streamUpdateSubject.next({ type: 'error', data: '连接错误' });
  205. observer.error(error);
  206. }
  207. });
  208. // 清理函数
  209. return () => {
  210. console.log('🔍 [ConversationService] 清理流式连接,取消请求');
  211. // 清除发送超时定时器
  212. clearTimeout(sendTimeout);
  213. // 取消fetch请求
  214. abortController.abort();
  215. };
  216. });
  217. }
  218. // 发送消息(非流式,同步)
  219. sendMessageSync(sessionId: string, message: string): Observable<ChatMessage> {
  220. const request: PromptStreamRequest = {
  221. sessionID: sessionId,
  222. parts: [
  223. {
  224. type: 'text',
  225. text: message
  226. }
  227. ]
  228. };
  229. // 注意:这里使用非流式端点,如果后端支持
  230. return this.http.post<any>('/api/prompt/sync', request).pipe(
  231. map(response => {
  232. console.log('🔍 [ConversationService] 同步响应:', response);
  233. if (response.success) {
  234. // 根据后端实际响应格式解析
  235. let content = '收到空响应';
  236. let messageId = Date.now().toString();
  237. if (response.data) {
  238. // 尝试从info.content获取内容
  239. if (response.data.info?.content) {
  240. content = response.data.info.content;
  241. messageId = response.data.info.id || messageId;
  242. }
  243. // 尝试从parts中提取文本内容
  244. else if (response.data.parts && Array.isArray(response.data.parts)) {
  245. const textParts = response.data.parts.filter((part: any) =>
  246. part.type === 'text' && part.text
  247. );
  248. if (textParts.length > 0) {
  249. content = textParts.map((part: any) => part.text).join('\n');
  250. }
  251. }
  252. // 如果是直接的content字段
  253. else if (response.data.content) {
  254. content = response.data.content;
  255. }
  256. }
  257. const aiMessage: ChatMessage = {
  258. id: messageId,
  259. role: 'assistant',
  260. content: content,
  261. timestamp: new Date(),
  262. sessionID: sessionId
  263. };
  264. this.newMessageSubject.next(aiMessage);
  265. return aiMessage;
  266. } else {
  267. throw new Error(response.message || '发送消息失败');
  268. }
  269. }),
  270. catchError(error => {
  271. console.error('发送消息失败:', error);
  272. throw error;
  273. })
  274. );
  275. }
  276. // 添加新消息(用于测试或手动添加)
  277. addMessage(message: ChatMessage) {
  278. this.newMessageSubject.next(message);
  279. }
  280. // 获取历史消息(调用后端API),使用缓存避免重复加载
  281. getHistory(sessionId: string, limit?: number): Observable<ChatMessage[]> {
  282. // 检查用户是否已认证
  283. if (!this.authService.isAuthenticated()) {
  284. console.warn('用户未认证,无法获取历史消息,返回空数组');
  285. return of([]);
  286. }
  287. console.log('🔍 [ConversationService] 获取历史消息,sessionId:', sessionId, 'limit:', limit);
  288. // 临时禁用缓存,强制每次加载(调试独立实例问题)
  289. console.log('🔍 [ConversationService] 临时禁用缓存,强制重新加载');
  290. this.messageCache.delete(sessionId); // 清除该会话的缓存
  291. // 直接获取并缓存
  292. return this.fetchAndCacheMessages(sessionId, limit);
  293. }
  294. // 获取并缓存消息
  295. private fetchAndCacheMessages(sessionId: string, limit?: number): Observable<ChatMessage[]> {
  296. const requestBody: any = { sessionID: sessionId };
  297. if (limit !== undefined) {
  298. requestBody.limit = limit;
  299. }
  300. return this.http.post<any>('/api/session/messages', requestBody).pipe(
  301. map(response => {
  302. console.log('🔍 [ConversationService] 获取历史消息响应:', response);
  303. if (response.success && response.data) {
  304. const messagesData = response.data.messages || [];
  305. console.log('🔍 [ConversationService] 收到历史消息数量:', messagesData.length);
  306. const messages = this.convertMessages(messagesData, sessionId);
  307. // 缓存消息
  308. this.messageCache.set(sessionId, messages);
  309. console.log('🔍 [ConversationService] 消息已缓存,数量:', messages.length);
  310. return messages;
  311. } else {
  312. console.error('🔍 [ConversationService] 获取历史消息失败:', response.message);
  313. return [];
  314. }
  315. }),
  316. catchError(error => {
  317. console.error('🔍 [ConversationService] 获取历史消息请求失败:', error);
  318. return of([]);
  319. })
  320. );
  321. }
  322. // 后台获取并更新缓存(不返回Observable)
  323. private fetchAndUpdateCache(sessionId: string, limit?: number): void {
  324. const requestBody: any = { sessionID: sessionId };
  325. if (limit !== undefined) {
  326. requestBody.limit = limit;
  327. }
  328. this.http.post<any>('/api/session/messages', requestBody).pipe(
  329. map(response => {
  330. if (response.success && response.data) {
  331. const messagesData = response.data.messages || [];
  332. const messages = this.convertMessages(messagesData, sessionId);
  333. this.messageCache.set(sessionId, messages);
  334. console.log('🔍 [ConversationService] 缓存已更新,数量:', messages.length);
  335. }
  336. return [];
  337. }),
  338. catchError(error => {
  339. console.error('🔍 [ConversationService] 更新缓存失败:', error);
  340. return of([]);
  341. })
  342. ).subscribe();
  343. }
  344. // 转换消息格式
  345. private convertMessages(messagesData: any[], sessionId: string): ChatMessage[] {
  346. return messagesData.map((msg: any, index: number) => {
  347. let content = '';
  348. let role = 'assistant';
  349. let messageId = '';
  350. let timestamp = new Date();
  351. // 提取消息ID
  352. if (msg.info?.id) {
  353. messageId = msg.info.id;
  354. } else if (msg.id) {
  355. messageId = msg.id;
  356. } else {
  357. messageId = `msg_${Date.now()}_${index}`;
  358. }
  359. // 提取角色
  360. if (msg.info?.role) {
  361. role = msg.info.role;
  362. } else if (msg.role) {
  363. role = msg.role;
  364. } else if (msg.sender_type === 'user' || msg.sender_type === 'human') {
  365. role = 'user';
  366. }
  367. // 提取时间戳
  368. if (msg.info?.time?.created) {
  369. timestamp = new Date(msg.info.time.created);
  370. } else if (msg.created_at) {
  371. timestamp = new Date(msg.created_at);
  372. } else if (msg.timestamp) {
  373. timestamp = new Date(msg.timestamp);
  374. } else {
  375. // 如果没有时间戳,使用当前时间减去索引(模拟历史顺序)
  376. timestamp = new Date(Date.now() - (messagesData.length - index) * 60000);
  377. }
  378. // 提取消息内容:从parts中查找text类型的内容
  379. const parts = msg.parts || [];
  380. if (Array.isArray(parts)) {
  381. // 从parts中提取文本内容
  382. const textParts = parts.filter((part: any) =>
  383. part.type === 'text' && part.text
  384. );
  385. if (textParts.length > 0) {
  386. content = textParts.map((part: any) => part.text).join('\n');
  387. }
  388. } else if (typeof msg.content === 'string') {
  389. content = msg.content;
  390. } else if (msg.content?.text) {
  391. content = msg.content.text;
  392. }
  393. return {
  394. id: messageId,
  395. role: role as 'user' | 'assistant',
  396. content: content || '(无内容)',
  397. timestamp: timestamp,
  398. sessionID: sessionId
  399. };
  400. });
  401. }
  402. // 清空消息流
  403. clearStream() {
  404. this.streamUpdateSubject.next({ type: 'done', data: '' });
  405. }
  406. }