Procházet zdrojové kódy

修改SSE输出-增加获取历史对话api,简化消息推送

qdy před 2 týdny
rodič
revize
1db6ee5f13

+ 7
- 0
internal/opencode/client.go Zobrazit soubor

@@ -11,6 +11,7 @@ type OpenCodeClient interface {
11 11
 	SendPromptStream(ctx context.Context, sessionID string, prompt *PromptRequest) (<-chan string, error)
12 12
 	GetSession(ctx context.Context, sessionID string) (*Session, error)
13 13
 	ListSessions(ctx context.Context) ([]Session, error)
14
+	GetSessionMessages(ctx context.Context, sessionID string, limit int) ([]SessionMessage, error)
14 15
 	GetBaseURL() string
15 16
 	GetPort() int
16 17
 }
@@ -72,3 +73,9 @@ type TokenInfo struct {
72 73
 	Input  int `json:"input"`
73 74
 	Output int `json:"output"`
74 75
 }
76
+
77
+// SessionMessage 会话消息(包含info和parts)
78
+type SessionMessage struct {
79
+	Info  map[string]interface{}   `json:"info"`
80
+	Parts []map[string]interface{} `json:"parts"`
81
+}

+ 44
- 0
internal/opencode/direct_client.go Zobrazit soubor

@@ -379,6 +379,50 @@ func (c *DirectClient) GetPort() int {
379 379
 	return c.port
380 380
 }
381 381
 
382
+// GetSessionMessages 获取会话消息历史
383
+func (c *DirectClient) GetSessionMessages(ctx context.Context, sessionID string, limit int) ([]SessionMessage, error) {
384
+	// 构造URL
385
+	url := fmt.Sprintf("%s/session/%s/message", c.baseURL, sessionID)
386
+
387
+	// 添加查询参数
388
+	if limit > 0 {
389
+		url = fmt.Sprintf("%s?limit=%d", url, limit)
390
+	}
391
+
392
+	// 发送HTTP请求
393
+	req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
394
+	if err != nil {
395
+		return nil, fmt.Errorf("创建请求失败: %w", err)
396
+	}
397
+	req.Header.Set("Accept", "application/json")
398
+
399
+	resp, err := c.httpClient.Do(req)
400
+	if err != nil {
401
+		return nil, fmt.Errorf("获取会话消息失败: %w", err)
402
+	}
403
+	defer resp.Body.Close()
404
+
405
+	if resp.StatusCode != http.StatusOK {
406
+		body, _ := io.ReadAll(resp.Body)
407
+		return nil, fmt.Errorf("获取会话消息失败,状态码: %d, 响应体: %s", resp.StatusCode, string(body))
408
+	}
409
+
410
+	// 解析响应
411
+	body, err := io.ReadAll(resp.Body)
412
+	if err != nil {
413
+		return nil, fmt.Errorf("读取响应体失败: %w", err)
414
+	}
415
+
416
+	// 解析JSON响应
417
+	var messages []SessionMessage
418
+	if err := json.Unmarshal(body, &messages); err != nil {
419
+		return nil, fmt.Errorf("解析消息响应失败: %w", err)
420
+	}
421
+
422
+	fmt.Fprintf(os.Stderr, "[opencode-direct-client] 获取会话消息成功: sessionID=%s, count=%d\n", sessionID, len(messages))
423
+	return messages, nil
424
+}
425
+
382 426
 // writeStreamLog 将流式数据写入日志文件用于分析
