mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-04-04 20:41:52 +00:00
Compare commits
123 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f082d72bb | ||
|
|
0fd0e5d309 | ||
|
|
d2297d2723 | ||
|
|
62ae46b552 | ||
|
|
0b1354ed51 | ||
|
|
132c71390c | ||
|
|
bb3deb7b93 | ||
|
|
f92d96e298 | ||
|
|
c86762b656 | ||
|
|
3409d7a6b6 | ||
|
|
bfba4866a5 | ||
|
|
4fc1fe318e | ||
|
|
b3576f24ef | ||
|
|
ed4d26fc9e | ||
|
|
ba56e2e8ca | ||
|
|
7c20e6d047 | ||
|
|
72d6898eb5 | ||
|
|
f2c9388139 | ||
|
|
aaf5cecefd | ||
|
|
fe2165ace6 | ||
|
|
3003d12a20 | ||
|
|
a8a2195ab1 | ||
|
|
d40e6ec25d | ||
|
|
8129aa76f9 | ||
|
|
fb8595da18 | ||
|
|
93cda60d44 | ||
|
|
2ec5eafbce | ||
|
|
be0c240e97 | ||
|
|
7180e6f114 | ||
|
|
61495a460a | ||
|
|
cf3287a10a | ||
|
|
f3f1817aea | ||
|
|
a4795737fe | ||
|
|
eec8f523ce | ||
|
|
58fac129d6 | ||
|
|
241c9389ef | ||
|
|
1d0ef89ce9 | ||
|
|
cce2990db6 | ||
|
|
a7e1d17c3e | ||
|
|
c4e256e69b | ||
|
|
87a5e40daf | ||
|
|
0c326556aa | ||
|
|
794f6a6e34 | ||
|
|
656e809202 | ||
|
|
53ab2aaee4 | ||
|
|
a02bc3342f | ||
|
|
f54d0cb3b0 | ||
|
|
a5c48c2772 | ||
|
|
cffaf0d636 | ||
|
|
865b98a454 | ||
|
|
5bdbf3a673 | ||
|
|
43a7b59b68 | ||
|
|
eac3463401 | ||
|
|
1fa478af20 | ||
|
|
3b58b4989d | ||
|
|
0b1ba2eeb9 | ||
|
|
35277f2b4a | ||
|
|
03256dbdad | ||
|
|
f9a7f6085e | ||
|
|
c289694fe8 | ||
|
|
c37319cb4c | ||
|
|
57f036747b | ||
|
|
158edab974 | ||
|
|
f3a7119745 | ||
|
|
3fa1d93b9c | ||
|
|
5e3ba45748 | ||
|
|
cab30014ad | ||
|
|
9a54b345c5 | ||
|
|
e3a0162206 | ||
|
|
2d865eb735 | ||
|
|
70e7860838 | ||
|
|
1f4fc2d590 | ||
|
|
c76021e9a1 | ||
|
|
173c9bc669 | ||
|
|
739c0cc334 | ||
|
|
4163713714 | ||
|
|
76acfdc9e2 | ||
|
|
fe6f3d7978 | ||
|
|
1f70904fc5 | ||
|
|
fd86de19b5 | ||
|
|
c8f437c13a | ||
|
|
e6c6bbdef9 | ||
|
|
aab3887d2c | ||
|
|
25ab1d400f | ||
|
|
6c995d4bd9 | ||
|
|
fb9f50ce79 | ||
|
|
e8444edce7 | ||
|
|
c1a5b0e1bb | ||
|
|
f2809917f8 | ||
|
|
a4c43bb83b | ||
|
|
6ca68651ff | ||
|
|
e2f6444b83 | ||
|
|
8e2b6d0aad | ||
|
|
41a7cee98e | ||
|
|
68b87736b6 | ||
|
|
b86aeb9150 | ||
|
|
cfdf6e48f1 | ||
|
|
68b7e0e96a | ||
|
|
84130cdf50 | ||
|
|
d54cab2ab9 | ||
|
|
7aa8b88c89 | ||
|
|
a485db5065 | ||
|
|
5f06feb9a1 | ||
|
|
221d7b5c99 | ||
|
|
3587f2c6e9 | ||
|
|
498590d9fd | ||
|
|
8504e07245 | ||
|
|
2eddb93432 | ||
|
|
369ecf365a | ||
|
|
36bf4b3926 | ||
|
|
2a6f3ad27f | ||
|
|
cd21aa1c56 | ||
|
|
16599a900b | ||
|
|
b0d655e2b9 | ||
|
|
e4ccaddf4a | ||
|
|
b139588aa1 | ||
|
|
ee17e307f0 | ||
|
|
574d7a0914 | ||
|
|
edce1f7046 | ||
|
|
bab718e9bc | ||
|
|
509a1a0bac | ||
|
|
0b2ae561bf | ||
|
|
016e071d50 |
6
.dockerignore
Normal file
6
.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
||||
.github
|
||||
.git
|
||||
*.md
|
||||
.vscode
|
||||
.gitignore
|
||||
Makefile
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -8,3 +8,5 @@ build
|
||||
logs
|
||||
web/dist
|
||||
.env
|
||||
one-api
|
||||
.DS_Store
|
||||
151
README.en.md
Normal file
151
README.en.md
Normal file
@@ -0,0 +1,151 @@
|
||||
<div align="center">
|
||||
|
||||

|
||||
|
||||
# New API
|
||||
|
||||
🍥 Next Generation LLM Gateway and AI Asset Management System
|
||||
|
||||
<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>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://raw.githubusercontent.com/Calcium-Ion/new-api/main/LICENSE">
|
||||
<img src="https://img.shields.io/github/license/Calcium-Ion/new-api?color=brightgreen" alt="license">
|
||||
</a>
|
||||
<a href="https://github.com/Calcium-Ion/new-api/releases/latest">
|
||||
<img src="https://img.shields.io/github/v/release/Calcium-Ion/new-api?color=brightgreen&include_prereleases" alt="release">
|
||||
</a>
|
||||
<a href="https://github.com/users/Calcium-Ion/packages/container/package/new-api">
|
||||
<img src="https://img.shields.io/badge/docker-ghcr.io-blue" alt="docker">
|
||||
</a>
|
||||
<a href="https://hub.docker.com/r/CalciumIon/new-api">
|
||||
<img src="https://img.shields.io/badge/docker-dockerHub-blue" alt="docker">
|
||||
</a>
|
||||
<a href="https://goreportcard.com/report/github.com/Calcium-Ion/new-api">
|
||||
<img src="https://goreportcard.com/badge/github.com/Calcium-Ion/new-api" alt="GoReportCard">
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
## 📝 Project Description
|
||||
|
||||
> [!NOTE]
|
||||
> This is an open-source project developed based on [One API](https://github.com/songquanpeng/one-api)
|
||||
|
||||
> [!IMPORTANT]
|
||||
> - Users must comply with OpenAI's [Terms of Use](https://openai.com/policies/terms-of-use) and relevant laws and regulations. Not to be used for illegal purposes.
|
||||
> - This project is for personal learning only. Stability is not guaranteed, and no technical support is provided.
|
||||
|
||||
## ✨ Key Features
|
||||
|
||||
1. 🎨 New UI interface (some interfaces pending update)
|
||||
2. 🌍 Multi-language support (work in progress)
|
||||
3. 🎨 Added [Midjourney-Proxy(Plus)](https://github.com/novicezk/midjourney-proxy) interface support, [Integration Guide](Midjourney.md)
|
||||
4. 💰 Online recharge support, configurable in system settings:
|
||||
- [x] EasyPay
|
||||
5. 🔍 Query usage quota by key:
|
||||
- Works with [neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool)
|
||||
6. 📑 Configurable items per page in pagination
|
||||
7. 🔄 Compatible with original One API database (one-api.db)
|
||||
8. 💵 Support per-request model pricing, configurable in System Settings - Operation Settings
|
||||
9. ⚖️ Support channel **weighted random** selection
|
||||
10. 📈 Data dashboard (console)
|
||||
11. 🔒 Configurable model access per token
|
||||
12. 🤖 Telegram authorization login support:
|
||||
1. System Settings - Configure Login Registration - Allow Telegram Login
|
||||
2. Send /setdomain command to [@Botfather](https://t.me/botfather)
|
||||
3. Select your bot, then enter http(s)://your-website/login
|
||||
4. Telegram Bot name is the bot username without @
|
||||
13. 🎵 Added [Suno API](https://github.com/Suno-API/Suno-API) interface support, [Integration Guide](Suno.md)
|
||||
14. 🔄 Support for Rerank models, compatible with Cohere and Jina, can integrate with Dify, [Integration Guide](Rerank.md)
|
||||
15. ⚡ **[OpenAI Realtime API](https://platform.openai.com/docs/guides/realtime/integration)** - Support for OpenAI's Realtime API, including Azure channels
|
||||
|
||||
## Model Support
|
||||
This version additionally supports:
|
||||
1. Third-party model **gps** (gpt-4-gizmo-*)
|
||||
2. [Midjourney-Proxy(Plus)](https://github.com/novicezk/midjourney-proxy) interface, [Integration Guide](Midjourney.md)
|
||||
3. Custom channels with full API URL support
|
||||
4. [Suno API](https://github.com/Suno-API/Suno-API) interface, [Integration Guide](Suno.md)
|
||||
5. Rerank models, supporting [Cohere](https://cohere.ai/) and [Jina](https://jina.ai/), [Integration Guide](Rerank.md)
|
||||
6. Dify
|
||||
|
||||
You can add custom models gpt-4-gizmo-* in channels. These are third-party models and cannot be called with official OpenAI keys.
|
||||
|
||||
## Additional Configurations Beyond One API
|
||||
- `GENERATE_DEFAULT_TOKEN`: Generate initial token for new users, default `false`
|
||||
- `STREAMING_TIMEOUT`: Set streaming response timeout, default 60 seconds
|
||||
- `DIFY_DEBUG`: Output workflow and node info to client for Dify channel, default `true`
|
||||
- `FORCE_STREAM_OPTION`: Override client stream_options parameter, default `true`
|
||||
- `GET_MEDIA_TOKEN`: Calculate image tokens, default `true`
|
||||
- `GET_MEDIA_TOKEN_NOT_STREAM`: Calculate image tokens in non-stream mode, default `true`
|
||||
- `UPDATE_TASK`: Update async tasks (Midjourney, Suno), default `true`
|
||||
- `GEMINI_MODEL_MAP`: Specify Gemini model versions (v1/v1beta), format: "model:version", comma-separated
|
||||
- `COHERE_SAFETY_SETTING`: Cohere model [safety settings](https://docs.cohere.com/docs/safety-modes#overview), options: `NONE`, `CONTEXTUAL`, `STRICT`, default `NONE`
|
||||
- `GEMINI_VISION_MAX_IMAGE_NUM`: Gemini model maximum image number, default `16`, set to `-1` to disable
|
||||
|
||||
## Deployment
|
||||
> [!TIP]
|
||||
> Latest Docker image: `calciumion/new-api:latest`
|
||||
> Default account: root, password: 123456
|
||||
> Update command:
|
||||
> ```
|
||||
> docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower -cR
|
||||
> ```
|
||||
|
||||
### Requirements
|
||||
- Local database (default): SQLite (Docker deployment must mount `/data` directory)
|
||||
- Remote database: MySQL >= 5.7.8, PgSQL >= 9.6
|
||||
|
||||
### Docker Deployment
|
||||
### Using Docker Compose (Recommended)
|
||||
```shell
|
||||
# Clone project
|
||||
git clone https://github.com/Calcium-Ion/new-api.git
|
||||
cd new-api
|
||||
# Edit docker-compose.yml as needed
|
||||
# Start
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### Direct Docker Image Usage
|
||||
```shell
|
||||
# SQLite deployment:
|
||||
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
|
||||
# MySQL deployment (add -e SQL_DSN="root:123456@tcp(localhost:3306)/oneapi"), modify database connection parameters as needed
|
||||
# Example:
|
||||
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
|
||||
```
|
||||
|
||||
## Channel Retry
|
||||
Channel retry is implemented, configurable in `Settings->Operation Settings->General Settings`. **Cache recommended**.
|
||||
First retry uses same priority, second retry uses next priority, and so on.
|
||||
|
||||
### Cache Configuration
|
||||
1. `REDIS_CONN_STRING`: Use Redis as cache
|
||||
+ Example: `REDIS_CONN_STRING=redis://default:redispw@localhost:49153`
|
||||
2. `MEMORY_CACHE_ENABLED`: Enable memory cache, default `false`
|
||||
+ Example: `MEMORY_CACHE_ENABLED=true`
|
||||
|
||||
### Why Some Errors Don't Retry
|
||||
Error codes 400, 504, 524 won't retry
|
||||
### To Enable Retry for 400
|
||||
In `Channel->Edit`, set `Status Code Override` to:
|
||||
```json
|
||||
{
|
||||
"400": "500"
|
||||
}
|
||||
```
|
||||
|
||||
## Integration Guides
|
||||
- [Midjourney Integration](Midjourney.md)
|
||||
- [Suno Integration](Suno.md)
|
||||
|
||||
## Related Projects
|
||||
- [One API](https://github.com/songquanpeng/one-api): Original project
|
||||
- [Midjourney-Proxy](https://github.com/novicezk/midjourney-proxy): Midjourney interface support
|
||||
- [chatnio](https://github.com/Deeptrain-Community/chatnio): Next-gen AI B/C solution
|
||||
- [neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool): Query usage quota by key
|
||||
|
||||
## 🌟 Star History
|
||||
|
||||
[](https://star-history.com/#Calcium-Ion/new-api&Date)
|
||||
95
README.md
95
README.md
@@ -1,59 +1,79 @@
|
||||
<p align="right">
|
||||
<strong>中文</strong> | <a href="./README.en.md">English</a>
|
||||
</p>
|
||||
<div align="center">
|
||||
|
||||

|
||||
|
||||
# New API
|
||||
|
||||
|
||||
🍥新一代大模型网关与AI资产管理系统
|
||||
|
||||
<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>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://raw.githubusercontent.com/Calcium-Ion/new-api/main/LICENSE">
|
||||
<img src="https://img.shields.io/github/license/Calcium-Ion/new-api?color=brightgreen" alt="license">
|
||||
</a>
|
||||
<a href="https://github.com/Calcium-Ion/new-api/releases/latest">
|
||||
<img src="https://img.shields.io/github/v/release/Calcium-Ion/new-api?color=brightgreen&include_prereleases" alt="release">
|
||||
</a>
|
||||
<a href="https://github.com/users/Calcium-Ion/packages/container/package/new-api">
|
||||
<img src="https://img.shields.io/badge/docker-ghcr.io-blue" alt="docker">
|
||||
</a>
|
||||
<a href="https://hub.docker.com/r/CalciumIon/new-api">
|
||||
<img src="https://img.shields.io/badge/docker-dockerHub-blue" alt="docker">
|
||||
</a>
|
||||
<a href="https://goreportcard.com/report/github.com/Calcium-Ion/new-api">
|
||||
<img src="https://goreportcard.com/badge/github.com/Calcium-Ion/new-api" alt="GoReportCard">
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
> [!NOTE]
|
||||
## 📝 项目说明
|
||||
|
||||
> [!NOTE]
|
||||
> 本项目为开源项目,在[One API](https://github.com/songquanpeng/one-api)的基础上进行二次开发
|
||||
|
||||
> [!IMPORTANT]
|
||||
> 使用者必须在遵循 OpenAI 的[使用条款](https://openai.com/policies/terms-of-use)以及**法律法规**的情况下使用,不得用于非法用途。
|
||||
> 本项目仅供个人学习使用,不保证稳定性,且不提供任何技术支持。
|
||||
> 根据[《生成式人工智能服务管理暂行办法》](http://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm)的要求,请勿对中国地区公众提供一切未经备案的生成式人工智能服务。
|
||||
> [!IMPORTANT]
|
||||
> - 使用者必须在遵循 OpenAI 的[使用条款](https://openai.com/policies/terms-of-use)以及**法律法规**的情况下使用,不得用于非法用途。
|
||||
> - 本项目仅供个人学习使用,不保证稳定性,且不提供任何技术支持。
|
||||
> - 根据[《生成式人工智能服务管理暂行办法》](http://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm)的要求,请勿对中国地区公众提供一切未经备案的生成式人工智能服务。
|
||||
|
||||
## 主要变更
|
||||
此分叉版本的主要变更如下:
|
||||
## ✨ 主要特性
|
||||
|
||||
1. 全新的UI界面(部分界面还待更新)
|
||||
2. 添加[Midjourney-Proxy(Plus)](https://github.com/novicezk/midjourney-proxy)接口的支持,[对接文档](Midjourney.md)
|
||||
3. 支持在线充值功能,可在系统设置中设置,当前支持的支付接口:
|
||||
+ [x] 易支付
|
||||
4. 支持用key查询使用额度:
|
||||
+ 配合项目[neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool)可实现用key查询使用
|
||||
5. 渠道显示已使用额度,支持指定组织访问
|
||||
6. 分页支持选择每页显示数量
|
||||
7. 兼容原版One API的数据库,可直接使用原版数据库(one-api.db)
|
||||
8. 支持模型按次数收费,可在 系统设置-运营设置 中设置
|
||||
9. 支持渠道**加权随机**
|
||||
10. 数据看板
|
||||
11. 可设置令牌能调用的模型
|
||||
12. 支持Telegram授权登录。
|
||||
1. 🎨 全新的UI界面(部分界面还待更新)
|
||||
2. 🌍 多语言支持(待完善)
|
||||
3. 🎨 添加[Midjourney-Proxy(Plus)](https://github.com/novicezk/midjourney-proxy)接口支持,[对接文档](Midjourney.md)
|
||||
4. 💰 支持在线充值功能,可在系统设置中设置:
|
||||
- [x] 易支付
|
||||
5. 🔍 支持用key查询使用额度:
|
||||
- 配合项目[neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool)可实现用key查询使用
|
||||
6. 📑 分页支持选择每页显示数量
|
||||
7. 🔄 兼容原版One API的数据库,可直接使用原版数据库(one-api.db)
|
||||
8. 💵 支持模型按次数收费,可在 系统设置-运营设置 中设置
|
||||
9. ⚖️ 支持渠道**加权随机**
|
||||
10. 📈 数据看板(控制台)
|
||||
11. 🔒 可设置令牌能调用的模型
|
||||
12. 🤖 支持Telegram授权登录:
|
||||
1. 系统设置-配置登录注册-允许通过Telegram登录
|
||||
2. 对[@Botfather](https://t.me/botfather)输入指令/setdomain
|
||||
3. 选择你的bot,然后输入http(s)://你的网站地址/login
|
||||
4. Telegram Bot 名称是bot username 去掉@后的字符串
|
||||
13. 添加 [Suno API](https://github.com/Suno-API/Suno-API)接口的支持,[对接文档](Suno.md)
|
||||
14. 支持Rerank模型,目前仅兼容Cohere和Jina,可接入Dify,[对接文档](Rerank.md)
|
||||
15. **[OpenAI Realtime API](https://platform.openai.com/docs/guides/realtime/integration)** - 支持OpenAI的Realtime API,支持Azure渠道。
|
||||
13. 🎵 添加 [Suno API](https://github.com/Suno-API/Suno-API)接口支持,[对接文档](Suno.md)
|
||||
14. 🔄 支持Rerank模型,目前兼容Cohere和Jina,可接入Dify,[对接文档](Rerank.md)
|
||||
15. ⚡ **[OpenAI Realtime API](https://platform.openai.com/docs/guides/realtime/integration)** - 支持OpenAI的Realtime API,支持Azure渠道
|
||||
16. 支持使用路由/chat2link 进入聊天界面
|
||||
|
||||
## 模型支持
|
||||
此版本额外支持以下模型:
|
||||
1. 第三方模型 **gps** (gpt-4-gizmo-*)
|
||||
2. 智谱glm-4v,glm-4v识图
|
||||
3. Anthropic Claude 3
|
||||
4. [Ollama](https://github.com/ollama/ollama?tab=readme-ov-file),添加渠道时,密钥可以随便填写,默认的请求地址是[http://localhost:11434](http://localhost:11434),如果需要修改请在渠道中修改
|
||||
5. [Midjourney-Proxy(Plus)](https://github.com/novicezk/midjourney-proxy)接口,[对接文档](Midjourney.md)
|
||||
6. [零一万物](https://platform.lingyiwanwu.com/)
|
||||
7. 自定义渠道,支持填入完整调用地址
|
||||
8. [Suno API](https://github.com/Suno-API/Suno-API) 接口,[对接文档](Suno.md)
|
||||
9. Rerank模型,目前支持[Cohere](https://cohere.ai/)和[Jina](https://jina.ai/),[对接文档](Rerank.md)
|
||||
10. Dify
|
||||
11. Vertex AI,目前兼容Claude,Gemini,Llama3.1
|
||||
2. [Midjourney-Proxy(Plus)](https://github.com/novicezk/midjourney-proxy)接口,[对接文档](Midjourney.md)
|
||||
3. 自定义渠道,支持填入完整调用地址
|
||||
4. [Suno API](https://github.com/Suno-API/Suno-API) 接口,[对接文档](Suno.md)
|
||||
5. Rerank模型,目前支持[Cohere](https://cohere.ai/)和[Jina](https://jina.ai/),[对接文档](Rerank.md)
|
||||
6. Dify
|
||||
|
||||
您可以在渠道中添加自定义模型gpt-4-gizmo-*,此模型并非OpenAI官方模型,而是第三方模型,使用官方key无法调用。
|
||||
|
||||
@@ -67,6 +87,7 @@
|
||||
- `UPDATE_TASK`:是否更新异步任务(Midjourney、Suno),默认为 `true`,关闭后将不会更新任务进度。
|
||||
- `GEMINI_MODEL_MAP`:Gemini模型指定版本(v1/v1beta),使用“模型:版本”指定,","分隔,例如:-e GEMINI_MODEL_MAP="gemini-1.5-pro-latest:v1beta,gemini-1.5-pro-001:v1beta",为空则使用默认配置(v1beta)
|
||||
- `COHERE_SAFETY_SETTING`:Cohere模型[安全设置](https://docs.cohere.com/docs/safety-modes#overview),可选值为 `NONE`, `CONTEXTUAL`,`STRICT`,默认为 `NONE`。
|
||||
- `GEMINI_VISION_MAX_IMAGE_NUM`:Gemini模型最大图片数量,默认为 `16`,设置为 `-1` 则不限制。
|
||||
## 部署
|
||||
> [!TIP]
|
||||
> 最新版Docker镜像:`calciumion/new-api:latest`
|
||||
@@ -151,6 +172,10 @@ docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:1234
|
||||
- [chatnio](https://github.com/Deeptrain-Community/chatnio):下一代 AI 一站式 B/C 端解决方案
|
||||
- [neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool):用key查询使用额度
|
||||
|
||||
## Star History
|
||||
其他基于New API的项目:
|
||||
- [new-api-horizon](https://github.com/Calcium-Ion/new-api-horizon):New API高性能优化版,并支持Claude格式
|
||||
- [VoAPI](https://github.com/VoAPI/VoAPI):基于New API的闭源项目
|
||||
|
||||
## 🌟 Star History
|
||||
|
||||
[](https://star-history.com/#Calcium-Ion/new-api&Date)
|
||||
|
||||
@@ -46,6 +46,8 @@ var defaultModelRatio = map[string]float64{
|
||||
"gpt-4o-2024-08-06": 1.25, // $2.5 / 1M tokens
|
||||
"gpt-4o-2024-11-20": 1.25, // $2.5 / 1M tokens
|
||||
"gpt-4o-realtime-preview": 2.5,
|
||||
"o1": 7.5,
|
||||
"o1-2024-12-17": 7.5,
|
||||
"o1-preview": 7.5,
|
||||
"o1-preview-2024-09-12": 7.5,
|
||||
"o1-mini": 1.5,
|
||||
@@ -354,7 +356,7 @@ func GetCompletionRatio(name string) float64 {
|
||||
}
|
||||
return 2
|
||||
}
|
||||
if strings.HasPrefix(name, "o1-") {
|
||||
if strings.HasPrefix(name, "o1") {
|
||||
return 4
|
||||
}
|
||||
if name == "chatgpt-4o-latest" {
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
crand "crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"github.com/google/uuid"
|
||||
"html/template"
|
||||
"log"
|
||||
"math/big"
|
||||
@@ -15,6 +14,8 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func OpenBrowser(url string) {
|
||||
|
||||
5
constant/channel_setting.go
Normal file
5
constant/channel_setting.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package constant
|
||||
|
||||
var (
|
||||
ForceFormat = "force_format" // ForceFormat 强制格式化为OpenAI格式
|
||||
)
|
||||
5
constant/context_key.go
Normal file
5
constant/context_key.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package constant
|
||||
|
||||
const (
|
||||
ContextKeyRequestStartTime = "request_start_time"
|
||||
)
|
||||
@@ -23,6 +23,8 @@ var GeminiModelMap = map[string]string{
|
||||
"gemini-1.0-pro": "v1",
|
||||
}
|
||||
|
||||
var GeminiVisionMaxImageNum = common.GetEnvOrDefault("GEMINI_VISION_MAX_IMAGE_NUM", 16)
|
||||
|
||||
func InitEnv() {
|
||||
modelVersionMapStr := strings.TrimSpace(os.Getenv("GEMINI_MODEL_MAP"))
|
||||
if modelVersionMapStr == "" {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package constant
|
||||
|
||||
var (
|
||||
FinishReasonStop = "stop"
|
||||
FinishReasonToolCalls = "tool_calls"
|
||||
FinishReasonStop = "stop"
|
||||
FinishReasonToolCalls = "tool_calls"
|
||||
FinishReasonLength = "length"
|
||||
FinishReasonFunctionCall = "function_call"
|
||||
FinishReasonContentFilter = "content_filter"
|
||||
)
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
package constant
|
||||
|
||||
var MjNotifyEnabled = false
|
||||
var MjAccountFilterEnabled = false
|
||||
var MjModeClearEnabled = false
|
||||
var MjForwardUrlEnabled = true
|
||||
var MjActionCheckSuccessEnabled = true
|
||||
|
||||
const (
|
||||
MjErrorUnknown = 5
|
||||
MjRequestError = 4
|
||||
|
||||
@@ -141,7 +141,8 @@ func testChannel(channel *model.Channel, testModel string) (err error, openAIErr
|
||||
milliseconds := tok.Sub(tik).Milliseconds()
|
||||
consumedTime := float64(milliseconds) / 1000.0
|
||||
other := service.GenerateTextOtherInfo(c, meta, modelRatio, 1, completionRatio, modelPrice)
|
||||
model.RecordConsumeLog(c, 1, channel.Id, usage.PromptTokens, usage.CompletionTokens, testModel, "模型测试", quota, "模型测试", 0, quota, int(consumedTime), false, other)
|
||||
model.RecordConsumeLog(c, 1, channel.Id, usage.PromptTokens, usage.CompletionTokens, testModel, "模型测试",
|
||||
quota, "模型测试", 0, quota, int(consumedTime), false, "default", other)
|
||||
common.SysLog(fmt.Sprintf("testing channel #%d, response: \n%s", channel.Id, string(respBody)))
|
||||
return nil, nil
|
||||
}
|
||||
@@ -151,8 +152,10 @@ func buildTestRequest(model string) *dto.GeneralOpenAIRequest {
|
||||
Model: "", // this will be set later
|
||||
Stream: false,
|
||||
}
|
||||
if strings.HasPrefix(model, "o1-") {
|
||||
testRequest.MaxCompletionTokens = 1
|
||||
if strings.HasPrefix(model, "o1") {
|
||||
testRequest.MaxCompletionTokens = 10
|
||||
} else if strings.HasPrefix(model, "gemini-2.0-flash-thinking") {
|
||||
testRequest.MaxTokens = 2
|
||||
} else {
|
||||
testRequest.MaxTokens = 1
|
||||
}
|
||||
|
||||
@@ -97,6 +97,7 @@ func FetchUpstreamModels(c *gin.Context) {
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
channel, err := model.GetChannelById(id, true)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
@@ -105,34 +106,35 @@ func FetchUpstreamModels(c *gin.Context) {
|
||||
})
|
||||
return
|
||||
}
|
||||
if channel.Type != common.ChannelTypeOpenAI {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "仅支持 OpenAI 类型渠道",
|
||||
})
|
||||
return
|
||||
|
||||
//if channel.Type != common.ChannelTypeOpenAI {
|
||||
// c.JSON(http.StatusOK, gin.H{
|
||||
// "success": false,
|
||||
// "message": "仅支持 OpenAI 类型渠道",
|
||||
// })
|
||||
// return
|
||||
//}
|
||||
baseURL := common.ChannelBaseURLs[channel.Type]
|
||||
if channel.GetBaseURL() != "" {
|
||||
baseURL = channel.GetBaseURL()
|
||||
}
|
||||
url := fmt.Sprintf("%s/v1/models", *channel.BaseURL)
|
||||
url := fmt.Sprintf("%s/v1/models", baseURL)
|
||||
body, err := GetResponseBody("GET", url, channel, GetAuthHeader(channel.Key))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
result := OpenAIModelsResponse{}
|
||||
err = json.Unmarshal(body, &result)
|
||||
if err != nil {
|
||||
|
||||
var result OpenAIModelsResponse
|
||||
if err = json.Unmarshal(body, &result); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
}
|
||||
if !result.Success {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "上游返回错误",
|
||||
"message": fmt.Sprintf("解析响应失败: %s", err.Error()),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var ids []string
|
||||
@@ -417,7 +419,8 @@ func EditTagChannels(c *gin.Context) {
|
||||
}
|
||||
|
||||
type ChannelBatch struct {
|
||||
Ids []int `json:"ids"`
|
||||
Ids []int `json:"ids"`
|
||||
Tag *string `json:"tag"`
|
||||
}
|
||||
|
||||
func DeleteChannelBatch(c *gin.Context) {
|
||||
@@ -492,3 +495,105 @@ func UpdateChannel(c *gin.Context) {
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func FetchModels(c *gin.Context) {
|
||||
var req struct {
|
||||
BaseURL string `json:"base_url"`
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
"message": "Invalid request",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
baseURL := req.BaseURL
|
||||
if baseURL == "" {
|
||||
baseURL = "https://api.openai.com"
|
||||
}
|
||||
|
||||
client := &http.Client{}
|
||||
url := fmt.Sprintf("%s/v1/models", baseURL)
|
||||
|
||||
request, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
request.Header.Set("Authorization", "Bearer "+req.Key)
|
||||
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
//check status code
|
||||
if response.StatusCode != http.StatusOK {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"success": false,
|
||||
"message": "Failed to fetch models",
|
||||
})
|
||||
return
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
var result struct {
|
||||
Data []struct {
|
||||
ID string `json:"id"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(response.Body).Decode(&result); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var models []string
|
||||
for _, model := range result.Data {
|
||||
models = append(models, model.ID)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"data": models,
|
||||
})
|
||||
}
|
||||
|
||||
func BatchSetChannelTag(c *gin.Context) {
|
||||
channelBatch := ChannelBatch{}
|
||||
err := c.ShouldBindJSON(&channelBatch)
|
||||
if err != nil || len(channelBatch.Ids) == 0 {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "参数错误",
|
||||
})
|
||||
return
|
||||
}
|
||||
err = model.BatchSetChannelTag(channelBatch.Ids, channelBatch.Tag)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": len(channelBatch.Ids),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -3,13 +3,13 @@ package controller
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
"one-api/model"
|
||||
"one-api/setting"
|
||||
)
|
||||
|
||||
func GetGroups(c *gin.Context) {
|
||||
groupNames := make([]string, 0)
|
||||
for groupName, _ := range common.GroupRatio {
|
||||
for groupName, _ := range setting.GetGroupRatioCopy() {
|
||||
groupNames = append(groupNames, groupName)
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
@@ -24,9 +24,9 @@ func GetUserGroups(c *gin.Context) {
|
||||
userGroup := ""
|
||||
userId := c.GetInt("id")
|
||||
userGroup, _ = model.CacheGetUserGroup(userId)
|
||||
for groupName, _ := range common.GroupRatio {
|
||||
for groupName, _ := range setting.GetGroupRatioCopy() {
|
||||
// UserUsableGroups contains the groups that the user can use
|
||||
userUsableGroups := common.GetUserUsableGroups(userGroup)
|
||||
userUsableGroups := setting.GetUserUsableGroups(userGroup)
|
||||
if _, ok := userUsableGroups[groupName]; ok {
|
||||
usableGroups[groupName] = userUsableGroups[groupName]
|
||||
}
|
||||
|
||||
@@ -25,7 +25,8 @@ func GetAllLogs(c *gin.Context) {
|
||||
tokenName := c.Query("token_name")
|
||||
modelName := c.Query("model_name")
|
||||
channel, _ := strconv.Atoi(c.Query("channel"))
|
||||
logs, total, err := model.GetAllLogs(logType, startTimestamp, endTimestamp, modelName, username, tokenName, (p-1)*pageSize, pageSize, channel)
|
||||
group := c.Query("group")
|
||||
logs, total, err := model.GetAllLogs(logType, startTimestamp, endTimestamp, modelName, username, tokenName, (p-1)*pageSize, pageSize, channel, group)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
@@ -63,7 +64,8 @@ func GetUserLogs(c *gin.Context) {
|
||||
endTimestamp, _ := strconv.ParseInt(c.Query("end_timestamp"), 10, 64)
|
||||
tokenName := c.Query("token_name")
|
||||
modelName := c.Query("model_name")
|
||||
logs, total, err := model.GetUserLogs(userId, logType, startTimestamp, endTimestamp, modelName, tokenName, (p-1)*pageSize, pageSize)
|
||||
group := c.Query("group")
|
||||
logs, total, err := model.GetUserLogs(userId, logType, startTimestamp, endTimestamp, modelName, tokenName, (p-1)*pageSize, pageSize, group)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
@@ -146,7 +148,8 @@ func GetLogsStat(c *gin.Context) {
|
||||
username := c.Query("username")
|
||||
modelName := c.Query("model_name")
|
||||
channel, _ := strconv.Atoi(c.Query("channel"))
|
||||
stat := model.SumUsedQuota(logType, startTimestamp, endTimestamp, modelName, username, tokenName, channel)
|
||||
group := c.Query("group")
|
||||
stat := model.SumUsedQuota(logType, startTimestamp, endTimestamp, modelName, username, tokenName, channel, group)
|
||||
//tokenNum := model.SumUsedToken(logType, startTimestamp, endTimestamp, modelName, username, "")
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
@@ -168,7 +171,8 @@ func GetLogsSelfStat(c *gin.Context) {
|
||||
tokenName := c.Query("token_name")
|
||||
modelName := c.Query("model_name")
|
||||
channel, _ := strconv.Atoi(c.Query("channel"))
|
||||
quotaNum := model.SumUsedQuota(logType, startTimestamp, endTimestamp, modelName, username, tokenName, channel)
|
||||
group := c.Query("group")
|
||||
quotaNum := model.SumUsedQuota(logType, startTimestamp, endTimestamp, modelName, username, tokenName, channel, group)
|
||||
//tokenNum := model.SumUsedToken(logType, startTimestamp, endTimestamp, modelName, username, tokenName)
|
||||
c.JSON(200, gin.H{
|
||||
"success": true,
|
||||
|
||||
@@ -10,10 +10,10 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
"one-api/constant"
|
||||
"one-api/dto"
|
||||
"one-api/model"
|
||||
"one-api/service"
|
||||
"one-api/setting"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
@@ -231,9 +231,9 @@ func GetAllMidjourney(c *gin.Context) {
|
||||
if logs == nil {
|
||||
logs = make([]*model.Midjourney, 0)
|
||||
}
|
||||
if constant.MjForwardUrlEnabled {
|
||||
if setting.MjForwardUrlEnabled {
|
||||
for i, midjourney := range logs {
|
||||
midjourney.ImageUrl = constant.ServerAddress + "/mj/image/" + midjourney.MjId
|
||||
midjourney.ImageUrl = setting.ServerAddress + "/mj/image/" + midjourney.MjId
|
||||
logs[i] = midjourney
|
||||
}
|
||||
}
|
||||
@@ -263,9 +263,9 @@ func GetUserMidjourney(c *gin.Context) {
|
||||
if logs == nil {
|
||||
logs = make([]*model.Midjourney, 0)
|
||||
}
|
||||
if constant.MjForwardUrlEnabled {
|
||||
if setting.MjForwardUrlEnabled {
|
||||
for i, midjourney := range logs {
|
||||
midjourney.ImageUrl = constant.ServerAddress + "/mj/image/" + midjourney.MjId
|
||||
midjourney.ImageUrl = setting.ServerAddress + "/mj/image/" + midjourney.MjId
|
||||
logs[i] = midjourney
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
"one-api/constant"
|
||||
"one-api/model"
|
||||
"one-api/setting"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -47,9 +47,9 @@ func GetStatus(c *gin.Context) {
|
||||
"footer_html": common.Footer,
|
||||
"wechat_qrcode": common.WeChatAccountQRCodeImageURL,
|
||||
"wechat_login": common.WeChatAuthEnabled,
|
||||
"server_address": constant.ServerAddress,
|
||||
"price": constant.Price,
|
||||
"min_topup": constant.MinTopUp,
|
||||
"server_address": setting.ServerAddress,
|
||||
"price": setting.Price,
|
||||
"min_topup": setting.MinTopUp,
|
||||
"turnstile_check": common.TurnstileCheckEnabled,
|
||||
"turnstile_site_key": common.TurnstileSiteKey,
|
||||
"top_up_link": common.TopUpLink,
|
||||
@@ -63,9 +63,9 @@ func GetStatus(c *gin.Context) {
|
||||
"enable_data_export": common.DataExportEnabled,
|
||||
"data_export_default_time": common.DataExportDefaultTime,
|
||||
"default_collapse_sidebar": common.DefaultCollapseSidebar,
|
||||
"enable_online_topup": constant.PayAddress != "" && constant.EpayId != "" && constant.EpayKey != "",
|
||||
"mj_notify_enabled": constant.MjNotifyEnabled,
|
||||
"chats": constant.Chats,
|
||||
"enable_online_topup": setting.PayAddress != "" && setting.EpayId != "" && setting.EpayKey != "",
|
||||
"mj_notify_enabled": setting.MjNotifyEnabled,
|
||||
"chats": setting.Chats,
|
||||
},
|
||||
})
|
||||
return
|
||||
@@ -207,7 +207,7 @@ func SendPasswordResetEmail(c *gin.Context) {
|
||||
}
|
||||
code := common.GenerateVerificationCode(0)
|
||||
common.RegisterVerificationCodeWithKey(email, code, common.PasswordResetPurpose)
|
||||
link := fmt.Sprintf("%s/user/reset?email=%s&token=%s", constant.ServerAddress, email, code)
|
||||
link := fmt.Sprintf("%s/user/reset?email=%s&token=%s", setting.ServerAddress, email, code)
|
||||
subject := fmt.Sprintf("%s密码重置", common.SystemName)
|
||||
content := fmt.Sprintf("<p>您好,你正在进行%s密码重置。</p>"+
|
||||
"<p>点击 <a href='%s'>此处</a> 进行密码重置。</p>"+
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
"one-api/model"
|
||||
"one-api/setting"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -83,7 +84,7 @@ func UpdateOption(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
case "GroupRatio":
|
||||
err = common.CheckGroupRatio(option.Value)
|
||||
err = setting.CheckGroupRatio(option.Value)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
|
||||
@@ -4,14 +4,38 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"one-api/common"
|
||||
"one-api/model"
|
||||
"one-api/setting"
|
||||
)
|
||||
|
||||
func GetPricing(c *gin.Context) {
|
||||
pricing := model.GetPricing()
|
||||
userId, exists := c.Get("id")
|
||||
usableGroup := map[string]string{}
|
||||
groupRatio := map[string]float64{}
|
||||
for s, f := range setting.GetGroupRatioCopy() {
|
||||
groupRatio[s] = f
|
||||
}
|
||||
var group string
|
||||
if exists {
|
||||
user, err := model.GetUserById(userId.(int), false)
|
||||
if err == nil {
|
||||
group = user.Group
|
||||
}
|
||||
}
|
||||
|
||||
usableGroup = setting.GetUserUsableGroups(group)
|
||||
// check groupRatio contains usableGroup
|
||||
for group := range setting.GetGroupRatioCopy() {
|
||||
if _, ok := usableGroup[group]; !ok {
|
||||
delete(groupRatio, group)
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"success": true,
|
||||
"data": pricing,
|
||||
"group_ratio": common.GroupRatio,
|
||||
"success": true,
|
||||
"data": pricing,
|
||||
"group_ratio": groupRatio,
|
||||
"usable_group": usableGroup,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"one-api/relay/constant"
|
||||
relayconstant "one-api/relay/constant"
|
||||
"one-api/service"
|
||||
"one-api/setting"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -83,7 +84,7 @@ func Playground(c *gin.Context) {
|
||||
if group == "" {
|
||||
group = userGroup
|
||||
} else {
|
||||
if !common.GroupInUserUsableGroups(group) && group != userGroup {
|
||||
if !setting.GroupInUserUsableGroups(group) && group != userGroup {
|
||||
openaiErr = service.OpenAIErrorWrapperLocal(errors.New("无权访问该分组"), "group_not_allowed", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -8,9 +8,9 @@ import (
|
||||
"log"
|
||||
"net/url"
|
||||
"one-api/common"
|
||||
"one-api/constant"
|
||||
"one-api/model"
|
||||
"one-api/service"
|
||||
"one-api/setting"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -28,13 +28,13 @@ type AmountRequest struct {
|
||||
}
|
||||
|
||||
func GetEpayClient() *epay.Client {
|
||||
if constant.PayAddress == "" || constant.EpayId == "" || constant.EpayKey == "" {
|
||||
if setting.PayAddress == "" || setting.EpayId == "" || setting.EpayKey == "" {
|
||||
return nil
|
||||
}
|
||||
withUrl, err := epay.NewClient(&epay.Config{
|
||||
PartnerID: constant.EpayId,
|
||||
Key: constant.EpayKey,
|
||||
}, constant.PayAddress)
|
||||
PartnerID: setting.EpayId,
|
||||
Key: setting.EpayKey,
|
||||
}, setting.PayAddress)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -50,12 +50,12 @@ func getPayMoney(amount float64, group string) float64 {
|
||||
if topupGroupRatio == 0 {
|
||||
topupGroupRatio = 1
|
||||
}
|
||||
payMoney := amount * constant.Price * topupGroupRatio
|
||||
payMoney := amount * setting.Price * topupGroupRatio
|
||||
return payMoney
|
||||
}
|
||||
|
||||
func getMinTopup() int {
|
||||
minTopup := constant.MinTopUp
|
||||
minTopup := setting.MinTopUp
|
||||
if !common.DisplayInCurrencyEnabled {
|
||||
minTopup = minTopup * int(common.QuotaPerUnit)
|
||||
}
|
||||
@@ -94,7 +94,7 @@ func RequestEpay(c *gin.Context) {
|
||||
payType = "wxpay"
|
||||
}
|
||||
callBackAddress := service.GetCallbackAddress()
|
||||
returnUrl, _ := url.Parse(constant.ServerAddress + "/log")
|
||||
returnUrl, _ := url.Parse(setting.ServerAddress + "/log")
|
||||
notifyUrl, _ := url.Parse(callBackAddress + "/api/user/epay/notify")
|
||||
tradeNo := fmt.Sprintf("%s%d", common.GetRandomString(6), time.Now().Unix())
|
||||
tradeNo = fmt.Sprintf("USR%dNO%s", id, tradeNo)
|
||||
|
||||
@@ -3,39 +3,48 @@ package dto
|
||||
import "encoding/json"
|
||||
|
||||
type ResponseFormat struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
JsonSchema *FormatJsonSchema `json:"json_schema,omitempty"`
|
||||
}
|
||||
|
||||
type FormatJsonSchema struct {
|
||||
Description string `json:"description,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Schema any `json:"schema,omitempty"`
|
||||
Strict any `json:"strict,omitempty"`
|
||||
}
|
||||
|
||||
type GeneralOpenAIRequest struct {
|
||||
Model string `json:"model,omitempty"`
|
||||
Messages []Message `json:"messages,omitempty"`
|
||||
Prompt any `json:"prompt,omitempty"`
|
||||
Stream bool `json:"stream,omitempty"`
|
||||
StreamOptions *StreamOptions `json:"stream_options,omitempty"`
|
||||
MaxTokens uint `json:"max_tokens,omitempty"`
|
||||
MaxCompletionTokens uint `json:"max_completion_tokens,omitempty"`
|
||||
Temperature float64 `json:"temperature,omitempty"`
|
||||
TopP float64 `json:"top_p,omitempty"`
|
||||
TopK int `json:"top_k,omitempty"`
|
||||
Stop any `json:"stop,omitempty"`
|
||||
N int `json:"n,omitempty"`
|
||||
Input any `json:"input,omitempty"`
|
||||
Instruction string `json:"instruction,omitempty"`
|
||||
Size string `json:"size,omitempty"`
|
||||
Functions any `json:"functions,omitempty"`
|
||||
FrequencyPenalty float64 `json:"frequency_penalty,omitempty"`
|
||||
PresencePenalty float64 `json:"presence_penalty,omitempty"`
|
||||
ResponseFormat any `json:"response_format,omitempty"`
|
||||
EncodingFormat any `json:"encoding_format,omitempty"`
|
||||
Seed float64 `json:"seed,omitempty"`
|
||||
Tools []ToolCall `json:"tools,omitempty"`
|
||||
ToolChoice any `json:"tool_choice,omitempty"`
|
||||
User string `json:"user,omitempty"`
|
||||
LogProbs bool `json:"logprobs,omitempty"`
|
||||
TopLogProbs int `json:"top_logprobs,omitempty"`
|
||||
Dimensions int `json:"dimensions,omitempty"`
|
||||
Modalities any `json:"modalities,omitempty"`
|
||||
Audio any `json:"audio,omitempty"`
|
||||
Model string `json:"model,omitempty"`
|
||||
Messages []Message `json:"messages,omitempty"`
|
||||
Prompt any `json:"prompt,omitempty"`
|
||||
Stream bool `json:"stream,omitempty"`
|
||||
StreamOptions *StreamOptions `json:"stream_options,omitempty"`
|
||||
MaxTokens uint `json:"max_tokens,omitempty"`
|
||||
MaxCompletionTokens uint `json:"max_completion_tokens,omitempty"`
|
||||
ReasoningEffort string `json:"reasoning_effort,omitempty"`
|
||||
Temperature float64 `json:"temperature,omitempty"`
|
||||
TopP float64 `json:"top_p,omitempty"`
|
||||
TopK int `json:"top_k,omitempty"`
|
||||
Stop any `json:"stop,omitempty"`
|
||||
N int `json:"n,omitempty"`
|
||||
Input any `json:"input,omitempty"`
|
||||
Instruction string `json:"instruction,omitempty"`
|
||||
Size string `json:"size,omitempty"`
|
||||
Functions any `json:"functions,omitempty"`
|
||||
FrequencyPenalty float64 `json:"frequency_penalty,omitempty"`
|
||||
PresencePenalty float64 `json:"presence_penalty,omitempty"`
|
||||
ResponseFormat *ResponseFormat `json:"response_format,omitempty"`
|
||||
EncodingFormat any `json:"encoding_format,omitempty"`
|
||||
Seed float64 `json:"seed,omitempty"`
|
||||
Tools []ToolCall `json:"tools,omitempty"`
|
||||
ToolChoice any `json:"tool_choice,omitempty"`
|
||||
User string `json:"user,omitempty"`
|
||||
LogProbs bool `json:"logprobs,omitempty"`
|
||||
TopLogProbs int `json:"top_logprobs,omitempty"`
|
||||
Dimensions int `json:"dimensions,omitempty"`
|
||||
Modalities any `json:"modalities,omitempty"`
|
||||
Audio any `json:"audio,omitempty"`
|
||||
}
|
||||
|
||||
type OpenAITools struct {
|
||||
@@ -80,11 +89,11 @@ type Message struct {
|
||||
Role string `json:"role"`
|
||||
Content json.RawMessage `json:"content"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
ToolCalls any `json:"tool_calls,omitempty"`
|
||||
ToolCalls json.RawMessage `json:"tool_calls,omitempty"`
|
||||
ToolCallId string `json:"tool_call_id,omitempty"`
|
||||
}
|
||||
|
||||
type MediaMessage struct {
|
||||
type MediaContent struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text"`
|
||||
ImageUrl any `json:"image_url,omitempty"`
|
||||
@@ -107,7 +116,23 @@ const (
|
||||
ContentTypeInputAudio = "input_audio"
|
||||
)
|
||||
|
||||
func (m Message) StringContent() string {
|
||||
func (m *Message) ParseToolCalls() []ToolCall {
|
||||
if m.ToolCalls == nil {
|
||||
return nil
|
||||
}
|
||||
var toolCalls []ToolCall
|
||||
if err := json.Unmarshal(m.ToolCalls, &toolCalls); err == nil {
|
||||
return toolCalls
|
||||
}
|
||||
return toolCalls
|
||||
}
|
||||
|
||||
func (m *Message) SetToolCalls(toolCalls any) {
|
||||
toolCallsJson, _ := json.Marshal(toolCalls)
|
||||
m.ToolCalls = toolCallsJson
|
||||
}
|
||||
|
||||
func (m *Message) StringContent() string {
|
||||
var stringContent string
|
||||
if err := json.Unmarshal(m.Content, &stringContent); err == nil {
|
||||
return stringContent
|
||||
@@ -120,7 +145,7 @@ func (m *Message) SetStringContent(content string) {
|
||||
m.Content = jsonContent
|
||||
}
|
||||
|
||||
func (m Message) IsStringContent() bool {
|
||||
func (m *Message) IsStringContent() bool {
|
||||
var stringContent string
|
||||
if err := json.Unmarshal(m.Content, &stringContent); err == nil {
|
||||
return true
|
||||
@@ -128,11 +153,11 @@ func (m Message) IsStringContent() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (m Message) ParseContent() []MediaMessage {
|
||||
var contentList []MediaMessage
|
||||
func (m *Message) ParseContent() []MediaContent {
|
||||
var contentList []MediaContent
|
||||
var stringContent string
|
||||
if err := json.Unmarshal(m.Content, &stringContent); err == nil {
|
||||
contentList = append(contentList, MediaMessage{
|
||||
contentList = append(contentList, MediaContent{
|
||||
Type: ContentTypeText,
|
||||
Text: stringContent,
|
||||
})
|
||||
@@ -148,7 +173,7 @@ func (m Message) ParseContent() []MediaMessage {
|
||||
switch contentMap["type"] {
|
||||
case ContentTypeText:
|
||||
if subStr, ok := contentMap["text"].(string); ok {
|
||||
contentList = append(contentList, MediaMessage{
|
||||
contentList = append(contentList, MediaContent{
|
||||
Type: ContentTypeText,
|
||||
Text: subStr,
|
||||
})
|
||||
@@ -161,7 +186,7 @@ func (m Message) ParseContent() []MediaMessage {
|
||||
} else {
|
||||
subObj["detail"] = "high"
|
||||
}
|
||||
contentList = append(contentList, MediaMessage{
|
||||
contentList = append(contentList, MediaContent{
|
||||
Type: ContentTypeImageURL,
|
||||
ImageUrl: MessageImageUrl{
|
||||
Url: subObj["url"].(string),
|
||||
@@ -169,7 +194,7 @@ func (m Message) ParseContent() []MediaMessage {
|
||||
},
|
||||
})
|
||||
} else if url, ok := contentMap["image_url"].(string); ok {
|
||||
contentList = append(contentList, MediaMessage{
|
||||
contentList = append(contentList, MediaContent{
|
||||
Type: ContentTypeImageURL,
|
||||
ImageUrl: MessageImageUrl{
|
||||
Url: url,
|
||||
@@ -179,7 +204,7 @@ func (m Message) ParseContent() []MediaMessage {
|
||||
}
|
||||
case ContentTypeInputAudio:
|
||||
if subObj, ok := contentMap["input_audio"].(map[string]any); ok {
|
||||
contentList = append(contentList, MediaMessage{
|
||||
contentList = append(contentList, MediaContent{
|
||||
Type: ContentTypeInputAudio,
|
||||
InputAudio: MessageInputAudio{
|
||||
Data: subObj["data"].(string),
|
||||
|
||||
529
i18n/en.json
529
i18n/en.json
@@ -1,529 +0,0 @@
|
||||
{
|
||||
"$%.6f 额度": "$%.6f quota",
|
||||
"%d 点额度": "%d point quota",
|
||||
"尚未实现": "Not yet implemented",
|
||||
"余额不足": "Insufficient balance",
|
||||
"危险操作": "Hazardous operations",
|
||||
"输入你的账户名": "Enter your account name",
|
||||
"确认删除": "Confirm Delete",
|
||||
"确认绑定": "Confirm Binding",
|
||||
"您正在删除自己的帐户,将清空所有数据且不可恢复": "You are deleting your account, all data will be cleared and unrecoverable.",
|
||||
"\"通道「%s」(#%d)已被禁用\"": "\"Channel %s (#%d) has been disabled\"",
|
||||
"通道「%s」(#%d)已被禁用,原因:%s": "Channel %s (#%d) has been disabled, reason: %s",
|
||||
"测试已在运行中": "Test is already running",
|
||||
"响应时间 %.2fs 超过阈值 %.2fs": "Response time %.2fs exceeds threshold %.2fs",
|
||||
"通道测试完成": "Channel test completed",
|
||||
"通道测试完成,如果没有收到禁用通知,说明所有通道都正常": "Channel test completed, if you have not received the disable notification, it means that all channels are normal",
|
||||
"无法连接至 GitHub 服务器,请稍后重试!": "Unable to connect to GitHub server, please try again later!",
|
||||
"返回值非法,用户字段为空,请稍后重试!": "The return value is illegal, the user field is empty, please try again later!",
|
||||
"管理员未开启通过 GitHub 登录以及注册": "The administrator did not turn on login and registration via GitHub",
|
||||
"管理员关闭了新用户注册": "The administrator has turned off new user registration",
|
||||
"用户已被封禁": "User has been banned",
|
||||
"该 GitHub 账户已被绑定": "The GitHub account has been bound",
|
||||
"邮箱地址已被占用": "Email address is occupied",
|
||||
"%s邮箱验证邮件": "%s Email verification email",
|
||||
"<p>您好,你正在进行%s邮箱验证。</p>": "<p>Hello, you are verifying %s email.</p>",
|
||||
"<p>您的验证码为: <strong>%s</strong></p>": "<p>Your verification code is: <strong>%s</strong></p>",
|
||||
"<p>验证码 %d 分钟内有效,如果不是本人操作,请忽略。</p>": "<p>The verification code is valid within %d minutes. If it is not your operation, please ignore it.</p>",
|
||||
"无效的参数": "Invalid parameter",
|
||||
"该邮箱地址未注册": "The email address is not registered",
|
||||
"%s密码重置": "%s Password reset",
|
||||
"<p>您好,你正在进行%s密码重置。</p>": "<p>Hello, you are resetting %s password.</p>",
|
||||
"<p>点击<a href='%s'>此处</a>进行密码重置。</p>": "<p>Click <a href='%s'>here</a> to reset your password.</p>",
|
||||
"<p>重置链接 %d 分钟内有效,如果不是本人操作,请忽略。</p>": "<p>The reset link is valid within %d minutes. If it is not your operation, please ignore it.</p>",
|
||||
"重置链接非法或已过期": "Reset link is illegal or expired",
|
||||
"无法启用 GitHub OAuth,请先填入 GitHub Client ID 以及 GitHub Client Secret!": "Unable to enable GitHub OAuth, please fill in GitHub Client ID and GitHub Client Secret first!",
|
||||
"无法启用微信登录,请先填入微信登录相关配置信息!": "Unable to enable WeChat login, please fill in the relevant configuration information for WeChat login first!",
|
||||
"无法启用 Turnstile 校验,请先填入 Turnstile 校验相关配置信息!": "Unable to enable Turnstile verification, please fill in the relevant configuration information for Turnstile verification first!",
|
||||
"兑换码名称长度必须在1-20之间": "The length of the redemption code name must be between 1-20",
|
||||
"兑换码个数必须大于0": "The number of redemption codes must be greater than 0",
|
||||
"一次兑换码批量生成的个数不能大于 100": "The number of redemption codes generated in a batch cannot be greater than 100",
|
||||
"通过令牌「%s」使用模型 %s 消耗 %s(模型倍率 %.2f,分组倍率 %.2f)": "Using model %s with token %s consumes %s (model rate %.2f, group rate %.2f)",
|
||||
"当前分组上游负载已饱和,请稍后再试": "The current group load is saturated, please try again later",
|
||||
"令牌名称过长": "Token name is too long",
|
||||
"令牌已过期,无法启用,请先修改令牌过期时间,或者设置为永不过期": "The token has expired and cannot be enabled. Please modify the expiration time of the token, or set it to never expire.",
|
||||
"令牌可用额度已用尽,无法启用,请先修改令牌剩余额度,或者设置为无限额度": "The available quota of the token has been used up and cannot be enabled. Please modify the remaining quota of the token, or set it to unlimited quota",
|
||||
"管理员关闭了密码登录": "The administrator has turned off password login",
|
||||
"无法保存会话信息,请重试": "Unable to save session information, please try again",
|
||||
"管理员关闭了通过密码进行注册,请使用第三方账户验证的形式进行注册": "The administrator has turned off registration via password. Please use the form of third-party account verification to register",
|
||||
"输入不合法 ": "Input is illegal ",
|
||||
"管理员开启了邮箱验证,请输入邮箱地址和验证码": "The administrator has turned on email verification, please enter the email address and verification code",
|
||||
"验证码错误或已过期": "Verification code error or expired",
|
||||
"无权获取同级或更高等级用户的信息": "No permission to get information of users at the same level or higher",
|
||||
"请重试,系统生成的 UUID 竟然重复了!": "Please try again, the system-generated UUID is actually duplicated!",
|
||||
"输入不合法": "Input is illegal",
|
||||
"无权更新同权限等级或更高权限等级的用户信息": "No permission to update user information with the same permission level or higher permission level",
|
||||
"管理员将用户额度从 %s修改为 %s": "The administrator changed the user quota from %s to %s",
|
||||
"无权删除同权限等级或更高权限等级的用户": "No permission to delete users with the same permission level or higher permission level",
|
||||
"无法创建权限大于等于自己的用户": "Unable to create users with permissions greater than or equal to your own",
|
||||
"用户不存在": "User does not exist",
|
||||
"无法禁用超级管理员用户": "Unable to disable super administrator user",
|
||||
"无法删除超级管理员用户": "Unable to delete super administrator user",
|
||||
"普通管理员用户无法提升其他用户为管理员": "Ordinary administrator users cannot promote other users to administrators",
|
||||
"该用户已经是管理员": "The user is already an administrator",
|
||||
"无法降级超级管理员用户": "Unable to downgrade super administrator user",
|
||||
"该用户已经是普通用户": "The user is already an ordinary user",
|
||||
"管理员未开启通过微信登录以及注册": "The administrator has not enabled login and registration via WeChat",
|
||||
"该微信账号已被绑定": "The WeChat account has been bound",
|
||||
"无权进行此操作,未登录且未提供 access token": "No permission to perform this operation, not logged in and no access token provided",
|
||||
"无权进行此操作,access token 无效": "No permission to perform this operation, access token is invalid",
|
||||
"无权进行此操作,权限不足": "No permission to perform this operation, insufficient permissions",
|
||||
"普通用户不支持指定渠道": "Ordinary users do not support specifying channels",
|
||||
"无效的渠道 ID": "Invalid channel ID",
|
||||
"该渠道已被禁用": "The channel has been disabled",
|
||||
"无效的请求": "Invalid request",
|
||||
"无可用渠道": "No available channels",
|
||||
"Turnstile token 为空": "Turnstile token is empty",
|
||||
"Turnstile 校验失败,请刷新重试!": "Turnstile verification failed, please refresh and try again!",
|
||||
"id 为空!": "id is empty!",
|
||||
"未提供兑换码": "No redemption code provided",
|
||||
"无效的 user id": "Invalid user id",
|
||||
"无效的兑换码": "Invalid redemption code",
|
||||
"该兑换码已被使用": "The redemption code has been used",
|
||||
"通过兑换码充值 %s": "Recharge %s through redemption code",
|
||||
"未提供令牌": "No token provided",
|
||||
"该令牌状态不可用": "The token status is not available",
|
||||
"该令牌已过期": "The token has expired",
|
||||
"该令牌额度已用尽": "The token quota has been used up",
|
||||
"无效的令牌": "Invalid token",
|
||||
"id 或 userId 为空!": "id or userId is empty!",
|
||||
"quota 不能为负数!": "quota cannot be negative!",
|
||||
"令牌额度不足": "Insufficient token quota",
|
||||
"用户额度不足": "Insufficient user quota",
|
||||
"您的额度即将用尽": "Your quota is about to run out",
|
||||
"您的额度已用尽": "Your quota has been used up",
|
||||
"%s,当前剩余额度为 %d,为了不影响您的使用,请及时充值。<br/>充值链接:<a href='%s'>%s</a>": "%s, the current remaining quota is %d, in order not to affect your use, please recharge in time. <br/> Recharge link: <a href='%s'>%s</a>",
|
||||
"affCode 为空!": "affCode is empty!",
|
||||
"新用户注册赠送 %s": "New user registration gives %s",
|
||||
"使用邀请码赠送 %s": "Use invitation code to give %s",
|
||||
"邀请用户赠送 %s": "Invite users to give %s",
|
||||
"用户名或密码为空": "Username or password is empty",
|
||||
"用户名或密码错误,或用户已被封禁": "Username or password is wrong, or user has been banned",
|
||||
"email 为空!": "email is empty!",
|
||||
"GitHub id 为空!": "GitHub id is empty!",
|
||||
"WeChat id 为空!": "WeChat id is empty!",
|
||||
"username 为空!": "username is empty!",
|
||||
"邮箱地址或密码为空!": "Email address or password is empty!",
|
||||
"OpenAI 接口聚合管理,支持多种渠道包括 Azure,可用于二次分发管理 key,仅单可执行文件,已打包好 Docker 镜像,一键部署,开箱即用": "OpenAI interface aggregation management, supports multiple channels including Azure, can be used for secondary distribution management key, only single executable file, Docker image has been packaged, one-click deployment, out of the box",
|
||||
"未知类型": "Unknown type",
|
||||
"不支持": "Not supported",
|
||||
"操作成功完成!": "Operation completed successfully!",
|
||||
"已启用": "Enabled",
|
||||
"已禁用": "Disabled",
|
||||
"未知状态": "Unknown status",
|
||||
" 秒": "s",
|
||||
" 分钟 ": " m ",
|
||||
" 小时 ": " h ",
|
||||
" 天 ": " d ",
|
||||
" 个月 ": " M ",
|
||||
" 年 ": " y ",
|
||||
"未测试": "Not tested",
|
||||
"通道 ${name} 测试成功,耗时 ${time.toFixed(2)} 秒。": "Channel ${name} test succeeded, time consumed ${time.toFixed(2)} s.",
|
||||
"已成功开始测试所有已启用通道,请刷新页面查看结果。": "All enabled channels have been successfully tested, please refresh the page to view the results.",
|
||||
"通道 ${name} 余额更新成功!": "Channel ${name} balance updated successfully!",
|
||||
"已更新完毕所有已启用通道余额!": "The balance of all enabled channels has been updated!",
|
||||
"搜索渠道的 ID,名称和密钥 ...": "Search for channel ID, name and key ...",
|
||||
"名称": "Name",
|
||||
"分组": "Group",
|
||||
"类型": "Type",
|
||||
"状态": "Status",
|
||||
"响应时间": "Response time",
|
||||
"余额": "Balance",
|
||||
"操作": "Operation",
|
||||
"未更新": "Not updated",
|
||||
"测试": "Test",
|
||||
"更新余额": "Update balance",
|
||||
"删除": "Delete",
|
||||
"删除渠道 {channel.name}": "Delete channel {channel.name}",
|
||||
"禁用": "Disable",
|
||||
"启用": "Enable",
|
||||
"编辑": "Edit",
|
||||
"添加新的渠道": "Add a new channel",
|
||||
"测试所有已启用通道": "Test all enabled channels",
|
||||
"更新所有已启用通道余额": "Update the balance of all enabled channels",
|
||||
"刷新": "Refresh",
|
||||
"处理中...": "Processing...",
|
||||
"绑定成功!": "Binding succeeded!",
|
||||
"登录成功!": "Login succeeded!",
|
||||
"操作失败,重定向至登录界面中...": "Operation failed, redirecting to the login page...",
|
||||
"出现错误,第 ${count} 次重试中...": "An error occurred, retrying for the ${count} time...",
|
||||
"首页": "Home",
|
||||
"渠道": "Channel",
|
||||
"令牌": "Token",
|
||||
"兑换": "Redeem",
|
||||
"充值": "Recharge",
|
||||
"用户": "User",
|
||||
"日志": "Log",
|
||||
"设置": "Settings",
|
||||
"关于": "About",
|
||||
"聊天": "Chat",
|
||||
"注销成功!": "Logout succeeded!",
|
||||
"注销": "Logout",
|
||||
"登录": "Login",
|
||||
"注册": "Register",
|
||||
"加载{name}中...": "Loading {name}...",
|
||||
"未登录或登录已过期,请重新登录!": "Not logged in or login has expired, please log in again!",
|
||||
"用户登录": "User login",
|
||||
"\"用户名\"": "\"Username\"",
|
||||
"\"密码\"": "\"Password\"",
|
||||
"忘记密码?": "Forget password?",
|
||||
"点击重置": "Click to reset",
|
||||
"; 没有账户?": "; No account?",
|
||||
"点击注册": "Click to register",
|
||||
"微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)": "Scan the QR code of WeChat to follow the official account, enter \"verification code\" to get the verification code (valid within three minutes)",
|
||||
"\"验证码\"": "\"Verification code\"",
|
||||
"全部用户": "All users",
|
||||
"当前用户": "Current user",
|
||||
"'全部'": "'All'",
|
||||
"'充值'": "'Recharge'",
|
||||
"'消费'": "'Consumption'",
|
||||
"'管理'": "'Management'",
|
||||
"'系统'": "'System'",
|
||||
" 充值 ": " Recharge ",
|
||||
" 消费 ": " Consumption ",
|
||||
" 管理 ": " Management ",
|
||||
" 系统 ": " System ",
|
||||
" 未知 ": " Unknown ",
|
||||
"时间": "Time",
|
||||
"详情": "Details",
|
||||
"选择模式": "Select mode",
|
||||
"选择明细分类": "Select details category",
|
||||
"模型倍率不是合法的 JSON 字符串": "Model rate is not a valid JSON string",
|
||||
"分组倍率不是合法的 JSON 字符串": "Group rate is not a valid JSON string",
|
||||
"通用设置": "General Settings",
|
||||
"充值链接": "Recharge Link",
|
||||
"例如发卡网站的购买链接": "For example, the purchase link of the card issuing website",
|
||||
"聊天页面链接": "Chat Page Link",
|
||||
"例如 ChatGPT Next Web 的部署地址": "For example, the deployment address of ChatGPT Next Web",
|
||||
"单位美元额度": "Unit Dollar Quota",
|
||||
"一单位货币能兑换的额度": "Quota that can be exchanged for one unit of currency",
|
||||
"启用额度消费日志记录": "Enable quota consumption log recording",
|
||||
"以货币形式显示额度": "Display quota in the form of currency",
|
||||
"相关 API 显示令牌额度而非用户额度": "Related API displays token quota instead of user quota",
|
||||
"保存通用设置": "Save General Settings",
|
||||
"监控设置": "Monitoring Settings",
|
||||
"最长响应时间": "Longest Response Time",
|
||||
"单位秒": "Unit in seconds",
|
||||
"当运行通道全部测试时": "When all operating channels are tested",
|
||||
"超过此时间将自动禁用通道": "Channels will be automatically disabled if this time is exceeded",
|
||||
"额度提醒阈值": "Quota reminder threshold",
|
||||
"低于此额度时将发送邮件提醒用户": "Email will be sent to remind users when the quota is below this",
|
||||
"失败时自动禁用通道": "Automatically disable the channel when it fails",
|
||||
"保存监控设置": "Save Monitoring Settings",
|
||||
"额度设置": "Quota Settings",
|
||||
"新用户初始额度": "Initial quota for new users",
|
||||
"例如": "For example",
|
||||
"请求预扣费额度": "Request for pre-deducted quota",
|
||||
"请求结束后多退少补": "Refund more or less after the request ends",
|
||||
"邀请新用户奖励额度": "Invite new users to reward quota",
|
||||
"新用户使用邀请码奖励额度": "New user rewards quota using invitation code",
|
||||
"保存额度设置": "Save Quota Settings",
|
||||
"倍率设置": "Rate Settings",
|
||||
"模型倍率": "Model rate",
|
||||
"为一个 JSON 文本": "Is a JSON text",
|
||||
"键为模型名称": "Key is model name",
|
||||
"值为倍率": "Value is the rate",
|
||||
"分组倍率": "Group rate",
|
||||
"键为分组名称": "Key is group name",
|
||||
"保存倍率设置": "Save Rate Settings",
|
||||
"已是最新版本": "Is the latest version",
|
||||
"检查更新": "Check for updates",
|
||||
"公告": "Announcement",
|
||||
"在此输入新的公告内容,支持 Markdown & HTML 代码": "Enter the new announcement content here, supports Markdown & HTML code",
|
||||
"保存公告": "Save Announcement",
|
||||
"个性化设置": "Personalization Settings",
|
||||
"系统名称": "System Name",
|
||||
"在此输入系统名称": "Enter the system name here",
|
||||
"设置系统名称": "Set system name",
|
||||
"图片地址": "Image URL",
|
||||
"在此输入 Logo 图片地址": "Enter the Logo image URL here",
|
||||
"首页内容": "Home Page Content",
|
||||
"在此输入首页内容,支持 Markdown & HTML 代码,设置后首页的状态信息将不再显示。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为首页": "Enter the homepage content here, supports Markdown & HTML code. Once set, the status information of the homepage will not be displayed. If a link is entered, it will be used as the src attribute of the iframe, allowing you to set any webpage as the homepage.",
|
||||
"保存首页内容": "Save Home Page Content",
|
||||
"在此输入新的关于内容,支持 Markdown & HTML 代码。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为关于页面": "Enter new about content here, supports Markdown & HTML code. If a link is entered, it will be used as the src attribute of the iframe, allowing you to set any webpage as the about page.",
|
||||
"保存关于": "Save About",
|
||||
"移除 One API 的版权标识必须首先获得授权,项目维护需要花费大量精力,如果本项目对你有意义,请主动支持本项目": "Removal of One API copyright mark must first be authorized. Project maintenance requires a lot of effort. If this project is meaningful to you, please actively support it.",
|
||||
"页脚": "Footer",
|
||||
"在此输入新的页脚,留空则使用默认页脚,支持 HTML 代码": "Enter the new footer here, leave blank to use the default footer, supports HTML code.",
|
||||
"设置页脚": "Set Footer",
|
||||
"新版本": "New Version",
|
||||
"关闭": "Close",
|
||||
"密码已重置并已复制到剪贴板": "Password has been reset and copied to clipboard",
|
||||
"密码重置确认": "Password Reset Confirmation",
|
||||
"邮箱地址": "Email Address",
|
||||
"提交": "Submit",
|
||||
"请稍后几秒重试": "Please retry in a few seconds",
|
||||
"正在检查用户环境": "Checking user environment",
|
||||
"重置邮件发送成功": "Reset mail sent successfully",
|
||||
"请检查邮箱": "Please check your email",
|
||||
"密码重置": "Password Reset",
|
||||
"令牌已重置并已复制到剪贴板": "Token has been reset and copied to clipboard",
|
||||
"邀请链接已复制到剪切板": "Invitation link has been copied to clipboard",
|
||||
"微信账户绑定成功": "WeChat account binding succeeded",
|
||||
"验证码发送成功": "Verification code sent successfully",
|
||||
"邮箱账户绑定成功": "Email account binding succeeded",
|
||||
"注意": "Note",
|
||||
"此处生成的令牌用于系统管理": "The token generated here is used for system management",
|
||||
"而非用于请求 OpenAI 相关的服务": "Not for requesting OpenAI related services",
|
||||
"请知悉": "Please be aware",
|
||||
"更新个人信息": "Update Personal Information",
|
||||
"生成系统访问令牌": "Generate System Access Token",
|
||||
"复制邀请链接": "Copy Invitation Link",
|
||||
"账号绑定": "Account Binding",
|
||||
"绑定微信账号": "Bind WeChat Account",
|
||||
"微信扫码关注公众号": "Scan the QR code with WeChat to follow the official account",
|
||||
"输入": "Enter",
|
||||
"验证码": "Verification Code",
|
||||
"获取验证码": "Get Verification Code",
|
||||
"三分钟内有效": "Valid for three minutes",
|
||||
"绑定": "Bind",
|
||||
"绑定 GitHub 账号": "Bind GitHub Account",
|
||||
"绑定邮箱地址": "Bind Email Address",
|
||||
"输入邮箱地址": "Enter Email Address",
|
||||
"未使用": "Unused",
|
||||
"已使用": "Used",
|
||||
"操作成功完成": "Operation successfully completed",
|
||||
"搜索兑换码的 ID 和名称": "Search for ID and name",
|
||||
"额度": "Quota",
|
||||
"创建时间": "Creation Time",
|
||||
"兑换时间": "Redemption Time",
|
||||
"尚未兑换": "Not yet redeemed",
|
||||
"已复制到剪贴板": "Copied to clipboard",
|
||||
"无法复制到剪贴板": "Unable to copy to clipboard",
|
||||
"请手动复制": "Please copy manually",
|
||||
"已将兑换码填入搜索框": "The voucher code has been filled into the search box",
|
||||
"复制": "Copy",
|
||||
"添加新的兑换码": "Add a new voucher",
|
||||
"密码长度不得小于 8 位": "Password length must not be less than 8 characters",
|
||||
"两次输入的密码不一致": "The two passwords entered do not match",
|
||||
"注册成功": "Registration succeeded",
|
||||
"请稍后几秒重试,Turnstile 正在检查用户环境": "Please retry in a few seconds, Turnstile is checking user environment",
|
||||
"验证码发送成功,请检查你的邮箱": "Verification code sent successfully, please check your email",
|
||||
"新用户注册": "New User Registration",
|
||||
"输入用户名,最长 12 位": "Enter username, up to 12 characters",
|
||||
"输入密码,最短 8 位,最长 20 位": "Enter password, at least 8 characters and up to 20 characters",
|
||||
"输入验证码": "Enter Verification Code",
|
||||
"已有账户": "Already have an account",
|
||||
"点击登录": "Click to log in",
|
||||
"服务器地址": "Server Address",
|
||||
"更新服务器地址": "Update Server Address",
|
||||
"配置登录注册": "Configure Login/Registration",
|
||||
"允许通过密码进行登录": "Allow login via password",
|
||||
"允许通过密码进行注册": "Allow registration via password",
|
||||
"通过密码注册时需要进行邮箱验证": "Email verification is required when registering via password",
|
||||
"允许通过 GitHub 账户登录 & 注册": "Allow login & registration via GitHub account",
|
||||
"允许通过微信登录 & 注册": "Allow login & registration via WeChat",
|
||||
"允许新用户注册(此项为否时,新用户将无法以任何方式进行注册": "Allow new user registration (if this option is off, new users will not be able to register in any way",
|
||||
"启用 Turnstile 用户校验": "Enable Turnstile user verification",
|
||||
"配置 SMTP": "Configure SMTP",
|
||||
"用以支持系统的邮件发送": "To support the system email sending",
|
||||
"SMTP 服务器地址": "SMTP Server Address",
|
||||
"例如:smtp.qq.com": "For example: smtp.qq.com",
|
||||
"SMTP 端口": "SMTP Port",
|
||||
"默认: 587": "Default: 587",
|
||||
"SMTP 账户": "SMTP Account",
|
||||
"通常是邮箱地址": "Usually an email address",
|
||||
"发送者邮箱": "Sender email",
|
||||
"通常和邮箱地址保持一致": "Usually consistent with the email address",
|
||||
"SMTP 访问凭证": "SMTP Access Credential",
|
||||
"敏感信息不会发送到前端显示": "Sensitive information will not be displayed in the frontend",
|
||||
"保存 SMTP 设置": "Save SMTP Settings",
|
||||
"配置 GitHub OAuth App": "Configure GitHub OAuth App",
|
||||
"配置 Linuxdo OAuth App": "Configure Linuxdo OAuth App",
|
||||
"用以支持通过 GitHub 进行登录注册": "To support login & registration via GitHub",
|
||||
"点击此处": "Click here",
|
||||
"管理你的 GitHub OAuth App": "Manage your GitHub OAuth App",
|
||||
"输入你注册的 GitHub OAuth APP 的 ID": "Enter your registered GitHub OAuth APP ID",
|
||||
"保存 GitHub OAuth 设置": "Save GitHub OAuth Settings",
|
||||
"配置 WeChat Server": "Configure WeChat Server",
|
||||
"用以支持通过微信进行登录注册": "To support login & registration via WeChat",
|
||||
"了解 WeChat Server": "Learn about WeChat Server",
|
||||
"WeChat Server 访问凭证": "WeChat Server Access Credential",
|
||||
"微信公众号二维码图片链接": "WeChat Public Account QR Code Image Link",
|
||||
"输入一个图片链接": "Enter an image link",
|
||||
"保存 WeChat Server 设置": "Save WeChat Server Settings",
|
||||
"配置 Turnstile": "Configure Turnstile",
|
||||
"用以支持用户校验": "To support user verification",
|
||||
"管理你的 Turnstile Sites,推荐选择 Invisible Widget Type": "Manage your Turnstile Sites, recommend selecting Invisible Widget Type",
|
||||
"输入你注册的 Turnstile Site Key": "Enter your registered Turnstile Site Key",
|
||||
"保存 Turnstile 设置": "Save Turnstile Settings",
|
||||
"已过期": "Expired",
|
||||
"已耗尽": "Exhausted",
|
||||
"搜索令牌的名称 ...": "Search for the name of the token...",
|
||||
"已用额度": "Quota used",
|
||||
"剩余额度": "Remaining quota",
|
||||
"过期时间": "Expiration time",
|
||||
"无": "None",
|
||||
"无限制": "Unlimited",
|
||||
"永不过期": "Never expires",
|
||||
"无法复制到剪贴板,请手动复制,已将令牌填入搜索框": "Unable to copy to clipboard, please copy manually, the token has been entered into the search box",
|
||||
"删除令牌": "Delete Token",
|
||||
"添加新的令牌": "Add New Token",
|
||||
"普通用户": "Regular User",
|
||||
"管理员": "Admin",
|
||||
"超级管理员": "Super Admin",
|
||||
"未知身份": "Unknown Identity",
|
||||
"已激活": "Activated",
|
||||
"已封禁": "Banned",
|
||||
"搜索用户的 ID,用户名,显示名称,以及邮箱地址 ...": "Search user ID, username, display name, and email address...",
|
||||
"用户名": "Username",
|
||||
"统计信息": "Statistics",
|
||||
"用户角色": "User Role",
|
||||
"未绑定邮箱地址": "Email not bound",
|
||||
"请求次数": "Number of Requests",
|
||||
"提升": "Promote",
|
||||
"降级": "Demote",
|
||||
"删除用户": "Delete User",
|
||||
"添加新的用户": "Add New User",
|
||||
"自定义": "Custom",
|
||||
"等价金额": "Equivalent Amount",
|
||||
"未登录或登录已过期,请重新登录": "Not logged in or login has expired, please log in again",
|
||||
"请求次数过多,请稍后再试": "Too many requests, please try again later",
|
||||
"服务器内部错误,请联系管理员": "Server internal error, please contact the administrator",
|
||||
"本站仅作演示之用,无服务端": "This site is for demonstration purposes only, no server-side",
|
||||
"超级管理员未设置充值链接!": "Super administrator has not set the recharge link!",
|
||||
"错误:": "Error: ",
|
||||
"新版本可用:${data.version},请使用快捷键 Shift + F5 刷新页面": "New version available: ${data.version}, please refresh the page using shortcut Shift + F5",
|
||||
"无法正常连接至服务器": "Unable to connect to the server normally",
|
||||
"管理渠道": "Manage Channels",
|
||||
"系统状况": "System Status",
|
||||
"系统信息": "System Information",
|
||||
"系统信息总览": "System Information Overview",
|
||||
"版本": "Version",
|
||||
"源码": "Source Code",
|
||||
"启动时间": "Startup Time",
|
||||
"系统配置": "System Configuration",
|
||||
"系统配置总览": "System Configuration Overview",
|
||||
"邮箱验证": "Email Verification",
|
||||
"未启用": "Not Enabled",
|
||||
"GitHub 身份验证": "GitHub Authentication",
|
||||
"微信身份验证": "WeChat Authentication",
|
||||
"Turnstile 用户校验": "Turnstile User Verification",
|
||||
"创建新的渠道": "Create New Channel",
|
||||
"镜像": "Mirror",
|
||||
"请输入镜像站地址,格式为:https://domain.com,可不填,不填则使用渠道默认值": "Please enter the mirror site address, the format is: https://domain.com, it can be left blank, if left blank, the default value of the channel will be used",
|
||||
"模型": "Model",
|
||||
"请选择该通道所支持的模型": "Please select the model supported by the channel",
|
||||
"填入基础模型": "Fill in the basic model",
|
||||
"填入所有模型": "Fill in all models",
|
||||
"清除所有模型": "Clear all models",
|
||||
"密钥": "Key",
|
||||
"请输入密钥": "Please enter the key",
|
||||
"批量创建": "Batch Create",
|
||||
"更新渠道信息": "Update Channel Information",
|
||||
"我的令牌": "My Tokens",
|
||||
"管理兑换码": "Manage Redeem Codes",
|
||||
"兑换码": "Redeem Code",
|
||||
"管理用户": "Manage Users",
|
||||
"额度明细": "Quota Details",
|
||||
"个人设置": "Personal Settings",
|
||||
"运营设置": "Operation Settings",
|
||||
"系统设置": "System Settings",
|
||||
"其他设置": "Other Settings",
|
||||
"项目仓库地址": "Project Repository Address",
|
||||
"可在设置页面设置关于内容,支持 HTML & Markdown": "You can set the content about in the settings page, support HTML & Markdown",
|
||||
"由{' '}": "built by{' '}",
|
||||
"构建,源代码遵循{' '}": ", the source code licensed under{' '}",
|
||||
"MIT 协议": "MIT License",
|
||||
"充值额度": "Recharge Quota",
|
||||
"获取兑换码": "Get Redeem Code",
|
||||
"一个月后过期": "Expires after one month",
|
||||
"一天后过期": "Expires after one day",
|
||||
"一小时后过期": "Expires after one hour",
|
||||
"一分钟后过期": "Expires after one minute",
|
||||
"创建新的令牌": "Create New Token",
|
||||
"注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。": "Note that the quota of the token is only used to limit the maximum quota usage of the token itself, and the actual usage is limited by the remaining quota of the account.",
|
||||
"设为无限额度": "Set to unlimited quota",
|
||||
"更新令牌信息": "Update Token Information",
|
||||
"请输入充值码!": "Please enter the recharge code!",
|
||||
"请输入名称": "Please enter a name",
|
||||
"请输入密钥,一行一个": "Please enter the key, one per line",
|
||||
"请输入额度": "Please enter the quota",
|
||||
"令牌创建成功": "Token created successfully",
|
||||
"令牌更新成功": "Token updated successfully",
|
||||
"充值成功!": "Recharge successful!",
|
||||
"更新用户信息": "Update User Information",
|
||||
"请输入新的用户名": "Please enter a new username",
|
||||
"密码": "Password",
|
||||
"请输入新的密码": "Please enter a new password",
|
||||
"显示名称": "Display Name",
|
||||
"请输入新的显示名称": "Please enter a new display name",
|
||||
"已绑定的 GitHub 账户": "GitHub Account Bound",
|
||||
"此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改": "This item is read-only. Users need to bind through the relevant binding button on the personal settings page, and cannot be modified directly",
|
||||
"已绑定的微信账户": "WeChat Account Bound",
|
||||
"已绑定的邮箱账户": "Email Account Bound",
|
||||
"用户信息更新成功!": "User information updated successfully!",
|
||||
"模型倍率 %.2f,分组倍率 %.2f": "model rate %.2f, group rate %.2f",
|
||||
"使用明细(总消耗额度:{renderQuota(stat.quota)})": "Usage Details (Total Consumption Quota: {renderQuota(stat.quota)})",
|
||||
"用户名称": "User Name",
|
||||
"令牌名称": "Token Name",
|
||||
"留空则查询全部用户": "Leave blank to query all users",
|
||||
"留空则查询全部令牌": "Leave blank to query all tokens",
|
||||
"模型名称": "Model Name",
|
||||
"留空则查询全部模型": "Leave blank to query all models",
|
||||
"起始时间": "Start Time",
|
||||
"结束时间": "End Time",
|
||||
"查询": "Query",
|
||||
"提示": "Prompt",
|
||||
"补全": "Completion",
|
||||
"消耗额度": "Used Quota",
|
||||
"可选值": "Optional Values",
|
||||
"渠道不存在:%d": "Channel does not exist: %d",
|
||||
"数据库一致性已被破坏,请联系管理员": "Database consistency has been broken, please contact the administrator",
|
||||
"使用近似的方式估算 token 数以减少计算量": "Estimate the number of tokens in an approximate way to reduce computational load",
|
||||
"请填写ChannelName和ChannelKey!": "Please fill in the ChannelName and ChannelKey!",
|
||||
"请至少选择一个Model!": "Please select at least one Model!",
|
||||
"加载首页内容失败": "Failed to load the homepage content",
|
||||
"加载关于内容失败": "Failed to load the About content",
|
||||
"兑换码更新成功!": "Redemption code updated successfully!",
|
||||
"兑换码创建成功!": "Redemption code created successfully!",
|
||||
"用户账户创建成功!": "User account created successfully!",
|
||||
"生成数量": "Generate quantity",
|
||||
"请输入生成数量": "Please enter the quantity to generate",
|
||||
"创建新用户账户": "Create new user account",
|
||||
"渠道更新成功!": "Channel updated successfully!",
|
||||
"渠道创建成功!": "Channel created successfully!",
|
||||
"请选择分组": "Please select a group",
|
||||
"更新兑换码信息": "Update redemption code information",
|
||||
"创建新的兑换码": "Create a new redemption code",
|
||||
"请在系统设置页面编辑分组倍率以添加新的分组:": "Please edit the group ratio in the system settings page to add a new group:",
|
||||
"未找到所请求的页面": "The requested page was not found",
|
||||
"过期时间格式错误!": "Expiration time format error!",
|
||||
"请输入过期时间,格式为 yyyy-MM-dd HH:mm:ss,-1 表示无限制": "Please enter the expiration time, the format is yyyy-MM-dd HH:mm:ss, -1 means no limit",
|
||||
"此项可选,为一个 JSON 文本,键为用户请求的模型名称,值为要替换的模型名称,例如:": "This is optional, it's a JSON text, the key is the model name requested by the user, and the value is the model name to be replaced, for example:",
|
||||
"此项可选,输入镜像站地址,格式为:": "This is optional, enter the mirror site address, the format is:",
|
||||
"模型映射": "Model mapping",
|
||||
"请输入默认 API 版本,例如:2023-03-15-preview,该配置可以被实际的请求查询参数所覆盖": "Please enter the default API version, for example: 2023-03-15-preview, this configuration can be overridden by the actual request query parameters",
|
||||
"默认": "Default",
|
||||
"图片演示": "Image demo",
|
||||
"参数替换为你的部署名称(模型名称中的点会被剔除)": "Replace the parameter with your deployment name (dots in the model name will be removed)",
|
||||
"模型映射必须是合法的 JSON 格式!": "Model mapping must be in valid JSON format!",
|
||||
"取消无限额度": "Cancel unlimited quota",
|
||||
"取消": "Cancel",
|
||||
"请输入新的剩余额度": "Please enter the new remaining quota",
|
||||
"请输入单个兑换码中包含的额度": "Please enter the quota included in a single redemption code",
|
||||
"请输入用户名": "Please enter username",
|
||||
"请输入显示名称": "Please enter display name",
|
||||
"请输入密码": "Please enter password",
|
||||
"模型部署名称必须和模型名称保持一致": "The model deployment name must be consistent with the model name",
|
||||
",因为 One API 会把请求体中的 model": ", because One API will take the model in the request body",
|
||||
"请输入 AZURE_OPENAI_ENDPOINT": "Please enter AZURE_OPENAI_ENDPOINT",
|
||||
"请输入自定义渠道的 Base URL": "Please enter the Base URL of the custom channel",
|
||||
"Homepage URL 填": "Fill in the Homepage URL",
|
||||
"Authorization callback URL 填": "Fill in the Authorization callback URL",
|
||||
"请为通道命名": "Please name the channel",
|
||||
"此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:": "This is optional, used to modify the model name in the request body, it's a JSON string, the key is the model name in the request, and the value is the model name to be replaced, for example:",
|
||||
"模型重定向": "Model redirection",
|
||||
"请输入渠道对应的鉴权密钥": "Please enter the authentication key corresponding to the channel",
|
||||
"注意,": "Note that, ",
|
||||
",图片演示。": "related image demo.",
|
||||
"令牌创建成功,请在列表页面点击复制获取令牌!": "Token created successfully, please click copy on the list page to get the token!",
|
||||
"代理": "Proxy",
|
||||
"此项可选,用于通过代理站来进行 API 调用,请输入代理站地址,格式为:https://domain.com": "This is optional, used to make API calls through the proxy site, please enter the proxy site address, the format is: https://domain.com",
|
||||
"取消密码登录将导致所有未绑定其他登录方式的用户(包括管理员)无法通过密码登录,确认取消?": "Canceling password login will cause all users (including administrators) who have not bound other login methods to be unable to log in via password, confirm cancel?",
|
||||
"按照如下格式输入:": "Enter in the following format:",
|
||||
"模型版本": "Model version",
|
||||
"请输入星火大模型版本,注意是接口地址中的版本号,例如:v2.1": "Please enter the version of the Starfire model, note that it is the version number in the interface address, for example: v2.1",
|
||||
"点击查看": "click to view",
|
||||
"请确保已在 Azure 上创建了 gpt-35-turbo 模型,并且 apiVersion 已正确填写!": "Please make sure that the gpt-35-turbo model has been created on Azure, and the apiVersion has been filled in correctly!"
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
|
||||
def list_file_paths(path):
|
||||
file_paths = []
|
||||
for root, dirs, files in os.walk(path):
|
||||
if "node_modules" in dirs:
|
||||
dirs.remove("node_modules")
|
||||
if "build" in dirs:
|
||||
dirs.remove("build")
|
||||
if "i18n" in dirs:
|
||||
dirs.remove("i18n")
|
||||
for file in files:
|
||||
file_path = os.path.join(root, file)
|
||||
if file_path.endswith("png") or file_path.endswith("ico") or file_path.endswith("db") or file_path.endswith("exe"):
|
||||
continue
|
||||
file_paths.append(file_path)
|
||||
|
||||
for dir in dirs:
|
||||
dir_path = os.path.join(root, dir)
|
||||
file_paths += list_file_paths(dir_path)
|
||||
|
||||
return file_paths
|
||||
|
||||
|
||||
def replace_keys_in_repository(repo_path, json_file_path):
|
||||
with open(json_file_path, 'r', encoding="utf-8") as json_file:
|
||||
key_value_pairs = json.load(json_file)
|
||||
|
||||
pairs = []
|
||||
for key, value in key_value_pairs.items():
|
||||
pairs.append((key, value))
|
||||
pairs.sort(key=lambda x: len(x[0]), reverse=True)
|
||||
|
||||
files = list_file_paths(repo_path)
|
||||
print('Total files: {}'.format(len(files)))
|
||||
for file_path in files:
|
||||
replace_keys_in_file(file_path, pairs)
|
||||
|
||||
|
||||
def replace_keys_in_file(file_path, pairs):
|
||||
try:
|
||||
with open(file_path, 'r', encoding="utf-8") as file:
|
||||
content = file.read()
|
||||
|
||||
for key, value in pairs:
|
||||
content = content.replace(key, value)
|
||||
|
||||
with open(file_path, 'w', encoding="utf-8") as file:
|
||||
file.write(content)
|
||||
except UnicodeDecodeError:
|
||||
print('UnicodeDecodeError: {}'.format(file_path))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description='Replace keys in repository.')
|
||||
parser.add_argument('--repository_path', help='Path to repository')
|
||||
parser.add_argument('--json_file_path', help='Path to JSON file')
|
||||
args = parser.parse_args()
|
||||
replace_keys_in_repository(args.repository_path, args.json_file_path)
|
||||
@@ -10,8 +10,10 @@ import (
|
||||
"one-api/model"
|
||||
relayconstant "one-api/relay/constant"
|
||||
"one-api/service"
|
||||
"one-api/setting"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -42,12 +44,12 @@ func Distribute() func(c *gin.Context) {
|
||||
tokenGroup := c.GetString("token_group")
|
||||
if tokenGroup != "" {
|
||||
// check common.UserUsableGroups[userGroup]
|
||||
if _, ok := common.GetUserUsableGroups(userGroup)[tokenGroup]; !ok {
|
||||
if _, ok := setting.GetUserUsableGroups(userGroup)[tokenGroup]; !ok {
|
||||
abortWithOpenAiMessage(c, http.StatusForbidden, fmt.Sprintf("令牌分组 %s 已被禁用", tokenGroup))
|
||||
return
|
||||
}
|
||||
// check group in common.GroupRatio
|
||||
if _, ok := common.GroupRatio[tokenGroup]; !ok {
|
||||
if !setting.ContainsGroupRatio(tokenGroup) {
|
||||
abortWithOpenAiMessage(c, http.StatusForbidden, fmt.Sprintf("分组 %s 已被弃用", tokenGroup))
|
||||
return
|
||||
}
|
||||
@@ -112,6 +114,7 @@ func Distribute() func(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
}
|
||||
c.Set(constant.ContextKeyRequestStartTime, time.Now())
|
||||
SetupContextForSelectedChannel(c, channel, modelRequest.Model)
|
||||
c.Next()
|
||||
}
|
||||
@@ -213,6 +216,7 @@ func SetupContextForSelectedChannel(c *gin.Context, channel *model.Channel, mode
|
||||
c.Set("channel_id", channel.Id)
|
||||
c.Set("channel_name", channel.Name)
|
||||
c.Set("channel_type", channel.Type)
|
||||
c.Set("channel_setting", channel.GetSetting())
|
||||
if nil != channel.OpenAIOrganization && "" != *channel.OpenAIOrganization {
|
||||
c.Set("channel_organization", *channel.OpenAIOrganization)
|
||||
}
|
||||
|
||||
@@ -3,10 +3,11 @@ package model
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/samber/lo"
|
||||
"gorm.io/gorm"
|
||||
"one-api/common"
|
||||
"strings"
|
||||
|
||||
"github.com/samber/lo"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Ability struct {
|
||||
@@ -173,18 +174,67 @@ func (channel *Channel) DeleteAbilities() error {
|
||||
|
||||
// UpdateAbilities updates abilities of this channel.
|
||||
// Make sure the channel is completed before calling this function.
|
||||
func (channel *Channel) UpdateAbilities() error {
|
||||
// A quick and dirty way to update abilities
|
||||
func (channel *Channel) UpdateAbilities(tx *gorm.DB) error {
|
||||
isNewTx := false
|
||||
// 如果没有传入事务,创建新的事务
|
||||
if tx == nil {
|
||||
tx = DB.Begin()
|
||||
if tx.Error != nil {
|
||||
return tx.Error
|
||||
}
|
||||
isNewTx = true
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
tx.Rollback()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// First delete all abilities of this channel
|
||||
err := channel.DeleteAbilities()
|
||||
err := tx.Where("channel_id = ?", channel.Id).Delete(&Ability{}).Error
|
||||
if err != nil {
|
||||
if isNewTx {
|
||||
tx.Rollback()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Then add new abilities
|
||||
err = channel.AddAbilities()
|
||||
if err != nil {
|
||||
return err
|
||||
models_ := strings.Split(channel.Models, ",")
|
||||
groups_ := strings.Split(channel.Group, ",")
|
||||
abilities := make([]Ability, 0, len(models_))
|
||||
for _, model := range models_ {
|
||||
for _, group := range groups_ {
|
||||
ability := Ability{
|
||||
Group: group,
|
||||
Model: model,
|
||||
ChannelId: channel.Id,
|
||||
Enabled: channel.Status == common.ChannelStatusEnabled,
|
||||
Priority: channel.Priority,
|
||||
Weight: uint(channel.GetWeight()),
|
||||
Tag: channel.Tag,
|
||||
}
|
||||
abilities = append(abilities, ability)
|
||||
}
|
||||
}
|
||||
|
||||
if len(abilities) > 0 {
|
||||
for _, chunk := range lo.Chunk(abilities, 50) {
|
||||
err = tx.Create(&chunk).Error
|
||||
if err != nil {
|
||||
if isNewTx {
|
||||
tx.Rollback()
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是新创建的事务,需要提交
|
||||
if isNewTx {
|
||||
return tx.Commit().Error
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -246,7 +296,7 @@ func FixAbility() (int, error) {
|
||||
return 0, err
|
||||
}
|
||||
for _, channel := range channels {
|
||||
err := channel.UpdateAbilities()
|
||||
err := channel.UpdateAbilities(nil)
|
||||
if err != nil {
|
||||
common.SysError(fmt.Sprintf("Update abilities of channel %d failed: %s", channel.Id, err.Error()))
|
||||
} else {
|
||||
|
||||
@@ -342,3 +342,14 @@ func CacheGetChannel(id int) (*Channel, error) {
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func CacheUpdateChannelStatus(id int, status int) {
|
||||
if (!common.MemoryCacheEnabled) {
|
||||
return
|
||||
}
|
||||
channelSyncLock.Lock()
|
||||
defer channelSyncLock.Unlock()
|
||||
if channel, ok := channelsIDM[id]; ok {
|
||||
channel.Status = status
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"one-api/common"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -34,6 +35,7 @@ type Channel struct {
|
||||
AutoBan *int `json:"auto_ban" gorm:"default:1"`
|
||||
OtherInfo string `json:"other_info"`
|
||||
Tag *string `json:"tag" gorm:"index"`
|
||||
Setting string `json:"setting" gorm:"type:text"`
|
||||
}
|
||||
|
||||
func (channel *Channel) GetModels() []string {
|
||||
@@ -255,7 +257,7 @@ func (channel *Channel) Update() error {
|
||||
return err
|
||||
}
|
||||
DB.Model(channel).First(channel, "id = ?", channel.Id)
|
||||
err = channel.UpdateAbilities()
|
||||
err = channel.UpdateAbilities(nil)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -289,7 +291,25 @@ func (channel *Channel) Delete() error {
|
||||
return err
|
||||
}
|
||||
|
||||
var channelStatusLock sync.Mutex
|
||||
|
||||
func UpdateChannelStatusById(id int, status int, reason string) {
|
||||
if common.MemoryCacheEnabled {
|
||||
channelStatusLock.Lock()
|
||||
channelCache, _ := CacheGetChannel(id)
|
||||
// 如果缓存渠道存在,且状态已是目标状态,直接返回
|
||||
if channelCache != nil && channelCache.Status == status {
|
||||
channelStatusLock.Unlock()
|
||||
return
|
||||
}
|
||||
// 如果缓存渠道不存在(说明已经被禁用),且要设置的状态不为启用,直接返回
|
||||
if channelCache == nil && status != common.ChannelStatusEnabled {
|
||||
channelStatusLock.Unlock()
|
||||
return
|
||||
}
|
||||
CacheUpdateChannelStatus(id, status)
|
||||
channelStatusLock.Unlock()
|
||||
}
|
||||
err := UpdateAbilityStatus(id, status == common.ChannelStatusEnabled)
|
||||
if err != nil {
|
||||
common.SysError("failed to update ability status: " + err.Error())
|
||||
@@ -369,7 +389,7 @@ func EditChannelByTag(tag string, newTag *string, modelMapping *string, models *
|
||||
channels, err := GetChannelsByTag(updatedTag, false)
|
||||
if err == nil {
|
||||
for _, channel := range channels {
|
||||
err = channel.UpdateAbilities()
|
||||
err = channel.UpdateAbilities(nil)
|
||||
if err != nil {
|
||||
common.SysError("failed to update abilities: " + err.Error())
|
||||
}
|
||||
@@ -469,3 +489,62 @@ func SearchTags(keyword string, group string, model string, idSort bool) ([]*str
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
func (channel *Channel) GetSetting() map[string]interface{} {
|
||||
setting := make(map[string]interface{})
|
||||
if channel.Setting != "" {
|
||||
err := json.Unmarshal([]byte(channel.Setting), &setting)
|
||||
if err != nil {
|
||||
common.SysError("failed to unmarshal setting: " + err.Error())
|
||||
}
|
||||
}
|
||||
return setting
|
||||
}
|
||||
|
||||
func (channel *Channel) SetSetting(setting map[string]interface{}) {
|
||||
settingBytes, err := json.Marshal(setting)
|
||||
if err != nil {
|
||||
common.SysError("failed to marshal setting: " + err.Error())
|
||||
return
|
||||
}
|
||||
channel.Setting = string(settingBytes)
|
||||
}
|
||||
|
||||
func GetChannelsByIds(ids []int) ([]*Channel, error) {
|
||||
var channels []*Channel
|
||||
err := DB.Where("id in (?)", ids).Find(&channels).Error
|
||||
return channels, err
|
||||
}
|
||||
|
||||
func BatchSetChannelTag(ids []int, tag *string) error {
|
||||
// 开启事务
|
||||
tx := DB.Begin()
|
||||
if tx.Error != nil {
|
||||
return tx.Error
|
||||
}
|
||||
|
||||
// 更新标签
|
||||
err := tx.Model(&Channel{}).Where("id in (?)", ids).Update("tag", tag).Error
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
// update ability status
|
||||
channels, err := GetChannelsByIds(ids)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
for _, channel := range channels {
|
||||
err = channel.UpdateAbilities(tx)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 提交事务
|
||||
return tx.Commit().Error
|
||||
}
|
||||
|
||||
32
model/log.go
32
model/log.go
@@ -12,6 +12,16 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var groupCol string
|
||||
|
||||
func init() {
|
||||
if common.UsingPostgreSQL {
|
||||
groupCol = `"group"`
|
||||
} else {
|
||||
groupCol = "`group`"
|
||||
}
|
||||
}
|
||||
|
||||
type Log struct {
|
||||
Id int `json:"id" gorm:"index:idx_created_at_id,priority:1"`
|
||||
UserId int `json:"user_id" gorm:"index"`
|
||||
@@ -28,6 +38,7 @@ type Log struct {
|
||||
IsStream bool `json:"is_stream" gorm:"default:false"`
|
||||
ChannelId int `json:"channel" gorm:"index"`
|
||||
TokenId int `json:"token_id" gorm:"default:0;index"`
|
||||
Group string `json:"group" gorm:"index"`
|
||||
Other string `json:"other"`
|
||||
}
|
||||
|
||||
@@ -70,7 +81,9 @@ func RecordLog(userId int, logType int, content string) {
|
||||
}
|
||||
}
|
||||
|
||||
func RecordConsumeLog(ctx context.Context, userId int, channelId int, promptTokens int, completionTokens int, modelName string, tokenName string, quota int, content string, tokenId int, userQuota int, useTimeSeconds int, isStream bool, other map[string]interface{}) {
|
||||
func RecordConsumeLog(ctx context.Context, userId int, channelId int, promptTokens int, completionTokens int,
|
||||
modelName string, tokenName string, quota int, content string, tokenId int, userQuota int, useTimeSeconds int,
|
||||
isStream bool, group string, other map[string]interface{}) {
|
||||
common.LogInfo(ctx, fmt.Sprintf("record consume log: userId=%d, 用户调用前余额=%d, channelId=%d, promptTokens=%d, completionTokens=%d, modelName=%s, tokenName=%s, quota=%d, content=%s", userId, userQuota, channelId, promptTokens, completionTokens, modelName, tokenName, quota, content))
|
||||
if !common.LogConsumeEnabled {
|
||||
return
|
||||
@@ -92,6 +105,7 @@ func RecordConsumeLog(ctx context.Context, userId int, channelId int, promptToke
|
||||
TokenId: tokenId,
|
||||
UseTime: useTimeSeconds,
|
||||
IsStream: isStream,
|
||||
Group: group,
|
||||
Other: otherStr,
|
||||
}
|
||||
err := LOG_DB.Create(log).Error
|
||||
@@ -105,7 +119,7 @@ func RecordConsumeLog(ctx context.Context, userId int, channelId int, promptToke
|
||||
}
|
||||
}
|
||||
|
||||
func GetAllLogs(logType int, startTimestamp int64, endTimestamp int64, modelName string, username string, tokenName string, startIdx int, num int, channel int) (logs []*Log, total int64, err error) {
|
||||
func GetAllLogs(logType int, startTimestamp int64, endTimestamp int64, modelName string, username string, tokenName string, startIdx int, num int, channel int, group string) (logs []*Log, total int64, err error) {
|
||||
var tx *gorm.DB
|
||||
if logType == LogTypeUnknown {
|
||||
tx = LOG_DB
|
||||
@@ -130,6 +144,9 @@ func GetAllLogs(logType int, startTimestamp int64, endTimestamp int64, modelName
|
||||
if channel != 0 {
|
||||
tx = tx.Where("channel_id = ?", channel)
|
||||
}
|
||||
if group != "" {
|
||||
tx = tx.Where(groupCol+" = ?", group)
|
||||
}
|
||||
err = tx.Model(&Log{}).Count(&total).Error
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
@@ -141,7 +158,7 @@ func GetAllLogs(logType int, startTimestamp int64, endTimestamp int64, modelName
|
||||
return logs, total, err
|
||||
}
|
||||
|
||||
func GetUserLogs(userId int, logType int, startTimestamp int64, endTimestamp int64, modelName string, tokenName string, startIdx int, num int) (logs []*Log, total int64, err error) {
|
||||
func GetUserLogs(userId int, logType int, startTimestamp int64, endTimestamp int64, modelName string, tokenName string, startIdx int, num int, group string) (logs []*Log, total int64, err error) {
|
||||
var tx *gorm.DB
|
||||
if logType == LogTypeUnknown {
|
||||
tx = LOG_DB.Where("user_id = ?", userId)
|
||||
@@ -160,6 +177,9 @@ func GetUserLogs(userId int, logType int, startTimestamp int64, endTimestamp int
|
||||
if endTimestamp != 0 {
|
||||
tx = tx.Where("created_at <= ?", endTimestamp)
|
||||
}
|
||||
if group != "" {
|
||||
tx = tx.Where(groupCol+" = ?", group)
|
||||
}
|
||||
err = tx.Model(&Log{}).Count(&total).Error
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
@@ -193,7 +213,7 @@ type Stat struct {
|
||||
Tpm int `json:"tpm"`
|
||||
}
|
||||
|
||||
func SumUsedQuota(logType int, startTimestamp int64, endTimestamp int64, modelName string, username string, tokenName string, channel int) (stat Stat) {
|
||||
func SumUsedQuota(logType int, startTimestamp int64, endTimestamp int64, modelName string, username string, tokenName string, channel int, group string) (stat Stat) {
|
||||
tx := LOG_DB.Table("logs").Select("sum(quota) quota")
|
||||
|
||||
// 为rpm和tpm创建单独的查询
|
||||
@@ -221,6 +241,10 @@ func SumUsedQuota(logType int, startTimestamp int64, endTimestamp int64, modelNa
|
||||
tx = tx.Where("channel_id = ?", channel)
|
||||
rpmTpmQuery = rpmTpmQuery.Where("channel_id = ?", channel)
|
||||
}
|
||||
if group != "" {
|
||||
tx = tx.Where(groupCol+" = ?", group)
|
||||
rpmTpmQuery = rpmTpmQuery.Where(groupCol+" = ?", group)
|
||||
}
|
||||
|
||||
tx = tx.Where("type = ?", LogTypeConsume)
|
||||
rpmTpmQuery = rpmTpmQuery.Where("type = ?", LogTypeConsume)
|
||||
|
||||
@@ -2,7 +2,7 @@ package model
|
||||
|
||||
import (
|
||||
"one-api/common"
|
||||
"one-api/constant"
|
||||
"one-api/setting"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -61,16 +61,16 @@ func InitOptionMap() {
|
||||
common.OptionMap["SystemName"] = common.SystemName
|
||||
common.OptionMap["Logo"] = common.Logo
|
||||
common.OptionMap["ServerAddress"] = ""
|
||||
common.OptionMap["WorkerUrl"] = constant.WorkerUrl
|
||||
common.OptionMap["WorkerValidKey"] = constant.WorkerValidKey
|
||||
common.OptionMap["WorkerUrl"] = setting.WorkerUrl
|
||||
common.OptionMap["WorkerValidKey"] = setting.WorkerValidKey
|
||||
common.OptionMap["PayAddress"] = ""
|
||||
common.OptionMap["CustomCallbackAddress"] = ""
|
||||
common.OptionMap["EpayId"] = ""
|
||||
common.OptionMap["EpayKey"] = ""
|
||||
common.OptionMap["Price"] = strconv.FormatFloat(constant.Price, 'f', -1, 64)
|
||||
common.OptionMap["MinTopUp"] = strconv.Itoa(constant.MinTopUp)
|
||||
common.OptionMap["Price"] = strconv.FormatFloat(setting.Price, 'f', -1, 64)
|
||||
common.OptionMap["MinTopUp"] = strconv.Itoa(setting.MinTopUp)
|
||||
common.OptionMap["TopupGroupRatio"] = common.TopupGroupRatio2JSONString()
|
||||
common.OptionMap["Chats"] = constant.Chats2JsonString()
|
||||
common.OptionMap["Chats"] = setting.Chats2JsonString()
|
||||
common.OptionMap["GitHubClientId"] = ""
|
||||
common.OptionMap["GitHubClientSecret"] = ""
|
||||
common.OptionMap["TelegramBotToken"] = ""
|
||||
@@ -87,8 +87,8 @@ func InitOptionMap() {
|
||||
common.OptionMap["PreConsumedQuota"] = strconv.Itoa(common.PreConsumedQuota)
|
||||
common.OptionMap["ModelRatio"] = common.ModelRatio2JSONString()
|
||||
common.OptionMap["ModelPrice"] = common.ModelPrice2JSONString()
|
||||
common.OptionMap["GroupRatio"] = common.GroupRatio2JSONString()
|
||||
common.OptionMap["UserUsableGroups"] = common.UserUsableGroups2JSONString()
|
||||
common.OptionMap["GroupRatio"] = setting.GroupRatio2JSONString()
|
||||
common.OptionMap["UserUsableGroups"] = setting.UserUsableGroups2JSONString()
|
||||
common.OptionMap["CompletionRatio"] = common.CompletionRatio2JSONString()
|
||||
common.OptionMap["TopUpLink"] = common.TopUpLink
|
||||
common.OptionMap["ChatLink"] = common.ChatLink
|
||||
@@ -98,17 +98,17 @@ func InitOptionMap() {
|
||||
common.OptionMap["DataExportInterval"] = strconv.Itoa(common.DataExportInterval)
|
||||
common.OptionMap["DataExportDefaultTime"] = common.DataExportDefaultTime
|
||||
common.OptionMap["DefaultCollapseSidebar"] = strconv.FormatBool(common.DefaultCollapseSidebar)
|
||||
common.OptionMap["MjNotifyEnabled"] = strconv.FormatBool(constant.MjNotifyEnabled)
|
||||
common.OptionMap["MjAccountFilterEnabled"] = strconv.FormatBool(constant.MjAccountFilterEnabled)
|
||||
common.OptionMap["MjModeClearEnabled"] = strconv.FormatBool(constant.MjModeClearEnabled)
|
||||
common.OptionMap["MjForwardUrlEnabled"] = strconv.FormatBool(constant.MjForwardUrlEnabled)
|
||||
common.OptionMap["MjActionCheckSuccessEnabled"] = strconv.FormatBool(constant.MjActionCheckSuccessEnabled)
|
||||
common.OptionMap["CheckSensitiveEnabled"] = strconv.FormatBool(constant.CheckSensitiveEnabled)
|
||||
common.OptionMap["CheckSensitiveOnPromptEnabled"] = strconv.FormatBool(constant.CheckSensitiveOnPromptEnabled)
|
||||
common.OptionMap["MjNotifyEnabled"] = strconv.FormatBool(setting.MjNotifyEnabled)
|
||||
common.OptionMap["MjAccountFilterEnabled"] = strconv.FormatBool(setting.MjAccountFilterEnabled)
|
||||
common.OptionMap["MjModeClearEnabled"] = strconv.FormatBool(setting.MjModeClearEnabled)
|
||||
common.OptionMap["MjForwardUrlEnabled"] = strconv.FormatBool(setting.MjForwardUrlEnabled)
|
||||
common.OptionMap["MjActionCheckSuccessEnabled"] = strconv.FormatBool(setting.MjActionCheckSuccessEnabled)
|
||||
common.OptionMap["CheckSensitiveEnabled"] = strconv.FormatBool(setting.CheckSensitiveEnabled)
|
||||
common.OptionMap["CheckSensitiveOnPromptEnabled"] = strconv.FormatBool(setting.CheckSensitiveOnPromptEnabled)
|
||||
//common.OptionMap["CheckSensitiveOnCompletionEnabled"] = strconv.FormatBool(constant.CheckSensitiveOnCompletionEnabled)
|
||||
common.OptionMap["StopOnSensitiveEnabled"] = strconv.FormatBool(constant.StopOnSensitiveEnabled)
|
||||
common.OptionMap["SensitiveWords"] = constant.SensitiveWordsToString()
|
||||
common.OptionMap["StreamCacheQueueLength"] = strconv.Itoa(constant.StreamCacheQueueLength)
|
||||
common.OptionMap["StopOnSensitiveEnabled"] = strconv.FormatBool(setting.StopOnSensitiveEnabled)
|
||||
common.OptionMap["SensitiveWords"] = setting.SensitiveWordsToString()
|
||||
common.OptionMap["StreamCacheQueueLength"] = strconv.Itoa(setting.StreamCacheQueueLength)
|
||||
|
||||
common.OptionMapRWMutex.Unlock()
|
||||
loadOptionsFromDatabase()
|
||||
@@ -209,23 +209,23 @@ func updateOptionMap(key string, value string) (err error) {
|
||||
case "DefaultCollapseSidebar":
|
||||
common.DefaultCollapseSidebar = boolValue
|
||||
case "MjNotifyEnabled":
|
||||
constant.MjNotifyEnabled = boolValue
|
||||
setting.MjNotifyEnabled = boolValue
|
||||
case "MjAccountFilterEnabled":
|
||||
constant.MjAccountFilterEnabled = boolValue
|
||||
setting.MjAccountFilterEnabled = boolValue
|
||||
case "MjModeClearEnabled":
|
||||
constant.MjModeClearEnabled = boolValue
|
||||
setting.MjModeClearEnabled = boolValue
|
||||
case "MjForwardUrlEnabled":
|
||||
constant.MjForwardUrlEnabled = boolValue
|
||||
setting.MjForwardUrlEnabled = boolValue
|
||||
case "MjActionCheckSuccessEnabled":
|
||||
constant.MjActionCheckSuccessEnabled = boolValue
|
||||
setting.MjActionCheckSuccessEnabled = boolValue
|
||||
case "CheckSensitiveEnabled":
|
||||
constant.CheckSensitiveEnabled = boolValue
|
||||
setting.CheckSensitiveEnabled = boolValue
|
||||
case "CheckSensitiveOnPromptEnabled":
|
||||
constant.CheckSensitiveOnPromptEnabled = boolValue
|
||||
setting.CheckSensitiveOnPromptEnabled = boolValue
|
||||
//case "CheckSensitiveOnCompletionEnabled":
|
||||
// constant.CheckSensitiveOnCompletionEnabled = boolValue
|
||||
case "StopOnSensitiveEnabled":
|
||||
constant.StopOnSensitiveEnabled = boolValue
|
||||
setting.StopOnSensitiveEnabled = boolValue
|
||||
case "SMTPSSLEnabled":
|
||||
common.SMTPSSLEnabled = boolValue
|
||||
}
|
||||
@@ -245,25 +245,25 @@ func updateOptionMap(key string, value string) (err error) {
|
||||
case "SMTPToken":
|
||||
common.SMTPToken = value
|
||||
case "ServerAddress":
|
||||
constant.ServerAddress = value
|
||||
setting.ServerAddress = value
|
||||
case "WorkerUrl":
|
||||
constant.WorkerUrl = value
|
||||
setting.WorkerUrl = value
|
||||
case "WorkerValidKey":
|
||||
constant.WorkerValidKey = value
|
||||
setting.WorkerValidKey = value
|
||||
case "PayAddress":
|
||||
constant.PayAddress = value
|
||||
setting.PayAddress = value
|
||||
case "Chats":
|
||||
err = constant.UpdateChatsByJsonString(value)
|
||||
err = setting.UpdateChatsByJsonString(value)
|
||||
case "CustomCallbackAddress":
|
||||
constant.CustomCallbackAddress = value
|
||||
setting.CustomCallbackAddress = value
|
||||
case "EpayId":
|
||||
constant.EpayId = value
|
||||
setting.EpayId = value
|
||||
case "EpayKey":
|
||||
constant.EpayKey = value
|
||||
setting.EpayKey = value
|
||||
case "Price":
|
||||
constant.Price, _ = strconv.ParseFloat(value, 64)
|
||||
setting.Price, _ = strconv.ParseFloat(value, 64)
|
||||
case "MinTopUp":
|
||||
constant.MinTopUp, _ = strconv.Atoi(value)
|
||||
setting.MinTopUp, _ = strconv.Atoi(value)
|
||||
case "TopupGroupRatio":
|
||||
err = common.UpdateTopupGroupRatioByJSONString(value)
|
||||
case "GitHubClientId":
|
||||
@@ -313,9 +313,9 @@ func updateOptionMap(key string, value string) (err error) {
|
||||
case "ModelRatio":
|
||||
err = common.UpdateModelRatioByJSONString(value)
|
||||
case "GroupRatio":
|
||||
err = common.UpdateGroupRatioByJSONString(value)
|
||||
err = setting.UpdateGroupRatioByJSONString(value)
|
||||
case "UserUsableGroups":
|
||||
err = common.UpdateUserUsableGroupsByJSONString(value)
|
||||
err = setting.UpdateUserUsableGroupsByJSONString(value)
|
||||
case "CompletionRatio":
|
||||
err = common.UpdateCompletionRatioByJSONString(value)
|
||||
case "ModelPrice":
|
||||
@@ -331,9 +331,9 @@ func updateOptionMap(key string, value string) (err error) {
|
||||
case "QuotaPerUnit":
|
||||
common.QuotaPerUnit, _ = strconv.ParseFloat(value, 64)
|
||||
case "SensitiveWords":
|
||||
constant.SensitiveWordsFromString(value)
|
||||
setting.SensitiveWordsFromString(value)
|
||||
case "StreamCacheQueueLength":
|
||||
constant.StreamCacheQueueLength, _ = strconv.Atoi(value)
|
||||
setting.StreamCacheQueueLength, _ = strconv.Atoi(value)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"fmt"
|
||||
"gorm.io/gorm"
|
||||
"one-api/common"
|
||||
"one-api/constant"
|
||||
relaycommon "one-api/relay/common"
|
||||
"one-api/setting"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
@@ -325,7 +325,7 @@ func PostConsumeTokenQuota(relayInfo *relaycommon.RelayInfo, userQuota int, quot
|
||||
prompt = "您的额度已用尽"
|
||||
}
|
||||
if email != "" {
|
||||
topUpLink := fmt.Sprintf("%s/topup", constant.ServerAddress)
|
||||
topUpLink := fmt.Sprintf("%s/topup", setting.ServerAddress)
|
||||
err = common.SendEmail(prompt, email,
|
||||
fmt.Sprintf("%s,当前剩余额度为 %d,为了不影响您的使用,请及时充值。<br/>充值链接:<a href='%s'>%s</a>", prompt, userQuota, topUpLink, topUpLink))
|
||||
if err != nil {
|
||||
|
||||
@@ -89,26 +89,31 @@ func SearchUsers(keyword string, group string) ([]*User, error) {
|
||||
var users []*User
|
||||
var err error
|
||||
|
||||
groupCol := "`group`"
|
||||
if common.UsingPostgreSQL {
|
||||
groupCol = `"group"`
|
||||
}
|
||||
|
||||
// 尝试将关键字转换为整数ID
|
||||
keywordInt, err := strconv.Atoi(keyword)
|
||||
if err == nil {
|
||||
// 如果转换成功,按照ID和可选的组别搜索用户
|
||||
query := DB.Unscoped().Omit("password").Where("`id` = ?", keywordInt)
|
||||
query := DB.Unscoped().Omit("password").Where("id = ?", keywordInt)
|
||||
if group != "" {
|
||||
query = query.Where("`group` = ?", group) // 使用反引号包围group
|
||||
query = query.Where(groupCol+" = ?", group) // 使用反引号包围group
|
||||
}
|
||||
err = query.Find(&users).Error
|
||||
if err != nil || len(users) > 0 {
|
||||
return users, err
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
err = nil
|
||||
|
||||
query := DB.Unscoped().Omit("password")
|
||||
likeCondition := "`username` LIKE ? OR `email` LIKE ? OR `display_name` LIKE ?"
|
||||
likeCondition := "username LIKE ? OR email LIKE ? OR display_name LIKE ?"
|
||||
if group != "" {
|
||||
query = query.Where("("+likeCondition+") AND `group` = ?", "%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%", group)
|
||||
query = query.Where("("+likeCondition+") AND "+groupCol+" = ?", "%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%", group)
|
||||
} else {
|
||||
query = query.Where(likeCondition, "%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%")
|
||||
}
|
||||
|
||||
@@ -240,14 +240,7 @@ func RequestOpenAI2ClaudeMessage(textRequest dto.GeneralOpenAIRequest) (*ClaudeR
|
||||
claudeMediaMessages = append(claudeMediaMessages, claudeMediaMessage)
|
||||
}
|
||||
if message.ToolCalls != nil {
|
||||
for _, tc := range message.ToolCalls.([]interface{}) {
|
||||
toolCallJSON, _ := json.Marshal(tc)
|
||||
var toolCall dto.ToolCall
|
||||
err := json.Unmarshal(toolCallJSON, &toolCall)
|
||||
if err != nil {
|
||||
common.SysError("tool call is not a dto.ToolCall: " + fmt.Sprintf("%v", tc))
|
||||
continue
|
||||
}
|
||||
for _, toolCall := range message.ParseToolCalls() {
|
||||
inputObj := make(map[string]any)
|
||||
if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &inputObj); err != nil {
|
||||
common.SysError("tool call function arguments is not a map[string]any: " + fmt.Sprintf("%v", toolCall.Function.Arguments))
|
||||
@@ -393,7 +386,7 @@ func ResponseClaude2OpenAI(reqMode int, claudeResponse *ClaudeResponse) *dto.Ope
|
||||
}
|
||||
choice.SetStringContent(responseText)
|
||||
if len(tools) > 0 {
|
||||
choice.Message.ToolCalls = tools
|
||||
choice.Message.SetToolCalls(tools)
|
||||
}
|
||||
fullTextResponse.Model = claudeResponse.Model
|
||||
choices = append(choices, choice)
|
||||
|
||||
@@ -57,7 +57,11 @@ func (a *Adaptor) ConvertRequest(c *gin.Context, info *relaycommon.RelayInfo, re
|
||||
if request == nil {
|
||||
return nil, errors.New("request is nil")
|
||||
}
|
||||
return CovertGemini2OpenAI(*request), nil
|
||||
ai, err := CovertGemini2OpenAI(*request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ai, nil
|
||||
}
|
||||
|
||||
func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dto.RerankRequest) (any, error) {
|
||||
@@ -72,7 +76,7 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycom
|
||||
if info.IsStream {
|
||||
err, usage = GeminiChatStreamHandler(c, resp, info)
|
||||
} else {
|
||||
err, usage = GeminiChatHandler(c, resp)
|
||||
err, usage = GeminiChatHandler(c, resp, info)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
package gemini
|
||||
|
||||
const (
|
||||
GeminiVisionMaxImageNum = 16
|
||||
)
|
||||
|
||||
var ModelList = []string{
|
||||
"gemini-1.5-pro-latest", "gemini-1.5-flash-latest", "gemini-ultra",
|
||||
// stable version
|
||||
"gemini-1.5-pro", "gemini-1.5-flash", "gemini-1.5-flash-8b",
|
||||
// latest version
|
||||
"gemini-1.5-pro-latest", "gemini-1.5-flash-latest",
|
||||
// legacy version
|
||||
"gemini-1.5-pro-exp-0827", "gemini-1.5-flash-exp-0827",
|
||||
"gemini-exp-1114", "gemini-exp-1206",
|
||||
// exp
|
||||
"gemini-exp-1114", "gemini-exp-1121", "gemini-exp-1206",
|
||||
// flash exp
|
||||
"gemini-2.0-flash-exp",
|
||||
// thinking exp
|
||||
"gemini-2.0-flash-thinking-exp",
|
||||
"gemini-2.0-flash-thinking-exp-1219",
|
||||
}
|
||||
|
||||
var ChannelName = "google gemini"
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package gemini
|
||||
|
||||
type GeminiChatRequest struct {
|
||||
Contents []GeminiChatContent `json:"contents"`
|
||||
SafetySettings []GeminiChatSafetySettings `json:"safety_settings,omitempty"`
|
||||
GenerationConfig GeminiChatGenerationConfig `json:"generation_config,omitempty"`
|
||||
Tools []GeminiChatTools `json:"tools,omitempty"`
|
||||
Contents []GeminiChatContent `json:"contents"`
|
||||
SafetySettings []GeminiChatSafetySettings `json:"safety_settings,omitempty"`
|
||||
GenerationConfig GeminiChatGenerationConfig `json:"generation_config,omitempty"`
|
||||
Tools []GeminiChatTool `json:"tools,omitempty"`
|
||||
SystemInstructions *GeminiChatContent `json:"system_instruction,omitempty"`
|
||||
}
|
||||
|
||||
type GeminiInlineData struct {
|
||||
@@ -17,10 +18,39 @@ type FunctionCall struct {
|
||||
Arguments any `json:"args"`
|
||||
}
|
||||
|
||||
type GeminiFunctionResponseContent struct {
|
||||
Name string `json:"name"`
|
||||
Content any `json:"content"`
|
||||
}
|
||||
|
||||
type FunctionResponse struct {
|
||||
Name string `json:"name"`
|
||||
Response GeminiFunctionResponseContent `json:"response"`
|
||||
}
|
||||
|
||||
type GeminiPartExecutableCode struct {
|
||||
Language string `json:"language,omitempty"`
|
||||
Code string `json:"code,omitempty"`
|
||||
}
|
||||
|
||||
type GeminiPartCodeExecutionResult struct {
|
||||
Outcome string `json:"outcome,omitempty"`
|
||||
Output string `json:"output,omitempty"`
|
||||
}
|
||||
|
||||
type GeminiFileData struct {
|
||||
MimeType string `json:"mimeType,omitempty"`
|
||||
FileUri string `json:"fileUri,omitempty"`
|
||||
}
|
||||
|
||||
type GeminiPart struct {
|
||||
Text string `json:"text,omitempty"`
|
||||
InlineData *GeminiInlineData `json:"inlineData,omitempty"`
|
||||
FunctionCall *FunctionCall `json:"functionCall,omitempty"`
|
||||
Text string `json:"text,omitempty"`
|
||||
InlineData *GeminiInlineData `json:"inlineData,omitempty"`
|
||||
FunctionCall *FunctionCall `json:"functionCall,omitempty"`
|
||||
FunctionResponse *FunctionResponse `json:"functionResponse,omitempty"`
|
||||
FileData *GeminiFileData `json:"fileData,omitempty"`
|
||||
ExecutableCode *GeminiPartExecutableCode `json:"executableCode,omitempty"`
|
||||
CodeExecutionResult *GeminiPartCodeExecutionResult `json:"codeExecutionResult,omitempty"`
|
||||
}
|
||||
|
||||
type GeminiChatContent struct {
|
||||
@@ -33,23 +63,28 @@ type GeminiChatSafetySettings struct {
|
||||
Threshold string `json:"threshold"`
|
||||
}
|
||||
|
||||
type GeminiChatTools struct {
|
||||
GoogleSearch any `json:"googleSearch,omitempty"`
|
||||
FunctionDeclarations any `json:"functionDeclarations,omitempty"`
|
||||
type GeminiChatTool struct {
|
||||
GoogleSearch any `json:"googleSearch,omitempty"`
|
||||
GoogleSearchRetrieval any `json:"googleSearchRetrieval,omitempty"`
|
||||
CodeExecution any `json:"codeExecution,omitempty"`
|
||||
FunctionDeclarations any `json:"functionDeclarations,omitempty"`
|
||||
}
|
||||
|
||||
type GeminiChatGenerationConfig struct {
|
||||
Temperature float64 `json:"temperature,omitempty"`
|
||||
TopP float64 `json:"topP,omitempty"`
|
||||
TopK float64 `json:"topK,omitempty"`
|
||||
MaxOutputTokens uint `json:"maxOutputTokens,omitempty"`
|
||||
CandidateCount int `json:"candidateCount,omitempty"`
|
||||
StopSequences []string `json:"stopSequences,omitempty"`
|
||||
Temperature float64 `json:"temperature,omitempty"`
|
||||
TopP float64 `json:"topP,omitempty"`
|
||||
TopK float64 `json:"topK,omitempty"`
|
||||
MaxOutputTokens uint `json:"maxOutputTokens,omitempty"`
|
||||
CandidateCount int `json:"candidateCount,omitempty"`
|
||||
StopSequences []string `json:"stopSequences,omitempty"`
|
||||
ResponseMimeType string `json:"responseMimeType,omitempty"`
|
||||
ResponseSchema any `json:"responseSchema,omitempty"`
|
||||
Seed int64 `json:"seed,omitempty"`
|
||||
}
|
||||
|
||||
type GeminiChatCandidate struct {
|
||||
Content GeminiChatContent `json:"content"`
|
||||
FinishReason string `json:"finishReason"`
|
||||
FinishReason *string `json:"finishReason"`
|
||||
Index int64 `json:"index"`
|
||||
SafetyRatings []GeminiChatSafetyRating `json:"safetyRatings"`
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"io"
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
@@ -13,10 +12,13 @@ import (
|
||||
relaycommon "one-api/relay/common"
|
||||
"one-api/service"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Setting safety to the lowest possible values since Gemini is already powerless enough
|
||||
func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest) *GeminiChatRequest {
|
||||
func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest) (*GeminiChatRequest, error) {
|
||||
|
||||
geminiRequest := GeminiChatRequest{
|
||||
Contents: make([]GeminiChatContent, 0, len(textRequest.Messages)),
|
||||
SafetySettings: []GeminiChatSafetySettings{
|
||||
@@ -36,65 +38,156 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest) *GeminiChatReques
|
||||
Category: "HARM_CATEGORY_DANGEROUS_CONTENT",
|
||||
Threshold: common.GeminiSafetySetting,
|
||||
},
|
||||
{
|
||||
Category: "HARM_CATEGORY_CIVIC_INTEGRITY",
|
||||
Threshold: common.GeminiSafetySetting,
|
||||
},
|
||||
},
|
||||
GenerationConfig: GeminiChatGenerationConfig{
|
||||
Temperature: textRequest.Temperature,
|
||||
TopP: textRequest.TopP,
|
||||
MaxOutputTokens: textRequest.MaxTokens,
|
||||
Seed: int64(textRequest.Seed),
|
||||
},
|
||||
}
|
||||
|
||||
// openaiContent.FuncToToolCalls()
|
||||
if textRequest.Tools != nil {
|
||||
functions := make([]dto.FunctionCall, 0, len(textRequest.Tools))
|
||||
googleSearch := false
|
||||
codeExecution := false
|
||||
for _, tool := range textRequest.Tools {
|
||||
if tool.Function.Name == "googleSearch" {
|
||||
googleSearch = true
|
||||
continue
|
||||
}
|
||||
if tool.Function.Name == "codeExecution" {
|
||||
codeExecution = true
|
||||
continue
|
||||
}
|
||||
if tool.Function.Parameters != nil {
|
||||
params, ok := tool.Function.Parameters.(map[string]interface{})
|
||||
if ok {
|
||||
if props, hasProps := params["properties"].(map[string]interface{}); hasProps {
|
||||
if len(props) == 0 {
|
||||
tool.Function.Parameters = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
functions = append(functions, tool.Function)
|
||||
}
|
||||
if len(functions) > 0 {
|
||||
geminiRequest.Tools = []GeminiChatTools{
|
||||
{
|
||||
FunctionDeclarations: functions,
|
||||
},
|
||||
}
|
||||
if codeExecution {
|
||||
geminiRequest.Tools = append(geminiRequest.Tools, GeminiChatTool{
|
||||
CodeExecution: make(map[string]string),
|
||||
})
|
||||
}
|
||||
if googleSearch {
|
||||
geminiRequest.Tools = append(geminiRequest.Tools, GeminiChatTools{
|
||||
geminiRequest.Tools = append(geminiRequest.Tools, GeminiChatTool{
|
||||
GoogleSearch: make(map[string]string),
|
||||
})
|
||||
}
|
||||
if len(functions) > 0 {
|
||||
geminiRequest.Tools = append(geminiRequest.Tools, GeminiChatTool{
|
||||
FunctionDeclarations: functions,
|
||||
})
|
||||
}
|
||||
// common.SysLog("tools: " + fmt.Sprintf("%+v", geminiRequest.Tools))
|
||||
// json_data, _ := json.Marshal(geminiRequest.Tools)
|
||||
// common.SysLog("tools_json: " + string(json_data))
|
||||
} else if textRequest.Functions != nil {
|
||||
geminiRequest.Tools = []GeminiChatTools{
|
||||
geminiRequest.Tools = []GeminiChatTool{
|
||||
{
|
||||
FunctionDeclarations: textRequest.Functions,
|
||||
},
|
||||
}
|
||||
}
|
||||
shouldAddDummyModelMessage := false
|
||||
|
||||
if textRequest.ResponseFormat != nil && (textRequest.ResponseFormat.Type == "json_schema" || textRequest.ResponseFormat.Type == "json_object") {
|
||||
geminiRequest.GenerationConfig.ResponseMimeType = "application/json"
|
||||
|
||||
if textRequest.ResponseFormat.JsonSchema != nil && textRequest.ResponseFormat.JsonSchema.Schema != nil {
|
||||
cleanedSchema := removeAdditionalPropertiesWithDepth(textRequest.ResponseFormat.JsonSchema.Schema, 0)
|
||||
geminiRequest.GenerationConfig.ResponseSchema = cleanedSchema
|
||||
}
|
||||
}
|
||||
tool_call_ids := make(map[string]string)
|
||||
var system_content []string
|
||||
//shouldAddDummyModelMessage := false
|
||||
for _, message := range textRequest.Messages {
|
||||
if message.Role == "system" {
|
||||
system_content = append(system_content, message.StringContent())
|
||||
continue
|
||||
} else if message.Role == "tool" || message.Role == "function" {
|
||||
if len(geminiRequest.Contents) == 0 || geminiRequest.Contents[len(geminiRequest.Contents)-1].Role == "model" {
|
||||
geminiRequest.Contents = append(geminiRequest.Contents, GeminiChatContent{
|
||||
Role: "user",
|
||||
})
|
||||
}
|
||||
var parts = &geminiRequest.Contents[len(geminiRequest.Contents)-1].Parts
|
||||
name := ""
|
||||
if message.Name != nil {
|
||||
name = *message.Name
|
||||
} else if val, exists := tool_call_ids[message.ToolCallId]; exists {
|
||||
name = val
|
||||
}
|
||||
content := common.StrToMap(message.StringContent())
|
||||
functionResp := &FunctionResponse{
|
||||
Name: name,
|
||||
Response: GeminiFunctionResponseContent{
|
||||
Name: name,
|
||||
Content: content,
|
||||
},
|
||||
}
|
||||
if content == nil {
|
||||
functionResp.Response.Content = message.StringContent()
|
||||
}
|
||||
*parts = append(*parts, GeminiPart{
|
||||
FunctionResponse: functionResp,
|
||||
})
|
||||
continue
|
||||
}
|
||||
var parts []GeminiPart
|
||||
content := GeminiChatContent{
|
||||
Role: message.Role,
|
||||
Parts: []GeminiPart{
|
||||
{
|
||||
Text: message.StringContent(),
|
||||
},
|
||||
},
|
||||
}
|
||||
// isToolCall := false
|
||||
if message.ToolCalls != nil {
|
||||
// message.Role = "model"
|
||||
// isToolCall = true
|
||||
for _, call := range message.ParseToolCalls() {
|
||||
args := map[string]interface{}{}
|
||||
if call.Function.Arguments != "" {
|
||||
if json.Unmarshal([]byte(call.Function.Arguments), &args) != nil {
|
||||
return nil, fmt.Errorf("invalid arguments for function %s, args: %s", call.Function.Name, call.Function.Arguments)
|
||||
}
|
||||
}
|
||||
toolCall := GeminiPart{
|
||||
FunctionCall: &FunctionCall{
|
||||
FunctionName: call.Function.Name,
|
||||
Arguments: args,
|
||||
},
|
||||
}
|
||||
parts = append(parts, toolCall)
|
||||
tool_call_ids[call.ID] = call.Function.Name
|
||||
}
|
||||
}
|
||||
|
||||
openaiContent := message.ParseContent()
|
||||
var parts []GeminiPart
|
||||
imageNum := 0
|
||||
for _, part := range openaiContent {
|
||||
|
||||
if part.Type == dto.ContentTypeText {
|
||||
if part.Text == "" {
|
||||
continue
|
||||
}
|
||||
parts = append(parts, GeminiPart{
|
||||
Text: part.Text,
|
||||
})
|
||||
} else if part.Type == dto.ContentTypeImageURL {
|
||||
imageNum += 1
|
||||
if imageNum > GeminiVisionMaxImageNum {
|
||||
continue
|
||||
|
||||
if constant.GeminiVisionMaxImageNum != -1 && imageNum > constant.GeminiVisionMaxImageNum {
|
||||
return nil, fmt.Errorf("too many images in the message, max allowed is %d", constant.GeminiVisionMaxImageNum)
|
||||
}
|
||||
// 判断是否是url
|
||||
if strings.HasPrefix(part.ImageUrl.(dto.MessageImageUrl).Url, "http") {
|
||||
@@ -109,7 +202,7 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest) *GeminiChatReques
|
||||
} else {
|
||||
_, format, base64String, err := service.DecodeBase64ImageData(part.ImageUrl.(dto.MessageImageUrl).Url)
|
||||
if err != nil {
|
||||
continue
|
||||
return nil, fmt.Errorf("decode base64 image data failed: %s", err.Error())
|
||||
}
|
||||
parts = append(parts, GeminiPart{
|
||||
InlineData: &GeminiInlineData{
|
||||
@@ -120,58 +213,86 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest) *GeminiChatReques
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
content.Parts = parts
|
||||
|
||||
// there's no assistant role in gemini and API shall vomit if Role is not user or model
|
||||
if content.Role == "assistant" {
|
||||
content.Role = "model"
|
||||
}
|
||||
// Converting system prompt to prompt from user for the same reason
|
||||
if content.Role == "system" {
|
||||
content.Role = "user"
|
||||
shouldAddDummyModelMessage = true
|
||||
}
|
||||
geminiRequest.Contents = append(geminiRequest.Contents, content)
|
||||
}
|
||||
|
||||
// If a system message is the last message, we need to add a dummy model message to make gemini happy
|
||||
if shouldAddDummyModelMessage {
|
||||
geminiRequest.Contents = append(geminiRequest.Contents, GeminiChatContent{
|
||||
Role: "model",
|
||||
Parts: []GeminiPart{
|
||||
{
|
||||
Text: "Okay",
|
||||
},
|
||||
if len(system_content) > 0 {
|
||||
geminiRequest.SystemInstructions = &GeminiChatContent{
|
||||
Parts: []GeminiPart{
|
||||
{
|
||||
Text: strings.Join(system_content, "\n"),
|
||||
},
|
||||
})
|
||||
shouldAddDummyModelMessage = false
|
||||
},
|
||||
}
|
||||
}
|
||||
return &geminiRequest
|
||||
|
||||
return &geminiRequest, nil
|
||||
}
|
||||
|
||||
func (g *GeminiChatResponse) GetResponseText() string {
|
||||
if g == nil {
|
||||
return ""
|
||||
func removeAdditionalPropertiesWithDepth(schema interface{}, depth int) interface{} {
|
||||
if depth >= 5 {
|
||||
return schema
|
||||
}
|
||||
if len(g.Candidates) > 0 && len(g.Candidates[0].Content.Parts) > 0 {
|
||||
return g.Candidates[0].Content.Parts[0].Text
|
||||
|
||||
v, ok := schema.(map[string]interface{})
|
||||
if !ok || len(v) == 0 {
|
||||
return schema
|
||||
}
|
||||
return ""
|
||||
// 删除所有的title字段
|
||||
delete(v, "title")
|
||||
// 如果type不为object和array,则直接返回
|
||||
if typeVal, exists := v["type"]; !exists || (typeVal != "object" && typeVal != "array") {
|
||||
return schema
|
||||
}
|
||||
switch v["type"] {
|
||||
case "object":
|
||||
delete(v, "additionalProperties")
|
||||
// 处理 properties
|
||||
if properties, ok := v["properties"].(map[string]interface{}); ok {
|
||||
for key, value := range properties {
|
||||
properties[key] = removeAdditionalPropertiesWithDepth(value, depth+1)
|
||||
}
|
||||
}
|
||||
for _, field := range []string{"allOf", "anyOf", "oneOf"} {
|
||||
if nested, ok := v[field].([]interface{}); ok {
|
||||
for i, item := range nested {
|
||||
nested[i] = removeAdditionalPropertiesWithDepth(item, depth+1)
|
||||
}
|
||||
}
|
||||
}
|
||||
case "array":
|
||||
if items, ok := v["items"].(map[string]interface{}); ok {
|
||||
v["items"] = removeAdditionalPropertiesWithDepth(items, depth+1)
|
||||
}
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
func getToolCalls(candidate *GeminiChatCandidate) []dto.ToolCall {
|
||||
var toolCalls []dto.ToolCall
|
||||
// func (g *GeminiChatResponse) GetResponseText() string {
|
||||
// if g == nil {
|
||||
// return ""
|
||||
// }
|
||||
// if len(g.Candidates) > 0 && len(g.Candidates[0].Content.Parts) > 0 {
|
||||
// return g.Candidates[0].Content.Parts[0].Text
|
||||
// }
|
||||
// return ""
|
||||
// }
|
||||
|
||||
item := candidate.Content.Parts[0]
|
||||
if item.FunctionCall == nil {
|
||||
return toolCalls
|
||||
}
|
||||
func getToolCall(item *GeminiPart) *dto.ToolCall {
|
||||
argsBytes, err := json.Marshal(item.FunctionCall.Arguments)
|
||||
if err != nil {
|
||||
//common.SysError("getToolCalls failed: " + err.Error())
|
||||
return toolCalls
|
||||
//common.SysError("getToolCall failed: " + err.Error())
|
||||
return nil
|
||||
}
|
||||
toolCall := dto.ToolCall{
|
||||
return &dto.ToolCall{
|
||||
ID: fmt.Sprintf("call_%s", common.GetUUID()),
|
||||
Type: "function",
|
||||
Function: dto.FunctionCall{
|
||||
@@ -179,10 +300,32 @@ func getToolCalls(candidate *GeminiChatCandidate) []dto.ToolCall {
|
||||
Name: item.FunctionCall.FunctionName,
|
||||
},
|
||||
}
|
||||
toolCalls = append(toolCalls, toolCall)
|
||||
return toolCalls
|
||||
}
|
||||
|
||||
// func getToolCalls(candidate *GeminiChatCandidate, index int) []dto.ToolCall {
|
||||
// var toolCalls []dto.ToolCall
|
||||
|
||||
// item := candidate.Content.Parts[index]
|
||||
// if item.FunctionCall == nil {
|
||||
// return toolCalls
|
||||
// }
|
||||
// argsBytes, err := json.Marshal(item.FunctionCall.Arguments)
|
||||
// if err != nil {
|
||||
// //common.SysError("getToolCalls failed: " + err.Error())
|
||||
// return toolCalls
|
||||
// }
|
||||
// toolCall := dto.ToolCall{
|
||||
// ID: fmt.Sprintf("call_%s", common.GetUUID()),
|
||||
// Type: "function",
|
||||
// Function: dto.FunctionCall{
|
||||
// Arguments: string(argsBytes),
|
||||
// Name: item.FunctionCall.FunctionName,
|
||||
// },
|
||||
// }
|
||||
// toolCalls = append(toolCalls, toolCall)
|
||||
// return toolCalls
|
||||
// }
|
||||
|
||||
func responseGeminiChat2OpenAI(response *GeminiChatResponse) *dto.OpenAITextResponse {
|
||||
fullTextResponse := dto.OpenAITextResponse{
|
||||
Id: fmt.Sprintf("chatcmpl-%s", common.GetUUID()),
|
||||
@@ -191,9 +334,10 @@ func responseGeminiChat2OpenAI(response *GeminiChatResponse) *dto.OpenAITextResp
|
||||
Choices: make([]dto.OpenAITextResponseChoice, 0, len(response.Candidates)),
|
||||
}
|
||||
content, _ := json.Marshal("")
|
||||
for i, candidate := range response.Candidates {
|
||||
is_tool_call := false
|
||||
for _, candidate := range response.Candidates {
|
||||
choice := dto.OpenAITextResponseChoice{
|
||||
Index: i,
|
||||
Index: int(candidate.Index),
|
||||
Message: dto.Message{
|
||||
Role: "assistant",
|
||||
Content: content,
|
||||
@@ -201,40 +345,116 @@ func responseGeminiChat2OpenAI(response *GeminiChatResponse) *dto.OpenAITextResp
|
||||
FinishReason: constant.FinishReasonStop,
|
||||
}
|
||||
if len(candidate.Content.Parts) > 0 {
|
||||
if candidate.Content.Parts[0].FunctionCall != nil {
|
||||
choice.FinishReason = constant.FinishReasonToolCalls
|
||||
choice.Message.ToolCalls = getToolCalls(&candidate)
|
||||
} else {
|
||||
choice.Message.SetStringContent(candidate.Content.Parts[0].Text)
|
||||
var texts []string
|
||||
var tool_calls []dto.ToolCall
|
||||
for _, part := range candidate.Content.Parts {
|
||||
if part.FunctionCall != nil {
|
||||
choice.FinishReason = constant.FinishReasonToolCalls
|
||||
if call := getToolCall(&part); call != nil {
|
||||
tool_calls = append(tool_calls, *call)
|
||||
}
|
||||
} else {
|
||||
if part.ExecutableCode != nil {
|
||||
texts = append(texts, "```"+part.ExecutableCode.Language+"\n"+part.ExecutableCode.Code+"\n```")
|
||||
} else if part.CodeExecutionResult != nil {
|
||||
texts = append(texts, "```output\n"+part.CodeExecutionResult.Output+"\n```")
|
||||
} else {
|
||||
// 过滤掉空行
|
||||
if part.Text != "\n" {
|
||||
texts = append(texts, part.Text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(tool_calls) > 0 {
|
||||
choice.Message.SetToolCalls(tool_calls)
|
||||
is_tool_call = true
|
||||
}
|
||||
// 过滤掉空行
|
||||
|
||||
choice.Message.SetStringContent(strings.Join(texts, "\n"))
|
||||
|
||||
}
|
||||
if candidate.FinishReason != nil {
|
||||
switch *candidate.FinishReason {
|
||||
case "STOP":
|
||||
choice.FinishReason = constant.FinishReasonStop
|
||||
case "MAX_TOKENS":
|
||||
choice.FinishReason = constant.FinishReasonLength
|
||||
default:
|
||||
choice.FinishReason = constant.FinishReasonContentFilter
|
||||
}
|
||||
}
|
||||
if is_tool_call {
|
||||
choice.FinishReason = constant.FinishReasonToolCalls
|
||||
}
|
||||
|
||||
fullTextResponse.Choices = append(fullTextResponse.Choices, choice)
|
||||
}
|
||||
return &fullTextResponse
|
||||
}
|
||||
|
||||
func streamResponseGeminiChat2OpenAI(geminiResponse *GeminiChatResponse) *dto.ChatCompletionsStreamResponse {
|
||||
var choice dto.ChatCompletionsStreamResponseChoice
|
||||
//choice.Delta.SetContentString(geminiResponse.GetResponseText())
|
||||
if len(geminiResponse.Candidates) > 0 && len(geminiResponse.Candidates[0].Content.Parts) > 0 {
|
||||
respFirst := geminiResponse.Candidates[0].Content.Parts[0]
|
||||
if respFirst.FunctionCall != nil {
|
||||
// function response
|
||||
choice.Delta.ToolCalls = getToolCalls(&geminiResponse.Candidates[0])
|
||||
} else {
|
||||
// text response
|
||||
choice.Delta.SetContentString(respFirst.Text)
|
||||
func streamResponseGeminiChat2OpenAI(geminiResponse *GeminiChatResponse) (*dto.ChatCompletionsStreamResponse, bool) {
|
||||
choices := make([]dto.ChatCompletionsStreamResponseChoice, 0, len(geminiResponse.Candidates))
|
||||
is_stop := false
|
||||
for _, candidate := range geminiResponse.Candidates {
|
||||
if candidate.FinishReason != nil && *candidate.FinishReason == "STOP" {
|
||||
is_stop = true
|
||||
candidate.FinishReason = nil
|
||||
}
|
||||
choice := dto.ChatCompletionsStreamResponseChoice{
|
||||
Index: int(candidate.Index),
|
||||
Delta: dto.ChatCompletionsStreamResponseChoiceDelta{
|
||||
Role: "assistant",
|
||||
},
|
||||
}
|
||||
var texts []string
|
||||
isTools := false
|
||||
if candidate.FinishReason != nil {
|
||||
// p := GeminiConvertFinishReason(*candidate.FinishReason)
|
||||
switch *candidate.FinishReason {
|
||||
case "STOP":
|
||||
choice.FinishReason = &constant.FinishReasonStop
|
||||
case "MAX_TOKENS":
|
||||
choice.FinishReason = &constant.FinishReasonLength
|
||||
default:
|
||||
choice.FinishReason = &constant.FinishReasonContentFilter
|
||||
}
|
||||
}
|
||||
for _, part := range candidate.Content.Parts {
|
||||
if part.FunctionCall != nil {
|
||||
isTools = true
|
||||
if call := getToolCall(&part); call != nil {
|
||||
choice.Delta.ToolCalls = append(choice.Delta.ToolCalls, *call)
|
||||
}
|
||||
} else {
|
||||
if part.ExecutableCode != nil {
|
||||
texts = append(texts, "```"+part.ExecutableCode.Language+"\n"+part.ExecutableCode.Code+"\n```\n")
|
||||
} else if part.CodeExecutionResult != nil {
|
||||
texts = append(texts, "```output\n"+part.CodeExecutionResult.Output+"\n```\n")
|
||||
} else {
|
||||
if part.Text != "\n" {
|
||||
texts = append(texts, part.Text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
choice.Delta.SetContentString(strings.Join(texts, "\n"))
|
||||
if isTools {
|
||||
choice.FinishReason = &constant.FinishReasonToolCalls
|
||||
}
|
||||
choices = append(choices, choice)
|
||||
}
|
||||
|
||||
var response dto.ChatCompletionsStreamResponse
|
||||
response.Object = "chat.completion.chunk"
|
||||
response.Model = "gemini"
|
||||
response.Choices = []dto.ChatCompletionsStreamResponseChoice{choice}
|
||||
return &response
|
||||
response.Choices = choices
|
||||
return &response, is_stop
|
||||
}
|
||||
|
||||
func GeminiChatStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
|
||||
responseText := ""
|
||||
// responseText := ""
|
||||
id := fmt.Sprintf("chatcmpl-%s", common.GetUUID())
|
||||
createAt := common.GetTimestamp()
|
||||
var usage = &dto.Usage{}
|
||||
@@ -258,13 +478,11 @@ func GeminiChatStreamHandler(c *gin.Context, resp *http.Response, info *relaycom
|
||||
continue
|
||||
}
|
||||
|
||||
response := streamResponseGeminiChat2OpenAI(&geminiResponse)
|
||||
if response == nil {
|
||||
continue
|
||||
}
|
||||
response, is_stop := streamResponseGeminiChat2OpenAI(&geminiResponse)
|
||||
response.Id = id
|
||||
response.Created = createAt
|
||||
responseText += response.Choices[0].Delta.GetContentString()
|
||||
response.Model = info.UpstreamModelName
|
||||
// responseText += response.Choices[0].Delta.GetContentString()
|
||||
if geminiResponse.UsageMetadata.TotalTokenCount != 0 {
|
||||
usage.PromptTokens = geminiResponse.UsageMetadata.PromptTokenCount
|
||||
usage.CompletionTokens = geminiResponse.UsageMetadata.CandidatesTokenCount
|
||||
@@ -273,12 +491,17 @@ func GeminiChatStreamHandler(c *gin.Context, resp *http.Response, info *relaycom
|
||||
if err != nil {
|
||||
common.LogError(c, err.Error())
|
||||
}
|
||||
if is_stop {
|
||||
response := service.GenerateStopResponse(id, createAt, info.UpstreamModelName, constant.FinishReasonStop)
|
||||
service.ObjectData(c, response)
|
||||
}
|
||||
}
|
||||
|
||||
response := service.GenerateStopResponse(id, createAt, info.UpstreamModelName, constant.FinishReasonStop)
|
||||
service.ObjectData(c, response)
|
||||
var response *dto.ChatCompletionsStreamResponse
|
||||
|
||||
usage.TotalTokens = usage.PromptTokens + usage.CompletionTokens
|
||||
usage.PromptTokensDetails.TextTokens = usage.PromptTokens
|
||||
usage.CompletionTokenDetails.TextTokens = usage.CompletionTokens
|
||||
|
||||
if info.ShouldIncludeUsage {
|
||||
response = service.GenerateFinalUsageResponse(id, createAt, info.UpstreamModelName, *usage)
|
||||
@@ -292,7 +515,7 @@ func GeminiChatStreamHandler(c *gin.Context, resp *http.Response, info *relaycom
|
||||
return nil, usage
|
||||
}
|
||||
|
||||
func GeminiChatHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
|
||||
func GeminiChatHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
|
||||
responseBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
|
||||
@@ -318,6 +541,7 @@ func GeminiChatHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIErrorWit
|
||||
}, nil
|
||||
}
|
||||
fullTextResponse := responseGeminiChat2OpenAI(&geminiResponse)
|
||||
fullTextResponse.Model = info.UpstreamModelName
|
||||
usage := dto.Usage{
|
||||
PromptTokens: geminiResponse.UsageMetadata.PromptTokenCount,
|
||||
CompletionTokens: geminiResponse.UsageMetadata.CandidatesTokenCount,
|
||||
|
||||
@@ -106,15 +106,22 @@ func (a *Adaptor) ConvertRequest(c *gin.Context, info *relaycommon.RelayInfo, re
|
||||
if request == nil {
|
||||
return nil, errors.New("request is nil")
|
||||
}
|
||||
if info.ChannelType != common.ChannelTypeOpenAI {
|
||||
if info.ChannelType != common.ChannelTypeOpenAI && info.ChannelType != common.ChannelTypeAzure {
|
||||
request.StreamOptions = nil
|
||||
}
|
||||
if strings.HasPrefix(request.Model, "o1-") {
|
||||
if strings.HasPrefix(request.Model, "o1") {
|
||||
if request.MaxCompletionTokens == 0 && request.MaxTokens != 0 {
|
||||
request.MaxCompletionTokens = request.MaxTokens
|
||||
request.MaxTokens = 0
|
||||
}
|
||||
}
|
||||
if request.Model == "o1" || request.Model == "o1-2024-12-17" {
|
||||
//修改第一个Message的内容,将system改为developer
|
||||
if len(request.Messages) > 0 && request.Messages[0].Role == "system" {
|
||||
request.Messages[0].Role = "developer"
|
||||
}
|
||||
}
|
||||
|
||||
return request, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ var ModelList = []string{
|
||||
"gpt-4o-mini", "gpt-4o-mini-2024-07-18",
|
||||
"o1-preview", "o1-preview-2024-09-12",
|
||||
"o1-mini", "o1-mini-2024-09-12",
|
||||
"o1", "o1-2024-12-17",
|
||||
"gpt-4o-audio-preview", "gpt-4o-audio-preview-2024-10-01",
|
||||
"gpt-4o-realtime-preview", "gpt-4o-realtime-preview-2024-10-01",
|
||||
"text-embedding-ada-002", "text-embedding-3-small", "text-embedding-3-large",
|
||||
|
||||
@@ -5,9 +5,6 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/bytedance/gopkg/util/gopool"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
"io"
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
@@ -19,9 +16,33 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/gopkg/util/gopool"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
func sendStreamData(c *gin.Context, data string, forceFormat bool) error {
|
||||
if data == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if forceFormat {
|
||||
var lastStreamResponse dto.ChatCompletionsStreamResponse
|
||||
if err := json.Unmarshal(common.StringToByteSlice(data), &lastStreamResponse); err != nil {
|
||||
return err
|
||||
}
|
||||
return service.ObjectData(c, lastStreamResponse)
|
||||
}
|
||||
return service.StringData(c, data)
|
||||
}
|
||||
|
||||
func OaiStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
|
||||
if resp == nil || resp.Body == nil {
|
||||
common.LogError(c, "invalid response or response body")
|
||||
return service.OpenAIErrorWrapper(fmt.Errorf("invalid response"), "invalid_response", http.StatusInternalServerError), nil
|
||||
}
|
||||
|
||||
containStreamUsage := false
|
||||
var responseId string
|
||||
var createAt int64 = 0
|
||||
@@ -31,6 +52,13 @@ func OaiStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rel
|
||||
var responseTextBuilder strings.Builder
|
||||
var usage = &dto.Usage{}
|
||||
var streamItems []string // store stream items
|
||||
var forceFormat bool
|
||||
|
||||
if info.ChannelType == common.ChannelTypeCustom {
|
||||
if forceFmt, ok := info.ChannelSetting["force_format"].(bool); ok {
|
||||
forceFormat = forceFmt
|
||||
}
|
||||
}
|
||||
|
||||
toolCount := 0
|
||||
scanner := bufio.NewScanner(resp.Body)
|
||||
@@ -62,7 +90,7 @@ func OaiStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rel
|
||||
data = data[6:]
|
||||
if !strings.HasPrefix(data, "[DONE]") {
|
||||
if lastStreamData != "" {
|
||||
err := service.StringData(c, lastStreamData)
|
||||
err := sendStreamData(c, lastStreamData, forceFormat)
|
||||
if err != nil {
|
||||
common.LogError(c, "streaming error: "+err.Error())
|
||||
}
|
||||
@@ -105,7 +133,7 @@ func OaiStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rel
|
||||
}
|
||||
}
|
||||
if shouldSendLastResp {
|
||||
service.StringData(c, lastStreamData)
|
||||
sendStreamData(c, lastStreamData, forceFormat)
|
||||
}
|
||||
|
||||
// 计算token
|
||||
@@ -375,6 +403,10 @@ func getTextFromJSON(body []byte) (string, error) {
|
||||
}
|
||||
|
||||
func OpenaiRealtimeHandler(c *gin.Context, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.RealtimeUsage) {
|
||||
if info == nil || info.ClientWs == nil || info.TargetWs == nil {
|
||||
return service.OpenAIErrorWrapper(fmt.Errorf("invalid websocket connection"), "invalid_connection", http.StatusBadRequest), nil
|
||||
}
|
||||
|
||||
info.IsStream = true
|
||||
clientConn := info.ClientWs
|
||||
targetConn := info.TargetWs
|
||||
@@ -390,6 +422,11 @@ func OpenaiRealtimeHandler(c *gin.Context, info *relaycommon.RelayInfo) (*dto.Op
|
||||
sumUsage := &dto.RealtimeUsage{}
|
||||
|
||||
gopool.Go(func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
errChan <- fmt.Errorf("panic in client reader: %v", r)
|
||||
}
|
||||
}()
|
||||
for {
|
||||
select {
|
||||
case <-c.Done():
|
||||
@@ -445,6 +482,11 @@ func OpenaiRealtimeHandler(c *gin.Context, info *relaycommon.RelayInfo) (*dto.Op
|
||||
})
|
||||
|
||||
gopool.Go(func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
errChan <- fmt.Errorf("panic in target reader: %v", r)
|
||||
}
|
||||
}()
|
||||
for {
|
||||
select {
|
||||
case <-c.Done():
|
||||
@@ -568,6 +610,10 @@ func OpenaiRealtimeHandler(c *gin.Context, info *relaycommon.RelayInfo) (*dto.Op
|
||||
}
|
||||
|
||||
func preConsumeUsage(ctx *gin.Context, info *relaycommon.RelayInfo, usage *dto.RealtimeUsage, totalUsage *dto.RealtimeUsage) error {
|
||||
if usage == nil || totalUsage == nil {
|
||||
return fmt.Errorf("invalid usage pointer")
|
||||
}
|
||||
|
||||
totalUsage.TotalTokens += usage.TotalTokens
|
||||
totalUsage.InputTokens += usage.InputTokens
|
||||
totalUsage.OutputTokens += usage.OutputTokens
|
||||
|
||||
@@ -135,7 +135,10 @@ func (a *Adaptor) ConvertRequest(c *gin.Context, info *relaycommon.RelayInfo, re
|
||||
c.Set("request_model", request.Model)
|
||||
return vertexClaudeReq, nil
|
||||
} else if a.RequestMode == RequestModeGemini {
|
||||
geminiRequest := gemini.CovertGemini2OpenAI(*request)
|
||||
geminiRequest, err := gemini.CovertGemini2OpenAI(*request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.Set("request_model", request.Model)
|
||||
return geminiRequest, nil
|
||||
} else if a.RequestMode == RequestModeLlama {
|
||||
@@ -167,7 +170,7 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycom
|
||||
case RequestModeClaude:
|
||||
err, usage = claude.ClaudeHandler(c, resp, claude.RequestModeMessage, info)
|
||||
case RequestModeGemini:
|
||||
err, usage = gemini.GeminiChatHandler(c, resp)
|
||||
err, usage = gemini.GeminiChatHandler(c, resp, info)
|
||||
case RequestModeLlama:
|
||||
err, usage = openai.OpenaiHandler(c, resp, info.PromptTokens, info.OriginModelName)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
"one-api/common"
|
||||
"one-api/constant"
|
||||
"one-api/dto"
|
||||
"one-api/relay/constant"
|
||||
relayconstant "one-api/relay/constant"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
type RelayInfo struct {
|
||||
@@ -43,6 +45,7 @@ type RelayInfo struct {
|
||||
RealtimeTools []dto.RealTimeTool
|
||||
IsFirstRequest bool
|
||||
AudioUsage bool
|
||||
ChannelSetting map[string]interface{}
|
||||
}
|
||||
|
||||
func GenRelayInfoWs(c *gin.Context, ws *websocket.Conn) *RelayInfo {
|
||||
@@ -57,19 +60,20 @@ func GenRelayInfoWs(c *gin.Context, ws *websocket.Conn) *RelayInfo {
|
||||
func GenRelayInfo(c *gin.Context) *RelayInfo {
|
||||
channelType := c.GetInt("channel_type")
|
||||
channelId := c.GetInt("channel_id")
|
||||
channelSetting := c.GetStringMap("channel_setting")
|
||||
|
||||
tokenId := c.GetInt("token_id")
|
||||
tokenKey := c.GetString("token_key")
|
||||
userId := c.GetInt("id")
|
||||
group := c.GetString("group")
|
||||
tokenUnlimited := c.GetBool("token_unlimited_quota")
|
||||
startTime := time.Now()
|
||||
startTime := c.GetTime(constant.ContextKeyRequestStartTime)
|
||||
// firstResponseTime = time.Now() - 1 second
|
||||
|
||||
apiType, _ := constant.ChannelType2APIType(channelType)
|
||||
apiType, _ := relayconstant.ChannelType2APIType(channelType)
|
||||
|
||||
info := &RelayInfo{
|
||||
RelayMode: constant.Path2RelayMode(c.Request.URL.Path),
|
||||
RelayMode: relayconstant.Path2RelayMode(c.Request.URL.Path),
|
||||
BaseUrl: c.GetString("base_url"),
|
||||
RequestURLPath: c.Request.URL.String(),
|
||||
ChannelType: channelType,
|
||||
@@ -87,6 +91,7 @@ func GenRelayInfo(c *gin.Context) *RelayInfo {
|
||||
ApiVersion: c.GetString("api_version"),
|
||||
ApiKey: strings.TrimPrefix(c.Request.Header.Get("Authorization"), "Bearer "),
|
||||
Organization: c.GetString("channel_organization"),
|
||||
ChannelSetting: channelSetting,
|
||||
}
|
||||
if strings.HasPrefix(c.Request.URL.Path, "/pg") {
|
||||
info.IsPlayground = true
|
||||
@@ -104,7 +109,7 @@ func GenRelayInfo(c *gin.Context) *RelayInfo {
|
||||
}
|
||||
if info.ChannelType == common.ChannelTypeOpenAI || info.ChannelType == common.ChannelTypeAnthropic ||
|
||||
info.ChannelType == common.ChannelTypeAws || info.ChannelType == common.ChannelTypeGemini ||
|
||||
info.ChannelType == common.ChannelCloudflare {
|
||||
info.ChannelType == common.ChannelCloudflare || info.ChannelType == common.ChannelTypeAzure {
|
||||
info.SupportStreamOptions = true
|
||||
}
|
||||
return info
|
||||
@@ -154,10 +159,10 @@ func GenTaskRelayInfo(c *gin.Context) *TaskRelayInfo {
|
||||
group := c.GetString("group")
|
||||
startTime := time.Now()
|
||||
|
||||
apiType, _ := constant.ChannelType2APIType(channelType)
|
||||
apiType, _ := relayconstant.ChannelType2APIType(channelType)
|
||||
|
||||
info := &TaskRelayInfo{
|
||||
RelayMode: constant.Path2RelayMode(c.Request.URL.Path),
|
||||
RelayMode: relayconstant.Path2RelayMode(c.Request.URL.Path),
|
||||
BaseUrl: c.GetString("base_url"),
|
||||
RequestURLPath: c.Request.URL.String(),
|
||||
ChannelType: channelType,
|
||||
|
||||
@@ -7,12 +7,12 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
"one-api/constant"
|
||||
"one-api/dto"
|
||||
"one-api/model"
|
||||
relaycommon "one-api/relay/common"
|
||||
relayconstant "one-api/relay/constant"
|
||||
"one-api/service"
|
||||
"one-api/setting"
|
||||
)
|
||||
|
||||
func getAndValidAudioRequest(c *gin.Context, info *relaycommon.RelayInfo) (*dto.AudioRequest, error) {
|
||||
@@ -26,7 +26,7 @@ func getAndValidAudioRequest(c *gin.Context, info *relaycommon.RelayInfo) (*dto.
|
||||
if audioRequest.Model == "" {
|
||||
return nil, errors.New("model is required")
|
||||
}
|
||||
if constant.ShouldCheckPromptSensitive() {
|
||||
if setting.ShouldCheckPromptSensitive() {
|
||||
err := service.CheckSensitiveInput(audioRequest.Input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -74,7 +74,7 @@ func AudioHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
|
||||
}
|
||||
|
||||
modelRatio := common.GetModelRatio(audioRequest.Model)
|
||||
groupRatio := common.GetGroupRatio(relayInfo.Group)
|
||||
groupRatio := setting.GetGroupRatio(relayInfo.Group)
|
||||
ratio := modelRatio * groupRatio
|
||||
preConsumedQuota := int(float64(preConsumedTokens) * ratio)
|
||||
userQuota, err := model.CacheGetUserQuota(relayInfo.UserId)
|
||||
|
||||
@@ -9,11 +9,11 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
"one-api/constant"
|
||||
"one-api/dto"
|
||||
"one-api/model"
|
||||
relaycommon "one-api/relay/common"
|
||||
"one-api/service"
|
||||
"one-api/setting"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -59,7 +59,7 @@ func getAndValidImageRequest(c *gin.Context, info *relaycommon.RelayInfo) (*dto.
|
||||
//if imageRequest.N != 0 && (imageRequest.N < 1 || imageRequest.N > 10) {
|
||||
// return service.OpenAIErrorWrapper(errors.New("n must be between 1 and 10"), "invalid_field_value", http.StatusBadRequest)
|
||||
//}
|
||||
if constant.ShouldCheckPromptSensitive() {
|
||||
if setting.ShouldCheckPromptSensitive() {
|
||||
err := service.CheckSensitiveInput(imageRequest.Prompt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -99,7 +99,7 @@ func ImageHelper(c *gin.Context, relayMode int) *dto.OpenAIErrorWithStatusCode {
|
||||
modelPrice = 0.0025 * modelRatio
|
||||
}
|
||||
|
||||
groupRatio := common.GetGroupRatio(relayInfo.Group)
|
||||
groupRatio := setting.GetGroupRatio(relayInfo.Group)
|
||||
userQuota, err := model.CacheGetUserQuota(relayInfo.UserId)
|
||||
|
||||
sizeRatio := 1.0
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
relaycommon "one-api/relay/common"
|
||||
relayconstant "one-api/relay/constant"
|
||||
"one-api/service"
|
||||
"one-api/setting"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -111,8 +112,8 @@ func coverMidjourneyTaskDto(c *gin.Context, originTask *model.Midjourney) (midjo
|
||||
midjourneyTask.StartTime = originTask.StartTime
|
||||
midjourneyTask.FinishTime = originTask.FinishTime
|
||||
midjourneyTask.ImageUrl = ""
|
||||
if originTask.ImageUrl != "" && constant.MjForwardUrlEnabled {
|
||||
midjourneyTask.ImageUrl = constant.ServerAddress + "/mj/image/" + originTask.MjId
|
||||
if originTask.ImageUrl != "" && setting.MjForwardUrlEnabled {
|
||||
midjourneyTask.ImageUrl = setting.ServerAddress + "/mj/image/" + originTask.MjId
|
||||
if originTask.Status != "SUCCESS" {
|
||||
midjourneyTask.ImageUrl += "?rand=" + strconv.FormatInt(time.Now().UnixNano(), 10)
|
||||
}
|
||||
@@ -167,7 +168,7 @@ func RelaySwapFace(c *gin.Context) *dto.MidjourneyResponse {
|
||||
modelPrice = defaultPrice
|
||||
}
|
||||
}
|
||||
groupRatio := common.GetGroupRatio(group)
|
||||
groupRatio := setting.GetGroupRatio(group)
|
||||
ratio := modelPrice * groupRatio
|
||||
userQuota, err := model.CacheGetUserQuota(userId)
|
||||
if err != nil {
|
||||
@@ -207,7 +208,8 @@ func RelaySwapFace(c *gin.Context) *dto.MidjourneyResponse {
|
||||
other := make(map[string]interface{})
|
||||
other["model_price"] = modelPrice
|
||||
other["group_ratio"] = groupRatio
|
||||
model.RecordConsumeLog(ctx, userId, channelId, 0, 0, modelName, tokenName, quota, logContent, tokenId, userQuota, 0, false, other)
|
||||
model.RecordConsumeLog(ctx, userId, channelId, 0, 0, modelName, tokenName,
|
||||
quota, logContent, tokenId, userQuota, 0, false, group, other)
|
||||
model.UpdateUserUsedQuotaAndRequestCount(userId, quota)
|
||||
channelId := c.GetInt("channel_id")
|
||||
model.UpdateChannelUsedQuota(channelId, quota)
|
||||
@@ -421,7 +423,7 @@ func RelayMidjourneySubmit(c *gin.Context, relayMode int) *dto.MidjourneyRespons
|
||||
if originTask == nil {
|
||||
return service.MidjourneyErrorWrapper(constant.MjRequestError, "task_not_found")
|
||||
} else { //原任务的Status=SUCCESS,则可以做放大UPSCALE、变换VARIATION等动作,此时必须使用原来的请求地址才能正确处理
|
||||
if constant.MjActionCheckSuccessEnabled {
|
||||
if setting.MjActionCheckSuccessEnabled {
|
||||
if originTask.Status != "SUCCESS" && relayMode != relayconstant.RelayModeMidjourneyModal {
|
||||
return service.MidjourneyErrorWrapper(constant.MjRequestError, "task_status_not_success")
|
||||
}
|
||||
@@ -472,7 +474,7 @@ func RelayMidjourneySubmit(c *gin.Context, relayMode int) *dto.MidjourneyRespons
|
||||
modelPrice = defaultPrice
|
||||
}
|
||||
}
|
||||
groupRatio := common.GetGroupRatio(group)
|
||||
groupRatio := setting.GetGroupRatio(group)
|
||||
ratio := modelPrice * groupRatio
|
||||
userQuota, err := model.CacheGetUserQuota(userId)
|
||||
if err != nil {
|
||||
@@ -512,7 +514,8 @@ func RelayMidjourneySubmit(c *gin.Context, relayMode int) *dto.MidjourneyRespons
|
||||
other := make(map[string]interface{})
|
||||
other["model_price"] = modelPrice
|
||||
other["group_ratio"] = groupRatio
|
||||
model.RecordConsumeLog(ctx, userId, channelId, 0, 0, modelName, tokenName, quota, logContent, tokenId, userQuota, 0, false, other)
|
||||
model.RecordConsumeLog(ctx, userId, channelId, 0, 0, modelName, tokenName,
|
||||
quota, logContent, tokenId, userQuota, 0, false, group, other)
|
||||
model.UpdateUserUsedQuotaAndRequestCount(userId, quota)
|
||||
channelId := c.GetInt("channel_id")
|
||||
model.UpdateChannelUsedQuota(channelId, quota)
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
relaycommon "one-api/relay/common"
|
||||
relayconstant "one-api/relay/constant"
|
||||
"one-api/service"
|
||||
"one-api/setting"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -93,24 +94,31 @@ func TextHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
|
||||
}
|
||||
relayInfo.UpstreamModelName = textRequest.Model
|
||||
modelPrice, getModelPriceSuccess := common.GetModelPrice(textRequest.Model, false)
|
||||
groupRatio := common.GetGroupRatio(relayInfo.Group)
|
||||
groupRatio := setting.GetGroupRatio(relayInfo.Group)
|
||||
|
||||
var preConsumedQuota int
|
||||
var ratio float64
|
||||
var modelRatio float64
|
||||
//err := service.SensitiveWordsCheck(textRequest)
|
||||
|
||||
if constant.ShouldCheckPromptSensitive() {
|
||||
if setting.ShouldCheckPromptSensitive() {
|
||||
err = checkRequestSensitive(textRequest, relayInfo)
|
||||
if err != nil {
|
||||
return service.OpenAIErrorWrapperLocal(err, "sensitive_words_detected", http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
promptTokens, err := getPromptTokens(textRequest, relayInfo)
|
||||
// count messages token error 计算promptTokens错误
|
||||
if err != nil {
|
||||
return service.OpenAIErrorWrapper(err, "count_token_messages_failed", http.StatusInternalServerError)
|
||||
// 获取 promptTokens,如果上下文中已经存在,则直接使用
|
||||
var promptTokens int
|
||||
if value, exists := c.Get("prompt_tokens"); exists {
|
||||
promptTokens = value.(int)
|
||||
} else {
|
||||
promptTokens, err = getPromptTokens(textRequest, relayInfo)
|
||||
// count messages token error 计算promptTokens错误
|
||||
if err != nil {
|
||||
return service.OpenAIErrorWrapper(err, "count_token_messages_failed", http.StatusInternalServerError)
|
||||
}
|
||||
c.Set("prompt_tokens", promptTokens)
|
||||
}
|
||||
|
||||
if !getModelPriceSuccess {
|
||||
@@ -384,7 +392,7 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, modelN
|
||||
}
|
||||
other := service.GenerateTextOtherInfo(ctx, relayInfo, modelRatio, groupRatio, completionRatio, modelPrice)
|
||||
model.RecordConsumeLog(ctx, relayInfo.UserId, relayInfo.ChannelId, promptTokens, completionTokens, logModel,
|
||||
tokenName, quota, logContent, relayInfo.TokenId, userQuota, int(useTimeSeconds), relayInfo.IsStream, other)
|
||||
tokenName, quota, logContent, relayInfo.TokenId, userQuota, int(useTimeSeconds), relayInfo.IsStream, relayInfo.Group, other)
|
||||
|
||||
//if quota != 0 {
|
||||
//
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"one-api/dto"
|
||||
relaycommon "one-api/relay/common"
|
||||
"one-api/service"
|
||||
"one-api/setting"
|
||||
)
|
||||
|
||||
func getRerankPromptToken(rerankRequest dto.RerankRequest) int {
|
||||
@@ -57,7 +58,7 @@ func RerankHelper(c *gin.Context, relayMode int) (openaiErr *dto.OpenAIErrorWith
|
||||
|
||||
relayInfo.UpstreamModelName = rerankRequest.Model
|
||||
modelPrice, success := common.GetModelPrice(rerankRequest.Model, false)
|
||||
groupRatio := common.GetGroupRatio(relayInfo.Group)
|
||||
groupRatio := setting.GetGroupRatio(relayInfo.Group)
|
||||
|
||||
var preConsumedQuota int
|
||||
var ratio float64
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
relaycommon "one-api/relay/common"
|
||||
relayconstant "one-api/relay/constant"
|
||||
"one-api/service"
|
||||
"one-api/setting"
|
||||
)
|
||||
|
||||
/*
|
||||
@@ -48,7 +49,7 @@ func RelayTaskSubmit(c *gin.Context, relayMode int) (taskErr *dto.TaskError) {
|
||||
}
|
||||
|
||||
// 预扣
|
||||
groupRatio := common.GetGroupRatio(relayInfo.Group)
|
||||
groupRatio := setting.GetGroupRatio(relayInfo.Group)
|
||||
ratio := modelPrice * groupRatio
|
||||
userQuota, err := model.CacheGetUserQuota(relayInfo.UserId)
|
||||
if err != nil {
|
||||
@@ -126,7 +127,8 @@ func RelayTaskSubmit(c *gin.Context, relayMode int) (taskErr *dto.TaskError) {
|
||||
other := make(map[string]interface{})
|
||||
other["model_price"] = modelPrice
|
||||
other["group_ratio"] = groupRatio
|
||||
model.RecordConsumeLog(ctx, relayInfo.UserId, relayInfo.ChannelId, 0, 0, modelName, tokenName, quota, logContent, relayInfo.TokenId, userQuota, 0, false, other)
|
||||
model.RecordConsumeLog(ctx, relayInfo.UserId, relayInfo.ChannelId, 0, 0,
|
||||
modelName, tokenName, quota, logContent, relayInfo.TokenId, userQuota, 0, false, relayInfo.Group, other)
|
||||
model.UpdateUserUsedQuotaAndRequestCount(relayInfo.UserId, quota)
|
||||
model.UpdateChannelUsedQuota(relayInfo.ChannelId, quota)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"one-api/dto"
|
||||
relaycommon "one-api/relay/common"
|
||||
"one-api/service"
|
||||
"one-api/setting"
|
||||
)
|
||||
|
||||
//func getAndValidateWssRequest(c *gin.Context, ws *websocket.Conn) (*dto.RealtimeEvent, error) {
|
||||
@@ -57,7 +58,7 @@ func WssHelper(c *gin.Context, ws *websocket.Conn) (openaiErr *dto.OpenAIErrorWi
|
||||
}
|
||||
//relayInfo.UpstreamModelName = textRequest.Model
|
||||
modelPrice, getModelPriceSuccess := common.GetModelPrice(relayInfo.UpstreamModelName, false)
|
||||
groupRatio := common.GetGroupRatio(relayInfo.Group)
|
||||
groupRatio := setting.GetGroupRatio(relayInfo.Group)
|
||||
|
||||
var preConsumedQuota int
|
||||
var ratio float64
|
||||
|
||||
@@ -28,10 +28,10 @@ func SetApiRouter(router *gin.Engine) {
|
||||
apiRouter.GET("/oauth/linuxdo", middleware.CriticalRateLimit(), controller.LinuxdoOAuth)
|
||||
apiRouter.GET("/oauth/state", middleware.CriticalRateLimit(), controller.GenerateOAuthCode)
|
||||
apiRouter.GET("/oauth/wechat", middleware.CriticalRateLimit(), controller.WeChatAuth)
|
||||
apiRouter.GET("/oauth/wechat/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.WeChatBind)
|
||||
apiRouter.GET("/oauth/email/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.EmailBind)
|
||||
apiRouter.GET("/oauth/wechat/bind", middleware.CriticalRateLimit(), controller.WeChatBind)
|
||||
apiRouter.GET("/oauth/email/bind", middleware.CriticalRateLimit(), controller.EmailBind)
|
||||
apiRouter.GET("/oauth/telegram/login", middleware.CriticalRateLimit(), controller.TelegramLogin)
|
||||
apiRouter.GET("/oauth/telegram/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.TelegramBind)
|
||||
apiRouter.GET("/oauth/telegram/bind", middleware.CriticalRateLimit(), controller.TelegramBind)
|
||||
|
||||
userRoute := apiRouter.Group("/user")
|
||||
{
|
||||
@@ -98,7 +98,8 @@ func SetApiRouter(router *gin.Engine) {
|
||||
channelRoute.POST("/batch", controller.DeleteChannelBatch)
|
||||
channelRoute.POST("/fix", controller.FixChannelsAbilities)
|
||||
channelRoute.GET("/fetch_models/:id", controller.FetchUpstreamModels)
|
||||
|
||||
channelRoute.POST("/fetch_models", controller.FetchModels)
|
||||
channelRoute.POST("/batch/tag", controller.BatchSetChannelTag)
|
||||
}
|
||||
tokenRoute := apiRouter.Group("/token")
|
||||
tokenRoute.Use(middleware.UserAuth())
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"one-api/constant"
|
||||
"one-api/setting"
|
||||
)
|
||||
|
||||
func GetCallbackAddress() string {
|
||||
if constant.CustomCallbackAddress == "" {
|
||||
return constant.ServerAddress
|
||||
if setting.CustomCallbackAddress == "" {
|
||||
return setting.ServerAddress
|
||||
}
|
||||
return constant.CustomCallbackAddress
|
||||
return setting.CustomCallbackAddress
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"one-api/constant"
|
||||
"one-api/dto"
|
||||
relayconstant "one-api/relay/constant"
|
||||
"one-api/setting"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -167,16 +168,16 @@ func DoMidjourneyHttpRequest(c *gin.Context, timeout time.Duration, fullRequestU
|
||||
if err != nil {
|
||||
return MidjourneyErrorWithStatusCodeWrapper(constant.MjErrorUnknown, "read_request_body_failed", http.StatusInternalServerError), nullBytes, err
|
||||
}
|
||||
if !constant.MjAccountFilterEnabled {
|
||||
if !setting.MjAccountFilterEnabled {
|
||||
delete(mapResult, "accountFilter")
|
||||
}
|
||||
if !constant.MjNotifyEnabled {
|
||||
if !setting.MjNotifyEnabled {
|
||||
delete(mapResult, "notifyHook")
|
||||
}
|
||||
//req, err := http.NewRequest(c.Request.Method, fullRequestURL, requestBody)
|
||||
// make new request with mapResult
|
||||
}
|
||||
if constant.MjModeClearEnabled {
|
||||
if setting.MjModeClearEnabled {
|
||||
if prompt, ok := mapResult["prompt"].(string); ok {
|
||||
prompt = strings.Replace(prompt, "--fast", "", -1)
|
||||
prompt = strings.Replace(prompt, "--relax", "", -1)
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"one-api/dto"
|
||||
"one-api/model"
|
||||
relaycommon "one-api/relay/common"
|
||||
"one-api/setting"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -36,7 +37,7 @@ func PreWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usag
|
||||
completionRatio := common.GetCompletionRatio(modelName)
|
||||
audioRatio := common.GetAudioRatio(relayInfo.UpstreamModelName)
|
||||
audioCompletionRatio := common.GetAudioCompletionRatio(modelName)
|
||||
groupRatio := common.GetGroupRatio(relayInfo.Group)
|
||||
groupRatio := setting.GetGroupRatio(relayInfo.Group)
|
||||
modelRatio := common.GetModelRatio(modelName)
|
||||
|
||||
ratio := groupRatio * modelRatio
|
||||
@@ -139,7 +140,7 @@ func PostWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, mod
|
||||
}
|
||||
other := GenerateWssOtherInfo(ctx, relayInfo, usage, modelRatio, groupRatio, completionRatio, audioRatio, audioCompletionRatio, modelPrice)
|
||||
model.RecordConsumeLog(ctx, relayInfo.UserId, relayInfo.ChannelId, usage.InputTokens, usage.OutputTokens, logModel,
|
||||
tokenName, quota, logContent, relayInfo.TokenId, userQuota, int(useTimeSeconds), relayInfo.IsStream, other)
|
||||
tokenName, quota, logContent, relayInfo.TokenId, userQuota, int(useTimeSeconds), relayInfo.IsStream, relayInfo.Group, other)
|
||||
}
|
||||
|
||||
func PostAudioConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
|
||||
@@ -208,5 +209,5 @@ func PostAudioConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
|
||||
}
|
||||
other := GenerateAudioOtherInfo(ctx, relayInfo, usage, modelRatio, groupRatio, completionRatio, audioRatio, audioCompletionRatio, modelPrice)
|
||||
model.RecordConsumeLog(ctx, relayInfo.UserId, relayInfo.ChannelId, usage.PromptTokens, usage.CompletionTokens, logModel,
|
||||
tokenName, quota, logContent, relayInfo.TokenId, userQuota, int(useTimeSeconds), relayInfo.IsStream, other)
|
||||
tokenName, quota, logContent, relayInfo.TokenId, userQuota, int(useTimeSeconds), relayInfo.IsStream, relayInfo.Group, other)
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ package service
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"one-api/constant"
|
||||
"one-api/dto"
|
||||
"one-api/setting"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -56,7 +56,7 @@ func CheckSensitiveInput(input any) error {
|
||||
|
||||
// SensitiveWordContains 是否包含敏感词,返回是否包含敏感词和敏感词列表
|
||||
func SensitiveWordContains(text string) (bool, []string) {
|
||||
if len(constant.SensitiveWords) == 0 {
|
||||
if len(setting.SensitiveWords) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
checkText := strings.ToLower(text)
|
||||
@@ -75,7 +75,7 @@ func SensitiveWordContains(text string) (bool, []string) {
|
||||
|
||||
// SensitiveWordReplace 敏感词替换,返回是否包含敏感词和替换后的文本
|
||||
func SensitiveWordReplace(text string, returnImmediately bool) (bool, []string, string) {
|
||||
if len(constant.SensitiveWords) == 0 {
|
||||
if len(setting.SensitiveWords) == 0 {
|
||||
return false, nil, text
|
||||
}
|
||||
checkText := strings.ToLower(text)
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
goahocorasick "github.com/anknown/ahocorasick"
|
||||
"one-api/constant"
|
||||
"one-api/setting"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -70,7 +70,7 @@ func InitAc() *goahocorasick.Machine {
|
||||
func readRunes() [][]rune {
|
||||
var dict [][]rune
|
||||
|
||||
for _, word := range constant.SensitiveWords {
|
||||
for _, word := range setting.SensitiveWords {
|
||||
word = strings.ToLower(word)
|
||||
l := bytes.TrimSpace([]byte(word))
|
||||
dict = append(dict, bytes.Runes(l))
|
||||
|
||||
@@ -19,42 +19,40 @@ import (
|
||||
// tokenEncoderMap won't grow after initialization
|
||||
var tokenEncoderMap = map[string]*tiktoken.Tiktoken{}
|
||||
var defaultTokenEncoder *tiktoken.Tiktoken
|
||||
var cl200kTokenEncoder *tiktoken.Tiktoken
|
||||
var o200kTokenEncoder *tiktoken.Tiktoken
|
||||
|
||||
func InitTokenEncoders() {
|
||||
common.SysLog("initializing token encoders")
|
||||
gpt35TokenEncoder, err := tiktoken.EncodingForModel("gpt-3.5-turbo")
|
||||
cl100TokenEncoder, err := tiktoken.GetEncoding(tiktoken.MODEL_CL100K_BASE)
|
||||
if err != nil {
|
||||
common.FatalLog(fmt.Sprintf("failed to get gpt-3.5-turbo token encoder: %s", err.Error()))
|
||||
}
|
||||
defaultTokenEncoder = gpt35TokenEncoder
|
||||
gpt4TokenEncoder, err := tiktoken.EncodingForModel("gpt-4")
|
||||
if err != nil {
|
||||
common.FatalLog(fmt.Sprintf("failed to get gpt-4 token encoder: %s", err.Error()))
|
||||
}
|
||||
cl200kTokenEncoder, err = tiktoken.EncodingForModel("gpt-4o")
|
||||
defaultTokenEncoder = cl100TokenEncoder
|
||||
o200kTokenEncoder, err = tiktoken.GetEncoding(tiktoken.MODEL_O200K_BASE)
|
||||
if err != nil {
|
||||
common.FatalLog(fmt.Sprintf("failed to get gpt-4o token encoder: %s", err.Error()))
|
||||
}
|
||||
for model, _ := range common.GetDefaultModelRatioMap() {
|
||||
if strings.HasPrefix(model, "gpt-3.5") {
|
||||
tokenEncoderMap[model] = gpt35TokenEncoder
|
||||
tokenEncoderMap[model] = cl100TokenEncoder
|
||||
} else if strings.HasPrefix(model, "gpt-4") {
|
||||
if strings.HasPrefix(model, "gpt-4o") {
|
||||
tokenEncoderMap[model] = cl200kTokenEncoder
|
||||
tokenEncoderMap[model] = o200kTokenEncoder
|
||||
} else {
|
||||
tokenEncoderMap[model] = gpt4TokenEncoder
|
||||
tokenEncoderMap[model] = defaultTokenEncoder
|
||||
}
|
||||
} else if strings.HasPrefix(model, "o1") {
|
||||
tokenEncoderMap[model] = o200kTokenEncoder
|
||||
} else {
|
||||
tokenEncoderMap[model] = nil
|
||||
tokenEncoderMap[model] = defaultTokenEncoder
|
||||
}
|
||||
}
|
||||
common.SysLog("token encoders initialized")
|
||||
}
|
||||
|
||||
func getModelDefaultTokenEncoder(model string) *tiktoken.Tiktoken {
|
||||
if strings.HasPrefix(model, "gpt-4o") || strings.HasPrefix(model, "chatgpt-4o") {
|
||||
return cl200kTokenEncoder
|
||||
if strings.HasPrefix(model, "gpt-4o") || strings.HasPrefix(model, "chatgpt-4o") || strings.HasPrefix(model, "o1") {
|
||||
return o200kTokenEncoder
|
||||
}
|
||||
return defaultTokenEncoder
|
||||
}
|
||||
@@ -92,11 +90,11 @@ func getImageToken(imageUrl *dto.MessageImageUrl, model string, stream bool) (in
|
||||
}
|
||||
// TODO: 非流模式下不计算图片token数量
|
||||
if !constant.GetMediaTokenNotStream && !stream {
|
||||
return 1000, nil
|
||||
return 256, nil
|
||||
}
|
||||
// 是否统计图片token
|
||||
if !constant.GetMediaToken {
|
||||
return 1000, nil
|
||||
return 256, nil
|
||||
}
|
||||
// 同步One API的图片计费逻辑
|
||||
if imageUrl.Detail == "auto" || imageUrl.Detail == "" {
|
||||
|
||||
@@ -5,20 +5,20 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
"one-api/constant"
|
||||
"one-api/setting"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func DoImageRequest(originUrl string) (resp *http.Response, err error) {
|
||||
if constant.EnableWorker() {
|
||||
if setting.EnableWorker() {
|
||||
common.SysLog(fmt.Sprintf("downloading image from worker: %s", originUrl))
|
||||
workerUrl := constant.WorkerUrl
|
||||
workerUrl := setting.WorkerUrl
|
||||
if !strings.HasSuffix(workerUrl, "/") {
|
||||
workerUrl += "/"
|
||||
}
|
||||
// post request to worker
|
||||
data := []byte(`{"url":"` + originUrl + `","key":"` + constant.WorkerValidKey + `"}`)
|
||||
return http.Post(constant.WorkerUrl, "application/json", bytes.NewBuffer(data))
|
||||
data := []byte(`{"url":"` + originUrl + `","key":"` + setting.WorkerValidKey + `"}`)
|
||||
return http.Post(setting.WorkerUrl, "application/json", bytes.NewBuffer(data))
|
||||
} else {
|
||||
common.SysLog(fmt.Sprintf("downloading image from origin: %s", originUrl))
|
||||
return http.Get(originUrl)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package constant
|
||||
package setting
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -1,33 +1,47 @@
|
||||
package common
|
||||
package setting
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"one-api/common"
|
||||
)
|
||||
|
||||
var GroupRatio = map[string]float64{
|
||||
var groupRatio = map[string]float64{
|
||||
"default": 1,
|
||||
"vip": 1,
|
||||
"svip": 1,
|
||||
}
|
||||
|
||||
func GetGroupRatioCopy() map[string]float64 {
|
||||
groupRatioCopy := make(map[string]float64)
|
||||
for k, v := range groupRatio {
|
||||
groupRatioCopy[k] = v
|
||||
}
|
||||
return groupRatioCopy
|
||||
}
|
||||
|
||||
func ContainsGroupRatio(name string) bool {
|
||||
_, ok := groupRatio[name]
|
||||
return ok
|
||||
}
|
||||
|
||||
func GroupRatio2JSONString() string {
|
||||
jsonBytes, err := json.Marshal(GroupRatio)
|
||||
jsonBytes, err := json.Marshal(groupRatio)
|
||||
if err != nil {
|
||||
SysError("error marshalling model ratio: " + err.Error())
|
||||
common.SysError("error marshalling model ratio: " + err.Error())
|
||||
}
|
||||
return string(jsonBytes)
|
||||
}
|
||||
|
||||
func UpdateGroupRatioByJSONString(jsonStr string) error {
|
||||
GroupRatio = make(map[string]float64)
|
||||
return json.Unmarshal([]byte(jsonStr), &GroupRatio)
|
||||
groupRatio = make(map[string]float64)
|
||||
return json.Unmarshal([]byte(jsonStr), &groupRatio)
|
||||
}
|
||||
|
||||
func GetGroupRatio(name string) float64 {
|
||||
ratio, ok := GroupRatio[name]
|
||||
ratio, ok := groupRatio[name]
|
||||
if !ok {
|
||||
SysError("group ratio not found: " + name)
|
||||
common.SysError("group ratio not found: " + name)
|
||||
return 1
|
||||
}
|
||||
return ratio
|
||||
7
setting/midjourney.go
Normal file
7
setting/midjourney.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package setting
|
||||
|
||||
var MjNotifyEnabled = false
|
||||
var MjAccountFilterEnabled = false
|
||||
var MjModeClearEnabled = false
|
||||
var MjForwardUrlEnabled = true
|
||||
var MjActionCheckSuccessEnabled = true
|
||||
@@ -1,4 +1,4 @@
|
||||
package constant
|
||||
package setting
|
||||
|
||||
var PayAddress = ""
|
||||
var CustomCallbackAddress = ""
|
||||
@@ -1,4 +1,4 @@
|
||||
package constant
|
||||
package setting
|
||||
|
||||
import "strings"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package constant
|
||||
package setting
|
||||
|
||||
var ServerAddress = "http://localhost:3000"
|
||||
var WorkerUrl = ""
|
||||
@@ -1,7 +1,8 @@
|
||||
package common
|
||||
package setting
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"one-api/common"
|
||||
)
|
||||
|
||||
var UserUsableGroups = map[string]string{
|
||||
@@ -12,7 +13,7 @@ var UserUsableGroups = map[string]string{
|
||||
func UserUsableGroups2JSONString() string {
|
||||
jsonBytes, err := json.Marshal(UserUsableGroups)
|
||||
if err != nil {
|
||||
SysError("error marshalling user groups: " + err.Error())
|
||||
common.SysError("error marshalling user groups: " + err.Error())
|
||||
}
|
||||
return string(jsonBytes)
|
||||
}
|
||||
@@ -23,7 +23,10 @@
|
||||
"react-turnstile": "^1.0.5",
|
||||
"semantic-ui-offline": "^2.5.0",
|
||||
"semantic-ui-react": "^2.1.3",
|
||||
"sse": "github:mpetazzoni/sse.js"
|
||||
"sse": "github:mpetazzoni/sse.js",
|
||||
"i18next": "^23.16.8",
|
||||
"react-i18next": "^13.0.0",
|
||||
"i18next-browser-languagedetector": "^7.2.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
58
web/pnpm-lock.yaml
generated
58
web/pnpm-lock.yaml
generated
@@ -32,6 +32,12 @@ importers:
|
||||
history:
|
||||
specifier: ^5.3.0
|
||||
version: 5.3.0
|
||||
i18next:
|
||||
specifier: ^23.16.8
|
||||
version: 23.16.8
|
||||
i18next-browser-languagedetector:
|
||||
specifier: ^7.2.0
|
||||
version: 7.2.2
|
||||
marked:
|
||||
specifier: ^4.1.1
|
||||
version: 4.3.0
|
||||
@@ -47,6 +53,9 @@ importers:
|
||||
react-fireworks:
|
||||
specifier: ^1.0.4
|
||||
version: 1.0.4
|
||||
react-i18next:
|
||||
specifier: ^13.0.0
|
||||
version: 13.5.0(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
react-router-dom:
|
||||
specifier: ^6.3.0
|
||||
version: 6.28.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
@@ -1071,6 +1080,15 @@ packages:
|
||||
history@5.3.0:
|
||||
resolution: {integrity: sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==}
|
||||
|
||||
html-parse-stringify@3.0.1:
|
||||
resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
|
||||
|
||||
i18next-browser-languagedetector@7.2.2:
|
||||
resolution: {integrity: sha512-6b7r75uIJDWCcCflmbof+sJ94k9UQO4X0YR62oUfqGI/GjCLVzlCwu8TFdRZIqVLzWbzNcmkmhfqKEr4TLz4HQ==}
|
||||
|
||||
i18next@23.16.8:
|
||||
resolution: {integrity: sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg==}
|
||||
|
||||
iconv-lite@0.4.24:
|
||||
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -1487,6 +1505,19 @@ packages:
|
||||
react-fireworks@1.0.4:
|
||||
resolution: {integrity: sha512-jj1a+HTicB4pR6g2lqhVyAox0GTE0TOrZK2XaJFRYOwltgQWeYErZxnvU9+zH/blY+Hpmu9IKyb39OD3KcCMJw==}
|
||||
|
||||
react-i18next@13.5.0:
|
||||
resolution: {integrity: sha512-CFJ5NDGJ2MUyBohEHxljOq/39NQ972rh1ajnadG9BjTk+UXbHLq4z5DKEbEQBDoIhUmmbuS/fIMJKo6VOax1HA==}
|
||||
peerDependencies:
|
||||
i18next: '>= 23.2.3'
|
||||
react: '>= 16.8.0'
|
||||
react-dom: '*'
|
||||
react-native: '*'
|
||||
peerDependenciesMeta:
|
||||
react-dom:
|
||||
optional: true
|
||||
react-native:
|
||||
optional: true
|
||||
|
||||
react-is@16.13.1:
|
||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||
|
||||
@@ -1812,6 +1843,10 @@ packages:
|
||||
terser:
|
||||
optional: true
|
||||
|
||||
void-elements@3.1.0:
|
||||
resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
warning@4.0.3:
|
||||
resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==}
|
||||
|
||||
@@ -2935,6 +2970,18 @@ snapshots:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.0
|
||||
|
||||
html-parse-stringify@3.0.1:
|
||||
dependencies:
|
||||
void-elements: 3.1.0
|
||||
|
||||
i18next-browser-languagedetector@7.2.2:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.0
|
||||
|
||||
i18next@23.16.8:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.0
|
||||
|
||||
iconv-lite@0.4.24:
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
@@ -3611,6 +3658,15 @@ snapshots:
|
||||
|
||||
react-fireworks@1.0.4: {}
|
||||
|
||||
react-i18next@13.5.0(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.0
|
||||
html-parse-stringify: 3.0.1
|
||||
i18next: 23.16.8
|
||||
react: 18.3.1
|
||||
optionalDependencies:
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
|
||||
react-is@16.13.1: {}
|
||||
|
||||
react-is@18.3.1: {}
|
||||
@@ -3998,6 +4054,8 @@ snapshots:
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
||||
void-elements@3.1.0: {}
|
||||
|
||||
warning@4.0.3:
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
|
||||
@@ -26,38 +26,16 @@ import Pricing from './pages/Pricing/index.js';
|
||||
import Task from "./pages/Task/index.js";
|
||||
import Playground from './pages/Playground/Playground.js';
|
||||
import OAuth2Callback from "./components/OAuth2Callback.js";
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { StatusContext } from './context/Status';
|
||||
import { setStatusData } from './helpers/data.js';
|
||||
import { API, showError } from './helpers';
|
||||
|
||||
const Home = lazy(() => import('./pages/Home'));
|
||||
const Detail = lazy(() => import('./pages/Detail'));
|
||||
const About = lazy(() => import('./pages/About'));
|
||||
|
||||
function App() {
|
||||
const [userState, userDispatch] = useContext(UserContext);
|
||||
// const [statusState, statusDispatch] = useContext(StatusContext);
|
||||
|
||||
const loadUser = () => {
|
||||
let user = localStorage.getItem('user');
|
||||
if (user) {
|
||||
let data = JSON.parse(user);
|
||||
userDispatch({ type: 'login', payload: data });
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadUser();
|
||||
let systemName = getSystemName();
|
||||
if (systemName) {
|
||||
document.title = systemName;
|
||||
}
|
||||
let logo = getLogo();
|
||||
if (logo) {
|
||||
let linkElement = document.querySelector("link[rel~='icon']");
|
||||
if (linkElement) {
|
||||
linkElement.href = logo;
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Routes>
|
||||
|
||||
@@ -36,43 +36,111 @@ import { IconList, IconTreeTriangleDown } from '@douyinfe/semi-icons';
|
||||
import { loadChannelModels } from './utils.js';
|
||||
import EditTagModal from '../pages/Channel/EditTagModal.js';
|
||||
import TextNumberInput from './custom/TextNumberInput.js';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function renderTimestamp(timestamp) {
|
||||
return <>{timestamp2string(timestamp)}</>;
|
||||
}
|
||||
|
||||
let type2label = undefined;
|
||||
|
||||
function renderType(type) {
|
||||
if (!type2label) {
|
||||
type2label = new Map();
|
||||
for (let i = 0; i < CHANNEL_OPTIONS.length; i++) {
|
||||
type2label[CHANNEL_OPTIONS[i].value] = CHANNEL_OPTIONS[i];
|
||||
}
|
||||
type2label[0] = { value: 0, text: '未知类型', color: 'grey' };
|
||||
}
|
||||
return (
|
||||
<Tag size="large" color={type2label[type]?.color}>
|
||||
{type2label[type]?.text}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
function renderTagType(type) {
|
||||
return (
|
||||
<Tag
|
||||
color='light-blue'
|
||||
prefixIcon={<IconList />}
|
||||
size='large'
|
||||
shape='circle'
|
||||
type='light'
|
||||
>
|
||||
标签聚合
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
const ChannelsTable = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
let type2label = undefined;
|
||||
|
||||
const renderType = (type) => {
|
||||
if (!type2label) {
|
||||
type2label = new Map();
|
||||
for (let i = 0; i < CHANNEL_OPTIONS.length; i++) {
|
||||
type2label[CHANNEL_OPTIONS[i].value] = CHANNEL_OPTIONS[i];
|
||||
}
|
||||
type2label[0] = { value: 0, text: t('未知类型'), color: 'grey' };
|
||||
}
|
||||
return (
|
||||
<Tag size="large" color={type2label[type]?.color}>
|
||||
{type2label[type]?.text}
|
||||
</Tag>
|
||||
);
|
||||
};
|
||||
|
||||
const renderTagType = () => {
|
||||
return (
|
||||
<Tag
|
||||
color='light-blue'
|
||||
prefixIcon={<IconList />}
|
||||
size='large'
|
||||
shape='circle'
|
||||
type='light'
|
||||
>
|
||||
{t('标签聚合')}
|
||||
</Tag>
|
||||
);
|
||||
};
|
||||
|
||||
const renderStatus = (status) => {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return (
|
||||
<Tag size="large" color="green">
|
||||
{t('已启用')}
|
||||
</Tag>
|
||||
);
|
||||
case 2:
|
||||
return (
|
||||
<Tag size="large" color="yellow">
|
||||
{t('已禁用')}
|
||||
</Tag>
|
||||
);
|
||||
case 3:
|
||||
return (
|
||||
<Tag size="large" color="yellow">
|
||||
{t('自动禁用')}
|
||||
</Tag>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Tag size="large" color="grey">
|
||||
{t('未知状态')}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const renderResponseTime = (responseTime) => {
|
||||
let time = responseTime / 1000;
|
||||
time = time.toFixed(2) + t(' 秒');
|
||||
if (responseTime === 0) {
|
||||
return (
|
||||
<Tag size="large" color="grey">
|
||||
{t('未测试')}
|
||||
</Tag>
|
||||
);
|
||||
} else if (responseTime <= 1000) {
|
||||
return (
|
||||
<Tag size="large" color="green">
|
||||
{time}
|
||||
</Tag>
|
||||
);
|
||||
} else if (responseTime <= 3000) {
|
||||
return (
|
||||
<Tag size="large" color="lime">
|
||||
{time}
|
||||
</Tag>
|
||||
);
|
||||
} else if (responseTime <= 5000) {
|
||||
return (
|
||||
<Tag size="large" color="yellow">
|
||||
{time}
|
||||
</Tag>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Tag size="large" color="red">
|
||||
{time}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const columns = [
|
||||
// {
|
||||
// title: '',
|
||||
@@ -80,41 +148,47 @@ const ChannelsTable = () => {
|
||||
// className: 'checkbox',
|
||||
// },
|
||||
{
|
||||
title: 'ID',
|
||||
title: t('ID'),
|
||||
dataIndex: 'id'
|
||||
},
|
||||
{
|
||||
title: '名称',
|
||||
title: t('名称'),
|
||||
dataIndex: 'name'
|
||||
},
|
||||
{
|
||||
title: '分组',
|
||||
title: t('分组'),
|
||||
dataIndex: 'group',
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<div>
|
||||
<Space spacing={2}>
|
||||
{text?.split(',').map((item, index) => {
|
||||
return renderGroup(item);
|
||||
})}
|
||||
{text?.split(',')
|
||||
.sort((a, b) => {
|
||||
if (a === 'default') return -1;
|
||||
if (b === 'default') return 1;
|
||||
return a.localeCompare(b);
|
||||
})
|
||||
.map((item, index) => {
|
||||
return renderGroup(item);
|
||||
})}
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
title: t('类型'),
|
||||
dataIndex: 'type',
|
||||
render: (text, record, index) => {
|
||||
if (record.children === undefined) {
|
||||
return <>{renderType(text)}</>;
|
||||
} else {
|
||||
return <>{renderTagType(0)}</>;
|
||||
return <>{renderTagType()}</>;
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
title: t('状态'),
|
||||
dataIndex: 'status',
|
||||
render: (text, record, index) => {
|
||||
if (text === 3) {
|
||||
@@ -126,7 +200,7 @@ const ChannelsTable = () => {
|
||||
let time = otherInfo['status_time'];
|
||||
return (
|
||||
<div>
|
||||
<Tooltip content={'原因:' + reason + ',时间:' + timestamp2string(time)}>
|
||||
<Tooltip content={t('原因:') + reason + t(',时间:') + timestamp2string(time)}>
|
||||
{renderStatus(text)}
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -137,26 +211,26 @@ const ChannelsTable = () => {
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '响应时间',
|
||||
title: t('响应时间'),
|
||||
dataIndex: 'response_time',
|
||||
render: (text, record, index) => {
|
||||
return <div>{renderResponseTime(text)}</div>;
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '已用/剩余',
|
||||
title: t('已用/剩余'),
|
||||
dataIndex: 'expired_time',
|
||||
render: (text, record, index) => {
|
||||
if (record.children === undefined) {
|
||||
return (
|
||||
<div>
|
||||
<Space spacing={1}>
|
||||
<Tooltip content={'已用额度'}>
|
||||
<Tooltip content={t('已用额度')}>
|
||||
<Tag color="white" type="ghost" size="large">
|
||||
{renderQuota(record.used_quota)}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
<Tooltip content={'剩余额度' + record.balance + ',点击更新'}>
|
||||
<Tooltip content={t('剩余额度') + record.balance + t(',点击更新')}>
|
||||
<Tag
|
||||
color="white"
|
||||
type="ghost"
|
||||
@@ -172,7 +246,7 @@ const ChannelsTable = () => {
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return <Tooltip content={'已用额度'}>
|
||||
return <Tooltip content={t('已用额度')}>
|
||||
<Tag color="white" type="ghost" size="large">
|
||||
{renderQuota(record.used_quota)}
|
||||
</Tag>
|
||||
@@ -181,7 +255,7 @@ const ChannelsTable = () => {
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '优先级',
|
||||
title: t('优先级'),
|
||||
dataIndex: 'priority',
|
||||
render: (text, record, index) => {
|
||||
if (record.children === undefined) {
|
||||
@@ -208,8 +282,8 @@ const ChannelsTable = () => {
|
||||
keepFocus={true}
|
||||
onBlur={(e) => {
|
||||
Modal.warning({
|
||||
title: '修改子渠道优先级',
|
||||
content: '确定要修改所有子渠道优先级为 ' + e.target.value + ' 吗?',
|
||||
title: t('修改子渠道优先级'),
|
||||
content: t('确定要修改所有子渠道优先级为 ') + e.target.value + t(' 吗?'),
|
||||
onOk: () => {
|
||||
if (e.target.value === '') {
|
||||
return;
|
||||
@@ -230,7 +304,7 @@ const ChannelsTable = () => {
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '权重',
|
||||
title: t('权重'),
|
||||
dataIndex: 'weight',
|
||||
render: (text, record, index) => {
|
||||
if (record.children === undefined) {
|
||||
@@ -257,8 +331,8 @@ const ChannelsTable = () => {
|
||||
keepFocus={true}
|
||||
onBlur={(e) => {
|
||||
Modal.warning({
|
||||
title: '修改子渠道权重',
|
||||
content: '确定要修改所有子渠道权重为 ' + e.target.value + ' 吗?',
|
||||
title: t('修改子渠道权重'),
|
||||
content: t('确定要修改所有子渠道权重为 ') + e.target.value + t(' 吗?'),
|
||||
onOk: () => {
|
||||
if (e.target.value === '') {
|
||||
return;
|
||||
@@ -287,7 +361,7 @@ const ChannelsTable = () => {
|
||||
<div>
|
||||
<SplitButtonGroup
|
||||
style={{ marginRight: 1 }}
|
||||
aria-label="测试单个渠道操作项目组"
|
||||
aria-label={t('测试单个渠道操作项目组')}
|
||||
>
|
||||
<Button
|
||||
theme="light"
|
||||
@@ -295,7 +369,7 @@ const ChannelsTable = () => {
|
||||
testChannel(record, '');
|
||||
}}
|
||||
>
|
||||
测试
|
||||
{t('测试')}
|
||||
</Button>
|
||||
<Dropdown
|
||||
trigger="click"
|
||||
@@ -309,10 +383,9 @@ const ChannelsTable = () => {
|
||||
></Button>
|
||||
</Dropdown>
|
||||
</SplitButtonGroup>
|
||||
{/*<Button theme='light' type='primary' style={{marginRight: 1}} onClick={()=>testChannel(record)}>测试</Button>*/}
|
||||
<Popconfirm
|
||||
title="确定是否要删除此渠道?"
|
||||
content="此修改将不可逆"
|
||||
title={t('确定是否要删除此渠道?')}
|
||||
content={t('此修改将不可逆')}
|
||||
okType={'danger'}
|
||||
position={'left'}
|
||||
onConfirm={() => {
|
||||
@@ -322,7 +395,7 @@ const ChannelsTable = () => {
|
||||
}}
|
||||
>
|
||||
<Button theme="light" type="danger" style={{ marginRight: 1 }}>
|
||||
删除
|
||||
{t('删除')}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
{record.status === 1 ? (
|
||||
@@ -334,7 +407,7 @@ const ChannelsTable = () => {
|
||||
manageChannel(record.id, 'disable', record);
|
||||
}}
|
||||
>
|
||||
禁用
|
||||
{t('禁用')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
@@ -345,7 +418,7 @@ const ChannelsTable = () => {
|
||||
manageChannel(record.id, 'enable', record);
|
||||
}}
|
||||
>
|
||||
启用
|
||||
{t('启用')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
@@ -357,11 +430,11 @@ const ChannelsTable = () => {
|
||||
setShowEdit(true);
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
{t('编辑')}
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确定是否要复制此渠道?"
|
||||
content="复制渠道的所有信息"
|
||||
title={t('确定是否要复制此渠道?')}
|
||||
content={t('复制渠道的所有信息')}
|
||||
okType={'danger'}
|
||||
position={'left'}
|
||||
onConfirm={async () => {
|
||||
@@ -369,7 +442,7 @@ const ChannelsTable = () => {
|
||||
}}
|
||||
>
|
||||
<Button theme="light" type="primary" style={{ marginRight: 1 }}>
|
||||
复制
|
||||
{t('复制')}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
@@ -385,7 +458,7 @@ const ChannelsTable = () => {
|
||||
manageTag(record.key, 'enable');
|
||||
}}
|
||||
>
|
||||
启用全部
|
||||
{t('启用全部')}
|
||||
</Button>
|
||||
<Button
|
||||
theme="light"
|
||||
@@ -395,7 +468,7 @@ const ChannelsTable = () => {
|
||||
manageTag(record.key, 'disable');
|
||||
}}
|
||||
>
|
||||
禁用全部
|
||||
{t('禁用全部')}
|
||||
</Button>
|
||||
<Button
|
||||
theme="light"
|
||||
@@ -406,7 +479,7 @@ const ChannelsTable = () => {
|
||||
setEditingTag(record.key);
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
{t('编辑')}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
@@ -440,6 +513,8 @@ const ChannelsTable = () => {
|
||||
const [selectedChannels, setSelectedChannels] = useState([]);
|
||||
const [showEditPriority, setShowEditPriority] = useState(false);
|
||||
const [enableTagMode, setEnableTagMode] = useState(false);
|
||||
const [showBatchSetTag, setShowBatchSetTag] = useState(false);
|
||||
const [batchSetTagValue, setBatchSetTagValue] = useState('');
|
||||
|
||||
|
||||
const removeRecord = (record) => {
|
||||
@@ -579,25 +654,25 @@ const ChannelsTable = () => {
|
||||
|
||||
const copySelectedChannel = async (record) => {
|
||||
const channelToCopy = record
|
||||
channelToCopy.name += '_复制';
|
||||
channelToCopy.name += t('_复制');
|
||||
channelToCopy.created_time = null;
|
||||
channelToCopy.balance = 0;
|
||||
channelToCopy.used_quota = 0;
|
||||
if (!channelToCopy) {
|
||||
showError('渠道未找到,请刷新页面后重试。');
|
||||
showError(t('渠道未找到,请刷新页面后重试。'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const newChannel = { ...channelToCopy, id: undefined };
|
||||
const response = await API.post('/api/channel/', newChannel);
|
||||
if (response.data.success) {
|
||||
showSuccess('渠道复制成功');
|
||||
showSuccess(t('渠道复制成功'));
|
||||
await refresh();
|
||||
} else {
|
||||
showError(response.data.message);
|
||||
}
|
||||
} catch (error) {
|
||||
showError('渠道复制失败: ' + error.message);
|
||||
showError(t('渠道复制失败: ') + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -656,7 +731,7 @@ const ChannelsTable = () => {
|
||||
}
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
showSuccess('操作成功完成!');
|
||||
showSuccess(t('操作成功完成!'));
|
||||
let channel = res.data.data;
|
||||
let newChannels = [...channels];
|
||||
if (action === 'delete') {
|
||||
@@ -703,71 +778,6 @@ const ChannelsTable = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const renderStatus = (status) => {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return (
|
||||
<Tag size="large" color="green">
|
||||
已启用
|
||||
</Tag>
|
||||
);
|
||||
case 2:
|
||||
return (
|
||||
<Tag size="large" color="yellow">
|
||||
已禁用
|
||||
</Tag>
|
||||
);
|
||||
case 3:
|
||||
return (
|
||||
<Tag size="large" color="yellow">
|
||||
自动禁用
|
||||
</Tag>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Tag size="large" color="grey">
|
||||
未知状态
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const renderResponseTime = (responseTime) => {
|
||||
let time = responseTime / 1000;
|
||||
time = time.toFixed(2) + ' 秒';
|
||||
if (responseTime === 0) {
|
||||
return (
|
||||
<Tag size="large" color="grey">
|
||||
未测试
|
||||
</Tag>
|
||||
);
|
||||
} else if (responseTime <= 1000) {
|
||||
return (
|
||||
<Tag size="large" color="green">
|
||||
{time}
|
||||
</Tag>
|
||||
);
|
||||
} else if (responseTime <= 3000) {
|
||||
return (
|
||||
<Tag size="large" color="lime">
|
||||
{time}
|
||||
</Tag>
|
||||
);
|
||||
} else if (responseTime <= 5000) {
|
||||
return (
|
||||
<Tag size="large" color="yellow">
|
||||
{time}
|
||||
</Tag>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Tag size="large" color="red">
|
||||
{time}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const searchChannels = async (searchKeyword, searchGroup, searchModel, enableTagMode) => {
|
||||
if (searchKeyword === '' && searchGroup === '' && searchModel === '') {
|
||||
await loadChannels(0, pageSize, idSort, enableTagMode);
|
||||
@@ -794,7 +804,7 @@ const ChannelsTable = () => {
|
||||
if (success) {
|
||||
record.response_time = time * 1000;
|
||||
record.test_time = Date.now() / 1000;
|
||||
showInfo(`通道 ${record.name} 测试成功,耗时 ${time.toFixed(2)} 秒。`);
|
||||
showInfo(t('通道 ${name} 测试成功,耗时 ${time.toFixed(2)} 秒。').replace('${name}', record.name).replace('${time.toFixed(2)}', time.toFixed(2)));
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
@@ -804,7 +814,7 @@ const ChannelsTable = () => {
|
||||
const res = await API.get(`/api/channel/test`);
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
showInfo('已成功开始测试所有通道,请刷新页面查看结果。');
|
||||
showInfo(t('已成功开始测试所有已启用通道,请刷新页面查看结果。'));
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
@@ -814,7 +824,7 @@ const ChannelsTable = () => {
|
||||
const res = await API.delete(`/api/channel/disabled`);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
showSuccess(`已删除所有禁用渠道,共计 ${data} 个`);
|
||||
showSuccess(t('已删除所有禁用渠道,共计 ${data} 个').replace('${data}', data));
|
||||
await refresh();
|
||||
} else {
|
||||
showError(message);
|
||||
@@ -827,7 +837,7 @@ const ChannelsTable = () => {
|
||||
if (success) {
|
||||
record.balance = balance;
|
||||
record.balance_updated_time = Date.now() / 1000;
|
||||
showInfo(`通道 ${record.name} 余额更新成功!`);
|
||||
showInfo(t('通道 ${name} 余额更新成功!').replace('${name}', record.name));
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
@@ -838,7 +848,7 @@ const ChannelsTable = () => {
|
||||
const res = await API.get(`/api/channel/update_balance`);
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
showInfo('已更新完毕所有已启用通道余额!');
|
||||
showInfo(t('已更新完毕所有已启用通道余额!'));
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
@@ -847,7 +857,7 @@ const ChannelsTable = () => {
|
||||
|
||||
const batchDeleteChannels = async () => {
|
||||
if (selectedChannels.length === 0) {
|
||||
showError('请先选择要删除的通道!');
|
||||
showError(t('请先选择要删除的通道!'));
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
@@ -858,7 +868,7 @@ const ChannelsTable = () => {
|
||||
const res = await API.post(`/api/channel/batch`, { ids: ids });
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
showSuccess(`已删除 ${data} 个通道!`);
|
||||
showSuccess(t('已删除 ${data} 个通道!').replace('${data}', data));
|
||||
await refresh();
|
||||
} else {
|
||||
showError(message);
|
||||
@@ -870,7 +880,7 @@ const ChannelsTable = () => {
|
||||
const res = await API.post(`/api/channel/fix`);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
showSuccess(`已修复 ${data} 个通道!`);
|
||||
showSuccess(t('已修复 ${data} 个通道!').replace('${data}', data));
|
||||
await refresh();
|
||||
} else {
|
||||
showError(message);
|
||||
@@ -966,6 +976,29 @@ const ChannelsTable = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const batchSetChannelTag = async () => {
|
||||
if (selectedChannels.length === 0) {
|
||||
showError(t('请先选择要设置标签的渠道!'));
|
||||
return;
|
||||
}
|
||||
if (batchSetTagValue === '') {
|
||||
showError(t('标签不能为空!'));
|
||||
return;
|
||||
}
|
||||
let ids = selectedChannels.map(channel => channel.id);
|
||||
const res = await API.post('/api/channel/batch/tag', {
|
||||
ids: ids,
|
||||
tag: batchSetTagValue === '' ? null : batchSetTagValue
|
||||
});
|
||||
if (res.data.success) {
|
||||
showSuccess(t('已为 ${count} 个渠道设置标签!').replace('${count}', res.data.data));
|
||||
await refresh();
|
||||
setShowBatchSetTag(false);
|
||||
} else {
|
||||
showError(res.data.message);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<EditTagModal
|
||||
@@ -990,8 +1023,8 @@ const ChannelsTable = () => {
|
||||
<Space>
|
||||
<Form.Input
|
||||
field="search_keyword"
|
||||
label="搜索渠道关键词"
|
||||
placeholder="ID,名称和密钥 ..."
|
||||
label={t('搜索渠道关键词')}
|
||||
placeholder={t('搜索渠道的 ID,名称和密钥 ...')}
|
||||
value={searchKeyword}
|
||||
loading={searching}
|
||||
onChange={(v) => {
|
||||
@@ -1000,8 +1033,8 @@ const ChannelsTable = () => {
|
||||
/>
|
||||
<Form.Input
|
||||
field="search_model"
|
||||
label="模型"
|
||||
placeholder="模型关键字"
|
||||
label={t('模型')}
|
||||
placeholder={t('模型关键字')}
|
||||
value={searchModel}
|
||||
loading={searching}
|
||||
onChange={(v) => {
|
||||
@@ -1010,8 +1043,8 @@ const ChannelsTable = () => {
|
||||
/>
|
||||
<Form.Select
|
||||
field="group"
|
||||
label="分组"
|
||||
optionList={[{ label: '选择分组', value: null }, ...groupOptions]}
|
||||
label={t('分组')}
|
||||
optionList={[{ label: t('选择分组'), value: null }, ...groupOptions]}
|
||||
initValue={null}
|
||||
onChange={(v) => {
|
||||
setSearchGroup(v);
|
||||
@@ -1019,13 +1052,13 @@ const ChannelsTable = () => {
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
label="查询"
|
||||
label={t('查询')}
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
className="btn-margin-right"
|
||||
style={{ marginRight: 8 }}
|
||||
>
|
||||
查询
|
||||
{t('查询')}
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
@@ -1042,12 +1075,12 @@ const ChannelsTable = () => {
|
||||
<Space
|
||||
style={{ pointerEvents: 'auto', marginTop: isMobile() ? 0 : 45 }}
|
||||
>
|
||||
<Typography.Text strong>使用ID排序</Typography.Text>
|
||||
<Typography.Text strong>{t('使用ID排序')}</Typography.Text>
|
||||
<Switch
|
||||
checked={idSort}
|
||||
label="使用ID排序"
|
||||
uncheckedText="关"
|
||||
aria-label="是否用ID排序"
|
||||
label={t('使用ID排序')}
|
||||
uncheckedText={t('关')}
|
||||
aria-label={t('是否用ID排序')}
|
||||
onChange={(v) => {
|
||||
localStorage.setItem('id-sort', v + '');
|
||||
setIdSort(v);
|
||||
@@ -1069,35 +1102,35 @@ const ChannelsTable = () => {
|
||||
setShowEdit(true);
|
||||
}}
|
||||
>
|
||||
添加渠道
|
||||
{t('添加渠道')}
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确定?"
|
||||
title={t('确定?')}
|
||||
okType={'warning'}
|
||||
onConfirm={testAllChannels}
|
||||
position={isMobile() ? 'top' : 'top'}
|
||||
>
|
||||
<Button theme="light" type="warning" style={{ marginRight: 8 }}>
|
||||
测试所有通道
|
||||
{t('测试所有通道')}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
<Popconfirm
|
||||
title="确定?"
|
||||
title={t('确定?')}
|
||||
okType={'secondary'}
|
||||
onConfirm={updateAllChannelsBalance}
|
||||
>
|
||||
<Button theme="light" type="secondary" style={{ marginRight: 8 }}>
|
||||
更新所有已启用通道余额
|
||||
{t('更新所有已启用通道余额')}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
<Popconfirm
|
||||
title="确定是否要删除禁用通道?"
|
||||
content="此修改将不可逆"
|
||||
title={t('确定是否要删除禁用通道?')}
|
||||
content={t('此修改将不可逆')}
|
||||
okType={'danger'}
|
||||
onConfirm={deleteAllDisabledChannels}
|
||||
>
|
||||
<Button theme="light" type="danger" style={{ marginRight: 8 }}>
|
||||
删除禁用通道
|
||||
{t('删除禁用通道')}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
|
||||
@@ -1107,24 +1140,24 @@ const ChannelsTable = () => {
|
||||
style={{ marginRight: 8 }}
|
||||
onClick={refresh}
|
||||
>
|
||||
刷新
|
||||
{t('刷新')}
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<Space>
|
||||
<Typography.Text strong>开启批量删除</Typography.Text>
|
||||
<Typography.Text strong>{t('开启批量操作')}</Typography.Text>
|
||||
<Switch
|
||||
label="开启批量删除"
|
||||
uncheckedText="关"
|
||||
aria-label="是否开启批量删除"
|
||||
label={t('开启批量操作')}
|
||||
uncheckedText={t('关')}
|
||||
aria-label={t('是否开启批量操作')}
|
||||
onChange={(v) => {
|
||||
setEnableBatchDelete(v);
|
||||
}}
|
||||
></Switch>
|
||||
<Popconfirm
|
||||
title="确定是否要删除所选通道?"
|
||||
content="此修改将不可逆"
|
||||
title={t('确定是否要删除所选通道?')}
|
||||
content={t('此修改将不可逆')}
|
||||
okType={'danger'}
|
||||
onConfirm={batchDeleteChannels}
|
||||
disabled={!enableBatchDelete}
|
||||
@@ -1136,37 +1169,46 @@ const ChannelsTable = () => {
|
||||
type="danger"
|
||||
style={{ marginRight: 8 }}
|
||||
>
|
||||
删除所选通道
|
||||
{t('删除所选通道')}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
<Popconfirm
|
||||
title="确定是否要修复数据库一致性?"
|
||||
content="进行该操作时,可能导致渠道访问错误,请仅在数据库出现问题时使用"
|
||||
title={t('确定是否要修复数据库一致性?')}
|
||||
content={t('进行该操作时,可能导致渠道访问错误,请仅在数据库出现问题时使用')}
|
||||
okType={'warning'}
|
||||
onConfirm={fixChannelsAbilities}
|
||||
position={'top'}
|
||||
>
|
||||
<Button theme="light" type="secondary" style={{ marginRight: 8 }}>
|
||||
修复数据库一致性
|
||||
{t('修复数据库一致性')}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
</div>
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<Space>
|
||||
<Typography.Text strong>标签聚合模式</Typography.Text>
|
||||
<Typography.Text strong>{t('标签聚合模式')}</Typography.Text>
|
||||
<Switch
|
||||
checked={enableTagMode}
|
||||
label="标签聚合模式"
|
||||
uncheckedText="关"
|
||||
aria-label="是否启用标签聚合"
|
||||
label={t('标签聚合模式')}
|
||||
uncheckedText={t('关')}
|
||||
aria-label={t('是否启用标签聚合')}
|
||||
onChange={(v) => {
|
||||
setEnableTagMode(v);
|
||||
// 切换模式时重新加载数据
|
||||
loadChannels(0, pageSize, idSort, v);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
disabled={!enableBatchDelete}
|
||||
theme="light"
|
||||
type="primary"
|
||||
style={{ marginRight: 8 }}
|
||||
onClick={() => setShowBatchSetTag(true)}
|
||||
>
|
||||
{t('批量设置标签')}
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1200,6 +1242,23 @@ const ChannelsTable = () => {
|
||||
: null
|
||||
}
|
||||
/>
|
||||
<Modal
|
||||
title={t('批量设置标签')}
|
||||
visible={showBatchSetTag}
|
||||
onOk={batchSetChannelTag}
|
||||
onCancel={() => setShowBatchSetTag(false)}
|
||||
maskClosable={false}
|
||||
centered={true}
|
||||
>
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
<Typography.Text>{t('请输入要设置的标签名称')}</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
placeholder={t('请输入标签名称')}
|
||||
value={batchSetTagValue}
|
||||
onChange={(v) => setBatchSetTagValue(v)}
|
||||
/>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { getFooterHTML, getSystemName } from '../helpers';
|
||||
import { Layout, Tooltip } from '@douyinfe/semi-ui';
|
||||
|
||||
const FooterBar = () => {
|
||||
const { t } = useTranslation();
|
||||
const systemName = getSystemName();
|
||||
const [footer, setFooter] = useState(getFooterHTML());
|
||||
let remainCheckTimes = 5;
|
||||
@@ -24,7 +25,7 @@ const FooterBar = () => {
|
||||
>
|
||||
New API {import.meta.env.VITE_REACT_APP_VERSION}{' '}
|
||||
</a>
|
||||
由{' '}
|
||||
{t('由')}{' '}
|
||||
<a
|
||||
href='https://github.com/Calcium-Ion'
|
||||
target='_blank'
|
||||
@@ -32,7 +33,7 @@ const FooterBar = () => {
|
||||
>
|
||||
Calcium-Ion
|
||||
</a>{' '}
|
||||
开发,基于{' '}
|
||||
{t('开发,基于')}{' '}
|
||||
<a
|
||||
href='https://github.com/songquanpeng/one-api'
|
||||
target='_blank'
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useContext, useEffect, useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { UserContext } from '../context/User';
|
||||
import { useSetTheme, useTheme } from '../context/Theme';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { API, getLogo, getSystemName, isMobile, showSuccess } from '../helpers';
|
||||
import '../index.css';
|
||||
@@ -13,70 +14,58 @@ import {
|
||||
IconHelpCircle,
|
||||
IconHome,
|
||||
IconHomeStroked, IconIndentLeft,
|
||||
IconComment,
|
||||
IconKey, IconMenu,
|
||||
IconNoteMoneyStroked,
|
||||
IconPriceTag,
|
||||
IconUser
|
||||
IconUser,
|
||||
IconLanguage
|
||||
} from '@douyinfe/semi-icons';
|
||||
import { Avatar, Button, Dropdown, Layout, Nav, Switch } from '@douyinfe/semi-ui';
|
||||
import { stringToColor } from '../helpers/render';
|
||||
import Text from '@douyinfe/semi-ui/lib/es/typography/text';
|
||||
import { StyleContext } from '../context/Style/index.js';
|
||||
|
||||
// HeaderBar Buttons
|
||||
let headerButtons = [
|
||||
{
|
||||
text: '关于',
|
||||
itemKey: 'about',
|
||||
to: '/about',
|
||||
icon: <IconHelpCircle />,
|
||||
},
|
||||
];
|
||||
|
||||
if (localStorage.getItem('chat_link')) {
|
||||
headerButtons.splice(1, 0, {
|
||||
name: '聊天',
|
||||
to: '/chat',
|
||||
icon: 'comments',
|
||||
});
|
||||
}
|
||||
|
||||
const HeaderBar = () => {
|
||||
const { t, i18n } = useTranslation();
|
||||
const [userState, userDispatch] = useContext(UserContext);
|
||||
const [styleState, styleDispatch] = useContext(StyleContext);
|
||||
let navigate = useNavigate();
|
||||
const [currentLang, setCurrentLang] = useState(i18n.language);
|
||||
|
||||
const systemName = getSystemName();
|
||||
const logo = getLogo();
|
||||
const currentDate = new Date();
|
||||
// enable fireworks on new year(1.1 and 2.9-2.24)
|
||||
const isNewYear =
|
||||
(currentDate.getMonth() === 0 && currentDate.getDate() === 1) ||
|
||||
(currentDate.getMonth() === 1 &&
|
||||
currentDate.getDate() >= 9 &&
|
||||
currentDate.getDate() <= 24);
|
||||
(currentDate.getMonth() === 0 && currentDate.getDate() === 1);
|
||||
|
||||
let buttons = [
|
||||
{
|
||||
text: '首页',
|
||||
text: t('首页'),
|
||||
itemKey: 'home',
|
||||
to: '/',
|
||||
},
|
||||
{
|
||||
text: '控制台',
|
||||
text: t('控制台'),
|
||||
itemKey: 'detail',
|
||||
to: '/',
|
||||
},
|
||||
{
|
||||
text: '定价',
|
||||
text: t('定价'),
|
||||
itemKey: 'pricing',
|
||||
to: '/pricing',
|
||||
},
|
||||
{
|
||||
text: t('关于'),
|
||||
itemKey: 'about',
|
||||
to: '/about',
|
||||
},
|
||||
];
|
||||
|
||||
async function logout() {
|
||||
await API.get('/api/user/logout');
|
||||
showSuccess('注销成功!');
|
||||
showSuccess(t('注销成功!'));
|
||||
userDispatch({ type: 'logout' });
|
||||
localStorage.removeItem('user');
|
||||
navigate('/login');
|
||||
@@ -99,18 +88,46 @@ const HeaderBar = () => {
|
||||
useEffect(() => {
|
||||
if (theme === 'dark') {
|
||||
document.body.setAttribute('theme-mode', 'dark');
|
||||
} else {
|
||||
document.body.removeAttribute('theme-mode');
|
||||
}
|
||||
// 发送当前主题模式给子页面
|
||||
const iframe = document.querySelector('iframe');
|
||||
if (iframe) {
|
||||
iframe.contentWindow.postMessage({ themeMode: theme }, '*');
|
||||
}
|
||||
|
||||
if (isNewYear) {
|
||||
console.log('Happy New Year!');
|
||||
}
|
||||
}, []);
|
||||
}, [theme]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleLanguageChanged = (lng) => {
|
||||
setCurrentLang(lng);
|
||||
const iframe = document.querySelector('iframe');
|
||||
if (iframe) {
|
||||
iframe.contentWindow.postMessage({ lang: lng }, '*');
|
||||
}
|
||||
};
|
||||
|
||||
i18n.on('languageChanged', handleLanguageChanged);
|
||||
|
||||
return () => {
|
||||
i18n.off('languageChanged', handleLanguageChanged);
|
||||
};
|
||||
}, [i18n]);
|
||||
|
||||
const handleLanguageChange = (lang) => {
|
||||
i18n.changeLanguage(lang);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Layout>
|
||||
<div style={{ width: '100%' }}>
|
||||
<Nav
|
||||
className={'topnav'}
|
||||
mode={'horizontal'}
|
||||
renderWrapper={({ itemElement, isSubNav, isInSubNav, props }) => {
|
||||
const routerMap = {
|
||||
@@ -120,6 +137,7 @@ const HeaderBar = () => {
|
||||
pricing: '/pricing',
|
||||
detail: '/detail',
|
||||
home: '/',
|
||||
chat: '/chat',
|
||||
};
|
||||
return (
|
||||
<div onClick={(e) => {
|
||||
@@ -128,7 +146,9 @@ const HeaderBar = () => {
|
||||
styleDispatch({ type: 'SET_SIDER', payload: false });
|
||||
} else {
|
||||
styleDispatch({ type: 'SET_INNER_PADDING', payload: true });
|
||||
styleDispatch({ type: 'SET_SIDER', payload: true });
|
||||
if (!styleState.isMobile) {
|
||||
styleDispatch({ type: 'SET_SIDER', payload: true });
|
||||
}
|
||||
}
|
||||
}}>
|
||||
<Link
|
||||
@@ -149,10 +169,10 @@ const HeaderBar = () => {
|
||||
<>
|
||||
{
|
||||
!styleState.showSider ?
|
||||
<Button icon={<IconMenu />} theme="light" aria-label="展开侧边栏" onClick={
|
||||
<Button icon={<IconMenu />} theme="light" aria-label={t('展开侧边栏')} onClick={
|
||||
() => styleDispatch({ type: 'SET_SIDER', payload: true })
|
||||
} />:
|
||||
<Button icon={<IconIndentLeft />} theme="light" aria-label="关闭侧边栏" onClick={
|
||||
<Button icon={<IconIndentLeft />} theme="light" aria-label={t('闭侧边栏')} onClick={
|
||||
() => styleDispatch({ type: 'SET_SIDER', payload: false })
|
||||
} />
|
||||
}
|
||||
@@ -182,7 +202,7 @@ const HeaderBar = () => {
|
||||
<Nav.Item itemKey={'new-year'} text={'🏮'} />
|
||||
</Dropdown>
|
||||
)}
|
||||
<Nav.Item itemKey={'about'} icon={<IconHelpCircle />} />
|
||||
{/* <Nav.Item itemKey={'about'} icon={<IconHelpCircle />} /> */}
|
||||
<>
|
||||
<Switch
|
||||
checkedText='🌞'
|
||||
@@ -194,13 +214,37 @@ const HeaderBar = () => {
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
<Dropdown
|
||||
position='bottomRight'
|
||||
render={
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item
|
||||
onClick={() => handleLanguageChange('zh')}
|
||||
type={currentLang === 'zh' ? 'primary' : 'tertiary'}
|
||||
>
|
||||
中文
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
onClick={() => handleLanguageChange('en')}
|
||||
type={currentLang === 'en' ? 'primary' : 'tertiary'}
|
||||
>
|
||||
English
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
}
|
||||
>
|
||||
<Nav.Item
|
||||
itemKey={'language'}
|
||||
icon={<IconLanguage />}
|
||||
/>
|
||||
</Dropdown>
|
||||
{userState.user ? (
|
||||
<>
|
||||
<Dropdown
|
||||
position='bottomRight'
|
||||
render={
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item onClick={logout}>退出</Dropdown.Item>
|
||||
<Dropdown.Item onClick={logout}>{t('退出')}</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
}
|
||||
>
|
||||
@@ -218,14 +262,18 @@ const HeaderBar = () => {
|
||||
<>
|
||||
<Nav.Item
|
||||
itemKey={'login'}
|
||||
text={'登录'}
|
||||
// icon={<IconKey />}
|
||||
/>
|
||||
<Nav.Item
|
||||
itemKey={'register'}
|
||||
text={'注册'}
|
||||
text={!styleState.isMobile?t('登录'):null}
|
||||
icon={<IconUser />}
|
||||
/>
|
||||
{
|
||||
!styleState.isMobile && (
|
||||
<Nav.Item
|
||||
itemKey={'register'}
|
||||
text={t('注册')}
|
||||
icon={<IconKey />}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -28,6 +28,7 @@ import { IconGithubLogo, IconAlarm } from '@douyinfe/semi-icons';
|
||||
import WeChatIcon from './WeChatIcon';
|
||||
import { setUserData } from '../helpers/data.js';
|
||||
import LinuxDoIcon from './LinuxDoIcon.js';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const LoginForm = () => {
|
||||
const [inputs, setInputs] = useState({
|
||||
@@ -45,6 +46,7 @@ const LoginForm = () => {
|
||||
let navigate = useNavigate();
|
||||
const [status, setStatus] = useState({});
|
||||
const [showWeChatLoginModal, setShowWeChatLoginModal] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const logo = getLogo();
|
||||
|
||||
@@ -55,7 +57,7 @@ const LoginForm = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (searchParams.get('expired')) {
|
||||
showError('未登录或登录已过期,请重新登录!');
|
||||
showError(t('未登录或登录已过期,请重新登录'));
|
||||
}
|
||||
let status = localStorage.getItem('status');
|
||||
if (status) {
|
||||
@@ -182,20 +184,20 @@ const LoginForm = () => {
|
||||
<div style={{ width: 500 }}>
|
||||
<Card>
|
||||
<Title heading={2} style={{ textAlign: 'center' }}>
|
||||
用户登录
|
||||
{t('用户登录')}
|
||||
</Title>
|
||||
<Form>
|
||||
<Form.Input
|
||||
field={'username'}
|
||||
label={'用户名'}
|
||||
placeholder='用户名'
|
||||
label={t('用户名/邮箱')}
|
||||
placeholder={t('用户名/邮箱')}
|
||||
name='username'
|
||||
onChange={(value) => handleChange('username', value)}
|
||||
/>
|
||||
<Form.Input
|
||||
field={'password'}
|
||||
label={'密码'}
|
||||
placeholder='密码'
|
||||
label={t('密码')}
|
||||
placeholder={t('密码')}
|
||||
name='password'
|
||||
type='password'
|
||||
onChange={(value) => handleChange('password', value)}
|
||||
@@ -209,7 +211,7 @@ const LoginForm = () => {
|
||||
htmlType={'submit'}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
登录
|
||||
{t('登录')}
|
||||
</Button>
|
||||
</Form>
|
||||
<div
|
||||
@@ -220,10 +222,10 @@ const LoginForm = () => {
|
||||
}}
|
||||
>
|
||||
<Text>
|
||||
没有账号请先 <Link to='/register'>注册账号</Link>
|
||||
{t('没有账户?')} <Link to='/register'>{t('点击注册')}</Link>
|
||||
</Text>
|
||||
<Text>
|
||||
忘记密码 <Link to='/reset'>点击重置</Link>
|
||||
{t('忘记密码?')} <Link to='/reset'>{t('点击重置')}</Link>
|
||||
</Text>
|
||||
</div>
|
||||
{status.github_oauth ||
|
||||
@@ -232,7 +234,7 @@ const LoginForm = () => {
|
||||
status.linuxdo_oauth ? (
|
||||
<>
|
||||
<Divider margin='12px' align='center'>
|
||||
第三方登录
|
||||
{t('第三方登录')}
|
||||
</Divider>
|
||||
<div
|
||||
style={{
|
||||
@@ -296,12 +298,12 @@ const LoginForm = () => {
|
||||
<></>
|
||||
)}
|
||||
<Modal
|
||||
title='微信扫码登录'
|
||||
title={t('微信扫码登录')}
|
||||
visible={showWeChatLoginModal}
|
||||
maskClosable={true}
|
||||
onOk={onSubmitWeChatVerificationCode}
|
||||
onCancel={() => setShowWeChatLoginModal(false)}
|
||||
okText={'登录'}
|
||||
okText={t('登录')}
|
||||
size={'small'}
|
||||
centered={true}
|
||||
>
|
||||
@@ -316,14 +318,14 @@ const LoginForm = () => {
|
||||
</div>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<p>
|
||||
微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)
|
||||
{t('微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)')}
|
||||
</p>
|
||||
</div>
|
||||
<Form size='large'>
|
||||
<Form.Input
|
||||
field={'wechat_verification_code'}
|
||||
placeholder='验证码'
|
||||
label={'验证码'}
|
||||
placeholder={t('验证码')}
|
||||
label={t('验证码')}
|
||||
value={inputs.wechat_verification_code}
|
||||
onChange={(value) =>
|
||||
handleChange('wechat_verification_code', value)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
API,
|
||||
copy,
|
||||
@@ -24,7 +25,7 @@ import {
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { ITEMS_PER_PAGE } from '../constants';
|
||||
import {
|
||||
renderAudioModelPrice,
|
||||
renderAudioModelPrice, renderGroup,
|
||||
renderModelPrice, renderModelPriceSimple,
|
||||
renderNumber,
|
||||
renderQuota,
|
||||
@@ -32,6 +33,7 @@ import {
|
||||
} from '../helpers/render';
|
||||
import Paragraph from '@douyinfe/semi-ui/lib/es/typography/paragraph';
|
||||
import { getLogOther } from '../helpers/other.js';
|
||||
import { StyleContext } from '../context/Style/index.js';
|
||||
|
||||
const { Header } = Layout;
|
||||
|
||||
@@ -40,8 +42,8 @@ function renderTimestamp(timestamp) {
|
||||
}
|
||||
|
||||
const MODE_OPTIONS = [
|
||||
{ key: 'all', text: '全部用户', value: 'all' },
|
||||
{ key: 'self', text: '当前用户', value: 'self' },
|
||||
{ key: 'all', text: 'all', value: 'all' },
|
||||
{ key: 'self', text: 'current user', value: 'self' },
|
||||
];
|
||||
|
||||
const colors = [
|
||||
@@ -62,123 +64,92 @@ const colors = [
|
||||
'yellow',
|
||||
];
|
||||
|
||||
function renderType(type) {
|
||||
switch (type) {
|
||||
case 1:
|
||||
const LogsTable = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
function renderType(type) {
|
||||
switch (type) {
|
||||
case 1:
|
||||
return <Tag color='cyan' size='large'>{t('充值')}</Tag>;
|
||||
case 2:
|
||||
return <Tag color='lime' size='large'>{t('消费')}</Tag>;
|
||||
case 3:
|
||||
return <Tag color='orange' size='large'>{t('管理')}</Tag>;
|
||||
case 4:
|
||||
return <Tag color='purple' size='large'>{t('系统')}</Tag>;
|
||||
default:
|
||||
return <Tag color='black' size='large'>{t('未知')}</Tag>;
|
||||
}
|
||||
}
|
||||
|
||||
function renderIsStream(bool) {
|
||||
if (bool) {
|
||||
return <Tag color='blue' size='large'>{t('流')}</Tag>;
|
||||
} else {
|
||||
return <Tag color='purple' size='large'>{t('非流')}</Tag>;
|
||||
}
|
||||
}
|
||||
|
||||
function renderUseTime(type) {
|
||||
const time = parseInt(type);
|
||||
if (time < 101) {
|
||||
return (
|
||||
<Tag color='cyan' size='large'>
|
||||
<Tag color='green' size='large'>
|
||||
{' '}
|
||||
充值{' '}
|
||||
{time} s{' '}
|
||||
</Tag>
|
||||
);
|
||||
case 2:
|
||||
return (
|
||||
<Tag color='lime' size='large'>
|
||||
{' '}
|
||||
消费{' '}
|
||||
</Tag>
|
||||
);
|
||||
case 3:
|
||||
} else if (time < 300) {
|
||||
return (
|
||||
<Tag color='orange' size='large'>
|
||||
{' '}
|
||||
管理{' '}
|
||||
{time} s{' '}
|
||||
</Tag>
|
||||
);
|
||||
case 4:
|
||||
} else {
|
||||
return (
|
||||
<Tag color='purple' size='large'>
|
||||
<Tag color='red' size='large'>
|
||||
{' '}
|
||||
系统{' '}
|
||||
{time} s{' '}
|
||||
</Tag>
|
||||
);
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
function renderFirstUseTime(type) {
|
||||
let time = parseFloat(type) / 1000.0;
|
||||
time = time.toFixed(1);
|
||||
if (time < 3) {
|
||||
return (
|
||||
<Tag color='black' size='large'>
|
||||
<Tag color='green' size='large'>
|
||||
{' '}
|
||||
未知{' '}
|
||||
{time} s{' '}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (time < 10) {
|
||||
return (
|
||||
<Tag color='orange' size='large'>
|
||||
{' '}
|
||||
{time} s{' '}
|
||||
</Tag>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Tag color='red' size='large'>
|
||||
{' '}
|
||||
{time} s{' '}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function renderIsStream(bool) {
|
||||
if (bool) {
|
||||
return (
|
||||
<Tag color='blue' size='large'>
|
||||
流
|
||||
</Tag>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Tag color='purple' size='large'>
|
||||
非流
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function renderUseTime(type) {
|
||||
const time = parseInt(type);
|
||||
if (time < 101) {
|
||||
return (
|
||||
<Tag color='green' size='large'>
|
||||
{' '}
|
||||
{time} s{' '}
|
||||
</Tag>
|
||||
);
|
||||
} else if (time < 300) {
|
||||
return (
|
||||
<Tag color='orange' size='large'>
|
||||
{' '}
|
||||
{time} s{' '}
|
||||
</Tag>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Tag color='red' size='large'>
|
||||
{' '}
|
||||
{time} s{' '}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function renderFirstUseTime(type) {
|
||||
let time = parseFloat(type) / 1000.0;
|
||||
time = time.toFixed(1);
|
||||
if (time < 3) {
|
||||
return (
|
||||
<Tag color='green' size='large'>
|
||||
{' '}
|
||||
{time} s{' '}
|
||||
</Tag>
|
||||
);
|
||||
} else if (time < 10) {
|
||||
return (
|
||||
<Tag color='orange' size='large'>
|
||||
{' '}
|
||||
{time} s{' '}
|
||||
</Tag>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Tag color='red' size='large'>
|
||||
{' '}
|
||||
{time} s{' '}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const LogsTable = () => {
|
||||
const columns = [
|
||||
{
|
||||
title: '时间',
|
||||
title: t('时间'),
|
||||
dataIndex: 'timestamp2string',
|
||||
},
|
||||
{
|
||||
title: '渠道',
|
||||
title: t('渠道'),
|
||||
dataIndex: 'channel',
|
||||
className: isAdmin() ? 'tableShow' : 'tableHiddle',
|
||||
render: (text, record, index) => {
|
||||
@@ -204,7 +175,7 @@ const LogsTable = () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '用户',
|
||||
title: t('用户'),
|
||||
dataIndex: 'username',
|
||||
className: isAdmin() ? 'tableShow' : 'tableHiddle',
|
||||
render: (text, record, index) => {
|
||||
@@ -226,7 +197,7 @@ const LogsTable = () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '令牌',
|
||||
title: t('令牌'),
|
||||
dataIndex: 'token_name',
|
||||
render: (text, record, index) => {
|
||||
return record.type === 0 || record.type === 2 ? (
|
||||
@@ -239,7 +210,7 @@ const LogsTable = () => {
|
||||
}}
|
||||
>
|
||||
{' '}
|
||||
{text}{' '}
|
||||
{t(text)}{' '}
|
||||
</Tag>
|
||||
</div>
|
||||
) : (
|
||||
@@ -248,14 +219,45 @@ const LogsTable = () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
title: t('分组'),
|
||||
dataIndex: 'group',
|
||||
render: (text, record, index) => {
|
||||
if (record.type === 0 || record.type === 2) {
|
||||
if (record.group) {
|
||||
return (
|
||||
<>
|
||||
{renderGroup(record.group)}
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
let other = JSON.parse(record.other);
|
||||
if (other === null) {
|
||||
return <></>;
|
||||
}
|
||||
if (other.group !== undefined) {
|
||||
return (
|
||||
<>
|
||||
{renderGroup(other.group)}
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return <></>;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return <></>;
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('类型'),
|
||||
dataIndex: 'type',
|
||||
render: (text, record, index) => {
|
||||
return <>{renderType(text)}</>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '模型',
|
||||
title: t('模型'),
|
||||
dataIndex: 'model_name',
|
||||
render: (text, record, index) => {
|
||||
return record.type === 0 || record.type === 2 ? (
|
||||
@@ -277,7 +279,7 @@ const LogsTable = () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '用时/首字',
|
||||
title: t('用时/首字'),
|
||||
dataIndex: 'use_time',
|
||||
render: (text, record, index) => {
|
||||
if (record.is_stream) {
|
||||
@@ -304,7 +306,7 @@ const LogsTable = () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '提示',
|
||||
title: t('提示'),
|
||||
dataIndex: 'prompt_tokens',
|
||||
render: (text, record, index) => {
|
||||
return record.type === 0 || record.type === 2 ? (
|
||||
@@ -315,7 +317,7 @@ const LogsTable = () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '补全',
|
||||
title: t('补全'),
|
||||
dataIndex: 'completion_tokens',
|
||||
render: (text, record, index) => {
|
||||
return parseInt(text) > 0 &&
|
||||
@@ -327,7 +329,7 @@ const LogsTable = () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '花费',
|
||||
title: t('花费'),
|
||||
dataIndex: 'quota',
|
||||
render: (text, record, index) => {
|
||||
return record.type === 0 || record.type === 2 ? (
|
||||
@@ -338,11 +340,11 @@ const LogsTable = () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '重试',
|
||||
title: t('重试'),
|
||||
dataIndex: 'retry',
|
||||
className: isAdmin() ? 'tableShow' : 'tableHiddle',
|
||||
render: (text, record, index) => {
|
||||
let content = '渠道:' + record.channel;
|
||||
let content = t('渠道') + `:${record.channel}`;
|
||||
if (record.other !== '') {
|
||||
let other = JSON.parse(record.other);
|
||||
if (other === null) {
|
||||
@@ -357,7 +359,7 @@ const LogsTable = () => {
|
||||
// channel id array
|
||||
let useChannel = other.admin_info.use_channel;
|
||||
let useChannelStr = useChannel.join('->');
|
||||
content = `渠道:${useChannelStr}`;
|
||||
content = t('渠道') + `:${useChannelStr}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -365,7 +367,7 @@ const LogsTable = () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '详情',
|
||||
title: t('详情'),
|
||||
dataIndex: 'content',
|
||||
render: (text, record, index) => {
|
||||
let other = getLogOther(record.other);
|
||||
@@ -405,6 +407,7 @@ const LogsTable = () => {
|
||||
},
|
||||
];
|
||||
|
||||
const [styleState, styleDispatch] = useContext(StyleContext);
|
||||
const [logs, setLogs] = useState([]);
|
||||
const [expandData, setExpandData] = useState({});
|
||||
const [showStat, setShowStat] = useState(false);
|
||||
@@ -424,6 +427,7 @@ const LogsTable = () => {
|
||||
start_timestamp: timestamp2string(getTodayStartTimestamp()),
|
||||
end_timestamp: timestamp2string(now.getTime() / 1000 + 3600),
|
||||
channel: '',
|
||||
group: '',
|
||||
});
|
||||
const {
|
||||
username,
|
||||
@@ -432,6 +436,7 @@ const LogsTable = () => {
|
||||
start_timestamp,
|
||||
end_timestamp,
|
||||
channel,
|
||||
group,
|
||||
} = inputs;
|
||||
|
||||
const [stat, setStat] = useState({
|
||||
@@ -440,13 +445,13 @@ const LogsTable = () => {
|
||||
});
|
||||
|
||||
const handleInputChange = (value, name) => {
|
||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||||
setInputs(inputs => ({ ...inputs, [name]: value }));
|
||||
};
|
||||
|
||||
const getLogSelfStat = async () => {
|
||||
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
|
||||
let localEndTimestamp = Date.parse(end_timestamp) / 1000;
|
||||
let url = `/api/log/self/stat?type=${logType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
|
||||
let url = `/api/log/self/stat?type=${logType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&group=${group}`;
|
||||
url = encodeURI(url);
|
||||
let res = await API.get(url);
|
||||
const { success, message, data } = res.data;
|
||||
@@ -460,7 +465,7 @@ const LogsTable = () => {
|
||||
const getLogStat = async () => {
|
||||
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
|
||||
let localEndTimestamp = Date.parse(end_timestamp) / 1000;
|
||||
let url = `/api/log/stat?type=${logType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&channel=${channel}`;
|
||||
let url = `/api/log/stat?type=${logType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&channel=${channel}&group=${group}`;
|
||||
url = encodeURI(url);
|
||||
let res = await API.get(url);
|
||||
const { success, message, data } = res.data;
|
||||
@@ -493,13 +498,13 @@ const LogsTable = () => {
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
Modal.info({
|
||||
title: '用户信息',
|
||||
title: t('用户信息'),
|
||||
content: (
|
||||
<div style={{ padding: 12 }}>
|
||||
<p>用户名: {data.username}</p>
|
||||
<p>余额: {renderQuota(data.quota)}</p>
|
||||
<p>已用额度:{renderQuota(data.used_quota)}</p>
|
||||
<p>请求次数:{renderNumber(data.request_count)}</p>
|
||||
<p>{t('用户名')}: {data.username}</p>
|
||||
<p>{t('余额')}: {renderQuota(data.quota)}</p>
|
||||
<p>{t('已用额度')}:{renderQuota(data.used_quota)}</p>
|
||||
<p>{t('请求次数')}:{renderNumber(data.request_count)}</p>
|
||||
</div>
|
||||
),
|
||||
centered: true,
|
||||
@@ -537,26 +542,26 @@ const LogsTable = () => {
|
||||
}
|
||||
if (other?.ws || other?.audio) {
|
||||
expandDataLocal.push({
|
||||
key: '语音输入',
|
||||
key: t('语音输入'),
|
||||
value: other.audio_input,
|
||||
});
|
||||
expandDataLocal.push({
|
||||
key: '语音输出',
|
||||
key: t('语音输出'),
|
||||
value: other.audio_output,
|
||||
});
|
||||
expandDataLocal.push({
|
||||
key: '文字输入',
|
||||
key: t('文字输入'),
|
||||
value: other.text_input,
|
||||
});
|
||||
expandDataLocal.push({
|
||||
key: '文字输出',
|
||||
key: t('文字输出'),
|
||||
value: other.text_output,
|
||||
});
|
||||
}
|
||||
expandDataLocal.push({
|
||||
key: '日志详情',
|
||||
key: t('日志详情'),
|
||||
value: logs[i].content,
|
||||
})
|
||||
});
|
||||
if (logs[i].type === 2) {
|
||||
let content = '';
|
||||
if (other?.ws || other?.audio) {
|
||||
@@ -583,7 +588,7 @@ const LogsTable = () => {
|
||||
);
|
||||
}
|
||||
expandDataLocal.push({
|
||||
key: '计费过程',
|
||||
key: t('计费过程'),
|
||||
value: content,
|
||||
});
|
||||
}
|
||||
@@ -603,9 +608,9 @@ const LogsTable = () => {
|
||||
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
|
||||
let localEndTimestamp = Date.parse(end_timestamp) / 1000;
|
||||
if (isAdminUser) {
|
||||
url = `/api/log/?p=${startIdx}&page_size=${pageSize}&type=${logType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&channel=${channel}`;
|
||||
url = `/api/log/?p=${startIdx}&page_size=${pageSize}&type=${logType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&channel=${channel}&group=${group}`;
|
||||
} else {
|
||||
url = `/api/log/self/?p=${startIdx}&page_size=${pageSize}&type=${logType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
|
||||
url = `/api/log/self/?p=${startIdx}&page_size=${pageSize}&type=${logType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&group=${group}`;
|
||||
}
|
||||
url = encodeURI(url);
|
||||
const res = await API.get(url);
|
||||
@@ -676,7 +681,7 @@ const LogsTable = () => {
|
||||
<Spin spinning={loadingStat}>
|
||||
<Space>
|
||||
<Tag color='green' size='large' style={{ padding: 15 }}>
|
||||
总消耗额度: {renderQuota(stat.quota)}
|
||||
{t('总消耗额度')}: {renderQuota(stat.quota)}
|
||||
</Tag>
|
||||
<Tag color='blue' size='large' style={{ padding: 15 }}>
|
||||
RPM: {stat.rpm}
|
||||
@@ -689,69 +694,96 @@ const LogsTable = () => {
|
||||
</Header>
|
||||
<Form layout='horizontal' style={{ marginTop: 10 }}>
|
||||
<>
|
||||
<Form.Section>
|
||||
<div style={{ marginBottom: 10 }}>
|
||||
{
|
||||
styleState.isMobile ? (
|
||||
<div>
|
||||
<Form.DatePicker
|
||||
field='start_timestamp'
|
||||
label={t('起始时间')}
|
||||
style={{ width: 272 }}
|
||||
initValue={start_timestamp}
|
||||
type='dateTime'
|
||||
onChange={(value) => {
|
||||
console.log(value);
|
||||
handleInputChange(value, 'start_timestamp')
|
||||
}}
|
||||
/>
|
||||
<Form.DatePicker
|
||||
field='end_timestamp'
|
||||
fluid
|
||||
label={t('结束时间')}
|
||||
style={{ width: 272 }}
|
||||
initValue={end_timestamp}
|
||||
type='dateTime'
|
||||
onChange={(value) => handleInputChange(value, 'end_timestamp')}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Form.DatePicker
|
||||
field="range_timestamp"
|
||||
label={t('时间范围')}
|
||||
initValue={[start_timestamp, end_timestamp]}
|
||||
type="dateTimeRange"
|
||||
name="range_timestamp"
|
||||
onChange={(value) => {
|
||||
if (Array.isArray(value) && value.length === 2) {
|
||||
handleInputChange(value[0], 'start_timestamp');
|
||||
handleInputChange(value[1], 'end_timestamp');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</Form.Section>
|
||||
<Form.Input
|
||||
field='token_name'
|
||||
label='令牌名称'
|
||||
style={{ width: 176 }}
|
||||
label={t('令牌名称')}
|
||||
value={token_name}
|
||||
placeholder={'可选值'}
|
||||
placeholder={t('可选值')}
|
||||
name='token_name'
|
||||
onChange={(value) => handleInputChange(value, 'token_name')}
|
||||
/>
|
||||
<Form.Input
|
||||
field='model_name'
|
||||
label='模型名称'
|
||||
style={{ width: 176 }}
|
||||
label={t('模型名称')}
|
||||
value={model_name}
|
||||
placeholder='可选值'
|
||||
placeholder={t('可选值')}
|
||||
name='model_name'
|
||||
onChange={(value) => handleInputChange(value, 'model_name')}
|
||||
/>
|
||||
<Form.DatePicker
|
||||
field='start_timestamp'
|
||||
label='起始时间'
|
||||
style={{ width: 272 }}
|
||||
initValue={start_timestamp}
|
||||
value={start_timestamp}
|
||||
type='dateTime'
|
||||
name='start_timestamp'
|
||||
onChange={(value) => handleInputChange(value, 'start_timestamp')}
|
||||
/>
|
||||
<Form.DatePicker
|
||||
field='end_timestamp'
|
||||
fluid
|
||||
label='结束时间'
|
||||
style={{ width: 272 }}
|
||||
initValue={end_timestamp}
|
||||
value={end_timestamp}
|
||||
type='dateTime'
|
||||
name='end_timestamp'
|
||||
onChange={(value) => handleInputChange(value, 'end_timestamp')}
|
||||
<Form.Input
|
||||
field='group'
|
||||
label={t('分组')}
|
||||
value={group}
|
||||
placeholder={t('可选值')}
|
||||
name='group'
|
||||
onChange={(value) => handleInputChange(value, 'group')}
|
||||
/>
|
||||
{isAdminUser && (
|
||||
<>
|
||||
<Form.Input
|
||||
field='channel'
|
||||
label='渠道 ID'
|
||||
style={{ width: 176 }}
|
||||
label={t('渠道 ID')}
|
||||
value={channel}
|
||||
placeholder='可选值'
|
||||
placeholder={t('可选值')}
|
||||
name='channel'
|
||||
onChange={(value) => handleInputChange(value, 'channel')}
|
||||
/>
|
||||
<Form.Input
|
||||
field='username'
|
||||
label='用户名称'
|
||||
style={{ width: 176 }}
|
||||
label={t('用户名称')}
|
||||
value={username}
|
||||
placeholder={'可选值'}
|
||||
placeholder={t('可选值')}
|
||||
name='username'
|
||||
onChange={(value) => handleInputChange(value, 'username')}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
label='查询'
|
||||
label={t('查询')}
|
||||
type='primary'
|
||||
htmlType='submit'
|
||||
className='btn-margin-right'
|
||||
@@ -759,7 +791,7 @@ const LogsTable = () => {
|
||||
loading={loading}
|
||||
style={{ marginTop: 24 }}
|
||||
>
|
||||
查询
|
||||
{t('查询')}
|
||||
</Button>
|
||||
<Form.Section></Form.Section>
|
||||
</>
|
||||
@@ -773,11 +805,11 @@ const LogsTable = () => {
|
||||
loadLogs(0, pageSize, parseInt(value));
|
||||
}}
|
||||
>
|
||||
<Select.Option value='0'>全部</Select.Option>
|
||||
<Select.Option value='1'>充值</Select.Option>
|
||||
<Select.Option value='2'>消费</Select.Option>
|
||||
<Select.Option value='3'>管理</Select.Option>
|
||||
<Select.Option value='4'>系统</Select.Option>
|
||||
<Select.Option value='0'>{t('全部')}</Select.Option>
|
||||
<Select.Option value='1'>{t('充值')}</Select.Option>
|
||||
<Select.Option value='2'>{t('消费')}</Select.Option>
|
||||
<Select.Option value='3'>{t('管理')}</Select.Option>
|
||||
<Select.Option value='4'>{t('系统')}</Select.Option>
|
||||
</Select>
|
||||
</div>
|
||||
<Table
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
Typography,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { ITEMS_PER_PAGE } from '../constants';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const colors = [
|
||||
'amber',
|
||||
@@ -40,247 +41,241 @@ const colors = [
|
||||
'yellow',
|
||||
];
|
||||
|
||||
function renderType(type) {
|
||||
switch (type) {
|
||||
case 'IMAGINE':
|
||||
return (
|
||||
<Tag color='blue' size='large'>
|
||||
绘图
|
||||
</Tag>
|
||||
);
|
||||
case 'UPSCALE':
|
||||
return (
|
||||
<Tag color='orange' size='large'>
|
||||
放大
|
||||
</Tag>
|
||||
);
|
||||
case 'VARIATION':
|
||||
return (
|
||||
<Tag color='purple' size='large'>
|
||||
变换
|
||||
</Tag>
|
||||
);
|
||||
case 'HIGH_VARIATION':
|
||||
return (
|
||||
<Tag color='purple' size='large'>
|
||||
强变换
|
||||
</Tag>
|
||||
);
|
||||
case 'LOW_VARIATION':
|
||||
return (
|
||||
<Tag color='purple' size='large'>
|
||||
弱变换
|
||||
</Tag>
|
||||
);
|
||||
case 'PAN':
|
||||
return (
|
||||
<Tag color='cyan' size='large'>
|
||||
平移
|
||||
</Tag>
|
||||
);
|
||||
case 'DESCRIBE':
|
||||
return (
|
||||
<Tag color='yellow' size='large'>
|
||||
图生文
|
||||
</Tag>
|
||||
);
|
||||
case 'BLEND':
|
||||
return (
|
||||
<Tag color='lime' size='large'>
|
||||
图混合
|
||||
</Tag>
|
||||
);
|
||||
case 'UPLOAD':
|
||||
return (
|
||||
<Tag color='blue' size='large'>
|
||||
上传文件
|
||||
</Tag>
|
||||
);
|
||||
case 'SHORTEN':
|
||||
return (
|
||||
<Tag color='pink' size='large'>
|
||||
缩词
|
||||
</Tag>
|
||||
);
|
||||
case 'REROLL':
|
||||
return (
|
||||
<Tag color='indigo' size='large'>
|
||||
重绘
|
||||
</Tag>
|
||||
);
|
||||
case 'INPAINT':
|
||||
return (
|
||||
<Tag color='violet' size='large'>
|
||||
局部重绘-提交
|
||||
</Tag>
|
||||
);
|
||||
case 'ZOOM':
|
||||
return (
|
||||
<Tag color='teal' size='large'>
|
||||
变焦
|
||||
</Tag>
|
||||
);
|
||||
case 'CUSTOM_ZOOM':
|
||||
return (
|
||||
<Tag color='teal' size='large'>
|
||||
自定义变焦-提交
|
||||
</Tag>
|
||||
);
|
||||
case 'MODAL':
|
||||
return (
|
||||
<Tag color='green' size='large'>
|
||||
窗口处理
|
||||
</Tag>
|
||||
);
|
||||
case 'SWAP_FACE':
|
||||
return (
|
||||
<Tag color='light-green' size='large'>
|
||||
换脸
|
||||
</Tag>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Tag color='white' size='large'>
|
||||
未知
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function renderCode(code) {
|
||||
switch (code) {
|
||||
case 1:
|
||||
return (
|
||||
<Tag color='green' size='large'>
|
||||
已提交
|
||||
</Tag>
|
||||
);
|
||||
case 21:
|
||||
return (
|
||||
<Tag color='lime' size='large'>
|
||||
等待中
|
||||
</Tag>
|
||||
);
|
||||
case 22:
|
||||
return (
|
||||
<Tag color='orange' size='large'>
|
||||
重复提交
|
||||
</Tag>
|
||||
);
|
||||
case 0:
|
||||
return (
|
||||
<Tag color='yellow' size='large'>
|
||||
未提交
|
||||
</Tag>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Tag color='white' size='large'>
|
||||
未知
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function renderStatus(type) {
|
||||
// Ensure all cases are string literals by adding quotes.
|
||||
switch (type) {
|
||||
case 'SUCCESS':
|
||||
return (
|
||||
<Tag color='green' size='large'>
|
||||
成功
|
||||
</Tag>
|
||||
);
|
||||
case 'NOT_START':
|
||||
return (
|
||||
<Tag color='grey' size='large'>
|
||||
未启动
|
||||
</Tag>
|
||||
);
|
||||
case 'SUBMITTED':
|
||||
return (
|
||||
<Tag color='yellow' size='large'>
|
||||
队列中
|
||||
</Tag>
|
||||
);
|
||||
case 'IN_PROGRESS':
|
||||
return (
|
||||
<Tag color='blue' size='large'>
|
||||
执行中
|
||||
</Tag>
|
||||
);
|
||||
case 'FAILURE':
|
||||
return (
|
||||
<Tag color='red' size='large'>
|
||||
失败
|
||||
</Tag>
|
||||
);
|
||||
case 'MODAL':
|
||||
return (
|
||||
<Tag color='yellow' size='large'>
|
||||
窗口等待
|
||||
</Tag>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Tag color='white' size='large'>
|
||||
未知
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const renderTimestamp = (timestampInSeconds) => {
|
||||
const date = new Date(timestampInSeconds * 1000); // 从秒转换为毫秒
|
||||
|
||||
const year = date.getFullYear(); // 获取年份
|
||||
const month = ('0' + (date.getMonth() + 1)).slice(-2); // 获取月份,从0开始需要+1,并保证两位数
|
||||
const day = ('0' + date.getDate()).slice(-2); // 获取日期,并保证两位数
|
||||
const hours = ('0' + date.getHours()).slice(-2); // 获取小时,并保证两位数
|
||||
const minutes = ('0' + date.getMinutes()).slice(-2); // 获取分钟,并保证两位数
|
||||
const seconds = ('0' + date.getSeconds()).slice(-2); // 获取秒钟,并保证两位数
|
||||
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; // 格式化输出
|
||||
};
|
||||
// 修改renderDuration函数以包含颜色逻辑
|
||||
function renderDuration(submit_time, finishTime) {
|
||||
// 确保startTime和finishTime都是有效的时间戳
|
||||
if (!submit_time || !finishTime) return 'N/A';
|
||||
|
||||
// 将时间戳转换为Date对象
|
||||
const start = new Date(submit_time);
|
||||
const finish = new Date(finishTime);
|
||||
|
||||
// 计算时间差(毫秒)
|
||||
const durationMs = finish - start;
|
||||
|
||||
// 将时间差转换为秒,并保留一位小数
|
||||
const durationSec = (durationMs / 1000).toFixed(1);
|
||||
|
||||
// 设置颜色:大于60秒则为红色,小于等于60秒则为绿色
|
||||
const color = durationSec > 60 ? 'red' : 'green';
|
||||
|
||||
// 返回带有样式的颜色标签
|
||||
return (
|
||||
<Tag color={color} size="large">
|
||||
{durationSec} 秒
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
const LogsTable = () => {
|
||||
const { t } = useTranslation();
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [modalContent, setModalContent] = useState('');
|
||||
function renderType(type) {
|
||||
|
||||
switch (type) {
|
||||
case 'IMAGINE':
|
||||
return (
|
||||
<Tag color='blue' size='large'>
|
||||
{t('绘图')}
|
||||
</Tag>
|
||||
);
|
||||
case 'UPSCALE':
|
||||
return (
|
||||
<Tag color='orange' size='large'>
|
||||
{t('放大')}
|
||||
</Tag>
|
||||
);
|
||||
case 'VARIATION':
|
||||
return (
|
||||
<Tag color='purple' size='large'>
|
||||
{t('变换')}
|
||||
</Tag>
|
||||
);
|
||||
case 'HIGH_VARIATION':
|
||||
return (
|
||||
<Tag color='purple' size='large'>
|
||||
{t('强变换')}
|
||||
</Tag>
|
||||
);
|
||||
case 'LOW_VARIATION':
|
||||
return (
|
||||
<Tag color='purple' size='large'>
|
||||
{t('弱变换')}
|
||||
</Tag>
|
||||
);
|
||||
case 'PAN':
|
||||
return (
|
||||
<Tag color='cyan' size='large'>
|
||||
{t('平移')}
|
||||
</Tag>
|
||||
);
|
||||
case 'DESCRIBE':
|
||||
return (
|
||||
<Tag color='yellow' size='large'>
|
||||
{t('图生文')}
|
||||
</Tag>
|
||||
);
|
||||
case 'BLEND':
|
||||
return (
|
||||
<Tag color='lime' size='large'>
|
||||
{t('图混合')}
|
||||
</Tag>
|
||||
);
|
||||
case 'UPLOAD':
|
||||
return (
|
||||
<Tag color='blue' size='large'>
|
||||
上传文件
|
||||
</Tag>
|
||||
);
|
||||
case 'SHORTEN':
|
||||
return (
|
||||
<Tag color='pink' size='large'>
|
||||
{t('缩词')}
|
||||
</Tag>
|
||||
);
|
||||
case 'REROLL':
|
||||
return (
|
||||
<Tag color='indigo' size='large'>
|
||||
{t('重绘')}
|
||||
</Tag>
|
||||
);
|
||||
case 'INPAINT':
|
||||
return (
|
||||
<Tag color='violet' size='large'>
|
||||
{t('局部重绘-提交')}
|
||||
</Tag>
|
||||
);
|
||||
case 'ZOOM':
|
||||
return (
|
||||
<Tag color='teal' size='large'>
|
||||
{t('变焦')}
|
||||
</Tag>
|
||||
);
|
||||
case 'CUSTOM_ZOOM':
|
||||
return (
|
||||
<Tag color='teal' size='large'>
|
||||
{t('自定义变焦-提交')}
|
||||
</Tag>
|
||||
);
|
||||
case 'MODAL':
|
||||
return (
|
||||
<Tag color='green' size='large'>
|
||||
{t('窗口处理')}
|
||||
</Tag>
|
||||
);
|
||||
case 'SWAP_FACE':
|
||||
return (
|
||||
<Tag color='light-green' size='large'>
|
||||
{t('换脸')}
|
||||
</Tag>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Tag color='white' size='large'>
|
||||
{t('未知')}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function renderCode(code) {
|
||||
|
||||
switch (code) {
|
||||
case 1:
|
||||
return (
|
||||
<Tag color='green' size='large'>
|
||||
{t('已提交')}
|
||||
</Tag>
|
||||
);
|
||||
case 21:
|
||||
return (
|
||||
<Tag color='lime' size='large'>
|
||||
{t('等待中')}
|
||||
</Tag>
|
||||
);
|
||||
case 22:
|
||||
return (
|
||||
<Tag color='orange' size='large'>
|
||||
{t('重复提交')}
|
||||
</Tag>
|
||||
);
|
||||
case 0:
|
||||
return (
|
||||
<Tag color='yellow' size='large'>
|
||||
{t('未提交')}
|
||||
</Tag>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Tag color='white' size='large'>
|
||||
{t('未知')}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function renderStatus(type) {
|
||||
|
||||
switch (type) {
|
||||
case 'SUCCESS':
|
||||
return (
|
||||
<Tag color='green' size='large'>
|
||||
{t('成功')}
|
||||
</Tag>
|
||||
);
|
||||
case 'NOT_START':
|
||||
return (
|
||||
<Tag color='grey' size='large'>
|
||||
{t('未启动')}
|
||||
</Tag>
|
||||
);
|
||||
case 'SUBMITTED':
|
||||
return (
|
||||
<Tag color='yellow' size='large'>
|
||||
{t('队列中')}
|
||||
</Tag>
|
||||
);
|
||||
case 'IN_PROGRESS':
|
||||
return (
|
||||
<Tag color='blue' size='large'>
|
||||
{t('执行中')}
|
||||
</Tag>
|
||||
);
|
||||
case 'FAILURE':
|
||||
return (
|
||||
<Tag color='red' size='large'>
|
||||
{t('失败')}
|
||||
</Tag>
|
||||
);
|
||||
case 'MODAL':
|
||||
return (
|
||||
<Tag color='yellow' size='large'>
|
||||
{t('窗口等待')}
|
||||
</Tag>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Tag color='white' size='large'>
|
||||
{t('未知')}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const renderTimestamp = (timestampInSeconds) => {
|
||||
const date = new Date(timestampInSeconds * 1000); // 从秒转换为毫秒
|
||||
|
||||
const year = date.getFullYear(); // 获取年份
|
||||
const month = ('0' + (date.getMonth() + 1)).slice(-2); // 获取月份,从0开始需要+1,并保证两位数
|
||||
const day = ('0' + date.getDate()).slice(-2); // 获取日期,并保证两位数
|
||||
const hours = ('0' + date.getHours()).slice(-2); // 获取小时,并保证两位数
|
||||
const minutes = ('0' + date.getMinutes()).slice(-2); // 获取分钟,并保证两位数
|
||||
const seconds = ('0' + date.getSeconds()).slice(-2); // 获取秒钟,并保证两位数
|
||||
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; // 格式化输出
|
||||
};
|
||||
// 修改renderDuration函数以包含颜色逻辑
|
||||
function renderDuration(submit_time, finishTime) {
|
||||
|
||||
if (!submit_time || !finishTime) return 'N/A';
|
||||
|
||||
const start = new Date(submit_time);
|
||||
const finish = new Date(finishTime);
|
||||
const durationMs = finish - start;
|
||||
const durationSec = (durationMs / 1000).toFixed(1);
|
||||
const color = durationSec > 60 ? 'red' : 'green';
|
||||
|
||||
return (
|
||||
<Tag color={color} size="large">
|
||||
{durationSec} {t('秒')}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
const columns = [
|
||||
{
|
||||
title: '提交时间',
|
||||
title: t('提交时间'),
|
||||
dataIndex: 'submit_time',
|
||||
render: (text, record, index) => {
|
||||
return <div>{renderTimestamp(text / 1000)}</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '花费时间',
|
||||
title: t('花费时间'),
|
||||
dataIndex: 'finish_time', // 以finish_time作为dataIndex
|
||||
key: 'finish_time',
|
||||
render: (finish, record) => {
|
||||
@@ -289,7 +284,7 @@ const LogsTable = () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '渠道',
|
||||
title: t('渠道'),
|
||||
dataIndex: 'channel_id',
|
||||
className: isAdmin() ? 'tableShow' : 'tableHiddle',
|
||||
render: (text, record, index) => {
|
||||
@@ -310,21 +305,21 @@ const LogsTable = () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
title: t('类型'),
|
||||
dataIndex: 'action',
|
||||
render: (text, record, index) => {
|
||||
return <div>{renderType(text)}</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '任务ID',
|
||||
title: t('任务ID'),
|
||||
dataIndex: 'mj_id',
|
||||
render: (text, record, index) => {
|
||||
return <div>{text}</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '提交结果',
|
||||
title: t('提交结果'),
|
||||
dataIndex: 'code',
|
||||
className: isAdmin() ? 'tableShow' : 'tableHiddle',
|
||||
render: (text, record, index) => {
|
||||
@@ -332,7 +327,7 @@ const LogsTable = () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '任务状态',
|
||||
title: t('任务状态'),
|
||||
dataIndex: 'status',
|
||||
className: isAdmin() ? 'tableShow' : 'tableHiddle',
|
||||
render: (text, record, index) => {
|
||||
@@ -340,7 +335,7 @@ const LogsTable = () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '进度',
|
||||
title: t('进度'),
|
||||
dataIndex: 'progress',
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
@@ -363,11 +358,11 @@ const LogsTable = () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '结果图片',
|
||||
title: t('结果图片'),
|
||||
dataIndex: 'image_url',
|
||||
render: (text, record, index) => {
|
||||
if (!text) {
|
||||
return '无';
|
||||
return t('无');
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
@@ -376,7 +371,7 @@ const LogsTable = () => {
|
||||
setIsModalOpenurl(true); // 打开模态框
|
||||
}}
|
||||
>
|
||||
查看图片
|
||||
{t('查看图片')}
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
@@ -387,7 +382,7 @@ const LogsTable = () => {
|
||||
render: (text, record, index) => {
|
||||
// 如果text未定义,返回替代文本,例如空字符串''或其他
|
||||
if (!text) {
|
||||
return '无';
|
||||
return t('无');
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -410,7 +405,7 @@ const LogsTable = () => {
|
||||
render: (text, record, index) => {
|
||||
// 如果text未定义,返回替代文本,例如空字符串''或其他
|
||||
if (!text) {
|
||||
return '无';
|
||||
return t('无');
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -428,12 +423,12 @@ const LogsTable = () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '失败原因',
|
||||
title: t('失败原因'),
|
||||
dataIndex: 'fail_reason',
|
||||
render: (text, record, index) => {
|
||||
// 如果text未定义,返回替代文本,例如空字符串''或其他
|
||||
if (!text) {
|
||||
return '无';
|
||||
return t('无');
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -565,7 +560,7 @@ const LogsTable = () => {
|
||||
{isAdminUser && showBanner ? (
|
||||
<Banner
|
||||
type='info'
|
||||
description='当前未开启Midjourney回调,部分项目可能无法获得绘图结果,可在运营设置中开启。'
|
||||
description={t('当前未开启Midjourney回调,部分项目可能无法获得绘图结果,可在运营设置中开启。')}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
@@ -574,25 +569,25 @@ const LogsTable = () => {
|
||||
<>
|
||||
<Form.Input
|
||||
field='channel_id'
|
||||
label='渠道 ID'
|
||||
label={t('渠道 ID')}
|
||||
style={{ width: 176 }}
|
||||
value={channel_id}
|
||||
placeholder={'可选值'}
|
||||
placeholder={t('可选值')}
|
||||
name='channel_id'
|
||||
onChange={(value) => handleInputChange(value, 'channel_id')}
|
||||
/>
|
||||
<Form.Input
|
||||
field='mj_id'
|
||||
label='任务 ID'
|
||||
label={t('任务 ID')}
|
||||
style={{ width: 176 }}
|
||||
value={mj_id}
|
||||
placeholder='可选值'
|
||||
placeholder={t('可选值')}
|
||||
name='mj_id'
|
||||
onChange={(value) => handleInputChange(value, 'mj_id')}
|
||||
/>
|
||||
<Form.DatePicker
|
||||
field='start_timestamp'
|
||||
label='起始时间'
|
||||
label={t('起始时间')}
|
||||
style={{ width: 272 }}
|
||||
initValue={start_timestamp}
|
||||
value={start_timestamp}
|
||||
@@ -603,7 +598,7 @@ const LogsTable = () => {
|
||||
<Form.DatePicker
|
||||
field='end_timestamp'
|
||||
fluid
|
||||
label='结束时间'
|
||||
label={t('结束时间')}
|
||||
style={{ width: 272 }}
|
||||
initValue={end_timestamp}
|
||||
value={end_timestamp}
|
||||
@@ -614,13 +609,13 @@ const LogsTable = () => {
|
||||
|
||||
<Form.Section>
|
||||
<Button
|
||||
label='查询'
|
||||
label={t('查询')}
|
||||
type='primary'
|
||||
htmlType='submit'
|
||||
className='btn-margin-right'
|
||||
onClick={refresh}
|
||||
>
|
||||
查询
|
||||
{t('查询')}
|
||||
</Button>
|
||||
</Form.Section>
|
||||
</>
|
||||
@@ -635,6 +630,12 @@ const LogsTable = () => {
|
||||
total: logCount,
|
||||
pageSizeOpts: [10, 20, 50, 100],
|
||||
onPageChange: handlePageChange,
|
||||
formatPageText: (page) =>
|
||||
t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
|
||||
start: page.currentStart,
|
||||
end: page.currentEnd,
|
||||
total: logCount
|
||||
}),
|
||||
}}
|
||||
loading={loading}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useContext, useEffect, useRef, useMemo, useState } from 'react';
|
||||
import { API, copy, showError, showInfo, showSuccess } from '../helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import {
|
||||
Banner,
|
||||
@@ -23,65 +24,8 @@ import {
|
||||
import { UserContext } from '../context/User/index.js';
|
||||
import Text from '@douyinfe/semi-ui/lib/es/typography/text';
|
||||
|
||||
function renderQuotaType(type) {
|
||||
// Ensure all cases are string literals by adding quotes.
|
||||
switch (type) {
|
||||
case 1:
|
||||
return (
|
||||
<Tag color='teal' size='large'>
|
||||
按次计费
|
||||
</Tag>
|
||||
);
|
||||
case 0:
|
||||
return (
|
||||
<Tag color='violet' size='large'>
|
||||
按量计费
|
||||
</Tag>
|
||||
);
|
||||
default:
|
||||
return '未知';
|
||||
}
|
||||
}
|
||||
|
||||
function renderAvailable(available) {
|
||||
return available ? (
|
||||
<Popover
|
||||
content={
|
||||
<div style={{ padding: 8 }}>您的分组可以使用该模型</div>
|
||||
}
|
||||
position='top'
|
||||
key={available}
|
||||
style={{
|
||||
backgroundColor: 'rgba(var(--semi-blue-4),1)',
|
||||
borderColor: 'rgba(var(--semi-blue-4),1)',
|
||||
color: 'var(--semi-color-white)',
|
||||
borderWidth: 1,
|
||||
borderStyle: 'solid',
|
||||
}}
|
||||
>
|
||||
<IconVerify style={{ color: 'green' }} size="large" />
|
||||
</Popover>
|
||||
) : (
|
||||
<Popover
|
||||
content={
|
||||
<div style={{ padding: 8 }}>您的分组无权使用该模型</div>
|
||||
}
|
||||
position='top'
|
||||
key={available}
|
||||
style={{
|
||||
backgroundColor: 'rgba(var(--semi-blue-4),1)',
|
||||
borderColor: 'rgba(var(--semi-blue-4),1)',
|
||||
color: 'var(--semi-color-white)',
|
||||
borderWidth: 1,
|
||||
borderStyle: 'solid',
|
||||
}}
|
||||
>
|
||||
<IconUploadError style={{ color: '#FFA54F' }} size="large" />
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
const ModelPricing = () => {
|
||||
const { t } = useTranslation();
|
||||
const [filteredValue, setFilteredValue] = useState([]);
|
||||
const compositionRef = useRef({ isComposition: false });
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
|
||||
@@ -115,10 +59,68 @@ const ModelPricing = () => {
|
||||
const newFilteredValue = value ? [value] : [];
|
||||
setFilteredValue(newFilteredValue);
|
||||
};
|
||||
|
||||
function renderQuotaType(type) {
|
||||
// Ensure all cases are string literals by adding quotes.
|
||||
switch (type) {
|
||||
case 1:
|
||||
return (
|
||||
<Tag color='teal' size='large'>
|
||||
{t('按次计费')}
|
||||
</Tag>
|
||||
);
|
||||
case 0:
|
||||
return (
|
||||
<Tag color='violet' size='large'>
|
||||
{t('按量计费')}
|
||||
</Tag>
|
||||
);
|
||||
default:
|
||||
return t('未知');
|
||||
}
|
||||
}
|
||||
|
||||
function renderAvailable(available) {
|
||||
return available ? (
|
||||
<Popover
|
||||
content={
|
||||
<div style={{ padding: 8 }}>{t('您的分组可以使用该模型')}</div>
|
||||
}
|
||||
position='top'
|
||||
key={available}
|
||||
style={{
|
||||
backgroundColor: 'rgba(var(--semi-blue-4),1)',
|
||||
borderColor: 'rgba(var(--semi-blue-4),1)',
|
||||
color: 'var(--semi-color-white)',
|
||||
borderWidth: 1,
|
||||
borderStyle: 'solid',
|
||||
}}
|
||||
>
|
||||
<IconVerify style={{ color: 'green' }} size="large" />
|
||||
</Popover>
|
||||
) : (
|
||||
<Popover
|
||||
content={
|
||||
<div style={{ padding: 8 }}>{t('您的分组无权使用该模型')}</div>
|
||||
}
|
||||
position='top'
|
||||
key={available}
|
||||
style={{
|
||||
backgroundColor: 'rgba(var(--semi-blue-4),1)',
|
||||
borderColor: 'rgba(var(--semi-blue-4),1)',
|
||||
color: 'var(--semi-color-white)',
|
||||
borderWidth: 1,
|
||||
borderStyle: 'solid',
|
||||
}}
|
||||
>
|
||||
<IconUploadError style={{ color: '#FFA54F' }} size="large" />
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '可用性',
|
||||
title: t('可用性'),
|
||||
dataIndex: 'available',
|
||||
render: (text, record, index) => {
|
||||
// if record.enable_groups contains selectedGroup, then available is true
|
||||
@@ -127,20 +129,8 @@ const ModelPricing = () => {
|
||||
sorter: (a, b) => a.available - b.available,
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<Space>
|
||||
<span>模型名称</span>
|
||||
<Input
|
||||
placeholder='模糊搜索'
|
||||
style={{ width: 200 }}
|
||||
onCompositionStart={handleCompositionStart}
|
||||
onCompositionEnd={handleCompositionEnd}
|
||||
onChange={handleChange}
|
||||
showClear
|
||||
/>
|
||||
</Space>
|
||||
),
|
||||
dataIndex: 'model_name', // 以finish_time作为dataIndex
|
||||
title: t('模型名称'),
|
||||
dataIndex: 'model_name',
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<>
|
||||
@@ -161,7 +151,7 @@ const ModelPricing = () => {
|
||||
filteredValue,
|
||||
},
|
||||
{
|
||||
title: '计费类型',
|
||||
title: t('计费类型'),
|
||||
dataIndex: 'quota_type',
|
||||
render: (text, record, index) => {
|
||||
return renderQuotaType(parseInt(text));
|
||||
@@ -169,36 +159,42 @@ const ModelPricing = () => {
|
||||
sorter: (a, b) => a.quota_type - b.quota_type,
|
||||
},
|
||||
{
|
||||
title: '可用分组',
|
||||
title: t('可用分组'),
|
||||
dataIndex: 'enable_groups',
|
||||
render: (text, record, index) => {
|
||||
|
||||
// enable_groups is a string array
|
||||
return (
|
||||
<Space>
|
||||
{text.map((group) => {
|
||||
if (group === selectedGroup) {
|
||||
return (
|
||||
<Tag
|
||||
color='blue'
|
||||
size='large'
|
||||
prefixIcon={<IconVerify />}
|
||||
>
|
||||
{group}
|
||||
</Tag>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Tag
|
||||
color='blue'
|
||||
size='large'
|
||||
onClick={() => {
|
||||
setSelectedGroup(group);
|
||||
showInfo('当前查看的分组为:' + group + ',倍率为:' + groupRatio[group]);
|
||||
}}
|
||||
>
|
||||
{group}
|
||||
</Tag>
|
||||
);
|
||||
if (usableGroup[group]) {
|
||||
if (group === selectedGroup) {
|
||||
return (
|
||||
<Tag
|
||||
color='blue'
|
||||
size='large'
|
||||
prefixIcon={<IconVerify />}
|
||||
>
|
||||
{group}
|
||||
</Tag>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Tag
|
||||
color='blue'
|
||||
size='large'
|
||||
onClick={() => {
|
||||
setSelectedGroup(group);
|
||||
showInfo(t('当前查看的分组为:{{group}},倍率为:{{ratio}}', {
|
||||
group: group,
|
||||
ratio: groupRatio[group]
|
||||
}));
|
||||
}}
|
||||
>
|
||||
{group}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
}
|
||||
})}
|
||||
</Space>
|
||||
@@ -208,10 +204,13 @@ const ModelPricing = () => {
|
||||
{
|
||||
title: () => (
|
||||
<span style={{'display':'flex','alignItems':'center'}}>
|
||||
倍率
|
||||
{t('倍率')}
|
||||
<Popover
|
||||
content={
|
||||
<div style={{ padding: 8 }}>倍率是为了方便换算不同价格的模型<br/>点击查看倍率说明</div>
|
||||
<div style={{ padding: 8 }}>
|
||||
{t('倍率是为了方便换算不同价格的模型')}<br/>
|
||||
{t('点击查看倍率说明')}
|
||||
</div>
|
||||
}
|
||||
position='top'
|
||||
style={{
|
||||
@@ -237,18 +236,18 @@ const ModelPricing = () => {
|
||||
let completionRatio = parseFloat(record.completion_ratio.toFixed(3));
|
||||
content = (
|
||||
<>
|
||||
<Text>模型:{record.quota_type === 0 ? text : '无'}</Text>
|
||||
<Text>{t('模型倍率')}:{record.quota_type === 0 ? text : t('无')}</Text>
|
||||
<br />
|
||||
<Text>补全:{record.quota_type === 0 ? completionRatio : '无'}</Text>
|
||||
<Text>{t('补全倍率')}:{record.quota_type === 0 ? completionRatio : t('无')}</Text>
|
||||
<br />
|
||||
<Text>分组:{groupRatio[selectedGroup]}</Text>
|
||||
<Text>{t('分组倍率')}:{groupRatio[selectedGroup]}</Text>
|
||||
</>
|
||||
);
|
||||
return <div>{content}</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '模型价格',
|
||||
title: t('模型价格'),
|
||||
dataIndex: 'model_price',
|
||||
render: (text, record, index) => {
|
||||
let content = text;
|
||||
@@ -261,14 +260,14 @@ const ModelPricing = () => {
|
||||
groupRatio[selectedGroup];
|
||||
content = (
|
||||
<>
|
||||
<Text>提示 ${inputRatioPrice} / 1M tokens</Text>
|
||||
<Text>{t('提示')} ${inputRatioPrice} / 1M tokens</Text>
|
||||
<br />
|
||||
<Text>补全 ${completionRatioPrice} / 1M tokens</Text>
|
||||
<Text>{t('补全')} ${completionRatioPrice} / 1M tokens</Text>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
let price = parseFloat(text) * groupRatio[selectedGroup];
|
||||
content = <>模型价格:${price}</>;
|
||||
content = <>${t('模型价格')}:${price}</>;
|
||||
}
|
||||
return <div>{content}</div>;
|
||||
},
|
||||
@@ -279,6 +278,7 @@ const ModelPricing = () => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [userState, userDispatch] = useContext(UserContext);
|
||||
const [groupRatio, setGroupRatio] = useState({});
|
||||
const [usableGroup, setUsableGroup] = useState({});
|
||||
|
||||
const setModelsFormat = (models, groupRatio) => {
|
||||
for (let i = 0; i < models.length; i++) {
|
||||
@@ -313,9 +313,10 @@ const ModelPricing = () => {
|
||||
let url = '';
|
||||
url = `/api/pricing`;
|
||||
const res = await API.get(url);
|
||||
const { success, message, data, group_ratio } = res.data;
|
||||
const { success, message, data, group_ratio, usable_group } = res.data;
|
||||
if (success) {
|
||||
setGroupRatio(group_ratio);
|
||||
setUsableGroup(usable_group);
|
||||
setSelectedGroup(userState.user ? userState.user.group : 'default')
|
||||
setModelsFormat(data, group_ratio);
|
||||
} else {
|
||||
@@ -349,41 +350,62 @@ const ModelPricing = () => {
|
||||
type="success"
|
||||
fullMode={false}
|
||||
closeIcon="null"
|
||||
description={`您的默认分组为:${userState.user.group},分组倍率为:${groupRatio[userState.user.group]}`}
|
||||
description={t('您的默认分组为:{{group}},分组倍率为:{{ratio}}', {
|
||||
group: userState.user.group,
|
||||
ratio: groupRatio[userState.user.group]
|
||||
})}
|
||||
/>
|
||||
) : (
|
||||
<Banner
|
||||
type='warning'
|
||||
fullMode={false}
|
||||
closeIcon="null"
|
||||
description={`您还未登陆,显示的价格为默认分组倍率: ${groupRatio['default']}`}
|
||||
description={t('您还未登陆,显示的价格为默认分组倍率: {{ratio}}', {
|
||||
ratio: groupRatio['default']
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
<br/>
|
||||
<Banner
|
||||
type="info"
|
||||
fullMode={false}
|
||||
description={<div>按量计费费用 = 分组倍率 × 模型倍率 × (提示token数 + 补全token数 × 补全倍率)/ 500000 (单位:美元)</div>}
|
||||
description={<div>{t('按量计费费用 = 分组倍率 × 模型倍率 × (提示token数 + 补全token数 × 补全倍率)/ 500000 (单位:美元)')}</div>}
|
||||
closeIcon="null"
|
||||
/>
|
||||
<br/>
|
||||
<Button
|
||||
theme='light'
|
||||
type='tertiary'
|
||||
style={{width: 150}}
|
||||
onClick={() => {
|
||||
copyText(selectedRowKeys);
|
||||
}}
|
||||
disabled={selectedRowKeys == ""}
|
||||
>
|
||||
复制选中模型
|
||||
</Button>
|
||||
<Space style={{ marginBottom: 16 }}>
|
||||
<Input
|
||||
placeholder={t('模糊搜索模型名称')}
|
||||
style={{ width: 200 }}
|
||||
onCompositionStart={handleCompositionStart}
|
||||
onCompositionEnd={handleCompositionEnd}
|
||||
onChange={handleChange}
|
||||
showClear
|
||||
/>
|
||||
<Button
|
||||
theme='light'
|
||||
type='tertiary'
|
||||
style={{width: 150}}
|
||||
onClick={() => {
|
||||
copyText(selectedRowKeys);
|
||||
}}
|
||||
disabled={selectedRowKeys == ""}
|
||||
>
|
||||
{t('复制选中模型')}
|
||||
</Button>
|
||||
</Space>
|
||||
<Table
|
||||
style={{ marginTop: 5 }}
|
||||
columns={columns}
|
||||
dataSource={models}
|
||||
loading={loading}
|
||||
pagination={{
|
||||
formatPageText: (page) =>
|
||||
t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
|
||||
start: page.currentStart,
|
||||
end: page.currentEnd,
|
||||
total: models.length
|
||||
}),
|
||||
pageSize: models.length,
|
||||
showSizeChanger: false,
|
||||
}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Card, Spin } from '@douyinfe/semi-ui';
|
||||
import { Card, Spin, Tabs } from '@douyinfe/semi-ui';
|
||||
import SettingsGeneral from '../pages/Setting/Operation/SettingsGeneral.js';
|
||||
import SettingsDrawing from '../pages/Setting/Operation/SettingsDrawing.js';
|
||||
import SettingsSensitiveWords from '../pages/Setting/Operation/SettingsSensitiveWords.js';
|
||||
@@ -8,11 +8,17 @@ import SettingsDataDashboard from '../pages/Setting/Operation/SettingsDataDashbo
|
||||
import SettingsMonitoring from '../pages/Setting/Operation/SettingsMonitoring.js';
|
||||
import SettingsCreditLimit from '../pages/Setting/Operation/SettingsCreditLimit.js';
|
||||
import SettingsMagnification from '../pages/Setting/Operation/SettingsMagnification.js';
|
||||
import ModelSettingsVisualEditor from '../pages/Setting/Operation/ModelSettingsVisualEditor.js';
|
||||
import GroupRatioSettings from '../pages/Setting/Operation/GroupRatioSettings.js';
|
||||
import ModelRatioSettings from '../pages/Setting/Operation/ModelRatioSettings.js';
|
||||
|
||||
|
||||
import { API, showError, showSuccess } from '../helpers';
|
||||
import SettingsChats from '../pages/Setting/Operation/SettingsChats.js';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const OperationSetting = () => {
|
||||
const { t } = useTranslation();
|
||||
let [inputs, setInputs] = useState({
|
||||
QuotaForNewUser: 0,
|
||||
QuotaForInviter: 0,
|
||||
@@ -137,9 +143,20 @@ const OperationSetting = () => {
|
||||
<Card style={{ marginTop: '10px' }}>
|
||||
<SettingsChats options={inputs} refresh={onRefresh} />
|
||||
</Card>
|
||||
{/* 倍率设置 */}
|
||||
{/* 分组倍率设置 */}
|
||||
<Card style={{ marginTop: '10px' }}>
|
||||
<SettingsMagnification options={inputs} refresh={onRefresh} />
|
||||
<GroupRatioSettings options={inputs} refresh={onRefresh} />
|
||||
</Card>
|
||||
{/* 合并模型倍率设置和可视化倍率设置 */}
|
||||
<Card style={{ marginTop: '10px' }}>
|
||||
<Tabs type="line">
|
||||
<Tabs.TabPane tab={t('模型倍率设置')} itemKey="model">
|
||||
<ModelRatioSettings options={inputs} refresh={onRefresh} />
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab={t('可视化倍率设置')} itemKey="visual">
|
||||
<ModelSettingsVisualEditor options={inputs} refresh={onRefresh} />
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</Card>
|
||||
</Spin>
|
||||
</>
|
||||
|
||||
@@ -2,8 +2,10 @@ import React, { useEffect, useRef, useState } from 'react';
|
||||
import { Banner, Button, Col, Form, Row } from '@douyinfe/semi-ui';
|
||||
import { API, showError, showSuccess } from '../helpers';
|
||||
import { marked } from 'marked';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const OtherSetting = () => {
|
||||
const { t } = useTranslation();
|
||||
let [inputs, setInputs] = useState({
|
||||
Notice: '',
|
||||
SystemName: '',
|
||||
@@ -54,10 +56,10 @@ const OtherSetting = () => {
|
||||
try {
|
||||
setLoadingInput((loadingInput) => ({ ...loadingInput, Notice: true }));
|
||||
await updateOption('Notice', inputs.Notice);
|
||||
showSuccess('公告已更新');
|
||||
showSuccess(t('公告已更新'));
|
||||
} catch (error) {
|
||||
console.error('公告更新失败', error);
|
||||
showError('公告更新失败');
|
||||
console.error(t('公告更新失败'), error);
|
||||
showError(t('公告更新失败'));
|
||||
} finally {
|
||||
setLoadingInput((loadingInput) => ({ ...loadingInput, Notice: false }));
|
||||
}
|
||||
@@ -72,10 +74,10 @@ const OtherSetting = () => {
|
||||
SystemName: true,
|
||||
}));
|
||||
await updateOption('SystemName', inputs.SystemName);
|
||||
showSuccess('系统名称已更新');
|
||||
showSuccess(t('系统名称已更新'));
|
||||
} catch (error) {
|
||||
console.error('系统名称更新失败', error);
|
||||
showError('系统名称更新失败');
|
||||
console.error(t('系统名称更新失败'), error);
|
||||
showError(t('系统名称更新失败'));
|
||||
} finally {
|
||||
setLoadingInput((loadingInput) => ({
|
||||
...loadingInput,
|
||||
@@ -193,17 +195,17 @@ const OtherSetting = () => {
|
||||
getFormApi={(formAPI) => (formAPISettingGeneral.current = formAPI)}
|
||||
style={{ marginBottom: 15 }}
|
||||
>
|
||||
<Form.Section text={'通用设置'}>
|
||||
<Form.Section text={t('通用设置')}>
|
||||
<Form.TextArea
|
||||
label={'公告'}
|
||||
placeholder={'在此输入新的公告内容,支持 Markdown & HTML 代码'}
|
||||
label={t('公告')}
|
||||
placeholder={t('在此输入新的公告内容,支持 Markdown & HTML 代码')}
|
||||
field={'Notice'}
|
||||
onChange={handleInputChange}
|
||||
style={{ fontFamily: 'JetBrains Mono, Consolas' }}
|
||||
autosize={{ minRows: 6, maxRows: 12 }}
|
||||
/>
|
||||
<Button onClick={submitNotice} loading={loadingInput['Notice']}>
|
||||
设置公告
|
||||
{t('设置公告')}
|
||||
</Button>
|
||||
</Form.Section>
|
||||
</Form>
|
||||
@@ -213,10 +215,10 @@ const OtherSetting = () => {
|
||||
getFormApi={(formAPI) => (formAPIPersonalization.current = formAPI)}
|
||||
style={{ marginBottom: 15 }}
|
||||
>
|
||||
<Form.Section text={'个性化设置'}>
|
||||
<Form.Section text={t('个性化设置')}>
|
||||
<Form.Input
|
||||
label={'系统名称'}
|
||||
placeholder={'在此输入系统名称'}
|
||||
label={t('系统名称')}
|
||||
placeholder={t('在此输入系统名称')}
|
||||
field={'SystemName'}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
@@ -224,22 +226,20 @@ const OtherSetting = () => {
|
||||
onClick={submitSystemName}
|
||||
loading={loadingInput['SystemName']}
|
||||
>
|
||||
设置系统名称
|
||||
{t('设置系统名称')}
|
||||
</Button>
|
||||
<Form.Input
|
||||
label={'Logo 图片地址'}
|
||||
placeholder={'在此输入 Logo 图片地址'}
|
||||
label={t('Logo 图片地址')}
|
||||
placeholder={t('在此输入 Logo 图片地址')}
|
||||
field={'Logo'}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<Button onClick={submitLogo} loading={loadingInput['Logo']}>
|
||||
设置 Logo
|
||||
{t('设置 Logo')}
|
||||
</Button>
|
||||
<Form.TextArea
|
||||
label={'首页内容'}
|
||||
placeholder={
|
||||
'在此输入首页内容,支持 Markdown & HTML 代码,设置后首页的状态信息将不再显示。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为首页。'
|
||||
}
|
||||
label={t('首页内容')}
|
||||
placeholder={t('在此输入首页内容,支持 Markdown & HTML 代码,设置后首页的状态信息将不再显示。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为首页')}
|
||||
field={'HomePageContent'}
|
||||
onChange={handleInputChange}
|
||||
style={{ fontFamily: 'JetBrains Mono, Consolas' }}
|
||||
@@ -249,39 +249,35 @@ const OtherSetting = () => {
|
||||
onClick={() => submitOption('HomePageContent')}
|
||||
loading={loadingInput['HomePageContent']}
|
||||
>
|
||||
设置首页内容
|
||||
{t('设置首页内容')}
|
||||
</Button>
|
||||
<Form.TextArea
|
||||
label={'关于'}
|
||||
placeholder={
|
||||
'在此输入新的关于内容,支持 Markdown & HTML 代码。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为关于页面。'
|
||||
}
|
||||
label={t('关于')}
|
||||
placeholder={t('在此输入新的关于内容,支持 Markdown & HTML 代码。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为关于页面')}
|
||||
field={'About'}
|
||||
onChange={handleInputChange}
|
||||
style={{ fontFamily: 'JetBrains Mono, Consolas' }}
|
||||
autosize={{ minRows: 6, maxRows: 12 }}
|
||||
/>
|
||||
<Button onClick={submitAbout} loading={loadingInput['About']}>
|
||||
设置关于
|
||||
{t('设置关于')}
|
||||
</Button>
|
||||
{/* */}
|
||||
<Banner
|
||||
fullMode={false}
|
||||
type='info'
|
||||
description='移除 One API 的版权标识必须首先获得授权,项目维护需要花费大量精力,如果本项目对你有意义,请主动支持本项目。'
|
||||
description={t('移除 One API 的版权标识必须首先获得授权,项目维护需要花费大量精力,如果本项目对你有意义,请主动支持本项目')}
|
||||
closeIcon={null}
|
||||
style={{ marginTop: 15 }}
|
||||
/>
|
||||
<Form.Input
|
||||
label={'页脚'}
|
||||
placeholder={
|
||||
'在此输入新的页脚,留空则使用默认页脚,支持 HTML 代码'
|
||||
}
|
||||
label={t('页脚')}
|
||||
placeholder={t('在此输入新的页脚,留空则使用默认页脚,支持 HTML 代码')}
|
||||
field={'Footer'}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<Button onClick={submitFooter} loading={loadingInput['Footer']}>
|
||||
设置页脚
|
||||
{t('设置页脚')}
|
||||
</Button>
|
||||
</Form.Section>
|
||||
</Form>
|
||||
|
||||
@@ -4,13 +4,65 @@ import SiderBar from './SiderBar.js';
|
||||
import App from '../App.js';
|
||||
import FooterBar from './Footer.js';
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
import React, { useContext } from 'react';
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import { StyleContext } from '../context/Style/index.js';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { API, getLogo, getSystemName, showError } from '../helpers/index.js';
|
||||
import { setStatusData } from '../helpers/data.js';
|
||||
import { UserContext } from '../context/User/index.js';
|
||||
import { StatusContext } from '../context/Status/index.js';
|
||||
const { Sider, Content, Header, Footer } = Layout;
|
||||
|
||||
|
||||
const PageLayout = () => {
|
||||
const [userState, userDispatch] = useContext(UserContext);
|
||||
const [statusState, statusDispatch] = useContext(StatusContext);
|
||||
const [styleState, styleDispatch] = useContext(StyleContext);
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
const loadUser = () => {
|
||||
let user = localStorage.getItem('user');
|
||||
if (user) {
|
||||
let data = JSON.parse(user);
|
||||
userDispatch({ type: 'login', payload: data });
|
||||
}
|
||||
};
|
||||
|
||||
const loadStatus = async () => {
|
||||
try {
|
||||
const res = await API.get('/api/status');
|
||||
const { success, data } = res.data;
|
||||
if (success) {
|
||||
statusDispatch({ type: 'set', payload: data });
|
||||
setStatusData(data);
|
||||
} else {
|
||||
showError('Unable to connect to server');
|
||||
}
|
||||
} catch (error) {
|
||||
showError('Failed to load status');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadUser();
|
||||
loadStatus().catch(console.error);
|
||||
let systemName = getSystemName();
|
||||
if (systemName) {
|
||||
document.title = systemName;
|
||||
}
|
||||
let logo = getLogo();
|
||||
if (logo) {
|
||||
let linkElement = document.querySelector("link[rel~='icon']");
|
||||
if (linkElement) {
|
||||
linkElement.href = logo;
|
||||
}
|
||||
}
|
||||
// 从localStorage获取上次使用的语言
|
||||
const savedLang = localStorage.getItem('i18nextLng');
|
||||
if (savedLang) {
|
||||
i18n.changeLanguage(savedLang);
|
||||
}
|
||||
}, [i18n]);
|
||||
|
||||
return (
|
||||
<Layout style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
|
||||
@@ -23,12 +75,12 @@ const PageLayout = () => {
|
||||
</Sider>
|
||||
<Layout>
|
||||
<Content
|
||||
style={{ overflowY: styleState.shouldInnerPadding?'auto':'hidden', padding: styleState.shouldInnerPadding? '24px': '0' }}
|
||||
style={{ overflowY: 'auto', padding: styleState.shouldInnerPadding? '24px': '0' }}
|
||||
>
|
||||
<App />
|
||||
</Content>
|
||||
<Layout.Footer>
|
||||
<FooterBar></FooterBar>
|
||||
<FooterBar />
|
||||
</Layout.Footer>
|
||||
</Layout>
|
||||
</Layout>
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
Space,
|
||||
Tag,
|
||||
Typography,
|
||||
Collapsible,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
getQuotaPerUnit,
|
||||
@@ -33,10 +34,12 @@ import {
|
||||
stringToColor,
|
||||
} from '../helpers/render';
|
||||
import TelegramLoginButton from 'react-telegram-login';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const PersonalSetting = () => {
|
||||
const [userState, userDispatch] = useContext(UserContext);
|
||||
let navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [inputs, setInputs] = useState({
|
||||
wechat_verification_code: '',
|
||||
@@ -62,6 +65,8 @@ const PersonalSetting = () => {
|
||||
const [models, setModels] = useState([]);
|
||||
const [openTransfer, setOpenTransfer] = useState(false);
|
||||
const [transferAmount, setTransferAmount] = useState(0);
|
||||
const [isModelsExpanded, setIsModelsExpanded] = useState(false);
|
||||
const MODELS_DISPLAY_COUNT = 10; // 默认显示的模型数量
|
||||
|
||||
useEffect(() => {
|
||||
// let user = localStorage.getItem('user');
|
||||
@@ -110,7 +115,7 @@ const PersonalSetting = () => {
|
||||
if (success) {
|
||||
setSystemToken(data);
|
||||
await copy(data);
|
||||
showSuccess(`令牌已重置并已复制到剪贴板`);
|
||||
showSuccess(t('令牌已重置并已复制到剪贴板'));
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
@@ -151,18 +156,18 @@ const PersonalSetting = () => {
|
||||
const handleAffLinkClick = async (e) => {
|
||||
e.target.select();
|
||||
await copy(e.target.value);
|
||||
showSuccess(`邀请链接已复制到剪切板`);
|
||||
showSuccess(t('邀请链接已复制到剪切板'));
|
||||
};
|
||||
|
||||
const handleSystemTokenClick = async (e) => {
|
||||
e.target.select();
|
||||
await copy(e.target.value);
|
||||
showSuccess(`系统令牌已复制到剪切板`);
|
||||
showSuccess(t('系统令牌已复制到剪切板'));
|
||||
};
|
||||
|
||||
const deleteAccount = async () => {
|
||||
if (inputs.self_account_deletion_confirmation !== userState.user.username) {
|
||||
showError('请输入你的账户名以确认删除!');
|
||||
showError(t('请输入你的账户名以确认删除!'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -170,7 +175,7 @@ const PersonalSetting = () => {
|
||||
const {success, message} = res.data;
|
||||
|
||||
if (success) {
|
||||
showSuccess('账户已删除!');
|
||||
showSuccess(t('账户已删除!'));
|
||||
await API.get('/api/user/logout');
|
||||
userDispatch({type: 'logout'});
|
||||
localStorage.removeItem('user');
|
||||
@@ -187,7 +192,7 @@ const PersonalSetting = () => {
|
||||
);
|
||||
const {success, message} = res.data;
|
||||
if (success) {
|
||||
showSuccess('微信账户绑定成功!');
|
||||
showSuccess(t('微信账户绑定成功!'));
|
||||
setShowWeChatBindModal(false);
|
||||
} else {
|
||||
showError(message);
|
||||
@@ -196,7 +201,7 @@ const PersonalSetting = () => {
|
||||
|
||||
const changePassword = async () => {
|
||||
if (inputs.set_new_password !== inputs.set_new_password_confirmation) {
|
||||
showError('两次输入的密码不一致!');
|
||||
showError(t('两次输入的密码不一致!'));
|
||||
return;
|
||||
}
|
||||
const res = await API.put(`/api/user/self`, {
|
||||
@@ -204,7 +209,7 @@ const PersonalSetting = () => {
|
||||
});
|
||||
const {success, message} = res.data;
|
||||
if (success) {
|
||||
showSuccess('密码修改成功!');
|
||||
showSuccess(t('密码修改成功!'));
|
||||
setShowWeChatBindModal(false);
|
||||
} else {
|
||||
showError(message);
|
||||
@@ -214,7 +219,7 @@ const PersonalSetting = () => {
|
||||
|
||||
const transfer = async () => {
|
||||
if (transferAmount < getQuotaPerUnit()) {
|
||||
showError('划转金额最低为' + renderQuota(getQuotaPerUnit()));
|
||||
showError(t('划转金额最低为') + ' ' + renderQuota(getQuotaPerUnit()));
|
||||
return;
|
||||
}
|
||||
const res = await API.post(`/api/user/aff_transfer`, {
|
||||
@@ -232,7 +237,7 @@ const PersonalSetting = () => {
|
||||
|
||||
const sendVerificationCode = async () => {
|
||||
if (inputs.email === '') {
|
||||
showError('请输入邮箱!');
|
||||
showError(t('请输入邮箱!'));
|
||||
return;
|
||||
}
|
||||
setDisableButton(true);
|
||||
@@ -246,7 +251,7 @@ const PersonalSetting = () => {
|
||||
);
|
||||
const {success, message} = res.data;
|
||||
if (success) {
|
||||
showSuccess('验证码发送成功,请检查邮箱!');
|
||||
showSuccess(t('验证码发送成功,请检查邮箱!'));
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
@@ -255,7 +260,7 @@ const PersonalSetting = () => {
|
||||
|
||||
const bindEmail = async () => {
|
||||
if (inputs.email_verification_code === '') {
|
||||
showError('请输入邮箱验证码!');
|
||||
showError(t('请输入邮箱验证码!'));
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
@@ -264,7 +269,7 @@ const PersonalSetting = () => {
|
||||
);
|
||||
const {success, message} = res.data;
|
||||
if (success) {
|
||||
showSuccess('邮箱账户绑定成功!');
|
||||
showSuccess(t('邮箱账户绑定成功!'));
|
||||
setShowEmailBindModal(false);
|
||||
userState.user.email = inputs.email;
|
||||
} else {
|
||||
@@ -287,10 +292,10 @@ const PersonalSetting = () => {
|
||||
|
||||
const copyText = async (text) => {
|
||||
if (await copy(text)) {
|
||||
showSuccess('已复制:' + text);
|
||||
showSuccess(t('已复制:') + text);
|
||||
} else {
|
||||
// setSearchKeyword(text);
|
||||
Modal.error({title: '无法复制到剪贴板,请手动复制', content: text});
|
||||
Modal.error({title: t('无法复制到剪贴板,请手动复制'), content: text});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -299,7 +304,7 @@ const PersonalSetting = () => {
|
||||
<Layout>
|
||||
<Layout.Content>
|
||||
<Modal
|
||||
title='请输入要划转的数量'
|
||||
title={t('请输入要划转的数量')}
|
||||
visible={openTransfer}
|
||||
onOk={transfer}
|
||||
onCancel={handleCancel}
|
||||
@@ -308,7 +313,7 @@ const PersonalSetting = () => {
|
||||
centered={true}
|
||||
>
|
||||
<div style={{marginTop: 20}}>
|
||||
<Typography.Text>{`可用额度${renderQuotaWithPrompt(userState?.user?.aff_quota)}`}</Typography.Text>
|
||||
<Typography.Text>{t('可用额度')}{renderQuotaWithPrompt(userState?.user?.aff_quota)}</Typography.Text>
|
||||
<Input
|
||||
style={{marginTop: 5}}
|
||||
value={userState?.user?.aff_quota}
|
||||
@@ -317,8 +322,7 @@ const PersonalSetting = () => {
|
||||
</div>
|
||||
<div style={{marginTop: 20}}>
|
||||
<Typography.Text>
|
||||
{`划转额度${renderQuotaWithPrompt(transferAmount)} 最低` +
|
||||
renderQuota(getQuotaPerUnit())}
|
||||
{t('划转额度')}{renderQuotaWithPrompt(transferAmount)} {t('最低') + renderQuota(getQuotaPerUnit())}
|
||||
</Typography.Text>
|
||||
<div>
|
||||
<InputNumber
|
||||
@@ -348,9 +352,9 @@ const PersonalSetting = () => {
|
||||
title={<Typography.Text>{getUsername()}</Typography.Text>}
|
||||
description={
|
||||
isRoot() ? (
|
||||
<Tag color='red'>管理员</Tag>
|
||||
<Tag color='red'>{t('管理员')}</Tag>
|
||||
) : (
|
||||
<Tag color='blue'>普通用户</Tag>
|
||||
<Tag color='blue'>{t('普通用户')}</Tag>
|
||||
)
|
||||
}
|
||||
></Card.Meta>
|
||||
@@ -363,15 +367,89 @@ const PersonalSetting = () => {
|
||||
</Space>
|
||||
</>
|
||||
}
|
||||
footer={
|
||||
<>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Typography.Title heading={6}>{t('可用模型')}</Typography.Title>
|
||||
</div>
|
||||
<div style={{marginTop: 10}}>
|
||||
{models.length <= MODELS_DISPLAY_COUNT ? (
|
||||
<Space wrap>
|
||||
{models.map((model) => (
|
||||
<Tag
|
||||
key={model}
|
||||
color='cyan'
|
||||
onClick={() => {
|
||||
copyText(model);
|
||||
}}
|
||||
>
|
||||
{model}
|
||||
</Tag>
|
||||
))}
|
||||
</Space>
|
||||
) : (
|
||||
<>
|
||||
<Collapsible isOpen={isModelsExpanded}>
|
||||
<Space wrap>
|
||||
{models.map((model) => (
|
||||
<Tag
|
||||
key={model}
|
||||
color='cyan'
|
||||
onClick={() => {
|
||||
copyText(model);
|
||||
}}
|
||||
>
|
||||
{model}
|
||||
</Tag>
|
||||
))}
|
||||
<Tag
|
||||
color='blue'
|
||||
type="light"
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => setIsModelsExpanded(false)}
|
||||
>
|
||||
{t('收起')}
|
||||
</Tag>
|
||||
</Space>
|
||||
</Collapsible>
|
||||
{!isModelsExpanded && (
|
||||
<Space wrap>
|
||||
{models.slice(0, MODELS_DISPLAY_COUNT).map((model) => (
|
||||
<Tag
|
||||
key={model}
|
||||
color='cyan'
|
||||
onClick={() => {
|
||||
copyText(model);
|
||||
}}
|
||||
>
|
||||
{model}
|
||||
</Tag>
|
||||
))}
|
||||
<Tag
|
||||
color='blue'
|
||||
type="light"
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => setIsModelsExpanded(true)}
|
||||
>
|
||||
{t('更多')} {models.length - MODELS_DISPLAY_COUNT} {t('个模型')}
|
||||
</Tag>
|
||||
</Space>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
|
||||
}
|
||||
>
|
||||
<Descriptions row>
|
||||
<Descriptions.Item itemKey='当前余额'>
|
||||
<Descriptions.Item itemKey={t('当前余额')}>
|
||||
{renderQuota(userState?.user?.quota)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item itemKey='历史消耗'>
|
||||
<Descriptions.Item itemKey={t('历史消耗')}>
|
||||
{renderQuota(userState?.user?.used_quota)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item itemKey='请求次数'>
|
||||
<Descriptions.Item itemKey={t('请求次数')}>
|
||||
{userState.user?.request_count}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
@@ -380,7 +458,7 @@ const PersonalSetting = () => {
|
||||
style={{marginTop: 10}}
|
||||
footer={
|
||||
<div>
|
||||
<Typography.Text>邀请链接</Typography.Text>
|
||||
<Typography.Text>{t('邀请链接')}</Typography.Text>
|
||||
<Input
|
||||
style={{marginTop: 10}}
|
||||
value={affLink}
|
||||
@@ -390,35 +468,35 @@ const PersonalSetting = () => {
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Typography.Title heading={6}>邀请信息</Typography.Title>
|
||||
<Typography.Title heading={6}>{t('邀请信息')}</Typography.Title>
|
||||
<div style={{marginTop: 10}}>
|
||||
<Descriptions row>
|
||||
<Descriptions.Item itemKey='待使用收益'>
|
||||
<span style={{color: 'rgba(var(--semi-red-5), 1)'}}>
|
||||
{renderQuota(userState?.user?.aff_quota)}
|
||||
</span>
|
||||
<Descriptions.Item itemKey={t('待使用收益')}>
|
||||
<span style={{color: 'rgba(var(--semi-red-5), 1)'}}>
|
||||
{renderQuota(userState?.user?.aff_quota)}
|
||||
</span>
|
||||
<Button
|
||||
type={'secondary'}
|
||||
onClick={() => setOpenTransfer(true)}
|
||||
size={'small'}
|
||||
style={{marginLeft: 10}}
|
||||
>
|
||||
划转
|
||||
{t('划转')}
|
||||
</Button>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item itemKey='总收益'>
|
||||
<Descriptions.Item itemKey={t('总收益')}>
|
||||
{renderQuota(userState?.user?.aff_history_quota)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item itemKey='邀请人数'>
|
||||
<Descriptions.Item itemKey={t('邀请人数')}>
|
||||
{userState?.user?.aff_count}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</div>
|
||||
</Card>
|
||||
<Card style={{marginTop: 10}}>
|
||||
<Typography.Title heading={6}>个人信息</Typography.Title>
|
||||
<Typography.Title heading={6}>{t('个人信息')}</Typography.Title>
|
||||
<div style={{marginTop: 20}}>
|
||||
<Typography.Text strong>邮箱</Typography.Text>
|
||||
<Typography.Text strong>{t('邮箱')}</Typography.Text>
|
||||
<div
|
||||
style={{display: 'flex', justifyContent: 'space-between'}}
|
||||
>
|
||||
@@ -427,7 +505,7 @@ const PersonalSetting = () => {
|
||||
value={
|
||||
userState.user && userState.user.email !== ''
|
||||
? userState.user.email
|
||||
: '未绑定'
|
||||
: t('未绑定')
|
||||
}
|
||||
readonly={true}
|
||||
></Input>
|
||||
@@ -439,14 +517,14 @@ const PersonalSetting = () => {
|
||||
}}
|
||||
>
|
||||
{userState.user && userState.user.email !== ''
|
||||
? '修改绑定'
|
||||
: '绑定邮箱'}
|
||||
? t('修改绑定')
|
||||
: t('绑定邮箱')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{marginTop: 10}}>
|
||||
<Typography.Text strong>微信</Typography.Text>
|
||||
<Typography.Text strong>{t('微信')}</Typography.Text>
|
||||
<div
|
||||
style={{display: 'flex', justifyContent: 'space-between'}}
|
||||
>
|
||||
@@ -454,8 +532,8 @@ const PersonalSetting = () => {
|
||||
<Input
|
||||
value={
|
||||
userState.user && userState.user.wechat_id !== ''
|
||||
? '已绑定'
|
||||
: '未绑定'
|
||||
? t('已绑定')
|
||||
: t('未绑定')
|
||||
}
|
||||
readonly={true}
|
||||
></Input>
|
||||
@@ -467,13 +545,13 @@ const PersonalSetting = () => {
|
||||
!status.wechat_login
|
||||
}
|
||||
>
|
||||
{status.wechat_login ? '绑定' : '未启用'}
|
||||
{status.wechat_login ? t('绑定') : t('未启用')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{marginTop: 10}}>
|
||||
<Typography.Text strong>GitHub</Typography.Text>
|
||||
<Typography.Text strong>{t('GitHub')}</Typography.Text>
|
||||
<div
|
||||
style={{display: 'flex', justifyContent: 'space-between'}}
|
||||
>
|
||||
@@ -482,7 +560,7 @@ const PersonalSetting = () => {
|
||||
value={
|
||||
userState.user && userState.user.github_id !== ''
|
||||
? userState.user.github_id
|
||||
: '未绑定'
|
||||
: t('未绑定')
|
||||
}
|
||||
readonly={true}
|
||||
></Input>
|
||||
@@ -497,13 +575,13 @@ const PersonalSetting = () => {
|
||||
!status.github_oauth
|
||||
}
|
||||
>
|
||||
{status.github_oauth ? '绑定' : '未启用'}
|
||||
{status.github_oauth ? t('绑定') : t('未启用')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{marginTop: 10}}>
|
||||
<Typography.Text strong>Telegram</Typography.Text>
|
||||
<Typography.Text strong>{t('Telegram')}</Typography.Text>
|
||||
<div
|
||||
style={{display: 'flex', justifyContent: 'space-between'}}
|
||||
>
|
||||
@@ -512,7 +590,7 @@ const PersonalSetting = () => {
|
||||
value={
|
||||
userState.user && userState.user.telegram_id !== ''
|
||||
? userState.user.telegram_id
|
||||
: '未绑定'
|
||||
: t('未绑定')
|
||||
}
|
||||
readonly={true}
|
||||
></Input>
|
||||
@@ -520,7 +598,7 @@ const PersonalSetting = () => {
|
||||
<div>
|
||||
{status.telegram_oauth ? (
|
||||
userState.user.telegram_id !== '' ? (
|
||||
<Button disabled={true}>已绑定</Button>
|
||||
<Button disabled={true}>{t('已绑定')}</Button>
|
||||
) : (
|
||||
<TelegramLoginButton
|
||||
dataAuthUrl='/api/oauth/telegram/bind'
|
||||
@@ -528,13 +606,13 @@ const PersonalSetting = () => {
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<Button disabled={true}>未启用</Button>
|
||||
<Button disabled={true}>{t('未启用')}</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{marginTop: 10}}>
|
||||
<Typography.Text strong>LinuxDO</Typography.Text>
|
||||
<Typography.Text strong>{t('LinuxDO')}</Typography.Text>
|
||||
<div
|
||||
style={{display: 'flex', justifyContent: 'space-between'}}
|
||||
>
|
||||
@@ -543,7 +621,7 @@ const PersonalSetting = () => {
|
||||
value={
|
||||
userState.user && userState.user.linux_do_id !== ''
|
||||
? userState.user.linux_do_id
|
||||
: '未绑定'
|
||||
: t('未绑定')
|
||||
}
|
||||
readonly={true}
|
||||
></Input>
|
||||
@@ -558,7 +636,7 @@ const PersonalSetting = () => {
|
||||
!status.linuxdo_oauth
|
||||
}
|
||||
>
|
||||
{status.linuxdo_oauth ? '绑定' : '未启用'}
|
||||
{status.linuxdo_oauth ? t('绑定') : t('未启用')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -566,14 +644,14 @@ const PersonalSetting = () => {
|
||||
<div style={{marginTop: 10}}>
|
||||
<Space>
|
||||
<Button onClick={generateAccessToken}>
|
||||
生成系统访问令牌
|
||||
{t('生成系统访问令牌')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowChangePasswordModal(true);
|
||||
}}
|
||||
>
|
||||
修改密码
|
||||
{t('修改密码')}
|
||||
</Button>
|
||||
<Button
|
||||
type={'danger'}
|
||||
@@ -581,7 +659,7 @@ const PersonalSetting = () => {
|
||||
setShowAccountDeleteModal(true);
|
||||
}}
|
||||
>
|
||||
删除个人账户
|
||||
{t('删除个人账户')}
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
@@ -599,7 +677,7 @@ const PersonalSetting = () => {
|
||||
setShowWeChatBindModal(true);
|
||||
}}
|
||||
>
|
||||
绑定微信账号
|
||||
{t('绑定微信账号')}
|
||||
</Button>
|
||||
)}
|
||||
<Modal
|
||||
@@ -623,7 +701,7 @@ const PersonalSetting = () => {
|
||||
}
|
||||
/>
|
||||
<Button color='' fluid size='large' onClick={bindWeChat}>
|
||||
绑定
|
||||
{t('绑定')}
|
||||
</Button>
|
||||
</Modal>
|
||||
</div>
|
||||
@@ -637,7 +715,7 @@ const PersonalSetting = () => {
|
||||
centered={true}
|
||||
maskClosable={false}
|
||||
>
|
||||
<Typography.Title heading={6}>绑定邮箱地址</Typography.Title>
|
||||
<Typography.Title heading={6}>{t('绑定邮箱地址')}</Typography.Title>
|
||||
<div
|
||||
style={{
|
||||
marginTop: 20,
|
||||
@@ -729,7 +807,7 @@ const PersonalSetting = () => {
|
||||
<div style={{marginTop: 20}}>
|
||||
<Input
|
||||
name='set_new_password'
|
||||
placeholder='新密码'
|
||||
placeholder={t('新密码')}
|
||||
value={inputs.set_new_password}
|
||||
onChange={(value) =>
|
||||
handleInputChange('set_new_password', value)
|
||||
@@ -738,7 +816,7 @@ const PersonalSetting = () => {
|
||||
<Input
|
||||
style={{marginTop: 20}}
|
||||
name='set_new_password_confirmation'
|
||||
placeholder='确认新密码'
|
||||
placeholder={t('确认新密码')}
|
||||
value={inputs.set_new_password_confirmation}
|
||||
onChange={(value) =>
|
||||
handleInputChange('set_new_password_confirmation', value)
|
||||
|
||||
@@ -19,55 +19,55 @@ import {
|
||||
Tag,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import EditRedemption from '../pages/Redemption/EditRedemption';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function renderTimestamp(timestamp) {
|
||||
return <>{timestamp2string(timestamp)}</>;
|
||||
}
|
||||
|
||||
function renderStatus(status) {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return (
|
||||
<Tag color='green' size='large'>
|
||||
未使用
|
||||
</Tag>
|
||||
);
|
||||
case 2:
|
||||
return (
|
||||
<Tag color='red' size='large'>
|
||||
{' '}
|
||||
已禁用{' '}
|
||||
</Tag>
|
||||
);
|
||||
case 3:
|
||||
return (
|
||||
<Tag color='grey' size='large'>
|
||||
{' '}
|
||||
已使用{' '}
|
||||
</Tag>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Tag color='black' size='large'>
|
||||
{' '}
|
||||
未知状态{' '}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const RedemptionsTable = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const renderStatus = (status) => {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return (
|
||||
<Tag color='green' size='large'>
|
||||
{t('未使用')}
|
||||
</Tag>
|
||||
);
|
||||
case 2:
|
||||
return (
|
||||
<Tag color='red' size='large'>
|
||||
{t('已禁用')}
|
||||
</Tag>
|
||||
);
|
||||
case 3:
|
||||
return (
|
||||
<Tag color='grey' size='large'>
|
||||
{t('已使用')}
|
||||
</Tag>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Tag color='black' size='large'>
|
||||
{t('未知状态')}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'ID',
|
||||
title: t('ID'),
|
||||
dataIndex: 'id',
|
||||
},
|
||||
{
|
||||
title: '名称',
|
||||
title: t('名称'),
|
||||
dataIndex: 'name',
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
title: t('状态'),
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
render: (text, record, index) => {
|
||||
@@ -75,24 +75,24 @@ const RedemptionsTable = () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '额度',
|
||||
title: t('额度'),
|
||||
dataIndex: 'quota',
|
||||
render: (text, record, index) => {
|
||||
return <div>{renderQuota(parseInt(text))}</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
title: t('创建时间'),
|
||||
dataIndex: 'created_time',
|
||||
render: (text, record, index) => {
|
||||
return <div>{renderTimestamp(text)}</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '兑换人ID',
|
||||
title: t('兑换人ID'),
|
||||
dataIndex: 'used_user_id',
|
||||
render: (text, record, index) => {
|
||||
return <div>{text === 0 ? '无' : text}</div>;
|
||||
return <div>{text === 0 ? t('无') : text}</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -102,7 +102,7 @@ const RedemptionsTable = () => {
|
||||
<div>
|
||||
<Popover content={record.key} style={{ padding: 20 }} position='top'>
|
||||
<Button theme='light' type='tertiary' style={{ marginRight: 1 }}>
|
||||
查看
|
||||
{t('查看')}
|
||||
</Button>
|
||||
</Popover>
|
||||
<Button
|
||||
@@ -113,11 +113,11 @@ const RedemptionsTable = () => {
|
||||
await copyText(record.key);
|
||||
}}
|
||||
>
|
||||
复制
|
||||
{t('复制')}
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title='确定是否要删除此兑换码?'
|
||||
content='此修改将不可逆'
|
||||
title={t('确定是否要删除此兑换码?')}
|
||||
content={t('此修改将不可逆')}
|
||||
okType={'danger'}
|
||||
position={'left'}
|
||||
onConfirm={() => {
|
||||
@@ -127,7 +127,7 @@ const RedemptionsTable = () => {
|
||||
}}
|
||||
>
|
||||
<Button theme='light' type='danger' style={{ marginRight: 1 }}>
|
||||
删除
|
||||
{t('删除')}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
{record.status === 1 ? (
|
||||
@@ -139,7 +139,7 @@ const RedemptionsTable = () => {
|
||||
manageRedemption(record.id, 'disable', record);
|
||||
}}
|
||||
>
|
||||
禁用
|
||||
{t('禁用')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
@@ -151,7 +151,7 @@ const RedemptionsTable = () => {
|
||||
}}
|
||||
disabled={record.status === 3}
|
||||
>
|
||||
启用
|
||||
{t('启用')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
@@ -164,7 +164,7 @@ const RedemptionsTable = () => {
|
||||
}}
|
||||
disabled={record.status !== 1}
|
||||
>
|
||||
编辑
|
||||
{t('编辑')}
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
@@ -239,10 +239,10 @@ const RedemptionsTable = () => {
|
||||
|
||||
const copyText = async (text) => {
|
||||
if (await copy(text)) {
|
||||
showSuccess('已复制到剪贴板!');
|
||||
showSuccess(t('已复制到剪贴板!'));
|
||||
} else {
|
||||
// setSearchKeyword(text);
|
||||
Modal.error({ title: '无法复制到剪贴板,请手动复制', content: text });
|
||||
Modal.error({ title: t('无法复制到剪贴板,请手动复制'), content: text });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -286,7 +286,7 @@ const RedemptionsTable = () => {
|
||||
}
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
showSuccess('操作成功完成!');
|
||||
showSuccess(t('操作成功完成!'));
|
||||
let redemption = res.data.data;
|
||||
let newRedemptions = [...redemptions];
|
||||
// let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx;
|
||||
@@ -381,11 +381,11 @@ const RedemptionsTable = () => {
|
||||
></EditRedemption>
|
||||
<Form onSubmit={searchRedemptions}>
|
||||
<Form.Input
|
||||
label='搜索关键字'
|
||||
label={t('搜索关键字')}
|
||||
field='keyword'
|
||||
icon='search'
|
||||
iconPosition='left'
|
||||
placeholder='关键字(id或者名称)'
|
||||
placeholder={t('关键字(id或者名称)')}
|
||||
value={searchKeyword}
|
||||
loading={searching}
|
||||
onChange={handleKeywordChange}
|
||||
@@ -404,14 +404,14 @@ const RedemptionsTable = () => {
|
||||
setShowEdit(true);
|
||||
}}
|
||||
>
|
||||
添加兑换码
|
||||
{t('添加兑换码')}
|
||||
</Button>
|
||||
<Button
|
||||
label='复制所选兑换码'
|
||||
label={t('复制所选兑换码')}
|
||||
type='warning'
|
||||
onClick={async () => {
|
||||
if (selectedKeys.length === 0) {
|
||||
showError('请至少选择一个兑换码!');
|
||||
showError(t('请至少选择一个兑换码!'));
|
||||
return;
|
||||
}
|
||||
let keys = '';
|
||||
@@ -421,7 +421,7 @@ const RedemptionsTable = () => {
|
||||
await copyText(keys);
|
||||
}}
|
||||
>
|
||||
复制所选兑换码到剪贴板
|
||||
{t('复制所选兑换码到剪贴板')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -436,7 +436,11 @@ const RedemptionsTable = () => {
|
||||
// showSizeChanger: true,
|
||||
// pageSizeOptions: [10, 20, 50, 100],
|
||||
formatPageText: (page) =>
|
||||
`第 ${page.currentStart} - ${page.currentEnd} 条,共 ${redemptions.length} 条`,
|
||||
t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
|
||||
start: page.currentStart,
|
||||
end: page.currentEnd,
|
||||
total: redemptions.length
|
||||
}),
|
||||
// onPageSizeChange: (size) => {
|
||||
// setPageSize(size);
|
||||
// setActivePage(1);
|
||||
|
||||
@@ -12,8 +12,10 @@ import WeChatIcon from './WeChatIcon.js';
|
||||
import TelegramLoginButton from 'react-telegram-login/src';
|
||||
import { setUserData } from '../helpers/data.js';
|
||||
import { UserContext } from '../context/User/index.js';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const RegisterForm = () => {
|
||||
const { t } = useTranslation();
|
||||
const [inputs, setInputs] = useState({
|
||||
username: '',
|
||||
password: '',
|
||||
@@ -182,28 +184,28 @@ const RegisterForm = () => {
|
||||
<div style={{ width: 500 }}>
|
||||
<Card>
|
||||
<Title heading={2} style={{ textAlign: 'center' }}>
|
||||
新用户注册
|
||||
{t('新用户注册')}
|
||||
</Title>
|
||||
<Form size="large">
|
||||
<Form.Input
|
||||
field={'username'}
|
||||
label={'用户名'}
|
||||
placeholder="用户名"
|
||||
label={t('用户名')}
|
||||
placeholder={t('用户名')}
|
||||
name="username"
|
||||
onChange={(value) => handleChange('username', value)}
|
||||
/>
|
||||
<Form.Input
|
||||
field={'password'}
|
||||
label={'密码'}
|
||||
placeholder="密码,最短 8 位,最长 20 位"
|
||||
label={t('密码')}
|
||||
placeholder={t('输入密码,最短 8 位,最长 20 位')}
|
||||
name="password"
|
||||
type="password"
|
||||
onChange={(value) => handleChange('password', value)}
|
||||
/>
|
||||
<Form.Input
|
||||
field={'password2'}
|
||||
label={'确认密码'}
|
||||
placeholder="确认密码"
|
||||
label={t('确认密码')}
|
||||
placeholder={t('确认密码')}
|
||||
name="password2"
|
||||
type="password"
|
||||
onChange={(value) => handleChange('password2', value)}
|
||||
@@ -212,21 +214,21 @@ const RegisterForm = () => {
|
||||
<>
|
||||
<Form.Input
|
||||
field={'email'}
|
||||
label={'邮箱'}
|
||||
placeholder="输入邮箱地址"
|
||||
label={t('邮箱')}
|
||||
placeholder={t('输入邮箱地址')}
|
||||
onChange={(value) => handleChange('email', value)}
|
||||
name="email"
|
||||
type="email"
|
||||
suffix={
|
||||
<Button onClick={sendVerificationCode} disabled={loading}>
|
||||
获取验证码
|
||||
{t('获取验证码')}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<Form.Input
|
||||
field={'verification_code'}
|
||||
label={'验证码'}
|
||||
placeholder="输入验证码"
|
||||
label={t('验证码')}
|
||||
placeholder={t('输入验证码')}
|
||||
onChange={(value) => handleChange('verification_code', value)}
|
||||
name="verification_code"
|
||||
/>
|
||||
@@ -242,7 +244,7 @@ const RegisterForm = () => {
|
||||
htmlType={'submit'}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
注册
|
||||
{t('注册')}
|
||||
</Button>
|
||||
</Form>
|
||||
<div
|
||||
@@ -253,9 +255,9 @@ const RegisterForm = () => {
|
||||
}}
|
||||
>
|
||||
<Text>
|
||||
已有账户?
|
||||
{t('已有账户?')}
|
||||
<Link to="/login">
|
||||
点击登录
|
||||
{t('点击登录')}
|
||||
</Link>
|
||||
</Text>
|
||||
</div>
|
||||
@@ -265,7 +267,7 @@ const RegisterForm = () => {
|
||||
status.linuxdo_oauth ? (
|
||||
<>
|
||||
<Divider margin='12px' align='center'>
|
||||
第三方登录
|
||||
{t('第三方登录')}
|
||||
</Divider>
|
||||
<div
|
||||
style={{
|
||||
@@ -330,12 +332,12 @@ const RegisterForm = () => {
|
||||
)}
|
||||
</Card>
|
||||
<Modal
|
||||
title='微信扫码登录'
|
||||
title={t('微信扫码登录')}
|
||||
visible={showWeChatLoginModal}
|
||||
maskClosable={true}
|
||||
onOk={onSubmitWeChatVerificationCode}
|
||||
onCancel={() => setShowWeChatLoginModal(false)}
|
||||
okText={'登录'}
|
||||
okText={t('登录')}
|
||||
size={'small'}
|
||||
centered={true}
|
||||
>
|
||||
@@ -350,14 +352,14 @@ const RegisterForm = () => {
|
||||
</div>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<p>
|
||||
微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)
|
||||
{t('微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)')}
|
||||
</p>
|
||||
</div>
|
||||
<Form size='large'>
|
||||
<Form.Input
|
||||
field={'wechat_verification_code'}
|
||||
placeholder='验证码'
|
||||
label={'验证码'}
|
||||
placeholder={t('验证码')}
|
||||
label={t('验证码')}
|
||||
value={inputs.wechat_verification_code}
|
||||
onChange={(value) =>
|
||||
handleChange('wechat_verification_code', value)
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { UserContext } from '../context/User';
|
||||
import { StatusContext } from '../context/Status';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import {
|
||||
API,
|
||||
@@ -36,6 +37,7 @@ import { StyleContext } from '../context/Style/index.js';
|
||||
// HeaderBar Buttons
|
||||
|
||||
const SiderBar = () => {
|
||||
const { t } = useTranslation();
|
||||
const [styleState, styleDispatch] = useContext(StyleContext);
|
||||
const [statusState, statusDispatch] = useContext(StatusContext);
|
||||
const defaultIsCollapsed =
|
||||
@@ -74,30 +76,26 @@ const SiderBar = () => {
|
||||
icon: <IconCommentStroked />,
|
||||
},
|
||||
{
|
||||
text: '渠道',
|
||||
text: t('渠道'),
|
||||
itemKey: 'channel',
|
||||
to: '/channel',
|
||||
icon: <IconLayers />,
|
||||
className: isAdmin() ? 'semi-navigation-item-normal' : 'tableHiddle',
|
||||
},
|
||||
{
|
||||
text: '聊天',
|
||||
text: t('聊天'),
|
||||
itemKey: 'chat',
|
||||
// to: '/chat',
|
||||
items: chatItems,
|
||||
icon: <IconComment />,
|
||||
// className: localStorage.getItem('chat_link')
|
||||
// ? 'semi-navigation-item-normal'
|
||||
// : 'tableHiddle',
|
||||
},
|
||||
{
|
||||
text: '令牌',
|
||||
text: t('令牌'),
|
||||
itemKey: 'token',
|
||||
to: '/token',
|
||||
icon: <IconKey />,
|
||||
},
|
||||
{
|
||||
text: '数据看板',
|
||||
text: t('数据看板'),
|
||||
itemKey: 'detail',
|
||||
to: '/detail',
|
||||
icon: <IconCalendarClock />,
|
||||
@@ -107,33 +105,33 @@ const SiderBar = () => {
|
||||
: 'tableHiddle',
|
||||
},
|
||||
{
|
||||
text: '兑换码',
|
||||
text: t('兑换码'),
|
||||
itemKey: 'redemption',
|
||||
to: '/redemption',
|
||||
icon: <IconGift />,
|
||||
className: isAdmin() ? 'semi-navigation-item-normal' : 'tableHiddle',
|
||||
},
|
||||
{
|
||||
text: '钱包',
|
||||
text: t('钱包'),
|
||||
itemKey: 'topup',
|
||||
to: '/topup',
|
||||
icon: <IconCreditCard />,
|
||||
},
|
||||
{
|
||||
text: '用户管理',
|
||||
text: t('用户管理'),
|
||||
itemKey: 'user',
|
||||
to: '/user',
|
||||
icon: <IconUser />,
|
||||
className: isAdmin() ? 'semi-navigation-item-normal' : 'tableHiddle',
|
||||
},
|
||||
{
|
||||
text: '日志',
|
||||
text: t('日志'),
|
||||
itemKey: 'log',
|
||||
to: '/log',
|
||||
icon: <IconHistogram />,
|
||||
},
|
||||
{
|
||||
text: '绘图',
|
||||
text: t('绘图'),
|
||||
itemKey: 'midjourney',
|
||||
to: '/midjourney',
|
||||
icon: <IconImage />,
|
||||
@@ -143,7 +141,7 @@ const SiderBar = () => {
|
||||
: 'tableHiddle',
|
||||
},
|
||||
{
|
||||
text: '异步任务',
|
||||
text: t('异步任务'),
|
||||
itemKey: 'task',
|
||||
to: '/task',
|
||||
icon: <IconChecklistStroked />,
|
||||
@@ -153,52 +151,30 @@ const SiderBar = () => {
|
||||
: 'tableHiddle',
|
||||
},
|
||||
{
|
||||
text: '设置',
|
||||
text: t('设置'),
|
||||
itemKey: 'setting',
|
||||
to: '/setting',
|
||||
icon: <IconSetting />,
|
||||
},
|
||||
// {
|
||||
// text: '关于',
|
||||
// itemKey: 'about',
|
||||
// to: '/about',
|
||||
// icon: <IconAt/>
|
||||
// }
|
||||
],
|
||||
[
|
||||
localStorage.getItem('enable_data_export'),
|
||||
localStorage.getItem('enable_drawing'),
|
||||
localStorage.getItem('enable_task'),
|
||||
localStorage.getItem('chat_link'), chatItems,
|
||||
localStorage.getItem('chat_link'),
|
||||
chatItems,
|
||||
isAdmin(),
|
||||
t,
|
||||
],
|
||||
);
|
||||
|
||||
const loadStatus = async () => {
|
||||
const res = await API.get('/api/status');
|
||||
if (res === undefined) {
|
||||
return;
|
||||
}
|
||||
const { success, data } = res.data;
|
||||
if (success) {
|
||||
statusDispatch({ type: 'set', payload: data });
|
||||
setStatusData(data);
|
||||
} else {
|
||||
showError('无法正常连接至服务器!');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadStatus().then(() => {
|
||||
setIsCollapsed(
|
||||
localStorage.getItem('default_collapse_sidebar') === 'true',
|
||||
);
|
||||
});
|
||||
let localKey = window.location.pathname.split('/')[1];
|
||||
if (localKey === '') {
|
||||
localKey = 'home';
|
||||
}
|
||||
setSelectedKeys([localKey]);
|
||||
|
||||
let chatLink = localStorage.getItem('chat_link');
|
||||
if (!chatLink) {
|
||||
let chats = localStorage.getItem('chats');
|
||||
@@ -226,6 +202,8 @@ const SiderBar = () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setIsCollapsed(localStorage.getItem('default_collapse_sidebar') === 'true');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
||||
@@ -23,67 +23,66 @@ import {
|
||||
|
||||
import { IconTreeTriangleDown } from '@douyinfe/semi-icons';
|
||||
import EditToken from '../pages/Token/EditToken';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function renderTimestamp(timestamp) {
|
||||
return <>{timestamp2string(timestamp)}</>;
|
||||
}
|
||||
|
||||
function renderStatus(status, model_limits_enabled = false) {
|
||||
switch (status) {
|
||||
case 1:
|
||||
if (model_limits_enabled) {
|
||||
return (
|
||||
<Tag color='green' size='large'>
|
||||
已启用:限制模型
|
||||
</Tag>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Tag color='green' size='large'>
|
||||
已启用
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
case 2:
|
||||
return (
|
||||
<Tag color='red' size='large'>
|
||||
{' '}
|
||||
已禁用{' '}
|
||||
</Tag>
|
||||
);
|
||||
case 3:
|
||||
return (
|
||||
<Tag color='yellow' size='large'>
|
||||
{' '}
|
||||
已过期{' '}
|
||||
</Tag>
|
||||
);
|
||||
case 4:
|
||||
return (
|
||||
<Tag color='grey' size='large'>
|
||||
{' '}
|
||||
已耗尽{' '}
|
||||
</Tag>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Tag color='black' size='large'>
|
||||
{' '}
|
||||
未知状态{' '}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const TokensTable = () => {
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const renderStatus = (status, model_limits_enabled = false) => {
|
||||
switch (status) {
|
||||
case 1:
|
||||
if (model_limits_enabled) {
|
||||
return (
|
||||
<Tag color='green' size='large'>
|
||||
{t('已启用:限制模型')}
|
||||
</Tag>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Tag color='green' size='large'>
|
||||
{t('已启用')}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
case 2:
|
||||
return (
|
||||
<Tag color='red' size='large'>
|
||||
{t('已禁用')}
|
||||
</Tag>
|
||||
);
|
||||
case 3:
|
||||
return (
|
||||
<Tag color='yellow' size='large'>
|
||||
{t('已过期')}
|
||||
</Tag>
|
||||
);
|
||||
case 4:
|
||||
return (
|
||||
<Tag color='grey' size='large'>
|
||||
{t('已耗尽')}
|
||||
</Tag>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Tag color='black' size='large'>
|
||||
{t('未知状态')}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '名称',
|
||||
title: t('名称'),
|
||||
dataIndex: 'name',
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
title: t('状态'),
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
render: (text, record, index) => {
|
||||
@@ -96,21 +95,21 @@ const TokensTable = () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '已用额度',
|
||||
title: t('已用额度'),
|
||||
dataIndex: 'used_quota',
|
||||
render: (text, record, index) => {
|
||||
return <div>{renderQuota(parseInt(text))}</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '剩余额度',
|
||||
title: t('剩余额度'),
|
||||
dataIndex: 'remain_quota',
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<div>
|
||||
{record.unlimited_quota ? (
|
||||
<Tag size={'large'} color={'white'}>
|
||||
无限制
|
||||
{t('无限制')}
|
||||
</Tag>
|
||||
) : (
|
||||
<Tag size={'large'} color={'light-blue'}>
|
||||
@@ -122,19 +121,19 @@ const TokensTable = () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
title: t('创建时间'),
|
||||
dataIndex: 'created_time',
|
||||
render: (text, record, index) => {
|
||||
return <div>{renderTimestamp(text)}</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '过期时间',
|
||||
title: t('过期时间'),
|
||||
dataIndex: 'expired_time',
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<div>
|
||||
{record.expired_time === -1 ? '永不过期' : renderTimestamp(text)}
|
||||
{record.expired_time === -1 ? t('永不过期') : renderTimestamp(text)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
@@ -199,7 +198,7 @@ const TokensTable = () => {
|
||||
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
showError('聊天链接配置错误,请联系管理员');
|
||||
showError(t('聊天链接配置错误,请联系管理员'));
|
||||
}
|
||||
}
|
||||
return (
|
||||
@@ -210,7 +209,7 @@ const TokensTable = () => {
|
||||
position='top'
|
||||
>
|
||||
<Button theme='light' type='tertiary' style={{ marginRight: 1 }}>
|
||||
查看
|
||||
{t('查看')}
|
||||
</Button>
|
||||
</Popover>
|
||||
<Button
|
||||
@@ -221,24 +220,24 @@ const TokensTable = () => {
|
||||
await copyText('sk-' + record.key);
|
||||
}}
|
||||
>
|
||||
复制
|
||||
{t('复制')}
|
||||
</Button>
|
||||
<SplitButtonGroup
|
||||
style={{ marginRight: 1 }}
|
||||
aria-label='项目操作按钮组'
|
||||
aria-label={t('项目操作按钮组')}
|
||||
>
|
||||
<Button
|
||||
theme='light'
|
||||
style={{ color: 'rgba(var(--semi-teal-7), 1)' }}
|
||||
onClick={() => {
|
||||
if (chatsArray.length === 0) {
|
||||
showError('请联系管理员配置聊天链接');
|
||||
showError(t('请联系管理员配置聊天链接'));
|
||||
} else {
|
||||
onOpenLink('default', chats[0][Object.keys(chats[0])[0]], record);
|
||||
}
|
||||
}}
|
||||
>
|
||||
聊天
|
||||
{t('聊天')}
|
||||
</Button>
|
||||
<Dropdown
|
||||
trigger='click'
|
||||
@@ -256,8 +255,8 @@ const TokensTable = () => {
|
||||
</Dropdown>
|
||||
</SplitButtonGroup>
|
||||
<Popconfirm
|
||||
title='确定是否要删除此令牌?'
|
||||
content='此修改将不可逆'
|
||||
title={t('确定是否要删除此令牌?')}
|
||||
content={t('此修改将不可逆')}
|
||||
okType={'danger'}
|
||||
position={'left'}
|
||||
onConfirm={() => {
|
||||
@@ -267,7 +266,7 @@ const TokensTable = () => {
|
||||
}}
|
||||
>
|
||||
<Button theme='light' type='danger' style={{ marginRight: 1 }}>
|
||||
删除
|
||||
{t('删除')}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
{record.status === 1 ? (
|
||||
@@ -279,7 +278,7 @@ const TokensTable = () => {
|
||||
manageToken(record.id, 'disable', record);
|
||||
}}
|
||||
>
|
||||
禁用
|
||||
{t('禁用')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
@@ -290,7 +289,7 @@ const TokensTable = () => {
|
||||
manageToken(record.id, 'enable', record);
|
||||
}}
|
||||
>
|
||||
启用
|
||||
{t('启用')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
@@ -302,7 +301,7 @@ const TokensTable = () => {
|
||||
setShowEdit(true);
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
{t('编辑')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
@@ -371,10 +370,10 @@ const TokensTable = () => {
|
||||
|
||||
const copyText = async (text) => {
|
||||
if (await copy(text)) {
|
||||
showSuccess('已复制到剪贴板!');
|
||||
showSuccess(t('已复制到剪贴板!'));
|
||||
} else {
|
||||
Modal.error({
|
||||
title: '无法复制到剪贴板,请手动复制',
|
||||
title: t('无法复制到剪贴板,请手动复制'),
|
||||
content: text,
|
||||
size: 'large',
|
||||
});
|
||||
@@ -395,37 +394,6 @@ const TokensTable = () => {
|
||||
let encodedServerAddress = encodeURIComponent(serverAddress);
|
||||
url = url.replaceAll('{address}', encodedServerAddress);
|
||||
url = url.replaceAll('{key}', 'sk-' + record.key);
|
||||
// console.log(url);
|
||||
// const chatLink = localStorage.getItem('chat_link');
|
||||
// const mjLink = localStorage.getItem('chat_link2');
|
||||
// let defaultUrl;
|
||||
//
|
||||
// if (chatLink) {
|
||||
// defaultUrl =
|
||||
// chatLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
|
||||
// }
|
||||
// let url;
|
||||
// switch (type) {
|
||||
// case 'ama':
|
||||
// url = `ama://set-api-key?server=${encodedServerAddress}&key=sk-${key}`;
|
||||
// break;
|
||||
// case 'opencat':
|
||||
// url = `opencat://team/join?domain=${encodedServerAddress}&token=sk-${key}`;
|
||||
// break;
|
||||
// case 'lobe':
|
||||
// url = `https://chat-preview.lobehub.com/?settings={"keyVaults":{"openai":{"apiKey":"sk-${key}","baseURL":"${encodedServerAddress}/v1"}}}`;
|
||||
// break;
|
||||
// case 'next-mj':
|
||||
// url =
|
||||
// mjLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
|
||||
// break;
|
||||
// default:
|
||||
// if (!chatLink) {
|
||||
// showError('管理员未设置聊天链接');
|
||||
// return;
|
||||
// }
|
||||
// url = defaultUrl;
|
||||
// }
|
||||
|
||||
window.open(url, '_blank');
|
||||
};
|
||||
@@ -571,29 +539,29 @@ const TokensTable = () => {
|
||||
>
|
||||
<Form.Input
|
||||
field='keyword'
|
||||
label='搜索关键字'
|
||||
placeholder='令牌名称'
|
||||
label={t('搜索关键字')}
|
||||
placeholder={t('令牌名称')}
|
||||
value={searchKeyword}
|
||||
loading={searching}
|
||||
onChange={handleKeywordChange}
|
||||
/>
|
||||
<Form.Input
|
||||
field='token'
|
||||
label='Key'
|
||||
placeholder='密钥'
|
||||
label={t('密钥')}
|
||||
placeholder={t('密钥')}
|
||||
value={searchToken}
|
||||
loading={searching}
|
||||
onChange={handleSearchTokenChange}
|
||||
/>
|
||||
<Button
|
||||
label='查询'
|
||||
label={t('查询')}
|
||||
type='primary'
|
||||
htmlType='submit'
|
||||
className='btn-margin-right'
|
||||
onClick={searchTokens}
|
||||
style={{ marginRight: 8 }}
|
||||
>
|
||||
查询
|
||||
{t('查询')}
|
||||
</Button>
|
||||
</Form>
|
||||
<Divider style={{margin:'15px 0'}}/>
|
||||
@@ -609,14 +577,14 @@ const TokensTable = () => {
|
||||
setShowEdit(true);
|
||||
}}
|
||||
>
|
||||
添加令牌
|
||||
{t('添加令牌')}
|
||||
</Button>
|
||||
<Button
|
||||
label='复制所选令牌'
|
||||
label={t('复制所选令牌')}
|
||||
type='warning'
|
||||
onClick={async () => {
|
||||
if (selectedKeys.length === 0) {
|
||||
showError('请至少选择一个令牌!');
|
||||
showError(t('请至少选择一个令牌!'));
|
||||
return;
|
||||
}
|
||||
let keys = '';
|
||||
@@ -627,7 +595,7 @@ const TokensTable = () => {
|
||||
await copyText(keys);
|
||||
}}
|
||||
>
|
||||
复制所选令牌到剪贴板
|
||||
{t('复制所选令牌到剪贴板')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -642,7 +610,11 @@ const TokensTable = () => {
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: [10, 20, 50, 100],
|
||||
formatPageText: (page) =>
|
||||
`第 ${page.currentStart} - ${page.currentEnd} 条,共 ${tokens.length} 条`,
|
||||
t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
|
||||
start: page.currentStart,
|
||||
end: page.currentEnd,
|
||||
total: tokens.length
|
||||
}),
|
||||
onPageSizeChange: (size) => {
|
||||
setPageSize(size);
|
||||
setActivePage(1);
|
||||
|
||||
@@ -13,67 +13,69 @@ import { ITEMS_PER_PAGE } from '../constants';
|
||||
import { renderGroup, renderNumber, renderQuota } from '../helpers/render';
|
||||
import AddUser from '../pages/User/AddUser';
|
||||
import EditUser from '../pages/User/EditUser';
|
||||
|
||||
function renderRole(role) {
|
||||
switch (role) {
|
||||
case 1:
|
||||
return <Tag size='large'>普通用户</Tag>;
|
||||
case 10:
|
||||
return (
|
||||
<Tag color='yellow' size='large'>
|
||||
管理员
|
||||
</Tag>
|
||||
);
|
||||
case 100:
|
||||
return (
|
||||
<Tag color='orange' size='large'>
|
||||
超级管理员
|
||||
</Tag>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Tag color='red' size='large'>
|
||||
未知身份
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
}
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const UsersTable = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
function renderRole(role) {
|
||||
switch (role) {
|
||||
case 1:
|
||||
return <Tag size='large'>{t('普通用户')}</Tag>;
|
||||
case 10:
|
||||
return (
|
||||
<Tag color='yellow' size='large'>
|
||||
{t('管理员')}
|
||||
</Tag>
|
||||
);
|
||||
case 100:
|
||||
return (
|
||||
<Tag color='orange' size='large'>
|
||||
{t('超级管理员')}
|
||||
</Tag>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Tag color='red' size='large'>
|
||||
{t('未知身份')}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
}
|
||||
const columns = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
},
|
||||
{
|
||||
title: '用户名',
|
||||
title: t('用户名'),
|
||||
dataIndex: 'username',
|
||||
},
|
||||
{
|
||||
title: '分组',
|
||||
title: t('分组'),
|
||||
dataIndex: 'group',
|
||||
render: (text, record, index) => {
|
||||
return <div>{renderGroup(text)}</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '统计信息',
|
||||
title: t('统计信息'),
|
||||
dataIndex: 'info',
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<div>
|
||||
<Space spacing={1}>
|
||||
<Tooltip content={'剩余额度'}>
|
||||
<Tooltip content={t('剩余额度')}>
|
||||
<Tag color='white' size='large'>
|
||||
{renderQuota(record.quota)}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
<Tooltip content={'已用额度'}>
|
||||
<Tooltip content={t('已用额度')}>
|
||||
<Tag color='white' size='large'>
|
||||
{renderQuota(record.used_quota)}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
<Tooltip content={'调用次数'}>
|
||||
<Tooltip content={t('调用次数')}>
|
||||
<Tag color='white' size='large'>
|
||||
{renderNumber(record.request_count)}
|
||||
</Tag>
|
||||
@@ -84,26 +86,26 @@ const UsersTable = () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '邀请信息',
|
||||
title: t('邀请信息'),
|
||||
dataIndex: 'invite',
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<div>
|
||||
<Space spacing={1}>
|
||||
<Tooltip content={'邀请人数'}>
|
||||
<Tooltip content={t('邀请人数')}>
|
||||
<Tag color='white' size='large'>
|
||||
{renderNumber(record.aff_count)}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
<Tooltip content={'邀请总收益'}>
|
||||
<Tooltip content={t('邀请总收益')}>
|
||||
<Tag color='white' size='large'>
|
||||
{renderQuota(record.aff_history_quota)}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
<Tooltip content={'邀请人ID'}>
|
||||
<Tooltip content={t('邀请人ID')}>
|
||||
{record.inviter_id === 0 ? (
|
||||
<Tag color='white' size='large'>
|
||||
无
|
||||
{t('无')}
|
||||
</Tag>
|
||||
) : (
|
||||
<Tag color='white' size='large'>
|
||||
@@ -117,20 +119,20 @@ const UsersTable = () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '角色',
|
||||
title: t('角色'),
|
||||
dataIndex: 'role',
|
||||
render: (text, record, index) => {
|
||||
return <div>{renderRole(text)}</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
title: t('状态'),
|
||||
dataIndex: 'status',
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<div>
|
||||
{record.DeletedAt !== null ? (
|
||||
<Tag color='red'>已注销</Tag>
|
||||
<Tag color='red'>{t('已注销')}</Tag>
|
||||
) : (
|
||||
renderStatus(text)
|
||||
)}
|
||||
@@ -148,29 +150,25 @@ const UsersTable = () => {
|
||||
) : (
|
||||
<>
|
||||
<Popconfirm
|
||||
title='确定?'
|
||||
title={t('确定?')}
|
||||
okType={'warning'}
|
||||
onConfirm={() => {
|
||||
manageUser(record.id, 'promote', record);
|
||||
}}
|
||||
>
|
||||
<Button theme='light' type='warning' style={{ marginRight: 1 }}>
|
||||
提升
|
||||
{t('提升')}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
<Popconfirm
|
||||
title='确定?'
|
||||
title={t('确定?')}
|
||||
okType={'warning'}
|
||||
onConfirm={() => {
|
||||
manageUser(record.id, 'demote', record);
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
theme='light'
|
||||
type='secondary'
|
||||
style={{ marginRight: 1 }}
|
||||
>
|
||||
降级
|
||||
<Button theme='light' type='secondary' style={{ marginRight: 1 }}>
|
||||
{t('降级')}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
{record.status === 1 ? (
|
||||
@@ -182,7 +180,7 @@ const UsersTable = () => {
|
||||
manageUser(record.id, 'disable', record);
|
||||
}}
|
||||
>
|
||||
禁用
|
||||
{t('禁用')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
@@ -194,7 +192,7 @@ const UsersTable = () => {
|
||||
}}
|
||||
disabled={record.status === 3}
|
||||
>
|
||||
启用
|
||||
{t('启用')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
@@ -206,11 +204,11 @@ const UsersTable = () => {
|
||||
setShowEditUser(true);
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
{t('编辑')}
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title='确定是否要注销此用户?'
|
||||
content='相当于删除用户,此修改将不可逆'
|
||||
title={t('确定是否要注销此用户?')}
|
||||
content={t('相当于删除用户,此修改将不可逆')}
|
||||
okType={'danger'}
|
||||
position={'left'}
|
||||
onConfirm={() => {
|
||||
@@ -220,7 +218,7 @@ const UsersTable = () => {
|
||||
}}
|
||||
>
|
||||
<Button theme='light' type='danger' style={{ marginRight: 1 }}>
|
||||
注销
|
||||
{t('注销')}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</>
|
||||
@@ -327,17 +325,17 @@ const UsersTable = () => {
|
||||
const renderStatus = (status) => {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return <Tag size='large'>已激活</Tag>;
|
||||
return <Tag size='large'>{t('已激活')}</Tag>;
|
||||
case 2:
|
||||
return (
|
||||
<Tag size='large' color='red'>
|
||||
已封禁
|
||||
{t('已封禁')}
|
||||
</Tag>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Tag size='large' color='grey'>
|
||||
未知状态
|
||||
{t('未知状态')}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
@@ -408,7 +406,7 @@ const UsersTable = () => {
|
||||
if (searchKeyword === '') {
|
||||
await loadUsers(activePage - 1);
|
||||
} else {
|
||||
await searchUsers();
|
||||
await searchUsers(searchKeyword, searchGroup);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -452,41 +450,41 @@ const UsersTable = () => {
|
||||
>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<Space>
|
||||
<Form.Input
|
||||
label='搜索关键字'
|
||||
icon='search'
|
||||
field='keyword'
|
||||
iconPosition='left'
|
||||
placeholder='搜索用户的 ID,用户名,显示名称,以及邮箱地址 ...'
|
||||
value={searchKeyword}
|
||||
loading={searching}
|
||||
onChange={(value) => handleKeywordChange(value)}
|
||||
/>
|
||||
<Form.Select
|
||||
field='group'
|
||||
label='分组'
|
||||
optionList={groupOptions}
|
||||
onChange={(value) => {
|
||||
setSearchGroup(value);
|
||||
searchUsers(searchKeyword, value);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
label='查询'
|
||||
type='primary'
|
||||
htmlType='submit'
|
||||
className='btn-margin-right'
|
||||
>
|
||||
查询
|
||||
</Button>
|
||||
<Form.Input
|
||||
label={t('搜索关键字')}
|
||||
icon='search'
|
||||
field='keyword'
|
||||
iconPosition='left'
|
||||
placeholder={t('搜索用户的 ID,用户名,显示名称,以及邮箱地址 ...')}
|
||||
value={searchKeyword}
|
||||
loading={searching}
|
||||
onChange={(value) => handleKeywordChange(value)}
|
||||
/>
|
||||
<Form.Select
|
||||
field='group'
|
||||
label={t('分组')}
|
||||
optionList={groupOptions}
|
||||
onChange={(value) => {
|
||||
setSearchGroup(value);
|
||||
searchUsers(searchKeyword, value);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
theme='light'
|
||||
type='primary'
|
||||
onClick={() => {
|
||||
setShowAddUser(true);
|
||||
}}
|
||||
label={t('查询')}
|
||||
type='primary'
|
||||
htmlType='submit'
|
||||
className='btn-margin-right'
|
||||
>
|
||||
添加用户
|
||||
{t('查询')}
|
||||
</Button>
|
||||
<Button
|
||||
theme='light'
|
||||
type='primary'
|
||||
onClick={() => {
|
||||
setShowAddUser(true);
|
||||
}}
|
||||
>
|
||||
{t('添加用户')}
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
@@ -496,6 +494,12 @@ const UsersTable = () => {
|
||||
columns={columns}
|
||||
dataSource={pageData}
|
||||
pagination={{
|
||||
formatPageText: (page) =>
|
||||
t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
|
||||
start: page.currentStart,
|
||||
end: page.currentEnd,
|
||||
total: users.length
|
||||
}),
|
||||
currentPage: activePage,
|
||||
pageSize: ITEMS_PER_PAGE,
|
||||
total: userCount,
|
||||
|
||||
@@ -45,19 +45,18 @@ export const StyleProvider = ({ children }) => {
|
||||
updateIsMobile();
|
||||
|
||||
const updateShowSider = () => {
|
||||
// check pathname
|
||||
const pathname = window.location.pathname;
|
||||
if (pathname === '' || pathname === '/' || pathname.includes('/home') || pathname.includes('/chat')) {
|
||||
dispatch({ type: 'SET_SIDER', payload: false });
|
||||
dispatch({ type: 'SET_INNER_PADDING', payload: false });
|
||||
} else {
|
||||
dispatch({ type: 'SET_SIDER', payload: true });
|
||||
dispatch({ type: 'SET_INNER_PADDING', payload: true });
|
||||
}
|
||||
|
||||
if (isMobile()) {
|
||||
dispatch({ type: 'SET_SIDER', payload: false });
|
||||
} else {
|
||||
// check pathname
|
||||
const pathname = window.location.pathname;
|
||||
console.log(pathname)
|
||||
if (pathname === '' || pathname === '/' || pathname.includes('/home') || pathname.includes('/chat')) {
|
||||
dispatch({ type: 'SET_SIDER', payload: false });
|
||||
dispatch({ type: 'SET_INNER_PADDING', payload: false });
|
||||
} else {
|
||||
dispatch({ type: 'SET_SIDER', payload: true });
|
||||
dispatch({ type: 'SET_INNER_PADDING', payload: true });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import i18next from 'i18next';
|
||||
import { Tag } from '@douyinfe/semi-ui';
|
||||
|
||||
export function renderText(text, limit) {
|
||||
@@ -16,7 +17,7 @@ export function renderGroup(group) {
|
||||
if (group === '') {
|
||||
return (
|
||||
<Tag size='large' key='default' color='orange'>
|
||||
用户分组
|
||||
{i18next.t('用户分组')}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
@@ -58,6 +59,9 @@ export function renderNumber(num) {
|
||||
}
|
||||
|
||||
export function renderQuotaNumberWithDigit(num, digits = 2) {
|
||||
if (typeof num !== 'number' || isNaN(num)) {
|
||||
return 0;
|
||||
}
|
||||
let displayInCurrency = localStorage.getItem('display_in_currency');
|
||||
num = num.toFixed(digits);
|
||||
if (displayInCurrency) {
|
||||
@@ -144,14 +148,16 @@ export function renderModelPrice(
|
||||
completionRatio,
|
||||
groupRatio,
|
||||
) {
|
||||
// 1 ratio = $0.002 / 1K tokens
|
||||
if (modelPrice !== -1) {
|
||||
return '模型价格:$' + modelPrice + ' * 分组倍率:' + groupRatio + ' = $' + modelPrice * groupRatio;
|
||||
return i18next.t('模型价格:${{price}} * 分组倍率:{{ratio}} = ${{total}}', {
|
||||
price: modelPrice,
|
||||
ratio: groupRatio,
|
||||
total: modelPrice * groupRatio
|
||||
});
|
||||
} else {
|
||||
if (completionRatio === undefined) {
|
||||
completionRatio = 0;
|
||||
}
|
||||
// 这里的 *2 是因为 1倍率=0.002刀,请勿删除
|
||||
let inputRatioPrice = modelRatio * 2.0;
|
||||
let completionRatioPrice = modelRatio * 2.0 * completionRatio;
|
||||
let price =
|
||||
@@ -160,15 +166,28 @@ export function renderModelPrice(
|
||||
return (
|
||||
<>
|
||||
<article>
|
||||
<p>提示:${inputRatioPrice} * {groupRatio} = ${inputRatioPrice * groupRatio} / 1M tokens</p>
|
||||
<p>补全:${completionRatioPrice} * {groupRatio} = ${completionRatioPrice * groupRatio} / 1M tokens</p>
|
||||
<p>{i18next.t('提示:${{price}} * {{ratio}} = ${{total}} / 1M tokens', {
|
||||
price: inputRatioPrice,
|
||||
ratio: groupRatio,
|
||||
total: inputRatioPrice * groupRatio
|
||||
})}</p>
|
||||
<p>{i18next.t('补全:${{price}} * {{ratio}} = ${{total}} / 1M tokens', {
|
||||
price: completionRatioPrice,
|
||||
ratio: groupRatio,
|
||||
total: completionRatioPrice * groupRatio
|
||||
})}</p>
|
||||
<p></p>
|
||||
<p>
|
||||
提示 {inputTokens} tokens / 1M tokens * ${inputRatioPrice} + 补全{' '}
|
||||
{completionTokens} tokens / 1M tokens * ${completionRatioPrice} * 分组 {groupRatio} =
|
||||
${price.toFixed(6)}
|
||||
{i18next.t('提示 {{input}} tokens / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}', {
|
||||
input: inputTokens,
|
||||
price: inputRatioPrice,
|
||||
completion: completionTokens,
|
||||
compPrice: completionRatioPrice,
|
||||
ratio: groupRatio,
|
||||
total: price.toFixed(6)
|
||||
})}
|
||||
</p>
|
||||
<p>仅供参考,以实际扣费为准</p>
|
||||
<p>{i18next.t('仅供参考,以实际扣费为准')}</p>
|
||||
</article>
|
||||
</>
|
||||
);
|
||||
@@ -180,11 +199,16 @@ export function renderModelPriceSimple(
|
||||
modelPrice = -1,
|
||||
groupRatio,
|
||||
) {
|
||||
// 1 ratio = $0.002 / 1K tokens
|
||||
if (modelPrice !== -1) {
|
||||
return '价格:$' + modelPrice + ' * 分组:' + groupRatio;
|
||||
return i18next.t('价格:${{price}} * 分组:{{ratio}}', {
|
||||
price: modelPrice,
|
||||
ratio: groupRatio
|
||||
});
|
||||
} else {
|
||||
return '模型: ' + modelRatio + ' * 分组: ' + groupRatio;
|
||||
return i18next.t('模型: {{ratio}} * 分组: {{groupRatio}}', {
|
||||
ratio: modelRatio,
|
||||
groupRatio: groupRatio
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,8 +248,12 @@ export function renderAudioModelPrice(
|
||||
<p>音频补全:${inputRatioPrice} * {groupRatio} * {audioRatio} * {audioCompletionRatio} = ${inputRatioPrice * audioRatio * audioCompletionRatio * groupRatio} / 1M tokens</p>
|
||||
<p></p>
|
||||
<p>
|
||||
提示 {inputTokens} tokens / 1M tokens * ${inputRatioPrice} + 补全{' '}
|
||||
{completionTokens} tokens / 1M tokens * ${completionRatioPrice} +
|
||||
{i18next.t('提示 {{input}} tokens / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} +', {
|
||||
input: inputTokens,
|
||||
price: inputRatioPrice,
|
||||
completion: completionTokens,
|
||||
compPrice: completionRatioPrice
|
||||
})}
|
||||
</p>
|
||||
<p>
|
||||
音频提示 {audioInputTokens} tokens / 1M tokens * ${inputRatioPrice} * {audioRatio} + 音频补全 {audioCompletionTokens} tokens / 1M tokens * ${inputRatioPrice} * {audioRatio} * {audioCompletionRatio}
|
||||
@@ -245,7 +273,7 @@ export function renderQuotaWithPrompt(quota, digits) {
|
||||
let displayInCurrency = localStorage.getItem('display_in_currency');
|
||||
displayInCurrency = displayInCurrency === 'true';
|
||||
if (displayInCurrency) {
|
||||
return `(等价金额:${renderQuota(quota, digits)})`;
|
||||
return '|' + i18next.t('等价金额') + ': ' + renderQuota(quota, digits) + '';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
@@ -315,7 +343,7 @@ export const modelColorMap = {
|
||||
'gpt-3.5-turbo-0613': 'rgb(60,179,113)', // 海洋绿
|
||||
'gpt-3.5-turbo-1106': 'rgb(32,178,170)', // 浅海洋绿
|
||||
'gpt-3.5-turbo-16k': 'rgb(149,252,206)', // 淡橙色
|
||||
'gpt-3.5-turbo-16k-0613': 'rgb(119,255,214)', // 淡桃色
|
||||
'gpt-3.5-turbo-16k-0613': 'rgb(119,255,214)', // 淡桃<EFBFBD><EFBFBD><EFBFBD>
|
||||
'gpt-3.5-turbo-instruct': 'rgb(175,238,238)', // 粉蓝色
|
||||
'gpt-4': 'rgb(135,206,235)', // 天蓝色
|
||||
// 'gpt-4-0314': 'rgb(70,130,180)', // 钢蓝色
|
||||
@@ -338,7 +366,7 @@ export const modelColorMap = {
|
||||
'text-embedding-ada-002': 'rgb(255,182,193)', // 浅粉红
|
||||
'text-embedding-v1': 'rgb(255,174,185)', // 浅粉红色(略有区别)
|
||||
'text-moderation-latest': 'rgb(255,130,171)', // 强粉色
|
||||
'text-moderation-stable': 'rgb(255,160,122)', // 浅珊瑚色(与Babbage相同,表示同一类功能)
|
||||
'text-moderation-stable': 'rgb(255,160,122)', // 浅珊瑚色(<EFBFBD><EFBFBD><EFBFBD>Babbage相同,表示同一类功能)
|
||||
'tts-1': 'rgb(255,140,0)', // 深橙色
|
||||
'tts-1-1106': 'rgb(255,165,0)', // 橙色
|
||||
'tts-1-hd': 'rgb(255,215,0)', // 金色
|
||||
|
||||
@@ -180,6 +180,9 @@ export function timestamp2string1(timestamp, dataExportDefaultTime = 'hour') {
|
||||
let month = (date.getMonth() + 1).toString();
|
||||
let day = date.getDate().toString();
|
||||
let hour = date.getHours().toString();
|
||||
if (day === '24') {
|
||||
console.log("timestamp", timestamp);
|
||||
}
|
||||
if (month.length === 1) {
|
||||
month = '0' + month;
|
||||
}
|
||||
|
||||
26
web/src/i18n/i18n.js
Normal file
26
web/src/i18n/i18n.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
|
||||
import enTranslation from './locales/en.json';
|
||||
import zhTranslation from './locales/zh.json';
|
||||
|
||||
i18n
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources: {
|
||||
en: {
|
||||
translation: enTranslation
|
||||
},
|
||||
zh: {
|
||||
translation: zhTranslation
|
||||
}
|
||||
},
|
||||
fallbackLng: 'zh',
|
||||
interpolation: {
|
||||
escapeValue: false
|
||||
}
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
1238
web/src/i18n/locales/en copy.json
Normal file
1238
web/src/i18n/locales/en copy.json
Normal file
File diff suppressed because it is too large
Load Diff
1244
web/src/i18n/locales/en.json
Normal file
1244
web/src/i18n/locales/en.json
Normal file
File diff suppressed because it is too large
Load Diff
13
web/src/i18n/locales/zh.json
Normal file
13
web/src/i18n/locales/zh.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"首页": "首页",
|
||||
"控制台": "控制台",
|
||||
"定价": "定价",
|
||||
"关于": "关于",
|
||||
"登录": "登录",
|
||||
"注册": "注册",
|
||||
"退出": "退出",
|
||||
"语言": "语言",
|
||||
"展开侧边栏": "展开侧边栏",
|
||||
"关闭侧边栏": "关闭侧边栏",
|
||||
"注销成功!": "注销成功!"
|
||||
}
|
||||
@@ -25,17 +25,28 @@ body {
|
||||
padding-right: 30px;
|
||||
}
|
||||
|
||||
.panel-desc-card {
|
||||
/*min-width: 320px;*/
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 767px) {
|
||||
/*.semi-navigation-sub-wrap .semi-navigation-sub-title, .semi-navigation-item {*/
|
||||
/* padding: 0 0;*/
|
||||
/*}*/
|
||||
.topnav .semi-navigation-list-wrapper {
|
||||
max-width: calc(55vw - 20px);
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
#root > section > header > section > div > div > div > div.semi-navigation-footer > div > a > li {
|
||||
padding: 0 0;
|
||||
}
|
||||
#root > section > header > section > div > div > div > div.semi-navigation-header-list-outer > div.semi-navigation-list-wrapper > ul > div > a > li {
|
||||
padding: 0 5px;
|
||||
}
|
||||
#root > section > header > section > div > div > div > div.semi-navigation-footer > div:nth-child(1) > a > li {
|
||||
padding: 0 5px;
|
||||
}
|
||||
.semi-navigation-footer {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
.semi-table-tbody,
|
||||
.semi-table-row,
|
||||
.semi-table-row-cell {
|
||||
@@ -105,11 +116,6 @@ code {
|
||||
monospace;
|
||||
}
|
||||
|
||||
.semi-navigation-vertical {
|
||||
/*display: flex;*/
|
||||
/*flex-direction: column;*/
|
||||
}
|
||||
|
||||
.semi-navigation-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import { ThemeProvider } from './context/Theme';
|
||||
import FooterBar from './components/Footer';
|
||||
import { StyleProvider } from './context/Style/index.js';
|
||||
import PageLayout from './components/PageLayout.js';
|
||||
import './i18n/i18n.js';
|
||||
|
||||
// initialization
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
API,
|
||||
isMobile,
|
||||
@@ -61,6 +62,7 @@ function type2secretPrompt(type) {
|
||||
}
|
||||
|
||||
const EditChannel = (props) => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const channelId = props.editingChannel.id;
|
||||
const isEdit = channelId !== undefined;
|
||||
@@ -191,14 +193,16 @@ const EditChannel = (props) => {
|
||||
|
||||
|
||||
const fetchUpstreamModelList = async (name) => {
|
||||
if (inputs['type'] !== 1) {
|
||||
showError('仅支持 OpenAI 接口格式');
|
||||
return;
|
||||
}
|
||||
// if (inputs['type'] !== 1) {
|
||||
// showError(t('仅支持 OpenAI 接口格式'));
|
||||
// return;
|
||||
// }
|
||||
setLoading(true);
|
||||
const models = inputs['models'] || [];
|
||||
let err = false;
|
||||
|
||||
if (isEdit) {
|
||||
// 如果是编辑模式,使用已有的channel id获取模型列表
|
||||
const res = await API.get('/api/channel/fetch_models/' + channelId);
|
||||
if (res.data && res.data?.success) {
|
||||
models.push(...res.data.data);
|
||||
@@ -206,35 +210,34 @@ const EditChannel = (props) => {
|
||||
err = true;
|
||||
}
|
||||
} else {
|
||||
// 如果是新建模式,通过后端代理获取模型列表
|
||||
if (!inputs?.['key']) {
|
||||
showError('请填写密钥');
|
||||
showError(t('请填写密钥'));
|
||||
err = true;
|
||||
} else {
|
||||
try {
|
||||
const host = new URL((inputs['base_url'] || 'https://api.openai.com'));
|
||||
|
||||
const url = `https://${host.hostname}/v1/models`;
|
||||
const key = inputs['key'];
|
||||
const res = await axios.get(url, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${key}`
|
||||
}
|
||||
const res = await API.post('/api/channel/fetch_models', {
|
||||
base_url: inputs['base_url'],
|
||||
key: inputs['key']
|
||||
});
|
||||
if (res.data && res.data?.success) {
|
||||
models.push(...res.data.data.map((model) => model.id));
|
||||
|
||||
if (res.data && res.data.success) {
|
||||
models.push(...res.data.data);
|
||||
} else {
|
||||
err = true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching models:', error);
|
||||
err = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!err) {
|
||||
handleInputChange(name, Array.from(new Set(models)));
|
||||
showSuccess('获取模型列表成功');
|
||||
showSuccess(t('获取模型列表成功'));
|
||||
} else {
|
||||
showError('获取模型列表失败');
|
||||
showError(t('获取模型列表失败'));
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
@@ -305,15 +308,15 @@ const EditChannel = (props) => {
|
||||
|
||||
const submit = async () => {
|
||||
if (!isEdit && (inputs.name === '' || inputs.key === '')) {
|
||||
showInfo('请填写渠道名称和渠道密钥!');
|
||||
showInfo(t('请填写渠道名称和渠道密钥!'));
|
||||
return;
|
||||
}
|
||||
if (inputs.models.length === 0) {
|
||||
showInfo('请至少选择一个模型!');
|
||||
showInfo(t('请至少选择一个模型!'));
|
||||
return;
|
||||
}
|
||||
if (inputs.model_mapping !== '' && !verifyJSON(inputs.model_mapping)) {
|
||||
showInfo('模型映射必须是合法的 JSON 格式!');
|
||||
showInfo(t('模型映射必须是合法的 JSON 格式!'));
|
||||
return;
|
||||
}
|
||||
let localInputs = { ...inputs };
|
||||
@@ -331,7 +334,7 @@ const EditChannel = (props) => {
|
||||
}
|
||||
let res;
|
||||
if (!Array.isArray(localInputs.models)) {
|
||||
showError('提交失败,请勿重复提交!');
|
||||
showError(t('提交失败,请勿重复提交!'));
|
||||
handleCancel();
|
||||
return;
|
||||
}
|
||||
@@ -349,9 +352,9 @@ const EditChannel = (props) => {
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
if (isEdit) {
|
||||
showSuccess('渠道更新成功!');
|
||||
showSuccess(t('渠道更新成功!'));
|
||||
} else {
|
||||
showSuccess('渠道创建成功!');
|
||||
showSuccess(t('渠道创建成功!'));
|
||||
setInputs(originInputs);
|
||||
}
|
||||
props.refresh();
|
||||
@@ -363,7 +366,6 @@ const EditChannel = (props) => {
|
||||
|
||||
const addCustomModels = () => {
|
||||
if (customModel.trim() === '') return;
|
||||
// 使用逗号分隔字符串,然后去除每个模型名称前后的空格
|
||||
const modelArray = customModel.split(',').map((model) => model.trim());
|
||||
|
||||
let localModels = [...inputs.models];
|
||||
@@ -371,24 +373,21 @@ const EditChannel = (props) => {
|
||||
let hasError = false;
|
||||
|
||||
modelArray.forEach((model) => {
|
||||
// 检查模型是否已存在,且模型名称非空
|
||||
if (model && !localModels.includes(model)) {
|
||||
localModels.push(model); // 添加到模型列表
|
||||
localModels.push(model);
|
||||
localModelOptions.push({
|
||||
// 添加到下拉选项
|
||||
key: model,
|
||||
text: model,
|
||||
value: model
|
||||
});
|
||||
} else if (model) {
|
||||
showError('某些模型已存在!');
|
||||
showError(t('某些模型已存在!'));
|
||||
hasError = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (hasError) return; // 如果有错误则终止操作
|
||||
if (hasError) return;
|
||||
|
||||
// 更新状态值
|
||||
setModelOptions(localModelOptions);
|
||||
setCustomModel('');
|
||||
handleInputChange('models', localModels);
|
||||
@@ -401,7 +400,7 @@ const EditChannel = (props) => {
|
||||
maskClosable={false}
|
||||
placement={isEdit ? 'right' : 'left'}
|
||||
title={
|
||||
<Title level={3}>{isEdit ? '更新渠道信息' : '创建新的渠道'}</Title>
|
||||
<Title level={3}>{isEdit ? t('更新渠道信息') : t('创建新的渠道')}</Title>
|
||||
}
|
||||
headerStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
|
||||
bodyStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
|
||||
@@ -410,7 +409,7 @@ const EditChannel = (props) => {
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Space>
|
||||
<Button theme="solid" size={'large'} onClick={submit}>
|
||||
提交
|
||||
{t('提交')}
|
||||
</Button>
|
||||
<Button
|
||||
theme="solid"
|
||||
@@ -418,7 +417,7 @@ const EditChannel = (props) => {
|
||||
type={'tertiary'}
|
||||
onClick={handleCancel}
|
||||
>
|
||||
取消
|
||||
{t('取消')}
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
@@ -429,7 +428,7 @@ const EditChannel = (props) => {
|
||||
>
|
||||
<Spin spinning={loading}>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>类型:</Typography.Text>
|
||||
<Typography.Text strong>{t('类型')}:</Typography.Text>
|
||||
</div>
|
||||
<Select
|
||||
name="type"
|
||||
@@ -444,20 +443,7 @@ const EditChannel = (props) => {
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Banner
|
||||
type={'warning'}
|
||||
description={
|
||||
<>
|
||||
注意,<strong>模型部署名称必须和模型名称保持一致</strong>
|
||||
,因为 One API 会把请求体中的 model
|
||||
参数替换为你的部署名称(模型名称中的点会被剔除),
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://github.com/songquanpeng/one-api/issues/133?notification_referrer_id=NT_kwDOAmJSYrM2NjIwMzI3NDgyOjM5OTk4MDUw#issuecomment-1571602271"
|
||||
>
|
||||
图片演示
|
||||
</a>
|
||||
。
|
||||
</>
|
||||
}
|
||||
description={t('注意,模型部署名称必须和模型名称保持一致,因为 One API 会把请求体中的 model 参数替换为你的部署名称(模型名称中的点会被剔除)')}
|
||||
></Banner>
|
||||
</div>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
@@ -468,9 +454,7 @@ const EditChannel = (props) => {
|
||||
<Input
|
||||
label="AZURE_OPENAI_ENDPOINT"
|
||||
name="azure_base_url"
|
||||
placeholder={
|
||||
'请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com'
|
||||
}
|
||||
placeholder={t('请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com')}
|
||||
onChange={(value) => {
|
||||
handleInputChange('base_url', value);
|
||||
}}
|
||||
@@ -478,14 +462,12 @@ const EditChannel = (props) => {
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>默认 API 版本:</Typography.Text>
|
||||
<Typography.Text strong>{t('默认 API 版本')}:</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
label="默认 API 版本"
|
||||
label={t('默认 API 版本')}
|
||||
name="azure_other"
|
||||
placeholder={
|
||||
'请输入默认 API 版本,例如:2023-06-01-preview,该配置可以被实际的请求查询参数所覆盖'
|
||||
}
|
||||
placeholder={t('请输入默认 API 版本,例如:2023-06-01-preview,该配置可以被实际的请求查询参数所覆盖')}
|
||||
onChange={(value) => {
|
||||
handleInputChange('other', value);
|
||||
}}
|
||||
@@ -499,23 +481,17 @@ const EditChannel = (props) => {
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Banner
|
||||
type={'warning'}
|
||||
description={
|
||||
<>
|
||||
如果你对接的是上游One API或者New API等转发项目,请使用OpenAI类型,不要使用此类型,除非你知道你在做什么。
|
||||
</>
|
||||
}
|
||||
description={t('如果你对接的是上游One API或者New API等转发项目,请使用OpenAI类型,不要使用此类型,除非你知道你在做什么。')}
|
||||
></Banner>
|
||||
</div>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>
|
||||
完整的 Base URL,支持变量{'{model}'}:
|
||||
{t('完整的 Base URL,支持变量{model}')}:
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
name="base_url"
|
||||
placeholder={
|
||||
'请输入完整的URL,例如:https://api.openai.com/v1/chat/completions'
|
||||
}
|
||||
placeholder={t('请输入完整的URL,例如:https://api.openai.com/v1/chat/completions')}
|
||||
onChange={(value) => {
|
||||
handleInputChange('base_url', value);
|
||||
}}
|
||||
@@ -527,12 +503,12 @@ const EditChannel = (props) => {
|
||||
{inputs.type !== 3 && inputs.type !== 8 && inputs.type !== 22 && inputs.type !== 36 && (
|
||||
<>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>代理:</Typography.Text>
|
||||
<Typography.Text strong>{t('代理')}:</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
label="代理"
|
||||
label={t('代理')}
|
||||
name="base_url"
|
||||
placeholder={'此项可选,用于通过代理站来进行 API 调用'}
|
||||
placeholder={t('此项可选,用于通过代理站来进行 API 调用')}
|
||||
onChange={(value) => {
|
||||
handleInputChange('base_url', value);
|
||||
}}
|
||||
@@ -544,13 +520,11 @@ const EditChannel = (props) => {
|
||||
{inputs.type === 22 && (
|
||||
<>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>私有部署地址:</Typography.Text>
|
||||
<Typography.Text strong>{t('私有部署地址')}:</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
name="base_url"
|
||||
placeholder={
|
||||
'请输入私有部署地址,格式为:https://fastgpt.run/api/openapi'
|
||||
}
|
||||
placeholder={t('请输入私有部署地址,格式为:https://fastgpt.run/api/openapi')}
|
||||
onChange={(value) => {
|
||||
handleInputChange('base_url', value);
|
||||
}}
|
||||
@@ -563,14 +537,12 @@ const EditChannel = (props) => {
|
||||
<>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>
|
||||
注意非Chat API,请务必填写正确的API地址,否则可能导致无法使用
|
||||
{t('注意非Chat API,请务必填写正确的API地址,否则可能导致无法使用')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
name="base_url"
|
||||
placeholder={
|
||||
'请输入到 /suno 前的路径,通常就是域名,例如:https://api.example.com '
|
||||
}
|
||||
placeholder={t('请输入到 /suno 前的路径,通常就是域名,例如:https://api.example.com')}
|
||||
onChange={(value) => {
|
||||
handleInputChange('base_url', value);
|
||||
}}
|
||||
@@ -580,12 +552,12 @@ const EditChannel = (props) => {
|
||||
</>
|
||||
)}
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>名称:</Typography.Text>
|
||||
<Typography.Text strong>{t('名称')}:</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
required
|
||||
name="name"
|
||||
placeholder={'请为渠道命名'}
|
||||
placeholder={t('请为渠道命名')}
|
||||
onChange={(value) => {
|
||||
handleInputChange('name', value);
|
||||
}}
|
||||
@@ -593,16 +565,16 @@ const EditChannel = (props) => {
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>分组:</Typography.Text>
|
||||
<Typography.Text strong>{t('分组')}:</Typography.Text>
|
||||
</div>
|
||||
<Select
|
||||
placeholder={'请选择可以使用该渠道的分组'}
|
||||
placeholder={t('请选择可以使用该渠道的分组')}
|
||||
name="groups"
|
||||
required
|
||||
multiple
|
||||
selection
|
||||
allowAdditions
|
||||
additionLabel={'请在系统设置页面编辑分组倍率以添加新的分组:'}
|
||||
additionLabel={t('请在系统设置页面编辑分组倍率以添加新的分组:')}
|
||||
onChange={(value) => {
|
||||
handleInputChange('groups', value);
|
||||
}}
|
||||
@@ -631,17 +603,15 @@ const EditChannel = (props) => {
|
||||
{inputs.type === 41 && (
|
||||
<>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>部署地区:</Typography.Text>
|
||||
<Typography.Text strong>{t('部署地区')}:</Typography.Text>
|
||||
</div>
|
||||
<TextArea
|
||||
name="other"
|
||||
placeholder={
|
||||
'请输入部署地区,例如:us-central1\n支持使用模型映射格式\n' +
|
||||
placeholder={t('请输入部署地区,例如:us-central1\n支持使用模型映射格式\n' +
|
||||
'{\n' +
|
||||
' "default": "us-central1",\n' +
|
||||
' "claude-3-5-sonnet-20240620": "europe-west1"\n' +
|
||||
'}'
|
||||
}
|
||||
'}')}
|
||||
autosize={{ minRows: 2 }}
|
||||
onChange={(value) => {
|
||||
handleInputChange('other', value);
|
||||
@@ -662,14 +632,14 @@ const EditChannel = (props) => {
|
||||
);
|
||||
}}
|
||||
>
|
||||
填入模板
|
||||
{t('填入模板')}
|
||||
</Typography.Text>
|
||||
</>
|
||||
)}
|
||||
{inputs.type === 21 && (
|
||||
<>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>知识库 ID:</Typography.Text>
|
||||
<Typography.Text strong><EFBFBD><EFBFBD>识库 ID:</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
label="知识库 ID"
|
||||
@@ -702,7 +672,7 @@ const EditChannel = (props) => {
|
||||
</>
|
||||
)}
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>模型:</Typography.Text>
|
||||
<Typography.Text strong>{t('模型')}:</Typography.Text>
|
||||
</div>
|
||||
<Select
|
||||
placeholder={'请选择该渠道所支持的模型'}
|
||||
@@ -727,7 +697,7 @@ const EditChannel = (props) => {
|
||||
handleInputChange('models', basicModels);
|
||||
}}
|
||||
>
|
||||
填入相关模型
|
||||
{t('填入相关模型')}
|
||||
</Button>
|
||||
<Button
|
||||
type="secondary"
|
||||
@@ -735,16 +705,16 @@ const EditChannel = (props) => {
|
||||
handleInputChange('models', fullModels);
|
||||
}}
|
||||
>
|
||||
填入所有模型
|
||||
{t('填入所有模型')}
|
||||
</Button>
|
||||
<Tooltip content={fetchButtonTips}>
|
||||
<Tooltip content={t('新建渠道时,请求通过当前浏览器发出;编辑已有渠道,请求通过后端服务器发出')}>
|
||||
<Button
|
||||
type="tertiary"
|
||||
onClick={() => {
|
||||
fetchUpstreamModelList('models');
|
||||
}}
|
||||
>
|
||||
获取模型列表
|
||||
{t('获取模型列表')}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Button
|
||||
@@ -753,16 +723,16 @@ const EditChannel = (props) => {
|
||||
handleInputChange('models', []);
|
||||
}}
|
||||
>
|
||||
清除所有模型
|
||||
{t('清除所有模型')}
|
||||
</Button>
|
||||
</Space>
|
||||
<Input
|
||||
addonAfter={
|
||||
<Button type="primary" onClick={addCustomModels}>
|
||||
填入
|
||||
{t('填入')}
|
||||
</Button>
|
||||
}
|
||||
placeholder="输入自定义模型名称"
|
||||
placeholder={t('输入自定义模型名称')}
|
||||
value={customModel}
|
||||
onChange={(value) => {
|
||||
setCustomModel(value.trim());
|
||||
@@ -770,10 +740,10 @@ const EditChannel = (props) => {
|
||||
/>
|
||||
</div>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>模型重定向:</Typography.Text>
|
||||
<Typography.Text strong>{t('模型重定向')}:</Typography.Text>
|
||||
</div>
|
||||
<TextArea
|
||||
placeholder={`此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:\n${JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)}`}
|
||||
placeholder={t('此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:') + `\n${JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)}`}
|
||||
name="model_mapping"
|
||||
onChange={(value) => {
|
||||
handleInputChange('model_mapping', value);
|
||||
@@ -795,17 +765,17 @@ const EditChannel = (props) => {
|
||||
);
|
||||
}}
|
||||
>
|
||||
填入模板
|
||||
{t('填入模板')}
|
||||
</Typography.Text>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>密钥:</Typography.Text>
|
||||
<Typography.Text strong>{t('密钥')}:</Typography.Text>
|
||||
</div>
|
||||
{batch ? (
|
||||
<TextArea
|
||||
label="密钥"
|
||||
label={t('密钥')}
|
||||
name="key"
|
||||
required
|
||||
placeholder={'请输入密钥,一行一个'}
|
||||
placeholder={t('请输入密钥,一行一个')}
|
||||
onChange={(value) => {
|
||||
handleInputChange('key', value);
|
||||
}}
|
||||
@@ -817,7 +787,7 @@ const EditChannel = (props) => {
|
||||
<>
|
||||
{inputs.type === 41 ? (
|
||||
<TextArea
|
||||
label="鉴权json"
|
||||
label={t('鉴权json')}
|
||||
name="key"
|
||||
required
|
||||
placeholder={'{\n' +
|
||||
@@ -842,18 +812,17 @@ const EditChannel = (props) => {
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
label="密钥"
|
||||
label={t('密钥')}
|
||||
name="key"
|
||||
required
|
||||
placeholder={type2secretPrompt(inputs.type)}
|
||||
placeholder={t(type2secretPrompt(inputs.type))}
|
||||
onChange={(value) => {
|
||||
handleInputChange('key', value);
|
||||
}}
|
||||
value={inputs.key}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
)
|
||||
}
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{!isEdit && (
|
||||
@@ -861,23 +830,23 @@ const EditChannel = (props) => {
|
||||
<Space>
|
||||
<Checkbox
|
||||
checked={batch}
|
||||
label="批量创建"
|
||||
label={t('批量创建')}
|
||||
name="batch"
|
||||
onChange={() => setBatch(!batch)}
|
||||
/>
|
||||
<Typography.Text strong>批量创建</Typography.Text>
|
||||
<Typography.Text strong>{t('批量创建')}</Typography.Text>
|
||||
</Space>
|
||||
</div>
|
||||
)}
|
||||
{inputs.type === 1 && (
|
||||
<>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>组织:</Typography.Text>
|
||||
<Typography.Text strong>{t('组织')}:</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
label="组织,可选,不填则为默认组织"
|
||||
label={t('组织,可选,不填则为默认组织')}
|
||||
name="openai_organization"
|
||||
placeholder="请输入组织org-xxx"
|
||||
placeholder={t('请输入组织org-xxx')}
|
||||
onChange={(value) => {
|
||||
handleInputChange('openai_organization', value);
|
||||
}}
|
||||
@@ -886,11 +855,11 @@ const EditChannel = (props) => {
|
||||
</>
|
||||
)}
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>默认测试模型:</Typography.Text>
|
||||
<Typography.Text strong>{t('默认测试模型')}:</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
name="test_model"
|
||||
placeholder="不填则为模型列表第一个"
|
||||
placeholder={t('不填则为模型列表第一个')}
|
||||
onChange={(value) => {
|
||||
handleInputChange('test_model', value);
|
||||
}}
|
||||
@@ -904,20 +873,20 @@ const EditChannel = (props) => {
|
||||
onChange={() => {
|
||||
setAutoBan(!autoBan);
|
||||
}}
|
||||
// onChange={handleInputChange}
|
||||
/>
|
||||
<Typography.Text strong>
|
||||
是否自动禁用(仅当自动禁用开启时有效),关闭后不会自动禁用该渠道:
|
||||
{t('是否自动禁用(仅当自动禁用开启时有效),关闭后不会自动禁用该渠道:')}
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
</div>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>
|
||||
状态码复写(仅影响本地判断,不修改返回到上游的状态码):
|
||||
{t('状态码复写(仅影响本地判断,不修改返回到上游的状态码)')}:
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<TextArea
|
||||
placeholder={`此项可选,用于复写返回的状态码,比如将claude渠道的400错误复写为500(用于重试),请勿滥用该功能,例如:\n${JSON.stringify(STATUS_CODE_MAPPING_EXAMPLE, null, 2)}`}
|
||||
placeholder={t('此项可选,用于复写返回的状态码,比如将claude渠道的400错误复写为500(用于重试),请勿滥用该功能,例如:') +
|
||||
'\n' + JSON.stringify(STATUS_CODE_MAPPING_EXAMPLE, null, 2)}
|
||||
name="status_code_mapping"
|
||||
onChange={(value) => {
|
||||
handleInputChange('status_code_mapping', value);
|
||||
@@ -939,17 +908,17 @@ const EditChannel = (props) => {
|
||||
);
|
||||
}}
|
||||
>
|
||||
填入模板
|
||||
{t('填入模板')}
|
||||
</Typography.Text>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>
|
||||
渠道标签
|
||||
{t('渠道标签')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
label="渠道标签"
|
||||
label={t('渠道标签')}
|
||||
name="tag"
|
||||
placeholder={'渠道标签'}
|
||||
placeholder={t('渠道标签')}
|
||||
onChange={(value) => {
|
||||
handleInputChange('tag', value);
|
||||
}}
|
||||
@@ -958,13 +927,13 @@ const EditChannel = (props) => {
|
||||
/>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>
|
||||
渠道优先级
|
||||
{t('渠道优先级')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
label="渠道优先级"
|
||||
label={t('渠道优先级')}
|
||||
name="priority"
|
||||
placeholder={'渠道优先级'}
|
||||
placeholder={t('渠道优先级')}
|
||||
onChange={(value) => {
|
||||
const number = parseInt(value);
|
||||
if (isNaN(number)) {
|
||||
@@ -978,13 +947,13 @@ const EditChannel = (props) => {
|
||||
/>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>
|
||||
渠道权重
|
||||
{t('渠道权重')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
label="渠道权重"
|
||||
label={t('渠道权重')}
|
||||
name="weight"
|
||||
placeholder={'渠道权重'}
|
||||
placeholder={t('渠道权重')}
|
||||
onChange={(value) => {
|
||||
const number = parseInt(value);
|
||||
if (isNaN(number)) {
|
||||
@@ -996,6 +965,42 @@ const EditChannel = (props) => {
|
||||
value={inputs.weight}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
{inputs.type === 8 && (
|
||||
<>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>
|
||||
{t('渠道额外设置')}:
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<TextArea
|
||||
placeholder={t('此项可选,用于配置渠道特定设置,为一个 JSON 字符串,例如:') + '\n{\n "force_format": true\n}'}
|
||||
name="setting"
|
||||
onChange={(value) => {
|
||||
handleInputChange('setting', value);
|
||||
}}
|
||||
autosize
|
||||
value={inputs.setting}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
<Typography.Text
|
||||
style={{
|
||||
color: 'rgba(var(--semi-blue-5), 1)',
|
||||
userSelect: 'none',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
onClick={() => {
|
||||
handleInputChange(
|
||||
'setting',
|
||||
JSON.stringify({
|
||||
force_format: true
|
||||
}, null, 2)
|
||||
);
|
||||
}}
|
||||
>
|
||||
{t('填入模板')}
|
||||
</Typography.Text>
|
||||
</>
|
||||
)}
|
||||
</Spin>
|
||||
</SideSheet>
|
||||
</>
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
import React from 'react';
|
||||
import ChannelsTable from '../../components/ChannelsTable';
|
||||
import { Layout } from '@douyinfe/semi-ui';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const File = () => (
|
||||
<>
|
||||
<Layout>
|
||||
<Layout.Header>
|
||||
<h3>管理渠道</h3>
|
||||
const File = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
<Layout>
|
||||
<Layout.Header>
|
||||
<h3>{t('管理渠道')}</h3>
|
||||
</Layout.Header>
|
||||
<Layout.Content>
|
||||
<ChannelsTable />
|
||||
</Layout.Content>
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
</Layout.Content>
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default File;
|
||||
|
||||
@@ -21,8 +21,10 @@ import {
|
||||
} from '../../helpers/render';
|
||||
import { UserContext } from '../../context/User/index.js';
|
||||
import { StyleContext } from '../../context/Style/index.js';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const Detail = (props) => {
|
||||
const { t } = useTranslation();
|
||||
const formRef = useRef();
|
||||
let now = new Date();
|
||||
const [userState, userDispatch] = useContext(UserContext);
|
||||
@@ -85,8 +87,8 @@ const Detail = (props) => {
|
||||
},
|
||||
title: {
|
||||
visible: true,
|
||||
text: '模型调用次数占比',
|
||||
subtext: `总计:${renderNumber(times)}`,
|
||||
text: t('模型调用次数占比'),
|
||||
subtext: `${t('总计')}:${renderNumber(times)}`,
|
||||
},
|
||||
legends: {
|
||||
visible: true,
|
||||
@@ -125,11 +127,10 @@ const Detail = (props) => {
|
||||
},
|
||||
title: {
|
||||
visible: true,
|
||||
text: '模型消耗分布',
|
||||
subtext: `总计:${renderQuota(consumeQuota, 2)}`,
|
||||
text: t('模型消耗分布'),
|
||||
subtext: `${t('总计')}:${renderQuota(consumeQuota, 2)}`,
|
||||
},
|
||||
bar: {
|
||||
// The state style of bar
|
||||
state: {
|
||||
hover: {
|
||||
stroke: '#000',
|
||||
@@ -142,8 +143,7 @@ const Detail = (props) => {
|
||||
content: [
|
||||
{
|
||||
key: (datum) => datum['Model'],
|
||||
value: (datum) =>
|
||||
renderQuotaNumberWithDigit(parseFloat(datum['Usage']), 4),
|
||||
value: (datum) => renderQuota(datum['rawQuota'] || 0, 4),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -151,25 +151,28 @@ const Detail = (props) => {
|
||||
content: [
|
||||
{
|
||||
key: (datum) => datum['Model'],
|
||||
value: (datum) => datum['Usage'],
|
||||
value: (datum) => datum['rawQuota'] || 0,
|
||||
},
|
||||
],
|
||||
updateContent: (array) => {
|
||||
// sort by value
|
||||
array.sort((a, b) => b.value - a.value);
|
||||
// add $
|
||||
let sum = 0;
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
sum += parseFloat(array[i].value);
|
||||
array[i].value = renderQuotaNumberWithDigit(
|
||||
parseFloat(array[i].value),
|
||||
4,
|
||||
);
|
||||
if (array[i].key == "其他") {
|
||||
continue;
|
||||
}
|
||||
let value = parseFloat(array[i].value);
|
||||
if (isNaN(value)) {
|
||||
value = 0;
|
||||
}
|
||||
if (array[i].datum && array[i].datum.TimeSum) {
|
||||
sum = array[i].datum.TimeSum;
|
||||
}
|
||||
array[i].value = renderQuota(value, 4);
|
||||
}
|
||||
// add to first
|
||||
array.unshift({
|
||||
key: '总计',
|
||||
value: renderQuotaNumberWithDigit(sum, 4),
|
||||
key: t('总计'),
|
||||
value: renderQuota(sum, 4),
|
||||
});
|
||||
return array;
|
||||
},
|
||||
@@ -214,19 +217,8 @@ const Detail = (props) => {
|
||||
created_at: now.getTime() / 1000,
|
||||
});
|
||||
}
|
||||
// 根据dataExportDefaultTime重制时间粒度
|
||||
let timeGranularity = 3600;
|
||||
if (dataExportDefaultTime === 'day') {
|
||||
timeGranularity = 86400;
|
||||
} else if (dataExportDefaultTime === 'week') {
|
||||
timeGranularity = 604800;
|
||||
}
|
||||
// sort created_at
|
||||
data.sort((a, b) => a.created_at - b.created_at);
|
||||
data.forEach((item) => {
|
||||
item['created_at'] =
|
||||
Math.floor(item['created_at'] / timeGranularity) * timeGranularity;
|
||||
});
|
||||
updateChartData(data);
|
||||
} else {
|
||||
showError(message);
|
||||
@@ -252,14 +244,14 @@ const Detail = (props) => {
|
||||
let uniqueModels = new Set();
|
||||
let totalTokens = 0;
|
||||
|
||||
// 收集所有唯一的模型名称和时间点
|
||||
let uniqueTimes = new Set();
|
||||
// 收集所有唯一的模型名称
|
||||
data.forEach(item => {
|
||||
uniqueModels.add(item.model_name);
|
||||
uniqueTimes.add(timestamp2string1(item.created_at, dataExportDefaultTime));
|
||||
totalTokens += item.token_used;
|
||||
totalQuota += item.quota;
|
||||
totalTimes += item.count;
|
||||
});
|
||||
|
||||
|
||||
// 处理颜色映射
|
||||
const newModelColors = {};
|
||||
Array.from(uniqueModels).forEach((modelName) => {
|
||||
@@ -269,56 +261,82 @@ const Detail = (props) => {
|
||||
});
|
||||
setModelColors(newModelColors);
|
||||
|
||||
// 处理饼图数据
|
||||
for (let item of data) {
|
||||
totalQuota += item.quota;
|
||||
totalTimes += item.count;
|
||||
|
||||
let pieItem = newPieData.find((it) => it.type === item.model_name);
|
||||
if (pieItem) {
|
||||
pieItem.value += item.count;
|
||||
} else {
|
||||
newPieData.push({
|
||||
type: item.model_name,
|
||||
value: item.count,
|
||||
// 按时间和模型聚合数据
|
||||
let aggregatedData = new Map();
|
||||
data.forEach(item => {
|
||||
const timeKey = timestamp2string1(item.created_at, dataExportDefaultTime);
|
||||
const modelKey = item.model_name;
|
||||
const key = `${timeKey}-${modelKey}`;
|
||||
|
||||
if (!aggregatedData.has(key)) {
|
||||
aggregatedData.set(key, {
|
||||
time: timeKey,
|
||||
model: modelKey,
|
||||
quota: 0,
|
||||
count: 0
|
||||
});
|
||||
}
|
||||
|
||||
const existing = aggregatedData.get(key);
|
||||
existing.quota += item.quota;
|
||||
existing.count += item.count;
|
||||
});
|
||||
|
||||
// 处理饼图数据
|
||||
let modelTotals = new Map();
|
||||
for (let [_, value] of aggregatedData) {
|
||||
if (!modelTotals.has(value.model)) {
|
||||
modelTotals.set(value.model, 0);
|
||||
}
|
||||
modelTotals.set(value.model, modelTotals.get(value.model) + value.count);
|
||||
}
|
||||
|
||||
// 处理柱状图数据
|
||||
let timePoints = Array.from(uniqueTimes);
|
||||
newPieData = Array.from(modelTotals).map(([model, count]) => ({
|
||||
type: model,
|
||||
value: count
|
||||
}));
|
||||
|
||||
// 生成时间点序列
|
||||
let timePoints = Array.from(new Set([...aggregatedData.values()].map(d => d.time)));
|
||||
if (timePoints.length < 7) {
|
||||
// 根据时间粒度生成合适的时间点
|
||||
const generateTimePoints = () => {
|
||||
let lastTime = Math.max(...data.map(item => item.created_at));
|
||||
let points = [];
|
||||
let interval = dataExportDefaultTime === 'hour' ? 3600
|
||||
const lastTime = Math.max(...data.map(item => item.created_at));
|
||||
const interval = dataExportDefaultTime === 'hour' ? 3600
|
||||
: dataExportDefaultTime === 'day' ? 86400
|
||||
: 604800;
|
||||
|
||||
for (let i = 0; i < 7; i++) {
|
||||
points.push(timestamp2string1(lastTime - (i * interval), dataExportDefaultTime));
|
||||
}
|
||||
return points.reverse();
|
||||
};
|
||||
|
||||
timePoints = generateTimePoints();
|
||||
|
||||
timePoints = Array.from({length: 7}, (_, i) =>
|
||||
timestamp2string1(lastTime - (6-i) * interval, dataExportDefaultTime)
|
||||
);
|
||||
}
|
||||
|
||||
// 为每个时间点和模型生成数据
|
||||
// 生成柱状图数据
|
||||
timePoints.forEach(time => {
|
||||
Array.from(uniqueModels).forEach(model => {
|
||||
let existingData = data.find(item =>
|
||||
timestamp2string1(item.created_at, dataExportDefaultTime) === time &&
|
||||
item.model_name === model
|
||||
);
|
||||
|
||||
newLineData.push({
|
||||
// 为每个时间点收集所有模型的数据
|
||||
let timeData = Array.from(uniqueModels).map(model => {
|
||||
const key = `${time}-${model}`;
|
||||
const aggregated = aggregatedData.get(key);
|
||||
return {
|
||||
Time: time,
|
||||
Model: model,
|
||||
Usage: existingData ? parseFloat(getQuotaWithUnit(existingData.quota)) : 0
|
||||
});
|
||||
rawQuota: aggregated?.quota || 0,
|
||||
Usage: aggregated?.quota ? getQuotaWithUnit(aggregated.quota, 4) : 0
|
||||
};
|
||||
});
|
||||
|
||||
// 计算该时间点的总计
|
||||
const timeSum = timeData.reduce((sum, item) => sum + item.rawQuota, 0);
|
||||
|
||||
// 按照 rawQuota 从大到小排序
|
||||
timeData.sort((a, b) => b.rawQuota - a.rawQuota);
|
||||
|
||||
// 为每个数据点添加该时间的总计
|
||||
timeData = timeData.map(item => ({
|
||||
...item,
|
||||
TimeSum: timeSum
|
||||
}));
|
||||
|
||||
// 将排序后的数据添加到 newLineData
|
||||
newLineData.push(...timeData);
|
||||
});
|
||||
|
||||
// 排序
|
||||
@@ -331,7 +349,7 @@ const Detail = (props) => {
|
||||
data: [{ id: 'id0', values: newPieData }],
|
||||
title: {
|
||||
...prev.title,
|
||||
subtext: `总计:${renderNumber(totalTimes)}`
|
||||
subtext: `${t('总计')}:${renderNumber(totalTimes)}`
|
||||
},
|
||||
color: {
|
||||
specified: newModelColors
|
||||
@@ -343,7 +361,7 @@ const Detail = (props) => {
|
||||
data: [{ id: 'barData', values: newLineData }],
|
||||
title: {
|
||||
...prev.title,
|
||||
subtext: `总计:${renderQuota(totalQuota, 2)}`
|
||||
subtext: `${t('总计')}:${renderQuota(totalQuota, 2)}`
|
||||
},
|
||||
color: {
|
||||
specified: newModelColors
|
||||
@@ -382,14 +400,14 @@ const Detail = (props) => {
|
||||
<>
|
||||
<Layout>
|
||||
<Layout.Header>
|
||||
<h3>数据看板</h3>
|
||||
<h3>{t('数据看板')}</h3>
|
||||
</Layout.Header>
|
||||
<Layout.Content>
|
||||
<Form ref={formRef} layout='horizontal' style={{ marginTop: 10 }}>
|
||||
<>
|
||||
<Form.DatePicker
|
||||
field='start_timestamp'
|
||||
label='起始时间'
|
||||
label={t('起始时间')}
|
||||
style={{ width: 272 }}
|
||||
initValue={start_timestamp}
|
||||
value={start_timestamp}
|
||||
@@ -402,7 +420,7 @@ const Detail = (props) => {
|
||||
<Form.DatePicker
|
||||
field='end_timestamp'
|
||||
fluid
|
||||
label='结束时间'
|
||||
label={t('结束时间')}
|
||||
style={{ width: 272 }}
|
||||
initValue={end_timestamp}
|
||||
value={end_timestamp}
|
||||
@@ -412,15 +430,15 @@ const Detail = (props) => {
|
||||
/>
|
||||
<Form.Select
|
||||
field='data_export_default_time'
|
||||
label='时间粒度'
|
||||
label={t('时间粒度')}
|
||||
style={{ width: 176 }}
|
||||
initValue={dataExportDefaultTime}
|
||||
placeholder={'时间粒度'}
|
||||
placeholder={t('时间粒度')}
|
||||
name='data_export_default_time'
|
||||
optionList={[
|
||||
{ label: '小时', value: 'hour' },
|
||||
{ label: '天', value: 'day' },
|
||||
{ label: '周', value: 'week' },
|
||||
{ label: t('小时'), value: 'hour' },
|
||||
{ label: t('天'), value: 'day' },
|
||||
{ label: t('周'), value: 'week' },
|
||||
]}
|
||||
onChange={(value) =>
|
||||
handleInputChange(value, 'data_export_default_time')
|
||||
@@ -430,17 +448,17 @@ const Detail = (props) => {
|
||||
<>
|
||||
<Form.Input
|
||||
field='username'
|
||||
label='用户名称'
|
||||
label={t('用户名称')}
|
||||
style={{ width: 176 }}
|
||||
value={username}
|
||||
placeholder={'可选值'}
|
||||
placeholder={t('可选值')}
|
||||
name='username'
|
||||
onChange={(value) => handleInputChange(value, 'username')}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
label='查询'
|
||||
label={t('查询')}
|
||||
type='primary'
|
||||
htmlType='submit'
|
||||
className='btn-margin-right'
|
||||
@@ -448,7 +466,7 @@ const Detail = (props) => {
|
||||
loading={loading}
|
||||
style={{ marginTop: 24 }}
|
||||
>
|
||||
查询
|
||||
{t('查询')}
|
||||
</Button>
|
||||
<Form.Section>
|
||||
</Form.Section>
|
||||
@@ -459,13 +477,13 @@ const Detail = (props) => {
|
||||
<Col span={styleState.isMobile?24:8}>
|
||||
<Card className='panel-desc-card'>
|
||||
<Descriptions row size="small">
|
||||
<Descriptions.Item itemKey='当前余额'>
|
||||
<Descriptions.Item itemKey={t('当前余额')}>
|
||||
{renderQuota(userState?.user?.quota)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item itemKey='历史消耗'>
|
||||
<Descriptions.Item itemKey={t('历史消耗')}>
|
||||
{renderQuota(userState?.user?.used_quota)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item itemKey='请求次数'>
|
||||
<Descriptions.Item itemKey={t('请求次数')}>
|
||||
{userState.user?.request_count}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
@@ -474,13 +492,13 @@ const Detail = (props) => {
|
||||
<Col span={styleState.isMobile?24:8}>
|
||||
<Card>
|
||||
<Descriptions row size="small">
|
||||
<Descriptions.Item itemKey='统计额度'>
|
||||
<Descriptions.Item itemKey={t('统计额度')}>
|
||||
{renderQuota(consumeQuota)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item itemKey='统计Tokens'>
|
||||
<Descriptions.Item itemKey={t('统计Tokens')}>
|
||||
{consumeTokens}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item itemKey='统计次数'>
|
||||
<Descriptions.Item itemKey={t('统计次数')}>
|
||||
{times}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
@@ -489,13 +507,13 @@ const Detail = (props) => {
|
||||
<Col span={styleState.isMobile ? 24 : 8}>
|
||||
<Card>
|
||||
<Descriptions row size='small'>
|
||||
<Descriptions.Item itemKey='平均RPM'>
|
||||
<Descriptions.Item itemKey={t('平均RPM')}>
|
||||
{(times /
|
||||
((Date.parse(end_timestamp) -
|
||||
Date.parse(start_timestamp)) /
|
||||
60000)).toFixed(3)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item itemKey='平均TPM'>
|
||||
<Descriptions.Item itemKey={t('平均TPM')}>
|
||||
{(consumeTokens /
|
||||
((Date.parse(end_timestamp) -
|
||||
Date.parse(start_timestamp)) /
|
||||
@@ -507,7 +525,7 @@ const Detail = (props) => {
|
||||
</Row>
|
||||
<Card style={{marginTop: 20}}>
|
||||
<Tabs type="line" defaultActiveKey="1">
|
||||
<Tabs.TabPane tab="消耗分布" itemKey="1">
|
||||
<Tabs.TabPane tab={t('消耗分布')} itemKey="1">
|
||||
<div style={{ height: 500 }}>
|
||||
<VChart
|
||||
spec={spec_line}
|
||||
@@ -515,7 +533,7 @@ const Detail = (props) => {
|
||||
/>
|
||||
</div>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="调用次数分布" itemKey="2">
|
||||
<Tabs.TabPane tab={t('调用次数分布')} itemKey="2">
|
||||
<div style={{ height: 500 }}>
|
||||
<VChart
|
||||
spec={spec_pie}
|
||||
|
||||
@@ -4,8 +4,10 @@ import { API, showError, showNotice, timestamp2string } from '../../helpers';
|
||||
import { StatusContext } from '../../context/Status';
|
||||
import { marked } from 'marked';
|
||||
import { StyleContext } from '../../context/Style/index.js';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const Home = () => {
|
||||
const { t, i18n } = useTranslation();
|
||||
const [statusState] = useContext(StatusContext);
|
||||
const [homePageContentLoaded, setHomePageContentLoaded] = useState(false);
|
||||
const [homePageContent, setHomePageContent] = useState('');
|
||||
@@ -37,6 +39,20 @@ const Home = () => {
|
||||
}
|
||||
setHomePageContent(content);
|
||||
localStorage.setItem('home_page_content', content);
|
||||
|
||||
// 如果内容是 URL,则发送主题模式
|
||||
if (data.startsWith('https://')) {
|
||||
const iframe = document.querySelector('iframe');
|
||||
if (iframe) {
|
||||
const theme = localStorage.getItem('theme-mode') || 'light';
|
||||
// 测试是否正确传递theme-mode给iframe
|
||||
// console.log('Sending theme-mode to iframe:', theme);
|
||||
iframe.onload = () => {
|
||||
iframe.contentWindow.postMessage({ themeMode: theme }, '*');
|
||||
iframe.contentWindow.postMessage({ lang: i18n.language }, '*');
|
||||
};
|
||||
}
|
||||
}
|
||||
} else {
|
||||
showError(message);
|
||||
setHomePageContent('加载首页内容失败...');
|
||||
@@ -52,7 +68,8 @@ const Home = () => {
|
||||
useEffect(() => {
|
||||
displayNotice().then();
|
||||
displayHomePageContent().then();
|
||||
}, []);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{homePageContentLoaded && homePageContent === '' ? (
|
||||
@@ -60,13 +77,13 @@ const Home = () => {
|
||||
<Card
|
||||
bordered={false}
|
||||
headerLine={false}
|
||||
title='系统状况'
|
||||
title={t('系统状况')}
|
||||
bodyStyle={{ padding: '10px 20px' }}
|
||||
>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Card
|
||||
title='系统信息'
|
||||
title={t('系统信息')}
|
||||
headerExtraContent={
|
||||
<span
|
||||
style={{
|
||||
@@ -74,19 +91,19 @@ const Home = () => {
|
||||
color: 'var(--semi-color-text-1)',
|
||||
}}
|
||||
>
|
||||
系统信息总览
|
||||
{t('系统信息总览')}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<p>名称:{statusState?.status?.system_name}</p>
|
||||
<p>{t('名称')}:{statusState?.status?.system_name}</p>
|
||||
<p>
|
||||
版本:
|
||||
{t('版本')}:
|
||||
{statusState?.status?.version
|
||||
? statusState?.status?.version
|
||||
: 'unknown'}
|
||||
</p>
|
||||
<p>
|
||||
源码:
|
||||
{t('源码')}:
|
||||
<a
|
||||
href='https://github.com/Calcium-Ion/new-api'
|
||||
target='_blank'
|
||||
@@ -96,7 +113,7 @@ const Home = () => {
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
协议:
|
||||
{t('协议')}:
|
||||
<a
|
||||
href='https://www.apache.org/licenses/LICENSE-2.0'
|
||||
target='_blank'
|
||||
@@ -105,12 +122,12 @@ const Home = () => {
|
||||
Apache-2.0 License
|
||||
</a>
|
||||
</p>
|
||||
<p>启动时间:{getStartTimeString()}</p>
|
||||
<p>{t('启动时间')}:{getStartTimeString()}</p>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Card
|
||||
title='系统配置'
|
||||
title={t('系统配置')}
|
||||
headerExtraContent={
|
||||
<span
|
||||
style={{
|
||||
@@ -118,45 +135,45 @@ const Home = () => {
|
||||
color: 'var(--semi-color-text-1)',
|
||||
}}
|
||||
>
|
||||
系统配置总览
|
||||
{t('系统配置总览')}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<p>
|
||||
邮箱验证:
|
||||
{t('邮箱验证')}:
|
||||
{statusState?.status?.email_verification === true
|
||||
? '已启用'
|
||||
: '未启用'}
|
||||
? t('已启用')
|
||||
: t('未启用')}
|
||||
</p>
|
||||
<p>
|
||||
GitHub 身份验证:
|
||||
{t('GitHub 身份验证')}:
|
||||
{statusState?.status?.github_oauth === true
|
||||
? '已启用'
|
||||
: '未启用'}
|
||||
? t('已启用')
|
||||
: t('未启用')}
|
||||
</p>
|
||||
<p>
|
||||
微信身份验证:
|
||||
{t('微信身份验证')}:
|
||||
{statusState?.status?.wechat_login === true
|
||||
? '已启用'
|
||||
: '未启用'}
|
||||
? t('已启用')
|
||||
: t('未启用')}
|
||||
</p>
|
||||
<p>
|
||||
Turnstile 用户校验:
|
||||
{t('Turnstile 用户校验')}:
|
||||
{statusState?.status?.turnstile_check === true
|
||||
? '已启用'
|
||||
: '未启用'}
|
||||
? t('已启用')
|
||||
: t('未启用')}
|
||||
</p>
|
||||
<p>
|
||||
Telegram 身份验证:
|
||||
{t('Telegram 身份验证')}:
|
||||
{statusState?.status?.telegram_oauth === true
|
||||
? '已启用'
|
||||
: '未启用'}
|
||||
? t('已启用')
|
||||
: t('未启用')}
|
||||
</p>
|
||||
<p>
|
||||
Linux DO 身份验证:
|
||||
{t('Linux DO 身份验证')}:
|
||||
{statusState?.status?.linuxdo_oauth === true
|
||||
? '已启用'
|
||||
: '未启用'}
|
||||
? t('已启用')
|
||||
: t('未启用')}
|
||||
</p>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
@@ -6,24 +6,10 @@ import { Card, Chat, Input, Layout, Select, Slider, TextArea, Typography, Button
|
||||
import { SSE } from 'sse';
|
||||
import { IconSetting } from '@douyinfe/semi-icons';
|
||||
import { StyleContext } from '../../context/Style/index.js';
|
||||
|
||||
const defaultMessage = [
|
||||
{
|
||||
role: 'user',
|
||||
id: '2',
|
||||
createAt: 1715676751919,
|
||||
content: "你好",
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
id: '3',
|
||||
createAt: 1715676751919,
|
||||
content: "你好,请问有什么可以帮助您的吗?",
|
||||
}
|
||||
];
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const roleInfo = {
|
||||
user: {
|
||||
user: {
|
||||
name: 'User',
|
||||
avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png'
|
||||
},
|
||||
@@ -43,6 +29,23 @@ function getId() {
|
||||
}
|
||||
|
||||
const Playground = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const defaultMessage = [
|
||||
{
|
||||
role: 'user',
|
||||
id: '2',
|
||||
createAt: 1715676751919,
|
||||
content: t('你好'),
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
id: '3',
|
||||
createAt: 1715676751919,
|
||||
content: t('你好,请问有什么可以帮助您的吗?'),
|
||||
}
|
||||
];
|
||||
|
||||
const [inputs, setInputs] = useState({
|
||||
model: 'gpt-4o-mini',
|
||||
group: '',
|
||||
@@ -65,7 +68,7 @@ const Playground = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (searchParams.get('expired')) {
|
||||
showError('未登录或登录已过期,请重新登录!');
|
||||
showError(t('未登录或登录已过期,请重新登录!'));
|
||||
}
|
||||
let status = localStorage.getItem('status');
|
||||
if (status) {
|
||||
@@ -86,7 +89,7 @@ const Playground = () => {
|
||||
}));
|
||||
setModels(localModelOptions);
|
||||
} else {
|
||||
showError(message);
|
||||
showError(t(message));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -94,36 +97,33 @@ const Playground = () => {
|
||||
let res = await API.get(`/api/user/self/groups`);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
// return data is a map, key is group name, value is group description
|
||||
// label is group description, value is group name
|
||||
let localGroupOptions = Object.keys(data).map((group) => ({
|
||||
label: data[group],
|
||||
value: group,
|
||||
}));
|
||||
// handleInputChange('group', localGroupOptions[0].value);
|
||||
|
||||
if (localGroupOptions.length > 0) {
|
||||
// set user group at first
|
||||
if (userState.user && userState.user.group) {
|
||||
let userGroup = userState.user.group;
|
||||
// Find and move user's group to the front
|
||||
if (localGroupOptions.length === 0) {
|
||||
localGroupOptions = [{
|
||||
label: t('用户分组'),
|
||||
value: '',
|
||||
}];
|
||||
} else {
|
||||
const localUser = JSON.parse(localStorage.getItem('user'));
|
||||
const userGroup = (userState.user && userState.user.group) || (localUser && localUser.group);
|
||||
|
||||
if (userGroup) {
|
||||
const userGroupIndex = localGroupOptions.findIndex(g => g.value === userGroup);
|
||||
if (userGroupIndex > -1) {
|
||||
const userGroupOption = localGroupOptions.splice(userGroupIndex, 1)[0];
|
||||
localGroupOptions.unshift(userGroupOption);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
localGroupOptions = [{
|
||||
label: '用户分组',
|
||||
value: '',
|
||||
}];
|
||||
setGroups(localGroupOptions);
|
||||
}
|
||||
|
||||
setGroups(localGroupOptions);
|
||||
handleInputChange('group', localGroupOptions[0].value);
|
||||
} else {
|
||||
showError(message);
|
||||
showError(t(message));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -314,10 +314,10 @@ const Playground = () => {
|
||||
<Layout.Sider style={{ display: styleState.isMobile ? 'block' : 'initial' }}>
|
||||
<Card style={commonOuterStyle}>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>分组:</Typography.Text>
|
||||
<Typography.Text strong>{t('分组')}:</Typography.Text>
|
||||
</div>
|
||||
<Select
|
||||
placeholder={'请选择分组'}
|
||||
placeholder={t('请选择分组')}
|
||||
name='group'
|
||||
required
|
||||
selection
|
||||
@@ -334,10 +334,10 @@ const Playground = () => {
|
||||
}))}
|
||||
/>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>模型:</Typography.Text>
|
||||
<Typography.Text strong>{t('模型')}:</Typography.Text>
|
||||
</div>
|
||||
<Select
|
||||
placeholder={'请选择模型'}
|
||||
placeholder={t('请选择模型')}
|
||||
name='model'
|
||||
required
|
||||
selection
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
API,
|
||||
downloadTextAsFile,
|
||||
@@ -22,6 +23,7 @@ import Title from '@douyinfe/semi-ui/lib/es/typography/title';
|
||||
import { Divider } from 'semantic-ui-react';
|
||||
|
||||
const EditRedemption = (props) => {
|
||||
const { t } = useTranslation();
|
||||
const isEdit = props.editingRedemption.id !== undefined;
|
||||
const [loading, setLoading] = useState(isEdit);
|
||||
|
||||
@@ -69,7 +71,7 @@ const EditRedemption = (props) => {
|
||||
let name = inputs.name;
|
||||
if (!isEdit && inputs.name === '') {
|
||||
// set default name
|
||||
name = '兑换码-' + renderQuota(quota);
|
||||
name = renderQuota(quota);
|
||||
}
|
||||
setLoading(true);
|
||||
let localInputs = inputs;
|
||||
@@ -90,11 +92,11 @@ const EditRedemption = (props) => {
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
if (isEdit) {
|
||||
showSuccess('兑换码更新成功!');
|
||||
showSuccess(t('兑换码更新成功!'));
|
||||
props.refresh();
|
||||
props.handleClose();
|
||||
} else {
|
||||
showSuccess('兑换码创建成功!');
|
||||
showSuccess(t('兑换码创建成功!'));
|
||||
setInputs(originInputs);
|
||||
props.refresh();
|
||||
props.handleClose();
|
||||
@@ -107,13 +109,12 @@ const EditRedemption = (props) => {
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
text += data[i] + '\n';
|
||||
}
|
||||
// downloadTextAsFile(text, `${inputs.name}.txt`);
|
||||
Modal.confirm({
|
||||
title: '兑换码创建成功',
|
||||
title: t('兑换码创建成功'),
|
||||
content: (
|
||||
<div>
|
||||
<p>兑换码创建成功,是否下载兑换码?</p>
|
||||
<p>兑换码将以文本文件的形式下载,文件名为兑换码的名称。</p>
|
||||
<p>{t('兑换码创建成功,是否下载兑换码?')}</p>
|
||||
<p>{t('兑换码将以文本文件的形式下载,文件名为兑换码的名称。')}</p>
|
||||
</div>
|
||||
),
|
||||
onOk: () => {
|
||||
@@ -130,7 +131,7 @@ const EditRedemption = (props) => {
|
||||
placement={isEdit ? 'right' : 'left'}
|
||||
title={
|
||||
<Title level={3}>
|
||||
{isEdit ? '更新兑换码信息' : '创建新的兑换码'}
|
||||
{isEdit ? t('更新兑换码信息') : t('创建新的兑换码')}
|
||||
</Title>
|
||||
}
|
||||
headerStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
|
||||
@@ -140,7 +141,7 @@ const EditRedemption = (props) => {
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Space>
|
||||
<Button theme='solid' size={'large'} onClick={submit}>
|
||||
提交
|
||||
{t('提交')}
|
||||
</Button>
|
||||
<Button
|
||||
theme='solid'
|
||||
@@ -148,7 +149,7 @@ const EditRedemption = (props) => {
|
||||
type={'tertiary'}
|
||||
onClick={handleCancel}
|
||||
>
|
||||
取消
|
||||
{t('取消')}
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
@@ -160,9 +161,9 @@ const EditRedemption = (props) => {
|
||||
<Spin spinning={loading}>
|
||||
<Input
|
||||
style={{ marginTop: 20 }}
|
||||
label='名称'
|
||||
label={t('名称')}
|
||||
name='name'
|
||||
placeholder={'请输入名称'}
|
||||
placeholder={t('请输入名称')}
|
||||
onChange={(value) => handleInputChange('name', value)}
|
||||
value={name}
|
||||
autoComplete='new-password'
|
||||
@@ -170,12 +171,12 @@ const EditRedemption = (props) => {
|
||||
/>
|
||||
<Divider />
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<Typography.Text>{`额度${renderQuotaWithPrompt(quota)}`}</Typography.Text>
|
||||
<Typography.Text>{t('额度') + renderQuotaWithPrompt(quota)}</Typography.Text>
|
||||
</div>
|
||||
<AutoComplete
|
||||
style={{ marginTop: 8 }}
|
||||
name='quota'
|
||||
placeholder={'请输入额度'}
|
||||
placeholder={t('请输入额度')}
|
||||
onChange={(value) => handleInputChange('quota', value)}
|
||||
value={quota}
|
||||
autoComplete='new-password'
|
||||
@@ -193,12 +194,12 @@ const EditRedemption = (props) => {
|
||||
{!isEdit && (
|
||||
<>
|
||||
<Divider />
|
||||
<Typography.Text>生成数量</Typography.Text>
|
||||
<Typography.Text>{t('生成数量')}</Typography.Text>
|
||||
<Input
|
||||
style={{ marginTop: 8 }}
|
||||
label='生成数量'
|
||||
label={t('生成数量')}
|
||||
name='count'
|
||||
placeholder={'请输入生成数量'}
|
||||
placeholder={t('请输入生成数量')}
|
||||
onChange={(value) => handleInputChange('count', value)}
|
||||
value={count}
|
||||
autoComplete='new-password'
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import React from 'react';
|
||||
import RedemptionsTable from '../../components/RedemptionsTable';
|
||||
import { Layout } from '@douyinfe/semi-ui';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const Redemption = () => (
|
||||
<>
|
||||
<Layout>
|
||||
<Layout.Header>
|
||||
<h3>管理兑换码</h3>
|
||||
const Redemption = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
<Layout>
|
||||
<Layout.Header>
|
||||
<h3>{t('管理兑换码')}</h3>
|
||||
</Layout.Header>
|
||||
<Layout.Content>
|
||||
<RedemptionsTable />
|
||||
@@ -14,5 +17,6 @@ const Redemption = () => (
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Redemption;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user