├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md ├── close_issue.py └── workflows │ ├── CloseIssue.yml │ ├── docker-image-amd64.yml │ ├── docker-image-arm64.yml │ ├── github-pages.yml │ ├── linux-release.yml │ ├── macos-release.yml │ └── windows-release.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.en.md ├── README.md ├── VERSION ├── common ├── config │ └── config.go ├── constants.go ├── env │ └── helper.go ├── init.go ├── logger.go ├── myerr │ ├── discordunauthorizederror.go │ ├── modelnotfounderror.go │ └── myerror.go ├── rate-limit.go ├── snowflakeid.go ├── tiktoken.go ├── utils.go ├── validate.go └── verification.go ├── controller ├── channel.go ├── chat.go └── thread.go ├── discord ├── channel.go ├── discord.go ├── processmessage.go └── sendmessage.go ├── docker-compose.yml ├── docs ├── docs.go ├── img.png ├── img2.png ├── img3.png ├── img5.png ├── swagger.json └── swagger.yaml ├── go.mod ├── go.sum ├── main.go ├── middleware ├── auth.go ├── cors.go ├── logger.go ├── rate-limit.go └── request-id.go ├── model ├── bot.go ├── channel.go ├── chat.go ├── openai.go ├── reply.go └── thread.go ├── router └── api-router.go └── telegram └── bot.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: [ 'https://cdp-docs.pages.dev/page/sponsors.html' ] 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 报告问题 3 | about: 使用简练详细的语言描述你遇到的问题 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **温馨提示: 未`star`项目会被自动关闭issue哦!** 11 | 12 | **例行检查** 13 | 14 | + [ ] 我已确认目前没有类似 issue 15 | + [ ] 我已确认我已升级到最新版本 16 | + [ ] 我理解并愿意跟进此 issue,协助测试和提供反馈 17 | + [ ] 我理解并认可上述内容,并理解项目维护者精力有限,不遵循规则的 issue 可能会被无视或直接关闭 18 | 19 | **问题描述** 20 | 21 | **复现步骤** 22 | 23 | **预期结果** 24 | 25 | **相关截图** 26 | 如果没有的话,请删除此节。 -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: 赞赏支持 4 | # url: 5 | about: 请作者喝杯咖啡,以激励作者持续开发 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 功能请求 3 | about: 使用简练详细的语言描述希望加入的新功能 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **温馨提示: 未`star`项目会被自动关闭issue哦!** 11 | 12 | **例行检查** 13 | 14 | + [ ] 我已确认目前没有类似 issue 15 | + [ ] 我已确认我已升级到最新版本 16 | + [ ] 我理解并愿意跟进此 issue,协助测试和提供反馈 17 | + [ ] 我理解并认可上述内容,并理解项目维护者精力有限,不遵循规则的 issue 可能会被无视或直接关闭 18 | 19 | **功能描述** 20 | 21 | **应用场景** 22 | -------------------------------------------------------------------------------- /.github/close_issue.py: -------------------------------------------------------------------------------- 1 | import os 2 | import requests 3 | 4 | issue_labels = ['no respect'] 5 | github_repo = 'deanxv/coze-discord-proxy' 6 | github_token = os.getenv("GITHUB_TOKEN") 7 | headers = { 8 | 'Authorization': 'Bearer ' + github_token, 9 | 'Accept': 'application/vnd.github+json', 10 | 'X-GitHub-Api-Version': '2022-11-28', 11 | } 12 | 13 | def get_stargazers(repo): 14 | page = 1 15 | _stargazers = {} 16 | while True: 17 | queries = { 18 | 'per_page': 100, 19 | 'page': page, 20 | } 21 | url = 'https://api.github.com/repos/{}/stargazers?'.format(repo) 22 | 23 | resp = requests.get(url, headers=headers, params=queries) 24 | if resp.status_code != 200: 25 | raise Exception('Error get stargazers: ' + resp.text) 26 | 27 | data = resp.json() 28 | if not data: 29 | break 30 | 31 | for stargazer in data: 32 | _stargazers[stargazer['login']] = True 33 | page += 1 34 | 35 | print('list stargazers done, total: ' + str(len(_stargazers))) 36 | return _stargazers 37 | 38 | 39 | def get_issues(repo): 40 | page = 1 41 | _issues = [] 42 | while True: 43 | queries = { 44 | 'state': 'open', 45 | 'sort': 'created', 46 | 'direction': 'desc', 47 | 'per_page': 100, 48 | 'page': page, 49 | } 50 | url = 'https://api.github.com/repos/{}/issues?'.format(repo) 51 | 52 | resp = requests.get(url, headers=headers, params=queries) 53 | if resp.status_code != 200: 54 | raise Exception('Error get issues: ' + resp.text) 55 | 56 | data = resp.json() 57 | if not data: 58 | break 59 | 60 | _issues += data 61 | page += 1 62 | 63 | print('list issues done, total: ' + str(len(_issues))) 64 | return _issues 65 | 66 | 67 | def close_issue(repo, issue_number): 68 | url = 'https://api.github.com/repos/{}/issues/{}'.format(repo, issue_number) 69 | data = { 70 | 'state': 'closed', 71 | 'state_reason': 'not_planned', 72 | 'labels': issue_labels, 73 | } 74 | resp = requests.patch(url, headers=headers, json=data) 75 | if resp.status_code != 200: 76 | raise Exception('Error close issue: ' + resp.text) 77 | 78 | print('issue: {} closed'.format(issue_number)) 79 | 80 | 81 | def lock_issue(repo, issue_number): 82 | url = 'https://api.github.com/repos/{}/issues/{}/lock'.format(repo, issue_number) 83 | data = { 84 | 'lock_reason': 'spam', 85 | } 86 | resp = requests.put(url, headers=headers, json=data) 87 | if resp.status_code != 204: 88 | raise Exception('Error lock issue: ' + resp.text) 89 | 90 | print('issue: {} locked'.format(issue_number)) 91 | 92 | 93 | if '__main__' == __name__: 94 | stargazers = get_stargazers(github_repo) 95 | 96 | issues = get_issues(github_repo) 97 | for issue in issues: 98 | login = issue['user']['login'] 99 | if login not in stargazers: 100 | print('issue: {}, login: {} not in stargazers'.format(issue['number'], login)) 101 | close_issue(github_repo, issue['number']) 102 | lock_issue(github_repo, issue['number']) 103 | 104 | print('done') -------------------------------------------------------------------------------- /.github/workflows/CloseIssue.yml: -------------------------------------------------------------------------------- 1 | name: CloseIssue 2 | 3 | on: 4 | workflow_dispatch: 5 | issues: 6 | types: [ opened ] 7 | 8 | jobs: 9 | run-python-script: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: actions/setup-python@v4 15 | with: 16 | python-version: "3.10" 17 | 18 | - name: Install Dependencies 19 | run: pip install requests 20 | 21 | - name: Run close_issue.py Script 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | run: python .github/close_issue.py -------------------------------------------------------------------------------- /.github/workflows/docker-image-amd64.yml: -------------------------------------------------------------------------------- 1 | name: Publish Docker image (amd64) 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | workflow_dispatch: 8 | inputs: 9 | name: 10 | description: 'reason' 11 | required: false 12 | jobs: 13 | push_to_registries: 14 | name: Push Docker image to multiple registries 15 | runs-on: ubuntu-latest 16 | environment: github-pages 17 | permissions: 18 | packages: write 19 | contents: read 20 | steps: 21 | - name: Check out the repo 22 | uses: actions/checkout@v3 23 | 24 | - name: Save version info 25 | run: | 26 | git describe --tags > VERSION 27 | 28 | - name: Log in to Docker Hub 29 | uses: docker/login-action@v2 30 | with: 31 | username: ${{ secrets.DOCKERHUB_USERNAME }} 32 | password: ${{ secrets.DOCKERHUB_TOKEN }} 33 | 34 | - name: Log in to the Container registry 35 | uses: docker/login-action@v2 36 | with: 37 | registry: ghcr.io 38 | username: ${{ github.actor }} 39 | password: ${{ secrets.GITHUB_TOKEN }} 40 | 41 | - name: Extract metadata (tags, labels) for Docker 42 | id: meta 43 | uses: docker/metadata-action@v4 44 | with: 45 | images: | 46 | deanxv/coze-discord-proxy 47 | ghcr.io/${{ github.repository }} 48 | 49 | - name: Build and push Docker images 50 | uses: docker/build-push-action@v3 51 | with: 52 | context: . 53 | push: true 54 | tags: ${{ steps.meta.outputs.tags }} 55 | labels: ${{ steps.meta.outputs.labels }} -------------------------------------------------------------------------------- /.github/workflows/docker-image-arm64.yml: -------------------------------------------------------------------------------- 1 | name: Publish Docker image (arm64) 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | - '!*-alpha*' 8 | workflow_dispatch: 9 | inputs: 10 | name: 11 | description: 'reason' 12 | required: false 13 | jobs: 14 | push_to_registries: 15 | name: Push Docker image to multiple registries 16 | runs-on: ubuntu-latest 17 | environment: github-pages 18 | permissions: 19 | packages: write 20 | contents: read 21 | steps: 22 | - name: Check out the repo 23 | uses: actions/checkout@v3 24 | 25 | - name: Save version info 26 | run: | 27 | git describe --tags > VERSION 28 | 29 | - name: Set up QEMU 30 | uses: docker/setup-qemu-action@v2 31 | 32 | - name: Set up Docker Buildx 33 | uses: docker/setup-buildx-action@v2 34 | 35 | - name: Log in to Docker Hub 36 | uses: docker/login-action@v2 37 | with: 38 | username: ${{ secrets.DOCKERHUB_USERNAME }} 39 | password: ${{ secrets.DOCKERHUB_TOKEN }} 40 | 41 | - name: Log in to the Container registry 42 | uses: docker/login-action@v2 43 | with: 44 | registry: ghcr.io 45 | username: ${{ github.actor }} 46 | password: ${{ secrets.GITHUB_TOKEN }} 47 | 48 | - name: Extract metadata (tags, labels) for Docker 49 | id: meta 50 | uses: docker/metadata-action@v4 51 | with: 52 | images: | 53 | deanxv/coze-discord-proxy 54 | ghcr.io/${{ github.repository }} 55 | 56 | - name: Build and push Docker images 57 | uses: docker/build-push-action@v3 58 | with: 59 | context: . 60 | platforms: linux/amd64,linux/arm64 61 | push: true 62 | tags: ${{ steps.meta.outputs.tags }} 63 | labels: ${{ steps.meta.outputs.labels }} -------------------------------------------------------------------------------- /.github/workflows/github-pages.yml: -------------------------------------------------------------------------------- 1 | name: Build GitHub Pages 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | name: 6 | description: 'Reason' 7 | required: false 8 | jobs: 9 | build-and-deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 🛎️ 13 | uses: actions/checkout@v2 # If you're using actions/checkout@v2 you must set persist-credentials to false in most cases for the deployment to work correctly. 14 | with: 15 | persist-credentials: false 16 | - name: Install and Build 🔧 # This example project is built using npm and outputs the result to the 'build' folder. Replace with the commands required to build your project, or remove this step entirely if your site is pre-built. 17 | env: 18 | CI: "" 19 | run: | 20 | cd web 21 | npm install 22 | npm run build 23 | 24 | - name: Deploy 🚀 25 | uses: JamesIves/github-pages-deploy-action@releases/v3 26 | with: 27 | ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }} 28 | BRANCH: gh-pages # The branch the action should deploy to. 29 | FOLDER: web/build # The folder the action should deploy. -------------------------------------------------------------------------------- /.github/workflows/linux-release.yml: -------------------------------------------------------------------------------- 1 | name: Linux Release 2 | permissions: 3 | contents: write 4 | 5 | on: 6 | push: 7 | tags: 8 | - '*' 9 | - '!*-alpha*' 10 | jobs: 11 | release: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | with: 17 | fetch-depth: 0 18 | - uses: actions/setup-node@v3 19 | with: 20 | node-version: 16 21 | - name: Set up Go 22 | uses: actions/setup-go@v3 23 | with: 24 | go-version: '>=1.18.0' 25 | - name: Build Backend (amd64) 26 | run: | 27 | go mod download 28 | go build -ldflags "-s -w -X 'coze-discord-proxy/common.Version=$(git describe --tags)' -extldflags '-static'" -o coze-discord-proxy 29 | 30 | - name: Build Backend (arm64) 31 | run: | 32 | sudo apt-get update 33 | sudo apt-get install gcc-aarch64-linux-gnu 34 | CC=aarch64-linux-gnu-gcc CGO_ENABLED=1 GOOS=linux GOARCH=arm64 go build -ldflags "-s -w -X 'coze-discord-proxy/common.Version=$(git describe --tags)' -extldflags '-static'" -o coze-discord-proxy-arm64 35 | 36 | - name: Release 37 | uses: softprops/action-gh-release@v1 38 | if: startsWith(github.ref, 'refs/tags/') 39 | with: 40 | files: | 41 | coze-discord-proxy 42 | coze-discord-proxy-arm64 43 | draft: false 44 | generate_release_notes: true 45 | env: 46 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/macos-release.yml: -------------------------------------------------------------------------------- 1 | name: macOS Release 2 | permissions: 3 | contents: write 4 | 5 | on: 6 | push: 7 | tags: 8 | - '*' 9 | - '!*-alpha*' 10 | jobs: 11 | release: 12 | runs-on: macos-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | with: 17 | fetch-depth: 0 18 | - uses: actions/setup-node@v3 19 | with: 20 | node-version: 16 21 | - name: Set up Go 22 | uses: actions/setup-go@v3 23 | with: 24 | go-version: '>=1.18.0' 25 | - name: Build Backend 26 | run: | 27 | go mod download 28 | go build -ldflags "-X 'coze-discord-proxy/common.Version=$(git describe --tags)'" -o coze-discord-proxy-macos 29 | - name: Release 30 | uses: softprops/action-gh-release@v1 31 | if: startsWith(github.ref, 'refs/tags/') 32 | with: 33 | files: coze-discord-proxy-macos 34 | draft: false 35 | generate_release_notes: true 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 38 | -------------------------------------------------------------------------------- /.github/workflows/windows-release.yml: -------------------------------------------------------------------------------- 1 | name: Windows Release 2 | permissions: 3 | contents: write 4 | 5 | on: 6 | push: 7 | tags: 8 | - '*' 9 | - '!*-alpha*' 10 | jobs: 11 | release: 12 | runs-on: windows-latest 13 | defaults: 14 | run: 15 | shell: bash 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v3 19 | with: 20 | fetch-depth: 0 21 | - uses: actions/setup-node@v3 22 | with: 23 | node-version: 16 24 | - name: Set up Go 25 | uses: actions/setup-go@v3 26 | with: 27 | go-version: '>=1.18.0' 28 | - name: Build Backend 29 | run: | 30 | go mod download 31 | go build -ldflags "-s -w -X 'coze-discord-proxy/common.Version=$(git describe --tags)'" -o coze-discord-proxy.exe 32 | - name: Release 33 | uses: softprops/action-gh-release@v1 34 | if: startsWith(github.ref, 'refs/tags/') 35 | with: 36 | files: coze-discord-proxy.exe 37 | draft: false 38 | generate_release_notes: true 39 | env: 40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | upload 4 | *.exe 5 | *.db 6 | build -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # 使用 Golang 镜像作为构建阶段 2 | FROM golang AS builder 3 | 4 | # 设置环境变量 5 | ENV GO111MODULE=on \ 6 | CGO_ENABLED=0 \ 7 | GOOS=linux 8 | 9 | # 设置工作目录 10 | WORKDIR /build 11 | 12 | # 复制 go.mod 和 go.sum 文件,先下载依赖 13 | COPY go.mod go.sum ./ 14 | #ENV GOPROXY=https://goproxy.cn,direct 15 | RUN go mod download 16 | 17 | # 复制整个项目并构建可执行文件 18 | COPY . . 19 | RUN go build -o /coze-discord-proxy 20 | 21 | # 使用 Alpine 镜像作为最终镜像 22 | FROM alpine 23 | 24 | # 安装基本的运行时依赖 25 | RUN apk --no-cache add ca-certificates tzdata 26 | 27 | # 从构建阶段复制可执行文件 28 | COPY --from=builder /coze-discord-proxy . 29 | 30 | # 暴露端口 31 | EXPOSE 7077 32 | # 工作目录 33 | WORKDIR /app/coze-discord-proxy/data 34 | # 设置入口命令 35 | ENTRYPOINT ["/coze-discord-proxy"] 36 | -------------------------------------------------------------------------------- /README.en.md: -------------------------------------------------------------------------------- 1 |

2 | English | 中文 3 |