383 427
 func writeStreamLog(sessionID string, data string) {
384 428
 	// 创建日志目录

+ 89
- 0
internal/opencode/mock_client.go Zobrazit soubor

@@ -180,6 +180,95 @@ func (m *MockClient) GetBaseURL() string {
180 180
 	return m.baseURL
181 181
 }
182 182
 
183
+// GetSessionMessages 模拟获取会话消息
184
+func (m *MockClient) GetSessionMessages(ctx context.Context, sessionID string, limit int) ([]SessionMessage, error) {
185
+	m.mu.RLock()
186
+	defer m.mu.RUnlock()
187
+
188
+	if m.shouldFail {
189
+		return nil, fmt.Errorf("模拟错误: %s", m.failMessage)
190
+	}
191
+
192
+	if _, exists := m.sessions[sessionID]; !exists {
193
+		return nil, fmt.Errorf("会话不存在: %s", sessionID)
194
+	}
195
+
196
+	// 创建模拟消息
197
+	messages := []SessionMessage{
198
+		{
199
+			Info: map[string]interface{}{
200
+				"id":        "msg-mock-001",
201
+				"sessionID": sessionID,
202
+				"role":      "user",
203
+				"time": map[string]interface{}{
204
+					"created": float64(1770797040043),
205
+				},
206
+				"agent": "code-sql",
207
+				"model": map[string]interface{}{
208
+					"providerID": "deepseek",
209
+					"modelID":    "deepseek-reasoner",
210
+				},
211
+			},
212
+			Parts: []map[string]interface{}{
213
+				{
214
+					"id":   "prt-mock-001",
215
+					"type": "text",
216
+					"text": "编写查询昨天销售总额的sql代码。简单,用户没有提到的,保持最简单的方式返回内容",
217
+				},
218
+			},
219
+		},
220
+		{
221
+			Info: map[string]interface{}{
222
+				"id":        "msg-mock-002",
223
+				"sessionID": sessionID,
224
+				"role":      "assistant",
225
+				"time": map[string]interface{}{
226
+					"created":   float64(1770797040072),
227
+					"completed": float64(1770797045880),
228
+				},
229
+				"parentID":   "msg-mock-001",
230
+				"modelID":    "deepseek-reasoner",
231
+				"providerID": "deepseek",
232
+				"mode":       "code-sql",
233
+				"agent":      "code-sql",
234
+				"path": map[string]interface{}{
235
+					"cwd":  "/Users/kenqdy/Documents/v-bdx-workspace",
236
+					"root": "/",
237
+				},
238
+				"cost": 0.000514752,
239
+				"tokens": map[string]interface{}{
240
+					"input":     55,
241
+					"output":    114,
242
+					"reasoning": 68,
243
+					"cache": map[string]interface{}{
244
+						"read":  15104,
245
+						"write": 0,
246
+					},
247
+				},
248
+				"finish": "tool-calls",
249
+			},
250
+			Parts: []map[string]interface{}{
251
+				{
252
+					"id":   "prt-mock-002",
253
+					"type": "step-start",
254
+				},
255
+				{
256
+					"id":   "prt-mock-003",
257
+					"type": "reasoning",
258
+					"text": "用户想要查询昨天销售总额的SQL代码...",
259
+				},
260
+			},
261
+		},
262
+	}
263
+
264
+	// 如果有限制,截取消息
265
+	if limit > 0 && limit < len(messages) {
266
+		messages = messages[:limit]
267
+	}
268
+
269
+	return messages, nil
270
+}
271
+
183 272
 // GetPort 获取端口
184 273
 func (m *MockClient) GetPort() int {
185 274
 	return m.port

+ 14
- 42
internal/routes/prompt_stream_routes.go Zobrazit soubor

@@ -9,7 +9,6 @@ import (
9 9
 	"time"
10 10
 
11 11
 	"git.x2erp.com/qdy/go-base/authbase"
12
-	goctx "git.x2erp.com/qdy/go-base/ctx"
13 12
 	"git.x2erp.com/qdy/go-base/logger"
14 13
 	"git.x2erp.com/qdy/go-base/webx"
15 14
 	"git.x2erp.com/qdy/go-base/webx/router"
@@ -75,9 +74,8 @@ func StreamPromptHandler(client opencode.OpenCodeClient) http.HandlerFunc {
75 74
 		ctx, cancel := context.WithTimeout(r.Context(), 15*time.Minute)
76 75
 		defer cancel()
77 76
 
78
-		// 获取事件分发器实例和订阅服务
77
+		// 获取事件分发器实例
79 78
 		dispatcher := event.GetEventDispatcher(client.GetBaseURL(), client.GetPort())
80
-		subscriptionService := event.GetSubscriptionService()
81 79
 
82 80
 		// 从认证上下文中获取用户ID(用于缓存,按sessionID分发事件)
83 81
 		userID := "unknown-user"
@@ -99,40 +97,14 @@ func StreamPromptHandler(client opencode.OpenCodeClient) http.HandlerFunc {
99 97
 
100 98
 		logger.Debug(fmt.Sprintf("🔍 [StreamPromptHandler] 用户ID: %s, 用户名: %s, 会话ID: %s", userID, username, req.SessionID))
101 99
 
102
-		// 构建请求上下文
103
-		reqCtx := &goctx.RequestContext{
104
-			UserID:   userID,
105
-			Username: username,
106
-			// 可以添加更多字段:TraceID、TenantID等
107
-			TraceID: fmt.Sprintf("stream-%d", time.Now().UnixNano()),
108
-		}
109
-
110
-		// 使用订阅服务注册会话和订阅事件(如果可用)
111
-		var ch <-chan string
112
-		var err error
113
-
114
-		if subscriptionService != nil {
115
-			// 使用订阅服务(集成MongoDB和上下文)
116
-			subscriptionService.RegisterSessionWithContext(req.SessionID, reqCtx)
117
-			ch, err = subscriptionService.SubscribeWithContext(req.SessionID, reqCtx)
118
-			if err != nil {
119
-				logger.Error(fmt.Sprintf("🔍 [StreamPromptHandler] 订阅服务订阅失败: %v", err))
120
-				http.Error(w, fmt.Sprintf("订阅失败: %v", err), http.StatusInternalServerError)
121
-				return
122
-			}
123
-			defer subscriptionService.UnsubscribeWithContext(req.SessionID, ch, reqCtx)
124
-		} else {
125
-			// 回退到直接使用事件分发器
126
-			logger.Warn("⚠️ [StreamPromptHandler] 订阅服务不可用,使用事件分发器回退")
127
-			dispatcher.RegisterSession(req.SessionID, userID)
128
-			ch, err = dispatcher.Subscribe(req.SessionID, userID)
129
-			if err != nil {
130
-				logger.Error(fmt.Sprintf("🔍 [StreamPromptHandler] 事件分发器订阅失败: %v", err))
131
-				http.Error(w, fmt.Sprintf("订阅失败: %v", err), http.StatusInternalServerError)
132
-				return
133
-			}
134
-			defer dispatcher.Unsubscribe(req.SessionID, ch)
100
+		// 使用事件分发器订阅会话事件
101
+		ch, err := dispatcher.Subscribe(req.SessionID, userID)
102
+		if err != nil {
103
+			logger.Error(fmt.Sprintf("🔍 [StreamPromptHandler] 事件分发器订阅失败: %v", err))
104
+			http.Error(w, fmt.Sprintf("订阅失败: %v", err), http.StatusInternalServerError)
105
+			return
135 106
 		}
107
+		defer dispatcher.Unsubscribe(req.SessionID, ch)
136 108
 
137 109
 		// 发送异步请求到 opencode(触发AI处理)
138 110
 		logger.Debug(fmt.Sprintf("🔍 [StreamPromptHandler] 发送异步请求到 opencode, sessionID=%s", req.SessionID))
@@ -143,7 +115,7 @@ func StreamPromptHandler(client opencode.OpenCodeClient) http.HandlerFunc {
143 115
 			http.Error(w, fmt.Sprintf("发送请求失败: %v", err), http.StatusInternalServerError)
144 116
 			return
145 117
 		}
146
-		logger.Debug(fmt.Sprintf("🔍 [StreamPromptHandler] 异步请求发送成功,等待事件流"))
118
+		logger.Debug("🔍 [StreamPromptHandler] 异步请求发送成功,等待事件流")
147 119
 
148 120
 		// 发送流式响应
149 121
 		flusher, ok := w.(http.Flusher)
@@ -152,7 +124,7 @@ func StreamPromptHandler(client opencode.OpenCodeClient) http.HandlerFunc {
152 124
 			return
153 125
 		}
154 126
 
155
-		logger.Debug(fmt.Sprintf("🔍 [StreamPromptHandler] 开始发送流式响应"))
127
+		logger.Debug("🔍 [StreamPromptHandler] 开始发送流式响应")
156 128
 		eventCount := 0
157 129
 
158 130
 		// 创建心跳定时器,每30秒发送一次心跳保活(SSE注释格式)
@@ -181,7 +153,7 @@ func StreamPromptHandler(client opencode.OpenCodeClient) http.HandlerFunc {
181 153
 					if len(preview) > 100 {
182 154
 						preview = preview[:100] + "..."
183 155
 					}
184
-					logger.Debug(fmt.Sprintf("🔍 [StreamPromptHandler] 发送SSE数据[%d]: %s", eventCount, preview))
156
+					//logger.Debug(fmt.Sprintf("🔍 [StreamPromptHandler] 发送SSE数据[%d]: %s", eventCount, preview))
185 157
 				}
186 158
 
187 159
 				// 发送 SSE 数据,opencode 数据已包含 payload 字段,不需要额外包装
@@ -221,15 +193,15 @@ func StreamPromptHandler(client opencode.OpenCodeClient) http.HandlerFunc {
221 193
 				fmt.Fprintf(w, "data: %s\n\n", wrappedData)
222 194
 				flusher.Flush()
223 195
 			case <-ctx.Done():
224
-				logger.Info(fmt.Sprintf("🔍 [StreamPromptHandler] 上下文超时"))
196
+				logger.Debug("🔍 [StreamPromptHandler] 上下文超时")
225 197
 				return
226 198
 			case <-heartbeatTicker.C:
227 199
 				// 发送心跳保活(SSE注释格式)
228
-				logger.Debug(fmt.Sprintf("🔍 [StreamPromptHandler] 发送心跳保活"))
200
+				logger.Debug("🔍 [StreamPromptHandler] 发送心跳保活")
229 201
 				fmt.Fprintf(w, ": heartbeat\n\n")
230 202
 				flusher.Flush()
231 203
 			case <-r.Context().Done():
232
-				logger.Info(fmt.Sprintf("🔍 [StreamPromptHandler] 客户端断开连接"))
204
+				logger.Debug("🔍 [StreamPromptHandler] 客户端断开连接")
233 205
 				return
234 206
 			}
235 207
 		}

+ 57
- 0
internal/routes/session_messages_routes.go Zobrazit soubor

@@ -0,0 +1,57 @@
1
+package routes
2
+
3
+import (
4
+	"context"
5
+
6
+	"git.x2erp.com/qdy/go-base/authbase"
7
+	"git.x2erp.com/qdy/go-base/ctx"
8
+	"git.x2erp.com/qdy/go-base/logger"
9
+	"git.x2erp.com/qdy/go-base/model/response"
10
+	"git.x2erp.com/qdy/go-base/webx/router"
11
+	"git.x2erp.com/qdy/go-svc-code/internal/opencode"
12
+)
13
+
14
+// GetSessionMessagesRequest 获取会话消息请求
15
+type GetSessionMessagesRequest struct {
16
+	SessionID string `form:"sessionID" binding:"required"`
17
+	Limit     int    `form:"limit,omitempty"`
18
+}
19
+
20
+// SessionMessagesResponse 会话消息响应
21
+type SessionMessagesResponse struct {
22
+	Messages []opencode.SessionMessage `json:"messages"`
23
+	Count    int                       `json:"count"`
24
+}
25
+
26
+// RegisterSessionMessagesRoutes 注册会话消息路由
27
+func RegisterSessionMessagesRoutes(ws *router.RouterService, client opencode.OpenCodeClient) {
28
+	// 获取会话消息历史(需要Token认证)
29
+	ws.GET("/api/session/messages",
30
+		func(req *GetSessionMessagesRequest, ctx context.Context, reqCtx *ctx.RequestContext) (*response.QueryResult[SessionMessagesResponse], error) {
31
+			if req.SessionID == "" {
32
+				return &response.QueryResult[SessionMessagesResponse]{
33
+					Success: false,
34
+					Message: "参数 sessionID 不能为空",
35
+				}, nil
36
+			}
37
+
38
+			// 调用 opencode 客户端获取消息
39
+			messages, err := client.GetSessionMessages(ctx, req.SessionID, req.Limit)
40
+			if err != nil {
41
+				logger.Error("获取会话消息失败", "session_id", req.SessionID, "error", err)
42
+				return &response.QueryResult[SessionMessagesResponse]{
43
+					Success: false,
44
+					Message: "获取会话消息失败: " + err.Error(),
45
+				}, nil
46
+			}
47
+
48
+			return &response.QueryResult[SessionMessagesResponse]{
49
+				Success: true,
50
+				Data: SessionMessagesResponse{
51
+					Messages: messages,
52
+					Count:    len(messages),
53
+				},
54
+			}, nil
55
+		},
56
+	).Use(authbase.TokenAuth).Desc("获取指定会话的消息历史").Register()
57
+}

+ 0
- 118
internal/service/event/cache.go Zobrazit soubor

@@ -1,118 +0,0 @@
1
-package event
2
-
3
-import (
4
-	"sync"
5
-	"time"
6
-)
7
-
8
-// SessionUserCache 会话-用户映射缓存,支持TTL过期
9
-type SessionUserCache struct {
10
-	mu       sync.RWMutex
11
-	cache    map[string]cacheEntry
12
-	ttl      time.Duration
13
-	stopChan chan struct{}
14
-}
15
-
16
-// cacheEntry 缓存条目
17
-type cacheEntry struct {
18
-	userID    string
19
-	expiresAt time.Time
20
-}
21
-
22
-// NewSessionUserCache 创建新的会话-用户缓存
23
-func NewSessionUserCache(ttl time.Duration) *SessionUserCache {
24
-	cache := &SessionUserCache{
25
-		cache:    make(map[string]cacheEntry),
26
-		ttl:      ttl,
27
-		stopChan: make(chan struct{}),
28
-	}
29
-
30
-	// 启动清理协程
31
-	go cache.cleanupWorker()
32
-
33
-	return cache
34
-}
35
-
36
-// Set 设置会话-用户映射
37
-func (c *SessionUserCache) Set(sessionID, userID string) {
38
-	c.mu.Lock()
39
-	defer c.mu.Unlock()
40
-
41
-	c.cache[sessionID] = cacheEntry{
42
-		userID:    userID,
43
-		expiresAt: time.Now().Add(c.ttl),
44
-	}
45
-}
46
-
47
-// Get 获取用户ID,如果不存在或已过期返回空字符串
48
-func (c *SessionUserCache) Get(sessionID string) string {
49
-	c.mu.RLock()
50
-	entry, exists := c.cache[sessionID]
51
-	c.mu.RUnlock()
52
-
53
-	if !exists {
54
-		return ""
55
-	}
56
-
57
-	// 检查是否过期
58
-	if time.Now().After(entry.expiresAt) {
59
-		// 异步删除过期条目
60
-		go c.deleteIfExpired(sessionID)
61
-		return ""
62
-	}
63
-
64
-	return entry.userID
65
-}
66
-
67
-// Delete 删除指定会话的缓存
68
-func (c *SessionUserCache) Delete(sessionID string) {
69
-	c.mu.Lock()
70
-	defer c.mu.Unlock()
71
-
72
-	delete(c.cache, sessionID)
73
-}
74
-
75
-// deleteIfExpired 检查并删除过期条目
76
-func (c *SessionUserCache) deleteIfExpired(sessionID string) {
77
-	c.mu.Lock()
78
-	defer c.mu.Unlock()
79
-
80
-	if entry, exists := c.cache[sessionID]; exists {
81
-		if time.Now().After(entry.expiresAt) {
82
-			delete(c.cache, sessionID)
83
-		}
84
-	}
85
-}
86
-
87
-// cleanupWorker 定期清理过期条目的工作协程
88
-func (c *SessionUserCache) cleanupWorker() {
89
-	ticker := time.NewTicker(c.ttl / 2)
90
-	defer ticker.Stop()
91
-
92
-	for {
93
-		select {
94
-		case <-ticker.C:
95
-			c.cleanupExpired()
96
-		case <-c.stopChan:
97
-			return
98
-		}
99
-	}
100
-}
101
-
102
-// cleanupExpired 清理所有过期条目
103
-func (c *SessionUserCache) cleanupExpired() {
104
-	c.mu.Lock()
105
-	defer c.mu.Unlock()
106
-
107
-	now := time.Now()
108
-	for sessionID, entry := range c.cache {
109
-		if now.After(entry.expiresAt) {
110
-			delete(c.cache, sessionID)
111
-		}
112
-	}
113
-}
114
-
115
-// Stop 停止缓存清理协程
116
-func (c *SessionUserCache) Stop() {
117
-	close(c.stopChan)
118
-}

+ 26
- 575
internal/service/event/dispatcher.go Zobrazit soubor

@@ -14,426 +14,23 @@ import (
14 14
 	"git.x2erp.com/qdy/go-base/logger"
15 15
 )
16 16
 
17
-// MessageType 消息类型枚举
18
-type MessageType string
19
-
20
-const (
21
-	MessageTypeThinking MessageType = "thinking" // 思考过程
22
-	MessageTypeTool     MessageType = "tool"     // 工具调用
23
-	MessageTypeReply    MessageType = "reply"    // 最终回复
24
-	MessageTypeUnknown  MessageType = "unknown"  // 未知类型
25
-)
26
-
27
-// CompletionHook 完成钩子接口,用于消息完成时的处理(如保存到数据库)
28
-type CompletionHook interface {
29
-	OnMessageComplete(sessionID string, messageID string, completeText string, eventType string, metadata map[string]interface{})
30
-}
31
-
32
-// MessageState 消息状态,用于跟踪单个消息的增量内容
33
-type MessageState struct {
34
-	SessionID  string
35
-	MessageID  string
36
-	StartTime  time.Time
37
-	LastUpdate time.Time
38
-	Metadata   map[string]interface{}
39
-
40
-	// 分离的缓冲区,用于不同类型的内容
41
-	ReasoningBuffer strings.Builder // 思考内容
42
-	ReplyBuffer     strings.Builder // 回答内容
43
-	ToolBuffer      strings.Builder // 工具调用
44
-
45
-	// 类型完成状态跟踪
46
-	CompletedTypes map[string]bool // 已完成的类型: "reasoning", "text", "tool"
47
-	HookTriggered  map[string]bool // 钩子触发状态,按类型记录
48
-
49
-	// 当前活跃类型(用于跟踪正在处理的内容)
50
-	CurrentType string // 当前正在处理的类型
51
-}
52
-
53
-// MessageAggregator 消息聚合器,负责合并增量内容并检测完成状态
54
-type MessageAggregator struct {
55
-	mu                    sync.RWMutex
56
-	messages              map[string]*MessageState // key: sessionID_messageID
57
-	hooks                 []CompletionHook
58
-	OnMessageCompleteFunc func(sessionID string, messageID string, completeText string, messageType MessageType, metadata map[string]interface{}) // 消息完成回调函数
59
-	OnEventProcessedFunc  func(sessionID string, eventType string, eventData string, eventMap map[string]interface{})                             // 事件处理回调函数
60
-}
61
-
62
-// NewMessageAggregator 创建新的消息聚合器
63
-func NewMessageAggregator() *MessageAggregator {
64
-	return &MessageAggregator{
65
-		messages:              make(map[string]*MessageState),
66
-		hooks:                 make([]CompletionHook, 0),
67
-		OnMessageCompleteFunc: nil,
68
-		OnEventProcessedFunc:  nil,
69
-	}
70
-}
71
-
72
-// RegisterHook 注册完成钩子
73
-func (ma *MessageAggregator) RegisterHook(hook CompletionHook) {
74
-	ma.mu.Lock()
75
-	defer ma.mu.Unlock()
76
-	ma.hooks = append(ma.hooks, hook)
77
-}
78
-
79
-// ProcessEvent 处理事件,合并增量内容并检测完成状态
80
-func (ma *MessageAggregator) ProcessEvent(eventData string, sessionID string) {
81
-	// 解析事件数据
82
-	var eventMap map[string]interface{}
83
-	if err := json.Unmarshal([]byte(eventData), &eventMap); err != nil {
84
-		logger.Debug(fmt.Sprintf("无法解析事件JSON error=%s dataPreview=%s",
85
-			err.Error(), safeSubstring(eventData, 0, 200)))
86
-		return
87
-	}
88
-
89
-	eventType := getEventType(eventMap)
90
-
91
-	// 诊断日志:记录处理的事件类型
92
-	logger.Debug(fmt.Sprintf("🔍 ProcessEvent: sessionID=%s eventType=%s dataPreview=%s",
93
-		sessionID, eventType, safeSubstring(eventData, 0, 100)))
94
-
95
-	// 调用事件处理回调函数(如果有设置)
96
-	if ma.OnEventProcessedFunc != nil {
97
-		ma.OnEventProcessedFunc(sessionID, eventType, eventData, eventMap)
98
-	}
99
-
100
-	// 只处理 message.part.updated, message.updated, session.status 事件
101
-	if eventType == "message.part.updated" {
102
-		ma.handleMessagePartUpdated(eventMap, sessionID, eventData)
103
-	} else if eventType == "message.updated" {
104
-		ma.handleMessageUpdated(eventMap, sessionID)
105
-	} else if eventType == "session.status" {
106
-		ma.handleSessionStatus(eventMap, sessionID)
107
-	} else if eventType == "session.idle" {
108
-		ma.handleSessionIdle(sessionID)
109
-	}
110
-	// 其他事件类型忽略
111
-}
112
-
113
-// handleMessagePartUpdated 处理 message.part.updated 事件
114
-func (ma *MessageAggregator) handleMessagePartUpdated(eventMap map[string]interface{}, sessionID string, eventData string) {
115
-	// 提取消息部分信息
116
-	payload, _ := eventMap["payload"].(map[string]interface{})
117
-	props, _ := payload["properties"].(map[string]interface{})
118
-	part, _ := props["part"].(map[string]interface{})
119
-
120
-	messageID, _ := part["messageID"].(string)
121
-	partType, _ := part["type"].(string)
122
-
123
-	if messageID == "" || (partType != "text" && partType != "reasoning" && partType != "step-finish" && partType != "tool") {
124
-		return
125
-	}
126
-
127
-	ma.mu.Lock()
128
-	defer ma.mu.Unlock()
129
-
130
-	key := sessionID + "_" + messageID
131
-	state, exists := ma.messages[key]
132
-
133
-	if !exists {
134
-		// step-finish 事件不应该创建新状态,它应该总是跟随在text/reasoning事件之后
135
-		if partType == "step-finish" {
136
-			logger.Debug(fmt.Sprintf("忽略step-finish事件,无对应消息状态 sessionID=%s messageID=%s",
137
-				sessionID, messageID))
138
-			return
139
-		}
140
-
141
-		// 新消息,初始化状态
142
-		state = &MessageState{
143
-			SessionID:      sessionID,
144
-			MessageID:      messageID,
145
-			StartTime:      time.Now(),
146
-			LastUpdate:     time.Now(),
147
-			Metadata:       make(map[string]interface{}),
148
-			CompletedTypes: make(map[string]bool),
149
-			HookTriggered:  make(map[string]bool),
150
-			CurrentType:    partType,
151
-		}
152
-		ma.messages[key] = state
153
-		logger.Debug(fmt.Sprintf("开始跟踪新消息 sessionID=%s messageID=%s type=%s",
154
-			sessionID, messageID, partType))
155
-	}
156
-
157
-	// 更新增量内容 - 根据类型写入不同的缓冲区
158
-	state.CurrentType = partType
159
-
160
-	if text, ok := part["text"].(string); ok && text != "" {
161
-		// 根据类型选择缓冲区
162
-		switch partType {
163
-		case "reasoning":
164
-			state.ReasoningBuffer.WriteString(text)
165
-		case "text":
166
-			state.ReplyBuffer.WriteString(text)
167
-		case "tool":
168
-			// 工具调用可能有text字段,也可能通过其他方式处理
169
-			state.ToolBuffer.WriteString(text)
170
-		}
171
-		state.LastUpdate = time.Now()
172
-
173
-		// 记录增量合并日志(仅调试)- 已禁用以减少日志量
174
-		// logger.Debug(fmt.Sprintf("合并增量内容 sessionID=%s messageID=%s type=%s deltaLength=%d",
175
-		// 	sessionID, messageID, partType, len(text)))
176
-	} else if partType == "tool" {
177
-		// 处理工具调用事件,提取工具信息
178
-		if name, ok := part["name"].(string); ok && name != "" {
179
-			toolText := fmt.Sprintf("[工具调用: %s", name)
180
-			if args, ok := part["arguments"].(string); ok && args != "" {
181
-				// 参数可能是JSON字符串,可以尝试美化
182
-				toolText += fmt.Sprintf(" 参数: %s", args)
183
-			}
184
-			toolText += "]"
185
-			state.ToolBuffer.WriteString(toolText)
186
-			state.LastUpdate = time.Now()
187
-			logger.Debug(fmt.Sprintf("合并工具调用内容 sessionID=%s messageID=%s toolName=%s",
188
-				sessionID, messageID, name))
189
-		}
190
-	}
191
-
192
-	// 检查是否为 step-finish
193
-	if partType == "step-finish" {
194
-		// 标记当前类型为完成
195
-		if state.CurrentType != "" {
196
-			state.CompletedTypes[state.CurrentType] = true
197
-			logger.Info(fmt.Sprintf("步骤完成 sessionID=%s messageID=%s type=%s",
198
-				sessionID, messageID, state.CurrentType))
199
-
200
-			// 触发该类型的完成钩子
201
-			ma.triggerTypeCompletionHooks(state, state.CurrentType)
202
-		} else {
203
-			logger.Warn(fmt.Sprintf("step-finish事件无当前类型 sessionID=%s messageID=%s",
204
-				sessionID, messageID))
205
-		}
206
-	}
207
-}
208
-
209
-// handleMessageUpdated 处理 message.updated 事件
210
-func (ma *MessageAggregator) handleMessageUpdated(eventMap map[string]interface{}, sessionID string) {
211
-	payload, _ := eventMap["payload"].(map[string]interface{})
212
-	props, _ := payload["properties"].(map[string]interface{})
213
-	info, _ := props["info"].(map[string]interface{})
214
-
215
-	messageID, _ := info["id"].(string)
216
-	finishReason, _ := info["finish"].(string)
217
-
218
-	if messageID == "" || finishReason == "" {
219
-		return
220
-	}
221
-
222
-	key := sessionID + "_" + messageID
223
-	ma.mu.Lock()
224
-	defer ma.mu.Unlock()
225
-
226
-	if state, exists := ma.messages[key]; exists {
227
-		logger.Info(fmt.Sprintf("消息完成 sessionID=%s messageID=%s finishReason=%s",
228
-			sessionID, messageID, finishReason))
229
-
230
-		// 触发完成钩子(处理所有类型的内容)
231
-		ma.triggerCompletionHooks(state)
232
-
233
-		// 清理完成的消息状态
234
-		delete(ma.messages, key)
235
-	}
236
-}
237
-
238
-// handleSessionStatus 处理 session.status 事件
239
-func (ma *MessageAggregator) handleSessionStatus(eventMap map[string]interface{}, sessionID string) {
240
-	payload, _ := eventMap["payload"].(map[string]interface{})
241
-	props, _ := payload["properties"].(map[string]interface{})
242
-	status, _ := props["status"].(map[string]interface{})
243
-
244
-	statusType, _ := status["type"].(string)
245
-	if statusType == "idle" {
246
-		logger.Info(fmt.Sprintf("会话进入空闲状态 sessionID=%s", sessionID))
247
-		// 可以清理该会话的所有消息状态
248
-		ma.cleanupSession(sessionID)
249
-	}
250
-}
251
-
252
-// handleSessionIdle 处理 session.idle 事件
253
-func (ma *MessageAggregator) handleSessionIdle(sessionID string) {
254
-	logger.Info(fmt.Sprintf("会话空闲事件 sessionID=%s", sessionID))
255
-	ma.cleanupSession(sessionID)
256
-}
257
-
258
-// cleanupSession 清理指定会话的所有消息状态
259
-func (ma *MessageAggregator) cleanupSession(sessionID string) {
260
-	ma.mu.Lock()
261
-	defer ma.mu.Unlock()
262
-
263
-	keysToDelete := make([]string, 0)
264
-	for key, state := range ma.messages {
265
-		if state.SessionID == sessionID {
266
-			keysToDelete = append(keysToDelete, key)
267
-
268
-			// 检查是否有未触发的类型内容,强制触发完成钩子
269
-			hasUnfinishedContent := false
270
-
271
-			// 检查每种类型是否有内容但钩子未触发
272
-			if state.ReasoningBuffer.Len() > 0 {
273
-				if triggered, exists := state.HookTriggered["reasoning"]; !exists || !triggered {
274
-					hasUnfinishedContent = true
275
-				}
276
-			}
277
-			if state.ReplyBuffer.Len() > 0 {
278
-				if triggered, exists := state.HookTriggered["text"]; !exists || !triggered {
279
-					hasUnfinishedContent = true
280
-				}
281
-			}
282
-			if state.ToolBuffer.Len() > 0 {
283
-				if triggered, exists := state.HookTriggered["tool"]; !exists || !triggered {
284
-					hasUnfinishedContent = true
285
-				}
286
-			}
287
-
288
-			if hasUnfinishedContent {
289
-				logger.Warn(fmt.Sprintf("强制完成未完成消息 sessionID=%s messageID=%s",
290
-					sessionID, state.MessageID))
291
-				ma.triggerCompletionHooks(state)
292
-			}
293
-		}
294
-	}
295
-
296
-	for _, key := range keysToDelete {
297
-		delete(ma.messages, key)
298
-	}
299
-
300
-	if len(keysToDelete) > 0 {
301
-		logger.Info(fmt.Sprintf("清理会话消息状态 sessionID=%s cleanedCount=%d",
302
-			sessionID, len(keysToDelete)))
303
-	}
304
-}
305
-
306
-// triggerTypeCompletionHooks 触发特定类型的完成钩子
307
-func (ma *MessageAggregator) triggerTypeCompletionHooks(state *MessageState, partType string) {
308
-	// 避免重复触发
309
-	if triggered, exists := state.HookTriggered[partType]; exists && triggered {
310
-		logger.Debug(fmt.Sprintf("钩子已触发过,跳过 sessionID=%s messageID=%s type=%s",
311
-			state.SessionID, state.MessageID, partType))
312
-		return
313
-	}
314
-
315
-	// 获取该类型的缓冲区内容
316
-	var completeText string
317
-	var textLength int
318
-
319
-	switch partType {
320
-	case "reasoning":
321
-		completeText = state.ReasoningBuffer.String()
322
-		textLength = len(completeText)
323
-	case "text":
324
-		completeText = state.ReplyBuffer.String()
325
-		textLength = len(completeText)
326
-	case "tool":
327
-		completeText = state.ToolBuffer.String()
328
-		textLength = len(completeText)
329
-	default:
330
-		logger.Warn(fmt.Sprintf("未知类型,跳过钩子触发 sessionID=%s messageID=%s type=%s",
331
-			state.SessionID, state.MessageID, partType))
332
-		return
333
-	}
334
-
335
-	duration := time.Since(state.StartTime)
336
-
337
-	// 记录完成事件
338
-	logger.Info(fmt.Sprintf("🔔 类型完成总结 sessionID=%s messageID=%s type=%s textLength=%d duration=%v",
339
-		state.SessionID, state.MessageID, partType, textLength, duration))
340
-
341
-	if textLength > 0 {
342
-		// 记录文本预览(前200字符)
343
-		preview := completeText
344
-		if len(preview) > 200 {
345
-			preview = preview[:200] + "..."
346
-		}
347
-		logger.Info(fmt.Sprintf("📝 类型文本预览: %s", preview))
348
-	} else {
349
-		logger.Info("📭 空文本完成事件")
350
-	}
351
-
352
-	// 转换事件类型为消息类型枚举
353
-	messageType := convertToMessageType(partType)
354
-
355
-	// 调用消息完成回调函数(如果设置)- 使用新的类型化接口
356
-	// 注意:这里调用现有的OnMessageCompleteFunc,传入特定类型的内容
357
-	if ma.OnMessageCompleteFunc != nil {
358
-		ma.OnMessageCompleteFunc(state.SessionID, state.MessageID, completeText, messageType, state.Metadata)
359
-	}
360
-
361
-	// 调用所有注册的钩子(保持向后兼容)
362
-	for _, hook := range ma.hooks {
363
-		hook.OnMessageComplete(state.SessionID, state.MessageID, completeText, partType, state.Metadata)
364
-	}
365
-
366
-	// 标记该类型的钩子已触发
367
-	state.HookTriggered[partType] = true
368
-	logger.Debug(fmt.Sprintf("✅ 类型钩子标记为已触发 sessionID=%s messageID=%s type=%s",
369
-		state.SessionID, state.MessageID, partType))
370
-}
371
-
372
-// triggerCompletionHooks 触发完成钩子(向后兼容,触发所有类型)
373
-func (ma *MessageAggregator) triggerCompletionHooks(state *MessageState) {
374
-	// 遍历所有支持的类型,触发各自的完成钩子
375
-	types := []string{"reasoning", "text", "tool"}
376
-	hasAnyContent := false
377
-
378
-	for _, partType := range types {
379
-		// 检查该类型是否有内容
380
-		var hasContent bool
381
-		switch partType {
382
-		case "reasoning":
383
-			hasContent = state.ReasoningBuffer.Len() > 0
384
-		case "text":
385
-			hasContent = state.ReplyBuffer.Len() > 0
386
-		case "tool":
387
-			hasContent = state.ToolBuffer.Len() > 0
388
-		}
389
-
390
-		if hasContent {
391
-			hasAnyContent = true
392
-			// 触发该类型的完成钩子(函数内部会检查是否已触发)
393
-			ma.triggerTypeCompletionHooks(state, partType)
394
-		}
395
-	}
396
-
397
-	// 如果没有内容,至少记录一个事件(保持向后兼容)
398
-	if !hasAnyContent {
399
-		logger.Info(fmt.Sprintf("📭 空消息完成事件 sessionID=%s messageID=%s",
400
-			state.SessionID, state.MessageID))
401
-	}
402
-}
403
-
404 17
 // EventDispatcher 事件分发器 - 单例模式
405 18
 type EventDispatcher struct {
406
-	mu                sync.RWMutex
407
-	baseURL           string
408
-	port              int
409
-	subscriptions     map[string]map[chan string]struct{} // sessionID -> set of channels
410
-	sessionUserCache  *SessionUserCache                   // sessionID -> userID 映射缓存(用于用户验证)
411
-	client            *http.Client
412
-	cancelFunc        context.CancelFunc
413
-	running           bool
414
-	messageAggregator *MessageAggregator // 消息聚合器
415
-}
416
-
417
-// EventData opencode事件数据结构 - 匹配实际事件格式
418
-type EventData struct {
419
-	Directory string                 `json:"directory,omitempty"`
420
-	Payload   map[string]interface{} `json:"payload"`
421
-}
422
-
423
-// PayloadData payload内部结构(辅助类型)
424
-type PayloadData struct {
425
-	Type       string                 `json:"type"`
426
-	Properties map[string]interface{} `json:"properties,omitempty"`
19
+	mu            sync.RWMutex
20
+	baseURL       string
21
+	port          int
22
+	subscriptions map[string]map[chan string]struct{} // sessionID -> set of channels
23
+	client        *http.Client
24
+	cancelFunc    context.CancelFunc
25
+	running       bool
427 26
 }
428 27
 
429 28
 // NewEventDispatcher 创建新的事件分发器
430 29
 func NewEventDispatcher(baseURL string, port int) *EventDispatcher {
431 30
 	return &EventDispatcher{
432
-		baseURL:           baseURL,
433
-		port:              port,
434
-		subscriptions:     make(map[string]map[chan string]struct{}),
435
-		sessionUserCache:  NewSessionUserCache(20 * time.Minute),
436
-		messageAggregator: NewMessageAggregator(),
31
+		baseURL:       baseURL,
32
+		port:          port,
33
+		subscriptions: make(map[string]map[chan string]struct{}),
437 34
 		client: &http.Client{
438 35
 			Timeout: 0, // 无超时限制,用于长连接
439 36
 		},
@@ -494,9 +91,6 @@ func (ed *EventDispatcher) Subscribe(sessionID, userID string) (<-chan string, e
494 91
 	ed.mu.Lock()
495 92
 	defer ed.mu.Unlock()
496 93
 
497
-	// 缓存会话-用户映射(用于未来需要用户验证时)
498
-	ed.sessionUserCache.Set(sessionID, userID)
499
-
500 94
 	// 创建缓冲通道
501 95
 	ch := make(chan string, 100)
502 96
 
@@ -538,18 +132,10 @@ func (ed *EventDispatcher) Unsubscribe(sessionID string, ch <-chan string) {
538 132
 		// 如果没有订阅者了,清理该会话的映射
539 133
 		if len(channels) == 0 {
540 134
 			delete(ed.subscriptions, sessionID)
541
-			ed.sessionUserCache.Delete(sessionID)
542 135
 		}
543 136
 	}
544 137
 }
545 138
 
546
-// RegisterSession 注册会话(前端调用SendPromptStream时调用)
547
-func (ed *EventDispatcher) RegisterSession(sessionID, userID string) {
548
-	ed.sessionUserCache.Set(sessionID, userID)
549
-	logger.Debug(fmt.Sprintf("会话已注册 sessionID=%s userID=%s",
550
-		sessionID, userID))
551
-}
552
-
553 139
 // buildSSEURL 构建SSE URL,避免端口重复
554 140
 func (ed *EventDispatcher) buildSSEURL() string {
555 141
 	// 检查baseURL是否已包含端口
@@ -674,10 +260,6 @@ func (ed *EventDispatcher) connectAndProcessSSE(ctx context.Context, url string)
674 260
 				// 分发事件
675 261
 				ed.dispatchEvent(data)
676 262
 
677
-				if eventCount%100 == 0 {
678
-					//logger.Debug(fmt.Sprintf("事件处理统计 totalEvents=%d activeSessions=%d",
679
-					//	eventCount, len(ed.subscriptions)))
680
-				}
681 263
 			}
682 264
 		}
683 265
 	}
@@ -688,32 +270,28 @@ func (ed *EventDispatcher) dispatchEvent(data string) {
688 270
 	// 解析事件数据获取sessionID
689 271
 	sessionID := extractSessionIDFromEvent(data)
690 272
 
691
-	// 处理事件聚合(无论是否有sessionID都处理)
692
-	if ed.messageAggregator != nil && sessionID != "" {
693
-		ed.messageAggregator.ProcessEvent(data, sessionID)
694
-	}
695
-
696 273
 	if sessionID == "" {
697
-		// 没有sessionID的事件(如全局心跳)分发给所有订阅者
698
-		//logger.Debug(fmt.Sprintf("广播全局事件 dataPreview=%s", safeSubstring(data, 0, 100)))
699
-		ed.broadcastToAll(data)
274
+		// 没有sessionID的事件(如全局心跳)丢弃,不广播给所有订阅者
275
+		// 确保按sessionID严格隔离,避免多用户消息交叉
700 276
 		return
701 277
 	}
702 278
 
703
-	// 只记录非增量文本事件的路由日志,减少日志量
704
-	shouldLog := true
279
+	// 只记录关键事件的路由日志,减少日志输出
705 280
 	var eventMap map[string]interface{}
706 281
 	if err := json.Unmarshal([]byte(data), &eventMap); err == nil {
707
-		eventType := getEventType(eventMap)
708
-		// message.part.updated 是最频繁的事件,跳过其路由日志
709
-		if eventType == "message.part.updated" {
710
-			shouldLog = false
282
+		// 提取事件类型
283
+		var eventType string
284
+		if payload, ok := eventMap["payload"].(map[string]interface{}); ok {
285
+			if t, ok := payload["type"].(string); ok {
286
+				eventType = t
287
+			}
288
+		}
289
+		// 只记录关键事件类型的路由信息
290
+		switch eventType {
291
+		case "session.status", "message.updated", "session.diff", "session.idle":
292
+			logger.Debug(fmt.Sprintf("路由事件到会话 sessionID=%s type=%s",
293
+				sessionID, eventType))
711 294
 		}
712
-	}
713
-
714
-	if shouldLog {
715
-		logger.Debug(fmt.Sprintf("路由事件到会话 sessionID=%s dataPreview=%s",
716
-			sessionID, safeSubstring(data, 0, 100)))
717 295
 	}
718 296
 
719 297
 	// 只分发给订阅该会话的通道
@@ -723,7 +301,6 @@ func (ed *EventDispatcher) dispatchEvent(data string) {
723 301
 
724 302
 	if !exists {
725 303
 		// 没有该会话的订阅者,忽略事件
726
-		//logger.Debug(fmt.Sprintf("忽略事件,无订阅者 sessionID=%s", sessionID))
727 304
 		return
728 305
 	}
729 306
 
@@ -742,52 +319,18 @@ func (ed *EventDispatcher) dispatchEvent(data string) {
742 319
 	ed.mu.RUnlock()
743 320
 }
744 321
 
745
-// broadcastToAll 广播事件给所有订阅者(用于全局事件如心跳)
746
-func (ed *EventDispatcher) broadcastToAll(data string) {
747
-	//logger.Debug(fmt.Sprintf("广播事件给所有订阅者 dataPreview=%s", safeSubstring(data, 0, 100)))
748
-	ed.mu.RLock()
749
-	defer ed.mu.RUnlock()
750
-
751
-	for sessionID, channels := range ed.subscriptions {
752
-		for ch := range channels {
753
-			select {
754
-			case ch <- data:
755
-				// 成功发送
756
-			default:
757
-				// 通道已满,丢弃事件
758
-				logger.Warn(fmt.Sprintf("事件通道已满,丢弃全局事件 sessionID=%s",
759
-					sessionID))
760
-			}
761
-		}
762
-	}
763
-}
764
-
765 322
 // extractSessionIDFromEvent 从事件数据中提取sessionID
766 323
 func extractSessionIDFromEvent(data string) string {
767 324
 	// 尝试解析为JSON
768 325
 	var eventMap map[string]interface{}
769 326
 	if err := json.Unmarshal([]byte(data), &eventMap); err != nil {
770
-		logger.ErrorC(fmt.Sprintf("无法解析事件JSON error=%s dataPreview=%s",
771
-			err.Error(), safeSubstring(data, 0, 200)))
327
+		logger.Error("无法解析事件JSON", "error", err.Error(), "dataPreview", safeSubstring(data, 0, 200))
772 328
 		return ""
773 329
 	}
774 330
 
775
-	// 添加调试日志,显示完整事件结构(仅调试时启用)
776
-	debugMode := true
777
-	if debugMode {
778
-		eventJSON, _ := json.MarshalIndent(eventMap, "", "  ")
779
-		logger.Debug(fmt.Sprintf("事件数据结构 eventStructure=%s",
780
-			string(eventJSON)))
781
-	}
782
-
783 331
 	// 递归查找sessionID字段
784 332
 	sessionID := findSessionIDRecursive(eventMap)
785 333
 
786
-	if sessionID == "" {
787
-		//logger.Debug(fmt.Sprintf("未找到sessionID字段 eventType=%s dataPreview=%s",
788
-		//	getEventType(eventMap), safeSubstring(data, 0, 100)))
789
-	}
790
-
791 334
 	return sessionID
792 335
 }
793 336
 
@@ -855,37 +398,6 @@ func findSessionIDRecursive(data interface{}) string {
855 398
 	return ""
856 399
 }
857 400
 
858
-// getEventType 获取事件类型
859
-func getEventType(eventMap map[string]interface{}) string {
860
-	if payload, ok := eventMap["payload"].(map[string]interface{}); ok {
861
-		if eventType, ok := payload["type"].(string); ok {
862
-			return eventType
863
-		}
864
-	}
865
-	return "unknown"
866
-}
867
-
868
-// convertToMessageType 将事件类型转换为消息类型枚举
869
-func convertToMessageType(eventType string) MessageType {
870
-	switch eventType {
871
-	case "reasoning":
872
-		return MessageTypeThinking
873
-	case "text":
874
-		return MessageTypeReply
875
-	case "tool":
876
-		return MessageTypeTool
877
-	default:
878
-		// 尝试识别其他类型
879
-		if strings.Contains(eventType, "reasoning") || strings.Contains(eventType, "thinking") {
880
-			return MessageTypeThinking
881
-		}
882
-		if strings.Contains(eventType, "tool") || strings.Contains(eventType, "function") {
883
-			return MessageTypeTool
884
-		}
885
-		return MessageTypeUnknown
886
-	}
887
-}
888
-
889 401
 // safeSubstring 安全的子字符串函数
890 402
 func safeSubstring(s string, start, length int) string {
891 403
 	if start < 0 {
@@ -901,40 +413,6 @@ func safeSubstring(s string, start, length int) string {
901 413
 	return s[start:end]
902 414
 }
903 415
 
904
-// getHookTriggerSource 获取钩子触发源(用于调试)
905
-func getHookTriggerSource(state *MessageState) string {
906
-	// 检查是否有任何类型的钩子已触发
907
-	for _, triggered := range state.HookTriggered {
908
-		if triggered {
909
-			return "completed"
910
-		}
911
-	}
912
-	return "unknown"
913
-}
914
-
915
-// RegisterHook 注册完成钩子
916
-func (ed *EventDispatcher) RegisterHook(hook CompletionHook) {
917
-	if ed.messageAggregator != nil {
918
-		ed.messageAggregator.RegisterHook(hook)
919
-	}
920
-}
921
-
922
-// SetOnMessageCompleteFunc 设置消息完成回调函数
923
-func (ed *EventDispatcher) SetOnMessageCompleteFunc(f func(sessionID string, messageID string, completeText string, messageType MessageType, metadata map[string]interface{})) {
924
-	if ed.messageAggregator != nil {
925
-		ed.messageAggregator.OnMessageCompleteFunc = f
926
-		logger.Info("✅ 消息完成回调函数已设置")
927
-	}
928
-}
929
-
930
-// SetOnEventProcessedFunc 设置事件处理回调函数
931
-func (ed *EventDispatcher) SetOnEventProcessedFunc(f func(sessionID string, eventType string, eventData string, eventMap map[string]interface{})) {
932
-	if ed.messageAggregator != nil {
933
-		ed.messageAggregator.OnEventProcessedFunc = f
934
-		logger.Info("✅ 事件处理回调函数已设置")
935
-	}
936
-}
937
-
938 416
 // GetInstance 获取单例实例(线程安全)
939 417
 var (
940 418
 	instance     *EventDispatcher
@@ -948,30 +426,3 @@ func GetEventDispatcher(baseURL string, port int) *EventDispatcher {
948 426
 	})
949 427
 	return instance
950 428
 }
951
-
952
-// DiagnosticHook 诊断钩子实现(用于调试和测试)
953
-type DiagnosticHook struct{}
954
-
955
-func (h *DiagnosticHook) OnMessageComplete(sessionID string, messageID string, completeText string, eventType string, metadata map[string]interface{}) {
956
-	logger.Info(fmt.Sprintf("🔍 诊断钩子触发: session=%s message=%s type=%s textLength=%d",
957
-		sessionID, messageID, eventType, len(completeText)))
958
-
959
-	if len(completeText) > 0 {
960
-		preview := completeText
961
-		if len(preview) > 150 {
962
-			preview = preview[:150] + "..."
963
-		}
964
-		logger.Info(fmt.Sprintf("📋 诊断钩子文本预览: %s", preview))
965
-	} else {
966
-		logger.Info("📭 诊断钩子: 空文本")
967
-	}
968
-
969
-	// 记录元数据(如果有)
970
-	if metadata != nil && len(metadata) > 0 {
971
-		logger.Info(fmt.Sprintf("📊 诊断钩子元数据: %+v", metadata))
972
-	}
973
-}
974
-
975
-// 使用示例:
976
-// dispatcher := GetEventDispatcher("http://localhost", 3000)
977
-// dispatcher.RegisterHook(&DiagnosticHook{})

+ 0
- 146
internal/service/event/subscription_service.go Zobrazit soubor

@@ -1,146 +0,0 @@
1
-package event
2
-
3
-import (
4
-	"fmt"
5
-	"sync"
6
-
7
-	"git.x2erp.com/qdy/go-base/ctx"
8
-	"git.x2erp.com/qdy/go-base/logger"
9
-	"git.x2erp.com/qdy/go-db/factory/mongodb"
10
-)
11
-
12
-// SubscriptionService 订阅服务,负责管理事件订阅并记录相关日志
13
-type SubscriptionService struct {
14
-	dispatcher   *EventDispatcher
15
-	mongoFactory *mongodb.MongoDBFactory
16
-}
17
-
18
-// NewSubscriptionService 创建新的订阅服务
19
-func NewSubscriptionService(dispatcher *EventDispatcher, mongoFactory *mongodb.MongoDBFactory) *SubscriptionService {
20
-	return &SubscriptionService{
21
-		dispatcher:   dispatcher,
22
-		mongoFactory: mongoFactory,
23
-	}
24
-}
25
-
26
-// SubscribeWithContext 使用上下文信息订阅会话事件
27
-func (s *SubscriptionService) SubscribeWithContext(sessionID string, reqCtx *ctx.RequestContext) (<-chan string, error) {
28
-	// 提取用户ID(优先使用reqCtx.UserID,如果为空则使用Username)
29
-	userID := reqCtx.UserID
30
-	if userID == "" {
31
-		userID = reqCtx.Username
32
-	}
33
-	if userID == "" {
34
-		userID = "unknown-user"
35
-	}
36
-
37
-	// 记录详细的调试日志
38
-	logger.Debug(fmt.Sprintf("🔔 订阅服务: 开始订阅 sessionID=%s, userID=%s, tenantID=%s, traceID=%s",
39
-		sessionID, userID, reqCtx.TenantID, reqCtx.TraceID))
40
-
41
-	// 调用事件分发器的Subscribe方法
42
-	ch, err := s.dispatcher.Subscribe(sessionID, userID)
43
-	if err != nil {
44
-		logger.Error(fmt.Sprintf("❌ 订阅服务: 订阅失败 sessionID=%s, error=%v", sessionID, err))
45
-		return nil, err
46
-	}
47
-
48
-	// 记录成功的订阅信息
49
-	logger.Info(fmt.Sprintf("✅ 订阅服务: 订阅成功 sessionID=%s, userID=%s, tenantID=%s",
50
-		sessionID, userID, reqCtx.TenantID))
51
-
52
-	// 这里可以扩展:将订阅信息保存到MongoDB
53
-	// 例如:s.saveSubscriptionToDB(sessionID, reqCtx)
54
-
55
-	// 对于调试,记录MongoDB工厂状态
56
-	if s.mongoFactory != nil {
57
-		logger.Debug(fmt.Sprintf("🔍 订阅服务: MongoDB工厂可用 sessionID=%s", sessionID))
58
-		// 可以测试连接或执行其他操作
59
-	} else {
60
-		logger.Debug(fmt.Sprintf("⚠️ 订阅服务: MongoDB工厂未配置 sessionID=%s", sessionID))
61
-	}
62
-
63
-	return ch, nil
64
-}
65
-
66
-// UnsubscribeWithContext 使用上下文信息取消订阅
67
-func (s *SubscriptionService) UnsubscribeWithContext(sessionID string, ch <-chan string, reqCtx *ctx.RequestContext) {
68
-	// 记录取消订阅日志
69
-	logger.Debug(fmt.Sprintf("🔔 订阅服务: 取消订阅 sessionID=%s, userID=%s, traceID=%s",
70
-		sessionID, reqCtx.UserID, reqCtx.TraceID))
71
-
72
-	s.dispatcher.Unsubscribe(sessionID, ch)
73
-
74
-	// 这里可以扩展:更新MongoDB中的订阅状态
75
-	// 例如:s.updateSubscriptionStatus(sessionID, "unsubscribed")
76
-
77
-	logger.Info(fmt.Sprintf("✅ 订阅服务: 已取消订阅 sessionID=%s", sessionID))
78
-}
79
-
80
-// RegisterSessionWithContext 使用上下文信息注册会话
81
-func (s *SubscriptionService) RegisterSessionWithContext(sessionID string, reqCtx *ctx.RequestContext) {
82
-	userID := reqCtx.UserID
83
-	if userID == "" {
84
-		userID = reqCtx.Username
85
-	}
86
-	if userID == "" {
87
-		userID = "unknown-user"
88
-	}
89
-
90
-	logger.Debug(fmt.Sprintf("🔔 订阅服务: 注册会话 sessionID=%s, userID=%s, tenantID=%s",
91
-		sessionID, userID, reqCtx.TenantID))
92
-
93
-	s.dispatcher.RegisterSession(sessionID, userID)
94
-
95
-	// 这里可以扩展:将会话注册信息保存到MongoDB
96
-	logger.Info(fmt.Sprintf("✅ 订阅服务: 会话已注册 sessionID=%s", sessionID))
97
-}
98
-
99
-// GetSubscriptionStats 获取订阅统计信息(用于调试和监控)
100
-func (s *SubscriptionService) GetSubscriptionStats() map[string]interface{} {
101
-	// 这里可以扩展:从MongoDB获取订阅统计
102
-	// 目前返回基本信息
103
-	stats := map[string]interface{}{
104
-		"service": "SubscriptionService",
105
-		"status":  "active",
106
-		"mongoDB": s.mongoFactory != nil,
107
-	}
108
-
109
-	logger.Debug(fmt.Sprintf("📊 订阅服务: 获取统计信息 stats=%+v", stats))
110
-	return stats
111
-}
112
-
113
-// saveSubscriptionToDB 将订阅信息保存到MongoDB(待扩展)
114
-func (s *SubscriptionService) saveSubscriptionToDB(sessionID string, reqCtx *ctx.RequestContext) {
115
-	// 待实现:将订阅信息保存到MongoDB
116
-	// 可以记录:sessionID, userID, tenantID, subscriptionTime, status等
117
-	logger.Debug(fmt.Sprintf("💾 订阅服务: 待实现 - 保存订阅信息到数据库 sessionID=%s", sessionID))
118
-}
119
-
120
-// updateSubscriptionStatus 更新MongoDB中的订阅状态(待扩展)
121
-func (s *SubscriptionService) updateSubscriptionStatus(sessionID string, status string) {
122
-	// 待实现:更新订阅状态
123
-	logger.Debug(fmt.Sprintf("💾 订阅服务: 待实现 - 更新订阅状态 sessionID=%s, status=%s", sessionID, status))
124
-}
125
-
126
-// 单例模式
127
-var (
128
-	subscriptionServiceInstance *SubscriptionService
129
-	subscriptionServiceOnce     sync.Once
130
-)
131
-
132
-// InitSubscriptionService 初始化订阅服务单例
133
-func InitSubscriptionService(dispatcher *EventDispatcher, mongoFactory *mongodb.MongoDBFactory) {
134
-	subscriptionServiceOnce.Do(func() {
135
-		subscriptionServiceInstance = NewSubscriptionService(dispatcher, mongoFactory)
136
-		logger.Info("✅ 订阅服务单例已初始化")
137
-	})
138
-}
139
-
140
-// GetSubscriptionService 获取订阅服务单例
141
-func GetSubscriptionService() *SubscriptionService {
142
-	if subscriptionServiceInstance == nil {
143
-		logger.Warn("⚠️ 订阅服务单例未初始化,返回nil")
144
-	}
145
-	return subscriptionServiceInstance
146
-}

+ 3
- 46
main.go Zobrazit soubor

@@ -87,52 +87,6 @@ func main() {
87 87
 		defer dispatcher.Stop()
88 88
 	}
89 89
 
90
-	// 注册诊断钩子(用于调试和日志输出)
91
-	dispatcher.RegisterHook(&event.DiagnosticHook{})
92
-	log.Printf("诊断钩子已注册")
93
-
94
-	// 设置消息完成回调函数(简单方案)
95
-	dispatcher.SetOnMessageCompleteFunc(func(sessionID string, messageID string, completeText string, messageType event.MessageType, metadata map[string]interface{}) {
96
-		textLength := len(completeText)
97
-		log.Printf("🔔 消息完成总结 sessionID=%s messageID=%s type=%s textLength=%d",
98
-			sessionID, messageID, messageType, textLength)
99
-
100
-		if textLength > 0 {
101
-			preview := completeText
102
-			if len(preview) > 200 {
103
-				preview = preview[:200] + "..."
104
-			}
105
-			log.Printf("📝 文本预览: %s", preview)
106
-		} else {
107
-			log.Printf("📭 空文本完成事件")
108
-		}
109
-
110
-		// 记录元数据(如果有)
111
-		if metadata != nil && len(metadata) > 0 {
112
-			log.Printf("📊 元数据: %+v", metadata)
113
-		}
114
-	})
115
-	log.Printf("消息完成回调函数已设置")
116
-
117
-	// 设置事件处理回调函数(记录所有事件)
118
-	dispatcher.SetOnEventProcessedFunc(func(sessionID string, eventType string, eventData string, eventMap map[string]interface{}) {
119
-		// 记录所有事件类型(不限于message.part.updated等)
120
-		dataPreview := eventData
121
-		if len(dataPreview) > 200 {
122
-			dataPreview = dataPreview[:200] + "..."
123
-		}
124
-		log.Printf("📡 事件处理: sessionID=%s eventType=%s dataPreview=%s",
125
-			sessionID, eventType, dataPreview)
126
-
127
-		// 函数内部可以决定是否要保存特定类型的事件
128
-		// 例如:只保存message.part.updated事件
129
-		if eventType == "message.part.updated" {
130
-			// 可以在这里提取更多信息并保存
131
-			log.Printf("💾 需要保存的事件类型: %s", eventType)
132
-		}
133
-	})
134
-	log.Printf("事件处理回调函数已设置")
135
-
136 90
 	// 初始化订阅服务单例(暂时注释,使用简单回调方案)
137 91
 	// event.InitSubscriptionService(dispatcher, mongoDBFactory)
138 92
 	// log.Printf("订阅服务单例已初始化")
@@ -185,6 +139,9 @@ func registerRoutes(ws *router.RouterService, webService *webx.WebService, confi
185 139
 	// 日志流路由(需要直接 HTTP 处理器)
186 140
 	routes.RegisterLogStreamRoutes(ws, webService, opencodeProcess, opencodePort)
187 141
 
142
+	// 会话消息路由
143
+	routes.RegisterSessionMessagesRoutes(ws, client)
144
+
188 145
 	// 菜单路由
189 146
 	routes.RegisterMenuRoutes(ws, mappingService)
190 147
 }

+ 6
- 6
test/integration_test.go Zobrazit soubor

@@ -68,12 +68,12 @@ func TestProcessFunctions(t *testing.T) {
68 68
 	t.Logf("✓ 获取可用端口成功: %d", port)
69 69
 }
70 70
 
71
-// TestClientInterface 测试客户端接口一致性
72
-func TestClientInterface(t *testing.T) {
73
-	// 验证 MockClient 实现了 OpenCodeClient 接口
74
-	var _ opencode.OpenCodeClient = opencode.NewMockClient(8080)
75
-	t.Log("✓ MockClient 正确实现了 OpenCodeClient 接口")
76
-}
71
+// // TestClientInterface 测试客户端接口一致性
72
+// func TestClientInterface(t *testing.T) {
73
+// 	// 验证 MockClient 实现了 OpenCodeClient 接口
74
+// 	var _ opencode.OpenCodeClient = opencode.NewMockClient(8080)
75
+// 	t.Log("✓ MockClient 正确实现了 OpenCodeClient 接口")
76
+// }
77 77
 
78 78
 // TestAPIEndpoints 测试 API 端点定义
79 79
 func TestAPIEndpoints(t *testing.T) {

Loading…
Zrušit
Uložit