lengfeng1360 commited on
Commit
f3e8cce
·
verified ·
1 Parent(s): 085e6f1

Delete new-api

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. new-api/.dockerignore +0 -8
  2. new-api/.env.example +0 -73
  3. new-api/.gitignore +0 -14
  4. new-api/Dockerfile +0 -35
  5. new-api/LICENSE +0 -103
  6. new-api/README.en.md +0 -216
  7. new-api/README.fr.md +0 -216
  8. new-api/README.md +0 -219
  9. new-api/VERSION +0 -0
  10. new-api/bin/migration_v0.2-v0.3.sql +0 -6
  11. new-api/bin/migration_v0.3-v0.4.sql +0 -17
  12. new-api/bin/time_test.sh +0 -40
  13. new-api/common/api_type.go +0 -77
  14. new-api/common/constants.go +0 -202
  15. new-api/common/copy.go +0 -19
  16. new-api/common/crypto.go +0 -31
  17. new-api/common/custom-event.go +0 -87
  18. new-api/common/database.go +0 -15
  19. new-api/common/email-outlook-auth.go +0 -40
  20. new-api/common/email.go +0 -90
  21. new-api/common/embed-file-system.go +0 -32
  22. new-api/common/endpoint_defaults.go +0 -33
  23. new-api/common/endpoint_type.go +0 -41
  24. new-api/common/env.go +0 -38
  25. new-api/common/gin.go +0 -115
  26. new-api/common/go-channel.go +0 -53
  27. new-api/common/gopool.go +0 -24
  28. new-api/common/hash.go +0 -34
  29. new-api/common/init.go +0 -120
  30. new-api/common/ip.go +0 -22
  31. new-api/common/json.go +0 -44
  32. new-api/common/limiter/limiter.go +0 -89
  33. new-api/common/limiter/lua/rate_limit.lua +0 -44
  34. new-api/common/model.go +0 -42
  35. new-api/common/page_info.go +0 -82
  36. new-api/common/pprof.go +0 -44
  37. new-api/common/quota.go +0 -5
  38. new-api/common/rate-limit.go +0 -70
  39. new-api/common/redis.go +0 -327
  40. new-api/common/ssrf_protection.go +0 -327
  41. new-api/common/str.go +0 -237
  42. new-api/common/sys_log.go +0 -55
  43. new-api/common/topup-ratio.go +0 -33
  44. new-api/common/totp.go +0 -150
  45. new-api/common/utils.go +0 -384
  46. new-api/common/validate.go +0 -9
  47. new-api/common/verification.go +0 -77
  48. new-api/constant/README.md +0 -26
  49. new-api/constant/api_type.go +0 -37
  50. new-api/constant/azure.go +0 -5
new-api/.dockerignore DELETED
@@ -1,8 +0,0 @@
1
- .github
2
- .git
3
- *.md
4
- .vscode
5
- .gitignore
6
- Makefile
7
- docs
8
- .eslintcache
 
 
 
 
 
 
 
 
 
