feat[Go]: implement /api/v1/agents/<agent_id> and test_db_connection (#15771)

### What problem does this PR solve?

Add two API in go
```
/api/v1/agents/test_db_connection POST

/api/v1/agents/<agent_id>/sessions DELETE
```

### Type of change

- [x] New Feature (non-breaking change which adds functionality)

---------

Co-authored-by: Yingfeng <yingfeng.zhang@gmail.com>
This commit is contained in:
Haruko386
2026-06-10 09:54:07 +08:00
committed by GitHub
parent 87b8062df4
commit a396b1ace2
7 changed files with 1173 additions and 1 deletions

View File

@@ -19,6 +19,7 @@ package handler
import (
"errors"
"fmt"
"io"
"mime/multipart"
"net/http"
"strconv"
@@ -307,6 +308,89 @@ func (h *AgentHandler) DeleteAgentSessionItem(c *gin.Context) {
})
}
type deleteAgentSessionsRequest struct {
IDs []string `json:"ids"`
DeleteAll bool `json:"delete_all,omitempty"`
}
func (h *AgentHandler) DeleteAgentSessions(c *gin.Context) {
user, errorCode, errorMessage := GetUser(c)
if errorCode != common.CodeSuccess {
jsonError(c, errorCode, errorMessage)
return
}
agentID := strings.TrimSpace(c.Param("agent_id"))
if agentID == "" {
c.JSON(http.StatusOK, gin.H{
"code": common.CodeOperatingError,
"data": nil,
"message": "agent_id is required",
})
return
}
var req deleteAgentSessionsRequest
if err := c.ShouldBindJSON(&req); err != nil && !errors.Is(err, io.EOF) {
c.JSON(http.StatusOK, gin.H{
"code": common.CodeBadRequest,
"data": false,
"message": err.Error(),
})
return
}
result, code, err := h.agentService.DeleteAgentSessions(strings.TrimSpace(user.ID), agentID, req.IDs, req.DeleteAll)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"code": code,
"message": err.Error(),
})
return
}
response := gin.H{"code": common.CodeSuccess}
if result != nil && result.Data != nil {
response["data"] = result.Data
}
if result != nil && result.Message != "" {
response["message"] = result.Message
}
c.JSON(http.StatusOK, response)
}
// TestDBConnection Test DB connection
func (h *AgentHandler) TestDBConnection(c *gin.Context) {
user, errorCode, errorMessage := GetUser(c)
if errorCode != common.CodeSuccess {
jsonError(c, errorCode, errorMessage)
return
}
var req service.TestDBConnectionRequest
if err := c.ShouldBindJSON(&req); err != nil && !errors.Is(err, io.EOF) {
c.JSON(http.StatusOK, gin.H{
"code": common.CodeBadRequest,
"message": err.Error(),
})
return
}
code, err := h.agentService.TestDBConnection(user.ID, &req)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"code": code,
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": common.CodeSuccess,
"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

@@ -508,6 +508,215 @@ func TestDeleteAgentSessionItemHandlerIgnoresSessionFromAnotherAgent(t *testing.
}
}
func TestDeleteAgentSessionsHandlerDeletesDuplicateIDsPartially(t *testing.T) {
c, w, db := setupGinContextWithUserAndDB(t, http.MethodDelete, "/api/v1/agents/canvas-1/sessions")
c.Request = httptest.NewRequest(http.MethodDelete, "/api/v1/agents/canvas-1/sessions", strings.NewReader(`{"ids":["session-1","session-1"]}`))
c.Request.Header.Set("Content-Type", "application/json")
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(`[]`),
Reference: json.RawMessage(`[]`),
})
h := NewAgentHandler(service.NewAgentService(), nil)
h.DeleteAgentSessions(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"])
}
data, ok := resp["data"].(map[string]interface{})
if !ok {
t.Fatalf("expected partial data object, got %T", resp["data"])
}
if data["success_count"] != float64(1) {
t.Fatalf("expected success_count 1, got %v", data["success_count"])
}
errorsList, ok := data["errors"].([]interface{})
if !ok || len(errorsList) != 1 {
t.Fatalf("expected one duplicate error, got %v", data["errors"])
}
if errorsList[0] != "Duplicate session ids: session-1" {
t.Fatalf("unexpected duplicate error: %v", errorsList[0])
}
var count int64
if err := db.Model(&entity.API4Conversation{}).Where("id = ?", "session-1").Count(&count).Error; err != nil {
t.Fatalf("failed to count deleted session: %v", err)
}
if count != 0 {
t.Fatalf("expected session-1 to be deleted, count=%d", count)
}
}
func TestDeleteAgentSessionsHandlerDeleteAll(t *testing.T) {
c, w, db := setupGinContextWithUserAndDB(t, http.MethodDelete, "/api/v1/agents/canvas-1/sessions")
c.Request = httptest.NewRequest(http.MethodDelete, "/api/v1/agents/canvas-1/sessions", strings.NewReader(`{"delete_all":true}`))
c.Request.Header.Set("Content-Type", "application/json")
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(`[]`),
Reference: json.RawMessage(`[]`),
})
db.Create(&entity.API4Conversation{
ID: "session-2",
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.DeleteAgentSessions(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"])
}
var ownCount int64
if err := db.Model(&entity.API4Conversation{}).Where("dialog_id = ?", "canvas-1").Count(&ownCount).Error; err != nil {
t.Fatalf("failed to count own sessions: %v", err)
}
if ownCount != 0 {
t.Fatalf("expected all canvas-1 sessions to be deleted, count=%d", ownCount)
}
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 other agent session to remain, count=%d", otherCount)
}
}
func TestDeleteAgentSessionsHandlerRequiresOwner(t *testing.T) {
c, w, db := setupGinContextWithUserAndDB(t, http.MethodDelete, "/api/v1/agents/canvas-1/sessions")
c.Request = httptest.NewRequest(http.MethodDelete, "/api/v1/agents/canvas-1/sessions", strings.NewReader(`{"ids":["session-1"]}`))
c.Request.Header.Set("Content-Type", "application/json")
c.Params = gin.Params{{Key: "agent_id", Value: "canvas-1"}}
db.Create(&entity.UserCanvas{
ID: "canvas-1",
UserID: "user-2",
Permission: "team",
Title: sptr("Team Agent"),
})
db.Create(&entity.API4Conversation{
ID: "session-1",
DialogID: "canvas-1",
UserID: "user-1",
Message: json.RawMessage(`[]`),
Reference: json.RawMessage(`[]`),
})
h := NewAgentHandler(service.NewAgentService(), nil)
h.DeleteAgentSessions(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.CodeDataError) {
t.Fatalf("expected code %d, got %v: %v", common.CodeDataError, resp["code"], resp["message"])
}
var count int64
if err := db.Model(&entity.API4Conversation{}).Where("id = ?", "session-1").Count(&count).Error; err != nil {
t.Fatalf("failed to count session: %v", err)
}
if count != 1 {
t.Fatalf("expected session to remain, count=%d", count)
}
}
func TestTestDBConnectionHandlerMissingFields(t *testing.T) {
c, w, _ := setupGinContextWithUserAndDB(t, http.MethodPost, "/api/v1/agents/test_db_connection")
c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/agents/test_db_connection", strings.NewReader(`{"db_type":"mysql"}`))
c.Request.Header.Set("Content-Type", "application/json")
h := NewAgentHandler(service.NewAgentService(), nil)
h.TestDBConnection(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.CodeArgumentError) {
t.Fatalf("expected code %d, got %v: %v", common.CodeArgumentError, resp["code"], resp["message"])
}
if resp["data"] != nil {
t.Fatalf("expected nil data, got %v", resp["data"])
}
want := "required argument are missing: database,username,host,port,password; "
if resp["message"] != want {
t.Fatalf("expected message %q, got %v", want, resp["message"])
}
}
func TestTestDBConnectionHandlerRejectsLocalhost(t *testing.T) {
c, w, _ := setupGinContextWithUserAndDB(t, http.MethodPost, "/api/v1/agents/test_db_connection")
c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/agents/test_db_connection", strings.NewReader(`{
"db_type":"mysql",
"database":"rag_flow",
"username":"root",
"host":"localhost",
"port":3306,
"password":"infini_rag_flow"
}`))
c.Request.Header.Set("Content-Type", "application/json")
h := NewAgentHandler(service.NewAgentService(), nil)
h.TestDBConnection(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.CodeDataError) {
t.Fatalf("expected code %d, got %v: %v", common.CodeDataError, resp["code"], resp["message"])
}
if resp["data"] != nil {
t.Fatalf("expected nil data, got %v", resp["data"])
}
message, ok := resp["message"].(string)
if !ok || !strings.Contains(message, "non-public address") {
t.Fatalf("expected non-public host message, got %v", resp["message"])
}
}
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"]}`))