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:
Renzo
2026-07-02 00:46:00 -10:00
committed by GitHub
parent 7ae18a45ee
commit 7d422ba67d
4 changed files with 25 additions and 17 deletions

View File

@@ -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)

View File

@@ -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)
}
}

View File

@@ -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)

View File

@@ -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 {