package routes import ( "context" "strings" "git.x2erp.com/qdy/go-base/authbase" "git.x2erp.com/qdy/go-base/ctx" "git.x2erp.com/qdy/go-base/logger" "git.x2erp.com/qdy/go-base/model/response" "git.x2erp.com/qdy/go-base/util" "git.x2erp.com/qdy/go-base/webx/router" "git.x2erp.com/qdy/go-svc-code/internal/model" "git.x2erp.com/qdy/go-svc-code/internal/opencode" "git.x2erp.com/qdy/go-svc-code/internal/service" ) // SessionCreateRequest 创建会话请求 type SessionCreateRequest struct { Title string `json:"title" binding:"required"` AgentName string `json:"agent_name" binding:"required"` // 智能体名称 ProjectID string `json:"project_id,omitempty"` // 项目ID(新增,前端生成proj_前缀UUID) Description string `json:"description,omitempty"` // 项目描述(新增) } // SessionUpdateRequest 更新会话请求 type SessionUpdateRequest struct { Title string `json:"title,omitempty"` Status string `json:"status,omitempty"` } // SessionListQuery 会话列表查询参数 type SessionListQuery struct { Page int `form:"page" binding:"min=1"` // 页码,从1开始 PageSize int `form:"pageSize" binding:"min=1,max=100"` // 每页大小,最大100 Title string `form:"title,omitempty"` // 标题搜索(模糊匹配) Status string `form:"status,omitempty"` // 状态筛选 } // RegisterSessionRoutes 注册会话管理路由 func RegisterSessionRoutes(ws *router.RouterService, client opencode.OpenCodeClient, sessionStore *service.SessionStore, detailStore *service.SessionDetailStore) { // 创建会话(需要Token认证) ws.POST("/api/session/create", func(req *SessionCreateRequest, ctx context.Context, reqCtx *ctx.RequestContext) (*response.QueryResult[*model.Session], error) { // 从认证信息中获取用户ID和租户ID userID := reqCtx.UserID tenantID := reqCtx.TenantID if userID == "" { userID = "unknown_user" logger.Warn("无法从请求上下文获取用户ID,使用默认值") } if tenantID == "" { tenantID = "default" } // 1. 验证智能体名称是否有效 if !model.IsValidAgent(req.AgentName) { // 构建支持智能体列表的错误信息 supportedAgents := "" for id, name := range model.AgentDisplayName { supportedAgents += id + "(" + name + ")、" } // 移除最后一个"、" if len(supportedAgents) > 0 { supportedAgents = supportedAgents[:len(supportedAgents)-len("、")] } return &response.QueryResult[*model.Session]{ Success: false, Message: "无效的智能体名称,支持的智能体有:" + supportedAgents, }, nil } // 2. 首先使用opencodeapi创建一个会话ID session, err := client.CreateSession(ctx, req.Title) if err != nil { logger.Error("创建opencode会话失败", "title", req.Title, "error", err) return &response.QueryResult[*model.Session]{ Success: false, Message: "创建会话失败: " + err.Error(), }, nil } // 2. 保存到MongoDB projectID := req.ProjectID if projectID == "" { // 向后兼容:如果没有提供项目ID,使用会话ID作为项目ID projectID = session.ID } dbSession := &model.Session{ ID: session.ID, ProjectID: projectID, Title: req.Title, AgentName: req.AgentName, Description: req.Description, Status: model.StatusRequirementDocument, // 默认状态为需求文档 UserID: userID, TenantID: tenantID, } if err := sessionStore.Create(ctx, dbSession); err != nil { logger.Error("保存会话到数据库失败", "session_id", session.ID, "error", err) // 注意:opencode会话已创建,但数据库保存失败 // 可以考虑回滚(删除opencode会话),但先记录错误 return &response.QueryResult[*model.Session]{ Success: false, Message: "保存会话信息失败: " + err.Error(), }, nil } // 3. 创建空的明细文档 detail := &model.SessionDetail{ SessionID: session.ID, RequirementDoc: "", TechnicalDoc: "", CodeItems: []model.CodeItem{}, HistorySessions: []string{session.ID}, // 初始化历史会话数组 } if err := detailStore.Create(ctx, detail); err != nil { logger.Warn("创建会话明细失败", "session_id", session.ID, "error", err) // 明细创建失败不影响主流程,但记录警告 } logger.Debug("创建会话成功", "session_id", session.ID, "title", req.Title, "agent", req.AgentName) return util.CreateSuccessResultData(dbSession, reqCtx), nil }, ).Use(authbase.TokenAuth).Desc("创建新的会话").Register() // 分页查询会话列表(需要Token认证) ws.GET("/api/sessions", func(query *SessionListQuery, ctx context.Context, reqCtx *ctx.RequestContext) (*response.QueryResult[*service.ListResult], error) { // 处理nil查询参数 if query == nil { logger.Warn("查询参数为nil,使用默认值") query = &SessionListQuery{ Page: 1, PageSize: 20, } } // 设置默认值 if query.Page == 0 { query.Page = 1 } if query.PageSize == 0 { query.PageSize = 20 } // 从认证信息中获取用户ID和租户ID userID := reqCtx.UserID tenantID := reqCtx.TenantID if userID == "" { return &response.QueryResult[*service.ListResult]{ Success: false, Message: "用户认证信息不完整", }, nil } if tenantID == "" { tenantID = "default" } // 构建查询参数 listQuery := &service.ListQuery{ Page: query.Page, PageSize: query.PageSize, Title: strings.TrimSpace(query.Title), Status: strings.TrimSpace(query.Status), UserID: userID, TenantID: tenantID, } // 查询会话列表 result, err := sessionStore.List(ctx, listQuery) if err != nil { logger.Error("查询会话列表失败", "error", err) return &response.QueryResult[*service.ListResult]{ Success: false, Message: "查询会话列表失败: " + err.Error(), }, nil } return util.CreateSuccessResultData(result, reqCtx), nil }, ).Use(authbase.TokenAuth).Desc("分页查询会话列表,支持按标题搜索和按状态筛选").Register() // 获取单个会话详情(需要Token认证) ws.GET("/api/session/{id}", func(id string, ctx context.Context, reqCtx *ctx.RequestContext) (*response.QueryResult[*model.SessionWithDetail], error) { if id == "" { return &response.QueryResult[*model.SessionWithDetail]{ Success: false, Message: "参数 id 不能为空", }, nil } // 1. 获取主会话 session, err := sessionStore.GetByID(ctx, id) if err != nil { logger.Error("获取会话失败", "session_id", id, "error", err) return &response.QueryResult[*model.SessionWithDetail]{ Success: false, Message: "获取会话失败: " + err.Error(), }, nil } if session == nil { return &response.QueryResult[*model.SessionWithDetail]{ Success: false, Message: "会话不存在", }, nil } // 2. 获取明细文档 detail, err := detailStore.GetBySessionID(ctx, id) if err != nil { logger.Error("获取会话明细失败", "session_id", id, "error", err) return &response.QueryResult[*model.SessionWithDetail]{ Success: false, Message: "获取会话明细失败: " + err.Error(), }, nil } // 3. 获取历史会话上下文(从opencode获取) var historyCount int messages, err := client.GetSessionMessages(ctx, id, 50) // 获取最近50条消息 if err != nil { logger.Warn("获取会话历史消息失败", "session_id", id, "error", err) // 历史消息获取失败不影响主流程,继续返回其他数据 } else { historyCount = len(messages) } // 4. 组合响应 result := &model.SessionWithDetail{ Session: session, Detail: detail, HistoryCount: historyCount, } return util.CreateSuccessResultData(result, reqCtx), nil }, ).Use(authbase.TokenAuth).Desc("获取会话详情(包括主对象、明细文档和历史消息数量)").Register() // 更新会话(状态和标题)(需要Token认证) ws.PUT("/api/session/{id}", func(id string, req *SessionUpdateRequest, ctx context.Context, reqCtx *ctx.RequestContext) (*response.QueryResult[bool], error) { if id == "" { return &response.QueryResult[bool]{ Success: false, Message: "参数 id 不能为空", }, nil } // 验证请求数据 if req.Title == "" && req.Status == "" { return &response.QueryResult[bool]{ Success: false, Message: "至少需要提供标题或状态中的一个", }, nil } // 更新会话 if err := sessionStore.Update(ctx, id, req.Title, req.Status); err != nil { logger.Error("更新会话失败", "session_id", id, "error", err) return &response.QueryResult[bool]{ Success: false, Message: "更新会话失败: " + err.Error(), }, nil } return util.CreateSuccessResultData(true, reqCtx), nil }, ).Use(authbase.TokenAuth).Desc("更新会话状态和标题(已发布的会话不能修改)").Register() // 删除会话(需要Token认证) ws.DELETE("/api/session/{id}", func(id string, ctx context.Context, reqCtx *ctx.RequestContext) (*response.QueryResult[bool], error) { if id == "" { return &response.QueryResult[bool]{ Success: false, Message: "参数 id 不能为空", }, nil } // 1. 从MongoDB删除主会话 if err := sessionStore.Delete(ctx, id); err != nil { logger.Error("删除会话失败", "session_id", id, "error", err) return &response.QueryResult[bool]{ Success: false, Message: "删除会话失败: " + err.Error(), }, nil } // 2. 删除明细文档 if err := detailStore.DeleteBySessionID(ctx, id); err != nil { logger.Warn("删除会话明细失败", "session_id", id, "error", err) // 明细删除失败不影响主流程,但记录警告 } // 3. 调用opencode-api删除会话 // 注意:opencode API可能不支持直接删除会话,这里暂时记录日志 logger.Debug("需要调用opencode-api删除会话", "session_id", id) // TODO: 实现opencode会话删除 // if err := client.DeleteSession(ctx, id); err != nil { // logger.Warn("删除opencode会话失败", "session_id", id, "error", err) // } return util.CreateSuccessResultData(true, reqCtx), nil }, ).Use(authbase.TokenAuth).Desc("删除会话(同时删除明细文档,已发布的会话不能删除)").Register() }