mirror of
				https://github.com/songquanpeng/one-api.git
				synced 2025-10-31 22:03:41 +08:00 
			
		
		
		
	Compare commits
	
		
			12 Commits
		
	
	
		
			v0.2.4-alp
			...
			v0.2.5
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 2cdc718fde | ||
|  | 57cb150177 | ||
|  | 6167e20b34 | ||
|  | 8835d8302e | ||
|  | 224bebe67a | ||
|  | cf6883778e | ||
|  | 246b981e23 | ||
|  | 2edd52e851 | ||
|  | e123c66bc7 | ||
|  | 9edc82bde0 | ||
|  | d84c2f5c70 | ||
|  | 46e77389a4 | 
							
								
								
									
										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 进行部署 | ||||
|   | ||||
| @@ -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, SMTPFrom, 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 | ||||
| } | ||||
|   | ||||
| @@ -54,6 +54,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: | ||||
|   | ||||
| @@ -39,6 +39,7 @@ func InitOptionMap() { | ||||
| 	common.OptionMap["SMTPToken"] = "" | ||||
| 	common.OptionMap["Notice"] = "" | ||||
| 	common.OptionMap["About"] = "" | ||||
| 	common.OptionMap["HomePageContent"] = "" | ||||
| 	common.OptionMap["Footer"] = common.Footer | ||||
| 	common.OptionMap["ServerAddress"] = "" | ||||
| 	common.OptionMap["GitHubClientId"] = "" | ||||
| @@ -59,9 +60,6 @@ func InitOptionMap() { | ||||
| 			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 { | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -8,6 +8,7 @@ const OtherSetting = () => { | ||||
|     Footer: '', | ||||
|     Notice: '', | ||||
|     About: '', | ||||
|     HomePageContent: '', | ||||
|   }); | ||||
|   let originInputs = {}; | ||||
|   let [loading, setLoading] = useState(false); | ||||
| @@ -69,6 +70,10 @@ const OtherSetting = () => { | ||||
|     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 +114,21 @@ const OtherSetting = () => { | ||||
|           <Form.Button onClick={submitNotice}>保存公告</Form.Button> | ||||
|           <Divider /> | ||||
|           <Header as='h3'>个性化设置</Header> | ||||
|           <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} | ||||
|   | ||||
| @@ -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> | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
|   | ||||
| @@ -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