Nenhuma descrição
Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

session_routes.go 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. package routes
  2. import (
  3. "context"
  4. "strings"
  5. "git.x2erp.com/qdy/go-base/authbase"
  6. "git.x2erp.com/qdy/go-base/ctx"
  7. "git.x2erp.com/qdy/go-base/logger"
  8. "git.x2erp.com/qdy/go-base/model/response"
  9. "git.x2erp.com/qdy/go-base/util"
  10. "git.x2erp.com/qdy/go-base/webx/router"
  11. "git.x2erp.com/qdy/go-svc-code/internal/model"
  12. "git.x2erp.com/qdy/go-svc-code/internal/opencode"
  13. "git.x2erp.com/qdy/go-svc-code/internal/service"
  14. )
  15. // SessionCreateRequest 创建会话请求
  16. type SessionCreateRequest struct {
  17. Title string `json:"title" binding:"required"`
  18. AgentName string `json:"agent_name" binding:"required"` // 智能体名称
  19. ProjectID string `json:"project_id,omitempty"` // 项目ID(新增,前端生成proj_前缀UUID)
  20. Description string `json:"description,omitempty"` // 项目描述(新增)
  21. }
  22. // SessionUpdateRequest 更新会话请求
  23. type SessionUpdateRequest struct {
  24. Title string `json:"title,omitempty"`
  25. Status string `json:"status,omitempty"`
  26. }
  27. // SessionListQuery 会话列表查询参数
  28. type SessionListQuery struct {
  29. Page int `form:"page" binding:"min=1"` // 页码,从1开始
  30. PageSize int `form:"pageSize" binding:"min=1,max=100"` // 每页大小,最大100
  31. Title string `form:"title,omitempty"` // 标题搜索(模糊匹配)
  32. Status string `form:"status,omitempty"` // 状态筛选
  33. }
  34. // RegisterSessionRoutes 注册会话管理路由
  35. func RegisterSessionRoutes(ws *router.RouterService, client opencode.OpenCodeClient, sessionStore *service.SessionStore, detailStore *service.SessionDetailStore) {
  36. // 创建会话(需要Token认证)
  37. ws.POST("/api/session/create",
  38. func(req *SessionCreateRequest, ctx context.Context, reqCtx *ctx.RequestContext) (*response.QueryResult[*model.Session], error) {
  39. // 从认证信息中获取用户ID和租户ID
  40. userID := reqCtx.UserID
  41. tenantID := reqCtx.TenantID
  42. if userID == "" {
  43. userID = "unknown_user"
  44. logger.Warn("无法从请求上下文获取用户ID,使用默认值")
  45. }
  46. if tenantID == "" {
  47. tenantID = "default"
  48. }
  49. // 1. 验证智能体名称是否有效
  50. if !model.IsValidAgent(req.AgentName) {
  51. // 构建支持智能体列表的错误信息
  52. supportedAgents := ""
  53. for id, name := range model.AgentDisplayName {
  54. supportedAgents += id + "(" + name + ")、"
  55. }
  56. // 移除最后一个"、"
  57. if len(supportedAgents) > 0 {
  58. supportedAgents = supportedAgents[:len(supportedAgents)-len("、")]
  59. }
  60. return &response.QueryResult[*model.Session]{
  61. Success: false,
  62. Message: "无效的智能体名称,支持的智能体有:" + supportedAgents,
  63. }, nil
  64. }
  65. // 2. 首先使用opencodeapi创建一个会话ID
  66. session, err := client.CreateSession(ctx, req.Title)
  67. if err != nil {
  68. logger.Error("创建opencode会话失败", "title", req.Title, "error", err)
  69. return &response.QueryResult[*model.Session]{
  70. Success: false,
  71. Message: "创建会话失败: " + err.Error(),
  72. }, nil
  73. }
  74. // 2. 保存到MongoDB
  75. projectID := req.ProjectID
  76. if projectID == "" {
  77. // 向后兼容:如果没有提供项目ID,使用会话ID作为项目ID
  78. projectID = session.ID
  79. }
  80. dbSession := &model.Session{
  81. ID: session.ID,
  82. ProjectID: projectID,
  83. Title: req.Title,
  84. AgentName: req.AgentName,
  85. Description: req.Description,
  86. Status: model.StatusRequirementDocument, // 默认状态为需求文档
  87. UserID: userID,
  88. TenantID: tenantID,
  89. }
  90. if err := sessionStore.Create(ctx, dbSession); err != nil {
  91. logger.Error("保存会话到数据库失败", "session_id", session.ID, "error", err)
  92. // 注意:opencode会话已创建,但数据库保存失败
  93. // 可以考虑回滚(删除opencode会话),但先记录错误
  94. return &response.QueryResult[*model.Session]{
  95. Success: false,
  96. Message: "保存会话信息失败: " + err.Error(),
  97. }, nil
  98. }
  99. // 3. 创建空的明细文档
  100. detail := &model.SessionDetail{
  101. SessionID: session.ID,
  102. RequirementDoc: "",
  103. TechnicalDoc: "",
  104. CodeItems: []model.CodeItem{},
  105. HistorySessions: []string{session.ID}, // 初始化历史会话数组
  106. }
  107. if err := detailStore.Create(ctx, detail); err != nil {
  108. logger.Warn("创建会话明细失败", "session_id", session.ID, "error", err)
  109. // 明细创建失败不影响主流程,但记录警告
  110. }
  111. logger.Debug("创建会话成功", "session_id", session.ID, "title", req.Title, "agent", req.AgentName)
  112. return util.CreateSuccessResultData(dbSession, reqCtx), nil
  113. },
  114. ).Use(authbase.TokenAuth).Desc("创建新的会话").Register()
  115. // 分页查询会话列表(需要Token认证)
  116. ws.GET("/api/sessions",
  117. func(query *SessionListQuery, ctx context.Context, reqCtx *ctx.RequestContext) (*response.QueryResult[*service.ListResult], error) {
  118. // 处理nil查询参数
  119. if query == nil {
  120. logger.Warn("查询参数为nil,使用默认值")
  121. query = &SessionListQuery{
  122. Page: 1,
  123. PageSize: 20,
  124. }
  125. }
  126. // 设置默认值
  127. if query.Page == 0 {
  128. query.Page = 1
  129. }
  130. if query.PageSize == 0 {
  131. query.PageSize = 20
  132. }
  133. // 从认证信息中获取用户ID和租户ID
  134. userID := reqCtx.UserID
  135. tenantID := reqCtx.TenantID
  136. if userID == "" {
  137. return &response.QueryResult[*service.ListResult]{
  138. Success: false,
  139. Message: "用户认证信息不完整",
  140. }, nil
  141. }
  142. if tenantID == "" {
  143. tenantID = "default"
  144. }
  145. // 构建查询参数
  146. listQuery := &service.ListQuery{
  147. Page: query.Page,
  148. PageSize: query.PageSize,
  149. Title: strings.TrimSpace(query.Title),
  150. Status: strings.TrimSpace(query.Status),
  151. UserID: userID,
  152. TenantID: tenantID,
  153. }
  154. // 查询会话列表
  155. result, err := sessionStore.List(ctx, listQuery)
  156. if err != nil {
  157. logger.Error("查询会话列表失败", "error", err)
  158. return &response.QueryResult[*service.ListResult]{
  159. Success: false,
  160. Message: "查询会话列表失败: " + err.Error(),
  161. }, nil
  162. }
  163. return util.CreateSuccessResultData(result, reqCtx), nil
  164. },
  165. ).Use(authbase.TokenAuth).Desc("分页查询会话列表,支持按标题搜索和按状态筛选").Register()
  166. // 获取单个会话详情(需要Token认证)
  167. ws.GET("/api/session/{id}",
  168. func(id string, ctx context.Context, reqCtx *ctx.RequestContext) (*response.QueryResult[*model.SessionWithDetail], error) {
  169. if id == "" {
  170. return &response.QueryResult[*model.SessionWithDetail]{
  171. Success: false,
  172. Message: "参数 id 不能为空",
  173. }, nil
  174. }
  175. // 1. 获取主会话
  176. session, err := sessionStore.GetByID(ctx, id)
  177. if err != nil {
  178. logger.Error("获取会话失败", "session_id", id, "error", err)
  179. return &response.QueryResult[*model.SessionWithDetail]{
  180. Success: false,
  181. Message: "获取会话失败: " + err.Error(),
  182. }, nil
  183. }
  184. if session == nil {
  185. return &response.QueryResult[*model.SessionWithDetail]{
  186. Success: false,
  187. Message: "会话不存在",
  188. }, nil
  189. }
  190. // 2. 获取明细文档
  191. detail, err := detailStore.GetBySessionID(ctx, id)
  192. if err != nil {
  193. logger.Error("获取会话明细失败", "session_id", id, "error", err)
  194. return &response.QueryResult[*model.SessionWithDetail]{
  195. Success: false,
  196. Message: "获取会话明细失败: " + err.Error(),
  197. }, nil
  198. }
  199. // 3. 获取历史会话上下文(从opencode获取)
  200. var historyCount int
  201. messages, err := client.GetSessionMessages(ctx, id, 50) // 获取最近50条消息
  202. if err != nil {
  203. logger.Warn("获取会话历史消息失败", "session_id", id, "error", err)
  204. // 历史消息获取失败不影响主流程,继续返回其他数据
  205. } else {
  206. historyCount = len(messages)
  207. }
  208. // 4. 组合响应
  209. result := &model.SessionWithDetail{
  210. Session: session,
  211. Detail: detail,
  212. HistoryCount: historyCount,
  213. }
  214. return util.CreateSuccessResultData(result, reqCtx), nil
  215. },
  216. ).Use(authbase.TokenAuth).Desc("获取会话详情(包括主对象、明细文档和历史消息数量)").Register()
  217. // 更新会话(状态和标题)(需要Token认证)
  218. ws.PUT("/api/session/{id}",
  219. func(id string, req *SessionUpdateRequest, ctx context.Context, reqCtx *ctx.RequestContext) (*response.QueryResult[bool], error) {
  220. if id == "" {
  221. return &response.QueryResult[bool]{
  222. Success: false,
  223. Message: "参数 id 不能为空",
  224. }, nil
  225. }
  226. // 验证请求数据
  227. if req.Title == "" && req.Status == "" {
  228. return &response.QueryResult[bool]{
  229. Success: false,
  230. Message: "至少需要提供标题或状态中的一个",
  231. }, nil
  232. }
  233. // 更新会话
  234. if err := sessionStore.Update(ctx, id, req.Title, req.Status); err != nil {
  235. logger.Error("更新会话失败", "session_id", id, "error", err)
  236. return &response.QueryResult[bool]{
  237. Success: false,
  238. Message: "更新会话失败: " + err.Error(),
  239. }, nil
  240. }
  241. return util.CreateSuccessResultData(true, reqCtx), nil
  242. },
  243. ).Use(authbase.TokenAuth).Desc("更新会话状态和标题(已发布的会话不能修改)").Register()
  244. // 删除会话(需要Token认证)
  245. ws.DELETE("/api/session/{id}",
  246. func(id string, ctx context.Context, reqCtx *ctx.RequestContext) (*response.QueryResult[bool], error) {
  247. if id == "" {
  248. return &response.QueryResult[bool]{
  249. Success: false,
  250. Message: "参数 id 不能为空",
  251. }, nil
  252. }
  253. // 1. 从MongoDB删除主会话
  254. if err := sessionStore.Delete(ctx, id); err != nil {
  255. logger.Error("删除会话失败", "session_id", id, "error", err)
  256. return &response.QueryResult[bool]{
  257. Success: false,
  258. Message: "删除会话失败: " + err.Error(),
  259. }, nil
  260. }
  261. // 2. 删除明细文档
  262. if err := detailStore.DeleteBySessionID(ctx, id); err != nil {
  263. logger.Warn("删除会话明细失败", "session_id", id, "error", err)
  264. // 明细删除失败不影响主流程,但记录警告
  265. }
  266. // 3. 调用opencode-api删除会话
  267. // 注意:opencode API可能不支持直接删除会话,这里暂时记录日志
  268. logger.Debug("需要调用opencode-api删除会话", "session_id", id)
  269. // TODO: 实现opencode会话删除
  270. // if err := client.DeleteSession(ctx, id); err != nil {
  271. // logger.Warn("删除opencode会话失败", "session_id", id, "error", err)
  272. // }
  273. return util.CreateSuccessResultData(true, reqCtx), nil
  274. },
  275. ).Use(authbase.TokenAuth).Desc("删除会话(同时删除明细文档,已发布的会话不能删除)").Register()
  276. }