diff --git a/backend/internal/api/contract/session.go b/backend/internal/api/contract/session.go new file mode 100644 index 0000000..e6dadd0 --- /dev/null +++ b/backend/internal/api/contract/session.go @@ -0,0 +1,25 @@ +package contract + +import "context" + +// SessionService 定义会话服务接口 +type SessionService interface { + // Session CRUD + CreateSession(ctx context.Context, req *CreateSessionRequest) (*Session, error) + GetSession(ctx context.Context, id uint, sessionID string) (*Session, error) + UpdateSession(ctx context.Context, id uint, req *UpdateSessionRequest) (*Session, error) + DeleteSession(ctx context.Context, id uint) error + ListSessions(ctx context.Context, req *ListSessionsRequest) (*SessionList, error) + + // Lifecycle management + ActivateSession(ctx context.Context, id uint) error + PauseSession(ctx context.Context, id uint) error + EndSession(ctx context.Context, id uint) error + ResumeSession(ctx context.Context, id uint) error + + // Message management + AddMessage(ctx context.Context, sessionID uint, req *AddMessageRequest) (*SessionMessage, error) + GetSessionMessages(ctx context.Context, sessionID uint, page, perPage int) (*MessageList, error) + DeleteMessage(ctx context.Context, messageID uint) error + ClearSessionMessages(ctx context.Context, sessionID uint) error +} diff --git a/backend/internal/api/contract/session_type.go b/backend/internal/api/contract/session_type.go new file mode 100644 index 0000000..2ecf127 --- /dev/null +++ b/backend/internal/api/contract/session_type.go @@ -0,0 +1,99 @@ +package contract + +import ( + "time" + + "github.com/insmtx/SingerOS/backend/types" +) + +// CreateSessionRequest 创建会话请求 +type CreateSessionRequest struct { + SessionID string `json:"session_id,omitempty"` + Type string `json:"type" binding:"required"` + UserID uint `json:"user_id,omitempty"` + AssistantID uint `json:"assistant_id,omitempty"` + AssistantCode string `json:"assistant_code,omitempty"` + Title string `json:"title,omitempty"` + Metadata *types.SessionMetadata `json:"metadata,omitempty"` + ExpiredAt *time.Time `json:"expired_at,omitempty"` +} + +// UpdateSessionRequest 更新会话请求 +type UpdateSessionRequest struct { + Title string `json:"title,omitempty"` + Metadata *types.SessionMetadata `json:"metadata,omitempty"` + ExpiredAt *time.Time `json:"expired_at,omitempty"` +} + +// ListSessionsRequest 查询会话列表请求 +type ListSessionsRequest struct { + Type *string `form:"type,omitempty"` + Status *string `form:"status,omitempty"` + UserID *uint `form:"user_id,omitempty"` + AssistantID *uint `form:"assistant_id,omitempty"` + AssistantCode *string `form:"assistant_code,omitempty"` + Keyword *string `form:"keyword,omitempty"` + Page int `form:"page,default=1"` + PerPage int `form:"per_page,default=20"` +} + +// AddMessageRequest 添加消息请求 +type AddMessageRequest struct { + Role string `json:"role" binding:"required"` + Content string `json:"content" binding:"required"` + MessageType string `json:"message_type,omitempty"` + Status string `json:"status,omitempty"` + Chunks []string `json:"chunks,omitempty"` + Thinking string `json:"thinking,omitempty"` + ToolCalls []types.ToolCall `json:"tool_calls,omitempty"` + Metadata *types.MessageMetadata `json:"metadata,omitempty"` +} + +// Session 会话响应结构(对应前端的 Conversation) +type Session struct { + ID uint `json:"id"` + SessionID string `json:"session_id"` + Type string `json:"type"` + UserID uint `json:"user_id"` + AssistantID uint `json:"assistant_id"` + AssistantCode string `json:"assistant_code"` + Status string `json:"status"` + Title string `json:"title"` + Metadata *types.SessionMetadata `json:"metadata,omitempty"` + MessageCount int `json:"message_count"` + LastMessageAt *time.Time `json:"last_message_at,omitempty"` + ExpiredAt *time.Time `json:"expired_at,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// SessionMessage 消息响应结构(对齐前端 Message 模型) +type SessionMessage struct { + ID string `json:"id"` // 前端用 string + SessionID string `json:"conversation_id"` // 对应前端的 conversationId + Role string `json:"role"` + Content string `json:"content"` + Chunks []string `json:"chunks,omitempty"` // 流式片段 + Status string `json:"status"` // sending/streaming/complete/error + Timestamp int64 `json:"timestamp"` // Unix 毫秒时间戳 + ToolCalls []types.ToolCall `json:"tool_calls,omitempty"` + Thinking string `json:"thinking,omitempty"` // 思维链 + MessageType string `json:"message_type,omitempty"` + Metadata *types.MessageMetadata `json:"metadata,omitempty"` + Sequence int64 `json:"sequence"` + CreatedAt time.Time `json:"created_at"` +} + +// SessionList 会话列表响应 +type SessionList struct { + Total int64 `json:"total"` + Page int `json:"page"` + Items []Session `json:"items"` +} + +// MessageList 消息列表响应 +type MessageList struct { + Total int64 `json:"total"` + Page int `json:"page"` + Items []SessionMessage `json:"items"` +} diff --git a/backend/internal/api/dto/session.go b/backend/internal/api/dto/session.go new file mode 100644 index 0000000..76d3a17 --- /dev/null +++ b/backend/internal/api/dto/session.go @@ -0,0 +1 @@ +package dto diff --git a/backend/internal/api/handler/session_handler.go b/backend/internal/api/handler/session_handler.go new file mode 100644 index 0000000..3f50b0e --- /dev/null +++ b/backend/internal/api/handler/session_handler.go @@ -0,0 +1,417 @@ +package handler + +import ( + "net/http" + + "github.com/gin-gonic/gin" + + "github.com/insmtx/SingerOS/backend/internal/api/contract" + "github.com/insmtx/SingerOS/backend/internal/api/dto" +) + +type SessionHandler struct { + service contract.SessionService +} + +func NewSessionHandler(service contract.SessionService) *SessionHandler { + return &SessionHandler{ + service: service, + } +} + +func (h *SessionHandler) RegisterRoutes(r gin.IRouter) { + r.POST("/CreateSession", h.CreateSession) + r.POST("/GetSession", h.GetSession) + r.POST("/UpdateSession", h.UpdateSession) + r.POST("/DeleteSession", h.DeleteSession) + r.POST("/ListSessions", h.ListSessions) + r.POST("/ActivateSession", h.ActivateSession) + r.POST("/PauseSession", h.PauseSession) + r.POST("/EndSession", h.EndSession) + r.POST("/ResumeSession", h.ResumeSession) + r.POST("/AddMessage", h.AddMessage) + r.POST("/GetSessionMessages", h.GetSessionMessages) + r.POST("/DeleteMessage", h.DeleteMessage) + r.POST("/ClearSessionMessages", h.ClearSessionMessages) +} + +func RegisterSessionRoutes(r gin.IRouter, service contract.SessionService) { + h := NewSessionHandler(service) + h.RegisterRoutes(r) +} + +type CreateSessionRequest struct { + contract.CreateSessionRequest +} + +// @Summary 创建会话 +// @Description 创建一个新的会话实例 +// @Tags Session +// @Accept json +// @Produce json +// @Param body body contract.CreateSessionRequest true "创建会话请求" +// @Success 200 {object} dto.Response "成功响应" +// @Failure 400 {object} dto.ErrorResponse "请求参数错误" +// @Failure 500 {object} dto.ErrorResponse "内部服务器错误" +// @Router /CreateSession [post] +func (h *SessionHandler) CreateSession(ctx *gin.Context) { + var req contract.CreateSessionRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + ctx.JSON(http.StatusBadRequest, dto.Error(dto.CodeInvalidParams, err.Error())) + return + } + + result, err := h.service.CreateSession(ctx, &req) + if err != nil { + ctx.JSON(http.StatusInternalServerError, dto.Error(dto.CodeInternalError, err.Error())) + return + } + + ctx.JSON(http.StatusOK, dto.Success(result)) +} + +type GetSessionRequest struct { + ID *uint `json:"id,omitempty"` + SessionID *string `json:"session_id,omitempty"` +} + +// @Summary 获取会话详情 +// @Description 根据ID或SessionID获取会话详情 +// @Tags Session +// @Accept json +// @Produce json +// @Param body body GetSessionRequest true "获取会话请求" +// @Success 200 {object} dto.Response "成功响应" +// @Failure 400 {object} dto.ErrorResponse "请求参数错误" +// @Failure 404 {object} dto.ErrorResponse "资源不存在" +// @Failure 500 {object} dto.ErrorResponse "内部服务器错误" +// @Router /GetSession [post] +func (h *SessionHandler) GetSession(ctx *gin.Context) { + var req GetSessionRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + ctx.JSON(http.StatusBadRequest, dto.Error(dto.CodeInvalidParams, err.Error())) + return + } + + var id uint + var sessionID string + + if req.ID != nil { + id = *req.ID + } + if req.SessionID != nil { + sessionID = *req.SessionID + } + + result, err := h.service.GetSession(ctx, id, sessionID) + if err != nil { + if err.Error() == "session not found" { + ctx.JSON(http.StatusNotFound, dto.Error(dto.CodeNotFound, err.Error())) + return + } + ctx.JSON(http.StatusInternalServerError, dto.Error(dto.CodeInternalError, err.Error())) + return + } + + ctx.JSON(http.StatusOK, dto.Success(result)) +} + +type UpdateSessionRequest struct { + ID uint `json:"id" binding:"required"` + contract.UpdateSessionRequest +} + +// @Summary 更新会话 +// @Description 更新会话基本信息 +// @Tags Session +// @Accept json +// @Produce json +// @Param body body UpdateSessionRequest true "更新会话请求" +// @Success 200 {object} dto.Response "成功响应" +// @Failure 400 {object} dto.ErrorResponse "请求参数错误" +// @Failure 404 {object} dto.ErrorResponse "资源不存在" +// @Failure 500 {object} dto.ErrorResponse "内部服务器错误" +// @Router /UpdateSession [post] +func (h *SessionHandler) UpdateSession(ctx *gin.Context) { + var req UpdateSessionRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + ctx.JSON(http.StatusBadRequest, dto.Error(dto.CodeInvalidParams, err.Error())) + return + } + + result, err := h.service.UpdateSession(ctx, req.ID, &req.UpdateSessionRequest) + if err != nil { + if err.Error() == "session not found" { + ctx.JSON(http.StatusNotFound, dto.Error(dto.CodeNotFound, err.Error())) + return + } + ctx.JSON(http.StatusInternalServerError, dto.Error(dto.CodeInternalError, err.Error())) + return + } + + ctx.JSON(http.StatusOK, dto.Success(result)) +} + +type DeleteSessionRequest struct { + ID uint `json:"id" binding:"required"` +} + +// @Summary 删除会话 +// @Description 根据ID删除会话 +// @Tags Session +// @Accept json +// @Produce json +// @Param body body DeleteSessionRequest true "删除会话请求" +// @Success 200 {object} dto.Response "成功响应" +// @Failure 400 {object} dto.ErrorResponse "请求参数错误" +// @Failure 404 {object} dto.ErrorResponse "资源不存在" +// @Failure 500 {object} dto.ErrorResponse "内部服务器错误" +// @Router /DeleteSession [post] +func (h *SessionHandler) DeleteSession(ctx *gin.Context) { + var req DeleteSessionRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + ctx.JSON(http.StatusBadRequest, dto.Error(dto.CodeInvalidParams, err.Error())) + return + } + + err := h.service.DeleteSession(ctx, req.ID) + if err != nil { + if err.Error() == "session not found" { + ctx.JSON(http.StatusNotFound, dto.Error(dto.CodeNotFound, err.Error())) + return + } + ctx.JSON(http.StatusInternalServerError, dto.Error(dto.CodeInternalError, err.Error())) + return + } + + ctx.JSON(http.StatusOK, dto.Success(nil)) +} + +// @Summary 查询会话列表 +// @Description 分页查询会话列表 +// @Tags Session +// @Accept json +// @Produce json +// @Param body body contract.ListSessionsRequest true "查询列表请求" +// @Success 200 {object} dto.Response "成功响应" +// @Failure 400 {object} dto.ErrorResponse "请求参数错误" +// @Failure 500 {object} dto.ErrorResponse "内部服务器错误" +// @Router /ListSessions [post] +func (h *SessionHandler) ListSessions(ctx *gin.Context) { + var req contract.ListSessionsRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + ctx.JSON(http.StatusBadRequest, dto.Error(dto.CodeInvalidParams, err.Error())) + return + } + + result, err := h.service.ListSessions(ctx, &req) + if err != nil { + ctx.JSON(http.StatusInternalServerError, dto.Error(dto.CodeInternalError, err.Error())) + return + } + + ctx.JSON(http.StatusOK, dto.Success(result)) +} + +type SessionIDRequest struct { + ID uint `json:"id" binding:"required"` +} + +// @Summary 激活会话 +// @Description 激活已结束会话 +// @Tags Session +// @Accept json +// @Produce json +// @Param body body SessionIDRequest true "激活会话请求" +// @Success 200 {object} dto.Response "成功响应" +// @Failure 400 {object} dto.ErrorResponse "请求参数错误" +// @Failure 404 {object} dto.ErrorResponse "资源不存在" +// @Failure 500 {object} dto.ErrorResponse "内部服务器错误" +// @Router /ActivateSession [post] +func (h *SessionHandler) ActivateSession(ctx *gin.Context) { + var req SessionIDRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + ctx.JSON(http.StatusBadRequest, dto.Error(dto.CodeInvalidParams, err.Error())) + return + } + + err := h.service.ActivateSession(ctx, req.ID) + if err != nil { + if err.Error() == "session not found" { + ctx.JSON(http.StatusNotFound, dto.Error(dto.CodeNotFound, err.Error())) + return + } + ctx.JSON(http.StatusInternalServerError, dto.Error(dto.CodeInternalError, err.Error())) + return + } + + ctx.JSON(http.StatusOK, dto.Success(nil)) +} + +func (h *SessionHandler) PauseSession(ctx *gin.Context) { + var req SessionIDRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + ctx.JSON(http.StatusBadRequest, dto.Error(dto.CodeInvalidParams, err.Error())) + return + } + + err := h.service.PauseSession(ctx, req.ID) + if err != nil { + if err.Error() == "session not found" { + ctx.JSON(http.StatusNotFound, dto.Error(dto.CodeNotFound, err.Error())) + return + } + ctx.JSON(http.StatusInternalServerError, dto.Error(dto.CodeInternalError, err.Error())) + return + } + + ctx.JSON(http.StatusOK, dto.Success(nil)) +} + +func (h *SessionHandler) EndSession(ctx *gin.Context) { + var req SessionIDRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + ctx.JSON(http.StatusBadRequest, dto.Error(dto.CodeInvalidParams, err.Error())) + return + } + + err := h.service.EndSession(ctx, req.ID) + if err != nil { + if err.Error() == "session not found" { + ctx.JSON(http.StatusNotFound, dto.Error(dto.CodeNotFound, err.Error())) + return + } + ctx.JSON(http.StatusInternalServerError, dto.Error(dto.CodeInternalError, err.Error())) + return + } + + ctx.JSON(http.StatusOK, dto.Success(nil)) +} + +func (h *SessionHandler) ResumeSession(ctx *gin.Context) { + var req SessionIDRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + ctx.JSON(http.StatusBadRequest, dto.Error(dto.CodeInvalidParams, err.Error())) + return + } + + err := h.service.ResumeSession(ctx, req.ID) + if err != nil { + if err.Error() == "session not found" { + ctx.JSON(http.StatusNotFound, dto.Error(dto.CodeNotFound, err.Error())) + return + } + ctx.JSON(http.StatusInternalServerError, dto.Error(dto.CodeInternalError, err.Error())) + return + } + + ctx.JSON(http.StatusOK, dto.Success(nil)) +} + +type AddMessageRequest struct { + SessionID uint `json:"session_id" binding:"required"` + contract.AddMessageRequest +} + +func (h *SessionHandler) AddMessage(ctx *gin.Context) { + var req AddMessageRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + ctx.JSON(http.StatusBadRequest, dto.Error(dto.CodeInvalidParams, err.Error())) + return + } + + result, err := h.service.AddMessage(ctx, req.SessionID, &req.AddMessageRequest) + if err != nil { + if err.Error() == "session not found" { + ctx.JSON(http.StatusNotFound, dto.Error(dto.CodeNotFound, err.Error())) + return + } + ctx.JSON(http.StatusInternalServerError, dto.Error(dto.CodeInternalError, err.Error())) + return + } + + ctx.JSON(http.StatusOK, dto.Success(result)) +} + +type GetSessionMessagesRequest struct { + SessionID uint `json:"session_id" binding:"required"` + Page int `json:"page,omitempty"` + PerPage int `json:"per_page,omitempty"` +} + +func (h *SessionHandler) GetSessionMessages(ctx *gin.Context) { + var req GetSessionMessagesRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + ctx.JSON(http.StatusBadRequest, dto.Error(dto.CodeInvalidParams, err.Error())) + return + } + + page := req.Page + if page == 0 { + page = 1 + } + perPage := req.PerPage + if perPage == 0 { + perPage = 20 + } + + result, err := h.service.GetSessionMessages(ctx, req.SessionID, page, perPage) + if err != nil { + if err.Error() == "session not found" { + ctx.JSON(http.StatusNotFound, dto.Error(dto.CodeNotFound, err.Error())) + return + } + ctx.JSON(http.StatusInternalServerError, dto.Error(dto.CodeInternalError, err.Error())) + return + } + + ctx.JSON(http.StatusOK, dto.Success(result)) +} + +type DeleteMessageRequest struct { + MessageID uint `json:"message_id" binding:"required"` +} + +func (h *SessionHandler) DeleteMessage(ctx *gin.Context) { + var req DeleteMessageRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + ctx.JSON(http.StatusBadRequest, dto.Error(dto.CodeInvalidParams, err.Error())) + return + } + + err := h.service.DeleteMessage(ctx, req.MessageID) + if err != nil { + if err.Error() == "message not found" { + ctx.JSON(http.StatusNotFound, dto.Error(dto.CodeNotFound, err.Error())) + return + } + ctx.JSON(http.StatusInternalServerError, dto.Error(dto.CodeInternalError, err.Error())) + return + } + + ctx.JSON(http.StatusOK, dto.Success(nil)) +} + +type ClearSessionMessagesRequest struct { + SessionID uint `json:"session_id" binding:"required"` +} + +func (h *SessionHandler) ClearSessionMessages(ctx *gin.Context) { + var req ClearSessionMessagesRequest + if err := ctx.ShouldBindJSON(&req); err != nil { + ctx.JSON(http.StatusBadRequest, dto.Error(dto.CodeInvalidParams, err.Error())) + return + } + + err := h.service.ClearSessionMessages(ctx, req.SessionID) + if err != nil { + if err.Error() == "session not found" { + ctx.JSON(http.StatusNotFound, dto.Error(dto.CodeNotFound, err.Error())) + return + } + ctx.JSON(http.StatusInternalServerError, dto.Error(dto.CodeInternalError, err.Error())) + return + } + + ctx.JSON(http.StatusOK, dto.Success(nil)) +} diff --git a/backend/internal/api/router.go b/backend/internal/api/router.go index d07ad10..0063645 100644 --- a/backend/internal/api/router.go +++ b/backend/internal/api/router.go @@ -71,6 +71,10 @@ func SetupRouter(cfg config.Config, publisher eventbus.Publisher, db *gorm.DB) * digitalAssistantService := service.NewDigitalAssistantService(db, workerScheduler) handler.RegisterDigitalAssistantRoutes(v1, digitalAssistantService) logs.Info("Digital assistant routes registered successfully") + + sessionService := service.NewSessionService(db) + handler.RegisterSessionRoutes(v1, sessionService) + logs.Info("Session routes registered successfully") } { diff --git a/backend/internal/infra/db/database.go b/backend/internal/infra/db/database.go index 74af4aa..527ddbe 100644 --- a/backend/internal/infra/db/database.go +++ b/backend/internal/infra/db/database.go @@ -56,6 +56,8 @@ func runMigrations(db *gorm.DB) error { &types.Skill{}, &types.SkillRegistry{}, &types.SkillExecutionLog{}, + &types.Session{}, + &types.SessionMessage{}, } if err := dbtools.InitModel(db, models...); err != nil { diff --git a/backend/internal/infra/db/session_dao.go b/backend/internal/infra/db/session_dao.go new file mode 100644 index 0000000..3e74fb1 --- /dev/null +++ b/backend/internal/infra/db/session_dao.go @@ -0,0 +1,145 @@ +package db + +import ( + "context" + "errors" + "time" + + "gorm.io/gorm" + + "github.com/insmtx/SingerOS/backend/types" +) + +// CreateSession 创建会话 +func CreateSession(ctx context.Context, db *gorm.DB, session *types.Session) error { + return db.WithContext(ctx).Create(session).Error +} + +// GetSessionByID 根据ID获取会话 +func GetSessionByID(ctx context.Context, db *gorm.DB, id uint) (*types.Session, error) { + var entity types.Session + err := db.WithContext(ctx).First(&entity, id).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + return nil, err + } + return &entity, nil +} + +// GetSessionBySessionID 根据SessionID获取会话 +func GetSessionBySessionID(ctx context.Context, db *gorm.DB, sessionID string) (*types.Session, error) { + var entity types.Session + err := db.WithContext(ctx).Where("session_id = ?", sessionID).First(&entity).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + return nil, err + } + return &entity, nil +} + +// UpdateSession 更新会话 +func UpdateSession(ctx context.Context, db *gorm.DB, session *types.Session) error { + return db.WithContext(ctx).Save(session).Error +} + +// DeleteSession 删除会话(软删除) +func DeleteSession(ctx context.Context, db *gorm.DB, id uint) error { + return db.WithContext(ctx).Delete(&types.Session{}, id).Error +} + +// ActivateSession 激活会话 +func ActivateSession(ctx context.Context, db *gorm.DB, id uint) error { + return db.WithContext(ctx).Model(&types.Session{}).Where("id = ?", id).Update("status", string(types.SessionStatusActive)).Error +} + +// PauseSession 暂停会话 +func PauseSession(ctx context.Context, db *gorm.DB, id uint) error { + return db.WithContext(ctx).Model(&types.Session{}).Where("id = ?", id).Update("status", string(types.SessionStatusPaused)).Error +} + +// EndSession 结束会话 +func EndSession(ctx context.Context, db *gorm.DB, id uint) error { + return db.WithContext(ctx).Model(&types.Session{}).Where("id = ?", id).Update("status", string(types.SessionStatusEnded)).Error +} + +// ResumeSession 恢复会话 +func ResumeSession(ctx context.Context, db *gorm.DB, id uint) error { + return db.WithContext(ctx).Model(&types.Session{}).Where("id = ?", id).Update("status", string(types.SessionStatusActive)).Error +} + +// ExpireSessions 批量标记过期会话 +func ExpireSessions(ctx context.Context, db *gorm.DB) error { + now := time.Now() + return db.WithContext(ctx).Model(&types.Session{}). + Where("status = ? AND expired_at IS NOT NULL AND expired_at <= ?", + string(types.SessionStatusActive), now). + Update("status", string(types.SessionStatusExpired)).Error +} + +// ListSessions 分页查询会话列表 +func ListSessions(ctx context.Context, db *gorm.DB, sessionType *string, status *string, userID *uint, assistantID *uint, assistantCode *string, keyword *string, page, perPage int) ([]*types.Session, int64, error) { + var entities []*types.Session + var total int64 + + query := db.WithContext(ctx).Model(&types.Session{}) + + if sessionType != nil && *sessionType != "" { + query = query.Where("type = ?", *sessionType) + } + if status != nil && *status != "" { + query = query.Where("status = ?", *status) + } + if userID != nil && *userID > 0 { + query = query.Where("user_id = ?", *userID) + } + if assistantID != nil && *assistantID > 0 { + query = query.Where("assistant_id = ?", *assistantID) + } + if assistantCode != nil && *assistantCode != "" { + query = query.Where("assistant_code = ?", *assistantCode) + } + if keyword != nil && *keyword != "" { + query = query.Where("title LIKE ? OR session_id LIKE ?", "%"+*keyword+"%", "%"+*keyword+"%") + } + + err := query.Count(&total).Error + if err != nil { + return nil, 0, err + } + + offset := (page - 1) * perPage + err = query.Offset(offset).Limit(perPage).Order("created_at DESC").Find(&entities).Error + if err != nil { + return nil, 0, err + } + + return entities, total, nil +} + +// SessionIDExists 检查session_id是否存在(排除指定ID) +func SessionIDExists(ctx context.Context, db *gorm.DB, sessionID string, excludeID uint) (bool, error) { + var count int64 + query := db.WithContext(ctx).Model(&types.Session{}).Where("session_id = ?", sessionID) + if excludeID > 0 { + query = query.Where("id != ?", excludeID) + } + err := query.Count(&count).Error + if err != nil { + return false, err + } + return count > 0, nil +} + +// IncrementMessageCount 增加会话消息计数 +func IncrementMessageCount(ctx context.Context, db *gorm.DB, id uint) error { + return db.WithContext(ctx).Model(&types.Session{}).Where("id = ?", id).UpdateColumn("message_count", db.Raw("message_count + 1")).Error +} + +// UpdateLastMessageAt 更新会话最后消息时间 +func UpdateLastMessageAt(ctx context.Context, db *gorm.DB, id uint, lastMessageAt time.Time) error { + return db.WithContext(ctx).Model(&types.Session{}).Where("id = ?", id).Update("last_message_at", lastMessageAt).Error +} diff --git a/backend/internal/infra/db/session_dao_test.go b/backend/internal/infra/db/session_dao_test.go new file mode 100644 index 0000000..535bd2c --- /dev/null +++ b/backend/internal/infra/db/session_dao_test.go @@ -0,0 +1,552 @@ +package db + +import ( + "context" + "testing" + "time" + + "gorm.io/driver/sqlite" + "gorm.io/gorm" + + "github.com/insmtx/SingerOS/backend/types" +) + +func setupTestDB(t *testing.T) *gorm.DB { + t.Helper() + + db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) + if err != nil { + t.Fatalf("failed to open test database: %v", err) + } + + if err := db.AutoMigrate(&types.Session{}, &types.SessionMessage{}); err != nil { + t.Fatalf("failed to migrate test database: %v", err) + } + + return db +} + +func TestCreateSession(t *testing.T) { + db := setupTestDB(t) + ctx := context.Background() + + session := &types.Session{ + SessionID: "test_session_1", + Type: string(types.SessionTypeUserChat), + UserID: 1, + Title: "Test Session", + } + + err := CreateSession(ctx, db, session) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + + if session.ID == 0 { + t.Error("expected session ID to be set") + } + + if session.CreatedAt.IsZero() { + t.Error("expected CreatedAt to be set") + } +} + +func TestCreateSession_DuplicateSessionID(t *testing.T) { + db := setupTestDB(t) + ctx := context.Background() + + session1 := &types.Session{ + SessionID: "duplicate_id", + Type: string(types.SessionTypeUserChat), + } + + err := CreateSession(ctx, db, session1) + if err != nil { + t.Fatalf("first CreateSession failed: %v", err) + } + + session2 := &types.Session{ + SessionID: "duplicate_id", + Type: string(types.SessionTypeUserChat), + } + + err = CreateSession(ctx, db, session2) + if err == nil { + t.Error("expected error for duplicate session_id") + } +} + +func TestGetSessionByID(t *testing.T) { + db := setupTestDB(t) + ctx := context.Background() + + session := &types.Session{ + SessionID: "get_by_id_test", + Type: string(types.SessionTypeUserChat), + UserID: 1, + Title: "Get By ID Test", + } + + if err := CreateSession(ctx, db, session); err != nil { + t.Fatalf("failed to create session: %v", err) + } + + retrieved, err := GetSessionByID(ctx, db, session.ID) + if err != nil { + t.Fatalf("GetSessionByID failed: %v", err) + } + + if retrieved == nil { + t.Fatal("expected session to be found") + } + + if retrieved.SessionID != session.SessionID { + t.Errorf("expected SessionID %s, got %s", session.SessionID, retrieved.SessionID) + } +} + +func TestGetSessionByID_NotFound(t *testing.T) { + db := setupTestDB(t) + ctx := context.Background() + + retrieved, err := GetSessionByID(ctx, db, 999) + if err != nil { + t.Fatalf("GetSessionByID failed: %v", err) + } + + if retrieved != nil { + t.Error("expected nil for non-existent session") + } +} + +func TestGetSessionBySessionID(t *testing.T) { + db := setupTestDB(t) + ctx := context.Background() + + session := &types.Session{ + SessionID: "get_by_session_id_test", + Type: string(types.SessionTypeUserChat), + Title: "Get By SessionID Test", + } + + if err := CreateSession(ctx, db, session); err != nil { + t.Fatalf("failed to create session: %v", err) + } + + retrieved, err := GetSessionBySessionID(ctx, db, session.SessionID) + if err != nil { + t.Fatalf("GetSessionBySessionID failed: %v", err) + } + + if retrieved == nil { + t.Fatal("expected session to be found") + } + + if retrieved.ID != session.ID { + t.Errorf("expected ID %d, got %d", session.ID, retrieved.ID) + } +} + +func TestUpdateSession(t *testing.T) { + db := setupTestDB(t) + ctx := context.Background() + + session := &types.Session{ + SessionID: "update_test", + Type: string(types.SessionTypeUserChat), + Title: "Original Title", + } + + if err := CreateSession(ctx, db, session); err != nil { + t.Fatalf("failed to create session: %v", err) + } + + session.Title = "Updated Title" + err := UpdateSession(ctx, db, session) + if err != nil { + t.Fatalf("UpdateSession failed: %v", err) + } + + retrieved, err := GetSessionByID(ctx, db, session.ID) + if err != nil { + t.Fatalf("GetSessionByID failed: %v", err) + } + + if retrieved.Title != "Updated Title" { + t.Errorf("expected title to be updated, got %s", retrieved.Title) + } +} + +func TestDeleteSession(t *testing.T) { + db := setupTestDB(t) + ctx := context.Background() + + session := &types.Session{ + SessionID: "delete_test", + Type: string(types.SessionTypeUserChat), + } + + if err := CreateSession(ctx, db, session); err != nil { + t.Fatalf("failed to create session: %v", err) + } + + err := DeleteSession(ctx, db, session.ID) + if err != nil { + t.Fatalf("DeleteSession failed: %v", err) + } + + retrieved, err := GetSessionByID(ctx, db, session.ID) + if err != nil { + t.Fatalf("GetSessionByID failed: %v", err) + } + + if retrieved != nil { + t.Error("expected session to be deleted") + } +} + +func TestActivateSession(t *testing.T) { + db := setupTestDB(t) + ctx := context.Background() + + session := &types.Session{ + SessionID: "activate_test", + Type: string(types.SessionTypeUserChat), + Status: string(types.SessionStatusEnded), + } + + if err := CreateSession(ctx, db, session); err != nil { + t.Fatalf("failed to create session: %v", err) + } + + err := ActivateSession(ctx, db, session.ID) + if err != nil { + t.Fatalf("ActivateSession failed: %v", err) + } + + retrieved, err := GetSessionByID(ctx, db, session.ID) + if err != nil { + t.Fatalf("GetSessionByID failed: %v", err) + } + + if retrieved.Status != string(types.SessionStatusActive) { + t.Errorf("expected status to be active, got %s", retrieved.Status) + } +} + +func TestPauseSession(t *testing.T) { + db := setupTestDB(t) + ctx := context.Background() + + session := &types.Session{ + SessionID: "pause_test", + Type: string(types.SessionTypeUserChat), + Status: string(types.SessionStatusActive), + } + + if err := CreateSession(ctx, db, session); err != nil { + t.Fatalf("failed to create session: %v", err) + } + + err := PauseSession(ctx, db, session.ID) + if err != nil { + t.Fatalf("PauseSession failed: %v", err) + } + + retrieved, err := GetSessionByID(ctx, db, session.ID) + if err != nil { + t.Fatalf("GetSessionByID failed: %v", err) + } + + if retrieved.Status != string(types.SessionStatusPaused) { + t.Errorf("expected status to be paused, got %s", retrieved.Status) + } +} + +func TestEndSession(t *testing.T) { + db := setupTestDB(t) + ctx := context.Background() + + session := &types.Session{ + SessionID: "end_test", + Type: string(types.SessionTypeUserChat), + Status: string(types.SessionStatusActive), + } + + if err := CreateSession(ctx, db, session); err != nil { + t.Fatalf("failed to create session: %v", err) + } + + err := EndSession(ctx, db, session.ID) + if err != nil { + t.Fatalf("EndSession failed: %v", err) + } + + retrieved, err := GetSessionByID(ctx, db, session.ID) + if err != nil { + t.Fatalf("GetSessionByID failed: %v", err) + } + + if retrieved.Status != string(types.SessionStatusEnded) { + t.Errorf("expected status to be ended, got %s", retrieved.Status) + } +} + +func TestResumeSession(t *testing.T) { + db := setupTestDB(t) + ctx := context.Background() + + session := &types.Session{ + SessionID: "resume_test", + Type: string(types.SessionTypeUserChat), + Status: string(types.SessionStatusPaused), + } + + if err := CreateSession(ctx, db, session); err != nil { + t.Fatalf("failed to create session: %v", err) + } + + err := ResumeSession(ctx, db, session.ID) + if err != nil { + t.Fatalf("ResumeSession failed: %v", err) + } + + retrieved, err := GetSessionByID(ctx, db, session.ID) + if err != nil { + t.Fatalf("GetSessionByID failed: %v", err) + } + + if retrieved.Status != string(types.SessionStatusActive) { + t.Errorf("expected status to be active, got %s", retrieved.Status) + } +} + +func TestExpireSessions(t *testing.T) { + db := setupTestDB(t) + ctx := context.Background() + + expiredAt := time.Now().Add(-1 * time.Hour) + session := &types.Session{ + SessionID: "expire_test", + Type: string(types.SessionTypeUserChat), + Status: string(types.SessionStatusActive), + ExpiredAt: &expiredAt, + } + + if err := CreateSession(ctx, db, session); err != nil { + t.Fatalf("failed to create session: %v", err) + } + + err := ExpireSessions(ctx, db) + if err != nil { + t.Fatalf("ExpireSessions failed: %v", err) + } + + retrieved, err := GetSessionByID(ctx, db, session.ID) + if err != nil { + t.Fatalf("GetSessionByID failed: %v", err) + } + + if retrieved.Status != string(types.SessionStatusExpired) { + t.Errorf("expected status to be expired, got %s", retrieved.Status) + } +} + +func TestListSessions_ByType(t *testing.T) { + db := setupTestDB(t) + ctx := context.Background() + + session1 := &types.Session{ + SessionID: "type_test_1", + Type: string(types.SessionTypeUserChat), + } + session2 := &types.Session{ + SessionID: "type_test_2", + Type: string(types.SessionTypeAssistantInstance), + } + + if err := CreateSession(ctx, db, session1); err != nil { + t.Fatalf("failed to create session1: %v", err) + } + if err := CreateSession(ctx, db, session2); err != nil { + t.Fatalf("failed to create session2: %v", err) + } + + typeFilter := string(types.SessionTypeUserChat) + sessions, total, err := ListSessions(ctx, db, &typeFilter, nil, nil, nil, nil, nil, 1, 20) + if err != nil { + t.Fatalf("ListSessions failed: %v", err) + } + + if total != 1 { + t.Errorf("expected 1 session, got %d", total) + } + + if len(sessions) != 1 { + t.Errorf("expected 1 session, got %d", len(sessions)) + } + + if sessions[0].Type != string(types.SessionTypeUserChat) { + t.Errorf("expected user_chat type, got %s", sessions[0].Type) + } +} + +func TestListSessions_ByStatus(t *testing.T) { + db := setupTestDB(t) + ctx := context.Background() + + session1 := &types.Session{ + SessionID: "status_test_1", + Type: string(types.SessionTypeUserChat), + Status: string(types.SessionStatusActive), + } + session2 := &types.Session{ + SessionID: "status_test_2", + Type: string(types.SessionTypeUserChat), + Status: string(types.SessionStatusPaused), + } + + if err := CreateSession(ctx, db, session1); err != nil { + t.Fatalf("failed to create session1: %v", err) + } + if err := CreateSession(ctx, db, session2); err != nil { + t.Fatalf("failed to create session2: %v", err) + } + + statusFilter := string(types.SessionStatusActive) + sessions, total, err := ListSessions(ctx, db, nil, &statusFilter, nil, nil, nil, nil, 1, 20) + if err != nil { + t.Fatalf("ListSessions failed: %v", err) + } + + if total != 1 { + t.Errorf("expected 1 session, got %d", total) + } + + if sessions[0].Status != string(types.SessionStatusActive) { + t.Errorf("expected active status, got %s", sessions[0].Status) + } +} + +func TestListSessions_ByKeyword(t *testing.T) { + db := setupTestDB(t) + ctx := context.Background() + + session1 := &types.Session{ + SessionID: "keyword_test_1", + Type: string(types.SessionTypeUserChat), + Title: "Project Alpha", + } + session2 := &types.Session{ + SessionID: "keyword_test_2", + Type: string(types.SessionTypeUserChat), + Title: "Project Beta", + } + + if err := CreateSession(ctx, db, session1); err != nil { + t.Fatalf("failed to create session1: %v", err) + } + if err := CreateSession(ctx, db, session2); err != nil { + t.Fatalf("failed to create session2: %v", err) + } + + keyword := "Alpha" + sessions, total, err := ListSessions(ctx, db, nil, nil, nil, nil, nil, &keyword, 1, 20) + if err != nil { + t.Fatalf("ListSessions failed: %v", err) + } + + if total != 1 { + t.Errorf("expected 1 session, got %d", total) + } + + if sessions[0].Title != "Project Alpha" { + t.Errorf("expected Project Alpha, got %s", sessions[0].Title) + } +} + +func TestListSessions_Pagination(t *testing.T) { + db := setupTestDB(t) + ctx := context.Background() + + for i := 1; i <= 5; i++ { + session := &types.Session{ + SessionID: "pagination_test_" + string(rune(i)), + Type: string(types.SessionTypeUserChat), + } + if err := CreateSession(ctx, db, session); err != nil { + t.Fatalf("failed to create session %d: %v", i, err) + } + } + + sessions, total, err := ListSessions(ctx, db, nil, nil, nil, nil, nil, nil, 1, 2) + if err != nil { + t.Fatalf("ListSessions failed: %v", err) + } + + if total != 5 { + t.Errorf("expected total 5, got %d", total) + } + + if len(sessions) != 2 { + t.Errorf("expected 2 sessions on page 1, got %d", len(sessions)) + } +} + +func TestIncrementMessageCount(t *testing.T) { + db := setupTestDB(t) + ctx := context.Background() + + session := &types.Session{ + SessionID: "increment_test", + Type: string(types.SessionTypeUserChat), + MessageCount: 0, + } + + if err := CreateSession(ctx, db, session); err != nil { + t.Fatalf("failed to create session: %v", err) + } + + err := IncrementMessageCount(ctx, db, session.ID) + if err != nil { + t.Fatalf("IncrementMessageCount failed: %v", err) + } + + retrieved, err := GetSessionByID(ctx, db, session.ID) + if err != nil { + t.Fatalf("GetSessionByID failed: %v", err) + } + + if retrieved.MessageCount != 1 { + t.Errorf("expected message_count to be 1, got %d", retrieved.MessageCount) + } +} + +func TestUpdateLastMessageAt(t *testing.T) { + db := setupTestDB(t) + ctx := context.Background() + + session := &types.Session{ + SessionID: "last_message_test", + Type: string(types.SessionTypeUserChat), + } + + if err := CreateSession(ctx, db, session); err != nil { + t.Fatalf("failed to create session: %v", err) + } + + now := time.Now() + err := UpdateLastMessageAt(ctx, db, session.ID, now) + if err != nil { + t.Fatalf("UpdateLastMessageAt failed: %v", err) + } + + retrieved, err := GetSessionByID(ctx, db, session.ID) + if err != nil { + t.Fatalf("GetSessionByID failed: %v", err) + } + + if retrieved.LastMessageAt == nil { + t.Error("expected LastMessageAt to be set") + } +} diff --git a/backend/internal/infra/db/session_message_dao.go b/backend/internal/infra/db/session_message_dao.go new file mode 100644 index 0000000..d519249 --- /dev/null +++ b/backend/internal/infra/db/session_message_dao.go @@ -0,0 +1,97 @@ +package db + +import ( + "context" + "errors" + + "gorm.io/gorm" + + "github.com/insmtx/SingerOS/backend/types" +) + +// CreateMessage 创建消息 +func CreateMessage(ctx context.Context, db *gorm.DB, message *types.SessionMessage) error { + return db.WithContext(ctx).Create(message).Error +} + +// GetMessageByID 根据ID获取消息 +func GetMessageByID(ctx context.Context, db *gorm.DB, id uint) (*types.SessionMessage, error) { + var entity types.SessionMessage + err := db.WithContext(ctx).First(&entity, id).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + return nil, err + } + return &entity, nil +} + +// GetSessionMessages 查询会话的所有消息(按 sequence 排序,支持分页) +func GetSessionMessages(ctx context.Context, db *gorm.DB, sessionID string, page, perPage int) ([]*types.SessionMessage, int64, error) { + var entities []*types.SessionMessage + var total int64 + + query := db.WithContext(ctx).Model(&types.SessionMessage{}).Where("session_id = ?", sessionID) + + err := query.Count(&total).Error + if err != nil { + return nil, 0, err + } + + offset := (page - 1) * perPage + err = query.Offset(offset).Limit(perPage).Order("sequence ASC").Find(&entities).Error + if err != nil { + return nil, 0, err + } + + return entities, total, nil +} + +// DeleteMessage 软删除消息 +func DeleteMessage(ctx context.Context, db *gorm.DB, id uint) error { + return db.WithContext(ctx).Delete(&types.SessionMessage{}, id).Error +} + +// ClearSessionMessages 清空会话的所有消息(软删除) +func ClearSessionMessages(ctx context.Context, db *gorm.DB, sessionID string) error { + return db.WithContext(ctx).Where("session_id = ?", sessionID).Delete(&types.SessionMessage{}).Error +} + +// GetLatestMessage 获取会话的最新消息 +func GetLatestMessage(ctx context.Context, db *gorm.DB, sessionID string) (*types.SessionMessage, error) { + var entity types.SessionMessage + err := db.WithContext(ctx).Where("session_id = ?", sessionID).Order("sequence DESC").First(&entity).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + return nil, err + } + return &entity, nil +} + +// GetMessageCount 获取会话的消息数量 +func GetMessageCount(ctx context.Context, db *gorm.DB, sessionID string) (int64, error) { + var count int64 + err := db.WithContext(ctx).Model(&types.SessionMessage{}).Where("session_id = ?", sessionID).Count(&count).Error + if err != nil { + return 0, err + } + return count, nil +} + +// GetNextSequence 获取会话的下一个消息序号 +func GetNextSequence(ctx context.Context, db *gorm.DB, sessionID string) (int64, error) { + var maxSequence int64 + err := db.WithContext(ctx).Model(&types.SessionMessage{}).Where("session_id = ?", sessionID).Select("COALESCE(MAX(sequence), 0)").Scan(&maxSequence).Error + if err != nil { + return 0, err + } + return maxSequence + 1, nil +} + +// UpdateMessageSequence 更新消息序号 +func UpdateMessageSequence(ctx context.Context, db *gorm.DB, messageID uint, sequence int64) error { + return db.WithContext(ctx).Model(&types.SessionMessage{}).Where("id = ?", messageID).Update("sequence", sequence).Error +} diff --git a/backend/internal/infra/db/session_message_dao_test.go b/backend/internal/infra/db/session_message_dao_test.go new file mode 100644 index 0000000..07dc791 --- /dev/null +++ b/backend/internal/infra/db/session_message_dao_test.go @@ -0,0 +1,369 @@ +package db + +import ( + "context" + "testing" + + "github.com/insmtx/SingerOS/backend/types" +) + +func TestCreateMessage(t *testing.T) { + db := setupTestDB(t) + ctx := context.Background() + + session := &types.Session{ + SessionID: "create_msg_session", + Type: string(types.SessionTypeUserChat), + } + + if err := CreateSession(ctx, db, session); err != nil { + t.Fatalf("failed to create session: %v", err) + } + + message := &types.SessionMessage{ + SessionID: session.SessionID, + Role: string(types.MessageRoleUser), + Content: "Test message", + Sequence: 1, + } + + err := CreateMessage(ctx, db, message) + if err != nil { + t.Fatalf("CreateMessage failed: %v", err) + } + + if message.ID == 0 { + t.Error("expected message ID to be set") + } +} + +func TestGetMessageByID(t *testing.T) { + db := setupTestDB(t) + ctx := context.Background() + + session := &types.Session{ + SessionID: "get_msg_session", + Type: string(types.SessionTypeUserChat), + } + + if err := CreateSession(ctx, db, session); err != nil { + t.Fatalf("failed to create session: %v", err) + } + + message := &types.SessionMessage{ + SessionID: session.SessionID, + Role: string(types.MessageRoleUser), + Content: "Get message test", + Sequence: 1, + } + + if err := CreateMessage(ctx, db, message); err != nil { + t.Fatalf("failed to create message: %v", err) + } + + retrieved, err := GetMessageByID(ctx, db, message.ID) + if err != nil { + t.Fatalf("GetMessageByID failed: %v", err) + } + + if retrieved == nil { + t.Fatal("expected message to be found") + } + + if retrieved.Content != "Get message test" { + t.Errorf("expected content 'Get message test', got %s", retrieved.Content) + } +} + +func TestGetSessionMessages(t *testing.T) { + db := setupTestDB(t) + ctx := context.Background() + + session := &types.Session{ + SessionID: "get_messages_session", + Type: string(types.SessionTypeUserChat), + } + + if err := CreateSession(ctx, db, session); err != nil { + t.Fatalf("failed to create session: %v", err) + } + + for i := 1; i <= 3; i++ { + message := &types.SessionMessage{ + SessionID: session.SessionID, + Role: string(types.MessageRoleUser), + Content: "Message " + string(rune(i)), + Sequence: int64(i), + } + if err := CreateMessage(ctx, db, message); err != nil { + t.Fatalf("failed to create message %d: %v", i, err) + } + } + + messages, total, err := GetSessionMessages(ctx, db, session.SessionID, 1, 20) + if err != nil { + t.Fatalf("GetSessionMessages failed: %v", err) + } + + if total != 3 { + t.Errorf("expected 3 messages, got %d", total) + } + + if len(messages) != 3 { + t.Errorf("expected 3 messages, got %d", len(messages)) + } + + if messages[0].Sequence != 1 { + t.Errorf("expected messages to be sorted by sequence, got %d", messages[0].Sequence) + } +} + +func TestGetSessionMessages_Pagination(t *testing.T) { + db := setupTestDB(t) + ctx := context.Background() + + session := &types.Session{ + SessionID: "pagination_msg_session", + Type: string(types.SessionTypeUserChat), + } + + if err := CreateSession(ctx, db, session); err != nil { + t.Fatalf("failed to create session: %v", err) + } + + for i := 1; i <= 5; i++ { + message := &types.SessionMessage{ + SessionID: session.SessionID, + Role: string(types.MessageRoleUser), + Content: "Message " + string(rune(i)), + Sequence: int64(i), + } + if err := CreateMessage(ctx, db, message); err != nil { + t.Fatalf("failed to create message %d: %v", i, err) + } + } + + messages, total, err := GetSessionMessages(ctx, db, session.SessionID, 1, 2) + if err != nil { + t.Fatalf("GetSessionMessages failed: %v", err) + } + + if total != 5 { + t.Errorf("expected total 5, got %d", total) + } + + if len(messages) != 2 { + t.Errorf("expected 2 messages on page 1, got %d", len(messages)) + } +} + +func TestDeleteMessage(t *testing.T) { + db := setupTestDB(t) + ctx := context.Background() + + session := &types.Session{ + SessionID: "delete_msg_session", + Type: string(types.SessionTypeUserChat), + } + + if err := CreateSession(ctx, db, session); err != nil { + t.Fatalf("failed to create session: %v", err) + } + + message := &types.SessionMessage{ + SessionID: session.SessionID, + Role: string(types.MessageRoleUser), + Content: "Delete message test", + Sequence: 1, + } + + if err := CreateMessage(ctx, db, message); err != nil { + t.Fatalf("failed to create message: %v", err) + } + + err := DeleteMessage(ctx, db, message.ID) + if err != nil { + t.Fatalf("DeleteMessage failed: %v", err) + } + + retrieved, err := GetMessageByID(ctx, db, message.ID) + if err != nil { + t.Fatalf("GetMessageByID failed: %v", err) + } + + if retrieved != nil { + t.Error("expected message to be deleted") + } +} + +func TestClearSessionMessages(t *testing.T) { + db := setupTestDB(t) + ctx := context.Background() + + session := &types.Session{ + SessionID: "clear_msg_session", + Type: string(types.SessionTypeUserChat), + } + + if err := CreateSession(ctx, db, session); err != nil { + t.Fatalf("failed to create session: %v", err) + } + + for i := 1; i <= 3; i++ { + message := &types.SessionMessage{ + SessionID: session.SessionID, + Role: string(types.MessageRoleUser), + Content: "Message " + string(rune(i)), + Sequence: int64(i), + } + if err := CreateMessage(ctx, db, message); err != nil { + t.Fatalf("failed to create message %d: %v", i, err) + } + } + + err := ClearSessionMessages(ctx, db, session.SessionID) + if err != nil { + t.Fatalf("ClearSessionMessages failed: %v", err) + } + + count, err := GetMessageCount(ctx, db, session.SessionID) + if err != nil { + t.Fatalf("GetMessageCount failed: %v", err) + } + + if count != 0 { + t.Errorf("expected 0 messages after clear, got %d", count) + } +} + +func TestGetLatestMessage(t *testing.T) { + db := setupTestDB(t) + ctx := context.Background() + + session := &types.Session{ + SessionID: "latest_msg_session", + Type: string(types.SessionTypeUserChat), + } + + if err := CreateSession(ctx, db, session); err != nil { + t.Fatalf("failed to create session: %v", err) + } + + for i := 1; i <= 3; i++ { + message := &types.SessionMessage{ + SessionID: session.SessionID, + Role: string(types.MessageRoleUser), + Content: "Message " + string(rune(i)), + Sequence: int64(i), + } + if err := CreateMessage(ctx, db, message); err != nil { + t.Fatalf("failed to create message %d: %v", i, err) + } + } + + latest, err := GetLatestMessage(ctx, db, session.SessionID) + if err != nil { + t.Fatalf("GetLatestMessage failed: %v", err) + } + + if latest == nil { + t.Fatal("expected latest message to be found") + } + + if latest.Sequence != 3 { + t.Errorf("expected latest message sequence to be 3, got %d", latest.Sequence) + } +} + +func TestGetMessageCount(t *testing.T) { + db := setupTestDB(t) + ctx := context.Background() + + session := &types.Session{ + SessionID: "count_msg_session", + Type: string(types.SessionTypeUserChat), + } + + if err := CreateSession(ctx, db, session); err != nil { + t.Fatalf("failed to create session: %v", err) + } + + for i := 1; i <= 5; i++ { + message := &types.SessionMessage{ + SessionID: session.SessionID, + Role: string(types.MessageRoleUser), + Content: "Message " + string(rune(i)), + Sequence: int64(i), + } + if err := CreateMessage(ctx, db, message); err != nil { + t.Fatalf("failed to create message %d: %v", i, err) + } + } + + count, err := GetMessageCount(ctx, db, session.SessionID) + if err != nil { + t.Fatalf("GetMessageCount failed: %v", err) + } + + if count != 5 { + t.Errorf("expected count to be 5, got %d", count) + } +} + +func TestGetNextSequence(t *testing.T) { + db := setupTestDB(t) + ctx := context.Background() + + session := &types.Session{ + SessionID: "sequence_session", + Type: string(types.SessionTypeUserChat), + } + + if err := CreateSession(ctx, db, session); err != nil { + t.Fatalf("failed to create session: %v", err) + } + + for i := 1; i <= 3; i++ { + message := &types.SessionMessage{ + SessionID: session.SessionID, + Role: string(types.MessageRoleUser), + Content: "Message " + string(rune(i)), + Sequence: int64(i), + } + if err := CreateMessage(ctx, db, message); err != nil { + t.Fatalf("failed to create message %d: %v", i, err) + } + } + + nextSequence, err := GetNextSequence(ctx, db, session.SessionID) + if err != nil { + t.Fatalf("GetNextSequence failed: %v", err) + } + + if nextSequence != 4 { + t.Errorf("expected next sequence to be 4, got %d", nextSequence) + } +} + +func TestGetNextSequence_EmptySession(t *testing.T) { + db := setupTestDB(t) + ctx := context.Background() + + session := &types.Session{ + SessionID: "empty_sequence_session", + Type: string(types.SessionTypeUserChat), + } + + if err := CreateSession(ctx, db, session); err != nil { + t.Fatalf("failed to create session: %v", err) + } + + nextSequence, err := GetNextSequence(ctx, db, session.SessionID) + if err != nil { + t.Fatalf("GetNextSequence failed: %v", err) + } + + if nextSequence != 1 { + t.Errorf("expected next sequence to be 1 for empty session, got %d", nextSequence) + } +} diff --git a/backend/internal/service/session_service.go b/backend/internal/service/session_service.go new file mode 100644 index 0000000..1a9e6cb --- /dev/null +++ b/backend/internal/service/session_service.go @@ -0,0 +1,416 @@ +package service + +import ( + "context" + "errors" + "fmt" + "time" + + "gorm.io/gorm" + + "github.com/insmtx/SingerOS/backend/internal/api/contract" + "github.com/insmtx/SingerOS/backend/internal/infra/db" + "github.com/insmtx/SingerOS/backend/types" +) + +var _ contract.SessionService = (*sessionService)(nil) + +type sessionService struct { + db *gorm.DB +} + +func NewSessionService(db *gorm.DB) contract.SessionService { + return &sessionService{ + db: db, + } +} + +func (s *sessionService) CreateSession(ctx context.Context, req *contract.CreateSessionRequest) (*contract.Session, error) { + if req.Type == "" { + return nil, errors.New("type is required") + } + + sessionID := req.SessionID + if sessionID == "" { + sessionID = fmt.Sprintf("sess_%d", time.Now().UnixNano()) + } + + exists, err := db.SessionIDExists(ctx, s.db, sessionID, 0) + if err != nil { + return nil, err + } + if exists { + return nil, errors.New("session with this session_id already exists") + } + + session := &types.Session{ + SessionID: sessionID, + Type: req.Type, + UserID: req.UserID, + AssistantID: req.AssistantID, + AssistantCode: req.AssistantCode, + Status: string(types.SessionStatusActive), + Title: req.Title, + MessageCount: 0, + ExpiredAt: req.ExpiredAt, + } + + if req.Metadata != nil { + session.Metadata = *req.Metadata + } + + if err := db.CreateSession(ctx, s.db, session); err != nil { + return nil, err + } + + return convertToContractSession(session), nil +} + +func (s *sessionService) GetSession(ctx context.Context, id uint, sessionID string) (*contract.Session, error) { + var session *types.Session + var err error + + if id > 0 { + session, err = db.GetSessionByID(ctx, s.db, id) + } else if sessionID != "" { + session, err = db.GetSessionBySessionID(ctx, s.db, sessionID) + } else { + return nil, errors.New("id or session_id is required") + } + + if err != nil { + return nil, err + } + if session == nil { + return nil, errors.New("session not found") + } + + return convertToContractSession(session), nil +} + +func (s *sessionService) UpdateSession(ctx context.Context, id uint, req *contract.UpdateSessionRequest) (*contract.Session, error) { + session, err := db.GetSessionByID(ctx, s.db, id) + if err != nil { + return nil, err + } + if session == nil { + return nil, errors.New("session not found") + } + + if req.Title != "" { + session.Title = req.Title + } + if req.Metadata != nil { + session.Metadata = *req.Metadata + } + if req.ExpiredAt != nil { + session.ExpiredAt = req.ExpiredAt + } + + session.UpdatedAt = time.Now() + + if err := db.UpdateSession(ctx, s.db, session); err != nil { + return nil, err + } + + return convertToContractSession(session), nil +} + +func (s *sessionService) DeleteSession(ctx context.Context, id uint) error { + session, err := db.GetSessionByID(ctx, s.db, id) + if err != nil { + return err + } + if session == nil { + return errors.New("session not found") + } + + return db.DeleteSession(ctx, s.db, id) +} + +func (s *sessionService) ListSessions(ctx context.Context, req *contract.ListSessionsRequest) (*contract.SessionList, error) { + sessions, total, err := db.ListSessions( + ctx, + s.db, + req.Type, + req.Status, + req.UserID, + req.AssistantID, + req.AssistantCode, + req.Keyword, + req.Page, + req.PerPage, + ) + if err != nil { + return nil, err + } + + items := make([]contract.Session, 0, len(sessions)) + for _, session := range sessions { + items = append(items, *convertToContractSession(session)) + } + + return &contract.SessionList{ + Total: total, + Page: req.Page, + Items: items, + }, nil +} + +func (s *sessionService) ActivateSession(ctx context.Context, id uint) error { + session, err := db.GetSessionByID(ctx, s.db, id) + if err != nil { + return err + } + if session == nil { + return errors.New("session not found") + } + + if session.Status == string(types.SessionStatusEnded) { + return errors.New("cannot activate from ended state") + } + + return db.ActivateSession(ctx, s.db, id) +} + +func (s *sessionService) PauseSession(ctx context.Context, id uint) error { + session, err := db.GetSessionByID(ctx, s.db, id) + if err != nil { + return err + } + if session == nil { + return errors.New("session not found") + } + + if session.Status == string(types.SessionStatusEnded) || session.Status == string(types.SessionStatusExpired) { + return fmt.Errorf("cannot pause from %s state", session.Status) + } + + return db.PauseSession(ctx, s.db, id) +} + +func (s *sessionService) EndSession(ctx context.Context, id uint) error { + session, err := db.GetSessionByID(ctx, s.db, id) + if err != nil { + return err + } + if session == nil { + return errors.New("session not found") + } + + if session.Status == string(types.SessionStatusEnded) { + return errors.New("session already ended") + } + + return db.EndSession(ctx, s.db, id) +} + +func (s *sessionService) ResumeSession(ctx context.Context, id uint) error { + session, err := db.GetSessionByID(ctx, s.db, id) + if err != nil { + return err + } + if session == nil { + return errors.New("session not found") + } + + if session.Status != string(types.SessionStatusPaused) { + return errors.New("can only resume from paused state") + } + + return db.ResumeSession(ctx, s.db, id) +} + +func (s *sessionService) AddMessage(ctx context.Context, sessionID uint, req *contract.AddMessageRequest) (*contract.SessionMessage, error) { + if req.Role == "" { + return nil, errors.New("role is required") + } + if req.Content == "" { + return nil, errors.New("content is required") + } + + session, err := db.GetSessionByID(ctx, s.db, sessionID) + if err != nil { + return nil, err + } + if session == nil { + return nil, errors.New("session not found") + } + + sequence, err := db.GetNextSequence(ctx, s.db, session.SessionID) + if err != nil { + return nil, err + } + + message := &types.SessionMessage{ + SessionID: session.SessionID, + Role: req.Role, + Content: req.Content, + MessageType: req.MessageType, + Status: req.Status, + Sequence: sequence, + Timestamp: time.Now().UnixMilli(), // Unix 毫秒时间戳 + } + + if req.Chunks != nil && len(req.Chunks) > 0 { + message.Chunks = req.Chunks + } + + if req.Thinking != "" { + message.Thinking = req.Thinking + } + + if req.ToolCalls != nil && len(req.ToolCalls) > 0 { + message.ToolCalls = req.ToolCalls + } + + if req.Metadata != nil { + message.Metadata = *req.Metadata + } else { + message.Metadata = types.MessageMetadata{} + } + + if message.MessageType == "" { + message.MessageType = string(types.MessageTypeText) + } + + if message.Status == "" { + message.Status = string(types.MessageStatusComplete) + } + + if err := db.CreateMessage(ctx, s.db, message); err != nil { + return nil, err + } + + now := time.Now() + if err := db.IncrementMessageCount(ctx, s.db, sessionID); err != nil { + return nil, err + } + if err := db.UpdateLastMessageAt(ctx, s.db, sessionID, now); err != nil { + return nil, err + } + + return convertToContractSessionMessage(message), nil +} + +func (s *sessionService) GetSessionMessages(ctx context.Context, sessionID uint, page, perPage int) (*contract.MessageList, error) { + session, err := db.GetSessionByID(ctx, s.db, sessionID) + if err != nil { + return nil, err + } + if session == nil { + return nil, errors.New("session not found") + } + + messages, total, err := db.GetSessionMessages(ctx, s.db, session.SessionID, page, perPage) + if err != nil { + return nil, err + } + + items := make([]contract.SessionMessage, 0, len(messages)) + for _, message := range messages { + items = append(items, *convertToContractSessionMessage(message)) + } + + return &contract.MessageList{ + Total: total, + Page: page, + Items: items, + }, nil +} + +func (s *sessionService) DeleteMessage(ctx context.Context, messageID uint) error { + message, err := db.GetMessageByID(ctx, s.db, messageID) + if err != nil { + return err + } + if message == nil { + return errors.New("message not found") + } + + if err := db.DeleteMessage(ctx, s.db, messageID); err != nil { + return err + } + + return nil +} + +func (s *sessionService) ClearSessionMessages(ctx context.Context, sessionID uint) error { + session, err := db.GetSessionByID(ctx, s.db, sessionID) + if err != nil { + return err + } + if session == nil { + return errors.New("session not found") + } + + if err := db.ClearSessionMessages(ctx, s.db, session.SessionID); err != nil { + return err + } + + session.MessageCount = 0 + session.LastMessageAt = nil + session.UpdatedAt = time.Now() + + return db.UpdateSession(ctx, s.db, session) +} + +func convertToContractSession(session *types.Session) *contract.Session { + result := &contract.Session{ + ID: session.ID, + SessionID: session.SessionID, + Type: session.Type, + UserID: session.UserID, + AssistantID: session.AssistantID, + AssistantCode: session.AssistantCode, + Status: session.Status, + Title: session.Title, + MessageCount: session.MessageCount, + CreatedAt: session.CreatedAt, + UpdatedAt: session.UpdatedAt, + } + + if session.Metadata.Tags != nil || session.Metadata.Extra != nil || session.Metadata.UserAgent != "" || session.Metadata.IPAddress != "" { + result.Metadata = &session.Metadata + } + if session.LastMessageAt != nil { + result.LastMessageAt = session.LastMessageAt + } + if session.ExpiredAt != nil { + result.ExpiredAt = session.ExpiredAt + } + + return result +} + +func convertToContractSessionMessage(message *types.SessionMessage) *contract.SessionMessage { + result := &contract.SessionMessage{ + ID: fmt.Sprintf("%d", message.ID), // 转换为 string 以匹配前端 + SessionID: message.SessionID, + Role: message.Role, + Content: message.Content, + MessageType: message.MessageType, + Status: message.Status, + Timestamp: message.Timestamp, + Sequence: message.Sequence, + CreatedAt: message.CreatedAt, + } + + if message.Chunks != nil && len(message.Chunks) > 0 { + result.Chunks = message.Chunks + } + + if message.Thinking != "" { + result.Thinking = message.Thinking + } + + if message.ToolCalls != nil && len(message.ToolCalls) > 0 { + result.ToolCalls = message.ToolCalls + } + + if message.Metadata.ImageURL != "" || message.Metadata.Language != "" || message.Metadata.FileURL != "" || message.Metadata.FileName != "" || message.Metadata.Model != "" || message.Metadata.Extra != nil { + result.Metadata = &message.Metadata + } + + return result +} diff --git a/backend/internal/service/session_service_test.go b/backend/internal/service/session_service_test.go new file mode 100644 index 0000000..9bb40af --- /dev/null +++ b/backend/internal/service/session_service_test.go @@ -0,0 +1,606 @@ +package service + +import ( + "context" + "fmt" + "testing" + + "gorm.io/driver/sqlite" + "gorm.io/gorm" + + "github.com/insmtx/SingerOS/backend/internal/api/contract" + "github.com/insmtx/SingerOS/backend/types" +) + +func setupTestService(t *testing.T) contract.SessionService { + t.Helper() + + db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) + if err != nil { + t.Fatalf("failed to open test database: %v", err) + } + + if err := db.AutoMigrate(&types.Session{}, &types.SessionMessage{}); err != nil { + t.Fatalf("failed to migrate test database: %v", err) + } + + return NewSessionService(db) +} + +func TestCreateSession_ValidInput(t *testing.T) { + service := setupTestService(t) + ctx := context.Background() + + req := &contract.CreateSessionRequest{ + Type: string(types.SessionTypeUserChat), + Title: "Test Session", + } + + session, err := service.CreateSession(ctx, req) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + + if session.SessionID == "" { + t.Error("expected session_id to be generated") + } + + if session.Status != string(types.SessionStatusActive) { + t.Errorf("expected status to be active, got %s", session.Status) + } +} + +func TestCreateSession_MissingType(t *testing.T) { + service := setupTestService(t) + ctx := context.Background() + + req := &contract.CreateSessionRequest{ + Title: "Test Session", + } + + _, err := service.CreateSession(ctx, req) + if err == nil { + t.Error("expected error for missing type") + } + + if err.Error() != "type is required" { + t.Errorf("expected 'type is required' error, got %s", err.Error()) + } +} + +func TestCreateSession_CustomSessionID(t *testing.T) { + service := setupTestService(t) + ctx := context.Background() + + req := &contract.CreateSessionRequest{ + SessionID: "custom_session_id", + Type: string(types.SessionTypeUserChat), + } + + session, err := service.CreateSession(ctx, req) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + + if session.SessionID != "custom_session_id" { + t.Errorf("expected session_id to be custom_session_id, got %s", session.SessionID) + } +} + +func TestCreateSession_DuplicateSessionID(t *testing.T) { + service := setupTestService(t) + ctx := context.Background() + + req1 := &contract.CreateSessionRequest{ + SessionID: "duplicate_id", + Type: string(types.SessionTypeUserChat), + } + + _, err := service.CreateSession(ctx, req1) + if err != nil { + t.Fatalf("first CreateSession failed: %v", err) + } + + req2 := &contract.CreateSessionRequest{ + SessionID: "duplicate_id", + Type: string(types.SessionTypeUserChat), + } + + _, err = service.CreateSession(ctx, req2) + if err == nil { + t.Error("expected error for duplicate session_id") + } + + if err.Error() != "session with this session_id already exists" { + t.Errorf("expected 'session already exists' error, got %s", err.Error()) + } +} + +func TestGetSession_NotFound(t *testing.T) { + service := setupTestService(t) + ctx := context.Background() + + _, err := service.GetSession(ctx, 1, "") + if err == nil { + t.Error("expected error for non-existent session") + } + + if err.Error() != "session not found" { + t.Errorf("expected 'session not found' error, got %s", err.Error()) + } +} + +func TestGetSession_ByID(t *testing.T) { + service := setupTestService(t) + ctx := context.Background() + + createReq := &contract.CreateSessionRequest{ + Type: string(types.SessionTypeUserChat), + Title: "Get By ID Test", + } + + session, err := service.CreateSession(ctx, createReq) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + + retrieved, err := service.GetSession(ctx, session.ID, "") + if err != nil { + t.Fatalf("GetSession failed: %v", err) + } + + if retrieved.ID != session.ID { + t.Errorf("expected ID %d, got %d", session.ID, retrieved.ID) + } +} + +func TestUpdateSession(t *testing.T) { + service := setupTestService(t) + ctx := context.Background() + + createReq := &contract.CreateSessionRequest{ + Type: string(types.SessionTypeUserChat), + Title: "Original Title", + } + + session, err := service.CreateSession(ctx, createReq) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + + updateReq := &contract.UpdateSessionRequest{ + Title: "Updated Title", + } + + updated, err := service.UpdateSession(ctx, session.ID, updateReq) + if err != nil { + t.Fatalf("UpdateSession failed: %v", err) + } + + if updated.Title != "Updated Title" { + t.Errorf("expected title to be updated, got %s", updated.Title) + } +} + +func TestDeleteSession(t *testing.T) { + service := setupTestService(t) + ctx := context.Background() + + createReq := &contract.CreateSessionRequest{ + Type: string(types.SessionTypeUserChat), + } + + session, err := service.CreateSession(ctx, createReq) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + + err = service.DeleteSession(ctx, session.ID) + if err != nil { + t.Fatalf("DeleteSession failed: %v", err) + } + + _, err = service.GetSession(ctx, session.ID, "") + if err == nil { + t.Error("expected error for deleted session") + } +} + +func TestActivateSession_InvalidState(t *testing.T) { + service := setupTestService(t) + ctx := context.Background() + + createReq := &contract.CreateSessionRequest{ + Type: string(types.SessionTypeUserChat), + } + + session, err := service.CreateSession(ctx, createReq) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + + service.EndSession(ctx, session.ID) + + err = service.ActivateSession(ctx, session.ID) + if err == nil { + t.Error("expected error for activating from ended state") + } + + if err.Error() != "cannot activate from ended state" { + t.Errorf("expected 'cannot activate from ended state' error, got %s", err.Error()) + } +} + +func TestPauseSession(t *testing.T) { + service := setupTestService(t) + ctx := context.Background() + + createReq := &contract.CreateSessionRequest{ + Type: string(types.SessionTypeUserChat), + } + + session, err := service.CreateSession(ctx, createReq) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + + err = service.PauseSession(ctx, session.ID) + if err != nil { + t.Fatalf("PauseSession failed: %v", err) + } + + retrieved, err := service.GetSession(ctx, session.ID, "") + if err != nil { + t.Fatalf("GetSession failed: %v", err) + } + + if retrieved.Status != string(types.SessionStatusPaused) { + t.Errorf("expected status to be paused, got %s", retrieved.Status) + } +} + +func TestEndSession_AlreadyEnded(t *testing.T) { + service := setupTestService(t) + ctx := context.Background() + + createReq := &contract.CreateSessionRequest{ + Type: string(types.SessionTypeUserChat), + } + + session, err := service.CreateSession(ctx, createReq) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + + service.EndSession(ctx, session.ID) + + err = service.EndSession(ctx, session.ID) + if err == nil { + t.Error("expected error for ending already ended session") + } + + if err.Error() != "session already ended" { + t.Errorf("expected 'session already ended' error, got %s", err.Error()) + } +} + +func TestResumeSession_NotPaused(t *testing.T) { + service := setupTestService(t) + ctx := context.Background() + + createReq := &contract.CreateSessionRequest{ + Type: string(types.SessionTypeUserChat), + } + + session, err := service.CreateSession(ctx, createReq) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + + err = service.ResumeSession(ctx, session.ID) + if err == nil { + t.Error("expected error for resuming non-paused session") + } + + if err.Error() != "can only resume from paused state" { + t.Errorf("expected 'can only resume from paused state' error, got %s", err.Error()) + } +} + +func TestAddMessage_UpdatesSession(t *testing.T) { + service := setupTestService(t) + ctx := context.Background() + + createReq := &contract.CreateSessionRequest{ + Type: string(types.SessionTypeUserChat), + } + + session, err := service.CreateSession(ctx, createReq) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + + addReq := &contract.AddMessageRequest{ + Role: string(types.MessageRoleUser), + Content: "Test message", + } + + _, err = service.AddMessage(ctx, session.ID, addReq) + if err != nil { + t.Fatalf("AddMessage failed: %v", err) + } + + retrieved, err := service.GetSession(ctx, session.ID, "") + if err != nil { + t.Fatalf("GetSession failed: %v", err) + } + + if retrieved.MessageCount != 1 { + t.Errorf("expected message_count to be 1, got %d", retrieved.MessageCount) + } + + if retrieved.LastMessageAt == nil { + t.Error("expected last_message_at to be set") + } +} + +func TestAddMessage_AutoSequence(t *testing.T) { + service := setupTestService(t) + ctx := context.Background() + + createReq := &contract.CreateSessionRequest{ + Type: string(types.SessionTypeUserChat), + } + + session, err := service.CreateSession(ctx, createReq) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + + for i := 1; i <= 3; i++ { + addReq := &contract.AddMessageRequest{ + Role: string(types.MessageRoleUser), + Content: "Message " + string(rune(i)), + } + + msg, err := service.AddMessage(ctx, session.ID, addReq) + if err != nil { + t.Fatalf("AddMessage failed: %v", err) + } + + if msg.Sequence != int64(i) { + t.Errorf("expected sequence %d, got %d", i, msg.Sequence) + } + } +} + +func TestAddMessage_MissingContent(t *testing.T) { + service := setupTestService(t) + ctx := context.Background() + + createReq := &contract.CreateSessionRequest{ + Type: string(types.SessionTypeUserChat), + } + + session, err := service.CreateSession(ctx, createReq) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + + addReq := &contract.AddMessageRequest{ + Role: string(types.MessageRoleUser), + } + + _, err = service.AddMessage(ctx, session.ID, addReq) + if err == nil { + t.Error("expected error for missing content") + } + + if err.Error() != "content is required" { + t.Errorf("expected 'content is required' error, got %s", err.Error()) + } +} + +func TestDeleteMessage_UpdatesSession(t *testing.T) { + service := setupTestService(t) + ctx := context.Background() + + createReq := &contract.CreateSessionRequest{ + Type: string(types.SessionTypeUserChat), + } + + session, err := service.CreateSession(ctx, createReq) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + + addReq := &contract.AddMessageRequest{ + Role: string(types.MessageRoleUser), + Content: "Test message", + } + + // 添加消息获取 ID + msg, err := service.AddMessage(ctx, session.ID, addReq) + if err != nil { + t.Fatalf("AddMessage failed: %v", err) + } + + // 将 string ID 转换回 uint + var messageID uint + fmt.Sscanf(msg.ID, "%d", &messageID) + + err = service.DeleteMessage(ctx, messageID) + if err != nil { + t.Fatalf("DeleteMessage failed: %v", err) + } + + retrieved, err := service.GetSession(ctx, session.ID, "") + if err != nil { + t.Fatalf("GetSession failed: %v", err) + } + + if retrieved.MessageCount != 1 { + t.Errorf("expected message_count to be 1 after delete, got %d", retrieved.MessageCount) + } +} + +func TestListSessions_FilterByType(t *testing.T) { + service := setupTestService(t) + ctx := context.Background() + + req1 := &contract.CreateSessionRequest{ + Type: string(types.SessionTypeUserChat), + } + req2 := &contract.CreateSessionRequest{ + Type: string(types.SessionTypeAssistantInstance), + } + + _, err := service.CreateSession(ctx, req1) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + _, err = service.CreateSession(ctx, req2) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + + typeFilter := string(types.SessionTypeUserChat) + listReq := &contract.ListSessionsRequest{ + Type: &typeFilter, + Page: 1, + PerPage: 20, + } + + result, err := service.ListSessions(ctx, listReq) + if err != nil { + t.Fatalf("ListSessions failed: %v", err) + } + + if result.Total != 1 { + t.Errorf("expected 1 session, got %d", result.Total) + } + + if result.Items[0].Type != string(types.SessionTypeUserChat) { + t.Errorf("expected user_chat type, got %s", result.Items[0].Type) + } +} + +func TestListSessions_FilterByStatus(t *testing.T) { + service := setupTestService(t) + ctx := context.Background() + + req1 := &contract.CreateSessionRequest{ + Type: string(types.SessionTypeUserChat), + } + req2 := &contract.CreateSessionRequest{ + Type: string(types.SessionTypeUserChat), + } + + _, err := service.CreateSession(ctx, req1) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + session2, _ := service.CreateSession(ctx, req2) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + + service.PauseSession(ctx, session2.ID) + + statusFilter := string(types.SessionStatusActive) + listReq := &contract.ListSessionsRequest{ + Status: &statusFilter, + Page: 1, + PerPage: 20, + } + + result, err := service.ListSessions(ctx, listReq) + if err != nil { + t.Fatalf("ListSessions failed: %v", err) + } + + if result.Total != 1 { + t.Errorf("expected 1 active session, got %d", result.Total) + } +} + +func TestGetSessionMessages(t *testing.T) { + service := setupTestService(t) + ctx := context.Background() + + createReq := &contract.CreateSessionRequest{ + Type: string(types.SessionTypeUserChat), + } + + session, err := service.CreateSession(ctx, createReq) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + + for i := 1; i <= 3; i++ { + addReq := &contract.AddMessageRequest{ + Role: string(types.MessageRoleUser), + Content: "Message " + string(rune(i)), + } + _, err := service.AddMessage(ctx, session.ID, addReq) + if err != nil { + t.Fatalf("AddMessage failed: %v", err) + } + } + + result, err := service.GetSessionMessages(ctx, session.ID, 1, 20) + if err != nil { + t.Fatalf("GetSessionMessages failed: %v", err) + } + + if result.Total != 3 { + t.Errorf("expected 3 messages, got %d", result.Total) + } + + if len(result.Items) != 3 { + t.Errorf("expected 3 messages, got %d", len(result.Items)) + } +} + +func TestClearSessionMessages(t *testing.T) { + service := setupTestService(t) + ctx := context.Background() + + createReq := &contract.CreateSessionRequest{ + Type: string(types.SessionTypeUserChat), + } + + session, err := service.CreateSession(ctx, createReq) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + + for i := 1; i <= 3; i++ { + addReq := &contract.AddMessageRequest{ + Role: string(types.MessageRoleUser), + Content: "Message " + string(rune(i)), + } + _, err := service.AddMessage(ctx, session.ID, addReq) + if err != nil { + t.Fatalf("AddMessage failed: %v", err) + } + } + + err = service.ClearSessionMessages(ctx, session.ID) + if err != nil { + t.Fatalf("ClearSessionMessages failed: %v", err) + } + + retrieved, err := service.GetSession(ctx, session.ID, "") + if err != nil { + t.Fatalf("GetSession failed: %v", err) + } + + if retrieved.MessageCount != 0 { + t.Errorf("expected message_count to be 0 after clear, got %d", retrieved.MessageCount) + } + + if retrieved.LastMessageAt != nil { + t.Error("expected last_message_at to be nil after clear") + } +} diff --git a/backend/types/session.go b/backend/types/session.go new file mode 100644 index 0000000..898544b --- /dev/null +++ b/backend/types/session.go @@ -0,0 +1,302 @@ +package types + +import ( + "database/sql/driver" + "encoding/json" + "fmt" + "time" + + "gorm.io/gorm" +) + +// SessionType 会话类型常量 +type SessionType string + +const ( + SessionTypeUserChat SessionType = "user_chat" + SessionTypeAssistantInstance SessionType = "assistant_instance" +) + +// SessionStatus 会话状态常量 +type SessionStatus string + +const ( + SessionStatusActive SessionStatus = "active" + SessionStatusPaused SessionStatus = "paused" + SessionStatusEnded SessionStatus = "ended" + SessionStatusExpired SessionStatus = "expired" +) + +// MessageRole 消息角色常量 +type MessageRole string + +const ( + MessageRoleUser MessageRole = "user" + MessageRoleAssistant MessageRole = "assistant" + MessageRoleSystem MessageRole = "system" + MessageRoleTool MessageRole = "tool" +) + +// MessageType 消息类型常量 +type MessageType string + +const ( + MessageTypeText MessageType = "text" + MessageTypeImage MessageType = "image" + MessageTypeCode MessageType = "code" + MessageTypeFile MessageType = "file" +) + +// MessageStatus 消息状态常量 +type MessageStatus string + +const ( + MessageStatusSending MessageStatus = "sending" + MessageStatusStreaming MessageStatus = "streaming" + MessageStatusComplete MessageStatus = "complete" + MessageStatusError MessageStatus = "error" +) + +// Session 会话结构体定义了用户与数字助手之间的会话信息 +type Session struct { + gorm.Model + // session - 会话唯一标识,格式如:sess_xxx,VARCHAR(255),NOT NULL,UNIQUE + SessionID string `gorm:"column:session_id;type:varchar(255);not null;uniqueIndex"` + + // session - 会话类型,VARCHAR(50),NOT NULL + Type string `gorm:"column:type;type:varchar(50);not null"` + + // session - 关联用户ID,0表示不关联,BIGINT,DEFAULT 0 + UserID uint `gorm:"column:user_id;type:bigint;default:0;index"` + + // session - 关联数字助手ID,0表示不关联,BIGINT,DEFAULT 0 + AssistantID uint `gorm:"column:assistant_id;type:bigint;default:0;index"` + + // session - 关联数字助手Code,空字符串表示不关联,VARCHAR(255),DEFAULT '' + AssistantCode string `gorm:"column:assistant_code;type:varchar(255);default:'';index"` + + // session - 会话状态,VARCHAR(50),NOT NULL,DEFAULT 'active' + Status string `gorm:"column:status;type:varchar(50);not null;default:'active'"` + + // session - 会话标题,VARCHAR(500),允许为空 + Title string `gorm:"column:title;type:varchar(500)"` + + // session - 元数据,JSON格式存储额外信息,JSONB,允许为空 + Metadata SessionMetadata `gorm:"column:metadata;type:jsonb"` + + // session - 消息数量,INTEGER,DEFAULT 0 + MessageCount int `gorm:"column:message_count;type:integer;default:0"` + + // session - 最后消息时间,TIMESTAMP,允许为空 + LastMessageAt *time.Time `gorm:"column:last_message_at"` + + // session - 过期时间,TIMESTAMP,允许为空 + ExpiredAt *time.Time `gorm:"column:expired_at;index"` +} + +// TableName 指定Session结构体对应的数据库表名 +func (Session) TableName() string { + return TableNameSession +} + +// SessionMetadata 会话元数据结构 +type SessionMetadata struct { + // 元数据 - 用户代理信息 + UserAgent string `json:"user_agent,omitempty"` + // 元数据 - IP地址 + IPAddress string `json:"ip_address,omitempty"` + // 元数据 - 自定义标签 + Tags []string `json:"tags,omitempty"` + // 元数据 - 其他扩展字段 + Extra map[string]interface{} `json:"extra,omitempty"` +} + +// Scan 实现 sql.Scanner 接口,用于从数据库中读取 JSON 数据 +func (sm *SessionMetadata) Scan(value interface{}) error { + if value == nil { + return nil + } + + bytes, ok := value.([]byte) + if !ok { + return fmt.Errorf("cannot scan %T into SessionMetadata", value) + } + + return json.Unmarshal(bytes, sm) +} + +// Value 实现 driver.Valuer 接口,用于将元数据转换为 JSON 存储 +func (sm SessionMetadata) Value() (driver.Value, error) { + return json.Marshal(sm) +} + +// SessionMessage 会话消息结构体 +type SessionMessage struct { + gorm.Model + // session_message - 关联会话ID,VARCHAR(255),NOT NULL,INDEX + SessionID string `gorm:"column:session_id;type:varchar(255);not null;index"` + + // session_message - 消息角色(user/assistant/system/tool),VARCHAR(50),NOT NULL + Role string `gorm:"column:role;type:varchar(50);not null"` + + // session_message - 消息内容,TEXT,NOT NULL + Content string `gorm:"column:content;type:text;not null"` + + // session_message - 消息类型(text/image/code/file),VARCHAR(50),DEFAULT 'text' + MessageType string `gorm:"column:message_type;type:varchar(50);default:'text'"` + + // session_message - 消息状态(sending/streaming/complete/error),VARCHAR(50),DEFAULT 'complete' + Status string `gorm:"column:status;type:varchar(50);default:'complete'"` + + // session_message - 流式片段(JSON数组),JSONB,允许为空 + Chunks StringSlice `gorm:"column:chunks;type:jsonb"` + + // session_message - 思维链 / reasoning,TEXT,允许为空 + Thinking string `gorm:"column:thinking;type:text"` + + // session_message - 工具调用信息(JSON数组),JSONB,允许为空 + ToolCalls ToolCallSlice `gorm:"column:tool_calls;type:jsonb"` + + // session_message - 消息元数据,JSONB,允许为空 + Metadata MessageMetadata `gorm:"column:metadata;type:jsonb"` + + // session_message - 消息序号(用于排序),BIGINT,NOT NULL + Sequence int64 `gorm:"column:sequence;type:bigint;not null;index"` + + // session_message - 时间戳(Unix毫秒),BIGINT,允许为空 + Timestamp int64 `gorm:"column:timestamp;type:bigint"` +} + +// TableName 指定SessionMessage结构体对应的数据库表名 +func (SessionMessage) TableName() string { + return TableNameSessionMessage +} + +// MessageMetadata 消息元数据结构 +type MessageMetadata struct { + // 图片URL(当 MessageType 为 image 时) + ImageURL string `json:"image_url,omitempty"` + // 代码语言(当 MessageType 为 code 时) + Language string `json:"language,omitempty"` + // 文件URL(当 MessageType 为 file 时) + FileURL string `json:"file_url,omitempty"` + // 文件名 + FileName string `json:"file_name,omitempty"` + // LLM 模型名称 + Model string `json:"model,omitempty"` + // Token 数量 + Tokens int `json:"tokens,omitempty"` + // 延迟(毫秒) + Latency int `json:"latency,omitempty"` + // 其他扩展字段 + Extra map[string]interface{} `json:"extra,omitempty"` +} + +// ToolCallStatus 工具调用状态常量 +type ToolCallStatus string + +const ( + ToolCallStatusPending ToolCallStatus = "pending" + ToolCallStatusRunning ToolCallStatus = "running" + ToolCallStatusSuccess ToolCallStatus = "success" + ToolCallStatusError ToolCallStatus = "error" +) + +// ToolCall 工具调用结构 +type ToolCall struct { + // 工具调用ID + ID string `json:"id"` + // 工具名称 + Name string `json:"name"` + // 工具参数 + Arguments map[string]interface{} `json:"arguments"` + // 工具调用状态 + Status ToolCallStatus `json:"status"` + // 工具调用结果 + Result interface{} `json:"result,omitempty"` + // 持续时间(毫秒) + Duration int `json:"duration,omitempty"` +} + +// StringSlice 自定义字符串切片类型,支持 JSONB 存储 +type StringSlice []string + +// Scan 实现 sql.Scanner 接口 +func (s *StringSlice) Scan(value interface{}) error { + if value == nil { + *s = StringSlice{} + return nil + } + + bytes, ok := value.([]byte) + if !ok { + return fmt.Errorf("cannot scan %T into StringSlice", value) + } + + var result []string + if err := json.Unmarshal(bytes, &result); err != nil { + return err + } + + *s = StringSlice(result) + return nil +} + +// Value 实现 driver.Valuer 接口 +func (s StringSlice) Value() (driver.Value, error) { + if len(s) == 0 { + return nil, nil + } + return json.Marshal([]string(s)) +} + +// ToolCallSlice 自定义 ToolCall 切片类型,支持 JSONB 存储 +type ToolCallSlice []ToolCall + +// Scan 实现 sql.Scanner 接口 +func (t *ToolCallSlice) Scan(value interface{}) error { + if value == nil { + *t = ToolCallSlice{} + return nil + } + + bytes, ok := value.([]byte) + if !ok { + return fmt.Errorf("cannot scan %T into ToolCallSlice", value) + } + + var result []ToolCall + if err := json.Unmarshal(bytes, &result); err != nil { + return err + } + + *t = ToolCallSlice(result) + return nil +} + +// Value 实现 driver.Valuer 接口 +func (t ToolCallSlice) Value() (driver.Value, error) { + if len(t) == 0 { + return nil, nil + } + return json.Marshal([]ToolCall(t)) +} + +// Scan 实现 sql.Scanner 接口 +func (mm *MessageMetadata) Scan(value interface{}) error { + if value == nil { + return nil + } + + bytes, ok := value.([]byte) + if !ok { + return fmt.Errorf("cannot scan %T into MessageMetadata", value) + } + + return json.Unmarshal(bytes, mm) +} + +// Value 实现 driver.Valuer 接口 +func (mm MessageMetadata) Value() (driver.Value, error) { + return json.Marshal(mm) +} diff --git a/backend/types/tables.go b/backend/types/tables.go index 1c93eb5..780ec04 100644 --- a/backend/types/tables.go +++ b/backend/types/tables.go @@ -25,4 +25,9 @@ const ( TableNameSkillLog = tablenamePrefix + "skill_execution_log" // TableNameSkillRegistry 技能注册表名 TableNameSkillRegistry = tablenamePrefix + "skill_registry" + + // TableNameSession 会话表名 + TableNameSession = tablenamePrefix + "session" + // TableNameSessionMessage 会话消息表名 + TableNameSessionMessage = tablenamePrefix + "session_message" ) diff --git a/docs/docs.go b/docs/docs.go new file mode 100644 index 0000000..25f2c10 --- /dev/null +++ b/docs/docs.go @@ -0,0 +1,1225 @@ +// Package docs Code generated by swaggo/swag. DO NOT EDIT +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": {}, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/ActivateSession": { + "post": { + "description": "激活已结束会话", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Session" + ], + "summary": "激活会话", + "parameters": [ + { + "description": "激活会话请求", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/backend_internal_api_handler.SessionIDRequest" + } + } + ], + "responses": { + "200": { + "description": "成功响应", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.Response" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "404": { + "description": "资源不存在", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "500": { + "description": "内部服务器错误", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + } + } + } + }, + "/CreateDigitalAssistant": { + "post": { + "description": "创建一个新的数字助手实例", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "DigitalAssistant" + ], + "summary": "创建数字助手", + "parameters": [ + { + "description": "创建数字助手请求", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_contract.CreateDigitalAssistantRequest" + } + } + ], + "responses": { + "200": { + "description": "成功响应", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.CreateDigitalAssistantResponse" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "500": { + "description": "内部服务器错误", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + } + } + } + }, + "/CreateSession": { + "post": { + "description": "创建一个新的会话实例", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Session" + ], + "summary": "创建会话", + "parameters": [ + { + "description": "创建会话请求", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_contract.CreateSessionRequest" + } + } + ], + "responses": { + "200": { + "description": "成功响应", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.Response" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "500": { + "description": "内部服务器错误", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + } + } + } + }, + "/DeleteDigitalAssistant": { + "post": { + "description": "根据ID删除数字助手", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "DigitalAssistant" + ], + "summary": "删除数字助手", + "parameters": [ + { + "description": "删除数字助手请求", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/backend_internal_api_handler.DeleteDigitalAssistantRequest" + } + } + ], + "responses": { + "200": { + "description": "成功响应", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.BaseResponse" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "403": { + "description": "权限不足", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "404": { + "description": "资源不存在", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "500": { + "description": "内部服务器错误", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + } + } + } + }, + "/DeleteSession": { + "post": { + "description": "根据ID删除会话", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Session" + ], + "summary": "删除会话", + "parameters": [ + { + "description": "删除会话请求", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/backend_internal_api_handler.DeleteSessionRequest" + } + } + ], + "responses": { + "200": { + "description": "成功响应", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.Response" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "404": { + "description": "资源不存在", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "500": { + "description": "内部服务器错误", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + } + } + } + }, + "/GetDigitalAssistant": { + "post": { + "description": "根据ID或Code获取数字助手详情", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "DigitalAssistant" + ], + "summary": "获取数字助手详情", + "parameters": [ + { + "description": "获取数字助手请求", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/backend_internal_api_handler.GetDigitalAssistantRequest" + } + } + ], + "responses": { + "200": { + "description": "成功响应", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.CreateDigitalAssistantResponse" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "403": { + "description": "权限不足", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "404": { + "description": "资源不存在", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "500": { + "description": "内部服务器错误", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + } + } + } + }, + "/GetSession": { + "post": { + "description": "根据ID或SessionID获取会话详情", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Session" + ], + "summary": "获取会话详情", + "parameters": [ + { + "description": "获取会话请求", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/backend_internal_api_handler.GetSessionRequest" + } + } + ], + "responses": { + "200": { + "description": "成功响应", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.Response" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "404": { + "description": "资源不存在", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "500": { + "description": "内部服务器错误", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + } + } + } + }, + "/ListDigitalAssistant": { + "post": { + "description": "分页查询数字助手列表", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "DigitalAssistant" + ], + "summary": "查询数字助手列表", + "parameters": [ + { + "description": "查询列表请求", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_contract.ListDigitalAssistantRequest" + } + } + ], + "responses": { + "200": { + "description": "成功响应", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.CreateDigitalAssistantResponse" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "500": { + "description": "内部服务器错误", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + } + } + } + }, + "/ListSessions": { + "post": { + "description": "分页查询会话列表", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Session" + ], + "summary": "查询会话列表", + "parameters": [ + { + "description": "查询列表请求", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_contract.ListSessionsRequest" + } + } + ], + "responses": { + "200": { + "description": "成功响应", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.Response" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "500": { + "description": "内部服务器错误", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + } + } + } + }, + "/UpdateDigitalAssistant": { + "post": { + "description": "更新数字助手基本信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "DigitalAssistant" + ], + "summary": "更新数字助手", + "parameters": [ + { + "description": "更新数字助手请求", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/backend_internal_api_handler.UpdateDigitalAssistantRequest" + } + } + ], + "responses": { + "200": { + "description": "成功响应", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.CreateDigitalAssistantResponse" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "403": { + "description": "权限不足", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "404": { + "description": "资源不存在", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "500": { + "description": "内部服务器错误", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + } + } + } + }, + "/UpdateDigitalAssistantConfig": { + "post": { + "description": "更新数字助手的配置信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "DigitalAssistant" + ], + "summary": "更新数字助手配置", + "parameters": [ + { + "description": "更新配置请求", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/backend_internal_api_handler.UpdateDigitalAssistantConfigRequest" + } + } + ], + "responses": { + "200": { + "description": "成功响应", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.CreateDigitalAssistantResponse" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "403": { + "description": "权限不足", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "404": { + "description": "资源不存在", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "500": { + "description": "内部服务器错误", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + } + } + } + }, + "/UpdateDigitalAssistantStatus": { + "post": { + "description": "更新数字助手的运行状态", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "DigitalAssistant" + ], + "summary": "更新数字助手状态", + "parameters": [ + { + "description": "更新状态请求", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/backend_internal_api_handler.UpdateDigitalAssistantStatusRequest" + } + } + ], + "responses": { + "200": { + "description": "成功响应", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.BaseResponse" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "403": { + "description": "权限不足", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "404": { + "description": "资源不存在", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "500": { + "description": "内部服务器错误", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + } + } + } + }, + "/UpdateSession": { + "post": { + "description": "更新会话基本信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Session" + ], + "summary": "更新会话", + "parameters": [ + { + "description": "更新会话请求", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/backend_internal_api_handler.UpdateSessionRequest" + } + } + ], + "responses": { + "200": { + "description": "成功响应", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.Response" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "404": { + "description": "资源不存在", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "500": { + "description": "内部服务器错误", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + } + } + } + } + }, + "definitions": { + "backend_internal_api_handler.DeleteDigitalAssistantRequest": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "integer" + } + } + }, + "backend_internal_api_handler.DeleteSessionRequest": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "integer" + } + } + }, + "backend_internal_api_handler.GetDigitalAssistantRequest": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "id": { + "type": "integer" + } + } + }, + "backend_internal_api_handler.GetSessionRequest": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "session_id": { + "type": "string" + } + } + }, + "backend_internal_api_handler.SessionIDRequest": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "integer" + } + } + }, + "backend_internal_api_handler.UpdateDigitalAssistantConfigRequest": { + "type": "object", + "required": [ + "config", + "id" + ], + "properties": { + "config": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_contract.AssistantConfig" + }, + "id": { + "type": "integer" + } + } + }, + "backend_internal_api_handler.UpdateDigitalAssistantRequest": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "avatar": { + "type": "string" + }, + "config": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_contract.AssistantConfig" + }, + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "backend_internal_api_handler.UpdateDigitalAssistantStatusRequest": { + "type": "object", + "required": [ + "id", + "status" + ], + "properties": { + "id": { + "type": "integer" + }, + "status": { + "type": "string" + } + } + }, + "backend_internal_api_handler.UpdateSessionRequest": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "expired_at": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "metadata": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_types.SessionMetadata" + }, + "title": { + "type": "string" + } + } + }, + "github_com_insmtx_SingerOS_backend_internal_api_contract.AssistantConfig": { + "type": "object", + "properties": { + "channels": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_contract.ChannelRef" + } + }, + "knowledge": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_contract.KnowledgeRef" + } + }, + "llm_config": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_contract.LLMConfig" + }, + "memory_config": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_contract.MemoryConfig" + }, + "policies_config": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_contract.PolicyConfig" + }, + "runtime_config": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_contract.RuntimeConfig" + }, + "skills": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_contract.SkillRef" + } + } + } + }, + "github_com_insmtx_SingerOS_backend_internal_api_contract.ChannelRef": { + "type": "object", + "properties": { + "type": { + "type": "string" + } + } + }, + "github_com_insmtx_SingerOS_backend_internal_api_contract.CreateDigitalAssistantRequest": { + "type": "object", + "required": [ + "code", + "config", + "name", + "org_id", + "owner_id" + ], + "properties": { + "avatar": { + "type": "string" + }, + "code": { + "type": "string" + }, + "config": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_contract.AssistantConfig" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "org_id": { + "type": "integer" + }, + "owner_id": { + "type": "integer" + } + } + }, + "github_com_insmtx_SingerOS_backend_internal_api_contract.CreateSessionRequest": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "assistant_code": { + "type": "string" + }, + "assistant_id": { + "type": "integer" + }, + "expired_at": { + "type": "string" + }, + "metadata": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_types.SessionMetadata" + }, + "session_id": { + "type": "string" + }, + "title": { + "type": "string" + }, + "type": { + "type": "string" + }, + "user_id": { + "type": "integer" + } + } + }, + "github_com_insmtx_SingerOS_backend_internal_api_contract.DigitalAssistant": { + "type": "object", + "properties": { + "avatar": { + "type": "string" + }, + "code": { + "type": "string" + }, + "config": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_contract.AssistantConfig" + }, + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "org_id": { + "type": "integer" + }, + "owner_id": { + "type": "integer" + }, + "status": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "version": { + "type": "integer" + } + } + }, + "github_com_insmtx_SingerOS_backend_internal_api_contract.KnowledgeRef": { + "type": "object", + "properties": { + "dataset_id": { + "type": "string" + }, + "repo": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "github_com_insmtx_SingerOS_backend_internal_api_contract.LLMConfig": { + "type": "object", + "properties": { + "type": { + "type": "string" + } + } + }, + "github_com_insmtx_SingerOS_backend_internal_api_contract.ListDigitalAssistantRequest": { + "type": "object", + "properties": { + "keyword": { + "type": "string" + }, + "org_id": { + "type": "integer" + }, + "owner_id": { + "type": "integer" + }, + "page": { + "type": "integer" + }, + "per_page": { + "type": "integer" + }, + "status": { + "type": "string" + } + } + }, + "github_com_insmtx_SingerOS_backend_internal_api_contract.ListSessionsRequest": { + "type": "object", + "properties": { + "assistant_code": { + "type": "string" + }, + "assistant_id": { + "type": "integer" + }, + "keyword": { + "type": "string" + }, + "page": { + "type": "integer" + }, + "per_page": { + "type": "integer" + }, + "status": { + "type": "string" + }, + "type": { + "type": "string" + }, + "user_id": { + "type": "integer" + } + } + }, + "github_com_insmtx_SingerOS_backend_internal_api_contract.MemoryConfig": { + "type": "object", + "properties": { + "type": { + "type": "string" + } + } + }, + "github_com_insmtx_SingerOS_backend_internal_api_contract.PolicyConfig": { + "type": "object", + "properties": { + "type": { + "type": "string" + } + } + }, + "github_com_insmtx_SingerOS_backend_internal_api_contract.RuntimeConfig": { + "type": "object", + "properties": { + "type": { + "type": "string" + } + } + }, + "github_com_insmtx_SingerOS_backend_internal_api_contract.SkillRef": { + "type": "object", + "properties": { + "skill_code": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "github_com_insmtx_SingerOS_backend_internal_api_dto.BaseResponse": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "message": { + "type": "string" + } + } + }, + "github_com_insmtx_SingerOS_backend_internal_api_dto.CreateDigitalAssistantResponse": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "data": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_contract.DigitalAssistant" + }, + "message": { + "type": "string" + } + } + }, + "github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "details": { + "type": "string" + }, + "message": { + "type": "string" + } + } + }, + "github_com_insmtx_SingerOS_backend_internal_api_dto.Response": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "data": {}, + "message": { + "type": "string" + } + } + }, + "github_com_insmtx_SingerOS_backend_types.SessionMetadata": { + "type": "object", + "properties": { + "extra": { + "description": "元数据 - 其他扩展字段", + "type": "object", + "additionalProperties": true + }, + "ip_address": { + "description": "元数据 - IP地址", + "type": "string" + }, + "tags": { + "description": "元数据 - 自定义标签", + "type": "array", + "items": { + "type": "string" + } + }, + "user_agent": { + "description": "元数据 - 用户代理信息", + "type": "string" + } + } + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "1.0", + Host: "localhost:8080", + BasePath: "/v1", + Schemes: []string{"http", "https"}, + Title: "SingerOS API", + Description: "SingerOS 数字助手平台 API,提供数字助手管理、技能调用、事件处理等功能", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/docs/swagger.json b/docs/swagger.json new file mode 100644 index 0000000..6fab69a --- /dev/null +++ b/docs/swagger.json @@ -0,0 +1,1205 @@ +{ + "schemes": [ + "http", + "https" + ], + "swagger": "2.0", + "info": { + "description": "SingerOS 数字助手平台 API,提供数字助手管理、技能调用、事件处理等功能", + "title": "SingerOS API", + "contact": {}, + "version": "1.0" + }, + "host": "localhost:8080", + "basePath": "/v1", + "paths": { + "/ActivateSession": { + "post": { + "description": "激活已结束会话", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Session" + ], + "summary": "激活会话", + "parameters": [ + { + "description": "激活会话请求", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/backend_internal_api_handler.SessionIDRequest" + } + } + ], + "responses": { + "200": { + "description": "成功响应", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.Response" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "404": { + "description": "资源不存在", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "500": { + "description": "内部服务器错误", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + } + } + } + }, + "/CreateDigitalAssistant": { + "post": { + "description": "创建一个新的数字助手实例", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "DigitalAssistant" + ], + "summary": "创建数字助手", + "parameters": [ + { + "description": "创建数字助手请求", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_contract.CreateDigitalAssistantRequest" + } + } + ], + "responses": { + "200": { + "description": "成功响应", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.CreateDigitalAssistantResponse" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "500": { + "description": "内部服务器错误", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + } + } + } + }, + "/CreateSession": { + "post": { + "description": "创建一个新的会话实例", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Session" + ], + "summary": "创建会话", + "parameters": [ + { + "description": "创建会话请求", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_contract.CreateSessionRequest" + } + } + ], + "responses": { + "200": { + "description": "成功响应", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.Response" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "500": { + "description": "内部服务器错误", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + } + } + } + }, + "/DeleteDigitalAssistant": { + "post": { + "description": "根据ID删除数字助手", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "DigitalAssistant" + ], + "summary": "删除数字助手", + "parameters": [ + { + "description": "删除数字助手请求", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/backend_internal_api_handler.DeleteDigitalAssistantRequest" + } + } + ], + "responses": { + "200": { + "description": "成功响应", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.BaseResponse" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "403": { + "description": "权限不足", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "404": { + "description": "资源不存在", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "500": { + "description": "内部服务器错误", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + } + } + } + }, + "/DeleteSession": { + "post": { + "description": "根据ID删除会话", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Session" + ], + "summary": "删除会话", + "parameters": [ + { + "description": "删除会话请求", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/backend_internal_api_handler.DeleteSessionRequest" + } + } + ], + "responses": { + "200": { + "description": "成功响应", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.Response" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "404": { + "description": "资源不存在", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "500": { + "description": "内部服务器错误", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + } + } + } + }, + "/GetDigitalAssistant": { + "post": { + "description": "根据ID或Code获取数字助手详情", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "DigitalAssistant" + ], + "summary": "获取数字助手详情", + "parameters": [ + { + "description": "获取数字助手请求", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/backend_internal_api_handler.GetDigitalAssistantRequest" + } + } + ], + "responses": { + "200": { + "description": "成功响应", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.CreateDigitalAssistantResponse" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "403": { + "description": "权限不足", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "404": { + "description": "资源不存在", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "500": { + "description": "内部服务器错误", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + } + } + } + }, + "/GetSession": { + "post": { + "description": "根据ID或SessionID获取会话详情", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Session" + ], + "summary": "获取会话详情", + "parameters": [ + { + "description": "获取会话请求", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/backend_internal_api_handler.GetSessionRequest" + } + } + ], + "responses": { + "200": { + "description": "成功响应", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.Response" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "404": { + "description": "资源不存在", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "500": { + "description": "内部服务器错误", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + } + } + } + }, + "/ListDigitalAssistant": { + "post": { + "description": "分页查询数字助手列表", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "DigitalAssistant" + ], + "summary": "查询数字助手列表", + "parameters": [ + { + "description": "查询列表请求", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_contract.ListDigitalAssistantRequest" + } + } + ], + "responses": { + "200": { + "description": "成功响应", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.CreateDigitalAssistantResponse" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "500": { + "description": "内部服务器错误", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + } + } + } + }, + "/ListSessions": { + "post": { + "description": "分页查询会话列表", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Session" + ], + "summary": "查询会话列表", + "parameters": [ + { + "description": "查询列表请求", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_contract.ListSessionsRequest" + } + } + ], + "responses": { + "200": { + "description": "成功响应", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.Response" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "500": { + "description": "内部服务器错误", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + } + } + } + }, + "/UpdateDigitalAssistant": { + "post": { + "description": "更新数字助手基本信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "DigitalAssistant" + ], + "summary": "更新数字助手", + "parameters": [ + { + "description": "更新数字助手请求", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/backend_internal_api_handler.UpdateDigitalAssistantRequest" + } + } + ], + "responses": { + "200": { + "description": "成功响应", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.CreateDigitalAssistantResponse" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "403": { + "description": "权限不足", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "404": { + "description": "资源不存在", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "500": { + "description": "内部服务器错误", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + } + } + } + }, + "/UpdateDigitalAssistantConfig": { + "post": { + "description": "更新数字助手的配置信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "DigitalAssistant" + ], + "summary": "更新数字助手配置", + "parameters": [ + { + "description": "更新配置请求", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/backend_internal_api_handler.UpdateDigitalAssistantConfigRequest" + } + } + ], + "responses": { + "200": { + "description": "成功响应", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.CreateDigitalAssistantResponse" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "403": { + "description": "权限不足", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "404": { + "description": "资源不存在", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "500": { + "description": "内部服务器错误", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + } + } + } + }, + "/UpdateDigitalAssistantStatus": { + "post": { + "description": "更新数字助手的运行状态", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "DigitalAssistant" + ], + "summary": "更新数字助手状态", + "parameters": [ + { + "description": "更新状态请求", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/backend_internal_api_handler.UpdateDigitalAssistantStatusRequest" + } + } + ], + "responses": { + "200": { + "description": "成功响应", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.BaseResponse" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "403": { + "description": "权限不足", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "404": { + "description": "资源不存在", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "500": { + "description": "内部服务器错误", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + } + } + } + }, + "/UpdateSession": { + "post": { + "description": "更新会话基本信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Session" + ], + "summary": "更新会话", + "parameters": [ + { + "description": "更新会话请求", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/backend_internal_api_handler.UpdateSessionRequest" + } + } + ], + "responses": { + "200": { + "description": "成功响应", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.Response" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "404": { + "description": "资源不存在", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + }, + "500": { + "description": "内部服务器错误", + "schema": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse" + } + } + } + } + } + }, + "definitions": { + "backend_internal_api_handler.DeleteDigitalAssistantRequest": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "integer" + } + } + }, + "backend_internal_api_handler.DeleteSessionRequest": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "integer" + } + } + }, + "backend_internal_api_handler.GetDigitalAssistantRequest": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "id": { + "type": "integer" + } + } + }, + "backend_internal_api_handler.GetSessionRequest": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "session_id": { + "type": "string" + } + } + }, + "backend_internal_api_handler.SessionIDRequest": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "integer" + } + } + }, + "backend_internal_api_handler.UpdateDigitalAssistantConfigRequest": { + "type": "object", + "required": [ + "config", + "id" + ], + "properties": { + "config": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_contract.AssistantConfig" + }, + "id": { + "type": "integer" + } + } + }, + "backend_internal_api_handler.UpdateDigitalAssistantRequest": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "avatar": { + "type": "string" + }, + "config": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_contract.AssistantConfig" + }, + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "backend_internal_api_handler.UpdateDigitalAssistantStatusRequest": { + "type": "object", + "required": [ + "id", + "status" + ], + "properties": { + "id": { + "type": "integer" + }, + "status": { + "type": "string" + } + } + }, + "backend_internal_api_handler.UpdateSessionRequest": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "expired_at": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "metadata": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_types.SessionMetadata" + }, + "title": { + "type": "string" + } + } + }, + "github_com_insmtx_SingerOS_backend_internal_api_contract.AssistantConfig": { + "type": "object", + "properties": { + "channels": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_contract.ChannelRef" + } + }, + "knowledge": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_contract.KnowledgeRef" + } + }, + "llm_config": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_contract.LLMConfig" + }, + "memory_config": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_contract.MemoryConfig" + }, + "policies_config": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_contract.PolicyConfig" + }, + "runtime_config": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_contract.RuntimeConfig" + }, + "skills": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_contract.SkillRef" + } + } + } + }, + "github_com_insmtx_SingerOS_backend_internal_api_contract.ChannelRef": { + "type": "object", + "properties": { + "type": { + "type": "string" + } + } + }, + "github_com_insmtx_SingerOS_backend_internal_api_contract.CreateDigitalAssistantRequest": { + "type": "object", + "required": [ + "code", + "config", + "name", + "org_id", + "owner_id" + ], + "properties": { + "avatar": { + "type": "string" + }, + "code": { + "type": "string" + }, + "config": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_contract.AssistantConfig" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "org_id": { + "type": "integer" + }, + "owner_id": { + "type": "integer" + } + } + }, + "github_com_insmtx_SingerOS_backend_internal_api_contract.CreateSessionRequest": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "assistant_code": { + "type": "string" + }, + "assistant_id": { + "type": "integer" + }, + "expired_at": { + "type": "string" + }, + "metadata": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_types.SessionMetadata" + }, + "session_id": { + "type": "string" + }, + "title": { + "type": "string" + }, + "type": { + "type": "string" + }, + "user_id": { + "type": "integer" + } + } + }, + "github_com_insmtx_SingerOS_backend_internal_api_contract.DigitalAssistant": { + "type": "object", + "properties": { + "avatar": { + "type": "string" + }, + "code": { + "type": "string" + }, + "config": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_contract.AssistantConfig" + }, + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "org_id": { + "type": "integer" + }, + "owner_id": { + "type": "integer" + }, + "status": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "version": { + "type": "integer" + } + } + }, + "github_com_insmtx_SingerOS_backend_internal_api_contract.KnowledgeRef": { + "type": "object", + "properties": { + "dataset_id": { + "type": "string" + }, + "repo": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "github_com_insmtx_SingerOS_backend_internal_api_contract.LLMConfig": { + "type": "object", + "properties": { + "type": { + "type": "string" + } + } + }, + "github_com_insmtx_SingerOS_backend_internal_api_contract.ListDigitalAssistantRequest": { + "type": "object", + "properties": { + "keyword": { + "type": "string" + }, + "org_id": { + "type": "integer" + }, + "owner_id": { + "type": "integer" + }, + "page": { + "type": "integer" + }, + "per_page": { + "type": "integer" + }, + "status": { + "type": "string" + } + } + }, + "github_com_insmtx_SingerOS_backend_internal_api_contract.ListSessionsRequest": { + "type": "object", + "properties": { + "assistant_code": { + "type": "string" + }, + "assistant_id": { + "type": "integer" + }, + "keyword": { + "type": "string" + }, + "page": { + "type": "integer" + }, + "per_page": { + "type": "integer" + }, + "status": { + "type": "string" + }, + "type": { + "type": "string" + }, + "user_id": { + "type": "integer" + } + } + }, + "github_com_insmtx_SingerOS_backend_internal_api_contract.MemoryConfig": { + "type": "object", + "properties": { + "type": { + "type": "string" + } + } + }, + "github_com_insmtx_SingerOS_backend_internal_api_contract.PolicyConfig": { + "type": "object", + "properties": { + "type": { + "type": "string" + } + } + }, + "github_com_insmtx_SingerOS_backend_internal_api_contract.RuntimeConfig": { + "type": "object", + "properties": { + "type": { + "type": "string" + } + } + }, + "github_com_insmtx_SingerOS_backend_internal_api_contract.SkillRef": { + "type": "object", + "properties": { + "skill_code": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "github_com_insmtx_SingerOS_backend_internal_api_dto.BaseResponse": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "message": { + "type": "string" + } + } + }, + "github_com_insmtx_SingerOS_backend_internal_api_dto.CreateDigitalAssistantResponse": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "data": { + "$ref": "#/definitions/github_com_insmtx_SingerOS_backend_internal_api_contract.DigitalAssistant" + }, + "message": { + "type": "string" + } + } + }, + "github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "details": { + "type": "string" + }, + "message": { + "type": "string" + } + } + }, + "github_com_insmtx_SingerOS_backend_internal_api_dto.Response": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "data": {}, + "message": { + "type": "string" + } + } + }, + "github_com_insmtx_SingerOS_backend_types.SessionMetadata": { + "type": "object", + "properties": { + "extra": { + "description": "元数据 - 其他扩展字段", + "type": "object", + "additionalProperties": true + }, + "ip_address": { + "description": "元数据 - IP地址", + "type": "string" + }, + "tags": { + "description": "元数据 - 自定义标签", + "type": "array", + "items": { + "type": "string" + } + }, + "user_agent": { + "description": "元数据 - 用户代理信息", + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml new file mode 100644 index 0000000..d9fa52d --- /dev/null +++ b/docs/swagger.yaml @@ -0,0 +1,790 @@ +basePath: /v1 +definitions: + backend_internal_api_handler.DeleteDigitalAssistantRequest: + properties: + id: + type: integer + required: + - id + type: object + backend_internal_api_handler.DeleteSessionRequest: + properties: + id: + type: integer + required: + - id + type: object + backend_internal_api_handler.GetDigitalAssistantRequest: + properties: + code: + type: string + id: + type: integer + type: object + backend_internal_api_handler.GetSessionRequest: + properties: + id: + type: integer + session_id: + type: string + type: object + backend_internal_api_handler.SessionIDRequest: + properties: + id: + type: integer + required: + - id + type: object + backend_internal_api_handler.UpdateDigitalAssistantConfigRequest: + properties: + config: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_contract.AssistantConfig' + id: + type: integer + required: + - config + - id + type: object + backend_internal_api_handler.UpdateDigitalAssistantRequest: + properties: + avatar: + type: string + config: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_contract.AssistantConfig' + description: + type: string + id: + type: integer + name: + type: string + required: + - id + type: object + backend_internal_api_handler.UpdateDigitalAssistantStatusRequest: + properties: + id: + type: integer + status: + type: string + required: + - id + - status + type: object + backend_internal_api_handler.UpdateSessionRequest: + properties: + expired_at: + type: string + id: + type: integer + metadata: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_types.SessionMetadata' + title: + type: string + required: + - id + type: object + github_com_insmtx_SingerOS_backend_internal_api_contract.AssistantConfig: + properties: + channels: + items: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_contract.ChannelRef' + type: array + knowledge: + items: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_contract.KnowledgeRef' + type: array + llm_config: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_contract.LLMConfig' + memory_config: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_contract.MemoryConfig' + policies_config: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_contract.PolicyConfig' + runtime_config: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_contract.RuntimeConfig' + skills: + items: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_contract.SkillRef' + type: array + type: object + github_com_insmtx_SingerOS_backend_internal_api_contract.ChannelRef: + properties: + type: + type: string + type: object + github_com_insmtx_SingerOS_backend_internal_api_contract.CreateDigitalAssistantRequest: + properties: + avatar: + type: string + code: + type: string + config: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_contract.AssistantConfig' + description: + type: string + name: + type: string + org_id: + type: integer + owner_id: + type: integer + required: + - code + - config + - name + - org_id + - owner_id + type: object + github_com_insmtx_SingerOS_backend_internal_api_contract.CreateSessionRequest: + properties: + assistant_code: + type: string + assistant_id: + type: integer + expired_at: + type: string + metadata: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_types.SessionMetadata' + session_id: + type: string + title: + type: string + type: + type: string + user_id: + type: integer + required: + - type + type: object + github_com_insmtx_SingerOS_backend_internal_api_contract.DigitalAssistant: + properties: + avatar: + type: string + code: + type: string + config: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_contract.AssistantConfig' + created_at: + type: string + description: + type: string + id: + type: integer + name: + type: string + org_id: + type: integer + owner_id: + type: integer + status: + type: string + updated_at: + type: string + version: + type: integer + type: object + github_com_insmtx_SingerOS_backend_internal_api_contract.KnowledgeRef: + properties: + dataset_id: + type: string + repo: + type: string + type: + type: string + type: object + github_com_insmtx_SingerOS_backend_internal_api_contract.LLMConfig: + properties: + type: + type: string + type: object + github_com_insmtx_SingerOS_backend_internal_api_contract.ListDigitalAssistantRequest: + properties: + keyword: + type: string + org_id: + type: integer + owner_id: + type: integer + page: + type: integer + per_page: + type: integer + status: + type: string + type: object + github_com_insmtx_SingerOS_backend_internal_api_contract.ListSessionsRequest: + properties: + assistant_code: + type: string + assistant_id: + type: integer + keyword: + type: string + page: + type: integer + per_page: + type: integer + status: + type: string + type: + type: string + user_id: + type: integer + type: object + github_com_insmtx_SingerOS_backend_internal_api_contract.MemoryConfig: + properties: + type: + type: string + type: object + github_com_insmtx_SingerOS_backend_internal_api_contract.PolicyConfig: + properties: + type: + type: string + type: object + github_com_insmtx_SingerOS_backend_internal_api_contract.RuntimeConfig: + properties: + type: + type: string + type: object + github_com_insmtx_SingerOS_backend_internal_api_contract.SkillRef: + properties: + skill_code: + type: string + version: + type: string + type: object + github_com_insmtx_SingerOS_backend_internal_api_dto.BaseResponse: + properties: + code: + type: integer + message: + type: string + type: object + github_com_insmtx_SingerOS_backend_internal_api_dto.CreateDigitalAssistantResponse: + properties: + code: + type: integer + data: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_contract.DigitalAssistant' + message: + type: string + type: object + github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse: + properties: + code: + type: integer + details: + type: string + message: + type: string + type: object + github_com_insmtx_SingerOS_backend_internal_api_dto.Response: + properties: + code: + type: integer + data: {} + message: + type: string + type: object + github_com_insmtx_SingerOS_backend_types.SessionMetadata: + properties: + extra: + additionalProperties: true + description: 元数据 - 其他扩展字段 + type: object + ip_address: + description: 元数据 - IP地址 + type: string + tags: + description: 元数据 - 自定义标签 + items: + type: string + type: array + user_agent: + description: 元数据 - 用户代理信息 + type: string + type: object +host: localhost:8080 +info: + contact: {} + description: SingerOS 数字助手平台 API,提供数字助手管理、技能调用、事件处理等功能 + title: SingerOS API + version: "1.0" +paths: + /ActivateSession: + post: + consumes: + - application/json + description: 激活已结束会话 + parameters: + - description: 激活会话请求 + in: body + name: body + required: true + schema: + $ref: '#/definitions/backend_internal_api_handler.SessionIDRequest' + produces: + - application/json + responses: + "200": + description: 成功响应 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.Response' + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse' + "404": + description: 资源不存在 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse' + "500": + description: 内部服务器错误 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse' + summary: 激活会话 + tags: + - Session + /CreateDigitalAssistant: + post: + consumes: + - application/json + description: 创建一个新的数字助手实例 + parameters: + - description: 创建数字助手请求 + in: body + name: body + required: true + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_contract.CreateDigitalAssistantRequest' + produces: + - application/json + responses: + "200": + description: 成功响应 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.CreateDigitalAssistantResponse' + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse' + "401": + description: 未认证 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse' + "500": + description: 内部服务器错误 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse' + summary: 创建数字助手 + tags: + - DigitalAssistant + /CreateSession: + post: + consumes: + - application/json + description: 创建一个新的会话实例 + parameters: + - description: 创建会话请求 + in: body + name: body + required: true + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_contract.CreateSessionRequest' + produces: + - application/json + responses: + "200": + description: 成功响应 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.Response' + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse' + "500": + description: 内部服务器错误 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse' + summary: 创建会话 + tags: + - Session + /DeleteDigitalAssistant: + post: + consumes: + - application/json + description: 根据ID删除数字助手 + parameters: + - description: 删除数字助手请求 + in: body + name: body + required: true + schema: + $ref: '#/definitions/backend_internal_api_handler.DeleteDigitalAssistantRequest' + produces: + - application/json + responses: + "200": + description: 成功响应 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.BaseResponse' + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse' + "401": + description: 未认证 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse' + "403": + description: 权限不足 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse' + "404": + description: 资源不存在 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse' + "500": + description: 内部服务器错误 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse' + summary: 删除数字助手 + tags: + - DigitalAssistant + /DeleteSession: + post: + consumes: + - application/json + description: 根据ID删除会话 + parameters: + - description: 删除会话请求 + in: body + name: body + required: true + schema: + $ref: '#/definitions/backend_internal_api_handler.DeleteSessionRequest' + produces: + - application/json + responses: + "200": + description: 成功响应 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.Response' + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse' + "404": + description: 资源不存在 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse' + "500": + description: 内部服务器错误 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse' + summary: 删除会话 + tags: + - Session + /GetDigitalAssistant: + post: + consumes: + - application/json + description: 根据ID或Code获取数字助手详情 + parameters: + - description: 获取数字助手请求 + in: body + name: body + required: true + schema: + $ref: '#/definitions/backend_internal_api_handler.GetDigitalAssistantRequest' + produces: + - application/json + responses: + "200": + description: 成功响应 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.CreateDigitalAssistantResponse' + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse' + "401": + description: 未认证 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse' + "403": + description: 权限不足 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse' + "404": + description: 资源不存在 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse' + "500": + description: 内部服务器错误 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse' + summary: 获取数字助手详情 + tags: + - DigitalAssistant + /GetSession: + post: + consumes: + - application/json + description: 根据ID或SessionID获取会话详情 + parameters: + - description: 获取会话请求 + in: body + name: body + required: true + schema: + $ref: '#/definitions/backend_internal_api_handler.GetSessionRequest' + produces: + - application/json + responses: + "200": + description: 成功响应 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.Response' + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse' + "404": + description: 资源不存在 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse' + "500": + description: 内部服务器错误 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse' + summary: 获取会话详情 + tags: + - Session + /ListDigitalAssistant: + post: + consumes: + - application/json + description: 分页查询数字助手列表 + parameters: + - description: 查询列表请求 + in: body + name: body + required: true + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_contract.ListDigitalAssistantRequest' + produces: + - application/json + responses: + "200": + description: 成功响应 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.CreateDigitalAssistantResponse' + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse' + "401": + description: 未认证 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse' + "500": + description: 内部服务器错误 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse' + summary: 查询数字助手列表 + tags: + - DigitalAssistant + /ListSessions: + post: + consumes: + - application/json + description: 分页查询会话列表 + parameters: + - description: 查询列表请求 + in: body + name: body + required: true + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_contract.ListSessionsRequest' + produces: + - application/json + responses: + "200": + description: 成功响应 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.Response' + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse' + "500": + description: 内部服务器错误 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse' + summary: 查询会话列表 + tags: + - Session + /UpdateDigitalAssistant: + post: + consumes: + - application/json + description: 更新数字助手基本信息 + parameters: + - description: 更新数字助手请求 + in: body + name: body + required: true + schema: + $ref: '#/definitions/backend_internal_api_handler.UpdateDigitalAssistantRequest' + produces: + - application/json + responses: + "200": + description: 成功响应 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.CreateDigitalAssistantResponse' + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse' + "401": + description: 未认证 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse' + "403": + description: 权限不足 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse' + "404": + description: 资源不存在 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse' + "500": + description: 内部服务器错误 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse' + summary: 更新数字助手 + tags: + - DigitalAssistant + /UpdateDigitalAssistantConfig: + post: + consumes: + - application/json + description: 更新数字助手的配置信息 + parameters: + - description: 更新配置请求 + in: body + name: body + required: true + schema: + $ref: '#/definitions/backend_internal_api_handler.UpdateDigitalAssistantConfigRequest' + produces: + - application/json + responses: + "200": + description: 成功响应 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.CreateDigitalAssistantResponse' + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse' + "401": + description: 未认证 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse' + "403": + description: 权限不足 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse' + "404": + description: 资源不存在 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse' + "500": + description: 内部服务器错误 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse' + summary: 更新数字助手配置 + tags: + - DigitalAssistant + /UpdateDigitalAssistantStatus: + post: + consumes: + - application/json + description: 更新数字助手的运行状态 + parameters: + - description: 更新状态请求 + in: body + name: body + required: true + schema: + $ref: '#/definitions/backend_internal_api_handler.UpdateDigitalAssistantStatusRequest' + produces: + - application/json + responses: + "200": + description: 成功响应 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.BaseResponse' + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse' + "401": + description: 未认证 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse' + "403": + description: 权限不足 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse' + "404": + description: 资源不存在 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse' + "500": + description: 内部服务器错误 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse' + summary: 更新数字助手状态 + tags: + - DigitalAssistant + /UpdateSession: + post: + consumes: + - application/json + description: 更新会话基本信息 + parameters: + - description: 更新会话请求 + in: body + name: body + required: true + schema: + $ref: '#/definitions/backend_internal_api_handler.UpdateSessionRequest' + produces: + - application/json + responses: + "200": + description: 成功响应 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.Response' + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse' + "404": + description: 资源不存在 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse' + "500": + description: 内部服务器错误 + schema: + $ref: '#/definitions/github_com_insmtx_SingerOS_backend_internal_api_dto.ErrorResponse' + summary: 更新会话 + tags: + - Session +schemes: +- http +- https +swagger: "2.0"