mirror of
https://github.com/songquanpeng/one-api.git
synced 2025-11-07 09:13:42 +08:00
Compare commits
14 Commits
v0.2.4-alp
...
v0.2.6-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a0e81fe83 | ||
|
|
976c29ea9f | ||
|
|
926951ee03 | ||
|
|
2cdc718fde | ||
|
|
57cb150177 | ||
|
|
6167e20b34 | ||
|
|
8835d8302e | ||
|
|
224bebe67a | ||
|
|
cf6883778e | ||
|
|
246b981e23 | ||
|
|
2edd52e851 | ||
|
|
e123c66bc7 | ||
|
|
9edc82bde0 | ||
|
|
d84c2f5c70 |
23
README.md
23
README.md
@@ -43,27 +43,28 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用
|
|||||||
## 功能
|
## 功能
|
||||||
1. 支持多种 API 访问渠道,欢迎 PR 或提 issue 添加更多渠道:
|
1. 支持多种 API 访问渠道,欢迎 PR 或提 issue 添加更多渠道:
|
||||||
+ [x] OpenAI 官方通道
|
+ [x] OpenAI 官方通道
|
||||||
|
+ [x] **Azure OpenAI API**
|
||||||
+ [x] [API2D](https://api2d.com/r/197971)
|
+ [x] [API2D](https://api2d.com/r/197971)
|
||||||
+ [x] Azure OpenAI API
|
|
||||||
+ [x] [CloseAI](https://console.openai-asia.com)
|
+ [x] [CloseAI](https://console.openai-asia.com)
|
||||||
+ [x] [OpenAI-SB](https://openai-sb.com)
|
+ [x] [OpenAI-SB](https://openai-sb.com)
|
||||||
+ [x] [OpenAI Max](https://openaimax.com)
|
+ [x] [OpenAI Max](https://openaimax.com)
|
||||||
+ [x] [OhMyGPT](https://www.ohmygpt.com)
|
+ [x] [OhMyGPT](https://www.ohmygpt.com)
|
||||||
+ [x] 自定义渠道:例如使用自行搭建的 OpenAI 代理
|
+ [x] 自定义渠道:例如使用自行搭建的 OpenAI 代理
|
||||||
2. 支持通过负载均衡的方式访问多个渠道。
|
2. 支持通过**负载均衡**的方式访问多个渠道。
|
||||||
3. 支持单个访问渠道设置多个 API Key,利用起来你的多个 API Key。
|
3. 支持 **stream 模式**,可以通过流式传输实现打字机效果。
|
||||||
4. 支持 HTTP SSE,可以通过流式传输实现打字机效果。
|
4. 支持**令牌管理**,设置令牌的过期时间和使用次数。
|
||||||
5. 支持设置令牌的过期时间和使用次数。
|
5. 支持**兑换码管理**,支持批量生成和导出兑换码,可使用兑换码为令牌进行充值。
|
||||||
6. 支持批量生成和导出兑换码,可使用兑换码为令牌进行充值。
|
6. 支持**通道管理**,批量创建通道。
|
||||||
7. 支持为新用户设置初始配额。
|
7. 支持发布公告,设置充值链接,设置新用户初始额度。
|
||||||
8. 支持发布公告,在线修改关于页面,设置充值链接,自定义页脚。
|
8. 支持丰富的**自定义**设置,
|
||||||
|
1. 支持自定义系统名称,logo 以及页脚。
|
||||||
|
2. 支持自定义首页和关于页面,可以选择使用 HTML & Markdown 代码进行自定义,或者使用一个单独的网页通过 iframe 嵌入。
|
||||||
9. 支持通过系统访问令牌访问管理 API。
|
9. 支持通过系统访问令牌访问管理 API。
|
||||||
10. 多种用户登录注册方式:
|
10. 支持用户管理,支持**多种用户登录注册方式**:
|
||||||
+ 邮箱登录注册以及通过邮箱进行密码重置。
|
+ 邮箱登录注册以及通过邮箱进行密码重置。
|
||||||
+ [GitHub 开放授权](https://github.com/settings/applications/new)。
|
+ [GitHub 开放授权](https://github.com/settings/applications/new)。
|
||||||
+ 微信公众号授权(需要额外部署 [WeChat Server](https://github.com/songquanpeng/wechat-server))。
|
+ 微信公众号授权(需要额外部署 [WeChat Server](https://github.com/songquanpeng/wechat-server))。
|
||||||
11. 支持用户管理。
|
11. 未来其他大模型开放 API 后,将第一时间支持,并将其封装成同样的 API 访问方式。
|
||||||
12. 未来其他大模型开放 API 后,将第一时间支持,并将其封装成同样的 API 访问方式。
|
|
||||||
|
|
||||||
## 部署
|
## 部署
|
||||||
### 基于 Docker 进行部署
|
### 基于 Docker 进行部署
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ var Version = "v0.0.0" // this hard coding will be replaced automatic
|
|||||||
var SystemName = "One API"
|
var SystemName = "One API"
|
||||||
var ServerAddress = "http://localhost:3000"
|
var ServerAddress = "http://localhost:3000"
|
||||||
var Footer = ""
|
var Footer = ""
|
||||||
|
var Logo = ""
|
||||||
var TopUpLink = ""
|
var TopUpLink = ""
|
||||||
|
|
||||||
var UsingSQLite = false
|
var UsingSQLite = false
|
||||||
|
|||||||
@@ -2,17 +2,22 @@ package common
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/smtp"
|
"net/smtp"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func SendEmail(subject string, receiver string, content string) error {
|
func SendEmail(subject string, receiver string, content string) error {
|
||||||
|
if SMTPFrom == "" { // for compatibility
|
||||||
|
SMTPFrom = SMTPAccount
|
||||||
|
}
|
||||||
|
encodedSubject := fmt.Sprintf("=?UTF-8?B?%s?=", base64.StdEncoding.EncodeToString([]byte(subject)))
|
||||||
mail := []byte(fmt.Sprintf("To: %s\r\n"+
|
mail := []byte(fmt.Sprintf("To: %s\r\n"+
|
||||||
"From: %s<%s>\r\n"+
|
"From: %s<%s>\r\n"+
|
||||||
"Subject: %s\r\n"+
|
"Subject: %s\r\n"+
|
||||||
"Content-Type: text/html; charset=UTF-8\r\n\r\n%s\r\n",
|
"Content-Type: text/html; charset=UTF-8\r\n\r\n%s\r\n",
|
||||||
receiver, SystemName, SMTPFrom, subject, content))
|
receiver, SystemName, SMTPFrom, encodedSubject, content))
|
||||||
auth := smtp.PlainAuth("", SMTPAccount, SMTPToken, SMTPServer)
|
auth := smtp.PlainAuth("", SMTPAccount, SMTPToken, SMTPServer)
|
||||||
addr := fmt.Sprintf("%s:%d", SMTPServer, SMTPPort)
|
addr := fmt.Sprintf("%s:%d", SMTPServer, SMTPPort)
|
||||||
to := strings.Split(receiver, ";")
|
to := strings.Split(receiver, ";")
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ func GetStatus(c *gin.Context) {
|
|||||||
"github_oauth": common.GitHubOAuthEnabled,
|
"github_oauth": common.GitHubOAuthEnabled,
|
||||||
"github_client_id": common.GitHubClientId,
|
"github_client_id": common.GitHubClientId,
|
||||||
"system_name": common.SystemName,
|
"system_name": common.SystemName,
|
||||||
|
"logo": common.Logo,
|
||||||
"footer_html": common.Footer,
|
"footer_html": common.Footer,
|
||||||
"wechat_qrcode": common.WeChatAccountQRCodeImageURL,
|
"wechat_qrcode": common.WeChatAccountQRCodeImageURL,
|
||||||
"wechat_login": common.WeChatAuthEnabled,
|
"wechat_login": common.WeChatAuthEnabled,
|
||||||
@@ -54,6 +55,17 @@ func GetAbout(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetHomePageContent(c *gin.Context) {
|
||||||
|
common.OptionMapRWMutex.RLock()
|
||||||
|
defer common.OptionMapRWMutex.RUnlock()
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
"data": common.OptionMap["HomePageContent"],
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func SendEmailVerification(c *gin.Context) {
|
func SendEmailVerification(c *gin.Context) {
|
||||||
email := c.Query("email")
|
email := c.Query("email")
|
||||||
if err := common.Validate.Var(email, "required,email"); err != nil {
|
if err := common.Validate.Var(email, "required,email"); err != nil {
|
||||||
|
|||||||
@@ -94,10 +94,12 @@ func relayHelper(c *gin.Context) error {
|
|||||||
if channelType == common.ChannelTypeAzure {
|
if channelType == common.ChannelTypeAzure {
|
||||||
// https://learn.microsoft.com/en-us/azure/cognitive-services/openai/chatgpt-quickstart?pivots=rest-api&tabs=command-line#rest-api
|
// https://learn.microsoft.com/en-us/azure/cognitive-services/openai/chatgpt-quickstart?pivots=rest-api&tabs=command-line#rest-api
|
||||||
query := c.Request.URL.Query()
|
query := c.Request.URL.Query()
|
||||||
if query.Get("api-version") == "" {
|
apiVersion := query.Get("api-version")
|
||||||
apiVersion := c.GetString("api_version")
|
if apiVersion == "" {
|
||||||
requestURL = fmt.Sprintf("%s?api-version=%s", requestURL, apiVersion)
|
apiVersion = c.GetString("api_version")
|
||||||
}
|
}
|
||||||
|
requestURL := strings.Split(requestURL, "?")[0]
|
||||||
|
requestURL = fmt.Sprintf("%s?api-version=%s", requestURL, apiVersion)
|
||||||
baseURL = c.GetString("base_url")
|
baseURL = c.GetString("base_url")
|
||||||
task := strings.TrimPrefix(requestURL, "/v1/")
|
task := strings.TrimPrefix(requestURL, "/v1/")
|
||||||
model_ := textRequest.Model
|
model_ := textRequest.Model
|
||||||
@@ -186,7 +188,7 @@ func relayHelper(c *gin.Context) error {
|
|||||||
data := scanner.Text()
|
data := scanner.Text()
|
||||||
dataChan <- data
|
dataChan <- data
|
||||||
data = data[6:]
|
data = data[6:]
|
||||||
if data != "[DONE]" {
|
if !strings.HasPrefix(data, "[DONE]") {
|
||||||
var streamResponse StreamResponse
|
var streamResponse StreamResponse
|
||||||
err = json.Unmarshal([]byte(data), &streamResponse)
|
err = json.Unmarshal([]byte(data), &streamResponse)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -207,6 +209,9 @@ func relayHelper(c *gin.Context) error {
|
|||||||
c.Stream(func(w io.Writer) bool {
|
c.Stream(func(w io.Writer) bool {
|
||||||
select {
|
select {
|
||||||
case data := <-dataChan:
|
case data := <-dataChan:
|
||||||
|
if strings.HasPrefix(data, "data: [DONE]") {
|
||||||
|
data = data[:12]
|
||||||
|
}
|
||||||
c.Render(-1, common.CustomEvent{Data: data})
|
c.Render(-1, common.CustomEvent{Data: data})
|
||||||
return true
|
return true
|
||||||
case <-stopChan:
|
case <-stopChan:
|
||||||
|
|||||||
@@ -111,14 +111,9 @@ func TokenAuth() func(c *gin.Context) {
|
|||||||
c.Set("id", token.UserId)
|
c.Set("id", token.UserId)
|
||||||
c.Set("token_id", token.Id)
|
c.Set("token_id", token.Id)
|
||||||
requestURL := c.Request.URL.String()
|
requestURL := c.Request.URL.String()
|
||||||
consumeQuota := false
|
consumeQuota := !token.UnlimitedQuota
|
||||||
switch requestURL {
|
if strings.HasPrefix(requestURL, "/models") {
|
||||||
case "/v1/chat/completions":
|
consumeQuota = false
|
||||||
consumeQuota = !token.UnlimitedQuota
|
|
||||||
case "/v1/completions":
|
|
||||||
consumeQuota = !token.UnlimitedQuota
|
|
||||||
case "/v1/edits":
|
|
||||||
consumeQuota = !token.UnlimitedQuota
|
|
||||||
}
|
}
|
||||||
c.Set("consume_quota", consumeQuota)
|
c.Set("consume_quota", consumeQuota)
|
||||||
if len(parts) > 1 {
|
if len(parts) > 1 {
|
||||||
|
|||||||
@@ -39,7 +39,10 @@ func InitOptionMap() {
|
|||||||
common.OptionMap["SMTPToken"] = ""
|
common.OptionMap["SMTPToken"] = ""
|
||||||
common.OptionMap["Notice"] = ""
|
common.OptionMap["Notice"] = ""
|
||||||
common.OptionMap["About"] = ""
|
common.OptionMap["About"] = ""
|
||||||
|
common.OptionMap["HomePageContent"] = ""
|
||||||
common.OptionMap["Footer"] = common.Footer
|
common.OptionMap["Footer"] = common.Footer
|
||||||
|
common.OptionMap["SystemName"] = common.SystemName
|
||||||
|
common.OptionMap["Logo"] = common.Logo
|
||||||
common.OptionMap["ServerAddress"] = ""
|
common.OptionMap["ServerAddress"] = ""
|
||||||
common.OptionMap["GitHubClientId"] = ""
|
common.OptionMap["GitHubClientId"] = ""
|
||||||
common.OptionMap["GitHubClientSecret"] = ""
|
common.OptionMap["GitHubClientSecret"] = ""
|
||||||
@@ -59,9 +62,6 @@ func InitOptionMap() {
|
|||||||
common.SysError("Failed to update option map: " + err.Error())
|
common.SysError("Failed to update option map: " + err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if common.SMTPFrom == "" { // for compatibility
|
|
||||||
common.SMTPFrom = common.SMTPAccount
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func UpdateOption(key string, value string) error {
|
func UpdateOption(key string, value string) error {
|
||||||
@@ -136,6 +136,10 @@ func updateOptionMap(key string, value string) (err error) {
|
|||||||
common.GitHubClientSecret = value
|
common.GitHubClientSecret = value
|
||||||
case "Footer":
|
case "Footer":
|
||||||
common.Footer = value
|
common.Footer = value
|
||||||
|
case "SystemName":
|
||||||
|
common.SystemName = value
|
||||||
|
case "Logo":
|
||||||
|
common.Logo = value
|
||||||
case "WeChatServerAddress":
|
case "WeChatServerAddress":
|
||||||
common.WeChatServerAddress = value
|
common.WeChatServerAddress = value
|
||||||
case "WeChatServerToken":
|
case "WeChatServerToken":
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ func SetApiRouter(router *gin.Engine) {
|
|||||||
apiRouter.GET("/status", controller.GetStatus)
|
apiRouter.GET("/status", controller.GetStatus)
|
||||||
apiRouter.GET("/notice", controller.GetNotice)
|
apiRouter.GET("/notice", controller.GetNotice)
|
||||||
apiRouter.GET("/about", controller.GetAbout)
|
apiRouter.GET("/about", controller.GetAbout)
|
||||||
|
apiRouter.GET("/home_page_content", controller.GetHomePageContent)
|
||||||
apiRouter.GET("/verification", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendEmailVerification)
|
apiRouter.GET("/verification", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendEmailVerification)
|
||||||
apiRouter.GET("/reset_password", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendPasswordResetEmail)
|
apiRouter.GET("/reset_password", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendPasswordResetEmail)
|
||||||
apiRouter.POST("/user/reset", middleware.CriticalRateLimit(), controller.ResetPassword)
|
apiRouter.POST("/user/reset", middleware.CriticalRateLimit(), controller.ResetPassword)
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ function App() {
|
|||||||
if (success) {
|
if (success) {
|
||||||
localStorage.setItem('status', JSON.stringify(data));
|
localStorage.setItem('status', JSON.stringify(data));
|
||||||
statusDispatch({ type: 'set', payload: data });
|
statusDispatch({ type: 'set', payload: data });
|
||||||
|
localStorage.setItem('system_name', data.system_name);
|
||||||
|
localStorage.setItem('logo', data.logo);
|
||||||
localStorage.setItem('footer_html', data.footer_html);
|
localStorage.setItem('footer_html', data.footer_html);
|
||||||
if (
|
if (
|
||||||
data.version !== process.env.REACT_APP_VERSION &&
|
data.version !== process.env.REACT_APP_VERSION &&
|
||||||
|
|||||||
@@ -1,40 +1,37 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { Container, Segment } from 'semantic-ui-react';
|
import { Container, Segment } from 'semantic-ui-react';
|
||||||
|
import { getFooterHTML, getSystemName } from '../helpers';
|
||||||
|
|
||||||
const Footer = () => {
|
const Footer = () => {
|
||||||
const [Footer, setFooter] = useState('');
|
const systemName = getSystemName();
|
||||||
useEffect(() => {
|
const footer = getFooterHTML();
|
||||||
let savedFooter = localStorage.getItem('footer_html');
|
|
||||||
if (!savedFooter) savedFooter = '';
|
|
||||||
setFooter(savedFooter);
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Segment vertical>
|
<Segment vertical>
|
||||||
<Container textAlign="center">
|
<Container textAlign='center'>
|
||||||
{Footer === '' ? (
|
{footer ? (
|
||||||
<div className="custom-footer">
|
<div
|
||||||
|
className='custom-footer'
|
||||||
|
dangerouslySetInnerHTML={{ __html: footer }}
|
||||||
|
></div>
|
||||||
|
) : (
|
||||||
|
<div className='custom-footer'>
|
||||||
<a
|
<a
|
||||||
href="https://github.com/songquanpeng/one-api"
|
href='https://github.com/songquanpeng/one-api'
|
||||||
target="_blank"
|
target='_blank'
|
||||||
>
|
>
|
||||||
One API {process.env.REACT_APP_VERSION}{' '}
|
{systemName} {process.env.REACT_APP_VERSION}{' '}
|
||||||
</a>
|
</a>
|
||||||
由{' '}
|
由{' '}
|
||||||
<a href="https://github.com/songquanpeng" target="_blank">
|
<a href='https://github.com/songquanpeng' target='_blank'>
|
||||||
JustSong
|
JustSong
|
||||||
</a>{' '}
|
</a>{' '}
|
||||||
构建,源代码遵循{' '}
|
构建,源代码遵循{' '}
|
||||||
<a href="https://opensource.org/licenses/mit-license.php">
|
<a href='https://opensource.org/licenses/mit-license.php'>
|
||||||
MIT 协议
|
MIT 协议
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className="custom-footer"
|
|
||||||
dangerouslySetInnerHTML={{ __html: Footer }}
|
|
||||||
></div>
|
|
||||||
)}
|
)}
|
||||||
</Container>
|
</Container>
|
||||||
</Segment>
|
</Segment>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Link, useNavigate } from 'react-router-dom';
|
|||||||
import { UserContext } from '../context/User';
|
import { UserContext } from '../context/User';
|
||||||
|
|
||||||
import { Button, Container, Dropdown, Icon, Menu, Segment } from 'semantic-ui-react';
|
import { Button, Container, Dropdown, Icon, Menu, Segment } from 'semantic-ui-react';
|
||||||
import { API, isAdmin, isMobile, showSuccess } from '../helpers';
|
import { API, getLogo, getSystemName, isAdmin, isMobile, showSuccess } from '../helpers';
|
||||||
import '../index.css';
|
import '../index.css';
|
||||||
|
|
||||||
// Header Buttons
|
// Header Buttons
|
||||||
@@ -53,6 +53,8 @@ const Header = () => {
|
|||||||
let navigate = useNavigate();
|
let navigate = useNavigate();
|
||||||
|
|
||||||
const [showSidebar, setShowSidebar] = useState(false);
|
const [showSidebar, setShowSidebar] = useState(false);
|
||||||
|
const systemName = getSystemName();
|
||||||
|
const logo = getLogo();
|
||||||
|
|
||||||
async function logout() {
|
async function logout() {
|
||||||
setShowSidebar(false);
|
setShowSidebar(false);
|
||||||
@@ -111,12 +113,12 @@ const Header = () => {
|
|||||||
<Container>
|
<Container>
|
||||||
<Menu.Item as={Link} to='/'>
|
<Menu.Item as={Link} to='/'>
|
||||||
<img
|
<img
|
||||||
src='/logo.png'
|
src={logo}
|
||||||
alt='logo'
|
alt='logo'
|
||||||
style={{ marginRight: '0.75em' }}
|
style={{ marginRight: '0.75em' }}
|
||||||
/>
|
/>
|
||||||
<div style={{ fontSize: '20px' }}>
|
<div style={{ fontSize: '20px' }}>
|
||||||
<b>One API</b>
|
<b>{systemName}</b>
|
||||||
</div>
|
</div>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Menu position='right'>
|
<Menu.Menu position='right'>
|
||||||
@@ -168,9 +170,9 @@ const Header = () => {
|
|||||||
<Menu borderless style={{ borderTop: 'none' }}>
|
<Menu borderless style={{ borderTop: 'none' }}>
|
||||||
<Container>
|
<Container>
|
||||||
<Menu.Item as={Link} to='/' className={'hide-on-mobile'}>
|
<Menu.Item as={Link} to='/' className={'hide-on-mobile'}>
|
||||||
<img src='/logo.png' alt='logo' style={{ marginRight: '0.75em' }} />
|
<img src={logo} alt='logo' style={{ marginRight: '0.75em' }} />
|
||||||
<div style={{ fontSize: '20px' }}>
|
<div style={{ fontSize: '20px' }}>
|
||||||
<b>One API</b>
|
<b>{systemName}</b>
|
||||||
</div>
|
</div>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
{renderButtons(false)}
|
{renderButtons(false)}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
} from 'semantic-ui-react';
|
} from 'semantic-ui-react';
|
||||||
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
import { UserContext } from '../context/User';
|
import { UserContext } from '../context/User';
|
||||||
import { API, showError, showSuccess } from '../helpers';
|
import { API, getLogo, showError, showSuccess } from '../helpers';
|
||||||
|
|
||||||
const LoginForm = () => {
|
const LoginForm = () => {
|
||||||
const [inputs, setInputs] = useState({
|
const [inputs, setInputs] = useState({
|
||||||
@@ -27,6 +27,7 @@ const LoginForm = () => {
|
|||||||
let navigate = useNavigate();
|
let navigate = useNavigate();
|
||||||
|
|
||||||
const [status, setStatus] = useState({});
|
const [status, setStatus] = useState({});
|
||||||
|
const logo = getLogo();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (searchParams.get("expired")) {
|
if (searchParams.get("expired")) {
|
||||||
@@ -95,7 +96,7 @@ const LoginForm = () => {
|
|||||||
<Grid textAlign="center" style={{ marginTop: '48px' }}>
|
<Grid textAlign="center" style={{ marginTop: '48px' }}>
|
||||||
<Grid.Column style={{ maxWidth: 450 }}>
|
<Grid.Column style={{ maxWidth: 450 }}>
|
||||||
<Header as="h2" color="" textAlign="center">
|
<Header as="h2" color="" textAlign="center">
|
||||||
<Image src="/logo.png" /> 用户登录
|
<Image src={logo} /> 用户登录
|
||||||
</Header>
|
</Header>
|
||||||
<Form size="large">
|
<Form size="large">
|
||||||
<Segment>
|
<Segment>
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ const OtherSetting = () => {
|
|||||||
Footer: '',
|
Footer: '',
|
||||||
Notice: '',
|
Notice: '',
|
||||||
About: '',
|
About: '',
|
||||||
|
SystemName: '',
|
||||||
|
Logo: '',
|
||||||
|
HomePageContent: '',
|
||||||
});
|
});
|
||||||
let originInputs = {};
|
let originInputs = {};
|
||||||
let [loading, setLoading] = useState(false);
|
let [loading, setLoading] = useState(false);
|
||||||
@@ -65,10 +68,22 @@ const OtherSetting = () => {
|
|||||||
await updateOption('Footer', inputs.Footer);
|
await updateOption('Footer', inputs.Footer);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const submitSystemName = async () => {
|
||||||
|
await updateOption('SystemName', inputs.SystemName);
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitLogo = async () => {
|
||||||
|
await updateOption('Logo', inputs.Logo);
|
||||||
|
};
|
||||||
|
|
||||||
const submitAbout = async () => {
|
const submitAbout = async () => {
|
||||||
await updateOption('About', inputs.About);
|
await updateOption('About', inputs.About);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const submitOption = async (key) => {
|
||||||
|
await updateOption(key, inputs[key]);
|
||||||
|
};
|
||||||
|
|
||||||
const openGitHubRelease = () => {
|
const openGitHubRelease = () => {
|
||||||
window.location =
|
window.location =
|
||||||
'https://github.com/songquanpeng/one-api/releases/latest';
|
'https://github.com/songquanpeng/one-api/releases/latest';
|
||||||
@@ -109,10 +124,42 @@ const OtherSetting = () => {
|
|||||||
<Form.Button onClick={submitNotice}>保存公告</Form.Button>
|
<Form.Button onClick={submitNotice}>保存公告</Form.Button>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Header as='h3'>个性化设置</Header>
|
<Header as='h3'>个性化设置</Header>
|
||||||
|
<Form.Group widths='equal'>
|
||||||
|
<Form.Input
|
||||||
|
label='系统名称'
|
||||||
|
placeholder='在此输入系统名称'
|
||||||
|
value={inputs.SystemName}
|
||||||
|
name='SystemName'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Button onClick={submitSystemName}>设置系统名称</Form.Button>
|
||||||
|
<Form.Group widths='equal'>
|
||||||
|
<Form.Input
|
||||||
|
label='Logo 图片地址'
|
||||||
|
placeholder='在此输入 Logo 图片地址'
|
||||||
|
value={inputs.Logo}
|
||||||
|
name='Logo'
|
||||||
|
type='url'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Button onClick={submitLogo}>设置 Logo</Form.Button>
|
||||||
|
<Form.Group widths='equal'>
|
||||||
|
<Form.TextArea
|
||||||
|
label='首页内容'
|
||||||
|
placeholder='在此输入首页内容,支持 Markdown & HTML 代码,设置后首页的状态信息将不再显示。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为首页。'
|
||||||
|
value={inputs.HomePageContent}
|
||||||
|
name='HomePageContent'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
style={{ minHeight: 150, fontFamily: 'JetBrains Mono, Consolas' }}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Button onClick={()=>submitOption('HomePageContent')}>保存首页内容</Form.Button>
|
||||||
<Form.Group widths='equal'>
|
<Form.Group widths='equal'>
|
||||||
<Form.TextArea
|
<Form.TextArea
|
||||||
label='关于'
|
label='关于'
|
||||||
placeholder='在此输入新的关于内容,支持 Markdown & HTML 代码'
|
placeholder='在此输入新的关于内容,支持 Markdown & HTML 代码。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为关于页面。'
|
||||||
value={inputs.About}
|
value={inputs.About}
|
||||||
name='About'
|
name='About'
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
Segment,
|
Segment,
|
||||||
} from 'semantic-ui-react';
|
} from 'semantic-ui-react';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import { API, showError, showInfo, showSuccess } from '../helpers';
|
import { API, getLogo, showError, showInfo, showSuccess } from '../helpers';
|
||||||
import Turnstile from 'react-turnstile';
|
import Turnstile from 'react-turnstile';
|
||||||
|
|
||||||
const RegisterForm = () => {
|
const RegisterForm = () => {
|
||||||
@@ -26,6 +26,7 @@ const RegisterForm = () => {
|
|||||||
const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
|
const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
|
||||||
const [turnstileToken, setTurnstileToken] = useState('');
|
const [turnstileToken, setTurnstileToken] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const logo = getLogo();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let status = localStorage.getItem('status');
|
let status = localStorage.getItem('status');
|
||||||
@@ -100,7 +101,7 @@ const RegisterForm = () => {
|
|||||||
<Grid textAlign='center' style={{ marginTop: '48px' }}>
|
<Grid textAlign='center' style={{ marginTop: '48px' }}>
|
||||||
<Grid.Column style={{ maxWidth: 450 }}>
|
<Grid.Column style={{ maxWidth: 450 }}>
|
||||||
<Header as='h2' color='' textAlign='center'>
|
<Header as='h2' color='' textAlign='center'>
|
||||||
<Image src='/logo.png' /> 新用户注册
|
<Image src={logo} /> 新用户注册
|
||||||
</Header>
|
</Header>
|
||||||
<Form size='large'>
|
<Form size='large'>
|
||||||
<Segment>
|
<Segment>
|
||||||
|
|||||||
@@ -15,6 +15,22 @@ export function isRoot() {
|
|||||||
return user.role >= 100;
|
return user.role >= 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getSystemName() {
|
||||||
|
let system_name = localStorage.getItem('system_name');
|
||||||
|
if (!system_name) return 'One API';
|
||||||
|
return system_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLogo() {
|
||||||
|
let logo = localStorage.getItem('logo');
|
||||||
|
if (!logo) return '/logo.png';
|
||||||
|
return logo
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFooterHTML() {
|
||||||
|
return localStorage.getItem('footer_html');
|
||||||
|
}
|
||||||
|
|
||||||
export async function copy(text) {
|
export async function copy(text) {
|
||||||
let okay = true;
|
let okay = true;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ body {
|
|||||||
font-family: Lato, 'Helvetica Neue', Arial, Helvetica, "Microsoft YaHei", sans-serif;
|
font-family: Lato, 'Helvetica Neue', Arial, Helvetica, "Microsoft YaHei", sans-serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
|
|||||||
@@ -5,18 +5,24 @@ import { marked } from 'marked';
|
|||||||
|
|
||||||
const About = () => {
|
const About = () => {
|
||||||
const [about, setAbout] = useState('');
|
const [about, setAbout] = useState('');
|
||||||
|
const [aboutLoaded, setAboutLoaded] = useState(false);
|
||||||
|
|
||||||
const displayAbout = async () => {
|
const displayAbout = async () => {
|
||||||
|
setAbout(localStorage.getItem('about') || '');
|
||||||
const res = await API.get('/api/about');
|
const res = await API.get('/api/about');
|
||||||
const { success, message, data } = res.data;
|
const { success, message, data } = res.data;
|
||||||
if (success) {
|
if (success) {
|
||||||
let HTMLAbout = marked.parse(data);
|
let aboutContent = data;
|
||||||
localStorage.setItem('about', HTMLAbout);
|
if (!data.startsWith('https://')) {
|
||||||
setAbout(HTMLAbout);
|
aboutContent = marked.parse(data);
|
||||||
|
}
|
||||||
|
setAbout(aboutContent);
|
||||||
|
localStorage.setItem('about', aboutContent);
|
||||||
} else {
|
} else {
|
||||||
showError(message);
|
showError(message);
|
||||||
setAbout('加载关于内容失败...');
|
setAbout('加载关于内容失败...');
|
||||||
}
|
}
|
||||||
|
setAboutLoaded(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -25,20 +31,27 @@ const About = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Segment>
|
{
|
||||||
{
|
aboutLoaded && about === '' ? <>
|
||||||
about === '' ? <>
|
<Segment>
|
||||||
<Header as='h3'>关于</Header>
|
<Header as='h3'>关于</Header>
|
||||||
<p>可在设置页面设置关于内容,支持 HTML & Markdown</p>
|
<p>可在设置页面设置关于内容,支持 HTML & Markdown</p>
|
||||||
项目仓库地址:
|
项目仓库地址:
|
||||||
<a href="https://github.com/songquanpeng/one-api">
|
<a href='https://github.com/songquanpeng/one-api'>
|
||||||
https://github.com/songquanpeng/one-api
|
https://github.com/songquanpeng/one-api
|
||||||
</a>
|
</a>
|
||||||
</> : <>
|
</Segment>
|
||||||
<div dangerouslySetInnerHTML={{ __html: about}}></div>
|
</> : <>
|
||||||
</>
|
{
|
||||||
}
|
about.startsWith('https://') ? <iframe
|
||||||
</Segment>
|
src={about}
|
||||||
|
style={{ width: '100%', height: '100vh', border: 'none' }}
|
||||||
|
/> : <Segment>
|
||||||
|
<div style={{ fontSize: 'larger' }} dangerouslySetInnerHTML={{ __html: about }}></div>
|
||||||
|
</Segment>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import React, { useContext, useEffect } from 'react';
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
import { Card, Grid, Header, Segment } from 'semantic-ui-react';
|
import { Card, Grid, Header, Segment } from 'semantic-ui-react';
|
||||||
import { API, showError, showNotice, timestamp2string } from '../../helpers';
|
import { API, showError, showNotice, timestamp2string } from '../../helpers';
|
||||||
import { StatusContext } from '../../context/Status';
|
import { StatusContext } from '../../context/Status';
|
||||||
|
import { marked } from 'marked';
|
||||||
|
|
||||||
const Home = () => {
|
const Home = () => {
|
||||||
const [statusState, statusDispatch] = useContext(StatusContext);
|
const [statusState, statusDispatch] = useContext(StatusContext);
|
||||||
|
const [homePageContentLoaded, setHomePageContentLoaded] = useState(false);
|
||||||
|
const [homePageContent, setHomePageContent] = useState('');
|
||||||
|
|
||||||
const displayNotice = async () => {
|
const displayNotice = async () => {
|
||||||
const res = await API.get('/api/notice');
|
const res = await API.get('/api/notice');
|
||||||
@@ -20,6 +23,24 @@ const Home = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const displayHomePageContent = async () => {
|
||||||
|
setHomePageContent(localStorage.getItem('home_page_content') || '');
|
||||||
|
const res = await API.get('/api/home_page_content');
|
||||||
|
const { success, message, data } = res.data;
|
||||||
|
if (success) {
|
||||||
|
let content = data;
|
||||||
|
if (!data.startsWith('https://')) {
|
||||||
|
content = marked.parse(data);
|
||||||
|
}
|
||||||
|
setHomePageContent(content);
|
||||||
|
localStorage.setItem('home_page_content', content);
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
setHomePageContent('加载首页内容失败...');
|
||||||
|
}
|
||||||
|
setHomePageContentLoaded(true);
|
||||||
|
};
|
||||||
|
|
||||||
const getStartTimeString = () => {
|
const getStartTimeString = () => {
|
||||||
const timestamp = statusState?.status?.start_time;
|
const timestamp = statusState?.status?.start_time;
|
||||||
return timestamp2string(timestamp);
|
return timestamp2string(timestamp);
|
||||||
@@ -27,70 +48,83 @@ const Home = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
displayNotice().then();
|
displayNotice().then();
|
||||||
|
displayHomePageContent().then();
|
||||||
}, []);
|
}, []);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Segment>
|
{
|
||||||
<Header as='h3'>系统状况</Header>
|
homePageContentLoaded && homePageContent === '' ? <>
|
||||||
<Grid columns={2} stackable>
|
<Segment>
|
||||||
<Grid.Column>
|
<Header as='h3'>系统状况</Header>
|
||||||
<Card fluid>
|
<Grid columns={2} stackable>
|
||||||
<Card.Content>
|
<Grid.Column>
|
||||||
<Card.Header>系统信息</Card.Header>
|
<Card fluid>
|
||||||
<Card.Meta>系统信息总览</Card.Meta>
|
<Card.Content>
|
||||||
<Card.Description>
|
<Card.Header>系统信息</Card.Header>
|
||||||
<p>名称:{statusState?.status?.system_name}</p>
|
<Card.Meta>系统信息总览</Card.Meta>
|
||||||
<p>版本:{statusState?.status?.version}</p>
|
<Card.Description>
|
||||||
<p>
|
<p>名称:{statusState?.status?.system_name}</p>
|
||||||
源码:
|
<p>版本:{statusState?.status?.version}</p>
|
||||||
<a
|
<p>
|
||||||
href='https://github.com/songquanpeng/one-api'
|
源码:
|
||||||
target='_blank'
|
<a
|
||||||
>
|
href='https://github.com/songquanpeng/one-api'
|
||||||
https://github.com/songquanpeng/one-api
|
target='_blank'
|
||||||
</a>
|
>
|
||||||
</p>
|
https://github.com/songquanpeng/one-api
|
||||||
<p>启动时间:{getStartTimeString()}</p>
|
</a>
|
||||||
</Card.Description>
|
</p>
|
||||||
</Card.Content>
|
<p>启动时间:{getStartTimeString()}</p>
|
||||||
</Card>
|
</Card.Description>
|
||||||
</Grid.Column>
|
</Card.Content>
|
||||||
<Grid.Column>
|
</Card>
|
||||||
<Card fluid>
|
</Grid.Column>
|
||||||
<Card.Content>
|
<Grid.Column>
|
||||||
<Card.Header>系统配置</Card.Header>
|
<Card fluid>
|
||||||
<Card.Meta>系统配置总览</Card.Meta>
|
<Card.Content>
|
||||||
<Card.Description>
|
<Card.Header>系统配置</Card.Header>
|
||||||
<p>
|
<Card.Meta>系统配置总览</Card.Meta>
|
||||||
邮箱验证:
|
<Card.Description>
|
||||||
{statusState?.status?.email_verification === true
|
<p>
|
||||||
? '已启用'
|
邮箱验证:
|
||||||
: '未启用'}
|
{statusState?.status?.email_verification === true
|
||||||
</p>
|
? '已启用'
|
||||||
<p>
|
: '未启用'}
|
||||||
GitHub 身份验证:
|
</p>
|
||||||
{statusState?.status?.github_oauth === true
|
<p>
|
||||||
? '已启用'
|
GitHub 身份验证:
|
||||||
: '未启用'}
|
{statusState?.status?.github_oauth === true
|
||||||
</p>
|
? '已启用'
|
||||||
<p>
|
: '未启用'}
|
||||||
微信身份验证:
|
</p>
|
||||||
{statusState?.status?.wechat_login === true
|
<p>
|
||||||
? '已启用'
|
微信身份验证:
|
||||||
: '未启用'}
|
{statusState?.status?.wechat_login === true
|
||||||
</p>
|
? '已启用'
|
||||||
<p>
|
: '未启用'}
|
||||||
Turnstile 用户校验:
|
</p>
|
||||||
{statusState?.status?.turnstile_check === true
|
<p>
|
||||||
? '已启用'
|
Turnstile 用户校验:
|
||||||
: '未启用'}
|
{statusState?.status?.turnstile_check === true
|
||||||
</p>
|
? '已启用'
|
||||||
</Card.Description>
|
: '未启用'}
|
||||||
</Card.Content>
|
</p>
|
||||||
</Card>
|
</Card.Description>
|
||||||
</Grid.Column>
|
</Card.Content>
|
||||||
</Grid>
|
</Card>
|
||||||
</Segment>
|
</Grid.Column>
|
||||||
|
</Grid>
|
||||||
|
</Segment>
|
||||||
|
</> : <>
|
||||||
|
{
|
||||||
|
homePageContent.startsWith('https://') ? <iframe
|
||||||
|
src={homePageContent}
|
||||||
|
style={{ width: '100%', height: '100vh', border: 'none' }}
|
||||||
|
/> : <div style={{ fontSize: 'larger' }} dangerouslySetInnerHTML={{ __html: homePageContent }}></div>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user