Pārlūkot izejas kodu

接收sse-增强

qdy 2 nedēļas atpakaļ
vecāks
revīzija
d753cab42e

+ 24
- 0
ng-code.log Parādīt failu

@@ -18,3 +18,27 @@ Application bundle generation complete. [4.173 seconds]
18 18
 Watch mode enabled. Watching for file changes...
19 19
 NOTE: Raw file sizes do not reflect development server per-request transformations.
20 20
   ➜  Local:   http://localhost:4200/
21
+❯ Changes detected. Rebuilding...
22
+✔ Changes detected. Rebuilding...
23
+Initial chunk files | Names |  Raw size
24
+main.js             | main  | 303.26 kB | 
25
+
26
+Application bundle generation complete. [3.544 seconds]
27
+
28
+Page reload sent to client(s).
29
+❯ Changes detected. Rebuilding...
30
+✔ Changes detected. Rebuilding...
31
+Initial chunk files | Names |  Raw size
32
+main.js             | main  | 303.23 kB | 
33
+
34
+Application bundle generation complete. [1.331 seconds]
35
+
36
+Page reload sent to client(s).
37
+❯ Changes detected. Rebuilding...
38
+✔ Changes detected. Rebuilding...
39
+Initial chunk files | Names |  Raw size
40
+main.js             | main  | 303.24 kB | 
41
+
42
+Application bundle generation complete. [1.370 seconds]
43
+
44
+Page reload sent to client(s).

+ 70
- 44
src/app/components/conversation.component.ts Parādīt failu

@@ -201,6 +201,8 @@ export class ConversationComponent implements OnInit, OnDestroy, AfterViewChecke
201 201
   private currentStreamSubscription: Subscription | null = null;
202 202
   private sessionEventSubscription: Subscription | null = null;
203 203
   private shouldScroll = false;
204
+  private isInThinking = false; // 是否正在思考过程中
205
+  private thinkingStartIndex = 0; // 思考内容的起始位置(在消息内容中的索引)
204 206
 
205 207
   constructor(
206 208
     private conversationService: ConversationService,
@@ -248,53 +250,70 @@ export class ConversationComponent implements OnInit, OnDestroy, AfterViewChecke
248 250
     );
249 251
 
250 252
     // 订阅流式更新
251
-    this.subscriptions.add(
252
-      this.conversationService.streamUpdate$.subscribe(update => {
253
-        console.log('🔍 [ConversationComponent] 收到流式更新:', update.type, 'data:', update.data?.substring(0, 50));
254
-        if (update.type === 'text') {
255
-          // 更新最后一条消息的内容
256
-          const lastMessage = this.messages[this.messages.length - 1];
257
-          if (lastMessage && lastMessage.role === 'assistant') {
258
-            console.log('🔍 [ConversationComponent] 更新AI消息内容,当前长度:', lastMessage.content.length);
259
-            lastMessage.content += update.data;
260
-            this.shouldScroll = true;
261
-          }
262
-        } else if (update.type === 'done') {
263
-          // 标记加载完成 - 找到最后一个AI消息
264
-          let lastAIMessage = null;
265
-          for (let i = this.messages.length - 1; i >= 0; i--) {
266
-            if (this.messages[i].role === 'assistant') {
267
-              lastAIMessage = this.messages[i];
253
+     this.subscriptions.add(
254
+       this.conversationService.streamUpdate$.subscribe(update => {
255
+         console.log('🔍 [ConversationComponent] 收到流式更新:', update.type, 'data:', update.data?.substring(0, 50));
256
+         
257
+         // 找到最后一个AI消息
258
+         let lastAIMessage = null;
259
+         for (let i = this.messages.length - 1; i >= 0; i--) {
260
+           if (this.messages[i].role === 'assistant') {
261
+             lastAIMessage = this.messages[i];
262
+             break;
263
+           }
264
+         }
265
+         
266
+         if (!lastAIMessage) return;
267
+         
268
+          switch (update.type) {
269
+            case 'thinking':
270
+              // 思考过程 - 只在开始时添加标签
271
+              if (!this.isInThinking) {
272
+                // 第一次思考,添加标签和换行
273
+                lastAIMessage.content += '\n[思考] ' + update.data;
274
+                this.isInThinking = true;
275
+                this.thinkingStartIndex = lastAIMessage.content.length - update.data.length - 4; // 减去"[思考] "的长度
276
+              } else {
277
+                // 继续思考,直接追加内容
278
+                lastAIMessage.content += update.data;
279
+              }
280
+              this.shouldScroll = true;
268 281
               break;
269
-            }
270
-          }
271
-          if (lastAIMessage) {
272
-            console.log('🔍 [ConversationComponent] 设置AI消息loading为false');
273
-            lastAIMessage.loading = false;
274
-          }
275
-          // 重置加载状态并聚焦输入框
276
-          this.isLoading = false;
277
-          this.focusInput();
278
-        } else if (update.type === 'error') {
279
-          // 处理错误情况 - 找到最后一个AI消息
280
-          let lastAIMessage = null;
281
-          for (let i = this.messages.length - 1; i >= 0; i--) {
282
-            if (this.messages[i].role === 'assistant') {
283
-              lastAIMessage = this.messages[i];
282
+              
283
+            case 'tool':
284
+              // 工具调用 - 添加标签(每个工具调用独立)
285
+              lastAIMessage.content += '\n[工具] ' + update.data;
286
+              this.shouldScroll = true;
287
+              break;
288
+              
289
+            case 'reply':
290
+              // 最终回复 - 直接追加,清除思考状态
291
+              this.isInThinking = false;
292
+              lastAIMessage.content += update.data;
293
+              this.shouldScroll = true;
294
+              break;
295
+              
296
+            case 'error':
297
+              // 错误信息 - 添加标签,清除思考状态
298
+              this.isInThinking = false;
299
+              console.log('🔍 [ConversationComponent] 错误,设置AI消息loading为false');
300
+              lastAIMessage.content += '\n\n[错误] ' + update.data;
301
+              lastAIMessage.loading = false;
302
+              this.isLoading = false;
303
+              this.focusInput();
304
+              break;
305
+              
306
+            case 'done':
307
+              // 完成标记 - 停止加载,清除思考状态
308
+              this.isInThinking = false;
309
+              console.log('🔍 [ConversationComponent] 设置AI消息loading为false');
310
+              lastAIMessage.loading = false;
311
+              this.isLoading = false;
312
+              this.focusInput();
284 313
               break;
285
-            }
286
-          }
287
-          if (lastAIMessage) {
288
-            console.log('🔍 [ConversationComponent] 错误,设置AI消息loading为false');
289
-            lastAIMessage.content += '\n\n[错误: ' + update.data + ']';
290
-            lastAIMessage.loading = false;
291 314
           }
292
-          // 重置加载状态并聚焦输入框
293
-          this.isLoading = false;
294
-          this.focusInput();
295
-        }
296
-      })
297
-    );
315
+       })
316
+     );
298 317
   }
