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:
@@ -333,11 +333,15 @@ func main() {
|
||||
|
||||
// 7. 设置路由
|
||||
r := gin.New()
|
||||
|
||||
// 添加日志和恢复中间件
|
||||
r.Use(gin.Logger())
|
||||
r.Use(gin.Recovery())
|
||||
|
||||
// 禁用响应缓冲,用于流式输出
|
||||
r.Use(func(c *gin.Context) {
|
||||
c.Header("X-Accel-Buffering", "no")
|
||||
c.Next()
|
||||
})
|
||||
|
||||
// 请求日志中间件
|
||||
r.Use(func(c *gin.Context) {
|
||||
start := time.Now()
|
||||
@@ -495,6 +499,7 @@ func main() {
|
||||
agentGroup := r.Group("/api/agent")
|
||||
{
|
||||
agentGroup.POST("/chat", agentHandler.Chat)
|
||||
agentGroup.POST("/chat/stream", agentHandler.ChatStream)
|
||||
agentGroup.POST("/team/chat", agentHandler.TeamChat)
|
||||
}
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ func InitDB(cfg *Config) (*gorm.DB, error) {
|
||||
}
|
||||
|
||||
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Info),
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect database: %w", err)
|
||||
|
||||
@@ -26,6 +26,7 @@ type ChatRequest struct {
|
||||
Message string `json:"message" binding:"required"`
|
||||
SessionID string `json:"session_id"`
|
||||
ModelID string `json:"model_id"`
|
||||
UseXBot bool `json:"use_xbot"`
|
||||
}
|
||||
|
||||
// ChatResponse 对话响应
|
||||
@@ -56,6 +57,7 @@ func (h *AgentHandler) Chat(c *gin.Context) {
|
||||
UserID: userID,
|
||||
SessionID: req.SessionID,
|
||||
ModelID: req.ModelID,
|
||||
UseXBot: req.UseXBot,
|
||||
}
|
||||
|
||||
result, err := h.agentService.Chat(pythonReq)
|
||||
@@ -85,6 +87,30 @@ func (h *AgentHandler) Chat(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// ChatStream 单智能体对话(流式输出)
|
||||
func (h *AgentHandler) ChatStream(c *gin.Context) {
|
||||
var req ChatRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取用户 ID
|
||||
userID := 1 // TODO: 从 c.Get("user_id") 获取
|
||||
|
||||
// 构建 SSE 流
|
||||
c.Header("Content-Type", "text/event-stream")
|
||||
c.Header("Cache-Control", "no-cache")
|
||||
c.Header("Connection", "keep-alive")
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
|
||||
// 调用 Python 服务的流式端点
|
||||
err := h.agentService.ChatStream(c, req.AgentID, req.Message, req.SessionID, req.ModelID, userID)
|
||||
if err != nil && !c.IsAborted() {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
}
|
||||
}
|
||||
|
||||
// TeamChatRequest 多智能体群聊请求
|
||||
type TeamChatRequest struct {
|
||||
SupervisorAgentID int `json:"supervisor_agent_id" binding:"required"`
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -41,66 +41,75 @@ func (s *SkillService) UpdateSkill(skill *model.Skill) error {
|
||||
}
|
||||
|
||||
func (s *SkillService) DeleteSkill(id string) error {
|
||||
return s.skillRepo.Delete(id)
|
||||
// 先获取 skill 信息,以便删除本地文件
|
||||
skill, err := s.skillRepo.FindByID(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 删除数据库记录
|
||||
if err := s.skillRepo.Delete(id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 删除本地文件(skill 目录)
|
||||
if skill.Path != "" {
|
||||
// 获取 skill 所在目录(SKILL.md 的父目录)
|
||||
skillDir := filepath.Dir(skill.Path)
|
||||
if err := os.RemoveAll(skillDir); err != nil {
|
||||
log.Printf("[SkillService] Warning: failed to delete skill directory %s: %v", skillDir, err)
|
||||
// 数据库记录已删除,不返回错误
|
||||
} else {
|
||||
log.Printf("[SkillService] Deleted skill directory: %s", skillDir)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// InitSkills 初始化扫描所有 skills 目录
|
||||
func (s *SkillService) InitSkills() error {
|
||||
log.Println("[SkillService] Starting init skills...")
|
||||
|
||||
// 获取项目根目录
|
||||
projectRoot := s.getProjectRoot()
|
||||
if projectRoot == "" {
|
||||
log.Println("[SkillService] Cannot determine project root, skipping skill init")
|
||||
return nil
|
||||
}
|
||||
|
||||
var totalCount int
|
||||
|
||||
// 扫描 system skills: account/admin/skills
|
||||
systemSkillsPath := filepath.Join(projectRoot, "account", "admin", "skills")
|
||||
if _, err := os.Stat(systemSkillsPath); err == nil {
|
||||
log.Printf("[SkillService] Scanning system skills from: %s", systemSkillsPath)
|
||||
systemSkills, err := s.scanSkillsDirectory(systemSkillsPath, "system")
|
||||
if err != nil {
|
||||
log.Printf("[SkillService] Error scanning system skills: %v", err)
|
||||
} else {
|
||||
log.Printf("[SkillService] Found %d system skills", len(systemSkills))
|
||||
// 先删除旧的 system skills
|
||||
if err == nil && len(systemSkills) > 0 {
|
||||
s.skillRepo.DeleteByType("system")
|
||||
// 批量插入
|
||||
if err := s.skillRepo.UpsertBatch(systemSkills); err != nil {
|
||||
log.Printf("[SkillService] Error saving system skills: %v", err)
|
||||
}
|
||||
s.skillRepo.UpsertBatch(systemSkills)
|
||||
totalCount += len(systemSkills)
|
||||
}
|
||||
}
|
||||
|
||||
// 扫描 user skills: account/{username}/skills (除了 admin)
|
||||
accountPath := filepath.Join(projectRoot, "account")
|
||||
entries, err := os.ReadDir(accountPath)
|
||||
if err != nil {
|
||||
log.Printf("[SkillService] Error reading account directory: %v", err)
|
||||
} else {
|
||||
if err == nil {
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() || entry.Name() == "admin" {
|
||||
continue
|
||||
}
|
||||
userSkillsPath := filepath.Join(accountPath, entry.Name(), "skills")
|
||||
if _, err := os.Stat(userSkillsPath); err == nil {
|
||||
log.Printf("[SkillService] Scanning user skills for %s from: %s", entry.Name(), userSkillsPath)
|
||||
userSkills, err := s.scanSkillsDirectory(userSkillsPath, "user")
|
||||
if err != nil {
|
||||
log.Printf("[SkillService] Error scanning user skills for %s: %v", entry.Name(), err)
|
||||
} else {
|
||||
log.Printf("[SkillService] Found %d user skills for %s", len(userSkills), entry.Name())
|
||||
// 批量插入
|
||||
if err := s.skillRepo.UpsertBatch(userSkills); err != nil {
|
||||
log.Printf("[SkillService] Error saving user skills for %s: %v", entry.Name(), err)
|
||||
}
|
||||
if err == nil && len(userSkills) > 0 {
|
||||
s.skillRepo.UpsertBatch(userSkills)
|
||||
totalCount += len(userSkills)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("[SkillService] Skills initialized successfully")
|
||||
if totalCount > 0 {
|
||||
log.Printf("[SkillService] Loaded %d skills", totalCount)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -56,23 +56,18 @@ func (s *ToolService) DeleteTool(id string) error {
|
||||
|
||||
// InitDefaultTools 初始化默认工具到数据库
|
||||
func (s *ToolService) InitDefaultTools() error {
|
||||
log.Println("[ToolService] Starting init default tools...")
|
||||
|
||||
// 删除现有的系统工具,重新插入
|
||||
s.toolRepo.DB().Where("provider = ?", "system").Delete(&model.Tool{})
|
||||
|
||||
// 插入默认工具
|
||||
tools := s.getDefaultTools()
|
||||
log.Printf("[ToolService] Inserting %d default tools...", len(tools))
|
||||
|
||||
for _, tool := range tools {
|
||||
if err := s.toolRepo.Create(&tool); err != nil {
|
||||
log.Printf("[ToolService] Create tool error: %v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("[ToolService] Default tools initialized successfully")
|
||||
log.Printf("[ToolService] Loaded %d default tools", len(tools))
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user