feat: Implement API of ragflow server in Go (#15256)

## Summary
- Implemented the Go API endpoint for Memory message forgetting:
  - `DELETE /api/v1/messages/{memory_id}:{message_id}`
- Added route registration for the Memory message DELETE endpoint only.
- Added request path validation for `memory_id:message_id`.
- Added service logic to mark a message as forgotten by setting
`forget_at`.
- Preserved Python-compatible response behavior:
  - Success returns `code: 0`, `message: true`, `data: null`.
- Added focused unit tests for message path parsing and invalid message
ID handling.
- Fixed Linux cgo linker config to use the installed shared PCRE2
library so Go tests/builds can run in this environment.
## Related Issue
Closes: #15240 
## Change Type
- [x] Feature
- [x] Test
- [x] Build / CI compatibility

## Implemented API
- `DELETE /api/v1/messages/{memory_id}:{message_id}`
## Real Behavior Proof
Validated with targeted Go tests:
```bash
/tmp/go1.25.0/bin/go test ./internal/handler ./internal/router
```
Result:
```text
ok  	ragflow/internal/handler
?   	ragflow/internal/router	[no test files]
```
Validated server entrypoint build:
```bash
/tmp/go1.25.0/bin/go build -o /tmp/ragflow-server-main ./cmd/server_main.go
```

Result:
```text
build succeeded
```
Validated patch formatting:
```bash
git diff --check
```

Result:

```text
no whitespace errors
```
## Checklist
- [x] Implemented only `DELETE
/api/v1/messages/{memory_id}:{message_id}`.
- [x] Did not implement unrelated Memory message APIs.
- [x] Added route registration.
- [x] Added handler validation.
- [x] Added service-level memory access check.
- [x] Added tests.
- [x] Ran targeted Go tests.
- [x] Ran server build validation.
- [x] Ran `git diff --check`.
This commit is contained in:
Jonathan Chang
2026-06-10 20:27:35 +07:00
committed by GitHub
parent 3e4fb8cf1c
commit dfcf226ba3
10 changed files with 322 additions and 26 deletions

View File

@@ -20,6 +20,7 @@
package handler
import (
"errors"
"net/http"
"os"
"strconv"
@@ -549,8 +550,8 @@ func (h *MemoryHandler) AddMessage(c *gin.Context) {
})
}
// ForgetMessage handles DELETE request for forgetting messages
// API Path: DELETE /api/v1/messages/:memory_id/:message_id
// ForgetMessage handles DELETE request for forgetting messages.
// API Path: DELETE /api/v1/messages/{memory_id}:{message_id}
//
// Function:
// - Soft-deletes the specified message (sets forget_at timestamp)
@@ -559,16 +560,82 @@ func (h *MemoryHandler) AddMessage(c *gin.Context) {
// Parameter Format:
// - memory_id: Memory ID
// - message_id: Message ID (integer)
//
// TODO: Implementation pending - depends on embedding engine
func (h *MemoryHandler) ForgetMessage(c *gin.Context) {
user, errorCode, errorMessage := GetUser(c)
if errorCode != common.CodeSuccess {
jsonError(c, errorCode, errorMessage)
return
}
memoryID, messageID, err := parseMemoryMessagePath(c.Param("memory_message"))
if err != nil {
c.JSON(http.StatusOK, gin.H{
"code": common.CodeArgumentError,
"message": err.Error(),
"data": nil,
})
return
}
if err := h.memoryService.ForgetMessage(c.Request.Context(), user.ID, memoryID, messageID); err != nil {
errMsg := err.Error()
if isMemoryServiceNotFound(err) {
c.JSON(http.StatusOK, gin.H{
"code": common.CodeNotFound,
"message": errMsg,
"data": nil,
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": common.CodeServerError,
"message": "Internal server error",
"data": nil,
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": common.CodeServerError,
"message": "ForgetMessage not implemented - pending embedding engine dependency",
"code": common.CodeSuccess,
"message": true,
"data": nil,
})
}
func isMemoryServiceNotFound(err error) bool {
var notFoundErr *service.ResourceNotFoundError
return errors.As(err, &notFoundErr) && notFoundErr.Resource == "Memory"
}
func parseMemoryMessagePath(memoryMessage string) (string, int64, error) {
memoryMessage = strings.TrimSpace(memoryMessage)
if memoryMessage == "" {
return "", 0, errors.New("memory_id and message_id are required")
}
parts := strings.Split(memoryMessage, ":")
if len(parts) != 2 {
return "", 0, errors.New("message path must be formatted as memory_id:message_id")
}
memoryID := strings.TrimSpace(parts[0])
messageIDText := strings.TrimSpace(parts[1])
if memoryID == "" {
return "", 0, errors.New("memory_id is required")
}
if messageIDText == "" {
return "", 0, errors.New("message_id is required")
}
messageID, err := strconv.ParseInt(messageIDText, 10, 64)
if err != nil || messageID < 0 {
return "", 0, errors.New("message_id must be a non-negative integer")
}
return memoryID, messageID, nil
}
// UpdateMessage handles PUT request for updating message status
// API Path: PUT /api/v1/messages/:memory_id/:message_id
//

View File

@@ -0,0 +1,91 @@
package handler
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"ragflow/internal/common"
"ragflow/internal/entity"
"ragflow/internal/service"
)
func TestIsMemoryServiceNotFound(t *testing.T) {
notFoundErr := &service.ResourceNotFoundError{Resource: "Memory", ID: "memory-1"}
if !isMemoryServiceNotFound(fmt.Errorf("wrapped: %w", notFoundErr)) {
t.Fatal("expected wrapped service not found error to map to not found")
}
messageNotFoundErr := &service.ResourceNotFoundError{Resource: "Message", ID: "message-1"}
if isMemoryServiceNotFound(messageNotFoundErr) {
t.Fatal("expected non-memory resource not found error to avoid memory 404 mapping")
}
if isMemoryServiceNotFound(fmt.Errorf("backend index does not exist")) {
t.Fatal("backend text should not map to not found without service error type")
}
}
func TestParseMemoryMessagePath(t *testing.T) {
tests := []struct {
name string
value string
wantMemoryID string
wantMessageID int64
wantErr bool
}{
{name: "valid", value: "memory-1:42", wantMemoryID: "memory-1", wantMessageID: 42},
{name: "empty", value: "", wantErr: true},
{name: "missing message id", value: "memory-1:", wantErr: true},
{name: "missing memory id", value: ":42", wantErr: true},
{name: "invalid message id", value: "memory-1:not-int", wantErr: true},
{name: "negative message id", value: "memory-1:-1", wantErr: true},
{name: "too many separators", value: "memory-1:2:3", wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
memoryID, messageID, err := parseMemoryMessagePath(tt.value)
if tt.wantErr {
if err == nil {
t.Fatalf("expected error")
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if memoryID != tt.wantMemoryID || messageID != tt.wantMessageID {
t.Fatalf("got (%q, %d), want (%q, %d)", memoryID, messageID, tt.wantMemoryID, tt.wantMessageID)
}
})
}
}
func TestForgetMessageRejectsMalformedPath(t *testing.T) {
gin.SetMode(gin.TestMode)
router := gin.New()
h := NewMemoryHandler(service.NewMemoryService())
router.DELETE("/api/v1/messages/:memory_message", func(c *gin.Context) {
c.Set("user", &entity.User{ID: "user-1"})
h.ForgetMessage(c)
})
req := httptest.NewRequest(http.MethodDelete, "/api/v1/messages/memory-1:not-int", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status = %d, want %d", w.Code, http.StatusOK)
}
var res map[string]interface{}
if err := json.Unmarshal(w.Body.Bytes(), &res); err != nil {
t.Fatalf("decode response: %v", err)
}
if code := common.ErrorCode(res["code"].(float64)); code != common.CodeArgumentError {
t.Fatalf("code = %v, want %v; body=%s", code, common.CodeArgumentError, w.Body.String())
}
}