├── .github
├── 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.md
├── VERSION
├── check
└── check.go
├── common
├── config
│ └── config.go
├── constants.go
├── database.go
├── embed-file-system.go
├── env
│ └── helper.go
├── filetype.go
├── helper
│ ├── helper.go
│ ├── key.go
│ └── time.go
├── init.go
├── loggger
│ ├── constants.go
│ └── logger.go
├── random
│ └── main.go
├── rate-limit.go
├── response.go
├── send-res.go
├── snowflakeid.go
└── utils.go
├── controller
└── chat.go
├── cycletls
├── client.go
├── connect.go
├── cookie.go
├── errors.go
├── extensions.go
├── index.go
├── roundtripper.go
└── utils.go
├── docker-compose.yml
├── docs
├── docs.go
├── img.png
├── img2.png
├── swagger.json
└── swagger.yaml
├── go.mod
├── go.sum
├── main.go
├── middleware
├── auth.go
├── cache.go
├── cors.go
├── ip-list.go
├── logger.go
├── rate-limit.go
└── request-id.go
├── model
├── openai.go
└── token_encoder.go
├── router
├── api-router.go
├── main.go
└── web.go
└── unlimitedai-api
└── api.go
/.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/unlimitedai2api'
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 |
14 | def get_stargazers(repo):
15 | page = 1
16 | _stargazers = {}
17 | while True:
18 | queries = {
19 | 'per_page': 100,
20 | 'page': page,
21 | }
22 | url = 'https://api.github.com/repos/{}/stargazers?'.format(repo)
23 |
24 | resp = requests.get(url, headers=headers, params=queries)
25 | if resp.status_code != 200:
26 | raise Exception('Error get stargazers: ' + resp.text)
27 |
28 | data = resp.json()
29 | if not data:
30 | break
31 |
32 | for stargazer in data:
33 | _stargazers[stargazer['login']] = True
34 | page += 1
35 |
36 | print('list stargazers done, total: ' + str(len(_stargazers)))
37 | return _stargazers
38 |
39 |
40 | def get_issues(repo):
41 | page = 1
42 | _issues = []
43 | while True:
44 | queries = {
45 | 'state': 'open',
46 | 'sort': 'created',
47 | 'direction': 'desc',
48 | 'per_page': 100,
49 | 'page': page,
50 | }
51 | url = 'https://api.github.com/repos/{}/issues?'.format(repo)
52 |
53 | resp = requests.get(url, headers=headers, params=queries)
54 | if resp.status_code != 200:
55 | raise Exception('Error get issues: ' + resp.text)
56 |
57 | data = resp.json()
58 | if not data:
59 | break
60 |
61 | _issues += data
62 | page += 1
63 |
64 | print('list issues done, total: ' + str(len(_issues)))
65 | return _issues
66 |
67 |
68 | def close_issue(repo, issue_number):
69 | url = 'https://api.github.com/repos/{}/issues/{}'.format(repo, issue_number)
70 | data = {
71 | 'state': 'closed',
72 | 'state_reason': 'not_planned',
73 | 'labels': issue_labels,
74 | }
75 | resp = requests.patch(url, headers=headers, json=data)
76 | if resp.status_code != 200:
77 | raise Exception('Error close issue: ' + resp.text)
78 |
79 | print('issue: {} closed'.format(issue_number))
80 |
81 |
82 | def lock_issue(repo, issue_number):
83 | url = 'https://api.github.com/repos/{}/issues/{}/lock'.format(repo, issue_number)
84 | data = {
85 | 'lock_reason': 'spam',
86 | }
87 | resp = requests.put(url, headers=headers, json=data)
88 | if resp.status_code != 204:
89 | raise Exception('Error lock issue: ' + resp.text)
90 |
91 | print('issue: {} locked'.format(issue_number))
92 |
93 |
94 | if '__main__' == __name__:
95 | stargazers = get_stargazers(github_repo)
96 |
97 | issues = get_issues(github_repo)
98 | for issue in issues:
99 | login = issue['user']['login']
100 | if login not in stargazers:
101 | print('issue: {}, login: {} not in stargazers'.format(issue['number'], login))
102 | close_issue(github_repo, issue['number'])
103 | lock_issue(github_repo, issue['number'])
104 |
105 | print('done')
106 |
--------------------------------------------------------------------------------
/.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/unlimitedai2api
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/unlimitedai2api
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 'unlimitedai2api/common.Version=$(git describe --tags)' -extldflags '-static'" -o unlimitedai2api
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 'unlimitedai2api/common.Version=$(git describe --tags)' -extldflags '-static'" -o unlimitedai2api-arm64
35 |
36 | - name: Release
37 | uses: softprops/action-gh-release@v1
38 | if: startsWith(github.ref, 'refs/tags/')
39 | with:
40 | files: |
41 | unlimitedai2api
42 | unlimitedai2api-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 'unlimitedai2api/common.Version=$(git describe --tags)'" -o unlimitedai2api-macos
29 | - name: Release
30 | uses: softprops/action-gh-release@v1
31 | if: startsWith(github.ref, 'refs/tags/')
32 | with:
33 | files: unlimitedai2api-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 'unlimitedai2api/common.Version=$(git describe --tags)'" -o unlimitedai2api.exe
32 | - name: Release
33 | uses: softprops/action-gh-release@v1
34 | if: startsWith(github.ref, 'refs/tags/')
35 | with:
36 | files: unlimitedai2api.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
7 | *.db-journal
8 | logs
9 | data
10 | dist
11 | /web/node_modules
12 | /web/node_modules
13 | cmd.md
14 | .env
15 | temp
16 | .DS_Store
--------------------------------------------------------------------------------
/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 /unlimitedai2api
20 |
21 | # 使用 Alpine 镜像作为最终镜像
22 | FROM alpine
23 |
24 | # 安装基本的运行时依赖
25 | RUN apk --no-cache add ca-certificates tzdata
26 |
27 | # 从构建阶段复制可执行文件
28 | COPY --from=builder /unlimitedai2api .
29 |
30 | # 暴露端口
31 | EXPOSE 10033
32 | # 工作目录
33 | WORKDIR /app/unlimitedai2api/data
34 | # 设置入口命令
35 | ENTRYPOINT ["/unlimitedai2api"]
36 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | 中文
3 |
4 |
5 |
6 | # unlimitedai2api
7 |
8 | _觉得有点意思的话 别忘了点个 ⭐_
9 |
10 |
11 |
12 | Telegram 交流群
13 |
14 |
15 |
(原`coze-discord-proxy`交流群, 此项目仍可进此群**交流** / **反馈bug**)
16 |
(群内提供公益API、AI机器人)
17 |
18 |
19 |
20 | ## 功能
21 |
22 | - [x] 支持对话接口(流式/非流式)(`/chat/completions`),详情查看[支持模型](#支持模型)
23 | - [x] 支持自定义请求头校验值(Authorization)
24 | - [x] 可配置代理请求(环境变量`PROXY_URL`)
25 |
26 | ### 接口文档:
27 |
28 | 略
29 |
30 | ### 示例:
31 |
32 | 略
33 |
34 | ## 如何使用
35 |
36 | 略
37 |
38 | ## 如何集成NextChat
39 |
40 | 略
41 |
42 | ## 如何集成one-api
43 |
44 | 略
45 |
46 | ## 部署
47 |
48 | ### 基于 Docker-Compose(All In One) 进行部署
49 |
50 | ```shell
51 | docker-compose pull && docker-compose up -d
52 | ```
53 |
54 | #### docker-compose.yml
55 |
56 | ```docker
57 | version: '3.4'
58 |
59 | services:
60 | unlimitedai2api:
61 | image: deanxv/unlimitedai2api:latest
62 | container_name: unlimitedai2api
63 | restart: always
64 | ports:
65 | - "10033:10033"
66 | volumes:
67 | - ./data:/app/unlimitedai2api/data
68 | environment:
69 | - API_SECRET=123456 # [可选]接口密钥-修改此行为请求头校验的值(多个请以,分隔)
70 | - TZ=Asia/Shanghai
71 | ```
72 |
73 | ### 基于 Docker 进行部署
74 |
75 | ```docker
76 | docker run --name unlimitedai2api -d --restart always \
77 | -p 10033:10033 \
78 | -v $(pwd)/data:/app/unlimitedai2api/data \
79 | -e API_SECRET="123456" \
80 | -e TZ=Asia/Shanghai \
81 | deanxv/unlimitedai2api
82 | ```
83 |
84 | 其中`API_SECRET`修改为自己的。
85 |
86 | 如果上面的镜像无法拉取,可以尝试使用 GitHub 的 Docker 镜像,将上面的`deanxv/unlimitedai2api`替换为
87 | `ghcr.io/deanxv/unlimitedai2api`即可。
88 |
89 | ### 部署到第三方平台
90 |
91 |
92 | 部署到 Zeabur
93 |
94 |
95 | [](https://zeabur.com?referralCode=deanxv&utm_source=deanxv)
96 |
97 | > Zeabur 的服务器在国外,自动解决了网络的问题,~~同时免费的额度也足够个人使用~~
98 |
99 | 1. 首先 **fork** 一份代码。
100 | 2. 进入 [Zeabur](https://zeabur.com?referralCode=deanxv),使用github登录,进入控制台。
101 | 3. 在 Service -> Add Service,选择 Git(第一次使用需要先授权),选择你 fork 的仓库。
102 | 4. Deploy 会自动开始,先取消。
103 | 5. 添加环境变量
104 |
105 | `API_SECRET:123456` [可选]接口密钥-修改此行为请求头校验的值(多个请以,分隔)(与openai-API-KEY用法一致)
106 |
107 | 保存。
108 |
109 | 6. 选择 Redeploy。
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 | 部署到 Render
118 |
119 |
120 | > Render 提供免费额度,绑卡后可以进一步提升额度
121 |
122 | Render 可以直接部署 docker 镜像,不需要 fork 仓库:[Render](https://dashboard.render.com)
123 |
124 |
125 |
126 |
127 | ## 配置
128 |
129 | ### 环境变量
130 |
131 | 1. `PORT=10033` [可选]端口,默认为10033
132 | 2. `DEBUG=true` [可选]DEBUG模式,可打印更多信息[true:打开、false:关闭]
133 | 3. `API_SECRET=123456` [可选]接口密钥-修改此行为请求头(Authorization)校验的值(同API-KEY)(多个请以,分隔)
134 | 4. `REQUEST_RATE_LIMIT=60` [可选]每分钟下的单ip请求速率限制,默认:60次/min
135 | 5. `PROXY_URL=http://127.0.0.1:10801` [可选]代理
136 | 6. `ROUTE_PREFIX=hf` [可选]路由前缀,默认为空,添加该变量后的接口示例:`/hf/v1/chat/completions`
137 | 7. `REASONING_HIDE=0` [可选]**隐藏**推理过程(默认:0)[0:关闭,1:开启]
138 |
139 | ## 进阶配置
140 |
141 | 略
142 |
143 | ## 支持模型
144 |
145 | | 模型名称 |
146 | |----------------------|
147 | | chat-model-reasoning |
148 |
149 | ## 报错排查
150 |
151 | 略
152 |
153 | ## 其他
154 |
155 | [unlimitedai](https://app.unlimitedai.chat/)
--------------------------------------------------------------------------------
/VERSION:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deanxv/unlimitedai2api/e0ee1afba94febb1e779dc92941d9d450916b500/VERSION
--------------------------------------------------------------------------------
/check/check.go:
--------------------------------------------------------------------------------
1 | package check
2 |
3 | import (
4 | logger "unlimitedai2api/common/loggger"
5 | )
6 |
7 | func CheckEnvVariable() {
8 | logger.SysLog("environment variable checking...")
9 |
10 | //if config.KLCookie == "" {
11 | // logger.FatalLog("环境变量 KL_COOKIE 未设置")
12 | //}
13 |
14 | logger.SysLog("environment variable check passed.")
15 | }
16 |
--------------------------------------------------------------------------------
/common/config/config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "errors"
5 | "math/rand"
6 | "os"
7 | "strings"
8 | "sync"
9 | "time"
10 | "unlimitedai2api/common/env"
11 | )
12 |
13 | var BackendSecret = os.Getenv("BACKEND_SECRET")
14 | var KLCookie = os.Getenv("KL_COOKIE")
15 | var MysqlDsn = os.Getenv("MYSQL_DSN")
16 | var IpBlackList = strings.Split(os.Getenv("IP_BLACK_LIST"), ",")
17 | var DebugSQLEnabled = strings.ToLower(os.Getenv("DEBUG_SQL")) == "true"
18 | var ProxyUrl = env.String("PROXY_URL", "")
19 | var UserAgent = env.String("USER_AGENT", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome")
20 | var CheatEnabled = env.Bool("CHEAT_ENABLED", false)
21 | var CheatUrl = env.String("CHEAT_URL", "https://kl.goeast.io/kilo/cheat")
22 | var ChatMaxDays = env.Int("CHAT_MAX_DAYS", -1)
23 | var ApiSecret = os.Getenv("API_SECRET")
24 | var ApiSecrets = strings.Split(os.Getenv("API_SECRET"), ",")
25 |
26 | var RateLimitCookieLockDuration = env.Int("RATE_LIMIT_COOKIE_LOCK_DURATION", 10*60)
27 |
28 | // 隐藏思考过程
29 | var ReasoningHide = env.Int("REASONING_HIDE", 0)
30 |
31 | // 前置message
32 | var PRE_MESSAGES_JSON = env.String("PRE_MESSAGES_JSON", "")
33 |
34 | // 路由前缀
35 | var RoutePrefix = env.String("ROUTE_PREFIX", "")
36 | var SwaggerEnable = os.Getenv("SWAGGER_ENABLE")
37 | var BackendApiEnable = env.Int("BACKEND_API_ENABLE", 1)
38 |
39 | var DebugEnabled = os.Getenv("DEBUG") == "true"
40 |
41 | var RateLimitKeyExpirationDuration = 20 * time.Minute
42 |
43 | var RequestOutTimeDuration = 5 * time.Minute
44 |
45 | var (
46 | RequestRateLimitNum = env.Int("REQUEST_RATE_LIMIT", 60)
47 | RequestRateLimitDuration int64 = 1 * 60
48 | )
49 |
50 | type RateLimitCookie struct {
51 | ExpirationTime time.Time // 过期时间
52 | }
53 |
54 | var (
55 | rateLimitCookies sync.Map // 使用 sync.Map 管理限速 Cookie
56 | )
57 |
58 | func AddRateLimitCookie(cookie string, expirationTime time.Time) {
59 | rateLimitCookies.Store(cookie, RateLimitCookie{
60 | ExpirationTime: expirationTime,
61 | })
62 | //fmt.Printf("Storing cookie: %s with value: %+v\n", cookie, RateLimitCookie{ExpirationTime: expirationTime})
63 | }
64 |
65 | var (
66 | KLCookies []string // 存储所有的 cookies
67 | cookiesMutex sync.Mutex // 保护 KLCookies 的互斥锁
68 | )
69 |
70 | func InitSGCookies() {
71 | cookiesMutex.Lock()
72 | defer cookiesMutex.Unlock()
73 |
74 | KLCookies = []string{}
75 |
76 | // 从环境变量中读取 KL_COOKIE 并拆分为切片
77 | cookieStr := os.Getenv("KL_COOKIE")
78 | cookieStr = "test"
79 | if cookieStr != "" {
80 |
81 | for _, cookie := range strings.Split(cookieStr, ",") {
82 | KLCookies = append(KLCookies, cookie)
83 | }
84 | }
85 | }
86 |
87 | type CookieManager struct {
88 | Cookies []string
89 | currentIndex int
90 | mu sync.Mutex
91 | }
92 |
93 | // GetSGCookies 获取 KLCookies 的副本
94 | func GetKLCookies() []string {
95 | //cookiesMutex.Lock()
96 | //defer cookiesMutex.Unlock()
97 |
98 | // 返回 KLCookies 的副本,避免外部直接修改
99 | cookiesCopy := make([]string, len(KLCookies))
100 | copy(cookiesCopy, KLCookies)
101 | return cookiesCopy
102 | }
103 |
104 | func NewCookieManager() *CookieManager {
105 | var validCookies []string
106 | // 遍历 KLCookies
107 | for _, cookie := range GetKLCookies() {
108 | cookie = strings.TrimSpace(cookie)
109 | if cookie == "" {
110 | continue // 忽略空字符串
111 | }
112 |
113 | // 检查是否在 RateLimitCookies 中
114 | if value, ok := rateLimitCookies.Load(cookie); ok {
115 | rateLimitCookie, ok := value.(RateLimitCookie) // 正确转换为 RateLimitCookie
116 | if !ok {
117 | continue
118 | }
119 | if rateLimitCookie.ExpirationTime.After(time.Now()) {
120 | // 如果未过期,忽略该 cookie
121 | continue
122 | } else {
123 | // 如果已过期,从 RateLimitCookies 中删除
124 | rateLimitCookies.Delete(cookie)
125 | }
126 | }
127 |
128 | // 添加到有效 cookie 列表
129 | validCookies = append(validCookies, cookie)
130 | }
131 |
132 | return &CookieManager{
133 | Cookies: validCookies,
134 | currentIndex: 0,
135 | }
136 | }
137 |
138 | func (cm *CookieManager) GetRandomCookie() (string, error) {
139 | cm.mu.Lock()
140 | defer cm.mu.Unlock()
141 |
142 | if len(cm.Cookies) == 0 {
143 | return "", errors.New("no cookies available")
144 | }
145 |
146 | // 生成随机索引
147 | randomIndex := rand.Intn(len(cm.Cookies))
148 | // 更新当前索引
149 | cm.currentIndex = randomIndex
150 |
151 | return cm.Cookies[randomIndex], nil
152 | }
153 |
154 | func (cm *CookieManager) GetNextCookie() (string, error) {
155 | cm.mu.Lock()
156 | defer cm.mu.Unlock()
157 |
158 | if len(cm.Cookies) == 0 {
159 | return "", errors.New("no cookies available")
160 | }
161 |
162 | cm.currentIndex = (cm.currentIndex + 1) % len(cm.Cookies)
163 | return cm.Cookies[cm.currentIndex], nil
164 | }
165 |
166 | // RemoveCookie 删除指定的 cookie(支持并发)
167 | func RemoveCookie(cookieToRemove string) {
168 | cookiesMutex.Lock()
169 | defer cookiesMutex.Unlock()
170 |
171 | // 创建一个新的切片,过滤掉需要删除的 cookie
172 | var newCookies []string
173 | for _, cookie := range GetKLCookies() {
174 | if cookie != cookieToRemove {
175 | newCookies = append(newCookies, cookie)
176 | }
177 | }
178 |
179 | // 更新 GSCookies
180 | KLCookies = newCookies
181 | }
182 |
--------------------------------------------------------------------------------
/common/constants.go:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | import "time"
4 |
5 | var StartTime = time.Now().Unix() // unit: second
6 | var Version = "v1.1.0" // this hard coding will be replaced automatically when building, no need to manually change
7 |
8 | type ModelInfo struct {
9 | Model string
10 | MaxTokens int
11 | }
12 |
13 | // ModelRegistry 创建映射表(假设用 model 名称作为 key)
14 | var ModelRegistry = map[string]ModelInfo{
15 | "chat-model-reasoning": {"chat-model-reasoning", 100000},
16 | }
17 |
18 | // 通过 model 名称查询的方法
19 | func GetModelInfo(modelName string) (ModelInfo, bool) {
20 | info, exists := ModelRegistry[modelName]
21 | return info, exists
22 | }
23 |
24 | func GetModelList() []string {
25 | var modelList []string
26 | for k := range ModelRegistry {
27 | modelList = append(modelList, k)
28 | }
29 | return modelList
30 | }
31 |
--------------------------------------------------------------------------------
/common/database.go:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | var UsingSQLite = false
4 | var UsingPostgreSQL = false
5 | var UsingMySQL = false
6 |
7 | //var SQLitePath = "kilo-2-api.db"
8 | //var SQLiteBusyTimeout = env.Int("SQLITE_BUSY_TIMEOUT", 3000)
9 |
--------------------------------------------------------------------------------
/common/embed-file-system.go:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | import (
4 | "embed"
5 | "github.com/gin-contrib/static"
6 | "io/fs"
7 | "net/http"
8 | )
9 |
10 | type embedFileSystem struct {
11 | http.FileSystem
12 | }
13 |
14 | func (e embedFileSystem) Exists(prefix string, path string) bool {
15 | _, err := e.Open(path)
16 | return err == nil
17 | }
18 |
19 | func EmbedFolder(fsEmbed embed.FS, targetPath string) static.ServeFileSystem {
20 | efs, err := fs.Sub(fsEmbed, targetPath)
21 | if err != nil {
22 | panic(err)
23 | }
24 | return embedFileSystem{
25 | FileSystem: http.FS(efs),
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/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/filetype.go:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | import (
4 | "bytes"
5 | "encoding/base64"
6 | "fmt"
7 | "regexp"
8 | "strings"
9 | )
10 |
11 | // 文件类型常量
12 | const (
13 | TXT_TYPE = "text/plain"
14 | PDF_TYPE = "application/pdf"
15 | DOC_TYPE = "application/msword"
16 | JPG_TYPE = "image/jpeg"
17 | PNG_TYPE = "image/png"
18 | WEBP_TYPE = "image/webp"
19 | )
20 |
21 | // 检测文件类型结果
22 | type FileTypeResult struct {
23 | MimeType string
24 | Extension string
25 | Description string
26 | IsValid bool
27 | }
28 |
29 | // 从带前缀的base64数据中直接解析MIME类型
30 | func getMimeTypeFromDataURI(dataURI string) string {
31 | // data:text/plain;base64,xxxxx 格式
32 | regex := regexp.MustCompile(`data:([^;]+);base64,`)
33 | matches := regex.FindStringSubmatch(dataURI)
34 | if len(matches) > 1 {
35 | return matches[1]
36 | }
37 | return ""
38 | }
39 |
40 | // 检测是否为文本文件的函数 - 增强版
41 | func isTextFile(data []byte) bool {
42 | // 检查多种文本文件格式
43 |
44 | // 如果数据为空,则不是有效的文本文件
45 | if len(data) == 0 {
46 | return false
47 | }
48 |
49 | // 检查是否有BOM (UTF-8, UTF-16)
50 | if bytes.HasPrefix(data, []byte{0xEF, 0xBB, 0xBF}) || // UTF-8 BOM
51 | bytes.HasPrefix(data, []byte{0xFE, 0xFF}) || // UTF-16 BE BOM
52 | bytes.HasPrefix(data, []byte{0xFF, 0xFE}) { // UTF-16 LE BOM
53 | return true
54 | }
55 |
56 | // 检查是否只包含ASCII字符或常见UTF-8序列
57 | // 我们会检查文件的前4KB和最后1KB(或整个文件如果小于5KB)
58 | checkSize := 4096
59 | if len(data) < checkSize {
60 | checkSize = len(data)
61 | }
62 |
63 | totalNonPrintable := 0
64 | totalChars := 0
65 |
66 | // 检查文件开头
67 | for i := 0; i < checkSize; i++ {
68 | b := data[i]
69 | totalChars++
70 |
71 | // 允许常见控制字符:TAB(9), LF(10), CR(13)
72 | if b != 9 && b != 10 && b != 13 {
73 | // 检查是否为可打印ASCII或常见UTF-8多字节序列的开始
74 | if (b < 32 || b > 126) && b < 192 { // 非可打印ASCII且不是UTF-8多字节序列开始
75 | totalNonPrintable++
76 | }
77 | }
78 | }
79 |
80 | // 如果文件较大,也检查文件结尾
81 | if len(data) > 5120 {
82 | endOffset := len(data) - 1024
83 | for i := 0; i < 1024; i++ {
84 | b := data[endOffset+i]
85 | totalChars++
86 |
87 | if b != 9 && b != 10 && b != 13 {
88 | if (b < 32 || b > 126) && b < 192 {
89 | totalNonPrintable++
90 | }
91 | }
92 | }
93 | }
94 |
95 | // 如果非可打印字符比例低于5%,则认为是文本文件
96 | return float64(totalNonPrintable)/float64(totalChars) < 0.05
97 | }
98 |
99 | // 增强的文件类型检测,专门处理text/plain
100 | func DetectFileType(base64Data string) *FileTypeResult {
101 | // 检查是否有数据URI前缀
102 | mimeFromPrefix := getMimeTypeFromDataURI(base64Data)
103 | if mimeFromPrefix == TXT_TYPE {
104 | // 直接从前缀确认是文本类型
105 | return &FileTypeResult{
106 | MimeType: TXT_TYPE,
107 | Extension: ".txt",
108 | Description: "Plain Text Document",
109 | IsValid: true,
110 | }
111 | }
112 |
113 | // 移除base64前缀
114 | commaIndex := strings.Index(base64Data, ",")
115 | if commaIndex != -1 {
116 | base64Data = base64Data[commaIndex+1:]
117 | }
118 |
119 | // 解码base64
120 | data, err := base64.StdEncoding.DecodeString(base64Data)
121 | if err != nil {
122 | return &FileTypeResult{
123 | IsValid: false,
124 | Description: "Base64 解码失败",
125 | }
126 | }
127 |
128 | // 检查常见文件魔数
129 | if len(data) >= 4 && bytes.HasPrefix(data, []byte("%PDF")) {
130 | return &FileTypeResult{
131 | MimeType: PDF_TYPE,
132 | Extension: ".pdf",
133 | Description: "PDF Document",
134 | IsValid: true,
135 | }
136 | }
137 |
138 | if len(data) >= 3 && data[0] == 0xFF && data[1] == 0xD8 && data[2] == 0xFF {
139 | return &FileTypeResult{
140 | MimeType: JPG_TYPE,
141 | Extension: ".jpg",
142 | Description: "JPEG Image",
143 | IsValid: true,
144 | }
145 | }
146 |
147 | if len(data) >= 8 && bytes.HasPrefix(data, []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}) {
148 | return &FileTypeResult{
149 | MimeType: PNG_TYPE,
150 | Extension: ".png",
151 | Description: "PNG Image",
152 | IsValid: true,
153 | }
154 | }
155 |
156 | if len(data) >= 12 && bytes.HasPrefix(data, []byte("RIFF")) && bytes.Equal(data[8:12], []byte("WEBP")) {
157 | return &FileTypeResult{
158 | MimeType: WEBP_TYPE,
159 | Extension: ".webp",
160 | Description: "WebP Image",
161 | IsValid: true,
162 | }
163 | }
164 |
165 | if len(data) >= 8 && bytes.HasPrefix(data, []byte{0xD0, 0xCF, 0x11, 0xE0}) {
166 | return &FileTypeResult{
167 | MimeType: DOC_TYPE,
168 | Extension: ".doc",
169 | Description: "Microsoft Word Document",
170 | IsValid: true,
171 | }
172 | }
173 |
174 | // 增强的文本检测
175 | if isTextFile(data) {
176 | return &FileTypeResult{
177 | MimeType: TXT_TYPE,
178 | Extension: ".txt",
179 | Description: "Plain Text Document",
180 | IsValid: true,
181 | }
182 | }
183 |
184 | // 默认返回未知类型
185 | return &FileTypeResult{
186 | IsValid: false,
187 | Description: "未识别文件类型",
188 | }
189 | }
190 |
191 | func main() {
192 | // 示例:检测携带MIME前缀的TXT文件
193 | textWithPrefix := "data:text/plain;base64,SGVsbG8gV29ybGQh" // "Hello World!" 的base64
194 |
195 | result := DetectFileType(textWithPrefix)
196 | if result.IsValid {
197 | fmt.Printf("检测结果: %s (%s)\n", result.Description, result.MimeType)
198 | } else {
199 | fmt.Printf("错误: %s\n", result.Description)
200 | }
201 | }
202 |
--------------------------------------------------------------------------------
/common/helper/helper.go:
--------------------------------------------------------------------------------
1 | package helper
2 |
3 | import (
4 | "fmt"
5 | "github.com/gin-gonic/gin"
6 | "html/template"
7 | "log"
8 | "net"
9 | "os/exec"
10 | "runtime"
11 | "strconv"
12 | "strings"
13 | "unlimitedai2api/common/random"
14 | )
15 |
16 | func OpenBrowser(url string) {
17 | var err error
18 |
19 | switch runtime.GOOS {
20 | case "linux":
21 | err = exec.Command("xdg-open", url).Start()
22 | case "windows":
23 | err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
24 | case "darwin":
25 | err = exec.Command("open", url).Start()
26 | }
27 | if err != nil {
28 | log.Println(err)
29 | }
30 | }
31 |
32 | func GetIp() (ip string) {
33 | ips, err := net.InterfaceAddrs()
34 | if err != nil {
35 | log.Println(err)
36 | return ip
37 | }
38 |
39 | for _, a := range ips {
40 | if ipNet, ok := a.(*net.IPNet); ok && !ipNet.IP.IsLoopback() {
41 | if ipNet.IP.To4() != nil {
42 | ip = ipNet.IP.String()
43 | if strings.HasPrefix(ip, "10") {
44 | return
45 | }
46 | if strings.HasPrefix(ip, "172") {
47 | return
48 | }
49 | if strings.HasPrefix(ip, "192.168") {
50 | return
51 | }
52 | ip = ""
53 | }
54 | }
55 | }
56 | return
57 | }
58 |
59 | var sizeKB = 1024
60 | var sizeMB = sizeKB * 1024
61 | var sizeGB = sizeMB * 1024
62 |
63 | func Bytes2Size(num int64) string {
64 | numStr := ""
65 | unit := "B"
66 | if num/int64(sizeGB) > 1 {
67 | numStr = fmt.Sprintf("%.2f", float64(num)/float64(sizeGB))
68 | unit = "GB"
69 | } else if num/int64(sizeMB) > 1 {
70 | numStr = fmt.Sprintf("%d", int(float64(num)/float64(sizeMB)))
71 | unit = "MB"
72 | } else if num/int64(sizeKB) > 1 {
73 | numStr = fmt.Sprintf("%d", int(float64(num)/float64(sizeKB)))
74 | unit = "KB"
75 | } else {
76 | numStr = fmt.Sprintf("%d", num)
77 | }
78 | return numStr + " " + unit
79 | }
80 |
81 | func Interface2String(inter interface{}) string {
82 | switch inter := inter.(type) {
83 | case string:
84 | return inter
85 | case int:
86 | return fmt.Sprintf("%d", inter)
87 | case float64:
88 | return fmt.Sprintf("%f", inter)
89 | }
90 | return "Not Implemented"
91 | }
92 |
93 | func UnescapeHTML(x string) interface{} {
94 | return template.HTML(x)
95 | }
96 |
97 | func IntMax(a int, b int) int {
98 | if a >= b {
99 | return a
100 | } else {
101 | return b
102 | }
103 | }
104 |
105 | func GenRequestID() string {
106 | return GetTimeString() + random.GetRandomNumberString(8)
107 | }
108 |
109 | func GetResponseID(c *gin.Context) string {
110 | logID := c.GetString(RequestIdKey)
111 | return fmt.Sprintf("chatcmpl-%s", logID)
112 | }
113 |
114 | func Max(a int, b int) int {
115 | if a >= b {
116 | return a
117 | } else {
118 | return b
119 | }
120 | }
121 |
122 | func AssignOrDefault(value string, defaultValue string) string {
123 | if len(value) != 0 {
124 | return value
125 | }
126 | return defaultValue
127 | }
128 |
129 | func MessageWithRequestId(message string, id string) string {
130 | return fmt.Sprintf("%s (request id: %s)", message, id)
131 | }
132 |
133 | func String2Int(str string) int {
134 | num, err := strconv.Atoi(str)
135 | if err != nil {
136 | return 0
137 | }
138 | return num
139 | }
140 |
--------------------------------------------------------------------------------
/common/helper/key.go:
--------------------------------------------------------------------------------
1 | package helper
2 |
3 | const (
4 | RequestIdKey = "X-Request-Id"
5 | )
6 |
--------------------------------------------------------------------------------
/common/helper/time.go:
--------------------------------------------------------------------------------
1 | package helper
2 |
3 | import (
4 | "fmt"
5 | "time"
6 | )
7 |
8 | func GetTimestamp() int64 {
9 | return time.Now().Unix()
10 | }
11 |
12 | func GetTimeString() string {
13 | now := time.Now()
14 | return fmt.Sprintf("%s%d", now.Format("20060102150405"), now.UnixNano()%1e9)
15 | }
16 |
--------------------------------------------------------------------------------
/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", 10033, "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("unlimitedai2api" + Version + "")
23 | fmt.Println("Copyright (C) 2025 Dean. All rights reserved.")
24 | fmt.Println("GitHub: https://github.com/deanxv/unlimitedai2api ")
25 | fmt.Println("Usage: unlimitedai2api [--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/loggger/constants.go:
--------------------------------------------------------------------------------
1 | package logger
2 |
3 | var LogDir string
4 |
--------------------------------------------------------------------------------
/common/loggger/logger.go:
--------------------------------------------------------------------------------
1 | package logger
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "io"
7 | "log"
8 | "os"
9 | "path/filepath"
10 | "sync"
11 | "time"
12 | "unlimitedai2api/common/config"
13 | "unlimitedai2api/common/helper"
14 |
15 | "github.com/gin-gonic/gin"
16 | )
17 |
18 | const (
19 | loggerDEBUG = "DEBUG"
20 | loggerINFO = "INFO"
21 | loggerWarn = "WARN"
22 | loggerError = "ERR"
23 | )
24 |
25 | var setupLogOnce sync.Once
26 |
27 | func SetupLogger() {
28 | setupLogOnce.Do(func() {
29 | if LogDir != "" {
30 | logPath := filepath.Join(LogDir, fmt.Sprintf("unlimitedai2api-%s.log", time.Now().Format("20060102")))
31 | fd, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
32 | if err != nil {
33 | log.Fatal("failed to open log file")
34 | }
35 | gin.DefaultWriter = io.MultiWriter(os.Stdout, fd)
36 | gin.DefaultErrorWriter = io.MultiWriter(os.Stderr, fd)
37 | }
38 | })
39 | }
40 |
41 | func SysLog(s string) {
42 | t := time.Now()
43 | _, _ = fmt.Fprintf(gin.DefaultWriter, "[SYS] %v | %s \n", t.Format("2006/01/02 - 15:04:05"), s)
44 | }
45 |
46 | func SysError(s string) {
47 | t := time.Now()
48 | _, _ = fmt.Fprintf(gin.DefaultErrorWriter, "[SYS] %v | %s \n", t.Format("2006/01/02 - 15:04:05"), s)
49 | }
50 |
51 | func Debug(ctx context.Context, msg string) {
52 | if config.DebugEnabled {
53 | logHelper(ctx, loggerDEBUG, msg)
54 | }
55 | }
56 |
57 | func Info(ctx context.Context, msg string) {
58 | logHelper(ctx, loggerINFO, msg)
59 | }
60 |
61 | func Warn(ctx context.Context, msg string) {
62 | logHelper(ctx, loggerWarn, msg)
63 | }
64 |
65 | func Error(ctx context.Context, msg string) {
66 | logHelper(ctx, loggerError, msg)
67 | }
68 |
69 | func Debugf(ctx context.Context, format string, a ...any) {
70 | Debug(ctx, fmt.Sprintf(format, a...))
71 | }
72 |
73 | func Infof(ctx context.Context, format string, a ...any) {
74 | Info(ctx, fmt.Sprintf(format, a...))
75 | }
76 |
77 | func Warnf(ctx context.Context, format string, a ...any) {
78 | Warn(ctx, fmt.Sprintf(format, a...))
79 | }
80 |
81 | func Errorf(ctx context.Context, format string, a ...any) {
82 | Error(ctx, fmt.Sprintf(format, a...))
83 | }
84 |
85 | func logHelper(ctx context.Context, level string, msg string) {
86 | writer := gin.DefaultErrorWriter
87 | if level == loggerINFO {
88 | writer = gin.DefaultWriter
89 | }
90 | id := ctx.Value(helper.RequestIdKey)
91 | if id == nil {
92 | id = helper.GenRequestID()
93 | }
94 | now := time.Now()
95 | _, _ = fmt.Fprintf(writer, "[%s] %v | %s | %s \n", level, now.Format("2006/01/02 - 15:04:05"), id, msg)
96 | SetupLogger()
97 | }
98 |
99 | func FatalLog(v ...any) {
100 | t := time.Now()
101 | _, _ = fmt.Fprintf(gin.DefaultErrorWriter, "[FATAL] %v | %v \n", t.Format("2006/01/02 - 15:04:05"), v)
102 | os.Exit(1)
103 | }
104 |
--------------------------------------------------------------------------------
/common/random/main.go:
--------------------------------------------------------------------------------
1 | package random
2 |
3 | import (
4 | "github.com/google/uuid"
5 | "math/rand"
6 | "strings"
7 | "time"
8 | )
9 |
10 | func GetUUID() string {
11 | code := uuid.New().String()
12 | code = strings.Replace(code, "-", "", -1)
13 | return code
14 | }
15 |
16 | const keyChars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
17 | const keyNumbers = "0123456789"
18 |
19 | func init() {
20 | rand.Seed(time.Now().UnixNano())
21 | }
22 |
23 | func GenerateKey() string {
24 | rand.Seed(time.Now().UnixNano())
25 | key := make([]byte, 48)
26 | for i := 0; i < 16; i++ {
27 | key[i] = keyChars[rand.Intn(len(keyChars))]
28 | }
29 | uuid_ := GetUUID()
30 | for i := 0; i < 32; i++ {
31 | c := uuid_[i]
32 | if i%2 == 0 && c >= 'a' && c <= 'z' {
33 | c = c - 'a' + 'A'
34 | }
35 | key[i+16] = c
36 | }
37 | return string(key)
38 | }
39 |
40 | func GetRandomString(length int) string {
41 | rand.Seed(time.Now().UnixNano())
42 | key := make([]byte, length)
43 | for i := 0; i < length; i++ {
44 | key[i] = keyChars[rand.Intn(len(keyChars))]
45 | }
46 | return string(key)
47 | }
48 |
49 | func GetRandomNumberString(length int) string {
50 | rand.Seed(time.Now().UnixNano())
51 | key := make([]byte, length)
52 | for i := 0; i < length; i++ {
53 | key[i] = keyNumbers[rand.Intn(len(keyNumbers))]
54 | }
55 | return string(key)
56 | }
57 |
58 | // RandRange returns a random number between min and max (max is not included)
59 | func RandRange(min, max int) int {
60 | return min + rand.Intn(max-min)
61 | }
62 |
--------------------------------------------------------------------------------
/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/response.go:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | type ResponseResult struct {
4 | Code int `json:"code"`
5 | Message string `json:"message"`
6 | Data interface{} `json:"data,omitempty"`
7 | }
8 |
9 | func NewResponseResult(code int, message string, data interface{}) ResponseResult {
10 | return ResponseResult{
11 | Code: code,
12 | Message: message,
13 | Data: data,
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/common/send-res.go:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | )
6 |
7 | func SendResponse(c *gin.Context, httpCode int, code int, message string, data interface{}) {
8 | c.JSON(httpCode, NewResponseResult(code, message, data))
9 | }
10 |
--------------------------------------------------------------------------------
/common/snowflakeid.go:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | import (
4 | "fmt"
5 | "sync"
6 | "time"
7 | logger "unlimitedai2api/common/loggger"
8 |
9 | "github.com/sony/sonyflake"
10 | )
11 |
12 | // snowflakeGenerator 单例
13 | var (
14 | generator *SnowflakeGenerator
15 | once sync.Once
16 | )
17 |
18 | // SnowflakeGenerator 是雪花ID生成器的封装
19 | type SnowflakeGenerator struct {
20 | flake *sonyflake.Sonyflake
21 | }
22 |
23 | // NextID 生成一个新的雪花ID
24 | func NextID() (string, error) {
25 | once.Do(initGenerator)
26 | id, err := generator.flake.NextID()
27 | if err != nil {
28 | return "", err
29 | }
30 | return fmt.Sprintf("%d", id), nil
31 | }
32 |
33 | // initGenerator 初始化生成器,只调用一次
34 | func initGenerator() {
35 | st := sonyflake.Settings{
36 | StartTime: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC),
37 | }
38 | flake := sonyflake.NewSonyflake(st)
39 | if flake == nil {
40 | logger.FatalLog("sonyflake not created")
41 | }
42 | generator = &SnowflakeGenerator{
43 | flake: flake,
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/common/utils.go:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | import (
4 | "crypto/md5"
5 | "crypto/sha1"
6 | "crypto/sha256"
7 | "encoding/base64"
8 | "encoding/hex"
9 | "fmt"
10 | "github.com/google/uuid"
11 | jsoniter "github.com/json-iterator/go"
12 | _ "github.com/pkoukk/tiktoken-go"
13 | "math/rand"
14 | "regexp"
15 | "strings"
16 | "time"
17 | "unicode/utf8"
18 | )
19 |
20 | // splitStringByBytes 将字符串按照指定的字节数进行切割
21 | func SplitStringByBytes(s string, size int) []string {
22 | var result []string
23 |
24 | for len(s) > 0 {
25 | // 初始切割点
26 | l := size
27 | if l > len(s) {
28 | l = len(s)
29 | }
30 |
31 | // 确保不在字符中间切割
32 | for l > 0 && !utf8.ValidString(s[:l]) {
33 | l--
34 | }
35 |
36 | // 如果 l 减到 0,说明 size 太小,无法容纳一个完整的字符
37 | if l == 0 {
38 | l = len(s)
39 | for l > 0 && !utf8.ValidString(s[:l]) {
40 | l--
41 | }
42 | }
43 |
44 | result = append(result, s[:l])
45 | s = s[l:]
46 | }
47 |
48 | return result
49 | }
50 |
51 | func Obj2Bytes(obj interface{}) ([]byte, error) {
52 | // 创建一个jsonIter的Encoder
53 | configCompatibleWithStandardLibrary := jsoniter.ConfigCompatibleWithStandardLibrary
54 | // 将结构体转换为JSON文本并保持顺序
55 | bytes, err := configCompatibleWithStandardLibrary.Marshal(obj)
56 | if err != nil {
57 | return nil, err
58 | }
59 | return bytes, nil
60 | }
61 |
62 | func GetUUID() string {
63 | code := uuid.New().String()
64 | code = strings.Replace(code, "-", "", -1)
65 | return code
66 | }
67 |
68 | // RandomElement 返回给定切片中的随机元素
69 | func RandomElement[T any](slice []T) (T, error) {
70 | if len(slice) == 0 {
71 | var zero T
72 | return zero, fmt.Errorf("empty slice")
73 | }
74 |
75 | // 确保每次随机都不一样
76 | rand.Seed(time.Now().UnixNano())
77 |
78 | // 随机选择一个索引
79 | index := rand.Intn(len(slice))
80 | return slice[index], nil
81 | }
82 |
83 | func SliceContains(slice []string, str string) bool {
84 | for _, item := range slice {
85 | if strings.Contains(str, item) {
86 | return true
87 | }
88 | }
89 | return false
90 | }
91 |
92 | func IsImageBase64(s string) bool {
93 | // 检查字符串是否符合数据URL的格式
94 | if !strings.HasPrefix(s, "data:image/") || !strings.Contains(s, ";base64,") {
95 | return false
96 | }
97 |
98 | if !strings.Contains(s, ";base64,") {
99 | return false
100 | }
101 |
102 | // 获取";base64,"后的Base64编码部分
103 | dataParts := strings.Split(s, ";base64,")
104 | if len(dataParts) != 2 {
105 | return false
106 | }
107 | base64Data := dataParts[1]
108 |
109 | // 尝试Base64解码
110 | _, err := base64.StdEncoding.DecodeString(base64Data)
111 | return err == nil
112 | }
113 |
114 | func IsBase64(s string) bool {
115 | // 检查字符串是否符合数据URL的格式
116 | //if !strings.HasPrefix(s, "data:image/") || !strings.Contains(s, ";base64,") {
117 | // return false
118 | //}
119 |
120 | if !strings.Contains(s, ";base64,") {
121 | return false
122 | }
123 |
124 | // 获取";base64,"后的Base64编码部分
125 | dataParts := strings.Split(s, ";base64,")
126 | if len(dataParts) != 2 {
127 | return false
128 | }
129 | base64Data := dataParts[1]
130 |
131 | // 尝试Base64解码
132 | _, err := base64.StdEncoding.DecodeString(base64Data)
133 | return err == nil
134 | }
135 |
136 | //Sorry, you have been blocked
137 |
138 | func IsCloudflareBlock(data string) bool {
139 | if strings.Contains(data, `Sorry, you have been blocked
`) {
140 | return true
141 | }
142 |
143 | return false
144 | }
145 |
146 | func IsCloudflareChallenge(data string) bool {
147 | // 检查基本的 HTML 结构
148 | htmlPattern := `^.*?.*?