From 7d422ba67d337a946961efc31a06a33ed14c99c7 Mon Sep 17 00:00:00 2001 From: Renzo <170978465+RenzoMXD@users.noreply.github.com> Date: Thu, 2 Jul 2026 00:46:00 -1000 Subject: [PATCH] feat(go): implement chatbots//info and searchbots/detail (#15420) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 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//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 `) 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 Co-authored-by: Claude Sonnet 4.6 Co-authored-by: Ling Qin --- internal/dao/api_token.go | 14 ++++++-------- internal/dao/api_token_beta_test.go | 12 ++++++------ internal/router/router.go | 2 ++ internal/service/user.go | 14 +++++++++++--- 4 files changed, 25 insertions(+), 17 deletions(-) diff --git a/internal/dao/api_token.go b/internal/dao/api_token.go index a91560076b..54a1eb8bc2 100644 --- a/internal/dao/api_token.go +++ b/internal/dao/api_token.go @@ -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) diff --git a/internal/dao/api_token_beta_test.go b/internal/dao/api_token_beta_test.go index afb1b9a8d6..77b3322c87 100644 --- a/internal/dao/api_token_beta_test.go +++ b/internal/dao/api_token_beta_test.go @@ -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) } } diff --git a/internal/router/router.go b/internal/router/router.go index a6ede3fb8e..fa1ee7cc2e 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -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) diff --git a/internal/service/user.go b/internal/service/user.go index 9533df8a8b..c66e0a40e7 100644 --- a/internal/service/user.go +++ b/internal/service/user.go @@ -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 {