Add streaming support and refactor Chat UI

- Add run_stream method to AgentCore for streaming output
- Add base_url parameter to LLM clients for OpenRouter support
- Add xbot module for new agent implementation
- Refactor Chat.vue into composable + components (ChatHeader, ChatMessage, ChatInput, ChatSidebar, ChatAgentSelector)
- Add ChatStream handler for SSE streaming in Go server
- Add UseXBot field to chat request

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 10:49:44 +08:00
parent 8062144001
commit 5c435ab21e
31 changed files with 2762 additions and 760 deletions

View File

@@ -10,6 +10,8 @@ import (
"time"
"x-agents/server/internal/repository"
"github.com/gin-gonic/gin"
)
// AgentChatRequest Python Agent 对话请求
@@ -23,6 +25,7 @@ type AgentChatRequest struct {
ModelProvider string `json:"model_provider,omitempty"`
APIKey string `json:"api_key,omitempty"`
BaseURL string `json:"base_url,omitempty"`
UseXBot bool `json:"use_xbot"`
}
// AgentChatResponse Python Agent 对话响应
@@ -186,3 +189,93 @@ func (s *AgentService) TeamChat(req TeamChatRequest) (*TeamChatResponse, error)
return &result, nil
}
// ChatStream 流式对话
func (s *AgentService) ChatStream(c interface{}, agentID int, message, sessionID, modelID string, userID int) error {
// 获取 gin.Context
ginCtx, ok := c.(*gin.Context)
if !ok {
return fmt.Errorf("invalid context type")
}
// 初始化请求体
reqBody := map[string]interface{}{
"agent_id": agentID,
"message": message,
"user_id": userID,
"session_id": sessionID,
"use_xbot": false,
}
// 如果传入了 model_id查询模型配置获取 api_key 和 base_url
if modelID != "" && s.modelRepo != nil {
model, err := s.modelRepo.FindByID(modelID)
if err != nil {
log.Printf("[ChatStream] Model not found: %s, error: %v", modelID, err)
} else if model != nil {
log.Printf("[ChatStream] Using model: provider=%s, model=%s, base_url=%s", model.Provider, model.Model, model.BaseURL)
// 将模型配置添加到请求体
reqBody["model_provider"] = model.Provider
reqBody["model_name"] = model.Model
reqBody["api_key"] = model.APIKey
reqBody["base_url"] = model.BaseURL
}
} else {
log.Printf("[ChatStream] modelID is empty or modelRepo is nil: modelID=%s, modelRepo=%v", modelID, s.modelRepo != nil)
}
streamURL := fmt.Sprintf("%s/agent/chat/stream", s.pythonURL)
jsonData, err := json.Marshal(reqBody)
if err != nil {
return fmt.Errorf("failed to marshal request: %w", err)
}
// 创建 HTTP 请求,设置不缓冲
httpReq, err := http.NewRequest("POST", streamURL, bytes.NewBuffer(jsonData))
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Accept", "text/event-stream")
httpReq.Header.Set("Cache-Control", "no-cache")
// 创建不缓冲的 HTTP 客户端
client := &http.Client{
Transport: &http.Transport{
DisableKeepAlives: true,
},
}
resp, err := client.Do(httpReq)
if err != nil {
return fmt.Errorf("failed to call python agent: %w", err)
}
defer resp.Body.Close()
// 设置 SSE 响应头
ginCtx.Header("Content-Type", "text/event-stream")
ginCtx.Header("Cache-Control", "no-cache")
ginCtx.Header("Connection", "keep-alive")
ginCtx.Header("X-Accel-Buffering", "no")
// 分块读取并转发,使用小 buffer 减少延迟
buf := make([]byte, 1024)
for {
n, err := resp.Body.Read(buf)
if n > 0 {
_, writeErr := ginCtx.Writer.Write(buf[:n])
if writeErr != nil {
break
}
// 强制刷新到客户端
if flusher, ok := ginCtx.Writer.(interface{ Flush() }); ok {
flusher.Flush()
}
}
if err != nil {
break
}
}
return nil
}