[
  {
    "path": ".dockerignore",
    "content": "# Git\n.git\n.gitignore\n\n# Data\ndata/\n\n# Node.js files (no longer needed - registration is pure Go)\nnode_modules/\npackage-lock.json\npackage.json\nmain.js\n\n# IDE\n.idea/\n.vscode/\n\n# Build artifacts\n*.exe\n*.dll\n*.so\n*.dylib\n\n# Documentation\nREADME.md\nLICENSE\n\n# GitHub\n.github/\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where the package manifests are located.\n# Please see the documentation for all configuration options:\n# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file\n\nversion: 2\nupdates:\n  - package-ecosystem: \"\" # See documentation for possible values\n    directory: \"/\" # Location of package manifests\n    schedule:\n      interval: \"weekly\"\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "name: Build and Release\n\non:\n  push:\n    branches: [main, master]\n    tags:\n      - 'v*'\n  pull_request:\n    branches: [main, master]\n  workflow_dispatch:\n\nenv:\n  GO_VERSION: '1.23'\n  REGISTRY: ghcr.io\n  IMAGE_NAME: ${{ github.repository }}\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        goos: [linux, darwin, windows]\n        goarch: [amd64, arm64]\n        exclude:\n          - goos: windows\n            goarch: arm64\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n\n      - name: Setup Go\n        uses: actions/setup-go@v5\n        with:\n          go-version: ${{ env.GO_VERSION }}\n          cache: false\n\n      - name: Get version\n        id: version\n        run: |\n          if [[ \"$GITHUB_REF\" == refs/tags/v* ]]; then\n            VERSION=${GITHUB_REF#refs/tags/v}\n          else\n            VERSION=$(git describe --tags --always --dirty 2>/dev/null || echo \"dev\")\n          fi\n          echo \"VERSION=$VERSION\" >> $GITHUB_OUTPUT\n          echo \"Building version: $VERSION\"\n\n      - name: Build\n        env:\n          GOOS: ${{ matrix.goos }}\n          GOARCH: ${{ matrix.goarch }}\n          VERSION: ${{ steps.version.outputs.VERSION }}\n          GOTOOLCHAIN: auto\n        run: |\n          EXT=\"\"\n          if [ \"$GOOS\" = \"windows\" ]; then\n            EXT=\".exe\"\n          fi\n          CGO_ENABLED=0 go build \\\n            -tags \"with_quic,with_utls\" \\\n            -ldflags=\"-s -w -X main.Version=${VERSION}\" \\\n            -o business2api-${{ matrix.goos }}-${{ matrix.goarch }}${EXT} .\n\n      - name: Prepare package\n        run: |\n          mkdir -p dist\n          mv business2api-* dist/\n          cp config/config.json.example dist/\n          cp README.md dist/\n\n      - name: Upload Artifact\n        uses: actions/upload-artifact@v4\n        with:\n          name: business2api-${{ matrix.goos }}-${{ matrix.goarch }}\n          path: dist/*\n          retention-days: 30\n\n  docker:\n    runs-on: ubuntu-latest\n    needs: build\n    if: github.event_name == 'push'\n    permissions:\n      contents: read\n      packages: write\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v3\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n\n      - name: Login to GitHub Container Registry\n        uses: docker/login-action@v3\n        with:\n          registry: ${{ env.REGISTRY }}\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Extract metadata\n        id: meta\n        uses: docker/metadata-action@v5\n        with:\n          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}\n          tags: |\n            type=ref,event=branch\n            type=semver,pattern={{version}}\n            type=semver,pattern={{major}}.{{minor}}\n            type=sha,prefix=\n\n      - name: Build and push\n        uses: docker/build-push-action@v5\n        with:\n          context: .\n          platforms: linux/amd64,linux/arm64\n          push: true\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n          cache-from: type=gha\n          cache-to: type=gha,mode=max\n\n  release:\n    runs-on: ubuntu-latest\n    needs: build\n    if: startsWith(github.ref, 'refs/tags/v')\n    permissions:\n      contents: write\n\n    steps:\n      - name: Download all artifacts\n        uses: actions/download-artifact@v4\n        with:\n          path: artifacts\n\n      - name: Prepare release files\n        run: |\n          mkdir -p release\n          for dir in artifacts/business2api-*/; do\n            # 获取平台名称\n            platform=$(basename \"$dir\")\n            # 创建临时目录\n            mkdir -p \"tmp/$platform\"\n            cp \"$dir\"/* \"tmp/$platform/\"\n            # 设置可执行权限\n            for f in \"tmp/$platform\"/business2api-*; do\n              if [[ ! \"$f\" == *.exe ]]; then\n                chmod +x \"$f\"\n              fi\n            done\n            # 打包\n            tar -czvf \"release/${platform}.tar.gz\" -C tmp \"$platform\"\n            rm -rf \"tmp/$platform\"\n          done\n          rm -rf tmp\n\n      - name: Create Release\n        uses: softprops/action-gh-release@v1\n        with:\n          files: release/*\n          generate_release_notes: true\n"
  },
  {
    "path": ".github/workflows/codeql.yml",
    "content": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# You may wish to alter this file to override the set of languages analyzed,\n# or to provide custom queries or build logic.\n#\n# ******** NOTE ********\n# We have attempted to detect the languages in your repository. Please check\n# the `language` matrix defined below to confirm you have the correct set of\n# supported CodeQL languages.\n#\nname: \"CodeQL Advanced\"\n\non:\n  push:\n    branches: [ \"master\" ]\n  pull_request:\n    branches: [ \"master\" ]\n  schedule:\n    - cron: '33 14 * * 3'\n\njobs:\n  analyze:\n    name: Analyze (${{ matrix.language }})\n    # Runner size impacts CodeQL analysis time. To learn more, please see:\n    #   - https://gh.io/recommended-hardware-resources-for-running-codeql\n    #   - https://gh.io/supported-runners-and-hardware-resources\n    #   - https://gh.io/using-larger-runners (GitHub.com only)\n    # Consider using larger runners or machines with greater resources for possible analysis time improvements.\n    runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}\n    permissions:\n      # required for all workflows\n      security-events: write\n\n      # required to fetch internal or private CodeQL packs\n      packages: read\n\n      # only required for workflows in private repositories\n      actions: read\n      contents: read\n\n    strategy:\n      fail-fast: false\n      matrix:\n        include:\n        - language: actions\n          build-mode: none\n        - language: go\n          build-mode: autobuild\n        # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift'\n        # Use `c-cpp` to analyze code written in C, C++ or both\n        # Use 'java-kotlin' to analyze code written in Java, Kotlin or both\n        # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both\n        # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,\n        # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.\n        # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how\n        # 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\n    steps:\n    - name: Checkout repository\n      uses: actions/checkout@v4\n\n    # Add any setup steps before running the `github/codeql-action/init` action.\n    # This includes steps like installing compilers or runtimes (`actions/setup-node`\n    # or others). This is typically only required for manual builds.\n    # - name: Setup runtime (example)\n    #   uses: actions/setup-example@v1\n\n    # Initializes the CodeQL tools for scanning.\n    - name: Initialize CodeQL\n      uses: github/codeql-action/init@v4\n      with:\n        languages: ${{ matrix.language }}\n        build-mode: ${{ matrix.build-mode }}\n        # If you wish to specify custom queries, you can do so here or in a config file.\n        # By default, queries listed here will override any specified in a config file.\n        # Prefix the list here with \"+\" to use these queries and those in the config file.\n\n        # 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\n        # queries: security-extended,security-and-quality\n\n    # If the analyze step fails for one of the languages you are analyzing with\n    # \"We were unable to automatically build your code\", modify the matrix above\n    # to set the build mode to \"manual\" for that language. Then modify this step\n    # to build your code.\n    # ℹ️ Command-line programs to run using the OS shell.\n    # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun\n    - name: Run manual build steps\n      if: matrix.build-mode == 'manual'\n      shell: bash\n      run: |\n        echo 'If you are using a \"manual\" build mode for one or more of the' \\\n          'languages you are analyzing, replace this with the commands to build' \\\n          'your code, for example:'\n        echo '  make bootstrap'\n        echo '  make release'\n        exit 1\n\n    - name: Perform CodeQL Analysis\n      uses: github/codeql-action/analyze@v4\n      with:\n        category: \"/language:${{matrix.language}}\"\n"
  },
  {
    "path": ".gitignore",
    "content": "# Binaries\ngemini-gateway\ngemini-gateway.exe\n*.exe\n*.dll\n*.so\n*.dylib\n\n# Data\ndata/\n*.json\n!config.json.example\n!package.json\n\n# Node\nnode_modules/\nnpm-debug.log*\n\n# IDE\n.idea/\n.vscode/\n*.swp\n*.swo\n\n# OS\n*.zip\n.DS_Store\nThumbs.db\n\n# Build\ndist/\nbuild/\ndata/\nconfig.json\npackage-lock.json\nmain.js\npackage.json\nflow.txt\nconfig/config.json\nbusiness2api\nflow/\n"
  },
  {
    "path": "Dockerfile",
    "content": "# Build stage\nFROM golang:1.23-alpine AS builder\n\nWORKDIR /app\n\n# Install build dependencies\nRUN apk add --no-cache git ca-certificates\n\n# Copy go mod files\nCOPY go.mod go.sum ./\nENV GOTOOLCHAIN=auto\nRUN go mod download\n\n# Copy source code\nCOPY *.go ./\nCOPY src/ ./src/\n\n# Build binary\nRUN CGO_ENABLED=0 GOOS=linux go build -tags \"with_quic,with_utls\" -ldflags=\"-s -w\" -o business2api .\n\n# Runtime stage\nFROM alpine:latest\n\nWORKDIR /app\n\n# Install runtime dependencies (Chromium for rod browser automation)\nRUN apk add --no-cache \\\n    ca-certificates \\\n    tzdata \\\n    chromium \\\n    nss \\\n    freetype \\\n    harfbuzz \\\n    ttf-freefont \\\n    font-noto-cjk\n\n# Copy binary from builder\nCOPY --from=builder /app/business2api .\n\n# Copy config template if exists\nCOPY config.json.exampl[e] ./\n\n# Create data directory\nRUN mkdir -p /app/data\n\n# Environment variables\nENV LISTEN_ADDR=\":8000\"\nENV DATA_DIR=\"/app/data\"\n\nEXPOSE 8000\n\nENTRYPOINT [\"./business2api\"]\n"
  },
  {
    "path": "README.md",
    "content": "# Business2API\n\n> 🚀 OpenAI/Gemini 兼容的 Gemini Business API 代理服务，支持账号池管理、自动注册和 Flow 图片/视频生成。\n\n[![Build](https://github.com/XxxXTeam/business2api/actions/workflows/build.yml/badge.svg)](https://github.com/XxxXTeam/business2api/actions/workflows/build.yml)\n[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)\n[![Go Version](https://img.shields.io/badge/Go-1.24+-00ADD8?logo=go)](https://golang.org)\n\n## ✨ 功能特性\n\n| 功能 | 描述 |\n|------|------|\n| 🔌 **多 API 兼容** | OpenAI (`/v1/chat/completions`)、Gemini (`/v1beta/models`)、Claude (`/v1/messages`) |\n| 🏊 **智能账号池** | 自动轮询、刷新、冷却管理、401/403 自动换号 |\n| 🌊 **流式响应** | SSE 流式输出，支持 `stream: true` |\n| 🎨 **多模态** | 图片/视频输入、原生图片生成（`-image` 后缀）|\n| 🤖 **自动注册** | 浏览器自动化注册，支持 Windows/Linux/macOS |\n| 🌐 **代理池** | HTTP/SOCKS5 代理，订阅链接，健康检查 |\n| 📊 **遥测监控** | IP 请求统计、Token 使用量、RPM 监控 |\n| 🔄 **热重载** | 配置文件自动监听，无需重启 |\n\n## 📦 支持的模型\n\n### Gemini Business 模型\n\n| 模型 | 文本 | 图片生成 | 视频生成 | 搜索 |\n|------|:----:|:--------:|:--------:|:----:|\n| gemini-2.5-flash | ✅ | ✅ | ✅ | ✅ |\n| gemini-2.5-pro | ✅ | ✅ | ✅ | ✅ |\n| gemini-2.5-flash-preview-latest | ✅ | ✅ | ✅ | ✅ |\n| gemini-3-pro-preview | ✅ | ✅ | ✅ | ✅ |\n| gemini-3-flash-preview | ✅ | ✅ | ✅ | ✅ |\n| gemini-3-flash | ✅ | ✅ | ✅ | ✅ |\n\n### 功能后缀\n\n支持单个或混合后缀启用指定功能：\n\n| 后缀 | 功能 | 示例 |\n|------|------|------|\n| `-image` | 图片生成 | `gemini-2.5-flash-image` |\n| `-video` | 视频生成 | `gemini-2.5-flash-video` |\n| `-search` | 联网搜索 | `gemini-2.5-flash-search` |\n| 混合后缀 | 同时启用多功能 | `gemini-2.5-flash-image-search` |\n\n**说明：**\n- 无后缀：启用所有功能（图片/视频/搜索/工具）\n- 有后缀：只启用指定功能，支持任意组合如 `-image-search`、`-video-search`\n\n### ⚠️ 限制说明\n\n| 限制 | 说明 |\n|------|------|\n| **不支持自定义工具** | Function Calling / Tools 参数会被忽略，仅支持内置工具（图片/视频生成、搜索） |\n| **上下文拼接实现** | 多轮对话通过拼接 `messages` 为单次请求实现，非原生会话管理 |\n| **无状态** | 每次请求独立，不保留会话状态，历史消息需客户端自行维护 |\n\n---\n\n\n>> 公益 Demo（免费调用）  \n> 🔗 链接：<https://business2api.openel.top>\n>\n> 在线绘图预设测试 [https://chat.openel.top](https://chat.openel.top/)\n\n<img width=\"1880\" height=\"919\" alt=\"image\" src=\"https://github.com/user-attachments/assets/d05d4b06-2c2a-468f-b8fb-fb6dad8dc3ab\" />\n\n\n>\n> > API Key 获取请访问 https://business2api.openel.top/auth 获取个人专属免费APIKEY\n\n\n>> GLM 公益测试 API\n> 🔗 链接：<https://GLM.openel.top>\n>\n> XiaoMi 网页逆向公益 API\n> 🔗 链接：[https://xiaomi.openel.top](https://xiaomi.openel.top/)\n>\n>  API Key : `sk-3d2f9b84e7f510b1a08f7b3d6c9a6a7f17fbbad5624ea29f22d9c742bf39c863`\n\n\n\n## 快速开始\n\n### 方式一：Docker 部署（推荐）\n\n#### 1. 使用 Docker Compose\n\n```bash\n# 创建目录\nmkdir business2api && cd business2api\n\n# 下载必要文件\nwget https://raw.githubusercontent.com/XxxXTeam/business2api/master/docker/docker-compose.yml\nwget https://raw.githubusercontent.com/XxxXTeam/business2api/master/config/config.json.example -O config.json\n\n# 编辑配置\nvim config.json\n\n# 创建数据目录\nmkdir data\n\n# 启动服务\ndocker compose up -d\n```\n\n#### 2. 使用 Docker Run\n\n```bash\n# 拉取镜像\ndocker pull ghcr.io/xxxteam/business2api:latest\n\n# 创建配置文件\nwget https://raw.githubusercontent.com/XxxXTeam/business2api/master/config/config.json.example -O config.json\n\n# 运行容器\ndocker run -d \\\n  --name business2api \\\n  -p 8000:8000 \\\n  -v $(pwd)/data:/app/data \\\n  -v $(pwd)/config.json:/app/config/config.json:ro \\\n  ghcr.io/xxxteam/business2api:latest\n```\n\n### 方式二：二进制部署\n\n#### 1. 下载预编译版本\n\n从 [Releases](https://github.com/XxxXTeam/business2api/releases) 下载对应平台的二进制文件。\n\n```bash\n# Linux amd64\nwget https://github.com/XxxXTeam/business2api/releases/latest/download/business2api-linux-amd64.tar.gz\ntar -xzf business2api-linux-amd64.tar.gz\nchmod +x business2api-linux-amd64\n```\n\n#### 2. 从源码编译\n\n```bash\n# 需要 Go 1.24+\ngit clone https://github.com/XxxXTeam/business2api.git\ncd business2api\n\n# 编译\ngo build -o business2api .\n\n# 运行\n./business2api\n```\n\n### 方式三：使用 Systemd 服务\n\n```bash\n# 创建服务文件\nsudo tee /etc/systemd/system/business2api.service << EOF\n[Unit]\nDescription=Gemini Gateway Service\nAfter=network.target\n\n[Service]\nType=simple\nUser=nobody\nWorkingDirectory=/opt/business2api\nExecStart=/opt/business2api/business2api\nRestart=always\nRestartSec=5\nEnvironment=LISTEN_ADDR=:8000\nEnvironment=DATA_DIR=/opt/business2api/data\n\n[Install]\nWantedBy=multi-user.target\nEOF\n\n# 启动服务\nsudo systemctl daemon-reload\nsudo systemctl enable business2api\nsudo systemctl start business2api\n```\n\n---\n\n## 配置说明\n\n### config.json\n\n```json\n{\n  \"api_keys\": [\"sk-your-api-key\"],    // API 密钥列表，用于鉴权\n  \"listen_addr\": \":8000\",              // 监听地址\n  \"data_dir\": \"./data\",                // 账号数据目录\n  \"default_config\": \"\",                // 默认 configId（可选）\n  \"debug\": false,                      // 调试模式（输出详细日志）\n  \n  \"pool\": {\n    \"target_count\": 50,                // 目标账号数量\n    \"min_count\": 10,                   // 最小账号数，低于此值触发注册\n    \"check_interval_minutes\": 30,      // 检查间隔（分钟）\n    \"register_threads\": 1,             // 本地注册线程数\n    \"register_headless\": true,         // 无头模式注册\n    \"refresh_on_startup\": true,        // 启动时刷新账号\n    \"refresh_cooldown_sec\": 240,       // 刷新冷却时间（秒）\n    \"use_cooldown_sec\": 15,            // 使用冷却时间（秒）\n    \"max_fail_count\": 3,               // 最大连续失败次数\n    \"enable_browser_refresh\": true,    // 启用浏览器刷新401账号\n    \"browser_refresh_headless\": true,  // 浏览器刷新无头模式\n    \"browser_refresh_max_retry\": 1,    // 浏览器刷新最大重试次数\n    \"auto_delete_401\": false           // 401时自动删除账号\n  },\n\n  \"pool_server\": {                     \n    \"enable\": false,                   // 是否启用分离模式\n    \"mode\": \"local\",                   // 运行模式：local/server/client\n    \"server_addr\": \"http://ip:8000\",   // 服务器地址（client模式）\n    \"listen_addr\": \":8000\",            // 监听地址（server模式）\n    \"secret\": \"your-secret-key\",       // 通信密钥\n    \"target_count\": 50,                // 目标账号数（server模式）\n    \"client_threads\": 2,               // 客户端并发线程数\n    \"data_dir\": \"./data\",              // 数据目录（server模式）\n    \"expired_action\": \"delete\"         // 过期账号处理：delete/refresh/queue\n  },\n\n  \"proxy_pool\": {\n    \"subscribes\": [],                  // 代理订阅链接列表\n    \"files\": [],                       // 本地代理文件列表\n    \"health_check\": true,              // 启用健康检查\n    \"check_on_startup\": true           // 启动时检查\n  }\n}\n```\n\n### 多 API Key 支持\n\n支持配置多个 API Key，所有 Key 都可以用于鉴权：\n\n```json\n{\n  \"api_keys\": [\n    \"sk-key-1\",\n    \"sk-key-2\", \n    \"sk-key-3\"\n  ]\n}\n```\n\n### 配置热重载\n\n服务运行时自动监听 `config/config.json` 文件变更，无需重启即可生效。\n\n**可热重载的配置项：**\n\n| 配置项 | 说明 |\n|----------|------|\n| `api_keys` | API 密钥列表 |\n| `debug` | 调试模式 |\n| `pool.refresh_cooldown_sec` | 刷新冷却时间 |\n| `pool.use_cooldown_sec` | 使用冷却时间 |\n| `pool.max_fail_count` | 最大失败次数 |\n| `pool.enable_browser_refresh` | 浏览器刷新开关 | \n\n**配置合并机制：** 配置文件中缺失的字段会自动使用默认值，无需手动同步示例文件。\n\n```bash\n# 手动触发重载\ncurl -X POST http://localhost:8000/admin/reload-config \\\n  -H \"Authorization: Bearer sk-your-api-key\"\n```\n\n---\n\n## C/S 分离架构\n\n支持将号池管理与API服务分离部署，适用于多节点场景。\n\n### 架构说明\n\n```\n┌─────────────────┐         ┌─────────────────┐\n│   API Server    │◄───────►│   Pool Server   │\n│   (客户端模式)   │   HTTP   │   (服务器模式)   │\n└─────────────────┘         └────────┬────────┘\n                                     │\n                            WebSocket│\n                                     │\n                            ┌────────▼────────┐\n                            │  Worker Client  │\n                            │  (注册/续期)     │\n                            └─────────────────┘\n```\n\n### 运行模式\n\n| 模式 | 说明 |\n|------|------|\n| `local` | 本地模式（默认），API服务和号池管理在同一进程 |\n| `server` | 服务器模式，提供号池服务和任务分发 |\n| `client` | 客户端模式，只接收任务（注册/续期），不提供API服务 |\n\n### Server 模式配置\n\n```json\n{\n  \"api_keys\": [\"sk-your-api-key\"],\n  \"listen_addr\": \":8000\",\n  \"pool_server\": {\n    \"enable\": true,\n    \"mode\": \"server\",\n    \"secret\": \"shared-secret-key\",\n    \"target_count\": 100,\n    \"data_dir\": \"./data\",\n    \"expired_action\": \"delete\"\n  }\n}\n```\n\n### Client 模式配置（仅注册/续期工作节点）\n\n```json\n{\n  \"pool_server\": {\n    \"enable\": true,\n    \"mode\": \"client\",\n    \"server_addr\": \"http://server-ip:8000\",\n    \"secret\": \"shared-secret-key\",\n    \"client_threads\": 3\n  },\n  \"proxy_pool\": {\n    \"subscribes\": [\"https://your-proxy-subscribe-url\"],\n    \"health_check\": true,\n    \"check_on_startup\": true\n  }\n}\n```\n\n### 配置项说明\n\n| 配置项 | 说明 | 默认值 |\n|--------|------|--------|\n| `client_threads` | 客户端并发任务数 | 1 |\n| `expired_action` | 过期账号处理方式 | delete |\n\n**expired_action 可选值：**\n- `delete` - 删除过期账号\n- `refresh` - 尝试浏览器刷新\n- `queue` - 保留在队列等待重试\n\n**架构说明（v2.x）：**\n```\n┌─────────────────────────────────────────────────────┐\n│                   Server (:8000)                     │\n│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐  │\n│  │   API 服务   │  │   WS 服务   │  │  号池管理    │  │\n│  │ /v1/chat/*  │  │    /ws      │  │  Pool Mgr   │  │\n│  └─────────────┘  └──────┬──────┘  └─────────────┘  │\n└──────────────────────────┼──────────────────────────┘\n                           │ WebSocket\n              ┌────────────┼────────────┐\n              │            │            │\n        ┌─────▼─────┐ ┌────▼────┐ ┌─────▼─────┐\n        │  Client1  │ │ Client2 │ │  Client3  │\n        │  (注册)    │ │ (注册)   │ │  (注册)   │\n        └───────────┘ └─────────┘ └───────────┘\n```\n\n**Client 模式说明：**\n- 通过 WebSocket 连接 Server (`/ws`) 接收任务\n- 执行注册新账号任务\n- 执行401账号Cookie续期任务\n- 完成后自动回传账号数据到Server\n- **不提供API服务**，只作为工作节点\n\n### 环境变量\n\n| 变量 | 说明 | 默认值 |\n|------|------|--------|\n| `LISTEN_ADDR` | 监听地址 | `:8000` |\n| `DATA_DIR` | 数据目录 | `./data` |\n| `PROXY` | 代理地址 | - |\n| `API_KEY` | API 密钥 | - |\n| `CONFIG_ID` | 默认 configId | - |\n\n---\n\n## API 使用\n\n### 获取模型列表\n\n```bash\ncurl http://localhost:8000/v1/models \\\n  -H \"Authorization: Bearer sk-your-api-key\"\n```\n\n### 聊天补全\n\n```bash\ncurl http://localhost:8000/v1/chat/completions \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Authorization: Bearer sk-your-api-key\" \\\n  -d '{\n    \"model\": \"gemini-2.5-flash\",\n    \"messages\": [\n      {\"role\": \"user\", \"content\": \"Hello!\"}\n    ],\n    \"stream\": true\n  }'\n```\n\n### 多模态（图片输入）\n\n```bash\ncurl http://localhost:8000/v1/chat/completions \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Authorization: Bearer sk-your-api-key\" \\\n  -d '{\n    \"model\": \"gemini-2.5-flash\",\n    \"messages\": [\n      {\n        \"role\": \"user\",\n        \"content\": [\n          {\"type\": \"text\", \"text\": \"描述这张图片\"},\n          {\"type\": \"image_url\", \"image_url\": {\"url\": \"data:image/jpeg;base64,...\"}}\n        ]\n      }\n    ]\n  }'\n```\n\n---\n\n## Flow 图片/视频生成\n\nFlow 集成了 Google VideoFX (Veo/Imagen) API，支持图片和视频生成。\n\n### 配置\n\n```json\n{\n  \"flow\": {\n    \"enable\": true,\n    \"tokens\": [],              // 配置文件中的 Token（可选）\n    \"proxy\": \"\",               // Flow 专用代理\n    \"timeout\": 120,            // 超时时间(秒)\n    \"poll_interval\": 3,        // 轮询间隔(秒)\n    \"max_poll_attempts\": 500   // 最大轮询次数\n  }\n}\n```\n\n### 获取 Flow Token\n\n**方式一：文件目录（推荐）**\n\n将完整的 cookie 字符串保存到 `data/at/` 目录下的任意 `.txt` 文件：\n\n```bash\nmkdir -p data/at\necho \"your-cookie-string\" > data/at/account1.txt\n```\n\n服务启动时自动加载，支持文件监听自动热加载。\n\n**方式二：API 添加**\n\n```bash\ncurl -X POST http://localhost:8000/admin/flow/add-token \\\n  -H \"Authorization: Bearer sk-xxx\" \\\n  -d '{\"cookie\": \"your-cookie-string\"}'\n```\n\n**Cookie 获取方法：**\n1. 访问 [labs.google/fx](https://labs.google/fx) 并登录\n2. 打开开发者工具 → Application → Cookies\n3. 复制所有 cookie 或 `__Secure-next-auth.session-token` 的值\n\n### Flow 模型列表\n\n| 模型 | 类型 | 说明 |\n|------|------|------|\n| `gemini-2.5-flash-image-landscape/portrait` | 图片 | Gemini 2.5 Flash 图片生成 |\n| `gemini-3.0-pro-image-landscape/portrait` | 图片 | Gemini 3.0 Pro 图片生成 |\n| `imagen-4.0-generate-preview-landscape/portrait` | 图片 | Imagen 4.0 图片生成 |\n| `veo_3_1_t2v_fast_landscape/portrait` | 视频 | Veo 3.1 文生视频 |\n| `veo_2_1_fast_d_15_t2v_landscape/portrait` | 视频 | Veo 2.1 文生视频 |\n| `veo_2_0_t2v_landscape/portrait` | 视频 | Veo 2.0 文生视频 |\n| `veo_3_1_i2v_s_fast_fl_landscape/portrait` | 视频 | Veo 3.1 图生视频 (I2V) |\n| `veo_2_1_fast_d_15_i2v_landscape/portrait` | 视频 | Veo 2.1 图生视频 (I2V) |\n| `veo_2_0_i2v_landscape/portrait` | 视频 | Veo 2.0 图生视频 (I2V) |\n| `veo_3_0_r2v_fast_landscape/portrait` | 视频 | Veo 3.0 多图生视频 (R2V) |\n\n### 使用示例\n\n```bash\n# 图片生成\ncurl http://localhost:8000/v1/chat/completions \\\n  -H \"Authorization: Bearer sk-xxx\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"model\": \"gemini-2.5-flash-image-landscape\", \"messages\": [{\"role\": \"user\", \"content\": \"一只可爱的猫咪\"}], \"stream\": true}'\n\n# 文生视频 (T2V)\ncurl http://localhost:8000/v1/chat/completions \\\n  -H \"Authorization: Bearer sk-xxx\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"model\": \"veo_3_1_t2v_fast_landscape\", \"messages\": [{\"role\": \"user\", \"content\": \"猫咪在草地上追蝴蝶\"}], \"stream\": true}'\n\n# 图生视频 (I2V) - 支持首尾帧\ncurl http://localhost:8000/v1/chat/completions \\\n  -H \"Authorization: Bearer sk-xxx\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"model\": \"veo_3_1_i2v_s_fast_fl_landscape\",\n    \"messages\": [{\n      \"role\": \"user\",\n      \"content\": [\n        {\"type\": \"text\", \"text\": \"猫咪跳跃\"},\n        {\"type\": \"image_url\", \"image_url\": {\"url\": \"data:image/jpeg;base64,...\"}}\n      ]\n    }],\n    \"stream\": true\n  }'\n```\n\n---\n\n## 🔧 常见问题与解决方案\n\n### 注册相关\n\n> **拟人化行为**：浏览器自动化已内置拟人化操作（随机延迟、贝塞尔曲线鼠标移动、自然打字节奏），以降低被检测风险。可通过 `register_headless: false` 开启可视模式观察行为。\n\n| 错误 | 原因 | 解决方案 |\n|------|------|----------|\n| `无法获取验证码邮件` | 临时邮箱服务不稳定或邮件延迟 | 代理遭到拉黑，更换代理 |\n| `panic: nil pointer` | 浏览器启动失败或页面未加载 | 检查 Chrome 是否安装，确保有足够内存 |\n| `找不到提交按钮` | 页面结构变化或加载超时 | 升级到最新版本，检查网络 |\n\n\n### API 相关\n\n| 错误 | 原因 | 解决方案 |\n|------|------|----------|\n| `401 Unauthorized` | API Key 无效或未配置 | 检查 `api_keys` 配置 |\n| `429 Too Many Requests` | 账号触发速率限制 | 增加账号池数量，调整 `use_cooldown_sec` |\n| `503 Service Unavailable` | 无可用账号 | 等待账号刷新或增加注册 |\n| `空响应` | Google 返回空内容 | 重试请求，检查 prompt 是否触发过滤 |\n\n### WebSocket 相关\n\n| 错误 | 原因 | 解决方案 |\n|------|------|----------|\n| `客户端频繁断开` | 心跳超时或网络不稳定 | 检查网络，确保 Server 和 Client 时间同步 |\n| `上传注册结果失败` | Server 端口或路径错误 | 确保 `server_addr` 指向正确地址 |\n\n### Flow 相关\n\n| 错误 | 原因 | 解决方案 |\n|------|------|----------|\n| `Flow 服务未启用` | 未配置或 Token 为空 | 检查 `flow.enable` 和 `flow.tokens` |\n| `Token 认证失败` | ST Token 过期 | 重新获取 Token |\n| `视频生成超时` | 生成时间过长 | 增加 `max_poll_attempts` |\n\n### Docker 相关\n\n| 错误 | 原因 | 解决方案 |\n|------|------|----------|\n| `无法启动浏览器` | Docker 容器缺少 Chrome | 使用包含 Chrome 的镜像或挂载主机浏览器 |\n| `权限被拒绝` | 数据目录权限问题 | `chown -R 1000:1000 ./data` |\n\n---\n\n## 📡 API 端点一览\n\n### 公开端点\n\n| 端点 | 方法 | 说明 |\n|------|------|------|\n| `/` | GET | 服务状态和信息 |\n| `/health` | GET | 健康检查 |\n| `/ws` | WS | WebSocket 端点 (Server 模式) |\n\n### API 端点（需要 API Key）\n\n| 端点 | 方法 | 说明 |\n|------|------|------|\n| `/v1/models` | GET | OpenAI 格式模型列表 |\n| `/v1/chat/completions` | POST | OpenAI 格式聊天补全 |\n| `/v1/messages` | POST | Claude 格式消息 |\n| `/v1beta/models` | GET | Gemini 格式模型列表 |\n| `/v1beta/models/:model` | GET | Gemini 格式模型详情 |\n| `/v1beta/models/:model:generateContent` | POST | Gemini 格式生成内容 |\n\n### 管理端点（需要 API Key）\n\n| 端点 | 方法 | 说明 |\n|------|------|------|\n| `/admin/status` | GET | 账号池状态 |\n| `/admin/stats` | GET | 详细 API 统计 |\n| `/admin/ip` | GET | IP 遥测统计（请求数/Token/RPM） |\n| `/admin/register` | POST | 触发注册 |\n| `/admin/refresh` | POST | 刷新账号池 |\n| `/admin/reload-config` | POST | 热重载配置文件 |\n| `/admin/force-refresh` | POST | 强制刷新所有账号 |\n| `/admin/config/cooldown` | POST | 动态调整冷却时间 |\n| `/admin/browser-refresh` | POST | 手动触发浏览器刷新指定账号 |\n| `/admin/config/browser-refresh` | POST | 配置浏览器刷新开关 |\n| `/admin/flow/status` | GET | Flow 服务状态 |\n| `/admin/flow/add-token` | POST | 添加 Flow Token |\n| `/admin/flow/remove-token` | POST | 移除 Flow Token |\n| `/admin/flow/reload` | POST | 重新加载 Flow Token |\n\n---\n\n## 🛠️ 开发\n\n### 本地运行\n\n```bash\n# 安装依赖\ngo mod download\n\n# 运行\ngo run .\n\n# 调试模式\ngo run . -d\n```\n\n### 构建\n\n```bash\n# 标准构建\ngo build -o business2api .\n\n# 带 QUIC/uTLS 支持（推荐）\ngo build -tags \"with_quic with_utls\" -o business2api .\n\n# 生产构建（压缩体积）\nCGO_ENABLED=0 go build -ldflags=\"-s -w\" -tags \"with_quic with_utls\" -o business2api .\n\n# 多平台构建\nGOOS=linux GOARCH=amd64 go build -tags \"with_quic with_utls\" -o business2api-linux-amd64 .\nGOOS=windows GOARCH=amd64 go build -tags \"with_quic with_utls\" -o business2api-windows-amd64.exe .\nGOOS=darwin GOARCH=arm64 go build -tags \"with_quic with_utls\" -o business2api-darwin-arm64 .\n```\n\n### 项目结构\n\n```\n.\n├── main.go              # 主程序入口\n├── config/              # 配置文件\n│   ├── config.json.example\n│   └── README.md\n├── src/\n│   ├── flow/            # Flow 图片/视频生成\n│   ├── logger/          # 日志模块\n│   ├── pool/            # 账号池管理（C/S架构）\n│   ├── proxy/           # 代理池管理\n│   ├── register/        # 浏览器自动注册\n│   └── utils/           # 工具函数\n├── docker/              # Docker 相关\n│   └── docker-compose.yml\n└── .github/             # GitHub Actions\n```\n\n---\n\n## 📊 IP 遥测接口\n\n访问 `/admin/ip` 获取全部 IP 请求统计：\n\n```bash\ncurl http://localhost:8000/admin/ip \\\n  -H \"Authorization: Bearer sk-your-api-key\"\n```\n\n**返回字段说明：**\n\n| 字段 | 说明 |\n|------|------|\n| `unique_ips` | 独立 IP 数量 |\n| `total_requests` | 总请求数 |\n| `total_tokens` | 总 Token 消耗 |\n| `total_images` | 图片生成数 |\n| `total_videos` | 视频生成数 |\n| `ips[].rpm` | 单 IP 每分钟请求数 |\n| `ips[].input_tokens` | 输入 Token |\n| `ips[].output_tokens` | 输出 Token |\n| `ips[].models` | 各模型使用次数 |\n\n---\n\n\n## Star History\n\n[![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)\n\n## 📄 License\n\nMIT License\n"
  },
  {
    "path": "config/README.md",
    "content": "# 配置说明\n\n## 代理池配置 (`proxy_pool`)\n\n**内置 xray-core**，支持 vmess、vless、shadowsocks、trojan 等协议，自动转换为本地 socks5 代理。\n\n```json\n\"proxy_pool\": {\n  \"proxy\": \"\",                    // 备用单个代理 (http/socks5 格式)\n  \"subscribes\": [                 // 订阅链接列表 (支持 base64 编码)\n    \"https://example.com/sub1\",\n    \"https://example.com/sub2\"\n  ],\n  \"files\": [                      // 本地代理文件列表\n    \"./proxies.txt\"\n  ],\n  \"health_check\": true,           // 是否启用健康检查\n  \"check_on_startup\": false       // 启动时是否检查所有节点\n}\n```\n\n### 支持的代理格式\n\n**代理文件/订阅内容格式** (每行一个):\n\n```\n# VMess\nvmess://eyJ2IjoiMiIsInBzIjoi5ZCN56ewIiwiYWRkIjoic2VydmVyLmNvbSIsInBvcnQiOiI0NDMiLCJpZCI6InV1aWQiLCJhaWQiOiIwIiwic2N5IjoiYXV0byIsIm5ldCI6IndzIiwicGF0aCI6Ii9wYXRoIiwiaG9zdCI6Imhvc3QuY29tIiwidGxzIjoidGxzIn0=\n\n# VLESS\nvless://uuid@server.com:443?type=ws&security=tls&path=/path&host=host.com&sni=sni.com#名称\n\n# Shadowsocks\nss://YWVzLTI1Ni1nY206cGFzc3dvcmQ=@server.com:8388#名称\n\n# Trojan\ntrojan://password@server.com:443?sni=sni.com#名称\n\n# 直接代理\nhttp://proxy.com:8080\nsocks5://127.0.0.1:1080\n```\n\n---\n\n## 号池配置 (`pool`)\n\n```json\n\"pool\": {\n  \"target_count\": 50,              // 目标账号数量\n  \"min_count\": 10,                 // 最小账号数，低于此值触发注册\n  \"check_interval_minutes\": 30,    // 检查间隔(分钟)\n  \"register_threads\": 1,           // 注册线程数\n  \"register_headless\": false,      // 注册时是否无头模式\n  \"refresh_on_startup\": true,      // 启动时是否刷新账号\n  \"refresh_cooldown_sec\": 240,     // 刷新冷却时间(秒)\n  \"use_cooldown_sec\": 15,          // 使用冷却时间(秒)\n  \"max_fail_count\": 3,             // 最大失败次数\n  \"enable_browser_refresh\": true,  // 启用浏览器刷新\n  \"browser_refresh_headless\": false, // 浏览器刷新无头模式\n  \"browser_refresh_max_retry\": 1   // 浏览器刷新最大重试次数\n}\n```\n\n---\n\n## 号池服务器配置 (`pool_server`)\n\n```json\n\"pool_server\": {\n  \"enable\": false,                 // 是否启用\n  \"mode\": \"local\",                 // 模式: local/server/client\n  \"server_addr\": \"\",               // 服务器地址 (客户端模式)\n  \"listen_addr\": \":8000\",          // 监听地址 (服务器模式)\n  \"secret\": \"\",                    // 认证密钥\n  \"target_count\": 50,              // 目标账号数\n  \"client_threads\": 2,             // 客户端并发线程数\n  \"data_dir\": \"./data\",            // 数据目录\n  \"expired_action\": \"delete\"       // 过期账号处理方式\n}\n```\n\n**模式说明**:\n- `local`: 本地模式，独立运行\n- `server`: 服务器模式，提供号池服务和API\n- `client`: 客户端模式，连接服务器接收注册/续期任务\n\n**expired_action 说明**:\n- `delete`: 删除过期/失败账号\n- `refresh`: 尝试浏览器刷新Cookie\n- `queue`: 保留在队列等待重试\n\n---\n\n## Flow 配置 (`flow`)\n\n```json\n\"flow\": {\n  \"enable\": false,                 // 是否启用 Flow 视频生成\n  \"tokens\": [],                    // Flow ST Tokens\n  \"proxy\": \"\",                     // Flow 专用代理\n  \"timeout\": 120,                  // 超时时间(秒)\n  \"poll_interval\": 3,              // 轮询间隔(秒)\n  \"max_poll_attempts\": 500         // 最大轮询次数\n}\n```\n\n---\n\n## 其他配置\n\n```json\n{\n  \"api_keys\": [\"key1\", \"key2\"],    // API 密钥列表\n  \"listen_addr\": \":8000\",          // 监听地址\n  \"data_dir\": \"./data\",            // 数据目录\n  \"default_config\": \"\",            // 默认 configId\n  \"debug\": false,                  // 调试模式\n  \"proxy\": \"http://127.0.0.1:10808\" // 全局代理 (兼容旧配置)\n}\n```\n"
  },
  {
    "path": "config/config.json.example",
    "content": "{\n  \"api_keys\": [\"your-api-key-here\"],\n  \"listen_addr\": \":8000\",\n  \"data_dir\": \"./data\",\n  \"default_config\": \"\",\n  \"debug\": false,\n  \"pool\": {\n    \"target_count\": 50,\n    \"min_count\": 10,\n    \"check_interval_minutes\": 30,\n    \"register_threads\": 1,\n    \"register_headless\": true,\n    \"refresh_on_startup\": true,\n    \"refresh_cooldown_sec\": 240,\n    \"use_cooldown_sec\": 15,\n    \"max_fail_count\": 3,\n    \"enable_browser_refresh\": true,\n    \"browser_refresh_headless\": true,\n    \"browser_refresh_max_retry\": 1\n  },\n  \"pool_server\": {\n    \"enable\": false,\n    \"mode\": \"local\",\n    \"server_addr\": \"http://server-ip:8000\",\n    \"listen_addr\": \":8000\",\n    \"secret\": \"your-secret-key\",\n    \"target_count\": 50,\n    \"client_threads\": 2,\n    \"data_dir\": \"./data\",\n    \"expired_action\": \"delete\"\n  },\n  \"proxy_pool\": {\n    \"subscribes\": [\n      \"http://example.com/s/example\"\n    ],\n    \"files\": [],\n    \"health_check\": true,\n    \"check_on_startup\": true\n  },\n  \"flow\": {\n    \"enable\": false,\n    \"tokens\": [],\n    \"timeout\": 120,\n    \"poll_interval\": 3,\n    \"max_poll_attempts\": 500\n  }\n}\n"
  },
  {
    "path": "docker/docker-compose.yml",
    "content": "version: '3.8'\n\nservices:\n  business2api:\n    image: ghcr.io/xxxxteam/business2api:latest\n    container_name: business2api\n    restart: unless-stopped\n    ports:\n      - \"8000:8000\"\n    volumes:\n      - ./data:/app/data\n      - ./config.json:/app/config/config.json:ro\n    environment:\n      - TZ=Asia/Shanghai\n      - LISTEN_ADDR=:8000\n      - DATA_DIR=/app/data\n      # - PROXY=http://proxy:port\n      # - API_KEY=your-api-key\n    healthcheck:\n      test: [\"CMD\", \"wget\", \"-q\", \"--spider\", \"http://localhost:8000/v1/models\"]\n      interval: 30s\n      timeout: 10s\n      retries: 3\n    logging:\n      driver: json-file\n      options:\n        max-size: \"10m\"\n        max-file: \"3\"\n"
  },
  {
    "path": "go.mod",
    "content": "module business2api\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/fsnotify/fsnotify v1.9.0\n\tgithub.com/gin-gonic/gin v1.9.1\n\tgithub.com/go-rod/rod v0.116.2\n\tgithub.com/google/uuid v1.6.0\n\tgithub.com/gorilla/websocket v1.5.3\n\tgithub.com/sagernet/sing-box v1.12.12\n\tgithub.com/sagernet/sing-quic v0.5.2-0.20250909083218-00a55617c0fb\n\tgolang.org/x/image v0.38.0\n)\n\nrequire (\n\tfilippo.io/edwards25519 v1.1.1 // indirect\n\tgithub.com/ajg/form v1.5.1 // indirect\n\tgithub.com/akutz/memconn v0.1.0 // indirect\n\tgithub.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect\n\tgithub.com/andybalholm/brotli v1.1.0 // indirect\n\tgithub.com/anytls/sing-anytls v0.0.11 // indirect\n\tgithub.com/bits-and-blooms/bitset v1.13.0 // indirect\n\tgithub.com/bytedance/sonic v1.9.1 // indirect\n\tgithub.com/caddyserver/certmagic v0.23.0 // indirect\n\tgithub.com/caddyserver/zerossl v0.1.3 // indirect\n\tgithub.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect\n\tgithub.com/coder/websocket v1.8.13 // indirect\n\tgithub.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 // indirect\n\tgithub.com/cretz/bine v0.2.0 // indirect\n\tgithub.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect\n\tgithub.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e // indirect\n\tgithub.com/fxamacker/cbor/v2 v2.7.0 // indirect\n\tgithub.com/gabriel-vasile/mimetype v1.4.2 // indirect\n\tgithub.com/gaissmai/bart v0.11.1 // indirect\n\tgithub.com/gin-contrib/sse v0.1.0 // indirect\n\tgithub.com/go-chi/chi/v5 v5.2.2 // indirect\n\tgithub.com/go-chi/render v1.0.3 // indirect\n\tgithub.com/go-json-experiment/json v0.0.0-20250103232110-6a9a0fde9288 // indirect\n\tgithub.com/go-ole/go-ole v1.3.0 // indirect\n\tgithub.com/go-playground/locales v0.14.1 // indirect\n\tgithub.com/go-playground/universal-translator v0.18.1 // indirect\n\tgithub.com/go-playground/validator/v10 v10.14.0 // indirect\n\tgithub.com/gobwas/httphead v0.1.0 // indirect\n\tgithub.com/gobwas/pool v0.2.1 // indirect\n\tgithub.com/goccy/go-json v0.10.2 // indirect\n\tgithub.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 // indirect\n\tgithub.com/gofrs/uuid/v5 v5.3.2 // indirect\n\tgithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect\n\tgithub.com/google/btree v1.1.3 // indirect\n\tgithub.com/google/go-cmp v0.7.0 // indirect\n\tgithub.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 // indirect\n\tgithub.com/gorilla/csrf v1.7.3-0.20250123201450-9dd6af1f6d30 // indirect\n\tgithub.com/gorilla/securecookie v1.1.2 // indirect\n\tgithub.com/hashicorp/yamux v0.1.2 // indirect\n\tgithub.com/hdevalence/ed25519consensus v0.2.0 // indirect\n\tgithub.com/illarion/gonotify/v2 v2.0.3 // indirect\n\tgithub.com/insomniacslk/dhcp v0.0.0-20250417080101-5f8cf70e8c5f // indirect\n\tgithub.com/jsimonetti/rtnetlink v1.4.0 // indirect\n\tgithub.com/json-iterator/go v1.1.12 // indirect\n\tgithub.com/klauspost/compress v1.17.11 // indirect\n\tgithub.com/klauspost/cpuid/v2 v2.2.10 // indirect\n\tgithub.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a // indirect\n\tgithub.com/kr/pretty v0.3.1 // indirect\n\tgithub.com/leodido/go-urn v1.2.4 // indirect\n\tgithub.com/libdns/alidns v1.0.5-libdns.v1.beta1 // indirect\n\tgithub.com/libdns/cloudflare v0.2.2-0.20250708034226-c574dccb31a6 // indirect\n\tgithub.com/libdns/libdns v1.1.0 // indirect\n\tgithub.com/logrusorgru/aurora v2.0.3+incompatible // indirect\n\tgithub.com/mattn/go-isatty v0.0.19 // indirect\n\tgithub.com/mdlayher/genetlink v1.3.2 // indirect\n\tgithub.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 // indirect\n\tgithub.com/mdlayher/sdnotify v1.0.0 // indirect\n\tgithub.com/mdlayher/socket v0.5.1 // indirect\n\tgithub.com/metacubex/tfo-go v0.0.0-20250921095601-b102db4216c0 // indirect\n\tgithub.com/metacubex/utls v1.8.3 // indirect\n\tgithub.com/mholt/acmez/v3 v3.1.2 // indirect\n\tgithub.com/miekg/dns v1.1.68 // indirect\n\tgithub.com/mitchellh/go-ps v1.0.0 // indirect\n\tgithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect\n\tgithub.com/modern-go/reflect2 v1.0.2 // indirect\n\tgithub.com/pelletier/go-toml/v2 v2.0.8 // indirect\n\tgithub.com/pierrec/lz4/v4 v4.1.21 // indirect\n\tgithub.com/prometheus-community/pro-bing v0.4.0 // indirect\n\tgithub.com/quic-go/qpack v0.5.1 // indirect\n\tgithub.com/safchain/ethtool v0.3.0 // indirect\n\tgithub.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a // indirect\n\tgithub.com/sagernet/cors v1.2.1 // indirect\n\tgithub.com/sagernet/fswatch v0.1.1 // indirect\n\tgithub.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb // indirect\n\tgithub.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect\n\tgithub.com/sagernet/nftables v0.3.0-beta.4 // indirect\n\tgithub.com/sagernet/quic-go v0.52.0-sing-box-mod.3 // indirect\n\tgithub.com/sagernet/sing v0.7.13 // indirect\n\tgithub.com/sagernet/sing-mux v0.3.3 // indirect\n\tgithub.com/sagernet/sing-shadowsocks v0.2.8 // indirect\n\tgithub.com/sagernet/sing-shadowsocks2 v0.2.1 // indirect\n\tgithub.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 // indirect\n\tgithub.com/sagernet/sing-tun v0.7.3 // indirect\n\tgithub.com/sagernet/sing-vmess v0.2.7 // indirect\n\tgithub.com/sagernet/smux v1.5.34-mod.2 // indirect\n\tgithub.com/sagernet/tailscale v1.80.3-sing-box-1.12-mod.2 // indirect\n\tgithub.com/sagernet/wireguard-go v0.0.1-beta.7 // indirect\n\tgithub.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 // indirect\n\tgithub.com/stretchr/testify v1.11.1 // indirect\n\tgithub.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e // indirect\n\tgithub.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect\n\tgithub.com/tailscale/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4 // indirect\n\tgithub.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 // indirect\n\tgithub.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a // indirect\n\tgithub.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 // indirect\n\tgithub.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc // indirect\n\tgithub.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 // indirect\n\tgithub.com/twitchyliquid64/golang-asm v0.15.1 // indirect\n\tgithub.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect\n\tgithub.com/ugorji/go/codec v1.2.11 // indirect\n\tgithub.com/vishvananda/netns v0.0.5 // indirect\n\tgithub.com/x448/float16 v0.8.4 // indirect\n\tgithub.com/ysmood/fetchup v0.2.3 // indirect\n\tgithub.com/ysmood/goob v0.4.0 // indirect\n\tgithub.com/ysmood/got v0.40.0 // indirect\n\tgithub.com/ysmood/gson v0.7.3 // indirect\n\tgithub.com/ysmood/leakless v0.9.0 // indirect\n\tgithub.com/zeebo/blake3 v0.2.4 // indirect\n\tgo.uber.org/multierr v1.11.0 // indirect\n\tgo.uber.org/zap v1.27.0 // indirect\n\tgo.uber.org/zap/exp v0.3.0 // indirect\n\tgo4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect\n\tgo4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect\n\tgolang.org/x/arch v0.3.0 // indirect\n\tgolang.org/x/crypto v0.48.0 // indirect\n\tgolang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect\n\tgolang.org/x/mod v0.33.0 // indirect\n\tgolang.org/x/net v0.50.0 // indirect\n\tgolang.org/x/sync v0.20.0 // indirect\n\tgolang.org/x/sys v0.41.0 // indirect\n\tgolang.org/x/term v0.40.0 // indirect\n\tgolang.org/x/text v0.35.0 // indirect\n\tgolang.org/x/time v0.12.0 // indirect\n\tgolang.org/x/tools v0.42.0 // indirect\n\tgolang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect\n\tgolang.zx2c4.com/wireguard/windows v0.5.3 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect\n\tgoogle.golang.org/grpc v1.79.3 // indirect\n\tgoogle.golang.org/protobuf v1.36.10 // indirect\n\tgopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n\tlukechampine.com/blake3 v1.4.1 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw=\nfilippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=\ngithub.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=\ngithub.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=\ngithub.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A=\ngithub.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw=\ngithub.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=\ngithub.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=\ngithub.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=\ngithub.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=\ngithub.com/anytls/sing-anytls v0.0.11 h1:w8e9Uj1oP3m4zxkyZDewPk0EcQbvVxb7Nn+rapEx4fc=\ngithub.com/anytls/sing-anytls v0.0.11/go.mod h1:7rjN6IukwysmdusYsrV51Fgu1uW6vsrdd6ctjnEAln8=\ngithub.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE=\ngithub.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=\ngithub.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=\ngithub.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=\ngithub.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=\ngithub.com/caddyserver/certmagic v0.23.0 h1:CfpZ/50jMfG4+1J/u2LV6piJq4HOfO6ppOnOf7DkFEU=\ngithub.com/caddyserver/certmagic v0.23.0/go.mod h1:9mEZIWqqWoI+Gf+4Trh04MOVPD0tGSxtqsxg87hAIH4=\ngithub.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA=\ngithub.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=\ngithub.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=\ngithub.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=\ngithub.com/cilium/ebpf v0.15.0 h1:7NxJhNiBT3NG8pZJ3c+yfrVdHY8ScgKD27sScgjLMMk=\ngithub.com/cilium/ebpf v0.15.0/go.mod h1:DHp1WyrLeiBh19Cf/tfiSMhqheEiK8fXFZ4No0P1Hso=\ngithub.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE=\ngithub.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=\ngithub.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0=\ngithub.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/cretz/bine v0.2.0 h1:8GiDRGlTgz+o8H9DSnsl+5MeBK4HsExxgl6WgzOCuZo=\ngithub.com/cretz/bine v0.2.0/go.mod h1:WU4o9QR9wWp8AVKtTM1XD5vUHkEqnf2vVSo6dBqbetI=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa h1:h8TfIT1xc8FWbwwpmHn1J5i43Y0uZP97GqasGCzSRJk=\ngithub.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa/go.mod h1:Nx87SkVqTKd8UtT+xu7sM/l+LgXs6c0aHrlKusR+2EQ=\ngithub.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yezR0y7jJ5pceLHthLaYf4bA5T14B6q39S4q2Q=\ngithub.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A=\ngithub.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=\ngithub.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=\ngithub.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=\ngithub.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=\ngithub.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=\ngithub.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=\ngithub.com/gaissmai/bart v0.11.1 h1:5Uv5XwsaFBRo4E5VBcb9TzY8B7zxFf+U7isDxqOrRfc=\ngithub.com/gaissmai/bart v0.11.1/go.mod h1:KHeYECXQiBjTzQz/om2tqn3sZF1J7hw9m6z41ftj3fg=\ngithub.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=\ngithub.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=\ngithub.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=\ngithub.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=\ngithub.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I=\ngithub.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo=\ngithub.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618=\ngithub.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=\ngithub.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=\ngithub.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=\ngithub.com/go-json-experiment/json v0.0.0-20250103232110-6a9a0fde9288 h1:KbX3Z3CgiYlbaavUq3Cj9/MjpO+88S7/AGXzynVDv84=\ngithub.com/go-json-experiment/json v0.0.0-20250103232110-6a9a0fde9288/go.mod h1:BWmvoE1Xia34f3l/ibJweyhrT+aROb/FQ6d+37F0e2s=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=\ngithub.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=\ngithub.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=\ngithub.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=\ngithub.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=\ngithub.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=\ngithub.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=\ngithub.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=\ngithub.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=\ngithub.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=\ngithub.com/go-rod/rod v0.116.2 h1:A5t2Ky2A+5eD/ZJQr1EfsQSe5rms5Xof/qj296e+ZqA=\ngithub.com/go-rod/rod v0.116.2/go.mod h1:H+CMO9SCNc2TJ2WfrG+pKhITz57uGNYU43qYHh438Mg=\ngithub.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=\ngithub.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=\ngithub.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=\ngithub.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=\ngithub.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=\ngithub.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=\ngithub.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 h1:sQspH8M4niEijh3PFscJRLDnkL547IeP7kpPe3uUhEg=\ngithub.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466/go.mod h1:ZiQxhyQ+bbbfxUKVvjfO498oPYvtYhZzycal3G/NHmU=\ngithub.com/gofrs/uuid/v5 v5.3.2 h1:2jfO8j3XgSwlz/wHqemAEugfnTlikAYHhnqQ8Xh4fE0=\ngithub.com/gofrs/uuid/v5 v5.3.2/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=\ngithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=\ngithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=\ngithub.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=\ngithub.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 h1:wG8RYIyctLhdFk6Vl1yPGtSRtwGpVkWyZww1OCil2MI=\ngithub.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/gorilla/csrf v1.7.3-0.20250123201450-9dd6af1f6d30 h1:fiJdrgVBkjZ5B1HJ2WQwNOaXB+QyYcNXTA3t1XYLz0M=\ngithub.com/gorilla/csrf v1.7.3-0.20250123201450-9dd6af1f6d30/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk=\ngithub.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=\ngithub.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=\ngithub.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=\ngithub.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=\ngithub.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=\ngithub.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=\ngithub.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU=\ngithub.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo=\ngithub.com/illarion/gonotify/v2 v2.0.3 h1:B6+SKPo/0Sw8cRJh1aLzNEeNVFfzE3c6N+o+vyxM+9A=\ngithub.com/illarion/gonotify/v2 v2.0.3/go.mod h1:38oIJTgFqupkEydkkClkbL6i5lXV/bxdH9do5TALPEE=\ngithub.com/insomniacslk/dhcp v0.0.0-20250417080101-5f8cf70e8c5f h1:dd33oobuIv9PcBVqvbEiCXEbNTomOHyj3WFuC5YiPRU=\ngithub.com/insomniacslk/dhcp v0.0.0-20250417080101-5f8cf70e8c5f/go.mod h1:zhFlBeJssZ1YBCMZ5Lzu1pX4vhftDvU10WUVb1uXKtM=\ngithub.com/jsimonetti/rtnetlink v1.4.0 h1:Z1BF0fRgcETPEa0Kt0MRk3yV5+kF1FWTni6KUFKrq2I=\ngithub.com/jsimonetti/rtnetlink v1.4.0/go.mod h1:5W1jDvWdnthFJ7fxYX1GMK07BUpI4oskfOqvPteYS6E=\ngithub.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=\ngithub.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=\ngithub.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=\ngithub.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=\ngithub.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=\ngithub.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=\ngithub.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=\ngithub.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a h1:+RR6SqnTkDLWyICxS1xpjCi/3dhyV+TgZwA6Ww3KncQ=\ngithub.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk=\ngithub.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=\ngithub.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=\ngithub.com/libdns/alidns v1.0.5-libdns.v1.beta1 h1:txHK7UxDed3WFBDjrTZPuMn8X+WmhjBTTAMW5xdy5pQ=\ngithub.com/libdns/alidns v1.0.5-libdns.v1.beta1/go.mod h1:ystHmPwcGoWjPrGpensQSMY9VoCx4cpR2hXNlwk9H/g=\ngithub.com/libdns/cloudflare v0.2.2-0.20250708034226-c574dccb31a6 h1:3MGrVWs2COjMkQR17oUw1zMIPbm2YAzxDC3oGVZvQs8=\ngithub.com/libdns/cloudflare v0.2.2-0.20250708034226-c574dccb31a6/go.mod h1:w9uTmRCDlAoafAsTPnn2nJ0XHK/eaUMh86DUk8BWi60=\ngithub.com/libdns/libdns v1.0.0-beta.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=\ngithub.com/libdns/libdns v1.1.0 h1:9ze/tWvt7Df6sbhOJRB8jT33GHEHpEQXdtkE3hPthbU=\ngithub.com/libdns/libdns v1.1.0/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=\ngithub.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8=\ngithub.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=\ngithub.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=\ngithub.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=\ngithub.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=\ngithub.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 h1:A1Cq6Ysb0GM0tpKMbdCXCIfBclan4oHk1Jb+Hrejirg=\ngithub.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42/go.mod h1:BB4YCPDOzfy7FniQ/lxuYQ3dgmM2cZumHbK8RpTjN2o=\ngithub.com/mdlayher/sdnotify v1.0.0 h1:Ma9XeLVN/l0qpyx1tNeMSeTjCPH6NtuD6/N9XdTlQ3c=\ngithub.com/mdlayher/sdnotify v1.0.0/go.mod h1:HQUmpM4XgYkhDLtd+Uad8ZFK1T9D5+pNxnXQjCeJlGE=\ngithub.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=\ngithub.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=\ngithub.com/metacubex/tfo-go v0.0.0-20250921095601-b102db4216c0 h1:Ui+/2s5Qz0lSnDUBmEL12M5Oi/PzvFxGTNohm8ZcsmE=\ngithub.com/metacubex/tfo-go v0.0.0-20250921095601-b102db4216c0/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw=\ngithub.com/metacubex/utls v1.8.3 h1:0m/yCxm3SK6kWve2lKiFb1pue1wHitJ8sQQD4Ikqde4=\ngithub.com/metacubex/utls v1.8.3/go.mod h1:kncGGVhFaoGn5M3pFe3SXhZCzsbCJayNOH4UEqTKTko=\ngithub.com/mholt/acmez/v3 v3.1.2 h1:auob8J/0FhmdClQicvJvuDavgd5ezwLBfKuYmynhYzc=\ngithub.com/mholt/acmez/v3 v3.1.2/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=\ngithub.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=\ngithub.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=\ngithub.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=\ngithub.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=\ngithub.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=\ngithub.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=\ngithub.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=\ngithub.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=\ngithub.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=\ngithub.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=\ngithub.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=\ngithub.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=\ngithub.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/prometheus-community/pro-bing v0.4.0 h1:YMbv+i08gQz97OZZBwLyvmmQEEzyfyrrjEaAchdy3R4=\ngithub.com/prometheus-community/pro-bing v0.4.0/go.mod h1:b7wRYZtCcPmt4Sz319BykUU241rWLe1VFXyiyWK/dH4=\ngithub.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=\ngithub.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=\ngithub.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=\ngithub.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=\ngithub.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=\ngithub.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0=\ngithub.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs=\ngithub.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a h1:+NkI2670SQpQWvkkD2QgdTuzQG263YZ+2emfpeyGqW0=\ngithub.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a/go.mod h1:63s7jpZqcDAIpj8oI/1v4Izok+npJOHACFCU6+huCkM=\ngithub.com/sagernet/cors v1.2.1 h1:Cv5Z8y9YSD6Gm+qSpNrL3LO4lD3eQVvbFYJSG7JCMHQ=\ngithub.com/sagernet/cors v1.2.1/go.mod h1:O64VyOjjhrkLmQIjF4KGRrJO/5dVXFdpEmCW/eISRAI=\ngithub.com/sagernet/fswatch v0.1.1 h1:YqID+93B7VRfqIH3PArW/XpJv5H4OLEVWDfProGoRQs=\ngithub.com/sagernet/fswatch v0.1.1/go.mod h1:nz85laH0mkQqJfaOrqPpkwtU1znMFNVTpT/5oRsVz/o=\ngithub.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb h1:pprQtDqNgqXkRsXn+0E8ikKOemzmum8bODjSfDene38=\ngithub.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb/go.mod h1:QkkPEJLw59/tfxgapHta14UL5qMUah5NXhO0Kw2Kan4=\ngithub.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis=\ngithub.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=\ngithub.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I=\ngithub.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8=\ngithub.com/sagernet/quic-go v0.52.0-sing-box-mod.3 h1:ySqffGm82rPqI1TUPqmtHIYd12pfEGScygnOxjTL56w=\ngithub.com/sagernet/quic-go v0.52.0-sing-box-mod.3/go.mod h1:OV+V5kEBb8kJS7k29MzDu6oj9GyMc7HA07sE1tedxz4=\ngithub.com/sagernet/sing v0.6.9/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=\ngithub.com/sagernet/sing v0.7.13 h1:XNYgd8e3cxMULs/LLJspdn/deHrnPWyrrglNHeCUAYM=\ngithub.com/sagernet/sing v0.7.13/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=\ngithub.com/sagernet/sing-box v1.12.12 h1:brSb4zdL5CfqXB0ss1jrT+srPUbKanyWhbswkte/z5Y=\ngithub.com/sagernet/sing-box v1.12.12/go.mod h1:ObMeEc1VAcJdXN6B/3SUIJRuK7J38m0W6yLNJ+E5f+0=\ngithub.com/sagernet/sing-mux v0.3.3 h1:YFgt9plMWzH994BMZLmyKL37PdIVaIilwP0Jg+EcLfw=\ngithub.com/sagernet/sing-mux v0.3.3/go.mod h1:pht8iFY4c9Xltj7rhVd208npkNaeCxzyXCgulDPLUDA=\ngithub.com/sagernet/sing-quic v0.5.2-0.20250909083218-00a55617c0fb h1:5Wx3XeTiKrrrcrAky7Hc1bO3CGxrvho2Vu5b/adlEIM=\ngithub.com/sagernet/sing-quic v0.5.2-0.20250909083218-00a55617c0fb/go.mod h1:evP1e++ZG8TJHVV5HudXV4vWeYzGfCdF4HwSJZcdqkI=\ngithub.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE=\ngithub.com/sagernet/sing-shadowsocks v0.2.8/go.mod h1:lo7TWEMDcN5/h5B8S0ew+r78ZODn6SwVaFhvB6H+PTI=\ngithub.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnqqs2gQ2/Qioo=\ngithub.com/sagernet/sing-shadowsocks2 v0.2.1/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ=\ngithub.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 h1:tK+75l64tm9WvEFrYRE1t0YxoFdWQqw/h7Uhzj0vJ+w=\ngithub.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA=\ngithub.com/sagernet/sing-tun v0.7.3 h1:MFnAir+l24ElEyxdfwtY8mqvUUL9nPnL9TDYLkOmVes=\ngithub.com/sagernet/sing-tun v0.7.3/go.mod h1:pUEjh9YHQ2gJT6Lk0TYDklh3WJy7lz+848vleGM3JPM=\ngithub.com/sagernet/sing-vmess v0.2.7 h1:2ee+9kO0xW5P4mfe6TYVWf9VtY8k1JhNysBqsiYj0sk=\ngithub.com/sagernet/sing-vmess v0.2.7/go.mod h1:5aYoOtYksAyS0NXDm0qKeTYW1yoE1bJVcv+XLcVoyJs=\ngithub.com/sagernet/smux v1.5.34-mod.2 h1:gkmBjIjlJ2zQKpLigOkFur5kBKdV6bNRoFu2WkltRQ4=\ngithub.com/sagernet/smux v1.5.34-mod.2/go.mod h1:0KW0+R+ycvA2INW4gbsd7BNyg+HEfLIAxa5N02/28Zc=\ngithub.com/sagernet/tailscale v1.80.3-sing-box-1.12-mod.2 h1:MO7s4ni2bSfAOhcan2rdQSWCztkMXmqyg6jYPZp8bEE=\ngithub.com/sagernet/tailscale v1.80.3-sing-box-1.12-mod.2/go.mod h1:EBxXsWu4OH2ELbQLq32WoBeIubG8KgDrg4/Oaxjs6lI=\ngithub.com/sagernet/wireguard-go v0.0.1-beta.7 h1:ltgBwYHfr+9Wz1eG59NiWnHrYEkDKHG7otNZvu85DXI=\ngithub.com/sagernet/wireguard-go v0.0.1-beta.7/go.mod h1:jGXij2Gn2wbrWuYNUmmNhf1dwcZtvyAvQoe8Xd8MbUo=\ngithub.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc=\ngithub.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854/go.mod h1:LtfoSK3+NG57tvnVEHgcuBW9ujgE8enPSgzgwStwCAA=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=\ngithub.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=\ngithub.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e h1:PtWT87weP5LWHEY//SWsYkSO3RWRZo4OSWagh3YD2vQ=\ngithub.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e/go.mod h1:XrBNfAFN+pwoWuksbFS9Ccxnopa15zJGgXRFN90l3K4=\ngithub.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4=\ngithub.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg=\ngithub.com/tailscale/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4 h1:rXZGgEa+k2vJM8xT0PoSKfVXwFGPQ3z3CJfmnHJkZZw=\ngithub.com/tailscale/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4/go.mod h1:ikbF+YT089eInTp9f2vmvy4+ZVnW5hzX1q2WknxSprQ=\ngithub.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPxRs2O36yuGRW3f9SYV+bMTTvMBI0EKio=\ngithub.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8=\ngithub.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29XwJucQo73FrleVK6t4kYz4NVhp34Yw=\ngithub.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8=\ngithub.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 h1:uFsXVBE9Qr4ZoF094vE6iYTLDl0qCiKzYXlL6UeWObU=\ngithub.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0=\ngithub.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc h1:24heQPtnFR+yfntqhI3oAu9i27nEojcQ4NuBQOo5ZFA=\ngithub.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc/go.mod h1:f93CXfllFsO9ZQVq+Zocb1Gp4G5Fz0b0rXHLOzt/Djc=\ngithub.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 h1:UBPHPtv8+nEAy2PD8RyAhOYvau1ek0HDJqLS/Pysi14=\ngithub.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ=\ngithub.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA=\ngithub.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk=\ngithub.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=\ngithub.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=\ngithub.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM=\ngithub.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA=\ngithub.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=\ngithub.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=\ngithub.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=\ngithub.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=\ngithub.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=\ngithub.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=\ngithub.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=\ngithub.com/ysmood/fetchup v0.2.3 h1:ulX+SonA0Vma5zUFXtv52Kzip/xe7aj4vqT5AJwQ+ZQ=\ngithub.com/ysmood/fetchup v0.2.3/go.mod h1:xhibcRKziSvol0H1/pj33dnKrYyI2ebIvz5cOOkYGns=\ngithub.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ=\ngithub.com/ysmood/goob v0.4.0/go.mod h1:u6yx7ZhS4Exf2MwciFr6nIM8knHQIE22lFpWHnfql18=\ngithub.com/ysmood/gop v0.2.0 h1:+tFrG0TWPxT6p9ZaZs+VY+opCvHU8/3Fk6BaNv6kqKg=\ngithub.com/ysmood/gop v0.2.0/go.mod h1:rr5z2z27oGEbyB787hpEcx4ab8cCiPnKxn0SUHt6xzk=\ngithub.com/ysmood/got v0.40.0 h1:ZQk1B55zIvS7zflRrkGfPDrPG3d7+JOza1ZkNxcc74Q=\ngithub.com/ysmood/got v0.40.0/go.mod h1:W7DdpuX6skL3NszLmAsC5hT7JAhuLZhByVzHTq874Qg=\ngithub.com/ysmood/gotrace v0.6.0 h1:SyI1d4jclswLhg7SWTL6os3L1WOKeNn/ZtzVQF8QmdY=\ngithub.com/ysmood/gotrace v0.6.0/go.mod h1:TzhIG7nHDry5//eYZDYcTzuJLYQIkykJzCRIo4/dzQM=\ngithub.com/ysmood/gson v0.7.3 h1:QFkWbTH8MxyUTKPkVWAENJhxqdBa4lYTQWqZCiLG6kE=\ngithub.com/ysmood/gson v0.7.3/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg=\ngithub.com/ysmood/leakless v0.9.0 h1:qxCG5VirSBvmi3uynXFkcnLMzkphdh3xx5FtrORwDCU=\ngithub.com/ysmood/leakless v0.9.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ=\ngithub.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=\ngithub.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=\ngithub.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=\ngithub.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE=\ngithub.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=\ngithub.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=\ngo.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=\ngo.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=\ngo.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=\ngo.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=\ngo.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=\ngo.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=\ngo.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=\ngo.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=\ngo.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=\ngo.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=\ngo.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=\ngo.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=\ngo.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=\ngo.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=\ngo.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=\ngo.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=\ngo.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=\ngo.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=\ngo.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=\ngo.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=\ngo4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4hOxG5YpKCzkek=\ngo4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=\ngo4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=\ngo4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=\ngolang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=\ngolang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=\ngolang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=\ngolang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=\ngolang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=\ngolang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=\ngolang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=\ngolang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=\ngolang.org/x/image v0.38.0 h1:5l+q+Y9JDC7mBOMjo4/aPhMDcxEptsX+Tt3GgRQRPuE=\ngolang.org/x/image v0.38.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY=\ngolang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=\ngolang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=\ngolang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=\ngolang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=\ngolang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=\ngolang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=\ngolang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=\ngolang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=\ngolang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=\ngolang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=\ngolang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=\ngolang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=\ngolang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=\ngolang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=\ngolang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE=\ngolang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI=\ngonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=\ngonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=\ngoogle.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=\ngoogle.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=\ngoogle.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=\ngoogle.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\nlukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg=\nlukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo=\nrsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=\nsoftware.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k=\nsoftware.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=\n"
  },
  {
    "path": "main.go",
    "content": "package main\n\nimport (\n\t\"bytes\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"image\"\n\t_ \"image/gif\"\n\t_ \"image/jpeg\"\n\t\"image/png\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t_ \"golang.org/x/image/bmp\"\n\t_ \"golang.org/x/image/tiff\"\n\t_ \"golang.org/x/image/webp\"\n\n\t\"github.com/fsnotify/fsnotify\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/google/uuid\"\n\n\t\"business2api/src/flow\"\n\t\"business2api/src/logger\"\n\t\"business2api/src/pool\"\n\t\"business2api/src/proxy\"\n\t\"business2api/src/register\"\n\t\"business2api/src/utils\"\n)\n\n// ==================== 配置结构 ====================\ntype PoolConfig struct {\n\tTargetCount            int  `json:\"target_count\"`              // 目标账号数量\n\tMinCount               int  `json:\"min_count\"`                 // 最小账号数，低于此值触发注册\n\tCheckIntervalMinutes   int  `json:\"check_interval_minutes\"`    // 检查间隔(分钟)\n\tRegisterThreads        int  `json:\"register_threads\"`          // 注册线程数\n\tRegisterHeadless       bool `json:\"register_headless\"`         // 无头模式\n\tRefreshOnStartup       bool `json:\"refresh_on_startup\"`        // 启动时刷新账号\n\tRefreshCooldownSec     int  `json:\"refresh_cooldown_sec\"`      // 刷新冷却时间(秒)\n\tUseCooldownSec         int  `json:\"use_cooldown_sec\"`          // 使用冷却时间(秒)\n\tMaxFailCount           int  `json:\"max_fail_count\"`            // 最大连续失败次数\n\tEnableBrowserRefresh   bool `json:\"enable_browser_refresh\"`    // 启用浏览器刷新401账号\n\tBrowserRefreshHeadless bool `json:\"browser_refresh_headless\"`  // 浏览器刷新无头模式\n\tBrowserRefreshMaxRetry int  `json:\"browser_refresh_max_retry\"` // 浏览器刷新最大重试次数(0=禁用)\n\tAutoDelete401          bool `json:\"auto_delete_401\"`           // 401时自动删除账号\n}\n\n// FlowConfig Flow 服务配置\ntype FlowConfigSection struct {\n\tEnable          bool     `json:\"enable\"`            // 是否启用 Flow\n\tTokens          []string `json:\"tokens\"`            // Flow ST Tokens\n\tProxy           string   `json:\"proxy\"`             // Flow 专用代理\n\tTimeout         int      `json:\"timeout\"`           // 超时时间\n\tPollInterval    int      `json:\"poll_interval\"`     // 轮询间隔\n\tMaxPollAttempts int      `json:\"max_poll_attempts\"` // 最大轮询次数\n}\n\n// ProxyConfig 代理配置\ntype ProxyConfig struct {\n\tProxy          string   `json:\"proxy\"`            // 单个代理 (http/socks5)\n\tSubscribes     []string `json:\"subscribes\"`       // 订阅链接列表\n\tFiles          []string `json:\"files\"`            // 代理文件列表\n\tHealthCheck    bool     `json:\"health_check\"`     // 是否启用健康检查\n\tCheckOnStartup bool     `json:\"check_on_startup\"` // 启动时检查\n}\n\ntype AppConfig struct {\n\tAPIKeys        []string              `json:\"api_keys\"`        // API 密钥列表\n\tListenAddr     string                `json:\"listen_addr\"`     // 监听地址\n\tDataDir        string                `json:\"data_dir\"`        // 数据目录\n\tPool           PoolConfig            `json:\"pool\"`            // 号池配置\n\tProxy          string                `json:\"proxy\"`           // 代理 (兼容旧配置)\n\tProxySubscribe string                `json:\"proxy_subscribe\"` // 代理订阅链接 (兼容旧配置)\n\tProxyPool      ProxyConfig           `json:\"proxy_pool\"`      // 代理池配置\n\tDefaultConfig  string                `json:\"default_config\"`  // 默认 configId\n\tPoolServer     pool.PoolServerConfig `json:\"pool_server\"`     // 号池服务器配置\n\tDebug          bool                  `json:\"debug\"`           // 调试模式\n\tFlow           FlowConfigSection     `json:\"flow\"`            // Flow 配置\n\tNote           []string              `json:\"note\"`            // 备注信息（支持多行）\n}\n\n// PoolMode 号池模式\ntype PoolMode int\n\nconst (\n\tPoolModeLocal  PoolMode = iota // 本地模式\n\tPoolModeServer                 // 服务器模式（提供号池服务）\n\tPoolModeClient                 // 客户端模式（使用远程号池）\n)\n\nvar (\n\tpoolMode         PoolMode\n\tremotePoolClient *pool.RemotePoolClient\n\tflowClient       *flow.FlowClient\n\tflowHandler      *flow.GenerationHandler\n\tflowTokenPool    *flow.TokenPool\n)\n\n// 配置热重载相关\nvar (\n\tconfigMu      sync.RWMutex           // 配置读写锁\n\tconfigWatcher *fsnotify.Watcher      // 配置文件监听器\n\tconfigPath    = \"config/config.json\" // 配置文件路径\n)\n\n// APIStats API 调用统计\ntype APIStats struct {\n\tmu              sync.RWMutex\n\tstartTime       time.Time              // 服务启动时间\n\ttotalRequests   int64                  // 总请求数\n\tsuccessRequests int64                  // 成功请求数\n\tfailedRequests  int64                  // 失败请求数\n\tinputTokens     int64                  // 输入 tokens\n\toutputTokens    int64                  // 输出 tokens\n\timageGenerated  int64                  // 生成的图片数\n\tvideoGenerated  int64                  // 生成的视频数\n\trequestTimes    []time.Time            // 最近请求时间（用于计算 RPM）\n\tmodelStats      map[string]*ModelStats // 每个模型的统计\n\thourlyStats     [24]HourlyStats        // 24小时统计\n\tlastHour        int                    // 上次记录的小时\n}\n\n// ModelStats 模型统计\ntype ModelStats struct {\n\tRequests     int64 `json:\"requests\"`\n\tSuccess      int64 `json:\"success\"`\n\tInputTokens  int64 `json:\"input_tokens\"`\n\tOutputTokens int64 `json:\"output_tokens\"`\n\tImages       int64 `json:\"images\"`\n}\n\n// HourlyStats 小时统计\ntype HourlyStats struct {\n\tHour         int   `json:\"hour\"`\n\tRequests     int64 `json:\"requests\"`\n\tSuccess      int64 `json:\"success\"`\n\tInputTokens  int64 `json:\"input_tokens\"`\n\tOutputTokens int64 `json:\"output_tokens\"`\n}\n\nvar apiStats = &APIStats{\n\tstartTime:    time.Now(),\n\trequestTimes: make([]time.Time, 0, 1000),\n\tmodelStats:   make(map[string]*ModelStats),\n\tlastHour:     time.Now().Hour(),\n}\n\n// IPStats IP请求统计\ntype IPStats struct {\n\tmu         sync.RWMutex\n\tipRequests map[string]*IPRequestInfo\n}\n\n// IPRequestInfo 单个IP的请求信息\ntype IPRequestInfo struct {\n\tIP           string           `json:\"ip\"`\n\tTotalCount   int64            `json:\"total_count\"`\n\tSuccessCount int64            `json:\"success_count\"`\n\tFailedCount  int64            `json:\"failed_count\"`\n\tInputTokens  int64            `json:\"input_tokens\"`\n\tOutputTokens int64            `json:\"output_tokens\"`\n\tImagesCount  int64            `json:\"images_count\"`\n\tVideosCount  int64            `json:\"videos_count\"`\n\tFirstSeen    time.Time        `json:\"first_seen\"`\n\tLastSeen     time.Time        `json:\"last_seen\"`\n\tRequestTimes []time.Time      `json:\"-\"` // 用于计算RPM\n\tModels       map[string]int64 `json:\"models\"`\n\tUserAgents   map[string]int64 `json:\"user_agents,omitempty\"`\n}\n\nvar ipStats = &IPStats{\n\tipRequests: make(map[string]*IPRequestInfo),\n}\n\n// RecordIPRequest 记录IP请求（包含tokens、图片、视频统计）\nfunc (s *IPStats) RecordIPRequest(ip, model, userAgent string, success bool, inputTokens, outputTokens, images, videos int64) {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\n\tnow := time.Now()\n\tinfo, exists := s.ipRequests[ip]\n\tif !exists {\n\t\tinfo = &IPRequestInfo{\n\t\t\tIP:           ip,\n\t\t\tFirstSeen:    now,\n\t\t\tModels:       make(map[string]int64),\n\t\t\tUserAgents:   make(map[string]int64),\n\t\t\tRequestTimes: make([]time.Time, 0, 100),\n\t\t}\n\t\ts.ipRequests[ip] = info\n\t}\n\n\tinfo.TotalCount++\n\tinfo.LastSeen = now\n\tinfo.InputTokens += inputTokens\n\tinfo.OutputTokens += outputTokens\n\tinfo.ImagesCount += images\n\tinfo.VideosCount += videos\n\n\t// 记录请求时间用于计算RPM（保留最近100条）\n\tinfo.RequestTimes = append(info.RequestTimes, now)\n\tif len(info.RequestTimes) > 100 {\n\t\tinfo.RequestTimes = info.RequestTimes[len(info.RequestTimes)-100:]\n\t}\n\n\tif success {\n\t\tinfo.SuccessCount++\n\t} else {\n\t\tinfo.FailedCount++\n\t}\n\tif model != \"\" {\n\t\tinfo.Models[model]++\n\t}\n\tif userAgent != \"\" && len(info.UserAgents) < 50 {\n\t\tinfo.UserAgents[userAgent]++\n\t}\n}\n\n// GetIPRPM 计算单个IP的RPM\nfunc (info *IPRequestInfo) GetRPM() float64 {\n\toneMinuteAgo := time.Now().Add(-time.Minute)\n\tcount := 0\n\tfor i := len(info.RequestTimes) - 1; i >= 0; i-- {\n\t\tif info.RequestTimes[i].After(oneMinuteAgo) {\n\t\t\tcount++\n\t\t} else {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn float64(count)\n}\n\nfunc (s *IPStats) GetAllIPStats() map[string]interface{} {\n\ts.mu.RLock()\n\tdefer s.mu.RUnlock()\n\ttype ipSortInfo struct {\n\t\tIP    string\n\t\tCount int64\n\t}\n\tsorted := make([]ipSortInfo, 0, len(s.ipRequests))\n\tfor ip, info := range s.ipRequests {\n\t\tsorted = append(sorted, ipSortInfo{IP: ip, Count: info.TotalCount})\n\t}\n\tn := len(sorted)\n\tfor i := 1; i < n; i++ {\n\t\tfor j := i; j > 0 && sorted[j].Count > sorted[j-1].Count; j-- {\n\t\t\tsorted[j], sorted[j-1] = sorted[j-1], sorted[j]\n\t\t}\n\t}\n\tvar totalRequests, totalSuccess, totalFailed int64\n\tvar totalInputTokens, totalOutputTokens int64\n\tvar totalImages, totalVideos int64\n\tips := make([]map[string]interface{}, 0, n)\n\tfor i := 0; i < n; i++ {\n\t\tinfo := s.ipRequests[sorted[i].IP]\n\t\trpm := info.GetRPM()\n\t\ttotalRequests += info.TotalCount\n\t\ttotalSuccess += info.SuccessCount\n\t\ttotalFailed += info.FailedCount\n\t\ttotalInputTokens += info.InputTokens\n\t\ttotalOutputTokens += info.OutputTokens\n\t\ttotalImages += info.ImagesCount\n\t\ttotalVideos += info.VideosCount\n\n\t\tips = append(ips, map[string]interface{}{\n\t\t\t\"ip\":            info.IP,\n\t\t\t\"total_count\":   info.TotalCount,\n\t\t\t\"success_count\": info.SuccessCount,\n\t\t\t\"failed_count\":  info.FailedCount,\n\t\t\t\"success_rate\":  fmt.Sprintf(\"%.1f%%\", float64(info.SuccessCount)/float64(max(info.TotalCount, 1))*100),\n\t\t\t\"input_tokens\":  info.InputTokens,\n\t\t\t\"output_tokens\": info.OutputTokens,\n\t\t\t\"total_tokens\":  info.InputTokens + info.OutputTokens,\n\t\t\t\"images\":        info.ImagesCount,\n\t\t\t\"videos\":        info.VideosCount,\n\t\t\t\"rpm\":           rpm,\n\t\t\t\"first_seen\":    info.FirstSeen.Format(time.RFC3339),\n\t\t\t\"last_seen\":     info.LastSeen.Format(time.RFC3339),\n\t\t\t\"models\":        info.Models,\n\t\t\t\"user_agents\":   info.UserAgents,\n\t\t})\n\t}\n\n\treturn map[string]interface{}{\n\t\t\"server_time\":         time.Now().Format(time.RFC3339),\n\t\t\"unique_ips\":          n,\n\t\t\"total_requests\":      totalRequests,\n\t\t\"total_success\":       totalSuccess,\n\t\t\"total_failed\":        totalFailed,\n\t\t\"total_input_tokens\":  totalInputTokens,\n\t\t\"total_output_tokens\": totalOutputTokens,\n\t\t\"total_tokens\":        totalInputTokens + totalOutputTokens,\n\t\t\"total_images\":        totalImages,\n\t\t\"total_videos\":        totalVideos,\n\t\t\"ips\":                 ips,\n\t}\n}\n\n// GetIPDetail 获取单个IP的详细信息\nfunc (s *IPStats) GetIPDetail(ip string) *IPRequestInfo {\n\ts.mu.RLock()\n\tdefer s.mu.RUnlock()\n\treturn s.ipRequests[ip]\n}\n\n// RecordRequest 记录请求\nfunc (s *APIStats) RecordRequest(success bool, inputTokens, outputTokens, images, videos int64) {\n\ts.RecordRequestWithModel(\"\", success, inputTokens, outputTokens, images, videos)\n}\n\nfunc (s *APIStats) RecordRequestWithModel(model string, success bool, inputTokens, outputTokens, images, videos int64) {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\n\ts.totalRequests++\n\tif success {\n\t\ts.successRequests++\n\t} else {\n\t\ts.failedRequests++\n\t}\n\ts.inputTokens += inputTokens\n\ts.outputTokens += outputTokens\n\ts.imageGenerated += images\n\ts.videoGenerated += videos\n\n\t// 记录请求时间（保留最近1000条）\n\tnow := time.Now()\n\ts.requestTimes = append(s.requestTimes, now)\n\tif len(s.requestTimes) > 1000 {\n\t\ts.requestTimes = s.requestTimes[len(s.requestTimes)-1000:]\n\t}\n\n\t// 模型统计\n\tif model != \"\" {\n\t\tif s.modelStats[model] == nil {\n\t\t\ts.modelStats[model] = &ModelStats{}\n\t\t}\n\t\tms := s.modelStats[model]\n\t\tms.Requests++\n\t\tif success {\n\t\t\tms.Success++\n\t\t}\n\t\tms.InputTokens += inputTokens\n\t\tms.OutputTokens += outputTokens\n\t\tms.Images += images\n\t}\n\n\t// 小时统计\n\tcurrentHour := now.Hour()\n\tif currentHour != s.lastHour {\n\t\t// 新的小时，重置该小时统计\n\t\ts.hourlyStats[currentHour] = HourlyStats{Hour: currentHour}\n\t\ts.lastHour = currentHour\n\t}\n\ths := &s.hourlyStats[currentHour]\n\ths.Requests++\n\tif success {\n\t\ths.Success++\n\t}\n\ths.InputTokens += inputTokens\n\ths.OutputTokens += outputTokens\n}\n\nfunc (s *APIStats) GetRPM() float64 {\n\ts.mu.RLock()\n\tdefer s.mu.RUnlock()\n\n\toneMinuteAgo := time.Now().Add(-time.Minute)\n\tcount := 0\n\tfor i := len(s.requestTimes) - 1; i >= 0; i-- {\n\t\tif s.requestTimes[i].After(oneMinuteAgo) {\n\t\t\tcount++\n\t\t} else {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn float64(count)\n}\n\n// GetStats 获取统计数据\nfunc (s *APIStats) GetStats() map[string]interface{} {\n\ts.mu.RLock()\n\tdefer s.mu.RUnlock()\n\n\tuptime := time.Since(s.startTime)\n\tavgRPM := float64(0)\n\tif uptime.Minutes() > 0 {\n\t\tavgRPM = float64(s.totalRequests) / uptime.Minutes()\n\t}\n\n\treturn map[string]interface{}{\n\t\t\"uptime\":           uptime.String(),\n\t\t\"uptime_seconds\":   int64(uptime.Seconds()),\n\t\t\"total_requests\":   s.totalRequests,\n\t\t\"success_requests\": s.successRequests,\n\t\t\"failed_requests\":  s.failedRequests,\n\t\t\"success_rate\":     fmt.Sprintf(\"%.2f%%\", float64(s.successRequests)/float64(max(s.totalRequests, 1))*100),\n\t\t\"input_tokens\":     s.inputTokens,\n\t\t\"output_tokens\":    s.outputTokens,\n\t\t\"total_tokens\":     s.inputTokens + s.outputTokens,\n\t\t\"images_generated\": s.imageGenerated,\n\t\t\"videos_generated\": s.videoGenerated,\n\t\t\"current_rpm\":      s.GetRPM(),\n\t\t\"average_rpm\":      fmt.Sprintf(\"%.2f\", avgRPM),\n\t}\n}\n\n// GetDetailedStats 获取详细统计数据\nfunc (s *APIStats) GetDetailedStats() map[string]interface{} {\n\ts.mu.RLock()\n\tdefer s.mu.RUnlock()\n\n\tuptime := time.Since(s.startTime)\n\tavgRPM := float64(0)\n\tif uptime.Minutes() > 0 {\n\t\tavgRPM = float64(s.totalRequests) / uptime.Minutes()\n\t}\n\n\t// 转换模型统计\n\tmodelStatsMap := make(map[string]interface{})\n\tfor model, ms := range s.modelStats {\n\t\tmodelStatsMap[model] = map[string]interface{}{\n\t\t\t\"requests\":      ms.Requests,\n\t\t\t\"success\":       ms.Success,\n\t\t\t\"success_rate\":  fmt.Sprintf(\"%.2f%%\", float64(ms.Success)/float64(max(ms.Requests, 1))*100),\n\t\t\t\"input_tokens\":  ms.InputTokens,\n\t\t\t\"output_tokens\": ms.OutputTokens,\n\t\t\t\"total_tokens\":  ms.InputTokens + ms.OutputTokens,\n\t\t\t\"images\":        ms.Images,\n\t\t}\n\t}\n\n\t// 转换小时统计\n\thourlyStatsArr := make([]map[string]interface{}, 0, 24)\n\tfor i := 0; i < 24; i++ {\n\t\ths := s.hourlyStats[i]\n\t\tif hs.Requests > 0 {\n\t\t\thourlyStatsArr = append(hourlyStatsArr, map[string]interface{}{\n\t\t\t\t\"hour\":          i,\n\t\t\t\t\"requests\":      hs.Requests,\n\t\t\t\t\"success\":       hs.Success,\n\t\t\t\t\"input_tokens\":  hs.InputTokens,\n\t\t\t\t\"output_tokens\": hs.OutputTokens,\n\t\t\t})\n\t\t}\n\t}\n\n\treturn map[string]interface{}{\n\t\t\"uptime\":           uptime.String(),\n\t\t\"uptime_seconds\":   int64(uptime.Seconds()),\n\t\t\"total_requests\":   s.totalRequests,\n\t\t\"success_requests\": s.successRequests,\n\t\t\"failed_requests\":  s.failedRequests,\n\t\t\"success_rate\":     fmt.Sprintf(\"%.2f%%\", float64(s.successRequests)/float64(max(s.totalRequests, 1))*100),\n\t\t\"input_tokens\":     s.inputTokens,\n\t\t\"output_tokens\":    s.outputTokens,\n\t\t\"total_tokens\":     s.inputTokens + s.outputTokens,\n\t\t\"images_generated\": s.imageGenerated,\n\t\t\"videos_generated\": s.videoGenerated,\n\t\t\"current_rpm\":      s.GetRPM(),\n\t\t\"average_rpm\":      fmt.Sprintf(\"%.2f\", avgRPM),\n\t\t\"models\":           modelStatsMap,\n\t\t\"hourly\":           hourlyStatsArr,\n\t}\n}\n\nfunc max(a, b int64) int64 {\n\tif a > b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nvar appConfig = AppConfig{\n\tListenAddr: \":8000\",\n\tDataDir:    \"./data\",\n\tPool: PoolConfig{\n\t\tTargetCount:            50,\n\t\tMinCount:               10,\n\t\tCheckIntervalMinutes:   30,\n\t\tRegisterThreads:        1,\n\t\tRegisterHeadless:       false,\n\t\tRefreshOnStartup:       true,\n\t\tRefreshCooldownSec:     240, // 4分钟\n\t\tUseCooldownSec:         15,  // 15秒\n\t\tMaxFailCount:           3,\n\t\tEnableBrowserRefresh:   true, // 默认启用浏览器刷新\n\t\tBrowserRefreshHeadless: false,\n\t\tBrowserRefreshMaxRetry: 1, // 浏览器刷新最多重试1次\n\t},\n}\n\n// GetAPIKeys 线程安全获取 API Keys\nfunc GetAPIKeys() []string {\n\tconfigMu.RLock()\n\tdefer configMu.RUnlock()\n\tkeys := make([]string, len(appConfig.APIKeys))\n\tcopy(keys, appConfig.APIKeys)\n\treturn keys\n}\n\n// reloadConfig 重新加载配置文件（热重载）\nfunc reloadConfig() error {\n\tdata, err := os.ReadFile(configPath)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"读取配置文件失败: %w\", err)\n\t}\n\n\tvar newConfig AppConfig\n\tif err := json.Unmarshal(data, &newConfig); err != nil {\n\t\treturn fmt.Errorf(\"解析配置文件失败: %w\", err)\n\t}\n\n\tconfigMu.Lock()\n\toldAPIKeys := appConfig.APIKeys\n\toldDebug := appConfig.Debug\n\toldPoolConfig := appConfig.Pool\n\n\t// 更新可热重载的配置项\n\tappConfig.APIKeys = newConfig.APIKeys\n\tappConfig.Debug = newConfig.Debug\n\tappConfig.Note = newConfig.Note\n\n\t// 更新号池配置\n\tappConfig.Pool.RefreshCooldownSec = newConfig.Pool.RefreshCooldownSec\n\tappConfig.Pool.UseCooldownSec = newConfig.Pool.UseCooldownSec\n\tappConfig.Pool.MaxFailCount = newConfig.Pool.MaxFailCount\n\tappConfig.Pool.EnableBrowserRefresh = newConfig.Pool.EnableBrowserRefresh\n\tappConfig.Pool.BrowserRefreshHeadless = newConfig.Pool.BrowserRefreshHeadless\n\tappConfig.Pool.BrowserRefreshMaxRetry = newConfig.Pool.BrowserRefreshMaxRetry\n\tappConfig.Pool.AutoDelete401 = newConfig.Pool.AutoDelete401\n\tconfigMu.Unlock()\n\n\t// 应用变更\n\tapplyConfigChanges(oldAPIKeys, oldDebug, oldPoolConfig, newConfig)\n\n\treturn nil\n}\n\n// applyConfigChanges 应用配置变更\nfunc applyConfigChanges(oldAPIKeys []string, oldDebug bool, oldPoolConfig PoolConfig, newConfig AppConfig) {\n\t// 日志模式变更\n\tif oldDebug != newConfig.Debug {\n\t\tlogger.SetDebugMode(newConfig.Debug)\n\t\tlogger.Info(\"🔄 调试模式: %v -> %v\", oldDebug, newConfig.Debug)\n\t}\n\n\t// API Keys 变更\n\tif len(oldAPIKeys) != len(newConfig.APIKeys) {\n\t\tlogger.Info(\"🔄 API Keys 数量: %d -> %d\", len(oldAPIKeys), len(newConfig.APIKeys))\n\t}\n\n\t// 号池配置变更\n\tif oldPoolConfig.RefreshCooldownSec != newConfig.Pool.RefreshCooldownSec ||\n\t\toldPoolConfig.UseCooldownSec != newConfig.Pool.UseCooldownSec {\n\t\tpool.SetCooldowns(newConfig.Pool.RefreshCooldownSec, newConfig.Pool.UseCooldownSec)\n\t\tlogger.Info(\"🔄 冷却配置已更新: refresh=%ds, use=%ds\",\n\t\t\tnewConfig.Pool.RefreshCooldownSec, newConfig.Pool.UseCooldownSec)\n\t}\n\n\tif newConfig.Pool.MaxFailCount > 0 {\n\t\tpool.MaxFailCount = newConfig.Pool.MaxFailCount\n\t}\n\n\tpool.EnableBrowserRefresh = newConfig.Pool.EnableBrowserRefresh\n\tpool.BrowserRefreshHeadless = newConfig.Pool.BrowserRefreshHeadless\n\tif newConfig.Pool.BrowserRefreshMaxRetry >= 0 {\n\t\tpool.BrowserRefreshMaxRetry = newConfig.Pool.BrowserRefreshMaxRetry\n\t}\n\tpool.AutoDelete401 = newConfig.Pool.AutoDelete401\n\n\tlogger.Info(\"✅ 配置热重载完成\")\n}\n\n// startConfigWatcher 启动配置文件监听\nfunc startConfigWatcher() error {\n\twatcher, err := fsnotify.NewWatcher()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"创建配置监听器失败: %w\", err)\n\t}\n\tconfigWatcher = watcher\n\n\tgo configWatchLoop()\n\n\t// 监听配置目录\n\tconfigDir := filepath.Dir(configPath)\n\tif err := watcher.Add(configDir); err != nil {\n\t\treturn fmt.Errorf(\"添加配置目录监听失败: %w\", err)\n\t}\n\n\tlogger.Info(\"🔄 配置文件热重载已启用: %s\", configPath)\n\treturn nil\n}\n\n// configWatchLoop 配置文件监听循环\nfunc configWatchLoop() {\n\tvar lastReload time.Time\n\tconst debounceDelay = 500 * time.Millisecond\n\n\tfor {\n\t\tselect {\n\t\tcase event, ok := <-configWatcher.Events:\n\t\t\tif !ok {\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// 只关注配置文件\n\t\t\tif filepath.Base(event.Name) != \"config.json\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// 只处理写入和创建事件\n\t\t\tif event.Op&(fsnotify.Write|fsnotify.Create) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// 防抖：避免短时间内多次触发\n\t\t\tif time.Since(lastReload) < debounceDelay {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tlastReload = time.Now()\n\n\t\t\t// 等待文件写入完成\n\t\t\ttime.Sleep(100 * time.Millisecond)\n\n\t\t\tlogger.Info(\"📝 检测到配置文件变更，正在重载...\")\n\t\t\tif err := reloadConfig(); err != nil {\n\t\t\t\tlogger.Error(\"❌ 配置重载失败: %v\", err)\n\t\t\t}\n\n\t\tcase err, ok := <-configWatcher.Errors:\n\t\t\tif !ok {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tlogger.Error(\"❌ 配置监听错误: %v\", err)\n\t\t}\n\t}\n}\n\n// stopConfigWatcher 停止配置文件监听\nfunc stopConfigWatcher() {\n\tif configWatcher != nil {\n\t\tconfigWatcher.Close()\n\t}\n}\n\nvar (\n\tDataDir       string\n\tProxy         string\n\tListenAddr    string\n\tDefaultConfig string\n\tJwtTTL        = 270 * time.Second\n)\n\n// mergeConfig 合并配置：loaded 中有值的字段覆盖 base 中的默认值\nfunc mergeConfig(base, loaded *AppConfig) {\n\t// 基本字段\n\tif len(loaded.APIKeys) > 0 {\n\t\tbase.APIKeys = loaded.APIKeys\n\t}\n\tif loaded.ListenAddr != \"\" {\n\t\tbase.ListenAddr = loaded.ListenAddr\n\t}\n\tif loaded.DataDir != \"\" {\n\t\tbase.DataDir = loaded.DataDir\n\t}\n\tif loaded.Proxy != \"\" {\n\t\tbase.Proxy = loaded.Proxy\n\t}\n\tif loaded.DefaultConfig != \"\" {\n\t\tbase.DefaultConfig = loaded.DefaultConfig\n\t}\n\t// Debug 是 bool，直接覆盖\n\tbase.Debug = loaded.Debug\n\n\t// Pool 配置\n\tif loaded.Pool.TargetCount > 0 {\n\t\tbase.Pool.TargetCount = loaded.Pool.TargetCount\n\t}\n\tif loaded.Pool.MinCount > 0 {\n\t\tbase.Pool.MinCount = loaded.Pool.MinCount\n\t}\n\tif loaded.Pool.CheckIntervalMinutes > 0 {\n\t\tbase.Pool.CheckIntervalMinutes = loaded.Pool.CheckIntervalMinutes\n\t}\n\tif loaded.Pool.RegisterThreads > 0 {\n\t\tbase.Pool.RegisterThreads = loaded.Pool.RegisterThreads\n\t}\n\t// bool 字段直接覆盖\n\tbase.Pool.RegisterHeadless = loaded.Pool.RegisterHeadless\n\tbase.Pool.RefreshOnStartup = loaded.Pool.RefreshOnStartup\n\tbase.Pool.EnableBrowserRefresh = loaded.Pool.EnableBrowserRefresh\n\tbase.Pool.BrowserRefreshHeadless = loaded.Pool.BrowserRefreshHeadless\n\tbase.Pool.AutoDelete401 = loaded.Pool.AutoDelete401\n\n\tif loaded.Pool.RefreshCooldownSec > 0 {\n\t\tbase.Pool.RefreshCooldownSec = loaded.Pool.RefreshCooldownSec\n\t}\n\tif loaded.Pool.UseCooldownSec > 0 {\n\t\tbase.Pool.UseCooldownSec = loaded.Pool.UseCooldownSec\n\t}\n\tif loaded.Pool.MaxFailCount > 0 {\n\t\tbase.Pool.MaxFailCount = loaded.Pool.MaxFailCount\n\t}\n\tif loaded.Pool.BrowserRefreshMaxRetry > 0 {\n\t\tbase.Pool.BrowserRefreshMaxRetry = loaded.Pool.BrowserRefreshMaxRetry\n\t}\n\n\t// PoolServer 配置\n\tbase.PoolServer = loaded.PoolServer\n\n\t// Flow 配置\n\tbase.Flow = loaded.Flow\n\n\t// ProxyPool 配置\n\tif len(loaded.ProxyPool.Subscribes) > 0 {\n\t\tbase.ProxyPool.Subscribes = loaded.ProxyPool.Subscribes\n\t}\n\tif len(loaded.ProxyPool.Files) > 0 {\n\t\tbase.ProxyPool.Files = loaded.ProxyPool.Files\n\t}\n\tif loaded.ProxyPool.Proxy != \"\" {\n\t\tbase.ProxyPool.Proxy = loaded.ProxyPool.Proxy\n\t}\n\tbase.ProxyPool.HealthCheck = loaded.ProxyPool.HealthCheck\n\tbase.ProxyPool.CheckOnStartup = loaded.ProxyPool.CheckOnStartup\n\n\t// Note\n\tif len(loaded.Note) > 0 {\n\t\tbase.Note = loaded.Note\n\t}\n}\n\n// 保存默认配置到文件\nfunc saveDefaultConfig(configPath string) error {\n\t// 确保目录存在\n\tdir := filepath.Dir(configPath)\n\tif err := os.MkdirAll(dir, 0755); err != nil {\n\t\treturn err\n\t}\n\tdata, err := json.MarshalIndent(appConfig, \"\", \"  \")\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn os.WriteFile(configPath, data, 0644)\n}\n\nfunc loadAppConfig() {\n\t// 尝试加载配置文件\n\tconfigPath := \"config/config.json\"\n\tif data, err := os.ReadFile(configPath); err == nil {\n\t\t// 保留默认值，仅覆盖配置文件中存在的字段\n\t\tvar loadedConfig AppConfig\n\t\tif err := json.Unmarshal(data, &loadedConfig); err != nil {\n\t\t\tlogger.Warn(\"⚠️ 解析配置文件失败: %v，使用默认配置\", err)\n\t\t} else {\n\t\t\t// 合并配置：配置文件中有的字段覆盖默认值，没有的保留默认值\n\t\t\tmergeConfig(&appConfig, &loadedConfig)\n\t\t\tlogger.Info(\"✅ 加载配置文件: %s\", configPath)\n\t\t}\n\t} else if os.IsNotExist(err) {\n\t\t// 配置文件不存在，创建默认配置\n\t\tlogger.Warn(\"⚠️ 配置文件不存在，创建默认配置: %s\", configPath)\n\t\tif err := saveDefaultConfig(configPath); err != nil {\n\t\t\tlogger.Error(\"❌ 创建默认配置失败: %v\", err)\n\t\t}\n\t}\n\tif v := os.Getenv(\"DATA_DIR\"); v != \"\" {\n\t\tappConfig.DataDir = v\n\t}\n\tif v := os.Getenv(\"PROXY\"); v != \"\" {\n\t\tappConfig.Proxy = v\n\t}\n\tif v := os.Getenv(\"LISTEN_ADDR\"); v != \"\" {\n\t\tappConfig.ListenAddr = v\n\t}\n\tif v := os.Getenv(\"CONFIG_ID\"); v != \"\" {\n\t\tappConfig.DefaultConfig = v\n\t}\n\tif v := os.Getenv(\"API_KEY\"); v != \"\" {\n\t\tappConfig.APIKeys = append(appConfig.APIKeys, v)\n\t}\n\n\t// 设置全局变量\n\tDataDir = appConfig.DataDir\n\tProxy = appConfig.Proxy\n\tListenAddr = appConfig.ListenAddr\n\tDefaultConfig = appConfig.DefaultConfig\n\n\t// 应用调试模式\n\tlogger.SetDebugMode(appConfig.Debug)\n\n\t// 应用号池配置\n\tpool.SetCooldowns(appConfig.Pool.RefreshCooldownSec, appConfig.Pool.UseCooldownSec)\n\tif appConfig.Pool.MaxFailCount > 0 {\n\t\tpool.MaxFailCount = appConfig.Pool.MaxFailCount\n\t}\n\tpool.EnableBrowserRefresh = appConfig.Pool.EnableBrowserRefresh\n\tpool.BrowserRefreshHeadless = appConfig.Pool.BrowserRefreshHeadless\n\tif appConfig.Pool.BrowserRefreshMaxRetry >= 0 {\n\t\tpool.BrowserRefreshMaxRetry = appConfig.Pool.BrowserRefreshMaxRetry\n\t}\n\tpool.AutoDelete401 = appConfig.Pool.AutoDelete401\n\t// 服务端模式下，如果 expired_action 是 delete，则同步设置 AutoDelete401\n\tif appConfig.PoolServer.Enable && appConfig.PoolServer.Mode == \"server\" && appConfig.PoolServer.ExpiredAction == \"delete\" {\n\t\tpool.AutoDelete401 = true\n\t\tlogger.Info(\"🗑️ 服务端模式 expired_action=delete，启用 AutoDelete401\")\n\t}\n\tpool.DataDir = DataDir\n\tpool.DefaultConfig = DefaultConfig\n\tpool.Proxy = Proxy\n\tregister.DataDir = DataDir\n\tregister.TargetCount = appConfig.Pool.TargetCount\n\tregister.MinCount = appConfig.Pool.MinCount\n\tregister.CheckInterval = time.Duration(appConfig.Pool.CheckIntervalMinutes) * time.Minute\n\tregister.Threads = appConfig.Pool.RegisterThreads\n\tregister.Headless = appConfig.Pool.RegisterHeadless\n\tregister.Proxy = Proxy\n\n\t// 初始化代理池\n\tinitProxyPool()\n\n\tif pool.EnableBrowserRefresh && pool.BrowserRefreshMaxRetry > 0 {\n\t\tlogger.Info(\"🌐 浏览器刷新已启用 (headless=%v, 最大重试=%d)\", pool.BrowserRefreshHeadless, pool.BrowserRefreshMaxRetry)\n\t} else if pool.EnableBrowserRefresh {\n\t\tlogger.Info(\"🌐 浏览器刷新已禁用 (max_retry=0)\")\n\t\tpool.EnableBrowserRefresh = false\n\t}\n\n\t// 初始化 Flow 客户端\n\tinitFlowClient()\n}\n\n// initFlowClient 初始化 Flow 客户端\nfunc initFlowClient() {\n\tif !appConfig.Flow.Enable {\n\t\tlogger.Info(\"📹 Flow 服务已禁用\")\n\t\treturn\n\t}\n\n\tcfg := flow.FlowConfig{\n\t\tProxy:           appConfig.Flow.Proxy,\n\t\tTimeout:         appConfig.Flow.Timeout,\n\t\tPollInterval:    appConfig.Flow.PollInterval,\n\t\tMaxPollAttempts: appConfig.Flow.MaxPollAttempts,\n\t}\n\tif cfg.Proxy == \"\" {\n\t\tcfg.Proxy = Proxy\n\t}\n\n\tflowClient = flow.NewFlowClient(cfg)\n\n\t// 初始化 Token 池\n\tflowTokenPool = flow.NewTokenPool(DataDir, flowClient)\n\n\t// 从 data/at 目录加载 Token\n\tloadedFromDir, err := flowTokenPool.LoadFromDir()\n\tif err != nil {\n\t\tlogger.Warn(\"⚠️ 从 data/at 加载 Flow Token 失败: %v\", err)\n\t}\n\n\t// 添加配置文件中的 Tokens（兼容旧配置）\n\tfor i, st := range appConfig.Flow.Tokens {\n\t\ttoken := &flow.FlowToken{\n\t\t\tID: fmt.Sprintf(\"flow_token_%d\", i),\n\t\t\tST: st,\n\t\t}\n\t\tflowClient.AddToken(token)\n\t}\n\n\ttotalTokens := loadedFromDir + len(appConfig.Flow.Tokens)\n\tif totalTokens == 0 {\n\t\tlogger.Info(\"📹 Flow 服务已启用但无可用 Token (请将 cookie 放入 data/at/ 目录)\")\n\t\tflowHandler = flow.NewGenerationHandler(flowClient)\n\t\treturn\n\t}\n\n\t// 启动 AT 刷新 worker (每 30 分钟刷新一次)\n\tflowTokenPool.StartRefreshWorker(30 * time.Minute)\n\n\t// 启动文件监听 (自动加载新增 Token)\n\tif err := flowTokenPool.StartWatcher(); err != nil {\n\t\tlogger.Warn(\"⚠️ Flow 文件监听启动失败: %v\", err)\n\t}\n\n\tflowHandler = flow.NewGenerationHandler(flowClient)\n\tlogger.Info(\"📹 Flow 服务已启用，共 %d 个 Token (目录: %d, 配置: %d)\", totalTokens, loadedFromDir, len(appConfig.Flow.Tokens))\n}\n\nfunc initProxyPool() {\n\t// 服务端模式不需要代理池\n\tif appConfig.PoolServer.Enable && appConfig.PoolServer.Mode == \"server\" {\n\t\tlogger.Info(\"🖥️ 服务端模式，跳过代理初始化\")\n\t\treturn\n\t}\n\n\t// 初始化 sing-box（用于 hysteria2/tuic 等协议）\n\tproxy.InitSingbox()\n\n\t// 添加订阅链接（新配置）\n\tfor _, sub := range appConfig.ProxyPool.Subscribes {\n\t\tproxy.Manager.AddSubscribeURL(sub)\n\t}\n\t// 兼容旧配置\n\tif appConfig.ProxySubscribe != \"\" {\n\t\tproxy.Manager.AddSubscribeURL(appConfig.ProxySubscribe)\n\t}\n\n\t// 添加代理文件\n\tfor _, file := range appConfig.ProxyPool.Files {\n\t\tproxy.Manager.AddProxyFile(file)\n\t}\n\tif err := proxy.Manager.LoadAll(); err != nil {\n\t\tlogger.Warn(\"⚠️ 加载代理失败: %v\", err)\n\t}\n\n\t// 当有代理配置时，默认开启健康检查（除非明确关闭）\n\thasProxyConfig := len(appConfig.ProxyPool.Subscribes) > 0 || len(appConfig.ProxyPool.Files) > 0 || appConfig.ProxySubscribe != \"\"\n\tshouldHealthCheck := hasProxyConfig || appConfig.ProxyPool.HealthCheck\n\n\tif shouldHealthCheck && appConfig.ProxyPool.CheckOnStartup {\n\t\tgo func() {\n\t\t\tproxy.Manager.CheckAllHealth()\n\t\t\t// 健康检查完成后初始化实例池\n\t\t\tif proxy.Manager.HealthyCount() > 0 {\n\t\t\t\tpoolSize := appConfig.Pool.RegisterThreads\n\t\t\t\tif poolSize <= 0 {\n\t\t\t\t\tpoolSize = pool.DefaultProxyCount\n\t\t\t\t}\n\t\t\t\tif poolSize > 10 {\n\t\t\t\t\tpoolSize = 10\n\t\t\t\t}\n\t\t\t\tproxy.Manager.SetMaxPoolSize(poolSize)\n\t\t\t\tif err := proxy.Manager.InitInstancePool(poolSize); err != nil {\n\t\t\t\t\tlogger.Warn(\"⚠️ 初始化代理实例池失败: %v\", err)\n\t\t\t\t} else {\n\t\t\t\t\tlogger.Info(\"✅ 代理实例池初始化完成: %d 个实例\", poolSize)\n\t\t\t\t}\n\t\t\t}\n\t\t}()\n\t} else if proxy.Manager.TotalCount() > 0 {\n\t\t// 不需要健康检查时直接标记就绪\n\t\tproxy.Manager.SetReady(true)\n\t}\n\tif proxy.Manager.TotalCount() == 0 {\n\t\tif appConfig.ProxyPool.Proxy != \"\" {\n\t\t\tproxy.Manager.SetProxies([]string{appConfig.ProxyPool.Proxy})\n\t\t} else if Proxy != \"\" {\n\t\t\tproxy.Manager.SetProxies([]string{Proxy})\n\t\t}\n\t}\n\tif proxy.Manager.TotalCount() == 0 || AutoSubscribeEnabled {\n\t\tlogger.Info(\"🔄 启动自动订阅服务（每小时注册获取代理）...\")\n\t\tproxy.Manager.StartAutoSubscribe()\n\t}\n\n\tif proxy.Manager.TotalCount() > 0 {\n\t\tproxy.Manager.StartAutoUpdate()\n\t\tlogger.Info(\"✅ 代理池已初始化: %d 个节点, %d 个健康\",\n\t\t\tproxy.Manager.TotalCount(), proxy.Manager.HealthyCount())\n\t}\n\tregister.GetProxy = func() string {\n\t\tif proxy.Manager.Count() > 0 {\n\t\t\treturn proxy.Manager.Next()\n\t\t}\n\t\treturn Proxy\n\t}\n\tregister.ReleaseProxy = func(proxyURL string) {\n\t\tproxy.Manager.ReleaseByURL(proxyURL)\n\t}\n}\n\nvar BaseModels = []string{\n\t// Gemini 文本模型\n\t\"gemini-2.5-flash\",\n\t\"gemini-2.5-pro\",\n\t\"gemini-3-pro-preview\",\n\t\"gemini-3-pro\",\n\t// Gemini 图片生成\n\t\"gemini-2.5-flash-image\",\n\t\"gemini-2.5-pro-image\",\n\t\"gemini-3-pro-preview-image\",\n\t\"gemini-3-pro-image\",\n\t// Gemini 视频生成\n\t\"gemini-2.5-flash-video\",\n\t\"gemini-2.5-pro-video\",\n\t\"gemini-3-pro-preview-video\",\n\t\"gemini-3-pro-video\",\n\t// Gemini 搜索\n\t\"gemini-2.5-flash-search\",\n\t\"gemini-2.5-pro-search\",\n\t\"gemini-3-pro-preview-search\",\n\t\"gemini-3-pro-search\",\n\n\t\"gemini-3-flash-preview\",\n\t\"gemini-3-flash-preview-image\",\n\t\"gemini-3-flash-preview-video\",\n\t\"gemini-3-flash-preview-search\",\n\n\t\"gemini-3-flash\",\n\t\"gemini-3-flash-image\",\n\t\"gemini-3-flash-video\",\n\t\"gemini-3-flash-search\",\n\n\t\"gemini-2.5-flash-preview-latest\",\n\t\"gemini-2.5-flash-preview-latest-image\",\n\t\"gemini-2.5-flash-preview-latest-video\",\n\t\"gemini-2.5-flash-preview-latest-search\",\n}\nvar FlowModels = []string{\n\t// Flow 图片生成模型\n\t\"gemini-2.5-flash-image-landscape\",\n\t\"gemini-2.5-flash-image-portrait\",\n\t\"gemini-3.0-pro-image-landscape\",\n\t\"gemini-3.0-pro-image-portrait\",\n\t\"imagen-4.0-generate-preview-landscape\",\n\t\"imagen-4.0-generate-preview-portrait\",\n\t// Flow 文生视频 (T2V)\n\t\"veo_3_1_t2v_fast_portrait\",\n\t\"veo_3_1_t2v_fast_landscape\",\n\t\"veo_2_1_fast_d_15_t2v_portrait\",\n\t\"veo_2_1_fast_d_15_t2v_landscape\",\n\t\"veo_2_0_t2v_portrait\",\n\t\"veo_2_0_t2v_landscape\",\n\t// Flow 图生视频 (I2V)\n\t\"veo_3_1_i2v_s_fast_fl_portrait\",\n\t\"veo_3_1_i2v_s_fast_fl_landscape\",\n\t\"veo_2_1_fast_d_15_i2v_portrait\",\n\t\"veo_2_1_fast_d_15_i2v_landscape\",\n\t\"veo_2_0_i2v_portrait\",\n\t\"veo_2_0_i2v_landscape\",\n\t// Flow 多图生成视频 (R2V)\n\t\"veo_3_0_r2v_fast_portrait\",\n\t\"veo_3_0_r2v_fast_landscape\",\n}\n\nfunc GetAvailableModels() []string {\n\tif flowHandler != nil {\n\t\t// Flow 已启用，返回全部模型\n\t\treturn append(BaseModels, FlowModels...)\n\t}\n\t// Flow 未启用，只返回基础模型\n\treturn BaseModels\n}\n\n// 模型名称映射到 Google API 的 modelId\nvar modelMapping = map[string]string{\n\t\"gemini-2.5-flash\":     \"gemini-2.5-flash\",\n\t\"gemini-2.5-pro\":       \"gemini-2.5-pro\",\n\t\"gemini-3-pro-preview\": \"gemini-3-pro-preview\",\n\t\"gemini-3-pro\":         \"gemini-3-pro\",\n}\n\nfunc getEnv(key, def string) string {\n\tif v := os.Getenv(key); v != \"\" {\n\t\treturn v\n\t}\n\treturn def\n}\n\nfunc getCommonHeaders(jwt, origAuth string) map[string]string {\n\theaders := map[string]string{\n\t\t\"accept\":             \"*/*\",\n\t\t\"accept-encoding\":    \"gzip, deflate, br, zstd\",\n\t\t\"accept-language\":    \"zh-CN,zh;q=0.9,en;q=0.8\",\n\t\t\"authorization\":      \"Bearer \" + jwt,\n\t\t\"content-type\":       \"application/json\",\n\t\t\"origin\":             \"https://business.gemini.google\",\n\t\t\"referer\":            \"https://business.gemini.google/\",\n\t\t\"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\",\n\t\t\"x-server-timeout\":   \"1800\",\n\t\t\"sec-ch-ua\":          `\"Chromium\";v=\"124\", \"Google Chrome\";v=\"124\", \"Not-A.Brand\";v=\"99\"`,\n\t\t\"sec-ch-ua-mobile\":   \"?0\",\n\t\t\"sec-ch-ua-platform\": `\"Windows\"`,\n\t\t\"sec-fetch-dest\":     \"empty\",\n\t\t\"sec-fetch-mode\":     \"cors\",\n\t\t\"sec-fetch-site\":     \"cross-site\",\n\t}\n\t// 同时携带原始 authorization\n\tif origAuth != \"\" {\n\t\theaders[\"x-original-authorization\"] = origAuth\n\t}\n\treturn headers\n}\n\nfunc createSession(jwt, configID, origAuth string) (string, error) {\n\treturn createSessionWithRetry(jwt, configID, origAuth, 3)\n}\n\n// createSessionWithRetry 创建session带重试（处理400错误）\nfunc createSessionWithRetry(jwt, configID, origAuth string, maxRetries int) (string, error) {\n\tvar lastErr error\n\n\tfor retry := 0; retry < maxRetries; retry++ {\n\t\tif retry > 0 {\n\t\t\t// 等待后重试\n\t\t\twaitTime := time.Duration(retry*500) * time.Millisecond\n\t\t\ttime.Sleep(waitTime)\n\t\t\tlogger.Info(\"🔄 createSession 重试 %d/%d\", retry+1, maxRetries)\n\t\t}\n\n\t\tsessionName, err := createSessionOnce(jwt, configID, origAuth)\n\t\tif err == nil {\n\t\t\treturn sessionName, nil\n\t\t}\n\n\t\tlastErr = err\n\t\terrMsg := err.Error()\n\n\t\t// 400错误可以重试\n\t\tif strings.Contains(errMsg, \"400\") {\n\t\t\tlogger.Warn(\"⚠️ createSession 400 错误，尝试重试...\")\n\t\t\tcontinue\n\t\t}\n\n\t\t// 401/403 不重试\n\t\tif strings.Contains(errMsg, \"401\") || strings.Contains(errMsg, \"403\") {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\t// 其他错误继续重试\n\t}\n\n\treturn \"\", lastErr\n}\n\n// createSessionOnce 单次创建session\nfunc createSessionOnce(jwt, configID, origAuth string) (string, error) {\n\tbody := map[string]interface{}{\n\t\t\"configId\":         configID,\n\t\t\"additionalParams\": map[string]string{\"token\": \"-\"},\n\t\t\"createSessionRequest\": map[string]interface{}{\n\t\t\t\"session\": map[string]string{\"name\": \"\", \"displayName\": \"\"},\n\t\t},\n\t}\n\n\tbodyBytes, _ := json.Marshal(body)\n\treq, _ := http.NewRequest(\"POST\", \"https://biz-discoveryengine.googleapis.com/v1alpha/locations/global/widgetCreateSession\", bytes.NewReader(bodyBytes))\n\n\tfor k, v := range getCommonHeaders(jwt, origAuth) {\n\t\treq.Header.Set(k, v)\n\t}\n\n\tresp, err := utils.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"createSession 请求失败: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\trespBody, err := utils.ReadResponseBody(resp)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"读取响应失败: %w\", err)\n\t}\n\n\tif resp.StatusCode != 200 {\n\t\treturn \"\", fmt.Errorf(\"createSession 失败: %d %s\", resp.StatusCode, string(respBody))\n\t}\n\n\tvar result struct {\n\t\tSession struct {\n\t\t\tName string `json:\"name\"`\n\t\t} `json:\"session\"`\n\t}\n\tif err := json.Unmarshal(respBody, &result); err != nil {\n\t\treturn \"\", fmt.Errorf(\"解析 session 响应失败: %w\", err)\n\t}\n\n\treturn result.Session.Name, nil\n}\nfunc uploadContextFile(jwt, configID, sessionName, mimeType, base64Content, origAuth string) (string, error) {\n\text := \"jpg\"\n\tif parts := strings.Split(mimeType, \"/\"); len(parts) == 2 {\n\t\text = parts[1]\n\t}\n\tfileName := fmt.Sprintf(\"upload_%d_%s.%s\", time.Now().Unix(), uuid.New().String()[:6], ext)\n\n\tbody := map[string]interface{}{\n\t\t\"configId\":         configID,\n\t\t\"additionalParams\": map[string]string{\"token\": \"-\"},\n\t\t\"addContextFileRequest\": map[string]interface{}{\n\t\t\t\"name\":         sessionName,\n\t\t\t\"fileName\":     fileName,\n\t\t\t\"mimeType\":     mimeType,\n\t\t\t\"fileContents\": base64Content,\n\t\t},\n\t}\n\n\tbodyBytes, _ := json.Marshal(body)\n\treq, _ := http.NewRequest(\"POST\", \"https://biz-discoveryengine.googleapis.com/v1alpha/locations/global/widgetAddContextFile\", bytes.NewReader(bodyBytes))\n\n\tfor k, v := range getCommonHeaders(jwt, origAuth) {\n\t\treq.Header.Set(k, v)\n\t}\n\n\tresp, err := utils.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"上传文件请求失败: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\trespBody, err := utils.ReadResponseBody(resp)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"读取响应失败: %w\", err)\n\t}\n\n\tif resp.StatusCode != 200 {\n\t\treturn \"\", fmt.Errorf(\"上传文件失败: %d %s\", resp.StatusCode, string(respBody))\n\t}\n\n\tvar result struct {\n\t\tAddContextFileResponse struct {\n\t\t\tFileID string `json:\"fileId\"`\n\t\t} `json:\"addContextFileResponse\"`\n\t}\n\tif err := json.Unmarshal(respBody, &result); err != nil {\n\t\treturn \"\", fmt.Errorf(\"解析上传响应失败: %w\", err)\n\t}\n\n\tif result.AddContextFileResponse.FileID == \"\" {\n\t\treturn \"\", fmt.Errorf(\"上传成功但 fileId 为空，响应: %s\", string(respBody))\n\t}\n\n\treturn result.AddContextFileResponse.FileID, nil\n}\nfunc uploadContextFileByURL(jwt, configID, sessionName, imageURL, origAuth string) (string, error) {\n\tbody := map[string]interface{}{\n\t\t\"configId\":         configID,\n\t\t\"additionalParams\": map[string]string{\"token\": \"-\"},\n\t\t\"addContextFileRequest\": map[string]interface{}{\n\t\t\t\"name\":    sessionName,\n\t\t\t\"fileUri\": imageURL,\n\t\t},\n\t}\n\n\tbodyBytes, _ := json.Marshal(body)\n\treq, _ := http.NewRequest(\"POST\", \"https://biz-discoveryengine.googleapis.com/v1alpha/locations/global/widgetAddContextFile\", bytes.NewReader(bodyBytes))\n\n\tfor k, v := range getCommonHeaders(jwt, origAuth) {\n\t\treq.Header.Set(k, v)\n\t}\n\n\tresp, err := utils.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"上传文件请求失败: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\trespBody, err := utils.ReadResponseBody(resp)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"读取响应失败: %w\", err)\n\t}\n\n\tif resp.StatusCode != 200 {\n\t\treturn \"\", fmt.Errorf(\"URL上传文件失败: %d %s\", resp.StatusCode, string(respBody))\n\t}\n\n\tvar result struct {\n\t\tAddContextFileResponse struct {\n\t\t\tFileID string `json:\"fileId\"`\n\t\t} `json:\"addContextFileResponse\"`\n\t}\n\tif err := json.Unmarshal(respBody, &result); err != nil {\n\t\treturn \"\", fmt.Errorf(\"解析上传响应失败: %w\", err)\n\t}\n\n\tif result.AddContextFileResponse.FileID == \"\" {\n\t\treturn \"\", fmt.Errorf(\"URL上传成功但 fileId 为空，响应: %s\", string(respBody))\n\t}\n\n\treturn result.AddContextFileResponse.FileID, nil\n}\n\ntype Message struct {\n\tRole       string      `json:\"role\"`\n\tContent    interface{} `json:\"content\"`                // string 或 []ContentPart\n\tName       string      `json:\"name,omitempty\"`         // 函数名称（tool角色时）\n\tToolCalls  []ToolCall  `json:\"tool_calls,omitempty\"`   // 工具调用（assistant角色时）\n\tToolCallID string      `json:\"tool_call_id,omitempty\"` // 工具调用ID（tool角色时）\n}\n\ntype ContentPart struct {\n\tType     string    `json:\"type\"`\n\tText     string    `json:\"text,omitempty\"`\n\tImageURL *ImageURL `json:\"image_url,omitempty\"`\n}\n\ntype ImageURL struct {\n\tURL string `json:\"url\"`\n}\n\n// OpenAI格式的工具定义\ntype ToolDef struct {\n\tType     string      `json:\"type\"` // \"function\"\n\tFunction FunctionDef `json:\"function\"`\n}\n\ntype FunctionDef struct {\n\tName        string                 `json:\"name\"`\n\tDescription string                 `json:\"description\"`\n\tParameters  map[string]interface{} `json:\"parameters\"`\n}\n\n// 工具调用结果\ntype ToolCall struct {\n\tID       string       `json:\"id\"`\n\tType     string       `json:\"type\"` // \"function\"\n\tFunction FunctionCall `json:\"function\"`\n}\n\ntype FunctionCall struct {\n\tName      string `json:\"name\"`\n\tArguments string `json:\"arguments\"`\n}\n\ntype ChatRequest struct {\n\tModel       string    `json:\"model\"`\n\tMessages    []Message `json:\"messages\"`\n\tStream      bool      `json:\"stream\"`\n\tTemperature float64   `json:\"temperature\"`\n\tTopP        float64   `json:\"top_p\"`\n\tTools       []ToolDef `json:\"tools,omitempty\"`       // 工具定义\n\tToolChoice  string    `json:\"tool_choice,omitempty\"` // \"auto\", \"none\", \"required\"\n}\n\ntype ChatChoice struct {\n\tIndex        int                    `json:\"index\"`\n\tDelta        map[string]interface{} `json:\"delta,omitempty\"`\n\tMessage      map[string]interface{} `json:\"message,omitempty\"`\n\tFinishReason *string                `json:\"finish_reason\"`\n\tLogprobs     interface{}            `json:\"logprobs\"` // OpenAI兼容\n}\n\ntype ChatChunk struct {\n\tID                string       `json:\"id\"`\n\tObject            string       `json:\"object\"`\n\tCreated           int64        `json:\"created\"`\n\tModel             string       `json:\"model\"`\n\tSystemFingerprint string       `json:\"system_fingerprint,omitempty\"`\n\tChoices           []ChatChoice `json:\"choices\"`\n}\n\nfunc createChunk(id string, created int64, model string, delta map[string]interface{}, finishReason *string) string {\n\tif delta == nil {\n\t\tdelta = map[string]interface{}{}\n\t}\n\tchunk := ChatChunk{\n\t\tID:      id,\n\t\tObject:  \"chat.completion.chunk\",\n\t\tCreated: created,\n\t\tModel:   model,\n\t\tChoices: []ChatChoice{{\n\t\t\tIndex:        0,\n\t\t\tDelta:        delta,\n\t\t\tFinishReason: finishReason,\n\t\t\tLogprobs:     nil,\n\t\t}},\n\t}\n\tdata, _ := json.Marshal(chunk)\n\treturn string(data)\n}\n\nfunc extractContentFromReply(replyMap map[string]interface{}, jwt, session, configID, origAuth string) (text string, imageData string, imageMime string, reasoning string, downloadErr error) {\n\tgroundedContent, ok := replyMap[\"groundedContent\"].(map[string]interface{})\n\tif !ok {\n\t\treturn\n\t}\n\tcontent, ok := groundedContent[\"content\"].(map[string]interface{})\n\tif !ok {\n\t\treturn\n\t}\n\tif thought, ok := content[\"thought\"].(bool); ok && thought {\n\t\tif t, ok := content[\"text\"].(string); ok && t != \"\" {\n\t\t\treasoning = t\n\t\t}\n\t\treturn\n\t}\n\tif t, ok := content[\"text\"].(string); ok && t != \"\" {\n\t\ttext = t\n\t}\n\tif inlineData, ok := content[\"inlineData\"].(map[string]interface{}); ok {\n\t\tif mime, ok := inlineData[\"mimeType\"].(string); ok {\n\t\t\timageMime = mime\n\t\t}\n\t\tif data, ok := inlineData[\"data\"].(string); ok {\n\t\t\timageData = data\n\t\t}\n\t}\n\tif file, ok := content[\"file\"].(map[string]interface{}); ok {\n\t\tfileId, _ := file[\"fileId\"].(string)\n\t\tmimeType, _ := file[\"mimeType\"].(string)\n\t\tif fileId != \"\" {\n\t\t\tfileType := \"文件\"\n\t\t\tif strings.HasPrefix(mimeType, \"image/\") {\n\t\t\t\tfileType = \"图片\"\n\t\t\t} else if strings.HasPrefix(mimeType, \"video/\") {\n\t\t\t\tfileType = \"视频\"\n\t\t\t}\n\t\t\tdata, err := downloadGeneratedFile(jwt, fileId, session, configID, origAuth)\n\t\t\tif err != nil {\n\t\t\t\tlogger.Error(\"❌ 下载%s失败: %v\", fileType, err)\n\t\t\t\tdownloadErr = err // 返回错误供上层处理\n\t\t\t} else {\n\t\t\t\timageData = data\n\t\t\t\timageMime = mimeType\n\t\t\t}\n\t\t}\n\t}\n\n\treturn\n}\n\n// ErrDownloadNeedsRetry 标识下载失败需要整体重试（换号重新生成）\nvar ErrDownloadNeedsRetry = fmt.Errorf(\"DOWNLOAD_NEEDS_RETRY\")\n\nfunc downloadGeneratedFile(jwt, fileId, session, configID, origAuth string) (string, error) {\n\treturn downloadGeneratedFileWithRetry(jwt, fileId, session, configID, origAuth, 2)\n}\n\nfunc downloadGeneratedFileWithRetry(jwt, fileId, session, configID, origAuth string, maxRetries int) (string, error) {\n\t// 参数验证\n\tif jwt == \"\" {\n\t\treturn \"\", fmt.Errorf(\"JWT 为空，无法下载文件\")\n\t}\n\tif session == \"\" {\n\t\treturn \"\", fmt.Errorf(\"session 为空，无法下载文件\")\n\t}\n\tif configID == \"\" {\n\t\treturn \"\", fmt.Errorf(\"configID 为空，无法下载文件\")\n\t}\n\tvar lastErr error\n\tvar authFailCount int\n\n\tfor retry := 0; retry < maxRetries; retry++ {\n\t\tresult, err := downloadGeneratedFileOnce(jwt, fileId, session, configID, origAuth)\n\t\tif err == nil {\n\t\t\treturn result, nil\n\t\t}\n\n\t\tlastErr = err\n\t\terrMsg := err.Error()\n\n\t\t// 检测认证失败（401/403）\n\t\tif strings.Contains(errMsg, \"401\") || strings.Contains(errMsg, \"403\") ||\n\t\t\tstrings.Contains(errMsg, \"UNAUTHENTICATED\") || strings.Contains(errMsg, \"SESSION_COOKIE_INVALID\") {\n\t\t\tauthFailCount++\n\t\t\tlogger.Warn(\"⚠️ 下载文件认证失败 (尝试 %d/%d): %v\", retry+1, maxRetries, err)\n\n\t\t\t// 认证失败超过1次，返回特殊错误让上层重新发起整个请求\n\t\t\tif authFailCount >= 1 {\n\t\t\t\tlogger.Info(\"🔄 下载认证失败，需要换号重新生成\")\n\t\t\t\treturn \"\", fmt.Errorf(\"%w: 401/403 认证失败\", ErrDownloadNeedsRetry)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\t// 其他错误，等待后重试\n\t\tlogger.Error(\"❌ 下载文件失败 (尝试 %d/%d): %v\", retry+1, maxRetries, err)\n\t\ttime.Sleep(300 * time.Millisecond)\n\t}\n\n\treturn \"\", fmt.Errorf(\"下载文件失败，已重试 %d 次: %w\", maxRetries, lastErr)\n}\n\n// downloadGeneratedFileOnce 单次下载文件尝试\nfunc downloadGeneratedFileOnce(jwt, fileId, session, configID, origAuth string) (string, error) {\n\n\t// 步骤1: 使用 widgetListSessionFileMetadata 获取文件下载 URL\n\tlistBody := map[string]interface{}{\n\t\t\"configId\":         configID,\n\t\t\"additionalParams\": map[string]string{\"token\": \"-\"},\n\t\t\"listSessionFileMetadataRequest\": map[string]interface{}{\n\t\t\t\"name\":   session,\n\t\t\t\"filter\": \"file_origin_type = AI_GENERATED\",\n\t\t},\n\t}\n\tlistBodyBytes, _ := json.Marshal(listBody)\n\n\tlistReq, _ := http.NewRequest(\"POST\", \"https://biz-discoveryengine.googleapis.com/v1alpha/locations/global/widgetListSessionFileMetadata\", bytes.NewReader(listBodyBytes))\n\tfor k, v := range getCommonHeaders(jwt, origAuth) {\n\t\tlistReq.Header.Set(k, v)\n\t}\n\n\tlistResp, err := utils.HTTPClient.Do(listReq)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"获取文件元数据失败: %w\", err)\n\t}\n\tdefer listResp.Body.Close()\n\n\tlistRespBody, _ := utils.ReadResponseBody(listResp)\n\n\tif listResp.StatusCode != 200 {\n\t\treturn \"\", fmt.Errorf(\"获取文件元数据失败: HTTP %d: %s\", listResp.StatusCode, string(listRespBody))\n\t}\n\n\t// 解析响应，查找匹配的 fileId\n\tvar listResult struct {\n\t\tListSessionFileMetadataResponse struct {\n\t\t\tFileMetadata []struct {\n\t\t\t\tFileID      string `json:\"fileId\"`\n\t\t\t\tSession     string `json:\"session\"` // 包含完整的 projects 路径\n\t\t\t\tDownloadURI string `json:\"downloadUri\"`\n\t\t\t} `json:\"fileMetadata\"`\n\t\t} `json:\"listSessionFileMetadataResponse\"`\n\t}\n\tif err := json.Unmarshal(listRespBody, &listResult); err != nil {\n\t\treturn \"\", fmt.Errorf(\"解析文件元数据失败: %w\", err)\n\t}\n\n\t// 查找匹配的文件，获取完整 session 路径\n\tvar fullSession string\n\tfor _, meta := range listResult.ListSessionFileMetadataResponse.FileMetadata {\n\t\tif meta.FileID == fileId {\n\t\t\tfullSession = meta.Session // 如: projects/372889301682/locations/global/collections/...\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif fullSession == \"\" {\n\t\treturn \"\", fmt.Errorf(\"未找到 fileId=%s 的文件信息\", fileId)\n\t}\n\n\tdownloadURL := fmt.Sprintf(\"https://biz-discoveryengine.googleapis.com/download/v1alpha/%s:downloadFile?fileId=%s&alt=media\", fullSession, fileId)\n\tdownloadReq, _ := http.NewRequest(\"GET\", downloadURL, nil)\n\tfor k, v := range getCommonHeaders(jwt, origAuth) {\n\t\tdownloadReq.Header.Set(k, v)\n\t}\n\n\tdownloadResp, err := utils.HTTPClient.Do(downloadReq)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"下载图片失败: %w\", err)\n\t}\n\tdefer downloadResp.Body.Close()\n\n\timgBody, _ := utils.ReadResponseBody(downloadResp)\n\n\tif downloadResp.StatusCode != 200 {\n\t\treturn \"\", fmt.Errorf(\"下载图片失败: HTTP %d: %s\", downloadResp.StatusCode, string(imgBody))\n\t}\n\n\t// 响应是原始二进制图片数据，需要转为 base64\n\treturn base64.StdEncoding.EncodeToString(imgBody), nil\n}\n\n// 将图片转换为 Markdown 格式的 data URI\nfunc formatImageAsMarkdown(mimeType, base64Data string) string {\n\treturn fmt.Sprintf(\"![image](data:%s;base64,%s)\", mimeType, base64Data)\n}\n\n// 媒体信息（图片/视频）\ntype MediaInfo struct {\n\tMimeType  string\n\tData      string // base64 数据\n\tURL       string // 原始 URL（如果有）\n\tIsURL     bool   // 是否使用 URL 直接上传\n\tMediaType string // \"image\" 或 \"video\"\n}\n\n// 别名，保持向后兼容\ntype ImageInfo = MediaInfo\n\n// 解析消息内容，支持文本、图片和视频\nfunc parseMessageContent(msg Message) (string, []MediaInfo) {\n\tvar textContent string\n\tvar medias []MediaInfo\n\n\tswitch content := msg.Content.(type) {\n\tcase string:\n\t\ttextContent = content\n\tcase []interface{}:\n\t\tfor _, part := range content {\n\t\t\tpartMap, ok := part.(map[string]interface{})\n\t\t\tif !ok {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tpartType, _ := partMap[\"type\"].(string)\n\t\t\tswitch partType {\n\t\t\tcase \"text\":\n\t\t\t\tif text, ok := partMap[\"text\"].(string); ok {\n\t\t\t\t\ttextContent += text\n\t\t\t\t}\n\t\t\tcase \"image_url\":\n\t\t\t\tif imgURL, ok := partMap[\"image_url\"].(map[string]interface{}); ok {\n\t\t\t\t\tif urlStr, ok := imgURL[\"url\"].(string); ok {\n\t\t\t\t\t\tmedia := parseMediaURL(urlStr, \"image\")\n\t\t\t\t\t\tif media != nil {\n\t\t\t\t\t\t\tmedias = append(medias, *media)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\tcase \"video_url\":\n\t\t\t\t// 支持视频 URL\n\t\t\t\tif videoURL, ok := partMap[\"video_url\"].(map[string]interface{}); ok {\n\t\t\t\t\tif urlStr, ok := videoURL[\"url\"].(string); ok {\n\t\t\t\t\t\tmedia := parseMediaURL(urlStr, \"video\")\n\t\t\t\t\t\tif media != nil {\n\t\t\t\t\t\t\tmedias = append(medias, *media)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\tcase \"file\":\n\t\t\t\t// 支持通用文件类型\n\t\t\t\tif fileData, ok := partMap[\"file\"].(map[string]interface{}); ok {\n\t\t\t\t\tif urlStr, ok := fileData[\"url\"].(string); ok {\n\t\t\t\t\t\tmediaType := \"image\" // 默认图片\n\t\t\t\t\t\tif mime, ok := fileData[\"mime_type\"].(string); ok {\n\t\t\t\t\t\t\tif strings.HasPrefix(mime, \"video/\") {\n\t\t\t\t\t\t\t\tmediaType = \"video\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tmedia := parseMediaURL(urlStr, mediaType)\n\t\t\t\t\t\tif media != nil {\n\t\t\t\t\t\t\tmedias = append(medias, *media)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn textContent, medias\n}\n\n// 解析媒体 URL（图片或视频）\nfunc parseMediaURL(urlStr, defaultType string) *MediaInfo {\n\t// 处理 base64 数据\n\tif strings.HasPrefix(urlStr, \"data:\") {\n\t\t// data:image/jpeg;base64,/9j/4AAQ... 或 data:video/mp4;base64,...\n\t\tparts := strings.SplitN(urlStr, \",\", 2)\n\t\tif len(parts) != 2 {\n\t\t\treturn nil\n\t\t}\n\n\t\tbase64Data := parts[1]\n\t\tvar mediaType string\n\t\tvar mimeType string\n\n\t\t// 检测媒体类型\n\t\tif strings.Contains(parts[0], \"video/\") {\n\t\t\tmediaType = \"video\"\n\t\t\t// 视频格式处理\n\t\t\tif strings.Contains(parts[0], \"video/mp4\") {\n\t\t\t\tmimeType = \"video/mp4\"\n\t\t\t} else if strings.Contains(parts[0], \"video/webm\") {\n\t\t\t\tmimeType = \"video/webm\"\n\t\t\t} else if strings.Contains(parts[0], \"video/quicktime\") || strings.Contains(parts[0], \"video/mov\") {\n\t\t\t\t// MOV 格式，尝试作为 mp4 上传\n\t\t\t\tmimeType = \"video/mp4\"\n\t\t\t\tlogger.Debug(\"ℹ️ MOV 视频将作为 MP4 上传\")\n\t\t\t} else if strings.Contains(parts[0], \"video/avi\") || strings.Contains(parts[0], \"video/x-msvideo\") {\n\t\t\t\tmimeType = \"video/mp4\"\n\t\t\t\tlogger.Debug(\"ℹ️ AVI 视频将作为 MP4 上传\")\n\t\t\t} else {\n\t\t\t\t// 其他视频格式默认作为 mp4\n\t\t\t\tmimeType = \"video/mp4\"\n\t\t\t\tlogger.Debug(\"ℹ️ 未知视频格式 %s 将作为 MP4 上传\", parts[0])\n\t\t\t}\n\t\t} else {\n\t\t\tmediaType = \"image\"\n\t\t\t// 图片格式处理\n\t\t\tif strings.Contains(parts[0], \"image/png\") {\n\t\t\t\tmimeType = \"image/png\"\n\t\t\t} else if strings.Contains(parts[0], \"image/jpeg\") {\n\t\t\t\tmimeType = \"image/jpeg\"\n\t\t\t} else {\n\t\t\t\t// 其他图片格式需要转换为 PNG\n\t\t\t\tconverted, err := convertBase64ToPNG(base64Data)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlogger.Warn(\"⚠️ %s base64 转换失败: %v\", parts[0], err)\n\t\t\t\t\tmimeType = \"image/jpeg\" // 回退\n\t\t\t\t} else {\n\t\t\t\t\tlogger.Info(\"✅ %s base64 已转换为 PNG\", parts[0])\n\t\t\t\t\tbase64Data = converted\n\t\t\t\t\tmimeType = \"image/png\"\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn &MediaInfo{\n\t\t\tMimeType:  mimeType,\n\t\t\tData:      base64Data,\n\t\t\tIsURL:     false,\n\t\t\tMediaType: mediaType,\n\t\t}\n\t}\n\n\t// URL 媒体 - 优先尝试直接使用 URL 上传\n\tmediaType := defaultType\n\tlowerURL := strings.ToLower(urlStr)\n\tif strings.HasSuffix(lowerURL, \".mp4\") || strings.HasSuffix(lowerURL, \".webm\") ||\n\t\tstrings.HasSuffix(lowerURL, \".mov\") || strings.HasSuffix(lowerURL, \".avi\") ||\n\t\tstrings.HasSuffix(lowerURL, \".mkv\") || strings.HasSuffix(lowerURL, \".m4v\") {\n\t\tmediaType = \"video\"\n\t}\n\n\treturn &MediaInfo{\n\t\tURL:       urlStr,\n\t\tIsURL:     true,\n\t\tMediaType: mediaType,\n\t}\n}\n\nfunc downloadImage(urlStr string) (string, string, error) {\n\treturn downloadMedia(urlStr, \"image\")\n}\n\n// downloadMedia 下载媒体文件（图片或视频）\nfunc downloadMedia(urlStr, mediaType string) (string, string, error) {\n\tresp, err := utils.HTTPClient.Get(urlStr)\n\tif err != nil {\n\t\treturn \"\", \"\", err\n\t}\n\tdefer resp.Body.Close()\n\n\t// 检查上游返回的状态码\n\tif resp.StatusCode == 401 || resp.StatusCode == 403 {\n\t\treturn \"\", \"\", fmt.Errorf(\"UPSTREAM_%d: 上游返回状态码 %d 多媒体下载失败\", resp.StatusCode, resp.StatusCode)\n\t}\n\tif resp.StatusCode >= 400 {\n\t\treturn \"\", \"\", fmt.Errorf(\"UPSTREAM_%d: 上游返回状态码 %d\", resp.StatusCode, resp.StatusCode)\n\t}\n\n\tdata, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", \"\", err\n\t}\n\n\tmimeType := resp.Header.Get(\"Content-Type\")\n\n\tif mediaType == \"video\" || strings.HasPrefix(mimeType, \"video/\") {\n\t\t// 视频处理\n\t\tif mimeType == \"\" {\n\t\t\tmimeType = \"video/mp4\"\n\t\t}\n\t\t// 规范化视频 MIME 类型\n\t\tmimeType = normalizeVideoMimeType(mimeType)\n\t\treturn base64.StdEncoding.EncodeToString(data), mimeType, nil\n\t}\n\n\t// 图片处理\n\tif mimeType == \"\" {\n\t\tmimeType = \"image/jpeg\"\n\t}\n\tneedConvert := !strings.Contains(mimeType, \"jpeg\") && !strings.Contains(mimeType, \"png\")\n\tif needConvert {\n\t\tconverted, err := convertToPNG(data)\n\t\tif err != nil {\n\t\t\tlogger.Warn(\"⚠️ %s 转换失败: %v，尝试原格式\", mimeType, err)\n\t\t} else {\n\t\t\tlogger.Info(\"✅ %s 已转换为 PNG\", mimeType)\n\t\t\treturn base64.StdEncoding.EncodeToString(converted), \"image/png\", nil\n\t\t}\n\t}\n\n\treturn base64.StdEncoding.EncodeToString(data), mimeType, nil\n}\n\n// normalizeVideoMimeType 规范化视频 MIME 类型\nfunc normalizeVideoMimeType(mimeType string) string {\n\tswitch {\n\tcase strings.Contains(mimeType, \"mp4\"):\n\t\treturn \"video/mp4\"\n\tcase strings.Contains(mimeType, \"webm\"):\n\t\treturn \"video/webm\"\n\tcase strings.Contains(mimeType, \"quicktime\"), strings.Contains(mimeType, \"mov\"):\n\t\tlogger.Debug(\"ℹ️ MOV 视频将作为 MP4 上传\")\n\t\treturn \"video/mp4\"\n\tcase strings.Contains(mimeType, \"avi\"), strings.Contains(mimeType, \"x-msvideo\"):\n\t\tlogger.Debug(\"ℹ️ AVI 视频将作为 MP4 上传\")\n\t\treturn \"video/mp4\"\n\tcase strings.Contains(mimeType, \"x-matroska\"), strings.Contains(mimeType, \"mkv\"):\n\t\tlogger.Debug(\"ℹ️ MKV 视频将作为 MP4 上传\")\n\t\treturn \"video/mp4\"\n\tcase strings.Contains(mimeType, \"3gpp\"):\n\t\treturn \"video/3gpp\"\n\tdefault:\n\t\tlogger.Debug(\"ℹ️ 未知视频格式 %s 将作为 MP4 上传\", mimeType)\n\t\treturn \"video/mp4\"\n\t}\n}\n\n// convertToPNG 将图片转换为 PNG 格式\nfunc convertToPNG(data []byte) ([]byte, error) {\n\timg, _, err := image.Decode(bytes.NewReader(data))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"解码图片失败: %w\", err)\n\t}\n\n\tvar buf bytes.Buffer\n\tif err := png.Encode(&buf, img); err != nil {\n\t\treturn nil, fmt.Errorf(\"编码 PNG 失败: %w\", err)\n\t}\n\n\treturn buf.Bytes(), nil\n}\n\n// convertBase64ToPNG 将 base64 图片转换为 PNG\nfunc convertBase64ToPNG(base64Data string) (string, error) {\n\tdata, err := base64.StdEncoding.DecodeString(base64Data)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"解码 base64 失败: %w\", err)\n\t}\n\n\tconverted, err := convertToPNG(data)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn base64.StdEncoding.EncodeToString(converted), nil\n}\n\nconst maxRetries = 3\n\n// convertMessagesToPrompt 将多轮对话转换为Gemini格式的prompt\n// extractSystemPrompt 提取并返回系统提示词\nfunc extractSystemPrompt(messages []Message) string {\n\tfor _, msg := range messages {\n\t\tif msg.Role == \"system\" {\n\t\t\ttext, _ := parseMessageContent(msg)\n\t\t\treturn text\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// convertMessagesToPrompt 将多轮对话转换为带系统提示词的prompt\n// 支持OpenAI/Claude/Gemini格式的messages\nfunc convertMessagesToPrompt(messages []Message) string {\n\tvar dialogParts []string\n\tvar systemPrompt string\n\n\tfor _, msg := range messages {\n\t\ttext, _ := parseMessageContent(msg)\n\t\tif text == \"\" && msg.Role != \"assistant\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tswitch msg.Role {\n\t\tcase \"system\":\n\t\t\t// 支持多个system消息拼接\n\t\t\tif systemPrompt != \"\" {\n\t\t\t\tsystemPrompt += \"\\n\" + text\n\t\t\t} else {\n\t\t\t\tsystemPrompt = text\n\t\t\t}\n\t\tcase \"user\", \"human\": // Claude使用human\n\t\t\tdialogParts = append(dialogParts, fmt.Sprintf(\"Human: %s\", text))\n\t\tcase \"assistant\":\n\t\t\t// 检查是否有工具调用\n\t\t\tif len(msg.ToolCalls) > 0 {\n\t\t\t\tfor _, tc := range msg.ToolCalls {\n\t\t\t\t\tdialogParts = append(dialogParts, fmt.Sprintf(\"Assistant: [调用工具 %s(%s)]\", tc.Function.Name, tc.Function.Arguments))\n\t\t\t\t}\n\t\t\t} else if text != \"\" {\n\t\t\t\tdialogParts = append(dialogParts, fmt.Sprintf(\"Assistant: %s\", text))\n\t\t\t}\n\t\tcase \"tool\", \"tool_result\": // Claude使用tool_result\n\t\t\tdialogParts = append(dialogParts, fmt.Sprintf(\"Tool Result [%s]: %s\", msg.Name, text))\n\t\t}\n\t}\n\n\t// 组合最终prompt，系统提示词使用更强的格式\n\tvar result strings.Builder\n\tif systemPrompt != \"\" {\n\t\t// 使用更明确的系统提示词格式，确保生效\n\t\tresult.WriteString(\"<system>\\n\")\n\t\tresult.WriteString(systemPrompt)\n\t\tresult.WriteString(\"\\n</system>\\n\\n\")\n\t}\n\tif len(dialogParts) > 0 {\n\t\tresult.WriteString(strings.Join(dialogParts, \"\\n\\n\"))\n\t}\n\t// 添加Assistant前缀引导回复\n\tresult.WriteString(\"\\n\\nAssistant:\")\n\treturn result.String()\n}\n\n// ==================== Gemini API 兼容 ====================\n\n// GeminiRequest Gemini generateContent API 请求格式\ntype GeminiRequest struct {\n\tContents          []GeminiContent          `json:\"contents\"`\n\tSystemInstruction *GeminiContent           `json:\"systemInstruction,omitempty\"`\n\tGenerationConfig  map[string]interface{}   `json:\"generationConfig,omitempty\"`\n\tGeminiTools       []map[string]interface{} `json:\"tools,omitempty\"`\n}\n\ntype GeminiContent struct {\n\tRole  string       `json:\"role,omitempty\"`\n\tParts []GeminiPart `json:\"parts\"`\n}\n\ntype GeminiPart struct {\n\tText       string            `json:\"text,omitempty\"`\n\tInlineData *GeminiInlineData `json:\"inlineData,omitempty\"`\n}\n\ntype GeminiInlineData struct {\n\tMimeType string `json:\"mimeType\"`\n\tData     string `json:\"data\"`\n}\n\n// handleGeminiGenerate 处理Gemini generateContent API格式的请求\nfunc handleGeminiGenerate(c *gin.Context) {\n\taction := c.Param(\"action\")\n\tif action == \"\" {\n\t\tc.JSON(400, gin.H{\"error\": gin.H{\"code\": 400, \"message\": \"Missing model action\", \"status\": \"INVALID_ARGUMENT\"}})\n\t\treturn\n\t}\n\n\taction = strings.TrimPrefix(action, \"/\")\n\n\tvar model string\n\tvar isStream bool\n\tif idx := strings.LastIndex(action, \":\"); idx > 0 {\n\t\tmodel = action[:idx]\n\t\tactionType := action[idx+1:]\n\t\tisStream = actionType == \"streamGenerateContent\"\n\t} else {\n\t\tmodel = action\n\t}\n\n\tif model == \"\" {\n\t\tmodel = GetAvailableModels()[0]\n\t}\n\n\tvar geminiReq GeminiRequest\n\tif err := c.ShouldBindJSON(&geminiReq); err != nil {\n\t\tc.JSON(400, gin.H{\"error\": gin.H{\"code\": 400, \"message\": err.Error(), \"status\": \"INVALID_ARGUMENT\"}})\n\t\treturn\n\t}\n\n\tvar messages []Message\n\n\t// 处理systemInstruction\n\tif geminiReq.SystemInstruction != nil && len(geminiReq.SystemInstruction.Parts) > 0 {\n\t\tvar sysText string\n\t\tfor _, part := range geminiReq.SystemInstruction.Parts {\n\t\t\tif part.Text != \"\" {\n\t\t\t\tsysText += part.Text\n\t\t\t}\n\t\t}\n\t\tif sysText != \"\" {\n\t\t\tmessages = append(messages, Message{Role: \"system\", Content: sysText})\n\t\t}\n\t}\n\n\t// 处理contents\n\tfor _, content := range geminiReq.Contents {\n\t\trole := content.Role\n\t\tif role == \"model\" {\n\t\t\trole = \"assistant\"\n\t\t}\n\n\t\tvar textParts []string\n\t\tvar contentParts []interface{}\n\n\t\tfor _, part := range content.Parts {\n\t\t\tif part.Text != \"\" {\n\t\t\t\ttextParts = append(textParts, part.Text)\n\t\t\t}\n\t\t\tif part.InlineData != nil {\n\t\t\t\tcontentParts = append(contentParts, map[string]interface{}{\n\t\t\t\t\t\"type\": \"image_url\",\n\t\t\t\t\t\"image_url\": map[string]string{\n\t\t\t\t\t\t\"url\": fmt.Sprintf(\"data:%s;base64,%s\", part.InlineData.MimeType, part.InlineData.Data),\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\n\t\tif len(contentParts) > 0 {\n\t\t\tif len(textParts) > 0 {\n\t\t\t\tcontentParts = append([]interface{}{map[string]interface{}{\"type\": \"text\", \"text\": strings.Join(textParts, \"\\n\")}}, contentParts...)\n\t\t\t}\n\t\t\tmessages = append(messages, Message{Role: role, Content: contentParts})\n\t\t} else if len(textParts) > 0 {\n\t\t\tmessages = append(messages, Message{Role: role, Content: strings.Join(textParts, \"\\n\")})\n\t\t}\n\t}\n\n\tstream := isStream || c.Query(\"alt\") == \"sse\"\n\n\t// 转换Gemini工具格式\n\tvar tools []ToolDef\n\tfor _, gt := range geminiReq.GeminiTools {\n\t\tif funcDecls, ok := gt[\"functionDeclarations\"].([]interface{}); ok {\n\t\t\tfor _, fd := range funcDecls {\n\t\t\t\tif funcMap, ok := fd.(map[string]interface{}); ok {\n\t\t\t\t\tname, _ := funcMap[\"name\"].(string)\n\t\t\t\t\tdesc, _ := funcMap[\"description\"].(string)\n\t\t\t\t\tparams, _ := funcMap[\"parameters\"].(map[string]interface{})\n\t\t\t\t\ttools = append(tools, ToolDef{\n\t\t\t\t\t\tType: \"function\",\n\t\t\t\t\t\tFunction: FunctionDef{\n\t\t\t\t\t\t\tName:        name,\n\t\t\t\t\t\t\tDescription: desc,\n\t\t\t\t\t\t\tParameters:  params,\n\t\t\t\t\t\t},\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treq := ChatRequest{\n\t\tModel:    model,\n\t\tMessages: messages,\n\t\tStream:   stream,\n\t\tTools:    tools,\n\t}\n\n\tstreamChat(c, req)\n}\n\n// ==================== Claude API 兼容 ====================\n\ntype ClaudeRequest struct {\n\tModel       string    `json:\"model\"`\n\tMessages    []Message `json:\"messages\"`\n\tSystem      string    `json:\"system,omitempty\"`\n\tMaxTokens   int       `json:\"max_tokens,omitempty\"`\n\tStream      bool      `json:\"stream\"`\n\tTemperature float64   `json:\"temperature,omitempty\"`\n\tTools       []ToolDef `json:\"tools,omitempty\"`\n}\n\n// handleClaudeMessages 处理Claude Messages API格式的请求\nfunc handleClaudeMessages(c *gin.Context) {\n\tvar claudeReq ClaudeRequest\n\tif err := c.ShouldBindJSON(&claudeReq); err != nil {\n\t\tc.JSON(400, gin.H{\"type\": \"error\", \"error\": gin.H{\"type\": \"invalid_request_error\", \"message\": err.Error()}})\n\t\treturn\n\t}\n\n\treq := ChatRequest{\n\t\tModel:       claudeReq.Model,\n\t\tMessages:    claudeReq.Messages,\n\t\tStream:      claudeReq.Stream,\n\t\tTemperature: claudeReq.Temperature,\n\t\tTools:       claudeReq.Tools,\n\t}\n\n\t// 如果Claude格式有单独的system字段，插入到messages开头\n\tif claudeReq.System != \"\" {\n\t\tsystemMsg := Message{Role: \"system\", Content: claudeReq.System}\n\t\treq.Messages = append([]Message{systemMsg}, req.Messages...)\n\t}\n\n\tif req.Model == \"\" {\n\t\treq.Model = GetAvailableModels()[0]\n\t}\n\n\tstreamChat(c, req)\n}\n\n// buildToolsSpec 将OpenAI格式的工具定义转换为Gemini的toolsSpec\n// 支持混合后缀同时启用多个功能，如 -image-search 同时启用图片生成和搜索\nfunc buildToolsSpec(tools []ToolDef, isImageModel, isVideoModel, isSearchModel bool) map[string]interface{} {\n\ttoolsSpec := make(map[string]interface{})\n\n\t// 检查是否指定了任何功能后缀\n\thasAnySpec := isImageModel || isVideoModel || isSearchModel\n\n\tif !hasAnySpec {\n\t\ttoolsSpec[\"webGroundingSpec\"] = map[string]interface{}{}\n\t\ttoolsSpec[\"toolRegistry\"] = \"default_tool_registry\"\n\t\ttoolsSpec[\"imageGenerationSpec\"] = map[string]interface{}{}\n\t\ttoolsSpec[\"videoGenerationSpec\"] = map[string]interface{}{}\n\t} else {\n\t\tif isImageModel {\n\t\t\ttoolsSpec[\"imageGenerationSpec\"] = map[string]interface{}{}\n\t\t}\n\t\tif isVideoModel {\n\t\t\ttoolsSpec[\"videoGenerationSpec\"] = map[string]interface{}{}\n\t\t}\n\t\tif isSearchModel {\n\t\t\ttoolsSpec[\"webGroundingSpec\"] = map[string]interface{}{}\n\t\t}\n\t}\n\t_ = tools\n\n\treturn toolsSpec\n}\n\n// extractToolCalls 从Gemini响应中提取工具调用\nfunc extractToolCalls(dataList []map[string]interface{}) []ToolCall {\n\tvar toolCalls []ToolCall\n\n\tfor _, data := range dataList {\n\t\tstreamResp, ok := data[\"streamAssistResponse\"].(map[string]interface{})\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\tanswer, ok := streamResp[\"answer\"].(map[string]interface{})\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\treplies, ok := answer[\"replies\"].([]interface{})\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, reply := range replies {\n\t\t\treplyMap, ok := reply.(map[string]interface{})\n\t\t\tif !ok {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tgroundedContent, ok := replyMap[\"groundedContent\"].(map[string]interface{})\n\t\t\tif !ok {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tcontent, ok := groundedContent[\"content\"].(map[string]interface{})\n\t\t\tif !ok {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 检查functionCall\n\t\t\tif fc, ok := content[\"functionCall\"].(map[string]interface{}); ok {\n\t\t\t\tname, _ := fc[\"name\"].(string)\n\t\t\t\targs, _ := fc[\"args\"].(map[string]interface{})\n\t\t\t\targsBytes, _ := json.Marshal(args)\n\n\t\t\t\ttoolCalls = append(toolCalls, ToolCall{\n\t\t\t\t\tID:   \"call_\" + uuid.New().String()[:8],\n\t\t\t\t\tType: \"function\",\n\t\t\t\t\tFunction: FunctionCall{\n\t\t\t\t\t\tName:      name,\n\t\t\t\t\t\tArguments: string(argsBytes),\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t}\n\n\treturn toolCalls\n}\n\n// needsConversationContext 检查是否需要对话上下文（多轮对话）\nfunc needsConversationContext(messages []Message) bool {\n\t// 检查是否有多轮对话标志：存在assistant或tool消息\n\tfor _, msg := range messages {\n\t\tif msg.Role == \"assistant\" || msg.Role == \"tool\" || msg.Role == \"tool_result\" {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// handleFlowRequest 处理 Flow 模型请求\nfunc handleFlowRequest(c *gin.Context, req ChatRequest, chatID string, createdTime int64) {\n\tif flowHandler == nil {\n\t\tc.JSON(503, gin.H{\"error\": gin.H{\n\t\t\t\"message\": \"Flow 服务未启用，请在配置文件中启用并添加 Token\",\n\t\t\t\"type\":    \"service_unavailable\",\n\t\t}})\n\t\treturn\n\t}\n\n\t// 解析消息内容和图片\n\tvar prompt string\n\tvar imageBytes [][]byte\n\n\tfor _, msg := range req.Messages {\n\t\tif msg.Role == \"user\" || msg.Role == \"human\" {\n\t\t\ttext, images := parseMessageContent(msg)\n\t\t\tif text != \"\" {\n\t\t\t\tprompt = text\n\t\t\t}\n\t\t\t// 提取图片数据\n\t\t\tfor _, img := range images {\n\t\t\t\tif img.Data != \"\" {\n\t\t\t\t\timgData, err := base64.StdEncoding.DecodeString(img.Data)\n\t\t\t\t\tif err == nil {\n\t\t\t\t\t\timageBytes = append(imageBytes, imgData)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif prompt == \"\" {\n\t\tc.JSON(400, gin.H{\"error\": gin.H{\n\t\t\t\"message\": \"Prompt cannot be empty\",\n\t\t\t\"type\":    \"invalid_request_error\",\n\t\t}})\n\t\treturn\n\t}\n\n\tflowReq := flow.GenerationRequest{\n\t\tModel:  req.Model,\n\t\tPrompt: prompt,\n\t\tImages: imageBytes,\n\t\tStream: req.Stream,\n\t}\n\n\tif req.Stream {\n\t\t// 流式响应\n\t\tc.Header(\"Content-Type\", \"text/event-stream\")\n\t\tc.Header(\"Cache-Control\", \"no-cache\")\n\t\tc.Header(\"Connection\", \"keep-alive\")\n\t\tc.Header(\"X-Accel-Buffering\", \"no\")\n\t\tc.Status(200)\n\n\t\tflusher, ok := c.Writer.(http.Flusher)\n\t\tif !ok {\n\t\t\tc.JSON(500, gin.H{\"error\": \"Streaming not supported\"})\n\t\t\treturn\n\t\t}\n\n\t\tresult, _ := flowHandler.HandleGeneration(flowReq, func(chunk string) {\n\t\t\tc.Writer.WriteString(chunk)\n\t\t\tflusher.Flush()\n\t\t})\n\n\t\t// 发送 [DONE]\n\t\tc.Writer.WriteString(\"data: [DONE]\\n\\n\")\n\t\tflusher.Flush()\n\n\t\tif result != nil && !result.Success && result.Error != \"\" {\n\t\t\tlogger.Error(\"❌ [Flow] 生成失败: %s\", result.Error)\n\t\t}\n\t} else {\n\t\t// 非流式响应\n\t\tresult, err := flowHandler.HandleGeneration(flowReq, nil)\n\t\tif err != nil {\n\t\t\tc.JSON(500, gin.H{\"error\": gin.H{\n\t\t\t\t\"message\": err.Error(),\n\t\t\t\t\"type\":    \"internal_error\",\n\t\t\t}})\n\t\t\treturn\n\t\t}\n\n\t\tif !result.Success {\n\t\t\tc.JSON(500, gin.H{\"error\": gin.H{\n\t\t\t\t\"message\": result.Error,\n\t\t\t\t\"type\":    \"generation_failed\",\n\t\t\t}})\n\t\t\treturn\n\t\t}\n\n\t\t// 构建响应\n\t\tcontent := result.URL\n\t\tif result.Type == \"image\" {\n\t\t\tcontent = fmt.Sprintf(\"![Generated Image](%s)\", result.URL)\n\t\t} else if result.Type == \"video\" {\n\t\t\tcontent = fmt.Sprintf(\"<video src='%s' controls></video>\", result.URL)\n\t\t}\n\n\t\tc.JSON(200, gin.H{\n\t\t\t\"id\":      chatID,\n\t\t\t\"object\":  \"chat.completion\",\n\t\t\t\"created\": createdTime,\n\t\t\t\"model\":   req.Model,\n\t\t\t\"choices\": []gin.H{{\n\t\t\t\t\"index\": 0,\n\t\t\t\t\"message\": gin.H{\n\t\t\t\t\t\"role\":    \"assistant\",\n\t\t\t\t\t\"content\": content,\n\t\t\t\t},\n\t\t\t\t\"finish_reason\": \"stop\",\n\t\t\t}},\n\t\t})\n\t}\n}\n\nfunc streamChat(c *gin.Context, req ChatRequest) {\n\tchatID := \"chatcmpl-\" + uuid.New().String()\n\tcreatedTime := time.Now().Unix()\n\tclientIP := c.ClientIP()\n\tuserAgent := c.GetHeader(\"User-Agent\")\n\n\t// 统计变量\n\tvar statsSuccess bool\n\tvar statsInputTokens int64\n\tvar statsOutputTokens int64\n\tvar statsImages int64\n\tvar statsVideos int64\n\tstatsModel := req.Model\n\tdefer func() {\n\t\tapiStats.RecordRequestWithModel(statsModel, statsSuccess, statsInputTokens, statsOutputTokens, statsImages, statsVideos)\n\t\t// 记录IP统计（包含tokens、图片、视频）\n\t\tipStats.RecordIPRequest(clientIP, statsModel, userAgent, statsSuccess, statsInputTokens, statsOutputTokens, statsImages, statsVideos)\n\t}()\n\n\t// 入站日志\n\tlogger.Info(\"📥 [%s] 请求: model=%s \", clientIP, req.Model)\n\tif flow.IsFlowModel(req.Model) {\n\t\thandleFlowRequest(c, req, chatID, createdTime)\n\t\treturn\n\t}\n\tvar textContent string\n\tvar images []MediaInfo\n\tsystemPrompt := extractSystemPrompt(req.Messages)\n\tif needsConversationContext(req.Messages) {\n\t\t// 多轮对话：拼接所有消息（包含system）\n\t\ttextContent = convertMessagesToPrompt(req.Messages)\n\t\t// 只从最后一条用户消息提取图片\n\t\tfor i := len(req.Messages) - 1; i >= 0; i-- {\n\t\t\tif req.Messages[i].Role == \"user\" || req.Messages[i].Role == \"human\" {\n\t\t\t\t_, images = parseMessageContent(req.Messages[i])\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t} else {\n\t\tlastMsg := req.Messages[len(req.Messages)-1]\n\t\tuserText, userImages := parseMessageContent(lastMsg)\n\t\timages = userImages\n\t\tif systemPrompt != \"\" {\n\t\t\ttextContent = fmt.Sprintf(\"<system>\\n%s\\n</system>\\n\\nHuman: %s\\n\\nAssistant:\", systemPrompt, userText)\n\t\t} else {\n\t\t\ttextContent = userText\n\t\t}\n\t}\n\tvar respBody []byte\n\tvar lastErr error\n\tvar lastErrStatusCode int // 保存最后一次错误的 HTTP 状态码\n\tvar lastErrBody []byte    // 保存最后一次错误的响应体\n\tvar usedAcc *pool.Account\n\tvar usedJWT, usedOrigAuth, usedConfigID, usedSession string\n\tisLongRunning := !req.Stream && (strings.Contains(req.Model, \"video\") ||\n\t\tstrings.Contains(req.Model, \"imagen\") ||\n\t\tstrings.Contains(req.Model, \"image\"))\n\n\tvar heartbeatDone chan struct{}\n\tif isLongRunning {\n\t\theartbeatDone = make(chan struct{})\n\t\tc.Header(\"Content-Type\", \"application/json\")\n\t\tc.Header(\"Transfer-Encoding\", \"chunked\")\n\t\tc.Status(200)\n\t\twriter := c.Writer\n\t\tflusher, ok := writer.(http.Flusher)\n\t\tif ok {\n\t\t\tflusher.Flush() // 先发送头部\n\t\t}\n\t\tgo func() {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t}\n\t\t\t}()\n\t\t\tticker := time.NewTicker(15 * time.Second)\n\t\t\tdefer ticker.Stop()\n\t\t\tfor {\n\t\t\t\tselect {\n\t\t\t\tcase <-heartbeatDone:\n\t\t\t\t\treturn\n\t\t\t\tcase <-ticker.C:\n\t\t\t\t\tif _, err := writer.Write([]byte(\" \")); err != nil {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tif flusher, ok := writer.(http.Flusher); ok {\n\t\t\t\t\t\tflusher.Flush()\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}()\n\t}\n\tdefer func() {\n\t\tif heartbeatDone != nil {\n\t\t\tselect {\n\t\t\tcase <-heartbeatDone:\n\t\t\tdefault:\n\t\t\t\tclose(heartbeatDone)\n\t\t\t}\n\t\t}\n\t}()\n\n\t// 估算输入 tokens（基于文本长度）\n\tstatsInputTokens = int64(len(textContent)/4) + int64(len(images)*500) // 文本 + 图片估算\n\n\t// 流式请求：提前发送 SSE 头部，避免上游请求期间客户端等待超时\n\tvar streamWriter http.ResponseWriter\n\tvar streamFlusher http.Flusher\n\tvar streamStarted bool\n\tif req.Stream {\n\t\tc.Header(\"Content-Type\", \"text/event-stream\")\n\t\tc.Header(\"Cache-Control\", \"no-cache\")\n\t\tc.Header(\"Connection\", \"keep-alive\")\n\t\tc.Header(\"X-Accel-Buffering\", \"no\")\n\t\tstreamWriter = c.Writer\n\t\tstreamFlusher, _ = streamWriter.(http.Flusher)\n\t\tchunk := createChunk(chatID, createdTime, req.Model, map[string]interface{}{\"role\": \"assistant\"}, nil)\n\t\tfmt.Fprintf(streamWriter, \"data: %s\\n\\n\", chunk)\n\t\tstreamFlusher.Flush()\n\t\tstreamStarted = true\n\t}\n\n\tfor retry := 0; retry < maxRetries; retry++ {\n\t\tacc := pool.Pool.Next()\n\t\tif acc == nil {\n\t\t\tif streamStarted {\n\t\t\t\t// 流式请求已开始，发送 SSE 格式错误\n\t\t\t\terrChunk := createChunk(chatID, createdTime, req.Model, map[string]interface{}{\"content\": \"[错误] 没有可用账号\"}, nil)\n\t\t\t\tfmt.Fprintf(streamWriter, \"data: %s\\n\\n\", errChunk)\n\t\t\t\tfinishReason := \"stop\"\n\t\t\t\tfinalChunk := createChunk(chatID, createdTime, req.Model, nil, &finishReason)\n\t\t\t\tfmt.Fprintf(streamWriter, \"data: %s\\n\\n\", finalChunk)\n\t\t\t\tfmt.Fprintf(streamWriter, \"data: [DONE]\\n\\n\")\n\t\t\t\tstreamFlusher.Flush()\n\t\t\t} else {\n\t\t\t\tc.JSON(500, gin.H{\"error\": \"没有可用账号\"})\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t\tusedAcc = acc\n\t\tlogger.Info(\"📤 [%s] 使用账号: %s\", clientIP, acc.Data.Email)\n\n\t\tif retry > 0 {\n\t\t\tlogger.Info(\"🔄 第 %d 次重试，切换账号: %s\", retry+1, acc.Data.Email)\n\t\t}\n\n\t\tjwt, configID, err := acc.GetJWT()\n\t\tif err != nil {\n\t\t\tlogger.Error(\"❌ [%s] 获取 JWT 失败: %v\", acc.Data.Email, err)\n\t\t\tlastErr = err\n\t\t\tcontinue\n\t\t}\n\n\t\tsession, err := createSession(jwt, configID, acc.Data.Authorization)\n\t\tif err != nil {\n\t\t\tlogger.Error(\"❌ [%s] 创建 Session 失败: %v\", acc.Data.Email, err)\n\t\t\t// 401 错误标记账号需要刷新\n\t\t\tif strings.Contains(err.Error(), \"401\") || strings.Contains(err.Error(), \"UNAUTHENTICATED\") {\n\t\t\t\t//\t\tpool.Pool.MarkNeedsRefresh(acc)\n\t\t\t}\n\t\t\tlastErr = err\n\t\t\tcontinue\n\t\t}\n\n\t\t// 上传媒体文件并获取 fileIds\n\t\tvar fileIds []string\n\t\tuploadFailed := false\n\t\tfor _, media := range images {\n\t\t\tvar fileId string\n\t\t\tvar err error\n\n\t\t\tmediaTypeName := \"图片\"\n\t\t\tif media.MediaType == \"video\" {\n\t\t\t\tmediaTypeName = \"视频\"\n\t\t\t}\n\n\t\t\tif media.IsURL {\n\t\t\t\t// 优先尝试 URL 直接上传\n\t\t\t\tfileId, err = uploadContextFileByURL(jwt, configID, session, media.URL, acc.Data.Authorization)\n\t\t\t\tif err != nil {\n\t\t\t\t\t// URL 上传失败，回退到下载后上传\n\t\t\t\t\tmediaData, mimeType, dlErr := downloadMedia(media.URL, media.MediaType)\n\t\t\t\t\tif dlErr != nil {\n\t\t\t\t\t\tlogger.Warn(\"⚠️ [%s] %s下载失败: %v\", acc.Data.Email, mediaTypeName, dlErr)\n\t\t\t\t\t\tif strings.Contains(dlErr.Error(), \"UPSTREAM_401\") || strings.Contains(dlErr.Error(), \"UPSTREAM_403\") {\n\t\t\t\t\t\t\tc.JSON(500, gin.H{\"error\": gin.H{\n\t\t\t\t\t\t\t\t\"message\": dlErr.Error(),\n\t\t\t\t\t\t\t\t\"type\":    \"upstream_error\",\n\t\t\t\t\t\t\t\t\"code\":    \"media_download_failed\",\n\t\t\t\t\t\t\t}})\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t\tuploadFailed = true\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t\tfileId, err = uploadContextFile(jwt, configID, session, mimeType, mediaData, acc.Data.Authorization)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tfileId, err = uploadContextFile(jwt, configID, session, media.MimeType, media.Data, acc.Data.Authorization)\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\tlogger.Warn(\"⚠️ [%s] %s上传失败: %v\", acc.Data.Email, mediaTypeName, err)\n\t\t\t\tuploadFailed = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tfileIds = append(fileIds, fileId)\n\t\t}\n\t\tif uploadFailed {\n\t\t\tlastErr = fmt.Errorf(\"媒体上传失败\")\n\t\t\tcontinue\n\t\t}\n\t\t// 构建 query parts（只包含文本）\n\t\tqueryParts := []map[string]interface{}{}\n\t\tif textContent != \"\" {\n\t\t\tqueryParts = append(queryParts, map[string]interface{}{\"text\": textContent})\n\t\t}\n\t\t// 确保 queryParts 不为空，避免 Google 返回空响应\n\t\tif len(queryParts) == 0 {\n\t\t\tqueryParts = append(queryParts, map[string]interface{}{\"text\": \" \"})\n\t\t}\n\t\tisImageModel := strings.Contains(req.Model, \"-image\")\n\t\tisVideoModel := strings.Contains(req.Model, \"-video\")\n\t\tisSearchModel := strings.Contains(req.Model, \"-search\")\n\t\tactualModel := req.Model\n\t\tactualModel = strings.ReplaceAll(actualModel, \"-image\", \"\")\n\t\tactualModel = strings.ReplaceAll(actualModel, \"-video\", \"\")\n\t\tactualModel = strings.ReplaceAll(actualModel, \"-search\", \"\")\n\n\t\t// 构建 toolsSpec（支持自定义工具）\n\t\ttoolsSpec := buildToolsSpec(req.Tools, isImageModel, isVideoModel, isSearchModel)\n\n\t\tbody := map[string]interface{}{\n\t\t\t\"configId\":         configID,\n\t\t\t\"additionalParams\": map[string]string{\"token\": \"-\"},\n\t\t\t\"streamAssistRequest\": map[string]interface{}{\n\t\t\t\t\"session\":              session,\n\t\t\t\t\"query\":                map[string]interface{}{\"parts\": queryParts},\n\t\t\t\t\"filter\":               \"\",\n\t\t\t\t\"fileIds\":              fileIds,\n\t\t\t\t\"answerGenerationMode\": \"NORMAL\",\n\t\t\t\t\"toolsSpec\":            toolsSpec,\n\t\t\t\t\"languageCode\":         \"zh-CN\",\n\t\t\t\t\"userMetadata\":         map[string]string{\"timeZone\": \"Asia/Shanghai\"},\n\t\t\t\t\"assistSkippingMode\":   \"REQUEST_ASSIST\",\n\t\t\t},\n\t\t}\n\n\t\t// 设置模型 ID（去掉 -image 后缀）\n\t\tif targetModelID, ok := modelMapping[actualModel]; ok && targetModelID != \"\" {\n\t\t\tbody[\"streamAssistRequest\"].(map[string]interface{})[\"assistGenerationConfig\"] = map[string]interface{}{\n\t\t\t\t\"modelId\": targetModelID,\n\t\t\t}\n\t\t}\n\n\t\tbodyBytes, _ := json.Marshal(body)\n\t\thttpReq, _ := http.NewRequest(\"POST\", \"https://biz-discoveryengine.googleapis.com/v1alpha/locations/global/widgetStreamAssist\", bytes.NewReader(bodyBytes))\n\n\t\tfor k, v := range getCommonHeaders(jwt, acc.Data.Authorization) {\n\t\t\thttpReq.Header.Set(k, v)\n\t\t}\n\n\t\tresp, err := utils.HTTPClient.Do(httpReq)\n\t\tif err != nil {\n\t\t\tlogger.Error(\"❌ [%s] 请求失败: %v\", acc.Data.Email, err)\n\t\t\tlastErr = err\n\t\t\tcontinue\n\t\t}\n\n\t\tif resp.StatusCode != 200 {\n\t\t\tbody, _ := utils.ReadResponseBody(resp)\n\t\t\tresp.Body.Close()\n\t\t\tlogger.Error(\"❌ [%s] Google 报错: %d %s (重试 %d/%d)\", acc.Data.Email, resp.StatusCode, string(body), retry+1, maxRetries)\n\t\t\tlastErr = fmt.Errorf(\"HTTP %d: %s\", resp.StatusCode, string(body))\n\t\t\tlastErrStatusCode = resp.StatusCode\n\t\t\tlastErrBody = body\n\t\t\t// 401/403 无权限，标记需要刷新\n\t\t\tif resp.StatusCode == 401 || resp.StatusCode == 403 {\n\t\t\t\tlogger.Warn(\"⚠️ [%s] %d 无权限，标记需要刷新\", acc.Data.Email, resp.StatusCode)\n\t\t\t\tpool.Pool.MarkNeedsRefresh(acc)\n\t\t\t}\n\t\t\t// 429 限流，延长使用冷却时间（3倍冷却）\n\t\t\tif resp.StatusCode == 429 {\n\t\t\t\tcooldownTime := pool.UseCooldown * 3\n\t\t\t\tacc.Mu.Lock()\n\t\t\t\tacc.LastUsed = time.Now().Add(cooldownTime)\n\t\t\t\tacc.Mu.Unlock()\n\t\t\t\tlogger.Info(\"⏳ [%s] 429 限流，账号进入延长冷却 %v\", acc.Data.Email, cooldownTime)\n\t\t\t\tpool.Pool.MarkUsed(acc, false)\n\t\t\t\ttime.Sleep(1 * time.Second) // 短暂等待后切换账号\n\t\t\t\tretry--                     // 不计入重试次数\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif resp.StatusCode == 400 {\n\t\t\t\tlogger.Warn(\"⚠️ [%s] 400 错误，换账号重试\", acc.Data.Email)\n\t\t\t\tpool.Pool.MarkUsed(acc, false)\n\t\t\t\ttime.Sleep(500 * time.Millisecond)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tpool.Pool.MarkUsed(acc, false) // 标记失败\n\t\t\tcontinue\n\t\t}\n\t\t// 成功，读取响应\n\t\trespBody, _ = utils.ReadResponseBody(resp)\n\t\tresp.Body.Close()\n\n\t\t// Debug 模式输出上游响应\n\t\tif logger.IsDebug() {\n\t\t\trespSnippet := string(respBody)\n\t\t\tif len(respSnippet) > 2000 {\n\t\t\t\trespSnippet = respSnippet[:2000] + \"...\"\n\t\t\t}\n\t\t\tlogger.Debug(\"[%s] 上游响应: %s\", acc.Data.Email, respSnippet)\n\t\t}\n\n\t\t// 快速检查是否是认证错误响应\n\t\tif bytes.Contains(respBody, []byte(\"uToken\")) && !bytes.Contains(respBody, []byte(\"streamAssistResponse\")) {\n\t\t\tlogger.Warn(\"[%s] 收到认证响应，标记需要刷新\", acc.Data.Email)\n\t\t\tpool.Pool.MarkNeedsRefresh(acc)\n\t\t\tlastErr = fmt.Errorf(\"认证失败，需要刷新账号\")\n\t\t\tcontinue\n\t\t}\n\n\t\t// 检查是否有实际内容（非空返回）\n\t\thasText := bytes.Contains(respBody, []byte(`\"text\"`))\n\t\thasFile := bytes.Contains(respBody, []byte(`\"file\"`))\n\t\thasInlineData := bytes.Contains(respBody, []byte(`\"inlineData\"`))\n\t\thasThought := bytes.Contains(respBody, []byte(`\"thought\"`))\n\t\thasFunctionCall := bytes.Contains(respBody, []byte(`\"functionCall\"`))\n\t\thasError := bytes.Contains(respBody, []byte(`\"error\"`)) || bytes.Contains(respBody, []byte(`\"errorMessage\"`))\n\t\thasContent := hasText || hasFile || hasInlineData || hasFunctionCall\n\n\t\t// 检测是否有服务端错误信息\n\t\tif hasError && !hasContent {\n\t\t\tlogger.Warn(\"[%s] 响应包含错误信息，重试 (%d/%d)\", acc.Data.Email, retry+1, maxRetries)\n\t\t\t// 简单解析错误类型\n\t\t\tif bytes.Contains(respBody, []byte(\"RESOURCE_EXHAUSTED\")) || bytes.Contains(respBody, []byte(\"quota\")) {\n\t\t\t\tlogger.Info(\"⏳ [%s] 检测到配额耗尽，标记冷却\", acc.Data.Email)\n\t\t\t\tacc.SetCooldownMultiplier(5) // 5倍冷却\n\t\t\t\tpool.Pool.MarkUsed(acc, false)\n\t\t\t}\n\t\t\tlastErr = fmt.Errorf(\"上游返回错误响应\")\n\t\t\tcontinue\n\t\t}\n\n\t\t// 响应完全为空或只有思考内容\n\t\tif !hasContent {\n\t\t\tif hasThought {\n\t\t\t\tlogger.Warn(\"[%s] 响应只有思考内容，无实际输出，换号重试 (%d/%d)\", acc.Data.Email, retry+1, maxRetries)\n\t\t\t\tlastErr = fmt.Errorf(\"空返回，只有思考内容\")\n\t\t\t\t// 思考中的账号不标记失败，可能只是请求太慢\n\t\t\t\ttime.Sleep(500 * time.Millisecond)\n\t\t\t} else {\n\t\t\t\tlogger.Warn(\"[%s] 响应无有效内容 (text/file/inlineData/functionCall)，换号重试 (%d/%d)\", acc.Data.Email, retry+1, maxRetries)\n\t\t\t\tlastErr = fmt.Errorf(\"空返回，无有效内容\")\n\t\t\t\tpool.Pool.MarkUsed(acc, false)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tusedJWT = jwt\n\t\tusedOrigAuth = acc.Data.Authorization\n\t\tusedConfigID = configID\n\t\tusedSession = session // 保存创建的 session 作为回退\n\t\tusedAcc = acc\n\t\tlastErr = nil\n\t\tpool.Pool.MarkUsed(acc, true) // 标记成功\n\t\tbreak\n\t}\n\n\tif lastErr != nil {\n\t\tlogger.Error(\"❌ 所有重试均失败: %v\", lastErr)\n\t\tif streamStarted {\n\t\t\t// 流式请求已开始，发送 SSE 格式错误\n\t\t\terrMsg := fmt.Sprintf(\"[错误] %v\", lastErr)\n\t\t\terrChunk := createChunk(chatID, createdTime, req.Model, map[string]interface{}{\"content\": errMsg}, nil)\n\t\t\tfmt.Fprintf(streamWriter, \"data: %s\\n\\n\", errChunk)\n\t\t\tfinishReason := \"stop\"\n\t\t\tfinalChunk := createChunk(chatID, createdTime, req.Model, nil, &finishReason)\n\t\t\tfmt.Fprintf(streamWriter, \"data: %s\\n\\n\", finalChunk)\n\t\t\tfmt.Fprintf(streamWriter, \"data: [DONE]\\n\\n\")\n\t\t\tstreamFlusher.Flush()\n\t\t} else if lastErrStatusCode > 0 && len(lastErrBody) > 0 {\n\t\t\t// 如果有 HTTP 错误响应体，原样透传\n\t\t\tc.Data(lastErrStatusCode, \"application/json\", lastErrBody)\n\t\t} else {\n\t\t\tc.JSON(500, gin.H{\"error\": lastErr.Error()})\n\t\t}\n\t\treturn\n\t}\n\n\t_ = usedAcc\n\n\t// 检查空响应\n\tif len(respBody) == 0 {\n\t\tlogger.Error(\"❌ 响应为空\")\n\t\tif streamStarted {\n\t\t\terrChunk := createChunk(chatID, createdTime, req.Model, map[string]interface{}{\"content\": \"[错误] 上游返回空响应\"}, nil)\n\t\t\tfmt.Fprintf(streamWriter, \"data: %s\\n\\n\", errChunk)\n\t\t\tfinishReason := \"stop\"\n\t\t\tfinalChunk := createChunk(chatID, createdTime, req.Model, nil, &finishReason)\n\t\t\tfmt.Fprintf(streamWriter, \"data: %s\\n\\n\", finalChunk)\n\t\t\tfmt.Fprintf(streamWriter, \"data: [DONE]\\n\\n\")\n\t\t\tstreamFlusher.Flush()\n\t\t} else {\n\t\t\tc.JSON(500, gin.H{\"error\": \"Empty response from Google\"})\n\t\t}\n\t\treturn\n\t}\n\n\t// 解析响应：支持多种格式\n\tvar dataList []map[string]interface{}\n\tvar parseErr error\n\n\t// 1. 尝试标准 JSON 数组\n\tif parseErr = json.Unmarshal(respBody, &dataList); parseErr != nil {\n\t\tlogger.Warn(\"⚠️ JSON 数组解析失败: %v, 响应前100字符: %s\", parseErr, string(respBody[:min(100, len(respBody))]))\n\n\t\t// 2. 尝试修复不完整的 JSON 数组\n\t\tdataList = utils.ParseIncompleteJSONArray(respBody)\n\t\tif dataList == nil {\n\t\t\t// 3. 尝试 NDJSON 格式\n\t\t\tlogger.Warn(\"⚠️ 尝试 NDJSON 格式...\")\n\t\t\tdataList = utils.ParseNDJSON(respBody)\n\t\t}\n\n\t\tif len(dataList) == 0 {\n\t\t\t// 输出完整响应用于调试\n\t\t\trespStr := string(respBody)\n\t\t\tif len(respStr) > 500 {\n\t\t\t\tlogger.Error(\"❌ 所有解析方式均失败, 响应长度: %d, 前500字符: %s\", len(respBody), respStr[:500])\n\t\t\t\tlogger.Error(\"❌ 后200字符: %s\", respStr[len(respStr)-200:])\n\t\t\t} else {\n\t\t\t\tlogger.Error(\"❌ 所有解析方式均失败, 响应长度: %d, 完整响应: %s\", len(respBody), respStr)\n\t\t\t}\n\t\t\tif streamStarted {\n\t\t\t\terrChunk := createChunk(chatID, createdTime, req.Model, map[string]interface{}{\"content\": \"[错误] 响应解析失败\"}, nil)\n\t\t\t\tfmt.Fprintf(streamWriter, \"data: %s\\n\\n\", errChunk)\n\t\t\t\tfinishReason := \"stop\"\n\t\t\t\tfinalChunk := createChunk(chatID, createdTime, req.Model, nil, &finishReason)\n\t\t\t\tfmt.Fprintf(streamWriter, \"data: %s\\n\\n\", finalChunk)\n\t\t\t\tfmt.Fprintf(streamWriter, \"data: [DONE]\\n\\n\")\n\t\t\t\tstreamFlusher.Flush()\n\t\t\t} else {\n\t\t\t\tc.JSON(500, gin.H{\"error\": \"JSON Parse Error\"})\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t\tlogger.Info(\"✅ 备用解析成功，共 %d 个对象\", len(dataList))\n\t}\n\n\t// 检查是否有有效响应\n\tif len(dataList) > 0 {\n\t\thasValidResponse := false\n\t\thasFileContent := false\n\t\tfor _, data := range dataList {\n\t\t\tif streamResp, ok := data[\"streamAssistResponse\"].(map[string]interface{}); ok {\n\t\t\t\thasValidResponse = true\n\t\t\t\t// 检查是否有文件内容\n\t\t\t\tif answer, ok := streamResp[\"answer\"].(map[string]interface{}); ok {\n\t\t\t\t\tif replies, ok := answer[\"replies\"].([]interface{}); ok {\n\t\t\t\t\t\tfor _, reply := range replies {\n\t\t\t\t\t\t\tif replyMap, ok := reply.(map[string]interface{}); ok {\n\t\t\t\t\t\t\t\tif gc, ok := replyMap[\"groundedContent\"].(map[string]interface{}); ok {\n\t\t\t\t\t\t\t\t\tif content, ok := gc[\"content\"].(map[string]interface{}); ok {\n\t\t\t\t\t\t\t\t\t\tif _, ok := content[\"file\"]; ok {\n\t\t\t\t\t\t\t\t\t\t\thasFileContent = true\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif !hasValidResponse {\n\t\t\tlogger.Warn(\"⚠️ 响应中没有 streamAssistResponse，响应内容: %v\", dataList[0])\n\t\t}\n\t\tlogger.Debug(\"📊 响应统计: %d 个数据块, 有效响应=%v, 包含文件=%v\", len(dataList), hasValidResponse, hasFileContent)\n\t}\n\n\t// 从响应中提取 session（用于下载图片）\n\tvar respSession string\n\tfor _, data := range dataList {\n\t\tif streamResp, ok := data[\"streamAssistResponse\"].(map[string]interface{}); ok {\n\t\t\tif sessionInfo, ok := streamResp[\"sessionInfo\"].(map[string]interface{}); ok {\n\t\t\t\tif s, ok := sessionInfo[\"session\"].(string); ok && s != \"\" {\n\t\t\t\t\trespSession = s\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// 如果响应中没有 session，使用请求时创建的 session 作为回退\n\tif respSession == \"\" {\n\t\tif usedSession != \"\" {\n\t\t\tlogger.Warn(\"⚠️ 响应中未找到 session，使用请求时创建的 session: %s\", usedSession)\n\t\t\trespSession = usedSession\n\t\t} else {\n\t\t\tlogger.Warn(\"⚠️ 响应中未找到 session 且无回退 session，图片/视频下载可能失败\")\n\t\t}\n\t} else {\n\t}\n\n\t// 待下载的文件信息\n\ttype PendingFile struct {\n\t\tFileID   string\n\t\tMimeType string\n\t}\n\n\tif req.Stream {\n\t\t// 流式响应：文本/思考实时输出，图片最后处理\n\t\t// SSE 头部和 role chunk 已在请求前发送，复用 streamWriter/streamFlusher\n\t\twriter := streamWriter\n\t\tflusher := streamFlusher\n\n\t\t// 统计输出内容长度\n\t\tvar outputLen int64\n\n\t\t// 收集待下载的文件和工具调用\n\t\tvar pendingFiles []PendingFile\n\t\thasToolCalls := false\n\t\tfor _, data := range dataList {\n\t\t\tstreamResp, ok := data[\"streamAssistResponse\"].(map[string]interface{})\n\t\t\tif !ok {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tanswer, ok := streamResp[\"answer\"].(map[string]interface{})\n\t\t\tif !ok {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treplies, ok := answer[\"replies\"].([]interface{})\n\t\t\tif !ok {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfor _, reply := range replies {\n\t\t\t\treplyMap, ok := reply.(map[string]interface{})\n\t\t\t\tif !ok {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tgroundedContent, ok := replyMap[\"groundedContent\"].(map[string]interface{})\n\t\t\t\tif !ok {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tcontent, ok := groundedContent[\"content\"].(map[string]interface{})\n\t\t\t\tif !ok {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\t// 检查是否是思考内容\n\t\t\t\tif thought, ok := content[\"thought\"].(bool); ok && thought {\n\t\t\t\t\tif t, ok := content[\"text\"].(string); ok && t != \"\" {\n\t\t\t\t\t\tchunk := createChunk(chatID, createdTime, req.Model, map[string]interface{}{\"reasoning_content\": t}, nil)\n\t\t\t\t\t\tfmt.Fprintf(writer, \"data: %s\\n\\n\", chunk)\n\t\t\t\t\t\tflusher.Flush()\n\t\t\t\t\t\toutputLen += int64(len(t))\n\t\t\t\t\t}\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\t// 输出文本（实时）\n\t\t\t\tif t, ok := content[\"text\"].(string); ok && t != \"\" {\n\t\t\t\t\tchunk := createChunk(chatID, createdTime, req.Model, map[string]interface{}{\"content\": t}, nil)\n\t\t\t\t\tfmt.Fprintf(writer, \"data: %s\\n\\n\", chunk)\n\t\t\t\t\tflusher.Flush()\n\t\t\t\t\toutputLen += int64(len(t))\n\t\t\t\t}\n\n\t\t\t\t// 处理 inlineData（直接有 base64 数据的图片）\n\t\t\t\tif inlineData, ok := content[\"inlineData\"].(map[string]interface{}); ok {\n\t\t\t\t\tmime, _ := inlineData[\"mimeType\"].(string)\n\t\t\t\t\tdata, _ := inlineData[\"data\"].(string)\n\t\t\t\t\tif mime != \"\" && data != \"\" {\n\t\t\t\t\t\timgMarkdown := formatImageAsMarkdown(mime, data)\n\t\t\t\t\t\tchunk := createChunk(chatID, createdTime, req.Model, map[string]interface{}{\"content\": imgMarkdown}, nil)\n\t\t\t\t\t\tfmt.Fprintf(writer, \"data: %s\\n\\n\", chunk)\n\t\t\t\t\t\tflusher.Flush()\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// 收集需要下载的文件（图片/视频）\n\t\t\t\tif file, ok := content[\"file\"].(map[string]interface{}); ok {\n\t\t\t\t\tfileId, _ := file[\"fileId\"].(string)\n\t\t\t\t\tmimeType, _ := file[\"mimeType\"].(string)\n\t\t\t\t\tif fileId != \"\" {\n\t\t\t\t\t\tpendingFiles = append(pendingFiles, PendingFile{FileID: fileId, MimeType: mimeType})\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif fc, ok := content[\"functionCall\"].(map[string]interface{}); ok {\n\t\t\t\t\thasToolCalls = true\n\t\t\t\t\tname, _ := fc[\"name\"].(string)\n\t\t\t\t\targs, _ := fc[\"args\"].(map[string]interface{})\n\t\t\t\t\targsBytes, _ := json.Marshal(args)\n\n\t\t\t\t\ttoolCall := ToolCall{\n\t\t\t\t\t\tID:   \"call_\" + uuid.New().String()[:8],\n\t\t\t\t\t\tType: \"function\",\n\t\t\t\t\t\tFunction: FunctionCall{\n\t\t\t\t\t\t\tName:      name,\n\t\t\t\t\t\t\tArguments: string(argsBytes),\n\t\t\t\t\t\t},\n\t\t\t\t\t}\n\t\t\t\t\tchunk := createChunk(chatID, createdTime, req.Model, map[string]interface{}{\n\t\t\t\t\t\t\"tool_calls\": []map[string]interface{}{{\n\t\t\t\t\t\t\t\"index\": 0,\n\t\t\t\t\t\t\t\"id\":    toolCall.ID,\n\t\t\t\t\t\t\t\"type\":  \"function\",\n\t\t\t\t\t\t\t\"function\": map[string]interface{}{\n\t\t\t\t\t\t\t\t\"name\":      toolCall.Function.Name,\n\t\t\t\t\t\t\t\t\"arguments\": toolCall.Function.Arguments,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}},\n\t\t\t\t\t}, nil)\n\t\t\t\t\tfmt.Fprintf(writer, \"data: %s\\n\\n\", chunk)\n\t\t\t\t\tflusher.Flush()\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif len(pendingFiles) > 0 {\n\t\t\tlogger.Info(\"📥 开始下载 %d 个文件...\", len(pendingFiles))\n\t\t\ttype downloadResult struct {\n\t\t\t\tIndex    int\n\t\t\t\tData     string\n\t\t\t\tMimeType string\n\t\t\t\tErr      error\n\t\t\t}\n\t\t\tresults := make(chan downloadResult, len(pendingFiles))\n\t\t\tvar wg sync.WaitGroup\n\t\t\tfor i, pf := range pendingFiles {\n\t\t\t\twg.Add(1)\n\t\t\t\tgo func(idx int, file PendingFile) {\n\t\t\t\t\tdefer wg.Done()\n\t\t\t\t\tdata, err := downloadGeneratedFile(usedJWT, file.FileID, respSession, usedConfigID, usedOrigAuth)\n\t\t\t\t\tresults <- downloadResult{Index: idx, Data: data, MimeType: file.MimeType, Err: err}\n\t\t\t\t}(i, pf)\n\t\t\t}\n\t\t\tgo func() {\n\t\t\t\twg.Wait()\n\t\t\t\tclose(results)\n\t\t\t}()\n\t\t\tdownloaded := make([]downloadResult, len(pendingFiles))\n\t\t\tfor r := range results {\n\t\t\t\tdownloaded[r.Index] = r\n\t\t\t}\n\n\t\t\t// 按顺序输出\n\t\t\tsuccessCount := 0\n\t\t\tvar lastErr error\n\t\t\tneedsRetry := false\n\t\t\tfor i, r := range downloaded {\n\t\t\t\tif r.Err != nil {\n\t\t\t\t\tlogger.Error(\"❌ 下载文件[%d]失败: %v\", i, r.Err)\n\t\t\t\t\tlastErr = r.Err\n\t\t\t\t\t// 检测是否需要换号重试\n\t\t\t\t\tif errors.Is(r.Err, ErrDownloadNeedsRetry) {\n\t\t\t\t\t\tneedsRetry = true\n\t\t\t\t\t}\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\timgMarkdown := formatImageAsMarkdown(r.MimeType, r.Data)\n\t\t\t\tchunk := createChunk(chatID, createdTime, req.Model, map[string]interface{}{\"content\": imgMarkdown}, nil)\n\t\t\t\tfmt.Fprintf(writer, \"data: %s\\n\\n\", chunk)\n\t\t\t\tflusher.Flush()\n\t\t\t\tsuccessCount++\n\t\t\t}\n\n\t\t\t// 如果所有文件都下载失败\n\t\t\tif successCount == 0 && lastErr != nil {\n\t\t\t\tvar errMsg string\n\t\t\t\tif needsRetry {\n\t\t\t\t\t// 401/403 认证失败，提示用户重试（下次会使用新账号）\n\t\t\t\t\terrMsg = \"[提示] 文件下载认证失败，请重新发送请求（系统将自动切换账号）\"\n\t\t\t\t\tpool.Pool.MarkNeedsRefresh(usedAcc) // 标记当前账号需要刷新\n\t\t\t\t} else {\n\t\t\t\t\terrMsg = fmt.Sprintf(\"生成的文件下载失败: %v\", lastErr)\n\t\t\t\t}\n\t\t\t\tchunk := createChunk(chatID, createdTime, req.Model, map[string]interface{}{\"content\": errMsg}, nil)\n\t\t\t\tfmt.Fprintf(writer, \"data: %s\\n\\n\", chunk)\n\t\t\t\tflusher.Flush()\n\t\t\t}\n\t\t}\n\n\t\t// 发送结束\n\t\tfinishReason := \"stop\"\n\t\tif hasToolCalls {\n\t\t\tfinishReason = \"tool_calls\"\n\t\t}\n\t\tfinalChunk := createChunk(chatID, createdTime, req.Model, nil, &finishReason)\n\t\tfmt.Fprintf(writer, \"data: %s\\n\\n\", finalChunk)\n\t\tfmt.Fprintf(writer, \"data: [DONE]\\n\\n\")\n\t\tflusher.Flush()\n\n\t\t// 更新统计（区分图片和视频）\n\t\tstatsSuccess = true\n\t\tstatsOutputTokens = outputLen / 4 // 估算输出 tokens\n\t\tfor _, pf := range pendingFiles {\n\t\t\tif strings.HasPrefix(pf.MimeType, \"video/\") {\n\t\t\t\tstatsVideos++\n\t\t\t} else {\n\t\t\t\tstatsImages++\n\t\t\t}\n\t\t}\n\t} else {\n\t\t// 非流式响应\n\t\tvar fullContent strings.Builder\n\t\tvar fullReasoning strings.Builder\n\t\treplyCount := 0\n\t\tvar fileCount int64\n\t\tvar videoCount int64\n\n\t\tfor _, data := range dataList {\n\t\t\tstreamResp, ok := data[\"streamAssistResponse\"].(map[string]interface{})\n\t\t\tif !ok {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tanswer, ok := streamResp[\"answer\"].(map[string]interface{})\n\t\t\tif !ok {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treplies, ok := answer[\"replies\"].([]interface{})\n\t\t\tif !ok {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfor _, reply := range replies {\n\t\t\t\treplyMap, ok := reply.(map[string]interface{})\n\t\t\t\tif !ok {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\treplyCount++\n\t\t\t\tif gc, ok := replyMap[\"groundedContent\"].(map[string]interface{}); ok {\n\t\t\t\t\tif content, ok := gc[\"content\"].(map[string]interface{}); ok {\n\t\t\t\t\t\tif file, ok := content[\"file\"].(map[string]interface{}); ok {\n\t\t\t\t\t\t\tif mimeType, _ := file[\"mimeType\"].(string); strings.HasPrefix(mimeType, \"video/\") {\n\t\t\t\t\t\t\t\tvideoCount++\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tfileCount++\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\ttext, imageData, imageMime, reasoning, dlErr := extractContentFromReply(replyMap, usedJWT, respSession, usedConfigID, usedOrigAuth)\n\t\t\t\tif reasoning != \"\" {\n\t\t\t\t\tfullReasoning.WriteString(reasoning)\n\t\t\t\t}\n\t\t\t\tif text != \"\" {\n\t\t\t\t\tfullContent.WriteString(text)\n\t\t\t\t}\n\t\t\t\tif imageData != \"\" && imageMime != \"\" {\n\t\t\t\t\tfullContent.WriteString(formatImageAsMarkdown(imageMime, imageData))\n\t\t\t\t}\n\t\t\t\t// 检测下载是否需要重试（401/403）\n\t\t\t\tif dlErr != nil && errors.Is(dlErr, ErrDownloadNeedsRetry) {\n\t\t\t\t\tpool.Pool.MarkNeedsRefresh(usedAcc)\n\t\t\t\t\tfullContent.WriteString(\"\\n\\n[提示] 文件下载认证失败，请重新发送请求（系统将自动切换账号）\")\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\ttoolCalls := extractToolCalls(dataList)\n\t\t// 调试日志\n\t\tlogger.Debug(\"📊 非流式响应统计: %d 个 reply, 图片=%d, 视频=%d, content长度=%d, reasoning长度=%d, 工具调用=%d\",\n\t\t\treplyCount, fileCount, videoCount, fullContent.Len(), fullReasoning.Len(), len(toolCalls))\n\n\t\t// 构建响应消息\n\t\tmessage := gin.H{\n\t\t\t\"role\":    \"assistant\",\n\t\t\t\"content\": fullContent.String(),\n\t\t}\n\t\tif fullReasoning.Len() > 0 {\n\t\t\tmessage[\"reasoning_content\"] = fullReasoning.String()\n\t\t}\n\t\tfinishReason := \"stop\"\n\t\tif len(toolCalls) > 0 {\n\t\t\tmessage[\"tool_calls\"] = toolCalls\n\t\t\tmessage[\"content\"] = nil\n\t\t\tfinishReason = \"tool_calls\"\n\t\t}\n\n\t\t// 构建最终响应（完全符合OpenAI格式）\n\t\tresponse := gin.H{\n\t\t\t\"id\":                 chatID,\n\t\t\t\"object\":             \"chat.completion\",\n\t\t\t\"created\":            createdTime,\n\t\t\t\"model\":              req.Model,\n\t\t\t\"system_fingerprint\": \"fp_gemini_\" + req.Model,\n\t\t\t\"choices\": []gin.H{{\n\t\t\t\t\"index\":         0,\n\t\t\t\t\"message\":       message,\n\t\t\t\t\"logprobs\":      nil,\n\t\t\t\t\"finish_reason\": finishReason,\n\t\t\t}},\n\t\t\t\"usage\": gin.H{\n\t\t\t\t\"prompt_tokens\":     0,\n\t\t\t\t\"completion_tokens\": 0,\n\t\t\t\t\"total_tokens\":      0,\n\t\t\t},\n\t\t}\n\t\tif isLongRunning && heartbeatDone != nil {\n\t\t\tclose(heartbeatDone) // 停止心跳\n\t\t\tjsonBytes, _ := json.Marshal(response)\n\t\t\tc.Writer.Write(jsonBytes)\n\t\t} else {\n\t\t\tc.JSON(200, response)\n\t\t}\n\n\t\t// 更新统计\n\t\tstatsSuccess = true\n\t\tstatsOutputTokens = int64(fullContent.Len() / 4) // 粗略估算输出 tokens\n\t\tstatsImages = fileCount\n\t\tstatsVideos = videoCount\n\t}\n}\nfunc apiKeyAuth() gin.HandlerFunc {\n\treturn func(c *gin.Context) {\n\t\t// 使用线程安全的方式获取 API Keys\n\t\tapiKeys := GetAPIKeys()\n\t\tif len(apiKeys) == 0 {\n\t\t\tc.Next()\n\t\t\treturn\n\t\t}\n\t\tauthHeader := c.GetHeader(\"Authorization\")\n\t\tapiKey := \"\"\n\n\t\tif strings.HasPrefix(authHeader, \"Bearer \") {\n\t\t\tapiKey = strings.TrimPrefix(authHeader, \"Bearer \")\n\t\t} else {\n\t\t\tapiKey = c.GetHeader(\"X-API-Key\")\n\t\t}\n\n\t\tif apiKey == \"\" {\n\t\t\tc.JSON(401, gin.H{\"error\": \"Missing API key\"})\n\t\t\tc.Abort()\n\t\t\treturn\n\t\t}\n\n\t\t// 验证 API Key\n\t\tvalid := false\n\t\tfor _, key := range apiKeys {\n\t\t\tif key == apiKey {\n\t\t\t\tvalid = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif !valid {\n\t\t\tc.JSON(401, gin.H{\"error\": \"Invalid API key\"})\n\t\t\tc.Abort()\n\t\t\treturn\n\t\t}\n\n\t\tc.Next()\n\t}\n}\n\n// runBrowserRefreshMode 有头浏览器刷新模式\nfunc runBrowserRefreshMode(email string) {\n\tloadAppConfig()\n\tutils.InitHTTPClient(Proxy)\n\n\t// 强制有头模式\n\tpool.BrowserRefreshHeadless = false\n\tlogger.Info(\"🌐 有头浏览器刷新模式\")\n\n\tif err := pool.Pool.Load(DataDir); err != nil {\n\t\tlog.Fatalf(\"❌ 加载账号失败: %v\", err)\n\t}\n\n\tif pool.Pool.TotalCount() == 0 {\n\t\tlog.Fatal(\"❌ 没有可用账号\")\n\t}\n\n\t// 查找目标账号\n\tvar targetAcc *pool.Account\n\tpool.Pool.WithLock(func(ready, pending []*pool.Account) {\n\t\tif email != \"\" {\n\t\t\t// 指定邮箱\n\t\t\tfor _, acc := range ready {\n\t\t\t\tif acc.Data.Email == email {\n\t\t\t\t\ttargetAcc = acc\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t\tif targetAcc == nil {\n\t\t\t\tfor _, acc := range pending {\n\t\t\t\t\tif acc.Data.Email == email {\n\t\t\t\t\t\ttargetAcc = acc\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t// 使用第一个账号\n\t\t\tif len(ready) > 0 {\n\t\t\t\ttargetAcc = ready[0]\n\t\t\t} else if len(pending) > 0 {\n\t\t\t\ttargetAcc = pending[0]\n\t\t\t}\n\t\t}\n\t})\n\n\tif targetAcc == nil {\n\t\tif email != \"\" {\n\t\t\tlog.Fatalf(\"❌ 找不到账号: %s\", email)\n\t\t}\n\t\tlog.Fatal(\"❌ 没有可用账号\")\n\t}\n\tresult := register.RefreshCookieWithBrowser(targetAcc, false, Proxy)\n\n\tif result.Success {\n\n\t\tif len(result.NewCookies) > 0 {\n\t\t}\n\t\tif len(result.ResponseHeaders) > 0 {\n\t\t}\n\n\t\t// 更新账号数据\n\t\ttargetAcc.Mu.Lock()\n\t\ttargetAcc.Data.Cookies = result.SecureCookies\n\t\tif result.Authorization != \"\" {\n\t\t\ttargetAcc.Data.Authorization = result.Authorization\n\t\t}\n\t\tif result.ConfigID != \"\" {\n\t\t\ttargetAcc.ConfigID = result.ConfigID\n\t\t\ttargetAcc.Data.ConfigID = result.ConfigID\n\t\t}\n\t\tif result.CSESIDX != \"\" {\n\t\t\ttargetAcc.CSESIDX = result.CSESIDX\n\t\t\ttargetAcc.Data.CSESIDX = result.CSESIDX\n\t\t}\n\t\t// 保存响应头\n\t\tif len(result.ResponseHeaders) > 0 {\n\t\t\ttargetAcc.Data.ResponseHeaders = result.ResponseHeaders\n\t\t}\n\t\ttargetAcc.Mu.Unlock()\n\n\t\t// 保存到文件\n\t\tif err := targetAcc.SaveToFile(); err != nil {\n\t\t\tlogger.Warn(\"⚠️ 保存失败: %v\", err)\n\t\t} else {\n\t\t\tlogger.Info(\"💾 已保存到: %s\", targetAcc.FilePath)\n\t\t}\n\t} else {\n\t\tlogger.Error(\"❌ 刷新失败: %v\", result.Error)\n\t}\n}\n\nvar AutoSubscribeEnabled bool\n\nfunc init() {\n\t// 设置环境变量禁用 quic-go 的警告\n\tos.Setenv(\"QUIC_GO_DISABLE_RECEIVE_BUFFER_WARNING\", \"true\")\n\tfilterStdout()\n}\nfunc filterStdout() {\n\t// 创建管道\n\tr, w, err := os.Pipe()\n\tif err != nil {\n\t\treturn\n\t}\n\torigStdout := os.Stdout\n\tos.Stdout = w\n\tgo func() {\n\t\tbuf := make([]byte, 4096)\n\t\tfor {\n\t\t\tn, err := r.Read(buf)\n\t\t\tif err != nil {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tline := string(buf[:n])\n\t\t\t// 过滤特定日志\n\t\t\tif strings.Contains(line, \"REALITY localAddr:\") ||\n\t\t\t\tstrings.Contains(line, \"DialTLSContext\") ||\n\t\t\t\tstrings.Contains(line, \"sys_conn.go\") ||\n\t\t\t\tstrings.Contains(line, \"failed to sufficiently increase receive buffer size\") {\n\t\t\t\tcontinue // 丢弃\n\t\t\t}\n\t\t\torigStdout.Write(buf[:n])\n\t\t}\n\t}()\n}\n\nfunc main() {\n\tlog.SetFlags(log.Ltime | log.Lshortfile)\n\n\tvar refreshEmail string\n\tvar refreshMode bool\n\n\t// 解析命令行参数\n\tfor i, arg := range os.Args[1:] {\n\t\tswitch arg {\n\t\tcase \"--debug\", \"-d\":\n\t\t\tregister.RegisterDebug = true\n\t\t\tlogger.Info(\"🔧 调试模式已启用，将保存截图到 data/screenshots/\")\n\t\tcase \"--once\":\n\t\t\tregister.RegisterOnce = true\n\t\t\tlogger.Info(\"🔧 单次运行模式\")\n\t\tcase \"--auto\":\n\t\t\tAutoSubscribeEnabled = true\n\t\tcase \"--refresh\":\n\t\t\trefreshMode = true\n\t\t\t// 检查下一个参数是否是邮箱\n\t\t\tif i+2 < len(os.Args) && !strings.HasPrefix(os.Args[i+2], \"-\") {\n\t\t\t\trefreshEmail = os.Args[i+2]\n\t\t\t}\n\t\tcase \"--help\", \"-h\":\n\t\t\tfmt.Println(`用法: ./business2api [选项]\n\n选项:\n  --debug, -d           调试模式，保存注册过程截图\n  --auto                自动订阅模式，每小时注册获取代理\n  --refresh [email]     有头浏览器刷新账号（不指定email则使用第一个账号）\n  --help, -h            显示帮助`)\n\t\t\tos.Exit(0)\n\t\t}\n\t}\n\n\t// 刷新模式：直接执行浏览器刷新后退出\n\tif refreshMode {\n\t\trunBrowserRefreshMode(refreshEmail)\n\t\treturn\n\t}\n\n\tloadAppConfig()\n\tutils.InitHTTPClient(Proxy)\n\tif appConfig.PoolServer.Enable {\n\t\tswitch appConfig.PoolServer.Mode {\n\t\tcase \"client\":\n\t\t\trunAsClient()\n\t\t\treturn\n\t\tcase \"server\":\n\t\t\trunAsServer()\n\t\t\treturn\n\t\t}\n\t}\n\n\t// 本地模式\n\trunLocalMode()\n}\nfunc runAsClient() {\n\tlogger.Info(\"🔌 启动客户端模式...\")\n\n\t// 代理实例池由异步健康检查完成后初始化\n\t// 设置代理就绪检查回调\n\tpool.IsProxyReady = func() bool {\n\t\treturn proxy.Manager.IsReady()\n\t}\n\tpool.WaitProxyReady = func(timeout time.Duration) bool {\n\t\tlogger.Info(\"⏳ 等待代理就绪...\")\n\t\tresult := proxy.Manager.WaitReady(timeout)\n\t\tif result {\n\t\t\tlogger.Info(\"✅ 代理已就绪\")\n\t\t} else {\n\t\t\tlogger.Warn(\"⚠️ 代理等待超时\")\n\t\t}\n\t\treturn result\n\t}\n\n\tpool.RunBrowserRegister = func(headless bool, proxyURL string, id int) *pool.BrowserRegisterResult {\n\t\tresult := register.RunBrowserRegister(headless, proxyURL, id)\n\t\treturn &pool.BrowserRegisterResult{\n\t\t\tSuccess:       result.Success,\n\t\t\tEmail:         result.Email,\n\t\t\tFullName:      result.FullName,\n\t\t\tSecureCookies: result.Cookies,\n\t\t\tAuthorization: result.Authorization,\n\t\t\tConfigID:      result.ConfigID,\n\t\t\tCSESIDX:       result.CSESIDX,\n\t\t\tError:         result.Error,\n\t\t}\n\t}\n\tpool.RefreshCookieWithBrowser = func(acc *pool.Account, headless bool, proxyURL string) *pool.BrowserRefreshResult {\n\t\tresult := register.RefreshCookieWithBrowser(acc, headless, proxyURL)\n\t\treturn &pool.BrowserRefreshResult{\n\t\t\tSuccess:         result.Success,\n\t\t\tSecureCookies:   result.SecureCookies,\n\t\t\tConfigID:        result.ConfigID,\n\t\t\tCSESIDX:         result.CSESIDX,\n\t\t\tAuthorization:   result.Authorization,\n\t\t\tResponseHeaders: result.ResponseHeaders,\n\t\t\tError:           result.Error,\n\t\t}\n\t}\n\tpool.ClientHeadless = appConfig.Pool.RegisterHeadless\n\tpool.ClientProxy = Proxy\n\tpool.GetClientProxy = func() string {\n\t\tif proxy.Manager.HealthyCount() > 0 {\n\t\t\tproxyURL := proxy.Manager.Next()\n\t\t\tif proxyURL != \"\" {\n\t\t\t\treturn proxyURL\n\t\t\t}\n\t\t}\n\t\treturn Proxy\n\t}\n\tpool.ReleaseProxy = func(proxyURL string) {\n\t\tproxy.Manager.ReleaseByURL(proxyURL)\n\t\tlogger.Debug(\"释放代理: %s\", proxyURL)\n\t}\n\tpool.GetHealthyCount = func() int {\n\t\treturn proxy.Manager.HealthyCount()\n\t}\n\tgo func() {\n\t\tproxy.Manager.CheckAllHealth()\n\t\tif proxy.Manager.HealthyCount() > 0 {\n\t\t\tpoolSize := appConfig.Pool.RegisterThreads\n\t\t\tif poolSize <= 0 {\n\t\t\t\tpoolSize = pool.DefaultProxyCount\n\t\t\t}\n\t\t\tif poolSize > 10 {\n\t\t\t\tpoolSize = 10\n\t\t\t}\n\t\t\tproxy.Manager.SetMaxPoolSize(poolSize)\n\t\t\tproxy.Manager.InitInstancePool(poolSize)\n\t\t}\n\t}()\n\tclient := pool.NewPoolClient(appConfig.PoolServer)\n\tif err := client.Start(); err != nil {\n\t\tlog.Fatalf(\"❌ 客户端启动失败: %v\", err)\n\t}\n}\n\nvar poolServer *pool.PoolServer\n\nfunc runAsServer() {\n\tlogger.Info(\"🖥️ 启动服务器模式...\")\n\n\t// 加载账号\n\tdataDir := appConfig.PoolServer.DataDir\n\tif dataDir == \"\" {\n\t\tdataDir = DataDir\n\t}\n\tif err := pool.Pool.Load(dataDir); err != nil {\n\t\tlog.Fatalf(\"❌ 加载账号失败: %v\", err)\n\t}\n\n\t// 启动配置文件热重载监听\n\tif err := startConfigWatcher(); err != nil {\n\t\tlogger.Warn(\"⚠️ 配置热重载启动失败: %v\", err)\n\t}\n\n\tpoolServer = pool.NewPoolServer(pool.Pool, appConfig.PoolServer)\n\tpoolServer.StartBackground() // 启动后台任务分发和心跳检测\n\tpool.Pool.StartPoolManager()\n\trunAPIServer()\n}\n\n// runAPIServer 启动 API 服务\nfunc runAPIServer() {\n\tgin.SetMode(gin.ReleaseMode)\n\tr := gin.New()\n\tr.Use(gin.Recovery())\n\tsetupAPIRoutes(r)\n\tlogger.Info(\"🚀 API 服务启动于 %s，账号: ready=%d, pending=%d\", ListenAddr, pool.Pool.ReadyCount(), pool.Pool.PendingCount())\n\tif err := r.Run(ListenAddr); err != nil {\n\t\tlog.Fatalf(\"❌ API 服务启动失败: %v\", err)\n\t}\n}\n\nfunc setupAPIRoutes(r *gin.Engine) {\n\t// 请求日志中间件\n\tr.Use(func(c *gin.Context) {\n\t\tstart := time.Now()\n\t\tpath := c.Request.URL.Path\n\t\tmethod := c.Request.Method\n\t\tclientIP := c.ClientIP()\n\n\t\tc.Next()\n\n\t\tlatency := time.Since(start)\n\t\tstatusCode := c.Writer.Status()\n\n\t\tif statusCode >= 400 {\n\t\t\tlogger.Error(\"❌ %s %s %s %d %v\", clientIP, method, path, statusCode, latency)\n\t\t} else {\n\t\t\tlogger.Info(\"✅ %s %s %s %d %v\", clientIP, method, path, statusCode, latency)\n\t\t}\n\t})\n\n\tr.GET(\"/\", func(c *gin.Context) {\n\t\tstats := apiStats.GetStats()\n\t\tresponse := gin.H{\n\t\t\t\"status\":  \"running\",\n\t\t\t\"service\": \"business2api\",\n\t\t\t\"version\": \"2.1.6\",\n\t\t\t\"mode\":    map[PoolMode]string{PoolModeLocal: \"local\", PoolModeServer: \"server\", PoolModeClient: \"client\"}[poolMode],\n\t\t\t// 统计数据\n\t\t\t\"uptime\":           stats[\"uptime\"],\n\t\t\t\"total_requests\":   stats[\"total_requests\"],\n\t\t\t\"success_requests\": stats[\"success_requests\"],\n\t\t\t\"failed_requests\":  stats[\"failed_requests\"],\n\t\t\t\"success_rate\":     stats[\"success_rate\"],\n\t\t\t\"input_tokens\":     stats[\"input_tokens\"],\n\t\t\t\"output_tokens\":    stats[\"output_tokens\"],\n\t\t\t\"total_tokens\":     stats[\"total_tokens\"],\n\t\t\t\"images_generated\": stats[\"images_generated\"],\n\t\t\t\"videos_generated\": stats[\"videos_generated\"],\n\t\t\t\"current_rpm\":      stats[\"current_rpm\"],\n\t\t\t\"average_rpm\":      stats[\"average_rpm\"],\n\t\t\t\"pool\": gin.H{\n\t\t\t\t\"ready\":   pool.Pool.ReadyCount(),\n\t\t\t\t\"pending\": pool.Pool.PendingCount(),\n\t\t\t\t\"total\":   pool.Pool.TotalCount(),\n\t\t\t},\n\t\t\t// Flow 状态\n\t\t\t\"flow_enabled\": flowHandler != nil,\n\t\t}\n\t\t// 添加备注信息\n\t\tif len(appConfig.Note) > 0 {\n\t\t\tresponse[\"note\"] = appConfig.Note\n\t\t}\n\t\t// 服务端模式：添加客户端信息\n\t\tif poolServer != nil {\n\t\t\tresponse[\"clients\"] = gin.H{\n\t\t\t\t\"count\":         poolServer.GetClientCount(),\n\t\t\t\t\"total_threads\": poolServer.GetTotalThreads(),\n\t\t\t\t\"list\":          poolServer.GetClientsInfo(),\n\t\t\t}\n\t\t}\n\t\tc.JSON(200, response)\n\t})\n\n\tr.GET(\"/health\", func(c *gin.Context) {\n\t\tc.JSON(200, gin.H{\n\t\t\t\"status\":  \"ok\",\n\t\t\t\"time\":    time.Now().UTC().Format(time.RFC3339),\n\t\t\t\"ready\":   pool.Pool.ReadyCount(),\n\t\t\t\"pending\": pool.Pool.PendingCount(),\n\t\t\t\"mode\":    map[PoolMode]string{PoolModeLocal: \"local\", PoolModeServer: \"server\", PoolModeClient: \"client\"}[poolMode],\n\t\t})\n\t})\n\n\t// WebSocket 端点（服务端模式下用于客户端连接）\n\tr.GET(\"/ws\", func(c *gin.Context) {\n\t\tif poolServer == nil {\n\t\t\tc.JSON(503, gin.H{\"error\": \"WebSocket 服务未启用，仅在服务端模式下可用\"})\n\t\t\treturn\n\t\t}\n\t\tpoolServer.HandleWS(c.Writer, c.Request)\n\t})\n\n\t// Pool 内部端点（客户端上传账号等，使用 X-Pool-Secret 鉴权）\n\tpoolGroup := r.Group(\"/pool\")\n\tpoolGroup.Use(func(c *gin.Context) {\n\t\tif poolServer == nil {\n\t\t\tc.JSON(503, gin.H{\"error\": \"Pool 服务未启用\"})\n\t\t\tc.Abort()\n\t\t\treturn\n\t\t}\n\t\tsecret := appConfig.PoolServer.Secret\n\t\tif secret != \"\" && c.GetHeader(\"X-Pool-Secret\") != secret {\n\t\t\tc.JSON(401, gin.H{\"error\": \"Unauthorized\"})\n\t\t\tc.Abort()\n\t\t\treturn\n\t\t}\n\t\tc.Next()\n\t})\n\tpoolGroup.POST(\"/upload-account\", func(c *gin.Context) {\n\t\tpoolServer.HandleUploadAccount(c.Writer, c.Request)\n\t})\n\n\tapiGroup := r.Group(\"/\")\n\tapiGroup.Use(apiKeyAuth())\n\n\t// Gemini 风格模型列表 /v1beta/models\n\tapiGroup.GET(\"/v1beta/models\", func(c *gin.Context) {\n\t\tvar models []gin.H\n\t\tfor _, m := range GetAvailableModels() {\n\t\t\tmodels = append(models, gin.H{\n\t\t\t\t\"name\":                       \"models/\" + m,\n\t\t\t\t\"version\":                    \"001\",\n\t\t\t\t\"displayName\":                m,\n\t\t\t\t\"description\":                \"Gemini model: \" + m,\n\t\t\t\t\"inputTokenLimit\":            1048576,\n\t\t\t\t\"outputTokenLimit\":           8192,\n\t\t\t\t\"supportedGenerationMethods\": []string{\"generateContent\", \"countTokens\"},\n\t\t\t\t\"temperature\":                1.0,\n\t\t\t\t\"topP\":                       0.95,\n\t\t\t\t\"topK\":                       64,\n\t\t\t})\n\t\t}\n\t\tc.JSON(200, gin.H{\"models\": models})\n\t})\n\n\t// OpenAI 风格模型列表\n\tapiGroup.GET(\"/v1/models\", func(c *gin.Context) {\n\t\tnow := time.Now().Unix()\n\t\tvar models []gin.H\n\t\tfor _, m := range GetAvailableModels() {\n\t\t\tmodels = append(models, gin.H{\n\t\t\t\t\"id\":         m,\n\t\t\t\t\"object\":     \"model\",\n\t\t\t\t\"created\":    now,\n\t\t\t\t\"owned_by\":   \"google\",\n\t\t\t\t\"permission\": []interface{}{},\n\t\t\t})\n\t\t}\n\t\tc.JSON(200, gin.H{\"object\": \"list\", \"data\": models})\n\t})\n\n\tapiGroup.POST(\"/v1/chat/completions\", func(c *gin.Context) {\n\t\tvar req ChatRequest\n\t\tif err := c.ShouldBindJSON(&req); err != nil {\n\t\t\tc.JSON(400, gin.H{\"error\": err.Error()})\n\t\t\treturn\n\t\t}\n\t\tif req.Model == \"\" {\n\t\t\treq.Model = GetAvailableModels()[0]\n\t\t}\n\t\tstreamChat(c, req)\n\t})\n\n\tapiGroup.POST(\"/v1/messages\", handleClaudeMessages)\n\n\t// Gemini 单模型详情 GET /v1beta/models/{model}\n\tapiGroup.GET(\"/v1beta/models/:model\", func(c *gin.Context) {\n\t\tmodelName := c.Param(\"model\")\n\t\t// 移除 \"models/\" 前缀（如果有）\n\t\tmodelName = strings.TrimPrefix(modelName, \"models/\")\n\n\t\t// 检查模型是否存在\n\t\tfound := false\n\t\tfor _, m := range GetAvailableModels() {\n\t\t\tif m == modelName {\n\t\t\t\tfound = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !found {\n\t\t\tc.JSON(404, gin.H{\"error\": gin.H{\n\t\t\t\t\"code\":    404,\n\t\t\t\t\"message\": \"Model not found: \" + modelName,\n\t\t\t\t\"status\":  \"NOT_FOUND\",\n\t\t\t}})\n\t\t\treturn\n\t\t}\n\n\t\tc.JSON(200, gin.H{\n\t\t\t\"name\":                       \"models/\" + modelName,\n\t\t\t\"version\":                    \"001\",\n\t\t\t\"displayName\":                modelName,\n\t\t\t\"description\":                \"Gemini model: \" + modelName,\n\t\t\t\"inputTokenLimit\":            1048576,\n\t\t\t\"outputTokenLimit\":           8192,\n\t\t\t\"supportedGenerationMethods\": []string{\"generateContent\", \"countTokens\"},\n\t\t\t\"temperature\":                1.0,\n\t\t\t\"topP\":                       0.95,\n\t\t\t\"topK\":                       64,\n\t\t})\n\t})\n\n\t// Gemini generateContent/streamGenerateContent\n\tapiGroup.POST(\"/v1beta/models/*action\", handleGeminiGenerate)\n\tapiGroup.POST(\"/v1/models/*action\", handleGeminiGenerate)\n\n\tadmin := r.Group(\"/admin\")\n\tadmin.Use(apiKeyAuth())\n\tadmin.POST(\"/register\", func(c *gin.Context) {\n\t\tvar req struct {\n\t\t\tCount int `json:\"count\"`\n\t\t}\n\t\tif err := c.ShouldBindJSON(&req); err != nil || req.Count <= 0 {\n\t\t\treq.Count = appConfig.Pool.TargetCount - pool.Pool.TotalCount()\n\t\t}\n\t\tif req.Count <= 0 {\n\t\t\tc.JSON(200, gin.H{\"message\": \"账号数量已足够\", \"count\": pool.Pool.TotalCount()})\n\t\t\treturn\n\t\t}\n\t\tif poolMode == PoolModeServer {\n\t\t\t// 服务端模式：注册任务会通过 WS 分发给客户端\n\t\t\tc.JSON(200, gin.H{\"message\": \"注册任务已加入队列，将通过 WS 分发给客户端\", \"target\": req.Count})\n\t\t\treturn\n\t\t}\n\t\tif err := register.StartRegister(req.Count); err != nil {\n\t\t\tc.JSON(500, gin.H{\"error\": err.Error()})\n\t\t\treturn\n\t\t}\n\t\tc.JSON(200, gin.H{\"message\": \"注册已启动\", \"target\": req.Count})\n\t})\n\n\tadmin.POST(\"/refresh\", func(c *gin.Context) {\n\t\tpool.Pool.Load(DataDir)\n\t\tc.JSON(200, gin.H{\n\t\t\t\"message\": \"刷新完成\",\n\t\t\t\"ready\":   pool.Pool.ReadyCount(),\n\t\t\t\"pending\": pool.Pool.PendingCount(),\n\t\t})\n\t})\n\n\tadmin.GET(\"/status\", func(c *gin.Context) {\n\t\tstats := pool.Pool.Stats()\n\t\tstats[\"target\"] = appConfig.Pool.TargetCount\n\t\tstats[\"min\"] = appConfig.Pool.MinCount\n\t\tstats[\"is_registering\"] = atomic.LoadInt32(&register.IsRegistering) == 1\n\t\tstats[\"register_stats\"] = register.Stats.Get()\n\t\tstats[\"mode\"] = map[PoolMode]string{PoolModeLocal: \"local\", PoolModeServer: \"server\", PoolModeClient: \"client\"}[poolMode]\n\t\tc.JSON(200, stats)\n\t})\n\n\t// 详细API统计\n\tadmin.GET(\"/stats\", func(c *gin.Context) {\n\t\tdetailed := apiStats.GetDetailedStats()\n\t\tdetailed[\"pool\"] = pool.Pool.Stats()\n\t\tdetailed[\"proxy_pool\"] = proxy.Manager.PoolStats()\n\t\tc.JSON(200, detailed)\n\t})\n\tadmin.GET(\"/ip\", func(c *gin.Context) {\n\t\tc.JSON(200, ipStats.GetAllIPStats())\n\t})\n\n\tadmin.POST(\"/force-refresh\", func(c *gin.Context) {\n\t\tcount := pool.Pool.ForceRefreshAll()\n\t\tc.JSON(200, gin.H{\n\t\t\t\"message\": \"已触发强制刷新\",\n\t\t\t\"count\":   count,\n\t\t})\n\t})\n\tadmin.POST(\"/reload-config\", func(c *gin.Context) {\n\t\tif err := reloadConfig(); err != nil {\n\t\t\tc.JSON(500, gin.H{\"error\": err.Error()})\n\t\t\treturn\n\t\t}\n\t\tconfigMu.RLock()\n\t\tc.JSON(200, gin.H{\n\t\t\t\"message\":  \"配置已重载\",\n\t\t\t\"api_keys\": len(appConfig.APIKeys),\n\t\t\t\"debug\":    appConfig.Debug,\n\t\t\t\"pool_config\": gin.H{\n\t\t\t\t\"refresh_cooldown_sec\":      appConfig.Pool.RefreshCooldownSec,\n\t\t\t\t\"use_cooldown_sec\":          appConfig.Pool.UseCooldownSec,\n\t\t\t\t\"max_fail_count\":            appConfig.Pool.MaxFailCount,\n\t\t\t\t\"enable_browser_refresh\":    appConfig.Pool.EnableBrowserRefresh,\n\t\t\t\t\"browser_refresh_headless\":  appConfig.Pool.BrowserRefreshHeadless,\n\t\t\t\t\"browser_refresh_max_retry\": appConfig.Pool.BrowserRefreshMaxRetry,\n\t\t\t\t\"auto_delete_401\":           appConfig.Pool.AutoDelete401,\n\t\t\t},\n\t\t})\n\t\tconfigMu.RUnlock()\n\t})\n\n\tadmin.POST(\"/config/cooldown\", func(c *gin.Context) {\n\t\tvar req struct {\n\t\t\tRefreshCooldownSec int `json:\"refresh_cooldown_sec\"`\n\t\t\tUseCooldownSec     int `json:\"use_cooldown_sec\"`\n\t\t}\n\t\tif err := c.ShouldBindJSON(&req); err != nil {\n\t\t\tc.JSON(400, gin.H{\"error\": err.Error()})\n\t\t\treturn\n\t\t}\n\t\tpool.SetCooldowns(req.RefreshCooldownSec, req.UseCooldownSec)\n\t\tc.JSON(200, gin.H{\n\t\t\t\"message\":              \"冷却配置已更新\",\n\t\t\t\"refresh_cooldown_sec\": int(pool.RefreshCooldown.Seconds()),\n\t\t\t\"use_cooldown_sec\":     int(pool.UseCooldown.Seconds()),\n\t\t})\n\t})\n\n\tadmin.POST(\"/browser-refresh\", func(c *gin.Context) {\n\t\tvar req struct {\n\t\t\tEmail string `json:\"email\"`\n\t\t}\n\t\tif err := c.ShouldBindJSON(&req); err != nil {\n\t\t\tc.JSON(400, gin.H{\"error\": err.Error()})\n\t\t\treturn\n\t\t}\n\t\tif req.Email == \"\" {\n\t\t\tc.JSON(400, gin.H{\"error\": \"需要提供 email\"})\n\t\t\treturn\n\t\t}\n\n\t\tvar targetAcc *pool.Account\n\t\tpool.Pool.WithLock(func(ready, pending []*pool.Account) {\n\t\t\tfor _, acc := range ready {\n\t\t\t\tif acc.Data.Email == req.Email {\n\t\t\t\t\ttargetAcc = acc\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t\tif targetAcc == nil {\n\t\t\t\tfor _, acc := range pending {\n\t\t\t\t\tif acc.Data.Email == req.Email {\n\t\t\t\t\t\ttargetAcc = acc\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\n\t\tif targetAcc == nil {\n\t\t\tc.JSON(404, gin.H{\"error\": \"账号未找到\", \"email\": req.Email})\n\t\t\treturn\n\t\t}\n\n\t\tgo func() {\n\t\t\tlogger.Info(\"🔄 手动触发浏览器刷新: %s\", req.Email)\n\t\t\tresult := register.RefreshCookieWithBrowser(targetAcc, pool.BrowserRefreshHeadless, Proxy)\n\t\t\tif result.Success {\n\t\t\t\ttargetAcc.Mu.Lock()\n\t\t\t\t// 更新完整信息\n\t\t\t\ttargetAcc.Data.Cookies = result.SecureCookies\n\t\t\t\tif result.Authorization != \"\" {\n\t\t\t\t\ttargetAcc.Data.Authorization = result.Authorization\n\t\t\t\t}\n\t\t\t\tif result.CSESIDX != \"\" {\n\t\t\t\t\ttargetAcc.CSESIDX = result.CSESIDX\n\t\t\t\t\ttargetAcc.Data.CSESIDX = result.CSESIDX\n\t\t\t\t}\n\t\t\t\tif result.ConfigID != \"\" {\n\t\t\t\t\ttargetAcc.ConfigID = result.ConfigID\n\t\t\t\t\ttargetAcc.Data.ConfigID = result.ConfigID\n\t\t\t\t}\n\t\t\t\ttargetAcc.Data.Timestamp = time.Now().Format(time.RFC3339)\n\t\t\t\ttargetAcc.FailCount = 0\n\t\t\t\ttargetAcc.Mu.Unlock()\n\n\t\t\t\tif err := targetAcc.SaveToFile(); err != nil {\n\t\t\t\t\tlogger.Error(\"❌ [%s] 保存刷新后的数据失败: %v\", req.Email, err)\n\t\t\t\t} else {\n\t\t\t\t\tlogger.Info(\"✅ [%s] 刷新数据已保存到文件\", req.Email)\n\t\t\t\t}\n\t\t\t\tpool.Pool.MarkNeedsRefresh(targetAcc)\n\t\t\t\tlogger.Info(\"✅ 手动浏览器刷新成功: %s\", req.Email)\n\t\t\t} else {\n\t\t\t\tlogger.Error(\"❌ 手动浏览器刷新失败: %s - %v\", req.Email, result.Error)\n\t\t\t}\n\t\t}()\n\n\t\tc.JSON(200, gin.H{\n\t\t\t\"message\": \"浏览器刷新已触发\",\n\t\t\t\"email\":   req.Email,\n\t\t})\n\t})\n\n\t// Flow Token 管理\n\tadmin.GET(\"/flow/status\", func(c *gin.Context) {\n\t\tif flowTokenPool == nil {\n\t\t\tc.JSON(200, gin.H{\n\t\t\t\t\"enabled\": false,\n\t\t\t\t\"message\": \"Flow 服务未启用\",\n\t\t\t})\n\t\t\treturn\n\t\t}\n\t\tstats := flowTokenPool.Stats()\n\t\tstats[\"enabled\"] = flowHandler != nil\n\t\tc.JSON(200, stats)\n\t})\n\n\tadmin.POST(\"/flow/add-token\", func(c *gin.Context) {\n\t\tif flowTokenPool == nil {\n\t\t\tc.JSON(503, gin.H{\"error\": \"Flow 服务未启用\"})\n\t\t\treturn\n\t\t}\n\t\tvar req struct {\n\t\t\tCookie string `json:\"cookie\"`\n\t\t}\n\t\tif err := c.ShouldBindJSON(&req); err != nil {\n\t\t\tc.JSON(400, gin.H{\"error\": err.Error()})\n\t\t\treturn\n\t\t}\n\t\tif req.Cookie == \"\" {\n\t\t\tc.JSON(400, gin.H{\"error\": \"需要提供 cookie\"})\n\t\t\treturn\n\t\t}\n\t\ttokenID, err := flowTokenPool.AddFromCookie(req.Cookie)\n\t\tif err != nil {\n\t\t\tc.JSON(400, gin.H{\"error\": err.Error()})\n\t\t\treturn\n\t\t}\n\t\tc.JSON(200, gin.H{\n\t\t\t\"message\":  \"Token 添加成功\",\n\t\t\t\"token_id\": tokenID,\n\t\t\t\"total\":    flowTokenPool.Count(),\n\t\t})\n\t})\n\n\tadmin.POST(\"/flow/remove-token\", func(c *gin.Context) {\n\t\tif flowTokenPool == nil {\n\t\t\tc.JSON(503, gin.H{\"error\": \"Flow 服务未启用\"})\n\t\t\treturn\n\t\t}\n\t\tvar req struct {\n\t\t\tTokenID string `json:\"token_id\"`\n\t\t}\n\t\tif err := c.ShouldBindJSON(&req); err != nil {\n\t\t\tc.JSON(400, gin.H{\"error\": err.Error()})\n\t\t\treturn\n\t\t}\n\t\tif err := flowTokenPool.RemoveToken(req.TokenID); err != nil {\n\t\t\tc.JSON(400, gin.H{\"error\": err.Error()})\n\t\t\treturn\n\t\t}\n\t\tc.JSON(200, gin.H{\n\t\t\t\"message\": \"Token 已移除\",\n\t\t\t\"total\":   flowTokenPool.Count(),\n\t\t})\n\t})\n\n\tadmin.POST(\"/flow/reload\", func(c *gin.Context) {\n\t\tif flowTokenPool == nil {\n\t\t\tc.JSON(503, gin.H{\"error\": \"Flow 服务未启用\"})\n\t\t\treturn\n\t\t}\n\t\tloaded, err := flowTokenPool.LoadFromDir()\n\t\tif err != nil {\n\t\t\tc.JSON(500, gin.H{\"error\": err.Error()})\n\t\t\treturn\n\t\t}\n\t\tc.JSON(200, gin.H{\n\t\t\t\"message\": \"已重新加载\",\n\t\t\t\"loaded\":  loaded,\n\t\t\t\"total\":   flowTokenPool.Count(),\n\t\t})\n\t})\n\n\tadmin.POST(\"/config/browser-refresh\", func(c *gin.Context) {\n\t\tvar req struct {\n\t\t\tEnable   *bool `json:\"enable\"`\n\t\t\tHeadless *bool `json:\"headless\"`\n\t\t}\n\t\tif err := c.ShouldBindJSON(&req); err != nil {\n\t\t\tc.JSON(400, gin.H{\"error\": err.Error()})\n\t\t\treturn\n\t\t}\n\t\tif req.Enable != nil {\n\t\t\tpool.EnableBrowserRefresh = *req.Enable\n\t\t}\n\t\tif req.Headless != nil {\n\t\t\tpool.BrowserRefreshHeadless = *req.Headless\n\t\t}\n\t\tc.JSON(200, gin.H{\n\t\t\t\"message\":  \"浏览器刷新配置已更新\",\n\t\t\t\"enable\":   pool.EnableBrowserRefresh,\n\t\t\t\"headless\": pool.BrowserRefreshHeadless,\n\t\t})\n\t})\n}\n\nfunc runLocalMode() {\n\t// 本地模式：正常启动\n\tif err := pool.Pool.Load(DataDir); err != nil {\n\t\tlog.Fatalf(\"❌ 加载账号失败: %v\", err)\n\t}\n\n\t// 启动配置文件热重载监听\n\tif err := startConfigWatcher(); err != nil {\n\t\tlogger.Warn(\"⚠️ 配置热重载启动失败: %v\", err)\n\t}\n\n\t// 代理实例池由异步健康检查完成后初始化\n\n\t// 检查 CONFIG_ID\n\tif DefaultConfig != \"\" {\n\t\tlogger.Info(\"✅ 使用默认 configId: %s\", DefaultConfig)\n\t}\n\n\t// 检查 API Key 配置\n\tif len(GetAPIKeys()) == 0 {\n\t\tlogger.Warn(\"⚠️ 未配置 API Key，API 将无鉴权运行\")\n\t}\n\n\t// 启动号池管理\n\tif appConfig.Pool.RefreshOnStartup {\n\t\tpool.Pool.StartPoolManager()\n\t}\n\tif pool.Pool.TotalCount() == 0 {\n\t\tneedCount := appConfig.Pool.TargetCount\n\t\tlogger.Info(\"📝 无账号，启动注册 %d 个...\", needCount)\n\t\tregister.StartRegister(needCount)\n\t}\n\tif appConfig.Pool.CheckIntervalMinutes > 0 {\n\t\tgo register.PoolMaintainer()\n\t}\n\n\t// 启动 API 服务\n\trunAPIServer()\n}\n"
  },
  {
    "path": "src/api/api.go",
    "content": "package api\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/gin-gonic/gin\"\n)\n\n// ChatRequest 聊天请求\ntype ChatRequest struct {\n\tModel       string    `json:\"model\"`\n\tMessages    []Message `json:\"messages\"`\n\tStream      bool      `json:\"stream\"`\n\tTemperature float64   `json:\"temperature,omitempty\"`\n\tMaxTokens   int       `json:\"max_tokens,omitempty\"`\n\tTools       []ToolDef `json:\"tools,omitempty\"`\n}\n\n// Message 消息\ntype Message struct {\n\tRole    string      `json:\"role\"`\n\tContent interface{} `json:\"content\"`\n}\n\n// ToolDef 工具定义\ntype ToolDef struct {\n\tType     string      `json:\"type\"`\n\tFunction FunctionDef `json:\"function\"`\n}\n\n// FunctionDef 函数定义\ntype FunctionDef struct {\n\tName        string                 `json:\"name\"`\n\tDescription string                 `json:\"description\"`\n\tParameters  map[string]interface{} `json:\"parameters,omitempty\"`\n}\n\nvar (\n\tStreamChat  func(c *gin.Context, req ChatRequest)\n\tFixedModels []string\n)\ntype GeminiRequest struct {\n\tContents          []GeminiContent          `json:\"contents\"`\n\tSystemInstruction *GeminiContent           `json:\"systemInstruction,omitempty\"`\n\tGenerationConfig  map[string]interface{}   `json:\"generationConfig,omitempty\"`\n\tGeminiTools       []map[string]interface{} `json:\"tools,omitempty\"`\n}\n\ntype GeminiContent struct {\n\tRole  string       `json:\"role,omitempty\"`\n\tParts []GeminiPart `json:\"parts\"`\n}\n\ntype GeminiPart struct {\n\tText       string            `json:\"text,omitempty\"`\n\tInlineData *GeminiInlineData `json:\"inlineData,omitempty\"`\n}\n\ntype GeminiInlineData struct {\n\tMimeType string `json:\"mimeType\"`\n\tData     string `json:\"data\"`\n}\n\nfunc HandleGeminiGenerate(c *gin.Context) {\n\taction := c.Param(\"action\")\n\tif action == \"\" {\n\t\tc.JSON(400, gin.H{\"error\": gin.H{\"code\": 400, \"message\": \"Missing model action\", \"status\": \"INVALID_ARGUMENT\"}})\n\t\treturn\n\t}\n\n\t// 去掉开头的 /\n\taction = strings.TrimPrefix(action, \"/\")\n\n\t// 解析模型名和动作\n\tvar model string\n\tvar isStream bool\n\tif idx := strings.LastIndex(action, \":\"); idx > 0 {\n\t\tmodel = action[:idx]\n\t\tactionType := action[idx+1:]\n\t\tisStream = actionType == \"streamGenerateContent\"\n\t} else {\n\t\tmodel = action\n\t}\n\n\tif model == \"\" {\n\t\tmodel = FixedModels[0]\n\t}\n\n\tvar geminiReq GeminiRequest\n\tif err := c.ShouldBindJSON(&geminiReq); err != nil {\n\t\tc.JSON(400, gin.H{\"error\": gin.H{\"code\": 400, \"message\": err.Error(), \"status\": \"INVALID_ARGUMENT\"}})\n\t\treturn\n\t}\n\n\tvar messages []Message\n\n\t// 处理systemInstruction\n\tif geminiReq.SystemInstruction != nil && len(geminiReq.SystemInstruction.Parts) > 0 {\n\t\tvar sysText string\n\t\tfor _, part := range geminiReq.SystemInstruction.Parts {\n\t\t\tif part.Text != \"\" {\n\t\t\t\tsysText += part.Text\n\t\t\t}\n\t\t}\n\t\tif sysText != \"\" {\n\t\t\tmessages = append(messages, Message{Role: \"system\", Content: sysText})\n\t\t}\n\t}\n\tfor _, content := range geminiReq.Contents {\n\t\trole := content.Role\n\t\tif role == \"model\" {\n\t\t\trole = \"assistant\"\n\t\t}\n\n\t\tvar textParts []string\n\t\tvar contentParts []interface{}\n\n\t\tfor _, part := range content.Parts {\n\t\t\tif part.Text != \"\" {\n\t\t\t\ttextParts = append(textParts, part.Text)\n\t\t\t}\n\t\t\tif part.InlineData != nil {\n\t\t\t\tcontentParts = append(contentParts, map[string]interface{}{\n\t\t\t\t\t\"type\": \"image_url\",\n\t\t\t\t\t\"image_url\": map[string]string{\n\t\t\t\t\t\t\"url\": fmt.Sprintf(\"data:%s;base64,%s\", part.InlineData.MimeType, part.InlineData.Data),\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\n\t\tif len(contentParts) > 0 {\n\t\t\tif len(textParts) > 0 {\n\t\t\t\tcontentParts = append([]interface{}{map[string]interface{}{\"type\": \"text\", \"text\": strings.Join(textParts, \"\\n\")}}, contentParts...)\n\t\t\t}\n\t\t\tmessages = append(messages, Message{Role: role, Content: contentParts})\n\t\t} else if len(textParts) > 0 {\n\t\t\tmessages = append(messages, Message{Role: role, Content: strings.Join(textParts, \"\\n\")})\n\t\t}\n\t}\n\n\t// 流式判断：路径中包含streamGenerateContent 或 query参数 alt=sse\n\tstream := isStream || c.Query(\"alt\") == \"sse\"\n\tvar tools []ToolDef\n\tfor _, gt := range geminiReq.GeminiTools {\n\t\tif funcDecls, ok := gt[\"functionDeclarations\"].([]interface{}); ok {\n\t\t\tfor _, fd := range funcDecls {\n\t\t\t\tif funcMap, ok := fd.(map[string]interface{}); ok {\n\t\t\t\t\tname, _ := funcMap[\"name\"].(string)\n\t\t\t\t\tdesc, _ := funcMap[\"description\"].(string)\n\t\t\t\t\tparams, _ := funcMap[\"parameters\"].(map[string]interface{})\n\t\t\t\t\ttools = append(tools, ToolDef{\n\t\t\t\t\t\tType: \"function\",\n\t\t\t\t\t\tFunction: FunctionDef{\n\t\t\t\t\t\t\tName:        name,\n\t\t\t\t\t\t\tDescription: desc,\n\t\t\t\t\t\t\tParameters:  params,\n\t\t\t\t\t\t},\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treq := ChatRequest{\n\t\tModel:    model,\n\t\tMessages: messages,\n\t\tStream:   stream,\n\t\tTools:    tools,\n\t}\n\n\tStreamChat(c, req)\n}\n\ntype ClaudeRequest struct {\n\tModel       string    `json:\"model\"`\n\tMessages    []Message `json:\"messages\"`\n\tSystem      string    `json:\"system,omitempty\"`\n\tMaxTokens   int       `json:\"max_tokens,omitempty\"`\n\tStream      bool      `json:\"stream\"`\n\tTemperature float64   `json:\"temperature,omitempty\"`\n\tTools       []ToolDef `json:\"tools,omitempty\"`\n}\nfunc HandleClaudeMessages(c *gin.Context) {\n\tvar claudeReq ClaudeRequest\n\tif err := c.ShouldBindJSON(&claudeReq); err != nil {\n\t\tc.JSON(400, gin.H{\"type\": \"error\", \"error\": gin.H{\"type\": \"invalid_request_error\", \"message\": err.Error()}})\n\t\treturn\n\t}\n\n\treq := ChatRequest{\n\t\tModel:       claudeReq.Model,\n\t\tMessages:    claudeReq.Messages,\n\t\tStream:      claudeReq.Stream,\n\t\tTemperature: claudeReq.Temperature,\n\t\tTools:       claudeReq.Tools,\n\t}\n\tif claudeReq.System != \"\" {\n\t\tsystemMsg := Message{Role: \"system\", Content: claudeReq.System}\n\t\treq.Messages = append([]Message{systemMsg}, req.Messages...)\n\t}\n\n\t// 保持模型名原样，不做映射\n\tif req.Model == \"\" {\n\t\treq.Model = FixedModels[0]\n\t}\n\n\tStreamChat(c, req)\n}\n"
  },
  {
    "path": "src/logger/logger.go",
    "content": "package logger\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"sync\"\n\t\"time\"\n)\n\n// Level 日志级别\ntype Level int\n\nconst (\n\tLevelError Level = iota\n\tLevelWarn\n\tLevelInfo\n\tLevelDebug\n)\n\nvar levelNames = map[Level]string{\n\tLevelError: \"ERROR\",\n\tLevelWarn:  \"WARN\",\n\tLevelInfo:  \"INFO\",\n\tLevelDebug: \"DEBUG\",\n}\n\n// Logger 日志记录器\ntype Logger struct {\n\tlevel  Level\n\tprefix string\n\tmu     sync.Mutex\n}\n\nvar (\n\tdefaultLogger = &Logger{level: LevelInfo}\n\tdebugMode     = false\n)\n\n// SetDebugMode 设置调试模式\nfunc SetDebugMode(debug bool) {\n\tdebugMode = debug\n\tif debug {\n\t\tdefaultLogger.level = LevelDebug\n\t} else {\n\t\tdefaultLogger.level = LevelInfo\n\t}\n}\n\n// IsDebug 是否为调试模式\nfunc IsDebug() bool {\n\treturn debugMode\n}\n\n// SetLevel 设置日志级别\nfunc SetLevel(level Level) {\n\tdefaultLogger.level = level\n}\n\nfunc (l *Logger) log(level Level, format string, args ...interface{}) {\n\tif level > l.level {\n\t\treturn\n\t}\n\tl.mu.Lock()\n\tdefer l.mu.Unlock()\n\n\ttimestamp := time.Now().Format(\"15:04:05\")\n\tlevelStr := levelNames[level]\n\tmsg := fmt.Sprintf(format, args...)\n\n\tif l.prefix != \"\" {\n\t\tlog.Printf(\"[%s] [%s] [%s] %s\", timestamp, levelStr, l.prefix, msg)\n\t} else {\n\t\tlog.Printf(\"[%s] [%s] %s\", timestamp, levelStr, msg)\n\t}\n}\n\n// Error 错误日志（始终输出）\nfunc Error(format string, args ...interface{}) {\n\tdefaultLogger.log(LevelError, format, args...)\n}\n\n// Warn 警告日志（始终输出）\nfunc Warn(format string, args ...interface{}) {\n\tdefaultLogger.log(LevelWarn, format, args...)\n}\n\n// Info 信息日志（正常模式输出）\nfunc Info(format string, args ...interface{}) {\n\tdefaultLogger.log(LevelInfo, format, args...)\n}\n\n// Debug 调试日志（仅debug模式输出）\nfunc Debug(format string, args ...interface{}) {\n\tdefaultLogger.log(LevelDebug, format, args...)\n}\n\n// WithPrefix 创建带前缀的子日志器\nfunc WithPrefix(prefix string) *Logger {\n\treturn &Logger{\n\t\tlevel:  defaultLogger.level,\n\t\tprefix: prefix,\n\t}\n}\n\nfunc (l *Logger) Error(format string, args ...interface{}) { l.log(LevelError, format, args...) }\nfunc (l *Logger) Warn(format string, args ...interface{})  { l.log(LevelWarn, format, args...) }\nfunc (l *Logger) Info(format string, args ...interface{})  { l.log(LevelInfo, format, args...) }\nfunc (l *Logger) Debug(format string, args ...interface{}) { l.log(LevelDebug, format, args...) }\n\nfunc init() {\n\tlog.SetFlags(0)\n\tlog.SetOutput(os.Stdout)\n}\n"
  },
  {
    "path": "src/pool/pool.go",
    "content": "package pool\n\nimport (\n\t\"crypto/hmac\"\n\t\"crypto/sha256\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"business2api/src/logger\"\n)\n\n// ==================== 数据结构 ====================\n\n// Cookie 账号Cookie\ntype Cookie struct {\n\tName   string `json:\"name\"`\n\tValue  string `json:\"value\"`\n\tDomain string `json:\"domain\"`\n}\n\n// AccountData 账号数据\ntype AccountData struct {\n\tEmail           string            `json:\"email\"`\n\tFullName        string            `json:\"fullName\"`\n\tAuthorization   string            `json:\"authorization\"`\n\tCookies         []Cookie          `json:\"cookies\"`\n\tCookieString    string            `json:\"cookie_string,omitempty\"`\n\tResponseHeaders map[string]string `json:\"response_headers,omitempty\"`\n\tTimestamp       string            `json:\"timestamp\"`\n\tConfigID        string            `json:\"configId,omitempty\"`\n\tCSESIDX         string            `json:\"csesidx,omitempty\"`\n}\n\nfunc ParseCookieString(cookieStr string) []Cookie {\n\tvar cookies []Cookie\n\tif cookieStr == \"\" {\n\t\treturn cookies\n\t}\n\n\tparts := strings.Split(cookieStr, \"; \")\n\tfor _, part := range parts {\n\t\tpart = strings.TrimSpace(part)\n\t\tif part == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tidx := strings.Index(part, \"=\")\n\t\tif idx > 0 {\n\t\t\tcookies = append(cookies, Cookie{\n\t\t\t\tName:   part[:idx],\n\t\t\t\tValue:  part[idx+1:],\n\t\t\t\tDomain: \".gemini.google\", // 默认域名\n\t\t\t})\n\t\t}\n\t}\n\treturn cookies\n}\n\nfunc (a *AccountData) GetAllCookies() []Cookie {\n\tif len(a.Cookies) > 0 {\n\t\treturn a.Cookies\n\t}\n\tif a.CookieString != \"\" {\n\t\treturn ParseCookieString(a.CookieString)\n\t}\n\treturn nil\n}\n\n// AccountStatus 账号状态\ntype AccountStatus int\n\nconst (\n\tStatusPending  AccountStatus = iota // 待刷新\n\tStatusReady                         // 就绪可用\n\tStatusCooldown                      // 冷却中\n\tStatusInvalid                       // 失效\n)\n\n// Account 账号实例\ntype Account struct {\n\tData                AccountData\n\tFilePath            string\n\tJWT                 string\n\tJWTExpires          time.Time\n\tConfigID            string\n\tCSESIDX             string\n\tLastRefresh         time.Time\n\tLastUsed            time.Time // 最后使用时间\n\tRefreshed           bool\n\tFailCount           int    // 连续失败次数\n\tBrowserRefreshCount int    // 浏览器刷新尝试次数\n\tSuccessCount        int    // 成功次数\n\tTotalCount          int    // 总使用次数\n\tDailyCount          int    // 每日调用次数\n\tDailyCountDate      string // 每日计数日期 (YYYY-MM-DD)\n\tStatus              AccountStatus\n\tMu                  sync.Mutex\n}\n\n// SetCooldownMultiplier 设置冷却时间倍数（用于429限流）\nfunc (acc *Account) SetCooldownMultiplier(multiplier int) {\n\tacc.Mu.Lock()\n\tacc.LastUsed = time.Now().Add(UseCooldown * time.Duration(multiplier-1))\n\tacc.Mu.Unlock()\n}\n\n// 默认冷却时间（可通过配置覆盖）\nvar (\n\tRefreshCooldown        = 4 * time.Minute  // 刷新冷却\n\tUseCooldown            = 15 * time.Second // 使用冷却\n\tJWTRefreshThreshold    = 60 * time.Second // JWT刷新阈值\n\tMaxFailCount           = 3                // 最大连续失败次数\n\tEnableBrowserRefresh   = true             // 是否启用浏览器刷新\n\tBrowserRefreshHeadless = true             // 浏览器刷新是否无头模式\n\tBrowserRefreshMaxRetry = 1                // 浏览器刷新最大重试次数\n\tAutoDelete401          = false            // 401时是否自动删除账号\n\tDailyLimit             = 3000             // 每账号每日最大调用次数\n\tDataDir                string\n\tDefaultConfig          string\n\tProxy                  string\n\tJwtTTL                 = 270 * time.Second\n\tHTTPClient             *http.Client\n)\n\ntype RefreshCookieFunc func(acc *Account, headless bool, proxy string) *BrowserRefreshResult\ntype BrowserRefreshResult struct {\n\tSuccess         bool\n\tSecureCookies   []Cookie\n\tAuthorization   string\n\tConfigID        string\n\tCSESIDX         string\n\tResponseHeaders map[string]string\n\tError           error\n}\n\nvar RefreshCookieWithBrowser RefreshCookieFunc\n\nfunc readResponseBody(resp *http.Response) ([]byte, error) {\n\tbody := make([]byte, 0)\n\tbuf := make([]byte, 4096)\n\tfor {\n\t\tn, err := resp.Body.Read(buf)\n\t\tif n > 0 {\n\t\t\tbody = append(body, buf[:n]...)\n\t\t}\n\t\tif err != nil {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn body, nil\n}\n\ntype AccountPool struct {\n\treadyAccounts   []*Account\n\tpendingAccounts []*Account\n\tindex           uint64\n\tmu              sync.RWMutex\n\trefreshInterval time.Duration\n\trefreshWorkers  int\n\tstopChan        chan struct{}\n\ttotalSuccess    int64\n\ttotalFailed     int64\n\ttotalRequests   int64\n}\n\nfunc (p *AccountPool) GetReadyAccounts() []*Account {\n\tp.mu.RLock()\n\tdefer p.mu.RUnlock()\n\treturn p.readyAccounts\n}\nfunc (p *AccountPool) GetPendingAccounts() []*Account {\n\tp.mu.RLock()\n\tdefer p.mu.RUnlock()\n\treturn p.pendingAccounts\n}\nfunc (p *AccountPool) WithLock(fn func(ready, pending []*Account)) {\n\tp.mu.RLock()\n\tdefer p.mu.RUnlock()\n\tfn(p.readyAccounts, p.pendingAccounts)\n}\nfunc (p *AccountPool) WithWriteLock(fn func(ready, pending []*Account) ([]*Account, []*Account)) {\n\tp.mu.Lock()\n\tdefer p.mu.Unlock()\n\tp.readyAccounts, p.pendingAccounts = fn(p.readyAccounts, p.pendingAccounts)\n}\n\nvar Pool = &AccountPool{\n\trefreshInterval: 5 * time.Second,\n\trefreshWorkers:  5,\n\tstopChan:        make(chan struct{}),\n}\n\nfunc SetCooldowns(refreshSec, useSec int) {\n\tif refreshSec > 0 {\n\t\tRefreshCooldown = time.Duration(refreshSec) * time.Second\n\t}\n\tif useSec > 0 {\n\t\tUseCooldown = time.Duration(useSec) * time.Second\n\t}\n\tlogger.Info(\"⚙️ 冷却配置: 刷新=%v, 使用=%v\", RefreshCooldown, UseCooldown)\n}\n\n// SetDailyLimit 设置每账号每日最大调用次数\nfunc SetDailyLimit(limit int) {\n\tif limit >= 0 {\n\t\tDailyLimit = limit\n\t\tif limit == 0 {\n\t\t\tlogger.Info(\"⚙️ 每日调用限制: 无限制\")\n\t\t} else {\n\t\t\tlogger.Info(\"⚙️ 每日调用限制: %d次/账号\", limit)\n\t\t}\n\t}\n}\n\nfunc (p *AccountPool) Load(dir string) error {\n\tp.mu.Lock()\n\tdefer p.mu.Unlock()\n\n\tfiles, err := filepath.Glob(filepath.Join(dir, \"*.json\"))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\texistingAccounts := make(map[string]*Account)\n\tfor _, acc := range p.readyAccounts {\n\t\texistingAccounts[acc.FilePath] = acc\n\t}\n\tfor _, acc := range p.pendingAccounts {\n\t\texistingAccounts[acc.FilePath] = acc\n\t}\n\n\tvar newReadyAccounts []*Account\n\tvar newPendingAccounts []*Account\n\n\tfor _, f := range files {\n\t\tif acc, ok := existingAccounts[f]; ok {\n\t\t\tif acc.Refreshed {\n\t\t\t\tnewReadyAccounts = append(newReadyAccounts, acc)\n\t\t\t} else {\n\t\t\t\tnewPendingAccounts = append(newPendingAccounts, acc)\n\t\t\t}\n\t\t\tdelete(existingAccounts, f)\n\t\t\tcontinue\n\t\t}\n\n\t\tdata, err := os.ReadFile(f)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"⚠️ 读取 %s 失败: %v\", f, err)\n\t\t\tcontinue\n\t\t}\n\n\t\tvar acc AccountData\n\t\tif err := json.Unmarshal(data, &acc); err != nil {\n\t\t\tlog.Printf(\"⚠️ 解析 %s 失败: %v\", f, err)\n\t\t\tcontinue\n\t\t}\n\n\t\tcsesidx := acc.CSESIDX\n\t\tif csesidx == \"\" {\n\t\t\tcsesidx = extractCSESIDX(acc.Authorization)\n\t\t}\n\t\tif csesidx == \"\" {\n\t\t\tlog.Printf(\"⚠️ %s 无法获取 csesidx\", f)\n\t\t\tcontinue\n\t\t}\n\n\t\tconfigID := acc.ConfigID\n\t\tif configID == \"\" && DefaultConfig != \"\" {\n\t\t\tconfigID = DefaultConfig\n\t\t}\n\n\t\tnewPendingAccounts = append(newPendingAccounts, &Account{\n\t\t\tData:      acc,\n\t\t\tFilePath:  f,\n\t\t\tCSESIDX:   csesidx,\n\t\t\tConfigID:  configID,\n\t\t\tRefreshed: false,\n\t\t})\n\t}\n\n\tp.readyAccounts = newReadyAccounts\n\tp.pendingAccounts = newPendingAccounts\n\treturn nil\n}\n\n// GetPendingAccount 获取待刷新账号\nfunc (p *AccountPool) GetPendingAccount() *Account {\n\tp.mu.Lock()\n\tdefer p.mu.Unlock()\n\n\tif len(p.pendingAccounts) == 0 {\n\t\treturn nil\n\t}\n\n\tacc := p.pendingAccounts[0]\n\tp.pendingAccounts = p.pendingAccounts[1:]\n\treturn acc\n}\n\n// MarkReady 标记账号为就绪\nfunc (p *AccountPool) MarkReady(acc *Account) {\n\tp.mu.Lock()\n\tdefer p.mu.Unlock()\n\tacc.Refreshed = true\n\tp.readyAccounts = append(p.readyAccounts, acc)\n}\n\n// MarkPending 标记账号待刷新\nfunc (p *AccountPool) MarkPending(acc *Account) {\n\tp.mu.Lock()\n\tdefer p.mu.Unlock()\n\n\tfor i, a := range p.readyAccounts {\n\t\tif a == acc {\n\t\t\tp.readyAccounts = append(p.readyAccounts[:i], p.readyAccounts[i+1:]...)\n\t\t\tbreak\n\t\t}\n\t}\n\n\tacc.Mu.Lock()\n\tacc.Refreshed = false\n\tacc.Mu.Unlock()\n\n\tp.pendingAccounts = append(p.pendingAccounts, acc)\n\tlog.Printf(\"🔄 账号 %s 移至刷新池\", filepath.Base(acc.FilePath))\n}\n\n// RemoveAccount 删除失效账号\nfunc (p *AccountPool) RemoveAccount(acc *Account) {\n\tif err := os.Remove(acc.FilePath); err != nil {\n\t\tlog.Printf(\"⚠️ 删除文件失败 %s: %v\", acc.FilePath, err)\n\t} else {\n\t\tlog.Printf(\"🗑️ 已删除失效账号: %s\", filepath.Base(acc.FilePath))\n\t}\n}\n\n// SaveToFile 保存账号到文件\nfunc (acc *Account) SaveToFile() error {\n\tacc.Mu.Lock()\n\tdefer acc.Mu.Unlock()\n\n\tacc.Data.Timestamp = time.Now().Format(time.RFC3339)\n\n\t// 同时生成 cookie 字符串（方便调试和兼容老版本）\n\tif len(acc.Data.Cookies) > 0 {\n\t\tvar cookieParts []string\n\t\tfor _, c := range acc.Data.Cookies {\n\t\t\tcookieParts = append(cookieParts, fmt.Sprintf(\"%s=%s\", c.Name, c.Value))\n\t\t}\n\t\tacc.Data.CookieString = strings.Join(cookieParts, \"; \")\n\t}\n\n\tdata, err := json.MarshalIndent(acc.Data, \"\", \"  \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"序列化账号数据失败: %w\", err)\n\t}\n\n\tif err := os.WriteFile(acc.FilePath, data, 0644); err != nil {\n\t\treturn fmt.Errorf(\"写入文件失败: %w\", err)\n\t}\n\treturn nil\n}\n\n// StartPoolManager 启动号池管理器\nfunc (p *AccountPool) StartPoolManager() {\n\tfor i := 0; i < p.refreshWorkers; i++ {\n\t\tgo p.refreshWorker(i)\n\t}\n\tgo p.scanWorker()\n}\n\nfunc (p *AccountPool) refreshWorker(id int) {\n\tfor {\n\t\tselect {\n\t\tcase <-p.stopChan:\n\t\t\treturn\n\t\tdefault:\n\t\t}\n\n\t\tacc := p.GetPendingAccount()\n\t\tif acc == nil {\n\t\t\ttime.Sleep(time.Second)\n\t\t\tcontinue\n\t\t}\n\n\t\t// 检查冷却\n\t\tif time.Since(acc.LastRefresh) < RefreshCooldown {\n\t\t\tacc.Mu.Lock()\n\t\t\tacc.Refreshed = true\n\t\t\tacc.Status = StatusReady\n\t\t\tacc.Mu.Unlock()\n\t\t\tp.MarkReady(acc)\n\t\t\tcontinue\n\t\t}\n\n\t\tacc.JWTExpires = time.Time{}\n\t\tif err := acc.RefreshJWT(); err != nil {\n\t\t\terrMsg := err.Error()\n\n\t\t\t// 认证失败：根据配置决定是否删除或尝试刷新\n\t\t\tif strings.Contains(errMsg, \"账号失效\") ||\n\t\t\t\tstrings.Contains(errMsg, \"401\") ||\n\t\t\t\tstrings.Contains(errMsg, \"403\") {\n\t\t\t\tlog.Printf(\"⚠️ [worker-%d] [%s] 认证失效: %v\", id, acc.Data.Email, err)\n\n\t\t\t\t// 如果配置了401自动删除，直接删除账号\n\t\t\t\tif AutoDelete401 {\n\t\t\t\t\tlog.Printf(\"🗑️ [worker-%d] [%s] 401自动删除已启用，移除账号\", id, acc.Data.Email)\n\t\t\t\t\tacc.Mu.Lock()\n\t\t\t\t\tacc.Status = StatusInvalid\n\t\t\t\t\tacc.Mu.Unlock()\n\t\t\t\t\tp.RemoveAccount(acc)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\t// 检查是否可以进行浏览器刷新\n\t\t\t\tacc.Mu.Lock()\n\t\t\t\tbrowserRefreshCount := acc.BrowserRefreshCount\n\t\t\t\tacc.Mu.Unlock()\n\t\t\t\tif EnableBrowserRefresh && BrowserRefreshMaxRetry > 0 && browserRefreshCount < BrowserRefreshMaxRetry && RefreshCookieWithBrowser != nil {\n\t\t\t\t\tacc.Mu.Lock()\n\t\t\t\t\tacc.BrowserRefreshCount++\n\t\t\t\t\tacc.Mu.Unlock()\n\t\t\t\t\trefreshResult := RefreshCookieWithBrowser(acc, BrowserRefreshHeadless, Proxy)\n\n\t\t\t\t\tif refreshResult.Success {\n\t\t\t\t\t\tacc.Mu.Lock()\n\t\t\t\t\t\tacc.Data.Cookies = refreshResult.SecureCookies\n\t\t\t\t\t\tif refreshResult.Authorization != \"\" {\n\t\t\t\t\t\t\tacc.Data.Authorization = refreshResult.Authorization\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif refreshResult.ConfigID != \"\" {\n\t\t\t\t\t\t\tacc.ConfigID = refreshResult.ConfigID\n\t\t\t\t\t\t\tacc.Data.ConfigID = refreshResult.ConfigID\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif refreshResult.CSESIDX != \"\" {\n\t\t\t\t\t\t\tacc.CSESIDX = refreshResult.CSESIDX\n\t\t\t\t\t\t\tacc.Data.CSESIDX = refreshResult.CSESIDX\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif len(refreshResult.ResponseHeaders) > 0 {\n\t\t\t\t\t\t\tacc.Data.ResponseHeaders = refreshResult.ResponseHeaders\n\t\t\t\t\t\t}\n\t\t\t\t\t\tacc.FailCount = 0\n\t\t\t\t\t\tacc.BrowserRefreshCount = 0  // 成功后重置计数\n\t\t\t\t\t\tacc.JWTExpires = time.Time{} // 重置JWT过期时间\n\t\t\t\t\t\tacc.Status = StatusPending\n\t\t\t\t\t\tacc.Mu.Unlock()\n\n\t\t\t\t\t\t// 保存更新后的账号\n\t\t\t\t\t\tif err := acc.SaveToFile(); err != nil {\n\t\t\t\t\t\t\tlog.Printf(\"⚠️ [%s] 保存刷新后的账号失败: %v\", acc.Data.Email, err)\n\t\t\t\t\t\t}\n\t\t\t\t\t\tp.mu.Lock()\n\t\t\t\t\t\tp.pendingAccounts = append(p.pendingAccounts, acc)\n\t\t\t\t\t\tp.mu.Unlock()\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t} else {\n\t\t\t\t\t\tlog.Printf(\"⚠️ [worker-%d] [%s] 浏览器刷新失败: %v\", id, acc.Data.Email, refreshResult.Error)\n\t\t\t\t\t}\n\t\t\t\t} else if browserRefreshCount >= BrowserRefreshMaxRetry && BrowserRefreshMaxRetry > 0 {\n\t\t\t\t\tlog.Printf(\"⚠️ [worker-%d] [%s] 已达浏览器刷新上限 (%d次)，跳过浏览器刷新\", id, acc.Data.Email, BrowserRefreshMaxRetry)\n\t\t\t\t}\n\t\t\t\tacc.Mu.Lock()\n\t\t\t\tacc.FailCount++\n\t\t\t\tfailCount := acc.FailCount\n\t\t\t\tbrowserRefreshCount = acc.BrowserRefreshCount\n\t\t\t\tacc.Mu.Unlock()\n\t\t\t\tmaxRetry := MaxFailCount * 3 // 401的最大重试次数更宽松\n\t\t\t\tif maxRetry < 10 {\n\t\t\t\t\tmaxRetry = 10 // 至少重试10次\n\t\t\t\t}\n\t\t\t\tif browserRefreshCount >= BrowserRefreshMaxRetry && failCount >= maxRetry {\n\t\t\t\t\tacc.Mu.Lock()\n\t\t\t\t\tacc.Status = StatusInvalid\n\t\t\t\t\tacc.Mu.Unlock()\n\t\t\t\t\tp.RemoveAccount(acc)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\twaitTime := time.Duration(failCount*30) * time.Second\n\t\t\t\tif waitTime > 5*time.Minute {\n\t\t\t\t\twaitTime = 5 * time.Minute // 最大等待5分钟\n\t\t\t\t}\n\t\t\t\tlog.Printf(\"⏳ [worker-%d] [%s] 401刷新失败 (%d/%d次)，%v后重试\", id, acc.Data.Email, failCount, maxRetry, waitTime)\n\t\t\t\ttime.Sleep(waitTime)\n\n\t\t\t\tp.mu.Lock()\n\t\t\t\tp.pendingAccounts = append(p.pendingAccounts, acc)\n\t\t\t\tp.mu.Unlock()\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 冷却中：直接标记就绪\n\t\t\tif strings.Contains(errMsg, \"刷新冷却中\") {\n\t\t\t\tacc.Mu.Lock()\n\t\t\t\tacc.Refreshed = true\n\t\t\t\tacc.Status = StatusReady\n\t\t\t\tacc.Mu.Unlock()\n\t\t\t\tp.MarkReady(acc)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 其他错误：累计失败次数\n\t\t\tacc.Mu.Lock()\n\t\t\tacc.FailCount++\n\t\t\tfailCount := acc.FailCount\n\t\t\tacc.Mu.Unlock()\n\n\t\t\tif failCount >= MaxFailCount {\n\t\t\t\tlog.Printf(\"❌ [worker-%d] [%s] 连续失败 %d 次，移除账号: %v\", id, acc.Data.Email, failCount, err)\n\t\t\t\tacc.Mu.Lock()\n\t\t\t\tacc.Status = StatusInvalid\n\t\t\t\tacc.Mu.Unlock()\n\t\t\t\tp.RemoveAccount(acc)\n\t\t\t} else {\n\t\t\t\tlog.Printf(\"⚠️ [worker-%d] [%s] 刷新失败 (%d/%d): %v\", id, acc.Data.Email, failCount, MaxFailCount, err)\n\t\t\t\t// 延迟后重试\n\t\t\t\ttime.Sleep(time.Duration(failCount) * 5 * time.Second)\n\t\t\t\tp.mu.Lock()\n\t\t\t\tp.pendingAccounts = append(p.pendingAccounts, acc)\n\t\t\t\tp.mu.Unlock()\n\t\t\t}\n\t\t} else {\n\t\t\t// 刷新成功：重置失败计数\n\t\t\tacc.Mu.Lock()\n\t\t\tacc.FailCount = 0\n\t\t\tacc.Status = StatusReady\n\t\t\tacc.Mu.Unlock()\n\n\t\t\tif err := acc.SaveToFile(); err != nil {\n\t\t\t\tlog.Printf(\"⚠️ [%s] 写回文件失败: %v\", acc.Data.Email, err)\n\t\t\t}\n\t\t\tp.MarkReady(acc)\n\t\t}\n\t}\n}\n\nfunc (p *AccountPool) scanWorker() {\n\tticker := time.NewTicker(p.refreshInterval)\n\tfileScanTicker := time.NewTicker(5 * time.Minute)\n\tdefer ticker.Stop()\n\tdefer fileScanTicker.Stop()\n\n\tfor {\n\t\tselect {\n\t\tcase <-p.stopChan:\n\t\t\treturn\n\t\tcase <-fileScanTicker.C:\n\t\t\tp.Load(DataDir)\n\t\tcase <-ticker.C:\n\t\t\tp.RefreshExpiredAccounts()\n\t\t}\n\t}\n}\n\n// RefreshExpiredAccounts 刷新即将过期的账号\nfunc (p *AccountPool) RefreshExpiredAccounts() {\n\tp.mu.Lock()\n\tdefer p.mu.Unlock()\n\n\tvar stillReady []*Account\n\trefreshed := 0\n\tnow := time.Now()\n\n\tfor _, acc := range p.readyAccounts {\n\t\tacc.Mu.Lock()\n\t\tjwtExpires := acc.JWTExpires\n\t\tlastRefresh := acc.LastRefresh\n\t\tacc.Mu.Unlock()\n\n\t\tneedsRefresh := jwtExpires.IsZero() || now.Add(JWTRefreshThreshold).After(jwtExpires)\n\t\tinCooldown := now.Sub(lastRefresh) < RefreshCooldown\n\n\t\tif needsRefresh && !inCooldown {\n\t\t\tacc.Mu.Lock()\n\t\t\tacc.Refreshed = false\n\t\t\tacc.Mu.Unlock()\n\t\t\tp.pendingAccounts = append(p.pendingAccounts, acc)\n\t\t\trefreshed++\n\t\t} else {\n\t\t\tstillReady = append(stillReady, acc)\n\t\t}\n\t}\n\n\tp.readyAccounts = stillReady\n\tif refreshed > 0 {\n\t\tlog.Printf(\"🔄 扫描刷新: %d 个账号JWT即将过期\", refreshed)\n\t}\n}\n\nfunc (p *AccountPool) RefreshAllAccounts() {\n\tp.mu.Lock()\n\tdefer p.mu.Unlock()\n\n\tvar stillReady []*Account\n\trefreshed, skipped := 0, 0\n\n\tfor _, acc := range p.readyAccounts {\n\t\tif time.Since(acc.LastRefresh) < RefreshCooldown {\n\t\t\tstillReady = append(stillReady, acc)\n\t\t\tskipped++\n\t\t\tcontinue\n\t\t}\n\t\tacc.Refreshed = false\n\t\tacc.JWTExpires = time.Time{}\n\t\tp.pendingAccounts = append(p.pendingAccounts, acc)\n\t\trefreshed++\n\t}\n\n\tp.readyAccounts = stillReady\n\tif refreshed > 0 {\n\t\tlog.Printf(\"🔄 全量刷新: %d 个账号已加入刷新队列，%d 个在冷却中跳过\", refreshed, skipped)\n\t}\n}\n\n// checkAndUpdateDailyCount 检查并更新每日计数，返回是否超限\nfunc (acc *Account) checkAndUpdateDailyCount() bool {\n\ttoday := time.Now().Format(\"2006-01-02\")\n\tif acc.DailyCountDate != today {\n\t\t// 新的一天，重置计数\n\t\tacc.DailyCountDate = today\n\t\tacc.DailyCount = 0\n\t}\n\t// 检查是否超过每日限制\n\tif DailyLimit > 0 && acc.DailyCount >= DailyLimit {\n\t\treturn true // 超限\n\t}\n\tacc.DailyCount++\n\treturn false\n}\n\n// GetDailyUsage 获取每日使用情况\nfunc (acc *Account) GetDailyUsage() (count int, limit int, date string) {\n\tacc.Mu.Lock()\n\tdefer acc.Mu.Unlock()\n\ttoday := time.Now().Format(\"2006-01-02\")\n\tif acc.DailyCountDate != today {\n\t\treturn 0, DailyLimit, today\n\t}\n\treturn acc.DailyCount, DailyLimit, acc.DailyCountDate\n}\n\nfunc (p *AccountPool) Next() *Account {\n\tp.mu.RLock()\n\tdefer p.mu.RUnlock()\n\n\tif len(p.readyAccounts) == 0 {\n\t\treturn nil\n\t}\n\n\tn := len(p.readyAccounts)\n\tstartIdx := atomic.AddUint64(&p.index, 1) - 1\n\tnow := time.Now()\n\n\tvar bestAccount *Account\n\tvar oldestUsed time.Time\n\tvar allExceededDaily bool = true\n\n\t// 第一轮：找不在使用冷却中且未超日限的账号\n\tfor i := 0; i < n; i++ {\n\t\tacc := p.readyAccounts[(startIdx+uint64(i))%uint64(n)]\n\t\tacc.Mu.Lock()\n\t\tinUseCooldown := now.Sub(acc.LastUsed) < UseCooldown\n\t\tlastUsed := acc.LastUsed\n\n\t\t// 检查每日限制（不更新计数）\n\t\ttoday := now.Format(\"2006-01-02\")\n\t\tdailyCount := acc.DailyCount\n\t\tif acc.DailyCountDate != today {\n\t\t\tdailyCount = 0\n\t\t}\n\t\texceededDaily := DailyLimit > 0 && dailyCount >= DailyLimit\n\t\tacc.Mu.Unlock()\n\n\t\tif exceededDaily {\n\t\t\tcontinue // 跳过已达每日限制的账号\n\t\t}\n\t\tallExceededDaily = false\n\n\t\tif !inUseCooldown {\n\t\t\t// 找到可用账号，标记使用时间并更新每日计数\n\t\t\tacc.Mu.Lock()\n\t\t\tacc.LastUsed = now\n\t\t\tacc.TotalCount++\n\t\t\tacc.checkAndUpdateDailyCount()\n\t\t\tacc.Mu.Unlock()\n\t\t\tatomic.AddInt64(&p.totalRequests, 1)\n\t\t\treturn acc\n\t\t}\n\n\t\t// 记录最久未使用的账号作为备选\n\t\tif bestAccount == nil || lastUsed.Before(oldestUsed) {\n\t\t\tbestAccount = acc\n\t\t\toldestUsed = lastUsed\n\t\t}\n\t}\n\n\t// 所有账号都超过每日限制\n\tif allExceededDaily {\n\t\tlog.Printf(\"⚠️ 所有账号已达每日调用上限 (%d次/天)\", DailyLimit)\n\t\treturn nil\n\t}\n\n\t// 所有未超限的账号都在冷却中，返回最久未使用的\n\tif bestAccount != nil {\n\t\tbestAccount.Mu.Lock()\n\t\tbestAccount.LastUsed = now\n\t\tbestAccount.TotalCount++\n\t\tbestAccount.checkAndUpdateDailyCount()\n\t\tbestAccount.Mu.Unlock()\n\t\tatomic.AddInt64(&p.totalRequests, 1)\n\t\tlog.Printf(\"⏳ 所有账号在使用冷却中，选择最久未用: %s\", bestAccount.Data.Email)\n\t}\n\treturn bestAccount\n}\n\n// MarkUsed 标记账号已使用（成功）\nfunc (p *AccountPool) MarkUsed(acc *Account, success bool) {\n\tif acc == nil {\n\t\treturn\n\t}\n\tacc.Mu.Lock()\n\tdefer acc.Mu.Unlock()\n\n\tif success {\n\t\tacc.SuccessCount++\n\t\tacc.FailCount = 0 // 重置连续失败\n\t\tatomic.AddInt64(&p.totalSuccess, 1)\n\t} else {\n\t\tacc.FailCount++\n\t\tatomic.AddInt64(&p.totalFailed, 1)\n\t}\n}\n\n// MarkNeedsRefresh 标记账号需要刷新（遇到401/403等）\nfunc (p *AccountPool) MarkNeedsRefresh(acc *Account) {\n\tif acc == nil {\n\t\treturn\n\t}\n\tacc.Mu.Lock()\n\tacc.LastRefresh = time.Time{} // 重置刷新时间，强制刷新\n\tacc.Mu.Unlock()\n\tp.MarkPending(acc)\n}\n\nfunc (p *AccountPool) Count() int { p.mu.RLock(); defer p.mu.RUnlock(); return len(p.readyAccounts) }\nfunc (p *AccountPool) PendingCount() int {\n\tp.mu.RLock()\n\tdefer p.mu.RUnlock()\n\treturn len(p.pendingAccounts)\n}\nfunc (p *AccountPool) ReadyCount() int {\n\tp.mu.RLock()\n\tdefer p.mu.RUnlock()\n\treturn len(p.readyAccounts)\n}\nfunc (p *AccountPool) TotalCount() int {\n\tp.mu.RLock()\n\tdefer p.mu.RUnlock()\n\treturn len(p.readyAccounts) + len(p.pendingAccounts)\n}\n\n// Stats 返回号池统计信息\nfunc (p *AccountPool) Stats() map[string]interface{} {\n\tp.mu.RLock()\n\tdefer p.mu.RUnlock()\n\n\ttotalSuccess := atomic.LoadInt64(&p.totalSuccess)\n\ttotalFailed := atomic.LoadInt64(&p.totalFailed)\n\ttotalRequests := atomic.LoadInt64(&p.totalRequests)\n\n\tsuccessRate := float64(0)\n\tif totalRequests > 0 {\n\t\tsuccessRate = float64(totalSuccess) / float64(totalRequests) * 100\n\t}\n\n\t// 统计每日可用账号数\n\ttoday := time.Now().Format(\"2006-01-02\")\n\tavailableToday := 0\n\texceededToday := 0\n\tfor _, acc := range p.readyAccounts {\n\t\tacc.Mu.Lock()\n\t\tdailyCount := acc.DailyCount\n\t\tif acc.DailyCountDate != today {\n\t\t\tdailyCount = 0\n\t\t}\n\t\tacc.Mu.Unlock()\n\t\tif DailyLimit == 0 || dailyCount < DailyLimit {\n\t\t\tavailableToday++\n\t\t} else {\n\t\t\texceededToday++\n\t\t}\n\t}\n\n\treturn map[string]interface{}{\n\t\t\"ready\":           len(p.readyAccounts),\n\t\t\"pending\":         len(p.pendingAccounts),\n\t\t\"total\":           len(p.readyAccounts) + len(p.pendingAccounts),\n\t\t\"available_today\": availableToday,\n\t\t\"exceeded_today\":  exceededToday,\n\t\t\"total_requests\":  totalRequests,\n\t\t\"total_success\":   totalSuccess,\n\t\t\"total_failed\":    totalFailed,\n\t\t\"success_rate\":    fmt.Sprintf(\"%.1f%%\", successRate),\n\t\t\"daily_limit\":     DailyLimit,\n\t\t\"cooldowns\": map[string]interface{}{\n\t\t\t\"refresh_sec\": int(RefreshCooldown.Seconds()),\n\t\t\t\"use_sec\":     int(UseCooldown.Seconds()),\n\t\t},\n\t}\n}\n\n// AccountInfo 账号信息（用于API返回）\ntype AccountInfo struct {\n\tEmail          string    `json:\"email\"`\n\tStatus         string    `json:\"status\"`\n\tLastRefresh    time.Time `json:\"last_refresh\"`\n\tLastUsed       time.Time `json:\"last_used\"`\n\tFailCount      int       `json:\"fail_count\"`\n\tSuccessCount   int       `json:\"success_count\"`\n\tTotalCount     int       `json:\"total_count\"`\n\tDailyCount     int       `json:\"daily_count\"`\n\tDailyLimit     int       `json:\"daily_limit\"`\n\tDailyRemaining int       `json:\"daily_remaining\"`\n\tJWTExpires     time.Time `json:\"jwt_expires\"`\n}\n\n// ListAccounts 列出所有账号信息\nfunc (p *AccountPool) ListAccounts() []AccountInfo {\n\tp.mu.RLock()\n\tdefer p.mu.RUnlock()\n\n\tvar accounts []AccountInfo\n\tstatusNames := map[AccountStatus]string{\n\t\tStatusPending:  \"pending\",\n\t\tStatusReady:    \"ready\",\n\t\tStatusCooldown: \"cooldown\",\n\t\tStatusInvalid:  \"invalid\",\n\t}\n\n\ttoday := time.Now().Format(\"2006-01-02\")\n\taddAccounts := func(list []*Account) {\n\t\tfor _, acc := range list {\n\t\t\tacc.Mu.Lock()\n\t\t\tdailyCount := acc.DailyCount\n\t\t\tif acc.DailyCountDate != today {\n\t\t\t\tdailyCount = 0\n\t\t\t}\n\t\t\tdailyRemaining := DailyLimit - dailyCount\n\t\t\tif DailyLimit == 0 {\n\t\t\t\tdailyRemaining = -1 // -1 表示无限制\n\t\t\t} else if dailyRemaining < 0 {\n\t\t\t\tdailyRemaining = 0\n\t\t\t}\n\t\t\tinfo := AccountInfo{\n\t\t\t\tEmail:          acc.Data.Email,\n\t\t\t\tStatus:         statusNames[acc.Status],\n\t\t\t\tLastRefresh:    acc.LastRefresh,\n\t\t\t\tLastUsed:       acc.LastUsed,\n\t\t\t\tFailCount:      acc.FailCount,\n\t\t\t\tSuccessCount:   acc.SuccessCount,\n\t\t\t\tTotalCount:     acc.TotalCount,\n\t\t\t\tDailyCount:     dailyCount,\n\t\t\t\tDailyLimit:     DailyLimit,\n\t\t\t\tDailyRemaining: dailyRemaining,\n\t\t\t\tJWTExpires:     acc.JWTExpires,\n\t\t\t}\n\t\t\tacc.Mu.Unlock()\n\t\t\taccounts = append(accounts, info)\n\t\t}\n\t}\n\n\taddAccounts(p.readyAccounts)\n\taddAccounts(p.pendingAccounts)\n\n\treturn accounts\n}\n\n// ForceRefreshAll 强制刷新所有账号\nfunc (p *AccountPool) ForceRefreshAll() int {\n\tp.mu.Lock()\n\tdefer p.mu.Unlock()\n\n\tcount := 0\n\tfor _, acc := range p.readyAccounts {\n\t\tacc.Mu.Lock()\n\t\tacc.Refreshed = false\n\t\tacc.JWTExpires = time.Time{}\n\t\tacc.LastRefresh = time.Time{} // 强制跳过冷却\n\t\tacc.Mu.Unlock()\n\t\tp.pendingAccounts = append(p.pendingAccounts, acc)\n\t\tcount++\n\t}\n\tp.readyAccounts = nil\n\n\tlog.Printf(\"🔄 强制刷新: %d 个账号已加入刷新队列\", count)\n\treturn count\n}\n\nfunc urlsafeB64Encode(data []byte) string {\n\treturn strings.TrimRight(base64.URLEncoding.EncodeToString(data), \"=\")\n}\n\nfunc kqEncode(s string) string {\n\tvar b []byte\n\tfor _, ch := range s {\n\t\tv := int(ch)\n\t\tif v > 255 {\n\t\t\tb = append(b, byte(v&255), byte(v>>8))\n\t\t} else {\n\t\t\tb = append(b, byte(v))\n\t\t}\n\t}\n\treturn urlsafeB64Encode(b)\n}\n\nfunc createJWT(keyBytes []byte, keyID, csesidx string) string {\n\tnow := time.Now().Unix()\n\theader := map[string]interface{}{\"alg\": \"HS256\", \"typ\": \"JWT\", \"kid\": keyID}\n\tpayload := map[string]interface{}{\n\t\t\"iss\": \"https://business.gemini.google\",\n\t\t\"aud\": \"https://biz-discoveryengine.googleapis.com\",\n\t\t\"sub\": fmt.Sprintf(\"csesidx/%s\", csesidx),\n\t\t\"iat\": now, \"exp\": now + 300, \"nbf\": now,\n\t}\n\n\theaderJSON, _ := json.Marshal(header)\n\tpayloadJSON, _ := json.Marshal(payload)\n\tmessage := kqEncode(string(headerJSON)) + \".\" + kqEncode(string(payloadJSON))\n\n\th := hmac.New(sha256.New, keyBytes)\n\th.Write([]byte(message))\n\treturn message + \".\" + urlsafeB64Encode(h.Sum(nil))\n}\n\nfunc extractCSESIDX(auth string) string {\n\tparts := strings.Split(auth, \" \")\n\tif len(parts) != 2 {\n\t\treturn \"\"\n\t}\n\tjwtParts := strings.Split(parts[1], \".\")\n\tif len(jwtParts) != 3 {\n\t\treturn \"\"\n\t}\n\n\tpayload, err := base64.RawURLEncoding.DecodeString(jwtParts[1])\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\tvar claims struct {\n\t\tSub string `json:\"sub\"`\n\t}\n\tif err := json.Unmarshal(payload, &claims); err != nil {\n\t\treturn \"\"\n\t}\n\n\tif strings.HasPrefix(claims.Sub, \"csesidx/\") {\n\t\treturn strings.TrimPrefix(claims.Sub, \"csesidx/\")\n\t}\n\treturn \"\"\n}\n\n// ==================== 账号操作 ====================\n\nfunc (acc *Account) getCookie(name string) string {\n\tfor _, c := range acc.Data.Cookies {\n\t\tif c.Name == name {\n\t\t\treturn c.Value\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// RefreshJWT 刷新JWT\nfunc (acc *Account) RefreshJWT() error {\n\tacc.Mu.Lock()\n\tdefer acc.Mu.Unlock()\n\n\t// 检查JWT是否仍有效\n\tif time.Now().Before(acc.JWTExpires) {\n\t\treturn nil\n\t}\n\n\t// 检查刷新冷却\n\tif time.Since(acc.LastRefresh) < RefreshCooldown {\n\t\treturn fmt.Errorf(\"刷新冷却中，剩余 %.0f 秒\", (RefreshCooldown - time.Since(acc.LastRefresh)).Seconds())\n\t}\n\n\t// 获取必要的Cookie\n\tsecureSES := acc.getCookie(\"__Secure-C_SES\")\n\thostOSES := acc.getCookie(\"__Host-C_OSES\")\n\n\t// 验证Cookie是否存在\n\tif secureSES == \"\" {\n\t\treturn fmt.Errorf(\"账号失效: 缺少 __Secure-C_SES Cookie\")\n\t}\n\n\t// 构建Cookie字符串\n\tcookie := fmt.Sprintf(\"__Secure-C_SES=%s\", secureSES)\n\tif hostOSES != \"\" {\n\t\tcookie += fmt.Sprintf(\"; __Host-C_OSES=%s\", hostOSES)\n\t}\n\n\t// 添加其他可能需要的Cookie\n\tfor _, c := range acc.Data.Cookies {\n\t\tif c.Name != \"__Secure-C_SES\" && c.Name != \"__Host-C_OSES\" {\n\t\t\tif strings.HasPrefix(c.Name, \"__Secure-\") || strings.HasPrefix(c.Name, \"__Host-\") {\n\t\t\t\tcookie += fmt.Sprintf(\"; %s=%s\", c.Name, c.Value)\n\t\t\t}\n\t\t}\n\t}\n\n\treq, _ := http.NewRequest(\"GET\", \"https://business.gemini.google/auth/getoxsrf\", nil)\n\tq := req.URL.Query()\n\tq.Add(\"csesidx\", acc.CSESIDX)\n\treq.URL.RawQuery = q.Encode()\n\n\treq.Header.Set(\"Cookie\", cookie)\n\treq.Header.Set(\"User-Agent\", \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36\")\n\treq.Header.Set(\"Referer\", \"https://business.gemini.google/\")\n\n\tresp, err := HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"getoxsrf 请求失败: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != 200 {\n\t\tbody, _ := readResponseBody(resp)\n\t\tbodyStr := string(body)\n\t\tif len(bodyStr) > 200 {\n\t\t\tbodyStr = bodyStr[:200]\n\t\t}\n\n\t\t// 详细的错误分类\n\t\tswitch resp.StatusCode {\n\t\tcase 401, 403:\n\t\t\t// 认证失败，Cookie可能过期\n\t\t\treturn fmt.Errorf(\"账号失效: %d - Cookie可能已过期\", resp.StatusCode)\n\t\tcase 429:\n\t\t\t// 速率限制\n\t\t\treturn fmt.Errorf(\"请求频率过高: %d\", resp.StatusCode)\n\t\tcase 500, 502, 503, 504:\n\t\t\t// 服务器错误，可能是临时的\n\t\t\treturn fmt.Errorf(\"服务器错误: %d, 稍后重试\", resp.StatusCode)\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"getoxsrf 失败: %d %s\", resp.StatusCode, bodyStr)\n\t\t}\n\t}\n\n\tbody, _ := readResponseBody(resp)\n\ttxt := strings.TrimPrefix(string(body), \")]}'\")\n\ttxt = strings.TrimSpace(txt)\n\n\tvar data struct {\n\t\tXsrfToken string `json:\"xsrfToken\"`\n\t\tKeyID     string `json:\"keyId\"`\n\t}\n\tif err := json.Unmarshal([]byte(txt), &data); err != nil {\n\t\treturn fmt.Errorf(\"解析 xsrf 响应失败: %w\", err)\n\t}\n\n\ttoken := data.XsrfToken\n\tswitch len(token) % 4 {\n\tcase 2:\n\t\ttoken += \"==\"\n\tcase 3:\n\t\ttoken += \"=\"\n\t}\n\tkeyBytes, err := base64.URLEncoding.DecodeString(token)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"解码 xsrfToken 失败: %w\", err)\n\t}\n\n\tacc.JWT = createJWT(keyBytes, data.KeyID, acc.CSESIDX)\n\tacc.JWTExpires = time.Now().Add(JwtTTL)\n\tacc.LastRefresh = time.Now()\n\n\tif acc.ConfigID == \"\" {\n\t\tconfigID, err := acc.fetchConfigID()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"获取 configId 失败: %w\", err)\n\t\t}\n\t\tacc.ConfigID = configID\n\t}\n\treturn nil\n}\n\n// GetJWT 获取JWT\nfunc (acc *Account) GetJWT() (string, string, error) {\n\tacc.Mu.Lock()\n\tdefer acc.Mu.Unlock()\n\tif acc.JWT == \"\" {\n\t\treturn \"\", \"\", fmt.Errorf(\"JWT 为空，账号未刷新\")\n\t}\n\treturn acc.JWT, acc.ConfigID, nil\n}\n\nfunc (acc *Account) fetchConfigID() (string, error) {\n\tif acc.Data.ConfigID != \"\" {\n\t\treturn acc.Data.ConfigID, nil\n\t}\n\tif DefaultConfig != \"\" {\n\t\treturn DefaultConfig, nil\n\t}\n\treturn \"\", fmt.Errorf(\"未配置 configId\")\n}\n"
  },
  {
    "path": "src/pool/pool_client.go",
    "content": "package pool\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"sync\"\n\t\"time\"\n\n\t\"business2api/src/logger\"\n\n\t\"github.com/gorilla/websocket\"\n)\n\ntype BrowserRegisterResult struct {\n\tSuccess       bool\n\tEmail         string\n\tFullName      string\n\tSecureCookies []Cookie\n\tAuthorization string\n\tConfigID      string\n\tCSESIDX       string\n\tError         error\n}\n\n// RunBrowserRegisterFunc 注册函数类型\ntype RunBrowserRegisterFunc func(headless bool, proxy string, id int) *BrowserRegisterResult\n\n// 客户端版本\nconst ClientVersion = \"2.0.0\"\n\nvar (\n\tRunBrowserRegister RunBrowserRegisterFunc\n\tClientHeadless     bool\n\tClientProxy        string\n\tGetClientProxy     func() string                    // 获取代理的函数\n\tReleaseProxy       func(proxyURL string)            // 释放代理的函数\n\tDefaultProxyCount  = 3                              // 客户端模式默认启动的代理实例数\n\tIsProxyReady       func() bool                      // 检查代理是否就绪\n\tWaitProxyReady     func(timeout time.Duration) bool // 等待代理就绪\n\tGetHealthyCount    func() int                       // 获取健康代理数量\n\tproxyReadyTimeout  = 30 * time.Second               // 代理就绪超时时间（减少等待）\n)\n\n// PoolClient 号池客户端\ntype PoolClient struct {\n\tconfig    PoolServerConfig\n\tconn      *websocket.Conn\n\tsend      chan []byte\n\tdone      chan struct{}\n\treconnect chan struct{}\n\tstopPump  chan struct{} // 停止当前pump\n\tmu        sync.Mutex\n\twriteMu   sync.Mutex // WebSocket写入锁\n\tisRunning bool\n\ttaskSem   chan struct{} // 任务并发信号量\n}\n\n// NewPoolClient 创建号池客户端\nfunc NewPoolClient(config PoolServerConfig) *PoolClient {\n\tthreads := config.ClientThreads\n\tif threads <= 0 {\n\t\tthreads = 1\n\t}\n\treturn &PoolClient{\n\t\tconfig:    config,\n\t\tsend:      make(chan []byte, 256),\n\t\tdone:      make(chan struct{}),\n\t\treconnect: make(chan struct{}, 1),\n\t\ttaskSem:   make(chan struct{}, threads),\n\t}\n}\n\n// Start 启动客户端\nfunc (pc *PoolClient) Start() error {\n\tpc.mu.Lock()\n\tpc.isRunning = true\n\tpc.mu.Unlock()\n\n\t// 连接循环\n\tfor pc.isRunning {\n\t\tif err := pc.connect(); err != nil {\n\t\t\tlogger.Warn(\"连接服务器失败: %v, 5秒后重试...\", err)\n\t\t\ttime.Sleep(5 * time.Second)\n\t\t\tcontinue\n\t\t}\n\t\tpc.work()\n\t\tselect {\n\t\tcase <-pc.done:\n\t\t\treturn nil\n\t\tcase <-pc.reconnect:\n\t\t\tlog.Printf(\"[PoolClient] 准备重连...\")\n\t\t\ttime.Sleep(2 * time.Second)\n\t\t}\n\t}\n\n\treturn nil\n}\nfunc (pc *PoolClient) Stop() {\n\tpc.mu.Lock()\n\tpc.isRunning = false\n\tpc.mu.Unlock()\n\tclose(pc.done)\n}\n\n// connect 连接到服务器\nfunc (pc *PoolClient) connect() error {\n\tu, err := url.Parse(pc.config.ServerAddr)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"解析服务器地址失败: %w\", err)\n\t}\n\twsScheme := \"ws\"\n\tif u.Scheme == \"https\" {\n\t\twsScheme = \"wss\"\n\t}\n\twsURL := fmt.Sprintf(\"%s://%s/ws\", wsScheme, u.Host)\n\tif pc.config.Secret != \"\" {\n\t\twsURL += \"?secret=\" + pc.config.Secret\n\t}\n\n\tlogger.Debug(\"连接到 %s\", wsURL)\n\n\tconn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"WebSocket连接失败: %w\", err)\n\t}\n\n\tpc.conn = conn\n\tthreads := pc.config.ClientThreads\n\tif threads <= 0 {\n\t\tthreads = 1\n\t}\n\tpc.sendMessage(WSMessage{\n\t\tType:      WSMsgClientReady,\n\t\tVersion:   ClientVersion,\n\t\tTimestamp: time.Now().Unix(),\n\t\tData: map[string]interface{}{\n\t\t\t\"max_threads\":      threads,\n\t\t\t\"client_version\":   ClientVersion,\n\t\t\t\"protocol_version\": ProtocolVersion,\n\t\t},\n\t})\n\n\treturn nil\n}\nfunc (pc *PoolClient) work() {\n\tpc.stopPump = make(chan struct{})\n\tgo pc.writePump()     // 消息发送\n\tgo pc.heartbeatPump() // 独立心跳保活\n\tpc.readPump()         // 消息读取（阻塞）\n\tclose(pc.stopPump)\n}\n\nfunc (pc *PoolClient) heartbeatPump() {\n\tticker := time.NewTicker(15 * time.Second)\n\tdefer ticker.Stop()\n\n\tfor {\n\t\tselect {\n\t\tcase <-pc.done:\n\t\t\treturn\n\t\tcase <-pc.stopPump:\n\t\t\treturn\n\t\tcase <-ticker.C:\n\t\t\t// 发送心跳保持连接活跃\n\t\t\tpc.writeMu.Lock()\n\t\t\tpc.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))\n\t\t\terr := pc.conn.WriteMessage(websocket.PingMessage, nil)\n\t\t\tpc.writeMu.Unlock()\n\t\t\tif err != nil {\n\t\t\t\tlogger.Debug(\"[PoolClient] 心跳发送失败: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n}\nfunc (pc *PoolClient) writePump() {\n\tregisterTicker := time.NewTicker(30 * time.Minute)\n\tdefer registerTicker.Stop()\n\tgo pc.doPeriodicRegister()\n\n\tfor {\n\t\tselect {\n\t\tcase <-pc.done:\n\t\t\treturn\n\t\tcase <-pc.stopPump:\n\t\t\treturn\n\t\tcase message := <-pc.send:\n\t\t\tpc.writeMu.Lock()\n\t\t\tpc.conn.SetWriteDeadline(time.Now().Add(30 * time.Second))\n\t\t\terr := pc.conn.WriteMessage(websocket.TextMessage, message)\n\t\t\tpc.writeMu.Unlock()\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"[PoolClient] 发送消息失败: %v\", err)\n\t\t\t\tpc.triggerReconnect()\n\t\t\t\treturn\n\t\t\t}\n\t\tcase <-registerTicker.C:\n\t\t\t// 每30分钟注册一轮\n\t\t\tgo pc.doPeriodicRegister()\n\t\t}\n\t}\n}\n\nfunc (pc *PoolClient) doPeriodicRegister() {\n\tmaxThreads := pc.config.ClientThreads\n\tif maxThreads <= 0 {\n\t\tmaxThreads = 1\n\t}\n\n\tlogger.Info(\"[定时注册] 开始注册 %d 个账号\", maxThreads)\n\n\tfor i := 0; i < maxThreads; i++ {\n\t\tgo pc.handleRegisterTask(map[string]interface{}{\"count\": 1})\n\t}\n}\n\n// readPump 读取消息\nfunc (pc *PoolClient) readPump() {\n\tdefer func() {\n\t\tpc.conn.Close()\n\t\tpc.triggerReconnect()\n\t}()\n\n\t// 延长读取超时到240秒（4分钟），确保不会因为任务执行而断开\n\tpc.conn.SetReadDeadline(time.Now().Add(240 * time.Second))\n\tpc.conn.SetPongHandler(func(string) error {\n\t\tpc.conn.SetReadDeadline(time.Now().Add(240 * time.Second))\n\t\treturn nil\n\t})\n\n\tfor {\n\t\t_, message, err := pc.conn.ReadMessage()\n\t\tif err != nil {\n\t\t\tif websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {\n\t\t\t\tlog.Printf(\"[PoolClient] 读取错误: %v\", err)\n\t\t\t}\n\t\t\treturn\n\t\t}\n\n\t\t// 收到消息时重置读取超时\n\t\tpc.conn.SetReadDeadline(time.Now().Add(240 * time.Second))\n\n\t\tvar msg WSMessage\n\t\tif err := json.Unmarshal(message, &msg); err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tpc.handleMessage(msg)\n\t}\n}\n\n// handleMessage 处理服务器消息\nfunc (pc *PoolClient) handleMessage(msg WSMessage) {\n\tswitch msg.Type {\n\tcase WSMsgHeartbeat:\n\t\t// 立即响应心跳\n\t\tpc.sendMessage(WSMessage{\n\t\t\tType:      WSMsgHeartbeatAck,\n\t\t\tTimestamp: time.Now().Unix(),\n\t\t})\n\n\tcase WSMsgTaskRegister:\n\t\t// 注册任务（独立心跳线程已保活，无需额外处理）\n\t\tgo pc.handleRegisterTask(msg.Data)\n\n\tcase WSMsgTaskRefresh:\n\t\t// 续期任务\n\t\tgo pc.handleRefreshTask(msg.Data)\n\n\tcase WSMsgStatus:\n\t\t// 状态同步，自主判断是否需要注册\n\t\tgo pc.handleStatusAndRegister(msg.Data)\n\t}\n}\n\n// handleRegisterTask 处理注册任务（每次只处理1个）\nfunc (pc *PoolClient) handleRegisterTask(data map[string]interface{}) {\n\t// 获取信号量（控制并发数）\n\tpc.taskSem <- struct{}{}\n\tdefer func() { <-pc.taskSem }()\n\ttaskID := time.Now().UnixNano() % 1000\n\tresultSent := false\n\tdefer func() {\n\t\tif !resultSent {\n\t\t\tif r := recover(); r != nil {\n\t\t\t\tlogger.Error(\"[注册 %d] handleRegisterTask panic: %v\", taskID, r)\n\t\t\t\tpc.sendRegisterResult(false, \"\", fmt.Sprintf(\"client panic: %v\", r))\n\t\t\t} else {\n\t\t\t\tpc.sendRegisterResult(false, \"\", \"task incomplete\")\n\t\t\t}\n\t\t}\n\t}()\n\n\tlogger.Info(\"收到注册任务 [%d]\", taskID)\n\tif GetHealthyCount != nil && GetHealthyCount() >= 1 {\n\t\t// 已有健康代理，直接开始\n\t} else if WaitProxyReady != nil {\n\t\tif !WaitProxyReady(proxyReadyTimeout) {\n\t\t\tlogger.Warn(\"代理未就绪，使用静态代理: %s\", ClientProxy)\n\t\t}\n\t}\n\n\t// 获取代理（优先使用代理池）\n\tcurrentProxy := ClientProxy\n\tif GetClientProxy != nil {\n\t\tcurrentProxy = GetClientProxy()\n\t}\n\tlogger.Info(\"[注册 %d] 使用代理: %s\", taskID, currentProxy)\n\tresult := RunBrowserRegister(ClientHeadless, currentProxy, int(taskID))\n\n\t// 任务完成后释放代理\n\tif ReleaseProxy != nil && currentProxy != \"\" && currentProxy != ClientProxy {\n\t\tReleaseProxy(currentProxy)\n\t}\n\n\tif result.Success {\n\t\t// 上传账号到服务器\n\t\tif err := pc.uploadAccount(result, true); err != nil {\n\t\t\tlogger.Error(\"上传注册结果失败: %v\", err)\n\t\t\tpc.sendRegisterResult(false, \"\", err.Error())\n\t\t} else {\n\t\t\tlogger.Info(\"✅ 注册成功: %s\", result.Email)\n\t\t\tpc.sendRegisterResult(true, result.Email, \"\")\n\t\t}\n\t} else {\n\t\terrMsg := \"未知错误\"\n\t\tif result.Error != nil {\n\t\t\terrMsg = result.Error.Error()\n\t\t}\n\t\tlogger.Warn(\"❌ 注册失败: %s\", errMsg)\n\t\tpc.sendRegisterResult(false, \"\", errMsg)\n\t}\n\tresultSent = true\n}\n\nfunc (pc *PoolClient) handleStatusAndRegister(data map[string]interface{}) {\n\tneedCount := 0\n\tif v, ok := data[\"need_count\"].(float64); ok {\n\t\tneedCount = int(v)\n\t}\n\tcurrentCount := 0\n\tif v, ok := data[\"current_count\"].(float64); ok {\n\t\tcurrentCount = int(v)\n\t}\n\ttargetCount := 0\n\tif v, ok := data[\"target_count\"].(float64); ok {\n\t\ttargetCount = int(v)\n\t}\n\n\tif needCount <= 0 {\n\t\tlogger.Debug(\"[自主] 无需注册 (当前: %d, 目标: %d)\", currentCount, targetCount)\n\t\treturn\n\t}\n\n\t// 计算本客户端应该注册的数量（不超过线程数和需要数量）\n\tmaxThreads := pc.config.ClientThreads\n\tif maxThreads <= 0 {\n\t\tmaxThreads = 1\n\t}\n\tregisterCount := needCount\n\tif registerCount > maxThreads {\n\t\tregisterCount = maxThreads\n\t}\n\n\tlogger.Info(\"[自主] 需要注册 %d 个账号 (当前: %d, 目标: %d, 本次: %d)\",\n\t\tneedCount, currentCount, targetCount, registerCount)\n\n\t// 启动注册任务\n\tfor i := 0; i < registerCount; i++ {\n\t\tgo pc.handleRegisterTask(map[string]interface{}{\"count\": 1})\n\t}\n}\n\n// handleRefreshTask 处理续期任务\nfunc (pc *PoolClient) handleRefreshTask(data map[string]interface{}) {\n\t// 获取信号量\n\tpc.taskSem <- struct{}{}\n\tdefer func() { <-pc.taskSem }()\n\n\temail, _ := data[\"email\"].(string)\n\tif email == \"\" {\n\t\tlogger.Warn(\"续期任务缺少email\")\n\t\treturn\n\t}\n\n\tlogger.Info(\"收到续期任务: %s\", email)\n\n\t// 检查代理：如果已有健康代理则不等待\n\tif GetHealthyCount != nil && GetHealthyCount() >= 1 {\n\t\t// 已有健康代理，直接开始\n\t} else if WaitProxyReady != nil {\n\t\tif !WaitProxyReady(proxyReadyTimeout) {\n\t\t\tlogger.Warn(\"代理未就绪，使用静态代理: %s\", Proxy)\n\t\t}\n\t}\n\n\t// 构建临时账号对象\n\tacc := &Account{\n\t\tData: AccountData{\n\t\t\tEmail: email,\n\t\t},\n\t}\n\n\t// 从data中提取cookies\n\tif cookiesData, ok := data[\"cookies\"].([]interface{}); ok {\n\t\tfor _, c := range cookiesData {\n\t\t\tif cm, ok := c.(map[string]interface{}); ok {\n\t\t\t\tacc.Data.Cookies = append(acc.Data.Cookies, Cookie{\n\t\t\t\t\tName:   getString(cm, \"name\"),\n\t\t\t\t\tValue:  getString(cm, \"value\"),\n\t\t\t\t\tDomain: getString(cm, \"domain\"),\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t}\n\n\tif auth, ok := data[\"authorization\"].(string); ok {\n\t\tacc.Data.Authorization = auth\n\t}\n\tif configID, ok := data[\"config_id\"].(string); ok {\n\t\tacc.ConfigID = configID\n\t}\n\tif csesidx, ok := data[\"csesidx\"].(string); ok {\n\t\tacc.CSESIDX = csesidx\n\t}\n\n\t// 获取代理（优先使用代理池）\n\tcurrentProxy := Proxy\n\tif GetClientProxy != nil {\n\t\tcurrentProxy = GetClientProxy()\n\t}\n\n\t// 执行浏览器刷新\n\tresult := RefreshCookieWithBrowser(acc, BrowserRefreshHeadless, currentProxy)\n\n\t// 任务完成后释放代理\n\tif ReleaseProxy != nil && currentProxy != \"\" && currentProxy != Proxy {\n\t\tReleaseProxy(currentProxy)\n\t}\n\n\tif result.Success {\n\t\tlogger.Info(\"✅ 账号续期成功: %s\", email)\n\n\t\t// 使用刷新后的新值（如果有的话）\n\t\tauthorization := acc.Data.Authorization\n\t\tif result.Authorization != \"\" {\n\t\t\tauthorization = result.Authorization\n\t\t}\n\t\tconfigID := acc.ConfigID\n\t\tif result.ConfigID != \"\" {\n\t\t\tconfigID = result.ConfigID\n\t\t}\n\t\tcsesidx := acc.CSESIDX\n\t\tif result.CSESIDX != \"\" {\n\t\t\tcsesidx = result.CSESIDX\n\t\t}\n\n\t\t// 上传更新后的账号数据到服务器\n\t\tuploadReq := &AccountUploadRequest{\n\t\t\tEmail:         email,\n\t\t\tCookies:       result.SecureCookies,\n\t\t\tAuthorization: authorization,\n\t\t\tConfigID:      configID,\n\t\t\tCSESIDX:       csesidx,\n\t\t\tIsNew:         false,\n\t\t}\n\t\tlogger.Info(\"[%s] 上传续期数据: configID=%s, csesidx=%s, auth长度=%d\",\n\t\t\temail, configID, csesidx, len(authorization))\n\t\tif err := pc.uploadAccountData(uploadReq); err != nil {\n\t\t\tlogger.Warn(\"上传续期数据失败: %v\", err)\n\t\t}\n\t\tpc.sendRefreshResult(email, true, result.SecureCookies, \"\")\n\t} else {\n\t\terrMsg := \"未知错误\"\n\t\tif result.Error != nil {\n\t\t\terrMsg = result.Error.Error()\n\t\t}\n\t\tlogger.Warn(\"❌ 账号续期失败 %s: %s\", email, errMsg)\n\t\tpc.sendRefreshResult(email, false, nil, errMsg)\n\t}\n}\n\n// sendMessage 发送消息\nfunc (pc *PoolClient) sendMessage(msg WSMessage) {\n\tdata, err := json.Marshal(msg)\n\tif err != nil {\n\t\treturn\n\t}\n\tselect {\n\tcase pc.send <- data:\n\tdefault:\n\t\tlogger.Warn(\"发送队列已满\")\n\t}\n}\n\n// uploadAccount 上传注册结果到服务器\nfunc (pc *PoolClient) uploadAccount(result *BrowserRegisterResult, isNew bool) error {\n\t// 构建cookie字符串\n\tvar cookieStr string\n\tfor i, c := range result.SecureCookies {\n\t\tif i > 0 {\n\t\t\tcookieStr += \"; \"\n\t\t}\n\t\tcookieStr += c.Name + \"=\" + c.Value\n\t}\n\n\treq := &AccountUploadRequest{\n\t\tEmail:         result.Email,\n\t\tFullName:      result.FullName,\n\t\tCookies:       result.SecureCookies,\n\t\tCookieString:  cookieStr,\n\t\tAuthorization: result.Authorization,\n\t\tConfigID:      result.ConfigID,\n\t\tCSESIDX:       result.CSESIDX,\n\t\tIsNew:         isNew,\n\t}\n\treturn pc.uploadAccountData(req)\n}\n\n// uploadAccountData 上传账号数据到服务器（带重试）\nfunc (pc *PoolClient) uploadAccountData(req *AccountUploadRequest) error {\n\tu, err := url.Parse(pc.config.ServerAddr)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tuploadURL := fmt.Sprintf(\"%s://%s/pool/upload-account\", u.Scheme, u.Host)\n\n\tdata, err := json.Marshal(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tmaxRetries := 3\n\tvar lastErr error\n\n\tfor i := 0; i < maxRetries; i++ {\n\t\tif i > 0 {\n\t\t\tlogger.Info(\"[%s] 上传重试 %d/%d...\", req.Email, i+1, maxRetries)\n\t\t\ttime.Sleep(time.Duration(i*2) * time.Second)\n\t\t}\n\n\t\thttpReq, err := http.NewRequest(\"POST\", uploadURL, bytes.NewReader(data))\n\t\tif err != nil {\n\t\t\tlastErr = err\n\t\t\tcontinue\n\t\t}\n\n\t\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\t\tif pc.config.Secret != \"\" {\n\t\t\thttpReq.Header.Set(\"X-Pool-Secret\", pc.config.Secret)\n\t\t}\n\n\t\tclient := &http.Client{Timeout: 60 * time.Second}\n\t\tresp, err := client.Do(httpReq)\n\t\tif err != nil {\n\t\t\tlastErr = err\n\t\t\tcontinue\n\t\t}\n\n\t\tvar result map[string]interface{}\n\t\tif err := json.NewDecoder(resp.Body).Decode(&result); err != nil {\n\t\t\tresp.Body.Close()\n\t\t\tlastErr = err\n\t\t\tcontinue\n\t\t}\n\t\tresp.Body.Close()\n\n\t\tif success, ok := result[\"success\"].(bool); !ok || !success {\n\t\t\terrMsg, _ := result[\"error\"].(string)\n\t\t\tlastErr = fmt.Errorf(\"上传失败: %s\", errMsg)\n\t\t\tcontinue\n\t\t}\n\n\t\tlogger.Debug(\"账号数据已上传: %s\", req.Email)\n\t\treturn nil\n\t}\n\n\treturn fmt.Errorf(\"上传失败（重试%d次）: %v\", maxRetries, lastErr)\n}\n\n// sendRegisterResult 发送注册结果\nfunc (pc *PoolClient) sendRegisterResult(success bool, email, errMsg string) {\n\tpc.sendMessage(WSMessage{\n\t\tType:      WSMsgRegisterResult,\n\t\tTimestamp: time.Now().Unix(),\n\t\tData: map[string]interface{}{\n\t\t\t\"success\": success,\n\t\t\t\"email\":   email,\n\t\t\t\"error\":   errMsg,\n\t\t},\n\t})\n}\n\n// sendRefreshResult 发送续期结果\nfunc (pc *PoolClient) sendRefreshResult(email string, success bool, cookies []Cookie, errMsg string) {\n\tpc.sendMessage(WSMessage{\n\t\tType:      WSMsgRefreshResult,\n\t\tTimestamp: time.Now().Unix(),\n\t\tData: map[string]interface{}{\n\t\t\t\"email\":   email,\n\t\t\t\"success\": success,\n\t\t\t\"cookies\": cookies,\n\t\t\t\"error\":   errMsg,\n\t\t},\n\t})\n}\n\n// triggerReconnect 触发重连\nfunc (pc *PoolClient) triggerReconnect() {\n\tselect {\n\tcase pc.reconnect <- struct{}{}:\n\tdefault:\n\t}\n}\n\n// getString 安全获取字符串\nfunc getString(m map[string]interface{}, key string) string {\n\tif v, ok := m[key].(string); ok {\n\t\treturn v\n\t}\n\treturn \"\"\n}\n"
  },
  {
    "path": "src/pool/pool_server.go",
    "content": "package pool\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"business2api/src/logger\"\n\n\t\"github.com/gorilla/websocket\"\n)\n\n// PoolServerConfig 号池服务器配置\ntype PoolServerConfig struct {\n\tEnable        bool   `json:\"enable\"`         // 是否启用分离模式\n\tMode          string `json:\"mode\"`           // 模式: \"server\" 或 \"client\"\n\tServerAddr    string `json:\"server_addr\"`    // 服务器地址（客户端模式使用）\n\tListenAddr    string `json:\"listen_addr\"`    // WebSocket监听地址（服务端模式使用）\n\tSecret        string `json:\"secret\"`         // 通信密钥\n\tTargetCount   int    `json:\"target_count\"`   // 目标账号数量\n\tDataDir       string `json:\"data_dir\"`       // 数据目录\n\tClientThreads int    `json:\"client_threads\"` // 客户端并发线程数\n\tExpiredAction string `json:\"expired_action\"` // 账号过期处理: \"delete\"=删除, \"refresh\"=浏览器刷新, \"queue\"=排队等待\n}\n\n// WSMessageType WebSocket消息类型\ntype WSMessageType string\n\nconst (\n\t// Server -> Client\n\tWSMsgTaskRegister WSMessageType = \"task_register\" // 分配注册任务\n\tWSMsgTaskRefresh  WSMessageType = \"task_refresh\"  // 分配Cookie续期任务\n\tWSMsgHeartbeat    WSMessageType = \"heartbeat\"     // 心跳\n\tWSMsgStatus       WSMessageType = \"status\"        // 状态同步\n\n\t// Client -> Server\n\tWSMsgRegisterResult WSMessageType = \"register_result\" // 注册结果\n\tWSMsgRefreshResult  WSMessageType = \"refresh_result\"  // 续期结果\n\tWSMsgHeartbeatAck   WSMessageType = \"heartbeat_ack\"   // 心跳响应\n\tWSMsgClientReady    WSMessageType = \"client_ready\"    // 客户端就绪\n\tWSMsgRequestTask    WSMessageType = \"request_task\"    // 请求任务\n\tWSMsgQueryStatus    WSMessageType = \"query_status\"    // 查询状态（客户端自主模式）\n)\n\n// 版本信息\nconst (\n\tProtocolVersion = \"1.0\"\n\tServerVersion   = \"4.0.0\"\n)\n\n// WSMessage WebSocket消息\ntype WSMessage struct {\n\tType      WSMessageType          `json:\"type\"`\n\tVersion   string                 `json:\"version,omitempty\"`\n\tTimestamp int64                  `json:\"timestamp\"`\n\tData      map[string]interface{} `json:\"data,omitempty\"`\n}\n\n// WSClient WebSocket客户端连接\ntype WSClient struct {\n\tID            string\n\tConn          *websocket.Conn\n\tServer        *PoolServer\n\tSend          chan []byte\n\tIsAlive       bool\n\tLastPing      time.Time\n\tMaxThreads    int    // 客户端最大线程数\n\tClientVersion string // 客户端版本\n\tmu            sync.Mutex\n}\n\n// PoolServer 号池服务器（管理端）\ntype PoolServer struct {\n\tpool      *AccountPool\n\tconfig    PoolServerConfig\n\tclients   map[string]*WSClient\n\tclientsMu sync.RWMutex\n\tupgrader  websocket.Upgrader\n\n\t// 任务队列\n\tregisterQueue chan int      // 注册任务队列\n\trefreshQueue  chan *Account // 续期任务队列\n\n\t// 轮询分配\n\tnextClientIdx int // 下一个分配任务的客户端索引\n\n\t// 正在进行中的注册任务计数\n\tpendingRegisterCount int32\n}\n\n// NewPoolServer 创建号池服务器\nfunc NewPoolServer(pool *AccountPool, config PoolServerConfig) *PoolServer {\n\treturn &PoolServer{\n\t\tpool:    pool,\n\t\tconfig:  config,\n\t\tclients: make(map[string]*WSClient),\n\t\tupgrader: websocket.Upgrader{\n\t\t\tCheckOrigin:     func(r *http.Request) bool { return true },\n\t\t\tReadBufferSize:  1024,\n\t\t\tWriteBufferSize: 1024,\n\t\t},\n\t\tregisterQueue: make(chan int, 100),\n\t\trefreshQueue:  make(chan *Account, 100),\n\t}\n}\n\n// Start 启动号池服务器（独立端口模式，已弃用）\nfunc (ps *PoolServer) Start() error {\n\tmux := http.NewServeMux()\n\n\t// 鉴权中间件\n\tauthMiddleware := func(next http.HandlerFunc) http.HandlerFunc {\n\t\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\t\tif ps.config.Secret != \"\" {\n\t\t\t\tauth := r.Header.Get(\"X-Pool-Secret\")\n\t\t\t\tif auth != ps.config.Secret {\n\t\t\t\t\thttp.Error(w, \"Unauthorized\", http.StatusUnauthorized)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t\tnext(w, r)\n\t\t}\n\t}\n\n\t// WebSocket端点\n\tmux.HandleFunc(\"/ws\", ps.handleWebSocket)\n\n\t// REST API端点\n\tmux.HandleFunc(\"/pool/next\", authMiddleware(ps.handleNext))\n\tmux.HandleFunc(\"/pool/mark\", authMiddleware(ps.handleMark))\n\tmux.HandleFunc(\"/pool/refresh\", authMiddleware(ps.handleRefresh))\n\tmux.HandleFunc(\"/pool/status\", authMiddleware(ps.handleStatus))\n\tmux.HandleFunc(\"/pool/jwt\", authMiddleware(ps.handleGetJWT))\n\n\t// 任务分发\n\tmux.HandleFunc(\"/pool/queue-register\", authMiddleware(ps.handleQueueRegister))\n\tmux.HandleFunc(\"/pool/queue-refresh\", authMiddleware(ps.handleQueueRefresh))\n\n\t// 接收账号数据（客户端回传）\n\tmux.HandleFunc(\"/pool/upload-account\", authMiddleware(ps.handleUploadAccount))\n\n\t// 启动任务分发协程\n\tgo ps.taskDispatcher()\n\t// 启动心跳检测\n\tgo ps.heartbeatChecker()\n\t// 启动定时任务广播（每半小时）\n\tgo ps.periodicTaskBroadcaster()\n\t// 启动号池维护（缺账号就下发）\n\tgo ps.poolMaintainer()\n\treturn http.ListenAndServe(ps.config.ListenAddr, mux)\n}\n\n// StartBackground 启动后台任务（任务分发、心跳检测、定时广播、账号扫描）\nfunc (ps *PoolServer) StartBackground() {\n\tgo ps.taskDispatcher()\n\tgo ps.heartbeatChecker()\n\tgo ps.periodicTaskBroadcaster()\n\tgo ps.periodicAccountScanner()\n\tgo ps.poolMaintainer()\n}\n\n// HandleWS 处理 WebSocket 连接（供 gin 路由使用）\nfunc (ps *PoolServer) HandleWS(w http.ResponseWriter, r *http.Request) {\n\tps.handleWebSocket(w, r)\n}\n\nfunc (ps *PoolServer) HandleUploadAccount(w http.ResponseWriter, r *http.Request) {\n\tps.handleUploadAccount(w, r)\n}\nfunc (ps *PoolServer) handleWebSocket(w http.ResponseWriter, r *http.Request) {\n\tif ps.config.Secret != \"\" {\n\t\tsecret := r.URL.Query().Get(\"secret\")\n\t\tif secret != ps.config.Secret {\n\t\t\thttp.Error(w, \"Unauthorized\", http.StatusUnauthorized)\n\t\t\treturn\n\t\t}\n\t}\n\n\tconn, err := ps.upgrader.Upgrade(w, r, nil)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tclientID := fmt.Sprintf(\"client_%d\", time.Now().UnixNano())\n\tclient := &WSClient{\n\t\tID:       clientID,\n\t\tConn:     conn,\n\t\tServer:   ps,\n\t\tSend:     make(chan []byte, 256),\n\t\tIsAlive:  true,\n\t\tLastPing: time.Now(),\n\t}\n\n\tps.clientsMu.Lock()\n\tps.clients[clientID] = client\n\tps.clientsMu.Unlock()\n\n\tlogger.Info(\"[WS] 客户端连接: %s (当前: %d)\", clientID, len(ps.clients))\n\n\t// 启动读写协程\n\tgo client.writePump()\n\tgo client.readPump()\n}\n\n// writePump 发送消息到客户端\nfunc (c *WSClient) writePump() {\n\t// 缩短心跳间隔到20秒，确保连接保持活跃\n\tticker := time.NewTicker(20 * time.Second)\n\tdefer func() {\n\t\tticker.Stop()\n\t\tc.Conn.Close()\n\t}()\n\n\tfor {\n\t\tselect {\n\t\tcase message, ok := <-c.Send:\n\t\t\tif !ok {\n\t\t\t\tc.Conn.WriteMessage(websocket.CloseMessage, []byte{})\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// 设置写入超时\n\t\t\tc.Conn.SetWriteDeadline(time.Now().Add(30 * time.Second))\n\t\t\tif err := c.Conn.WriteMessage(websocket.TextMessage, message); err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\tcase <-ticker.C:\n\t\t\t// 发送心跳\n\t\t\tmsg := WSMessage{\n\t\t\t\tType:      WSMsgHeartbeat,\n\t\t\t\tTimestamp: time.Now().Unix(),\n\t\t\t}\n\t\t\tdata, _ := json.Marshal(msg)\n\t\t\tc.Conn.SetWriteDeadline(time.Now().Add(30 * time.Second))\n\t\t\tif err := c.Conn.WriteMessage(websocket.TextMessage, data); err != nil {\n\t\t\t\tlogger.Debug(\"[WS] 发送心跳失败: %s - %v\", c.ID, err)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n}\n\n// readPump 从客户端读取消息\nfunc (c *WSClient) readPump() {\n\tdefer func() {\n\t\tc.Server.removeClient(c.ID)\n\t\tc.Conn.Close()\n\t}()\n\n\t// 延长读取超时到180秒（3分钟），以适应长时间注册任务\n\tc.Conn.SetReadDeadline(time.Now().Add(180 * time.Second))\n\tc.Conn.SetPongHandler(func(string) error {\n\t\tc.Conn.SetReadDeadline(time.Now().Add(180 * time.Second))\n\t\treturn nil\n\t})\n\t// 处理客户端的 Ping 消息，自动回复 Pong 并重置超时\n\tc.Conn.SetPingHandler(func(appData string) error {\n\t\tc.Conn.SetReadDeadline(time.Now().Add(180 * time.Second))\n\t\tc.mu.Lock()\n\t\tc.LastPing = time.Now()\n\t\tc.mu.Unlock()\n\t\treturn c.Conn.WriteControl(websocket.PongMessage, []byte(appData), time.Now().Add(10*time.Second))\n\t})\n\n\tfor {\n\t\t_, message, err := c.Conn.ReadMessage()\n\t\tif err != nil {\n\t\t\tif websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {\n\t\t\t\tlogger.Debug(\"[WS] 读取错误: %v\", err)\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\n\t\tvar msg WSMessage\n\t\tif err := json.Unmarshal(message, &msg); err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tc.handleMessage(msg)\n\t}\n}\n\n// handleMessage 处理客户端消息\nfunc (c *WSClient) handleMessage(msg WSMessage) {\n\tc.mu.Lock()\n\tc.LastPing = time.Now()\n\tc.mu.Unlock()\n\n\t// 收到任何消息都重置读取超时\n\tc.Conn.SetReadDeadline(time.Now().Add(180 * time.Second))\n\n\tswitch msg.Type {\n\tcase WSMsgHeartbeatAck:\n\t\tlogger.Debug(\"[WS] 收到心跳响应: %s\", c.ID)\n\n\tcase WSMsgClientReady:\n\t\tif threads, ok := msg.Data[\"max_threads\"].(float64); ok && threads > 0 {\n\t\t\tc.MaxThreads = int(threads)\n\t\t} else {\n\t\t\tc.MaxThreads = 1\n\t\t}\n\t\tif ver, ok := msg.Data[\"client_version\"].(string); ok {\n\t\t\tc.ClientVersion = ver\n\t\t}\n\t\tlogger.Info(\"[WS] 客户端 %s 就绪 (v%s, 线程:%d)\", c.ID, c.ClientVersion, c.MaxThreads)\n\t\tc.Server.assignTask(c)\n\n\tcase WSMsgRequestTask:\n\t\tlogger.Debug(\"[WS] 客户端 %s 请求任务\", c.ID)\n\t\tc.Server.assignTask(c)\n\n\tcase WSMsgRegisterResult:\n\t\t// 注册结果\n\t\tc.Server.handleRegisterResult(msg.Data)\n\n\tcase WSMsgRefreshResult:\n\t\t// 续期结果\n\t\tc.Server.handleRefreshResult(msg.Data)\n\n\tcase WSMsgQueryStatus:\n\t\t// 客户端查询状态（自主模式）\n\t\tc.Server.sendStatusTo(c)\n\t}\n}\n\nfunc (ps *PoolServer) removeClient(clientID string) {\n\tps.clientsMu.Lock()\n\tdefer ps.clientsMu.Unlock()\n\tif client, ok := ps.clients[clientID]; ok {\n\t\tclose(client.Send)\n\t\tdelete(ps.clients, clientID)\n\t\tlogger.Info(\"[WS] 客户端断开: %s (剩余: %d)\", clientID, len(ps.clients))\n\t}\n}\nfunc (ps *PoolServer) taskDispatcher() {\n\tfor {\n\t\tselect {\n\t\tcase count := <-ps.registerQueue:\n\t\t\t// 检查是否还需要注册\n\t\t\tcurrentCount := ps.pool.TotalCount()\n\t\t\tpendingCount := int(atomic.LoadInt32(&ps.pendingRegisterCount))\n\t\t\tneedCount := ps.config.TargetCount - currentCount - pendingCount\n\t\t\tif needCount <= 0 {\n\t\t\t\tlogger.Debug(\"[分配] 已达目标数量，跳过注册任务 (当前: %d, 进行中: %d, 目标: %d)\",\n\t\t\t\t\tcurrentCount, pendingCount, ps.config.TargetCount)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// 分发注册任务（轮询分配）\n\t\t\tif ps.assignTaskRoundRobin(WSMsgTaskRegister, map[string]interface{}{\n\t\t\t\t\"count\": count,\n\t\t\t}) {\n\t\t\t\tatomic.AddInt32(&ps.pendingRegisterCount, 1)\n\t\t\t}\n\n\t\tcase acc := <-ps.refreshQueue:\n\t\t\t// 分发续期任务（轮询分配）\n\t\t\tps.assignTaskRoundRobin(WSMsgTaskRefresh, map[string]interface{}{\n\t\t\t\t\"email\":         acc.Data.Email,\n\t\t\t\t\"cookies\":       acc.Data.Cookies,\n\t\t\t\t\"authorization\": acc.Data.Authorization,\n\t\t\t\t\"config_id\":     acc.ConfigID,\n\t\t\t\t\"csesidx\":       acc.CSESIDX,\n\t\t\t})\n\t\t}\n\t}\n}\n\n// assignTaskRoundRobin 轮询分配任务给单个客户端\nfunc (ps *PoolServer) assignTaskRoundRobin(msgType WSMessageType, data map[string]interface{}) bool {\n\tmsg := WSMessage{\n\t\tType:      msgType,\n\t\tTimestamp: time.Now().Unix(),\n\t\tData:      data,\n\t}\n\tmsgBytes, _ := json.Marshal(msg)\n\n\tps.clientsMu.Lock()\n\tdefer ps.clientsMu.Unlock()\n\n\tif len(ps.clients) == 0 {\n\t\treturn false\n\t}\n\n\t// 获取客户端列表\n\tclientList := make([]*WSClient, 0, len(ps.clients))\n\tfor _, client := range ps.clients {\n\t\tif client.IsAlive {\n\t\t\tclientList = append(clientList, client)\n\t\t}\n\t}\n\n\tif len(clientList) == 0 {\n\t\treturn false\n\t}\n\n\t// 轮询分配\n\tps.nextClientIdx = ps.nextClientIdx % len(clientList)\n\tclient := clientList[ps.nextClientIdx]\n\tps.nextClientIdx++\n\n\tselect {\n\tcase client.Send <- msgBytes:\n\t\tlogger.Info(\"[分配] 任务 %s 分配给 %s\", msgType, client.ID)\n\t\treturn true\n\tdefault:\n\t\t// 发送队列满，尝试下一个\n\t\tfor i := 0; i < len(clientList)-1; i++ {\n\t\t\tps.nextClientIdx = ps.nextClientIdx % len(clientList)\n\t\t\tclient = clientList[ps.nextClientIdx]\n\t\t\tps.nextClientIdx++\n\t\t\tselect {\n\t\t\tcase client.Send <- msgBytes:\n\t\t\t\tlogger.Info(\"[分配] 任务 %s 分配给 %s\", msgType, client.ID)\n\t\t\t\treturn true\n\t\t\tdefault:\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t}\n\treturn false\n}\nfunc (ps *PoolServer) assignTask(client *WSClient) {\n\tmaxThreads := client.MaxThreads\n\tif maxThreads <= 0 {\n\t\tmaxThreads = 1\n\t}\n\tassignedCount := 0\n\tif AutoDelete401 {\n\t\tps.pool.mu.Lock()\n\t\tvar toDelete []*Account\n\t\tvar remaining []*Account\n\t\tfor _, acc := range ps.pool.pendingAccounts {\n\t\t\tif !acc.Refreshed && acc.FailCount > 0 {\n\t\t\t\t// 401账号，标记删除\n\t\t\t\ttoDelete = append(toDelete, acc)\n\t\t\t} else {\n\t\t\t\tremaining = append(remaining, acc)\n\t\t\t}\n\t\t}\n\t\tps.pool.pendingAccounts = remaining\n\t\tps.pool.mu.Unlock()\n\n\t\t// 删除401账号文件\n\t\tfor _, acc := range toDelete {\n\t\t\tlogger.Info(\"🗑️ [服务端] 401自动删除账号: %s\", acc.Data.Email)\n\t\t\tps.pool.RemoveAccount(acc)\n\t\t}\n\t} else {\n\t\t// 未配置自动删除，分配续期任务给节点\n\t\t// 计算401最大重试次数\n\t\tmaxRetry := MaxFailCount * 3\n\t\tif maxRetry < 10 {\n\t\t\tmaxRetry = 10\n\t\t}\n\n\t\tps.pool.mu.RLock()\n\t\tvar refreshAccounts []*Account\n\t\tfor _, acc := range ps.pool.pendingAccounts {\n\t\t\tif !acc.Refreshed && acc.FailCount > 0 {\n\t\t\t\t// 跳过已达上限的账号（浏览器刷新已达上限且401失败次数超过阈值）\n\t\t\t\tif acc.BrowserRefreshCount >= BrowserRefreshMaxRetry && acc.FailCount >= maxRetry {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\trefreshAccounts = append(refreshAccounts, acc)\n\t\t\t\tif len(refreshAccounts) >= maxThreads {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tps.pool.mu.RUnlock()\n\t\tfor _, acc := range refreshAccounts {\n\t\t\tlogger.Info(\"[WS] 分配续期任务给 %s: %s\", client.ID, acc.Data.Email)\n\t\t\tmsg := WSMessage{\n\t\t\t\tType:      WSMsgTaskRefresh,\n\t\t\t\tTimestamp: time.Now().Unix(),\n\t\t\t\tData: map[string]interface{}{\n\t\t\t\t\t\"email\":         acc.Data.Email,\n\t\t\t\t\t\"cookies\":       acc.Data.Cookies,\n\t\t\t\t\t\"authorization\": acc.Data.Authorization,\n\t\t\t\t\t\"config_id\":     acc.ConfigID,\n\t\t\t\t\t\"csesidx\":       acc.CSESIDX,\n\t\t\t\t},\n\t\t\t}\n\t\t\tmsgBytes, _ := json.Marshal(msg)\n\t\t\tselect {\n\t\t\tcase client.Send <- msgBytes:\n\t\t\t\tassignedCount++\n\t\t\tdefault:\n\t\t\t}\n\t\t}\n\t}\n\tremainingSlots := maxThreads - assignedCount\n\tif remainingSlots > 0 {\n\t\tcurrentCount := ps.pool.TotalCount()\n\t\tpendingCount := int(atomic.LoadInt32(&ps.pendingRegisterCount))\n\t\ttargetCount := ps.config.TargetCount\n\t\t// 计算需要注册的数量时，考虑正在进行中的任务\n\t\tneedCount := targetCount - currentCount - pendingCount\n\n\t\tif needCount > 0 {\n\t\t\tregisterCount := remainingSlots\n\t\t\tif registerCount > needCount {\n\t\t\t\tregisterCount = needCount\n\t\t\t}\n\n\t\t\tlogger.Info(\"[WS] 分配注册任务给 %s: %d个 (当前: %d, 进行中: %d, 目标: %d, 线程: %d)\",\n\t\t\t\tclient.ID, registerCount, currentCount, pendingCount, targetCount, maxThreads)\n\t\t\tfor i := 0; i < registerCount; i++ {\n\t\t\t\tmsg := WSMessage{\n\t\t\t\t\tType:      WSMsgTaskRegister,\n\t\t\t\t\tTimestamp: time.Now().Unix(),\n\t\t\t\t\tData: map[string]interface{}{\n\t\t\t\t\t\t\"count\": 1,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tmsgBytes, _ := json.Marshal(msg)\n\t\t\t\tselect {\n\t\t\t\tcase client.Send <- msgBytes:\n\t\t\t\t\tassignedCount++\n\t\t\t\t\tatomic.AddInt32(&ps.pendingRegisterCount, 1) // 增加进行中计数\n\t\t\t\tdefault:\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif assignedCount == 0 {\n\t\tcurrentCount := ps.pool.TotalCount()\n\t\tpendingCount := int(atomic.LoadInt32(&ps.pendingRegisterCount))\n\t\ttargetCount := ps.config.TargetCount\n\t\tlogger.Info(\"[WS] 无任务需要分配给 %s (当前: %d, 进行中: %d, 目标: %d)\",\n\t\t\tclient.ID, currentCount, pendingCount, targetCount)\n\t}\n}\n\n// heartbeatChecker 心跳检测\nfunc (ps *PoolServer) heartbeatChecker() {\n\tticker := time.NewTicker(60 * time.Second)\n\tdefer ticker.Stop()\n\n\tfor range ticker.C {\n\t\tps.clientsMu.RLock()\n\t\taliveClients := 0\n\t\ttotalThreads := 0\n\t\tfor id, client := range ps.clients {\n\t\t\tclient.mu.Lock()\n\t\t\tif time.Since(client.LastPing) > 180*time.Second {\n\t\t\t\tclient.IsAlive = false\n\t\t\t\tlogger.Warn(\"[WS] 客户端 %s 心跳超时 (last: %v ago)\", id, time.Since(client.LastPing))\n\t\t\t} else if client.IsAlive {\n\t\t\t\taliveClients++\n\t\t\t\ttotalThreads += client.MaxThreads\n\t\t\t}\n\t\t\tclient.mu.Unlock()\n\t\t}\n\t\tps.clientsMu.RUnlock()\n\n\t\tpendingCount := atomic.LoadInt32(&ps.pendingRegisterCount)\n\n\t\tif aliveClients == 0 {\n\t\t\tif pendingCount > 0 {\n\t\t\t\tatomic.StoreInt32(&ps.pendingRegisterCount, 0)\n\t\t\t\tlogger.Info(\"[心跳] 无活跃客户端，重置 pendingRegisterCount: %d -> 0\", pendingCount)\n\t\t\t}\n\t\t} else {\n\t\t\tmaxReasonable := int32(totalThreads * 2)\n\t\t\tif maxReasonable < 10 {\n\t\t\t\tmaxReasonable = 10\n\t\t\t}\n\t\t\tif pendingCount > maxReasonable {\n\t\t\t\tatomic.StoreInt32(&ps.pendingRegisterCount, 0)\n\t\t\t\tlogger.Warn(\"[心跳] pendingRegisterCount 异常 (%d > %d)，已重置\", pendingCount, maxReasonable)\n\t\t\t} else if pendingCount > 0 {\n\t\t\t\t// 每分钟衰减：pending 任务不应该长期存在，逐步减少\n\t\t\t\tnewCount := pendingCount - int32(aliveClients)\n\t\t\t\tif newCount < 0 {\n\t\t\t\t\tnewCount = 0\n\t\t\t\t}\n\t\t\t\tif newCount != pendingCount {\n\t\t\t\t\tatomic.StoreInt32(&ps.pendingRegisterCount, newCount)\n\t\t\t\t\tlogger.Debug(\"[心跳] pendingRegisterCount 衰减: %d -> %d\", pendingCount, newCount)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (ps *PoolServer) periodicTaskBroadcaster() {\n\t// 启动时5秒后立即执行一次\n\ttime.AfterFunc(5*time.Second, func() {\n\t\tps.broadcastRegisterTasks()\n\t})\n\n\t// 广播保持30分钟一次\n\tticker := time.NewTicker(30 * time.Minute)\n\tdefer ticker.Stop()\n\n\tfor range ticker.C {\n\t\tps.broadcastRegisterTasks()\n\t}\n}\n\n// poolMaintainer 号池维护：持续检测缺账号就下发\nfunc (ps *PoolServer) poolMaintainer() {\n\tticker := time.NewTicker(30 * time.Second)\n\tdefer ticker.Stop()\n\n\tfor range ticker.C {\n\t\tcurrentCount := ps.pool.TotalCount()\n\t\tpendingCount := int(atomic.LoadInt32(&ps.pendingRegisterCount))\n\t\ttargetCount := ps.config.TargetCount\n\t\tneedCount := targetCount - currentCount - pendingCount\n\n\t\tif needCount <= 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\t// 缺账号，向所有活跃客户端下发任务\n\t\tps.clientsMu.RLock()\n\t\tfor _, client := range ps.clients {\n\t\t\tif !client.IsAlive {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// 每个客户端分配其线程数的任务\n\t\t\tassignCount := client.MaxThreads\n\t\t\tif assignCount <= 0 {\n\t\t\t\tassignCount = 1\n\t\t\t}\n\t\t\tif assignCount > needCount {\n\t\t\t\tassignCount = needCount\n\t\t\t}\n\n\t\t\tlogger.Info(\"[号池维护] 向 %s 下发 %d 个注册任务 (当前: %d, 进行中: %d, 目标: %d)\",\n\t\t\t\tclient.ID, assignCount, currentCount, pendingCount, targetCount)\n\n\t\t\tfor i := 0; i < assignCount; i++ {\n\t\t\t\tmsg := WSMessage{\n\t\t\t\t\tType:      WSMsgTaskRegister,\n\t\t\t\t\tTimestamp: time.Now().Unix(),\n\t\t\t\t\tData:      map[string]interface{}{\"count\": 1},\n\t\t\t\t}\n\t\t\t\tmsgBytes, _ := json.Marshal(msg)\n\t\t\t\tselect {\n\t\t\t\tcase client.Send <- msgBytes:\n\t\t\t\t\tatomic.AddInt32(&ps.pendingRegisterCount, 1)\n\t\t\t\t\tneedCount--\n\t\t\t\tdefault:\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif needCount <= 0 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tps.clientsMu.RUnlock()\n\t}\n}\n\nfunc (ps *PoolServer) periodicAccountScanner() {\n\tticker := time.NewTicker(5 * time.Minute)\n\tdefer ticker.Stop()\n\n\tfor range ticker.C {\n\t\tdataDir := ps.config.DataDir\n\t\tif dataDir == \"\" {\n\t\t\tdataDir = DataDir\n\t\t}\n\t\toldCount := ps.pool.TotalCount()\n\t\tif err := ps.pool.Load(dataDir); err != nil {\n\t\t\tlogger.Warn(\"[扫描] 加载账号失败: %v\", err)\n\t\t} else {\n\t\t\tnewCount := ps.pool.TotalCount()\n\t\t\tif newCount != oldCount {\n\t\t\t\tlogger.Info(\"[扫描] 账号数量变化: %d -> %d\", oldCount, newCount)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (ps *PoolServer) broadcastRegisterTasks() {\n\tcurrentCount := ps.pool.TotalCount()\n\tpendingCount := int(atomic.LoadInt32(&ps.pendingRegisterCount))\n\ttargetCount := ps.config.TargetCount\n\tneedCount := targetCount - currentCount - pendingCount\n\n\tlogger.Info(\"[广播] 检查注册任务 (当前: %d, 进行中: %d, 目标: %d, 需要: %d)\",\n\t\tcurrentCount, pendingCount, targetCount, needCount)\n\n\t// 检查是否需要注册\n\tif needCount <= 0 {\n\t\tlogger.Info(\"[广播] 已达目标数量，跳过任务广播\")\n\t\treturn\n\t}\n\n\tps.clientsMu.RLock()\n\tdefer ps.clientsMu.RUnlock()\n\n\tif len(ps.clients) == 0 {\n\t\tlogger.Warn(\"[广播] 无在线客户端，跳过任务广播\")\n\t\treturn\n\t}\n\n\ttotalAssigned := 0\n\tfor _, client := range ps.clients {\n\t\tif !client.IsAlive {\n\t\t\tcontinue\n\t\t}\n\n\t\t// 检查剩余需要的数量\n\t\tremainingNeed := needCount - totalAssigned\n\t\tif remainingNeed <= 0 {\n\t\t\tbreak\n\t\t}\n\n\t\t// 每个节点分配其线程数量的任务，但不超过剩余需要的数量\n\t\tassignCount := client.MaxThreads\n\t\tif assignCount <= 0 {\n\t\t\tassignCount = 1\n\t\t}\n\t\tif assignCount > remainingNeed {\n\t\t\tassignCount = remainingNeed\n\t\t}\n\n\t\tlogger.Info(\"[广播] 向 %s 分配 %d 个注册任务 (线程: %d)\", client.ID, assignCount, client.MaxThreads)\n\t\tfor i := 0; i < assignCount; i++ {\n\t\t\tmsg := WSMessage{\n\t\t\t\tType:      WSMsgTaskRegister,\n\t\t\t\tTimestamp: time.Now().Unix(),\n\t\t\t\tData: map[string]interface{}{\n\t\t\t\t\t\"count\": 1,\n\t\t\t\t},\n\t\t\t}\n\t\t\tmsgBytes, _ := json.Marshal(msg)\n\t\t\tselect {\n\t\t\tcase client.Send <- msgBytes:\n\t\t\t\tatomic.AddInt32(&ps.pendingRegisterCount, 1)\n\t\t\t\ttotalAssigned++\n\t\t\tdefault:\n\t\t\t\tlogger.Warn(\"[广播] 客户端 %s 发送队列满\", client.ID)\n\t\t\t}\n\t\t}\n\t}\n\n\tif totalAssigned > 0 {\n\t\tlogger.Info(\"[广播] 本轮共分配 %d 个注册任务给 %d 个节点\", totalAssigned, len(ps.clients))\n\t}\n}\n\nfunc (ps *PoolServer) sendStatusTo(client *WSClient) {\n\tcurrentCount := ps.pool.TotalCount()\n\tpendingCount := int(atomic.LoadInt32(&ps.pendingRegisterCount))\n\ttargetCount := ps.config.TargetCount\n\tneedCount := targetCount - currentCount - pendingCount\n\n\tmsg := WSMessage{\n\t\tType:      WSMsgStatus,\n\t\tTimestamp: time.Now().Unix(),\n\t\tData: map[string]interface{}{\n\t\t\t\"current_count\": currentCount,\n\t\t\t\"pending_count\": pendingCount,\n\t\t\t\"target_count\":  targetCount,\n\t\t\t\"need_count\":    needCount,\n\t\t},\n\t}\n\tmsgBytes, _ := json.Marshal(msg)\n\tselect {\n\tcase client.Send <- msgBytes:\n\t\tlogger.Debug(\"[WS] 发送状态给 %s: current=%d, target=%d, need=%d\",\n\t\t\tclient.ID, currentCount, targetCount, needCount)\n\tdefault:\n\t\tlogger.Warn(\"[WS] 发送状态失败，队列满: %s\", client.ID)\n\t}\n}\n\nfunc (ps *PoolServer) handleRegisterResult(data map[string]interface{}) {\n\t// 减少进行中计数\n\tatomic.AddInt32(&ps.pendingRegisterCount, -1)\n\n\tsuccess, _ := data[\"success\"].(bool)\n\temail, _ := data[\"email\"].(string)\n\n\tif success {\n\t\tlogger.Info(\"✅ 注册成功: %s\", email)\n\t\t// 重新加载账号\n\t\tps.pool.Load(ps.config.DataDir)\n\t} else {\n\t\terrMsg, _ := data[\"error\"].(string)\n\t\tlogger.Warn(\"❌ 注册失败: %s\", errMsg)\n\t}\n}\nfunc (ps *PoolServer) handleRefreshResult(data map[string]interface{}) {\n\temail, _ := data[\"email\"].(string)\n\tsuccess, _ := data[\"success\"].(bool)\n\n\tif success {\n\t\tlogger.Info(\"✅ 账号续期成功: %s\", email)\n\t\t// 更新账号数据\n\t\tif cookiesData, ok := data[\"cookies\"]; ok {\n\t\t\tps.updateAccountCookies(email, cookiesData)\n\t\t}\n\t} else {\n\t\terrMsg, _ := data[\"error\"].(string)\n\t\tlogger.Warn(\"❌ 账号续期失败 %s: %s\", email, errMsg)\n\t\taction := ps.config.ExpiredAction\n\t\tif action == \"\" {\n\t\t\taction = \"delete\" // 默认删除\n\t\t}\n\n\t\tswitch action {\n\t\tcase \"delete\":\n\t\t\tps.deleteAccount(email)\n\t\tcase \"queue\":\n\t\t\t// 保持在队列中，不做处理\n\t\tcase \"refresh\":\n\t\tdefault:\n\t\t\tps.deleteAccount(email)\n\t\t}\n\t}\n}\n\n// deleteAccount 删除账号\nfunc (ps *PoolServer) deleteAccount(email string) {\n\tps.pool.mu.Lock()\n\tdefer ps.pool.mu.Unlock()\n\n\t// 从 pending 队列删除\n\tfor i, acc := range ps.pool.pendingAccounts {\n\t\tif acc.Data.Email == email {\n\t\t\t// 删除文件\n\t\t\tif acc.FilePath != \"\" {\n\t\t\t\tos.Remove(acc.FilePath)\n\t\t\t}\n\t\t\tps.pool.pendingAccounts = append(ps.pool.pendingAccounts[:i], ps.pool.pendingAccounts[i+1:]...)\n\t\t\tlogger.Info(\"🗑️ 已删除续期失败账号: %s\", email)\n\t\t\treturn\n\t\t}\n\t}\n\n\t// 从 ready 队列删除\n\tfor i, acc := range ps.pool.readyAccounts {\n\t\tif acc.Data.Email == email {\n\t\t\tif acc.FilePath != \"\" {\n\t\t\t\tos.Remove(acc.FilePath)\n\t\t\t}\n\t\t\tps.pool.readyAccounts = append(ps.pool.readyAccounts[:i], ps.pool.readyAccounts[i+1:]...)\n\t\t\tlogger.Info(\"🗑️ 已删除续期失败账号: %s\", email)\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// updateAccountCookies 更新账号Cookie\nfunc (ps *PoolServer) updateAccountCookies(email string, cookiesData interface{}) {\n\tps.pool.mu.Lock()\n\tdefer ps.pool.mu.Unlock()\n\n\tfor _, acc := range ps.pool.readyAccounts {\n\t\tif acc.Data.Email == email {\n\t\t\t// 更新cookies\n\t\t\tif cookies, ok := cookiesData.([]interface{}); ok {\n\t\t\t\tvar newCookies []Cookie\n\t\t\t\tfor _, c := range cookies {\n\t\t\t\t\tif cm, ok := c.(map[string]interface{}); ok {\n\t\t\t\t\t\tnewCookies = append(newCookies, Cookie{\n\t\t\t\t\t\t\tName:   cm[\"name\"].(string),\n\t\t\t\t\t\t\tValue:  cm[\"value\"].(string),\n\t\t\t\t\t\t\tDomain: cm[\"domain\"].(string),\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tacc.Data.Cookies = newCookies\n\t\t\t\tacc.Refreshed = true\n\t\t\t\tacc.FailCount = 0\n\t\t\t\tacc.SaveToFile()\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// handleQueueRegister 队列注册任务\nfunc (ps *PoolServer) handleQueueRegister(w http.ResponseWriter, r *http.Request) {\n\tvar req struct {\n\t\tCount int `json:\"count\"`\n\t}\n\tif err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Count <= 0 {\n\t\treq.Count = 1\n\t}\n\n\tselect {\n\tcase ps.registerQueue <- req.Count:\n\t\tjson.NewEncoder(w).Encode(map[string]interface{}{\n\t\t\t\"success\": true,\n\t\t\t\"message\": fmt.Sprintf(\"已添加 %d 个注册任务到队列\", req.Count),\n\t\t})\n\tdefault:\n\t\tjson.NewEncoder(w).Encode(map[string]interface{}{\n\t\t\t\"success\": false,\n\t\t\t\"error\":   \"任务队列已满\",\n\t\t})\n\t}\n}\n\n// handleQueueRefresh 队列续期任务\nfunc (ps *PoolServer) handleQueueRefresh(w http.ResponseWriter, r *http.Request) {\n\tvar req struct {\n\t\tEmail string `json:\"email\"`\n\t}\n\tif err := json.NewDecoder(r.Body).Decode(&req); err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusBadRequest)\n\t\treturn\n\t}\n\n\t// 查找账号\n\tps.pool.mu.RLock()\n\tvar targetAcc *Account\n\tfor _, acc := range ps.pool.readyAccounts {\n\t\tif acc.Data.Email == req.Email {\n\t\t\ttargetAcc = acc\n\t\t\tbreak\n\t\t}\n\t}\n\tps.pool.mu.RUnlock()\n\n\tif targetAcc == nil {\n\t\tjson.NewEncoder(w).Encode(map[string]interface{}{\n\t\t\t\"success\": false,\n\t\t\t\"error\":   \"账号未找到\",\n\t\t})\n\t\treturn\n\t}\n\n\tselect {\n\tcase ps.refreshQueue <- targetAcc:\n\t\tjson.NewEncoder(w).Encode(map[string]interface{}{\n\t\t\t\"success\": true,\n\t\t\t\"message\": fmt.Sprintf(\"已添加账号 %s 续期任务到队列\", req.Email),\n\t\t})\n\tdefault:\n\t\tjson.NewEncoder(w).Encode(map[string]interface{}{\n\t\t\t\"success\": false,\n\t\t\t\"error\":   \"任务队列已满\",\n\t\t})\n\t}\n}\n\n// AccountResponse 账号响应\ntype AccountResponse struct {\n\tSuccess       bool   `json:\"success\"`\n\tEmail         string `json:\"email,omitempty\"`\n\tJWT           string `json:\"jwt,omitempty\"`\n\tConfigID      string `json:\"config_id,omitempty\"`\n\tAuthorization string `json:\"authorization,omitempty\"`\n\tError         string `json:\"error,omitempty\"`\n}\n\nfunc (ps *PoolServer) handleNext(w http.ResponseWriter, r *http.Request) {\n\tacc := ps.pool.Next()\n\tif acc == nil {\n\t\tjson.NewEncoder(w).Encode(AccountResponse{\n\t\t\tSuccess: false,\n\t\t\tError:   \"没有可用账号\",\n\t\t})\n\t\treturn\n\t}\n\n\tjwt, configID, err := acc.GetJWT()\n\tif err != nil {\n\t\tjson.NewEncoder(w).Encode(AccountResponse{\n\t\t\tSuccess: false,\n\t\t\tEmail:   acc.Data.Email,\n\t\t\tError:   err.Error(),\n\t\t})\n\t\treturn\n\t}\n\n\tjson.NewEncoder(w).Encode(AccountResponse{\n\t\tSuccess:       true,\n\t\tEmail:         acc.Data.Email,\n\t\tJWT:           jwt,\n\t\tConfigID:      configID,\n\t\tAuthorization: acc.Data.Authorization,\n\t})\n}\n\nfunc (ps *PoolServer) handleMark(w http.ResponseWriter, r *http.Request) {\n\tvar req struct {\n\t\tEmail   string `json:\"email\"`\n\t\tSuccess bool   `json:\"success\"`\n\t}\n\tif err := json.NewDecoder(r.Body).Decode(&req); err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusBadRequest)\n\t\treturn\n\t}\n\n\t// 查找账号并标记\n\tps.pool.mu.RLock()\n\tvar targetAcc *Account\n\tfor _, acc := range ps.pool.readyAccounts {\n\t\tif acc.Data.Email == req.Email {\n\t\t\ttargetAcc = acc\n\t\t\tbreak\n\t\t}\n\t}\n\tps.pool.mu.RUnlock()\n\n\tif targetAcc != nil {\n\t\tps.pool.MarkUsed(targetAcc, req.Success)\n\t}\n\n\tjson.NewEncoder(w).Encode(map[string]bool{\"success\": true})\n}\n\nfunc (ps *PoolServer) handleRefresh(w http.ResponseWriter, r *http.Request) {\n\tvar req struct {\n\t\tEmail string `json:\"email\"`\n\t}\n\tif err := json.NewDecoder(r.Body).Decode(&req); err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusBadRequest)\n\t\treturn\n\t}\n\n\t// 查找账号并标记需要刷新\n\tps.pool.mu.RLock()\n\tvar targetAcc *Account\n\tfor _, acc := range ps.pool.readyAccounts {\n\t\tif acc.Data.Email == req.Email {\n\t\t\ttargetAcc = acc\n\t\t\tbreak\n\t\t}\n\t}\n\tps.pool.mu.RUnlock()\n\n\tif targetAcc != nil {\n\t\tps.pool.MarkNeedsRefresh(targetAcc)\n\t}\n\n\tjson.NewEncoder(w).Encode(map[string]bool{\"success\": true})\n}\n\nfunc (ps *PoolServer) handleStatus(w http.ResponseWriter, r *http.Request) {\n\tjson.NewEncoder(w).Encode(ps.pool.Stats())\n}\n\nfunc (ps *PoolServer) handleGetJWT(w http.ResponseWriter, r *http.Request) {\n\temail := r.URL.Query().Get(\"email\")\n\tif email == \"\" {\n\t\thttp.Error(w, \"缺少email参数\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tps.pool.mu.RLock()\n\tvar targetAcc *Account\n\tfor _, acc := range ps.pool.readyAccounts {\n\t\tif acc.Data.Email == email {\n\t\t\ttargetAcc = acc\n\t\t\tbreak\n\t\t}\n\t}\n\tps.pool.mu.RUnlock()\n\n\tif targetAcc == nil {\n\t\tjson.NewEncoder(w).Encode(AccountResponse{\n\t\t\tSuccess: false,\n\t\t\tError:   \"账号未找到\",\n\t\t})\n\t\treturn\n\t}\n\n\tjwt, configID, err := targetAcc.GetJWT()\n\tif err != nil {\n\t\tjson.NewEncoder(w).Encode(AccountResponse{\n\t\t\tSuccess: false,\n\t\t\tEmail:   email,\n\t\t\tError:   err.Error(),\n\t\t})\n\t\treturn\n\t}\n\n\tjson.NewEncoder(w).Encode(AccountResponse{\n\t\tSuccess:       true,\n\t\tEmail:         email,\n\t\tJWT:           jwt,\n\t\tConfigID:      configID,\n\t\tAuthorization: targetAcc.Data.Authorization,\n\t})\n}\n\n// ==================== 远程号池客户端 ====================\n\n// RemotePoolClient 远程号池客户端\ntype RemotePoolClient struct {\n\tserverAddr string\n\tsecret     string\n\tclient     *http.Client\n\tmu         sync.RWMutex\n\t// 本地缓存\n\tcachedAccounts map[string]*CachedAccount\n}\n\n// CachedAccount 缓存的账号信息\ntype CachedAccount struct {\n\tEmail         string\n\tJWT           string\n\tConfigID      string\n\tAuthorization string\n\tFetchedAt     time.Time\n}\n\n// NewRemotePoolClient 创建远程号池客户端\nfunc NewRemotePoolClient(serverAddr, secret string) *RemotePoolClient {\n\treturn &RemotePoolClient{\n\t\tserverAddr: serverAddr,\n\t\tsecret:     secret,\n\t\tclient: &http.Client{\n\t\t\tTimeout: 30 * time.Second,\n\t\t},\n\t\tcachedAccounts: make(map[string]*CachedAccount),\n\t}\n}\n\n// doRequest 发送请求到号池服务器\nfunc (rc *RemotePoolClient) doRequest(method, path string, body interface{}) (*http.Response, error) {\n\tvar reqBody io.Reader\n\tif body != nil {\n\t\tdata, err := json.Marshal(body)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treqBody = bytes.NewReader(data)\n\t}\n\n\treq, err := http.NewRequest(method, rc.serverAddr+path, reqBody)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\tif rc.secret != \"\" {\n\t\treq.Header.Set(\"X-Pool-Secret\", rc.secret)\n\t}\n\n\treturn rc.client.Do(req)\n}\n\n// Next 获取下一个可用账号\nfunc (rc *RemotePoolClient) Next() (*CachedAccount, error) {\n\tresp, err := rc.doRequest(\"GET\", \"/pool/next\", nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"请求号池服务器失败: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tvar result AccountResponse\n\tif err := json.NewDecoder(resp.Body).Decode(&result); err != nil {\n\t\treturn nil, fmt.Errorf(\"解析响应失败: %w\", err)\n\t}\n\n\tif !result.Success {\n\t\treturn nil, fmt.Errorf(\"%s\", result.Error)\n\t}\n\n\tacc := &CachedAccount{\n\t\tEmail:         result.Email,\n\t\tJWT:           result.JWT,\n\t\tConfigID:      result.ConfigID,\n\t\tAuthorization: result.Authorization,\n\t\tFetchedAt:     time.Now(),\n\t}\n\n\t// 缓存账号\n\trc.mu.Lock()\n\trc.cachedAccounts[result.Email] = acc\n\trc.mu.Unlock()\n\n\treturn acc, nil\n}\n\n// MarkUsed 标记账号使用结果\nfunc (rc *RemotePoolClient) MarkUsed(email string, success bool) error {\n\tresp, err := rc.doRequest(\"POST\", \"/pool/mark\", map[string]interface{}{\n\t\t\"email\":   email,\n\t\t\"success\": success,\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\tresp.Body.Close()\n\treturn nil\n}\n\n// MarkNeedsRefresh 标记账号需要刷新\nfunc (rc *RemotePoolClient) MarkNeedsRefresh(email string) error {\n\tresp, err := rc.doRequest(\"POST\", \"/pool/refresh\", map[string]interface{}{\n\t\t\"email\": email,\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\tresp.Body.Close()\n\treturn nil\n}\n\n// GetStatus 获取号池状态\nfunc (rc *RemotePoolClient) GetStatus() (map[string]interface{}, error) {\n\tresp, err := rc.doRequest(\"GET\", \"/pool/status\", nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\n\tvar result map[string]interface{}\n\tif err := json.NewDecoder(resp.Body).Decode(&result); err != nil {\n\t\treturn nil, err\n\t}\n\treturn result, nil\n}\n\n// RefreshJWT 刷新指定账号的JWT\nfunc (rc *RemotePoolClient) RefreshJWT(email string) (*CachedAccount, error) {\n\tresp, err := rc.doRequest(\"GET\", \"/pool/jwt?email=\"+email, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\n\tvar result AccountResponse\n\tif err := json.NewDecoder(resp.Body).Decode(&result); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif !result.Success {\n\t\treturn nil, fmt.Errorf(\"%s\", result.Error)\n\t}\n\n\tacc := &CachedAccount{\n\t\tEmail:         result.Email,\n\t\tJWT:           result.JWT,\n\t\tConfigID:      result.ConfigID,\n\t\tAuthorization: result.Authorization,\n\t\tFetchedAt:     time.Now(),\n\t}\n\n\trc.mu.Lock()\n\trc.cachedAccounts[email] = acc\n\trc.mu.Unlock()\n\n\treturn acc, nil\n}\n\ntype AccountUploadRequest struct {\n\tEmail         string   `json:\"email\"`\n\tFullName      string   `json:\"full_name\"`\n\tCookies       []Cookie `json:\"cookies\"`\n\tCookieString  string   `json:\"cookie_string\"`\n\tAuthorization string   `json:\"authorization\"`\n\tConfigID      string   `json:\"config_id\"`\n\tCSESIDX       string   `json:\"csesidx\"`\n\tIsNew         bool     `json:\"is_new\"`\n}\n\n// handleUploadAccount 处理账号上传（客户端回传鉴权文件）\nfunc (ps *PoolServer) handleUploadAccount(w http.ResponseWriter, r *http.Request) {\n\tif r.Method != http.MethodPost {\n\t\thttp.Error(w, \"Method not allowed\", http.StatusMethodNotAllowed)\n\t\treturn\n\t}\n\n\tvar req AccountUploadRequest\n\tif err := json.NewDecoder(r.Body).Decode(&req); err != nil {\n\t\tlogger.Error(\"解析账号上传请求失败: %v\", err)\n\t\tjson.NewEncoder(w).Encode(map[string]interface{}{\n\t\t\t\"success\": false,\n\t\t\t\"error\":   \"无效的请求格式\",\n\t\t})\n\t\treturn\n\t}\n\n\tif req.Email == \"\" {\n\t\tjson.NewEncoder(w).Encode(map[string]interface{}{\n\t\t\t\"success\": false,\n\t\t\t\"error\":   \"邮箱不能为空\",\n\t\t})\n\t\treturn\n\t}\n\n\t// 构建账号数据\n\taccData := AccountData{\n\t\tEmail:         req.Email,\n\t\tFullName:      req.FullName,\n\t\tCookies:       req.Cookies,\n\t\tCookieString:  req.CookieString,\n\t\tAuthorization: req.Authorization,\n\t\tConfigID:      req.ConfigID,\n\t\tCSESIDX:       req.CSESIDX,\n\t\tTimestamp:     time.Now().Format(time.RFC3339),\n\t}\n\n\t// 保存到文件\n\tdataDir := ps.config.DataDir\n\tif dataDir == \"\" {\n\t\tdataDir = \"./data\"\n\t}\n\n\t// 确保目录存在\n\tif err := os.MkdirAll(dataDir, 0755); err != nil {\n\t\tlogger.Error(\"创建数据目录失败: %v\", err)\n\t\tjson.NewEncoder(w).Encode(map[string]interface{}{\n\t\t\t\"success\": false,\n\t\t\t\"error\":   \"服务器内部错误\",\n\t\t})\n\t\treturn\n\t}\n\n\t// 生成文件名\n\tfilename := fmt.Sprintf(\"%s.json\", req.Email)\n\tfilePath := filepath.Join(dataDir, filename)\n\n\t// 序列化并保存\n\tdata, err := json.MarshalIndent(accData, \"\", \"  \")\n\tif err != nil {\n\t\tlogger.Error(\"序列化账号数据失败: %v\", err)\n\t\tjson.NewEncoder(w).Encode(map[string]interface{}{\n\t\t\t\"success\": false,\n\t\t\t\"error\":   \"序列化失败\",\n\t\t})\n\t\treturn\n\t}\n\n\tif err := os.WriteFile(filePath, data, 0644); err != nil {\n\t\tlogger.Error(\"保存账号文件失败: %v\", err)\n\t\tjson.NewEncoder(w).Encode(map[string]interface{}{\n\t\t\t\"success\": false,\n\t\t\t\"error\":   \"保存失败\",\n\t\t})\n\t\treturn\n\t}\n\n\tif req.IsNew {\n\t\tlogger.Info(\"✅ 收到新注册账号: %s\", req.Email)\n\t} else {\n\t\tlogger.Info(\"✅ 收到账号续期数据: %s\", req.Email)\n\t}\n\n\t// 先加载文件确保账号存在\n\tps.pool.Load(dataDir)\n\n\t// 更新内存中的账号数据\n\tps.pool.mu.Lock()\n\tfound := false\n\n\t// 查找并更新 pending 队列\n\tfor _, acc := range ps.pool.pendingAccounts {\n\t\tif acc.Data.Email == req.Email {\n\t\t\tacc.Mu.Lock()\n\t\t\tacc.Data.Cookies = req.Cookies\n\t\t\tacc.Data.CookieString = req.CookieString\n\t\t\tacc.Data.Authorization = req.Authorization\n\t\t\tacc.Data.ConfigID = req.ConfigID\n\t\t\tacc.Data.CSESIDX = req.CSESIDX\n\t\t\tacc.ConfigID = req.ConfigID\n\t\t\tacc.CSESIDX = req.CSESIDX\n\t\t\tacc.FailCount = 0\n\t\t\tacc.BrowserRefreshCount = 0\n\t\t\tacc.JWTExpires = time.Time{} // 重置JWT过期时间，让refreshWorker去刷新\n\t\t\tacc.Mu.Unlock()\n\t\t\t// 保留在 pending 队列，让 refreshWorker 去刷新 JWT\n\t\t\tlogger.Info(\"🔄 [%s] 账号已更新，等待JWT刷新\", req.Email)\n\t\t\tfound = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif !found {\n\t\t// 查找并更新 ready 队列\n\t\tfor _, acc := range ps.pool.readyAccounts {\n\t\t\tif acc.Data.Email == req.Email {\n\t\t\t\tacc.Mu.Lock()\n\t\t\t\tacc.Data.Cookies = req.Cookies\n\t\t\t\tacc.Data.CookieString = req.CookieString\n\t\t\t\tacc.Data.Authorization = req.Authorization\n\t\t\t\tacc.Data.ConfigID = req.ConfigID\n\t\t\t\tacc.Data.CSESIDX = req.CSESIDX\n\t\t\t\tacc.ConfigID = req.ConfigID\n\t\t\t\tacc.CSESIDX = req.CSESIDX\n\t\t\t\tacc.FailCount = 0\n\t\t\t\tacc.BrowserRefreshCount = 0\n\t\t\t\tacc.JWTExpires = time.Time{} // 重置JWT过期时间，下次使用时会触发刷新\n\t\t\t\tacc.Mu.Unlock()\n\t\t\t\tlogger.Info(\"🔄 [%s] 账号已更新，下次使用时刷新JWT\", req.Email)\n\t\t\t\tfound = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\tps.pool.mu.Unlock()\n\n\tif !found {\n\t\tlogger.Warn(\"⚠️ [%s] 账号已保存但未在内存中找到\", req.Email)\n\t}\n\n\tjson.NewEncoder(w).Encode(map[string]interface{}{\n\t\t\"success\": true,\n\t\t\"message\": fmt.Sprintf(\"账号 %s 已保存\", req.Email),\n\t})\n}\n\nfunc (rc *RemotePoolClient) UploadAccount(acc *AccountUploadRequest) error {\n\tdata, err := json.Marshal(acc)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tresp, err := rc.doRequest(\"POST\", \"/pool/upload-account\", bytes.NewReader(data))\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer resp.Body.Close()\n\n\tvar result map[string]interface{}\n\tif err := json.NewDecoder(resp.Body).Decode(&result); err != nil {\n\t\treturn err\n\t}\n\n\tif success, ok := result[\"success\"].(bool); !ok || !success {\n\t\terrMsg, _ := result[\"error\"].(string)\n\t\treturn fmt.Errorf(\"上传失败: %s\", errMsg)\n\t}\n\treturn nil\n}\n\ntype ClientInfo struct {\n\tID       string `json:\"id\"`\n\tVersion  string `json:\"version\"`\n\tThreads  int    `json:\"threads\"`\n\tIsAlive  bool   `json:\"is_alive\"`\n\tLastPing int64  `json:\"last_ping\"`\n}\n\nfunc (ps *PoolServer) GetClientsInfo() []ClientInfo {\n\tps.clientsMu.RLock()\n\tdefer ps.clientsMu.RUnlock()\n\n\tclients := make([]ClientInfo, 0, len(ps.clients))\n\tfor id, c := range ps.clients {\n\t\tclients = append(clients, ClientInfo{\n\t\t\tID:       id,\n\t\t\tVersion:  c.ClientVersion,\n\t\t\tThreads:  c.MaxThreads,\n\t\t\tIsAlive:  c.IsAlive,\n\t\t\tLastPing: c.LastPing.Unix(),\n\t\t})\n\t}\n\treturn clients\n}\nfunc (ps *PoolServer) GetClientCount() int {\n\tps.clientsMu.RLock()\n\tdefer ps.clientsMu.RUnlock()\n\treturn len(ps.clients)\n}\n\nfunc (ps *PoolServer) GetTotalThreads() int {\n\tps.clientsMu.RLock()\n\tdefer ps.clientsMu.RUnlock()\n\ttotal := 0\n\tfor _, c := range ps.clients {\n\t\ttotal += c.MaxThreads\n\t}\n\treturn total\n}\n"
  },
  {
    "path": "src/proxy/proxy.go",
    "content": "package proxy\n\nimport (\n\t\"bytes\"\n\t\"compress/gzip\"\n\t\"crypto/rand\"\n\t\"crypto/tls\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"math/big\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n)\n\n// tlsConfig 全局 TLS 配置，跳过证书验证\nvar tlsConfig = &tls.Config{InsecureSkipVerify: true}\n\n// ProxyNode 代理节点\ntype ProxyNode struct {\n\tRaw       string // 原始链接\n\tProtocol  string // vmess, vless, ss, trojan, http, socks5, hysteria2, anytls\n\tName      string\n\tServer    string\n\tPort      int\n\tUUID      string // vmess/vless\n\tAlterId   int    // vmess\n\tSecurity  string // vmess 加密方式 / vless: none,tls,reality\n\tNetwork   string // tcp, ws, grpc, kcp, quic, httpupgrade, splithttp, xhttp\n\tPath      string // ws/http path\n\tHost      string // ws/http host\n\tTLS       bool\n\tSNI       string\n\tPassword  string // ss/trojan/anytls password\n\tMethod    string // ss method\n\tType      string // kcp/quic header type (none, srtp, utp, wechat-video, dtls, wireguard)\n\tHealthy   bool\n\tLastCheck time.Time\n\tLocalPort int\n\tLatency   time.Duration\n\tExitIP    string\n\n\t// Reality 相关\n\tFlow        string // xtls-rprx-vision\n\tFingerprint string // chrome, firefox, safari, ios, android, edge, 360, qq, random\n\tPublicKey   string // reality pbk\n\tShortId     string // reality sid\n\tSpiderX     string // reality spx\n\tALPN        string\n\n\t// 使用统计\n\tLastUsed    time.Time     // 最后使用时间\n\tFailCount   int           // 连续失败次数\n\tUseCooldown time.Duration // 使用冷却时间（失败后动态调整）\n}\n\n// InstanceStatus 实例状态\ntype InstanceStatus int\n\nconst (\n\tInstanceStatusIdle    InstanceStatus = iota // 空闲可用\n\tInstanceStatusInUse                         // 使用中\n\tInstanceStatusStopped                       // 已停止\n)\n\n// ProxyInstance 代理实例（使用 sing-box）\ntype ProxyInstance struct {\n\tlocalPort int\n\tnode      *ProxyNode\n\trunning   bool\n\tstatus    InstanceStatus\n\tlastUsed  time.Time\n\tproxyURL  string\n\tmu        sync.Mutex\n}\n\n// ProxyManager 代理管理器\ntype ProxyManager struct {\n\tmu             sync.RWMutex\n\tnodes          []*ProxyNode\n\thealthyNodes   []*ProxyNode\n\tinstancePool   []*ProxyInstance // 活跃实例追踪\n\tmaxPoolSize    int              // 最大实例池大小\n\tsubscribeURLs  []string\n\tproxyFiles     []string\n\tlastUpdate     time.Time\n\tupdateInterval time.Duration\n\tcheckInterval  time.Duration\n\thealthCheckURL string\n\tstopChan       chan struct{}\n\tready          bool       // 代理池是否就绪\n\treadyCond      *sync.Cond // 就绪条件变量\n\thealthChecking bool       // 是否正在健康检查\n}\n\n// 默认代理使用冷却时间\nvar (\n\tDefaultProxyUseCooldown = 5 * time.Second  // 默认使用冷却\n\tMaxProxyFailCount       = 3                // 最大连续失败次数，超过后增加冷却\n\tDefaultProxyCount       = 5                // 默认代理池大小\n\tMinHealthyForReady      = 1                // 最少健康节点数才提示就绪（改为1，更快就绪）\n\tHealthCheckTimeout      = 10 * time.Second // 健康检查超时（增加到10秒，给慢速代理更多时间）\n)\n\nvar Manager = &ProxyManager{\n\tinstancePool:   make([]*ProxyInstance, 0),\n\tmaxPoolSize:    5,\n\tupdateInterval: 30 * time.Minute,\n\tcheckInterval:  5 * time.Minute,\n\thealthCheckURL: \"https://www.google.com/generate_204\",\n\tstopChan:       make(chan struct{}),\n}\n\nfunc init() {\n\tManager.readyCond = sync.NewCond(&Manager.mu)\n}\n\n// IsReady 检查代理池是否就绪\nfunc (pm *ProxyManager) IsReady() bool {\n\tpm.mu.RLock()\n\tdefer pm.mu.RUnlock()\n\treturn pm.ready\n}\nfunc (pm *ProxyManager) WaitReady(timeout time.Duration) bool {\n\tdeadline := time.Now().Add(timeout)\n\n\tfor time.Now().Before(deadline) {\n\t\tpm.mu.RLock()\n\t\tready := pm.ready\n\t\thealthyCount := len(pm.healthyNodes)\n\t\tpm.mu.RUnlock()\n\n\t\tif ready || healthyCount > 0 {\n\t\t\treturn true\n\t\t}\n\t\ttime.Sleep(100 * time.Millisecond)\n\t}\n\tpm.mu.RLock()\n\tdefer pm.mu.RUnlock()\n\treturn pm.ready || len(pm.healthyNodes) > 0\n}\n\n// SetReady 设置就绪状态\nfunc (pm *ProxyManager) SetReady(ready bool) {\n\tpm.mu.Lock()\n\tdefer pm.mu.Unlock()\n\tpm.ready = ready\n\tif ready {\n\t\tpm.readyCond.Broadcast()\n\t}\n}\n\n// SetMaxPoolSize 设置最大实例池大小\nfunc (pm *ProxyManager) SetMaxPoolSize(size int) {\n\tpm.mu.Lock()\n\tdefer pm.mu.Unlock()\n\tif size > 0 {\n\t\tpm.maxPoolSize = size\n\t}\n}\n\n// InitInstancePool 初始化实例池（按需启动指定数量的代理实例）\nfunc (pm *ProxyManager) InitInstancePool(count int) error {\n\tpm.mu.Lock()\n\tdefer pm.mu.Unlock()\n\n\tif len(pm.healthyNodes) == 0 && len(pm.nodes) == 0 {\n\t\treturn fmt.Errorf(\"没有可用的代理节点\")\n\t}\n\n\tif count > pm.maxPoolSize {\n\t\tcount = pm.maxPoolSize\n\t}\n\n\tnodes := pm.healthyNodes\n\tif len(nodes) == 0 {\n\t\tnodes = pm.nodes\n\t}\n\n\tlog.Printf(\"🔧 初始化代理实例池: 目标 %d 个实例\", count)\n\n\tfor i := 0; i < count && i < len(nodes); i++ {\n\t\tnode := nodes[i%len(nodes)]\n\t\tinstance, err := pm.startInstanceLocked(node)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"⚠️ 启动实例 %d 失败: %v\", i, err)\n\t\t\tcontinue\n\t\t}\n\t\tinstance.status = InstanceStatusIdle\n\t\tpm.instancePool = append(pm.instancePool, instance)\n\t}\n\n\tlog.Printf(\"✅ 实例池初始化完成: %d 个实例就绪\", len(pm.instancePool))\n\treturn nil\n}\n\nfunc (pm *ProxyManager) SetXrayPath(path string) {\n}\n\n// AddSubscribeURL 添加订阅链接\nfunc (pm *ProxyManager) AddSubscribeURL(url string) {\n\turl = strings.TrimSpace(url)\n\tif url == \"\" {\n\t\treturn // 过滤空字符串\n\t}\n\tpm.mu.Lock()\n\tdefer pm.mu.Unlock()\n\tpm.subscribeURLs = append(pm.subscribeURLs, url)\n}\n\n// AddProxyFile 添加代理文件\nfunc (pm *ProxyManager) AddProxyFile(path string) {\n\tpath = strings.TrimSpace(path)\n\tif path == \"\" {\n\t\treturn // 过滤空字符串\n\t}\n\tpm.mu.Lock()\n\tdefer pm.mu.Unlock()\n\tpm.proxyFiles = append(pm.proxyFiles, path)\n}\n\n// LoadAll 加载所有代理源\nfunc (pm *ProxyManager) LoadAll() error {\n\tvar allNodes []*ProxyNode\n\n\t// 从订阅加载\n\tfor _, url := range pm.subscribeURLs {\n\t\tlog.Printf(\"🔄 正在加载订阅: %s\", url)\n\t\tnodes, err := pm.loadFromURL(url)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"⚠️ 加载订阅失败 %s: %v\", url, err)\n\t\t\tcontinue\n\t\t}\n\t\tlog.Printf(\"✅ 订阅加载成功: %d 个节点\", len(nodes))\n\t\tallNodes = append(allNodes, nodes...)\n\t}\n\n\t// 从文件加载\n\tfor _, file := range pm.proxyFiles {\n\t\tlog.Printf(\"🔄 正在加载代理文件: %s\", file)\n\t\tnodes, err := pm.loadFromFile(file)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"⚠️ 加载文件失败 %s: %v\", file, err)\n\t\t\tcontinue\n\t\t}\n\t\tlog.Printf(\"✅ 文件加载成功: %d 个节点\", len(nodes))\n\t\tallNodes = append(allNodes, nodes...)\n\t}\n\n\tpm.mu.Lock()\n\tpm.nodes = allNodes\n\tpm.lastUpdate = time.Now()\n\tpm.mu.Unlock()\n\n\tlog.Printf(\"✅ 共加载 %d 个代理节点 (订阅: %d, 文件: %d)\", len(allNodes), len(pm.subscribeURLs), len(pm.proxyFiles))\n\treturn nil\n}\n\ntype SubscriptionInfo struct {\n\tUpload   int64\n\tDownload int64\n\tTotal    int64\n\tExpire   int64\n}\n\n// parseSubscriptionUserinfo 解析 subscription-userinfo 头\nfunc parseSubscriptionUserinfo(header string) *SubscriptionInfo {\n\tif header == \"\" {\n\t\treturn nil\n\t}\n\tinfo := &SubscriptionInfo{}\n\tparts := strings.Split(header, \";\")\n\tfor _, part := range parts {\n\t\tkv := strings.SplitN(strings.TrimSpace(part), \"=\", 2)\n\t\tif len(kv) != 2 {\n\t\t\tcontinue\n\t\t}\n\t\tkey := strings.TrimSpace(kv[0])\n\t\tvalue, _ := strconv.ParseInt(strings.TrimSpace(kv[1]), 10, 64)\n\t\tswitch key {\n\t\tcase \"upload\":\n\t\t\tinfo.Upload = value\n\t\tcase \"download\":\n\t\t\tinfo.Download = value\n\t\tcase \"total\":\n\t\t\tinfo.Total = value\n\t\tcase \"expire\":\n\t\t\tinfo.Expire = value\n\t\t}\n\t}\n\treturn info\n}\n\n// getRemainingTraffic 获取剩余流量（字节）\nfunc (si *SubscriptionInfo) getRemainingTraffic() int64 {\n\tif si == nil || si.Total == 0 {\n\t\treturn -1 // 未知\n\t}\n\treturn si.Total - si.Upload - si.Download\n}\nfunc (pm *ProxyManager) loadFromURL(urlStr string) ([]*ProxyNode, error) {\n\tclient := &http.Client{Timeout: 30 * time.Second}\n\tresp, err := client.Get(urlStr)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\n\t// 检查订阅流量信息\n\tuserinfo := resp.Header.Get(\"subscription-userinfo\")\n\tif userinfo == \"\" {\n\t\tuserinfo = resp.Header.Get(\"Subscription-Userinfo\")\n\t}\n\tif subInfo := parseSubscriptionUserinfo(userinfo); subInfo != nil {\n\t\tremaining := subInfo.getRemainingTraffic()\n\t\tif remaining == 0 {\n\t\t\treturn nil, fmt.Errorf(\"订阅流量已耗尽\")\n\t\t}\n\t\tif remaining > 0 && remaining < 100*1024*1024 {\n\t\t}\n\t}\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn pm.parseContent(string(body))\n}\n\n// loadFromFile 从文件加载\nfunc (pm *ProxyManager) loadFromFile(path string) ([]*ProxyNode, error) {\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn pm.parseContent(string(data))\n}\n\nfunc (pm *ProxyManager) parseContent(content string) ([]*ProxyNode, error) {\n\tdecoded, err := base64.StdEncoding.DecodeString(strings.TrimSpace(content))\n\tif err == nil {\n\t\tcontent = string(decoded)\n\t}\n\n\tvar nodes []*ProxyNode\n\tlines := strings.Split(content, \"\\n\")\n\n\tfor _, line := range lines {\n\t\tline = strings.TrimSpace(line)\n\t\tif line == \"\" || strings.HasPrefix(line, \"#\") {\n\t\t\tcontinue\n\t\t}\n\n\t\tnode := pm.parseLine(line)\n\t\tif node != nil {\n\t\t\tnodes = append(nodes, node)\n\t\t}\n\t}\n\n\treturn nodes, nil\n}\n\n// tryBase64Decode 尝试多种 base64 解码方式\nfunc tryBase64Decode(s string) []byte {\n\ts = strings.TrimSpace(s)\n\t// 尝试标准 base64\n\tif decoded, err := base64.StdEncoding.DecodeString(s); err == nil {\n\t\treturn decoded\n\t}\n\t// 尝试 URL-safe base64\n\tif decoded, err := base64.URLEncoding.DecodeString(s); err == nil {\n\t\treturn decoded\n\t}\n\t// 尝试无填充的标准 base64\n\tif decoded, err := base64.RawStdEncoding.DecodeString(s); err == nil {\n\t\treturn decoded\n\t}\n\t// 尝试无填充的 URL-safe base64\n\tif decoded, err := base64.RawURLEncoding.DecodeString(s); err == nil {\n\t\treturn decoded\n\t}\n\treturn nil\n}\n\n// parseLine 解析单行\nfunc (pm *ProxyManager) parseLine(line string) *ProxyNode {\n\tif strings.HasPrefix(line, \"vmess://\") {\n\t\treturn parseVmess(line)\n\t}\n\tif strings.HasPrefix(line, \"vless://\") {\n\t\treturn parseVless(line)\n\t}\n\tif strings.HasPrefix(line, \"ss://\") {\n\t\treturn parseSS(line)\n\t}\n\tif strings.HasPrefix(line, \"trojan://\") {\n\t\treturn parseTrojan(line)\n\t}\n\tif strings.HasPrefix(line, \"hysteria2://\") || strings.HasPrefix(line, \"hy2://\") {\n\t\treturn parseHysteria2(line)\n\t}\n\tif strings.HasPrefix(line, \"anytls://\") {\n\t\treturn parseAnyTLS(line)\n\t}\n\tif strings.HasPrefix(line, \"http://\") || strings.HasPrefix(line, \"https://\") || strings.HasPrefix(line, \"socks5://\") {\n\t\treturn parseDirectProxy(line)\n\t}\n\treturn nil\n}\n\n// getStringFromMap 安全获取 map 中的字符串值\nfunc getStringFromMap(m map[string]interface{}, key string) string {\n\tif v, ok := m[key]; ok {\n\t\tswitch s := v.(type) {\n\t\tcase string:\n\t\t\treturn s\n\t\tcase float64:\n\t\t\treturn strconv.FormatFloat(s, 'f', -1, 64)\n\t\tcase int:\n\t\t\treturn strconv.Itoa(s)\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// getIntFromMap 安全获取 map 中的整数值\nfunc getIntFromMap(m map[string]interface{}, key string) int {\n\tif v, ok := m[key]; ok {\n\t\tswitch n := v.(type) {\n\t\tcase float64:\n\t\t\treturn int(n)\n\t\tcase int:\n\t\t\treturn n\n\t\tcase string:\n\t\t\ti, _ := strconv.Atoi(n)\n\t\t\treturn i\n\t\t}\n\t}\n\treturn 0\n}\n\n// parseVmess 解析 vmess 链接\nfunc parseVmess(link string) *ProxyNode {\n\t// vmess://base64(json)\n\tdata := strings.TrimPrefix(link, \"vmess://\")\n\tdecoded := tryBase64Decode(data)\n\tif decoded == nil {\n\t\treturn nil\n\t}\n\n\tvar config map[string]interface{}\n\tif err := json.Unmarshal(decoded, &config); err != nil {\n\t\treturn nil\n\t}\n\n\tnode := &ProxyNode{\n\t\tRaw:      link,\n\t\tProtocol: \"vmess\",\n\t}\n\n\tnode.Name = getStringFromMap(config, \"ps\")\n\tnode.Server = getStringFromMap(config, \"add\")\n\tnode.Port = getIntFromMap(config, \"port\")\n\tnode.UUID = getStringFromMap(config, \"id\")\n\tnode.AlterId = getIntFromMap(config, \"aid\")\n\n\t// 加密方式\n\tnode.Security = getStringFromMap(config, \"scy\")\n\tif node.Security == \"\" {\n\t\tnode.Security = \"auto\"\n\t}\n\n\t// 传输协议\n\tnode.Network = getStringFromMap(config, \"net\")\n\tif node.Network == \"\" {\n\t\tnode.Network = \"tcp\"\n\t}\n\n\t// 路径和 Host\n\tnode.Path = getStringFromMap(config, \"path\")\n\tnode.Host = getStringFromMap(config, \"host\")\n\n\t// TLS 设置（支持多种写法）\n\ttlsVal := getStringFromMap(config, \"tls\")\n\tif tlsVal != \"\" && tlsVal != \"none\" && tlsVal != \"0\" && tlsVal != \"false\" {\n\t\tnode.TLS = true\n\t}\n\tnode.SNI = getStringFromMap(config, \"sni\")\n\tif node.SNI == \"\" && node.TLS {\n\t\tnode.SNI = node.Host\n\t}\n\n\t// Header 类型（kcp/quic）\n\tnode.Type = getStringFromMap(config, \"type\")\n\n\tif node.Server == \"\" || node.Port == 0 || node.UUID == \"\" {\n\t\treturn nil\n\t}\n\treturn node\n}\n\n// parseVless 解析 vless 链接\nfunc parseVless(link string) *ProxyNode {\n\t// vless://uuid@server:port?params#name\n\tu, err := url.Parse(link)\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\tport, _ := strconv.Atoi(u.Port())\n\t// URL 解码名称\n\tname, _ := url.QueryUnescape(u.Fragment)\n\n\tnode := &ProxyNode{\n\t\tRaw:      link,\n\t\tProtocol: \"vless\",\n\t\tUUID:     u.User.Username(),\n\t\tServer:   u.Hostname(),\n\t\tPort:     port,\n\t\tName:     name,\n\t}\n\n\tquery := u.Query()\n\n\t// 传输协议（支持更多类型）\n\tnode.Network = query.Get(\"type\")\n\tif node.Network == \"\" {\n\t\tnode.Network = \"tcp\"\n\t}\n\n\t// 安全类型\n\tnode.Security = query.Get(\"security\")\n\tif node.Security == \"\" {\n\t\tnode.Security = \"none\"\n\t}\n\tif node.Security == \"tls\" || node.Security == \"reality\" {\n\t\tnode.TLS = true\n\t}\n\n\t// Flow（XTLS）\n\tnode.Flow = query.Get(\"flow\")\n\n\t// 路径（需要 URL 解码）\n\tif path := query.Get(\"path\"); path != \"\" {\n\t\tnode.Path, _ = url.QueryUnescape(path)\n\t}\n\n\t// Host\n\tnode.Host = query.Get(\"host\")\n\tif node.Host == \"\" {\n\t\tnode.Host = query.Get(\"sni\")\n\t}\n\n\t// SNI\n\tnode.SNI = query.Get(\"sni\")\n\tif node.SNI == \"\" && node.TLS && node.Security != \"reality\" {\n\t\tnode.SNI = node.Host\n\t\tif node.SNI == \"\" {\n\t\t\tnode.SNI = node.Server\n\t\t}\n\t}\n\n\t// Fingerprint（TLS/Reality 指纹）\n\tnode.Fingerprint = query.Get(\"fp\")\n\tif node.Fingerprint == \"\" {\n\t\tnode.Fingerprint = query.Get(\"fingerprint\")\n\t}\n\n\t// Reality 相关参数\n\tif node.Security == \"reality\" {\n\t\tnode.PublicKey = query.Get(\"pbk\")\n\t\tnode.ShortId = query.Get(\"sid\")\n\t\tnode.SpiderX = query.Get(\"spx\")\n\t\t// Reality 必须有 SNI\n\t\tif node.SNI == \"\" {\n\t\t\tnode.SNI = query.Get(\"serverName\")\n\t\t}\n\t}\n\n\t// ALPN\n\tnode.ALPN = query.Get(\"alpn\")\n\n\t// Header 类型（kcp/quic 等）\n\tnode.Type = query.Get(\"headerType\")\n\n\t// GRPC 服务名\n\tif serviceName := query.Get(\"serviceName\"); serviceName != \"\" && node.Network == \"grpc\" {\n\t\tnode.Path = serviceName\n\t}\n\n\t// xhttp/splithttp/httpupgrade 的额外参数\n\tif node.Network == \"xhttp\" || node.Network == \"splithttp\" || node.Network == \"httpupgrade\" {\n\t\tif node.Path == \"\" {\n\t\t\tnode.Path = \"/\"\n\t\t}\n\t}\n\n\tif node.Server == \"\" || node.Port == 0 || node.UUID == \"\" {\n\t\treturn nil\n\t}\n\treturn node\n}\n\n// xray-core 支持的 shadowsocks 加密方法\nvar supportedSSCiphers = map[string]bool{\n\t// AEAD 加密（推荐）\n\t\"aes-128-gcm\":             true,\n\t\"aes-256-gcm\":             true,\n\t\"chacha20-poly1305\":       true,\n\t\"chacha20-ietf-poly1305\":  true,\n\t\"xchacha20-poly1305\":      true,\n\t\"xchacha20-ietf-poly1305\": true,\n\t// 流式加密（xray-core 支持）\n\t\"aes-128-ctr\": true,\n\t\"aes-192-ctr\": true,\n\t\"aes-256-ctr\": true,\n\t// 其他支持的\n\t\"none\":                          true,\n\t\"plain\":                         true,\n\t\"2022-blake3-aes-128-gcm\":       true,\n\t\"2022-blake3-aes-256-gcm\":       true,\n\t\"2022-blake3-chacha20-poly1305\": true,\n}\n\n// 不支持的 cipher 方法映射（旧的 CFB/OFB 等）\nvar unsupportedSSCiphers = map[string]string{\n\t\"aes-128-cfb\":   \"\", // 不支持，跳过\n\t\"aes-192-cfb\":   \"\",\n\t\"aes-256-cfb\":   \"\",\n\t\"aes-128-ofb\":   \"\",\n\t\"aes-192-ofb\":   \"\",\n\t\"aes-256-ofb\":   \"\",\n\t\"bf-cfb\":        \"\",\n\t\"cast5-cfb\":     \"\",\n\t\"des-cfb\":       \"\",\n\t\"idea-cfb\":      \"\",\n\t\"rc2-cfb\":       \"\",\n\t\"rc4\":           \"\",\n\t\"rc4-md5\":       \"\",\n\t\"rc4-md5-6\":     \"\",\n\t\"seed-cfb\":      \"\",\n\t\"salsa20\":       \"\",\n\t\"chacha20\":      \"chacha20-ietf-poly1305\", // 尝试升级\n\t\"chacha20-ietf\": \"chacha20-ietf-poly1305\",\n}\n\n// isSupportedSSCipher 检查是否支持的 cipher\nfunc isSupportedSSCipher(method string) bool {\n\tmethod = strings.ToLower(method)\n\treturn supportedSSCiphers[method]\n}\n\n// tryMapSSCipher 尝试映射不支持的 cipher 到支持的\nfunc tryMapSSCipher(method string) (string, bool) {\n\tmethod = strings.ToLower(method)\n\tif isSupportedSSCipher(method) {\n\t\treturn method, true\n\t}\n\tif mapped, ok := unsupportedSSCiphers[method]; ok {\n\t\tif mapped == \"\" {\n\t\t\treturn \"\", false // 不支持且无法映射\n\t\t}\n\t\treturn mapped, true\n\t}\n\treturn \"\", false\n}\n\n// parseSS 解析 ss 链接\nfunc parseSS(link string) *ProxyNode {\n\t// 支持多种格式:\n\t// ss://base64(method:password)@host:port#name (SIP002)\n\t// ss://base64(method:password@host:port)#name (旧格式)\n\t// ss://method:password@host:port#name (明文格式)\n\torigLink := link\n\tlink = strings.TrimPrefix(link, \"ss://\")\n\n\tvar name string\n\tif idx := strings.Index(link, \"#\"); idx != -1 {\n\t\tname = link[idx+1:]\n\t\tlink = link[:idx]\n\t}\n\tname, _ = url.QueryUnescape(name)\n\n\tnode := &ProxyNode{\n\t\tProtocol: \"shadowsocks\",\n\t\tName:     name,\n\t}\n\n\t// 尝试解析 SIP002 格式: base64(method:password)@host:port\n\tif atIdx := strings.LastIndex(link, \"@\"); atIdx != -1 {\n\t\tuserInfo := link[:atIdx]\n\t\thostPort := link[atIdx+1:]\n\n\t\t// 尝试 base64 解码 userInfo\n\t\tif decoded := tryBase64Decode(userInfo); decoded != nil {\n\t\t\tparts := strings.SplitN(string(decoded), \":\", 2)\n\t\t\tif len(parts) == 2 {\n\t\t\t\tnode.Method = parts[0]\n\t\t\t\tnode.Password = parts[1]\n\t\t\t}\n\t\t} else {\n\t\t\t// 可能是明文格式 method:password\n\t\t\tparts := strings.SplitN(userInfo, \":\", 2)\n\t\t\tif len(parts) == 2 {\n\t\t\t\tnode.Method = parts[0]\n\t\t\t\tnode.Password = parts[1]\n\t\t\t}\n\t\t}\n\n\t\t// 解析 host:port（可能包含 IPv6）\n\t\tif strings.HasPrefix(hostPort, \"[\") {\n\t\t\t// IPv6: [::1]:port\n\t\t\tif endBracket := strings.Index(hostPort, \"]:\"); endBracket != -1 {\n\t\t\t\tnode.Server = hostPort[1:endBracket]\n\t\t\t\tnode.Port, _ = strconv.Atoi(hostPort[endBracket+2:])\n\t\t\t}\n\t\t} else {\n\t\t\tparts := strings.Split(hostPort, \":\")\n\t\t\tif len(parts) >= 2 {\n\t\t\t\tnode.Server = parts[0]\n\t\t\t\tnode.Port, _ = strconv.Atoi(parts[len(parts)-1])\n\t\t\t}\n\t\t}\n\t} else {\n\t\t// 旧格式: 整个内容是 base64 编码\n\t\tdecoded := tryBase64Decode(link)\n\t\tif decoded == nil {\n\t\t\treturn nil\n\t\t}\n\t\t// method:password@host:port\n\t\tdecodedStr := string(decoded)\n\t\tif atIdx := strings.LastIndex(decodedStr, \"@\"); atIdx != -1 {\n\t\t\tuserInfo := decodedStr[:atIdx]\n\t\t\thostPort := decodedStr[atIdx+1:]\n\n\t\t\tparts := strings.SplitN(userInfo, \":\", 2)\n\t\t\tif len(parts) == 2 {\n\t\t\t\tnode.Method = parts[0]\n\t\t\t\tnode.Password = parts[1]\n\t\t\t}\n\n\t\t\thpParts := strings.Split(hostPort, \":\")\n\t\t\tif len(hpParts) >= 2 {\n\t\t\t\tnode.Server = hpParts[0]\n\t\t\t\tnode.Port, _ = strconv.Atoi(hpParts[len(hpParts)-1])\n\t\t\t}\n\t\t}\n\t}\n\n\tnode.Raw = origLink\n\t// 验证必要字段\n\tif node.Server == \"\" || node.Port == 0 || node.Method == \"\" {\n\t\treturn nil\n\t}\n\n\t// 检查并映射 cipher 方法\n\tif mappedMethod, ok := tryMapSSCipher(node.Method); ok {\n\t\tif mappedMethod != node.Method {\n\t\t\tnode.Method = mappedMethod\n\t\t}\n\t} else {\n\t\treturn nil // 跳过不支持的节点\n\t}\n\treturn node\n}\n\n// parseTrojan 解析 trojan 链接\nfunc parseTrojan(link string) *ProxyNode {\n\t// trojan://password@server:port?params#name\n\tu, err := url.Parse(link)\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\tport, _ := strconv.Atoi(u.Port())\n\tname, _ := url.QueryUnescape(u.Fragment)\n\tnode := &ProxyNode{\n\t\tRaw:      link,\n\t\tProtocol: \"trojan\",\n\t\tPassword: u.User.Username(),\n\t\tServer:   u.Hostname(),\n\t\tPort:     port,\n\t\tName:     name,\n\t\tTLS:      true, // trojan 默认 TLS\n\t}\n\n\tquery := u.Query()\n\tnode.SNI = query.Get(\"sni\")\n\tif node.SNI == \"\" {\n\t\tnode.SNI = node.Server\n\t}\n\tif host := query.Get(\"host\"); host != \"\" {\n\t\tnode.Host = host\n\t}\n\n\t// 传输协议\n\tnode.Network = query.Get(\"type\")\n\tif node.Network == \"\" {\n\t\tnode.Network = \"tcp\"\n\t}\n\n\t// 路径\n\tif path := query.Get(\"path\"); path != \"\" {\n\t\tnode.Path, _ = url.QueryUnescape(path)\n\t}\n\n\t// Fingerprint\n\tnode.Fingerprint = query.Get(\"fp\")\n\n\t// ALPN\n\tnode.ALPN = query.Get(\"alpn\")\n\n\tif node.Server == \"\" || node.Port == 0 || node.Password == \"\" {\n\t\treturn nil\n\t}\n\treturn node\n}\n\n// parseHysteria2 解析 hysteria2/hy2 链接\nfunc parseHysteria2(link string) *ProxyNode {\n\t// hysteria2://password@server:port?params#name\n\t// hy2://password@server:port?params#name\n\tlink = strings.Replace(link, \"hy2://\", \"hysteria2://\", 1)\n\tu, err := url.Parse(link)\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\tport, _ := strconv.Atoi(u.Port())\n\tname, _ := url.QueryUnescape(u.Fragment)\n\n\tnode := &ProxyNode{\n\t\tRaw:      link,\n\t\tProtocol: \"hysteria2\",\n\t\tPassword: u.User.Username(),\n\t\tServer:   u.Hostname(),\n\t\tPort:     port,\n\t\tName:     name,\n\t\tTLS:      true, // hysteria2 默认 TLS\n\t}\n\n\tquery := u.Query()\n\tnode.SNI = query.Get(\"sni\")\n\tif node.SNI == \"\" {\n\t\tnode.SNI = node.Server\n\t}\n\n\t// ALPN\n\tnode.ALPN = query.Get(\"alpn\")\n\tif node.ALPN == \"\" {\n\t\tnode.ALPN = \"h3\"\n\t}\n\n\t// Fingerprint\n\tnode.Fingerprint = query.Get(\"pinSHA256\")\n\n\t// obfs\n\tif obfs := query.Get(\"obfs\"); obfs != \"\" {\n\t\tnode.Type = obfs\n\t\tnode.Path = query.Get(\"obfs-password\")\n\t}\n\n\tif node.Server == \"\" || node.Port == 0 || node.Password == \"\" {\n\t\treturn nil\n\t}\n\treturn node\n}\n\n// parseAnyTLS 解析 anytls 链接\nfunc parseAnyTLS(link string) *ProxyNode {\n\t// anytls://password@server:port?params#name\n\tu, err := url.Parse(link)\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\tport, _ := strconv.Atoi(u.Port())\n\tname, _ := url.QueryUnescape(u.Fragment)\n\n\tnode := &ProxyNode{\n\t\tRaw:      link,\n\t\tProtocol: \"anytls\",\n\t\tPassword: u.User.Username(),\n\t\tServer:   u.Hostname(),\n\t\tPort:     port,\n\t\tName:     name,\n\t\tTLS:      true,\n\t}\n\n\tquery := u.Query()\n\tnode.SNI = query.Get(\"sni\")\n\tif node.SNI == \"\" {\n\t\tnode.SNI = query.Get(\"serverName\")\n\t}\n\tif node.SNI == \"\" {\n\t\tnode.SNI = node.Server\n\t}\n\n\t// Fingerprint\n\tnode.Fingerprint = query.Get(\"fp\")\n\tif node.Fingerprint == \"\" {\n\t\tnode.Fingerprint = query.Get(\"fingerprint\")\n\t}\n\n\t// ALPN\n\tnode.ALPN = query.Get(\"alpn\")\n\n\t// insecure\n\tif query.Get(\"allowInsecure\") == \"1\" || query.Get(\"insecure\") == \"1\" {\n\t\t// 标记跳过证书验证\n\t}\n\n\tif node.Server == \"\" || node.Port == 0 || node.Password == \"\" {\n\t\treturn nil\n\t}\n\treturn node\n}\n\n// parseDirectProxy 解析直接代理\nfunc parseDirectProxy(link string) *ProxyNode {\n\tu, err := url.Parse(link)\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\tport, _ := strconv.Atoi(u.Port())\n\tif port == 0 {\n\t\tif u.Scheme == \"https\" {\n\t\t\tport = 443\n\t\t} else {\n\t\t\tport = 80\n\t\t}\n\t}\n\n\treturn &ProxyNode{\n\t\tRaw:       link,\n\t\tProtocol:  u.Scheme,\n\t\tServer:    u.Hostname(),\n\t\tPort:      port,\n\t\tLocalPort: port, // 直接代理使用原端口\n\t\tHealthy:   true,\n\t}\n}\n\n// startInstanceLocked 内部方法：启动实例（使用 sing-box）\nfunc (pm *ProxyManager) startInstanceLocked(node *ProxyNode) (*ProxyInstance, error) {\n\t// 直接代理不需要启动\n\tif node.Protocol == \"http\" || node.Protocol == \"https\" || node.Protocol == \"socks5\" {\n\t\treturn &ProxyInstance{\n\t\t\tnode:     node,\n\t\t\trunning:  true,\n\t\t\tstatus:   InstanceStatusIdle,\n\t\t\tproxyURL: node.Raw,\n\t\t\tlastUsed: time.Now(),\n\t\t}, nil\n\t}\n\n\t// 使用 sing-box 启动代理\n\tproxyURL, err := singboxMgr.Start(node)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"sing-box 启动失败: %w\", err)\n\t}\n\n\treturn &ProxyInstance{\n\t\tnode:      node,\n\t\trunning:   true,\n\t\tstatus:    InstanceStatusIdle,\n\t\tproxyURL:  proxyURL,\n\t\tlastUsed:  time.Now(),\n\t\tlocalPort: node.LocalPort,\n\t}, nil\n}\n\nfunc (pm *ProxyManager) StartXray(node *ProxyNode) (string, error) {\n\tpm.mu.Lock()\n\tdefer pm.mu.Unlock()\n\n\tinstance, err := pm.startInstanceLocked(node)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn instance.proxyURL, nil\n}\n\n// StopProxy 停止代理实例\nfunc (pm *ProxyManager) StopProxy(localPort int) {\n\tsingboxMgr.Stop(localPort)\n}\n\n// StopXray 停止代理实例（兼容旧接口）\nfunc (pm *ProxyManager) StopXray(localPort int) {\n\tpm.StopProxy(localPort)\n}\n\n// StopAll 停止所有实例\nfunc (pm *ProxyManager) StopAll() {\n\tsingboxMgr.StopAll()\n\tlog.Printf(\"🛑 所有代理实例已停止\")\n}\n\n// 健康检查备选URL列表\nvar healthCheckURLs = []string{\n\t\"https://cp.cloudflare.com/generate_204\",\n}\n\n// CheckHealth 检查节点健康并获取出口IP（优化：使用 StartRaw 避免双重测试）\nfunc (pm *ProxyManager) CheckHealth(node *ProxyNode) bool {\n\t// 直接代理不需要启动\n\tif node.Protocol == \"http\" || node.Protocol == \"https\" || node.Protocol == \"socks5\" {\n\t\tnode.Healthy = true\n\t\tnode.LastCheck = time.Now()\n\t\treturn true\n\t}\n\n\t// 使用 StartRaw 只启动不测试，避免双重测试\n\tproxyURL, err := singboxMgr.StartRaw(node)\n\tif err != nil {\n\t\treturn false\n\t}\n\tdefer pm.StopXray(node.LocalPort)\n\n\ttransport := &http.Transport{\n\t\tTLSClientConfig: tlsConfig,\n\t}\n\tif proxyURL != \"\" {\n\t\tproxy, _ := url.Parse(proxyURL)\n\t\ttransport.Proxy = http.ProxyURL(proxy)\n\t}\n\n\tclient := &http.Client{\n\t\tTransport: transport,\n\t\tTimeout:   HealthCheckTimeout,\n\t}\n\n\t// 基本连通性检查（支持多URL重试）\n\tvar success bool\n\tvar latency time.Duration\n\tfor _, testURL := range healthCheckURLs {\n\t\tstart := time.Now()\n\t\tresp, err := client.Get(testURL)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tresp.Body.Close()\n\t\tif resp.StatusCode == 204 || resp.StatusCode == 200 {\n\t\t\tlatency = time.Since(start)\n\t\t\tsuccess = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif !success {\n\t\treturn false\n\t}\n\tnode.Latency = latency\n\n\t// 获取出口IP（可选，失败不影响健康状态）\n\tipClient := &http.Client{\n\t\tTransport: transport,\n\t\tTimeout:   3 * time.Second,\n\t}\n\tipResp, err := ipClient.Get(\"https://ipinfo.io/ip\")\n\tif err == nil {\n\t\tdefer ipResp.Body.Close()\n\t\tif ipResp.StatusCode == 200 {\n\t\t\tipBytes, _ := io.ReadAll(ipResp.Body)\n\t\t\tnode.ExitIP = strings.TrimSpace(string(ipBytes))\n\t\t}\n\t}\n\n\treturn true\n}\n\n// CheckHealthQuick 快速健康检查（优化：使用 StartRaw 避免双重测试）\nfunc (pm *ProxyManager) CheckHealthQuick(node *ProxyNode) bool {\n\t// 直接代理不需要启动\n\tif node.Protocol == \"http\" || node.Protocol == \"https\" || node.Protocol == \"socks5\" {\n\t\treturn true\n\t}\n\n\t// 使用 StartRaw 只启动不测试\n\tproxyURL, err := singboxMgr.StartRaw(node)\n\tif err != nil {\n\t\treturn false\n\t}\n\tdefer pm.StopXray(node.LocalPort)\n\n\ttransport := &http.Transport{\n\t\tTLSClientConfig: tlsConfig,\n\t}\n\tif proxyURL != \"\" {\n\t\tproxy, _ := url.Parse(proxyURL)\n\t\ttransport.Proxy = http.ProxyURL(proxy)\n\t}\n\n\tclient := &http.Client{\n\t\tTransport: transport,\n\t\tTimeout:   6 * time.Second,\n\t}\n\n\t// 尝试多个测试URL\n\tfor _, testURL := range healthCheckURLs {\n\t\tstart := time.Now()\n\t\tresp, err := client.Get(testURL)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tresp.Body.Close()\n\t\tif resp.StatusCode == 204 || resp.StatusCode == 200 {\n\t\t\tnode.Latency = time.Since(start)\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (pm *ProxyManager) CheckAllHealth() {\n\tpm.mu.Lock()\n\tif pm.healthChecking {\n\t\tpm.mu.Unlock()\n\t\treturn\n\t}\n\tpm.healthChecking = true\n\thasSubscribes := len(pm.subscribeURLs) > 0\n\tpm.mu.Unlock()\n\tif hasSubscribes {\n\t\tif err := pm.LoadAll(); err != nil {\n\t\t\tlog.Printf(\"⚠️ 刷新订阅失败: %v\", err)\n\t\t}\n\t}\n\n\tpm.mu.Lock()\n\tnodes := make([]*ProxyNode, len(pm.nodes))\n\tcopy(nodes, pm.nodes)\n\tpm.mu.Unlock()\n\n\tif len(nodes) == 0 {\n\t\tpm.mu.Lock()\n\t\tpm.healthChecking = false\n\t\tpm.mu.Unlock()\n\t\tpm.SetReady(true)\n\t\treturn\n\t}\n\tvar healthyNodes, unhealthyNodes, newNodes []*ProxyNode\n\tfor _, n := range nodes {\n\t\tif n.LastCheck.IsZero() {\n\t\t\tnewNodes = append(newNodes, n)\n\t\t} else if n.Healthy {\n\t\t\thealthyNodes = append(healthyNodes, n)\n\t\t} else {\n\t\t\tunhealthyNodes = append(unhealthyNodes, n)\n\t\t}\n\t}\n\tvar healthy []*ProxyNode\n\tvar checked int32\n\tvar mainWg sync.WaitGroup\n\tvar mu sync.Mutex\n\tipSeen := make(map[string]bool)\n\ttotal := len(nodes)\n\n\t// 检查单个节点的函数\n\tcheckNode := func(n *ProxyNode, sem chan struct{}) {\n\t\tsem <- struct{}{}\n\t\tdefer func() { <-sem }()\n\n\t\tn.Healthy = pm.CheckHealth(n)\n\t\tn.LastCheck = time.Now()\n\n\t\tcurrent := int(atomic.AddInt32(&checked, 1))\n\n\t\tmu.Lock()\n\t\tif n.Healthy {\n\t\t\tif n.ExitIP != \"\" {\n\t\t\t\tif ipSeen[n.ExitIP] {\n\t\t\t\t\tfor i, existing := range healthy {\n\t\t\t\t\t\tif existing.ExitIP == n.ExitIP && n.Latency < existing.Latency {\n\t\t\t\t\t\t\thealthy[i] = n\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tipSeen[n.ExitIP] = true\n\t\t\t\t\thealthy = append(healthy, n)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\thealthy = append(healthy, n)\n\t\t\t}\n\t\t\tif len(healthy) >= MinHealthyForReady {\n\t\t\t\tpm.mu.Lock()\n\t\t\t\tif !pm.ready {\n\t\t\t\t\tpm.ready = true\n\t\t\t\t\tpm.healthyNodes = healthy\n\t\t\t\t\tpm.readyCond.Broadcast()\n\t\t\t\t}\n\t\t\t\tpm.mu.Unlock()\n\t\t\t}\n\t\t}\n\t\thealthyCount := len(healthy)\n\t\tmu.Unlock()\n\n\t\tif current%50 == 0 || current == total {\n\t\t\tlog.Printf(\"🔍 进度: %d/%d, 健康: %d\", current, total, healthyCount)\n\t\t}\n\t}\n\tmainWg.Add(1)\n\tgo func() {\n\t\tdefer mainWg.Done()\n\t\tvar wg sync.WaitGroup\n\t\tsem := make(chan struct{}, 32)\n\t\tfor _, n := range healthyNodes {\n\t\t\twg.Add(1)\n\t\t\tgo func(node *ProxyNode) {\n\t\t\t\tdefer wg.Done()\n\t\t\t\tcheckNode(node, sem)\n\t\t\t}(n)\n\t\t}\n\t\twg.Wait()\n\t}()\n\tmainWg.Add(1)\n\tgo func() {\n\t\tdefer mainWg.Done()\n\t\tvar wg sync.WaitGroup\n\t\tsem := make(chan struct{}, 32)\n\t\tfor _, n := range unhealthyNodes {\n\t\t\twg.Add(1)\n\t\t\tgo func(node *ProxyNode) {\n\t\t\t\tdefer wg.Done()\n\t\t\t\tcheckNode(node, sem)\n\t\t\t}(n)\n\t\t}\n\t\twg.Wait()\n\t}()\n\tmainWg.Add(1)\n\tgo func() {\n\t\tdefer mainWg.Done()\n\t\tvar wg sync.WaitGroup\n\t\tsem := make(chan struct{}, 32)\n\t\tfor _, n := range newNodes {\n\t\t\twg.Add(1)\n\t\t\tgo func(node *ProxyNode) {\n\t\t\t\tdefer wg.Done()\n\t\t\t\tcheckNode(node, sem)\n\t\t\t}(n)\n\t\t}\n\t\twg.Wait()\n\t}()\n\n\tmainWg.Wait()\n\n\t// 按延迟排序（延迟低的排前面）\n\tsort.Slice(healthy, func(i, j int) bool {\n\t\treturn healthy[i].Latency < healthy[j].Latency\n\t})\n\n\tpm.mu.Lock()\n\tpm.healthyNodes = healthy\n\tpm.healthChecking = false\n\t// 只有达到最少健康节点数才提示就绪\n\tpm.ready = len(healthy) >= MinHealthyForReady\n\tpm.readyCond.Broadcast()\n\tpm.mu.Unlock()\n\n\t// 输出健康检查结果\n\tuniqueIPs := len(ipSeen)\n\tif len(healthy) > 0 {\n\t\ttopN := 5\n\t\tif len(healthy) < topN {\n\t\t\ttopN = len(healthy)\n\t\t}\n\t\tlog.Printf(\"✅ 健康检查完成: %d/%d 节点可用 \",\n\t\t\tlen(healthy), total)\n\t\tlog.Printf(\"📊 最快前%d节点: %v ~ %v\",\n\t\t\ttopN, healthy[0].Latency.Round(time.Millisecond),\n\t\t\thealthy[topN-1].Latency.Round(time.Millisecond))\n\n\t\t// 输出IP分布信息\n\t\tif uniqueIPs < len(healthy) {\n\t\t}\n\t} else {\n\t\tlog.Printf(\"⚠️ 健康检查完成: 0/%d 节点可用\", total)\n\t}\n\n\t// 就绪状态提示\n\tif pm.ready {\n\t\tlog.Printf(\"🟢 代理池就绪 (健康节点: %d >= 最低要求: %d)\", len(healthy), MinHealthyForReady)\n\t} else {\n\t\tlog.Printf(\"🔴 代理池未就绪 (健康节点: %d < 最低要求: %d)\", len(healthy), MinHealthyForReady)\n\t}\n}\n\n// GetFromPool 从实例池获取一个空闲实例\nfunc (pm *ProxyManager) GetFromPool() *ProxyInstance {\n\tpm.mu.Lock()\n\tdefer pm.mu.Unlock()\n\n\t// 查找空闲实例\n\tfor _, inst := range pm.instancePool {\n\t\tinst.mu.Lock()\n\t\tif inst.status == InstanceStatusIdle && inst.running {\n\t\t\tinst.status = InstanceStatusInUse\n\t\t\tinst.lastUsed = time.Now()\n\t\t\tinst.mu.Unlock()\n\t\t\treturn inst\n\t\t}\n\t\tinst.mu.Unlock()\n\t}\n\treturn nil\n}\n\n// ReturnToPool 归还实例到池\nfunc (pm *ProxyManager) ReturnToPool(inst *ProxyInstance) {\n\tif inst == nil {\n\t\treturn\n\t}\n\tinst.mu.Lock()\n\tinst.status = InstanceStatusIdle\n\tinst.mu.Unlock()\n}\n\n// ReleaseByURL 通过proxyURL停止并释放实例\nfunc (pm *ProxyManager) ReleaseByURL(proxyURL string) {\n\tpm.mu.Lock()\n\t// 查找并移除实例\n\tvar toStop *ProxyInstance\n\tfor i, inst := range pm.instancePool {\n\t\tinst.mu.Lock()\n\t\tif inst.proxyURL == proxyURL {\n\t\t\ttoStop = inst\n\t\t\t// 从池中移除\n\t\t\tpm.instancePool = append(pm.instancePool[:i], pm.instancePool[i+1:]...)\n\t\t\tinst.mu.Unlock()\n\t\t\tbreak\n\t\t}\n\t\tinst.mu.Unlock()\n\t}\n\tpm.mu.Unlock()\n\n\t// 停止实例（在锁外执行）\n\tif toStop != nil {\n\t\tpm.StopXray(toStop.localPort)\n\t}\n}\n\nfunc (pm *ProxyManager) Next() string {\n\tpm.mu.Lock()\n\tdefer pm.mu.Unlock()\n\n\tif len(pm.healthyNodes) == 0 && len(pm.nodes) == 0 {\n\t\treturn \"\"\n\t}\n\n\tnow := time.Now()\n\tvar selectedNode *ProxyNode\n\tvar selectedIdx int = -1\n\n\t// 从健康节点列表中找第一个可用的\n\tfor i, node := range pm.healthyNodes {\n\t\t// 检查冷却时间\n\t\tcooldown := node.UseCooldown\n\t\tif cooldown == 0 {\n\t\t\tcooldown = DefaultProxyUseCooldown\n\t\t}\n\t\tif now.Sub(node.LastUsed) < cooldown {\n\t\t\tcontinue\n\t\t}\n\t\t// 跳过失败次数过多的节点\n\t\tif node.FailCount >= MaxProxyFailCount {\n\t\t\tcontinue\n\t\t}\n\t\tselectedNode = node\n\t\tselectedIdx = i\n\t\tbreak\n\t}\n\n\t// 如果健康节点都不可用，尝试普通节点\n\tif selectedNode == nil {\n\t\tfor i, node := range pm.nodes {\n\t\t\tcooldown := node.UseCooldown\n\t\t\tif cooldown == 0 {\n\t\t\t\tcooldown = DefaultProxyUseCooldown\n\t\t\t}\n\t\t\tif now.Sub(node.LastUsed) < cooldown {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif node.FailCount >= MaxProxyFailCount {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tselectedNode = node\n\t\t\tselectedIdx = i\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// 如果所有节点都在冷却中，选择最久未用的健康节点\n\tif selectedNode == nil {\n\t\tvar oldest *ProxyNode\n\t\tvar oldestIdx int\n\t\tfor i, node := range pm.healthyNodes {\n\t\t\tif oldest == nil || node.LastUsed.Before(oldest.LastUsed) {\n\t\t\t\toldest = node\n\t\t\t\toldestIdx = i\n\t\t\t}\n\t\t}\n\t\tif oldest == nil {\n\t\t\tfor i, node := range pm.nodes {\n\t\t\t\tif oldest == nil || node.LastUsed.Before(oldest.LastUsed) {\n\t\t\t\t\toldest = node\n\t\t\t\t\toldestIdx = i\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif oldest == nil {\n\t\t\treturn \"\"\n\t\t}\n\t\tselectedNode = oldest\n\t\tselectedIdx = oldestIdx\n\t}\n\n\tselectedNode.LastUsed = now\n\tisFromHealthy := false\n\tfor i, node := range pm.healthyNodes {\n\t\tif node == selectedNode {\n\t\t\tisFromHealthy = true\n\t\t\tselectedIdx = i\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif isFromHealthy && selectedIdx >= 0 && selectedIdx < len(pm.healthyNodes) {\n\t\t// 从健康节点列表移动到末尾\n\t\tpm.healthyNodes = append(pm.healthyNodes[:selectedIdx], pm.healthyNodes[selectedIdx+1:]...)\n\t\tpm.healthyNodes = append(pm.healthyNodes, selectedNode)\n\t} else if !isFromHealthy {\n\t\t// 从普通节点列表移动到末尾\n\t\tfor i, node := range pm.nodes {\n\t\t\tif node == selectedNode {\n\t\t\t\tpm.nodes = append(pm.nodes[:i], pm.nodes[i+1:]...)\n\t\t\t\tpm.nodes = append(pm.nodes, selectedNode)\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\t// 启动新实例\n\tinstance, err := pm.startInstanceLocked(selectedNode)\n\tif err != nil {\n\t\tlog.Printf(\"⚠️ 启动代理失败: %v\", err)\n\t\tselectedNode.FailCount++\n\t\t// 失败后增加冷却时间\n\t\tselectedNode.UseCooldown = time.Duration(selectedNode.FailCount) * 10 * time.Second\n\t\treturn \"\"\n\t}\n\tinstance.status = InstanceStatusInUse\n\n\t// 始终追踪实例（用于 MarkProxyFailed/ReleaseByURL）\n\tpm.instancePool = append(pm.instancePool, instance)\n\treturn instance.proxyURL\n}\n\n// MarkProxyFailed 标记代理失败（如403等）\nfunc (pm *ProxyManager) MarkProxyFailed(proxyURL string) {\n\tpm.mu.Lock()\n\tdefer pm.mu.Unlock()\n\n\t// 从实例池找到对应节点\n\tfor _, inst := range pm.instancePool {\n\t\tif inst.proxyURL == proxyURL && inst.node != nil {\n\t\t\tinst.node.FailCount++\n\t\t\t// 失败后动态增加冷却时间\n\t\t\tinst.node.UseCooldown = time.Duration(inst.node.FailCount) * 15 * time.Second\n\t\t\tif inst.node.UseCooldown > 2*time.Minute {\n\t\t\t\tinst.node.UseCooldown = 2 * time.Minute\n\t\t\t}\n\t\t\tlog.Printf(\"⚠️ 代理失败标记: %s, 失败次数=%d, 冷却=%v\",\n\t\t\t\tinst.node.Name, inst.node.FailCount, inst.node.UseCooldown)\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// MarkProxySuccess 标记代理成功（重置失败计数）\nfunc (pm *ProxyManager) MarkProxySuccess(proxyURL string) {\n\tpm.mu.Lock()\n\tdefer pm.mu.Unlock()\n\n\tfor _, inst := range pm.instancePool {\n\t\tif inst.proxyURL == proxyURL && inst.node != nil {\n\t\t\tinst.node.FailCount = 0\n\t\t\tinst.node.UseCooldown = DefaultProxyUseCooldown\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// PoolStats 返回实例池统计\nfunc (pm *ProxyManager) PoolStats() map[string]int {\n\tpm.mu.RLock()\n\tdefer pm.mu.RUnlock()\n\n\tidle, inUse := 0, 0\n\tfor _, inst := range pm.instancePool {\n\t\tinst.mu.Lock()\n\t\tswitch inst.status {\n\t\tcase InstanceStatusIdle:\n\t\t\tidle++\n\t\tcase InstanceStatusInUse:\n\t\t\tinUse++\n\t\t}\n\t\tinst.mu.Unlock()\n\t}\n\treturn map[string]int{\n\t\t\"idle\":   idle,\n\t\t\"in_use\": inUse,\n\t\t\"total\":  len(pm.instancePool),\n\t}\n}\n\n// Count 获取代理数量\nfunc (pm *ProxyManager) Count() int {\n\tpm.mu.RLock()\n\tdefer pm.mu.RUnlock()\n\tif len(pm.healthyNodes) > 0 {\n\t\treturn len(pm.healthyNodes)\n\t}\n\treturn len(pm.nodes)\n}\n\n// HealthyCount 获取健康代理数量\nfunc (pm *ProxyManager) HealthyCount() int {\n\tpm.mu.RLock()\n\tdefer pm.mu.RUnlock()\n\treturn len(pm.healthyNodes)\n}\n\n// TotalCount 获取总代理数量\nfunc (pm *ProxyManager) TotalCount() int {\n\tpm.mu.RLock()\n\tdefer pm.mu.RUnlock()\n\treturn len(pm.nodes)\n}\n\n// StartAutoUpdate 启动自动更新和健康检查\nfunc (pm *ProxyManager) StartAutoUpdate() {\n\t// 自动更新订阅\n\tgo func() {\n\t\tfor {\n\t\t\ttime.Sleep(pm.updateInterval)\n\t\t\tif len(pm.subscribeURLs) > 0 || len(pm.proxyFiles) > 0 {\n\t\t\t\tif err := pm.LoadAll(); err != nil {\n\t\t\t\t\tlog.Printf(\"⚠️ 自动更新代理失败: %v\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}()\n\n\t// 后台健康检查（启动时立即开始，不阻塞）\n\tgo func() {\n\t\t// 延迟几秒后开始首次检查\n\t\ttime.Sleep(3 * time.Second)\n\t\tpm.CheckAllHealth()\n\n\t\t// 定期检查\n\t\tfor {\n\t\t\ttime.Sleep(pm.checkInterval)\n\t\t\tpm.CheckAllHealth()\n\t\t}\n\t}()\n}\n\n// SetProxies 直接设置代理（兼容旧接口）\nfunc (pm *ProxyManager) SetProxies(proxies []string) {\n\tvar nodes []*ProxyNode\n\tfor _, p := range proxies {\n\t\tif node := pm.parseLine(p); node != nil {\n\t\t\tnodes = append(nodes, node)\n\t\t}\n\t}\n\tpm.mu.Lock()\n\tpm.nodes = nodes\n\tpm.healthyNodes = nodes // 假设都健康\n\tpm.mu.Unlock()\n\tlog.Printf(\"✅ 代理池已设置 %d 个代理\", len(nodes))\n}\n\nconst (\n\tautoRegisterURL      = \"https://jgpyjc.top/api/v1/passport/auth/register\"\n\tautoSubscribeBaseURL = \"https://bb1.jgpyjc.top/api/v1/client/subscribe?token=\"\n\tautoRegisterInterval = 1 * time.Hour\n)\n\n// AutoSubscriber 自动订阅管理器\ntype AutoSubscriber struct {\n\tmu              sync.RWMutex\n\tcurrentToken    string\n\tsubscribeURL    string\n\tlastRefresh     time.Time\n\trunning         bool\n\tstopChan        chan struct{}\n\tproxyManager    *ProxyManager\n\trefreshInterval time.Duration\n}\n\nvar autoSubscriber = &AutoSubscriber{\n\trefreshInterval: autoRegisterInterval,\n\tstopChan:        make(chan struct{}),\n}\n\n// randString 生成随机字符串\nfunc randString(n int) string {\n\tconst letters = \"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\"\n\tout := make([]byte, n)\n\tfor i := 0; i < n; i++ {\n\t\tr, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letters))))\n\t\tout[i] = letters[r.Int64()]\n\t}\n\treturn string(out)\n}\n\n// ungzipIfNeeded 解压 gzip 数据\nfunc ungzipIfNeeded(data []byte, header http.Header) ([]byte, error) {\n\tce := strings.ToLower(header.Get(\"Content-Encoding\"))\n\tif ce == \"gzip\" || (len(data) >= 2 && data[0] == 0x1f && data[1] == 0x8b) {\n\t\tr, err := gzip.NewReader(bytes.NewReader(data))\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tdefer r.Close()\n\t\treturn io.ReadAll(r)\n\t}\n\treturn data, nil\n}\n\n// extractToken 从响应中提取 token\nfunc extractToken(body []byte) string {\n\tvar j interface{}\n\tif err := json.Unmarshal(body, &j); err != nil {\n\t\treturn \"\"\n\t}\n\n\tvar walk func(interface{}) string\n\twalk = func(x interface{}) string {\n\t\tswitch v := x.(type) {\n\t\tcase map[string]interface{}:\n\t\t\tfor _, key := range []string{\"token\", \"access_token\", \"data\", \"result\", \"auth\", \"jwt\"} {\n\t\t\t\tif val, ok := v[key]; ok {\n\t\t\t\t\tif s, ok2 := val.(string); ok2 && s != \"\" {\n\t\t\t\t\t\treturn s\n\t\t\t\t\t}\n\t\t\t\t\tif res := walk(val); res != \"\" {\n\t\t\t\t\t\treturn res\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t// 检查 JWT 格式\n\t\t\tfor _, val := range v {\n\t\t\t\tif s, ok := val.(string); ok && looksLikeJWT(s) {\n\t\t\t\t\treturn s\n\t\t\t\t}\n\t\t\t}\n\t\tcase []interface{}:\n\t\t\tfor _, item := range v {\n\t\t\t\tif res := walk(item); res != \"\" {\n\t\t\t\t\treturn res\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn \"\"\n\t}\n\treturn walk(j)\n}\n\n// looksLikeJWT 判断是否像 JWT\nfunc looksLikeJWT(s string) bool {\n\tparts := strings.Count(s, \".\")\n\treturn parts >= 2 && len(s) > 30\n}\n\n// 常见邮箱域名\nvar emailDomains = []string{\n\t\"gmail.com\", \"yahoo.com\", \"outlook.com\", \"hotmail.com\", \"icloud.com\",\n\t\"protonmail.com\", \"mail.com\", \"zoho.com\", \"aol.com\", \"yandex.com\",\n\t\"163.com\", \"qq.com\", \"126.com\", \"sina.com\", \"foxmail.com\",\n}\n\n// doAutoRegister 执行一次自动注册\nfunc doAutoRegister() (email, password, token string, err error) {\n\t// 随机邮箱：随机用户名 + 随机域名\n\tdomainIdx, _ := rand.Int(rand.Reader, big.NewInt(int64(len(emailDomains))))\n\temail = randString(8+int(domainIdx.Int64()%5)) + \"@\" + emailDomains[domainIdx.Int64()]\n\tpassword = randString(20)\n\n\tform := url.Values{}\n\tform.Set(\"email\", email)\n\tform.Set(\"password\", password)\n\tform.Set(\"invite_code\", \"odtRDsfd\")\n\tform.Set(\"email_code\", \"\")\n\n\treq, err := http.NewRequest(\"POST\", autoRegisterURL, strings.NewReader(form.Encode()))\n\tif err != nil {\n\t\treturn \"\", \"\", \"\", err\n\t}\n\n\treq.Header.Set(\"User-Agent\", \"Mozilla/5.0 (Linux; Android 10)\")\n\treq.Header.Set(\"Accept-Encoding\", \"gzip\")\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\treq.Header.Set(\"Origin\", \"https://jgpyjc.top\")\n\treq.Header.Set(\"Referer\", \"https://jgpyjc.top/\")\n\n\tclient := &http.Client{Timeout: 30 * time.Second}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn email, password, \"\", err\n\t}\n\tdefer resp.Body.Close()\n\n\traw, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn email, password, \"\", err\n\t}\n\n\tbody, err := ungzipIfNeeded(raw, resp.Header)\n\tif err != nil {\n\t\tbody = raw\n\t}\n\n\ttoken = extractToken(body)\n\tif token == \"\" {\n\t\ts := strings.TrimSpace(string(body))\n\t\tif looksLikeJWT(s) {\n\t\t\ttoken = s\n\t\t}\n\t}\n\n\tif token == \"\" {\n\t\treturn email, password, \"\", fmt.Errorf(\"未能从响应中提取 token: %s\", string(body[:min(200, len(body))]))\n\t}\n\treturn email, password, token, nil\n}\n\n// refreshSubscription 刷新订阅\nfunc (as *AutoSubscriber) refreshSubscription() error {\n\n\t_, _, token, err := doAutoRegister()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"注册失败: %w\", err)\n\t}\n\n\tsubscribeURL := autoSubscribeBaseURL + token\n\n\tas.mu.Lock()\n\tas.currentToken = token\n\tas.subscribeURL = subscribeURL\n\tas.lastRefresh = time.Now()\n\tas.mu.Unlock()\n\t// 加载订阅到代理池\n\tif as.proxyManager != nil {\n\t\tif err := as.loadToProxyManager(); err != nil {\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (as *AutoSubscriber) loadToProxyManager() error {\n\tas.mu.RLock()\n\tsubURL := as.subscribeURL\n\tas.mu.RUnlock()\n\n\tif subURL == \"\" {\n\t\treturn fmt.Errorf(\"订阅URL为空\")\n\t}\n\n\tnodes, err := as.proxyManager.loadFromURL(subURL)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif len(nodes) == 0 {\n\t\treturn fmt.Errorf(\"订阅中没有可用节点\")\n\t}\n\n\tas.proxyManager.mu.Lock()\n\tas.proxyManager.nodes = append(as.proxyManager.nodes, nodes...)\n\tas.proxyManager.mu.Unlock()\n\tgo as.proxyManager.CheckAllHealth()\n\n\treturn nil\n}\nfunc (as *AutoSubscriber) Start(pm *ProxyManager) {\n\tas.mu.Lock()\n\tif as.running {\n\t\tas.mu.Unlock()\n\t\treturn\n\t}\n\tas.running = true\n\tas.proxyManager = pm\n\tas.stopChan = make(chan struct{})\n\tas.mu.Unlock()\n\tgo func() {\n\t\tif err := as.refreshSubscription(); err != nil {\n\t\t}\n\n\t\tticker := time.NewTicker(as.refreshInterval)\n\t\tdefer ticker.Stop()\n\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-as.stopChan:\n\t\t\t\treturn\n\t\t\tcase <-ticker.C:\n\t\t\t\tif err := as.refreshSubscription(); err != nil {\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}()\n}\n\nfunc (as *AutoSubscriber) Stop() {\n\tas.mu.Lock()\n\tdefer as.mu.Unlock()\n\n\tif as.running {\n\t\tclose(as.stopChan)\n\t\tas.running = false\n\t}\n}\n\nfunc (as *AutoSubscriber) GetCurrentSubscribeURL() string {\n\tas.mu.RLock()\n\tdefer as.mu.RUnlock()\n\treturn as.subscribeURL\n}\n\nfunc (as *AutoSubscriber) GetCurrentToken() string {\n\tas.mu.RLock()\n\tdefer as.mu.RUnlock()\n\treturn as.currentToken\n}\nfunc (as *AutoSubscriber) IsExpired() bool {\n\tas.mu.RLock()\n\tdefer as.mu.RUnlock()\n\treturn time.Since(as.lastRefresh) > 2*time.Hour\n}\nfunc (pm *ProxyManager) StartAutoSubscribe() {\n\tautoSubscriber.Start(pm)\n}\nfunc (pm *ProxyManager) StopAutoSubscribe() {\n\tautoSubscriber.Stop()\n}\nfunc (pm *ProxyManager) GetAutoSubscribeURL() string {\n\treturn autoSubscriber.GetCurrentSubscribeURL()\n}\nfunc (pm *ProxyManager) HasAutoSubscribe() bool {\n\treturn autoSubscriber.GetCurrentToken() != \"\"\n}\n"
  },
  {
    "path": "src/proxy/singbox.go",
    "content": "package proxy\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\tbox \"github.com/sagernet/sing-box\"\n\t\"github.com/sagernet/sing-box/include\"\n\t\"github.com/sagernet/sing-box/option\"\n\n\t// 启用 QUIC 协议支持（hysteria, hysteria2, tuic）\n\t_ \"github.com/sagernet/sing-quic/hysteria\"\n\t_ \"github.com/sagernet/sing-quic/hysteria2\"\n\t_ \"github.com/sagernet/sing-quic/tuic\"\n)\n\ntype SingboxManager struct {\n\tmu        sync.Mutex\n\tinstances map[int]*SingboxInstance\n\tbasePort  int\n\tready     bool\n}\n\n// SingboxInstance sing-box 实例\ntype SingboxInstance struct {\n\tPort     int\n\tBox      *box.Box\n\tCtx      context.Context\n\tCancel   context.CancelFunc\n\tRunning  bool\n\tProxyURL string\n\tNode     *ProxyNode\n}\n\nvar singboxMgr = &SingboxManager{\n\tinstances: make(map[int]*SingboxInstance),\n\tbasePort:  11800,\n\tready:     true,\n}\n\n// IsSingboxProtocol 所有协议都由 sing-box 处理\nfunc IsSingboxProtocol(protocol string) bool {\n\tswitch protocol {\n\tcase \"vmess\", \"vless\", \"shadowsocks\", \"trojan\", \"socks\", \"http\",\n\t\t\"hysteria\", \"hysteria2\", \"hy2\", \"tuic\", \"wireguard\", \"anytls\":\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc CanSingboxHandle(protocol string) bool {\n\treturn IsSingboxProtocol(protocol)\n}\n\nfunc (sm *SingboxManager) IsAvailable() bool {\n\treturn sm.ready\n}\n\n// 连通性测试备选URL列表\nvar connectivityTestURLs = []string{\n\t\"https://cp.cloudflare.com/generate_204\",\n}\n\nfunc (sm *SingboxManager) StartRaw(node *ProxyNode) (string, error) {\n\tsm.mu.Lock()\n\tdefer sm.mu.Unlock()\n\treturn sm.startInternal(node, false)\n}\n\n// Start 启动代理并验证连通性（用于需要立即可用的场景）\nfunc (sm *SingboxManager) Start(node *ProxyNode) (string, error) {\n\tsm.mu.Lock()\n\tdefer sm.mu.Unlock()\n\treturn sm.startInternal(node, true)\n}\n\n// startInternal 内部启动方法\nfunc (sm *SingboxManager) startInternal(node *ProxyNode, doTest bool) (string, error) {\n\t// 分配端口\n\tport := sm.findAvailablePort()\n\tif port == 0 {\n\t\treturn \"\", fmt.Errorf(\"无可用端口\")\n\t}\n\tconfigJSON := sm.generateConfigJSON(node, port)\n\tctx, cancel := context.WithCancel(context.Background())\n\tctx = box.Context(ctx, include.InboundRegistry(), include.OutboundRegistry(),\n\t\tinclude.EndpointRegistry(), include.DNSTransportRegistry(), include.ServiceRegistry())\n\tvar opts option.Options\n\terr := opts.UnmarshalJSONContext(ctx, []byte(configJSON))\n\tif err != nil {\n\t\tcancel()\n\t\treturn \"\", fmt.Errorf(\"解析配置失败: %w\", err)\n\t}\n\n\tsingBox, err := box.New(box.Options{\n\t\tContext: ctx,\n\t\tOptions: opts,\n\t})\n\tif err != nil {\n\t\tcancel()\n\t\treturn \"\", fmt.Errorf(\"创建 sing-box 失败: %w\", err)\n\t}\n\n\tif err := singBox.Start(); err != nil {\n\t\tcancel()\n\t\treturn \"\", fmt.Errorf(\"启动 sing-box 失败: %w\", err)\n\t}\n\n\t// 等待端口就绪（使用渐进式等待，更快响应）\n\tproxyURL := fmt.Sprintf(\"http://127.0.0.1:%d\", port)\n\tportReady := false\n\twaitTimes := []time.Duration{10, 20, 30, 50, 50, 100, 100, 100, 150, 150} // 总计约760ms\n\tfor _, wait := range waitTimes {\n\t\ttime.Sleep(wait * time.Millisecond)\n\t\tconn, err := net.DialTimeout(\"tcp\", fmt.Sprintf(\"127.0.0.1:%d\", port), 100*time.Millisecond)\n\t\tif err == nil {\n\t\t\tconn.Close()\n\t\t\tportReady = true\n\t\t\tbreak\n\t\t}\n\t}\n\tif !portReady {\n\t\tsingBox.Close()\n\t\tcancel()\n\t\treturn \"\", fmt.Errorf(\"端口 %d 未就绪\", port)\n\t}\n\n\t// 如果需要连通性测试\n\tif doTest {\n\t\tif err := sm.testConnectivity(proxyURL); err != nil {\n\t\t\tsingBox.Close()\n\t\t\tcancel()\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\n\tinstance := &SingboxInstance{\n\t\tPort:     port,\n\t\tBox:      singBox,\n\t\tCtx:      ctx,\n\t\tCancel:   cancel,\n\t\tRunning:  true,\n\t\tProxyURL: proxyURL,\n\t\tNode:     node,\n\t}\n\n\tsm.instances[port] = instance\n\tnode.LocalPort = port\n\treturn proxyURL, nil\n}\n\n// testConnectivity 测试代理连通性（支持多URL重试）\nfunc (sm *SingboxManager) testConnectivity(proxyURL string) error {\n\tproxyURLParsed, _ := url.Parse(proxyURL)\n\ttestClient := &http.Client{\n\t\tTransport: &http.Transport{\n\t\t\tProxy: http.ProxyURL(proxyURLParsed),\n\t\t},\n\t\tTimeout: 8 * time.Second,\n\t}\n\n\tvar lastErr error\n\tfor _, testURL := range connectivityTestURLs {\n\t\tresp, err := testClient.Get(testURL)\n\t\tif err != nil {\n\t\t\tlastErr = err\n\t\t\tcontinue\n\t\t}\n\t\tresp.Body.Close()\n\t\tif resp.StatusCode == 204 || resp.StatusCode == 200 {\n\t\t\treturn nil // 成功\n\t\t}\n\t\tlastErr = fmt.Errorf(\"状态码: %d\", resp.StatusCode)\n\t}\n\treturn fmt.Errorf(\"连通性测试失败: %w\", lastErr)\n}\n\n// Stop 停止实例\nfunc (sm *SingboxManager) Stop(port int) {\n\tsm.mu.Lock()\n\tdefer sm.mu.Unlock()\n\n\tif inst, ok := sm.instances[port]; ok {\n\t\tif inst.Box != nil {\n\t\t\tinst.Box.Close()\n\t\t}\n\t\tif inst.Cancel != nil {\n\t\t\tinst.Cancel()\n\t\t}\n\t\tinst.Running = false\n\t\tdelete(sm.instances, port)\n\t}\n}\n\n// StopAll 停止所有实例\nfunc (sm *SingboxManager) StopAll() {\n\tsm.mu.Lock()\n\tdefer sm.mu.Unlock()\n\n\tfor port, inst := range sm.instances {\n\t\tif inst.Box != nil {\n\t\t\tinst.Box.Close()\n\t\t}\n\t\tif inst.Cancel != nil {\n\t\t\tinst.Cancel()\n\t\t}\n\t\tdelete(sm.instances, port)\n\t}\n}\n\nfunc (sm *SingboxManager) findAvailablePort() int {\n\tfor port := sm.basePort; port < sm.basePort+1000; port++ {\n\t\tif _, exists := sm.instances[port]; !exists {\n\t\t\tconn, err := net.DialTimeout(\"tcp\", fmt.Sprintf(\"127.0.0.1:%d\", port), 50*time.Millisecond)\n\t\t\tif err != nil {\n\t\t\t\treturn port\n\t\t\t}\n\t\t\tconn.Close()\n\t\t}\n\t}\n\treturn 0\n}\n\n// generateConfigJSON 生成 sing-box JSON 配置\nfunc (sm *SingboxManager) generateConfigJSON(node *ProxyNode, localPort int) string {\n\toutbound := sm.buildOutboundJSON(node)\n\n\tconfig := map[string]interface{}{\n\t\t\"log\": map[string]interface{}{\n\t\t\t\"disabled\": true,\n\t\t},\n\t\t\"inbounds\": []map[string]interface{}{\n\t\t\t{\n\t\t\t\t\"type\":        \"http\",\n\t\t\t\t\"tag\":         \"http-in\",\n\t\t\t\t\"listen\":      \"127.0.0.1\",\n\t\t\t\t\"listen_port\": localPort,\n\t\t\t},\n\t\t},\n\t\t\"outbounds\": []interface{}{\n\t\t\toutbound,\n\t\t},\n\t}\n\n\tdata, _ := json.Marshal(config)\n\treturn string(data)\n}\n\n// buildOutboundJSON 构建 outbound JSON 配置\nfunc (sm *SingboxManager) buildOutboundJSON(node *ProxyNode) map[string]interface{} {\n\tsni := node.SNI\n\tif sni == \"\" {\n\t\tsni = node.Server\n\t}\n\n\tswitch node.Protocol {\n\tcase \"hysteria2\", \"hy2\":\n\t\treturn map[string]interface{}{\n\t\t\t\"type\":        \"hysteria2\",\n\t\t\t\"tag\":         \"proxy\",\n\t\t\t\"server\":      node.Server,\n\t\t\t\"server_port\": node.Port,\n\t\t\t\"password\":    node.Password,\n\t\t\t\"tls\": map[string]interface{}{\n\t\t\t\t\"enabled\":     true,\n\t\t\t\t\"insecure\":    true,\n\t\t\t\t\"server_name\": sni,\n\t\t\t},\n\t\t}\n\n\tcase \"hysteria\":\n\t\treturn map[string]interface{}{\n\t\t\t\"type\":        \"hysteria\",\n\t\t\t\"tag\":         \"proxy\",\n\t\t\t\"server\":      node.Server,\n\t\t\t\"server_port\": node.Port,\n\t\t\t\"auth_str\":    node.Password,\n\t\t\t\"tls\": map[string]interface{}{\n\t\t\t\t\"enabled\":     true,\n\t\t\t\t\"insecure\":    true,\n\t\t\t\t\"server_name\": sni,\n\t\t\t},\n\t\t}\n\n\tcase \"tuic\":\n\t\treturn map[string]interface{}{\n\t\t\t\"type\":        \"tuic\",\n\t\t\t\"tag\":         \"proxy\",\n\t\t\t\"server\":      node.Server,\n\t\t\t\"server_port\": node.Port,\n\t\t\t\"uuid\":        node.UUID,\n\t\t\t\"password\":    node.Password,\n\t\t\t\"tls\": map[string]interface{}{\n\t\t\t\t\"enabled\":     true,\n\t\t\t\t\"insecure\":    true,\n\t\t\t\t\"server_name\": sni,\n\t\t\t},\n\t\t}\n\n\tcase \"vmess\":\n\t\tout := map[string]interface{}{\n\t\t\t\"type\":        \"vmess\",\n\t\t\t\"tag\":         \"proxy\",\n\t\t\t\"server\":      node.Server,\n\t\t\t\"server_port\": node.Port,\n\t\t\t\"uuid\":        node.UUID,\n\t\t\t\"security\":    node.Security,\n\t\t\t\"alter_id\":    node.AlterId,\n\t\t}\n\t\tif node.TLS {\n\t\t\tout[\"tls\"] = map[string]interface{}{\n\t\t\t\t\"enabled\":     true,\n\t\t\t\t\"insecure\":    true,\n\t\t\t\t\"server_name\": sni,\n\t\t\t}\n\t\t}\n\t\tif transport := sm.buildTransportJSON(node); transport != nil {\n\t\t\tout[\"transport\"] = transport\n\t\t}\n\t\treturn out\n\n\tcase \"vless\":\n\t\tout := map[string]interface{}{\n\t\t\t\"type\":        \"vless\",\n\t\t\t\"tag\":         \"proxy\",\n\t\t\t\"server\":      node.Server,\n\t\t\t\"server_port\": node.Port,\n\t\t\t\"uuid\":        node.UUID,\n\t\t}\n\t\tif node.Flow != \"\" {\n\t\t\tout[\"flow\"] = node.Flow\n\t\t}\n\t\tif node.Security == \"reality\" {\n\t\t\tfp := node.Fingerprint\n\t\t\tif fp == \"\" {\n\t\t\t\tfp = \"chrome\"\n\t\t\t}\n\t\t\tout[\"tls\"] = map[string]interface{}{\n\t\t\t\t\"enabled\":     true,\n\t\t\t\t\"server_name\": node.SNI,\n\t\t\t\t\"reality\": map[string]interface{}{\n\t\t\t\t\t\"enabled\":    true,\n\t\t\t\t\t\"public_key\": node.PublicKey,\n\t\t\t\t\t\"short_id\":   node.ShortId,\n\t\t\t\t},\n\t\t\t\t\"utls\": map[string]interface{}{\n\t\t\t\t\t\"enabled\":     true,\n\t\t\t\t\t\"fingerprint\": fp,\n\t\t\t\t},\n\t\t\t}\n\t\t} else if node.TLS {\n\t\t\tout[\"tls\"] = map[string]interface{}{\n\t\t\t\t\"enabled\":     true,\n\t\t\t\t\"insecure\":    true,\n\t\t\t\t\"server_name\": sni,\n\t\t\t}\n\t\t}\n\t\tif transport := sm.buildTransportJSON(node); transport != nil {\n\t\t\tout[\"transport\"] = transport\n\t\t}\n\t\treturn out\n\n\tcase \"shadowsocks\":\n\t\treturn map[string]interface{}{\n\t\t\t\"type\":        \"shadowsocks\",\n\t\t\t\"tag\":         \"proxy\",\n\t\t\t\"server\":      node.Server,\n\t\t\t\"server_port\": node.Port,\n\t\t\t\"method\":      node.Method,\n\t\t\t\"password\":    node.Password,\n\t\t}\n\n\tcase \"trojan\":\n\t\tout := map[string]interface{}{\n\t\t\t\"type\":        \"trojan\",\n\t\t\t\"tag\":         \"proxy\",\n\t\t\t\"server\":      node.Server,\n\t\t\t\"server_port\": node.Port,\n\t\t\t\"password\":    node.Password,\n\t\t\t\"tls\": map[string]interface{}{\n\t\t\t\t\"enabled\":     true,\n\t\t\t\t\"insecure\":    true,\n\t\t\t\t\"server_name\": sni,\n\t\t\t},\n\t\t}\n\t\tif transport := sm.buildTransportJSON(node); transport != nil {\n\t\t\tout[\"transport\"] = transport\n\t\t}\n\t\treturn out\n\n\tdefault:\n\t\treturn map[string]interface{}{\n\t\t\t\"type\": \"direct\",\n\t\t\t\"tag\":  \"proxy\",\n\t\t}\n\t}\n}\n\n// buildTransportJSON 构建传输层 JSON 配置\nfunc (sm *SingboxManager) buildTransportJSON(node *ProxyNode) map[string]interface{} {\n\tswitch node.Network {\n\tcase \"ws\":\n\t\ttransport := map[string]interface{}{\n\t\t\t\"type\": \"ws\",\n\t\t\t\"path\": node.Path,\n\t\t}\n\t\tif node.Host != \"\" {\n\t\t\ttransport[\"headers\"] = map[string]string{\n\t\t\t\t\"Host\": node.Host,\n\t\t\t}\n\t\t}\n\t\treturn transport\n\tcase \"grpc\":\n\t\treturn map[string]interface{}{\n\t\t\t\"type\":         \"grpc\",\n\t\t\t\"service_name\": node.Path,\n\t\t}\n\tcase \"httpupgrade\":\n\t\treturn map[string]interface{}{\n\t\t\t\"type\": \"httpupgrade\",\n\t\t\t\"path\": node.Path,\n\t\t\t\"host\": node.Host,\n\t\t}\n\tcase \"h2\", \"http\":\n\t\treturn map[string]interface{}{\n\t\t\t\"type\": \"http\",\n\t\t\t\"path\": node.Path,\n\t\t\t\"host\": []string{node.Host},\n\t\t}\n\t}\n\treturn nil\n}\n\n// GetSingboxManager 获取 sing-box 管理器\nfunc GetSingboxManager() *SingboxManager {\n\treturn singboxMgr\n}\n\n// InitSingbox 初始化 sing-box（内置 core 无需初始化）\nfunc InitSingbox() {\n\n}\n\n// TrySingboxStart 尝试用 sing-box 启动节点（xray 失败时的回退）\nfunc TrySingboxStart(node *ProxyNode) (string, error) {\n\tif !CanSingboxHandle(node.Protocol) {\n\t\treturn \"\", fmt.Errorf(\"sing-box 不支持协议: %s\", node.Protocol)\n\t}\n\treturn singboxMgr.Start(node)\n}\n\n// StopSingbox 停止指定端口的 sing-box 实例\nfunc StopSingbox(port int) {\n\tsingboxMgr.Stop(port)\n}\n\n// ParseProxyLinkWithSingbox 使用 sing-box 解析代理链接\nfunc ParseProxyLinkWithSingbox(link string) *ProxyNode {\n\tnode := Manager.parseLine(link)\n\tif node != nil {\n\t\treturn node\n\t}\n\n\tlink = strings.TrimSpace(link)\n\tif strings.HasPrefix(link, \"hy2://\") || strings.HasPrefix(link, \"hysteria2://\") {\n\t\treturn parseHysteria2(link)\n\t}\n\tif strings.HasPrefix(link, \"tuic://\") {\n\t\treturn parseTUIC(link)\n\t}\n\n\treturn nil\n}\n\n// parseTUIC 解析 TUIC 链接\nfunc parseTUIC(link string) *ProxyNode {\n\torigLink := link\n\tlink = strings.TrimPrefix(link, \"tuic://\")\n\n\tvar name string\n\tif idx := strings.LastIndex(link, \"#\"); idx != -1 {\n\t\tname, _ = url.QueryUnescape(link[idx+1:])\n\t\tlink = link[:idx]\n\t}\n\n\tvar params string\n\tif idx := strings.Index(link, \"?\"); idx != -1 {\n\t\tparams = link[idx+1:]\n\t\tlink = link[:idx]\n\t}\n\n\tatIdx := strings.LastIndex(link, \"@\")\n\tif atIdx == -1 {\n\t\treturn nil\n\t}\n\n\tuserPart := link[:atIdx]\n\thostPart := link[atIdx+1:]\n\n\tvar uuid, password string\n\tif colonIdx := strings.Index(userPart, \":\"); colonIdx != -1 {\n\t\tuuid = userPart[:colonIdx]\n\t\tpassword = userPart[colonIdx+1:]\n\t} else {\n\t\tuuid = userPart\n\t}\n\n\tvar server string\n\tvar port int\n\tif lastColon := strings.LastIndex(hostPart, \":\"); lastColon != -1 {\n\t\tserver = hostPart[:lastColon]\n\t\tport, _ = strconv.Atoi(hostPart[lastColon+1:])\n\t}\n\n\tnode := &ProxyNode{\n\t\tProtocol: \"tuic\",\n\t\tName:     name,\n\t\tServer:   server,\n\t\tPort:     port,\n\t\tUUID:     uuid,\n\t\tPassword: password,\n\t\tRaw:      origLink,\n\t}\n\n\tfor _, param := range strings.Split(params, \"&\") {\n\t\tif kv := strings.SplitN(param, \"=\", 2); len(kv) == 2 {\n\t\t\tswitch kv[0] {\n\t\t\tcase \"sni\":\n\t\t\t\tnode.SNI = kv[1]\n\t\t\tcase \"alpn\":\n\t\t\t\tnode.ALPN = kv[1]\n\t\t\t}\n\t\t}\n\t}\n\n\tif node.Server == \"\" || node.Port == 0 {\n\t\treturn nil\n\t}\n\n\treturn node\n}\n"
  },
  {
    "path": "src/register/browser.go",
    "content": "package register\n\nimport (\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"math/rand\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"business2api/src/logger\"\n\t\"business2api/src/pool\"\n\n\t\"github.com/go-rod/rod\"\n\t\"github.com/go-rod/rod/lib/input\"\n\t\"github.com/go-rod/rod/lib/launcher\"\n\t\"github.com/go-rod/rod/lib/proto\"\n)\n\nvar (\n\tRegisterDebug bool\n\tRegisterOnce  bool\n\thttpClient    *http.Client\n\tGetProxy      func() string\n\tReleaseProxy  func(proxyURL string) // 释放代理的函数\n\tfirstNames    = []string{\"John\", \"Jane\", \"Michael\", \"Sarah\", \"David\", \"Emily\", \"Robert\", \"Lisa\", \"James\", \"Emma\"}\n\tlastNames     = []string{\"Smith\", \"Johnson\", \"Williams\", \"Brown\", \"Jones\", \"Garcia\", \"Miller\", \"Davis\", \"Wilson\", \"Taylor\"}\n\tcommonWords   = map[string]bool{\n\t\t\"VERIFY\": true, \"GOOGLE\": true, \"UPDATE\": true, \"MOBILE\": true, \"DEVICE\": true,\n\t\t\"SUBMIT\": true, \"RESEND\": true, \"CANCEL\": true, \"DELETE\": true, \"REMOVE\": true,\n\t\t\"SEARCH\": true, \"VIDEOS\": true, \"IMAGES\": true, \"GMAIL\": true, \"EMAIL\": true,\n\t\t\"ACCOUNT\": true, \"CHROME\": true,\n\t}\n)\n\n// SetHTTPClient 设置HTTP客户端\nfunc SetHTTPClient(c *http.Client) {\n\thttpClient = c\n}\nfunc readResponseBody(resp *http.Response) ([]byte, error) {\n\tdefer resp.Body.Close()\n\tvar reader = resp.Body\n\tif resp.Header.Get(\"Content-Encoding\") == \"gzip\" {\n\t}\n\n\tbody := make([]byte, 0)\n\tbuf := make([]byte, 4096)\n\tfor {\n\t\tn, err := reader.Read(buf)\n\t\tif n > 0 {\n\t\t\tbody = append(body, buf[:n]...)\n\t\t}\n\t\tif err != nil {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn body, nil\n}\n\ntype TempEmailResponse struct {\n\tEmail string `json:\"email\"`\n\tData  struct {\n\t\tEmail string `json:\"email\"`\n\t} `json:\"data\"`\n}\ntype EmailListResponse struct {\n\tSuccess bool `json:\"success\"`\n\tData    struct {\n\t\tEmails []EmailContent `json:\"emails\"`\n\t} `json:\"data\"`\n}\ntype EmailContent struct {\n\tSubject string `json:\"subject\"`\n\tContent string `json:\"content\"`\n}\ntype BrowserRegisterResult struct {\n\tSuccess       bool\n\tEmail         string\n\tFullName      string\n\tAuthorization string\n\tCookies       []pool.Cookie\n\tConfigID      string\n\tCSESIDX       string\n\tError         error\n}\n\nfunc generateRandomName() string {\n\treturn firstNames[rand.Intn(len(firstNames))] + \" \" + lastNames[rand.Intn(len(lastNames))]\n}\n\n// ==================== 拟人化操作工具函数 ====================\n\n// humanDelay 随机延迟（模拟人类反应时间）\nfunc humanDelay(minMs, maxMs int) {\n\tif minMs >= maxMs {\n\t\tmaxMs = minMs + 1\n\t}\n\tdelay := minMs + rand.Intn(maxMs-minMs)\n\ttime.Sleep(time.Duration(delay) * time.Millisecond)\n}\n\n// humanMouseMove 模拟人类鼠标移动到元素（贝塞尔曲线轨迹）\nfunc humanMouseMove(page *rod.Page, el *rod.Element) {\n\tif page == nil || el == nil {\n\t\treturn\n\t}\n\tbox, err := el.Shape()\n\tif err != nil || box == nil || len(box.Quads) == 0 {\n\t\treturn\n\t}\n\tquad := box.Quads[0]\n\tif len(quad) < 8 {\n\t\treturn\n\t}\n\t// 计算元素中心点（添加随机偏移）\n\tcenterX := (quad[0] + quad[2] + quad[4] + quad[6]) / 4\n\tcenterY := (quad[1] + quad[3] + quad[5] + quad[7]) / 4\n\toffsetX := float64(rand.Intn(10) - 5)\n\toffsetY := float64(rand.Intn(10) - 5)\n\n\t// 模拟贝塞尔曲线移动（分多步）\n\tsteps := 5 + rand.Intn(5)\n\tfor i := 1; i <= steps; i++ {\n\t\tprogress := float64(i) / float64(steps)\n\t\t// 使用 ease-out 曲线\n\t\teased := 1 - (1-progress)*(1-progress)\n\t\tx := centerX*eased + offsetX\n\t\ty := centerY*eased + offsetY\n\t\tpage.Mouse.MoveTo(proto.Point{X: x, Y: y})\n\t\ttime.Sleep(time.Duration(10+rand.Intn(20)) * time.Millisecond)\n\t}\n}\n\n// humanClick 拟人化点击元素\nfunc humanClick(page *rod.Page, el *rod.Element) error {\n\tif page == nil || el == nil {\n\t\treturn fmt.Errorf(\"page or element is nil\")\n\t}\n\t// 1. 先移动鼠标到元素附近\n\thumanMouseMove(page, el)\n\thumanDelay(50, 150)\n\n\t// 2. 点击前短暂停顿（模拟人类犹豫）\n\thumanDelay(30, 100)\n\n\t// 3. 执行点击\n\terr := el.Click(proto.InputMouseButtonLeft, 1)\n\n\t// 4. 点击后短暂停顿\n\thumanDelay(80, 200)\n\treturn err\n}\n\n// humanType 拟人化打字（自然节奏，有随机停顿）\nfunc humanType(page *rod.Page, text string) {\n\tif page == nil || text == \"\" {\n\t\treturn\n\t}\n\tfor i, char := range text {\n\t\tpage.Keyboard.Type(input.Key(char))\n\t\t// 基础延迟 + 随机变化\n\t\tbaseDelay := 50 + rand.Intn(80)\n\t\t// 偶尔有较长停顿（模拟思考）\n\t\tif rand.Float32() < 0.1 {\n\t\t\tbaseDelay += 150 + rand.Intn(200)\n\t\t}\n\t\t// 某些字符后停顿更长（如空格、标点）\n\t\tif char == ' ' || char == '.' || char == '@' {\n\t\t\tbaseDelay += 30 + rand.Intn(50)\n\t\t}\n\t\ttime.Sleep(time.Duration(baseDelay) * time.Millisecond)\n\t\t// 每 8-12 个字符偶尔短暂休息\n\t\tif i > 0 && i%(8+rand.Intn(5)) == 0 {\n\t\t\thumanDelay(100, 300)\n\t\t}\n\t}\n}\n\n// humanScrollToElement 拟人化滚动到元素\nfunc humanScrollToElement(page *rod.Page, el *rod.Element) {\n\tif el == nil {\n\t\treturn\n\t}\n\thumanDelay(100, 300)\n\tel.ScrollIntoView()\n\thumanDelay(200, 400)\n}\n\n// humanFocusInput 拟人化聚焦输入框\nfunc humanFocusInput(page *rod.Page, el *rod.Element) error {\n\tif page == nil || el == nil {\n\t\treturn fmt.Errorf(\"page or element is nil\")\n\t}\n\t// 滚动到元素\n\thumanScrollToElement(page, el)\n\t// 点击聚焦\n\treturn humanClick(page, el)\n}\n\ntype TempMailProvider struct {\n\tName        string\n\tGenerateURL string\n\tCheckURL    string\n\tHeaders     map[string]string\n}\n\n// 支持的临时邮箱提供商列表\nvar tempMailProviders = []TempMailProvider{\n\t{\n\t\tName:        \"chatgpt.org.uk\",\n\t\tGenerateURL: \"https://mail.chatgpt.org.uk/api/generate-email\",\n\t\tCheckURL:    \"https://mail.chatgpt.org.uk/api/emails?email=%s\",\n\t\tHeaders: map[string]string{\n\t\t\t\"User-Agent\": \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36\",\n\t\t\t\"Referer\":    \"https://mail.chatgpt.org.uk\",\n\t\t},\n\t},\n\t// 备用邮箱服务可以在这里添加\n}\n\nfunc getTemporaryEmail() (string, error) {\n\tvar lastErr error\n\tfor _, provider := range tempMailProviders {\n\t\tfor retry := 0; retry < 3; retry++ {\n\t\t\temail, err := getEmailFromProvider(provider)\n\t\t\tif err != nil {\n\t\t\t\tlastErr = err\n\t\t\t\tif retry < 2 {\n\t\t\t\t\tlog.Printf(\"⚠️ 临时邮箱 %s 失败 (重试 %d/3): %v\", provider.Name, retry+1, err)\n\t\t\t\t\ttime.Sleep(time.Duration(retry+1) * time.Second)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tlog.Printf(\"⚠️ 临时邮箱 %s 失败，尝试下一个提供商\", provider.Name)\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif !strings.Contains(email, \"@\") {\n\t\t\t\tlastErr = fmt.Errorf(\"邮箱格式无效: %s\", email)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn email, nil\n\t\t}\n\t}\n\treturn \"\", fmt.Errorf(\"所有临时邮箱服务均失败: %v\", lastErr)\n}\nfunc getEmailFromProvider(provider TempMailProvider) (string, error) {\n\treq, _ := http.NewRequest(\"GET\", provider.GenerateURL, nil)\n\tfor k, v := range provider.Headers {\n\t\treq.Header.Set(k, v)\n\t}\n\tclient := &http.Client{Timeout: 30 * time.Second}\n\tif httpClient != nil {\n\t\tclient = httpClient\n\t}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"请求失败: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\tif resp.StatusCode != 200 {\n\t\treturn \"\", fmt.Errorf(\"HTTP %d\", resp.StatusCode)\n\t}\n\n\tbody, err := readResponseBody(resp)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"读取响应失败: %w\", err)\n\t}\n\n\tvar result TempEmailResponse\n\tif err := json.Unmarshal(body, &result); err != nil {\n\t\treturn \"\", fmt.Errorf(\"解析响应失败: %w, body: %s\", err, string(body[:min(100, len(body))]))\n\t}\n\n\temail := result.Email\n\tif email == \"\" {\n\t\temail = result.Data.Email\n\t}\n\tif email == \"\" {\n\t\treturn \"\", fmt.Errorf(\"返回的邮箱为空, 响应: %s\", string(body[:min(100, len(body))]))\n\t}\n\treturn email, nil\n}\nfunc getEmailCount(email string) int {\n\tfor retry := 0; retry < 3; retry++ {\n\t\treq, _ := http.NewRequest(\"GET\", fmt.Sprintf(\"https://mail.chatgpt.org.uk/api/emails?email=%s\", email), nil)\n\t\treq.Header.Set(\"User-Agent\", \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36\")\n\t\treq.Header.Set(\"Referer\", \"https://mail.chatgpt.org.uk\")\n\n\t\tclient := &http.Client{Timeout: 15 * time.Second}\n\t\tif httpClient != nil {\n\t\t\tclient = httpClient\n\t\t}\n\n\t\tresp, err := client.Do(req)\n\t\tif err != nil {\n\t\t\ttime.Sleep(time.Second)\n\t\t\tcontinue\n\t\t}\n\t\tbody, _ := readResponseBody(resp)\n\t\tvar result EmailListResponse\n\t\tif err := json.Unmarshal(body, &result); err != nil {\n\t\t\tcontinue\n\t\t}\n\t\treturn len(result.Data.Emails)\n\t}\n\treturn 0\n}\n\ntype VerificationState struct {\n\tUsedCodes    map[string]bool // 已使用过的验证码\n\tLastEmailID  string          // 上次处理的邮件ID\n\tResendCount  int             // 重发次数\n\tLastResendAt time.Time       // 上次重发时间\n\tmu           sync.Mutex\n}\n\nfunc NewVerificationState() *VerificationState {\n\treturn &VerificationState{\n\t\tUsedCodes: make(map[string]bool),\n\t}\n}\n\nfunc (vs *VerificationState) MarkCodeUsed(code string) {\n\tvs.mu.Lock()\n\tdefer vs.mu.Unlock()\n\tvs.UsedCodes[code] = true\n}\nfunc (vs *VerificationState) IsCodeUsed(code string) bool {\n\tvs.mu.Lock()\n\tdefer vs.mu.Unlock()\n\treturn vs.UsedCodes[code]\n}\nfunc (vs *VerificationState) CanResend() bool {\n\tvs.mu.Lock()\n\tdefer vs.mu.Unlock()\n\tif vs.ResendCount >= 3 {\n\t\treturn false\n\t}\n\tif time.Since(vs.LastResendAt) < 10*time.Second {\n\t\treturn false\n\t}\n\treturn true\n}\n\n// RecordResend 记录重发\nfunc (vs *VerificationState) RecordResend() {\n\tvs.mu.Lock()\n\tdefer vs.mu.Unlock()\n\tvs.ResendCount++\n\tvs.LastResendAt = time.Now()\n}\nfunc getVerificationEmailQuick(email string, retries int, intervalSec int) (*EmailContent, error) {\n\treturn getVerificationEmailAfter(email, retries, intervalSec, 0)\n}\nfunc getVerificationEmailAfter(email string, retries int, intervalSec int, initialCount int) (*EmailContent, error) {\n\treturn getVerificationEmailWithState(email, retries, intervalSec, initialCount, nil)\n}\nfunc getVerificationEmailWithState(email string, retries int, intervalSec int, initialCount int, state *VerificationState) (*EmailContent, error) {\n\tclient := &http.Client{Timeout: 15 * time.Second}\n\tif httpClient != nil {\n\t\tclient = httpClient\n\t}\n\tfor i := 0; i < retries; i++ {\n\t\treq, _ := http.NewRequest(\"GET\", fmt.Sprintf(\"https://mail.chatgpt.org.uk/api/emails?email=%s\", email), nil)\n\t\treq.Header.Set(\"User-Agent\", \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36\")\n\t\treq.Header.Set(\"Referer\", \"https://mail.chatgpt.org.uk\")\n\n\t\tresp, err := client.Do(req)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"[验证码] 获取邮件列表失败: %v\", err)\n\t\t\ttime.Sleep(time.Duration(intervalSec) * time.Second)\n\t\t\tcontinue\n\t\t}\n\t\tbody, _ := readResponseBody(resp) // readResponseBody 内部会关闭 Body\n\n\t\tvar result EmailListResponse\n\t\tif err := json.Unmarshal(body, &result); err != nil {\n\t\t\ttime.Sleep(time.Duration(intervalSec) * time.Second)\n\t\t\tcontinue\n\t\t}\n\t\tif result.Success && len(result.Data.Emails) > initialCount {\n\t\t\tfor idx := 0; idx < len(result.Data.Emails)-initialCount; idx++ {\n\t\t\t\tlatestEmail := &result.Data.Emails[idx]\n\t\t\t\tcode, err := extractVerificationCode(latestEmail.Content)\n\t\t\t\tif err != nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif state != nil && state.IsCodeUsed(code) {\n\t\t\t\t\tlog.Printf(\"[验证码] 跳过已使用的验证码: %s\", code)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\treturn latestEmail, nil\n\t\t\t}\n\t\t\tlog.Printf(\"[验证码] 所有新邮件的验证码均已使用，等待新邮件...\")\n\t\t}\n\t\ttime.Sleep(time.Duration(intervalSec) * time.Second)\n\t}\n\treturn nil, fmt.Errorf(\"未收到新的验证码邮件\")\n}\n\n// PageState 页面状态类型\ntype PageState int\n\nconst (\n\tPageStateUnknown PageState = iota\n\tPageStateEmailInput\n\tPageStateCodeInput\n\tPageStateNameInput\n\tPageStateLoggedIn\n\tPageStateError\n)\n\nfunc GetPageState(pageURL string) PageState {\n\tif pageURL == \"\" {\n\t\treturn PageStateUnknown\n\t}\n\tif strings.Contains(pageURL, \"accountverification.business.gemini.google\") {\n\t\treturn PageStateCodeInput\n\t}\n\tif strings.Contains(pageURL, \"auth.business.gemini.google\") {\n\t\treturn PageStateEmailInput\n\t}\n\tif strings.Contains(pageURL, \"business.gemini.google/admin/create\") {\n\t\treturn PageStateNameInput\n\t}\n\tif strings.Contains(pageURL, \"business.gemini.google\") &&\n\t\t!strings.Contains(pageURL, \"auth.\") &&\n\t\t!strings.Contains(pageURL, \"accountverification.\") &&\n\t\t!strings.Contains(pageURL, \"/admin/create\") {\n\t\treturn PageStateLoggedIn\n\t}\n\treturn PageStateUnknown\n}\n\nfunc GetPageStateString(state PageState) string {\n\tswitch state {\n\tcase PageStateEmailInput:\n\t\treturn \"邮箱输入\"\n\tcase PageStateCodeInput:\n\t\treturn \"验证码输入\"\n\tcase PageStateNameInput:\n\t\treturn \"名字输入\"\n\tcase PageStateLoggedIn:\n\t\treturn \"已登录\"\n\tcase PageStateError:\n\t\treturn \"错误页面\"\n\tdefault:\n\t\treturn \"未知\"\n\t}\n}\n\n// WaitForPageState 等待页面达到指定状态\nfunc WaitForPageState(page *rod.Page, targetState PageState, timeout time.Duration) (PageState, error) {\n\tstart := time.Now()\n\tfor time.Since(start) < timeout {\n\t\tinfo, err := page.Info()\n\t\tif err != nil {\n\t\t\ttime.Sleep(500 * time.Millisecond)\n\t\t\tcontinue\n\t\t}\n\n\t\tcurrentState := GetPageState(info.URL)\n\t\tif currentState == targetState {\n\t\t\treturn currentState, nil\n\t\t}\n\n\t\t// 如果已经登录，直接返回\n\t\tif currentState == PageStateLoggedIn {\n\t\t\treturn currentState, nil\n\t\t}\n\n\t\ttime.Sleep(500 * time.Millisecond)\n\t}\n\n\t// 超时，返回当前状态\n\tinfo, _ := page.Info()\n\tif info != nil {\n\t\treturn GetPageState(info.URL), fmt.Errorf(\"等待页面状态超时\")\n\t}\n\treturn PageStateUnknown, fmt.Errorf(\"等待页面状态超时\")\n}\n\n// 邮箱输入框选择器列表（优先级从高到低）\nvar emailInputSelectors = []string{\n\t\"#email-input\",\n\t\"input[name='loginHint']\",\n\t\"input[jsname='YPqjbf']\",\n\t\"input[type='email']\",\n\t\"input[type='text'][aria-label]\",\n\t\"input:not([type='hidden']):not([type='submit']):not([type='checkbox'])\",\n}\n\n// 浏览器环境变量列表（按优先级）\nvar browserEnvVars = []string{\n\t\"CHROME_PATH\",\n\t\"CHROMIUM_PATH\",\n\t\"EDGE_PATH\",\n\t\"BROWSER_PATH\",\n\t\"GOOGLE_CHROME_BIN\",\n\t\"CHROMIUM_BIN\",\n}\n\n// getWindowsBrowserPaths 获取 Windows 浏览器路径列表\nfunc getWindowsBrowserPaths() []string {\n\tpaths := []string{}\n\n\t// 程序安装目录\n\tprogramFiles := os.Getenv(\"ProgramFiles\")\n\tprogramFilesX86 := os.Getenv(\"ProgramFiles(x86)\")\n\tlocalAppData := os.Getenv(\"LOCALAPPDATA\")\n\tuserProfile := os.Getenv(\"USERPROFILE\")\n\n\t// Chrome 路径\n\tchromePaths := []string{\n\t\tfilepath.Join(programFiles, \"Google\", \"Chrome\", \"Application\", \"chrome.exe\"),\n\t\tfilepath.Join(programFilesX86, \"Google\", \"Chrome\", \"Application\", \"chrome.exe\"),\n\t\tfilepath.Join(localAppData, \"Google\", \"Chrome\", \"Application\", \"chrome.exe\"),\n\t\tfilepath.Join(userProfile, \"AppData\", \"Local\", \"Google\", \"Chrome\", \"Application\", \"chrome.exe\"),\n\t}\n\tpaths = append(paths, chromePaths...)\n\n\t// Edge 路径\n\tedgePaths := []string{\n\t\tfilepath.Join(programFiles, \"Microsoft\", \"Edge\", \"Application\", \"msedge.exe\"),\n\t\tfilepath.Join(programFilesX86, \"Microsoft\", \"Edge\", \"Application\", \"msedge.exe\"),\n\t\tfilepath.Join(localAppData, \"Microsoft\", \"Edge\", \"Application\", \"msedge.exe\"),\n\t}\n\tpaths = append(paths, edgePaths...)\n\n\t// Brave 路径\n\tbravePaths := []string{\n\t\tfilepath.Join(programFiles, \"BraveSoftware\", \"Brave-Browser\", \"Application\", \"brave.exe\"),\n\t\tfilepath.Join(programFilesX86, \"BraveSoftware\", \"Brave-Browser\", \"Application\", \"brave.exe\"),\n\t\tfilepath.Join(localAppData, \"BraveSoftware\", \"Brave-Browser\", \"Application\", \"brave.exe\"),\n\t}\n\tpaths = append(paths, bravePaths...)\n\n\t// Vivaldi 路径\n\tvivaldiPaths := []string{\n\t\tfilepath.Join(localAppData, \"Vivaldi\", \"Application\", \"vivaldi.exe\"),\n\t}\n\tpaths = append(paths, vivaldiPaths...)\n\n\t// Opera 路径\n\toperaPaths := []string{\n\t\tfilepath.Join(localAppData, \"Programs\", \"Opera\", \"opera.exe\"),\n\t\tfilepath.Join(localAppData, \"Programs\", \"Opera GX\", \"opera.exe\"),\n\t}\n\tpaths = append(paths, operaPaths...)\n\n\treturn paths\n}\n\n// getLinuxBrowserPaths 获取 Linux 浏览器路径列表\nfunc getLinuxBrowserPaths() []string {\n\treturn []string{\n\t\t// Chrome\n\t\t\"/usr/bin/google-chrome\",\n\t\t\"/usr/bin/google-chrome-stable\",\n\t\t\"/usr/bin/google-chrome-beta\",\n\t\t\"/usr/bin/google-chrome-unstable\",\n\t\t\"/opt/google/chrome/chrome\",\n\t\t\"/opt/google/chrome/google-chrome\",\n\t\t// Chromium\n\t\t\"/usr/bin/chromium\",\n\t\t\"/usr/bin/chromium-browser\",\n\t\t\"/usr/lib/chromium/chromium\",\n\t\t\"/usr/lib/chromium-browser/chromium-browser\",\n\t\t\"/snap/bin/chromium\",\n\t\t\"/snap/chromium/current/usr/lib/chromium-browser/chrome\",\n\t\t// Edge\n\t\t\"/usr/bin/microsoft-edge\",\n\t\t\"/usr/bin/microsoft-edge-stable\",\n\t\t\"/usr/bin/microsoft-edge-beta\",\n\t\t\"/usr/bin/microsoft-edge-dev\",\n\t\t\"/opt/microsoft/msedge/msedge\",\n\t\t// Brave\n\t\t\"/usr/bin/brave-browser\",\n\t\t\"/usr/bin/brave-browser-stable\",\n\t\t\"/opt/brave.com/brave/brave-browser\",\n\t\t// Vivaldi\n\t\t\"/usr/bin/vivaldi\",\n\t\t\"/usr/bin/vivaldi-stable\",\n\t\t// Opera\n\t\t\"/usr/bin/opera\",\n\t}\n}\n\n// getMacOSBrowserPaths 获取 macOS 浏览器路径列表\nfunc getMacOSBrowserPaths() []string {\n\thomeDir, _ := os.UserHomeDir()\n\tpaths := []string{\n\t\t// Chrome\n\t\t\"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome\",\n\t\t\"/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary\",\n\t\tfilepath.Join(homeDir, \"Applications\", \"Google Chrome.app\", \"Contents\", \"MacOS\", \"Google Chrome\"),\n\t\t// Chromium\n\t\t\"/Applications/Chromium.app/Contents/MacOS/Chromium\",\n\t\t// Edge\n\t\t\"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge\",\n\t\t\"/Applications/Microsoft Edge Beta.app/Contents/MacOS/Microsoft Edge Beta\",\n\t\t\"/Applications/Microsoft Edge Canary.app/Contents/MacOS/Microsoft Edge Canary\",\n\t\t// Brave\n\t\t\"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser\",\n\t\t// Vivaldi\n\t\t\"/Applications/Vivaldi.app/Contents/MacOS/Vivaldi\",\n\t\t// Opera\n\t\t\"/Applications/Opera.app/Contents/MacOS/Opera\",\n\t}\n\treturn paths\n}\n\n// getBrowserPathsForOS 根据操作系统获取浏览器路径列表\nfunc getBrowserPathsForOS() []string {\n\tswitch runtime.GOOS {\n\tcase \"windows\":\n\t\treturn getWindowsBrowserPaths()\n\tcase \"darwin\":\n\t\treturn getMacOSBrowserPaths()\n\tdefault: // linux, freebsd, etc.\n\t\treturn getLinuxBrowserPaths()\n\t}\n}\n\n// findBrowser 查找可用浏览器（完整兼容 Windows/Linux/macOS）\nfunc findBrowser() (string, bool) {\n\t// 1. 优先检查环境变量\n\tfor _, envVar := range browserEnvVars {\n\t\tif path := os.Getenv(envVar); path != \"\" {\n\t\t\t// 扩展环境变量\n\t\t\tpath = expandPath(path)\n\t\t\tif _, err := os.Stat(path); err == nil {\n\t\t\t\tlog.Printf(\"🌐 从环境变量 %s 获取浏览器: %s\", envVar, path)\n\t\t\t\treturn path, true\n\t\t\t}\n\t\t}\n\t}\n\n\t// 2. 检查系统路径（根据操作系统）\n\tfor _, path := range getBrowserPathsForOS() {\n\t\texpandedPath := expandPath(path)\n\t\tif expandedPath != \"\" {\n\t\t\tif _, err := os.Stat(expandedPath); err == nil {\n\t\t\t\tlog.Printf(\"🌐 找到浏览器: %s\", expandedPath)\n\t\t\t\treturn expandedPath, true\n\t\t\t}\n\t\t}\n\t}\n\n\t// 3. 尝试通过 which/where 命令查找\n\tif path := findBrowserByCommand(); path != \"\" {\n\t\tlog.Printf(\"🌐 通过系统命令找到浏览器: %s\", path)\n\t\treturn path, true\n\t}\n\n\t// 4. 尝试通过 PATH 手动查找\n\tbrowserNames := getBrowserNamesForOS()\n\tfor _, name := range browserNames {\n\t\tif path, err := findInPath(name); err == nil && path != \"\" {\n\t\t\tlog.Printf(\"🌐 从 PATH 找到浏览器: %s\", path)\n\t\t\treturn path, true\n\t\t}\n\t}\n\n\treturn \"\", false\n}\n\n// expandPath 扩展路径中的环境变量\nfunc expandPath(path string) string {\n\tif path == \"\" {\n\t\treturn \"\"\n\t}\n\t// 扩展 $VAR 和 ${VAR} 格式\n\texpanded := os.ExpandEnv(path)\n\t// Windows 特殊处理: 扩展 %VAR% 格式\n\tif runtime.GOOS == \"windows\" && strings.Contains(expanded, \"%\") {\n\t\tfor _, env := range os.Environ() {\n\t\t\tparts := strings.SplitN(env, \"=\", 2)\n\t\t\tif len(parts) == 2 {\n\t\t\t\texpanded = strings.ReplaceAll(expanded, \"%\"+parts[0]+\"%\", parts[1])\n\t\t\t}\n\t\t}\n\t}\n\treturn expanded\n}\n\n// getBrowserNamesForOS 获取当前操作系统的浏览器可执行文件名\nfunc getBrowserNamesForOS() []string {\n\tif runtime.GOOS == \"windows\" {\n\t\treturn []string{\"chrome\", \"msedge\", \"brave\", \"vivaldi\", \"opera\"}\n\t}\n\treturn []string{\"google-chrome\", \"google-chrome-stable\", \"chromium\", \"chromium-browser\", \"microsoft-edge\", \"brave-browser\", \"vivaldi\"}\n}\n\n// findBrowserByCommand 通过系统命令查找浏览器\nfunc findBrowserByCommand() string {\n\tvar cmd *exec.Cmd\n\tvar browsers []string\n\n\tif runtime.GOOS == \"windows\" {\n\t\t// Windows 使用 where 命令\n\t\tbrowsers = []string{\"chrome.exe\", \"msedge.exe\", \"brave.exe\"}\n\t\tfor _, browser := range browsers {\n\t\t\tcmd = exec.Command(\"where\", browser)\n\t\t\tif output, err := cmd.Output(); err == nil {\n\t\t\t\tlines := strings.Split(strings.TrimSpace(string(output)), \"\\n\")\n\t\t\t\tif len(lines) > 0 && lines[0] != \"\" {\n\t\t\t\t\treturn strings.TrimSpace(lines[0])\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} else {\n\t\t// Unix 使用 which 命令\n\t\tbrowsers = []string{\"google-chrome\", \"google-chrome-stable\", \"chromium\", \"chromium-browser\", \"microsoft-edge\", \"brave-browser\"}\n\t\tfor _, browser := range browsers {\n\t\t\tcmd = exec.Command(\"which\", browser)\n\t\t\tif output, err := cmd.Output(); err == nil {\n\t\t\t\tpath := strings.TrimSpace(string(output))\n\t\t\t\tif path != \"\" {\n\t\t\t\t\treturn path\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// findInPath 在 PATH 中查找可执行文件\nfunc findInPath(name string) (string, error) {\n\tpathEnv := os.Getenv(\"PATH\")\n\tvar separator string\n\tif runtime.GOOS == \"windows\" {\n\t\tseparator = \";\"\n\t} else {\n\t\tseparator = \":\"\n\t}\n\n\tfor _, dir := range strings.Split(pathEnv, separator) {\n\t\tif dir == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tdir = expandPath(dir)\n\n\t\t// 根据操作系统构建候选路径\n\t\tvar candidates []string\n\t\tif runtime.GOOS == \"windows\" {\n\t\t\tcandidates = []string{\n\t\t\t\tfilepath.Join(dir, name+\".exe\"),\n\t\t\t\tfilepath.Join(dir, name+\".cmd\"),\n\t\t\t\tfilepath.Join(dir, name+\".bat\"),\n\t\t\t\tfilepath.Join(dir, name),\n\t\t\t}\n\t\t} else {\n\t\t\tcandidates = []string{\n\t\t\t\tfilepath.Join(dir, name),\n\t\t\t}\n\t\t}\n\n\t\tfor _, path := range candidates {\n\t\t\tif info, err := os.Stat(path); err == nil && !info.IsDir() {\n\t\t\t\treturn path, nil\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\", fmt.Errorf(\"not found: %s\", name)\n}\n\n// BrowserSession 浏览器会话（封装公共逻辑）\ntype BrowserSession struct {\n\tLauncher      *launcher.Launcher\n\tBrowser       *rod.Browser\n\tPage          *rod.Page\n\tAuthorization string\n\tConfigID      string\n\tCSESIDX       string\n\tmu            sync.Mutex\n}\n\nfunc createBrowserSession(headless bool, proxy string, logPrefix string) (*BrowserSession, error) {\n\tsession := &BrowserSession{}\n\n\t// 启动浏览器 - 使用统一的浏览器查找逻辑\n\tl := launcher.New()\n\tif browserPath, found := findBrowser(); found {\n\t\tl = l.Bin(browserPath)\n\t\tlog.Printf(\"%s 使用浏览器: %s\", logPrefix, browserPath)\n\t} else {\n\t\tlog.Printf(\"%s ⚠️ 未找到系统浏览器，尝试使用 rod 自动下载\", logPrefix)\n\t}\n\n\t// 配置浏览器启动参数 - 原生反检测，不依赖JS注入\n\tl = configureBrowserLauncher(l, headless, proxy)\n\n\tlauncherURL, err := l.Launch()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"启动浏览器失败: %w\", err)\n\t}\n\tsession.Launcher = l\n\n\tbrowser := rod.New().ControlURL(launcherURL)\n\tif err := browser.Connect(); err != nil {\n\t\tl.Kill()\n\t\tl.Cleanup()\n\t\treturn nil, fmt.Errorf(\"连接浏览器失败: %w\", err)\n\t}\n\tsession.Browser = browser.Timeout(120 * time.Second)\n\tpage, err := session.Browser.Page(proto.TargetCreateTarget{URL: \"about:blank\"})\n\tif err != nil {\n\t\tsession.Close()\n\t\treturn nil, fmt.Errorf(\"创建页面失败: %w\", err)\n\t}\n\tsession.Page = page\n\n\t// 设置视口（使用常见分辨率）\n\tpage.SetViewport(&proto.EmulationSetDeviceMetricsOverride{\n\t\tWidth:  1920,\n\t\tHeight: 1080,\n\t})\n\n\treturn session, nil\n}\n\n// configureBrowserLauncher 配置浏览器启动参数（原生反检测，无需JS注入）\nfunc configureBrowserLauncher(l *launcher.Launcher, headless bool, proxy string) *launcher.Launcher {\n\t// 基础参数\n\tl = l.Set(\"no-sandbox\").\n\t\tSet(\"disable-setuid-sandbox\").\n\t\tSet(\"disable-dev-shm-usage\").\n\t\tSet(\"disable-gpu\").\n\t\tSet(\"no-first-run\").\n\t\tSet(\"no-default-browser-check\")\n\n\t// 核心反检测参数 - 通过启动参数原生禁用自动化标志\n\tl = l.Set(\"disable-blink-features\", \"AutomationControlled\").\n\t\tDelete(\"enable-automation\"). // 删除自动化标志\n\t\tSet(\"disable-features\", \"TranslateUI,AutofillServerCommunication\").\n\t\tSet(\"disable-ipc-flooding-protection\")\n\n\t// 窗口和显示参数\n\tl = l.Set(\"window-size\", \"1920,1080\").\n\t\tSet(\"start-maximized\").\n\t\tSet(\"lang\", \"en-US\")\n\n\t// 禁用可能暴露自动化的功能\n\tl = l.Set(\"disable-extensions\").\n\t\tSet(\"disable-component-extensions-with-background-pages\").\n\t\tSet(\"disable-background-networking\").\n\t\tSet(\"disable-sync\").\n\t\tSet(\"disable-default-apps\").\n\t\tSet(\"disable-infobars\").\n\t\tSet(\"disable-hang-monitor\").\n\t\tSet(\"disable-popup-blocking\").\n\t\tSet(\"disable-prompt-on-repost\").\n\t\tSet(\"disable-client-side-phishing-detection\").\n\t\tSet(\"disable-background-timer-throttling\").\n\t\tSet(\"disable-renderer-backgrounding\").\n\t\tSet(\"disable-backgrounding-occluded-windows\")\n\n\t// 性能相关参数\n\tl = l.Set(\"metrics-recording-only\").\n\t\tSet(\"safebrowsing-disable-auto-update\")\n\n\t// Headless 模式配置\n\tif headless {\n\t\t// 使用新版 headless 模式（Chrome 112+），更接近真实浏览器\n\t\t// 旧的 --headless 模式容易被检测\n\t\tl = l.Headless(false). // 不使用 rod 的 headless\n\t\t\t\t\tSet(\"headless\", \"new\") // 使用 Chrome 的新 headless 模式\n\t} else {\n\t\tl = l.Headless(false)\n\t}\n\n\t// 代理配置\n\tif proxy != \"\" {\n\t\tl = l.Proxy(proxy)\n\t}\n\n\treturn l\n}\n\n// SetupNetworkCapture 设置网络捕获（监听 authorization/configID/csesidx）\nfunc (s *BrowserSession) SetupNetworkCapture() {\n\tgo s.Page.EachEvent(func(e *proto.NetworkRequestWillBeSent) {\n\t\ts.mu.Lock()\n\t\tdefer s.mu.Unlock()\n\t\tif auth, ok := e.Request.Headers[\"authorization\"]; ok {\n\t\t\tif authStr := auth.String(); authStr != \"\" {\n\t\t\t\ts.Authorization = authStr\n\t\t\t}\n\t\t}\n\t\turl := e.Request.URL\n\t\tif m := regexp.MustCompile(`/cid/([a-f0-9-]+)`).FindStringSubmatch(url); len(m) > 1 && s.ConfigID == \"\" {\n\t\t\ts.ConfigID = m[1]\n\t\t}\n\t\tif m := regexp.MustCompile(`[?&]csesidx=(\\d+)`).FindStringSubmatch(url); len(m) > 1 && s.CSESIDX == \"\" {\n\t\t\ts.CSESIDX = m[1]\n\t\t}\n\t})()\n}\n\n// ExtractFromURL 从URL提取 configID 和 csesidx\nfunc (s *BrowserSession) ExtractFromURL() {\n\tinfo, _ := s.Page.Info()\n\tif info == nil {\n\t\treturn\n\t}\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\tif m := regexp.MustCompile(`/cid/([a-f0-9-]+)`).FindStringSubmatch(info.URL); len(m) > 1 && s.ConfigID == \"\" {\n\t\ts.ConfigID = m[1]\n\t}\n\tif m := regexp.MustCompile(`[?&]csesidx=(\\d+)`).FindStringSubmatch(info.URL); len(m) > 1 && s.CSESIDX == \"\" {\n\t\ts.CSESIDX = m[1]\n\t}\n}\n\n// ExtractCSESIDXFromAuth 从 authorization 提取 csesidx\nfunc (s *BrowserSession) ExtractCSESIDXFromAuth() {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\tif s.CSESIDX == \"\" && s.Authorization != \"\" {\n\t\ts.CSESIDX = extractCSESIDXFromAuth(s.Authorization)\n\t}\n}\n\n// Close 关闭浏览器会话\nfunc (s *BrowserSession) Close() {\n\tif s.Browser != nil {\n\t\ts.Browser.Close()\n\t}\n\tif s.Launcher != nil {\n\t\ts.Launcher.Kill()\n\t\ts.Launcher.Cleanup()\n\t}\n}\n\n// FindEmailInput 查找邮箱输入框\nfunc (s *BrowserSession) FindEmailInput() *rod.Element {\n\tfor _, sel := range emailInputSelectors {\n\t\tel, err := s.Page.Timeout(2 * time.Second).Element(sel)\n\t\tif err == nil && el != nil {\n\t\t\tvisible, _ := el.Visible()\n\t\t\tif visible {\n\t\t\t\treturn el\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\n// InputTextWithKeyboard 使用键盘逐字符输入\nfunc (s *BrowserSession) InputTextWithKeyboard(text string, delayMs int) {\n\tfor _, char := range text {\n\t\ts.Page.Keyboard.Type(input.Key(char))\n\t\ttime.Sleep(time.Duration(delayMs+rand.Intn(50)) * time.Millisecond)\n\t}\n}\n\n// ClickButton 点击匹配文本的按钮\nfunc (s *BrowserSession) ClickButton(targets []string, maxRetries int) bool {\n\tfor i := 0; i < maxRetries; i++ {\n\t\tclickResult, _ := s.Page.Eval(fmt.Sprintf(`() => {\n\t\t\tconst targets = %s;\n\t\t\tconst elements = [...document.querySelectorAll('button'), ...document.querySelectorAll('div[role=\"button\"]')];\n\t\t\tfor (const el of elements) {\n\t\t\t\tif (!el || el.disabled) continue;\n\t\t\t\tconst style = window.getComputedStyle(el);\n\t\t\t\tif (style.display === 'none' || style.visibility === 'hidden') continue;\n\t\t\t\tconst text = el.textContent ? el.textContent.trim() : '';\n\t\t\t\tif (targets.some(t => text.includes(t))) { el.click(); return {clicked:true}; }\n\t\t\t}\n\t\t\treturn {clicked:false};\n\t\t}`, toJSArray(targets)))\n\t\tif clickResult != nil && clickResult.Value.Get(\"clicked\").Bool() {\n\t\t\treturn true\n\t\t}\n\t\ttime.Sleep(1 * time.Second)\n\t}\n\treturn false\n}\n\n// toJSArray 将字符串数组转换为 JS 数组字符串\nfunc toJSArray(arr []string) string {\n\tquoted := make([]string, len(arr))\n\tfor i, s := range arr {\n\t\tquoted[i] = fmt.Sprintf(`\"%s\"`, s)\n\t}\n\treturn \"[\" + strings.Join(quoted, \",\") + \"]\"\n}\n\n// CollectCookies 收集页面 Cookies\nfunc (s *BrowserSession) CollectCookies(existingCookies []pool.Cookie) []pool.Cookie {\n\tcookieMap := make(map[string]pool.Cookie)\n\tfor _, c := range existingCookies {\n\t\tcookieMap[c.Name] = c\n\t}\n\tcookies, _ := s.Page.Cookies(nil)\n\tfor _, c := range cookies {\n\t\tcookieMap[c.Name] = pool.Cookie{\n\t\t\tName:   c.Name,\n\t\t\tValue:  c.Value,\n\t\t\tDomain: c.Domain,\n\t\t}\n\t}\n\tvar result []pool.Cookie\n\tfor _, c := range cookieMap {\n\t\tresult = append(result, c)\n\t}\n\treturn result\n}\n\nfunc extractVerificationCode(content string) (string, error) {\n\tre := regexp.MustCompile(`\\b[A-Z0-9]{6}\\b`)\n\tmatches := re.FindAllString(content, -1)\n\n\tfor _, code := range matches {\n\t\tif commonWords[code] {\n\t\t\tcontinue\n\t\t}\n\t\tif regexp.MustCompile(`[0-9]`).MatchString(code) {\n\t\t\treturn code, nil\n\t\t}\n\t}\n\n\tfor _, code := range matches {\n\t\tif !commonWords[code] {\n\t\t\treturn code, nil\n\t\t}\n\t}\n\n\tre2 := regexp.MustCompile(`(?i)code\\s*[:is]\\s*([A-Z0-9]{6})`)\n\tif m := re2.FindStringSubmatch(content); len(m) > 1 {\n\t\treturn m[1], nil\n\t}\n\n\treturn \"\", fmt.Errorf(\"无法从邮件中提取验证码\")\n}\nfunc safeType(page *rod.Page, text string, delay int) error {\n\t// 一次性设置输入框值（更稳定）\n\ttext = strings.TrimSpace(text)\n\tif text == \"\" {\n\t\treturn nil\n\t}\n\n\t// 先尝试使用JS直接设置值（更稳定）\n\t_, err := page.Eval(fmt.Sprintf(`() => {\n\t\tconst inputs = document.querySelectorAll('input');\n\t\tif (inputs.length > 0) {\n\t\t\tconst input = inputs[0];\n\t\t\tinput.value = %q;\n\t\t\tinput.dispatchEvent(new Event('input', { bubbles: true }));\n\t\t\tinput.dispatchEvent(new Event('change', { bubbles: true }));\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}`, text))\n\tif err == nil {\n\t\ttime.Sleep(200 * time.Millisecond)\n\t\treturn nil\n\t}\n\n\t// 回退到逐字符输入\n\tfor _, char := range text {\n\t\tif err := page.Keyboard.Type(input.Key(char)); err != nil {\n\t\t\treturn err\n\t\t}\n\t\ttime.Sleep(time.Duration(delay) * time.Millisecond)\n\t}\n\treturn nil\n}\n\n// debugScreenshot 调试截图\nfunc debugScreenshot(page *rod.Page, threadID int, step string) {\n\tif !RegisterDebug {\n\t\treturn\n\t}\n\tscreenshotDir := filepath.Join(DataDir, \"screenshots\")\n\tos.MkdirAll(screenshotDir, 0755)\n\n\tfilename := filepath.Join(screenshotDir, fmt.Sprintf(\"thread%d_%s_%d.png\", threadID, step, time.Now().Unix()))\n\tdata, err := page.Screenshot(true, nil)\n\tif err != nil {\n\t\tlog.Printf(\"[注册 %d] 📸 截图失败: %v\", threadID, err)\n\t\treturn\n\t}\n\tif err := os.WriteFile(filename, data, 0644); err != nil {\n\t\tlog.Printf(\"[注册 %d] 📸 保存截图失败: %v\", threadID, err)\n\t\treturn\n\t}\n\tlog.Printf(\"[注册 %d] 📸 截图保存: %s\", threadID, filename)\n}\n\n// handleAdditionalSteps 处理额外步骤（复选框等）\nfunc handleAdditionalSteps(page *rod.Page, threadID int) bool {\n\tlog.Printf(\"[注册 %d] 检查是否需要处理额外步骤...\", threadID)\n\n\thasAdditionalSteps := false\n\n\t// 首先检查是否有\"出了点问题\"错误页面，需要点击重试\n\tretryResult, _ := page.Eval(`() => {\n\t\tconst pageText = document.body ? document.body.innerText : '';\n\t\tif (pageText.includes('出了点问题') || pageText.includes('Something went wrong') || \n\t\t\tpageText.includes('went wrong')) {\n\t\t\t// 查找重试按钮 - 优先使用 mdc-button__label\n\t\t\tconst tryAgainLabel = document.querySelector('.mdc-button__label');\n\t\t\tif (tryAgainLabel && (tryAgainLabel.textContent.includes('Try again') || \n\t\t\t\ttryAgainLabel.textContent.includes('重试') || tryAgainLabel.textContent.includes('再试'))) {\n\t\t\t\tconst btn = tryAgainLabel.closest('button');\n\t\t\t\tif (btn) {\n\t\t\t\t\tbtn.click();\n\t\t\t\t\treturn { clicked: true, action: 'retry_mdc' };\n\t\t\t\t}\n\t\t\t}\n\t\t\t// 备用：查找所有按钮\n\t\t\tconst buttons = document.querySelectorAll('button');\n\t\t\tfor (const btn of buttons) {\n\t\t\t\tconst text = btn.textContent || '';\n\t\t\t\tif (text.includes('重试') || text.includes('Retry') || text.includes('再试') || \n\t\t\t\t\ttext.includes('Try again') || text.includes('try again')) {\n\t\t\t\t\tbtn.click();\n\t\t\t\t\treturn { clicked: true, action: 'retry' };\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn { clicked: false };\n\t}`)\n\n\tif retryResult != nil && retryResult.Value.Get(\"clicked\").Bool() {\n\t\tlog.Printf(\"[注册 %d] 检测到错误页面，已点击重试按钮\", threadID)\n\t\ttime.Sleep(3 * time.Second)\n\t\treturn true\n\t}\n\n\t// 检查是否需要同意条款（主要处理复选框）\n\tcheckboxResult, _ := page.Eval(`() => {\n\t\tconst checkboxes = document.querySelectorAll('input[type=\"checkbox\"]');\n\t\tfor (const checkbox of checkboxes) {\n\t\t\tif (!checkbox.checked) {\n\t\t\t\tcheckbox.click();\n\t\t\t\treturn { clicked: true };\n\t\t\t}\n\t\t}\n\t\treturn { clicked: false };\n\t}`)\n\n\tif checkboxResult != nil && checkboxResult.Value.Get(\"clicked\").Bool() {\n\t\thasAdditionalSteps = true\n\t\tlog.Printf(\"[注册 %d] 已勾选条款复选框\", threadID)\n\t\ttime.Sleep(1 * time.Second)\n\t}\n\n\t// 如果有额外步骤，尝试提交\n\tif hasAdditionalSteps {\n\t\tlog.Printf(\"[注册 %d] 发现有额外步骤，尝试提交...\", threadID)\n\n\t\t// 尝试提交额外信息\n\t\tfor i := 0; i < 3; i++ {\n\t\t\tsubmitResult, _ := page.Eval(`() => {\n\t\t\t\tconst submitButtons = [\n\t\t\t\t\t...document.querySelectorAll('button'),\n\t\t\t\t\t...document.querySelectorAll('input[type=\"submit\"]')\n\t\t\t\t];\n\t\t\t\t\n\t\t\t\tfor (const button of submitButtons) {\n\t\t\t\t\tif (!button.disabled && button.offsetParent !== null) {\n\t\t\t\t\t\tconst text = button.textContent || '';\n\t\t\t\t\t\tif (text.includes('同意') || text.includes('Confirm') || \n\t\t\t\t\t\t\ttext.includes('继续') || text.includes('Next') || \n\t\t\t\t\t\t\ttext.includes('Submit') || text.includes('完成')) {\n\t\t\t\t\t\t\tbutton.click();\n\t\t\t\t\t\t\treturn { clicked: true };\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// 点击第一个可用的提交按钮\n\t\t\t\tfor (const button of submitButtons) {\n\t\t\t\t\tif (!button.disabled && button.offsetParent !== null) {\n\t\t\t\t\t\tbutton.click();\n\t\t\t\t\t\treturn { clicked: true };\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\treturn { clicked: false };\n\t\t\t}`)\n\n\t\t\tif submitResult != nil && submitResult.Value.Get(\"clicked\").Bool() {\n\t\t\t\tlog.Printf(\"[注册 %d] 已提交额外信息\", threadID)\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\ttime.Sleep(1 * time.Second)\n\t\t}\n\n\t\t// 等待可能的跳转\n\t\ttime.Sleep(3 * time.Second)\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n// checkAndHandleAdminPage 检查并处理管理创建页面\nfunc checkAndHandleAdminPage(page *rod.Page, threadID int) bool {\n\tcurrentURL := \"\"\n\tinfo, _ := page.Info()\n\tif info != nil {\n\t\tcurrentURL = info.URL\n\t}\n\n\t// 检查是否是管理创建页面\n\tif strings.Contains(currentURL, \"/admin/create\") {\n\t\tlog.Printf(\"[注册 %d] 检测到管理创建页面，尝试完成设置...\", threadID)\n\n\t\t// 尝试查找并点击继续按钮\n\t\tformCompleted, _ := page.Eval(`() => {\n\t\t\tlet completed = false;\n\t\t\t\n\t\t\t// 查找并点击继续按钮\n\t\t\tconst continueTexts = ['Continue', '继续', 'Next', 'Submit', 'Finish', '完成'];\n\t\t\tconst allButtons = document.querySelectorAll('button');\n\t\t\t\n\t\t\tfor (const button of allButtons) {\n\t\t\t\tif (button.offsetParent !== null && !button.disabled) {\n\t\t\t\t\tconst text = (button.textContent || '').trim();\n\t\t\t\t\tif (continueTexts.some(t => text.includes(t))) {\n\t\t\t\t\t\tbutton.click();\n\t\t\t\t\t\tconsole.log('点击继续按钮:', text);\n\t\t\t\t\t\tcompleted = true;\n\t\t\t\t\t\treturn completed;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// 如果没有找到特定按钮，尝试点击第一个可见按钮\n\t\t\tfor (const button of allButtons) {\n\t\t\t\tif (button.offsetParent !== null && !button.disabled) {\n\t\t\t\t\tconst text = button.textContent || '';\n\t\t\t\t\tif (text.trim() && !text.includes('Cancel') && !text.includes('取消')) {\n\t\t\t\t\t\tbutton.click();\n\t\t\t\t\t\tconsole.log('点击通用按钮:', text);\n\t\t\t\t\t\tcompleted = true;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\treturn completed;\n\t\t}`)\n\n\t\tif formCompleted != nil && formCompleted.Value.Bool() {\n\t\t\tlog.Printf(\"[注册 %d] 已处理管理表单，等待跳转...\", threadID)\n\t\t\ttime.Sleep(5 * time.Second)\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc RunBrowserRegister(headless bool, proxy string, threadID int) (result *BrowserRegisterResult) {\n\tresult = &BrowserRegisterResult{}\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\tlog.Printf(\"[注册 %d] ☠️ panic 恢复: %v\", threadID, r)\n\t\t\tresult.Error = fmt.Errorf(\"panic: %v\", r)\n\t\t}\n\t}()\n\n\t// 获取临时邮箱\n\temail, err := getTemporaryEmail()\n\tif err != nil {\n\t\tresult.Error = err\n\t\treturn result\n\t}\n\tresult.Email = email\n\n\t// 启动浏览器 - 使用统一的浏览器查找逻辑\n\tl := launcher.New()\n\tif browserPath, found := findBrowser(); found {\n\t\tl = l.Bin(browserPath)\n\t\tlog.Printf(\"[注册 %d] 使用浏览器: %s\", threadID, browserPath)\n\t} else {\n\t\tlog.Printf(\"[注册 %d] ⚠️ 未找到系统浏览器，尝试使用 rod 自动下载\", threadID)\n\t}\n\n\t// 使用统一的浏览器配置（原生反检测，无需JS注入）\n\tl = configureBrowserLauncher(l, headless, proxy)\n\n\tlauncherURL, err := l.Launch()\n\tif err != nil {\n\t\tresult.Error = fmt.Errorf(\"启动浏览器失败: %w\", err)\n\t\treturn result\n\t}\n\n\t// 确保浏览器进程和临时目录被清理（即使连接失败）\n\tdefer func() {\n\t\tif l != nil {\n\t\t\tl.Kill()\n\t\t\tl.Cleanup() // 等待浏览器退出并清理临时用户数据目录\n\t\t}\n\t}()\n\n\tbrowser := rod.New().ControlURL(launcherURL)\n\tif err := browser.Connect(); err != nil {\n\t\tresult.Error = fmt.Errorf(\"连接浏览器失败: %w\", err)\n\t\treturn result\n\t}\n\tdefer browser.Close()\n\n\tbrowser = browser.Timeout(120 * time.Second)\n\n\t// 直接创建页面，不使用 stealth 注入（依赖启动参数实现反检测）\n\tpage, err := browser.Page(proto.TargetCreateTarget{URL: \"about:blank\"})\n\tif err != nil {\n\t\tresult.Error = fmt.Errorf(\"创建页面失败: %w\", err)\n\t\treturn result\n\t}\n\n\t// 设置视口（使用常见分辨率）\n\tif err := page.SetViewport(&proto.EmulationSetDeviceMetricsOverride{\n\t\tWidth:  1920,\n\t\tHeight: 1080,\n\t}); err != nil {\n\t\tlog.Printf(\"[注册 %d] ⚠️ 设置视口失败: %v\", threadID, err)\n\t}\n\n\t// 监听请求以捕获 authorization\n\tvar authorization string\n\tvar configID, csesidx string\n\n\tgo page.EachEvent(func(e *proto.NetworkRequestWillBeSent) {\n\t\tif auth, ok := e.Request.Headers[\"authorization\"]; ok {\n\t\t\tif authStr := auth.String(); authStr != \"\" {\n\t\t\t\tauthorization = authStr\n\t\t\t}\n\t\t}\n\t\turl := e.Request.URL\n\t\tif m := regexp.MustCompile(`/cid/([a-f0-9-]+)`).FindStringSubmatch(url); len(m) > 1 && configID == \"\" {\n\t\t\tconfigID = m[1]\n\t\t}\n\t\tif m := regexp.MustCompile(`[?&]csesidx=(\\d+)`).FindStringSubmatch(url); len(m) > 1 && csesidx == \"\" {\n\t\t\tcsesidx = m[1]\n\t\t}\n\t})()\n\tif err := page.Navigate(\"https://business.gemini.google\"); err != nil {\n\t\tresult.Error = fmt.Errorf(\"打开页面失败: %w\", err)\n\t\treturn result\n\t}\n\tpage.WaitLoad()\n\ttime.Sleep(1 * time.Second)\n\n\t// 检查是否被代理403阻止\n\tstatusCheck, _ := page.Eval(`() => {\n\t\tconst pageText = document.body ? document.body.innerText : '';\n\t\tconst title = document.title || '';\n\t\tconst html = document.documentElement ? document.documentElement.outerHTML : '';\n\t\t\n\t\t// 检查403/被阻止的特征\n\t\tconst is403 = title.includes('403') || pageText.includes('403 Forbidden') || \n\t\t\tpageText.includes('Access Denied') || pageText.includes('访问被拒绝') ||\n\t\t\thtml.length < 500; // 页面内容过少可能是403\n\t\t\t\n\t\t// 检查是否还在加载\n\t\tconst hasLoader = document.querySelector('[class*=\"loading\"]') || \n\t\t\tdocument.querySelector('[class*=\"spinner\"]');\n\t\t\n\t\treturn {\n\t\t\tis403: is403,\n\t\t\tisLoading: !!hasLoader,\n\t\t\thtmlLen: html.length,\n\t\t\ttitle: title,\n\t\t\turl: window.location.href\n\t\t};\n\t}`)\n\n\tif statusCheck != nil {\n\t\tis403 := statusCheck.Value.Get(\"is403\").Bool()\n\t\tisLoading := statusCheck.Value.Get(\"isLoading\").Bool()\n\t\thtmlLen := statusCheck.Value.Get(\"htmlLen\").Int()\n\t\tpageURL := statusCheck.Value.Get(\"url\").String()\n\n\t\tlog.Printf(\"[注册 %d] 页面状态: is403=%v, loading=%v, htmlLen=%d, url=%s\",\n\t\t\tthreadID, is403, isLoading, htmlLen, pageURL)\n\n\t\tif is403 {\n\t\t\tresult.Error = fmt.Errorf(\"代理被403阻止，请更换代理\")\n\t\t\treturn result\n\t\t}\n\n\t\t// 如果还在加载，多等待一会儿\n\t\tif isLoading || htmlLen < 1000 {\n\t\t\ttime.Sleep(3 * time.Second)\n\t\t\tpage.WaitLoad()\n\t\t}\n\t}\n\n\tdebugScreenshot(page, threadID, \"01_page_loaded\")\n\twelcomeResult, _ := page.Eval(`() => {\n\t\tconst text = document.body ? document.body.textContent : '';\n\t\tconst isWelcome = text.includes('Welcome to Gemini') || text.includes('欢迎使用 Gemini') ||\n\t\t\ttext.includes('Start free trial') || text.includes('开始免费试用') ||\n\t\t\ttext.includes('Sign in or create');\n\t\treturn { isWelcome };\n\t}`)\n\tif welcomeResult != nil && welcomeResult.Value.Get(\"isWelcome\").Bool() {\n\t\t// 尝试点击各种可能的按钮\n\t\tpage.Eval(`() => {\n\t\t\tconst buttons = document.querySelectorAll('a, button');\n\t\t\tfor (const btn of buttons) {\n\t\t\t\tconst text = btn.textContent || btn.innerText || '';\n\t\t\t\tif (text.includes('free trial') || text.includes('免费试用') ||\n\t\t\t\t\ttext.includes('Create') || text.includes('创建') ||\n\t\t\t\t\ttext.includes('Get started') || text.includes('开始')) {\n\t\t\t\t\tbtn.click();\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t\t// 尝试点击主要的 CTA 按钮\n\t\t\tconst cta = document.querySelector('[data-iph=\"free_trial\"], .cta-button, a[href*=\"signup\"], a[href*=\"create\"]');\n\t\t\tif (cta) cta.click();\n\t\t\treturn false;\n\t\t}`)\n\t\ttime.Sleep(1 * time.Second)\n\t\tpage.WaitLoad()\n\t}\n\n\tif _, err := page.Timeout(15 * time.Second).Element(\"input\"); err != nil {\n\t\tresult.Error = fmt.Errorf(\"等待输入框超时: %w\", err)\n\t\treturn result\n\t}\n\ttime.Sleep(200 * time.Millisecond)\n\tlog.Printf(\"[注册 %d] 准备输入邮箱: %s\", threadID, email)\n\ttime.Sleep(500 * time.Millisecond)\n\tvar emailInput *rod.Element\n\tselectors := []string{\n\t\t\"#email-input\",            // Google Business 特定 ID\n\t\t\"input[name='loginHint']\", // Google Business 特定 name\n\t\t\"input[jsname='YPqjbf']\",  // Google jsname\n\t\t\"input[type='email']\",\n\t\t\"input[type='text'][aria-label]\",\n\t\t\"input:not([type='hidden']):not([type='submit']):not([type='checkbox'])\",\n\t}\n\tfor _, sel := range selectors {\n\t\tel, err := page.Timeout(3 * time.Second).Element(sel)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tif el != nil {\n\t\t\tvisible, _ := el.Visible()\n\t\t\tif visible {\n\t\t\t\temailInput = el\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\tif emailInput == nil {\n\t\t// 先检查页面状态\n\t\tpageState, _ := page.Eval(`() => {\n\t\t\tconst pageText = document.body ? document.body.innerText : '';\n\t\t\tconst htmlLen = document.documentElement ? document.documentElement.outerHTML.length : 0;\n\t\t\treturn {\n\t\t\t\thtmlLen: htmlLen,\n\t\t\t\thas403: pageText.includes('403') || pageText.includes('Forbidden') || pageText.includes('Denied'),\n\t\t\t\thasError: pageText.includes('出了点问题') || pageText.includes('went wrong'),\n\t\t\t\tisAuthPage: window.location.href.includes('auth.business.gemini'),\n\t\t\t\turl: window.location.href\n\t\t\t};\n\t\t}`)\n\n\t\tif pageState != nil {\n\t\t\thas403 := pageState.Value.Get(\"has403\").Bool()\n\t\t\thasError := pageState.Value.Get(\"hasError\").Bool()\n\t\t\thtmlLen := pageState.Value.Get(\"htmlLen\").Int()\n\t\t\tisAuthPage := pageState.Value.Get(\"isAuthPage\").Bool()\n\n\t\t\tif has403 || htmlLen < 500 {\n\t\t\t\tresult.Error = fmt.Errorf(\"代理403/被阻止，页面未正常加载\")\n\t\t\t\treturn result\n\t\t\t}\n\t\t\tif hasError {\n\t\t\t\tresult.Error = fmt.Errorf(\"页面显示错误，可能被IP限制\")\n\t\t\t\treturn result\n\t\t\t}\n\t\t\tif isAuthPage && htmlLen < 2000 {\n\t\t\t\ttime.Sleep(5 * time.Second)\n\t\t\t\tfor _, sel := range selectors {\n\t\t\t\t\tel, err := page.Timeout(3 * time.Second).Element(sel)\n\t\t\t\t\tif err == nil && el != nil {\n\t\t\t\t\t\tif visible, _ := el.Visible(); visible {\n\t\t\t\t\t\t\temailInput = el\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif emailInput == nil {\n\t\t\thtml, _ := page.HTML()\n\t\t\tif len(html) > 2000 {\n\t\t\t\thtml = html[:2000]\n\t\t\t}\n\t\t\tlog.Printf(\"[注册 %d] ❌ 找不到邮箱输入框，页面HTML片段: %s\", threadID, html)\n\t\t\tresult.Error = fmt.Errorf(\"找不到邮箱输入框（页面未正常加载）\")\n\t\t\treturn result\n\t\t}\n\t}\n\n\t// 获取元素信息\n\ttagName, _ := emailInput.Property(\"tagName\")\n\tinputType, _ := emailInput.Property(\"type\")\n\tinputId, _ := emailInput.Property(\"id\")\n\tinputName, _ := emailInput.Property(\"name\")\n\tlog.Printf(\"[注册 %d] 📝 元素信息: tag=%s, type=%s, id=%s, name=%s\",\n\t\tthreadID, tagName.String(), inputType.String(), inputId.String(), inputName.String())\n\tlog.Printf(\"[注册 %d] 📍 拟人化聚焦输入框...\", threadID)\n\tif err := humanFocusInput(page, emailInput); err != nil {\n\t\tlog.Printf(\"[注册 %d] ⚠️ 聚焦失败，回退到直接点击: %v\", threadID, err)\n\t\temailInput.MustScrollIntoView()\n\t\thumanDelay(100, 300)\n\t\temailInput.MustClick()\n\t}\n\thumanDelay(200, 400)\n\thasFocus, _ := page.Eval(`() => document.activeElement && document.activeElement.id`)\n\tlog.Printf(\"[注册 %d] 🎯 当前焦点元素ID: %v\", threadID, hasFocus.Value)\n\n\t// 清空输入框 - 使用 triple-click 全选然后删除\n\tlog.Printf(\"[注册 %d] 🗑️ 清空输入框...\", threadID)\n\t// 先检查当前是否有内容\n\tcurrentVal, _ := emailInput.Property(\"value\")\n\tif currentVal.String() != \"\" {\n\t\temailInput.SelectAllText()\n\t\thumanDelay(80, 150)\n\t\tpage.Keyboard.Type(input.Backspace)\n\t\thumanDelay(80, 150)\n\t}\n\n\t// 使用拟人化打字输入邮箱\n\tlog.Printf(\"[注册 %d] ⌨️ 开始拟人化输入邮箱: %s\", threadID, email)\n\thumanType(page, email)\n\tlog.Printf(\"[注册 %d] ⌨️ 邮箱输入完成\", threadID)\n\n\ttime.Sleep(500 * time.Millisecond)\n\n\t// 验证输入\n\tpropVal, _ := emailInput.Property(\"value\")\n\tinputValue := propVal.String()\n\tlog.Printf(\"[注册 %d] 📋 最终输入值: [%s]\", threadID, inputValue)\n\n\tif inputValue != email {\n\t} else {\n\t\tlog.Printf(\"[注册 %d] ✅ 输入验证成功\", threadID)\n\t}\n\n\t// 触发 blur\n\tpage.Eval(`() => {\n\t\tconst inputs = document.querySelectorAll('input');\n\t\tif (inputs.length > 0) {\n\t\t\tinputs[0].blur();\n\t\t}\n\t}`)\n\ttime.Sleep(500 * time.Millisecond)\n\tdebugScreenshot(page, threadID, \"03_before_submit\")\n\temailSubmitted := false\n\tfor i := 0; i < 8; i++ {\n\t\t// 拟人化延迟（模拟人类寻找按钮的时间）\n\t\thumanDelay(200, 500)\n\n\t\t// 使用 rod 原生方式查找并点击按钮（避免 JS click() 被检测）\n\t\tbuttonSelectors := []string{\n\t\t\t`button`,\n\t\t\t`input[type=\"submit\"]`,\n\t\t\t`div[role=\"button\"]`,\n\t\t\t`span[role=\"button\"]`,\n\t\t}\n\t\ttargetTexts := []string{\"继续\", \"Next\", \"邮箱\", \"Continue\"}\n\n\t\tvar targetButton *rod.Element\n\t\tfor _, sel := range buttonSelectors {\n\t\t\telements, err := page.Elements(sel)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfor _, el := range elements {\n\t\t\t\tvisible, _ := el.Visible()\n\t\t\t\tif !visible {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\ttext, _ := el.Text()\n\t\t\t\ttext = strings.TrimSpace(text)\n\t\t\t\tfor _, target := range targetTexts {\n\t\t\t\t\tif strings.Contains(text, target) {\n\t\t\t\t\t\ttargetButton = el\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif targetButton != nil {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t\tif targetButton != nil {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif targetButton != nil {\n\t\t\tlog.Printf(\"[注册 %d] 找到提交按钮，执行拟人化点击\", threadID)\n\t\t\tif err := humanClick(page, targetButton); err != nil {\n\t\t\t\tlog.Printf(\"[注册 %d] ⚠️ humanClick 失败: %v，回退到直接点击\", threadID, err)\n\t\t\t\ttargetButton.MustClick()\n\t\t\t}\n\t\t\thumanDelay(300, 600)\n\t\t\temailSubmitted = true\n\t\t\tbreak\n\t\t}\n\n\t\tlog.Printf(\"[注册 %d] 尝试 %d/8: 未找到按钮\", threadID, i+1)\n\t\thumanDelay(800, 1200)\n\t}\n\tif !emailSubmitted {\n\t\tresult.Error = fmt.Errorf(\"找不到提交按钮\")\n\t\treturn result\n\t}\n\n\t// 等待页面跳转，最多等待15秒\n\tvar needsVerification bool\n\tvar pageTransitioned bool\n\tvar detectedSigninError bool\n\tfor waitCount := 0; waitCount < 12; waitCount++ { // 优化：减少最大等待次数\n\t\thumanDelay(600, 1000)\n\n\t\t// 检查是否被重定向到 signin-error 页面（提前检测）\n\t\tpageInfo, _ := page.Info()\n\t\tif pageInfo != nil && strings.Contains(pageInfo.URL, \"signin-error\") {\n\t\t\tlog.Printf(\"[注册 %d] ⚠️ 在等待过程中检测到 signin-error 页面\", threadID)\n\t\t\tdetectedSigninError = true\n\t\t\tpageTransitioned = true\n\t\t\tbreak\n\t\t}\n\n\t\t// 检查页面是否已经离开邮箱输入页面\n\t\ttransitionResult, _ := page.Eval(`() => {\n\t\t\tconst pageText = document.body ? document.body.textContent : '';\n\t\t\tconst emailInput = document.querySelector('input[type=\"email\"]');\n\t\t\tconst continueBtn = document.querySelector('button[jsname=\"LgbsSe\"]');\n\t\t\tconst stillOnEmailPage = (emailInput && emailInput.offsetParent !== null) || \n\t\t\t\t(continueBtn && continueBtn.innerText && \n\t\t\t\t (continueBtn.innerText.includes('继续') || continueBtn.innerText.includes('Continue')));\n\t\t\tconst isVerifyPage = pageText.includes('验证') || pageText.includes('Verify') || \n\t\t\t\tpageText.includes('输入代码') || pageText.includes('Enter code') ||\n\t\t\t\tpageText.includes('发送到') || pageText.includes('sent to');\n\t\t\tconst isNamePage = pageText.includes('姓氏') || pageText.includes('名字') || \n\t\t\t\tpageText.includes('Full name') || pageText.includes('全名');\n\t\t\tconst errorElement = document.querySelector('.zyTWof-Ng57nc, .zyTWof-gIZMF');\n\t\t\tconst hasErrorElement = errorElement && errorElement.offsetParent !== null && \n\t\t\t\terrorElement.textContent && errorElement.textContent.length > 0;\n\t\t\tconst hasError = hasErrorElement || \n\t\t\t\tpageText.includes('出了点问题') || pageText.includes('Something went wrong') ||\n\t\t\t\tpageText.includes('无法创建') || pageText.includes('cannot create') ||\n\t\t\t\tpageText.includes('try again later') || pageText.includes('稀后再试') ||\n\t\t\t\tpageText.includes('需要电话') || pageText.includes('电话号码') || \n\t\t\t\tpageText.includes('Phone number') || pageText.includes('Verify your phone');\n\t\t\treturn {\n\t\t\t\tstillOnEmailPage: stillOnEmailPage && !isVerifyPage && !isNamePage,\n\t\t\t\tisVerifyPage: isVerifyPage,\n\t\t\t\tisNamePage: isNamePage,\n\t\t\t\thasError: hasError,\n\t\t\t\terrorText: hasError ? document.body.innerText.substring(0, 100) : ''\n\t\t\t};\n\t\t}`)\n\n\t\tif transitionResult != nil {\n\t\t\tif transitionResult.Value.Get(\"hasError\").Bool() {\n\t\t\t\tresult.Error = fmt.Errorf(\"页面显示错误: %s\", transitionResult.Value.Get(\"errorText\").String())\n\t\t\t\tlog.Printf(\"[注册 %d] ❌ %v\", threadID, result.Error)\n\t\t\t\treturn result\n\t\t\t}\n\n\t\t\tif !transitionResult.Value.Get(\"stillOnEmailPage\").Bool() {\n\t\t\t\tpageTransitioned = true\n\t\t\t\tneedsVerification = transitionResult.Value.Get(\"isVerifyPage\").Bool()\n\t\t\t\tisNamePage := transitionResult.Value.Get(\"isNamePage\").Bool()\n\t\t\t\tlog.Printf(\"[注册 %d] 页面已跳转: needsVerification=%v, isNamePage=%v\", threadID, needsVerification, isNamePage)\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif waitCount%3 == 2 {\n\t\t\tlog.Printf(\"[注册 %d] 等待页面跳转... (%d/15秒)\", threadID, waitCount+1)\n\t\t}\n\t}\n\n\t// 跳转后再次检查 signin-error（以防在 break 后才跳转）\n\tif !detectedSigninError {\n\t\tpageInfo, _ := page.Info()\n\t\tif pageInfo != nil && strings.Contains(pageInfo.URL, \"signin-error\") {\n\t\t\tdetectedSigninError = true\n\t\t}\n\t}\n\n\tdebugScreenshot(page, threadID, \"04_after_submit\")\n\n\tif !pageTransitioned {\n\t\t// 页面没有跳转，可能需要重新点击按钮\n\t\tlog.Printf(\"[注册 %d] 页面未跳转，尝试重新点击按钮\", threadID)\n\t\tpage.Eval(`() => {\n\t\t\tconst btn = document.querySelector('button[jsname=\"LgbsSe\"]');\n\t\t\tif (btn) btn.click();\n\t\t}`)\n\t\ttime.Sleep(3 * time.Second)\n\t\tneedsVerification = true // 假设需要验证\n\t}\n\n\t// 再次检查页面状态\n\tcheckResult, _ := page.Eval(`() => {\n\t\tconst pageText = document.body ? document.body.textContent : '';\n\t\t\n\t\t// 检查常见错误\n\t\tif (pageText.includes('出了点问题') || pageText.includes('Something went wrong') ||\n\t\t\tpageText.includes('无法创建') || pageText.includes('cannot create') ||\n\t\t\tpageText.includes('不安全') || pageText.includes('secure') ||\n\t\t\tpageText.includes('电话') || pageText.includes('Phone') || pageText.includes('number')) {\n\t\t\treturn { error: true, text: document.body.innerText.substring(0, 100) };\n\t\t}\n\n\t\t// 检查是否需要验证码\n\t\tif (pageText.includes('验证') || pageText.includes('Verify') || \n\t\t\tpageText.includes('code') || pageText.includes('sent')) {\n\t\t\treturn { needsVerification: true, isNamePage: false };\n\t\t}\n\t\t\n\t\t// 检查是否已经到了姓名页面\n\t\tif (pageText.includes('姓氏') || pageText.includes('名字') || \n\t\t\tpageText.includes('Full name') || pageText.includes('全名')) {\n\t\t\treturn { needsVerification: false, isNamePage: true };\n\t\t}\n\t\t\n\t\treturn { needsVerification: true, isNamePage: false };\n\t}`)\n\n\tif checkResult != nil {\n\t\tif checkResult.Value.Get(\"error\").Bool() {\n\t\t\terrText := checkResult.Value.Get(\"text\").String()\n\t\t\tresult.Error = fmt.Errorf(\"页面显示错误: %s\", errText)\n\t\t\tlog.Printf(\"[注册 %d] ❌ %v\", threadID, result.Error)\n\t\t\treturn result\n\t\t}\n\t\tneedsVerification = checkResult.Value.Get(\"needsVerification\").Bool()\n\t\tisNamePage := checkResult.Value.Get(\"isNamePage\").Bool()\n\t\tlog.Printf(\"[注册 %d] 页面状态: needsVerification=%v, isNamePage=%v\", threadID, needsVerification, isNamePage)\n\t} else {\n\t\tneedsVerification = true\n\t}\n\n\t// 检测并处理 signin-error 页面（被检测到后的恢复）\n\tif detectedSigninError {\n\t\tlog.Printf(\"[注册 %d] ⚠️ 开始处理 signin-error 页面恢复...\", threadID)\n\n\t\t// 等待页面完全加载\n\t\tpage.WaitLoad()\n\t\thumanDelay(500, 800)\n\n\t\tlog.Printf(\"[注册 %d] 开始尝试恢复...\", threadID)\n\n\t\t// 查找 \"Sign up or sign in\" 按钮（带重试）\n\t\tvar signupButton *rod.Element\n\t\tvar findErr error\n\t\tfor attempt := 0; attempt < 3; attempt++ {\n\t\t\tsignupButton, findErr = func() (*rod.Element, error) {\n\t\t\t\tbuttonSelectors := []string{\"button\", `div[role=\"button\"]`, `span[role=\"button\"]`}\n\t\t\t\ttargetTexts := []string{\"Sign up\", \"sign in\", \"Sign in\", \"注册\", \"登录\"}\n\n\t\t\t\tfor _, sel := range buttonSelectors {\n\t\t\t\t\telements, err := page.Elements(sel)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tfor _, el := range elements {\n\t\t\t\t\t\tvisible, _ := el.Visible()\n\t\t\t\t\t\tif !visible {\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\t\t\t\t\t\ttext, _ := el.Text()\n\t\t\t\t\t\tfor _, target := range targetTexts {\n\t\t\t\t\t\t\tif strings.Contains(text, target) {\n\t\t\t\t\t\t\t\treturn el, nil\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn nil, fmt.Errorf(\"未找到 Sign up or sign in 按钮\")\n\t\t\t}()\n\n\t\t\tif signupButton != nil {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tlog.Printf(\"[注册 %d] 尝试 %d/3: %v，等待后重试...\", threadID, attempt+1, findErr)\n\t\t\thumanDelay(400, 600)\n\t\t}\n\n\t\tif signupButton == nil {\n\t\t\tlog.Printf(\"[注册 %d] ❌ %v，放弃恢复\", threadID, findErr)\n\t\t\tresult.Error = fmt.Errorf(\"被检测并重定向到 signin-error，恢复失败\")\n\t\t\treturn result\n\t\t}\n\n\t\tlog.Printf(\"[注册 %d] 找到恢复按钮，执行拟人化点击\", threadID)\n\t\thumanClick(page, signupButton)\n\t\thumanDelay(500, 800)\n\n\t\t// 等待页面加载，重新查找邮箱输入框\n\t\tpage.WaitLoad()\n\t\thumanDelay(300, 500)\n\n\t\t// 重新输入邮箱\n\t\tvar retryEmailInput *rod.Element\n\t\tretryEmailSelectors := []string{\n\t\t\t\"#email-input\",\n\t\t\t\"input[name='loginHint']\",\n\t\t\t\"input[type='email']\",\n\t\t\t\"input[type='text'][aria-label]\",\n\t\t}\n\t\tfor _, sel := range retryEmailSelectors {\n\t\t\tel, err := page.Timeout(3 * time.Second).Element(sel)\n\t\t\tif err == nil && el != nil {\n\t\t\t\tif visible, _ := el.Visible(); visible {\n\t\t\t\t\tretryEmailInput = el\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif retryEmailInput != nil {\n\t\t\tlog.Printf(\"[注册 %d] 重新输入邮箱: %s\", threadID, email)\n\t\t\thumanFocusInput(page, retryEmailInput)\n\t\t\thumanDelay(200, 400)\n\t\t\tretryEmailInput.SelectAllText()\n\t\t\thumanDelay(50, 100)\n\t\t\tpage.Keyboard.Type(input.Backspace)\n\t\t\thumanDelay(80, 150)\n\t\t\thumanType(page, email)\n\t\t\thumanDelay(400, 700)\n\n\t\t\t// 重新点击提交按钮\n\t\t\tretryButtonSelectors := []string{\"button\", `input[type=\"submit\"]`, `div[role=\"button\"]`}\n\t\t\tretryTargetTexts := []string{\"继续\", \"Next\", \"Continue\"}\n\t\t\tvar retrySubmitBtn *rod.Element\n\t\t\tfor _, sel := range retryButtonSelectors {\n\t\t\t\telements, _ := page.Elements(sel)\n\t\t\t\tfor _, el := range elements {\n\t\t\t\t\tif visible, _ := el.Visible(); !visible {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\ttext, _ := el.Text()\n\t\t\t\t\tfor _, target := range retryTargetTexts {\n\t\t\t\t\t\tif strings.Contains(text, target) {\n\t\t\t\t\t\t\tretrySubmitBtn = el\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif retrySubmitBtn != nil {\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif retrySubmitBtn != nil {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif retrySubmitBtn != nil {\n\t\t\t\tlog.Printf(\"[注册 %d] 重新提交邮箱\", threadID)\n\t\t\t\thumanClick(page, retrySubmitBtn)\n\t\t\t\thumanDelay(800, 1200)\n\n\t\t\t\t// 重新检查页面状态\n\t\t\t\tpage.WaitLoad()\n\t\t\t\thumanDelay(500, 800)\n\t\t\t}\n\t\t} else {\n\t\t\tlog.Printf(\"[注册 %d] ⚠️ 恢复后未找到邮箱输入框\", threadID)\n\t\t}\n\t}\n\n\t// 处理验证码\n\tif needsVerification {\n\n\t\tvar emailContent *EmailContent\n\t\tmaxWaitTime := 3 * time.Minute\n\t\tstartTime := time.Now()\n\t\tresendCount := 0\n\t\tmaxResend := 3\n\t\tlastEmailCheck := time.Time{}\n\t\temailCheckInterval := 3 * time.Second\n\t\tcodePageStableTime := time.Time{} // 验证码页面稳定时间\n\n\t\tfor time.Since(startTime) < maxWaitTime {\n\t\t\t// 检查页面状态\n\t\t\tpageStatus, _ := page.Eval(`() => {\n\t\t\t\tconst pageText = document.body ? document.body.innerText : '';\n\t\t\t\t\n\t\t\t\t// 检查是否在验证码页面\n\t\t\t\tconst isCodePage = pageText.includes('6-character code') || \n\t\t\t\t\tpageText.includes('verification code') ||\n\t\t\t\t\tpageText.includes('Enter verification') ||\n\t\t\t\t\tpageText.includes('验证码') ||\n\t\t\t\t\tpageText.includes('We sent');\n\t\t\t\t\n\t\t\t\t// 检查验证码页面上的错误提示（验证码错误、发送失败等）\n\t\t\t\tconst codePageErrors = [\n\t\t\t\t\t'Wrong code', 'wrong code', '验证码错误', '代码错误',\n\t\t\t\t\t'expired', '已过期', '过期',\n\t\t\t\t\t'try again', '重试', 'Try again',\n\t\t\t\t\t'too many attempts', '尝试次数过多'\n\t\t\t\t];\n\t\t\t\tconst hasCodeError = isCodePage && codePageErrors.some(err => \n\t\t\t\t\tpageText.toLowerCase().includes(err.toLowerCase()));\n\t\t\t\t\n\t\t\t\t// 检查底部 toast/snackbar 错误提示\n\t\t\t\tconst toastSelectors = ['[role=\"alert\"]', 'aside', '[jscontroller=\"Q9PAie\"]'];\n\t\t\t\tlet toastError = null;\n\t\t\t\tfor (const sel of toastSelectors) {\n\t\t\t\t\tconst el = document.querySelector(sel);\n\t\t\t\t\tif (el && el.offsetParent !== null) {\n\t\t\t\t\t\tconst text = el.textContent || '';\n\t\t\t\t\t\tif (text.includes('went wrong') || text.includes('出了点问题') ||\n\t\t\t\t\t\t\ttext.includes('choose another') || text.includes('login method') ||\n\t\t\t\t\t\t\ttext.includes('无法发送') || text.includes('failed')) {\n\t\t\t\t\t\t\ttoastError = text;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// 检查是否是严重错误页面（不是验证码页面）\n\t\t\t\tconst fatalErrors = ['出了点问题', 'Something went wrong', 'choose another login method'];\n\t\t\t\tconst hasFatalError = !isCodePage && fatalErrors.some(err => \n\t\t\t\t\tpageText.toLowerCase().includes(err.toLowerCase()));\n\t\t\t\t\n\t\t\t\t// 检查 Try again 按钮（错误页面）\n\t\t\t\tconst tryAgainBtn = document.querySelector('.mdc-button__label');\n\t\t\t\tconst hasTryAgainBtn = tryAgainBtn && \n\t\t\t\t\t(tryAgainBtn.textContent.includes('Try again') || tryAgainBtn.textContent.includes('重试'));\n\t\t\t\t\n\t\t\t\t// 查找重发按钮（验证码页面）\n\t\t\t\tconst resendBtn = document.querySelector('span[jsname=\"V67aGc\"].YuMlnb-vQzf8d') ||\n\t\t\t\t\tdocument.querySelector('span.YuMlnb-vQzf8d') ||\n\t\t\t\t\tArray.from(document.querySelectorAll('span, button, a')).find(el => \n\t\t\t\t\t\tel.textContent && (el.textContent.includes('重新发送') || \n\t\t\t\t\t\tel.textContent.toLowerCase().includes('resend')));\n\t\t\t\t\n\t\t\t\treturn { \n\t\t\t\t\tisCodePage: isCodePage,\n\t\t\t\t\thasCodeError: hasCodeError,\n\t\t\t\t\ttoastError: toastError || '',\n\t\t\t\t\thasFatalError: hasFatalError || !!toastError,\n\t\t\t\t\thasTryAgainBtn: hasTryAgainBtn,\n\t\t\t\t\thasResendBtn: !!resendBtn,\n\t\t\t\t\tpageText: pageText.substring(0, 200)\n\t\t\t\t};\n\t\t\t}`)\n\n\t\t\tif pageStatus == nil {\n\t\t\t\ttime.Sleep(1 * time.Second)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tisCodePage := pageStatus.Value.Get(\"isCodePage\").Bool()\n\t\t\thasCodeError := pageStatus.Value.Get(\"hasCodeError\").Bool()\n\t\t\thasFatalError := pageStatus.Value.Get(\"hasFatalError\").Bool()\n\t\t\thasTryAgainBtn := pageStatus.Value.Get(\"hasTryAgainBtn\").Bool()\n\t\t\thasResendBtn := pageStatus.Value.Get(\"hasResendBtn\").Bool()\n\t\t\ttoastError := pageStatus.Value.Get(\"toastError\").String()\n\n\t\t\t// 处理严重错误（不是验证码页面的错误）\n\t\t\tif hasFatalError && !isCodePage {\n\t\t\t\tif hasTryAgainBtn {\n\t\t\t\t\tlog.Printf(\"[注册 %d] 检测到错误页面，点击 Try again\", threadID)\n\t\t\t\t\tpage.Eval(`() => {\n\t\t\t\t\t\tconst btn = document.querySelector('.mdc-button__label');\n\t\t\t\t\t\tif (btn) {\n\t\t\t\t\t\t\tconst parent = btn.closest('button');\n\t\t\t\t\t\t\tif (parent) parent.click();\n\t\t\t\t\t\t}\n\t\t\t\t\t}`)\n\t\t\t\t\ttime.Sleep(3 * time.Second)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\terrMsg := toastError\n\t\t\t\tif errMsg == \"\" {\n\t\t\t\t\terrMsg = pageStatus.Value.Get(\"pageText\").String()\n\t\t\t\t}\n\t\t\t\tif len(errMsg) > 80 {\n\t\t\t\t\terrMsg = errMsg[:80]\n\t\t\t\t}\n\t\t\t\tresult.Error = fmt.Errorf(\"验证码发送失败: %s\", errMsg)\n\t\t\t\tlog.Printf(\"[注册 %d] ❌ %v\", threadID, result.Error)\n\t\t\t\treturn result\n\t\t\t}\n\n\t\t\t// 在验证码页面\n\t\t\tif isCodePage {\n\t\t\t\t// 首次进入验证码页面，记录时间\n\t\t\t\tif codePageStableTime.IsZero() {\n\t\t\t\t\tcodePageStableTime = time.Now()\n\t\t\t\t}\n\t\t\t\tpageStableDuration := time.Since(codePageStableTime)\n\t\t\t\tif hasCodeError && hasResendBtn && resendCount < maxResend && pageStableDuration > 5*time.Second {\n\t\t\t\t\tlog.Printf(\"[注册 %d] 验证码页面出现错误，点击重发 (%d/%d)\", threadID, resendCount+1, maxResend)\n\t\t\t\t\tpage.Eval(`() => {\n\t\t\t\t\t\tconst btn = document.querySelector('span[jsname=\"V67aGc\"].YuMlnb-vQzf8d') ||\n\t\t\t\t\t\t\tdocument.querySelector('span.YuMlnb-vQzf8d') ||\n\t\t\t\t\t\t\tArray.from(document.querySelectorAll('span, button, a')).find(el => \n\t\t\t\t\t\t\t\tel.textContent && (el.textContent.includes('重新发送') || \n\t\t\t\t\t\t\t\tel.textContent.toLowerCase().includes('resend')));\n\t\t\t\t\t\tif (btn) {\n\t\t\t\t\t\t\tbtn.click();\n\t\t\t\t\t\t\tif (btn.parentElement) btn.parentElement.click();\n\t\t\t\t\t\t}\n\t\t\t\t\t}`)\n\t\t\t\t\tresendCount++\n\t\t\t\t\ttime.Sleep(3 * time.Second)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif time.Since(lastEmailCheck) >= emailCheckInterval {\n\t\t\t\t\temailContent, _ = getVerificationEmailQuick(email, 1, 2)\n\t\t\t\t\tlastEmailCheck = time.Now()\n\t\t\t\t\tif emailContent != nil {\n\t\t\t\t\t\tlog.Printf(\"[注册 %d] ✅ 获取到验证码邮件\", threadID)\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ttime.Sleep(1 * time.Second)\n\t\t}\n\n\t\tif emailContent == nil {\n\t\t\tresult.Error = fmt.Errorf(\"无法获取验证码邮件\")\n\t\t\treturn result\n\t\t}\n\n\t\t// 提取验证码\n\t\tcode, err := extractVerificationCode(emailContent.Content)\n\t\tif err != nil {\n\t\t\tresult.Error = err\n\t\t\treturn result\n\t\t}\n\n\t\t// 等待验证码输入框\n\t\ttime.Sleep(500 * time.Millisecond)\n\t\tlog.Printf(\"[注册 %d] 准备输入验证码: %s\", threadID, code)\n\n\t\t// 检查是否是OTP风格的多个输入框\n\t\tinputInfo, _ := page.Eval(`() => {\n\t\t\t// 检查标准input\n\t\t\tconst inputs = document.querySelectorAll('input:not([type=\"hidden\"])');\n\t\t\tconst visibleInputs = Array.from(inputs).filter(i => i.offsetParent !== null);\n\t\t\t\n\t\t\t// 检查Google风格的OTP框（可能是div实现）\n\t\t\tconst otpContainers = document.querySelectorAll('[data-otp-input], [class*=\"otp\"], [class*=\"code-input\"], [class*=\"verification\"]');\n\t\t\t\n\t\t\t// 检查页面是否包含验证码相关文本\n\t\t\tconst pageText = document.body ? document.body.innerText : '';\n\t\t\tconst isVerifyPage = pageText.includes('验证码') || pageText.includes('verification') || \n\t\t\t\tpageText.includes('verify') || window.location.href.includes('verify');\n\t\t\tconst isOTP = (visibleInputs.length >= 4 && visibleInputs.length <= 8) || \n\t\t\t\t(isVerifyPage && visibleInputs.length <= 2);\n\t\t\t\n\t\t\treturn { \n\t\t\t\tcount: visibleInputs.length,\n\t\t\t\tisOTP: isOTP,\n\t\t\t\tisVerifyPage: isVerifyPage,\n\t\t\t\turl: window.location.href\n\t\t\t};\n\t\t}`)\n\n\t\tisOTP := false\n\t\tif inputInfo != nil {\n\t\t\tisOTP = inputInfo.Value.Get(\"isOTP\").Bool()\n\t\t\tlog.Printf(\"[注册 %d] 验证码输入框: count=%d, isOTP=%v\", threadID,\n\t\t\t\tinputInfo.Value.Get(\"count\").Int(), isOTP)\n\t\t}\n\n\t\t// 使用 rod Element API 查找验证码输入框\n\t\tcodeInputs, _ := page.Elements(\"input:not([type='hidden'])\")\n\t\tvar firstCodeInput *rod.Element\n\t\tfor _, el := range codeInputs {\n\t\t\tvisible, _ := el.Visible()\n\t\t\tif visible {\n\t\t\t\tfirstCodeInput = el\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif firstCodeInput == nil {\n\t\t\tlog.Printf(\"[注册 %d] ⚠️ 未找到验证码输入框\", threadID)\n\t\t} else {\n\t\t\t// 使用拟人化点击验证码框\n\t\t\tfunc() {\n\t\t\t\tdefer func() {\n\t\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\t\tlog.Printf(\"[注册 %d] 点击验证码框异常: %v\", threadID, r)\n\t\t\t\t\t}\n\t\t\t\t}()\n\t\t\t\thumanClick(page, firstCodeInput)\n\t\t\t}()\n\t\t\thumanDelay(200, 400)\n\n\t\t\t// 清空输入框（带超时保护）\n\t\t\tfunc() {\n\t\t\t\tdefer func() {\n\t\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\t\tlog.Printf(\"[注册 %d] 清空验证码框异常: %v\", threadID, r)\n\t\t\t\t\t}\n\t\t\t\t}()\n\t\t\t\tfirstCodeInput.SelectAllText()\n\t\t\t\tfirstCodeInput.Input(\"\")\n\t\t\t}()\n\t\t\thumanDelay(150, 300)\n\n\t\t\t// 使用拟人化打字输入验证码\n\t\t\tlog.Printf(\"[注册 %d] ⌨️ 开始拟人化输入验证码...\", threadID)\n\t\t\thumanType(page, code)\n\t\t\tlog.Printf(\"[注册 %d] 验证码输入完成\", threadID)\n\t\t}\n\n\t\thumanDelay(400, 700)\n\n\t\tfor i := 0; i < 5; i++ {\n\t\t\tclickResult, _ := page.Eval(`() => {\n\t\t\t\tconst targets = ['验证', 'Verify', '继续', 'Next', 'Continue'];\n\t\t\t\tconst elements = [\n\t\t\t\t\t...document.querySelectorAll('button'),\n\t\t\t\t\t...document.querySelectorAll('input[type=\"submit\"]'),\n\t\t\t\t\t...document.querySelectorAll('div[role=\"button\"]')\n\t\t\t\t];\n\n\t\t\t\tfor (const element of elements) {\n\t\t\t\t\tif (!element) continue;\n\t\t\t\t\tconst style = window.getComputedStyle(element);\n\t\t\t\t\tif (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') continue;\n\t\t\t\t\tif (element.disabled) continue;\n\n\t\t\t\t\tconst text = element.textContent ? element.textContent.trim() : '';\n\t\t\t\t\tif (targets.some(t => text.includes(t))) {\n\t\t\t\t\t\telement.click();\n\t\t\t\t\t\treturn { clicked: true, text: text };\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn { clicked: false };\n\t\t\t}`)\n\n\t\t\tif clickResult != nil && clickResult.Value.Get(\"clicked\").Bool() {\n\t\t\t\thumanDelay(300, 600)\n\t\t\t\tbreak\n\t\t\t}\n\t\t\thumanDelay(800, 1200)\n\t\t}\n\n\t\thumanDelay(1500, 2500)\n\t}\n\n\t// 填写姓名\n\tfullName := generateRandomName()\n\tresult.FullName = fullName\n\tlog.Printf(\"[注册 %d] 准备输入姓名: %s\", threadID, fullName)\n\n\thumanDelay(400, 700)\n\n\t// 查找姓名输入框并使用 rod 原生方式输入\n\tnameSelectors := []string{\n\t\t`input[name=\"fullName\"]`,\n\t\t`input[autocomplete=\"name\"]`,\n\t\t`input[type=\"text\"]:not([type=\"hidden\"]):not([type=\"email\"])`,\n\t}\n\n\tvar nameInput *rod.Element\n\tfor _, sel := range nameSelectors {\n\t\tnameInput, _ = page.Timeout(2 * time.Second).Element(sel)\n\t\tif nameInput != nil {\n\t\t\tvisible, _ := nameInput.Visible()\n\t\t\tif visible {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tnameInput = nil\n\t\t}\n\t}\n\n\t// 兜底：获取第一个可见的文本输入框\n\tif nameInput == nil {\n\t\tinputs, _ := page.Elements(`input:not([type=\"hidden\"]):not([type=\"submit\"]):not([type=\"email\"])`)\n\t\tfor _, inp := range inputs {\n\t\t\tif visible, _ := inp.Visible(); visible {\n\t\t\t\tnameInput = inp\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\tif nameInput != nil {\n\t\t// 拟人化聚焦并清空\n\t\tlog.Printf(\"[注册 %d] 📍 拟人化聚焦姓名输入框...\", threadID)\n\t\thumanClick(page, nameInput)\n\t\thumanDelay(100, 200)\n\t\tnameInput.SelectAllText()\n\t\thumanDelay(50, 100)\n\t\tpage.Keyboard.Type(input.Backspace)\n\t\thumanDelay(80, 150)\n\n\t\t// 拟人化输入姓名\n\t\tlog.Printf(\"[注册 %d] ⌨️ 开始拟人化输入姓名: %s\", threadID, fullName)\n\t\thumanType(page, fullName)\n\t\tlog.Printf(\"[注册 %d] 姓名输入完成: %s\", threadID, fullName)\n\t} else {\n\t\tlog.Printf(\"[注册 %d] ⚠️ 未找到姓名输入框，尝试直接键盘输入\", threadID)\n\t\t// 使用拟人化打字作为备用\n\t\thumanType(page, fullName)\n\t}\n\thumanDelay(400, 700)\n\n\t// 确认提交姓名\n\tconfirmSubmitted := false\n\tfor i := 0; i < 5; i++ {\n\t\tclickResult, _ := page.Eval(`() => {\n\t\t\tconst targets = ['同意', 'Confirm', '继续', 'Next', 'Continue', 'I agree'];\n\t\t\tconst elements = [\n\t\t\t\t...document.querySelectorAll('button'),\n\t\t\t\t...document.querySelectorAll('input[type=\"submit\"]'),\n\t\t\t\t...document.querySelectorAll('div[role=\"button\"]')\n\t\t\t];\n\n\t\t\tfor (const element of elements) {\n\t\t\t\tif (!element) continue;\n\t\t\t\tconst style = window.getComputedStyle(element);\n\t\t\t\tif (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') continue;\n\t\t\t\tif (element.disabled) continue;\n\n\t\t\t\tconst text = element.textContent ? element.textContent.trim() : '';\n\t\t\t\tif (targets.some(t => text.includes(t))) {\n\t\t\t\t\telement.click();\n\t\t\t\t\treturn { clicked: true, text: text };\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// 备用：点击第一个可见按钮\n\t\t\tfor (const element of elements) {\n\t\t\t\tif (element && element.offsetParent !== null && !element.disabled) {\n\t\t\t\t\telement.click();\n\t\t\t\t\treturn { clicked: true, text: 'fallback' };\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn { clicked: false };\n\t\t}`)\n\n\t\tif clickResult != nil && clickResult.Value.Get(\"clicked\").Bool() {\n\t\t\tconfirmSubmitted = true\n\t\t\tbreak\n\t\t}\n\t\ttime.Sleep(1000 * time.Millisecond)\n\t}\n\n\tif !confirmSubmitted {\n\t\tlog.Printf(\"[注册 %d] ⚠️ 未能点击确认按钮，尝试继续\", threadID)\n\t}\n\n\ttime.Sleep(3 * time.Second)\n\n\t// 等待页面稳定\n\tpage.WaitLoad()\n\ttime.Sleep(2 * time.Second)\n\n\t// 处理额外步骤（主要是复选框）\n\thandleAdditionalSteps(page, threadID)\n\n\t// 检查并处理管理创建页面\n\tcheckAndHandleAdminPage(page, threadID)\n\n\t// 等待更多可能的跳转\n\ttime.Sleep(3 * time.Second)\n\n\t// 尝试多次点击可能出现的额外按钮\n\tfor i := 0; i < 15; i++ {\n\t\ttime.Sleep(2 * time.Second)\n\n\t\t// 尝试点击可能出现的额外按钮\n\t\tpage.Eval(`() => {\n\t\t\tconst buttons = document.querySelectorAll('button');\n\t\t\tfor (const button of buttons) {\n\t\t\t\tif (!button) continue;\n\t\t\t\tconst text = button.textContent || '';\n\t\t\t\tif (text.includes('同意') || text.includes('Confirm') || text.includes('继续') || \n\t\t\t\t\ttext.includes('Next') || text.includes('I agree')) {\n\t\t\t\t\tif (button.offsetParent !== null && !button.disabled) {\n\t\t\t\t\t\tbutton.click();\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false;\n\t\t}`)\n\n\t\t// 从 URL 提取信息\n\t\tinfo, _ := page.Info()\n\t\tif info != nil {\n\t\t\tcurrentURL := info.URL\n\t\t\tif m := regexp.MustCompile(`/cid/([a-f0-9-]+)`).FindStringSubmatch(currentURL); len(m) > 1 && configID == \"\" {\n\t\t\t\tconfigID = m[1]\n\t\t\t\tlog.Printf(\"[注册 %d] 从URL提取 configId: %s\", threadID, configID)\n\t\t\t}\n\t\t\tif m := regexp.MustCompile(`[?&]csesidx=(\\d+)`).FindStringSubmatch(currentURL); len(m) > 1 && csesidx == \"\" {\n\t\t\t\tcsesidx = m[1]\n\t\t\t\tlog.Printf(\"[注册 %d] 从URL提取 csesidx: %s\", threadID, csesidx)\n\t\t\t}\n\t\t}\n\n\t\tif authorization != \"\" {\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// 增强的 Authorization 获取逻辑\n\tif authorization == \"\" {\n\t\tlog.Printf(\"[注册 %d] 仍未获取到 Authorization，尝试更多方法...\", threadID)\n\n\t\t// 尝试刷新页面\n\t\tpage.Reload()\n\t\tpage.WaitLoad()\n\t\ttime.Sleep(3 * time.Second)\n\n\t\t// 尝试从 localStorage 获取\n\t\tlocalStorageAuth, _ := page.Eval(`() => {\n\t\t\treturn localStorage.getItem('Authorization') || \n\t\t\t\t   localStorage.getItem('authorization') ||\n\t\t\t\t   localStorage.getItem('auth_token') ||\n\t\t\t\t   localStorage.getItem('token');\n\t\t}`)\n\n\t\tif localStorageAuth != nil && localStorageAuth.Value.String() != \"\" {\n\t\t\tauthorization = localStorageAuth.Value.String()\n\t\t\tlog.Printf(\"[注册 %d] 从 localStorage 获取 Authorization\", threadID)\n\t\t}\n\n\t\t// 从页面源代码中提取\n\t\tpageContent, _ := page.Eval(`() => document.body ? document.body.innerHTML : ''`)\n\t\tif pageContent != nil && pageContent.Value.String() != \"\" {\n\t\t\tcontent := pageContent.Value.String()\n\t\t\tre := regexp.MustCompile(`\"authorization\"\\s*:\\s*\"([^\"]+)\"`)\n\t\t\tif matches := re.FindStringSubmatch(content); len(matches) > 1 {\n\t\t\t\tauthorization = matches[1]\n\t\t\t\tlog.Printf(\"[注册 %d] 从页面内容提取 Authorization\", threadID)\n\t\t\t}\n\t\t}\n\n\t\t// 从当前 URL 中提取\n\t\tinfo, _ := page.Info()\n\t\tif info != nil {\n\t\t\tcurrentURL := info.URL\n\t\t\tre := regexp.MustCompile(`[?&](?:token|auth)=([^&]+)`)\n\t\t\tif matches := re.FindStringSubmatch(currentURL); len(matches) > 1 {\n\t\t\t\tauthorization = matches[1]\n\t\t\t\tlog.Printf(\"[注册 %d] 从 URL 提取 Authorization\", threadID)\n\t\t\t}\n\t\t}\n\t}\n\n\tif authorization == \"\" {\n\t\tresult.Error = fmt.Errorf(\"未能获取 Authorization\")\n\t\treturn result\n\t}\n\tvar resultCookies []pool.Cookie\n\tcookieMap := make(map[string]bool)\n\n\t// 获取当前页面所有 cookie\n\tcookies, _ := page.Cookies(nil)\n\tfor _, c := range cookies {\n\t\tkey := c.Name + \"|\" + c.Domain\n\t\tif !cookieMap[key] {\n\t\t\tcookieMap[key] = true\n\t\t\tresultCookies = append(resultCookies, pool.Cookie{\n\t\t\t\tName:   c.Name,\n\t\t\t\tValue:  c.Value,\n\t\t\t\tDomain: c.Domain,\n\t\t\t})\n\t\t}\n\t}\n\n\t// 尝试从特定域名获取更多 cookie\n\tdomains := []string{\n\t\t\"https://business.gemini.google\",\n\t\t\"https://gemini.google\",\n\t\t\"https://accounts.google.com\",\n\t}\n\tfor _, domain := range domains {\n\t\tdomainCookies, err := page.Cookies([]string{domain})\n\t\tif err == nil {\n\t\t\tfor _, c := range domainCookies {\n\t\t\t\tkey := c.Name + \"|\" + c.Domain\n\t\t\t\tif !cookieMap[key] {\n\t\t\t\t\tcookieMap[key] = true\n\t\t\t\t\tresultCookies = append(resultCookies, pool.Cookie{\n\t\t\t\t\t\tName:   c.Name,\n\t\t\t\t\t\tValue:  c.Value,\n\t\t\t\t\t\tDomain: c.Domain,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tlog.Printf(\"[注册 %d] 获取到 %d 个 Cookie\", threadID, len(resultCookies))\n\n\t// 如果 csesidx 为空，尝试从 authorization 中提取\n\tif csesidx == \"\" && authorization != \"\" {\n\t\tcsesidx = extractCSESIDXFromAuth(authorization)\n\t\tif csesidx != \"\" {\n\t\t\tlog.Printf(\"[注册 %d] 从 authorization 提取 csesidx: %s\", threadID, csesidx)\n\t\t}\n\t}\n\n\t// 如果仍为空，尝试访问主页获取\n\tif csesidx == \"\" {\n\t\tlog.Printf(\"[注册 %d] ⚠️ csesidx 为空，尝试访问主页获取...\", threadID)\n\t\tpage.Navigate(\"https://business.gemini.google/\")\n\t\ttime.Sleep(3 * time.Second)\n\t\tinfo, _ := page.Info()\n\t\tif info != nil {\n\t\t\tif m := regexp.MustCompile(`[?&]csesidx=(\\d+)`).FindStringSubmatch(info.URL); len(m) > 1 {\n\t\t\t\tcsesidx = m[1]\n\t\t\t\tlog.Printf(\"[注册 %d] 从主页URL提取 csesidx: %s\", threadID, csesidx)\n\t\t\t}\n\t\t}\n\t}\n\n\t// 如果 csesidx 为空，尝试从 authorization 提取\n\tif csesidx == \"\" && authorization != \"\" {\n\t\tcsesidx = extractCSESIDXFromAuth(authorization)\n\t}\n\n\t// csesidx 是必须的，没有则注册失败\n\tif csesidx == \"\" {\n\t\tresult.Error = fmt.Errorf(\"未能获取 csesidx\")\n\t\treturn result\n\t}\n\n\tresult.Success = true\n\tresult.Authorization = authorization\n\tresult.Cookies = resultCookies\n\tresult.ConfigID = configID\n\tresult.CSESIDX = csesidx\n\n\tlog.Printf(\"[注册 %d] ✅ 注册成功: %s\", threadID, email)\n\treturn result\n}\n\n// SaveBrowserRegisterResult 保存注册结果\nfunc SaveBrowserRegisterResult(result *BrowserRegisterResult, dataDir string) error {\n\tif !result.Success {\n\t\treturn result.Error\n\t}\n\n\tdata := pool.AccountData{\n\t\tEmail:         result.Email,\n\t\tFullName:      result.FullName,\n\t\tAuthorization: result.Authorization,\n\t\tCookies:       result.Cookies,\n\t\tConfigID:      result.ConfigID,\n\t\tCSESIDX:       result.CSESIDX,\n\t\tTimestamp:     time.Now().Format(time.RFC3339),\n\t}\n\n\tjsonData, err := json.MarshalIndent(data, \"\", \"  \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"序列化失败: %w\", err)\n\t}\n\n\tfilename := filepath.Join(dataDir, fmt.Sprintf(\"%s.json\", result.Email))\n\tif err := os.WriteFile(filename, jsonData, 0644); err != nil {\n\t\treturn fmt.Errorf(\"写入文件失败: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// BrowserRefreshResult Cookie刷新结果\ntype BrowserRefreshResult struct {\n\tSuccess         bool\n\tSecureCookies   []pool.Cookie\n\tConfigID        string\n\tCSESIDX         string\n\tAuthorization   string\n\tResponseHeaders map[string]string // 捕获的响应头\n\tNewCookies      []pool.Cookie     // 从响应头提取的新Cookie\n\tError           error\n}\n\nfunc RefreshCookieWithBrowser(acc *pool.Account, headless bool, proxy string) *BrowserRefreshResult {\n\tresult := &BrowserRefreshResult{}\n\temail := acc.Data.Email\n\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\tresult.Error = fmt.Errorf(\"panic: %v\", r)\n\t\t}\n\t}()\n\n\t// 使用公共函数创建浏览器会话\n\tsession, err := createBrowserSession(headless, proxy, \"[Cookie刷新]\")\n\tif err != nil {\n\t\tresult.Error = err\n\t\treturn result\n\t}\n\tdefer session.Close()\n\tpage := session.Page\n\n\tvar authorization string\n\tvar configID, csesidx string\n\tvar responseHeadersMu sync.Mutex\n\tresponseHeaders := make(map[string]string)\n\tvar newCookiesFromResponse []pool.Cookie\n\tgo page.EachEvent(func(e *proto.NetworkResponseReceived) {\n\t\tresponseHeadersMu.Lock()\n\t\tdefer responseHeadersMu.Unlock()\n\t\theaders := e.Response.Headers\n\t\timportantKeys := []string{\"set-cookie\", \"Set-Cookie\", \"authorization\", \"Authorization\",\n\t\t\t\"x-goog-authenticated-user\", \"X-Goog-Authenticated-User\"}\n\n\t\tfor _, key := range importantKeys {\n\t\t\tif val, ok := headers[key]; ok {\n\t\t\t\tstr := val.Str()\n\t\t\t\tif str == \"\" {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tresponseHeaders[key] = str\n\t\t\t\t// 解析 Set-Cookie\n\t\t\t\tif strings.ToLower(key) == \"set-cookie\" {\n\t\t\t\t\tparts := strings.Split(str, \";\")\n\t\t\t\t\tif len(parts) > 0 {\n\t\t\t\t\t\tnv := strings.SplitN(parts[0], \"=\", 2)\n\t\t\t\t\t\tif len(nv) == 2 {\n\t\t\t\t\t\t\tnewCookiesFromResponse = append(newCookiesFromResponse, pool.Cookie{\n\t\t\t\t\t\t\t\tName:   strings.TrimSpace(nv[0]),\n\t\t\t\t\t\t\t\tValue:  strings.TrimSpace(nv[1]),\n\t\t\t\t\t\t\t\tDomain: \".gemini.google\",\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t})()\n\n\tgo page.EachEvent(func(e *proto.NetworkRequestWillBeSent) {\n\t\tif auth, ok := e.Request.Headers[\"authorization\"]; ok {\n\t\t\tif authStr := auth.String(); authStr != \"\" {\n\t\t\t\tauthorization = authStr\n\t\t\t}\n\t\t}\n\t\treqURL := e.Request.URL\n\t\tif m := regexp.MustCompile(`/cid/([a-f0-9-]+)`).FindStringSubmatch(reqURL); len(m) > 1 && configID == \"\" {\n\t\t\tconfigID = m[1]\n\t\t}\n\t\tif m := regexp.MustCompile(`[?&]csesidx=(\\d+)`).FindStringSubmatch(reqURL); len(m) > 1 && csesidx == \"\" {\n\t\t\tcsesidx = m[1]\n\t\t}\n\t})()\n\n\t// 导航到目标页面\n\ttargetURL := \"https://business.gemini.google/\"\n\tpage.Navigate(targetURL)\n\tpage.WaitLoad()\n\ttime.Sleep(2 * time.Second)\n\n\t// 检查页面状态\n\tinfo, _ := page.Info()\n\tvar currentURL string\n\tif info != nil {\n\t\tcurrentURL = info.URL\n\t}\n\t_ = currentURL // 后续 extractResult 中使用\n\tinitialEmailCount := 0\n\tmaxCodeRetries := 3 // 验证码重试次数（必须在goto之前声明）\n\n\t// 检查是否已经登录成功（有authorization）\n\tif authorization != \"\" {\n\t\tlog.Printf(\"[Cookie刷新] [%s] Cookie有效，已自动登录\", email)\n\t\tgoto extractResult\n\t}\n\n\t// 获取实际邮件数量\n\tinitialEmailCount = getEmailCount(email)\n\n\t// 检查是否在登录页面需要输入邮箱\n\tif _, err := page.Timeout(5 * time.Second).Element(\"input\"); err == nil {\n\t\tlog.Printf(\"[Cookie刷新] [%s] 🔍 查找邮箱输入框...\", email)\n\n\t\t// 使用精确选择器查找输入框\n\t\tvar emailInput *rod.Element\n\t\tselectors := []string{\n\t\t\t\"#email-input\",\n\t\t\t\"input[name='loginHint']\",\n\t\t\t\"input[jsname='YPqjbf']\",\n\t\t\t\"input[type='email']\",\n\t\t\t\"input[type='text'][aria-label]\",\n\t\t\t\"input:not([type='hidden']):not([type='submit']):not([type='checkbox'])\",\n\t\t}\n\t\tfor _, sel := range selectors {\n\t\t\tel, err := page.Timeout(2 * time.Second).Element(sel)\n\t\t\tif err == nil && el != nil {\n\t\t\t\tvisible, _ := el.Visible()\n\t\t\t\tif visible {\n\t\t\t\t\temailInput = el\n\t\t\t\t\tlog.Printf(\"[Cookie刷新] [%s] ✅ 找到输入框: %s\", email, sel)\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif emailInput != nil {\n\t\t\t// 点击获取焦点\n\t\t\temailInput.MustScrollIntoView()\n\t\t\temailInput.MustClick()\n\t\t\ttime.Sleep(300 * time.Millisecond)\n\n\t\t\t// 清空输入框（仅当有内容时）\n\t\t\tcurrentVal, _ := emailInput.Property(\"value\")\n\t\t\tif currentVal.String() != \"\" {\n\t\t\t\temailInput.SelectAllText()\n\t\t\t\ttime.Sleep(100 * time.Millisecond)\n\t\t\t\tpage.Keyboard.Type(input.Backspace)\n\t\t\t\ttime.Sleep(100 * time.Millisecond)\n\t\t\t}\n\n\t\t\tlog.Printf(\"[Cookie刷新] [%s] ⌨️ 开始键盘输入邮箱...\", email)\n\t\t\tfor _, char := range email {\n\t\t\t\tpage.Keyboard.Type(input.Key(char))\n\t\t\t\ttime.Sleep(time.Duration(50+rand.Intn(80)) * time.Millisecond)\n\t\t\t}\n\n\t\t\t// 验证输入\n\t\t\tpropVal, _ := emailInput.Property(\"value\")\n\t\t\tlog.Printf(\"[Cookie刷新] [%s] 📋 输入值: %s\", email, propVal.String())\n\t\t} else {\n\t\t\tlog.Printf(\"[Cookie刷新] [%s] ⚠️ 未找到输入框，尝试旧方式\", email)\n\t\t\tpage.Eval(`() => {\n\t\t\t\tconst inputs = document.querySelectorAll('input');\n\t\t\t\tif (inputs.length > 0) {\n\t\t\t\t\tinputs[0].value = '';\n\t\t\t\t\tinputs[0].click();\n\t\t\t\t\tinputs[0].focus();\n\t\t\t\t}\n\t\t\t}`)\n\t\t\ttime.Sleep(300 * time.Millisecond)\n\t\t\tsafeType(page, email, 30)\n\t\t}\n\n\t\ttime.Sleep(500 * time.Millisecond)\n\t\tpage.Eval(`() => {\n\t\t\tconst inputs = document.querySelectorAll('input');\n\t\t\tif (inputs.length > 0) { inputs[0].blur(); }\n\t\t}`)\n\t\ttime.Sleep(500 * time.Millisecond)\n\n\t\t// 点击继续按钮\n\t\tfor i := 0; i < 5; i++ {\n\t\t\tclickResult, _ := page.Eval(`() => {\n\t\t\t\tconst targets = ['继续', 'Next', 'Continue', '邮箱'];\n\t\t\t\tconst elements = [...document.querySelectorAll('button'), ...document.querySelectorAll('div[role=\"button\"]')];\n\t\t\t\tfor (const el of elements) {\n\t\t\t\t\tif (!el || el.disabled) continue;\n\t\t\t\t\tconst style = window.getComputedStyle(el);\n\t\t\t\t\tif (style.display === 'none' || style.visibility === 'hidden') continue;\n\t\t\t\t\tconst text = el.textContent ? el.textContent.trim() : '';\n\t\t\t\t\tif (targets.some(t => text.includes(t))) { el.click(); return {clicked:true}; }\n\t\t\t\t}\n\t\t\t\treturn {clicked:false};\n\t\t\t}`)\n\t\t\tif clickResult != nil && clickResult.Value.Get(\"clicked\").Bool() {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\ttime.Sleep(1 * time.Second)\n\t\t}\n\t\ttime.Sleep(2 * time.Second)\n\t}\n\ttime.Sleep(3 * time.Second)\n\n\t// 验证码重试循环\n\tfor codeRetry := 0; codeRetry < maxCodeRetries; codeRetry++ {\n\t\tif codeRetry > 0 {\n\t\t\tlog.Printf(\"[Cookie刷新] [%s] 验证码验证失败，重试 %d/%d\", email, codeRetry+1, maxCodeRetries)\n\t\t\t// 点击\"重新发送验证码\"按钮\n\t\t\tpage.Eval(`() => {\n\t\t\t\tconst links = document.querySelectorAll('a, span, button');\n\t\t\t\tfor (const el of links) {\n\t\t\t\t\tconst text = el.textContent || '';\n\t\t\t\t\tif (text.includes('重新发送') || text.includes('Resend')) {\n\t\t\t\t\t\tel.click();\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn false;\n\t\t\t}`)\n\t\t\ttime.Sleep(2 * time.Second)\n\t\t\t// 更新邮件计数基准\n\t\t\tinitialEmailCount = getEmailCount(email)\n\t\t}\n\n\t\tvar emailContent *EmailContent\n\t\tmaxWaitTime := 3 * time.Minute\n\t\tstartTime := time.Now()\n\n\t\tfor time.Since(startTime) < maxWaitTime {\n\t\t\t// 快速检查新邮件（只接受数量增加的情况）\n\t\t\temailContent, _ = getVerificationEmailAfter(email, 1, 1, initialEmailCount)\n\t\t\tif emailContent != nil {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\ttime.Sleep(2 * time.Second)\n\t\t}\n\n\t\tif emailContent == nil {\n\t\t\tresult.Error = fmt.Errorf(\"无法获取验证码邮件\")\n\t\t\treturn result\n\t\t}\n\n\t\t// 提取验证码\n\t\tcode, err := extractVerificationCode(emailContent.Content)\n\t\tif err != nil {\n\t\t\tcontinue // 重试\n\t\t}\n\n\t\t// 输入验证码 - OTP 风格使用键盘逐字符输入\n\t\tlog.Printf(\"[Cookie刷新] [%s] ⌨️ 开始输入验证码: %s\", email, code)\n\t\ttime.Sleep(500 * time.Millisecond)\n\n\t\t// 查找第一个可见输入框并点击获取焦点\n\t\tcodeInputs, _ := page.Elements(\"input:not([type='hidden'])\")\n\t\tvar firstCodeInput *rod.Element\n\t\tfor _, el := range codeInputs {\n\t\t\tvisible, _ := el.Visible()\n\t\t\tif visible {\n\t\t\t\tfirstCodeInput = el\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif firstCodeInput != nil {\n\t\t\t// 清空所有输入框\n\t\t\tpage.Eval(`() => {\n\t\t\t\tconst inputs = document.querySelectorAll('input');\n\t\t\t\tfor (const inp of inputs) { inp.value = ''; }\n\t\t\t}`)\n\t\t\ttime.Sleep(200 * time.Millisecond)\n\n\t\t\t// 点击第一个输入框获取焦点\n\t\t\tfirstCodeInput.MustClick()\n\t\t\ttime.Sleep(300 * time.Millisecond)\n\n\t\t\t// 逐字符键盘输入（OTP 会自动跳转到下一个框）\n\t\t\tfor i, char := range code {\n\t\t\t\tpage.Keyboard.Type(input.Key(char))\n\t\t\t\tif i < len(code)-1 {\n\t\t\t\t\ttime.Sleep(time.Duration(100+rand.Intn(100)) * time.Millisecond)\n\t\t\t\t}\n\t\t\t}\n\t\t\tlog.Printf(\"[Cookie刷新] [%s] ✅ 验证码输入完成\", email)\n\t\t} else {\n\t\t\tlog.Printf(\"[Cookie刷新] [%s] ⚠️ 未找到验证码输入框\", email)\n\t\t}\n\t\ttime.Sleep(500 * time.Millisecond)\n\n\t\t// 点击验证按钮\n\t\tfor i := 0; i < 5; i++ {\n\t\t\tclickResult, _ := page.Eval(`() => {\n\t\t\t\tconst targets = ['验证', 'Verify', '继续', 'Next', 'Continue'];\n\t\t\t\tconst els = [...document.querySelectorAll('button'), ...document.querySelectorAll('div[role=\"button\"]')];\n\t\t\t\tfor (const el of els) {\n\t\t\t\t\tif (!el || el.disabled) continue;\n\t\t\t\t\tconst style = window.getComputedStyle(el);\n\t\t\t\t\tif (style.display === 'none' || style.visibility === 'hidden') continue;\n\t\t\t\t\tconst text = el.textContent ? el.textContent.trim() : '';\n\t\t\t\t\tif (targets.some(t => text.includes(t))) { el.click(); return {clicked:true}; }\n\t\t\t\t}\n\t\t\t\treturn {clicked:false};\n\t\t\t}`)\n\t\t\tif clickResult != nil && clickResult.Value.Get(\"clicked\").Bool() {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\ttime.Sleep(1 * time.Second)\n\t\t}\n\t\ttime.Sleep(2 * time.Second)\n\n\t\t// 检测验证码错误\n\t\thasError, _ := page.Eval(`() => {\n\t\t\tconst text = document.body.innerText || '';\n\t\t\treturn text.includes('验证码有误') || text.includes('incorrect') || text.includes('wrong code') || text.includes('请重试');\n\t\t}`)\n\t\tif hasError != nil && hasError.Value.Bool() {\n\t\t\tcontinue // 重试\n\t\t}\n\n\t\t// 验证成功，跳出重试循环\n\t\tbreak\n\t}\n\tfor i := 0; i < 15; i++ {\n\t\ttime.Sleep(2 * time.Second)\n\n\t\t// 点击可能出现的确认按钮\n\t\tpage.Eval(`() => {\n\t\t\tconst btns = document.querySelectorAll('button');\n\t\t\tfor (const btn of btns) {\n\t\t\t\tconst text = btn.textContent || '';\n\t\t\t\tif ((text.includes('同意') || text.includes('Confirm') || text.includes('继续') || text.includes('I agree')) && btn.offsetParent !== null && !btn.disabled) {\n\t\t\t\t\tbtn.click(); return true;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false;\n\t\t}`)\n\n\t\t// 从URL提取信息\n\t\tinfo, _ := page.Info()\n\t\tif info != nil {\n\t\t\tif m := regexp.MustCompile(`/cid/([a-f0-9-]+)`).FindStringSubmatch(info.URL); len(m) > 1 && configID == \"\" {\n\t\t\t\tconfigID = m[1]\n\t\t\t}\n\t\t\tif m := regexp.MustCompile(`[?&]csesidx=(\\d+)`).FindStringSubmatch(info.URL); len(m) > 1 && csesidx == \"\" {\n\t\t\t\tcsesidx = m[1]\n\t\t\t}\n\t\t}\n\n\t\tif authorization != \"\" {\n\t\t\tbreak\n\t\t}\n\t}\n\nextractResult:\n\tif authorization == \"\" {\n\t\tresult.Error = fmt.Errorf(\"未能获取 Authorization\")\n\t\treturn result\n\t}\n\tcookies, _ := page.Cookies(nil)\n\tcookieMap := make(map[string]pool.Cookie)\n\tfor _, c := range acc.Data.GetAllCookies() {\n\t\tcookieMap[c.Name] = c\n\t}\n\n\tfor _, c := range cookies {\n\t\tcookieMap[c.Name] = pool.Cookie{\n\t\t\tName:   c.Name,\n\t\t\tValue:  c.Value,\n\t\t\tDomain: c.Domain,\n\t\t}\n\t}\n\tresponseHeadersMu.Lock()\n\tfor _, c := range newCookiesFromResponse {\n\t\tcookieMap[c.Name] = c\n\t}\n\t// 复制响应头\n\tresult.ResponseHeaders = make(map[string]string)\n\tfor k, v := range responseHeaders {\n\t\tresult.ResponseHeaders[k] = v\n\t}\n\tresult.NewCookies = newCookiesFromResponse\n\tresponseHeadersMu.Unlock()\n\tvar resultCookies []pool.Cookie\n\tfor _, c := range cookieMap {\n\t\tresultCookies = append(resultCookies, c)\n\t}\n\tinfo, _ = page.Info()\n\tif info != nil {\n\t\tcurrentURL = info.URL\n\t\tif m := regexp.MustCompile(`/cid/([a-f0-9-]+)`).FindStringSubmatch(currentURL); len(m) > 1 && configID == \"\" {\n\t\t\tconfigID = m[1]\n\t\t}\n\t\tif m := regexp.MustCompile(`[?&]csesidx=(\\d+)`).FindStringSubmatch(currentURL); len(m) > 1 && csesidx == \"\" {\n\t\t\tcsesidx = m[1]\n\t\t}\n\t}\n\n\t// 如果 csesidx 为空，尝试从 authorization 中提取\n\tif csesidx == \"\" && authorization != \"\" {\n\t\tcsesidx = extractCSESIDXFromAuth(authorization)\n\t\tif csesidx != \"\" {\n\t\t\tlog.Printf(\"[Cookie刷新] [%s] 从 authorization 提取 csesidx: %s\", email, csesidx)\n\t\t}\n\t}\n\n\t// 如果仍为空，尝试访问主页获取\n\tif csesidx == \"\" {\n\t\tlog.Printf(\"[Cookie刷新] [%s] ⚠️ csesidx 为空，尝试访问主页获取...\", email)\n\t\tpage.Navigate(\"https://business.gemini.google/\")\n\t\ttime.Sleep(3 * time.Second)\n\t\tinfo, _ = page.Info()\n\t\tif info != nil {\n\t\t\tif m := regexp.MustCompile(`[?&]csesidx=(\\d+)`).FindStringSubmatch(info.URL); len(m) > 1 {\n\t\t\t\tcsesidx = m[1]\n\t\t\t\tlog.Printf(\"[Cookie刷新] [%s] 从主页URL提取 csesidx: %s\", email, csesidx)\n\t\t\t}\n\t\t}\n\t}\n\n\t// 如果 csesidx 为空，尝试从 authorization 提取\n\tif csesidx == \"\" && authorization != \"\" {\n\t\tcsesidx = extractCSESIDXFromAuth(authorization)\n\t}\n\n\t// csesidx 是必须的\n\tif csesidx == \"\" {\n\t\tresult.Error = fmt.Errorf(\"未能获取 csesidx\")\n\t\treturn result\n\t}\n\n\tresult.Success = true\n\tresult.Authorization = authorization\n\tresult.SecureCookies = resultCookies\n\tresult.ConfigID = configID\n\tresult.CSESIDX = csesidx\n\n\treturn result\n}\n\n// extractCSESIDXFromAuth 从 authorization header 中提取 csesidx\nfunc extractCSESIDXFromAuth(auth string) string {\n\tparts := strings.Split(auth, \" \")\n\tif len(parts) != 2 {\n\t\treturn \"\"\n\t}\n\tjwtParts := strings.Split(parts[1], \".\")\n\tif len(jwtParts) != 3 {\n\t\treturn \"\"\n\t}\n\t// 解码 payload\n\tpayload := jwtParts[1]\n\t// 补齐 padding\n\tswitch len(payload) % 4 {\n\tcase 2:\n\t\tpayload += \"==\"\n\tcase 3:\n\t\tpayload += \"=\"\n\t}\n\tdecoded, err := base64.URLEncoding.DecodeString(payload)\n\tif err != nil {\n\t\tdecoded, err = base64.RawURLEncoding.DecodeString(jwtParts[1])\n\t\tif err != nil {\n\t\t\treturn \"\"\n\t\t}\n\t}\n\t// 提取 sub 字段\n\tvar claims map[string]interface{}\n\tif err := json.Unmarshal(decoded, &claims); err != nil {\n\t\treturn \"\"\n\t}\n\tif sub, ok := claims[\"sub\"].(string); ok && strings.HasPrefix(sub, \"csesidx/\") {\n\t\treturn strings.TrimPrefix(sub, \"csesidx/\")\n\t}\n\treturn \"\"\n}\nfunc NativeRegisterWorker(id int, dataDirAbs string) {\n\ttime.Sleep(time.Duration(id) * 3 * time.Second)\n\n\tfor atomic.LoadInt32(&IsRegistering) == 1 {\n\t\tif pool.Pool.TotalCount() >= TargetCount {\n\t\t\treturn\n\t\t}\n\n\t\t// 获取代理（优先使用代理池）\n\t\tcurrentProxy := Proxy\n\t\tif GetProxy != nil {\n\t\t\tcurrentProxy = GetProxy()\n\t\t}\n\t\tlogger.Debug(\"[注册线程 %d] 启动注册任务, 代理: %s\", id, currentProxy)\n\n\t\tresult := RunBrowserRegister(Headless, currentProxy, id)\n\n\t\t// 释放代理\n\t\tif ReleaseProxy != nil && currentProxy != \"\" && currentProxy != Proxy {\n\t\t\tReleaseProxy(currentProxy)\n\t\t}\n\n\t\tif result.Success {\n\t\t\tif err := SaveBrowserRegisterResult(result, dataDirAbs); err != nil {\n\t\t\t\tlogger.Warn(\"[注册线程 %d] ⚠️ 保存失败: %v\", id, err)\n\t\t\t\tStats.AddFailed(err.Error())\n\t\t\t} else {\n\t\t\t\tStats.AddSuccess()\n\t\t\t\tpool.Pool.Load(DataDir)\n\t\t\t}\n\t\t} else {\n\t\t\terrMsg := \"未知错误\"\n\t\t\tif result.Error != nil {\n\t\t\t\terrMsg = result.Error.Error()\n\t\t\t}\n\t\t\tlogger.Warn(\"[注册线程 %d] ❌ 注册失败: %s\", id, errMsg)\n\t\t\tStats.AddFailed(errMsg)\n\n\t\t\tif strings.Contains(errMsg, \"频繁\") || strings.Contains(errMsg, \"rate\") ||\n\t\t\t\tstrings.Contains(errMsg, \"timeout\") || strings.Contains(errMsg, \"连接\") {\n\t\t\t\twaitTime := 10 + id*2\n\t\t\t\tlogger.Debug(\"[注册线程 %d] ⏳ 等待 %d 秒后重试...\", id, waitTime)\n\t\t\t\ttime.Sleep(time.Duration(waitTime) * time.Second)\n\t\t\t} else {\n\t\t\t\ttime.Sleep(3 * time.Second)\n\t\t\t}\n\t\t}\n\t}\n\tlogger.Debug(\"[注册线程 %d] 停止\", id)\n}\n"
  },
  {
    "path": "src/register/register.go",
    "content": "package register\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"business2api/src/logger\"\n\t\"business2api/src/pool\"\n)\n\n// ==================== 注册与刷新 ====================\n\nvar (\n\tDataDir       string\n\tTargetCount   int\n\tMinCount      int\n\tCheckInterval time.Duration\n\tThreads       int\n\tHeadless      bool   // 注册无头模式\n\tProxy         string // 代理\n)\n\nvar IsRegistering int32\nvar registeringTarget int32 // 正在注册的目标数量\nvar registerMu sync.Mutex   // 注册启动互斥锁\n\nvar Stats = &RegisterStats{}\n\ntype RegisterStats struct {\n\tTotal     int       `json:\"total\"`\n\tSuccess   int       `json:\"success\"`\n\tFailed    int       `json:\"failed\"`\n\tLastError string    `json:\"lastError\"`\n\tUpdatedAt time.Time `json:\"updatedAt\"`\n\tmu        sync.RWMutex\n}\n\nfunc (s *RegisterStats) AddSuccess() {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\ts.Total++\n\ts.Success++\n\ts.UpdatedAt = time.Now()\n}\n\nfunc (s *RegisterStats) AddFailed(err string) {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\ts.Total++\n\ts.Failed++\n\ts.LastError = err\n\ts.UpdatedAt = time.Now()\n}\n\nfunc (s *RegisterStats) Get() map[string]interface{} {\n\ts.mu.RLock()\n\tdefer s.mu.RUnlock()\n\treturn map[string]interface{}{\n\t\t\"total\":      s.Total,\n\t\t\"success\":    s.Success,\n\t\t\"failed\":     s.Failed,\n\t\t\"last_error\": s.LastError,\n\t\t\"updated_at\": s.UpdatedAt,\n\t}\n}\n\n// 注册结果\ntype RegisterResult struct {\n\tSuccess  bool   `json:\"success\"`\n\tEmail    string `json:\"email\"`\n\tError    string `json:\"error\"`\n\tNeedWait bool   `json:\"needWait\"`\n}\n\n// StartRegister 启动注册任务（优化并发控制）\nfunc StartRegister(count int) error {\n\tregisterMu.Lock()\n\tdefer registerMu.Unlock()\n\n\t// 再次检查当前账号数是否已满足\n\tpool.Pool.Load(DataDir)\n\tcurrentCount := pool.Pool.TotalCount()\n\tif currentCount >= TargetCount {\n\t\tlogger.Info(\"✅ 账号数已满足: %d >= %d，无需注册\", currentCount, TargetCount)\n\t\treturn nil\n\t}\n\n\t// 如果已经在注册中，检查是否需要调整\n\tif atomic.LoadInt32(&IsRegistering) == 1 {\n\t\tcurrentTarget := atomic.LoadInt32(&registeringTarget)\n\t\tif int(currentTarget) >= count {\n\t\t\treturn fmt.Errorf(\"注册进程已在运行，目标: %d\", currentTarget)\n\t\t}\n\t\t// 更新目标数量\n\t\tatomic.StoreInt32(&registeringTarget, int32(count))\n\t\tlogger.Info(\"🔄 注册目标已更新: %d\", count)\n\t\treturn nil\n\t}\n\n\tif !atomic.CompareAndSwapInt32(&IsRegistering, 0, 1) {\n\t\treturn fmt.Errorf(\"注册进程已在运行\")\n\t}\n\tatomic.StoreInt32(&registeringTarget, int32(count))\n\n\t// 获取数据目录的绝对路径\n\tdataDirAbs, _ := filepath.Abs(DataDir)\n\tif err := os.MkdirAll(dataDirAbs, 0755); err != nil {\n\t\tatomic.StoreInt32(&IsRegistering, 0)\n\t\tatomic.StoreInt32(&registeringTarget, 0)\n\t\treturn fmt.Errorf(\"创建数据目录失败: %w\", err)\n\t}\n\n\t// 使用配置的线程数\n\tthreads := Threads\n\tif threads <= 0 {\n\t\tthreads = 1\n\t}\n\tfor i := 0; i < threads; i++ {\n\t\tgo NativeRegisterWorker(i+1, dataDirAbs)\n\t}\n\n\t// 监控进度\n\tgo func() {\n\t\tfor {\n\t\t\ttime.Sleep(10 * time.Second)\n\t\t\tpool.Pool.Load(DataDir)\n\t\t\tcurrentCount := pool.Pool.TotalCount()\n\t\t\ttarget := atomic.LoadInt32(&registeringTarget)\n\n\t\t\t// 检查是否达到目标（使用当前目标和全局目标的较大值）\n\t\t\teffectiveTarget := TargetCount\n\t\t\tif int(target) > effectiveTarget {\n\t\t\t\teffectiveTarget = int(target)\n\t\t\t}\n\n\t\t\tif currentCount >= effectiveTarget {\n\t\t\t\tlogger.Info(\"✅ 已达到目标账号数: %d >= %d，停止注册\", currentCount, effectiveTarget)\n\t\t\t\tatomic.StoreInt32(&IsRegistering, 0)\n\t\t\t\tatomic.StoreInt32(&registeringTarget, 0)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}()\n\n\treturn nil\n}\n\n// PoolMaintainer 号池维护器\nfunc PoolMaintainer() {\n\tinterval := CheckInterval\n\tif interval < time.Minute {\n\t\tinterval = 30 * time.Minute\n\t}\n\n\tticker := time.NewTicker(interval)\n\tdefer ticker.Stop()\n\tCheckAndMaintainPool()\n\n\tfor range ticker.C {\n\t\tCheckAndMaintainPool()\n\t}\n}\n\n// CheckAndMaintainPool 检查并维护号池（优化并发控制）\nfunc CheckAndMaintainPool() {\n\t// 如果正在注册中，跳过检查\n\tif atomic.LoadInt32(&IsRegistering) == 1 {\n\t\tlogger.Debug(\"⏳ 注册进程运行中，跳过本次检查\")\n\t\treturn\n\t}\n\n\tpool.Pool.Load(DataDir)\n\n\treadyCount := pool.Pool.ReadyCount()\n\tpendingCount := pool.Pool.PendingCount()\n\ttotalCount := pool.Pool.TotalCount()\n\n\tlogger.Info(\"📊 号池检查: ready=%d, pending=%d, total=%d, 目标=%d, 最小=%d\",\n\t\treadyCount, pendingCount, totalCount, TargetCount, MinCount)\n\n\t// 只有当总数小于最小数时才触发注册，避免频繁注册\n\tif totalCount < MinCount {\n\t\tneedCount := TargetCount - totalCount\n\t\tlogger.Info(\"⚠️ 账号数低于最小值 (%d < %d)，需要注册 %d 个\", totalCount, MinCount, needCount)\n\t\tif err := StartRegister(needCount); err != nil {\n\t\t\tlogger.Error(\"❌ 启动注册失败: %v\", err)\n\t\t}\n\t} else if totalCount < TargetCount {\n\t\tlogger.Debug(\"📊 账号数未达目标 (%d < %d)，但高于最小值，暂不触发注册\", totalCount, TargetCount)\n\t}\n}\n"
  },
  {
    "path": "src/utils/utils.go",
    "content": "package utils\n\nimport (\n\t\"bytes\"\n\t\"compress/gzip\"\n\t\"crypto/tls\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"business2api/src/logger\"\n\t\"business2api/src/pool\"\n)\n\n// ==================== HTTP 客户端 ====================\n\nvar HTTPClient *http.Client\n\n// NewHTTPClient 创建 HTTP 客户端\nfunc NewHTTPClient(proxy string) *http.Client {\n\ttransport := &http.Transport{\n\t\tTLSClientConfig:     &tls.Config{InsecureSkipVerify: true},\n\t\tMaxIdleConns:        100,\n\t\tMaxIdleConnsPerHost: 20,\n\t\tMaxConnsPerHost:     50,\n\t\tIdleConnTimeout:     90 * time.Second,\n\t\tDisableCompression:  false,\n\t\tForceAttemptHTTP2:   true,\n\t}\n\n\tif proxy != \"\" {\n\t\tproxyURL, err := url.Parse(proxy)\n\t\tif err == nil {\n\t\t\ttransport.Proxy = http.ProxyURL(proxyURL)\n\t\t}\n\t}\n\n\treturn &http.Client{\n\t\tTransport: transport,\n\t\tTimeout:   1800 * time.Second,\n\t}\n}\n\n// InitHTTPClient 初始化全局 HTTP 客户端\nfunc InitHTTPClient(proxy string) {\n\tHTTPClient = NewHTTPClient(proxy)\n\tpool.HTTPClient = HTTPClient\n\tif proxy != \"\" {\n\t\tlogger.Info(\"✅ 使用代理: %s\", proxy)\n\t}\n}\n\n// ReadResponseBody 读取 HTTP 响应体（支持 gzip）\nfunc ReadResponseBody(resp *http.Response) ([]byte, error) {\n\tvar reader io.Reader = resp.Body\n\tif resp.Header.Get(\"Content-Encoding\") == \"gzip\" {\n\t\tgzReader, err := gzip.NewReader(resp.Body)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tdefer gzReader.Close()\n\t\treader = gzReader\n\t}\n\treturn io.ReadAll(reader)\n}\n\n// ParseNDJSON 解析 NDJSON 格式数据\nfunc ParseNDJSON(data []byte) []map[string]interface{} {\n\tvar result []map[string]interface{}\n\tlines := bytes.Split(data, []byte(\"\\n\"))\n\tfor _, line := range lines {\n\t\tline = bytes.TrimSpace(line)\n\t\tif len(line) == 0 {\n\t\t\tcontinue\n\t\t}\n\t\tvar obj map[string]interface{}\n\t\tif err := json.Unmarshal(line, &obj); err == nil {\n\t\t\tresult = append(result, obj)\n\t\t}\n\t}\n\treturn result\n}\n\n// ParseIncompleteJSONArray 解析可能不完整的 JSON 数组\nfunc ParseIncompleteJSONArray(data []byte) []map[string]interface{} {\n\tvar result []map[string]interface{}\n\tif err := json.Unmarshal(data, &result); err == nil {\n\t\treturn result\n\t}\n\n\ttrimmed := bytes.TrimSpace(data)\n\tif len(trimmed) > 0 && trimmed[0] == '[' {\n\t\tif trimmed[len(trimmed)-1] != ']' {\n\t\t\tlastBrace := bytes.LastIndex(trimmed, []byte(\"}\"))\n\t\t\tif lastBrace > 0 {\n\t\t\t\tfixed := append(trimmed[:lastBrace+1], ']')\n\t\t\t\tif err := json.Unmarshal(fixed, &result); err == nil {\n\t\t\t\t\tlogger.Warn(\"JSON 数组不完整，已修复\")\n\t\t\t\t\treturn result\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\n// TruncateString 截断字符串\nfunc TruncateString(s string, maxLen int) string {\n\tif len(s) <= maxLen {\n\t\treturn s\n\t}\n\treturn s[:maxLen] + \"...\"\n}\n\n// Min 返回两个整数中的较小值\nfunc Min(a, b int) int {\n\tif a < b {\n\t\treturn a\n\t}\n\treturn b\n}\n"
  }
]