mirror of
				https://github.com/songquanpeng/one-api.git
				synced 2025-10-31 05:43:42 +08:00 
			
		
		
		
	Compare commits
	
		
			13 Commits
		
	
	
		
			v0.6.5-alp
			...
			v0.6.5-alp
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | dcf24b98dc | ||
|  | af679e04f4 | ||
|  | 93cbca6a9f | ||
|  | 840ef80d94 | ||
|  | 9a2662af0d | ||
|  | 77f9e75654 | ||
|  | 5b41f57423 | ||
|  | 0bb7db0b44 | ||
|  | 4d61b9937b | ||
|  | 68605800af | ||
|  | c49778c254 | ||
|  | f02c7138ea | ||
|  | ca3228855a | 
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -8,3 +8,4 @@ build | |||||||
| logs | logs | ||||||
| data | data | ||||||
| /web/node_modules | /web/node_modules | ||||||
|  | cmd.md | ||||||
| @@ -81,11 +81,12 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用  | |||||||
|    + [x] [Groq](https://wow.groq.com/) |    + [x] [Groq](https://wow.groq.com/) | ||||||
|    + [x] [Ollama](https://github.com/ollama/ollama) |    + [x] [Ollama](https://github.com/ollama/ollama) | ||||||
|    + [x] [零一万物](https://platform.lingyiwanwu.com/) |    + [x] [零一万物](https://platform.lingyiwanwu.com/) | ||||||
|  |    + [x] [阶跃星辰](https://platform.stepfun.com/) | ||||||
| 2. 支持配置镜像以及众多[第三方代理服务](https://iamazing.cn/page/openai-api-third-party-services)。 | 2. 支持配置镜像以及众多[第三方代理服务](https://iamazing.cn/page/openai-api-third-party-services)。 | ||||||
| 3. 支持通过**负载均衡**的方式访问多个渠道。 | 3. 支持通过**负载均衡**的方式访问多个渠道。 | ||||||
| 4. 支持 **stream 模式**,可以通过流式传输实现打字机效果。 | 4. 支持 **stream 模式**,可以通过流式传输实现打字机效果。 | ||||||
| 5. 支持**多机部署**,[详见此处](#多机部署)。 | 5. 支持**多机部署**,[详见此处](#多机部署)。 | ||||||
| 6. 支持**令牌管理**,设置令牌的过期时间和额度。 | 6. 支持**令牌管理**,设置令牌的过期时间、额度、允许的 IP 范围以及允许的模型访问。 | ||||||
| 7. 支持**兑换码管理**,支持批量生成和导出兑换码,可使用兑换码为账户进行充值。 | 7. 支持**兑换码管理**,支持批量生成和导出兑换码,可使用兑换码为账户进行充值。 | ||||||
| 8. 支持**渠道管理**,批量创建渠道。 | 8. 支持**渠道管理**,批量创建渠道。 | ||||||
| 9. 支持**用户分组**以及**渠道分组**,支持为不同分组设置不同的倍率。 | 9. 支持**用户分组**以及**渠道分组**,支持为不同分组设置不同的倍率。 | ||||||
| @@ -101,15 +102,15 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用  | |||||||
| 19. 支持丰富的**自定义**设置, | 19. 支持丰富的**自定义**设置, | ||||||
|     1. 支持自定义系统名称,logo 以及页脚。 |     1. 支持自定义系统名称,logo 以及页脚。 | ||||||
|     2. 支持自定义首页和关于页面,可以选择使用 HTML & Markdown 代码进行自定义,或者使用一个单独的网页通过 iframe 嵌入。 |     2. 支持自定义首页和关于页面,可以选择使用 HTML & Markdown 代码进行自定义,或者使用一个单独的网页通过 iframe 嵌入。 | ||||||
| 20. 支持通过系统访问令牌访问管理 API(bearer token,用以替代 cookie,你可以自行抓包来查看 API 的用法)。 | 20. 支持通过系统访问令牌调用管理 API,进而**在无需二开的情况下扩展和自定义** One API 的功能,详情请参考此处 [API 文档](./docs/API.md)。。 | ||||||
| 21. 支持 Cloudflare Turnstile 用户校验。 | 21. 支持 Cloudflare Turnstile 用户校验。 | ||||||
| 22. 支持用户管理,支持**多种用户登录注册方式**: | 22. 支持用户管理,支持**多种用户登录注册方式**: | ||||||
|     + 邮箱登录注册(支持注册邮箱白名单)以及通过邮箱进行密码重置。 |     + 邮箱登录注册(支持注册邮箱白名单)以及通过邮箱进行密码重置。 | ||||||
|  |     + 支持使用飞书进行授权登录。 | ||||||
|     + [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))。 | ||||||
| 23. 支持主题切换,设置环境变量 `THEME` 即可,默认为 `default`,欢迎 PR 更多主题,具体参考[此处](./web/README.md)。 | 23. 支持主题切换,设置环境变量 `THEME` 即可,默认为 `default`,欢迎 PR 更多主题,具体参考[此处](./web/README.md)。 | ||||||
| 24. 配合 [Message Pusher](https://github.com/songquanpeng/message-pusher) 可将报警信息推送到多种 App 上。 | 24. 配合 [Message Pusher](https://github.com/songquanpeng/message-pusher) 可将报警信息推送到多种 App 上。 | ||||||
| 25. 支持**扩展**,详情请参考此处 [API 文档](./docs/API.md)。 |  | ||||||
|  |  | ||||||
| ## 部署 | ## 部署 | ||||||
| ### 基于 Docker 进行部署 | ### 基于 Docker 进行部署 | ||||||
|   | |||||||
| @@ -66,6 +66,9 @@ var SMTPToken = "" | |||||||
| var GitHubClientId = "" | var GitHubClientId = "" | ||||||
| var GitHubClientSecret = "" | var GitHubClientSecret = "" | ||||||
|  |  | ||||||
|  | var LarkClientId = "" | ||||||
|  | var LarkClientSecret = "" | ||||||
|  |  | ||||||
| var WeChatServerAddress = "" | var WeChatServerAddress = "" | ||||||
| var WeChatServerToken = "" | var WeChatServerToken = "" | ||||||
| var WeChatAccountQRCodeImageURL = "" | var WeChatAccountQRCodeImageURL = "" | ||||||
|   | |||||||
| @@ -71,6 +71,7 @@ const ( | |||||||
| 	ChannelTypeGroq | 	ChannelTypeGroq | ||||||
| 	ChannelTypeOllama | 	ChannelTypeOllama | ||||||
| 	ChannelTypeLingYiWanWu | 	ChannelTypeLingYiWanWu | ||||||
|  | 	ChannelTypeStepFun | ||||||
|  |  | ||||||
| 	ChannelTypeDummy | 	ChannelTypeDummy | ||||||
| ) | ) | ||||||
| @@ -108,6 +109,7 @@ var ChannelBaseURLs = []string{ | |||||||
| 	"https://api.groq.com/openai",               // 29 | 	"https://api.groq.com/openai",               // 29 | ||||||
| 	"http://localhost:11434",                    // 30 | 	"http://localhost:11434",                    // 30 | ||||||
| 	"https://api.lingyiwanwu.com",               // 31 | 	"https://api.lingyiwanwu.com",               // 31 | ||||||
|  | 	"https://api.stepfun.com",                   // 32 | ||||||
| } | } | ||||||
|  |  | ||||||
| const ( | const ( | ||||||
|   | |||||||
							
								
								
									
										25
									
								
								common/network/ip.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								common/network/ip.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | |||||||
|  | package network | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"fmt" | ||||||
|  | 	"github.com/songquanpeng/one-api/common/logger" | ||||||
|  | 	"net" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func IsValidSubnet(subnet string) error { | ||||||
|  | 	_, _, err := net.ParseCIDR(subnet) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("failed to parse subnet: %w", err) | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func IsIpInSubnet(ctx context.Context, ip string, subnet string) bool { | ||||||
|  | 	_, ipNet, err := net.ParseCIDR(subnet) | ||||||
|  | 	if err != nil { | ||||||
|  | 		logger.Errorf(ctx, "failed to parse subnet: %s", err.Error()) | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | 	return ipNet.Contains(net.ParseIP(ip)) | ||||||
|  | } | ||||||
							
								
								
									
										19
									
								
								common/network/ip_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								common/network/ip_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | |||||||
|  | package network | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"testing" | ||||||
|  |  | ||||||
|  | 	. "github.com/smartystreets/goconvey/convey" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestIsIpInSubnet(t *testing.T) { | ||||||
|  | 	ctx := context.Background() | ||||||
|  | 	ip1 := "192.168.0.5" | ||||||
|  | 	ip2 := "125.216.250.89" | ||||||
|  | 	subnet := "192.168.0.0/24" | ||||||
|  | 	Convey("TestIsIpInSubnet", t, func() { | ||||||
|  | 		So(IsIpInSubnet(ctx, ip1, subnet), ShouldBeTrue) | ||||||
|  | 		So(IsIpInSubnet(ctx, ip2, subnet), ShouldBeFalse) | ||||||
|  | 	}) | ||||||
|  | } | ||||||
| @@ -1,4 +1,4 @@ | |||||||
| package controller | package auth | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"bytes" | 	"bytes" | ||||||
| @@ -11,6 +11,7 @@ import ( | |||||||
| 	"github.com/songquanpeng/one-api/common/config" | 	"github.com/songquanpeng/one-api/common/config" | ||||||
| 	"github.com/songquanpeng/one-api/common/helper" | 	"github.com/songquanpeng/one-api/common/helper" | ||||||
| 	"github.com/songquanpeng/one-api/common/logger" | 	"github.com/songquanpeng/one-api/common/logger" | ||||||
|  | 	"github.com/songquanpeng/one-api/controller" | ||||||
| 	"github.com/songquanpeng/one-api/model" | 	"github.com/songquanpeng/one-api/model" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| @@ -159,7 +160,7 @@ func GitHubOAuth(c *gin.Context) { | |||||||
| 		}) | 		}) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	setupLogin(&user, c) | 	controller.SetupLogin(&user, c) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func GitHubBind(c *gin.Context) { | func GitHubBind(c *gin.Context) { | ||||||
							
								
								
									
										201
									
								
								controller/auth/lark.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										201
									
								
								controller/auth/lark.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,201 @@ | |||||||
|  | package auth | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"github.com/gin-contrib/sessions" | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
|  | 	"github.com/songquanpeng/one-api/common" | ||||||
|  | 	"github.com/songquanpeng/one-api/common/config" | ||||||
|  | 	"github.com/songquanpeng/one-api/common/logger" | ||||||
|  | 	"github.com/songquanpeng/one-api/controller" | ||||||
|  | 	"github.com/songquanpeng/one-api/model" | ||||||
|  | 	"net/http" | ||||||
|  | 	"strconv" | ||||||
|  | 	"time" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type LarkOAuthResponse struct { | ||||||
|  | 	AccessToken string `json:"access_token"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type LarkUser struct { | ||||||
|  | 	Name   string `json:"name"` | ||||||
|  | 	OpenID string `json:"open_id"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func getLarkUserInfoByCode(code string) (*LarkUser, error) { | ||||||
|  | 	if code == "" { | ||||||
|  | 		return nil, errors.New("无效的参数") | ||||||
|  | 	} | ||||||
|  | 	values := map[string]string{ | ||||||
|  | 		"client_id":     config.LarkClientId, | ||||||
|  | 		"client_secret": config.LarkClientSecret, | ||||||
|  | 		"code":          code, | ||||||
|  | 		"grant_type":    "authorization_code", | ||||||
|  | 		"redirect_uri":  fmt.Sprintf("%s/oauth/lark", config.ServerAddress), | ||||||
|  | 	} | ||||||
|  | 	jsonData, err := json.Marshal(values) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	req, err := http.NewRequest("POST", "https://passport.feishu.cn/suite/passport/oauth/token", bytes.NewBuffer(jsonData)) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	req.Header.Set("Content-Type", "application/json") | ||||||
|  | 	req.Header.Set("Accept", "application/json") | ||||||
|  | 	client := http.Client{ | ||||||
|  | 		Timeout: 5 * time.Second, | ||||||
|  | 	} | ||||||
|  | 	res, err := client.Do(req) | ||||||
|  | 	if err != nil { | ||||||
|  | 		logger.SysLog(err.Error()) | ||||||
|  | 		return nil, errors.New("无法连接至飞书服务器,请稍后重试!") | ||||||
|  | 	} | ||||||
|  | 	defer res.Body.Close() | ||||||
|  | 	var oAuthResponse LarkOAuthResponse | ||||||
|  | 	err = json.NewDecoder(res.Body).Decode(&oAuthResponse) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	req, err = http.NewRequest("GET", "https://passport.feishu.cn/suite/passport/oauth/userinfo", nil) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", oAuthResponse.AccessToken)) | ||||||
|  | 	res2, err := client.Do(req) | ||||||
|  | 	if err != nil { | ||||||
|  | 		logger.SysLog(err.Error()) | ||||||
|  | 		return nil, errors.New("无法连接至飞书服务器,请稍后重试!") | ||||||
|  | 	} | ||||||
|  | 	var larkUser LarkUser | ||||||
|  | 	err = json.NewDecoder(res2.Body).Decode(&larkUser) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return &larkUser, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func LarkOAuth(c *gin.Context) { | ||||||
|  | 	session := sessions.Default(c) | ||||||
|  | 	state := c.Query("state") | ||||||
|  | 	if state == "" || session.Get("oauth_state") == nil || state != session.Get("oauth_state").(string) { | ||||||
|  | 		c.JSON(http.StatusForbidden, gin.H{ | ||||||
|  | 			"success": false, | ||||||
|  | 			"message": "state is empty or not same", | ||||||
|  | 		}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	username := session.Get("username") | ||||||
|  | 	if username != nil { | ||||||
|  | 		LarkBind(c) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	code := c.Query("code") | ||||||
|  | 	larkUser, err := getLarkUserInfoByCode(code) | ||||||
|  | 	if err != nil { | ||||||
|  | 		c.JSON(http.StatusOK, gin.H{ | ||||||
|  | 			"success": false, | ||||||
|  | 			"message": err.Error(), | ||||||
|  | 		}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	user := model.User{ | ||||||
|  | 		LarkId: larkUser.OpenID, | ||||||
|  | 	} | ||||||
|  | 	if model.IsLarkIdAlreadyTaken(user.LarkId) { | ||||||
|  | 		err := user.FillUserByLarkId() | ||||||
|  | 		if err != nil { | ||||||
|  | 			c.JSON(http.StatusOK, gin.H{ | ||||||
|  | 				"success": false, | ||||||
|  | 				"message": err.Error(), | ||||||
|  | 			}) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		if config.RegisterEnabled { | ||||||
|  | 			user.Username = "lark_" + strconv.Itoa(model.GetMaxUserId()+1) | ||||||
|  | 			if larkUser.Name != "" { | ||||||
|  | 				user.DisplayName = larkUser.Name | ||||||
|  | 			} else { | ||||||
|  | 				user.DisplayName = "Lark User" | ||||||
|  | 			} | ||||||
|  | 			user.Role = common.RoleCommonUser | ||||||
|  | 			user.Status = common.UserStatusEnabled | ||||||
|  |  | ||||||
|  | 			if err := user.Insert(0); err != nil { | ||||||
|  | 				c.JSON(http.StatusOK, gin.H{ | ||||||
|  | 					"success": false, | ||||||
|  | 					"message": err.Error(), | ||||||
|  | 				}) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 		} else { | ||||||
|  | 			c.JSON(http.StatusOK, gin.H{ | ||||||
|  | 				"success": false, | ||||||
|  | 				"message": "管理员关闭了新用户注册", | ||||||
|  | 			}) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if user.Status != common.UserStatusEnabled { | ||||||
|  | 		c.JSON(http.StatusOK, gin.H{ | ||||||
|  | 			"message": "用户已被封禁", | ||||||
|  | 			"success": false, | ||||||
|  | 		}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	controller.SetupLogin(&user, c) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func LarkBind(c *gin.Context) { | ||||||
|  | 	code := c.Query("code") | ||||||
|  | 	larkUser, err := getLarkUserInfoByCode(code) | ||||||
|  | 	if err != nil { | ||||||
|  | 		c.JSON(http.StatusOK, gin.H{ | ||||||
|  | 			"success": false, | ||||||
|  | 			"message": err.Error(), | ||||||
|  | 		}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	user := model.User{ | ||||||
|  | 		LarkId: larkUser.OpenID, | ||||||
|  | 	} | ||||||
|  | 	if model.IsLarkIdAlreadyTaken(user.LarkId) { | ||||||
|  | 		c.JSON(http.StatusOK, gin.H{ | ||||||
|  | 			"success": false, | ||||||
|  | 			"message": "该飞书账户已被绑定", | ||||||
|  | 		}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	session := sessions.Default(c) | ||||||
|  | 	id := session.Get("id") | ||||||
|  | 	// id := c.GetInt("id")  // critical bug! | ||||||
|  | 	user.Id = id.(int) | ||||||
|  | 	err = user.FillUserById() | ||||||
|  | 	if err != nil { | ||||||
|  | 		c.JSON(http.StatusOK, gin.H{ | ||||||
|  | 			"success": false, | ||||||
|  | 			"message": err.Error(), | ||||||
|  | 		}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	user.LarkId = larkUser.OpenID | ||||||
|  | 	err = user.Update(false) | ||||||
|  | 	if err != nil { | ||||||
|  | 		c.JSON(http.StatusOK, gin.H{ | ||||||
|  | 			"success": false, | ||||||
|  | 			"message": err.Error(), | ||||||
|  | 		}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	c.JSON(http.StatusOK, gin.H{ | ||||||
|  | 		"success": true, | ||||||
|  | 		"message": "bind", | ||||||
|  | 	}) | ||||||
|  | 	return | ||||||
|  | } | ||||||
| @@ -1,4 +1,4 @@ | |||||||
| package controller | package auth | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| @@ -7,6 +7,7 @@ import ( | |||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/songquanpeng/one-api/common" | 	"github.com/songquanpeng/one-api/common" | ||||||
| 	"github.com/songquanpeng/one-api/common/config" | 	"github.com/songquanpeng/one-api/common/config" | ||||||
|  | 	"github.com/songquanpeng/one-api/controller" | ||||||
| 	"github.com/songquanpeng/one-api/model" | 	"github.com/songquanpeng/one-api/model" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| @@ -109,7 +110,7 @@ func WeChatAuth(c *gin.Context) { | |||||||
| 		}) | 		}) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	setupLogin(&user, c) | 	controller.SetupLogin(&user, c) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func WeChatBind(c *gin.Context) { | func WeChatBind(c *gin.Context) { | ||||||
| @@ -23,6 +23,7 @@ func GetStatus(c *gin.Context) { | |||||||
| 			"email_verification":  config.EmailVerificationEnabled, | 			"email_verification":  config.EmailVerificationEnabled, | ||||||
| 			"github_oauth":        config.GitHubOAuthEnabled, | 			"github_oauth":        config.GitHubOAuthEnabled, | ||||||
| 			"github_client_id":    config.GitHubClientId, | 			"github_client_id":    config.GitHubClientId, | ||||||
|  | 			"lark_client_id":      config.LarkClientId, | ||||||
| 			"system_name":         config.SystemName, | 			"system_name":         config.SystemName, | ||||||
| 			"logo":                config.Logo, | 			"logo":                config.Logo, | ||||||
| 			"footer_html":         config.Footer, | 			"footer_html":         config.Footer, | ||||||
|   | |||||||
| @@ -1,10 +1,12 @@ | |||||||
| package controller | package controller | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"fmt" | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/songquanpeng/one-api/common" | 	"github.com/songquanpeng/one-api/common" | ||||||
| 	"github.com/songquanpeng/one-api/common/config" | 	"github.com/songquanpeng/one-api/common/config" | ||||||
| 	"github.com/songquanpeng/one-api/common/helper" | 	"github.com/songquanpeng/one-api/common/helper" | ||||||
|  | 	"github.com/songquanpeng/one-api/common/network" | ||||||
| 	"github.com/songquanpeng/one-api/model" | 	"github.com/songquanpeng/one-api/model" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| @@ -104,6 +106,19 @@ func GetTokenStatus(c *gin.Context) { | |||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func validateToken(c *gin.Context, token model.Token) error { | ||||||
|  | 	if len(token.Name) > 30 { | ||||||
|  | 		return fmt.Errorf("令牌名称过长") | ||||||
|  | 	} | ||||||
|  | 	if token.Subnet != nil && *token.Subnet != "" { | ||||||
|  | 		err := network.IsValidSubnet(*token.Subnet) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return fmt.Errorf("无效的网段:%s", err.Error()) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
| func AddToken(c *gin.Context) { | func AddToken(c *gin.Context) { | ||||||
| 	token := model.Token{} | 	token := model.Token{} | ||||||
| 	err := c.ShouldBindJSON(&token) | 	err := c.ShouldBindJSON(&token) | ||||||
| @@ -114,13 +129,15 @@ func AddToken(c *gin.Context) { | |||||||
| 		}) | 		}) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	if len(token.Name) > 30 { | 	err = validateToken(c, token) | ||||||
|  | 	if err != nil { | ||||||
| 		c.JSON(http.StatusOK, gin.H{ | 		c.JSON(http.StatusOK, gin.H{ | ||||||
| 			"success": false, | 			"success": false, | ||||||
| 			"message": "令牌名称过长", | 			"message": fmt.Sprintf("参数错误:%s", err.Error()), | ||||||
| 		}) | 		}) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	cleanToken := model.Token{ | 	cleanToken := model.Token{ | ||||||
| 		UserId:         c.GetInt("id"), | 		UserId:         c.GetInt("id"), | ||||||
| 		Name:           token.Name, | 		Name:           token.Name, | ||||||
| @@ -131,6 +148,7 @@ func AddToken(c *gin.Context) { | |||||||
| 		RemainQuota:    token.RemainQuota, | 		RemainQuota:    token.RemainQuota, | ||||||
| 		UnlimitedQuota: token.UnlimitedQuota, | 		UnlimitedQuota: token.UnlimitedQuota, | ||||||
| 		Models:         token.Models, | 		Models:         token.Models, | ||||||
|  | 		Subnet:         token.Subnet, | ||||||
| 	} | 	} | ||||||
| 	err = cleanToken.Insert() | 	err = cleanToken.Insert() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @@ -178,10 +196,11 @@ func UpdateToken(c *gin.Context) { | |||||||
| 		}) | 		}) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	if len(token.Name) > 30 { | 	err = validateToken(c, token) | ||||||
|  | 	if err != nil { | ||||||
| 		c.JSON(http.StatusOK, gin.H{ | 		c.JSON(http.StatusOK, gin.H{ | ||||||
| 			"success": false, | 			"success": false, | ||||||
| 			"message": "令牌名称过长", | 			"message": fmt.Sprintf("参数错误:%s", err.Error()), | ||||||
| 		}) | 		}) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| @@ -218,6 +237,7 @@ func UpdateToken(c *gin.Context) { | |||||||
| 		cleanToken.RemainQuota = token.RemainQuota | 		cleanToken.RemainQuota = token.RemainQuota | ||||||
| 		cleanToken.UnlimitedQuota = token.UnlimitedQuota | 		cleanToken.UnlimitedQuota = token.UnlimitedQuota | ||||||
| 		cleanToken.Models = token.Models | 		cleanToken.Models = token.Models | ||||||
|  | 		cleanToken.Subnet = token.Subnet | ||||||
| 	} | 	} | ||||||
| 	err = cleanToken.Update() | 	err = cleanToken.Update() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
|   | |||||||
| @@ -58,11 +58,11 @@ func Login(c *gin.Context) { | |||||||
| 		}) | 		}) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	setupLogin(&user, c) | 	SetupLogin(&user, c) | ||||||
| } | } | ||||||
|  |  | ||||||
| // setup session & cookies and then return user info | // setup session & cookies and then return user info | ||||||
| func setupLogin(user *model.User, c *gin.Context) { | func SetupLogin(user *model.User, c *gin.Context) { | ||||||
| 	session := sessions.Default(c) | 	session := sessions.Default(c) | ||||||
| 	session.Set("id", user.Id) | 	session.Set("id", user.Id) | ||||||
| 	session.Set("username", user.Username) | 	session.Set("username", user.Username) | ||||||
|   | |||||||
							
								
								
									
										11
									
								
								docs/API.md
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								docs/API.md
									
									
									
									
									
								
							| @@ -41,4 +41,13 @@ One API 使用 JSON 格式进行请求和响应。 | |||||||
|   "quota": 100000, |   "quota": 100000, | ||||||
|   "remark": "充值 100000 额度" |   "remark": "充值 100000 额度" | ||||||
| } | } | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
|  | ## 其他 | ||||||
|  | ### 充值链接上的附加参数 | ||||||
|  | One API 会在用户点击充值按钮的时候,将用户的信息和充值信息附加在链接上,例如: | ||||||
|  | `https://example.com?username=root&user_id=1&transaction_id=4b3eed80-55d5-443f-bd44-fb18c648c837` | ||||||
|  |  | ||||||
|  | 你可以通过解析链接上的参数来获取用户信息和充值信息,然后调用 API 来为用户充值。 | ||||||
|  |  | ||||||
|  | 注意,不是所有主题都支持该功能,欢迎 PR 补齐。 | ||||||
							
								
								
									
										4
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								go.mod
									
									
									
									
									
								
							| @@ -15,6 +15,7 @@ require ( | |||||||
| 	github.com/google/uuid v1.3.0 | 	github.com/google/uuid v1.3.0 | ||||||
| 	github.com/gorilla/websocket v1.5.0 | 	github.com/gorilla/websocket v1.5.0 | ||||||
| 	github.com/pkoukk/tiktoken-go v0.1.5 | 	github.com/pkoukk/tiktoken-go v0.1.5 | ||||||
|  | 	github.com/smartystreets/goconvey v1.8.1 | ||||||
| 	github.com/stretchr/testify v1.8.3 | 	github.com/stretchr/testify v1.8.3 | ||||||
| 	golang.org/x/crypto v0.17.0 | 	golang.org/x/crypto v0.17.0 | ||||||
| 	golang.org/x/image v0.14.0 | 	golang.org/x/image v0.14.0 | ||||||
| @@ -37,6 +38,7 @@ require ( | |||||||
| 	github.com/go-playground/universal-translator v0.18.1 // indirect | 	github.com/go-playground/universal-translator v0.18.1 // indirect | ||||||
| 	github.com/go-sql-driver/mysql v1.6.0 // indirect | 	github.com/go-sql-driver/mysql v1.6.0 // indirect | ||||||
| 	github.com/goccy/go-json v0.10.2 // indirect | 	github.com/goccy/go-json v0.10.2 // indirect | ||||||
|  | 	github.com/gopherjs/gopherjs v1.17.2 // indirect | ||||||
| 	github.com/gorilla/context v1.1.1 // indirect | 	github.com/gorilla/context v1.1.1 // indirect | ||||||
| 	github.com/gorilla/securecookie v1.1.1 // indirect | 	github.com/gorilla/securecookie v1.1.1 // indirect | ||||||
| 	github.com/gorilla/sessions v1.2.1 // indirect | 	github.com/gorilla/sessions v1.2.1 // indirect | ||||||
| @@ -47,6 +49,7 @@ require ( | |||||||
| 	github.com/jinzhu/inflection v1.0.0 // indirect | 	github.com/jinzhu/inflection v1.0.0 // indirect | ||||||
| 	github.com/jinzhu/now v1.1.5 // indirect | 	github.com/jinzhu/now v1.1.5 // indirect | ||||||
| 	github.com/json-iterator/go v1.1.12 // indirect | 	github.com/json-iterator/go v1.1.12 // indirect | ||||||
|  | 	github.com/jtolds/gls v4.20.0+incompatible // indirect | ||||||
| 	github.com/klauspost/cpuid/v2 v2.2.4 // indirect | 	github.com/klauspost/cpuid/v2 v2.2.4 // indirect | ||||||
| 	github.com/leodido/go-urn v1.2.4 // indirect | 	github.com/leodido/go-urn v1.2.4 // indirect | ||||||
| 	github.com/mattn/go-isatty v0.0.19 // indirect | 	github.com/mattn/go-isatty v0.0.19 // indirect | ||||||
| @@ -55,6 +58,7 @@ require ( | |||||||
| 	github.com/modern-go/reflect2 v1.0.2 // indirect | 	github.com/modern-go/reflect2 v1.0.2 // indirect | ||||||
| 	github.com/pelletier/go-toml/v2 v2.0.8 // indirect | 	github.com/pelletier/go-toml/v2 v2.0.8 // indirect | ||||||
| 	github.com/pmezard/go-difflib v1.0.0 // indirect | 	github.com/pmezard/go-difflib v1.0.0 // indirect | ||||||
|  | 	github.com/smarty/assertions v1.15.0 // indirect | ||||||
| 	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect | 	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect | ||||||
| 	github.com/ugorji/go/codec v1.2.11 // indirect | 	github.com/ugorji/go/codec v1.2.11 // indirect | ||||||
| 	golang.org/x/arch v0.3.0 // indirect | 	golang.org/x/arch v0.3.0 // indirect | ||||||
|   | |||||||
							
								
								
									
										12
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								go.sum
									
									
									
									
									
								
							| @@ -56,11 +56,13 @@ github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keL | |||||||
| github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= | github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= | ||||||
| github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= | ||||||
| github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= | ||||||
| github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= |  | ||||||
| github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= | ||||||
|  | github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= | ||||||
| github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= | ||||||
| github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= | ||||||
| github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | ||||||
|  | github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= | ||||||
|  | github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= | ||||||
| github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= | github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= | ||||||
| github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= | github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= | ||||||
| github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= | github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= | ||||||
| @@ -85,6 +87,8 @@ github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/ | |||||||
| github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= | ||||||
| github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= | ||||||
| github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= | ||||||
|  | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= | ||||||
|  | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= | ||||||
| github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= | ||||||
| github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= | github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= | ||||||
| github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= | github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= | ||||||
| @@ -127,6 +131,10 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN | |||||||
| github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= | ||||||
| github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= | github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= | ||||||
| github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= | github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= | ||||||
|  | github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY= | ||||||
|  | github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec= | ||||||
|  | github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY= | ||||||
|  | github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60= | ||||||
| github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||||||
| github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= | ||||||
| github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= | ||||||
| @@ -177,8 +185,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | |||||||
| golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= | ||||||
| golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= | ||||||
| golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | ||||||
| golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= |  | ||||||
| golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||||
|  | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= | ||||||
| google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= | ||||||
| google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= | google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= | ||||||
| google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ import ( | |||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/songquanpeng/one-api/common" | 	"github.com/songquanpeng/one-api/common" | ||||||
| 	"github.com/songquanpeng/one-api/common/blacklist" | 	"github.com/songquanpeng/one-api/common/blacklist" | ||||||
|  | 	"github.com/songquanpeng/one-api/common/network" | ||||||
| 	"github.com/songquanpeng/one-api/model" | 	"github.com/songquanpeng/one-api/model" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"strings" | 	"strings" | ||||||
| @@ -89,6 +90,7 @@ func RootAuth() func(c *gin.Context) { | |||||||
|  |  | ||||||
| func TokenAuth() func(c *gin.Context) { | func TokenAuth() func(c *gin.Context) { | ||||||
| 	return func(c *gin.Context) { | 	return func(c *gin.Context) { | ||||||
|  | 		ctx := c.Request.Context() | ||||||
| 		key := c.Request.Header.Get("Authorization") | 		key := c.Request.Header.Get("Authorization") | ||||||
| 		key = strings.TrimPrefix(key, "Bearer ") | 		key = strings.TrimPrefix(key, "Bearer ") | ||||||
| 		key = strings.TrimPrefix(key, "sk-") | 		key = strings.TrimPrefix(key, "sk-") | ||||||
| @@ -99,6 +101,12 @@ func TokenAuth() func(c *gin.Context) { | |||||||
| 			abortWithMessage(c, http.StatusUnauthorized, err.Error()) | 			abortWithMessage(c, http.StatusUnauthorized, err.Error()) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|  | 		if token.Subnet != nil && *token.Subnet != "" { | ||||||
|  | 			if !network.IsIpInSubnet(ctx, c.ClientIP(), *token.Subnet) { | ||||||
|  | 				abortWithMessage(c, http.StatusForbidden, fmt.Sprintf("该令牌只能在指定网段使用:%s,当前 ip:%s", *token.Subnet, c.ClientIP())) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
| 		userEnabled, err := model.CacheIsUserEnabled(token.UserId) | 		userEnabled, err := model.CacheIsUserEnabled(token.UserId) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			abortWithMessage(c, http.StatusInternalServerError, err.Error()) | 			abortWithMessage(c, http.StatusInternalServerError, err.Error()) | ||||||
| @@ -109,7 +117,7 @@ func TokenAuth() func(c *gin.Context) { | |||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 		requestModel, err := getRequestModel(c) | 		requestModel, err := getRequestModel(c) | ||||||
| 		if err != nil { | 		if err != nil && !strings.HasPrefix(c.Request.URL.Path, "/v1/models") { | ||||||
| 			abortWithMessage(c, http.StatusBadRequest, err.Error()) | 			abortWithMessage(c, http.StatusBadRequest, err.Error()) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|   | |||||||
| @@ -172,6 +172,10 @@ func updateOptionMap(key string, value string) (err error) { | |||||||
| 		config.GitHubClientId = value | 		config.GitHubClientId = value | ||||||
| 	case "GitHubClientSecret": | 	case "GitHubClientSecret": | ||||||
| 		config.GitHubClientSecret = value | 		config.GitHubClientSecret = value | ||||||
|  | 	case "LarkClientId": | ||||||
|  | 		config.LarkClientId = value | ||||||
|  | 	case "LarkClientSecret": | ||||||
|  | 		config.LarkClientSecret = value | ||||||
| 	case "Footer": | 	case "Footer": | ||||||
| 		config.Footer = value | 		config.Footer = value | ||||||
| 	case "SystemName": | 	case "SystemName": | ||||||
|   | |||||||
| @@ -23,7 +23,8 @@ type Token struct { | |||||||
| 	RemainQuota    int64   `json:"remain_quota" gorm:"bigint;default:0"` | 	RemainQuota    int64   `json:"remain_quota" gorm:"bigint;default:0"` | ||||||
| 	UnlimitedQuota bool    `json:"unlimited_quota" gorm:"default:false"` | 	UnlimitedQuota bool    `json:"unlimited_quota" gorm:"default:false"` | ||||||
| 	UsedQuota      int64   `json:"used_quota" gorm:"bigint;default:0"` // used quota | 	UsedQuota      int64   `json:"used_quota" gorm:"bigint;default:0"` // used quota | ||||||
| 	Models         *string `json:"models" gorm:"default:''"` | 	Models         *string `json:"models" gorm:"default:''"`           // allowed models | ||||||
|  | 	Subnet         *string `json:"subnet" gorm:"default:''"`           // allowed subnet | ||||||
| } | } | ||||||
|  |  | ||||||
| func GetAllUserTokens(userId int, startIdx int, num int, order string) ([]*Token, error) { | func GetAllUserTokens(userId int, startIdx int, num int, order string) ([]*Token, error) { | ||||||
| @@ -62,7 +63,7 @@ func ValidateUserToken(key string) (token *Token, err error) { | |||||||
| 		return nil, errors.New("令牌验证失败") | 		return nil, errors.New("令牌验证失败") | ||||||
| 	} | 	} | ||||||
| 	if token.Status == common.TokenStatusExhausted { | 	if token.Status == common.TokenStatusExhausted { | ||||||
| 		return nil, errors.New("该令牌额度已用尽") | 		return nil, fmt.Errorf("令牌 %s(#%d)额度已用尽", token.Name, token.Id) | ||||||
| 	} else if token.Status == common.TokenStatusExpired { | 	} else if token.Status == common.TokenStatusExpired { | ||||||
| 		return nil, errors.New("该令牌已过期") | 		return nil, errors.New("该令牌已过期") | ||||||
| 	} | 	} | ||||||
| @@ -122,7 +123,7 @@ func (token *Token) Insert() error { | |||||||
| // Update Make sure your token's fields is completed, because this will update non-zero values | // Update Make sure your token's fields is completed, because this will update non-zero values | ||||||
| func (token *Token) Update() error { | func (token *Token) Update() error { | ||||||
| 	var err error | 	var err error | ||||||
| 	err = DB.Model(token).Select("name", "status", "expired_time", "remain_quota", "unlimited_quota", "models").Updates(token).Error | 	err = DB.Model(token).Select("name", "status", "expired_time", "remain_quota", "unlimited_quota", "models", "subnet").Updates(token).Error | ||||||
| 	return err | 	return err | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -24,6 +24,7 @@ type User struct { | |||||||
| 	Email            string `json:"email" gorm:"index" validate:"max=50"` | 	Email            string `json:"email" gorm:"index" validate:"max=50"` | ||||||
| 	GitHubId         string `json:"github_id" gorm:"column:github_id;index"` | 	GitHubId         string `json:"github_id" gorm:"column:github_id;index"` | ||||||
| 	WeChatId         string `json:"wechat_id" gorm:"column:wechat_id;index"` | 	WeChatId         string `json:"wechat_id" gorm:"column:wechat_id;index"` | ||||||
|  | 	LarkId           string `json:"lark_id" gorm:"column:lark_id;index"` | ||||||
| 	VerificationCode string `json:"verification_code" gorm:"-:all"`                                    // this field is only for Email verification, don't save it to database! | 	VerificationCode string `json:"verification_code" gorm:"-:all"`                                    // this field is only for Email verification, don't save it to database! | ||||||
| 	AccessToken      string `json:"access_token" gorm:"type:char(32);column:access_token;uniqueIndex"` // this token is for system management | 	AccessToken      string `json:"access_token" gorm:"type:char(32);column:access_token;uniqueIndex"` // this token is for system management | ||||||
| 	Quota            int64  `json:"quota" gorm:"bigint;default:0"` | 	Quota            int64  `json:"quota" gorm:"bigint;default:0"` | ||||||
| @@ -41,21 +42,21 @@ func GetMaxUserId() int { | |||||||
| } | } | ||||||
|  |  | ||||||
| func GetAllUsers(startIdx int, num int, order string) (users []*User, err error) { | func GetAllUsers(startIdx int, num int, order string) (users []*User, err error) { | ||||||
|     query := DB.Limit(num).Offset(startIdx).Omit("password").Where("status != ?", common.UserStatusDeleted) | 	query := DB.Limit(num).Offset(startIdx).Omit("password").Where("status != ?", common.UserStatusDeleted) | ||||||
|      |  | ||||||
|     switch order { | 	switch order { | ||||||
|     case "quota": | 	case "quota": | ||||||
|         query = query.Order("quota desc") | 		query = query.Order("quota desc") | ||||||
|     case "used_quota": | 	case "used_quota": | ||||||
|         query = query.Order("used_quota desc") | 		query = query.Order("used_quota desc") | ||||||
|     case "request_count": | 	case "request_count": | ||||||
|         query = query.Order("request_count desc") | 		query = query.Order("request_count desc") | ||||||
|     default: | 	default: | ||||||
|         query = query.Order("id desc") | 		query = query.Order("id desc") | ||||||
|     } | 	} | ||||||
|      |  | ||||||
|     err = query.Find(&users).Error | 	err = query.Find(&users).Error | ||||||
|     return users, err | 	return users, err | ||||||
| } | } | ||||||
|  |  | ||||||
| func SearchUsers(keyword string) (users []*User, err error) { | func SearchUsers(keyword string) (users []*User, err error) { | ||||||
| @@ -206,6 +207,14 @@ func (user *User) FillUserByGitHubId() error { | |||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (user *User) FillUserByLarkId() error { | ||||||
|  | 	if user.LarkId == "" { | ||||||
|  | 		return errors.New("lark id 为空!") | ||||||
|  | 	} | ||||||
|  | 	DB.Where(User{LarkId: user.LarkId}).First(user) | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
| func (user *User) FillUserByWeChatId() error { | func (user *User) FillUserByWeChatId() error { | ||||||
| 	if user.WeChatId == "" { | 	if user.WeChatId == "" { | ||||||
| 		return errors.New("WeChat id 为空!") | 		return errors.New("WeChat id 为空!") | ||||||
| @@ -234,6 +243,10 @@ func IsGitHubIdAlreadyTaken(githubId string) bool { | |||||||
| 	return DB.Where("github_id = ?", githubId).Find(&User{}).RowsAffected == 1 | 	return DB.Where("github_id = ?", githubId).Find(&User{}).RowsAffected == 1 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func IsLarkIdAlreadyTaken(githubId string) bool { | ||||||
|  | 	return DB.Where("lark_id = ?", githubId).Find(&User{}).RowsAffected == 1 | ||||||
|  | } | ||||||
|  |  | ||||||
| func IsUsernameAlreadyTaken(username string) bool { | func IsUsernameAlreadyTaken(username string) bool { | ||||||
| 	return DB.Where("username = ?", username).Find(&User{}).RowsAffected == 1 | 	return DB.Where("username = ?", username).Find(&User{}).RowsAffected == 1 | ||||||
| } | } | ||||||
|   | |||||||
| @@ -9,6 +9,7 @@ import ( | |||||||
| 	"github.com/songquanpeng/one-api/relay/channel/minimax" | 	"github.com/songquanpeng/one-api/relay/channel/minimax" | ||||||
| 	"github.com/songquanpeng/one-api/relay/channel/mistral" | 	"github.com/songquanpeng/one-api/relay/channel/mistral" | ||||||
| 	"github.com/songquanpeng/one-api/relay/channel/moonshot" | 	"github.com/songquanpeng/one-api/relay/channel/moonshot" | ||||||
|  | 	"github.com/songquanpeng/one-api/relay/channel/stepfun" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var CompatibleChannels = []int{ | var CompatibleChannels = []int{ | ||||||
| @@ -20,6 +21,7 @@ var CompatibleChannels = []int{ | |||||||
| 	common.ChannelTypeMistral, | 	common.ChannelTypeMistral, | ||||||
| 	common.ChannelTypeGroq, | 	common.ChannelTypeGroq, | ||||||
| 	common.ChannelTypeLingYiWanWu, | 	common.ChannelTypeLingYiWanWu, | ||||||
|  | 	common.ChannelTypeStepFun, | ||||||
| } | } | ||||||
|  |  | ||||||
| func GetCompatibleChannelMeta(channelType int) (string, []string) { | func GetCompatibleChannelMeta(channelType int) (string, []string) { | ||||||
| @@ -40,6 +42,8 @@ func GetCompatibleChannelMeta(channelType int) (string, []string) { | |||||||
| 		return "groq", groq.ModelList | 		return "groq", groq.ModelList | ||||||
| 	case common.ChannelTypeLingYiWanWu: | 	case common.ChannelTypeLingYiWanWu: | ||||||
| 		return "lingyiwanwu", lingyiwanwu.ModelList | 		return "lingyiwanwu", lingyiwanwu.ModelList | ||||||
|  | 	case common.ChannelTypeStepFun: | ||||||
|  | 		return "stepfun", stepfun.ModelList | ||||||
| 	default: | 	default: | ||||||
| 		return "openai", ModelList | 		return "openai", ModelList | ||||||
| 	} | 	} | ||||||
|   | |||||||
							
								
								
									
										7
									
								
								relay/channel/stepfun/constants.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								relay/channel/stepfun/constants.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | |||||||
|  | package stepfun | ||||||
|  |  | ||||||
|  | var ModelList = []string{ | ||||||
|  | 	"step-1-32k", | ||||||
|  | 	"step-1v-32k", | ||||||
|  | 	"step-1-200k", | ||||||
|  | } | ||||||
| @@ -46,9 +46,9 @@ func ShouldDisableChannel(err *relaymodel.Error, statusCode int) bool { | |||||||
| 	} else if strings.HasPrefix(err.Message, "This organization has been disabled.") { | 	} else if strings.HasPrefix(err.Message, "This organization has been disabled.") { | ||||||
| 		return true | 		return true | ||||||
| 	} | 	} | ||||||
| 	if strings.Contains(err.Message, "quota") { | 	//if strings.Contains(err.Message, "quota") { | ||||||
| 		return true | 	//	return true | ||||||
| 	} | 	//} | ||||||
| 	if strings.Contains(err.Message, "credit") { | 	if strings.Contains(err.Message, "credit") { | ||||||
| 		return true | 		return true | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ package router | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"github.com/songquanpeng/one-api/controller" | 	"github.com/songquanpeng/one-api/controller" | ||||||
|  | 	"github.com/songquanpeng/one-api/controller/auth" | ||||||
| 	"github.com/songquanpeng/one-api/middleware" | 	"github.com/songquanpeng/one-api/middleware" | ||||||
|  |  | ||||||
| 	"github.com/gin-contrib/gzip" | 	"github.com/gin-contrib/gzip" | ||||||
| @@ -21,10 +22,11 @@ func SetApiRouter(router *gin.Engine) { | |||||||
| 		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) | ||||||
| 		apiRouter.GET("/oauth/github", middleware.CriticalRateLimit(), controller.GitHubOAuth) | 		apiRouter.GET("/oauth/github", middleware.CriticalRateLimit(), auth.GitHubOAuth) | ||||||
| 		apiRouter.GET("/oauth/state", middleware.CriticalRateLimit(), controller.GenerateOAuthCode) | 		apiRouter.GET("/oauth/lark", middleware.CriticalRateLimit(), auth.LarkOAuth) | ||||||
| 		apiRouter.GET("/oauth/wechat", middleware.CriticalRateLimit(), controller.WeChatAuth) | 		apiRouter.GET("/oauth/state", middleware.CriticalRateLimit(), auth.GenerateOAuthCode) | ||||||
| 		apiRouter.GET("/oauth/wechat/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.WeChatBind) | 		apiRouter.GET("/oauth/wechat", middleware.CriticalRateLimit(), auth.WeChatAuth) | ||||||
|  | 		apiRouter.GET("/oauth/wechat/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), auth.WeChatBind) | ||||||
| 		apiRouter.GET("/oauth/email/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.EmailBind) | 		apiRouter.GET("/oauth/email/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.EmailBind) | ||||||
| 		apiRouter.POST("/topup", middleware.AdminAuth(), controller.AdminTopUp) | 		apiRouter.POST("/topup", middleware.AdminAuth(), controller.AdminTopUp) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -2,6 +2,9 @@ | |||||||
|  |  | ||||||
| > 每个文件夹代表一个主题,欢迎提交你的主题 | > 每个文件夹代表一个主题,欢迎提交你的主题 | ||||||
|  |  | ||||||
|  | > [!WARNING] | ||||||
|  | > 不是每一个主题都及时同步了所有功能,由于精力有限,优先更新默认主题,其他主题欢迎 & 期待 PR | ||||||
|  |  | ||||||
| ## 提交新的主题 | ## 提交新的主题 | ||||||
|  |  | ||||||
| > 欢迎在页面底部保留你和 One API 的版权信息以及指向链接 | > 欢迎在页面底部保留你和 One API 的版权信息以及指向链接 | ||||||
|   | |||||||
| @@ -107,6 +107,12 @@ export const CHANNEL_OPTIONS = { | |||||||
|     value: 31, |     value: 31, | ||||||
|     color: 'primary' |     color: 'primary' | ||||||
|   }, |   }, | ||||||
|  |   32: { | ||||||
|  |     key: 32, | ||||||
|  |     text: '阶跃星辰', | ||||||
|  |     value: 32, | ||||||
|  |     color: 'primary' | ||||||
|  |   }, | ||||||
|   8: { |   8: { | ||||||
|     key: 8, |     key: 8, | ||||||
|     text: '自定义渠道', |     text: '自定义渠道', | ||||||
|   | |||||||
| @@ -18,7 +18,7 @@ export const snackbarConstants = { | |||||||
|     }, |     }, | ||||||
|     NOTICE: { |     NOTICE: { | ||||||
|       variant: 'info', |       variant: 'info', | ||||||
|       autoHideDuration: 20000 |       autoHideDuration: 7000 | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   Mobile: { |   Mobile: { | ||||||
|   | |||||||
| @@ -51,9 +51,9 @@ export function showError(error) { | |||||||
|  |  | ||||||
| export function showNotice(message, isHTML = false) { | export function showNotice(message, isHTML = false) { | ||||||
|   if (isHTML) { |   if (isHTML) { | ||||||
|     enqueueSnackbar(<SnackbarHTMLContent htmlContent={message} />, getSnackbarOptions('INFO')); |     enqueueSnackbar(<SnackbarHTMLContent htmlContent={message} />, getSnackbarOptions('NOTICE')); | ||||||
|   } else { |   } else { | ||||||
|     enqueueSnackbar(message, getSnackbarOptions('INFO')); |     enqueueSnackbar(message, getSnackbarOptions('NOTICE')); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -340,7 +340,9 @@ const EditModal = ({ open, channelId, onCancel, onOk }) => { | |||||||
|                     }, |                     }, | ||||||
|                   }} |                   }} | ||||||
|                 > |                 > | ||||||
|                   {Object.values(CHANNEL_OPTIONS).map((option) => { |                   {Object.values(CHANNEL_OPTIONS).sort((a, b) => { | ||||||
|  |                     return a.text.localeCompare(b.text) | ||||||
|  |                   }).map((option) => { | ||||||
|                     return ( |                     return ( | ||||||
|                       <MenuItem key={option.value} value={option.value}> |                       <MenuItem key={option.value} value={option.value}> | ||||||
|                         {option.text} |                         {option.text} | ||||||
|   | |||||||
| @@ -103,7 +103,7 @@ const EditModal = ({ open, tokenId, onCancel, onOk }) => { | |||||||
|           fontSize: "1.125rem", |           fontSize: "1.125rem", | ||||||
|         }} |         }} | ||||||
|       > |       > | ||||||
|         {tokenId ? "编辑Token" : "新建Token"} |         {tokenId ? "编辑令牌" : "新建令牌"} | ||||||
|       </DialogTitle> |       </DialogTitle> | ||||||
|       <Divider /> |       <Divider /> | ||||||
|       <DialogContent> |       <DialogContent> | ||||||
|   | |||||||
| @@ -24,6 +24,7 @@ import EditRedemption from './pages/Redemption/EditRedemption'; | |||||||
| import TopUp from './pages/TopUp'; | import TopUp from './pages/TopUp'; | ||||||
| import Log from './pages/Log'; | import Log from './pages/Log'; | ||||||
| import Chat from './pages/Chat'; | import Chat from './pages/Chat'; | ||||||
|  | import LarkOAuth from './components/LarkOAuth'; | ||||||
|  |  | ||||||
| const Home = lazy(() => import('./pages/Home')); | const Home = lazy(() => import('./pages/Home')); | ||||||
| const About = lazy(() => import('./pages/About')); | const About = lazy(() => import('./pages/About')); | ||||||
| @@ -239,6 +240,14 @@ function App() { | |||||||
|           </Suspense> |           </Suspense> | ||||||
|         } |         } | ||||||
|       /> |       /> | ||||||
|  |       <Route | ||||||
|  |         path='/oauth/lark' | ||||||
|  |         element={ | ||||||
|  |           <Suspense fallback={<Loading></Loading>}> | ||||||
|  |             <LarkOAuth /> | ||||||
|  |           </Suspense> | ||||||
|  |         } | ||||||
|  |       /> | ||||||
|       <Route |       <Route | ||||||
|         path='/setting' |         path='/setting' | ||||||
|         element={ |         element={ | ||||||
|   | |||||||
							
								
								
									
										58
									
								
								web/default/src/components/LarkOAuth.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								web/default/src/components/LarkOAuth.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | |||||||
|  | import React, { useContext, useEffect, useState } from 'react'; | ||||||
|  | import { Dimmer, Loader, Segment } from 'semantic-ui-react'; | ||||||
|  | import { useNavigate, useSearchParams } from 'react-router-dom'; | ||||||
|  | import { API, showError, showSuccess } from '../helpers'; | ||||||
|  | import { UserContext } from '../context/User'; | ||||||
|  |  | ||||||
|  | const LarkOAuth = () => { | ||||||
|  |   const [searchParams, setSearchParams] = useSearchParams(); | ||||||
|  |  | ||||||
|  |   const [userState, userDispatch] = useContext(UserContext); | ||||||
|  |   const [prompt, setPrompt] = useState('处理中...'); | ||||||
|  |   const [processing, setProcessing] = useState(true); | ||||||
|  |  | ||||||
|  |   let navigate = useNavigate(); | ||||||
|  |  | ||||||
|  |   const sendCode = async (code, state, count) => { | ||||||
|  |     const res = await API.get(`/api/oauth/lark?code=${code}&state=${state}`); | ||||||
|  |     const { success, message, data } = res.data; | ||||||
|  |     if (success) { | ||||||
|  |       if (message === 'bind') { | ||||||
|  |         showSuccess('绑定成功!'); | ||||||
|  |         navigate('/setting'); | ||||||
|  |       } else { | ||||||
|  |         userDispatch({ type: 'login', payload: data }); | ||||||
|  |         localStorage.setItem('user', JSON.stringify(data)); | ||||||
|  |         showSuccess('登录成功!'); | ||||||
|  |         navigate('/'); | ||||||
|  |       } | ||||||
|  |     } else { | ||||||
|  |       showError(message); | ||||||
|  |       if (count === 0) { | ||||||
|  |         setPrompt(`操作失败,重定向至登录界面中...`); | ||||||
|  |         navigate('/setting'); // in case this is failed to bind lark | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |       count++; | ||||||
|  |       setPrompt(`出现错误,第 ${count} 次重试中...`); | ||||||
|  |       await new Promise((resolve) => setTimeout(resolve, count * 2000)); | ||||||
|  |       await sendCode(code, state, count); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   useEffect(() => { | ||||||
|  |     let code = searchParams.get('code'); | ||||||
|  |     let state = searchParams.get('state'); | ||||||
|  |     sendCode(code, state, 0).then(); | ||||||
|  |   }, []); | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <Segment style={{ minHeight: '300px' }}> | ||||||
|  |       <Dimmer active inverted> | ||||||
|  |         <Loader size='large'>{prompt}</Loader> | ||||||
|  |       </Dimmer> | ||||||
|  |     </Segment> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default LarkOAuth; | ||||||
| @@ -3,7 +3,8 @@ import { Button, Divider, Form, Grid, Header, Image, Message, Modal, Segment } f | |||||||
| 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, getLogo, showError, showSuccess, showWarning } from '../helpers'; | import { API, getLogo, showError, showSuccess, showWarning } from '../helpers'; | ||||||
| import { onGitHubOAuthClicked } from './utils'; | import { onGitHubOAuthClicked, onLarkOAuthClicked } from './utils'; | ||||||
|  | import larkIcon from '../images/lark.svg'; | ||||||
|  |  | ||||||
| const LoginForm = () => { | const LoginForm = () => { | ||||||
|   const [inputs, setInputs] = useState({ |   const [inputs, setInputs] = useState({ | ||||||
| @@ -124,7 +125,7 @@ const LoginForm = () => { | |||||||
|             点击注册 |             点击注册 | ||||||
|           </Link> |           </Link> | ||||||
|         </Message> |         </Message> | ||||||
|         {status.github_oauth || status.wechat_login ? ( |         {status.github_oauth || status.wechat_login || status.lark_client_id ? ( | ||||||
|           <> |           <> | ||||||
|             <Divider horizontal>Or</Divider> |             <Divider horizontal>Or</Divider> | ||||||
|             {status.github_oauth ? ( |             {status.github_oauth ? ( | ||||||
| @@ -137,6 +138,18 @@ const LoginForm = () => { | |||||||
|             ) : ( |             ) : ( | ||||||
|               <></> |               <></> | ||||||
|             )} |             )} | ||||||
|  |             {status.lark_client_id ? ( | ||||||
|  |               <Button | ||||||
|  |                 // circular | ||||||
|  |                 color='' | ||||||
|  |                 onClick={() => onLarkOAuthClicked(status.lark_client_id)} | ||||||
|  |                 style={{ padding: 0, width: 36, height: 36 }} | ||||||
|  |               > | ||||||
|  |                 <img src={larkIcon} width={36} height={36} /> | ||||||
|  |               </Button> | ||||||
|  |             ) : ( | ||||||
|  |               <></> | ||||||
|  |             )} | ||||||
|             {status.wechat_login ? ( |             {status.wechat_login ? ( | ||||||
|               <Button |               <Button | ||||||
|                 circular |                 circular | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ import { Link, useNavigate } from 'react-router-dom'; | |||||||
| import { API, copy, showError, showInfo, showNotice, showSuccess } from '../helpers'; | import { API, copy, showError, showInfo, showNotice, showSuccess } from '../helpers'; | ||||||
| import Turnstile from 'react-turnstile'; | import Turnstile from 'react-turnstile'; | ||||||
| import { UserContext } from '../context/User'; | import { UserContext } from '../context/User'; | ||||||
| import { onGitHubOAuthClicked } from './utils'; | import { onGitHubOAuthClicked, onLarkOAuthClicked } from './utils'; | ||||||
|  |  | ||||||
| const PersonalSetting = () => { | const PersonalSetting = () => { | ||||||
|   const [userState, userDispatch] = useContext(UserContext); |   const [userState, userDispatch] = useContext(UserContext); | ||||||
| @@ -247,6 +247,11 @@ const PersonalSetting = () => { | |||||||
|           <Button onClick={()=>{onGitHubOAuthClicked(status.github_client_id)}}>绑定 GitHub 账号</Button> |           <Button onClick={()=>{onGitHubOAuthClicked(status.github_client_id)}}>绑定 GitHub 账号</Button> | ||||||
|         ) |         ) | ||||||
|       } |       } | ||||||
|  |       { | ||||||
|  |         status.lark_client_id && ( | ||||||
|  |           <Button onClick={()=>{onLarkOAuthClicked(status.lark_client_id)}}>绑定飞书账号</Button> | ||||||
|  |         ) | ||||||
|  |       } | ||||||
|       <Button |       <Button | ||||||
|         onClick={() => { |         onClick={() => { | ||||||
|           setShowEmailBindModal(true); |           setShowEmailBindModal(true); | ||||||
|   | |||||||
| @@ -10,6 +10,8 @@ const SystemSetting = () => { | |||||||
|     GitHubOAuthEnabled: '', |     GitHubOAuthEnabled: '', | ||||||
|     GitHubClientId: '', |     GitHubClientId: '', | ||||||
|     GitHubClientSecret: '', |     GitHubClientSecret: '', | ||||||
|  |     LarkClientId: '', | ||||||
|  |     LarkClientSecret: '', | ||||||
|     Notice: '', |     Notice: '', | ||||||
|     SMTPServer: '', |     SMTPServer: '', | ||||||
|     SMTPPort: '', |     SMTPPort: '', | ||||||
| @@ -109,6 +111,8 @@ const SystemSetting = () => { | |||||||
|       name === 'ServerAddress' || |       name === 'ServerAddress' || | ||||||
|       name === 'GitHubClientId' || |       name === 'GitHubClientId' || | ||||||
|       name === 'GitHubClientSecret' || |       name === 'GitHubClientSecret' || | ||||||
|  |       name === 'LarkClientId' || | ||||||
|  |       name === 'LarkClientSecret' || | ||||||
|       name === 'WeChatServerAddress' || |       name === 'WeChatServerAddress' || | ||||||
|       name === 'WeChatServerToken' || |       name === 'WeChatServerToken' || | ||||||
|       name === 'WeChatAccountQRCodeImageURL' || |       name === 'WeChatAccountQRCodeImageURL' || | ||||||
| @@ -212,6 +216,18 @@ const SystemSetting = () => { | |||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|  |    const submitLarkOAuth = async () => { | ||||||
|  |     if (originInputs['LarkClientId'] !== inputs.LarkClientId) { | ||||||
|  |       await updateOption('LarkClientId', inputs.LarkClientId); | ||||||
|  |     } | ||||||
|  |     if ( | ||||||
|  |       originInputs['LarkClientSecret'] !== inputs.LarkClientSecret && | ||||||
|  |       inputs.LarkClientSecret !== '' | ||||||
|  |     ) { | ||||||
|  |       await updateOption('LarkClientSecret', inputs.LarkClientSecret); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|   const submitTurnstile = async () => { |   const submitTurnstile = async () => { | ||||||
|     if (originInputs['TurnstileSiteKey'] !== inputs.TurnstileSiteKey) { |     if (originInputs['TurnstileSiteKey'] !== inputs.TurnstileSiteKey) { | ||||||
|       await updateOption('TurnstileSiteKey', inputs.TurnstileSiteKey); |       await updateOption('TurnstileSiteKey', inputs.TurnstileSiteKey); | ||||||
| @@ -469,6 +485,44 @@ const SystemSetting = () => { | |||||||
|             保存 GitHub OAuth 设置 |             保存 GitHub OAuth 设置 | ||||||
|           </Form.Button> |           </Form.Button> | ||||||
|           <Divider /> |           <Divider /> | ||||||
|  |           <Header as='h3'> | ||||||
|  |             配置飞书授权登录 | ||||||
|  |             <Header.Subheader> | ||||||
|  |               用以支持通过飞书进行登录注册, | ||||||
|  |               <a href='https://open.feishu.cn/app' target='_blank'> | ||||||
|  |                 点击此处 | ||||||
|  |               </a> | ||||||
|  |               管理你的飞书应用 | ||||||
|  |             </Header.Subheader> | ||||||
|  |           </Header> | ||||||
|  |           <Message> | ||||||
|  |             主页链接填 <code>{inputs.ServerAddress}</code> | ||||||
|  |             ,重定向 URL 填{' '} | ||||||
|  |             <code>{`${inputs.ServerAddress}/oauth/lark`}</code> | ||||||
|  |           </Message> | ||||||
|  |           <Form.Group widths={3}> | ||||||
|  |             <Form.Input | ||||||
|  |               label='App ID' | ||||||
|  |               name='LarkClientId' | ||||||
|  |               onChange={handleInputChange} | ||||||
|  |               autoComplete='new-password' | ||||||
|  |               value={inputs.LarkClientId} | ||||||
|  |               placeholder='输入 App ID' | ||||||
|  |             /> | ||||||
|  |             <Form.Input | ||||||
|  |               label='App Secret' | ||||||
|  |               name='LarkClientSecret' | ||||||
|  |               onChange={handleInputChange} | ||||||
|  |               type='password' | ||||||
|  |               autoComplete='new-password' | ||||||
|  |               value={inputs.LarkClientSecret} | ||||||
|  |               placeholder='敏感信息不会发送到前端显示' | ||||||
|  |             /> | ||||||
|  |           </Form.Group> | ||||||
|  |           <Form.Button onClick={submitLarkOAuth}> | ||||||
|  |             保存飞书 OAuth 设置 | ||||||
|  |           </Form.Button> | ||||||
|  |           <Divider /> | ||||||
|           <Header as='h3'> |           <Header as='h3'> | ||||||
|             配置 WeChat Server |             配置 WeChat Server | ||||||
|             <Header.Subheader> |             <Header.Subheader> | ||||||
|   | |||||||
| @@ -17,4 +17,13 @@ export async function onGitHubOAuthClicked(github_client_id) { | |||||||
|   window.open( |   window.open( | ||||||
|     `https://github.com/login/oauth/authorize?client_id=${github_client_id}&state=${state}&scope=user:email` |     `https://github.com/login/oauth/authorize?client_id=${github_client_id}&state=${state}&scope=user:email` | ||||||
|   ); |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export async function onLarkOAuthClicked(lark_client_id) { | ||||||
|  |   const state = await getOAuthState(); | ||||||
|  |   if (!state) return; | ||||||
|  |   let redirect_uri = `${window.location.origin}/oauth/lark`; | ||||||
|  |   window.open( | ||||||
|  |     `https://open.feishu.cn/open-apis/authen/v1/index?redirect_uri=${redirect_uri}&app_id=${lark_client_id}&state=${state}` | ||||||
|  |   ); | ||||||
| } | } | ||||||
| @@ -17,6 +17,7 @@ export const CHANNEL_OPTIONS = [ | |||||||
|   { key: 29, text: 'Groq', value: 29, color: 'orange' }, |   { key: 29, text: 'Groq', value: 29, color: 'orange' }, | ||||||
|   { key: 30, text: 'Ollama', value: 30, color: 'black' }, |   { key: 30, text: 'Ollama', value: 30, color: 'black' }, | ||||||
|   { key: 31, text: '零一万物', value: 31, color: 'green' }, |   { key: 31, text: '零一万物', value: 31, color: 'green' }, | ||||||
|  |   { key: 31, text: '阶跃星辰', value: 32, color: 'blue' }, | ||||||
|   { key: 8, text: '自定义渠道', value: 8, color: 'pink' }, |   { key: 8, text: '自定义渠道', value: 8, color: 'pink' }, | ||||||
|   { key: 22, text: '知识库:FastGPT', value: 22, color: 'blue' }, |   { key: 22, text: '知识库:FastGPT', value: 22, color: 'blue' }, | ||||||
|   { key: 21, text: '知识库:AI Proxy', value: 21, color: 'purple' }, |   { key: 21, text: '知识库:AI Proxy', value: 21, color: 'purple' }, | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								web/default/src/images/lark.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								web/default/src/images/lark.svg
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| After Width: | Height: | Size: 5.4 KiB | 
| @@ -15,7 +15,8 @@ const EditToken = () => { | |||||||
|     remain_quota: isEdit ? 0 : 500000, |     remain_quota: isEdit ? 0 : 500000, | ||||||
|     expired_time: -1, |     expired_time: -1, | ||||||
|     unlimited_quota: false, |     unlimited_quota: false, | ||||||
|     models: [] |     models: [], | ||||||
|  |     subnet: "", | ||||||
|   }; |   }; | ||||||
|   const [inputs, setInputs] = useState(originInputs); |   const [inputs, setInputs] = useState(originInputs); | ||||||
|   const { name, remain_quota, expired_time, unlimited_quota } = inputs; |   const { name, remain_quota, expired_time, unlimited_quota } = inputs; | ||||||
| @@ -153,6 +154,16 @@ const EditToken = () => { | |||||||
|               options={modelOptions} |               options={modelOptions} | ||||||
|             /> |             /> | ||||||
|           </Form.Field> |           </Form.Field> | ||||||
|  |           <Form.Field> | ||||||
|  |             <Form.Input | ||||||
|  |               label='IP 限制' | ||||||
|  |               name='subnet' | ||||||
|  |               placeholder={'请输入允许访问的网段,例如:192.168.0.0/24'} | ||||||
|  |               onChange={handleInputChange} | ||||||
|  |               value={inputs.subnet} | ||||||
|  |               autoComplete='new-password' | ||||||
|  |             /> | ||||||
|  |           </Form.Field> | ||||||
|           <Form.Field> |           <Form.Field> | ||||||
|             <Form.Input |             <Form.Input | ||||||
|               label='过期时间' |               label='过期时间' | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user