4 | 5 |
6 | 7 | # coze-discord-proxy 8 | 9 | deanxv%2Fcoze-discord-proxy | Trendshift 10 | 11 | _Proxy `Discord` conversations for `Coze-Bot`, enabling API requests to the GPT4 model with features like conversation, text-to-image, image-to-text, and knowledge base retrieval._ 12 | 13 | _If you find this interesting, don't forget to give it a 🌟_ 14 | 15 | 📄CDP Project Documentation Site (Must-read tutorial) 16 | 17 | 🐞CDP Project - Discussion Group (Discussion) 18 | 19 | 📢CDP Project - Notification Channel (Notifications) 20 | 21 |
22 | 23 | ## Features (The project's features are now stable, updates will not be frequent, feel free to raise an issue if you find bugs!) 24 | 25 | - [x] Perfectly compatible with `NextChat`, `one-api`, `LobeChat` and other conversation panels. 26 | - [x] Perfect support for conversation isolation. 27 | - [x] Conversation interface supports streaming responses. 28 | - [x] Supports creating `discord` categories/channels/threads. 29 | - [x] Supports conversation interface aligned with `openai` (`v1/chat/completions`) (also supports `dall-e-3` text-to-image) (supports specifying `discord-channel`). 30 | - [x] Supports image-to-text/image-to-image/file-to-text interfaces aligned with `openai` (`v1/chat/completions`) (following the `GPT4V` interface request format [ supports `url` or `base64` ])(supports specifying `discord-channel`). 31 | - [x] Supports `dall-e-3` text-to-image interface aligned with `openai` (`v1/images/generations`). 32 | - [x] Supports daily `9 AM` scheduled tasks to keep the bot active. 33 | - [x] Supports configuring multiple discord user `Authorization` (environment variable `USER_AUTHORIZATION`) for request load balancing (**currently each discord user has a 24-hour limit on coze-bot calls, configure multiple users to stack request counts and balance load**). 34 | - [x] Supports configuring multiple coze bots for response load balancing (specified through `PROXY_SECRET`/`model`), see [Advanced Configuration](#advanced-configuration) for details. 35 | 36 | ### API Documentation: 37 | 38 | `http://:/swagger/index.html` 39 | 40 | 41 | 42 | ### Example: 43 | 44 | 45 | 46 | ## How to Use 47 | 48 | 1. Open [Discord's official website](https://discord.com/app), log in, click settings-advanced settings-developer mode-turn on. 49 | 2. Create a discord server, right-click this server to select `Copy Server ID (GUILD_ID)` and record it, create a default channel in this server, right-click this channel to select `Copy Channel ID (CHANNEL_ID)` and record it. 50 | 3. Open [Discord Developer Portal](https://discord.com/developers/applications) and log in. 51 | 4. Create a new application-Bot, i.e., `COZE-BOT`, and record its unique `token` and `id (COZE_BOT_ID)`, this bot will be managed by coze. 52 | 5. Create a new application-Bot, i.e., `CDP-BOT`, and record its unique `token (BOT_TOKEN)`, this bot will listen for discord messages. 53 | 6. Grant corresponding permissions (`Administrator`) to both bots and invite them to the created discord server (the process is not described here). 54 | 7. Open [Discord's official website](https://discord.com/app), enter the server, press F12 to open developer tools, send a message in any channel, find the request `https://discord.com/api/v9/channels/1206*******703/messages` in developer tools-`Network`, get `Authorization (USER_AUTHORIZATION)` from the header of this interface and record it. 55 | 8. Open [Coze's official website](https://www.coze.com), create and configure a bot (note `Auto-Suggestion` should be `Default/on` (usually no need to change)). 56 | 9. After configuration, choose to publish to discord, fill in the `token` of `COZE-BOT`, after publishing, you can see `COZE-BOT` online and can be used with @ in the discord server. 57 | 10. Start configuring [environment variables](#environment-variables) and [deploy](#deployment) this project using the recorded parameters. 58 | 11. Visit the API documentation address, and you can start debugging or integrating other projects. 59 | 60 | ## How to Integrate with NextChat 61 | 62 | Fill in the interface address (ip:port/domain) and API-Key (`PROXY_SECRET`), other fields are optional. 63 | 64 | > If you haven't set up a NextChat panel yourself, here's one already set up that you can use: [NextChat](https://ci.goeast.io/) 65 | 66 | 67 | 68 | ## How to Integrate with one-api 69 | 70 | Fill in `BaseURL` (ip:port/domain) and key (`PROXY_SECRET`), other fields are optional. 71 | 72 | 73 | 74 | ## Deployment 75 | 76 | ### Deploying with Docker-Compose (All In One) 77 | 78 | ```shell 79 | docker-compose pull && docker-compose up -d 80 | ``` 81 | 82 | #### docker-compose.yml 83 | 84 | ```docker 85 | version: '3.4' 86 | 87 | services: 88 | coze-discord-proxy: 89 | image: deanxv/coze-discord-proxy:latest 90 | container_name: coze-discord-proxy 91 | restart: always 92 | ports: 93 | - "7077:7077" 94 | volumes: 95 | - ./data:/app/coze-discord-proxy/data 96 | environment: 97 | - USER_AUTHORIZATION=MTA5OTg5N************aXUBHVI # Must modify to your discord user's authorization key (multiple keys separated by commas) 98 | - BOT_TOKEN=MTE5OT************UrUWNbG63w # Must modify to the listening bot's token 99 | - GUILD_ID=11************96 # Must modify to the server ID where both bots are located 100 | - COZE_BOT_ID=11************97 # Must modify to the bot ID managed by coze 101 | - CHANNEL_ID=11************94 # [Optional] Default channel - (currently this parameter is only used to keep the bot active) 102 | - PROXY_SECRET=123456 # [Optional] API key - modify this line to the value used for request header verification (multiple keys separated by commas) 103 | - TZ=Asia/Shanghai 104 | ``` 105 | 106 | ### Deploying with Docker 107 | 108 | ```docker 109 | docker run --name coze-discord-proxy -d --restart always \ 110 | -p 7077:7077 \ 111 | -v $(pwd)/data:/app/coze-discord-proxy/data \ 112 | -e USER_AUTHORIZATION="MTA5OTg5N************uIfytxUgJfmaXUBHVI" \ 113 | -e BOT_TOKEN="MTE5OTk2************rUWNbG63w" \ 114 | -e GUILD_ID="11************96" \ 115 | -e COZE_BOT_ID="11************97" \ 116 | -e PROXY_SECRET="123456" \ 117 | -e CHANNEL_ID="11************24" \ 118 | -e TZ=Asia/Shanghai \ 119 | deanxv/coze-discord-proxy 120 | ``` 121 | 122 | Modify `USER_AUTHORIZATION`, `BOT_TOKEN`, `GUILD_ID`, `COZE_BOT_ID`, `PROXY_SECRET`, `CHANNEL_ID` to your own values. 123 | 124 | If the above image cannot be pulled, try using the GitHub Docker image by replacing `deanxv/coze-discord-proxy` with `ghcr.io/deanxv/coze-discord-proxy`. 125 | 126 | ### Deploying to a Third-Party Platform 127 | 128 |
129 | Deploy to Zeabur 130 |
131 | 132 | > Zeabur's servers are located abroad, automatically solving network issues, and the free tier is sufficient for personal use. 133 | 134 | Click to deploy: 135 | 136 | [![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/templates/GMU8C8?referralCode=deanxv) 137 | 138 | **After one-click deployment, the variables `USER_AUTHORIZATION`, `BOT_TOKEN`, `GUILD_ID`, `COZE_BOT_ID`, `PROXY_SECRET`, `CHANNEL_ID` must also be replaced!** 139 | 140 | Or manually deploy: 141 | 142 | 1. First **fork** a copy of the code. 143 | 2. Enter [Zeabur](https://zeabur.com?referralCode=deanxv), log in with GitHub, go to the console. 144 | 3. In Service -> Add Service, choose Git (authorize first if it's your first time), select the repository you forked. 145 | 4. Deployment will automatically start, cancel it first. 146 | 5. Add environment variables 147 | 148 | `USER_AUTHORIZATION:MTA5OTg5N************uIfytxUgJfmaXUBHVI` Authorization key for discord users initiating messages (separated by commas) 149 | 150 | `BOT_TOKEN:MTE5OTk************WNbG63w` Token for the bot listening to messages 151 | 152 | `GUILD_ID:11************96` Server ID where both bots are located 153 | 154 | `COZE_BOT_ID:11************97` Bot ID managed by coze 155 | 156 | `CHANNEL_ID:11************24` # [Optional] Default channel - (currently this parameter is only used to keep the bot active) 157 | 158 | `PROXY_SECRET:123456` [Optional] API key - modify this line to the value used for request header verification (separated by commas) (similar to the openai-API-KEY) 159 | 160 | Save. 161 | 162 | 6. Choose Redeploy. 163 | 164 |
165 |
166 | 167 |
168 | Deploy to Render 169 |
170 | 171 | > Render provides a free tier, and linking a card can further increase the limit. 172 | 173 | Render can directly deploy Docker images without needing to fork the repository: [Render](https://dashboard.render.com) 174 | 175 |
176 |
177 | 178 | ## Configuration 179 | 180 | ### Environment Variables 181 | 182 | 1. `USER_AUTHORIZATION=MTA5OTg5N************uIfytxUgJfmaXUBHVI` Authorization key for discord users initiating messages (separated by commas) 183 | 2. `BOT_TOKEN=MTE5OTk2************rUWNbG63w` Token for the bot listening to messages 184 | 3. `GUILD_ID=11************96` Server ID where all bots are located 185 | 4. `COZE_BOT_ID=11************97` Bot ID managed by coze 186 | 5. `PORT=7077` [Optional] Port, default is 7077 187 | 6. `SWAGGER_ENABLE=1` [Optional] Enable Swagger API documentation [0: No; 1: Yes] (default is 1) 188 | 7. `ONLY_OPENAI_API=0` [Optional] Expose only interfaces aligned with openai [0: No; 1: Yes] (default is 0) 189 | 8. `CHANNEL_ID=11************24` [Optional] Default channel - (currently this parameter is only used to keep the bot active) 190 | 9. `PROXY_SECRET=123456` [Optional] API key - modify this line to the value used for request header verification (separated by commas) (similar to the openai-API-KEY), **recommended to use this environment variable** 191 | 10. `DEFAULT_CHANNEL_ENABLE=0` [Optional] Enable default channel [0: No; 1: Yes] (default is 0) If enabled, each conversation will occur in the default channel, **session isolation will be ineffective**, **not recommended to use this environment variable** 192 | 11. `ALL_DIALOG_RECORD_ENABLE=1` [Optional] Enable full context [0: No; 1: Yes] (default is 1) If disabled, each conversation will only send the last `content` in `messages` where `role` is `user`, **not recommended to use this environment variable** 193 | 12. `CHANNEL_AUTO_DEL_TIME=5` [Optional] Channel auto-delete time (seconds) This parameter is for automatically deleting the channel after each conversation (default is 5s), if set to 0 then it will not delete, **not recommended to use this environment variable** 194 | 13. `COZE_BOT_STAY_ACTIVE_ENABLE=1` [Optional] Enable daily `9 AM` task to keep coze-bot active [0: No; 1: Yes] (default is 1), **not recommended to use this environment variable** 195 | 14. `REQUEST_OUT_TIME=60` [Optional] Non-stream response timeout for conversation interface, **not recommended to use this environment variable** 196 | 15. `STREAM_REQUEST_OUT_TIME=60` [Optional] Stream response timeout for each stream return in conversation interface, **not recommended to use this environment variable** 197 | 16. `REQUEST_RATE_LIMIT=60` [Optional] Request rate limit per minute per IP, default: 60 requests/min 198 | 17. `USER_AGENT=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36` [Optional] Discord user agent, using your own might help prevent being banned, if not set, defaults to the author's, **recommended to use this environment variable** 199 | 18. `NOTIFY_TELEGRAM_BOT_TOKEN=6232***********Niz9c` [Optional] Token for a Telegram bot used for notifications (Notification events: 1. No available `user_authorization`; 2. `BOT_TOKEN` related BOT triggers risk control) 200 | 19. `NOTIFY_TELEGRAM_USER_ID=10******35` [Optional] The `Telegram-Bot` associated with `NOTIFY_TELEGRAM_BOT_TOKEN` will push to the `Telegram-User` associated with this variable (**`NOTIFY_TELEGRAM_BOT_TOKEN` must not be empty if this variable is used**) 201 | 20. `PROXY_URL=http://127.0.0.1:10801` [Optional] Proxy (supports http only) 202 | 203 | ## Advanced Configuration 204 | 205 | ### Configuring Multiple Bots 206 | 207 | 1. Before deployment, create a `data/config/bot_config.json` file in the same directory as the `docker`/`docker-compose` deployment 208 | 2. Write the `json` file, `bot_config.json` format as follows 209 | 210 | ```shell 211 | [ 212 | { 213 | "proxySecret": "123", // API request key (PROXY_SECRET) (Note: this key must exist in the environment variable PROXY_SECRET for this Bot to be matched!) 214 | "cozeBotId": "12***************31", // Bot ID managed by coze 215 | "model": ["gpt-3.5","gpt-3.5-16k"], // Model names (array format) (if the model in the request does not match any in this json, an exception will be thrown) 216 | "channelId": "12***************56" // [Optional] Discord channel ID (the bot must be in the server where this channel is located) (currently this parameter is only used to keep the bot active) 217 | }, 218 | { 219 | "proxySecret": "456", 220 | "cozeBotId": "12***************64", 221 | "model": ["gpt-4","gpt-4-16k"], 222 | "channelId": "12***************78" 223 | }, 224 | { 225 | "proxySecret": "789", 226 | "cozeBotId": "12***************12", 227 | "model": ["dall-e-3"], 228 | "channelId": "12***************24" 229 | } 230 | ] 231 | ``` 232 | 233 | 3. Restart the service 234 | 235 | > When this json configuration is present, the bot will be matched through the [request key] carried in the request header and the [`model`] in the request body. 236 | > If multiple matches are found, one will be randomly selected. The configuration is very flexible and can be adjusted according to your needs. 237 | 238 | For services deployed on third-party platforms (such as `zeabur`) that need [configuring multiple bots], please refer to [issue#30](https://github.com/deanxv/coze-discord-proxy/issues/30) 239 | 240 | ## Limitations 241 | 242 | Current details of coze's free and paid subscriptions: https://www.coze.com/docs/guides/subscription?_lang=en 243 | 244 | You can configure multiple discord users `Authorization` (refer to [Environment Variables](#environment-variables) `USER_AUTHORIZATION`) or [configure multiple bots](#configuring-multiple-bots) to stack request counts and balance load. 245 | 246 | ## Q&A 247 | 248 | Q: How should I configure for high concurrency? 249 | 250 | A: First, [configure multiple bots](#configuring-multiple-bots) to serve as response bots. Secondly, prepare multiple discord accounts to serve as request load and invite them into the same server, obtain the `Authorization` for each account, separate them with commas, and configure them in the environment variable `USER_AUTHORIZATION`. Each request will then pick one discord account to initiate the conversation, effectively achieving load balancing. 251 | 252 | ## ⭐ Star History 253 | 254 | [![Star History Chart](https://api.star-history.com/svg?repos=deanxv/coze-discord-proxy&type=Date)](https://star-history.com/#deanxv/coze-discord-proxy&Date) 255 | 256 | ## Related 257 | 258 | [GPT-Content-Audit](https://github.com/deanxv/gpt-content-audit): An aggregation of Openai, Alibaba Cloud, Baidu Intelligent Cloud, Qiniu Cloud, and other open platforms, providing content audit services aligned with `openai` request formats. 259 | 260 | ## Others 261 | 262 | **Open source is not easy, if you refer to this project or base your project on it, could you please mention this project in your project documentation? Thank you!** 263 | 264 | Java: https://github.com/oddfar/coze-discord (Currently unavailable) 265 | 266 | ## References 267 | 268 | Coze Official Website: https://www.coze.com 269 | 270 | Discord Development Address: https://discord.com/developers/applications 271 | 272 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 中文 | English 3 |

4 | 5 |
6 | 7 | # coze-discord-proxy 8 | 9 | deanxv%2Fcoze-discord-proxy | Trendshift 10 | 11 | _代理`Discord`对话`Coze-Bot`,实现以API形式请求GPT4模型,提供对话、文生图、图生文、知识库检索等功能_ 12 | 13 | _觉得有点意思的话 别忘了点个🌟_ 14 | 15 | 📄CDP项目文档站(必看教程) 16 | 17 | 🐞CDP项目-交流群(交流) 18 |
(群内提供公益API、AI机器人) 19 | 20 | 📢CDP项目-通知频道(通知) 21 | 22 | 23 |
24 | 25 | ## 功能(目前项目功能已趋于稳定,迭代不会很频繁,有bug可以提issue哦!) 26 | 27 | - [x] 完美适配`NextChat`,`one-api`,`LobeChat`等对话面板。 28 | - [x] 完美支持对话隔离。 29 | - [x] 对话接口支持流式返回。 30 | - [x] 支持创建 `discord`分类/频道/线程。 31 | - [x] 支持和`openai`对齐的对话接口(`v1/chat/completions`)(也支持`dall-e-3`文生图)(支持指定`discord-channel`)。 32 | - [x] 支持和`openai`对齐的图生文/图改图/文件生文接口(`v1/chat/completions`)(按照`GPT4V`接口的请求格式 [ 支持`url`或`base64` ])(支持指定`discord-channel`)。 33 | - [x] 支持和`openai`对齐的`dall-e-3`文生图接口(`v1/images/generations`)。 34 | - [x] 支持每日`9`点定时任务自动活跃机器人。 35 | - [x] 支持配置多discord用户`Authorization`(环境变量`USER_AUTHORIZATION`)作请求负载均衡(**目前每个discord用户调用coze-bot在24h内有次数[限制](#限制),可配置多用户来实现叠加请求次数及请求负载均衡**)。 36 | - [x] 支持配置多coze机器人作响应负载均衡 (通过`PROXY_SECRET`/`model`指定) 详细请看[进阶配置](#进阶配置)。 37 | 38 | ### 接口文档: 39 | 40 | `http://:/swagger/index.html` 41 | 42 | 43 | 44 | ### 示例: 45 | 46 | 47 | 48 | ## 如何使用 49 | 50 | 1. 打开 [discord官网](https://discord.com/app) ,登陆后点击设置-高级设置-开发者模式-打开。 51 | 2. 创建discord服务器,右键点击此服务器选择`复制服务器ID(GUILD_ID)`并记录,在此服务器中创建默认频道,右键点击此频道选择`复制频道ID(CHANNEL_ID)`并记录。 52 | 3. 打开 [discord开发者平台](https://discord.com/developers/applications) 登陆。 53 | 4. 创建新应用-Bot即`COZE-BOT`,并记录该bot专属的`token`和`id(COZE_BOT_ID)`,此bot为即将被coze托管的bot。 54 | 5. 创建新应用-Bot即`CDP-BOT`,并记录该bot专属的`token(BOT_TOKEN)`,此bot为监听discord消息的bot。 55 | 6. 两个bot开通对应权限(`Administrator`)并邀请进创建好的discord服务器 (过程不在此赘述)。 56 | 7. 打开 [discord官网](https://discord.com/app)进入服务器,按F12打开开发者工具,在任一频道内发送一次消息,在开发者工具-`Network`中找到请求 `https://discord.com/api/v9/channels/1206*******703/messages`从该接口header中获取`Authorization(USER_AUTHORIZATION)`并记录。 57 | 8. 打开 [coze官网](https://www.coze.com) 创建bot并进行个性化配置(注意`Auto-Suggestion`为`Default/on`(默认不用改))。 58 | 9. 配置好后选择发布到discord,填写`COZE-BOT`的`token`,发布完成后在discord服务器中可看到`COZE-BOT`在线并可以@使用。 59 | 10. 使用上述记录的参数开始配置[环境变量](#环境变量)并[部署](#部署)本项目。 60 | 11. 访问接口文档地址,即可开始调试或集成其他项目。 61 | 62 | ## 如何集成NextChat 63 | 64 | 填 接口地址(ip:端口/域名) 及 API-Key(`PROXY_SECRET`),其它的随便填随便选。 65 | 66 | > 如果自己没有搭建NextChat面板,这里有个已经搭建好的可以使用 [NextChat](https://ci.goeast.io/) 67 | 68 | 69 | 70 | ## 如何集成one-api 71 | 72 | 填 `BaseURL`(ip:端口/域名) 及 密钥(`PROXY_SECRET`),其它的随便填随便选。 73 | 74 | 75 | 76 | ## 部署 77 | 78 | ### 基于 Docker-Compose(All In One) 进行部署 79 | 80 | ```shell 81 | docker-compose pull && docker-compose up -d 82 | ``` 83 | 84 | #### docker-compose.yml 85 | 86 | ```docker 87 | version: '3.4' 88 | 89 | services: 90 | coze-discord-proxy: 91 | image: deanxv/coze-discord-proxy:latest 92 | container_name: coze-discord-proxy 93 | restart: always 94 | ports: 95 | - "7077:7077" 96 | volumes: 97 | - ./data:/app/coze-discord-proxy/data 98 | environment: 99 | - USER_AUTHORIZATION=MTA5OTg5N************aXUBHVI # 必须修改为我们discord用户的授权密钥(多个请以,分隔) 100 | - BOT_TOKEN=MTE5OT************UrUWNbG63w # 必须修改为监听消息的Bot-Token 101 | - GUILD_ID=11************96 # 必须修改为两个机器人所在的服务器ID 102 | - COZE_BOT_ID=11************97 # 必须修改为由coze托管的机器人ID 103 | - CHANNEL_ID=11************94 # [可选]默认频道-(目前版本下该参数仅用来活跃机器人) 104 | - PROXY_SECRET=123456 # [可选]接口密钥-修改此行为请求头校验的值(多个请以,分隔) 105 | - TZ=Asia/Shanghai 106 | ``` 107 | 108 | ### 基于 Docker 进行部署 109 | 110 | ```docker 111 | docker run --name coze-discord-proxy -d --restart always \ 112 | -p 7077:7077 \ 113 | -v $(pwd)/data:/app/coze-discord-proxy/data \ 114 | -e USER_AUTHORIZATION="MTA5OTg5N************uIfytxUgJfmaXUBHVI" \ 115 | -e BOT_TOKEN="MTE5OTk2************rUWNbG63w" \ 116 | -e GUILD_ID="11************96" \ 117 | -e COZE_BOT_ID="11************97" \ 118 | -e PROXY_SECRET="123456" \ 119 | -e CHANNEL_ID="11************24" \ 120 | -e TZ=Asia/Shanghai \ 121 | deanxv/coze-discord-proxy 122 | ``` 123 | 124 | 其中`USER_AUTHORIZATION`,`BOT_TOKEN`,`GUILD_ID`,`COZE_BOT_ID`,`PROXY_SECRET`,`CHANNEL_ID`修改为自己的。 125 | 126 | 如果上面的镜像无法拉取,可以尝试使用 GitHub 的 Docker 镜像,将上面的`deanxv/coze-discord-proxy`替换为`ghcr.io/deanxv/coze-discord-proxy`即可。 127 | 128 | ### 部署到第三方平台 129 | 130 |
131 | 部署到 Zeabur 132 |
133 | 134 | > Zeabur 的服务器在国外,自动解决了网络的问题,同时免费的额度也足够个人使用 135 | 136 | 点击一键部署: 137 | 138 | [![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/templates/GMU8C8?referralCode=deanxv) 139 | 140 | **一键部署后 `USER_AUTHORIZATION`,`BOT_TOKEN`,`GUILD_ID`,`COZE_BOT_ID`,`PROXY_SECRET`,`CHANNEL_ID`变量也需要替换!** 141 | 142 | 或手动部署: 143 | 144 | 1. 首先 **fork** 一份代码。 145 | 2. 进入 [Zeabur](https://zeabur.com?referralCode=deanxv),使用github登录,进入控制台。 146 | 3. 在 Service -> Add Service,选择 Git(第一次使用需要先授权),选择你 fork 的仓库。 147 | 4. Deploy 会自动开始,先取消。 148 | 5. 添加环境变量 149 | 150 | `USER_AUTHORIZATION:MTA5OTg5N************uIfytxUgJfmaXUBHVI` 主动发送消息的discord用户的授权密钥(多个请以,分隔) 151 | 152 | `BOT_TOKEN:MTE5OTk************WNbG63w` 监听消息的Bot-Token 153 | 154 | `GUILD_ID:11************96` 两个机器人所在的服务器ID 155 | 156 | `COZE_BOT_ID:11************97` 由coze托管的机器人ID 157 | 158 | `CHANNEL_ID:11************24` # [可选]默认频道-(目前版本下该参数仅用来活跃机器人) 159 | 160 | `PROXY_SECRET:123456` [可选]接口密钥-修改此行为请求头校验的值(多个请以,分隔)(与openai-API-KEY用法一致) 161 | 162 | 保存。 163 | 164 | 6. 选择 Redeploy。 165 | 166 |
167 | 168 | 169 |
170 | 171 |
172 | 部署到 Render 173 |
174 | 175 | > Render 提供免费额度,绑卡后可以进一步提升额度 176 | 177 | Render 可以直接部署 docker 镜像,不需要 fork 仓库:[Render](https://dashboard.render.com) 178 | 179 |
180 |
181 | 182 | ## 配置 183 | 184 | ### 环境变量 185 | 186 | 1. `USER_AUTHORIZATION=MTA5OTg5N************uIfytxUgJfmaXUBHVI` 主动发送消息的discord用户的授权密钥(多个请以,分隔) 187 | 2. `BOT_TOKEN=MTE5OTk2************rUWNbG63w` 监听消息的Bot-Token 188 | 3. `GUILD_ID=11************96` 所有Bot所在的服务器ID 189 | 4. `COZE_BOT_ID=11************97` 由coze托管的Bot-ID 190 | 5. `PORT=7077` [可选]端口,默认为7077 191 | 6. `SWAGGER_ENABLE=1` [可选]是否启用Swagger接口文档[0:否;1:是] (默认为1) 192 | 7. `ONLY_OPENAI_API=0` [可选]是否只暴露与openai对齐的接口[0:否;1:是] (默认为0) 193 | 8. `CHANNEL_ID=11************24` [可选]默认频道-(目前版本下该参数仅用来活跃Bot) 194 | 9. `PROXY_SECRET=123456` [可选]接口密钥-修改此行为请求头校验的值(多个请以,分隔)(与openai-API-KEY用法一致),**推荐使用此环境变量** 195 | 10. `DEFAULT_CHANNEL_ENABLE=0` [可选]是否启用默认频道[0:否;1:是] (默认为0) 启用后每次对话都会在默认频道中,**会话隔离会失效**,**推荐不使用此环境变量** 196 | 11. `ALL_DIALOG_RECORD_ENABLE=1` [可选]是否启用全量上下文[0:否;1:是] (默认为1) 关闭后每次对话只会发送`messages`中最后一个`role`为`user`的`content`,**推荐不使用此环境变量** 197 | 12. `CHANNEL_AUTO_DEL_TIME=5` [可选]频道自动删除时间(秒) 此参数为每次对话完成后自动删除频道的时间(默认为5s),为0时则不删除,**推荐不使用此环境变量** 198 | 13. `COZE_BOT_STAY_ACTIVE_ENABLE=1` [可选]是否开启每日`9`点活跃coze-bot的定时任务[0:否;1:是] (默认为1),**推荐不使用此环境变量** 199 | 14. `REQUEST_OUT_TIME=60` [可选]对话接口非流响应下的请求超时时间,**推荐不使用此环境变量** 200 | 15. `STREAM_REQUEST_OUT_TIME=60` [可选]对话接口流响应下的每次流返回超时时间,**推荐不使用此环境变量** 201 | 16. `REQUEST_RATE_LIMIT=60` [可选]每分钟下的单ip请求速率限制,默认:60次/min 202 | 17. `USER_AGENT=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36` [可选]discord用户端Agent,使用自己的可能有效防止被ban,不设置时默认使用作者的 推荐使用此环境变量 203 | 18. `NOTIFY_TELEGRAM_BOT_TOKEN=6232***********Niz9c` [可选]作为通知TelegramBot的Token(通知事件:1.无可用`user_authorization`;2.`BOT_TOKEN`关联的BOT触发风控) 204 | 19. `NOTIFY_TELEGRAM_USER_ID=10******35` [可选]`NOTIFY_TELEGRAM_BOT_TOKEN`关联的`Telegram-Bot`推送至该变量关联的`Telegram-User`(**`NOTIFY_TELEGRAM_BOT_TOKEN`不为空时该变量也不可为空**) 205 | 20. `PROXY_URL=http://127.0.0.1:10801` [可选]代理(仅支持http) 206 | 207 | ## 进阶配置 208 | 209 | ### 配置多机器人 210 | 211 | 1. 部署前在`docker`/`docker-compose`部署同级目录下创建`data/config/bot_config.json`文件 212 | 2. 编写该`json`文件,`bot_config.json`格式如下 213 | 214 | ```shell 215 | [ 216 | { 217 | "proxySecret": "123", // 接口请求密钥(PROXY_SECRET)(注意:此密钥在环境变量PROXY_SECRET中存在时该Bot才可以被匹配到!) 218 | "cozeBotId": "12***************31", // coze托管的机器人ID 219 | "model": ["gpt-3.5","gpt-3.5-16k"], // 模型名称(数组格式)(与请求参数中的model对应,如请求中的model在该json中未匹配到则会抛出异常) 220 | "channelId": "12***************56" // [可选]discord频道ID(机器人必须在此频道所在的服务器)(目前版本下该参数仅用来活跃机器人) 221 | }, 222 | { 223 | "proxySecret": "456", 224 | "cozeBotId": "12***************64", 225 | "model": ["gpt-4","gpt-4-16k"], 226 | "channelId": "12***************78" 227 | }, 228 | { 229 | "proxySecret": "789", 230 | "cozeBotId": "12***************12", 231 | "model": ["dall-e-3"], 232 | "channelId": "12***************24" 233 | } 234 | ] 235 | ``` 236 | 237 | 3. 重启服务 238 | 239 | > 当有此json配置时,会通过请求头携带的[请求密钥]+请求体中的[`model`]联合匹配此配置中的`cozeBotId` 240 | > 若匹配到多个则随机选择一个。配置很灵活,可以根据自己的需求进行配置。 241 | 242 | 第三方平台(如: `zeabur`)部署的服务需要[配置多机器人]请参考[issue#30](https://github.com/deanxv/coze-discord-proxy/issues/30) 243 | 244 | ## 限制 245 | 246 | 目前的coze的免费及收费订阅详情:https://www.coze.com/docs/guides/subscription?_lang=zh 247 | 248 | 可配置多discord用户`Authorization`(参考[环境变量](#环境变量)`USER_AUTHORIZATION`)或[配置多机器人](#配置多机器人)实现叠加请求次数及请求负载均衡。 249 | 250 | ## Q&A 251 | 252 | Q: 并发量高时应如何配置? 253 | 254 | A: 首先为服务[配置多机器人](#配置多机器人)用来作响应bot的负载,其次准备多个discord账号用来作请求负载并邀请进同一个服务器,获取每个账号的`Authorization`英文逗号分隔配置在环境变量`USER_AUTHORIZATION`中,此时每次请求都会从多个discord账号中取出一个发起对话,有效实现负载均衡。 255 | 256 | ## ⭐ Star History 257 | 258 | [![Star History Chart](https://api.star-history.com/svg?repos=deanxv/coze-discord-proxy&type=Date)](https://star-history.com/#deanxv/coze-discord-proxy&Date) 259 | 260 | ## 相关 261 | 262 | [GPT-Content-Audit](https://github.com/deanxv/gpt-content-audit):聚合Openai、阿里云、百度智能云、七牛云等开放平台,提供与`openai`请求格式对齐的内容审核前置服务。 263 | 264 | ## 其他 265 | 266 | **开源不易,若你参考此项目或基于此项目二开可否麻烦在你的项目文档中标识此项目呢?谢谢你!** 267 | 268 | Java: https://github.com/oddfar/coze-discord (目前不可用) 269 | 270 | ## 引用 271 | 272 | Coze 官网 : https://www.coze.com 273 | 274 | Discord 开发地址 : https://discord.com/developers/applications 275 | 276 | 277 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanxv/coze-discord-proxy/2abd758127d6bcc5383d97212b7d736f122f713d/VERSION -------------------------------------------------------------------------------- /common/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "coze-discord-proxy/common/env" 5 | "os" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | var ProxySecret = os.Getenv("PROXY_SECRET") 11 | var ProxySecrets = strings.Split(os.Getenv("PROXY_SECRET"), ",") 12 | var AllDialogRecordEnable = os.Getenv("ALL_DIALOG_RECORD_ENABLE") 13 | var RequestOutTime = os.Getenv("REQUEST_OUT_TIME") 14 | var StreamRequestOutTime = os.Getenv("STREAM_REQUEST_OUT_TIME") 15 | var SwaggerEnable = os.Getenv("SWAGGER_ENABLE") 16 | var OnlyOpenaiApi = os.Getenv("ONLY_OPENAI_API") 17 | var MaxChannelDelType = os.Getenv("MAX_CHANNEL_DEL_TYPE") 18 | 19 | var DebugEnabled = os.Getenv("DEBUG") == "true" 20 | 21 | var RateLimitKeyExpirationDuration = 20 * time.Minute 22 | 23 | var RequestOutTimeDuration = 5 * time.Minute 24 | 25 | var ( 26 | RequestRateLimitNum = env.Int("REQUEST_RATE_LIMIT", 60) 27 | RequestRateLimitDuration int64 = 1 * 60 28 | ) 29 | -------------------------------------------------------------------------------- /common/constants.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | var Version = "v4.6.5" // this hard coding will be replaced automatically when building, no need to manually change 4 | 5 | const ( 6 | RequestIdKey = "X-Request-Id" 7 | OutTime = "out-time" 8 | ) 9 | 10 | var ImgGeneratePrompt = "Please adhere strictly to my instructions below for the drawing task. If I do not provide a specific directive for drawing, create an image that corresponds to the text I have provided:\\n" 11 | 12 | var DefaultOpenaiModelList = []string{ 13 | "gpt-3.5-turbo", 14 | "gpt-3.5-turbo-1106", 15 | "gpt-3.5-turbo-0125", 16 | "gpt-4", 17 | "gpt-4-0613", 18 | "gpt-4-32k", 19 | "gpt-4-32k-0613", 20 | "gpt-4-turbo", 21 | "gpt-4-turbo-preview", 22 | "gpt-4o", 23 | "gpt-4o-2024-05-13", 24 | "gpt-4o-2024-08-06", 25 | "gpt-4o-2024-11-20", 26 | "chatgpt-4o-latest", 27 | "gpt-4o-mini", 28 | "gpt-4o-mini-2024-07-18", 29 | "gpt-4-vision-preview", 30 | "gpt-4-turbo-2024-04-09", 31 | "gpt-4-1106-preview", 32 | "dall-e-3", 33 | "o1-mini", 34 | "o1-preview", 35 | "o3-mini", 36 | } 37 | 38 | var CozeErrorMessages = append(append(CozeOtherErrorMessages, CozeUserDailyLimitErrorMessages...), CozeCreatorDailyLimitErrorMessages...) 39 | 40 | var CozeOtherErrorMessages = []string{ 41 | "Something wrong occurs, please retry. If the error persists, please contact the support team.", 42 | "Some error occurred. Please try again or contact the support team in our communities.", 43 | "We've detected unusual traffic from your network, so Coze is temporarily unavailable.", 44 | "There are too many users now. Please try again a bit later.", 45 | "I'm sorry, but I can't assist with that.", 46 | } 47 | 48 | var CozeUserDailyLimitErrorMessages = []string{ 49 | "Hi there! You've used up your free chat credits. To continue enjoying our service, please consider upgrading to our premium plan [Upgrade to Coze Premium to chat]", 50 | "You have exceeded the daily limit for sending messages to the bot. Please try again later.", 51 | "Hi there! You've used up your credits for today. To continue enjoying our service, please try again tomorrow or consider upgrading to our premium plan.", 52 | } 53 | 54 | var CozeCreatorDailyLimitErrorMessages = []string{ 55 | "The bot's usage is covered by the developer, but due to the developer's message credits being exhausted, the bot is temporarily unavailable.", 56 | } 57 | 58 | var CozeDailyLimitErrorMessages = append(CozeUserDailyLimitErrorMessages, CozeCreatorDailyLimitErrorMessages...) 59 | -------------------------------------------------------------------------------- /common/env/helper.go: -------------------------------------------------------------------------------- 1 | package env 2 | 3 | import ( 4 | "os" 5 | "strconv" 6 | ) 7 | 8 | func Bool(env string, defaultValue bool) bool { 9 | if env == "" || os.Getenv(env) == "" { 10 | return defaultValue 11 | } 12 | return os.Getenv(env) == "true" 13 | } 14 | 15 | func Int(env string, defaultValue int) int { 16 | if env == "" || os.Getenv(env) == "" { 17 | return defaultValue 18 | } 19 | num, err := strconv.Atoi(os.Getenv(env)) 20 | if err != nil { 21 | return defaultValue 22 | } 23 | return num 24 | } 25 | 26 | func Float64(env string, defaultValue float64) float64 { 27 | if env == "" || os.Getenv(env) == "" { 28 | return defaultValue 29 | } 30 | num, err := strconv.ParseFloat(os.Getenv(env), 64) 31 | if err != nil { 32 | return defaultValue 33 | } 34 | return num 35 | } 36 | 37 | func String(env string, defaultValue string) string { 38 | if env == "" || os.Getenv(env) == "" { 39 | return defaultValue 40 | } 41 | return os.Getenv(env) 42 | } 43 | -------------------------------------------------------------------------------- /common/init.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | "path/filepath" 9 | ) 10 | 11 | var ( 12 | Port = flag.Int("port", 7077, "the listening port") 13 | PrintVersion = flag.Bool("version", false, "print version and exit") 14 | PrintHelp = flag.Bool("help", false, "print help and exit") 15 | LogDir = flag.String("log-dir", "", "specify the log directory") 16 | ) 17 | 18 | // UploadPath Maybe override by ENV_VAR 19 | var UploadPath = "upload" 20 | 21 | func printHelp() { 22 | fmt.Println("Coze Discord Proxy" + Version + "") 23 | fmt.Println("Copyright (C) 2024 Dean. All rights reserved.") 24 | fmt.Println("GitHub: https://github.com/deanxv/coze-discord-proxy ") 25 | fmt.Println("Usage: coze-discord-proxy [--port ] [--log-dir ] [--version] [--help]") 26 | } 27 | 28 | func init() { 29 | flag.Parse() 30 | 31 | if *PrintVersion { 32 | fmt.Println(Version) 33 | os.Exit(0) 34 | } 35 | 36 | if *PrintHelp { 37 | printHelp() 38 | os.Exit(0) 39 | } 40 | 41 | if os.Getenv("UPLOAD_PATH") != "" { 42 | UploadPath = os.Getenv("UPLOAD_PATH") 43 | } 44 | if *LogDir != "" { 45 | var err error 46 | *LogDir, err = filepath.Abs(*LogDir) 47 | if err != nil { 48 | log.Fatal(err) 49 | } 50 | if _, err := os.Stat(*LogDir); os.IsNotExist(err) { 51 | err = os.Mkdir(*LogDir, 0777) 52 | if err != nil { 53 | log.Fatal(err) 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /common/logger.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/gin-gonic/gin" 7 | "io" 8 | "log" 9 | "os" 10 | "path/filepath" 11 | "sync" 12 | "time" 13 | ) 14 | 15 | const ( 16 | loggerINFO = "INFO" 17 | loggerWarn = "WARN" 18 | loggerError = "ERR" 19 | ) 20 | 21 | const maxLogCount = 1000000 22 | 23 | var logCount int 24 | var setupLogLock sync.Mutex 25 | var setupLogWorking bool 26 | 27 | func SetupLogger() { 28 | if *LogDir != "" { 29 | ok := setupLogLock.TryLock() 30 | if !ok { 31 | log.Println("setup log is already working") 32 | return 33 | } 34 | defer func() { 35 | setupLogLock.Unlock() 36 | setupLogWorking = false 37 | }() 38 | logPath := filepath.Join(*LogDir, fmt.Sprintf("log-%s.log", time.Now().Format("20060102"))) 39 | fd, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 40 | if err != nil { 41 | log.Fatal("failed to open log file") 42 | } 43 | gin.DefaultWriter = io.MultiWriter(os.Stdout, fd) 44 | gin.DefaultErrorWriter = io.MultiWriter(os.Stderr, fd) 45 | } 46 | } 47 | 48 | func SysLog(s string) { 49 | t := time.Now() 50 | _, _ = fmt.Fprintf(gin.DefaultWriter, "[SYS] %v | %s \n", t.Format("2006/01/02 - 15:04:05"), s) 51 | } 52 | 53 | func SysError(s string) { 54 | t := time.Now() 55 | _, _ = fmt.Fprintf(gin.DefaultErrorWriter, "[SYS] %v | %s \n", t.Format("2006/01/02 - 15:04:05"), s) 56 | } 57 | 58 | func LogInfo(ctx context.Context, msg string) { 59 | logHelper(ctx, loggerINFO, msg) 60 | } 61 | 62 | func LogWarn(ctx context.Context, msg string) { 63 | logHelper(ctx, loggerWarn, msg) 64 | } 65 | 66 | func LogError(ctx context.Context, msg string) { 67 | logHelper(ctx, loggerError, msg) 68 | } 69 | 70 | func logHelper(ctx context.Context, level string, msg string) { 71 | writer := gin.DefaultErrorWriter 72 | if level == loggerINFO { 73 | writer = gin.DefaultWriter 74 | } 75 | id := ctx.Value(RequestIdKey) 76 | if id == nil { 77 | id = "UNKNOWN" 78 | } 79 | now := time.Now() 80 | _, _ = fmt.Fprintf(writer, "[%s] %v | %s | %s \n", level, now.Format("2006/01/02 - 15:04:05"), id, msg) 81 | logCount++ // we don't need accurate count, so no lock here 82 | if logCount > maxLogCount && !setupLogWorking { 83 | logCount = 0 84 | setupLogWorking = true 85 | go func() { 86 | SetupLogger() 87 | }() 88 | } 89 | } 90 | 91 | func FatalLog(v ...any) { 92 | t := time.Now() 93 | _, _ = fmt.Fprintf(gin.DefaultErrorWriter, "[FATAL] %v | %v \n", t.Format("2006/01/02 - 15:04:05"), v) 94 | os.Exit(1) 95 | } 96 | -------------------------------------------------------------------------------- /common/myerr/discordunauthorizederror.go: -------------------------------------------------------------------------------- 1 | package myerr 2 | 3 | import "fmt" 4 | 5 | // 自定义错误类型 6 | type DiscordUnauthorizedError struct { 7 | Message string 8 | ErrCode int 9 | } 10 | 11 | // 实现 error 接口的 Error 方法 12 | func (e *DiscordUnauthorizedError) Error() string { 13 | return fmt.Sprintf("errCode: %v, message: %v", e.ErrCode, e.Message) 14 | } 15 | -------------------------------------------------------------------------------- /common/myerr/modelnotfounderror.go: -------------------------------------------------------------------------------- 1 | package myerr 2 | 3 | import "fmt" 4 | 5 | type ModelNotFoundError struct { 6 | Message string 7 | ErrCode int 8 | } 9 | 10 | // 实现 error 接口的 Error 方法 11 | func (e *ModelNotFoundError) Error() string { 12 | return fmt.Sprintf("errCode: %v, message: %v", e.ErrCode, e.Message) 13 | } 14 | -------------------------------------------------------------------------------- /common/myerr/myerror.go: -------------------------------------------------------------------------------- 1 | package myerr 2 | 3 | import "errors" 4 | 5 | var ErrNoBotId = errors.New("no_available_bot") 6 | -------------------------------------------------------------------------------- /common/rate-limit.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | type InMemoryRateLimiter struct { 9 | store map[string]*[]int64 10 | mutex sync.Mutex 11 | expirationDuration time.Duration 12 | } 13 | 14 | func (l *InMemoryRateLimiter) Init(expirationDuration time.Duration) { 15 | if l.store == nil { 16 | l.mutex.Lock() 17 | if l.store == nil { 18 | l.store = make(map[string]*[]int64) 19 | l.expirationDuration = expirationDuration 20 | if expirationDuration > 0 { 21 | go l.clearExpiredItems() 22 | } 23 | } 24 | l.mutex.Unlock() 25 | } 26 | } 27 | 28 | func (l *InMemoryRateLimiter) clearExpiredItems() { 29 | for { 30 | time.Sleep(l.expirationDuration) 31 | l.mutex.Lock() 32 | now := time.Now().Unix() 33 | for key := range l.store { 34 | queue := l.store[key] 35 | size := len(*queue) 36 | if size == 0 || now-(*queue)[size-1] > int64(l.expirationDuration.Seconds()) { 37 | delete(l.store, key) 38 | } 39 | } 40 | l.mutex.Unlock() 41 | } 42 | } 43 | 44 | // Request parameter duration's unit is seconds 45 | func (l *InMemoryRateLimiter) Request(key string, maxRequestNum int, duration int64) bool { 46 | l.mutex.Lock() 47 | defer l.mutex.Unlock() 48 | // [old <-- new] 49 | queue, ok := l.store[key] 50 | now := time.Now().Unix() 51 | if ok { 52 | if len(*queue) < maxRequestNum { 53 | *queue = append(*queue, now) 54 | return true 55 | } else { 56 | if now-(*queue)[0] >= duration { 57 | *queue = (*queue)[1:] 58 | *queue = append(*queue, now) 59 | return true 60 | } else { 61 | return false 62 | } 63 | } 64 | } else { 65 | s := make([]int64, 0, maxRequestNum) 66 | l.store[key] = &s 67 | *(l.store[key]) = append(*(l.store[key]), now) 68 | } 69 | return true 70 | } 71 | -------------------------------------------------------------------------------- /common/snowflakeid.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "time" 7 | 8 | "github.com/sony/sonyflake" 9 | ) 10 | 11 | // snowflakeGenerator 单例 12 | var ( 13 | generator *SnowflakeGenerator 14 | once sync.Once 15 | ) 16 | 17 | // SnowflakeGenerator 是雪花ID生成器的封装 18 | type SnowflakeGenerator struct { 19 | flake *sonyflake.Sonyflake 20 | } 21 | 22 | // NextID 生成一个新的雪花ID 23 | func NextID() (string, error) { 24 | once.Do(initGenerator) 25 | id, err := generator.flake.NextID() 26 | if err != nil { 27 | return "", err 28 | } 29 | return fmt.Sprintf("%d", id), nil 30 | } 31 | 32 | // initGenerator 初始化生成器,只调用一次 33 | func initGenerator() { 34 | st := sonyflake.Settings{ 35 | StartTime: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC), 36 | } 37 | flake := sonyflake.NewSonyflake(st) 38 | if flake == nil { 39 | FatalLog("sonyflake not created") 40 | } 41 | generator = &SnowflakeGenerator{ 42 | flake: flake, 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /common/tiktoken.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import "github.com/pkoukk/tiktoken-go" 4 | 5 | var ( 6 | Tke *tiktoken.Tiktoken 7 | ) 8 | 9 | func init() { 10 | // gpt-4-turbo encoding 11 | tke, err := tiktoken.GetEncoding("cl100k_base") 12 | if err != nil { 13 | FatalLog(err.Error()) 14 | } 15 | Tke = tke 16 | 17 | } 18 | 19 | func CountTokens(text string) int { 20 | return len(Tke.Encode(text, nil, nil)) 21 | } 22 | -------------------------------------------------------------------------------- /common/utils.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "github.com/google/uuid" 7 | jsoniter "github.com/json-iterator/go" 8 | "html/template" 9 | "log" 10 | "math/rand" 11 | "net" 12 | "os/exec" 13 | "runtime" 14 | "strconv" 15 | "strings" 16 | "time" 17 | ) 18 | 19 | func OpenBrowser(url string) { 20 | var err error 21 | 22 | switch runtime.GOOS { 23 | case "linux": 24 | err = exec.Command("xdg-open", url).Start() 25 | case "windows": 26 | err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() 27 | case "darwin": 28 | err = exec.Command("open", url).Start() 29 | } 30 | if err != nil { 31 | log.Println(err) 32 | } 33 | } 34 | 35 | func GetIp() (ip string) { 36 | ips, err := net.InterfaceAddrs() 37 | if err != nil { 38 | log.Println(err) 39 | return ip 40 | } 41 | 42 | for _, a := range ips { 43 | if ipNet, ok := a.(*net.IPNet); ok && !ipNet.IP.IsLoopback() { 44 | if ipNet.IP.To4() != nil { 45 | ip = ipNet.IP.String() 46 | if strings.HasPrefix(ip, "10") { 47 | return 48 | } 49 | if strings.HasPrefix(ip, "172") { 50 | return 51 | } 52 | if strings.HasPrefix(ip, "192.168") { 53 | return 54 | } 55 | ip = "" 56 | } 57 | } 58 | } 59 | return 60 | } 61 | 62 | var sizeKB = 1024 63 | var sizeMB = sizeKB * 1024 64 | var sizeGB = sizeMB * 1024 65 | 66 | func Bytes2Size(num int64) string { 67 | numStr := "" 68 | unit := "B" 69 | if num/int64(sizeGB) > 1 { 70 | numStr = fmt.Sprintf("%.2f", float64(num)/float64(sizeGB)) 71 | unit = "GB" 72 | } else if num/int64(sizeMB) > 1 { 73 | numStr = fmt.Sprintf("%d", int(float64(num)/float64(sizeMB))) 74 | unit = "MB" 75 | } else if num/int64(sizeKB) > 1 { 76 | numStr = fmt.Sprintf("%d", int(float64(num)/float64(sizeKB))) 77 | unit = "KB" 78 | } else { 79 | numStr = fmt.Sprintf("%d", num) 80 | } 81 | return numStr + " " + unit 82 | } 83 | 84 | func Seconds2Time(num int) (time string) { 85 | if num/31104000 > 0 { 86 | time += strconv.Itoa(num/31104000) + " 年 " 87 | num %= 31104000 88 | } 89 | if num/2592000 > 0 { 90 | time += strconv.Itoa(num/2592000) + " 个月 " 91 | num %= 2592000 92 | } 93 | if num/86400 > 0 { 94 | time += strconv.Itoa(num/86400) + " 天 " 95 | num %= 86400 96 | } 97 | if num/3600 > 0 { 98 | time += strconv.Itoa(num/3600) + " 小时 " 99 | num %= 3600 100 | } 101 | if num/60 > 0 { 102 | time += strconv.Itoa(num/60) + " 分钟 " 103 | num %= 60 104 | } 105 | time += strconv.Itoa(num) + " 秒" 106 | return 107 | } 108 | 109 | func Interface2String(inter interface{}) string { 110 | switch inter.(type) { 111 | case string: 112 | return inter.(string) 113 | case int: 114 | return fmt.Sprintf("%d", inter.(int)) 115 | case float64: 116 | return fmt.Sprintf("%f", inter.(float64)) 117 | } 118 | return "Not Implemented" 119 | } 120 | 121 | func UnescapeHTML(x string) interface{} { 122 | return template.HTML(x) 123 | } 124 | 125 | func IntMax(a int, b int) int { 126 | if a >= b { 127 | return a 128 | } else { 129 | return b 130 | } 131 | } 132 | 133 | func GetUUID() string { 134 | code := uuid.New().String() 135 | code = strings.Replace(code, "-", "", -1) 136 | return code 137 | } 138 | 139 | func Max(a int, b int) int { 140 | if a >= b { 141 | return a 142 | } else { 143 | return b 144 | } 145 | } 146 | 147 | func GetRandomString(length int) string { 148 | rand.Seed(time.Now().UnixNano()) 149 | key := make([]byte, length) 150 | for i := 0; i < length; i++ { 151 | key[i] = keyChars[rand.Intn(len(keyChars))] 152 | } 153 | return string(key) 154 | } 155 | 156 | const keyChars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 157 | 158 | func GetTimestamp() int64 { 159 | return time.Now().Unix() 160 | } 161 | 162 | func GetTimeString() string { 163 | now := time.Now() 164 | return fmt.Sprintf("%s%d", now.Format("20060102150405"), now.UnixNano()%1e9) 165 | } 166 | 167 | func Obj2Bytes(obj interface{}) ([]byte, error) { 168 | // 创建一个jsonIter的Encoder 169 | configCompatibleWithStandardLibrary := jsoniter.ConfigCompatibleWithStandardLibrary 170 | // 将结构体转换为JSON文本并保持顺序 171 | bytes, err := configCompatibleWithStandardLibrary.Marshal(obj) 172 | if err != nil { 173 | return nil, err 174 | } 175 | return bytes, nil 176 | } 177 | 178 | // IsImageBase64 判断给定的字符串是否可能是 Base64 编码 179 | func IsImageBase64(s string) bool { 180 | // 检查字符串是否符合数据URL的格式 181 | //if !strings.HasPrefix(s, "data:image/") || !strings.Contains(s, ";base64,") { 182 | // return false 183 | //} 184 | 185 | if !strings.Contains(s, ";base64,") { 186 | return false 187 | } 188 | 189 | // 获取";base64,"后的Base64编码部分 190 | dataParts := strings.Split(s, ";base64,") 191 | if len(dataParts) != 2 { 192 | return false 193 | } 194 | base64Data := dataParts[1] 195 | 196 | // 尝试Base64解码 197 | _, err := base64.StdEncoding.DecodeString(base64Data) 198 | return err == nil 199 | } 200 | 201 | // IsURL 判断给定的字符串是否可能是 URL 202 | func IsURL(s string) bool { 203 | return strings.HasPrefix(s, "http://") || strings.HasPrefix(s, "https://") || strings.HasPrefix(s, "ftp://") 204 | } 205 | 206 | // Contains checks if a string is present in a slice of strings. 207 | func SliceContains(slice []string, str string) bool { 208 | for _, item := range slice { 209 | if strings.Contains(str, item) { 210 | return true 211 | } 212 | } 213 | return false 214 | } 215 | 216 | // RandomElement 返回给定切片中的随机元素 217 | func RandomElement[T any](slice []T) (T, error) { 218 | if len(slice) == 0 { 219 | var zero T 220 | return zero, fmt.Errorf("empty slice") 221 | } 222 | 223 | // 确保每次随机都不一样 224 | rand.Seed(time.Now().UnixNano()) 225 | 226 | // 随机选择一个索引 227 | index := rand.Intn(len(slice)) 228 | return slice[index], nil 229 | } 230 | 231 | func ReverseSegment(s string, segLen int) []string { 232 | var result []string 233 | runeSlice := []rune(s) // 将字符串转换为rune切片,以正确处理多字节字符 234 | 235 | // 从字符串末尾开始切片 236 | for i := len(runeSlice); i > 0; i -= segLen { 237 | // 检查是否到达或超过字符串开始 238 | if i-segLen < 0 { 239 | // 如果超过,直接从字符串开始到当前位置的所有字符都添加到结果切片中 240 | result = append([]string{string(runeSlice[0:i])}, result...) 241 | } else { 242 | // 否则,从i-segLen到当前位置的子切片添加到结果切片中 243 | result = append([]string{string(runeSlice[i-segLen : i])}, result...) 244 | } 245 | } 246 | return result 247 | } 248 | 249 | func FilterSlice(slice []string, filter string) []string { 250 | var result []string 251 | for _, value := range slice { 252 | if value != filter { 253 | result = append(result, value) 254 | } 255 | } 256 | return result 257 | } 258 | 259 | // isSameDay 检查两个时间是否为同一天 260 | func IsSameDay(t1, t2 time.Time) bool { 261 | y1, m1, d1 := t1.Date() 262 | y2, m2, d2 := t2.Date() 263 | return y1 == y2 && m1 == m2 && d1 == d2 264 | } 265 | -------------------------------------------------------------------------------- /common/validate.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import "github.com/go-playground/validator/v10" 4 | 5 | var Validate *validator.Validate 6 | 7 | func init() { 8 | Validate = validator.New() 9 | } 10 | -------------------------------------------------------------------------------- /common/verification.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "strings" 6 | "sync" 7 | "time" 8 | ) 9 | 10 | type verificationValue struct { 11 | code string 12 | time time.Time 13 | } 14 | 15 | const ( 16 | EmailVerificationPurpose = "v" 17 | PasswordResetPurpose = "r" 18 | ) 19 | 20 | var verificationMutex sync.Mutex 21 | var verificationMap map[string]verificationValue 22 | var verificationMapMaxSize = 10 23 | var VerificationValidMinutes = 10 24 | 25 | func GenerateVerificationCode(length int) string { 26 | code := uuid.New().String() 27 | code = strings.Replace(code, "-", "", -1) 28 | if length == 0 { 29 | return code 30 | } 31 | return code[:length] 32 | } 33 | 34 | func RegisterVerificationCodeWithKey(key string, code string, purpose string) { 35 | verificationMutex.Lock() 36 | defer verificationMutex.Unlock() 37 | verificationMap[purpose+key] = verificationValue{ 38 | code: code, 39 | time: time.Now(), 40 | } 41 | if len(verificationMap) > verificationMapMaxSize { 42 | removeExpiredPairs() 43 | } 44 | } 45 | 46 | func VerifyCodeWithKey(key string, code string, purpose string) bool { 47 | verificationMutex.Lock() 48 | defer verificationMutex.Unlock() 49 | value, okay := verificationMap[purpose+key] 50 | now := time.Now() 51 | if !okay || int(now.Sub(value.time).Seconds()) >= VerificationValidMinutes*60 { 52 | return false 53 | } 54 | return code == value.code 55 | } 56 | 57 | func DeleteKey(key string, purpose string) { 58 | verificationMutex.Lock() 59 | defer verificationMutex.Unlock() 60 | delete(verificationMap, purpose+key) 61 | } 62 | 63 | // no lock inside, so the caller must lock the verificationMap before calling! 64 | func removeExpiredPairs() { 65 | now := time.Now() 66 | for key := range verificationMap { 67 | if int(now.Sub(verificationMap[key].time).Seconds()) >= VerificationValidMinutes*60 { 68 | delete(verificationMap, key) 69 | } 70 | } 71 | } 72 | 73 | func init() { 74 | verificationMutex.Lock() 75 | defer verificationMutex.Unlock() 76 | verificationMap = make(map[string]verificationValue) 77 | } 78 | -------------------------------------------------------------------------------- /controller/channel.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "coze-discord-proxy/common" 5 | "coze-discord-proxy/discord" 6 | "coze-discord-proxy/model" 7 | "encoding/json" 8 | "fmt" 9 | "github.com/gin-gonic/gin" 10 | "net/http" 11 | ) 12 | 13 | // ChannelCreate 创建频道 14 | // @Summary 创建频道 15 | // @Description 创建频道 16 | // @Tags channel 17 | // @Accept json 18 | // @Produce json 19 | // @Param channelModel body model.ChannelReq true "channelModel" 20 | // @Success 200 {object} model.ChannelResp "Successful response" 21 | // @Router /api/channel/create [post] 22 | func ChannelCreate(c *gin.Context) { 23 | 24 | var channelModel model.ChannelReq 25 | channelModel.Type = 0 26 | err := json.NewDecoder(c.Request.Body).Decode(&channelModel) 27 | if err != nil { 28 | c.JSON(http.StatusOK, gin.H{ 29 | "success": false, 30 | "message": "无效的参数", 31 | }) 32 | return 33 | } 34 | 35 | var channelId string 36 | 37 | if channelModel.ParentId == "" { 38 | channelId, err = discord.ChannelCreate(discord.GuildId, channelModel.Name, channelModel.Type) 39 | } else { 40 | channelId, err = discord.ChannelCreateComplex(discord.GuildId, channelModel.ParentId, channelModel.Name, channelModel.Type) 41 | } 42 | 43 | if err != nil { 44 | common.LogError(c, fmt.Sprintf("创建频道时异常 %s", err.Error())) 45 | c.JSON(http.StatusOK, gin.H{ 46 | "success": false, 47 | "message": "discord创建频道异常", 48 | }) 49 | } else { 50 | var channel model.ChannelResp 51 | channel.Id = channelId 52 | channel.Name = channelModel.Name 53 | c.JSON(http.StatusOK, gin.H{ 54 | "success": true, 55 | "data": channel, 56 | }) 57 | } 58 | return 59 | } 60 | 61 | // ChannelDel 删除频道 62 | // @Summary 删除频道 63 | // @Description 删除频道 64 | // @Tags channel 65 | // @Accept json 66 | // @Produce json 67 | // @Param id path string true "id" 68 | // @Success 200 {object} string "Successful response" 69 | // @Router /api/channel/del/{id} [get] 70 | func ChannelDel(c *gin.Context) { 71 | channelId := c.Param("id") 72 | 73 | if channelId == "" { 74 | c.JSON(http.StatusOK, gin.H{ 75 | "success": false, 76 | "message": "无效的参数", 77 | }) 78 | return 79 | } 80 | 81 | channelId, err := discord.ChannelDel(channelId) 82 | if err != nil { 83 | c.JSON(http.StatusOK, gin.H{ 84 | "success": false, 85 | "message": "discord删除频道异常", 86 | }) 87 | } else { 88 | 89 | c.JSON(http.StatusOK, gin.H{ 90 | "success": true, 91 | "message": "discord删除频道成功", 92 | }) 93 | } 94 | return 95 | } 96 | 97 | // ChannelDelAllCdp 删除全部CDP临时频道[谨慎调用] 98 | // @Summary 删除全部CDP临时频道[谨慎调用] 99 | // @Description 删除全部CDP临时频道[谨慎调用] 100 | // @Tags channel 101 | // @Accept json 102 | // @Produce json 103 | // @Success 200 {object} string "Successful response" 104 | // @Router /api/del/all/cdp [get] 105 | func ChannelDelAllCdp(c *gin.Context) { 106 | _, err := discord.ChannelDelAllForCdp(c) 107 | if err != nil { 108 | c.JSON(http.StatusOK, gin.H{ 109 | "success": false, 110 | "message": "discord删除频道异常", 111 | }) 112 | } else { 113 | 114 | c.JSON(http.StatusOK, gin.H{ 115 | "success": true, 116 | "message": "discord删除频道成功", 117 | }) 118 | } 119 | return 120 | } 121 | -------------------------------------------------------------------------------- /controller/chat.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "coze-discord-proxy/common" 5 | "coze-discord-proxy/common/config" 6 | "coze-discord-proxy/common/myerr" 7 | "coze-discord-proxy/discord" 8 | "coze-discord-proxy/model" 9 | "coze-discord-proxy/telegram" 10 | "encoding/base64" 11 | "encoding/json" 12 | "errors" 13 | "fmt" 14 | "github.com/gin-gonic/gin" 15 | "github.com/samber/lo" 16 | "io" 17 | "net/http" 18 | "strconv" 19 | "strings" 20 | "time" 21 | ) 22 | 23 | // Chat 发送消息 24 | // @Summary 发送消息 25 | // @Description 发送消息 26 | // @Tags chat 27 | // @Accept json 28 | // @Produce json 29 | // @Param chatModel body model.ChatReq true "chatModel" 30 | // @Param proxy-secret header string false "proxy-secret" 31 | // @Param out-time header string false "out-time" 32 | // @Success 200 {object} model.ReplyResp "Successful response" 33 | // @Router /api/chat [post] 34 | //func Chat(c *gin.Context) { 35 | // 36 | // var chatModel model.ChatReq 37 | // myerr := json.NewDecoder(c.Request.Body).Decode(&chatModel) 38 | // if myerr != nil { 39 | // common.LogError(c.Request.Context(), myerr.Error()) 40 | // c.JSON(http.StatusOK, gin.H{ 41 | // "message": "无效的参数", 42 | // "success": false, 43 | // }) 44 | // return 45 | // } 46 | // 47 | // sendChannelId, calledCozeBotId, myerr := getSendChannelIdAndCozeBotId(c, false, chatModel) 48 | // if myerr != nil { 49 | // common.LogError(c.Request.Context(), myerr.Error()) 50 | // c.JSON(http.StatusOK, model.OpenAIErrorResponse{ 51 | // OpenAIError: model.OpenAIError{ 52 | // Message: "配置异常", 53 | // Type: "invalid_request_error", 54 | // Code: "discord_request_err", 55 | // }, 56 | // }) 57 | // return 58 | // } 59 | // 60 | // sentMsg, myerr := discord.SendMessage(c, sendChannelId, calledCozeBotId, chatModel.Content) 61 | // if myerr != nil { 62 | // c.JSON(http.StatusOK, gin.H{ 63 | // "success": false, 64 | // "message": myerr.Error(), 65 | // }) 66 | // return 67 | // } 68 | // 69 | // replyChan := make(chan model.ReplyResp) 70 | // discord.RepliesChans[sentMsg.ID] = replyChan 71 | // defer delete(discord.RepliesChans, sentMsg.ID) 72 | // 73 | // stopChan := make(chan model.ChannelStopChan) 74 | // discord.ReplyStopChans[sentMsg.ID] = stopChan 75 | // defer delete(discord.ReplyStopChans, sentMsg.ID) 76 | // 77 | // timer, myerr := setTimerWithHeader(c, chatModel.Stream, config.RequestOutTimeDuration) 78 | // if myerr != nil { 79 | // common.LogError(c.Request.Context(), myerr.Error()) 80 | // c.JSON(http.StatusBadRequest, gin.H{ 81 | // "success": false, 82 | // "message": "超时时间设置异常", 83 | // }) 84 | // return 85 | // } 86 | // 87 | // if chatModel.Stream { 88 | // c.Stream(func(w io.Writer) bool { 89 | // select { 90 | // case reply := <-replyChan: 91 | // timerReset(c, chatModel.Stream, timer, config.RequestOutTimeDuration) 92 | // urls := "" 93 | // if len(reply.EmbedUrls) > 0 { 94 | // for _, url := range reply.EmbedUrls { 95 | // urls += "\n" + fmt.Sprintf("![Image](%s)", url) 96 | // } 97 | // } 98 | // c.SSEvent("message", reply.Content+urls) 99 | // return true // 继续保持流式连接 100 | // case <-timer.C: 101 | // // 定时器到期时,关闭流 102 | // return false 103 | // case <-stopChan: 104 | // return false // 关闭流式连接 105 | // } 106 | // }) 107 | // } else { 108 | // var replyResp model.ReplyResp 109 | // for { 110 | // select { 111 | // case reply := <-replyChan: 112 | // replyResp.Content = reply.Content 113 | // replyResp.EmbedUrls = reply.EmbedUrls 114 | // case <-timer.C: 115 | // c.JSON(http.StatusOK, gin.H{ 116 | // "success": false, 117 | // "message": "request_out_time", 118 | // }) 119 | // return 120 | // case <-stopChan: 121 | // c.JSON(http.StatusOK, gin.H{ 122 | // "success": true, 123 | // "data": replyResp, 124 | // }) 125 | // return 126 | // } 127 | // } 128 | // } 129 | //} 130 | 131 | // ChatForOpenAI 发送消息-openai 132 | // @Summary 发送消息-openai 133 | // @Description 发送消息-openai 134 | // @Tags openai 135 | // @Accept json 136 | // @Produce json 137 | // @Param request body model.OpenAIChatCompletionRequest true "request" 138 | // @Param Authorization header string false "Authorization" 139 | // @Param out-time header string false "out-time" 140 | // @Success 200 {object} model.OpenAIChatCompletionResponse "Successful response" 141 | // @Router /v1/chat/completions [post] 142 | func ChatForOpenAI(c *gin.Context) { 143 | 144 | var request model.OpenAIChatCompletionRequest 145 | err := json.NewDecoder(c.Request.Body).Decode(&request) 146 | if err != nil { 147 | common.LogError(c.Request.Context(), err.Error()) 148 | c.JSON(http.StatusInternalServerError, model.OpenAIErrorResponse{ 149 | OpenAIError: model.OpenAIError{ 150 | Message: "Invalid request parameters", 151 | Type: "request_error", 152 | Code: "500", 153 | }, 154 | }) 155 | return 156 | } 157 | 158 | if err := checkUserAuths(c); err != nil { 159 | c.JSON(http.StatusInternalServerError, model.OpenAIErrorResponse{ 160 | OpenAIError: model.OpenAIError{ 161 | Message: err.Error(), 162 | Type: "request_error", 163 | Code: "500", 164 | }, 165 | }) 166 | return 167 | } 168 | 169 | sendChannelId, calledCozeBotId, maxToken, isNewChannel, err := getSendChannelIdAndCozeBotId(c, request.ChannelId, request.Model, true) 170 | 171 | if err != nil { 172 | response := model.OpenAIErrorResponse{ 173 | OpenAIError: model.OpenAIError{ 174 | Message: "config error,check logs", 175 | Type: "request_error", 176 | Code: "500", 177 | }, 178 | } 179 | common.LogError(c.Request.Context(), err.Error()) 180 | var myErr *myerr.ModelNotFoundError 181 | if errors.As(err, &myErr) { 182 | response.OpenAIError.Message = "model_not_found" 183 | } 184 | if errors.As(err, &myerr.ErrNoBotId) { 185 | response.OpenAIError.Message = "no_available_bot" 186 | } 187 | c.JSON(http.StatusInternalServerError, response) 188 | return 189 | } 190 | 191 | if isNewChannel { 192 | defer func() { 193 | if discord.ChannelAutoDelTime != "" { 194 | delTime, _ := strconv.Atoi(discord.ChannelAutoDelTime) 195 | if delTime == 0 { 196 | discord.CancelChannelDeleteTimer(sendChannelId) 197 | } else if delTime > 0 { 198 | // 删除该频道 199 | discord.SetChannelDeleteTimer(sendChannelId, time.Duration(delTime)*time.Second) 200 | } 201 | } else { 202 | // 删除该频道 203 | discord.SetChannelDeleteTimer(sendChannelId, 5*time.Second) 204 | } 205 | }() 206 | } 207 | 208 | content := "Hi!" 209 | messages := request.Messages 210 | 211 | loop: 212 | for i := len(messages) - 1; i >= 0; i-- { 213 | message := messages[i] 214 | if message.Role == "user" { 215 | switch contentObj := message.Content.(type) { 216 | case string: 217 | if config.AllDialogRecordEnable == "1" || config.AllDialogRecordEnable == "" { 218 | messages[i] = model.OpenAIChatMessage{ 219 | Role: "user", 220 | Content: contentObj, 221 | } 222 | } else { 223 | content = contentObj 224 | break loop 225 | } 226 | case []interface{}: 227 | content, err = buildOpenAIGPT4VForImageContent(sendChannelId, contentObj) 228 | if err != nil { 229 | c.JSON(http.StatusInternalServerError, model.OpenAIErrorResponse{ 230 | OpenAIError: model.OpenAIError{ 231 | Message: "Image URL parsing error", 232 | Type: "request_error", 233 | Code: "500", 234 | }, 235 | }) 236 | return 237 | } 238 | if config.AllDialogRecordEnable == "1" || config.AllDialogRecordEnable == "" { 239 | messages[i] = model.OpenAIChatMessage{ 240 | Role: "user", 241 | Content: content, 242 | } 243 | } else { 244 | break loop 245 | } 246 | default: 247 | c.JSON(http.StatusInternalServerError, model.OpenAIErrorResponse{ 248 | OpenAIError: model.OpenAIError{ 249 | Message: "Message format error", 250 | Type: "request_error", 251 | Code: "500", 252 | }, 253 | }) 254 | return 255 | 256 | } 257 | //break 258 | } else { 259 | messages[i] = model.OpenAIChatMessage{ 260 | Role: message.Role, 261 | Content: message.Content, 262 | } 263 | } 264 | } 265 | 266 | if config.AllDialogRecordEnable == "1" || config.AllDialogRecordEnable == "" { 267 | jsonData, err := json.Marshal(messages) 268 | if err != nil { 269 | c.JSON(http.StatusOK, gin.H{ 270 | "success": false, 271 | "message": err.Error(), 272 | }) 273 | return 274 | } 275 | content = string(jsonData) 276 | } 277 | 278 | sentMsg, userAuth, err := discord.SendMessage(c, sendChannelId, calledCozeBotId, content, maxToken) 279 | if err != nil { 280 | c.JSON(http.StatusInternalServerError, model.OpenAIErrorResponse{ 281 | OpenAIError: model.OpenAIError{ 282 | Message: err.Error(), 283 | Type: "request_error", 284 | Code: "500", 285 | }, 286 | }) 287 | return 288 | } 289 | 290 | replyChan := make(chan model.OpenAIChatCompletionResponse) 291 | discord.RepliesOpenAIChans[sentMsg.ID] = &model.OpenAIChatCompletionChan{ 292 | Model: request.Model, 293 | Response: replyChan, 294 | } 295 | defer delete(discord.RepliesOpenAIChans, sentMsg.ID) 296 | 297 | stopChan := make(chan model.ChannelStopChan) 298 | discord.ReplyStopChans[sentMsg.ID] = stopChan 299 | defer delete(discord.ReplyStopChans, sentMsg.ID) 300 | 301 | timer, err := setTimerWithHeader(c, request.Stream, config.RequestOutTimeDuration) 302 | if err != nil { 303 | common.LogError(c.Request.Context(), err.Error()) 304 | c.JSON(http.StatusBadRequest, gin.H{ 305 | "success": false, 306 | "message": "超时时间设置异常", 307 | }) 308 | return 309 | } 310 | 311 | if request.Stream { 312 | strLen := "" 313 | c.Stream(func(w io.Writer) bool { 314 | select { 315 | case reply := <-replyChan: 316 | timerReset(c, request.Stream, timer, config.RequestOutTimeDuration) 317 | 318 | // TODO 多张图片问题 319 | if !strings.HasPrefix(reply.Choices[0].Message.Content, strLen) { 320 | if len(strLen) > 3 && strings.HasPrefix(reply.Choices[0].Message.Content, "\\n1.") { 321 | strLen = strLen[:len(strLen)-2] 322 | } else { 323 | return true 324 | } 325 | } 326 | 327 | newContent := strings.Replace(reply.Choices[0].Message.Content, strLen, "", 1) 328 | if newContent == "" && strings.HasSuffix(newContent, "[DONE]") { 329 | return true 330 | } 331 | reply.Choices[0].Delta.Content = newContent 332 | strLen += newContent 333 | 334 | reply.Object = "chat.completion.chunk" 335 | bytes, _ := common.Obj2Bytes(reply) 336 | c.SSEvent("", " "+string(bytes)) 337 | 338 | if common.SliceContains(common.CozeErrorMessages, reply.Choices[0].Message.Content) { 339 | if common.SliceContains(common.CozeUserDailyLimitErrorMessages, reply.Choices[0].Message.Content) { 340 | common.LogWarn(c, fmt.Sprintf("USER_AUTHORIZATION:%s DAILY LIMIT", userAuth)) 341 | discord.UserAuthorizations = common.FilterSlice(discord.UserAuthorizations, userAuth) 342 | } 343 | if common.SliceContains(common.CozeCreatorDailyLimitErrorMessages, reply.Choices[0].Message.Content) { 344 | common.LogWarn(c, fmt.Sprintf("BOT_ID:%s DAILY LIMIT", calledCozeBotId)) 345 | //discord.BotConfigList = discord.FilterBotConfigByBotId(discord.BotConfigList, calledCozeBotId) 346 | discord.DelLimitBot(calledCozeBotId) 347 | } 348 | c.SSEvent("", " [DONE]") 349 | return false // 关闭流式连接 350 | } 351 | 352 | return true // 继续保持流式连接 353 | case <-timer.C: 354 | // 定时器到期时,关闭流 355 | c.SSEvent("", " [DONE]") 356 | return false 357 | case <-stopChan: 358 | c.SSEvent("", " [DONE]") 359 | return false // 关闭流式连接 360 | } 361 | }) 362 | } else { 363 | var replyResp model.OpenAIChatCompletionResponse 364 | for { 365 | select { 366 | case reply := <-replyChan: 367 | if common.SliceContains(common.CozeErrorMessages, reply.Choices[0].Message.Content) { 368 | if common.SliceContains(common.CozeUserDailyLimitErrorMessages, reply.Choices[0].Message.Content) { 369 | common.LogWarn(c, fmt.Sprintf("USER_AUTHORIZATION:%s DAILY LIMIT", userAuth)) 370 | discord.UserAuthorizations = common.FilterSlice(discord.UserAuthorizations, userAuth) 371 | } 372 | if common.SliceContains(common.CozeCreatorDailyLimitErrorMessages, reply.Choices[0].Message.Content) { 373 | common.LogWarn(c, fmt.Sprintf("BOT_ID:%s DAILY LIMIT", calledCozeBotId)) 374 | //discord.BotConfigList = discord.FilterBotConfigByBotId(discord.BotConfigList, calledCozeBotId) 375 | discord.DelLimitBot(calledCozeBotId) 376 | 377 | } 378 | c.JSON(http.StatusInternalServerError, model.OpenAIErrorResponse{ 379 | OpenAIError: model.OpenAIError{ 380 | Message: reply.Choices[0].Message.Content, 381 | Type: "request_error", 382 | Code: "500", 383 | }, 384 | }) 385 | return 386 | } 387 | replyResp = reply 388 | case <-timer.C: 389 | c.JSON(http.StatusOK, replyResp) 390 | return 391 | case <-stopChan: 392 | c.JSON(http.StatusOK, replyResp) 393 | return 394 | } 395 | } 396 | } 397 | } 398 | 399 | // OpenaiModels 模型列表-openai 400 | // @Summary 模型列表-openai 401 | // @Description 模型列表-openai 402 | // @Tags openai 403 | // @Accept json 404 | // @Produce json 405 | // @Param Authorization header string false "Authorization" 406 | // @Success 200 {object} model.OpenaiModelListResponse "Successful response" 407 | // @Router /v1/models [get] 408 | func OpenaiModels(c *gin.Context) { 409 | var modelsResp []string 410 | 411 | secret := "" 412 | if len(discord.BotConfigList) != 0 { 413 | if secret = c.Request.Header.Get("Authorization"); secret != "" { 414 | secret = strings.Replace(secret, "Bearer ", "", 1) 415 | } 416 | 417 | botConfigs := discord.FilterConfigs(discord.BotConfigList, secret, "", nil) 418 | for _, botConfig := range botConfigs { 419 | modelsResp = append(modelsResp, botConfig.Model...) 420 | } 421 | 422 | modelsResp = lo.Uniq(modelsResp) 423 | } else { 424 | modelsResp = common.DefaultOpenaiModelList 425 | } 426 | 427 | var openaiModelListResponse model.OpenaiModelListResponse 428 | var openaiModelResponse []model.OpenaiModelResponse 429 | openaiModelListResponse.Object = "list" 430 | 431 | for _, modelResp := range modelsResp { 432 | openaiModelResponse = append(openaiModelResponse, model.OpenaiModelResponse{ 433 | ID: modelResp, 434 | Object: "model", 435 | }) 436 | } 437 | openaiModelListResponse.Data = openaiModelResponse 438 | c.JSON(http.StatusOK, openaiModelListResponse) 439 | return 440 | } 441 | 442 | func buildOpenAIGPT4VForImageContent(sendChannelId string, objs []interface{}) (string, error) { 443 | var content string 444 | var url string 445 | 446 | for _, obj := range objs { 447 | 448 | jsonData, err := json.Marshal(obj) 449 | if err != nil { 450 | return "", err 451 | } 452 | 453 | var req model.OpenAIGPT4VImagesReq 454 | err = json.Unmarshal(jsonData, &req) 455 | if err != nil { 456 | return "", err 457 | } 458 | 459 | if req.Type == "text" { 460 | content = req.Text 461 | } else if req.Type == "image_url" { 462 | if common.IsURL(req.ImageURL.URL) { 463 | url = fmt.Sprintf("%s ", req.ImageURL.URL) 464 | } else if common.IsImageBase64(req.ImageURL.URL) { 465 | imgUrl, err := discord.UploadToDiscordAndGetURL(sendChannelId, req.ImageURL.URL) 466 | if err != nil { 467 | return "", fmt.Errorf("文件上传异常") 468 | } 469 | url = fmt.Sprintf("\n%s ", imgUrl) 470 | } else { 471 | return "", fmt.Errorf("文件格式有误") 472 | } 473 | } else { 474 | return "", fmt.Errorf("消息格式错误") 475 | } 476 | } 477 | 478 | return fmt.Sprintf("%s\n%s", content, url), nil 479 | 480 | } 481 | 482 | // ImagesForOpenAI 图片生成-openai 483 | // @Summary 图片生成-openai 484 | // @Description 图片生成-openai 485 | // @Tags openai 486 | // @Accept json 487 | // @Produce json 488 | // @Param request body model.OpenAIImagesGenerationRequest true "request" 489 | // @Param Authorization header string false "Authorization" 490 | // @Param out-time header string false "out-time" 491 | // @Success 200 {object} model.OpenAIImagesGenerationResponse "Successful response" 492 | // @Router /v1/images/generations [post] 493 | func ImagesForOpenAI(c *gin.Context) { 494 | 495 | var request model.OpenAIImagesGenerationRequest 496 | err := json.NewDecoder(c.Request.Body).Decode(&request) 497 | if err != nil { 498 | common.LogError(c.Request.Context(), err.Error()) 499 | c.JSON(http.StatusInternalServerError, model.OpenAIErrorResponse{ 500 | OpenAIError: model.OpenAIError{ 501 | Message: "Invalid request parameters", 502 | Type: "request_error", 503 | Code: "500", 504 | }, 505 | }) 506 | return 507 | } 508 | 509 | if err := checkUserAuths(c); err != nil { 510 | c.JSON(http.StatusInternalServerError, model.OpenAIErrorResponse{ 511 | OpenAIError: model.OpenAIError{ 512 | Message: err.Error(), 513 | Type: "request_error", 514 | Code: "500", 515 | }, 516 | }) 517 | return 518 | } 519 | 520 | sendChannelId, calledCozeBotId, maxToken, isNewChannel, err := getSendChannelIdAndCozeBotId(c, request.ChannelId, request.Model, true) 521 | if err != nil { 522 | common.LogError(c.Request.Context(), err.Error()) 523 | c.JSON(http.StatusInternalServerError, model.OpenAIErrorResponse{ 524 | OpenAIError: model.OpenAIError{ 525 | Message: "config error", 526 | Type: "request_error", 527 | Code: "500", 528 | }, 529 | }) 530 | return 531 | } 532 | 533 | if isNewChannel { 534 | defer func() { 535 | if discord.ChannelAutoDelTime != "" { 536 | delTime, _ := strconv.Atoi(discord.ChannelAutoDelTime) 537 | if delTime == 0 { 538 | discord.CancelChannelDeleteTimer(sendChannelId) 539 | } else if delTime > 0 { 540 | // 删除该频道 541 | discord.SetChannelDeleteTimer(sendChannelId, time.Duration(delTime)*time.Second) 542 | } 543 | } else { 544 | // 删除该频道 545 | discord.SetChannelDeleteTimer(sendChannelId, 5*time.Second) 546 | } 547 | }() 548 | } 549 | 550 | sentMsg, userAuth, err := discord.SendMessage(c, sendChannelId, calledCozeBotId, common.ImgGeneratePrompt+request.Prompt, maxToken) 551 | if err != nil { 552 | c.JSON(http.StatusInternalServerError, model.OpenAIErrorResponse{ 553 | OpenAIError: model.OpenAIError{ 554 | Message: err.Error(), 555 | Type: "request_error", 556 | Code: "500", 557 | }, 558 | }) 559 | return 560 | } 561 | 562 | replyChan := make(chan model.OpenAIImagesGenerationResponse) 563 | discord.RepliesOpenAIImageChans[sentMsg.ID] = replyChan 564 | defer delete(discord.RepliesOpenAIImageChans, sentMsg.ID) 565 | 566 | stopChan := make(chan model.ChannelStopChan) 567 | discord.ReplyStopChans[sentMsg.ID] = stopChan 568 | defer delete(discord.ReplyStopChans, sentMsg.ID) 569 | 570 | timer, err := setTimerWithHeader(c, false, config.RequestOutTimeDuration) 571 | if err != nil { 572 | common.LogError(c.Request.Context(), err.Error()) 573 | c.JSON(http.StatusBadRequest, gin.H{ 574 | "success": false, 575 | "message": "超时时间设置异常", 576 | }) 577 | return 578 | } 579 | var replyResp model.OpenAIImagesGenerationResponse 580 | for { 581 | select { 582 | case reply := <-replyChan: 583 | if len(reply.Data) > 0 { 584 | if common.SliceContains(common.CozeErrorMessages, reply.Data[0].RevisedPrompt) { 585 | if common.SliceContains(common.CozeUserDailyLimitErrorMessages, reply.Data[0].RevisedPrompt) { 586 | common.LogWarn(c, fmt.Sprintf("USER_AUTHORIZATION:%s DAILY LIMIT", userAuth)) 587 | discord.UserAuthorizations = common.FilterSlice(discord.UserAuthorizations, userAuth) 588 | } 589 | if common.SliceContains(common.CozeCreatorDailyLimitErrorMessages, reply.Data[0].RevisedPrompt) { 590 | common.LogWarn(c, fmt.Sprintf("BOT_ID:%s DAILY LIMIT", calledCozeBotId)) 591 | //discord.BotConfigList = discord.FilterBotConfigByBotId(discord.BotConfigList, calledCozeBotId) 592 | discord.DelLimitBot(calledCozeBotId) 593 | } 594 | c.JSON(http.StatusInternalServerError, model.OpenAIErrorResponse{ 595 | OpenAIError: model.OpenAIError{ 596 | Message: reply.Data[0].RevisedPrompt, 597 | Type: "request_error", 598 | Code: "500", 599 | }, 600 | }) 601 | return 602 | } 603 | if request.ResponseFormat == "b64_json" && reply.Data != nil && len(reply.Data) > 0 { 604 | for _, data := range reply.Data { 605 | if data.URL != "" { 606 | base64Str, err := getBase64ByUrl(data.URL) 607 | if err != nil { 608 | c.JSON(http.StatusInternalServerError, model.OpenAIErrorResponse{ 609 | OpenAIError: model.OpenAIError{ 610 | Message: err.Error(), 611 | Type: "request_error", 612 | Code: "500", 613 | }, 614 | }) 615 | return 616 | } 617 | 618 | data.B64Json = "data:image/webp;base64," + base64Str 619 | } 620 | } 621 | } 622 | } 623 | replyResp = reply 624 | case <-timer.C: 625 | if replyResp.Data == nil { 626 | c.JSON(http.StatusInternalServerError, model.OpenAIErrorResponse{ 627 | OpenAIError: model.OpenAIError{ 628 | Message: "Failed to fetch image URL, please try again later.", 629 | Type: "request_error", 630 | Code: "500", 631 | }, 632 | }) 633 | return 634 | } 635 | c.JSON(http.StatusOK, replyResp) 636 | return 637 | case <-stopChan: 638 | if replyResp.Data == nil { 639 | c.JSON(http.StatusInternalServerError, model.OpenAIErrorResponse{ 640 | OpenAIError: model.OpenAIError{ 641 | Message: "Failed to fetch image URL, please try again later.", 642 | Type: "request_error", 643 | Code: "500", 644 | }, 645 | }) 646 | return 647 | } 648 | c.JSON(http.StatusOK, replyResp) 649 | return 650 | } 651 | } 652 | 653 | } 654 | 655 | func getSendChannelIdAndCozeBotId(c *gin.Context, channelId *string, model string, isOpenAIAPI bool) (sendChannelId string, calledCozeBotId string, maxToken string, isNewChannel bool, err error) { 656 | secret := "" 657 | if isOpenAIAPI { 658 | if secret = c.Request.Header.Get("Authorization"); secret != "" { 659 | secret = strings.Replace(secret, "Bearer ", "", 1) 660 | } 661 | } else { 662 | secret = c.Request.Header.Get("proxy-secret") 663 | } 664 | 665 | // botConfigs不为空 666 | if len(discord.BotConfigList) != 0 { 667 | 668 | botConfigs := discord.FilterConfigs(discord.BotConfigList, secret, model, nil) 669 | if len(botConfigs) != 0 { 670 | // 有值则随机一个 671 | botConfig, err := common.RandomElement(botConfigs) 672 | if err != nil { 673 | return "", "", "", false, err 674 | } 675 | 676 | if channelId != nil && *channelId != "" { 677 | return *channelId, botConfig.CozeBotId, discord.MessageMaxToken, false, nil 678 | } 679 | 680 | if discord.DefaultChannelEnable == "1" { 681 | return botConfig.ChannelId, botConfig.CozeBotId, botConfig.MessageMaxToken, false, nil 682 | } else { 683 | var sendChannelId string 684 | sendChannelId, err := discord.CreateChannelWithRetry(c, discord.GuildId, fmt.Sprintf("cdp-chat-%s", c.Request.Context().Value(common.RequestIdKey)), 0) 685 | if err != nil { 686 | common.LogError(c, err.Error()) 687 | return "", "", "", false, err 688 | } 689 | return sendChannelId, botConfig.CozeBotId, botConfig.MessageMaxToken, true, nil 690 | } 691 | 692 | } 693 | // 没有值抛出异常 694 | return "", "", "", false, &myerr.ModelNotFoundError{ 695 | ErrCode: 500, 696 | Message: fmt.Sprintf("[proxy-secret:%s]+[model:%s]未匹配到有效bot", secret, model), 697 | } 698 | } else { 699 | 700 | if discord.BotConfigExist || discord.CozeBotId == "" { 701 | return "", "", "", false, myerr.ErrNoBotId 702 | } 703 | 704 | if channelId != nil && *channelId != "" { 705 | return *channelId, discord.CozeBotId, discord.MessageMaxToken, false, nil 706 | } 707 | 708 | if discord.DefaultChannelEnable == "1" { 709 | return discord.ChannelId, discord.CozeBotId, discord.MessageMaxToken, false, nil 710 | } else { 711 | sendChannelId, err := discord.CreateChannelWithRetry(c, discord.GuildId, fmt.Sprintf("cdp-chat-%s", c.Request.Context().Value(common.RequestIdKey)), 0) 712 | if err != nil { 713 | //common.LogError(c, myerr.Error()) 714 | return "", "", "", false, err 715 | } 716 | return sendChannelId, discord.CozeBotId, discord.MessageMaxToken, true, nil 717 | } 718 | } 719 | } 720 | 721 | func setTimerWithHeader(c *gin.Context, isStream bool, defaultTimeout time.Duration) (*time.Timer, error) { 722 | 723 | outTimeStr := getOutTimeStr(c, isStream) 724 | 725 | if outTimeStr != "" { 726 | outTime, err := strconv.ParseInt(outTimeStr, 10, 64) 727 | if err != nil { 728 | return nil, err 729 | } 730 | return time.NewTimer(time.Duration(outTime) * time.Second), nil 731 | } 732 | return time.NewTimer(defaultTimeout), nil 733 | } 734 | 735 | func getOutTimeStr(c *gin.Context, isStream bool) string { 736 | var outTimeStr string 737 | if outTime := c.GetHeader(common.OutTime); outTime != "" { 738 | outTimeStr = outTime 739 | } else { 740 | if isStream { 741 | outTimeStr = config.StreamRequestOutTime 742 | } else { 743 | outTimeStr = config.RequestOutTime 744 | } 745 | } 746 | return outTimeStr 747 | } 748 | 749 | func timerReset(c *gin.Context, isStream bool, timer *time.Timer, defaultTimeout time.Duration) error { 750 | 751 | outTimeStr := getOutTimeStr(c, isStream) 752 | 753 | if outTimeStr != "" { 754 | outTime, err := strconv.ParseInt(outTimeStr, 10, 64) 755 | if err != nil { 756 | return err 757 | } 758 | timer.Reset(time.Duration(outTime) * time.Second) 759 | return nil 760 | } 761 | timer.Reset(defaultTimeout) 762 | return nil 763 | } 764 | 765 | func checkUserAuths(c *gin.Context) error { 766 | if len(discord.UserAuthorizations) == 0 { 767 | common.LogError(c, fmt.Sprintf("无可用的 user_auth")) 768 | // tg发送通知 769 | if !common.IsSameDay(discord.NoAvailableUserAuthPreNotifyTime, time.Now()) && telegram.NotifyTelegramBotToken != "" && telegram.TgBot != nil { 770 | go func() { 771 | discord.NoAvailableUserAuthChan <- "stop" 772 | }() 773 | } 774 | 775 | return fmt.Errorf("no_available_user_auth") 776 | } 777 | return nil 778 | } 779 | 780 | func getBase64ByUrl(url string) (string, error) { 781 | resp, err := http.Get(url) 782 | if err != nil { 783 | return "", fmt.Errorf("failed to fetch image: %w", err) 784 | } 785 | defer resp.Body.Close() 786 | 787 | if resp.StatusCode != http.StatusOK { 788 | return "", fmt.Errorf("received non-200 status code: %d", resp.StatusCode) 789 | } 790 | 791 | imgData, err := io.ReadAll(resp.Body) 792 | if err != nil { 793 | return "", fmt.Errorf("failed to read image data: %w", err) 794 | } 795 | 796 | // Encode the image data to Base64 797 | base64Str := base64.StdEncoding.EncodeToString(imgData) 798 | return base64Str, nil 799 | } 800 | -------------------------------------------------------------------------------- /controller/thread.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "coze-discord-proxy/discord" 5 | "coze-discord-proxy/model" 6 | "encoding/json" 7 | "github.com/gin-gonic/gin" 8 | "net/http" 9 | ) 10 | 11 | // ThreadCreate 创建线程 12 | // @Summary 创建线程 13 | // @Description 创建线程 14 | // @Tags thread 15 | // @Accept json 16 | // @Produce json 17 | // @Param threadModel body model.ThreadReq true "threadModel" 18 | // @Success 200 {object} model.ThreadResp "Successful response" 19 | // @Router /api/thread/create [post] 20 | func ThreadCreate(c *gin.Context) { 21 | var threadModel model.ThreadReq 22 | err := json.NewDecoder(c.Request.Body).Decode(&threadModel) 23 | if err != nil { 24 | c.JSON(http.StatusOK, gin.H{ 25 | "success": false, 26 | "message": "无效的参数", 27 | }) 28 | return 29 | } 30 | 31 | if threadModel.ArchiveDuration != 60 && threadModel.ArchiveDuration != 1440 && threadModel.ArchiveDuration != 4320 && threadModel.ArchiveDuration != 10080 { 32 | c.JSON(http.StatusOK, gin.H{ 33 | "success": false, 34 | "message": "线程创建时间只可为[60,1440,4320,10080]", 35 | }) 36 | return 37 | } 38 | 39 | threadId, err := discord.ThreadStart(threadModel.ChannelId, threadModel.Name, threadModel.ArchiveDuration) 40 | 41 | if err != nil { 42 | c.JSON(http.StatusOK, gin.H{ 43 | "success": false, 44 | "message": "discord创建线程异常", 45 | }) 46 | } else { 47 | var thread model.ThreadResp 48 | thread.Id = threadId 49 | thread.Name = threadModel.Name 50 | c.JSON(http.StatusOK, gin.H{ 51 | "success": true, 52 | "data": thread, 53 | }) 54 | } 55 | return 56 | } 57 | -------------------------------------------------------------------------------- /discord/channel.go: -------------------------------------------------------------------------------- 1 | package discord 2 | 3 | import ( 4 | "context" 5 | "coze-discord-proxy/common" 6 | "coze-discord-proxy/common/config" 7 | "coze-discord-proxy/telegram" 8 | "fmt" 9 | "github.com/bwmarrin/discordgo" 10 | "github.com/gin-gonic/gin" 11 | "strings" 12 | "sync" 13 | "time" 14 | ) 15 | 16 | var ( 17 | channelTimers sync.Map // 用于存储频道ID和对应的定时器 18 | ) 19 | 20 | // SetChannelDeleteTimer 设置或重置频道的删除定时器 21 | func SetChannelDeleteTimer(channelId string, duration time.Duration) { 22 | 23 | channel, err := Session.Channel(channelId) 24 | // 非自动生成频道不删除 25 | if err == nil && !strings.HasPrefix(channel.Name, "cdp-chat-") { 26 | return 27 | } 28 | 29 | // 过滤掉配置中的频道id 30 | for _, config := range BotConfigList { 31 | if config.ChannelId == channelId { 32 | return 33 | } 34 | } 35 | 36 | if ChannelId == channelId { 37 | return 38 | } 39 | 40 | // 检查是否已存在定时器 41 | if timer, ok := channelTimers.Load(channelId); ok { 42 | if timer.(*time.Timer).Stop() { 43 | // 仅当定时器成功停止时才从映射中删除 44 | channelTimers.Delete(channelId) 45 | } 46 | } 47 | 48 | // 设置新的定时器 49 | newTimer := time.AfterFunc(duration, func() { 50 | ChannelDel(channelId) 51 | // 删除完成后从map中移除 52 | channelTimers.Delete(channelId) 53 | }) 54 | // 存储新的定时器 55 | channelTimers.Store(channelId, newTimer) 56 | } 57 | 58 | // CancelChannelDeleteTimer 取消频道的删除定时器 59 | func CancelChannelDeleteTimer(channelId string) { 60 | // 尝试从映射中获取定时器 61 | if timer, ok := channelTimers.Load(channelId); ok { 62 | // 如果定时器存在,尝试停止它 63 | if timer.(*time.Timer).Stop() { 64 | // 定时器成功停止后,从映射中移除 65 | channelTimers.Delete(channelId) 66 | } else { 67 | common.SysError(fmt.Sprintf("定时器无法停止或已触发,频道可能已被删除:%s", channelId)) 68 | } 69 | } else { 70 | //common.SysError(fmt.Sprintf("频道无定时删除:%s", channelId)) 71 | } 72 | } 73 | 74 | func ChannelCreate(guildID, channelName string, channelType int) (string, error) { 75 | // 创建新的频道 76 | st, err := Session.GuildChannelCreate(guildID, channelName, discordgo.ChannelType(channelType)) 77 | if err != nil { 78 | return "", err 79 | } 80 | return st.ID, nil 81 | } 82 | 83 | func ChannelDel(channelId string) (string, error) { 84 | // 删除频道 85 | st, err := Session.ChannelDelete(channelId) 86 | if err != nil { 87 | common.LogError(context.Background(), fmt.Sprintf("删除频道时异常 %s", err.Error())) 88 | return "", err 89 | } 90 | return st.ID, nil 91 | } 92 | 93 | func ChannelCreateComplex(guildID, parentId, channelName string, channelType int) (string, error) { 94 | // 创建新的子频道 95 | st, err := Session.GuildChannelCreateComplex(guildID, discordgo.GuildChannelCreateData{ 96 | Name: channelName, 97 | Type: discordgo.ChannelType(channelType), 98 | ParentID: parentId, 99 | }) 100 | if err != nil { 101 | common.LogError(context.Background(), fmt.Sprintf("创建子频道时异常 %s", err.Error())) 102 | return "", err 103 | } 104 | return st.ID, nil 105 | } 106 | 107 | type channelCreateResult struct { 108 | ID string 109 | Err error 110 | } 111 | 112 | func CreateChannelWithRetry(c *gin.Context, guildID, channelName string, channelType int) (string, error) { 113 | 114 | for attempt := 1; attempt <= 3; attempt++ { 115 | resultChan := make(chan channelCreateResult, 1) 116 | 117 | go func() { 118 | id, err := ChannelCreate(guildID, channelName, channelType) 119 | resultChan <- channelCreateResult{ 120 | ID: id, 121 | Err: err, 122 | } 123 | }() 124 | 125 | // 设置超时时间为60秒 126 | select { 127 | case result := <-resultChan: 128 | if result.Err != nil { 129 | if strings.Contains(result.Err.Error(), "Maximum number of server channels reached") { 130 | 131 | var ok bool 132 | var err error 133 | 134 | if config.MaxChannelDelType == "ALL" { 135 | // 频道已满 删除所有频道 136 | ok, err = ChannelDelAllForCdp(c) 137 | } else if config.MaxChannelDelType == "OLDEST" { 138 | // 频道已满 删除最旧频道 139 | ok, err = ChannelDelOldestForCdp(c) 140 | } else { 141 | return "", result.Err 142 | } 143 | if err != nil { 144 | return "", err 145 | } 146 | if ok { 147 | return ChannelCreate(guildID, channelName, channelType) 148 | } else { 149 | return "", fmt.Errorf("当前Discord服务器频道已满,且无CDP临时频道可删除") 150 | } 151 | } else { 152 | return "", result.Err 153 | } 154 | } 155 | // 成功创建频道,返回结果 156 | return result.ID, nil 157 | case <-time.After(60 * time.Second): 158 | common.LogWarn(c, fmt.Sprintf("Create channel timed out, retrying...%v", attempt)) 159 | } 160 | } 161 | // tg发送通知 162 | if !common.IsSameDay(CreateChannelRiskPreNotifyTime, time.Now()) && telegram.NotifyTelegramBotToken != "" && telegram.TgBot != nil { 163 | go func() { 164 | CreateChannelRiskChan <- "stop" 165 | }() 166 | } 167 | // 所有尝试后仍失败,返回最后的错误 168 | return "", fmt.Errorf("failed after 3 attempts due to timeout, please reset BOT_TOKEN") 169 | } 170 | 171 | func ChannelDelAllForCdp(c *gin.Context) (bool, error) { 172 | // 获取服务器内所有频道的信息 173 | channels, err := Session.GuildChannels(GuildId) 174 | if err != nil { 175 | common.LogError(c, fmt.Sprintf("服务器Id查询频道失败 %s", err.Error())) 176 | return false, err 177 | } 178 | 179 | flag := false 180 | 181 | // 遍历所有频道 182 | for _, channel := range channels { 183 | 184 | // 过滤掉配置中的频道id 185 | for _, config := range BotConfigList { 186 | if config.ChannelId == channel.ID { 187 | continue 188 | } 189 | } 190 | 191 | if ChannelId == channel.ID { 192 | continue 193 | } 194 | 195 | // 检查频道名是否以"cdp-"开头 196 | if strings.HasPrefix(channel.Name, "cdp-chat-") { 197 | // 删除该频道 198 | _, err := Session.ChannelDelete(channel.ID) 199 | if err != nil { 200 | common.LogError(c, fmt.Sprintf("频道数量已满-删除频道异常(可能原因:对话请求频道已被自动删除) %s", err.Error())) 201 | return false, err 202 | } 203 | common.LogWarn(c, fmt.Sprintf("频道数量已满-自动删除频道Id %s", channel.ID)) 204 | flag = true 205 | } 206 | } 207 | return flag, nil 208 | } 209 | 210 | func ChannelDelOldestForCdp(c *gin.Context) (bool, error) { 211 | // 获取服务器内所有频道的信息 212 | channels, err := Session.GuildChannels(GuildId) 213 | if err != nil { 214 | common.LogError(c, fmt.Sprintf("服务器Id查询频道失败 %s", err.Error())) 215 | return false, err 216 | } 217 | 218 | // 遍历所有频道 219 | for _, channel := range channels { 220 | 221 | // 过滤掉配置中的频道id 222 | for _, config := range BotConfigList { 223 | if config.ChannelId == channel.ID { 224 | continue 225 | } 226 | } 227 | 228 | if ChannelId == channel.ID { 229 | continue 230 | } 231 | 232 | // 检查频道名是否以"cdp-"开头 233 | if strings.HasPrefix(channel.Name, "cdp-chat-") { 234 | // 删除该频道 235 | _, err := Session.ChannelDelete(channel.ID) 236 | if err != nil { 237 | common.LogError(c, fmt.Sprintf("频道数量已满-删除频道异常(可能原因:对话请求频道已被自动删除) %s", err.Error())) 238 | return false, err 239 | } 240 | common.LogWarn(c, fmt.Sprintf("频道数量已满-自动删除频道Id %s", channel.ID)) 241 | return true, nil 242 | } 243 | } 244 | return false, nil 245 | } 246 | -------------------------------------------------------------------------------- /discord/discord.go: -------------------------------------------------------------------------------- 1 | package discord 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "coze-discord-proxy/common" 7 | "coze-discord-proxy/common/myerr" 8 | "coze-discord-proxy/model" 9 | "coze-discord-proxy/telegram" 10 | "encoding/base64" 11 | "encoding/json" 12 | "errors" 13 | "fmt" 14 | "github.com/bwmarrin/discordgo" 15 | "github.com/gin-gonic/gin" 16 | tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" 17 | "github.com/h2non/filetype" 18 | "golang.org/x/net/proxy" 19 | "log" 20 | "math/rand" 21 | "net" 22 | "net/http" 23 | "net/url" 24 | "os" 25 | "os/signal" 26 | "strconv" 27 | "strings" 28 | "syscall" 29 | "time" 30 | ) 31 | 32 | var BotToken = os.Getenv("BOT_TOKEN") 33 | var CozeBotId = os.Getenv("COZE_BOT_ID") 34 | var GuildId = os.Getenv("GUILD_ID") 35 | var ChannelId = os.Getenv("CHANNEL_ID") 36 | var DefaultChannelEnable = os.Getenv("DEFAULT_CHANNEL_ENABLE") 37 | var MessageMaxToken = os.Getenv("MESSAGE_MAX_TOKEN") 38 | var ProxyUrl = os.Getenv("PROXY_URL") 39 | var ChannelAutoDelTime = os.Getenv("CHANNEL_AUTO_DEL_TIME") 40 | var CozeBotStayActiveEnable = os.Getenv("COZE_BOT_STAY_ACTIVE_ENABLE") 41 | var UserAgent = os.Getenv("USER_AGENT") 42 | var UserAuthorization = os.Getenv("USER_AUTHORIZATION") 43 | var UserAuthorizations = strings.Split(UserAuthorization, ",") 44 | 45 | var NoAvailableUserAuthChan = make(chan string) 46 | var CreateChannelRiskChan = make(chan string) 47 | 48 | var NoAvailableUserAuthPreNotifyTime time.Time 49 | var CreateChannelRiskPreNotifyTime time.Time 50 | 51 | var BotConfigList []*model.BotConfig 52 | var BotConfigExist bool 53 | 54 | var RepliesChans = make(map[string]chan model.ReplyResp) 55 | var RepliesOpenAIChans = make(map[string]*model.OpenAIChatCompletionChan) 56 | var RepliesOpenAIImageChans = make(map[string]chan model.OpenAIImagesGenerationResponse) 57 | 58 | var ReplyStopChans = make(map[string]chan model.ChannelStopChan) 59 | var Session *discordgo.Session 60 | 61 | func StartBot(ctx context.Context, token string) { 62 | var err error 63 | Session, err = discordgo.New("Bot " + token) 64 | 65 | if err != nil { 66 | common.FatalLog("error creating Discord session,", err) 67 | return 68 | } 69 | 70 | if ProxyUrl != "" { 71 | proxyParse, client, err := NewProxyClient(ProxyUrl) 72 | if err != nil { 73 | common.FatalLog("error creating proxy client,", err) 74 | } 75 | Session.Client = client 76 | Session.Dialer.Proxy = http.ProxyURL(proxyParse) 77 | common.SysLog("Proxy Set Success!") 78 | } 79 | // 注册消息处理函数 80 | Session.AddHandler(messageCreate) 81 | Session.AddHandler(messageUpdate) 82 | 83 | // 打开websocket连接并开始监听 84 | err = Session.Open() 85 | if err != nil { 86 | common.FatalLog("error opening connection,", err) 87 | return 88 | } 89 | // 读取机器人配置文件 90 | loadBotConfig() 91 | // 验证docker配置文件 92 | checkEnvVariable() 93 | common.SysLog("Bot is now running. Enjoy It.") 94 | 95 | // 每日9点 重新加载BotConfig 96 | go loadBotConfigTask() 97 | // 每日9.5点 重新加载userAuth 98 | go loadUserAuthTask() 99 | 100 | if CozeBotStayActiveEnable == "1" || CozeBotStayActiveEnable == "" { 101 | // 开启coze保活任务 每日9.10 102 | go stayActiveMessageTask() 103 | } 104 | 105 | if telegram.NotifyTelegramBotToken != "" && telegram.TgBot != nil { 106 | // 开启tgbot消息推送任务 107 | go telegramNotifyMsgTask() 108 | } 109 | 110 | go func() { 111 | <-ctx.Done() 112 | if err := Session.Close(); err != nil { 113 | common.FatalLog("error closing Discord session,", err) 114 | } 115 | }() 116 | 117 | // 等待信号 118 | sc := make(chan os.Signal, 1) 119 | signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill) 120 | <-sc 121 | } 122 | 123 | func loadBotConfigTask() { 124 | for { 125 | source := rand.NewSource(time.Now().UnixNano()) 126 | randomNumber := rand.New(source).Intn(60) // 生成0到60之间的随机整数 127 | 128 | // 计算距离下一个时间间隔 129 | now := time.Now() 130 | next := time.Date(now.Year(), now.Month(), now.Day(), 9, 5, 0, 0, now.Location()) 131 | 132 | // 如果当前时间已经超过9点,那么等待到第二天的9点 133 | if now.After(next) { 134 | next = next.Add(24 * time.Hour) 135 | } 136 | 137 | delay := next.Sub(now) 138 | 139 | // 等待直到下一个间隔 140 | time.Sleep(delay + time.Duration(randomNumber)*time.Second) 141 | 142 | common.SysLog("CDP Scheduled loadBotConfig Task Job Start!") 143 | loadBotConfig() 144 | common.SysLog("CDP Scheduled loadBotConfig Task Job End!") 145 | 146 | } 147 | } 148 | 149 | func telegramNotifyMsgTask() { 150 | 151 | for NoAvailableUserAuthChan != nil || CreateChannelRiskChan != nil { 152 | select { 153 | case msg, ok := <-NoAvailableUserAuthChan: 154 | if ok && msg == "stop" { 155 | tgMsgConfig := tgbotapi.NewMessage(telegram.NotifyTelegramUserIdInt64, fmt.Sprintf("⚠️【CDP-服务通知】\n服务已无可用USER_AUTHORIZATION,请及时更换!")) 156 | err := telegram.SendMessage(&tgMsgConfig) 157 | if err != nil { 158 | common.LogWarn(nil, fmt.Sprintf("Telegram 推送消息异常 error:%s", err.Error())) 159 | } else { 160 | //NoAvailableUserAuthChan = nil // 停止监听ch1 161 | NoAvailableUserAuthPreNotifyTime = time.Now() 162 | } 163 | } else if !ok { 164 | NoAvailableUserAuthChan = nil // 如果ch1已关闭,停止监听 165 | } 166 | case msg, ok := <-CreateChannelRiskChan: 167 | if ok && msg == "stop" { 168 | tgMsgConfig := tgbotapi.NewMessage(telegram.NotifyTelegramUserIdInt64, fmt.Sprintf("⚠️【CDP-服务通知】\n服务BOT_TOKEN关联的BOT已被风控,请及时ResetToken并更换!")) 169 | err := telegram.SendMessage(&tgMsgConfig) 170 | if err != nil { 171 | common.LogWarn(nil, fmt.Sprintf("Telegram 推送消息异常 error:%s", err.Error())) 172 | } else { 173 | //CreateChannelRiskChan = nil 174 | CreateChannelRiskPreNotifyTime = time.Now() 175 | } 176 | } else if !ok { 177 | CreateChannelRiskChan = nil 178 | } 179 | } 180 | } 181 | 182 | } 183 | 184 | func loadUserAuthTask() { 185 | for { 186 | source := rand.NewSource(time.Now().UnixNano()) 187 | randomNumber := rand.New(source).Intn(60) // 生成0到60之间的随机整数 188 | 189 | // 计算距离下一个时间间隔 190 | now := time.Now() 191 | next := time.Date(now.Year(), now.Month(), now.Day(), 9, 5, 0, 0, now.Location()) 192 | 193 | // 如果当前时间已经超过9点,那么等待到第二天的9点 194 | if now.After(next) { 195 | next = next.Add(24 * time.Hour) 196 | } 197 | 198 | delay := next.Sub(now) 199 | 200 | // 等待直到下一个间隔 201 | time.Sleep(delay + time.Duration(randomNumber)*time.Second) 202 | 203 | common.SysLog("CDP Scheduled loadUserAuth Task Job Start!") 204 | UserAuthorizations = strings.Split(UserAuthorization, ",") 205 | common.LogInfo(context.Background(), fmt.Sprintf("UserAuths: %+v", UserAuthorizations)) 206 | common.SysLog("CDP Scheduled loadUserAuth Task Job End!") 207 | 208 | } 209 | } 210 | 211 | func checkEnvVariable() { 212 | if UserAuthorization == "" { 213 | common.FatalLog("环境变量 USER_AUTHORIZATION 未设置") 214 | } 215 | if BotToken == "" { 216 | common.FatalLog("环境变量 BOT_TOKEN 未设置") 217 | } 218 | if GuildId == "" { 219 | common.FatalLog("环境变量 GUILD_ID 未设置") 220 | } 221 | if DefaultChannelEnable == "1" && ChannelId == "" { 222 | common.FatalLog("环境变量 CHANNEL_ID 未设置") 223 | } else if DefaultChannelEnable == "0" || DefaultChannelEnable == "" { 224 | ChannelId = "" 225 | } 226 | if CozeBotId == "" { 227 | common.FatalLog("环境变量 COZE_BOT_ID 未设置") 228 | } else if Session.State.User.ID == CozeBotId { 229 | common.FatalLog("环境变量 COZE_BOT_ID 不可为当前服务 BOT_TOKEN 关联的 BOT_ID") 230 | } 231 | 232 | if ProxyUrl != "" { 233 | _, _, err := NewProxyClient(ProxyUrl) 234 | if err != nil { 235 | common.FatalLog("环境变量 PROXY_URL 设置有误") 236 | } 237 | } 238 | if ChannelAutoDelTime != "" { 239 | _, err := strconv.Atoi(ChannelAutoDelTime) 240 | if err != nil { 241 | common.FatalLog("环境变量 CHANNEL_AUTO_DEL_TIME 设置有误") 242 | } 243 | } 244 | 245 | if MessageMaxToken == "" { 246 | MessageMaxToken = strconv.Itoa(128 * 1000) 247 | } else { 248 | _, err := strconv.Atoi(MessageMaxToken) 249 | if err != nil { 250 | common.FatalLog("环境变量 MESSAGE_MAX_TOKEN 设置有误") 251 | } 252 | } 253 | 254 | if telegram.NotifyTelegramBotToken != "" { 255 | err := telegram.InitTelegramBot() 256 | if err != nil { 257 | common.FatalLog(fmt.Sprintf("环境变量 NotifyTelegramBotToken 设置有误 error:%s", err.Error())) 258 | } 259 | 260 | if telegram.NotifyTelegramUserId == "" { 261 | common.FatalLog("环境变量 NOTIFY_TELEGRAM_USER_ID 未设置") 262 | } else { 263 | telegram.NotifyTelegramUserIdInt64, err = strconv.ParseInt(telegram.NotifyTelegramUserId, 10, 64) 264 | if err != nil { 265 | common.FatalLog(fmt.Sprintf("环境变量 NOTIFY_TELEGRAM_USER_ID 设置有误 error:%s", err.Error())) 266 | } 267 | } 268 | } 269 | 270 | common.SysLog("Environment variable check passed.") 271 | } 272 | 273 | func loadBotConfig() { 274 | // 检查文件是否存在 275 | _, err := os.Stat("config/bot_config.json") 276 | if err != nil { 277 | if !os.IsNotExist(err) { 278 | common.SysError("载入bot_config.json文件异常") 279 | } 280 | BotConfigExist = false 281 | return 282 | } 283 | 284 | // 读取文件 285 | file, err := os.ReadFile("config/bot_config.json") 286 | if err != nil { 287 | common.FatalLog("error reading bot config file,", err) 288 | } 289 | if len(file) == 0 { 290 | return 291 | } 292 | 293 | // 解析JSON到结构体切片 并载入内存 294 | err = json.Unmarshal(file, &BotConfigList) 295 | if err != nil { 296 | common.FatalLog("Error parsing JSON:", err) 297 | } 298 | 299 | for _, botConfig := range BotConfigList { 300 | // 校验默认频道 301 | if DefaultChannelEnable == "1" && botConfig.ChannelId == "" { 302 | common.FatalLog("默认频道开关开启时,必须为每个Coze-Bot配置ChannelId") 303 | } 304 | // 校验MaxToken 305 | if botConfig.MessageMaxToken != "" { 306 | _, err := strconv.Atoi(botConfig.MessageMaxToken) 307 | if err != nil { 308 | common.FatalLog(fmt.Sprintf("messageMaxToken 必须为数字!")) 309 | } 310 | } else { 311 | botConfig.MessageMaxToken = strconv.Itoa(128 * 1000) 312 | } 313 | } 314 | 315 | BotConfigExist = true 316 | 317 | // 将结构体切片转换为 JSON 字符串 318 | jsonData, err := json.MarshalIndent(BotConfigList, "", " ") 319 | if err != nil { 320 | common.FatalLog(fmt.Sprintf("Error marshalling BotConfigs: %v", err)) 321 | } 322 | 323 | // 打印 JSON 字符串 324 | common.SysLog(fmt.Sprintf("载入配置文件成功 BotConfigs:\n%s", string(jsonData))) 325 | } 326 | 327 | // messageCreate handles the create messages in Discord. 328 | func messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { 329 | // 提前检查参考消息是否为 nil 330 | if m.ReferencedMessage == nil { 331 | return 332 | } 333 | 334 | // 尝试获取 stopChan 335 | stopChan, exists := ReplyStopChans[m.ReferencedMessage.ID] 336 | if !exists { 337 | //channel, myerr := Session.Channel(m.ChannelID) 338 | // 不存在则直接删除频道 339 | //if myerr != nil || strings.HasPrefix(channel.Name, "cdp-chat-") { 340 | //SetChannelDeleteTimer(m.ChannelID, 5*time.Minute) 341 | return 342 | //} 343 | } 344 | 345 | // 如果作者为 nil 或消息来自 bot 本身,则发送停止信号 346 | if m.Author == nil || m.Author.ID == s.State.User.ID { 347 | //SetChannelDeleteTimer(m.ChannelID, 5*time.Minute) 348 | stopChan <- model.ChannelStopChan{ 349 | Id: m.ChannelID, 350 | } 351 | return 352 | } 353 | 354 | replyChan, exists := RepliesChans[m.ReferencedMessage.ID] 355 | if exists { 356 | reply := processMessageCreate(m) 357 | replyChan <- reply 358 | } else { 359 | replyOpenAIChan, exists := RepliesOpenAIChans[m.ReferencedMessage.ID] 360 | if exists { 361 | reply := processMessageCreateForOpenAI(m) 362 | reply.Model = replyOpenAIChan.Model 363 | replyOpenAIChan.Response <- reply 364 | } else { 365 | replyOpenAIImageChan, exists := RepliesOpenAIImageChans[m.ReferencedMessage.ID] 366 | if exists { 367 | reply := processMessageCreateForOpenAIImage(m) 368 | replyOpenAIImageChan <- reply 369 | } else { 370 | return 371 | } 372 | } 373 | } 374 | // data: {"id":"chatcmpl-8lho2xvdDFyBdFkRwWAcMpWWAgymJ","object":"chat.completion.chunk","created":1706380498,"model":"gpt-4-turbo-0613","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"?"},"logprobs":null,"finish_reason":null}]} 375 | // data :{"id":"1200873365351698694","object":"chat.completion.chunk","created":1706380922,"model":"COZE","choices":[{"index":0,"message":{"role":"assistant","content":"你好!有什么我可以帮您的吗?如果有任"},"logprobs":null,"finish_reason":"","delta":{"content":"吗?如果有任"}}],"usage":{"prompt_tokens":13,"completion_tokens":19,"total_tokens":32},"system_fingerprint":null} 376 | 377 | // 如果消息包含组件或嵌入,则发送停止信号 378 | if len(m.Message.Components) > 0 { 379 | 380 | var suggestions []string 381 | 382 | actionRow, _ := m.Message.Components[0].(*discordgo.ActionsRow) 383 | for _, component := range actionRow.Components { 384 | button := component.(*discordgo.Button) 385 | suggestions = append(suggestions, button.Label) 386 | } 387 | 388 | replyOpenAIChan, exists := RepliesOpenAIChans[m.ReferencedMessage.ID] 389 | if exists { 390 | reply := processMessageCreateForOpenAI(m) 391 | stopStr := "stop" 392 | reply.Choices[0].FinishReason = &stopStr 393 | reply.Suggestions = suggestions 394 | reply.Model = replyOpenAIChan.Model 395 | replyOpenAIChan.Response <- reply 396 | } 397 | 398 | replyOpenAIImageChan, exists := RepliesOpenAIImageChans[m.ReferencedMessage.ID] 399 | if exists { 400 | reply := processMessageCreateForOpenAIImage(m) 401 | reply.Suggestions = suggestions 402 | replyOpenAIImageChan <- reply 403 | } 404 | 405 | stopChan <- model.ChannelStopChan{ 406 | Id: m.ChannelID, 407 | } 408 | } 409 | 410 | return 411 | } 412 | 413 | // messageUpdate handles the updated messages in Discord. 414 | func messageUpdate(s *discordgo.Session, m *discordgo.MessageUpdate) { 415 | // 提前检查参考消息是否为 nil 416 | if m.ReferencedMessage == nil { 417 | return 418 | } 419 | 420 | // 尝试获取 stopChan 421 | stopChan, exists := ReplyStopChans[m.ReferencedMessage.ID] 422 | if !exists { 423 | channel, err := Session.Channel(m.ChannelID) 424 | // 不存在则直接删除频道 425 | if err != nil || strings.HasPrefix(channel.Name, "cdp-chat-") { 426 | return 427 | } 428 | } 429 | 430 | // 如果作者为 nil 或消息来自 bot 本身,则发送停止信号 431 | if m.Author == nil || m.Author.ID == s.State.User.ID { 432 | stopChan <- model.ChannelStopChan{ 433 | Id: m.ChannelID, 434 | } 435 | return 436 | } 437 | 438 | replyChan, exists := RepliesChans[m.ReferencedMessage.ID] 439 | if exists { 440 | reply := processMessageUpdate(m) 441 | replyChan <- reply 442 | } else { 443 | replyOpenAIChan, exists := RepliesOpenAIChans[m.ReferencedMessage.ID] 444 | if exists { 445 | reply := processMessageUpdateForOpenAI(m) 446 | reply.Model = replyOpenAIChan.Model 447 | replyOpenAIChan.Response <- reply 448 | } else { 449 | replyOpenAIImageChan, exists := RepliesOpenAIImageChans[m.ReferencedMessage.ID] 450 | if exists { 451 | reply := processMessageUpdateForOpenAIImage(m) 452 | replyOpenAIImageChan <- reply 453 | } else { 454 | return 455 | } 456 | } 457 | } 458 | // data: {"id":"chatcmpl-8lho2xvdDFyBdFkRwWAcMpWWAgymJ","object":"chat.completion.chunk","created":1706380498,"model":"gpt-4-turbo-0613","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"?"},"logprobs":null,"finish_reason":null}]} 459 | // data :{"id":"1200873365351698694","object":"chat.completion.chunk","created":1706380922,"model":"COZE","choices":[{"index":0,"message":{"role":"assistant","content":"你好!有什么我可以帮您的吗?如果有任"},"logprobs":null,"finish_reason":"","delta":{"content":"吗?如果有任"}}],"usage":{"prompt_tokens":13,"completion_tokens":19,"total_tokens":32},"system_fingerprint":null} 460 | 461 | // 如果消息包含组件或嵌入,则发送停止信号 462 | if len(m.Message.Components) > 0 { 463 | 464 | var suggestions []string 465 | 466 | actionRow, _ := m.Message.Components[0].(*discordgo.ActionsRow) 467 | for _, component := range actionRow.Components { 468 | button := component.(*discordgo.Button) 469 | suggestions = append(suggestions, button.Label) 470 | } 471 | 472 | replyOpenAIChan, exists := RepliesOpenAIChans[m.ReferencedMessage.ID] 473 | if exists { 474 | reply := processMessageUpdateForOpenAI(m) 475 | stopStr := "stop" 476 | reply.Choices[0].FinishReason = &stopStr 477 | reply.Suggestions = suggestions 478 | reply.Model = replyOpenAIChan.Model 479 | replyOpenAIChan.Response <- reply 480 | } 481 | 482 | replyOpenAIImageChan, exists := RepliesOpenAIImageChans[m.ReferencedMessage.ID] 483 | if exists { 484 | reply := processMessageUpdateForOpenAIImage(m) 485 | reply.Suggestions = suggestions 486 | replyOpenAIImageChan <- reply 487 | } 488 | 489 | stopChan <- model.ChannelStopChan{ 490 | Id: m.ChannelID, 491 | } 492 | } 493 | 494 | return 495 | } 496 | 497 | func SendMessage(c *gin.Context, channelID, cozeBotId, message, maxToken string) (*discordgo.Message, string, error) { 498 | var ctx context.Context 499 | if c == nil { 500 | ctx = context.Background() 501 | } else { 502 | ctx = c.Request.Context() 503 | } 504 | 505 | if Session == nil { 506 | common.LogError(ctx, "discord session is nil") 507 | return nil, "", fmt.Errorf("discord session not initialized") 508 | } 509 | 510 | //var sentMsg *discordgo.Message 511 | 512 | content := fmt.Sprintf("%s \n <@%s>", message, cozeBotId) 513 | 514 | content = strings.Replace(content, `\u0026`, "&", -1) 515 | content = strings.Replace(content, `\u003c`, "<", -1) 516 | content = strings.Replace(content, `\u003e`, ">", -1) 517 | 518 | tokens := common.CountTokens(content) 519 | maxTokenInt, err := strconv.Atoi(maxToken) 520 | if err != nil { 521 | common.LogError(ctx, fmt.Sprintf("error sending message: %s", err)) 522 | return &discordgo.Message{}, "", fmt.Errorf("error sending message") 523 | } 524 | 525 | if tokens > maxTokenInt { 526 | common.LogError(ctx, fmt.Sprintf("prompt已超过限制,请分段发送 [%v] %s", tokens, content)) 527 | return nil, "", fmt.Errorf("prompt已超过限制,请分段发送 [%v]", tokens) 528 | } 529 | 530 | if len(UserAuthorizations) == 0 { 531 | //SetChannelDeleteTimer(channelID, 5*time.Second) 532 | common.LogError(ctx, fmt.Sprintf("无可用的 user_auth")) 533 | 534 | // tg发送通知 535 | if !common.IsSameDay(NoAvailableUserAuthPreNotifyTime, time.Now()) && telegram.NotifyTelegramBotToken != "" && telegram.TgBot != nil { 536 | go func() { 537 | NoAvailableUserAuthChan <- "stop" 538 | }() 539 | } 540 | 541 | return nil, "", fmt.Errorf("no_available_user_auth") 542 | } 543 | 544 | userAuth, err := common.RandomElement(UserAuthorizations) 545 | if err != nil { 546 | return nil, "", err 547 | } 548 | 549 | for i, sendContent := range common.ReverseSegment(content, 1990) { 550 | //sentMsg, myerr := Session.ChannelMessageSend(channelID, sendContent) 551 | //sentMsgId := sentMsg.ID 552 | // 4.0.0 版本下 用户端发送消息 553 | sendContent = strings.ReplaceAll(sendContent, "\\n", "\n") 554 | sentMsgId, err := SendMsgByAuthorization(c, userAuth, sendContent, channelID) 555 | if err != nil { 556 | var myErr *myerr.DiscordUnauthorizedError 557 | if errors.As(err, &myErr) { 558 | // 无效则将此 auth 移除 559 | UserAuthorizations = common.FilterSlice(UserAuthorizations, userAuth) 560 | return SendMessage(c, channelID, cozeBotId, message, maxToken) 561 | } 562 | common.LogError(ctx, fmt.Sprintf("error sending message: %s", err)) 563 | return nil, "", fmt.Errorf("error sending message") 564 | } 565 | 566 | time.Sleep(1 * time.Second) 567 | 568 | if i == len(common.ReverseSegment(content, 1990))-1 { 569 | return &discordgo.Message{ 570 | ID: sentMsgId, 571 | }, userAuth, nil 572 | } 573 | } 574 | return &discordgo.Message{}, "", fmt.Errorf("error sending message") 575 | } 576 | 577 | func ThreadStart(channelId, threadName string, archiveDuration int) (string, error) { 578 | // 创建新的线程 579 | th, err := Session.ThreadStart(channelId, threadName, discordgo.ChannelTypeGuildText, archiveDuration) 580 | 581 | if err != nil { 582 | common.LogError(context.Background(), fmt.Sprintf("创建线程时异常 %s", err.Error())) 583 | return "", err 584 | } 585 | return th.ID, nil 586 | } 587 | 588 | func NewProxyClient(proxyUrl string) (proxyParse *url.URL, client *http.Client, err error) { 589 | 590 | proxyParse, err = url.Parse(proxyUrl) 591 | if err != nil { 592 | common.FatalLog("代理地址设置有误") 593 | } 594 | 595 | if strings.HasPrefix(proxyParse.Scheme, "http") { 596 | httpTransport := &http.Transport{ 597 | Proxy: http.ProxyURL(proxyParse), 598 | } 599 | return proxyParse, &http.Client{ 600 | Transport: httpTransport, 601 | }, nil 602 | } else if strings.HasPrefix(proxyParse.Scheme, "sock") { 603 | dialer, err := proxy.SOCKS5("tcp", proxyParse.Host, nil, proxy.Direct) 604 | if err != nil { 605 | log.Fatal("Error creating dialer, ", err) 606 | } 607 | 608 | dialContext := func(ctx context.Context, network, addr string) (net.Conn, error) { 609 | return dialer.Dial(network, addr) 610 | } 611 | 612 | // 使用该拨号器创建一个 HTTP 客户端 613 | httpClient := &http.Client{ 614 | Transport: &http.Transport{ 615 | DialContext: dialContext, 616 | }, 617 | } 618 | 619 | return proxyParse, httpClient, nil 620 | } else { 621 | return nil, nil, fmt.Errorf("仅支持sock和http代理!") 622 | } 623 | 624 | } 625 | 626 | func stayActiveMessageTask() { 627 | for { 628 | source := rand.NewSource(time.Now().UnixNano()) 629 | randomNumber := rand.New(source).Intn(60) // 生成0到60之间的随机整数 630 | 631 | // 计算距离下一个时间间隔 632 | now := time.Now() 633 | // 9点10分 为了保证loadUserAuthTask每日任务执行完毕 634 | next := time.Date(now.Year(), now.Month(), now.Day(), 9, 10, 0, 0, now.Location()) 635 | 636 | // 如果当前时间已经超过9点,那么等待到第二天的9点 637 | if now.After(next) { 638 | next = next.Add(24 * time.Hour) 639 | } 640 | 641 | delay := next.Sub(now) 642 | 643 | // 等待直到下一个间隔 644 | time.Sleep(delay + time.Duration(randomNumber)*time.Second) 645 | 646 | var taskBotConfigs = BotConfigList 647 | 648 | taskBotConfigs = append(taskBotConfigs, &model.BotConfig{ 649 | ChannelId: ChannelId, 650 | CozeBotId: CozeBotId, 651 | MessageMaxToken: MessageMaxToken, 652 | }) 653 | 654 | taskBotConfigs = model.FilterUniqueBotChannel(taskBotConfigs) 655 | 656 | common.SysLog("CDP Scheduled Task Job Start!") 657 | var sendChannelList []string 658 | for _, config := range taskBotConfigs { 659 | var sendChannelId string 660 | var err error 661 | if config.ChannelId == "" { 662 | nextID, _ := common.NextID() 663 | sendChannelId, err = CreateChannelWithRetry(nil, GuildId, fmt.Sprintf("cdp-chat-%s", nextID), 0) 664 | if err != nil { 665 | common.LogError(nil, err.Error()) 666 | break 667 | } 668 | sendChannelList = append(sendChannelList, sendChannelId) 669 | } else { 670 | sendChannelId = config.ChannelId 671 | } 672 | nextID, err := common.NextID() 673 | if err != nil { 674 | common.SysError(fmt.Sprintf("ChannelId{%s} BotId{%s} 活跃机器人任务消息发送异常!雪花Id生成失败!", sendChannelId, config.CozeBotId)) 675 | continue 676 | } 677 | _, _, err = SendMessage(nil, sendChannelId, config.CozeBotId, fmt.Sprintf("【%v】 %s", nextID, "CDP Scheduled Task Job Send Msg Success!"), config.MessageMaxToken) 678 | if err != nil { 679 | common.SysError(fmt.Sprintf("ChannelId{%s} BotId{%s} 活跃机器人任务消息发送异常!", sendChannelId, config.CozeBotId)) 680 | } else { 681 | common.SysLog(fmt.Sprintf("ChannelId{%s} BotId{%s} 活跃机器人任务消息发送成功!", sendChannelId, config.CozeBotId)) 682 | } 683 | time.Sleep(5 * time.Second) 684 | } 685 | for _, channelId := range sendChannelList { 686 | ChannelDel(channelId) 687 | } 688 | common.SysLog("CDP Scheduled Task Job End!") 689 | 690 | } 691 | } 692 | 693 | func UploadToDiscordAndGetURL(channelID string, base64Data string) (string, error) { 694 | 695 | // 获取";base64,"后的Base64编码部分 696 | dataParts := strings.Split(base64Data, ";base64,") 697 | if len(dataParts) != 2 { 698 | return "", fmt.Errorf("") 699 | } 700 | base64Data = dataParts[1] 701 | 702 | data, err := base64.StdEncoding.DecodeString(base64Data) 703 | if err != nil { 704 | return "", err 705 | } 706 | // 创建一个新的文件读取器 707 | file := bytes.NewReader(data) 708 | 709 | kind, err := filetype.Match(data) 710 | 711 | if err != nil { 712 | return "", fmt.Errorf("无法识别的文件类型") 713 | } 714 | 715 | // 创建一个新的 MessageSend 结构 716 | m := &discordgo.MessageSend{ 717 | Files: []*discordgo.File{ 718 | { 719 | Name: fmt.Sprintf("file-%s.%s", common.GetTimeString(), kind.Extension), 720 | Reader: file, 721 | }, 722 | }, 723 | } 724 | 725 | // 发送消息 726 | message, err := Session.ChannelMessageSendComplex(channelID, m) 727 | if err != nil { 728 | return "", err 729 | } 730 | 731 | // 检查消息中是否包含附件,并获取 URL 732 | if len(message.Attachments) > 0 { 733 | return message.Attachments[0].URL, nil 734 | } 735 | 736 | return "", fmt.Errorf("no attachment found in the message") 737 | } 738 | 739 | // FilterConfigs 根据proxySecret和channelId过滤BotConfig 740 | func FilterConfigs(configs []*model.BotConfig, secret, gptModel string, channelId *string) []*model.BotConfig { 741 | var filteredConfigs []*model.BotConfig 742 | for _, config := range configs { 743 | matchSecret := secret == "" || config.ProxySecret == secret 744 | matchGptModel := gptModel == "" || common.SliceContains(config.Model, gptModel) 745 | matchChannelId := channelId == nil || *channelId == "" || config.ChannelId == *channelId 746 | if matchSecret && matchChannelId && matchGptModel { 747 | filteredConfigs = append(filteredConfigs, config) 748 | } 749 | } 750 | return filteredConfigs 751 | } 752 | 753 | func DelLimitBot(botId string) { 754 | if BotConfigExist { 755 | BotConfigList = FilterBotConfigByBotId(BotConfigList, botId) 756 | } else { 757 | if CozeBotId == botId { 758 | CozeBotId = "" 759 | } 760 | } 761 | } 762 | 763 | func FilterBotConfigByBotId(slice []*model.BotConfig, filter string) []*model.BotConfig { 764 | var result []*model.BotConfig 765 | for _, value := range slice { 766 | if value.CozeBotId != filter { 767 | result = append(result, value) 768 | } 769 | } 770 | return result 771 | } 772 | -------------------------------------------------------------------------------- /discord/processmessage.go: -------------------------------------------------------------------------------- 1 | package discord 2 | 3 | import ( 4 | "coze-discord-proxy/common" 5 | "coze-discord-proxy/model" 6 | "fmt" 7 | "github.com/bwmarrin/discordgo" 8 | "regexp" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | // processMessage 提取并处理消息内容及其嵌入元素 14 | func processMessageUpdate(m *discordgo.MessageUpdate) model.ReplyResp { 15 | var embedUrls []string 16 | for _, embed := range m.Embeds { 17 | if embed.Image != nil { 18 | embedUrls = append(embedUrls, embed.Image.URL) 19 | } 20 | } 21 | 22 | return model.ReplyResp{ 23 | Content: m.Content, 24 | EmbedUrls: embedUrls, 25 | } 26 | } 27 | 28 | func processMessageUpdateForOpenAI(m *discordgo.MessageUpdate) model.OpenAIChatCompletionResponse { 29 | 30 | if len(m.Embeds) != 0 { 31 | for _, embed := range m.Embeds { 32 | if embed.Image != nil && !strings.Contains(m.Content, embed.Image.URL) { 33 | if m.Content != "" { 34 | m.Content += "\n" 35 | } 36 | m.Content += fmt.Sprintf("%s\n![Image](%s)", embed.Image.URL, embed.Image.URL) 37 | } 38 | } 39 | } 40 | 41 | if len(m.Attachments) != 0 { 42 | for _, attachment := range m.Attachments { 43 | if attachment.ProxyURL != "" && !strings.Contains(m.Content, attachment.ProxyURL) { 44 | if m.Content != "" { 45 | m.Content += "\n" 46 | } 47 | m.Content += fmt.Sprintf("%s\n![Image](%s)", attachment.ProxyURL, attachment.ProxyURL) 48 | } 49 | } 50 | } 51 | 52 | promptTokens := common.CountTokens(m.ReferencedMessage.Content) 53 | completionTokens := common.CountTokens(m.Content) 54 | 55 | return model.OpenAIChatCompletionResponse{ 56 | ID: m.ID, 57 | Object: "chat.completion", 58 | Created: time.Now().Unix(), 59 | Model: "Coze-Model", 60 | Choices: []model.OpenAIChoice{ 61 | { 62 | Index: 0, 63 | Message: model.OpenAIMessage{ 64 | Role: "assistant", 65 | Content: m.Content, 66 | }, 67 | }, 68 | }, 69 | Usage: model.OpenAIUsage{ 70 | PromptTokens: promptTokens, 71 | CompletionTokens: completionTokens, 72 | TotalTokens: promptTokens + completionTokens, 73 | }, 74 | } 75 | } 76 | 77 | func processMessageUpdateForOpenAIImage(m *discordgo.MessageUpdate) model.OpenAIImagesGenerationResponse { 78 | var response model.OpenAIImagesGenerationResponse 79 | 80 | if common.SliceContains(common.CozeDailyLimitErrorMessages, m.Content) { 81 | response.Data = append(response.Data, &model.OpenAIImagesGenerationDataResponse{ 82 | RevisedPrompt: m.Content, 83 | }) 84 | res := model.OpenAIImagesGenerationResponse{ 85 | Created: time.Now().Unix(), 86 | Data: response.Data, 87 | } 88 | return res 89 | } 90 | 91 | re := regexp.MustCompile(`\]\((https?://[^\s\)]+)\)`) 92 | subMatches := re.FindAllStringSubmatch(m.Content, -1) 93 | 94 | if len(subMatches) == 0 { 95 | 96 | if len(m.Embeds) != 0 { 97 | for _, embed := range m.Embeds { 98 | if embed.Image != nil && !strings.Contains(m.Content, embed.Image.URL) { 99 | // if m.Content != "" { 100 | // m.Content += "\n" 101 | // } 102 | response.Data = append(response.Data, &model.OpenAIImagesGenerationDataResponse{ 103 | URL: embed.Image.URL, 104 | RevisedPrompt: m.Content, 105 | }) 106 | } 107 | } 108 | } 109 | 110 | if len(m.Attachments) != 0 { 111 | for _, attachment := range m.Attachments { 112 | if attachment.ProxyURL != "" && !strings.Contains(m.Content, attachment.ProxyURL) { 113 | // if m.Content != "" { 114 | // m.Content += "\n" 115 | // } 116 | response.Data = append(response.Data, &model.OpenAIImagesGenerationDataResponse{ 117 | URL: attachment.ProxyURL, 118 | RevisedPrompt: m.Content, 119 | }) 120 | } 121 | } 122 | } 123 | 124 | if len(m.Message.Components) > 0 && len(m.Embeds) == 0 && len(m.Attachments) == 0 { 125 | response.Data = append(response.Data, &model.OpenAIImagesGenerationDataResponse{ 126 | RevisedPrompt: m.Content, 127 | }) 128 | } 129 | } 130 | 131 | for _, match := range subMatches { 132 | response.Data = append(response.Data, &model.OpenAIImagesGenerationDataResponse{ 133 | URL: match[1], 134 | RevisedPrompt: m.Content, 135 | }) 136 | } 137 | 138 | return model.OpenAIImagesGenerationResponse{ 139 | Created: time.Now().Unix(), 140 | Data: response.Data, 141 | } 142 | } 143 | 144 | // processMessage 提取并处理消息内容及其嵌入元素 145 | func processMessageCreate(m *discordgo.MessageCreate) model.ReplyResp { 146 | var embedUrls []string 147 | for _, embed := range m.Embeds { 148 | if embed.Image != nil { 149 | embedUrls = append(embedUrls, embed.Image.URL) 150 | } 151 | } 152 | 153 | return model.ReplyResp{ 154 | Content: m.Content, 155 | EmbedUrls: embedUrls, 156 | } 157 | } 158 | 159 | func processMessageCreateForOpenAI(m *discordgo.MessageCreate) model.OpenAIChatCompletionResponse { 160 | 161 | if len(m.Embeds) != 0 { 162 | for _, embed := range m.Embeds { 163 | if embed.Image != nil && !strings.Contains(m.Content, embed.Image.URL) { 164 | if m.Content != "" { 165 | m.Content += "\n" 166 | } 167 | m.Content += fmt.Sprintf("%s\n![Image](%s)", embed.Image.URL, embed.Image.URL) 168 | } 169 | } 170 | } 171 | 172 | if len(m.Attachments) != 0 { 173 | for _, attachment := range m.Attachments { 174 | if attachment.ProxyURL != "" && !strings.Contains(m.Content, attachment.ProxyURL) { 175 | if m.Content != "" { 176 | m.Content += "\n" 177 | } 178 | m.Content += fmt.Sprintf("%s\n![Image](%s)", attachment.ProxyURL, attachment.ProxyURL) 179 | } 180 | } 181 | } 182 | 183 | promptTokens := common.CountTokens(m.ReferencedMessage.Content) 184 | completionTokens := common.CountTokens(m.Content) 185 | 186 | return model.OpenAIChatCompletionResponse{ 187 | ID: m.ID, 188 | Object: "chat.completion", 189 | Created: time.Now().Unix(), 190 | Model: "Coze-Model", 191 | Choices: []model.OpenAIChoice{ 192 | { 193 | Index: 0, 194 | Message: model.OpenAIMessage{ 195 | Role: "assistant", 196 | Content: m.Content, 197 | }, 198 | }, 199 | }, 200 | Usage: model.OpenAIUsage{ 201 | PromptTokens: promptTokens, 202 | CompletionTokens: completionTokens, 203 | TotalTokens: promptTokens + completionTokens, 204 | }, 205 | } 206 | } 207 | 208 | func processMessageCreateForOpenAIImage(m *discordgo.MessageCreate) model.OpenAIImagesGenerationResponse { 209 | var response model.OpenAIImagesGenerationResponse 210 | 211 | if common.SliceContains(common.CozeDailyLimitErrorMessages, m.Content) { 212 | response.Data = append(response.Data, &model.OpenAIImagesGenerationDataResponse{ 213 | RevisedPrompt: m.Content, 214 | }) 215 | res := model.OpenAIImagesGenerationResponse{ 216 | Created: time.Now().Unix(), 217 | Data: response.Data, 218 | } 219 | return res 220 | } 221 | 222 | re := regexp.MustCompile(`\]\((https?://[^\s\)]+)\)`) 223 | subMatches := re.FindAllStringSubmatch(m.Content, -1) 224 | 225 | if len(subMatches) == 0 { 226 | 227 | if len(m.Embeds) != 0 { 228 | for _, embed := range m.Embeds { 229 | if embed.Image != nil && !strings.Contains(m.Content, embed.Image.URL) { 230 | // if m.Content != "" { 231 | // m.Content += "\n" 232 | // } 233 | response.Data = append(response.Data, &model.OpenAIImagesGenerationDataResponse{ 234 | URL: embed.Image.URL, 235 | RevisedPrompt: m.Content, 236 | }) 237 | } 238 | } 239 | } 240 | 241 | if len(m.Attachments) != 0 { 242 | for _, attachment := range m.Attachments { 243 | if attachment.ProxyURL != "" && !strings.Contains(m.Content, attachment.ProxyURL) { 244 | // if m.Content != "" { 245 | // m.Content += "\n" 246 | // } 247 | response.Data = append(response.Data, &model.OpenAIImagesGenerationDataResponse{ 248 | URL: attachment.ProxyURL, 249 | RevisedPrompt: m.Content, 250 | }) 251 | } 252 | } 253 | } 254 | 255 | if len(m.Message.Components) > 0 && len(m.Embeds) == 0 && len(m.Attachments) == 0 { 256 | response.Data = append(response.Data, &model.OpenAIImagesGenerationDataResponse{ 257 | RevisedPrompt: m.Content, 258 | }) 259 | } 260 | } 261 | 262 | for _, match := range subMatches { 263 | response.Data = append(response.Data, &model.OpenAIImagesGenerationDataResponse{ 264 | URL: match[1], 265 | RevisedPrompt: m.Content, 266 | }) 267 | } 268 | 269 | return model.OpenAIImagesGenerationResponse{ 270 | Created: time.Now().Unix(), 271 | Data: response.Data, 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /discord/sendmessage.go: -------------------------------------------------------------------------------- 1 | package discord 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "coze-discord-proxy/common" 7 | "coze-discord-proxy/common/myerr" 8 | "encoding/json" 9 | "fmt" 10 | "github.com/gin-gonic/gin" 11 | "io" 12 | "net/http" 13 | "net/url" 14 | "strings" 15 | ) 16 | 17 | // 用户端发送消息 注意 此为临时解决方案 后续会优化代码 18 | func SendMsgByAuthorization(c *gin.Context, userAuth, content, channelId string) (string, error) { 19 | var ctx context.Context 20 | if c == nil { 21 | ctx = context.Background() 22 | } else { 23 | ctx = c.Request.Context() 24 | } 25 | 26 | postUrl := "https://discord.com/api/v9/channels/%s/messages" 27 | 28 | // 构造请求体 29 | requestBody, err := json.Marshal(map[string]interface{}{ 30 | "content": content, 31 | }) 32 | if err != nil { 33 | common.LogError(ctx, fmt.Sprintf("Error encoding request body:%s", err)) 34 | return "", err 35 | } 36 | 37 | req, err := http.NewRequest("POST", fmt.Sprintf(postUrl, channelId), bytes.NewBuffer(requestBody)) 38 | if err != nil { 39 | common.LogError(ctx, fmt.Sprintf("Error creating request:%s", err)) 40 | return "", err 41 | } 42 | 43 | // 设置请求头-部分请求头不传没问题,但目前仍有被discord检测异常的风险 44 | req.Header.Set("Content-Type", "application/json") 45 | req.Header.Set("Authorization", userAuth) 46 | req.Header.Set("Origin", "https://discord.com") 47 | req.Header.Set("Referer", fmt.Sprintf("https://discord.com/channels/%s/%s", GuildId, channelId)) 48 | if UserAgent != "" { 49 | req.Header.Set("User-Agent", UserAgent) 50 | } else { 51 | req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36") 52 | } 53 | 54 | // 发起请求 55 | client := &http.Client{} 56 | if ProxyUrl != "" { 57 | proxyURL, _ := url.Parse(ProxyUrl) 58 | transport := &http.Transport{ 59 | Proxy: http.ProxyURL(proxyURL), 60 | } 61 | client = &http.Client{ 62 | Transport: transport, 63 | } 64 | } 65 | 66 | resp, err := client.Do(req) 67 | if err != nil { 68 | common.LogError(ctx, fmt.Sprintf("Error sending request:%s", err)) 69 | return "", err 70 | } 71 | defer resp.Body.Close() 72 | 73 | // 读取响应体 74 | bodyBytes, err := io.ReadAll(resp.Body) 75 | if err != nil { 76 | return "", err 77 | } 78 | 79 | // 将响应体转换为字符串 80 | bodyString := string(bodyBytes) 81 | 82 | // 使用map来解码JSON 83 | var result map[string]interface{} 84 | 85 | // 解码JSON到map中 86 | err = json.Unmarshal([]byte(bodyString), &result) 87 | if err != nil { 88 | return "", err 89 | } 90 | 91 | // 类型断言来获取id的值 92 | id, ok := result["id"].(string) 93 | 94 | if !ok { 95 | // 401 96 | if errMessage, ok := result["message"].(string); ok { 97 | if strings.Contains(errMessage, "401: Unauthorized") || 98 | strings.Contains(errMessage, "You need to verify your account in order to perform this action.") { 99 | common.LogWarn(ctx, fmt.Sprintf("USER_AUTHORIZATION:%s EXPIRED", userAuth)) 100 | return "", &myerr.DiscordUnauthorizedError{ 101 | ErrCode: 401, 102 | Message: "discord 鉴权未通过", 103 | } 104 | } 105 | } 106 | common.LogError(ctx, fmt.Sprintf("user_auth:%s result:%s", userAuth, bodyString)) 107 | return "", fmt.Errorf("/api/v9/channels/%s/messages response myerr", channelId) 108 | } else { 109 | return id, nil 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | coze-discord-proxy: 5 | image: deanxv/coze-discord-proxy:latest 6 | container_name: coze-discord-proxy 7 | restart: always 8 | ports: 9 | - "7077:7077" 10 | volumes: 11 | - ./data:/app/coze-discord-proxy/data 12 | environment: 13 | - USER_AUTHORIZATION=MTA5OTg5N************aXUBHVI # 必须修改为我们discord用户的授权密钥(多个请以,分隔) 14 | - BOT_TOKEN=MTE5OTk2xxxxxxxxxxxxxxrwUrUWNbG63w # 必须修改为我们主动发送消息的Bot-Token 15 | - GUILD_ID=119xxxxxxxx796 # 必须修改为两个机器人所在的服务器ID 16 | - COZE_BOT_ID=119xxxxxxxx7 # 必须修改为由coze托管的机器人ID 17 | - CHANNEL_ID=119xxxxxx24 # 默认频道-在使用与openai对齐的接口时(/v1/chat/completions) 消息会默认发送到此频道 18 | - PROXY_SECRET=123456 # [可选]修改此行为请求头校验的值(前后端统一) 19 | - TZ=Asia/Shanghai -------------------------------------------------------------------------------- /docs/docs.go: -------------------------------------------------------------------------------- 1 | // Package docs Code generated by swaggo/swag. DO NOT EDIT 2 | package docs 3 | 4 | import "github.com/swaggo/swag" 5 | 6 | const docTemplate = `{ 7 | "schemes": {{ marshal .Schemes }}, 8 | "swagger": "2.0", 9 | "info": { 10 | "description": "{{escape .Description}}", 11 | "title": "{{.Title}}", 12 | "contact": {}, 13 | "version": "{{.Version}}" 14 | }, 15 | "host": "{{.Host}}", 16 | "basePath": "{{.BasePath}}", 17 | "paths": { 18 | "/api/channel/create": { 19 | "post": { 20 | "description": "创建频道", 21 | "consumes": [ 22 | "application/json" 23 | ], 24 | "produces": [ 25 | "application/json" 26 | ], 27 | "tags": [ 28 | "channel" 29 | ], 30 | "summary": "创建频道", 31 | "parameters": [ 32 | { 33 | "description": "channelModel", 34 | "name": "channelModel", 35 | "in": "body", 36 | "required": true, 37 | "schema": { 38 | "$ref": "#/definitions/model.ChannelReq" 39 | } 40 | } 41 | ], 42 | "responses": { 43 | "200": { 44 | "description": "Successful response", 45 | "schema": { 46 | "$ref": "#/definitions/model.ChannelResp" 47 | } 48 | } 49 | } 50 | } 51 | }, 52 | "/api/channel/del/{id}": { 53 | "get": { 54 | "description": "删除频道", 55 | "consumes": [ 56 | "application/json" 57 | ], 58 | "produces": [ 59 | "application/json" 60 | ], 61 | "tags": [ 62 | "channel" 63 | ], 64 | "summary": "删除频道", 65 | "parameters": [ 66 | { 67 | "type": "string", 68 | "description": "id", 69 | "name": "id", 70 | "in": "path", 71 | "required": true 72 | } 73 | ], 74 | "responses": { 75 | "200": { 76 | "description": "Successful response", 77 | "schema": { 78 | "type": "string" 79 | } 80 | } 81 | } 82 | } 83 | }, 84 | "/api/del/all/cdp": { 85 | "get": { 86 | "description": "删除全部CDP临时频道[谨慎调用]", 87 | "consumes": [ 88 | "application/json" 89 | ], 90 | "produces": [ 91 | "application/json" 92 | ], 93 | "tags": [ 94 | "channel" 95 | ], 96 | "summary": "删除全部CDP临时频道[谨慎调用]", 97 | "responses": { 98 | "200": { 99 | "description": "Successful response", 100 | "schema": { 101 | "type": "string" 102 | } 103 | } 104 | } 105 | } 106 | }, 107 | "/api/thread/create": { 108 | "post": { 109 | "description": "创建线程", 110 | "consumes": [ 111 | "application/json" 112 | ], 113 | "produces": [ 114 | "application/json" 115 | ], 116 | "tags": [ 117 | "thread" 118 | ], 119 | "summary": "创建线程", 120 | "parameters": [ 121 | { 122 | "description": "threadModel", 123 | "name": "threadModel", 124 | "in": "body", 125 | "required": true, 126 | "schema": { 127 | "$ref": "#/definitions/model.ThreadReq" 128 | } 129 | } 130 | ], 131 | "responses": { 132 | "200": { 133 | "description": "Successful response", 134 | "schema": { 135 | "$ref": "#/definitions/model.ThreadResp" 136 | } 137 | } 138 | } 139 | } 140 | }, 141 | "/v1/chat/completions": { 142 | "post": { 143 | "description": "发送消息-openai", 144 | "consumes": [ 145 | "application/json" 146 | ], 147 | "produces": [ 148 | "application/json" 149 | ], 150 | "tags": [ 151 | "openai" 152 | ], 153 | "summary": "发送消息-openai", 154 | "parameters": [ 155 | { 156 | "description": "request", 157 | "name": "request", 158 | "in": "body", 159 | "required": true, 160 | "schema": { 161 | "$ref": "#/definitions/model.OpenAIChatCompletionRequest" 162 | } 163 | }, 164 | { 165 | "type": "string", 166 | "description": "Authorization", 167 | "name": "Authorization", 168 | "in": "header" 169 | }, 170 | { 171 | "type": "string", 172 | "description": "out-time", 173 | "name": "out-time", 174 | "in": "header" 175 | } 176 | ], 177 | "responses": { 178 | "200": { 179 | "description": "Successful response", 180 | "schema": { 181 | "$ref": "#/definitions/model.OpenAIChatCompletionResponse" 182 | } 183 | } 184 | } 185 | } 186 | }, 187 | "/v1/images/generations": { 188 | "post": { 189 | "description": "图片生成-openai", 190 | "consumes": [ 191 | "application/json" 192 | ], 193 | "produces": [ 194 | "application/json" 195 | ], 196 | "tags": [ 197 | "openai" 198 | ], 199 | "summary": "图片生成-openai", 200 | "parameters": [ 201 | { 202 | "description": "request", 203 | "name": "request", 204 | "in": "body", 205 | "required": true, 206 | "schema": { 207 | "$ref": "#/definitions/model.OpenAIImagesGenerationRequest" 208 | } 209 | }, 210 | { 211 | "type": "string", 212 | "description": "Authorization", 213 | "name": "Authorization", 214 | "in": "header" 215 | }, 216 | { 217 | "type": "string", 218 | "description": "out-time", 219 | "name": "out-time", 220 | "in": "header" 221 | } 222 | ], 223 | "responses": { 224 | "200": { 225 | "description": "Successful response", 226 | "schema": { 227 | "$ref": "#/definitions/model.OpenAIImagesGenerationResponse" 228 | } 229 | } 230 | } 231 | } 232 | }, 233 | "/v1/models": { 234 | "get": { 235 | "description": "模型列表-openai", 236 | "consumes": [ 237 | "application/json" 238 | ], 239 | "produces": [ 240 | "application/json" 241 | ], 242 | "tags": [ 243 | "openai" 244 | ], 245 | "summary": "模型列表-openai", 246 | "parameters": [ 247 | { 248 | "type": "string", 249 | "description": "Authorization", 250 | "name": "Authorization", 251 | "in": "header" 252 | } 253 | ], 254 | "responses": { 255 | "200": { 256 | "description": "Successful response", 257 | "schema": { 258 | "$ref": "#/definitions/model.OpenaiModelListResponse" 259 | } 260 | } 261 | } 262 | } 263 | } 264 | }, 265 | "definitions": { 266 | "model.ChannelReq": { 267 | "type": "object", 268 | "properties": { 269 | "name": { 270 | "type": "string" 271 | }, 272 | "parentId": { 273 | "type": "string" 274 | }, 275 | "type": { 276 | "type": "number" 277 | } 278 | } 279 | }, 280 | "model.ChannelResp": { 281 | "type": "object", 282 | "properties": { 283 | "id": { 284 | "type": "string" 285 | }, 286 | "name": { 287 | "type": "string" 288 | } 289 | } 290 | }, 291 | "model.OpenAIChatCompletionRequest": { 292 | "type": "object", 293 | "properties": { 294 | "channelId": { 295 | "type": "string" 296 | }, 297 | "messages": { 298 | "type": "array", 299 | "items": { 300 | "$ref": "#/definitions/model.OpenAIChatMessage" 301 | } 302 | }, 303 | "model": { 304 | "type": "string" 305 | }, 306 | "stream": { 307 | "type": "boolean" 308 | } 309 | } 310 | }, 311 | "model.OpenAIChatCompletionResponse": { 312 | "type": "object", 313 | "properties": { 314 | "choices": { 315 | "type": "array", 316 | "items": { 317 | "$ref": "#/definitions/model.OpenAIChoice" 318 | } 319 | }, 320 | "created": { 321 | "type": "integer" 322 | }, 323 | "id": { 324 | "type": "string" 325 | }, 326 | "model": { 327 | "type": "string" 328 | }, 329 | "object": { 330 | "type": "string" 331 | }, 332 | "suggestions": { 333 | "type": "array", 334 | "items": { 335 | "type": "string" 336 | } 337 | }, 338 | "system_fingerprint": { 339 | "type": "string" 340 | }, 341 | "usage": { 342 | "$ref": "#/definitions/model.OpenAIUsage" 343 | } 344 | } 345 | }, 346 | "model.OpenAIChatMessage": { 347 | "type": "object", 348 | "properties": { 349 | "content": {}, 350 | "role": { 351 | "type": "string" 352 | } 353 | } 354 | }, 355 | "model.OpenAIChoice": { 356 | "type": "object", 357 | "properties": { 358 | "delta": { 359 | "$ref": "#/definitions/model.OpenAIDelta" 360 | }, 361 | "finish_reason": { 362 | "type": "string" 363 | }, 364 | "index": { 365 | "type": "integer" 366 | }, 367 | "logprobs": { 368 | "type": "string" 369 | }, 370 | "message": { 371 | "$ref": "#/definitions/model.OpenAIMessage" 372 | } 373 | } 374 | }, 375 | "model.OpenAIDelta": { 376 | "type": "object", 377 | "properties": { 378 | "content": { 379 | "type": "string" 380 | } 381 | } 382 | }, 383 | "model.OpenAIImagesGenerationDataResponse": { 384 | "type": "object", 385 | "properties": { 386 | "b64_json": { 387 | "type": "string" 388 | }, 389 | "revised_prompt": { 390 | "type": "string" 391 | }, 392 | "url": { 393 | "type": "string" 394 | } 395 | } 396 | }, 397 | "model.OpenAIImagesGenerationRequest": { 398 | "type": "object", 399 | "properties": { 400 | "channelId": { 401 | "type": "string" 402 | }, 403 | "model": { 404 | "type": "string" 405 | }, 406 | "prompt": { 407 | "type": "string" 408 | }, 409 | "response_format": { 410 | "type": "string" 411 | } 412 | } 413 | }, 414 | "model.OpenAIImagesGenerationResponse": { 415 | "type": "object", 416 | "properties": { 417 | "created": { 418 | "type": "integer" 419 | }, 420 | "dailyLimit": { 421 | "type": "boolean" 422 | }, 423 | "data": { 424 | "type": "array", 425 | "items": { 426 | "$ref": "#/definitions/model.OpenAIImagesGenerationDataResponse" 427 | } 428 | }, 429 | "suggestions": { 430 | "type": "array", 431 | "items": { 432 | "type": "string" 433 | } 434 | } 435 | } 436 | }, 437 | "model.OpenAIMessage": { 438 | "type": "object", 439 | "properties": { 440 | "content": { 441 | "type": "string" 442 | }, 443 | "role": { 444 | "type": "string" 445 | } 446 | } 447 | }, 448 | "model.OpenAIUsage": { 449 | "type": "object", 450 | "properties": { 451 | "completion_tokens": { 452 | "type": "integer" 453 | }, 454 | "prompt_tokens": { 455 | "type": "integer" 456 | }, 457 | "total_tokens": { 458 | "type": "integer" 459 | } 460 | } 461 | }, 462 | "model.OpenaiModelListResponse": { 463 | "type": "object", 464 | "properties": { 465 | "data": { 466 | "type": "array", 467 | "items": { 468 | "$ref": "#/definitions/model.OpenaiModelResponse" 469 | } 470 | }, 471 | "object": { 472 | "type": "string" 473 | } 474 | } 475 | }, 476 | "model.OpenaiModelResponse": { 477 | "type": "object", 478 | "properties": { 479 | "id": { 480 | "type": "string" 481 | }, 482 | "object": { 483 | "type": "string" 484 | } 485 | } 486 | }, 487 | "model.ThreadReq": { 488 | "type": "object", 489 | "properties": { 490 | "archiveDuration": { 491 | "type": "number" 492 | }, 493 | "channelId": { 494 | "type": "string" 495 | }, 496 | "name": { 497 | "type": "string" 498 | } 499 | } 500 | }, 501 | "model.ThreadResp": { 502 | "type": "object", 503 | "properties": { 504 | "id": { 505 | "type": "string" 506 | }, 507 | "name": { 508 | "type": "string" 509 | } 510 | } 511 | } 512 | } 513 | }` 514 | 515 | // SwaggerInfo holds exported Swagger Info so clients can modify it 516 | var SwaggerInfo = &swag.Spec{ 517 | Version: "1.0.0", 518 | Host: "", 519 | BasePath: "", 520 | Schemes: []string{}, 521 | Title: "COZE-DISCORD-PROXY", 522 | Description: "COZE-DISCORD-PROXY 代理服务", 523 | InfoInstanceName: "swagger", 524 | SwaggerTemplate: docTemplate, 525 | LeftDelim: "{{", 526 | RightDelim: "}}", 527 | } 528 | 529 | func init() { 530 | swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) 531 | } 532 | -------------------------------------------------------------------------------- /docs/img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanxv/coze-discord-proxy/2abd758127d6bcc5383d97212b7d736f122f713d/docs/img.png -------------------------------------------------------------------------------- /docs/img2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanxv/coze-discord-proxy/2abd758127d6bcc5383d97212b7d736f122f713d/docs/img2.png -------------------------------------------------------------------------------- /docs/img3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanxv/coze-discord-proxy/2abd758127d6bcc5383d97212b7d736f122f713d/docs/img3.png -------------------------------------------------------------------------------- /docs/img5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanxv/coze-discord-proxy/2abd758127d6bcc5383d97212b7d736f122f713d/docs/img5.png -------------------------------------------------------------------------------- /docs/swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "description": "COZE-DISCORD-PROXY 代理服务", 5 | "title": "COZE-DISCORD-PROXY", 6 | "contact": {}, 7 | "version": "1.0.0" 8 | }, 9 | "paths": { 10 | "/api/channel/create": { 11 | "post": { 12 | "description": "创建频道", 13 | "consumes": [ 14 | "application/json" 15 | ], 16 | "produces": [ 17 | "application/json" 18 | ], 19 | "tags": [ 20 | "channel" 21 | ], 22 | "summary": "创建频道", 23 | "parameters": [ 24 | { 25 | "description": "channelModel", 26 | "name": "channelModel", 27 | "in": "body", 28 | "required": true, 29 | "schema": { 30 | "$ref": "#/definitions/model.ChannelReq" 31 | } 32 | } 33 | ], 34 | "responses": { 35 | "200": { 36 | "description": "Successful response", 37 | "schema": { 38 | "$ref": "#/definitions/model.ChannelResp" 39 | } 40 | } 41 | } 42 | } 43 | }, 44 | "/api/channel/del/{id}": { 45 | "get": { 46 | "description": "删除频道", 47 | "consumes": [ 48 | "application/json" 49 | ], 50 | "produces": [ 51 | "application/json" 52 | ], 53 | "tags": [ 54 | "channel" 55 | ], 56 | "summary": "删除频道", 57 | "parameters": [ 58 | { 59 | "type": "string", 60 | "description": "id", 61 | "name": "id", 62 | "in": "path", 63 | "required": true 64 | } 65 | ], 66 | "responses": { 67 | "200": { 68 | "description": "Successful response", 69 | "schema": { 70 | "type": "string" 71 | } 72 | } 73 | } 74 | } 75 | }, 76 | "/api/del/all/cdp": { 77 | "get": { 78 | "description": "删除全部CDP临时频道[谨慎调用]", 79 | "consumes": [ 80 | "application/json" 81 | ], 82 | "produces": [ 83 | "application/json" 84 | ], 85 | "tags": [ 86 | "channel" 87 | ], 88 | "summary": "删除全部CDP临时频道[谨慎调用]", 89 | "responses": { 90 | "200": { 91 | "description": "Successful response", 92 | "schema": { 93 | "type": "string" 94 | } 95 | } 96 | } 97 | } 98 | }, 99 | "/api/thread/create": { 100 | "post": { 101 | "description": "创建线程", 102 | "consumes": [ 103 | "application/json" 104 | ], 105 | "produces": [ 106 | "application/json" 107 | ], 108 | "tags": [ 109 | "thread" 110 | ], 111 | "summary": "创建线程", 112 | "parameters": [ 113 | { 114 | "description": "threadModel", 115 | "name": "threadModel", 116 | "in": "body", 117 | "required": true, 118 | "schema": { 119 | "$ref": "#/definitions/model.ThreadReq" 120 | } 121 | } 122 | ], 123 | "responses": { 124 | "200": { 125 | "description": "Successful response", 126 | "schema": { 127 | "$ref": "#/definitions/model.ThreadResp" 128 | } 129 | } 130 | } 131 | } 132 | }, 133 | "/v1/chat/completions": { 134 | "post": { 135 | "description": "发送消息-openai", 136 | "consumes": [ 137 | "application/json" 138 | ], 139 | "produces": [ 140 | "application/json" 141 | ], 142 | "tags": [ 143 | "openai" 144 | ], 145 | "summary": "发送消息-openai", 146 | "parameters": [ 147 | { 148 | "description": "request", 149 | "name": "request", 150 | "in": "body", 151 | "required": true, 152 | "schema": { 153 | "$ref": "#/definitions/model.OpenAIChatCompletionRequest" 154 | } 155 | }, 156 | { 157 | "type": "string", 158 | "description": "Authorization", 159 | "name": "Authorization", 160 | "in": "header" 161 | }, 162 | { 163 | "type": "string", 164 | "description": "out-time", 165 | "name": "out-time", 166 | "in": "header" 167 | } 168 | ], 169 | "responses": { 170 | "200": { 171 | "description": "Successful response", 172 | "schema": { 173 | "$ref": "#/definitions/model.OpenAIChatCompletionResponse" 174 | } 175 | } 176 | } 177 | } 178 | }, 179 | "/v1/images/generations": { 180 | "post": { 181 | "description": "图片生成-openai", 182 | "consumes": [ 183 | "application/json" 184 | ], 185 | "produces": [ 186 | "application/json" 187 | ], 188 | "tags": [ 189 | "openai" 190 | ], 191 | "summary": "图片生成-openai", 192 | "parameters": [ 193 | { 194 | "description": "request", 195 | "name": "request", 196 | "in": "body", 197 | "required": true, 198 | "schema": { 199 | "$ref": "#/definitions/model.OpenAIImagesGenerationRequest" 200 | } 201 | }, 202 | { 203 | "type": "string", 204 | "description": "Authorization", 205 | "name": "Authorization", 206 | "in": "header" 207 | }, 208 | { 209 | "type": "string", 210 | "description": "out-time", 211 | "name": "out-time", 212 | "in": "header" 213 | } 214 | ], 215 | "responses": { 216 | "200": { 217 | "description": "Successful response", 218 | "schema": { 219 | "$ref": "#/definitions/model.OpenAIImagesGenerationResponse" 220 | } 221 | } 222 | } 223 | } 224 | }, 225 | "/v1/models": { 226 | "get": { 227 | "description": "模型列表-openai", 228 | "consumes": [ 229 | "application/json" 230 | ], 231 | "produces": [ 232 | "application/json" 233 | ], 234 | "tags": [ 235 | "openai" 236 | ], 237 | "summary": "模型列表-openai", 238 | "parameters": [ 239 | { 240 | "type": "string", 241 | "description": "Authorization", 242 | "name": "Authorization", 243 | "in": "header" 244 | } 245 | ], 246 | "responses": { 247 | "200": { 248 | "description": "Successful response", 249 | "schema": { 250 | "$ref": "#/definitions/model.OpenaiModelListResponse" 251 | } 252 | } 253 | } 254 | } 255 | } 256 | }, 257 | "definitions": { 258 | "model.ChannelReq": { 259 | "type": "object", 260 | "properties": { 261 | "name": { 262 | "type": "string" 263 | }, 264 | "parentId": { 265 | "type": "string" 266 | }, 267 | "type": { 268 | "type": "number" 269 | } 270 | } 271 | }, 272 | "model.ChannelResp": { 273 | "type": "object", 274 | "properties": { 275 | "id": { 276 | "type": "string" 277 | }, 278 | "name": { 279 | "type": "string" 280 | } 281 | } 282 | }, 283 | "model.OpenAIChatCompletionRequest": { 284 | "type": "object", 285 | "properties": { 286 | "channelId": { 287 | "type": "string" 288 | }, 289 | "messages": { 290 | "type": "array", 291 | "items": { 292 | "$ref": "#/definitions/model.OpenAIChatMessage" 293 | } 294 | }, 295 | "model": { 296 | "type": "string" 297 | }, 298 | "stream": { 299 | "type": "boolean" 300 | } 301 | } 302 | }, 303 | "model.OpenAIChatCompletionResponse": { 304 | "type": "object", 305 | "properties": { 306 | "choices": { 307 | "type": "array", 308 | "items": { 309 | "$ref": "#/definitions/model.OpenAIChoice" 310 | } 311 | }, 312 | "created": { 313 | "type": "integer" 314 | }, 315 | "id": { 316 | "type": "string" 317 | }, 318 | "model": { 319 | "type": "string" 320 | }, 321 | "object": { 322 | "type": "string" 323 | }, 324 | "suggestions": { 325 | "type": "array", 326 | "items": { 327 | "type": "string" 328 | } 329 | }, 330 | "system_fingerprint": { 331 | "type": "string" 332 | }, 333 | "usage": { 334 | "$ref": "#/definitions/model.OpenAIUsage" 335 | } 336 | } 337 | }, 338 | "model.OpenAIChatMessage": { 339 | "type": "object", 340 | "properties": { 341 | "content": {}, 342 | "role": { 343 | "type": "string" 344 | } 345 | } 346 | }, 347 | "model.OpenAIChoice": { 348 | "type": "object", 349 | "properties": { 350 | "delta": { 351 | "$ref": "#/definitions/model.OpenAIDelta" 352 | }, 353 | "finish_reason": { 354 | "type": "string" 355 | }, 356 | "index": { 357 | "type": "integer" 358 | }, 359 | "logprobs": { 360 | "type": "string" 361 | }, 362 | "message": { 363 | "$ref": "#/definitions/model.OpenAIMessage" 364 | } 365 | } 366 | }, 367 | "model.OpenAIDelta": { 368 | "type": "object", 369 | "properties": { 370 | "content": { 371 | "type": "string" 372 | } 373 | } 374 | }, 375 | "model.OpenAIImagesGenerationDataResponse": { 376 | "type": "object", 377 | "properties": { 378 | "b64_json": { 379 | "type": "string" 380 | }, 381 | "revised_prompt": { 382 | "type": "string" 383 | }, 384 | "url": { 385 | "type": "string" 386 | } 387 | } 388 | }, 389 | "model.OpenAIImagesGenerationRequest": { 390 | "type": "object", 391 | "properties": { 392 | "channelId": { 393 | "type": "string" 394 | }, 395 | "model": { 396 | "type": "string" 397 | }, 398 | "prompt": { 399 | "type": "string" 400 | }, 401 | "response_format": { 402 | "type": "string" 403 | } 404 | } 405 | }, 406 | "model.OpenAIImagesGenerationResponse": { 407 | "type": "object", 408 | "properties": { 409 | "created": { 410 | "type": "integer" 411 | }, 412 | "dailyLimit": { 413 | "type": "boolean" 414 | }, 415 | "data": { 416 | "type": "array", 417 | "items": { 418 | "$ref": "#/definitions/model.OpenAIImagesGenerationDataResponse" 419 | } 420 | }, 421 | "suggestions": { 422 | "type": "array", 423 | "items": { 424 | "type": "string" 425 | } 426 | } 427 | } 428 | }, 429 | "model.OpenAIMessage": { 430 | "type": "object", 431 | "properties": { 432 | "content": { 433 | "type": "string" 434 | }, 435 | "role": { 436 | "type": "string" 437 | } 438 | } 439 | }, 440 | "model.OpenAIUsage": { 441 | "type": "object", 442 | "properties": { 443 | "completion_tokens": { 444 | "type": "integer" 445 | }, 446 | "prompt_tokens": { 447 | "type": "integer" 448 | }, 449 | "total_tokens": { 450 | "type": "integer" 451 | } 452 | } 453 | }, 454 | "model.OpenaiModelListResponse": { 455 | "type": "object", 456 | "properties": { 457 | "data": { 458 | "type": "array", 459 | "items": { 460 | "$ref": "#/definitions/model.OpenaiModelResponse" 461 | } 462 | }, 463 | "object": { 464 | "type": "string" 465 | } 466 | } 467 | }, 468 | "model.OpenaiModelResponse": { 469 | "type": "object", 470 | "properties": { 471 | "id": { 472 | "type": "string" 473 | }, 474 | "object": { 475 | "type": "string" 476 | } 477 | } 478 | }, 479 | "model.ThreadReq": { 480 | "type": "object", 481 | "properties": { 482 | "archiveDuration": { 483 | "type": "number" 484 | }, 485 | "channelId": { 486 | "type": "string" 487 | }, 488 | "name": { 489 | "type": "string" 490 | } 491 | } 492 | }, 493 | "model.ThreadResp": { 494 | "type": "object", 495 | "properties": { 496 | "id": { 497 | "type": "string" 498 | }, 499 | "name": { 500 | "type": "string" 501 | } 502 | } 503 | } 504 | } 505 | } -------------------------------------------------------------------------------- /docs/swagger.yaml: -------------------------------------------------------------------------------- 1 | definitions: 2 | model.ChannelReq: 3 | properties: 4 | name: 5 | type: string 6 | parentId: 7 | type: string 8 | type: 9 | type: number 10 | type: object 11 | model.ChannelResp: 12 | properties: 13 | id: 14 | type: string 15 | name: 16 | type: string 17 | type: object 18 | model.OpenAIChatCompletionRequest: 19 | properties: 20 | channelId: 21 | type: string 22 | messages: 23 | items: 24 | $ref: '#/definitions/model.OpenAIChatMessage' 25 | type: array 26 | model: 27 | type: string 28 | stream: 29 | type: boolean 30 | type: object 31 | model.OpenAIChatCompletionResponse: 32 | properties: 33 | choices: 34 | items: 35 | $ref: '#/definitions/model.OpenAIChoice' 36 | type: array 37 | created: 38 | type: integer 39 | id: 40 | type: string 41 | model: 42 | type: string 43 | object: 44 | type: string 45 | suggestions: 46 | items: 47 | type: string 48 | type: array 49 | system_fingerprint: 50 | type: string 51 | usage: 52 | $ref: '#/definitions/model.OpenAIUsage' 53 | type: object 54 | model.OpenAIChatMessage: 55 | properties: 56 | content: { } 57 | role: 58 | type: string 59 | type: object 60 | model.OpenAIChoice: 61 | properties: 62 | delta: 63 | $ref: '#/definitions/model.OpenAIDelta' 64 | finish_reason: 65 | type: string 66 | index: 67 | type: integer 68 | logprobs: 69 | type: string 70 | message: 71 | $ref: '#/definitions/model.OpenAIMessage' 72 | type: object 73 | model.OpenAIDelta: 74 | properties: 75 | content: 76 | type: string 77 | type: object 78 | model.OpenAIImagesGenerationDataResponse: 79 | properties: 80 | b64_json: 81 | type: string 82 | revised_prompt: 83 | type: string 84 | url: 85 | type: string 86 | type: object 87 | model.OpenAIImagesGenerationRequest: 88 | properties: 89 | channelId: 90 | type: string 91 | model: 92 | type: string 93 | prompt: 94 | type: string 95 | response_format: 96 | type: string 97 | type: object 98 | model.OpenAIImagesGenerationResponse: 99 | properties: 100 | created: 101 | type: integer 102 | dailyLimit: 103 | type: boolean 104 | data: 105 | items: 106 | $ref: '#/definitions/model.OpenAIImagesGenerationDataResponse' 107 | type: array 108 | suggestions: 109 | items: 110 | type: string 111 | type: array 112 | type: object 113 | model.OpenAIMessage: 114 | properties: 115 | content: 116 | type: string 117 | role: 118 | type: string 119 | type: object 120 | model.OpenAIUsage: 121 | properties: 122 | completion_tokens: 123 | type: integer 124 | prompt_tokens: 125 | type: integer 126 | total_tokens: 127 | type: integer 128 | type: object 129 | model.OpenaiModelListResponse: 130 | properties: 131 | data: 132 | items: 133 | $ref: '#/definitions/model.OpenaiModelResponse' 134 | type: array 135 | object: 136 | type: string 137 | type: object 138 | model.OpenaiModelResponse: 139 | properties: 140 | id: 141 | type: string 142 | object: 143 | type: string 144 | type: object 145 | model.ThreadReq: 146 | properties: 147 | archiveDuration: 148 | type: number 149 | channelId: 150 | type: string 151 | name: 152 | type: string 153 | type: object 154 | model.ThreadResp: 155 | properties: 156 | id: 157 | type: string 158 | name: 159 | type: string 160 | type: object 161 | info: 162 | contact: { } 163 | description: COZE-DISCORD-PROXY 代理服务 164 | title: COZE-DISCORD-PROXY 165 | version: 1.0.0 166 | paths: 167 | /api/channel/create: 168 | post: 169 | consumes: 170 | - application/json 171 | description: 创建频道 172 | parameters: 173 | - description: channelModel 174 | in: body 175 | name: channelModel 176 | required: true 177 | schema: 178 | $ref: '#/definitions/model.ChannelReq' 179 | produces: 180 | - application/json 181 | responses: 182 | "200": 183 | description: Successful response 184 | schema: 185 | $ref: '#/definitions/model.ChannelResp' 186 | summary: 创建频道 187 | tags: 188 | - channel 189 | /api/channel/del/{id}: 190 | get: 191 | consumes: 192 | - application/json 193 | description: 删除频道 194 | parameters: 195 | - description: id 196 | in: path 197 | name: id 198 | required: true 199 | type: string 200 | produces: 201 | - application/json 202 | responses: 203 | "200": 204 | description: Successful response 205 | schema: 206 | type: string 207 | summary: 删除频道 208 | tags: 209 | - channel 210 | /api/del/all/cdp: 211 | get: 212 | consumes: 213 | - application/json 214 | description: 删除全部CDP临时频道[谨慎调用] 215 | produces: 216 | - application/json 217 | responses: 218 | "200": 219 | description: Successful response 220 | schema: 221 | type: string 222 | summary: 删除全部CDP临时频道[谨慎调用] 223 | tags: 224 | - channel 225 | /api/thread/create: 226 | post: 227 | consumes: 228 | - application/json 229 | description: 创建线程 230 | parameters: 231 | - description: threadModel 232 | in: body 233 | name: threadModel 234 | required: true 235 | schema: 236 | $ref: '#/definitions/model.ThreadReq' 237 | produces: 238 | - application/json 239 | responses: 240 | "200": 241 | description: Successful response 242 | schema: 243 | $ref: '#/definitions/model.ThreadResp' 244 | summary: 创建线程 245 | tags: 246 | - thread 247 | /v1/chat/completions: 248 | post: 249 | consumes: 250 | - application/json 251 | description: 发送消息-openai 252 | parameters: 253 | - description: request 254 | in: body 255 | name: request 256 | required: true 257 | schema: 258 | $ref: '#/definitions/model.OpenAIChatCompletionRequest' 259 | - description: Authorization 260 | in: header 261 | name: Authorization 262 | type: string 263 | - description: out-time 264 | in: header 265 | name: out-time 266 | type: string 267 | produces: 268 | - application/json 269 | responses: 270 | "200": 271 | description: Successful response 272 | schema: 273 | $ref: '#/definitions/model.OpenAIChatCompletionResponse' 274 | summary: 发送消息-openai 275 | tags: 276 | - openai 277 | /v1/images/generations: 278 | post: 279 | consumes: 280 | - application/json 281 | description: 图片生成-openai 282 | parameters: 283 | - description: request 284 | in: body 285 | name: request 286 | required: true 287 | schema: 288 | $ref: '#/definitions/model.OpenAIImagesGenerationRequest' 289 | - description: Authorization 290 | in: header 291 | name: Authorization 292 | type: string 293 | - description: out-time 294 | in: header 295 | name: out-time 296 | type: string 297 | produces: 298 | - application/json 299 | responses: 300 | "200": 301 | description: Successful response 302 | schema: 303 | $ref: '#/definitions/model.OpenAIImagesGenerationResponse' 304 | summary: 图片生成-openai 305 | tags: 306 | - openai 307 | /v1/models: 308 | get: 309 | consumes: 310 | - application/json 311 | description: 模型列表-openai 312 | parameters: 313 | - description: Authorization 314 | in: header 315 | name: Authorization 316 | type: string 317 | produces: 318 | - application/json 319 | responses: 320 | "200": 321 | description: Successful response 322 | schema: 323 | $ref: '#/definitions/model.OpenaiModelListResponse' 324 | summary: 模型列表-openai 325 | tags: 326 | - openai 327 | swagger: "2.0" 328 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module coze-discord-proxy 2 | 3 | // +heroku goVersion go1.18 4 | go 1.21 5 | 6 | //toolchain go1.21.5 7 | 8 | require ( 9 | github.com/bwmarrin/discordgo v0.27.1 10 | github.com/gin-contrib/cors v1.5.0 11 | github.com/gin-gonic/gin v1.9.1 12 | github.com/go-playground/validator/v10 v10.17.0 13 | github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 14 | github.com/google/uuid v1.3.0 15 | github.com/h2non/filetype v1.1.3 16 | github.com/json-iterator/go v1.1.12 17 | github.com/pkoukk/tiktoken-go v0.1.6 18 | github.com/samber/lo v1.39.0 19 | github.com/sony/sonyflake v1.2.0 20 | github.com/swaggo/files v1.0.1 21 | github.com/swaggo/gin-swagger v1.6.0 22 | github.com/swaggo/swag v1.16.3 23 | golang.org/x/net v0.21.0 24 | ) 25 | 26 | require ( 27 | github.com/KyleBanks/depth v1.2.1 // indirect 28 | github.com/bytedance/sonic v1.10.1 // indirect 29 | github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect 30 | github.com/chenzhuoyu/iasm v0.9.0 // indirect 31 | github.com/dlclark/regexp2 v1.10.0 // indirect 32 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect 33 | github.com/gin-contrib/sse v0.1.0 // indirect 34 | github.com/go-openapi/jsonpointer v0.20.2 // indirect 35 | github.com/go-openapi/jsonreference v0.20.4 // indirect 36 | github.com/go-openapi/spec v0.20.14 // indirect 37 | github.com/go-openapi/swag v0.22.9 // indirect 38 | github.com/go-playground/locales v0.14.1 // indirect 39 | github.com/go-playground/universal-translator v0.18.1 // indirect 40 | github.com/goccy/go-json v0.10.2 // indirect 41 | github.com/google/go-cmp v0.5.9 // indirect 42 | github.com/gorilla/websocket v1.4.2 // indirect 43 | github.com/josharian/intern v1.0.0 // indirect 44 | github.com/klauspost/cpuid/v2 v2.2.5 // indirect 45 | github.com/leodido/go-urn v1.2.4 // indirect 46 | github.com/mailru/easyjson v0.7.7 // indirect 47 | github.com/mattn/go-isatty v0.0.19 // indirect 48 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 49 | github.com/modern-go/reflect2 v1.0.2 // indirect 50 | github.com/pelletier/go-toml/v2 v2.1.0 // indirect 51 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 52 | github.com/ugorji/go/codec v1.2.11 // indirect 53 | golang.org/x/arch v0.5.0 // indirect 54 | golang.org/x/crypto v0.19.0 // indirect 55 | golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect 56 | golang.org/x/sys v0.17.0 // indirect 57 | golang.org/x/text v0.14.0 // indirect 58 | golang.org/x/tools v0.18.0 // indirect 59 | google.golang.org/protobuf v1.31.0 // indirect 60 | gopkg.in/yaml.v3 v3.0.1 // indirect 61 | ) 62 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= 2 | github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= 3 | github.com/bwmarrin/discordgo v0.27.1 h1:ib9AIc/dom1E/fSIulrBwnez0CToJE113ZGt4HoliGY= 4 | github.com/bwmarrin/discordgo v0.27.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= 5 | github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= 6 | github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= 7 | github.com/bytedance/sonic v1.10.1 h1:7a1wuFXL1cMy7a3f7/VFcEtriuXQnUBhtoVfOZiaysc= 8 | github.com/bytedance/sonic v1.10.1/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= 9 | github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= 10 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= 11 | github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0= 12 | github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA= 13 | github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo= 14 | github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= 15 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 16 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 17 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 18 | github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= 19 | github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 20 | github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= 21 | github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= 22 | github.com/gin-contrib/cors v1.5.0 h1:DgGKV7DDoOn36DFkNtbHrjoRiT5ExCe+PC9/xp7aKvk= 23 | github.com/gin-contrib/cors v1.5.0/go.mod h1:TvU7MAZ3EwrPLI2ztzTt3tqgvBCq+wn8WpZmfADjupI= 24 | github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= 25 | github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= 26 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 27 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 28 | github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= 29 | github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= 30 | github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q= 31 | github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs= 32 | github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU= 33 | github.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4= 34 | github.com/go-openapi/spec v0.20.14 h1:7CBlRnw+mtjFGlPDRZmAMnq35cRzI91xj03HVyUi/Do= 35 | github.com/go-openapi/spec v0.20.14/go.mod h1:8EOhTpBoFiask8rrgwbLC3zmJfz4zsCUueRuPM6GNkw= 36 | github.com/go-openapi/swag v0.22.9 h1:XX2DssF+mQKM2DHsbgZK74y/zj4mo9I99+89xUmuZCE= 37 | github.com/go-openapi/swag v0.22.9/go.mod h1:3/OXnFfnMAwBD099SwYRk7GD3xOrr1iL7d/XNLXVVwE= 38 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 39 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 40 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 41 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 42 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 43 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 44 | github.com/go-playground/validator/v10 v10.17.0 h1:SmVVlfAOtlZncTxRuinDPomC2DkXJ4E5T9gDA0AIH74= 45 | github.com/go-playground/validator/v10 v10.17.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= 46 | github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc= 47 | github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8= 48 | github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= 49 | github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 50 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 51 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 52 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 53 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 54 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 55 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 56 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 57 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 58 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 59 | github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg= 60 | github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY= 61 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 62 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 63 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 64 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 65 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 66 | github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= 67 | github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 68 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 69 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 70 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 71 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 72 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 73 | github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= 74 | github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= 75 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 76 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 77 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 78 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 79 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 80 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 81 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 82 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 83 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 84 | github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= 85 | github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= 86 | github.com/pkoukk/tiktoken-go v0.1.6 h1:JF0TlJzhTbrI30wCvFuiw6FzP2+/bR+FIxUdgEAcUsw= 87 | github.com/pkoukk/tiktoken-go v0.1.6/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg= 88 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 89 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 90 | github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= 91 | github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= 92 | github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA= 93 | github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= 94 | github.com/sony/sonyflake v1.2.0 h1:Pfr3A+ejSg+0SPqpoAmQgEtNDAhc2G1SUYk205qVMLQ= 95 | github.com/sony/sonyflake v1.2.0/go.mod h1:LORtCywH/cq10ZbyfhKrHYgAUGH7mOBa76enV9txy/Y= 96 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 97 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 98 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 99 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 100 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 101 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 102 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 103 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 104 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 105 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 106 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 107 | github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= 108 | github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= 109 | github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M= 110 | github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo= 111 | github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg= 112 | github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk= 113 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 114 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 115 | github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= 116 | github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 117 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 118 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 119 | golang.org/x/arch v0.5.0 h1:jpGode6huXQxcskEIpOCvrU+tzo81b6+oFLUYXWtH/Y= 120 | golang.org/x/arch v0.5.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 121 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 122 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 123 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 124 | golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= 125 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 126 | golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM= 127 | golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= 128 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 129 | golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8= 130 | golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 131 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 132 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 133 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 134 | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 135 | golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= 136 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 137 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 138 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 139 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 140 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 141 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 142 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 143 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 144 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 145 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 146 | golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= 147 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 148 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 149 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 150 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 151 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 152 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 153 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 154 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 155 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 156 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 157 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 158 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 159 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 160 | golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ= 161 | golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg= 162 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 163 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 164 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 165 | google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= 166 | google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 167 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 168 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 169 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 170 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 171 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 172 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 173 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= 174 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 175 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // @title COZE-DISCORD-PROXY 2 | // @version 1.0.0 3 | // @description COZE-DISCORD-PROXY 代理服务 4 | // @BasePath 5 | package main 6 | 7 | import ( 8 | "context" 9 | "coze-discord-proxy/common" 10 | "coze-discord-proxy/common/config" 11 | "coze-discord-proxy/discord" 12 | "coze-discord-proxy/middleware" 13 | "coze-discord-proxy/router" 14 | "errors" 15 | "github.com/gin-gonic/gin" 16 | "net/http" 17 | "os" 18 | "os/signal" 19 | "strconv" 20 | "syscall" 21 | "time" 22 | ) 23 | 24 | func main() { 25 | ctx, cancel := context.WithCancel(context.Background()) 26 | defer cancel() 27 | 28 | go discord.StartBot(ctx, discord.BotToken) 29 | 30 | common.SetupLogger() 31 | common.SysLog("COZE-DISCORD-PROXY " + common.Version + " started") 32 | if os.Getenv("GIN_MODE") != "debug" { 33 | gin.SetMode(gin.ReleaseMode) 34 | } 35 | if config.DebugEnabled { 36 | common.SysLog("running in debug mode") 37 | } 38 | 39 | // Initialize HTTP server 40 | server := gin.New() 41 | 42 | server.Use(gin.Recovery()) 43 | server.Use(middleware.RequestId()) 44 | middleware.SetUpLogger(server) 45 | router.SetApiRouter(server) 46 | 47 | var port = os.Getenv("PORT") 48 | if port == "" { 49 | port = strconv.Itoa(*common.Port) 50 | } 51 | 52 | srv := &http.Server{ 53 | Addr: ":" + port, 54 | Handler: server, 55 | } 56 | 57 | go func() { 58 | if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { 59 | common.FatalLog("failed to start HTTP server: " + err.Error()) 60 | } 61 | }() 62 | 63 | // 等待中断信号 64 | sc := make(chan os.Signal, 1) 65 | signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill) 66 | <-sc 67 | 68 | // 收到信号后取消 context 69 | cancel() 70 | 71 | // 给 HTTP 服务器一些时间来关闭 72 | ctxShutDown, cancelShutDown := context.WithTimeout(context.Background(), 5*time.Second) 73 | defer cancelShutDown() 74 | 75 | if err := srv.Shutdown(ctxShutDown); err != nil { 76 | common.FatalLog("HTTP server Shutdown failed:" + err.Error()) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /middleware/auth.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "coze-discord-proxy/common" 5 | "coze-discord-proxy/common/config" 6 | "coze-discord-proxy/model" 7 | "github.com/gin-gonic/gin" 8 | "net/http" 9 | "strings" 10 | ) 11 | 12 | func isValidSecret(secret string) bool { 13 | return config.ProxySecret != "" && !common.SliceContains(config.ProxySecrets, secret) 14 | } 15 | 16 | func authHelper(c *gin.Context) { 17 | secret := c.Request.Header.Get("proxy-secret") 18 | if isValidSecret(secret) { 19 | c.JSON(http.StatusUnauthorized, gin.H{ 20 | "success": false, 21 | "message": "无权进行此操作,未提供正确的 proxy-secret", 22 | }) 23 | c.Abort() 24 | return 25 | } 26 | c.Next() 27 | return 28 | } 29 | 30 | func authHelperForOpenai(c *gin.Context) { 31 | secret := c.Request.Header.Get("Authorization") 32 | secret = strings.Replace(secret, "Bearer ", "", 1) 33 | if isValidSecret(secret) { 34 | c.JSON(http.StatusUnauthorized, model.OpenAIErrorResponse{ 35 | OpenAIError: model.OpenAIError{ 36 | Message: "authorization(proxy-secret)校验失败", 37 | Type: "invalid_request_error", 38 | Code: "invalid_authorization", 39 | }, 40 | }) 41 | c.Abort() 42 | return 43 | } 44 | 45 | if config.ProxySecret == "" { 46 | c.Request.Header.Set("Authorization", "") 47 | } 48 | 49 | c.Next() 50 | return 51 | } 52 | 53 | func Auth() func(c *gin.Context) { 54 | return func(c *gin.Context) { 55 | authHelper(c) 56 | } 57 | } 58 | 59 | func OpenAIAuth() func(c *gin.Context) { 60 | return func(c *gin.Context) { 61 | authHelperForOpenai(c) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /middleware/cors.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/gin-contrib/cors" 5 | "github.com/gin-gonic/gin" 6 | ) 7 | 8 | func CORS() gin.HandlerFunc { 9 | config := cors.DefaultConfig() 10 | config.AllowAllOrigins = true 11 | config.AllowCredentials = true 12 | config.AllowMethods = []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"} 13 | config.AllowHeaders = []string{"*"} 14 | return cors.New(config) 15 | } 16 | -------------------------------------------------------------------------------- /middleware/logger.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "coze-discord-proxy/common" 5 | "fmt" 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | func SetUpLogger(server *gin.Engine) { 10 | server.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string { 11 | var requestID string 12 | if param.Keys != nil { 13 | requestID = param.Keys[common.RequestIdKey].(string) 14 | } 15 | return fmt.Sprintf("[GIN] %s | %s | %3d | %13v | %15s | %7s %s\n", 16 | param.TimeStamp.Format("2006/01/02 - 15:04:05"), 17 | requestID, 18 | param.StatusCode, 19 | param.Latency, 20 | param.ClientIP, 21 | param.Method, 22 | param.Path, 23 | ) 24 | })) 25 | } 26 | -------------------------------------------------------------------------------- /middleware/rate-limit.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "coze-discord-proxy/common" 5 | "coze-discord-proxy/common/config" 6 | "github.com/gin-gonic/gin" 7 | "net/http" 8 | ) 9 | 10 | var timeFormat = "2006-01-02T15:04:05.000Z" 11 | 12 | var inMemoryRateLimiter common.InMemoryRateLimiter 13 | 14 | func memoryRateLimiter(c *gin.Context, maxRequestNum int, duration int64, mark string) { 15 | key := mark + c.ClientIP() 16 | if !inMemoryRateLimiter.Request(key, maxRequestNum, duration) { 17 | c.JSON(http.StatusTooManyRequests, gin.H{ 18 | "success": false, 19 | "message": "请求过于频繁,请稍后再试", 20 | }) 21 | c.Abort() 22 | return 23 | } 24 | } 25 | 26 | func rateLimitFactory(maxRequestNum int, duration int64, mark string) func(c *gin.Context) { 27 | // It's safe to call multi times. 28 | inMemoryRateLimiter.Init(config.RateLimitKeyExpirationDuration) 29 | return func(c *gin.Context) { 30 | memoryRateLimiter(c, maxRequestNum, duration, mark) 31 | } 32 | 33 | } 34 | 35 | func RequestRateLimit() func(c *gin.Context) { 36 | return rateLimitFactory(config.RequestRateLimitNum, config.RequestRateLimitDuration, "REQUEST_RATE_LIMIT") 37 | } 38 | -------------------------------------------------------------------------------- /middleware/request-id.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "context" 5 | "coze-discord-proxy/common" 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | func RequestId() func(c *gin.Context) { 10 | return func(c *gin.Context) { 11 | id := common.GetTimeString() + common.GetRandomString(8) 12 | c.Set(common.RequestIdKey, id) 13 | ctx := context.WithValue(c.Request.Context(), common.RequestIdKey, id) 14 | c.Request = c.Request.WithContext(ctx) 15 | c.Header(common.RequestIdKey, id) 16 | c.Next() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /model/bot.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type BotConfig struct { 4 | ProxySecret string `json:"proxySecret"` 5 | CozeBotId string `json:"cozeBotId"` 6 | Model []string `json:"model"` 7 | ChannelId string `json:"channelId"` 8 | MessageMaxToken string `json:"messageMaxToken"` 9 | } 10 | 11 | // FilterUniqueBotChannel 给定BotConfig切片,筛选出具有不同CozeBotId+ChannelId组合的元素 12 | func FilterUniqueBotChannel(configs []*BotConfig) []*BotConfig { 13 | seen := make(map[string]struct{}) // 使用map来跟踪已见的CozeBotId+ChannelId组合 14 | var uniqueConfigs []*BotConfig 15 | 16 | for _, config := range configs { 17 | combo := config.CozeBotId + "+" + config.ChannelId // 创建组合键 18 | if _, exists := seen[combo]; !exists { 19 | seen[combo] = struct{}{} // 标记组合键为已见 20 | uniqueConfigs = append(uniqueConfigs, config) 21 | } 22 | } 23 | 24 | return uniqueConfigs 25 | } 26 | -------------------------------------------------------------------------------- /model/channel.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type ChannelResp struct { 4 | Id string `json:"id" swaggertype:"string" description:"频道ID"` 5 | Name string `json:"name" swaggertype:"string" description:"频道名称"` 6 | } 7 | 8 | type ChannelStopChan struct { 9 | Id string `json:"id" ` 10 | IsNew bool `json:"IsNew"` 11 | } 12 | 13 | type ChannelReq struct { 14 | ParentId string `json:"parentId" swaggertype:"string" description:"父频道Id,为空时默认为创建父频道"` 15 | Type int `json:"type" swaggertype:"number" description:"类型:[0:文本频道,4:频道分类](其它枚举请查阅discord-api文档)"` 16 | Name string `json:"name" swaggertype:"string" description:"频道名称"` 17 | } 18 | -------------------------------------------------------------------------------- /model/chat.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type ChatReq struct { 4 | ChannelId *string `json:"channelId" swaggertype:"string" description:"频道ID/线程ID"` 5 | Content string `json:"content" swaggertype:"string" description:"消息内容"` 6 | Stream bool `json:"stream" swaggertype:"boolean" description:"是否流式返回"` 7 | } 8 | 9 | func (request ChatReq) GetChannelId() *string { 10 | return request.ChannelId 11 | } 12 | -------------------------------------------------------------------------------- /model/openai.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type OpenAIChatCompletionRequest struct { 4 | Model string `json:"model"` 5 | Stream bool `json:"stream"` 6 | Messages []OpenAIChatMessage `json:"messages"` 7 | OpenAIChatCompletionExtraRequest 8 | } 9 | 10 | type OpenAIChatCompletionExtraRequest struct { 11 | ChannelId *string `json:"channelId"` 12 | } 13 | 14 | type OpenAIChatMessage struct { 15 | Role string `json:"role"` 16 | Content interface{} `json:"content"` 17 | } 18 | 19 | type OpenAIErrorResponse struct { 20 | OpenAIError OpenAIError `json:"error"` 21 | } 22 | 23 | type OpenAIError struct { 24 | Message string `json:"message"` 25 | Type string `json:"type"` 26 | Param string `json:"param"` 27 | Code string `json:"code"` 28 | } 29 | 30 | type OpenAIChatCompletionChan struct { 31 | Model string `json:"model"` 32 | Response chan OpenAIChatCompletionResponse 33 | } 34 | 35 | type OpenAIChatCompletionResponse struct { 36 | ID string `json:"id"` 37 | Object string `json:"object"` 38 | Created int64 `json:"created"` 39 | Model string `json:"model"` 40 | Choices []OpenAIChoice `json:"choices"` 41 | Usage OpenAIUsage `json:"usage"` 42 | SystemFingerprint *string `json:"system_fingerprint"` 43 | Suggestions []string `json:"suggestions"` 44 | } 45 | 46 | type OpenAIChoice struct { 47 | Index int `json:"index"` 48 | Message OpenAIMessage `json:"message"` 49 | LogProbs *string `json:"logprobs"` 50 | FinishReason *string `json:"finish_reason"` 51 | Delta OpenAIDelta `json:"delta"` 52 | } 53 | 54 | type OpenAIMessage struct { 55 | Role string `json:"role"` 56 | Content string `json:"content"` 57 | } 58 | 59 | type OpenAIUsage struct { 60 | PromptTokens int `json:"prompt_tokens"` 61 | CompletionTokens int `json:"completion_tokens"` 62 | TotalTokens int `json:"total_tokens"` 63 | } 64 | 65 | type OpenAIDelta struct { 66 | Content string `json:"content"` 67 | } 68 | 69 | type OpenAIImagesGenerationRequest struct { 70 | OpenAIChatCompletionExtraRequest 71 | Model string `json:"model"` 72 | Prompt string `json:"prompt"` 73 | ResponseFormat string `json:"response_format"` 74 | } 75 | 76 | type OpenAIImagesGenerationResponse struct { 77 | Created int64 `json:"created"` 78 | DailyLimit bool `json:"dailyLimit"` 79 | Data []*OpenAIImagesGenerationDataResponse `json:"data"` 80 | Suggestions []string `json:"suggestions"` 81 | } 82 | 83 | type OpenAIImagesGenerationDataResponse struct { 84 | URL string `json:"url"` 85 | RevisedPrompt string `json:"revised_prompt"` 86 | B64Json string `json:"b64_json"` 87 | } 88 | 89 | type OpenAIGPT4VImagesReq struct { 90 | Type string `json:"type"` 91 | Text string `json:"text"` 92 | ImageURL struct { 93 | URL string `json:"url"` 94 | } `json:"image_url"` 95 | } 96 | 97 | // Model represents a model with its properties. 98 | type OpenaiModelResponse struct { 99 | ID string `json:"id"` 100 | Object string `json:"object"` 101 | //Created time.Time `json:"created"` 102 | //OwnedBy string `json:"owned_by"` 103 | } 104 | 105 | // ModelList represents a list of models. 106 | type OpenaiModelListResponse struct { 107 | Object string `json:"object"` 108 | Data []OpenaiModelResponse `json:"data"` 109 | } 110 | 111 | //type ChannelIdentifier interface { 112 | // GetChannelId() *string 113 | //} 114 | // 115 | //func (request OpenAIChatCompletionRequest) GetChannelId() *string { 116 | // return request.ChannelId 117 | //} 118 | // 119 | //func (request OpenAIImagesGenerationRequest) GetChannelId() *string { 120 | // return request.ChannelId 121 | //} 122 | -------------------------------------------------------------------------------- /model/reply.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type ReplyResp struct { 4 | Content string `json:"content" swaggertype:"string" description:"回复内容"` 5 | EmbedUrls []string `json:"embedUrls" swaggertype:"array,string" description:"嵌入网址"` 6 | } 7 | -------------------------------------------------------------------------------- /model/thread.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type ThreadResp struct { 4 | Id string `json:"id" swaggertype:"string" description:"线程ID"` 5 | Name string `json:"name" swaggertype:"string" description:"线程名称"` 6 | } 7 | 8 | type ThreadReq struct { 9 | ChannelId string `json:"channelId" swaggertype:"string" description:"频道Id"` 10 | Name string `json:"name" swaggertype:"string" description:"线程名称"` 11 | ArchiveDuration int `json:"archiveDuration" swaggertype:"number" description:"线程存档时间[分钟]"` 12 | } 13 | -------------------------------------------------------------------------------- /router/api-router.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "coze-discord-proxy/common/config" 5 | "coze-discord-proxy/controller" 6 | _ "coze-discord-proxy/docs" 7 | "coze-discord-proxy/middleware" 8 | "github.com/gin-gonic/gin" 9 | swaggerFiles "github.com/swaggo/files" 10 | ginSwagger "github.com/swaggo/gin-swagger" 11 | ) 12 | 13 | func SetApiRouter(router *gin.Engine) { 14 | router.Use(middleware.CORS()) 15 | //router.Use(gzip.Gzip(gzip.DefaultCompression)) 16 | router.Use(middleware.RequestRateLimit()) 17 | if config.SwaggerEnable == "" || config.SwaggerEnable == "1" { 18 | router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) 19 | } 20 | 21 | if config.OnlyOpenaiApi != "1" { 22 | apiRouter := router.Group("/api") 23 | apiRouter.Use(middleware.Auth()) 24 | { 25 | //chatRoute := apiRouter.Group("/chat") 26 | //chatRoute.POST("", controller.Chat) 27 | 28 | channelRoute := apiRouter.Group("/channel") 29 | channelRoute.POST("/create", controller.ChannelCreate) 30 | channelRoute.GET("/del/:id", controller.ChannelDel) 31 | channelRoute.GET("/del/all/cdp", controller.ChannelDelAllCdp) 32 | 33 | threadRoute := apiRouter.Group("/thread") 34 | threadRoute.POST("/create", controller.ThreadCreate) 35 | } 36 | } 37 | 38 | //https://api.openai.com/v1/images/generations 39 | v1Router := router.Group("/v1") 40 | v1Router.Use(middleware.OpenAIAuth()) 41 | v1Router.POST("/chat/completions", controller.ChatForOpenAI) 42 | v1Router.POST("/images/generations", controller.ImagesForOpenAI) 43 | v1Router.GET("/models", controller.OpenaiModels) 44 | } 45 | -------------------------------------------------------------------------------- /telegram/bot.go: -------------------------------------------------------------------------------- 1 | package telegram 2 | 3 | import ( 4 | tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" 5 | "os" 6 | ) 7 | 8 | var NotifyTelegramBotToken = os.Getenv("NOTIFY_TELEGRAM_BOT_TOKEN") 9 | var NotifyTelegramUserId = os.Getenv("NOTIFY_TELEGRAM_USER_ID") 10 | 11 | var NotifyTelegramUserIdInt64 int64 12 | 13 | var TgBot *tgbotapi.BotAPI 14 | 15 | func InitTelegramBot() (err error) { 16 | 17 | TgBot, err = tgbotapi.NewBotAPI(NotifyTelegramBotToken) 18 | if err != nil { 19 | return err 20 | } 21 | TgBot.Debug = false 22 | return nil 23 | } 24 | 25 | func SendMessage(chattable tgbotapi.Chattable) (err error) { 26 | _, err = TgBot.Send(chattable) 27 | if err != nil { 28 | return err 29 | } 30 | return nil 31 | } 32 | --------------------------------------------------------------------------------