├── .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 |

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 | [](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 | [](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 |

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 | [](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 | [](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("", 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", 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", 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", 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", 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 |
--------------------------------------------------------------------------------