From d8029550f758fed626caa1615a8f6d66efa02db4 Mon Sep 17 00:00:00 2001
From: JustSong
Date: Mon, 1 Jan 2024 16:18:50 +0800
Subject: [PATCH 01/35] fix: do not consume user quota if failed (close #881)
---
controller/relay-image.go | 3 +++
1 file changed, 3 insertions(+)
diff --git a/controller/relay-image.go b/controller/relay-image.go
index 7e1fed39..14a2983b 100644
--- a/controller/relay-image.go
+++ b/controller/relay-image.go
@@ -168,6 +168,9 @@ func relayImageHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode
var textResponse ImageResponse
defer func(ctx context.Context) {
+ if resp.StatusCode != http.StatusOK {
+ return
+ }
err := model.PostConsumeTokenQuota(tokenId, quota)
if err != nil {
common.SysError("error consuming token remain quota: " + err.Error())
From af8908db540d0ad4650e7595b79b0ccb066a9a38 Mon Sep 17 00:00:00 2001
From: Tisfeng
Date: Mon, 1 Jan 2024 16:42:19 +0800
Subject: [PATCH 02/35] feat: able to change gemini safety setting (#867)
* perf: adjust gemini safety settings, set BLOCK_NONE by default
* feat: able to adjust by env variable
---------
Co-authored-by: JustSong
---
README.md | 1 +
common/constants.go | 2 ++
common/utils.go | 7 +++++++
controller/relay-gemini.go | 36 ++++++++++++++++++------------------
4 files changed, 28 insertions(+), 18 deletions(-)
diff --git a/README.md b/README.md
index ff9e0bc0..b53936c4 100644
--- a/README.md
+++ b/README.md
@@ -366,6 +366,7 @@ graph LR
+ `DATA_GYM_CACHE_DIR`:目前该配置作用与 `TIKTOKEN_CACHE_DIR` 一致,但是优先级没有它高。
15. `RELAY_TIMEOUT`:中继超时设置,单位为秒,默认不设置超时时间。
16. `SQLITE_BUSY_TIMEOUT`:SQLite 锁等待超时设置,单位为毫秒,默认 `3000`。
+17. `GEMINI_SAFETY_SETTING`:Gemini 的安全设置,默认 `BLOCK_NONE`。
### 命令行参数
1. `--port `: 指定服务器监听的端口号,默认为 `3000`。
diff --git a/common/constants.go b/common/constants.go
index 60700ec8..e4cbf8bf 100644
--- a/common/constants.go
+++ b/common/constants.go
@@ -98,6 +98,8 @@ var BatchUpdateInterval = GetOrDefault("BATCH_UPDATE_INTERVAL", 5)
var RelayTimeout = GetOrDefault("RELAY_TIMEOUT", 0) // unit is second
+var GeminiSafetySetting = GetOrDefaultString("GEMINI_SAFETY_SETTING", "BLOCK_NONE")
+
const (
RequestIdKey = "X-Oneapi-Request-Id"
)
diff --git a/common/utils.go b/common/utils.go
index 21bec8f5..9a7038e2 100644
--- a/common/utils.go
+++ b/common/utils.go
@@ -196,6 +196,13 @@ func GetOrDefault(env string, defaultValue int) int {
return num
}
+func GetOrDefaultString(env string, defaultValue string) string {
+ if env == "" || os.Getenv(env) == "" {
+ return defaultValue
+ }
+ return os.Getenv(env)
+}
+
func MessageWithRequestId(message string, id string) string {
return fmt.Sprintf("%s (request id: %s)", message, id)
}
diff --git a/controller/relay-gemini.go b/controller/relay-gemini.go
index ec55d4b6..d8ab58d6 100644
--- a/controller/relay-gemini.go
+++ b/controller/relay-gemini.go
@@ -63,24 +63,24 @@ type GeminiChatGenerationConfig struct {
func requestOpenAI2Gemini(textRequest GeneralOpenAIRequest) *GeminiChatRequest {
geminiRequest := GeminiChatRequest{
Contents: make([]GeminiChatContent, 0, len(textRequest.Messages)),
- //SafetySettings: []GeminiChatSafetySettings{
- // {
- // Category: "HARM_CATEGORY_HARASSMENT",
- // Threshold: "BLOCK_ONLY_HIGH",
- // },
- // {
- // Category: "HARM_CATEGORY_HATE_SPEECH",
- // Threshold: "BLOCK_ONLY_HIGH",
- // },
- // {
- // Category: "HARM_CATEGORY_SEXUALLY_EXPLICIT",
- // Threshold: "BLOCK_ONLY_HIGH",
- // },
- // {
- // Category: "HARM_CATEGORY_DANGEROUS_CONTENT",
- // Threshold: "BLOCK_ONLY_HIGH",
- // },
- //},
+ SafetySettings: []GeminiChatSafetySettings{
+ {
+ Category: "HARM_CATEGORY_HARASSMENT",
+ Threshold: common.GeminiSafetySetting,
+ },
+ {
+ Category: "HARM_CATEGORY_HATE_SPEECH",
+ Threshold: common.GeminiSafetySetting,
+ },
+ {
+ Category: "HARM_CATEGORY_SEXUALLY_EXPLICIT",
+ Threshold: common.GeminiSafetySetting,
+ },
+ {
+ Category: "HARM_CATEGORY_DANGEROUS_CONTENT",
+ Threshold: common.GeminiSafetySetting,
+ },
+ },
GenerationConfig: GeminiChatGenerationConfig{
Temperature: textRequest.Temperature,
TopP: textRequest.TopP,
From c725cc88429aed1e013729b6c87ced20a040544e Mon Sep 17 00:00:00 2001
From: Zhanliang Liu
Date: Mon, 1 Jan 2024 17:00:23 +0800
Subject: [PATCH 03/35] fix: base 64 encoded format support of
gemini-pro-vision for field image_url/url (#878)
---
common/image/image.go | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/common/image/image.go b/common/image/image.go
index a602936a..27045d28 100644
--- a/common/image/image.go
+++ b/common/image/image.go
@@ -44,6 +44,18 @@ func GetImageSizeFromUrl(url string) (width int, height int, err error) {
}
func GetImageFromUrl(url string) (mimeType string, data string, err error) {
+ // Regex to match data URL pattern
+ dataURLPattern := regexp.MustCompile(`data:image/([^;]+);base64,(.*)`)
+
+ // Check if the URL is a data URL
+ matches := dataURLPattern.FindStringSubmatch(url)
+ if len(matches) == 3 {
+ // URL is a data URL
+ mimeType = "image/" + matches[1]
+ data = matches[2]
+ return
+ }
+
isImage, err := IsImageUrl(url)
if !isImage {
return
From 498dea2dbbd7d68261ef32a900711d555181bb68 Mon Sep 17 00:00:00 2001
From: Tailen <15708073+Tailen@users.noreply.github.com>
Date: Mon, 1 Jan 2024 17:06:17 +0800
Subject: [PATCH 04/35] feat: add support for davinci-002 and babbage-002
(#888)
---
common/model-ratio.go | 2 ++
controller/model.go | 18 ++++++++++++++++++
2 files changed, 20 insertions(+)
diff --git a/common/model-ratio.go b/common/model-ratio.go
index fa2adaa1..2908be17 100644
--- a/common/model-ratio.go
+++ b/common/model-ratio.go
@@ -52,6 +52,8 @@ var ModelRatio = map[string]float64{
"gpt-3.5-turbo-16k-0613": 1.5,
"gpt-3.5-turbo-instruct": 0.75, // $0.0015 / 1K tokens
"gpt-3.5-turbo-1106": 0.5, // $0.001 / 1K tokens
+ "davinci-002" 1, // $0.002 / 1K tokens
+ "babbage-002" 0.2, // $0.0004 / 1K tokens
"text-ada-001": 0.2,
"text-babbage-001": 0.25,
"text-curie-001": 1,
diff --git a/controller/model.go b/controller/model.go
index 6a759b63..6cb530db 100644
--- a/controller/model.go
+++ b/controller/model.go
@@ -342,6 +342,24 @@ func init() {
Root: "code-davinci-edit-001",
Parent: nil,
},
+ {
+ Id: "davinci-002",
+ Object: "model",
+ Created: 1677649963,
+ OwnedBy: "openai",
+ Permission: permission,
+ Root: "davinci-002",
+ Parent: nil,
+ },
+ {
+ Id: "babbage-002",
+ Object: "model",
+ Created: 1677649963,
+ OwnedBy: "openai",
+ Permission: permission,
+ Root: "babbage-002",
+ Parent: nil,
+ },
{
Id: "claude-instant-1",
Object: "model",
From c50c6095651570d01a172c0eb2f166535ca24366 Mon Sep 17 00:00:00 2001
From: Seven Yu <422347121@qq.com>
Date: Mon, 1 Jan 2024 17:09:12 +0800
Subject: [PATCH 05/35] fix: fix button copywriting (#880)
* feat: rename Channel button
* fix: update en.json
---------
Co-authored-by: seven.yu
Co-authored-by: JustSong
---
i18n/en.json | 4 +++-
web/src/components/ChannelsTable.js | 4 ++--
2 files changed, 5 insertions(+), 3 deletions(-)
diff --git a/i18n/en.json b/i18n/en.json
index b0deb83a..7b51909b 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -526,5 +526,7 @@
"模型版本": "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!"
+ "请确保已在 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!",
+ "测试所有渠道": "Test all channels",
+ "更新已启用渠道余额": "Update the balance of enabled channels"
}
diff --git a/web/src/components/ChannelsTable.js b/web/src/components/ChannelsTable.js
index 5d68e2da..a2adfd32 100644
--- a/web/src/components/ChannelsTable.js
+++ b/web/src/components/ChannelsTable.js
@@ -523,10 +523,10 @@ const ChannelsTable = () => {
添加新的渠道
+ loading={loading || updatingBalance}>更新已启用渠道余额
From 7772064d87468d62c7f7afee20a033328c07080d Mon Sep 17 00:00:00 2001
From: "Laisky.Cai"
Date: Mon, 1 Jan 2024 17:38:35 +0800
Subject: [PATCH 06/35] fix: support base64 encoded image_url (#872)
- Add support for base64 encoded image in OpenAI's image_url
Co-authored-by: JustSong <39998050+songquanpeng@users.noreply.github.com>
---
common/image/image.go | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/common/image/image.go b/common/image/image.go
index 27045d28..de8fefd3 100644
--- a/common/image/image.go
+++ b/common/image/image.go
@@ -15,6 +15,9 @@ import (
_ "golang.org/x/image/webp"
)
+// Regex to match data URL pattern
+var dataURLPattern = regexp.MustCompile(`data:image/([^;]+);base64,(.*)`)
+
func IsImageUrl(url string) (bool, error) {
resp, err := http.Head(url)
if err != nil {
@@ -44,9 +47,6 @@ func GetImageSizeFromUrl(url string) (width int, height int, err error) {
}
func GetImageFromUrl(url string) (mimeType string, data string, err error) {
- // Regex to match data URL pattern
- dataURLPattern := regexp.MustCompile(`data:image/([^;]+);base64,(.*)`)
-
// Check if the URL is a data URL
matches := dataURLPattern.FindStringSubmatch(url)
if len(matches) == 3 {
From cb5a3df61687bee00eb4a93bd94e8f33786fb4db Mon Sep 17 00:00:00 2001
From: JustSong
Date: Mon, 1 Jan 2024 17:40:10 +0800
Subject: [PATCH 07/35] fix: fix pr error (#888)
---
common/model-ratio.go | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/common/model-ratio.go b/common/model-ratio.go
index 2908be17..97cb060d 100644
--- a/common/model-ratio.go
+++ b/common/model-ratio.go
@@ -52,8 +52,8 @@ var ModelRatio = map[string]float64{
"gpt-3.5-turbo-16k-0613": 1.5,
"gpt-3.5-turbo-instruct": 0.75, // $0.0015 / 1K tokens
"gpt-3.5-turbo-1106": 0.5, // $0.001 / 1K tokens
- "davinci-002" 1, // $0.002 / 1K tokens
- "babbage-002" 0.2, // $0.0004 / 1K tokens
+ "davinci-002": 1, // $0.002 / 1K tokens
+ "babbage-002": 0.2, // $0.0004 / 1K tokens
"text-ada-001": 0.2,
"text-babbage-001": 0.25,
"text-curie-001": 1,
From 505817ca171121cf6f9d9cf579d6846513a87bda Mon Sep 17 00:00:00 2001
From: JustSong
Date: Mon, 1 Jan 2024 17:46:45 +0800
Subject: [PATCH 08/35] chore: update en.json
---
i18n/en.json | 244 +++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 244 insertions(+)
diff --git a/i18n/en.json b/i18n/en.json
index 7b51909b..f67d8665 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -458,6 +458,7 @@
"使用明细(总消耗额度:{renderQuota(stat.quota)})": "Usage Details (Total Consumption Quota: {renderQuota(stat.quota)})",
"用户名称": "User Name",
"令牌名称": "Token Name",
+ "默认令牌": "Default Token",
"留空则查询全部用户": "Leave blank to query all users",
"留空则查询全部令牌": "Leave blank to query all tokens",
"模型名称": "Model Name",
@@ -527,6 +528,249 @@
"请输入星火大模型版本,注意是接口地址中的版本号,例如: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!",
+ "处理中...": "Processing...",
+ "绑定成功!": "Binding successful!",
+ "登录成功!": "Login successful!",
+ "操作失败,重定向至登录界面中...": "Operation failed, redirecting to login screen...",
+ "出现错误,第 ${count} 次重试中...": "An error occurred, retrying ${count}...",
+ "首页": "Home",
+ "渠道": "Channel",
+ "令牌": "API Keys",
+ "兑换": "Redeem",
+ "充值": "Recharge",
+ "用户": "Users",
+ "日志": "Logs",
+ "设置": "Settings",
+ "关于": "About",
+ "聊天": "Chat",
+ "注销成功!": "Logout successful!",
+ "注销": "Log out",
+ "登录": "Log in",
+ "注册": "Sign up",
+ "加载{name}中...": "Loading {name}...",
+ "未登录或登录已过期,请重新登录!": "Not logged in or login has expired, please log in again!",
+ "请立刻修改默认密码!": "Please change the default password immediately!",
+ "欢迎回来": "Welcome back",
+ "没有账户?": "No account?",
+ "立刻注册": "Sign up now",
+ "用户名": "Username",
+ "密码": "Password",
+ "正在登录……": "Logging in...",
+ "忘记密码": "Forgot password",
+ "其他方式": "Other methods",
+ "微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)": "Scan the QR code with WeChat, follow the official account and enter 'verification code' to get the verification code (valid within three minutes)",
+ "验证码": "Verification code",
+ "全部用户": "All users",
+ "当前用户": "Current user",
+ "全部": "All",
+ "消费": "Consumption",
+ "管理": "Management",
+ "系统": "System",
+ "未知": "Unknown",
+ "其他模型": "Other models",
+ "复制成功": "Copy successful",
+ "使用明细": "Usages",
+ "刷新": "Refresh",
+ "收起面板": "Collapse panel",
+ "展开面板": "Expand panel",
+ "显示查询选项": "Show search options",
+ "隐藏查询选项": "Hide search options",
+ "用户名称": "User name",
+ "可选值": "Optional values",
+ "渠道 ID": "Channel ID",
+ "令牌名称": "Key name",
+ "模型名称": "Model name",
+ "起始时间": "Start time",
+ "结束时间": "End time",
+ "查询": "Query",
+ "隐藏条形图": "Hide bar chart",
+ "显示条形图": "Show bar chart",
+ "折线条形图只展示最新50条数据": "Line and bar charts only show the latest 50 pieces of data",
+ "总消耗": "Total consumption",
+ "总共调用了 {payload[0].value} 次": "A total of {payload[0].value} calls were made",
+ "{model.name}: {model.value} 次": "{model.name}: {model.value} times",
+ "总共调用了 {payload[0].value} 次 {payload[0].name}": "A total of {payload[0].value} {payload[0].name} calls were made",
+ "总消耗额度": "Total consumption limit",
+ "暂无数据": "No data available",
+ "更多数据统计图形即将到来,敬请期待!": "More data statistics graphics are coming soon, stay tuned!",
+ "复制用户名": "Copy username",
+ "{`共 ${counts} 条数据`}": "{`A total of ${counts} pieces of data`}",
+ "共 0 条数据": "A total of 0 pieces of data",
+ "选择明细分类": "Select detail category",
+ "模型倍率": "model rate",
+ "分组倍率": "group rate",
+ "新密码已复制到剪贴板:": "New password has been copied to the clipboard:",
+ "密码重置确认": "Password reset confirmation",
+ "邮箱地址": "Email address",
+ "新密码": "New password",
+ "密码已复制到剪贴板:": "Password has been copied to the clipboard:",
+ "密码重置完成": "Password reset complete",
+ "提交": "Submit",
+ "返回登录": "Return to login",
+ "请稍后重试,浏览器环境检查未通过": "Please try again later, browser environment check failed",
+ "重置邮件发送成功,请检查邮箱!": "Reset email sent successfully, please check your email!",
+ "密码重置": "Password reset",
+ "重试": "Retry",
+ "组": "Group",
+ "令牌已重置并已复制到剪贴板": "Token has been reset and copied to the clipboard",
+ "邀请链接已复制到剪切板": "Invitation link has been copied to the clipboard",
+ "系统令牌已复制到剪切板": "System token has been copied to the clipboard",
+ "请输入你的账户名以确认删除!": "Please enter your account name to confirm deletion!",
+ "账户已删除!": "Account has been deleted!",
+ "微信账户绑定成功!": "WeChat account binding successful!",
+ "请稍后几秒重试,Turnstile 正在检查用户环境!": "Please try again in a few seconds, Turnstile is checking the user environment!",
+ "验证码发送成功,请检查邮箱!": "Verification code sent successfully, please check your email!",
+ "邮箱账户绑定成功!": "Email account binding successful!",
+ "个人信息": "Personal information",
+ "编辑个人信息": "Edit personal information",
+ "生成系统访问令牌": "Generate system access token",
+ "复制邀请链接": "Copy invitation link",
+ "删除个人帐户": "Delete personal account",
+ "普通用户": "Regular user",
+ "管理员": "Administrator",
+ "超级管理员": "Super administrator",
+ "显示名称": "Display name",
+ "GitHub 账号": "GitHub account",
+ "微信账号": "WeChat account",
+ "修改个人信息只允许在电脑端进行。生成的令牌用于系统管理,而非用于请求 OpenAI 相关的服务,请知悉。": "Modifying personal information is only allowed on a computer. The generated token is for system management, not for requesting OpenAI related services. Please be aware.",
+ "可用模型": "Available models",
+ "账号绑定": "Account binding",
+ "绑定微信": "Bind WeChat",
+ "绑定 GitHub": "Bind GitHub",
+ "绑定邮箱": "Bind Email",
+ "绑定": "Bind",
+ "绑定邮箱地址": "Bind email address",
+ "输入邮箱地址": "Enter email address",
+ "重新发送": "Resend",
+ "获取验证码": "Get verification code",
+ "确认绑定": "Confirm binding",
+ "取消": "Cancel",
+ "危险操作": "Dangerous operation",
+ "您正在删除自己的帐户,将清空所有数据且不可恢复": "You are deleting your own account, all data will be cleared and cannot be recovered",
+ "输入你的账户名": "Enter your account name",
+ "以确认删除": "To confirm deletion",
+ "确认删除": "Confirm deletion",
+ "未使用": "Not used",
+ "已禁用": "Disabled",
+ "已使用": "Used",
+ "未知状态": "Unknown status",
+ "操作成功完成!": "Operation successfully completed!",
+ "搜索兑换码的 ID 和名称 ...": "Search for the ID and name of the redemption code ...",
+ "名称": "Name",
+ "状态": "Status",
+ "额度": "Quota",
+ "创建时间": "Creation time",
+ "兑换时间": "Redemption time",
+ "操作": "Operation",
+ "尚未兑换": "Not yet redeemed",
+ "已复制到剪贴板!": "Copied to clipboard!",
+ "无法复制到剪贴板,请手动复制,已将兑换码填入搜索框。": "Unable to copy to clipboard, please copy manually. The redemption code has been filled in the search box.",
+ "复制": "Copy",
+ "删除": "Delete",
+ "禁用": "Disable",
+ "启用": "Enable",
+ "编辑": "Edit",
+ "添加新的兑换码": "Add new redemption code",
+ "密码长度不得小于 8 位!": "Password length must not be less than 8 characters!",
+ "两次输入的密码不一致": "The two passwords entered do not match",
+ "注册成功!": "Registration successful!",
+ "请填写注册邮箱!": "Please fill in the registration email!",
+ "请在${verificationTimeout}秒后再试": "Please try again after ${verificationTimeout} seconds",
+ "验证码发送成功,请检查你的邮箱!": "Verification code sent successfully, please check your email!",
+ "已有账户?": "Already have an account?",
+ "请输入用户名(最长 12 位)": "Please enter a username (up to 12 characters)",
+ "请输入密码(最短 8 位,最长 20 位)": "Please enter a password (minimum 8 characters, maximum 20 characters)",
+ "请再次输入密码": "Please enter the password again",
+ "请输入邮箱地址": "Please enter an email address",
+ "秒后可重发": "Can be resent after seconds",
+ "请输入邮箱验证码": "Please enter the email verification code",
+ "已过期": "Expired",
+ "已启用": "Enabled",
+ "已耗尽": "Exhausted",
+ "无": "None",
+ "令牌密钥": "API Key",
+ "令牌状态": "Key status",
+ "已用额度": "Used quota",
+ "剩余额度": "Remaining quota",
+ "过期时间": "Expiration time",
+ "你确定要删除这个令牌吗?": "Are you sure you want to delete this key?",
+ "无法复制到剪贴板,请手动复制,已将令牌密钥填入搜索框": "Unable to copy to clipboard, please copy manually. The key key has been filled in the search box.",
+ "无限制": "Unlimited",
+ "永不过期": "Never expires",
+ "使用 API 访问令牌进行服务鉴权和计费。": "Use API Key for service authentication and billing.",
+ "API 访问令牌关系到您的个人利益,请妥善留存,不要与其他人共享,也不要保存在客户端代码中。": "API Key is related to your personal interests. Please keep it properly. Do not share it with others or save it in client code.",
+ "创建令牌": "Create Key",
+ "什么都还没有,快去创建一个令牌开始使用吧!": "Nothing yet, go create a key to start using!",
+ "你确定要删除该令牌吗": "Are you sure you want to delete this key",
+ "导出令牌信息": "Export key information",
+ "错误:未登录或登录已过期,请重新登录!": "Error: Not logged in or login has expired, please log in again!",
+ "错误:请求次数过多,请稍后再试!": "Error: Too many requests, please try again later!",
+ "错误:服务器内部错误,请联系管理员!": "Error: Server internal error, please contact the online customer service!",
+ "本站仅作演示之用,无服务端!": "This site is for demonstration purposes only, no server!",
+ "错误:": "Error:",
+ "加载首页内容失败...": "Failed to load homepage content...",
+ "系统状况": "System status",
+ "系统信息": "System information",
+ "系统信息总览": "System information overview",
+ "名称:": "Name:",
+ "版本:": "Version:",
+ "源码:": "Source code:",
+ "启动时间:": "Startup time:",
+ "系统配置": "System configuration",
+ "系统配置总览": "System configuration overview",
+ "邮箱验证:": "Email verification:",
+ "未启用": "Not enabled",
+ "Turnstile 用户校验:": "Turnstile user verification:",
+ "页面不存在": "Page does not exist",
+ "请检查你的浏览器地址是否正确": "Please check if your browser address is correct",
+ "个人设置": "Personal settings",
+ "运营设置": "Operations settings",
+ "系统设置": "System settings",
+ "其他设置": "Other settings",
+ "默认令牌": "Default key",
+ "过期时间必须在当前时间之后!": "Expiration time must be after the current time!",
+ "额度必须大于等于 0!": "Quota must be greater than or equal to 0!",
+ "过期时间格式错误!": "Expiration time format error!",
+ "创建令牌数量必须大于等于 1!": "The number of keys to create must be greater than or equal to 1!",
+ "令牌修改成功": "API Key modification successful",
+ "令牌创建成功": "API Key creation successful",
+ "更新令牌信息": "Update key information",
+ "创建新的令牌": "Create a new key",
+ "请输入名称": "Please enter a name",
+ "请输入过期时间,格式为 yyyy-MM-dd HH:mm:ss,-1 表示无限制": "Please enter the expiration time, the format is yyyy-MM-dd HH:mm:ss, -1 means unlimited",
+ "无限额度": "Unlimited quota",
+ "注意:启用无限额度后,已用额度将不再进行计算。": "Note: After enabling unlimited quota, the used quota will no longer be calculated.",
+ "等于": "Equals",
+ "请输入额度(单位:token)": "Please enter the quota (unit: token)",
+ "创建令牌数量": "Create key quantity",
+ "请输入令牌数量": "Please enter the number of keys",
+ "注意:令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。": "Note: The quota of the key is only used to limit the maximum quota usage of the key itself, and the actual usage is subject to the remaining quota of the account.",
+ "我的令牌": "My keys",
+ "请输入额度兑换码!": "Please enter the redeem code!",
+ "充值成功!": "Recharge successful!",
+ "请求失败": "Request failed",
+ "超级管理员未设置充值链接!": "The super administrator did not set a recharge link!",
+ "充值额度": "Recharge quota",
+ "兑换中...": "Redeeming...",
+ "请点击充值以获取额度兑换码。": "Please click recharge to get the quota redemption code.",
+ "用户信息更新成功!": "User information updated successfully!",
+ "更新用户信息": "Update user information",
+ "请输入新的用户名": "Please enter a new username",
+ "请输入新的密码,最短 8 位": "Please enter a new password, at least 8 characters",
+ "请输入新的显示名称": "Please enter a new display name",
+ "分组": "Group",
+ "请选择分组": "Please select a group",
+ "请在系统设置页面编辑分组倍率以添加新的分组:": "Please edit the group rate on the system settings page to add a new group:",
+ "请输入新的剩余额度": "Please enter a new remaining quota",
+ "已绑定的 GitHub 账户": "Bound GitHub account",
+ "此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改": "This item is read-only, users need to bind through the relevant binding button on the personal settings page, cannot be directly modified",
+ "已绑定的微信账户": "Bound WeChat account",
+ "已绑定的邮箱账户": "Bound email account",
+ "新版本可用:${data.version},请使用快捷键 Shift + F5 刷新页面": "New version available: ${data.version}, please refresh the page using the shortcut key Shift + F5",
+ "无法正常连接至服务器!": "Unable to connect to the server normally!",
+ "提示:": "Input:",
+ "补全:": "Output:",
+ "搜索令牌名称": "Search key name",
"测试所有渠道": "Test all channels",
"更新已启用渠道余额": "Update the balance of enabled channels"
}
From aa03c89133c5d0f9a35ceedcecb08c6b57206174 Mon Sep 17 00:00:00 2001
From: JustSong
Date: Mon, 1 Jan 2024 18:55:03 +0800
Subject: [PATCH 09/35] feat: able to add more UI theme (#860)
---
.github/workflows/linux-release.yml | 6 ++--
.github/workflows/macos-release.yml | 6 ++--
.github/workflows/windows-release.yml | 6 ++--
Dockerfile | 13 ++++++--
common/constants.go | 2 ++
main.go | 9 ++----
router/main.go | 4 +--
router/web-router.go | 10 +++++--
web/README.md | 28 ++++++------------
web/THEMES | 1 +
web/build/.gitkeep | 0
web/{ => default}/.gitignore | 0
web/default/README.md | 21 +++++++++++++
web/{ => default}/package.json | 2 +-
web/{ => default}/public/favicon.ico | Bin
web/{ => default}/public/index.html | 0
web/{ => default}/public/logo.png | Bin
web/{ => default}/public/robots.txt | 0
web/{ => default}/src/App.js | 0
.../src/components/ChannelsTable.js | 0
web/{ => default}/src/components/Footer.js | 0
.../src/components/GitHubOAuth.js | 0
web/{ => default}/src/components/Header.js | 0
web/{ => default}/src/components/Loading.js | 0
web/{ => default}/src/components/LoginForm.js | 0
web/{ => default}/src/components/LogsTable.js | 0
.../src/components/OperationSetting.js | 0
.../src/components/OtherSetting.js | 0
.../src/components/PasswordResetConfirm.js | 0
.../src/components/PasswordResetForm.js | 0
.../src/components/PersonalSetting.js | 0
.../src/components/PrivateRoute.js | 0
.../src/components/RedemptionsTable.js | 0
.../src/components/RegisterForm.js | 0
.../src/components/SystemSetting.js | 0
.../src/components/TokensTable.js | 0
.../src/components/UsersTable.js | 0
web/{ => default}/src/components/utils.js | 0
.../src/constants/channel.constants.js | 0
.../src/constants/common.constant.js | 0
web/{ => default}/src/constants/index.js | 0
.../src/constants/toast.constants.js | 0
.../src/constants/user.constants.js | 0
web/{ => default}/src/context/Status/index.js | 0
.../src/context/Status/reducer.js | 0
web/{ => default}/src/context/User/index.js | 0
web/{ => default}/src/context/User/reducer.js | 0
web/{ => default}/src/helpers/api.js | 0
web/{ => default}/src/helpers/auth-header.js | 0
web/{ => default}/src/helpers/history.js | 0
web/{ => default}/src/helpers/index.js | 0
web/{ => default}/src/helpers/render.js | 0
web/{ => default}/src/helpers/utils.js | 0
web/{ => default}/src/index.css | 0
web/{ => default}/src/index.js | 0
web/{ => default}/src/pages/About/index.js | 0
.../src/pages/Channel/EditChannel.js | 0
web/{ => default}/src/pages/Channel/index.js | 0
web/{ => default}/src/pages/Chat/index.js | 0
web/{ => default}/src/pages/Home/index.js | 0
web/{ => default}/src/pages/Log/index.js | 0
web/{ => default}/src/pages/NotFound/index.js | 0
.../src/pages/Redemption/EditRedemption.js | 0
.../src/pages/Redemption/index.js | 0
web/{ => default}/src/pages/Setting/index.js | 0
.../src/pages/Token/EditToken.js | 0
web/{ => default}/src/pages/Token/index.js | 0
web/{ => default}/src/pages/TopUp/index.js | 0
web/{ => default}/src/pages/User/AddUser.js | 0
web/{ => default}/src/pages/User/EditUser.js | 0
web/{ => default}/src/pages/User/index.js | 0
web/{ => default}/vercel.json | 0
72 files changed, 66 insertions(+), 42 deletions(-)
create mode 100644 web/THEMES
create mode 100644 web/build/.gitkeep
rename web/{ => default}/.gitignore (100%)
create mode 100644 web/default/README.md
rename web/{ => default}/package.json (94%)
rename web/{ => default}/public/favicon.ico (100%)
rename web/{ => default}/public/index.html (100%)
rename web/{ => default}/public/logo.png (100%)
rename web/{ => default}/public/robots.txt (100%)
rename web/{ => default}/src/App.js (100%)
rename web/{ => default}/src/components/ChannelsTable.js (100%)
rename web/{ => default}/src/components/Footer.js (100%)
rename web/{ => default}/src/components/GitHubOAuth.js (100%)
rename web/{ => default}/src/components/Header.js (100%)
rename web/{ => default}/src/components/Loading.js (100%)
rename web/{ => default}/src/components/LoginForm.js (100%)
rename web/{ => default}/src/components/LogsTable.js (100%)
rename web/{ => default}/src/components/OperationSetting.js (100%)
rename web/{ => default}/src/components/OtherSetting.js (100%)
rename web/{ => default}/src/components/PasswordResetConfirm.js (100%)
rename web/{ => default}/src/components/PasswordResetForm.js (100%)
rename web/{ => default}/src/components/PersonalSetting.js (100%)
rename web/{ => default}/src/components/PrivateRoute.js (100%)
rename web/{ => default}/src/components/RedemptionsTable.js (100%)
rename web/{ => default}/src/components/RegisterForm.js (100%)
rename web/{ => default}/src/components/SystemSetting.js (100%)
rename web/{ => default}/src/components/TokensTable.js (100%)
rename web/{ => default}/src/components/UsersTable.js (100%)
rename web/{ => default}/src/components/utils.js (100%)
rename web/{ => default}/src/constants/channel.constants.js (100%)
rename web/{ => default}/src/constants/common.constant.js (100%)
rename web/{ => default}/src/constants/index.js (100%)
rename web/{ => default}/src/constants/toast.constants.js (100%)
rename web/{ => default}/src/constants/user.constants.js (100%)
rename web/{ => default}/src/context/Status/index.js (100%)
rename web/{ => default}/src/context/Status/reducer.js (100%)
rename web/{ => default}/src/context/User/index.js (100%)
rename web/{ => default}/src/context/User/reducer.js (100%)
rename web/{ => default}/src/helpers/api.js (100%)
rename web/{ => default}/src/helpers/auth-header.js (100%)
rename web/{ => default}/src/helpers/history.js (100%)
rename web/{ => default}/src/helpers/index.js (100%)
rename web/{ => default}/src/helpers/render.js (100%)
rename web/{ => default}/src/helpers/utils.js (100%)
rename web/{ => default}/src/index.css (100%)
rename web/{ => default}/src/index.js (100%)
rename web/{ => default}/src/pages/About/index.js (100%)
rename web/{ => default}/src/pages/Channel/EditChannel.js (100%)
rename web/{ => default}/src/pages/Channel/index.js (100%)
rename web/{ => default}/src/pages/Chat/index.js (100%)
rename web/{ => default}/src/pages/Home/index.js (100%)
rename web/{ => default}/src/pages/Log/index.js (100%)
rename web/{ => default}/src/pages/NotFound/index.js (100%)
rename web/{ => default}/src/pages/Redemption/EditRedemption.js (100%)
rename web/{ => default}/src/pages/Redemption/index.js (100%)
rename web/{ => default}/src/pages/Setting/index.js (100%)
rename web/{ => default}/src/pages/Token/EditToken.js (100%)
rename web/{ => default}/src/pages/Token/index.js (100%)
rename web/{ => default}/src/pages/TopUp/index.js (100%)
rename web/{ => default}/src/pages/User/AddUser.js (100%)
rename web/{ => default}/src/pages/User/EditUser.js (100%)
rename web/{ => default}/src/pages/User/index.js (100%)
rename web/{ => default}/vercel.json (100%)
diff --git a/.github/workflows/linux-release.yml b/.github/workflows/linux-release.yml
index 364b83ae..d9375795 100644
--- a/.github/workflows/linux-release.yml
+++ b/.github/workflows/linux-release.yml
@@ -18,14 +18,14 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version: 16
- - name: Build Frontend
+ - name: Build Frontend (theme default)
env:
CI: ""
run: |
- cd web
+ cd web/default
npm install
REACT_APP_VERSION=$(git describe --tags) npm run build
- cd ..
+ cd ../..
- name: Set up Go
uses: actions/setup-go@v3
with:
diff --git a/.github/workflows/macos-release.yml b/.github/workflows/macos-release.yml
index bdd0d208..69bb93f5 100644
--- a/.github/workflows/macos-release.yml
+++ b/.github/workflows/macos-release.yml
@@ -18,14 +18,14 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version: 16
- - name: Build Frontend
+ - name: Build Frontend (theme default)
env:
CI: ""
run: |
- cd web
+ cd web/default
npm install
REACT_APP_VERSION=$(git describe --tags) npm run build
- cd ..
+ cd ../..
- name: Set up Go
uses: actions/setup-go@v3
with:
diff --git a/.github/workflows/windows-release.yml b/.github/workflows/windows-release.yml
index 33193a89..c08e95d2 100644
--- a/.github/workflows/windows-release.yml
+++ b/.github/workflows/windows-release.yml
@@ -21,14 +21,14 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version: 16
- - name: Build Frontend
+ - name: Build Frontend (theme default)
env:
CI: ""
run: |
- cd web
+ cd web/default
npm install
REACT_APP_VERSION=$(git describe --tags) npm run build
- cd ..
+ cd ../..
- name: Set up Go
uses: actions/setup-go@v3
with:
diff --git a/Dockerfile b/Dockerfile
index ffb8c21b..56648168 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,11 +1,18 @@
FROM node:16 as builder
WORKDIR /build
-COPY web/package.json .
-RUN npm install
COPY ./web .
COPY ./VERSION .
-RUN DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat VERSION) npm run build
+RUN themes=$(cat THEMES) \
+ && IFS=$'\n' \
+ && for theme in $themes; do \
+ theme_path="web/$theme" \
+ && echo "Building theme: $theme" \
+ && cd $theme_path \
+ && npm install \
+ && DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat VERSION) npm run build \
+ && cd /app \
+ done
FROM golang AS builder2
diff --git a/common/constants.go b/common/constants.go
index e4cbf8bf..70589041 100644
--- a/common/constants.go
+++ b/common/constants.go
@@ -100,6 +100,8 @@ var RelayTimeout = GetOrDefault("RELAY_TIMEOUT", 0) // unit is second
var GeminiSafetySetting = GetOrDefaultString("GEMINI_SAFETY_SETTING", "BLOCK_NONE")
+var Theme = GetOrDefaultString("THEME", "default")
+
const (
RequestIdKey = "X-Oneapi-Request-Id"
)
diff --git a/main.go b/main.go
index 88938516..3ab1872c 100644
--- a/main.go
+++ b/main.go
@@ -15,15 +15,12 @@ import (
"strconv"
)
-//go:embed web/build
+//go:embed web/build/*
var buildFS embed.FS
-//go:embed web/build/index.html
-var indexPage []byte
-
func main() {
common.SetupLogger()
- common.SysLog("One API " + common.Version + " started")
+ common.SysLog(fmt.Sprintf("One API %s started with theme %s", common.Version, common.Theme))
if os.Getenv("GIN_MODE") != "debug" {
gin.SetMode(gin.ReleaseMode)
}
@@ -95,7 +92,7 @@ func main() {
store := cookie.NewStore([]byte(common.SessionSecret))
server.Use(sessions.Sessions("session", store))
- router.SetRouter(server, buildFS, indexPage)
+ router.SetRouter(server, buildFS)
var port = os.Getenv("PORT")
if port == "" {
port = strconv.Itoa(*common.Port)
diff --git a/router/main.go b/router/main.go
index b8ac4055..85127a1a 100644
--- a/router/main.go
+++ b/router/main.go
@@ -10,7 +10,7 @@ import (
"strings"
)
-func SetRouter(router *gin.Engine, buildFS embed.FS, indexPage []byte) {
+func SetRouter(router *gin.Engine, buildFS embed.FS) {
SetApiRouter(router)
SetDashboardRouter(router)
SetRelayRouter(router)
@@ -20,7 +20,7 @@ func SetRouter(router *gin.Engine, buildFS embed.FS, indexPage []byte) {
common.SysLog("FRONTEND_BASE_URL is ignored on master node")
}
if frontendBaseUrl == "" {
- SetWebRouter(router, buildFS, indexPage)
+ SetWebRouter(router, buildFS)
} else {
frontendBaseUrl = strings.TrimSuffix(frontendBaseUrl, "/")
router.NoRoute(func(c *gin.Context) {
diff --git a/router/web-router.go b/router/web-router.go
index 8f9c18a2..2f86db38 100644
--- a/router/web-router.go
+++ b/router/web-router.go
@@ -2,6 +2,7 @@ package router
import (
"embed"
+ "fmt"
"github.com/gin-contrib/gzip"
"github.com/gin-contrib/static"
"github.com/gin-gonic/gin"
@@ -12,17 +13,22 @@ import (
"strings"
)
-func SetWebRouter(router *gin.Engine, buildFS embed.FS, indexPage []byte) {
+func SetWebRouter(router *gin.Engine, buildFS embed.FS) {
router.Use(gzip.Gzip(gzip.DefaultCompression))
router.Use(middleware.GlobalWebRateLimit())
router.Use(middleware.Cache())
- router.Use(static.Serve("/", common.EmbedFolder(buildFS, "web/build")))
+ router.Use(static.Serve("/", common.EmbedFolder(buildFS, fmt.Sprintf("web/build/%s", common.Theme))))
router.NoRoute(func(c *gin.Context) {
if strings.HasPrefix(c.Request.RequestURI, "/v1") || strings.HasPrefix(c.Request.RequestURI, "/api") {
controller.RelayNotFound(c)
return
}
c.Header("Cache-Control", "no-cache")
+ indexPage, err := buildFS.ReadFile(fmt.Sprintf("web/build/%s/index.html", common.Theme))
+ if err != nil {
+ controller.RelayNotFound(c)
+ return
+ }
c.Data(http.StatusOK, "text/html; charset=utf-8", indexPage)
})
}
diff --git a/web/README.md b/web/README.md
index 1b1031a3..1454940f 100644
--- a/web/README.md
+++ b/web/README.md
@@ -1,21 +1,11 @@
-# React Template
+# One API 的前端界面
+> 每个文件夹代表一个主题,欢迎提交你的主题
-## Basic Usages
+## 提交新的主题
+1. 在 `web` 文件夹下新建一个文件夹,文件夹名为主题名。
+2. 把你的主题文件放到这个文件夹下。
+3. 修改 `package.json` 文件,把 `build` 命令改为:`"build": "react-scripts build && mv build ../build/default"`,其中 `default` 为你的主题名。
-```shell
-# Runs the app in the development mode
-npm start
-
-# Builds the app for production to the `build` folder
-npm run build
-```
-
-If you want to change the default server, please set `REACT_APP_SERVER` environment variables before build,
-for example: `REACT_APP_SERVER=http://your.domain.com`.
-
-Before you start editing, make sure your `Actions on Save` options have `Optimize imports` & `Run Prettier` enabled.
-
-## Reference
-
-1. https://github.com/OIerDb-ng/OIerDb
-2. https://github.com/cornflourblue/react-hooks-redux-registration-login-example
\ No newline at end of file
+## 主题列表
+### default
+默认主题
\ No newline at end of file
diff --git a/web/THEMES b/web/THEMES
new file mode 100644
index 00000000..331d858c
--- /dev/null
+++ b/web/THEMES
@@ -0,0 +1 @@
+default
\ No newline at end of file
diff --git a/web/build/.gitkeep b/web/build/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/web/.gitignore b/web/default/.gitignore
similarity index 100%
rename from web/.gitignore
rename to web/default/.gitignore
diff --git a/web/default/README.md b/web/default/README.md
new file mode 100644
index 00000000..1b1031a3
--- /dev/null
+++ b/web/default/README.md
@@ -0,0 +1,21 @@
+# React Template
+
+## Basic Usages
+
+```shell
+# Runs the app in the development mode
+npm start
+
+# Builds the app for production to the `build` folder
+npm run build
+```
+
+If you want to change the default server, please set `REACT_APP_SERVER` environment variables before build,
+for example: `REACT_APP_SERVER=http://your.domain.com`.
+
+Before you start editing, make sure your `Actions on Save` options have `Optimize imports` & `Run Prettier` enabled.
+
+## Reference
+
+1. https://github.com/OIerDb-ng/OIerDb
+2. https://github.com/cornflourblue/react-hooks-redux-registration-login-example
\ No newline at end of file
diff --git a/web/package.json b/web/default/package.json
similarity index 94%
rename from web/package.json
rename to web/default/package.json
index a2bf3054..872ad36a 100644
--- a/web/package.json
+++ b/web/default/package.json
@@ -18,7 +18,7 @@
},
"scripts": {
"start": "react-scripts start",
- "build": "react-scripts build",
+ "build": "react-scripts build && mv build ../build/default",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
diff --git a/web/public/favicon.ico b/web/default/public/favicon.ico
similarity index 100%
rename from web/public/favicon.ico
rename to web/default/public/favicon.ico
diff --git a/web/public/index.html b/web/default/public/index.html
similarity index 100%
rename from web/public/index.html
rename to web/default/public/index.html
diff --git a/web/public/logo.png b/web/default/public/logo.png
similarity index 100%
rename from web/public/logo.png
rename to web/default/public/logo.png
diff --git a/web/public/robots.txt b/web/default/public/robots.txt
similarity index 100%
rename from web/public/robots.txt
rename to web/default/public/robots.txt
diff --git a/web/src/App.js b/web/default/src/App.js
similarity index 100%
rename from web/src/App.js
rename to web/default/src/App.js
diff --git a/web/src/components/ChannelsTable.js b/web/default/src/components/ChannelsTable.js
similarity index 100%
rename from web/src/components/ChannelsTable.js
rename to web/default/src/components/ChannelsTable.js
diff --git a/web/src/components/Footer.js b/web/default/src/components/Footer.js
similarity index 100%
rename from web/src/components/Footer.js
rename to web/default/src/components/Footer.js
diff --git a/web/src/components/GitHubOAuth.js b/web/default/src/components/GitHubOAuth.js
similarity index 100%
rename from web/src/components/GitHubOAuth.js
rename to web/default/src/components/GitHubOAuth.js
diff --git a/web/src/components/Header.js b/web/default/src/components/Header.js
similarity index 100%
rename from web/src/components/Header.js
rename to web/default/src/components/Header.js
diff --git a/web/src/components/Loading.js b/web/default/src/components/Loading.js
similarity index 100%
rename from web/src/components/Loading.js
rename to web/default/src/components/Loading.js
diff --git a/web/src/components/LoginForm.js b/web/default/src/components/LoginForm.js
similarity index 100%
rename from web/src/components/LoginForm.js
rename to web/default/src/components/LoginForm.js
diff --git a/web/src/components/LogsTable.js b/web/default/src/components/LogsTable.js
similarity index 100%
rename from web/src/components/LogsTable.js
rename to web/default/src/components/LogsTable.js
diff --git a/web/src/components/OperationSetting.js b/web/default/src/components/OperationSetting.js
similarity index 100%
rename from web/src/components/OperationSetting.js
rename to web/default/src/components/OperationSetting.js
diff --git a/web/src/components/OtherSetting.js b/web/default/src/components/OtherSetting.js
similarity index 100%
rename from web/src/components/OtherSetting.js
rename to web/default/src/components/OtherSetting.js
diff --git a/web/src/components/PasswordResetConfirm.js b/web/default/src/components/PasswordResetConfirm.js
similarity index 100%
rename from web/src/components/PasswordResetConfirm.js
rename to web/default/src/components/PasswordResetConfirm.js
diff --git a/web/src/components/PasswordResetForm.js b/web/default/src/components/PasswordResetForm.js
similarity index 100%
rename from web/src/components/PasswordResetForm.js
rename to web/default/src/components/PasswordResetForm.js
diff --git a/web/src/components/PersonalSetting.js b/web/default/src/components/PersonalSetting.js
similarity index 100%
rename from web/src/components/PersonalSetting.js
rename to web/default/src/components/PersonalSetting.js
diff --git a/web/src/components/PrivateRoute.js b/web/default/src/components/PrivateRoute.js
similarity index 100%
rename from web/src/components/PrivateRoute.js
rename to web/default/src/components/PrivateRoute.js
diff --git a/web/src/components/RedemptionsTable.js b/web/default/src/components/RedemptionsTable.js
similarity index 100%
rename from web/src/components/RedemptionsTable.js
rename to web/default/src/components/RedemptionsTable.js
diff --git a/web/src/components/RegisterForm.js b/web/default/src/components/RegisterForm.js
similarity index 100%
rename from web/src/components/RegisterForm.js
rename to web/default/src/components/RegisterForm.js
diff --git a/web/src/components/SystemSetting.js b/web/default/src/components/SystemSetting.js
similarity index 100%
rename from web/src/components/SystemSetting.js
rename to web/default/src/components/SystemSetting.js
diff --git a/web/src/components/TokensTable.js b/web/default/src/components/TokensTable.js
similarity index 100%
rename from web/src/components/TokensTable.js
rename to web/default/src/components/TokensTable.js
diff --git a/web/src/components/UsersTable.js b/web/default/src/components/UsersTable.js
similarity index 100%
rename from web/src/components/UsersTable.js
rename to web/default/src/components/UsersTable.js
diff --git a/web/src/components/utils.js b/web/default/src/components/utils.js
similarity index 100%
rename from web/src/components/utils.js
rename to web/default/src/components/utils.js
diff --git a/web/src/constants/channel.constants.js b/web/default/src/constants/channel.constants.js
similarity index 100%
rename from web/src/constants/channel.constants.js
rename to web/default/src/constants/channel.constants.js
diff --git a/web/src/constants/common.constant.js b/web/default/src/constants/common.constant.js
similarity index 100%
rename from web/src/constants/common.constant.js
rename to web/default/src/constants/common.constant.js
diff --git a/web/src/constants/index.js b/web/default/src/constants/index.js
similarity index 100%
rename from web/src/constants/index.js
rename to web/default/src/constants/index.js
diff --git a/web/src/constants/toast.constants.js b/web/default/src/constants/toast.constants.js
similarity index 100%
rename from web/src/constants/toast.constants.js
rename to web/default/src/constants/toast.constants.js
diff --git a/web/src/constants/user.constants.js b/web/default/src/constants/user.constants.js
similarity index 100%
rename from web/src/constants/user.constants.js
rename to web/default/src/constants/user.constants.js
diff --git a/web/src/context/Status/index.js b/web/default/src/context/Status/index.js
similarity index 100%
rename from web/src/context/Status/index.js
rename to web/default/src/context/Status/index.js
diff --git a/web/src/context/Status/reducer.js b/web/default/src/context/Status/reducer.js
similarity index 100%
rename from web/src/context/Status/reducer.js
rename to web/default/src/context/Status/reducer.js
diff --git a/web/src/context/User/index.js b/web/default/src/context/User/index.js
similarity index 100%
rename from web/src/context/User/index.js
rename to web/default/src/context/User/index.js
diff --git a/web/src/context/User/reducer.js b/web/default/src/context/User/reducer.js
similarity index 100%
rename from web/src/context/User/reducer.js
rename to web/default/src/context/User/reducer.js
diff --git a/web/src/helpers/api.js b/web/default/src/helpers/api.js
similarity index 100%
rename from web/src/helpers/api.js
rename to web/default/src/helpers/api.js
diff --git a/web/src/helpers/auth-header.js b/web/default/src/helpers/auth-header.js
similarity index 100%
rename from web/src/helpers/auth-header.js
rename to web/default/src/helpers/auth-header.js
diff --git a/web/src/helpers/history.js b/web/default/src/helpers/history.js
similarity index 100%
rename from web/src/helpers/history.js
rename to web/default/src/helpers/history.js
diff --git a/web/src/helpers/index.js b/web/default/src/helpers/index.js
similarity index 100%
rename from web/src/helpers/index.js
rename to web/default/src/helpers/index.js
diff --git a/web/src/helpers/render.js b/web/default/src/helpers/render.js
similarity index 100%
rename from web/src/helpers/render.js
rename to web/default/src/helpers/render.js
diff --git a/web/src/helpers/utils.js b/web/default/src/helpers/utils.js
similarity index 100%
rename from web/src/helpers/utils.js
rename to web/default/src/helpers/utils.js
diff --git a/web/src/index.css b/web/default/src/index.css
similarity index 100%
rename from web/src/index.css
rename to web/default/src/index.css
diff --git a/web/src/index.js b/web/default/src/index.js
similarity index 100%
rename from web/src/index.js
rename to web/default/src/index.js
diff --git a/web/src/pages/About/index.js b/web/default/src/pages/About/index.js
similarity index 100%
rename from web/src/pages/About/index.js
rename to web/default/src/pages/About/index.js
diff --git a/web/src/pages/Channel/EditChannel.js b/web/default/src/pages/Channel/EditChannel.js
similarity index 100%
rename from web/src/pages/Channel/EditChannel.js
rename to web/default/src/pages/Channel/EditChannel.js
diff --git a/web/src/pages/Channel/index.js b/web/default/src/pages/Channel/index.js
similarity index 100%
rename from web/src/pages/Channel/index.js
rename to web/default/src/pages/Channel/index.js
diff --git a/web/src/pages/Chat/index.js b/web/default/src/pages/Chat/index.js
similarity index 100%
rename from web/src/pages/Chat/index.js
rename to web/default/src/pages/Chat/index.js
diff --git a/web/src/pages/Home/index.js b/web/default/src/pages/Home/index.js
similarity index 100%
rename from web/src/pages/Home/index.js
rename to web/default/src/pages/Home/index.js
diff --git a/web/src/pages/Log/index.js b/web/default/src/pages/Log/index.js
similarity index 100%
rename from web/src/pages/Log/index.js
rename to web/default/src/pages/Log/index.js
diff --git a/web/src/pages/NotFound/index.js b/web/default/src/pages/NotFound/index.js
similarity index 100%
rename from web/src/pages/NotFound/index.js
rename to web/default/src/pages/NotFound/index.js
diff --git a/web/src/pages/Redemption/EditRedemption.js b/web/default/src/pages/Redemption/EditRedemption.js
similarity index 100%
rename from web/src/pages/Redemption/EditRedemption.js
rename to web/default/src/pages/Redemption/EditRedemption.js
diff --git a/web/src/pages/Redemption/index.js b/web/default/src/pages/Redemption/index.js
similarity index 100%
rename from web/src/pages/Redemption/index.js
rename to web/default/src/pages/Redemption/index.js
diff --git a/web/src/pages/Setting/index.js b/web/default/src/pages/Setting/index.js
similarity index 100%
rename from web/src/pages/Setting/index.js
rename to web/default/src/pages/Setting/index.js
diff --git a/web/src/pages/Token/EditToken.js b/web/default/src/pages/Token/EditToken.js
similarity index 100%
rename from web/src/pages/Token/EditToken.js
rename to web/default/src/pages/Token/EditToken.js
diff --git a/web/src/pages/Token/index.js b/web/default/src/pages/Token/index.js
similarity index 100%
rename from web/src/pages/Token/index.js
rename to web/default/src/pages/Token/index.js
diff --git a/web/src/pages/TopUp/index.js b/web/default/src/pages/TopUp/index.js
similarity index 100%
rename from web/src/pages/TopUp/index.js
rename to web/default/src/pages/TopUp/index.js
diff --git a/web/src/pages/User/AddUser.js b/web/default/src/pages/User/AddUser.js
similarity index 100%
rename from web/src/pages/User/AddUser.js
rename to web/default/src/pages/User/AddUser.js
diff --git a/web/src/pages/User/EditUser.js b/web/default/src/pages/User/EditUser.js
similarity index 100%
rename from web/src/pages/User/EditUser.js
rename to web/default/src/pages/User/EditUser.js
diff --git a/web/src/pages/User/index.js b/web/default/src/pages/User/index.js
similarity index 100%
rename from web/src/pages/User/index.js
rename to web/default/src/pages/User/index.js
diff --git a/web/vercel.json b/web/default/vercel.json
similarity index 100%
rename from web/vercel.json
rename to web/default/vercel.json
From 83f95935de30e74ac012e5b1048a76d9aca9aac2 Mon Sep 17 00:00:00 2001
From: JustSong
Date: Mon, 1 Jan 2024 19:23:46 +0800
Subject: [PATCH 10/35] ci: fix Dockerfile & ci
---
.github/workflows/linux-release.yml | 13 +++++++++----
.github/workflows/macos-release.yml | 13 +++++++++----
.github/workflows/windows-release.yml | 5 +++++
Dockerfile | 11 +----------
README.md | 1 +
web/build.sh | 13 +++++++++++++
6 files changed, 38 insertions(+), 18 deletions(-)
create mode 100644 web/build.sh
diff --git a/.github/workflows/linux-release.yml b/.github/workflows/linux-release.yml
index d9375795..d93c70ca 100644
--- a/.github/workflows/linux-release.yml
+++ b/.github/workflows/linux-release.yml
@@ -7,6 +7,11 @@ on:
tags:
- '*'
- '!*-alpha*'
+ workflow_dispatch:
+ inputs:
+ name:
+ description: 'reason'
+ required: false
jobs:
release:
runs-on: ubuntu-latest
@@ -22,10 +27,10 @@ jobs:
env:
CI: ""
run: |
- cd web/default
- npm install
- REACT_APP_VERSION=$(git describe --tags) npm run build
- cd ../..
+ cd web
+ git describe --tags > VERSION
+ REACT_APP_VERSION=$(git describe --tags) chmod u+x ./build.sh && ./build.sh
+ cd ..
- name: Set up Go
uses: actions/setup-go@v3
with:
diff --git a/.github/workflows/macos-release.yml b/.github/workflows/macos-release.yml
index 69bb93f5..ce9d1f11 100644
--- a/.github/workflows/macos-release.yml
+++ b/.github/workflows/macos-release.yml
@@ -7,6 +7,11 @@ on:
tags:
- '*'
- '!*-alpha*'
+ workflow_dispatch:
+ inputs:
+ name:
+ description: 'reason'
+ required: false
jobs:
release:
runs-on: macos-latest
@@ -22,10 +27,10 @@ jobs:
env:
CI: ""
run: |
- cd web/default
- npm install
- REACT_APP_VERSION=$(git describe --tags) npm run build
- cd ../..
+ cd web
+ git describe --tags > VERSION
+ REACT_APP_VERSION=$(git describe --tags) chmod u+x ./build.sh && ./build.sh
+ cd ..
- name: Set up Go
uses: actions/setup-go@v3
with:
diff --git a/.github/workflows/windows-release.yml b/.github/workflows/windows-release.yml
index c08e95d2..9b1f16ba 100644
--- a/.github/workflows/windows-release.yml
+++ b/.github/workflows/windows-release.yml
@@ -7,6 +7,11 @@ on:
tags:
- '*'
- '!*-alpha*'
+ workflow_dispatch:
+ inputs:
+ name:
+ description: 'reason'
+ required: false
jobs:
release:
runs-on: windows-latest
diff --git a/Dockerfile b/Dockerfile
index 56648168..b21a7b3c 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -3,16 +3,7 @@ FROM node:16 as builder
WORKDIR /build
COPY ./web .
COPY ./VERSION .
-RUN themes=$(cat THEMES) \
- && IFS=$'\n' \
- && for theme in $themes; do \
- theme_path="web/$theme" \
- && echo "Building theme: $theme" \
- && cd $theme_path \
- && npm install \
- && DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat VERSION) npm run build \
- && cd /app \
- done
+RUN chmod u+x ./build.sh && ./build.sh
FROM golang AS builder2
diff --git a/README.md b/README.md
index b53936c4..28f4e5e6 100644
--- a/README.md
+++ b/README.md
@@ -99,6 +99,7 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用
+ 邮箱登录注册(支持注册邮箱白名单)以及通过邮箱进行密码重置。
+ [GitHub 开放授权](https://github.com/settings/applications/new)。
+ 微信公众号授权(需要额外部署 [WeChat Server](https://github.com/songquanpeng/wechat-server))。
+23. 支持主题切换,设置环境变量 `THEME` 即可,默认为 `default`,欢迎 PR 更多主题,具体参考[此处](./web/README.md)。
## 部署
### 基于 Docker 进行部署
diff --git a/web/build.sh b/web/build.sh
new file mode 100644
index 00000000..b3751ff4
--- /dev/null
+++ b/web/build.sh
@@ -0,0 +1,13 @@
+#!/bin/sh
+
+version=$(cat VERSION)
+themes=$(cat THEMES)
+IFS=$'\n'
+
+for theme in $themes; do
+ echo "Building theme: $theme"
+ cd $theme
+ npm install
+ DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$version npm run build
+ cd ..
+done
From 0c022f17cb28e500c05670f3c89f757fbcc5c864 Mon Sep 17 00:00:00 2001
From: JustSong
Date: Mon, 1 Jan 2024 20:25:53 +0800
Subject: [PATCH 11/35] chore: update theme related code
---
router/web-router.go | 8 ++------
web/README.md | 2 +-
web/default/package.json | 2 +-
3 files changed, 4 insertions(+), 8 deletions(-)
diff --git a/router/web-router.go b/router/web-router.go
index 2f86db38..7328c7a3 100644
--- a/router/web-router.go
+++ b/router/web-router.go
@@ -14,6 +14,7 @@ import (
)
func SetWebRouter(router *gin.Engine, buildFS embed.FS) {
+ indexPageData, _ := buildFS.ReadFile(fmt.Sprintf("web/build/%s/index.html", common.Theme))
router.Use(gzip.Gzip(gzip.DefaultCompression))
router.Use(middleware.GlobalWebRateLimit())
router.Use(middleware.Cache())
@@ -24,11 +25,6 @@ func SetWebRouter(router *gin.Engine, buildFS embed.FS) {
return
}
c.Header("Cache-Control", "no-cache")
- indexPage, err := buildFS.ReadFile(fmt.Sprintf("web/build/%s/index.html", common.Theme))
- if err != nil {
- controller.RelayNotFound(c)
- return
- }
- c.Data(http.StatusOK, "text/html; charset=utf-8", indexPage)
+ c.Data(http.StatusOK, "text/html; charset=utf-8", indexPageData)
})
}
diff --git a/web/README.md b/web/README.md
index 1454940f..dad20427 100644
--- a/web/README.md
+++ b/web/README.md
@@ -4,7 +4,7 @@
## 提交新的主题
1. 在 `web` 文件夹下新建一个文件夹,文件夹名为主题名。
2. 把你的主题文件放到这个文件夹下。
-3. 修改 `package.json` 文件,把 `build` 命令改为:`"build": "react-scripts build && mv build ../build/default"`,其中 `default` 为你的主题名。
+3. 修改 `package.json` 文件,把 `build` 命令改为:`"build": "react-scripts build && mv -f build ../build/default"`,其中 `default` 为你的主题名。
## 主题列表
### default
diff --git a/web/default/package.json b/web/default/package.json
index 872ad36a..438f020c 100644
--- a/web/default/package.json
+++ b/web/default/package.json
@@ -18,7 +18,7 @@
},
"scripts": {
"start": "react-scripts start",
- "build": "react-scripts build && mv build ../build/default",
+ "build": "react-scripts build && mv -f build ../build/default",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
From 92886093ae6c21fe75abf3c81347c8e516c3177f Mon Sep 17 00:00:00 2001
From: JustSong <39998050+songquanpeng@users.noreply.github.com>
Date: Mon, 1 Jan 2024 21:10:40 +0800
Subject: [PATCH 12/35] docs: update readme
---
web/README.md | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/web/README.md b/web/README.md
index dad20427..30846c6f 100644
--- a/web/README.md
+++ b/web/README.md
@@ -7,5 +7,10 @@
3. 修改 `package.json` 文件,把 `build` 命令改为:`"build": "react-scripts build && mv -f build ../build/default"`,其中 `default` 为你的主题名。
## 主题列表
-### default
-默认主题
\ No newline at end of file
+### 主题:default
+默认主题,由 [JustSong](https://github.com/songquanpeng) 开发。
+
+预览:
+|||
+|:---:|:---:|
+
From 4a96031ce60d88f71a5290e8771b2ba24debd7dc Mon Sep 17 00:00:00 2001
From: JustSong
Date: Mon, 1 Jan 2024 21:14:45 +0800
Subject: [PATCH 13/35] docs: update readme
---
README.md | 1 +
web/README.md | 1 +
2 files changed, 2 insertions(+)
diff --git a/README.md b/README.md
index 28f4e5e6..edc467aa 100644
--- a/README.md
+++ b/README.md
@@ -368,6 +368,7 @@ graph LR
15. `RELAY_TIMEOUT`:中继超时设置,单位为秒,默认不设置超时时间。
16. `SQLITE_BUSY_TIMEOUT`:SQLite 锁等待超时设置,单位为毫秒,默认 `3000`。
17. `GEMINI_SAFETY_SETTING`:Gemini 的安全设置,默认 `BLOCK_NONE`。
+18. `THEME`:系统的主题设置,默认为 `default`,具体可选值参考[此处](./web/README.md)。
### 命令行参数
1. `--port `: 指定服务器监听的端口号,默认为 `3000`。
diff --git a/web/README.md b/web/README.md
index 30846c6f..ca73b298 100644
--- a/web/README.md
+++ b/web/README.md
@@ -2,6 +2,7 @@
> 每个文件夹代表一个主题,欢迎提交你的主题
## 提交新的主题
+> 欢迎在页面底部保留你和 One API 的版权信息以及指向链接
1. 在 `web` 文件夹下新建一个文件夹,文件夹名为主题名。
2. 把你的主题文件放到这个文件夹下。
3. 修改 `package.json` 文件,把 `build` 命令改为:`"build": "react-scripts build && mv -f build ../build/default"`,其中 `default` 为你的主题名。
From cbf8f077470fceb38fe9d262c42a64eb4be4eb0c Mon Sep 17 00:00:00 2001
From: JustSong
Date: Mon, 1 Jan 2024 21:19:37 +0800
Subject: [PATCH 14/35] docs: fix logo
---
README.en.md | 2 +-
README.ja.md | 2 +-
README.md | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/README.en.md b/README.en.md
index 82dceb5b..e7f254f7 100644
--- a/README.en.md
+++ b/README.en.md
@@ -3,7 +3,7 @@
-
+
diff --git a/README.ja.md b/README.ja.md
index 089fc2b5..edfd2a28 100644
--- a/README.ja.md
+++ b/README.ja.md
@@ -3,7 +3,7 @@
-
+
diff --git a/README.md b/README.md
index edc467aa..27acfedd 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
-
+
From 6227eee5bc751446db4042d4e6bd8531919026a0 Mon Sep 17 00:00:00 2001
From: Seven Yu <422347121@qq.com>
Date: Sun, 7 Jan 2024 13:32:39 +0800
Subject: [PATCH 15/35] fix: fix token validation exception handling #901
* fix: fix exception handling
1. add error log for ValidateUserToken
2. update en.json
* chore: update log
---------
Co-authored-by: seven.yu
Co-authored-by: JustSong
---
i18n/en.json | 1 +
model/token.go | 66 ++++++++++++++++++++++++++------------------------
2 files changed, 36 insertions(+), 31 deletions(-)
diff --git a/i18n/en.json b/i18n/en.json
index f67d8665..774be837 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -86,6 +86,7 @@
"该令牌已过期": "The token has expired",
"该令牌额度已用尽": "The token quota has been used up",
"无效的令牌": "Invalid token",
+ "令牌验证失败": "Token verification failed",
"id 或 userId 为空!": "id or userId is empty!",
"quota 不能为负数!": "quota cannot be negative!",
"令牌额度不足": "Insufficient token quota",
diff --git a/model/token.go b/model/token.go
index 0fa984d3..2e53ac0b 100644
--- a/model/token.go
+++ b/model/token.go
@@ -38,39 +38,43 @@ func ValidateUserToken(key string) (token *Token, err error) {
return nil, errors.New("未提供令牌")
}
token, err = CacheGetTokenByKey(key)
- if err == nil {
- if token.Status == common.TokenStatusExhausted {
- return nil, errors.New("该令牌额度已用尽")
- } else if token.Status == common.TokenStatusExpired {
- return nil, errors.New("该令牌已过期")
+ if err != nil {
+ common.SysError("CacheGetTokenByKey failed: " + err.Error())
+ if errors.Is(err, gorm.ErrRecordNotFound) {
+ return nil, errors.New("无效的令牌")
}
- if token.Status != common.TokenStatusEnabled {
- return nil, errors.New("该令牌状态不可用")
- }
- if token.ExpiredTime != -1 && token.ExpiredTime < common.GetTimestamp() {
- if !common.RedisEnabled {
- token.Status = common.TokenStatusExpired
- err := token.SelectUpdate()
- if err != nil {
- common.SysError("failed to update token status" + err.Error())
- }
- }
- return nil, errors.New("该令牌已过期")
- }
- if !token.UnlimitedQuota && token.RemainQuota <= 0 {
- if !common.RedisEnabled {
- // in this case, we can make sure the token is exhausted
- token.Status = common.TokenStatusExhausted
- err := token.SelectUpdate()
- if err != nil {
- common.SysError("failed to update token status" + err.Error())
- }
- }
- return nil, errors.New("该令牌额度已用尽")
- }
- return token, nil
+ return nil, errors.New("令牌验证失败")
}
- return nil, errors.New("无效的令牌")
+ if token.Status == common.TokenStatusExhausted {
+ return nil, errors.New("该令牌额度已用尽")
+ } else if token.Status == common.TokenStatusExpired {
+ return nil, errors.New("该令牌已过期")
+ }
+ if token.Status != common.TokenStatusEnabled {
+ return nil, errors.New("该令牌状态不可用")
+ }
+ if token.ExpiredTime != -1 && token.ExpiredTime < common.GetTimestamp() {
+ if !common.RedisEnabled {
+ token.Status = common.TokenStatusExpired
+ err := token.SelectUpdate()
+ if err != nil {
+ common.SysError("failed to update token status" + err.Error())
+ }
+ }
+ return nil, errors.New("该令牌已过期")
+ }
+ if !token.UnlimitedQuota && token.RemainQuota <= 0 {
+ if !common.RedisEnabled {
+ // in this case, we can make sure the token is exhausted
+ token.Status = common.TokenStatusExhausted
+ err := token.SelectUpdate()
+ if err != nil {
+ common.SysError("failed to update token status" + err.Error())
+ }
+ }
+ return nil, errors.New("该令牌额度已用尽")
+ }
+ return token, nil
}
func GetTokenByIds(id int, userId int) (*Token, error) {
From 48989d4a0b63e826634896003582b308745c9cf8 Mon Sep 17 00:00:00 2001
From: Buer <42402987+MartialBE@users.noreply.github.com>
Date: Sun, 7 Jan 2024 14:20:07 +0800
Subject: [PATCH 16/35] feat: add new theme berry (#860)
* feat: add theme berry
* docs: add development notes
* fix: fix blank page
* chore: update implementation
* fix: fix package.json
* chore: update ui copy
---------
Co-authored-by: JustSong
---
controller/model.go | 6 +-
controller/user.go | 24 +
model/log.go | 42 +-
router/api-router.go | 1 +
web/README.md | 19 +
web/THEMES | 3 +-
web/berry/.gitignore | 26 +
web/berry/README.md | 61 ++
web/berry/jsconfig.json | 9 +
web/berry/package.json | 84 ++
web/berry/public/favicon.ico | Bin 0 -> 41270 bytes
web/berry/public/index.html | 26 +
web/berry/src/App.js | 43 ++
web/berry/src/assets/images/404.svg | 40 +
.../src/assets/images/auth/auth-blue-card.svg | 65 ++
.../assets/images/auth/auth-pattern-dark.svg | 39 +
.../src/assets/images/auth/auth-pattern.svg | 39 +
.../assets/images/auth/auth-purple-card.svg | 69 ++
.../images/auth/auth-signup-blue-card.svg | 1 +
.../images/auth/auth-signup-white-card.svg | 40 +
web/berry/src/assets/images/icons/earning.svg | 5 +
web/berry/src/assets/images/icons/github.svg | 1 +
.../src/assets/images/icons/shape-avatar.svg | 1 +
.../src/assets/images/icons/social-google.svg | 6 +
web/berry/src/assets/images/icons/wechat.svg | 1 +
web/berry/src/assets/images/invite/cover.jpg | Bin 0 -> 87952 bytes
.../assets/images/invite/cwok_casual_19.webp | Bin 0 -> 177892 bytes
web/berry/src/assets/images/logo-2.svg | 15 +
web/berry/src/assets/images/logo.svg | 13 +
.../src/assets/images/users/user-round.svg | 1 +
.../src/assets/scss/_themes-vars.module.scss | 157 ++++
web/berry/src/assets/scss/style.scss | 128 ++++
web/berry/src/config.js | 29 +
web/berry/src/constants/ChannelConstants.js | 146 ++++
web/berry/src/constants/CommonConstants.js | 1 +
web/berry/src/constants/SnackbarConstants.js | 27 +
web/berry/src/constants/index.js | 3 +
web/berry/src/contexts/StatusContext.js | 70 ++
web/berry/src/contexts/UserContext.js | 29 +
web/berry/src/hooks/useAuth.js | 13 +
web/berry/src/hooks/useLogin.js | 78 ++
web/berry/src/hooks/useRegister.js | 39 +
web/berry/src/hooks/useScriptRef.js | 18 +
web/berry/src/index.js | 31 +
.../MainLayout/Header/ProfileSection/index.js | 173 +++++
.../src/layout/MainLayout/Header/index.js | 68 ++
.../layout/MainLayout/LogoSection/index.js | 23 +
.../MainLayout/Sidebar/MenuCard/index.js | 130 ++++
.../Sidebar/MenuList/NavCollapse/index.js | 158 ++++
.../Sidebar/MenuList/NavGroup/index.js | 61 ++
.../Sidebar/MenuList/NavItem/index.js | 115 +++
.../MainLayout/Sidebar/MenuList/index.js | 36 +
.../src/layout/MainLayout/Sidebar/index.js | 94 +++
web/berry/src/layout/MainLayout/index.js | 103 +++
.../src/layout/MinimalLayout/Header/index.js | 75 ++
web/berry/src/layout/MinimalLayout/index.js | 39 +
web/berry/src/layout/NavMotion.js | 39 +
web/berry/src/layout/NavigationScroll.js | 26 +
web/berry/src/menu-items/index.js | 18 +
web/berry/src/menu-items/panel.js | 104 +++
web/berry/src/routes/MainRoutes.js | 73 ++
web/berry/src/routes/OtherRoutes.js | 58 ++
web/berry/src/routes/index.js | 11 +
web/berry/src/serviceWorker.js | 128 ++++
web/berry/src/store/accountReducer.js | 24 +
web/berry/src/store/actions.js | 9 +
web/berry/src/store/constant.js | 4 +
web/berry/src/store/customizationReducer.js | 46 ++
web/berry/src/store/index.js | 9 +
web/berry/src/store/reducer.js | 16 +
web/berry/src/store/siteInfoReducer.js | 18 +
web/berry/src/themes/compStyleOverride.js | 256 +++++++
web/berry/src/themes/index.js | 55 ++
web/berry/src/themes/palette.js | 73 ++
web/berry/src/themes/typography.js | 137 ++++
web/berry/src/ui-component/AdminContainer.js | 11 +
web/berry/src/ui-component/Footer.js | 37 +
web/berry/src/ui-component/Label.js | 158 ++++
web/berry/src/ui-component/Loadable.js | 15 +
web/berry/src/ui-component/Loader.js | 21 +
web/berry/src/ui-component/Logo.js | 21 +
web/berry/src/ui-component/SvgColor.js | 31 +
web/berry/src/ui-component/Switch.js | 37 +
web/berry/src/ui-component/TableToolBar.js | 47 ++
.../ui-component/cards/CardSecondaryAction.js | 55 ++
web/berry/src/ui-component/cards/MainCard.js | 80 ++
.../cards/Skeleton/EarningCard.js | 32 +
.../cards/Skeleton/ImagePlaceholder.js | 8 +
.../cards/Skeleton/PopularCard.js | 155 ++++
.../cards/Skeleton/ProductPlaceholder.js | 44 ++
.../cards/Skeleton/TotalGrowthBarChart.js | 39 +
.../cards/Skeleton/TotalIncomeCard.js | 19 +
web/berry/src/ui-component/cards/SubCard.js | 72 ++
web/berry/src/ui-component/cards/UserCard.js | 121 +++
.../ui-component/extended/AnimateButton.js | 92 +++
web/berry/src/ui-component/extended/Avatar.js | 72 ++
.../src/ui-component/extended/Breadcrumbs.js | 187 +++++
.../src/ui-component/extended/Transitions.js | 107 +++
web/berry/src/utils/api.js | 26 +
web/berry/src/utils/chart.js | 96 +++
web/berry/src/utils/common.js | 188 +++++
web/berry/src/utils/password-strength.js | 34 +
web/berry/src/utils/route-guard/AuthGuard.js | 20 +
web/berry/src/views/About/index.js | 69 ++
.../Authentication/Auth/ForgetPassword.js | 68 ++
.../views/Authentication/Auth/GitHubOAuth.js | 94 +++
.../src/views/Authentication/Auth/Login.js | 66 ++
.../src/views/Authentication/Auth/Register.js | 71 ++
.../Authentication/Auth/ResetPassword.js | 66 ++
.../views/Authentication/AuthCardWrapper.js | 32 +
.../Authentication/AuthForms/AuthLogin.js | 268 +++++++
.../Authentication/AuthForms/AuthRegister.js | 310 ++++++++
.../AuthForms/ForgetPasswordForm.js | 174 +++++
.../AuthForms/ResetPasswordForm.js | 75 ++
.../Authentication/AuthForms/WechatModal.js | 70 ++
.../src/views/Authentication/AuthWrapper.js | 28 +
.../src/views/Channel/component/EditModal.js | 718 ++++++++++++++++++
.../src/views/Channel/component/GroupLabel.js | 27 +
.../src/views/Channel/component/NameLabel.js | 54 ++
.../Channel/component/ResponseTimeLabel.js | 43 ++
.../src/views/Channel/component/TableHead.js | 21 +
.../src/views/Channel/component/TableRow.js | 284 +++++++
web/berry/src/views/Channel/index.js | 294 +++++++
web/berry/src/views/Channel/type/Config.js | 144 ++++
.../component/StatisticalBarChart.js | 169 +++++
.../Dashboard/component/StatisticalCard.js | 99 +++
.../component/StatisticalLineChartCard.js | 122 +++
web/berry/src/views/Dashboard/index.js | 218 ++++++
web/berry/src/views/Error/index.js | 47 ++
web/berry/src/views/Home/baseIndex.js | 42 +
web/berry/src/views/Home/index.js | 72 ++
.../src/views/Log/component/TableHead.js | 27 +
web/berry/src/views/Log/component/TableRow.js | 69 ++
.../src/views/Log/component/TableToolBar.js | 239 ++++++
web/berry/src/views/Log/index.js | 157 ++++
web/berry/src/views/Log/type/LogType.js | 9 +
.../src/views/Profile/component/EmailModal.js | 201 +++++
web/berry/src/views/Profile/index.js | 293 +++++++
.../views/Redemption/component/EditModal.js | 190 +++++
.../views/Redemption/component/TableHead.js | 19 +
.../views/Redemption/component/TableRow.js | 147 ++++
web/berry/src/views/Redemption/index.js | 203 +++++
.../Setting/component/OperationSetting.js | 532 +++++++++++++
.../views/Setting/component/OtherSetting.js | 286 +++++++
.../views/Setting/component/SystemSetting.js | 611 +++++++++++++++
web/berry/src/views/Setting/index.js | 90 +++
.../src/views/Token/component/EditModal.js | 275 +++++++
.../src/views/Token/component/TableHead.js | 19 +
.../src/views/Token/component/TableRow.js | 270 +++++++
web/berry/src/views/Token/index.js | 215 ++++++
.../src/views/Topup/component/InviteCard.js | 80 ++
.../src/views/Topup/component/TopupCard.js | 122 +++
web/berry/src/views/Topup/index.js | 26 +
.../src/views/User/component/EditModal.js | 288 +++++++
.../src/views/User/component/TableHead.js | 20 +
.../src/views/User/component/TableRow.js | 193 +++++
web/berry/src/views/User/index.js | 205 +++++
157 files changed, 13979 insertions(+), 5 deletions(-)
create mode 100644 web/berry/.gitignore
create mode 100644 web/berry/README.md
create mode 100644 web/berry/jsconfig.json
create mode 100644 web/berry/package.json
create mode 100644 web/berry/public/favicon.ico
create mode 100644 web/berry/public/index.html
create mode 100644 web/berry/src/App.js
create mode 100644 web/berry/src/assets/images/404.svg
create mode 100644 web/berry/src/assets/images/auth/auth-blue-card.svg
create mode 100644 web/berry/src/assets/images/auth/auth-pattern-dark.svg
create mode 100644 web/berry/src/assets/images/auth/auth-pattern.svg
create mode 100644 web/berry/src/assets/images/auth/auth-purple-card.svg
create mode 100644 web/berry/src/assets/images/auth/auth-signup-blue-card.svg
create mode 100644 web/berry/src/assets/images/auth/auth-signup-white-card.svg
create mode 100644 web/berry/src/assets/images/icons/earning.svg
create mode 100644 web/berry/src/assets/images/icons/github.svg
create mode 100644 web/berry/src/assets/images/icons/shape-avatar.svg
create mode 100644 web/berry/src/assets/images/icons/social-google.svg
create mode 100644 web/berry/src/assets/images/icons/wechat.svg
create mode 100644 web/berry/src/assets/images/invite/cover.jpg
create mode 100644 web/berry/src/assets/images/invite/cwok_casual_19.webp
create mode 100644 web/berry/src/assets/images/logo-2.svg
create mode 100644 web/berry/src/assets/images/logo.svg
create mode 100644 web/berry/src/assets/images/users/user-round.svg
create mode 100644 web/berry/src/assets/scss/_themes-vars.module.scss
create mode 100644 web/berry/src/assets/scss/style.scss
create mode 100644 web/berry/src/config.js
create mode 100644 web/berry/src/constants/ChannelConstants.js
create mode 100644 web/berry/src/constants/CommonConstants.js
create mode 100644 web/berry/src/constants/SnackbarConstants.js
create mode 100644 web/berry/src/constants/index.js
create mode 100644 web/berry/src/contexts/StatusContext.js
create mode 100644 web/berry/src/contexts/UserContext.js
create mode 100644 web/berry/src/hooks/useAuth.js
create mode 100644 web/berry/src/hooks/useLogin.js
create mode 100644 web/berry/src/hooks/useRegister.js
create mode 100644 web/berry/src/hooks/useScriptRef.js
create mode 100644 web/berry/src/index.js
create mode 100644 web/berry/src/layout/MainLayout/Header/ProfileSection/index.js
create mode 100644 web/berry/src/layout/MainLayout/Header/index.js
create mode 100644 web/berry/src/layout/MainLayout/LogoSection/index.js
create mode 100644 web/berry/src/layout/MainLayout/Sidebar/MenuCard/index.js
create mode 100644 web/berry/src/layout/MainLayout/Sidebar/MenuList/NavCollapse/index.js
create mode 100644 web/berry/src/layout/MainLayout/Sidebar/MenuList/NavGroup/index.js
create mode 100644 web/berry/src/layout/MainLayout/Sidebar/MenuList/NavItem/index.js
create mode 100644 web/berry/src/layout/MainLayout/Sidebar/MenuList/index.js
create mode 100644 web/berry/src/layout/MainLayout/Sidebar/index.js
create mode 100644 web/berry/src/layout/MainLayout/index.js
create mode 100644 web/berry/src/layout/MinimalLayout/Header/index.js
create mode 100644 web/berry/src/layout/MinimalLayout/index.js
create mode 100644 web/berry/src/layout/NavMotion.js
create mode 100644 web/berry/src/layout/NavigationScroll.js
create mode 100644 web/berry/src/menu-items/index.js
create mode 100644 web/berry/src/menu-items/panel.js
create mode 100644 web/berry/src/routes/MainRoutes.js
create mode 100644 web/berry/src/routes/OtherRoutes.js
create mode 100644 web/berry/src/routes/index.js
create mode 100644 web/berry/src/serviceWorker.js
create mode 100644 web/berry/src/store/accountReducer.js
create mode 100644 web/berry/src/store/actions.js
create mode 100644 web/berry/src/store/constant.js
create mode 100644 web/berry/src/store/customizationReducer.js
create mode 100644 web/berry/src/store/index.js
create mode 100644 web/berry/src/store/reducer.js
create mode 100644 web/berry/src/store/siteInfoReducer.js
create mode 100644 web/berry/src/themes/compStyleOverride.js
create mode 100644 web/berry/src/themes/index.js
create mode 100644 web/berry/src/themes/palette.js
create mode 100644 web/berry/src/themes/typography.js
create mode 100644 web/berry/src/ui-component/AdminContainer.js
create mode 100644 web/berry/src/ui-component/Footer.js
create mode 100644 web/berry/src/ui-component/Label.js
create mode 100644 web/berry/src/ui-component/Loadable.js
create mode 100644 web/berry/src/ui-component/Loader.js
create mode 100644 web/berry/src/ui-component/Logo.js
create mode 100644 web/berry/src/ui-component/SvgColor.js
create mode 100644 web/berry/src/ui-component/Switch.js
create mode 100644 web/berry/src/ui-component/TableToolBar.js
create mode 100644 web/berry/src/ui-component/cards/CardSecondaryAction.js
create mode 100644 web/berry/src/ui-component/cards/MainCard.js
create mode 100644 web/berry/src/ui-component/cards/Skeleton/EarningCard.js
create mode 100644 web/berry/src/ui-component/cards/Skeleton/ImagePlaceholder.js
create mode 100644 web/berry/src/ui-component/cards/Skeleton/PopularCard.js
create mode 100644 web/berry/src/ui-component/cards/Skeleton/ProductPlaceholder.js
create mode 100644 web/berry/src/ui-component/cards/Skeleton/TotalGrowthBarChart.js
create mode 100644 web/berry/src/ui-component/cards/Skeleton/TotalIncomeCard.js
create mode 100644 web/berry/src/ui-component/cards/SubCard.js
create mode 100644 web/berry/src/ui-component/cards/UserCard.js
create mode 100644 web/berry/src/ui-component/extended/AnimateButton.js
create mode 100644 web/berry/src/ui-component/extended/Avatar.js
create mode 100644 web/berry/src/ui-component/extended/Breadcrumbs.js
create mode 100644 web/berry/src/ui-component/extended/Transitions.js
create mode 100644 web/berry/src/utils/api.js
create mode 100644 web/berry/src/utils/chart.js
create mode 100644 web/berry/src/utils/common.js
create mode 100644 web/berry/src/utils/password-strength.js
create mode 100644 web/berry/src/utils/route-guard/AuthGuard.js
create mode 100644 web/berry/src/views/About/index.js
create mode 100644 web/berry/src/views/Authentication/Auth/ForgetPassword.js
create mode 100644 web/berry/src/views/Authentication/Auth/GitHubOAuth.js
create mode 100644 web/berry/src/views/Authentication/Auth/Login.js
create mode 100644 web/berry/src/views/Authentication/Auth/Register.js
create mode 100644 web/berry/src/views/Authentication/Auth/ResetPassword.js
create mode 100644 web/berry/src/views/Authentication/AuthCardWrapper.js
create mode 100644 web/berry/src/views/Authentication/AuthForms/AuthLogin.js
create mode 100644 web/berry/src/views/Authentication/AuthForms/AuthRegister.js
create mode 100644 web/berry/src/views/Authentication/AuthForms/ForgetPasswordForm.js
create mode 100644 web/berry/src/views/Authentication/AuthForms/ResetPasswordForm.js
create mode 100644 web/berry/src/views/Authentication/AuthForms/WechatModal.js
create mode 100644 web/berry/src/views/Authentication/AuthWrapper.js
create mode 100644 web/berry/src/views/Channel/component/EditModal.js
create mode 100644 web/berry/src/views/Channel/component/GroupLabel.js
create mode 100644 web/berry/src/views/Channel/component/NameLabel.js
create mode 100644 web/berry/src/views/Channel/component/ResponseTimeLabel.js
create mode 100644 web/berry/src/views/Channel/component/TableHead.js
create mode 100644 web/berry/src/views/Channel/component/TableRow.js
create mode 100644 web/berry/src/views/Channel/index.js
create mode 100644 web/berry/src/views/Channel/type/Config.js
create mode 100644 web/berry/src/views/Dashboard/component/StatisticalBarChart.js
create mode 100644 web/berry/src/views/Dashboard/component/StatisticalCard.js
create mode 100644 web/berry/src/views/Dashboard/component/StatisticalLineChartCard.js
create mode 100644 web/berry/src/views/Dashboard/index.js
create mode 100644 web/berry/src/views/Error/index.js
create mode 100644 web/berry/src/views/Home/baseIndex.js
create mode 100644 web/berry/src/views/Home/index.js
create mode 100644 web/berry/src/views/Log/component/TableHead.js
create mode 100644 web/berry/src/views/Log/component/TableRow.js
create mode 100644 web/berry/src/views/Log/component/TableToolBar.js
create mode 100644 web/berry/src/views/Log/index.js
create mode 100644 web/berry/src/views/Log/type/LogType.js
create mode 100644 web/berry/src/views/Profile/component/EmailModal.js
create mode 100644 web/berry/src/views/Profile/index.js
create mode 100644 web/berry/src/views/Redemption/component/EditModal.js
create mode 100644 web/berry/src/views/Redemption/component/TableHead.js
create mode 100644 web/berry/src/views/Redemption/component/TableRow.js
create mode 100644 web/berry/src/views/Redemption/index.js
create mode 100644 web/berry/src/views/Setting/component/OperationSetting.js
create mode 100644 web/berry/src/views/Setting/component/OtherSetting.js
create mode 100644 web/berry/src/views/Setting/component/SystemSetting.js
create mode 100644 web/berry/src/views/Setting/index.js
create mode 100644 web/berry/src/views/Token/component/EditModal.js
create mode 100644 web/berry/src/views/Token/component/TableHead.js
create mode 100644 web/berry/src/views/Token/component/TableRow.js
create mode 100644 web/berry/src/views/Token/index.js
create mode 100644 web/berry/src/views/Topup/component/InviteCard.js
create mode 100644 web/berry/src/views/Topup/component/TopupCard.js
create mode 100644 web/berry/src/views/Topup/index.js
create mode 100644 web/berry/src/views/User/component/EditModal.js
create mode 100644 web/berry/src/views/User/component/TableHead.js
create mode 100644 web/berry/src/views/User/component/TableRow.js
create mode 100644 web/berry/src/views/User/index.js
diff --git a/controller/model.go b/controller/model.go
index 6cb530db..c12ccf34 100644
--- a/controller/model.go
+++ b/controller/model.go
@@ -436,7 +436,7 @@ func init() {
Id: "PaLM-2",
Object: "model",
Created: 1677649963,
- OwnedBy: "google",
+ OwnedBy: "google palm",
Permission: permission,
Root: "PaLM-2",
Parent: nil,
@@ -445,7 +445,7 @@ func init() {
Id: "gemini-pro",
Object: "model",
Created: 1677649963,
- OwnedBy: "google",
+ OwnedBy: "google gemini",
Permission: permission,
Root: "gemini-pro",
Parent: nil,
@@ -454,7 +454,7 @@ func init() {
Id: "gemini-pro-vision",
Object: "model",
Created: 1677649963,
- OwnedBy: "google",
+ OwnedBy: "google gemini",
Permission: permission,
Root: "gemini-pro-vision",
Parent: nil,
diff --git a/controller/user.go b/controller/user.go
index 8fd10b82..174300ed 100644
--- a/controller/user.go
+++ b/controller/user.go
@@ -7,6 +7,7 @@ import (
"one-api/common"
"one-api/model"
"strconv"
+ "time"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
@@ -248,6 +249,29 @@ func GetUser(c *gin.Context) {
return
}
+func GetUserDashboard(c *gin.Context) {
+ id := c.GetInt("id")
+ now := time.Now()
+ startOfDay := now.Truncate(24*time.Hour).AddDate(0, 0, -6).Unix()
+ endOfDay := now.Truncate(24 * time.Hour).Add(24*time.Hour - time.Second).Unix()
+
+ dashboards, err := model.SearchLogsByDayAndModel(id, int(startOfDay), int(endOfDay))
+ if err != nil {
+ c.JSON(http.StatusOK, gin.H{
+ "success": false,
+ "message": "无法获取统计信息",
+ "data": nil,
+ })
+ return
+ }
+ c.JSON(http.StatusOK, gin.H{
+ "success": true,
+ "message": "",
+ "data": dashboards,
+ })
+ return
+}
+
func GenerateAccessToken(c *gin.Context) {
id := c.GetInt("id")
user, err := model.GetUserById(id, true)
diff --git a/model/log.go b/model/log.go
index 3d3ffae3..aa4be60d 100644
--- a/model/log.go
+++ b/model/log.go
@@ -3,8 +3,9 @@ package model
import (
"context"
"fmt"
- "gorm.io/gorm"
"one-api/common"
+
+ "gorm.io/gorm"
)
type Log struct {
@@ -182,3 +183,42 @@ func DeleteOldLog(targetTimestamp int64) (int64, error) {
result := DB.Where("created_at < ?", targetTimestamp).Delete(&Log{})
return result.RowsAffected, result.Error
}
+
+type LogStatistic struct {
+ Day string `gorm:"column:day"`
+ ModelName string `gorm:"column:model_name"`
+ RequestCount int `gorm:"column:request_count"`
+ Quota int `gorm:"column:quota"`
+ PromptTokens int `gorm:"column:prompt_tokens"`
+ CompletionTokens int `gorm:"column:completion_tokens"`
+}
+
+func SearchLogsByDayAndModel(userId, start, end int) (LogStatistics []*LogStatistic, err error) {
+ groupSelect := "DATE_FORMAT(FROM_UNIXTIME(created_at), '%Y-%m-%d') as day"
+
+ if common.UsingPostgreSQL {
+ groupSelect = "TO_CHAR(date_trunc('day', to_timestamp(created_at)), 'YYYY-MM-DD') as day"
+ }
+
+ if common.UsingSQLite {
+ groupSelect = "strftime('%Y-%m-%d', datetime(created_at, 'unixepoch')) as day"
+ }
+
+ err = DB.Raw(`
+ SELECT `+groupSelect+`,
+ model_name, count(1) as request_count,
+ sum(quota) as quota,
+ sum(prompt_tokens) as prompt_tokens,
+ sum(completion_tokens) as completion_tokens
+ FROM logs
+ WHERE type=2
+ AND userId= ?
+ AND created_at BETWEEN ? AND ?
+ GROUP BY day, model_name
+ ORDER BY day, model_name
+ `, userId, start, end).Scan(&LogStatistics).Error
+
+ fmt.Println(userId, start, end)
+
+ return LogStatistics, err
+}
diff --git a/router/api-router.go b/router/api-router.go
index da3f9e61..162675ce 100644
--- a/router/api-router.go
+++ b/router/api-router.go
@@ -35,6 +35,7 @@ func SetApiRouter(router *gin.Engine) {
selfRoute := userRoute.Group("/")
selfRoute.Use(middleware.UserAuth())
{
+ selfRoute.GET("/dashboard", controller.GetUserDashboard)
selfRoute.GET("/self", controller.GetSelf)
selfRoute.PUT("/self", controller.UpdateSelf)
selfRoute.DELETE("/self", controller.DeleteSelf)
diff --git a/web/README.md b/web/README.md
index ca73b298..8e6827c9 100644
--- a/web/README.md
+++ b/web/README.md
@@ -1,17 +1,36 @@
# One API 的前端界面
+
> 每个文件夹代表一个主题,欢迎提交你的主题
## 提交新的主题
+
> 欢迎在页面底部保留你和 One API 的版权信息以及指向链接
+
1. 在 `web` 文件夹下新建一个文件夹,文件夹名为主题名。
2. 把你的主题文件放到这个文件夹下。
3. 修改 `package.json` 文件,把 `build` 命令改为:`"build": "react-scripts build && mv -f build ../build/default"`,其中 `default` 为你的主题名。
## 主题列表
+
### 主题:default
+
默认主题,由 [JustSong](https://github.com/songquanpeng) 开发。
预览:
|||
|:---:|:---:|
+### 主题:berry
+
+由 [MartialBE](https://github.com/MartialBE) 开发。
+
+预览:
+|||
+|:---:|:---:|
+|||
+|||
+|||
+
+#### 开发说明
+
+请查看 [web/berry/README.md](https://github.com/songquanpeng/one-api/tree/main/web/berry/README.md)
diff --git a/web/THEMES b/web/THEMES
index 331d858c..b6597eeb 100644
--- a/web/THEMES
+++ b/web/THEMES
@@ -1 +1,2 @@
-default
\ No newline at end of file
+default
+berry
\ No newline at end of file
diff --git a/web/berry/.gitignore b/web/berry/.gitignore
new file mode 100644
index 00000000..2b5bba76
--- /dev/null
+++ b/web/berry/.gitignore
@@ -0,0 +1,26 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# production
+/build
+
+# misc
+.DS_Store
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.idea
+package-lock.json
+yarn.lock
\ No newline at end of file
diff --git a/web/berry/README.md b/web/berry/README.md
new file mode 100644
index 00000000..170feedc
--- /dev/null
+++ b/web/berry/README.md
@@ -0,0 +1,61 @@
+# One API 前端界面
+
+这个项目是 One API 的前端界面,它基于 [Berry Free React Admin Template](https://github.com/codedthemes/berry-free-react-admin-template) 进行开发。
+
+## 使用的开源项目
+
+使用了以下开源项目作为我们项目的一部分:
+
+- [Berry Free React Admin Template](https://github.com/codedthemes/berry-free-react-admin-template)
+- [minimal-ui-kit](minimal-ui-kit)
+
+## 开发说明
+
+当添加新的渠道时,需要修改以下地方:
+
+1. `web/berry/src/constants/ChannelConstants.js`
+
+在该文件中的 `CHANNEL_OPTIONS` 添加新的渠道
+
+```js
+export const CHANNEL_OPTIONS = {
+ //key 为渠道ID
+ 1: {
+ key: 1, // 渠道ID
+ text: "OpenAI", // 渠道名称
+ value: 1, // 渠道ID
+ color: "primary", // 渠道列表显示的颜色
+ },
+};
+```
+
+2. `web/berry/src/views/Channel/type/Config.js`
+
+在该文件中的`typeConfig`添加新的渠道配置, 如果无需配置,可以不添加
+
+```js
+const typeConfig = {
+ // key 为渠道ID
+ 3: {
+ inputLabel: {
+ // 输入框名称 配置
+ // 对应的字段名称
+ base_url: "AZURE_OPENAI_ENDPOINT",
+ other: "默认 API 版本",
+ },
+ prompt: {
+ // 输入框提示 配置
+ // 对应的字段名称
+ base_url: "请填写AZURE_OPENAI_ENDPOINT",
+
+ // 注意:通过判断 `other` 是否有值来判断是否需要显示 `other` 输入框, 默认是没有值的
+ other: "请输入默认API版本,例如:2023-06-01-preview",
+ },
+ modelGroup: "openai", // 模型组名称,这个值是给 填入渠道支持模型 按钮使用的。 填入渠道支持模型 按钮会根据这个值来获取模型组,如果填写默认是 openai
+ },
+};
+```
+
+## 许可证
+
+本项目中使用的代码遵循 MIT 许可证。
diff --git a/web/berry/jsconfig.json b/web/berry/jsconfig.json
new file mode 100644
index 00000000..35332c70
--- /dev/null
+++ b/web/berry/jsconfig.json
@@ -0,0 +1,9 @@
+{
+ "compilerOptions": {
+ "target": "esnext",
+ "module": "commonjs",
+ "baseUrl": "src"
+ },
+ "include": ["src/**/*"],
+ "exclude": ["node_modules"]
+}
diff --git a/web/berry/package.json b/web/berry/package.json
new file mode 100644
index 00000000..f428fd9c
--- /dev/null
+++ b/web/berry/package.json
@@ -0,0 +1,84 @@
+{
+ "name": "one_api_web",
+ "version": "1.0.0",
+ "proxy": "http://127.0.0.1:3000",
+ "private": true,
+ "homepage": "",
+ "dependencies": {
+ "@emotion/cache": "^11.9.3",
+ "@emotion/react": "^11.9.3",
+ "@emotion/styled": "^11.9.3",
+ "@mui/icons-material": "^5.8.4",
+ "@mui/lab": "^5.0.0-alpha.88",
+ "@mui/material": "^5.8.6",
+ "@mui/system": "^5.8.6",
+ "@mui/utils": "^5.8.6",
+ "@mui/x-date-pickers": "^6.18.5",
+ "@tabler/icons-react": "^2.44.0",
+ "apexcharts": "^3.35.3",
+ "axios": "^0.27.2",
+ "dayjs": "^1.11.10",
+ "formik": "^2.2.9",
+ "framer-motion": "^6.3.16",
+ "history": "^5.3.0",
+ "marked": "^4.1.1",
+ "material-ui-popup-state": "^4.0.1",
+ "notistack": "^3.0.1",
+ "prop-types": "^15.8.1",
+ "react": "^18.2.0",
+ "react-apexcharts": "^1.4.0",
+ "react-device-detect": "^2.2.2",
+ "react-dom": "^18.2.0",
+ "react-perfect-scrollbar": "^1.5.8",
+ "react-redux": "^8.0.2",
+ "react-router": "6.3.0",
+ "react-router-dom": "6.3.0",
+ "react-scripts": "^5.0.1",
+ "react-turnstile": "^1.1.2",
+ "redux": "^4.2.0",
+ "yup": "^0.32.11"
+ },
+ "scripts": {
+ "start": "react-scripts start",
+ "build": "react-scripts build && mv -f build ../build/berry",
+ "test": "react-scripts test",
+ "eject": "react-scripts eject"
+ },
+ "eslintConfig": {
+ "extends": [
+ "react-app"
+ ]
+ },
+ "babel": {
+ "presets": [
+ "@babel/preset-react"
+ ]
+ },
+ "browserslist": {
+ "production": [
+ "defaults",
+ "not IE 11"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ },
+ "devDependencies": {
+ "@babel/core": "^7.21.4",
+ "@babel/eslint-parser": "^7.21.3",
+ "eslint": "^8.38.0",
+ "eslint-config-prettier": "^8.8.0",
+ "eslint-config-react-app": "^7.0.1",
+ "eslint-plugin-flowtype": "^8.0.3",
+ "eslint-plugin-import": "^2.27.5",
+ "eslint-plugin-jsx-a11y": "^6.7.1",
+ "eslint-plugin-prettier": "^4.2.1",
+ "eslint-plugin-react": "^7.32.2",
+ "eslint-plugin-react-hooks": "^4.6.0",
+ "immutable": "^4.3.0",
+ "prettier": "^2.8.7",
+ "sass": "^1.53.0"
+ }
+}
diff --git a/web/berry/public/favicon.ico b/web/berry/public/favicon.ico
new file mode 100644
index 0000000000000000000000000000000000000000..fbcfb14a5f9236888475dff35f0749f90473377c
GIT binary patch
literal 41270
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
literal 0
HcmV?d00001
diff --git a/web/berry/public/index.html b/web/berry/public/index.html
new file mode 100644
index 00000000..6f232250
--- /dev/null
+++ b/web/berry/public/index.html
@@ -0,0 +1,26 @@
+
+
+
+ One API
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/berry/src/App.js b/web/berry/src/App.js
new file mode 100644
index 00000000..fc54c632
--- /dev/null
+++ b/web/berry/src/App.js
@@ -0,0 +1,43 @@
+import { useSelector } from 'react-redux';
+
+import { ThemeProvider } from '@mui/material/styles';
+import { CssBaseline, StyledEngineProvider } from '@mui/material';
+
+// routing
+import Routes from 'routes';
+
+// defaultTheme
+import themes from 'themes';
+
+// project imports
+import NavigationScroll from 'layout/NavigationScroll';
+
+// auth
+import UserProvider from 'contexts/UserContext';
+import StatusProvider from 'contexts/StatusContext';
+import { SnackbarProvider } from 'notistack';
+
+// ==============================|| APP ||============================== //
+
+const App = () => {
+ const customization = useSelector((state) => state.customization);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default App;
diff --git a/web/berry/src/assets/images/404.svg b/web/berry/src/assets/images/404.svg
new file mode 100644
index 00000000..352a14ad
--- /dev/null
+++ b/web/berry/src/assets/images/404.svg
@@ -0,0 +1,40 @@
+
\ No newline at end of file
diff --git a/web/berry/src/assets/images/auth/auth-blue-card.svg b/web/berry/src/assets/images/auth/auth-blue-card.svg
new file mode 100644
index 00000000..6c9fe3e7
--- /dev/null
+++ b/web/berry/src/assets/images/auth/auth-blue-card.svg
@@ -0,0 +1,65 @@
+
diff --git a/web/berry/src/assets/images/auth/auth-pattern-dark.svg b/web/berry/src/assets/images/auth/auth-pattern-dark.svg
new file mode 100644
index 00000000..aa0e4ab2
--- /dev/null
+++ b/web/berry/src/assets/images/auth/auth-pattern-dark.svg
@@ -0,0 +1,39 @@
+
diff --git a/web/berry/src/assets/images/auth/auth-pattern.svg b/web/berry/src/assets/images/auth/auth-pattern.svg
new file mode 100644
index 00000000..b7ac8e27
--- /dev/null
+++ b/web/berry/src/assets/images/auth/auth-pattern.svg
@@ -0,0 +1,39 @@
+
diff --git a/web/berry/src/assets/images/auth/auth-purple-card.svg b/web/berry/src/assets/images/auth/auth-purple-card.svg
new file mode 100644
index 00000000..c724e0a3
--- /dev/null
+++ b/web/berry/src/assets/images/auth/auth-purple-card.svg
@@ -0,0 +1,69 @@
+
diff --git a/web/berry/src/assets/images/auth/auth-signup-blue-card.svg b/web/berry/src/assets/images/auth/auth-signup-blue-card.svg
new file mode 100644
index 00000000..ebb8e85f
--- /dev/null
+++ b/web/berry/src/assets/images/auth/auth-signup-blue-card.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/berry/src/assets/images/auth/auth-signup-white-card.svg b/web/berry/src/assets/images/auth/auth-signup-white-card.svg
new file mode 100644
index 00000000..56b97e20
--- /dev/null
+++ b/web/berry/src/assets/images/auth/auth-signup-white-card.svg
@@ -0,0 +1,40 @@
+
diff --git a/web/berry/src/assets/images/icons/earning.svg b/web/berry/src/assets/images/icons/earning.svg
new file mode 100644
index 00000000..e877b599
--- /dev/null
+++ b/web/berry/src/assets/images/icons/earning.svg
@@ -0,0 +1,5 @@
+
diff --git a/web/berry/src/assets/images/icons/github.svg b/web/berry/src/assets/images/icons/github.svg
new file mode 100644
index 00000000..e5b1b82a
--- /dev/null
+++ b/web/berry/src/assets/images/icons/github.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/berry/src/assets/images/icons/shape-avatar.svg b/web/berry/src/assets/images/icons/shape-avatar.svg
new file mode 100644
index 00000000..38aac7e2
--- /dev/null
+++ b/web/berry/src/assets/images/icons/shape-avatar.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/berry/src/assets/images/icons/social-google.svg b/web/berry/src/assets/images/icons/social-google.svg
new file mode 100644
index 00000000..2231ce98
--- /dev/null
+++ b/web/berry/src/assets/images/icons/social-google.svg
@@ -0,0 +1,6 @@
+
diff --git a/web/berry/src/assets/images/icons/wechat.svg b/web/berry/src/assets/images/icons/wechat.svg
new file mode 100644
index 00000000..a0b2e36c
--- /dev/null
+++ b/web/berry/src/assets/images/icons/wechat.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/berry/src/assets/images/invite/cover.jpg b/web/berry/src/assets/images/invite/cover.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..93be1a40c9592049c1f4550e9c77e838a05d846a
GIT binary patch
literal 87952
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