mirror of
				https://github.com/songquanpeng/one-api.git
				synced 2025-10-31 13:53:41 +08:00 
			
		
		
		
	Compare commits
	
		
			16 Commits
		
	
	
		
			v0.2.4-alp
			...
			v0.2.6-alp
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 926951ee03 | ||
|  | 2cdc718fde | ||
|  | 57cb150177 | ||
|  | 6167e20b34 | ||
|  | 8835d8302e | ||
|  | 224bebe67a | ||
|  | cf6883778e | ||
|  | 246b981e23 | ||
|  | 2edd52e851 | ||
|  | e123c66bc7 | ||
|  | 9edc82bde0 | ||
|  | d84c2f5c70 | ||
|  | 46e77389a4 | ||
|  | f5f4e6fbc6 | ||
|  | dc4a6cb711 | ||
|  | 5798fdac50 | 
							
								
								
									
										15
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								README.md
									
									
									
									
									
								
							| @@ -43,8 +43,8 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用 | ||||
| ## 功能 | ||||
| 1. 支持多种 API 访问渠道,欢迎 PR 或提 issue 添加更多渠道: | ||||
|    + [x] OpenAI 官方通道 | ||||
|    + [x] [API2D](https://api2d.com/r/197971) | ||||
|    + [x] Azure OpenAI API | ||||
|    + [x] [API2D](https://api2d.com/r/197971) | ||||
|    + [x] [CloseAI](https://console.openai-asia.com) | ||||
|    + [x] [OpenAI-SB](https://openai-sb.com) | ||||
|    + [x] [OpenAI Max](https://openaimax.com) | ||||
| @@ -55,15 +55,16 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用 | ||||
| 4. 支持 HTTP SSE,可以通过流式传输实现打字机效果。 | ||||
| 5. 支持设置令牌的过期时间和使用次数。 | ||||
| 6. 支持批量生成和导出兑换码,可使用兑换码为令牌进行充值。 | ||||
| 7. 支持为新用户设置初始配额。 | ||||
| 8. 支持发布公告,在线修改关于页面,设置充值链接,自定义页脚。 | ||||
| 9. 支持通过系统访问令牌访问管理 API。 | ||||
| 10. 多种用户登录注册方式: | ||||
| 7. 支持批量创建通道。 | ||||
| 8. 支持发布公告,自定义关于页面,设置充值链接,自定义页脚。 | ||||
| 9. 支持自定义首页,可以选择使用 HTML & Markdown 代码进行自定义,或者使用一个单独的网页通过 iframe 嵌入。 | ||||
| 10. 支持通过系统访问令牌访问管理 API。 | ||||
| 11. 多种用户登录注册方式: | ||||
|     + 邮箱登录注册以及通过邮箱进行密码重置。 | ||||
|     + [GitHub 开放授权](https://github.com/settings/applications/new)。 | ||||
|     + 微信公众号授权(需要额外部署 [WeChat Server](https://github.com/songquanpeng/wechat-server))。 | ||||
| 11. 支持用户管理。 | ||||
| 12. 未来其他大模型开放 API 后,将第一时间支持,并将其封装成同样的 API 访问方式。 | ||||
| 12. 支持用户管理,支持为新用户设置初始配额。。 | ||||
| 13. 未来其他大模型开放 API 后,将第一时间支持,并将其封装成同样的 API 访问方式。 | ||||
|  | ||||
| ## 部署 | ||||
| ### 基于 Docker 进行部署 | ||||
|   | ||||
| @@ -11,6 +11,7 @@ var Version = "v0.0.0"            // this hard coding will be replaced automatic | ||||
| var SystemName = "One API" | ||||
| var ServerAddress = "http://localhost:3000" | ||||
| var Footer = "" | ||||
| var Logo = "" | ||||
| var TopUpLink = "" | ||||
|  | ||||
| var UsingSQLite = false | ||||
| @@ -36,6 +37,7 @@ var RegisterEnabled = true | ||||
| var SMTPServer = "" | ||||
| var SMTPPort = 587 | ||||
| var SMTPAccount = "" | ||||
| var SMTPFrom = "" | ||||
| var SMTPToken = "" | ||||
|  | ||||
| var GitHubClientId = "" | ||||
|   | ||||
| @@ -1,20 +1,67 @@ | ||||
| package common | ||||
|  | ||||
| import ( | ||||
| 	"crypto/tls" | ||||
| 	"encoding/base64" | ||||
| 	"fmt" | ||||
| 	"net/smtp" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| 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"+ | ||||
| 		"From: %s<%s>\r\n"+ | ||||
| 		"Subject: %s\r\n"+ | ||||
| 		"Content-Type: text/html; charset=UTF-8\r\n\r\n%s\r\n", | ||||
| 		receiver, SystemName, SMTPAccount, subject, content)) | ||||
| 		receiver, SystemName, SMTPFrom, encodedSubject, content)) | ||||
| 	auth := smtp.PlainAuth("", SMTPAccount, SMTPToken, SMTPServer) | ||||
| 	addr := fmt.Sprintf("%s:%d", SMTPServer, SMTPPort) | ||||
| 	to := strings.Split(receiver, ";") | ||||
| 	err := smtp.SendMail(addr, auth, SMTPAccount, to, mail) | ||||
| 	var err error | ||||
| 	if SMTPPort == 465 { | ||||
| 		tlsConfig := &tls.Config{ | ||||
| 			InsecureSkipVerify: true, | ||||
| 			ServerName:         SMTPServer, | ||||
| 		} | ||||
| 		conn, err := tls.Dial("tcp", fmt.Sprintf("%s:%d", SMTPServer, SMTPPort), tlsConfig) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		client, err := smtp.NewClient(conn, SMTPServer) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		defer client.Close() | ||||
| 		if err = client.Auth(auth); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		if err = client.Mail(SMTPFrom); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		receiverEmails := strings.Split(receiver, ";") | ||||
| 		for _, receiver := range receiverEmails { | ||||
| 			if err = client.Rcpt(receiver); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 		} | ||||
| 		w, err := client.Data() | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		_, err = w.Write(mail) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		err = w.Close() | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} else { | ||||
| 		err = smtp.SendMail(addr, auth, SMTPAccount, to, mail) | ||||
| 	} | ||||
| 	return err | ||||
| } | ||||
|   | ||||
| @@ -6,6 +6,7 @@ import ( | ||||
| 	"one-api/common" | ||||
| 	"one-api/model" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| func GetAllChannels(c *gin.Context) { | ||||
| @@ -84,7 +85,17 @@ func AddChannel(c *gin.Context) { | ||||
| 	} | ||||
| 	channel.CreatedTime = common.GetTimestamp() | ||||
| 	channel.AccessedTime = common.GetTimestamp() | ||||
| 	err = channel.Insert() | ||||
| 	keys := strings.Split(channel.Key, "\n") | ||||
| 	channels := make([]model.Channel, 0) | ||||
| 	for _, key := range keys { | ||||
| 		if key == "" { | ||||
| 			continue | ||||
| 		} | ||||
| 		localChannel := channel | ||||
| 		localChannel.Key = key | ||||
| 		channels = append(channels, localChannel) | ||||
| 	} | ||||
| 	err = model.BatchInsertChannels(channels) | ||||
| 	if err != nil { | ||||
| 		c.JSON(http.StatusOK, gin.H{ | ||||
| 			"success": false, | ||||
|   | ||||
| @@ -20,6 +20,7 @@ func GetStatus(c *gin.Context) { | ||||
| 			"github_oauth":       common.GitHubOAuthEnabled, | ||||
| 			"github_client_id":   common.GitHubClientId, | ||||
| 			"system_name":        common.SystemName, | ||||
| 			"logo":               common.Logo, | ||||
| 			"footer_html":        common.Footer, | ||||
| 			"wechat_qrcode":      common.WeChatAccountQRCodeImageURL, | ||||
| 			"wechat_login":       common.WeChatAuthEnabled, | ||||
| @@ -54,6 +55,17 @@ func GetAbout(c *gin.Context) { | ||||
| 	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) { | ||||
| 	email := c.Query("email") | ||||
| 	if err := common.Validate.Var(email, "required,email"); err != nil { | ||||
|   | ||||
| @@ -94,10 +94,12 @@ func relayHelper(c *gin.Context) error { | ||||
| 	if channelType == common.ChannelTypeAzure { | ||||
| 		// 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() | ||||
| 		if query.Get("api-version") == "" { | ||||
| 			apiVersion := c.GetString("api_version") | ||||
| 			requestURL = fmt.Sprintf("%s?api-version=%s", requestURL, apiVersion) | ||||
| 		apiVersion := query.Get("api-version") | ||||
| 		if 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") | ||||
| 		task := strings.TrimPrefix(requestURL, "/v1/") | ||||
| 		model_ := textRequest.Model | ||||
| @@ -186,7 +188,7 @@ func relayHelper(c *gin.Context) error { | ||||
| 				data := scanner.Text() | ||||
| 				dataChan <- data | ||||
| 				data = data[6:] | ||||
| 				if data != "[DONE]" { | ||||
| 				if !strings.HasPrefix(data, "[DONE]") { | ||||
| 					var streamResponse StreamResponse | ||||
| 					err = json.Unmarshal([]byte(data), &streamResponse) | ||||
| 					if err != nil { | ||||
| @@ -207,6 +209,9 @@ func relayHelper(c *gin.Context) error { | ||||
| 		c.Stream(func(w io.Writer) bool { | ||||
| 			select { | ||||
| 			case data := <-dataChan: | ||||
| 				if strings.HasPrefix(data, "data: [DONE]") { | ||||
| 					data = data[:12] | ||||
| 				} | ||||
| 				c.Render(-1, common.CustomEvent{Data: data}) | ||||
| 				return true | ||||
| 			case <-stopChan: | ||||
|   | ||||
| @@ -53,6 +53,12 @@ func GetRandomChannel() (*Channel, error) { | ||||
| 	return &channel, err | ||||
| } | ||||
|  | ||||
| func BatchInsertChannels(channels []Channel) error { | ||||
| 	var err error | ||||
| 	err = DB.Create(&channels).Error | ||||
| 	return err | ||||
| } | ||||
|  | ||||
| func (channel *Channel) Insert() error { | ||||
| 	var err error | ||||
| 	err = DB.Create(channel).Error | ||||
|   | ||||
| @@ -33,12 +33,16 @@ func InitOptionMap() { | ||||
| 	common.OptionMap["TurnstileCheckEnabled"] = strconv.FormatBool(common.TurnstileCheckEnabled) | ||||
| 	common.OptionMap["RegisterEnabled"] = strconv.FormatBool(common.RegisterEnabled) | ||||
| 	common.OptionMap["SMTPServer"] = "" | ||||
| 	common.OptionMap["SMTPFrom"] = "" | ||||
| 	common.OptionMap["SMTPPort"] = strconv.Itoa(common.SMTPPort) | ||||
| 	common.OptionMap["SMTPAccount"] = "" | ||||
| 	common.OptionMap["SMTPToken"] = "" | ||||
| 	common.OptionMap["Notice"] = "" | ||||
| 	common.OptionMap["About"] = "" | ||||
| 	common.OptionMap["HomePageContent"] = "" | ||||
| 	common.OptionMap["Footer"] = common.Footer | ||||
| 	common.OptionMap["SystemName"] = common.SystemName | ||||
| 	common.OptionMap["Logo"] = common.Logo | ||||
| 	common.OptionMap["ServerAddress"] = "" | ||||
| 	common.OptionMap["GitHubClientId"] = "" | ||||
| 	common.OptionMap["GitHubClientSecret"] = "" | ||||
| @@ -120,6 +124,8 @@ func updateOptionMap(key string, value string) (err error) { | ||||
| 		common.SMTPPort = intValue | ||||
| 	case "SMTPAccount": | ||||
| 		common.SMTPAccount = value | ||||
| 	case "SMTPFrom": | ||||
| 		common.SMTPFrom = value | ||||
| 	case "SMTPToken": | ||||
| 		common.SMTPToken = value | ||||
| 	case "ServerAddress": | ||||
| @@ -130,6 +136,10 @@ func updateOptionMap(key string, value string) (err error) { | ||||
| 		common.GitHubClientSecret = value | ||||
| 	case "Footer": | ||||
| 		common.Footer = value | ||||
| 	case "SystemName": | ||||
| 		common.SystemName = value | ||||
| 	case "Logo": | ||||
| 		common.Logo = value | ||||
| 	case "WeChatServerAddress": | ||||
| 		common.WeChatServerAddress = value | ||||
| 	case "WeChatServerToken": | ||||
|   | ||||
| @@ -15,6 +15,7 @@ func SetApiRouter(router *gin.Engine) { | ||||
| 		apiRouter.GET("/status", controller.GetStatus) | ||||
| 		apiRouter.GET("/notice", controller.GetNotice) | ||||
| 		apiRouter.GET("/about", controller.GetAbout) | ||||
| 		apiRouter.GET("/home_page_content", controller.GetHomePageContent) | ||||
| 		apiRouter.GET("/verification", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendEmailVerification) | ||||
| 		apiRouter.GET("/reset_password", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendPasswordResetEmail) | ||||
| 		apiRouter.POST("/user/reset", middleware.CriticalRateLimit(), controller.ResetPassword) | ||||
|   | ||||
| @@ -42,6 +42,8 @@ function App() { | ||||
|     if (success) { | ||||
|       localStorage.setItem('status', JSON.stringify(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); | ||||
|       if ( | ||||
|         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 { getFooterHTML, getSystemName } from '../helpers'; | ||||
|  | ||||
| const Footer = () => { | ||||
|   const [Footer, setFooter] = useState(''); | ||||
|   useEffect(() => { | ||||
|     let savedFooter = localStorage.getItem('footer_html'); | ||||
|     if (!savedFooter) savedFooter = ''; | ||||
|     setFooter(savedFooter); | ||||
|   }); | ||||
|   const systemName = getSystemName(); | ||||
|   const footer = getFooterHTML(); | ||||
|  | ||||
|   return ( | ||||
|     <Segment vertical> | ||||
|       <Container textAlign="center"> | ||||
|         {Footer === '' ? ( | ||||
|           <div className="custom-footer"> | ||||
|       <Container textAlign='center'> | ||||
|         {footer ? ( | ||||
|           <div | ||||
|             className='custom-footer' | ||||
|             dangerouslySetInnerHTML={{ __html: footer }} | ||||
|           ></div> | ||||
|         ) : ( | ||||
|           <div className='custom-footer'> | ||||
|             <a | ||||
|               href="https://github.com/songquanpeng/one-api" | ||||
|               target="_blank" | ||||
|               href='https://github.com/songquanpeng/one-api' | ||||
|               target='_blank' | ||||
|             > | ||||
|               One API {process.env.REACT_APP_VERSION}{' '} | ||||
|               {systemName} {process.env.REACT_APP_VERSION}{' '} | ||||
|             </a> | ||||
|             由{' '} | ||||
|             <a href="https://github.com/songquanpeng" target="_blank"> | ||||
|             <a href='https://github.com/songquanpeng' target='_blank'> | ||||
|               JustSong | ||||
|             </a>{' '} | ||||
|             构建,源代码遵循{' '} | ||||
|             <a href="https://opensource.org/licenses/mit-license.php"> | ||||
|             <a href='https://opensource.org/licenses/mit-license.php'> | ||||
|               MIT 协议 | ||||
|             </a> | ||||
|           </div> | ||||
|         ) : ( | ||||
|           <div | ||||
|             className="custom-footer" | ||||
|             dangerouslySetInnerHTML={{ __html: Footer }} | ||||
|           ></div> | ||||
|         )} | ||||
|       </Container> | ||||
|     </Segment> | ||||
|   | ||||
| @@ -3,7 +3,7 @@ import { Link, useNavigate } from 'react-router-dom'; | ||||
| import { UserContext } from '../context/User'; | ||||
|  | ||||
| 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'; | ||||
|  | ||||
| // Header Buttons | ||||
| @@ -53,6 +53,8 @@ const Header = () => { | ||||
|   let navigate = useNavigate(); | ||||
|  | ||||
|   const [showSidebar, setShowSidebar] = useState(false); | ||||
|   const systemName = getSystemName(); | ||||
|   const logo = getLogo(); | ||||
|  | ||||
|   async function logout() { | ||||
|     setShowSidebar(false); | ||||
| @@ -111,12 +113,12 @@ const Header = () => { | ||||
|           <Container> | ||||
|             <Menu.Item as={Link} to='/'> | ||||
|               <img | ||||
|                 src='/logo.png' | ||||
|                 src={logo} | ||||
|                 alt='logo' | ||||
|                 style={{ marginRight: '0.75em' }} | ||||
|               /> | ||||
|               <div style={{ fontSize: '20px' }}> | ||||
|                 <b>One API</b> | ||||
|                 <b>{systemName}</b> | ||||
|               </div> | ||||
|             </Menu.Item> | ||||
|             <Menu.Menu position='right'> | ||||
| @@ -168,9 +170,9 @@ const Header = () => { | ||||
|       <Menu borderless style={{ borderTop: 'none' }}> | ||||
|         <Container> | ||||
|           <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' }}> | ||||
|               <b>One API</b> | ||||
|               <b>{systemName}</b> | ||||
|             </div> | ||||
|           </Menu.Item> | ||||
|           {renderButtons(false)} | ||||
|   | ||||
| @@ -12,7 +12,7 @@ import { | ||||
| } from 'semantic-ui-react'; | ||||
| import { Link, useNavigate, useSearchParams } from 'react-router-dom'; | ||||
| import { UserContext } from '../context/User'; | ||||
| import { API, showError, showSuccess } from '../helpers'; | ||||
| import { API, getLogo, showError, showSuccess } from '../helpers'; | ||||
|  | ||||
| const LoginForm = () => { | ||||
|   const [inputs, setInputs] = useState({ | ||||
| @@ -27,6 +27,7 @@ const LoginForm = () => { | ||||
|   let navigate = useNavigate(); | ||||
|  | ||||
|   const [status, setStatus] = useState({}); | ||||
|   const logo = getLogo(); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     if (searchParams.get("expired")) { | ||||
| @@ -95,7 +96,7 @@ const LoginForm = () => { | ||||
|     <Grid textAlign="center" style={{ marginTop: '48px' }}> | ||||
|       <Grid.Column style={{ maxWidth: 450 }}> | ||||
|         <Header as="h2" color="" textAlign="center"> | ||||
|           <Image src="/logo.png" /> 用户登录 | ||||
|           <Image src={logo} /> 用户登录 | ||||
|         </Header> | ||||
|         <Form size="large"> | ||||
|           <Segment> | ||||
|   | ||||
| @@ -8,6 +8,9 @@ const OtherSetting = () => { | ||||
|     Footer: '', | ||||
|     Notice: '', | ||||
|     About: '', | ||||
|     SystemName: '', | ||||
|     Logo: '', | ||||
|     HomePageContent: '', | ||||
|   }); | ||||
|   let originInputs = {}; | ||||
|   let [loading, setLoading] = useState(false); | ||||
| @@ -65,10 +68,22 @@ const OtherSetting = () => { | ||||
|     await updateOption('Footer', inputs.Footer); | ||||
|   }; | ||||
|  | ||||
|   const submitSystemName = async () => { | ||||
|     await updateOption('SystemName', inputs.SystemName); | ||||
|   }; | ||||
|  | ||||
|   const submitLogo = async () => { | ||||
|     await updateOption('Logo', inputs.Logo); | ||||
|   }; | ||||
|  | ||||
|   const submitAbout = async () => { | ||||
|     await updateOption('About', inputs.About); | ||||
|   }; | ||||
|  | ||||
|   const submitOption = async (key) => { | ||||
|     await updateOption(key, inputs[key]); | ||||
|   }; | ||||
|  | ||||
|   const openGitHubRelease = () => { | ||||
|     window.location = | ||||
|       'https://github.com/songquanpeng/one-api/releases/latest'; | ||||
| @@ -109,10 +124,42 @@ const OtherSetting = () => { | ||||
|           <Form.Button onClick={submitNotice}>保存公告</Form.Button> | ||||
|           <Divider /> | ||||
|           <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.TextArea | ||||
|               label='关于' | ||||
|               placeholder='在此输入新的关于内容,支持 Markdown & HTML 代码' | ||||
|               placeholder='在此输入新的关于内容,支持 Markdown & HTML 代码。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为关于页面。' | ||||
|               value={inputs.About} | ||||
|               name='About' | ||||
|               onChange={handleInputChange} | ||||
|   | ||||
| @@ -9,7 +9,7 @@ import { | ||||
|   Segment, | ||||
| } from 'semantic-ui-react'; | ||||
| 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'; | ||||
|  | ||||
| const RegisterForm = () => { | ||||
| @@ -26,6 +26,7 @@ const RegisterForm = () => { | ||||
|   const [turnstileSiteKey, setTurnstileSiteKey] = useState(''); | ||||
|   const [turnstileToken, setTurnstileToken] = useState(''); | ||||
|   const [loading, setLoading] = useState(false); | ||||
|   const logo = getLogo(); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     let status = localStorage.getItem('status'); | ||||
| @@ -100,7 +101,7 @@ const RegisterForm = () => { | ||||
|     <Grid textAlign='center' style={{ marginTop: '48px' }}> | ||||
|       <Grid.Column style={{ maxWidth: 450 }}> | ||||
|         <Header as='h2' color='' textAlign='center'> | ||||
|           <Image src='/logo.png' /> 新用户注册 | ||||
|           <Image src={logo} /> 新用户注册 | ||||
|         </Header> | ||||
|         <Form size='large'> | ||||
|           <Segment> | ||||
|   | ||||
| @@ -14,6 +14,7 @@ const SystemSetting = () => { | ||||
|     SMTPServer: '', | ||||
|     SMTPPort: '', | ||||
|     SMTPAccount: '', | ||||
|     SMTPFrom: '', | ||||
|     SMTPToken: '', | ||||
|     ServerAddress: '', | ||||
|     Footer: '', | ||||
| @@ -129,6 +130,9 @@ const SystemSetting = () => { | ||||
|     if (originInputs['SMTPAccount'] !== inputs.SMTPAccount) { | ||||
|       await updateOption('SMTPAccount', inputs.SMTPAccount); | ||||
|     } | ||||
|     if (originInputs['SMTPFrom'] !== inputs.SMTPFrom) { | ||||
|       await updateOption('SMTPFrom', inputs.SMTPFrom); | ||||
|     } | ||||
|     if ( | ||||
|       originInputs['SMTPPort'] !== inputs.SMTPPort && | ||||
|       inputs.SMTPPort !== '' | ||||
| @@ -298,7 +302,7 @@ const SystemSetting = () => { | ||||
|             配置 SMTP | ||||
|             <Header.Subheader>用以支持系统的邮件发送</Header.Subheader> | ||||
|           </Header> | ||||
|           <Form.Group widths={4}> | ||||
|           <Form.Group widths={3}> | ||||
|             <Form.Input | ||||
|               label='SMTP 服务器地址' | ||||
|               name='SMTPServer' | ||||
| @@ -323,6 +327,16 @@ const SystemSetting = () => { | ||||
|               value={inputs.SMTPAccount} | ||||
|               placeholder='通常是邮箱地址' | ||||
|             /> | ||||
|           </Form.Group> | ||||
|           <Form.Group widths={3}> | ||||
|             <Form.Input | ||||
|               label='SMTP 发送者邮箱' | ||||
|               name='SMTPFrom' | ||||
|               onChange={handleInputChange} | ||||
|               autoComplete='new-password' | ||||
|               value={inputs.SMTPFrom} | ||||
|               placeholder='通常和邮箱地址保持一致' | ||||
|             /> | ||||
|             <Form.Input | ||||
|               label='SMTP 访问凭证' | ||||
|               name='SMTPToken' | ||||
|   | ||||
| @@ -15,6 +15,22 @@ export function isRoot() { | ||||
|   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) { | ||||
|   let okay = true; | ||||
|   try { | ||||
|   | ||||
| @@ -5,6 +5,11 @@ body { | ||||
|     font-family: Lato, 'Helvetica Neue', Arial, Helvetica, "Microsoft YaHei", sans-serif; | ||||
|     -webkit-font-smoothing: antialiased; | ||||
|     -moz-osx-font-smoothing: grayscale; | ||||
|     scrollbar-width: none; | ||||
| } | ||||
|  | ||||
| body::-webkit-scrollbar { | ||||
|     display: none; | ||||
| } | ||||
|  | ||||
| code { | ||||
|   | ||||
| @@ -5,18 +5,24 @@ import { marked } from 'marked'; | ||||
|  | ||||
| const About = () => { | ||||
|   const [about, setAbout] = useState(''); | ||||
|   const [aboutLoaded, setAboutLoaded] = useState(false); | ||||
|  | ||||
|   const displayAbout = async () => { | ||||
|     setAbout(localStorage.getItem('about') || ''); | ||||
|     const res = await API.get('/api/about'); | ||||
|     const { success, message, data } = res.data; | ||||
|     if (success) { | ||||
|       let HTMLAbout = marked.parse(data); | ||||
|       localStorage.setItem('about', HTMLAbout); | ||||
|       setAbout(HTMLAbout); | ||||
|       let aboutContent = data; | ||||
|       if (!data.startsWith('https://')) { | ||||
|         aboutContent = marked.parse(data); | ||||
|       } | ||||
|       setAbout(aboutContent); | ||||
|       localStorage.setItem('about', aboutContent); | ||||
|     } else { | ||||
|       showError(message); | ||||
|       setAbout('加载关于内容失败...'); | ||||
|     } | ||||
|     setAboutLoaded(true); | ||||
|   }; | ||||
|  | ||||
|   useEffect(() => { | ||||
| @@ -25,20 +31,27 @@ const About = () => { | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <Segment> | ||||
|       { | ||||
|           about === '' ? <> | ||||
|         aboutLoaded && about === '' ? <> | ||||
|           <Segment> | ||||
|             <Header as='h3'>关于</Header> | ||||
|             <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 | ||||
|             </a> | ||||
|           </Segment> | ||||
|         </> : <> | ||||
|             <div dangerouslySetInnerHTML={{ __html: about}}></div> | ||||
|           { | ||||
|             about.startsWith('https://') ? <iframe | ||||
|               src={about} | ||||
|               style={{ width: '100%', height: '100vh', border: 'none' }} | ||||
|             /> : <Segment> | ||||
|               <div style={{ fontSize: 'larger' }} dangerouslySetInnerHTML={{ __html: about }}></div> | ||||
|             </Segment> | ||||
|           } | ||||
|         </> | ||||
|       } | ||||
|       </Segment> | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
|   | ||||
| @@ -16,8 +16,10 @@ const EditChannel = () => { | ||||
|     base_url: '', | ||||
|     other: '' | ||||
|   }; | ||||
|   const [batch, setBatch] = useState(false); | ||||
|   const [inputs, setInputs] = useState(originInputs); | ||||
|   const handleInputChange = (e, { name, value }) => { | ||||
|     console.log(name, value); | ||||
|     setInputs((inputs) => ({ ...inputs, [name]: value })); | ||||
|   }; | ||||
|  | ||||
| @@ -130,17 +132,38 @@ const EditChannel = () => { | ||||
|               autoComplete='new-password' | ||||
|             /> | ||||
|           </Form.Field> | ||||
|           <Form.Field> | ||||
|           { | ||||
|             batch ? <Form.Field> | ||||
|               <Form.TextArea | ||||
|                 label='密钥' | ||||
|                 name='key' | ||||
|                 placeholder={'请输入密钥,一行一个'} | ||||
|                 onChange={handleInputChange} | ||||
|                 value={inputs.key} | ||||
|                 style={{ minHeight: 150, fontFamily: 'JetBrains Mono, Consolas' }} | ||||
|                 autoComplete='new-password' | ||||
|               /> | ||||
|             </Form.Field> : <Form.Field> | ||||
|               <Form.Input | ||||
|                 label='密钥' | ||||
|                 name='key' | ||||
|                 placeholder={'请输入密钥'} | ||||
|                 onChange={handleInputChange} | ||||
|                 value={inputs.key} | ||||
|               // type='password' | ||||
|                 autoComplete='new-password' | ||||
|                 /> | ||||
|             </Form.Field> | ||||
|           } | ||||
|           { | ||||
|             !isEdit && ( | ||||
|               <Form.Checkbox | ||||
|                 checked={batch} | ||||
|                 label='批量创建' | ||||
|                 name='batch' | ||||
|                 onChange={() => setBatch(!batch)} | ||||
|               /> | ||||
|             ) | ||||
|           } | ||||
|           <Button onClick={submit}>提交</Button> | ||||
|         </Form> | ||||
|       </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 { API, showError, showNotice, timestamp2string } from '../../helpers'; | ||||
| import { StatusContext } from '../../context/Status'; | ||||
| import { marked } from 'marked'; | ||||
|  | ||||
| const Home = () => { | ||||
|   const [statusState, statusDispatch] = useContext(StatusContext); | ||||
|   const [homePageContentLoaded, setHomePageContentLoaded] = useState(false); | ||||
|   const [homePageContent, setHomePageContent] = useState(''); | ||||
|  | ||||
|   const displayNotice = async () => { | ||||
|     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 timestamp = statusState?.status?.start_time; | ||||
|     return timestamp2string(timestamp); | ||||
| @@ -27,9 +48,12 @@ const Home = () => { | ||||
|  | ||||
|   useEffect(() => { | ||||
|     displayNotice().then(); | ||||
|     displayHomePageContent().then(); | ||||
|   }, []); | ||||
|   return ( | ||||
|     <> | ||||
|       { | ||||
|         homePageContentLoaded && homePageContent === '' ? <> | ||||
|           <Segment> | ||||
|             <Header as='h3'>系统状况</Header> | ||||
|             <Grid columns={2} stackable> | ||||
| @@ -91,6 +115,16 @@ const Home = () => { | ||||
|               </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