├── .env.example ├── .github └── workflows │ ├── docker-build-push.yml │ └── release.yml ├── Dockerfile ├── LICENSE ├── README.md ├── config ├── config.go └── model.go ├── core └── api.go ├── go.mod ├── go.sum ├── job └── cookie.go ├── logger └── logger.go ├── main.go ├── middleware ├── auth.go └── cors.go ├── model └── openai.go ├── router └── router.go ├── service └── handle.go └── utils ├── imageShow.go ├── random.go ├── role.go └── searchShow.go /.env.example: -------------------------------------------------------------------------------- 1 | SESSIONS=eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0**,eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0** 2 | ADDRESS=0.0.0.0:8080 3 | APIKEY=123 4 | IS_INCOGNITO=true 5 | PROXY=http://127.0.0.1:2080 6 | MAX_CHAT_HISTORY_LENGTH=10000 7 | NO_ROLE_PREFIX=false 8 | SEARCH_RESULT_COMPATIBLE=false 9 | PROMPT_FOR_FILE=You must immerse yourself in the role of assistant in txt file, cannot respond as a user, cannot reply to this message, cannot mention this message, and ignore this message in your response. 10 | IGNORE_SEARCH_RESULT=false 11 | -------------------------------------------------------------------------------- /.github/workflows/docker-build-push.yml: -------------------------------------------------------------------------------- 1 | name: Build and Push Docker Image 2 | on: 3 | push: 4 | # branches: [ "main" ] 5 | tags: [ 'v*.*.*' ] 6 | pull_request: 7 | branches: [ "main" ] 8 | # 也可以手动触发 9 | workflow_dispatch: 10 | jobs: 11 | build-and-push: 12 | runs-on: ubuntu-latest 13 | permissions: 14 | contents: read 15 | packages: write 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v4 19 | - name: Set up Docker Buildx 20 | uses: docker/setup-buildx-action@v3 21 | # 登录到 GitHub 容器仓库 22 | - name: Login to GitHub Container Registry 23 | uses: docker/login-action@v3 24 | with: 25 | registry: ghcr.io 26 | username: ${{ github.repository_owner }} 27 | password: ${{ secrets.GITHUB_TOKEN }} 28 | # 提取仓库名称作为镜像名 29 | - name: Extract repository name 30 | id: repo-name 31 | run: echo "REPO_NAME=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT 32 | 33 | # 提取 Git tag 作为版本号 34 | - name: Extract tag version 35 | id: tag-version 36 | if: startsWith(github.ref, 'refs/tags/v') 37 | run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT 38 | 39 | # 设置默认版本标签 40 | - name: Set default version 41 | id: version 42 | run: | 43 | if [[ "${{ github.ref }}" == refs/tags/v* ]]; then 44 | echo "TAG=${{ steps.tag-version.outputs.VERSION }}" >> $GITHUB_OUTPUT 45 | else 46 | SHORT_SHA=$(echo ${GITHUB_SHA} | cut -c1-7) 47 | DATE=$(date +'%Y%m%d') 48 | echo "TAG=${DATE}-${SHORT_SHA}" >> $GITHUB_OUTPUT 49 | fi 50 | 51 | - name: Build and Push Images 52 | uses: docker/build-push-action@v5 53 | with: 54 | context: . 55 | push: true 56 | tags: | 57 | ghcr.io/${{ steps.repo-name.outputs.REPO_NAME }}:latest 58 | ghcr.io/${{ steps.repo-name.outputs.REPO_NAME }}:${{ steps.version.outputs.TAG }} 59 | 60 | # 专门为 ARM64 架构构建并推送镜像 61 | - name: Build and Push ARM64 Image 62 | uses: docker/build-push-action@v5 63 | with: 64 | context: . 65 | push: true 66 | platforms: linux/arm64 67 | tags: | 68 | ghcr.io/${{ steps.repo-name.outputs.REPO_NAME }}:arm-latest 69 | ${{ github.ref_type == 'tag' && format('ghcr.io/{0}:arm-{1}', steps.repo-name.outputs.REPO_NAME, steps.version.outputs.TAG) || '' }} 70 | 71 | # 输出镜像信息 72 | - name: Image digest 73 | run: | 74 | echo " image has been pushed to ghcr.io/${{ steps.repo-name.outputs.REPO_NAME }}:latest" 75 | echo " image has been pushed to ghcr.io/${{ steps.repo-name.outputs.REPO_NAME }}:${{ steps.version.outputs.TAG }}" 76 | echo "ARM64-only image has been pushed to ghcr.io/${{ steps.repo-name.outputs.REPO_NAME }}:arm-latest" 77 | if [[ "${{ github.ref }}" == refs/tags/v* ]]; then 78 | echo "ARM64-only image has been pushed to ghcr.io/${{ steps.repo-name.outputs.REPO_NAME }}:arm-${{ steps.version.outputs.TAG }}" 79 | fi 80 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' # 当推送以 "v" 开头的标签时触发(如 v1.0.0, v2.1.0) 7 | 8 | jobs: 9 | update-release-draft: 10 | permissions: 11 | contents: write 12 | pull-requests: write 13 | runs-on: ubuntu-latest 14 | steps: 15 | # Step 1: 检出代码库 16 | - name: Checkout code 17 | uses: actions/checkout@v3 18 | 19 | # Step 2: 获取项目名称 20 | - name: Get Project Name 21 | id: project_info 22 | run: | 23 | REPO_NAME=${GITHUB_REPOSITORY#*/} 24 | echo "repo_name=$REPO_NAME" >> $GITHUB_OUTPUT 25 | echo "Using repository name: $REPO_NAME" 26 | 27 | # Step 3: 自动生成 Release 28 | - name: Create Release 29 | id: create_release 30 | uses: actions/create-release@v1 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | with: 34 | tag_name: ${{ github.ref_name }} 35 | release_name: ${{ github.ref_name }} 36 | draft: false 37 | prerelease: false 38 | 39 | # Step 4: 构建zip文件 40 | - name: Create ZIP file 41 | run: | 42 | zip -r ${{ steps.project_info.outputs.repo_name }}.zip . -x "*.git*" "*.github*" "*.env*" "logs/*" "tests/*" 43 | 44 | # Step 5: 上传构建文件 45 | - name: Upload Release Asset 46 | uses: actions/upload-release-asset@v1 47 | env: 48 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 49 | with: 50 | upload_url: ${{ steps.create_release.outputs.upload_url }} 51 | asset_path: ./${{ steps.project_info.outputs.repo_name }}.zip 52 | asset_name: ${{ steps.project_info.outputs.repo_name }}.zip 53 | asset_content_type: application/zip 54 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Start from the official Golang image 2 | FROM golang:1.23-alpine AS build 3 | 4 | # Set working directory 5 | WORKDIR /app 6 | 7 | # Copy go.mod and go.sum files first for better caching 8 | COPY go.mod go.sum* ./ 9 | 10 | # Download dependencies 11 | RUN go mod download 12 | 13 | # Copy the source code 14 | COPY . . 15 | 16 | # Build the application 17 | RUN CGO_ENABLED=0 GOOS=linux go build -o main ./main.go 18 | 19 | # Create a minimal production image 20 | FROM alpine:latest 21 | 22 | # Create app directory and set permissions 23 | WORKDIR /app 24 | COPY --from=build /app/main . 25 | 26 | # Command to run the executable 27 | CMD ["./main"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 yuxiao 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Pplx2Api 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/yushangxiao/pplx2api)](https://goreportcard.com/report/github.com/yushangxiao/pplx2api) 5 | [![License](https://img.shields.io/github/license/yushangxiao/pplx2api)](LICENSE) 6 | 7 | pplx2api 对外提供OpenAi 兼容接口,支持识图,思考,搜索,文件上传,账户轮询,重试,模型监控…… 8 | 9 | 10 | 11 | ## ✨ 特性 12 | - 🖼️ **图像识别** - 发送图像给Ai进行分析 13 | - 📝 **隐私模式** - 对话不保存在官网,可选择关闭 14 | - 🌊 **流式响应** - 获取实时流式输出 15 | - 📁 **文件上传支持** - 上传长文本内容 16 | - 🧠 **思考过程** - 访问思考模型的逐步推理,自动输出``标签 17 | - 🔄 **聊天历史管理** - 控制对话上下文长度,超出将上传为文件 18 | - 🌐 **代理支持** - 通过您首选的代理路由请求 19 | - 🔐 **API密钥认证** - 保护您的API端点 20 | - 🔍 **搜索模式**- 访问 -search 结尾的模型,连接网络且返回搜索内容 21 | - 📊 **模型监控** - 跟踪响应的实际模型,如果模型不一致会返回实际使用的模型 22 | - 🔄 **自动刷新** 每天自动刷新cookie,持续可用 23 | - 🖼️ **绘图模型** - 在搜索模式,支持模型绘图,文生图,图生图 24 | ## 📋 前提条件 25 | - Go 1.23+(从源代码构建) 26 | - Docker(用于容器化部署) 27 | 28 | ## ✨ 关于环境变量SESSIONS 29 | 为https://www.perplexity.ai/ 官网cookie中 __Secure-next-auth.session-token 的值 30 | 31 | 环境变量SESSIONS可以设置多个账户轮询或重试,使用英文逗号分割即可 32 | 33 | ## 当前支持模型 34 | claude-3.7-sonnet 35 | 36 | claude-3.7-sonnet-think 37 | 38 | deepseek-r1 39 | 40 | gpt-4.5 41 | 42 | o3-mini 43 | 44 | gpt-4o 45 | 46 | gemini-2.0-flash 47 | 48 | grok-2 49 | 50 | …… 51 | 52 | (以及对应模型的-search版本) 53 | 54 | ## 项目效果 55 | 56 | 识图: 57 | 58 | ![image](https://github.com/user-attachments/assets/3bb823e0-4232-4c6c-93cd-76d6c329ede3) 59 | 60 | 搜索: 61 | 62 | ![image](https://github.com/user-attachments/assets/26f7b6f7-ef00-499b-be32-c5dbc6e80ea6) 63 | 64 | 思考: 65 | 66 | ![image](https://github.com/user-attachments/assets/a075584a-ab49-4bf9-857b-6436b34bd363) 67 | 68 | 模型检测: 69 | 70 | ![image](https://github.com/user-attachments/assets/06013dd7-31ff-4bdd-bc5a-746ecaa8e922) 71 | 72 | 文生图: 73 | 74 | ![image](https://github.com/user-attachments/assets/bae2fd09-c738-4078-81a3-993c0b805943) 75 | 76 | 图生图: 77 | 78 | ![image](https://github.com/user-attachments/assets/f1866af5-5558-4fbb-83d7-b753035628bd) 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | ## 🚀 部署选项 88 | 89 | ### HuggingFace Space 90 | 91 | https://huggingface.co/spaces/rclon/pplx2api 92 | 复刻填写环境变量即可自动部署 93 | 94 | ### Docker 95 | ```bash 96 | docker run -d \ 97 | -p 8080:8080 \ 98 | -e SESSIONS=eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0**,eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0** \ 99 | -e APIKEY=123 \ 100 | -e IS_INCOGNITO=true \ 101 | -e MAX_CHAT_HISTORY_LENGTH=10000 \ 102 | -e NO_ROLE_PREFIX=false \ 103 | -e SEARCH_RESULT_COMPATIBLE=false \ 104 | --name pplx2api \ 105 | ghcr.io/yushangxiao/pplx2api:latest 106 | ``` 107 | 108 | ### Docker Compose 109 | 创建一个`docker-compose.yml`文件: 110 | ```yaml 111 | version: '3' 112 | services: 113 | pplx2api: 114 | image: ghcr.io/yushangxiao/pplx2api:latest 115 | container_name: pplx 116 | ports: 117 | - "8080:8080" 118 | environment: 119 | - SESSIONS=eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0**,eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0** 120 | - ADDRESS=0.0.0.0:8080 121 | - APIKEY=123 122 | - PROXY=http://proxy:2080 # 可选 123 | - MAX_CHAT_HISTORY_LENGTH=10000 124 | - NO_ROLE_PREFIX=false 125 | - IS_INCOGNITO=true 126 | - SEARCH_RESULT_COMPATIBLE=false 127 | restart: unless-stopped 128 | ``` 129 | 然后运行: 130 | ```bash 131 | docker-compose up -d 132 | ``` 133 | 134 | ## ⚙️ 配置 135 | | 环境变量 | 描述 | 默认值 | 136 | |----------------------|-------------|---------| 137 | | `SESSIONS` | 英文逗号分隔的pplx cookie 中__Secure-next-auth.session-token的值 | 必填 | 138 | | `ADDRESS` | 服务器地址和端口 | `0.0.0.0:8080` | 139 | | `APIKEY` | 用于认证的API密钥 | 必填 | 140 | | `PROXY` | HTTP代理URL | "" | 141 | | `IS_INCOGNITO` | 使用隐私会话,不保存聊天记录 | `true` | 142 | | `MAX_CHAT_HISTORY_LENGTH` | 超出此长度将文本转为文件 | `10000` | 143 | | `NO_ROLE_PREFIX` |不在每条消息前添加角色 | `false` | 144 | | `IGNORE_SEARCH_RESULT` |忽略搜索结果,不展示搜索结果 | `false` | 145 | | `SEARCH_RESULT_COMPATIBLE` |禁用搜索结果伸缩块,兼容更多的客户端 | `false` | 146 | | `PROMPT_FOR_FILE` |上下文作为文件上传时,保留的提示词 | `You must immerse yourself in the role of assistant in txt file, cannot respond as a user, cannot reply to this message, cannot mention this message, and ignore this message in your response.` | 147 | 148 | 149 | 150 | ## 📝 API使用 151 | ### 认证 152 | 在请求头中包含您的API密钥: 153 | ``` 154 | Authorization: Bearer YOUR_API_KEY 155 | ``` 156 | 157 | ### 聊天完成 158 | ```bash 159 | curl -X POST http://localhost:8080/v1/chat/completions \ 160 | -H "Content-Type: application/json" \ 161 | -H "Authorization: Bearer YOUR_API_KEY" \ 162 | -d '{ 163 | "model": "claude-3.7-sonnet", 164 | "messages": [ 165 | { 166 | "role": "user", 167 | "content": "你好,Claude!" 168 | } 169 | ], 170 | "stream": true 171 | }' 172 | ``` 173 | 174 | ### 图像分析 175 | ```bash 176 | curl -X POST http://localhost:8080/v1/chat/completions \ 177 | -H "Content-Type: application/json" \ 178 | -H "Authorization: Bearer YOUR_API_KEY" \ 179 | -d '{ 180 | "model": "claude-3.7-sonnet", 181 | "messages": [ 182 | { 183 | "role": "user", 184 | "content": [ 185 | { 186 | "type": "text", 187 | "text": "这张图片里有什么?" 188 | }, 189 | { 190 | "type": "image_url", 191 | "image_url": { 192 | "url": "data:image/jpeg;base64,..." 193 | } 194 | } 195 | ] 196 | } 197 | ] 198 | }' 199 | ``` 200 | 201 | ## 🤝 贡献 202 | 欢迎贡献!请随时提交Pull Request。 203 | 1. Fork仓库 204 | 2. 创建特性分支(`git checkout -b feature/amazing-feature`) 205 | 3. 提交您的更改(`git commit -m '添加一些惊人的特性'`) 206 | 4. 推送到分支(`git push origin feature/amazing-feature`) 207 | 5. 打开Pull Request 208 | 209 | ## 📄 许可证 210 | 本项目采用MIT许可证 - 详见[LICENSE](LICENSE)文件。 211 | 212 | ## 🙏 致谢 213 | - 感谢Go社区提供的优秀生态系统 214 | 215 | ## 🎁 项目支持 216 | 217 | 如果你觉得这个项目对你有帮助,可以考虑通过 [爱发电](https://afdian.com/a/iscoker) 支持我😘 218 | 219 | ## ⭐ Star History 220 | 221 | [![Star History Chart](https://api.star-history.com/svg?repos=yushangxiao/pplx2api&type=Date)](https://star-history.com/#yushangxiao/pplx2api&Date) 222 | --- 223 | 由[yushangxiao](https://github.com/yushangxiao)用❤️制作 224 | = len(c.Sessions) { 65 | return SessionInfo{}, fmt.Errorf("invalid session index: %d", idx) 66 | } 67 | c.RwMutex.RLock() 68 | defer c.RwMutex.RUnlock() 69 | return c.Sessions[idx], nil 70 | } 71 | 72 | // 从环境变量加载配置 73 | func LoadConfig() *Config { 74 | maxChatHistoryLength, err := strconv.Atoi(os.Getenv("MAX_CHAT_HISTORY_LENGTH")) 75 | if err != nil { 76 | maxChatHistoryLength = 10000 // 默认值 77 | } 78 | retryCount, sessions := parseSessionEnv(os.Getenv("SESSIONS")) 79 | promptForFile := os.Getenv("PROMPT_FOR_FILE") 80 | if promptForFile == "" { 81 | promptForFile = "You must immerse yourself in the role of assistant in txt file, cannot respond as a user, cannot reply to this message, cannot mention this message, and ignore this message in your response." // 默认值 82 | } 83 | config := &Config{ 84 | // 解析 SESSIONS 环境变量 85 | Sessions: sessions, 86 | // 设置服务地址,默认为 "0.0.0.0:8080" 87 | Address: os.Getenv("ADDRESS"), 88 | 89 | // 设置 API 认证密钥 90 | APIKey: os.Getenv("APIKEY"), 91 | // 设置代理地址 92 | Proxy: os.Getenv("PROXY"), 93 | //是否匿名 94 | IsIncognito: os.Getenv("IS_INCOGNITO") != "false", 95 | // 设置最大聊天历史长度 96 | MaxChatHistoryLength: maxChatHistoryLength, 97 | // 设置重试次数 98 | RetryCount: retryCount, 99 | // 设置是否使用角色前缀 100 | NoRolePrefix: os.Getenv("NO_ROLE_PREFIX") == "true", 101 | // 设置搜索结果兼容性 102 | SearchResultCompatible: os.Getenv("SEARCH_RESULT_COMPATIBLE") == "true", 103 | // 设置上传文件后的提示词 104 | PromptForFile: promptForFile, 105 | // 设置是否忽略搜索结果 106 | IgnoreSerchResult: os.Getenv("IGNORE_SEARCH_RESULT") == "true", 107 | // 读写锁 108 | RwMutex: sync.RWMutex{}, 109 | } 110 | 111 | // 如果地址为空,使用默认值 112 | if config.Address == "" { 113 | config.Address = "0.0.0.0:8080" 114 | } 115 | return config 116 | } 117 | 118 | var ConfigInstance *Config 119 | var Sr *SessionRagen 120 | 121 | func (sr *SessionRagen) NextIndex() int { 122 | sr.Mutex.Lock() 123 | defer sr.Mutex.Unlock() 124 | 125 | index := sr.Index 126 | sr.Index = (index + 1) % len(ConfigInstance.Sessions) 127 | return index 128 | } 129 | func init() { 130 | rand.Seed(time.Now().UnixNano()) 131 | // 加载环境变量 132 | _ = godotenv.Load() 133 | Sr = &SessionRagen{ 134 | Index: 0, 135 | Mutex: sync.Mutex{}, 136 | } 137 | ConfigInstance = LoadConfig() 138 | logger.Info("Loaded config:") 139 | logger.Info(fmt.Sprintf("Sessions count: %d", ConfigInstance.RetryCount)) 140 | for _, session := range ConfigInstance.Sessions { 141 | logger.Info(fmt.Sprintf("Session: %s", session.SessionKey)) 142 | } 143 | logger.Info(fmt.Sprintf("Address: %s", ConfigInstance.Address)) 144 | logger.Info(fmt.Sprintf("APIKey: %s", ConfigInstance.APIKey)) 145 | logger.Info(fmt.Sprintf("Proxy: %s", ConfigInstance.Proxy)) 146 | logger.Info(fmt.Sprintf("IsIncognito: %t", ConfigInstance.IsIncognito)) 147 | logger.Info(fmt.Sprintf("MaxChatHistoryLength: %d", ConfigInstance.MaxChatHistoryLength)) 148 | logger.Info(fmt.Sprintf("NoRolePrefix: %t", ConfigInstance.NoRolePrefix)) 149 | logger.Info(fmt.Sprintf("SearchResultCompatible: %t", ConfigInstance.SearchResultCompatible)) 150 | logger.Info(fmt.Sprintf("PromptForFile: %s", ConfigInstance.PromptForFile)) 151 | logger.Info(fmt.Sprintf("IgnoreSerchResult: %t", ConfigInstance.IgnoreSerchResult)) 152 | } 153 | -------------------------------------------------------------------------------- /config/model.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | var ModelReverseMap = map[string]string{} 4 | var ModelMap = map[string]string{ 5 | "claude-4.0-sonnet": "claude2", 6 | "claude-4.0-sonnet-think": "claude37sonnetthinking", 7 | "deepseek-r1": "r1", 8 | "gpt-4.5": "gpt45", 9 | "o3-mini": "o3mini", 10 | "o4-mini": "o4mini", 11 | "gpt-4o": "gpt4o", 12 | "gemini-2.5-pro": "gemini2flash", 13 | "grok-3-beta": "grok", 14 | "gpt-4.1": "gpt41", 15 | } 16 | 17 | // Get returns the value for the given key from the ModelMap. 18 | // If the key doesn't exist, it returns the provided default value. 19 | func ModelMapGet(key string, defaultValue string) string { 20 | if value, exists := ModelMap[key]; exists { 21 | return value 22 | } 23 | return defaultValue 24 | } 25 | 26 | // GetReverse returns the value for the given key from the ModelReverseMap. 27 | // If the key doesn't exist, it returns the provided default value. 28 | func ModelReverseMapGet(key string, defaultValue string) string { 29 | if value, exists := ModelReverseMap[key]; exists { 30 | return value 31 | } 32 | return defaultValue 33 | } 34 | 35 | var ResponseModles []map[string]string 36 | 37 | func init() { 38 | for k, v := range ModelMap { 39 | ModelReverseMap[v] = k 40 | model := map[string]string{ 41 | "id": k, 42 | } 43 | modelSearch := map[string]string{ 44 | "id": k + "-search", 45 | } 46 | ResponseModles = append(ResponseModles, model, modelSearch) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /core/api.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/base64" 7 | "encoding/json" 8 | "fmt" 9 | "io" 10 | "mime/multipart" 11 | "net/http" 12 | "pplx2api/config" 13 | "pplx2api/logger" 14 | "pplx2api/model" 15 | "pplx2api/utils" 16 | "strings" 17 | "time" 18 | 19 | "github.com/gin-gonic/gin" 20 | "github.com/google/uuid" 21 | "github.com/imroc/req/v3" 22 | ) 23 | 24 | // Client represents a Perplexity API client 25 | type Client struct { 26 | sessionToken string 27 | client *req.Client 28 | Model string 29 | Attachments []string 30 | OpenSerch bool 31 | } 32 | 33 | // Perplexity API structures 34 | type PerplexityRequest struct { 35 | Params PerplexityParams `json:"params"` 36 | QueryStr string `json:"query_str"` 37 | } 38 | 39 | type PerplexityParams struct { 40 | Attachments []string `json:"attachments"` 41 | Language string `json:"language"` 42 | Timezone string `json:"timezone"` 43 | SearchFocus string `json:"search_focus"` 44 | Sources []string `json:"sources"` 45 | SearchRecencyFilter interface{} `json:"search_recency_filter"` 46 | FrontendUUID string `json:"frontend_uuid"` 47 | Mode string `json:"mode"` 48 | ModelPreference string `json:"model_preference"` 49 | IsRelatedQuery bool `json:"is_related_query"` 50 | IsSponsored bool `json:"is_sponsored"` 51 | VisitorID string `json:"visitor_id"` 52 | UserNextauthID string `json:"user_nextauth_id"` 53 | FrontendContextUUID string `json:"frontend_context_uuid"` 54 | PromptSource string `json:"prompt_source"` 55 | QuerySource string `json:"query_source"` 56 | BrowserHistorySummary []interface{} `json:"browser_history_summary"` 57 | IsIncognito bool `json:"is_incognito"` 58 | UseSchematizedAPI bool `json:"use_schematized_api"` 59 | SendBackTextInStreaming bool `json:"send_back_text_in_streaming_api"` 60 | SupportedBlockUseCases []string `json:"supported_block_use_cases"` 61 | ClientCoordinates interface{} `json:"client_coordinates"` 62 | IsNavSuggestionsDisabled bool `json:"is_nav_suggestions_disabled"` 63 | Version string `json:"version"` 64 | } 65 | 66 | // Response structures 67 | type PerplexityResponse struct { 68 | Blocks []Block `json:"blocks"` 69 | Status string `json:"status"` 70 | DisplayModel string `json:"display_model"` 71 | } 72 | 73 | type Block struct { 74 | MarkdownBlock *MarkdownBlock `json:"markdown_block,omitempty"` 75 | ReasoningPlanBlock *ReasoningPlanBlock `json:"reasoning_plan_block,omitempty"` 76 | WebResultBlock *WebResultBlock `json:"web_result_block,omitempty"` 77 | ImageModeBlock *ImageModeBlock `json:"image_mode_block,omitempty"` 78 | } 79 | 80 | type MarkdownBlock struct { 81 | Chunks []string `json:"chunks"` 82 | } 83 | 84 | type ReasoningPlanBlock struct { 85 | Goals []Goal `json:"goals"` 86 | } 87 | 88 | type Goal struct { 89 | Description string `json:"description"` 90 | } 91 | 92 | type WebResultBlock struct { 93 | WebResults []WebResult `json:"web_results"` 94 | } 95 | 96 | type WebResult struct { 97 | Name string `json:"name"` 98 | Snippet string `json:"snippet"` 99 | URL string `json:"url"` 100 | } 101 | 102 | type ImageModeBlock struct { 103 | AnswerModeType string `json:"answer_mode_type"` 104 | Progress string `json:"progress"` 105 | MediaItems []struct { 106 | Medium string `json:"medium"` 107 | Image string `json:"image"` 108 | URL string `json:"url"` 109 | Name string `json:"name"` 110 | Source string `json:"source"` 111 | Thumbnail string `json:"thumbnail"` 112 | } `json:"media_items"` 113 | } 114 | 115 | // NewClient creates a new Perplexity API client 116 | func NewClient(sessionToken string, proxy string, model string, openSerch bool) *Client { 117 | client := req.C().ImpersonateChrome().SetTimeout(time.Minute * 10) 118 | client.Transport.SetResponseHeaderTimeout(time.Second * 10) 119 | if proxy != "" { 120 | client.SetProxyURL(proxy) 121 | } 122 | 123 | // Set common headers 124 | headers := map[string]string{ 125 | "accept-language": "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7,zh-TW;q=0.6", 126 | "cache-control": "no-cache", 127 | "origin": "https://www.perplexity.ai", 128 | "pragma": "no-cache", 129 | "priority": "u=1, i", 130 | "referer": "https://www.perplexity.ai/", 131 | } 132 | 133 | for key, value := range headers { 134 | client.SetCommonHeader(key, value) 135 | } 136 | 137 | // Set cookies 138 | if sessionToken != "" { 139 | client.SetCommonCookies(&http.Cookie{ 140 | Name: "__Secure-next-auth.session-token", 141 | Value: sessionToken, 142 | }) 143 | } 144 | 145 | // Create client with visitor ID 146 | c := &Client{ 147 | sessionToken: sessionToken, 148 | client: client, 149 | Model: model, 150 | Attachments: []string{}, 151 | OpenSerch: openSerch, 152 | } 153 | 154 | return c 155 | } 156 | 157 | // SendMessage sends a message to Perplexity and returns the status and response 158 | func (c *Client) SendMessage(message string, stream bool, is_incognito bool, gc *gin.Context) (int, error) { 159 | // Create request body 160 | requestBody := PerplexityRequest{ 161 | Params: PerplexityParams{ 162 | Attachments: c.Attachments, 163 | Language: "en-US", 164 | Timezone: "America/New_York", 165 | SearchFocus: "writing", 166 | Sources: []string{}, 167 | // SearchFocus: "internet", 168 | // Sources: []string{"web"}, 169 | SearchRecencyFilter: nil, 170 | FrontendUUID: uuid.New().String(), 171 | Mode: "copilot", 172 | ModelPreference: c.Model, 173 | IsRelatedQuery: false, 174 | IsSponsored: false, 175 | VisitorID: uuid.New().String(), 176 | UserNextauthID: uuid.New().String(), 177 | FrontendContextUUID: uuid.New().String(), 178 | PromptSource: "user", 179 | QuerySource: "home", 180 | BrowserHistorySummary: []interface{}{}, 181 | IsIncognito: is_incognito, 182 | UseSchematizedAPI: true, 183 | SendBackTextInStreaming: false, 184 | SupportedBlockUseCases: []string{ 185 | "answer_modes", 186 | "media_items", 187 | "knowledge_cards", 188 | "inline_entity_cards", 189 | "place_widgets", 190 | "finance_widgets", 191 | "sports_widgets", 192 | "shopping_widgets", 193 | "jobs_widgets", 194 | "search_result_widgets", 195 | "entity_list_answer", 196 | "todo_list", 197 | }, 198 | ClientCoordinates: nil, 199 | IsNavSuggestionsDisabled: false, 200 | Version: "2.18", 201 | }, 202 | QueryStr: message, 203 | } 204 | if c.OpenSerch { 205 | requestBody.Params.SearchFocus = "internet" 206 | requestBody.Params.Sources = append(requestBody.Params.Sources, "web") 207 | } 208 | logger.Info(fmt.Sprintf("Perplexity request body: %v", requestBody)) 209 | // Make the request 210 | resp, err := c.client.R().DisableAutoReadResponse(). 211 | SetBody(requestBody). 212 | Post("https://www.perplexity.ai/rest/sse/perplexity_ask") 213 | 214 | if err != nil { 215 | logger.Error(fmt.Sprintf("Error sending request: %v", err)) 216 | return 500, fmt.Errorf("request failed: %w", err) 217 | } 218 | 219 | logger.Info(fmt.Sprintf("Perplexity response status code: %d", resp.StatusCode)) 220 | 221 | if resp.StatusCode == http.StatusTooManyRequests { 222 | resp.Body.Close() 223 | return http.StatusTooManyRequests, fmt.Errorf("rate limit exceeded") 224 | } 225 | 226 | if resp.StatusCode != http.StatusOK { 227 | logger.Error(fmt.Sprintf("Unexpected return data: %s", resp.String())) 228 | resp.Body.Close() 229 | return resp.StatusCode, fmt.Errorf("unexpected status code: %d", resp.StatusCode) 230 | } 231 | 232 | return 200, c.HandleResponse(resp.Body, stream, gc) 233 | } 234 | 235 | func (c *Client) HandleResponse(body io.ReadCloser, stream bool, gc *gin.Context) error { 236 | defer body.Close() 237 | // Set headers for streaming 238 | if stream { 239 | gc.Writer.Header().Set("Content-Type", "text/event-stream") 240 | gc.Writer.Header().Set("Cache-Control", "no-cache") 241 | gc.Writer.Header().Set("Connection", "keep-alive") 242 | gc.Writer.WriteHeader(http.StatusOK) 243 | gc.Writer.Flush() 244 | } 245 | scanner := bufio.NewScanner(body) 246 | clientDone := gc.Request.Context().Done() 247 | // 增大缓冲区大小 248 | scanner.Buffer(make([]byte, 1024*1024), 1024*1024) 249 | full_text := "" 250 | inThinking := false 251 | thinkShown := false 252 | final := false 253 | for scanner.Scan() { 254 | select { 255 | case <-clientDone: 256 | logger.Info("Client connection closed") 257 | return nil 258 | default: 259 | } 260 | 261 | line := scanner.Text() 262 | // Skip empty lines 263 | if line == "" { 264 | continue 265 | } 266 | if !strings.HasPrefix(line, "data: ") { 267 | continue 268 | } 269 | data := line[6:] 270 | // logger.Info(fmt.Sprintf("Received data: %s", data)) 271 | var response PerplexityResponse 272 | if err := json.Unmarshal([]byte(data), &response); err != nil { 273 | logger.Error(fmt.Sprintf("Error parsing JSON: %v", err)) 274 | continue 275 | } 276 | // Check for completion and web results 277 | if response.Status == "COMPLETED" { 278 | final = true 279 | for _, block := range response.Blocks { 280 | if block.ImageModeBlock != nil && block.ImageModeBlock.Progress == "DONE" && len(block.ImageModeBlock.MediaItems) > 0 { 281 | imageResultsText := "" 282 | imageModelList := []string{} 283 | for i, result := range block.ImageModeBlock.MediaItems { 284 | imageResultsText += utils.ImageShow(i, result.Name, result.Image) 285 | imageModelList = append(imageModelList, result.Name) 286 | 287 | } 288 | if len(imageModelList) > 0 { 289 | imageResultsText = imageResultsText + "\n\n---\n" + strings.Join(imageModelList, ", ") 290 | } 291 | full_text += imageResultsText 292 | 293 | if stream { 294 | model.ReturnOpenAIResponse(imageResultsText, stream, gc) 295 | } 296 | } 297 | } 298 | for _, block := range response.Blocks { 299 | if !config.ConfigInstance.IgnoreSerchResult && block.WebResultBlock != nil && len(block.WebResultBlock.WebResults) > 0 { 300 | webResultsText := "\n\n---\n" 301 | for i, result := range block.WebResultBlock.WebResults { 302 | webResultsText += "\n\n" + utils.SearchShow(i, result.Name, result.URL, result.Snippet) 303 | } 304 | full_text += webResultsText 305 | 306 | if stream { 307 | model.ReturnOpenAIResponse(webResultsText, stream, gc) 308 | } 309 | } 310 | 311 | } 312 | 313 | if response.DisplayModel != c.Model { 314 | res_text := "\n\n---\n" 315 | res_text += fmt.Sprintf("Display Model: %s\n", config.ModelReverseMapGet(response.DisplayModel, response.DisplayModel)) 316 | full_text += res_text 317 | if !stream { 318 | break 319 | } 320 | model.ReturnOpenAIResponse(res_text, stream, gc) 321 | } 322 | break 323 | } 324 | if final { 325 | break 326 | } 327 | // Process each block in the response 328 | for _, block := range response.Blocks { 329 | // Handle reasoning plan blocks (thinking) 330 | if block.ReasoningPlanBlock != nil && len(block.ReasoningPlanBlock.Goals) > 0 { 331 | 332 | res_text := "" 333 | if !inThinking && !thinkShown { 334 | res_text += "" 335 | inThinking = true 336 | } 337 | 338 | for _, goal := range block.ReasoningPlanBlock.Goals { 339 | if goal.Description != "" && goal.Description != "Beginning analysis" && goal.Description != "Wrapping up analysis" { 340 | res_text += goal.Description 341 | } 342 | } 343 | full_text += res_text 344 | if !stream { 345 | continue 346 | } 347 | model.ReturnOpenAIResponse(res_text, stream, gc) 348 | } 349 | } 350 | for _, block := range response.Blocks { 351 | if block.MarkdownBlock != nil && len(block.MarkdownBlock.Chunks) > 0 { 352 | res_text := "" 353 | if inThinking { 354 | res_text += "\n" 355 | inThinking = false 356 | thinkShown = true 357 | } 358 | for _, chunk := range block.MarkdownBlock.Chunks { 359 | if chunk != "" { 360 | res_text += chunk 361 | } 362 | } 363 | full_text += res_text 364 | if !stream { 365 | continue 366 | } 367 | model.ReturnOpenAIResponse(res_text, stream, gc) 368 | } 369 | } 370 | 371 | } 372 | 373 | if err := scanner.Err(); err != nil { 374 | return fmt.Errorf("error reading response: %w", err) 375 | } 376 | 377 | if !stream { 378 | model.ReturnOpenAIResponse(full_text, stream, gc) 379 | } else { 380 | // Send end marker for streaming mode 381 | gc.Writer.Write([]byte("data: [DONE]\n\n")) 382 | gc.Writer.Flush() 383 | } 384 | 385 | return nil 386 | } 387 | 388 | // UploadURLResponse represents the response from the create_upload_url endpoint 389 | type UploadURLResponse struct { 390 | S3BucketURL string `json:"s3_bucket_url"` 391 | S3ObjectURL string `json:"s3_object_url"` 392 | Fields CloudinaryUploadInfo `json:"fields"` 393 | RateLimited bool `json:"rate_limited"` 394 | } 395 | 396 | type CloudinaryUploadInfo struct { 397 | Timestamp int `json:"timestamp"` 398 | UniqueFilename string `json:"unique_filename"` 399 | Folder string `json:"folder"` 400 | UseFilename string `json:"use_filename"` 401 | PublicID string `json:"public_id"` 402 | Transformation string `json:"transformation"` 403 | Moderation string `json:"moderation"` 404 | ResourceType string `json:"resource_type"` 405 | APIKey string `json:"api_key"` 406 | CloudName string `json:"cloud_name"` 407 | Signature string `json:"signature"` 408 | AWSAccessKeyId string `json:"AWSAccessKeyId"` 409 | Key string `json:"key"` 410 | Tagging string `json:"tagging"` 411 | Policy string `json:"policy"` 412 | Xamzsecuritytoken string `json:"x-amz-security-token"` 413 | ACL string `json:"acl"` 414 | } 415 | 416 | // UploadFile is a placeholder for file upload functionality 417 | func (c *Client) createUploadURL(filename string, contentType string) (*UploadURLResponse, error) { 418 | requestBody := map[string]interface{}{ 419 | "filename": filename, 420 | "content_type": contentType, 421 | "source": "default", 422 | "file_size": 12000, 423 | "force_image": false, 424 | } 425 | resp, err := c.client.R(). 426 | SetBody(requestBody). 427 | Post("https://www.perplexity.ai/rest/uploads/create_upload_url?version=2.18&source=default") 428 | if err != nil { 429 | logger.Error(fmt.Sprintf("Error creating upload URL: %v", err)) 430 | return nil, err 431 | } 432 | if resp.StatusCode != http.StatusOK { 433 | logger.Error(fmt.Sprintf("Image Upload with status code %d: %s", resp.StatusCode, resp.String())) 434 | return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) 435 | } 436 | var uploadURLResponse UploadURLResponse 437 | logger.Info(fmt.Sprintf("Create upload with status code %d: %s", resp.StatusCode, resp.String())) 438 | if err := json.Unmarshal(resp.Bytes(), &uploadURLResponse); err != nil { 439 | logger.Error(fmt.Sprintf("Error unmarshalling upload URL response: %v", err)) 440 | return nil, err 441 | } 442 | if uploadURLResponse.RateLimited { 443 | logger.Error("Rate limit exceeded for upload URL") 444 | return nil, fmt.Errorf("rate limit exceeded") 445 | } 446 | return &uploadURLResponse, nil 447 | 448 | } 449 | 450 | func (c *Client) UploadImage(img_list []string) error { 451 | logger.Info(fmt.Sprintf("Uploading %d images to Cloudinary", len(img_list))) 452 | 453 | // Upload images to Cloudinary 454 | for _, img := range img_list { 455 | filename := utils.RandomString(5) + ".jpg" 456 | // Create upload URL 457 | uploadURLResponse, err := c.createUploadURL(filename, "image/jpeg") 458 | if err != nil { 459 | logger.Error(fmt.Sprintf("Error creating upload URL: %v", err)) 460 | return err 461 | } 462 | logger.Info(fmt.Sprintf("Upload URL response: %v", uploadURLResponse)) 463 | // Upload image to Cloudinary 464 | err = c.UloadFileToCloudinary(uploadURLResponse.Fields, "img", img, filename) 465 | if err != nil { 466 | logger.Error(fmt.Sprintf("Error uploading image: %v", err)) 467 | return err 468 | } 469 | } 470 | return nil 471 | } 472 | 473 | func (c *Client) UloadFileToCloudinary(uploadInfo CloudinaryUploadInfo, contentType string, filedata string, filename string) error { 474 | if len(filedata) > 100 { 475 | logger.Info(fmt.Sprintf("filedata: %s ……", filedata[:50])) 476 | } 477 | // Add form fields 478 | logger.Info(fmt.Sprintf("Uploading file %s to Cloudinary", filename)) 479 | var formFields map[string]string 480 | if contentType == "img" { 481 | formFields = map[string]string{ 482 | "timestamp": fmt.Sprintf("%d", uploadInfo.Timestamp), 483 | "unique_filename": uploadInfo.UniqueFilename, 484 | "folder": uploadInfo.Folder, 485 | "use_filename": uploadInfo.UseFilename, 486 | "public_id": uploadInfo.PublicID, 487 | "transformation": uploadInfo.Transformation, 488 | "moderation": uploadInfo.Moderation, 489 | "resource_type": uploadInfo.ResourceType, 490 | "api_key": uploadInfo.APIKey, 491 | "cloud_name": uploadInfo.CloudName, 492 | "signature": uploadInfo.Signature, 493 | "type": "private", 494 | } 495 | } else { 496 | formFields = map[string]string{ 497 | "acl": uploadInfo.ACL, 498 | "Content-Type": "text/plain", 499 | "tagging": uploadInfo.Tagging, 500 | "key": uploadInfo.Key, 501 | "AWSAccessKeyId": uploadInfo.AWSAccessKeyId, 502 | "x-amz-security-token": uploadInfo.Xamzsecuritytoken, 503 | "policy": uploadInfo.Policy, 504 | "signature": uploadInfo.Signature, 505 | } 506 | } 507 | var requestBody bytes.Buffer 508 | writer := multipart.NewWriter(&requestBody) 509 | for key, value := range formFields { 510 | if err := writer.WriteField(key, value); err != nil { 511 | logger.Error(fmt.Sprintf("Error writing form field %s: %v", key, err)) 512 | return err 513 | } 514 | } 515 | 516 | // Add the file,filedata 是base64编码的字符串 517 | decodedData, err := base64.StdEncoding.DecodeString(filedata) 518 | if err != nil { 519 | logger.Error(fmt.Sprintf("Error decoding base64 data: %v", err)) 520 | return err 521 | } 522 | 523 | // 创建一个文件部分 524 | part, err := writer.CreateFormFile("file", filename) // 替换 filename.ext 为实际文件名 525 | if err != nil { 526 | logger.Error(fmt.Sprintf("Error creating form file: %v", err)) 527 | return err 528 | } 529 | 530 | // 将解码后的数据写入文件部分 531 | if _, err := part.Write(decodedData); err != nil { 532 | logger.Error(fmt.Sprintf("Error writing file data: %v", err)) 533 | return err 534 | } 535 | // Close the writer to finalize the form 536 | if err := writer.Close(); err != nil { 537 | logger.Error(fmt.Sprintf("Error closing writer: %v", err)) 538 | return err 539 | } 540 | 541 | // Create the upload request 542 | var uploadURL string 543 | if contentType == "img" { 544 | uploadURL = fmt.Sprintf("https://api.cloudinary.com/v1_1/%s/image/upload", uploadInfo.CloudName) 545 | } else { 546 | uploadURL = "https://ppl-ai-file-upload.s3.amazonaws.com/" 547 | } 548 | 549 | resp, err := c.client.R(). 550 | SetHeader("Content-Type", writer.FormDataContentType()). 551 | SetBodyBytes(requestBody.Bytes()). 552 | Post(uploadURL) 553 | 554 | if err != nil { 555 | logger.Error(fmt.Sprintf("Error uploading file: %v", err)) 556 | return err 557 | } 558 | logger.Info(fmt.Sprintf("Image Upload with status code %d: %s", resp.StatusCode, resp.String())) 559 | if contentType == "img" { 560 | var uploadResponse map[string]interface{} 561 | if err := json.Unmarshal(resp.Bytes(), &uploadResponse); err != nil { 562 | return err 563 | } 564 | imgUrl := uploadResponse["secure_url"].(string) 565 | imgUrl = "https://pplx-res.cloudinary.com/image/private" + imgUrl[strings.Index(imgUrl, "/user_uploads"):] 566 | c.Attachments = append(c.Attachments, imgUrl) 567 | } else { 568 | c.Attachments = append(c.Attachments, "https://ppl-ai-file-upload.s3.amazonaws.com/"+uploadInfo.Key) 569 | } 570 | return nil 571 | } 572 | 573 | // SetBigContext is a placeholder for setting context 574 | func (c *Client) UploadText(context string) error { 575 | logger.Info("Uploading txt to Cloudinary") 576 | filedata := base64.StdEncoding.EncodeToString([]byte(context)) 577 | filename := utils.RandomString(5) + ".txt" 578 | // Upload images to Cloudinary 579 | uploadURLResponse, err := c.createUploadURL(filename, "text/plain") 580 | if err != nil { 581 | logger.Error(fmt.Sprintf("Error creating upload URL: %v", err)) 582 | return err 583 | } 584 | logger.Info(fmt.Sprintf("Upload URL response: %v", uploadURLResponse)) 585 | // Upload txt to Cloudinary 586 | err = c.UloadFileToCloudinary(uploadURLResponse.Fields, "txt", filedata, filename) 587 | if err != nil { 588 | logger.Error(fmt.Sprintf("Error uploading image: %v", err)) 589 | return err 590 | } 591 | 592 | return nil 593 | } 594 | 595 | func (c *Client) GetNewCookie() (string, error) { 596 | resp, err := c.client.R().Get("https://www.perplexity.ai/api/auth/session") 597 | if err != nil { 598 | logger.Error(fmt.Sprintf("Error getting session cookie: %v", err)) 599 | return "", err 600 | } 601 | if resp.StatusCode != http.StatusOK { 602 | logger.Error(fmt.Sprintf("Error getting session cookie: %s", resp.String())) 603 | return "", fmt.Errorf("unexpected status code: %d", resp.StatusCode) 604 | } 605 | for _, cookie := range resp.Cookies() { 606 | if cookie.Name == "__Secure-next-auth.session-token" { 607 | return cookie.Value, nil 608 | } 609 | } 610 | return "", fmt.Errorf("session cookie not found") 611 | } 612 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module pplx2api 2 | 3 | go 1.22.2 4 | 5 | require ( 6 | github.com/fatih/color v1.18.0 7 | github.com/gin-gonic/gin v1.10.0 8 | github.com/google/uuid v1.6.0 9 | github.com/imroc/req/v3 v3.50.0 10 | github.com/joho/godotenv v1.5.1 11 | ) 12 | 13 | require ( 14 | github.com/andybalholm/brotli v1.1.1 // indirect 15 | github.com/bytedance/sonic v1.11.6 // indirect 16 | github.com/bytedance/sonic/loader v0.1.1 // indirect 17 | github.com/cloudflare/circl v1.5.0 // indirect 18 | github.com/cloudwego/base64x v0.1.4 // indirect 19 | github.com/cloudwego/iasm v0.2.0 // indirect 20 | github.com/gabriel-vasile/mimetype v1.4.3 // indirect 21 | github.com/gin-contrib/sse v0.1.0 // indirect 22 | github.com/go-playground/locales v0.14.1 // indirect 23 | github.com/go-playground/universal-translator v0.18.1 // indirect 24 | github.com/go-playground/validator/v10 v10.20.0 // indirect 25 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 26 | github.com/goccy/go-json v0.10.2 // indirect 27 | github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect 28 | github.com/hashicorp/errwrap v1.1.0 // indirect 29 | github.com/hashicorp/go-multierror v1.1.1 // indirect 30 | github.com/json-iterator/go v1.1.12 // indirect 31 | github.com/klauspost/compress v1.17.11 // indirect 32 | github.com/klauspost/cpuid/v2 v2.2.7 // indirect 33 | github.com/leodido/go-urn v1.4.0 // indirect 34 | github.com/mattn/go-colorable v0.1.13 // indirect 35 | github.com/mattn/go-isatty v0.0.20 // indirect 36 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 37 | github.com/modern-go/reflect2 v1.0.2 // indirect 38 | github.com/onsi/ginkgo/v2 v2.22.0 // indirect 39 | github.com/pelletier/go-toml/v2 v2.2.2 // indirect 40 | github.com/quic-go/qpack v0.5.1 // indirect 41 | github.com/quic-go/quic-go v0.48.2 // indirect 42 | github.com/refraction-networking/utls v1.6.7 // indirect 43 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 44 | github.com/ugorji/go/codec v1.2.12 // indirect 45 | go.uber.org/mock v0.5.0 // indirect 46 | golang.org/x/arch v0.8.0 // indirect 47 | golang.org/x/crypto v0.31.0 // indirect 48 | golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e // indirect 49 | golang.org/x/mod v0.22.0 // indirect 50 | golang.org/x/net v0.33.0 // indirect 51 | golang.org/x/sync v0.10.0 // indirect 52 | golang.org/x/sys v0.28.0 // indirect 53 | golang.org/x/text v0.21.0 // indirect 54 | golang.org/x/tools v0.28.0 // indirect 55 | google.golang.org/protobuf v1.34.1 // indirect 56 | gopkg.in/yaml.v3 v3.0.1 // indirect 57 | ) 58 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= 2 | github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= 3 | github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= 4 | github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= 5 | github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= 6 | github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 7 | github.com/cloudflare/circl v1.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys= 8 | github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= 9 | github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= 10 | github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= 11 | github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= 12 | github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= 13 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 15 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 16 | github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= 17 | github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= 18 | github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= 19 | github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= 20 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 21 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 22 | github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= 23 | github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= 24 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 25 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 26 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 27 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 28 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 29 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 30 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 31 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 32 | github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= 33 | github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= 34 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 35 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 36 | github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= 37 | github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 38 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 39 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 40 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 41 | github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg= 42 | github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= 43 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 44 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 45 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 46 | github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 47 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 48 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 49 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 50 | github.com/imroc/req/v3 v3.50.0 h1:n3BVnZiTRpvkN5T1IB79LC/THhFU9iXksNRMH4ZNVaY= 51 | github.com/imroc/req/v3 v3.50.0/go.mod h1:tsOk8K7zI6cU4xu/VWCZVtq9Djw9IWm4MslKzme5woU= 52 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 53 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 54 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 55 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 56 | github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= 57 | github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= 58 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 59 | github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= 60 | github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 61 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 62 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 63 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 64 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 65 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 66 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 67 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 68 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 69 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 70 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 71 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 72 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 73 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 74 | github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= 75 | github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= 76 | github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= 77 | github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= 78 | github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= 79 | github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= 80 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 81 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 82 | github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= 83 | github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= 84 | github.com/quic-go/quic-go v0.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KWE= 85 | github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs= 86 | github.com/refraction-networking/utls v1.6.7 h1:zVJ7sP1dJx/WtVuITug3qYUq034cDq9B2MR1K67ULZM= 87 | github.com/refraction-networking/utls v1.6.7/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0= 88 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 89 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 90 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 91 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 92 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 93 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 94 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 95 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 96 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 97 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 98 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 99 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 100 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 101 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 102 | github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= 103 | github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 104 | github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= 105 | github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= 106 | go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= 107 | go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= 108 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 109 | golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= 110 | golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= 111 | golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= 112 | golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= 113 | golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e h1:4qufH0hlUYs6AO6XmZC3GqfDPGSXHVXUFR6OND+iJX4= 114 | golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= 115 | golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= 116 | golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 117 | golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= 118 | golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= 119 | golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= 120 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 121 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 122 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 123 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 124 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 125 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 126 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 127 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 128 | golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= 129 | golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 130 | golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8= 131 | golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= 132 | google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= 133 | google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 134 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 135 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 136 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 137 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 138 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 139 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= 140 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 141 | -------------------------------------------------------------------------------- /job/cookie.go: -------------------------------------------------------------------------------- 1 | package job 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "log" 7 | "os" 8 | "sync" 9 | "time" 10 | 11 | "pplx2api/config" 12 | "pplx2api/core" 13 | ) 14 | 15 | const ( 16 | // ConfigFileName is the name of the file to store sessions 17 | ConfigFileName = "sessions.json" 18 | ) 19 | 20 | var ( 21 | sessionUpdaterInstance *SessionUpdater 22 | sessionUpdaterOnce sync.Once 23 | ) 24 | 25 | // SessionConfig represents the structure to be saved to file 26 | type SessionConfig struct { 27 | Sessions []config.SessionInfo `json:"sessions"` 28 | } 29 | 30 | // SessionUpdater 管理 Perplexity 会话的定时更新 31 | type SessionUpdater struct { 32 | interval time.Duration 33 | stopChan chan struct{} 34 | isRunning bool 35 | runningLock sync.Mutex 36 | configPath string 37 | } 38 | 39 | // NewSessionUpdater 创建一个新的会话更新器 40 | // interval: 更新间隔时间 41 | func GetSessionUpdater(interval time.Duration) *SessionUpdater { 42 | sessionUpdaterOnce.Do(func() { 43 | // 使用当前文件夹下的配置文件 44 | configPath := ConfigFileName 45 | 46 | sessionUpdaterInstance = &SessionUpdater{ 47 | interval: interval, 48 | stopChan: make(chan struct{}), 49 | isRunning: false, 50 | configPath: configPath, 51 | } 52 | // 初始化时从文件加载会话 53 | sessionUpdaterInstance.loadSessionsFromFile() 54 | }) 55 | return sessionUpdaterInstance 56 | } 57 | 58 | // loadSessionsFromFile loads sessions from the config file if it exists 59 | func (su *SessionUpdater) loadSessionsFromFile() { 60 | // Check if file exists 61 | if _, err := os.Stat(su.configPath); os.IsNotExist(err) { 62 | log.Println("No sessions config file found, will create on first update") 63 | return 64 | } 65 | 66 | // Read the file 67 | data, err := ioutil.ReadFile(su.configPath) 68 | if err != nil { 69 | log.Printf("Failed to read sessions config file: %v", err) 70 | return 71 | } 72 | 73 | // Parse the JSON 74 | var sessionConfig SessionConfig 75 | if err := json.Unmarshal(data, &sessionConfig); err != nil { 76 | log.Printf("Failed to parse sessions config file: %v", err) 77 | return 78 | } 79 | 80 | // Update the config with loaded sessions 81 | config.ConfigInstance.RwMutex.Lock() 82 | config.ConfigInstance.Sessions = sessionConfig.Sessions 83 | config.ConfigInstance.RwMutex.Unlock() 84 | 85 | log.Printf("Loaded %d sessions from config file", len(sessionConfig.Sessions)) 86 | } 87 | 88 | // saveSessionsToFile saves the current sessions to the config file 89 | func (su *SessionUpdater) saveSessionsToFile() error { 90 | // Get current sessions 91 | config.ConfigInstance.RwMutex.RLock() 92 | sessionsCopy := make([]config.SessionInfo, len(config.ConfigInstance.Sessions)) 93 | copy(sessionsCopy, config.ConfigInstance.Sessions) 94 | config.ConfigInstance.RwMutex.RUnlock() 95 | 96 | // Create config structure 97 | sessionConfig := SessionConfig{ 98 | Sessions: sessionsCopy, 99 | } 100 | 101 | // Convert to JSON 102 | data, err := json.MarshalIndent(sessionConfig, "", " ") 103 | if err != nil { 104 | return err 105 | } 106 | 107 | // Write to file 108 | err = ioutil.WriteFile(su.configPath, data, 0644) 109 | if err != nil { 110 | return err 111 | } 112 | 113 | log.Printf("Saved %d sessions to sessions.json file", len(sessionsCopy)) 114 | return nil 115 | } 116 | 117 | // Start 启动定时更新任务 118 | func (su *SessionUpdater) Start() { 119 | su.runningLock.Lock() 120 | defer su.runningLock.Unlock() 121 | if su.isRunning { 122 | log.Println("Session updater is already running") 123 | return 124 | } 125 | su.isRunning = true 126 | su.stopChan = make(chan struct{}) 127 | go su.runUpdateLoop() 128 | log.Println("Session updater started with interval:", su.interval) 129 | } 130 | 131 | // Stop 停止定时更新任务 132 | func (su *SessionUpdater) Stop() { 133 | su.runningLock.Lock() 134 | defer su.runningLock.Unlock() 135 | if !su.isRunning { 136 | log.Println("Session updater is not running") 137 | return 138 | } 139 | close(su.stopChan) 140 | su.isRunning = false 141 | log.Println("Session updater stopped") 142 | } 143 | 144 | // runUpdateLoop 运行更新循环 145 | func (su *SessionUpdater) runUpdateLoop() { 146 | ticker := time.NewTicker(su.interval) 147 | defer ticker.Stop() 148 | // 立即执行一次更新 149 | // su.updateAllSessions() 150 | for { 151 | select { 152 | case <-ticker.C: 153 | su.updateAllSessions() 154 | case <-su.stopChan: 155 | log.Println("Update loop terminated") 156 | return 157 | } 158 | } 159 | } 160 | 161 | // updateAllSessions 更新所有会话 162 | func (su *SessionUpdater) updateAllSessions() { 163 | log.Println("Starting session update for all sessions...") 164 | // 复制当前会话列表,避免长时间持有锁 165 | config.ConfigInstance.RwMutex.RLock() 166 | sessionsCopy := make([]config.SessionInfo, len(config.ConfigInstance.Sessions)) 167 | copy(sessionsCopy, config.ConfigInstance.Sessions) 168 | proxy := config.ConfigInstance.Proxy 169 | config.ConfigInstance.RwMutex.RUnlock() 170 | // 如果没有会话需要更新,直接返回 171 | if len(sessionsCopy) == 0 { 172 | log.Println("No sessions to update") 173 | return 174 | } 175 | // 创建更新后的会话切片 176 | updatedSessions := make([]config.SessionInfo, len(sessionsCopy)) 177 | var wg sync.WaitGroup 178 | // 对每个会话执行更新 179 | for i, session := range sessionsCopy { 180 | wg.Add(1) 181 | go func(index int, origSession config.SessionInfo) { 182 | defer wg.Done() 183 | // 创建客户端并更新 cookie 184 | // 写死 model 和 openSearch 参数 185 | client := core.NewClient(origSession.SessionKey, proxy, "claude-3-opus-20240229", false) 186 | newCookie, err := client.GetNewCookie() 187 | if err != nil { 188 | log.Printf("Failed to update session %d: %v", index, err) 189 | // 如果更新失败,保留原始会话 190 | updatedSessions[index] = origSession 191 | return 192 | } 193 | // 创建更新后的会话对象 194 | updatedSessions[index] = config.SessionInfo{ 195 | SessionKey: newCookie, 196 | } 197 | }(i, session) 198 | } 199 | // 等待所有更新完成 200 | wg.Wait() 201 | // 一次性替换所有会话 202 | config.ConfigInstance.RwMutex.Lock() 203 | config.ConfigInstance.Sessions = updatedSessions 204 | config.ConfigInstance.RwMutex.Unlock() 205 | log.Printf("All %d sessions have been updated", len(updatedSessions)) 206 | 207 | // 保存更新后的配置到文件 208 | if err := su.saveSessionsToFile(); err != nil { 209 | log.Printf("Failed to save updated config: %v", err) 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | 8 | "github.com/fatih/color" 9 | ) 10 | 11 | // 日志级别 12 | const ( 13 | DEBUG = iota 14 | INFO 15 | WARN 16 | ERROR 17 | FATAL 18 | ) 19 | 20 | var levelNames = map[int]string{ 21 | DEBUG: "DEBUG", 22 | INFO: "INFO", 23 | WARN: "WARN", 24 | ERROR: "ERROR", 25 | FATAL: "FATAL", 26 | } 27 | 28 | var levelColors = map[int]func(format string, a ...interface{}) string{ 29 | DEBUG: color.BlueString, 30 | INFO: color.GreenString, 31 | WARN: color.YellowString, 32 | ERROR: color.RedString, 33 | FATAL: color.New(color.FgHiRed, color.Bold).SprintfFunc(), 34 | } 35 | 36 | // 全局日志级别,默认为INFO 37 | var logLevel = INFO 38 | 39 | // SetLevel 设置日志级别 40 | func SetLevel(level int) { 41 | if level >= DEBUG && level <= FATAL { 42 | logLevel = level 43 | } 44 | } 45 | 46 | // GetLevel 获取当前日志级别 47 | func GetLevel() int { 48 | return logLevel 49 | } 50 | 51 | // GetLevelName 获取日志级别名称 52 | func GetLevelName(level int) string { 53 | if name, ok := levelNames[level]; ok { 54 | return name 55 | } 56 | return "UNKNOWN" 57 | } 58 | 59 | // 基础日志打印函数 60 | func log(level int, format string, args ...interface{}) { 61 | if level < logLevel { 62 | return 63 | } 64 | 65 | now := time.Now().Format("2006-01-02 15:04:05.000") 66 | levelName := levelNames[level] 67 | colorFunc := levelColors[level] 68 | 69 | logContent := fmt.Sprintf(format, args...) 70 | logPrefix := fmt.Sprintf("[%s] [%s] ", now, levelName) 71 | 72 | // 使用颜色输出日志级别 73 | fmt.Fprintf(os.Stdout, "%s%s\n", logPrefix, colorFunc(logContent)) 74 | 75 | // 如果是致命错误,则退出程序 76 | if level == FATAL { 77 | os.Exit(1) 78 | } 79 | } 80 | 81 | // Debug 打印调试日志 82 | func Debug(format string, args ...interface{}) { 83 | log(DEBUG, format, args...) 84 | } 85 | 86 | // Info 打印信息日志 87 | func Info(format string, args ...interface{}) { 88 | log(INFO, format, args...) 89 | } 90 | 91 | // Warn 打印警告日志 92 | func Warn(format string, args ...interface{}) { 93 | log(WARN, format, args...) 94 | } 95 | 96 | // Error 打印错误日志 97 | func Error(format string, args ...interface{}) { 98 | log(ERROR, format, args...) 99 | } 100 | 101 | // Fatal 打印致命错误日志并退出程序 102 | func Fatal(format string, args ...interface{}) { 103 | log(FATAL, format, args...) 104 | } 105 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "pplx2api/config" 5 | "pplx2api/job" 6 | "pplx2api/router" 7 | "time" 8 | 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | func main() { 13 | r := gin.Default() 14 | // Load configuration 15 | 16 | // Setup all routes 17 | router.SetupRoutes(r) 18 | // 创建会话更新器,设置更新间隔为24小时 19 | sessionUpdater := job.GetSessionUpdater(24 * time.Hour) 20 | 21 | // 启动会话更新器 22 | sessionUpdater.Start() 23 | defer sessionUpdater.Stop() 24 | 25 | // Run the server on 0.0.0.0:8080 26 | r.Run(config.ConfigInstance.Address) 27 | } 28 | -------------------------------------------------------------------------------- /middleware/auth.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "pplx2api/config" 5 | "strings" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | // AuthMiddleware initializes the Claude client from the request header 11 | func AuthMiddleware() gin.HandlerFunc { 12 | return func(c *gin.Context) { 13 | Key := c.GetHeader("Authorization") 14 | if Key != "" { 15 | Key = strings.TrimPrefix(Key, "Bearer ") 16 | if Key != config.ConfigInstance.APIKey { 17 | c.JSON(401, gin.H{ 18 | "error": "Invalid API key", 19 | }) 20 | c.Abort() 21 | return 22 | } 23 | c.Next() 24 | return 25 | } 26 | c.JSON(401, gin.H{ 27 | "error": "Missing or invalid Authorization header", 28 | }) 29 | c.Abort() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /middleware/cors.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import "github.com/gin-gonic/gin" 4 | 5 | // CORSMiddleware handles CORS headers 6 | func CORSMiddleware() gin.HandlerFunc { 7 | return func(c *gin.Context) { 8 | c.Writer.Header().Set("Access-Control-Allow-Origin", "*") 9 | c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE") 10 | c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, Authorization") 11 | if c.Request.Method == "OPTIONS" { 12 | c.AbortWithStatus(204) 13 | return 14 | } 15 | c.Next() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /model/openai.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "pplx2api/logger" 7 | "time" 8 | 9 | "github.com/gin-gonic/gin" 10 | "github.com/google/uuid" 11 | ) 12 | 13 | type ChatCompletionRequest struct { 14 | Model string `json:"model"` 15 | Messages []map[string]interface{} `json:"messages"` 16 | Stream bool `json:"stream"` 17 | Tools []map[string]interface{} `json:"tools,omitempty"` 18 | } 19 | 20 | // OpenAISrteamResponse 定义 OpenAI 的流式响应结构 21 | type OpenAISrteamResponse struct { 22 | ID string `json:"id"` 23 | Object string `json:"object"` 24 | Created int64 `json:"created"` 25 | Model string `json:"model"` 26 | Choices []StreamChoice `json:"choices"` 27 | } 28 | 29 | // Choice 结构表示 OpenAI 返回的单个选项 30 | type StreamChoice struct { 31 | Index int `json:"index"` 32 | Delta Delta `json:"delta"` 33 | Logprobs interface{} `json:"logprobs"` 34 | FinishReason interface{} `json:"finish_reason"` 35 | } 36 | 37 | type NoStreamChoice struct { 38 | Index int `json:"index"` 39 | Message Message `json:"message"` 40 | Logprobs interface{} `json:"logprobs"` 41 | FinishReason string `json:"finish_reason"` 42 | } 43 | 44 | // Delta 结构用于存储返回的文本内容 45 | type Delta struct { 46 | Content string `json:"content"` 47 | } 48 | type Message struct { 49 | Role string `json:"role"` 50 | Content string `json:"content"` 51 | Refusal interface{} `json:"refusal"` 52 | Annotation []interface{} `json:"annotation"` 53 | } 54 | 55 | type OpenAIResponse struct { 56 | ID string `json:"id"` 57 | Object string `json:"object"` 58 | Created int64 `json:"created"` 59 | Model string `json:"model"` 60 | Choices []NoStreamChoice `json:"choices"` 61 | Usage Usage `json:"usage"` 62 | } 63 | type Usage struct { 64 | PromptTokens int `json:"prompt_tokens"` 65 | CompletionTokens int `json:"completion_tokens"` 66 | TotalTokens int `json:"total_tokens"` 67 | } 68 | 69 | func ReturnOpenAIResponse(text string, stream bool, gc *gin.Context) error { 70 | if stream { 71 | return streamRespose(text, gc) 72 | } else { 73 | return noStreamResponse(text, gc) 74 | } 75 | } 76 | 77 | func streamRespose(text string, gc *gin.Context) error { 78 | openAIResp := &OpenAISrteamResponse{ 79 | ID: uuid.New().String(), 80 | Object: "chat.completion.chunk", 81 | Created: time.Now().Unix(), 82 | Model: "claude-3-7-sonnet-20250219", 83 | Choices: []StreamChoice{ 84 | { 85 | Index: 0, 86 | Delta: Delta{ 87 | Content: text, 88 | }, 89 | Logprobs: nil, 90 | FinishReason: nil, 91 | }, 92 | }, 93 | } 94 | 95 | jsonBytes, err := json.Marshal(openAIResp) 96 | jsonBytes = append([]byte("data: "), jsonBytes...) 97 | jsonBytes = append(jsonBytes, []byte("\n\n")...) 98 | if err != nil { 99 | logger.Error(fmt.Sprintf("Error marshalling JSON: %v", err)) 100 | return err 101 | } 102 | 103 | // 发送数据 104 | gc.Writer.Write(jsonBytes) 105 | gc.Writer.Flush() 106 | return nil 107 | } 108 | 109 | func noStreamResponse(text string, gc *gin.Context) error { 110 | openAIResp := &OpenAIResponse{ 111 | ID: uuid.New().String(), 112 | Object: "chat.completion", 113 | Created: time.Now().Unix(), 114 | Model: "claude-3-7-sonnet-20250219", 115 | Choices: []NoStreamChoice{ 116 | { 117 | Index: 0, 118 | Message: Message{ 119 | Role: "assistant", 120 | Content: text, 121 | }, 122 | Logprobs: nil, 123 | FinishReason: "stop", 124 | }, 125 | }, 126 | } 127 | 128 | gc.JSON(200, openAIResp) 129 | return nil 130 | } 131 | -------------------------------------------------------------------------------- /router/router.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "pplx2api/middleware" 5 | "pplx2api/service" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | func SetupRoutes(r *gin.Engine) { 11 | // Apply middleware 12 | r.Use(middleware.CORSMiddleware()) 13 | r.Use(middleware.AuthMiddleware()) 14 | 15 | // Health check endpoint 16 | r.GET("/health", service.HealthCheckHandler) 17 | 18 | // Chat completions endpoint (OpenAI-compatible) 19 | r.POST("/v1/chat/completions", service.ChatCompletionsHandler) 20 | r.GET("/v1/models", service.MoudlesHandler) 21 | // HuggingFace compatible routes 22 | hfRouter := r.Group("/hf") 23 | { 24 | v1Router := hfRouter.Group("/v1") 25 | { 26 | v1Router.POST("/chat/completions", service.ChatCompletionsHandler) 27 | v1Router.GET("/models", service.MoudlesHandler) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /service/handle.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "pplx2api/config" 7 | "pplx2api/core" 8 | "pplx2api/logger" 9 | "pplx2api/utils" 10 | "strings" 11 | 12 | "github.com/gin-gonic/gin" 13 | ) 14 | 15 | type ChatCompletionRequest struct { 16 | Model string `json:"model"` 17 | Messages []map[string]interface{} `json:"messages"` 18 | Stream bool `json:"stream"` 19 | Tools []map[string]interface{} `json:"tools,omitempty"` 20 | } 21 | 22 | type ErrorResponse struct { 23 | Error string `json:"error"` 24 | } 25 | 26 | // HealthCheckHandler handles the health check endpoint 27 | func HealthCheckHandler(c *gin.Context) { 28 | c.JSON(http.StatusOK, gin.H{ 29 | "status": "ok", 30 | }) 31 | } 32 | 33 | // ChatCompletionsHandler handles the chat completions endpoint 34 | func ChatCompletionsHandler(c *gin.Context) { 35 | 36 | // Parse request body 37 | var req ChatCompletionRequest 38 | if err := c.ShouldBindJSON(&req); err != nil { 39 | c.JSON(http.StatusBadRequest, ErrorResponse{ 40 | Error: fmt.Sprintf("Invalid request: %v", err), 41 | }) 42 | return 43 | } 44 | // logger.Info(fmt.Sprintf("Received request: %v", req)) 45 | // Validate request 46 | if len(req.Messages) == 0 { 47 | c.JSON(http.StatusBadRequest, ErrorResponse{ 48 | Error: "No messages provided", 49 | }) 50 | return 51 | } 52 | 53 | // Get model or use default 54 | model := req.Model 55 | if model == "" { 56 | model = "claude-3.7-sonnet" 57 | } 58 | openSearch := false 59 | if strings.HasSuffix(model, "-search") { 60 | openSearch = true 61 | model = strings.TrimSuffix(model, "-search") 62 | } 63 | model = config.ModelMapGet(model, model) // 获取模型名称 64 | var prompt strings.Builder 65 | img_data_list := []string{} 66 | // Format messages into a single prompt 67 | for _, msg := range req.Messages { 68 | role, roleOk := msg["role"].(string) 69 | if !roleOk { 70 | continue // 忽略无效格式 71 | } 72 | 73 | content, exists := msg["content"] 74 | if !exists { 75 | continue 76 | } 77 | 78 | prompt.WriteString(utils.GetRolePrefix(role)) // 获取角色前缀 79 | switch v := content.(type) { 80 | case string: // 如果 content 直接是 string 81 | prompt.WriteString(v + "\n\n") 82 | case []interface{}: // 如果 content 是 []interface{} 类型的数组 83 | for _, item := range v { 84 | if itemMap, ok := item.(map[string]interface{}); ok { 85 | if itemType, ok := itemMap["type"].(string); ok { 86 | if itemType == "text" { 87 | if text, ok := itemMap["text"].(string); ok { 88 | prompt.WriteString(text + "\n\n") 89 | } 90 | } else if itemType == "image_url" { 91 | if imageUrl, ok := itemMap["image_url"].(map[string]interface{}); ok { 92 | if url, ok := imageUrl["url"].(string); ok { 93 | if len(url) > 50 { 94 | logger.Info(fmt.Sprintf("Image URL: %s ……", url[:50])) 95 | } 96 | if strings.HasPrefix(url, "data:image/") { 97 | // 保留 base64 编码的图片数据 98 | url = strings.Split(url, ",")[1] 99 | } 100 | img_data_list = append(img_data_list, url) // 收集图片数据 101 | } 102 | } 103 | } 104 | } 105 | } 106 | } 107 | } 108 | } 109 | fmt.Println(prompt.String()) // 输出最终构造的内容 110 | fmt.Println("img_data_list_length:", len(img_data_list)) // 输出图片数据列表长度 111 | var rootPrompt strings.Builder 112 | rootPrompt.WriteString(prompt.String()) 113 | // 切号重试机制 114 | var pplxClient *core.Client 115 | index := config.Sr.NextIndex() 116 | for i := 0; i < config.ConfigInstance.RetryCount; i++ { 117 | if i > 0 { 118 | prompt.Reset() 119 | prompt.WriteString(rootPrompt.String()) 120 | } 121 | index = (index + 1) % len(config.ConfigInstance.Sessions) 122 | session, err := config.ConfigInstance.GetSessionForModel(index) 123 | logger.Info(fmt.Sprintf("Using session for model %s: %s", model, session.SessionKey)) 124 | if err != nil { 125 | logger.Error(fmt.Sprintf("Failed to get session for model %s: %v", model, err)) 126 | logger.Info("Retrying another session") 127 | continue 128 | } 129 | // Initialize the Claude client 130 | pplxClient = core.NewClient(session.SessionKey, config.ConfigInstance.Proxy, model, openSearch) 131 | if len(img_data_list) > 0 { 132 | err := pplxClient.UploadImage(img_data_list) 133 | if err != nil { 134 | logger.Error(fmt.Sprintf("Failed to upload file: %v", err)) 135 | logger.Info("Retrying another session") 136 | 137 | continue 138 | } 139 | } 140 | if prompt.Len() > config.ConfigInstance.MaxChatHistoryLength { 141 | err := pplxClient.UploadText(prompt.String()) 142 | if err != nil { 143 | logger.Error(fmt.Sprintf("Failed to upload text: %v", err)) 144 | logger.Info("Retrying another session") 145 | 146 | continue 147 | } 148 | prompt.Reset() 149 | prompt.WriteString(config.ConfigInstance.PromptForFile) 150 | } 151 | if _, err := pplxClient.SendMessage(prompt.String(), req.Stream, config.ConfigInstance.IsIncognito, c); err != nil { 152 | logger.Error(fmt.Sprintf("Failed to send message: %v", err)) 153 | logger.Info("Retrying another session") 154 | 155 | continue // Retry on error 156 | } 157 | 158 | return 159 | 160 | } 161 | logger.Error("Failed for all retries") 162 | c.JSON(http.StatusInternalServerError, ErrorResponse{ 163 | Error: "Failed to process request after multiple attempts"}) 164 | } 165 | 166 | func MoudlesHandler(c *gin.Context) { 167 | c.JSON(http.StatusOK, gin.H{ 168 | "data": config.ResponseModles, 169 | }) 170 | } 171 | -------------------------------------------------------------------------------- /utils/imageShow.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "fmt" 4 | 5 | func ImageShow(index int, modelName, url string) string { 6 | index++ 7 | return fmt.Sprintf("![%s](%s)", modelName, url) 8 | } 9 | -------------------------------------------------------------------------------- /utils/random.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "math/rand" 4 | 5 | func RandomString(length int) string { 6 | const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 7 | result := make([]byte, length) 8 | for i := range result { 9 | result[i] = charset[rand.Intn(len(charset))] 10 | } 11 | return string(result) 12 | } 13 | -------------------------------------------------------------------------------- /utils/role.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "pplx2api/config" 5 | ) 6 | 7 | // **获取角色前缀** 8 | func GetRolePrefix(role string) string { 9 | if config.ConfigInstance.NoRolePrefix { 10 | return "" 11 | } 12 | switch role { 13 | case "system": 14 | return "System: " 15 | case "user": 16 | return "Human: " 17 | case "assistant": 18 | return "Assistant: " 19 | default: 20 | return "Unknown: " 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /utils/searchShow.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "pplx2api/config" 6 | ) 7 | 8 | func searchShowDetails(index int, title, url, snippet string) string { 9 | return fmt.Sprintf("
\n[%d] %s\n\n%s\n\n[Link](%s)\n\n
", index, title, snippet, url) 10 | } 11 | 12 | func searchShowCompatible(index int, title, url, snippet string) string { 13 | return fmt.Sprintf("[%d] [%s](%s):\n%s\n", index, title, url, snippet) 14 | } 15 | 16 | func SearchShow(index int, title, url, snippet string) string { 17 | index++ 18 | if len([]rune(snippet)) > 150 { 19 | runeSnippet := []rune(snippet) 20 | snippet = fmt.Sprintf("%s ……", string(runeSnippet[:150])) 21 | } 22 | if config.ConfigInstance.SearchResultCompatible { 23 | return searchShowCompatible(index, title, url, snippet) 24 | } 25 | return searchShowDetails(index, title, url, snippet) 26 | } 27 | --------------------------------------------------------------------------------