new-api/.env.example DELETED
@@ -1,73 +0,0 @@
1
- # 端口号
2
- # PORT=3000
3
- # 前端基础URL
4
- # FRONTEND_BASE_URL=https://your-frontend-url.com
5
-
6
-
7
- # 调试相关配置
8
- # 启用pprof
9
- # ENABLE_PPROF=true
10
- # 启用调试模式
11
- # DEBUG=true
12
-
13
- # 数据库相关配置
14
- # 数据库连接字符串
15
- # SQL_DSN=user:password@tcp(127.0.0.1:3306)/dbname?parseTime=true
16
- # 日志数据库连接字符串
17
- # LOG_SQL_DSN=user:password@tcp(127.0.0.1:3306)/logdb?parseTime=true
18
- # SQLite数据库路径
19
- # SQLITE_PATH=/path/to/sqlite.db
20
- # 数据库最大空闲连接数
21
- # SQL_MAX_IDLE_CONNS=100
22
- # 数据库最大打开连接数
23
- # SQL_MAX_OPEN_CONNS=1000
24
- # 数据库连接最大生命周期(秒)
25
- # SQL_MAX_LIFETIME=60
26
-
27
-
28
- # 缓存相关配置
29
- # Redis连接字符串
30
- # REDIS_CONN_STRING=redis://user:password@localhost:6379/0
31
- # 同步频率(单位:秒)
32
- # SYNC_FREQUENCY=60
33
- # 内存缓存启用
34
- # MEMORY_CACHE_ENABLED=true
35
- # 渠道更新频率(单位:秒)
36
- # CHANNEL_UPDATE_FREQUENCY=30
37
- # 批量更新启用
38
- # BATCH_UPDATE_ENABLED=true
39
- # 批量更新间隔(单位:秒)
40
- # BATCH_UPDATE_INTERVAL=5
41
-
42
- # 任务和功能配置
43
- # 更新任务启用
44
- # UPDATE_TASK=true
45
-
46
- # 对话超时设置
47
- # 所有请求超时时间,单位秒,默认为0,表示不限制
48
- # RELAY_TIMEOUT=0
49
- # 流模式无响应超时时间,单位秒,如果出现空补全可以尝试改为更大值
50
- # STREAMING_TIMEOUT=300
51
-
52
- # Gemini 识别图片 最大图片数量
53
- # GEMINI_VISION_MAX_IMAGE_NUM=16
54
-
55
- # 会话密钥
56
- # SESSION_SECRET=random_string
57
-
58
- # 其他配置
59
- # 生成默认token
60
- # GENERATE_DEFAULT_TOKEN=false
61
- # Cohere 安全设置
62
- # COHERE_SAFETY_SETTING=NONE
63
- # 是否统计图片token
64
- # GET_MEDIA_TOKEN=true
65
- # 是否在非流(stream=false)情况下统计图片token
66
- # GET_MEDIA_TOKEN_NOT_STREAM=true
67
- # 设置 Dify 渠道是否输出工作流和节点信息到客户端
68
- # DIFY_DEBUG=true
69
-
70
-
71
- # 节点类型
72
- # 如果是主节点则为master
73
- # NODE_TYPE=master
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
new-api/.gitignore DELETED
@@ -1,14 +0,0 @@
1
- .idea
2
- .vscode
3
- upload
4
- *.exe
5
- *.db
6
- build
7
- *.db-journal
8
- logs
9
- web/dist
10
- .env
11
- one-api
12
- .DS_Store
13
- tiktoken_cache
14
- .eslintcache
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
new-api/Dockerfile DELETED
@@ -1,35 +0,0 @@
1
- FROM oven/bun:latest AS builder
2
-
3
- WORKDIR /build
4
- COPY web/package.json .
5
- COPY web/bun.lock .
6
- RUN bun install
7
- COPY ./web .
8
- COPY ./VERSION .
9
- RUN DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(cat VERSION) bun run build
10
-
11
- FROM golang:alpine AS builder2
12
-
13
- ENV GO111MODULE=on \
14
- CGO_ENABLED=0 \
15
- GOOS=linux
16
-
17
- WORKDIR /build
18
-
19
- ADD go.mod go.sum ./
20
- RUN go mod download
21
-
22
- COPY . .
23
- COPY --from=builder /build/dist ./web/dist
24
- RUN go build -ldflags "-s -w -X 'one-api/common.Version=$(cat VERSION)'" -o one-api
25
-
26
- FROM alpine
27
-
28
- RUN apk upgrade --no-cache \
29
- && apk add --no-cache ca-certificates tzdata ffmpeg \
30
- && update-ca-certificates
31
-
32
- COPY --from=builder2 /build/one-api /
33
- EXPOSE 3000
34
- WORKDIR /data
35
- ENTRYPOINT ["/one-api"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
new-api/LICENSE DELETED
@@ -1,103 +0,0 @@
1
- # **New API 许可协议 (Licensing)**
2
-
3
- 本项目采用**基于使用场景的双重许可 (Usage-Based Dual Licensing)** 模式。
4
-
5
- **核心原则:**
6
-
7
- - **默认许可:** 本项目默认在 **GNU Affero 通用公共许可证 v3.0 (AGPLv3)** 下提供。任何用户在遵守 AGPLv3 条款和下述附加限制的前提下,均可免费使用。
8
- - **商业许可:** 在特定商业场景下,或当您希望获得 AGPLv3 之外的权利时,**必须**获取**商业许可证 (Commercial License)**。
9
-
10
- ---
11
-
12
- ## **1. 开源许可证 (Open Source License): AGPLv3 - 适用于基础使用**
13
-
14
- - 在遵守 **AGPLv3** 条款的前提下,您可以自由地使用、修改和分发 New API。AGPLv3 的完整文本可以访问 [https://www.gnu.org/licenses/agpl-3.0.html](https://www.gnu.org/licenses/agpl-3.0.html) 获取。
15
- - **核心义务:** AGPLv3 的一个关键要求是,如果您修改了 New API 并通过网络提供服务 (SaaS),或者分发了修改后的版本,您必须以 AGPLv3 许可证向所有用户提供相应的**完整源代码**。
16
- - **附加限制 (重要):** 在仅使用 AGPLv3 开源许可证的情况下,您**必须**完整保留项目代码中原有的品牌标识、LOGO 及版权声明信息。**禁止以任何形式修改、移除或遮盖**这些信息。如需移除,必须获取商业许可证。
17
- - 使用前请务必仔细阅读并理解 AGPLv3 的所有条款及上述附加限制。
18
-
19
- ## **2. 商业许可证 (Commercial License) - 适用于高级场景及闭源需求**
20
-
21
- 在以下任一情况下,您**必须**联系我们获取并签署一份商业许可证,才能合法使用 New API:
22
-
23
- - **场景一:移除品牌和版权信息**
24
- 您希望在您的产品或服务中移除 New API 的 LOGO、UI界面中的版权声明或其他品牌标识。
25
-
26
- - **场景二:规避 AGPLv3 开源义务**
27
- 您基于 New API 进行了修改,并希望:
28
- - 通过网络提供服务(SaaS),但**不希望**向您的服务用户公开您修改后的源代码。
29
- - 分发一个集成了 New API 的软件产品,但**不希望**以 AGPLv3 许可证发布您的产品或公开源代码。
30
-
31
- - **场景三:企业政策与集成需求**
32
- - 您所在公司的政策、客户合同或项目要求不允许使用 AGPLv3 许可的软件。
33
- - 您需要进行 OEM 集成,将 New API 作为您闭源商业产品的一部分进行再分发。
34
-
35
- - **场景四:需要商业支持与保障**
36
- 您需要 AGPLv3 未提供的商业保障,如官方技术支持等。
37
-
38
- **获取商业许可:**
39
- 请通过电子邮件 **support@quantumnous.com** 联系 New API 团队洽谈商业授权事宜。
40
-
41
- ## **3. 贡献 (Contributions)**
42
-
43
- - 我们欢迎社区对 New API 的贡献。所有向本项目提交的贡献(例如通过 Pull Request)都将被视为在 **AGPLv3** 许可证下提供。
44
- - 通过向本项目提交贡献,即表示您同意您的代码以 AGPLv3 许可证授权给本项目及所有后续使用者(无论这些使用者最终遵循 AGPLv3 还是商业许可)。
45
- - 您也理解并同意,您的贡献可能会被包含在根据商业许可证分发的 New API 版本中。
46
-
47
- ## **4. 其他条款 (Other Terms)**
48
-
49
- - 关于商业许可证的具体条款、条件和价格,以双方签署的正式商业许可协议为准。
50
- - 项目维护者保留根据需要更新本许可政策的权利。相关更新将通过项目官方渠道(如代码仓库、官方网站)进行通知。
51
-
52
- ---
53
-
54
- # **New API Licensing**
55
-
56
- This project uses a **Usage-Based Dual Licensing** model.
57
-
58
- **Core Principles:**
59
-
60
- - **Default License:** This project is available by default under the **GNU Affero General Public License v3.0 (AGPLv3)**. Any user may use it free of charge, provided they comply with both the AGPLv3 terms and the additional restrictions listed below.
61
- - **Commercial License:** For specific commercial scenarios, or if you require rights beyond those granted by AGPLv3, you **must** obtain a **Commercial License**.
62
-
63
- ---
64
-
65
- ## **1. Open Source License: AGPLv3 – For Basic Usage**
66
-
67
- - Under the terms of the **AGPLv3**, you are free to use, modify, and distribute New API. The complete AGPLv3 license text can be viewed at [https://www.gnu.org/licenses/agpl-3.0.html](https://www.gnu.org/licenses/agpl-3.0.html).
68
- - **Core Obligation:** A key AGPLv3 requirement is that if you modify New API and provide it as a network service (SaaS), or distribute a modified version, you must make the **complete corresponding source code** available to all users under the AGPLv3 license.
69
- - **Additional Restriction (Important):** When using only the AGPLv3 open-source license, you **must** retain all original branding, logos, and copyright statements within the project’s code. **You are strictly prohibited from modifying, removing, or concealing** any such information. If you wish to remove this, you must obtain a Commercial License.
70
- - Please read and ensure that you fully understand all AGPLv3 terms and the above additional restriction before use.
71
-
72
- ## **2. Commercial License – For Advanced Scenarios & Closed Source Needs**
73
-
74
- You **must** contact us to obtain and sign a Commercial License in any of the following scenarios in order to legally use New API:
75
-
76
- - **Scenario 1: Removal of Branding and Copyright**
77
- You wish to remove the New API logo, copyright statement, or other branding elements from your product or service.
78
-
79
- - **Scenario 2: Avoidance of AGPLv3 Open Source Obligations**
80
- You have modified New API and wish to:
81
- - Offer it as a network service (SaaS) **without** disclosing your modifications' source code to your users.
82
- - Distribute a software product integrated with New API **without** releasing your product under AGPLv3 or open-sourcing the code.
83
-
84
- - **Scenario 3: Enterprise Policy & Integration Needs**
85
- - Your organization’s policies, client contracts, or project requirements prohibit the use of AGPLv3-licensed software.
86
- - You require OEM integration and need to redistribute New API as part of your closed-source commercial product.
87
-
88
- - **Scenario 4: Commercial Support and Assurances**
89
- You require commercial assurances not provided by AGPLv3, such as official technical support.
90
-
91
- **Obtaining a Commercial License:**
92
- Please contact the New API team via email at **support@quantumnous.com** to discuss commercial licensing.
93
-
94
- ## **3. Contributions**
95
-
96
- - We welcome community contributions to New API. All contributions (e.g., via Pull Request) are deemed to be provided under the **AGPLv3** license.
97
- - By submitting a contribution, you agree that your code is licensed to this project and all downstream users under the AGPLv3 license (regardless of whether those users ultimately operate under AGPLv3 or a Commercial License).
98
- - You also acknowledge and agree that your contribution may be included in New API releases distributed under a Commercial License.
99
-
100
- ## **4. Other Terms**
101
-
102
- - The specific terms, conditions, and pricing of the Commercial License are governed by the formal commercial license agreement executed by both parties.
103
- - Project maintainers reserve the right to update this licensing policy as needed. Updates will be communicated via official project channels (e.g., repository, official website).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
new-api/README.en.md DELETED
@@ -1,216 +0,0 @@
1
- <p align="right">
2
- <a href="./README.md">中文</a> | <strong>English</strong> | <a href="./README.fr.md">Français</a>
3
- </p>
4
- <div align="center">
5
-
6
- ![new-api](/web/public/logo.png)
7
-
8
- # New API
9
-
10
- 🍥 Next-Generation Large Model Gateway and AI Asset Management System
11
-
12
- <a href="https://trendshift.io/repositories/8227" target="_blank"><img src="https://trendshift.io/api/badge/repositories/8227" alt="Calcium-Ion%2Fnew-api | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
13
-
14
- <p align="center">
15
- <a href="https://raw.githubusercontent.com/Calcium-Ion/new-api/main/LICENSE">
16
- <img src="https://img.shields.io/github/license/Calcium-Ion/new-api?color=brightgreen" alt="license">
17
- </a>
18
- <a href="https://github.com/Calcium-Ion/new-api/releases/latest">
19
- <img src="https://img.shields.io/github/v/release/Calcium-Ion/new-api?color=brightgreen&include_prereleases" alt="release">
20
- </a>
21
- <a href="https://github.com/users/Calcium-Ion/packages/container/package/new-api">
22
- <img src="https://img.shields.io/badge/docker-ghcr.io-blue" alt="docker">
23
- </a>
24
- <a href="https://hub.docker.com/r/CalciumIon/new-api">
25
- <img src="https://img.shields.io/badge/docker-dockerHub-blue" alt="docker">
26
- </a>
27
- <a href="https://goreportcard.com/report/github.com/Calcium-Ion/new-api">
28
- <img src="https://goreportcard.com/badge/github.com/Calcium-Ion/new-api" alt="GoReportCard">
29
- </a>
30
- </p>
31
- </div>
32
-
33
- ## 📝 Project Description
34
-
35
- > [!NOTE]
36
- > This is an open-source project developed based on [One API](https://github.com/songquanpeng/one-api)
37
-
38
- > [!IMPORTANT]
39
- > - This project is for personal learning purposes only, with no guarantee of stability or technical support.
40
- > - Users must comply with OpenAI's [Terms of Use](https://openai.com/policies/terms-of-use) and **applicable laws and regulations**, and must not use it for illegal purposes.
41
- > - According to the [《Interim Measures for the Management of Generative Artificial Intelligence Services》](http://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm), please do not provide any unregistered generative AI services to the public in China.
42
-
43
- <h2>🤝 Trusted Partners</h2>
44
- <p id="premium-sponsors">&nbsp;</p>
45
- <p align="center"><strong>No particular order</strong></p>
46
- <p align="center">
47
- <a href="https://www.cherry-ai.com/" target=_blank><img
48
- src="./docs/images/cherry-studio.png" alt="Cherry Studio" height="120"
49
- /></a>
50
- <a href="https://bda.pku.edu.cn/" target=_blank><img
51
- src="./docs/images/pku.png" alt="Peking University" height="120"
52
- /></a>
53
- <a href="https://www.compshare.cn/?ytag=GPU_yy_gh_newapi" target=_blank><img
54
- src="./docs/images/ucloud.png" alt="UCloud" height="120"
55
- /></a>
56
- <a href="https://www.aliyun.com/" target=_blank><img
57
- src="./docs/images/aliyun.png" alt="Alibaba Cloud" height="120"
58
- /></a>
59
- <a href="https://io.net/" target=_blank><img
60
- src="./docs/images/io-net.png" alt="IO.NET" height="120"
61
- /></a>
62
- </p>
63
- <p>&nbsp;</p>
64
-
65
- ## 📚 Documentation
66
-
67
- For detailed documentation, please visit our official Wiki: [https://docs.newapi.pro/](https://docs.newapi.pro/)
68
-
69
- You can also access the AI-generated DeepWiki:
70
- [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/QuantumNous/new-api)
71
-
72
- ## ✨ Key Features
73
-
74
- New API offers a wide range of features, please refer to [Features Introduction](https://docs.newapi.pro/wiki/features-introduction) for details:
75
-
76
- 1. 🎨 Brand new UI interface
77
- 2. 🌍 Multi-language support
78
- 3. 💰 Online recharge functionality (YiPay)
79
- 4. 🔍 Support for querying usage quotas with keys (works with [neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool))
80
- 5. 🔄 Compatible with the original One API database
81
- 6. 💵 Support for pay-per-use model pricing
82
- 7. ⚖️ Support for weighted random channel selection
83
- 8. 📈 Data dashboard (console)
84
- 9. 🔒 Token grouping and model restrictions
85
- 10. 🤖 Support for more authorization login methods (LinuxDO, Telegram, OIDC)
86
- 11. 🔄 Support for Rerank models (Cohere and Jina), [API Documentation](https://docs.newapi.pro/api/jinaai-rerank)
87
- 12. ⚡ Support for OpenAI Realtime API (including Azure channels), [API Documentation](https://docs.newapi.pro/api/openai-realtime)
88
- 13. ⚡ Support for Claude Messages format, [API Documentation](https://docs.newapi.pro/api/anthropic-chat)
89
- 14. Support for entering chat interface via /chat2link route
90
- 15. 🧠 Support for setting reasoning effort through model name suffixes:
91
- 1. OpenAI o-series models
92
- - Add `-high` suffix for high reasoning effort (e.g.: `o3-mini-high`)
93
- - Add `-medium` suffix for medium reasoning effort (e.g.: `o3-mini-medium`)
94
- - Add `-low` suffix for low reasoning effort (e.g.: `o3-mini-low`)
95
- 2. Claude thinking models
96
- - Add `-thinking` suffix to enable thinking mode (e.g.: `claude-3-7-sonnet-20250219-thinking`)
97
- 16. 🔄 Thinking-to-content functionality
98
- 17. 🔄 Model rate limiting for users
99
- 18. 💰 Cache billing support, which allows billing at a set ratio when cache is hit:
100
- 1. Set the `Prompt Cache Ratio` option in `System Settings-Operation Settings`
101
- 2. Set `Prompt Cache Ratio` in the channel, range 0-1, e.g., setting to 0.5 means billing at 50% when cache is hit
102
- 3. Supported channels:
103
- - [x] OpenAI
104
- - [x] Azure
105
- - [x] DeepSeek
106
- - [x] Claude
107
-
108
- ## Model Support
109
-
110
- This version supports multiple models, please refer to [API Documentation-Relay Interface](https://docs.newapi.pro/api) for details:
111
-
112
- 1. Third-party models **gpts** (gpt-4-gizmo-*)
113
- 2. Third-party channel [Midjourney-Proxy(Plus)](https://github.com/novicezk/midjourney-proxy) interface, [API Documentation](https://docs.newapi.pro/api/midjourney-proxy-image)
114
- 3. Third-party channel [Suno API](https://github.com/Suno-API/Suno-API) interface, [API Documentation](https://docs.newapi.pro/api/suno-music)
115
- 4. Custom channels, supporting full call address input
116
- 5. Rerank models ([Cohere](https://cohere.ai/) and [Jina](https://jina.ai/)), [API Documentation](https://docs.newapi.pro/api/jinaai-rerank)
117
- 6. Claude Messages format, [API Documentation](https://docs.newapi.pro/api/anthropic-chat)
118
- 7. Dify, currently only supports chatflow
119
-
120
- ## Environment Variable Configuration
121
-
122
- For detailed configuration instructions, please refer to [Installation Guide-Environment Variables Configuration](https://docs.newapi.pro/installation/environment-variables):
123
-
124
- - `GENERATE_DEFAULT_TOKEN`: Whether to generate initial tokens for newly registered users, default is `false`
125
- - `STREAMING_TIMEOUT`: Streaming response timeout, default is 300 seconds
126
- - `DIFY_DEBUG`: Whether to output workflow and node information for Dify channels, default is `true`
127
- - `FORCE_STREAM_OPTION`: Whether to override client stream_options parameter, default is `true`
128
- - `GET_MEDIA_TOKEN`: Whether to count image tokens, default is `true`
129
- - `GET_MEDIA_TOKEN_NOT_STREAM`: Whether to count image tokens in non-streaming cases, default is `true`
130
- - `UPDATE_TASK`: Whether to update asynchronous tasks (Midjourney, Suno), default is `true`
131
- - `COHERE_SAFETY_SETTING`: Cohere model safety settings, options are `NONE`, `CONTEXTUAL`, `STRICT`, default is `NONE`
132
- - `GEMINI_VISION_MAX_IMAGE_NUM`: Maximum number of images for Gemini models, default is `16`
133
- - `MAX_FILE_DOWNLOAD_MB`: Maximum file download size in MB, default is `20`
134
- - `CRYPTO_SECRET`: Encryption key used for encrypting database content
135
- - `AZURE_DEFAULT_API_VERSION`: Azure channel default API version, default is `2025-04-01-preview`
136
- - `NOTIFICATION_LIMIT_DURATION_MINUTE`: Notification limit duration, default is `10` minutes
137
- - `NOTIFY_LIMIT_COUNT`: Maximum number of user notifications within the specified duration, default is `2`
138
- - `ERROR_LOG_ENABLED=true`: Whether to record and display error logs, default is `false`
139
-
140
- ## Deployment
141
-
142
- For detailed deployment guides, please refer to [Installation Guide-Deployment Methods](https://docs.newapi.pro/installation):
143
-
144
- > [!TIP]
145
- > Latest Docker image: `calciumion/new-api:latest`
146
-
147
- ### Multi-machine Deployment Considerations
148
- - Environment variable `SESSION_SECRET` must be set, otherwise login status will be inconsistent across multiple machines
149
- - If sharing Redis, `CRYPTO_SECRET` must be set, otherwise Redis content cannot be accessed across multiple machines
150
-
151
- ### Deployment Requirements
152
- - Local database (default): SQLite (Docker deployment must mount the `/data` directory)
153
- - Remote database: MySQL version >= 5.7.8, PgSQL version >= 9.6
154
-
155
- ### Deployment Methods
156
-
157
- #### Using BaoTa Panel Docker Feature
158
- Install BaoTa Panel (version **9.2.0** or above), find **New-API** in the application store and install it.
159
- [Tutorial with images](./docs/BT.md)
160
-
161
- #### Using Docker Compose (Recommended)
162
- ```shell
163
- # Download the project
164
- git clone https://github.com/Calcium-Ion/new-api.git
165
- cd new-api
166
- # Edit docker-compose.yml as needed
167
- # Start
168
- docker-compose up -d
169
- ```
170
-
171
- #### Using Docker Image Directly
172
- ```shell
173
- # Using SQLite
174
- docker run --name new-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/new-api:/data calciumion/new-api:latest
175
-
176
- # Using MySQL
177
- docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:123456@tcp(localhost:3306)/oneapi" -e TZ=Asia/Shanghai -v /home/ubuntu/data/new-api:/data calciumion/new-api:latest
178
- ```
179
-
180
- ## Channel Retry and Cache
181
- Channel retry functionality has been implemented, you can set the number of retries in `Settings->Operation Settings->General Settings`. It is **recommended to enable caching**.
182
-
183
- ### Cache Configuration Method
184
- 1. `REDIS_CONN_STRING`: Set Redis as cache
185
- 2. `MEMORY_CACHE_ENABLED`: Enable memory cache (no need to set manually if Redis is set)
186
-
187
- ## API Documentation
188
-
189
- For detailed API documentation, please refer to [API Documentation](https://docs.newapi.pro/api):
190
-
191
- - [Chat API](https://docs.newapi.pro/api/openai-chat)
192
- - [Image API](https://docs.newapi.pro/api/openai-image)
193
- - [Rerank API](https://docs.newapi.pro/api/jinaai-rerank)
194
- - [Realtime API](https://docs.newapi.pro/api/openai-realtime)
195
- - [Claude Chat API (messages)](https://docs.newapi.pro/api/anthropic-chat)
196
-
197
- ## Related Projects
198
- - [One API](https://github.com/songquanpeng/one-api): Original project
199
- - [Midjourney-Proxy](https://github.com/novicezk/midjourney-proxy): Midjourney interface support
200
- - [chatnio](https://github.com/Deeptrain-Community/chatnio): Next-generation AI one-stop B/C-end solution
201
- - [neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool): Query usage quota with key
202
-
203
- Other projects based on New API:
204
- - [new-api-horizon](https://github.com/Calcium-Ion/new-api-horizon): High-performance optimized version of New API
205
- - [VoAPI](https://github.com/VoAPI/VoAPI): Frontend beautified version based on New API
206
-
207
- ## Help and Support
208
-
209
- If you have any questions, please refer to [Help and Support](https://docs.newapi.pro/support):
210
- - [Community Interaction](https://docs.newapi.pro/support/community-interaction)
211
- - [Issue Feedback](https://docs.newapi.pro/support/feedback-issues)
212
- - [FAQ](https://docs.newapi.pro/support/faq)
213
-
214
- ## 🌟 Star History
215
-
216
- [![Star History Chart](https://api.star-history.com/svg?repos=Calcium-Ion/new-api&type=Date)](https://star-history.com/#Calcium-Ion/new-api&Date)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
new-api/README.fr.md DELETED
@@ -1,216 +0,0 @@
1
- <p align="right">
2
- <a href="./README.md">中文</a> | <a href="./README.en.md">English</a> | <strong>Français</strong>
3
- </p>
4
- <div align="center">
5
-
6
- ![new-api](/web/public/logo.png)
7
-
8
- # New API
9
-
10
- 🍥 Passerelle de modèles étendus de nouvelle génération et système de gestion d'actifs d'IA
11
-
12
- <a href="https://trendshift.io/repositories/8227" target="_blank"><img src="https://trendshift.io/api/badge/repositories/8227" alt="Calcium-Ion%2Fnew-api | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
13
-
14
- <p align="center">
15
- <a href="https://raw.githubusercontent.com/Calcium-Ion/new-api/main/LICENSE">
16
- <img src="https://img.shields.io/github/license/Calcium-Ion/new-api?color=brightgreen" alt="licence">
17
- </a>
18
- <a href="https://github.com/Calcium-Ion/new-api/releases/latest">
19
- <img src="https://img.shields.io/github/v/release/Calcium-Ion/new-api?color=brightgreen&include_prereleases" alt="version">
20
- </a>
21
- <a href="https://github.com/users/Calcium-Ion/packages/container/package/new-api">
22
- <img src="https://img.shields.io/badge/docker-ghcr.io-blue" alt="docker">
23
- </a>
24
- <a href="https://hub.docker.com/r/CalciumIon/new-api">
25
- <img src="https://img.shields.io/badge/docker-dockerHub-blue" alt="docker">
26
- </a>
27
- <a href="https://goreportcard.com/report/github.com/Calcium-Ion/new-api">
28
- <img src="https://goreportcard.com/badge/github.com/Calcium-Ion/new-api" alt="GoReportCard">
29
- </a>
30
- </p>
31
- </div>
32
-
33
- ## 📝 Description du projet
34
-
35
- > [!NOTE]
36
- > Il s'agit d'un projet open-source développé sur la base de [One API](https://github.com/songquanpeng/one-api)
37
-
38
- > [!IMPORTANT]
39
- > - Ce projet est uniquement destiné à des fins d'apprentissage personnel, sans garantie de stabilité ni de support technique.
40
- > - Les utilisateurs doivent se conformer aux [Conditions d'utilisation](https://openai.com/policies/terms-of-use) d'OpenAI et aux **lois et réglementations applicables**, et ne doivent pas l'utiliser à des fins illégales.
41
- > - Conformément aux [《Mesures provisoires pour la gestion des services d'intelligence artificielle générative》](http://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm), veuillez ne fournir aucun service d'IA générative non enregistré au public en Chine.
42
-
43
- <h2>🤝 Partenaires de confiance</h2>
44
- <p id="premium-sponsors">&nbsp;</p>
45
- <p align="center"><strong>Sans ordre particulier</strong></p>
46
- <p align="center">
47
- <a href="https://www.cherry-ai.com/" target=_blank><img
48
- src="./docs/images/cherry-studio.png" alt="Cherry Studio" height="120"
49
- /></a>
50
- <a href="https://bda.pku.edu.cn/" target=_blank><img
51
- src="./docs/images/pku.png" alt="Université de Pékin" height="120"
52
- /></a>
53
- <a href="https://www.compshare.cn/?ytag=GPU_yy_gh_newapi" target=_blank><img
54
- src="./docs/images/ucloud.png" alt="UCloud" height="120"
55
- /></a>
56
- <a href="https://www.aliyun.com/" target=_blank><img
57
- src="./docs/images/aliyun.png" alt="Alibaba Cloud" height="120"
58
- /></a>
59
- <a href="https://io.net/" target=_blank><img
60
- src="./docs/images/io-net.png" alt="IO.NET" height="120"
61
- /></a>
62
- </p>
63
- <p>&nbsp;</p>
64
-
65
- ## 📚 Documentation
66
-
67
- Pour une documentation détaillée, veuillez consulter notre Wiki officiel : [https://docs.newapi.pro/](https://docs.newapi.pro/)
68
-
69
- Vous pouvez également accéder au DeepWiki généré par l'IA :
70
- [![Demander à DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/QuantumNous/new-api)
71
-
72
- ## ✨ Fonctionnalités clés
73
-
74
- New API offre un large éventail de fonctionnalités, veuillez vous référer à [Présentation des fonctionnalités](https://docs.newapi.pro/wiki/features-introduction) pour plus de détails :
75
-
76
- 1. 🎨 Nouvelle interface utilisateur
77
- 2. 🌍 Prise en charge multilingue
78
- 3. 💰 Fonctionnalité de recharge en ligne (YiPay)
79
- 4. 🔍 Prise en charge de la recherche de quotas d'utilisation avec des clés (fonctionne avec [neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool))
80
- 5. 🔄 Compatible avec la base de données originale de One API
81
- 6. 💵 Prise en charge de la tarification des modèles de paiement à l'utilisation
82
- 7. ⚖️ Prise en charge de la sélection aléatoire pondérée des canaux
83
- 8. 📈 Tableau de bord des données (console)
84
- 9. 🔒 Regroupement de jetons et restrictions de modèles
85
- 10. 🤖 Prise en charge de plus de méthodes de connexion par autorisation (LinuxDO, Telegram, OIDC)
86
- 11. 🔄 Prise en charge des modèles Rerank (Cohere et Jina), [Documentation de l'API](https://docs.newapi.pro/api/jinaai-rerank)
87
- 12. ⚡ Prise en charge de l'API OpenAI Realtime (y compris les canaux Azure), [Documentation de l'API](https://docs.newapi.pro/api/openai-realtime)
88
- 13. ⚡ Prise en charge du format Claude Messages, [Documentation de l'API](https://docs.newapi.pro/api/anthropic-chat)
89
- 14. Prise en charge de l'accès à l'interface de discussion via la route /chat2link
90
- 15. 🧠 Prise en charge de la définition de l'effort de raisonnement via les suffixes de nom de modèle :
91
- 1. Modèles de la série o d'OpenAI
92
- - Ajouter le suffixe `-high` pour un effort de raisonnement élevé (par exemple : `o3-mini-high`)
93
- - Ajouter le suffixe `-medium` pour un effort de raisonnement moyen (par exemple : `o3-mini-medium`)
94
- - Ajouter le suffixe `-low` pour un effort de raisonnement faible (par exemple : `o3-mini-low`)
95
- 2. Modèles de pensée de Claude
96
- - Ajouter le suffixe `-thinking` pour activer le mode de pensée (par exemple : `claude-3-7-sonnet-20250219-thinking`)
97
- 16. 🔄 Fonctionnalité de la pensée au contenu
98
- 17. 🔄 Limitation du débit du modèle pour les utilisateurs
99
- 18. 💰 Prise en charge de la facturation du cache, qui permet de facturer à un ratio défini lorsque le cache est atteint :
100
- 1. Définir l'option `Ratio de cache d'invite` dans `Paramètres système->Paramètres de fonctionnement`
101
- 2. Définir le `Ratio de cache d'invite` dans le canal, plage de 0 à 1, par exemple, le définir sur 0,5 signifie facturer à 50 % lorsque le cache est atteint
102
- 3. Canaux pris en charge :
103
- - [x] OpenAI
104
- - [x] Azure
105
- - [x] DeepSeek
106
- - [x] Claude
107
-
108
- ## Prise en charge des modèles
109
-
110
- Cette version prend en charge plusieurs modèles, veuillez vous référer à [Documentation de l'API-Interface de relais](https://docs.newapi.pro/api) pour plus de détails :
111
-
112
- 1. Modèles tiers **gpts** (gpt-4-gizmo-*)
113
- 2. Canal tiers [Midjourney-Proxy(Plus)](https://github.com/novicezk/midjourney-proxy), [Documentation de l'API](https://docs.newapi.pro/api/midjourney-proxy-image)
114
- 3. Canal tiers [Suno API](https://github.com/Suno-API/Suno-API), [Documentation de l'API](https://docs.newapi.pro/api/suno-music)
115
- 4. Canaux personnalisés, prenant en charge la saisie complète de l'adresse d'appel
116
- 5. Modèles Rerank ([Cohere](https://cohere.ai/) et [Jina](https://jina.ai/)), [Documentation de l'API](https://docs.newapi.pro/api/jinaai-rerank)
117
- 6. Format de messages Claude, [Documentation de l'API](https://docs.newapi.pro/api/anthropic-chat)
118
- 7. Dify, ne prend actuellement en charge que chatflow
119
-
120
- ## Configuration des variables d'environnement
121
-
122
- Pour des instructions de configuration détaillées, veuillez vous référer à [Guide d'installation-Configuration des variables d'environnement](https://docs.newapi.pro/installation/environment-variables) :
123
-
124
- - `GENERATE_DEFAULT_TOKEN` : S'il faut générer des jetons initiaux pour les utilisateurs nouvellement enregistrés, la valeur par défaut est `false`
125
- - `STREAMING_TIMEOUT` : Délai d'expiration de la réponse en streaming, la valeur par défaut est de 300 secondes
126
- - `DIFY_DEBUG` : S'il faut afficher les informations sur le flux de travail et les nœuds pour les canaux Dify, la valeur par défaut est `true`
127
- - `FORCE_STREAM_OPTION` : S'il faut remplacer le paramètre client stream_options, la valeur par défaut est `true`
128
- - `GET_MEDIA_TOKEN` : S'il faut compter les jetons d'image, la valeur par défaut est `true`
129
- - `GET_MEDIA_TOKEN_NOT_STREAM` : S'il faut compter les jetons d'image dans les cas sans streaming, la valeur par défaut est `true`
130
- - `UPDATE_TASK` : S'il faut mettre à jour les tâches asynchrones (Midjourney, Suno), la valeur par défaut est `true`
131
- - `COHERE_SAFETY_SETTING` : Paramètres de sécurité du modèle Cohere, les options sont `NONE`, `CONTEXTUAL`, `STRICT`, la valeur par défaut est `NONE`
132
- - `GEMINI_VISION_MAX_IMAGE_NUM` : Nombre maximum d'images pour les modèles Gemini, la valeur par défaut est `16`
133
- - `MAX_FILE_DOWNLOAD_MB` : Taille maximale de téléchargement de fichier en Mo, la valeur par défaut est `20`
134
- - `CRYPTO_SECRET` : Clé de chiffrement utilisée pour chiffrer le contenu de la base de données
135
- - `AZURE_DEFAULT_API_VERSION` : Version de l'API par défaut du canal Azure, la valeur par défaut est `2025-04-01-preview`
136
- - `NOTIFICATION_LIMIT_DURATION_MINUTE` : Durée de la limite de notification, la valeur par défaut est de `10` minutes
137
- - `NOTIFY_LIMIT_COUNT` : Nombre maximal de notifications utilisateur dans la durée spécifiée, la valeur par défaut est `2`
138
- - `ERROR_LOG_ENABLED=true` : S'il faut enregistrer et afficher les journaux d'erreurs, la valeur par défaut est `false`
139
-
140
- ## Déploiement
141
-
142
- Pour des guides de déploiement détaillés, veuillez vous référer à [Guide d'installation-Méthodes de déploiement](https://docs.newapi.pro/installation) :
143
-
144
- > [!TIP]
145
- > Dernière image Docker : `calciumion/new-api:latest`
146
-
147
- ### Considérations sur le déploiement multi-machines
148
- - La variable d'environnement `SESSION_SECRET` doit être définie, sinon l'état de connexion sera incohérent sur plusieurs machines
149
- - Si vous partagez Redis, `CRYPTO_SECRET` doit être défini, sinon le contenu de Redis ne pourra pas être consulté sur plusieurs machines
150
-
151
- ### Exigences de déploiement
152
- - Base de données locale (par défaut) : SQLite (le déploiement Docker doit monter le répertoire `/data`)
153
- - Base de données distante : MySQL version >= 5.7.8, PgSQL version >= 9.6
154
-
155
- ### Méthodes de déploiement
156
-
157
- #### Utilisation de la fonctionnalité Docker du panneau BaoTa
158
- Installez le panneau BaoTa (version **9.2.0** ou supérieure), recherchez **New-API** dans le magasin d'applications et installez-le.
159
- [Tutoriel avec des images](./docs/BT.md)
160
-
161
- #### Utilisation de Docker Compose (recommandé)
162
- ```shell
163
- # Télécharger le projet
164
- git clone https://github.com/Calcium-Ion/new-api.git
165
- cd new-api
166
- # Modifier docker-compose.yml si nécessaire
167
- # Démarrer
168
- docker-compose up -d
169
- ```
170
-
171
- #### Utilisation directe de l'image Docker
172
- ```shell
173
- # Utilisation de SQLite
174
- docker run --name new-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/new-api:/data calciumion/new-api:latest
175
-
176
- # Utilisation de MySQL
177
- docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:123456@tcp(localhost:3306)/oneapi" -e TZ=Asia/Shanghai -v /home/ubuntu/data/new-api:/data calciumion/new-api:latest
178
- ```
179
-
180
- ## Nouvelle tentative de canal et cache
181
- La fonctionnalité de nouvelle tentative de canal a été implémentée, vous pouvez définir le nombre de tentatives dans `Paramètres->Paramètres de fonctionnement->Paramètres généraux`. Il est **recommandé d'activer la mise en cache**.
182
-
183
- ### Méthode de configuration du cache
184
- 1. `REDIS_CONN_STRING` : Définir Redis comme cache
185
- 2. `MEMORY_CACHE_ENABLED` : Activer le cache mémoire (pas besoin de le définir manuellement si Redis est défini)
186
-
187
- ## Documentation de l'API
188
-
189
- Pour une documentation détaillée de l'API, veuillez vous référer à [Documentation de l'API](https://docs.newapi.pro/api) :
190
-
191
- - [API de discussion](https://docs.newapi.pro/api/openai-chat)
192
- - [API d'image](https://docs.newapi.pro/api/openai-image)
193
- - [API de rerank](https://docs.newapi.pro/api/jinaai-rerank)
194
- - [API en temps réel](https://docs.newapi.pro/api/openai-realtime)
195
- - [API de discussion Claude (messages)](https://docs.newapi.pro/api/anthropic-chat)
196
-
197
- ## Projets connexes
198
- - [One API](https://github.com/songquanpeng/one-api) : Projet original
199
- - [Midjourney-Proxy](https://github.com/novicezk/midjourney-proxy) : Prise en charge de l'interface Midjourney
200
- - [chatnio](https://github.com/Deeptrain-Community/chatnio) : Solution B/C unique d'IA de nouvelle génération
201
- - [neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool) : Interroger le quota d'utilisation avec une clé
202
-
203
- Autres projets basés sur New API :
204
- - [new-api-horizon](https://github.com/Calcium-Ion/new-api-horizon) : Version optimisée hautes performances de New API
205
- - [VoAPI](https://github.com/VoAPI/VoAPI) : Version embellie du frontend basée sur New API
206
-
207
- ## Aide et support
208
-
209
- Si vous avez des questions, veuillez vous référer à [Aide et support](https://docs.newapi.pro/support) :
210
- - [Interaction avec la communauté](https://docs.newapi.pro/support/community-interaction)
211
- - [Commentaires sur les problèmes](https://docs.newapi.pro/support/feedback-issues)
212
- - [FAQ](https://docs.newapi.pro/support/faq)
213
-
214
- ## 🌟 Historique des étoiles
215
-
216
- [![Graphique de l'historique des étoiles](https://api.star-history.com/svg?repos=Calcium-Ion/new-api&type=Date)](https://star-history.com/#Calcium-Ion/new-api&Date)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
new-api/README.md DELETED
@@ -1,219 +0,0 @@
1
- <p align="right">
2
- <strong>中文</strong> | <a href="./README.en.md">English</a> | <a href="./README.fr.md">Français</a>
3
- </p>
4
- <div align="center">
5
-
6
- ![new-api](/web/public/logo.png)
7
-
8
- # New API
9
-
10
- 🍥新一代大模型网关与AI资产管理系统
11
-
12
- <a href="https://trendshift.io/repositories/8227" target="_blank"><img src="https://trendshift.io/api/badge/repositories/8227" alt="Calcium-Ion%2Fnew-api | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
13
-
14
- <p align="center">
15
- <a href="https://raw.githubusercontent.com/Calcium-Ion/new-api/main/LICENSE">
16
- <img src="https://img.shields.io/github/license/Calcium-Ion/new-api?color=brightgreen" alt="license">
17
- </a>
18
- <a href="https://github.com/Calcium-Ion/new-api/releases/latest">
19
- <img src="https://img.shields.io/github/v/release/Calcium-Ion/new-api?color=brightgreen&include_prereleases" alt="release">
20
- </a>
21
- <a href="https://github.com/users/Calcium-Ion/packages/container/package/new-api">
22
- <img src="https://img.shields.io/badge/docker-ghcr.io-blue" alt="docker">
23
- </a>
24
- <a href="https://hub.docker.com/r/CalciumIon/new-api">
25
- <img src="https://img.shields.io/badge/docker-dockerHub-blue" alt="docker">
26
- </a>
27
- <a href="https://goreportcard.com/report/github.com/Calcium-Ion/new-api">
28
- <img src="https://goreportcard.com/badge/github.com/Calcium-Ion/new-api" alt="GoReportCard">
29
- </a>
30
- </p>
31
- </div>
32
-
33
- ## 📝 项目说明
34
-
35
- > [!NOTE]
36
- > 本项目为开源项目,在[One API](https://github.com/songquanpeng/one-api)的基础上进行二次开发
37
-
38
- > [!IMPORTANT]
39
- > - 本项目仅供个人学习使用,不保证稳定性,且不提供任何技术支持。
40
- > - 使用者必须在遵循 OpenAI 的[使用条款](https://openai.com/policies/terms-of-use)以及**法律法规**的情况下使用,不得用于非法用途。
41
- > - 根据[《生成式人工智能服务管理暂行办法》](http://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm)的要求,请勿对中国地区公众提供一切未经备案的生成式人工智能服务。
42
-
43
- <h2>🤝 我们信任的合作伙伴</h2>
44
- <p id="premium-sponsors">&nbsp;</p>
45
- <p align="center"><strong>排名不分先后</strong></p>
46
- <p align="center">
47
- <a href="https://www.cherry-ai.com/" target=_blank><img
48
- src="./docs/images/cherry-studio.png" alt="Cherry Studio" height="120"
49
- /></a>
50
- <a href="https://bda.pku.edu.cn/" target=_blank><img
51
- src="./docs/images/pku.png" alt="北京大学" height="120"
52
- /></a>
53
- <a href="https://www.compshare.cn/?ytag=GPU_yy_gh_newapi" target=_blank><img
54
- src="./docs/images/ucloud.png" alt="UCloud 优刻得" height="120"
55
- /></a>
56
- <a href="https://www.aliyun.com/" target=_blank><img
57
- src="./docs/images/aliyun.png" alt="阿里云" height="120"
58
- /></a>
59
- <a href="https://io.net/" target=_blank><img
60
- src="./docs/images/io-net.png" alt="IO.NET" height="120"
61
- /></a>
62
- </p>
63
- <p>&nbsp;</p>
64
-
65
- ## 📚 文档
66
-
67
- 详细文档请访问我们的官方Wiki:[https://docs.newapi.pro/](https://docs.newapi.pro/)
68
-
69
- 也可访问AI生成的DeepWiki:
70
- [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/QuantumNous/new-api)
71
-
72
- ## ✨ 主要特性
73
-
74
- New API提供了丰富的功能,详细特性请参考[特性说明](https://docs.newapi.pro/wiki/features-introduction):
75
-
76
- 1. 🎨 全新的UI界面
77
- 2. 🌍 多语言支持
78
- 3. 💰 支持在线充值功能(易支付)
79
- 4. 🔍 支持用key查询使用额度(配合[neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool))
80
- 5. 🔄 兼容原版One API的数据库
81
- 6. 💵 支持模型按次数收费
82
- 7. ⚖️ 支持渠道加权随机
83
- 8. 📈 数据看板(控制台)
84
- 9. 🔒 令牌分组、模型限制
85
- 10. 🤖 支持更多授权登陆方式(LinuxDO,Telegram、OIDC)
86
- 11. 🔄 支持Rerank模型(Cohere和Jina),[接口文档](https://docs.newapi.pro/api/jinaai-rerank)
87
- 12. ⚡ 支持OpenAI Realtime API(包括Azure渠道),[接口文档](https://docs.newapi.pro/api/openai-realtime)
88
- 13. ⚡ 支持Claude Messages 格式,[接口文档](https://docs.newapi.pro/api/anthropic-chat)
89
- 14. 支持使用路由/chat2link进入聊天界面
90
- 15. 🧠 支持通过模型名称后缀设置 reasoning effort:
91
- 1. OpenAI o系列模型
92
- - 添加后缀 `-high` 设置为 high reasoning effort (例如: `o3-mini-high`)
93
- - 添加后缀 `-medium` 设置为 medium reasoning effort (例如: `o3-mini-medium`)
94
- - 添加后缀 `-low` 设置为 low reasoning effort (例如: `o3-mini-low`)
95
- 2. Claude 思考模型
96
- - 添加后缀 `-thinking` 启用思考模式 (例如: `claude-3-7-sonnet-20250219-thinking`)
97
- 16. 🔄 思考转内容功能
98
- 17. 🔄 针对用户的模型限流功能
99
- 18. 🔄 请求格式转换功能,支持以下三种格式转换:
100
- 1. OpenAI Chat Completions => Claude Messages
101
- 2. Clade Messages => OpenAI Chat Completions (可用于Claude Code调用第三方���型)
102
- 3. OpenAI Chat Completions => Gemini Chat
103
- 19. 💰 缓存计费支持,开启后可以在缓存命中时按照设定的比例计费:
104
- 1. 在 `系统设置-运营设置` 中设置 `提示缓存倍率` 选项
105
- 2. 在渠道中设置 `提示缓存倍率`,范围 0-1,例如设置为 0.5 表示缓存命中时按照 50% 计费
106
- 3. 支持的渠道:
107
- - [x] OpenAI
108
- - [x] Azure
109
- - [x] DeepSeek
110
- - [x] Claude
111
-
112
- ## 模型支持
113
-
114
- 此版本支持多种模型,详情请参考[接口文档-中继接口](https://docs.newapi.pro/api):
115
-
116
- 1. 第三方模型 **gpts** (gpt-4-gizmo-*)
117
- 2. 第三方渠道[Midjourney-Proxy(Plus)](https://github.com/novicezk/midjourney-proxy)接口,[接口文档](https://docs.newapi.pro/api/midjourney-proxy-image)
118
- 3. 第三方渠道[Suno API](https://github.com/Suno-API/Suno-API)接口,[接口文档](https://docs.newapi.pro/api/suno-music)
119
- 4. 自定义渠道,支持填入完整调用地址
120
- 5. Rerank模型([Cohere](https://cohere.ai/)和[Jina](https://jina.ai/)),[接口文档](https://docs.newapi.pro/api/jinaai-rerank)
121
- 6. Claude Messages 格式,[接口文档](https://docs.newapi.pro/api/anthropic-chat)
122
- 7. Dify,当前仅支持chatflow
123
-
124
- ## 环境变量配置
125
-
126
- 详细配置说明请参考[安装指南-环境变量配置](https://docs.newapi.pro/installation/environment-variables):
127
-
128
- - `GENERATE_DEFAULT_TOKEN`:是否为新注册用户生成初始令牌,默认为 `false`
129
- - `STREAMING_TIMEOUT`:流式回复超时时间,默认300秒
130
- - `DIFY_DEBUG`:Dify渠道是否输出工作流和节点信息,默认 `true`
131
- - `FORCE_STREAM_OPTION`:是否覆盖客户端stream_options参数,默认 `true`
132
- - `GET_MEDIA_TOKEN`:是否统计图片token,默认 `true`
133
- - `GET_MEDIA_TOKEN_NOT_STREAM`:非流情况下是否统计图片token,默认 `true`
134
- - `UPDATE_TASK`:是否更新异步任务(Midjourney、Suno),默认 `true`
135
- - `COHERE_SAFETY_SETTING`:Cohere模型安全设置,可选值为 `NONE`, `CONTEXTUAL`, `STRICT`,默认 `NONE`
136
- - `GEMINI_VISION_MAX_IMAGE_NUM`:Gemini模型最大图片数量,默认 `16`
137
- - `MAX_FILE_DOWNLOAD_MB`: 最大文件下载大小,单位MB,默认 `20`
138
- - `CRYPTO_SECRET`:加密密钥,用于加密数据库内容
139
- - `AZURE_DEFAULT_API_VERSION`:Azure渠道默认API版本,默认 `2025-04-01-preview`
140
- - `NOTIFICATION_LIMIT_DURATION_MINUTE`:通知限制持续时间,默认 `10`分钟
141
- - `NOTIFY_LIMIT_COUNT`:用户通知在指定持续时间内的最大数量,默认 `2`
142
- - `ERROR_LOG_ENABLED=true`: 是否记录并显示错误日志,默认`false`
143
-
144
- ## 部署
145
-
146
- 详细部署指南请参考[安装指南-部署方式](https://docs.newapi.pro/installation):
147
-
148
- > [!TIP]
149
- > 最新版Docker镜像:`calciumion/new-api:latest`
150
-
151
- ### 多机部署注意事项
152
- - 必须设置环境变量 `SESSION_SECRET`,否则会导致多机部署时登录状态不一致
153
- - 如果公用Redis,必须设置 `CRYPTO_SECRET`,否则会导致多机部署时Redis内容无法获取
154
-
155
- ### 部署要求
156
- - 本地数据库(默认):SQLite(Docker部署必须挂载`/data`目录)
157
- - 远程数据库:MySQL版本 >= 5.7.8,PgSQL版本 >= 9.6
158
-
159
- ### 部署方式
160
-
161
- #### 使用宝塔面板Docker功能部署
162
- 安装宝塔面板(**9.2.0版本**及以上),在应用商店中找到**New-API**安装即可。
163
- [图文教程](./docs/BT.md)
164
-
165
- #### 使用Docker Compose部署(推荐)
166
- ```shell
167
- # 下载项目
168
- git clone https://github.com/Calcium-Ion/new-api.git
169
- cd new-api
170
- # 按需编辑docker-compose.yml
171
- # 启动
172
- docker-compose up -d
173
- ```
174
-
175
- #### 直接使用Docker镜像
176
- ```shell
177
- # 使用SQLite
178
- docker run --name new-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/new-api:/data calciumion/new-api:latest
179
-
180
- # 使用MySQL
181
- docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:123456@tcp(localhost:3306)/oneapi" -e TZ=Asia/Shanghai -v /home/ubuntu/data/new-api:/data calciumion/new-api:latest
182
- ```
183
-
184
- ## 渠道重试与缓存
185
- 渠道重试功能已经实现,可以在`设置->运营设置->通用设置`设置重试次数,**建议开启缓存**功能。
186
-
187
- ### 缓存设置方法
188
- 1. `REDIS_CONN_STRING`:设置Redis作为缓存
189
- 2. `MEMORY_CACHE_ENABLED`:启用内存缓存(设置了Redis则无需手动设置)
190
-
191
- ## 接口文档
192
-
193
- 详细接口文档请参考[接口文档](https://docs.newapi.pro/api):
194
-
195
- - [聊天接口(Chat)](https://docs.newapi.pro/api/openai-chat)
196
- - [图像接口(Image)](https://docs.newapi.pro/api/openai-image)
197
- - [重排序接口(Rerank)](https://docs.newapi.pro/api/jinaai-rerank)
198
- - [实时对话接口(Realtime)](https://docs.newapi.pro/api/openai-realtime)
199
- - [Claude聊天接口(messages)](https://docs.newapi.pro/api/anthropic-chat)
200
-
201
- ## 相关项目
202
- - [One API](https://github.com/songquanpeng/one-api):原版项目
203
- - [Midjourney-Proxy](https://github.com/novicezk/midjourney-proxy):Midjourney接口支持
204
- - [chatnio](https://github.com/Deeptrain-Community/chatnio):下一代AI一站式B/C端解决方案
205
- - [neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool):用key查询使用额度
206
-
207
- 其他基于New API的项目:
208
- - [new-api-horizon](https://github.com/Calcium-Ion/new-api-horizon):New API高性能优化版
209
-
210
- ## 帮助支持
211
-
212
- 如有问题,请参考[帮助支持](https://docs.newapi.pro/support):
213
- - [社区交流](https://docs.newapi.pro/support/community-interaction)
214
- - [反馈问题](https://docs.newapi.pro/support/feedback-issues)
215
- - [常见问题](https://docs.newapi.pro/support/faq)
216
-
217
- ## 🌟 Star History
218
-
219
- [![Star History Chart](https://api.star-history.com/svg?repos=Calcium-Ion/new-api&type=Date)](https://star-history.com/#Calcium-Ion/new-api&Date)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
new-api/VERSION DELETED
File without changes
new-api/bin/migration_v0.2-v0.3.sql DELETED
@@ -1,6 +0,0 @@
1
- UPDATE users
2
- SET quota = quota + (
3
- SELECT SUM(remain_quota)
4
- FROM tokens
5
- WHERE tokens.user_id = users.id
6
- )
 
 
 
 
 
 
 
new-api/bin/migration_v0.3-v0.4.sql DELETED
@@ -1,17 +0,0 @@
1
- INSERT INTO abilities (`group`, model, channel_id, enabled)
2
- SELECT c.`group`, m.model, c.id, 1
3
- FROM channels c
4
- CROSS JOIN (
5
- SELECT 'gpt-3.5-turbo' AS model UNION ALL
6
- SELECT 'gpt-3.5-turbo-0301' AS model UNION ALL
7
- SELECT 'gpt-4' AS model UNION ALL
8
- SELECT 'gpt-4-0314' AS model
9
- ) AS m
10
- WHERE c.status = 1
11
- AND NOT EXISTS (
12
- SELECT 1
13
- FROM abilities a
14
- WHERE a.`group` = c.`group`
15
- AND a.model = m.model
16
- AND a.channel_id = c.id
17
- );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
new-api/bin/time_test.sh DELETED
@@ -1,40 +0,0 @@
1
- #!/bin/bash
2
-
3
- if [ $# -lt 3 ]; then
4
- echo "Usage: time_test.sh <domain> <key> <count> [<model>]"
5
- exit 1
6
- fi
7
-
8
- domain=$1
9
- key=$2
10
- count=$3
11
- model=${4:-"gpt-3.5-turbo"} # 设置默认模型为 gpt-3.5-turbo
12
-
13
- total_time=0
14
- times=()
15
-
16
- for ((i=1; i<=count; i++)); do
17
- result=$(curl -o /dev/null -s -w "%{http_code} %{time_total}\\n" \
18
- https://"$domain"/v1/chat/completions \
19
- -H "Content-Type: application/json" \
20
- -H "Authorization: Bearer $key" \
21
- -d '{"messages": [{"content": "echo hi", "role": "user"}], "model": "'"$model"'", "stream": false, "max_tokens": 1}')
22
- http_code=$(echo "$result" | awk '{print $1}')
23
- time=$(echo "$result" | awk '{print $2}')
24
- echo "HTTP status code: $http_code, Time taken: $time"
25
- total_time=$(bc <<< "$total_time + $time")
26
- times+=("$time")
27
- done
28
-
29
- average_time=$(echo "scale=4; $total_time / $count" | bc)
30
-
31
- sum_of_squares=0
32
- for time in "${times[@]}"; do
33
- difference=$(echo "scale=4; $time - $average_time" | bc)
34
- square=$(echo "scale=4; $difference * $difference" | bc)
35
- sum_of_squares=$(echo "scale=4; $sum_of_squares + $square" | bc)
36
- done
37
-
38
- standard_deviation=$(echo "scale=4; sqrt($sum_of_squares / $count)" | bc)
39
-
40
- echo "Average time: $average_time±$standard_deviation"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
new-api/common/api_type.go DELETED
@@ -1,77 +0,0 @@
1
- package common
2
-
3
- import "one-api/constant"
4
-
5
- func ChannelType2APIType(channelType int) (int, bool) {
6
- apiType := -1
7
- switch channelType {
8
- case constant.ChannelTypeOpenAI:
9
- apiType = constant.APITypeOpenAI
10
- case constant.ChannelTypeAnthropic:
11
- apiType = constant.APITypeAnthropic
12
- case constant.ChannelTypeBaidu:
13
- apiType = constant.APITypeBaidu
14
- case constant.ChannelTypePaLM:
15
- apiType = constant.APITypePaLM
16
- case constant.ChannelTypeZhipu:
17
- apiType = constant.APITypeZhipu
18
- case constant.ChannelTypeAli:
19
- apiType = constant.APITypeAli
20
- case constant.ChannelTypeXunfei:
21
- apiType = constant.APITypeXunfei
22
- case constant.ChannelTypeAIProxyLibrary:
23
- apiType = constant.APITypeAIProxyLibrary
24
- case constant.ChannelTypeTencent:
25
- apiType = constant.APITypeTencent
26
- case constant.ChannelTypeGemini:
27
- apiType = constant.APITypeGemini
28
- case constant.ChannelTypeZhipu_v4:
29
- apiType = constant.APITypeZhipuV4
30
- case constant.ChannelTypeOllama:
31
- apiType = constant.APITypeOllama
32
- case constant.ChannelTypePerplexity:
33
- apiType = constant.APITypePerplexity
34
- case constant.ChannelTypeAws:
35
- apiType = constant.APITypeAws
36
- case constant.ChannelTypeCohere:
37
- apiType = constant.APITypeCohere
38
- case constant.ChannelTypeDify:
39
- apiType = constant.APITypeDify
40
- case constant.ChannelTypeJina:
41
- apiType = constant.APITypeJina
42
- case constant.ChannelCloudflare:
43
- apiType = constant.APITypeCloudflare
44
- case constant.ChannelTypeSiliconFlow:
45
- apiType = constant.APITypeSiliconFlow
46
- case constant.ChannelTypeVertexAi:
47
- apiType = constant.APITypeVertexAi
48
- case constant.ChannelTypeMistral:
49
- apiType = constant.APITypeMistral
50
- case constant.ChannelTypeDeepSeek:
51
- apiType = constant.APITypeDeepSeek
52
- case constant.ChannelTypeMokaAI:
53
- apiType = constant.APITypeMokaAI
54
- case constant.ChannelTypeVolcEngine:
55
- apiType = constant.APITypeVolcEngine
56
- case constant.ChannelTypeBaiduV2:
57
- apiType = constant.APITypeBaiduV2
58
- case constant.ChannelTypeOpenRouter:
59
- apiType = constant.APITypeOpenRouter
60
- case constant.ChannelTypeXinference:
61
- apiType = constant.APITypeXinference
62
- case constant.ChannelTypeXai:
63
- apiType = constant.APITypeXai
64
- case constant.ChannelTypeCoze:
65
- apiType = constant.APITypeCoze
66
- case constant.ChannelTypeJimeng:
67
- apiType = constant.APITypeJimeng
68
- case constant.ChannelTypeMoonshot:
69
- apiType = constant.APITypeMoonshot
70
- case constant.ChannelTypeSubmodel:
71
- apiType = constant.APITypeSubmodel
72
- }
73
- if apiType == -1 {
74
- return constant.APITypeOpenAI, false
75
- }
76
- return apiType, true
77
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
new-api/common/constants.go DELETED
@@ -1,202 +0,0 @@
1
- package common
2
-
3
- import (
4
- //"os"
5
- //"strconv"
6
- "sync"
7
- "time"
8
-
9
- "github.com/google/uuid"
10
- )
11
-
12
- var StartTime = time.Now().Unix() // unit: second
13
- var Version = "v0.0.0" // this hard coding will be replaced automatically when building, no need to manually change
14
- var SystemName = "New API"
15
- var Footer = ""
16
- var Logo = ""
17
- var TopUpLink = ""
18
-
19
- // var ChatLink = ""
20
- // var ChatLink2 = ""
21
- var QuotaPerUnit = 500 * 1000.0 // $0.002 / 1K tokens
22
- var DisplayInCurrencyEnabled = true
23
- var DisplayTokenStatEnabled = true
24
- var DrawingEnabled = true
25
- var TaskEnabled = true
26
- var DataExportEnabled = true
27
- var DataExportInterval = 5 // unit: minute
28
- var DataExportDefaultTime = "hour" // unit: minute
29
- var DefaultCollapseSidebar = false // default value of collapse sidebar
30
-
31
- // Any options with "Secret", "Token" in its key won't be return by GetOptions
32
-
33
- var SessionSecret = uuid.New().String()
34
- var CryptoSecret = uuid.New().String()
35
-
36
- var OptionMap map[string]string
37
- var OptionMapRWMutex sync.RWMutex
38
-
39
- var ItemsPerPage = 10
40
- var MaxRecentItems = 100
41
-
42
- var PasswordLoginEnabled = true
43
- var PasswordRegisterEnabled = true
44
- var EmailVerificationEnabled = false
45
- var GitHubOAuthEnabled = false
46
- var LinuxDOOAuthEnabled = false
47
- var WeChatAuthEnabled = false
48
- var TelegramOAuthEnabled = false
49
- var TurnstileCheckEnabled = false
50
- var RegisterEnabled = true
51
-
52
- var EmailDomainRestrictionEnabled = false // 是否启用邮箱域名限制
53
- var EmailAliasRestrictionEnabled = false // 是否启用邮箱别名限制
54
- var EmailDomainWhitelist = []string{
55
- "gmail.com",
56
- "163.com",
57
- "126.com",
58
- "qq.com",
59
- "outlook.com",
60
- "hotmail.com",
61
- "icloud.com",
62
- "yahoo.com",
63
- "foxmail.com",
64
- }
65
- var EmailLoginAuthServerList = []string{
66
- "smtp.sendcloud.net",
67
- "smtp.azurecomm.net",
68
- }
69
-
70
- var DebugEnabled bool
71
- var MemoryCacheEnabled bool
72
-
73
- var LogConsumeEnabled = true
74
-
75
- var SMTPServer = ""
76
- var SMTPPort = 587
77
- var SMTPSSLEnabled = false
78
- var SMTPAccount = ""
79
- var SMTPFrom = ""
80
- var SMTPToken = ""
81
-
82
- var GitHubClientId = ""
83
- var GitHubClientSecret = ""
84
- var LinuxDOClientId = ""
85
- var LinuxDOClientSecret = ""
86
- var LinuxDOMinimumTrustLevel = 0
87
-
88
- var WeChatServerAddress = ""
89
- var WeChatServerToken = ""
90
- var WeChatAccountQRCodeImageURL = ""
91
-
92
- var TurnstileSiteKey = ""
93
- var TurnstileSecretKey = ""
94
-
95
- var TelegramBotToken = ""
96
- var TelegramBotName = ""
97
-
98
- var QuotaForNewUser = 0
99
- var QuotaForInviter = 0
100
- var QuotaForInvitee = 0
101
- var ChannelDisableThreshold = 5.0
102
- var AutomaticDisableChannelEnabled = false
103
- var AutomaticEnableChannelEnabled = false
104
- var QuotaRemindThreshold = 1000
105
- var PreConsumedQuota = 500
106
-
107
- var RetryTimes = 0
108
-
109
- //var RootUserEmail = ""
110
-
111
- var IsMasterNode bool
112
-
113
- var requestInterval int
114
- var RequestInterval time.Duration
115
-
116
- var SyncFrequency int // unit is second
117
-
118
- var BatchUpdateEnabled = false
119
- var BatchUpdateInterval int
120
-
121
- var RelayTimeout int // unit is second
122
-
123
- var GeminiSafetySetting string
124
-
125
- // https://docs.cohere.com/docs/safety-modes Type; NONE/CONTEXTUAL/STRICT
126
- var CohereSafetySetting string
127
-
128
- const (
129
- RequestIdKey = "X-Oneapi-Request-Id"
130
- )
131
-
132
- const (
133
- RoleGuestUser = 0
134
- RoleCommonUser = 1
135
- RoleAdminUser = 10
136
- RoleRootUser = 100
137
- )
138
-
139
- func IsValidateRole(role int) bool {
140
- return role == RoleGuestUser || role == RoleCommonUser || role == RoleAdminUser || role == RoleRootUser
141
- }
142
-
143
- var (
144
- FileUploadPermission = RoleGuestUser
145
- FileDownloadPermission = RoleGuestUser
146
- ImageUploadPermission = RoleGuestUser
147
- ImageDownloadPermission = RoleGuestUser
148
- )
149
-
150
- // All duration's unit is seconds
151
- // Shouldn't larger then RateLimitKeyExpirationDuration
152
- var (
153
- GlobalApiRateLimitEnable bool
154
- GlobalApiRateLimitNum int
155
- GlobalApiRateLimitDuration int64
156
-
157
- GlobalWebRateLimitEnable bool
158
- GlobalWebRateLimitNum int
159
- GlobalWebRateLimitDuration int64
160
-
161
- UploadRateLimitNum = 10
162
- UploadRateLimitDuration int64 = 60
163
-
164
- DownloadRateLimitNum = 10
165
- DownloadRateLimitDuration int64 = 60
166
-
167
- CriticalRateLimitNum = 20
168
- CriticalRateLimitDuration int64 = 20 * 60
169
- )
170
-
171
- var RateLimitKeyExpirationDuration = 20 * time.Minute
172
-
173
- const (
174
- UserStatusEnabled = 1 // don't use 0, 0 is the default value!
175
- UserStatusDisabled = 2 // also don't use 0
176
- )
177
-
178
- const (
179
- TokenStatusEnabled = 1 // don't use 0, 0 is the default value!
180
- TokenStatusDisabled = 2 // also don't use 0
181
- TokenStatusExpired = 3
182
- TokenStatusExhausted = 4
183
- )
184
-
185
- const (
186
- RedemptionCodeStatusEnabled = 1 // don't use 0, 0 is the default value!
187
- RedemptionCodeStatusDisabled = 2 // also don't use 0
188
- RedemptionCodeStatusUsed = 3 // also don't use 0
189
- )
190
-
191
- const (
192
- ChannelStatusUnknown = 0
193
- ChannelStatusEnabled = 1 // don't use 0, 0 is the default value!
194
- ChannelStatusManuallyDisabled = 2 // also don't use 0
195
- ChannelStatusAutoDisabled = 3
196
- )
197
-
198
- const (
199
- TopUpStatusPending = "pending"
200
- TopUpStatusSuccess = "success"
201
- TopUpStatusExpired = "expired"
202
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
new-api/common/copy.go DELETED
@@ -1,19 +0,0 @@
1
- package common
2
-
3
- import (
4
- "fmt"
5
-
6
- "github.com/jinzhu/copier"
7
- )
8
-
9
- func DeepCopy[T any](src *T) (*T, error) {
10
- if src == nil {
11
- return nil, fmt.Errorf("copy source cannot be nil")
12
- }
13
- var dst T
14
- err := copier.CopyWithOption(&dst, src, copier.Option{DeepCopy: true, IgnoreEmpty: true})
15
- if err != nil {
16
- return nil, err
17
- }
18
- return &dst, nil
19
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
new-api/common/crypto.go DELETED
@@ -1,31 +0,0 @@
1
- package common
2
-
3
- import (
4
- "crypto/hmac"
5
- "crypto/sha256"
6
- "encoding/hex"
7
- "golang.org/x/crypto/bcrypt"
8
- )
9
-
10
- func GenerateHMACWithKey(key []byte, data string) string {
11
- h := hmac.New(sha256.New, key)
12
- h.Write([]byte(data))
13
- return hex.EncodeToString(h.Sum(nil))
14
- }
15
-
16
- func GenerateHMAC(data string) string {
17
- h := hmac.New(sha256.New, []byte(CryptoSecret))
18
- h.Write([]byte(data))
19
- return hex.EncodeToString(h.Sum(nil))
20
- }
21
-
22
- func Password2Hash(password string) (string, error) {
23
- passwordBytes := []byte(password)
24
- hashedPassword, err := bcrypt.GenerateFromPassword(passwordBytes, bcrypt.DefaultCost)
25
- return string(hashedPassword), err
26
- }
27
-
28
- func ValidatePasswordAndHash(password string, hash string) bool {
29
- err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
30
- return err == nil
31
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
new-api/common/custom-event.go DELETED
@@ -1,87 +0,0 @@
1
- // Copyright 2014 Manu Martinez-Almeida. All rights reserved.
2
- // Use of this source code is governed by a MIT style
3
- // license that can be found in the LICENSE file.
4
-
5
- package common
6
-
7
- import (
8
- "fmt"
9
- "io"
10
- "net/http"
11
- "strings"
12
- "sync"
13
- )
14
-
15
- type stringWriter interface {
16
- io.Writer
17
- writeString(string) (int, error)
18
- }
19
-
20
- type stringWrapper struct {
21
- io.Writer
22
- }
23
-
24
- func (w stringWrapper) writeString(str string) (int, error) {
25
- return w.Writer.Write([]byte(str))
26
- }
27
-
28
- func checkWriter(writer io.Writer) stringWriter {
29
- if w, ok := writer.(stringWriter); ok {
30
- return w
31
- } else {
32
- return stringWrapper{writer}
33
- }
34
- }
35
-
36
- // Server-Sent Events
37
- // W3C Working Draft 29 October 2009
38
- // http://www.w3.org/TR/2009/WD-eventsource-20091029/
39
-
40
- var contentType = []string{"text/event-stream"}
41
- var noCache = []string{"no-cache"}
42
-
43
- var fieldReplacer = strings.NewReplacer(
44
- "\n", "\\n",
45
- "\r", "\\r")
46
-
47
- var dataReplacer = strings.NewReplacer(
48
- "\n", "\n",
49
- "\r", "\\r")
50
-
51
- type CustomEvent struct {
52
- Event string
53
- Id string
54
- Retry uint
55
- Data interface{}
56
-
57
- Mutex sync.Mutex
58
- }
59
-
60
- func encode(writer io.Writer, event CustomEvent) error {
61
- w := checkWriter(writer)
62
- return writeData(w, event.Data)
63
- }
64
-
65
- func writeData(w stringWriter, data interface{}) error {
66
- dataReplacer.WriteString(w, fmt.Sprint(data))
67
- if strings.HasPrefix(data.(string), "data") {
68
- w.writeString("\n\n")
69
- }
70
- return nil
71
- }
72
-
73
- func (r CustomEvent) Render(w http.ResponseWriter) error {
74
- r.WriteContentType(w)
75
- return encode(w, r)
76
- }
77
-
78
- func (r CustomEvent) WriteContentType(w http.ResponseWriter) {
79
- r.Mutex.Lock()
80
- defer r.Mutex.Unlock()
81
- header := w.Header()
82
- header["Content-Type"] = contentType
83
-
84
- if _, exist := header["Cache-Control"]; !exist {
85
- header["Cache-Control"] = noCache
86
- }
87
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
new-api/common/database.go DELETED
@@ -1,15 +0,0 @@
1
- package common
2
-
3
- const (
4
- DatabaseTypeMySQL = "mysql"
5
- DatabaseTypeSQLite = "sqlite"
6
- DatabaseTypePostgreSQL = "postgres"
7
- )
8
-
9
- var UsingSQLite = false
10
- var UsingPostgreSQL = false
11
- var LogSqlType = DatabaseTypeSQLite // Default to SQLite for logging SQL queries
12
- var UsingMySQL = false
13
- var UsingClickHouse = false
14
-
15
- var SQLitePath = "one-api.db?_busy_timeout=30000"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
new-api/common/email-outlook-auth.go DELETED
@@ -1,40 +0,0 @@
1
- package common
2
-
3
- import (
4
- "errors"
5
- "net/smtp"
6
- "strings"
7
- )
8
-
9
- type outlookAuth struct {
10
- username, password string
11
- }
12
-
13
- func LoginAuth(username, password string) smtp.Auth {
14
- return &outlookAuth{username, password}
15
- }
16
-
17
- func (a *outlookAuth) Start(_ *smtp.ServerInfo) (string, []byte, error) {
18
- return "LOGIN", []byte{}, nil
19
- }
20
-
21
- func (a *outlookAuth) Next(fromServer []byte, more bool) ([]byte, error) {
22
- if more {
23
- switch string(fromServer) {
24
- case "Username:":
25
- return []byte(a.username), nil
26
- case "Password:":
27
- return []byte(a.password), nil
28
- default:
29
- return nil, errors.New("unknown fromServer")
30
- }
31
- }
32
- return nil, nil
33
- }
34
-
35
- func isOutlookServer(server string) bool {
36
- // 兼容多地区的outlook邮箱和ofb邮箱
37
- // 其实应该加一个Option来区分是否用LOGIN的方式登录
38
- // 先临时兼容一下
39
- return strings.Contains(server, "outlook") || strings.Contains(server, "onmicrosoft")
40
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
new-api/common/email.go DELETED
@@ -1,90 +0,0 @@
1
- package common
2
-
3
- import (
4
- "crypto/tls"
5
- "encoding/base64"
6
- "fmt"
7
- "net/smtp"
8
- "slices"
9
- "strings"
10
- "time"
11
- )
12
-
13
- func generateMessageID() (string, error) {
14
- split := strings.Split(SMTPFrom, "@")
15
- if len(split) < 2 {
16
- return "", fmt.Errorf("invalid SMTP account")
17
- }
18
- domain := strings.Split(SMTPFrom, "@")[1]
19
- return fmt.Sprintf("<%d.%s@%s>", time.Now().UnixNano(), GetRandomString(12), domain), nil
20
- }
21
-
22
- func SendEmail(subject string, receiver string, content string) error {
23
- if SMTPFrom == "" { // for compatibility
24
- SMTPFrom = SMTPAccount
25
- }
26
- id, err2 := generateMessageID()
27
- if err2 != nil {
28
- return err2
29
- }
30
- if SMTPServer == "" && SMTPAccount == "" {
31
- return fmt.Errorf("SMTP 服务器未配置")
32
- }
33
- encodedSubject := fmt.Sprintf("=?UTF-8?B?%s?=", base64.StdEncoding.EncodeToString([]byte(subject)))
34
- mail := []byte(fmt.Sprintf("To: %s\r\n"+
35
- "From: %s<%s>\r\n"+
36
- "Subject: %s\r\n"+
37
- "Date: %s\r\n"+
38
- "Message-ID: %s\r\n"+ // 添加 Message-ID 头
39
- "Content-Type: text/html; charset=UTF-8\r\n\r\n%s\r\n",
40
- receiver, SystemName, SMTPFrom, encodedSubject, time.Now().Format(time.RFC1123Z), id, content))
41
- auth := smtp.PlainAuth("", SMTPAccount, SMTPToken, SMTPServer)
42
- addr := fmt.Sprintf("%s:%d", SMTPServer, SMTPPort)
43
- to := strings.Split(receiver, ";")
44
- var err error
45
- if SMTPPort == 465 || SMTPSSLEnabled {
46
- tlsConfig := &tls.Config{
47
- InsecureSkipVerify: true,
48
- ServerName: SMTPServer,
49
- }
50
- conn, err := tls.Dial("tcp", fmt.Sprintf("%s:%d", SMTPServer, SMTPPort), tlsConfig)
51
- if err != nil {
52
- return err
53
- }
54
- client, err := smtp.NewClient(conn, SMTPServer)
55
- if err != nil {
56
- return err
57
- }
58
- defer client.Close()
59
- if err = client.Auth(auth); err != nil {
60
- return err
61
- }
62
- if err = client.Mail(SMTPFrom); err != nil {
63
- return err
64
- }
65
- receiverEmails := strings.Split(receiver, ";")
66
- for _, receiver := range receiverEmails {
67
- if err = client.Rcpt(receiver); err != nil {
68
- return err
69
- }
70
- }
71
- w, err := client.Data()
72
- if err != nil {
73
- return err
74
- }
75
- _, err = w.Write(mail)
76
- if err != nil {
77
- return err
78
- }
79
- err = w.Close()
80
- if err != nil {
81
- return err
82
- }
83
- } else if isOutlookServer(SMTPAccount) || slices.Contains(EmailLoginAuthServerList, SMTPServer) {
84
- auth = LoginAuth(SMTPAccount, SMTPToken)
85
- err = smtp.SendMail(addr, auth, SMTPFrom, to, mail)
86
- } else {
87
- err = smtp.SendMail(addr, auth, SMTPFrom, to, mail)
88
- }
89
- return err
90
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
new-api/common/embed-file-system.go DELETED
@@ -1,32 +0,0 @@
1
- package common
2
-
3
- import (
4
- "embed"
5
- "github.com/gin-contrib/static"
6
- "io/fs"
7
- "net/http"
8
- )
9
-
10
- // Credit: https://github.com/gin-contrib/static/issues/19
11
-
12
- type embedFileSystem struct {
13
- http.FileSystem
14
- }
15
-
16
- func (e embedFileSystem) Exists(prefix string, path string) bool {
17
- _, err := e.Open(path)
18
- if err != nil {
19
- return false
20
- }
21
- return true
22
- }
23
-
24
- func EmbedFolder(fsEmbed embed.FS, targetPath string) static.ServeFileSystem {
25
- efs, err := fs.Sub(fsEmbed, targetPath)
26
- if err != nil {
27
- panic(err)
28
- }
29
- return embedFileSystem{
30
- FileSystem: http.FS(efs),
31
- }
32
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
new-api/common/endpoint_defaults.go DELETED
@@ -1,33 +0,0 @@
1
- package common
2
-
3
- import "one-api/constant"
4
-
5
- // EndpointInfo 描述单个端点的默认请求信息
6
- // path: 上游路径
7
- // method: HTTP 请求方式,例如 POST/GET
8
- // 目前均为 POST,后续可扩展
9
- //
10
- // json 标签用于直接序列化到 API 输出
11
- // 例如:{"path":"/v1/chat/completions","method":"POST"}
12
-
13
- type EndpointInfo struct {
14
- Path string `json:"path"`
15
- Method string `json:"method"`
16
- }
17
-
18
- // defaultEndpointInfoMap 保存内置端点的默认 Path 与 Method
19
- var defaultEndpointInfoMap = map[constant.EndpointType]EndpointInfo{
20
- constant.EndpointTypeOpenAI: {Path: "/v1/chat/completions", Method: "POST"},
21
- constant.EndpointTypeOpenAIResponse: {Path: "/v1/responses", Method: "POST"},
22
- constant.EndpointTypeAnthropic: {Path: "/v1/messages", Method: "POST"},
23
- constant.EndpointTypeGemini: {Path: "/v1beta/models/{model}:generateContent", Method: "POST"},
24
- constant.EndpointTypeJinaRerank: {Path: "/rerank", Method: "POST"},
25
- constant.EndpointTypeImageGeneration: {Path: "/v1/images/generations", Method: "POST"},
26
- constant.EndpointTypeEmbeddings: {Path: "/v1/embeddings", Method: "POST"},
27
- }
28
-
29
- // GetDefaultEndpointInfo 返回指定端点类型的默认信息以及是否存在
30
- func GetDefaultEndpointInfo(et constant.EndpointType) (EndpointInfo, bool) {
31
- info, ok := defaultEndpointInfoMap[et]
32
- return info, ok
33
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
new-api/common/endpoint_type.go DELETED
@@ -1,41 +0,0 @@
1
- package common
2
-
3
- import "one-api/constant"
4
-
5
- // GetEndpointTypesByChannelType 获取渠道最优先端点类型(所有的渠道都支持 OpenAI 端点)
6
- func GetEndpointTypesByChannelType(channelType int, modelName string) []constant.EndpointType {
7
- var endpointTypes []constant.EndpointType
8
- switch channelType {
9
- case constant.ChannelTypeJina:
10
- endpointTypes = []constant.EndpointType{constant.EndpointTypeJinaRerank}
11
- //case constant.ChannelTypeMidjourney, constant.ChannelTypeMidjourneyPlus:
12
- // endpointTypes = []constant.EndpointType{constant.EndpointTypeMidjourney}
13
- //case constant.ChannelTypeSunoAPI:
14
- // endpointTypes = []constant.EndpointType{constant.EndpointTypeSuno}
15
- //case constant.ChannelTypeKling:
16
- // endpointTypes = []constant.EndpointType{constant.EndpointTypeKling}
17
- //case constant.ChannelTypeJimeng:
18
- // endpointTypes = []constant.EndpointType{constant.EndpointTypeJimeng}
19
- case constant.ChannelTypeAws:
20
- fallthrough
21
- case constant.ChannelTypeAnthropic:
22
- endpointTypes = []constant.EndpointType{constant.EndpointTypeAnthropic, constant.EndpointTypeOpenAI}
23
- case constant.ChannelTypeVertexAi:
24
- fallthrough
25
- case constant.ChannelTypeGemini:
26
- endpointTypes = []constant.EndpointType{constant.EndpointTypeGemini, constant.EndpointTypeOpenAI}
27
- case constant.ChannelTypeOpenRouter: // OpenRouter 只支持 OpenAI 端点
28
- endpointTypes = []constant.EndpointType{constant.EndpointTypeOpenAI}
29
- default:
30
- if IsOpenAIResponseOnlyModel(modelName) {
31
- endpointTypes = []constant.EndpointType{constant.EndpointTypeOpenAIResponse}
32
- } else {
33
- endpointTypes = []constant.EndpointType{constant.EndpointTypeOpenAI}
34
- }
35
- }
36
- if IsImageGenerationModel(modelName) {
37
- // add to first
38
- endpointTypes = append([]constant.EndpointType{constant.EndpointTypeImageGeneration}, endpointTypes...)
39
- }
40
- return endpointTypes
41
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
new-api/common/env.go DELETED
@@ -1,38 +0,0 @@
1
- package common
2
-
3
- import (
4
- "fmt"
5
- "os"
6
- "strconv"
7
- )
8
-
9
- func GetEnvOrDefault(env string, defaultValue int) int {
10
- if env == "" || os.Getenv(env) == "" {
11
- return defaultValue
12
- }
13
- num, err := strconv.Atoi(os.Getenv(env))
14
- if err != nil {
15
- SysError(fmt.Sprintf("failed to parse %s: %s, using default value: %d", env, err.Error(), defaultValue))
16
- return defaultValue
17
- }
18
- return num
19
- }
20
-
21
- func GetEnvOrDefaultString(env string, defaultValue string) string {
22
- if env == "" || os.Getenv(env) == "" {
23
- return defaultValue
24
- }
25
- return os.Getenv(env)
26
- }
27
-
28
- func GetEnvOrDefaultBool(env string, defaultValue bool) bool {
29
- if env == "" || os.Getenv(env) == "" {
30
- return defaultValue
31
- }
32
- b, err := strconv.ParseBool(os.Getenv(env))
33
- if err != nil {
34
- SysError(fmt.Sprintf("failed to parse %s: %s, using default value: %t", env, err.Error(), defaultValue))
35
- return defaultValue
36
- }
37
- return b
38
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
new-api/common/gin.go DELETED
@@ -1,115 +0,0 @@
1
- package common
2
-
3
- import (
4
- "bytes"
5
- "io"
6
- "net/http"
7
- "one-api/constant"
8
- "strings"
9
- "time"
10
-
11
- "github.com/gin-gonic/gin"
12
- )
13
-
14
- const KeyRequestBody = "key_request_body"
15
-
16
- func GetRequestBody(c *gin.Context) ([]byte, error) {
17
- requestBody, _ := c.Get(KeyRequestBody)
18
- if requestBody != nil {
19
- return requestBody.([]byte), nil
20
- }
21
- requestBody, err := io.ReadAll(c.Request.Body)
22
- if err != nil {
23
- return nil, err
24
- }
25
- _ = c.Request.Body.Close()
26
- c.Set(KeyRequestBody, requestBody)
27
- return requestBody.([]byte), nil
28
- }
29
-
30
- func UnmarshalBodyReusable(c *gin.Context, v any) error {
31
- requestBody, err := GetRequestBody(c)
32
- if err != nil {
33
- return err
34
- }
35
- //if DebugEnabled {
36
- // println("UnmarshalBodyReusable request body:", string(requestBody))
37
- //}
38
- contentType := c.Request.Header.Get("Content-Type")
39
- if strings.HasPrefix(contentType, "application/json") {
40
- err = Unmarshal(requestBody, &v)
41
- } else {
42
- // skip for now
43
- // TODO: someday non json request have variant model, we will need to implementation this
44
- }
45
- if err != nil {
46
- return err
47
- }
48
- // Reset request body
49
- c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody))
50
- return nil
51
- }
52
-
53
- func SetContextKey(c *gin.Context, key constant.ContextKey, value any) {
54
- c.Set(string(key), value)
55
- }
56
-
57
- func GetContextKey(c *gin.Context, key constant.ContextKey) (any, bool) {
58
- return c.Get(string(key))
59
- }
60
-
61
- func GetContextKeyString(c *gin.Context, key constant.ContextKey) string {
62
- return c.GetString(string(key))
63
- }
64
-
65
- func GetContextKeyInt(c *gin.Context, key constant.ContextKey) int {
66
- return c.GetInt(string(key))
67
- }
68
-
69
- func GetContextKeyBool(c *gin.Context, key constant.ContextKey) bool {
70
- return c.GetBool(string(key))
71
- }
72
-
73
- func GetContextKeyStringSlice(c *gin.Context, key constant.ContextKey) []string {
74
- return c.GetStringSlice(string(key))
75
- }
76
-
77
- func GetContextKeyStringMap(c *gin.Context, key constant.ContextKey) map[string]any {
78
- return c.GetStringMap(string(key))
79
- }
80
-
81
- func GetContextKeyTime(c *gin.Context, key constant.ContextKey) time.Time {
82
- return c.GetTime(string(key))
83
- }
84
-
85
- func GetContextKeyType[T any](c *gin.Context, key constant.ContextKey) (T, bool) {
86
- if value, ok := c.Get(string(key)); ok {
87
- if v, ok := value.(T); ok {
88
- return v, true
89
- }
90
- }
91
- var t T
92
- return t, false
93
- }
94
-
95
- func ApiError(c *gin.Context, err error) {
96
- c.JSON(http.StatusOK, gin.H{
97
- "success": false,
98
- "message": err.Error(),
99
- })
100
- }
101
-
102
- func ApiErrorMsg(c *gin.Context, msg string) {
103
- c.JSON(http.StatusOK, gin.H{
104
- "success": false,
105
- "message": msg,
106
- })
107
- }
108
-
109
- func ApiSuccess(c *gin.Context, data any) {
110
- c.JSON(http.StatusOK, gin.H{
111
- "success": true,
112
- "message": "",
113
- "data": data,
114
- })
115
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
new-api/common/go-channel.go DELETED
@@ -1,53 +0,0 @@
1
- package common
2
-
3
- import (
4
- "time"
5
- )
6
-
7
- func SafeSendBool(ch chan bool, value bool) (closed bool) {
8
- defer func() {
9
- // Recover from panic if one occured. A panic would mean the channel was closed.
10
- if recover() != nil {
11
- closed = true
12
- }
13
- }()
14
-
15
- // This will panic if the channel is closed.
16
- ch <- value
17
-
18
- // If the code reaches here, then the channel was not closed.
19
- return false
20
- }
21
-
22
- func SafeSendString(ch chan string, value string) (closed bool) {
23
- defer func() {
24
- // Recover from panic if one occured. A panic would mean the channel was closed.
25
- if recover() != nil {
26
- closed = true
27
- }
28
- }()
29
-
30
- // This will panic if the channel is closed.
31
- ch <- value
32
-
33
- // If the code reaches here, then the channel was not closed.
34
- return false
35
- }
36
-
37
- // SafeSendStringTimeout send, return true, else return false
38
- func SafeSendStringTimeout(ch chan string, value string, timeout int) (closed bool) {
39
- defer func() {
40
- // Recover from panic if one occured. A panic would mean the channel was closed.
41
- if recover() != nil {
42
- closed = false
43
- }
44
- }()
45
-
46
- // This will panic if the channel is closed.
47
- select {
48
- case ch <- value:
49
- return true
50
- case <-time.After(time.Duration(timeout) * time.Second):
51
- return false
52
- }
53
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
new-api/common/gopool.go DELETED
@@ -1,24 +0,0 @@
1
- package common
2
-
3
- import (
4
- "context"
5
- "fmt"
6
- "github.com/bytedance/gopkg/util/gopool"
7
- "math"
8
- )
9
-
10
- var relayGoPool gopool.Pool
11
-
12
- func init() {
13
- relayGoPool = gopool.NewPool("gopool.RelayPool", math.MaxInt32, gopool.NewConfig())
14
- relayGoPool.SetPanicHandler(func(ctx context.Context, i interface{}) {
15
- if stopChan, ok := ctx.Value("stop_chan").(chan bool); ok {
16
- SafeSendBool(stopChan, true)
17
- }
18
- SysError(fmt.Sprintf("panic in gopool.RelayPool: %v", i))
19
- })
20
- }
21
-
22
- func RelayCtxGo(ctx context.Context, f func()) {
23
- relayGoPool.CtxGo(ctx, f)
24
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
new-api/common/hash.go DELETED
@@ -1,34 +0,0 @@
1
- package common
2
-
3
- import (
4
- "crypto/hmac"
5
- "crypto/sha1"
6
- "crypto/sha256"
7
- "encoding/hex"
8
- )
9
-
10
- func Sha256Raw(data []byte) []byte {
11
- h := sha256.New()
12
- h.Write(data)
13
- return h.Sum(nil)
14
- }
15
-
16
- func Sha1Raw(data []byte) []byte {
17
- h := sha1.New()
18
- h.Write(data)
19
- return h.Sum(nil)
20
- }
21
-
22
- func Sha1(data []byte) string {
23
- return hex.EncodeToString(Sha1Raw(data))
24
- }
25
-
26
- func HmacSha256Raw(message, key []byte) []byte {
27
- h := hmac.New(sha256.New, key)
28
- h.Write(message)
29
- return h.Sum(nil)
30
- }
31
-
32
- func HmacSha256(message, key string) string {
33
- return hex.EncodeToString(HmacSha256Raw([]byte(message), []byte(key)))
34
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
new-api/common/init.go DELETED
@@ -1,120 +0,0 @@
1
- package common
2
-
3
- import (
4
- "flag"
5
- "fmt"
6
- "log"
7
- "one-api/constant"
8
- "os"
9
- "path/filepath"
10
- "strconv"
11
- "time"
12
- )
13
-
14
- var (
15
- Port = flag.Int("port", 3000, "the listening port")
16
- PrintVersion = flag.Bool("version", false, "print version and exit")
17
- PrintHelp = flag.Bool("help", false, "print help and exit")
18
- LogDir = flag.String("log-dir", "./logs", "specify the log directory")
19
- )
20
-
21
- func printHelp() {
22
- fmt.Println("New API " + Version + " - All in one API service for OpenAI API.")
23
- fmt.Println("Copyright (C) 2023 JustSong. All rights reserved.")
24
- fmt.Println("GitHub: https://github.com/songquanpeng/one-api")
25
- fmt.Println("Usage: one-api [--port <port>] [--log-dir <log directory>] [--version] [--help]")
26
- }
27
-
28
- func InitEnv() {
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("SESSION_SECRET") != "" {
42
- ss := os.Getenv("SESSION_SECRET")
43
- if ss == "random_string" {
44
- log.Println("WARNING: SESSION_SECRET is set to the default value 'random_string', please change it to a random string.")
45
- log.Println("警告:SESSION_SECRET被设置为默认值'random_string',请修改为随机字符串。")
46
- log.Fatal("Please set SESSION_SECRET to a random string.")
47
- } else {
48
- SessionSecret = ss
49
- }
50
- }
51
- if os.Getenv("CRYPTO_SECRET") != "" {
52
- CryptoSecret = os.Getenv("CRYPTO_SECRET")
53
- } else {
54
- CryptoSecret = SessionSecret
55
- }
56
- if os.Getenv("SQLITE_PATH") != "" {
57
- SQLitePath = os.Getenv("SQLITE_PATH")
58
- }
59
- if *LogDir != "" {
60
- var err error
61
- *LogDir, err = filepath.Abs(*LogDir)
62
- if err != nil {
63
- log.Fatal(err)
64
- }
65
- if _, err := os.Stat(*LogDir); os.IsNotExist(err) {
66
- err = os.Mkdir(*LogDir, 0777)
67
- if err != nil {
68
- log.Fatal(err)
69
- }
70
- }
71
- }
72
-
73
- // Initialize variables from constants.go that were using environment variables
74
- DebugEnabled = os.Getenv("DEBUG") == "true"
75
- MemoryCacheEnabled = os.Getenv("MEMORY_CACHE_ENABLED") == "true"
76
- IsMasterNode = os.Getenv("NODE_TYPE") != "slave"
77
-
78
- // Parse requestInterval and set RequestInterval
79
- requestInterval, _ = strconv.Atoi(os.Getenv("POLLING_INTERVAL"))
80
- RequestInterval = time.Duration(requestInterval) * time.Second
81
-
82
- // Initialize variables with GetEnvOrDefault
83
- SyncFrequency = GetEnvOrDefault("SYNC_FREQUENCY", 60)
84
- BatchUpdateInterval = GetEnvOrDefault("BATCH_UPDATE_INTERVAL", 5)
85
- RelayTimeout = GetEnvOrDefault("RELAY_TIMEOUT", 0)
86
-
87
- // Initialize string variables with GetEnvOrDefaultString
88
- GeminiSafetySetting = GetEnvOrDefaultString("GEMINI_SAFETY_SETTING", "BLOCK_NONE")
89
- CohereSafetySetting = GetEnvOrDefaultString("COHERE_SAFETY_SETTING", "NONE")
90
-
91
- // Initialize rate limit variables
92
- GlobalApiRateLimitEnable = GetEnvOrDefaultBool("GLOBAL_API_RATE_LIMIT_ENABLE", true)
93
- GlobalApiRateLimitNum = GetEnvOrDefault("GLOBAL_API_RATE_LIMIT", 180)
94
- GlobalApiRateLimitDuration = int64(GetEnvOrDefault("GLOBAL_API_RATE_LIMIT_DURATION", 180))
95
-
96
- GlobalWebRateLimitEnable = GetEnvOrDefaultBool("GLOBAL_WEB_RATE_LIMIT_ENABLE", true)
97
- GlobalWebRateLimitNum = GetEnvOrDefault("GLOBAL_WEB_RATE_LIMIT", 60)
98
- GlobalWebRateLimitDuration = int64(GetEnvOrDefault("GLOBAL_WEB_RATE_LIMIT_DURATION", 180))
99
-
100
- initConstantEnv()
101
- }
102
-
103
- func initConstantEnv() {
104
- constant.StreamingTimeout = GetEnvOrDefault("STREAMING_TIMEOUT", 300)
105
- constant.DifyDebug = GetEnvOrDefaultBool("DIFY_DEBUG", true)
106
- constant.MaxFileDownloadMB = GetEnvOrDefault("MAX_FILE_DOWNLOAD_MB", 20)
107
- // ForceStreamOption 覆盖请求参数,强制返回usage信息
108
- constant.ForceStreamOption = GetEnvOrDefaultBool("FORCE_STREAM_OPTION", true)
109
- constant.GetMediaToken = GetEnvOrDefaultBool("GET_MEDIA_TOKEN", true)
110
- constant.GetMediaTokenNotStream = GetEnvOrDefaultBool("GET_MEDIA_TOKEN_NOT_STREAM", true)
111
- constant.UpdateTask = GetEnvOrDefaultBool("UPDATE_TASK", true)
112
- constant.AzureDefaultAPIVersion = GetEnvOrDefaultString("AZURE_DEFAULT_API_VERSION", "2025-04-01-preview")
113
- constant.GeminiVisionMaxImageNum = GetEnvOrDefault("GEMINI_VISION_MAX_IMAGE_NUM", 16)
114
- constant.NotifyLimitCount = GetEnvOrDefault("NOTIFY_LIMIT_COUNT", 2)
115
- constant.NotificationLimitDurationMinute = GetEnvOrDefault("NOTIFICATION_LIMIT_DURATION_MINUTE", 10)
116
- // GenerateDefaultToken 是否生成初始令牌,默认关闭。
117
- constant.GenerateDefaultToken = GetEnvOrDefaultBool("GENERATE_DEFAULT_TOKEN", false)
118
- // 是否启用错误日志
119
- constant.ErrorLogEnabled = GetEnvOrDefaultBool("ERROR_LOG_ENABLED", false)
120
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
new-api/common/ip.go DELETED
@@ -1,22 +0,0 @@
1
- package common
2
-
3
- import "net"
4
-
5
- func IsPrivateIP(ip net.IP) bool {
6
- if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() {
7
- return true
8
- }
9
-
10
- private := []net.IPNet{
11
- {IP: net.IPv4(10, 0, 0, 0), Mask: net.CIDRMask(8, 32)},
12
- {IP: net.IPv4(172, 16, 0, 0), Mask: net.CIDRMask(12, 32)},
13
- {IP: net.IPv4(192, 168, 0, 0), Mask: net.CIDRMask(16, 32)},
14
- }
15
-
16
- for _, privateNet := range private {
17
- if privateNet.Contains(ip) {
18
- return true
19
- }
20
- }
21
- return false
22
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
new-api/common/json.go DELETED
@@ -1,44 +0,0 @@
1
- package common
2
-
3
- import (
4
- "bytes"
5
- "encoding/json"
6
- )
7
-
8
- func Unmarshal(data []byte, v any) error {
9
- return json.Unmarshal(data, v)
10
- }
11
-
12
- func UnmarshalJsonStr(data string, v any) error {
13
- return json.Unmarshal(StringToByteSlice(data), v)
14
- }
15
-
16
- func DecodeJson(reader *bytes.Reader, v any) error {
17
- return json.NewDecoder(reader).Decode(v)
18
- }
19
-
20
- func Marshal(v any) ([]byte, error) {
21
- return json.Marshal(v)
22
- }
23
-
24
- func GetJsonType(data json.RawMessage) string {
25
- data = bytes.TrimSpace(data)
26
- if len(data) == 0 {
27
- return "unknown"
28
- }
29
- firstChar := bytes.TrimSpace(data)[0]
30
- switch firstChar {
31
- case '{':
32
- return "object"
33
- case '[':
34
- return "array"
35
- case '"':
36
- return "string"
37
- case 't', 'f':
38
- return "boolean"
39
- case 'n':
40
- return "null"
41
- default:
42
- return "number"
43
- }
44
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
new-api/common/limiter/limiter.go DELETED
@@ -1,89 +0,0 @@
1
- package limiter
2
-
3
- import (
4
- "context"
5
- _ "embed"
6
- "fmt"
7
- "github.com/go-redis/redis/v8"
8
- "one-api/common"
9
- "sync"
10
- )
11
-
12
- //go:embed lua/rate_limit.lua
13
- var rateLimitScript string
14
-
15
- type RedisLimiter struct {
16
- client *redis.Client
17
- limitScriptSHA string
18
- }
19
-
20
- var (
21
- instance *RedisLimiter
22
- once sync.Once
23
- )
24
-
25
- func New(ctx context.Context, r *redis.Client) *RedisLimiter {
26
- once.Do(func() {
27
- // 预加载脚本
28
- limitSHA, err := r.ScriptLoad(ctx, rateLimitScript).Result()
29
- if err != nil {
30
- common.SysLog(fmt.Sprintf("Failed to load rate limit script: %v", err))
31
- }
32
- instance = &RedisLimiter{
33
- client: r,
34
- limitScriptSHA: limitSHA,
35
- }
36
- })
37
-
38
- return instance
39
- }
40
-
41
- func (rl *RedisLimiter) Allow(ctx context.Context, key string, opts ...Option) (bool, error) {
42
- // 默认配置
43
- config := &Config{
44
- Capacity: 10,
45
- Rate: 1,
46
- Requested: 1,
47
- }
48
-
49
- // 应用选项模式
50
- for _, opt := range opts {
51
- opt(config)
52
- }
53
-
54
- // 执行限流
55
- result, err := rl.client.EvalSha(
56
- ctx,
57
- rl.limitScriptSHA,
58
- []string{key},
59
- config.Requested,
60
- config.Rate,
61
- config.Capacity,
62
- ).Int()
63
-
64
- if err != nil {
65
- return false, fmt.Errorf("rate limit failed: %w", err)
66
- }
67
- return result == 1, nil
68
- }
69
-
70
- // Config 配置选项模式
71
- type Config struct {
72
- Capacity int64
73
- Rate int64
74
- Requested int64
75
- }
76
-
77
- type Option func(*Config)
78
-
79
- func WithCapacity(c int64) Option {
80
- return func(cfg *Config) { cfg.Capacity = c }
81
- }
82
-
83
- func WithRate(r int64) Option {
84
- return func(cfg *Config) { cfg.Rate = r }
85
- }
86
-
87
- func WithRequested(n int64) Option {
88
- return func(cfg *Config) { cfg.Requested = n }
89
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
new-api/common/limiter/lua/rate_limit.lua DELETED
@@ -1,44 +0,0 @@
1
- -- 令牌桶限流器
2
- -- KEYS[1]: 限流器唯一标识
3
- -- ARGV[1]: 请求令牌数 (通常为1)
4
- -- ARGV[2]: 令牌生成速率 (每秒)
5
- -- ARGV[3]: 桶容量
6
-
7
- local key = KEYS[1]
8
- local requested = tonumber(ARGV[1])
9
- local rate = tonumber(ARGV[2])
10
- local capacity = tonumber(ARGV[3])
11
-
12
- -- 获取当前时间(Redis服务器时间)
13
- local now = redis.call('TIME')
14
- local nowInSeconds = tonumber(now[1])
15
-
16
- -- 获取桶状态
17
- local bucket = redis.call('HMGET', key, 'tokens', 'last_time')
18
- local tokens = tonumber(bucket[1])
19
- local last_time = tonumber(bucket[2])
20
-
21
- -- 初始化桶(首次请求或过期)
22
- if not tokens or not last_time then
23
- tokens = capacity
24
- last_time = nowInSeconds
25
- else
26
- -- 计算新增令牌
27
- local elapsed = nowInSeconds - last_time
28
- local add_tokens = elapsed * rate
29
- tokens = math.min(capacity, tokens + add_tokens)
30
- last_time = nowInSeconds
31
- end
32
-
33
- -- 判断是否允许请求
34
- local allowed = false
35
- if tokens >= requested then
36
- tokens = tokens - requested
37
- allowed = true
38
- end
39
-
40
- ---- 更新桶状态并设置过期时间
41
- redis.call('HMSET', key, 'tokens', tokens, 'last_time', last_time)
42
- --redis.call('EXPIRE', key, math.ceil(capacity / rate) + 60) -- 适当延长过期时间
43
-
44
- return allowed and 1 or 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
new-api/common/model.go DELETED
@@ -1,42 +0,0 @@
1
- package common
2
-
3
- import "strings"
4
-
5
- var (
6
- // OpenAIResponseOnlyModels is a list of models that are only available for OpenAI responses.
7
- OpenAIResponseOnlyModels = []string{
8
- "o3-pro",
9
- "o3-deep-research",
10
- "o4-mini-deep-research",
11
- }
12
- ImageGenerationModels = []string{
13
- "dall-e-3",
14
- "dall-e-2",
15
- "gpt-image-1",
16
- "prefix:imagen-",
17
- "flux-",
18
- "flux.1-",
19
- }
20
- )
21
-
22
- func IsOpenAIResponseOnlyModel(modelName string) bool {
23
- for _, m := range OpenAIResponseOnlyModels {
24
- if strings.Contains(modelName, m) {
25
- return true
26
- }
27
- }
28
- return false
29
- }
30
-
31
- func IsImageGenerationModel(modelName string) bool {
32
- modelName = strings.ToLower(modelName)
33
- for _, m := range ImageGenerationModels {
34
- if strings.Contains(modelName, m) {
35
- return true
36
- }
37
- if strings.HasPrefix(m, "prefix:") && strings.HasPrefix(modelName, strings.TrimPrefix(m, "prefix:")) {
38
- return true
39
- }
40
- }
41
- return false
42
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
new-api/common/page_info.go DELETED
@@ -1,82 +0,0 @@
1
- package common
2
-
3
- import (
4
- "strconv"
5
-
6
- "github.com/gin-gonic/gin"
7
- )
8
-
9
- type PageInfo struct {
10
- Page int `json:"page"` // page num 页码
11
- PageSize int `json:"page_size"` // page size 页大小
12
-
13
- Total int `json:"total"` // 总条数,后设置
14
- Items any `json:"items"` // 数据,后设置
15
- }
16
-
17
- func (p *PageInfo) GetStartIdx() int {
18
- return (p.Page - 1) * p.PageSize
19
- }
20
-
21
- func (p *PageInfo) GetEndIdx() int {
22
- return p.Page * p.PageSize
23
- }
24
-
25
- func (p *PageInfo) GetPageSize() int {
26
- return p.PageSize
27
- }
28
-
29
- func (p *PageInfo) GetPage() int {
30
- return p.Page
31
- }
32
-
33
- func (p *PageInfo) SetTotal(total int) {
34
- p.Total = total
35
- }
36
-
37
- func (p *PageInfo) SetItems(items any) {
38
- p.Items = items
39
- }
40
-
41
- func GetPageQuery(c *gin.Context) *PageInfo {
42
- pageInfo := &PageInfo{}
43
- // 手动获取并处理每个参数
44
- if page, err := strconv.Atoi(c.Query("p")); err == nil {
45
- pageInfo.Page = page
46
- }
47
- if pageSize, err := strconv.Atoi(c.Query("page_size")); err == nil {
48
- pageInfo.PageSize = pageSize
49
- }
50
- if pageInfo.Page < 1 {
51
- // 兼容
52
- page, _ := strconv.Atoi(c.Query("p"))
53
- if page != 0 {
54
- pageInfo.Page = page
55
- } else {
56
- pageInfo.Page = 1
57
- }
58
- }
59
-
60
- if pageInfo.PageSize == 0 {
61
- // 兼容
62
- pageSize, _ := strconv.Atoi(c.Query("ps"))
63
- if pageSize != 0 {
64
- pageInfo.PageSize = pageSize
65
- }
66
- if pageInfo.PageSize == 0 {
67
- pageSize, _ = strconv.Atoi(c.Query("size")) // token page
68
- if pageSize != 0 {
69
- pageInfo.PageSize = pageSize
70
- }
71
- }
72
- if pageInfo.PageSize == 0 {
73
- pageInfo.PageSize = ItemsPerPage
74
- }
75
- }
76
-
77
- if pageInfo.PageSize > 100 {
78
- pageInfo.PageSize = 100
79
- }
80
-
81
- return pageInfo
82
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
new-api/common/pprof.go DELETED
@@ -1,44 +0,0 @@
1
- package common
2
-
3
- import (
4
- "fmt"
5
- "github.com/shirou/gopsutil/cpu"
6
- "os"
7
- "runtime/pprof"
8
- "time"
9
- )
10
-
11
- // Monitor 定时监控cpu使用率,超过阈值输出pprof文件
12
- func Monitor() {
13
- for {
14
- percent, err := cpu.Percent(time.Second, false)
15
- if err != nil {
16
- panic(err)
17
- }
18
- if percent[0] > 80 {
19
- fmt.Println("cpu usage too high")
20
- // write pprof file
21
- if _, err := os.Stat("./pprof"); os.IsNotExist(err) {
22
- err := os.Mkdir("./pprof", os.ModePerm)
23
- if err != nil {
24
- SysLog("创建pprof文件夹失败 " + err.Error())
25
- continue
26
- }
27
- }
28
- f, err := os.Create("./pprof/" + fmt.Sprintf("cpu-%s.pprof", time.Now().Format("20060102150405")))
29
- if err != nil {
30
- SysLog("创建pprof文件失败 " + err.Error())
31
- continue
32
- }
33
- err = pprof.StartCPUProfile(f)
34
- if err != nil {
35
- SysLog("启动pprof失败 " + err.Error())
36
- continue
37
- }
38
- time.Sleep(10 * time.Second) // profile for 30 seconds
39
- pprof.StopCPUProfile()
40
- f.Close()
41
- }
42
- time.Sleep(30 * time.Second)
43
- }
44
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
new-api/common/quota.go DELETED
@@ -1,5 +0,0 @@
1
- package common
2
-
3
- func GetTrustQuota() int {
4
- return int(10 * QuotaPerUnit)
5
- }
 
 
 
 
 
 
new-api/common/rate-limit.go DELETED
@@ -1,70 +0,0 @@
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
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
new-api/common/redis.go DELETED
@@ -1,327 +0,0 @@
1
- package common
2
-
3
- import (
4
- "context"
5
- "errors"
6
- "fmt"
7
- "os"
8
- "reflect"
9
- "strconv"
10
- "time"
11
-
12
- "github.com/go-redis/redis/v8"
13
- "gorm.io/gorm"
14
- )
15
-
16
- var RDB *redis.Client
17
- var RedisEnabled = true
18
-
19
- func RedisKeyCacheSeconds() int {
20
- return SyncFrequency
21
- }
22
-
23
- // InitRedisClient This function is called after init()
24
- func InitRedisClient() (err error) {
25
- if os.Getenv("REDIS_CONN_STRING") == "" {
26
- RedisEnabled = false
27
- SysLog("REDIS_CONN_STRING not set, Redis is not enabled")
28
- return nil
29
- }
30
- if os.Getenv("SYNC_FREQUENCY") == "" {
31
- SysLog("SYNC_FREQUENCY not set, use default value 60")
32
- SyncFrequency = 60
33
- }
34
- SysLog("Redis is enabled")
35
- opt, err := redis.ParseURL(os.Getenv("REDIS_CONN_STRING"))
36
- if err != nil {
37
- FatalLog("failed to parse Redis connection string: " + err.Error())
38
- }
39
- opt.PoolSize = GetEnvOrDefault("REDIS_POOL_SIZE", 10)
40
- RDB = redis.NewClient(opt)
41
-
42
- ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
43
- defer cancel()
44
-
45
- _, err = RDB.Ping(ctx).Result()
46
- if err != nil {
47
- FatalLog("Redis ping test failed: " + err.Error())
48
- }
49
- if DebugEnabled {
50
- SysLog(fmt.Sprintf("Redis connected to %s", opt.Addr))
51
- SysLog(fmt.Sprintf("Redis database: %d", opt.DB))
52
- }
53
- return err
54
- }
55
-
56
- func ParseRedisOption() *redis.Options {
57
- opt, err := redis.ParseURL(os.Getenv("REDIS_CONN_STRING"))
58
- if err != nil {
59
- FatalLog("failed to parse Redis connection string: " + err.Error())
60
- }
61
- return opt
62
- }
63
-
64
- func RedisSet(key string, value string, expiration time.Duration) error {
65
- if DebugEnabled {
66
- SysLog(fmt.Sprintf("Redis SET: key=%s, value=%s, expiration=%v", key, value, expiration))
67
- }
68
- ctx := context.Background()
69
- return RDB.Set(ctx, key, value, expiration).Err()
70
- }
71
-
72
- func RedisGet(key string) (string, error) {
73
- if DebugEnabled {
74
- SysLog(fmt.Sprintf("Redis GET: key=%s", key))
75
- }
76
- ctx := context.Background()
77
- val, err := RDB.Get(ctx, key).Result()
78
- return val, err
79
- }
80
-
81
- //func RedisExpire(key string, expiration time.Duration) error {
82
- // ctx := context.Background()
83
- // return RDB.Expire(ctx, key, expiration).Err()
84
- //}
85
- //
86
- //func RedisGetEx(key string, expiration time.Duration) (string, error) {
87
- // ctx := context.Background()
88
- // return RDB.GetSet(ctx, key, expiration).Result()
89
- //}
90
-
91
- func RedisDel(key string) error {
92
- if DebugEnabled {
93
- SysLog(fmt.Sprintf("Redis DEL: key=%s", key))
94
- }
95
- ctx := context.Background()
96
- return RDB.Del(ctx, key).Err()
97
- }
98
-
99
- func RedisDelKey(key string) error {
100
- if DebugEnabled {
101
- SysLog(fmt.Sprintf("Redis DEL Key: key=%s", key))
102
- }
103
- ctx := context.Background()
104
- return RDB.Del(ctx, key).Err()
105
- }
106
-
107
- func RedisHSetObj(key string, obj interface{}, expiration time.Duration) error {
108
- if DebugEnabled {
109
- SysLog(fmt.Sprintf("Redis HSET: key=%s, obj=%+v, expiration=%v", key, obj, expiration))
110
- }
111
- ctx := context.Background()
112
-
113
- data := make(map[string]interface{})
114
-
115
- // 使用反射遍历结构体字段
116
- v := reflect.ValueOf(obj).Elem()
117
- t := v.Type()
118
- for i := 0; i < v.NumField(); i++ {
119
- field := t.Field(i)
120
- value := v.Field(i)
121
-
122
- // Skip DeletedAt field
123
- if field.Type.String() == "gorm.DeletedAt" {
124
- continue
125
- }
126
-
127
- // 处理指针类型
128
- if value.Kind() == reflect.Ptr {
129
- if value.IsNil() {
130
- data[field.Name] = ""
131
- continue
132
- }
133
- value = value.Elem()
134
- }
135
-
136
- // 处理布尔类型
137
- if value.Kind() == reflect.Bool {
138
- data[field.Name] = strconv.FormatBool(value.Bool())
139
- continue
140
- }
141
-
142
- // 其他类型直接转换为字符串
143
- data[field.Name] = fmt.Sprintf("%v", value.Interface())
144
- }
145
-
146
- txn := RDB.TxPipeline()
147
- txn.HSet(ctx, key, data)
148
-
149
- // 只有在 expiration 大于 0 时才设置过期时间
150
- if expiration > 0 {
151
- txn.Expire(ctx, key, expiration)
152
- }
153
-
154
- _, err := txn.Exec(ctx)
155
- if err != nil {
156
- return fmt.Errorf("failed to execute transaction: %w", err)
157
- }
158
- return nil
159
- }
160
-
161
- func RedisHGetObj(key string, obj interface{}) error {
162
- if DebugEnabled {
163
- SysLog(fmt.Sprintf("Redis HGETALL: key=%s", key))
164
- }
165
- ctx := context.Background()
166
-
167
- result, err := RDB.HGetAll(ctx, key).Result()
168
- if err != nil {
169
- return fmt.Errorf("failed to load hash from Redis: %w", err)
170
- }
171
-
172
- if len(result) == 0 {
173
- return fmt.Errorf("key %s not found in Redis", key)
174
- }
175
-
176
- // Handle both pointer and non-pointer values
177
- val := reflect.ValueOf(obj)
178
- if val.Kind() != reflect.Ptr {
179
- return fmt.Errorf("obj must be a pointer to a struct, got %T", obj)
180
- }
181
-
182
- v := val.Elem()
183
- if v.Kind() != reflect.Struct {
184
- return fmt.Errorf("obj must be a pointer to a struct, got pointer to %T", v.Interface())
185
- }
186
-
187
- t := v.Type()
188
- for i := 0; i < v.NumField(); i++ {
189
- field := t.Field(i)
190
- fieldName := field.Name
191
- if value, ok := result[fieldName]; ok {
192
- fieldValue := v.Field(i)
193
-
194
- // Handle pointer types
195
- if fieldValue.Kind() == reflect.Ptr {
196
- if value == "" {
197
- continue
198
- }
199
- if fieldValue.IsNil() {
200
- fieldValue.Set(reflect.New(fieldValue.Type().Elem()))
201
- }
202
- fieldValue = fieldValue.Elem()
203
- }
204
-
205
- // Enhanced type handling for Token struct
206
- switch fieldValue.Kind() {
207
- case reflect.String:
208
- fieldValue.SetString(value)
209
- case reflect.Int, reflect.Int64:
210
- intValue, err := strconv.ParseInt(value, 10, 64)
211
- if err != nil {
212
- return fmt.Errorf("failed to parse int field %s: %w", fieldName, err)
213
- }
214
- fieldValue.SetInt(intValue)
215
- case reflect.Bool:
216
- boolValue, err := strconv.ParseBool(value)
217
- if err != nil {
218
- return fmt.Errorf("failed to parse bool field %s: %w", fieldName, err)
219
- }
220
- fieldValue.SetBool(boolValue)
221
- case reflect.Struct:
222
- // Special handling for gorm.DeletedAt
223
- if fieldValue.Type().String() == "gorm.DeletedAt" {
224
- if value != "" {
225
- timeValue, err := time.Parse(time.RFC3339, value)
226
- if err != nil {
227
- return fmt.Errorf("failed to parse DeletedAt field %s: %w", fieldName, err)
228
- }
229
- fieldValue.Set(reflect.ValueOf(gorm.DeletedAt{Time: timeValue, Valid: true}))
230
- }
231
- }
232
- default:
233
- return fmt.Errorf("unsupported field type: %s for field %s", fieldValue.Kind(), fieldName)
234
- }
235
- }
236
- }
237
-
238
- return nil
239
- }
240
-
241
- // RedisIncr Add this function to handle atomic increments
242
- func RedisIncr(key string, delta int64) error {
243
- if DebugEnabled {
244
- SysLog(fmt.Sprintf("Redis INCR: key=%s, delta=%d", key, delta))
245
- }
246
- // 检查键的剩余生存时间
247
- ttlCmd := RDB.TTL(context.Background(), key)
248
- ttl, err := ttlCmd.Result()
249
- if err != nil && !errors.Is(err, redis.Nil) {
250
- return fmt.Errorf("failed to get TTL: %w", err)
251
- }
252
-
253
- // 只有在 key 存在且有 TTL 时才需要特殊处理
254
- if ttl > 0 {
255
- ctx := context.Background()
256
- // 开始一个Redis事务
257
- txn := RDB.TxPipeline()
258
-
259
- // 减少余额
260
- decrCmd := txn.IncrBy(ctx, key, delta)
261
- if err := decrCmd.Err(); err != nil {
262
- return err // 如果减少失败,则直接返回错误
263
- }
264
-
265
- // 重新设置过期时间,使用原来的过期时间
266
- txn.Expire(ctx, key, ttl)
267
-
268
- // 执行事务
269
- _, err = txn.Exec(ctx)
270
- return err
271
- }
272
- return nil
273
- }
274
-
275
- func RedisHIncrBy(key, field string, delta int64) error {
276
- if DebugEnabled {
277
- SysLog(fmt.Sprintf("Redis HINCRBY: key=%s, field=%s, delta=%d", key, field, delta))
278
- }
279
- ttlCmd := RDB.TTL(context.Background(), key)
280
- ttl, err := ttlCmd.Result()
281
- if err != nil && !errors.Is(err, redis.Nil) {
282
- return fmt.Errorf("failed to get TTL: %w", err)
283
- }
284
-
285
- if ttl > 0 {
286
- ctx := context.Background()
287
- txn := RDB.TxPipeline()
288
-
289
- incrCmd := txn.HIncrBy(ctx, key, field, delta)
290
- if err := incrCmd.Err(); err != nil {
291
- return err
292
- }
293
-
294
- txn.Expire(ctx, key, ttl)
295
-
296
- _, err = txn.Exec(ctx)
297
- return err
298
- }
299
- return nil
300
- }
301
-
302
- func RedisHSetField(key, field string, value interface{}) error {
303
- if DebugEnabled {
304
- SysLog(fmt.Sprintf("Redis HSET field: key=%s, field=%s, value=%v", key, field, value))
305
- }
306
- ttlCmd := RDB.TTL(context.Background(), key)
307
- ttl, err := ttlCmd.Result()
308
- if err != nil && !errors.Is(err, redis.Nil) {
309
- return fmt.Errorf("failed to get TTL: %w", err)
310
- }
311
-
312
- if ttl > 0 {
313
- ctx := context.Background()
314
- txn := RDB.TxPipeline()
315
-
316
- hsetCmd := txn.HSet(ctx, key, field, value)
317
- if err := hsetCmd.Err(); err != nil {
318
- return err
319
- }
320
-
321
- txn.Expire(ctx, key, ttl)
322
-
323
- _, err = txn.Exec(ctx)
324
- return err
325
- }
326
- return nil
327
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
new-api/common/ssrf_protection.go DELETED
@@ -1,327 +0,0 @@
1
- package common
2
-
3
- import (
4
- "fmt"
5
- "net"
6
- "net/url"
7
- "strconv"
8
- "strings"
9
- )
10
-
11
- // SSRFProtection SSRF防护配置
12
- type SSRFProtection struct {
13
- AllowPrivateIp bool
14
- DomainFilterMode bool // true: 白名单, false: 黑名单
15
- DomainList []string // domain format, e.g. example.com, *.example.com
16
- IpFilterMode bool // true: 白名单, false: 黑名单
17
- IpList []string // CIDR or single IP
18
- AllowedPorts []int // 允许的端口范围
19
- ApplyIPFilterForDomain bool // 对域名启用IP过滤
20
- }
21
-
22
- // DefaultSSRFProtection 默认SSRF防护配置
23
- var DefaultSSRFProtection = &SSRFProtection{
24
- AllowPrivateIp: false,
25
- DomainFilterMode: true,
26
- DomainList: []string{},
27
- IpFilterMode: true,
28
- IpList: []string{},
29
- AllowedPorts: []int{},
30
- }
31
-
32
- // isPrivateIP 检查IP是否为私有地址
33
- func isPrivateIP(ip net.IP) bool {
34
- if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() {
35
- return true
36
- }
37
-
38
- // 检查私有网段
39
- private := []net.IPNet{
40
- {IP: net.IPv4(10, 0, 0, 0), Mask: net.CIDRMask(8, 32)}, // 10.0.0.0/8
41
- {IP: net.IPv4(172, 16, 0, 0), Mask: net.CIDRMask(12, 32)}, // 172.16.0.0/12
42
- {IP: net.IPv4(192, 168, 0, 0), Mask: net.CIDRMask(16, 32)}, // 192.168.0.0/16
43
- {IP: net.IPv4(127, 0, 0, 0), Mask: net.CIDRMask(8, 32)}, // 127.0.0.0/8
44
- {IP: net.IPv4(169, 254, 0, 0), Mask: net.CIDRMask(16, 32)}, // 169.254.0.0/16 (链路本地)
45
- {IP: net.IPv4(224, 0, 0, 0), Mask: net.CIDRMask(4, 32)}, // 224.0.0.0/4 (组播)
46
- {IP: net.IPv4(240, 0, 0, 0), Mask: net.CIDRMask(4, 32)}, // 240.0.0.0/4 (保留)
47
- }
48
-
49
- for _, privateNet := range private {
50
- if privateNet.Contains(ip) {
51
- return true
52
- }
53
- }
54
-
55
- // 检查IPv6私有地址
56
- if ip.To4() == nil {
57
- // IPv6 loopback
58
- if ip.Equal(net.IPv6loopback) {
59
- return true
60
- }
61
- // IPv6 link-local
62
- if strings.HasPrefix(ip.String(), "fe80:") {
63
- return true
64
- }
65
- // IPv6 unique local
66
- if strings.HasPrefix(ip.String(), "fc") || strings.HasPrefix(ip.String(), "fd") {
67
- return true
68
- }
69
- }
70
-
71
- return false
72
- }
73
-
74
- // parsePortRanges 解析端口范围配置
75
- // 支持格式: "80", "443", "8000-9000"
76
- func parsePortRanges(portConfigs []string) ([]int, error) {
77
- var ports []int
78
-
79
- for _, config := range portConfigs {
80
- config = strings.TrimSpace(config)
81
- if config == "" {
82
- continue
83
- }
84
-
85
- if strings.Contains(config, "-") {
86
- // 处理端口范围 "8000-9000"
87
- parts := strings.Split(config, "-")
88
- if len(parts) != 2 {
89
- return nil, fmt.Errorf("invalid port range format: %s", config)
90
- }
91
-
92
- startPort, err := strconv.Atoi(strings.TrimSpace(parts[0]))
93
- if err != nil {
94
- return nil, fmt.Errorf("invalid start port in range %s: %v", config, err)
95
- }
96
-
97
- endPort, err := strconv.Atoi(strings.TrimSpace(parts[1]))
98
- if err != nil {
99
- return nil, fmt.Errorf("invalid end port in range %s: %v", config, err)
100
- }
101
-
102
- if startPort > endPort {
103
- return nil, fmt.Errorf("invalid port range %s: start port cannot be greater than end port", config)
104
- }
105
-
106
- if startPort < 1 || startPort > 65535 || endPort < 1 || endPort > 65535 {
107
- return nil, fmt.Errorf("port range %s contains invalid port numbers (must be 1-65535)", config)
108
- }
109
-
110
- // 添加范围内的所有端口
111
- for port := startPort; port <= endPort; port++ {
112
- ports = append(ports, port)
113
- }
114
- } else {
115
- // 处理单个端口 "80"
116
- port, err := strconv.Atoi(config)
117
- if err != nil {
118
- return nil, fmt.Errorf("invalid port number: %s", config)
119
- }
120
-
121
- if port < 1 || port > 65535 {
122
- return nil, fmt.Errorf("invalid port number %d (must be 1-65535)", port)
123
- }
124
-
125
- ports = append(ports, port)
126
- }
127
- }
128
-
129
- return ports, nil
130
- }
131
-
132
- // isAllowedPort 检查端口是否被允许
133
- func (p *SSRFProtection) isAllowedPort(port int) bool {
134
- if len(p.AllowedPorts) == 0 {
135
- return true // 如果没有配置端口限制,则允许所有端口
136
- }
137
-
138
- for _, allowedPort := range p.AllowedPorts {
139
- if port == allowedPort {
140
- return true
141
- }
142
- }
143
- return false
144
- }
145
-
146
- // isDomainWhitelisted 检查域名是否在白名单中
147
- func isDomainListed(domain string, list []string) bool {
148
- if len(list) == 0 {
149
- return false
150
- }
151
-
152
- domain = strings.ToLower(domain)
153
- for _, item := range list {
154
- item = strings.ToLower(strings.TrimSpace(item))
155
- if item == "" {
156
- continue
157
- }
158
- // 精确匹配
159
- if domain == item {
160
- return true
161
- }
162
- // 通配符匹配 (*.example.com)
163
- if strings.HasPrefix(item, "*.") {
164
- suffix := strings.TrimPrefix(item, "*.")
165
- if strings.HasSuffix(domain, "."+suffix) || domain == suffix {
166
- return true
167
- }
168
- }
169
- }
170
- return false
171
- }
172
-
173
- func (p *SSRFProtection) isDomainAllowed(domain string) bool {
174
- listed := isDomainListed(domain, p.DomainList)
175
- if p.DomainFilterMode { // 白名单
176
- return listed
177
- }
178
- // 黑名单
179
- return !listed
180
- }
181
-
182
- // isIPWhitelisted 检查IP是否在白名单中
183
-
184
- func isIPListed(ip net.IP, list []string) bool {
185
- if len(list) == 0 {
186
- return false
187
- }
188
-
189
- for _, whitelistCIDR := range list {
190
- _, network, err := net.ParseCIDR(whitelistCIDR)
191
- if err != nil {
192
- // 尝试作为单个IP处理
193
- if whitelistIP := net.ParseIP(whitelistCIDR); whitelistIP != nil {
194
- if ip.Equal(whitelistIP) {
195
- return true
196
- }
197
- }
198
- continue
199
- }
200
-
201
- if network.Contains(ip) {
202
- return true
203
- }
204
- }
205
- return false
206
- }
207
-
208
- // IsIPAccessAllowed 检查IP是否允许访问
209
- func (p *SSRFProtection) IsIPAccessAllowed(ip net.IP) bool {
210
- // 私有IP限制
211
- if isPrivateIP(ip) && !p.AllowPrivateIp {
212
- return false
213
- }
214
-
215
- listed := isIPListed(ip, p.IpList)
216
- if p.IpFilterMode { // 白名单
217
- return listed
218
- }
219
- // 黑名单
220
- return !listed
221
- }
222
-
223
- // ValidateURL 验证URL是否安全
224
- func (p *SSRFProtection) ValidateURL(urlStr string) error {
225
- // 解析URL
226
- u, err := url.Parse(urlStr)
227
- if err != nil {
228
- return fmt.Errorf("invalid URL format: %v", err)
229
- }
230
-
231
- // 只允许HTTP/HTTPS协议
232
- if u.Scheme != "http" && u.Scheme != "https" {
233
- return fmt.Errorf("unsupported protocol: %s (only http/https allowed)", u.Scheme)
234
- }
235
-
236
- // 解析主机和端口
237
- host, portStr, err := net.SplitHostPort(u.Host)
238
- if err != nil {
239
- // 没有端口,使用默认端口
240
- host = u.Hostname()
241
- if u.Scheme == "https" {
242
- portStr = "443"
243
- } else {
244
- portStr = "80"
245
- }
246
- }
247
-
248
- // 验证端口
249
- port, err := strconv.Atoi(portStr)
250
- if err != nil {
251
- return fmt.Errorf("invalid port: %s", portStr)
252
- }
253
-
254
- if !p.isAllowedPort(port) {
255
- return fmt.Errorf("port %d is not allowed", port)
256
- }
257
-
258
- // 如果 host 是 IP,则跳过域名检查
259
- if ip := net.ParseIP(host); ip != nil {
260
- if !p.IsIPAccessAllowed(ip) {
261
- if isPrivateIP(ip) {
262
- return fmt.Errorf("private IP address not allowed: %s", ip.String())
263
- }
264
- if p.IpFilterMode {
265
- return fmt.Errorf("ip not in whitelist: %s", ip.String())
266
- }
267
- return fmt.Errorf("ip in blacklist: %s", ip.String())
268
- }
269
- return nil
270
- }
271
-
272
- // 先进行域名过滤
273
- if !p.isDomainAllowed(host) {
274
- if p.DomainFilterMode {
275
- return fmt.Errorf("domain not in whitelist: %s", host)
276
- }
277
- return fmt.Errorf("domain in blacklist: %s", host)
278
- }
279
-
280
- // 若未启用对域名应用IP过滤,则到此通过
281
- if !p.ApplyIPFilterForDomain {
282
- return nil
283
- }
284
-
285
- // 解析域名对应IP并检查
286
- ips, err := net.LookupIP(host)
287
- if err != nil {
288
- return fmt.Errorf("DNS resolution failed for %s: %v", host, err)
289
- }
290
- for _, ip := range ips {
291
- if !p.IsIPAccessAllowed(ip) {
292
- if isPrivateIP(ip) && !p.AllowPrivateIp {
293
- return fmt.Errorf("private IP address not allowed: %s resolves to %s", host, ip.String())
294
- }
295
- if p.IpFilterMode {
296
- return fmt.Errorf("ip not in whitelist: %s resolves to %s", host, ip.String())
297
- }
298
- return fmt.Errorf("ip in blacklist: %s resolves to %s", host, ip.String())
299
- }
300
- }
301
- return nil
302
- }
303
-
304
- // ValidateURLWithFetchSetting 使用FetchSetting配置验证URL
305
- func ValidateURLWithFetchSetting(urlStr string, enableSSRFProtection, allowPrivateIp bool, domainFilterMode bool, ipFilterMode bool, domainList, ipList, allowedPorts []string, applyIPFilterForDomain bool) error {
306
- // 如果SSRF防护被禁用,直接返回成功
307
- if !enableSSRFProtection {
308
- return nil
309
- }
310
-
311
- // 解析端口范围配置
312
- allowedPortInts, err := parsePortRanges(allowedPorts)
313
- if err != nil {
314
- return fmt.Errorf("request reject - invalid port configuration: %v", err)
315
- }
316
-
317
- protection := &SSRFProtection{
318
- AllowPrivateIp: allowPrivateIp,
319
- DomainFilterMode: domainFilterMode,
320
- DomainList: domainList,
321
- IpFilterMode: ipFilterMode,
322
- IpList: ipList,
323
- AllowedPorts: allowedPortInts,
324
- ApplyIPFilterForDomain: applyIPFilterForDomain,
325
- }
326
- return protection.ValidateURL(urlStr)
327
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
new-api/common/str.go DELETED
@@ -1,237 +0,0 @@
1
- package common
2
-
3
- import (
4
- "encoding/base64"
5
- "encoding/json"
6
- "math/rand"
7
- "net/url"
8
- "regexp"
9
- "strconv"
10
- "strings"
11
- "unsafe"
12
- )
13
-
14
- func GetStringIfEmpty(str string, defaultValue string) string {
15
- if str == "" {
16
- return defaultValue
17
- }
18
- return str
19
- }
20
-
21
- func GetRandomString(length int) string {
22
- //rand.Seed(time.Now().UnixNano())
23
- key := make([]byte, length)
24
- for i := 0; i < length; i++ {
25
- key[i] = keyChars[rand.Intn(len(keyChars))]
26
- }
27
- return string(key)
28
- }
29
-
30
- func MapToJsonStr(m map[string]interface{}) string {
31
- bytes, err := json.Marshal(m)
32
- if err != nil {
33
- return ""
34
- }
35
- return string(bytes)
36
- }
37
-
38
- func StrToMap(str string) (map[string]interface{}, error) {
39
- m := make(map[string]interface{})
40
- err := Unmarshal([]byte(str), &m)
41
- if err != nil {
42
- return nil, err
43
- }
44
- return m, nil
45
- }
46
-
47
- func StrToJsonArray(str string) ([]interface{}, error) {
48
- var js []interface{}
49
- err := json.Unmarshal([]byte(str), &js)
50
- if err != nil {
51
- return nil, err
52
- }
53
- return js, nil
54
- }
55
-
56
- func IsJsonArray(str string) bool {
57
- var js []interface{}
58
- return json.Unmarshal([]byte(str), &js) == nil
59
- }
60
-
61
- func IsJsonObject(str string) bool {
62
- var js map[string]interface{}
63
- return json.Unmarshal([]byte(str), &js) == nil
64
- }
65
-
66
- func String2Int(str string) int {
67
- num, err := strconv.Atoi(str)
68
- if err != nil {
69
- return 0
70
- }
71
- return num
72
- }
73
-
74
- func StringsContains(strs []string, str string) bool {
75
- for _, s := range strs {
76
- if s == str {
77
- return true
78
- }
79
- }
80
- return false
81
- }
82
-
83
- // StringToByteSlice []byte only read, panic on append
84
- func StringToByteSlice(s string) []byte {
85
- tmp1 := (*[2]uintptr)(unsafe.Pointer(&s))
86
- tmp2 := [3]uintptr{tmp1[0], tmp1[1], tmp1[1]}
87
- return *(*[]byte)(unsafe.Pointer(&tmp2))
88
- }
89
-
90
- func EncodeBase64(str string) string {
91
- return base64.StdEncoding.EncodeToString([]byte(str))
92
- }
93
-
94
- func GetJsonString(data any) string {
95
- if data == nil {
96
- return ""
97
- }
98
- b, _ := json.Marshal(data)
99
- return string(b)
100
- }
101
-
102
- // MaskEmail masks a user email to prevent PII leakage in logs
103
- // Returns "***masked***" if email is empty, otherwise shows only the domain part
104
- func MaskEmail(email string) string {
105
- if email == "" {
106
- return "***masked***"
107
- }
108
-
109
- // Find the @ symbol
110
- atIndex := strings.Index(email, "@")
111
- if atIndex == -1 {
112
- // No @ symbol found, return masked
113
- return "***masked***"
114
- }
115
-
116
- // Return only the domain part with @ symbol
117
- return "***@" + email[atIndex+1:]
118
- }
119
-
120
- // maskHostTail returns the tail parts of a domain/host that should be preserved.
121
- // It keeps 2 parts for likely country-code TLDs (e.g., co.uk, com.cn), otherwise keeps only the TLD.
122
- func maskHostTail(parts []string) []string {
123
- if len(parts) < 2 {
124
- return parts
125
- }
126
- lastPart := parts[len(parts)-1]
127
- secondLastPart := parts[len(parts)-2]
128
- if len(lastPart) == 2 && len(secondLastPart) <= 3 {
129
- // Likely country code TLD like co.uk, com.cn
130
- return []string{secondLastPart, lastPart}
131
- }
132
- return []string{lastPart}
133
- }
134
-
135
- // maskHostForURL collapses subdomains and keeps only masked prefix + preserved tail.
136
- // Example: api.openai.com -> ***.com, sub.domain.co.uk -> ***.co.uk
137
- func maskHostForURL(host string) string {
138
- parts := strings.Split(host, ".")
139
- if len(parts) < 2 {
140
- return "***"
141
- }
142
- tail := maskHostTail(parts)
143
- return "***." + strings.Join(tail, ".")
144
- }
145
-
146
- // maskHostForPlainDomain masks a plain domain and reflects subdomain depth with multiple ***.
147
- // Example: openai.com -> ***.com, api.openai.com -> ***.***.com, sub.domain.co.uk -> ***.***.co.uk
148
- func maskHostForPlainDomain(domain string) string {
149
- parts := strings.Split(domain, ".")
150
- if len(parts) < 2 {
151
- return domain
152
- }
153
- tail := maskHostTail(parts)
154
- numStars := len(parts) - len(tail)
155
- if numStars < 1 {
156
- numStars = 1
157
- }
158
- stars := strings.TrimSuffix(strings.Repeat("***.", numStars), ".")
159
- return stars + "." + strings.Join(tail, ".")
160
- }
161
-
162
- // MaskSensitiveInfo masks sensitive information like URLs, IPs, and domain names in a string
163
- // Example:
164
- // http://example.com -> http://***.com
165
- // https://api.test.org/v1/users/123?key=secret -> https://***.org/***/***/?key=***
166
- // https://sub.domain.co.uk/path/to/resource -> https://***.co.uk/***/***
167
- // 192.168.1.1 -> ***.***.***.***
168
- // openai.com -> ***.com
169
- // www.openai.com -> ***.***.com
170
- // api.openai.com -> ***.***.com
171
- func MaskSensitiveInfo(str string) string {
172
- // Mask URLs
173
- urlPattern := regexp.MustCompile(`(http|https)://[^\s/$.?#].[^\s]*`)
174
- str = urlPattern.ReplaceAllStringFunc(str, func(urlStr string) string {
175
- u, err := url.Parse(urlStr)
176
- if err != nil {
177
- return urlStr
178
- }
179
-
180
- host := u.Host
181
- if host == "" {
182
- return urlStr
183
- }
184
-
185
- // Mask host with unified logic
186
- maskedHost := maskHostForURL(host)
187
-
188
- result := u.Scheme + "://" + maskedHost
189
-
190
- // Mask path
191
- if u.Path != "" && u.Path != "/" {
192
- pathParts := strings.Split(strings.Trim(u.Path, "/"), "/")
193
- maskedPathParts := make([]string, len(pathParts))
194
- for i := range pathParts {
195
- if pathParts[i] != "" {
196
- maskedPathParts[i] = "***"
197
- }
198
- }
199
- if len(maskedPathParts) > 0 {
200
- result += "/" + strings.Join(maskedPathParts, "/")
201
- }
202
- } else if u.Path == "/" {
203
- result += "/"
204
- }
205
-
206
- // Mask query parameters
207
- if u.RawQuery != "" {
208
- values, err := url.ParseQuery(u.RawQuery)
209
- if err != nil {
210
- // If can't parse query, just mask the whole query string
211
- result += "?***"
212
- } else {
213
- maskedParams := make([]string, 0, len(values))
214
- for key := range values {
215
- maskedParams = append(maskedParams, key+"=***")
216
- }
217
- if len(maskedParams) > 0 {
218
- result += "?" + strings.Join(maskedParams, "&")
219
- }
220
- }
221
- }
222
-
223
- return result
224
- })
225
-
226
- // Mask domain names without protocol (like openai.com, www.openai.com)
227
- domainPattern := regexp.MustCompile(`\b(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}\b`)
228
- str = domainPattern.ReplaceAllStringFunc(str, func(domain string) string {
229
- return maskHostForPlainDomain(domain)
230
- })
231
-
232
- // Mask IP addresses
233
- ipPattern := regexp.MustCompile(`\b(?:\d{1,3}\.){3}\d{1,3}\b`)
234
- str = ipPattern.ReplaceAllString(str, "***.***.***.***")
235
-
236
- return str
237
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
new-api/common/sys_log.go DELETED
@@ -1,55 +0,0 @@
1
- package common
2
-
3
- import (
4
- "fmt"
5
- "os"
6
- "time"
7
-
8
- "github.com/gin-gonic/gin"
9
- )
10
-
11
- func SysLog(s string) {
12
- t := time.Now()
13
- _, _ = fmt.Fprintf(gin.DefaultWriter, "[SYS] %v | %s \n", t.Format("2006/01/02 - 15:04:05"), s)
14
- }
15
-
16
- func SysError(s string) {
17
- t := time.Now()
18
- _, _ = fmt.Fprintf(gin.DefaultErrorWriter, "[SYS] %v | %s \n", t.Format("2006/01/02 - 15:04:05"), s)
19
- }
20
-
21
- func FatalLog(v ...any) {
22
- t := time.Now()
23
- _, _ = fmt.Fprintf(gin.DefaultErrorWriter, "[FATAL] %v | %v \n", t.Format("2006/01/02 - 15:04:05"), v)
24
- os.Exit(1)
25
- }
26
-
27
- func LogStartupSuccess(startTime time.Time, port string) {
28
-
29
- duration := time.Since(startTime)
30
- durationMs := duration.Milliseconds()
31
-
32
- // Get network IPs
33
- networkIps := GetNetworkIps()
34
-
35
- // Print blank line for spacing
36
- fmt.Fprintf(gin.DefaultWriter, "\n")
37
-
38
- // Print the main success message
39
- fmt.Fprintf(gin.DefaultWriter, " \033[32m%s %s\033[0m ready in %d ms\n", SystemName, Version, durationMs)
40
- fmt.Fprintf(gin.DefaultWriter, "\n")
41
-
42
- // Skip fancy startup message in container environments
43
- if !IsRunningInContainer() {
44
- // Print local URL
45
- fmt.Fprintf(gin.DefaultWriter, " ➜ \033[1mLocal:\033[0m http://localhost:%s/\n", port)
46
- }
47
-
48
- // Print network URLs
49
- for _, ip := range networkIps {
50
- fmt.Fprintf(gin.DefaultWriter, " ➜ \033[1mNetwork:\033[0m http://%s:%s/\n", ip, port)
51
- }
52
-
53
- // Print blank line for spacing
54
- fmt.Fprintf(gin.DefaultWriter, "\n")
55
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
new-api/common/topup-ratio.go DELETED
@@ -1,33 +0,0 @@
1
- package common
2
-
3
- import (
4
- "encoding/json"
5
- )
6
-
7
- var TopupGroupRatio = map[string]float64{
8
- "default": 1,
9
- "vip": 1,
10
- "svip": 1,
11
- }
12
-
13
- func TopupGroupRatio2JSONString() string {
14
- jsonBytes, err := json.Marshal(TopupGroupRatio)
15
- if err != nil {
16
- SysError("error marshalling model ratio: " + err.Error())
17
- }
18
- return string(jsonBytes)
19
- }
20
-
21
- func UpdateTopupGroupRatioByJSONString(jsonStr string) error {
22
- TopupGroupRatio = make(map[string]float64)
23
- return json.Unmarshal([]byte(jsonStr), &TopupGroupRatio)
24
- }
25
-
26
- func GetTopupGroupRatio(name string) float64 {
27
- ratio, ok := TopupGroupRatio[name]
28
- if !ok {
29
- SysError("topup group ratio not found: " + name)
30
- return 1
31
- }
32
- return ratio
33
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
new-api/common/totp.go DELETED
@@ -1,150 +0,0 @@
1
- package common
2
-
3
- import (
4
- "crypto/rand"
5
- "fmt"
6
- "os"
7
- "strconv"
8
- "strings"
9
-
10
- "github.com/pquerna/otp"
11
- "github.com/pquerna/otp/totp"
12
- )
13
-
14
- const (
15
- // 备用码配置
16
- BackupCodeLength = 8 // 备用码长度
17
- BackupCodeCount = 4 // 生成备用码数量
18
-
19
- // 限制配置
20
- MaxFailAttempts = 5 // 最大失败尝试次数
21
- LockoutDuration = 300 // 锁定时间(秒)
22
- )
23
-
24
- // GenerateTOTPSecret 生成TOTP密钥和配置
25
- func GenerateTOTPSecret(accountName string) (*otp.Key, error) {
26
- issuer := Get2FAIssuer()
27
- return totp.Generate(totp.GenerateOpts{
28
- Issuer: issuer,
29
- AccountName: accountName,
30
- Period: 30,
31
- Digits: otp.DigitsSix,
32
- Algorithm: otp.AlgorithmSHA1,
33
- })
34
- }
35
-
36
- // ValidateTOTPCode 验证TOTP验证码
37
- func ValidateTOTPCode(secret, code string) bool {
38
- // 清理验证码格式
39
- cleanCode := strings.ReplaceAll(code, " ", "")
40
- if len(cleanCode) != 6 {
41
- return false
42
- }
43
-
44
- // 验证验证码
45
- return totp.Validate(cleanCode, secret)
46
- }
47
-
48
- // GenerateBackupCodes 生成备用恢复码
49
- func GenerateBackupCodes() ([]string, error) {
50
- codes := make([]string, BackupCodeCount)
51
-
52
- for i := 0; i < BackupCodeCount; i++ {
53
- code, err := generateRandomBackupCode()
54
- if err != nil {
55
- return nil, err
56
- }
57
- codes[i] = code
58
- }
59
-
60
- return codes, nil
61
- }
62
-
63
- // generateRandomBackupCode 生成单个备用码
64
- func generateRandomBackupCode() (string, error) {
65
- const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
66
- code := make([]byte, BackupCodeLength)
67
-
68
- for i := range code {
69
- randomBytes := make([]byte, 1)
70
- _, err := rand.Read(randomBytes)
71
- if err != nil {
72
- return "", err
73
- }
74
- code[i] = charset[int(randomBytes[0])%len(charset)]
75
- }
76
-
77
- // 格式化为 XXXX-XXXX 格式
78
- return fmt.Sprintf("%s-%s", string(code[:4]), string(code[4:])), nil
79
- }
80
-
81
- // ValidateBackupCode 验证备用码格式
82
- func ValidateBackupCode(code string) bool {
83
- // 移除所有分隔符并转为大写
84
- cleanCode := strings.ToUpper(strings.ReplaceAll(code, "-", ""))
85
- if len(cleanCode) != BackupCodeLength {
86
- return false
87
- }
88
-
89
- // 检查字符是否合法
90
- for _, char := range cleanCode {
91
- if !((char >= 'A' && char <= 'Z') || (char >= '0' && char <= '9')) {
92
- return false
93
- }
94
- }
95
-
96
- return true
97
- }
98
-
99
- // NormalizeBackupCode 标准化备用码格式
100
- func NormalizeBackupCode(code string) string {
101
- cleanCode := strings.ToUpper(strings.ReplaceAll(code, "-", ""))
102
- if len(cleanCode) == BackupCodeLength {
103
- return fmt.Sprintf("%s-%s", cleanCode[:4], cleanCode[4:])
104
- }
105
- return code
106
- }
107
-
108
- // HashBackupCode 对备用码进行哈希
109
- func HashBackupCode(code string) (string, error) {
110
- normalizedCode := NormalizeBackupCode(code)
111
- return Password2Hash(normalizedCode)
112
- }
113
-
114
- // Get2FAIssuer 获取2FA发行者名称
115
- func Get2FAIssuer() string {
116
- return SystemName
117
- }
118
-
119
- // getEnvOrDefault 获取环境变量或默认值
120
- func getEnvOrDefault(key, defaultValue string) string {
121
- if value, exists := os.LookupEnv(key); exists {
122
- return value
123
- }
124
- return defaultValue
125
- }
126
-
127
- // ValidateNumericCode 验证数字验证码格式
128
- func ValidateNumericCode(code string) (string, error) {
129
- // 移除空格
130
- code = strings.ReplaceAll(code, " ", "")
131
-
132
- if len(code) != 6 {
133
- return "", fmt.Errorf("验证码必须是6位数字")
134
- }
135
-
136
- // 检查是否为纯数字
137
- if _, err := strconv.Atoi(code); err != nil {
138
- return "", fmt.Errorf("验证码只能包含数字")
139
- }
140
-
141
- return code, nil
142
- }
143
-
144
- // GenerateQRCodeData 生成二维码数据
145
- func GenerateQRCodeData(secret, username string) string {
146
- issuer := Get2FAIssuer()
147
- accountName := fmt.Sprintf("%s (%s)", username, issuer)
148
- return fmt.Sprintf("otpauth://totp/%s:%s?secret=%s&issuer=%s&digits=6&period=30",
149
- issuer, accountName, secret, issuer)
150
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
new-api/common/utils.go DELETED
@@ -1,384 +0,0 @@
1
- package common
2
-
3
- import (
4
- "bytes"
5
- "context"
6
- crand "crypto/rand"
7
- "encoding/base64"
8
- "encoding/json"
9
- "fmt"
10
- "html/template"
11
- "io"
12
- "log"
13
- "math/big"
14
- "math/rand"
15
- "net"
16
- "net/url"
17
- "os"
18
- "os/exec"
19
- "runtime"
20
- "strconv"
21
- "strings"
22
- "time"
23
-
24
- "github.com/google/uuid"
25
- "github.com/pkg/errors"
26
- )
27
-
28
- func OpenBrowser(url string) {
29
- var err error
30
-
31
- switch runtime.GOOS {
32
- case "linux":
33
- err = exec.Command("xdg-open", url).Start()
34
- case "windows":
35
- err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
36
- case "darwin":
37
- err = exec.Command("open", url).Start()
38
- }
39
- if err != nil {
40
- log.Println(err)
41
- }
42
- }
43
-
44
- func GetIp() (ip string) {
45
- ips, err := net.InterfaceAddrs()
46
- if err != nil {
47
- log.Println(err)
48
- return ip
49
- }
50
-
51
- for _, a := range ips {
52
- if ipNet, ok := a.(*net.IPNet); ok && !ipNet.IP.IsLoopback() {
53
- if ipNet.IP.To4() != nil {
54
- ip = ipNet.IP.String()
55
- if strings.HasPrefix(ip, "10") {
56
- return
57
- }
58
- if strings.HasPrefix(ip, "172") {
59
- return
60
- }
61
- if strings.HasPrefix(ip, "192.168") {
62
- return
63
- }
64
- ip = ""
65
- }
66
- }
67
- }
68
- return
69
- }
70
-
71
- func GetNetworkIps() []string {
72
- var networkIps []string
73
- ips, err := net.InterfaceAddrs()
74
- if err != nil {
75
- log.Println(err)
76
- return networkIps
77
- }
78
-
79
- for _, a := range ips {
80
- if ipNet, ok := a.(*net.IPNet); ok && !ipNet.IP.IsLoopback() {
81
- if ipNet.IP.To4() != nil {
82
- ip := ipNet.IP.String()
83
- // Include common private network ranges
84
- if strings.HasPrefix(ip, "10.") ||
85
- strings.HasPrefix(ip, "172.") ||
86
- strings.HasPrefix(ip, "192.168.") {
87
- networkIps = append(networkIps, ip)
88
- }
89
- }
90
- }
91
- }
92
- return networkIps
93
- }
94
-
95
- // IsRunningInContainer detects if the application is running inside a container
96
- func IsRunningInContainer() bool {
97
- // Method 1: Check for .dockerenv file (Docker containers)
98
- if _, err := os.Stat("/.dockerenv"); err == nil {
99
- return true
100
- }
101
-
102
- // Method 2: Check cgroup for container indicators
103
- if data, err := os.ReadFile("/proc/1/cgroup"); err == nil {
104
- content := string(data)
105
- if strings.Contains(content, "docker") ||
106
- strings.Contains(content, "containerd") ||
107
- strings.Contains(content, "kubepods") ||
108
- strings.Contains(content, "/lxc/") {
109
- return true
110
- }
111
- }
112
-
113
- // Method 3: Check environment variables commonly set by container runtimes
114
- containerEnvVars := []string{
115
- "KUBERNETES_SERVICE_HOST",
116
- "DOCKER_CONTAINER",
117
- "container",
118
- }
119
-
120
- for _, envVar := range containerEnvVars {
121
- if os.Getenv(envVar) != "" {
122
- return true
123
- }
124
- }
125
-
126
- // Method 4: Check if init process is not the traditional init
127
- if data, err := os.ReadFile("/proc/1/comm"); err == nil {
128
- comm := strings.TrimSpace(string(data))
129
- // In containers, process 1 is often not "init" or "systemd"
130
- if comm != "init" && comm != "systemd" {
131
- // Additional check: if it's a common container entrypoint
132
- if strings.Contains(comm, "docker") ||
133
- strings.Contains(comm, "containerd") ||
134
- strings.Contains(comm, "runc") {
135
- return true
136
- }
137
- }
138
- }
139
-
140
- return false
141
- }
142
-
143
- var sizeKB = 1024
144
- var sizeMB = sizeKB * 1024
145
- var sizeGB = sizeMB * 1024
146
-
147
- func Bytes2Size(num int64) string {
148
- numStr := ""
149
- unit := "B"
150
- if num/int64(sizeGB) > 1 {
151
- numStr = fmt.Sprintf("%.2f", float64(num)/float64(sizeGB))
152
- unit = "GB"
153
- } else if num/int64(sizeMB) > 1 {
154
- numStr = fmt.Sprintf("%d", int(float64(num)/float64(sizeMB)))
155
- unit = "MB"
156
- } else if num/int64(sizeKB) > 1 {
157
- numStr = fmt.Sprintf("%d", int(float64(num)/float64(sizeKB)))
158
- unit = "KB"
159
- } else {
160
- numStr = fmt.Sprintf("%d", num)
161
- }
162
- return numStr + " " + unit
163
- }
164
-
165
- func Seconds2Time(num int) (time string) {
166
- if num/31104000 > 0 {
167
- time += strconv.Itoa(num/31104000) + " 年 "
168
- num %= 31104000
169
- }
170
- if num/2592000 > 0 {
171
- time += strconv.Itoa(num/2592000) + " 个月 "
172
- num %= 2592000
173
- }
174
- if num/86400 > 0 {
175
- time += strconv.Itoa(num/86400) + " 天 "
176
- num %= 86400
177
- }
178
- if num/3600 > 0 {
179
- time += strconv.Itoa(num/3600) + " 小时 "
180
- num %= 3600
181
- }
182
- if num/60 > 0 {
183
- time += strconv.Itoa(num/60) + " 分钟 "
184
- num %= 60
185
- }
186
- time += strconv.Itoa(num) + " 秒"
187
- return
188
- }
189
-
190
- func Interface2String(inter interface{}) string {
191
- switch inter.(type) {
192
- case string:
193
- return inter.(string)
194
- case int:
195
- return fmt.Sprintf("%d", inter.(int))
196
- case float64:
197
- return fmt.Sprintf("%f", inter.(float64))
198
- case bool:
199
- if inter.(bool) {
200
- return "true"
201
- } else {
202
- return "false"
203
- }
204
- case nil:
205
- return ""
206
- }
207
- return fmt.Sprintf("%v", inter)
208
- }
209
-
210
- func UnescapeHTML(x string) interface{} {
211
- return template.HTML(x)
212
- }
213
-
214
- func IntMax(a int, b int) int {
215
- if a >= b {
216
- return a
217
- } else {
218
- return b
219
- }
220
- }
221
-
222
- func IsIP(s string) bool {
223
- ip := net.ParseIP(s)
224
- return ip != nil
225
- }
226
-
227
- func GetUUID() string {
228
- code := uuid.New().String()
229
- code = strings.Replace(code, "-", "", -1)
230
- return code
231
- }
232
-
233
- const keyChars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
234
-
235
- func init() {
236
- rand.New(rand.NewSource(time.Now().UnixNano()))
237
- }
238
-
239
- func GenerateRandomCharsKey(length int) (string, error) {
240
- b := make([]byte, length)
241
- maxI := big.NewInt(int64(len(keyChars)))
242
-
243
- for i := range b {
244
- n, err := crand.Int(crand.Reader, maxI)
245
- if err != nil {
246
- return "", err
247
- }
248
- b[i] = keyChars[n.Int64()]
249
- }
250
-
251
- return string(b), nil
252
- }
253
-
254
- func GenerateRandomKey(length int) (string, error) {
255
- bytes := make([]byte, length*3/4) // 对于48位的输出,这里应该是36
256
- if _, err := crand.Read(bytes); err != nil {
257
- return "", err
258
- }
259
- return base64.StdEncoding.EncodeToString(bytes), nil
260
- }
261
-
262
- func GenerateKey() (string, error) {
263
- //rand.Seed(time.Now().UnixNano())
264
- return GenerateRandomCharsKey(48)
265
- }
266
-
267
- func GetRandomInt(max int) int {
268
- //rand.Seed(time.Now().UnixNano())
269
- return rand.Intn(max)
270
- }
271
-
272
- func GetTimestamp() int64 {
273
- return time.Now().Unix()
274
- }
275
-
276
- func GetTimeString() string {
277
- now := time.Now()
278
- return fmt.Sprintf("%s%d", now.Format("20060102150405"), now.UnixNano()%1e9)
279
- }
280
-
281
- func Max(a int, b int) int {
282
- if a >= b {
283
- return a
284
- } else {
285
- return b
286
- }
287
- }
288
-
289
- func MessageWithRequestId(message string, id string) string {
290
- return fmt.Sprintf("%s (request id: %s)", message, id)
291
- }
292
-
293
- func RandomSleep() {
294
- // Sleep for 0-3000 ms
295
- time.Sleep(time.Duration(rand.Intn(3000)) * time.Millisecond)
296
- }
297
-
298
- func GetPointer[T any](v T) *T {
299
- return &v
300
- }
301
-
302
- func Any2Type[T any](data any) (T, error) {
303
- var zero T
304
- bytes, err := json.Marshal(data)
305
- if err != nil {
306
- return zero, err
307
- }
308
- var res T
309
- err = json.Unmarshal(bytes, &res)
310
- if err != nil {
311
- return zero, err
312
- }
313
- return res, nil
314
- }
315
-
316
- // SaveTmpFile saves data to a temporary file. The filename would be apppended with a random string.
317
- func SaveTmpFile(filename string, data io.Reader) (string, error) {
318
- f, err := os.CreateTemp(os.TempDir(), filename)
319
- if err != nil {
320
- return "", errors.Wrapf(err, "failed to create temporary file %s", filename)
321
- }
322
- defer f.Close()
323
-
324
- _, err = io.Copy(f, data)
325
- if err != nil {
326
- return "", errors.Wrapf(err, "failed to copy data to temporary file %s", filename)
327
- }
328
-
329
- return f.Name(), nil
330
- }
331
-
332
- // GetAudioDuration returns the duration of an audio file in seconds.
333
- func GetAudioDuration(ctx context.Context, filename string, ext string) (float64, error) {
334
- // ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 {{input}}
335
- c := exec.CommandContext(ctx, "ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", filename)
336
- output, err := c.Output()
337
- if err != nil {
338
- return 0, errors.Wrap(err, "failed to get audio duration")
339
- }
340
- durationStr := string(bytes.TrimSpace(output))
341
- if durationStr == "N/A" {
342
- // Create a temporary output file name
343
- tmpFp, err := os.CreateTemp("", "audio-*"+ext)
344
- if err != nil {
345
- return 0, errors.Wrap(err, "failed to create temporary file")
346
- }
347
- tmpName := tmpFp.Name()
348
- // Close immediately so ffmpeg can open the file on Windows.
349
- _ = tmpFp.Close()
350
- defer os.Remove(tmpName)
351
-
352
- // ffmpeg -y -i filename -vcodec copy -acodec copy <tmpName>
353
- ffmpegCmd := exec.CommandContext(ctx, "ffmpeg", "-y", "-i", filename, "-vcodec", "copy", "-acodec", "copy", tmpName)
354
- if err := ffmpegCmd.Run(); err != nil {
355
- return 0, errors.Wrap(err, "failed to run ffmpeg")
356
- }
357
-
358
- // Recalculate the duration of the new file
359
- c = exec.CommandContext(ctx, "ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", tmpName)
360
- output, err := c.Output()
361
- if err != nil {
362
- return 0, errors.Wrap(err, "failed to get audio duration after ffmpeg")
363
- }
364
- durationStr = string(bytes.TrimSpace(output))
365
- }
366
- return strconv.ParseFloat(durationStr, 64)
367
- }
368
-
369
- // BuildURL concatenates base and endpoint, returns the complete url string
370
- func BuildURL(base string, endpoint string) string {
371
- u, err := url.Parse(base)
372
- if err != nil {
373
- return base + endpoint
374
- }
375
- end := endpoint
376
- if end == "" {
377
- end = "/"
378
- }
379
- ref, err := url.Parse(end)
380
- if err != nil {
381
- return base + endpoint
382
- }
383
- return u.ResolveReference(ref).String()
384
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
new-api/common/validate.go DELETED
@@ -1,9 +0,0 @@
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
- }
 
 
 
 
 
 
 
 
 
 
new-api/common/verification.go DELETED
@@ -1,77 +0,0 @@
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
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
new-api/constant/README.md DELETED
@@ -1,26 +0,0 @@
1
- # constant 包 (`/constant`)
2
-
3
- 该目录仅用于放置全局可复用的**常量定义**,不包含任何业务逻辑或依赖关系。
4
-
5
- ## 当前文件
6
-
7
- | 文件 | 说明 |
8
- |----------------------|---------------------------------------------------------------------|
9
- | `azure.go` | 定义与 Azure 相关的全局常量,如 `AzureNoRemoveDotTime`(控制删除 `.` 的截止时间)。 |
10
- | `cache_key.go` | 缓存键格式字符串及 Token 相关字段常量,统一缓存命名规则。 |
11
- | `channel_setting.go` | Channel 级别的设置键,如 `proxy`、`force_format` 等。 |
12
- | `context_key.go` | 定义 `ContextKey` 类型以及在整个项目中使用的上下文键常量(请求时间、Token/Channel/User 相关信息等)。 |
13
- | `env.go` | 环境配置相关的全局变量,在启动阶段根据配置文件或环境变量注入。 |
14
- | `finish_reason.go` | OpenAI/GPT 请求返回的 `finish_reason` 字符串常量集合。 |
15
- | `midjourney.go` | Midjourney 相关错误码及动作(Action)常量与模型到动作的映射表。 |
16
- | `setup.go` | 标识项目是否已完成初始化安装 (`Setup` 布尔值)。 |
17
- | `task.go` | 各种任务(Task)平台、动作常量及模型与动作映射表,如 Suno、Midjourney 等。 |
18
- | `user_setting.go` | 用户设置相关键常量以及通知类型(Email/Webhook)等。 |
19
-
20
- ## 使用约定
21
-
22
- 1. `constant` 包**只能被其他包引用**(import),**禁止在此包中引用项目内的其他自定义包**。如确有需要,仅允许引用 **Go 标准库**。
23
- 2. 不允许在此目录内编写任何与业务流程、数据库操作、第三方服务调用等相关的逻辑代码。
24
- 3. 新增类型时,请保持命名语义清晰,并在本 README 的 **当前文件** 表格中补充说明,确保团队成员能够快速了解其用途。
25
-
26
- > ⚠️ 违反以上约定将导致包之间产生不必要的耦合,影响代码可维护性与可测试性。请在提交代码前自行检查。
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
new-api/constant/api_type.go DELETED
@@ -1,37 +0,0 @@
1
- package constant
2
-
3
- const (
4
- APITypeOpenAI = iota
5
- APITypeAnthropic
6
- APITypePaLM
7
- APITypeBaidu
8
- APITypeZhipu
9
- APITypeAli
10
- APITypeXunfei
11
- APITypeAIProxyLibrary
12
- APITypeTencent
13
- APITypeGemini
14
- APITypeZhipuV4
15
- APITypeOllama
16
- APITypePerplexity
17
- APITypeAws
18
- APITypeCohere
19
- APITypeDify
20
- APITypeJina
21
- APITypeCloudflare
22
- APITypeSiliconFlow
23
- APITypeVertexAi
24
- APITypeMistral
25
- APITypeDeepSeek
26
- APITypeMokaAI
27
- APITypeVolcEngine
28
- APITypeBaiduV2
29
- APITypeOpenRouter
30
- APITypeXinference
31
- APITypeXai
32
- APITypeCoze
33
- APITypeJimeng
34
- APITypeMoonshot
35
- APITypeSubmodel
36
- APITypeDummy // this one is only for count, do not add any channel after this
37
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
new-api/constant/azure.go DELETED
@@ -1,5 +0,0 @@
1
- package constant
2
-
3
- import "time"
4
-
5
- var AzureNoRemoveDotTime = time.Date(2025, time.May, 10, 0, 0, 0, 0, time.UTC).Unix()