├── .dockerignore ├── .gitignore ├── .github ├── dependabot.yml └── workflows │ ├── build.yml │ └── codeql.yml ├── docker └── docker-compose.yml ├── Dockerfile ├── config ├── config.json.example └── README.md ├── src ├── logger │ └── logger.go ├── utils │ └── utils.go ├── register │ └── register.go ├── api │ └── api.go ├── flow │ ├── models.go │ ├── token_pool.go │ ├── handler.go │ └── flow_client.go ├── proxy │ └── singbox.go └── pool │ ├── pool_client.go │ ├── pool.go │ └── pool_server.go ├── go.mod └── README.md /.dockerignore: -------------------------------------------------------------------------------- 1 | # Git 2 | .git 3 | .gitignore 4 | 5 | # Data 6 | data/ 7 | 8 | # Node.js files (no longer needed - registration is pure Go) 9 | node_modules/ 10 | package-lock.json 11 | package.json 12 | main.js 13 | 14 | # IDE 15 | .idea/ 16 | .vscode/ 17 | 18 | # Build artifacts 19 | *.exe 20 | *.dll 21 | *.so 22 | *.dylib 23 | 24 | # Documentation 25 | README.md 26 | LICENSE 27 | 28 | # GitHub 29 | .github/ 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries 2 | gemini-gateway 3 | gemini-gateway.exe 4 | *.exe 5 | *.dll 6 | *.so 7 | *.dylib 8 | 9 | # Data 10 | data/ 11 | *.json 12 | !config.json.example 13 | !package.json 14 | 15 | # Node 16 | node_modules/ 17 | npm-debug.log* 18 | 19 | # IDE 20 | .idea/ 21 | .vscode/ 22 | *.swp 23 | *.swo 24 | 25 | # OS 26 | *.zip 27 | .DS_Store 28 | Thumbs.db 29 | 30 | # Build 31 | dist/ 32 | build/ 33 | data/ 34 | config.json 35 | package-lock.json 36 | main.js 37 | package.json 38 | flow.txt 39 | config/config.json 40 | business2api 41 | flow/ 42 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | business2api: 5 | image: ghcr.io/xxxxteam/business2api:latest 6 | container_name: business2api 7 | restart: unless-stopped 8 | ports: 9 | - "8000:8000" 10 | volumes: 11 | - ./data:/app/data 12 | - ./config.json:/app/config/config.json:ro 13 | environment: 14 | - TZ=Asia/Shanghai 15 | - LISTEN_ADDR=:8000 16 | - DATA_DIR=/app/data 17 | # - PROXY=http://proxy:port 18 | # - API_KEY=your-api-key 19 | healthcheck: 20 | test: ["CMD", "wget", "-q", "--spider", "http://localhost:8000/v1/models"] 21 | interval: 30s 22 | timeout: 10s 23 | retries: 3 24 | logging: 25 | driver: json-file 26 | options: 27 | max-size: "10m" 28 | max-file: "3" 29 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build stage 2 | FROM golang:1.23-alpine AS builder 3 | 4 | WORKDIR /app 5 | 6 | # Install build dependencies 7 | RUN apk add --no-cache git ca-certificates 8 | 9 | # Copy go mod files 10 | COPY go.mod go.sum ./ 11 | ENV GOTOOLCHAIN=auto 12 | RUN go mod download 13 | 14 | # Copy source code 15 | COPY *.go ./ 16 | COPY src/ ./src/ 17 | 18 | # Build binary 19 | RUN CGO_ENABLED=0 GOOS=linux go build -tags "with_quic,with_utls" -ldflags="-s -w" -o business2api . 20 | 21 | # Runtime stage 22 | FROM alpine:latest 23 | 24 | WORKDIR /app 25 | 26 | # Install runtime dependencies (Chromium for rod browser automation) 27 | RUN apk add --no-cache \ 28 | ca-certificates \ 29 | tzdata \ 30 | chromium \ 31 | nss \ 32 | freetype \ 33 | harfbuzz \ 34 | ttf-freefont \ 35 | font-noto-cjk 36 | 37 | # Copy binary from builder 38 | COPY --from=builder /app/business2api . 39 | 40 | # Copy config template if exists 41 | COPY config.json.exampl[e] ./ 42 | 43 | # Create data directory 44 | RUN mkdir -p /app/data 45 | 46 | # Environment variables 47 | ENV LISTEN_ADDR=":8000" 48 | ENV DATA_DIR="/app/data" 49 | 50 | EXPOSE 8000 51 | 52 | ENTRYPOINT ["./business2api"] 53 | -------------------------------------------------------------------------------- /config/config.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "api_keys": ["your-api-key-here"], 3 | "listen_addr": ":8000", 4 | "data_dir": "./data", 5 | "default_config": "", 6 | "debug": false, 7 | "pool": { 8 | "target_count": 50, 9 | "min_count": 10, 10 | "check_interval_minutes": 30, 11 | "register_threads": 1, 12 | "register_headless": true, 13 | "refresh_on_startup": true, 14 | "refresh_cooldown_sec": 240, 15 | "use_cooldown_sec": 15, 16 | "max_fail_count": 3, 17 | "enable_browser_refresh": true, 18 | "browser_refresh_headless": true, 19 | "browser_refresh_max_retry": 1 20 | }, 21 | "pool_server": { 22 | "enable": false, 23 | "mode": "local", 24 | "server_addr": "http://server-ip:8000", 25 | "listen_addr": ":8000", 26 | "secret": "your-secret-key", 27 | "target_count": 50, 28 | "client_threads": 2, 29 | "data_dir": "./data", 30 | "expired_action": "delete" 31 | }, 32 | "proxy_pool": { 33 | "subscribes": [ 34 | "http://example.com/s/example" 35 | ], 36 | "files": [], 37 | "health_check": true, 38 | "check_on_startup": true 39 | }, 40 | "flow": { 41 | "enable": false, 42 | "tokens": [], 43 | "timeout": 120, 44 | "poll_interval": 3, 45 | "max_poll_attempts": 500 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "sync" 8 | "time" 9 | ) 10 | 11 | // Level 日志级别 12 | type Level int 13 | 14 | const ( 15 | LevelError Level = iota 16 | LevelWarn 17 | LevelInfo 18 | LevelDebug 19 | ) 20 | 21 | var levelNames = map[Level]string{ 22 | LevelError: "ERROR", 23 | LevelWarn: "WARN", 24 | LevelInfo: "INFO", 25 | LevelDebug: "DEBUG", 26 | } 27 | 28 | // Logger 日志记录器 29 | type Logger struct { 30 | level Level 31 | prefix string 32 | mu sync.Mutex 33 | } 34 | 35 | var ( 36 | defaultLogger = &Logger{level: LevelInfo} 37 | debugMode = false 38 | ) 39 | 40 | // SetDebugMode 设置调试模式 41 | func SetDebugMode(debug bool) { 42 | debugMode = debug 43 | if debug { 44 | defaultLogger.level = LevelDebug 45 | } else { 46 | defaultLogger.level = LevelInfo 47 | } 48 | } 49 | 50 | // IsDebug 是否为调试模式 51 | func IsDebug() bool { 52 | return debugMode 53 | } 54 | 55 | // SetLevel 设置日志级别 56 | func SetLevel(level Level) { 57 | defaultLogger.level = level 58 | } 59 | 60 | func (l *Logger) log(level Level, format string, args ...interface{}) { 61 | if level > l.level { 62 | return 63 | } 64 | l.mu.Lock() 65 | defer l.mu.Unlock() 66 | 67 | timestamp := time.Now().Format("15:04:05") 68 | levelStr := levelNames[level] 69 | msg := fmt.Sprintf(format, args...) 70 | 71 | if l.prefix != "" { 72 | log.Printf("[%s] [%s] [%s] %s", timestamp, levelStr, l.prefix, msg) 73 | } else { 74 | log.Printf("[%s] [%s] %s", timestamp, levelStr, msg) 75 | } 76 | } 77 | 78 | // Error 错误日志(始终输出) 79 | func Error(format string, args ...interface{}) { 80 | defaultLogger.log(LevelError, format, args...) 81 | } 82 | 83 | // Warn 警告日志(始终输出) 84 | func Warn(format string, args ...interface{}) { 85 | defaultLogger.log(LevelWarn, format, args...) 86 | } 87 | 88 | // Info 信息日志(正常模式输出) 89 | func Info(format string, args ...interface{}) { 90 | defaultLogger.log(LevelInfo, format, args...) 91 | } 92 | 93 | // Debug 调试日志(仅debug模式输出) 94 | func Debug(format string, args ...interface{}) { 95 | defaultLogger.log(LevelDebug, format, args...) 96 | } 97 | 98 | // WithPrefix 创建带前缀的子日志器 99 | func WithPrefix(prefix string) *Logger { 100 | return &Logger{ 101 | level: defaultLogger.level, 102 | prefix: prefix, 103 | } 104 | } 105 | 106 | func (l *Logger) Error(format string, args ...interface{}) { l.log(LevelError, format, args...) } 107 | func (l *Logger) Warn(format string, args ...interface{}) { l.log(LevelWarn, format, args...) } 108 | func (l *Logger) Info(format string, args ...interface{}) { l.log(LevelInfo, format, args...) } 109 | func (l *Logger) Debug(format string, args ...interface{}) { l.log(LevelDebug, format, args...) } 110 | 111 | func init() { 112 | log.SetFlags(0) 113 | log.SetOutput(os.Stdout) 114 | } 115 | -------------------------------------------------------------------------------- /src/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | "crypto/tls" 7 | "encoding/json" 8 | "io" 9 | "net/http" 10 | "net/url" 11 | "time" 12 | 13 | "business2api/src/logger" 14 | "business2api/src/pool" 15 | ) 16 | 17 | // ==================== HTTP 客户端 ==================== 18 | 19 | var HTTPClient *http.Client 20 | 21 | // NewHTTPClient 创建 HTTP 客户端 22 | func NewHTTPClient(proxy string) *http.Client { 23 | transport := &http.Transport{ 24 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 25 | MaxIdleConns: 100, 26 | MaxIdleConnsPerHost: 20, 27 | MaxConnsPerHost: 50, 28 | IdleConnTimeout: 90 * time.Second, 29 | DisableCompression: false, 30 | ForceAttemptHTTP2: true, 31 | } 32 | 33 | if proxy != "" { 34 | proxyURL, err := url.Parse(proxy) 35 | if err == nil { 36 | transport.Proxy = http.ProxyURL(proxyURL) 37 | } 38 | } 39 | 40 | return &http.Client{ 41 | Transport: transport, 42 | Timeout: 1800 * time.Second, 43 | } 44 | } 45 | 46 | // InitHTTPClient 初始化全局 HTTP 客户端 47 | func InitHTTPClient(proxy string) { 48 | HTTPClient = NewHTTPClient(proxy) 49 | pool.HTTPClient = HTTPClient 50 | if proxy != "" { 51 | logger.Info("✅ 使用代理: %s", proxy) 52 | } 53 | } 54 | 55 | // ReadResponseBody 读取 HTTP 响应体(支持 gzip) 56 | func ReadResponseBody(resp *http.Response) ([]byte, error) { 57 | var reader io.Reader = resp.Body 58 | if resp.Header.Get("Content-Encoding") == "gzip" { 59 | gzReader, err := gzip.NewReader(resp.Body) 60 | if err != nil { 61 | return nil, err 62 | } 63 | defer gzReader.Close() 64 | reader = gzReader 65 | } 66 | return io.ReadAll(reader) 67 | } 68 | 69 | // ParseNDJSON 解析 NDJSON 格式数据 70 | func ParseNDJSON(data []byte) []map[string]interface{} { 71 | var result []map[string]interface{} 72 | lines := bytes.Split(data, []byte("\n")) 73 | for _, line := range lines { 74 | line = bytes.TrimSpace(line) 75 | if len(line) == 0 { 76 | continue 77 | } 78 | var obj map[string]interface{} 79 | if err := json.Unmarshal(line, &obj); err == nil { 80 | result = append(result, obj) 81 | } 82 | } 83 | return result 84 | } 85 | 86 | // ParseIncompleteJSONArray 解析可能不完整的 JSON 数组 87 | func ParseIncompleteJSONArray(data []byte) []map[string]interface{} { 88 | var result []map[string]interface{} 89 | if err := json.Unmarshal(data, &result); err == nil { 90 | return result 91 | } 92 | 93 | trimmed := bytes.TrimSpace(data) 94 | if len(trimmed) > 0 && trimmed[0] == '[' { 95 | if trimmed[len(trimmed)-1] != ']' { 96 | lastBrace := bytes.LastIndex(trimmed, []byte("}")) 97 | if lastBrace > 0 { 98 | fixed := append(trimmed[:lastBrace+1], ']') 99 | if err := json.Unmarshal(fixed, &result); err == nil { 100 | logger.Warn("JSON 数组不完整,已修复") 101 | return result 102 | } 103 | } 104 | } 105 | } 106 | return nil 107 | } 108 | 109 | // TruncateString 截断字符串 110 | func TruncateString(s string, maxLen int) string { 111 | if len(s) <= maxLen { 112 | return s 113 | } 114 | return s[:maxLen] + "..." 115 | } 116 | 117 | // Min 返回两个整数中的较小值 118 | func Min(a, b int) int { 119 | if a < b { 120 | return a 121 | } 122 | return b 123 | } 124 | -------------------------------------------------------------------------------- /config/README.md: -------------------------------------------------------------------------------- 1 | # 配置说明 2 | 3 | ## 代理池配置 (`proxy_pool`) 4 | 5 | **内置 xray-core**,支持 vmess、vless、shadowsocks、trojan 等协议,自动转换为本地 socks5 代理。 6 | 7 | ```json 8 | "proxy_pool": { 9 | "proxy": "", // 备用单个代理 (http/socks5 格式) 10 | "subscribes": [ // 订阅链接列表 (支持 base64 编码) 11 | "https://example.com/sub1", 12 | "https://example.com/sub2" 13 | ], 14 | "files": [ // 本地代理文件列表 15 | "./proxies.txt" 16 | ], 17 | "health_check": true, // 是否启用健康检查 18 | "check_on_startup": false // 启动时是否检查所有节点 19 | } 20 | ``` 21 | 22 | ### 支持的代理格式 23 | 24 | **代理文件/订阅内容格式** (每行一个): 25 | 26 | ``` 27 | # VMess 28 | vmess://eyJ2IjoiMiIsInBzIjoi5ZCN56ewIiwiYWRkIjoic2VydmVyLmNvbSIsInBvcnQiOiI0NDMiLCJpZCI6InV1aWQiLCJhaWQiOiIwIiwic2N5IjoiYXV0byIsIm5ldCI6IndzIiwicGF0aCI6Ii9wYXRoIiwiaG9zdCI6Imhvc3QuY29tIiwidGxzIjoidGxzIn0= 29 | 30 | # VLESS 31 | vless://uuid@server.com:443?type=ws&security=tls&path=/path&host=host.com&sni=sni.com#名称 32 | 33 | # Shadowsocks 34 | ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ=@server.com:8388#名称 35 | 36 | # Trojan 37 | trojan://password@server.com:443?sni=sni.com#名称 38 | 39 | # 直接代理 40 | http://proxy.com:8080 41 | socks5://127.0.0.1:1080 42 | ``` 43 | 44 | --- 45 | 46 | ## 号池配置 (`pool`) 47 | 48 | ```json 49 | "pool": { 50 | "target_count": 50, // 目标账号数量 51 | "min_count": 10, // 最小账号数,低于此值触发注册 52 | "check_interval_minutes": 30, // 检查间隔(分钟) 53 | "register_threads": 1, // 注册线程数 54 | "register_headless": false, // 注册时是否无头模式 55 | "refresh_on_startup": true, // 启动时是否刷新账号 56 | "refresh_cooldown_sec": 240, // 刷新冷却时间(秒) 57 | "use_cooldown_sec": 15, // 使用冷却时间(秒) 58 | "max_fail_count": 3, // 最大失败次数 59 | "enable_browser_refresh": true, // 启用浏览器刷新 60 | "browser_refresh_headless": false, // 浏览器刷新无头模式 61 | "browser_refresh_max_retry": 1 // 浏览器刷新最大重试次数 62 | } 63 | ``` 64 | 65 | --- 66 | 67 | ## 号池服务器配置 (`pool_server`) 68 | 69 | ```json 70 | "pool_server": { 71 | "enable": false, // 是否启用 72 | "mode": "local", // 模式: local/server/client 73 | "server_addr": "", // 服务器地址 (客户端模式) 74 | "listen_addr": ":8000", // 监听地址 (服务器模式) 75 | "secret": "", // 认证密钥 76 | "target_count": 50, // 目标账号数 77 | "client_threads": 2, // 客户端并发线程数 78 | "data_dir": "./data", // 数据目录 79 | "expired_action": "delete" // 过期账号处理方式 80 | } 81 | ``` 82 | 83 | **模式说明**: 84 | - `local`: 本地模式,独立运行 85 | - `server`: 服务器模式,提供号池服务和API 86 | - `client`: 客户端模式,连接服务器接收注册/续期任务 87 | 88 | **expired_action 说明**: 89 | - `delete`: 删除过期/失败账号 90 | - `refresh`: 尝试浏览器刷新Cookie 91 | - `queue`: 保留在队列等待重试 92 | 93 | --- 94 | 95 | ## Flow 配置 (`flow`) 96 | 97 | ```json 98 | "flow": { 99 | "enable": false, // 是否启用 Flow 视频生成 100 | "tokens": [], // Flow ST Tokens 101 | "proxy": "", // Flow 专用代理 102 | "timeout": 120, // 超时时间(秒) 103 | "poll_interval": 3, // 轮询间隔(秒) 104 | "max_poll_attempts": 500 // 最大轮询次数 105 | } 106 | ``` 107 | 108 | --- 109 | 110 | ## 其他配置 111 | 112 | ```json 113 | { 114 | "api_keys": ["key1", "key2"], // API 密钥列表 115 | "listen_addr": ":8000", // 监听地址 116 | "data_dir": "./data", // 数据目录 117 | "default_config": "", // 默认 configId 118 | "debug": false, // 调试模式 119 | "proxy": "http://127.0.0.1:10808" // 全局代理 (兼容旧配置) 120 | } 121 | ``` 122 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and Release 2 | 3 | on: 4 | push: 5 | branches: [main, master] 6 | tags: 7 | - 'v*' 8 | pull_request: 9 | branches: [main, master] 10 | workflow_dispatch: 11 | 12 | env: 13 | GO_VERSION: '1.23' 14 | REGISTRY: ghcr.io 15 | IMAGE_NAME: ${{ github.repository }} 16 | 17 | jobs: 18 | build: 19 | runs-on: ubuntu-latest 20 | strategy: 21 | matrix: 22 | goos: [linux, darwin, windows] 23 | goarch: [amd64, arm64] 24 | exclude: 25 | - goos: windows 26 | goarch: arm64 27 | 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v4 31 | with: 32 | fetch-depth: 0 33 | 34 | - name: Setup Go 35 | uses: actions/setup-go@v5 36 | with: 37 | go-version: ${{ env.GO_VERSION }} 38 | cache: false 39 | 40 | - name: Get version 41 | id: version 42 | run: | 43 | if [[ "$GITHUB_REF" == refs/tags/v* ]]; then 44 | VERSION=${GITHUB_REF#refs/tags/v} 45 | else 46 | VERSION=$(git describe --tags --always --dirty 2>/dev/null || echo "dev") 47 | fi 48 | echo "VERSION=$VERSION" >> $GITHUB_OUTPUT 49 | echo "Building version: $VERSION" 50 | 51 | - name: Build 52 | env: 53 | GOOS: ${{ matrix.goos }} 54 | GOARCH: ${{ matrix.goarch }} 55 | VERSION: ${{ steps.version.outputs.VERSION }} 56 | GOTOOLCHAIN: auto 57 | run: | 58 | EXT="" 59 | if [ "$GOOS" = "windows" ]; then 60 | EXT=".exe" 61 | fi 62 | CGO_ENABLED=0 go build \ 63 | -tags "with_quic,with_utls" \ 64 | -ldflags="-s -w -X main.Version=${VERSION}" \ 65 | -o business2api-${{ matrix.goos }}-${{ matrix.goarch }}${EXT} . 66 | 67 | - name: Prepare package 68 | run: | 69 | mkdir -p dist 70 | mv business2api-* dist/ 71 | cp config/config.json.example dist/ 72 | cp README.md dist/ 73 | 74 | - name: Upload Artifact 75 | uses: actions/upload-artifact@v4 76 | with: 77 | name: business2api-${{ matrix.goos }}-${{ matrix.goarch }} 78 | path: dist/* 79 | retention-days: 30 80 | 81 | docker: 82 | runs-on: ubuntu-latest 83 | needs: build 84 | if: github.event_name == 'push' 85 | permissions: 86 | contents: read 87 | packages: write 88 | 89 | steps: 90 | - name: Checkout 91 | uses: actions/checkout@v4 92 | 93 | - name: Set up QEMU 94 | uses: docker/setup-qemu-action@v3 95 | 96 | - name: Set up Docker Buildx 97 | uses: docker/setup-buildx-action@v3 98 | 99 | - name: Login to GitHub Container Registry 100 | uses: docker/login-action@v3 101 | with: 102 | registry: ${{ env.REGISTRY }} 103 | username: ${{ github.actor }} 104 | password: ${{ secrets.GITHUB_TOKEN }} 105 | 106 | - name: Extract metadata 107 | id: meta 108 | uses: docker/metadata-action@v5 109 | with: 110 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 111 | tags: | 112 | type=ref,event=branch 113 | type=semver,pattern={{version}} 114 | type=semver,pattern={{major}}.{{minor}} 115 | type=sha,prefix= 116 | 117 | - name: Build and push 118 | uses: docker/build-push-action@v5 119 | with: 120 | context: . 121 | platforms: linux/amd64,linux/arm64 122 | push: true 123 | tags: ${{ steps.meta.outputs.tags }} 124 | labels: ${{ steps.meta.outputs.labels }} 125 | cache-from: type=gha 126 | cache-to: type=gha,mode=max 127 | 128 | release: 129 | runs-on: ubuntu-latest 130 | needs: build 131 | if: startsWith(github.ref, 'refs/tags/v') 132 | permissions: 133 | contents: write 134 | 135 | steps: 136 | - name: Download all artifacts 137 | uses: actions/download-artifact@v4 138 | with: 139 | path: artifacts 140 | 141 | - name: Prepare release files 142 | run: | 143 | mkdir -p release 144 | for dir in artifacts/business2api-*/; do 145 | # 获取平台名称 146 | platform=$(basename "$dir") 147 | # 创建临时目录 148 | mkdir -p "tmp/$platform" 149 | cp "$dir"/* "tmp/$platform/" 150 | # 设置可执行权限 151 | for f in "tmp/$platform"/business2api-*; do 152 | if [[ ! "$f" == *.exe ]]; then 153 | chmod +x "$f" 154 | fi 155 | done 156 | # 打包 157 | tar -czvf "release/${platform}.tar.gz" -C tmp "$platform" 158 | rm -rf "tmp/$platform" 159 | done 160 | rm -rf tmp 161 | 162 | - name: Create Release 163 | uses: softprops/action-gh-release@v1 164 | with: 165 | files: release/* 166 | generate_release_notes: true 167 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL Advanced" 13 | 14 | on: 15 | push: 16 | branches: [ "master" ] 17 | pull_request: 18 | branches: [ "master" ] 19 | schedule: 20 | - cron: '33 14 * * 3' 21 | 22 | jobs: 23 | analyze: 24 | name: Analyze (${{ matrix.language }}) 25 | # Runner size impacts CodeQL analysis time. To learn more, please see: 26 | # - https://gh.io/recommended-hardware-resources-for-running-codeql 27 | # - https://gh.io/supported-runners-and-hardware-resources 28 | # - https://gh.io/using-larger-runners (GitHub.com only) 29 | # Consider using larger runners or machines with greater resources for possible analysis time improvements. 30 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} 31 | permissions: 32 | # required for all workflows 33 | security-events: write 34 | 35 | # required to fetch internal or private CodeQL packs 36 | packages: read 37 | 38 | # only required for workflows in private repositories 39 | actions: read 40 | contents: read 41 | 42 | strategy: 43 | fail-fast: false 44 | matrix: 45 | include: 46 | - language: actions 47 | build-mode: none 48 | - language: go 49 | build-mode: autobuild 50 | # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift' 51 | # Use `c-cpp` to analyze code written in C, C++ or both 52 | # Use 'java-kotlin' to analyze code written in Java, Kotlin or both 53 | # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both 54 | # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, 55 | # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. 56 | # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how 57 | # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages 58 | steps: 59 | - name: Checkout repository 60 | uses: actions/checkout@v4 61 | 62 | # Add any setup steps before running the `github/codeql-action/init` action. 63 | # This includes steps like installing compilers or runtimes (`actions/setup-node` 64 | # or others). This is typically only required for manual builds. 65 | # - name: Setup runtime (example) 66 | # uses: actions/setup-example@v1 67 | 68 | # Initializes the CodeQL tools for scanning. 69 | - name: Initialize CodeQL 70 | uses: github/codeql-action/init@v4 71 | with: 72 | languages: ${{ matrix.language }} 73 | build-mode: ${{ matrix.build-mode }} 74 | # If you wish to specify custom queries, you can do so here or in a config file. 75 | # By default, queries listed here will override any specified in a config file. 76 | # Prefix the list here with "+" to use these queries and those in the config file. 77 | 78 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 79 | # queries: security-extended,security-and-quality 80 | 81 | # If the analyze step fails for one of the languages you are analyzing with 82 | # "We were unable to automatically build your code", modify the matrix above 83 | # to set the build mode to "manual" for that language. Then modify this step 84 | # to build your code. 85 | # ℹ️ Command-line programs to run using the OS shell. 86 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 87 | - name: Run manual build steps 88 | if: matrix.build-mode == 'manual' 89 | shell: bash 90 | run: | 91 | echo 'If you are using a "manual" build mode for one or more of the' \ 92 | 'languages you are analyzing, replace this with the commands to build' \ 93 | 'your code, for example:' 94 | echo ' make bootstrap' 95 | echo ' make release' 96 | exit 1 97 | 98 | - name: Perform CodeQL Analysis 99 | uses: github/codeql-action/analyze@v4 100 | with: 101 | category: "/language:${{matrix.language}}" 102 | -------------------------------------------------------------------------------- /src/register/register.go: -------------------------------------------------------------------------------- 1 | package register 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "sync" 8 | "sync/atomic" 9 | "time" 10 | 11 | "business2api/src/logger" 12 | "business2api/src/pool" 13 | ) 14 | 15 | // ==================== 注册与刷新 ==================== 16 | 17 | var ( 18 | DataDir string 19 | TargetCount int 20 | MinCount int 21 | CheckInterval time.Duration 22 | Threads int 23 | Headless bool // 注册无头模式 24 | Proxy string // 代理 25 | ) 26 | 27 | var IsRegistering int32 28 | var registeringTarget int32 // 正在注册的目标数量 29 | var registerMu sync.Mutex // 注册启动互斥锁 30 | 31 | var Stats = &RegisterStats{} 32 | 33 | type RegisterStats struct { 34 | Total int `json:"total"` 35 | Success int `json:"success"` 36 | Failed int `json:"failed"` 37 | LastError string `json:"lastError"` 38 | UpdatedAt time.Time `json:"updatedAt"` 39 | mu sync.RWMutex 40 | } 41 | 42 | func (s *RegisterStats) AddSuccess() { 43 | s.mu.Lock() 44 | defer s.mu.Unlock() 45 | s.Total++ 46 | s.Success++ 47 | s.UpdatedAt = time.Now() 48 | } 49 | 50 | func (s *RegisterStats) AddFailed(err string) { 51 | s.mu.Lock() 52 | defer s.mu.Unlock() 53 | s.Total++ 54 | s.Failed++ 55 | s.LastError = err 56 | s.UpdatedAt = time.Now() 57 | } 58 | 59 | func (s *RegisterStats) Get() map[string]interface{} { 60 | s.mu.RLock() 61 | defer s.mu.RUnlock() 62 | return map[string]interface{}{ 63 | "total": s.Total, 64 | "success": s.Success, 65 | "failed": s.Failed, 66 | "last_error": s.LastError, 67 | "updated_at": s.UpdatedAt, 68 | } 69 | } 70 | 71 | // 注册结果 72 | type RegisterResult struct { 73 | Success bool `json:"success"` 74 | Email string `json:"email"` 75 | Error string `json:"error"` 76 | NeedWait bool `json:"needWait"` 77 | } 78 | 79 | // StartRegister 启动注册任务(优化并发控制) 80 | func StartRegister(count int) error { 81 | registerMu.Lock() 82 | defer registerMu.Unlock() 83 | 84 | // 再次检查当前账号数是否已满足 85 | pool.Pool.Load(DataDir) 86 | currentCount := pool.Pool.TotalCount() 87 | if currentCount >= TargetCount { 88 | logger.Info("✅ 账号数已满足: %d >= %d,无需注册", currentCount, TargetCount) 89 | return nil 90 | } 91 | 92 | // 如果已经在注册中,检查是否需要调整 93 | if atomic.LoadInt32(&IsRegistering) == 1 { 94 | currentTarget := atomic.LoadInt32(®isteringTarget) 95 | if int(currentTarget) >= count { 96 | return fmt.Errorf("注册进程已在运行,目标: %d", currentTarget) 97 | } 98 | // 更新目标数量 99 | atomic.StoreInt32(®isteringTarget, int32(count)) 100 | logger.Info("🔄 注册目标已更新: %d", count) 101 | return nil 102 | } 103 | 104 | if !atomic.CompareAndSwapInt32(&IsRegistering, 0, 1) { 105 | return fmt.Errorf("注册进程已在运行") 106 | } 107 | atomic.StoreInt32(®isteringTarget, int32(count)) 108 | 109 | // 获取数据目录的绝对路径 110 | dataDirAbs, _ := filepath.Abs(DataDir) 111 | if err := os.MkdirAll(dataDirAbs, 0755); err != nil { 112 | atomic.StoreInt32(&IsRegistering, 0) 113 | atomic.StoreInt32(®isteringTarget, 0) 114 | return fmt.Errorf("创建数据目录失败: %w", err) 115 | } 116 | 117 | // 使用配置的线程数 118 | threads := Threads 119 | if threads <= 0 { 120 | threads = 1 121 | } 122 | for i := 0; i < threads; i++ { 123 | go NativeRegisterWorker(i+1, dataDirAbs) 124 | } 125 | 126 | // 监控进度 127 | go func() { 128 | for { 129 | time.Sleep(10 * time.Second) 130 | pool.Pool.Load(DataDir) 131 | currentCount := pool.Pool.TotalCount() 132 | target := atomic.LoadInt32(®isteringTarget) 133 | 134 | // 检查是否达到目标(使用当前目标和全局目标的较大值) 135 | effectiveTarget := TargetCount 136 | if int(target) > effectiveTarget { 137 | effectiveTarget = int(target) 138 | } 139 | 140 | if currentCount >= effectiveTarget { 141 | logger.Info("✅ 已达到目标账号数: %d >= %d,停止注册", currentCount, effectiveTarget) 142 | atomic.StoreInt32(&IsRegistering, 0) 143 | atomic.StoreInt32(®isteringTarget, 0) 144 | return 145 | } 146 | } 147 | }() 148 | 149 | return nil 150 | } 151 | 152 | // PoolMaintainer 号池维护器 153 | func PoolMaintainer() { 154 | interval := CheckInterval 155 | if interval < time.Minute { 156 | interval = 30 * time.Minute 157 | } 158 | 159 | ticker := time.NewTicker(interval) 160 | defer ticker.Stop() 161 | CheckAndMaintainPool() 162 | 163 | for range ticker.C { 164 | CheckAndMaintainPool() 165 | } 166 | } 167 | 168 | // CheckAndMaintainPool 检查并维护号池(优化并发控制) 169 | func CheckAndMaintainPool() { 170 | // 如果正在注册中,跳过检查 171 | if atomic.LoadInt32(&IsRegistering) == 1 { 172 | logger.Debug("⏳ 注册进程运行中,跳过本次检查") 173 | return 174 | } 175 | 176 | pool.Pool.Load(DataDir) 177 | 178 | readyCount := pool.Pool.ReadyCount() 179 | pendingCount := pool.Pool.PendingCount() 180 | totalCount := pool.Pool.TotalCount() 181 | 182 | logger.Info("📊 号池检查: ready=%d, pending=%d, total=%d, 目标=%d, 最小=%d", 183 | readyCount, pendingCount, totalCount, TargetCount, MinCount) 184 | 185 | // 只有当总数小于最小数时才触发注册,避免频繁注册 186 | if totalCount < MinCount { 187 | needCount := TargetCount - totalCount 188 | logger.Info("⚠️ 账号数低于最小值 (%d < %d),需要注册 %d 个", totalCount, MinCount, needCount) 189 | if err := StartRegister(needCount); err != nil { 190 | logger.Error("❌ 启动注册失败: %v", err) 191 | } 192 | } else if totalCount < TargetCount { 193 | logger.Debug("📊 账号数未达目标 (%d < %d),但高于最小值,暂不触发注册", totalCount, TargetCount) 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/api/api.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | // ChatRequest 聊天请求 11 | type ChatRequest struct { 12 | Model string `json:"model"` 13 | Messages []Message `json:"messages"` 14 | Stream bool `json:"stream"` 15 | Temperature float64 `json:"temperature,omitempty"` 16 | MaxTokens int `json:"max_tokens,omitempty"` 17 | Tools []ToolDef `json:"tools,omitempty"` 18 | } 19 | 20 | // Message 消息 21 | type Message struct { 22 | Role string `json:"role"` 23 | Content interface{} `json:"content"` 24 | } 25 | 26 | // ToolDef 工具定义 27 | type ToolDef struct { 28 | Type string `json:"type"` 29 | Function FunctionDef `json:"function"` 30 | } 31 | 32 | // FunctionDef 函数定义 33 | type FunctionDef struct { 34 | Name string `json:"name"` 35 | Description string `json:"description"` 36 | Parameters map[string]interface{} `json:"parameters,omitempty"` 37 | } 38 | 39 | var ( 40 | StreamChat func(c *gin.Context, req ChatRequest) 41 | FixedModels []string 42 | ) 43 | type GeminiRequest struct { 44 | Contents []GeminiContent `json:"contents"` 45 | SystemInstruction *GeminiContent `json:"systemInstruction,omitempty"` 46 | GenerationConfig map[string]interface{} `json:"generationConfig,omitempty"` 47 | GeminiTools []map[string]interface{} `json:"tools,omitempty"` 48 | } 49 | 50 | type GeminiContent struct { 51 | Role string `json:"role,omitempty"` 52 | Parts []GeminiPart `json:"parts"` 53 | } 54 | 55 | type GeminiPart struct { 56 | Text string `json:"text,omitempty"` 57 | InlineData *GeminiInlineData `json:"inlineData,omitempty"` 58 | } 59 | 60 | type GeminiInlineData struct { 61 | MimeType string `json:"mimeType"` 62 | Data string `json:"data"` 63 | } 64 | 65 | func HandleGeminiGenerate(c *gin.Context) { 66 | action := c.Param("action") 67 | if action == "" { 68 | c.JSON(400, gin.H{"error": gin.H{"code": 400, "message": "Missing model action", "status": "INVALID_ARGUMENT"}}) 69 | return 70 | } 71 | 72 | // 去掉开头的 / 73 | action = strings.TrimPrefix(action, "/") 74 | 75 | // 解析模型名和动作 76 | var model string 77 | var isStream bool 78 | if idx := strings.LastIndex(action, ":"); idx > 0 { 79 | model = action[:idx] 80 | actionType := action[idx+1:] 81 | isStream = actionType == "streamGenerateContent" 82 | } else { 83 | model = action 84 | } 85 | 86 | if model == "" { 87 | model = FixedModels[0] 88 | } 89 | 90 | var geminiReq GeminiRequest 91 | if err := c.ShouldBindJSON(&geminiReq); err != nil { 92 | c.JSON(400, gin.H{"error": gin.H{"code": 400, "message": err.Error(), "status": "INVALID_ARGUMENT"}}) 93 | return 94 | } 95 | 96 | var messages []Message 97 | 98 | // 处理systemInstruction 99 | if geminiReq.SystemInstruction != nil && len(geminiReq.SystemInstruction.Parts) > 0 { 100 | var sysText string 101 | for _, part := range geminiReq.SystemInstruction.Parts { 102 | if part.Text != "" { 103 | sysText += part.Text 104 | } 105 | } 106 | if sysText != "" { 107 | messages = append(messages, Message{Role: "system", Content: sysText}) 108 | } 109 | } 110 | for _, content := range geminiReq.Contents { 111 | role := content.Role 112 | if role == "model" { 113 | role = "assistant" 114 | } 115 | 116 | var textParts []string 117 | var contentParts []interface{} 118 | 119 | for _, part := range content.Parts { 120 | if part.Text != "" { 121 | textParts = append(textParts, part.Text) 122 | } 123 | if part.InlineData != nil { 124 | contentParts = append(contentParts, map[string]interface{}{ 125 | "type": "image_url", 126 | "image_url": map[string]string{ 127 | "url": fmt.Sprintf("data:%s;base64,%s", part.InlineData.MimeType, part.InlineData.Data), 128 | }, 129 | }) 130 | } 131 | } 132 | 133 | if len(contentParts) > 0 { 134 | if len(textParts) > 0 { 135 | contentParts = append([]interface{}{map[string]interface{}{"type": "text", "text": strings.Join(textParts, "\n")}}, contentParts...) 136 | } 137 | messages = append(messages, Message{Role: role, Content: contentParts}) 138 | } else if len(textParts) > 0 { 139 | messages = append(messages, Message{Role: role, Content: strings.Join(textParts, "\n")}) 140 | } 141 | } 142 | 143 | // 流式判断:路径中包含streamGenerateContent 或 query参数 alt=sse 144 | stream := isStream || c.Query("alt") == "sse" 145 | var tools []ToolDef 146 | for _, gt := range geminiReq.GeminiTools { 147 | if funcDecls, ok := gt["functionDeclarations"].([]interface{}); ok { 148 | for _, fd := range funcDecls { 149 | if funcMap, ok := fd.(map[string]interface{}); ok { 150 | name, _ := funcMap["name"].(string) 151 | desc, _ := funcMap["description"].(string) 152 | params, _ := funcMap["parameters"].(map[string]interface{}) 153 | tools = append(tools, ToolDef{ 154 | Type: "function", 155 | Function: FunctionDef{ 156 | Name: name, 157 | Description: desc, 158 | Parameters: params, 159 | }, 160 | }) 161 | } 162 | } 163 | } 164 | } 165 | 166 | req := ChatRequest{ 167 | Model: model, 168 | Messages: messages, 169 | Stream: stream, 170 | Tools: tools, 171 | } 172 | 173 | StreamChat(c, req) 174 | } 175 | 176 | type ClaudeRequest struct { 177 | Model string `json:"model"` 178 | Messages []Message `json:"messages"` 179 | System string `json:"system,omitempty"` 180 | MaxTokens int `json:"max_tokens,omitempty"` 181 | Stream bool `json:"stream"` 182 | Temperature float64 `json:"temperature,omitempty"` 183 | Tools []ToolDef `json:"tools,omitempty"` 184 | } 185 | func HandleClaudeMessages(c *gin.Context) { 186 | var claudeReq ClaudeRequest 187 | if err := c.ShouldBindJSON(&claudeReq); err != nil { 188 | c.JSON(400, gin.H{"type": "error", "error": gin.H{"type": "invalid_request_error", "message": err.Error()}}) 189 | return 190 | } 191 | 192 | req := ChatRequest{ 193 | Model: claudeReq.Model, 194 | Messages: claudeReq.Messages, 195 | Stream: claudeReq.Stream, 196 | Temperature: claudeReq.Temperature, 197 | Tools: claudeReq.Tools, 198 | } 199 | if claudeReq.System != "" { 200 | systemMsg := Message{Role: "system", Content: claudeReq.System} 201 | req.Messages = append([]Message{systemMsg}, req.Messages...) 202 | } 203 | 204 | // 保持模型名原样,不做映射 205 | if req.Model == "" { 206 | req.Model = FixedModels[0] 207 | } 208 | 209 | StreamChat(c, req) 210 | } 211 | -------------------------------------------------------------------------------- /src/flow/models.go: -------------------------------------------------------------------------------- 1 | package flow 2 | 3 | // ModelType 模型类型 4 | type ModelType string 5 | 6 | const ( 7 | ModelTypeImage ModelType = "image" 8 | ModelTypeVideo ModelType = "video" 9 | ) 10 | 11 | // VideoType 视频生成类型 12 | type VideoType string 13 | 14 | const ( 15 | VideoTypeT2V VideoType = "t2v" // Text to Video 16 | VideoTypeI2V VideoType = "i2v" // Image to Video (首尾帧) 17 | VideoTypeR2V VideoType = "r2v" // Reference Images to Video (多图) 18 | ) 19 | 20 | // ModelConfig 模型配置 21 | type ModelConfig struct { 22 | Type ModelType `json:"type"` 23 | ModelName string `json:"model_name,omitempty"` // 图片模型名称 24 | ModelKey string `json:"model_key,omitempty"` // 视频模型键 25 | AspectRatio string `json:"aspect_ratio"` 26 | VideoType VideoType `json:"video_type,omitempty"` 27 | SupportsImages bool `json:"supports_images"` 28 | MinImages int `json:"min_images"` 29 | MaxImages int `json:"max_images"` // 0 表示不限制 30 | } 31 | 32 | // FlowModelConfig Flow 模型配置表 33 | var FlowModelConfig = map[string]ModelConfig{ 34 | // ========== 图片生成 ========== 35 | // GEM_PIX (Gemini 2.5 Flash) 36 | "gemini-2.5-flash-image-landscape": { 37 | Type: ModelTypeImage, 38 | ModelName: "GEM_PIX", 39 | AspectRatio: "IMAGE_ASPECT_RATIO_LANDSCAPE", 40 | }, 41 | "gemini-2.5-flash-image-portrait": { 42 | Type: ModelTypeImage, 43 | ModelName: "GEM_PIX", 44 | AspectRatio: "IMAGE_ASPECT_RATIO_PORTRAIT", 45 | }, 46 | // GEM_PIX_2 (Gemini 3.0 Pro) 47 | "gemini-3.0-pro-image-landscape": { 48 | Type: ModelTypeImage, 49 | ModelName: "GEM_PIX_2", 50 | AspectRatio: "IMAGE_ASPECT_RATIO_LANDSCAPE", 51 | }, 52 | "gemini-3.0-pro-image-portrait": { 53 | Type: ModelTypeImage, 54 | ModelName: "GEM_PIX_2", 55 | AspectRatio: "IMAGE_ASPECT_RATIO_PORTRAIT", 56 | }, 57 | // IMAGEN_3_5 (Imagen 4.0) 58 | "imagen-4.0-generate-preview-landscape": { 59 | Type: ModelTypeImage, 60 | ModelName: "IMAGEN_3_5", 61 | AspectRatio: "IMAGE_ASPECT_RATIO_LANDSCAPE", 62 | }, 63 | "imagen-4.0-generate-preview-portrait": { 64 | Type: ModelTypeImage, 65 | ModelName: "IMAGEN_3_5", 66 | AspectRatio: "IMAGE_ASPECT_RATIO_PORTRAIT", 67 | }, 68 | 69 | // ========== 文生视频 (T2V) ========== 70 | "veo_3_1_t2v_fast_portrait": { 71 | Type: ModelTypeVideo, 72 | VideoType: VideoTypeT2V, 73 | ModelKey: "veo_3_1_t2v_fast_portrait", 74 | AspectRatio: "VIDEO_ASPECT_RATIO_PORTRAIT", 75 | SupportsImages: false, 76 | }, 77 | "veo_3_1_t2v_fast_landscape": { 78 | Type: ModelTypeVideo, 79 | VideoType: VideoTypeT2V, 80 | ModelKey: "veo_3_1_t2v_fast", 81 | AspectRatio: "VIDEO_ASPECT_RATIO_LANDSCAPE", 82 | SupportsImages: false, 83 | }, 84 | "veo_2_1_fast_d_15_t2v_portrait": { 85 | Type: ModelTypeVideo, 86 | VideoType: VideoTypeT2V, 87 | ModelKey: "veo_2_1_fast_d_15_t2v", 88 | AspectRatio: "VIDEO_ASPECT_RATIO_PORTRAIT", 89 | SupportsImages: false, 90 | }, 91 | "veo_2_1_fast_d_15_t2v_landscape": { 92 | Type: ModelTypeVideo, 93 | VideoType: VideoTypeT2V, 94 | ModelKey: "veo_2_1_fast_d_15_t2v", 95 | AspectRatio: "VIDEO_ASPECT_RATIO_LANDSCAPE", 96 | SupportsImages: false, 97 | }, 98 | "veo_2_0_t2v_portrait": { 99 | Type: ModelTypeVideo, 100 | VideoType: VideoTypeT2V, 101 | ModelKey: "veo_2_0_t2v", 102 | AspectRatio: "VIDEO_ASPECT_RATIO_PORTRAIT", 103 | SupportsImages: false, 104 | }, 105 | "veo_2_0_t2v_landscape": { 106 | Type: ModelTypeVideo, 107 | VideoType: VideoTypeT2V, 108 | ModelKey: "veo_2_0_t2v", 109 | AspectRatio: "VIDEO_ASPECT_RATIO_LANDSCAPE", 110 | SupportsImages: false, 111 | }, 112 | 113 | // ========== 首尾帧 (I2V) ========== 114 | "veo_3_1_i2v_s_fast_fl_portrait": { 115 | Type: ModelTypeVideo, 116 | VideoType: VideoTypeI2V, 117 | ModelKey: "veo_3_1_i2v_s_fast_fl", 118 | AspectRatio: "VIDEO_ASPECT_RATIO_PORTRAIT", 119 | SupportsImages: true, 120 | MinImages: 1, 121 | MaxImages: 2, 122 | }, 123 | "veo_3_1_i2v_s_fast_fl_landscape": { 124 | Type: ModelTypeVideo, 125 | VideoType: VideoTypeI2V, 126 | ModelKey: "veo_3_1_i2v_s_fast_fl", 127 | AspectRatio: "VIDEO_ASPECT_RATIO_LANDSCAPE", 128 | SupportsImages: true, 129 | MinImages: 1, 130 | MaxImages: 2, 131 | }, 132 | "veo_2_1_fast_d_15_i2v_portrait": { 133 | Type: ModelTypeVideo, 134 | VideoType: VideoTypeI2V, 135 | ModelKey: "veo_2_1_fast_d_15_i2v", 136 | AspectRatio: "VIDEO_ASPECT_RATIO_PORTRAIT", 137 | SupportsImages: true, 138 | MinImages: 1, 139 | MaxImages: 2, 140 | }, 141 | "veo_2_1_fast_d_15_i2v_landscape": { 142 | Type: ModelTypeVideo, 143 | VideoType: VideoTypeI2V, 144 | ModelKey: "veo_2_1_fast_d_15_i2v", 145 | AspectRatio: "VIDEO_ASPECT_RATIO_LANDSCAPE", 146 | SupportsImages: true, 147 | MinImages: 1, 148 | MaxImages: 2, 149 | }, 150 | "veo_2_0_i2v_portrait": { 151 | Type: ModelTypeVideo, 152 | VideoType: VideoTypeI2V, 153 | ModelKey: "veo_2_0_i2v", 154 | AspectRatio: "VIDEO_ASPECT_RATIO_PORTRAIT", 155 | SupportsImages: true, 156 | MinImages: 1, 157 | MaxImages: 2, 158 | }, 159 | "veo_2_0_i2v_landscape": { 160 | Type: ModelTypeVideo, 161 | VideoType: VideoTypeI2V, 162 | ModelKey: "veo_2_0_i2v", 163 | AspectRatio: "VIDEO_ASPECT_RATIO_LANDSCAPE", 164 | SupportsImages: true, 165 | MinImages: 1, 166 | MaxImages: 2, 167 | }, 168 | 169 | // ========== 多图生成 (R2V) ========== 170 | "veo_3_0_r2v_fast_portrait": { 171 | Type: ModelTypeVideo, 172 | VideoType: VideoTypeR2V, 173 | ModelKey: "veo_3_0_r2v_fast", 174 | AspectRatio: "VIDEO_ASPECT_RATIO_PORTRAIT", 175 | SupportsImages: true, 176 | MinImages: 0, 177 | MaxImages: 0, // 不限制 178 | }, 179 | "veo_3_0_r2v_fast_landscape": { 180 | Type: ModelTypeVideo, 181 | VideoType: VideoTypeR2V, 182 | ModelKey: "veo_3_0_r2v_fast", 183 | AspectRatio: "VIDEO_ASPECT_RATIO_LANDSCAPE", 184 | SupportsImages: true, 185 | MinImages: 0, 186 | MaxImages: 0, // 不限制 187 | }, 188 | } 189 | 190 | // IsFlowModel 检查是否是 Flow 模型 191 | func IsFlowModel(model string) bool { 192 | _, ok := FlowModelConfig[model] 193 | return ok 194 | } 195 | 196 | // GetFlowModelConfig 获取 Flow 模型配置 197 | func GetFlowModelConfig(model string) (ModelConfig, bool) { 198 | cfg, ok := FlowModelConfig[model] 199 | return cfg, ok 200 | } 201 | 202 | // GetAllFlowModels 获取所有 Flow 模型名称 203 | func GetAllFlowModels() []string { 204 | models := make([]string, 0, len(FlowModelConfig)) 205 | for name := range FlowModelConfig { 206 | models = append(models, name) 207 | } 208 | return models 209 | } 210 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module business2api 2 | 3 | go 1.25 4 | 5 | require ( 6 | github.com/fsnotify/fsnotify v1.9.0 7 | github.com/gin-gonic/gin v1.9.1 8 | github.com/go-rod/rod v0.116.2 9 | github.com/google/uuid v1.6.0 10 | github.com/gorilla/websocket v1.5.3 11 | github.com/sagernet/sing-box v1.12.12 12 | github.com/sagernet/sing-quic v0.5.2-0.20250909083218-00a55617c0fb 13 | golang.org/x/image v0.33.0 14 | ) 15 | 16 | require ( 17 | filippo.io/edwards25519 v1.1.0 // indirect 18 | github.com/ajg/form v1.5.1 // indirect 19 | github.com/akutz/memconn v0.1.0 // indirect 20 | github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect 21 | github.com/andybalholm/brotli v1.1.0 // indirect 22 | github.com/anytls/sing-anytls v0.0.11 // indirect 23 | github.com/bits-and-blooms/bitset v1.13.0 // indirect 24 | github.com/bytedance/sonic v1.9.1 // indirect 25 | github.com/caddyserver/certmagic v0.23.0 // indirect 26 | github.com/caddyserver/zerossl v0.1.3 // indirect 27 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect 28 | github.com/coder/websocket v1.8.13 // indirect 29 | github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 // indirect 30 | github.com/cretz/bine v0.2.0 // indirect 31 | github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect 32 | github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e // indirect 33 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 34 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect 35 | github.com/gaissmai/bart v0.11.1 // indirect 36 | github.com/gin-contrib/sse v0.1.0 // indirect 37 | github.com/go-chi/chi/v5 v5.2.2 // indirect 38 | github.com/go-chi/render v1.0.3 // indirect 39 | github.com/go-json-experiment/json v0.0.0-20250103232110-6a9a0fde9288 // indirect 40 | github.com/go-ole/go-ole v1.3.0 // indirect 41 | github.com/go-playground/locales v0.14.1 // indirect 42 | github.com/go-playground/universal-translator v0.18.1 // indirect 43 | github.com/go-playground/validator/v10 v10.14.0 // indirect 44 | github.com/gobwas/httphead v0.1.0 // indirect 45 | github.com/gobwas/pool v0.2.1 // indirect 46 | github.com/goccy/go-json v0.10.2 // indirect 47 | github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 // indirect 48 | github.com/gofrs/uuid/v5 v5.3.2 // indirect 49 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 50 | github.com/google/btree v1.1.3 // indirect 51 | github.com/google/go-cmp v0.7.0 // indirect 52 | github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 // indirect 53 | github.com/gorilla/csrf v1.7.3-0.20250123201450-9dd6af1f6d30 // indirect 54 | github.com/gorilla/securecookie v1.1.2 // indirect 55 | github.com/hashicorp/yamux v0.1.2 // indirect 56 | github.com/hdevalence/ed25519consensus v0.2.0 // indirect 57 | github.com/illarion/gonotify/v2 v2.0.3 // indirect 58 | github.com/insomniacslk/dhcp v0.0.0-20250417080101-5f8cf70e8c5f // indirect 59 | github.com/jsimonetti/rtnetlink v1.4.0 // indirect 60 | github.com/json-iterator/go v1.1.12 // indirect 61 | github.com/klauspost/compress v1.17.11 // indirect 62 | github.com/klauspost/cpuid/v2 v2.2.10 // indirect 63 | github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a // indirect 64 | github.com/kr/pretty v0.3.1 // indirect 65 | github.com/leodido/go-urn v1.2.4 // indirect 66 | github.com/libdns/alidns v1.0.5-libdns.v1.beta1 // indirect 67 | github.com/libdns/cloudflare v0.2.2-0.20250708034226-c574dccb31a6 // indirect 68 | github.com/libdns/libdns v1.1.0 // indirect 69 | github.com/logrusorgru/aurora v2.0.3+incompatible // indirect 70 | github.com/mattn/go-isatty v0.0.19 // indirect 71 | github.com/mdlayher/genetlink v1.3.2 // indirect 72 | github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 // indirect 73 | github.com/mdlayher/sdnotify v1.0.0 // indirect 74 | github.com/mdlayher/socket v0.5.1 // indirect 75 | github.com/metacubex/tfo-go v0.0.0-20250921095601-b102db4216c0 // indirect 76 | github.com/metacubex/utls v1.8.3 // indirect 77 | github.com/mholt/acmez/v3 v3.1.2 // indirect 78 | github.com/miekg/dns v1.1.68 // indirect 79 | github.com/mitchellh/go-ps v1.0.0 // indirect 80 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 81 | github.com/modern-go/reflect2 v1.0.2 // indirect 82 | github.com/pelletier/go-toml/v2 v2.0.8 // indirect 83 | github.com/pierrec/lz4/v4 v4.1.21 // indirect 84 | github.com/prometheus-community/pro-bing v0.4.0 // indirect 85 | github.com/quic-go/qpack v0.5.1 // indirect 86 | github.com/safchain/ethtool v0.3.0 // indirect 87 | github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a // indirect 88 | github.com/sagernet/cors v1.2.1 // indirect 89 | github.com/sagernet/fswatch v0.1.1 // indirect 90 | github.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb // indirect 91 | github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect 92 | github.com/sagernet/nftables v0.3.0-beta.4 // indirect 93 | github.com/sagernet/quic-go v0.52.0-sing-box-mod.3 // indirect 94 | github.com/sagernet/sing v0.7.13 // indirect 95 | github.com/sagernet/sing-mux v0.3.3 // indirect 96 | github.com/sagernet/sing-shadowsocks v0.2.8 // indirect 97 | github.com/sagernet/sing-shadowsocks2 v0.2.1 // indirect 98 | github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 // indirect 99 | github.com/sagernet/sing-tun v0.7.3 // indirect 100 | github.com/sagernet/sing-vmess v0.2.7 // indirect 101 | github.com/sagernet/smux v1.5.34-mod.2 // indirect 102 | github.com/sagernet/tailscale v1.80.3-sing-box-1.12-mod.2 // indirect 103 | github.com/sagernet/wireguard-go v0.0.1-beta.7 // indirect 104 | github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 // indirect 105 | github.com/stretchr/testify v1.11.1 // indirect 106 | github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e // indirect 107 | github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect 108 | github.com/tailscale/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4 // indirect 109 | github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 // indirect 110 | github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a // indirect 111 | github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 // indirect 112 | github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc // indirect 113 | github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 // indirect 114 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 115 | github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect 116 | github.com/ugorji/go/codec v1.2.11 // indirect 117 | github.com/vishvananda/netns v0.0.5 // indirect 118 | github.com/x448/float16 v0.8.4 // indirect 119 | github.com/ysmood/fetchup v0.2.3 // indirect 120 | github.com/ysmood/goob v0.4.0 // indirect 121 | github.com/ysmood/got v0.40.0 // indirect 122 | github.com/ysmood/gson v0.7.3 // indirect 123 | github.com/ysmood/leakless v0.9.0 // indirect 124 | github.com/zeebo/blake3 v0.2.4 // indirect 125 | go.uber.org/multierr v1.11.0 // indirect 126 | go.uber.org/zap v1.27.0 // indirect 127 | go.uber.org/zap/exp v0.3.0 // indirect 128 | go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect 129 | go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect 130 | golang.org/x/arch v0.3.0 // indirect 131 | golang.org/x/crypto v0.45.0 // indirect 132 | golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect 133 | golang.org/x/mod v0.29.0 // indirect 134 | golang.org/x/net v0.47.0 // indirect 135 | golang.org/x/sync v0.18.0 // indirect 136 | golang.org/x/sys v0.38.0 // indirect 137 | golang.org/x/term v0.37.0 // indirect 138 | golang.org/x/text v0.31.0 // indirect 139 | golang.org/x/time v0.12.0 // indirect 140 | golang.org/x/tools v0.38.0 // indirect 141 | golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect 142 | golang.zx2c4.com/wireguard/windows v0.5.3 // indirect 143 | google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect 144 | google.golang.org/grpc v1.77.0 // indirect 145 | google.golang.org/protobuf v1.36.10 // indirect 146 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 147 | gopkg.in/yaml.v3 v3.0.1 // indirect 148 | lukechampine.com/blake3 v1.4.1 // indirect 149 | ) 150 | -------------------------------------------------------------------------------- /src/flow/token_pool.go: -------------------------------------------------------------------------------- 1 | // Package flow 实现 Flow Token 池管理 2 | package flow 3 | 4 | import ( 5 | "crypto/md5" 6 | "encoding/hex" 7 | "fmt" 8 | "log" 9 | "os" 10 | "path/filepath" 11 | "regexp" 12 | "strings" 13 | "sync" 14 | "time" 15 | 16 | "github.com/fsnotify/fsnotify" 17 | ) 18 | 19 | // TokenPool Flow Token 池管理器 20 | type TokenPool struct { 21 | mu sync.RWMutex 22 | tokens map[string]*FlowToken 23 | dataDir string 24 | client *FlowClient 25 | stopChan chan struct{} 26 | watcher *fsnotify.Watcher 27 | fileIndex map[string]string // fileName -> tokenID 28 | } 29 | 30 | // NewTokenPool 创建新的 Token 池 31 | func NewTokenPool(dataDir string, client *FlowClient) *TokenPool { 32 | return &TokenPool{ 33 | tokens: make(map[string]*FlowToken), 34 | dataDir: dataDir, 35 | client: client, 36 | stopChan: make(chan struct{}), 37 | fileIndex: make(map[string]string), 38 | } 39 | } 40 | 41 | // LoadFromDir 从目录加载所有 Token 42 | // 每个文件包含一个完整的 cookie,自动提取 __Secure-next-auth.session-token 43 | func (p *TokenPool) LoadFromDir() (int, error) { 44 | atDir := filepath.Join(p.dataDir, "at") 45 | 46 | // 确保目录存在 47 | if err := os.MkdirAll(atDir, 0755); err != nil { 48 | return 0, fmt.Errorf("创建目录失败: %w", err) 49 | } 50 | 51 | files, err := os.ReadDir(atDir) 52 | if err != nil { 53 | return 0, fmt.Errorf("读取目录失败: %w", err) 54 | } 55 | 56 | loaded := 0 57 | for _, f := range files { 58 | if f.IsDir() { 59 | continue 60 | } 61 | 62 | filePath := filepath.Join(atDir, f.Name()) 63 | content, err := os.ReadFile(filePath) 64 | if err != nil { 65 | log.Printf("[FlowPool] 读取文件失败 %s: %v", f.Name(), err) 66 | continue 67 | } 68 | 69 | // 提取 session-token 70 | st := extractSessionToken(string(content)) 71 | if st == "" { 72 | log.Printf("[FlowPool] 文件 %s 中未找到有效的 session-token", f.Name()) 73 | continue 74 | } 75 | 76 | // 生成唯一ID 77 | tokenID := generateTokenID(st) 78 | 79 | p.mu.Lock() 80 | if _, exists := p.tokens[tokenID]; !exists { 81 | token := &FlowToken{ 82 | ID: tokenID, 83 | ST: st, 84 | } 85 | p.tokens[tokenID] = token 86 | if p.client != nil { 87 | p.client.AddToken(token) 88 | } 89 | loaded++ 90 | log.Printf("[FlowPool] 加载 Token: %s (来自 %s)", tokenID[:16]+"...", f.Name()) 91 | } 92 | p.mu.Unlock() 93 | } 94 | 95 | return loaded, nil 96 | } 97 | 98 | // AddFromCookie 从完整 cookie 字符串添加 Token 99 | func (p *TokenPool) AddFromCookie(cookie string) (string, error) { 100 | st := extractSessionToken(cookie) 101 | if st == "" { 102 | return "", fmt.Errorf("cookie 中未找到有效的 session-token") 103 | } 104 | 105 | tokenID := generateTokenID(st) 106 | 107 | p.mu.Lock() 108 | defer p.mu.Unlock() 109 | 110 | if _, exists := p.tokens[tokenID]; exists { 111 | return tokenID, fmt.Errorf("Token 已存在") 112 | } 113 | 114 | token := &FlowToken{ 115 | ID: tokenID, 116 | ST: st, 117 | } 118 | p.tokens[tokenID] = token 119 | if p.client != nil { 120 | p.client.AddToken(token) 121 | } 122 | 123 | // 保存到文件 124 | if err := p.saveTokenToFile(tokenID, cookie); err != nil { 125 | log.Printf("[FlowPool] 保存 Token 到文件失败: %v", err) 126 | } 127 | 128 | return tokenID, nil 129 | } 130 | 131 | // saveTokenToFile 保存 Token 到文件 132 | func (p *TokenPool) saveTokenToFile(tokenID, cookie string) error { 133 | atDir := filepath.Join(p.dataDir, "at") 134 | if err := os.MkdirAll(atDir, 0755); err != nil { 135 | return err 136 | } 137 | 138 | fileName := fmt.Sprintf("%s.txt", tokenID[:16]) 139 | filePath := filepath.Join(atDir, fileName) 140 | 141 | return os.WriteFile(filePath, []byte(cookie), 0600) 142 | } 143 | 144 | // RemoveToken 移除 Token 145 | func (p *TokenPool) RemoveToken(tokenID string) error { 146 | p.mu.Lock() 147 | defer p.mu.Unlock() 148 | 149 | if _, exists := p.tokens[tokenID]; !exists { 150 | return fmt.Errorf("Token 不存在") 151 | } 152 | 153 | delete(p.tokens, tokenID) 154 | 155 | // 删除文件 156 | atDir := filepath.Join(p.dataDir, "at") 157 | files, _ := os.ReadDir(atDir) 158 | for _, f := range files { 159 | if strings.HasPrefix(f.Name(), tokenID[:16]) { 160 | os.Remove(filepath.Join(atDir, f.Name())) 161 | break 162 | } 163 | } 164 | 165 | return nil 166 | } 167 | 168 | // Count 返回 Token 数量 169 | func (p *TokenPool) Count() int { 170 | p.mu.RLock() 171 | defer p.mu.RUnlock() 172 | return len(p.tokens) 173 | } 174 | 175 | // ReadyCount 返回可用 Token 数量 176 | func (p *TokenPool) ReadyCount() int { 177 | p.mu.RLock() 178 | defer p.mu.RUnlock() 179 | 180 | count := 0 181 | for _, t := range p.tokens { 182 | if !t.Disabled && t.ErrorCount < 3 { 183 | count++ 184 | } 185 | } 186 | return count 187 | } 188 | 189 | // Stats 返回统计信息 190 | func (p *TokenPool) Stats() map[string]interface{} { 191 | p.mu.RLock() 192 | defer p.mu.RUnlock() 193 | 194 | ready := 0 195 | disabled := 0 196 | errored := 0 197 | 198 | tokenInfos := make([]map[string]interface{}, 0) 199 | 200 | for _, t := range p.tokens { 201 | t.mu.RLock() 202 | info := map[string]interface{}{ 203 | "id": t.ID[:16] + "...", 204 | "email": t.Email, 205 | "credits": t.Credits, 206 | "disabled": t.Disabled, 207 | "error_count": t.ErrorCount, 208 | "last_used": t.LastUsed.Format(time.RFC3339), 209 | } 210 | t.mu.RUnlock() 211 | 212 | tokenInfos = append(tokenInfos, info) 213 | 214 | if t.Disabled { 215 | disabled++ 216 | } else if t.ErrorCount >= 3 { 217 | errored++ 218 | } else { 219 | ready++ 220 | } 221 | } 222 | 223 | return map[string]interface{}{ 224 | "total": len(p.tokens), 225 | "ready": ready, 226 | "disabled": disabled, 227 | "errored": errored, 228 | "tokens": tokenInfos, 229 | } 230 | } 231 | 232 | // StartRefreshWorker 启动定期刷新 AT 的 worker 233 | func (p *TokenPool) StartRefreshWorker(interval time.Duration) { 234 | go func() { 235 | ticker := time.NewTicker(interval) 236 | defer ticker.Stop() 237 | 238 | for { 239 | select { 240 | case <-ticker.C: 241 | p.refreshAllAT() 242 | case <-p.stopChan: 243 | return 244 | } 245 | } 246 | }() 247 | log.Printf("[FlowPool] 刷新 worker 已启动,间隔: %v", interval) 248 | } 249 | 250 | // Stop 停止 Token 池 251 | func (p *TokenPool) Stop() { 252 | close(p.stopChan) 253 | if p.watcher != nil { 254 | p.watcher.Close() 255 | } 256 | } 257 | 258 | // StartWatcher 启动文件监听 259 | func (p *TokenPool) StartWatcher() error { 260 | atDir := filepath.Join(p.dataDir, "at") 261 | 262 | // 确保目录存在 263 | if err := os.MkdirAll(atDir, 0755); err != nil { 264 | return fmt.Errorf("创建目录失败: %w", err) 265 | } 266 | 267 | watcher, err := fsnotify.NewWatcher() 268 | if err != nil { 269 | return fmt.Errorf("创建文件监听器失败: %w", err) 270 | } 271 | p.watcher = watcher 272 | 273 | go p.watchLoop() 274 | 275 | if err := watcher.Add(atDir); err != nil { 276 | return fmt.Errorf("添加监听目录失败: %w", err) 277 | } 278 | 279 | log.Printf("[FlowPool] 文件监听已启动: %s", atDir) 280 | return nil 281 | } 282 | 283 | // watchLoop 文件监听循环 284 | func (p *TokenPool) watchLoop() { 285 | for { 286 | select { 287 | case event, ok := <-p.watcher.Events: 288 | if !ok { 289 | return 290 | } 291 | p.handleFileEvent(event) 292 | case err, ok := <-p.watcher.Errors: 293 | if !ok { 294 | return 295 | } 296 | log.Printf("[FlowPool] 文件监听错误: %v", err) 297 | case <-p.stopChan: 298 | return 299 | } 300 | } 301 | } 302 | 303 | // handleFileEvent 处理文件事件 304 | func (p *TokenPool) handleFileEvent(event fsnotify.Event) { 305 | fileName := filepath.Base(event.Name) 306 | 307 | // 忽略 README 和隐藏文件 308 | if strings.HasPrefix(fileName, ".") || strings.EqualFold(fileName, "README.md") { 309 | return 310 | } 311 | 312 | switch { 313 | case event.Op&fsnotify.Create == fsnotify.Create: 314 | // 新文件创建 315 | time.Sleep(100 * time.Millisecond) // 等待文件写入完成 316 | p.loadTokenFromFile(event.Name) 317 | 318 | case event.Op&fsnotify.Write == fsnotify.Write: 319 | // 文件修改 320 | time.Sleep(100 * time.Millisecond) 321 | p.loadTokenFromFile(event.Name) 322 | 323 | case event.Op&fsnotify.Remove == fsnotify.Remove: 324 | // 文件删除 325 | p.removeTokenByFile(fileName) 326 | 327 | case event.Op&fsnotify.Rename == fsnotify.Rename: 328 | // 文件重命名 (视为删除) 329 | p.removeTokenByFile(fileName) 330 | } 331 | } 332 | 333 | // loadTokenFromFile 从单个文件加载 Token 334 | func (p *TokenPool) loadTokenFromFile(filePath string) { 335 | fileName := filepath.Base(filePath) 336 | 337 | content, err := os.ReadFile(filePath) 338 | if err != nil { 339 | log.Printf("[FlowPool] 读取文件失败 %s: %v", fileName, err) 340 | return 341 | } 342 | 343 | st := extractSessionToken(string(content)) 344 | if st == "" { 345 | log.Printf("[FlowPool] 文件 %s 中未找到有效的 session-token", fileName) 346 | return 347 | } 348 | 349 | tokenID := generateTokenID(st) 350 | 351 | p.mu.Lock() 352 | defer p.mu.Unlock() 353 | 354 | // 检查是否已存在 355 | if existingID, ok := p.fileIndex[fileName]; ok { 356 | if existingID == tokenID { 357 | // 同一个 Token,无需更新 358 | return 359 | } 360 | // 文件内容变了,移除旧 Token 361 | delete(p.tokens, existingID) 362 | log.Printf("[FlowPool] Token 已更新: %s", fileName) 363 | } 364 | 365 | if _, exists := p.tokens[tokenID]; !exists { 366 | token := &FlowToken{ 367 | ID: tokenID, 368 | ST: st, 369 | } 370 | p.tokens[tokenID] = token 371 | p.fileIndex[fileName] = tokenID 372 | if p.client != nil { 373 | p.client.AddToken(token) 374 | } 375 | log.Printf("[FlowPool] 自动加载 Token: %s (来自 %s)", tokenID[:16]+"...", fileName) 376 | 377 | // 立即尝试刷新 AT 378 | go p.refreshSingleToken(token) 379 | } 380 | } 381 | 382 | // removeTokenByFile 根据文件名移除 Token 383 | func (p *TokenPool) removeTokenByFile(fileName string) { 384 | p.mu.Lock() 385 | defer p.mu.Unlock() 386 | 387 | tokenID, ok := p.fileIndex[fileName] 388 | if !ok { 389 | return 390 | } 391 | 392 | delete(p.tokens, tokenID) 393 | delete(p.fileIndex, fileName) 394 | log.Printf("[FlowPool] Token 已移除: %s (文件 %s 已删除)", tokenID[:16]+"...", fileName) 395 | } 396 | 397 | // refreshSingleToken 刷新单个 Token 的 AT 398 | func (p *TokenPool) refreshSingleToken(token *FlowToken) { 399 | if p.client == nil { 400 | return 401 | } 402 | 403 | resp, err := p.client.STToAT(token.ST) 404 | if err != nil { 405 | token.mu.Lock() 406 | token.ErrorCount++ 407 | token.mu.Unlock() 408 | log.Printf("[FlowPool] Token %s AT 刷新失败: %v", token.ID[:16]+"...", err) 409 | return 410 | } 411 | 412 | token.mu.Lock() 413 | token.AT = resp.AccessToken 414 | if resp.Expires != "" { 415 | if t, err := time.Parse(time.RFC3339, resp.Expires); err == nil { 416 | token.ATExpires = t 417 | } 418 | } 419 | token.Email = resp.Email 420 | token.ErrorCount = 0 421 | token.Disabled = false 422 | token.mu.Unlock() 423 | 424 | log.Printf("[FlowPool] Token %s AT 已刷新, Email: %s", token.ID[:16]+"...", resp.Email) 425 | } 426 | 427 | // refreshAllAT 刷新所有 Token 的 AT 428 | func (p *TokenPool) refreshAllAT() { 429 | p.mu.RLock() 430 | tokens := make([]*FlowToken, 0, len(p.tokens)) 431 | for _, t := range p.tokens { 432 | tokens = append(tokens, t) 433 | } 434 | p.mu.RUnlock() 435 | 436 | for _, token := range tokens { 437 | token.mu.Lock() 438 | // 检查是否需要刷新 439 | needRefresh := token.AT == "" || time.Now().After(token.ATExpires.Add(-5*time.Minute)) 440 | token.mu.Unlock() 441 | 442 | if !needRefresh { 443 | continue 444 | } 445 | 446 | if p.client == nil { 447 | continue 448 | } 449 | 450 | resp, err := p.client.STToAT(token.ST) 451 | if err != nil { 452 | token.mu.Lock() 453 | token.ErrorCount++ 454 | if token.ErrorCount >= 3 { 455 | token.Disabled = true 456 | log.Printf("[FlowPool] Token %s 刷新失败次数过多,已禁用: %v", token.ID[:16]+"...", err) 457 | } 458 | token.mu.Unlock() 459 | continue 460 | } 461 | 462 | token.mu.Lock() 463 | token.AT = resp.AccessToken 464 | if resp.Expires != "" { 465 | if t, err := time.Parse(time.RFC3339, resp.Expires); err == nil { 466 | token.ATExpires = t 467 | } 468 | } 469 | token.Email = resp.Email 470 | token.ErrorCount = 0 471 | token.Disabled = false 472 | token.mu.Unlock() 473 | 474 | log.Printf("[FlowPool] Token %s AT 已刷新, Email: %s", token.ID[:16]+"...", resp.Email) 475 | } 476 | } 477 | 478 | // extractSessionToken 从 cookie 字符串提取 __Secure-next-auth.session-token 479 | func extractSessionToken(cookie string) string { 480 | // 正则匹配 __Secure-next-auth.session-token=... 481 | // Token 可能以 ; 或空格或行尾结束 482 | patterns := []string{ 483 | `__Secure-next-auth\.session-token=([^;\s]+)`, 484 | `__Secure-next-auth\.session-token=([^\s;]+)`, 485 | } 486 | 487 | for _, pattern := range patterns { 488 | re := regexp.MustCompile(pattern) 489 | matches := re.FindStringSubmatch(cookie) 490 | if len(matches) >= 2 { 491 | return strings.TrimSpace(matches[1]) 492 | } 493 | } 494 | 495 | // 如果输入本身就是 token(不包含 = 的长字符串) 496 | cookie = strings.TrimSpace(cookie) 497 | if !strings.Contains(cookie, "=") && len(cookie) > 100 { 498 | return cookie 499 | } 500 | 501 | return "" 502 | } 503 | 504 | // generateTokenID 根据 ST 生成唯一 ID 505 | func generateTokenID(st string) string { 506 | hash := md5.Sum([]byte(st)) 507 | return hex.EncodeToString(hash[:]) 508 | } 509 | -------------------------------------------------------------------------------- /src/flow/handler.go: -------------------------------------------------------------------------------- 1 | package flow 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "time" 8 | ) 9 | 10 | // GenerationHandler Flow 生成处理器 11 | type GenerationHandler struct { 12 | client *FlowClient 13 | } 14 | 15 | // NewGenerationHandler 创建生成处理器 16 | func NewGenerationHandler(client *FlowClient) *GenerationHandler { 17 | return &GenerationHandler{client: client} 18 | } 19 | 20 | // GenerationRequest 生成请求 21 | type GenerationRequest struct { 22 | Model string `json:"model"` 23 | Prompt string `json:"prompt"` 24 | Images [][]byte `json:"images,omitempty"` // 图片字节数据 25 | Stream bool `json:"stream"` 26 | } 27 | 28 | // GenerationResult 生成结果 29 | type GenerationResult struct { 30 | Success bool `json:"success"` 31 | Type string `json:"type"` // "image" 或 "video" 32 | URL string `json:"url"` 33 | Error string `json:"error,omitempty"` 34 | Progress int `json:"progress,omitempty"` 35 | Message string `json:"message,omitempty"` 36 | } 37 | 38 | // StreamCallback 流式回调函数 39 | type StreamCallback func(chunk string) 40 | 41 | // HandleGeneration 处理生成请求 42 | func (h *GenerationHandler) HandleGeneration(req GenerationRequest, streamCb StreamCallback) (*GenerationResult, error) { 43 | // 验证模型 44 | modelConfig, ok := GetFlowModelConfig(req.Model) 45 | if !ok { 46 | return &GenerationResult{ 47 | Success: false, 48 | Error: fmt.Sprintf("不支持的模型: %s", req.Model), 49 | }, nil 50 | } 51 | 52 | // 选择 Token 53 | token := h.client.SelectToken() 54 | if token == nil { 55 | return &GenerationResult{ 56 | Success: false, 57 | Error: "没有可用的 Flow Token", 58 | }, nil 59 | } 60 | 61 | // 确保 AT 有效 62 | if err := h.ensureATValid(token); err != nil { 63 | return &GenerationResult{ 64 | Success: false, 65 | Error: fmt.Sprintf("Token 认证失败: %v", err), 66 | }, nil 67 | } 68 | 69 | // 更新余额信息 (异步) 70 | go h.updateTokenCredits(token) 71 | 72 | // 确保 Project 存在 73 | if err := h.ensureProjectExists(token); err != nil { 74 | return &GenerationResult{ 75 | Success: false, 76 | Error: fmt.Sprintf("创建项目失败: %v", err), 77 | }, nil 78 | } 79 | 80 | // 根据类型处理 81 | if modelConfig.Type == ModelTypeImage { 82 | return h.handleImageGeneration(token, modelConfig, req, streamCb) 83 | } else { 84 | return h.handleVideoGeneration(token, modelConfig, req, streamCb) 85 | } 86 | } 87 | 88 | // ensureATValid 确保 AT 有效 89 | func (h *GenerationHandler) ensureATValid(token *FlowToken) error { 90 | token.mu.Lock() 91 | defer token.mu.Unlock() 92 | 93 | // AT 还有效且未过期 94 | if token.AT != "" && time.Now().Before(token.ATExpires.Add(-5*time.Minute)) { 95 | return nil 96 | } 97 | 98 | // 刷新 AT 99 | resp, err := h.client.STToAT(token.ST) 100 | if err != nil { 101 | return err 102 | } 103 | 104 | token.AT = resp.AccessToken 105 | if resp.Expires != "" { 106 | if t, err := time.Parse(time.RFC3339, resp.Expires); err == nil { 107 | token.ATExpires = t 108 | } 109 | } 110 | token.Email = resp.Email 111 | 112 | log.Printf("[Flow] Token %s AT 已刷新, 过期时间: %v", token.ID, token.ATExpires) 113 | return nil 114 | } 115 | 116 | // updateTokenCredits 更新 Token 余额信息 117 | func (h *GenerationHandler) updateTokenCredits(token *FlowToken) { 118 | if token.AT == "" { 119 | return 120 | } 121 | 122 | resp, err := h.client.GetCredits(token.AT) 123 | if err != nil { 124 | log.Printf("[Flow] 查询余额失败: %v", err) 125 | return 126 | } 127 | 128 | token.mu.Lock() 129 | token.Credits = resp.Credits 130 | token.UserPaygateTier = resp.UserPaygateTier 131 | token.mu.Unlock() 132 | 133 | log.Printf("[Flow] Token %s 余额: %d, Tier: %s", token.ID[:16]+"...", resp.Credits, resp.UserPaygateTier) 134 | } 135 | 136 | // ensureProjectExists 确保 Project 存在 137 | func (h *GenerationHandler) ensureProjectExists(token *FlowToken) error { 138 | token.mu.Lock() 139 | defer token.mu.Unlock() 140 | 141 | if token.ProjectID != "" { 142 | return nil 143 | } 144 | 145 | projectID, err := h.client.CreateProject(token.ST, "Flow2API") 146 | if err != nil { 147 | return err 148 | } 149 | 150 | token.ProjectID = projectID 151 | log.Printf("[Flow] Token %s 创建项目: %s", token.ID, projectID) 152 | return nil 153 | } 154 | 155 | // handleImageGeneration 处理图片生成 156 | func (h *GenerationHandler) handleImageGeneration(token *FlowToken, modelConfig ModelConfig, req GenerationRequest, streamCb StreamCallback) (*GenerationResult, error) { 157 | if streamCb != nil { 158 | streamCb(h.createStreamChunk("✨ 图片生成任务已启动\n", false)) 159 | } 160 | 161 | // 上传图片 (如果有) 162 | var imageInputs []map[string]interface{} 163 | if len(req.Images) > 0 { 164 | if streamCb != nil { 165 | streamCb(h.createStreamChunk(fmt.Sprintf("上传 %d 张参考图片...\n", len(req.Images)), false)) 166 | } 167 | 168 | for i, imgBytes := range req.Images { 169 | mediaID, err := h.client.UploadImage(token.AT, imgBytes, modelConfig.AspectRatio) 170 | if err != nil { 171 | return &GenerationResult{ 172 | Success: false, 173 | Error: fmt.Sprintf("上传图片失败: %v", err), 174 | }, nil 175 | } 176 | imageInputs = append(imageInputs, map[string]interface{}{ 177 | "name": mediaID, 178 | "imageInputType": "IMAGE_INPUT_TYPE_REFERENCE", 179 | }) 180 | if streamCb != nil { 181 | streamCb(h.createStreamChunk(fmt.Sprintf("已上传第 %d/%d 张图片\n", i+1, len(req.Images)), false)) 182 | } 183 | } 184 | } 185 | 186 | if streamCb != nil { 187 | streamCb(h.createStreamChunk("正在生成图片...\n", false)) 188 | } 189 | 190 | // 调用生成 API 191 | result, err := h.client.GenerateImage( 192 | token.AT, 193 | token.ProjectID, 194 | req.Prompt, 195 | modelConfig.ModelName, 196 | modelConfig.AspectRatio, 197 | imageInputs, 198 | ) 199 | if err != nil { 200 | token.mu.Lock() 201 | token.ErrorCount++ 202 | token.mu.Unlock() 203 | return &GenerationResult{ 204 | Success: false, 205 | Error: fmt.Sprintf("生成图片失败: %v", err), 206 | }, nil 207 | } 208 | 209 | if result.ImageURL == "" { 210 | return &GenerationResult{ 211 | Success: false, 212 | Error: "生成结果为空", 213 | }, nil 214 | } 215 | 216 | // 更新 Token 使用 217 | token.mu.Lock() 218 | token.LastUsed = time.Now() 219 | token.ErrorCount = 0 220 | token.mu.Unlock() 221 | 222 | if streamCb != nil { 223 | streamCb(h.createStreamChunk(fmt.Sprintf("![Generated Image](%s)", result.ImageURL), true)) 224 | } 225 | 226 | return &GenerationResult{ 227 | Success: true, 228 | Type: "image", 229 | URL: result.ImageURL, 230 | }, nil 231 | } 232 | 233 | // handleVideoGeneration 处理视频生成 234 | func (h *GenerationHandler) handleVideoGeneration(token *FlowToken, modelConfig ModelConfig, req GenerationRequest, streamCb StreamCallback) (*GenerationResult, error) { 235 | if streamCb != nil { 236 | streamCb(h.createStreamChunk("✨ 视频生成任务已启动\n", false)) 237 | } 238 | 239 | imageCount := len(req.Images) 240 | 241 | // 验证图片数量 242 | if modelConfig.VideoType == VideoTypeT2V { 243 | if imageCount > 0 { 244 | if streamCb != nil { 245 | streamCb(h.createStreamChunk("⚠️ 文生视频模型不支持图片,将忽略图片仅使用文本提示词\n", false)) 246 | } 247 | req.Images = nil 248 | imageCount = 0 249 | } 250 | } else if modelConfig.VideoType == VideoTypeI2V { 251 | if imageCount < modelConfig.MinImages || imageCount > modelConfig.MaxImages { 252 | return &GenerationResult{ 253 | Success: false, 254 | Error: fmt.Sprintf("首尾帧模型需要 %d-%d 张图片,当前提供了 %d 张", modelConfig.MinImages, modelConfig.MaxImages, imageCount), 255 | }, nil 256 | } 257 | } 258 | 259 | // 上传图片 260 | var startMediaID, endMediaID string 261 | var referenceImages []map[string]interface{} 262 | 263 | if modelConfig.VideoType == VideoTypeI2V && len(req.Images) > 0 { 264 | if streamCb != nil { 265 | streamCb(h.createStreamChunk("上传首帧图片...\n", false)) 266 | } 267 | var err error 268 | startMediaID, err = h.client.UploadImage(token.AT, req.Images[0], modelConfig.AspectRatio) 269 | if err != nil { 270 | return &GenerationResult{Success: false, Error: fmt.Sprintf("上传首帧失败: %v", err)}, nil 271 | } 272 | 273 | if len(req.Images) == 2 { 274 | if streamCb != nil { 275 | streamCb(h.createStreamChunk("上传尾帧图片...\n", false)) 276 | } 277 | endMediaID, err = h.client.UploadImage(token.AT, req.Images[1], modelConfig.AspectRatio) 278 | if err != nil { 279 | return &GenerationResult{Success: false, Error: fmt.Sprintf("上传尾帧失败: %v", err)}, nil 280 | } 281 | } 282 | } else if modelConfig.VideoType == VideoTypeR2V && len(req.Images) > 0 { 283 | if streamCb != nil { 284 | streamCb(h.createStreamChunk(fmt.Sprintf("上传 %d 张参考图片...\n", len(req.Images)), false)) 285 | } 286 | for _, imgBytes := range req.Images { 287 | mediaID, err := h.client.UploadImage(token.AT, imgBytes, modelConfig.AspectRatio) 288 | if err != nil { 289 | return &GenerationResult{Success: false, Error: fmt.Sprintf("上传图片失败: %v", err)}, nil 290 | } 291 | referenceImages = append(referenceImages, map[string]interface{}{ 292 | "imageUsageType": "IMAGE_USAGE_TYPE_ASSET", 293 | "mediaId": mediaID, 294 | }) 295 | } 296 | } 297 | 298 | if streamCb != nil { 299 | streamCb(h.createStreamChunk("提交视频生成任务...\n", false)) 300 | } 301 | 302 | // 调用生成 API 303 | var videoResp *GenerateVideoResponse 304 | var err error 305 | 306 | userTier := token.UserPaygateTier 307 | if userTier == "" { 308 | userTier = "PAYGATE_TIER_ONE" 309 | } 310 | 311 | switch modelConfig.VideoType { 312 | case VideoTypeI2V: 313 | videoResp, err = h.client.GenerateVideoStartEnd( 314 | token.AT, token.ProjectID, req.Prompt, 315 | modelConfig.ModelKey, modelConfig.AspectRatio, 316 | startMediaID, endMediaID, userTier, 317 | ) 318 | case VideoTypeR2V: 319 | videoResp, err = h.client.GenerateVideoReferenceImages( 320 | token.AT, token.ProjectID, req.Prompt, 321 | modelConfig.ModelKey, modelConfig.AspectRatio, 322 | referenceImages, userTier, 323 | ) 324 | default: // T2V 325 | videoResp, err = h.client.GenerateVideoText( 326 | token.AT, token.ProjectID, req.Prompt, 327 | modelConfig.ModelKey, modelConfig.AspectRatio, userTier, 328 | ) 329 | } 330 | 331 | if err != nil { 332 | token.mu.Lock() 333 | token.ErrorCount++ 334 | token.mu.Unlock() 335 | return &GenerationResult{Success: false, Error: fmt.Sprintf("提交任务失败: %v", err)}, nil 336 | } 337 | 338 | if videoResp.TaskID == "" { 339 | return &GenerationResult{Success: false, Error: "任务创建失败"}, nil 340 | } 341 | 342 | if streamCb != nil { 343 | streamCb(h.createStreamChunk("视频生成中...\n", false)) 344 | } 345 | 346 | // 轮询结果 347 | videoURL, err := h.pollVideoResult(token, videoResp.TaskID, videoResp.SceneID, streamCb) 348 | if err != nil { 349 | return &GenerationResult{Success: false, Error: err.Error()}, nil 350 | } 351 | 352 | // 更新 Token 使用 353 | token.mu.Lock() 354 | token.LastUsed = time.Now() 355 | token.ErrorCount = 0 356 | token.mu.Unlock() 357 | 358 | if streamCb != nil { 359 | streamCb(h.createStreamChunk(fmt.Sprintf("", videoURL), true)) 360 | } 361 | 362 | return &GenerationResult{ 363 | Success: true, 364 | Type: "video", 365 | URL: videoURL, 366 | }, nil 367 | } 368 | 369 | // pollVideoResult 轮询视频生成结果 370 | func (h *GenerationHandler) pollVideoResult(token *FlowToken, taskID, sceneID string, streamCb StreamCallback) (string, error) { 371 | operations := []map[string]interface{}{{ 372 | "operation": map[string]interface{}{ 373 | "name": taskID, 374 | }, 375 | "sceneId": sceneID, 376 | }} 377 | 378 | maxAttempts := h.client.config.MaxPollAttempts 379 | pollInterval := h.client.config.PollInterval 380 | 381 | for i := 0; i < maxAttempts; i++ { 382 | time.Sleep(time.Duration(pollInterval) * time.Second) 383 | 384 | resp, err := h.client.CheckVideoStatus(token.AT, operations) 385 | if err != nil { 386 | continue 387 | } 388 | 389 | // 进度更新 390 | if streamCb != nil && i%7 == 0 { 391 | progress := min(i*100/maxAttempts, 95) 392 | streamCb(h.createStreamChunk(fmt.Sprintf("生成进度: %d%%\n", progress), false)) 393 | } 394 | 395 | switch resp.Status { 396 | case "MEDIA_GENERATION_STATUS_SUCCESSFUL": 397 | if resp.VideoURL != "" { 398 | return resp.VideoURL, nil 399 | } 400 | case "MEDIA_GENERATION_STATUS_ERROR_UNKNOWN", 401 | "MEDIA_GENERATION_STATUS_ERROR_NSFW", 402 | "MEDIA_GENERATION_STATUS_ERROR_PERSON", 403 | "MEDIA_GENERATION_STATUS_ERROR_SAFETY": 404 | return "", fmt.Errorf("视频生成失败: %s", resp.Status) 405 | } 406 | } 407 | 408 | return "", fmt.Errorf("视频生成超时 (已轮询 %d 次)", maxAttempts) 409 | } 410 | 411 | // createStreamChunk 创建流式响应块 412 | func (h *GenerationHandler) createStreamChunk(content string, isFinish bool) string { 413 | chunk := map[string]interface{}{ 414 | "id": fmt.Sprintf("chatcmpl-%d", time.Now().Unix()), 415 | "object": "chat.completion.chunk", 416 | "created": time.Now().Unix(), 417 | "model": "flow2api", 418 | "choices": []map[string]interface{}{{ 419 | "index": 0, 420 | "delta": map[string]interface{}{}, 421 | "finish_reason": nil, 422 | }}, 423 | } 424 | 425 | if isFinish { 426 | chunk["choices"].([]map[string]interface{})[0]["delta"].(map[string]interface{})["content"] = content 427 | chunk["choices"].([]map[string]interface{})[0]["finish_reason"] = "stop" 428 | } else { 429 | chunk["choices"].([]map[string]interface{})[0]["delta"].(map[string]interface{})["reasoning_content"] = content 430 | } 431 | 432 | data, _ := json.Marshal(chunk) 433 | return fmt.Sprintf("data: %s\n\n", string(data)) 434 | } 435 | 436 | func min(a, b int) int { 437 | if a < b { 438 | return a 439 | } 440 | return b 441 | } 442 | -------------------------------------------------------------------------------- /src/proxy/singbox.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "net" 8 | "net/http" 9 | "net/url" 10 | "strconv" 11 | "strings" 12 | "sync" 13 | "time" 14 | 15 | box "github.com/sagernet/sing-box" 16 | "github.com/sagernet/sing-box/include" 17 | "github.com/sagernet/sing-box/option" 18 | 19 | // 启用 QUIC 协议支持(hysteria, hysteria2, tuic) 20 | _ "github.com/sagernet/sing-quic/hysteria" 21 | _ "github.com/sagernet/sing-quic/hysteria2" 22 | _ "github.com/sagernet/sing-quic/tuic" 23 | ) 24 | 25 | type SingboxManager struct { 26 | mu sync.Mutex 27 | instances map[int]*SingboxInstance 28 | basePort int 29 | ready bool 30 | } 31 | 32 | // SingboxInstance sing-box 实例 33 | type SingboxInstance struct { 34 | Port int 35 | Box *box.Box 36 | Ctx context.Context 37 | Cancel context.CancelFunc 38 | Running bool 39 | ProxyURL string 40 | Node *ProxyNode 41 | } 42 | 43 | var singboxMgr = &SingboxManager{ 44 | instances: make(map[int]*SingboxInstance), 45 | basePort: 11800, 46 | ready: true, 47 | } 48 | 49 | // IsSingboxProtocol 所有协议都由 sing-box 处理 50 | func IsSingboxProtocol(protocol string) bool { 51 | switch protocol { 52 | case "vmess", "vless", "shadowsocks", "trojan", "socks", "http", 53 | "hysteria", "hysteria2", "hy2", "tuic", "wireguard", "anytls": 54 | return true 55 | } 56 | return false 57 | } 58 | 59 | func CanSingboxHandle(protocol string) bool { 60 | return IsSingboxProtocol(protocol) 61 | } 62 | 63 | func (sm *SingboxManager) IsAvailable() bool { 64 | return sm.ready 65 | } 66 | 67 | func (sm *SingboxManager) Start(node *ProxyNode) (string, error) { 68 | sm.mu.Lock() 69 | defer sm.mu.Unlock() 70 | 71 | // 分配端口 72 | port := sm.findAvailablePort() 73 | if port == 0 { 74 | return "", fmt.Errorf("无可用端口") 75 | } 76 | configJSON := sm.generateConfigJSON(node, port) 77 | ctx, cancel := context.WithCancel(context.Background()) 78 | ctx = box.Context(ctx, include.InboundRegistry(), include.OutboundRegistry(), 79 | include.EndpointRegistry(), include.DNSTransportRegistry(), include.ServiceRegistry()) 80 | var opts option.Options 81 | err := opts.UnmarshalJSONContext(ctx, []byte(configJSON)) 82 | if err != nil { 83 | cancel() 84 | return "", fmt.Errorf("解析配置失败: %w", err) 85 | } 86 | 87 | singBox, err := box.New(box.Options{ 88 | Context: ctx, 89 | Options: opts, 90 | }) 91 | if err != nil { 92 | cancel() 93 | return "", fmt.Errorf("创建 sing-box 失败: %w", err) 94 | } 95 | 96 | if err := singBox.Start(); err != nil { 97 | cancel() 98 | return "", fmt.Errorf("启动 sing-box 失败: %w", err) 99 | } 100 | 101 | // 等待端口就绪 102 | proxyURL := fmt.Sprintf("http://127.0.0.1:%d", port) 103 | portReady := false 104 | for i := 0; i < 20; i++ { 105 | time.Sleep(50 * time.Millisecond) 106 | conn, err := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", port), 100*time.Millisecond) 107 | if err == nil { 108 | conn.Close() 109 | portReady = true 110 | break 111 | } 112 | } 113 | if !portReady { 114 | singBox.Close() 115 | cancel() 116 | return "", fmt.Errorf("端口 %d 未就绪", port) 117 | } 118 | proxyURLParsed, _ := url.Parse(proxyURL) 119 | testClient := &http.Client{ 120 | Transport: &http.Transport{ 121 | Proxy: http.ProxyURL(proxyURLParsed), 122 | }, 123 | Timeout: 5 * time.Second, 124 | } 125 | testResp, testErr := testClient.Get("https://www.gstatic.com/generate_204") 126 | if testErr != nil { 127 | singBox.Close() 128 | cancel() 129 | return "", fmt.Errorf("连通性测试失败: %w", testErr) 130 | } 131 | testResp.Body.Close() 132 | if testResp.StatusCode != 204 && testResp.StatusCode != 200 { 133 | singBox.Close() 134 | cancel() 135 | return "", fmt.Errorf("连通性测试状态码: %d", testResp.StatusCode) 136 | } 137 | 138 | instance := &SingboxInstance{ 139 | Port: port, 140 | Box: singBox, 141 | Ctx: ctx, 142 | Cancel: cancel, 143 | Running: true, 144 | ProxyURL: proxyURL, 145 | Node: node, 146 | } 147 | 148 | sm.instances[port] = instance 149 | node.LocalPort = port 150 | return proxyURL, nil 151 | } 152 | 153 | // Stop 停止实例 154 | func (sm *SingboxManager) Stop(port int) { 155 | sm.mu.Lock() 156 | defer sm.mu.Unlock() 157 | 158 | if inst, ok := sm.instances[port]; ok { 159 | if inst.Box != nil { 160 | inst.Box.Close() 161 | } 162 | if inst.Cancel != nil { 163 | inst.Cancel() 164 | } 165 | inst.Running = false 166 | delete(sm.instances, port) 167 | } 168 | } 169 | 170 | // StopAll 停止所有实例 171 | func (sm *SingboxManager) StopAll() { 172 | sm.mu.Lock() 173 | defer sm.mu.Unlock() 174 | 175 | for port, inst := range sm.instances { 176 | if inst.Box != nil { 177 | inst.Box.Close() 178 | } 179 | if inst.Cancel != nil { 180 | inst.Cancel() 181 | } 182 | delete(sm.instances, port) 183 | } 184 | } 185 | 186 | func (sm *SingboxManager) findAvailablePort() int { 187 | for port := sm.basePort; port < sm.basePort+1000; port++ { 188 | if _, exists := sm.instances[port]; !exists { 189 | conn, err := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", port), 50*time.Millisecond) 190 | if err != nil { 191 | return port 192 | } 193 | conn.Close() 194 | } 195 | } 196 | return 0 197 | } 198 | 199 | // generateConfigJSON 生成 sing-box JSON 配置 200 | func (sm *SingboxManager) generateConfigJSON(node *ProxyNode, localPort int) string { 201 | outbound := sm.buildOutboundJSON(node) 202 | 203 | config := map[string]interface{}{ 204 | "log": map[string]interface{}{ 205 | "disabled": true, 206 | }, 207 | "inbounds": []map[string]interface{}{ 208 | { 209 | "type": "http", 210 | "tag": "http-in", 211 | "listen": "127.0.0.1", 212 | "listen_port": localPort, 213 | }, 214 | }, 215 | "outbounds": []interface{}{ 216 | outbound, 217 | }, 218 | } 219 | 220 | data, _ := json.Marshal(config) 221 | return string(data) 222 | } 223 | 224 | // buildOutboundJSON 构建 outbound JSON 配置 225 | func (sm *SingboxManager) buildOutboundJSON(node *ProxyNode) map[string]interface{} { 226 | sni := node.SNI 227 | if sni == "" { 228 | sni = node.Server 229 | } 230 | 231 | switch node.Protocol { 232 | case "hysteria2", "hy2": 233 | return map[string]interface{}{ 234 | "type": "hysteria2", 235 | "tag": "proxy", 236 | "server": node.Server, 237 | "server_port": node.Port, 238 | "password": node.Password, 239 | "tls": map[string]interface{}{ 240 | "enabled": true, 241 | "insecure": true, 242 | "server_name": sni, 243 | }, 244 | } 245 | 246 | case "hysteria": 247 | return map[string]interface{}{ 248 | "type": "hysteria", 249 | "tag": "proxy", 250 | "server": node.Server, 251 | "server_port": node.Port, 252 | "auth_str": node.Password, 253 | "tls": map[string]interface{}{ 254 | "enabled": true, 255 | "insecure": true, 256 | "server_name": sni, 257 | }, 258 | } 259 | 260 | case "tuic": 261 | return map[string]interface{}{ 262 | "type": "tuic", 263 | "tag": "proxy", 264 | "server": node.Server, 265 | "server_port": node.Port, 266 | "uuid": node.UUID, 267 | "password": node.Password, 268 | "tls": map[string]interface{}{ 269 | "enabled": true, 270 | "insecure": true, 271 | "server_name": sni, 272 | }, 273 | } 274 | 275 | case "vmess": 276 | out := map[string]interface{}{ 277 | "type": "vmess", 278 | "tag": "proxy", 279 | "server": node.Server, 280 | "server_port": node.Port, 281 | "uuid": node.UUID, 282 | "security": node.Security, 283 | "alter_id": node.AlterId, 284 | } 285 | if node.TLS { 286 | out["tls"] = map[string]interface{}{ 287 | "enabled": true, 288 | "insecure": true, 289 | "server_name": sni, 290 | } 291 | } 292 | if transport := sm.buildTransportJSON(node); transport != nil { 293 | out["transport"] = transport 294 | } 295 | return out 296 | 297 | case "vless": 298 | out := map[string]interface{}{ 299 | "type": "vless", 300 | "tag": "proxy", 301 | "server": node.Server, 302 | "server_port": node.Port, 303 | "uuid": node.UUID, 304 | } 305 | if node.Flow != "" { 306 | out["flow"] = node.Flow 307 | } 308 | if node.Security == "reality" { 309 | fp := node.Fingerprint 310 | if fp == "" { 311 | fp = "chrome" 312 | } 313 | out["tls"] = map[string]interface{}{ 314 | "enabled": true, 315 | "server_name": node.SNI, 316 | "reality": map[string]interface{}{ 317 | "enabled": true, 318 | "public_key": node.PublicKey, 319 | "short_id": node.ShortId, 320 | }, 321 | "utls": map[string]interface{}{ 322 | "enabled": true, 323 | "fingerprint": fp, 324 | }, 325 | } 326 | } else if node.TLS { 327 | out["tls"] = map[string]interface{}{ 328 | "enabled": true, 329 | "insecure": true, 330 | "server_name": sni, 331 | } 332 | } 333 | if transport := sm.buildTransportJSON(node); transport != nil { 334 | out["transport"] = transport 335 | } 336 | return out 337 | 338 | case "shadowsocks": 339 | return map[string]interface{}{ 340 | "type": "shadowsocks", 341 | "tag": "proxy", 342 | "server": node.Server, 343 | "server_port": node.Port, 344 | "method": node.Method, 345 | "password": node.Password, 346 | } 347 | 348 | case "trojan": 349 | out := map[string]interface{}{ 350 | "type": "trojan", 351 | "tag": "proxy", 352 | "server": node.Server, 353 | "server_port": node.Port, 354 | "password": node.Password, 355 | "tls": map[string]interface{}{ 356 | "enabled": true, 357 | "insecure": true, 358 | "server_name": sni, 359 | }, 360 | } 361 | if transport := sm.buildTransportJSON(node); transport != nil { 362 | out["transport"] = transport 363 | } 364 | return out 365 | 366 | default: 367 | return map[string]interface{}{ 368 | "type": "direct", 369 | "tag": "proxy", 370 | } 371 | } 372 | } 373 | 374 | // buildTransportJSON 构建传输层 JSON 配置 375 | func (sm *SingboxManager) buildTransportJSON(node *ProxyNode) map[string]interface{} { 376 | switch node.Network { 377 | case "ws": 378 | transport := map[string]interface{}{ 379 | "type": "ws", 380 | "path": node.Path, 381 | } 382 | if node.Host != "" { 383 | transport["headers"] = map[string]string{ 384 | "Host": node.Host, 385 | } 386 | } 387 | return transport 388 | case "grpc": 389 | return map[string]interface{}{ 390 | "type": "grpc", 391 | "service_name": node.Path, 392 | } 393 | case "httpupgrade": 394 | return map[string]interface{}{ 395 | "type": "httpupgrade", 396 | "path": node.Path, 397 | "host": node.Host, 398 | } 399 | case "h2", "http": 400 | return map[string]interface{}{ 401 | "type": "http", 402 | "path": node.Path, 403 | "host": []string{node.Host}, 404 | } 405 | } 406 | return nil 407 | } 408 | 409 | // GetSingboxManager 获取 sing-box 管理器 410 | func GetSingboxManager() *SingboxManager { 411 | return singboxMgr 412 | } 413 | 414 | // InitSingbox 初始化 sing-box(内置 core 无需初始化) 415 | func InitSingbox() { 416 | 417 | } 418 | 419 | // TrySingboxStart 尝试用 sing-box 启动节点(xray 失败时的回退) 420 | func TrySingboxStart(node *ProxyNode) (string, error) { 421 | if !CanSingboxHandle(node.Protocol) { 422 | return "", fmt.Errorf("sing-box 不支持协议: %s", node.Protocol) 423 | } 424 | return singboxMgr.Start(node) 425 | } 426 | 427 | // StopSingbox 停止指定端口的 sing-box 实例 428 | func StopSingbox(port int) { 429 | singboxMgr.Stop(port) 430 | } 431 | 432 | // ParseProxyLinkWithSingbox 使用 sing-box 解析代理链接 433 | func ParseProxyLinkWithSingbox(link string) *ProxyNode { 434 | node := Manager.parseLine(link) 435 | if node != nil { 436 | return node 437 | } 438 | 439 | link = strings.TrimSpace(link) 440 | if strings.HasPrefix(link, "hy2://") || strings.HasPrefix(link, "hysteria2://") { 441 | return parseHysteria2(link) 442 | } 443 | if strings.HasPrefix(link, "tuic://") { 444 | return parseTUIC(link) 445 | } 446 | 447 | return nil 448 | } 449 | 450 | // parseTUIC 解析 TUIC 链接 451 | func parseTUIC(link string) *ProxyNode { 452 | origLink := link 453 | link = strings.TrimPrefix(link, "tuic://") 454 | 455 | var name string 456 | if idx := strings.LastIndex(link, "#"); idx != -1 { 457 | name, _ = url.QueryUnescape(link[idx+1:]) 458 | link = link[:idx] 459 | } 460 | 461 | var params string 462 | if idx := strings.Index(link, "?"); idx != -1 { 463 | params = link[idx+1:] 464 | link = link[:idx] 465 | } 466 | 467 | atIdx := strings.LastIndex(link, "@") 468 | if atIdx == -1 { 469 | return nil 470 | } 471 | 472 | userPart := link[:atIdx] 473 | hostPart := link[atIdx+1:] 474 | 475 | var uuid, password string 476 | if colonIdx := strings.Index(userPart, ":"); colonIdx != -1 { 477 | uuid = userPart[:colonIdx] 478 | password = userPart[colonIdx+1:] 479 | } else { 480 | uuid = userPart 481 | } 482 | 483 | var server string 484 | var port int 485 | if lastColon := strings.LastIndex(hostPart, ":"); lastColon != -1 { 486 | server = hostPart[:lastColon] 487 | port, _ = strconv.Atoi(hostPart[lastColon+1:]) 488 | } 489 | 490 | node := &ProxyNode{ 491 | Protocol: "tuic", 492 | Name: name, 493 | Server: server, 494 | Port: port, 495 | UUID: uuid, 496 | Password: password, 497 | Raw: origLink, 498 | } 499 | 500 | for _, param := range strings.Split(params, "&") { 501 | if kv := strings.SplitN(param, "=", 2); len(kv) == 2 { 502 | switch kv[0] { 503 | case "sni": 504 | node.SNI = kv[1] 505 | case "alpn": 506 | node.ALPN = kv[1] 507 | } 508 | } 509 | } 510 | 511 | if node.Server == "" || node.Port == 0 { 512 | return nil 513 | } 514 | 515 | return node 516 | } 517 | -------------------------------------------------------------------------------- /src/pool/pool_client.go: -------------------------------------------------------------------------------- 1 | package pool 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "log" 8 | "net/http" 9 | "net/url" 10 | "sync" 11 | "time" 12 | 13 | "business2api/src/logger" 14 | 15 | "github.com/gorilla/websocket" 16 | ) 17 | 18 | type BrowserRegisterResult struct { 19 | Success bool 20 | Email string 21 | FullName string 22 | SecureCookies []Cookie 23 | Authorization string 24 | ConfigID string 25 | CSESIDX string 26 | Error error 27 | } 28 | 29 | // RunBrowserRegisterFunc 注册函数类型 30 | type RunBrowserRegisterFunc func(headless bool, proxy string, id int) *BrowserRegisterResult 31 | 32 | // 客户端版本 33 | const ClientVersion = "2.0.0" 34 | 35 | var ( 36 | RunBrowserRegister RunBrowserRegisterFunc 37 | ClientHeadless bool 38 | ClientProxy string 39 | GetClientProxy func() string // 获取代理的函数 40 | ReleaseProxy func(proxyURL string) // 释放代理的函数 41 | DefaultProxyCount = 3 // 客户端模式默认启动的代理实例数 42 | IsProxyReady func() bool // 检查代理是否就绪 43 | WaitProxyReady func(timeout time.Duration) bool // 等待代理就绪 44 | GetHealthyCount func() int // 获取健康代理数量 45 | proxyReadyTimeout = 30 * time.Second // 代理就绪超时时间(减少等待) 46 | ) 47 | 48 | // PoolClient 号池客户端 49 | type PoolClient struct { 50 | config PoolServerConfig 51 | conn *websocket.Conn 52 | send chan []byte 53 | done chan struct{} 54 | reconnect chan struct{} 55 | stopPump chan struct{} // 停止当前pump 56 | mu sync.Mutex 57 | writeMu sync.Mutex // WebSocket写入锁 58 | isRunning bool 59 | taskSem chan struct{} // 任务并发信号量 60 | } 61 | 62 | // NewPoolClient 创建号池客户端 63 | func NewPoolClient(config PoolServerConfig) *PoolClient { 64 | threads := config.ClientThreads 65 | if threads <= 0 { 66 | threads = 1 67 | } 68 | return &PoolClient{ 69 | config: config, 70 | send: make(chan []byte, 256), 71 | done: make(chan struct{}), 72 | reconnect: make(chan struct{}, 1), 73 | taskSem: make(chan struct{}, threads), 74 | } 75 | } 76 | 77 | // Start 启动客户端 78 | func (pc *PoolClient) Start() error { 79 | pc.mu.Lock() 80 | pc.isRunning = true 81 | pc.mu.Unlock() 82 | 83 | // 连接循环 84 | for pc.isRunning { 85 | if err := pc.connect(); err != nil { 86 | logger.Warn("连接服务器失败: %v, 5秒后重试...", err) 87 | time.Sleep(5 * time.Second) 88 | continue 89 | } 90 | pc.work() 91 | select { 92 | case <-pc.done: 93 | return nil 94 | case <-pc.reconnect: 95 | log.Printf("[PoolClient] 准备重连...") 96 | time.Sleep(2 * time.Second) 97 | } 98 | } 99 | 100 | return nil 101 | } 102 | func (pc *PoolClient) Stop() { 103 | pc.mu.Lock() 104 | pc.isRunning = false 105 | pc.mu.Unlock() 106 | close(pc.done) 107 | } 108 | 109 | // connect 连接到服务器 110 | func (pc *PoolClient) connect() error { 111 | u, err := url.Parse(pc.config.ServerAddr) 112 | if err != nil { 113 | return fmt.Errorf("解析服务器地址失败: %w", err) 114 | } 115 | wsScheme := "ws" 116 | if u.Scheme == "https" { 117 | wsScheme = "wss" 118 | } 119 | wsURL := fmt.Sprintf("%s://%s/ws", wsScheme, u.Host) 120 | if pc.config.Secret != "" { 121 | wsURL += "?secret=" + pc.config.Secret 122 | } 123 | 124 | logger.Debug("连接到 %s", wsURL) 125 | 126 | conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil) 127 | if err != nil { 128 | return fmt.Errorf("WebSocket连接失败: %w", err) 129 | } 130 | 131 | pc.conn = conn 132 | threads := pc.config.ClientThreads 133 | if threads <= 0 { 134 | threads = 1 135 | } 136 | pc.sendMessage(WSMessage{ 137 | Type: WSMsgClientReady, 138 | Version: ClientVersion, 139 | Timestamp: time.Now().Unix(), 140 | Data: map[string]interface{}{ 141 | "max_threads": threads, 142 | "client_version": ClientVersion, 143 | "protocol_version": ProtocolVersion, 144 | }, 145 | }) 146 | 147 | return nil 148 | } 149 | func (pc *PoolClient) work() { 150 | pc.stopPump = make(chan struct{}) 151 | go pc.writePump() // 消息发送 152 | go pc.heartbeatPump() // 独立心跳保活 153 | pc.readPump() // 消息读取(阻塞) 154 | close(pc.stopPump) 155 | } 156 | 157 | func (pc *PoolClient) heartbeatPump() { 158 | ticker := time.NewTicker(15 * time.Second) 159 | defer ticker.Stop() 160 | 161 | for { 162 | select { 163 | case <-pc.done: 164 | return 165 | case <-pc.stopPump: 166 | return 167 | case <-ticker.C: 168 | // 发送心跳保持连接活跃 169 | pc.writeMu.Lock() 170 | pc.conn.SetWriteDeadline(time.Now().Add(10 * time.Second)) 171 | err := pc.conn.WriteMessage(websocket.PingMessage, nil) 172 | pc.writeMu.Unlock() 173 | if err != nil { 174 | logger.Debug("[PoolClient] 心跳发送失败: %v", err) 175 | return 176 | } 177 | } 178 | } 179 | } 180 | func (pc *PoolClient) writePump() { 181 | // 任务请求间隔60秒 182 | taskTicker := time.NewTicker(60 * time.Second) 183 | defer taskTicker.Stop() 184 | 185 | for { 186 | select { 187 | case <-pc.done: 188 | return 189 | case <-pc.stopPump: 190 | return 191 | case message := <-pc.send: 192 | pc.writeMu.Lock() 193 | pc.conn.SetWriteDeadline(time.Now().Add(30 * time.Second)) 194 | err := pc.conn.WriteMessage(websocket.TextMessage, message) 195 | pc.writeMu.Unlock() 196 | if err != nil { 197 | log.Printf("[PoolClient] 发送消息失败: %v", err) 198 | pc.triggerReconnect() 199 | return 200 | } 201 | case <-taskTicker.C: 202 | // 定期请求任务 203 | pc.sendMessage(WSMessage{ 204 | Type: WSMsgRequestTask, 205 | Timestamp: time.Now().Unix(), 206 | }) 207 | } 208 | } 209 | } 210 | 211 | // readPump 读取消息 212 | func (pc *PoolClient) readPump() { 213 | defer func() { 214 | pc.conn.Close() 215 | pc.triggerReconnect() 216 | }() 217 | 218 | // 延长读取超时到240秒(4分钟),确保不会因为任务执行而断开 219 | pc.conn.SetReadDeadline(time.Now().Add(240 * time.Second)) 220 | pc.conn.SetPongHandler(func(string) error { 221 | pc.conn.SetReadDeadline(time.Now().Add(240 * time.Second)) 222 | return nil 223 | }) 224 | 225 | for { 226 | _, message, err := pc.conn.ReadMessage() 227 | if err != nil { 228 | if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { 229 | log.Printf("[PoolClient] 读取错误: %v", err) 230 | } 231 | return 232 | } 233 | 234 | // 收到消息时重置读取超时 235 | pc.conn.SetReadDeadline(time.Now().Add(240 * time.Second)) 236 | 237 | var msg WSMessage 238 | if err := json.Unmarshal(message, &msg); err != nil { 239 | continue 240 | } 241 | 242 | pc.handleMessage(msg) 243 | } 244 | } 245 | 246 | // handleMessage 处理服务器消息 247 | func (pc *PoolClient) handleMessage(msg WSMessage) { 248 | switch msg.Type { 249 | case WSMsgHeartbeat: 250 | // 立即响应心跳 251 | pc.sendMessage(WSMessage{ 252 | Type: WSMsgHeartbeatAck, 253 | Timestamp: time.Now().Unix(), 254 | }) 255 | 256 | case WSMsgTaskRegister: 257 | // 注册任务(独立心跳线程已保活,无需额外处理) 258 | go pc.handleRegisterTask(msg.Data) 259 | 260 | case WSMsgTaskRefresh: 261 | // 续期任务 262 | go pc.handleRefreshTask(msg.Data) 263 | 264 | case WSMsgStatus: 265 | // 状态同步 266 | logger.Debug("收到状态同步: %v", msg.Data) 267 | } 268 | } 269 | 270 | // handleRegisterTask 处理注册任务(每次只处理1个) 271 | func (pc *PoolClient) handleRegisterTask(data map[string]interface{}) { 272 | // 获取信号量(控制并发数) 273 | pc.taskSem <- struct{}{} 274 | defer func() { <-pc.taskSem }() 275 | 276 | // 生成任务ID用于日志 277 | taskID := time.Now().UnixNano() % 1000 278 | 279 | logger.Info("收到注册任务 [%d]", taskID) 280 | if GetHealthyCount != nil && GetHealthyCount() >= 1 { 281 | // 已有健康代理,直接开始 282 | } else if WaitProxyReady != nil { 283 | if !WaitProxyReady(proxyReadyTimeout) { 284 | logger.Warn("代理未就绪,使用静态代理: %s", ClientProxy) 285 | } 286 | } 287 | 288 | // 获取代理(优先使用代理池) 289 | currentProxy := ClientProxy 290 | if GetClientProxy != nil { 291 | currentProxy = GetClientProxy() 292 | } 293 | logger.Info("[注册 %d] 使用代理: %s", taskID, currentProxy) 294 | result := RunBrowserRegister(ClientHeadless, currentProxy, int(taskID)) 295 | 296 | // 任务完成后释放代理 297 | if ReleaseProxy != nil && currentProxy != "" && currentProxy != ClientProxy { 298 | ReleaseProxy(currentProxy) 299 | } 300 | 301 | if result.Success { 302 | // 上传账号到服务器 303 | if err := pc.uploadAccount(result, true); err != nil { 304 | logger.Error("上传注册结果失败: %v", err) 305 | pc.sendRegisterResult(false, "", err.Error()) 306 | } else { 307 | logger.Info("✅ 注册成功: %s", result.Email) 308 | pc.sendRegisterResult(true, result.Email, "") 309 | } 310 | } else { 311 | errMsg := "未知错误" 312 | if result.Error != nil { 313 | errMsg = result.Error.Error() 314 | } 315 | logger.Warn("❌ 注册失败: %s", errMsg) 316 | pc.sendRegisterResult(false, "", errMsg) 317 | } 318 | } 319 | 320 | // handleRefreshTask 处理续期任务 321 | func (pc *PoolClient) handleRefreshTask(data map[string]interface{}) { 322 | // 获取信号量 323 | pc.taskSem <- struct{}{} 324 | defer func() { <-pc.taskSem }() 325 | 326 | email, _ := data["email"].(string) 327 | if email == "" { 328 | logger.Warn("续期任务缺少email") 329 | return 330 | } 331 | 332 | logger.Info("收到续期任务: %s", email) 333 | 334 | // 检查代理:如果已有健康代理则不等待 335 | if GetHealthyCount != nil && GetHealthyCount() >= 1 { 336 | // 已有健康代理,直接开始 337 | } else if WaitProxyReady != nil { 338 | if !WaitProxyReady(proxyReadyTimeout) { 339 | logger.Warn("代理未就绪,使用静态代理: %s", Proxy) 340 | } 341 | } 342 | 343 | // 构建临时账号对象 344 | acc := &Account{ 345 | Data: AccountData{ 346 | Email: email, 347 | }, 348 | } 349 | 350 | // 从data中提取cookies 351 | if cookiesData, ok := data["cookies"].([]interface{}); ok { 352 | for _, c := range cookiesData { 353 | if cm, ok := c.(map[string]interface{}); ok { 354 | acc.Data.Cookies = append(acc.Data.Cookies, Cookie{ 355 | Name: getString(cm, "name"), 356 | Value: getString(cm, "value"), 357 | Domain: getString(cm, "domain"), 358 | }) 359 | } 360 | } 361 | } 362 | 363 | if auth, ok := data["authorization"].(string); ok { 364 | acc.Data.Authorization = auth 365 | } 366 | if configID, ok := data["config_id"].(string); ok { 367 | acc.ConfigID = configID 368 | } 369 | if csesidx, ok := data["csesidx"].(string); ok { 370 | acc.CSESIDX = csesidx 371 | } 372 | 373 | // 获取代理(优先使用代理池) 374 | currentProxy := Proxy 375 | if GetClientProxy != nil { 376 | currentProxy = GetClientProxy() 377 | } 378 | 379 | // 执行浏览器刷新 380 | result := RefreshCookieWithBrowser(acc, BrowserRefreshHeadless, currentProxy) 381 | 382 | // 任务完成后释放代理 383 | if ReleaseProxy != nil && currentProxy != "" && currentProxy != Proxy { 384 | ReleaseProxy(currentProxy) 385 | } 386 | 387 | if result.Success { 388 | logger.Info("✅ 账号续期成功: %s", email) 389 | 390 | // 使用刷新后的新值(如果有的话) 391 | authorization := acc.Data.Authorization 392 | if result.Authorization != "" { 393 | authorization = result.Authorization 394 | } 395 | configID := acc.ConfigID 396 | if result.ConfigID != "" { 397 | configID = result.ConfigID 398 | } 399 | csesidx := acc.CSESIDX 400 | if result.CSESIDX != "" { 401 | csesidx = result.CSESIDX 402 | } 403 | 404 | // 上传更新后的账号数据到服务器 405 | uploadReq := &AccountUploadRequest{ 406 | Email: email, 407 | Cookies: result.SecureCookies, 408 | Authorization: authorization, 409 | ConfigID: configID, 410 | CSESIDX: csesidx, 411 | IsNew: false, 412 | } 413 | logger.Info("[%s] 上传续期数据: configID=%s, csesidx=%s, auth长度=%d", 414 | email, configID, csesidx, len(authorization)) 415 | if err := pc.uploadAccountData(uploadReq); err != nil { 416 | logger.Warn("上传续期数据失败: %v", err) 417 | } 418 | pc.sendRefreshResult(email, true, result.SecureCookies, "") 419 | } else { 420 | errMsg := "未知错误" 421 | if result.Error != nil { 422 | errMsg = result.Error.Error() 423 | } 424 | logger.Warn("❌ 账号续期失败 %s: %s", email, errMsg) 425 | pc.sendRefreshResult(email, false, nil, errMsg) 426 | } 427 | } 428 | 429 | // sendMessage 发送消息 430 | func (pc *PoolClient) sendMessage(msg WSMessage) { 431 | data, err := json.Marshal(msg) 432 | if err != nil { 433 | return 434 | } 435 | select { 436 | case pc.send <- data: 437 | default: 438 | logger.Warn("发送队列已满") 439 | } 440 | } 441 | 442 | // uploadAccount 上传注册结果到服务器 443 | func (pc *PoolClient) uploadAccount(result *BrowserRegisterResult, isNew bool) error { 444 | // 构建cookie字符串 445 | var cookieStr string 446 | for i, c := range result.SecureCookies { 447 | if i > 0 { 448 | cookieStr += "; " 449 | } 450 | cookieStr += c.Name + "=" + c.Value 451 | } 452 | 453 | req := &AccountUploadRequest{ 454 | Email: result.Email, 455 | FullName: result.FullName, 456 | Cookies: result.SecureCookies, 457 | CookieString: cookieStr, 458 | Authorization: result.Authorization, 459 | ConfigID: result.ConfigID, 460 | CSESIDX: result.CSESIDX, 461 | IsNew: isNew, 462 | } 463 | return pc.uploadAccountData(req) 464 | } 465 | 466 | // uploadAccountData 上传账号数据到服务器(带重试) 467 | func (pc *PoolClient) uploadAccountData(req *AccountUploadRequest) error { 468 | u, err := url.Parse(pc.config.ServerAddr) 469 | if err != nil { 470 | return err 471 | } 472 | 473 | uploadURL := fmt.Sprintf("%s://%s/pool/upload-account", u.Scheme, u.Host) 474 | 475 | data, err := json.Marshal(req) 476 | if err != nil { 477 | return err 478 | } 479 | 480 | maxRetries := 3 481 | var lastErr error 482 | 483 | for i := 0; i < maxRetries; i++ { 484 | if i > 0 { 485 | logger.Info("[%s] 上传重试 %d/%d...", req.Email, i+1, maxRetries) 486 | time.Sleep(time.Duration(i*2) * time.Second) 487 | } 488 | 489 | httpReq, err := http.NewRequest("POST", uploadURL, bytes.NewReader(data)) 490 | if err != nil { 491 | lastErr = err 492 | continue 493 | } 494 | 495 | httpReq.Header.Set("Content-Type", "application/json") 496 | if pc.config.Secret != "" { 497 | httpReq.Header.Set("X-Pool-Secret", pc.config.Secret) 498 | } 499 | 500 | client := &http.Client{Timeout: 60 * time.Second} 501 | resp, err := client.Do(httpReq) 502 | if err != nil { 503 | lastErr = err 504 | continue 505 | } 506 | 507 | var result map[string]interface{} 508 | if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { 509 | resp.Body.Close() 510 | lastErr = err 511 | continue 512 | } 513 | resp.Body.Close() 514 | 515 | if success, ok := result["success"].(bool); !ok || !success { 516 | errMsg, _ := result["error"].(string) 517 | lastErr = fmt.Errorf("上传失败: %s", errMsg) 518 | continue 519 | } 520 | 521 | logger.Debug("账号数据已上传: %s", req.Email) 522 | return nil 523 | } 524 | 525 | return fmt.Errorf("上传失败(重试%d次): %v", maxRetries, lastErr) 526 | } 527 | 528 | // sendRegisterResult 发送注册结果 529 | func (pc *PoolClient) sendRegisterResult(success bool, email, errMsg string) { 530 | pc.sendMessage(WSMessage{ 531 | Type: WSMsgRegisterResult, 532 | Timestamp: time.Now().Unix(), 533 | Data: map[string]interface{}{ 534 | "success": success, 535 | "email": email, 536 | "error": errMsg, 537 | }, 538 | }) 539 | } 540 | 541 | // sendRefreshResult 发送续期结果 542 | func (pc *PoolClient) sendRefreshResult(email string, success bool, cookies []Cookie, errMsg string) { 543 | pc.sendMessage(WSMessage{ 544 | Type: WSMsgRefreshResult, 545 | Timestamp: time.Now().Unix(), 546 | Data: map[string]interface{}{ 547 | "email": email, 548 | "success": success, 549 | "cookies": cookies, 550 | "error": errMsg, 551 | }, 552 | }) 553 | } 554 | 555 | // triggerReconnect 触发重连 556 | func (pc *PoolClient) triggerReconnect() { 557 | select { 558 | case pc.reconnect <- struct{}{}: 559 | default: 560 | } 561 | } 562 | 563 | // getString 安全获取字符串 564 | func getString(m map[string]interface{}, key string) string { 565 | if v, ok := m[key].(string); ok { 566 | return v 567 | } 568 | return "" 569 | } 570 | -------------------------------------------------------------------------------- /src/flow/flow_client.go: -------------------------------------------------------------------------------- 1 | // Package flow implements VideoFX (Veo) API client for image/video generation 2 | package flow 3 | 4 | import ( 5 | "bytes" 6 | "encoding/base64" 7 | "encoding/json" 8 | "fmt" 9 | "io" 10 | "math/rand" 11 | "net/http" 12 | "strings" 13 | "sync" 14 | "time" 15 | 16 | "github.com/google/uuid" 17 | ) 18 | 19 | const ( 20 | DefaultLabsBaseURL = "https://labs.google/fx/api" 21 | DefaultAPIBaseURL = "https://aisandbox-pa.googleapis.com/v1" 22 | DefaultTimeout = 120 23 | DefaultPollInterval = 3 24 | DefaultMaxPollAttempts = 500 25 | ) 26 | 27 | // FlowConfig Flow 服务配置 28 | type FlowConfig struct { 29 | LabsBaseURL string `json:"labs_base_url"` 30 | APIBaseURL string `json:"api_base_url"` 31 | Timeout int `json:"timeout"` 32 | PollInterval int `json:"poll_interval"` 33 | MaxPollAttempts int `json:"max_poll_attempts"` 34 | Proxy string `json:"proxy"` 35 | } 36 | 37 | // FlowToken Flow Token (ST/AT) 38 | type FlowToken struct { 39 | ID string `json:"id"` 40 | ST string `json:"st"` // Session Token 41 | AT string `json:"at"` // Access Token 42 | ATExpires time.Time `json:"at_expires"` // AT 过期时间 43 | Email string `json:"email"` 44 | ProjectID string `json:"project_id"` 45 | Credits int `json:"credits"` 46 | UserPaygateTier string `json:"user_paygate_tier"` 47 | Disabled bool `json:"disabled"` 48 | LastUsed time.Time `json:"last_used"` 49 | ErrorCount int `json:"error_count"` 50 | mu sync.RWMutex 51 | } 52 | 53 | // FlowClient VideoFX API 客户端 54 | type FlowClient struct { 55 | config FlowConfig 56 | httpClient *http.Client 57 | tokens map[string]*FlowToken 58 | tokensMu sync.RWMutex 59 | } 60 | 61 | // NewFlowClient 创建新的 Flow 客户端 62 | func NewFlowClient(config FlowConfig) *FlowClient { 63 | if config.LabsBaseURL == "" { 64 | config.LabsBaseURL = DefaultLabsBaseURL 65 | } 66 | if config.APIBaseURL == "" { 67 | config.APIBaseURL = DefaultAPIBaseURL 68 | } 69 | if config.Timeout == 0 { 70 | config.Timeout = DefaultTimeout 71 | } 72 | if config.PollInterval == 0 { 73 | config.PollInterval = DefaultPollInterval 74 | } 75 | if config.MaxPollAttempts == 0 { 76 | config.MaxPollAttempts = DefaultMaxPollAttempts 77 | } 78 | 79 | return &FlowClient{ 80 | config: config, 81 | httpClient: &http.Client{ 82 | Timeout: time.Duration(config.Timeout) * time.Second, 83 | }, 84 | tokens: make(map[string]*FlowToken), 85 | } 86 | } 87 | 88 | // AddToken 添加 Token 89 | func (fc *FlowClient) AddToken(token *FlowToken) { 90 | fc.tokensMu.Lock() 91 | defer fc.tokensMu.Unlock() 92 | fc.tokens[token.ID] = token 93 | } 94 | 95 | // GetToken 获取 Token 96 | func (fc *FlowClient) GetToken(id string) *FlowToken { 97 | fc.tokensMu.RLock() 98 | defer fc.tokensMu.RUnlock() 99 | return fc.tokens[id] 100 | } 101 | 102 | // SelectToken 选择可用 Token 103 | func (fc *FlowClient) SelectToken() *FlowToken { 104 | fc.tokensMu.RLock() 105 | defer fc.tokensMu.RUnlock() 106 | 107 | var best *FlowToken 108 | for _, t := range fc.tokens { 109 | if t.Disabled || t.ErrorCount >= 3 { 110 | continue 111 | } 112 | if best == nil || t.LastUsed.Before(best.LastUsed) { 113 | best = t 114 | } 115 | } 116 | return best 117 | } 118 | 119 | // makeRequest 发送 HTTP 请求 120 | func (fc *FlowClient) makeRequest(method, url string, headers map[string]string, body interface{}) (map[string]interface{}, error) { 121 | var reqBody io.Reader 122 | if body != nil { 123 | data, err := json.Marshal(body) 124 | if err != nil { 125 | return nil, fmt.Errorf("marshal request body: %w", err) 126 | } 127 | reqBody = bytes.NewReader(data) 128 | } 129 | 130 | req, err := http.NewRequest(method, url, reqBody) 131 | if err != nil { 132 | return nil, fmt.Errorf("create request: %w", err) 133 | } 134 | 135 | req.Header.Set("Content-Type", "application/json") 136 | req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") 137 | for k, v := range headers { 138 | req.Header.Set(k, v) 139 | } 140 | 141 | resp, err := fc.httpClient.Do(req) 142 | if err != nil { 143 | return nil, fmt.Errorf("do request: %w", err) 144 | } 145 | defer resp.Body.Close() 146 | 147 | respBody, err := io.ReadAll(resp.Body) 148 | if err != nil { 149 | return nil, fmt.Errorf("read response: %w", err) 150 | } 151 | 152 | if resp.StatusCode >= 400 { 153 | return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(respBody)) 154 | } 155 | 156 | var result map[string]interface{} 157 | if err := json.Unmarshal(respBody, &result); err != nil { 158 | return nil, fmt.Errorf("unmarshal response: %w", err) 159 | } 160 | 161 | return result, nil 162 | } 163 | 164 | // generateSessionID 生成 sessionId 165 | func (fc *FlowClient) generateSessionID() string { 166 | return fmt.Sprintf(";%d", time.Now().UnixMilli()) 167 | } 168 | 169 | // ==================== 认证相关 (使用ST) ==================== 170 | 171 | // STToAT ST 转 AT 172 | func (fc *FlowClient) STToAT(st string) (*STToATResponse, error) { 173 | url := fmt.Sprintf("%s/auth/session", fc.config.LabsBaseURL) 174 | headers := map[string]string{ 175 | "Cookie": fmt.Sprintf("__Secure-next-auth.session-token=%s", st), 176 | } 177 | 178 | result, err := fc.makeRequest("GET", url, headers, nil) 179 | if err != nil { 180 | return nil, err 181 | } 182 | 183 | resp := &STToATResponse{} 184 | if at, ok := result["access_token"].(string); ok { 185 | resp.AccessToken = at 186 | } 187 | if expires, ok := result["expires"].(string); ok { 188 | resp.Expires = expires 189 | } 190 | if user, ok := result["user"].(map[string]interface{}); ok { 191 | if email, ok := user["email"].(string); ok { 192 | resp.Email = email 193 | } 194 | } 195 | 196 | return resp, nil 197 | } 198 | 199 | type STToATResponse struct { 200 | AccessToken string `json:"access_token"` 201 | Expires string `json:"expires"` 202 | Email string `json:"email"` 203 | } 204 | 205 | // ==================== 项目管理 (使用ST) ==================== 206 | 207 | // CreateProject 创建项目 208 | func (fc *FlowClient) CreateProject(st, title string) (string, error) { 209 | url := fmt.Sprintf("%s/trpc/project.createProject", fc.config.LabsBaseURL) 210 | headers := map[string]string{ 211 | "Cookie": fmt.Sprintf("__Secure-next-auth.session-token=%s", st), 212 | } 213 | body := map[string]interface{}{ 214 | "json": map[string]interface{}{ 215 | "projectTitle": title, 216 | "toolName": "PINHOLE", 217 | }, 218 | } 219 | 220 | result, err := fc.makeRequest("POST", url, headers, body) 221 | if err != nil { 222 | return "", err 223 | } 224 | 225 | // 解析 project_id 226 | if res, ok := result["result"].(map[string]interface{}); ok { 227 | if data, ok := res["data"].(map[string]interface{}); ok { 228 | if jsonData, ok := data["json"].(map[string]interface{}); ok { 229 | if innerRes, ok := jsonData["result"].(map[string]interface{}); ok { 230 | if projectID, ok := innerRes["projectId"].(string); ok { 231 | return projectID, nil 232 | } 233 | } 234 | } 235 | } 236 | } 237 | 238 | return "", fmt.Errorf("failed to parse project_id from response") 239 | } 240 | 241 | // DeleteProject 删除项目 242 | func (fc *FlowClient) DeleteProject(st, projectID string) error { 243 | url := fmt.Sprintf("%s/trpc/project.deleteProject", fc.config.LabsBaseURL) 244 | headers := map[string]string{ 245 | "Cookie": fmt.Sprintf("__Secure-next-auth.session-token=%s", st), 246 | } 247 | body := map[string]interface{}{ 248 | "json": map[string]interface{}{ 249 | "projectToDeleteId": projectID, 250 | }, 251 | } 252 | 253 | _, err := fc.makeRequest("POST", url, headers, body) 254 | return err 255 | } 256 | 257 | // ==================== 余额查询 (使用AT) ==================== 258 | 259 | // GetCredits 查询余额 260 | func (fc *FlowClient) GetCredits(at string) (*CreditsResponse, error) { 261 | url := fmt.Sprintf("%s/credits", fc.config.APIBaseURL) 262 | headers := map[string]string{ 263 | "authorization": "Bearer " + at, 264 | } 265 | 266 | result, err := fc.makeRequest("GET", url, headers, nil) 267 | if err != nil { 268 | return nil, err 269 | } 270 | 271 | resp := &CreditsResponse{} 272 | if credits, ok := result["credits"].(float64); ok { 273 | resp.Credits = int(credits) 274 | } 275 | if tier, ok := result["userPaygateTier"].(string); ok { 276 | resp.UserPaygateTier = tier 277 | } 278 | 279 | return resp, nil 280 | } 281 | 282 | type CreditsResponse struct { 283 | Credits int `json:"credits"` 284 | UserPaygateTier string `json:"userPaygateTier"` 285 | } 286 | 287 | // ==================== 图片上传 (使用AT) ==================== 288 | 289 | // UploadImage 上传图片 290 | func (fc *FlowClient) UploadImage(at string, imageBytes []byte, aspectRatio string) (string, error) { 291 | // 转换视频 aspect_ratio 为图片 aspect_ratio 292 | if strings.HasPrefix(aspectRatio, "VIDEO_") { 293 | aspectRatio = strings.Replace(aspectRatio, "VIDEO_", "IMAGE_", 1) 294 | } 295 | 296 | imageBase64 := base64.StdEncoding.EncodeToString(imageBytes) 297 | 298 | url := fmt.Sprintf("%s:uploadUserImage", fc.config.APIBaseURL) 299 | headers := map[string]string{ 300 | "authorization": "Bearer " + at, 301 | } 302 | body := map[string]interface{}{ 303 | "imageInput": map[string]interface{}{ 304 | "rawImageBytes": imageBase64, 305 | "mimeType": "image/jpeg", 306 | "isUserUploaded": true, 307 | "aspectRatio": aspectRatio, 308 | }, 309 | "clientContext": map[string]interface{}{ 310 | "sessionId": fc.generateSessionID(), 311 | "tool": "ASSET_MANAGER", 312 | }, 313 | } 314 | 315 | result, err := fc.makeRequest("POST", url, headers, body) 316 | if err != nil { 317 | return "", err 318 | } 319 | 320 | // 解析 mediaGenerationId 321 | if mediaGen, ok := result["mediaGenerationId"].(map[string]interface{}); ok { 322 | if mediaID, ok := mediaGen["mediaGenerationId"].(string); ok { 323 | return mediaID, nil 324 | } 325 | } 326 | 327 | return "", fmt.Errorf("failed to parse mediaGenerationId") 328 | } 329 | 330 | // ==================== 图片生成 (使用AT) ==================== 331 | 332 | // GenerateImage 生成图片 333 | func (fc *FlowClient) GenerateImage(at, projectID, prompt, modelName, aspectRatio string, imageInputs []map[string]interface{}) (*GenerateImageResponse, error) { 334 | url := fmt.Sprintf("%s/projects/%s/flowMedia:batchGenerateImages", fc.config.APIBaseURL, projectID) 335 | headers := map[string]string{ 336 | "authorization": "Bearer " + at, 337 | } 338 | 339 | requestData := map[string]interface{}{ 340 | "clientContext": map[string]interface{}{ 341 | "sessionId": fc.generateSessionID(), 342 | }, 343 | "seed": rand.Intn(99999) + 1, 344 | "imageModelName": modelName, 345 | "imageAspectRatio": aspectRatio, 346 | "prompt": prompt, 347 | "imageInputs": imageInputs, 348 | } 349 | 350 | body := map[string]interface{}{ 351 | "requests": []map[string]interface{}{requestData}, 352 | } 353 | 354 | result, err := fc.makeRequest("POST", url, headers, body) 355 | if err != nil { 356 | return nil, err 357 | } 358 | 359 | resp := &GenerateImageResponse{} 360 | if media, ok := result["media"].([]interface{}); ok && len(media) > 0 { 361 | if m, ok := media[0].(map[string]interface{}); ok { 362 | if img, ok := m["image"].(map[string]interface{}); ok { 363 | if genImg, ok := img["generatedImage"].(map[string]interface{}); ok { 364 | if fifeURL, ok := genImg["fifeUrl"].(string); ok { 365 | resp.ImageURL = fifeURL 366 | } 367 | } 368 | } 369 | } 370 | } 371 | 372 | return resp, nil 373 | } 374 | 375 | type GenerateImageResponse struct { 376 | ImageURL string `json:"image_url"` 377 | } 378 | 379 | // ==================== 视频生成 (使用AT) ==================== 380 | 381 | // GenerateVideoText 文生视频 382 | func (fc *FlowClient) GenerateVideoText(at, projectID, prompt, modelKey, aspectRatio, userPaygateTier string) (*GenerateVideoResponse, error) { 383 | url := fmt.Sprintf("%s/video:batchAsyncGenerateVideoText", fc.config.APIBaseURL) 384 | headers := map[string]string{ 385 | "authorization": "Bearer " + at, 386 | } 387 | 388 | sceneID := uuid.New().String() 389 | body := map[string]interface{}{ 390 | "clientContext": map[string]interface{}{ 391 | "sessionId": fc.generateSessionID(), 392 | "projectId": projectID, 393 | "tool": "PINHOLE", 394 | "userPaygateTier": userPaygateTier, 395 | }, 396 | "requests": []map[string]interface{}{{ 397 | "aspectRatio": aspectRatio, 398 | "seed": rand.Intn(99999) + 1, 399 | "textInput": map[string]interface{}{ 400 | "prompt": prompt, 401 | }, 402 | "videoModelKey": modelKey, 403 | "metadata": map[string]interface{}{ 404 | "sceneId": sceneID, 405 | }, 406 | }}, 407 | } 408 | 409 | return fc.parseVideoResponse(fc.makeRequest("POST", url, headers, body)) 410 | } 411 | 412 | // GenerateVideoStartEnd 首尾帧生成视频 413 | func (fc *FlowClient) GenerateVideoStartEnd(at, projectID, prompt, modelKey, aspectRatio, startMediaID, endMediaID, userPaygateTier string) (*GenerateVideoResponse, error) { 414 | url := fmt.Sprintf("%s/video:batchAsyncGenerateVideoStartAndEndImage", fc.config.APIBaseURL) 415 | headers := map[string]string{ 416 | "authorization": "Bearer " + at, 417 | } 418 | 419 | sceneID := uuid.New().String() 420 | request := map[string]interface{}{ 421 | "aspectRatio": aspectRatio, 422 | "seed": rand.Intn(99999) + 1, 423 | "textInput": map[string]interface{}{ 424 | "prompt": prompt, 425 | }, 426 | "videoModelKey": modelKey, 427 | "startImage": map[string]interface{}{ 428 | "mediaId": startMediaID, 429 | }, 430 | "metadata": map[string]interface{}{ 431 | "sceneId": sceneID, 432 | }, 433 | } 434 | 435 | // 如果有尾帧 436 | if endMediaID != "" { 437 | request["endImage"] = map[string]interface{}{ 438 | "mediaId": endMediaID, 439 | } 440 | } 441 | 442 | body := map[string]interface{}{ 443 | "clientContext": map[string]interface{}{ 444 | "sessionId": fc.generateSessionID(), 445 | "projectId": projectID, 446 | "tool": "PINHOLE", 447 | "userPaygateTier": userPaygateTier, 448 | }, 449 | "requests": []map[string]interface{}{request}, 450 | } 451 | 452 | return fc.parseVideoResponse(fc.makeRequest("POST", url, headers, body)) 453 | } 454 | 455 | // GenerateVideoReferenceImages 多图生成视频 456 | func (fc *FlowClient) GenerateVideoReferenceImages(at, projectID, prompt, modelKey, aspectRatio string, referenceImages []map[string]interface{}, userPaygateTier string) (*GenerateVideoResponse, error) { 457 | url := fmt.Sprintf("%s/video:batchAsyncGenerateVideoReferenceImages", fc.config.APIBaseURL) 458 | headers := map[string]string{ 459 | "authorization": "Bearer " + at, 460 | } 461 | 462 | sceneID := uuid.New().String() 463 | body := map[string]interface{}{ 464 | "clientContext": map[string]interface{}{ 465 | "sessionId": fc.generateSessionID(), 466 | "projectId": projectID, 467 | "tool": "PINHOLE", 468 | "userPaygateTier": userPaygateTier, 469 | }, 470 | "requests": []map[string]interface{}{{ 471 | "aspectRatio": aspectRatio, 472 | "seed": rand.Intn(99999) + 1, 473 | "textInput": map[string]interface{}{ 474 | "prompt": prompt, 475 | }, 476 | "videoModelKey": modelKey, 477 | "referenceImages": referenceImages, 478 | "metadata": map[string]interface{}{ 479 | "sceneId": sceneID, 480 | }, 481 | }}, 482 | } 483 | 484 | return fc.parseVideoResponse(fc.makeRequest("POST", url, headers, body)) 485 | } 486 | 487 | func (fc *FlowClient) parseVideoResponse(result map[string]interface{}, err error) (*GenerateVideoResponse, error) { 488 | if err != nil { 489 | return nil, err 490 | } 491 | 492 | resp := &GenerateVideoResponse{} 493 | if ops, ok := result["operations"].([]interface{}); ok && len(ops) > 0 { 494 | if op, ok := ops[0].(map[string]interface{}); ok { 495 | if operation, ok := op["operation"].(map[string]interface{}); ok { 496 | if name, ok := operation["name"].(string); ok { 497 | resp.TaskID = name 498 | } 499 | } 500 | if sceneID, ok := op["sceneId"].(string); ok { 501 | resp.SceneID = sceneID 502 | } 503 | if status, ok := op["status"].(string); ok { 504 | resp.Status = status 505 | } 506 | } 507 | } 508 | if credits, ok := result["remainingCredits"].(float64); ok { 509 | resp.RemainingCredits = int(credits) 510 | } 511 | 512 | return resp, nil 513 | } 514 | 515 | type GenerateVideoResponse struct { 516 | TaskID string `json:"task_id"` 517 | SceneID string `json:"scene_id"` 518 | Status string `json:"status"` 519 | RemainingCredits int `json:"remaining_credits"` 520 | } 521 | 522 | // ==================== 任务轮询 (使用AT) ==================== 523 | 524 | // CheckVideoStatus 查询视频生成状态 525 | func (fc *FlowClient) CheckVideoStatus(at string, operations []map[string]interface{}) (*VideoStatusResponse, error) { 526 | url := fmt.Sprintf("%s/video:batchCheckAsyncVideoGenerationStatus", fc.config.APIBaseURL) 527 | headers := map[string]string{ 528 | "authorization": "Bearer " + at, 529 | } 530 | body := map[string]interface{}{ 531 | "operations": operations, 532 | } 533 | 534 | result, err := fc.makeRequest("POST", url, headers, body) 535 | if err != nil { 536 | return nil, err 537 | } 538 | 539 | resp := &VideoStatusResponse{} 540 | if ops, ok := result["operations"].([]interface{}); ok && len(ops) > 0 { 541 | if op, ok := ops[0].(map[string]interface{}); ok { 542 | if status, ok := op["status"].(string); ok { 543 | resp.Status = status 544 | } 545 | if operation, ok := op["operation"].(map[string]interface{}); ok { 546 | if name, ok := operation["name"].(string); ok { 547 | resp.TaskID = name 548 | } 549 | if metadata, ok := operation["metadata"].(map[string]interface{}); ok { 550 | if video, ok := metadata["video"].(map[string]interface{}); ok { 551 | if fifeURL, ok := video["fifeUrl"].(string); ok { 552 | resp.VideoURL = fifeURL 553 | } 554 | } 555 | } 556 | } 557 | } 558 | } 559 | 560 | return resp, nil 561 | } 562 | 563 | type VideoStatusResponse struct { 564 | TaskID string `json:"task_id"` 565 | Status string `json:"status"` 566 | VideoURL string `json:"video_url"` 567 | } 568 | 569 | // PollVideoResult 轮询视频生成结果 570 | func (fc *FlowClient) PollVideoResult(at, taskID, sceneID string) (string, error) { 571 | operations := []map[string]interface{}{{ 572 | "operation": map[string]interface{}{ 573 | "name": taskID, 574 | }, 575 | "sceneId": sceneID, 576 | }} 577 | 578 | for i := 0; i < fc.config.MaxPollAttempts; i++ { 579 | time.Sleep(time.Duration(fc.config.PollInterval) * time.Second) 580 | 581 | resp, err := fc.CheckVideoStatus(at, operations) 582 | if err != nil { 583 | continue 584 | } 585 | 586 | switch resp.Status { 587 | case "MEDIA_GENERATION_STATUS_SUCCESSFUL": 588 | if resp.VideoURL != "" { 589 | return resp.VideoURL, nil 590 | } 591 | case "MEDIA_GENERATION_STATUS_ERROR_UNKNOWN", 592 | "MEDIA_GENERATION_STATUS_ERROR_NSFW", 593 | "MEDIA_GENERATION_STATUS_ERROR_PERSON", 594 | "MEDIA_GENERATION_STATUS_ERROR_SAFETY": 595 | return "", fmt.Errorf("video generation failed: %s", resp.Status) 596 | } 597 | } 598 | 599 | return "", fmt.Errorf("video generation timeout after %d attempts", fc.config.MaxPollAttempts) 600 | } 601 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Business2API 2 | 3 | > 🚀 OpenAI/Gemini 兼容的 Gemini Business API 代理服务,支持账号池管理、自动注册和 Flow 图片/视频生成。 4 | 5 | [![Build](https://github.com/XxxXTeam/business2api/actions/workflows/build.yml/badge.svg)](https://github.com/XxxXTeam/business2api/actions/workflows/build.yml) 6 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) 7 | [![Go Version](https://img.shields.io/badge/Go-1.24+-00ADD8?logo=go)](https://golang.org) 8 | 9 | ## ✨ 功能特性 10 | 11 | | 功能 | 描述 | 12 | |------|------| 13 | | 🔌 **多 API 兼容** | OpenAI (`/v1/chat/completions`)、Gemini (`/v1beta/models`)、Claude (`/v1/messages`) | 14 | | 🏊 **智能账号池** | 自动轮询、刷新、冷却管理、401/403 自动换号 | 15 | | 🌊 **流式响应** | SSE 流式输出,支持 `stream: true` | 16 | | 🎨 **多模态** | 图片/视频输入、原生图片生成(`-image` 后缀)| 17 | | 🤖 **自动注册** | 浏览器自动化注册,支持 Windows/Linux/macOS | 18 | | 🌐 **代理池** | HTTP/SOCKS5 代理,订阅链接,健康检查 | 19 | | 📊 **遥测监控** | IP 请求统计、Token 使用量、RPM 监控 | 20 | | 🔄 **热重载** | 配置文件自动监听,无需重启 | 21 | 22 | ## 📦 支持的模型 23 | 24 | ### Gemini Business 模型 25 | 26 | | 模型 | 文本 | 图片生成 | 视频生成 | 搜索 | 27 | |------|:----:|:--------:|:--------:|:----:| 28 | | gemini-2.5-flash | ✅ | ✅ | ✅ | ✅ | 29 | | gemini-2.5-pro | ✅ | ✅ | ✅ | ✅ | 30 | | gemini-2.5-flash-preview-latest | ✅ | ✅ | ✅ | ✅ | 31 | | gemini-3-pro-preview | ✅ | ✅ | ✅ | ✅ | 32 | | gemini-3-pro | ✅ | ✅ | ✅ | ✅ | 33 | | gemini-3-flash-preview | ✅ | ✅ | ✅ | ✅ | 34 | | gemini-3-flash | ✅ | ✅ | ✅ | ✅ | 35 | 36 | ### 功能后缀 37 | 38 | 支持单个或混合后缀启用指定功能: 39 | 40 | | 后缀 | 功能 | 示例 | 41 | |------|------|------| 42 | | `-image` | 图片生成 | `gemini-2.5-flash-image` | 43 | | `-video` | 视频生成 | `gemini-2.5-flash-video` | 44 | | `-search` | 联网搜索 | `gemini-2.5-flash-search` | 45 | | 混合后缀 | 同时启用多功能 | `gemini-2.5-flash-image-search` | 46 | 47 | **说明:** 48 | - 无后缀:启用所有功能(图片/视频/搜索/工具) 49 | - 有后缀:只启用指定功能,支持任意组合如 `-image-search`、`-video-search` 50 | 51 | ### ⚠️ 限制说明 52 | 53 | | 限制 | 说明 | 54 | |------|------| 55 | | **不支持自定义工具** | Function Calling / Tools 参数会被忽略,仅支持内置工具(图片/视频生成、搜索) | 56 | | **上下文拼接实现** | 多轮对话通过拼接 `messages` 为单次请求实现,非原生会话管理 | 57 | | **无状态** | 每次请求独立,不保留会话状态,历史消息需客户端自行维护 | 58 | 59 | --- 60 | 61 | 62 | >> 公益 Demo(免费调用) 63 | > 🔗 链接: 64 | > 65 | > > API Key 获取请访问 https://business2api.openel.top/auth 获取个人专属免费APIKEY 66 | 67 | 68 | >> GLM 公益测试 API 69 | > 🔗 链接: 70 | > 71 | > XiaoMi 网页逆向公益 API 72 | > 🔗 链接:[https://xiaomi.openel.top](https://xiaomi.openel.top/) 73 | > 74 | > API Key : `sk-3d2f9b84e7f510b1a08f7b3d6c9a6a7f17fbbad5624ea29f22d9c742bf39c863` 75 | 76 | 77 | 78 | ## 快速开始 79 | 80 | ### 方式一:Docker 部署(推荐) 81 | 82 | #### 1. 使用 Docker Compose 83 | 84 | ```bash 85 | # 创建目录 86 | mkdir business2api && cd business2api 87 | 88 | # 下载必要文件 89 | wget https://raw.githubusercontent.com/XxxXTeam/business2api/master/docker/docker-compose.yml 90 | wget https://raw.githubusercontent.com/XxxXTeam/business2api/master/config/config.json.example -O config.json 91 | 92 | # 编辑配置 93 | vim config.json 94 | 95 | # 创建数据目录 96 | mkdir data 97 | 98 | # 启动服务 99 | docker compose up -d 100 | ``` 101 | 102 | #### 2. 使用 Docker Run 103 | 104 | ```bash 105 | # 拉取镜像 106 | docker pull ghcr.io/xxxteam/business2api:latest 107 | 108 | # 创建配置文件 109 | wget https://raw.githubusercontent.com/XxxXTeam/business2api/master/config/config.json.example -O config.json 110 | 111 | # 运行容器 112 | docker run -d \ 113 | --name business2api \ 114 | -p 8000:8000 \ 115 | -v $(pwd)/data:/app/data \ 116 | -v $(pwd)/config.json:/app/config/config.json:ro \ 117 | ghcr.io/xxxteam/business2api:latest 118 | ``` 119 | 120 | ### 方式二:二进制部署 121 | 122 | #### 1. 下载预编译版本 123 | 124 | 从 [Releases](https://github.com/XxxXTeam/business2api/releases) 下载对应平台的二进制文件。 125 | 126 | ```bash 127 | # Linux amd64 128 | wget https://github.com/XxxXTeam/business2api/releases/latest/download/business2api-linux-amd64.tar.gz 129 | tar -xzf business2api-linux-amd64.tar.gz 130 | chmod +x business2api-linux-amd64 131 | ``` 132 | 133 | #### 2. 从源码编译 134 | 135 | ```bash 136 | # 需要 Go 1.24+ 137 | git clone https://github.com/XxxXTeam/business2api.git 138 | cd business2api 139 | 140 | # 编译 141 | go build -o business2api . 142 | 143 | # 运行 144 | ./business2api 145 | ``` 146 | 147 | ### 方式三:使用 Systemd 服务 148 | 149 | ```bash 150 | # 创建服务文件 151 | sudo tee /etc/systemd/system/business2api.service << EOF 152 | [Unit] 153 | Description=Gemini Gateway Service 154 | After=network.target 155 | 156 | [Service] 157 | Type=simple 158 | User=nobody 159 | WorkingDirectory=/opt/business2api 160 | ExecStart=/opt/business2api/business2api 161 | Restart=always 162 | RestartSec=5 163 | Environment=LISTEN_ADDR=:8000 164 | Environment=DATA_DIR=/opt/business2api/data 165 | 166 | [Install] 167 | WantedBy=multi-user.target 168 | EOF 169 | 170 | # 启动服务 171 | sudo systemctl daemon-reload 172 | sudo systemctl enable business2api 173 | sudo systemctl start business2api 174 | ``` 175 | 176 | --- 177 | 178 | ## 配置说明 179 | 180 | ### config.json 181 | 182 | ```json 183 | { 184 | "api_keys": ["sk-your-api-key"], // API 密钥列表,用于鉴权 185 | "listen_addr": ":8000", // 监听地址 186 | "data_dir": "./data", // 账号数据目录 187 | "default_config": "", // 默认 configId(可选) 188 | "debug": false, // 调试模式(输出详细日志) 189 | 190 | "pool": { 191 | "target_count": 50, // 目标账号数量 192 | "min_count": 10, // 最小账号数,低于此值触发注册 193 | "check_interval_minutes": 30, // 检查间隔(分钟) 194 | "register_threads": 1, // 本地注册线程数 195 | "register_headless": true, // 无头模式注册 196 | "refresh_on_startup": true, // 启动时刷新账号 197 | "refresh_cooldown_sec": 240, // 刷新冷却时间(秒) 198 | "use_cooldown_sec": 15, // 使用冷却时间(秒) 199 | "max_fail_count": 3, // 最大连续失败次数 200 | "enable_browser_refresh": true, // 启用浏览器刷新401账号 201 | "browser_refresh_headless": true, // 浏览器刷新无头模式 202 | "browser_refresh_max_retry": 1, // 浏览器刷新最大重试次数 203 | "auto_delete_401": false // 401时自动删除账号 204 | }, 205 | 206 | "pool_server": { 207 | "enable": false, // 是否启用分离模式 208 | "mode": "local", // 运行模式:local/server/client 209 | "server_addr": "http://ip:8000", // 服务器地址(client模式) 210 | "listen_addr": ":8000", // 监听地址(server模式) 211 | "secret": "your-secret-key", // 通信密钥 212 | "target_count": 50, // 目标账号数(server模式) 213 | "client_threads": 2, // 客户端并发线程数 214 | "data_dir": "./data", // 数据目录(server模式) 215 | "expired_action": "delete" // 过期账号处理:delete/refresh/queue 216 | }, 217 | 218 | "proxy_pool": { 219 | "subscribes": [], // 代理订阅链接列表 220 | "files": [], // 本地代理文件列表 221 | "health_check": true, // 启用健康检查 222 | "check_on_startup": true // 启动时检查 223 | } 224 | } 225 | ``` 226 | 227 | ### 多 API Key 支持 228 | 229 | 支持配置多个 API Key,所有 Key 都可以用于鉴权: 230 | 231 | ```json 232 | { 233 | "api_keys": [ 234 | "sk-key-1", 235 | "sk-key-2", 236 | "sk-key-3" 237 | ] 238 | } 239 | ``` 240 | 241 | ### 配置热重载 242 | 243 | 服务运行时自动监听 `config/config.json` 文件变更,无需重启即可生效。 244 | 245 | **可热重载的配置项:** 246 | 247 | | 配置项 | 说明 | 248 | |----------|------| 249 | | `api_keys` | API 密钥列表 | 250 | | `debug` | 调试模式 | 251 | | `pool.refresh_cooldown_sec` | 刷新冷却时间 | 252 | | `pool.use_cooldown_sec` | 使用冷却时间 | 253 | | `pool.max_fail_count` | 最大失败次数 | 254 | | `pool.enable_browser_refresh` | 浏览器刷新开关 | 255 | 256 | **配置合并机制:** 配置文件中缺失的字段会自动使用默认值,无需手动同步示例文件。 257 | 258 | ```bash 259 | # 手动触发重载 260 | curl -X POST http://localhost:8000/admin/reload-config \ 261 | -H "Authorization: Bearer sk-your-api-key" 262 | ``` 263 | 264 | --- 265 | 266 | ## C/S 分离架构 267 | 268 | 支持将号池管理与API服务分离部署,适用于多节点场景。 269 | 270 | ### 架构说明 271 | 272 | ``` 273 | ┌─────────────────┐ ┌─────────────────┐ 274 | │ API Server │◄───────►│ Pool Server │ 275 | │ (客户端模式) │ HTTP │ (服务器模式) │ 276 | └─────────────────┘ └────────┬────────┘ 277 | │ 278 | WebSocket│ 279 | │ 280 | ┌────────▼────────┐ 281 | │ Worker Client │ 282 | │ (注册/续期) │ 283 | └─────────────────┘ 284 | ``` 285 | 286 | ### 运行模式 287 | 288 | | 模式 | 说明 | 289 | |------|------| 290 | | `local` | 本地模式(默认),API服务和号池管理在同一进程 | 291 | | `server` | 服务器模式,提供号池服务和任务分发 | 292 | | `client` | 客户端模式,只接收任务(注册/续期),不提供API服务 | 293 | 294 | ### Server 模式配置 295 | 296 | ```json 297 | { 298 | "api_keys": ["sk-your-api-key"], 299 | "listen_addr": ":8000", 300 | "pool_server": { 301 | "enable": true, 302 | "mode": "server", 303 | "secret": "shared-secret-key", 304 | "target_count": 100, 305 | "data_dir": "./data", 306 | "expired_action": "delete" 307 | } 308 | } 309 | ``` 310 | 311 | ### Client 模式配置(仅注册/续期工作节点) 312 | 313 | ```json 314 | { 315 | "pool_server": { 316 | "enable": true, 317 | "mode": "client", 318 | "server_addr": "http://server-ip:8000", 319 | "secret": "shared-secret-key", 320 | "client_threads": 3 321 | }, 322 | "proxy_pool": { 323 | "subscribes": ["https://your-proxy-subscribe-url"], 324 | "health_check": true, 325 | "check_on_startup": true 326 | } 327 | } 328 | ``` 329 | 330 | ### 配置项说明 331 | 332 | | 配置项 | 说明 | 默认值 | 333 | |--------|------|--------| 334 | | `client_threads` | 客户端并发任务数 | 1 | 335 | | `expired_action` | 过期账号处理方式 | delete | 336 | 337 | **expired_action 可选值:** 338 | - `delete` - 删除过期账号 339 | - `refresh` - 尝试浏览器刷新 340 | - `queue` - 保留在队列等待重试 341 | 342 | **架构说明(v2.x):** 343 | ``` 344 | ┌─────────────────────────────────────────────────────┐ 345 | │ Server (:8000) │ 346 | │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ 347 | │ │ API 服务 │ │ WS 服务 │ │ 号池管理 │ │ 348 | │ │ /v1/chat/* │ │ /ws │ │ Pool Mgr │ │ 349 | │ └─────────────┘ └──────┬──────┘ └─────────────┘ │ 350 | └──────────────────────────┼──────────────────────────┘ 351 | │ WebSocket 352 | ┌────────────┼────────────┐ 353 | │ │ │ 354 | ┌─────▼─────┐ ┌────▼────┐ ┌─────▼─────┐ 355 | │ Client1 │ │ Client2 │ │ Client3 │ 356 | │ (注册) │ │ (注册) │ │ (注册) │ 357 | └───────────┘ └─────────┘ └───────────┘ 358 | ``` 359 | 360 | **Client 模式说明:** 361 | - 通过 WebSocket 连接 Server (`/ws`) 接收任务 362 | - 执行注册新账号任务 363 | - 执行401账号Cookie续期任务 364 | - 完成后自动回传账号数据到Server 365 | - **不提供API服务**,只作为工作节点 366 | 367 | ### 环境变量 368 | 369 | | 变量 | 说明 | 默认值 | 370 | |------|------|--------| 371 | | `LISTEN_ADDR` | 监听地址 | `:8000` | 372 | | `DATA_DIR` | 数据目录 | `./data` | 373 | | `PROXY` | 代理地址 | - | 374 | | `API_KEY` | API 密钥 | - | 375 | | `CONFIG_ID` | 默认 configId | - | 376 | 377 | --- 378 | 379 | ## API 使用 380 | 381 | ### 获取模型列表 382 | 383 | ```bash 384 | curl http://localhost:8000/v1/models \ 385 | -H "Authorization: Bearer sk-your-api-key" 386 | ``` 387 | 388 | ### 聊天补全 389 | 390 | ```bash 391 | curl http://localhost:8000/v1/chat/completions \ 392 | -H "Content-Type: application/json" \ 393 | -H "Authorization: Bearer sk-your-api-key" \ 394 | -d '{ 395 | "model": "gemini-2.5-flash", 396 | "messages": [ 397 | {"role": "user", "content": "Hello!"} 398 | ], 399 | "stream": true 400 | }' 401 | ``` 402 | 403 | ### 多模态(图片输入) 404 | 405 | ```bash 406 | curl http://localhost:8000/v1/chat/completions \ 407 | -H "Content-Type: application/json" \ 408 | -H "Authorization: Bearer sk-your-api-key" \ 409 | -d '{ 410 | "model": "gemini-2.5-flash", 411 | "messages": [ 412 | { 413 | "role": "user", 414 | "content": [ 415 | {"type": "text", "text": "描述这张图片"}, 416 | {"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,..."}} 417 | ] 418 | } 419 | ] 420 | }' 421 | ``` 422 | 423 | --- 424 | 425 | ## Flow 图片/视频生成 426 | 427 | Flow 集成了 Google VideoFX (Veo/Imagen) API,支持图片和视频生成。 428 | 429 | ### 配置 430 | 431 | ```json 432 | { 433 | "flow": { 434 | "enable": true, 435 | "tokens": [], // 配置文件中的 Token(可选) 436 | "proxy": "", // Flow 专用代理 437 | "timeout": 120, // 超时时间(秒) 438 | "poll_interval": 3, // 轮询间隔(秒) 439 | "max_poll_attempts": 500 // 最大轮询次数 440 | } 441 | } 442 | ``` 443 | 444 | ### 获取 Flow Token 445 | 446 | **方式一:文件目录(推荐)** 447 | 448 | 将完整的 cookie 字符串保存到 `data/at/` 目录下的任意 `.txt` 文件: 449 | 450 | ```bash 451 | mkdir -p data/at 452 | echo "your-cookie-string" > data/at/account1.txt 453 | ``` 454 | 455 | 服务启动时自动加载,支持文件监听自动热加载。 456 | 457 | **方式二:API 添加** 458 | 459 | ```bash 460 | curl -X POST http://localhost:8000/admin/flow/add-token \ 461 | -H "Authorization: Bearer sk-xxx" \ 462 | -d '{"cookie": "your-cookie-string"}' 463 | ``` 464 | 465 | **Cookie 获取方法:** 466 | 1. 访问 [labs.google/fx](https://labs.google/fx) 并登录 467 | 2. 打开开发者工具 → Application → Cookies 468 | 3. 复制所有 cookie 或 `__Secure-next-auth.session-token` 的值 469 | 470 | ### Flow 模型列表 471 | 472 | | 模型 | 类型 | 说明 | 473 | |------|------|------| 474 | | `gemini-2.5-flash-image-landscape/portrait` | 图片 | Gemini 2.5 Flash 图片生成 | 475 | | `gemini-3.0-pro-image-landscape/portrait` | 图片 | Gemini 3.0 Pro 图片生成 | 476 | | `imagen-4.0-generate-preview-landscape/portrait` | 图片 | Imagen 4.0 图片生成 | 477 | | `veo_3_1_t2v_fast_landscape/portrait` | 视频 | Veo 3.1 文生视频 | 478 | | `veo_2_1_fast_d_15_t2v_landscape/portrait` | 视频 | Veo 2.1 文生视频 | 479 | | `veo_2_0_t2v_landscape/portrait` | 视频 | Veo 2.0 文生视频 | 480 | | `veo_3_1_i2v_s_fast_fl_landscape/portrait` | 视频 | Veo 3.1 图生视频 (I2V) | 481 | | `veo_2_1_fast_d_15_i2v_landscape/portrait` | 视频 | Veo 2.1 图生视频 (I2V) | 482 | | `veo_2_0_i2v_landscape/portrait` | 视频 | Veo 2.0 图生视频 (I2V) | 483 | | `veo_3_0_r2v_fast_landscape/portrait` | 视频 | Veo 3.0 多图生视频 (R2V) | 484 | 485 | ### 使用示例 486 | 487 | ```bash 488 | # 图片生成 489 | curl http://localhost:8000/v1/chat/completions \ 490 | -H "Authorization: Bearer sk-xxx" \ 491 | -H "Content-Type: application/json" \ 492 | -d '{"model": "gemini-2.5-flash-image-landscape", "messages": [{"role": "user", "content": "一只可爱的猫咪"}], "stream": true}' 493 | 494 | # 文生视频 (T2V) 495 | curl http://localhost:8000/v1/chat/completions \ 496 | -H "Authorization: Bearer sk-xxx" \ 497 | -H "Content-Type: application/json" \ 498 | -d '{"model": "veo_3_1_t2v_fast_landscape", "messages": [{"role": "user", "content": "猫咪在草地上追蝴蝶"}], "stream": true}' 499 | 500 | # 图生视频 (I2V) - 支持首尾帧 501 | curl http://localhost:8000/v1/chat/completions \ 502 | -H "Authorization: Bearer sk-xxx" \ 503 | -H "Content-Type: application/json" \ 504 | -d '{ 505 | "model": "veo_3_1_i2v_s_fast_fl_landscape", 506 | "messages": [{ 507 | "role": "user", 508 | "content": [ 509 | {"type": "text", "text": "猫咪跳跃"}, 510 | {"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,..."}} 511 | ] 512 | }], 513 | "stream": true 514 | }' 515 | ``` 516 | 517 | --- 518 | 519 | ## 🔧 常见问题与解决方案 520 | 521 | ### 注册相关 522 | 523 | | 错误 | 原因 | 解决方案 | 524 | |------|------|----------| 525 | | `无法获取验证码邮件` | 临时邮箱服务不稳定或邮件延迟 | 代理遭到拉黑,更换代理 | 526 | | `panic: nil pointer` | 浏览器启动失败或页面未加载 | 检查 Chrome 是否安装,确保有足够内存 | 527 | | `找不到提交按钮` | 页面结构变化或加载超时 | 升级到最新版本,检查网络 | 528 | 529 | 530 | ### API 相关 531 | 532 | | 错误 | 原因 | 解决方案 | 533 | |------|------|----------| 534 | | `401 Unauthorized` | API Key 无效或未配置 | 检查 `api_keys` 配置 | 535 | | `429 Too Many Requests` | 账号触发速率限制 | 增加账号池数量,调整 `use_cooldown_sec` | 536 | | `503 Service Unavailable` | 无可用账号 | 等待账号刷新或增加注册 | 537 | | `空响应` | Google 返回空内容 | 重试请求,检查 prompt 是否触发过滤 | 538 | 539 | ### WebSocket 相关 540 | 541 | | 错误 | 原因 | 解决方案 | 542 | |------|------|----------| 543 | | `客户端频繁断开` | 心跳超时或网络不稳定 | 检查网络,确保 Server 和 Client 时间同步 | 544 | | `上传注册结果失败` | Server 端口或路径错误 | 确保 `server_addr` 指向正确地址 | 545 | 546 | ### Flow 相关 547 | 548 | | 错误 | 原因 | 解决方案 | 549 | |------|------|----------| 550 | | `Flow 服务未启用` | 未配置或 Token 为空 | 检查 `flow.enable` 和 `flow.tokens` | 551 | | `Token 认证失败` | ST Token 过期 | 重新获取 Token | 552 | | `视频生成超时` | 生成时间过长 | 增加 `max_poll_attempts` | 553 | 554 | ### Docker 相关 555 | 556 | | 错误 | 原因 | 解决方案 | 557 | |------|------|----------| 558 | | `无法启动浏览器` | Docker 容器缺少 Chrome | 使用包含 Chrome 的镜像或挂载主机浏览器 | 559 | | `权限被拒绝` | 数据目录权限问题 | `chown -R 1000:1000 ./data` | 560 | 561 | --- 562 | 563 | ## 📡 API 端点一览 564 | 565 | ### 公开端点 566 | 567 | | 端点 | 方法 | 说明 | 568 | |------|------|------| 569 | | `/` | GET | 服务状态和信息 | 570 | | `/health` | GET | 健康检查 | 571 | | `/ws` | WS | WebSocket 端点 (Server 模式) | 572 | 573 | ### API 端点(需要 API Key) 574 | 575 | | 端点 | 方法 | 说明 | 576 | |------|------|------| 577 | | `/v1/models` | GET | OpenAI 格式模型列表 | 578 | | `/v1/chat/completions` | POST | OpenAI 格式聊天补全 | 579 | | `/v1/messages` | POST | Claude 格式消息 | 580 | | `/v1beta/models` | GET | Gemini 格式模型列表 | 581 | | `/v1beta/models/:model` | GET | Gemini 格式模型详情 | 582 | | `/v1beta/models/:model:generateContent` | POST | Gemini 格式生成内容 | 583 | 584 | ### 管理端点(需要 API Key) 585 | 586 | | 端点 | 方法 | 说明 | 587 | |------|------|------| 588 | | `/admin/status` | GET | 账号池状态 | 589 | | `/admin/stats` | GET | 详细 API 统计 | 590 | | `/admin/ip` | GET | IP 遥测统计(请求数/Token/RPM) | 591 | | `/admin/register` | POST | 触发注册 | 592 | | `/admin/refresh` | POST | 刷新账号池 | 593 | | `/admin/reload-config` | POST | 热重载配置文件 | 594 | | `/admin/force-refresh` | POST | 强制刷新所有账号 | 595 | | `/admin/config/cooldown` | POST | 动态调整冷却时间 | 596 | | `/admin/browser-refresh` | POST | 手动触发浏览器刷新指定账号 | 597 | | `/admin/config/browser-refresh` | POST | 配置浏览器刷新开关 | 598 | | `/admin/flow/status` | GET | Flow 服务状态 | 599 | | `/admin/flow/add-token` | POST | 添加 Flow Token | 600 | | `/admin/flow/remove-token` | POST | 移除 Flow Token | 601 | | `/admin/flow/reload` | POST | 重新加载 Flow Token | 602 | 603 | --- 604 | 605 | ## 🛠️ 开发 606 | 607 | ### 本地运行 608 | 609 | ```bash 610 | # 安装依赖 611 | go mod download 612 | 613 | # 运行 614 | go run . 615 | 616 | # 调试模式 617 | go run . -d 618 | ``` 619 | 620 | ### 构建 621 | 622 | ```bash 623 | # 标准构建 624 | go build -o business2api . 625 | 626 | # 带 QUIC/uTLS 支持(推荐) 627 | go build -tags "with_quic with_utls" -o business2api . 628 | 629 | # 生产构建(压缩体积) 630 | CGO_ENABLED=0 go build -ldflags="-s -w" -tags "with_quic with_utls" -o business2api . 631 | 632 | # 多平台构建 633 | GOOS=linux GOARCH=amd64 go build -tags "with_quic with_utls" -o business2api-linux-amd64 . 634 | GOOS=windows GOARCH=amd64 go build -tags "with_quic with_utls" -o business2api-windows-amd64.exe . 635 | GOOS=darwin GOARCH=arm64 go build -tags "with_quic with_utls" -o business2api-darwin-arm64 . 636 | ``` 637 | 638 | ### 项目结构 639 | 640 | ``` 641 | . 642 | ├── main.go # 主程序入口 643 | ├── config/ # 配置文件 644 | │ ├── config.json.example 645 | │ └── README.md 646 | ├── src/ 647 | │ ├── flow/ # Flow 图片/视频生成 648 | │ ├── logger/ # 日志模块 649 | │ ├── pool/ # 账号池管理(C/S架构) 650 | │ ├── proxy/ # 代理池管理 651 | │ ├── register/ # 浏览器自动注册 652 | │ └── utils/ # 工具函数 653 | ├── docker/ # Docker 相关 654 | │ └── docker-compose.yml 655 | └── .github/ # GitHub Actions 656 | ``` 657 | 658 | --- 659 | 660 | ## 📊 IP 遥测接口 661 | 662 | 访问 `/admin/ip` 获取全部 IP 请求统计: 663 | 664 | ```bash 665 | curl http://localhost:8000/admin/ip \ 666 | -H "Authorization: Bearer sk-your-api-key" 667 | ``` 668 | 669 | **返回字段说明:** 670 | 671 | | 字段 | 说明 | 672 | |------|------| 673 | | `unique_ips` | 独立 IP 数量 | 674 | | `total_requests` | 总请求数 | 675 | | `total_tokens` | 总 Token 消耗 | 676 | | `total_images` | 图片生成数 | 677 | | `total_videos` | 视频生成数 | 678 | | `ips[].rpm` | 单 IP 每分钟请求数 | 679 | | `ips[].input_tokens` | 输入 Token | 680 | | `ips[].output_tokens` | 输出 Token | 681 | | `ips[].models` | 各模型使用次数 | 682 | 683 | --- 684 | 685 | 686 | ## Star History 687 | 688 | [![Star History Chart](https://api.star-history.com/svg?repos=XxxXTeam/business2api&type=date&legend=top-left)](https://www.star-history.com/#XxxXTeam/business2api&type=date&legend=top-left) 689 | 690 | ## 📄 License 691 | 692 | MIT License 693 | -------------------------------------------------------------------------------- /src/pool/pool.go: -------------------------------------------------------------------------------- 1 | package pool 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha256" 6 | "encoding/base64" 7 | "encoding/json" 8 | "fmt" 9 | "log" 10 | "net/http" 11 | "os" 12 | "path/filepath" 13 | "strings" 14 | "sync" 15 | "sync/atomic" 16 | "time" 17 | 18 | "business2api/src/logger" 19 | ) 20 | 21 | // ==================== 数据结构 ==================== 22 | 23 | // Cookie 账号Cookie 24 | type Cookie struct { 25 | Name string `json:"name"` 26 | Value string `json:"value"` 27 | Domain string `json:"domain"` 28 | } 29 | 30 | // AccountData 账号数据 31 | type AccountData struct { 32 | Email string `json:"email"` 33 | FullName string `json:"fullName"` 34 | Authorization string `json:"authorization"` 35 | Cookies []Cookie `json:"cookies"` 36 | CookieString string `json:"cookie_string,omitempty"` 37 | ResponseHeaders map[string]string `json:"response_headers,omitempty"` 38 | Timestamp string `json:"timestamp"` 39 | ConfigID string `json:"configId,omitempty"` 40 | CSESIDX string `json:"csesidx,omitempty"` 41 | } 42 | 43 | func ParseCookieString(cookieStr string) []Cookie { 44 | var cookies []Cookie 45 | if cookieStr == "" { 46 | return cookies 47 | } 48 | 49 | parts := strings.Split(cookieStr, "; ") 50 | for _, part := range parts { 51 | part = strings.TrimSpace(part) 52 | if part == "" { 53 | continue 54 | } 55 | idx := strings.Index(part, "=") 56 | if idx > 0 { 57 | cookies = append(cookies, Cookie{ 58 | Name: part[:idx], 59 | Value: part[idx+1:], 60 | Domain: ".gemini.google", // 默认域名 61 | }) 62 | } 63 | } 64 | return cookies 65 | } 66 | 67 | func (a *AccountData) GetAllCookies() []Cookie { 68 | if len(a.Cookies) > 0 { 69 | return a.Cookies 70 | } 71 | if a.CookieString != "" { 72 | return ParseCookieString(a.CookieString) 73 | } 74 | return nil 75 | } 76 | 77 | // AccountStatus 账号状态 78 | type AccountStatus int 79 | 80 | const ( 81 | StatusPending AccountStatus = iota // 待刷新 82 | StatusReady // 就绪可用 83 | StatusCooldown // 冷却中 84 | StatusInvalid // 失效 85 | ) 86 | 87 | // Account 账号实例 88 | type Account struct { 89 | Data AccountData 90 | FilePath string 91 | JWT string 92 | JWTExpires time.Time 93 | ConfigID string 94 | CSESIDX string 95 | LastRefresh time.Time 96 | LastUsed time.Time // 最后使用时间 97 | Refreshed bool 98 | FailCount int // 连续失败次数 99 | BrowserRefreshCount int // 浏览器刷新尝试次数 100 | SuccessCount int // 成功次数 101 | TotalCount int // 总使用次数 102 | DailyCount int // 每日调用次数 103 | DailyCountDate string // 每日计数日期 (YYYY-MM-DD) 104 | Status AccountStatus 105 | Mu sync.Mutex 106 | } 107 | 108 | // SetCooldownMultiplier 设置冷却时间倍数(用于429限流) 109 | func (acc *Account) SetCooldownMultiplier(multiplier int) { 110 | acc.Mu.Lock() 111 | acc.LastUsed = time.Now().Add(UseCooldown * time.Duration(multiplier-1)) 112 | acc.Mu.Unlock() 113 | } 114 | 115 | // 默认冷却时间(可通过配置覆盖) 116 | var ( 117 | RefreshCooldown = 4 * time.Minute // 刷新冷却 118 | UseCooldown = 15 * time.Second // 使用冷却 119 | JWTRefreshThreshold = 60 * time.Second // JWT刷新阈值 120 | MaxFailCount = 3 // 最大连续失败次数 121 | EnableBrowserRefresh = true // 是否启用浏览器刷新 122 | BrowserRefreshHeadless = true // 浏览器刷新是否无头模式 123 | BrowserRefreshMaxRetry = 1 // 浏览器刷新最大重试次数 124 | AutoDelete401 = false // 401时是否自动删除账号 125 | DailyLimit = 3000 // 每账号每日最大调用次数 126 | DataDir string 127 | DefaultConfig string 128 | Proxy string 129 | JwtTTL = 270 * time.Second 130 | HTTPClient *http.Client 131 | ) 132 | 133 | type RefreshCookieFunc func(acc *Account, headless bool, proxy string) *BrowserRefreshResult 134 | type BrowserRefreshResult struct { 135 | Success bool 136 | SecureCookies []Cookie 137 | Authorization string 138 | ConfigID string 139 | CSESIDX string 140 | ResponseHeaders map[string]string 141 | Error error 142 | } 143 | 144 | var RefreshCookieWithBrowser RefreshCookieFunc 145 | 146 | func readResponseBody(resp *http.Response) ([]byte, error) { 147 | body := make([]byte, 0) 148 | buf := make([]byte, 4096) 149 | for { 150 | n, err := resp.Body.Read(buf) 151 | if n > 0 { 152 | body = append(body, buf[:n]...) 153 | } 154 | if err != nil { 155 | break 156 | } 157 | } 158 | return body, nil 159 | } 160 | 161 | type AccountPool struct { 162 | readyAccounts []*Account 163 | pendingAccounts []*Account 164 | index uint64 165 | mu sync.RWMutex 166 | refreshInterval time.Duration 167 | refreshWorkers int 168 | stopChan chan struct{} 169 | totalSuccess int64 170 | totalFailed int64 171 | totalRequests int64 172 | } 173 | 174 | func (p *AccountPool) GetReadyAccounts() []*Account { 175 | p.mu.RLock() 176 | defer p.mu.RUnlock() 177 | return p.readyAccounts 178 | } 179 | func (p *AccountPool) GetPendingAccounts() []*Account { 180 | p.mu.RLock() 181 | defer p.mu.RUnlock() 182 | return p.pendingAccounts 183 | } 184 | func (p *AccountPool) WithLock(fn func(ready, pending []*Account)) { 185 | p.mu.RLock() 186 | defer p.mu.RUnlock() 187 | fn(p.readyAccounts, p.pendingAccounts) 188 | } 189 | func (p *AccountPool) WithWriteLock(fn func(ready, pending []*Account) ([]*Account, []*Account)) { 190 | p.mu.Lock() 191 | defer p.mu.Unlock() 192 | p.readyAccounts, p.pendingAccounts = fn(p.readyAccounts, p.pendingAccounts) 193 | } 194 | 195 | var Pool = &AccountPool{ 196 | refreshInterval: 5 * time.Second, 197 | refreshWorkers: 5, 198 | stopChan: make(chan struct{}), 199 | } 200 | 201 | func SetCooldowns(refreshSec, useSec int) { 202 | if refreshSec > 0 { 203 | RefreshCooldown = time.Duration(refreshSec) * time.Second 204 | } 205 | if useSec > 0 { 206 | UseCooldown = time.Duration(useSec) * time.Second 207 | } 208 | logger.Info("⚙️ 冷却配置: 刷新=%v, 使用=%v", RefreshCooldown, UseCooldown) 209 | } 210 | 211 | // SetDailyLimit 设置每账号每日最大调用次数 212 | func SetDailyLimit(limit int) { 213 | if limit >= 0 { 214 | DailyLimit = limit 215 | if limit == 0 { 216 | logger.Info("⚙️ 每日调用限制: 无限制") 217 | } else { 218 | logger.Info("⚙️ 每日调用限制: %d次/账号", limit) 219 | } 220 | } 221 | } 222 | 223 | func (p *AccountPool) Load(dir string) error { 224 | p.mu.Lock() 225 | defer p.mu.Unlock() 226 | 227 | files, err := filepath.Glob(filepath.Join(dir, "*.json")) 228 | if err != nil { 229 | return err 230 | } 231 | 232 | existingAccounts := make(map[string]*Account) 233 | for _, acc := range p.readyAccounts { 234 | existingAccounts[acc.FilePath] = acc 235 | } 236 | for _, acc := range p.pendingAccounts { 237 | existingAccounts[acc.FilePath] = acc 238 | } 239 | 240 | var newReadyAccounts []*Account 241 | var newPendingAccounts []*Account 242 | 243 | for _, f := range files { 244 | if acc, ok := existingAccounts[f]; ok { 245 | if acc.Refreshed { 246 | newReadyAccounts = append(newReadyAccounts, acc) 247 | } else { 248 | newPendingAccounts = append(newPendingAccounts, acc) 249 | } 250 | delete(existingAccounts, f) 251 | continue 252 | } 253 | 254 | data, err := os.ReadFile(f) 255 | if err != nil { 256 | log.Printf("⚠️ 读取 %s 失败: %v", f, err) 257 | continue 258 | } 259 | 260 | var acc AccountData 261 | if err := json.Unmarshal(data, &acc); err != nil { 262 | log.Printf("⚠️ 解析 %s 失败: %v", f, err) 263 | continue 264 | } 265 | 266 | csesidx := acc.CSESIDX 267 | if csesidx == "" { 268 | csesidx = extractCSESIDX(acc.Authorization) 269 | } 270 | if csesidx == "" { 271 | log.Printf("⚠️ %s 无法获取 csesidx", f) 272 | continue 273 | } 274 | 275 | configID := acc.ConfigID 276 | if configID == "" && DefaultConfig != "" { 277 | configID = DefaultConfig 278 | } 279 | 280 | newPendingAccounts = append(newPendingAccounts, &Account{ 281 | Data: acc, 282 | FilePath: f, 283 | CSESIDX: csesidx, 284 | ConfigID: configID, 285 | Refreshed: false, 286 | }) 287 | } 288 | 289 | p.readyAccounts = newReadyAccounts 290 | p.pendingAccounts = newPendingAccounts 291 | return nil 292 | } 293 | 294 | // GetPendingAccount 获取待刷新账号 295 | func (p *AccountPool) GetPendingAccount() *Account { 296 | p.mu.Lock() 297 | defer p.mu.Unlock() 298 | 299 | if len(p.pendingAccounts) == 0 { 300 | return nil 301 | } 302 | 303 | acc := p.pendingAccounts[0] 304 | p.pendingAccounts = p.pendingAccounts[1:] 305 | return acc 306 | } 307 | 308 | // MarkReady 标记账号为就绪 309 | func (p *AccountPool) MarkReady(acc *Account) { 310 | p.mu.Lock() 311 | defer p.mu.Unlock() 312 | acc.Refreshed = true 313 | p.readyAccounts = append(p.readyAccounts, acc) 314 | } 315 | 316 | // MarkPending 标记账号待刷新 317 | func (p *AccountPool) MarkPending(acc *Account) { 318 | p.mu.Lock() 319 | defer p.mu.Unlock() 320 | 321 | for i, a := range p.readyAccounts { 322 | if a == acc { 323 | p.readyAccounts = append(p.readyAccounts[:i], p.readyAccounts[i+1:]...) 324 | break 325 | } 326 | } 327 | 328 | acc.Mu.Lock() 329 | acc.Refreshed = false 330 | acc.Mu.Unlock() 331 | 332 | p.pendingAccounts = append(p.pendingAccounts, acc) 333 | log.Printf("🔄 账号 %s 移至刷新池", filepath.Base(acc.FilePath)) 334 | } 335 | 336 | // RemoveAccount 删除失效账号 337 | func (p *AccountPool) RemoveAccount(acc *Account) { 338 | if err := os.Remove(acc.FilePath); err != nil { 339 | log.Printf("⚠️ 删除文件失败 %s: %v", acc.FilePath, err) 340 | } else { 341 | log.Printf("🗑️ 已删除失效账号: %s", filepath.Base(acc.FilePath)) 342 | } 343 | } 344 | 345 | // SaveToFile 保存账号到文件 346 | func (acc *Account) SaveToFile() error { 347 | acc.Mu.Lock() 348 | defer acc.Mu.Unlock() 349 | 350 | acc.Data.Timestamp = time.Now().Format(time.RFC3339) 351 | 352 | // 同时生成 cookie 字符串(方便调试和兼容老版本) 353 | if len(acc.Data.Cookies) > 0 { 354 | var cookieParts []string 355 | for _, c := range acc.Data.Cookies { 356 | cookieParts = append(cookieParts, fmt.Sprintf("%s=%s", c.Name, c.Value)) 357 | } 358 | acc.Data.CookieString = strings.Join(cookieParts, "; ") 359 | } 360 | 361 | data, err := json.MarshalIndent(acc.Data, "", " ") 362 | if err != nil { 363 | return fmt.Errorf("序列化账号数据失败: %w", err) 364 | } 365 | 366 | if err := os.WriteFile(acc.FilePath, data, 0644); err != nil { 367 | return fmt.Errorf("写入文件失败: %w", err) 368 | } 369 | return nil 370 | } 371 | 372 | // StartPoolManager 启动号池管理器 373 | func (p *AccountPool) StartPoolManager() { 374 | for i := 0; i < p.refreshWorkers; i++ { 375 | go p.refreshWorker(i) 376 | } 377 | go p.scanWorker() 378 | } 379 | 380 | func (p *AccountPool) refreshWorker(id int) { 381 | for { 382 | select { 383 | case <-p.stopChan: 384 | return 385 | default: 386 | } 387 | 388 | acc := p.GetPendingAccount() 389 | if acc == nil { 390 | time.Sleep(time.Second) 391 | continue 392 | } 393 | 394 | // 检查冷却 395 | if time.Since(acc.LastRefresh) < RefreshCooldown { 396 | acc.Mu.Lock() 397 | acc.Refreshed = true 398 | acc.Status = StatusReady 399 | acc.Mu.Unlock() 400 | p.MarkReady(acc) 401 | continue 402 | } 403 | 404 | acc.JWTExpires = time.Time{} 405 | if err := acc.RefreshJWT(); err != nil { 406 | errMsg := err.Error() 407 | 408 | // 认证失败:根据配置决定是否删除或尝试刷新 409 | if strings.Contains(errMsg, "账号失效") || 410 | strings.Contains(errMsg, "401") || 411 | strings.Contains(errMsg, "403") { 412 | log.Printf("⚠️ [worker-%d] [%s] 认证失效: %v", id, acc.Data.Email, err) 413 | 414 | // 如果配置了401自动删除,直接删除账号 415 | if AutoDelete401 { 416 | log.Printf("🗑️ [worker-%d] [%s] 401自动删除已启用,移除账号", id, acc.Data.Email) 417 | acc.Mu.Lock() 418 | acc.Status = StatusInvalid 419 | acc.Mu.Unlock() 420 | p.RemoveAccount(acc) 421 | continue 422 | } 423 | 424 | // 检查是否可以进行浏览器刷新 425 | acc.Mu.Lock() 426 | browserRefreshCount := acc.BrowserRefreshCount 427 | acc.Mu.Unlock() 428 | if EnableBrowserRefresh && BrowserRefreshMaxRetry > 0 && browserRefreshCount < BrowserRefreshMaxRetry && RefreshCookieWithBrowser != nil { 429 | acc.Mu.Lock() 430 | acc.BrowserRefreshCount++ 431 | acc.Mu.Unlock() 432 | refreshResult := RefreshCookieWithBrowser(acc, BrowserRefreshHeadless, Proxy) 433 | 434 | if refreshResult.Success { 435 | acc.Mu.Lock() 436 | acc.Data.Cookies = refreshResult.SecureCookies 437 | if refreshResult.Authorization != "" { 438 | acc.Data.Authorization = refreshResult.Authorization 439 | } 440 | if refreshResult.ConfigID != "" { 441 | acc.ConfigID = refreshResult.ConfigID 442 | acc.Data.ConfigID = refreshResult.ConfigID 443 | } 444 | if refreshResult.CSESIDX != "" { 445 | acc.CSESIDX = refreshResult.CSESIDX 446 | acc.Data.CSESIDX = refreshResult.CSESIDX 447 | } 448 | if len(refreshResult.ResponseHeaders) > 0 { 449 | acc.Data.ResponseHeaders = refreshResult.ResponseHeaders 450 | } 451 | acc.FailCount = 0 452 | acc.BrowserRefreshCount = 0 // 成功后重置计数 453 | acc.JWTExpires = time.Time{} // 重置JWT过期时间 454 | acc.Status = StatusPending 455 | acc.Mu.Unlock() 456 | 457 | // 保存更新后的账号 458 | if err := acc.SaveToFile(); err != nil { 459 | log.Printf("⚠️ [%s] 保存刷新后的账号失败: %v", acc.Data.Email, err) 460 | } 461 | p.mu.Lock() 462 | p.pendingAccounts = append(p.pendingAccounts, acc) 463 | p.mu.Unlock() 464 | continue 465 | } else { 466 | log.Printf("⚠️ [worker-%d] [%s] 浏览器刷新失败: %v", id, acc.Data.Email, refreshResult.Error) 467 | } 468 | } else if browserRefreshCount >= BrowserRefreshMaxRetry && BrowserRefreshMaxRetry > 0 { 469 | log.Printf("⚠️ [worker-%d] [%s] 已达浏览器刷新上限 (%d次),跳过浏览器刷新", id, acc.Data.Email, BrowserRefreshMaxRetry) 470 | } 471 | acc.Mu.Lock() 472 | acc.FailCount++ 473 | failCount := acc.FailCount 474 | browserRefreshCount = acc.BrowserRefreshCount 475 | acc.Mu.Unlock() 476 | maxRetry := MaxFailCount * 3 // 401的最大重试次数更宽松 477 | if maxRetry < 10 { 478 | maxRetry = 10 // 至少重试10次 479 | } 480 | if browserRefreshCount >= BrowserRefreshMaxRetry && failCount >= maxRetry { 481 | acc.Mu.Lock() 482 | acc.Status = StatusInvalid 483 | acc.Mu.Unlock() 484 | p.RemoveAccount(acc) 485 | continue 486 | } 487 | 488 | waitTime := time.Duration(failCount*30) * time.Second 489 | if waitTime > 5*time.Minute { 490 | waitTime = 5 * time.Minute // 最大等待5分钟 491 | } 492 | log.Printf("⏳ [worker-%d] [%s] 401刷新失败 (%d/%d次),%v后重试", id, acc.Data.Email, failCount, maxRetry, waitTime) 493 | time.Sleep(waitTime) 494 | 495 | p.mu.Lock() 496 | p.pendingAccounts = append(p.pendingAccounts, acc) 497 | p.mu.Unlock() 498 | continue 499 | } 500 | 501 | // 冷却中:直接标记就绪 502 | if strings.Contains(errMsg, "刷新冷却中") { 503 | acc.Mu.Lock() 504 | acc.Refreshed = true 505 | acc.Status = StatusReady 506 | acc.Mu.Unlock() 507 | p.MarkReady(acc) 508 | continue 509 | } 510 | 511 | // 其他错误:累计失败次数 512 | acc.Mu.Lock() 513 | acc.FailCount++ 514 | failCount := acc.FailCount 515 | acc.Mu.Unlock() 516 | 517 | if failCount >= MaxFailCount { 518 | log.Printf("❌ [worker-%d] [%s] 连续失败 %d 次,移除账号: %v", id, acc.Data.Email, failCount, err) 519 | acc.Mu.Lock() 520 | acc.Status = StatusInvalid 521 | acc.Mu.Unlock() 522 | p.RemoveAccount(acc) 523 | } else { 524 | log.Printf("⚠️ [worker-%d] [%s] 刷新失败 (%d/%d): %v", id, acc.Data.Email, failCount, MaxFailCount, err) 525 | // 延迟后重试 526 | time.Sleep(time.Duration(failCount) * 5 * time.Second) 527 | p.mu.Lock() 528 | p.pendingAccounts = append(p.pendingAccounts, acc) 529 | p.mu.Unlock() 530 | } 531 | } else { 532 | // 刷新成功:重置失败计数 533 | acc.Mu.Lock() 534 | acc.FailCount = 0 535 | acc.Status = StatusReady 536 | acc.Mu.Unlock() 537 | 538 | if err := acc.SaveToFile(); err != nil { 539 | log.Printf("⚠️ [%s] 写回文件失败: %v", acc.Data.Email, err) 540 | } 541 | p.MarkReady(acc) 542 | } 543 | } 544 | } 545 | 546 | func (p *AccountPool) scanWorker() { 547 | ticker := time.NewTicker(p.refreshInterval) 548 | fileScanTicker := time.NewTicker(5 * time.Minute) 549 | defer ticker.Stop() 550 | defer fileScanTicker.Stop() 551 | 552 | for { 553 | select { 554 | case <-p.stopChan: 555 | return 556 | case <-fileScanTicker.C: 557 | p.Load(DataDir) 558 | case <-ticker.C: 559 | p.RefreshExpiredAccounts() 560 | } 561 | } 562 | } 563 | 564 | // RefreshExpiredAccounts 刷新即将过期的账号 565 | func (p *AccountPool) RefreshExpiredAccounts() { 566 | p.mu.Lock() 567 | defer p.mu.Unlock() 568 | 569 | var stillReady []*Account 570 | refreshed := 0 571 | now := time.Now() 572 | 573 | for _, acc := range p.readyAccounts { 574 | acc.Mu.Lock() 575 | jwtExpires := acc.JWTExpires 576 | lastRefresh := acc.LastRefresh 577 | acc.Mu.Unlock() 578 | 579 | needsRefresh := jwtExpires.IsZero() || now.Add(JWTRefreshThreshold).After(jwtExpires) 580 | inCooldown := now.Sub(lastRefresh) < RefreshCooldown 581 | 582 | if needsRefresh && !inCooldown { 583 | acc.Mu.Lock() 584 | acc.Refreshed = false 585 | acc.Mu.Unlock() 586 | p.pendingAccounts = append(p.pendingAccounts, acc) 587 | refreshed++ 588 | } else { 589 | stillReady = append(stillReady, acc) 590 | } 591 | } 592 | 593 | p.readyAccounts = stillReady 594 | if refreshed > 0 { 595 | log.Printf("🔄 扫描刷新: %d 个账号JWT即将过期", refreshed) 596 | } 597 | } 598 | 599 | func (p *AccountPool) RefreshAllAccounts() { 600 | p.mu.Lock() 601 | defer p.mu.Unlock() 602 | 603 | var stillReady []*Account 604 | refreshed, skipped := 0, 0 605 | 606 | for _, acc := range p.readyAccounts { 607 | if time.Since(acc.LastRefresh) < RefreshCooldown { 608 | stillReady = append(stillReady, acc) 609 | skipped++ 610 | continue 611 | } 612 | acc.Refreshed = false 613 | acc.JWTExpires = time.Time{} 614 | p.pendingAccounts = append(p.pendingAccounts, acc) 615 | refreshed++ 616 | } 617 | 618 | p.readyAccounts = stillReady 619 | if refreshed > 0 { 620 | log.Printf("🔄 全量刷新: %d 个账号已加入刷新队列,%d 个在冷却中跳过", refreshed, skipped) 621 | } 622 | } 623 | 624 | // checkAndUpdateDailyCount 检查并更新每日计数,返回是否超限 625 | func (acc *Account) checkAndUpdateDailyCount() bool { 626 | today := time.Now().Format("2006-01-02") 627 | if acc.DailyCountDate != today { 628 | // 新的一天,重置计数 629 | acc.DailyCountDate = today 630 | acc.DailyCount = 0 631 | } 632 | // 检查是否超过每日限制 633 | if DailyLimit > 0 && acc.DailyCount >= DailyLimit { 634 | return true // 超限 635 | } 636 | acc.DailyCount++ 637 | return false 638 | } 639 | 640 | // GetDailyUsage 获取每日使用情况 641 | func (acc *Account) GetDailyUsage() (count int, limit int, date string) { 642 | acc.Mu.Lock() 643 | defer acc.Mu.Unlock() 644 | today := time.Now().Format("2006-01-02") 645 | if acc.DailyCountDate != today { 646 | return 0, DailyLimit, today 647 | } 648 | return acc.DailyCount, DailyLimit, acc.DailyCountDate 649 | } 650 | 651 | func (p *AccountPool) Next() *Account { 652 | p.mu.RLock() 653 | defer p.mu.RUnlock() 654 | 655 | if len(p.readyAccounts) == 0 { 656 | return nil 657 | } 658 | 659 | n := len(p.readyAccounts) 660 | startIdx := atomic.AddUint64(&p.index, 1) - 1 661 | now := time.Now() 662 | 663 | var bestAccount *Account 664 | var oldestUsed time.Time 665 | var allExceededDaily bool = true 666 | 667 | // 第一轮:找不在使用冷却中且未超日限的账号 668 | for i := 0; i < n; i++ { 669 | acc := p.readyAccounts[(startIdx+uint64(i))%uint64(n)] 670 | acc.Mu.Lock() 671 | inUseCooldown := now.Sub(acc.LastUsed) < UseCooldown 672 | lastUsed := acc.LastUsed 673 | 674 | // 检查每日限制(不更新计数) 675 | today := now.Format("2006-01-02") 676 | dailyCount := acc.DailyCount 677 | if acc.DailyCountDate != today { 678 | dailyCount = 0 679 | } 680 | exceededDaily := DailyLimit > 0 && dailyCount >= DailyLimit 681 | acc.Mu.Unlock() 682 | 683 | if exceededDaily { 684 | continue // 跳过已达每日限制的账号 685 | } 686 | allExceededDaily = false 687 | 688 | if !inUseCooldown { 689 | // 找到可用账号,标记使用时间并更新每日计数 690 | acc.Mu.Lock() 691 | acc.LastUsed = now 692 | acc.TotalCount++ 693 | acc.checkAndUpdateDailyCount() 694 | acc.Mu.Unlock() 695 | atomic.AddInt64(&p.totalRequests, 1) 696 | return acc 697 | } 698 | 699 | // 记录最久未使用的账号作为备选 700 | if bestAccount == nil || lastUsed.Before(oldestUsed) { 701 | bestAccount = acc 702 | oldestUsed = lastUsed 703 | } 704 | } 705 | 706 | // 所有账号都超过每日限制 707 | if allExceededDaily { 708 | log.Printf("⚠️ 所有账号已达每日调用上限 (%d次/天)", DailyLimit) 709 | return nil 710 | } 711 | 712 | // 所有未超限的账号都在冷却中,返回最久未使用的 713 | if bestAccount != nil { 714 | bestAccount.Mu.Lock() 715 | bestAccount.LastUsed = now 716 | bestAccount.TotalCount++ 717 | bestAccount.checkAndUpdateDailyCount() 718 | bestAccount.Mu.Unlock() 719 | atomic.AddInt64(&p.totalRequests, 1) 720 | log.Printf("⏳ 所有账号在使用冷却中,选择最久未用: %s", bestAccount.Data.Email) 721 | } 722 | return bestAccount 723 | } 724 | 725 | // MarkUsed 标记账号已使用(成功) 726 | func (p *AccountPool) MarkUsed(acc *Account, success bool) { 727 | if acc == nil { 728 | return 729 | } 730 | acc.Mu.Lock() 731 | defer acc.Mu.Unlock() 732 | 733 | if success { 734 | acc.SuccessCount++ 735 | acc.FailCount = 0 // 重置连续失败 736 | atomic.AddInt64(&p.totalSuccess, 1) 737 | } else { 738 | acc.FailCount++ 739 | atomic.AddInt64(&p.totalFailed, 1) 740 | } 741 | } 742 | 743 | // MarkNeedsRefresh 标记账号需要刷新(遇到401/403等) 744 | func (p *AccountPool) MarkNeedsRefresh(acc *Account) { 745 | if acc == nil { 746 | return 747 | } 748 | acc.Mu.Lock() 749 | acc.LastRefresh = time.Time{} // 重置刷新时间,强制刷新 750 | acc.Mu.Unlock() 751 | p.MarkPending(acc) 752 | } 753 | 754 | func (p *AccountPool) Count() int { p.mu.RLock(); defer p.mu.RUnlock(); return len(p.readyAccounts) } 755 | func (p *AccountPool) PendingCount() int { 756 | p.mu.RLock() 757 | defer p.mu.RUnlock() 758 | return len(p.pendingAccounts) 759 | } 760 | func (p *AccountPool) ReadyCount() int { 761 | p.mu.RLock() 762 | defer p.mu.RUnlock() 763 | return len(p.readyAccounts) 764 | } 765 | func (p *AccountPool) TotalCount() int { 766 | p.mu.RLock() 767 | defer p.mu.RUnlock() 768 | return len(p.readyAccounts) + len(p.pendingAccounts) 769 | } 770 | 771 | // Stats 返回号池统计信息 772 | func (p *AccountPool) Stats() map[string]interface{} { 773 | p.mu.RLock() 774 | defer p.mu.RUnlock() 775 | 776 | totalSuccess := atomic.LoadInt64(&p.totalSuccess) 777 | totalFailed := atomic.LoadInt64(&p.totalFailed) 778 | totalRequests := atomic.LoadInt64(&p.totalRequests) 779 | 780 | successRate := float64(0) 781 | if totalRequests > 0 { 782 | successRate = float64(totalSuccess) / float64(totalRequests) * 100 783 | } 784 | 785 | // 统计每日可用账号数 786 | today := time.Now().Format("2006-01-02") 787 | availableToday := 0 788 | exceededToday := 0 789 | for _, acc := range p.readyAccounts { 790 | acc.Mu.Lock() 791 | dailyCount := acc.DailyCount 792 | if acc.DailyCountDate != today { 793 | dailyCount = 0 794 | } 795 | acc.Mu.Unlock() 796 | if DailyLimit == 0 || dailyCount < DailyLimit { 797 | availableToday++ 798 | } else { 799 | exceededToday++ 800 | } 801 | } 802 | 803 | return map[string]interface{}{ 804 | "ready": len(p.readyAccounts), 805 | "pending": len(p.pendingAccounts), 806 | "total": len(p.readyAccounts) + len(p.pendingAccounts), 807 | "available_today": availableToday, 808 | "exceeded_today": exceededToday, 809 | "total_requests": totalRequests, 810 | "total_success": totalSuccess, 811 | "total_failed": totalFailed, 812 | "success_rate": fmt.Sprintf("%.1f%%", successRate), 813 | "daily_limit": DailyLimit, 814 | "cooldowns": map[string]interface{}{ 815 | "refresh_sec": int(RefreshCooldown.Seconds()), 816 | "use_sec": int(UseCooldown.Seconds()), 817 | }, 818 | } 819 | } 820 | 821 | // AccountInfo 账号信息(用于API返回) 822 | type AccountInfo struct { 823 | Email string `json:"email"` 824 | Status string `json:"status"` 825 | LastRefresh time.Time `json:"last_refresh"` 826 | LastUsed time.Time `json:"last_used"` 827 | FailCount int `json:"fail_count"` 828 | SuccessCount int `json:"success_count"` 829 | TotalCount int `json:"total_count"` 830 | DailyCount int `json:"daily_count"` 831 | DailyLimit int `json:"daily_limit"` 832 | DailyRemaining int `json:"daily_remaining"` 833 | JWTExpires time.Time `json:"jwt_expires"` 834 | } 835 | 836 | // ListAccounts 列出所有账号信息 837 | func (p *AccountPool) ListAccounts() []AccountInfo { 838 | p.mu.RLock() 839 | defer p.mu.RUnlock() 840 | 841 | var accounts []AccountInfo 842 | statusNames := map[AccountStatus]string{ 843 | StatusPending: "pending", 844 | StatusReady: "ready", 845 | StatusCooldown: "cooldown", 846 | StatusInvalid: "invalid", 847 | } 848 | 849 | today := time.Now().Format("2006-01-02") 850 | addAccounts := func(list []*Account) { 851 | for _, acc := range list { 852 | acc.Mu.Lock() 853 | dailyCount := acc.DailyCount 854 | if acc.DailyCountDate != today { 855 | dailyCount = 0 856 | } 857 | dailyRemaining := DailyLimit - dailyCount 858 | if DailyLimit == 0 { 859 | dailyRemaining = -1 // -1 表示无限制 860 | } else if dailyRemaining < 0 { 861 | dailyRemaining = 0 862 | } 863 | info := AccountInfo{ 864 | Email: acc.Data.Email, 865 | Status: statusNames[acc.Status], 866 | LastRefresh: acc.LastRefresh, 867 | LastUsed: acc.LastUsed, 868 | FailCount: acc.FailCount, 869 | SuccessCount: acc.SuccessCount, 870 | TotalCount: acc.TotalCount, 871 | DailyCount: dailyCount, 872 | DailyLimit: DailyLimit, 873 | DailyRemaining: dailyRemaining, 874 | JWTExpires: acc.JWTExpires, 875 | } 876 | acc.Mu.Unlock() 877 | accounts = append(accounts, info) 878 | } 879 | } 880 | 881 | addAccounts(p.readyAccounts) 882 | addAccounts(p.pendingAccounts) 883 | 884 | return accounts 885 | } 886 | 887 | // ForceRefreshAll 强制刷新所有账号 888 | func (p *AccountPool) ForceRefreshAll() int { 889 | p.mu.Lock() 890 | defer p.mu.Unlock() 891 | 892 | count := 0 893 | for _, acc := range p.readyAccounts { 894 | acc.Mu.Lock() 895 | acc.Refreshed = false 896 | acc.JWTExpires = time.Time{} 897 | acc.LastRefresh = time.Time{} // 强制跳过冷却 898 | acc.Mu.Unlock() 899 | p.pendingAccounts = append(p.pendingAccounts, acc) 900 | count++ 901 | } 902 | p.readyAccounts = nil 903 | 904 | log.Printf("🔄 强制刷新: %d 个账号已加入刷新队列", count) 905 | return count 906 | } 907 | 908 | func urlsafeB64Encode(data []byte) string { 909 | return strings.TrimRight(base64.URLEncoding.EncodeToString(data), "=") 910 | } 911 | 912 | func kqEncode(s string) string { 913 | var b []byte 914 | for _, ch := range s { 915 | v := int(ch) 916 | if v > 255 { 917 | b = append(b, byte(v&255), byte(v>>8)) 918 | } else { 919 | b = append(b, byte(v)) 920 | } 921 | } 922 | return urlsafeB64Encode(b) 923 | } 924 | 925 | func createJWT(keyBytes []byte, keyID, csesidx string) string { 926 | now := time.Now().Unix() 927 | header := map[string]interface{}{"alg": "HS256", "typ": "JWT", "kid": keyID} 928 | payload := map[string]interface{}{ 929 | "iss": "https://business.gemini.google", 930 | "aud": "https://biz-discoveryengine.googleapis.com", 931 | "sub": fmt.Sprintf("csesidx/%s", csesidx), 932 | "iat": now, "exp": now + 300, "nbf": now, 933 | } 934 | 935 | headerJSON, _ := json.Marshal(header) 936 | payloadJSON, _ := json.Marshal(payload) 937 | message := kqEncode(string(headerJSON)) + "." + kqEncode(string(payloadJSON)) 938 | 939 | h := hmac.New(sha256.New, keyBytes) 940 | h.Write([]byte(message)) 941 | return message + "." + urlsafeB64Encode(h.Sum(nil)) 942 | } 943 | 944 | func extractCSESIDX(auth string) string { 945 | parts := strings.Split(auth, " ") 946 | if len(parts) != 2 { 947 | return "" 948 | } 949 | jwtParts := strings.Split(parts[1], ".") 950 | if len(jwtParts) != 3 { 951 | return "" 952 | } 953 | 954 | payload, err := base64.RawURLEncoding.DecodeString(jwtParts[1]) 955 | if err != nil { 956 | return "" 957 | } 958 | 959 | var claims struct { 960 | Sub string `json:"sub"` 961 | } 962 | if err := json.Unmarshal(payload, &claims); err != nil { 963 | return "" 964 | } 965 | 966 | if strings.HasPrefix(claims.Sub, "csesidx/") { 967 | return strings.TrimPrefix(claims.Sub, "csesidx/") 968 | } 969 | return "" 970 | } 971 | 972 | // ==================== 账号操作 ==================== 973 | 974 | func (acc *Account) getCookie(name string) string { 975 | for _, c := range acc.Data.Cookies { 976 | if c.Name == name { 977 | return c.Value 978 | } 979 | } 980 | return "" 981 | } 982 | 983 | // RefreshJWT 刷新JWT 984 | func (acc *Account) RefreshJWT() error { 985 | acc.Mu.Lock() 986 | defer acc.Mu.Unlock() 987 | 988 | // 检查JWT是否仍有效 989 | if time.Now().Before(acc.JWTExpires) { 990 | return nil 991 | } 992 | 993 | // 检查刷新冷却 994 | if time.Since(acc.LastRefresh) < RefreshCooldown { 995 | return fmt.Errorf("刷新冷却中,剩余 %.0f 秒", (RefreshCooldown - time.Since(acc.LastRefresh)).Seconds()) 996 | } 997 | 998 | // 获取必要的Cookie 999 | secureSES := acc.getCookie("__Secure-C_SES") 1000 | hostOSES := acc.getCookie("__Host-C_OSES") 1001 | 1002 | // 验证Cookie是否存在 1003 | if secureSES == "" { 1004 | return fmt.Errorf("账号失效: 缺少 __Secure-C_SES Cookie") 1005 | } 1006 | 1007 | // 构建Cookie字符串 1008 | cookie := fmt.Sprintf("__Secure-C_SES=%s", secureSES) 1009 | if hostOSES != "" { 1010 | cookie += fmt.Sprintf("; __Host-C_OSES=%s", hostOSES) 1011 | } 1012 | 1013 | // 添加其他可能需要的Cookie 1014 | for _, c := range acc.Data.Cookies { 1015 | if c.Name != "__Secure-C_SES" && c.Name != "__Host-C_OSES" { 1016 | if strings.HasPrefix(c.Name, "__Secure-") || strings.HasPrefix(c.Name, "__Host-") { 1017 | cookie += fmt.Sprintf("; %s=%s", c.Name, c.Value) 1018 | } 1019 | } 1020 | } 1021 | 1022 | req, _ := http.NewRequest("GET", "https://business.gemini.google/auth/getoxsrf", nil) 1023 | q := req.URL.Query() 1024 | q.Add("csesidx", acc.CSESIDX) 1025 | req.URL.RawQuery = q.Encode() 1026 | 1027 | req.Header.Set("Cookie", cookie) 1028 | req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") 1029 | req.Header.Set("Referer", "https://business.gemini.google/") 1030 | 1031 | resp, err := HTTPClient.Do(req) 1032 | if err != nil { 1033 | return fmt.Errorf("getoxsrf 请求失败: %w", err) 1034 | } 1035 | defer resp.Body.Close() 1036 | 1037 | if resp.StatusCode != 200 { 1038 | body, _ := readResponseBody(resp) 1039 | bodyStr := string(body) 1040 | if len(bodyStr) > 200 { 1041 | bodyStr = bodyStr[:200] 1042 | } 1043 | 1044 | // 详细的错误分类 1045 | switch resp.StatusCode { 1046 | case 401, 403: 1047 | // 认证失败,Cookie可能过期 1048 | return fmt.Errorf("账号失效: %d - Cookie可能已过期", resp.StatusCode) 1049 | case 429: 1050 | // 速率限制 1051 | return fmt.Errorf("请求频率过高: %d", resp.StatusCode) 1052 | case 500, 502, 503, 504: 1053 | // 服务器错误,可能是临时的 1054 | return fmt.Errorf("服务器错误: %d, 稍后重试", resp.StatusCode) 1055 | default: 1056 | return fmt.Errorf("getoxsrf 失败: %d %s", resp.StatusCode, bodyStr) 1057 | } 1058 | } 1059 | 1060 | body, _ := readResponseBody(resp) 1061 | txt := strings.TrimPrefix(string(body), ")]}'") 1062 | txt = strings.TrimSpace(txt) 1063 | 1064 | var data struct { 1065 | XsrfToken string `json:"xsrfToken"` 1066 | KeyID string `json:"keyId"` 1067 | } 1068 | if err := json.Unmarshal([]byte(txt), &data); err != nil { 1069 | return fmt.Errorf("解析 xsrf 响应失败: %w", err) 1070 | } 1071 | 1072 | token := data.XsrfToken 1073 | switch len(token) % 4 { 1074 | case 2: 1075 | token += "==" 1076 | case 3: 1077 | token += "=" 1078 | } 1079 | keyBytes, err := base64.URLEncoding.DecodeString(token) 1080 | if err != nil { 1081 | return fmt.Errorf("解码 xsrfToken 失败: %w", err) 1082 | } 1083 | 1084 | acc.JWT = createJWT(keyBytes, data.KeyID, acc.CSESIDX) 1085 | acc.JWTExpires = time.Now().Add(JwtTTL) 1086 | acc.LastRefresh = time.Now() 1087 | 1088 | if acc.ConfigID == "" { 1089 | configID, err := acc.fetchConfigID() 1090 | if err != nil { 1091 | return fmt.Errorf("获取 configId 失败: %w", err) 1092 | } 1093 | acc.ConfigID = configID 1094 | } 1095 | return nil 1096 | } 1097 | 1098 | // GetJWT 获取JWT 1099 | func (acc *Account) GetJWT() (string, string, error) { 1100 | acc.Mu.Lock() 1101 | defer acc.Mu.Unlock() 1102 | if acc.JWT == "" { 1103 | return "", "", fmt.Errorf("JWT 为空,账号未刷新") 1104 | } 1105 | return acc.JWT, acc.ConfigID, nil 1106 | } 1107 | 1108 | func (acc *Account) fetchConfigID() (string, error) { 1109 | if acc.Data.ConfigID != "" { 1110 | return acc.Data.ConfigID, nil 1111 | } 1112 | if DefaultConfig != "" { 1113 | return DefaultConfig, nil 1114 | } 1115 | return "", fmt.Errorf("未配置 configId") 1116 | } 1117 | -------------------------------------------------------------------------------- /src/pool/pool_server.go: -------------------------------------------------------------------------------- 1 | package pool 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "os" 10 | "path/filepath" 11 | "sync" 12 | "time" 13 | 14 | "business2api/src/logger" 15 | 16 | "github.com/gorilla/websocket" 17 | ) 18 | 19 | // ==================== 号池服务器(C/S架构 + WebSocket) ==================== 20 | // Server: 管理端 - 负责API服务、账号分配、状态监控 21 | // Client: 工作端 - 负责注册新账号、401账号Cookie续期 22 | 23 | // PoolServerConfig 号池服务器配置 24 | type PoolServerConfig struct { 25 | Enable bool `json:"enable"` // 是否启用分离模式 26 | Mode string `json:"mode"` // 模式: "server" 或 "client" 27 | ServerAddr string `json:"server_addr"` // 服务器地址(客户端模式使用) 28 | ListenAddr string `json:"listen_addr"` // WebSocket监听地址(服务端模式使用) 29 | Secret string `json:"secret"` // 通信密钥 30 | TargetCount int `json:"target_count"` // 目标账号数量 31 | DataDir string `json:"data_dir"` // 数据目录 32 | ClientThreads int `json:"client_threads"` // 客户端并发线程数 33 | ExpiredAction string `json:"expired_action"` // 账号过期处理: "delete"=删除, "refresh"=浏览器刷新, "queue"=排队等待 34 | } 35 | 36 | // WSMessageType WebSocket消息类型 37 | type WSMessageType string 38 | 39 | const ( 40 | // Server -> Client 41 | WSMsgTaskRegister WSMessageType = "task_register" // 分配注册任务 42 | WSMsgTaskRefresh WSMessageType = "task_refresh" // 分配Cookie续期任务 43 | WSMsgHeartbeat WSMessageType = "heartbeat" // 心跳 44 | WSMsgStatus WSMessageType = "status" // 状态同步 45 | 46 | // Client -> Server 47 | WSMsgRegisterResult WSMessageType = "register_result" // 注册结果 48 | WSMsgRefreshResult WSMessageType = "refresh_result" // 续期结果 49 | WSMsgHeartbeatAck WSMessageType = "heartbeat_ack" // 心跳响应 50 | WSMsgClientReady WSMessageType = "client_ready" // 客户端就绪 51 | WSMsgRequestTask WSMessageType = "request_task" // 请求任务 52 | ) 53 | 54 | // 版本信息 55 | const ( 56 | ProtocolVersion = "1.0" 57 | ServerVersion = "2.0.0" 58 | ) 59 | 60 | // WSMessage WebSocket消息 61 | type WSMessage struct { 62 | Type WSMessageType `json:"type"` 63 | Version string `json:"version,omitempty"` 64 | Timestamp int64 `json:"timestamp"` 65 | Data map[string]interface{} `json:"data,omitempty"` 66 | } 67 | 68 | // WSClient WebSocket客户端连接 69 | type WSClient struct { 70 | ID string 71 | Conn *websocket.Conn 72 | Server *PoolServer 73 | Send chan []byte 74 | IsAlive bool 75 | LastPing time.Time 76 | MaxThreads int // 客户端最大线程数 77 | ClientVersion string // 客户端版本 78 | mu sync.Mutex 79 | } 80 | 81 | // PoolServer 号池服务器(管理端) 82 | type PoolServer struct { 83 | pool *AccountPool 84 | config PoolServerConfig 85 | clients map[string]*WSClient 86 | clientsMu sync.RWMutex 87 | upgrader websocket.Upgrader 88 | 89 | // 任务队列 90 | registerQueue chan int // 注册任务队列 91 | refreshQueue chan *Account // 续期任务队列 92 | 93 | // 轮询分配 94 | nextClientIdx int // 下一个分配任务的客户端索引 95 | } 96 | 97 | // NewPoolServer 创建号池服务器 98 | func NewPoolServer(pool *AccountPool, config PoolServerConfig) *PoolServer { 99 | return &PoolServer{ 100 | pool: pool, 101 | config: config, 102 | clients: make(map[string]*WSClient), 103 | upgrader: websocket.Upgrader{ 104 | CheckOrigin: func(r *http.Request) bool { return true }, 105 | ReadBufferSize: 1024, 106 | WriteBufferSize: 1024, 107 | }, 108 | registerQueue: make(chan int, 100), 109 | refreshQueue: make(chan *Account, 100), 110 | } 111 | } 112 | 113 | // Start 启动号池服务器(独立端口模式,已弃用) 114 | func (ps *PoolServer) Start() error { 115 | mux := http.NewServeMux() 116 | 117 | // 鉴权中间件 118 | authMiddleware := func(next http.HandlerFunc) http.HandlerFunc { 119 | return func(w http.ResponseWriter, r *http.Request) { 120 | if ps.config.Secret != "" { 121 | auth := r.Header.Get("X-Pool-Secret") 122 | if auth != ps.config.Secret { 123 | http.Error(w, "Unauthorized", http.StatusUnauthorized) 124 | return 125 | } 126 | } 127 | next(w, r) 128 | } 129 | } 130 | 131 | // WebSocket端点 132 | mux.HandleFunc("/ws", ps.handleWebSocket) 133 | 134 | // REST API端点 135 | mux.HandleFunc("/pool/next", authMiddleware(ps.handleNext)) 136 | mux.HandleFunc("/pool/mark", authMiddleware(ps.handleMark)) 137 | mux.HandleFunc("/pool/refresh", authMiddleware(ps.handleRefresh)) 138 | mux.HandleFunc("/pool/status", authMiddleware(ps.handleStatus)) 139 | mux.HandleFunc("/pool/jwt", authMiddleware(ps.handleGetJWT)) 140 | 141 | // 任务分发 142 | mux.HandleFunc("/pool/queue-register", authMiddleware(ps.handleQueueRegister)) 143 | mux.HandleFunc("/pool/queue-refresh", authMiddleware(ps.handleQueueRefresh)) 144 | 145 | // 接收账号数据(客户端回传) 146 | mux.HandleFunc("/pool/upload-account", authMiddleware(ps.handleUploadAccount)) 147 | 148 | // 启动任务分发协程 149 | go ps.taskDispatcher() 150 | // 启动心跳检测 151 | go ps.heartbeatChecker() 152 | return http.ListenAndServe(ps.config.ListenAddr, mux) 153 | } 154 | 155 | // StartBackground 启动后台任务(任务分发和心跳检测) 156 | func (ps *PoolServer) StartBackground() { 157 | go ps.taskDispatcher() 158 | go ps.heartbeatChecker() 159 | } 160 | 161 | // HandleWS 处理 WebSocket 连接(供 gin 路由使用) 162 | func (ps *PoolServer) HandleWS(w http.ResponseWriter, r *http.Request) { 163 | ps.handleWebSocket(w, r) 164 | } 165 | 166 | func (ps *PoolServer) HandleUploadAccount(w http.ResponseWriter, r *http.Request) { 167 | ps.handleUploadAccount(w, r) 168 | } 169 | func (ps *PoolServer) handleWebSocket(w http.ResponseWriter, r *http.Request) { 170 | if ps.config.Secret != "" { 171 | secret := r.URL.Query().Get("secret") 172 | if secret != ps.config.Secret { 173 | http.Error(w, "Unauthorized", http.StatusUnauthorized) 174 | return 175 | } 176 | } 177 | 178 | conn, err := ps.upgrader.Upgrade(w, r, nil) 179 | if err != nil { 180 | return 181 | } 182 | 183 | clientID := fmt.Sprintf("client_%d", time.Now().UnixNano()) 184 | client := &WSClient{ 185 | ID: clientID, 186 | Conn: conn, 187 | Server: ps, 188 | Send: make(chan []byte, 256), 189 | IsAlive: true, 190 | LastPing: time.Now(), 191 | } 192 | 193 | ps.clientsMu.Lock() 194 | ps.clients[clientID] = client 195 | ps.clientsMu.Unlock() 196 | 197 | logger.Info("[WS] 客户端连接: %s (当前: %d)", clientID, len(ps.clients)) 198 | 199 | // 启动读写协程 200 | go client.writePump() 201 | go client.readPump() 202 | } 203 | 204 | // writePump 发送消息到客户端 205 | func (c *WSClient) writePump() { 206 | // 缩短心跳间隔到20秒,确保连接保持活跃 207 | ticker := time.NewTicker(20 * time.Second) 208 | defer func() { 209 | ticker.Stop() 210 | c.Conn.Close() 211 | }() 212 | 213 | for { 214 | select { 215 | case message, ok := <-c.Send: 216 | if !ok { 217 | c.Conn.WriteMessage(websocket.CloseMessage, []byte{}) 218 | return 219 | } 220 | // 设置写入超时 221 | c.Conn.SetWriteDeadline(time.Now().Add(30 * time.Second)) 222 | if err := c.Conn.WriteMessage(websocket.TextMessage, message); err != nil { 223 | return 224 | } 225 | case <-ticker.C: 226 | // 发送心跳 227 | msg := WSMessage{ 228 | Type: WSMsgHeartbeat, 229 | Timestamp: time.Now().Unix(), 230 | } 231 | data, _ := json.Marshal(msg) 232 | c.Conn.SetWriteDeadline(time.Now().Add(30 * time.Second)) 233 | if err := c.Conn.WriteMessage(websocket.TextMessage, data); err != nil { 234 | logger.Debug("[WS] 发送心跳失败: %s - %v", c.ID, err) 235 | return 236 | } 237 | } 238 | } 239 | } 240 | 241 | // readPump 从客户端读取消息 242 | func (c *WSClient) readPump() { 243 | defer func() { 244 | c.Server.removeClient(c.ID) 245 | c.Conn.Close() 246 | }() 247 | 248 | // 延长读取超时到180秒(3分钟),以适应长时间注册任务 249 | c.Conn.SetReadDeadline(time.Now().Add(180 * time.Second)) 250 | c.Conn.SetPongHandler(func(string) error { 251 | c.Conn.SetReadDeadline(time.Now().Add(180 * time.Second)) 252 | return nil 253 | }) 254 | // 处理客户端的 Ping 消息,自动回复 Pong 并重置超时 255 | c.Conn.SetPingHandler(func(appData string) error { 256 | c.Conn.SetReadDeadline(time.Now().Add(180 * time.Second)) 257 | c.mu.Lock() 258 | c.LastPing = time.Now() 259 | c.mu.Unlock() 260 | return c.Conn.WriteControl(websocket.PongMessage, []byte(appData), time.Now().Add(10*time.Second)) 261 | }) 262 | 263 | for { 264 | _, message, err := c.Conn.ReadMessage() 265 | if err != nil { 266 | if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { 267 | logger.Debug("[WS] 读取错误: %v", err) 268 | } 269 | break 270 | } 271 | 272 | var msg WSMessage 273 | if err := json.Unmarshal(message, &msg); err != nil { 274 | continue 275 | } 276 | 277 | c.handleMessage(msg) 278 | } 279 | } 280 | 281 | // handleMessage 处理客户端消息 282 | func (c *WSClient) handleMessage(msg WSMessage) { 283 | c.mu.Lock() 284 | c.LastPing = time.Now() 285 | c.mu.Unlock() 286 | 287 | // 收到任何消息都重置读取超时 288 | c.Conn.SetReadDeadline(time.Now().Add(180 * time.Second)) 289 | 290 | switch msg.Type { 291 | case WSMsgHeartbeatAck: 292 | logger.Debug("[WS] 收到心跳响应: %s", c.ID) 293 | 294 | case WSMsgClientReady: 295 | if threads, ok := msg.Data["max_threads"].(float64); ok && threads > 0 { 296 | c.MaxThreads = int(threads) 297 | } else { 298 | c.MaxThreads = 1 299 | } 300 | if ver, ok := msg.Data["client_version"].(string); ok { 301 | c.ClientVersion = ver 302 | } 303 | logger.Info("[WS] 客户端 %s 就绪 (v%s, 线程:%d)", c.ID, c.ClientVersion, c.MaxThreads) 304 | c.Server.assignTask(c) 305 | 306 | case WSMsgRequestTask: 307 | logger.Debug("[WS] 客户端 %s 请求任务", c.ID) 308 | c.Server.assignTask(c) 309 | 310 | case WSMsgRegisterResult: 311 | // 注册结果 312 | c.Server.handleRegisterResult(msg.Data) 313 | 314 | case WSMsgRefreshResult: 315 | // 续期结果 316 | c.Server.handleRefreshResult(msg.Data) 317 | } 318 | } 319 | 320 | func (ps *PoolServer) removeClient(clientID string) { 321 | ps.clientsMu.Lock() 322 | defer ps.clientsMu.Unlock() 323 | if client, ok := ps.clients[clientID]; ok { 324 | close(client.Send) 325 | delete(ps.clients, clientID) 326 | logger.Info("[WS] 客户端断开: %s (剩余: %d)", clientID, len(ps.clients)) 327 | } 328 | } 329 | func (ps *PoolServer) taskDispatcher() { 330 | for { 331 | select { 332 | case count := <-ps.registerQueue: 333 | // 分发注册任务(轮询分配) 334 | ps.assignTaskRoundRobin(WSMsgTaskRegister, map[string]interface{}{ 335 | "count": count, 336 | }) 337 | 338 | case acc := <-ps.refreshQueue: 339 | // 分发续期任务(轮询分配) 340 | ps.assignTaskRoundRobin(WSMsgTaskRefresh, map[string]interface{}{ 341 | "email": acc.Data.Email, 342 | "cookies": acc.Data.Cookies, 343 | "authorization": acc.Data.Authorization, 344 | "config_id": acc.ConfigID, 345 | "csesidx": acc.CSESIDX, 346 | }) 347 | } 348 | } 349 | } 350 | 351 | // assignTaskRoundRobin 轮询分配任务给单个客户端 352 | func (ps *PoolServer) assignTaskRoundRobin(msgType WSMessageType, data map[string]interface{}) bool { 353 | msg := WSMessage{ 354 | Type: msgType, 355 | Timestamp: time.Now().Unix(), 356 | Data: data, 357 | } 358 | msgBytes, _ := json.Marshal(msg) 359 | 360 | ps.clientsMu.Lock() 361 | defer ps.clientsMu.Unlock() 362 | 363 | if len(ps.clients) == 0 { 364 | return false 365 | } 366 | 367 | // 获取客户端列表 368 | clientList := make([]*WSClient, 0, len(ps.clients)) 369 | for _, client := range ps.clients { 370 | if client.IsAlive { 371 | clientList = append(clientList, client) 372 | } 373 | } 374 | 375 | if len(clientList) == 0 { 376 | return false 377 | } 378 | 379 | // 轮询分配 380 | ps.nextClientIdx = ps.nextClientIdx % len(clientList) 381 | client := clientList[ps.nextClientIdx] 382 | ps.nextClientIdx++ 383 | 384 | select { 385 | case client.Send <- msgBytes: 386 | logger.Info("[分配] 任务 %s 分配给 %s", msgType, client.ID) 387 | return true 388 | default: 389 | // 发送队列满,尝试下一个 390 | for i := 0; i < len(clientList)-1; i++ { 391 | ps.nextClientIdx = ps.nextClientIdx % len(clientList) 392 | client = clientList[ps.nextClientIdx] 393 | ps.nextClientIdx++ 394 | select { 395 | case client.Send <- msgBytes: 396 | logger.Info("[分配] 任务 %s 分配给 %s", msgType, client.ID) 397 | return true 398 | default: 399 | continue 400 | } 401 | } 402 | } 403 | return false 404 | } 405 | func (ps *PoolServer) assignTask(client *WSClient) { 406 | maxThreads := client.MaxThreads 407 | if maxThreads <= 0 { 408 | maxThreads = 1 409 | } 410 | assignedCount := 0 411 | 412 | // 如果配置了401自动删除,直接删除待续期的401账号,不下发续期任务 413 | if AutoDelete401 { 414 | ps.pool.mu.Lock() 415 | var toDelete []*Account 416 | var remaining []*Account 417 | for _, acc := range ps.pool.pendingAccounts { 418 | if !acc.Refreshed && acc.FailCount > 0 { 419 | // 401账号,标记删除 420 | toDelete = append(toDelete, acc) 421 | } else { 422 | remaining = append(remaining, acc) 423 | } 424 | } 425 | ps.pool.pendingAccounts = remaining 426 | ps.pool.mu.Unlock() 427 | 428 | // 删除401账号文件 429 | for _, acc := range toDelete { 430 | logger.Info("🗑️ [服务端] 401自动删除账号: %s", acc.Data.Email) 431 | ps.pool.RemoveAccount(acc) 432 | } 433 | } else { 434 | // 未配置自动删除,分配续期任务给节点 435 | // 计算401最大重试次数 436 | maxRetry := MaxFailCount * 3 437 | if maxRetry < 10 { 438 | maxRetry = 10 439 | } 440 | 441 | ps.pool.mu.RLock() 442 | var refreshAccounts []*Account 443 | for _, acc := range ps.pool.pendingAccounts { 444 | if !acc.Refreshed && acc.FailCount > 0 { 445 | // 跳过已达上限的账号(浏览器刷新已达上限且401失败次数超过阈值) 446 | if acc.BrowserRefreshCount >= BrowserRefreshMaxRetry && acc.FailCount >= maxRetry { 447 | continue 448 | } 449 | refreshAccounts = append(refreshAccounts, acc) 450 | if len(refreshAccounts) >= maxThreads { 451 | break 452 | } 453 | } 454 | } 455 | ps.pool.mu.RUnlock() 456 | for _, acc := range refreshAccounts { 457 | logger.Info("[WS] 分配续期任务给 %s: %s", client.ID, acc.Data.Email) 458 | msg := WSMessage{ 459 | Type: WSMsgTaskRefresh, 460 | Timestamp: time.Now().Unix(), 461 | Data: map[string]interface{}{ 462 | "email": acc.Data.Email, 463 | "cookies": acc.Data.Cookies, 464 | "authorization": acc.Data.Authorization, 465 | "config_id": acc.ConfigID, 466 | "csesidx": acc.CSESIDX, 467 | }, 468 | } 469 | msgBytes, _ := json.Marshal(msg) 470 | select { 471 | case client.Send <- msgBytes: 472 | assignedCount++ 473 | default: 474 | } 475 | } 476 | } 477 | remainingSlots := maxThreads - assignedCount 478 | if remainingSlots > 0 { 479 | currentCount := ps.pool.TotalCount() 480 | targetCount := ps.config.TargetCount 481 | needCount := targetCount - currentCount 482 | 483 | if needCount > 0 { 484 | registerCount := remainingSlots 485 | if registerCount > needCount { 486 | registerCount = needCount 487 | } 488 | 489 | logger.Info("[WS] 分配注册任务给 %s: %d个 (当前: %d, 目标: %d, 线程: %d)", 490 | client.ID, registerCount, currentCount, targetCount, maxThreads) 491 | for i := 0; i < registerCount; i++ { 492 | msg := WSMessage{ 493 | Type: WSMsgTaskRegister, 494 | Timestamp: time.Now().Unix(), 495 | Data: map[string]interface{}{ 496 | "count": 1, 497 | }, 498 | } 499 | msgBytes, _ := json.Marshal(msg) 500 | select { 501 | case client.Send <- msgBytes: 502 | assignedCount++ 503 | default: 504 | } 505 | } 506 | } 507 | } 508 | 509 | if assignedCount == 0 { 510 | logger.Debug("[WS] 无任务需要分配给 %s", client.ID) 511 | } 512 | } 513 | 514 | // heartbeatChecker 心跳检测 515 | func (ps *PoolServer) heartbeatChecker() { 516 | ticker := time.NewTicker(60 * time.Second) 517 | defer ticker.Stop() 518 | 519 | for range ticker.C { 520 | ps.clientsMu.RLock() 521 | for id, client := range ps.clients { 522 | client.mu.Lock() 523 | if time.Since(client.LastPing) > 180*time.Second { 524 | client.IsAlive = false 525 | logger.Warn("[WS] 客户端 %s 心跳超时 (last: %v ago)", id, time.Since(client.LastPing)) 526 | } 527 | client.mu.Unlock() 528 | } 529 | ps.clientsMu.RUnlock() 530 | } 531 | } 532 | 533 | func (ps *PoolServer) handleRegisterResult(data map[string]interface{}) { 534 | success, _ := data["success"].(bool) 535 | email, _ := data["email"].(string) 536 | 537 | if success { 538 | logger.Info("✅ 注册成功: %s", email) 539 | // 重新加载账号 540 | ps.pool.Load(ps.config.DataDir) 541 | } else { 542 | errMsg, _ := data["error"].(string) 543 | logger.Warn("❌ 注册失败: %s", errMsg) 544 | } 545 | } 546 | func (ps *PoolServer) handleRefreshResult(data map[string]interface{}) { 547 | email, _ := data["email"].(string) 548 | success, _ := data["success"].(bool) 549 | 550 | if success { 551 | logger.Info("✅ 账号续期成功: %s", email) 552 | // 更新账号数据 553 | if cookiesData, ok := data["cookies"]; ok { 554 | ps.updateAccountCookies(email, cookiesData) 555 | } 556 | } else { 557 | errMsg, _ := data["error"].(string) 558 | logger.Warn("❌ 账号续期失败 %s: %s", email, errMsg) 559 | action := ps.config.ExpiredAction 560 | if action == "" { 561 | action = "delete" // 默认删除 562 | } 563 | 564 | switch action { 565 | case "delete": 566 | ps.deleteAccount(email) 567 | case "queue": 568 | // 保持在队列中,不做处理 569 | case "refresh": 570 | default: 571 | ps.deleteAccount(email) 572 | } 573 | } 574 | } 575 | 576 | // deleteAccount 删除账号 577 | func (ps *PoolServer) deleteAccount(email string) { 578 | ps.pool.mu.Lock() 579 | defer ps.pool.mu.Unlock() 580 | 581 | // 从 pending 队列删除 582 | for i, acc := range ps.pool.pendingAccounts { 583 | if acc.Data.Email == email { 584 | // 删除文件 585 | if acc.FilePath != "" { 586 | os.Remove(acc.FilePath) 587 | } 588 | ps.pool.pendingAccounts = append(ps.pool.pendingAccounts[:i], ps.pool.pendingAccounts[i+1:]...) 589 | logger.Info("🗑️ 已删除续期失败账号: %s", email) 590 | return 591 | } 592 | } 593 | 594 | // 从 ready 队列删除 595 | for i, acc := range ps.pool.readyAccounts { 596 | if acc.Data.Email == email { 597 | if acc.FilePath != "" { 598 | os.Remove(acc.FilePath) 599 | } 600 | ps.pool.readyAccounts = append(ps.pool.readyAccounts[:i], ps.pool.readyAccounts[i+1:]...) 601 | logger.Info("🗑️ 已删除续期失败账号: %s", email) 602 | return 603 | } 604 | } 605 | } 606 | 607 | // updateAccountCookies 更新账号Cookie 608 | func (ps *PoolServer) updateAccountCookies(email string, cookiesData interface{}) { 609 | ps.pool.mu.Lock() 610 | defer ps.pool.mu.Unlock() 611 | 612 | for _, acc := range ps.pool.readyAccounts { 613 | if acc.Data.Email == email { 614 | // 更新cookies 615 | if cookies, ok := cookiesData.([]interface{}); ok { 616 | var newCookies []Cookie 617 | for _, c := range cookies { 618 | if cm, ok := c.(map[string]interface{}); ok { 619 | newCookies = append(newCookies, Cookie{ 620 | Name: cm["name"].(string), 621 | Value: cm["value"].(string), 622 | Domain: cm["domain"].(string), 623 | }) 624 | } 625 | } 626 | acc.Data.Cookies = newCookies 627 | acc.Refreshed = true 628 | acc.FailCount = 0 629 | acc.SaveToFile() 630 | } 631 | return 632 | } 633 | } 634 | } 635 | 636 | // handleQueueRegister 队列注册任务 637 | func (ps *PoolServer) handleQueueRegister(w http.ResponseWriter, r *http.Request) { 638 | var req struct { 639 | Count int `json:"count"` 640 | } 641 | if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Count <= 0 { 642 | req.Count = 1 643 | } 644 | 645 | select { 646 | case ps.registerQueue <- req.Count: 647 | json.NewEncoder(w).Encode(map[string]interface{}{ 648 | "success": true, 649 | "message": fmt.Sprintf("已添加 %d 个注册任务到队列", req.Count), 650 | }) 651 | default: 652 | json.NewEncoder(w).Encode(map[string]interface{}{ 653 | "success": false, 654 | "error": "任务队列已满", 655 | }) 656 | } 657 | } 658 | 659 | // handleQueueRefresh 队列续期任务 660 | func (ps *PoolServer) handleQueueRefresh(w http.ResponseWriter, r *http.Request) { 661 | var req struct { 662 | Email string `json:"email"` 663 | } 664 | if err := json.NewDecoder(r.Body).Decode(&req); err != nil { 665 | http.Error(w, err.Error(), http.StatusBadRequest) 666 | return 667 | } 668 | 669 | // 查找账号 670 | ps.pool.mu.RLock() 671 | var targetAcc *Account 672 | for _, acc := range ps.pool.readyAccounts { 673 | if acc.Data.Email == req.Email { 674 | targetAcc = acc 675 | break 676 | } 677 | } 678 | ps.pool.mu.RUnlock() 679 | 680 | if targetAcc == nil { 681 | json.NewEncoder(w).Encode(map[string]interface{}{ 682 | "success": false, 683 | "error": "账号未找到", 684 | }) 685 | return 686 | } 687 | 688 | select { 689 | case ps.refreshQueue <- targetAcc: 690 | json.NewEncoder(w).Encode(map[string]interface{}{ 691 | "success": true, 692 | "message": fmt.Sprintf("已添加账号 %s 续期任务到队列", req.Email), 693 | }) 694 | default: 695 | json.NewEncoder(w).Encode(map[string]interface{}{ 696 | "success": false, 697 | "error": "任务队列已满", 698 | }) 699 | } 700 | } 701 | 702 | // AccountResponse 账号响应 703 | type AccountResponse struct { 704 | Success bool `json:"success"` 705 | Email string `json:"email,omitempty"` 706 | JWT string `json:"jwt,omitempty"` 707 | ConfigID string `json:"config_id,omitempty"` 708 | Authorization string `json:"authorization,omitempty"` 709 | Error string `json:"error,omitempty"` 710 | } 711 | 712 | func (ps *PoolServer) handleNext(w http.ResponseWriter, r *http.Request) { 713 | acc := ps.pool.Next() 714 | if acc == nil { 715 | json.NewEncoder(w).Encode(AccountResponse{ 716 | Success: false, 717 | Error: "没有可用账号", 718 | }) 719 | return 720 | } 721 | 722 | jwt, configID, err := acc.GetJWT() 723 | if err != nil { 724 | json.NewEncoder(w).Encode(AccountResponse{ 725 | Success: false, 726 | Email: acc.Data.Email, 727 | Error: err.Error(), 728 | }) 729 | return 730 | } 731 | 732 | json.NewEncoder(w).Encode(AccountResponse{ 733 | Success: true, 734 | Email: acc.Data.Email, 735 | JWT: jwt, 736 | ConfigID: configID, 737 | Authorization: acc.Data.Authorization, 738 | }) 739 | } 740 | 741 | func (ps *PoolServer) handleMark(w http.ResponseWriter, r *http.Request) { 742 | var req struct { 743 | Email string `json:"email"` 744 | Success bool `json:"success"` 745 | } 746 | if err := json.NewDecoder(r.Body).Decode(&req); err != nil { 747 | http.Error(w, err.Error(), http.StatusBadRequest) 748 | return 749 | } 750 | 751 | // 查找账号并标记 752 | ps.pool.mu.RLock() 753 | var targetAcc *Account 754 | for _, acc := range ps.pool.readyAccounts { 755 | if acc.Data.Email == req.Email { 756 | targetAcc = acc 757 | break 758 | } 759 | } 760 | ps.pool.mu.RUnlock() 761 | 762 | if targetAcc != nil { 763 | ps.pool.MarkUsed(targetAcc, req.Success) 764 | } 765 | 766 | json.NewEncoder(w).Encode(map[string]bool{"success": true}) 767 | } 768 | 769 | func (ps *PoolServer) handleRefresh(w http.ResponseWriter, r *http.Request) { 770 | var req struct { 771 | Email string `json:"email"` 772 | } 773 | if err := json.NewDecoder(r.Body).Decode(&req); err != nil { 774 | http.Error(w, err.Error(), http.StatusBadRequest) 775 | return 776 | } 777 | 778 | // 查找账号并标记需要刷新 779 | ps.pool.mu.RLock() 780 | var targetAcc *Account 781 | for _, acc := range ps.pool.readyAccounts { 782 | if acc.Data.Email == req.Email { 783 | targetAcc = acc 784 | break 785 | } 786 | } 787 | ps.pool.mu.RUnlock() 788 | 789 | if targetAcc != nil { 790 | ps.pool.MarkNeedsRefresh(targetAcc) 791 | } 792 | 793 | json.NewEncoder(w).Encode(map[string]bool{"success": true}) 794 | } 795 | 796 | func (ps *PoolServer) handleStatus(w http.ResponseWriter, r *http.Request) { 797 | json.NewEncoder(w).Encode(ps.pool.Stats()) 798 | } 799 | 800 | func (ps *PoolServer) handleGetJWT(w http.ResponseWriter, r *http.Request) { 801 | email := r.URL.Query().Get("email") 802 | if email == "" { 803 | http.Error(w, "缺少email参数", http.StatusBadRequest) 804 | return 805 | } 806 | 807 | ps.pool.mu.RLock() 808 | var targetAcc *Account 809 | for _, acc := range ps.pool.readyAccounts { 810 | if acc.Data.Email == email { 811 | targetAcc = acc 812 | break 813 | } 814 | } 815 | ps.pool.mu.RUnlock() 816 | 817 | if targetAcc == nil { 818 | json.NewEncoder(w).Encode(AccountResponse{ 819 | Success: false, 820 | Error: "账号未找到", 821 | }) 822 | return 823 | } 824 | 825 | jwt, configID, err := targetAcc.GetJWT() 826 | if err != nil { 827 | json.NewEncoder(w).Encode(AccountResponse{ 828 | Success: false, 829 | Email: email, 830 | Error: err.Error(), 831 | }) 832 | return 833 | } 834 | 835 | json.NewEncoder(w).Encode(AccountResponse{ 836 | Success: true, 837 | Email: email, 838 | JWT: jwt, 839 | ConfigID: configID, 840 | Authorization: targetAcc.Data.Authorization, 841 | }) 842 | } 843 | 844 | // ==================== 远程号池客户端 ==================== 845 | 846 | // RemotePoolClient 远程号池客户端 847 | type RemotePoolClient struct { 848 | serverAddr string 849 | secret string 850 | client *http.Client 851 | mu sync.RWMutex 852 | // 本地缓存 853 | cachedAccounts map[string]*CachedAccount 854 | } 855 | 856 | // CachedAccount 缓存的账号信息 857 | type CachedAccount struct { 858 | Email string 859 | JWT string 860 | ConfigID string 861 | Authorization string 862 | FetchedAt time.Time 863 | } 864 | 865 | // NewRemotePoolClient 创建远程号池客户端 866 | func NewRemotePoolClient(serverAddr, secret string) *RemotePoolClient { 867 | return &RemotePoolClient{ 868 | serverAddr: serverAddr, 869 | secret: secret, 870 | client: &http.Client{ 871 | Timeout: 30 * time.Second, 872 | }, 873 | cachedAccounts: make(map[string]*CachedAccount), 874 | } 875 | } 876 | 877 | // doRequest 发送请求到号池服务器 878 | func (rc *RemotePoolClient) doRequest(method, path string, body interface{}) (*http.Response, error) { 879 | var reqBody io.Reader 880 | if body != nil { 881 | data, err := json.Marshal(body) 882 | if err != nil { 883 | return nil, err 884 | } 885 | reqBody = bytes.NewReader(data) 886 | } 887 | 888 | req, err := http.NewRequest(method, rc.serverAddr+path, reqBody) 889 | if err != nil { 890 | return nil, err 891 | } 892 | 893 | req.Header.Set("Content-Type", "application/json") 894 | if rc.secret != "" { 895 | req.Header.Set("X-Pool-Secret", rc.secret) 896 | } 897 | 898 | return rc.client.Do(req) 899 | } 900 | 901 | // Next 获取下一个可用账号 902 | func (rc *RemotePoolClient) Next() (*CachedAccount, error) { 903 | resp, err := rc.doRequest("GET", "/pool/next", nil) 904 | if err != nil { 905 | return nil, fmt.Errorf("请求号池服务器失败: %w", err) 906 | } 907 | defer resp.Body.Close() 908 | 909 | var result AccountResponse 910 | if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { 911 | return nil, fmt.Errorf("解析响应失败: %w", err) 912 | } 913 | 914 | if !result.Success { 915 | return nil, fmt.Errorf("%s", result.Error) 916 | } 917 | 918 | acc := &CachedAccount{ 919 | Email: result.Email, 920 | JWT: result.JWT, 921 | ConfigID: result.ConfigID, 922 | Authorization: result.Authorization, 923 | FetchedAt: time.Now(), 924 | } 925 | 926 | // 缓存账号 927 | rc.mu.Lock() 928 | rc.cachedAccounts[result.Email] = acc 929 | rc.mu.Unlock() 930 | 931 | return acc, nil 932 | } 933 | 934 | // MarkUsed 标记账号使用结果 935 | func (rc *RemotePoolClient) MarkUsed(email string, success bool) error { 936 | resp, err := rc.doRequest("POST", "/pool/mark", map[string]interface{}{ 937 | "email": email, 938 | "success": success, 939 | }) 940 | if err != nil { 941 | return err 942 | } 943 | resp.Body.Close() 944 | return nil 945 | } 946 | 947 | // MarkNeedsRefresh 标记账号需要刷新 948 | func (rc *RemotePoolClient) MarkNeedsRefresh(email string) error { 949 | resp, err := rc.doRequest("POST", "/pool/refresh", map[string]interface{}{ 950 | "email": email, 951 | }) 952 | if err != nil { 953 | return err 954 | } 955 | resp.Body.Close() 956 | return nil 957 | } 958 | 959 | // GetStatus 获取号池状态 960 | func (rc *RemotePoolClient) GetStatus() (map[string]interface{}, error) { 961 | resp, err := rc.doRequest("GET", "/pool/status", nil) 962 | if err != nil { 963 | return nil, err 964 | } 965 | defer resp.Body.Close() 966 | 967 | var result map[string]interface{} 968 | if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { 969 | return nil, err 970 | } 971 | return result, nil 972 | } 973 | 974 | // RefreshJWT 刷新指定账号的JWT 975 | func (rc *RemotePoolClient) RefreshJWT(email string) (*CachedAccount, error) { 976 | resp, err := rc.doRequest("GET", "/pool/jwt?email="+email, nil) 977 | if err != nil { 978 | return nil, err 979 | } 980 | defer resp.Body.Close() 981 | 982 | var result AccountResponse 983 | if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { 984 | return nil, err 985 | } 986 | 987 | if !result.Success { 988 | return nil, fmt.Errorf("%s", result.Error) 989 | } 990 | 991 | acc := &CachedAccount{ 992 | Email: result.Email, 993 | JWT: result.JWT, 994 | ConfigID: result.ConfigID, 995 | Authorization: result.Authorization, 996 | FetchedAt: time.Now(), 997 | } 998 | 999 | rc.mu.Lock() 1000 | rc.cachedAccounts[email] = acc 1001 | rc.mu.Unlock() 1002 | 1003 | return acc, nil 1004 | } 1005 | 1006 | type AccountUploadRequest struct { 1007 | Email string `json:"email"` 1008 | FullName string `json:"full_name"` 1009 | Cookies []Cookie `json:"cookies"` 1010 | CookieString string `json:"cookie_string"` 1011 | Authorization string `json:"authorization"` 1012 | ConfigID string `json:"config_id"` 1013 | CSESIDX string `json:"csesidx"` 1014 | IsNew bool `json:"is_new"` 1015 | } 1016 | 1017 | // handleUploadAccount 处理账号上传(客户端回传鉴权文件) 1018 | func (ps *PoolServer) handleUploadAccount(w http.ResponseWriter, r *http.Request) { 1019 | if r.Method != http.MethodPost { 1020 | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) 1021 | return 1022 | } 1023 | 1024 | var req AccountUploadRequest 1025 | if err := json.NewDecoder(r.Body).Decode(&req); err != nil { 1026 | logger.Error("解析账号上传请求失败: %v", err) 1027 | json.NewEncoder(w).Encode(map[string]interface{}{ 1028 | "success": false, 1029 | "error": "无效的请求格式", 1030 | }) 1031 | return 1032 | } 1033 | 1034 | if req.Email == "" { 1035 | json.NewEncoder(w).Encode(map[string]interface{}{ 1036 | "success": false, 1037 | "error": "邮箱不能为空", 1038 | }) 1039 | return 1040 | } 1041 | 1042 | // 构建账号数据 1043 | accData := AccountData{ 1044 | Email: req.Email, 1045 | FullName: req.FullName, 1046 | Cookies: req.Cookies, 1047 | CookieString: req.CookieString, 1048 | Authorization: req.Authorization, 1049 | ConfigID: req.ConfigID, 1050 | CSESIDX: req.CSESIDX, 1051 | Timestamp: time.Now().Format(time.RFC3339), 1052 | } 1053 | 1054 | // 保存到文件 1055 | dataDir := ps.config.DataDir 1056 | if dataDir == "" { 1057 | dataDir = "./data" 1058 | } 1059 | 1060 | // 确保目录存在 1061 | if err := os.MkdirAll(dataDir, 0755); err != nil { 1062 | logger.Error("创建数据目录失败: %v", err) 1063 | json.NewEncoder(w).Encode(map[string]interface{}{ 1064 | "success": false, 1065 | "error": "服务器内部错误", 1066 | }) 1067 | return 1068 | } 1069 | 1070 | // 生成文件名 1071 | filename := fmt.Sprintf("%s.json", req.Email) 1072 | filePath := filepath.Join(dataDir, filename) 1073 | 1074 | // 序列化并保存 1075 | data, err := json.MarshalIndent(accData, "", " ") 1076 | if err != nil { 1077 | logger.Error("序列化账号数据失败: %v", err) 1078 | json.NewEncoder(w).Encode(map[string]interface{}{ 1079 | "success": false, 1080 | "error": "序列化失败", 1081 | }) 1082 | return 1083 | } 1084 | 1085 | if err := os.WriteFile(filePath, data, 0644); err != nil { 1086 | logger.Error("保存账号文件失败: %v", err) 1087 | json.NewEncoder(w).Encode(map[string]interface{}{ 1088 | "success": false, 1089 | "error": "保存失败", 1090 | }) 1091 | return 1092 | } 1093 | 1094 | if req.IsNew { 1095 | logger.Info("✅ 收到新注册账号: %s", req.Email) 1096 | } else { 1097 | logger.Info("✅ 收到账号续期数据: %s", req.Email) 1098 | } 1099 | 1100 | // 先加载文件确保账号存在 1101 | ps.pool.Load(dataDir) 1102 | 1103 | // 更新内存中的账号数据 1104 | ps.pool.mu.Lock() 1105 | found := false 1106 | 1107 | // 查找并更新 pending 队列 1108 | for i, acc := range ps.pool.pendingAccounts { 1109 | if acc.Data.Email == req.Email { 1110 | acc.Data.Cookies = req.Cookies 1111 | acc.Data.CookieString = req.CookieString 1112 | acc.Data.Authorization = req.Authorization 1113 | acc.Data.ConfigID = req.ConfigID 1114 | acc.Data.CSESIDX = req.CSESIDX 1115 | acc.ConfigID = req.ConfigID 1116 | acc.CSESIDX = req.CSESIDX 1117 | acc.Refreshed = true 1118 | acc.FailCount = 0 1119 | acc.BrowserRefreshCount = 0 1120 | acc.LastRefresh = time.Now() 1121 | acc.JWTExpires = time.Time{} 1122 | // 从 pending 移除 1123 | ps.pool.pendingAccounts = append(ps.pool.pendingAccounts[:i], ps.pool.pendingAccounts[i+1:]...) 1124 | ps.pool.mu.Unlock() 1125 | // 加入 ready 队列 1126 | ps.pool.MarkReady(acc) 1127 | found = true 1128 | goto respond 1129 | } 1130 | } 1131 | 1132 | // 查找并更新 ready 队列 1133 | for _, acc := range ps.pool.readyAccounts { 1134 | if acc.Data.Email == req.Email { 1135 | acc.Mu.Lock() 1136 | acc.Data.Cookies = req.Cookies 1137 | acc.Data.CookieString = req.CookieString 1138 | acc.Data.Authorization = req.Authorization 1139 | acc.Data.ConfigID = req.ConfigID 1140 | acc.Data.CSESIDX = req.CSESIDX 1141 | acc.ConfigID = req.ConfigID 1142 | acc.CSESIDX = req.CSESIDX 1143 | acc.FailCount = 0 1144 | acc.BrowserRefreshCount = 0 1145 | acc.LastRefresh = time.Now() 1146 | acc.JWTExpires = time.Time{} 1147 | acc.Mu.Unlock() 1148 | found = true 1149 | break 1150 | } 1151 | } 1152 | ps.pool.mu.Unlock() 1153 | 1154 | if !found { 1155 | logger.Warn("⚠️ [%s] 账号已保存但未在内存中找到", req.Email) 1156 | } 1157 | 1158 | respond: 1159 | 1160 | json.NewEncoder(w).Encode(map[string]interface{}{ 1161 | "success": true, 1162 | "message": fmt.Sprintf("账号 %s 已保存", req.Email), 1163 | }) 1164 | } 1165 | 1166 | func (rc *RemotePoolClient) UploadAccount(acc *AccountUploadRequest) error { 1167 | data, err := json.Marshal(acc) 1168 | if err != nil { 1169 | return err 1170 | } 1171 | 1172 | resp, err := rc.doRequest("POST", "/pool/upload-account", bytes.NewReader(data)) 1173 | if err != nil { 1174 | return err 1175 | } 1176 | defer resp.Body.Close() 1177 | 1178 | var result map[string]interface{} 1179 | if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { 1180 | return err 1181 | } 1182 | 1183 | if success, ok := result["success"].(bool); !ok || !success { 1184 | errMsg, _ := result["error"].(string) 1185 | return fmt.Errorf("上传失败: %s", errMsg) 1186 | } 1187 | return nil 1188 | } 1189 | 1190 | type ClientInfo struct { 1191 | ID string `json:"id"` 1192 | Version string `json:"version"` 1193 | Threads int `json:"threads"` 1194 | IsAlive bool `json:"is_alive"` 1195 | LastPing int64 `json:"last_ping"` 1196 | } 1197 | 1198 | func (ps *PoolServer) GetClientsInfo() []ClientInfo { 1199 | ps.clientsMu.RLock() 1200 | defer ps.clientsMu.RUnlock() 1201 | 1202 | clients := make([]ClientInfo, 0, len(ps.clients)) 1203 | for id, c := range ps.clients { 1204 | clients = append(clients, ClientInfo{ 1205 | ID: id, 1206 | Version: c.ClientVersion, 1207 | Threads: c.MaxThreads, 1208 | IsAlive: c.IsAlive, 1209 | LastPing: c.LastPing.Unix(), 1210 | }) 1211 | } 1212 | return clients 1213 | } 1214 | func (ps *PoolServer) GetClientCount() int { 1215 | ps.clientsMu.RLock() 1216 | defer ps.clientsMu.RUnlock() 1217 | return len(ps.clients) 1218 | } 1219 | 1220 | func (ps *PoolServer) GetTotalThreads() int { 1221 | ps.clientsMu.RLock() 1222 | defer ps.clientsMu.RUnlock() 1223 | total := 0 1224 | for _, c := range ps.clients { 1225 | total += c.MaxThreads 1226 | } 1227 | return total 1228 | } 1229 | --------------------------------------------------------------------------------