feat[Go]: implement /api/v1/agents/<agent_id>/sessions (#15705)

### What problem does this PR solve?

As Title
Codes were tested by Postman

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
Haruko386
2026-06-08 16:26:27 +08:00
committed by GitHub
parent e2b0da9eea
commit 67ce0c896d
11 changed files with 1310 additions and 31 deletions

View File

@@ -131,6 +131,182 @@ func (h *AgentHandler) ListAgents(c *gin.Context) {
})
}
// ListAgentSessions List all sessions
func (h *AgentHandler) ListAgentSessions(c *gin.Context) {
user, errorCode, errorMessage := GetUser(c)
if errorCode != common.CodeSuccess {
jsonError(c, errorCode, errorMessage)
return
}
agentID := c.Param("agent_id")
if agentID == "" {
c.JSON(http.StatusOK, gin.H{
"code": common.CodeArgumentError,
"data": nil,
"message": "agent_id is required",
})
return
}
page := parsePositiveIntQuery(c, "page", 1)
pageSize := parsePositiveIntQuery(c, "page_size", 30)
if pageSize > 100 {
pageSize = 100
}
req := service.ListAgentSessionsRequest{
SessionID: c.Query("id"),
UserID: c.Query("user_id"),
Page: page,
PageSize: pageSize,
Keywords: c.Query("keywords"),
FromDate: c.Query("from_date"),
ToDate: c.Query("to_date"),
OrderBy: defaultQueryString(c.Query("orderby"), "update_time"),
ExpUserID: c.Query("exp_user_id"),
Desc: c.Query("desc") != "False" && c.Query("desc") != "false",
IncludeDSL: c.Query("dsl") != "False" && c.Query("dsl") != "false",
}
tenantID := user.ID
result, code, err := h.agentService.ListAgentSessions(user.ID, tenantID, agentID, req)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"code": code,
"data": nil,
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": common.CodeSuccess,
"data": result.Data,
"message": "success",
"total": result.Total,
})
}
func parsePositiveIntQuery(c *gin.Context, key string, defaultValue int) int {
raw := c.Query(key)
if raw == "" {
return defaultValue
}
value, err := strconv.Atoi(raw)
if err != nil || value <= 0 {
return defaultValue
}
return value
}
func defaultQueryString(value, defaultValue string) string {
if value == "" {
return defaultValue
}
return value
}
func (h *AgentHandler) GetAgentSession(c *gin.Context) {
user, errorCode, errorMessage := GetUser(c)
if errorCode != common.CodeSuccess {
jsonError(c, errorCode, errorMessage)
return
}
agentID := c.Param("agent_id")
if agentID == "" {
c.JSON(http.StatusOK, gin.H{
"code": common.CodeOperatingError,
"data": nil,
"message": "agent_id is required",
})
return
}
sessionID := c.Param("session_id")
if sessionID == "" {
c.JSON(http.StatusOK, gin.H{
"code": common.CodeDataError,
"data": nil,
"message": "session_id is required",
})
return
}
userID := user.ID
userID = strings.TrimSpace(userID)
sessionID = strings.TrimSpace(sessionID)
agentID = strings.TrimSpace(agentID)
data, code, err := h.agentService.GetAgentSession(userID, agentID, sessionID)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"code": code,
"data": nil,
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": common.CodeSuccess,
"data": data,
"message": "success",
})
}
func (h *AgentHandler) DeleteAgentSessionItem(c *gin.Context) {
user, errorCode, errorMessage := GetUser(c)
if errorCode != common.CodeSuccess {
jsonError(c, errorCode, errorMessage)
return
}
agentID := c.Param("agent_id")
if agentID == "" {
c.JSON(http.StatusOK, gin.H{
"code": common.CodeOperatingError,
"data": nil,
"message": "agent_id is required",
})
return
}
sessionID := c.Param("session_id")
if sessionID == "" {
c.JSON(http.StatusOK, gin.H{
"code": common.CodeDataError,
"data": nil,
"message": "session_id is required",
})
return
}
userID := user.ID
userID = strings.TrimSpace(userID)
sessionID = strings.TrimSpace(sessionID)
agentID = strings.TrimSpace(agentID)
ok, code, err := h.agentService.DeleteAgentSessionItem(userID, agentID, sessionID)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"code": code,
"data": false,
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": code,
"data": ok,
"message": "success",
})
}
// ListAgentVersions returns versions for a specific agent.
// @Summary List Agent Versions
// @Description Returns all versions for a specific agent, ordered by update_time DESC.

View File

