Sfoglia il codice sorgente

Release v0.2.21101

qdy 2 settimane fa
commit
f80912eedb

+ 24
- 0
ng-code.log Vedi File

18
 Watch mode enabled. Watching for file changes...
18
 Watch mode enabled. Watching for file changes...
19
 NOTE: Raw file sizes do not reflect development server per-request transformations.
19
 NOTE: Raw file sizes do not reflect development server per-request transformations.
20
   ➜  Local:   http://localhost:4200/
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 Vedi File

201
   private currentStreamSubscription: Subscription | null = null;
201
   private currentStreamSubscription: Subscription | null = null;
202
   private sessionEventSubscription: Subscription | null = null;
202
   private sessionEventSubscription: Subscription | null = null;
203
   private shouldScroll = false;
203
   private shouldScroll = false;
204
+  private isInThinking = false; // 是否正在思考过程中
205
+  private thinkingStartIndex = 0; // 思考内容的起始位置(在消息内容中的索引)
204
 
206
 
205
   constructor(
207
   constructor(
206
     private conversationService: ConversationService,
208
     private conversationService: ConversationService,
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
               break;
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
               break;
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
     if (resetCount > 0) {
440
     if (resetCount > 0) {
422
       console.log(`🔍 [ConversationComponent] 取消时重置${resetCount}个AI消息的loading状态`);
441
       console.log(`🔍 [ConversationComponent] 取消时重置${resetCount}个AI消息的loading状态`);
423
     }
442
     }
443
+    // 重置思考状态
444
+    this.isInThinking = false;
445
+    this.thinkingStartIndex = 0;
424
   }
446
   }
425
 
447
 
426
   loadMessages(sessionId: string) {
448
   loadMessages(sessionId: string) {
519
     console.log('🔍 [ConversationComponent] 创建AI消息,loading=true');
541
     console.log('🔍 [ConversationComponent] 创建AI消息,loading=true');
520
     this.messages.push(aiMessage);
542
     this.messages.push(aiMessage);
521
     this.shouldScroll = true;
543
     this.shouldScroll = true;
544
+    
545
+    // 重置思考状态
546
+    this.isInThinking = false;
547
+    this.thinkingStartIndex = 0;
522
 
548
 
523
     // 发送到服务
549
     // 发送到服务
524
     this.isLoading = true;
550
     this.isLoading = true;

+ 5
- 1
src/app/models/conversation.model.ts Vedi File

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

+ 54
- 57
src/app/services/conversation.service.ts Vedi File

50
       // 创建AbortController用于取消请求
50
       // 创建AbortController用于取消请求
51
       const abortController = new AbortController();
51
       const abortController = new AbortController();
52
       
52
       
53
-      // 心跳超时定时器(5分钟)- 用于清理函数访问
54
-      let heartbeatTimeout: any = null;
53
+      // 发送超时定时器 - 控制发送消息到后端的超时(30秒)
54
+      let sendTimeout: any = null;
55
       
55
       
56
       // 使用fetch API以便流式读取SSE响应
56
       // 使用fetch API以便流式读取SSE响应
57
       fetch('/api/prompt/stream', {
57
       fetch('/api/prompt/stream', {
85
         const decoder = new TextDecoder();
85
         const decoder = new TextDecoder();
86
         let buffer = '';
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
         const readStream = () => {
100
         const readStream = () => {
110
            reader.read().then(({ done, value }) => {
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
               const event = buffer.substring(0, eventEnd);
117
               const event = buffer.substring(0, eventEnd);
130
               buffer = buffer.substring(eventEnd + 2); // 移除已处理的事件和两个换行符
118
               buffer = buffer.substring(eventEnd + 2); // 移除已处理的事件和两个换行符
131
               
119
               
132
-              // 重置心跳超时(收到任何事件,包括注释)
133
-              resetHeartbeatTimeout();
120
+              // 收到事件,继续处理
134
               
121
               
135
               // 检查是否为注释行(以冒号开头)
122
               // 检查是否为注释行(以冒号开头)
136
               if (event.startsWith(':')) {
123
               if (event.startsWith(':')) {
147
                 console.log('🔍 [ConversationService] 收到SSE数据:', data.substring(0, 100));
134
                 console.log('🔍 [ConversationService] 收到SSE数据:', data.substring(0, 100));
148
                 
135
                 
149
                 if (data === '[DONE]') {
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
                 } else {
143
                 } else {
155
                   try {
144
                   try {
156
                     // 解析JSON格式的SSE数据
145
                     // 解析JSON格式的SSE数据
165
                         // 处理消息部分更新事件(包含文本内容)
154
                         // 处理消息部分更新事件(包含文本内容)
166
                         if (payload.type === 'message.part.updated' && payload.properties?.part) {
155
                         if (payload.type === 'message.part.updated' && payload.properties?.part) {
167
                           const part = payload.properties.part;
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
                          else if (payload.type === 'message.updated' && payload.properties?.info) {
177
                          else if (payload.type === 'message.updated' && payload.properties?.info) {
197
                          }
197
                          }
198
                   } catch (e) {
198
                   } catch (e) {
199
                     console.error('🔍 [ConversationService] 解析SSE JSON数据失败:', e, '原始数据:', data);
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
         }
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 Vedi File

51
       throw new Error(`参数 menu_item_id 不能为空`);
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
       map(response => {
56
       map(response => {
59
         console.log('getSessionIdsByMenuItem响应:', response);
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
         } else {
62
         } else {
64
           console.error('获取会话ID列表失败:', response.message || response.error);
63
           console.error('获取会话ID列表失败:', response.message || response.error);
65
           throw new Error(response.message || response.error || '获取会话ID列表失败');
64
           throw new Error(response.message || response.error || '获取会话ID列表失败');

Loading…
Annulla
Salva