Repository: XxxXTeam/business2api Branch: master Commit: 4218208432a1 Files: 23 Total size: 404.7 KB Directory structure: gitextract_zttsb6rv/ ├── .dockerignore ├── .github/ │ ├── dependabot.yml │ └── workflows/ │ ├── build.yml │ └── codeql.yml ├── .gitignore ├── Dockerfile ├── README.md ├── config/ │ ├── README.md │ └── config.json.example ├── docker/ │ └── docker-compose.yml ├── go.mod ├── go.sum ├── main.go └── src/ ├── api/ │ └── api.go ├── logger/ │ └── logger.go ├── pool/ │ ├── pool.go │ ├── pool_client.go │ └── pool_server.go ├── proxy/ │ ├── proxy.go │ └── singbox.go ├── register/ │ ├── browser.go │ └── register.go └── utils/ └── utils.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ # Git .git .gitignore # Data data/ # Node.js files (no longer needed - registration is pure Go) node_modules/ package-lock.json package.json main.js # IDE .idea/ .vscode/ # Build artifacts *.exe *.dll *.so *.dylib # Documentation README.md LICENSE # GitHub .github/ ================================================ FILE: .github/dependabot.yml ================================================ # To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file version: 2 updates: - package-ecosystem: "" # See documentation for possible values directory: "/" # Location of package manifests schedule: interval: "weekly" ================================================ FILE: .github/workflows/build.yml ================================================ name: Build and Release on: push: branches: [main, master] tags: - 'v*' pull_request: branches: [main, master] workflow_dispatch: env: GO_VERSION: '1.23' REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} jobs: build: runs-on: ubuntu-latest strategy: matrix: goos: [linux, darwin, windows] goarch: [amd64, arm64] exclude: - goos: windows goarch: arm64 steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup Go uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} cache: false - name: Get version id: version run: | if [[ "$GITHUB_REF" == refs/tags/v* ]]; then VERSION=${GITHUB_REF#refs/tags/v} else VERSION=$(git describe --tags --always --dirty 2>/dev/null || echo "dev") fi echo "VERSION=$VERSION" >> $GITHUB_OUTPUT echo "Building version: $VERSION" - name: Build env: GOOS: ${{ matrix.goos }} GOARCH: ${{ matrix.goarch }} VERSION: ${{ steps.version.outputs.VERSION }} GOTOOLCHAIN: auto run: | EXT="" if [ "$GOOS" = "windows" ]; then EXT=".exe" fi CGO_ENABLED=0 go build \ -tags "with_quic,with_utls" \ -ldflags="-s -w -X main.Version=${VERSION}" \ -o business2api-${{ matrix.goos }}-${{ matrix.goarch }}${EXT} . - name: Prepare package run: | mkdir -p dist mv business2api-* dist/ cp config/config.json.example dist/ cp README.md dist/ - name: Upload Artifact uses: actions/upload-artifact@v4 with: name: business2api-${{ matrix.goos }}-${{ matrix.goarch }} path: dist/* retention-days: 30 docker: runs-on: ubuntu-latest needs: build if: github.event_name == 'push' permissions: contents: read packages: write steps: - name: Checkout uses: actions/checkout@v4 - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to GitHub Container Registry uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata id: meta uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | type=ref,event=branch type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=sha,prefix= - name: Build and push uses: docker/build-push-action@v5 with: context: . platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max release: runs-on: ubuntu-latest needs: build if: startsWith(github.ref, 'refs/tags/v') permissions: contents: write steps: - name: Download all artifacts uses: actions/download-artifact@v4 with: path: artifacts - name: Prepare release files run: | mkdir -p release for dir in artifacts/business2api-*/; do # 获取平台名称 platform=$(basename "$dir") # 创建临时目录 mkdir -p "tmp/$platform" cp "$dir"/* "tmp/$platform/" # 设置可执行权限 for f in "tmp/$platform"/business2api-*; do if [[ ! "$f" == *.exe ]]; then chmod +x "$f" fi done # 打包 tar -czvf "release/${platform}.tar.gz" -C tmp "$platform" rm -rf "tmp/$platform" done rm -rf tmp - name: Create Release uses: softprops/action-gh-release@v1 with: files: release/* generate_release_notes: true ================================================ FILE: .github/workflows/codeql.yml ================================================ # For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. # # You may wish to alter this file to override the set of languages analyzed, # or to provide custom queries or build logic. # # ******** NOTE ******** # We have attempted to detect the languages in your repository. Please check # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # name: "CodeQL Advanced" on: push: branches: [ "master" ] pull_request: branches: [ "master" ] schedule: - cron: '33 14 * * 3' jobs: analyze: name: Analyze (${{ matrix.language }}) # Runner size impacts CodeQL analysis time. To learn more, please see: # - https://gh.io/recommended-hardware-resources-for-running-codeql # - https://gh.io/supported-runners-and-hardware-resources # - https://gh.io/using-larger-runners (GitHub.com only) # Consider using larger runners or machines with greater resources for possible analysis time improvements. runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} permissions: # required for all workflows security-events: write # required to fetch internal or private CodeQL packs packages: read # only required for workflows in private repositories actions: read contents: read strategy: fail-fast: false matrix: include: - language: actions build-mode: none - language: go build-mode: autobuild # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift' # Use `c-cpp` to analyze code written in C, C++ or both # Use 'java-kotlin' to analyze code written in Java, Kotlin or both # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how # 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 steps: - name: Checkout repository uses: actions/checkout@v4 # Add any setup steps before running the `github/codeql-action/init` action. # This includes steps like installing compilers or runtimes (`actions/setup-node` # or others). This is typically only required for manual builds. # - name: Setup runtime (example) # uses: actions/setup-example@v1 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v4 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # 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 # queries: security-extended,security-and-quality # If the analyze step fails for one of the languages you are analyzing with # "We were unable to automatically build your code", modify the matrix above # to set the build mode to "manual" for that language. Then modify this step # to build your code. # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - name: Run manual build steps if: matrix.build-mode == 'manual' shell: bash run: | echo 'If you are using a "manual" build mode for one or more of the' \ 'languages you are analyzing, replace this with the commands to build' \ 'your code, for example:' echo ' make bootstrap' echo ' make release' exit 1 - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v4 with: category: "/language:${{matrix.language}}" ================================================ FILE: .gitignore ================================================ # Binaries gemini-gateway gemini-gateway.exe *.exe *.dll *.so *.dylib # Data data/ *.json !config.json.example !package.json # Node node_modules/ npm-debug.log* # IDE .idea/ .vscode/ *.swp *.swo # OS *.zip .DS_Store Thumbs.db # Build dist/ build/ data/ config.json package-lock.json main.js package.json flow.txt config/config.json business2api flow/ ================================================ FILE: Dockerfile ================================================ # Build stage FROM golang:1.23-alpine AS builder WORKDIR /app # Install build dependencies RUN apk add --no-cache git ca-certificates # Copy go mod files COPY go.mod go.sum ./ ENV GOTOOLCHAIN=auto RUN go mod download # Copy source code COPY *.go ./ COPY src/ ./src/ # Build binary RUN CGO_ENABLED=0 GOOS=linux go build -tags "with_quic,with_utls" -ldflags="-s -w" -o business2api . # Runtime stage FROM alpine:latest WORKDIR /app # Install runtime dependencies (Chromium for rod browser automation) RUN apk add --no-cache \ ca-certificates \ tzdata \ chromium \ nss \ freetype \ harfbuzz \ ttf-freefont \ font-noto-cjk # Copy binary from builder COPY --from=builder /app/business2api . # Copy config template if exists COPY config.json.exampl[e] ./ # Create data directory RUN mkdir -p /app/data # Environment variables ENV LISTEN_ADDR=":8000" ENV DATA_DIR="/app/data" EXPOSE 8000 ENTRYPOINT ["./business2api"] ================================================ FILE: README.md ================================================ # Business2API > 🚀 OpenAI/Gemini 兼容的 Gemini Business API 代理服务,支持账号池管理、自动注册和 Flow 图片/视频生成。 [![Build](https://github.com/XxxXTeam/business2api/actions/workflows/build.yml/badge.svg)](https://github.com/XxxXTeam/business2api/actions/workflows/build.yml) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![Go Version](https://img.shields.io/badge/Go-1.24+-00ADD8?logo=go)](https://golang.org) ## ✨ 功能特性 | 功能 | 描述 | |------|------| | 🔌 **多 API 兼容** | OpenAI (`/v1/chat/completions`)、Gemini (`/v1beta/models`)、Claude (`/v1/messages`) | | 🏊 **智能账号池** | 自动轮询、刷新、冷却管理、401/403 自动换号 | | 🌊 **流式响应** | SSE 流式输出,支持 `stream: true` | | 🎨 **多模态** | 图片/视频输入、原生图片生成(`-image` 后缀)| | 🤖 **自动注册** | 浏览器自动化注册,支持 Windows/Linux/macOS | | 🌐 **代理池** | HTTP/SOCKS5 代理,订阅链接,健康检查 | | 📊 **遥测监控** | IP 请求统计、Token 使用量、RPM 监控 | | 🔄 **热重载** | 配置文件自动监听,无需重启 | ## 📦 支持的模型 ### Gemini Business 模型 | 模型 | 文本 | 图片生成 | 视频生成 | 搜索 | |------|:----:|:--------:|:--------:|:----:| | gemini-2.5-flash | ✅ | ✅ | ✅ | ✅ | | gemini-2.5-pro | ✅ | ✅ | ✅ | ✅ | | gemini-2.5-flash-preview-latest | ✅ | ✅ | ✅ | ✅ | | gemini-3-pro-preview | ✅ | ✅ | ✅ | ✅ | | gemini-3-flash-preview | ✅ | ✅ | ✅ | ✅ | | gemini-3-flash | ✅ | ✅ | ✅ | ✅ | ### 功能后缀 支持单个或混合后缀启用指定功能: | 后缀 | 功能 | 示例 | |------|------|------| | `-image` | 图片生成 | `gemini-2.5-flash-image` | | `-video` | 视频生成 | `gemini-2.5-flash-video` | | `-search` | 联网搜索 | `gemini-2.5-flash-search` | | 混合后缀 | 同时启用多功能 | `gemini-2.5-flash-image-search` | **说明:** - 无后缀:启用所有功能(图片/视频/搜索/工具) - 有后缀:只启用指定功能,支持任意组合如 `-image-search`、`-video-search` ### ⚠️ 限制说明 | 限制 | 说明 | |------|------| | **不支持自定义工具** | Function Calling / Tools 参数会被忽略,仅支持内置工具(图片/视频生成、搜索) | | **上下文拼接实现** | 多轮对话通过拼接 `messages` 为单次请求实现,非原生会话管理 | | **无状态** | 每次请求独立,不保留会话状态,历史消息需客户端自行维护 | --- >> 公益 Demo(免费调用) > 🔗 链接: > > 在线绘图预设测试 [https://chat.openel.top](https://chat.openel.top/) image > > > API Key 获取请访问 https://business2api.openel.top/auth 获取个人专属免费APIKEY >> GLM 公益测试 API > 🔗 链接: > > XiaoMi 网页逆向公益 API > 🔗 链接:[https://xiaomi.openel.top](https://xiaomi.openel.top/) > > API Key : `sk-3d2f9b84e7f510b1a08f7b3d6c9a6a7f17fbbad5624ea29f22d9c742bf39c863` ## 快速开始 ### 方式一:Docker 部署(推荐) #### 1. 使用 Docker Compose ```bash # 创建目录 mkdir business2api && cd business2api # 下载必要文件 wget https://raw.githubusercontent.com/XxxXTeam/business2api/master/docker/docker-compose.yml wget https://raw.githubusercontent.com/XxxXTeam/business2api/master/config/config.json.example -O config.json # 编辑配置 vim config.json # 创建数据目录 mkdir data # 启动服务 docker compose up -d ``` #### 2. 使用 Docker Run ```bash # 拉取镜像 docker pull ghcr.io/xxxteam/business2api:latest # 创建配置文件 wget https://raw.githubusercontent.com/XxxXTeam/business2api/master/config/config.json.example -O config.json # 运行容器 docker run -d \ --name business2api \ -p 8000:8000 \ -v $(pwd)/data:/app/data \ -v $(pwd)/config.json:/app/config/config.json:ro \ ghcr.io/xxxteam/business2api:latest ``` ### 方式二:二进制部署 #### 1. 下载预编译版本 从 [Releases](https://github.com/XxxXTeam/business2api/releases) 下载对应平台的二进制文件。 ```bash # Linux amd64 wget https://github.com/XxxXTeam/business2api/releases/latest/download/business2api-linux-amd64.tar.gz tar -xzf business2api-linux-amd64.tar.gz chmod +x business2api-linux-amd64 ``` #### 2. 从源码编译 ```bash # 需要 Go 1.24+ git clone https://github.com/XxxXTeam/business2api.git cd business2api # 编译 go build -o business2api . # 运行 ./business2api ``` ### 方式三:使用 Systemd 服务 ```bash # 创建服务文件 sudo tee /etc/systemd/system/business2api.service << EOF [Unit] Description=Gemini Gateway Service After=network.target [Service] Type=simple User=nobody WorkingDirectory=/opt/business2api ExecStart=/opt/business2api/business2api Restart=always RestartSec=5 Environment=LISTEN_ADDR=:8000 Environment=DATA_DIR=/opt/business2api/data [Install] WantedBy=multi-user.target EOF # 启动服务 sudo systemctl daemon-reload sudo systemctl enable business2api sudo systemctl start business2api ``` --- ## 配置说明 ### config.json ```json { "api_keys": ["sk-your-api-key"], // API 密钥列表,用于鉴权 "listen_addr": ":8000", // 监听地址 "data_dir": "./data", // 账号数据目录 "default_config": "", // 默认 configId(可选) "debug": false, // 调试模式(输出详细日志) "pool": { "target_count": 50, // 目标账号数量 "min_count": 10, // 最小账号数,低于此值触发注册 "check_interval_minutes": 30, // 检查间隔(分钟) "register_threads": 1, // 本地注册线程数 "register_headless": true, // 无头模式注册 "refresh_on_startup": true, // 启动时刷新账号 "refresh_cooldown_sec": 240, // 刷新冷却时间(秒) "use_cooldown_sec": 15, // 使用冷却时间(秒) "max_fail_count": 3, // 最大连续失败次数 "enable_browser_refresh": true, // 启用浏览器刷新401账号 "browser_refresh_headless": true, // 浏览器刷新无头模式 "browser_refresh_max_retry": 1, // 浏览器刷新最大重试次数 "auto_delete_401": false // 401时自动删除账号 }, "pool_server": { "enable": false, // 是否启用分离模式 "mode": "local", // 运行模式:local/server/client "server_addr": "http://ip:8000", // 服务器地址(client模式) "listen_addr": ":8000", // 监听地址(server模式) "secret": "your-secret-key", // 通信密钥 "target_count": 50, // 目标账号数(server模式) "client_threads": 2, // 客户端并发线程数 "data_dir": "./data", // 数据目录(server模式) "expired_action": "delete" // 过期账号处理:delete/refresh/queue }, "proxy_pool": { "subscribes": [], // 代理订阅链接列表 "files": [], // 本地代理文件列表 "health_check": true, // 启用健康检查 "check_on_startup": true // 启动时检查 } } ``` ### 多 API Key 支持 支持配置多个 API Key,所有 Key 都可以用于鉴权: ```json { "api_keys": [ "sk-key-1", "sk-key-2", "sk-key-3" ] } ``` ### 配置热重载 服务运行时自动监听 `config/config.json` 文件变更,无需重启即可生效。 **可热重载的配置项:** | 配置项 | 说明 | |----------|------| | `api_keys` | API 密钥列表 | | `debug` | 调试模式 | | `pool.refresh_cooldown_sec` | 刷新冷却时间 | | `pool.use_cooldown_sec` | 使用冷却时间 | | `pool.max_fail_count` | 最大失败次数 | | `pool.enable_browser_refresh` | 浏览器刷新开关 | **配置合并机制:** 配置文件中缺失的字段会自动使用默认值,无需手动同步示例文件。 ```bash # 手动触发重载 curl -X POST http://localhost:8000/admin/reload-config \ -H "Authorization: Bearer sk-your-api-key" ``` --- ## C/S 分离架构 支持将号池管理与API服务分离部署,适用于多节点场景。 ### 架构说明 ``` ┌─────────────────┐ ┌─────────────────┐ │ API Server │◄───────►│ Pool Server │ │ (客户端模式) │ HTTP │ (服务器模式) │ └─────────────────┘ └────────┬────────┘ │ WebSocket│ │ ┌────────▼────────┐ │ Worker Client │ │ (注册/续期) │ └─────────────────┘ ``` ### 运行模式 | 模式 | 说明 | |------|------| | `local` | 本地模式(默认),API服务和号池管理在同一进程 | | `server` | 服务器模式,提供号池服务和任务分发 | | `client` | 客户端模式,只接收任务(注册/续期),不提供API服务 | ### Server 模式配置 ```json { "api_keys": ["sk-your-api-key"], "listen_addr": ":8000", "pool_server": { "enable": true, "mode": "server", "secret": "shared-secret-key", "target_count": 100, "data_dir": "./data", "expired_action": "delete" } } ``` ### Client 模式配置(仅注册/续期工作节点) ```json { "pool_server": { "enable": true, "mode": "client", "server_addr": "http://server-ip:8000", "secret": "shared-secret-key", "client_threads": 3 }, "proxy_pool": { "subscribes": ["https://your-proxy-subscribe-url"], "health_check": true, "check_on_startup": true } } ``` ### 配置项说明 | 配置项 | 说明 | 默认值 | |--------|------|--------| | `client_threads` | 客户端并发任务数 | 1 | | `expired_action` | 过期账号处理方式 | delete | **expired_action 可选值:** - `delete` - 删除过期账号 - `refresh` - 尝试浏览器刷新 - `queue` - 保留在队列等待重试 **架构说明(v2.x):** ``` ┌─────────────────────────────────────────────────────┐ │ Server (:8000) │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ API 服务 │ │ WS 服务 │ │ 号池管理 │ │ │ │ /v1/chat/* │ │ /ws │ │ Pool Mgr │ │ │ └─────────────┘ └──────┬──────┘ └─────────────┘ │ └──────────────────────────┼──────────────────────────┘ │ WebSocket ┌────────────┼────────────┐ │ │ │ ┌─────▼─────┐ ┌────▼────┐ ┌─────▼─────┐ │ Client1 │ │ Client2 │ │ Client3 │ │ (注册) │ │ (注册) │ │ (注册) │ └───────────┘ └─────────┘ └───────────┘ ``` **Client 模式说明:** - 通过 WebSocket 连接 Server (`/ws`) 接收任务 - 执行注册新账号任务 - 执行401账号Cookie续期任务 - 完成后自动回传账号数据到Server - **不提供API服务**,只作为工作节点 ### 环境变量 | 变量 | 说明 | 默认值 | |------|------|--------| | `LISTEN_ADDR` | 监听地址 | `:8000` | | `DATA_DIR` | 数据目录 | `./data` | | `PROXY` | 代理地址 | - | | `API_KEY` | API 密钥 | - | | `CONFIG_ID` | 默认 configId | - | --- ## API 使用 ### 获取模型列表 ```bash curl http://localhost:8000/v1/models \ -H "Authorization: Bearer sk-your-api-key" ``` ### 聊天补全 ```bash curl http://localhost:8000/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer sk-your-api-key" \ -d '{ "model": "gemini-2.5-flash", "messages": [ {"role": "user", "content": "Hello!"} ], "stream": true }' ``` ### 多模态(图片输入) ```bash curl http://localhost:8000/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer sk-your-api-key" \ -d '{ "model": "gemini-2.5-flash", "messages": [ { "role": "user", "content": [ {"type": "text", "text": "描述这张图片"}, {"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,..."}} ] } ] }' ``` --- ## Flow 图片/视频生成 Flow 集成了 Google VideoFX (Veo/Imagen) API,支持图片和视频生成。 ### 配置 ```json { "flow": { "enable": true, "tokens": [], // 配置文件中的 Token(可选) "proxy": "", // Flow 专用代理 "timeout": 120, // 超时时间(秒) "poll_interval": 3, // 轮询间隔(秒) "max_poll_attempts": 500 // 最大轮询次数 } } ``` ### 获取 Flow Token **方式一:文件目录(推荐)** 将完整的 cookie 字符串保存到 `data/at/` 目录下的任意 `.txt` 文件: ```bash mkdir -p data/at echo "your-cookie-string" > data/at/account1.txt ``` 服务启动时自动加载,支持文件监听自动热加载。 **方式二:API 添加** ```bash curl -X POST http://localhost:8000/admin/flow/add-token \ -H "Authorization: Bearer sk-xxx" \ -d '{"cookie": "your-cookie-string"}' ``` **Cookie 获取方法:** 1. 访问 [labs.google/fx](https://labs.google/fx) 并登录 2. 打开开发者工具 → Application → Cookies 3. 复制所有 cookie 或 `__Secure-next-auth.session-token` 的值 ### Flow 模型列表 | 模型 | 类型 | 说明 | |------|------|------| | `gemini-2.5-flash-image-landscape/portrait` | 图片 | Gemini 2.5 Flash 图片生成 | | `gemini-3.0-pro-image-landscape/portrait` | 图片 | Gemini 3.0 Pro 图片生成 | | `imagen-4.0-generate-preview-landscape/portrait` | 图片 | Imagen 4.0 图片生成 | | `veo_3_1_t2v_fast_landscape/portrait` | 视频 | Veo 3.1 文生视频 | | `veo_2_1_fast_d_15_t2v_landscape/portrait` | 视频 | Veo 2.1 文生视频 | | `veo_2_0_t2v_landscape/portrait` | 视频 | Veo 2.0 文生视频 | | `veo_3_1_i2v_s_fast_fl_landscape/portrait` | 视频 | Veo 3.1 图生视频 (I2V) | | `veo_2_1_fast_d_15_i2v_landscape/portrait` | 视频 | Veo 2.1 图生视频 (I2V) | | `veo_2_0_i2v_landscape/portrait` | 视频 | Veo 2.0 图生视频 (I2V) | | `veo_3_0_r2v_fast_landscape/portrait` | 视频 | Veo 3.0 多图生视频 (R2V) | ### 使用示例 ```bash # 图片生成 curl http://localhost:8000/v1/chat/completions \ -H "Authorization: Bearer sk-xxx" \ -H "Content-Type: application/json" \ -d '{"model": "gemini-2.5-flash-image-landscape", "messages": [{"role": "user", "content": "一只可爱的猫咪"}], "stream": true}' # 文生视频 (T2V) curl http://localhost:8000/v1/chat/completions \ -H "Authorization: Bearer sk-xxx" \ -H "Content-Type: application/json" \ -d '{"model": "veo_3_1_t2v_fast_landscape", "messages": [{"role": "user", "content": "猫咪在草地上追蝴蝶"}], "stream": true}' # 图生视频 (I2V) - 支持首尾帧 curl http://localhost:8000/v1/chat/completions \ -H "Authorization: Bearer sk-xxx" \ -H "Content-Type: application/json" \ -d '{ "model": "veo_3_1_i2v_s_fast_fl_landscape", "messages": [{ "role": "user", "content": [ {"type": "text", "text": "猫咪跳跃"}, {"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,..."}} ] }], "stream": true }' ``` --- ## 🔧 常见问题与解决方案 ### 注册相关 > **拟人化行为**:浏览器自动化已内置拟人化操作(随机延迟、贝塞尔曲线鼠标移动、自然打字节奏),以降低被检测风险。可通过 `register_headless: false` 开启可视模式观察行为。 | 错误 | 原因 | 解决方案 | |------|------|----------| | `无法获取验证码邮件` | 临时邮箱服务不稳定或邮件延迟 | 代理遭到拉黑,更换代理 | | `panic: nil pointer` | 浏览器启动失败或页面未加载 | 检查 Chrome 是否安装,确保有足够内存 | | `找不到提交按钮` | 页面结构变化或加载超时 | 升级到最新版本,检查网络 | ### API 相关 | 错误 | 原因 | 解决方案 | |------|------|----------| | `401 Unauthorized` | API Key 无效或未配置 | 检查 `api_keys` 配置 | | `429 Too Many Requests` | 账号触发速率限制 | 增加账号池数量,调整 `use_cooldown_sec` | | `503 Service Unavailable` | 无可用账号 | 等待账号刷新或增加注册 | | `空响应` | Google 返回空内容 | 重试请求,检查 prompt 是否触发过滤 | ### WebSocket 相关 | 错误 | 原因 | 解决方案 | |------|------|----------| | `客户端频繁断开` | 心跳超时或网络不稳定 | 检查网络,确保 Server 和 Client 时间同步 | | `上传注册结果失败` | Server 端口或路径错误 | 确保 `server_addr` 指向正确地址 | ### Flow 相关 | 错误 | 原因 | 解决方案 | |------|------|----------| | `Flow 服务未启用` | 未配置或 Token 为空 | 检查 `flow.enable` 和 `flow.tokens` | | `Token 认证失败` | ST Token 过期 | 重新获取 Token | | `视频生成超时` | 生成时间过长 | 增加 `max_poll_attempts` | ### Docker 相关 | 错误 | 原因 | 解决方案 | |------|------|----------| | `无法启动浏览器` | Docker 容器缺少 Chrome | 使用包含 Chrome 的镜像或挂载主机浏览器 | | `权限被拒绝` | 数据目录权限问题 | `chown -R 1000:1000 ./data` | --- ## 📡 API 端点一览 ### 公开端点 | 端点 | 方法 | 说明 | |------|------|------| | `/` | GET | 服务状态和信息 | | `/health` | GET | 健康检查 | | `/ws` | WS | WebSocket 端点 (Server 模式) | ### API 端点(需要 API Key) | 端点 | 方法 | 说明 | |------|------|------| | `/v1/models` | GET | OpenAI 格式模型列表 | | `/v1/chat/completions` | POST | OpenAI 格式聊天补全 | | `/v1/messages` | POST | Claude 格式消息 | | `/v1beta/models` | GET | Gemini 格式模型列表 | | `/v1beta/models/:model` | GET | Gemini 格式模型详情 | | `/v1beta/models/:model:generateContent` | POST | Gemini 格式生成内容 | ### 管理端点(需要 API Key) | 端点 | 方法 | 说明 | |------|------|------| | `/admin/status` | GET | 账号池状态 | | `/admin/stats` | GET | 详细 API 统计 | | `/admin/ip` | GET | IP 遥测统计(请求数/Token/RPM) | | `/admin/register` | POST | 触发注册 | | `/admin/refresh` | POST | 刷新账号池 | | `/admin/reload-config` | POST | 热重载配置文件 | | `/admin/force-refresh` | POST | 强制刷新所有账号 | | `/admin/config/cooldown` | POST | 动态调整冷却时间 | | `/admin/browser-refresh` | POST | 手动触发浏览器刷新指定账号 | | `/admin/config/browser-refresh` | POST | 配置浏览器刷新开关 | | `/admin/flow/status` | GET | Flow 服务状态 | | `/admin/flow/add-token` | POST | 添加 Flow Token | | `/admin/flow/remove-token` | POST | 移除 Flow Token | | `/admin/flow/reload` | POST | 重新加载 Flow Token | --- ## 🛠️ 开发 ### 本地运行 ```bash # 安装依赖 go mod download # 运行 go run . # 调试模式 go run . -d ``` ### 构建 ```bash # 标准构建 go build -o business2api . # 带 QUIC/uTLS 支持(推荐) go build -tags "with_quic with_utls" -o business2api . # 生产构建(压缩体积) CGO_ENABLED=0 go build -ldflags="-s -w" -tags "with_quic with_utls" -o business2api . # 多平台构建 GOOS=linux GOARCH=amd64 go build -tags "with_quic with_utls" -o business2api-linux-amd64 . GOOS=windows GOARCH=amd64 go build -tags "with_quic with_utls" -o business2api-windows-amd64.exe . GOOS=darwin GOARCH=arm64 go build -tags "with_quic with_utls" -o business2api-darwin-arm64 . ``` ### 项目结构 ``` . ├── main.go # 主程序入口 ├── config/ # 配置文件 │ ├── config.json.example │ └── README.md ├── src/ │ ├── flow/ # Flow 图片/视频生成 │ ├── logger/ # 日志模块 │ ├── pool/ # 账号池管理(C/S架构) │ ├── proxy/ # 代理池管理 │ ├── register/ # 浏览器自动注册 │ └── utils/ # 工具函数 ├── docker/ # Docker 相关 │ └── docker-compose.yml └── .github/ # GitHub Actions ``` --- ## 📊 IP 遥测接口 访问 `/admin/ip` 获取全部 IP 请求统计: ```bash curl http://localhost:8000/admin/ip \ -H "Authorization: Bearer sk-your-api-key" ``` **返回字段说明:** | 字段 | 说明 | |------|------| | `unique_ips` | 独立 IP 数量 | | `total_requests` | 总请求数 | | `total_tokens` | 总 Token 消耗 | | `total_images` | 图片生成数 | | `total_videos` | 视频生成数 | | `ips[].rpm` | 单 IP 每分钟请求数 | | `ips[].input_tokens` | 输入 Token | | `ips[].output_tokens` | 输出 Token | | `ips[].models` | 各模型使用次数 | --- ## Star History [![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) ## 📄 License MIT License ================================================ FILE: config/README.md ================================================ # 配置说明 ## 代理池配置 (`proxy_pool`) **内置 xray-core**,支持 vmess、vless、shadowsocks、trojan 等协议,自动转换为本地 socks5 代理。 ```json "proxy_pool": { "proxy": "", // 备用单个代理 (http/socks5 格式) "subscribes": [ // 订阅链接列表 (支持 base64 编码) "https://example.com/sub1", "https://example.com/sub2" ], "files": [ // 本地代理文件列表 "./proxies.txt" ], "health_check": true, // 是否启用健康检查 "check_on_startup": false // 启动时是否检查所有节点 } ``` ### 支持的代理格式 **代理文件/订阅内容格式** (每行一个): ``` # VMess vmess://eyJ2IjoiMiIsInBzIjoi5ZCN56ewIiwiYWRkIjoic2VydmVyLmNvbSIsInBvcnQiOiI0NDMiLCJpZCI6InV1aWQiLCJhaWQiOiIwIiwic2N5IjoiYXV0byIsIm5ldCI6IndzIiwicGF0aCI6Ii9wYXRoIiwiaG9zdCI6Imhvc3QuY29tIiwidGxzIjoidGxzIn0= # VLESS vless://uuid@server.com:443?type=ws&security=tls&path=/path&host=host.com&sni=sni.com#名称 # Shadowsocks ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ=@server.com:8388#名称 # Trojan trojan://password@server.com:443?sni=sni.com#名称 # 直接代理 http://proxy.com:8080 socks5://127.0.0.1:1080 ``` --- ## 号池配置 (`pool`) ```json "pool": { "target_count": 50, // 目标账号数量 "min_count": 10, // 最小账号数,低于此值触发注册 "check_interval_minutes": 30, // 检查间隔(分钟) "register_threads": 1, // 注册线程数 "register_headless": false, // 注册时是否无头模式 "refresh_on_startup": true, // 启动时是否刷新账号 "refresh_cooldown_sec": 240, // 刷新冷却时间(秒) "use_cooldown_sec": 15, // 使用冷却时间(秒) "max_fail_count": 3, // 最大失败次数 "enable_browser_refresh": true, // 启用浏览器刷新 "browser_refresh_headless": false, // 浏览器刷新无头模式 "browser_refresh_max_retry": 1 // 浏览器刷新最大重试次数 } ``` --- ## 号池服务器配置 (`pool_server`) ```json "pool_server": { "enable": false, // 是否启用 "mode": "local", // 模式: local/server/client "server_addr": "", // 服务器地址 (客户端模式) "listen_addr": ":8000", // 监听地址 (服务器模式) "secret": "", // 认证密钥 "target_count": 50, // 目标账号数 "client_threads": 2, // 客户端并发线程数 "data_dir": "./data", // 数据目录 "expired_action": "delete" // 过期账号处理方式 } ``` **模式说明**: - `local`: 本地模式,独立运行 - `server`: 服务器模式,提供号池服务和API - `client`: 客户端模式,连接服务器接收注册/续期任务 **expired_action 说明**: - `delete`: 删除过期/失败账号 - `refresh`: 尝试浏览器刷新Cookie - `queue`: 保留在队列等待重试 --- ## Flow 配置 (`flow`) ```json "flow": { "enable": false, // 是否启用 Flow 视频生成 "tokens": [], // Flow ST Tokens "proxy": "", // Flow 专用代理 "timeout": 120, // 超时时间(秒) "poll_interval": 3, // 轮询间隔(秒) "max_poll_attempts": 500 // 最大轮询次数 } ``` --- ## 其他配置 ```json { "api_keys": ["key1", "key2"], // API 密钥列表 "listen_addr": ":8000", // 监听地址 "data_dir": "./data", // 数据目录 "default_config": "", // 默认 configId "debug": false, // 调试模式 "proxy": "http://127.0.0.1:10808" // 全局代理 (兼容旧配置) } ``` ================================================ FILE: config/config.json.example ================================================ { "api_keys": ["your-api-key-here"], "listen_addr": ":8000", "data_dir": "./data", "default_config": "", "debug": false, "pool": { "target_count": 50, "min_count": 10, "check_interval_minutes": 30, "register_threads": 1, "register_headless": true, "refresh_on_startup": true, "refresh_cooldown_sec": 240, "use_cooldown_sec": 15, "max_fail_count": 3, "enable_browser_refresh": true, "browser_refresh_headless": true, "browser_refresh_max_retry": 1 }, "pool_server": { "enable": false, "mode": "local", "server_addr": "http://server-ip:8000", "listen_addr": ":8000", "secret": "your-secret-key", "target_count": 50, "client_threads": 2, "data_dir": "./data", "expired_action": "delete" }, "proxy_pool": { "subscribes": [ "http://example.com/s/example" ], "files": [], "health_check": true, "check_on_startup": true }, "flow": { "enable": false, "tokens": [], "timeout": 120, "poll_interval": 3, "max_poll_attempts": 500 } } ================================================ FILE: docker/docker-compose.yml ================================================ version: '3.8' services: business2api: image: ghcr.io/xxxxteam/business2api:latest container_name: business2api restart: unless-stopped ports: - "8000:8000" volumes: - ./data:/app/data - ./config.json:/app/config/config.json:ro environment: - TZ=Asia/Shanghai - LISTEN_ADDR=:8000 - DATA_DIR=/app/data # - PROXY=http://proxy:port # - API_KEY=your-api-key healthcheck: test: ["CMD", "wget", "-q", "--spider", "http://localhost:8000/v1/models"] interval: 30s timeout: 10s retries: 3 logging: driver: json-file options: max-size: "10m" max-file: "3" ================================================ FILE: go.mod ================================================ module business2api go 1.25.0 require ( github.com/fsnotify/fsnotify v1.9.0 github.com/gin-gonic/gin v1.9.1 github.com/go-rod/rod v0.116.2 github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 github.com/sagernet/sing-box v1.12.12 github.com/sagernet/sing-quic v0.5.2-0.20250909083218-00a55617c0fb golang.org/x/image v0.38.0 ) require ( filippo.io/edwards25519 v1.1.1 // indirect github.com/ajg/form v1.5.1 // indirect github.com/akutz/memconn v0.1.0 // indirect github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect github.com/andybalholm/brotli v1.1.0 // indirect github.com/anytls/sing-anytls v0.0.11 // indirect github.com/bits-and-blooms/bitset v1.13.0 // indirect github.com/bytedance/sonic v1.9.1 // indirect github.com/caddyserver/certmagic v0.23.0 // indirect github.com/caddyserver/zerossl v0.1.3 // indirect github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/coder/websocket v1.8.13 // indirect github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 // indirect github.com/cretz/bine v0.2.0 // indirect github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/gaissmai/bart v0.11.1 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-chi/chi/v5 v5.2.2 // indirect github.com/go-chi/render v1.0.3 // indirect github.com/go-json-experiment/json v0.0.0-20250103232110-6a9a0fde9288 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.14.0 // indirect github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 // indirect github.com/gofrs/uuid/v5 v5.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/btree v1.1.3 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 // indirect github.com/gorilla/csrf v1.7.3-0.20250123201450-9dd6af1f6d30 // indirect github.com/gorilla/securecookie v1.1.2 // indirect github.com/hashicorp/yamux v0.1.2 // indirect github.com/hdevalence/ed25519consensus v0.2.0 // indirect github.com/illarion/gonotify/v2 v2.0.3 // indirect github.com/insomniacslk/dhcp v0.0.0-20250417080101-5f8cf70e8c5f // indirect github.com/jsimonetti/rtnetlink v1.4.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.17.11 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a // indirect github.com/kr/pretty v0.3.1 // indirect github.com/leodido/go-urn v1.2.4 // indirect github.com/libdns/alidns v1.0.5-libdns.v1.beta1 // indirect github.com/libdns/cloudflare v0.2.2-0.20250708034226-c574dccb31a6 // indirect github.com/libdns/libdns v1.1.0 // indirect github.com/logrusorgru/aurora v2.0.3+incompatible // indirect github.com/mattn/go-isatty v0.0.19 // indirect github.com/mdlayher/genetlink v1.3.2 // indirect github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 // indirect github.com/mdlayher/sdnotify v1.0.0 // indirect github.com/mdlayher/socket v0.5.1 // indirect github.com/metacubex/tfo-go v0.0.0-20250921095601-b102db4216c0 // indirect github.com/metacubex/utls v1.8.3 // indirect github.com/mholt/acmez/v3 v3.1.2 // indirect github.com/miekg/dns v1.1.68 // indirect github.com/mitchellh/go-ps v1.0.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.0.8 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/prometheus-community/pro-bing v0.4.0 // indirect github.com/quic-go/qpack v0.5.1 // indirect github.com/safchain/ethtool v0.3.0 // indirect github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a // indirect github.com/sagernet/cors v1.2.1 // indirect github.com/sagernet/fswatch v0.1.1 // indirect github.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb // indirect github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect github.com/sagernet/nftables v0.3.0-beta.4 // indirect github.com/sagernet/quic-go v0.52.0-sing-box-mod.3 // indirect github.com/sagernet/sing v0.7.13 // indirect github.com/sagernet/sing-mux v0.3.3 // indirect github.com/sagernet/sing-shadowsocks v0.2.8 // indirect github.com/sagernet/sing-shadowsocks2 v0.2.1 // indirect github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 // indirect github.com/sagernet/sing-tun v0.7.3 // indirect github.com/sagernet/sing-vmess v0.2.7 // indirect github.com/sagernet/smux v1.5.34-mod.2 // indirect github.com/sagernet/tailscale v1.80.3-sing-box-1.12-mod.2 // indirect github.com/sagernet/wireguard-go v0.0.1-beta.7 // indirect github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 // indirect github.com/stretchr/testify v1.11.1 // indirect github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e // indirect github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect github.com/tailscale/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4 // indirect github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 // indirect github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a // indirect github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 // indirect github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc // indirect github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect github.com/ugorji/go/codec v1.2.11 // indirect github.com/vishvananda/netns v0.0.5 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/ysmood/fetchup v0.2.3 // indirect github.com/ysmood/goob v0.4.0 // indirect github.com/ysmood/got v0.40.0 // indirect github.com/ysmood/gson v0.7.3 // indirect github.com/ysmood/leakless v0.9.0 // indirect github.com/zeebo/blake3 v0.2.4 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect go.uber.org/zap/exp v0.3.0 // indirect go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect golang.org/x/arch v0.3.0 // indirect golang.org/x/crypto v0.48.0 // indirect golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect golang.org/x/mod v0.33.0 // indirect golang.org/x/net v0.50.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.41.0 // indirect golang.org/x/term v0.40.0 // indirect golang.org/x/text v0.35.0 // indirect golang.org/x/time v0.12.0 // indirect golang.org/x/tools v0.42.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect golang.zx2c4.com/wireguard/windows v0.5.3 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect google.golang.org/grpc v1.79.3 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect lukechampine.com/blake3 v1.4.1 // indirect ) ================================================ FILE: go.sum ================================================ filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw= filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A= github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/anytls/sing-anytls v0.0.11 h1:w8e9Uj1oP3m4zxkyZDewPk0EcQbvVxb7Nn+rapEx4fc= github.com/anytls/sing-anytls v0.0.11/go.mod h1:7rjN6IukwysmdusYsrV51Fgu1uW6vsrdd6ctjnEAln8= github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE= github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= github.com/caddyserver/certmagic v0.23.0 h1:CfpZ/50jMfG4+1J/u2LV6piJq4HOfO6ppOnOf7DkFEU= github.com/caddyserver/certmagic v0.23.0/go.mod h1:9mEZIWqqWoI+Gf+4Trh04MOVPD0tGSxtqsxg87hAIH4= github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA= github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= github.com/cilium/ebpf v0.15.0 h1:7NxJhNiBT3NG8pZJ3c+yfrVdHY8ScgKD27sScgjLMMk= github.com/cilium/ebpf v0.15.0/go.mod h1:DHp1WyrLeiBh19Cf/tfiSMhqheEiK8fXFZ4No0P1Hso= github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE= github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0= github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cretz/bine v0.2.0 h1:8GiDRGlTgz+o8H9DSnsl+5MeBK4HsExxgl6WgzOCuZo= github.com/cretz/bine v0.2.0/go.mod h1:WU4o9QR9wWp8AVKtTM1XD5vUHkEqnf2vVSo6dBqbetI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa h1:h8TfIT1xc8FWbwwpmHn1J5i43Y0uZP97GqasGCzSRJk= github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa/go.mod h1:Nx87SkVqTKd8UtT+xu7sM/l+LgXs6c0aHrlKusR+2EQ= github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yezR0y7jJ5pceLHthLaYf4bA5T14B6q39S4q2Q= github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= github.com/gaissmai/bart v0.11.1 h1:5Uv5XwsaFBRo4E5VBcb9TzY8B7zxFf+U7isDxqOrRfc= github.com/gaissmai/bart v0.11.1/go.mod h1:KHeYECXQiBjTzQz/om2tqn3sZF1J7hw9m6z41ftj3fg= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I= github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo= github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618= github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= github.com/go-json-experiment/json v0.0.0-20250103232110-6a9a0fde9288 h1:KbX3Z3CgiYlbaavUq3Cj9/MjpO+88S7/AGXzynVDv84= github.com/go-json-experiment/json v0.0.0-20250103232110-6a9a0fde9288/go.mod h1:BWmvoE1Xia34f3l/ibJweyhrT+aROb/FQ6d+37F0e2s= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-rod/rod v0.116.2 h1:A5t2Ky2A+5eD/ZJQr1EfsQSe5rms5Xof/qj296e+ZqA= github.com/go-rod/rod v0.116.2/go.mod h1:H+CMO9SCNc2TJ2WfrG+pKhITz57uGNYU43qYHh438Mg= github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 h1:sQspH8M4niEijh3PFscJRLDnkL547IeP7kpPe3uUhEg= github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466/go.mod h1:ZiQxhyQ+bbbfxUKVvjfO498oPYvtYhZzycal3G/NHmU= github.com/gofrs/uuid/v5 v5.3.2 h1:2jfO8j3XgSwlz/wHqemAEugfnTlikAYHhnqQ8Xh4fE0= github.com/gofrs/uuid/v5 v5.3.2/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 h1:wG8RYIyctLhdFk6Vl1yPGtSRtwGpVkWyZww1OCil2MI= github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/csrf v1.7.3-0.20250123201450-9dd6af1f6d30 h1:fiJdrgVBkjZ5B1HJ2WQwNOaXB+QyYcNXTA3t1XYLz0M= github.com/gorilla/csrf v1.7.3-0.20250123201450-9dd6af1f6d30/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU= github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo= github.com/illarion/gonotify/v2 v2.0.3 h1:B6+SKPo/0Sw8cRJh1aLzNEeNVFfzE3c6N+o+vyxM+9A= github.com/illarion/gonotify/v2 v2.0.3/go.mod h1:38oIJTgFqupkEydkkClkbL6i5lXV/bxdH9do5TALPEE= github.com/insomniacslk/dhcp v0.0.0-20250417080101-5f8cf70e8c5f h1:dd33oobuIv9PcBVqvbEiCXEbNTomOHyj3WFuC5YiPRU= github.com/insomniacslk/dhcp v0.0.0-20250417080101-5f8cf70e8c5f/go.mod h1:zhFlBeJssZ1YBCMZ5Lzu1pX4vhftDvU10WUVb1uXKtM= github.com/jsimonetti/rtnetlink v1.4.0 h1:Z1BF0fRgcETPEa0Kt0MRk3yV5+kF1FWTni6KUFKrq2I= github.com/jsimonetti/rtnetlink v1.4.0/go.mod h1:5W1jDvWdnthFJ7fxYX1GMK07BUpI4oskfOqvPteYS6E= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a h1:+RR6SqnTkDLWyICxS1xpjCi/3dhyV+TgZwA6Ww3KncQ= github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/libdns/alidns v1.0.5-libdns.v1.beta1 h1:txHK7UxDed3WFBDjrTZPuMn8X+WmhjBTTAMW5xdy5pQ= github.com/libdns/alidns v1.0.5-libdns.v1.beta1/go.mod h1:ystHmPwcGoWjPrGpensQSMY9VoCx4cpR2hXNlwk9H/g= github.com/libdns/cloudflare v0.2.2-0.20250708034226-c574dccb31a6 h1:3MGrVWs2COjMkQR17oUw1zMIPbm2YAzxDC3oGVZvQs8= github.com/libdns/cloudflare v0.2.2-0.20250708034226-c574dccb31a6/go.mod h1:w9uTmRCDlAoafAsTPnn2nJ0XHK/eaUMh86DUk8BWi60= github.com/libdns/libdns v1.0.0-beta.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= github.com/libdns/libdns v1.1.0 h1:9ze/tWvt7Df6sbhOJRB8jT33GHEHpEQXdtkE3hPthbU= github.com/libdns/libdns v1.1.0/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw= github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o= github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 h1:A1Cq6Ysb0GM0tpKMbdCXCIfBclan4oHk1Jb+Hrejirg= github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42/go.mod h1:BB4YCPDOzfy7FniQ/lxuYQ3dgmM2cZumHbK8RpTjN2o= github.com/mdlayher/sdnotify v1.0.0 h1:Ma9XeLVN/l0qpyx1tNeMSeTjCPH6NtuD6/N9XdTlQ3c= github.com/mdlayher/sdnotify v1.0.0/go.mod h1:HQUmpM4XgYkhDLtd+Uad8ZFK1T9D5+pNxnXQjCeJlGE= github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos= github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ= github.com/metacubex/tfo-go v0.0.0-20250921095601-b102db4216c0 h1:Ui+/2s5Qz0lSnDUBmEL12M5Oi/PzvFxGTNohm8ZcsmE= github.com/metacubex/tfo-go v0.0.0-20250921095601-b102db4216c0/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw= github.com/metacubex/utls v1.8.3 h1:0m/yCxm3SK6kWve2lKiFb1pue1wHitJ8sQQD4Ikqde4= github.com/metacubex/utls v1.8.3/go.mod h1:kncGGVhFaoGn5M3pFe3SXhZCzsbCJayNOH4UEqTKTko= github.com/mholt/acmez/v3 v3.1.2 h1:auob8J/0FhmdClQicvJvuDavgd5ezwLBfKuYmynhYzc= github.com/mholt/acmez/v3 v3.1.2/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ= github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA= github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps= github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus-community/pro-bing v0.4.0 h1:YMbv+i08gQz97OZZBwLyvmmQEEzyfyrrjEaAchdy3R4= github.com/prometheus-community/pro-bing v0.4.0/go.mod h1:b7wRYZtCcPmt4Sz319BykUU241rWLe1VFXyiyWK/dH4= github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0= github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs= github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a h1:+NkI2670SQpQWvkkD2QgdTuzQG263YZ+2emfpeyGqW0= github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a/go.mod h1:63s7jpZqcDAIpj8oI/1v4Izok+npJOHACFCU6+huCkM= github.com/sagernet/cors v1.2.1 h1:Cv5Z8y9YSD6Gm+qSpNrL3LO4lD3eQVvbFYJSG7JCMHQ= github.com/sagernet/cors v1.2.1/go.mod h1:O64VyOjjhrkLmQIjF4KGRrJO/5dVXFdpEmCW/eISRAI= github.com/sagernet/fswatch v0.1.1 h1:YqID+93B7VRfqIH3PArW/XpJv5H4OLEVWDfProGoRQs= github.com/sagernet/fswatch v0.1.1/go.mod h1:nz85laH0mkQqJfaOrqPpkwtU1znMFNVTpT/5oRsVz/o= github.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb h1:pprQtDqNgqXkRsXn+0E8ikKOemzmum8bODjSfDene38= github.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb/go.mod h1:QkkPEJLw59/tfxgapHta14UL5qMUah5NXhO0Kw2Kan4= github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis= github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I= github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8= github.com/sagernet/quic-go v0.52.0-sing-box-mod.3 h1:ySqffGm82rPqI1TUPqmtHIYd12pfEGScygnOxjTL56w= github.com/sagernet/quic-go v0.52.0-sing-box-mod.3/go.mod h1:OV+V5kEBb8kJS7k29MzDu6oj9GyMc7HA07sE1tedxz4= github.com/sagernet/sing v0.6.9/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/sagernet/sing v0.7.13 h1:XNYgd8e3cxMULs/LLJspdn/deHrnPWyrrglNHeCUAYM= github.com/sagernet/sing v0.7.13/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/sagernet/sing-box v1.12.12 h1:brSb4zdL5CfqXB0ss1jrT+srPUbKanyWhbswkte/z5Y= github.com/sagernet/sing-box v1.12.12/go.mod h1:ObMeEc1VAcJdXN6B/3SUIJRuK7J38m0W6yLNJ+E5f+0= github.com/sagernet/sing-mux v0.3.3 h1:YFgt9plMWzH994BMZLmyKL37PdIVaIilwP0Jg+EcLfw= github.com/sagernet/sing-mux v0.3.3/go.mod h1:pht8iFY4c9Xltj7rhVd208npkNaeCxzyXCgulDPLUDA= github.com/sagernet/sing-quic v0.5.2-0.20250909083218-00a55617c0fb h1:5Wx3XeTiKrrrcrAky7Hc1bO3CGxrvho2Vu5b/adlEIM= github.com/sagernet/sing-quic v0.5.2-0.20250909083218-00a55617c0fb/go.mod h1:evP1e++ZG8TJHVV5HudXV4vWeYzGfCdF4HwSJZcdqkI= github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE= github.com/sagernet/sing-shadowsocks v0.2.8/go.mod h1:lo7TWEMDcN5/h5B8S0ew+r78ZODn6SwVaFhvB6H+PTI= github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnqqs2gQ2/Qioo= github.com/sagernet/sing-shadowsocks2 v0.2.1/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ= github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 h1:tK+75l64tm9WvEFrYRE1t0YxoFdWQqw/h7Uhzj0vJ+w= github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA= github.com/sagernet/sing-tun v0.7.3 h1:MFnAir+l24ElEyxdfwtY8mqvUUL9nPnL9TDYLkOmVes= github.com/sagernet/sing-tun v0.7.3/go.mod h1:pUEjh9YHQ2gJT6Lk0TYDklh3WJy7lz+848vleGM3JPM= github.com/sagernet/sing-vmess v0.2.7 h1:2ee+9kO0xW5P4mfe6TYVWf9VtY8k1JhNysBqsiYj0sk= github.com/sagernet/sing-vmess v0.2.7/go.mod h1:5aYoOtYksAyS0NXDm0qKeTYW1yoE1bJVcv+XLcVoyJs= github.com/sagernet/smux v1.5.34-mod.2 h1:gkmBjIjlJ2zQKpLigOkFur5kBKdV6bNRoFu2WkltRQ4= github.com/sagernet/smux v1.5.34-mod.2/go.mod h1:0KW0+R+ycvA2INW4gbsd7BNyg+HEfLIAxa5N02/28Zc= github.com/sagernet/tailscale v1.80.3-sing-box-1.12-mod.2 h1:MO7s4ni2bSfAOhcan2rdQSWCztkMXmqyg6jYPZp8bEE= github.com/sagernet/tailscale v1.80.3-sing-box-1.12-mod.2/go.mod h1:EBxXsWu4OH2ELbQLq32WoBeIubG8KgDrg4/Oaxjs6lI= github.com/sagernet/wireguard-go v0.0.1-beta.7 h1:ltgBwYHfr+9Wz1eG59NiWnHrYEkDKHG7otNZvu85DXI= github.com/sagernet/wireguard-go v0.0.1-beta.7/go.mod h1:jGXij2Gn2wbrWuYNUmmNhf1dwcZtvyAvQoe8Xd8MbUo= github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc= github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854/go.mod h1:LtfoSK3+NG57tvnVEHgcuBW9ujgE8enPSgzgwStwCAA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e h1:PtWT87weP5LWHEY//SWsYkSO3RWRZo4OSWagh3YD2vQ= github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e/go.mod h1:XrBNfAFN+pwoWuksbFS9Ccxnopa15zJGgXRFN90l3K4= github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4= github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg= github.com/tailscale/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4 h1:rXZGgEa+k2vJM8xT0PoSKfVXwFGPQ3z3CJfmnHJkZZw= github.com/tailscale/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4/go.mod h1:ikbF+YT089eInTp9f2vmvy4+ZVnW5hzX1q2WknxSprQ= github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPxRs2O36yuGRW3f9SYV+bMTTvMBI0EKio= github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8= github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29XwJucQo73FrleVK6t4kYz4NVhp34Yw= github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8= github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 h1:uFsXVBE9Qr4ZoF094vE6iYTLDl0qCiKzYXlL6UeWObU= github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0= github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc h1:24heQPtnFR+yfntqhI3oAu9i27nEojcQ4NuBQOo5ZFA= github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc/go.mod h1:f93CXfllFsO9ZQVq+Zocb1Gp4G5Fz0b0rXHLOzt/Djc= github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 h1:UBPHPtv8+nEAy2PD8RyAhOYvau1ek0HDJqLS/Pysi14= github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ= github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA= github.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM= github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/ysmood/fetchup v0.2.3 h1:ulX+SonA0Vma5zUFXtv52Kzip/xe7aj4vqT5AJwQ+ZQ= github.com/ysmood/fetchup v0.2.3/go.mod h1:xhibcRKziSvol0H1/pj33dnKrYyI2ebIvz5cOOkYGns= github.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ= github.com/ysmood/goob v0.4.0/go.mod h1:u6yx7ZhS4Exf2MwciFr6nIM8knHQIE22lFpWHnfql18= github.com/ysmood/gop v0.2.0 h1:+tFrG0TWPxT6p9ZaZs+VY+opCvHU8/3Fk6BaNv6kqKg= github.com/ysmood/gop v0.2.0/go.mod h1:rr5z2z27oGEbyB787hpEcx4ab8cCiPnKxn0SUHt6xzk= github.com/ysmood/got v0.40.0 h1:ZQk1B55zIvS7zflRrkGfPDrPG3d7+JOza1ZkNxcc74Q= github.com/ysmood/got v0.40.0/go.mod h1:W7DdpuX6skL3NszLmAsC5hT7JAhuLZhByVzHTq874Qg= github.com/ysmood/gotrace v0.6.0 h1:SyI1d4jclswLhg7SWTL6os3L1WOKeNn/ZtzVQF8QmdY= github.com/ysmood/gotrace v0.6.0/go.mod h1:TzhIG7nHDry5//eYZDYcTzuJLYQIkykJzCRIo4/dzQM= github.com/ysmood/gson v0.7.3 h1:QFkWbTH8MxyUTKPkVWAENJhxqdBa4lYTQWqZCiLG6kE= github.com/ysmood/gson v0.7.3/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg= github.com/ysmood/leakless v0.9.0 h1:qxCG5VirSBvmi3uynXFkcnLMzkphdh3xx5FtrORwDCU= github.com/ysmood/leakless v0.9.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ= github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI= github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE= github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U= go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ= go4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4hOxG5YpKCzkek= go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI= golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= golang.org/x/image v0.38.0 h1:5l+q+Y9JDC7mBOMjo4/aPhMDcxEptsX+Tt3GgRQRPuE= golang.org/x/image v0.38.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY= golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE= golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= ================================================ FILE: main.go ================================================ package main import ( "bytes" "encoding/base64" "encoding/json" "errors" "fmt" "image" _ "image/gif" _ "image/jpeg" "image/png" "io" "log" "net/http" "os" "path/filepath" "strings" "sync" "sync/atomic" "time" _ "golang.org/x/image/bmp" _ "golang.org/x/image/tiff" _ "golang.org/x/image/webp" "github.com/fsnotify/fsnotify" "github.com/gin-gonic/gin" "github.com/google/uuid" "business2api/src/flow" "business2api/src/logger" "business2api/src/pool" "business2api/src/proxy" "business2api/src/register" "business2api/src/utils" ) // ==================== 配置结构 ==================== type PoolConfig struct { TargetCount int `json:"target_count"` // 目标账号数量 MinCount int `json:"min_count"` // 最小账号数,低于此值触发注册 CheckIntervalMinutes int `json:"check_interval_minutes"` // 检查间隔(分钟) RegisterThreads int `json:"register_threads"` // 注册线程数 RegisterHeadless bool `json:"register_headless"` // 无头模式 RefreshOnStartup bool `json:"refresh_on_startup"` // 启动时刷新账号 RefreshCooldownSec int `json:"refresh_cooldown_sec"` // 刷新冷却时间(秒) UseCooldownSec int `json:"use_cooldown_sec"` // 使用冷却时间(秒) MaxFailCount int `json:"max_fail_count"` // 最大连续失败次数 EnableBrowserRefresh bool `json:"enable_browser_refresh"` // 启用浏览器刷新401账号 BrowserRefreshHeadless bool `json:"browser_refresh_headless"` // 浏览器刷新无头模式 BrowserRefreshMaxRetry int `json:"browser_refresh_max_retry"` // 浏览器刷新最大重试次数(0=禁用) AutoDelete401 bool `json:"auto_delete_401"` // 401时自动删除账号 } // FlowConfig Flow 服务配置 type FlowConfigSection struct { Enable bool `json:"enable"` // 是否启用 Flow Tokens []string `json:"tokens"` // Flow ST Tokens Proxy string `json:"proxy"` // Flow 专用代理 Timeout int `json:"timeout"` // 超时时间 PollInterval int `json:"poll_interval"` // 轮询间隔 MaxPollAttempts int `json:"max_poll_attempts"` // 最大轮询次数 } // ProxyConfig 代理配置 type ProxyConfig struct { Proxy string `json:"proxy"` // 单个代理 (http/socks5) Subscribes []string `json:"subscribes"` // 订阅链接列表 Files []string `json:"files"` // 代理文件列表 HealthCheck bool `json:"health_check"` // 是否启用健康检查 CheckOnStartup bool `json:"check_on_startup"` // 启动时检查 } type AppConfig struct { APIKeys []string `json:"api_keys"` // API 密钥列表 ListenAddr string `json:"listen_addr"` // 监听地址 DataDir string `json:"data_dir"` // 数据目录 Pool PoolConfig `json:"pool"` // 号池配置 Proxy string `json:"proxy"` // 代理 (兼容旧配置) ProxySubscribe string `json:"proxy_subscribe"` // 代理订阅链接 (兼容旧配置) ProxyPool ProxyConfig `json:"proxy_pool"` // 代理池配置 DefaultConfig string `json:"default_config"` // 默认 configId PoolServer pool.PoolServerConfig `json:"pool_server"` // 号池服务器配置 Debug bool `json:"debug"` // 调试模式 Flow FlowConfigSection `json:"flow"` // Flow 配置 Note []string `json:"note"` // 备注信息(支持多行) } // PoolMode 号池模式 type PoolMode int const ( PoolModeLocal PoolMode = iota // 本地模式 PoolModeServer // 服务器模式(提供号池服务) PoolModeClient // 客户端模式(使用远程号池) ) var ( poolMode PoolMode remotePoolClient *pool.RemotePoolClient flowClient *flow.FlowClient flowHandler *flow.GenerationHandler flowTokenPool *flow.TokenPool ) // 配置热重载相关 var ( configMu sync.RWMutex // 配置读写锁 configWatcher *fsnotify.Watcher // 配置文件监听器 configPath = "config/config.json" // 配置文件路径 ) // APIStats API 调用统计 type APIStats struct { mu sync.RWMutex startTime time.Time // 服务启动时间 totalRequests int64 // 总请求数 successRequests int64 // 成功请求数 failedRequests int64 // 失败请求数 inputTokens int64 // 输入 tokens outputTokens int64 // 输出 tokens imageGenerated int64 // 生成的图片数 videoGenerated int64 // 生成的视频数 requestTimes []time.Time // 最近请求时间(用于计算 RPM) modelStats map[string]*ModelStats // 每个模型的统计 hourlyStats [24]HourlyStats // 24小时统计 lastHour int // 上次记录的小时 } // ModelStats 模型统计 type ModelStats struct { Requests int64 `json:"requests"` Success int64 `json:"success"` InputTokens int64 `json:"input_tokens"` OutputTokens int64 `json:"output_tokens"` Images int64 `json:"images"` } // HourlyStats 小时统计 type HourlyStats struct { Hour int `json:"hour"` Requests int64 `json:"requests"` Success int64 `json:"success"` InputTokens int64 `json:"input_tokens"` OutputTokens int64 `json:"output_tokens"` } var apiStats = &APIStats{ startTime: time.Now(), requestTimes: make([]time.Time, 0, 1000), modelStats: make(map[string]*ModelStats), lastHour: time.Now().Hour(), } // IPStats IP请求统计 type IPStats struct { mu sync.RWMutex ipRequests map[string]*IPRequestInfo } // IPRequestInfo 单个IP的请求信息 type IPRequestInfo struct { IP string `json:"ip"` TotalCount int64 `json:"total_count"` SuccessCount int64 `json:"success_count"` FailedCount int64 `json:"failed_count"` InputTokens int64 `json:"input_tokens"` OutputTokens int64 `json:"output_tokens"` ImagesCount int64 `json:"images_count"` VideosCount int64 `json:"videos_count"` FirstSeen time.Time `json:"first_seen"` LastSeen time.Time `json:"last_seen"` RequestTimes []time.Time `json:"-"` // 用于计算RPM Models map[string]int64 `json:"models"` UserAgents map[string]int64 `json:"user_agents,omitempty"` } var ipStats = &IPStats{ ipRequests: make(map[string]*IPRequestInfo), } // RecordIPRequest 记录IP请求(包含tokens、图片、视频统计) func (s *IPStats) RecordIPRequest(ip, model, userAgent string, success bool, inputTokens, outputTokens, images, videos int64) { s.mu.Lock() defer s.mu.Unlock() now := time.Now() info, exists := s.ipRequests[ip] if !exists { info = &IPRequestInfo{ IP: ip, FirstSeen: now, Models: make(map[string]int64), UserAgents: make(map[string]int64), RequestTimes: make([]time.Time, 0, 100), } s.ipRequests[ip] = info } info.TotalCount++ info.LastSeen = now info.InputTokens += inputTokens info.OutputTokens += outputTokens info.ImagesCount += images info.VideosCount += videos // 记录请求时间用于计算RPM(保留最近100条) info.RequestTimes = append(info.RequestTimes, now) if len(info.RequestTimes) > 100 { info.RequestTimes = info.RequestTimes[len(info.RequestTimes)-100:] } if success { info.SuccessCount++ } else { info.FailedCount++ } if model != "" { info.Models[model]++ } if userAgent != "" && len(info.UserAgents) < 50 { info.UserAgents[userAgent]++ } } // GetIPRPM 计算单个IP的RPM func (info *IPRequestInfo) GetRPM() float64 { oneMinuteAgo := time.Now().Add(-time.Minute) count := 0 for i := len(info.RequestTimes) - 1; i >= 0; i-- { if info.RequestTimes[i].After(oneMinuteAgo) { count++ } else { break } } return float64(count) } func (s *IPStats) GetAllIPStats() map[string]interface{} { s.mu.RLock() defer s.mu.RUnlock() type ipSortInfo struct { IP string Count int64 } sorted := make([]ipSortInfo, 0, len(s.ipRequests)) for ip, info := range s.ipRequests { sorted = append(sorted, ipSortInfo{IP: ip, Count: info.TotalCount}) } n := len(sorted) for i := 1; i < n; i++ { for j := i; j > 0 && sorted[j].Count > sorted[j-1].Count; j-- { sorted[j], sorted[j-1] = sorted[j-1], sorted[j] } } var totalRequests, totalSuccess, totalFailed int64 var totalInputTokens, totalOutputTokens int64 var totalImages, totalVideos int64 ips := make([]map[string]interface{}, 0, n) for i := 0; i < n; i++ { info := s.ipRequests[sorted[i].IP] rpm := info.GetRPM() totalRequests += info.TotalCount totalSuccess += info.SuccessCount totalFailed += info.FailedCount totalInputTokens += info.InputTokens totalOutputTokens += info.OutputTokens totalImages += info.ImagesCount totalVideos += info.VideosCount ips = append(ips, map[string]interface{}{ "ip": info.IP, "total_count": info.TotalCount, "success_count": info.SuccessCount, "failed_count": info.FailedCount, "success_rate": fmt.Sprintf("%.1f%%", float64(info.SuccessCount)/float64(max(info.TotalCount, 1))*100), "input_tokens": info.InputTokens, "output_tokens": info.OutputTokens, "total_tokens": info.InputTokens + info.OutputTokens, "images": info.ImagesCount, "videos": info.VideosCount, "rpm": rpm, "first_seen": info.FirstSeen.Format(time.RFC3339), "last_seen": info.LastSeen.Format(time.RFC3339), "models": info.Models, "user_agents": info.UserAgents, }) } return map[string]interface{}{ "server_time": time.Now().Format(time.RFC3339), "unique_ips": n, "total_requests": totalRequests, "total_success": totalSuccess, "total_failed": totalFailed, "total_input_tokens": totalInputTokens, "total_output_tokens": totalOutputTokens, "total_tokens": totalInputTokens + totalOutputTokens, "total_images": totalImages, "total_videos": totalVideos, "ips": ips, } } // GetIPDetail 获取单个IP的详细信息 func (s *IPStats) GetIPDetail(ip string) *IPRequestInfo { s.mu.RLock() defer s.mu.RUnlock() return s.ipRequests[ip] } // RecordRequest 记录请求 func (s *APIStats) RecordRequest(success bool, inputTokens, outputTokens, images, videos int64) { s.RecordRequestWithModel("", success, inputTokens, outputTokens, images, videos) } func (s *APIStats) RecordRequestWithModel(model string, success bool, inputTokens, outputTokens, images, videos int64) { s.mu.Lock() defer s.mu.Unlock() s.totalRequests++ if success { s.successRequests++ } else { s.failedRequests++ } s.inputTokens += inputTokens s.outputTokens += outputTokens s.imageGenerated += images s.videoGenerated += videos // 记录请求时间(保留最近1000条) now := time.Now() s.requestTimes = append(s.requestTimes, now) if len(s.requestTimes) > 1000 { s.requestTimes = s.requestTimes[len(s.requestTimes)-1000:] } // 模型统计 if model != "" { if s.modelStats[model] == nil { s.modelStats[model] = &ModelStats{} } ms := s.modelStats[model] ms.Requests++ if success { ms.Success++ } ms.InputTokens += inputTokens ms.OutputTokens += outputTokens ms.Images += images } // 小时统计 currentHour := now.Hour() if currentHour != s.lastHour { // 新的小时,重置该小时统计 s.hourlyStats[currentHour] = HourlyStats{Hour: currentHour} s.lastHour = currentHour } hs := &s.hourlyStats[currentHour] hs.Requests++ if success { hs.Success++ } hs.InputTokens += inputTokens hs.OutputTokens += outputTokens } func (s *APIStats) GetRPM() float64 { s.mu.RLock() defer s.mu.RUnlock() oneMinuteAgo := time.Now().Add(-time.Minute) count := 0 for i := len(s.requestTimes) - 1; i >= 0; i-- { if s.requestTimes[i].After(oneMinuteAgo) { count++ } else { break } } return float64(count) } // GetStats 获取统计数据 func (s *APIStats) GetStats() map[string]interface{} { s.mu.RLock() defer s.mu.RUnlock() uptime := time.Since(s.startTime) avgRPM := float64(0) if uptime.Minutes() > 0 { avgRPM = float64(s.totalRequests) / uptime.Minutes() } return map[string]interface{}{ "uptime": uptime.String(), "uptime_seconds": int64(uptime.Seconds()), "total_requests": s.totalRequests, "success_requests": s.successRequests, "failed_requests": s.failedRequests, "success_rate": fmt.Sprintf("%.2f%%", float64(s.successRequests)/float64(max(s.totalRequests, 1))*100), "input_tokens": s.inputTokens, "output_tokens": s.outputTokens, "total_tokens": s.inputTokens + s.outputTokens, "images_generated": s.imageGenerated, "videos_generated": s.videoGenerated, "current_rpm": s.GetRPM(), "average_rpm": fmt.Sprintf("%.2f", avgRPM), } } // GetDetailedStats 获取详细统计数据 func (s *APIStats) GetDetailedStats() map[string]interface{} { s.mu.RLock() defer s.mu.RUnlock() uptime := time.Since(s.startTime) avgRPM := float64(0) if uptime.Minutes() > 0 { avgRPM = float64(s.totalRequests) / uptime.Minutes() } // 转换模型统计 modelStatsMap := make(map[string]interface{}) for model, ms := range s.modelStats { modelStatsMap[model] = map[string]interface{}{ "requests": ms.Requests, "success": ms.Success, "success_rate": fmt.Sprintf("%.2f%%", float64(ms.Success)/float64(max(ms.Requests, 1))*100), "input_tokens": ms.InputTokens, "output_tokens": ms.OutputTokens, "total_tokens": ms.InputTokens + ms.OutputTokens, "images": ms.Images, } } // 转换小时统计 hourlyStatsArr := make([]map[string]interface{}, 0, 24) for i := 0; i < 24; i++ { hs := s.hourlyStats[i] if hs.Requests > 0 { hourlyStatsArr = append(hourlyStatsArr, map[string]interface{}{ "hour": i, "requests": hs.Requests, "success": hs.Success, "input_tokens": hs.InputTokens, "output_tokens": hs.OutputTokens, }) } } return map[string]interface{}{ "uptime": uptime.String(), "uptime_seconds": int64(uptime.Seconds()), "total_requests": s.totalRequests, "success_requests": s.successRequests, "failed_requests": s.failedRequests, "success_rate": fmt.Sprintf("%.2f%%", float64(s.successRequests)/float64(max(s.totalRequests, 1))*100), "input_tokens": s.inputTokens, "output_tokens": s.outputTokens, "total_tokens": s.inputTokens + s.outputTokens, "images_generated": s.imageGenerated, "videos_generated": s.videoGenerated, "current_rpm": s.GetRPM(), "average_rpm": fmt.Sprintf("%.2f", avgRPM), "models": modelStatsMap, "hourly": hourlyStatsArr, } } func max(a, b int64) int64 { if a > b { return a } return b } var appConfig = AppConfig{ ListenAddr: ":8000", DataDir: "./data", Pool: PoolConfig{ TargetCount: 50, MinCount: 10, CheckIntervalMinutes: 30, RegisterThreads: 1, RegisterHeadless: false, RefreshOnStartup: true, RefreshCooldownSec: 240, // 4分钟 UseCooldownSec: 15, // 15秒 MaxFailCount: 3, EnableBrowserRefresh: true, // 默认启用浏览器刷新 BrowserRefreshHeadless: false, BrowserRefreshMaxRetry: 1, // 浏览器刷新最多重试1次 }, } // GetAPIKeys 线程安全获取 API Keys func GetAPIKeys() []string { configMu.RLock() defer configMu.RUnlock() keys := make([]string, len(appConfig.APIKeys)) copy(keys, appConfig.APIKeys) return keys } // reloadConfig 重新加载配置文件(热重载) func reloadConfig() error { data, err := os.ReadFile(configPath) if err != nil { return fmt.Errorf("读取配置文件失败: %w", err) } var newConfig AppConfig if err := json.Unmarshal(data, &newConfig); err != nil { return fmt.Errorf("解析配置文件失败: %w", err) } configMu.Lock() oldAPIKeys := appConfig.APIKeys oldDebug := appConfig.Debug oldPoolConfig := appConfig.Pool // 更新可热重载的配置项 appConfig.APIKeys = newConfig.APIKeys appConfig.Debug = newConfig.Debug appConfig.Note = newConfig.Note // 更新号池配置 appConfig.Pool.RefreshCooldownSec = newConfig.Pool.RefreshCooldownSec appConfig.Pool.UseCooldownSec = newConfig.Pool.UseCooldownSec appConfig.Pool.MaxFailCount = newConfig.Pool.MaxFailCount appConfig.Pool.EnableBrowserRefresh = newConfig.Pool.EnableBrowserRefresh appConfig.Pool.BrowserRefreshHeadless = newConfig.Pool.BrowserRefreshHeadless appConfig.Pool.BrowserRefreshMaxRetry = newConfig.Pool.BrowserRefreshMaxRetry appConfig.Pool.AutoDelete401 = newConfig.Pool.AutoDelete401 configMu.Unlock() // 应用变更 applyConfigChanges(oldAPIKeys, oldDebug, oldPoolConfig, newConfig) return nil } // applyConfigChanges 应用配置变更 func applyConfigChanges(oldAPIKeys []string, oldDebug bool, oldPoolConfig PoolConfig, newConfig AppConfig) { // 日志模式变更 if oldDebug != newConfig.Debug { logger.SetDebugMode(newConfig.Debug) logger.Info("🔄 调试模式: %v -> %v", oldDebug, newConfig.Debug) } // API Keys 变更 if len(oldAPIKeys) != len(newConfig.APIKeys) { logger.Info("🔄 API Keys 数量: %d -> %d", len(oldAPIKeys), len(newConfig.APIKeys)) } // 号池配置变更 if oldPoolConfig.RefreshCooldownSec != newConfig.Pool.RefreshCooldownSec || oldPoolConfig.UseCooldownSec != newConfig.Pool.UseCooldownSec { pool.SetCooldowns(newConfig.Pool.RefreshCooldownSec, newConfig.Pool.UseCooldownSec) logger.Info("🔄 冷却配置已更新: refresh=%ds, use=%ds", newConfig.Pool.RefreshCooldownSec, newConfig.Pool.UseCooldownSec) } if newConfig.Pool.MaxFailCount > 0 { pool.MaxFailCount = newConfig.Pool.MaxFailCount } pool.EnableBrowserRefresh = newConfig.Pool.EnableBrowserRefresh pool.BrowserRefreshHeadless = newConfig.Pool.BrowserRefreshHeadless if newConfig.Pool.BrowserRefreshMaxRetry >= 0 { pool.BrowserRefreshMaxRetry = newConfig.Pool.BrowserRefreshMaxRetry } pool.AutoDelete401 = newConfig.Pool.AutoDelete401 logger.Info("✅ 配置热重载完成") } // startConfigWatcher 启动配置文件监听 func startConfigWatcher() error { watcher, err := fsnotify.NewWatcher() if err != nil { return fmt.Errorf("创建配置监听器失败: %w", err) } configWatcher = watcher go configWatchLoop() // 监听配置目录 configDir := filepath.Dir(configPath) if err := watcher.Add(configDir); err != nil { return fmt.Errorf("添加配置目录监听失败: %w", err) } logger.Info("🔄 配置文件热重载已启用: %s", configPath) return nil } // configWatchLoop 配置文件监听循环 func configWatchLoop() { var lastReload time.Time const debounceDelay = 500 * time.Millisecond for { select { case event, ok := <-configWatcher.Events: if !ok { return } // 只关注配置文件 if filepath.Base(event.Name) != "config.json" { continue } // 只处理写入和创建事件 if event.Op&(fsnotify.Write|fsnotify.Create) == 0 { continue } // 防抖:避免短时间内多次触发 if time.Since(lastReload) < debounceDelay { continue } lastReload = time.Now() // 等待文件写入完成 time.Sleep(100 * time.Millisecond) logger.Info("📝 检测到配置文件变更,正在重载...") if err := reloadConfig(); err != nil { logger.Error("❌ 配置重载失败: %v", err) } case err, ok := <-configWatcher.Errors: if !ok { return } logger.Error("❌ 配置监听错误: %v", err) } } } // stopConfigWatcher 停止配置文件监听 func stopConfigWatcher() { if configWatcher != nil { configWatcher.Close() } } var ( DataDir string Proxy string ListenAddr string DefaultConfig string JwtTTL = 270 * time.Second ) // mergeConfig 合并配置:loaded 中有值的字段覆盖 base 中的默认值 func mergeConfig(base, loaded *AppConfig) { // 基本字段 if len(loaded.APIKeys) > 0 { base.APIKeys = loaded.APIKeys } if loaded.ListenAddr != "" { base.ListenAddr = loaded.ListenAddr } if loaded.DataDir != "" { base.DataDir = loaded.DataDir } if loaded.Proxy != "" { base.Proxy = loaded.Proxy } if loaded.DefaultConfig != "" { base.DefaultConfig = loaded.DefaultConfig } // Debug 是 bool,直接覆盖 base.Debug = loaded.Debug // Pool 配置 if loaded.Pool.TargetCount > 0 { base.Pool.TargetCount = loaded.Pool.TargetCount } if loaded.Pool.MinCount > 0 { base.Pool.MinCount = loaded.Pool.MinCount } if loaded.Pool.CheckIntervalMinutes > 0 { base.Pool.CheckIntervalMinutes = loaded.Pool.CheckIntervalMinutes } if loaded.Pool.RegisterThreads > 0 { base.Pool.RegisterThreads = loaded.Pool.RegisterThreads } // bool 字段直接覆盖 base.Pool.RegisterHeadless = loaded.Pool.RegisterHeadless base.Pool.RefreshOnStartup = loaded.Pool.RefreshOnStartup base.Pool.EnableBrowserRefresh = loaded.Pool.EnableBrowserRefresh base.Pool.BrowserRefreshHeadless = loaded.Pool.BrowserRefreshHeadless base.Pool.AutoDelete401 = loaded.Pool.AutoDelete401 if loaded.Pool.RefreshCooldownSec > 0 { base.Pool.RefreshCooldownSec = loaded.Pool.RefreshCooldownSec } if loaded.Pool.UseCooldownSec > 0 { base.Pool.UseCooldownSec = loaded.Pool.UseCooldownSec } if loaded.Pool.MaxFailCount > 0 { base.Pool.MaxFailCount = loaded.Pool.MaxFailCount } if loaded.Pool.BrowserRefreshMaxRetry > 0 { base.Pool.BrowserRefreshMaxRetry = loaded.Pool.BrowserRefreshMaxRetry } // PoolServer 配置 base.PoolServer = loaded.PoolServer // Flow 配置 base.Flow = loaded.Flow // ProxyPool 配置 if len(loaded.ProxyPool.Subscribes) > 0 { base.ProxyPool.Subscribes = loaded.ProxyPool.Subscribes } if len(loaded.ProxyPool.Files) > 0 { base.ProxyPool.Files = loaded.ProxyPool.Files } if loaded.ProxyPool.Proxy != "" { base.ProxyPool.Proxy = loaded.ProxyPool.Proxy } base.ProxyPool.HealthCheck = loaded.ProxyPool.HealthCheck base.ProxyPool.CheckOnStartup = loaded.ProxyPool.CheckOnStartup // Note if len(loaded.Note) > 0 { base.Note = loaded.Note } } // 保存默认配置到文件 func saveDefaultConfig(configPath string) error { // 确保目录存在 dir := filepath.Dir(configPath) if err := os.MkdirAll(dir, 0755); err != nil { return err } data, err := json.MarshalIndent(appConfig, "", " ") if err != nil { return err } return os.WriteFile(configPath, data, 0644) } func loadAppConfig() { // 尝试加载配置文件 configPath := "config/config.json" if data, err := os.ReadFile(configPath); err == nil { // 保留默认值,仅覆盖配置文件中存在的字段 var loadedConfig AppConfig if err := json.Unmarshal(data, &loadedConfig); err != nil { logger.Warn("⚠️ 解析配置文件失败: %v,使用默认配置", err) } else { // 合并配置:配置文件中有的字段覆盖默认值,没有的保留默认值 mergeConfig(&appConfig, &loadedConfig) logger.Info("✅ 加载配置文件: %s", configPath) } } else if os.IsNotExist(err) { // 配置文件不存在,创建默认配置 logger.Warn("⚠️ 配置文件不存在,创建默认配置: %s", configPath) if err := saveDefaultConfig(configPath); err != nil { logger.Error("❌ 创建默认配置失败: %v", err) } } if v := os.Getenv("DATA_DIR"); v != "" { appConfig.DataDir = v } if v := os.Getenv("PROXY"); v != "" { appConfig.Proxy = v } if v := os.Getenv("LISTEN_ADDR"); v != "" { appConfig.ListenAddr = v } if v := os.Getenv("CONFIG_ID"); v != "" { appConfig.DefaultConfig = v } if v := os.Getenv("API_KEY"); v != "" { appConfig.APIKeys = append(appConfig.APIKeys, v) } // 设置全局变量 DataDir = appConfig.DataDir Proxy = appConfig.Proxy ListenAddr = appConfig.ListenAddr DefaultConfig = appConfig.DefaultConfig // 应用调试模式 logger.SetDebugMode(appConfig.Debug) // 应用号池配置 pool.SetCooldowns(appConfig.Pool.RefreshCooldownSec, appConfig.Pool.UseCooldownSec) if appConfig.Pool.MaxFailCount > 0 { pool.MaxFailCount = appConfig.Pool.MaxFailCount } pool.EnableBrowserRefresh = appConfig.Pool.EnableBrowserRefresh pool.BrowserRefreshHeadless = appConfig.Pool.BrowserRefreshHeadless if appConfig.Pool.BrowserRefreshMaxRetry >= 0 { pool.BrowserRefreshMaxRetry = appConfig.Pool.BrowserRefreshMaxRetry } pool.AutoDelete401 = appConfig.Pool.AutoDelete401 // 服务端模式下,如果 expired_action 是 delete,则同步设置 AutoDelete401 if appConfig.PoolServer.Enable && appConfig.PoolServer.Mode == "server" && appConfig.PoolServer.ExpiredAction == "delete" { pool.AutoDelete401 = true logger.Info("🗑️ 服务端模式 expired_action=delete,启用 AutoDelete401") } pool.DataDir = DataDir pool.DefaultConfig = DefaultConfig pool.Proxy = Proxy register.DataDir = DataDir register.TargetCount = appConfig.Pool.TargetCount register.MinCount = appConfig.Pool.MinCount register.CheckInterval = time.Duration(appConfig.Pool.CheckIntervalMinutes) * time.Minute register.Threads = appConfig.Pool.RegisterThreads register.Headless = appConfig.Pool.RegisterHeadless register.Proxy = Proxy // 初始化代理池 initProxyPool() if pool.EnableBrowserRefresh && pool.BrowserRefreshMaxRetry > 0 { logger.Info("🌐 浏览器刷新已启用 (headless=%v, 最大重试=%d)", pool.BrowserRefreshHeadless, pool.BrowserRefreshMaxRetry) } else if pool.EnableBrowserRefresh { logger.Info("🌐 浏览器刷新已禁用 (max_retry=0)") pool.EnableBrowserRefresh = false } // 初始化 Flow 客户端 initFlowClient() } // initFlowClient 初始化 Flow 客户端 func initFlowClient() { if !appConfig.Flow.Enable { logger.Info("📹 Flow 服务已禁用") return } cfg := flow.FlowConfig{ Proxy: appConfig.Flow.Proxy, Timeout: appConfig.Flow.Timeout, PollInterval: appConfig.Flow.PollInterval, MaxPollAttempts: appConfig.Flow.MaxPollAttempts, } if cfg.Proxy == "" { cfg.Proxy = Proxy } flowClient = flow.NewFlowClient(cfg) // 初始化 Token 池 flowTokenPool = flow.NewTokenPool(DataDir, flowClient) // 从 data/at 目录加载 Token loadedFromDir, err := flowTokenPool.LoadFromDir() if err != nil { logger.Warn("⚠️ 从 data/at 加载 Flow Token 失败: %v", err) } // 添加配置文件中的 Tokens(兼容旧配置) for i, st := range appConfig.Flow.Tokens { token := &flow.FlowToken{ ID: fmt.Sprintf("flow_token_%d", i), ST: st, } flowClient.AddToken(token) } totalTokens := loadedFromDir + len(appConfig.Flow.Tokens) if totalTokens == 0 { logger.Info("📹 Flow 服务已启用但无可用 Token (请将 cookie 放入 data/at/ 目录)") flowHandler = flow.NewGenerationHandler(flowClient) return } // 启动 AT 刷新 worker (每 30 分钟刷新一次) flowTokenPool.StartRefreshWorker(30 * time.Minute) // 启动文件监听 (自动加载新增 Token) if err := flowTokenPool.StartWatcher(); err != nil { logger.Warn("⚠️ Flow 文件监听启动失败: %v", err) } flowHandler = flow.NewGenerationHandler(flowClient) logger.Info("📹 Flow 服务已启用,共 %d 个 Token (目录: %d, 配置: %d)", totalTokens, loadedFromDir, len(appConfig.Flow.Tokens)) } func initProxyPool() { // 服务端模式不需要代理池 if appConfig.PoolServer.Enable && appConfig.PoolServer.Mode == "server" { logger.Info("🖥️ 服务端模式,跳过代理初始化") return } // 初始化 sing-box(用于 hysteria2/tuic 等协议) proxy.InitSingbox() // 添加订阅链接(新配置) for _, sub := range appConfig.ProxyPool.Subscribes { proxy.Manager.AddSubscribeURL(sub) } // 兼容旧配置 if appConfig.ProxySubscribe != "" { proxy.Manager.AddSubscribeURL(appConfig.ProxySubscribe) } // 添加代理文件 for _, file := range appConfig.ProxyPool.Files { proxy.Manager.AddProxyFile(file) } if err := proxy.Manager.LoadAll(); err != nil { logger.Warn("⚠️ 加载代理失败: %v", err) } // 当有代理配置时,默认开启健康检查(除非明确关闭) hasProxyConfig := len(appConfig.ProxyPool.Subscribes) > 0 || len(appConfig.ProxyPool.Files) > 0 || appConfig.ProxySubscribe != "" shouldHealthCheck := hasProxyConfig || appConfig.ProxyPool.HealthCheck if shouldHealthCheck && appConfig.ProxyPool.CheckOnStartup { go func() { proxy.Manager.CheckAllHealth() // 健康检查完成后初始化实例池 if proxy.Manager.HealthyCount() > 0 { poolSize := appConfig.Pool.RegisterThreads if poolSize <= 0 { poolSize = pool.DefaultProxyCount } if poolSize > 10 { poolSize = 10 } proxy.Manager.SetMaxPoolSize(poolSize) if err := proxy.Manager.InitInstancePool(poolSize); err != nil { logger.Warn("⚠️ 初始化代理实例池失败: %v", err) } else { logger.Info("✅ 代理实例池初始化完成: %d 个实例", poolSize) } } }() } else if proxy.Manager.TotalCount() > 0 { // 不需要健康检查时直接标记就绪 proxy.Manager.SetReady(true) } if proxy.Manager.TotalCount() == 0 { if appConfig.ProxyPool.Proxy != "" { proxy.Manager.SetProxies([]string{appConfig.ProxyPool.Proxy}) } else if Proxy != "" { proxy.Manager.SetProxies([]string{Proxy}) } } if proxy.Manager.TotalCount() == 0 || AutoSubscribeEnabled { logger.Info("🔄 启动自动订阅服务(每小时注册获取代理)...") proxy.Manager.StartAutoSubscribe() } if proxy.Manager.TotalCount() > 0 { proxy.Manager.StartAutoUpdate() logger.Info("✅ 代理池已初始化: %d 个节点, %d 个健康", proxy.Manager.TotalCount(), proxy.Manager.HealthyCount()) } register.GetProxy = func() string { if proxy.Manager.Count() > 0 { return proxy.Manager.Next() } return Proxy } register.ReleaseProxy = func(proxyURL string) { proxy.Manager.ReleaseByURL(proxyURL) } } var BaseModels = []string{ // Gemini 文本模型 "gemini-2.5-flash", "gemini-2.5-pro", "gemini-3-pro-preview", "gemini-3-pro", // Gemini 图片生成 "gemini-2.5-flash-image", "gemini-2.5-pro-image", "gemini-3-pro-preview-image", "gemini-3-pro-image", // Gemini 视频生成 "gemini-2.5-flash-video", "gemini-2.5-pro-video", "gemini-3-pro-preview-video", "gemini-3-pro-video", // Gemini 搜索 "gemini-2.5-flash-search", "gemini-2.5-pro-search", "gemini-3-pro-preview-search", "gemini-3-pro-search", "gemini-3-flash-preview", "gemini-3-flash-preview-image", "gemini-3-flash-preview-video", "gemini-3-flash-preview-search", "gemini-3-flash", "gemini-3-flash-image", "gemini-3-flash-video", "gemini-3-flash-search", "gemini-2.5-flash-preview-latest", "gemini-2.5-flash-preview-latest-image", "gemini-2.5-flash-preview-latest-video", "gemini-2.5-flash-preview-latest-search", } var FlowModels = []string{ // Flow 图片生成模型 "gemini-2.5-flash-image-landscape", "gemini-2.5-flash-image-portrait", "gemini-3.0-pro-image-landscape", "gemini-3.0-pro-image-portrait", "imagen-4.0-generate-preview-landscape", "imagen-4.0-generate-preview-portrait", // Flow 文生视频 (T2V) "veo_3_1_t2v_fast_portrait", "veo_3_1_t2v_fast_landscape", "veo_2_1_fast_d_15_t2v_portrait", "veo_2_1_fast_d_15_t2v_landscape", "veo_2_0_t2v_portrait", "veo_2_0_t2v_landscape", // Flow 图生视频 (I2V) "veo_3_1_i2v_s_fast_fl_portrait", "veo_3_1_i2v_s_fast_fl_landscape", "veo_2_1_fast_d_15_i2v_portrait", "veo_2_1_fast_d_15_i2v_landscape", "veo_2_0_i2v_portrait", "veo_2_0_i2v_landscape", // Flow 多图生成视频 (R2V) "veo_3_0_r2v_fast_portrait", "veo_3_0_r2v_fast_landscape", } func GetAvailableModels() []string { if flowHandler != nil { // Flow 已启用,返回全部模型 return append(BaseModels, FlowModels...) } // Flow 未启用,只返回基础模型 return BaseModels } // 模型名称映射到 Google API 的 modelId var modelMapping = map[string]string{ "gemini-2.5-flash": "gemini-2.5-flash", "gemini-2.5-pro": "gemini-2.5-pro", "gemini-3-pro-preview": "gemini-3-pro-preview", "gemini-3-pro": "gemini-3-pro", } func getEnv(key, def string) string { if v := os.Getenv(key); v != "" { return v } return def } func getCommonHeaders(jwt, origAuth string) map[string]string { headers := map[string]string{ "accept": "*/*", "accept-encoding": "gzip, deflate, br, zstd", "accept-language": "zh-CN,zh;q=0.9,en;q=0.8", "authorization": "Bearer " + jwt, "content-type": "application/json", "origin": "https://business.gemini.google", "referer": "https://business.gemini.google/", "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36", "x-server-timeout": "1800", "sec-ch-ua": `"Chromium";v="124", "Google Chrome";v="124", "Not-A.Brand";v="99"`, "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": `"Windows"`, "sec-fetch-dest": "empty", "sec-fetch-mode": "cors", "sec-fetch-site": "cross-site", } // 同时携带原始 authorization if origAuth != "" { headers["x-original-authorization"] = origAuth } return headers } func createSession(jwt, configID, origAuth string) (string, error) { return createSessionWithRetry(jwt, configID, origAuth, 3) } // createSessionWithRetry 创建session带重试(处理400错误) func createSessionWithRetry(jwt, configID, origAuth string, maxRetries int) (string, error) { var lastErr error for retry := 0; retry < maxRetries; retry++ { if retry > 0 { // 等待后重试 waitTime := time.Duration(retry*500) * time.Millisecond time.Sleep(waitTime) logger.Info("🔄 createSession 重试 %d/%d", retry+1, maxRetries) } sessionName, err := createSessionOnce(jwt, configID, origAuth) if err == nil { return sessionName, nil } lastErr = err errMsg := err.Error() // 400错误可以重试 if strings.Contains(errMsg, "400") { logger.Warn("⚠️ createSession 400 错误,尝试重试...") continue } // 401/403 不重试 if strings.Contains(errMsg, "401") || strings.Contains(errMsg, "403") { return "", err } // 其他错误继续重试 } return "", lastErr } // createSessionOnce 单次创建session func createSessionOnce(jwt, configID, origAuth string) (string, error) { body := map[string]interface{}{ "configId": configID, "additionalParams": map[string]string{"token": "-"}, "createSessionRequest": map[string]interface{}{ "session": map[string]string{"name": "", "displayName": ""}, }, } bodyBytes, _ := json.Marshal(body) req, _ := http.NewRequest("POST", "https://biz-discoveryengine.googleapis.com/v1alpha/locations/global/widgetCreateSession", bytes.NewReader(bodyBytes)) for k, v := range getCommonHeaders(jwt, origAuth) { req.Header.Set(k, v) } resp, err := utils.HTTPClient.Do(req) if err != nil { return "", fmt.Errorf("createSession 请求失败: %w", err) } defer resp.Body.Close() respBody, err := utils.ReadResponseBody(resp) if err != nil { return "", fmt.Errorf("读取响应失败: %w", err) } if resp.StatusCode != 200 { return "", fmt.Errorf("createSession 失败: %d %s", resp.StatusCode, string(respBody)) } var result struct { Session struct { Name string `json:"name"` } `json:"session"` } if err := json.Unmarshal(respBody, &result); err != nil { return "", fmt.Errorf("解析 session 响应失败: %w", err) } return result.Session.Name, nil } func uploadContextFile(jwt, configID, sessionName, mimeType, base64Content, origAuth string) (string, error) { ext := "jpg" if parts := strings.Split(mimeType, "/"); len(parts) == 2 { ext = parts[1] } fileName := fmt.Sprintf("upload_%d_%s.%s", time.Now().Unix(), uuid.New().String()[:6], ext) body := map[string]interface{}{ "configId": configID, "additionalParams": map[string]string{"token": "-"}, "addContextFileRequest": map[string]interface{}{ "name": sessionName, "fileName": fileName, "mimeType": mimeType, "fileContents": base64Content, }, } bodyBytes, _ := json.Marshal(body) req, _ := http.NewRequest("POST", "https://biz-discoveryengine.googleapis.com/v1alpha/locations/global/widgetAddContextFile", bytes.NewReader(bodyBytes)) for k, v := range getCommonHeaders(jwt, origAuth) { req.Header.Set(k, v) } resp, err := utils.HTTPClient.Do(req) if err != nil { return "", fmt.Errorf("上传文件请求失败: %w", err) } defer resp.Body.Close() respBody, err := utils.ReadResponseBody(resp) if err != nil { return "", fmt.Errorf("读取响应失败: %w", err) } if resp.StatusCode != 200 { return "", fmt.Errorf("上传文件失败: %d %s", resp.StatusCode, string(respBody)) } var result struct { AddContextFileResponse struct { FileID string `json:"fileId"` } `json:"addContextFileResponse"` } if err := json.Unmarshal(respBody, &result); err != nil { return "", fmt.Errorf("解析上传响应失败: %w", err) } if result.AddContextFileResponse.FileID == "" { return "", fmt.Errorf("上传成功但 fileId 为空,响应: %s", string(respBody)) } return result.AddContextFileResponse.FileID, nil } func uploadContextFileByURL(jwt, configID, sessionName, imageURL, origAuth string) (string, error) { body := map[string]interface{}{ "configId": configID, "additionalParams": map[string]string{"token": "-"}, "addContextFileRequest": map[string]interface{}{ "name": sessionName, "fileUri": imageURL, }, } bodyBytes, _ := json.Marshal(body) req, _ := http.NewRequest("POST", "https://biz-discoveryengine.googleapis.com/v1alpha/locations/global/widgetAddContextFile", bytes.NewReader(bodyBytes)) for k, v := range getCommonHeaders(jwt, origAuth) { req.Header.Set(k, v) } resp, err := utils.HTTPClient.Do(req) if err != nil { return "", fmt.Errorf("上传文件请求失败: %w", err) } defer resp.Body.Close() respBody, err := utils.ReadResponseBody(resp) if err != nil { return "", fmt.Errorf("读取响应失败: %w", err) } if resp.StatusCode != 200 { return "", fmt.Errorf("URL上传文件失败: %d %s", resp.StatusCode, string(respBody)) } var result struct { AddContextFileResponse struct { FileID string `json:"fileId"` } `json:"addContextFileResponse"` } if err := json.Unmarshal(respBody, &result); err != nil { return "", fmt.Errorf("解析上传响应失败: %w", err) } if result.AddContextFileResponse.FileID == "" { return "", fmt.Errorf("URL上传成功但 fileId 为空,响应: %s", string(respBody)) } return result.AddContextFileResponse.FileID, nil } type Message struct { Role string `json:"role"` Content interface{} `json:"content"` // string 或 []ContentPart Name string `json:"name,omitempty"` // 函数名称(tool角色时) ToolCalls []ToolCall `json:"tool_calls,omitempty"` // 工具调用(assistant角色时) ToolCallID string `json:"tool_call_id,omitempty"` // 工具调用ID(tool角色时) } type ContentPart struct { Type string `json:"type"` Text string `json:"text,omitempty"` ImageURL *ImageURL `json:"image_url,omitempty"` } type ImageURL struct { URL string `json:"url"` } // OpenAI格式的工具定义 type ToolDef struct { Type string `json:"type"` // "function" Function FunctionDef `json:"function"` } type FunctionDef struct { Name string `json:"name"` Description string `json:"description"` Parameters map[string]interface{} `json:"parameters"` } // 工具调用结果 type ToolCall struct { ID string `json:"id"` Type string `json:"type"` // "function" Function FunctionCall `json:"function"` } type FunctionCall struct { Name string `json:"name"` Arguments string `json:"arguments"` } type ChatRequest struct { Model string `json:"model"` Messages []Message `json:"messages"` Stream bool `json:"stream"` Temperature float64 `json:"temperature"` TopP float64 `json:"top_p"` Tools []ToolDef `json:"tools,omitempty"` // 工具定义 ToolChoice string `json:"tool_choice,omitempty"` // "auto", "none", "required" } type ChatChoice struct { Index int `json:"index"` Delta map[string]interface{} `json:"delta,omitempty"` Message map[string]interface{} `json:"message,omitempty"` FinishReason *string `json:"finish_reason"` Logprobs interface{} `json:"logprobs"` // OpenAI兼容 } type ChatChunk struct { ID string `json:"id"` Object string `json:"object"` Created int64 `json:"created"` Model string `json:"model"` SystemFingerprint string `json:"system_fingerprint,omitempty"` Choices []ChatChoice `json:"choices"` } func createChunk(id string, created int64, model string, delta map[string]interface{}, finishReason *string) string { if delta == nil { delta = map[string]interface{}{} } chunk := ChatChunk{ ID: id, Object: "chat.completion.chunk", Created: created, Model: model, Choices: []ChatChoice{{ Index: 0, Delta: delta, FinishReason: finishReason, Logprobs: nil, }}, } data, _ := json.Marshal(chunk) return string(data) } func extractContentFromReply(replyMap map[string]interface{}, jwt, session, configID, origAuth string) (text string, imageData string, imageMime string, reasoning string, downloadErr error) { groundedContent, ok := replyMap["groundedContent"].(map[string]interface{}) if !ok { return } content, ok := groundedContent["content"].(map[string]interface{}) if !ok { return } if thought, ok := content["thought"].(bool); ok && thought { if t, ok := content["text"].(string); ok && t != "" { reasoning = t } return } if t, ok := content["text"].(string); ok && t != "" { text = t } if inlineData, ok := content["inlineData"].(map[string]interface{}); ok { if mime, ok := inlineData["mimeType"].(string); ok { imageMime = mime } if data, ok := inlineData["data"].(string); ok { imageData = data } } if file, ok := content["file"].(map[string]interface{}); ok { fileId, _ := file["fileId"].(string) mimeType, _ := file["mimeType"].(string) if fileId != "" { fileType := "文件" if strings.HasPrefix(mimeType, "image/") { fileType = "图片" } else if strings.HasPrefix(mimeType, "video/") { fileType = "视频" } data, err := downloadGeneratedFile(jwt, fileId, session, configID, origAuth) if err != nil { logger.Error("❌ 下载%s失败: %v", fileType, err) downloadErr = err // 返回错误供上层处理 } else { imageData = data imageMime = mimeType } } } return } // ErrDownloadNeedsRetry 标识下载失败需要整体重试(换号重新生成) var ErrDownloadNeedsRetry = fmt.Errorf("DOWNLOAD_NEEDS_RETRY") func downloadGeneratedFile(jwt, fileId, session, configID, origAuth string) (string, error) { return downloadGeneratedFileWithRetry(jwt, fileId, session, configID, origAuth, 2) } func downloadGeneratedFileWithRetry(jwt, fileId, session, configID, origAuth string, maxRetries int) (string, error) { // 参数验证 if jwt == "" { return "", fmt.Errorf("JWT 为空,无法下载文件") } if session == "" { return "", fmt.Errorf("session 为空,无法下载文件") } if configID == "" { return "", fmt.Errorf("configID 为空,无法下载文件") } var lastErr error var authFailCount int for retry := 0; retry < maxRetries; retry++ { result, err := downloadGeneratedFileOnce(jwt, fileId, session, configID, origAuth) if err == nil { return result, nil } lastErr = err errMsg := err.Error() // 检测认证失败(401/403) if strings.Contains(errMsg, "401") || strings.Contains(errMsg, "403") || strings.Contains(errMsg, "UNAUTHENTICATED") || strings.Contains(errMsg, "SESSION_COOKIE_INVALID") { authFailCount++ logger.Warn("⚠️ 下载文件认证失败 (尝试 %d/%d): %v", retry+1, maxRetries, err) // 认证失败超过1次,返回特殊错误让上层重新发起整个请求 if authFailCount >= 1 { logger.Info("🔄 下载认证失败,需要换号重新生成") return "", fmt.Errorf("%w: 401/403 认证失败", ErrDownloadNeedsRetry) } continue } // 其他错误,等待后重试 logger.Error("❌ 下载文件失败 (尝试 %d/%d): %v", retry+1, maxRetries, err) time.Sleep(300 * time.Millisecond) } return "", fmt.Errorf("下载文件失败,已重试 %d 次: %w", maxRetries, lastErr) } // downloadGeneratedFileOnce 单次下载文件尝试 func downloadGeneratedFileOnce(jwt, fileId, session, configID, origAuth string) (string, error) { // 步骤1: 使用 widgetListSessionFileMetadata 获取文件下载 URL listBody := map[string]interface{}{ "configId": configID, "additionalParams": map[string]string{"token": "-"}, "listSessionFileMetadataRequest": map[string]interface{}{ "name": session, "filter": "file_origin_type = AI_GENERATED", }, } listBodyBytes, _ := json.Marshal(listBody) listReq, _ := http.NewRequest("POST", "https://biz-discoveryengine.googleapis.com/v1alpha/locations/global/widgetListSessionFileMetadata", bytes.NewReader(listBodyBytes)) for k, v := range getCommonHeaders(jwt, origAuth) { listReq.Header.Set(k, v) } listResp, err := utils.HTTPClient.Do(listReq) if err != nil { return "", fmt.Errorf("获取文件元数据失败: %w", err) } defer listResp.Body.Close() listRespBody, _ := utils.ReadResponseBody(listResp) if listResp.StatusCode != 200 { return "", fmt.Errorf("获取文件元数据失败: HTTP %d: %s", listResp.StatusCode, string(listRespBody)) } // 解析响应,查找匹配的 fileId var listResult struct { ListSessionFileMetadataResponse struct { FileMetadata []struct { FileID string `json:"fileId"` Session string `json:"session"` // 包含完整的 projects 路径 DownloadURI string `json:"downloadUri"` } `json:"fileMetadata"` } `json:"listSessionFileMetadataResponse"` } if err := json.Unmarshal(listRespBody, &listResult); err != nil { return "", fmt.Errorf("解析文件元数据失败: %w", err) } // 查找匹配的文件,获取完整 session 路径 var fullSession string for _, meta := range listResult.ListSessionFileMetadataResponse.FileMetadata { if meta.FileID == fileId { fullSession = meta.Session // 如: projects/372889301682/locations/global/collections/... break } } if fullSession == "" { return "", fmt.Errorf("未找到 fileId=%s 的文件信息", fileId) } downloadURL := fmt.Sprintf("https://biz-discoveryengine.googleapis.com/download/v1alpha/%s:downloadFile?fileId=%s&alt=media", fullSession, fileId) downloadReq, _ := http.NewRequest("GET", downloadURL, nil) for k, v := range getCommonHeaders(jwt, origAuth) { downloadReq.Header.Set(k, v) } downloadResp, err := utils.HTTPClient.Do(downloadReq) if err != nil { return "", fmt.Errorf("下载图片失败: %w", err) } defer downloadResp.Body.Close() imgBody, _ := utils.ReadResponseBody(downloadResp) if downloadResp.StatusCode != 200 { return "", fmt.Errorf("下载图片失败: HTTP %d: %s", downloadResp.StatusCode, string(imgBody)) } // 响应是原始二进制图片数据,需要转为 base64 return base64.StdEncoding.EncodeToString(imgBody), nil } // 将图片转换为 Markdown 格式的 data URI func formatImageAsMarkdown(mimeType, base64Data string) string { return fmt.Sprintf("![image](data:%s;base64,%s)", mimeType, base64Data) } // 媒体信息(图片/视频) type MediaInfo struct { MimeType string Data string // base64 数据 URL string // 原始 URL(如果有) IsURL bool // 是否使用 URL 直接上传 MediaType string // "image" 或 "video" } // 别名,保持向后兼容 type ImageInfo = MediaInfo // 解析消息内容,支持文本、图片和视频 func parseMessageContent(msg Message) (string, []MediaInfo) { var textContent string var medias []MediaInfo switch content := msg.Content.(type) { case string: textContent = content case []interface{}: for _, part := range content { partMap, ok := part.(map[string]interface{}) if !ok { continue } partType, _ := partMap["type"].(string) switch partType { case "text": if text, ok := partMap["text"].(string); ok { textContent += text } case "image_url": if imgURL, ok := partMap["image_url"].(map[string]interface{}); ok { if urlStr, ok := imgURL["url"].(string); ok { media := parseMediaURL(urlStr, "image") if media != nil { medias = append(medias, *media) } } } case "video_url": // 支持视频 URL if videoURL, ok := partMap["video_url"].(map[string]interface{}); ok { if urlStr, ok := videoURL["url"].(string); ok { media := parseMediaURL(urlStr, "video") if media != nil { medias = append(medias, *media) } } } case "file": // 支持通用文件类型 if fileData, ok := partMap["file"].(map[string]interface{}); ok { if urlStr, ok := fileData["url"].(string); ok { mediaType := "image" // 默认图片 if mime, ok := fileData["mime_type"].(string); ok { if strings.HasPrefix(mime, "video/") { mediaType = "video" } } media := parseMediaURL(urlStr, mediaType) if media != nil { medias = append(medias, *media) } } } } } } return textContent, medias } // 解析媒体 URL(图片或视频) func parseMediaURL(urlStr, defaultType string) *MediaInfo { // 处理 base64 数据 if strings.HasPrefix(urlStr, "data:") { // data:image/jpeg;base64,/9j/4AAQ... 或 data:video/mp4;base64,... parts := strings.SplitN(urlStr, ",", 2) if len(parts) != 2 { return nil } base64Data := parts[1] var mediaType string var mimeType string // 检测媒体类型 if strings.Contains(parts[0], "video/") { mediaType = "video" // 视频格式处理 if strings.Contains(parts[0], "video/mp4") { mimeType = "video/mp4" } else if strings.Contains(parts[0], "video/webm") { mimeType = "video/webm" } else if strings.Contains(parts[0], "video/quicktime") || strings.Contains(parts[0], "video/mov") { // MOV 格式,尝试作为 mp4 上传 mimeType = "video/mp4" logger.Debug("ℹ️ MOV 视频将作为 MP4 上传") } else if strings.Contains(parts[0], "video/avi") || strings.Contains(parts[0], "video/x-msvideo") { mimeType = "video/mp4" logger.Debug("ℹ️ AVI 视频将作为 MP4 上传") } else { // 其他视频格式默认作为 mp4 mimeType = "video/mp4" logger.Debug("ℹ️ 未知视频格式 %s 将作为 MP4 上传", parts[0]) } } else { mediaType = "image" // 图片格式处理 if strings.Contains(parts[0], "image/png") { mimeType = "image/png" } else if strings.Contains(parts[0], "image/jpeg") { mimeType = "image/jpeg" } else { // 其他图片格式需要转换为 PNG converted, err := convertBase64ToPNG(base64Data) if err != nil { logger.Warn("⚠️ %s base64 转换失败: %v", parts[0], err) mimeType = "image/jpeg" // 回退 } else { logger.Info("✅ %s base64 已转换为 PNG", parts[0]) base64Data = converted mimeType = "image/png" } } } return &MediaInfo{ MimeType: mimeType, Data: base64Data, IsURL: false, MediaType: mediaType, } } // URL 媒体 - 优先尝试直接使用 URL 上传 mediaType := defaultType lowerURL := strings.ToLower(urlStr) if strings.HasSuffix(lowerURL, ".mp4") || strings.HasSuffix(lowerURL, ".webm") || strings.HasSuffix(lowerURL, ".mov") || strings.HasSuffix(lowerURL, ".avi") || strings.HasSuffix(lowerURL, ".mkv") || strings.HasSuffix(lowerURL, ".m4v") { mediaType = "video" } return &MediaInfo{ URL: urlStr, IsURL: true, MediaType: mediaType, } } func downloadImage(urlStr string) (string, string, error) { return downloadMedia(urlStr, "image") } // downloadMedia 下载媒体文件(图片或视频) func downloadMedia(urlStr, mediaType string) (string, string, error) { resp, err := utils.HTTPClient.Get(urlStr) if err != nil { return "", "", err } defer resp.Body.Close() // 检查上游返回的状态码 if resp.StatusCode == 401 || resp.StatusCode == 403 { return "", "", fmt.Errorf("UPSTREAM_%d: 上游返回状态码 %d 多媒体下载失败", resp.StatusCode, resp.StatusCode) } if resp.StatusCode >= 400 { return "", "", fmt.Errorf("UPSTREAM_%d: 上游返回状态码 %d", resp.StatusCode, resp.StatusCode) } data, err := io.ReadAll(resp.Body) if err != nil { return "", "", err } mimeType := resp.Header.Get("Content-Type") if mediaType == "video" || strings.HasPrefix(mimeType, "video/") { // 视频处理 if mimeType == "" { mimeType = "video/mp4" } // 规范化视频 MIME 类型 mimeType = normalizeVideoMimeType(mimeType) return base64.StdEncoding.EncodeToString(data), mimeType, nil } // 图片处理 if mimeType == "" { mimeType = "image/jpeg" } needConvert := !strings.Contains(mimeType, "jpeg") && !strings.Contains(mimeType, "png") if needConvert { converted, err := convertToPNG(data) if err != nil { logger.Warn("⚠️ %s 转换失败: %v,尝试原格式", mimeType, err) } else { logger.Info("✅ %s 已转换为 PNG", mimeType) return base64.StdEncoding.EncodeToString(converted), "image/png", nil } } return base64.StdEncoding.EncodeToString(data), mimeType, nil } // normalizeVideoMimeType 规范化视频 MIME 类型 func normalizeVideoMimeType(mimeType string) string { switch { case strings.Contains(mimeType, "mp4"): return "video/mp4" case strings.Contains(mimeType, "webm"): return "video/webm" case strings.Contains(mimeType, "quicktime"), strings.Contains(mimeType, "mov"): logger.Debug("ℹ️ MOV 视频将作为 MP4 上传") return "video/mp4" case strings.Contains(mimeType, "avi"), strings.Contains(mimeType, "x-msvideo"): logger.Debug("ℹ️ AVI 视频将作为 MP4 上传") return "video/mp4" case strings.Contains(mimeType, "x-matroska"), strings.Contains(mimeType, "mkv"): logger.Debug("ℹ️ MKV 视频将作为 MP4 上传") return "video/mp4" case strings.Contains(mimeType, "3gpp"): return "video/3gpp" default: logger.Debug("ℹ️ 未知视频格式 %s 将作为 MP4 上传", mimeType) return "video/mp4" } } // convertToPNG 将图片转换为 PNG 格式 func convertToPNG(data []byte) ([]byte, error) { img, _, err := image.Decode(bytes.NewReader(data)) if err != nil { return nil, fmt.Errorf("解码图片失败: %w", err) } var buf bytes.Buffer if err := png.Encode(&buf, img); err != nil { return nil, fmt.Errorf("编码 PNG 失败: %w", err) } return buf.Bytes(), nil } // convertBase64ToPNG 将 base64 图片转换为 PNG func convertBase64ToPNG(base64Data string) (string, error) { data, err := base64.StdEncoding.DecodeString(base64Data) if err != nil { return "", fmt.Errorf("解码 base64 失败: %w", err) } converted, err := convertToPNG(data) if err != nil { return "", err } return base64.StdEncoding.EncodeToString(converted), nil } const maxRetries = 3 // convertMessagesToPrompt 将多轮对话转换为Gemini格式的prompt // extractSystemPrompt 提取并返回系统提示词 func extractSystemPrompt(messages []Message) string { for _, msg := range messages { if msg.Role == "system" { text, _ := parseMessageContent(msg) return text } } return "" } // convertMessagesToPrompt 将多轮对话转换为带系统提示词的prompt // 支持OpenAI/Claude/Gemini格式的messages func convertMessagesToPrompt(messages []Message) string { var dialogParts []string var systemPrompt string for _, msg := range messages { text, _ := parseMessageContent(msg) if text == "" && msg.Role != "assistant" { continue } switch msg.Role { case "system": // 支持多个system消息拼接 if systemPrompt != "" { systemPrompt += "\n" + text } else { systemPrompt = text } case "user", "human": // Claude使用human dialogParts = append(dialogParts, fmt.Sprintf("Human: %s", text)) case "assistant": // 检查是否有工具调用 if len(msg.ToolCalls) > 0 { for _, tc := range msg.ToolCalls { dialogParts = append(dialogParts, fmt.Sprintf("Assistant: [调用工具 %s(%s)]", tc.Function.Name, tc.Function.Arguments)) } } else if text != "" { dialogParts = append(dialogParts, fmt.Sprintf("Assistant: %s", text)) } case "tool", "tool_result": // Claude使用tool_result dialogParts = append(dialogParts, fmt.Sprintf("Tool Result [%s]: %s", msg.Name, text)) } } // 组合最终prompt,系统提示词使用更强的格式 var result strings.Builder if systemPrompt != "" { // 使用更明确的系统提示词格式,确保生效 result.WriteString("\n") result.WriteString(systemPrompt) result.WriteString("\n\n\n") } if len(dialogParts) > 0 { result.WriteString(strings.Join(dialogParts, "\n\n")) } // 添加Assistant前缀引导回复 result.WriteString("\n\nAssistant:") return result.String() } // ==================== Gemini API 兼容 ==================== // GeminiRequest Gemini generateContent API 请求格式 type GeminiRequest struct { Contents []GeminiContent `json:"contents"` SystemInstruction *GeminiContent `json:"systemInstruction,omitempty"` GenerationConfig map[string]interface{} `json:"generationConfig,omitempty"` GeminiTools []map[string]interface{} `json:"tools,omitempty"` } type GeminiContent struct { Role string `json:"role,omitempty"` Parts []GeminiPart `json:"parts"` } type GeminiPart struct { Text string `json:"text,omitempty"` InlineData *GeminiInlineData `json:"inlineData,omitempty"` } type GeminiInlineData struct { MimeType string `json:"mimeType"` Data string `json:"data"` } // handleGeminiGenerate 处理Gemini generateContent API格式的请求 func handleGeminiGenerate(c *gin.Context) { action := c.Param("action") if action == "" { c.JSON(400, gin.H{"error": gin.H{"code": 400, "message": "Missing model action", "status": "INVALID_ARGUMENT"}}) return } action = strings.TrimPrefix(action, "/") var model string var isStream bool if idx := strings.LastIndex(action, ":"); idx > 0 { model = action[:idx] actionType := action[idx+1:] isStream = actionType == "streamGenerateContent" } else { model = action } if model == "" { model = GetAvailableModels()[0] } var geminiReq GeminiRequest if err := c.ShouldBindJSON(&geminiReq); err != nil { c.JSON(400, gin.H{"error": gin.H{"code": 400, "message": err.Error(), "status": "INVALID_ARGUMENT"}}) return } var messages []Message // 处理systemInstruction if geminiReq.SystemInstruction != nil && len(geminiReq.SystemInstruction.Parts) > 0 { var sysText string for _, part := range geminiReq.SystemInstruction.Parts { if part.Text != "" { sysText += part.Text } } if sysText != "" { messages = append(messages, Message{Role: "system", Content: sysText}) } } // 处理contents for _, content := range geminiReq.Contents { role := content.Role if role == "model" { role = "assistant" } var textParts []string var contentParts []interface{} for _, part := range content.Parts { if part.Text != "" { textParts = append(textParts, part.Text) } if part.InlineData != nil { contentParts = append(contentParts, map[string]interface{}{ "type": "image_url", "image_url": map[string]string{ "url": fmt.Sprintf("data:%s;base64,%s", part.InlineData.MimeType, part.InlineData.Data), }, }) } } if len(contentParts) > 0 { if len(textParts) > 0 { contentParts = append([]interface{}{map[string]interface{}{"type": "text", "text": strings.Join(textParts, "\n")}}, contentParts...) } messages = append(messages, Message{Role: role, Content: contentParts}) } else if len(textParts) > 0 { messages = append(messages, Message{Role: role, Content: strings.Join(textParts, "\n")}) } } stream := isStream || c.Query("alt") == "sse" // 转换Gemini工具格式 var tools []ToolDef for _, gt := range geminiReq.GeminiTools { if funcDecls, ok := gt["functionDeclarations"].([]interface{}); ok { for _, fd := range funcDecls { if funcMap, ok := fd.(map[string]interface{}); ok { name, _ := funcMap["name"].(string) desc, _ := funcMap["description"].(string) params, _ := funcMap["parameters"].(map[string]interface{}) tools = append(tools, ToolDef{ Type: "function", Function: FunctionDef{ Name: name, Description: desc, Parameters: params, }, }) } } } } req := ChatRequest{ Model: model, Messages: messages, Stream: stream, Tools: tools, } streamChat(c, req) } // ==================== Claude API 兼容 ==================== type ClaudeRequest struct { Model string `json:"model"` Messages []Message `json:"messages"` System string `json:"system,omitempty"` MaxTokens int `json:"max_tokens,omitempty"` Stream bool `json:"stream"` Temperature float64 `json:"temperature,omitempty"` Tools []ToolDef `json:"tools,omitempty"` } // handleClaudeMessages 处理Claude Messages API格式的请求 func handleClaudeMessages(c *gin.Context) { var claudeReq ClaudeRequest if err := c.ShouldBindJSON(&claudeReq); err != nil { c.JSON(400, gin.H{"type": "error", "error": gin.H{"type": "invalid_request_error", "message": err.Error()}}) return } req := ChatRequest{ Model: claudeReq.Model, Messages: claudeReq.Messages, Stream: claudeReq.Stream, Temperature: claudeReq.Temperature, Tools: claudeReq.Tools, } // 如果Claude格式有单独的system字段,插入到messages开头 if claudeReq.System != "" { systemMsg := Message{Role: "system", Content: claudeReq.System} req.Messages = append([]Message{systemMsg}, req.Messages...) } if req.Model == "" { req.Model = GetAvailableModels()[0] } streamChat(c, req) } // buildToolsSpec 将OpenAI格式的工具定义转换为Gemini的toolsSpec // 支持混合后缀同时启用多个功能,如 -image-search 同时启用图片生成和搜索 func buildToolsSpec(tools []ToolDef, isImageModel, isVideoModel, isSearchModel bool) map[string]interface{} { toolsSpec := make(map[string]interface{}) // 检查是否指定了任何功能后缀 hasAnySpec := isImageModel || isVideoModel || isSearchModel if !hasAnySpec { toolsSpec["webGroundingSpec"] = map[string]interface{}{} toolsSpec["toolRegistry"] = "default_tool_registry" toolsSpec["imageGenerationSpec"] = map[string]interface{}{} toolsSpec["videoGenerationSpec"] = map[string]interface{}{} } else { if isImageModel { toolsSpec["imageGenerationSpec"] = map[string]interface{}{} } if isVideoModel { toolsSpec["videoGenerationSpec"] = map[string]interface{}{} } if isSearchModel { toolsSpec["webGroundingSpec"] = map[string]interface{}{} } } _ = tools return toolsSpec } // extractToolCalls 从Gemini响应中提取工具调用 func extractToolCalls(dataList []map[string]interface{}) []ToolCall { var toolCalls []ToolCall for _, data := range dataList { streamResp, ok := data["streamAssistResponse"].(map[string]interface{}) if !ok { continue } answer, ok := streamResp["answer"].(map[string]interface{}) if !ok { continue } replies, ok := answer["replies"].([]interface{}) if !ok { continue } for _, reply := range replies { replyMap, ok := reply.(map[string]interface{}) if !ok { continue } groundedContent, ok := replyMap["groundedContent"].(map[string]interface{}) if !ok { continue } content, ok := groundedContent["content"].(map[string]interface{}) if !ok { continue } // 检查functionCall if fc, ok := content["functionCall"].(map[string]interface{}); ok { name, _ := fc["name"].(string) args, _ := fc["args"].(map[string]interface{}) argsBytes, _ := json.Marshal(args) toolCalls = append(toolCalls, ToolCall{ ID: "call_" + uuid.New().String()[:8], Type: "function", Function: FunctionCall{ Name: name, Arguments: string(argsBytes), }, }) } } } return toolCalls } // needsConversationContext 检查是否需要对话上下文(多轮对话) func needsConversationContext(messages []Message) bool { // 检查是否有多轮对话标志:存在assistant或tool消息 for _, msg := range messages { if msg.Role == "assistant" || msg.Role == "tool" || msg.Role == "tool_result" { return true } } return false } // handleFlowRequest 处理 Flow 模型请求 func handleFlowRequest(c *gin.Context, req ChatRequest, chatID string, createdTime int64) { if flowHandler == nil { c.JSON(503, gin.H{"error": gin.H{ "message": "Flow 服务未启用,请在配置文件中启用并添加 Token", "type": "service_unavailable", }}) return } // 解析消息内容和图片 var prompt string var imageBytes [][]byte for _, msg := range req.Messages { if msg.Role == "user" || msg.Role == "human" { text, images := parseMessageContent(msg) if text != "" { prompt = text } // 提取图片数据 for _, img := range images { if img.Data != "" { imgData, err := base64.StdEncoding.DecodeString(img.Data) if err == nil { imageBytes = append(imageBytes, imgData) } } } } } if prompt == "" { c.JSON(400, gin.H{"error": gin.H{ "message": "Prompt cannot be empty", "type": "invalid_request_error", }}) return } flowReq := flow.GenerationRequest{ Model: req.Model, Prompt: prompt, Images: imageBytes, Stream: req.Stream, } if req.Stream { // 流式响应 c.Header("Content-Type", "text/event-stream") c.Header("Cache-Control", "no-cache") c.Header("Connection", "keep-alive") c.Header("X-Accel-Buffering", "no") c.Status(200) flusher, ok := c.Writer.(http.Flusher) if !ok { c.JSON(500, gin.H{"error": "Streaming not supported"}) return } result, _ := flowHandler.HandleGeneration(flowReq, func(chunk string) { c.Writer.WriteString(chunk) flusher.Flush() }) // 发送 [DONE] c.Writer.WriteString("data: [DONE]\n\n") flusher.Flush() if result != nil && !result.Success && result.Error != "" { logger.Error("❌ [Flow] 生成失败: %s", result.Error) } } else { // 非流式响应 result, err := flowHandler.HandleGeneration(flowReq, nil) if err != nil { c.JSON(500, gin.H{"error": gin.H{ "message": err.Error(), "type": "internal_error", }}) return } if !result.Success { c.JSON(500, gin.H{"error": gin.H{ "message": result.Error, "type": "generation_failed", }}) return } // 构建响应 content := result.URL if result.Type == "image" { content = fmt.Sprintf("![Generated Image](%s)", result.URL) } else if result.Type == "video" { content = fmt.Sprintf("", result.URL) } c.JSON(200, gin.H{ "id": chatID, "object": "chat.completion", "created": createdTime, "model": req.Model, "choices": []gin.H{{ "index": 0, "message": gin.H{ "role": "assistant", "content": content, }, "finish_reason": "stop", }}, }) } } func streamChat(c *gin.Context, req ChatRequest) { chatID := "chatcmpl-" + uuid.New().String() createdTime := time.Now().Unix() clientIP := c.ClientIP() userAgent := c.GetHeader("User-Agent") // 统计变量 var statsSuccess bool var statsInputTokens int64 var statsOutputTokens int64 var statsImages int64 var statsVideos int64 statsModel := req.Model defer func() { apiStats.RecordRequestWithModel(statsModel, statsSuccess, statsInputTokens, statsOutputTokens, statsImages, statsVideos) // 记录IP统计(包含tokens、图片、视频) ipStats.RecordIPRequest(clientIP, statsModel, userAgent, statsSuccess, statsInputTokens, statsOutputTokens, statsImages, statsVideos) }() // 入站日志 logger.Info("📥 [%s] 请求: model=%s ", clientIP, req.Model) if flow.IsFlowModel(req.Model) { handleFlowRequest(c, req, chatID, createdTime) return } var textContent string var images []MediaInfo systemPrompt := extractSystemPrompt(req.Messages) if needsConversationContext(req.Messages) { // 多轮对话:拼接所有消息(包含system) textContent = convertMessagesToPrompt(req.Messages) // 只从最后一条用户消息提取图片 for i := len(req.Messages) - 1; i >= 0; i-- { if req.Messages[i].Role == "user" || req.Messages[i].Role == "human" { _, images = parseMessageContent(req.Messages[i]) break } } } else { lastMsg := req.Messages[len(req.Messages)-1] userText, userImages := parseMessageContent(lastMsg) images = userImages if systemPrompt != "" { textContent = fmt.Sprintf("\n%s\n\n\nHuman: %s\n\nAssistant:", systemPrompt, userText) } else { textContent = userText } } var respBody []byte var lastErr error var lastErrStatusCode int // 保存最后一次错误的 HTTP 状态码 var lastErrBody []byte // 保存最后一次错误的响应体 var usedAcc *pool.Account var usedJWT, usedOrigAuth, usedConfigID, usedSession string isLongRunning := !req.Stream && (strings.Contains(req.Model, "video") || strings.Contains(req.Model, "imagen") || strings.Contains(req.Model, "image")) var heartbeatDone chan struct{} if isLongRunning { heartbeatDone = make(chan struct{}) c.Header("Content-Type", "application/json") c.Header("Transfer-Encoding", "chunked") c.Status(200) writer := c.Writer flusher, ok := writer.(http.Flusher) if ok { flusher.Flush() // 先发送头部 } go func() { defer func() { if r := recover(); r != nil { } }() ticker := time.NewTicker(15 * time.Second) defer ticker.Stop() for { select { case <-heartbeatDone: return case <-ticker.C: if _, err := writer.Write([]byte(" ")); err != nil { return } if flusher, ok := writer.(http.Flusher); ok { flusher.Flush() } } } }() } defer func() { if heartbeatDone != nil { select { case <-heartbeatDone: default: close(heartbeatDone) } } }() // 估算输入 tokens(基于文本长度) statsInputTokens = int64(len(textContent)/4) + int64(len(images)*500) // 文本 + 图片估算 // 流式请求:提前发送 SSE 头部,避免上游请求期间客户端等待超时 var streamWriter http.ResponseWriter var streamFlusher http.Flusher var streamStarted bool if req.Stream { c.Header("Content-Type", "text/event-stream") c.Header("Cache-Control", "no-cache") c.Header("Connection", "keep-alive") c.Header("X-Accel-Buffering", "no") streamWriter = c.Writer streamFlusher, _ = streamWriter.(http.Flusher) chunk := createChunk(chatID, createdTime, req.Model, map[string]interface{}{"role": "assistant"}, nil) fmt.Fprintf(streamWriter, "data: %s\n\n", chunk) streamFlusher.Flush() streamStarted = true } for retry := 0; retry < maxRetries; retry++ { acc := pool.Pool.Next() if acc == nil { if streamStarted { // 流式请求已开始,发送 SSE 格式错误 errChunk := createChunk(chatID, createdTime, req.Model, map[string]interface{}{"content": "[错误] 没有可用账号"}, nil) fmt.Fprintf(streamWriter, "data: %s\n\n", errChunk) finishReason := "stop" finalChunk := createChunk(chatID, createdTime, req.Model, nil, &finishReason) fmt.Fprintf(streamWriter, "data: %s\n\n", finalChunk) fmt.Fprintf(streamWriter, "data: [DONE]\n\n") streamFlusher.Flush() } else { c.JSON(500, gin.H{"error": "没有可用账号"}) } return } usedAcc = acc logger.Info("📤 [%s] 使用账号: %s", clientIP, acc.Data.Email) if retry > 0 { logger.Info("🔄 第 %d 次重试,切换账号: %s", retry+1, acc.Data.Email) } jwt, configID, err := acc.GetJWT() if err != nil { logger.Error("❌ [%s] 获取 JWT 失败: %v", acc.Data.Email, err) lastErr = err continue } session, err := createSession(jwt, configID, acc.Data.Authorization) if err != nil { logger.Error("❌ [%s] 创建 Session 失败: %v", acc.Data.Email, err) // 401 错误标记账号需要刷新 if strings.Contains(err.Error(), "401") || strings.Contains(err.Error(), "UNAUTHENTICATED") { // pool.Pool.MarkNeedsRefresh(acc) } lastErr = err continue } // 上传媒体文件并获取 fileIds var fileIds []string uploadFailed := false for _, media := range images { var fileId string var err error mediaTypeName := "图片" if media.MediaType == "video" { mediaTypeName = "视频" } if media.IsURL { // 优先尝试 URL 直接上传 fileId, err = uploadContextFileByURL(jwt, configID, session, media.URL, acc.Data.Authorization) if err != nil { // URL 上传失败,回退到下载后上传 mediaData, mimeType, dlErr := downloadMedia(media.URL, media.MediaType) if dlErr != nil { logger.Warn("⚠️ [%s] %s下载失败: %v", acc.Data.Email, mediaTypeName, dlErr) if strings.Contains(dlErr.Error(), "UPSTREAM_401") || strings.Contains(dlErr.Error(), "UPSTREAM_403") { c.JSON(500, gin.H{"error": gin.H{ "message": dlErr.Error(), "type": "upstream_error", "code": "media_download_failed", }}) return } uploadFailed = true break } fileId, err = uploadContextFile(jwt, configID, session, mimeType, mediaData, acc.Data.Authorization) } } else { fileId, err = uploadContextFile(jwt, configID, session, media.MimeType, media.Data, acc.Data.Authorization) } if err != nil { logger.Warn("⚠️ [%s] %s上传失败: %v", acc.Data.Email, mediaTypeName, err) uploadFailed = true break } fileIds = append(fileIds, fileId) } if uploadFailed { lastErr = fmt.Errorf("媒体上传失败") continue } // 构建 query parts(只包含文本) queryParts := []map[string]interface{}{} if textContent != "" { queryParts = append(queryParts, map[string]interface{}{"text": textContent}) } // 确保 queryParts 不为空,避免 Google 返回空响应 if len(queryParts) == 0 { queryParts = append(queryParts, map[string]interface{}{"text": " "}) } isImageModel := strings.Contains(req.Model, "-image") isVideoModel := strings.Contains(req.Model, "-video") isSearchModel := strings.Contains(req.Model, "-search") actualModel := req.Model actualModel = strings.ReplaceAll(actualModel, "-image", "") actualModel = strings.ReplaceAll(actualModel, "-video", "") actualModel = strings.ReplaceAll(actualModel, "-search", "") // 构建 toolsSpec(支持自定义工具) toolsSpec := buildToolsSpec(req.Tools, isImageModel, isVideoModel, isSearchModel) body := map[string]interface{}{ "configId": configID, "additionalParams": map[string]string{"token": "-"}, "streamAssistRequest": map[string]interface{}{ "session": session, "query": map[string]interface{}{"parts": queryParts}, "filter": "", "fileIds": fileIds, "answerGenerationMode": "NORMAL", "toolsSpec": toolsSpec, "languageCode": "zh-CN", "userMetadata": map[string]string{"timeZone": "Asia/Shanghai"}, "assistSkippingMode": "REQUEST_ASSIST", }, } // 设置模型 ID(去掉 -image 后缀) if targetModelID, ok := modelMapping[actualModel]; ok && targetModelID != "" { body["streamAssistRequest"].(map[string]interface{})["assistGenerationConfig"] = map[string]interface{}{ "modelId": targetModelID, } } bodyBytes, _ := json.Marshal(body) httpReq, _ := http.NewRequest("POST", "https://biz-discoveryengine.googleapis.com/v1alpha/locations/global/widgetStreamAssist", bytes.NewReader(bodyBytes)) for k, v := range getCommonHeaders(jwt, acc.Data.Authorization) { httpReq.Header.Set(k, v) } resp, err := utils.HTTPClient.Do(httpReq) if err != nil { logger.Error("❌ [%s] 请求失败: %v", acc.Data.Email, err) lastErr = err continue } if resp.StatusCode != 200 { body, _ := utils.ReadResponseBody(resp) resp.Body.Close() logger.Error("❌ [%s] Google 报错: %d %s (重试 %d/%d)", acc.Data.Email, resp.StatusCode, string(body), retry+1, maxRetries) lastErr = fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body)) lastErrStatusCode = resp.StatusCode lastErrBody = body // 401/403 无权限,标记需要刷新 if resp.StatusCode == 401 || resp.StatusCode == 403 { logger.Warn("⚠️ [%s] %d 无权限,标记需要刷新", acc.Data.Email, resp.StatusCode) pool.Pool.MarkNeedsRefresh(acc) } // 429 限流,延长使用冷却时间(3倍冷却) if resp.StatusCode == 429 { cooldownTime := pool.UseCooldown * 3 acc.Mu.Lock() acc.LastUsed = time.Now().Add(cooldownTime) acc.Mu.Unlock() logger.Info("⏳ [%s] 429 限流,账号进入延长冷却 %v", acc.Data.Email, cooldownTime) pool.Pool.MarkUsed(acc, false) time.Sleep(1 * time.Second) // 短暂等待后切换账号 retry-- // 不计入重试次数 continue } if resp.StatusCode == 400 { logger.Warn("⚠️ [%s] 400 错误,换账号重试", acc.Data.Email) pool.Pool.MarkUsed(acc, false) time.Sleep(500 * time.Millisecond) continue } pool.Pool.MarkUsed(acc, false) // 标记失败 continue } // 成功,读取响应 respBody, _ = utils.ReadResponseBody(resp) resp.Body.Close() // Debug 模式输出上游响应 if logger.IsDebug() { respSnippet := string(respBody) if len(respSnippet) > 2000 { respSnippet = respSnippet[:2000] + "..." } logger.Debug("[%s] 上游响应: %s", acc.Data.Email, respSnippet) } // 快速检查是否是认证错误响应 if bytes.Contains(respBody, []byte("uToken")) && !bytes.Contains(respBody, []byte("streamAssistResponse")) { logger.Warn("[%s] 收到认证响应,标记需要刷新", acc.Data.Email) pool.Pool.MarkNeedsRefresh(acc) lastErr = fmt.Errorf("认证失败,需要刷新账号") continue } // 检查是否有实际内容(非空返回) hasText := bytes.Contains(respBody, []byte(`"text"`)) hasFile := bytes.Contains(respBody, []byte(`"file"`)) hasInlineData := bytes.Contains(respBody, []byte(`"inlineData"`)) hasThought := bytes.Contains(respBody, []byte(`"thought"`)) hasFunctionCall := bytes.Contains(respBody, []byte(`"functionCall"`)) hasError := bytes.Contains(respBody, []byte(`"error"`)) || bytes.Contains(respBody, []byte(`"errorMessage"`)) hasContent := hasText || hasFile || hasInlineData || hasFunctionCall // 检测是否有服务端错误信息 if hasError && !hasContent { logger.Warn("[%s] 响应包含错误信息,重试 (%d/%d)", acc.Data.Email, retry+1, maxRetries) // 简单解析错误类型 if bytes.Contains(respBody, []byte("RESOURCE_EXHAUSTED")) || bytes.Contains(respBody, []byte("quota")) { logger.Info("⏳ [%s] 检测到配额耗尽,标记冷却", acc.Data.Email) acc.SetCooldownMultiplier(5) // 5倍冷却 pool.Pool.MarkUsed(acc, false) } lastErr = fmt.Errorf("上游返回错误响应") continue } // 响应完全为空或只有思考内容 if !hasContent { if hasThought { logger.Warn("[%s] 响应只有思考内容,无实际输出,换号重试 (%d/%d)", acc.Data.Email, retry+1, maxRetries) lastErr = fmt.Errorf("空返回,只有思考内容") // 思考中的账号不标记失败,可能只是请求太慢 time.Sleep(500 * time.Millisecond) } else { logger.Warn("[%s] 响应无有效内容 (text/file/inlineData/functionCall),换号重试 (%d/%d)", acc.Data.Email, retry+1, maxRetries) lastErr = fmt.Errorf("空返回,无有效内容") pool.Pool.MarkUsed(acc, false) } continue } usedJWT = jwt usedOrigAuth = acc.Data.Authorization usedConfigID = configID usedSession = session // 保存创建的 session 作为回退 usedAcc = acc lastErr = nil pool.Pool.MarkUsed(acc, true) // 标记成功 break } if lastErr != nil { logger.Error("❌ 所有重试均失败: %v", lastErr) if streamStarted { // 流式请求已开始,发送 SSE 格式错误 errMsg := fmt.Sprintf("[错误] %v", lastErr) errChunk := createChunk(chatID, createdTime, req.Model, map[string]interface{}{"content": errMsg}, nil) fmt.Fprintf(streamWriter, "data: %s\n\n", errChunk) finishReason := "stop" finalChunk := createChunk(chatID, createdTime, req.Model, nil, &finishReason) fmt.Fprintf(streamWriter, "data: %s\n\n", finalChunk) fmt.Fprintf(streamWriter, "data: [DONE]\n\n") streamFlusher.Flush() } else if lastErrStatusCode > 0 && len(lastErrBody) > 0 { // 如果有 HTTP 错误响应体,原样透传 c.Data(lastErrStatusCode, "application/json", lastErrBody) } else { c.JSON(500, gin.H{"error": lastErr.Error()}) } return } _ = usedAcc // 检查空响应 if len(respBody) == 0 { logger.Error("❌ 响应为空") if streamStarted { errChunk := createChunk(chatID, createdTime, req.Model, map[string]interface{}{"content": "[错误] 上游返回空响应"}, nil) fmt.Fprintf(streamWriter, "data: %s\n\n", errChunk) finishReason := "stop" finalChunk := createChunk(chatID, createdTime, req.Model, nil, &finishReason) fmt.Fprintf(streamWriter, "data: %s\n\n", finalChunk) fmt.Fprintf(streamWriter, "data: [DONE]\n\n") streamFlusher.Flush() } else { c.JSON(500, gin.H{"error": "Empty response from Google"}) } return } // 解析响应:支持多种格式 var dataList []map[string]interface{} var parseErr error // 1. 尝试标准 JSON 数组 if parseErr = json.Unmarshal(respBody, &dataList); parseErr != nil { logger.Warn("⚠️ JSON 数组解析失败: %v, 响应前100字符: %s", parseErr, string(respBody[:min(100, len(respBody))])) // 2. 尝试修复不完整的 JSON 数组 dataList = utils.ParseIncompleteJSONArray(respBody) if dataList == nil { // 3. 尝试 NDJSON 格式 logger.Warn("⚠️ 尝试 NDJSON 格式...") dataList = utils.ParseNDJSON(respBody) } if len(dataList) == 0 { // 输出完整响应用于调试 respStr := string(respBody) if len(respStr) > 500 { logger.Error("❌ 所有解析方式均失败, 响应长度: %d, 前500字符: %s", len(respBody), respStr[:500]) logger.Error("❌ 后200字符: %s", respStr[len(respStr)-200:]) } else { logger.Error("❌ 所有解析方式均失败, 响应长度: %d, 完整响应: %s", len(respBody), respStr) } if streamStarted { errChunk := createChunk(chatID, createdTime, req.Model, map[string]interface{}{"content": "[错误] 响应解析失败"}, nil) fmt.Fprintf(streamWriter, "data: %s\n\n", errChunk) finishReason := "stop" finalChunk := createChunk(chatID, createdTime, req.Model, nil, &finishReason) fmt.Fprintf(streamWriter, "data: %s\n\n", finalChunk) fmt.Fprintf(streamWriter, "data: [DONE]\n\n") streamFlusher.Flush() } else { c.JSON(500, gin.H{"error": "JSON Parse Error"}) } return } logger.Info("✅ 备用解析成功,共 %d 个对象", len(dataList)) } // 检查是否有有效响应 if len(dataList) > 0 { hasValidResponse := false hasFileContent := false for _, data := range dataList { if streamResp, ok := data["streamAssistResponse"].(map[string]interface{}); ok { hasValidResponse = true // 检查是否有文件内容 if answer, ok := streamResp["answer"].(map[string]interface{}); ok { if replies, ok := answer["replies"].([]interface{}); ok { for _, reply := range replies { if replyMap, ok := reply.(map[string]interface{}); ok { if gc, ok := replyMap["groundedContent"].(map[string]interface{}); ok { if content, ok := gc["content"].(map[string]interface{}); ok { if _, ok := content["file"]; ok { hasFileContent = true } } } } } } } } } if !hasValidResponse { logger.Warn("⚠️ 响应中没有 streamAssistResponse,响应内容: %v", dataList[0]) } logger.Debug("📊 响应统计: %d 个数据块, 有效响应=%v, 包含文件=%v", len(dataList), hasValidResponse, hasFileContent) } // 从响应中提取 session(用于下载图片) var respSession string for _, data := range dataList { if streamResp, ok := data["streamAssistResponse"].(map[string]interface{}); ok { if sessionInfo, ok := streamResp["sessionInfo"].(map[string]interface{}); ok { if s, ok := sessionInfo["session"].(string); ok && s != "" { respSession = s break } } } } // 如果响应中没有 session,使用请求时创建的 session 作为回退 if respSession == "" { if usedSession != "" { logger.Warn("⚠️ 响应中未找到 session,使用请求时创建的 session: %s", usedSession) respSession = usedSession } else { logger.Warn("⚠️ 响应中未找到 session 且无回退 session,图片/视频下载可能失败") } } else { } // 待下载的文件信息 type PendingFile struct { FileID string MimeType string } if req.Stream { // 流式响应:文本/思考实时输出,图片最后处理 // SSE 头部和 role chunk 已在请求前发送,复用 streamWriter/streamFlusher writer := streamWriter flusher := streamFlusher // 统计输出内容长度 var outputLen int64 // 收集待下载的文件和工具调用 var pendingFiles []PendingFile hasToolCalls := false for _, data := range dataList { streamResp, ok := data["streamAssistResponse"].(map[string]interface{}) if !ok { continue } answer, ok := streamResp["answer"].(map[string]interface{}) if !ok { continue } replies, ok := answer["replies"].([]interface{}) if !ok { continue } for _, reply := range replies { replyMap, ok := reply.(map[string]interface{}) if !ok { continue } groundedContent, ok := replyMap["groundedContent"].(map[string]interface{}) if !ok { continue } content, ok := groundedContent["content"].(map[string]interface{}) if !ok { continue } // 检查是否是思考内容 if thought, ok := content["thought"].(bool); ok && thought { if t, ok := content["text"].(string); ok && t != "" { chunk := createChunk(chatID, createdTime, req.Model, map[string]interface{}{"reasoning_content": t}, nil) fmt.Fprintf(writer, "data: %s\n\n", chunk) flusher.Flush() outputLen += int64(len(t)) } continue } // 输出文本(实时) if t, ok := content["text"].(string); ok && t != "" { chunk := createChunk(chatID, createdTime, req.Model, map[string]interface{}{"content": t}, nil) fmt.Fprintf(writer, "data: %s\n\n", chunk) flusher.Flush() outputLen += int64(len(t)) } // 处理 inlineData(直接有 base64 数据的图片) if inlineData, ok := content["inlineData"].(map[string]interface{}); ok { mime, _ := inlineData["mimeType"].(string) data, _ := inlineData["data"].(string) if mime != "" && data != "" { imgMarkdown := formatImageAsMarkdown(mime, data) chunk := createChunk(chatID, createdTime, req.Model, map[string]interface{}{"content": imgMarkdown}, nil) fmt.Fprintf(writer, "data: %s\n\n", chunk) flusher.Flush() } } // 收集需要下载的文件(图片/视频) if file, ok := content["file"].(map[string]interface{}); ok { fileId, _ := file["fileId"].(string) mimeType, _ := file["mimeType"].(string) if fileId != "" { pendingFiles = append(pendingFiles, PendingFile{FileID: fileId, MimeType: mimeType}) } } if fc, ok := content["functionCall"].(map[string]interface{}); ok { hasToolCalls = true name, _ := fc["name"].(string) args, _ := fc["args"].(map[string]interface{}) argsBytes, _ := json.Marshal(args) toolCall := ToolCall{ ID: "call_" + uuid.New().String()[:8], Type: "function", Function: FunctionCall{ Name: name, Arguments: string(argsBytes), }, } chunk := createChunk(chatID, createdTime, req.Model, map[string]interface{}{ "tool_calls": []map[string]interface{}{{ "index": 0, "id": toolCall.ID, "type": "function", "function": map[string]interface{}{ "name": toolCall.Function.Name, "arguments": toolCall.Function.Arguments, }, }}, }, nil) fmt.Fprintf(writer, "data: %s\n\n", chunk) flusher.Flush() } } } if len(pendingFiles) > 0 { logger.Info("📥 开始下载 %d 个文件...", len(pendingFiles)) type downloadResult struct { Index int Data string MimeType string Err error } results := make(chan downloadResult, len(pendingFiles)) var wg sync.WaitGroup for i, pf := range pendingFiles { wg.Add(1) go func(idx int, file PendingFile) { defer wg.Done() data, err := downloadGeneratedFile(usedJWT, file.FileID, respSession, usedConfigID, usedOrigAuth) results <- downloadResult{Index: idx, Data: data, MimeType: file.MimeType, Err: err} }(i, pf) } go func() { wg.Wait() close(results) }() downloaded := make([]downloadResult, len(pendingFiles)) for r := range results { downloaded[r.Index] = r } // 按顺序输出 successCount := 0 var lastErr error needsRetry := false for i, r := range downloaded { if r.Err != nil { logger.Error("❌ 下载文件[%d]失败: %v", i, r.Err) lastErr = r.Err // 检测是否需要换号重试 if errors.Is(r.Err, ErrDownloadNeedsRetry) { needsRetry = true } continue } imgMarkdown := formatImageAsMarkdown(r.MimeType, r.Data) chunk := createChunk(chatID, createdTime, req.Model, map[string]interface{}{"content": imgMarkdown}, nil) fmt.Fprintf(writer, "data: %s\n\n", chunk) flusher.Flush() successCount++ } // 如果所有文件都下载失败 if successCount == 0 && lastErr != nil { var errMsg string if needsRetry { // 401/403 认证失败,提示用户重试(下次会使用新账号) errMsg = "[提示] 文件下载认证失败,请重新发送请求(系统将自动切换账号)" pool.Pool.MarkNeedsRefresh(usedAcc) // 标记当前账号需要刷新 } else { errMsg = fmt.Sprintf("生成的文件下载失败: %v", lastErr) } chunk := createChunk(chatID, createdTime, req.Model, map[string]interface{}{"content": errMsg}, nil) fmt.Fprintf(writer, "data: %s\n\n", chunk) flusher.Flush() } } // 发送结束 finishReason := "stop" if hasToolCalls { finishReason = "tool_calls" } finalChunk := createChunk(chatID, createdTime, req.Model, nil, &finishReason) fmt.Fprintf(writer, "data: %s\n\n", finalChunk) fmt.Fprintf(writer, "data: [DONE]\n\n") flusher.Flush() // 更新统计(区分图片和视频) statsSuccess = true statsOutputTokens = outputLen / 4 // 估算输出 tokens for _, pf := range pendingFiles { if strings.HasPrefix(pf.MimeType, "video/") { statsVideos++ } else { statsImages++ } } } else { // 非流式响应 var fullContent strings.Builder var fullReasoning strings.Builder replyCount := 0 var fileCount int64 var videoCount int64 for _, data := range dataList { streamResp, ok := data["streamAssistResponse"].(map[string]interface{}) if !ok { continue } answer, ok := streamResp["answer"].(map[string]interface{}) if !ok { continue } replies, ok := answer["replies"].([]interface{}) if !ok { continue } for _, reply := range replies { replyMap, ok := reply.(map[string]interface{}) if !ok { continue } replyCount++ if gc, ok := replyMap["groundedContent"].(map[string]interface{}); ok { if content, ok := gc["content"].(map[string]interface{}); ok { if file, ok := content["file"].(map[string]interface{}); ok { if mimeType, _ := file["mimeType"].(string); strings.HasPrefix(mimeType, "video/") { videoCount++ } else { fileCount++ } } } } text, imageData, imageMime, reasoning, dlErr := extractContentFromReply(replyMap, usedJWT, respSession, usedConfigID, usedOrigAuth) if reasoning != "" { fullReasoning.WriteString(reasoning) } if text != "" { fullContent.WriteString(text) } if imageData != "" && imageMime != "" { fullContent.WriteString(formatImageAsMarkdown(imageMime, imageData)) } // 检测下载是否需要重试(401/403) if dlErr != nil && errors.Is(dlErr, ErrDownloadNeedsRetry) { pool.Pool.MarkNeedsRefresh(usedAcc) fullContent.WriteString("\n\n[提示] 文件下载认证失败,请重新发送请求(系统将自动切换账号)") } } } toolCalls := extractToolCalls(dataList) // 调试日志 logger.Debug("📊 非流式响应统计: %d 个 reply, 图片=%d, 视频=%d, content长度=%d, reasoning长度=%d, 工具调用=%d", replyCount, fileCount, videoCount, fullContent.Len(), fullReasoning.Len(), len(toolCalls)) // 构建响应消息 message := gin.H{ "role": "assistant", "content": fullContent.String(), } if fullReasoning.Len() > 0 { message["reasoning_content"] = fullReasoning.String() } finishReason := "stop" if len(toolCalls) > 0 { message["tool_calls"] = toolCalls message["content"] = nil finishReason = "tool_calls" } // 构建最终响应(完全符合OpenAI格式) response := gin.H{ "id": chatID, "object": "chat.completion", "created": createdTime, "model": req.Model, "system_fingerprint": "fp_gemini_" + req.Model, "choices": []gin.H{{ "index": 0, "message": message, "logprobs": nil, "finish_reason": finishReason, }}, "usage": gin.H{ "prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0, }, } if isLongRunning && heartbeatDone != nil { close(heartbeatDone) // 停止心跳 jsonBytes, _ := json.Marshal(response) c.Writer.Write(jsonBytes) } else { c.JSON(200, response) } // 更新统计 statsSuccess = true statsOutputTokens = int64(fullContent.Len() / 4) // 粗略估算输出 tokens statsImages = fileCount statsVideos = videoCount } } func apiKeyAuth() gin.HandlerFunc { return func(c *gin.Context) { // 使用线程安全的方式获取 API Keys apiKeys := GetAPIKeys() if len(apiKeys) == 0 { c.Next() return } authHeader := c.GetHeader("Authorization") apiKey := "" if strings.HasPrefix(authHeader, "Bearer ") { apiKey = strings.TrimPrefix(authHeader, "Bearer ") } else { apiKey = c.GetHeader("X-API-Key") } if apiKey == "" { c.JSON(401, gin.H{"error": "Missing API key"}) c.Abort() return } // 验证 API Key valid := false for _, key := range apiKeys { if key == apiKey { valid = true break } } if !valid { c.JSON(401, gin.H{"error": "Invalid API key"}) c.Abort() return } c.Next() } } // runBrowserRefreshMode 有头浏览器刷新模式 func runBrowserRefreshMode(email string) { loadAppConfig() utils.InitHTTPClient(Proxy) // 强制有头模式 pool.BrowserRefreshHeadless = false logger.Info("🌐 有头浏览器刷新模式") if err := pool.Pool.Load(DataDir); err != nil { log.Fatalf("❌ 加载账号失败: %v", err) } if pool.Pool.TotalCount() == 0 { log.Fatal("❌ 没有可用账号") } // 查找目标账号 var targetAcc *pool.Account pool.Pool.WithLock(func(ready, pending []*pool.Account) { if email != "" { // 指定邮箱 for _, acc := range ready { if acc.Data.Email == email { targetAcc = acc break } } if targetAcc == nil { for _, acc := range pending { if acc.Data.Email == email { targetAcc = acc break } } } } else { // 使用第一个账号 if len(ready) > 0 { targetAcc = ready[0] } else if len(pending) > 0 { targetAcc = pending[0] } } }) if targetAcc == nil { if email != "" { log.Fatalf("❌ 找不到账号: %s", email) } log.Fatal("❌ 没有可用账号") } result := register.RefreshCookieWithBrowser(targetAcc, false, Proxy) if result.Success { if len(result.NewCookies) > 0 { } if len(result.ResponseHeaders) > 0 { } // 更新账号数据 targetAcc.Mu.Lock() targetAcc.Data.Cookies = result.SecureCookies if result.Authorization != "" { targetAcc.Data.Authorization = result.Authorization } if result.ConfigID != "" { targetAcc.ConfigID = result.ConfigID targetAcc.Data.ConfigID = result.ConfigID } if result.CSESIDX != "" { targetAcc.CSESIDX = result.CSESIDX targetAcc.Data.CSESIDX = result.CSESIDX } // 保存响应头 if len(result.ResponseHeaders) > 0 { targetAcc.Data.ResponseHeaders = result.ResponseHeaders } targetAcc.Mu.Unlock() // 保存到文件 if err := targetAcc.SaveToFile(); err != nil { logger.Warn("⚠️ 保存失败: %v", err) } else { logger.Info("💾 已保存到: %s", targetAcc.FilePath) } } else { logger.Error("❌ 刷新失败: %v", result.Error) } } var AutoSubscribeEnabled bool func init() { // 设置环境变量禁用 quic-go 的警告 os.Setenv("QUIC_GO_DISABLE_RECEIVE_BUFFER_WARNING", "true") filterStdout() } func filterStdout() { // 创建管道 r, w, err := os.Pipe() if err != nil { return } origStdout := os.Stdout os.Stdout = w go func() { buf := make([]byte, 4096) for { n, err := r.Read(buf) if err != nil { break } line := string(buf[:n]) // 过滤特定日志 if strings.Contains(line, "REALITY localAddr:") || strings.Contains(line, "DialTLSContext") || strings.Contains(line, "sys_conn.go") || strings.Contains(line, "failed to sufficiently increase receive buffer size") { continue // 丢弃 } origStdout.Write(buf[:n]) } }() } func main() { log.SetFlags(log.Ltime | log.Lshortfile) var refreshEmail string var refreshMode bool // 解析命令行参数 for i, arg := range os.Args[1:] { switch arg { case "--debug", "-d": register.RegisterDebug = true logger.Info("🔧 调试模式已启用,将保存截图到 data/screenshots/") case "--once": register.RegisterOnce = true logger.Info("🔧 单次运行模式") case "--auto": AutoSubscribeEnabled = true case "--refresh": refreshMode = true // 检查下一个参数是否是邮箱 if i+2 < len(os.Args) && !strings.HasPrefix(os.Args[i+2], "-") { refreshEmail = os.Args[i+2] } case "--help", "-h": fmt.Println(`用法: ./business2api [选项] 选项: --debug, -d 调试模式,保存注册过程截图 --auto 自动订阅模式,每小时注册获取代理 --refresh [email] 有头浏览器刷新账号(不指定email则使用第一个账号) --help, -h 显示帮助`) os.Exit(0) } } // 刷新模式:直接执行浏览器刷新后退出 if refreshMode { runBrowserRefreshMode(refreshEmail) return } loadAppConfig() utils.InitHTTPClient(Proxy) if appConfig.PoolServer.Enable { switch appConfig.PoolServer.Mode { case "client": runAsClient() return case "server": runAsServer() return } } // 本地模式 runLocalMode() } func runAsClient() { logger.Info("🔌 启动客户端模式...") // 代理实例池由异步健康检查完成后初始化 // 设置代理就绪检查回调 pool.IsProxyReady = func() bool { return proxy.Manager.IsReady() } pool.WaitProxyReady = func(timeout time.Duration) bool { logger.Info("⏳ 等待代理就绪...") result := proxy.Manager.WaitReady(timeout) if result { logger.Info("✅ 代理已就绪") } else { logger.Warn("⚠️ 代理等待超时") } return result } pool.RunBrowserRegister = func(headless bool, proxyURL string, id int) *pool.BrowserRegisterResult { result := register.RunBrowserRegister(headless, proxyURL, id) return &pool.BrowserRegisterResult{ Success: result.Success, Email: result.Email, FullName: result.FullName, SecureCookies: result.Cookies, Authorization: result.Authorization, ConfigID: result.ConfigID, CSESIDX: result.CSESIDX, Error: result.Error, } } pool.RefreshCookieWithBrowser = func(acc *pool.Account, headless bool, proxyURL string) *pool.BrowserRefreshResult { result := register.RefreshCookieWithBrowser(acc, headless, proxyURL) return &pool.BrowserRefreshResult{ Success: result.Success, SecureCookies: result.SecureCookies, ConfigID: result.ConfigID, CSESIDX: result.CSESIDX, Authorization: result.Authorization, ResponseHeaders: result.ResponseHeaders, Error: result.Error, } } pool.ClientHeadless = appConfig.Pool.RegisterHeadless pool.ClientProxy = Proxy pool.GetClientProxy = func() string { if proxy.Manager.HealthyCount() > 0 { proxyURL := proxy.Manager.Next() if proxyURL != "" { return proxyURL } } return Proxy } pool.ReleaseProxy = func(proxyURL string) { proxy.Manager.ReleaseByURL(proxyURL) logger.Debug("释放代理: %s", proxyURL) } pool.GetHealthyCount = func() int { return proxy.Manager.HealthyCount() } go func() { proxy.Manager.CheckAllHealth() if proxy.Manager.HealthyCount() > 0 { poolSize := appConfig.Pool.RegisterThreads if poolSize <= 0 { poolSize = pool.DefaultProxyCount } if poolSize > 10 { poolSize = 10 } proxy.Manager.SetMaxPoolSize(poolSize) proxy.Manager.InitInstancePool(poolSize) } }() client := pool.NewPoolClient(appConfig.PoolServer) if err := client.Start(); err != nil { log.Fatalf("❌ 客户端启动失败: %v", err) } } var poolServer *pool.PoolServer func runAsServer() { logger.Info("🖥️ 启动服务器模式...") // 加载账号 dataDir := appConfig.PoolServer.DataDir if dataDir == "" { dataDir = DataDir } if err := pool.Pool.Load(dataDir); err != nil { log.Fatalf("❌ 加载账号失败: %v", err) } // 启动配置文件热重载监听 if err := startConfigWatcher(); err != nil { logger.Warn("⚠️ 配置热重载启动失败: %v", err) } poolServer = pool.NewPoolServer(pool.Pool, appConfig.PoolServer) poolServer.StartBackground() // 启动后台任务分发和心跳检测 pool.Pool.StartPoolManager() runAPIServer() } // runAPIServer 启动 API 服务 func runAPIServer() { gin.SetMode(gin.ReleaseMode) r := gin.New() r.Use(gin.Recovery()) setupAPIRoutes(r) logger.Info("🚀 API 服务启动于 %s,账号: ready=%d, pending=%d", ListenAddr, pool.Pool.ReadyCount(), pool.Pool.PendingCount()) if err := r.Run(ListenAddr); err != nil { log.Fatalf("❌ API 服务启动失败: %v", err) } } func setupAPIRoutes(r *gin.Engine) { // 请求日志中间件 r.Use(func(c *gin.Context) { start := time.Now() path := c.Request.URL.Path method := c.Request.Method clientIP := c.ClientIP() c.Next() latency := time.Since(start) statusCode := c.Writer.Status() if statusCode >= 400 { logger.Error("❌ %s %s %s %d %v", clientIP, method, path, statusCode, latency) } else { logger.Info("✅ %s %s %s %d %v", clientIP, method, path, statusCode, latency) } }) r.GET("/", func(c *gin.Context) { stats := apiStats.GetStats() response := gin.H{ "status": "running", "service": "business2api", "version": "2.1.6", "mode": map[PoolMode]string{PoolModeLocal: "local", PoolModeServer: "server", PoolModeClient: "client"}[poolMode], // 统计数据 "uptime": stats["uptime"], "total_requests": stats["total_requests"], "success_requests": stats["success_requests"], "failed_requests": stats["failed_requests"], "success_rate": stats["success_rate"], "input_tokens": stats["input_tokens"], "output_tokens": stats["output_tokens"], "total_tokens": stats["total_tokens"], "images_generated": stats["images_generated"], "videos_generated": stats["videos_generated"], "current_rpm": stats["current_rpm"], "average_rpm": stats["average_rpm"], "pool": gin.H{ "ready": pool.Pool.ReadyCount(), "pending": pool.Pool.PendingCount(), "total": pool.Pool.TotalCount(), }, // Flow 状态 "flow_enabled": flowHandler != nil, } // 添加备注信息 if len(appConfig.Note) > 0 { response["note"] = appConfig.Note } // 服务端模式:添加客户端信息 if poolServer != nil { response["clients"] = gin.H{ "count": poolServer.GetClientCount(), "total_threads": poolServer.GetTotalThreads(), "list": poolServer.GetClientsInfo(), } } c.JSON(200, response) }) r.GET("/health", func(c *gin.Context) { c.JSON(200, gin.H{ "status": "ok", "time": time.Now().UTC().Format(time.RFC3339), "ready": pool.Pool.ReadyCount(), "pending": pool.Pool.PendingCount(), "mode": map[PoolMode]string{PoolModeLocal: "local", PoolModeServer: "server", PoolModeClient: "client"}[poolMode], }) }) // WebSocket 端点(服务端模式下用于客户端连接) r.GET("/ws", func(c *gin.Context) { if poolServer == nil { c.JSON(503, gin.H{"error": "WebSocket 服务未启用,仅在服务端模式下可用"}) return } poolServer.HandleWS(c.Writer, c.Request) }) // Pool 内部端点(客户端上传账号等,使用 X-Pool-Secret 鉴权) poolGroup := r.Group("/pool") poolGroup.Use(func(c *gin.Context) { if poolServer == nil { c.JSON(503, gin.H{"error": "Pool 服务未启用"}) c.Abort() return } secret := appConfig.PoolServer.Secret if secret != "" && c.GetHeader("X-Pool-Secret") != secret { c.JSON(401, gin.H{"error": "Unauthorized"}) c.Abort() return } c.Next() }) poolGroup.POST("/upload-account", func(c *gin.Context) { poolServer.HandleUploadAccount(c.Writer, c.Request) }) apiGroup := r.Group("/") apiGroup.Use(apiKeyAuth()) // Gemini 风格模型列表 /v1beta/models apiGroup.GET("/v1beta/models", func(c *gin.Context) { var models []gin.H for _, m := range GetAvailableModels() { models = append(models, gin.H{ "name": "models/" + m, "version": "001", "displayName": m, "description": "Gemini model: " + m, "inputTokenLimit": 1048576, "outputTokenLimit": 8192, "supportedGenerationMethods": []string{"generateContent", "countTokens"}, "temperature": 1.0, "topP": 0.95, "topK": 64, }) } c.JSON(200, gin.H{"models": models}) }) // OpenAI 风格模型列表 apiGroup.GET("/v1/models", func(c *gin.Context) { now := time.Now().Unix() var models []gin.H for _, m := range GetAvailableModels() { models = append(models, gin.H{ "id": m, "object": "model", "created": now, "owned_by": "google", "permission": []interface{}{}, }) } c.JSON(200, gin.H{"object": "list", "data": models}) }) apiGroup.POST("/v1/chat/completions", func(c *gin.Context) { var req ChatRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } if req.Model == "" { req.Model = GetAvailableModels()[0] } streamChat(c, req) }) apiGroup.POST("/v1/messages", handleClaudeMessages) // Gemini 单模型详情 GET /v1beta/models/{model} apiGroup.GET("/v1beta/models/:model", func(c *gin.Context) { modelName := c.Param("model") // 移除 "models/" 前缀(如果有) modelName = strings.TrimPrefix(modelName, "models/") // 检查模型是否存在 found := false for _, m := range GetAvailableModels() { if m == modelName { found = true break } } if !found { c.JSON(404, gin.H{"error": gin.H{ "code": 404, "message": "Model not found: " + modelName, "status": "NOT_FOUND", }}) return } c.JSON(200, gin.H{ "name": "models/" + modelName, "version": "001", "displayName": modelName, "description": "Gemini model: " + modelName, "inputTokenLimit": 1048576, "outputTokenLimit": 8192, "supportedGenerationMethods": []string{"generateContent", "countTokens"}, "temperature": 1.0, "topP": 0.95, "topK": 64, }) }) // Gemini generateContent/streamGenerateContent apiGroup.POST("/v1beta/models/*action", handleGeminiGenerate) apiGroup.POST("/v1/models/*action", handleGeminiGenerate) admin := r.Group("/admin") admin.Use(apiKeyAuth()) admin.POST("/register", func(c *gin.Context) { var req struct { Count int `json:"count"` } if err := c.ShouldBindJSON(&req); err != nil || req.Count <= 0 { req.Count = appConfig.Pool.TargetCount - pool.Pool.TotalCount() } if req.Count <= 0 { c.JSON(200, gin.H{"message": "账号数量已足够", "count": pool.Pool.TotalCount()}) return } if poolMode == PoolModeServer { // 服务端模式:注册任务会通过 WS 分发给客户端 c.JSON(200, gin.H{"message": "注册任务已加入队列,将通过 WS 分发给客户端", "target": req.Count}) return } if err := register.StartRegister(req.Count); err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } c.JSON(200, gin.H{"message": "注册已启动", "target": req.Count}) }) admin.POST("/refresh", func(c *gin.Context) { pool.Pool.Load(DataDir) c.JSON(200, gin.H{ "message": "刷新完成", "ready": pool.Pool.ReadyCount(), "pending": pool.Pool.PendingCount(), }) }) admin.GET("/status", func(c *gin.Context) { stats := pool.Pool.Stats() stats["target"] = appConfig.Pool.TargetCount stats["min"] = appConfig.Pool.MinCount stats["is_registering"] = atomic.LoadInt32(®ister.IsRegistering) == 1 stats["register_stats"] = register.Stats.Get() stats["mode"] = map[PoolMode]string{PoolModeLocal: "local", PoolModeServer: "server", PoolModeClient: "client"}[poolMode] c.JSON(200, stats) }) // 详细API统计 admin.GET("/stats", func(c *gin.Context) { detailed := apiStats.GetDetailedStats() detailed["pool"] = pool.Pool.Stats() detailed["proxy_pool"] = proxy.Manager.PoolStats() c.JSON(200, detailed) }) admin.GET("/ip", func(c *gin.Context) { c.JSON(200, ipStats.GetAllIPStats()) }) admin.POST("/force-refresh", func(c *gin.Context) { count := pool.Pool.ForceRefreshAll() c.JSON(200, gin.H{ "message": "已触发强制刷新", "count": count, }) }) admin.POST("/reload-config", func(c *gin.Context) { if err := reloadConfig(); err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } configMu.RLock() c.JSON(200, gin.H{ "message": "配置已重载", "api_keys": len(appConfig.APIKeys), "debug": appConfig.Debug, "pool_config": gin.H{ "refresh_cooldown_sec": appConfig.Pool.RefreshCooldownSec, "use_cooldown_sec": appConfig.Pool.UseCooldownSec, "max_fail_count": appConfig.Pool.MaxFailCount, "enable_browser_refresh": appConfig.Pool.EnableBrowserRefresh, "browser_refresh_headless": appConfig.Pool.BrowserRefreshHeadless, "browser_refresh_max_retry": appConfig.Pool.BrowserRefreshMaxRetry, "auto_delete_401": appConfig.Pool.AutoDelete401, }, }) configMu.RUnlock() }) admin.POST("/config/cooldown", func(c *gin.Context) { var req struct { RefreshCooldownSec int `json:"refresh_cooldown_sec"` UseCooldownSec int `json:"use_cooldown_sec"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } pool.SetCooldowns(req.RefreshCooldownSec, req.UseCooldownSec) c.JSON(200, gin.H{ "message": "冷却配置已更新", "refresh_cooldown_sec": int(pool.RefreshCooldown.Seconds()), "use_cooldown_sec": int(pool.UseCooldown.Seconds()), }) }) admin.POST("/browser-refresh", func(c *gin.Context) { var req struct { Email string `json:"email"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } if req.Email == "" { c.JSON(400, gin.H{"error": "需要提供 email"}) return } var targetAcc *pool.Account pool.Pool.WithLock(func(ready, pending []*pool.Account) { for _, acc := range ready { if acc.Data.Email == req.Email { targetAcc = acc break } } if targetAcc == nil { for _, acc := range pending { if acc.Data.Email == req.Email { targetAcc = acc break } } } }) if targetAcc == nil { c.JSON(404, gin.H{"error": "账号未找到", "email": req.Email}) return } go func() { logger.Info("🔄 手动触发浏览器刷新: %s", req.Email) result := register.RefreshCookieWithBrowser(targetAcc, pool.BrowserRefreshHeadless, Proxy) if result.Success { targetAcc.Mu.Lock() // 更新完整信息 targetAcc.Data.Cookies = result.SecureCookies if result.Authorization != "" { targetAcc.Data.Authorization = result.Authorization } if result.CSESIDX != "" { targetAcc.CSESIDX = result.CSESIDX targetAcc.Data.CSESIDX = result.CSESIDX } if result.ConfigID != "" { targetAcc.ConfigID = result.ConfigID targetAcc.Data.ConfigID = result.ConfigID } targetAcc.Data.Timestamp = time.Now().Format(time.RFC3339) targetAcc.FailCount = 0 targetAcc.Mu.Unlock() if err := targetAcc.SaveToFile(); err != nil { logger.Error("❌ [%s] 保存刷新后的数据失败: %v", req.Email, err) } else { logger.Info("✅ [%s] 刷新数据已保存到文件", req.Email) } pool.Pool.MarkNeedsRefresh(targetAcc) logger.Info("✅ 手动浏览器刷新成功: %s", req.Email) } else { logger.Error("❌ 手动浏览器刷新失败: %s - %v", req.Email, result.Error) } }() c.JSON(200, gin.H{ "message": "浏览器刷新已触发", "email": req.Email, }) }) // Flow Token 管理 admin.GET("/flow/status", func(c *gin.Context) { if flowTokenPool == nil { c.JSON(200, gin.H{ "enabled": false, "message": "Flow 服务未启用", }) return } stats := flowTokenPool.Stats() stats["enabled"] = flowHandler != nil c.JSON(200, stats) }) admin.POST("/flow/add-token", func(c *gin.Context) { if flowTokenPool == nil { c.JSON(503, gin.H{"error": "Flow 服务未启用"}) return } var req struct { Cookie string `json:"cookie"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } if req.Cookie == "" { c.JSON(400, gin.H{"error": "需要提供 cookie"}) return } tokenID, err := flowTokenPool.AddFromCookie(req.Cookie) if err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } c.JSON(200, gin.H{ "message": "Token 添加成功", "token_id": tokenID, "total": flowTokenPool.Count(), }) }) admin.POST("/flow/remove-token", func(c *gin.Context) { if flowTokenPool == nil { c.JSON(503, gin.H{"error": "Flow 服务未启用"}) return } var req struct { TokenID string `json:"token_id"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } if err := flowTokenPool.RemoveToken(req.TokenID); err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } c.JSON(200, gin.H{ "message": "Token 已移除", "total": flowTokenPool.Count(), }) }) admin.POST("/flow/reload", func(c *gin.Context) { if flowTokenPool == nil { c.JSON(503, gin.H{"error": "Flow 服务未启用"}) return } loaded, err := flowTokenPool.LoadFromDir() if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } c.JSON(200, gin.H{ "message": "已重新加载", "loaded": loaded, "total": flowTokenPool.Count(), }) }) admin.POST("/config/browser-refresh", func(c *gin.Context) { var req struct { Enable *bool `json:"enable"` Headless *bool `json:"headless"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } if req.Enable != nil { pool.EnableBrowserRefresh = *req.Enable } if req.Headless != nil { pool.BrowserRefreshHeadless = *req.Headless } c.JSON(200, gin.H{ "message": "浏览器刷新配置已更新", "enable": pool.EnableBrowserRefresh, "headless": pool.BrowserRefreshHeadless, }) }) } func runLocalMode() { // 本地模式:正常启动 if err := pool.Pool.Load(DataDir); err != nil { log.Fatalf("❌ 加载账号失败: %v", err) } // 启动配置文件热重载监听 if err := startConfigWatcher(); err != nil { logger.Warn("⚠️ 配置热重载启动失败: %v", err) } // 代理实例池由异步健康检查完成后初始化 // 检查 CONFIG_ID if DefaultConfig != "" { logger.Info("✅ 使用默认 configId: %s", DefaultConfig) } // 检查 API Key 配置 if len(GetAPIKeys()) == 0 { logger.Warn("⚠️ 未配置 API Key,API 将无鉴权运行") } // 启动号池管理 if appConfig.Pool.RefreshOnStartup { pool.Pool.StartPoolManager() } if pool.Pool.TotalCount() == 0 { needCount := appConfig.Pool.TargetCount logger.Info("📝 无账号,启动注册 %d 个...", needCount) register.StartRegister(needCount) } if appConfig.Pool.CheckIntervalMinutes > 0 { go register.PoolMaintainer() } // 启动 API 服务 runAPIServer() } ================================================ FILE: src/api/api.go ================================================ package api import ( "fmt" "strings" "github.com/gin-gonic/gin" ) // ChatRequest 聊天请求 type ChatRequest struct { Model string `json:"model"` Messages []Message `json:"messages"` Stream bool `json:"stream"` Temperature float64 `json:"temperature,omitempty"` MaxTokens int `json:"max_tokens,omitempty"` Tools []ToolDef `json:"tools,omitempty"` } // Message 消息 type Message struct { Role string `json:"role"` Content interface{} `json:"content"` } // ToolDef 工具定义 type ToolDef struct { Type string `json:"type"` Function FunctionDef `json:"function"` } // FunctionDef 函数定义 type FunctionDef struct { Name string `json:"name"` Description string `json:"description"` Parameters map[string]interface{} `json:"parameters,omitempty"` } var ( StreamChat func(c *gin.Context, req ChatRequest) FixedModels []string ) type GeminiRequest struct { Contents []GeminiContent `json:"contents"` SystemInstruction *GeminiContent `json:"systemInstruction,omitempty"` GenerationConfig map[string]interface{} `json:"generationConfig,omitempty"` GeminiTools []map[string]interface{} `json:"tools,omitempty"` } type GeminiContent struct { Role string `json:"role,omitempty"` Parts []GeminiPart `json:"parts"` } type GeminiPart struct { Text string `json:"text,omitempty"` InlineData *GeminiInlineData `json:"inlineData,omitempty"` } type GeminiInlineData struct { MimeType string `json:"mimeType"` Data string `json:"data"` } func HandleGeminiGenerate(c *gin.Context) { action := c.Param("action") if action == "" { c.JSON(400, gin.H{"error": gin.H{"code": 400, "message": "Missing model action", "status": "INVALID_ARGUMENT"}}) return } // 去掉开头的 / action = strings.TrimPrefix(action, "/") // 解析模型名和动作 var model string var isStream bool if idx := strings.LastIndex(action, ":"); idx > 0 { model = action[:idx] actionType := action[idx+1:] isStream = actionType == "streamGenerateContent" } else { model = action } if model == "" { model = FixedModels[0] } var geminiReq GeminiRequest if err := c.ShouldBindJSON(&geminiReq); err != nil { c.JSON(400, gin.H{"error": gin.H{"code": 400, "message": err.Error(), "status": "INVALID_ARGUMENT"}}) return } var messages []Message // 处理systemInstruction if geminiReq.SystemInstruction != nil && len(geminiReq.SystemInstruction.Parts) > 0 { var sysText string for _, part := range geminiReq.SystemInstruction.Parts { if part.Text != "" { sysText += part.Text } } if sysText != "" { messages = append(messages, Message{Role: "system", Content: sysText}) } } for _, content := range geminiReq.Contents { role := content.Role if role == "model" { role = "assistant" } var textParts []string var contentParts []interface{} for _, part := range content.Parts { if part.Text != "" { textParts = append(textParts, part.Text) } if part.InlineData != nil { contentParts = append(contentParts, map[string]interface{}{ "type": "image_url", "image_url": map[string]string{ "url": fmt.Sprintf("data:%s;base64,%s", part.InlineData.MimeType, part.InlineData.Data), }, }) } } if len(contentParts) > 0 { if len(textParts) > 0 { contentParts = append([]interface{}{map[string]interface{}{"type": "text", "text": strings.Join(textParts, "\n")}}, contentParts...) } messages = append(messages, Message{Role: role, Content: contentParts}) } else if len(textParts) > 0 { messages = append(messages, Message{Role: role, Content: strings.Join(textParts, "\n")}) } } // 流式判断:路径中包含streamGenerateContent 或 query参数 alt=sse stream := isStream || c.Query("alt") == "sse" var tools []ToolDef for _, gt := range geminiReq.GeminiTools { if funcDecls, ok := gt["functionDeclarations"].([]interface{}); ok { for _, fd := range funcDecls { if funcMap, ok := fd.(map[string]interface{}); ok { name, _ := funcMap["name"].(string) desc, _ := funcMap["description"].(string) params, _ := funcMap["parameters"].(map[string]interface{}) tools = append(tools, ToolDef{ Type: "function", Function: FunctionDef{ Name: name, Description: desc, Parameters: params, }, }) } } } } req := ChatRequest{ Model: model, Messages: messages, Stream: stream, Tools: tools, } StreamChat(c, req) } type ClaudeRequest struct { Model string `json:"model"` Messages []Message `json:"messages"` System string `json:"system,omitempty"` MaxTokens int `json:"max_tokens,omitempty"` Stream bool `json:"stream"` Temperature float64 `json:"temperature,omitempty"` Tools []ToolDef `json:"tools,omitempty"` } func HandleClaudeMessages(c *gin.Context) { var claudeReq ClaudeRequest if err := c.ShouldBindJSON(&claudeReq); err != nil { c.JSON(400, gin.H{"type": "error", "error": gin.H{"type": "invalid_request_error", "message": err.Error()}}) return } req := ChatRequest{ Model: claudeReq.Model, Messages: claudeReq.Messages, Stream: claudeReq.Stream, Temperature: claudeReq.Temperature, Tools: claudeReq.Tools, } if claudeReq.System != "" { systemMsg := Message{Role: "system", Content: claudeReq.System} req.Messages = append([]Message{systemMsg}, req.Messages...) } // 保持模型名原样,不做映射 if req.Model == "" { req.Model = FixedModels[0] } StreamChat(c, req) } ================================================ FILE: src/logger/logger.go ================================================ package logger import ( "fmt" "log" "os" "sync" "time" ) // Level 日志级别 type Level int const ( LevelError Level = iota LevelWarn LevelInfo LevelDebug ) var levelNames = map[Level]string{ LevelError: "ERROR", LevelWarn: "WARN", LevelInfo: "INFO", LevelDebug: "DEBUG", } // Logger 日志记录器 type Logger struct { level Level prefix string mu sync.Mutex } var ( defaultLogger = &Logger{level: LevelInfo} debugMode = false ) // SetDebugMode 设置调试模式 func SetDebugMode(debug bool) { debugMode = debug if debug { defaultLogger.level = LevelDebug } else { defaultLogger.level = LevelInfo } } // IsDebug 是否为调试模式 func IsDebug() bool { return debugMode } // SetLevel 设置日志级别 func SetLevel(level Level) { defaultLogger.level = level } func (l *Logger) log(level Level, format string, args ...interface{}) { if level > l.level { return } l.mu.Lock() defer l.mu.Unlock() timestamp := time.Now().Format("15:04:05") levelStr := levelNames[level] msg := fmt.Sprintf(format, args...) if l.prefix != "" { log.Printf("[%s] [%s] [%s] %s", timestamp, levelStr, l.prefix, msg) } else { log.Printf("[%s] [%s] %s", timestamp, levelStr, msg) } } // Error 错误日志(始终输出) func Error(format string, args ...interface{}) { defaultLogger.log(LevelError, format, args...) } // Warn 警告日志(始终输出) func Warn(format string, args ...interface{}) { defaultLogger.log(LevelWarn, format, args...) } // Info 信息日志(正常模式输出) func Info(format string, args ...interface{}) { defaultLogger.log(LevelInfo, format, args...) } // Debug 调试日志(仅debug模式输出) func Debug(format string, args ...interface{}) { defaultLogger.log(LevelDebug, format, args...) } // WithPrefix 创建带前缀的子日志器 func WithPrefix(prefix string) *Logger { return &Logger{ level: defaultLogger.level, prefix: prefix, } } func (l *Logger) Error(format string, args ...interface{}) { l.log(LevelError, format, args...) } func (l *Logger) Warn(format string, args ...interface{}) { l.log(LevelWarn, format, args...) } func (l *Logger) Info(format string, args ...interface{}) { l.log(LevelInfo, format, args...) } func (l *Logger) Debug(format string, args ...interface{}) { l.log(LevelDebug, format, args...) } func init() { log.SetFlags(0) log.SetOutput(os.Stdout) } ================================================ FILE: src/pool/pool.go ================================================ package pool import ( "crypto/hmac" "crypto/sha256" "encoding/base64" "encoding/json" "fmt" "log" "net/http" "os" "path/filepath" "strings" "sync" "sync/atomic" "time" "business2api/src/logger" ) // ==================== 数据结构 ==================== // Cookie 账号Cookie type Cookie struct { Name string `json:"name"` Value string `json:"value"` Domain string `json:"domain"` } // AccountData 账号数据 type AccountData struct { Email string `json:"email"` FullName string `json:"fullName"` Authorization string `json:"authorization"` Cookies []Cookie `json:"cookies"` CookieString string `json:"cookie_string,omitempty"` ResponseHeaders map[string]string `json:"response_headers,omitempty"` Timestamp string `json:"timestamp"` ConfigID string `json:"configId,omitempty"` CSESIDX string `json:"csesidx,omitempty"` } func ParseCookieString(cookieStr string) []Cookie { var cookies []Cookie if cookieStr == "" { return cookies } parts := strings.Split(cookieStr, "; ") for _, part := range parts { part = strings.TrimSpace(part) if part == "" { continue } idx := strings.Index(part, "=") if idx > 0 { cookies = append(cookies, Cookie{ Name: part[:idx], Value: part[idx+1:], Domain: ".gemini.google", // 默认域名 }) } } return cookies } func (a *AccountData) GetAllCookies() []Cookie { if len(a.Cookies) > 0 { return a.Cookies } if a.CookieString != "" { return ParseCookieString(a.CookieString) } return nil } // AccountStatus 账号状态 type AccountStatus int const ( StatusPending AccountStatus = iota // 待刷新 StatusReady // 就绪可用 StatusCooldown // 冷却中 StatusInvalid // 失效 ) // Account 账号实例 type Account struct { Data AccountData FilePath string JWT string JWTExpires time.Time ConfigID string CSESIDX string LastRefresh time.Time LastUsed time.Time // 最后使用时间 Refreshed bool FailCount int // 连续失败次数 BrowserRefreshCount int // 浏览器刷新尝试次数 SuccessCount int // 成功次数 TotalCount int // 总使用次数 DailyCount int // 每日调用次数 DailyCountDate string // 每日计数日期 (YYYY-MM-DD) Status AccountStatus Mu sync.Mutex } // SetCooldownMultiplier 设置冷却时间倍数(用于429限流) func (acc *Account) SetCooldownMultiplier(multiplier int) { acc.Mu.Lock() acc.LastUsed = time.Now().Add(UseCooldown * time.Duration(multiplier-1)) acc.Mu.Unlock() } // 默认冷却时间(可通过配置覆盖) var ( RefreshCooldown = 4 * time.Minute // 刷新冷却 UseCooldown = 15 * time.Second // 使用冷却 JWTRefreshThreshold = 60 * time.Second // JWT刷新阈值 MaxFailCount = 3 // 最大连续失败次数 EnableBrowserRefresh = true // 是否启用浏览器刷新 BrowserRefreshHeadless = true // 浏览器刷新是否无头模式 BrowserRefreshMaxRetry = 1 // 浏览器刷新最大重试次数 AutoDelete401 = false // 401时是否自动删除账号 DailyLimit = 3000 // 每账号每日最大调用次数 DataDir string DefaultConfig string Proxy string JwtTTL = 270 * time.Second HTTPClient *http.Client ) type RefreshCookieFunc func(acc *Account, headless bool, proxy string) *BrowserRefreshResult type BrowserRefreshResult struct { Success bool SecureCookies []Cookie Authorization string ConfigID string CSESIDX string ResponseHeaders map[string]string Error error } var RefreshCookieWithBrowser RefreshCookieFunc func readResponseBody(resp *http.Response) ([]byte, error) { body := make([]byte, 0) buf := make([]byte, 4096) for { n, err := resp.Body.Read(buf) if n > 0 { body = append(body, buf[:n]...) } if err != nil { break } } return body, nil } type AccountPool struct { readyAccounts []*Account pendingAccounts []*Account index uint64 mu sync.RWMutex refreshInterval time.Duration refreshWorkers int stopChan chan struct{} totalSuccess int64 totalFailed int64 totalRequests int64 } func (p *AccountPool) GetReadyAccounts() []*Account { p.mu.RLock() defer p.mu.RUnlock() return p.readyAccounts } func (p *AccountPool) GetPendingAccounts() []*Account { p.mu.RLock() defer p.mu.RUnlock() return p.pendingAccounts } func (p *AccountPool) WithLock(fn func(ready, pending []*Account)) { p.mu.RLock() defer p.mu.RUnlock() fn(p.readyAccounts, p.pendingAccounts) } func (p *AccountPool) WithWriteLock(fn func(ready, pending []*Account) ([]*Account, []*Account)) { p.mu.Lock() defer p.mu.Unlock() p.readyAccounts, p.pendingAccounts = fn(p.readyAccounts, p.pendingAccounts) } var Pool = &AccountPool{ refreshInterval: 5 * time.Second, refreshWorkers: 5, stopChan: make(chan struct{}), } func SetCooldowns(refreshSec, useSec int) { if refreshSec > 0 { RefreshCooldown = time.Duration(refreshSec) * time.Second } if useSec > 0 { UseCooldown = time.Duration(useSec) * time.Second } logger.Info("⚙️ 冷却配置: 刷新=%v, 使用=%v", RefreshCooldown, UseCooldown) } // SetDailyLimit 设置每账号每日最大调用次数 func SetDailyLimit(limit int) { if limit >= 0 { DailyLimit = limit if limit == 0 { logger.Info("⚙️ 每日调用限制: 无限制") } else { logger.Info("⚙️ 每日调用限制: %d次/账号", limit) } } } func (p *AccountPool) Load(dir string) error { p.mu.Lock() defer p.mu.Unlock() files, err := filepath.Glob(filepath.Join(dir, "*.json")) if err != nil { return err } existingAccounts := make(map[string]*Account) for _, acc := range p.readyAccounts { existingAccounts[acc.FilePath] = acc } for _, acc := range p.pendingAccounts { existingAccounts[acc.FilePath] = acc } var newReadyAccounts []*Account var newPendingAccounts []*Account for _, f := range files { if acc, ok := existingAccounts[f]; ok { if acc.Refreshed { newReadyAccounts = append(newReadyAccounts, acc) } else { newPendingAccounts = append(newPendingAccounts, acc) } delete(existingAccounts, f) continue } data, err := os.ReadFile(f) if err != nil { log.Printf("⚠️ 读取 %s 失败: %v", f, err) continue } var acc AccountData if err := json.Unmarshal(data, &acc); err != nil { log.Printf("⚠️ 解析 %s 失败: %v", f, err) continue } csesidx := acc.CSESIDX if csesidx == "" { csesidx = extractCSESIDX(acc.Authorization) } if csesidx == "" { log.Printf("⚠️ %s 无法获取 csesidx", f) continue } configID := acc.ConfigID if configID == "" && DefaultConfig != "" { configID = DefaultConfig } newPendingAccounts = append(newPendingAccounts, &Account{ Data: acc, FilePath: f, CSESIDX: csesidx, ConfigID: configID, Refreshed: false, }) } p.readyAccounts = newReadyAccounts p.pendingAccounts = newPendingAccounts return nil } // GetPendingAccount 获取待刷新账号 func (p *AccountPool) GetPendingAccount() *Account { p.mu.Lock() defer p.mu.Unlock() if len(p.pendingAccounts) == 0 { return nil } acc := p.pendingAccounts[0] p.pendingAccounts = p.pendingAccounts[1:] return acc } // MarkReady 标记账号为就绪 func (p *AccountPool) MarkReady(acc *Account) { p.mu.Lock() defer p.mu.Unlock() acc.Refreshed = true p.readyAccounts = append(p.readyAccounts, acc) } // MarkPending 标记账号待刷新 func (p *AccountPool) MarkPending(acc *Account) { p.mu.Lock() defer p.mu.Unlock() for i, a := range p.readyAccounts { if a == acc { p.readyAccounts = append(p.readyAccounts[:i], p.readyAccounts[i+1:]...) break } } acc.Mu.Lock() acc.Refreshed = false acc.Mu.Unlock() p.pendingAccounts = append(p.pendingAccounts, acc) log.Printf("🔄 账号 %s 移至刷新池", filepath.Base(acc.FilePath)) } // RemoveAccount 删除失效账号 func (p *AccountPool) RemoveAccount(acc *Account) { if err := os.Remove(acc.FilePath); err != nil { log.Printf("⚠️ 删除文件失败 %s: %v", acc.FilePath, err) } else { log.Printf("🗑️ 已删除失效账号: %s", filepath.Base(acc.FilePath)) } } // SaveToFile 保存账号到文件 func (acc *Account) SaveToFile() error { acc.Mu.Lock() defer acc.Mu.Unlock() acc.Data.Timestamp = time.Now().Format(time.RFC3339) // 同时生成 cookie 字符串(方便调试和兼容老版本) if len(acc.Data.Cookies) > 0 { var cookieParts []string for _, c := range acc.Data.Cookies { cookieParts = append(cookieParts, fmt.Sprintf("%s=%s", c.Name, c.Value)) } acc.Data.CookieString = strings.Join(cookieParts, "; ") } data, err := json.MarshalIndent(acc.Data, "", " ") if err != nil { return fmt.Errorf("序列化账号数据失败: %w", err) } if err := os.WriteFile(acc.FilePath, data, 0644); err != nil { return fmt.Errorf("写入文件失败: %w", err) } return nil } // StartPoolManager 启动号池管理器 func (p *AccountPool) StartPoolManager() { for i := 0; i < p.refreshWorkers; i++ { go p.refreshWorker(i) } go p.scanWorker() } func (p *AccountPool) refreshWorker(id int) { for { select { case <-p.stopChan: return default: } acc := p.GetPendingAccount() if acc == nil { time.Sleep(time.Second) continue } // 检查冷却 if time.Since(acc.LastRefresh) < RefreshCooldown { acc.Mu.Lock() acc.Refreshed = true acc.Status = StatusReady acc.Mu.Unlock() p.MarkReady(acc) continue } acc.JWTExpires = time.Time{} if err := acc.RefreshJWT(); err != nil { errMsg := err.Error() // 认证失败:根据配置决定是否删除或尝试刷新 if strings.Contains(errMsg, "账号失效") || strings.Contains(errMsg, "401") || strings.Contains(errMsg, "403") { log.Printf("⚠️ [worker-%d] [%s] 认证失效: %v", id, acc.Data.Email, err) // 如果配置了401自动删除,直接删除账号 if AutoDelete401 { log.Printf("🗑️ [worker-%d] [%s] 401自动删除已启用,移除账号", id, acc.Data.Email) acc.Mu.Lock() acc.Status = StatusInvalid acc.Mu.Unlock() p.RemoveAccount(acc) continue } // 检查是否可以进行浏览器刷新 acc.Mu.Lock() browserRefreshCount := acc.BrowserRefreshCount acc.Mu.Unlock() if EnableBrowserRefresh && BrowserRefreshMaxRetry > 0 && browserRefreshCount < BrowserRefreshMaxRetry && RefreshCookieWithBrowser != nil { acc.Mu.Lock() acc.BrowserRefreshCount++ acc.Mu.Unlock() refreshResult := RefreshCookieWithBrowser(acc, BrowserRefreshHeadless, Proxy) if refreshResult.Success { acc.Mu.Lock() acc.Data.Cookies = refreshResult.SecureCookies if refreshResult.Authorization != "" { acc.Data.Authorization = refreshResult.Authorization } if refreshResult.ConfigID != "" { acc.ConfigID = refreshResult.ConfigID acc.Data.ConfigID = refreshResult.ConfigID } if refreshResult.CSESIDX != "" { acc.CSESIDX = refreshResult.CSESIDX acc.Data.CSESIDX = refreshResult.CSESIDX } if len(refreshResult.ResponseHeaders) > 0 { acc.Data.ResponseHeaders = refreshResult.ResponseHeaders } acc.FailCount = 0 acc.BrowserRefreshCount = 0 // 成功后重置计数 acc.JWTExpires = time.Time{} // 重置JWT过期时间 acc.Status = StatusPending acc.Mu.Unlock() // 保存更新后的账号 if err := acc.SaveToFile(); err != nil { log.Printf("⚠️ [%s] 保存刷新后的账号失败: %v", acc.Data.Email, err) } p.mu.Lock() p.pendingAccounts = append(p.pendingAccounts, acc) p.mu.Unlock() continue } else { log.Printf("⚠️ [worker-%d] [%s] 浏览器刷新失败: %v", id, acc.Data.Email, refreshResult.Error) } } else if browserRefreshCount >= BrowserRefreshMaxRetry && BrowserRefreshMaxRetry > 0 { log.Printf("⚠️ [worker-%d] [%s] 已达浏览器刷新上限 (%d次),跳过浏览器刷新", id, acc.Data.Email, BrowserRefreshMaxRetry) } acc.Mu.Lock() acc.FailCount++ failCount := acc.FailCount browserRefreshCount = acc.BrowserRefreshCount acc.Mu.Unlock() maxRetry := MaxFailCount * 3 // 401的最大重试次数更宽松 if maxRetry < 10 { maxRetry = 10 // 至少重试10次 } if browserRefreshCount >= BrowserRefreshMaxRetry && failCount >= maxRetry { acc.Mu.Lock() acc.Status = StatusInvalid acc.Mu.Unlock() p.RemoveAccount(acc) continue } waitTime := time.Duration(failCount*30) * time.Second if waitTime > 5*time.Minute { waitTime = 5 * time.Minute // 最大等待5分钟 } log.Printf("⏳ [worker-%d] [%s] 401刷新失败 (%d/%d次),%v后重试", id, acc.Data.Email, failCount, maxRetry, waitTime) time.Sleep(waitTime) p.mu.Lock() p.pendingAccounts = append(p.pendingAccounts, acc) p.mu.Unlock() continue } // 冷却中:直接标记就绪 if strings.Contains(errMsg, "刷新冷却中") { acc.Mu.Lock() acc.Refreshed = true acc.Status = StatusReady acc.Mu.Unlock() p.MarkReady(acc) continue } // 其他错误:累计失败次数 acc.Mu.Lock() acc.FailCount++ failCount := acc.FailCount acc.Mu.Unlock() if failCount >= MaxFailCount { log.Printf("❌ [worker-%d] [%s] 连续失败 %d 次,移除账号: %v", id, acc.Data.Email, failCount, err) acc.Mu.Lock() acc.Status = StatusInvalid acc.Mu.Unlock() p.RemoveAccount(acc) } else { log.Printf("⚠️ [worker-%d] [%s] 刷新失败 (%d/%d): %v", id, acc.Data.Email, failCount, MaxFailCount, err) // 延迟后重试 time.Sleep(time.Duration(failCount) * 5 * time.Second) p.mu.Lock() p.pendingAccounts = append(p.pendingAccounts, acc) p.mu.Unlock() } } else { // 刷新成功:重置失败计数 acc.Mu.Lock() acc.FailCount = 0 acc.Status = StatusReady acc.Mu.Unlock() if err := acc.SaveToFile(); err != nil { log.Printf("⚠️ [%s] 写回文件失败: %v", acc.Data.Email, err) } p.MarkReady(acc) } } } func (p *AccountPool) scanWorker() { ticker := time.NewTicker(p.refreshInterval) fileScanTicker := time.NewTicker(5 * time.Minute) defer ticker.Stop() defer fileScanTicker.Stop() for { select { case <-p.stopChan: return case <-fileScanTicker.C: p.Load(DataDir) case <-ticker.C: p.RefreshExpiredAccounts() } } } // RefreshExpiredAccounts 刷新即将过期的账号 func (p *AccountPool) RefreshExpiredAccounts() { p.mu.Lock() defer p.mu.Unlock() var stillReady []*Account refreshed := 0 now := time.Now() for _, acc := range p.readyAccounts { acc.Mu.Lock() jwtExpires := acc.JWTExpires lastRefresh := acc.LastRefresh acc.Mu.Unlock() needsRefresh := jwtExpires.IsZero() || now.Add(JWTRefreshThreshold).After(jwtExpires) inCooldown := now.Sub(lastRefresh) < RefreshCooldown if needsRefresh && !inCooldown { acc.Mu.Lock() acc.Refreshed = false acc.Mu.Unlock() p.pendingAccounts = append(p.pendingAccounts, acc) refreshed++ } else { stillReady = append(stillReady, acc) } } p.readyAccounts = stillReady if refreshed > 0 { log.Printf("🔄 扫描刷新: %d 个账号JWT即将过期", refreshed) } } func (p *AccountPool) RefreshAllAccounts() { p.mu.Lock() defer p.mu.Unlock() var stillReady []*Account refreshed, skipped := 0, 0 for _, acc := range p.readyAccounts { if time.Since(acc.LastRefresh) < RefreshCooldown { stillReady = append(stillReady, acc) skipped++ continue } acc.Refreshed = false acc.JWTExpires = time.Time{} p.pendingAccounts = append(p.pendingAccounts, acc) refreshed++ } p.readyAccounts = stillReady if refreshed > 0 { log.Printf("🔄 全量刷新: %d 个账号已加入刷新队列,%d 个在冷却中跳过", refreshed, skipped) } } // checkAndUpdateDailyCount 检查并更新每日计数,返回是否超限 func (acc *Account) checkAndUpdateDailyCount() bool { today := time.Now().Format("2006-01-02") if acc.DailyCountDate != today { // 新的一天,重置计数 acc.DailyCountDate = today acc.DailyCount = 0 } // 检查是否超过每日限制 if DailyLimit > 0 && acc.DailyCount >= DailyLimit { return true // 超限 } acc.DailyCount++ return false } // GetDailyUsage 获取每日使用情况 func (acc *Account) GetDailyUsage() (count int, limit int, date string) { acc.Mu.Lock() defer acc.Mu.Unlock() today := time.Now().Format("2006-01-02") if acc.DailyCountDate != today { return 0, DailyLimit, today } return acc.DailyCount, DailyLimit, acc.DailyCountDate } func (p *AccountPool) Next() *Account { p.mu.RLock() defer p.mu.RUnlock() if len(p.readyAccounts) == 0 { return nil } n := len(p.readyAccounts) startIdx := atomic.AddUint64(&p.index, 1) - 1 now := time.Now() var bestAccount *Account var oldestUsed time.Time var allExceededDaily bool = true // 第一轮:找不在使用冷却中且未超日限的账号 for i := 0; i < n; i++ { acc := p.readyAccounts[(startIdx+uint64(i))%uint64(n)] acc.Mu.Lock() inUseCooldown := now.Sub(acc.LastUsed) < UseCooldown lastUsed := acc.LastUsed // 检查每日限制(不更新计数) today := now.Format("2006-01-02") dailyCount := acc.DailyCount if acc.DailyCountDate != today { dailyCount = 0 } exceededDaily := DailyLimit > 0 && dailyCount >= DailyLimit acc.Mu.Unlock() if exceededDaily { continue // 跳过已达每日限制的账号 } allExceededDaily = false if !inUseCooldown { // 找到可用账号,标记使用时间并更新每日计数 acc.Mu.Lock() acc.LastUsed = now acc.TotalCount++ acc.checkAndUpdateDailyCount() acc.Mu.Unlock() atomic.AddInt64(&p.totalRequests, 1) return acc } // 记录最久未使用的账号作为备选 if bestAccount == nil || lastUsed.Before(oldestUsed) { bestAccount = acc oldestUsed = lastUsed } } // 所有账号都超过每日限制 if allExceededDaily { log.Printf("⚠️ 所有账号已达每日调用上限 (%d次/天)", DailyLimit) return nil } // 所有未超限的账号都在冷却中,返回最久未使用的 if bestAccount != nil { bestAccount.Mu.Lock() bestAccount.LastUsed = now bestAccount.TotalCount++ bestAccount.checkAndUpdateDailyCount() bestAccount.Mu.Unlock() atomic.AddInt64(&p.totalRequests, 1) log.Printf("⏳ 所有账号在使用冷却中,选择最久未用: %s", bestAccount.Data.Email) } return bestAccount } // MarkUsed 标记账号已使用(成功) func (p *AccountPool) MarkUsed(acc *Account, success bool) { if acc == nil { return } acc.Mu.Lock() defer acc.Mu.Unlock() if success { acc.SuccessCount++ acc.FailCount = 0 // 重置连续失败 atomic.AddInt64(&p.totalSuccess, 1) } else { acc.FailCount++ atomic.AddInt64(&p.totalFailed, 1) } } // MarkNeedsRefresh 标记账号需要刷新(遇到401/403等) func (p *AccountPool) MarkNeedsRefresh(acc *Account) { if acc == nil { return } acc.Mu.Lock() acc.LastRefresh = time.Time{} // 重置刷新时间,强制刷新 acc.Mu.Unlock() p.MarkPending(acc) } func (p *AccountPool) Count() int { p.mu.RLock(); defer p.mu.RUnlock(); return len(p.readyAccounts) } func (p *AccountPool) PendingCount() int { p.mu.RLock() defer p.mu.RUnlock() return len(p.pendingAccounts) } func (p *AccountPool) ReadyCount() int { p.mu.RLock() defer p.mu.RUnlock() return len(p.readyAccounts) } func (p *AccountPool) TotalCount() int { p.mu.RLock() defer p.mu.RUnlock() return len(p.readyAccounts) + len(p.pendingAccounts) } // Stats 返回号池统计信息 func (p *AccountPool) Stats() map[string]interface{} { p.mu.RLock() defer p.mu.RUnlock() totalSuccess := atomic.LoadInt64(&p.totalSuccess) totalFailed := atomic.LoadInt64(&p.totalFailed) totalRequests := atomic.LoadInt64(&p.totalRequests) successRate := float64(0) if totalRequests > 0 { successRate = float64(totalSuccess) / float64(totalRequests) * 100 } // 统计每日可用账号数 today := time.Now().Format("2006-01-02") availableToday := 0 exceededToday := 0 for _, acc := range p.readyAccounts { acc.Mu.Lock() dailyCount := acc.DailyCount if acc.DailyCountDate != today { dailyCount = 0 } acc.Mu.Unlock() if DailyLimit == 0 || dailyCount < DailyLimit { availableToday++ } else { exceededToday++ } } return map[string]interface{}{ "ready": len(p.readyAccounts), "pending": len(p.pendingAccounts), "total": len(p.readyAccounts) + len(p.pendingAccounts), "available_today": availableToday, "exceeded_today": exceededToday, "total_requests": totalRequests, "total_success": totalSuccess, "total_failed": totalFailed, "success_rate": fmt.Sprintf("%.1f%%", successRate), "daily_limit": DailyLimit, "cooldowns": map[string]interface{}{ "refresh_sec": int(RefreshCooldown.Seconds()), "use_sec": int(UseCooldown.Seconds()), }, } } // AccountInfo 账号信息(用于API返回) type AccountInfo struct { Email string `json:"email"` Status string `json:"status"` LastRefresh time.Time `json:"last_refresh"` LastUsed time.Time `json:"last_used"` FailCount int `json:"fail_count"` SuccessCount int `json:"success_count"` TotalCount int `json:"total_count"` DailyCount int `json:"daily_count"` DailyLimit int `json:"daily_limit"` DailyRemaining int `json:"daily_remaining"` JWTExpires time.Time `json:"jwt_expires"` } // ListAccounts 列出所有账号信息 func (p *AccountPool) ListAccounts() []AccountInfo { p.mu.RLock() defer p.mu.RUnlock() var accounts []AccountInfo statusNames := map[AccountStatus]string{ StatusPending: "pending", StatusReady: "ready", StatusCooldown: "cooldown", StatusInvalid: "invalid", } today := time.Now().Format("2006-01-02") addAccounts := func(list []*Account) { for _, acc := range list { acc.Mu.Lock() dailyCount := acc.DailyCount if acc.DailyCountDate != today { dailyCount = 0 } dailyRemaining := DailyLimit - dailyCount if DailyLimit == 0 { dailyRemaining = -1 // -1 表示无限制 } else if dailyRemaining < 0 { dailyRemaining = 0 } info := AccountInfo{ Email: acc.Data.Email, Status: statusNames[acc.Status], LastRefresh: acc.LastRefresh, LastUsed: acc.LastUsed, FailCount: acc.FailCount, SuccessCount: acc.SuccessCount, TotalCount: acc.TotalCount, DailyCount: dailyCount, DailyLimit: DailyLimit, DailyRemaining: dailyRemaining, JWTExpires: acc.JWTExpires, } acc.Mu.Unlock() accounts = append(accounts, info) } } addAccounts(p.readyAccounts) addAccounts(p.pendingAccounts) return accounts } // ForceRefreshAll 强制刷新所有账号 func (p *AccountPool) ForceRefreshAll() int { p.mu.Lock() defer p.mu.Unlock() count := 0 for _, acc := range p.readyAccounts { acc.Mu.Lock() acc.Refreshed = false acc.JWTExpires = time.Time{} acc.LastRefresh = time.Time{} // 强制跳过冷却 acc.Mu.Unlock() p.pendingAccounts = append(p.pendingAccounts, acc) count++ } p.readyAccounts = nil log.Printf("🔄 强制刷新: %d 个账号已加入刷新队列", count) return count } func urlsafeB64Encode(data []byte) string { return strings.TrimRight(base64.URLEncoding.EncodeToString(data), "=") } func kqEncode(s string) string { var b []byte for _, ch := range s { v := int(ch) if v > 255 { b = append(b, byte(v&255), byte(v>>8)) } else { b = append(b, byte(v)) } } return urlsafeB64Encode(b) } func createJWT(keyBytes []byte, keyID, csesidx string) string { now := time.Now().Unix() header := map[string]interface{}{"alg": "HS256", "typ": "JWT", "kid": keyID} payload := map[string]interface{}{ "iss": "https://business.gemini.google", "aud": "https://biz-discoveryengine.googleapis.com", "sub": fmt.Sprintf("csesidx/%s", csesidx), "iat": now, "exp": now + 300, "nbf": now, } headerJSON, _ := json.Marshal(header) payloadJSON, _ := json.Marshal(payload) message := kqEncode(string(headerJSON)) + "." + kqEncode(string(payloadJSON)) h := hmac.New(sha256.New, keyBytes) h.Write([]byte(message)) return message + "." + urlsafeB64Encode(h.Sum(nil)) } func extractCSESIDX(auth string) string { parts := strings.Split(auth, " ") if len(parts) != 2 { return "" } jwtParts := strings.Split(parts[1], ".") if len(jwtParts) != 3 { return "" } payload, err := base64.RawURLEncoding.DecodeString(jwtParts[1]) if err != nil { return "" } var claims struct { Sub string `json:"sub"` } if err := json.Unmarshal(payload, &claims); err != nil { return "" } if strings.HasPrefix(claims.Sub, "csesidx/") { return strings.TrimPrefix(claims.Sub, "csesidx/") } return "" } // ==================== 账号操作 ==================== func (acc *Account) getCookie(name string) string { for _, c := range acc.Data.Cookies { if c.Name == name { return c.Value } } return "" } // RefreshJWT 刷新JWT func (acc *Account) RefreshJWT() error { acc.Mu.Lock() defer acc.Mu.Unlock() // 检查JWT是否仍有效 if time.Now().Before(acc.JWTExpires) { return nil } // 检查刷新冷却 if time.Since(acc.LastRefresh) < RefreshCooldown { return fmt.Errorf("刷新冷却中,剩余 %.0f 秒", (RefreshCooldown - time.Since(acc.LastRefresh)).Seconds()) } // 获取必要的Cookie secureSES := acc.getCookie("__Secure-C_SES") hostOSES := acc.getCookie("__Host-C_OSES") // 验证Cookie是否存在 if secureSES == "" { return fmt.Errorf("账号失效: 缺少 __Secure-C_SES Cookie") } // 构建Cookie字符串 cookie := fmt.Sprintf("__Secure-C_SES=%s", secureSES) if hostOSES != "" { cookie += fmt.Sprintf("; __Host-C_OSES=%s", hostOSES) } // 添加其他可能需要的Cookie for _, c := range acc.Data.Cookies { if c.Name != "__Secure-C_SES" && c.Name != "__Host-C_OSES" { if strings.HasPrefix(c.Name, "__Secure-") || strings.HasPrefix(c.Name, "__Host-") { cookie += fmt.Sprintf("; %s=%s", c.Name, c.Value) } } } req, _ := http.NewRequest("GET", "https://business.gemini.google/auth/getoxsrf", nil) q := req.URL.Query() q.Add("csesidx", acc.CSESIDX) req.URL.RawQuery = q.Encode() req.Header.Set("Cookie", cookie) req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") req.Header.Set("Referer", "https://business.gemini.google/") resp, err := HTTPClient.Do(req) if err != nil { return fmt.Errorf("getoxsrf 请求失败: %w", err) } defer resp.Body.Close() if resp.StatusCode != 200 { body, _ := readResponseBody(resp) bodyStr := string(body) if len(bodyStr) > 200 { bodyStr = bodyStr[:200] } // 详细的错误分类 switch resp.StatusCode { case 401, 403: // 认证失败,Cookie可能过期 return fmt.Errorf("账号失效: %d - Cookie可能已过期", resp.StatusCode) case 429: // 速率限制 return fmt.Errorf("请求频率过高: %d", resp.StatusCode) case 500, 502, 503, 504: // 服务器错误,可能是临时的 return fmt.Errorf("服务器错误: %d, 稍后重试", resp.StatusCode) default: return fmt.Errorf("getoxsrf 失败: %d %s", resp.StatusCode, bodyStr) } } body, _ := readResponseBody(resp) txt := strings.TrimPrefix(string(body), ")]}'") txt = strings.TrimSpace(txt) var data struct { XsrfToken string `json:"xsrfToken"` KeyID string `json:"keyId"` } if err := json.Unmarshal([]byte(txt), &data); err != nil { return fmt.Errorf("解析 xsrf 响应失败: %w", err) } token := data.XsrfToken switch len(token) % 4 { case 2: token += "==" case 3: token += "=" } keyBytes, err := base64.URLEncoding.DecodeString(token) if err != nil { return fmt.Errorf("解码 xsrfToken 失败: %w", err) } acc.JWT = createJWT(keyBytes, data.KeyID, acc.CSESIDX) acc.JWTExpires = time.Now().Add(JwtTTL) acc.LastRefresh = time.Now() if acc.ConfigID == "" { configID, err := acc.fetchConfigID() if err != nil { return fmt.Errorf("获取 configId 失败: %w", err) } acc.ConfigID = configID } return nil } // GetJWT 获取JWT func (acc *Account) GetJWT() (string, string, error) { acc.Mu.Lock() defer acc.Mu.Unlock() if acc.JWT == "" { return "", "", fmt.Errorf("JWT 为空,账号未刷新") } return acc.JWT, acc.ConfigID, nil } func (acc *Account) fetchConfigID() (string, error) { if acc.Data.ConfigID != "" { return acc.Data.ConfigID, nil } if DefaultConfig != "" { return DefaultConfig, nil } return "", fmt.Errorf("未配置 configId") } ================================================ FILE: src/pool/pool_client.go ================================================ package pool import ( "bytes" "encoding/json" "fmt" "log" "net/http" "net/url" "sync" "time" "business2api/src/logger" "github.com/gorilla/websocket" ) type BrowserRegisterResult struct { Success bool Email string FullName string SecureCookies []Cookie Authorization string ConfigID string CSESIDX string Error error } // RunBrowserRegisterFunc 注册函数类型 type RunBrowserRegisterFunc func(headless bool, proxy string, id int) *BrowserRegisterResult // 客户端版本 const ClientVersion = "2.0.0" var ( RunBrowserRegister RunBrowserRegisterFunc ClientHeadless bool ClientProxy string GetClientProxy func() string // 获取代理的函数 ReleaseProxy func(proxyURL string) // 释放代理的函数 DefaultProxyCount = 3 // 客户端模式默认启动的代理实例数 IsProxyReady func() bool // 检查代理是否就绪 WaitProxyReady func(timeout time.Duration) bool // 等待代理就绪 GetHealthyCount func() int // 获取健康代理数量 proxyReadyTimeout = 30 * time.Second // 代理就绪超时时间(减少等待) ) // PoolClient 号池客户端 type PoolClient struct { config PoolServerConfig conn *websocket.Conn send chan []byte done chan struct{} reconnect chan struct{} stopPump chan struct{} // 停止当前pump mu sync.Mutex writeMu sync.Mutex // WebSocket写入锁 isRunning bool taskSem chan struct{} // 任务并发信号量 } // NewPoolClient 创建号池客户端 func NewPoolClient(config PoolServerConfig) *PoolClient { threads := config.ClientThreads if threads <= 0 { threads = 1 } return &PoolClient{ config: config, send: make(chan []byte, 256), done: make(chan struct{}), reconnect: make(chan struct{}, 1), taskSem: make(chan struct{}, threads), } } // Start 启动客户端 func (pc *PoolClient) Start() error { pc.mu.Lock() pc.isRunning = true pc.mu.Unlock() // 连接循环 for pc.isRunning { if err := pc.connect(); err != nil { logger.Warn("连接服务器失败: %v, 5秒后重试...", err) time.Sleep(5 * time.Second) continue } pc.work() select { case <-pc.done: return nil case <-pc.reconnect: log.Printf("[PoolClient] 准备重连...") time.Sleep(2 * time.Second) } } return nil } func (pc *PoolClient) Stop() { pc.mu.Lock() pc.isRunning = false pc.mu.Unlock() close(pc.done) } // connect 连接到服务器 func (pc *PoolClient) connect() error { u, err := url.Parse(pc.config.ServerAddr) if err != nil { return fmt.Errorf("解析服务器地址失败: %w", err) } wsScheme := "ws" if u.Scheme == "https" { wsScheme = "wss" } wsURL := fmt.Sprintf("%s://%s/ws", wsScheme, u.Host) if pc.config.Secret != "" { wsURL += "?secret=" + pc.config.Secret } logger.Debug("连接到 %s", wsURL) conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil) if err != nil { return fmt.Errorf("WebSocket连接失败: %w", err) } pc.conn = conn threads := pc.config.ClientThreads if threads <= 0 { threads = 1 } pc.sendMessage(WSMessage{ Type: WSMsgClientReady, Version: ClientVersion, Timestamp: time.Now().Unix(), Data: map[string]interface{}{ "max_threads": threads, "client_version": ClientVersion, "protocol_version": ProtocolVersion, }, }) return nil } func (pc *PoolClient) work() { pc.stopPump = make(chan struct{}) go pc.writePump() // 消息发送 go pc.heartbeatPump() // 独立心跳保活 pc.readPump() // 消息读取(阻塞) close(pc.stopPump) } func (pc *PoolClient) heartbeatPump() { ticker := time.NewTicker(15 * time.Second) defer ticker.Stop() for { select { case <-pc.done: return case <-pc.stopPump: return case <-ticker.C: // 发送心跳保持连接活跃 pc.writeMu.Lock() pc.conn.SetWriteDeadline(time.Now().Add(10 * time.Second)) err := pc.conn.WriteMessage(websocket.PingMessage, nil) pc.writeMu.Unlock() if err != nil { logger.Debug("[PoolClient] 心跳发送失败: %v", err) return } } } } func (pc *PoolClient) writePump() { registerTicker := time.NewTicker(30 * time.Minute) defer registerTicker.Stop() go pc.doPeriodicRegister() for { select { case <-pc.done: return case <-pc.stopPump: return case message := <-pc.send: pc.writeMu.Lock() pc.conn.SetWriteDeadline(time.Now().Add(30 * time.Second)) err := pc.conn.WriteMessage(websocket.TextMessage, message) pc.writeMu.Unlock() if err != nil { log.Printf("[PoolClient] 发送消息失败: %v", err) pc.triggerReconnect() return } case <-registerTicker.C: // 每30分钟注册一轮 go pc.doPeriodicRegister() } } } func (pc *PoolClient) doPeriodicRegister() { maxThreads := pc.config.ClientThreads if maxThreads <= 0 { maxThreads = 1 } logger.Info("[定时注册] 开始注册 %d 个账号", maxThreads) for i := 0; i < maxThreads; i++ { go pc.handleRegisterTask(map[string]interface{}{"count": 1}) } } // readPump 读取消息 func (pc *PoolClient) readPump() { defer func() { pc.conn.Close() pc.triggerReconnect() }() // 延长读取超时到240秒(4分钟),确保不会因为任务执行而断开 pc.conn.SetReadDeadline(time.Now().Add(240 * time.Second)) pc.conn.SetPongHandler(func(string) error { pc.conn.SetReadDeadline(time.Now().Add(240 * time.Second)) return nil }) for { _, message, err := pc.conn.ReadMessage() if err != nil { if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { log.Printf("[PoolClient] 读取错误: %v", err) } return } // 收到消息时重置读取超时 pc.conn.SetReadDeadline(time.Now().Add(240 * time.Second)) var msg WSMessage if err := json.Unmarshal(message, &msg); err != nil { continue } pc.handleMessage(msg) } } // handleMessage 处理服务器消息 func (pc *PoolClient) handleMessage(msg WSMessage) { switch msg.Type { case WSMsgHeartbeat: // 立即响应心跳 pc.sendMessage(WSMessage{ Type: WSMsgHeartbeatAck, Timestamp: time.Now().Unix(), }) case WSMsgTaskRegister: // 注册任务(独立心跳线程已保活,无需额外处理) go pc.handleRegisterTask(msg.Data) case WSMsgTaskRefresh: // 续期任务 go pc.handleRefreshTask(msg.Data) case WSMsgStatus: // 状态同步,自主判断是否需要注册 go pc.handleStatusAndRegister(msg.Data) } } // handleRegisterTask 处理注册任务(每次只处理1个) func (pc *PoolClient) handleRegisterTask(data map[string]interface{}) { // 获取信号量(控制并发数) pc.taskSem <- struct{}{} defer func() { <-pc.taskSem }() taskID := time.Now().UnixNano() % 1000 resultSent := false defer func() { if !resultSent { if r := recover(); r != nil { logger.Error("[注册 %d] handleRegisterTask panic: %v", taskID, r) pc.sendRegisterResult(false, "", fmt.Sprintf("client panic: %v", r)) } else { pc.sendRegisterResult(false, "", "task incomplete") } } }() logger.Info("收到注册任务 [%d]", taskID) if GetHealthyCount != nil && GetHealthyCount() >= 1 { // 已有健康代理,直接开始 } else if WaitProxyReady != nil { if !WaitProxyReady(proxyReadyTimeout) { logger.Warn("代理未就绪,使用静态代理: %s", ClientProxy) } } // 获取代理(优先使用代理池) currentProxy := ClientProxy if GetClientProxy != nil { currentProxy = GetClientProxy() } logger.Info("[注册 %d] 使用代理: %s", taskID, currentProxy) result := RunBrowserRegister(ClientHeadless, currentProxy, int(taskID)) // 任务完成后释放代理 if ReleaseProxy != nil && currentProxy != "" && currentProxy != ClientProxy { ReleaseProxy(currentProxy) } if result.Success { // 上传账号到服务器 if err := pc.uploadAccount(result, true); err != nil { logger.Error("上传注册结果失败: %v", err) pc.sendRegisterResult(false, "", err.Error()) } else { logger.Info("✅ 注册成功: %s", result.Email) pc.sendRegisterResult(true, result.Email, "") } } else { errMsg := "未知错误" if result.Error != nil { errMsg = result.Error.Error() } logger.Warn("❌ 注册失败: %s", errMsg) pc.sendRegisterResult(false, "", errMsg) } resultSent = true } func (pc *PoolClient) handleStatusAndRegister(data map[string]interface{}) { needCount := 0 if v, ok := data["need_count"].(float64); ok { needCount = int(v) } currentCount := 0 if v, ok := data["current_count"].(float64); ok { currentCount = int(v) } targetCount := 0 if v, ok := data["target_count"].(float64); ok { targetCount = int(v) } if needCount <= 0 { logger.Debug("[自主] 无需注册 (当前: %d, 目标: %d)", currentCount, targetCount) return } // 计算本客户端应该注册的数量(不超过线程数和需要数量) maxThreads := pc.config.ClientThreads if maxThreads <= 0 { maxThreads = 1 } registerCount := needCount if registerCount > maxThreads { registerCount = maxThreads } logger.Info("[自主] 需要注册 %d 个账号 (当前: %d, 目标: %d, 本次: %d)", needCount, currentCount, targetCount, registerCount) // 启动注册任务 for i := 0; i < registerCount; i++ { go pc.handleRegisterTask(map[string]interface{}{"count": 1}) } } // handleRefreshTask 处理续期任务 func (pc *PoolClient) handleRefreshTask(data map[string]interface{}) { // 获取信号量 pc.taskSem <- struct{}{} defer func() { <-pc.taskSem }() email, _ := data["email"].(string) if email == "" { logger.Warn("续期任务缺少email") return } logger.Info("收到续期任务: %s", email) // 检查代理:如果已有健康代理则不等待 if GetHealthyCount != nil && GetHealthyCount() >= 1 { // 已有健康代理,直接开始 } else if WaitProxyReady != nil { if !WaitProxyReady(proxyReadyTimeout) { logger.Warn("代理未就绪,使用静态代理: %s", Proxy) } } // 构建临时账号对象 acc := &Account{ Data: AccountData{ Email: email, }, } // 从data中提取cookies if cookiesData, ok := data["cookies"].([]interface{}); ok { for _, c := range cookiesData { if cm, ok := c.(map[string]interface{}); ok { acc.Data.Cookies = append(acc.Data.Cookies, Cookie{ Name: getString(cm, "name"), Value: getString(cm, "value"), Domain: getString(cm, "domain"), }) } } } if auth, ok := data["authorization"].(string); ok { acc.Data.Authorization = auth } if configID, ok := data["config_id"].(string); ok { acc.ConfigID = configID } if csesidx, ok := data["csesidx"].(string); ok { acc.CSESIDX = csesidx } // 获取代理(优先使用代理池) currentProxy := Proxy if GetClientProxy != nil { currentProxy = GetClientProxy() } // 执行浏览器刷新 result := RefreshCookieWithBrowser(acc, BrowserRefreshHeadless, currentProxy) // 任务完成后释放代理 if ReleaseProxy != nil && currentProxy != "" && currentProxy != Proxy { ReleaseProxy(currentProxy) } if result.Success { logger.Info("✅ 账号续期成功: %s", email) // 使用刷新后的新值(如果有的话) authorization := acc.Data.Authorization if result.Authorization != "" { authorization = result.Authorization } configID := acc.ConfigID if result.ConfigID != "" { configID = result.ConfigID } csesidx := acc.CSESIDX if result.CSESIDX != "" { csesidx = result.CSESIDX } // 上传更新后的账号数据到服务器 uploadReq := &AccountUploadRequest{ Email: email, Cookies: result.SecureCookies, Authorization: authorization, ConfigID: configID, CSESIDX: csesidx, IsNew: false, } logger.Info("[%s] 上传续期数据: configID=%s, csesidx=%s, auth长度=%d", email, configID, csesidx, len(authorization)) if err := pc.uploadAccountData(uploadReq); err != nil { logger.Warn("上传续期数据失败: %v", err) } pc.sendRefreshResult(email, true, result.SecureCookies, "") } else { errMsg := "未知错误" if result.Error != nil { errMsg = result.Error.Error() } logger.Warn("❌ 账号续期失败 %s: %s", email, errMsg) pc.sendRefreshResult(email, false, nil, errMsg) } } // sendMessage 发送消息 func (pc *PoolClient) sendMessage(msg WSMessage) { data, err := json.Marshal(msg) if err != nil { return } select { case pc.send <- data: default: logger.Warn("发送队列已满") } } // uploadAccount 上传注册结果到服务器 func (pc *PoolClient) uploadAccount(result *BrowserRegisterResult, isNew bool) error { // 构建cookie字符串 var cookieStr string for i, c := range result.SecureCookies { if i > 0 { cookieStr += "; " } cookieStr += c.Name + "=" + c.Value } req := &AccountUploadRequest{ Email: result.Email, FullName: result.FullName, Cookies: result.SecureCookies, CookieString: cookieStr, Authorization: result.Authorization, ConfigID: result.ConfigID, CSESIDX: result.CSESIDX, IsNew: isNew, } return pc.uploadAccountData(req) } // uploadAccountData 上传账号数据到服务器(带重试) func (pc *PoolClient) uploadAccountData(req *AccountUploadRequest) error { u, err := url.Parse(pc.config.ServerAddr) if err != nil { return err } uploadURL := fmt.Sprintf("%s://%s/pool/upload-account", u.Scheme, u.Host) data, err := json.Marshal(req) if err != nil { return err } maxRetries := 3 var lastErr error for i := 0; i < maxRetries; i++ { if i > 0 { logger.Info("[%s] 上传重试 %d/%d...", req.Email, i+1, maxRetries) time.Sleep(time.Duration(i*2) * time.Second) } httpReq, err := http.NewRequest("POST", uploadURL, bytes.NewReader(data)) if err != nil { lastErr = err continue } httpReq.Header.Set("Content-Type", "application/json") if pc.config.Secret != "" { httpReq.Header.Set("X-Pool-Secret", pc.config.Secret) } client := &http.Client{Timeout: 60 * time.Second} resp, err := client.Do(httpReq) if err != nil { lastErr = err continue } var result map[string]interface{} if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { resp.Body.Close() lastErr = err continue } resp.Body.Close() if success, ok := result["success"].(bool); !ok || !success { errMsg, _ := result["error"].(string) lastErr = fmt.Errorf("上传失败: %s", errMsg) continue } logger.Debug("账号数据已上传: %s", req.Email) return nil } return fmt.Errorf("上传失败(重试%d次): %v", maxRetries, lastErr) } // sendRegisterResult 发送注册结果 func (pc *PoolClient) sendRegisterResult(success bool, email, errMsg string) { pc.sendMessage(WSMessage{ Type: WSMsgRegisterResult, Timestamp: time.Now().Unix(), Data: map[string]interface{}{ "success": success, "email": email, "error": errMsg, }, }) } // sendRefreshResult 发送续期结果 func (pc *PoolClient) sendRefreshResult(email string, success bool, cookies []Cookie, errMsg string) { pc.sendMessage(WSMessage{ Type: WSMsgRefreshResult, Timestamp: time.Now().Unix(), Data: map[string]interface{}{ "email": email, "success": success, "cookies": cookies, "error": errMsg, }, }) } // triggerReconnect 触发重连 func (pc *PoolClient) triggerReconnect() { select { case pc.reconnect <- struct{}{}: default: } } // getString 安全获取字符串 func getString(m map[string]interface{}, key string) string { if v, ok := m[key].(string); ok { return v } return "" } ================================================ FILE: src/pool/pool_server.go ================================================ package pool import ( "bytes" "encoding/json" "fmt" "io" "net/http" "os" "path/filepath" "sync" "sync/atomic" "time" "business2api/src/logger" "github.com/gorilla/websocket" ) // PoolServerConfig 号池服务器配置 type PoolServerConfig struct { Enable bool `json:"enable"` // 是否启用分离模式 Mode string `json:"mode"` // 模式: "server" 或 "client" ServerAddr string `json:"server_addr"` // 服务器地址(客户端模式使用) ListenAddr string `json:"listen_addr"` // WebSocket监听地址(服务端模式使用) Secret string `json:"secret"` // 通信密钥 TargetCount int `json:"target_count"` // 目标账号数量 DataDir string `json:"data_dir"` // 数据目录 ClientThreads int `json:"client_threads"` // 客户端并发线程数 ExpiredAction string `json:"expired_action"` // 账号过期处理: "delete"=删除, "refresh"=浏览器刷新, "queue"=排队等待 } // WSMessageType WebSocket消息类型 type WSMessageType string const ( // Server -> Client WSMsgTaskRegister WSMessageType = "task_register" // 分配注册任务 WSMsgTaskRefresh WSMessageType = "task_refresh" // 分配Cookie续期任务 WSMsgHeartbeat WSMessageType = "heartbeat" // 心跳 WSMsgStatus WSMessageType = "status" // 状态同步 // Client -> Server WSMsgRegisterResult WSMessageType = "register_result" // 注册结果 WSMsgRefreshResult WSMessageType = "refresh_result" // 续期结果 WSMsgHeartbeatAck WSMessageType = "heartbeat_ack" // 心跳响应 WSMsgClientReady WSMessageType = "client_ready" // 客户端就绪 WSMsgRequestTask WSMessageType = "request_task" // 请求任务 WSMsgQueryStatus WSMessageType = "query_status" // 查询状态(客户端自主模式) ) // 版本信息 const ( ProtocolVersion = "1.0" ServerVersion = "4.0.0" ) // WSMessage WebSocket消息 type WSMessage struct { Type WSMessageType `json:"type"` Version string `json:"version,omitempty"` Timestamp int64 `json:"timestamp"` Data map[string]interface{} `json:"data,omitempty"` } // WSClient WebSocket客户端连接 type WSClient struct { ID string Conn *websocket.Conn Server *PoolServer Send chan []byte IsAlive bool LastPing time.Time MaxThreads int // 客户端最大线程数 ClientVersion string // 客户端版本 mu sync.Mutex } // PoolServer 号池服务器(管理端) type PoolServer struct { pool *AccountPool config PoolServerConfig clients map[string]*WSClient clientsMu sync.RWMutex upgrader websocket.Upgrader // 任务队列 registerQueue chan int // 注册任务队列 refreshQueue chan *Account // 续期任务队列 // 轮询分配 nextClientIdx int // 下一个分配任务的客户端索引 // 正在进行中的注册任务计数 pendingRegisterCount int32 } // NewPoolServer 创建号池服务器 func NewPoolServer(pool *AccountPool, config PoolServerConfig) *PoolServer { return &PoolServer{ pool: pool, config: config, clients: make(map[string]*WSClient), upgrader: websocket.Upgrader{ CheckOrigin: func(r *http.Request) bool { return true }, ReadBufferSize: 1024, WriteBufferSize: 1024, }, registerQueue: make(chan int, 100), refreshQueue: make(chan *Account, 100), } } // Start 启动号池服务器(独立端口模式,已弃用) func (ps *PoolServer) Start() error { mux := http.NewServeMux() // 鉴权中间件 authMiddleware := func(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if ps.config.Secret != "" { auth := r.Header.Get("X-Pool-Secret") if auth != ps.config.Secret { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } } next(w, r) } } // WebSocket端点 mux.HandleFunc("/ws", ps.handleWebSocket) // REST API端点 mux.HandleFunc("/pool/next", authMiddleware(ps.handleNext)) mux.HandleFunc("/pool/mark", authMiddleware(ps.handleMark)) mux.HandleFunc("/pool/refresh", authMiddleware(ps.handleRefresh)) mux.HandleFunc("/pool/status", authMiddleware(ps.handleStatus)) mux.HandleFunc("/pool/jwt", authMiddleware(ps.handleGetJWT)) // 任务分发 mux.HandleFunc("/pool/queue-register", authMiddleware(ps.handleQueueRegister)) mux.HandleFunc("/pool/queue-refresh", authMiddleware(ps.handleQueueRefresh)) // 接收账号数据(客户端回传) mux.HandleFunc("/pool/upload-account", authMiddleware(ps.handleUploadAccount)) // 启动任务分发协程 go ps.taskDispatcher() // 启动心跳检测 go ps.heartbeatChecker() // 启动定时任务广播(每半小时) go ps.periodicTaskBroadcaster() // 启动号池维护(缺账号就下发) go ps.poolMaintainer() return http.ListenAndServe(ps.config.ListenAddr, mux) } // StartBackground 启动后台任务(任务分发、心跳检测、定时广播、账号扫描) func (ps *PoolServer) StartBackground() { go ps.taskDispatcher() go ps.heartbeatChecker() go ps.periodicTaskBroadcaster() go ps.periodicAccountScanner() go ps.poolMaintainer() } // HandleWS 处理 WebSocket 连接(供 gin 路由使用) func (ps *PoolServer) HandleWS(w http.ResponseWriter, r *http.Request) { ps.handleWebSocket(w, r) } func (ps *PoolServer) HandleUploadAccount(w http.ResponseWriter, r *http.Request) { ps.handleUploadAccount(w, r) } func (ps *PoolServer) handleWebSocket(w http.ResponseWriter, r *http.Request) { if ps.config.Secret != "" { secret := r.URL.Query().Get("secret") if secret != ps.config.Secret { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } } conn, err := ps.upgrader.Upgrade(w, r, nil) if err != nil { return } clientID := fmt.Sprintf("client_%d", time.Now().UnixNano()) client := &WSClient{ ID: clientID, Conn: conn, Server: ps, Send: make(chan []byte, 256), IsAlive: true, LastPing: time.Now(), } ps.clientsMu.Lock() ps.clients[clientID] = client ps.clientsMu.Unlock() logger.Info("[WS] 客户端连接: %s (当前: %d)", clientID, len(ps.clients)) // 启动读写协程 go client.writePump() go client.readPump() } // writePump 发送消息到客户端 func (c *WSClient) writePump() { // 缩短心跳间隔到20秒,确保连接保持活跃 ticker := time.NewTicker(20 * time.Second) defer func() { ticker.Stop() c.Conn.Close() }() for { select { case message, ok := <-c.Send: if !ok { c.Conn.WriteMessage(websocket.CloseMessage, []byte{}) return } // 设置写入超时 c.Conn.SetWriteDeadline(time.Now().Add(30 * time.Second)) if err := c.Conn.WriteMessage(websocket.TextMessage, message); err != nil { return } case <-ticker.C: // 发送心跳 msg := WSMessage{ Type: WSMsgHeartbeat, Timestamp: time.Now().Unix(), } data, _ := json.Marshal(msg) c.Conn.SetWriteDeadline(time.Now().Add(30 * time.Second)) if err := c.Conn.WriteMessage(websocket.TextMessage, data); err != nil { logger.Debug("[WS] 发送心跳失败: %s - %v", c.ID, err) return } } } } // readPump 从客户端读取消息 func (c *WSClient) readPump() { defer func() { c.Server.removeClient(c.ID) c.Conn.Close() }() // 延长读取超时到180秒(3分钟),以适应长时间注册任务 c.Conn.SetReadDeadline(time.Now().Add(180 * time.Second)) c.Conn.SetPongHandler(func(string) error { c.Conn.SetReadDeadline(time.Now().Add(180 * time.Second)) return nil }) // 处理客户端的 Ping 消息,自动回复 Pong 并重置超时 c.Conn.SetPingHandler(func(appData string) error { c.Conn.SetReadDeadline(time.Now().Add(180 * time.Second)) c.mu.Lock() c.LastPing = time.Now() c.mu.Unlock() return c.Conn.WriteControl(websocket.PongMessage, []byte(appData), time.Now().Add(10*time.Second)) }) for { _, message, err := c.Conn.ReadMessage() if err != nil { if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { logger.Debug("[WS] 读取错误: %v", err) } break } var msg WSMessage if err := json.Unmarshal(message, &msg); err != nil { continue } c.handleMessage(msg) } } // handleMessage 处理客户端消息 func (c *WSClient) handleMessage(msg WSMessage) { c.mu.Lock() c.LastPing = time.Now() c.mu.Unlock() // 收到任何消息都重置读取超时 c.Conn.SetReadDeadline(time.Now().Add(180 * time.Second)) switch msg.Type { case WSMsgHeartbeatAck: logger.Debug("[WS] 收到心跳响应: %s", c.ID) case WSMsgClientReady: if threads, ok := msg.Data["max_threads"].(float64); ok && threads > 0 { c.MaxThreads = int(threads) } else { c.MaxThreads = 1 } if ver, ok := msg.Data["client_version"].(string); ok { c.ClientVersion = ver } logger.Info("[WS] 客户端 %s 就绪 (v%s, 线程:%d)", c.ID, c.ClientVersion, c.MaxThreads) c.Server.assignTask(c) case WSMsgRequestTask: logger.Debug("[WS] 客户端 %s 请求任务", c.ID) c.Server.assignTask(c) case WSMsgRegisterResult: // 注册结果 c.Server.handleRegisterResult(msg.Data) case WSMsgRefreshResult: // 续期结果 c.Server.handleRefreshResult(msg.Data) case WSMsgQueryStatus: // 客户端查询状态(自主模式) c.Server.sendStatusTo(c) } } func (ps *PoolServer) removeClient(clientID string) { ps.clientsMu.Lock() defer ps.clientsMu.Unlock() if client, ok := ps.clients[clientID]; ok { close(client.Send) delete(ps.clients, clientID) logger.Info("[WS] 客户端断开: %s (剩余: %d)", clientID, len(ps.clients)) } } func (ps *PoolServer) taskDispatcher() { for { select { case count := <-ps.registerQueue: // 检查是否还需要注册 currentCount := ps.pool.TotalCount() pendingCount := int(atomic.LoadInt32(&ps.pendingRegisterCount)) needCount := ps.config.TargetCount - currentCount - pendingCount if needCount <= 0 { logger.Debug("[分配] 已达目标数量,跳过注册任务 (当前: %d, 进行中: %d, 目标: %d)", currentCount, pendingCount, ps.config.TargetCount) continue } // 分发注册任务(轮询分配) if ps.assignTaskRoundRobin(WSMsgTaskRegister, map[string]interface{}{ "count": count, }) { atomic.AddInt32(&ps.pendingRegisterCount, 1) } case acc := <-ps.refreshQueue: // 分发续期任务(轮询分配) ps.assignTaskRoundRobin(WSMsgTaskRefresh, map[string]interface{}{ "email": acc.Data.Email, "cookies": acc.Data.Cookies, "authorization": acc.Data.Authorization, "config_id": acc.ConfigID, "csesidx": acc.CSESIDX, }) } } } // assignTaskRoundRobin 轮询分配任务给单个客户端 func (ps *PoolServer) assignTaskRoundRobin(msgType WSMessageType, data map[string]interface{}) bool { msg := WSMessage{ Type: msgType, Timestamp: time.Now().Unix(), Data: data, } msgBytes, _ := json.Marshal(msg) ps.clientsMu.Lock() defer ps.clientsMu.Unlock() if len(ps.clients) == 0 { return false } // 获取客户端列表 clientList := make([]*WSClient, 0, len(ps.clients)) for _, client := range ps.clients { if client.IsAlive { clientList = append(clientList, client) } } if len(clientList) == 0 { return false } // 轮询分配 ps.nextClientIdx = ps.nextClientIdx % len(clientList) client := clientList[ps.nextClientIdx] ps.nextClientIdx++ select { case client.Send <- msgBytes: logger.Info("[分配] 任务 %s 分配给 %s", msgType, client.ID) return true default: // 发送队列满,尝试下一个 for i := 0; i < len(clientList)-1; i++ { ps.nextClientIdx = ps.nextClientIdx % len(clientList) client = clientList[ps.nextClientIdx] ps.nextClientIdx++ select { case client.Send <- msgBytes: logger.Info("[分配] 任务 %s 分配给 %s", msgType, client.ID) return true default: continue } } } return false } func (ps *PoolServer) assignTask(client *WSClient) { maxThreads := client.MaxThreads if maxThreads <= 0 { maxThreads = 1 } assignedCount := 0 if AutoDelete401 { ps.pool.mu.Lock() var toDelete []*Account var remaining []*Account for _, acc := range ps.pool.pendingAccounts { if !acc.Refreshed && acc.FailCount > 0 { // 401账号,标记删除 toDelete = append(toDelete, acc) } else { remaining = append(remaining, acc) } } ps.pool.pendingAccounts = remaining ps.pool.mu.Unlock() // 删除401账号文件 for _, acc := range toDelete { logger.Info("🗑️ [服务端] 401自动删除账号: %s", acc.Data.Email) ps.pool.RemoveAccount(acc) } } else { // 未配置自动删除,分配续期任务给节点 // 计算401最大重试次数 maxRetry := MaxFailCount * 3 if maxRetry < 10 { maxRetry = 10 } ps.pool.mu.RLock() var refreshAccounts []*Account for _, acc := range ps.pool.pendingAccounts { if !acc.Refreshed && acc.FailCount > 0 { // 跳过已达上限的账号(浏览器刷新已达上限且401失败次数超过阈值) if acc.BrowserRefreshCount >= BrowserRefreshMaxRetry && acc.FailCount >= maxRetry { continue } refreshAccounts = append(refreshAccounts, acc) if len(refreshAccounts) >= maxThreads { break } } } ps.pool.mu.RUnlock() for _, acc := range refreshAccounts { logger.Info("[WS] 分配续期任务给 %s: %s", client.ID, acc.Data.Email) msg := WSMessage{ Type: WSMsgTaskRefresh, Timestamp: time.Now().Unix(), Data: map[string]interface{}{ "email": acc.Data.Email, "cookies": acc.Data.Cookies, "authorization": acc.Data.Authorization, "config_id": acc.ConfigID, "csesidx": acc.CSESIDX, }, } msgBytes, _ := json.Marshal(msg) select { case client.Send <- msgBytes: assignedCount++ default: } } } remainingSlots := maxThreads - assignedCount if remainingSlots > 0 { currentCount := ps.pool.TotalCount() pendingCount := int(atomic.LoadInt32(&ps.pendingRegisterCount)) targetCount := ps.config.TargetCount // 计算需要注册的数量时,考虑正在进行中的任务 needCount := targetCount - currentCount - pendingCount if needCount > 0 { registerCount := remainingSlots if registerCount > needCount { registerCount = needCount } logger.Info("[WS] 分配注册任务给 %s: %d个 (当前: %d, 进行中: %d, 目标: %d, 线程: %d)", client.ID, registerCount, currentCount, pendingCount, targetCount, maxThreads) for i := 0; i < registerCount; i++ { msg := WSMessage{ Type: WSMsgTaskRegister, Timestamp: time.Now().Unix(), Data: map[string]interface{}{ "count": 1, }, } msgBytes, _ := json.Marshal(msg) select { case client.Send <- msgBytes: assignedCount++ atomic.AddInt32(&ps.pendingRegisterCount, 1) // 增加进行中计数 default: } } } } if assignedCount == 0 { currentCount := ps.pool.TotalCount() pendingCount := int(atomic.LoadInt32(&ps.pendingRegisterCount)) targetCount := ps.config.TargetCount logger.Info("[WS] 无任务需要分配给 %s (当前: %d, 进行中: %d, 目标: %d)", client.ID, currentCount, pendingCount, targetCount) } } // heartbeatChecker 心跳检测 func (ps *PoolServer) heartbeatChecker() { ticker := time.NewTicker(60 * time.Second) defer ticker.Stop() for range ticker.C { ps.clientsMu.RLock() aliveClients := 0 totalThreads := 0 for id, client := range ps.clients { client.mu.Lock() if time.Since(client.LastPing) > 180*time.Second { client.IsAlive = false logger.Warn("[WS] 客户端 %s 心跳超时 (last: %v ago)", id, time.Since(client.LastPing)) } else if client.IsAlive { aliveClients++ totalThreads += client.MaxThreads } client.mu.Unlock() } ps.clientsMu.RUnlock() pendingCount := atomic.LoadInt32(&ps.pendingRegisterCount) if aliveClients == 0 { if pendingCount > 0 { atomic.StoreInt32(&ps.pendingRegisterCount, 0) logger.Info("[心跳] 无活跃客户端,重置 pendingRegisterCount: %d -> 0", pendingCount) } } else { maxReasonable := int32(totalThreads * 2) if maxReasonable < 10 { maxReasonable = 10 } if pendingCount > maxReasonable { atomic.StoreInt32(&ps.pendingRegisterCount, 0) logger.Warn("[心跳] pendingRegisterCount 异常 (%d > %d),已重置", pendingCount, maxReasonable) } else if pendingCount > 0 { // 每分钟衰减:pending 任务不应该长期存在,逐步减少 newCount := pendingCount - int32(aliveClients) if newCount < 0 { newCount = 0 } if newCount != pendingCount { atomic.StoreInt32(&ps.pendingRegisterCount, newCount) logger.Debug("[心跳] pendingRegisterCount 衰减: %d -> %d", pendingCount, newCount) } } } } } func (ps *PoolServer) periodicTaskBroadcaster() { // 启动时5秒后立即执行一次 time.AfterFunc(5*time.Second, func() { ps.broadcastRegisterTasks() }) // 广播保持30分钟一次 ticker := time.NewTicker(30 * time.Minute) defer ticker.Stop() for range ticker.C { ps.broadcastRegisterTasks() } } // poolMaintainer 号池维护:持续检测缺账号就下发 func (ps *PoolServer) poolMaintainer() { ticker := time.NewTicker(30 * time.Second) defer ticker.Stop() for range ticker.C { currentCount := ps.pool.TotalCount() pendingCount := int(atomic.LoadInt32(&ps.pendingRegisterCount)) targetCount := ps.config.TargetCount needCount := targetCount - currentCount - pendingCount if needCount <= 0 { continue } // 缺账号,向所有活跃客户端下发任务 ps.clientsMu.RLock() for _, client := range ps.clients { if !client.IsAlive { continue } // 每个客户端分配其线程数的任务 assignCount := client.MaxThreads if assignCount <= 0 { assignCount = 1 } if assignCount > needCount { assignCount = needCount } logger.Info("[号池维护] 向 %s 下发 %d 个注册任务 (当前: %d, 进行中: %d, 目标: %d)", client.ID, assignCount, currentCount, pendingCount, targetCount) for i := 0; i < assignCount; i++ { msg := WSMessage{ Type: WSMsgTaskRegister, Timestamp: time.Now().Unix(), Data: map[string]interface{}{"count": 1}, } msgBytes, _ := json.Marshal(msg) select { case client.Send <- msgBytes: atomic.AddInt32(&ps.pendingRegisterCount, 1) needCount-- default: } } if needCount <= 0 { break } } ps.clientsMu.RUnlock() } } func (ps *PoolServer) periodicAccountScanner() { ticker := time.NewTicker(5 * time.Minute) defer ticker.Stop() for range ticker.C { dataDir := ps.config.DataDir if dataDir == "" { dataDir = DataDir } oldCount := ps.pool.TotalCount() if err := ps.pool.Load(dataDir); err != nil { logger.Warn("[扫描] 加载账号失败: %v", err) } else { newCount := ps.pool.TotalCount() if newCount != oldCount { logger.Info("[扫描] 账号数量变化: %d -> %d", oldCount, newCount) } } } } func (ps *PoolServer) broadcastRegisterTasks() { currentCount := ps.pool.TotalCount() pendingCount := int(atomic.LoadInt32(&ps.pendingRegisterCount)) targetCount := ps.config.TargetCount needCount := targetCount - currentCount - pendingCount logger.Info("[广播] 检查注册任务 (当前: %d, 进行中: %d, 目标: %d, 需要: %d)", currentCount, pendingCount, targetCount, needCount) // 检查是否需要注册 if needCount <= 0 { logger.Info("[广播] 已达目标数量,跳过任务广播") return } ps.clientsMu.RLock() defer ps.clientsMu.RUnlock() if len(ps.clients) == 0 { logger.Warn("[广播] 无在线客户端,跳过任务广播") return } totalAssigned := 0 for _, client := range ps.clients { if !client.IsAlive { continue } // 检查剩余需要的数量 remainingNeed := needCount - totalAssigned if remainingNeed <= 0 { break } // 每个节点分配其线程数量的任务,但不超过剩余需要的数量 assignCount := client.MaxThreads if assignCount <= 0 { assignCount = 1 } if assignCount > remainingNeed { assignCount = remainingNeed } logger.Info("[广播] 向 %s 分配 %d 个注册任务 (线程: %d)", client.ID, assignCount, client.MaxThreads) for i := 0; i < assignCount; i++ { msg := WSMessage{ Type: WSMsgTaskRegister, Timestamp: time.Now().Unix(), Data: map[string]interface{}{ "count": 1, }, } msgBytes, _ := json.Marshal(msg) select { case client.Send <- msgBytes: atomic.AddInt32(&ps.pendingRegisterCount, 1) totalAssigned++ default: logger.Warn("[广播] 客户端 %s 发送队列满", client.ID) } } } if totalAssigned > 0 { logger.Info("[广播] 本轮共分配 %d 个注册任务给 %d 个节点", totalAssigned, len(ps.clients)) } } func (ps *PoolServer) sendStatusTo(client *WSClient) { currentCount := ps.pool.TotalCount() pendingCount := int(atomic.LoadInt32(&ps.pendingRegisterCount)) targetCount := ps.config.TargetCount needCount := targetCount - currentCount - pendingCount msg := WSMessage{ Type: WSMsgStatus, Timestamp: time.Now().Unix(), Data: map[string]interface{}{ "current_count": currentCount, "pending_count": pendingCount, "target_count": targetCount, "need_count": needCount, }, } msgBytes, _ := json.Marshal(msg) select { case client.Send <- msgBytes: logger.Debug("[WS] 发送状态给 %s: current=%d, target=%d, need=%d", client.ID, currentCount, targetCount, needCount) default: logger.Warn("[WS] 发送状态失败,队列满: %s", client.ID) } } func (ps *PoolServer) handleRegisterResult(data map[string]interface{}) { // 减少进行中计数 atomic.AddInt32(&ps.pendingRegisterCount, -1) success, _ := data["success"].(bool) email, _ := data["email"].(string) if success { logger.Info("✅ 注册成功: %s", email) // 重新加载账号 ps.pool.Load(ps.config.DataDir) } else { errMsg, _ := data["error"].(string) logger.Warn("❌ 注册失败: %s", errMsg) } } func (ps *PoolServer) handleRefreshResult(data map[string]interface{}) { email, _ := data["email"].(string) success, _ := data["success"].(bool) if success { logger.Info("✅ 账号续期成功: %s", email) // 更新账号数据 if cookiesData, ok := data["cookies"]; ok { ps.updateAccountCookies(email, cookiesData) } } else { errMsg, _ := data["error"].(string) logger.Warn("❌ 账号续期失败 %s: %s", email, errMsg) action := ps.config.ExpiredAction if action == "" { action = "delete" // 默认删除 } switch action { case "delete": ps.deleteAccount(email) case "queue": // 保持在队列中,不做处理 case "refresh": default: ps.deleteAccount(email) } } } // deleteAccount 删除账号 func (ps *PoolServer) deleteAccount(email string) { ps.pool.mu.Lock() defer ps.pool.mu.Unlock() // 从 pending 队列删除 for i, acc := range ps.pool.pendingAccounts { if acc.Data.Email == email { // 删除文件 if acc.FilePath != "" { os.Remove(acc.FilePath) } ps.pool.pendingAccounts = append(ps.pool.pendingAccounts[:i], ps.pool.pendingAccounts[i+1:]...) logger.Info("🗑️ 已删除续期失败账号: %s", email) return } } // 从 ready 队列删除 for i, acc := range ps.pool.readyAccounts { if acc.Data.Email == email { if acc.FilePath != "" { os.Remove(acc.FilePath) } ps.pool.readyAccounts = append(ps.pool.readyAccounts[:i], ps.pool.readyAccounts[i+1:]...) logger.Info("🗑️ 已删除续期失败账号: %s", email) return } } } // updateAccountCookies 更新账号Cookie func (ps *PoolServer) updateAccountCookies(email string, cookiesData interface{}) { ps.pool.mu.Lock() defer ps.pool.mu.Unlock() for _, acc := range ps.pool.readyAccounts { if acc.Data.Email == email { // 更新cookies if cookies, ok := cookiesData.([]interface{}); ok { var newCookies []Cookie for _, c := range cookies { if cm, ok := c.(map[string]interface{}); ok { newCookies = append(newCookies, Cookie{ Name: cm["name"].(string), Value: cm["value"].(string), Domain: cm["domain"].(string), }) } } acc.Data.Cookies = newCookies acc.Refreshed = true acc.FailCount = 0 acc.SaveToFile() } return } } } // handleQueueRegister 队列注册任务 func (ps *PoolServer) handleQueueRegister(w http.ResponseWriter, r *http.Request) { var req struct { Count int `json:"count"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Count <= 0 { req.Count = 1 } select { case ps.registerQueue <- req.Count: json.NewEncoder(w).Encode(map[string]interface{}{ "success": true, "message": fmt.Sprintf("已添加 %d 个注册任务到队列", req.Count), }) default: json.NewEncoder(w).Encode(map[string]interface{}{ "success": false, "error": "任务队列已满", }) } } // handleQueueRefresh 队列续期任务 func (ps *PoolServer) handleQueueRefresh(w http.ResponseWriter, r *http.Request) { var req struct { Email string `json:"email"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } // 查找账号 ps.pool.mu.RLock() var targetAcc *Account for _, acc := range ps.pool.readyAccounts { if acc.Data.Email == req.Email { targetAcc = acc break } } ps.pool.mu.RUnlock() if targetAcc == nil { json.NewEncoder(w).Encode(map[string]interface{}{ "success": false, "error": "账号未找到", }) return } select { case ps.refreshQueue <- targetAcc: json.NewEncoder(w).Encode(map[string]interface{}{ "success": true, "message": fmt.Sprintf("已添加账号 %s 续期任务到队列", req.Email), }) default: json.NewEncoder(w).Encode(map[string]interface{}{ "success": false, "error": "任务队列已满", }) } } // AccountResponse 账号响应 type AccountResponse struct { Success bool `json:"success"` Email string `json:"email,omitempty"` JWT string `json:"jwt,omitempty"` ConfigID string `json:"config_id,omitempty"` Authorization string `json:"authorization,omitempty"` Error string `json:"error,omitempty"` } func (ps *PoolServer) handleNext(w http.ResponseWriter, r *http.Request) { acc := ps.pool.Next() if acc == nil { json.NewEncoder(w).Encode(AccountResponse{ Success: false, Error: "没有可用账号", }) return } jwt, configID, err := acc.GetJWT() if err != nil { json.NewEncoder(w).Encode(AccountResponse{ Success: false, Email: acc.Data.Email, Error: err.Error(), }) return } json.NewEncoder(w).Encode(AccountResponse{ Success: true, Email: acc.Data.Email, JWT: jwt, ConfigID: configID, Authorization: acc.Data.Authorization, }) } func (ps *PoolServer) handleMark(w http.ResponseWriter, r *http.Request) { var req struct { Email string `json:"email"` Success bool `json:"success"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } // 查找账号并标记 ps.pool.mu.RLock() var targetAcc *Account for _, acc := range ps.pool.readyAccounts { if acc.Data.Email == req.Email { targetAcc = acc break } } ps.pool.mu.RUnlock() if targetAcc != nil { ps.pool.MarkUsed(targetAcc, req.Success) } json.NewEncoder(w).Encode(map[string]bool{"success": true}) } func (ps *PoolServer) handleRefresh(w http.ResponseWriter, r *http.Request) { var req struct { Email string `json:"email"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } // 查找账号并标记需要刷新 ps.pool.mu.RLock() var targetAcc *Account for _, acc := range ps.pool.readyAccounts { if acc.Data.Email == req.Email { targetAcc = acc break } } ps.pool.mu.RUnlock() if targetAcc != nil { ps.pool.MarkNeedsRefresh(targetAcc) } json.NewEncoder(w).Encode(map[string]bool{"success": true}) } func (ps *PoolServer) handleStatus(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(ps.pool.Stats()) } func (ps *PoolServer) handleGetJWT(w http.ResponseWriter, r *http.Request) { email := r.URL.Query().Get("email") if email == "" { http.Error(w, "缺少email参数", http.StatusBadRequest) return } ps.pool.mu.RLock() var targetAcc *Account for _, acc := range ps.pool.readyAccounts { if acc.Data.Email == email { targetAcc = acc break } } ps.pool.mu.RUnlock() if targetAcc == nil { json.NewEncoder(w).Encode(AccountResponse{ Success: false, Error: "账号未找到", }) return } jwt, configID, err := targetAcc.GetJWT() if err != nil { json.NewEncoder(w).Encode(AccountResponse{ Success: false, Email: email, Error: err.Error(), }) return } json.NewEncoder(w).Encode(AccountResponse{ Success: true, Email: email, JWT: jwt, ConfigID: configID, Authorization: targetAcc.Data.Authorization, }) } // ==================== 远程号池客户端 ==================== // RemotePoolClient 远程号池客户端 type RemotePoolClient struct { serverAddr string secret string client *http.Client mu sync.RWMutex // 本地缓存 cachedAccounts map[string]*CachedAccount } // CachedAccount 缓存的账号信息 type CachedAccount struct { Email string JWT string ConfigID string Authorization string FetchedAt time.Time } // NewRemotePoolClient 创建远程号池客户端 func NewRemotePoolClient(serverAddr, secret string) *RemotePoolClient { return &RemotePoolClient{ serverAddr: serverAddr, secret: secret, client: &http.Client{ Timeout: 30 * time.Second, }, cachedAccounts: make(map[string]*CachedAccount), } } // doRequest 发送请求到号池服务器 func (rc *RemotePoolClient) doRequest(method, path string, body interface{}) (*http.Response, error) { var reqBody io.Reader if body != nil { data, err := json.Marshal(body) if err != nil { return nil, err } reqBody = bytes.NewReader(data) } req, err := http.NewRequest(method, rc.serverAddr+path, reqBody) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/json") if rc.secret != "" { req.Header.Set("X-Pool-Secret", rc.secret) } return rc.client.Do(req) } // Next 获取下一个可用账号 func (rc *RemotePoolClient) Next() (*CachedAccount, error) { resp, err := rc.doRequest("GET", "/pool/next", nil) if err != nil { return nil, fmt.Errorf("请求号池服务器失败: %w", err) } defer resp.Body.Close() var result AccountResponse if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, fmt.Errorf("解析响应失败: %w", err) } if !result.Success { return nil, fmt.Errorf("%s", result.Error) } acc := &CachedAccount{ Email: result.Email, JWT: result.JWT, ConfigID: result.ConfigID, Authorization: result.Authorization, FetchedAt: time.Now(), } // 缓存账号 rc.mu.Lock() rc.cachedAccounts[result.Email] = acc rc.mu.Unlock() return acc, nil } // MarkUsed 标记账号使用结果 func (rc *RemotePoolClient) MarkUsed(email string, success bool) error { resp, err := rc.doRequest("POST", "/pool/mark", map[string]interface{}{ "email": email, "success": success, }) if err != nil { return err } resp.Body.Close() return nil } // MarkNeedsRefresh 标记账号需要刷新 func (rc *RemotePoolClient) MarkNeedsRefresh(email string) error { resp, err := rc.doRequest("POST", "/pool/refresh", map[string]interface{}{ "email": email, }) if err != nil { return err } resp.Body.Close() return nil } // GetStatus 获取号池状态 func (rc *RemotePoolClient) GetStatus() (map[string]interface{}, error) { resp, err := rc.doRequest("GET", "/pool/status", nil) if err != nil { return nil, err } defer resp.Body.Close() var result map[string]interface{} if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, err } return result, nil } // RefreshJWT 刷新指定账号的JWT func (rc *RemotePoolClient) RefreshJWT(email string) (*CachedAccount, error) { resp, err := rc.doRequest("GET", "/pool/jwt?email="+email, nil) if err != nil { return nil, err } defer resp.Body.Close() var result AccountResponse if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, err } if !result.Success { return nil, fmt.Errorf("%s", result.Error) } acc := &CachedAccount{ Email: result.Email, JWT: result.JWT, ConfigID: result.ConfigID, Authorization: result.Authorization, FetchedAt: time.Now(), } rc.mu.Lock() rc.cachedAccounts[email] = acc rc.mu.Unlock() return acc, nil } type AccountUploadRequest struct { Email string `json:"email"` FullName string `json:"full_name"` Cookies []Cookie `json:"cookies"` CookieString string `json:"cookie_string"` Authorization string `json:"authorization"` ConfigID string `json:"config_id"` CSESIDX string `json:"csesidx"` IsNew bool `json:"is_new"` } // handleUploadAccount 处理账号上传(客户端回传鉴权文件) func (ps *PoolServer) handleUploadAccount(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } var req AccountUploadRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { logger.Error("解析账号上传请求失败: %v", err) json.NewEncoder(w).Encode(map[string]interface{}{ "success": false, "error": "无效的请求格式", }) return } if req.Email == "" { json.NewEncoder(w).Encode(map[string]interface{}{ "success": false, "error": "邮箱不能为空", }) return } // 构建账号数据 accData := AccountData{ Email: req.Email, FullName: req.FullName, Cookies: req.Cookies, CookieString: req.CookieString, Authorization: req.Authorization, ConfigID: req.ConfigID, CSESIDX: req.CSESIDX, Timestamp: time.Now().Format(time.RFC3339), } // 保存到文件 dataDir := ps.config.DataDir if dataDir == "" { dataDir = "./data" } // 确保目录存在 if err := os.MkdirAll(dataDir, 0755); err != nil { logger.Error("创建数据目录失败: %v", err) json.NewEncoder(w).Encode(map[string]interface{}{ "success": false, "error": "服务器内部错误", }) return } // 生成文件名 filename := fmt.Sprintf("%s.json", req.Email) filePath := filepath.Join(dataDir, filename) // 序列化并保存 data, err := json.MarshalIndent(accData, "", " ") if err != nil { logger.Error("序列化账号数据失败: %v", err) json.NewEncoder(w).Encode(map[string]interface{}{ "success": false, "error": "序列化失败", }) return } if err := os.WriteFile(filePath, data, 0644); err != nil { logger.Error("保存账号文件失败: %v", err) json.NewEncoder(w).Encode(map[string]interface{}{ "success": false, "error": "保存失败", }) return } if req.IsNew { logger.Info("✅ 收到新注册账号: %s", req.Email) } else { logger.Info("✅ 收到账号续期数据: %s", req.Email) } // 先加载文件确保账号存在 ps.pool.Load(dataDir) // 更新内存中的账号数据 ps.pool.mu.Lock() found := false // 查找并更新 pending 队列 for _, acc := range ps.pool.pendingAccounts { if acc.Data.Email == req.Email { acc.Mu.Lock() acc.Data.Cookies = req.Cookies acc.Data.CookieString = req.CookieString acc.Data.Authorization = req.Authorization acc.Data.ConfigID = req.ConfigID acc.Data.CSESIDX = req.CSESIDX acc.ConfigID = req.ConfigID acc.CSESIDX = req.CSESIDX acc.FailCount = 0 acc.BrowserRefreshCount = 0 acc.JWTExpires = time.Time{} // 重置JWT过期时间,让refreshWorker去刷新 acc.Mu.Unlock() // 保留在 pending 队列,让 refreshWorker 去刷新 JWT logger.Info("🔄 [%s] 账号已更新,等待JWT刷新", req.Email) found = true break } } if !found { // 查找并更新 ready 队列 for _, acc := range ps.pool.readyAccounts { if acc.Data.Email == req.Email { acc.Mu.Lock() acc.Data.Cookies = req.Cookies acc.Data.CookieString = req.CookieString acc.Data.Authorization = req.Authorization acc.Data.ConfigID = req.ConfigID acc.Data.CSESIDX = req.CSESIDX acc.ConfigID = req.ConfigID acc.CSESIDX = req.CSESIDX acc.FailCount = 0 acc.BrowserRefreshCount = 0 acc.JWTExpires = time.Time{} // 重置JWT过期时间,下次使用时会触发刷新 acc.Mu.Unlock() logger.Info("🔄 [%s] 账号已更新,下次使用时刷新JWT", req.Email) found = true break } } } ps.pool.mu.Unlock() if !found { logger.Warn("⚠️ [%s] 账号已保存但未在内存中找到", req.Email) } json.NewEncoder(w).Encode(map[string]interface{}{ "success": true, "message": fmt.Sprintf("账号 %s 已保存", req.Email), }) } func (rc *RemotePoolClient) UploadAccount(acc *AccountUploadRequest) error { data, err := json.Marshal(acc) if err != nil { return err } resp, err := rc.doRequest("POST", "/pool/upload-account", bytes.NewReader(data)) if err != nil { return err } defer resp.Body.Close() var result map[string]interface{} if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return err } if success, ok := result["success"].(bool); !ok || !success { errMsg, _ := result["error"].(string) return fmt.Errorf("上传失败: %s", errMsg) } return nil } type ClientInfo struct { ID string `json:"id"` Version string `json:"version"` Threads int `json:"threads"` IsAlive bool `json:"is_alive"` LastPing int64 `json:"last_ping"` } func (ps *PoolServer) GetClientsInfo() []ClientInfo { ps.clientsMu.RLock() defer ps.clientsMu.RUnlock() clients := make([]ClientInfo, 0, len(ps.clients)) for id, c := range ps.clients { clients = append(clients, ClientInfo{ ID: id, Version: c.ClientVersion, Threads: c.MaxThreads, IsAlive: c.IsAlive, LastPing: c.LastPing.Unix(), }) } return clients } func (ps *PoolServer) GetClientCount() int { ps.clientsMu.RLock() defer ps.clientsMu.RUnlock() return len(ps.clients) } func (ps *PoolServer) GetTotalThreads() int { ps.clientsMu.RLock() defer ps.clientsMu.RUnlock() total := 0 for _, c := range ps.clients { total += c.MaxThreads } return total } ================================================ FILE: src/proxy/proxy.go ================================================ package proxy import ( "bytes" "compress/gzip" "crypto/rand" "crypto/tls" "encoding/base64" "encoding/json" "fmt" "io" "log" "math/big" "net/http" "net/url" "os" "sort" "strconv" "strings" "sync" "sync/atomic" "time" ) // tlsConfig 全局 TLS 配置,跳过证书验证 var tlsConfig = &tls.Config{InsecureSkipVerify: true} // ProxyNode 代理节点 type ProxyNode struct { Raw string // 原始链接 Protocol string // vmess, vless, ss, trojan, http, socks5, hysteria2, anytls Name string Server string Port int UUID string // vmess/vless AlterId int // vmess Security string // vmess 加密方式 / vless: none,tls,reality Network string // tcp, ws, grpc, kcp, quic, httpupgrade, splithttp, xhttp Path string // ws/http path Host string // ws/http host TLS bool SNI string Password string // ss/trojan/anytls password Method string // ss method Type string // kcp/quic header type (none, srtp, utp, wechat-video, dtls, wireguard) Healthy bool LastCheck time.Time LocalPort int Latency time.Duration ExitIP string // Reality 相关 Flow string // xtls-rprx-vision Fingerprint string // chrome, firefox, safari, ios, android, edge, 360, qq, random PublicKey string // reality pbk ShortId string // reality sid SpiderX string // reality spx ALPN string // 使用统计 LastUsed time.Time // 最后使用时间 FailCount int // 连续失败次数 UseCooldown time.Duration // 使用冷却时间(失败后动态调整) } // InstanceStatus 实例状态 type InstanceStatus int const ( InstanceStatusIdle InstanceStatus = iota // 空闲可用 InstanceStatusInUse // 使用中 InstanceStatusStopped // 已停止 ) // ProxyInstance 代理实例(使用 sing-box) type ProxyInstance struct { localPort int node *ProxyNode running bool status InstanceStatus lastUsed time.Time proxyURL string mu sync.Mutex } // ProxyManager 代理管理器 type ProxyManager struct { mu sync.RWMutex nodes []*ProxyNode healthyNodes []*ProxyNode instancePool []*ProxyInstance // 活跃实例追踪 maxPoolSize int // 最大实例池大小 subscribeURLs []string proxyFiles []string lastUpdate time.Time updateInterval time.Duration checkInterval time.Duration healthCheckURL string stopChan chan struct{} ready bool // 代理池是否就绪 readyCond *sync.Cond // 就绪条件变量 healthChecking bool // 是否正在健康检查 } // 默认代理使用冷却时间 var ( DefaultProxyUseCooldown = 5 * time.Second // 默认使用冷却 MaxProxyFailCount = 3 // 最大连续失败次数,超过后增加冷却 DefaultProxyCount = 5 // 默认代理池大小 MinHealthyForReady = 1 // 最少健康节点数才提示就绪(改为1,更快就绪) HealthCheckTimeout = 10 * time.Second // 健康检查超时(增加到10秒,给慢速代理更多时间) ) var Manager = &ProxyManager{ instancePool: make([]*ProxyInstance, 0), maxPoolSize: 5, updateInterval: 30 * time.Minute, checkInterval: 5 * time.Minute, healthCheckURL: "https://www.google.com/generate_204", stopChan: make(chan struct{}), } func init() { Manager.readyCond = sync.NewCond(&Manager.mu) } // IsReady 检查代理池是否就绪 func (pm *ProxyManager) IsReady() bool { pm.mu.RLock() defer pm.mu.RUnlock() return pm.ready } func (pm *ProxyManager) WaitReady(timeout time.Duration) bool { deadline := time.Now().Add(timeout) for time.Now().Before(deadline) { pm.mu.RLock() ready := pm.ready healthyCount := len(pm.healthyNodes) pm.mu.RUnlock() if ready || healthyCount > 0 { return true } time.Sleep(100 * time.Millisecond) } pm.mu.RLock() defer pm.mu.RUnlock() return pm.ready || len(pm.healthyNodes) > 0 } // SetReady 设置就绪状态 func (pm *ProxyManager) SetReady(ready bool) { pm.mu.Lock() defer pm.mu.Unlock() pm.ready = ready if ready { pm.readyCond.Broadcast() } } // SetMaxPoolSize 设置最大实例池大小 func (pm *ProxyManager) SetMaxPoolSize(size int) { pm.mu.Lock() defer pm.mu.Unlock() if size > 0 { pm.maxPoolSize = size } } // InitInstancePool 初始化实例池(按需启动指定数量的代理实例) func (pm *ProxyManager) InitInstancePool(count int) error { pm.mu.Lock() defer pm.mu.Unlock() if len(pm.healthyNodes) == 0 && len(pm.nodes) == 0 { return fmt.Errorf("没有可用的代理节点") } if count > pm.maxPoolSize { count = pm.maxPoolSize } nodes := pm.healthyNodes if len(nodes) == 0 { nodes = pm.nodes } log.Printf("🔧 初始化代理实例池: 目标 %d 个实例", count) for i := 0; i < count && i < len(nodes); i++ { node := nodes[i%len(nodes)] instance, err := pm.startInstanceLocked(node) if err != nil { log.Printf("⚠️ 启动实例 %d 失败: %v", i, err) continue } instance.status = InstanceStatusIdle pm.instancePool = append(pm.instancePool, instance) } log.Printf("✅ 实例池初始化完成: %d 个实例就绪", len(pm.instancePool)) return nil } func (pm *ProxyManager) SetXrayPath(path string) { } // AddSubscribeURL 添加订阅链接 func (pm *ProxyManager) AddSubscribeURL(url string) { url = strings.TrimSpace(url) if url == "" { return // 过滤空字符串 } pm.mu.Lock() defer pm.mu.Unlock() pm.subscribeURLs = append(pm.subscribeURLs, url) } // AddProxyFile 添加代理文件 func (pm *ProxyManager) AddProxyFile(path string) { path = strings.TrimSpace(path) if path == "" { return // 过滤空字符串 } pm.mu.Lock() defer pm.mu.Unlock() pm.proxyFiles = append(pm.proxyFiles, path) } // LoadAll 加载所有代理源 func (pm *ProxyManager) LoadAll() error { var allNodes []*ProxyNode // 从订阅加载 for _, url := range pm.subscribeURLs { log.Printf("🔄 正在加载订阅: %s", url) nodes, err := pm.loadFromURL(url) if err != nil { log.Printf("⚠️ 加载订阅失败 %s: %v", url, err) continue } log.Printf("✅ 订阅加载成功: %d 个节点", len(nodes)) allNodes = append(allNodes, nodes...) } // 从文件加载 for _, file := range pm.proxyFiles { log.Printf("🔄 正在加载代理文件: %s", file) nodes, err := pm.loadFromFile(file) if err != nil { log.Printf("⚠️ 加载文件失败 %s: %v", file, err) continue } log.Printf("✅ 文件加载成功: %d 个节点", len(nodes)) allNodes = append(allNodes, nodes...) } pm.mu.Lock() pm.nodes = allNodes pm.lastUpdate = time.Now() pm.mu.Unlock() log.Printf("✅ 共加载 %d 个代理节点 (订阅: %d, 文件: %d)", len(allNodes), len(pm.subscribeURLs), len(pm.proxyFiles)) return nil } type SubscriptionInfo struct { Upload int64 Download int64 Total int64 Expire int64 } // parseSubscriptionUserinfo 解析 subscription-userinfo 头 func parseSubscriptionUserinfo(header string) *SubscriptionInfo { if header == "" { return nil } info := &SubscriptionInfo{} parts := strings.Split(header, ";") for _, part := range parts { kv := strings.SplitN(strings.TrimSpace(part), "=", 2) if len(kv) != 2 { continue } key := strings.TrimSpace(kv[0]) value, _ := strconv.ParseInt(strings.TrimSpace(kv[1]), 10, 64) switch key { case "upload": info.Upload = value case "download": info.Download = value case "total": info.Total = value case "expire": info.Expire = value } } return info } // getRemainingTraffic 获取剩余流量(字节) func (si *SubscriptionInfo) getRemainingTraffic() int64 { if si == nil || si.Total == 0 { return -1 // 未知 } return si.Total - si.Upload - si.Download } func (pm *ProxyManager) loadFromURL(urlStr string) ([]*ProxyNode, error) { client := &http.Client{Timeout: 30 * time.Second} resp, err := client.Get(urlStr) if err != nil { return nil, err } defer resp.Body.Close() // 检查订阅流量信息 userinfo := resp.Header.Get("subscription-userinfo") if userinfo == "" { userinfo = resp.Header.Get("Subscription-Userinfo") } if subInfo := parseSubscriptionUserinfo(userinfo); subInfo != nil { remaining := subInfo.getRemainingTraffic() if remaining == 0 { return nil, fmt.Errorf("订阅流量已耗尽") } if remaining > 0 && remaining < 100*1024*1024 { } } body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } return pm.parseContent(string(body)) } // loadFromFile 从文件加载 func (pm *ProxyManager) loadFromFile(path string) ([]*ProxyNode, error) { data, err := os.ReadFile(path) if err != nil { return nil, err } return pm.parseContent(string(data)) } func (pm *ProxyManager) parseContent(content string) ([]*ProxyNode, error) { decoded, err := base64.StdEncoding.DecodeString(strings.TrimSpace(content)) if err == nil { content = string(decoded) } var nodes []*ProxyNode lines := strings.Split(content, "\n") for _, line := range lines { line = strings.TrimSpace(line) if line == "" || strings.HasPrefix(line, "#") { continue } node := pm.parseLine(line) if node != nil { nodes = append(nodes, node) } } return nodes, nil } // tryBase64Decode 尝试多种 base64 解码方式 func tryBase64Decode(s string) []byte { s = strings.TrimSpace(s) // 尝试标准 base64 if decoded, err := base64.StdEncoding.DecodeString(s); err == nil { return decoded } // 尝试 URL-safe base64 if decoded, err := base64.URLEncoding.DecodeString(s); err == nil { return decoded } // 尝试无填充的标准 base64 if decoded, err := base64.RawStdEncoding.DecodeString(s); err == nil { return decoded } // 尝试无填充的 URL-safe base64 if decoded, err := base64.RawURLEncoding.DecodeString(s); err == nil { return decoded } return nil } // parseLine 解析单行 func (pm *ProxyManager) parseLine(line string) *ProxyNode { if strings.HasPrefix(line, "vmess://") { return parseVmess(line) } if strings.HasPrefix(line, "vless://") { return parseVless(line) } if strings.HasPrefix(line, "ss://") { return parseSS(line) } if strings.HasPrefix(line, "trojan://") { return parseTrojan(line) } if strings.HasPrefix(line, "hysteria2://") || strings.HasPrefix(line, "hy2://") { return parseHysteria2(line) } if strings.HasPrefix(line, "anytls://") { return parseAnyTLS(line) } if strings.HasPrefix(line, "http://") || strings.HasPrefix(line, "https://") || strings.HasPrefix(line, "socks5://") { return parseDirectProxy(line) } return nil } // getStringFromMap 安全获取 map 中的字符串值 func getStringFromMap(m map[string]interface{}, key string) string { if v, ok := m[key]; ok { switch s := v.(type) { case string: return s case float64: return strconv.FormatFloat(s, 'f', -1, 64) case int: return strconv.Itoa(s) } } return "" } // getIntFromMap 安全获取 map 中的整数值 func getIntFromMap(m map[string]interface{}, key string) int { if v, ok := m[key]; ok { switch n := v.(type) { case float64: return int(n) case int: return n case string: i, _ := strconv.Atoi(n) return i } } return 0 } // parseVmess 解析 vmess 链接 func parseVmess(link string) *ProxyNode { // vmess://base64(json) data := strings.TrimPrefix(link, "vmess://") decoded := tryBase64Decode(data) if decoded == nil { return nil } var config map[string]interface{} if err := json.Unmarshal(decoded, &config); err != nil { return nil } node := &ProxyNode{ Raw: link, Protocol: "vmess", } node.Name = getStringFromMap(config, "ps") node.Server = getStringFromMap(config, "add") node.Port = getIntFromMap(config, "port") node.UUID = getStringFromMap(config, "id") node.AlterId = getIntFromMap(config, "aid") // 加密方式 node.Security = getStringFromMap(config, "scy") if node.Security == "" { node.Security = "auto" } // 传输协议 node.Network = getStringFromMap(config, "net") if node.Network == "" { node.Network = "tcp" } // 路径和 Host node.Path = getStringFromMap(config, "path") node.Host = getStringFromMap(config, "host") // TLS 设置(支持多种写法) tlsVal := getStringFromMap(config, "tls") if tlsVal != "" && tlsVal != "none" && tlsVal != "0" && tlsVal != "false" { node.TLS = true } node.SNI = getStringFromMap(config, "sni") if node.SNI == "" && node.TLS { node.SNI = node.Host } // Header 类型(kcp/quic) node.Type = getStringFromMap(config, "type") if node.Server == "" || node.Port == 0 || node.UUID == "" { return nil } return node } // parseVless 解析 vless 链接 func parseVless(link string) *ProxyNode { // vless://uuid@server:port?params#name u, err := url.Parse(link) if err != nil { return nil } port, _ := strconv.Atoi(u.Port()) // URL 解码名称 name, _ := url.QueryUnescape(u.Fragment) node := &ProxyNode{ Raw: link, Protocol: "vless", UUID: u.User.Username(), Server: u.Hostname(), Port: port, Name: name, } query := u.Query() // 传输协议(支持更多类型) node.Network = query.Get("type") if node.Network == "" { node.Network = "tcp" } // 安全类型 node.Security = query.Get("security") if node.Security == "" { node.Security = "none" } if node.Security == "tls" || node.Security == "reality" { node.TLS = true } // Flow(XTLS) node.Flow = query.Get("flow") // 路径(需要 URL 解码) if path := query.Get("path"); path != "" { node.Path, _ = url.QueryUnescape(path) } // Host node.Host = query.Get("host") if node.Host == "" { node.Host = query.Get("sni") } // SNI node.SNI = query.Get("sni") if node.SNI == "" && node.TLS && node.Security != "reality" { node.SNI = node.Host if node.SNI == "" { node.SNI = node.Server } } // Fingerprint(TLS/Reality 指纹) node.Fingerprint = query.Get("fp") if node.Fingerprint == "" { node.Fingerprint = query.Get("fingerprint") } // Reality 相关参数 if node.Security == "reality" { node.PublicKey = query.Get("pbk") node.ShortId = query.Get("sid") node.SpiderX = query.Get("spx") // Reality 必须有 SNI if node.SNI == "" { node.SNI = query.Get("serverName") } } // ALPN node.ALPN = query.Get("alpn") // Header 类型(kcp/quic 等) node.Type = query.Get("headerType") // GRPC 服务名 if serviceName := query.Get("serviceName"); serviceName != "" && node.Network == "grpc" { node.Path = serviceName } // xhttp/splithttp/httpupgrade 的额外参数 if node.Network == "xhttp" || node.Network == "splithttp" || node.Network == "httpupgrade" { if node.Path == "" { node.Path = "/" } } if node.Server == "" || node.Port == 0 || node.UUID == "" { return nil } return node } // xray-core 支持的 shadowsocks 加密方法 var supportedSSCiphers = map[string]bool{ // AEAD 加密(推荐) "aes-128-gcm": true, "aes-256-gcm": true, "chacha20-poly1305": true, "chacha20-ietf-poly1305": true, "xchacha20-poly1305": true, "xchacha20-ietf-poly1305": true, // 流式加密(xray-core 支持) "aes-128-ctr": true, "aes-192-ctr": true, "aes-256-ctr": true, // 其他支持的 "none": true, "plain": true, "2022-blake3-aes-128-gcm": true, "2022-blake3-aes-256-gcm": true, "2022-blake3-chacha20-poly1305": true, } // 不支持的 cipher 方法映射(旧的 CFB/OFB 等) var unsupportedSSCiphers = map[string]string{ "aes-128-cfb": "", // 不支持,跳过 "aes-192-cfb": "", "aes-256-cfb": "", "aes-128-ofb": "", "aes-192-ofb": "", "aes-256-ofb": "", "bf-cfb": "", "cast5-cfb": "", "des-cfb": "", "idea-cfb": "", "rc2-cfb": "", "rc4": "", "rc4-md5": "", "rc4-md5-6": "", "seed-cfb": "", "salsa20": "", "chacha20": "chacha20-ietf-poly1305", // 尝试升级 "chacha20-ietf": "chacha20-ietf-poly1305", } // isSupportedSSCipher 检查是否支持的 cipher func isSupportedSSCipher(method string) bool { method = strings.ToLower(method) return supportedSSCiphers[method] } // tryMapSSCipher 尝试映射不支持的 cipher 到支持的 func tryMapSSCipher(method string) (string, bool) { method = strings.ToLower(method) if isSupportedSSCipher(method) { return method, true } if mapped, ok := unsupportedSSCiphers[method]; ok { if mapped == "" { return "", false // 不支持且无法映射 } return mapped, true } return "", false } // parseSS 解析 ss 链接 func parseSS(link string) *ProxyNode { // 支持多种格式: // ss://base64(method:password)@host:port#name (SIP002) // ss://base64(method:password@host:port)#name (旧格式) // ss://method:password@host:port#name (明文格式) origLink := link link = strings.TrimPrefix(link, "ss://") var name string if idx := strings.Index(link, "#"); idx != -1 { name = link[idx+1:] link = link[:idx] } name, _ = url.QueryUnescape(name) node := &ProxyNode{ Protocol: "shadowsocks", Name: name, } // 尝试解析 SIP002 格式: base64(method:password)@host:port if atIdx := strings.LastIndex(link, "@"); atIdx != -1 { userInfo := link[:atIdx] hostPort := link[atIdx+1:] // 尝试 base64 解码 userInfo if decoded := tryBase64Decode(userInfo); decoded != nil { parts := strings.SplitN(string(decoded), ":", 2) if len(parts) == 2 { node.Method = parts[0] node.Password = parts[1] } } else { // 可能是明文格式 method:password parts := strings.SplitN(userInfo, ":", 2) if len(parts) == 2 { node.Method = parts[0] node.Password = parts[1] } } // 解析 host:port(可能包含 IPv6) if strings.HasPrefix(hostPort, "[") { // IPv6: [::1]:port if endBracket := strings.Index(hostPort, "]:"); endBracket != -1 { node.Server = hostPort[1:endBracket] node.Port, _ = strconv.Atoi(hostPort[endBracket+2:]) } } else { parts := strings.Split(hostPort, ":") if len(parts) >= 2 { node.Server = parts[0] node.Port, _ = strconv.Atoi(parts[len(parts)-1]) } } } else { // 旧格式: 整个内容是 base64 编码 decoded := tryBase64Decode(link) if decoded == nil { return nil } // method:password@host:port decodedStr := string(decoded) if atIdx := strings.LastIndex(decodedStr, "@"); atIdx != -1 { userInfo := decodedStr[:atIdx] hostPort := decodedStr[atIdx+1:] parts := strings.SplitN(userInfo, ":", 2) if len(parts) == 2 { node.Method = parts[0] node.Password = parts[1] } hpParts := strings.Split(hostPort, ":") if len(hpParts) >= 2 { node.Server = hpParts[0] node.Port, _ = strconv.Atoi(hpParts[len(hpParts)-1]) } } } node.Raw = origLink // 验证必要字段 if node.Server == "" || node.Port == 0 || node.Method == "" { return nil } // 检查并映射 cipher 方法 if mappedMethod, ok := tryMapSSCipher(node.Method); ok { if mappedMethod != node.Method { node.Method = mappedMethod } } else { return nil // 跳过不支持的节点 } return node } // parseTrojan 解析 trojan 链接 func parseTrojan(link string) *ProxyNode { // trojan://password@server:port?params#name u, err := url.Parse(link) if err != nil { return nil } port, _ := strconv.Atoi(u.Port()) name, _ := url.QueryUnescape(u.Fragment) node := &ProxyNode{ Raw: link, Protocol: "trojan", Password: u.User.Username(), Server: u.Hostname(), Port: port, Name: name, TLS: true, // trojan 默认 TLS } query := u.Query() node.SNI = query.Get("sni") if node.SNI == "" { node.SNI = node.Server } if host := query.Get("host"); host != "" { node.Host = host } // 传输协议 node.Network = query.Get("type") if node.Network == "" { node.Network = "tcp" } // 路径 if path := query.Get("path"); path != "" { node.Path, _ = url.QueryUnescape(path) } // Fingerprint node.Fingerprint = query.Get("fp") // ALPN node.ALPN = query.Get("alpn") if node.Server == "" || node.Port == 0 || node.Password == "" { return nil } return node } // parseHysteria2 解析 hysteria2/hy2 链接 func parseHysteria2(link string) *ProxyNode { // hysteria2://password@server:port?params#name // hy2://password@server:port?params#name link = strings.Replace(link, "hy2://", "hysteria2://", 1) u, err := url.Parse(link) if err != nil { return nil } port, _ := strconv.Atoi(u.Port()) name, _ := url.QueryUnescape(u.Fragment) node := &ProxyNode{ Raw: link, Protocol: "hysteria2", Password: u.User.Username(), Server: u.Hostname(), Port: port, Name: name, TLS: true, // hysteria2 默认 TLS } query := u.Query() node.SNI = query.Get("sni") if node.SNI == "" { node.SNI = node.Server } // ALPN node.ALPN = query.Get("alpn") if node.ALPN == "" { node.ALPN = "h3" } // Fingerprint node.Fingerprint = query.Get("pinSHA256") // obfs if obfs := query.Get("obfs"); obfs != "" { node.Type = obfs node.Path = query.Get("obfs-password") } if node.Server == "" || node.Port == 0 || node.Password == "" { return nil } return node } // parseAnyTLS 解析 anytls 链接 func parseAnyTLS(link string) *ProxyNode { // anytls://password@server:port?params#name u, err := url.Parse(link) if err != nil { return nil } port, _ := strconv.Atoi(u.Port()) name, _ := url.QueryUnescape(u.Fragment) node := &ProxyNode{ Raw: link, Protocol: "anytls", Password: u.User.Username(), Server: u.Hostname(), Port: port, Name: name, TLS: true, } query := u.Query() node.SNI = query.Get("sni") if node.SNI == "" { node.SNI = query.Get("serverName") } if node.SNI == "" { node.SNI = node.Server } // Fingerprint node.Fingerprint = query.Get("fp") if node.Fingerprint == "" { node.Fingerprint = query.Get("fingerprint") } // ALPN node.ALPN = query.Get("alpn") // insecure if query.Get("allowInsecure") == "1" || query.Get("insecure") == "1" { // 标记跳过证书验证 } if node.Server == "" || node.Port == 0 || node.Password == "" { return nil } return node } // parseDirectProxy 解析直接代理 func parseDirectProxy(link string) *ProxyNode { u, err := url.Parse(link) if err != nil { return nil } port, _ := strconv.Atoi(u.Port()) if port == 0 { if u.Scheme == "https" { port = 443 } else { port = 80 } } return &ProxyNode{ Raw: link, Protocol: u.Scheme, Server: u.Hostname(), Port: port, LocalPort: port, // 直接代理使用原端口 Healthy: true, } } // startInstanceLocked 内部方法:启动实例(使用 sing-box) func (pm *ProxyManager) startInstanceLocked(node *ProxyNode) (*ProxyInstance, error) { // 直接代理不需要启动 if node.Protocol == "http" || node.Protocol == "https" || node.Protocol == "socks5" { return &ProxyInstance{ node: node, running: true, status: InstanceStatusIdle, proxyURL: node.Raw, lastUsed: time.Now(), }, nil } // 使用 sing-box 启动代理 proxyURL, err := singboxMgr.Start(node) if err != nil { return nil, fmt.Errorf("sing-box 启动失败: %w", err) } return &ProxyInstance{ node: node, running: true, status: InstanceStatusIdle, proxyURL: proxyURL, lastUsed: time.Now(), localPort: node.LocalPort, }, nil } func (pm *ProxyManager) StartXray(node *ProxyNode) (string, error) { pm.mu.Lock() defer pm.mu.Unlock() instance, err := pm.startInstanceLocked(node) if err != nil { return "", err } return instance.proxyURL, nil } // StopProxy 停止代理实例 func (pm *ProxyManager) StopProxy(localPort int) { singboxMgr.Stop(localPort) } // StopXray 停止代理实例(兼容旧接口) func (pm *ProxyManager) StopXray(localPort int) { pm.StopProxy(localPort) } // StopAll 停止所有实例 func (pm *ProxyManager) StopAll() { singboxMgr.StopAll() log.Printf("🛑 所有代理实例已停止") } // 健康检查备选URL列表 var healthCheckURLs = []string{ "https://cp.cloudflare.com/generate_204", } // CheckHealth 检查节点健康并获取出口IP(优化:使用 StartRaw 避免双重测试) func (pm *ProxyManager) CheckHealth(node *ProxyNode) bool { // 直接代理不需要启动 if node.Protocol == "http" || node.Protocol == "https" || node.Protocol == "socks5" { node.Healthy = true node.LastCheck = time.Now() return true } // 使用 StartRaw 只启动不测试,避免双重测试 proxyURL, err := singboxMgr.StartRaw(node) if err != nil { return false } defer pm.StopXray(node.LocalPort) transport := &http.Transport{ TLSClientConfig: tlsConfig, } if proxyURL != "" { proxy, _ := url.Parse(proxyURL) transport.Proxy = http.ProxyURL(proxy) } client := &http.Client{ Transport: transport, Timeout: HealthCheckTimeout, } // 基本连通性检查(支持多URL重试) var success bool var latency time.Duration for _, testURL := range healthCheckURLs { start := time.Now() resp, err := client.Get(testURL) if err != nil { continue } resp.Body.Close() if resp.StatusCode == 204 || resp.StatusCode == 200 { latency = time.Since(start) success = true break } } if !success { return false } node.Latency = latency // 获取出口IP(可选,失败不影响健康状态) ipClient := &http.Client{ Transport: transport, Timeout: 3 * time.Second, } ipResp, err := ipClient.Get("https://ipinfo.io/ip") if err == nil { defer ipResp.Body.Close() if ipResp.StatusCode == 200 { ipBytes, _ := io.ReadAll(ipResp.Body) node.ExitIP = strings.TrimSpace(string(ipBytes)) } } return true } // CheckHealthQuick 快速健康检查(优化:使用 StartRaw 避免双重测试) func (pm *ProxyManager) CheckHealthQuick(node *ProxyNode) bool { // 直接代理不需要启动 if node.Protocol == "http" || node.Protocol == "https" || node.Protocol == "socks5" { return true } // 使用 StartRaw 只启动不测试 proxyURL, err := singboxMgr.StartRaw(node) if err != nil { return false } defer pm.StopXray(node.LocalPort) transport := &http.Transport{ TLSClientConfig: tlsConfig, } if proxyURL != "" { proxy, _ := url.Parse(proxyURL) transport.Proxy = http.ProxyURL(proxy) } client := &http.Client{ Transport: transport, Timeout: 6 * time.Second, } // 尝试多个测试URL for _, testURL := range healthCheckURLs { start := time.Now() resp, err := client.Get(testURL) if err != nil { continue } resp.Body.Close() if resp.StatusCode == 204 || resp.StatusCode == 200 { node.Latency = time.Since(start) return true } } return false } func (pm *ProxyManager) CheckAllHealth() { pm.mu.Lock() if pm.healthChecking { pm.mu.Unlock() return } pm.healthChecking = true hasSubscribes := len(pm.subscribeURLs) > 0 pm.mu.Unlock() if hasSubscribes { if err := pm.LoadAll(); err != nil { log.Printf("⚠️ 刷新订阅失败: %v", err) } } pm.mu.Lock() nodes := make([]*ProxyNode, len(pm.nodes)) copy(nodes, pm.nodes) pm.mu.Unlock() if len(nodes) == 0 { pm.mu.Lock() pm.healthChecking = false pm.mu.Unlock() pm.SetReady(true) return } var healthyNodes, unhealthyNodes, newNodes []*ProxyNode for _, n := range nodes { if n.LastCheck.IsZero() { newNodes = append(newNodes, n) } else if n.Healthy { healthyNodes = append(healthyNodes, n) } else { unhealthyNodes = append(unhealthyNodes, n) } } var healthy []*ProxyNode var checked int32 var mainWg sync.WaitGroup var mu sync.Mutex ipSeen := make(map[string]bool) total := len(nodes) // 检查单个节点的函数 checkNode := func(n *ProxyNode, sem chan struct{}) { sem <- struct{}{} defer func() { <-sem }() n.Healthy = pm.CheckHealth(n) n.LastCheck = time.Now() current := int(atomic.AddInt32(&checked, 1)) mu.Lock() if n.Healthy { if n.ExitIP != "" { if ipSeen[n.ExitIP] { for i, existing := range healthy { if existing.ExitIP == n.ExitIP && n.Latency < existing.Latency { healthy[i] = n break } } } else { ipSeen[n.ExitIP] = true healthy = append(healthy, n) } } else { healthy = append(healthy, n) } if len(healthy) >= MinHealthyForReady { pm.mu.Lock() if !pm.ready { pm.ready = true pm.healthyNodes = healthy pm.readyCond.Broadcast() } pm.mu.Unlock() } } healthyCount := len(healthy) mu.Unlock() if current%50 == 0 || current == total { log.Printf("🔍 进度: %d/%d, 健康: %d", current, total, healthyCount) } } mainWg.Add(1) go func() { defer mainWg.Done() var wg sync.WaitGroup sem := make(chan struct{}, 32) for _, n := range healthyNodes { wg.Add(1) go func(node *ProxyNode) { defer wg.Done() checkNode(node, sem) }(n) } wg.Wait() }() mainWg.Add(1) go func() { defer mainWg.Done() var wg sync.WaitGroup sem := make(chan struct{}, 32) for _, n := range unhealthyNodes { wg.Add(1) go func(node *ProxyNode) { defer wg.Done() checkNode(node, sem) }(n) } wg.Wait() }() mainWg.Add(1) go func() { defer mainWg.Done() var wg sync.WaitGroup sem := make(chan struct{}, 32) for _, n := range newNodes { wg.Add(1) go func(node *ProxyNode) { defer wg.Done() checkNode(node, sem) }(n) } wg.Wait() }() mainWg.Wait() // 按延迟排序(延迟低的排前面) sort.Slice(healthy, func(i, j int) bool { return healthy[i].Latency < healthy[j].Latency }) pm.mu.Lock() pm.healthyNodes = healthy pm.healthChecking = false // 只有达到最少健康节点数才提示就绪 pm.ready = len(healthy) >= MinHealthyForReady pm.readyCond.Broadcast() pm.mu.Unlock() // 输出健康检查结果 uniqueIPs := len(ipSeen) if len(healthy) > 0 { topN := 5 if len(healthy) < topN { topN = len(healthy) } log.Printf("✅ 健康检查完成: %d/%d 节点可用 ", len(healthy), total) log.Printf("📊 最快前%d节点: %v ~ %v", topN, healthy[0].Latency.Round(time.Millisecond), healthy[topN-1].Latency.Round(time.Millisecond)) // 输出IP分布信息 if uniqueIPs < len(healthy) { } } else { log.Printf("⚠️ 健康检查完成: 0/%d 节点可用", total) } // 就绪状态提示 if pm.ready { log.Printf("🟢 代理池就绪 (健康节点: %d >= 最低要求: %d)", len(healthy), MinHealthyForReady) } else { log.Printf("🔴 代理池未就绪 (健康节点: %d < 最低要求: %d)", len(healthy), MinHealthyForReady) } } // GetFromPool 从实例池获取一个空闲实例 func (pm *ProxyManager) GetFromPool() *ProxyInstance { pm.mu.Lock() defer pm.mu.Unlock() // 查找空闲实例 for _, inst := range pm.instancePool { inst.mu.Lock() if inst.status == InstanceStatusIdle && inst.running { inst.status = InstanceStatusInUse inst.lastUsed = time.Now() inst.mu.Unlock() return inst } inst.mu.Unlock() } return nil } // ReturnToPool 归还实例到池 func (pm *ProxyManager) ReturnToPool(inst *ProxyInstance) { if inst == nil { return } inst.mu.Lock() inst.status = InstanceStatusIdle inst.mu.Unlock() } // ReleaseByURL 通过proxyURL停止并释放实例 func (pm *ProxyManager) ReleaseByURL(proxyURL string) { pm.mu.Lock() // 查找并移除实例 var toStop *ProxyInstance for i, inst := range pm.instancePool { inst.mu.Lock() if inst.proxyURL == proxyURL { toStop = inst // 从池中移除 pm.instancePool = append(pm.instancePool[:i], pm.instancePool[i+1:]...) inst.mu.Unlock() break } inst.mu.Unlock() } pm.mu.Unlock() // 停止实例(在锁外执行) if toStop != nil { pm.StopXray(toStop.localPort) } } func (pm *ProxyManager) Next() string { pm.mu.Lock() defer pm.mu.Unlock() if len(pm.healthyNodes) == 0 && len(pm.nodes) == 0 { return "" } now := time.Now() var selectedNode *ProxyNode var selectedIdx int = -1 // 从健康节点列表中找第一个可用的 for i, node := range pm.healthyNodes { // 检查冷却时间 cooldown := node.UseCooldown if cooldown == 0 { cooldown = DefaultProxyUseCooldown } if now.Sub(node.LastUsed) < cooldown { continue } // 跳过失败次数过多的节点 if node.FailCount >= MaxProxyFailCount { continue } selectedNode = node selectedIdx = i break } // 如果健康节点都不可用,尝试普通节点 if selectedNode == nil { for i, node := range pm.nodes { cooldown := node.UseCooldown if cooldown == 0 { cooldown = DefaultProxyUseCooldown } if now.Sub(node.LastUsed) < cooldown { continue } if node.FailCount >= MaxProxyFailCount { continue } selectedNode = node selectedIdx = i break } } // 如果所有节点都在冷却中,选择最久未用的健康节点 if selectedNode == nil { var oldest *ProxyNode var oldestIdx int for i, node := range pm.healthyNodes { if oldest == nil || node.LastUsed.Before(oldest.LastUsed) { oldest = node oldestIdx = i } } if oldest == nil { for i, node := range pm.nodes { if oldest == nil || node.LastUsed.Before(oldest.LastUsed) { oldest = node oldestIdx = i } } } if oldest == nil { return "" } selectedNode = oldest selectedIdx = oldestIdx } selectedNode.LastUsed = now isFromHealthy := false for i, node := range pm.healthyNodes { if node == selectedNode { isFromHealthy = true selectedIdx = i break } } if isFromHealthy && selectedIdx >= 0 && selectedIdx < len(pm.healthyNodes) { // 从健康节点列表移动到末尾 pm.healthyNodes = append(pm.healthyNodes[:selectedIdx], pm.healthyNodes[selectedIdx+1:]...) pm.healthyNodes = append(pm.healthyNodes, selectedNode) } else if !isFromHealthy { // 从普通节点列表移动到末尾 for i, node := range pm.nodes { if node == selectedNode { pm.nodes = append(pm.nodes[:i], pm.nodes[i+1:]...) pm.nodes = append(pm.nodes, selectedNode) break } } } // 启动新实例 instance, err := pm.startInstanceLocked(selectedNode) if err != nil { log.Printf("⚠️ 启动代理失败: %v", err) selectedNode.FailCount++ // 失败后增加冷却时间 selectedNode.UseCooldown = time.Duration(selectedNode.FailCount) * 10 * time.Second return "" } instance.status = InstanceStatusInUse // 始终追踪实例(用于 MarkProxyFailed/ReleaseByURL) pm.instancePool = append(pm.instancePool, instance) return instance.proxyURL } // MarkProxyFailed 标记代理失败(如403等) func (pm *ProxyManager) MarkProxyFailed(proxyURL string) { pm.mu.Lock() defer pm.mu.Unlock() // 从实例池找到对应节点 for _, inst := range pm.instancePool { if inst.proxyURL == proxyURL && inst.node != nil { inst.node.FailCount++ // 失败后动态增加冷却时间 inst.node.UseCooldown = time.Duration(inst.node.FailCount) * 15 * time.Second if inst.node.UseCooldown > 2*time.Minute { inst.node.UseCooldown = 2 * time.Minute } log.Printf("⚠️ 代理失败标记: %s, 失败次数=%d, 冷却=%v", inst.node.Name, inst.node.FailCount, inst.node.UseCooldown) return } } } // MarkProxySuccess 标记代理成功(重置失败计数) func (pm *ProxyManager) MarkProxySuccess(proxyURL string) { pm.mu.Lock() defer pm.mu.Unlock() for _, inst := range pm.instancePool { if inst.proxyURL == proxyURL && inst.node != nil { inst.node.FailCount = 0 inst.node.UseCooldown = DefaultProxyUseCooldown return } } } // PoolStats 返回实例池统计 func (pm *ProxyManager) PoolStats() map[string]int { pm.mu.RLock() defer pm.mu.RUnlock() idle, inUse := 0, 0 for _, inst := range pm.instancePool { inst.mu.Lock() switch inst.status { case InstanceStatusIdle: idle++ case InstanceStatusInUse: inUse++ } inst.mu.Unlock() } return map[string]int{ "idle": idle, "in_use": inUse, "total": len(pm.instancePool), } } // Count 获取代理数量 func (pm *ProxyManager) Count() int { pm.mu.RLock() defer pm.mu.RUnlock() if len(pm.healthyNodes) > 0 { return len(pm.healthyNodes) } return len(pm.nodes) } // HealthyCount 获取健康代理数量 func (pm *ProxyManager) HealthyCount() int { pm.mu.RLock() defer pm.mu.RUnlock() return len(pm.healthyNodes) } // TotalCount 获取总代理数量 func (pm *ProxyManager) TotalCount() int { pm.mu.RLock() defer pm.mu.RUnlock() return len(pm.nodes) } // StartAutoUpdate 启动自动更新和健康检查 func (pm *ProxyManager) StartAutoUpdate() { // 自动更新订阅 go func() { for { time.Sleep(pm.updateInterval) if len(pm.subscribeURLs) > 0 || len(pm.proxyFiles) > 0 { if err := pm.LoadAll(); err != nil { log.Printf("⚠️ 自动更新代理失败: %v", err) } } } }() // 后台健康检查(启动时立即开始,不阻塞) go func() { // 延迟几秒后开始首次检查 time.Sleep(3 * time.Second) pm.CheckAllHealth() // 定期检查 for { time.Sleep(pm.checkInterval) pm.CheckAllHealth() } }() } // SetProxies 直接设置代理(兼容旧接口) func (pm *ProxyManager) SetProxies(proxies []string) { var nodes []*ProxyNode for _, p := range proxies { if node := pm.parseLine(p); node != nil { nodes = append(nodes, node) } } pm.mu.Lock() pm.nodes = nodes pm.healthyNodes = nodes // 假设都健康 pm.mu.Unlock() log.Printf("✅ 代理池已设置 %d 个代理", len(nodes)) } const ( autoRegisterURL = "https://jgpyjc.top/api/v1/passport/auth/register" autoSubscribeBaseURL = "https://bb1.jgpyjc.top/api/v1/client/subscribe?token=" autoRegisterInterval = 1 * time.Hour ) // AutoSubscriber 自动订阅管理器 type AutoSubscriber struct { mu sync.RWMutex currentToken string subscribeURL string lastRefresh time.Time running bool stopChan chan struct{} proxyManager *ProxyManager refreshInterval time.Duration } var autoSubscriber = &AutoSubscriber{ refreshInterval: autoRegisterInterval, stopChan: make(chan struct{}), } // randString 生成随机字符串 func randString(n int) string { const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" out := make([]byte, n) for i := 0; i < n; i++ { r, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letters)))) out[i] = letters[r.Int64()] } return string(out) } // ungzipIfNeeded 解压 gzip 数据 func ungzipIfNeeded(data []byte, header http.Header) ([]byte, error) { ce := strings.ToLower(header.Get("Content-Encoding")) if ce == "gzip" || (len(data) >= 2 && data[0] == 0x1f && data[1] == 0x8b) { r, err := gzip.NewReader(bytes.NewReader(data)) if err != nil { return nil, err } defer r.Close() return io.ReadAll(r) } return data, nil } // extractToken 从响应中提取 token func extractToken(body []byte) string { var j interface{} if err := json.Unmarshal(body, &j); err != nil { return "" } var walk func(interface{}) string walk = func(x interface{}) string { switch v := x.(type) { case map[string]interface{}: for _, key := range []string{"token", "access_token", "data", "result", "auth", "jwt"} { if val, ok := v[key]; ok { if s, ok2 := val.(string); ok2 && s != "" { return s } if res := walk(val); res != "" { return res } } } // 检查 JWT 格式 for _, val := range v { if s, ok := val.(string); ok && looksLikeJWT(s) { return s } } case []interface{}: for _, item := range v { if res := walk(item); res != "" { return res } } } return "" } return walk(j) } // looksLikeJWT 判断是否像 JWT func looksLikeJWT(s string) bool { parts := strings.Count(s, ".") return parts >= 2 && len(s) > 30 } // 常见邮箱域名 var emailDomains = []string{ "gmail.com", "yahoo.com", "outlook.com", "hotmail.com", "icloud.com", "protonmail.com", "mail.com", "zoho.com", "aol.com", "yandex.com", "163.com", "qq.com", "126.com", "sina.com", "foxmail.com", } // doAutoRegister 执行一次自动注册 func doAutoRegister() (email, password, token string, err error) { // 随机邮箱:随机用户名 + 随机域名 domainIdx, _ := rand.Int(rand.Reader, big.NewInt(int64(len(emailDomains)))) email = randString(8+int(domainIdx.Int64()%5)) + "@" + emailDomains[domainIdx.Int64()] password = randString(20) form := url.Values{} form.Set("email", email) form.Set("password", password) form.Set("invite_code", "odtRDsfd") form.Set("email_code", "") req, err := http.NewRequest("POST", autoRegisterURL, strings.NewReader(form.Encode())) if err != nil { return "", "", "", err } req.Header.Set("User-Agent", "Mozilla/5.0 (Linux; Android 10)") req.Header.Set("Accept-Encoding", "gzip") req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Origin", "https://jgpyjc.top") req.Header.Set("Referer", "https://jgpyjc.top/") client := &http.Client{Timeout: 30 * time.Second} resp, err := client.Do(req) if err != nil { return email, password, "", err } defer resp.Body.Close() raw, err := io.ReadAll(resp.Body) if err != nil { return email, password, "", err } body, err := ungzipIfNeeded(raw, resp.Header) if err != nil { body = raw } token = extractToken(body) if token == "" { s := strings.TrimSpace(string(body)) if looksLikeJWT(s) { token = s } } if token == "" { return email, password, "", fmt.Errorf("未能从响应中提取 token: %s", string(body[:min(200, len(body))])) } return email, password, token, nil } // refreshSubscription 刷新订阅 func (as *AutoSubscriber) refreshSubscription() error { _, _, token, err := doAutoRegister() if err != nil { return fmt.Errorf("注册失败: %w", err) } subscribeURL := autoSubscribeBaseURL + token as.mu.Lock() as.currentToken = token as.subscribeURL = subscribeURL as.lastRefresh = time.Now() as.mu.Unlock() // 加载订阅到代理池 if as.proxyManager != nil { if err := as.loadToProxyManager(); err != nil { } } return nil } func (as *AutoSubscriber) loadToProxyManager() error { as.mu.RLock() subURL := as.subscribeURL as.mu.RUnlock() if subURL == "" { return fmt.Errorf("订阅URL为空") } nodes, err := as.proxyManager.loadFromURL(subURL) if err != nil { return err } if len(nodes) == 0 { return fmt.Errorf("订阅中没有可用节点") } as.proxyManager.mu.Lock() as.proxyManager.nodes = append(as.proxyManager.nodes, nodes...) as.proxyManager.mu.Unlock() go as.proxyManager.CheckAllHealth() return nil } func (as *AutoSubscriber) Start(pm *ProxyManager) { as.mu.Lock() if as.running { as.mu.Unlock() return } as.running = true as.proxyManager = pm as.stopChan = make(chan struct{}) as.mu.Unlock() go func() { if err := as.refreshSubscription(); err != nil { } ticker := time.NewTicker(as.refreshInterval) defer ticker.Stop() for { select { case <-as.stopChan: return case <-ticker.C: if err := as.refreshSubscription(); err != nil { } } } }() } func (as *AutoSubscriber) Stop() { as.mu.Lock() defer as.mu.Unlock() if as.running { close(as.stopChan) as.running = false } } func (as *AutoSubscriber) GetCurrentSubscribeURL() string { as.mu.RLock() defer as.mu.RUnlock() return as.subscribeURL } func (as *AutoSubscriber) GetCurrentToken() string { as.mu.RLock() defer as.mu.RUnlock() return as.currentToken } func (as *AutoSubscriber) IsExpired() bool { as.mu.RLock() defer as.mu.RUnlock() return time.Since(as.lastRefresh) > 2*time.Hour } func (pm *ProxyManager) StartAutoSubscribe() { autoSubscriber.Start(pm) } func (pm *ProxyManager) StopAutoSubscribe() { autoSubscriber.Stop() } func (pm *ProxyManager) GetAutoSubscribeURL() string { return autoSubscriber.GetCurrentSubscribeURL() } func (pm *ProxyManager) HasAutoSubscribe() bool { return autoSubscriber.GetCurrentToken() != "" } ================================================ FILE: src/proxy/singbox.go ================================================ package proxy import ( "context" "encoding/json" "fmt" "net" "net/http" "net/url" "strconv" "strings" "sync" "time" box "github.com/sagernet/sing-box" "github.com/sagernet/sing-box/include" "github.com/sagernet/sing-box/option" // 启用 QUIC 协议支持(hysteria, hysteria2, tuic) _ "github.com/sagernet/sing-quic/hysteria" _ "github.com/sagernet/sing-quic/hysteria2" _ "github.com/sagernet/sing-quic/tuic" ) type SingboxManager struct { mu sync.Mutex instances map[int]*SingboxInstance basePort int ready bool } // SingboxInstance sing-box 实例 type SingboxInstance struct { Port int Box *box.Box Ctx context.Context Cancel context.CancelFunc Running bool ProxyURL string Node *ProxyNode } var singboxMgr = &SingboxManager{ instances: make(map[int]*SingboxInstance), basePort: 11800, ready: true, } // IsSingboxProtocol 所有协议都由 sing-box 处理 func IsSingboxProtocol(protocol string) bool { switch protocol { case "vmess", "vless", "shadowsocks", "trojan", "socks", "http", "hysteria", "hysteria2", "hy2", "tuic", "wireguard", "anytls": return true } return false } func CanSingboxHandle(protocol string) bool { return IsSingboxProtocol(protocol) } func (sm *SingboxManager) IsAvailable() bool { return sm.ready } // 连通性测试备选URL列表 var connectivityTestURLs = []string{ "https://cp.cloudflare.com/generate_204", } func (sm *SingboxManager) StartRaw(node *ProxyNode) (string, error) { sm.mu.Lock() defer sm.mu.Unlock() return sm.startInternal(node, false) } // Start 启动代理并验证连通性(用于需要立即可用的场景) func (sm *SingboxManager) Start(node *ProxyNode) (string, error) { sm.mu.Lock() defer sm.mu.Unlock() return sm.startInternal(node, true) } // startInternal 内部启动方法 func (sm *SingboxManager) startInternal(node *ProxyNode, doTest bool) (string, error) { // 分配端口 port := sm.findAvailablePort() if port == 0 { return "", fmt.Errorf("无可用端口") } configJSON := sm.generateConfigJSON(node, port) ctx, cancel := context.WithCancel(context.Background()) ctx = box.Context(ctx, include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), include.DNSTransportRegistry(), include.ServiceRegistry()) var opts option.Options err := opts.UnmarshalJSONContext(ctx, []byte(configJSON)) if err != nil { cancel() return "", fmt.Errorf("解析配置失败: %w", err) } singBox, err := box.New(box.Options{ Context: ctx, Options: opts, }) if err != nil { cancel() return "", fmt.Errorf("创建 sing-box 失败: %w", err) } if err := singBox.Start(); err != nil { cancel() return "", fmt.Errorf("启动 sing-box 失败: %w", err) } // 等待端口就绪(使用渐进式等待,更快响应) proxyURL := fmt.Sprintf("http://127.0.0.1:%d", port) portReady := false waitTimes := []time.Duration{10, 20, 30, 50, 50, 100, 100, 100, 150, 150} // 总计约760ms for _, wait := range waitTimes { time.Sleep(wait * time.Millisecond) conn, err := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", port), 100*time.Millisecond) if err == nil { conn.Close() portReady = true break } } if !portReady { singBox.Close() cancel() return "", fmt.Errorf("端口 %d 未就绪", port) } // 如果需要连通性测试 if doTest { if err := sm.testConnectivity(proxyURL); err != nil { singBox.Close() cancel() return "", err } } instance := &SingboxInstance{ Port: port, Box: singBox, Ctx: ctx, Cancel: cancel, Running: true, ProxyURL: proxyURL, Node: node, } sm.instances[port] = instance node.LocalPort = port return proxyURL, nil } // testConnectivity 测试代理连通性(支持多URL重试) func (sm *SingboxManager) testConnectivity(proxyURL string) error { proxyURLParsed, _ := url.Parse(proxyURL) testClient := &http.Client{ Transport: &http.Transport{ Proxy: http.ProxyURL(proxyURLParsed), }, Timeout: 8 * time.Second, } var lastErr error for _, testURL := range connectivityTestURLs { resp, err := testClient.Get(testURL) if err != nil { lastErr = err continue } resp.Body.Close() if resp.StatusCode == 204 || resp.StatusCode == 200 { return nil // 成功 } lastErr = fmt.Errorf("状态码: %d", resp.StatusCode) } return fmt.Errorf("连通性测试失败: %w", lastErr) } // Stop 停止实例 func (sm *SingboxManager) Stop(port int) { sm.mu.Lock() defer sm.mu.Unlock() if inst, ok := sm.instances[port]; ok { if inst.Box != nil { inst.Box.Close() } if inst.Cancel != nil { inst.Cancel() } inst.Running = false delete(sm.instances, port) } } // StopAll 停止所有实例 func (sm *SingboxManager) StopAll() { sm.mu.Lock() defer sm.mu.Unlock() for port, inst := range sm.instances { if inst.Box != nil { inst.Box.Close() } if inst.Cancel != nil { inst.Cancel() } delete(sm.instances, port) } } func (sm *SingboxManager) findAvailablePort() int { for port := sm.basePort; port < sm.basePort+1000; port++ { if _, exists := sm.instances[port]; !exists { conn, err := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", port), 50*time.Millisecond) if err != nil { return port } conn.Close() } } return 0 } // generateConfigJSON 生成 sing-box JSON 配置 func (sm *SingboxManager) generateConfigJSON(node *ProxyNode, localPort int) string { outbound := sm.buildOutboundJSON(node) config := map[string]interface{}{ "log": map[string]interface{}{ "disabled": true, }, "inbounds": []map[string]interface{}{ { "type": "http", "tag": "http-in", "listen": "127.0.0.1", "listen_port": localPort, }, }, "outbounds": []interface{}{ outbound, }, } data, _ := json.Marshal(config) return string(data) } // buildOutboundJSON 构建 outbound JSON 配置 func (sm *SingboxManager) buildOutboundJSON(node *ProxyNode) map[string]interface{} { sni := node.SNI if sni == "" { sni = node.Server } switch node.Protocol { case "hysteria2", "hy2": return map[string]interface{}{ "type": "hysteria2", "tag": "proxy", "server": node.Server, "server_port": node.Port, "password": node.Password, "tls": map[string]interface{}{ "enabled": true, "insecure": true, "server_name": sni, }, } case "hysteria": return map[string]interface{}{ "type": "hysteria", "tag": "proxy", "server": node.Server, "server_port": node.Port, "auth_str": node.Password, "tls": map[string]interface{}{ "enabled": true, "insecure": true, "server_name": sni, }, } case "tuic": return map[string]interface{}{ "type": "tuic", "tag": "proxy", "server": node.Server, "server_port": node.Port, "uuid": node.UUID, "password": node.Password, "tls": map[string]interface{}{ "enabled": true, "insecure": true, "server_name": sni, }, } case "vmess": out := map[string]interface{}{ "type": "vmess", "tag": "proxy", "server": node.Server, "server_port": node.Port, "uuid": node.UUID, "security": node.Security, "alter_id": node.AlterId, } if node.TLS { out["tls"] = map[string]interface{}{ "enabled": true, "insecure": true, "server_name": sni, } } if transport := sm.buildTransportJSON(node); transport != nil { out["transport"] = transport } return out case "vless": out := map[string]interface{}{ "type": "vless", "tag": "proxy", "server": node.Server, "server_port": node.Port, "uuid": node.UUID, } if node.Flow != "" { out["flow"] = node.Flow } if node.Security == "reality" { fp := node.Fingerprint if fp == "" { fp = "chrome" } out["tls"] = map[string]interface{}{ "enabled": true, "server_name": node.SNI, "reality": map[string]interface{}{ "enabled": true, "public_key": node.PublicKey, "short_id": node.ShortId, }, "utls": map[string]interface{}{ "enabled": true, "fingerprint": fp, }, } } else if node.TLS { out["tls"] = map[string]interface{}{ "enabled": true, "insecure": true, "server_name": sni, } } if transport := sm.buildTransportJSON(node); transport != nil { out["transport"] = transport } return out case "shadowsocks": return map[string]interface{}{ "type": "shadowsocks", "tag": "proxy", "server": node.Server, "server_port": node.Port, "method": node.Method, "password": node.Password, } case "trojan": out := map[string]interface{}{ "type": "trojan", "tag": "proxy", "server": node.Server, "server_port": node.Port, "password": node.Password, "tls": map[string]interface{}{ "enabled": true, "insecure": true, "server_name": sni, }, } if transport := sm.buildTransportJSON(node); transport != nil { out["transport"] = transport } return out default: return map[string]interface{}{ "type": "direct", "tag": "proxy", } } } // buildTransportJSON 构建传输层 JSON 配置 func (sm *SingboxManager) buildTransportJSON(node *ProxyNode) map[string]interface{} { switch node.Network { case "ws": transport := map[string]interface{}{ "type": "ws", "path": node.Path, } if node.Host != "" { transport["headers"] = map[string]string{ "Host": node.Host, } } return transport case "grpc": return map[string]interface{}{ "type": "grpc", "service_name": node.Path, } case "httpupgrade": return map[string]interface{}{ "type": "httpupgrade", "path": node.Path, "host": node.Host, } case "h2", "http": return map[string]interface{}{ "type": "http", "path": node.Path, "host": []string{node.Host}, } } return nil } // GetSingboxManager 获取 sing-box 管理器 func GetSingboxManager() *SingboxManager { return singboxMgr } // InitSingbox 初始化 sing-box(内置 core 无需初始化) func InitSingbox() { } // TrySingboxStart 尝试用 sing-box 启动节点(xray 失败时的回退) func TrySingboxStart(node *ProxyNode) (string, error) { if !CanSingboxHandle(node.Protocol) { return "", fmt.Errorf("sing-box 不支持协议: %s", node.Protocol) } return singboxMgr.Start(node) } // StopSingbox 停止指定端口的 sing-box 实例 func StopSingbox(port int) { singboxMgr.Stop(port) } // ParseProxyLinkWithSingbox 使用 sing-box 解析代理链接 func ParseProxyLinkWithSingbox(link string) *ProxyNode { node := Manager.parseLine(link) if node != nil { return node } link = strings.TrimSpace(link) if strings.HasPrefix(link, "hy2://") || strings.HasPrefix(link, "hysteria2://") { return parseHysteria2(link) } if strings.HasPrefix(link, "tuic://") { return parseTUIC(link) } return nil } // parseTUIC 解析 TUIC 链接 func parseTUIC(link string) *ProxyNode { origLink := link link = strings.TrimPrefix(link, "tuic://") var name string if idx := strings.LastIndex(link, "#"); idx != -1 { name, _ = url.QueryUnescape(link[idx+1:]) link = link[:idx] } var params string if idx := strings.Index(link, "?"); idx != -1 { params = link[idx+1:] link = link[:idx] } atIdx := strings.LastIndex(link, "@") if atIdx == -1 { return nil } userPart := link[:atIdx] hostPart := link[atIdx+1:] var uuid, password string if colonIdx := strings.Index(userPart, ":"); colonIdx != -1 { uuid = userPart[:colonIdx] password = userPart[colonIdx+1:] } else { uuid = userPart } var server string var port int if lastColon := strings.LastIndex(hostPart, ":"); lastColon != -1 { server = hostPart[:lastColon] port, _ = strconv.Atoi(hostPart[lastColon+1:]) } node := &ProxyNode{ Protocol: "tuic", Name: name, Server: server, Port: port, UUID: uuid, Password: password, Raw: origLink, } for _, param := range strings.Split(params, "&") { if kv := strings.SplitN(param, "=", 2); len(kv) == 2 { switch kv[0] { case "sni": node.SNI = kv[1] case "alpn": node.ALPN = kv[1] } } } if node.Server == "" || node.Port == 0 { return nil } return node } ================================================ FILE: src/register/browser.go ================================================ package register import ( "encoding/base64" "encoding/json" "fmt" "log" "math/rand" "net/http" "os" "os/exec" "path/filepath" "regexp" "runtime" "strings" "sync" "sync/atomic" "time" "business2api/src/logger" "business2api/src/pool" "github.com/go-rod/rod" "github.com/go-rod/rod/lib/input" "github.com/go-rod/rod/lib/launcher" "github.com/go-rod/rod/lib/proto" ) var ( RegisterDebug bool RegisterOnce bool httpClient *http.Client GetProxy func() string ReleaseProxy func(proxyURL string) // 释放代理的函数 firstNames = []string{"John", "Jane", "Michael", "Sarah", "David", "Emily", "Robert", "Lisa", "James", "Emma"} lastNames = []string{"Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", "Wilson", "Taylor"} commonWords = map[string]bool{ "VERIFY": true, "GOOGLE": true, "UPDATE": true, "MOBILE": true, "DEVICE": true, "SUBMIT": true, "RESEND": true, "CANCEL": true, "DELETE": true, "REMOVE": true, "SEARCH": true, "VIDEOS": true, "IMAGES": true, "GMAIL": true, "EMAIL": true, "ACCOUNT": true, "CHROME": true, } ) // SetHTTPClient 设置HTTP客户端 func SetHTTPClient(c *http.Client) { httpClient = c } func readResponseBody(resp *http.Response) ([]byte, error) { defer resp.Body.Close() var reader = resp.Body if resp.Header.Get("Content-Encoding") == "gzip" { } body := make([]byte, 0) buf := make([]byte, 4096) for { n, err := reader.Read(buf) if n > 0 { body = append(body, buf[:n]...) } if err != nil { break } } return body, nil } type TempEmailResponse struct { Email string `json:"email"` Data struct { Email string `json:"email"` } `json:"data"` } type EmailListResponse struct { Success bool `json:"success"` Data struct { Emails []EmailContent `json:"emails"` } `json:"data"` } type EmailContent struct { Subject string `json:"subject"` Content string `json:"content"` } type BrowserRegisterResult struct { Success bool Email string FullName string Authorization string Cookies []pool.Cookie ConfigID string CSESIDX string Error error } func generateRandomName() string { return firstNames[rand.Intn(len(firstNames))] + " " + lastNames[rand.Intn(len(lastNames))] } // ==================== 拟人化操作工具函数 ==================== // humanDelay 随机延迟(模拟人类反应时间) func humanDelay(minMs, maxMs int) { if minMs >= maxMs { maxMs = minMs + 1 } delay := minMs + rand.Intn(maxMs-minMs) time.Sleep(time.Duration(delay) * time.Millisecond) } // humanMouseMove 模拟人类鼠标移动到元素(贝塞尔曲线轨迹) func humanMouseMove(page *rod.Page, el *rod.Element) { if page == nil || el == nil { return } box, err := el.Shape() if err != nil || box == nil || len(box.Quads) == 0 { return } quad := box.Quads[0] if len(quad) < 8 { return } // 计算元素中心点(添加随机偏移) centerX := (quad[0] + quad[2] + quad[4] + quad[6]) / 4 centerY := (quad[1] + quad[3] + quad[5] + quad[7]) / 4 offsetX := float64(rand.Intn(10) - 5) offsetY := float64(rand.Intn(10) - 5) // 模拟贝塞尔曲线移动(分多步) steps := 5 + rand.Intn(5) for i := 1; i <= steps; i++ { progress := float64(i) / float64(steps) // 使用 ease-out 曲线 eased := 1 - (1-progress)*(1-progress) x := centerX*eased + offsetX y := centerY*eased + offsetY page.Mouse.MoveTo(proto.Point{X: x, Y: y}) time.Sleep(time.Duration(10+rand.Intn(20)) * time.Millisecond) } } // humanClick 拟人化点击元素 func humanClick(page *rod.Page, el *rod.Element) error { if page == nil || el == nil { return fmt.Errorf("page or element is nil") } // 1. 先移动鼠标到元素附近 humanMouseMove(page, el) humanDelay(50, 150) // 2. 点击前短暂停顿(模拟人类犹豫) humanDelay(30, 100) // 3. 执行点击 err := el.Click(proto.InputMouseButtonLeft, 1) // 4. 点击后短暂停顿 humanDelay(80, 200) return err } // humanType 拟人化打字(自然节奏,有随机停顿) func humanType(page *rod.Page, text string) { if page == nil || text == "" { return } for i, char := range text { page.Keyboard.Type(input.Key(char)) // 基础延迟 + 随机变化 baseDelay := 50 + rand.Intn(80) // 偶尔有较长停顿(模拟思考) if rand.Float32() < 0.1 { baseDelay += 150 + rand.Intn(200) } // 某些字符后停顿更长(如空格、标点) if char == ' ' || char == '.' || char == '@' { baseDelay += 30 + rand.Intn(50) } time.Sleep(time.Duration(baseDelay) * time.Millisecond) // 每 8-12 个字符偶尔短暂休息 if i > 0 && i%(8+rand.Intn(5)) == 0 { humanDelay(100, 300) } } } // humanScrollToElement 拟人化滚动到元素 func humanScrollToElement(page *rod.Page, el *rod.Element) { if el == nil { return } humanDelay(100, 300) el.ScrollIntoView() humanDelay(200, 400) } // humanFocusInput 拟人化聚焦输入框 func humanFocusInput(page *rod.Page, el *rod.Element) error { if page == nil || el == nil { return fmt.Errorf("page or element is nil") } // 滚动到元素 humanScrollToElement(page, el) // 点击聚焦 return humanClick(page, el) } type TempMailProvider struct { Name string GenerateURL string CheckURL string Headers map[string]string } // 支持的临时邮箱提供商列表 var tempMailProviders = []TempMailProvider{ { Name: "chatgpt.org.uk", GenerateURL: "https://mail.chatgpt.org.uk/api/generate-email", CheckURL: "https://mail.chatgpt.org.uk/api/emails?email=%s", Headers: map[string]string{ "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36", "Referer": "https://mail.chatgpt.org.uk", }, }, // 备用邮箱服务可以在这里添加 } func getTemporaryEmail() (string, error) { var lastErr error for _, provider := range tempMailProviders { for retry := 0; retry < 3; retry++ { email, err := getEmailFromProvider(provider) if err != nil { lastErr = err if retry < 2 { log.Printf("⚠️ 临时邮箱 %s 失败 (重试 %d/3): %v", provider.Name, retry+1, err) time.Sleep(time.Duration(retry+1) * time.Second) continue } log.Printf("⚠️ 临时邮箱 %s 失败,尝试下一个提供商", provider.Name) break } if !strings.Contains(email, "@") { lastErr = fmt.Errorf("邮箱格式无效: %s", email) continue } return email, nil } } return "", fmt.Errorf("所有临时邮箱服务均失败: %v", lastErr) } func getEmailFromProvider(provider TempMailProvider) (string, error) { req, _ := http.NewRequest("GET", provider.GenerateURL, nil) for k, v := range provider.Headers { req.Header.Set(k, v) } client := &http.Client{Timeout: 30 * time.Second} if httpClient != nil { client = httpClient } resp, err := client.Do(req) if err != nil { return "", fmt.Errorf("请求失败: %w", err) } defer resp.Body.Close() if resp.StatusCode != 200 { return "", fmt.Errorf("HTTP %d", resp.StatusCode) } body, err := readResponseBody(resp) if err != nil { return "", fmt.Errorf("读取响应失败: %w", err) } var result TempEmailResponse if err := json.Unmarshal(body, &result); err != nil { return "", fmt.Errorf("解析响应失败: %w, body: %s", err, string(body[:min(100, len(body))])) } email := result.Email if email == "" { email = result.Data.Email } if email == "" { return "", fmt.Errorf("返回的邮箱为空, 响应: %s", string(body[:min(100, len(body))])) } return email, nil } func getEmailCount(email string) int { for retry := 0; retry < 3; retry++ { req, _ := http.NewRequest("GET", fmt.Sprintf("https://mail.chatgpt.org.uk/api/emails?email=%s", email), nil) req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36") req.Header.Set("Referer", "https://mail.chatgpt.org.uk") client := &http.Client{Timeout: 15 * time.Second} if httpClient != nil { client = httpClient } resp, err := client.Do(req) if err != nil { time.Sleep(time.Second) continue } body, _ := readResponseBody(resp) var result EmailListResponse if err := json.Unmarshal(body, &result); err != nil { continue } return len(result.Data.Emails) } return 0 } type VerificationState struct { UsedCodes map[string]bool // 已使用过的验证码 LastEmailID string // 上次处理的邮件ID ResendCount int // 重发次数 LastResendAt time.Time // 上次重发时间 mu sync.Mutex } func NewVerificationState() *VerificationState { return &VerificationState{ UsedCodes: make(map[string]bool), } } func (vs *VerificationState) MarkCodeUsed(code string) { vs.mu.Lock() defer vs.mu.Unlock() vs.UsedCodes[code] = true } func (vs *VerificationState) IsCodeUsed(code string) bool { vs.mu.Lock() defer vs.mu.Unlock() return vs.UsedCodes[code] } func (vs *VerificationState) CanResend() bool { vs.mu.Lock() defer vs.mu.Unlock() if vs.ResendCount >= 3 { return false } if time.Since(vs.LastResendAt) < 10*time.Second { return false } return true } // RecordResend 记录重发 func (vs *VerificationState) RecordResend() { vs.mu.Lock() defer vs.mu.Unlock() vs.ResendCount++ vs.LastResendAt = time.Now() } func getVerificationEmailQuick(email string, retries int, intervalSec int) (*EmailContent, error) { return getVerificationEmailAfter(email, retries, intervalSec, 0) } func getVerificationEmailAfter(email string, retries int, intervalSec int, initialCount int) (*EmailContent, error) { return getVerificationEmailWithState(email, retries, intervalSec, initialCount, nil) } func getVerificationEmailWithState(email string, retries int, intervalSec int, initialCount int, state *VerificationState) (*EmailContent, error) { client := &http.Client{Timeout: 15 * time.Second} if httpClient != nil { client = httpClient } for i := 0; i < retries; i++ { req, _ := http.NewRequest("GET", fmt.Sprintf("https://mail.chatgpt.org.uk/api/emails?email=%s", email), nil) req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36") req.Header.Set("Referer", "https://mail.chatgpt.org.uk") resp, err := client.Do(req) if err != nil { log.Printf("[验证码] 获取邮件列表失败: %v", err) time.Sleep(time.Duration(intervalSec) * time.Second) continue } body, _ := readResponseBody(resp) // readResponseBody 内部会关闭 Body var result EmailListResponse if err := json.Unmarshal(body, &result); err != nil { time.Sleep(time.Duration(intervalSec) * time.Second) continue } if result.Success && len(result.Data.Emails) > initialCount { for idx := 0; idx < len(result.Data.Emails)-initialCount; idx++ { latestEmail := &result.Data.Emails[idx] code, err := extractVerificationCode(latestEmail.Content) if err != nil { continue } if state != nil && state.IsCodeUsed(code) { log.Printf("[验证码] 跳过已使用的验证码: %s", code) continue } return latestEmail, nil } log.Printf("[验证码] 所有新邮件的验证码均已使用,等待新邮件...") } time.Sleep(time.Duration(intervalSec) * time.Second) } return nil, fmt.Errorf("未收到新的验证码邮件") } // PageState 页面状态类型 type PageState int const ( PageStateUnknown PageState = iota PageStateEmailInput PageStateCodeInput PageStateNameInput PageStateLoggedIn PageStateError ) func GetPageState(pageURL string) PageState { if pageURL == "" { return PageStateUnknown } if strings.Contains(pageURL, "accountverification.business.gemini.google") { return PageStateCodeInput } if strings.Contains(pageURL, "auth.business.gemini.google") { return PageStateEmailInput } if strings.Contains(pageURL, "business.gemini.google/admin/create") { return PageStateNameInput } if strings.Contains(pageURL, "business.gemini.google") && !strings.Contains(pageURL, "auth.") && !strings.Contains(pageURL, "accountverification.") && !strings.Contains(pageURL, "/admin/create") { return PageStateLoggedIn } return PageStateUnknown } func GetPageStateString(state PageState) string { switch state { case PageStateEmailInput: return "邮箱输入" case PageStateCodeInput: return "验证码输入" case PageStateNameInput: return "名字输入" case PageStateLoggedIn: return "已登录" case PageStateError: return "错误页面" default: return "未知" } } // WaitForPageState 等待页面达到指定状态 func WaitForPageState(page *rod.Page, targetState PageState, timeout time.Duration) (PageState, error) { start := time.Now() for time.Since(start) < timeout { info, err := page.Info() if err != nil { time.Sleep(500 * time.Millisecond) continue } currentState := GetPageState(info.URL) if currentState == targetState { return currentState, nil } // 如果已经登录,直接返回 if currentState == PageStateLoggedIn { return currentState, nil } time.Sleep(500 * time.Millisecond) } // 超时,返回当前状态 info, _ := page.Info() if info != nil { return GetPageState(info.URL), fmt.Errorf("等待页面状态超时") } return PageStateUnknown, fmt.Errorf("等待页面状态超时") } // 邮箱输入框选择器列表(优先级从高到低) var emailInputSelectors = []string{ "#email-input", "input[name='loginHint']", "input[jsname='YPqjbf']", "input[type='email']", "input[type='text'][aria-label]", "input:not([type='hidden']):not([type='submit']):not([type='checkbox'])", } // 浏览器环境变量列表(按优先级) var browserEnvVars = []string{ "CHROME_PATH", "CHROMIUM_PATH", "EDGE_PATH", "BROWSER_PATH", "GOOGLE_CHROME_BIN", "CHROMIUM_BIN", } // getWindowsBrowserPaths 获取 Windows 浏览器路径列表 func getWindowsBrowserPaths() []string { paths := []string{} // 程序安装目录 programFiles := os.Getenv("ProgramFiles") programFilesX86 := os.Getenv("ProgramFiles(x86)") localAppData := os.Getenv("LOCALAPPDATA") userProfile := os.Getenv("USERPROFILE") // Chrome 路径 chromePaths := []string{ filepath.Join(programFiles, "Google", "Chrome", "Application", "chrome.exe"), filepath.Join(programFilesX86, "Google", "Chrome", "Application", "chrome.exe"), filepath.Join(localAppData, "Google", "Chrome", "Application", "chrome.exe"), filepath.Join(userProfile, "AppData", "Local", "Google", "Chrome", "Application", "chrome.exe"), } paths = append(paths, chromePaths...) // Edge 路径 edgePaths := []string{ filepath.Join(programFiles, "Microsoft", "Edge", "Application", "msedge.exe"), filepath.Join(programFilesX86, "Microsoft", "Edge", "Application", "msedge.exe"), filepath.Join(localAppData, "Microsoft", "Edge", "Application", "msedge.exe"), } paths = append(paths, edgePaths...) // Brave 路径 bravePaths := []string{ filepath.Join(programFiles, "BraveSoftware", "Brave-Browser", "Application", "brave.exe"), filepath.Join(programFilesX86, "BraveSoftware", "Brave-Browser", "Application", "brave.exe"), filepath.Join(localAppData, "BraveSoftware", "Brave-Browser", "Application", "brave.exe"), } paths = append(paths, bravePaths...) // Vivaldi 路径 vivaldiPaths := []string{ filepath.Join(localAppData, "Vivaldi", "Application", "vivaldi.exe"), } paths = append(paths, vivaldiPaths...) // Opera 路径 operaPaths := []string{ filepath.Join(localAppData, "Programs", "Opera", "opera.exe"), filepath.Join(localAppData, "Programs", "Opera GX", "opera.exe"), } paths = append(paths, operaPaths...) return paths } // getLinuxBrowserPaths 获取 Linux 浏览器路径列表 func getLinuxBrowserPaths() []string { return []string{ // Chrome "/usr/bin/google-chrome", "/usr/bin/google-chrome-stable", "/usr/bin/google-chrome-beta", "/usr/bin/google-chrome-unstable", "/opt/google/chrome/chrome", "/opt/google/chrome/google-chrome", // Chromium "/usr/bin/chromium", "/usr/bin/chromium-browser", "/usr/lib/chromium/chromium", "/usr/lib/chromium-browser/chromium-browser", "/snap/bin/chromium", "/snap/chromium/current/usr/lib/chromium-browser/chrome", // Edge "/usr/bin/microsoft-edge", "/usr/bin/microsoft-edge-stable", "/usr/bin/microsoft-edge-beta", "/usr/bin/microsoft-edge-dev", "/opt/microsoft/msedge/msedge", // Brave "/usr/bin/brave-browser", "/usr/bin/brave-browser-stable", "/opt/brave.com/brave/brave-browser", // Vivaldi "/usr/bin/vivaldi", "/usr/bin/vivaldi-stable", // Opera "/usr/bin/opera", } } // getMacOSBrowserPaths 获取 macOS 浏览器路径列表 func getMacOSBrowserPaths() []string { homeDir, _ := os.UserHomeDir() paths := []string{ // Chrome "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary", filepath.Join(homeDir, "Applications", "Google Chrome.app", "Contents", "MacOS", "Google Chrome"), // Chromium "/Applications/Chromium.app/Contents/MacOS/Chromium", // Edge "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge", "/Applications/Microsoft Edge Beta.app/Contents/MacOS/Microsoft Edge Beta", "/Applications/Microsoft Edge Canary.app/Contents/MacOS/Microsoft Edge Canary", // Brave "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser", // Vivaldi "/Applications/Vivaldi.app/Contents/MacOS/Vivaldi", // Opera "/Applications/Opera.app/Contents/MacOS/Opera", } return paths } // getBrowserPathsForOS 根据操作系统获取浏览器路径列表 func getBrowserPathsForOS() []string { switch runtime.GOOS { case "windows": return getWindowsBrowserPaths() case "darwin": return getMacOSBrowserPaths() default: // linux, freebsd, etc. return getLinuxBrowserPaths() } } // findBrowser 查找可用浏览器(完整兼容 Windows/Linux/macOS) func findBrowser() (string, bool) { // 1. 优先检查环境变量 for _, envVar := range browserEnvVars { if path := os.Getenv(envVar); path != "" { // 扩展环境变量 path = expandPath(path) if _, err := os.Stat(path); err == nil { log.Printf("🌐 从环境变量 %s 获取浏览器: %s", envVar, path) return path, true } } } // 2. 检查系统路径(根据操作系统) for _, path := range getBrowserPathsForOS() { expandedPath := expandPath(path) if expandedPath != "" { if _, err := os.Stat(expandedPath); err == nil { log.Printf("🌐 找到浏览器: %s", expandedPath) return expandedPath, true } } } // 3. 尝试通过 which/where 命令查找 if path := findBrowserByCommand(); path != "" { log.Printf("🌐 通过系统命令找到浏览器: %s", path) return path, true } // 4. 尝试通过 PATH 手动查找 browserNames := getBrowserNamesForOS() for _, name := range browserNames { if path, err := findInPath(name); err == nil && path != "" { log.Printf("🌐 从 PATH 找到浏览器: %s", path) return path, true } } return "", false } // expandPath 扩展路径中的环境变量 func expandPath(path string) string { if path == "" { return "" } // 扩展 $VAR 和 ${VAR} 格式 expanded := os.ExpandEnv(path) // Windows 特殊处理: 扩展 %VAR% 格式 if runtime.GOOS == "windows" && strings.Contains(expanded, "%") { for _, env := range os.Environ() { parts := strings.SplitN(env, "=", 2) if len(parts) == 2 { expanded = strings.ReplaceAll(expanded, "%"+parts[0]+"%", parts[1]) } } } return expanded } // getBrowserNamesForOS 获取当前操作系统的浏览器可执行文件名 func getBrowserNamesForOS() []string { if runtime.GOOS == "windows" { return []string{"chrome", "msedge", "brave", "vivaldi", "opera"} } return []string{"google-chrome", "google-chrome-stable", "chromium", "chromium-browser", "microsoft-edge", "brave-browser", "vivaldi"} } // findBrowserByCommand 通过系统命令查找浏览器 func findBrowserByCommand() string { var cmd *exec.Cmd var browsers []string if runtime.GOOS == "windows" { // Windows 使用 where 命令 browsers = []string{"chrome.exe", "msedge.exe", "brave.exe"} for _, browser := range browsers { cmd = exec.Command("where", browser) if output, err := cmd.Output(); err == nil { lines := strings.Split(strings.TrimSpace(string(output)), "\n") if len(lines) > 0 && lines[0] != "" { return strings.TrimSpace(lines[0]) } } } } else { // Unix 使用 which 命令 browsers = []string{"google-chrome", "google-chrome-stable", "chromium", "chromium-browser", "microsoft-edge", "brave-browser"} for _, browser := range browsers { cmd = exec.Command("which", browser) if output, err := cmd.Output(); err == nil { path := strings.TrimSpace(string(output)) if path != "" { return path } } } } return "" } // findInPath 在 PATH 中查找可执行文件 func findInPath(name string) (string, error) { pathEnv := os.Getenv("PATH") var separator string if runtime.GOOS == "windows" { separator = ";" } else { separator = ":" } for _, dir := range strings.Split(pathEnv, separator) { if dir == "" { continue } dir = expandPath(dir) // 根据操作系统构建候选路径 var candidates []string if runtime.GOOS == "windows" { candidates = []string{ filepath.Join(dir, name+".exe"), filepath.Join(dir, name+".cmd"), filepath.Join(dir, name+".bat"), filepath.Join(dir, name), } } else { candidates = []string{ filepath.Join(dir, name), } } for _, path := range candidates { if info, err := os.Stat(path); err == nil && !info.IsDir() { return path, nil } } } return "", fmt.Errorf("not found: %s", name) } // BrowserSession 浏览器会话(封装公共逻辑) type BrowserSession struct { Launcher *launcher.Launcher Browser *rod.Browser Page *rod.Page Authorization string ConfigID string CSESIDX string mu sync.Mutex } func createBrowserSession(headless bool, proxy string, logPrefix string) (*BrowserSession, error) { session := &BrowserSession{} // 启动浏览器 - 使用统一的浏览器查找逻辑 l := launcher.New() if browserPath, found := findBrowser(); found { l = l.Bin(browserPath) log.Printf("%s 使用浏览器: %s", logPrefix, browserPath) } else { log.Printf("%s ⚠️ 未找到系统浏览器,尝试使用 rod 自动下载", logPrefix) } // 配置浏览器启动参数 - 原生反检测,不依赖JS注入 l = configureBrowserLauncher(l, headless, proxy) launcherURL, err := l.Launch() if err != nil { return nil, fmt.Errorf("启动浏览器失败: %w", err) } session.Launcher = l browser := rod.New().ControlURL(launcherURL) if err := browser.Connect(); err != nil { l.Kill() l.Cleanup() return nil, fmt.Errorf("连接浏览器失败: %w", err) } session.Browser = browser.Timeout(120 * time.Second) page, err := session.Browser.Page(proto.TargetCreateTarget{URL: "about:blank"}) if err != nil { session.Close() return nil, fmt.Errorf("创建页面失败: %w", err) } session.Page = page // 设置视口(使用常见分辨率) page.SetViewport(&proto.EmulationSetDeviceMetricsOverride{ Width: 1920, Height: 1080, }) return session, nil } // configureBrowserLauncher 配置浏览器启动参数(原生反检测,无需JS注入) func configureBrowserLauncher(l *launcher.Launcher, headless bool, proxy string) *launcher.Launcher { // 基础参数 l = l.Set("no-sandbox"). Set("disable-setuid-sandbox"). Set("disable-dev-shm-usage"). Set("disable-gpu"). Set("no-first-run"). Set("no-default-browser-check") // 核心反检测参数 - 通过启动参数原生禁用自动化标志 l = l.Set("disable-blink-features", "AutomationControlled"). Delete("enable-automation"). // 删除自动化标志 Set("disable-features", "TranslateUI,AutofillServerCommunication"). Set("disable-ipc-flooding-protection") // 窗口和显示参数 l = l.Set("window-size", "1920,1080"). Set("start-maximized"). Set("lang", "en-US") // 禁用可能暴露自动化的功能 l = l.Set("disable-extensions"). Set("disable-component-extensions-with-background-pages"). Set("disable-background-networking"). Set("disable-sync"). Set("disable-default-apps"). Set("disable-infobars"). Set("disable-hang-monitor"). Set("disable-popup-blocking"). Set("disable-prompt-on-repost"). Set("disable-client-side-phishing-detection"). Set("disable-background-timer-throttling"). Set("disable-renderer-backgrounding"). Set("disable-backgrounding-occluded-windows") // 性能相关参数 l = l.Set("metrics-recording-only"). Set("safebrowsing-disable-auto-update") // Headless 模式配置 if headless { // 使用新版 headless 模式(Chrome 112+),更接近真实浏览器 // 旧的 --headless 模式容易被检测 l = l.Headless(false). // 不使用 rod 的 headless Set("headless", "new") // 使用 Chrome 的新 headless 模式 } else { l = l.Headless(false) } // 代理配置 if proxy != "" { l = l.Proxy(proxy) } return l } // SetupNetworkCapture 设置网络捕获(监听 authorization/configID/csesidx) func (s *BrowserSession) SetupNetworkCapture() { go s.Page.EachEvent(func(e *proto.NetworkRequestWillBeSent) { s.mu.Lock() defer s.mu.Unlock() if auth, ok := e.Request.Headers["authorization"]; ok { if authStr := auth.String(); authStr != "" { s.Authorization = authStr } } url := e.Request.URL if m := regexp.MustCompile(`/cid/([a-f0-9-]+)`).FindStringSubmatch(url); len(m) > 1 && s.ConfigID == "" { s.ConfigID = m[1] } if m := regexp.MustCompile(`[?&]csesidx=(\d+)`).FindStringSubmatch(url); len(m) > 1 && s.CSESIDX == "" { s.CSESIDX = m[1] } })() } // ExtractFromURL 从URL提取 configID 和 csesidx func (s *BrowserSession) ExtractFromURL() { info, _ := s.Page.Info() if info == nil { return } s.mu.Lock() defer s.mu.Unlock() if m := regexp.MustCompile(`/cid/([a-f0-9-]+)`).FindStringSubmatch(info.URL); len(m) > 1 && s.ConfigID == "" { s.ConfigID = m[1] } if m := regexp.MustCompile(`[?&]csesidx=(\d+)`).FindStringSubmatch(info.URL); len(m) > 1 && s.CSESIDX == "" { s.CSESIDX = m[1] } } // ExtractCSESIDXFromAuth 从 authorization 提取 csesidx func (s *BrowserSession) ExtractCSESIDXFromAuth() { s.mu.Lock() defer s.mu.Unlock() if s.CSESIDX == "" && s.Authorization != "" { s.CSESIDX = extractCSESIDXFromAuth(s.Authorization) } } // Close 关闭浏览器会话 func (s *BrowserSession) Close() { if s.Browser != nil { s.Browser.Close() } if s.Launcher != nil { s.Launcher.Kill() s.Launcher.Cleanup() } } // FindEmailInput 查找邮箱输入框 func (s *BrowserSession) FindEmailInput() *rod.Element { for _, sel := range emailInputSelectors { el, err := s.Page.Timeout(2 * time.Second).Element(sel) if err == nil && el != nil { visible, _ := el.Visible() if visible { return el } } } return nil } // InputTextWithKeyboard 使用键盘逐字符输入 func (s *BrowserSession) InputTextWithKeyboard(text string, delayMs int) { for _, char := range text { s.Page.Keyboard.Type(input.Key(char)) time.Sleep(time.Duration(delayMs+rand.Intn(50)) * time.Millisecond) } } // ClickButton 点击匹配文本的按钮 func (s *BrowserSession) ClickButton(targets []string, maxRetries int) bool { for i := 0; i < maxRetries; i++ { clickResult, _ := s.Page.Eval(fmt.Sprintf(`() => { const targets = %s; const elements = [...document.querySelectorAll('button'), ...document.querySelectorAll('div[role="button"]')]; for (const el of elements) { if (!el || el.disabled) continue; const style = window.getComputedStyle(el); if (style.display === 'none' || style.visibility === 'hidden') continue; const text = el.textContent ? el.textContent.trim() : ''; if (targets.some(t => text.includes(t))) { el.click(); return {clicked:true}; } } return {clicked:false}; }`, toJSArray(targets))) if clickResult != nil && clickResult.Value.Get("clicked").Bool() { return true } time.Sleep(1 * time.Second) } return false } // toJSArray 将字符串数组转换为 JS 数组字符串 func toJSArray(arr []string) string { quoted := make([]string, len(arr)) for i, s := range arr { quoted[i] = fmt.Sprintf(`"%s"`, s) } return "[" + strings.Join(quoted, ",") + "]" } // CollectCookies 收集页面 Cookies func (s *BrowserSession) CollectCookies(existingCookies []pool.Cookie) []pool.Cookie { cookieMap := make(map[string]pool.Cookie) for _, c := range existingCookies { cookieMap[c.Name] = c } cookies, _ := s.Page.Cookies(nil) for _, c := range cookies { cookieMap[c.Name] = pool.Cookie{ Name: c.Name, Value: c.Value, Domain: c.Domain, } } var result []pool.Cookie for _, c := range cookieMap { result = append(result, c) } return result } func extractVerificationCode(content string) (string, error) { re := regexp.MustCompile(`\b[A-Z0-9]{6}\b`) matches := re.FindAllString(content, -1) for _, code := range matches { if commonWords[code] { continue } if regexp.MustCompile(`[0-9]`).MatchString(code) { return code, nil } } for _, code := range matches { if !commonWords[code] { return code, nil } } re2 := regexp.MustCompile(`(?i)code\s*[:is]\s*([A-Z0-9]{6})`) if m := re2.FindStringSubmatch(content); len(m) > 1 { return m[1], nil } return "", fmt.Errorf("无法从邮件中提取验证码") } func safeType(page *rod.Page, text string, delay int) error { // 一次性设置输入框值(更稳定) text = strings.TrimSpace(text) if text == "" { return nil } // 先尝试使用JS直接设置值(更稳定) _, err := page.Eval(fmt.Sprintf(`() => { const inputs = document.querySelectorAll('input'); if (inputs.length > 0) { const input = inputs[0]; input.value = %q; input.dispatchEvent(new Event('input', { bubbles: true })); input.dispatchEvent(new Event('change', { bubbles: true })); return true; } return false; }`, text)) if err == nil { time.Sleep(200 * time.Millisecond) return nil } // 回退到逐字符输入 for _, char := range text { if err := page.Keyboard.Type(input.Key(char)); err != nil { return err } time.Sleep(time.Duration(delay) * time.Millisecond) } return nil } // debugScreenshot 调试截图 func debugScreenshot(page *rod.Page, threadID int, step string) { if !RegisterDebug { return } screenshotDir := filepath.Join(DataDir, "screenshots") os.MkdirAll(screenshotDir, 0755) filename := filepath.Join(screenshotDir, fmt.Sprintf("thread%d_%s_%d.png", threadID, step, time.Now().Unix())) data, err := page.Screenshot(true, nil) if err != nil { log.Printf("[注册 %d] 📸 截图失败: %v", threadID, err) return } if err := os.WriteFile(filename, data, 0644); err != nil { log.Printf("[注册 %d] 📸 保存截图失败: %v", threadID, err) return } log.Printf("[注册 %d] 📸 截图保存: %s", threadID, filename) } // handleAdditionalSteps 处理额外步骤(复选框等) func handleAdditionalSteps(page *rod.Page, threadID int) bool { log.Printf("[注册 %d] 检查是否需要处理额外步骤...", threadID) hasAdditionalSteps := false // 首先检查是否有"出了点问题"错误页面,需要点击重试 retryResult, _ := page.Eval(`() => { const pageText = document.body ? document.body.innerText : ''; if (pageText.includes('出了点问题') || pageText.includes('Something went wrong') || pageText.includes('went wrong')) { // 查找重试按钮 - 优先使用 mdc-button__label const tryAgainLabel = document.querySelector('.mdc-button__label'); if (tryAgainLabel && (tryAgainLabel.textContent.includes('Try again') || tryAgainLabel.textContent.includes('重试') || tryAgainLabel.textContent.includes('再试'))) { const btn = tryAgainLabel.closest('button'); if (btn) { btn.click(); return { clicked: true, action: 'retry_mdc' }; } } // 备用:查找所有按钮 const buttons = document.querySelectorAll('button'); for (const btn of buttons) { const text = btn.textContent || ''; if (text.includes('重试') || text.includes('Retry') || text.includes('再试') || text.includes('Try again') || text.includes('try again')) { btn.click(); return { clicked: true, action: 'retry' }; } } } return { clicked: false }; }`) if retryResult != nil && retryResult.Value.Get("clicked").Bool() { log.Printf("[注册 %d] 检测到错误页面,已点击重试按钮", threadID) time.Sleep(3 * time.Second) return true } // 检查是否需要同意条款(主要处理复选框) checkboxResult, _ := page.Eval(`() => { const checkboxes = document.querySelectorAll('input[type="checkbox"]'); for (const checkbox of checkboxes) { if (!checkbox.checked) { checkbox.click(); return { clicked: true }; } } return { clicked: false }; }`) if checkboxResult != nil && checkboxResult.Value.Get("clicked").Bool() { hasAdditionalSteps = true log.Printf("[注册 %d] 已勾选条款复选框", threadID) time.Sleep(1 * time.Second) } // 如果有额外步骤,尝试提交 if hasAdditionalSteps { log.Printf("[注册 %d] 发现有额外步骤,尝试提交...", threadID) // 尝试提交额外信息 for i := 0; i < 3; i++ { submitResult, _ := page.Eval(`() => { const submitButtons = [ ...document.querySelectorAll('button'), ...document.querySelectorAll('input[type="submit"]') ]; for (const button of submitButtons) { if (!button.disabled && button.offsetParent !== null) { const text = button.textContent || ''; if (text.includes('同意') || text.includes('Confirm') || text.includes('继续') || text.includes('Next') || text.includes('Submit') || text.includes('完成')) { button.click(); return { clicked: true }; } } } // 点击第一个可用的提交按钮 for (const button of submitButtons) { if (!button.disabled && button.offsetParent !== null) { button.click(); return { clicked: true }; } } return { clicked: false }; }`) if submitResult != nil && submitResult.Value.Get("clicked").Bool() { log.Printf("[注册 %d] 已提交额外信息", threadID) break } time.Sleep(1 * time.Second) } // 等待可能的跳转 time.Sleep(3 * time.Second) return true } return false } // checkAndHandleAdminPage 检查并处理管理创建页面 func checkAndHandleAdminPage(page *rod.Page, threadID int) bool { currentURL := "" info, _ := page.Info() if info != nil { currentURL = info.URL } // 检查是否是管理创建页面 if strings.Contains(currentURL, "/admin/create") { log.Printf("[注册 %d] 检测到管理创建页面,尝试完成设置...", threadID) // 尝试查找并点击继续按钮 formCompleted, _ := page.Eval(`() => { let completed = false; // 查找并点击继续按钮 const continueTexts = ['Continue', '继续', 'Next', 'Submit', 'Finish', '完成']; const allButtons = document.querySelectorAll('button'); for (const button of allButtons) { if (button.offsetParent !== null && !button.disabled) { const text = (button.textContent || '').trim(); if (continueTexts.some(t => text.includes(t))) { button.click(); console.log('点击继续按钮:', text); completed = true; return completed; } } } // 如果没有找到特定按钮,尝试点击第一个可见按钮 for (const button of allButtons) { if (button.offsetParent !== null && !button.disabled) { const text = button.textContent || ''; if (text.trim() && !text.includes('Cancel') && !text.includes('取消')) { button.click(); console.log('点击通用按钮:', text); completed = true; break; } } } return completed; }`) if formCompleted != nil && formCompleted.Value.Bool() { log.Printf("[注册 %d] 已处理管理表单,等待跳转...", threadID) time.Sleep(5 * time.Second) return true } } return false } func RunBrowserRegister(headless bool, proxy string, threadID int) (result *BrowserRegisterResult) { result = &BrowserRegisterResult{} defer func() { if r := recover(); r != nil { log.Printf("[注册 %d] ☠️ panic 恢复: %v", threadID, r) result.Error = fmt.Errorf("panic: %v", r) } }() // 获取临时邮箱 email, err := getTemporaryEmail() if err != nil { result.Error = err return result } result.Email = email // 启动浏览器 - 使用统一的浏览器查找逻辑 l := launcher.New() if browserPath, found := findBrowser(); found { l = l.Bin(browserPath) log.Printf("[注册 %d] 使用浏览器: %s", threadID, browserPath) } else { log.Printf("[注册 %d] ⚠️ 未找到系统浏览器,尝试使用 rod 自动下载", threadID) } // 使用统一的浏览器配置(原生反检测,无需JS注入) l = configureBrowserLauncher(l, headless, proxy) launcherURL, err := l.Launch() if err != nil { result.Error = fmt.Errorf("启动浏览器失败: %w", err) return result } // 确保浏览器进程和临时目录被清理(即使连接失败) defer func() { if l != nil { l.Kill() l.Cleanup() // 等待浏览器退出并清理临时用户数据目录 } }() browser := rod.New().ControlURL(launcherURL) if err := browser.Connect(); err != nil { result.Error = fmt.Errorf("连接浏览器失败: %w", err) return result } defer browser.Close() browser = browser.Timeout(120 * time.Second) // 直接创建页面,不使用 stealth 注入(依赖启动参数实现反检测) page, err := browser.Page(proto.TargetCreateTarget{URL: "about:blank"}) if err != nil { result.Error = fmt.Errorf("创建页面失败: %w", err) return result } // 设置视口(使用常见分辨率) if err := page.SetViewport(&proto.EmulationSetDeviceMetricsOverride{ Width: 1920, Height: 1080, }); err != nil { log.Printf("[注册 %d] ⚠️ 设置视口失败: %v", threadID, err) } // 监听请求以捕获 authorization var authorization string var configID, csesidx string go page.EachEvent(func(e *proto.NetworkRequestWillBeSent) { if auth, ok := e.Request.Headers["authorization"]; ok { if authStr := auth.String(); authStr != "" { authorization = authStr } } url := e.Request.URL if m := regexp.MustCompile(`/cid/([a-f0-9-]+)`).FindStringSubmatch(url); len(m) > 1 && configID == "" { configID = m[1] } if m := regexp.MustCompile(`[?&]csesidx=(\d+)`).FindStringSubmatch(url); len(m) > 1 && csesidx == "" { csesidx = m[1] } })() if err := page.Navigate("https://business.gemini.google"); err != nil { result.Error = fmt.Errorf("打开页面失败: %w", err) return result } page.WaitLoad() time.Sleep(1 * time.Second) // 检查是否被代理403阻止 statusCheck, _ := page.Eval(`() => { const pageText = document.body ? document.body.innerText : ''; const title = document.title || ''; const html = document.documentElement ? document.documentElement.outerHTML : ''; // 检查403/被阻止的特征 const is403 = title.includes('403') || pageText.includes('403 Forbidden') || pageText.includes('Access Denied') || pageText.includes('访问被拒绝') || html.length < 500; // 页面内容过少可能是403 // 检查是否还在加载 const hasLoader = document.querySelector('[class*="loading"]') || document.querySelector('[class*="spinner"]'); return { is403: is403, isLoading: !!hasLoader, htmlLen: html.length, title: title, url: window.location.href }; }`) if statusCheck != nil { is403 := statusCheck.Value.Get("is403").Bool() isLoading := statusCheck.Value.Get("isLoading").Bool() htmlLen := statusCheck.Value.Get("htmlLen").Int() pageURL := statusCheck.Value.Get("url").String() log.Printf("[注册 %d] 页面状态: is403=%v, loading=%v, htmlLen=%d, url=%s", threadID, is403, isLoading, htmlLen, pageURL) if is403 { result.Error = fmt.Errorf("代理被403阻止,请更换代理") return result } // 如果还在加载,多等待一会儿 if isLoading || htmlLen < 1000 { time.Sleep(3 * time.Second) page.WaitLoad() } } debugScreenshot(page, threadID, "01_page_loaded") welcomeResult, _ := page.Eval(`() => { const text = document.body ? document.body.textContent : ''; const isWelcome = text.includes('Welcome to Gemini') || text.includes('欢迎使用 Gemini') || text.includes('Start free trial') || text.includes('开始免费试用') || text.includes('Sign in or create'); return { isWelcome }; }`) if welcomeResult != nil && welcomeResult.Value.Get("isWelcome").Bool() { // 尝试点击各种可能的按钮 page.Eval(`() => { const buttons = document.querySelectorAll('a, button'); for (const btn of buttons) { const text = btn.textContent || btn.innerText || ''; if (text.includes('free trial') || text.includes('免费试用') || text.includes('Create') || text.includes('创建') || text.includes('Get started') || text.includes('开始')) { btn.click(); return true; } } // 尝试点击主要的 CTA 按钮 const cta = document.querySelector('[data-iph="free_trial"], .cta-button, a[href*="signup"], a[href*="create"]'); if (cta) cta.click(); return false; }`) time.Sleep(1 * time.Second) page.WaitLoad() } if _, err := page.Timeout(15 * time.Second).Element("input"); err != nil { result.Error = fmt.Errorf("等待输入框超时: %w", err) return result } time.Sleep(200 * time.Millisecond) log.Printf("[注册 %d] 准备输入邮箱: %s", threadID, email) time.Sleep(500 * time.Millisecond) var emailInput *rod.Element selectors := []string{ "#email-input", // Google Business 特定 ID "input[name='loginHint']", // Google Business 特定 name "input[jsname='YPqjbf']", // Google jsname "input[type='email']", "input[type='text'][aria-label]", "input:not([type='hidden']):not([type='submit']):not([type='checkbox'])", } for _, sel := range selectors { el, err := page.Timeout(3 * time.Second).Element(sel) if err != nil { continue } if el != nil { visible, _ := el.Visible() if visible { emailInput = el break } } } if emailInput == nil { // 先检查页面状态 pageState, _ := page.Eval(`() => { const pageText = document.body ? document.body.innerText : ''; const htmlLen = document.documentElement ? document.documentElement.outerHTML.length : 0; return { htmlLen: htmlLen, has403: pageText.includes('403') || pageText.includes('Forbidden') || pageText.includes('Denied'), hasError: pageText.includes('出了点问题') || pageText.includes('went wrong'), isAuthPage: window.location.href.includes('auth.business.gemini'), url: window.location.href }; }`) if pageState != nil { has403 := pageState.Value.Get("has403").Bool() hasError := pageState.Value.Get("hasError").Bool() htmlLen := pageState.Value.Get("htmlLen").Int() isAuthPage := pageState.Value.Get("isAuthPage").Bool() if has403 || htmlLen < 500 { result.Error = fmt.Errorf("代理403/被阻止,页面未正常加载") return result } if hasError { result.Error = fmt.Errorf("页面显示错误,可能被IP限制") return result } if isAuthPage && htmlLen < 2000 { time.Sleep(5 * time.Second) for _, sel := range selectors { el, err := page.Timeout(3 * time.Second).Element(sel) if err == nil && el != nil { if visible, _ := el.Visible(); visible { emailInput = el break } } } } } if emailInput == nil { html, _ := page.HTML() if len(html) > 2000 { html = html[:2000] } log.Printf("[注册 %d] ❌ 找不到邮箱输入框,页面HTML片段: %s", threadID, html) result.Error = fmt.Errorf("找不到邮箱输入框(页面未正常加载)") return result } } // 获取元素信息 tagName, _ := emailInput.Property("tagName") inputType, _ := emailInput.Property("type") inputId, _ := emailInput.Property("id") inputName, _ := emailInput.Property("name") log.Printf("[注册 %d] 📝 元素信息: tag=%s, type=%s, id=%s, name=%s", threadID, tagName.String(), inputType.String(), inputId.String(), inputName.String()) log.Printf("[注册 %d] 📍 拟人化聚焦输入框...", threadID) if err := humanFocusInput(page, emailInput); err != nil { log.Printf("[注册 %d] ⚠️ 聚焦失败,回退到直接点击: %v", threadID, err) emailInput.MustScrollIntoView() humanDelay(100, 300) emailInput.MustClick() } humanDelay(200, 400) hasFocus, _ := page.Eval(`() => document.activeElement && document.activeElement.id`) log.Printf("[注册 %d] 🎯 当前焦点元素ID: %v", threadID, hasFocus.Value) // 清空输入框 - 使用 triple-click 全选然后删除 log.Printf("[注册 %d] 🗑️ 清空输入框...", threadID) // 先检查当前是否有内容 currentVal, _ := emailInput.Property("value") if currentVal.String() != "" { emailInput.SelectAllText() humanDelay(80, 150) page.Keyboard.Type(input.Backspace) humanDelay(80, 150) } // 使用拟人化打字输入邮箱 log.Printf("[注册 %d] ⌨️ 开始拟人化输入邮箱: %s", threadID, email) humanType(page, email) log.Printf("[注册 %d] ⌨️ 邮箱输入完成", threadID) time.Sleep(500 * time.Millisecond) // 验证输入 propVal, _ := emailInput.Property("value") inputValue := propVal.String() log.Printf("[注册 %d] 📋 最终输入值: [%s]", threadID, inputValue) if inputValue != email { } else { log.Printf("[注册 %d] ✅ 输入验证成功", threadID) } // 触发 blur page.Eval(`() => { const inputs = document.querySelectorAll('input'); if (inputs.length > 0) { inputs[0].blur(); } }`) time.Sleep(500 * time.Millisecond) debugScreenshot(page, threadID, "03_before_submit") emailSubmitted := false for i := 0; i < 8; i++ { // 拟人化延迟(模拟人类寻找按钮的时间) humanDelay(200, 500) // 使用 rod 原生方式查找并点击按钮(避免 JS click() 被检测) buttonSelectors := []string{ `button`, `input[type="submit"]`, `div[role="button"]`, `span[role="button"]`, } targetTexts := []string{"继续", "Next", "邮箱", "Continue"} var targetButton *rod.Element for _, sel := range buttonSelectors { elements, err := page.Elements(sel) if err != nil { continue } for _, el := range elements { visible, _ := el.Visible() if !visible { continue } text, _ := el.Text() text = strings.TrimSpace(text) for _, target := range targetTexts { if strings.Contains(text, target) { targetButton = el break } } if targetButton != nil { break } } if targetButton != nil { break } } if targetButton != nil { log.Printf("[注册 %d] 找到提交按钮,执行拟人化点击", threadID) if err := humanClick(page, targetButton); err != nil { log.Printf("[注册 %d] ⚠️ humanClick 失败: %v,回退到直接点击", threadID, err) targetButton.MustClick() } humanDelay(300, 600) emailSubmitted = true break } log.Printf("[注册 %d] 尝试 %d/8: 未找到按钮", threadID, i+1) humanDelay(800, 1200) } if !emailSubmitted { result.Error = fmt.Errorf("找不到提交按钮") return result } // 等待页面跳转,最多等待15秒 var needsVerification bool var pageTransitioned bool var detectedSigninError bool for waitCount := 0; waitCount < 12; waitCount++ { // 优化:减少最大等待次数 humanDelay(600, 1000) // 检查是否被重定向到 signin-error 页面(提前检测) pageInfo, _ := page.Info() if pageInfo != nil && strings.Contains(pageInfo.URL, "signin-error") { log.Printf("[注册 %d] ⚠️ 在等待过程中检测到 signin-error 页面", threadID) detectedSigninError = true pageTransitioned = true break } // 检查页面是否已经离开邮箱输入页面 transitionResult, _ := page.Eval(`() => { const pageText = document.body ? document.body.textContent : ''; const emailInput = document.querySelector('input[type="email"]'); const continueBtn = document.querySelector('button[jsname="LgbsSe"]'); const stillOnEmailPage = (emailInput && emailInput.offsetParent !== null) || (continueBtn && continueBtn.innerText && (continueBtn.innerText.includes('继续') || continueBtn.innerText.includes('Continue'))); const isVerifyPage = pageText.includes('验证') || pageText.includes('Verify') || pageText.includes('输入代码') || pageText.includes('Enter code') || pageText.includes('发送到') || pageText.includes('sent to'); const isNamePage = pageText.includes('姓氏') || pageText.includes('名字') || pageText.includes('Full name') || pageText.includes('全名'); const errorElement = document.querySelector('.zyTWof-Ng57nc, .zyTWof-gIZMF'); const hasErrorElement = errorElement && errorElement.offsetParent !== null && errorElement.textContent && errorElement.textContent.length > 0; const hasError = hasErrorElement || pageText.includes('出了点问题') || pageText.includes('Something went wrong') || pageText.includes('无法创建') || pageText.includes('cannot create') || pageText.includes('try again later') || pageText.includes('稀后再试') || pageText.includes('需要电话') || pageText.includes('电话号码') || pageText.includes('Phone number') || pageText.includes('Verify your phone'); return { stillOnEmailPage: stillOnEmailPage && !isVerifyPage && !isNamePage, isVerifyPage: isVerifyPage, isNamePage: isNamePage, hasError: hasError, errorText: hasError ? document.body.innerText.substring(0, 100) : '' }; }`) if transitionResult != nil { if transitionResult.Value.Get("hasError").Bool() { result.Error = fmt.Errorf("页面显示错误: %s", transitionResult.Value.Get("errorText").String()) log.Printf("[注册 %d] ❌ %v", threadID, result.Error) return result } if !transitionResult.Value.Get("stillOnEmailPage").Bool() { pageTransitioned = true needsVerification = transitionResult.Value.Get("isVerifyPage").Bool() isNamePage := transitionResult.Value.Get("isNamePage").Bool() log.Printf("[注册 %d] 页面已跳转: needsVerification=%v, isNamePage=%v", threadID, needsVerification, isNamePage) break } } if waitCount%3 == 2 { log.Printf("[注册 %d] 等待页面跳转... (%d/15秒)", threadID, waitCount+1) } } // 跳转后再次检查 signin-error(以防在 break 后才跳转) if !detectedSigninError { pageInfo, _ := page.Info() if pageInfo != nil && strings.Contains(pageInfo.URL, "signin-error") { detectedSigninError = true } } debugScreenshot(page, threadID, "04_after_submit") if !pageTransitioned { // 页面没有跳转,可能需要重新点击按钮 log.Printf("[注册 %d] 页面未跳转,尝试重新点击按钮", threadID) page.Eval(`() => { const btn = document.querySelector('button[jsname="LgbsSe"]'); if (btn) btn.click(); }`) time.Sleep(3 * time.Second) needsVerification = true // 假设需要验证 } // 再次检查页面状态 checkResult, _ := page.Eval(`() => { const pageText = document.body ? document.body.textContent : ''; // 检查常见错误 if (pageText.includes('出了点问题') || pageText.includes('Something went wrong') || pageText.includes('无法创建') || pageText.includes('cannot create') || pageText.includes('不安全') || pageText.includes('secure') || pageText.includes('电话') || pageText.includes('Phone') || pageText.includes('number')) { return { error: true, text: document.body.innerText.substring(0, 100) }; } // 检查是否需要验证码 if (pageText.includes('验证') || pageText.includes('Verify') || pageText.includes('code') || pageText.includes('sent')) { return { needsVerification: true, isNamePage: false }; } // 检查是否已经到了姓名页面 if (pageText.includes('姓氏') || pageText.includes('名字') || pageText.includes('Full name') || pageText.includes('全名')) { return { needsVerification: false, isNamePage: true }; } return { needsVerification: true, isNamePage: false }; }`) if checkResult != nil { if checkResult.Value.Get("error").Bool() { errText := checkResult.Value.Get("text").String() result.Error = fmt.Errorf("页面显示错误: %s", errText) log.Printf("[注册 %d] ❌ %v", threadID, result.Error) return result } needsVerification = checkResult.Value.Get("needsVerification").Bool() isNamePage := checkResult.Value.Get("isNamePage").Bool() log.Printf("[注册 %d] 页面状态: needsVerification=%v, isNamePage=%v", threadID, needsVerification, isNamePage) } else { needsVerification = true } // 检测并处理 signin-error 页面(被检测到后的恢复) if detectedSigninError { log.Printf("[注册 %d] ⚠️ 开始处理 signin-error 页面恢复...", threadID) // 等待页面完全加载 page.WaitLoad() humanDelay(500, 800) log.Printf("[注册 %d] 开始尝试恢复...", threadID) // 查找 "Sign up or sign in" 按钮(带重试) var signupButton *rod.Element var findErr error for attempt := 0; attempt < 3; attempt++ { signupButton, findErr = func() (*rod.Element, error) { buttonSelectors := []string{"button", `div[role="button"]`, `span[role="button"]`} targetTexts := []string{"Sign up", "sign in", "Sign in", "注册", "登录"} for _, sel := range buttonSelectors { elements, err := page.Elements(sel) if err != nil { continue } for _, el := range elements { visible, _ := el.Visible() if !visible { continue } text, _ := el.Text() for _, target := range targetTexts { if strings.Contains(text, target) { return el, nil } } } } return nil, fmt.Errorf("未找到 Sign up or sign in 按钮") }() if signupButton != nil { break } log.Printf("[注册 %d] 尝试 %d/3: %v,等待后重试...", threadID, attempt+1, findErr) humanDelay(400, 600) } if signupButton == nil { log.Printf("[注册 %d] ❌ %v,放弃恢复", threadID, findErr) result.Error = fmt.Errorf("被检测并重定向到 signin-error,恢复失败") return result } log.Printf("[注册 %d] 找到恢复按钮,执行拟人化点击", threadID) humanClick(page, signupButton) humanDelay(500, 800) // 等待页面加载,重新查找邮箱输入框 page.WaitLoad() humanDelay(300, 500) // 重新输入邮箱 var retryEmailInput *rod.Element retryEmailSelectors := []string{ "#email-input", "input[name='loginHint']", "input[type='email']", "input[type='text'][aria-label]", } for _, sel := range retryEmailSelectors { el, err := page.Timeout(3 * time.Second).Element(sel) if err == nil && el != nil { if visible, _ := el.Visible(); visible { retryEmailInput = el break } } } if retryEmailInput != nil { log.Printf("[注册 %d] 重新输入邮箱: %s", threadID, email) humanFocusInput(page, retryEmailInput) humanDelay(200, 400) retryEmailInput.SelectAllText() humanDelay(50, 100) page.Keyboard.Type(input.Backspace) humanDelay(80, 150) humanType(page, email) humanDelay(400, 700) // 重新点击提交按钮 retryButtonSelectors := []string{"button", `input[type="submit"]`, `div[role="button"]`} retryTargetTexts := []string{"继续", "Next", "Continue"} var retrySubmitBtn *rod.Element for _, sel := range retryButtonSelectors { elements, _ := page.Elements(sel) for _, el := range elements { if visible, _ := el.Visible(); !visible { continue } text, _ := el.Text() for _, target := range retryTargetTexts { if strings.Contains(text, target) { retrySubmitBtn = el break } } if retrySubmitBtn != nil { break } } if retrySubmitBtn != nil { break } } if retrySubmitBtn != nil { log.Printf("[注册 %d] 重新提交邮箱", threadID) humanClick(page, retrySubmitBtn) humanDelay(800, 1200) // 重新检查页面状态 page.WaitLoad() humanDelay(500, 800) } } else { log.Printf("[注册 %d] ⚠️ 恢复后未找到邮箱输入框", threadID) } } // 处理验证码 if needsVerification { var emailContent *EmailContent maxWaitTime := 3 * time.Minute startTime := time.Now() resendCount := 0 maxResend := 3 lastEmailCheck := time.Time{} emailCheckInterval := 3 * time.Second codePageStableTime := time.Time{} // 验证码页面稳定时间 for time.Since(startTime) < maxWaitTime { // 检查页面状态 pageStatus, _ := page.Eval(`() => { const pageText = document.body ? document.body.innerText : ''; // 检查是否在验证码页面 const isCodePage = pageText.includes('6-character code') || pageText.includes('verification code') || pageText.includes('Enter verification') || pageText.includes('验证码') || pageText.includes('We sent'); // 检查验证码页面上的错误提示(验证码错误、发送失败等) const codePageErrors = [ 'Wrong code', 'wrong code', '验证码错误', '代码错误', 'expired', '已过期', '过期', 'try again', '重试', 'Try again', 'too many attempts', '尝试次数过多' ]; const hasCodeError = isCodePage && codePageErrors.some(err => pageText.toLowerCase().includes(err.toLowerCase())); // 检查底部 toast/snackbar 错误提示 const toastSelectors = ['[role="alert"]', 'aside', '[jscontroller="Q9PAie"]']; let toastError = null; for (const sel of toastSelectors) { const el = document.querySelector(sel); if (el && el.offsetParent !== null) { const text = el.textContent || ''; if (text.includes('went wrong') || text.includes('出了点问题') || text.includes('choose another') || text.includes('login method') || text.includes('无法发送') || text.includes('failed')) { toastError = text; break; } } } // 检查是否是严重错误页面(不是验证码页面) const fatalErrors = ['出了点问题', 'Something went wrong', 'choose another login method']; const hasFatalError = !isCodePage && fatalErrors.some(err => pageText.toLowerCase().includes(err.toLowerCase())); // 检查 Try again 按钮(错误页面) const tryAgainBtn = document.querySelector('.mdc-button__label'); const hasTryAgainBtn = tryAgainBtn && (tryAgainBtn.textContent.includes('Try again') || tryAgainBtn.textContent.includes('重试')); // 查找重发按钮(验证码页面) const resendBtn = document.querySelector('span[jsname="V67aGc"].YuMlnb-vQzf8d') || document.querySelector('span.YuMlnb-vQzf8d') || Array.from(document.querySelectorAll('span, button, a')).find(el => el.textContent && (el.textContent.includes('重新发送') || el.textContent.toLowerCase().includes('resend'))); return { isCodePage: isCodePage, hasCodeError: hasCodeError, toastError: toastError || '', hasFatalError: hasFatalError || !!toastError, hasTryAgainBtn: hasTryAgainBtn, hasResendBtn: !!resendBtn, pageText: pageText.substring(0, 200) }; }`) if pageStatus == nil { time.Sleep(1 * time.Second) continue } isCodePage := pageStatus.Value.Get("isCodePage").Bool() hasCodeError := pageStatus.Value.Get("hasCodeError").Bool() hasFatalError := pageStatus.Value.Get("hasFatalError").Bool() hasTryAgainBtn := pageStatus.Value.Get("hasTryAgainBtn").Bool() hasResendBtn := pageStatus.Value.Get("hasResendBtn").Bool() toastError := pageStatus.Value.Get("toastError").String() // 处理严重错误(不是验证码页面的错误) if hasFatalError && !isCodePage { if hasTryAgainBtn { log.Printf("[注册 %d] 检测到错误页面,点击 Try again", threadID) page.Eval(`() => { const btn = document.querySelector('.mdc-button__label'); if (btn) { const parent = btn.closest('button'); if (parent) parent.click(); } }`) time.Sleep(3 * time.Second) continue } errMsg := toastError if errMsg == "" { errMsg = pageStatus.Value.Get("pageText").String() } if len(errMsg) > 80 { errMsg = errMsg[:80] } result.Error = fmt.Errorf("验证码发送失败: %s", errMsg) log.Printf("[注册 %d] ❌ %v", threadID, result.Error) return result } // 在验证码页面 if isCodePage { // 首次进入验证码页面,记录时间 if codePageStableTime.IsZero() { codePageStableTime = time.Now() } pageStableDuration := time.Since(codePageStableTime) if hasCodeError && hasResendBtn && resendCount < maxResend && pageStableDuration > 5*time.Second { log.Printf("[注册 %d] 验证码页面出现错误,点击重发 (%d/%d)", threadID, resendCount+1, maxResend) page.Eval(`() => { const btn = document.querySelector('span[jsname="V67aGc"].YuMlnb-vQzf8d') || document.querySelector('span.YuMlnb-vQzf8d') || Array.from(document.querySelectorAll('span, button, a')).find(el => el.textContent && (el.textContent.includes('重新发送') || el.textContent.toLowerCase().includes('resend'))); if (btn) { btn.click(); if (btn.parentElement) btn.parentElement.click(); } }`) resendCount++ time.Sleep(3 * time.Second) continue } if time.Since(lastEmailCheck) >= emailCheckInterval { emailContent, _ = getVerificationEmailQuick(email, 1, 2) lastEmailCheck = time.Now() if emailContent != nil { log.Printf("[注册 %d] ✅ 获取到验证码邮件", threadID) break } } } time.Sleep(1 * time.Second) } if emailContent == nil { result.Error = fmt.Errorf("无法获取验证码邮件") return result } // 提取验证码 code, err := extractVerificationCode(emailContent.Content) if err != nil { result.Error = err return result } // 等待验证码输入框 time.Sleep(500 * time.Millisecond) log.Printf("[注册 %d] 准备输入验证码: %s", threadID, code) // 检查是否是OTP风格的多个输入框 inputInfo, _ := page.Eval(`() => { // 检查标准input const inputs = document.querySelectorAll('input:not([type="hidden"])'); const visibleInputs = Array.from(inputs).filter(i => i.offsetParent !== null); // 检查Google风格的OTP框(可能是div实现) const otpContainers = document.querySelectorAll('[data-otp-input], [class*="otp"], [class*="code-input"], [class*="verification"]'); // 检查页面是否包含验证码相关文本 const pageText = document.body ? document.body.innerText : ''; const isVerifyPage = pageText.includes('验证码') || pageText.includes('verification') || pageText.includes('verify') || window.location.href.includes('verify'); const isOTP = (visibleInputs.length >= 4 && visibleInputs.length <= 8) || (isVerifyPage && visibleInputs.length <= 2); return { count: visibleInputs.length, isOTP: isOTP, isVerifyPage: isVerifyPage, url: window.location.href }; }`) isOTP := false if inputInfo != nil { isOTP = inputInfo.Value.Get("isOTP").Bool() log.Printf("[注册 %d] 验证码输入框: count=%d, isOTP=%v", threadID, inputInfo.Value.Get("count").Int(), isOTP) } // 使用 rod Element API 查找验证码输入框 codeInputs, _ := page.Elements("input:not([type='hidden'])") var firstCodeInput *rod.Element for _, el := range codeInputs { visible, _ := el.Visible() if visible { firstCodeInput = el break } } if firstCodeInput == nil { log.Printf("[注册 %d] ⚠️ 未找到验证码输入框", threadID) } else { // 使用拟人化点击验证码框 func() { defer func() { if r := recover(); r != nil { log.Printf("[注册 %d] 点击验证码框异常: %v", threadID, r) } }() humanClick(page, firstCodeInput) }() humanDelay(200, 400) // 清空输入框(带超时保护) func() { defer func() { if r := recover(); r != nil { log.Printf("[注册 %d] 清空验证码框异常: %v", threadID, r) } }() firstCodeInput.SelectAllText() firstCodeInput.Input("") }() humanDelay(150, 300) // 使用拟人化打字输入验证码 log.Printf("[注册 %d] ⌨️ 开始拟人化输入验证码...", threadID) humanType(page, code) log.Printf("[注册 %d] 验证码输入完成", threadID) } humanDelay(400, 700) for i := 0; i < 5; i++ { clickResult, _ := page.Eval(`() => { const targets = ['验证', 'Verify', '继续', 'Next', 'Continue']; const elements = [ ...document.querySelectorAll('button'), ...document.querySelectorAll('input[type="submit"]'), ...document.querySelectorAll('div[role="button"]') ]; for (const element of elements) { if (!element) continue; const style = window.getComputedStyle(element); if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') continue; if (element.disabled) continue; const text = element.textContent ? element.textContent.trim() : ''; if (targets.some(t => text.includes(t))) { element.click(); return { clicked: true, text: text }; } } return { clicked: false }; }`) if clickResult != nil && clickResult.Value.Get("clicked").Bool() { humanDelay(300, 600) break } humanDelay(800, 1200) } humanDelay(1500, 2500) } // 填写姓名 fullName := generateRandomName() result.FullName = fullName log.Printf("[注册 %d] 准备输入姓名: %s", threadID, fullName) humanDelay(400, 700) // 查找姓名输入框并使用 rod 原生方式输入 nameSelectors := []string{ `input[name="fullName"]`, `input[autocomplete="name"]`, `input[type="text"]:not([type="hidden"]):not([type="email"])`, } var nameInput *rod.Element for _, sel := range nameSelectors { nameInput, _ = page.Timeout(2 * time.Second).Element(sel) if nameInput != nil { visible, _ := nameInput.Visible() if visible { break } nameInput = nil } } // 兜底:获取第一个可见的文本输入框 if nameInput == nil { inputs, _ := page.Elements(`input:not([type="hidden"]):not([type="submit"]):not([type="email"])`) for _, inp := range inputs { if visible, _ := inp.Visible(); visible { nameInput = inp break } } } if nameInput != nil { // 拟人化聚焦并清空 log.Printf("[注册 %d] 📍 拟人化聚焦姓名输入框...", threadID) humanClick(page, nameInput) humanDelay(100, 200) nameInput.SelectAllText() humanDelay(50, 100) page.Keyboard.Type(input.Backspace) humanDelay(80, 150) // 拟人化输入姓名 log.Printf("[注册 %d] ⌨️ 开始拟人化输入姓名: %s", threadID, fullName) humanType(page, fullName) log.Printf("[注册 %d] 姓名输入完成: %s", threadID, fullName) } else { log.Printf("[注册 %d] ⚠️ 未找到姓名输入框,尝试直接键盘输入", threadID) // 使用拟人化打字作为备用 humanType(page, fullName) } humanDelay(400, 700) // 确认提交姓名 confirmSubmitted := false for i := 0; i < 5; i++ { clickResult, _ := page.Eval(`() => { const targets = ['同意', 'Confirm', '继续', 'Next', 'Continue', 'I agree']; const elements = [ ...document.querySelectorAll('button'), ...document.querySelectorAll('input[type="submit"]'), ...document.querySelectorAll('div[role="button"]') ]; for (const element of elements) { if (!element) continue; const style = window.getComputedStyle(element); if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') continue; if (element.disabled) continue; const text = element.textContent ? element.textContent.trim() : ''; if (targets.some(t => text.includes(t))) { element.click(); return { clicked: true, text: text }; } } // 备用:点击第一个可见按钮 for (const element of elements) { if (element && element.offsetParent !== null && !element.disabled) { element.click(); return { clicked: true, text: 'fallback' }; } } return { clicked: false }; }`) if clickResult != nil && clickResult.Value.Get("clicked").Bool() { confirmSubmitted = true break } time.Sleep(1000 * time.Millisecond) } if !confirmSubmitted { log.Printf("[注册 %d] ⚠️ 未能点击确认按钮,尝试继续", threadID) } time.Sleep(3 * time.Second) // 等待页面稳定 page.WaitLoad() time.Sleep(2 * time.Second) // 处理额外步骤(主要是复选框) handleAdditionalSteps(page, threadID) // 检查并处理管理创建页面 checkAndHandleAdminPage(page, threadID) // 等待更多可能的跳转 time.Sleep(3 * time.Second) // 尝试多次点击可能出现的额外按钮 for i := 0; i < 15; i++ { time.Sleep(2 * time.Second) // 尝试点击可能出现的额外按钮 page.Eval(`() => { const buttons = document.querySelectorAll('button'); for (const button of buttons) { if (!button) continue; const text = button.textContent || ''; if (text.includes('同意') || text.includes('Confirm') || text.includes('继续') || text.includes('Next') || text.includes('I agree')) { if (button.offsetParent !== null && !button.disabled) { button.click(); return true; } } } return false; }`) // 从 URL 提取信息 info, _ := page.Info() if info != nil { currentURL := info.URL if m := regexp.MustCompile(`/cid/([a-f0-9-]+)`).FindStringSubmatch(currentURL); len(m) > 1 && configID == "" { configID = m[1] log.Printf("[注册 %d] 从URL提取 configId: %s", threadID, configID) } if m := regexp.MustCompile(`[?&]csesidx=(\d+)`).FindStringSubmatch(currentURL); len(m) > 1 && csesidx == "" { csesidx = m[1] log.Printf("[注册 %d] 从URL提取 csesidx: %s", threadID, csesidx) } } if authorization != "" { break } } // 增强的 Authorization 获取逻辑 if authorization == "" { log.Printf("[注册 %d] 仍未获取到 Authorization,尝试更多方法...", threadID) // 尝试刷新页面 page.Reload() page.WaitLoad() time.Sleep(3 * time.Second) // 尝试从 localStorage 获取 localStorageAuth, _ := page.Eval(`() => { return localStorage.getItem('Authorization') || localStorage.getItem('authorization') || localStorage.getItem('auth_token') || localStorage.getItem('token'); }`) if localStorageAuth != nil && localStorageAuth.Value.String() != "" { authorization = localStorageAuth.Value.String() log.Printf("[注册 %d] 从 localStorage 获取 Authorization", threadID) } // 从页面源代码中提取 pageContent, _ := page.Eval(`() => document.body ? document.body.innerHTML : ''`) if pageContent != nil && pageContent.Value.String() != "" { content := pageContent.Value.String() re := regexp.MustCompile(`"authorization"\s*:\s*"([^"]+)"`) if matches := re.FindStringSubmatch(content); len(matches) > 1 { authorization = matches[1] log.Printf("[注册 %d] 从页面内容提取 Authorization", threadID) } } // 从当前 URL 中提取 info, _ := page.Info() if info != nil { currentURL := info.URL re := regexp.MustCompile(`[?&](?:token|auth)=([^&]+)`) if matches := re.FindStringSubmatch(currentURL); len(matches) > 1 { authorization = matches[1] log.Printf("[注册 %d] 从 URL 提取 Authorization", threadID) } } } if authorization == "" { result.Error = fmt.Errorf("未能获取 Authorization") return result } var resultCookies []pool.Cookie cookieMap := make(map[string]bool) // 获取当前页面所有 cookie cookies, _ := page.Cookies(nil) for _, c := range cookies { key := c.Name + "|" + c.Domain if !cookieMap[key] { cookieMap[key] = true resultCookies = append(resultCookies, pool.Cookie{ Name: c.Name, Value: c.Value, Domain: c.Domain, }) } } // 尝试从特定域名获取更多 cookie domains := []string{ "https://business.gemini.google", "https://gemini.google", "https://accounts.google.com", } for _, domain := range domains { domainCookies, err := page.Cookies([]string{domain}) if err == nil { for _, c := range domainCookies { key := c.Name + "|" + c.Domain if !cookieMap[key] { cookieMap[key] = true resultCookies = append(resultCookies, pool.Cookie{ Name: c.Name, Value: c.Value, Domain: c.Domain, }) } } } } log.Printf("[注册 %d] 获取到 %d 个 Cookie", threadID, len(resultCookies)) // 如果 csesidx 为空,尝试从 authorization 中提取 if csesidx == "" && authorization != "" { csesidx = extractCSESIDXFromAuth(authorization) if csesidx != "" { log.Printf("[注册 %d] 从 authorization 提取 csesidx: %s", threadID, csesidx) } } // 如果仍为空,尝试访问主页获取 if csesidx == "" { log.Printf("[注册 %d] ⚠️ csesidx 为空,尝试访问主页获取...", threadID) page.Navigate("https://business.gemini.google/") time.Sleep(3 * time.Second) info, _ := page.Info() if info != nil { if m := regexp.MustCompile(`[?&]csesidx=(\d+)`).FindStringSubmatch(info.URL); len(m) > 1 { csesidx = m[1] log.Printf("[注册 %d] 从主页URL提取 csesidx: %s", threadID, csesidx) } } } // 如果 csesidx 为空,尝试从 authorization 提取 if csesidx == "" && authorization != "" { csesidx = extractCSESIDXFromAuth(authorization) } // csesidx 是必须的,没有则注册失败 if csesidx == "" { result.Error = fmt.Errorf("未能获取 csesidx") return result } result.Success = true result.Authorization = authorization result.Cookies = resultCookies result.ConfigID = configID result.CSESIDX = csesidx log.Printf("[注册 %d] ✅ 注册成功: %s", threadID, email) return result } // SaveBrowserRegisterResult 保存注册结果 func SaveBrowserRegisterResult(result *BrowserRegisterResult, dataDir string) error { if !result.Success { return result.Error } data := pool.AccountData{ Email: result.Email, FullName: result.FullName, Authorization: result.Authorization, Cookies: result.Cookies, ConfigID: result.ConfigID, CSESIDX: result.CSESIDX, Timestamp: time.Now().Format(time.RFC3339), } jsonData, err := json.MarshalIndent(data, "", " ") if err != nil { return fmt.Errorf("序列化失败: %w", err) } filename := filepath.Join(dataDir, fmt.Sprintf("%s.json", result.Email)) if err := os.WriteFile(filename, jsonData, 0644); err != nil { return fmt.Errorf("写入文件失败: %w", err) } return nil } // BrowserRefreshResult Cookie刷新结果 type BrowserRefreshResult struct { Success bool SecureCookies []pool.Cookie ConfigID string CSESIDX string Authorization string ResponseHeaders map[string]string // 捕获的响应头 NewCookies []pool.Cookie // 从响应头提取的新Cookie Error error } func RefreshCookieWithBrowser(acc *pool.Account, headless bool, proxy string) *BrowserRefreshResult { result := &BrowserRefreshResult{} email := acc.Data.Email defer func() { if r := recover(); r != nil { result.Error = fmt.Errorf("panic: %v", r) } }() // 使用公共函数创建浏览器会话 session, err := createBrowserSession(headless, proxy, "[Cookie刷新]") if err != nil { result.Error = err return result } defer session.Close() page := session.Page var authorization string var configID, csesidx string var responseHeadersMu sync.Mutex responseHeaders := make(map[string]string) var newCookiesFromResponse []pool.Cookie go page.EachEvent(func(e *proto.NetworkResponseReceived) { responseHeadersMu.Lock() defer responseHeadersMu.Unlock() headers := e.Response.Headers importantKeys := []string{"set-cookie", "Set-Cookie", "authorization", "Authorization", "x-goog-authenticated-user", "X-Goog-Authenticated-User"} for _, key := range importantKeys { if val, ok := headers[key]; ok { str := val.Str() if str == "" { continue } responseHeaders[key] = str // 解析 Set-Cookie if strings.ToLower(key) == "set-cookie" { parts := strings.Split(str, ";") if len(parts) > 0 { nv := strings.SplitN(parts[0], "=", 2) if len(nv) == 2 { newCookiesFromResponse = append(newCookiesFromResponse, pool.Cookie{ Name: strings.TrimSpace(nv[0]), Value: strings.TrimSpace(nv[1]), Domain: ".gemini.google", }) } } } } } })() go page.EachEvent(func(e *proto.NetworkRequestWillBeSent) { if auth, ok := e.Request.Headers["authorization"]; ok { if authStr := auth.String(); authStr != "" { authorization = authStr } } reqURL := e.Request.URL if m := regexp.MustCompile(`/cid/([a-f0-9-]+)`).FindStringSubmatch(reqURL); len(m) > 1 && configID == "" { configID = m[1] } if m := regexp.MustCompile(`[?&]csesidx=(\d+)`).FindStringSubmatch(reqURL); len(m) > 1 && csesidx == "" { csesidx = m[1] } })() // 导航到目标页面 targetURL := "https://business.gemini.google/" page.Navigate(targetURL) page.WaitLoad() time.Sleep(2 * time.Second) // 检查页面状态 info, _ := page.Info() var currentURL string if info != nil { currentURL = info.URL } _ = currentURL // 后续 extractResult 中使用 initialEmailCount := 0 maxCodeRetries := 3 // 验证码重试次数(必须在goto之前声明) // 检查是否已经登录成功(有authorization) if authorization != "" { log.Printf("[Cookie刷新] [%s] Cookie有效,已自动登录", email) goto extractResult } // 获取实际邮件数量 initialEmailCount = getEmailCount(email) // 检查是否在登录页面需要输入邮箱 if _, err := page.Timeout(5 * time.Second).Element("input"); err == nil { log.Printf("[Cookie刷新] [%s] 🔍 查找邮箱输入框...", email) // 使用精确选择器查找输入框 var emailInput *rod.Element selectors := []string{ "#email-input", "input[name='loginHint']", "input[jsname='YPqjbf']", "input[type='email']", "input[type='text'][aria-label]", "input:not([type='hidden']):not([type='submit']):not([type='checkbox'])", } for _, sel := range selectors { el, err := page.Timeout(2 * time.Second).Element(sel) if err == nil && el != nil { visible, _ := el.Visible() if visible { emailInput = el log.Printf("[Cookie刷新] [%s] ✅ 找到输入框: %s", email, sel) break } } } if emailInput != nil { // 点击获取焦点 emailInput.MustScrollIntoView() emailInput.MustClick() time.Sleep(300 * time.Millisecond) // 清空输入框(仅当有内容时) currentVal, _ := emailInput.Property("value") if currentVal.String() != "" { emailInput.SelectAllText() time.Sleep(100 * time.Millisecond) page.Keyboard.Type(input.Backspace) time.Sleep(100 * time.Millisecond) } log.Printf("[Cookie刷新] [%s] ⌨️ 开始键盘输入邮箱...", email) for _, char := range email { page.Keyboard.Type(input.Key(char)) time.Sleep(time.Duration(50+rand.Intn(80)) * time.Millisecond) } // 验证输入 propVal, _ := emailInput.Property("value") log.Printf("[Cookie刷新] [%s] 📋 输入值: %s", email, propVal.String()) } else { log.Printf("[Cookie刷新] [%s] ⚠️ 未找到输入框,尝试旧方式", email) page.Eval(`() => { const inputs = document.querySelectorAll('input'); if (inputs.length > 0) { inputs[0].value = ''; inputs[0].click(); inputs[0].focus(); } }`) time.Sleep(300 * time.Millisecond) safeType(page, email, 30) } time.Sleep(500 * time.Millisecond) page.Eval(`() => { const inputs = document.querySelectorAll('input'); if (inputs.length > 0) { inputs[0].blur(); } }`) time.Sleep(500 * time.Millisecond) // 点击继续按钮 for i := 0; i < 5; i++ { clickResult, _ := page.Eval(`() => { const targets = ['继续', 'Next', 'Continue', '邮箱']; const elements = [...document.querySelectorAll('button'), ...document.querySelectorAll('div[role="button"]')]; for (const el of elements) { if (!el || el.disabled) continue; const style = window.getComputedStyle(el); if (style.display === 'none' || style.visibility === 'hidden') continue; const text = el.textContent ? el.textContent.trim() : ''; if (targets.some(t => text.includes(t))) { el.click(); return {clicked:true}; } } return {clicked:false}; }`) if clickResult != nil && clickResult.Value.Get("clicked").Bool() { break } time.Sleep(1 * time.Second) } time.Sleep(2 * time.Second) } time.Sleep(3 * time.Second) // 验证码重试循环 for codeRetry := 0; codeRetry < maxCodeRetries; codeRetry++ { if codeRetry > 0 { log.Printf("[Cookie刷新] [%s] 验证码验证失败,重试 %d/%d", email, codeRetry+1, maxCodeRetries) // 点击"重新发送验证码"按钮 page.Eval(`() => { const links = document.querySelectorAll('a, span, button'); for (const el of links) { const text = el.textContent || ''; if (text.includes('重新发送') || text.includes('Resend')) { el.click(); return true; } } return false; }`) time.Sleep(2 * time.Second) // 更新邮件计数基准 initialEmailCount = getEmailCount(email) } var emailContent *EmailContent maxWaitTime := 3 * time.Minute startTime := time.Now() for time.Since(startTime) < maxWaitTime { // 快速检查新邮件(只接受数量增加的情况) emailContent, _ = getVerificationEmailAfter(email, 1, 1, initialEmailCount) if emailContent != nil { break } time.Sleep(2 * time.Second) } if emailContent == nil { result.Error = fmt.Errorf("无法获取验证码邮件") return result } // 提取验证码 code, err := extractVerificationCode(emailContent.Content) if err != nil { continue // 重试 } // 输入验证码 - OTP 风格使用键盘逐字符输入 log.Printf("[Cookie刷新] [%s] ⌨️ 开始输入验证码: %s", email, code) time.Sleep(500 * time.Millisecond) // 查找第一个可见输入框并点击获取焦点 codeInputs, _ := page.Elements("input:not([type='hidden'])") var firstCodeInput *rod.Element for _, el := range codeInputs { visible, _ := el.Visible() if visible { firstCodeInput = el break } } if firstCodeInput != nil { // 清空所有输入框 page.Eval(`() => { const inputs = document.querySelectorAll('input'); for (const inp of inputs) { inp.value = ''; } }`) time.Sleep(200 * time.Millisecond) // 点击第一个输入框获取焦点 firstCodeInput.MustClick() time.Sleep(300 * time.Millisecond) // 逐字符键盘输入(OTP 会自动跳转到下一个框) for i, char := range code { page.Keyboard.Type(input.Key(char)) if i < len(code)-1 { time.Sleep(time.Duration(100+rand.Intn(100)) * time.Millisecond) } } log.Printf("[Cookie刷新] [%s] ✅ 验证码输入完成", email) } else { log.Printf("[Cookie刷新] [%s] ⚠️ 未找到验证码输入框", email) } time.Sleep(500 * time.Millisecond) // 点击验证按钮 for i := 0; i < 5; i++ { clickResult, _ := page.Eval(`() => { const targets = ['验证', 'Verify', '继续', 'Next', 'Continue']; const els = [...document.querySelectorAll('button'), ...document.querySelectorAll('div[role="button"]')]; for (const el of els) { if (!el || el.disabled) continue; const style = window.getComputedStyle(el); if (style.display === 'none' || style.visibility === 'hidden') continue; const text = el.textContent ? el.textContent.trim() : ''; if (targets.some(t => text.includes(t))) { el.click(); return {clicked:true}; } } return {clicked:false}; }`) if clickResult != nil && clickResult.Value.Get("clicked").Bool() { break } time.Sleep(1 * time.Second) } time.Sleep(2 * time.Second) // 检测验证码错误 hasError, _ := page.Eval(`() => { const text = document.body.innerText || ''; return text.includes('验证码有误') || text.includes('incorrect') || text.includes('wrong code') || text.includes('请重试'); }`) if hasError != nil && hasError.Value.Bool() { continue // 重试 } // 验证成功,跳出重试循环 break } for i := 0; i < 15; i++ { time.Sleep(2 * time.Second) // 点击可能出现的确认按钮 page.Eval(`() => { const btns = document.querySelectorAll('button'); for (const btn of btns) { const text = btn.textContent || ''; if ((text.includes('同意') || text.includes('Confirm') || text.includes('继续') || text.includes('I agree')) && btn.offsetParent !== null && !btn.disabled) { btn.click(); return true; } } return false; }`) // 从URL提取信息 info, _ := page.Info() if info != nil { if m := regexp.MustCompile(`/cid/([a-f0-9-]+)`).FindStringSubmatch(info.URL); len(m) > 1 && configID == "" { configID = m[1] } if m := regexp.MustCompile(`[?&]csesidx=(\d+)`).FindStringSubmatch(info.URL); len(m) > 1 && csesidx == "" { csesidx = m[1] } } if authorization != "" { break } } extractResult: if authorization == "" { result.Error = fmt.Errorf("未能获取 Authorization") return result } cookies, _ := page.Cookies(nil) cookieMap := make(map[string]pool.Cookie) for _, c := range acc.Data.GetAllCookies() { cookieMap[c.Name] = c } for _, c := range cookies { cookieMap[c.Name] = pool.Cookie{ Name: c.Name, Value: c.Value, Domain: c.Domain, } } responseHeadersMu.Lock() for _, c := range newCookiesFromResponse { cookieMap[c.Name] = c } // 复制响应头 result.ResponseHeaders = make(map[string]string) for k, v := range responseHeaders { result.ResponseHeaders[k] = v } result.NewCookies = newCookiesFromResponse responseHeadersMu.Unlock() var resultCookies []pool.Cookie for _, c := range cookieMap { resultCookies = append(resultCookies, c) } info, _ = page.Info() if info != nil { currentURL = info.URL if m := regexp.MustCompile(`/cid/([a-f0-9-]+)`).FindStringSubmatch(currentURL); len(m) > 1 && configID == "" { configID = m[1] } if m := regexp.MustCompile(`[?&]csesidx=(\d+)`).FindStringSubmatch(currentURL); len(m) > 1 && csesidx == "" { csesidx = m[1] } } // 如果 csesidx 为空,尝试从 authorization 中提取 if csesidx == "" && authorization != "" { csesidx = extractCSESIDXFromAuth(authorization) if csesidx != "" { log.Printf("[Cookie刷新] [%s] 从 authorization 提取 csesidx: %s", email, csesidx) } } // 如果仍为空,尝试访问主页获取 if csesidx == "" { log.Printf("[Cookie刷新] [%s] ⚠️ csesidx 为空,尝试访问主页获取...", email) page.Navigate("https://business.gemini.google/") time.Sleep(3 * time.Second) info, _ = page.Info() if info != nil { if m := regexp.MustCompile(`[?&]csesidx=(\d+)`).FindStringSubmatch(info.URL); len(m) > 1 { csesidx = m[1] log.Printf("[Cookie刷新] [%s] 从主页URL提取 csesidx: %s", email, csesidx) } } } // 如果 csesidx 为空,尝试从 authorization 提取 if csesidx == "" && authorization != "" { csesidx = extractCSESIDXFromAuth(authorization) } // csesidx 是必须的 if csesidx == "" { result.Error = fmt.Errorf("未能获取 csesidx") return result } result.Success = true result.Authorization = authorization result.SecureCookies = resultCookies result.ConfigID = configID result.CSESIDX = csesidx return result } // extractCSESIDXFromAuth 从 authorization header 中提取 csesidx func extractCSESIDXFromAuth(auth string) string { parts := strings.Split(auth, " ") if len(parts) != 2 { return "" } jwtParts := strings.Split(parts[1], ".") if len(jwtParts) != 3 { return "" } // 解码 payload payload := jwtParts[1] // 补齐 padding switch len(payload) % 4 { case 2: payload += "==" case 3: payload += "=" } decoded, err := base64.URLEncoding.DecodeString(payload) if err != nil { decoded, err = base64.RawURLEncoding.DecodeString(jwtParts[1]) if err != nil { return "" } } // 提取 sub 字段 var claims map[string]interface{} if err := json.Unmarshal(decoded, &claims); err != nil { return "" } if sub, ok := claims["sub"].(string); ok && strings.HasPrefix(sub, "csesidx/") { return strings.TrimPrefix(sub, "csesidx/") } return "" } func NativeRegisterWorker(id int, dataDirAbs string) { time.Sleep(time.Duration(id) * 3 * time.Second) for atomic.LoadInt32(&IsRegistering) == 1 { if pool.Pool.TotalCount() >= TargetCount { return } // 获取代理(优先使用代理池) currentProxy := Proxy if GetProxy != nil { currentProxy = GetProxy() } logger.Debug("[注册线程 %d] 启动注册任务, 代理: %s", id, currentProxy) result := RunBrowserRegister(Headless, currentProxy, id) // 释放代理 if ReleaseProxy != nil && currentProxy != "" && currentProxy != Proxy { ReleaseProxy(currentProxy) } if result.Success { if err := SaveBrowserRegisterResult(result, dataDirAbs); err != nil { logger.Warn("[注册线程 %d] ⚠️ 保存失败: %v", id, err) Stats.AddFailed(err.Error()) } else { Stats.AddSuccess() pool.Pool.Load(DataDir) } } else { errMsg := "未知错误" if result.Error != nil { errMsg = result.Error.Error() } logger.Warn("[注册线程 %d] ❌ 注册失败: %s", id, errMsg) Stats.AddFailed(errMsg) if strings.Contains(errMsg, "频繁") || strings.Contains(errMsg, "rate") || strings.Contains(errMsg, "timeout") || strings.Contains(errMsg, "连接") { waitTime := 10 + id*2 logger.Debug("[注册线程 %d] ⏳ 等待 %d 秒后重试...", id, waitTime) time.Sleep(time.Duration(waitTime) * time.Second) } else { time.Sleep(3 * time.Second) } } } logger.Debug("[注册线程 %d] 停止", id) } ================================================ FILE: src/register/register.go ================================================ package register import ( "fmt" "os" "path/filepath" "sync" "sync/atomic" "time" "business2api/src/logger" "business2api/src/pool" ) // ==================== 注册与刷新 ==================== var ( DataDir string TargetCount int MinCount int CheckInterval time.Duration Threads int Headless bool // 注册无头模式 Proxy string // 代理 ) var IsRegistering int32 var registeringTarget int32 // 正在注册的目标数量 var registerMu sync.Mutex // 注册启动互斥锁 var Stats = &RegisterStats{} type RegisterStats struct { Total int `json:"total"` Success int `json:"success"` Failed int `json:"failed"` LastError string `json:"lastError"` UpdatedAt time.Time `json:"updatedAt"` mu sync.RWMutex } func (s *RegisterStats) AddSuccess() { s.mu.Lock() defer s.mu.Unlock() s.Total++ s.Success++ s.UpdatedAt = time.Now() } func (s *RegisterStats) AddFailed(err string) { s.mu.Lock() defer s.mu.Unlock() s.Total++ s.Failed++ s.LastError = err s.UpdatedAt = time.Now() } func (s *RegisterStats) Get() map[string]interface{} { s.mu.RLock() defer s.mu.RUnlock() return map[string]interface{}{ "total": s.Total, "success": s.Success, "failed": s.Failed, "last_error": s.LastError, "updated_at": s.UpdatedAt, } } // 注册结果 type RegisterResult struct { Success bool `json:"success"` Email string `json:"email"` Error string `json:"error"` NeedWait bool `json:"needWait"` } // StartRegister 启动注册任务(优化并发控制) func StartRegister(count int) error { registerMu.Lock() defer registerMu.Unlock() // 再次检查当前账号数是否已满足 pool.Pool.Load(DataDir) currentCount := pool.Pool.TotalCount() if currentCount >= TargetCount { logger.Info("✅ 账号数已满足: %d >= %d,无需注册", currentCount, TargetCount) return nil } // 如果已经在注册中,检查是否需要调整 if atomic.LoadInt32(&IsRegistering) == 1 { currentTarget := atomic.LoadInt32(®isteringTarget) if int(currentTarget) >= count { return fmt.Errorf("注册进程已在运行,目标: %d", currentTarget) } // 更新目标数量 atomic.StoreInt32(®isteringTarget, int32(count)) logger.Info("🔄 注册目标已更新: %d", count) return nil } if !atomic.CompareAndSwapInt32(&IsRegistering, 0, 1) { return fmt.Errorf("注册进程已在运行") } atomic.StoreInt32(®isteringTarget, int32(count)) // 获取数据目录的绝对路径 dataDirAbs, _ := filepath.Abs(DataDir) if err := os.MkdirAll(dataDirAbs, 0755); err != nil { atomic.StoreInt32(&IsRegistering, 0) atomic.StoreInt32(®isteringTarget, 0) return fmt.Errorf("创建数据目录失败: %w", err) } // 使用配置的线程数 threads := Threads if threads <= 0 { threads = 1 } for i := 0; i < threads; i++ { go NativeRegisterWorker(i+1, dataDirAbs) } // 监控进度 go func() { for { time.Sleep(10 * time.Second) pool.Pool.Load(DataDir) currentCount := pool.Pool.TotalCount() target := atomic.LoadInt32(®isteringTarget) // 检查是否达到目标(使用当前目标和全局目标的较大值) effectiveTarget := TargetCount if int(target) > effectiveTarget { effectiveTarget = int(target) } if currentCount >= effectiveTarget { logger.Info("✅ 已达到目标账号数: %d >= %d,停止注册", currentCount, effectiveTarget) atomic.StoreInt32(&IsRegistering, 0) atomic.StoreInt32(®isteringTarget, 0) return } } }() return nil } // PoolMaintainer 号池维护器 func PoolMaintainer() { interval := CheckInterval if interval < time.Minute { interval = 30 * time.Minute } ticker := time.NewTicker(interval) defer ticker.Stop() CheckAndMaintainPool() for range ticker.C { CheckAndMaintainPool() } } // CheckAndMaintainPool 检查并维护号池(优化并发控制) func CheckAndMaintainPool() { // 如果正在注册中,跳过检查 if atomic.LoadInt32(&IsRegistering) == 1 { logger.Debug("⏳ 注册进程运行中,跳过本次检查") return } pool.Pool.Load(DataDir) readyCount := pool.Pool.ReadyCount() pendingCount := pool.Pool.PendingCount() totalCount := pool.Pool.TotalCount() logger.Info("📊 号池检查: ready=%d, pending=%d, total=%d, 目标=%d, 最小=%d", readyCount, pendingCount, totalCount, TargetCount, MinCount) // 只有当总数小于最小数时才触发注册,避免频繁注册 if totalCount < MinCount { needCount := TargetCount - totalCount logger.Info("⚠️ 账号数低于最小值 (%d < %d),需要注册 %d 个", totalCount, MinCount, needCount) if err := StartRegister(needCount); err != nil { logger.Error("❌ 启动注册失败: %v", err) } } else if totalCount < TargetCount { logger.Debug("📊 账号数未达目标 (%d < %d),但高于最小值,暂不触发注册", totalCount, TargetCount) } } ================================================ FILE: src/utils/utils.go ================================================ package utils import ( "bytes" "compress/gzip" "crypto/tls" "encoding/json" "io" "net/http" "net/url" "time" "business2api/src/logger" "business2api/src/pool" ) // ==================== HTTP 客户端 ==================== var HTTPClient *http.Client // NewHTTPClient 创建 HTTP 客户端 func NewHTTPClient(proxy string) *http.Client { transport := &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, MaxIdleConns: 100, MaxIdleConnsPerHost: 20, MaxConnsPerHost: 50, IdleConnTimeout: 90 * time.Second, DisableCompression: false, ForceAttemptHTTP2: true, } if proxy != "" { proxyURL, err := url.Parse(proxy) if err == nil { transport.Proxy = http.ProxyURL(proxyURL) } } return &http.Client{ Transport: transport, Timeout: 1800 * time.Second, } } // InitHTTPClient 初始化全局 HTTP 客户端 func InitHTTPClient(proxy string) { HTTPClient = NewHTTPClient(proxy) pool.HTTPClient = HTTPClient if proxy != "" { logger.Info("✅ 使用代理: %s", proxy) } } // ReadResponseBody 读取 HTTP 响应体(支持 gzip) func ReadResponseBody(resp *http.Response) ([]byte, error) { var reader io.Reader = resp.Body if resp.Header.Get("Content-Encoding") == "gzip" { gzReader, err := gzip.NewReader(resp.Body) if err != nil { return nil, err } defer gzReader.Close() reader = gzReader } return io.ReadAll(reader) } // ParseNDJSON 解析 NDJSON 格式数据 func ParseNDJSON(data []byte) []map[string]interface{} { var result []map[string]interface{} lines := bytes.Split(data, []byte("\n")) for _, line := range lines { line = bytes.TrimSpace(line) if len(line) == 0 { continue } var obj map[string]interface{} if err := json.Unmarshal(line, &obj); err == nil { result = append(result, obj) } } return result } // ParseIncompleteJSONArray 解析可能不完整的 JSON 数组 func ParseIncompleteJSONArray(data []byte) []map[string]interface{} { var result []map[string]interface{} if err := json.Unmarshal(data, &result); err == nil { return result } trimmed := bytes.TrimSpace(data) if len(trimmed) > 0 && trimmed[0] == '[' { if trimmed[len(trimmed)-1] != ']' { lastBrace := bytes.LastIndex(trimmed, []byte("}")) if lastBrace > 0 { fixed := append(trimmed[:lastBrace+1], ']') if err := json.Unmarshal(fixed, &result); err == nil { logger.Warn("JSON 数组不完整,已修复") return result } } } } return nil } // TruncateString 截断字符串 func TruncateString(s string, maxLen int) string { if len(s) <= maxLen { return s } return s[:maxLen] + "..." } // Min 返回两个整数中的较小值 func Min(a, b int) int { if a < b { return a } return b }