@@ -49,6 +49,7 @@ func setupHandlerAgentsTestDB(t *testing.T) *gorm.DB {
&entity.User{},
&entity.UserCanvas{},
&entity.UserCanvasVersion{},
&entity.API4Conversation{},
); err != nil {
t.Fatalf("failed to migrate: %v", err)
}
@@ -262,6 +263,251 @@ func TestGetAgentVersionHandler_VersionNotFound(t *testing.T) {
}
}
func TestListAgentSessionsHandlerSuccess(t *testing.T) {
c, w, db := setupGinContextWithUserAndDB(t, http.MethodGet, "/api/v1/agents/canvas-1/sessions")
c.Params = gin.Params{{Key: "agent_id", Value: "canvas-1"}}
db.Create(&entity.UserCanvas{
ID: "canvas-1",
UserID: "user-1",
Title: sptr("Test Agent"),
})
db.Create(&entity.API4Conversation{
ID: "session-1",
DialogID: "canvas-1",
UserID: "user-1",
Message: json.RawMessage(`[{"role":"assistant","content":"hello","prompt":"hidden"}]`),
Reference: json.RawMessage(`[]`),
BaseModel: entity.BaseModel{
UpdateTime: ptr(time.Now().UnixMilli()),
},
})
db.Create(&entity.API4Conversation{
ID: "session-2",
DialogID: "canvas-1",
UserID: "user-1",
Message: json.RawMessage(`[{"role":"user","content":"question"}]`),
Reference: json.RawMessage(`[]`),
BaseModel: entity.BaseModel{
UpdateTime: ptr(time.Now().Add(-time.Hour).UnixMilli()),
},
})
db.Create(&entity.API4Conversation{
ID: "session-other-agent",
DialogID: "canvas-other",
UserID: "user-1",
Message: json.RawMessage(`[{"role":"assistant","content":"other"}]`),
Reference: json.RawMessage(`[]`),
})
h := NewAgentHandler(service.NewAgentService(), nil)
h.ListAgentSessions(c)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
}
var resp map[string]interface{}
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if resp["code"] != float64(common.CodeSuccess) {
t.Fatalf("expected code %d, got %v: %v", common.CodeSuccess, resp["code"], resp["message"])
}
if resp["total"] != float64(2) {
t.Fatalf("expected total 2, got %v", resp["total"])
}
data, ok := resp["data"].([]interface{})
if !ok {
t.Fatalf("expected data array, got %T", resp["data"])
}
if len(data) != 2 {
t.Fatalf("expected 2 sessions, got %d", len(data))
}
first := data[0].(map[string]interface{})
if first["agent_id"] != "canvas-1" {
t.Fatalf("expected agent_id canvas-1, got %v", first["agent_id"])
}
messages := first["message"].([]interface{})
message := messages[0].(map[string]interface{})
if _, ok := message["prompt"]; ok {
t.Fatalf("expected prompt to be stripped from list response")
}
}
func TestGetAgentSessionHandlerSuccess(t *testing.T) {
c, w, db := setupGinContextWithUserAndDB(t, http.MethodGet, "/api/v1/agents/canvas-1/sessions/session-1")
c.Params = gin.Params{{Key: "agent_id", Value: "canvas-1"}, {Key: "session_id", Value: "session-1"}}
db.Create(&entity.UserCanvas{
ID: "canvas-1",
UserID: "user-1",
Title: sptr("Test Agent"),
})
db.Create(&entity.API4Conversation{
ID: "session-1",
DialogID: "canvas-1",
UserID: "user-1",
Message: json.RawMessage(`[{"role":"assistant","content":"hello"}]`),
Reference: json.RawMessage(`[]`),
})
h := NewAgentHandler(service.NewAgentService(), nil)
h.GetAgentSession(c)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
}
var resp map[string]interface{}
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if resp["code"] != float64(common.CodeSuccess) {
t.Fatalf("expected code %d, got %v: %v", common.CodeSuccess, resp["code"], resp["message"])
}
data, ok := resp["data"].(map[string]interface{})
if !ok {
t.Fatalf("expected data object, got %T", resp["data"])
}
if data["id"] != "session-1" {
t.Fatalf("expected session-1, got %v", data["id"])
}
if data["dialog_id"] != "canvas-1" {
t.Fatalf("expected dialog_id canvas-1, got %v", data["dialog_id"])
}
}
func TestGetAgentSessionHandlerRejectsSessionFromAnotherAgent(t *testing.T) {
c, w, db := setupGinContextWithUserAndDB(t, http.MethodGet, "/api/v1/agents/canvas-1/sessions/session-other")
c.Params = gin.Params{{Key: "agent_id", Value: "canvas-1"}, {Key: "session_id", Value: "session-other"}}
db.Create(&entity.UserCanvas{
ID: "canvas-1",
UserID: "user-1",
Title: sptr("Test Agent"),
})
db.Create(&entity.API4Conversation{
ID: "session-other",
DialogID: "canvas-other",
UserID: "user-1",
Message: json.RawMessage(`[{"role":"assistant","content":"other"}]`),
Reference: json.RawMessage(`[]`),
})
h := NewAgentHandler(service.NewAgentService(), nil)
h.GetAgentSession(c)
var resp map[string]interface{}
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if resp["code"] == float64(common.CodeSuccess) {
t.Fatalf("expected non-success for cross-agent session, got response %v", resp)
}
if resp["data"] != nil {
t.Fatalf("expected nil data for cross-agent session, got %v", resp["data"])
}
}
func TestDeleteAgentSessionItemHandlerDeletesOnlyMatchingAgent(t *testing.T) {
c, w, db := setupGinContextWithUserAndDB(t, http.MethodDelete, "/api/v1/agents/canvas-1/sessions/session-1")
c.Params = gin.Params{{Key: "agent_id", Value: "canvas-1"}, {Key: "session_id", Value: "session-1"}}
db.Create(&entity.UserCanvas{
ID: "canvas-1",
UserID: "user-1",
Title: sptr("Test Agent"),
})
db.Create(&entity.API4Conversation{
ID: "session-1",
DialogID: "canvas-1",
UserID: "user-1",
Message: json.RawMessage(`[]`),
Reference: json.RawMessage(`[]`),
})
db.Create(&entity.API4Conversation{
ID: "session-other",
DialogID: "canvas-other",
UserID: "user-1",
Message: json.RawMessage(`[]`),
Reference: json.RawMessage(`[]`),
})
h := NewAgentHandler(service.NewAgentService(), nil)
h.DeleteAgentSessionItem(c)
var resp map[string]interface{}
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if resp["code"] != float64(common.CodeSuccess) {
t.Fatalf("expected code %d, got %v: %v", common.CodeSuccess, resp["code"], resp["message"])
}
if resp["data"] != true {
t.Fatalf("expected data true, got %v", resp["data"])
}
var deletedCount int64
if err := db.Model(&entity.API4Conversation{}).Where("id = ?", "session-1").Count(&deletedCount).Error; err != nil {
t.Fatalf("failed to count deleted session: %v", err)
}
if deletedCount != 0 {
t.Fatalf("expected session-1 to be deleted, count=%d", deletedCount)
}
var otherCount int64
if err := db.Model(&entity.API4Conversation{}).Where("id = ?", "session-other").Count(&otherCount).Error; err != nil {
t.Fatalf("failed to count other session: %v", err)
}
if otherCount != 1 {
t.Fatalf("expected session-other to remain, count=%d", otherCount)
}
}
func TestDeleteAgentSessionItemHandlerIgnoresSessionFromAnotherAgent(t *testing.T) {
c, w, db := setupGinContextWithUserAndDB(t, http.MethodDelete, "/api/v1/agents/canvas-1/sessions/session-other")
c.Params = gin.Params{{Key: "agent_id", Value: "canvas-1"}, {Key: "session_id", Value: "session-other"}}
db.Create(&entity.UserCanvas{
ID: "canvas-1",
UserID: "user-1",
Title: sptr("Test Agent"),
})
db.Create(&entity.API4Conversation{
ID: "session-other",
DialogID: "canvas-other",
UserID: "user-1",
Message: json.RawMessage(`[]`),
Reference: json.RawMessage(`[]`),
})
h := NewAgentHandler(service.NewAgentService(), nil)
h.DeleteAgentSessionItem(c)
var resp map[string]interface{}
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if resp["code"] != float64(common.CodeSuccess) {
t.Fatalf("expected code %d, got %v: %v", common.CodeSuccess, resp["code"], resp["message"])
}
if resp["data"] != false {
t.Fatalf("expected data false, got %v", resp["data"])
}
var count int64
if err := db.Model(&entity.API4Conversation{}).Where("id = ?", "session-other").Count(&count).Error; err != nil {
t.Fatalf("failed to count other session: %v", err)
}
if count != 1 {
t.Fatalf("expected cross-agent session to remain, count=%d", count)
}
}
func TestUpdateAgentTagsHandlerSuccess(t *testing.T) {
c, w, db := setupGinContextWithUserAndDB(t, http.MethodPut, "/api/v1/agents/canvas-1/tags")
c.Request = httptest.NewRequest(http.MethodPut, "/api/v1/agents/canvas-1/tags", strings.NewReader(`{"tags":["alpha","beta","alpha"]}`))