Showing preview only (453K chars total). Download the full file or copy to clipboard to get everything.
Repository: XxxXTeam/business2api
Branch: master
Commit: 4218208432a1
Files: 23
Total size: 404.7 KB
Directory structure:
gitextract_zttsb6rv/
├── .dockerignore
├── .github/
│ ├── dependabot.yml
│ └── workflows/
│ ├── build.yml
│ └── codeql.yml
├── .gitignore
├── Dockerfile
├── README.md
├── config/
│ ├── README.md
│ └── config.json.example
├── docker/
│ └── docker-compose.yml
├── go.mod
├── go.sum
├── main.go
└── src/
├── api/
│ └── api.go
├── logger/
│ └── logger.go
├── pool/
│ ├── pool.go
│ ├── pool_client.go
│ └── pool_server.go
├── proxy/
│ ├── proxy.go
│ └── singbox.go
├── register/
│ ├── browser.go
│ └── register.go
└── utils/
└── utils.go
================================================
FILE CONTENTS
================================================
================================================
FILE: .dockerignore
================================================
# Git
.git
.gitignore
# Data
data/
# Node.js files (no longer needed - registration is pure Go)
node_modules/
package-lock.json
package.json
main.js
# IDE
.idea/
.vscode/
# Build artifacts
*.exe
*.dll
*.so
*.dylib
# Documentation
README.md
LICENSE
# GitHub
.github/
================================================
FILE: .github/dependabot.yml
================================================
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: "" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"
================================================
FILE: .github/workflows/build.yml
================================================
name: Build and Release
on:
push:
branches: [main, master]
tags:
- 'v*'
pull_request:
branches: [main, master]
workflow_dispatch:
env:
GO_VERSION: '1.23'
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
goos: [linux, darwin, windows]
goarch: [amd64, arm64]
exclude:
- goos: windows
goarch: arm64
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
cache: false
- name: Get version
id: version
run: |
if [[ "$GITHUB_REF" == refs/tags/v* ]]; then
VERSION=${GITHUB_REF#refs/tags/v}
else
VERSION=$(git describe --tags --always --dirty 2>/dev/null || echo "dev")
fi
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
echo "Building version: $VERSION"
- name: Build
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
VERSION: ${{ steps.version.outputs.VERSION }}
GOTOOLCHAIN: auto
run: |
EXT=""
if [ "$GOOS" = "windows" ]; then
EXT=".exe"
fi
CGO_ENABLED=0 go build \
-tags "with_quic,with_utls" \
-ldflags="-s -w -X main.Version=${VERSION}" \
-o business2api-${{ matrix.goos }}-${{ matrix.goarch }}${EXT} .
- name: Prepare package
run: |
mkdir -p dist
mv business2api-* dist/
cp config/config.json.example dist/
cp README.md dist/
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: business2api-${{ matrix.goos }}-${{ matrix.goarch }}
path: dist/*
retention-days: 30
docker:
runs-on: ubuntu-latest
needs: build
if: github.event_name == 'push'
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,prefix=
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
release:
runs-on: ubuntu-latest
needs: build
if: startsWith(github.ref, 'refs/tags/v')
permissions:
contents: write
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Prepare release files
run: |
mkdir -p release
for dir in artifacts/business2api-*/; do
# 获取平台名称
platform=$(basename "$dir")
# 创建临时目录
mkdir -p "tmp/$platform"
cp "$dir"/* "tmp/$platform/"
# 设置可执行权限
for f in "tmp/$platform"/business2api-*; do
if [[ ! "$f" == *.exe ]]; then
chmod +x "$f"
fi
done
# 打包
tar -czvf "release/${platform}.tar.gz" -C tmp "$platform"
rm -rf "tmp/$platform"
done
rm -rf tmp
- name: Create Release
uses: softprops/action-gh-release@v1
with:
files: release/*
generate_release_notes: true
================================================
FILE: .github/workflows/codeql.yml
================================================
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL Advanced"
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
schedule:
- cron: '33 14 * * 3'
jobs:
analyze:
name: Analyze (${{ matrix.language }})
# Runner size impacts CodeQL analysis time. To learn more, please see:
# - https://gh.io/recommended-hardware-resources-for-running-codeql
# - https://gh.io/supported-runners-and-hardware-resources
# - https://gh.io/using-larger-runners (GitHub.com only)
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
permissions:
# required for all workflows
security-events: write
# required to fetch internal or private CodeQL packs
packages: read
# only required for workflows in private repositories
actions: read
contents: read
strategy:
fail-fast: false
matrix:
include:
- language: actions
build-mode: none
- language: go
build-mode: autobuild
# CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift'
# Use `c-cpp` to analyze code written in C, C++ or both
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Add any setup steps before running the `github/codeql-action/init` action.
# This includes steps like installing compilers or runtimes (`actions/setup-node`
# or others). This is typically only required for manual builds.
# - name: Setup runtime (example)
# uses: actions/setup-example@v1
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# If the analyze step fails for one of the languages you are analyzing with
# "We were unable to automatically build your code", modify the matrix above
# to set the build mode to "manual" for that language. Then modify this step
# to build your code.
# ℹ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
- name: Run manual build steps
if: matrix.build-mode == 'manual'
shell: bash
run: |
echo 'If you are using a "manual" build mode for one or more of the' \
'languages you are analyzing, replace this with the commands to build' \
'your code, for example:'
echo ' make bootstrap'
echo ' make release'
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
with:
category: "/language:${{matrix.language}}"
================================================
FILE: .gitignore
================================================
# Binaries
gemini-gateway
gemini-gateway.exe
*.exe
*.dll
*.so
*.dylib
# Data
data/
*.json
!config.json.example
!package.json
# Node
node_modules/
npm-debug.log*
# IDE
.idea/
.vscode/
*.swp
*.swo
# OS
*.zip
.DS_Store
Thumbs.db
# Build
dist/
build/
data/
config.json
package-lock.json
main.js
package.json
flow.txt
config/config.json
business2api
flow/
================================================
FILE: Dockerfile
================================================
# Build stage
FROM golang:1.23-alpine AS builder
WORKDIR /app
# Install build dependencies
RUN apk add --no-cache git ca-certificates
# Copy go mod files
COPY go.mod go.sum ./
ENV GOTOOLCHAIN=auto
RUN go mod download
# Copy source code
COPY *.go ./
COPY src/ ./src/
# Build binary
RUN CGO_ENABLED=0 GOOS=linux go build -tags "with_quic,with_utls" -ldflags="-s -w" -o business2api .
# Runtime stage
FROM alpine:latest
WORKDIR /app
# Install runtime dependencies (Chromium for rod browser automation)
RUN apk add --no-cache \
ca-certificates \
tzdata \
chromium \
nss \
freetype \
harfbuzz \
ttf-freefont \
font-noto-cjk
# Copy binary from builder
COPY --from=builder /app/business2api .
# Copy config template if exists
COPY config.json.exampl[e] ./
# Create data directory
RUN mkdir -p /app/data
# Environment variables
ENV LISTEN_ADDR=":8000"
ENV DATA_DIR="/app/data"
EXPOSE 8000
ENTRYPOINT ["./business2api"]
================================================
FILE: README.md
================================================
# Business2API
> 🚀 OpenAI/Gemini 兼容的 Gemini Business API 代理服务,支持账号池管理、自动注册和 Flow 图片/视频生成。
[](https://github.com/XxxXTeam/business2api/actions/workflows/build.yml)
[](LICENSE)
[](https://golang.org)
## ✨ 功能特性
| 功能 | 描述 |
|------|------|
| 🔌 **多 API 兼容** | OpenAI (`/v1/chat/completions`)、Gemini (`/v1beta/models`)、Claude (`/v1/messages`) |
| 🏊 **智能账号池** | 自动轮询、刷新、冷却管理、401/403 自动换号 |
| 🌊 **流式响应** | SSE 流式输出,支持 `stream: true` |
| 🎨 **多模态** | 图片/视频输入、原生图片生成(`-image` 后缀)|
| 🤖 **自动注册** | 浏览器自动化注册,支持 Windows/Linux/macOS |
| 🌐 **代理池** | HTTP/SOCKS5 代理,订阅链接,健康检查 |
| 📊 **遥测监控** | IP 请求统计、Token 使用量、RPM 监控 |
| 🔄 **热重载** | 配置文件自动监听,无需重启 |
## 📦 支持的模型
### Gemini Business 模型
| 模型 | 文本 | 图片生成 | 视频生成 | 搜索 |
|------|:----:|:--------:|:--------:|:----:|
| gemini-2.5-flash | ✅ | ✅ | ✅ | ✅ |
| gemini-2.5-pro | ✅ | ✅ | ✅ | ✅ |
| gemini-2.5-flash-preview-latest | ✅ | ✅ | ✅ | ✅ |
| gemini-3-pro-preview | ✅ | ✅ | ✅ | ✅ |
| gemini-3-flash-preview | ✅ | ✅ | ✅ | ✅ |
| gemini-3-flash | ✅ | ✅ | ✅ | ✅ |
### 功能后缀
支持单个或混合后缀启用指定功能:
| 后缀 | 功能 | 示例 |
|------|------|------|
| `-image` | 图片生成 | `gemini-2.5-flash-image` |
| `-video` | 视频生成 | `gemini-2.5-flash-video` |
| `-search` | 联网搜索 | `gemini-2.5-flash-search` |
| 混合后缀 | 同时启用多功能 | `gemini-2.5-flash-image-search` |
**说明:**
- 无后缀:启用所有功能(图片/视频/搜索/工具)
- 有后缀:只启用指定功能,支持任意组合如 `-image-search`、`-video-search`
### ⚠️ 限制说明
| 限制 | 说明 |
|------|------|
| **不支持自定义工具** | Function Calling / Tools 参数会被忽略,仅支持内置工具(图片/视频生成、搜索) |
| **上下文拼接实现** | 多轮对话通过拼接 `messages` 为单次请求实现,非原生会话管理 |
| **无状态** | 每次请求独立,不保留会话状态,历史消息需客户端自行维护 |
---
>> 公益 Demo(免费调用)
> 🔗 链接:<https://business2api.openel.top>
>
> 在线绘图预设测试 [https://chat.openel.top](https://chat.openel.top/)
<img width="1880" height="919" alt="image" src="https://github.com/user-attachments/assets/d05d4b06-2c2a-468f-b8fb-fb6dad8dc3ab" />
>
> > API Key 获取请访问 https://business2api.openel.top/auth 获取个人专属免费APIKEY
>> GLM 公益测试 API
> 🔗 链接:<https://GLM.openel.top>
>
> XiaoMi 网页逆向公益 API
> 🔗 链接:[https://xiaomi.openel.top](https://xiaomi.openel.top/)
>
> API Key : `sk-3d2f9b84e7f510b1a08f7b3d6c9a6a7f17fbbad5624ea29f22d9c742bf39c863`
## 快速开始
### 方式一:Docker 部署(推荐)
#### 1. 使用 Docker Compose
```bash
# 创建目录
mkdir business2api && cd business2api
# 下载必要文件
wget https://raw.githubusercontent.com/XxxXTeam/business2api/master/docker/docker-compose.yml
wget https://raw.githubusercontent.com/XxxXTeam/business2api/master/config/config.json.example -O config.json
# 编辑配置
vim config.json
# 创建数据目录
mkdir data
# 启动服务
docker compose up -d
```
#### 2. 使用 Docker Run
```bash
# 拉取镜像
docker pull ghcr.io/xxxteam/business2api:latest
# 创建配置文件
wget https://raw.githubusercontent.com/XxxXTeam/business2api/master/config/config.json.example -O config.json
# 运行容器
docker run -d \
--name business2api \
-p 8000:8000 \
-v $(pwd)/data:/app/data \
-v $(pwd)/config.json:/app/config/config.json:ro \
ghcr.io/xxxteam/business2api:latest
```
### 方式二:二进制部署
#### 1. 下载预编译版本
从 [Releases](https://github.com/XxxXTeam/business2api/releases) 下载对应平台的二进制文件。
```bash
# Linux amd64
wget https://github.com/XxxXTeam/business2api/releases/latest/download/business2api-linux-amd64.tar.gz
tar -xzf business2api-linux-amd64.tar.gz
chmod +x business2api-linux-amd64
```
#### 2. 从源码编译
```bash
# 需要 Go 1.24+
git clone https://github.com/XxxXTeam/business2api.git
cd business2api
# 编译
go build -o business2api .
# 运行
./business2api
```
### 方式三:使用 Systemd 服务
```bash
# 创建服务文件
sudo tee /etc/systemd/system/business2api.service << EOF
[Unit]
Description=Gemini Gateway Service
After=network.target
[Service]
Type=simple
User=nobody
WorkingDirectory=/opt/business2api
ExecStart=/opt/business2api/business2api
Restart=always
RestartSec=5
Environment=LISTEN_ADDR=:8000
Environment=DATA_DIR=/opt/business2api/data
[Install]
WantedBy=multi-user.target
EOF
# 启动服务
sudo systemctl daemon-reload
sudo systemctl enable business2api
sudo systemctl start business2api
```
---
## 配置说明
### config.json
```json
{
"api_keys": ["sk-your-api-key"], // API 密钥列表,用于鉴权
"listen_addr": ":8000", // 监听地址
"data_dir": "./data", // 账号数据目录
"default_config": "", // 默认 configId(可选)
"debug": false, // 调试模式(输出详细日志)
"pool": {
"target_count": 50, // 目标账号数量
"min_count": 10, // 最小账号数,低于此值触发注册
"check_interval_minutes": 30, // 检查间隔(分钟)
"register_threads": 1, // 本地注册线程数
"register_headless": true, // 无头模式注册
"refresh_on_startup": true, // 启动时刷新账号
"refresh_cooldown_sec": 240, // 刷新冷却时间(秒)
"use_cooldown_sec": 15, // 使用冷却时间(秒)
"max_fail_count": 3, // 最大连续失败次数
"enable_browser_refresh": true, // 启用浏览器刷新401账号
"browser_refresh_headless": true, // 浏览器刷新无头模式
"browser_refresh_max_retry": 1, // 浏览器刷新最大重试次数
"auto_delete_401": false // 401时自动删除账号
},
"pool_server": {
"enable": false, // 是否启用分离模式
"mode": "local", // 运行模式:local/server/client
"server_addr": "http://ip:8000", // 服务器地址(client模式)
"listen_addr": ":8000", // 监听地址(server模式)
"secret": "your-secret-key", // 通信密钥
"target_count": 50, // 目标账号数(server模式)
"client_threads": 2, // 客户端并发线程数
"data_dir": "./data", // 数据目录(server模式)
"expired_action": "delete" // 过期账号处理:delete/refresh/queue
},
"proxy_pool": {
"subscribes": [], // 代理订阅链接列表
"files": [], // 本地代理文件列表
"health_check": true, // 启用健康检查
"check_on_startup": true // 启动时检查
}
}
```
### 多 API Key 支持
支持配置多个 API Key,所有 Key 都可以用于鉴权:
```json
{
"api_keys": [
"sk-key-1",
"sk-key-2",
"sk-key-3"
]
}
```
### 配置热重载
服务运行时自动监听 `config/config.json` 文件变更,无需重启即可生效。
**可热重载的配置项:**
| 配置项 | 说明 |
|----------|------|
| `api_keys` | API 密钥列表 |
| `debug` | 调试模式 |
| `pool.refresh_cooldown_sec` | 刷新冷却时间 |
| `pool.use_cooldown_sec` | 使用冷却时间 |
| `pool.max_fail_count` | 最大失败次数 |
| `pool.enable_browser_refresh` | 浏览器刷新开关 |
**配置合并机制:** 配置文件中缺失的字段会自动使用默认值,无需手动同步示例文件。
```bash
# 手动触发重载
curl -X POST http://localhost:8000/admin/reload-config \
-H "Authorization: Bearer sk-your-api-key"
```
---
## C/S 分离架构
支持将号池管理与API服务分离部署,适用于多节点场景。
### 架构说明
```
┌─────────────────┐ ┌─────────────────┐
│ API Server │◄───────►│ Pool Server │
│ (客户端模式) │ HTTP │ (服务器模式) │
└─────────────────┘ └────────┬────────┘
│
WebSocket│
│
┌────────▼────────┐
│ Worker Client │
│ (注册/续期) │
└─────────────────┘
```
### 运行模式
| 模式 | 说明 |
|------|------|
| `local` | 本地模式(默认),API服务和号池管理在同一进程 |
| `server` | 服务器模式,提供号池服务和任务分发 |
| `client` | 客户端模式,只接收任务(注册/续期),不提供API服务 |
### Server 模式配置
```json
{
"api_keys": ["sk-your-api-key"],
"listen_addr": ":8000",
"pool_server": {
"enable": true,
"mode": "server",
"secret": "shared-secret-key",
"target_count": 100,
"data_dir": "./data",
"expired_action": "delete"
}
}
```
### Client 模式配置(仅注册/续期工作节点)
```json
{
"pool_server": {
"enable": true,
"mode": "client",
"server_addr": "http://server-ip:8000",
"secret": "shared-secret-key",
"client_threads": 3
},
"proxy_pool": {
"subscribes": ["https://your-proxy-subscribe-url"],
"health_check": true,
"check_on_startup": true
}
}
```
### 配置项说明
| 配置项 | 说明 | 默认值 |
|--------|------|--------|
| `client_threads` | 客户端并发任务数 | 1 |
| `expired_action` | 过期账号处理方式 | delete |
**expired_action 可选值:**
- `delete` - 删除过期账号
- `refresh` - 尝试浏览器刷新
- `queue` - 保留在队列等待重试
**架构说明(v2.x):**
```
┌─────────────────────────────────────────────────────┐
│ Server (:8000) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ API 服务 │ │ WS 服务 │ │ 号池管理 │ │
│ │ /v1/chat/* │ │ /ws │ │ Pool Mgr │ │
│ └─────────────┘ └──────┬──────┘ └─────────────┘ │
└──────────────────────────┼──────────────────────────┘
│ WebSocket
┌────────────┼────────────┐
│ │ │
┌─────▼─────┐ ┌────▼────┐ ┌─────▼─────┐
│ Client1 │ │ Client2 │ │ Client3 │
│ (注册) │ │ (注册) │ │ (注册) │
└───────────┘ └─────────┘ └───────────┘
```
**Client 模式说明:**
- 通过 WebSocket 连接 Server (`/ws`) 接收任务
- 执行注册新账号任务
- 执行401账号Cookie续期任务
- 完成后自动回传账号数据到Server
- **不提供API服务**,只作为工作节点
### 环境变量
| 变量 | 说明 | 默认值 |
|------|------|--------|
| `LISTEN_ADDR` | 监听地址 | `:8000` |
| `DATA_DIR` | 数据目录 | `./data` |
| `PROXY` | 代理地址 | - |
| `API_KEY` | API 密钥 | - |
| `CONFIG_ID` | 默认 configId | - |
---
## API 使用
### 获取模型列表
```bash
curl http://localhost:8000/v1/models \
-H "Authorization: Bearer sk-your-api-key"
```
### 聊天补全
```bash
curl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk-your-api-key" \
-d '{
"model": "gemini-2.5-flash",
"messages": [
{"role": "user", "content": "Hello!"}
],
"stream": true
}'
```
### 多模态(图片输入)
```bash
curl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk-your-api-key" \
-d '{
"model": "gemini-2.5-flash",
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": "描述这张图片"},
{"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,..."}}
]
}
]
}'
```
---
## Flow 图片/视频生成
Flow 集成了 Google VideoFX (Veo/Imagen) API,支持图片和视频生成。
### 配置
```json
{
"flow": {
"enable": true,
"tokens": [], // 配置文件中的 Token(可选)
"proxy": "", // Flow 专用代理
"timeout": 120, // 超时时间(秒)
"poll_interval": 3, // 轮询间隔(秒)
"max_poll_attempts": 500 // 最大轮询次数
}
}
```
### 获取 Flow Token
**方式一:文件目录(推荐)**
将完整的 cookie 字符串保存到 `data/at/` 目录下的任意 `.txt` 文件:
```bash
mkdir -p data/at
echo "your-cookie-string" > data/at/account1.txt
```
服务启动时自动加载,支持文件监听自动热加载。
**方式二:API 添加**
```bash
curl -X POST http://localhost:8000/admin/flow/add-token \
-H "Authorization: Bearer sk-xxx" \
-d '{"cookie": "your-cookie-string"}'
```
**Cookie 获取方法:**
1. 访问 [labs.google/fx](https://labs.google/fx) 并登录
2. 打开开发者工具 → Application → Cookies
3. 复制所有 cookie 或 `__Secure-next-auth.session-token` 的值
### Flow 模型列表
| 模型 | 类型 | 说明 |
|------|------|------|
| `gemini-2.5-flash-image-landscape/portrait` | 图片 | Gemini 2.5 Flash 图片生成 |
| `gemini-3.0-pro-image-landscape/portrait` | 图片 | Gemini 3.0 Pro 图片生成 |
| `imagen-4.0-generate-preview-landscape/portrait` | 图片 | Imagen 4.0 图片生成 |
| `veo_3_1_t2v_fast_landscape/portrait` | 视频 | Veo 3.1 文生视频 |
| `veo_2_1_fast_d_15_t2v_landscape/portrait` | 视频 | Veo 2.1 文生视频 |
| `veo_2_0_t2v_landscape/portrait` | 视频 | Veo 2.0 文生视频 |
| `veo_3_1_i2v_s_fast_fl_landscape/portrait` | 视频 | Veo 3.1 图生视频 (I2V) |
| `veo_2_1_fast_d_15_i2v_landscape/portrait` | 视频 | Veo 2.1 图生视频 (I2V) |
| `veo_2_0_i2v_landscape/portrait` | 视频 | Veo 2.0 图生视频 (I2V) |
| `veo_3_0_r2v_fast_landscape/portrait` | 视频 | Veo 3.0 多图生视频 (R2V) |
### 使用示例
```bash
# 图片生成
curl http://localhost:8000/v1/chat/completions \
-H "Authorization: Bearer sk-xxx" \
-H "Content-Type: application/json" \
-d '{"model": "gemini-2.5-flash-image-landscape", "messages": [{"role": "user", "content": "一只可爱的猫咪"}], "stream": true}'
# 文生视频 (T2V)
curl http://localhost:8000/v1/chat/completions \
-H "Authorization: Bearer sk-xxx" \
-H "Content-Type: application/json" \
-d '{"model": "veo_3_1_t2v_fast_landscape", "messages": [{"role": "user", "content": "猫咪在草地上追蝴蝶"}], "stream": true}'
# 图生视频 (I2V) - 支持首尾帧
curl http://localhost:8000/v1/chat/completions \
-H "Authorization: Bearer sk-xxx" \
-H "Content-Type: application/json" \
-d '{
"model": "veo_3_1_i2v_s_fast_fl_landscape",
"messages": [{
"role": "user",
"content": [
{"type": "text", "text": "猫咪跳跃"},
{"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,..."}}
]
}],
"stream": true
}'
```
---
## 🔧 常见问题与解决方案
### 注册相关
> **拟人化行为**:浏览器自动化已内置拟人化操作(随机延迟、贝塞尔曲线鼠标移动、自然打字节奏),以降低被检测风险。可通过 `register_headless: false` 开启可视模式观察行为。
| 错误 | 原因 | 解决方案 |
|------|------|----------|
| `无法获取验证码邮件` | 临时邮箱服务不稳定或邮件延迟 | 代理遭到拉黑,更换代理 |
| `panic: nil pointer` | 浏览器启动失败或页面未加载 | 检查 Chrome 是否安装,确保有足够内存 |
| `找不到提交按钮` | 页面结构变化或加载超时 | 升级到最新版本,检查网络 |
### API 相关
| 错误 | 原因 | 解决方案 |
|------|------|----------|
| `401 Unauthorized` | API Key 无效或未配置 | 检查 `api_keys` 配置 |
| `429 Too Many Requests` | 账号触发速率限制 | 增加账号池数量,调整 `use_cooldown_sec` |
| `503 Service Unavailable` | 无可用账号 | 等待账号刷新或增加注册 |
| `空响应` | Google 返回空内容 | 重试请求,检查 prompt 是否触发过滤 |
### WebSocket 相关
| 错误 | 原因 | 解决方案 |
|------|------|----------|
| `客户端频繁断开` | 心跳超时或网络不稳定 | 检查网络,确保 Server 和 Client 时间同步 |
| `上传注册结果失败` | Server 端口或路径错误 | 确保 `server_addr` 指向正确地址 |
### Flow 相关
| 错误 | 原因 | 解决方案 |
|------|------|----------|
| `Flow 服务未启用` | 未配置或 Token 为空 | 检查 `flow.enable` 和 `flow.tokens` |
| `Token 认证失败` | ST Token 过期 | 重新获取 Token |
| `视频生成超时` | 生成时间过长 | 增加 `max_poll_attempts` |
### Docker 相关
| 错误 | 原因 | 解决方案 |
|------|------|----------|
| `无法启动浏览器` | Docker 容器缺少 Chrome | 使用包含 Chrome 的镜像或挂载主机浏览器 |
| `权限被拒绝` | 数据目录权限问题 | `chown -R 1000:1000 ./data` |
---
## 📡 API 端点一览
### 公开端点
| 端点 | 方法 | 说明 |
|------|------|------|
| `/` | GET | 服务状态和信息 |
| `/health` | GET | 健康检查 |
| `/ws` | WS | WebSocket 端点 (Server 模式) |
### API 端点(需要 API Key)
| 端点 | 方法 | 说明 |
|------|------|------|
| `/v1/models` | GET | OpenAI 格式模型列表 |
| `/v1/chat/completions` | POST | OpenAI 格式聊天补全 |
| `/v1/messages` | POST | Claude 格式消息 |
| `/v1beta/models` | GET | Gemini 格式模型列表 |
| `/v1beta/models/:model` | GET | Gemini 格式模型详情 |
| `/v1beta/models/:model:generateContent` | POST | Gemini 格式生成内容 |
### 管理端点(需要 API Key)
| 端点 | 方法 | 说明 |
|------|------|------|
| `/admin/status` | GET | 账号池状态 |
| `/admin/stats` | GET | 详细 API 统计 |
| `/admin/ip` | GET | IP 遥测统计(请求数/Token/RPM) |
| `/admin/register` | POST | 触发注册 |
| `/admin/refresh` | POST | 刷新账号池 |
| `/admin/reload-config` | POST | 热重载配置文件 |
| `/admin/force-refresh` | POST | 强制刷新所有账号 |
| `/admin/config/cooldown` | POST | 动态调整冷却时间 |
| `/admin/browser-refresh` | POST | 手动触发浏览器刷新指定账号 |
| `/admin/config/browser-refresh` | POST | 配置浏览器刷新开关 |
| `/admin/flow/status` | GET | Flow 服务状态 |
| `/admin/flow/add-token` | POST | 添加 Flow Token |
| `/admin/flow/remove-token` | POST | 移除 Flow Token |
| `/admin/flow/reload` | POST | 重新加载 Flow Token |
---
## 🛠️ 开发
### 本地运行
```bash
# 安装依赖
go mod download
# 运行
go run .
# 调试模式
go run . -d
```
### 构建
```bash
# 标准构建
go build -o business2api .
# 带 QUIC/uTLS 支持(推荐)
go build -tags "with_quic with_utls" -o business2api .
# 生产构建(压缩体积)
CGO_ENABLED=0 go build -ldflags="-s -w" -tags "with_quic with_utls" -o business2api .
# 多平台构建
GOOS=linux GOARCH=amd64 go build -tags "with_quic with_utls" -o business2api-linux-amd64 .
GOOS=windows GOARCH=amd64 go build -tags "with_quic with_utls" -o business2api-windows-amd64.exe .
GOOS=darwin GOARCH=arm64 go build -tags "with_quic with_utls" -o business2api-darwin-arm64 .
```
### 项目结构
```
.
├── main.go # 主程序入口
├── config/ # 配置文件
│ ├── config.json.example
│ └── README.md
├── src/
│ ├── flow/ # Flow 图片/视频生成
│ ├── logger/ # 日志模块
│ ├── pool/ # 账号池管理(C/S架构)
│ ├── proxy/ # 代理池管理
│ ├── register/ # 浏览器自动注册
│ └── utils/ # 工具函数
├── docker/ # Docker 相关
│ └── docker-compose.yml
└── .github/ # GitHub Actions
```
---
## 📊 IP 遥测接口
访问 `/admin/ip` 获取全部 IP 请求统计:
```bash
curl http://localhost:8000/admin/ip \
-H "Authorization: Bearer sk-your-api-key"
```
**返回字段说明:**
| 字段 | 说明 |
|------|------|
| `unique_ips` | 独立 IP 数量 |
| `total_requests` | 总请求数 |
| `total_tokens` | 总 Token 消耗 |
| `total_images` | 图片生成数 |
| `total_videos` | 视频生成数 |
| `ips[].rpm` | 单 IP 每分钟请求数 |
| `ips[].input_tokens` | 输入 Token |
| `ips[].output_tokens` | 输出 Token |
| `ips[].models` | 各模型使用次数 |
---
## Star History
[](https://www.star-history.com/#XxxXTeam/business2api&type=date&legend=top-left)
## 📄 License
MIT License
================================================
FILE: config/README.md
================================================
# 配置说明
## 代理池配置 (`proxy_pool`)
**内置 xray-core**,支持 vmess、vless、shadowsocks、trojan 等协议,自动转换为本地 socks5 代理。
```json
"proxy_pool": {
"proxy": "", // 备用单个代理 (http/socks5 格式)
"subscribes": [ // 订阅链接列表 (支持 base64 编码)
"https://example.com/sub1",
"https://example.com/sub2"
],
"files": [ // 本地代理文件列表
"./proxies.txt"
],
"health_check": true, // 是否启用健康检查
"check_on_startup": false // 启动时是否检查所有节点
}
```
### 支持的代理格式
**代理文件/订阅内容格式** (每行一个):
```
# VMess
vmess://eyJ2IjoiMiIsInBzIjoi5ZCN56ewIiwiYWRkIjoic2VydmVyLmNvbSIsInBvcnQiOiI0NDMiLCJpZCI6InV1aWQiLCJhaWQiOiIwIiwic2N5IjoiYXV0byIsIm5ldCI6IndzIiwicGF0aCI6Ii9wYXRoIiwiaG9zdCI6Imhvc3QuY29tIiwidGxzIjoidGxzIn0=
# VLESS
vless://uuid@server.com:443?type=ws&security=tls&path=/path&host=host.com&sni=sni.com#名称
# Shadowsocks
ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ=@server.com:8388#名称
# Trojan
trojan://password@server.com:443?sni=sni.com#名称
# 直接代理
http://proxy.com:8080
socks5://127.0.0.1:1080
```
---
## 号池配置 (`pool`)
```json
"pool": {
"target_count": 50, // 目标账号数量
"min_count": 10, // 最小账号数,低于此值触发注册
"check_interval_minutes": 30, // 检查间隔(分钟)
"register_threads": 1, // 注册线程数
"register_headless": false, // 注册时是否无头模式
"refresh_on_startup": true, // 启动时是否刷新账号
"refresh_cooldown_sec": 240, // 刷新冷却时间(秒)
"use_cooldown_sec": 15, // 使用冷却时间(秒)
"max_fail_count": 3, // 最大失败次数
"enable_browser_refresh": true, // 启用浏览器刷新
"browser_refresh_headless": false, // 浏览器刷新无头模式
"browser_refresh_max_retry": 1 // 浏览器刷新最大重试次数
}
```
---
## 号池服务器配置 (`pool_server`)
```json
"pool_server": {
"enable": false, // 是否启用
"mode": "local", // 模式: local/server/client
"server_addr": "", // 服务器地址 (客户端模式)
"listen_addr": ":8000", // 监听地址 (服务器模式)
"secret": "", // 认证密钥
"target_count": 50, // 目标账号数
"client_threads": 2, // 客户端并发线程数
"data_dir": "./data", // 数据目录
"expired_action": "delete" // 过期账号处理方式
}
```
**模式说明**:
- `local`: 本地模式,独立运行
- `server`: 服务器模式,提供号池服务和API
- `client`: 客户端模式,连接服务器接收注册/续期任务
**expired_action 说明**:
- `delete`: 删除过期/失败账号
- `refresh`: 尝试浏览器刷新Cookie
- `queue`: 保留在队列等待重试
---
## Flow 配置 (`flow`)
```json
"flow": {
"enable": false, // 是否启用 Flow 视频生成
"tokens": [], // Flow ST Tokens
"proxy": "", // Flow 专用代理
"timeout": 120, // 超时时间(秒)
"poll_interval": 3, // 轮询间隔(秒)
"max_poll_attempts": 500 // 最大轮询次数
}
```
---
## 其他配置
```json
{
"api_keys": ["key1", "key2"], // API 密钥列表
"listen_addr": ":8000", // 监听地址
"data_dir": "./data", // 数据目录
"default_config": "", // 默认 configId
"debug": false, // 调试模式
"proxy": "http://127.0.0.1:10808" // 全局代理 (兼容旧配置)
}
```
================================================
FILE: config/config.json.example
================================================
{
"api_keys": ["your-api-key-here"],
"listen_addr": ":8000",
"data_dir": "./data",
"default_config": "",
"debug": false,
"pool": {
"target_count": 50,
"min_count": 10,
"check_interval_minutes": 30,
"register_threads": 1,
"register_headless": true,
"refresh_on_startup": true,
"refresh_cooldown_sec": 240,
"use_cooldown_sec": 15,
"max_fail_count": 3,
"enable_browser_refresh": true,
"browser_refresh_headless": true,
"browser_refresh_max_retry": 1
},
"pool_server": {
"enable": false,
"mode": "local",
"server_addr": "http://server-ip:8000",
"listen_addr": ":8000",
"secret": "your-secret-key",
"target_count": 50,
"client_threads": 2,
"data_dir": "./data",
"expired_action": "delete"
},
"proxy_pool": {
"subscribes": [
"http://example.com/s/example"
],
"files": [],
"health_check": true,
"check_on_startup": true
},
"flow": {
"enable": false,
"tokens": [],
"timeout": 120,
"poll_interval": 3,
"max_poll_attempts": 500
}
}
================================================
FILE: docker/docker-compose.yml
================================================
version: '3.8'
services:
business2api:
image: ghcr.io/xxxxteam/business2api:latest
container_name: business2api
restart: unless-stopped
ports:
- "8000:8000"
volumes:
- ./data:/app/data
- ./config.json:/app/config/config.json:ro
environment:
- TZ=Asia/Shanghai
- LISTEN_ADDR=:8000
- DATA_DIR=/app/data
# - PROXY=http://proxy:port
# - API_KEY=your-api-key
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:8000/v1/models"]
interval: 30s
timeout: 10s
retries: 3
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
================================================
FILE: go.mod
================================================
module business2api
go 1.25.0
require (
github.com/fsnotify/fsnotify v1.9.0
github.com/gin-gonic/gin v1.9.1
github.com/go-rod/rod v0.116.2
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3
github.com/sagernet/sing-box v1.12.12
github.com/sagernet/sing-quic v0.5.2-0.20250909083218-00a55617c0fb
golang.org/x/image v0.38.0
)
require (
filippo.io/edwards25519 v1.1.1 // indirect
github.com/ajg/form v1.5.1 // indirect
github.com/akutz/memconn v0.1.0 // indirect
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/anytls/sing-anytls v0.0.11 // indirect
github.com/bits-and-blooms/bitset v1.13.0 // indirect
github.com/bytedance/sonic v1.9.1 // indirect
github.com/caddyserver/certmagic v0.23.0 // indirect
github.com/caddyserver/zerossl v0.1.3 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/coder/websocket v1.8.13 // indirect
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 // indirect
github.com/cretz/bine v0.2.0 // indirect
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gaissmai/bart v0.11.1 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-chi/chi/v5 v5.2.2 // indirect
github.com/go-chi/render v1.0.3 // indirect
github.com/go-json-experiment/json v0.0.0-20250103232110-6a9a0fde9288 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.14.0 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 // indirect
github.com/gofrs/uuid/v5 v5.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 // indirect
github.com/gorilla/csrf v1.7.3-0.20250123201450-9dd6af1f6d30 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/hashicorp/yamux v0.1.2 // indirect
github.com/hdevalence/ed25519consensus v0.2.0 // indirect
github.com/illarion/gonotify/v2 v2.0.3 // indirect
github.com/insomniacslk/dhcp v0.0.0-20250417080101-5f8cf70e8c5f // indirect
github.com/jsimonetti/rtnetlink v1.4.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/libdns/alidns v1.0.5-libdns.v1.beta1 // indirect
github.com/libdns/cloudflare v0.2.2-0.20250708034226-c574dccb31a6 // indirect
github.com/libdns/libdns v1.1.0 // indirect
github.com/logrusorgru/aurora v2.0.3+incompatible // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mdlayher/genetlink v1.3.2 // indirect
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 // indirect
github.com/mdlayher/sdnotify v1.0.0 // indirect
github.com/mdlayher/socket v0.5.1 // indirect
github.com/metacubex/tfo-go v0.0.0-20250921095601-b102db4216c0 // indirect
github.com/metacubex/utls v1.8.3 // indirect
github.com/mholt/acmez/v3 v3.1.2 // indirect
github.com/miekg/dns v1.1.68 // indirect
github.com/mitchellh/go-ps v1.0.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/prometheus-community/pro-bing v0.4.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/safchain/ethtool v0.3.0 // indirect
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a // indirect
github.com/sagernet/cors v1.2.1 // indirect
github.com/sagernet/fswatch v0.1.1 // indirect
github.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb // indirect
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect
github.com/sagernet/nftables v0.3.0-beta.4 // indirect
github.com/sagernet/quic-go v0.52.0-sing-box-mod.3 // indirect
github.com/sagernet/sing v0.7.13 // indirect
github.com/sagernet/sing-mux v0.3.3 // indirect
github.com/sagernet/sing-shadowsocks v0.2.8 // indirect
github.com/sagernet/sing-shadowsocks2 v0.2.1 // indirect
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 // indirect
github.com/sagernet/sing-tun v0.7.3 // indirect
github.com/sagernet/sing-vmess v0.2.7 // indirect
github.com/sagernet/smux v1.5.34-mod.2 // indirect
github.com/sagernet/tailscale v1.80.3-sing-box-1.12-mod.2 // indirect
github.com/sagernet/wireguard-go v0.0.1-beta.7 // indirect
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 // indirect
github.com/stretchr/testify v1.11.1 // indirect
github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e // indirect
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect
github.com/tailscale/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4 // indirect
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 // indirect
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a // indirect
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 // indirect
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc // indirect
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
github.com/vishvananda/netns v0.0.5 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/ysmood/fetchup v0.2.3 // indirect
github.com/ysmood/goob v0.4.0 // indirect
github.com/ysmood/got v0.40.0 // indirect
github.com/ysmood/gson v0.7.3 // indirect
github.com/ysmood/leakless v0.9.0 // indirect
github.com/zeebo/blake3 v0.2.4 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
go.uber.org/zap/exp v0.3.0 // indirect
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect
golang.org/x/mod v0.33.0 // indirect
golang.org/x/net v0.50.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/term v0.40.0 // indirect
golang.org/x/text v0.35.0 // indirect
golang.org/x/time v0.12.0 // indirect
golang.org/x/tools v0.42.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/grpc v1.79.3 // indirect
google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/blake3 v1.4.1 // indirect
)
================================================
FILE: go.sum
================================================
filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw=
filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A=
github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/anytls/sing-anytls v0.0.11 h1:w8e9Uj1oP3m4zxkyZDewPk0EcQbvVxb7Nn+rapEx4fc=
github.com/anytls/sing-anytls v0.0.11/go.mod h1:7rjN6IukwysmdusYsrV51Fgu1uW6vsrdd6ctjnEAln8=
github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE=
github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/caddyserver/certmagic v0.23.0 h1:CfpZ/50jMfG4+1J/u2LV6piJq4HOfO6ppOnOf7DkFEU=
github.com/caddyserver/certmagic v0.23.0/go.mod h1:9mEZIWqqWoI+Gf+4Trh04MOVPD0tGSxtqsxg87hAIH4=
github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA=
github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/cilium/ebpf v0.15.0 h1:7NxJhNiBT3NG8pZJ3c+yfrVdHY8ScgKD27sScgjLMMk=
github.com/cilium/ebpf v0.15.0/go.mod h1:DHp1WyrLeiBh19Cf/tfiSMhqheEiK8fXFZ4No0P1Hso=
github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE=
github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0=
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cretz/bine v0.2.0 h1:8GiDRGlTgz+o8H9DSnsl+5MeBK4HsExxgl6WgzOCuZo=
github.com/cretz/bine v0.2.0/go.mod h1:WU4o9QR9wWp8AVKtTM1XD5vUHkEqnf2vVSo6dBqbetI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa h1:h8TfIT1xc8FWbwwpmHn1J5i43Y0uZP97GqasGCzSRJk=
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa/go.mod h1:Nx87SkVqTKd8UtT+xu7sM/l+LgXs6c0aHrlKusR+2EQ=
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yezR0y7jJ5pceLHthLaYf4bA5T14B6q39S4q2Q=
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/gaissmai/bart v0.11.1 h1:5Uv5XwsaFBRo4E5VBcb9TzY8B7zxFf+U7isDxqOrRfc=
github.com/gaissmai/bart v0.11.1/go.mod h1:KHeYECXQiBjTzQz/om2tqn3sZF1J7hw9m6z41ftj3fg=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I=
github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo=
github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618=
github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
github.com/go-json-experiment/json v0.0.0-20250103232110-6a9a0fde9288 h1:KbX3Z3CgiYlbaavUq3Cj9/MjpO+88S7/AGXzynVDv84=
github.com/go-json-experiment/json v0.0.0-20250103232110-6a9a0fde9288/go.mod h1:BWmvoE1Xia34f3l/ibJweyhrT+aROb/FQ6d+37F0e2s=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-rod/rod v0.116.2 h1:A5t2Ky2A+5eD/ZJQr1EfsQSe5rms5Xof/qj296e+ZqA=
github.com/go-rod/rod v0.116.2/go.mod h1:H+CMO9SCNc2TJ2WfrG+pKhITz57uGNYU43qYHh438Mg=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 h1:sQspH8M4niEijh3PFscJRLDnkL547IeP7kpPe3uUhEg=
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466/go.mod h1:ZiQxhyQ+bbbfxUKVvjfO498oPYvtYhZzycal3G/NHmU=
github.com/gofrs/uuid/v5 v5.3.2 h1:2jfO8j3XgSwlz/wHqemAEugfnTlikAYHhnqQ8Xh4fE0=
github.com/gofrs/uuid/v5 v5.3.2/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 h1:wG8RYIyctLhdFk6Vl1yPGtSRtwGpVkWyZww1OCil2MI=
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/csrf v1.7.3-0.20250123201450-9dd6af1f6d30 h1:fiJdrgVBkjZ5B1HJ2WQwNOaXB+QyYcNXTA3t1XYLz0M=
github.com/gorilla/csrf v1.7.3-0.20250123201450-9dd6af1f6d30/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU=
github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo=
github.com/illarion/gonotify/v2 v2.0.3 h1:B6+SKPo/0Sw8cRJh1aLzNEeNVFfzE3c6N+o+vyxM+9A=
github.com/illarion/gonotify/v2 v2.0.3/go.mod h1:38oIJTgFqupkEydkkClkbL6i5lXV/bxdH9do5TALPEE=
github.com/insomniacslk/dhcp v0.0.0-20250417080101-5f8cf70e8c5f h1:dd33oobuIv9PcBVqvbEiCXEbNTomOHyj3WFuC5YiPRU=
github.com/insomniacslk/dhcp v0.0.0-20250417080101-5f8cf70e8c5f/go.mod h1:zhFlBeJssZ1YBCMZ5Lzu1pX4vhftDvU10WUVb1uXKtM=
github.com/jsimonetti/rtnetlink v1.4.0 h1:Z1BF0fRgcETPEa0Kt0MRk3yV5+kF1FWTni6KUFKrq2I=
github.com/jsimonetti/rtnetlink v1.4.0/go.mod h1:5W1jDvWdnthFJ7fxYX1GMK07BUpI4oskfOqvPteYS6E=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a h1:+RR6SqnTkDLWyICxS1xpjCi/3dhyV+TgZwA6Ww3KncQ=
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/libdns/alidns v1.0.5-libdns.v1.beta1 h1:txHK7UxDed3WFBDjrTZPuMn8X+WmhjBTTAMW5xdy5pQ=
github.com/libdns/alidns v1.0.5-libdns.v1.beta1/go.mod h1:ystHmPwcGoWjPrGpensQSMY9VoCx4cpR2hXNlwk9H/g=
github.com/libdns/cloudflare v0.2.2-0.20250708034226-c574dccb31a6 h1:3MGrVWs2COjMkQR17oUw1zMIPbm2YAzxDC3oGVZvQs8=
github.com/libdns/cloudflare v0.2.2-0.20250708034226-c574dccb31a6/go.mod h1:w9uTmRCDlAoafAsTPnn2nJ0XHK/eaUMh86DUk8BWi60=
github.com/libdns/libdns v1.0.0-beta.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
github.com/libdns/libdns v1.1.0 h1:9ze/tWvt7Df6sbhOJRB8jT33GHEHpEQXdtkE3hPthbU=
github.com/libdns/libdns v1.1.0/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8=
github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 h1:A1Cq6Ysb0GM0tpKMbdCXCIfBclan4oHk1Jb+Hrejirg=
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42/go.mod h1:BB4YCPDOzfy7FniQ/lxuYQ3dgmM2cZumHbK8RpTjN2o=
github.com/mdlayher/sdnotify v1.0.0 h1:Ma9XeLVN/l0qpyx1tNeMSeTjCPH6NtuD6/N9XdTlQ3c=
github.com/mdlayher/sdnotify v1.0.0/go.mod h1:HQUmpM4XgYkhDLtd+Uad8ZFK1T9D5+pNxnXQjCeJlGE=
github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
github.com/metacubex/tfo-go v0.0.0-20250921095601-b102db4216c0 h1:Ui+/2s5Qz0lSnDUBmEL12M5Oi/PzvFxGTNohm8ZcsmE=
github.com/metacubex/tfo-go v0.0.0-20250921095601-b102db4216c0/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw=
github.com/metacubex/utls v1.8.3 h1:0m/yCxm3SK6kWve2lKiFb1pue1wHitJ8sQQD4Ikqde4=
github.com/metacubex/utls v1.8.3/go.mod h1:kncGGVhFaoGn5M3pFe3SXhZCzsbCJayNOH4UEqTKTko=
github.com/mholt/acmez/v3 v3.1.2 h1:auob8J/0FhmdClQicvJvuDavgd5ezwLBfKuYmynhYzc=
github.com/mholt/acmez/v3 v3.1.2/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus-community/pro-bing v0.4.0 h1:YMbv+i08gQz97OZZBwLyvmmQEEzyfyrrjEaAchdy3R4=
github.com/prometheus-community/pro-bing v0.4.0/go.mod h1:b7wRYZtCcPmt4Sz319BykUU241rWLe1VFXyiyWK/dH4=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0=
github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs=
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a h1:+NkI2670SQpQWvkkD2QgdTuzQG263YZ+2emfpeyGqW0=
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a/go.mod h1:63s7jpZqcDAIpj8oI/1v4Izok+npJOHACFCU6+huCkM=
github.com/sagernet/cors v1.2.1 h1:Cv5Z8y9YSD6Gm+qSpNrL3LO4lD3eQVvbFYJSG7JCMHQ=
github.com/sagernet/cors v1.2.1/go.mod h1:O64VyOjjhrkLmQIjF4KGRrJO/5dVXFdpEmCW/eISRAI=
github.com/sagernet/fswatch v0.1.1 h1:YqID+93B7VRfqIH3PArW/XpJv5H4OLEVWDfProGoRQs=
github.com/sagernet/fswatch v0.1.1/go.mod h1:nz85laH0mkQqJfaOrqPpkwtU1znMFNVTpT/5oRsVz/o=
github.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb h1:pprQtDqNgqXkRsXn+0E8ikKOemzmum8bODjSfDene38=
github.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb/go.mod h1:QkkPEJLw59/tfxgapHta14UL5qMUah5NXhO0Kw2Kan4=
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis=
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I=
github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8=
github.com/sagernet/quic-go v0.52.0-sing-box-mod.3 h1:ySqffGm82rPqI1TUPqmtHIYd12pfEGScygnOxjTL56w=
github.com/sagernet/quic-go v0.52.0-sing-box-mod.3/go.mod h1:OV+V5kEBb8kJS7k29MzDu6oj9GyMc7HA07sE1tedxz4=
github.com/sagernet/sing v0.6.9/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing v0.7.13 h1:XNYgd8e3cxMULs/LLJspdn/deHrnPWyrrglNHeCUAYM=
github.com/sagernet/sing v0.7.13/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-box v1.12.12 h1:brSb4zdL5CfqXB0ss1jrT+srPUbKanyWhbswkte/z5Y=
github.com/sagernet/sing-box v1.12.12/go.mod h1:ObMeEc1VAcJdXN6B/3SUIJRuK7J38m0W6yLNJ+E5f+0=
github.com/sagernet/sing-mux v0.3.3 h1:YFgt9plMWzH994BMZLmyKL37PdIVaIilwP0Jg+EcLfw=
github.com/sagernet/sing-mux v0.3.3/go.mod h1:pht8iFY4c9Xltj7rhVd208npkNaeCxzyXCgulDPLUDA=
github.com/sagernet/sing-quic v0.5.2-0.20250909083218-00a55617c0fb h1:5Wx3XeTiKrrrcrAky7Hc1bO3CGxrvho2Vu5b/adlEIM=
github.com/sagernet/sing-quic v0.5.2-0.20250909083218-00a55617c0fb/go.mod h1:evP1e++ZG8TJHVV5HudXV4vWeYzGfCdF4HwSJZcdqkI=
github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE=
github.com/sagernet/sing-shadowsocks v0.2.8/go.mod h1:lo7TWEMDcN5/h5B8S0ew+r78ZODn6SwVaFhvB6H+PTI=
github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnqqs2gQ2/Qioo=
github.com/sagernet/sing-shadowsocks2 v0.2.1/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ=
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 h1:tK+75l64tm9WvEFrYRE1t0YxoFdWQqw/h7Uhzj0vJ+w=
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA=
github.com/sagernet/sing-tun v0.7.3 h1:MFnAir+l24ElEyxdfwtY8mqvUUL9nPnL9TDYLkOmVes=
github.com/sagernet/sing-tun v0.7.3/go.mod h1:pUEjh9YHQ2gJT6Lk0TYDklh3WJy7lz+848vleGM3JPM=
github.com/sagernet/sing-vmess v0.2.7 h1:2ee+9kO0xW5P4mfe6TYVWf9VtY8k1JhNysBqsiYj0sk=
github.com/sagernet/sing-vmess v0.2.7/go.mod h1:5aYoOtYksAyS0NXDm0qKeTYW1yoE1bJVcv+XLcVoyJs=
github.com/sagernet/smux v1.5.34-mod.2 h1:gkmBjIjlJ2zQKpLigOkFur5kBKdV6bNRoFu2WkltRQ4=
github.com/sagernet/smux v1.5.34-mod.2/go.mod h1:0KW0+R+ycvA2INW4gbsd7BNyg+HEfLIAxa5N02/28Zc=
github.com/sagernet/tailscale v1.80.3-sing-box-1.12-mod.2 h1:MO7s4ni2bSfAOhcan2rdQSWCztkMXmqyg6jYPZp8bEE=
github.com/sagernet/tailscale v1.80.3-sing-box-1.12-mod.2/go.mod h1:EBxXsWu4OH2ELbQLq32WoBeIubG8KgDrg4/Oaxjs6lI=
github.com/sagernet/wireguard-go v0.0.1-beta.7 h1:ltgBwYHfr+9Wz1eG59NiWnHrYEkDKHG7otNZvu85DXI=
github.com/sagernet/wireguard-go v0.0.1-beta.7/go.mod h1:jGXij2Gn2wbrWuYNUmmNhf1dwcZtvyAvQoe8Xd8MbUo=
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc=
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854/go.mod h1:LtfoSK3+NG57tvnVEHgcuBW9ujgE8enPSgzgwStwCAA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e h1:PtWT87weP5LWHEY//SWsYkSO3RWRZo4OSWagh3YD2vQ=
github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e/go.mod h1:XrBNfAFN+pwoWuksbFS9Ccxnopa15zJGgXRFN90l3K4=
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4=
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg=
github.com/tailscale/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4 h1:rXZGgEa+k2vJM8xT0PoSKfVXwFGPQ3z3CJfmnHJkZZw=
github.com/tailscale/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4/go.mod h1:ikbF+YT089eInTp9f2vmvy4+ZVnW5hzX1q2WknxSprQ=
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPxRs2O36yuGRW3f9SYV+bMTTvMBI0EKio=
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8=
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29XwJucQo73FrleVK6t4kYz4NVhp34Yw=
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8=
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 h1:uFsXVBE9Qr4ZoF094vE6iYTLDl0qCiKzYXlL6UeWObU=
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0=
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc h1:24heQPtnFR+yfntqhI3oAu9i27nEojcQ4NuBQOo5ZFA=
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc/go.mod h1:f93CXfllFsO9ZQVq+Zocb1Gp4G5Fz0b0rXHLOzt/Djc=
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 h1:UBPHPtv8+nEAy2PD8RyAhOYvau1ek0HDJqLS/Pysi14=
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ=
github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA=
github.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM=
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/ysmood/fetchup v0.2.3 h1:ulX+SonA0Vma5zUFXtv52Kzip/xe7aj4vqT5AJwQ+ZQ=
github.com/ysmood/fetchup v0.2.3/go.mod h1:xhibcRKziSvol0H1/pj33dnKrYyI2ebIvz5cOOkYGns=
github.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ=
github.com/ysmood/goob v0.4.0/go.mod h1:u6yx7ZhS4Exf2MwciFr6nIM8knHQIE22lFpWHnfql18=
github.com/ysmood/gop v0.2.0 h1:+tFrG0TWPxT6p9ZaZs+VY+opCvHU8/3Fk6BaNv6kqKg=
github.com/ysmood/gop v0.2.0/go.mod h1:rr5z2z27oGEbyB787hpEcx4ab8cCiPnKxn0SUHt6xzk=
github.com/ysmood/got v0.40.0 h1:ZQk1B55zIvS7zflRrkGfPDrPG3d7+JOza1ZkNxcc74Q=
github.com/ysmood/got v0.40.0/go.mod h1:W7DdpuX6skL3NszLmAsC5hT7JAhuLZhByVzHTq874Qg=
github.com/ysmood/gotrace v0.6.0 h1:SyI1d4jclswLhg7SWTL6os3L1WOKeNn/ZtzVQF8QmdY=
github.com/ysmood/gotrace v0.6.0/go.mod h1:TzhIG7nHDry5//eYZDYcTzuJLYQIkykJzCRIo4/dzQM=
github.com/ysmood/gson v0.7.3 h1:QFkWbTH8MxyUTKPkVWAENJhxqdBa4lYTQWqZCiLG6kE=
github.com/ysmood/gson v0.7.3/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg=
github.com/ysmood/leakless v0.9.0 h1:qxCG5VirSBvmi3uynXFkcnLMzkphdh3xx5FtrORwDCU=
github.com/ysmood/leakless v0.9.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ=
github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE=
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4hOxG5YpKCzkek=
go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
golang.org/x/image v0.38.0 h1:5l+q+Y9JDC7mBOMjo4/aPhMDcxEptsX+Tt3GgRQRPuE=
golang.org/x/image v0.38.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE=
golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg=
lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k=
software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=
================================================
FILE: main.go
================================================
package main
import (
"bytes"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"image"
_ "image/gif"
_ "image/jpeg"
"image/png"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"sync/atomic"
"time"
_ "golang.org/x/image/bmp"
_ "golang.org/x/image/tiff"
_ "golang.org/x/image/webp"
"github.com/fsnotify/fsnotify"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"business2api/src/flow"
"business2api/src/logger"
"business2api/src/pool"
"business2api/src/proxy"
"business2api/src/register"
"business2api/src/utils"
)
// ==================== 配置结构 ====================
type PoolConfig struct {
TargetCount int `json:"target_count"` // 目标账号数量
MinCount int `json:"min_count"` // 最小账号数,低于此值触发注册
CheckIntervalMinutes int `json:"check_interval_minutes"` // 检查间隔(分钟)
RegisterThreads int `json:"register_threads"` // 注册线程数
RegisterHeadless bool `json:"register_headless"` // 无头模式
RefreshOnStartup bool `json:"refresh_on_startup"` // 启动时刷新账号
RefreshCooldownSec int `json:"refresh_cooldown_sec"` // 刷新冷却时间(秒)
UseCooldownSec int `json:"use_cooldown_sec"` // 使用冷却时间(秒)
MaxFailCount int `json:"max_fail_count"` // 最大连续失败次数
EnableBrowserRefresh bool `json:"enable_browser_refresh"` // 启用浏览器刷新401账号
BrowserRefreshHeadless bool `json:"browser_refresh_headless"` // 浏览器刷新无头模式
BrowserRefreshMaxRetry int `json:"browser_refresh_max_retry"` // 浏览器刷新最大重试次数(0=禁用)
AutoDelete401 bool `json:"auto_delete_401"` // 401时自动删除账号
}
// FlowConfig Flow 服务配置
type FlowConfigSection struct {
Enable bool `json:"enable"` // 是否启用 Flow
Tokens []string `json:"tokens"` // Flow ST Tokens
Proxy string `json:"proxy"` // Flow 专用代理
Timeout int `json:"timeout"` // 超时时间
PollInterval int `json:"poll_interval"` // 轮询间隔
MaxPollAttempts int `json:"max_poll_attempts"` // 最大轮询次数
}
// ProxyConfig 代理配置
type ProxyConfig struct {
Proxy string `json:"proxy"` // 单个代理 (http/socks5)
Subscribes []string `json:"subscribes"` // 订阅链接列表
Files []string `json:"files"` // 代理文件列表
HealthCheck bool `json:"health_check"` // 是否启用健康检查
CheckOnStartup bool `json:"check_on_startup"` // 启动时检查
}
type AppConfig struct {
APIKeys []string `json:"api_keys"` // API 密钥列表
ListenAddr string `json:"listen_addr"` // 监听地址
DataDir string `json:"data_dir"` // 数据目录
Pool PoolConfig `json:"pool"` // 号池配置
Proxy string `json:"proxy"` // 代理 (兼容旧配置)
ProxySubscribe string `json:"proxy_subscribe"` // 代理订阅链接 (兼容旧配置)
ProxyPool ProxyConfig `json:"proxy_pool"` // 代理池配置
DefaultConfig string `json:"default_config"` // 默认 configId
PoolServer pool.PoolServerConfig `json:"pool_server"` // 号池服务器配置
Debug bool `json:"debug"` // 调试模式
Flow FlowConfigSection `json:"flow"` // Flow 配置
Note []string `json:"note"` // 备注信息(支持多行)
}
// PoolMode 号池模式
type PoolMode int
const (
PoolModeLocal PoolMode = iota // 本地模式
PoolModeServer // 服务器模式(提供号池服务)
PoolModeClient // 客户端模式(使用远程号池)
)
var (
poolMode PoolMode
remotePoolClient *pool.RemotePoolClient
flowClient *flow.FlowClient
flowHandler *flow.GenerationHandler
flowTokenPool *flow.TokenPool
)
// 配置热重载相关
var (
configMu sync.RWMutex // 配置读写锁
configWatcher *fsnotify.Watcher // 配置文件监听器
configPath = "config/config.json" // 配置文件路径
)
// APIStats API 调用统计
type APIStats struct {
mu sync.RWMutex
startTime time.Time // 服务启动时间
totalRequests int64 // 总请求数
successRequests int64 // 成功请求数
failedRequests int64 // 失败请求数
inputTokens int64 // 输入 tokens
outputTokens int64 // 输出 tokens
imageGenerated int64 // 生成的图片数
videoGenerated int64 // 生成的视频数
requestTimes []time.Time // 最近请求时间(用于计算 RPM)
modelStats map[string]*ModelStats // 每个模型的统计
hourlyStats [24]HourlyStats // 24小时统计
lastHour int // 上次记录的小时
}
// ModelStats 模型统计
type ModelStats struct {
Requests int64 `json:"requests"`
Success int64 `json:"success"`
InputTokens int64 `json:"input_tokens"`
OutputTokens int64 `json:"output_tokens"`
Images int64 `json:"images"`
}
// HourlyStats 小时统计
type HourlyStats struct {
Hour int `json:"hour"`
Requests int64 `json:"requests"`
Success int64 `json:"success"`
InputTokens int64 `json:"input_tokens"`
OutputTokens int64 `json:"output_tokens"`
}
var apiStats = &APIStats{
startTime: time.Now(),
requestTimes: make([]time.Time, 0, 1000),
modelStats: make(map[string]*ModelStats),
lastHour: time.Now().Hour(),
}
// IPStats IP请求统计
type IPStats struct {
mu sync.RWMutex
ipRequests map[string]*IPRequestInfo
}
// IPRequestInfo 单个IP的请求信息
type IPRequestInfo struct {
IP string `json:"ip"`
TotalCount int64 `json:"total_count"`
SuccessCount int64 `json:"success_count"`
FailedCount int64 `json:"failed_count"`
InputTokens int64 `json:"input_tokens"`
OutputTokens int64 `json:"output_tokens"`
ImagesCount int64 `json:"images_count"`
VideosCount int64 `json:"videos_count"`
FirstSeen time.Time `json:"first_seen"`
LastSeen time.Time `json:"last_seen"`
RequestTimes []time.Time `json:"-"` // 用于计算RPM
Models map[string]int64 `json:"models"`
UserAgents map[string]int64 `json:"user_agents,omitempty"`
}
var ipStats = &IPStats{
ipRequests: make(map[string]*IPRequestInfo),
}
// RecordIPRequest 记录IP请求(包含tokens、图片、视频统计)
func (s *IPStats) RecordIPRequest(ip, model, userAgent string, success bool, inputTokens, outputTokens, images, videos int64) {
s.mu.Lock()
defer s.mu.Unlock()
now := time.Now()
info, exists := s.ipRequests[ip]
if !exists {
info = &IPRequestInfo{
IP: ip,
FirstSeen: now,
Models: make(map[string]int64),
UserAgents: make(map[string]int64),
RequestTimes: make([]time.Time, 0, 100),
}
s.ipRequests[ip] = info
}
info.TotalCount++
info.LastSeen = now
info.InputTokens += inputTokens
info.OutputTokens += outputTokens
info.ImagesCount += images
info.VideosCount += videos
// 记录请求时间用于计算RPM(保留最近100条)
info.RequestTimes = append(info.RequestTimes, now)
if len(info.RequestTimes) > 100 {
info.RequestTimes = info.RequestTimes[len(info.RequestTimes)-100:]
}
if success {
info.SuccessCount++
} else {
info.FailedCount++
}
if model != "" {
info.Models[model]++
}
if userAgent != "" && len(info.UserAgents) < 50 {
info.UserAgents[userAgent]++
}
}
// GetIPRPM 计算单个IP的RPM
func (info *IPRequestInfo) GetRPM() float64 {
oneMinuteAgo := time.Now().Add(-time.Minute)
count := 0
for i := len(info.RequestTimes) - 1; i >= 0; i-- {
if info.RequestTimes[i].After(oneMinuteAgo) {
count++
} else {
break
}
}
return float64(count)
}
func (s *IPStats) GetAllIPStats() map[string]interface{} {
s.mu.RLock()
defer s.mu.RUnlock()
type ipSortInfo struct {
IP string
Count int64
}
sorted := make([]ipSortInfo, 0, len(s.ipRequests))
for ip, info := range s.ipRequests {
sorted = append(sorted, ipSortInfo{IP: ip, Count: info.TotalCount})
}
n := len(sorted)
for i := 1; i < n; i++ {
for j := i; j > 0 && sorted[j].Count > sorted[j-1].Count; j-- {
sorted[j], sorted[j-1] = sorted[j-1], sorted[j]
}
}
var totalRequests, totalSuccess, totalFailed int64
var totalInputTokens, totalOutputTokens int64
var totalImages, totalVideos int64
ips := make([]map[string]interface{}, 0, n)
for i := 0; i < n; i++ {
info := s.ipRequests[sorted[i].IP]
rpm := info.GetRPM()
totalRequests += info.TotalCount
totalSuccess += info.SuccessCount
totalFailed += info.FailedCount
totalInputTokens += info.InputTokens
totalOutputTokens += info.OutputTokens
totalImages += info.ImagesCount
totalVideos += info.VideosCount
ips = append(ips, map[string]interface{}{
"ip": info.IP,
"total_count": info.TotalCount,
"success_count": info.SuccessCount,
"failed_count": info.FailedCount,
"success_rate": fmt.Sprintf("%.1f%%", float64(info.SuccessCount)/float64(max(info.TotalCount, 1))*100),
"input_tokens": info.InputTokens,
"output_tokens": info.OutputTokens,
"total_tokens": info.InputTokens + info.OutputTokens,
"images": info.ImagesCount,
"videos": info.VideosCount,
"rpm": rpm,
"first_seen": info.FirstSeen.Format(time.RFC3339),
"last_seen": info.LastSeen.Format(time.RFC3339),
"models": info.Models,
"user_agents": info.UserAgents,
})
}
return map[string]interface{}{
"server_time": time.Now().Format(time.RFC3339),
"unique_ips": n,
"total_requests": totalRequests,
"total_success": totalSuccess,
"total_failed": totalFailed,
"total_input_tokens": totalInputTokens,
"total_output_tokens": totalOutputTokens,
"total_tokens": totalInputTokens + totalOutputTokens,
"total_images": totalImages,
"total_videos": totalVideos,
"ips": ips,
}
}
// GetIPDetail 获取单个IP的详细信息
func (s *IPStats) GetIPDetail(ip string) *IPRequestInfo {
s.mu.RLock()
defer s.mu.RUnlock()
return s.ipRequests[ip]
}
// RecordRequest 记录请求
func (s *APIStats) RecordRequest(success bool, inputTokens, outputTokens, images, videos int64) {
s.RecordRequestWithModel("", success, inputTokens, outputTokens, images, videos)
}
func (s *APIStats) RecordRequestWithModel(model string, success bool, inputTokens, outputTokens, images, videos int64) {
s.mu.Lock()
defer s.mu.Unlock()
s.totalRequests++
if success {
s.successRequests++
} else {
s.failedRequests++
}
s.inputTokens += inputTokens
s.outputTokens += outputTokens
s.imageGenerated += images
s.videoGenerated += videos
// 记录请求时间(保留最近1000条)
now := time.Now()
s.requestTimes = append(s.requestTimes, now)
if len(s.requestTimes) > 1000 {
s.requestTimes = s.requestTimes[len(s.requestTimes)-1000:]
}
// 模型统计
if model != "" {
if s.modelStats[model] == nil {
s.modelStats[model] = &ModelStats{}
}
ms := s.modelStats[model]
ms.Requests++
if success {
ms.Success++
}
ms.InputTokens += inputTokens
ms.OutputTokens += outputTokens
ms.Images += images
}
// 小时统计
currentHour := now.Hour()
if currentHour != s.lastHour {
// 新的小时,重置该小时统计
s.hourlyStats[currentHour] = HourlyStats{Hour: currentHour}
s.lastHour = currentHour
}
hs := &s.hourlyStats[currentHour]
hs.Requests++
if success {
hs.Success++
}
hs.InputTokens += inputTokens
hs.OutputTokens += outputTokens
}
func (s *APIStats) GetRPM() float64 {
s.mu.RLock()
defer s.mu.RUnlock()
oneMinuteAgo := time.Now().Add(-time.Minute)
count := 0
for i := len(s.requestTimes) - 1; i >= 0; i-- {
if s.requestTimes[i].After(oneMinuteAgo) {
count++
} else {
break
}
}
return float64(count)
}
// GetStats 获取统计数据
func (s *APIStats) GetStats() map[string]interface{} {
s.mu.RLock()
defer s.mu.RUnlock()
uptime := time.Since(s.startTime)
avgRPM := float64(0)
if uptime.Minutes() > 0 {
avgRPM = float64(s.totalRequests) / uptime.Minutes()
}
return map[string]interface{}{
"uptime": uptime.String(),
"uptime_seconds": int64(uptime.Seconds()),
"total_requests": s.totalRequests,
"success_requests": s.successRequests,
"failed_requests": s.failedRequests,
"success_rate": fmt.Sprintf("%.2f%%", float64(s.successRequests)/float64(max(s.totalRequests, 1))*100),
"input_tokens": s.inputTokens,
"output_tokens": s.outputTokens,
"total_tokens": s.inputTokens + s.outputTokens,
"images_generated": s.imageGenerated,
"videos_generated": s.videoGenerated,
"current_rpm": s.GetRPM(),
"average_rpm": fmt.Sprintf("%.2f", avgRPM),
}
}
// GetDetailedStats 获取详细统计数据
func (s *APIStats) GetDetailedStats() map[string]interface{} {
s.mu.RLock()
defer s.mu.RUnlock()
uptime := time.Since(s.startTime)
avgRPM := float64(0)
if uptime.Minutes() > 0 {
avgRPM = float64(s.totalRequests) / uptime.Minutes()
}
// 转换模型统计
modelStatsMap := make(map[string]interface{})
for model, ms := range s.modelStats {
modelStatsMap[model] = map[string]interface{}{
"requests": ms.Requests,
"success": ms.Success,
"success_rate": fmt.Sprintf("%.2f%%", float64(ms.Success)/float64(max(ms.Requests, 1))*100),
"input_tokens": ms.InputTokens,
"output_tokens": ms.OutputTokens,
"total_tokens": ms.InputTokens + ms.OutputTokens,
"images": ms.Images,
}
}
// 转换小时统计
hourlyStatsArr := make([]map[string]interface{}, 0, 24)
for i := 0; i < 24; i++ {
hs := s.hourlyStats[i]
if hs.Requests > 0 {
hourlyStatsArr = append(hourlyStatsArr, map[string]interface{}{
"hour": i,
"requests": hs.Requests,
"success": hs.Success,
"input_tokens": hs.InputTokens,
"output_tokens": hs.OutputTokens,
})
}
}
return map[string]interface{}{
"uptime": uptime.String(),
"uptime_seconds": int64(uptime.Seconds()),
"total_requests": s.totalRequests,
"success_requests": s.successRequests,
"failed_requests": s.failedRequests,
"success_rate": fmt.Sprintf("%.2f%%", float64(s.successRequests)/float64(max(s.totalRequests, 1))*100),
"input_tokens": s.inputTokens,
"output_tokens": s.outputTokens,
"total_tokens": s.inputTokens + s.outputTokens,
"images_generated": s.imageGenerated,
"videos_generated": s.videoGenerated,
"current_rpm": s.GetRPM(),
"average_rpm": fmt.Sprintf("%.2f", avgRPM),
"models": modelStatsMap,
"hourly": hourlyStatsArr,
}
}
func max(a, b int64) int64 {
if a > b {
return a
}
return b
}
var appConfig = AppConfig{
ListenAddr: ":8000",
DataDir: "./data",
Pool: PoolConfig{
TargetCount: 50,
MinCount: 10,
CheckIntervalMinutes: 30,
RegisterThreads: 1,
RegisterHeadless: false,
RefreshOnStartup: true,
RefreshCooldownSec: 240, // 4分钟
UseCooldownSec: 15, // 15秒
MaxFailCount: 3,
EnableBrowserRefresh: true, // 默认启用浏览器刷新
BrowserRefreshHeadless: false,
BrowserRefreshMaxRetry: 1, // 浏览器刷新最多重试1次
},
}
// GetAPIKeys 线程安全获取 API Keys
func GetAPIKeys() []string {
configMu.RLock()
defer configMu.RUnlock()
keys := make([]string, len(appConfig.APIKeys))
copy(keys, appConfig.APIKeys)
return keys
}
// reloadConfig 重新加载配置文件(热重载)
func reloadConfig() error {
data, err := os.ReadFile(configPath)
if err != nil {
return fmt.Errorf("读取配置文件失败: %w", err)
}
var newConfig AppConfig
if err := json.Unmarshal(data, &newConfig); err != nil {
return fmt.Errorf("解析配置文件失败: %w", err)
}
configMu.Lock()
oldAPIKeys := appConfig.APIKeys
oldDebug := appConfig.Debug
oldPoolConfig := appConfig.Pool
// 更新可热重载的配置项
appConfig.APIKeys = newConfig.APIKeys
appConfig.Debug = newConfig.Debug
appConfig.Note = newConfig.Note
// 更新号池配置
appConfig.Pool.RefreshCooldownSec = newConfig.Pool.RefreshCooldownSec
appConfig.Pool.UseCooldownSec = newConfig.Pool.UseCooldownSec
appConfig.Pool.MaxFailCount = newConfig.Pool.MaxFailCount
appConfig.Pool.EnableBrowserRefresh = newConfig.Pool.EnableBrowserRefresh
appConfig.Pool.BrowserRefreshHeadless = newConfig.Pool.BrowserRefreshHeadless
appConfig.Pool.BrowserRefreshMaxRetry = newConfig.Pool.BrowserRefreshMaxRetry
appConfig.Pool.AutoDelete401 = newConfig.Pool.AutoDelete401
configMu.Unlock()
// 应用变更
applyConfigChanges(oldAPIKeys, oldDebug, oldPoolConfig, newConfig)
return nil
}
// applyConfigChanges 应用配置变更
func applyConfigChanges(oldAPIKeys []string, oldDebug bool, oldPoolConfig PoolConfig, newConfig AppConfig) {
// 日志模式变更
if oldDebug != newConfig.Debug {
logger.SetDebugMode(newConfig.Debug)
logger.Info("🔄 调试模式: %v -> %v", oldDebug, newConfig.Debug)
}
// API Keys 变更
if len(oldAPIKeys) != len(newConfig.APIKeys) {
logger.Info("🔄 API Keys 数量: %d -> %d", len(oldAPIKeys), len(newConfig.APIKeys))
}
// 号池配置变更
if oldPoolConfig.RefreshCooldownSec != newConfig.Pool.RefreshCooldownSec ||
oldPoolConfig.UseCooldownSec != newConfig.Pool.UseCooldownSec {
pool.SetCooldowns(newConfig.Pool.RefreshCooldownSec, newConfig.Pool.UseCooldownSec)
logger.Info("🔄 冷却配置已更新: refresh=%ds, use=%ds",
newConfig.Pool.RefreshCooldownSec, newConfig.Pool.UseCooldownSec)
}
if newConfig.Pool.MaxFailCount > 0 {
pool.MaxFailCount = newConfig.Pool.MaxFailCount
}
pool.EnableBrowserRefresh = newConfig.Pool.EnableBrowserRefresh
pool.BrowserRefreshHeadless = newConfig.Pool.BrowserRefreshHeadless
if newConfig.Pool.BrowserRefreshMaxRetry >= 0 {
pool.BrowserRefreshMaxRetry = newConfig.Pool.BrowserRefreshMaxRetry
}
pool.AutoDelete401 = newConfig.Pool.AutoDelete401
logger.Info("✅ 配置热重载完成")
}
// startConfigWatcher 启动配置文件监听
func startConfigWatcher() error {
watcher, err := fsnotify.NewWatcher()
if err != nil {
return fmt.Errorf("创建配置监听器失败: %w", err)
}
configWatcher = watcher
go configWatchLoop()
// 监听配置目录
configDir := filepath.Dir(configPath)
if err := watcher.Add(configDir); err != nil {
return fmt.Errorf("添加配置目录监听失败: %w", err)
}
logger.Info("🔄 配置文件热重载已启用: %s", configPath)
return nil
}
// configWatchLoop 配置文件监听循环
func configWatchLoop() {
var lastReload time.Time
const debounceDelay = 500 * time.Millisecond
for {
select {
case event, ok := <-configWatcher.Events:
if !ok {
return
}
// 只关注配置文件
if filepath.Base(event.Name) != "config.json" {
continue
}
// 只处理写入和创建事件
if event.Op&(fsnotify.Write|fsnotify.Create) == 0 {
continue
}
// 防抖:避免短时间内多次触发
if time.Since(lastReload) < debounceDelay {
continue
}
lastReload = time.Now()
// 等待文件写入完成
time.Sleep(100 * time.Millisecond)
logger.Info("📝 检测到配置文件变更,正在重载...")
if err := reloadConfig(); err != nil {
logger.Error("❌ 配置重载失败: %v", err)
}
case err, ok := <-configWatcher.Errors:
if !ok {
return
}
logger.Error("❌ 配置监听错误: %v", err)
}
}
}
// stopConfigWatcher 停止配置文件监听
func stopConfigWatcher() {
if configWatcher != nil {
configWatcher.Close()
}
}
var (
DataDir string
Proxy string
ListenAddr string
DefaultConfig string
JwtTTL = 270 * time.Second
)
// mergeConfig 合并配置:loaded 中有值的字段覆盖 base 中的默认值
func mergeConfig(base, loaded *AppConfig) {
// 基本字段
if len(loaded.APIKeys) > 0 {
base.APIKeys = loaded.APIKeys
}
if loaded.ListenAddr != "" {
base.ListenAddr = loaded.ListenAddr
}
if loaded.DataDir != "" {
base.DataDir = loaded.DataDir
}
if loaded.Proxy != "" {
base.Proxy = loaded.Proxy
}
if loaded.DefaultConfig != "" {
base.DefaultConfig = loaded.DefaultConfig
}
// Debug 是 bool,直接覆盖
base.Debug = loaded.Debug
// Pool 配置
if loaded.Pool.TargetCount > 0 {
base.Pool.TargetCount = loaded.Pool.TargetCount
}
if loaded.Pool.MinCount > 0 {
base.Pool.MinCount = loaded.Pool.MinCount
}
if loaded.Pool.CheckIntervalMinutes > 0 {
base.Pool.CheckIntervalMinutes = loaded.Pool.CheckIntervalMinutes
}
if loaded.Pool.RegisterThreads > 0 {
base.Pool.RegisterThreads = loaded.Pool.RegisterThreads
}
// bool 字段直接覆盖
base.Pool.RegisterHeadless = loaded.Pool.RegisterHeadless
base.Pool.RefreshOnStartup = loaded.Pool.RefreshOnStartup
base.Pool.EnableBrowserRefresh = loaded.Pool.EnableBrowserRefresh
base.Pool.BrowserRefreshHeadless = loaded.Pool.BrowserRefreshHeadless
base.Pool.AutoDelete401 = loaded.Pool.AutoDelete401
if loaded.Pool.RefreshCooldownSec > 0 {
base.Pool.RefreshCooldownSec = loaded.Pool.RefreshCooldownSec
}
if loaded.Pool.UseCooldownSec > 0 {
base.Pool.UseCooldownSec = loaded.Pool.UseCooldownSec
}
if loaded.Pool.MaxFailCount > 0 {
base.Pool.MaxFailCount = loaded.Pool.MaxFailCount
}
if loaded.Pool.BrowserRefreshMaxRetry > 0 {
base.Pool.BrowserRefreshMaxRetry = loaded.Pool.BrowserRefreshMaxRetry
}
// PoolServer 配置
base.PoolServer = loaded.PoolServer
// Flow 配置
base.Flow = loaded.Flow
// ProxyPool 配置
if len(loaded.ProxyPool.Subscribes) > 0 {
base.ProxyPool.Subscribes = loaded.ProxyPool.Subscribes
}
if len(loaded.ProxyPool.Files) > 0 {
base.ProxyPool.Files = loaded.ProxyPool.Files
}
if loaded.ProxyPool.Proxy != "" {
base.ProxyPool.Proxy = loaded.ProxyPool.Proxy
}
base.ProxyPool.HealthCheck = loaded.ProxyPool.HealthCheck
base.ProxyPool.CheckOnStartup = loaded.ProxyPool.CheckOnStartup
// Note
if len(loaded.Note) > 0 {
base.Note = loaded.Note
}
}
// 保存默认配置到文件
func saveDefaultConfig(configPath string) error {
// 确保目录存在
dir := filepath.Dir(configPath)
if err := os.MkdirAll(dir, 0755); err != nil {
return err
}
data, err := json.MarshalIndent(appConfig, "", " ")
if err != nil {
return err
}
return os.WriteFile(configPath, data, 0644)
}
func loadAppConfig() {
// 尝试加载配置文件
configPath := "config/config.json"
if data, err := os.ReadFile(configPath); err == nil {
// 保留默认值,仅覆盖配置文件中存在的字段
var loadedConfig AppConfig
if err := json.Unmarshal(data, &loadedConfig); err != nil {
logger.Warn("⚠️ 解析配置文件失败: %v,使用默认配置", err)
} else {
// 合并配置:配置文件中有的字段覆盖默认值,没有的保留默认值
mergeConfig(&appConfig, &loadedConfig)
logger.Info("✅ 加载配置文件: %s", configPath)
}
} else if os.IsNotExist(err) {
// 配置文件不存在,创建默认配置
logger.Warn("⚠️ 配置文件不存在,创建默认配置: %s", configPath)
if err := saveDefaultConfig(configPath); err != nil {
logger.Error("❌ 创建默认配置失败: %v", err)
}
}
if v := os.Getenv("DATA_DIR"); v != "" {
appConfig.DataDir = v
}
if v := os.Getenv("PROXY"); v != "" {
appConfig.Proxy = v
}
if v := os.Getenv("LISTEN_ADDR"); v != "" {
appConfig.ListenAddr = v
}
if v := os.Getenv("CONFIG_ID"); v != "" {
appConfig.DefaultConfig = v
}
if v := os.Getenv("API_KEY"); v != "" {
appConfig.APIKeys = append(appConfig.APIKeys, v)
}
// 设置全局变量
DataDir = appConfig.DataDir
Proxy = appConfig.Proxy
ListenAddr = appConfig.ListenAddr
DefaultConfig = appConfig.DefaultConfig
// 应用调试模式
logger.SetDebugMode(appConfig.Debug)
// 应用号池配置
pool.SetCooldowns(appConfig.Pool.RefreshCooldownSec, appConfig.Pool.UseCooldownSec)
if appConfig.Pool.MaxFailCount > 0 {
pool.MaxFailCount = appConfig.Pool.MaxFailCount
}
pool.EnableBrowserRefresh = appConfig.Pool.EnableBrowserRefresh
pool.BrowserRefreshHeadless = appConfig.Pool.BrowserRefreshHeadless
if appConfig.Pool.BrowserRefreshMaxRetry >= 0 {
pool.BrowserRefreshMaxRetry = appConfig.Pool.BrowserRefreshMaxRetry
}
pool.AutoDelete401 = appConfig.Pool.AutoDelete401
// 服务端模式下,如果 expired_action 是 delete,则同步设置 AutoDelete401
if appConfig.PoolServer.Enable && appConfig.PoolServer.Mode == "server" && appConfig.PoolServer.ExpiredAction == "delete" {
pool.AutoDelete401 = true
logger.Info("🗑️ 服务端模式 expired_action=delete,启用 AutoDelete401")
}
pool.DataDir = DataDir
pool.DefaultConfig = DefaultConfig
pool.Proxy = Proxy
register.DataDir = DataDir
register.TargetCount = appConfig.Pool.TargetCount
register.MinCount = appConfig.Pool.MinCount
register.CheckInterval = time.Duration(appConfig.Pool.CheckIntervalMinutes) * time.Minute
register.Threads = appConfig.Pool.RegisterThreads
register.Headless = appConfig.Pool.RegisterHeadless
register.Proxy = Proxy
// 初始化代理池
initProxyPool()
if pool.EnableBrowserRefresh && pool.BrowserRefreshMaxRetry > 0 {
logger.Info("🌐 浏览器刷新已启用 (headless=%v, 最大重试=%d)", pool.BrowserRefreshHeadless, pool.BrowserRefreshMaxRetry)
} else if pool.EnableBrowserRefresh {
logger.Info("🌐 浏览器刷新已禁用 (max_retry=0)")
pool.EnableBrowserRefresh = false
}
// 初始化 Flow 客户端
initFlowClient()
}
// initFlowClient 初始化 Flow 客户端
func initFlowClient() {
if !appConfig.Flow.Enable {
logger.Info("📹 Flow 服务已禁用")
return
}
cfg := flow.FlowConfig{
Proxy: appConfig.Flow.Proxy,
Timeout: appConfig.Flow.Timeout,
PollInterval: appConfig.Flow.PollInterval,
MaxPollAttempts: appConfig.Flow.MaxPollAttempts,
}
if cfg.Proxy == "" {
cfg.Proxy = Proxy
}
flowClient = flow.NewFlowClient(cfg)
// 初始化 Token 池
flowTokenPool = flow.NewTokenPool(DataDir, flowClient)
// 从 data/at 目录加载 Token
loadedFromDir, err := flowTokenPool.LoadFromDir()
if err != nil {
logger.Warn("⚠️ 从 data/at 加载 Flow Token 失败: %v", err)
}
// 添加配置文件中的 Tokens(兼容旧配置)
for i, st := range appConfig.Flow.Tokens {
token := &flow.FlowToken{
ID: fmt.Sprintf("flow_token_%d", i),
ST: st,
}
flowClient.AddToken(token)
}
totalTokens := loadedFromDir + len(appConfig.Flow.Tokens)
if totalTokens == 0 {
logger.Info("📹 Flow 服务已启用但无可用 Token (请将 cookie 放入 data/at/ 目录)")
flowHandler = flow.NewGenerationHandler(flowClient)
return
}
// 启动 AT 刷新 worker (每 30 分钟刷新一次)
flowTokenPool.StartRefreshWorker(30 * time.Minute)
// 启动文件监听 (自动加载新增 Token)
if err := flowTokenPool.StartWatcher(); err != nil {
logger.Warn("⚠️ Flow 文件监听启动失败: %v", err)
}
flowHandler = flow.NewGenerationHandler(flowClient)
logger.Info("📹 Flow 服务已启用,共 %d 个 Token (目录: %d, 配置: %d)", totalTokens, loadedFromDir, len(appConfig.Flow.Tokens))
}
func initProxyPool() {
// 服务端模式不需要代理池
if appConfig.PoolServer.Enable && appConfig.PoolServer.Mode == "server" {
logger.Info("🖥️ 服务端模式,跳过代理初始化")
return
}
// 初始化 sing-box(用于 hysteria2/tuic 等协议)
proxy.InitSingbox()
// 添加订阅链接(新配置)
for _, sub := range appConfig.ProxyPool.Subscribes {
proxy.Manager.AddSubscribeURL(sub)
}
// 兼容旧配置
if appConfig.ProxySubscribe != "" {
proxy.Manager.AddSubscribeURL(appConfig.ProxySubscribe)
}
// 添加代理文件
for _, file := range appConfig.ProxyPool.Files {
proxy.Manager.AddProxyFile(file)
}
if err := proxy.Manager.LoadAll(); err != nil {
logger.Warn("⚠️ 加载代理失败: %v", err)
}
// 当有代理配置时,默认开启健康检查(除非明确关闭)
hasProxyConfig := len(appConfig.ProxyPool.Subscribes) > 0 || len(appConfig.ProxyPool.Files) > 0 || appConfig.ProxySubscribe != ""
shouldHealthCheck := hasProxyConfig || appConfig.ProxyPool.HealthCheck
if shouldHealthCheck && appConfig.ProxyPool.CheckOnStartup {
go func() {
proxy.Manager.CheckAllHealth()
// 健康检查完成后初始化实例池
if proxy.Manager.HealthyCount() > 0 {
poolSize := appConfig.Pool.RegisterThreads
if poolSize <= 0 {
poolSize = pool.DefaultProxyCount
}
if poolSize > 10 {
poolSize = 10
}
proxy.Manager.SetMaxPoolSize(poolSize)
if err := proxy.Manager.InitInstancePool(poolSize); err != nil {
logger.Warn("⚠️ 初始化代理实例池失败: %v", err)
} else {
logger.Info("✅ 代理实例池初始化完成: %d 个实例", poolSize)
}
}
}()
} else if proxy.Manager.TotalCount() > 0 {
// 不需要健康检查时直接标记就绪
proxy.Manager.SetReady(true)
}
if proxy.Manager.TotalCount() == 0 {
if appConfig.ProxyPool.Proxy != "" {
proxy.Manager.SetProxies([]string{appConfig.ProxyPool.Proxy})
} else if Proxy != "" {
proxy.Manager.SetProxies([]string{Proxy})
}
}
if proxy.Manager.TotalCount() == 0 || AutoSubscribeEnabled {
logger.Info("🔄 启动自动订阅服务(每小时注册获取代理)...")
proxy.Manager.StartAutoSubscribe()
}
if proxy.Manager.TotalCount() > 0 {
proxy.Manager.StartAutoUpdate()
logger.Info("✅ 代理池已初始化: %d 个节点, %d 个健康",
proxy.Manager.TotalCount(), proxy.Manager.HealthyCount())
}
register.GetProxy = func() string {
if proxy.Manager.Count() > 0 {
return proxy.Manager.Next()
}
return Proxy
}
register.ReleaseProxy = func(proxyURL string) {
proxy.Manager.ReleaseByURL(proxyURL)
}
}
var BaseModels = []string{
// Gemini 文本模型
"gemini-2.5-flash",
"gemini-2.5-pro",
"gemini-3-pro-preview",
"gemini-3-pro",
// Gemini 图片生成
"gemini-2.5-flash-image",
"gemini-2.5-pro-image",
"gemini-3-pro-preview-image",
"gemini-3-pro-image",
// Gemini 视频生成
"gemini-2.5-flash-video",
"gemini-2.5-pro-video",
"gemini-3-pro-preview-video",
"gemini-3-pro-video",
// Gemini 搜索
"gemini-2.5-flash-search",
"gemini-2.5-pro-search",
"gemini-3-pro-preview-search",
"gemini-3-pro-search",
"gemini-3-flash-preview",
"gemini-3-flash-preview-image",
"gemini-3-flash-preview-video",
"gemini-3-flash-preview-search",
"gemini-3-flash",
"gemini-3-flash-image",
"gemini-3-flash-video",
"gemini-3-flash-search",
"gemini-2.5-flash-preview-latest",
"gemini-2.5-flash-preview-latest-image",
"gemini-2.5-flash-preview-latest-video",
"gemini-2.5-flash-preview-latest-search",
}
var FlowModels = []string{
// Flow 图片生成模型
"gemini-2.5-flash-image-landscape",
"gemini-2.5-flash-image-portrait",
"gemini-3.0-pro-image-landscape",
"gemini-3.0-pro-image-portrait",
"imagen-4.0-generate-preview-landscape",
"imagen-4.0-generate-preview-portrait",
// Flow 文生视频 (T2V)
"veo_3_1_t2v_fast_portrait",
"veo_3_1_t2v_fast_landscape",
"veo_2_1_fast_d_15_t2v_portrait",
"veo_2_1_fast_d_15_t2v_landscape",
"veo_2_0_t2v_portrait",
"veo_2_0_t2v_landscape",
// Flow 图生视频 (I2V)
"veo_3_1_i2v_s_fast_fl_portrait",
"veo_3_1_i2v_s_fast_fl_landscape",
"veo_2_1_fast_d_15_i2v_portrait",
"veo_2_1_fast_d_15_i2v_landscape",
"veo_2_0_i2v_portrait",
"veo_2_0_i2v_landscape",
// Flow 多图生成视频 (R2V)
"veo_3_0_r2v_fast_portrait",
"veo_3_0_r2v_fast_landscape",
}
func GetAvailableModels() []string {
if flowHandler != nil {
// Flow 已启用,返回全部模型
return append(BaseModels, FlowModels...)
}
// Flow 未启用,只返回基础模型
return BaseModels
}
// 模型名称映射到 Google API 的 modelId
var modelMapping = map[string]string{
"gemini-2.5-flash": "gemini-2.5-flash",
"gemini-2.5-pro": "gemini-2.5-pro",
"gemini-3-pro-preview": "gemini-3-pro-preview",
"gemini-3-pro": "gemini-3-pro",
}
func getEnv(key, def string) string {
if v := os.Getenv(key); v != "" {
return v
}
return def
}
func getCommonHeaders(jwt, origAuth string) map[string]string {
headers := map[string]string{
"accept": "*/*",
"accept-encoding": "gzip, deflate, br, zstd",
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8",
"authorization": "Bearer " + jwt,
"content-type": "application/json",
"origin": "https://business.gemini.google",
"referer": "https://business.gemini.google/",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
"x-server-timeout": "1800",
"sec-ch-ua": `"Chromium";v="124", "Google Chrome";v="124", "Not-A.Brand";v="99"`,
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": `"Windows"`,
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "cross-site",
}
// 同时携带原始 authorization
if origAuth != "" {
headers["x-original-authorization"] = origAuth
}
return headers
}
func createSession(jwt, configID, origAuth string) (string, error) {
return createSessionWithRetry(jwt, configID, origAuth, 3)
}
// createSessionWithRetry 创建session带重试(处理400错误)
func createSessionWithRetry(jwt, configID, origAuth string, maxRetries int) (string, error) {
var lastErr error
for retry := 0; retry < maxRetries; retry++ {
if retry > 0 {
// 等待后重试
waitTime := time.Duration(retry*500) * time.Millisecond
time.Sleep(waitTime)
logger.Info("🔄 createSession 重试 %d/%d", retry+1, maxRetries)
}
sessionName, err := createSessionOnce(jwt, configID, origAuth)
if err == nil {
return sessionName, nil
}
lastErr = err
errMsg := err.Error()
// 400错误可以重试
if strings.Contains(errMsg, "400") {
logger.Warn("⚠️ createSession 400 错误,尝试重试...")
continue
}
// 401/403 不重试
if strings.Contains(errMsg, "401") || strings.Contains(errMsg, "403") {
return "", err
}
// 其他错误继续重试
}
return "", lastErr
}
// createSessionOnce 单次创建session
func createSessionOnce(jwt, configID, origAuth string) (string, error) {
body := map[string]interface{}{
"configId": configID,
"additionalParams": map[string]string{"token": "-"},
"createSessionRequest": map[string]interface{}{
"session": map[string]string{"name": "", "displayName": ""},
},
}
bodyBytes, _ := json.Marshal(body)
req, _ := http.NewRequest("POST", "https://biz-discoveryengine.googleapis.com/v1alpha/locations/global/widgetCreateSession", bytes.NewReader(bodyBytes))
for k, v := range getCommonHeaders(jwt, origAuth) {
req.Header.Set(k, v)
}
resp, err := utils.HTTPClient.Do(req)
if err != nil {
return "", fmt.Errorf("createSession 请求失败: %w", err)
}
defer resp.Body.Close()
respBody, err := utils.ReadResponseBody(resp)
if err != nil {
return "", fmt.Errorf("读取响应失败: %w", err)
}
if resp.StatusCode != 200 {
return "", fmt.Errorf("createSession 失败: %d %s", resp.StatusCode, string(respBody))
}
var result struct {
Session struct {
Name string `json:"name"`
} `json:"session"`
}
if err := json.Unmarshal(respBody, &result); err != nil {
return "", fmt.Errorf("解析 session 响应失败: %w", err)
}
return result.Session.Name, nil
}
func uploadContextFile(jwt, configID, sessionName, mimeType, base64Content, origAuth string) (string, error) {
ext := "jpg"
if parts := strings.Split(mimeType, "/"); len(parts) == 2 {
ext = parts[1]
}
fileName := fmt.Sprintf("upload_%d_%s.%s", time.Now().Unix(), uuid.New().String()[:6], ext)
body := map[string]interface{}{
"configId": configID,
"additionalParams": map[string]string{"token": "-"},
"addContextFileRequest": map[string]interface{}{
"name": sessionName,
"fileName": fileName,
"mimeType": mimeType,
"fileContents": base64Content,
},
}
bodyBytes, _ := json.Marshal(body)
req, _ := http.NewRequest("POST", "https://biz-discoveryengine.googleapis.com/v1alpha/locations/global/widgetAddContextFile", bytes.NewReader(bodyBytes))
for k, v := range getCommonHeaders(jwt, origAuth) {
req.Header.Set(k, v)
}
resp, err := utils.HTTPClient.Do(req)
if err != nil {
return "", fmt.Errorf("上传文件请求失败: %w", err)
}
defer resp.Body.Close()
respBody, err := utils.ReadResponseBody(resp)
if err != nil {
return "", fmt.Errorf("读取响应失败: %w", err)
}
if resp.StatusCode != 200 {
return "", fmt.Errorf("上传文件失败: %d %s", resp.StatusCode, string(respBody))
}
var result struct {
AddContextFileResponse struct {
FileID string `json:"fileId"`
} `json:"addContextFileResponse"`
}
if err := json.Unmarshal(respBody, &result); err != nil {
return "", fmt.Errorf("解析上传响应失败: %w", err)
}
if result.AddContextFileResponse.FileID == "" {
return "", fmt.Errorf("上传成功但 fileId 为空,响应: %s", string(respBody))
}
return result.AddContextFileResponse.FileID, nil
}
func uploadContextFileByURL(jwt, configID, sessionName, imageURL, origAuth string) (string, error) {
body := map[string]interface{}{
"configId": configID,
"additionalParams": map[string]string{"token": "-"},
"addContextFileRequest": map[string]interface{}{
"name": sessionName,
"fileUri": imageURL,
},
}
bodyBytes, _ := json.Marshal(body)
req, _ := http.NewRequest("POST", "https://biz-discoveryengine.googleapis.com/v1alpha/locations/global/widgetAddContextFile", bytes.NewReader(bodyBytes))
for k, v := range getCommonHeaders(jwt, origAuth) {
req.Header.Set(k, v)
}
resp, err := utils.HTTPClient.Do(req)
if err != nil {
return "", fmt.Errorf("上传文件请求失败: %w", err)
}
defer resp.Body.Close()
respBody, err := utils.ReadResponseBody(resp)
if err != nil {
return "", fmt.Errorf("读取响应失败: %w", err)
}
if resp.StatusCode != 200 {
return "", fmt.Errorf("URL上传文件失败: %d %s", resp.StatusCode, string(respBody))
}
var result struct {
AddContextFileResponse struct {
FileID string `json:"fileId"`
} `json:"addContextFileResponse"`
}
if err := json.Unmarshal(respBody, &result); err != nil {
return "", fmt.Errorf("解析上传响应失败: %w", err)
}
if result.AddContextFileResponse.FileID == "" {
return "", fmt.Errorf("URL上传成功但 fileId 为空,响应: %s", string(respBody))
}
return result.AddContextFileResponse.FileID, nil
}
type Message struct {
Role string `json:"role"`
Content interface{} `json:"content"` // string 或 []ContentPart
Name string `json:"name,omitempty"` // 函数名称(tool角色时)
ToolCalls []ToolCall `json:"tool_calls,omitempty"` // 工具调用(assistant角色时)
ToolCallID string `json:"tool_call_id,omitempty"` // 工具调用ID(tool角色时)
}
type ContentPart struct {
Type string `json:"type"`
Text string `json:"text,omitempty"`
ImageURL *ImageURL `json:"image_url,omitempty"`
}
type ImageURL struct {
URL string `json:"url"`
}
// OpenAI格式的工具定义
type ToolDef struct {
Type string `json:"type"` // "function"
Function FunctionDef `json:"function"`
}
type FunctionDef struct {
Name string `json:"name"`
Description string `json:"description"`
Parameters map[string]interface{} `json:"parameters"`
}
// 工具调用结果
type ToolCall struct {
ID string `json:"id"`
Type string `json:"type"` // "function"
Function FunctionCall `json:"function"`
}
type FunctionCall struct {
Name string `json:"name"`
Arguments string `json:"arguments"`
}
type ChatRequest struct {
Model string `json:"model"`
Messages []Message `json:"messages"`
Stream bool `json:"stream"`
Temperature float64 `json:"temperature"`
TopP float64 `json:"top_p"`
Tools []ToolDef `json:"tools,omitempty"` // 工具定义
ToolChoice string `json:"tool_choice,omitempty"` // "auto", "none", "required"
}
type ChatChoice struct {
Index int `json:"index"`
Delta map[string]interface{} `json:"delta,omitempty"`
Message map[string]interface{} `json:"message,omitempty"`
FinishReason *string `json:"finish_reason"`
Logprobs interface{} `json:"logprobs"` // OpenAI兼容
}
type ChatChunk struct {
ID string `json:"id"`
Object string `json:"object"`
Created int64 `json:"created"`
Model string `json:"model"`
SystemFingerprint string `json:"system_fingerprint,omitempty"`
Choices []ChatChoice `json:"choices"`
}
func createChunk(id string, created int64, model string, delta map[string]interface{}, finishReason *string) string {
if delta == nil {
delta = map[string]interface{}{}
}
chunk := ChatChunk{
ID: id,
Object: "chat.completion.chunk",
Created: created,
Model: model,
Choices: []ChatChoice{{
Index: 0,
Delta: delta,
FinishReason: finishReason,
Logprobs: nil,
}},
}
data, _ := json.Marshal(chunk)
return string(data)
}
func extractContentFromReply(replyMap map[string]interface{}, jwt, session, configID, origAuth string) (text string, imageData string, imageMime string, reasoning string, downloadErr error) {
groundedContent, ok := replyMap["groundedContent"].(map[string]interface{})
if !ok {
return
}
content, ok := groundedContent["content"].(map[string]interface{})
if !ok {
return
}
if thought, ok := content["thought"].(bool); ok && thought {
if t, ok := content["text"].(string); ok && t != "" {
reasoning = t
}
return
}
if t, ok := content["text"].(string); ok && t != "" {
text = t
}
if inlineData, ok := content["inlineData"].(map[string]interface{}); ok {
if mime, ok := inlineData["mimeType"].(string); ok {
imageMime = mime
}
if data, ok := inlineData["data"].(string); ok {
imageData = data
}
}
if file, ok := content["file"].(map[string]interface{}); ok {
fileId, _ := file["fileId"].(string)
mimeType, _ := file["mimeType"].(string)
if fileId != "" {
fileType := "文件"
if strings.HasPrefix(mimeType, "image/") {
fileType = "图片"
} else if strings.HasPrefix(mimeType, "video/") {
fileType = "视频"
}
data, err := downloadGeneratedFile(jwt, fileId, session, configID, origAuth)
if err != nil {
logger.Error("❌ 下载%s失败: %v", fileType, err)
downloadErr = err // 返回错误供上层处理
} else {
imageData = data
imageMime = mimeType
}
}
}
return
}
// ErrDownloadNeedsRetry 标识下载失败需要整体重试(换号重新生成)
var ErrDownloadNeedsRetry = fmt.Errorf("DOWNLOAD_NEEDS_RETRY")
func downloadGeneratedFile(jwt, fileId, session, configID, origAuth string) (string, error) {
return downloadGeneratedFileWithRetry(jwt, fileId, session, configID, origAuth, 2)
}
func downloadGeneratedFileWithRetry(jwt, fileId, session, configID, origAuth string, maxRetries int) (string, error) {
// 参数验证
if jwt == "" {
return "", fmt.Errorf("JWT 为空,无法下载文件")
}
if session == "" {
return "", fmt.Errorf("session 为空,无法下载文件")
}
if configID == "" {
return "", fmt.Errorf("configID 为空,无法下载文件")
}
var lastErr error
var authFailCount int
for retry := 0; retry < maxRetries; retry++ {
result, err := downloadGeneratedFileOnce(jwt, fileId, session, configID, origAuth)
if err == nil {
return result, nil
}
lastErr = err
errMsg := err.Error()
// 检测认证失败(401/403)
if strings.Contains(errMsg, "401") || strings.Contains(errMsg, "403") ||
strings.Contains(errMsg, "UNAUTHENTICATED") || strings.Contains(errMsg, "SESSION_COOKIE_INVALID") {
authFailCount++
logger.Warn("⚠️ 下载文件认证失败 (尝试 %d/%d): %v", retry+1, maxRetries, err)
// 认证失败超过1次,返回特殊错误让上层重新发起整个请求
if authFailCount >= 1 {
logger.Info("🔄 下载认证失败,需要换号重新生成")
return "", fmt.Errorf("%w: 401/403 认证失败", ErrDownloadNeedsRetry)
}
continue
}
// 其他错误,等待后重试
logger.Error("❌ 下载文件失败 (尝试 %d/%d): %v", retry+1, maxRetries, err)
time.Sleep(300 * time.Millisecond)
}
return "", fmt.Errorf("下载文件失败,已重试 %d 次: %w", maxRetries, lastErr)
}
// downloadGeneratedFileOnce 单次下载文件尝试
func downloadGeneratedFileOnce(jwt, fileId, session, configID, origAuth string) (string, error) {
// 步骤1: 使用 widgetListSessionFileMetadata 获取文件下载 URL
listBody := map[string]interface{}{
"configId": configID,
"additionalParams": map[string]string{"token": "-"},
"listSessionFileMetadataRequest": map[string]interface{}{
"name": session,
"filter": "file_origin_type = AI_GENERATED",
},
}
listBodyBytes, _ := json.Marshal(listBody)
listReq, _ := http.NewRequest("POST", "https://biz-discoveryengine.googleapis.com/v1alpha/locations/global/widgetListSessionFileMetadata", bytes.NewReader(listBodyBytes))
for k, v := range getCommonHeaders(jwt, origAuth) {
listReq.Header.Set(k, v)
}
listResp, err := utils.HTTPClient.Do(listReq)
if err != nil {
return "", fmt.Errorf("获取文件元数据失败: %w", err)
}
defer listResp.Body.Close()
listRespBody, _ := utils.ReadResponseBody(listResp)
if listResp.StatusCode != 200 {
return "", fmt.Errorf("获取文件元数据失败: HTTP %d: %s", listResp.StatusCode, string(listRespBody))
}
// 解析响应,查找匹配的 fileId
var listResult struct {
ListSessionFileMetadataResponse struct {
FileMetadata []struct {
FileID string `json:"fileId"`
Session string `json:"session"` // 包含完整的 projects 路径
DownloadURI string `json:"downloadUri"`
} `json:"fileMetadata"`
} `json:"listSessionFileMetadataResponse"`
}
if err := json.Unmarshal(listRespBody, &listResult); err != nil {
return "", fmt.Errorf("解析文件元数据失败: %w", err)
}
// 查找匹配的文件,获取完整 session 路径
var fullSession string
for _, meta := range listResult.ListSessionFileMetadataResponse.FileMetadata {
if meta.FileID == fileId {
fullSession = meta.Session // 如: projects/372889301682/locations/global/collections/...
break
}
}
if fullSession == "" {
return "", fmt.Errorf("未找到 fileId=%s 的文件信息", fileId)
}
downloadURL := fmt.Sprintf("https://biz-discoveryengine.googleapis.com/download/v1alpha/%s:downloadFile?fileId=%s&alt=media", fullSession, fileId)
downloadReq, _ := http.NewRequest("GET", downloadURL, nil)
for k, v := range getCommonHeaders(jwt, origAuth) {
downloadReq.Header.Set(k, v)
}
downloadResp, err := utils.HTTPClient.Do(downloadReq)
if err != nil {
return "", fmt.Errorf("下载图片失败: %w", err)
}
defer downloadResp.Body.Close()
imgBody, _ := utils.ReadResponseBody(downloadResp)
if downloadResp.StatusCode != 200 {
return "", fmt.Errorf("下载图片失败: HTTP %d: %s", downloadResp.StatusCode, string(imgBody))
}
// 响应是原始二进制图片数据,需要转为 base64
return base64.StdEncoding.EncodeToString(imgBody), nil
}
// 将图片转换为 Markdown 格式的 data URI
func formatImageAsMarkdown(mimeType, base64Data string) string {
return fmt.Sprintf("", mimeType, base64Data)
}
// 媒体信息(图片/视频)
type MediaInfo struct {
MimeType string
Data string // base64 数据
URL string // 原始 URL(如果有)
IsURL bool // 是否使用 URL 直接上传
MediaType string // "image" 或 "video"
}
// 别名,保持向后兼容
type ImageInfo = MediaInfo
// 解析消息内容,支持文本、图片和视频
func parseMessageContent(msg Message) (string, []MediaInfo) {
var textContent string
var medias []MediaInfo
switch content := msg.Content.(type) {
case string:
textContent = content
case []interface{}:
for _, part := range content {
partMap, ok := part.(map[string]interface{})
if !ok {
continue
}
partType, _ := partMap["type"].(string)
switch partType {
case "text":
if text, ok := partMap["text"].(string); ok {
textContent += text
}
case "image_url":
if imgURL, ok := partMap["image_url"].(map[string]interface{}); ok {
if urlStr, ok := imgURL["url"].(string); ok {
media := parseMediaURL(urlStr, "image")
if media != nil {
medias = append(medias, *media)
}
}
}
case "video_url":
// 支持视频 URL
if videoURL, ok := partMap["video_url"].(map[string]interface{}); ok {
if urlStr, ok := videoURL["url"].(string); ok {
media := parseMediaURL(urlStr, "video")
if media != nil {
medias = append(medias, *media)
}
}
}
case "file":
// 支持通用文件类型
if fileData, ok := partMap["file"].(map[string]interface{}); ok {
if urlStr, ok := fileData["url"].(string); ok {
mediaType := "image" // 默认图片
if mime, ok := fileData["mime_type"].(string); ok {
if strings.HasPrefix(mime, "video/") {
mediaType = "video"
}
}
media := parseMediaURL(urlStr, mediaType)
if media != nil {
medias = append(medias, *media)
}
}
}
}
}
}
return textContent, medias
}
// 解析媒体 URL(图片或视频)
func parseMediaURL(urlStr, defaultType string) *MediaInfo {
// 处理 base64 数据
if strings.HasPrefix(urlStr, "data:") {
// data:image/jpeg;base64,/9j/4AAQ... 或 data:video/mp4;base64,...
parts := strings.SplitN(urlStr, ",", 2)
if len(parts) != 2 {
return nil
}
base64Data := parts[1]
var mediaType string
var mimeType string
// 检测媒体类型
if strings.Contains(parts[0], "video/") {
mediaType = "video"
// 视频格式处理
if strings.Contains(parts[0], "video/mp4") {
mimeType = "video/mp4"
} else if strings.Contains(parts[0], "video/webm") {
mimeType = "video/webm"
} else if strings.Contains(parts[0], "video/quicktime") || strings.Contains(parts[0], "video/mov") {
// MOV 格式,尝试作为 mp4 上传
mimeType = "video/mp4"
logger.Debug("ℹ️ MOV 视频将作为 MP4 上传")
} else if strings.Contains(parts[0], "video/avi") || strings.Contains(parts[0], "video/x-msvideo") {
mimeType = "video/mp4"
logger.Debug("ℹ️ AVI 视频将作为 MP4 上传")
} else {
// 其他视频格式默认作为 mp4
mimeType = "video/mp4"
logger.Debug("ℹ️ 未知视频格式 %s 将作为 MP4 上传", parts[0])
}
} else {
mediaType = "image"
// 图片格式处理
if strings.Contains(parts[0], "image/png") {
mimeType = "image/png"
} else if strings.Contains(parts[0], "image/jpeg") {
mimeType = "image/jpeg"
} else {
// 其他图片格式需要转换为 PNG
converted, err := convertBase64ToPNG(base64Data)
if err != nil {
logger.Warn("⚠️ %s base64 转换失败: %v", parts[0], err)
mimeType = "image/jpeg" // 回退
} else {
logger.Info("✅ %s base64 已转换为 PNG", parts[0])
base64Data = converted
mimeType = "image/png"
}
}
}
return &MediaInfo{
MimeType: mimeType,
Data: base64Data,
IsURL: false,
MediaType: mediaType,
}
}
// URL 媒体 - 优先尝试直接使用 URL 上传
mediaType := defaultType
lowerURL := strings.ToLower(urlStr)
if strings.HasSuffix(lowerURL, ".mp4") || strings.HasSuffix(lowerURL, ".webm") ||
strings.HasSuffix(lowerURL, ".mov") || strings.HasSuffix(lowerURL, ".avi") ||
strings.HasSuffix(lowerURL, ".mkv") || strings.HasSuffix(lowerURL, ".m4v") {
mediaType = "video"
}
return &MediaInfo{
URL: urlStr,
IsURL: true,
MediaType: mediaType,
}
}
func downloadImage(urlStr string) (string, string, error) {
return downloadMedia(urlStr, "image")
}
// downloadMedia 下载媒体文件(图片或视频)
func downloadMedia(urlStr, mediaType string) (string, string, error) {
resp, err := utils.HTTPClient.Get(urlStr)
if err != nil {
return "", "", err
}
defer resp.Body.Close()
// 检查上游返回的状态码
if resp.StatusCode == 401 || resp.StatusCode == 403 {
return "", "", fmt.Errorf("UPSTREAM_%d: 上游返回状态码 %d 多媒体下载失败", resp.StatusCode, resp.StatusCode)
}
if resp.StatusCode >= 400 {
return "", "", fmt.Errorf("UPSTREAM_%d: 上游返回状态码 %d", resp.StatusCode, resp.StatusCode)
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return "", "", err
}
mimeType := resp.Header.Get("Content-Type")
if mediaType == "video" || strings.HasPrefix(mimeType, "video/") {
// 视频处理
if mimeType == "" {
mimeType = "video/mp4"
}
// 规范化视频 MIME 类型
mimeType = normalizeVideoMimeType(mimeType)
return base64.StdEncoding.EncodeToString(data), mimeType, nil
}
// 图片处理
if mimeType == "" {
mimeType = "image/jpeg"
}
needConvert := !strings.Contains(mimeType, "jpeg") && !strings.Contains(mimeType, "png")
if needConvert {
converted, err := convertToPNG(data)
if err != nil {
logger.Warn("⚠️ %s 转换失败: %v,尝试原格式", mimeType, err)
} else {
logger.Info("✅ %s 已转换为 PNG", mimeType)
return base64.StdEncoding.EncodeToString(converted), "image/png", nil
}
}
return base64.StdEncoding.EncodeToString(data), mimeType, nil
}
// normalizeVideoMimeType 规范化视频 MIME 类型
func normalizeVideoMimeType(mimeType string) string {
switch {
case strings.Contains(mimeType, "mp4"):
return "video/mp4"
case strings.Contains(mimeType, "webm"):
return "video/webm"
case strings.Contains(mimeType, "quicktime"), strings.Contains(mimeType, "mov"):
logger.Debug("ℹ️ MOV 视频将作为 MP4 上传")
return "video/mp4"
case strings.Contains(mimeType, "avi"), strings.Contains(mimeType, "x-msvideo"):
logger.Debug("ℹ️ AVI 视频将作为 MP4 上传")
return "video/mp4"
case strings.Contains(mimeType, "x-matroska"), strings.Contains(mimeType, "mkv"):
logger.Debug("ℹ️ MKV 视频将作为 MP4 上传")
return "video/mp4"
case strings.Contains(mimeType, "3gpp"):
return "video/3gpp"
default:
logger.Debug("ℹ️ 未知视频格式 %s 将作为 MP4 上传", mimeType)
return "video/mp4"
}
}
// convertToPNG 将图片转换为 PNG 格式
func convertToPNG(data []byte) ([]byte, error) {
img, _, err := image.Decode(bytes.NewReader(data))
if err != nil {
return nil, fmt.Errorf("解码图片失败: %w", err)
}
var buf bytes.Buffer
if err := png.Encode(&buf, img); err != nil {
return nil, fmt.Errorf("编码 PNG 失败: %w", err)
}
return buf.Bytes(), nil
}
// convertBase64ToPNG 将 base64 图片转换为 PNG
func convertBase64ToPNG(base64Data string) (string, error) {
data, err := base64.StdEncoding.DecodeString(base64Data)
if err != nil {
return "", fmt.Errorf("解码 base64 失败: %w", err)
}
converted, err := convertToPNG(data)
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(converted), nil
}
const maxRetries = 3
// convertMessagesToPrompt 将多轮对话转换为Gemini格式的prompt
// extractSystemPrompt 提取并返回系统提示词
func extractSystemPrompt(messages []Message) string {
for _, msg := range messages {
if msg.Role == "system" {
text, _ := parseMessageContent(msg)
return text
}
}
return ""
}
// convertMessagesToPrompt 将多轮对话转换为带系统提示词的prompt
// 支持OpenAI/Claude/Gemini格式的messages
func convertMessagesToPrompt(messages []Message) string {
var dialogParts []string
var systemPrompt string
for _, msg := range messages {
text, _ := parseMessageContent(msg)
if text == "" && msg.Role != "assistant" {
continue
}
switch msg.Role {
case "system":
// 支持多个system消息拼接
if systemPrompt != "" {
systemPrompt += "\n" + text
} else {
systemPrompt = text
}
case "user", "human": // Claude使用human
dialogParts = append(dialogParts, fmt.Sprintf("Human: %s", text))
case "assistant":
// 检查是否有工具调用
if len(msg.ToolCalls) > 0 {
for _, tc := range msg.ToolCalls {
dialogParts = append(dialogParts, fmt.Sprintf("Assistant: [调用工具 %s(%s)]", tc.Function.Name, tc.Function.Arguments))
}
} else if text != "" {
dialogParts = append(dialogParts, fmt.Sprintf("Assistant: %s", text))
}
case "tool", "tool_result": // Claude使用tool_result
dialogParts = append(dialogParts, fmt.Sprintf("Tool Result [%s]: %s", msg.Name, text))
}
}
// 组合最终prompt,系统提示词使用更强的格式
var result strings.Builder
if systemPrompt != "" {
// 使用更明确的系统提示词格式,确保生效
result.WriteString("<system>\n")
result.WriteString(systemPrompt)
result.WriteString("\n</system>\n\n")
}
if len(dialogParts) > 0 {
result.WriteString(strings.Join(dialogParts, "\n\n"))
}
// 添加Assistant前缀引导回复
result.WriteString("\n\nAssistant:")
return result.String()
}
// ==================== Gemini API 兼容 ====================
// GeminiRequest Gemini generateContent API 请求格式
type GeminiRequest struct {
Contents []GeminiContent `json:"contents"`
SystemInstruction *GeminiContent `json:"systemInstruction,omitempty"`
GenerationConfig map[string]interface{} `json:"generationConfig,omitempty"`
GeminiTools []map[string]interface{} `json:"tools,omitempty"`
}
type GeminiContent struct {
Role string `json:"role,omitempty"`
Parts []GeminiPart `json:"parts"`
}
type GeminiPart struct {
Text string `json:"text,omitempty"`
InlineData *GeminiInlineData `json:"inlineData,omitempty"`
}
type GeminiInlineData struct {
MimeType string `json:"mimeType"`
Data string `json:"data"`
}
// handleGeminiGenerate 处理Gemini generateContent API格式的请求
func handleGeminiGenerate(c *gin.Context) {
action := c.Param("action")
if action == "" {
c.JSON(400, gin.H{"error": gin.H{"code": 400, "message": "Missing model action", "status": "INVALID_ARGUMENT"}})
return
}
action = strings.TrimPrefix(action, "/")
var model string
var isStream bool
if idx := strings.LastIndex(action, ":"); idx > 0 {
model = action[:idx]
actionType := action[idx+1:]
isStream = actionType == "streamGenerateContent"
} else {
model = action
}
if model == "" {
model = GetAvailableModels()[0]
}
var geminiReq GeminiRequest
if err := c.ShouldBindJSON(&geminiReq); err != nil {
c.JSON(400, gin.H{"error": gin.H{"code": 400, "message": err.Error(), "status": "INVALID_ARGUMENT"}})
return
}
var messages []Message
// 处理systemInstruction
if geminiReq.SystemInstruction != nil && len(geminiReq.SystemInstruction.Parts) > 0 {
var sysText string
for _, part := range geminiReq.SystemInstruction.Parts {
if part.Text != "" {
sysText += part.Text
}
}
if sysText != "" {
messages = append(messages, Message{Role: "system", Content: sysText})
}
}
// 处理contents
for _, content := range geminiReq.Contents {
role := content.Role
if role == "model" {
role = "assistant"
}
var textParts []string
var contentParts []interface{}
for _, part := range content.Parts {
if part.Text != "" {
textParts = append(textParts, part.Text)
}
if part.InlineData != nil {
contentParts = append(contentParts, map[string]interface{}{
"type": "image_url",
"image_url": map[string]string{
"url": fmt.Sprintf("data:%s;base64,%s", part.InlineData.MimeType, part.InlineData.Data),
},
})
}
}
if len(contentParts) > 0 {
if len(textParts) > 0 {
contentParts = append([]interface{}{map[string]interface{}{"type": "text", "text": strings.Join(textParts, "\n")}}, contentParts...)
}
messages = append(messages, Message{Role: role, Content: contentParts})
} else if len(textParts) > 0 {
messages = append(messages, Message{Role: role, Content: strings.Join(textParts, "\n")})
}
}
stream := isStream || c.Query("alt") == "sse"
// 转换Gemini工具格式
var tools []ToolDef
for _, gt := range geminiReq.GeminiTools {
if funcDecls, ok := gt["functionDeclarations"].([]interface{}); ok {
for _, fd := range funcDecls {
if funcMap, ok := fd.(map[string]interface{}); ok {
name, _ := funcMap["name"].(string)
desc, _ := funcMap["description"].(string)
params, _ := funcMap["parameters"].(map[string]interface{})
tools = append(tools, ToolDef{
Type: "function",
Function: FunctionDef{
Name: name,
Description: desc,
Parameters: params,
},
})
}
}
}
}
req := ChatRequest{
Model: model,
Messages: messages,
Stream: stream,
Tools: tools,
}
streamChat(c, req)
}
// ==================== Claude API 兼容 ====================
type ClaudeRequest struct {
Model string `json:"model"`
Messages []Message `json:"messages"`
System string `json:"system,omitempty"`
MaxTokens int `json:"max_tokens,omitempty"`
Stream bool `json:"stream"`
Temperature float64 `json:"temperature,omitempty"`
Tools []ToolDef `json:"tools,omitempty"`
}
// handleClaudeMessages 处理Claude Messages API格式的请求
func handleClaudeMessages(c *gin.Context) {
var claudeReq ClaudeRequest
if err := c.ShouldBindJSON(&claudeReq); err != nil {
c.JSON(400, gin.H{"type": "error", "error": gin.H{"type": "invalid_request_error", "message": err.Error()}})
return
}
req := ChatRequest{
Model: claudeReq.Model,
Messages: claudeReq.Messages,
Stream: claudeReq.Stream,
Temperature: claudeReq.Temperature,
Tools: claudeReq.Tools,
}
// 如果Claude格式有单独的system字段,插入到messages开头
if claudeReq.System != "" {
systemMsg := Message{Role: "system", Content: claudeReq.System}
req.Messages = append([]Message{systemMsg}, req.Messages...)
}
if req.Model == "" {
req.Model = GetAvailableModels()[0]
}
streamChat(c, req)
}
// buildToolsSpec 将OpenAI格式的工具定义转换为Gemini的toolsSpec
// 支持混合后缀同时启用多个功能,如 -image-search 同时启用图片生成和搜索
func buildToolsSpec(tools []ToolDef, isImageModel, isVideoModel, isSearchModel bool) map[string]interface{} {
toolsSpec := make(map[string]interface{})
// 检查是否指定了任何功能后缀
hasAnySpec := isImageModel || isVideoModel || isSearchModel
if !hasAnySpec {
toolsSpec["webGroundingSpec"] = map[string]interface{}{}
toolsSpec["toolRegistry"] = "default_tool_registry"
toolsSpec["imageGenerationSpec"] = map[string]interface{}{}
toolsSpec["videoGenerationSpec"] = map[string]interface{}{}
} else {
if isImageModel {
toolsSpec["imageGenerationSpec"] = map[string]interface{}{}
}
if isVideoModel {
toolsSpec["videoGenerationSpec"] = map[string]interface{}{}
}
if isSearchModel {
toolsSpec["webGroundingSpec"] = map[string]interface{}{}
}
}
_ = tools
return toolsSpec
}
// extractToolCalls 从Gemini响应中提取工具调用
func extractToolCalls(dataList []map[string]interface{}) []ToolCall {
var toolCalls []ToolCall
for _, data := range dataList {
streamResp, ok := data["streamAssistResponse"].(map[string]interface{})
if !ok {
continue
}
answer, ok := streamResp["answer"].(map[string]interface{})
if !ok {
continue
}
replies, ok := answer["replies"].([]interface{})
if !ok {
continue
}
for _, reply := range replies {
replyMap, ok := reply.(map[string]interface{})
if !ok {
continue
}
groundedContent, ok := replyMap["groundedContent"].(map[string]interface{})
if !ok {
continue
}
content, ok := groundedContent["content"].(map[string]interface{})
if !ok {
continue
}
// 检查functionCall
if fc, ok := content["functionCall"].(map[string]interface{}); ok {
name, _ := fc["name"].(string)
args, _ := fc["args"].(map[string]interface{})
argsBytes, _ := json.Marshal(args)
toolCalls = append(toolCalls, ToolCall{
ID: "call_" + uuid.New().String()[:8],
Type: "function",
Function: FunctionCall{
Name: name,
Arguments: string(argsBytes),
},
})
}
}
}
return toolCalls
}
// needsConversationContext 检查是否需要对话上下文(多轮对话)
func needsConversationContext(messages []Message) bool {
// 检查是否有多轮对话标志:存在assistant或tool消息
for _, msg := range messages {
if msg.Role == "assistant" || msg.Role == "tool" || msg.Role == "tool_result" {
return true
}
}
return false
}
// handleFlowRequest 处理 Flow 模型请求
func handleFlowRequest(c *gin.Context, req ChatRequest, chatID string, createdTime int64) {
if flowHandler == nil {
c.JSON(503, gin.H{"error": gin.H{
"message": "Flow 服务未启用,请在配置文件中启用并添加 Token",
"type": "service_unavailable",
}})
return
}
// 解析消息内容和图片
var prompt string
var imageBytes [][]byte
for _, msg := range req.Messages {
if msg.Role == "user" || msg.Role == "human" {
text, images := parseMessageContent(msg)
if text != "" {
prompt = text
}
// 提取图片数据
for _, img := range images {
if img.Data != "" {
imgData, err := base64.StdEncoding.DecodeString(img.Data)
if err == nil {
imageBytes = append(imageBytes, imgData)
}
}
}
}
}
if prompt == "" {
c.JSON(400, gin.H{"error": gin.H{
"message": "Prompt cannot be empty",
"type": "invalid_request_error",
}})
return
}
flowReq := flow.GenerationRequest{
Model: req.Model,
Prompt: prompt,
Images: imageBytes,
Stream: req.Stream,
}
if req.Stream {
// 流式响应
c.Header("Content-Type", "text/event-stream")
c.Header("Cache-Control", "no-cache")
c.Header("Connection", "keep-alive")
c.Header("X-Accel-Buffering", "no")
c.Status(200)
flusher, ok := c.Writer.(http.Flusher)
if !ok {
c.JSON(500, gin.H{"error": "Streaming not supported"})
return
}
result, _ := flowHandler.HandleGeneration(flowReq, func(chunk string) {
c.Writer.WriteString(chunk)
flusher.Flush()
})
// 发送 [DONE]
c.Writer.WriteString("data: [DONE]\n\n")
flusher.Flush()
if result != nil && !result.Success && result.Error != "" {
logger.Error("❌ [Flow] 生成失败: %s", result.Error)
}
} else {
// 非流式响应
result, err := flowHandler.HandleGeneration(flowReq, nil)
if err != nil {
c.JSON(500, gin.H{"error": gin.H{
"message": err.Error(),
"type": "internal_error",
}})
return
}
if !result.Success {
c.JSON(500, gin.H{"error": gin.H{
"message": result.Error,
"type": "generation_failed",
}})
return
}
// 构建响应
content := result.URL
if result.Type == "image" {
content = fmt.Sprintf("", result.URL)
} else if result.Type == "video" {
content = fmt.Sprintf("<video src='%s' controls></video>", result.URL)
}
c.JSON(200, gin.H{
"id": chatID,
"object": "chat.completion",
"created": createdTime,
"model": req.Model,
"choices": []gin.H{{
"index": 0,
"message": gin.H{
"role": "assistant",
"content": content,
},
"finish_reason": "stop",
}},
})
}
}
func streamChat(c *gin.Context, req ChatRequest) {
chatID := "chatcmpl-" + uuid.New().String()
createdTime := time.Now().Unix()
clientIP := c.ClientIP()
userAgent := c.GetHeader("User-Agent")
// 统计变量
var statsSuccess bool
var statsInputTokens int64
var statsOutputTokens int64
var statsImages int64
var statsVideos int64
statsModel := req.Model
defer func() {
apiStats.RecordRequestWithModel(statsModel, statsSuccess, statsInputTokens, statsOutputTokens, statsImages, statsVideos)
// 记录IP统计(包含tokens、图片、视频)
ipStats.RecordIPRequest(clientIP, statsModel, userAgent, statsSuccess, statsInputTokens, statsOutputTokens, statsImages, statsVideos)
}()
// 入站日志
logger.Info("📥 [%s] 请求: model=%s ", clientIP, req.Model)
if flow.IsFlowModel(req.Model) {
handleFlowRequest(c, req, chatID, createdTime)
return
}
var textContent string
var images []MediaInfo
systemPrompt := extractSystemPrompt(req.Messages)
if needsConversationContext(req.Messages) {
// 多轮对话:拼接所有消息(包含system)
textContent = convertMessagesToPrompt(req.Messages)
// 只从最后一条用户消息提取图片
for i := len(req.Messages) - 1; i >= 0; i-- {
if req.Messages[i].Role == "user" || req.Messages[i].Role == "human" {
_, images = parseMessageContent(req.Messages[i])
break
}
}
} else {
lastMsg := req.Messages[len(req.Messages)-1]
userText, userImages := parseMessageContent(lastMsg)
images = userImages
if systemPrompt != "" {
textContent = fmt.Sprintf("<system>\n%s\n</system>\n\nHuman: %s\n\nAssistant:", systemPrompt, userText)
} else {
textContent = userText
}
}
var respBody []byte
var lastErr error
var lastErrStatusCode int // 保存最后一次错误的 HTTP 状态码
var lastErrBody []byte // 保存最后一次错误的响应体
var usedAcc *pool.Account
var usedJWT, usedOrigAuth, usedConfigID, usedSession string
isLongRunning := !req.Stream && (strings.Contains(req.Model, "video") ||
strings.Contains(req.Model, "imagen") ||
strings.Contains(req.Model, "image"))
var heartbeatDone chan struct{}
if isLongRunning {
heartbeatDone = make(chan struct{})
c.Header("Content-Type", "application/json")
c.Header("Transfer-Encoding", "chunked")
c.Status(200)
writer := c.Writer
flusher, ok := writer.(http.Flusher)
if ok {
flusher.Flush() // 先发送头部
}
go func() {
defer func() {
if r := recover(); r != nil {
}
}()
ticker := time.NewTicker(15 * time.Second)
defer ticker.Stop()
for {
select {
case <-heartbeatDone:
return
case <-ticker.C:
if _, err := writer.Write([]byte(" ")); err != nil {
return
}
if flusher, ok := writer.(http.Flusher); ok {
flusher.Flush()
}
}
}
}()
}
defer func() {
if heartbeatDone != nil {
select {
case <-heartbeatDone:
default:
close(heartbeatDone)
}
}
}()
// 估算输入 tokens(基于文本长度)
statsInputTokens = int64(len(textContent)/4) + int64(len(images)*500) // 文本 + 图片估算
// 流式请求:提前发送 SSE 头部,避免上游请求期间客户端等待超时
var streamWriter http.ResponseWriter
var streamFlusher http.Flusher
var streamStarted bool
if req.Stream {
c.Header("Content-Type", "text/event-stream")
c.Header("Cache-Control", "no-cache")
c.Header("Connection", "keep-alive")
c.Header("X-Accel-Buffering", "no")
streamWriter = c.Writer
streamFlusher, _ = streamWriter.(http.Flusher)
chunk := createChunk(chatID, createdTime, req.Model, map[string]interface{}{"role": "assistant"}, nil)
fmt.Fprintf(streamWriter, "data: %s\n\n", chunk)
streamFlusher.Flush()
streamStarted = true
}
for retry := 0; retry < maxRetries; retry++ {
acc := pool.Pool.Next()
if acc == nil {
if streamStarted {
// 流式请求已开始,发送 SSE 格式错误
errChunk := createChunk(chatID, createdTime, req.Model, map[string]interface{}{"content": "[错误] 没有可用账号"}, nil)
fmt.Fprintf(streamWriter, "data: %s\n\n", errChunk)
finishReason := "stop"
finalChunk := createChunk(chatID, createdTime, req.Model, nil, &finishReason)
fmt.Fprintf(streamWriter, "data: %s\n\n", finalChunk)
fmt.Fprintf(streamWriter, "data: [DONE]\n\n")
streamFlusher.Flush()
} else {
c.JSON(500, gin.H{"error": "没有可用账号"})
}
return
}
usedAcc = acc
logger.Info("📤 [%s] 使用账号: %s", clientIP, acc.Data.Email)
if retry > 0 {
logger.Info("🔄 第 %d 次重试,切换账号: %s", retry+1, acc.Data.Email)
}
jwt, configID, err := acc.GetJWT()
if err != nil {
logger.Error("❌ [%s] 获取 JWT 失败: %v", acc.Data.Email, err)
lastErr = err
continue
}
session, err := createSession(jwt, configID, acc.Data.Authorization)
if err != nil {
logger.Error("❌ [%s] 创建 Session 失败: %v", acc.Data.Email, err)
// 401 错误标记账号需要刷新
if strings.Contains(err.Error(), "401") || strings.Contains(err.Error(), "UNAUTHENTICATED") {
// pool.Pool.MarkNeedsRefresh(acc)
}
lastErr = err
continue
}
// 上传媒体文件并获取 fileIds
var fileIds []string
uploadFailed := false
for _, media := range images {
var fileId string
var err error
mediaTypeName := "图片"
if media.MediaType == "video" {
mediaTypeName = "视频"
}
if media.IsURL {
// 优先尝试 URL 直接上传
fileId, err = uploadContextFileByURL(jwt, configID, session, media.URL, acc.Data.Authorization)
if err != nil {
// URL 上传失败,回退到下载后上传
mediaData, mimeType, dlErr := downloadMedia(media.URL, media.MediaType)
if dlErr != nil {
logger.Warn("⚠️ [%s] %s下载失败: %v", acc.Data.Email, mediaTypeName, dlErr)
if strings.Contains(dlErr.Error(), "UPSTREAM_401") || strings.Contains(dlErr.Error(), "UPSTREAM_403") {
c.JSON(500, gin.H{"error": gin.H{
"message": dlErr.Error(),
"type": "upstream_error",
"code": "media_download_failed",
}})
return
}
uploadFailed = true
break
}
fileId, err = uploadContextFile(jwt, configID, session, mimeType, mediaData, acc.Data.Authorization)
}
} else {
fileId, err = uploadContextFile(jwt, configID, session, media.MimeType, media.Data, acc.Data.Authorization)
}
if err != nil {
logger.Warn("⚠️ [%s] %s上传失败: %v", acc.Data.Email, mediaTypeName, err)
uploadFailed = true
break
}
fileIds = append(fileIds, fileId)
}
if uploadFailed {
lastErr = fmt.Errorf("媒体上传失败")
continue
}
// 构建 query parts(只包含文本)
queryParts := []map[string]interface{}{}
if textContent != "" {
queryParts = append(queryParts, map[string]interface{}{"text": textContent})
}
// 确保 queryParts 不为空,避免 Google 返回空响应
if len(queryParts) == 0 {
queryParts = append(queryParts, map[string]interface{}{"text": " "})
}
isImageModel := strings.Contains(req.Model, "-image")
isVideoModel := strings.Contains(req.Model, "-video")
isSearchModel := strings.Contains(req.Model, "-search")
actualModel := req.Model
actualModel = strings.ReplaceAll(actualModel, "-image", "")
actualModel = strings.ReplaceAll(actualModel, "-video", "")
actualModel = strings.ReplaceAll(actualModel, "-search", "")
// 构建 toolsSpec(支持自定义工具)
toolsSpec := buildToolsSpec(req.Tools, isImageModel, isVideoModel, isSearchModel)
body := map[string]interface{}{
"configId": configID,
"additionalParams": map[string]string{"token": "-"},
"streamAssistRequest": map[string]interface{}{
"session": session,
"query": map[string]interface{}{"parts": queryParts},
"filter": "",
"fileIds": fileIds,
"answerGenerationMode": "NORMAL",
"toolsSpec": toolsSpec,
"languageCode": "zh-CN",
"userMetadata": map[string]string{"timeZone": "Asia/Shanghai"},
"assistSkippingMode": "REQUEST_ASSIST",
},
}
// 设置模型 ID(去掉 -image 后缀)
if targetModelID, ok := modelMapping[actualModel]; ok && targetModelID != "" {
body["streamAssistRequest"].(map[string]interface{})["assistGenerationConfig"] = map[string]interface{}{
"modelId": targetModelID,
}
}
bodyBytes, _ := json.Marshal(body)
httpReq, _ := http.NewRequest("POST", "https://biz-discoveryengine.googleapis.com/v1alpha/locations/global/widgetStreamAssist", bytes.NewReader(bodyBytes))
for k, v := range getCommonHeaders(jwt, acc.Data.Authorization) {
httpReq.Header.Set(k, v)
}
resp, err := utils.HTTPClient.Do(httpReq)
if err != nil {
logger.Error("❌ [%s] 请求失败: %v", acc.Data.Email, err)
lastErr = err
continue
}
if resp.StatusCode != 200 {
body, _ := utils.ReadResponseBody(resp)
resp.Body.Close()
logger.Error("❌ [%s] Google 报错: %d %s (重试 %d/%d)", acc.Data.Email, resp.StatusCode, string(body), retry+1, maxRetries)
lastErr = fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
lastErrStatusCode = resp.StatusCode
lastErrBody = body
// 401/403 无权限,标记需要刷新
if resp.StatusCode == 401 || resp.StatusCode == 403 {
logger.Warn("⚠️ [%s] %d 无权限,标记需要刷新", acc.Data.Email, resp.StatusCode)
pool.Pool.MarkNeedsRefresh(acc)
}
// 429 限流,延长使用冷却时间(3倍冷却)
if resp.StatusCode == 429 {
cooldownTime := pool.UseCooldown * 3
acc.Mu.Lock()
acc.LastUsed = time.Now().Add(cooldownTime)
acc.Mu.Unlock()
logger.Info("⏳ [%s] 429 限流,账号进入延长冷却 %v", acc.Data.Email, cooldownTime)
pool.Pool.MarkUsed(acc, false)
time.Sleep(1 * time.Second) // 短暂等待后切换账号
retry-- // 不计入重试次数
continue
}
if resp.StatusCode == 400 {
logger.Warn("⚠️ [%s] 400 错误,换账号重试", acc.Data.Email)
pool.Pool.MarkUsed(acc, false)
time.Sleep(500 * time.Millisecond)
continue
}
pool.Pool.MarkUsed(acc, false) // 标记失败
continue
}
// 成功,读取响应
respBody, _ = utils.ReadResponseBody(resp)
resp.Body.Close()
// Debug 模式输出上游响应
if logger.IsDebug() {
respSnippet := string(respBody)
if len(respSnippet) > 2000 {
respSnippet = respSnippet[:2000] + "..."
}
logger.Debug("[%s] 上游响应: %s", acc.Data.Email, respSnippet)
}
// 快速检查是否是认证错误响应
if bytes.Contains(respBody, []byte("uToken")) && !bytes.Contains(respBody, []byte("streamAssistResponse")) {
logger.Warn("[%s] 收到认证响应,标记需要刷新", acc.Data.Email)
pool.Pool.MarkNeedsRefresh(acc)
lastErr = fmt.Errorf("认证失败,需要刷新账号")
continue
}
// 检查是否有实际内容(非空返回)
hasText := bytes.Contains(respBody, []byte(`"text"`))
hasFile := bytes.Contains(respBody, []byte(`"file"`))
hasInlineData := bytes.Contains(respBody, []byte(`"inlineData"`))
hasThought := bytes.Contains(respBody, []byte(`"thought"`))
hasFunctionCall := bytes.Contains(respBody, []byte(`"functionCall"`))
hasError := bytes.Contains(respBody, []byte(`"error"`)) || bytes.Contains(respBody, []byte(`"errorMessage"`))
hasContent := hasText || hasFile || hasInlineData || hasFunctionCall
// 检测是否有服务端错误信息
if hasError && !hasContent {
logger.Warn("[%s] 响应包含错误信息,重试 (%d/%d)", acc.Data.Email, retry+1, maxRetries)
// 简单解析错误类型
if bytes.Contains(respBody, []byte("RESOURCE_EXHAUSTED")) || bytes.Contains(respBody, []byte("quota")) {
logger.Info("⏳ [%s] 检测到配额耗尽,标记冷却", acc.Data.Email)
acc.SetCooldownMultiplier(5) // 5倍冷却
pool.Pool.MarkUsed(acc, false)
}
lastErr = fmt.Errorf("上游返回错误响应")
continue
}
// 响应完全为空或只有思考内容
if !hasContent {
if hasThought {
logger.Warn("[%s] 响应只有思考内容,无实际输出,换号重试 (%d/%d)", acc.Data.Email, retry+1, maxRetries)
lastErr = fmt.Errorf("空返回,只有思考内容")
// 思考中的账号不标记失败,可能只是请求太慢
time.Sleep(500 * time.Millisecond)
} else {
logger.Warn("[%s] 响应无有效内容 (text/file/inlineData/functionCall),换号重试 (%d/%d)", acc.Data.Email, retry+1, maxRetries)
lastErr = fmt.Errorf("空返回,无有效内容")
pool.Pool.MarkUsed(acc, false)
}
continue
}
usedJWT = jwt
usedOrigAuth = acc.Data.Authorization
usedConfigID = configID
usedSession = session // 保存创建的 session 作为回退
usedAcc = acc
lastErr = nil
pool.Pool.MarkUsed(acc, true) // 标记成功
break
}
if lastErr != nil {
logger.Error("❌ 所有重试均失败: %v", lastErr)
if streamStarted {
// 流式请求已开始,发送 SSE 格式错误
errMsg := fmt.Sprintf("[错误] %v", lastErr)
errChunk := createChunk(chatID, createdTime, req.Model, map[string]interface{}{"content": errMsg}, nil)
fmt.Fprintf(streamWriter, "data: %s\n\n", errChunk)
finishReason := "stop"
finalChunk := createChunk(chatID, createdTime, req.Model, nil, &finishReason)
fmt.Fprintf(streamWriter, "data: %s\n\n", finalChunk)
fmt.Fprintf(streamWriter, "data: [DONE]\n\n")
streamFlusher.Flush()
} else if lastErrStatusCode > 0 && len(lastErrBody) > 0 {
// 如果有 HTTP 错误响应体,原样透传
c.Data(lastErrStatusCode, "application/json", lastErrBody)
} else {
c.JSON(500, gin.H{"error": lastErr.Error()})
}
return
}
_ = usedAcc
// 检查空响应
if len(respBody) == 0 {
logger.Error("❌ 响应为空")
if streamStarted {
errChunk := createChunk(chatID, createdTime, req.Model, map[string]interface{}{"content": "[错误] 上游返回空响应"}, nil)
fmt.Fprintf(streamWriter, "data: %s\n\n", errChunk)
finishReason := "stop"
finalChunk := createChunk(chatID, createdTime, req.Model, nil, &finishReason)
fmt.Fprintf(streamWriter, "data: %s\n\n", finalChunk)
fmt.Fprintf(streamWriter, "data: [DONE]\n\n")
streamFlusher.Flush()
} else {
c.JSON(500, gin.H{"error": "Empty response from Google"})
}
return
}
// 解析响应:支持多种格式
var dataList []map[string]interface{}
var parseErr error
// 1. 尝试标准 JSON 数组
if parseErr = json.Unmarshal(respBody, &dataList); parseErr != nil {
logger.Warn("⚠️ JSON 数组解析失败: %v, 响应前100字符: %s", parseErr, string(respBody[:min(100, len(respBody))]))
// 2. 尝试修复不完整的 JSON 数组
dataList = utils.ParseIncompleteJSONArray(respBody)
if dataList == nil {
// 3. 尝试 NDJSON 格式
logger.Warn("⚠️ 尝试 NDJSON 格式...")
dataList = utils.ParseNDJSON(respBody)
}
if len(dataList) == 0 {
// 输出完整响应用于调试
respStr := string(respBody)
if len(respStr) > 500 {
logger.Error("❌ 所有解析方式均失败, 响应长度: %d, 前500字符: %s", len(respBody), respStr[:500])
logger.Error("❌ 后200字符: %s", respStr[len(respStr)-200:])
} else {
logger.Error("❌ 所有解析方式均失败, 响应长度: %d, 完整响应: %s", len(respBody), respStr)
}
if streamStarted {
errChunk := createChunk(chatID, createdTime, req.Model, map[string]interface{}{"content": "[错误] 响应解析失败"}, nil)
fmt.Fprintf(streamWriter, "data: %s\n\n", errChunk)
finishReason := "stop"
finalChunk := createChunk(chatID, createdTime, req.Model, nil, &finishReason)
fmt.Fprintf(streamWriter, "data: %s\n\n", finalChunk)
fmt.Fprintf(streamWriter, "data: [DONE]\n\n")
streamFlusher.Flush()
} else {
c.JSON(500, gin.H{"error": "JSON Parse Error"})
}
return
}
logger.Info("✅ 备用解析成功,共 %d 个对象", len(dataList))
}
// 检查是否有有效响应
if len(dataList) > 0 {
hasValidResponse := false
hasFileContent := false
for _, data := range dataList {
if streamResp, ok := data["streamAssistResponse"].(map[string]interface{}); ok {
hasValidResponse = true
// 检查是否有文件内容
if answer, ok := streamResp["answer"].(map[string]interface{}); ok {
if replies, ok := answer["replies"].([]interface{}); ok {
for _, reply := range replies {
if replyMap, ok := reply.(map[string]interface{}); ok {
if gc, ok := replyMap["groundedContent"].(map[string]interface{}); ok {
if content, ok := gc["content"].(map[string]interface{}); ok {
if _, ok := content["file"]; ok {
hasFileContent = true
}
}
}
}
}
}
}
}
}
if !hasValidResponse {
logger.Warn("⚠️ 响应中没有 streamAssistResponse,响应内容: %v", dataList[0])
}
logger.Debug("📊 响应统计: %d 个数据块, 有效响应=%v, 包含文件=%v", len(dataList), hasValidResponse, hasFileContent)
}
// 从响应中提取 session(用于下载图片)
var respSession string
for _, data := range dataList {
if streamResp, ok := data["streamAssistResponse"].(map[string]interface{}); ok {
if sessionInfo, ok := streamResp["sessionInfo"].(map[string]interface{}); ok {
if s, ok := sessionInfo["session"].(string); ok && s != "" {
respSession = s
break
}
}
}
}
// 如果响应中没有 session,使用请求时创建的 session 作为回退
if respSession == "" {
if usedSession != "" {
logger.Warn("⚠️ 响应中未找到 session,使用请求时创建的 session: %s", usedSession)
respSession = usedSession
} else {
logger.Warn("⚠️ 响应中未找到 session 且无回退 session,图片/视频下载可能失败")
}
} else {
}
// 待下载的文件信息
type PendingFile struct {
FileID string
MimeType string
}
if req.Stream {
// 流式响应:文本/思考实时输出,图片最后处理
// SSE 头部和 role chunk 已在请求前发送,复用 streamWriter/streamFlusher
writer := streamWriter
flusher := streamFlusher
// 统计输出内容长度
var outputLen int64
// 收集待下载的文件和工具调用
var pendingFiles []PendingFile
hasToolCalls := false
for _, data := range dataList {
streamResp, ok := data["streamAssistResponse"].(map[string]interface{})
if !ok {
continue
}
answer, ok := streamResp["answer"].(map[string]interface{})
if !ok {
continue
}
replies, ok := answer["replies"].([]interface{})
if !ok {
continue
}
for _, reply := range replies {
replyMap, ok := reply.(map[string]interface{})
if !ok {
continue
}
groundedContent, ok := replyMap["groundedContent"].(map[string]interface{})
if !ok {
continue
}
content, ok := groundedContent["content"].(map[string]interface{})
if !ok {
continue
}
// 检查是否是思考内容
if thought, ok := content["thought"].(bool); ok && thought {
if t, ok := content["text"].(string); ok && t != "" {
chunk := createChunk(chatID, createdTime, req.Model, map[string]interface{}{"reasoning_content": t}, nil)
fmt.Fprintf(writer, "data: %s\n\n", chunk)
flusher.Flush()
outputLen += int64(len(t))
}
continue
}
// 输出文本(实时)
if t, ok := content["text"].(string); ok && t != "" {
chunk := createChunk(chatID, createdTime, req.Model, map[string]interface{}{"content": t}, nil)
fmt.Fprintf(writer, "data: %s\n\n", chunk)
flusher.Flush()
outputLen += int64(len(t))
}
// 处理 inlineData(直接有 base64 数据的图片)
if inlineData, ok := content["inlineData"].(map[string]interface{}); ok {
mime, _ := inlineData["mimeType"].(string)
data, _ := inlineData["data"].(string)
if mime != "" && data != "" {
imgMarkdown := formatImageAsMarkdown(mime, data)
chunk := createChunk(chatID, createdTime, req.Model, map[string]interface{}{"content": imgMarkdown}, nil)
fmt.Fprintf(writer, "data: %s\n\n", chunk)
flusher.Flush()
}
}
// 收集需要下载的文件(图片/视频)
if file, ok := content["file"].(map[string]interface{}); ok {
fileId, _ := file["fileId"].(string)
mimeType, _ := file["mimeType"].(string)
if fileId != "" {
pendingFiles = append(pendingFiles, PendingFile{FileID: fileId, MimeType: mimeType})
}
}
if fc, ok := content["functionCall"].(map[string]interface{}); ok {
hasToolCalls = true
name, _ := fc["name"].(string)
args, _ := fc["args"].(map[string]interface{})
argsBytes, _ := json.Marshal(args)
toolCall := ToolCall{
ID: "call_" + uuid.New().String()[:8],
Type: "function",
Function: FunctionCall{
Name: name,
Arguments: string(argsBytes),
},
}
chunk := createChunk(chatID, createdTime, req.Model, map[string]interface{}{
"tool_calls": []map[string]interface{}{{
"index": 0,
"id": toolCall.ID,
"type": "function",
"function": map[string]interface{}{
"name": toolCall.Function.Name,
"arguments": toolCall.Function.Arguments,
},
}},
}, nil)
fmt.Fprintf(writer, "data: %s\n\n", chunk)
flusher.Flush()
}
}
}
if len(pendingFiles) > 0 {
logger.Info("📥 开始下载 %d 个文件...", len(pendingFiles))
type downloadResult struct {
Index int
Data string
MimeType string
Err error
}
results := make(chan downloadResult, len(pendingFiles))
var wg sync.WaitGroup
for i, pf := range pendingFiles {
wg.Add(1)
go func(idx int, file PendingFile) {
defer wg.Done()
data, err := downloadGeneratedFile(usedJWT, file.FileID, respSession, usedConfigID, usedOrigAuth)
results <- downloadResult{Index: idx, Data: data, MimeType: file.MimeType, Err: err}
}(i, pf)
}
go func() {
wg.Wait()
close(results)
}()
downloaded := make([]downloadResult, len(pendingFiles))
for r := range results {
downloaded[r.Index] = r
}
// 按顺序输出
successCount := 0
var lastErr error
needsRetry := false
for i, r := range downloaded {
if r.Err != nil {
logger.Error("❌ 下载文件[%d]失败: %v", i, r.Err)
lastErr = r.Err
// 检测是否需要换号重试
if errors.Is(r.Err, ErrDownloadNeedsRetry) {
needsRetry = true
}
continue
}
imgMarkdown := formatImageAsMarkdown(r.MimeType, r.Data)
chunk := createChunk(chatID, createdTime, req.Model, map[string]interface{}{"content": imgMarkdown}, nil)
fmt.Fprintf(writer, "data: %s\n\n", chunk)
flusher.Flush()
successCount++
}
// 如果所有文件都下载失败
if successCount == 0 && lastErr != nil {
var errMsg string
if needsRetry {
// 401/403 认证失败,提示用户重试(下次会使用新账号)
errMsg = "[提示] 文件下载认证失败,请重新发送请求(系统将自动切换账号)"
pool.Pool.MarkNeedsRefresh(usedAcc) // 标记当前账号需要刷新
} else {
errMsg = fmt.Sprintf("生成的文件下载失败: %v", lastErr)
}
chunk := createChunk(chatID, createdTime, req.Model, map[string]interface{}{"content": errMsg}, nil)
fmt.Fprintf(writer, "data: %s\n\n", chunk)
flusher.Flush()
}
}
// 发送结束
finishReason := "stop"
if hasToolCalls {
finishReason = "tool_calls"
}
finalChunk := createChunk(chatID, createdTime, req.Model, nil, &finishReason)
fmt.Fprintf(writer, "data: %s\n\n", finalChunk)
fmt.Fprintf(writer, "data: [DONE]\n\n")
flusher.Flush()
// 更新统计(区分图片和视频)
statsSuccess = true
statsOutputTokens = outputLen / 4 // 估算输出 tokens
for _, pf := range pendingFiles {
if strings.HasPrefix(pf.MimeType, "video/") {
statsVideos++
} else {
statsImages++
}
}
} else {
// 非流式响应
var fullContent strings.Builder
var fullReasoning strings.Builder
replyCount := 0
var fileCount int64
var videoCount int64
for _, data := range dataList {
streamResp, ok := data["streamAssistResponse"].(map[string]interface{})
if !ok {
continue
}
answer, ok := streamResp["answer"].(map[string]interface{})
if !ok {
continue
}
replies, ok := answer["replies"].([]interface{})
if !ok {
continue
}
for _, reply := range replies {
replyMap, ok := reply.(map[string]interface{})
if !ok {
continue
}
replyCount++
if gc, ok := replyMap["groundedContent"].(map[string]interface{}); ok {
if content, ok := gc["content"].(map[string]interface{}); ok {
if file, ok := content["file"].(map[string]interface{}); ok {
if mimeType, _ := file["mimeType"].(string); strings.HasPrefix(mimeType, "video/") {
videoCount++
} else {
fileCount++
}
}
}
}
text, imageData, imageMime, reasoning, dlErr := extractContentFromReply(replyMap, usedJWT, respSession, usedConfigID, usedOrigAuth)
if reasoning != "" {
fullReasoning.WriteString(reasoning)
}
if text != "" {
fullContent.WriteString(text)
}
if imageData != "" && imageMime != "" {
fullContent.WriteString(formatImageAsMarkdown(imageMime, imageData))
}
// 检测下载是否需要重试(401/403)
if dlErr != nil && errors.Is(dlErr, ErrDownloadNeedsRetry) {
pool.Pool.MarkNeedsRefresh(usedAcc)
fullContent.WriteString("\n\n[提示] 文件下载认证失败,请重新发送请求(系统将自动切换账号)")
}
}
}
toolCalls := extractToolCalls(dataList)
// 调试日志
logger.Debug("📊 非流式响应统计: %d 个 reply, 图片=%d, 视频=%d, content长度=%d, reasoning长度=%d, 工具调用=%d",
replyCount, fileCount, videoCount, fullContent.Len(), fullReasoning.Len(), len(toolCalls))
// 构建响应消息
message := gin.H{
"role": "assistant",
"content": fullContent.String(),
}
if fullReasoning.Len() > 0 {
message["reasoning_content"] = fullReasoning.String()
}
finishReason := "stop"
if len(toolCalls) > 0 {
message["tool_calls"] = toolCalls
message["content"] = nil
finishReason = "tool_calls"
}
// 构建最终响应(完全符合OpenAI格式)
response := gin.H{
"id": chatID,
"object": "chat.completion",
"created": createdTime,
"model": req.Model,
"system_fingerprint": "fp_gemini_" + req.Model,
"choices": []gin.H{{
"index": 0,
"message": message,
"logprobs": nil,
"finish_reason": finishReason,
}},
"usage": gin.H{
"prompt_tokens": 0,
"completion_tokens": 0,
"total_tokens": 0,
},
}
if isLongRunning && heartbeatDone != nil {
close(heartbeatDone) // 停止心跳
jsonBytes, _ := json.Marshal(response)
c.Writer.Write(jsonBytes)
} else {
c.JSON(200, response)
}
// 更新统计
statsSuccess = true
statsOutputTokens = int64(fullContent.Len() / 4) // 粗略估算输出 tokens
statsImages = fileCount
statsVideos = videoCount
}
}
func apiKeyAuth() gin.HandlerFunc {
return func(c *gin.Context) {
// 使用线程安全的方式获取 API Keys
apiKeys := GetAPIKeys()
if len(apiKeys) == 0 {
c.Next()
return
}
authHeader := c.GetHeader("Authorization")
apiKey := ""
if strings.HasPrefix(authHeader, "Bearer ") {
apiKey = strings.TrimPrefix(authHeader, "Bearer ")
} else {
apiKey = c.GetHeader("X-API-Key")
}
if apiKey == "" {
c.JSON(401, gin.H{"error": "Missing API key"})
c.Abort()
return
}
// 验证 API Key
valid := false
for _, key := range apiKeys {
if key == apiKey {
valid = true
break
}
}
if !valid {
c.JSON(401, gin.H{"error": "Invalid API key"})
c.Abort()
return
}
c.Next()
}
}
// runBrowserRefreshMode 有头浏览器刷新模式
func runBrowserRefreshMode(email string) {
loadAppConfig()
utils.InitHTTPClient(Proxy)
// 强制有头模式
pool.BrowserRefreshHeadless = false
logger.Info("🌐 有头浏览器刷新模式")
if err := pool.Pool.Load(DataDir); err != nil {
log.Fatalf("❌ 加载账号失败: %v", err)
}
if pool.Pool.TotalCount() == 0 {
log.Fatal("❌ 没有可用账号")
}
// 查找目标账号
var targetAcc *pool.Account
pool.Pool.WithLock(func(ready, pending []*pool.Account) {
if email != "" {
// 指定邮箱
for _, acc := range ready {
if acc.Data.Email == email {
targetAcc = acc
break
}
}
if targetAcc == nil {
for _, acc := range pending {
if acc.Data.Email == email {
targetAcc = acc
break
}
}
}
} else {
// 使用第一个账号
if len(ready) > 0 {
targetAcc = ready[0]
} else if len(pending) > 0 {
targetAcc = pending[0]
}
}
})
if targetAcc == nil {
if email != "" {
log.Fatalf("❌ 找不到账号: %s", email)
}
log.Fatal("❌ 没有可用账号")
}
result := register.RefreshCookieWithBrowser(targetAcc, false, Proxy)
if result.Success {
if len(result.NewCookies) > 0 {
}
if len(result.ResponseHeaders) > 0 {
}
// 更新账号数据
targetAcc.Mu.Lock()
targetAcc.Data.Cookies = result.SecureCookies
if result.Authorization != "" {
targetAcc.Data.Authorization = result.Authorization
}
if result.ConfigID != "" {
targetAcc.ConfigID = result.ConfigID
targetAcc.Data.ConfigID = result.ConfigID
}
if result.CSESIDX != "" {
targetAcc.CSESIDX = result.CSESIDX
targetAcc.Data.CSESIDX = result.CSESIDX
}
// 保存响应头
if len(result.ResponseHeaders) > 0 {
targetAcc.Data.ResponseHeaders = result.ResponseHeaders
}
targetAcc.Mu.Unlock()
// 保存到文件
if err := targetAcc.SaveToFile(); err != nil {
logger.Warn("⚠️ 保存失败: %v", err)
} else {
logger.Info("💾 已保存到: %s", targetAcc.FilePath)
}
} else {
logger.Error("❌ 刷新失败: %v", result.Error)
}
}
var AutoSubscribeEnabled bool
func init() {
// 设置环境变量禁用 quic-go 的警告
os.Setenv("QUIC_GO_DISABLE_RECEIVE_BUFFER_WARNING", "true")
filterStdout()
}
func filterStdout() {
// 创建管道
r, w, err := os.Pipe()
if err != nil {
return
}
origStdout := os.Stdout
os.Stdout = w
go func() {
buf := make([]byte, 4096)
for {
n, err := r.Read(buf)
if err != nil {
break
}
line := string(buf[:n])
// 过滤特定日志
if strings.Contains(line, "REALITY localAddr:") ||
strings.Contains(line, "DialTLSContext") ||
strings.Contains(line, "sys_conn.go") ||
strings.Contains(line, "failed to sufficiently increase receive buffer size") {
continue // 丢弃
}
origStdout.Write(buf[:n])
}
}()
}
func main() {
log.SetFlags(log.Ltime | log.Lshortfile)
var refreshEmail string
var refreshMode bool
// 解析命令行参数
for i, arg := range os.Args[1:] {
switch arg {
case "--debug", "-d":
register.RegisterDebug = true
logger.Info("🔧 调试模式已启用,将保存截图到 data/screenshots/")
case "--once":
register.RegisterOnce = true
logger.Info("🔧 单次运行模式")
case "--auto":
AutoSubscribeEnabled = true
case "--refresh":
refreshMode = true
// 检查下一个参数是否是邮箱
if i+2 < len(os.Args) && !strings.HasPrefix(os.Args[i+2], "-") {
refreshEmail = os.Args[i+2]
}
case "--help", "-h":
fmt.Println(`用法: ./business2api [选项]
选项:
--debug, -d 调试模式,保存注册过程截图
--auto 自动订阅模式,每小时注册获取代理
--refresh [email] 有头浏览器刷新账号(不指定email则使用第一个账号)
--help, -h 显示帮助`)
os.Exit(0)
}
}
// 刷新模式:直接执行浏览器刷新后退出
if refreshMode {
runBrowserRefreshMode(refreshEmail)
return
}
loadAppConfig()
utils.InitHTTPClient(Proxy)
if appConfig.PoolServer.Enable {
switch appConfig.PoolServer.Mode {
case "client":
runAsClient()
return
case "server":
runAsServer()
return
}
}
// 本地模式
runLocalMode()
}
func runAsClient() {
logger.Info("🔌 启动客户端模式...")
// 代理实例池由异步健康检查完成后初始化
// 设置代理就绪检查回调
pool.IsProxyReady = func() bool {
return proxy.Manager.IsReady()
}
pool.WaitProxyReady = func(timeout time.Duration) bool {
logger.Info("⏳ 等待代理就绪...")
result := proxy.Manager.WaitReady(timeout)
if result {
logger.Info("✅ 代理已就绪")
} else {
logger.Warn("⚠️ 代理等待超时")
}
return result
}
pool.RunBrowserRegister = func(headless bool, proxyURL string, id int) *pool.BrowserRegisterResult {
result := register.RunBrowserRegister(headless, proxyURL, id)
return &pool.BrowserRegisterResult{
Success: result.Success,
Email: result.Email,
FullName: result.FullName,
SecureCookies: result.Cookies,
Authorization: result.Authorization,
ConfigID: result.ConfigID,
CSESIDX: result.CSESIDX,
Error: result.Error,
}
}
pool.RefreshCookieWithBrowser = func(acc *pool.Account, headless bool, proxyURL string) *pool.BrowserRefreshResult {
result := register.RefreshCookieWithBrowser(acc, headless, proxyURL)
return &pool.BrowserRefreshResult{
Success: result.Success,
SecureCookies: result.SecureCookies,
ConfigID: result.ConfigID,
CSESIDX: result.CSESIDX,
Authorization: result.Authorization,
ResponseHeaders: result.ResponseHeaders,
Error: result.Error,
}
}
pool.ClientHeadless = appConfig.Pool.RegisterHeadless
pool.ClientProxy = Proxy
pool.GetClientProxy = func() string {
if proxy.Manager.HealthyCount() > 0 {
proxyURL := proxy.Manager.Next()
if proxyURL != "" {
return proxyURL
}
}
return Proxy
}
pool.ReleaseProxy = func(proxyURL string) {
proxy.Manager.ReleaseByURL(proxyURL)
logger.Debug("释放代理: %s", proxyURL)
}
pool.GetHealthyCount = func() int {
return proxy.Manager.HealthyCount()
}
go func() {
proxy.Manager.CheckAllHealth()
if proxy.Manager.HealthyCount() > 0 {
poolSize := appConfig.Pool.RegisterThreads
if poolSize <= 0 {
poolSize = pool.DefaultProxyCount
}
if poolSize > 10 {
poolSize = 10
}
proxy.Manager.SetMaxPoolSize(poolSize)
proxy.Manager.InitInstancePool(poolSize)
}
}()
client := pool.NewPoolClient(appConfig.PoolServer)
if err := client.Start(); err != nil {
log.Fatalf("❌ 客户端启动失败: %v", err)
}
}
var poolServer *pool.PoolServer
func runAsServer() {
logger.Info("🖥️ 启动服务器模式...")
// 加载账号
dataDir := appConfig.PoolServer.DataDir
if dataDir == "" {
dataDir = DataDir
}
if err := pool.Pool.Load(dataDir); err != nil {
log.Fatalf("❌ 加载账号失败: %v", err)
}
// 启动配置文件热重载监听
if err := startConfigWatcher(); err != nil {
logger.Warn("⚠️ 配置热重载启动失败: %v", err)
}
poolServer = pool.NewPoolServer(pool.Pool, appConfig.PoolServer)
poolServer.StartBackground() // 启动后台任务分发和心跳检测
pool.Pool.StartPoolManager()
runAPIServer()
}
// runAPIServer 启动 API 服务
func runAPIServer() {
gin.SetMode(gin.ReleaseMode)
r := gin.New()
r.Use(gin.Recovery())
setupAPIRoutes(r)
logger.Info("🚀 API 服务启动于 %s,账号: ready=%d, pending=%d", ListenAddr, pool.Pool.ReadyCount(), pool.Pool.PendingCount())
if err := r.Run(ListenAddr); err != nil {
log.Fatalf("❌ API 服务启动失败: %v", err)
}
}
func setupAPIRoutes(r *gin.Engine) {
// 请求日志中间件
r.Use(func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path
method := c.Request.Method
clientIP := c.ClientIP()
c.Next()
latency := time.Since(start)
statusCode := c.Writer.Status()
if statusCode >= 400 {
logger.Error("❌ %s %s %s %d %v", clientIP, method, path, statusCode, latency)
} else {
logger.Info("✅ %s %s %s %d %v", clientIP, method, path, statusCode, latency)
}
})
r.GET("/", func(c *gin.Context) {
stats := apiStats.GetStats()
response := gin.H{
"status": "running",
"service": "business2api",
"version": "2.1.6",
"mode": map[PoolMode]string{PoolModeLocal: "local", PoolModeServer: "server", PoolModeClient: "client"}[poolMode],
// 统计数据
"uptime": stats["uptime"],
"total_requests": stats["total_requests"],
"success_requests": stats["success_requests"],
"failed_requests": stats["failed_requests"],
"success_rate": stats["success_rate"],
"input_tokens": stats["input_tokens"],
"output_tokens": stats["output_tokens"],
"total_tokens": stats["total_tokens"],
"images_generated": stats["images_generated"],
"videos_generated": stats["videos_generated"],
"current_rpm": stats["current_rpm"],
"average_rpm": stats["average_rpm"],
"pool": gin.H{
"ready": pool.Pool.ReadyCount(),
"pending": pool.Pool.PendingCount(),
"total": pool.Pool.TotalCount(),
},
// Flow 状态
"flow_enabled": flowHandler != nil,
}
// 添加备注信息
if len(appConfig.Note) > 0 {
response["note"] = appConfig.Note
}
// 服务端模式:添加客户端信息
if poolServer != nil {
response["clients"] = gin.H{
"count": poolServer.GetClientCount(),
"total_threads": poolServer.GetTotalThreads(),
"list": poolServer.GetClientsInfo(),
}
}
c.JSON(200, response)
})
r.GET("/health", func(c *gin.Context) {
c.JSON(200, gin.H{
"status": "ok",
"time": time.Now().UTC().Format(time.RFC3339),
"ready": pool.Pool.ReadyCount(),
"pending": pool.Pool.PendingCount(),
"mode": map[PoolMode]string{PoolModeLocal: "local", PoolModeServer: "server", PoolModeClient: "client"}[poolMode],
})
})
// WebSocket 端点(服务端模式下用于客户端连接)
r.GET("/ws", func(c *gin.Context) {
if poolServer == nil {
c.JSON(503, gin.H{"error": "WebSocket 服务未启用,仅在服务端模式下可用"})
return
}
poolServer.HandleWS(c.Writer, c.Request)
})
// Pool 内部端点(客户端上传账号等,使用 X-Pool-Secret 鉴权)
poolGroup := r.Group("/pool")
poolGroup.Use(func(c *gin.Context) {
if poolServer == nil {
c.JSON(503, gin.H{"error": "Pool 服务未启用"})
c.Abort()
return
}
secret := appConfig.PoolServer.Secret
if secret != "" && c.GetHeader("X-Pool-Secret") != secret {
c.JSON(401, gin.H{"error": "Unauthorized"})
c.Abort()
return
}
c.Next()
})
poolGroup.POST("/upload-account", func(c *gin.Context) {
poolServer.HandleUploadAccount(c.Writer, c.Request)
})
apiGroup := r.Group("/")
apiGroup.Use(apiKeyAuth())
// Gemini 风格模型列表 /v1beta/models
apiGroup.GET("/v1beta/models", func(c *gin.Context) {
var models []gin.H
for _, m := range GetAvailableModels() {
models = append(models, gin.H{
"name": "models/" + m,
"version": "001",
"displayName": m,
"description": "Gemini model: " + m,
"inputTokenLimit": 1048576,
"outputTokenLimit": 8192,
"supportedGenerationMethods": []string{"generateContent", "countTokens"},
"temperature": 1.0,
"topP": 0.95,
"topK": 64,
})
}
c.JSON(200, gin.H{"models": models})
})
// OpenAI 风格模型列表
apiGroup.GET("/v1/models", func(c *gin.Context) {
now := time.Now().Unix()
var models []gin.H
for _, m := range GetAvailableModels() {
models = append(models, gin.H{
"id": m,
"object": "model",
"created": now,
"owned_by": "google",
"permission": []interface{}{},
})
}
c.JSON(200, gin.H{"object": "list", "data": models})
})
apiGroup.POST("/v1/chat/completions", func(c *gin.Context) {
var req ChatRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
if req.Model == "" {
req.Model = GetAvailableModels()[0]
}
streamChat(c, req)
})
apiGroup.POST("/v1/messages", handleClaudeMessages)
// Gemini 单模型详情 GET /v1beta/models/{model}
apiGroup.GET("/v1beta/models/:model", func(c *gin.Context) {
modelName := c.Param("model")
// 移除 "models/" 前缀(如果有)
modelName = strings.TrimPrefix(modelName, "models/")
// 检查模型是否存在
found := false
for _, m := range GetAvailableModels() {
if m == modelName {
found = true
break
}
}
if !found {
c.JSON(404, gin.H{"error": gin.H{
"code": 404,
"message": "Model not found: " + modelName,
"status": "NOT_FOUND",
}})
return
}
c.JSON(200, gin.H{
"name": "models/" + modelName,
"version": "001",
"displayName": modelName,
"description": "Gemini model: " + modelName,
"inputTokenLimit": 1048576,
"outputTokenLimit": 8192,
"supportedGenerationMethods": []string{"generateContent", "countTokens"},
"temperature": 1.0,
"topP": 0.95,
"topK": 64,
})
})
// Gemini generateContent/streamGenerateContent
apiGroup.POST("/v1beta/models/*action", handleGeminiGenerate)
apiGroup.POST("/v1/models/*action", handleGeminiGenerate)
admin := r.Group("/admin")
admin.Use(apiKeyAuth())
admin.POST("/register", func(c *gin.Context) {
var req struct {
Count int `json:"count"`
}
if err := c.ShouldBindJSON(&req); err != nil || req.Count <= 0 {
req.Count = appConfig.Pool.TargetCount - pool.Pool.TotalCount()
}
if req.Count <= 0 {
c.JSON(200, gin.H{"message": "账号数量已足够", "count": pool.Pool.TotalCount()})
return
}
if poolMode == PoolModeServer {
// 服务端模式:注册任务会通过 WS 分发给客户端
c.JSON(200, gin.H{"message": "注册任务已加入队列,将通过 WS 分发给客户端", "target": req.Count})
return
}
if err := register.StartRegister(req.Count); err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
c.JSON(200, gin.H{"message": "注册已启动", "target": req.Count})
})
admin.POST("/refresh", func(c *gin.Context) {
pool.Pool.Load(DataDir)
c.JSON(200, gin.H{
"message": "刷新完成",
"ready": pool.Pool.ReadyCount(),
"pending": pool.Pool.PendingCount(),
})
})
admin.GET("/status", func(c *gin.Context) {
stats := pool.Pool.Stats()
stats["target"] = appConfig.Pool.TargetCount
stats["min"] = appConfig.Pool.MinCount
stats["is_registering"] = atomic.LoadInt32(®ister.IsRegistering) == 1
stats["register_stats"] = register.Stats.Get()
stats["mode"] = map[PoolMode]string{PoolModeLocal: "local", PoolModeServer: "server", PoolModeClient: "client"}[poolMode]
c.JSON(200, stats)
})
// 详细API统计
admin.GET("/stats", func(c *gin.Context) {
detailed := apiStats.GetDetailedStats()
detailed["pool"] = pool.Pool.Stats()
detailed["proxy_pool"] = proxy.Manager.PoolStats()
c.JSON(200, detailed)
})
admin.GET("/ip", func(c *gin.Context) {
c.JSON(200, ipStats.GetAllIPStats())
})
admin.POST("/force-refresh", func(c *gin.Context) {
count := pool.Pool.ForceRefreshAll()
c.JSON(200, gin.H{
"message": "已触发强制刷新",
"count": count,
})
})
admin.POST("/reload-config", func(c *gin.Context) {
if err := reloadConfig(); err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
configMu.RLock()
c.JSON(200, gin.H{
"message": "配置已重载",
"api_keys": len(appConfig.APIKeys),
"debug": appConfig.Debug,
"pool_config": gin.H{
"refresh_cooldown_sec": appConfig.Pool.RefreshCooldownSec,
"use_cooldown_sec": appConfig.Pool.UseCooldownSec,
"max_fail_count": appConfig.Pool.MaxFailCount,
"enable_browser_refresh": appConfig.Pool.EnableBrowserRefresh,
"browser_refresh_headless": appConfig.Pool.BrowserRefreshHeadless,
"browser_refresh_max_retry": appConfig.Pool.BrowserRefreshMaxRetry,
"auto_delete_401": appConfig.Pool.AutoDelete401,
},
})
configMu.RUnlock()
})
admin.POST("/config/cooldown", func(c *gin.Context) {
var req struct {
RefreshCooldownSec int `json:"refresh_cooldown_sec"`
UseCooldownSec int `json:"use_cooldown_sec"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
pool.SetCooldowns(req.RefreshCooldownSec, req.UseCooldownSec)
c.JSON(200, gin.H{
"message": "冷却配置已更新",
"refresh_cooldown_sec": int(pool.RefreshCooldown.Seconds()),
"use_cooldown_sec": int(pool.UseCooldown.Seconds()),
})
})
admin.POST("/browser-refresh", func(c *gin.Context) {
var req struct {
Email string `json:"email"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
if req.Email == "" {
c.JSON(400, gin.H{"error": "需要提供 email"})
return
}
var targetAcc *pool.Account
pool.Pool.WithLock(func(ready, pending []*pool.Account) {
for _, acc := range ready {
if acc.Data.Email == req.Email {
targetAcc = acc
break
}
}
if targetAcc == nil {
for _, acc := range pending {
if acc.Data.Email == req.Email {
targetAcc = acc
break
}
}
}
})
if targetAcc == nil {
c.JSON(404, gin.H{"error": "账号未找到", "email": req.Email})
return
}
go func() {
logger.Info("🔄 手动触发浏览器刷新: %s", req.Email)
result := register.RefreshCookieWithBrowser(targetAcc, pool.BrowserRefreshHeadless, Proxy)
if result.Success {
targetAcc.Mu.Lock()
// 更新完整信息
targetAcc.Data.Cookies = result.SecureCookies
if result.Authorization != "" {
targetAcc.Data.Authorization = result.Authorization
}
if result.CSESIDX != "" {
targetAcc.CSESIDX = result.CSESIDX
targetAcc.Data.CSESIDX = result.CSESIDX
}
if result.ConfigID != "" {
targetAcc.ConfigID = result.ConfigID
targetAcc.Data.ConfigID = result.ConfigID
}
targetAcc.Data.Timestamp = time.Now().Format(time.RFC3339)
targetAcc.FailCount = 0
targetAcc.Mu.Unlock()
if err := targetAcc.SaveToFile(); err != nil {
logger.Error("❌ [%s] 保存刷新后的数据失败: %v", req.Email, err)
} else {
logger.Info("✅ [%s] 刷新数据已保存到文件", req.Email)
}
pool.Pool.MarkNeedsRefresh(targetAcc)
logger.Info("✅ 手动浏览器刷新成功: %s", req.Email)
} else {
logger.Error("❌ 手动浏览器刷新失败: %s - %v", req.Email, result.Error)
}
}()
c.JSON(200, gin.H{
"message": "浏览器刷新已触发",
"email": req.Email,
})
})
// Flow Token 管理
admin.GET("/flow/status", func(c *gin.Context) {
if flowTokenPool == nil {
c.JSON(200, gin.H{
"enabled": false,
"message": "Flow 服务未启用",
})
return
}
stats := flowTokenPool.Stats()
stats["enabled"] = flowHandler != nil
c.JSON(200, stats)
})
admin.POST("/flow/add-token", func(c *gin.Context) {
if flowTokenPool == nil {
c.JSON(503, gin.H{"error": "Flow 服务未启用"})
return
}
var req struct {
Cookie string `json:"cookie"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
if req.Cookie == "" {
c.JSON(400, gin.H{"error": "需要提供 cookie"})
return
}
tokenID, err := flowTokenPool.AddFromCookie(req.Cookie)
if err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
c.JSON(200, gin.H{
"message": "Token 添加成功",
"token_id": tokenID,
"total": flowTokenPool.Count(),
})
})
admin.POST("/flow/remove-token", func(c *gin.Context) {
if flowTokenPool == nil {
c.JSON(503, gin.H{"error": "Flow 服务未启用"})
return
}
var req struct {
TokenID string `json:"token_id"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
if err := flowTokenPool.RemoveToken(req.TokenID); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
c.JSON(200, gin.H{
"message": "Token 已移除",
"total": flowTokenPool.Count(),
})
})
admin.POST("/flow/reload", func(c *gin.Context) {
if flowTokenPool == nil {
c.JSON(503, gin.H{"error": "Flow 服务未启用"})
return
}
loaded, err := flowTokenPool.LoadFromDir()
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
c.JSON(200, gin.H{
"message": "已重新加载",
"loaded": loaded,
"total": flowTokenPool.Count(),
})
})
admin.POST("/config/browser-refresh", func(c *gin.Context) {
var req struct {
Enable *bool `json:"enable"`
Headless *bool `json:"headless"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
if req.Enable != nil {
pool.EnableBrowserRefresh = *req.Enable
}
if req.Headless != nil {
pool.BrowserRefreshHeadless = *req.Headless
}
c.JSON(200, gin.H{
"message": "浏览器刷新配置已更新",
"enable": pool.EnableBrowserRefresh,
"headless": pool.BrowserRefreshHeadless,
})
})
}
func runLocalMode() {
// 本地模式:正常启动
if err := pool.Pool.Load(DataDir); err != nil {
log.Fatalf("❌ 加载账号失败: %v", err)
}
// 启动配置文件热重载监听
if err := startConfigWatcher(); err != nil {
logger.Warn("⚠️ 配置热重载启动失败: %v", err)
}
// 代理实例池由异步健康检查完成后初始化
// 检查 CONFIG_ID
if DefaultConfig != "" {
logger.Info("✅ 使用默认 configId: %s", DefaultConfig)
}
// 检查 API Key 配置
if len(GetAPIKeys()) == 0 {
logger.Warn("⚠️ 未配置 API Key,API 将无鉴权运行")
}
// 启动号池管理
if appConfig.Pool.RefreshOnStartup {
pool.Pool.StartPoolManager()
}
if pool.Pool.TotalCount() == 0 {
needCount := appConfig.Pool.TargetCount
logger.Info("📝 无账号,启动注册 %d 个...", needCount)
register.StartRegister(needCount)
}
if appConfig.Pool.CheckIntervalMinutes > 0 {
go register.PoolMaintainer()
}
// 启动 API 服务
runAPIServer()
}
================================================
FILE: src/api/api.go
================================================
package api
import (
"fmt"
"strings"
"github.com/gin-gonic/gin"
)
// ChatRequest 聊天请求
type ChatRequest struct {
Model string `json:"model"`
Messages []Message `json:"messages"`
Stream bool `json:"stream"`
Temperature float64 `json:"temperature,omitempty"`
MaxTokens int `json:"max_tokens,omitempty"`
Tools []ToolDef `json:"tools,omitempty"`
}
// Message 消息
type Message struct {
Role string `json:"role"`
Content interface{} `json:"content"`
}
// ToolDef 工具定义
type ToolDef struct {
Type string `json:"type"`
Function FunctionDef `json:"function"`
}
// FunctionDef 函数定义
type FunctionDef struct {
Name string `json:"name"`
Description string `json:"description"`
Parameters map[string]interface{} `json:"parameters,omitempty"`
}
var (
StreamChat func(c *gin.Context, req ChatRequest)
FixedModels []string
)
type GeminiRequest struct {
Contents []GeminiContent `json:"contents"`
SystemInstruction *GeminiContent `json:"systemInstruction,omitempty"`
GenerationConfig map[string]interface{} `json:"generationConfig,omitempty"`
GeminiTools []map[string]interface{} `json:"tools,omitempty"`
}
type GeminiContent struct {
Role string `json:"role,omitempty"`
Parts []GeminiPart `json:"parts"`
}
type GeminiPart struct {
Text string `json:"text,omitempty"`
InlineData *GeminiInlineData `json:"inlineData,omitempty"`
}
type GeminiInlineData struct {
MimeType string `json:"mimeType"`
Data string `json:"data"`
}
func HandleGeminiGenerate(c *gin.Context) {
action := c.Param("action")
if action == "" {
c.JSON(400, gin.H{"error": gin.H{"code": 400, "message": "Missing model action", "status": "INVALID_ARGUMENT"}})
return
}
// 去掉开头的 /
action = strings.TrimPrefix(action, "/")
// 解析模型名和动作
var model string
var isStream bool
if idx := strings.LastIndex(action, ":"); idx > 0 {
model = action[:idx]
actionType := action[idx+1:]
isStream = actionType == "streamGenerateContent"
} else {
model = action
}
if model == "" {
model = FixedModels[0]
}
var geminiReq GeminiRequest
if err := c.ShouldBindJSON(&geminiReq); err != nil {
c.JSON(400, gin.H{"error": gin.H{"code": 400, "message": err.Error(), "status": "INVALID_ARGUMENT"}})
return
}
var messages []Message
// 处理systemInstruction
if geminiReq.SystemInstruction != nil && len(geminiReq.SystemInstruction.Parts) > 0 {
var sysText string
for _, part := range geminiReq.SystemInstruction.Parts {
if part.Text != "" {
sysText += part.Text
}
}
if sysText != "" {
messages = append(messages, Message{Role: "system", Content: sysText})
}
}
for _, content := range geminiReq.Contents {
role := content.Role
if role == "model" {
role = "assistant"
}
var textParts []string
var contentParts []interface{}
for _, part := range content.Parts {
if part.Text != "" {
textParts = append(textParts, part.Text)
}
if part.InlineData != nil {
contentParts = append(contentParts, map[string]interface{}{
"type": "image_url",
"image_url": map[string]string{
"url": fmt.Sprintf("data:%s;base64,%s", part.InlineData.MimeType, part.InlineData.Data),
},
})
}
}
if len(contentParts) > 0 {
if len(textParts) > 0 {
contentParts = append([]interface{}{map[string]interface{}{"type": "text", "text": strings.Join(textParts, "\n")}}, contentParts...)
}
messages = append(messages, Message{Role: role, Content: contentParts})
} else if len(textParts) > 0 {
messages = append(messages, Message{Role: role, Content: strings.Join(textParts, "\n")})
}
}
// 流式判断:路径中包含streamGenerateContent 或 query参数 alt=sse
stream := isStream || c.Query("alt") == "sse"
var tools []ToolDef
for _, gt := range geminiReq.GeminiTools {
if funcDecls, ok := gt["functionDeclarations"].([]interface{}); ok {
for _, fd := range funcDecls {
if funcMap, ok := fd.(map[string]interface{}); ok {
name, _ := funcMap["name"].(string)
desc, _ := funcMap["description"].(string)
params, _ := funcMap["parameters"].(map[string]interface{})
tools = append(tools, ToolDef{
Type: "function",
Function: FunctionDef{
Name: name,
Description: desc,
Parameters: params,
},
})
}
}
}
}
req := ChatRequest{
Model: model,
Messages: messages,
Stream: stream,
Tools: tools,
}
StreamChat(c, req)
}
type ClaudeRequest struct {
Model string `json:"model"`
Messages []Message `json:"messages"`
System string `json:"system,omitempty"`
MaxTokens int `json:"max_tokens,omitempty"`
Stream bool `json:"stream"`
Temperature float64 `json:"temperature,omitempty"`
Tools []ToolDef `json:"tools,omitempty"`
}
func HandleClaudeMessages(c *gin.Context) {
var claudeReq ClaudeRequest
if err := c.ShouldBindJSON(&claudeReq); err != nil {
c.JSON(400, gin.H{"type": "error", "error": gin.H{"type": "invalid_request_error", "message": err.Error()}})
return
}
req := ChatRequest{
Model: claudeReq.Model,
Messages: claudeReq.Messages,
Stream: claudeReq.Stream,
Temperature: claudeReq.Temperature,
Tools: claudeReq.Tools,
}
if claudeReq.System != "" {
systemMsg := Message{Role: "system", Content: claudeReq.System}
req.Messages = append([]Message{systemMsg}, req.Messages...)
}
// 保持模型名原样,不做映射
if req.Model == "" {
req.Model = FixedModels[0]
}
StreamChat(c, req)
}
================================================
FILE: src/logger/logger.go
================================================
package logger
import (
"fmt"
"log"
"os"
"sync"
"time"
)
// Level 日志级别
type Level int
const (
LevelError Level = iota
LevelWarn
LevelInfo
LevelDebug
)
var levelNames = map[Level]string{
LevelError: "ERROR",
LevelWarn: "WARN",
LevelInfo: "INFO",
LevelDebug: "DEBUG",
}
// Logger 日志记录器
type Logger struct {
level Level
prefix string
mu sync.Mutex
}
var (
defaultLogger = &Logger{level: LevelInfo}
debugMode = false
)
// SetDebugMode 设置调试模式
func SetDebugMode(debug bool) {
debugMode = debug
if debug {
defaultLogger.level = LevelDebug
} else {
defaultLogger.level = LevelInfo
}
}
// IsDebug 是否为调试模式
func IsDebug() bool {
return debugMode
}
// SetLevel 设置日志级别
func SetLevel(level Level) {
defaultLogger.level = level
}
func (l *Logger) log(level Level, format string, args ...interface{}) {
if level > l.level {
return
}
l.mu.Lock()
defer l.mu.Unlock()
timestamp := time.Now().Format("15:04:05")
levelStr := levelNames[level]
msg := fmt.Sprintf(format, args...)
if l.prefix != "" {
log.Printf("[%s] [%s] [%s] %s", timestamp, levelStr, l.prefix, msg)
} else {
log.Printf("[%s] [%s] %s", timestamp, levelStr, msg)
}
}
// Error 错误日志(始终输出)
func Error(format string, args ...interface{}) {
defaultLogger.log(LevelError, format, args...)
}
// Warn 警告日志(始终输出)
func Warn(format string, args ...interface{}) {
defaultLogger.log(LevelWarn, format, args...)
}
// Info 信息日志(正常模式输出)
func Info(format string, args ...interface{}) {
defaultLogger.log(LevelInfo, format, args...)
}
// Debug 调试日志(仅debug模式输出)
func Debug(format string, args ...interface{}) {
defaultLogger.log(LevelDebug, format, args...)
}
// WithPrefix 创建带前缀的子日志器
func WithPrefix(prefix string) *Logger {
return &Logger{
level: defaultLogger.level,
prefix: prefix,
}
}
func (l *Logger) Error(format string, args ...interface{}) { l.log(LevelError, format, args...) }
func (l *Logger) Warn(format string, args ...interface{}) { l.log(LevelWarn, format, args...) }
func (l *Logger) Info(format string, args ...interface{}) { l.log(LevelInfo, format, args...) }
func (l *Logger) Debug(format string, args ...interface{}) { l.log(LevelDebug, format, args...) }
func init() {
log.SetFlags(0)
log.SetOutput(os.Stdout)
}
================================================
FILE: src/pool/pool.go
================================================
package pool
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"sync/atomic"
"time"
"business2api/src/logger"
)
// ==================== 数据结构 ====================
// Cookie 账号Cookie
type Cookie struct {
Name string `json:"name"`
Value string `json:"value"`
Domain string `json:"domain"`
}
// AccountData 账号数据
type AccountData struct {
Email string `json:"email"`
FullName string `json:"fullName"`
Authorization string `json:"authorization"`
Cookies []Cookie `json:"cookies"`
CookieString string `json:"cookie_string,omitempty"`
ResponseHeaders map[string]string `json:"response_headers,omitempty"`
Timestamp string `json:"timestamp"`
ConfigID string `json:"configId,omitempty"`
CSESIDX string `json:"csesidx,omitempty"`
}
func ParseCookieString(cookieStr string) []Cookie {
var cookies []Cookie
if cookieStr == "" {
return cookies
}
parts := strings.Split(cookieStr, "; ")
for _, part := range parts {
part = strings.TrimSpace(part)
if part == "" {
continue
}
idx := strings.Index(part, "=")
if idx > 0 {
cookies = append(cookies, Cookie{
Name: part[:idx],
Value: part[idx+1:],
Domain: ".gemini.google", // 默认域名
})
}
}
return cookies
}
func (a *AccountData) GetAllCookies() []Cookie {
if len(a.Cookies) > 0 {
return a.Cookies
}
if a.CookieString != "" {
return ParseCookieString(a.CookieString)
}
return nil
}
// AccountStatus 账号状态
type AccountStatus int
const (
StatusPending AccountStatus = iota // 待刷新
StatusReady // 就绪可用
StatusCooldown // 冷却中
StatusInvalid // 失效
)
// Account 账号实例
type Account struct {
Data AccountData
FilePath string
JWT string
JWTExpires time.Time
ConfigID string
CSESIDX string
LastRefresh time.Time
LastUsed time.Time // 最后使用时间
Refreshed bool
FailCount int // 连续失败次数
BrowserRefreshCount int // 浏览器刷新尝试次数
SuccessCount int // 成功次数
TotalCount int // 总使用次数
DailyCount int // 每日调用次数
DailyCountDate string // 每日计数日期 (YYYY-MM-DD)
Status AccountStatus
Mu sync.Mutex
}
// SetCooldownMultiplier 设置冷却时间倍数(用于429限流)
func (acc *Account) SetCooldownMultiplier(multiplier int) {
acc.Mu.Lock()
acc.LastUsed = time.Now().Add(UseCooldown * time.Duration(multiplier-1))
acc.Mu.Unlock()
}
// 默认冷却时间(可通过配置覆盖)
var (
RefreshCooldown = 4 * time.Minute // 刷新冷却
UseCooldown = 15 * time.Second // 使用冷却
JWTRefreshThreshold = 60 * time.Second // JWT刷新阈值
MaxFailCount = 3 // 最大连续失败次数
EnableBrowserRefresh = true // 是否启用浏览器刷新
BrowserRefreshHeadless = true // 浏览器刷新是否无头模式
BrowserRefreshMaxRetry = 1 // 浏览器刷新最大重试次数
AutoDelete401 = false // 401时是否自动删除账号
DailyLimit = 3000 // 每账号每日最大调用次数
DataDir string
DefaultConfig string
Proxy string
JwtTTL = 270 * time.Second
HTTPClient *http.Client
)
type RefreshCookieFunc func(acc *Account, headless bool, proxy string) *BrowserRefreshResult
type BrowserRefreshResult struct {
Success bool
SecureCookies []Cookie
Authorization string
ConfigID string
CSESIDX string
ResponseHeaders map[string]string
Error error
}
var RefreshCookieWithBrowser RefreshCookieFunc
func readResponseBody(resp *http.Response) ([]byte, error) {
body := make([]byte, 0)
buf := make([]byte, 4096)
gitextract_zttsb6rv/
├── .dockerignore
├── .github/
│ ├── dependabot.yml
│ └── workflows/
│ ├── build.yml
│ └── codeql.yml
├── .gitignore
├── Dockerfile
├── README.md
├── config/
│ ├── README.md
│ └── config.json.example
├── docker/
│ └── docker-compose.yml
├── go.mod
├── go.sum
├── main.go
└── src/
├── api/
│ └── api.go
├── logger/
│ └── logger.go
├── pool/
│ ├── pool.go
│ ├── pool_client.go
│ └── pool_server.go
├── proxy/
│ ├── proxy.go
│ └── singbox.go
├── register/
│ ├── browser.go
│ └── register.go
└── utils/
└── utils.go
SYMBOL INDEX (443 symbols across 11 files)
FILE: main.go
type PoolConfig (line 40) | type PoolConfig struct
type FlowConfigSection (line 57) | type FlowConfigSection struct
type ProxyConfig (line 67) | type ProxyConfig struct
type AppConfig (line 75) | type AppConfig struct
type PoolMode (line 91) | type PoolMode
constant PoolModeLocal (line 94) | PoolModeLocal PoolMode = iota
constant PoolModeServer (line 95) | PoolModeServer
constant PoolModeClient (line 96) | PoolModeClient
type APIStats (line 115) | type APIStats struct
method RecordRequest (line 315) | func (s *APIStats) RecordRequest(success bool, inputTokens, outputToke...
method RecordRequestWithModel (line 319) | func (s *APIStats) RecordRequestWithModel(model string, success bool, ...
method GetRPM (line 372) | func (s *APIStats) GetRPM() float64 {
method GetStats (line 389) | func (s *APIStats) GetStats() map[string]interface{} {
method GetDetailedStats (line 417) | func (s *APIStats) GetDetailedStats() map[string]interface{} {
type ModelStats (line 132) | type ModelStats struct
type HourlyStats (line 141) | type HourlyStats struct
type IPStats (line 157) | type IPStats struct
method RecordIPRequest (line 184) | func (s *IPStats) RecordIPRequest(ip, model, userAgent string, success...
method GetAllIPStats (line 241) | func (s *IPStats) GetAllIPStats() map[string]interface{} {
method GetIPDetail (line 308) | func (s *IPStats) GetIPDetail(ip string) *IPRequestInfo {
type IPRequestInfo (line 163) | type IPRequestInfo struct
method GetRPM (line 228) | func (info *IPRequestInfo) GetRPM() float64 {
function max (line 475) | func max(a, b int64) int64 {
function GetAPIKeys (line 502) | func GetAPIKeys() []string {
function reloadConfig (line 511) | func reloadConfig() error {
function applyConfigChanges (line 549) | func applyConfigChanges(oldAPIKeys []string, oldDebug bool, oldPoolConfi...
function startConfigWatcher (line 584) | func startConfigWatcher() error {
function configWatchLoop (line 604) | func configWatchLoop() {
function stopConfigWatcher (line 646) | func stopConfigWatcher() {
function mergeConfig (line 661) | func mergeConfig(base, loaded *AppConfig) {
function saveDefaultConfig (line 740) | func saveDefaultConfig(configPath string) error {
function loadAppConfig (line 753) | func loadAppConfig() {
function initFlowClient (line 840) | func initFlowClient() {
function initProxyPool (line 895) | func initProxyPool() {
function GetAvailableModels (line 1042) | func GetAvailableModels() []string {
function getEnv (line 1059) | func getEnv(key, def string) string {
function getCommonHeaders (line 1066) | func getCommonHeaders(jwt, origAuth string) map[string]string {
function createSession (line 1091) | func createSession(jwt, configID, origAuth string) (string, error) {
function createSessionWithRetry (line 1096) | func createSessionWithRetry(jwt, configID, origAuth string, maxRetries i...
function createSessionOnce (line 1133) | func createSessionOnce(jwt, configID, origAuth string) (string, error) {
function uploadContextFile (line 1175) | func uploadContextFile(jwt, configID, sessionName, mimeType, base64Conte...
function uploadContextFileByURL (line 1230) | func uploadContextFileByURL(jwt, configID, sessionName, imageURL, origAu...
type Message (line 1278) | type Message struct
type ContentPart (line 1286) | type ContentPart struct
type ImageURL (line 1292) | type ImageURL struct
type ToolDef (line 1297) | type ToolDef struct
type FunctionDef (line 1302) | type FunctionDef struct
type ToolCall (line 1309) | type ToolCall struct
type FunctionCall (line 1315) | type FunctionCall struct
type ChatRequest (line 1320) | type ChatRequest struct
type ChatChoice (line 1330) | type ChatChoice struct
type ChatChunk (line 1338) | type ChatChunk struct
function createChunk (line 1347) | func createChunk(id string, created int64, model string, delta map[strin...
function extractContentFromReply (line 1367) | func extractContentFromReply(replyMap map[string]interface{}, jwt, sessi...
function downloadGeneratedFile (line 1420) | func downloadGeneratedFile(jwt, fileId, session, configID, origAuth stri...
function downloadGeneratedFileWithRetry (line 1424) | func downloadGeneratedFileWithRetry(jwt, fileId, session, configID, orig...
function downloadGeneratedFileOnce (line 1470) | func downloadGeneratedFileOnce(jwt, fileId, session, configID, origAuth ...
function formatImageAsMarkdown (line 1550) | func formatImageAsMarkdown(mimeType, base64Data string) string {
type MediaInfo (line 1555) | type MediaInfo struct
function parseMessageContent (line 1567) | func parseMessageContent(msg Message) (string, []MediaInfo) {
function parseMediaURL (line 1630) | func parseMediaURL(urlStr, defaultType string) *MediaInfo {
function downloadImage (line 1708) | func downloadImage(urlStr string) (string, string, error) {
function downloadMedia (line 1713) | func downloadMedia(urlStr, mediaType string) (string, string, error) {
function normalizeVideoMimeType (line 1764) | func normalizeVideoMimeType(mimeType string) string {
function convertToPNG (line 1788) | func convertToPNG(data []byte) ([]byte, error) {
function convertBase64ToPNG (line 1803) | func convertBase64ToPNG(base64Data string) (string, error) {
constant maxRetries (line 1817) | maxRetries = 3
function extractSystemPrompt (line 1821) | func extractSystemPrompt(messages []Message) string {
function convertMessagesToPrompt (line 1833) | func convertMessagesToPrompt(messages []Message) string {
type GeminiRequest (line 1886) | type GeminiRequest struct
type GeminiContent (line 1893) | type GeminiContent struct
type GeminiPart (line 1898) | type GeminiPart struct
type GeminiInlineData (line 1903) | type GeminiInlineData struct
function handleGeminiGenerate (line 1909) | func handleGeminiGenerate(c *gin.Context) {
type ClaudeRequest (line 2023) | type ClaudeRequest struct
function handleClaudeMessages (line 2034) | func handleClaudeMessages(c *gin.Context) {
function buildToolsSpec (line 2064) | func buildToolsSpec(tools []ToolDef, isImageModel, isVideoModel, isSearc...
function extractToolCalls (line 2092) | func extractToolCalls(dataList []map[string]interface{}) []ToolCall {
function needsConversationContext (line 2145) | func needsConversationContext(messages []Message) bool {
function handleFlowRequest (line 2156) | func handleFlowRequest(c *gin.Context, req ChatRequest, chatID string, c...
function streamChat (line 2272) | func streamChat(c *gin.Context, req ChatRequest) {
function apiKeyAuth (line 3083) | func apiKeyAuth() gin.HandlerFunc {
function runBrowserRefreshMode (line 3126) | func runBrowserRefreshMode(email string) {
function init (line 3219) | func init() {
function filterStdout (line 3224) | func filterStdout() {
function main (line 3252) | func main() {
function runAsClient (line 3309) | func runAsClient() {
function runAsServer (line 3393) | func runAsServer() {
function runAPIServer (line 3417) | func runAPIServer() {
function setupAPIRoutes (line 3428) | func setupAPIRoutes(r *gin.Engine) {
function runLocalMode (line 3906) | func runLocalMode() {
FILE: src/api/api.go
type ChatRequest (line 11) | type ChatRequest struct
type Message (line 21) | type Message struct
type ToolDef (line 27) | type ToolDef struct
type FunctionDef (line 33) | type FunctionDef struct
type GeminiRequest (line 43) | type GeminiRequest struct
type GeminiContent (line 50) | type GeminiContent struct
type GeminiPart (line 55) | type GeminiPart struct
type GeminiInlineData (line 60) | type GeminiInlineData struct
function HandleGeminiGenerate (line 65) | func HandleGeminiGenerate(c *gin.Context) {
type ClaudeRequest (line 176) | type ClaudeRequest struct
function HandleClaudeMessages (line 185) | func HandleClaudeMessages(c *gin.Context) {
FILE: src/logger/logger.go
type Level (line 12) | type Level
constant LevelError (line 15) | LevelError Level = iota
constant LevelWarn (line 16) | LevelWarn
constant LevelInfo (line 17) | LevelInfo
constant LevelDebug (line 18) | LevelDebug
type Logger (line 29) | type Logger struct
method log (line 60) | func (l *Logger) log(level Level, format string, args ...interface{}) {
method Error (line 106) | func (l *Logger) Error(format string, args ...interface{}) { l.log(Lev...
method Warn (line 107) | func (l *Logger) Warn(format string, args ...interface{}) { l.log(Lev...
method Info (line 108) | func (l *Logger) Info(format string, args ...interface{}) { l.log(Lev...
method Debug (line 109) | func (l *Logger) Debug(format string, args ...interface{}) { l.log(Lev...
function SetDebugMode (line 41) | func SetDebugMode(debug bool) {
function IsDebug (line 51) | func IsDebug() bool {
function SetLevel (line 56) | func SetLevel(level Level) {
function Error (line 79) | func Error(format string, args ...interface{}) {
function Warn (line 84) | func Warn(format string, args ...interface{}) {
function Info (line 89) | func Info(format string, args ...interface{}) {
function Debug (line 94) | func Debug(format string, args ...interface{}) {
function WithPrefix (line 99) | func WithPrefix(prefix string) *Logger {
function init (line 111) | func init() {
FILE: src/pool/pool.go
type Cookie (line 24) | type Cookie struct
type AccountData (line 31) | type AccountData struct
method GetAllCookies (line 67) | func (a *AccountData) GetAllCookies() []Cookie {
function ParseCookieString (line 43) | func ParseCookieString(cookieStr string) []Cookie {
type AccountStatus (line 78) | type AccountStatus
constant StatusPending (line 81) | StatusPending AccountStatus = iota
constant StatusReady (line 82) | StatusReady
constant StatusCooldown (line 83) | StatusCooldown
constant StatusInvalid (line 84) | StatusInvalid
type Account (line 88) | type Account struct
method SetCooldownMultiplier (line 109) | func (acc *Account) SetCooldownMultiplier(multiplier int) {
method SaveToFile (line 346) | func (acc *Account) SaveToFile() error {
method checkAndUpdateDailyCount (line 625) | func (acc *Account) checkAndUpdateDailyCount() bool {
method GetDailyUsage (line 641) | func (acc *Account) GetDailyUsage() (count int, limit int, date string) {
method getCookie (line 974) | func (acc *Account) getCookie(name string) string {
method RefreshJWT (line 984) | func (acc *Account) RefreshJWT() error {
method GetJWT (line 1099) | func (acc *Account) GetJWT() (string, string, error) {
method fetchConfigID (line 1108) | func (acc *Account) fetchConfigID() (string, error) {
type RefreshCookieFunc (line 133) | type RefreshCookieFunc
type BrowserRefreshResult (line 134) | type BrowserRefreshResult struct
function readResponseBody (line 146) | func readResponseBody(resp *http.Response) ([]byte, error) {
type AccountPool (line 161) | type AccountPool struct
method GetReadyAccounts (line 174) | func (p *AccountPool) GetReadyAccounts() []*Account {
method GetPendingAccounts (line 179) | func (p *AccountPool) GetPendingAccounts() []*Account {
method WithLock (line 184) | func (p *AccountPool) WithLock(fn func(ready, pending []*Account)) {
method WithWriteLock (line 189) | func (p *AccountPool) WithWriteLock(fn func(ready, pending []*Account)...
method Load (line 223) | func (p *AccountPool) Load(dir string) error {
method GetPendingAccount (line 295) | func (p *AccountPool) GetPendingAccount() *Account {
method MarkReady (line 309) | func (p *AccountPool) MarkReady(acc *Account) {
method MarkPending (line 317) | func (p *AccountPool) MarkPending(acc *Account) {
method RemoveAccount (line 337) | func (p *AccountPool) RemoveAccount(acc *Account) {
method StartPoolManager (line 373) | func (p *AccountPool) StartPoolManager() {
method refreshWorker (line 380) | func (p *AccountPool) refreshWorker(id int) {
method scanWorker (line 546) | func (p *AccountPool) scanWorker() {
method RefreshExpiredAccounts (line 565) | func (p *AccountPool) RefreshExpiredAccounts() {
method RefreshAllAccounts (line 599) | func (p *AccountPool) RefreshAllAccounts() {
method Next (line 651) | func (p *AccountPool) Next() *Account {
method MarkUsed (line 726) | func (p *AccountPool) MarkUsed(acc *Account, success bool) {
method MarkNeedsRefresh (line 744) | func (p *AccountPool) MarkNeedsRefresh(acc *Account) {
method Count (line 754) | func (p *AccountPool) Count() int { p.mu.RLock(); defer p.mu.RUnlock()...
method PendingCount (line 755) | func (p *AccountPool) PendingCount() int {
method ReadyCount (line 760) | func (p *AccountPool) ReadyCount() int {
method TotalCount (line 765) | func (p *AccountPool) TotalCount() int {
method Stats (line 772) | func (p *AccountPool) Stats() map[string]interface{} {
method ListAccounts (line 837) | func (p *AccountPool) ListAccounts() []AccountInfo {
method ForceRefreshAll (line 888) | func (p *AccountPool) ForceRefreshAll() int {
function SetCooldowns (line 201) | func SetCooldowns(refreshSec, useSec int) {
function SetDailyLimit (line 212) | func SetDailyLimit(limit int) {
type AccountInfo (line 822) | type AccountInfo struct
function urlsafeB64Encode (line 908) | func urlsafeB64Encode(data []byte) string {
function kqEncode (line 912) | func kqEncode(s string) string {
function createJWT (line 925) | func createJWT(keyBytes []byte, keyID, csesidx string) string {
function extractCSESIDX (line 944) | func extractCSESIDX(auth string) string {
FILE: src/pool/pool_client.go
type BrowserRegisterResult (line 18) | type BrowserRegisterResult struct
type RunBrowserRegisterFunc (line 30) | type RunBrowserRegisterFunc
constant ClientVersion (line 33) | ClientVersion = "2.0.0"
type PoolClient (line 49) | type PoolClient struct
method Start (line 78) | func (pc *PoolClient) Start() error {
method Stop (line 102) | func (pc *PoolClient) Stop() {
method connect (line 110) | func (pc *PoolClient) connect() error {
method work (line 149) | func (pc *PoolClient) work() {
method heartbeatPump (line 157) | func (pc *PoolClient) heartbeatPump() {
method writePump (line 180) | func (pc *PoolClient) writePump() {
method doPeriodicRegister (line 208) | func (pc *PoolClient) doPeriodicRegister() {
method readPump (line 222) | func (pc *PoolClient) readPump() {
method handleMessage (line 257) | func (pc *PoolClient) handleMessage(msg WSMessage) {
method handleRegisterTask (line 281) | func (pc *PoolClient) handleRegisterTask(data map[string]interface{}) {
method handleStatusAndRegister (line 340) | func (pc *PoolClient) handleStatusAndRegister(data map[string]interfac...
method handleRefreshTask (line 379) | func (pc *PoolClient) handleRefreshTask(data map[string]interface{}) {
method sendMessage (line 488) | func (pc *PoolClient) sendMessage(msg WSMessage) {
method uploadAccount (line 501) | func (pc *PoolClient) uploadAccount(result *BrowserRegisterResult, isN...
method uploadAccountData (line 525) | func (pc *PoolClient) uploadAccountData(req *AccountUploadRequest) err...
method sendRegisterResult (line 587) | func (pc *PoolClient) sendRegisterResult(success bool, email, errMsg s...
method sendRefreshResult (line 600) | func (pc *PoolClient) sendRefreshResult(email string, success bool, co...
method triggerReconnect (line 614) | func (pc *PoolClient) triggerReconnect() {
function NewPoolClient (line 63) | func NewPoolClient(config PoolServerConfig) *PoolClient {
function getString (line 622) | func getString(m map[string]interface{}, key string) string {
FILE: src/pool/pool_server.go
type PoolServerConfig (line 21) | type PoolServerConfig struct
type WSMessageType (line 34) | type WSMessageType
constant WSMsgTaskRegister (line 38) | WSMsgTaskRegister WSMessageType = "task_register"
constant WSMsgTaskRefresh (line 39) | WSMsgTaskRefresh WSMessageType = "task_refresh"
constant WSMsgHeartbeat (line 40) | WSMsgHeartbeat WSMessageType = "heartbeat"
constant WSMsgStatus (line 41) | WSMsgStatus WSMessageType = "status"
constant WSMsgRegisterResult (line 44) | WSMsgRegisterResult WSMessageType = "register_result"
constant WSMsgRefreshResult (line 45) | WSMsgRefreshResult WSMessageType = "refresh_result"
constant WSMsgHeartbeatAck (line 46) | WSMsgHeartbeatAck WSMessageType = "heartbeat_ack"
constant WSMsgClientReady (line 47) | WSMsgClientReady WSMessageType = "client_ready"
constant WSMsgRequestTask (line 48) | WSMsgRequestTask WSMessageType = "request_task"
constant WSMsgQueryStatus (line 49) | WSMsgQueryStatus WSMessageType = "query_status"
constant ProtocolVersion (line 54) | ProtocolVersion = "1.0"
constant ServerVersion (line 55) | ServerVersion = "4.0.0"
type WSMessage (line 59) | type WSMessage struct
type WSClient (line 67) | type WSClient struct
method writePump (line 213) | func (c *WSClient) writePump() {
method readPump (line 250) | func (c *WSClient) readPump() {
method handleMessage (line 290) | func (c *WSClient) handleMessage(msg WSMessage) {
type PoolServer (line 80) | type PoolServer struct
method Start (line 115) | func (ps *PoolServer) Start() error {
method StartBackground (line 161) | func (ps *PoolServer) StartBackground() {
method HandleWS (line 170) | func (ps *PoolServer) HandleWS(w http.ResponseWriter, r *http.Request) {
method HandleUploadAccount (line 174) | func (ps *PoolServer) HandleUploadAccount(w http.ResponseWriter, r *ht...
method handleWebSocket (line 177) | func (ps *PoolServer) handleWebSocket(w http.ResponseWriter, r *http.R...
method removeClient (line 332) | func (ps *PoolServer) removeClient(clientID string) {
method taskDispatcher (line 341) | func (ps *PoolServer) taskDispatcher() {
method assignTaskRoundRobin (line 375) | func (ps *PoolServer) assignTaskRoundRobin(msgType WSMessageType, data...
method assignTask (line 428) | func (ps *PoolServer) assignTask(client *WSClient) {
method heartbeatChecker (line 543) | func (ps *PoolServer) heartbeatChecker() {
method periodicTaskBroadcaster (line 594) | func (ps *PoolServer) periodicTaskBroadcaster() {
method poolMaintainer (line 610) | func (ps *PoolServer) poolMaintainer() {
method periodicAccountScanner (line 666) | func (ps *PoolServer) periodicAccountScanner() {
method broadcastRegisterTasks (line 687) | func (ps *PoolServer) broadcastRegisterTasks() {
method sendStatusTo (line 756) | func (ps *PoolServer) sendStatusTo(client *WSClient) {
method handleRegisterResult (line 782) | func (ps *PoolServer) handleRegisterResult(data map[string]interface{}) {
method handleRefreshResult (line 798) | func (ps *PoolServer) handleRefreshResult(data map[string]interface{}) {
method deleteAccount (line 829) | func (ps *PoolServer) deleteAccount(email string) {
method updateAccountCookies (line 860) | func (ps *PoolServer) updateAccountCookies(email string, cookiesData i...
method handleQueueRegister (line 889) | func (ps *PoolServer) handleQueueRegister(w http.ResponseWriter, r *ht...
method handleQueueRefresh (line 912) | func (ps *PoolServer) handleQueueRefresh(w http.ResponseWriter, r *htt...
method handleNext (line 964) | func (ps *PoolServer) handleNext(w http.ResponseWriter, r *http.Reques...
method handleMark (line 993) | func (ps *PoolServer) handleMark(w http.ResponseWriter, r *http.Reques...
method handleRefresh (line 1021) | func (ps *PoolServer) handleRefresh(w http.ResponseWriter, r *http.Req...
method handleStatus (line 1048) | func (ps *PoolServer) handleStatus(w http.ResponseWriter, r *http.Requ...
method handleGetJWT (line 1052) | func (ps *PoolServer) handleGetJWT(w http.ResponseWriter, r *http.Requ...
method handleUploadAccount (line 1270) | func (ps *PoolServer) handleUploadAccount(w http.ResponseWriter, r *ht...
method GetClientsInfo (line 1447) | func (ps *PoolServer) GetClientsInfo() []ClientInfo {
method GetClientCount (line 1463) | func (ps *PoolServer) GetClientCount() int {
method GetTotalThreads (line 1469) | func (ps *PoolServer) GetTotalThreads() int {
function NewPoolServer (line 99) | func NewPoolServer(pool *AccountPool, config PoolServerConfig) *PoolServ...
type AccountResponse (line 955) | type AccountResponse struct
type RemotePoolClient (line 1099) | type RemotePoolClient struct
method doRequest (line 1130) | func (rc *RemotePoolClient) doRequest(method, path string, body interf...
method Next (line 1154) | func (rc *RemotePoolClient) Next() (*CachedAccount, error) {
method MarkUsed (line 1187) | func (rc *RemotePoolClient) MarkUsed(email string, success bool) error {
method MarkNeedsRefresh (line 1200) | func (rc *RemotePoolClient) MarkNeedsRefresh(email string) error {
method GetStatus (line 1212) | func (rc *RemotePoolClient) GetStatus() (map[string]interface{}, error) {
method RefreshJWT (line 1227) | func (rc *RemotePoolClient) RefreshJWT(email string) (*CachedAccount, ...
method UploadAccount (line 1415) | func (rc *RemotePoolClient) UploadAccount(acc *AccountUploadRequest) e...
type CachedAccount (line 1109) | type CachedAccount struct
function NewRemotePoolClient (line 1118) | func NewRemotePoolClient(serverAddr, secret string) *RemotePoolClient {
type AccountUploadRequest (line 1258) | type AccountUploadRequest struct
type ClientInfo (line 1439) | type ClientInfo struct
FILE: src/proxy/proxy.go
type ProxyNode (line 29) | type ProxyNode struct
type InstanceStatus (line 67) | type InstanceStatus
constant InstanceStatusIdle (line 70) | InstanceStatusIdle InstanceStatus = iota
constant InstanceStatusInUse (line 71) | InstanceStatusInUse
constant InstanceStatusStopped (line 72) | InstanceStatusStopped
type ProxyInstance (line 76) | type ProxyInstance struct
type ProxyManager (line 87) | type ProxyManager struct
method IsReady (line 128) | func (pm *ProxyManager) IsReady() bool {
method WaitReady (line 133) | func (pm *ProxyManager) WaitReady(timeout time.Duration) bool {
method SetReady (line 153) | func (pm *ProxyManager) SetReady(ready bool) {
method SetMaxPoolSize (line 163) | func (pm *ProxyManager) SetMaxPoolSize(size int) {
method InitInstancePool (line 172) | func (pm *ProxyManager) InitInstancePool(count int) error {
method SetXrayPath (line 206) | func (pm *ProxyManager) SetXrayPath(path string) {
method AddSubscribeURL (line 210) | func (pm *ProxyManager) AddSubscribeURL(url string) {
method AddProxyFile (line 221) | func (pm *ProxyManager) AddProxyFile(path string) {
method LoadAll (line 232) | func (pm *ProxyManager) LoadAll() error {
method loadFromURL (line 310) | func (pm *ProxyManager) loadFromURL(urlStr string) ([]*ProxyNode, erro...
method loadFromFile (line 341) | func (pm *ProxyManager) loadFromFile(path string) ([]*ProxyNode, error) {
method parseContent (line 349) | func (pm *ProxyManager) parseContent(content string) ([]*ProxyNode, er...
method parseLine (line 396) | func (pm *ProxyManager) parseLine(line string) *ProxyNode {
method startInstanceLocked (line 956) | func (pm *ProxyManager) startInstanceLocked(node *ProxyNode) (*ProxyIn...
method StartXray (line 984) | func (pm *ProxyManager) StartXray(node *ProxyNode) (string, error) {
method StopProxy (line 996) | func (pm *ProxyManager) StopProxy(localPort int) {
method StopXray (line 1001) | func (pm *ProxyManager) StopXray(localPort int) {
method StopAll (line 1006) | func (pm *ProxyManager) StopAll() {
method CheckHealth (line 1017) | func (pm *ProxyManager) CheckHealth(node *ProxyNode) bool {
method CheckHealthQuick (line 1085) | func (pm *ProxyManager) CheckHealthQuick(node *ProxyNode) bool {
method CheckAllHealth (line 1127) | func (pm *ProxyManager) CheckAllHealth() {
method GetFromPool (line 1302) | func (pm *ProxyManager) GetFromPool() *ProxyInstance {
method ReturnToPool (line 1321) | func (pm *ProxyManager) ReturnToPool(inst *ProxyInstance) {
method ReleaseByURL (line 1331) | func (pm *ProxyManager) ReleaseByURL(proxyURL string) {
method Next (line 1354) | func (pm *ProxyManager) Next() string {
method MarkProxyFailed (line 1471) | func (pm *ProxyManager) MarkProxyFailed(proxyURL string) {
method MarkProxySuccess (line 1492) | func (pm *ProxyManager) MarkProxySuccess(proxyURL string) {
method PoolStats (line 1506) | func (pm *ProxyManager) PoolStats() map[string]int {
method Count (line 1529) | func (pm *ProxyManager) Count() int {
method HealthyCount (line 1539) | func (pm *ProxyManager) HealthyCount() int {
method TotalCount (line 1546) | func (pm *ProxyManager) TotalCount() int {
method StartAutoUpdate (line 1553) | func (pm *ProxyManager) StartAutoUpdate() {
method SetProxies (line 1581) | func (pm *ProxyManager) SetProxies(proxies []string) {
method StartAutoSubscribe (line 1854) | func (pm *ProxyManager) StartAutoSubscribe() {
method StopAutoSubscribe (line 1857) | func (pm *ProxyManager) StopAutoSubscribe() {
method GetAutoSubscribeURL (line 1860) | func (pm *ProxyManager) GetAutoSubscribeURL() string {
method HasAutoSubscribe (line 1863) | func (pm *ProxyManager) HasAutoSubscribe() bool {
function init (line 123) | func init() {
type SubscriptionInfo (line 268) | type SubscriptionInfo struct
method getRemainingTraffic (line 304) | func (si *SubscriptionInfo) getRemainingTraffic() int64 {
function parseSubscriptionUserinfo (line 276) | func parseSubscriptionUserinfo(header string) *SubscriptionInfo {
function tryBase64Decode (line 374) | func tryBase64Decode(s string) []byte {
function getStringFromMap (line 422) | func getStringFromMap(m map[string]interface{}, key string) string {
function getIntFromMap (line 437) | func getIntFromMap(m map[string]interface{}, key string) int {
function parseVmess (line 453) | func parseVmess(link string) *ProxyNode {
function parseVless (line 513) | func parseVless(link string) *ProxyNode {
function isSupportedSSCipher (line 658) | func isSupportedSSCipher(method string) bool {
function tryMapSSCipher (line 664) | func tryMapSSCipher(method string) (string, bool) {
function parseSS (line 679) | func parseSS(link string) *ProxyNode {
function parseTrojan (line 778) | func parseTrojan(link string) *ProxyNode {
function parseHysteria2 (line 830) | func parseHysteria2(link string) *ProxyNode {
function parseAnyTLS (line 880) | func parseAnyTLS(link string) *ProxyNode {
function parseDirectProxy (line 930) | func parseDirectProxy(link string) *ProxyNode {
constant autoRegisterURL (line 1596) | autoRegisterURL = "https://jgpyjc.top/api/v1/passport/auth/register"
constant autoSubscribeBaseURL (line 1597) | autoSubscribeBaseURL = "https://bb1.jgpyjc.top/api/v1/client/subscribe?t...
constant autoRegisterInterval (line 1598) | autoRegisterInterval = 1 * time.Hour
type AutoSubscriber (line 1602) | type AutoSubscriber struct
method refreshSubscription (line 1751) | func (as *AutoSubscriber) refreshSubscription() error {
method loadToProxyManager (line 1774) | func (as *AutoSubscriber) loadToProxyManager() error {
method Start (line 1799) | func (as *AutoSubscriber) Start(pm *ProxyManager) {
method Stop (line 1828) | func (as *AutoSubscriber) Stop() {
method GetCurrentSubscribeURL (line 1838) | func (as *AutoSubscriber) GetCurrentSubscribeURL() string {
method GetCurrentToken (line 1844) | func (as *AutoSubscriber) GetCurrentToken() string {
method IsExpired (line 1849) | func (as *AutoSubscriber) IsExpired() bool {
function randString (line 1619) | func randString(n int) string {
function ungzipIfNeeded (line 1630) | func ungzipIfNeeded(data []byte, header http.Header) ([]byte, error) {
function extractToken (line 1644) | func extractToken(body []byte) string {
function looksLikeJWT (line 1683) | func looksLikeJWT(s string) bool {
function doAutoRegister (line 1696) | func doAutoRegister() (email, password, token string, err error) {
FILE: src/proxy/singbox.go
type SingboxManager (line 25) | type SingboxManager struct
method IsAvailable (line 63) | func (sm *SingboxManager) IsAvailable() bool {
method StartRaw (line 72) | func (sm *SingboxManager) StartRaw(node *ProxyNode) (string, error) {
method Start (line 79) | func (sm *SingboxManager) Start(node *ProxyNode) (string, error) {
method startInternal (line 86) | func (sm *SingboxManager) startInternal(node *ProxyNode, doTest bool) ...
method testConnectivity (line 161) | func (sm *SingboxManager) testConnectivity(proxyURL string) error {
method Stop (line 187) | func (sm *SingboxManager) Stop(port int) {
method StopAll (line 204) | func (sm *SingboxManager) StopAll() {
method findAvailablePort (line 219) | func (sm *SingboxManager) findAvailablePort() int {
method generateConfigJSON (line 233) | func (sm *SingboxManager) generateConfigJSON(node *ProxyNode, localPor...
method buildOutboundJSON (line 258) | func (sm *SingboxManager) buildOutboundJSON(node *ProxyNode) map[strin...
method buildTransportJSON (line 408) | func (sm *SingboxManager) buildTransportJSON(node *ProxyNode) map[stri...
type SingboxInstance (line 33) | type SingboxInstance struct
function IsSingboxProtocol (line 50) | func IsSingboxProtocol(protocol string) bool {
function CanSingboxHandle (line 59) | func CanSingboxHandle(protocol string) bool {
function GetSingboxManager (line 443) | func GetSingboxManager() *SingboxManager {
function InitSingbox (line 448) | func InitSingbox() {
function TrySingboxStart (line 453) | func TrySingboxStart(node *ProxyNode) (string, error) {
function StopSingbox (line 461) | func StopSingbox(port int) {
function ParseProxyLinkWithSingbox (line 466) | func ParseProxyLinkWithSingbox(link string) *ProxyNode {
function parseTUIC (line 484) | func parseTUIC(link string) *ProxyNode {
FILE: src/register/browser.go
function SetHTTPClient (line 46) | func SetHTTPClient(c *http.Client) {
function readResponseBody (line 49) | func readResponseBody(resp *http.Response) ([]byte, error) {
type TempEmailResponse (line 69) | type TempEmailResponse struct
type EmailListResponse (line 75) | type EmailListResponse struct
type EmailContent (line 81) | type EmailContent struct
type BrowserRegisterResult (line 85) | type BrowserRegisterResult struct
function generateRandomName (line 96) | func generateRandomName() string {
function humanDelay (line 103) | func humanDelay(minMs, maxMs int) {
function humanMouseMove (line 112) | func humanMouseMove(page *rod.Page, el *rod.Element) {
function humanClick (line 144) | func humanClick(page *rod.Page, el *rod.Element) error {
function humanType (line 164) | func humanType(page *rod.Page, text string) {
function humanScrollToElement (line 189) | func humanScrollToElement(page *rod.Page, el *rod.Element) {
function humanFocusInput (line 199) | func humanFocusInput(page *rod.Page, el *rod.Element) error {
type TempMailProvider (line 209) | type TempMailProvider struct
function getTemporaryEmail (line 230) | func getTemporaryEmail() (string, error) {
function getEmailFromProvider (line 254) | func getEmailFromProvider(provider TempMailProvider) (string, error) {
function getEmailCount (line 291) | func getEmailCount(email string) int {
type VerificationState (line 317) | type VerificationState struct
method MarkCodeUsed (line 331) | func (vs *VerificationState) MarkCodeUsed(code string) {
method IsCodeUsed (line 336) | func (vs *VerificationState) IsCodeUsed(code string) bool {
method CanResend (line 341) | func (vs *VerificationState) CanResend() bool {
method RecordResend (line 354) | func (vs *VerificationState) RecordResend() {
function NewVerificationState (line 325) | func NewVerificationState() *VerificationState {
function getVerificationEmailQuick (line 360) | func getVerificationEmailQuick(email string, retries int, intervalSec in...
function getVerificationEmailAfter (line 363) | func getVerificationEmailAfter(email string, retries int, intervalSec in...
function getVerificationEmailWithState (line 366) | func getVerificationEmailWithState(email string, retries int, intervalSe...
type PageState (line 410) | type PageState
constant PageStateUnknown (line 413) | PageStateUnknown PageState = iota
constant PageStateEmailInput (line 414) | PageStateEmailInput
constant PageStateCodeInput (line 415) | PageStateCodeInput
constant PageStateNameInput (line 416) | PageStateNameInput
constant PageStateLoggedIn (line 417) | PageStateLoggedIn
constant PageStateError (line 418) | PageStateError
function GetPageState (line 421) | func GetPageState(pageURL string) PageState {
function GetPageStateString (line 443) | func GetPageStateString(state PageState) string {
function WaitForPageState (line 461) | func WaitForPageState(page *rod.Page, targetState PageState, timeout tim...
function getWindowsBrowserPaths (line 512) | func getWindowsBrowserPaths() []string {
function getLinuxBrowserPaths (line 563) | func getLinuxBrowserPaths() []string {
function getMacOSBrowserPaths (line 598) | func getMacOSBrowserPaths() []string {
function getBrowserPathsForOS (line 622) | func getBrowserPathsForOS() []string {
function findBrowser (line 634) | func findBrowser() (string, bool) {
function expandPath (line 677) | func expandPath(path string) string {
function getBrowserNamesForOS (line 696) | func getBrowserNamesForOS() []string {
function findBrowserByCommand (line 704) | func findBrowserByCommand() string {
function findInPath (line 737) | func findInPath(name string) (string, error) {
type BrowserSession (line 777) | type BrowserSession struct
method SetupNetworkCapture (line 890) | func (s *BrowserSession) SetupNetworkCapture() {
method ExtractFromURL (line 910) | func (s *BrowserSession) ExtractFromURL() {
method ExtractCSESIDXFromAuth (line 926) | func (s *BrowserSession) ExtractCSESIDXFromAuth() {
method Close (line 935) | func (s *BrowserSession) Close() {
method FindEmailInput (line 946) | func (s *BrowserSession) FindEmailInput() *rod.Element {
method InputTextWithKeyboard (line 960) | func (s *BrowserSession) InputTextWithKeyboard(text string, delayMs in...
method ClickButton (line 968) | func (s *BrowserSession) ClickButton(targets []string, maxRetries int)...
method CollectCookies (line 1000) | func (s *BrowserSession) CollectCookies(existingCookies []pool.Cookie)...
function createBrowserSession (line 787) | func createBrowserSession(headless bool, proxy string, logPrefix string)...
function configureBrowserLauncher (line 832) | func configureBrowserLauncher(l *launcher.Launcher, headless bool, proxy...
function toJSArray (line 991) | func toJSArray(arr []string) string {
function extractVerificationCode (line 1020) | func extractVerificationCode(content string) (string, error) {
function safeType (line 1046) | func safeType(page *rod.Page, text string, delay int) error {
function debugScreenshot (line 1081) | func debugScreenshot(page *rod.Page, threadID int, step string) {
function handleAdditionalSteps (line 1102) | func handleAdditionalSteps(page *rod.Page, threadID int) bool {
function checkAndHandleAdminPage (line 1212) | func checkAndHandleAdminPage(page *rod.Page, threadID int) bool {
function RunBrowserRegister (line 1269) | func RunBrowserRegister(headless bool, proxy string, threadID int) (resu...
function SaveBrowserRegisterResult (line 2466) | func SaveBrowserRegisterResult(result *BrowserRegisterResult, dataDir st...
type BrowserRefreshResult (line 2495) | type BrowserRefreshResult struct
function RefreshCookieWithBrowser (line 2506) | func RefreshCookieWithBrowser(acc *pool.Account, headless bool, proxy st...
function extractCSESIDXFromAuth (line 2932) | func extractCSESIDXFromAuth(auth string) string {
function NativeRegisterWorker (line 2967) | func NativeRegisterWorker(id int, dataDirAbs string) {
FILE: src/register/register.go
type RegisterStats (line 33) | type RegisterStats struct
method AddSuccess (line 42) | func (s *RegisterStats) AddSuccess() {
method AddFailed (line 50) | func (s *RegisterStats) AddFailed(err string) {
method Get (line 59) | func (s *RegisterStats) Get() map[string]interface{} {
type RegisterResult (line 72) | type RegisterResult struct
function StartRegister (line 80) | func StartRegister(count int) error {
function PoolMaintainer (line 153) | func PoolMaintainer() {
function CheckAndMaintainPool (line 169) | func CheckAndMaintainPool() {
FILE: src/utils/utils.go
function NewHTTPClient (line 22) | func NewHTTPClient(proxy string) *http.Client {
function InitHTTPClient (line 47) | func InitHTTPClient(proxy string) {
function ReadResponseBody (line 56) | func ReadResponseBody(resp *http.Response) ([]byte, error) {
function ParseNDJSON (line 70) | func ParseNDJSON(data []byte) []map[string]interface{} {
function ParseIncompleteJSONArray (line 87) | func ParseIncompleteJSONArray(data []byte) []map[string]interface{} {
function TruncateString (line 110) | func TruncateString(s string, maxLen int) string {
function Min (line 118) | func Min(a, b int) int {
Condensed preview — 23 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (495K chars).
[
{
"path": ".dockerignore",
"chars": 280,
"preview": "# Git\n.git\n.gitignore\n\n# Data\ndata/\n\n# Node.js files (no longer needed - registration is pure Go)\nnode_modules/\npackage-"
},
{
"path": ".github/dependabot.yml",
"chars": 522,
"preview": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where "
},
{
"path": ".github/workflows/build.yml",
"chars": 4312,
"preview": "name: Build and Release\n\non:\n push:\n branches: [main, master]\n tags:\n - 'v*'\n pull_request:\n branches: ["
},
{
"path": ".github/workflows/codeql.yml",
"chars": 4723,
"preview": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# Y"
},
{
"path": ".gitignore",
"chars": 356,
"preview": "# 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"
},
{
"path": "Dockerfile",
"chars": 957,
"preview": "# Build stage\nFROM golang:1.23-alpine AS builder\n\nWORKDIR /app\n\n# Install build dependencies\nRUN apk add --no-cache git "
},
{
"path": "README.md",
"chars": 16802,
"preview": "# Business2API\n\n> 🚀 OpenAI/Gemini 兼容的 Gemini Business API 代理服务,支持账号池管理、自动注册和 Flow 图片/视频生成。\n\n[\n\n**内置 xray-core**,支持 vmess、vless、shadowsocks、trojan 等协议,自动转换为本地 socks5 代理。\n\n```json\n\"pro"
},
{
"path": "config/config.json.example",
"chars": 1078,
"preview": "{\n \"api_keys\": [\"your-api-key-here\"],\n \"listen_addr\": \":8000\",\n \"data_dir\": \"./data\",\n \"default_config\": \"\",\n \"debu"
},
{
"path": "docker/docker-compose.yml",
"chars": 681,
"preview": "version: '3.8'\n\nservices:\n business2api:\n image: ghcr.io/xxxxteam/business2api:latest\n container_name: business2a"
},
{
"path": "go.mod",
"chars": 7732,
"preview": "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."
},
{
"path": "go.sum",
"chars": 35853,
"preview": "filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw=\nfilippo.io/edwards25519 v1.1.1/go.mod h1:"
},
{
"path": "main.go",
"chars": 110062,
"preview": "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/j"
},
{
"path": "src/api/api.go",
"chars": 5580,
"preview": "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\tM"
},
{
"path": "src/logger/logger.go",
"chars": 2250,
"preview": "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 "
},
{
"path": "src/pool/pool.go",
"chars": 27194,
"preview": "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\"o"
},
{
"path": "src/pool/pool_client.go",
"chars": 14328,
"preview": "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/sr"
},
{
"path": "src/pool/pool_server.go",
"chars": 35764,
"preview": "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"
},
{
"path": "src/proxy/proxy.go",
"chars": 41028,
"preview": "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"
},
{
"path": "src/proxy/singbox.go",
"chars": 11995,
"preview": "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"
},
{
"path": "src/register/browser.go",
"chars": 82933,
"preview": "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": "src/register/register.go",
"chars": 4348,
"preview": "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\"bu"
},
{
"path": "src/utils/utils.go",
"chars": 2614,
"preview": "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"
}
]
About this extraction
This page contains the full source code of the XxxXTeam/business2api GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 23 files (404.7 KB), approximately 137.4k tokens, and a symbol index with 443 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.