feat(captain): align custom tool limits

This commit is contained in:
2026-06-07 16:06:19 +08:00
parent 0d8de3be0b
commit dd7bcf4118
6 changed files with 166 additions and 13 deletions
@@ -3,9 +3,12 @@ package service
import (
"bytes"
"context"
"crypto/rand"
"encoding/json"
"errors"
"fmt"
"io"
"math/big"
"net/http"
"regexp"
"strings"
@@ -25,6 +28,14 @@ type CaptainCustomToolService struct {
httpClient HTTPDoer
}
const (
maxCaptainCustomToolsPerAccount = 15
maxCaptainCustomToolSlugLength = 64
customToolSlugCollisionSuffix = 7
)
var ErrCaptainCustomToolLimitExceeded = errors.New("You can create a maximum of 15 custom tools per account")
type HTTPDoer interface {
Do(req *http.Request) (*http.Response, error)
}
@@ -89,6 +100,14 @@ type UpdateCustomToolRequest struct {
// Create creates a new CaptainCustomTool.
func (s *CaptainCustomToolService) Create(ctx context.Context, accountID uint, req *CreateCustomToolRequest) (*model.CaptainCustomTool, error) {
count, err := s.toolRepo.CountByAccount(ctx, accountID)
if err != nil {
return nil, fmt.Errorf("count custom tools: %w", err)
}
if count >= maxCaptainCustomToolsPerAccount {
return nil, ErrCaptainCustomToolLimitExceeded
}
// Default values
httpMethod := req.HTTPMethod
if httpMethod == "" {
@@ -100,7 +119,10 @@ func (s *CaptainCustomToolService) Create(ctx context.Context, accountID uint, r
}
slug := req.Slug
if slug == "" {
slug = customToolSlug(req.Title)
slug, err = s.uniqueCustomToolSlug(ctx, accountID, req.Title)
if err != nil {
return nil, err
}
}
tool := &model.CaptainCustomTool{
@@ -265,14 +287,54 @@ func applyCustomToolUpdate(tool *model.CaptainCustomTool, req *UpdateCustomToolR
}
}
func (s *CaptainCustomToolService) uniqueCustomToolSlug(ctx context.Context, accountID uint, title string) (string, error) {
baseSlug := customToolSlug(title)
if !s.customToolSlugExists(ctx, accountID, baseSlug) {
return baseSlug, nil
}
truncated := truncateString(baseSlug, maxCaptainCustomToolSlugLength-customToolSlugCollisionSuffix)
for i := 0; i < 5; i++ {
candidate := truncated + "_" + randomLowerAlphanumeric(6)
if !s.customToolSlugExists(ctx, accountID, candidate) {
return candidate, nil
}
}
return "", fmt.Errorf("slug generation failed")
}
func (s *CaptainCustomToolService) customToolSlugExists(ctx context.Context, accountID uint, slug string) bool {
_, err := s.toolRepo.GetBySlug(ctx, accountID, slug)
return err == nil
}
func customToolSlug(title string) string {
slug := strings.ToLower(strings.TrimSpace(title))
slug = regexp.MustCompile(`[^a-z0-9]+`).ReplaceAllString(slug, "-")
slug = strings.Trim(slug, "-")
if slug == "" {
return "custom-tool"
slug = regexp.MustCompile(`[^a-z0-9]+`).ReplaceAllString(slug, "_")
slug = strings.Trim(slug, "_")
return truncateString("custom_"+slug, maxCaptainCustomToolSlugLength)
}
func truncateString(value string, maxLen int) string {
if len(value) <= maxLen {
return value
}
return slug
return value[:maxLen]
}
func randomLowerAlphanumeric(length int) string {
const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789"
var b strings.Builder
b.Grow(length)
for i := 0; i < length; i++ {
n, err := rand.Int(rand.Reader, big.NewInt(int64(len(alphabet))))
if err != nil {
b.WriteByte(alphabet[time.Now().UnixNano()%int64(len(alphabet))])
continue
}
b.WriteByte(alphabet[n.Int64()])
}
return b.String()
}
// --- Tool Execution ---