mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-03 01:01:56 +08:00
feat(go): implement chatbots/<dialog_id>/info and searchbots/detail (#15420)
### What problem does this PR solve? Part of #15240 (rewriting the RAGFlow API server in Go). Implements the two public bot endpoints from `api/apps/restful_apis/bot_api.py`: - **`GET /api/v1/chatbots/<dialog_id>/info`** (`chatbots_inputs`) — returns `{title, avatar, prologue, has_tavily_key}` for a dialog the authenticated tenant owns (tenant match + `status == VALID`), otherwise `"Authentication error: no access to this chatbot!"`. - **`GET /api/v1/searchbots/detail`** (`detail_share_embedded`) — returns search-app detail for a `search_id` the tenant can access. Permission is checked across the tenant's joined tenants; denial returns `"Has no permission for this operation."` (operating error, `data: false`) and a missing app returns `"Can't find this Search App!"`. Both endpoints authenticate with an SDK **beta token** (`Authorization: Bearer <beta>`) rather than a session — the token is resolved to a tenant via `APIToken.query(beta=token)`, backed by a new `APITokenDAO.GetByBeta`. Because they perform their own token-based auth, the routes are registered on the unauthenticated route group (mirroring the Python blueprint, which has no `@login_required`). Both live in a new `internal/handler/bot.go` + `internal/service/bot.go` since they share the same source module. Handler unit tests cover the auth, success, and error-mapping paths. ### Type of change - [x] New Feature (non-breaking change which adds functionality) --------- Co-authored-by: Claude Code <claude@anthropic.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Ling Qin <qinling0210@163.com>
This commit is contained in:
@@ -58,14 +58,12 @@ func (dao *APITokenDAO) GetUserByAPIToken(token string) (*entity.APIToken, error
|
||||
return &apiToken, nil
|
||||
}
|
||||
|
||||
// GetByBeta gets API token by beta access key.
|
||||
func (dao *APITokenDAO) GetByBeta(beta string) (*entity.APIToken, error) {
|
||||
var apiToken entity.APIToken
|
||||
err := DB.Where("beta = ?", beta).First(&apiToken).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &apiToken, nil
|
||||
// GetByBeta gets API tokens by beta key (SDK/bot authorization token).
|
||||
// Mirrors Python's APIToken.query(beta=token), which returns a list.
|
||||
func (dao *APITokenDAO) GetByBeta(beta string) ([]*entity.APIToken, error) {
|
||||
var tokens []*entity.APIToken
|
||||
err := DB.Where("beta = ?", beta).Find(&tokens).Error
|
||||
return tokens, err
|
||||
}
|
||||
|
||||
// DeleteByDialogIDs deletes API tokens by dialog IDs (hard delete)
|
||||
|
||||
@@ -59,13 +59,13 @@ func TestAPITokenDAOGetByBeta(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("GetByBeta failed: %v", err)
|
||||
}
|
||||
if got == nil {
|
||||
t.Fatal("expected token, got nil")
|
||||
if len(got) == 0 {
|
||||
t.Fatal("expected token(s), got empty list")
|
||||
}
|
||||
if got.TenantID != "tenant-1" {
|
||||
t.Fatalf("TenantID = %q, want tenant-1", got.TenantID)
|
||||
if got[0].TenantID != "tenant-1" {
|
||||
t.Fatalf("TenantID = %q, want tenant-1", got[0].TenantID)
|
||||
}
|
||||
if got.Beta == nil || *got.Beta != beta {
|
||||
t.Fatalf("Beta = %v, want %q", got.Beta, beta)
|
||||
if got[0].Beta == nil || *got[0].Beta != beta {
|
||||
t.Fatalf("Beta = %v, want %q", got[0].Beta, beta)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,6 +208,8 @@ func (r *Router) Setup(engine *gin.Engine) {
|
||||
agentbotGroup := apiBetaAuth.Group("/agentbots")
|
||||
RegisterAgentbotRoutes(agentbotGroup, betaMW, r.botHandler)
|
||||
}
|
||||
// Public bot endpoints (authenticated with an SDK beta token, not a session)
|
||||
apiBetaAuth.GET("/chatbots/:dialog_id/info", r.botHandler.ChatbotInfo)
|
||||
apiBetaAuth.GET("/documents/:id/preview", r.documentHandler.GetDocumentPreview)
|
||||
apiBetaAuth.GET("/documents/images/:image_id", r.documentHandler.GetDocumentImage)
|
||||
apiBetaAuth.GET("/thumbnails", r.documentHandler.GetThumbnail)
|
||||
|
||||
@@ -1092,7 +1092,14 @@ func (s *UserService) GetAPITokenByBeta(authorization string) (*entity.APIToken,
|
||||
return nil, fmt.Errorf("invalid authorization format")
|
||||
}
|
||||
apiTokenDAO := dao.NewAPITokenDAO()
|
||||
return apiTokenDAO.GetByBeta(token)
|
||||
tokens, err := apiTokenDAO.GetByBeta(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(tokens) == 0 {
|
||||
return nil, fmt.Errorf("invalid API token")
|
||||
}
|
||||
return tokens[0], nil
|
||||
}
|
||||
|
||||
// GetUserByBetaAPIToken gets user by beta access key from Authorization
|
||||
@@ -1120,10 +1127,11 @@ func (s *UserService) GetUserByBetaAPIToken(authorization string) (*entity.User,
|
||||
}
|
||||
|
||||
apiTokenDAO := dao.NewAPITokenDAO()
|
||||
userToken, err := apiTokenDAO.GetByBeta(token)
|
||||
if err != nil {
|
||||
userTokens, err := apiTokenDAO.GetByBeta(token)
|
||||
if err != nil || len(userTokens) == 0 {
|
||||
return nil, common.CodeUnauthorized, fmt.Errorf("invalid beta access token")
|
||||
}
|
||||
userToken := userTokens[0]
|
||||
|
||||
user, err := s.userDAO.GetByTenantID(userToken.TenantID)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user