feat(go-api): add chat update endpoints (#16378)

## Summary

- Added Go API route `PUT /api/v1/chats/:chat_id` to align with Python
`PUT /api/v1/chats/<chat_id>` chat update behavior.
- Added Go API route `PATCH /api/v1/chats/:chat_id` to align with Python
`PATCH /api/v1/chats/<chat_id>` partial chat update behavior.
- Added matching handler and service logic for owner checks, tenant
validation, persisted-field filtering, read-only field filtering,
`dataset_ids` to `kb_ids` conversion, and PATCH shallow merge semantics
for `prompt_config` and `llm_setting`.
This commit is contained in:
Hz_
2026-06-26 19:22:57 +08:00
committed by GitHub
parent a1f1dd5007
commit e3063da390
5 changed files with 692 additions and 1 deletions

View File

@@ -503,3 +503,57 @@ func (h *ChatHandler) GetChat(c *gin.Context) {
"message": "success",
})
}
// UpdateChat updates a chat by ID using REST PUT semantics.
func (h *ChatHandler) UpdateChat(c *gin.Context) {
h.updateChatByMethod(c, false)
}
// PatchChat updates a chat by ID using REST PATCH semantics.
func (h *ChatHandler) PatchChat(c *gin.Context) {
h.updateChatByMethod(c, true)
}
func (h *ChatHandler) updateChatByMethod(c *gin.Context, patch bool) {
user, errorCode, errorMessage := GetUser(c)
if errorCode != common.CodeSuccess {
jsonError(c, errorCode, errorMessage)
return
}
chatID := c.Param("chat_id")
if chatID == "" {
jsonError(c, common.CodeBadRequest, "chat_id is required")
return
}
var req map[string]interface{}
if err := c.ShouldBindJSON(&req); err != nil {
jsonError(c, common.CodeDataError, err.Error())
return
}
var (
result map[string]interface{}
err error
)
if patch {
result, err = h.chatService.PatchChat(user.ID, chatID, req)
} else {
result, err = h.chatService.UpdateChat(user.ID, chatID, req)
}
if err != nil {
if err.Error() == "no authorization" {
c.JSON(http.StatusOK, gin.H{
"code": common.CodeAuthenticationError,
"data": false,
"message": "No authorization.",
})
return
}
jsonError(c, common.CodeDataError, err.Error())
return
}
jsonResponse(c, common.CodeSuccess, result, "success")
}

View File

@@ -25,7 +25,7 @@ func setupChatHandlerTestDB(t *testing.T) *gorm.DB {
t.Fatalf("failed to open sqlite: %v", err)
}
if err := db.AutoMigrate(&entity.Chat{}); err != nil {
if err := db.AutoMigrate(&entity.Chat{}, &entity.Tenant{}); err != nil {
t.Fatalf("failed to migrate test schema: %v", err)
}
@@ -33,6 +33,20 @@ func setupChatHandlerTestDB(t *testing.T) *gorm.DB {
dao.DB = db
t.Cleanup(func() { dao.DB = origDB })
status := string(entity.StatusValid)
if err := db.Create(&entity.Tenant{
ID: "user-1",
LLMID: "model-a",
EmbdID: "embd-a",
ASRID: "asr-a",
Img2TxtID: "img2txt-a",
RerankID: "rerank-a",
ParserIDs: "naive",
Status: &status,
}).Error; err != nil {
t.Fatalf("failed to create tenant: %v", err)
}
return db
}
@@ -120,3 +134,68 @@ func TestBulkDeleteChatsHandlerPartialSuccess(t *testing.T) {
t.Fatalf("unexpected message: %v", resp["message"])
}
}
func TestPatchChatHandlerSuccess(t *testing.T) {
db := setupChatHandlerTestDB(t)
createChatHandlerTestChat(t, db, "chat-1", "user-1")
h := NewChatHandler(service.NewChatService(), service.NewUserService())
c, w := setupGinContextWithUser("PATCH", "/api/v1/chats/chat-1", `{"name":" updated chat "}`)
c.Params = []gin.Param{{Key: "chat_id", Value: "chat-1"}}
h.PatchChat(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 decode response: %v", err)
}
if resp["code"] != float64(common.CodeSuccess) {
t.Fatalf("expected success code, got %v", resp["code"])
}
data, ok := resp["data"].(map[string]interface{})
if !ok {
t.Fatalf("expected object data, got %+v", resp["data"])
}
if data["name"] != "updated chat" {
t.Fatalf("expected trimmed name in response, got %+v", data["name"])
}
if _, ok := data["kb_ids"]; ok {
t.Fatalf("response must not expose kb_ids: %+v", data)
}
if _, ok := data["dataset_ids"]; !ok {
t.Fatalf("response should expose dataset_ids: %+v", data)
}
}
func TestUpdateChatHandlerRejectsNonOwner(t *testing.T) {
db := setupChatHandlerTestDB(t)
createChatHandlerTestChat(t, db, "chat-1", "tenant-2")
h := NewChatHandler(service.NewChatService(), service.NewUserService())
c, w := setupGinContextWithUser("PUT", "/api/v1/chats/chat-1", `{"name":"updated"}`)
c.Params = []gin.Param{{Key: "chat_id", Value: "chat-1"}}
h.UpdateChat(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 decode response: %v", err)
}
if resp["code"] != float64(common.CodeAuthenticationError) {
t.Fatalf("expected auth error code, got %v", resp["code"])
}
if resp["data"] != false {
t.Fatalf("expected data=false, got %v", resp["data"])
}
if resp["message"] != "No authorization." {
t.Fatalf("unexpected message: %v", resp["message"])
}
}