299 318
 
300 319
   // 订阅后端推送的事件
@@ -421,6 +440,9 @@ export class ConversationComponent implements OnInit, OnDestroy, AfterViewChecke
421 440
     if (resetCount > 0) {
422 441
       console.log(`🔍 [ConversationComponent] 取消时重置${resetCount}个AI消息的loading状态`);
423 442
     }
443
+    // 重置思考状态
444
+    this.isInThinking = false;
445
+    this.thinkingStartIndex = 0;
424 446
   }
425 447
 
426 448
   loadMessages(sessionId: string) {
@@ -519,6 +541,10 @@ export class ConversationComponent implements OnInit, OnDestroy, AfterViewChecke
519 541
     console.log('🔍 [ConversationComponent] 创建AI消息,loading=true');
520 542
     this.messages.push(aiMessage);
521 543
     this.shouldScroll = true;
544
+    
545
+    // 重置思考状态
546
+    this.isInThinking = false;
547
+    this.thinkingStartIndex = 0;
522 548
 
523 549
     // 发送到服务
524 550
     this.isLoading = true;

+ 5
- 1
src/app/models/conversation.model.ts Parādīt failu

@@ -37,10 +37,14 @@ export interface TokenInfo {
37 37
   output: number;
38 38
 }
39 39
 
40
+// 消息类型
41
+export type MessageType = 'thinking' | 'tool' | 'reply' | 'error';
42
+
40 43
 // 对话消息(前端显示用)
41 44
 export interface ChatMessage {
42 45
   id: string;
43 46
   role: 'user' | 'assistant' | 'system';
47
+  type?: MessageType; // 消息类型(思考、工具、回复、错误)
44 48
   content: string;
45 49
   timestamp: Date;
46 50
   sessionID: string;
@@ -49,6 +53,6 @@ export interface ChatMessage {
49 53
 
50 54
 // 流式响应块
51 55
 export interface StreamChunk {
52
-  type: 'text' | 'error' | 'done';
56
+  type: MessageType | 'done';
53 57
   data: string;
54 58
 }

+ 54
- 57
src/app/services/conversation.service.ts Parādīt failu

@@ -50,8 +50,8 @@ export class ConversationService {
50 50
       // 创建AbortController用于取消请求
51 51
       const abortController = new AbortController();
52 52
       
53
-      // 心跳超时定时器(5分钟)- 用于清理函数访问
54
-      let heartbeatTimeout: any = null;
53
+      // 发送超时定时器 - 控制发送消息到后端的超时(30秒)
54
+      let sendTimeout: any = null;
55 55
       
56 56
       // 使用fetch API以便流式读取SSE响应
57 57
       fetch('/api/prompt/stream', {
@@ -85,39 +85,27 @@ export class ConversationService {
85 85
         const decoder = new TextDecoder();
86 86
         let buffer = '';
87 87
         
88
-        // 心跳超时定时器(5分钟)- 如果5分钟内未收到任何数据(包括心跳),则认为连接已断开
89
-        // 使用外层声明的heartbeatTimeout变量
90
-        const HEARTBEAT_TIMEOUT_MS = 5 * 60 * 1000; // 5分钟
88
+        // 发送请求超时(30秒)- 控制发送消息到后端的超时
89
+        const SEND_TIMEOUT_MS = 30 * 1000; // 30秒
90
+        sendTimeout = setTimeout(() => {
91
+          console.error('🔍 [ConversationService] 发送请求超时(30秒未收到初始响应)');
92
+          abortController.abort();
93
+          this.streamUpdateSubject.next({ type: 'error', data: '发送请求超时,请重试' });
94
+          observer.error(new Error('发送请求超时,请重试'));
95
+        }, SEND_TIMEOUT_MS);
91 96
         
92
-        const resetHeartbeatTimeout = () => {
93
-          // 清除现有定时器
94
-          if (heartbeatTimeout) {
95
-            clearTimeout(heartbeatTimeout);
96
-          }
97
-          // 设置新的超时定时器
98
-          heartbeatTimeout = setTimeout(() => {
99
-            console.error('🔍 [ConversationService] 心跳超时(5分钟未收到数据),主动断开连接');
100
-            abortController.abort();
101
-            this.streamUpdateSubject.next({ type: 'error', data: '连接超时,请重试' });
102
-            observer.error(new Error('连接超时,请重试'));
103
-          }, HEARTBEAT_TIMEOUT_MS);
104
-        };
105
-        
106
-        // 初始设置心跳超时定时器
107
-        resetHeartbeatTimeout();
97
+        // 请求成功后清除发送超时
98
+        clearTimeout(sendTimeout);
108 99
         
109 100
         const readStream = () => {
110 101
            reader.read().then(({ done, value }) => {
111
-             if (done) {
112
-               console.log('🔍 [ConversationService] 流结束');
113
-               // 清除心跳超时定时器
114
-               if (heartbeatTimeout) {
115
-                 clearTimeout(heartbeatTimeout);
116
-                 heartbeatTimeout = null;
117
-               }
118
-                this.streamUpdateSubject.next({ type: 'done', data: '' });
119
-                observer.complete();
120
-               return;
102
+              if (done) {
103
+                console.log('🔍 [ConversationService] 流结束');
104
+                     // 清除发送超时定时器
105
+                     clearTimeout(sendTimeout);
106
+                 this.streamUpdateSubject.next({ type: 'done', data: '' });
107
+                 observer.complete();
108
+                return;
121 109
              }
122 110
             
123 111
             // 解码数据
@@ -129,8 +117,7 @@ export class ConversationService {
129 117
               const event = buffer.substring(0, eventEnd);
130 118
               buffer = buffer.substring(eventEnd + 2); // 移除已处理的事件和两个换行符
131 119
               
132
-              // 重置心跳超时(收到任何事件,包括注释)
133
-              resetHeartbeatTimeout();
120
+              // 收到事件,继续处理
134 121
               
135 122
               // 检查是否为注释行(以冒号开头)
136 123
               if (event.startsWith(':')) {
@@ -147,10 +134,12 @@ export class ConversationService {
147 134
                 console.log('🔍 [ConversationService] 收到SSE数据:', data.substring(0, 100));
148 135
                 
149 136
                 if (data === '[DONE]') {
150
-                   console.log('🔍 [ConversationService] 收到DONE标记');
151
-                   this.streamUpdateSubject.next({ type: 'done', data: '' });
152
-                   observer.complete();
153
-                  return;
137
+                    console.log('🔍 [ConversationService] 收到DONE标记');
138
+                     // 清除发送超时定时器
139
+                     clearTimeout(sendTimeout);
140
+                    this.streamUpdateSubject.next({ type: 'done', data: '' });
141
+                    observer.complete();
142
+                   return;
154 143
                 } else {
155 144
                   try {
156 145
                     // 解析JSON格式的SSE数据
@@ -165,13 +154,24 @@ export class ConversationService {
165 154
                         // 处理消息部分更新事件(包含文本内容)
166 155
                         if (payload.type === 'message.part.updated' && payload.properties?.part) {
167 156
                           const part = payload.properties.part;
168
-                          // 支持 text 和 reasoning 类型
169
-                          if ((part.type === 'text' || part.type === 'reasoning') && part.text) {
170
-                            // 优先使用 delta 字段(增量),如果没有则使用完整文本
171
-                            const delta = payload.properties.delta || part.text;
172
-                            console.log('🔍 [ConversationService] 收到部分内容 (类型:', part.type, 'delta:', delta, '):', part.text.substring(0, 50));
173
-                            this.streamUpdateSubject.next({ type: 'text', data: delta });
174
-                          }
157
+                           // 支持 text、reasoning 和 tool 类型
158
+                           if ((part.type === 'text' || part.type === 'reasoning' || part.type === 'tool') && part.text) {
159
+                             // 优先使用 delta 字段(增量),如果没有则使用完整文本
160
+                             const delta = payload.properties.delta || part.text;
161
+                             
162
+                             // 映射事件类型到前端消息类型
163
+                             let frontendType: 'thinking' | 'tool' | 'reply' | 'error';
164
+                             if (part.type === 'reasoning') {
165
+                               frontendType = 'thinking';
166
+                             } else if (part.type === 'tool') {
167
+                               frontendType = 'tool';
168
+                             } else {
169
+                               frontendType = 'reply'; // text 类型
170
+                             }
171
+                             
172
+                             console.log('🔍 [ConversationService] 收到部分内容 (类型:', part.type, '=>', frontendType, 'delta:', delta, '):', part.text.substring(0, 50));
173
+                             this.streamUpdateSubject.next({ type: frontendType, data: delta });
174
+                           }
175 175
                         }
176 176
                          // 处理消息更新事件(包含完整消息信息)
177 177
                          else if (payload.type === 'message.updated' && payload.properties?.info) {
@@ -197,8 +197,8 @@ export class ConversationService {
197 197
                          }
198 198
                   } catch (e) {
199 199
                     console.error('🔍 [ConversationService] 解析SSE JSON数据失败:', e, '原始数据:', data);
200
-                    // 如果不是JSON,按纯文本处理
201
-                    this.streamUpdateSubject.next({ type: 'text', data });
200
+                     // 如果不是JSON,按纯文本处理
201
+                     this.streamUpdateSubject.next({ type: 'reply', data });
202 202
                   }
203 203
                 }
204 204
               }
@@ -236,17 +236,14 @@ export class ConversationService {
236 236
         }
237 237
       });
238 238
       
239
-      // 清理函数
240
-      return () => {
241
-        console.log('🔍 [ConversationService] 清理流式连接,取消请求');
242
-        // 清除心跳超时定时器
243
-        if (heartbeatTimeout) {
244
-          clearTimeout(heartbeatTimeout);
245
-          heartbeatTimeout = null;
246
-        }
247
-        // 取消fetch请求
248
-        abortController.abort();
249
-      };
239
+       // 清理函数
240
+       return () => {
241
+         console.log('🔍 [ConversationService] 清理流式连接,取消请求');
242
+         // 清除发送超时定时器
243
+         clearTimeout(sendTimeout);
244
+         // 取消fetch请求
245
+         abortController.abort();
246
+       };
250 247
     });
251 248
   }
252 249
 

+ 6
- 7
src/app/services/menu.service.ts Parādīt failu

@@ -51,15 +51,14 @@ export class MenuService {
51 51
       throw new Error(`参数 menu_item_id 不能为空`);
52 52
     }
53 53
     
54
-    console.log('发送请求到 /api/menu/sessions,参数 menu_item_id:', menuItemId);
55
-    return this.http.get<SessionIdsResponse>('/api/menu/sessions', {
56
-      params: { menu_item_id: menuItemId }
57
-    }).pipe(
54
+    console.log(`发送请求到 /api/menu/sessions/${menuItemId}`);
55
+    return this.http.get<SessionIdsResponse>(`/api/menu/sessions/${menuItemId}`).pipe(
58 56
       map(response => {
59 57
         console.log('getSessionIdsByMenuItem响应:', response);
60
-        if (response.success && response.data) {
61
-          console.log('返回会话ID列表:', response.data);
62
-          return response.data;
58
+        if (response.success) {
59
+          const data = response.data || [];
60
+          console.log('返回会话ID列表:', data);
61
+          return data;
63 62
         } else {
64 63
           console.error('获取会话ID列表失败:', response.message || response.error);
65 64
           throw new Error(response.message || response.error || '获取会话ID列表失败');

Notiek ielāde…
Atcelt
Saglabāt