mirror of
				https://github.com/songquanpeng/one-api.git
				synced 2025-10-31 13:53:41 +08:00 
			
		
		
		
	Compare commits
	
		
			18 Commits
		
	
	
		
			v0.5.2-alp
			...
			v0.5.2
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | b464e2907a | ||
|  | d96cf2e84d | ||
|  | 446337c329 | ||
|  | 1dfa190e79 | ||
|  | 2d49ca6a07 | ||
|  | 89bcaaf989 | ||
|  | afcd1bd27b | ||
|  | c2c455c980 | ||
|  | 30a7f1a1c7 | ||
|  | c9d2e42a9e | ||
|  | 3fca6ff534 | ||
|  | 8cbbeb784f | ||
|  | ec88c0c240 | ||
|  | 065147b440 | ||
|  | fe8f216dd9 | ||
|  | b7d0616ae0 | ||
|  | ce9c8024a6 | ||
|  | 8a866078b2 | 
| @@ -137,7 +137,7 @@ The initial account username is `root` and password is `123456`. | |||||||
|    cd one-api/web |    cd one-api/web | ||||||
|    npm install |    npm install | ||||||
|    npm run build |    npm run build | ||||||
|  |     | ||||||
|    # Build the backend |    # Build the backend | ||||||
|    cd .. |    cd .. | ||||||
|    go mod download |    go mod download | ||||||
| @@ -173,7 +173,12 @@ If you encounter a blank page after deployment, refer to [#97](https://github.co | |||||||
| <summary><strong>Deploy on Sealos</strong></summary> | <summary><strong>Deploy on Sealos</strong></summary> | ||||||
| <div> | <div> | ||||||
|  |  | ||||||
| Please refer to [this tutorial](https://github.com/c121914yu/FastGPT/blob/main/docs/deploy/one-api/sealos.md). | > Sealos supports high concurrency, dynamic scaling, and stable operations for millions of users. | ||||||
|  |  | ||||||
|  | > Click the button below to deploy with one click.👇 | ||||||
|  |  | ||||||
|  | [](https://cloud.sealos.io/?openapp=system-fastdeploy?templateName=one-api) | ||||||
|  |  | ||||||
|  |  | ||||||
| </div> | </div> | ||||||
| </details> | </details> | ||||||
|   | |||||||
							
								
								
									
										12
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								README.md
									
									
									
									
									
								
							| @@ -64,6 +64,7 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用  | |||||||
|    + [x] [Google PaLM2 系列模型](https://developers.generativeai.google) |    + [x] [Google PaLM2 系列模型](https://developers.generativeai.google) | ||||||
|    + [x] [百度文心一言系列模型](https://cloud.baidu.com/doc/WENXINWORKSHOP/index.html) |    + [x] [百度文心一言系列模型](https://cloud.baidu.com/doc/WENXINWORKSHOP/index.html) | ||||||
|    + [x] [阿里通义千问系列模型](https://help.aliyun.com/document_detail/2400395.html) |    + [x] [阿里通义千问系列模型](https://help.aliyun.com/document_detail/2400395.html) | ||||||
|  |    + [x] [讯飞星火认知大模型](https://www.xfyun.cn/doc/spark/Web.html) | ||||||
|    + [x] [智谱 ChatGLM 系列模型](https://bigmodel.cn) |    + [x] [智谱 ChatGLM 系列模型](https://bigmodel.cn) | ||||||
| 2. 支持配置镜像以及众多第三方代理服务: | 2. 支持配置镜像以及众多第三方代理服务: | ||||||
|    + [x] [OpenAI-SB](https://openai-sb.com) |    + [x] [OpenAI-SB](https://openai-sb.com) | ||||||
| @@ -93,7 +94,7 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用  | |||||||
| 19. 支持通过系统访问令牌访问管理 API。 | 19. 支持通过系统访问令牌访问管理 API。 | ||||||
| 20. 支持 Cloudflare Turnstile 用户校验。 | 20. 支持 Cloudflare Turnstile 用户校验。 | ||||||
| 21. 支持用户管理,支持**多种用户登录注册方式**: | 21. 支持用户管理,支持**多种用户登录注册方式**: | ||||||
|     + 邮箱登录注册以及通过邮箱进行密码重置。 |     + 邮箱登录注册(支持注册邮箱白名单)以及通过邮箱进行密码重置。 | ||||||
|     + [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))。 | ||||||
|  |  | ||||||
| @@ -152,7 +153,7 @@ sudo service nginx restart | |||||||
|    cd one-api/web |    cd one-api/web | ||||||
|    npm install |    npm install | ||||||
|    npm run build |    npm run build | ||||||
|  |     | ||||||
|    # 构建后端 |    # 构建后端 | ||||||
|    cd .. |    cd .. | ||||||
|    go mod download |    go mod download | ||||||
| @@ -210,9 +211,11 @@ docker run --name chatgpt-web -d -p 3002:3002 -e OPENAI_API_BASE_URL=https://ope | |||||||
| <summary><strong>部署到 Sealos </strong></summary> | <summary><strong>部署到 Sealos </strong></summary> | ||||||
| <div> | <div> | ||||||
|  |  | ||||||
| > Sealos 可视化部署,仅需 1 分钟。 | > Sealos 的服务器在国外,不需要额外处理网络问题,支持高并发 & 动态伸缩。 | ||||||
|  |  | ||||||
| 参考这个[教程](https://github.com/c121914yu/FastGPT/blob/main/docs/deploy/one-api/sealos.md)中 1~5 步。 | 点击以下按钮一键部署: | ||||||
|  |  | ||||||
|  | [](https://cloud.sealos.io/?openapp=system-fastdeploy?templateName=one-api) | ||||||
|  |  | ||||||
| </div> | </div> | ||||||
| </details> | </details> | ||||||
| @@ -313,6 +316,7 @@ https://openai.justsong.cn | |||||||
|    + 额度 = 分组倍率 * 模型倍率 * (提示 token 数 + 补全 token 数 * 补全倍率) |    + 额度 = 分组倍率 * 模型倍率 * (提示 token 数 + 补全 token 数 * 补全倍率) | ||||||
|    + 其中补全倍率对于 GPT3.5 固定为 1.33,GPT4 为 2,与官方保持一致。 |    + 其中补全倍率对于 GPT3.5 固定为 1.33,GPT4 为 2,与官方保持一致。 | ||||||
|    + 如果是非流模式,官方接口会返回消耗的总 token,但是你要注意提示和补全的消耗倍率不一样。 |    + 如果是非流模式,官方接口会返回消耗的总 token,但是你要注意提示和补全的消耗倍率不一样。 | ||||||
|  |    + 注意,One API 的默认倍率就是官方倍率,是已经调整过的。 | ||||||
| 2. 账户额度足够为什么提示额度不足? | 2. 账户额度足够为什么提示额度不足? | ||||||
|    + 请检查你的令牌额度是否足够,这个和账户额度是分开的。 |    + 请检查你的令牌额度是否足够,这个和账户额度是分开的。 | ||||||
|    + 令牌额度仅供用户设置最大使用量,用户可自由设置。 |    + 令牌额度仅供用户设置最大使用量,用户可自由设置。 | ||||||
|   | |||||||
| @@ -42,6 +42,19 @@ var WeChatAuthEnabled = false | |||||||
| var TurnstileCheckEnabled = false | var TurnstileCheckEnabled = false | ||||||
| var RegisterEnabled = true | var RegisterEnabled = true | ||||||
|  |  | ||||||
|  | var EmailDomainRestrictionEnabled = false | ||||||
|  | var EmailDomainWhitelist = []string{ | ||||||
|  | 	"gmail.com", | ||||||
|  | 	"163.com", | ||||||
|  | 	"126.com", | ||||||
|  | 	"qq.com", | ||||||
|  | 	"outlook.com", | ||||||
|  | 	"hotmail.com", | ||||||
|  | 	"icloud.com", | ||||||
|  | 	"yahoo.com", | ||||||
|  | 	"foxmail.com", | ||||||
|  | } | ||||||
|  |  | ||||||
| var LogConsumeEnabled = true | var LogConsumeEnabled = true | ||||||
|  |  | ||||||
| var SMTPServer = "" | var SMTPServer = "" | ||||||
| @@ -157,6 +170,7 @@ const ( | |||||||
| 	ChannelTypeBaidu     = 15 | 	ChannelTypeBaidu     = 15 | ||||||
| 	ChannelTypeZhipu     = 16 | 	ChannelTypeZhipu     = 16 | ||||||
| 	ChannelTypeAli       = 17 | 	ChannelTypeAli       = 17 | ||||||
|  | 	ChannelTypeXunfei    = 18 | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var ChannelBaseURLs = []string{ | var ChannelBaseURLs = []string{ | ||||||
| @@ -178,4 +192,5 @@ var ChannelBaseURLs = []string{ | |||||||
| 	"https://aip.baidubce.com",       // 15 | 	"https://aip.baidubce.com",       // 15 | ||||||
| 	"https://open.bigmodel.cn",       // 16 | 	"https://open.bigmodel.cn",       // 16 | ||||||
| 	"https://dashscope.aliyuncs.com", // 17 | 	"https://dashscope.aliyuncs.com", // 17 | ||||||
|  | 	"",                               // 18 | ||||||
| } | } | ||||||
|   | |||||||
| @@ -49,6 +49,7 @@ var ModelRatio = map[string]float64{ | |||||||
| 	"chatglm_lite":            0.1429, // ¥0.002 / 1k tokens | 	"chatglm_lite":            0.1429, // ¥0.002 / 1k tokens | ||||||
| 	"qwen-v1":                 0.8572, // TBD: https://help.aliyun.com/document_detail/2399482.html?spm=a2c4g.2399482.0.0.1ad347feilAgag | 	"qwen-v1":                 0.8572, // TBD: https://help.aliyun.com/document_detail/2399482.html?spm=a2c4g.2399482.0.0.1ad347feilAgag | ||||||
| 	"qwen-plus-v1":            0.5715, // Same as above | 	"qwen-plus-v1":            0.5715, // Same as above | ||||||
|  | 	"SparkDesk":               0.8572, // TBD | ||||||
| } | } | ||||||
|  |  | ||||||
| func ModelRatio2JSONString() string { | func ModelRatio2JSONString() string { | ||||||
|   | |||||||
| @@ -23,6 +23,8 @@ func testChannel(channel *model.Channel, request ChatRequest) (error, *OpenAIErr | |||||||
| 	case common.ChannelTypeBaidu: | 	case common.ChannelTypeBaidu: | ||||||
| 		fallthrough | 		fallthrough | ||||||
| 	case common.ChannelTypeZhipu: | 	case common.ChannelTypeZhipu: | ||||||
|  | 		fallthrough | ||||||
|  | 	case common.ChannelTypeXunfei: | ||||||
| 		return errors.New("该渠道类型当前版本不支持测试,请手动测试"), nil | 		return errors.New("该渠道类型当前版本不支持测试,请手动测试"), nil | ||||||
| 	case common.ChannelTypeAzure: | 	case common.ChannelTypeAzure: | ||||||
| 		request.Model = "gpt-35-turbo" | 		request.Model = "gpt-35-turbo" | ||||||
|   | |||||||
| @@ -3,10 +3,12 @@ package controller | |||||||
| import ( | import ( | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"github.com/gin-gonic/gin" |  | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"one-api/common" | 	"one-api/common" | ||||||
| 	"one-api/model" | 	"one-api/model" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func GetStatus(c *gin.Context) { | func GetStatus(c *gin.Context) { | ||||||
| @@ -78,6 +80,22 @@ func SendEmailVerification(c *gin.Context) { | |||||||
| 		}) | 		}) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  | 	if common.EmailDomainRestrictionEnabled { | ||||||
|  | 		allowed := false | ||||||
|  | 		for _, domain := range common.EmailDomainWhitelist { | ||||||
|  | 			if strings.HasSuffix(email, "@"+domain) { | ||||||
|  | 				allowed = true | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		if !allowed { | ||||||
|  | 			c.JSON(http.StatusOK, gin.H{ | ||||||
|  | 				"success": false, | ||||||
|  | 				"message": "管理员启用了邮箱域名白名单,您的邮箱地址的域名不在白名单中", | ||||||
|  | 			}) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
| 	if model.IsEmailAlreadyTaken(email) { | 	if model.IsEmailAlreadyTaken(email) { | ||||||
| 		c.JSON(http.StatusOK, gin.H{ | 		c.JSON(http.StatusOK, gin.H{ | ||||||
| 			"success": false, | 			"success": false, | ||||||
|   | |||||||
| @@ -351,6 +351,15 @@ func init() { | |||||||
| 			Root:       "qwen-plus-v1", | 			Root:       "qwen-plus-v1", | ||||||
| 			Parent:     nil, | 			Parent:     nil, | ||||||
| 		}, | 		}, | ||||||
|  | 		{ | ||||||
|  | 			Id:         "SparkDesk", | ||||||
|  | 			Object:     "model", | ||||||
|  | 			Created:    1677649963, | ||||||
|  | 			OwnedBy:    "xunfei", | ||||||
|  | 			Permission: permission, | ||||||
|  | 			Root:       "SparkDesk", | ||||||
|  | 			Parent:     nil, | ||||||
|  | 		}, | ||||||
| 	} | 	} | ||||||
| 	openAIModelsMap = make(map[string]OpenAIModels) | 	openAIModelsMap = make(map[string]OpenAIModels) | ||||||
| 	for _, model := range openAIModels { | 	for _, model := range openAIModels { | ||||||
|   | |||||||
| @@ -2,11 +2,12 @@ package controller | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"github.com/gin-gonic/gin" |  | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"one-api/common" | 	"one-api/common" | ||||||
| 	"one-api/model" | 	"one-api/model" | ||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func GetOptions(c *gin.Context) { | func GetOptions(c *gin.Context) { | ||||||
| @@ -49,6 +50,14 @@ func UpdateOption(c *gin.Context) { | |||||||
| 			}) | 			}) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|  | 	case "EmailDomainRestrictionEnabled": | ||||||
|  | 		if option.Value == "true" && len(common.EmailDomainWhitelist) == 0 { | ||||||
|  | 			c.JSON(http.StatusOK, gin.H{ | ||||||
|  | 				"success": false, | ||||||
|  | 				"message": "无法启用邮箱域名限制,请先填入限制的邮箱域名!", | ||||||
|  | 			}) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
| 	case "WeChatAuthEnabled": | 	case "WeChatAuthEnabled": | ||||||
| 		if option.Value == "true" && common.WeChatServerAddress == "" { | 		if option.Value == "true" && common.WeChatServerAddress == "" { | ||||||
| 			c.JSON(http.StatusOK, gin.H{ | 			c.JSON(http.StatusOK, gin.H{ | ||||||
|   | |||||||
| @@ -34,6 +34,9 @@ func openaiStreamHandler(c *gin.Context, resp *http.Response, relayMode int) (*O | |||||||
| 			if len(data) < 6 { // ignore blank line or wrong format | 			if len(data) < 6 { // ignore blank line or wrong format | ||||||
| 				continue | 				continue | ||||||
| 			} | 			} | ||||||
|  | 			if data[:6] != "data: " && data[:6] != "[DONE]" { | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
| 			dataChan <- data | 			dataChan <- data | ||||||
| 			data = data[6:] | 			data = data[6:] | ||||||
| 			if !strings.HasPrefix(data, "[DONE]") { | 			if !strings.HasPrefix(data, "[DONE]") { | ||||||
| @@ -43,7 +46,7 @@ func openaiStreamHandler(c *gin.Context, resp *http.Response, relayMode int) (*O | |||||||
| 					err := json.Unmarshal([]byte(data), &streamResponse) | 					err := json.Unmarshal([]byte(data), &streamResponse) | ||||||
| 					if err != nil { | 					if err != nil { | ||||||
| 						common.SysError("error unmarshalling stream response: " + err.Error()) | 						common.SysError("error unmarshalling stream response: " + err.Error()) | ||||||
| 						return | 						continue // just ignore the error | ||||||
| 					} | 					} | ||||||
| 					for _, choice := range streamResponse.Choices { | 					for _, choice := range streamResponse.Choices { | ||||||
| 						responseText += choice.Delta.Content | 						responseText += choice.Delta.Content | ||||||
| @@ -53,7 +56,7 @@ func openaiStreamHandler(c *gin.Context, resp *http.Response, relayMode int) (*O | |||||||
| 					err := json.Unmarshal([]byte(data), &streamResponse) | 					err := json.Unmarshal([]byte(data), &streamResponse) | ||||||
| 					if err != nil { | 					if err != nil { | ||||||
| 						common.SysError("error unmarshalling stream response: " + err.Error()) | 						common.SysError("error unmarshalling stream response: " + err.Error()) | ||||||
| 						return | 						continue | ||||||
| 					} | 					} | ||||||
| 					for _, choice := range streamResponse.Choices { | 					for _, choice := range streamResponse.Choices { | ||||||
| 						responseText += choice.Text | 						responseText += choice.Text | ||||||
| @@ -89,7 +92,7 @@ func openaiStreamHandler(c *gin.Context, resp *http.Response, relayMode int) (*O | |||||||
| 	return nil, responseText | 	return nil, responseText | ||||||
| } | } | ||||||
|  |  | ||||||
| func openaiHandler(c *gin.Context, resp *http.Response, consumeQuota bool) (*OpenAIErrorWithStatusCode, *Usage) { | func openaiHandler(c *gin.Context, resp *http.Response, consumeQuota bool, promptTokens int, model string) (*OpenAIErrorWithStatusCode, *Usage) { | ||||||
| 	var textResponse TextResponse | 	var textResponse TextResponse | ||||||
| 	if consumeQuota { | 	if consumeQuota { | ||||||
| 		responseBody, err := io.ReadAll(resp.Body) | 		responseBody, err := io.ReadAll(resp.Body) | ||||||
| @@ -129,5 +132,17 @@ func openaiHandler(c *gin.Context, resp *http.Response, consumeQuota bool) (*Ope | |||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil | 		return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	if textResponse.Usage.TotalTokens == 0 { | ||||||
|  | 		completionTokens := 0 | ||||||
|  | 		for _, choice := range textResponse.Choices { | ||||||
|  | 			completionTokens += countTokenText(choice.Message.Content, model) | ||||||
|  | 		} | ||||||
|  | 		textResponse.Usage = Usage{ | ||||||
|  | 			PromptTokens:     promptTokens, | ||||||
|  | 			CompletionTokens: completionTokens, | ||||||
|  | 			TotalTokens:      promptTokens + completionTokens, | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
| 	return nil, &textResponse.Usage | 	return nil, &textResponse.Usage | ||||||
| } | } | ||||||
|   | |||||||
| @@ -21,6 +21,7 @@ const ( | |||||||
| 	APITypeBaidu | 	APITypeBaidu | ||||||
| 	APITypeZhipu | 	APITypeZhipu | ||||||
| 	APITypeAli | 	APITypeAli | ||||||
|  | 	APITypeXunfei | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var httpClient *http.Client | var httpClient *http.Client | ||||||
| @@ -97,7 +98,8 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { | |||||||
| 		apiType = APITypeZhipu | 		apiType = APITypeZhipu | ||||||
| 	case common.ChannelTypeAli: | 	case common.ChannelTypeAli: | ||||||
| 		apiType = APITypeAli | 		apiType = APITypeAli | ||||||
|  | 	case common.ChannelTypeXunfei: | ||||||
|  | 		apiType = APITypeXunfei | ||||||
| 	} | 	} | ||||||
| 	baseURL := common.ChannelBaseURLs[channelType] | 	baseURL := common.ChannelBaseURLs[channelType] | ||||||
| 	requestURL := c.Request.URL.String() | 	requestURL := c.Request.URL.String() | ||||||
| @@ -250,53 +252,60 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { | |||||||
| 		} | 		} | ||||||
| 		requestBody = bytes.NewBuffer(jsonStr) | 		requestBody = bytes.NewBuffer(jsonStr) | ||||||
| 	} | 	} | ||||||
| 	req, err := http.NewRequest(c.Request.Method, fullRequestURL, requestBody) |  | ||||||
| 	if err != nil { | 	var req *http.Request | ||||||
| 		return errorWrapper(err, "new_request_failed", http.StatusInternalServerError) | 	var resp *http.Response | ||||||
| 	} | 	isStream := textRequest.Stream | ||||||
| 	apiKey := c.Request.Header.Get("Authorization") |  | ||||||
| 	apiKey = strings.TrimPrefix(apiKey, "Bearer ") | 	if apiType != APITypeXunfei { // cause xunfei use websocket | ||||||
| 	switch apiType { | 		req, err = http.NewRequest(c.Request.Method, fullRequestURL, requestBody) | ||||||
| 	case APITypeOpenAI: | 		if err != nil { | ||||||
| 		if channelType == common.ChannelTypeAzure { | 			return errorWrapper(err, "new_request_failed", http.StatusInternalServerError) | ||||||
| 			req.Header.Set("api-key", apiKey) |  | ||||||
| 		} else { |  | ||||||
| 			req.Header.Set("Authorization", c.Request.Header.Get("Authorization")) |  | ||||||
| 		} | 		} | ||||||
| 	case APITypeClaude: | 		apiKey := c.Request.Header.Get("Authorization") | ||||||
| 		req.Header.Set("x-api-key", apiKey) | 		apiKey = strings.TrimPrefix(apiKey, "Bearer ") | ||||||
| 		anthropicVersion := c.Request.Header.Get("anthropic-version") | 		switch apiType { | ||||||
| 		if anthropicVersion == "" { | 		case APITypeOpenAI: | ||||||
| 			anthropicVersion = "2023-06-01" | 			if channelType == common.ChannelTypeAzure { | ||||||
|  | 				req.Header.Set("api-key", apiKey) | ||||||
|  | 			} else { | ||||||
|  | 				req.Header.Set("Authorization", c.Request.Header.Get("Authorization")) | ||||||
|  | 			} | ||||||
|  | 		case APITypeClaude: | ||||||
|  | 			req.Header.Set("x-api-key", apiKey) | ||||||
|  | 			anthropicVersion := c.Request.Header.Get("anthropic-version") | ||||||
|  | 			if anthropicVersion == "" { | ||||||
|  | 				anthropicVersion = "2023-06-01" | ||||||
|  | 			} | ||||||
|  | 			req.Header.Set("anthropic-version", anthropicVersion) | ||||||
|  | 		case APITypeZhipu: | ||||||
|  | 			token := getZhipuToken(apiKey) | ||||||
|  | 			req.Header.Set("Authorization", token) | ||||||
|  | 		case APITypeAli: | ||||||
|  | 			req.Header.Set("Authorization", "Bearer "+apiKey) | ||||||
|  | 			if textRequest.Stream { | ||||||
|  | 				req.Header.Set("X-DashScope-SSE", "enable") | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
| 		req.Header.Set("anthropic-version", anthropicVersion) | 		req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type")) | ||||||
| 	case APITypeZhipu: | 		req.Header.Set("Accept", c.Request.Header.Get("Accept")) | ||||||
| 		token := getZhipuToken(apiKey) | 		//req.Header.Set("Connection", c.Request.Header.Get("Connection")) | ||||||
| 		req.Header.Set("Authorization", token) | 		resp, err = httpClient.Do(req) | ||||||
| 	case APITypeAli: | 		if err != nil { | ||||||
| 		req.Header.Set("Authorization", "Bearer "+apiKey) | 			return errorWrapper(err, "do_request_failed", http.StatusInternalServerError) | ||||||
| 		if textRequest.Stream { |  | ||||||
| 			req.Header.Set("X-DashScope-SSE", "enable") |  | ||||||
| 		} | 		} | ||||||
|  | 		err = req.Body.Close() | ||||||
|  | 		if err != nil { | ||||||
|  | 			return errorWrapper(err, "close_request_body_failed", http.StatusInternalServerError) | ||||||
|  | 		} | ||||||
|  | 		err = c.Request.Body.Close() | ||||||
|  | 		if err != nil { | ||||||
|  | 			return errorWrapper(err, "close_request_body_failed", http.StatusInternalServerError) | ||||||
|  | 		} | ||||||
|  | 		isStream = isStream || strings.HasPrefix(resp.Header.Get("Content-Type"), "text/event-stream") | ||||||
| 	} | 	} | ||||||
| 	req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type")) |  | ||||||
| 	req.Header.Set("Accept", c.Request.Header.Get("Accept")) |  | ||||||
| 	//req.Header.Set("Connection", c.Request.Header.Get("Connection")) |  | ||||||
| 	resp, err := httpClient.Do(req) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return errorWrapper(err, "do_request_failed", http.StatusInternalServerError) |  | ||||||
| 	} |  | ||||||
| 	err = req.Body.Close() |  | ||||||
| 	if err != nil { |  | ||||||
| 		return errorWrapper(err, "close_request_body_failed", http.StatusInternalServerError) |  | ||||||
| 	} |  | ||||||
| 	err = c.Request.Body.Close() |  | ||||||
| 	if err != nil { |  | ||||||
| 		return errorWrapper(err, "close_request_body_failed", http.StatusInternalServerError) |  | ||||||
| 	} |  | ||||||
| 	var textResponse TextResponse | 	var textResponse TextResponse | ||||||
| 	isStream := strings.HasPrefix(resp.Header.Get("Content-Type"), "text/event-stream") |  | ||||||
| 	var streamResponseText string |  | ||||||
|  |  | ||||||
| 	defer func() { | 	defer func() { | ||||||
| 		if consumeQuota { | 		if consumeQuota { | ||||||
| @@ -308,16 +317,10 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { | |||||||
| 			if strings.HasPrefix(textRequest.Model, "gpt-4") { | 			if strings.HasPrefix(textRequest.Model, "gpt-4") { | ||||||
| 				completionRatio = 2 | 				completionRatio = 2 | ||||||
| 			} | 			} | ||||||
| 			if isStream && apiType != APITypeBaidu && apiType != APITypeZhipu && apiType != APITypeAli { |  | ||||||
| 				completionTokens = countTokenText(streamResponseText, textRequest.Model) | 			promptTokens = textResponse.Usage.PromptTokens | ||||||
| 			} else { | 			completionTokens = textResponse.Usage.CompletionTokens | ||||||
| 				promptTokens = textResponse.Usage.PromptTokens |  | ||||||
| 				completionTokens = textResponse.Usage.CompletionTokens |  | ||||||
| 				if apiType == APITypeZhipu { |  | ||||||
| 					// zhipu's API does not return prompt tokens & completion tokens |  | ||||||
| 					promptTokens = textResponse.Usage.TotalTokens |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 			quota = promptTokens + int(float64(completionTokens)*completionRatio) | 			quota = promptTokens + int(float64(completionTokens)*completionRatio) | ||||||
| 			quota = int(float64(quota) * ratio) | 			quota = int(float64(quota) * ratio) | ||||||
| 			if ratio != 0 && quota <= 0 { | 			if ratio != 0 && quota <= 0 { | ||||||
| @@ -355,10 +358,11 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { | |||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				return err | 				return err | ||||||
| 			} | 			} | ||||||
| 			streamResponseText = responseText | 			textResponse.Usage.PromptTokens = promptTokens | ||||||
|  | 			textResponse.Usage.CompletionTokens = countTokenText(responseText, textRequest.Model) | ||||||
| 			return nil | 			return nil | ||||||
| 		} else { | 		} else { | ||||||
| 			err, usage := openaiHandler(c, resp, consumeQuota) | 			err, usage := openaiHandler(c, resp, consumeQuota, promptTokens, textRequest.Model) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				return err | 				return err | ||||||
| 			} | 			} | ||||||
| @@ -373,7 +377,8 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { | |||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				return err | 				return err | ||||||
| 			} | 			} | ||||||
| 			streamResponseText = responseText | 			textResponse.Usage.PromptTokens = promptTokens | ||||||
|  | 			textResponse.Usage.CompletionTokens = countTokenText(responseText, textRequest.Model) | ||||||
| 			return nil | 			return nil | ||||||
| 		} else { | 		} else { | ||||||
| 			err, usage := claudeHandler(c, resp, promptTokens, textRequest.Model) | 			err, usage := claudeHandler(c, resp, promptTokens, textRequest.Model) | ||||||
| @@ -418,7 +423,8 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { | |||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				return err | 				return err | ||||||
| 			} | 			} | ||||||
| 			streamResponseText = responseText | 			textResponse.Usage.PromptTokens = promptTokens | ||||||
|  | 			textResponse.Usage.CompletionTokens = countTokenText(responseText, textRequest.Model) | ||||||
| 			return nil | 			return nil | ||||||
| 		} else { | 		} else { | ||||||
| 			err, usage := palmHandler(c, resp, promptTokens, textRequest.Model) | 			err, usage := palmHandler(c, resp, promptTokens, textRequest.Model) | ||||||
| @@ -439,6 +445,8 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { | |||||||
| 			if usage != nil { | 			if usage != nil { | ||||||
| 				textResponse.Usage = *usage | 				textResponse.Usage = *usage | ||||||
| 			} | 			} | ||||||
|  | 			// zhipu's API does not return prompt tokens & completion tokens | ||||||
|  | 			textResponse.Usage.PromptTokens = textResponse.Usage.TotalTokens | ||||||
| 			return nil | 			return nil | ||||||
| 		} else { | 		} else { | ||||||
| 			err, usage := zhipuHandler(c, resp) | 			err, usage := zhipuHandler(c, resp) | ||||||
| @@ -448,6 +456,8 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { | |||||||
| 			if usage != nil { | 			if usage != nil { | ||||||
| 				textResponse.Usage = *usage | 				textResponse.Usage = *usage | ||||||
| 			} | 			} | ||||||
|  | 			// zhipu's API does not return prompt tokens & completion tokens | ||||||
|  | 			textResponse.Usage.PromptTokens = textResponse.Usage.TotalTokens | ||||||
| 			return nil | 			return nil | ||||||
| 		} | 		} | ||||||
| 	case APITypeAli: | 	case APITypeAli: | ||||||
| @@ -470,6 +480,25 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { | |||||||
| 			} | 			} | ||||||
| 			return nil | 			return nil | ||||||
| 		} | 		} | ||||||
|  | 	case APITypeXunfei: | ||||||
|  | 		if isStream { | ||||||
|  | 			auth := c.Request.Header.Get("Authorization") | ||||||
|  | 			auth = strings.TrimPrefix(auth, "Bearer ") | ||||||
|  | 			splits := strings.Split(auth, "|") | ||||||
|  | 			if len(splits) != 3 { | ||||||
|  | 				return errorWrapper(errors.New("invalid auth"), "invalid_auth", http.StatusBadRequest) | ||||||
|  | 			} | ||||||
|  | 			err, usage := xunfeiStreamHandler(c, textRequest, splits[0], splits[1], splits[2]) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
|  | 			if usage != nil { | ||||||
|  | 				textResponse.Usage = *usage | ||||||
|  | 			} | ||||||
|  | 			return nil | ||||||
|  | 		} else { | ||||||
|  | 			return errorWrapper(errors.New("xunfei api does not support non-stream mode"), "invalid_api_type", http.StatusBadRequest) | ||||||
|  | 		} | ||||||
| 	default: | 	default: | ||||||
| 		return errorWrapper(errors.New("unknown api type"), "unknown_api_type", http.StatusInternalServerError) | 		return errorWrapper(errors.New("unknown api type"), "unknown_api_type", http.StatusInternalServerError) | ||||||
| 	} | 	} | ||||||
|   | |||||||
							
								
								
									
										278
									
								
								controller/relay-xunfei.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										278
									
								
								controller/relay-xunfei.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,278 @@ | |||||||
|  | package controller | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"crypto/hmac" | ||||||
|  | 	"crypto/sha256" | ||||||
|  | 	"encoding/base64" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"fmt" | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
|  | 	"github.com/gorilla/websocket" | ||||||
|  | 	"io" | ||||||
|  | 	"net/http" | ||||||
|  | 	"net/url" | ||||||
|  | 	"one-api/common" | ||||||
|  | 	"strings" | ||||||
|  | 	"time" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // https://console.xfyun.cn/services/cbm | ||||||
|  | // https://www.xfyun.cn/doc/spark/Web.html | ||||||
|  |  | ||||||
|  | type XunfeiMessage struct { | ||||||
|  | 	Role    string `json:"role"` | ||||||
|  | 	Content string `json:"content"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type XunfeiChatRequest struct { | ||||||
|  | 	Header struct { | ||||||
|  | 		AppId string `json:"app_id"` | ||||||
|  | 	} `json:"header"` | ||||||
|  | 	Parameter struct { | ||||||
|  | 		Chat struct { | ||||||
|  | 			Domain      string  `json:"domain,omitempty"` | ||||||
|  | 			Temperature float64 `json:"temperature,omitempty"` | ||||||
|  | 			TopK        int     `json:"top_k,omitempty"` | ||||||
|  | 			MaxTokens   int     `json:"max_tokens,omitempty"` | ||||||
|  | 			Auditing    bool    `json:"auditing,omitempty"` | ||||||
|  | 		} `json:"chat"` | ||||||
|  | 	} `json:"parameter"` | ||||||
|  | 	Payload struct { | ||||||
|  | 		Message struct { | ||||||
|  | 			Text []XunfeiMessage `json:"text"` | ||||||
|  | 		} `json:"message"` | ||||||
|  | 	} `json:"payload"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type XunfeiChatResponseTextItem struct { | ||||||
|  | 	Content string `json:"content"` | ||||||
|  | 	Role    string `json:"role"` | ||||||
|  | 	Index   int    `json:"index"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type XunfeiChatResponse struct { | ||||||
|  | 	Header struct { | ||||||
|  | 		Code    int    `json:"code"` | ||||||
|  | 		Message string `json:"message"` | ||||||
|  | 		Sid     string `json:"sid"` | ||||||
|  | 		Status  int    `json:"status"` | ||||||
|  | 	} `json:"header"` | ||||||
|  | 	Payload struct { | ||||||
|  | 		Choices struct { | ||||||
|  | 			Status int                          `json:"status"` | ||||||
|  | 			Seq    int                          `json:"seq"` | ||||||
|  | 			Text   []XunfeiChatResponseTextItem `json:"text"` | ||||||
|  | 		} `json:"choices"` | ||||||
|  | 		Usage struct { | ||||||
|  | 			//Text struct { | ||||||
|  | 			//	QuestionTokens   string `json:"question_tokens"` | ||||||
|  | 			//	PromptTokens     string `json:"prompt_tokens"` | ||||||
|  | 			//	CompletionTokens string `json:"completion_tokens"` | ||||||
|  | 			//	TotalTokens      string `json:"total_tokens"` | ||||||
|  | 			//} `json:"text"` | ||||||
|  | 			Text Usage `json:"text"` | ||||||
|  | 		} `json:"usage"` | ||||||
|  | 	} `json:"payload"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func requestOpenAI2Xunfei(request GeneralOpenAIRequest, xunfeiAppId string) *XunfeiChatRequest { | ||||||
|  | 	messages := make([]XunfeiMessage, 0, len(request.Messages)) | ||||||
|  | 	for _, message := range request.Messages { | ||||||
|  | 		if message.Role == "system" { | ||||||
|  | 			messages = append(messages, XunfeiMessage{ | ||||||
|  | 				Role:    "user", | ||||||
|  | 				Content: message.Content, | ||||||
|  | 			}) | ||||||
|  | 			messages = append(messages, XunfeiMessage{ | ||||||
|  | 				Role:    "assistant", | ||||||
|  | 				Content: "Okay", | ||||||
|  | 			}) | ||||||
|  | 		} else { | ||||||
|  | 			messages = append(messages, XunfeiMessage{ | ||||||
|  | 				Role:    message.Role, | ||||||
|  | 				Content: message.Content, | ||||||
|  | 			}) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	xunfeiRequest := XunfeiChatRequest{} | ||||||
|  | 	xunfeiRequest.Header.AppId = xunfeiAppId | ||||||
|  | 	xunfeiRequest.Parameter.Chat.Domain = "general" | ||||||
|  | 	xunfeiRequest.Parameter.Chat.Temperature = request.Temperature | ||||||
|  | 	xunfeiRequest.Parameter.Chat.TopK = request.N | ||||||
|  | 	xunfeiRequest.Parameter.Chat.MaxTokens = request.MaxTokens | ||||||
|  | 	xunfeiRequest.Payload.Message.Text = messages | ||||||
|  | 	return &xunfeiRequest | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func responseXunfei2OpenAI(response *XunfeiChatResponse) *OpenAITextResponse { | ||||||
|  | 	if len(response.Payload.Choices.Text) == 0 { | ||||||
|  | 		response.Payload.Choices.Text = []XunfeiChatResponseTextItem{ | ||||||
|  | 			{ | ||||||
|  | 				Content: "", | ||||||
|  | 			}, | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	choice := OpenAITextResponseChoice{ | ||||||
|  | 		Index: 0, | ||||||
|  | 		Message: Message{ | ||||||
|  | 			Role:    "assistant", | ||||||
|  | 			Content: response.Payload.Choices.Text[0].Content, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	fullTextResponse := OpenAITextResponse{ | ||||||
|  | 		Object:  "chat.completion", | ||||||
|  | 		Created: common.GetTimestamp(), | ||||||
|  | 		Choices: []OpenAITextResponseChoice{choice}, | ||||||
|  | 		Usage:   response.Payload.Usage.Text, | ||||||
|  | 	} | ||||||
|  | 	return &fullTextResponse | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func streamResponseXunfei2OpenAI(xunfeiResponse *XunfeiChatResponse) *ChatCompletionsStreamResponse { | ||||||
|  | 	if len(xunfeiResponse.Payload.Choices.Text) == 0 { | ||||||
|  | 		xunfeiResponse.Payload.Choices.Text = []XunfeiChatResponseTextItem{ | ||||||
|  | 			{ | ||||||
|  | 				Content: "", | ||||||
|  | 			}, | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	var choice ChatCompletionsStreamResponseChoice | ||||||
|  | 	choice.Delta.Content = xunfeiResponse.Payload.Choices.Text[0].Content | ||||||
|  | 	response := ChatCompletionsStreamResponse{ | ||||||
|  | 		Object:  "chat.completion.chunk", | ||||||
|  | 		Created: common.GetTimestamp(), | ||||||
|  | 		Model:   "SparkDesk", | ||||||
|  | 		Choices: []ChatCompletionsStreamResponseChoice{choice}, | ||||||
|  | 	} | ||||||
|  | 	return &response | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func buildXunfeiAuthUrl(hostUrl string, apiKey, apiSecret string) string { | ||||||
|  | 	HmacWithShaToBase64 := func(algorithm, data, key string) string { | ||||||
|  | 		mac := hmac.New(sha256.New, []byte(key)) | ||||||
|  | 		mac.Write([]byte(data)) | ||||||
|  | 		encodeData := mac.Sum(nil) | ||||||
|  | 		return base64.StdEncoding.EncodeToString(encodeData) | ||||||
|  | 	} | ||||||
|  | 	ul, err := url.Parse(hostUrl) | ||||||
|  | 	if err != nil { | ||||||
|  | 		fmt.Println(err) | ||||||
|  | 	} | ||||||
|  | 	date := time.Now().UTC().Format(time.RFC1123) | ||||||
|  | 	signString := []string{"host: " + ul.Host, "date: " + date, "GET " + ul.Path + " HTTP/1.1"} | ||||||
|  | 	sign := strings.Join(signString, "\n") | ||||||
|  | 	sha := HmacWithShaToBase64("hmac-sha256", sign, apiSecret) | ||||||
|  | 	authUrl := fmt.Sprintf("hmac username=\"%s\", algorithm=\"%s\", headers=\"%s\", signature=\"%s\"", apiKey, | ||||||
|  | 		"hmac-sha256", "host date request-line", sha) | ||||||
|  | 	authorization := base64.StdEncoding.EncodeToString([]byte(authUrl)) | ||||||
|  | 	v := url.Values{} | ||||||
|  | 	v.Add("host", ul.Host) | ||||||
|  | 	v.Add("date", date) | ||||||
|  | 	v.Add("authorization", authorization) | ||||||
|  | 	callUrl := hostUrl + "?" + v.Encode() | ||||||
|  | 	return callUrl | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func xunfeiStreamHandler(c *gin.Context, textRequest GeneralOpenAIRequest, appId string, apiSecret string, apiKey string) (*OpenAIErrorWithStatusCode, *Usage) { | ||||||
|  | 	var usage Usage | ||||||
|  | 	d := websocket.Dialer{ | ||||||
|  | 		HandshakeTimeout: 5 * time.Second, | ||||||
|  | 	} | ||||||
|  | 	hostUrl := "wss://aichat.xf-yun.com/v1/chat" | ||||||
|  | 	conn, resp, err := d.Dial(buildXunfeiAuthUrl(hostUrl, apiKey, apiSecret), nil) | ||||||
|  | 	if err != nil || resp.StatusCode != 101 { | ||||||
|  | 		return errorWrapper(err, "dial_failed", http.StatusInternalServerError), nil | ||||||
|  | 	} | ||||||
|  | 	data := requestOpenAI2Xunfei(textRequest, appId) | ||||||
|  | 	err = conn.WriteJSON(data) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return errorWrapper(err, "write_json_failed", http.StatusInternalServerError), nil | ||||||
|  | 	} | ||||||
|  | 	dataChan := make(chan XunfeiChatResponse) | ||||||
|  | 	stopChan := make(chan bool) | ||||||
|  | 	go func() { | ||||||
|  | 		for { | ||||||
|  | 			_, msg, err := conn.ReadMessage() | ||||||
|  | 			if err != nil { | ||||||
|  | 				common.SysError("error reading stream response: " + err.Error()) | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
|  | 			var response XunfeiChatResponse | ||||||
|  | 			err = json.Unmarshal(msg, &response) | ||||||
|  | 			if err != nil { | ||||||
|  | 				common.SysError("error unmarshalling stream response: " + err.Error()) | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
|  | 			dataChan <- response | ||||||
|  | 			if response.Payload.Choices.Status == 2 { | ||||||
|  | 				err := conn.Close() | ||||||
|  | 				if err != nil { | ||||||
|  | 					common.SysError("error closing websocket connection: " + err.Error()) | ||||||
|  | 				} | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		stopChan <- true | ||||||
|  | 	}() | ||||||
|  | 	c.Writer.Header().Set("Content-Type", "text/event-stream") | ||||||
|  | 	c.Writer.Header().Set("Cache-Control", "no-cache") | ||||||
|  | 	c.Writer.Header().Set("Connection", "keep-alive") | ||||||
|  | 	c.Writer.Header().Set("Transfer-Encoding", "chunked") | ||||||
|  | 	c.Writer.Header().Set("X-Accel-Buffering", "no") | ||||||
|  | 	c.Stream(func(w io.Writer) bool { | ||||||
|  | 		select { | ||||||
|  | 		case xunfeiResponse := <-dataChan: | ||||||
|  | 			usage.PromptTokens += xunfeiResponse.Payload.Usage.Text.PromptTokens | ||||||
|  | 			usage.CompletionTokens += xunfeiResponse.Payload.Usage.Text.CompletionTokens | ||||||
|  | 			usage.TotalTokens += xunfeiResponse.Payload.Usage.Text.TotalTokens | ||||||
|  | 			response := streamResponseXunfei2OpenAI(&xunfeiResponse) | ||||||
|  | 			jsonResponse, err := json.Marshal(response) | ||||||
|  | 			if err != nil { | ||||||
|  | 				common.SysError("error marshalling stream response: " + err.Error()) | ||||||
|  | 				return true | ||||||
|  | 			} | ||||||
|  | 			c.Render(-1, common.CustomEvent{Data: "data: " + string(jsonResponse)}) | ||||||
|  | 			return true | ||||||
|  | 		case <-stopChan: | ||||||
|  | 			c.Render(-1, common.CustomEvent{Data: "data: [DONE]"}) | ||||||
|  | 			return false | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  | 	return nil, &usage | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func xunfeiHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithStatusCode, *Usage) { | ||||||
|  | 	var xunfeiResponse XunfeiChatResponse | ||||||
|  | 	responseBody, err := io.ReadAll(resp.Body) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return errorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil | ||||||
|  | 	} | ||||||
|  | 	err = resp.Body.Close() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil | ||||||
|  | 	} | ||||||
|  | 	err = json.Unmarshal(responseBody, &xunfeiResponse) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return errorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil | ||||||
|  | 	} | ||||||
|  | 	if xunfeiResponse.Header.Code != 0 { | ||||||
|  | 		return &OpenAIErrorWithStatusCode{ | ||||||
|  | 			OpenAIError: OpenAIError{ | ||||||
|  | 				Message: xunfeiResponse.Header.Message, | ||||||
|  | 				Type:    "xunfei_error", | ||||||
|  | 				Param:   "", | ||||||
|  | 				Code:    xunfeiResponse.Header.Code, | ||||||
|  | 			}, | ||||||
|  | 			StatusCode: resp.StatusCode, | ||||||
|  | 		}, nil | ||||||
|  | 	} | ||||||
|  | 	fullTextResponse := responseXunfei2OpenAI(&xunfeiResponse) | ||||||
|  | 	jsonResponse, err := json.Marshal(fullTextResponse) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return errorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil | ||||||
|  | 	} | ||||||
|  | 	c.Writer.Header().Set("Content-Type", "application/json") | ||||||
|  | 	c.Writer.WriteHeader(resp.StatusCode) | ||||||
|  | 	_, err = c.Writer.Write(jsonResponse) | ||||||
|  | 	return nil, &fullTextResponse.Usage | ||||||
|  | } | ||||||
| @@ -194,8 +194,8 @@ func zhipuStreamHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithSt | |||||||
| 		if atEOF && len(data) == 0 { | 		if atEOF && len(data) == 0 { | ||||||
| 			return 0, nil, nil | 			return 0, nil, nil | ||||||
| 		} | 		} | ||||||
| 		if i := strings.Index(string(data), "\n"); i >= 0 { | 		if i := strings.Index(string(data), "\n\n"); i >= 0 && strings.Index(string(data), ":") >= 0 { | ||||||
| 			return i + 1, data[0:i], nil | 			return i + 2, data[0:i], nil | ||||||
| 		} | 		} | ||||||
| 		if atEOF { | 		if atEOF { | ||||||
| 			return len(data), data, nil | 			return len(data), data, nil | ||||||
| @@ -208,14 +208,19 @@ func zhipuStreamHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithSt | |||||||
| 	go func() { | 	go func() { | ||||||
| 		for scanner.Scan() { | 		for scanner.Scan() { | ||||||
| 			data := scanner.Text() | 			data := scanner.Text() | ||||||
| 			data = strings.Trim(data, "\"") | 			lines := strings.Split(data, "\n") | ||||||
| 			if len(data) < 5 { // ignore blank line or wrong format | 			for i, line := range lines { | ||||||
| 				continue | 				if len(line) < 5 { | ||||||
| 			} | 					continue | ||||||
| 			if data[:5] == "data:" { | 				} | ||||||
| 				dataChan <- data[5:] | 				if line[:5] == "data:" { | ||||||
| 			} else if data[:5] == "meta:" { | 					dataChan <- line[5:] | ||||||
| 				metaChan <- data[5:] | 					if i != len(lines)-1 { | ||||||
|  | 						dataChan <- "\n" | ||||||
|  | 					} | ||||||
|  | 				} else if line[:5] == "meta:" { | ||||||
|  | 					metaChan <- line[5:] | ||||||
|  | 				} | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 		stopChan <- true | 		stopChan <- true | ||||||
|   | |||||||
| @@ -81,8 +81,9 @@ type OpenAIErrorWithStatusCode struct { | |||||||
| } | } | ||||||
|  |  | ||||||
| type TextResponse struct { | type TextResponse struct { | ||||||
| 	Usage `json:"usage"` | 	Choices []OpenAITextResponseChoice `json:"choices"` | ||||||
| 	Error OpenAIError `json:"error"` | 	Usage   `json:"usage"` | ||||||
|  | 	Error   OpenAIError `json:"error"` | ||||||
| } | } | ||||||
|  |  | ||||||
| type OpenAITextResponseChoice struct { | type OpenAITextResponseChoice struct { | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								go.mod
									
									
									
									
									
								
							| @@ -13,6 +13,7 @@ require ( | |||||||
| 	github.com/go-redis/redis/v8 v8.11.5 | 	github.com/go-redis/redis/v8 v8.11.5 | ||||||
| 	github.com/golang-jwt/jwt v3.2.2+incompatible | 	github.com/golang-jwt/jwt v3.2.2+incompatible | ||||||
| 	github.com/google/uuid v1.3.0 | 	github.com/google/uuid v1.3.0 | ||||||
|  | 	github.com/gorilla/websocket v1.5.0 | ||||||
| 	github.com/pkoukk/tiktoken-go v0.1.1 | 	github.com/pkoukk/tiktoken-go v0.1.1 | ||||||
| 	golang.org/x/crypto v0.9.0 | 	golang.org/x/crypto v0.9.0 | ||||||
| 	gorm.io/driver/mysql v1.4.3 | 	gorm.io/driver/mysql v1.4.3 | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								go.sum
									
									
									
									
									
								
							| @@ -67,6 +67,8 @@ github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyC | |||||||
| github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= | github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= | ||||||
| github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= | github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= | ||||||
| github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= | github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= | ||||||
|  | github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= | ||||||
|  | github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= | ||||||
| github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= | ||||||
| github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= | ||||||
| github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= | github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= | ||||||
|   | |||||||
| @@ -39,6 +39,8 @@ func InitOptionMap() { | |||||||
| 	common.OptionMap["DisplayInCurrencyEnabled"] = strconv.FormatBool(common.DisplayInCurrencyEnabled) | 	common.OptionMap["DisplayInCurrencyEnabled"] = strconv.FormatBool(common.DisplayInCurrencyEnabled) | ||||||
| 	common.OptionMap["DisplayTokenStatEnabled"] = strconv.FormatBool(common.DisplayTokenStatEnabled) | 	common.OptionMap["DisplayTokenStatEnabled"] = strconv.FormatBool(common.DisplayTokenStatEnabled) | ||||||
| 	common.OptionMap["ChannelDisableThreshold"] = strconv.FormatFloat(common.ChannelDisableThreshold, 'f', -1, 64) | 	common.OptionMap["ChannelDisableThreshold"] = strconv.FormatFloat(common.ChannelDisableThreshold, 'f', -1, 64) | ||||||
|  | 	common.OptionMap["EmailDomainRestrictionEnabled"] = strconv.FormatBool(common.EmailDomainRestrictionEnabled) | ||||||
|  | 	common.OptionMap["EmailDomainWhitelist"] = strings.Join(common.EmailDomainWhitelist, ",") | ||||||
| 	common.OptionMap["SMTPServer"] = "" | 	common.OptionMap["SMTPServer"] = "" | ||||||
| 	common.OptionMap["SMTPFrom"] = "" | 	common.OptionMap["SMTPFrom"] = "" | ||||||
| 	common.OptionMap["SMTPPort"] = strconv.Itoa(common.SMTPPort) | 	common.OptionMap["SMTPPort"] = strconv.Itoa(common.SMTPPort) | ||||||
| @@ -141,6 +143,8 @@ func updateOptionMap(key string, value string) (err error) { | |||||||
| 			common.TurnstileCheckEnabled = boolValue | 			common.TurnstileCheckEnabled = boolValue | ||||||
| 		case "RegisterEnabled": | 		case "RegisterEnabled": | ||||||
| 			common.RegisterEnabled = boolValue | 			common.RegisterEnabled = boolValue | ||||||
|  | 		case "EmailDomainRestrictionEnabled": | ||||||
|  | 			common.EmailDomainRestrictionEnabled = boolValue | ||||||
| 		case "AutomaticDisableChannelEnabled": | 		case "AutomaticDisableChannelEnabled": | ||||||
| 			common.AutomaticDisableChannelEnabled = boolValue | 			common.AutomaticDisableChannelEnabled = boolValue | ||||||
| 		case "ApproximateTokenEnabled": | 		case "ApproximateTokenEnabled": | ||||||
| @@ -154,6 +158,8 @@ func updateOptionMap(key string, value string) (err error) { | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	switch key { | 	switch key { | ||||||
|  | 	case "EmailDomainWhitelist": | ||||||
|  | 		common.EmailDomainWhitelist = strings.Split(value, ",") | ||||||
| 	case "SMTPServer": | 	case "SMTPServer": | ||||||
| 		common.SMTPServer = value | 		common.SMTPServer = value | ||||||
| 	case "SMTPPort": | 	case "SMTPPort": | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| import React, { useEffect, useState } from 'react'; | import React, { useEffect, useState } from 'react'; | ||||||
| import { Divider, Form, Grid, Header, Message } from 'semantic-ui-react'; | import { Button, Divider, Form, Grid, Header, Input, Message } from 'semantic-ui-react'; | ||||||
| import { API, removeTrailingSlash, showError, verifyJSON } from '../helpers'; | import { API, removeTrailingSlash, showError } from '../helpers'; | ||||||
|  |  | ||||||
| const SystemSetting = () => { | const SystemSetting = () => { | ||||||
|   let [inputs, setInputs] = useState({ |   let [inputs, setInputs] = useState({ | ||||||
| @@ -26,9 +26,13 @@ const SystemSetting = () => { | |||||||
|     TurnstileSiteKey: '', |     TurnstileSiteKey: '', | ||||||
|     TurnstileSecretKey: '', |     TurnstileSecretKey: '', | ||||||
|     RegisterEnabled: '', |     RegisterEnabled: '', | ||||||
|  |     EmailDomainRestrictionEnabled: '', | ||||||
|  |     EmailDomainWhitelist: '' | ||||||
|   }); |   }); | ||||||
|   const [originInputs, setOriginInputs] = useState({}); |   const [originInputs, setOriginInputs] = useState({}); | ||||||
|   let [loading, setLoading] = useState(false); |   let [loading, setLoading] = useState(false); | ||||||
|  |   const [EmailDomainWhitelist, setEmailDomainWhitelist] = useState([]); | ||||||
|  |   const [restrictedDomainInput, setRestrictedDomainInput] = useState(''); | ||||||
|  |  | ||||||
|   const getOptions = async () => { |   const getOptions = async () => { | ||||||
|     const res = await API.get('/api/option/'); |     const res = await API.get('/api/option/'); | ||||||
| @@ -38,8 +42,15 @@ const SystemSetting = () => { | |||||||
|       data.forEach((item) => { |       data.forEach((item) => { | ||||||
|         newInputs[item.key] = item.value; |         newInputs[item.key] = item.value; | ||||||
|       }); |       }); | ||||||
|       setInputs(newInputs); |       setInputs({ | ||||||
|  |         ...newInputs, | ||||||
|  |         EmailDomainWhitelist: newInputs.EmailDomainWhitelist.split(',') | ||||||
|  |       }); | ||||||
|       setOriginInputs(newInputs); |       setOriginInputs(newInputs); | ||||||
|  |  | ||||||
|  |       setEmailDomainWhitelist(newInputs.EmailDomainWhitelist.split(',').map((item) => { | ||||||
|  |         return { key: item, text: item, value: item }; | ||||||
|  |       })); | ||||||
|     } else { |     } else { | ||||||
|       showError(message); |       showError(message); | ||||||
|     } |     } | ||||||
| @@ -58,6 +69,7 @@ const SystemSetting = () => { | |||||||
|       case 'GitHubOAuthEnabled': |       case 'GitHubOAuthEnabled': | ||||||
|       case 'WeChatAuthEnabled': |       case 'WeChatAuthEnabled': | ||||||
|       case 'TurnstileCheckEnabled': |       case 'TurnstileCheckEnabled': | ||||||
|  |       case 'EmailDomainRestrictionEnabled': | ||||||
|       case 'RegisterEnabled': |       case 'RegisterEnabled': | ||||||
|         value = inputs[key] === 'true' ? 'false' : 'true'; |         value = inputs[key] === 'true' ? 'false' : 'true'; | ||||||
|         break; |         break; | ||||||
| @@ -70,7 +82,12 @@ const SystemSetting = () => { | |||||||
|     }); |     }); | ||||||
|     const { success, message } = res.data; |     const { success, message } = res.data; | ||||||
|     if (success) { |     if (success) { | ||||||
|       setInputs((inputs) => ({ ...inputs, [key]: value })); |       if (key === 'EmailDomainWhitelist') { | ||||||
|  |         value = value.split(','); | ||||||
|  |       } | ||||||
|  |       setInputs((inputs) => ({ | ||||||
|  |         ...inputs, [key]: value | ||||||
|  |       })); | ||||||
|     } else { |     } else { | ||||||
|       showError(message); |       showError(message); | ||||||
|     } |     } | ||||||
| @@ -88,7 +105,8 @@ const SystemSetting = () => { | |||||||
|       name === 'WeChatServerToken' || |       name === 'WeChatServerToken' || | ||||||
|       name === 'WeChatAccountQRCodeImageURL' || |       name === 'WeChatAccountQRCodeImageURL' || | ||||||
|       name === 'TurnstileSiteKey' || |       name === 'TurnstileSiteKey' || | ||||||
|       name === 'TurnstileSecretKey' |       name === 'TurnstileSecretKey' || | ||||||
|  |       name === 'EmailDomainWhitelist' | ||||||
|     ) { |     ) { | ||||||
|       setInputs((inputs) => ({ ...inputs, [name]: value })); |       setInputs((inputs) => ({ ...inputs, [name]: value })); | ||||||
|     } else { |     } else { | ||||||
| @@ -125,6 +143,16 @@ const SystemSetting = () => { | |||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   const submitEmailDomainWhitelist = async () => { | ||||||
|  |     if ( | ||||||
|  |       originInputs['EmailDomainWhitelist'] !== inputs.EmailDomainWhitelist.join(',') && | ||||||
|  |       inputs.SMTPToken !== '' | ||||||
|  |     ) { | ||||||
|  |       await updateOption('EmailDomainWhitelist', inputs.EmailDomainWhitelist.join(',')); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|   const submitWeChat = async () => { |   const submitWeChat = async () => { | ||||||
|     if (originInputs['WeChatServerAddress'] !== inputs.WeChatServerAddress) { |     if (originInputs['WeChatServerAddress'] !== inputs.WeChatServerAddress) { | ||||||
|       await updateOption( |       await updateOption( | ||||||
| @@ -173,6 +201,22 @@ const SystemSetting = () => { | |||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|  |   const submitNewRestrictedDomain = () => { | ||||||
|  |     const localDomainList = inputs.EmailDomainWhitelist; | ||||||
|  |     if (restrictedDomainInput !== '' && !localDomainList.includes(restrictedDomainInput)) { | ||||||
|  |       setRestrictedDomainInput(''); | ||||||
|  |       setInputs({ | ||||||
|  |         ...inputs, | ||||||
|  |         EmailDomainWhitelist: [...localDomainList, restrictedDomainInput], | ||||||
|  |       }); | ||||||
|  |       setEmailDomainWhitelist([...EmailDomainWhitelist, { | ||||||
|  |         key: restrictedDomainInput, | ||||||
|  |         text: restrictedDomainInput, | ||||||
|  |         value: restrictedDomainInput, | ||||||
|  |       }]); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Grid columns={1}> |     <Grid columns={1}> | ||||||
|       <Grid.Column> |       <Grid.Column> | ||||||
| @@ -239,6 +283,54 @@ const SystemSetting = () => { | |||||||
|             /> |             /> | ||||||
|           </Form.Group> |           </Form.Group> | ||||||
|           <Divider /> |           <Divider /> | ||||||
|  |           <Header as='h3'> | ||||||
|  |             配置邮箱域名白名单 | ||||||
|  |             <Header.Subheader>用以防止恶意用户利用临时邮箱批量注册</Header.Subheader> | ||||||
|  |           </Header> | ||||||
|  |           <Form.Group widths={3}> | ||||||
|  |             <Form.Checkbox | ||||||
|  |               label='启用邮箱域名白名单' | ||||||
|  |               name='EmailDomainRestrictionEnabled' | ||||||
|  |               onChange={handleInputChange} | ||||||
|  |               checked={inputs.EmailDomainRestrictionEnabled === 'true'} | ||||||
|  |             /> | ||||||
|  |           </Form.Group> | ||||||
|  |           <Form.Group widths={2}> | ||||||
|  |             <Form.Dropdown | ||||||
|  |               label='允许的邮箱域名' | ||||||
|  |               placeholder='允许的邮箱域名' | ||||||
|  |               name='EmailDomainWhitelist' | ||||||
|  |               required | ||||||
|  |               fluid | ||||||
|  |               multiple | ||||||
|  |               selection | ||||||
|  |               onChange={handleInputChange} | ||||||
|  |               value={inputs.EmailDomainWhitelist} | ||||||
|  |               autoComplete='new-password' | ||||||
|  |               options={EmailDomainWhitelist} | ||||||
|  |             /> | ||||||
|  |             <Form.Input | ||||||
|  |               label='添加新的允许的邮箱域名' | ||||||
|  |               action={ | ||||||
|  |                 <Button type='button' onClick={() => { | ||||||
|  |                   submitNewRestrictedDomain(); | ||||||
|  |                 }}>填入</Button> | ||||||
|  |               } | ||||||
|  |               onKeyDown={(e) => { | ||||||
|  |                 if (e.key === 'Enter') { | ||||||
|  |                   submitNewRestrictedDomain(); | ||||||
|  |                 } | ||||||
|  |               }} | ||||||
|  |               autoComplete='new-password' | ||||||
|  |               placeholder='输入新的允许的邮箱域名' | ||||||
|  |               value={restrictedDomainInput} | ||||||
|  |               onChange={(e, { value }) => { | ||||||
|  |                 setRestrictedDomainInput(value); | ||||||
|  |               }} | ||||||
|  |             /> | ||||||
|  |           </Form.Group> | ||||||
|  |           <Form.Button onClick={submitEmailDomainWhitelist}>保存邮箱域名白名单设置</Form.Button> | ||||||
|  |           <Divider /> | ||||||
|           <Header as='h3'> |           <Header as='h3'> | ||||||
|             配置 SMTP |             配置 SMTP | ||||||
|             <Header.Subheader>用以支持系统的邮件发送</Header.Subheader> |             <Header.Subheader>用以支持系统的邮件发送</Header.Subheader> | ||||||
| @@ -284,7 +376,7 @@ const SystemSetting = () => { | |||||||
|               onChange={handleInputChange} |               onChange={handleInputChange} | ||||||
|               type='password' |               type='password' | ||||||
|               autoComplete='new-password' |               autoComplete='new-password' | ||||||
|               value={inputs.SMTPToken} |               checked={inputs.RegisterEnabled === 'true'} | ||||||
|               placeholder='敏感信息不会发送到前端显示' |               placeholder='敏感信息不会发送到前端显示' | ||||||
|             /> |             /> | ||||||
|           </Form.Group> |           </Form.Group> | ||||||
|   | |||||||
| @@ -1,11 +1,17 @@ | |||||||
| import React, { useEffect, useState } from 'react'; | import React, { useEffect, useState } from 'react'; | ||||||
| import { Button, Form, Label, Modal, Pagination, Popup, Table } from 'semantic-ui-react'; | import { Button, Dropdown, Form, Label, Pagination, Popup, Table } from 'semantic-ui-react'; | ||||||
| import { Link } from 'react-router-dom'; | import { Link } from 'react-router-dom'; | ||||||
| import { API, copy, showError, showSuccess, showWarning, timestamp2string } from '../helpers'; | import { API, copy, showError, showSuccess, showWarning, timestamp2string } from '../helpers'; | ||||||
|  |  | ||||||
| import { ITEMS_PER_PAGE } from '../constants'; | import { ITEMS_PER_PAGE } from '../constants'; | ||||||
| import { renderQuota } from '../helpers/render'; | import { renderQuota } from '../helpers/render'; | ||||||
|  |  | ||||||
|  | const COPY_OPTIONS = [ | ||||||
|  |   { key: 'next', text: 'ChatGPT Next Web', value: 'next' }, | ||||||
|  |   { key: 'ama', text: 'AMA 问天', value: 'ama' }, | ||||||
|  |   { key: 'opencat', text: 'OpenCat', value: 'opencat' }, | ||||||
|  | ]; | ||||||
|  |  | ||||||
| function renderTimestamp(timestamp) { | function renderTimestamp(timestamp) { | ||||||
|   return ( |   return ( | ||||||
|     <> |     <> | ||||||
| @@ -68,7 +74,40 @@ const TokensTable = () => { | |||||||
|   const refresh = async () => { |   const refresh = async () => { | ||||||
|     setLoading(true); |     setLoading(true); | ||||||
|     await loadTokens(activePage - 1); |     await loadTokens(activePage - 1); | ||||||
|   } |   }; | ||||||
|  |  | ||||||
|  |   const onCopy = async (type, key) => { | ||||||
|  |     let status = localStorage.getItem('status'); | ||||||
|  |     let serverAddress = ''; | ||||||
|  |     if (status) { | ||||||
|  |       status = JSON.parse(status); | ||||||
|  |       serverAddress = status.server_address; | ||||||
|  |     } | ||||||
|  |     if (serverAddress === '') { | ||||||
|  |       serverAddress = window.location.origin; | ||||||
|  |     } | ||||||
|  |     let encodedServerAddress = encodeURIComponent(serverAddress); | ||||||
|  |     let url; | ||||||
|  |     switch (type) { | ||||||
|  |       case 'ama': | ||||||
|  |         url = `ama://set-api-key?server=${encodedServerAddress}&key=sk-${key}`; | ||||||
|  |         break; | ||||||
|  |       case 'opencat': | ||||||
|  |         url = `opencat://team/join?domain=${encodedServerAddress}&token=sk-${key}`; | ||||||
|  |         break; | ||||||
|  |       case 'next': | ||||||
|  |         url = `https://chat.oneapi.pro/#/?settings=%7B%22key%22:%22sk-${key}%22,%22url%22:%22${serverAddress}%22%7D`; | ||||||
|  |         break; | ||||||
|  |       default: | ||||||
|  |         url = `sk-${key}`; | ||||||
|  |     } | ||||||
|  |     if (await copy(url)) { | ||||||
|  |       showSuccess('已复制到剪贴板!'); | ||||||
|  |     } else { | ||||||
|  |       showWarning('无法复制到剪贴板,请手动复制,已将令牌填入搜索框。'); | ||||||
|  |       setSearchKeyword(url); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     loadTokens(0) |     loadTokens(0) | ||||||
| @@ -235,21 +274,28 @@ const TokensTable = () => { | |||||||
|                   <Table.Cell>{token.expired_time === -1 ? '永不过期' : renderTimestamp(token.expired_time)}</Table.Cell> |                   <Table.Cell>{token.expired_time === -1 ? '永不过期' : renderTimestamp(token.expired_time)}</Table.Cell> | ||||||
|                   <Table.Cell> |                   <Table.Cell> | ||||||
|                     <div> |                     <div> | ||||||
|                       <Button |                       <Button.Group color='green' size={'small'}> | ||||||
|                         size={'small'} |                         <Button | ||||||
|                         positive |                           size={'small'} | ||||||
|                         onClick={async () => { |                           positive | ||||||
|                           let key = "sk-" + token.key; |                           onClick={async () => { | ||||||
|                           if (await copy(key)) { |                             await onCopy('', token.key); | ||||||
|                             showSuccess('已复制到剪贴板!'); |  | ||||||
|                           } else { |  | ||||||
|                             showWarning('无法复制到剪贴板,请手动复制,已将令牌填入搜索框。'); |  | ||||||
|                             setSearchKeyword(key); |  | ||||||
|                           } |                           } | ||||||
|                         }} |                           } | ||||||
|                       > |                         > | ||||||
|                         复制 |                           复制 | ||||||
|                       </Button> |                         </Button> | ||||||
|  |                         <Dropdown | ||||||
|  |                           className='button icon' | ||||||
|  |                           floating | ||||||
|  |                           options={COPY_OPTIONS} | ||||||
|  |                           onChange={async (e, { value } = {}) => { | ||||||
|  |                             await onCopy(value, token.key); | ||||||
|  |                           }} | ||||||
|  |                           trigger={<></>} | ||||||
|  |                         /> | ||||||
|  |                       </Button.Group> | ||||||
|  |                       {' '} | ||||||
|                       <Popup |                       <Popup | ||||||
|                         trigger={ |                         trigger={ | ||||||
|                           <Button size='small' negative> |                           <Button size='small' negative> | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ export const CHANNEL_OPTIONS = [ | |||||||
|   { key: 11, text: 'Google PaLM2', value: 11, color: 'orange' }, |   { key: 11, text: 'Google PaLM2', value: 11, color: 'orange' }, | ||||||
|   { key: 15, text: '百度文心千帆', value: 15, color: 'blue' }, |   { key: 15, text: '百度文心千帆', value: 15, color: 'blue' }, | ||||||
|   { key: 17, text: '阿里通义千问', value: 17, color: 'orange' }, |   { key: 17, text: '阿里通义千问', value: 17, color: 'orange' }, | ||||||
|  |   { key: 18, text: '讯飞星火认知', value: 18, color: 'blue' }, | ||||||
|   { key: 16, text: '智谱 ChatGLM', value: 16, color: 'violet' }, |   { key: 16, text: '智谱 ChatGLM', value: 16, color: 'violet' }, | ||||||
|   { key: 8, text: '自定义渠道', value: 8, color: 'pink' }, |   { key: 8, text: '自定义渠道', value: 8, color: 'pink' }, | ||||||
|   { key: 2, text: '代理:API2D', value: 2, color: 'blue' }, |   { key: 2, text: '代理:API2D', value: 2, color: 'blue' }, | ||||||
| @@ -15,5 +16,5 @@ export const CHANNEL_OPTIONS = [ | |||||||
|   { key: 6, text: '代理:OpenAI Max', value: 6, color: 'violet' }, |   { key: 6, text: '代理:OpenAI Max', value: 6, color: 'violet' }, | ||||||
|   { key: 9, text: '代理:AI.LS', value: 9, color: 'yellow' }, |   { key: 9, text: '代理:AI.LS', value: 9, color: 'yellow' }, | ||||||
|   { key: 12, text: '代理:API2GPT', value: 12, color: 'blue' }, |   { key: 12, text: '代理:API2GPT', value: 12, color: 'blue' }, | ||||||
|   { key: 13, text: '代理:AIGC2D', value: 13, color: 'purple' }, |   { key: 13, text: '代理:AIGC2D', value: 13, color: 'purple' } | ||||||
| ]; | ]; | ||||||
| @@ -53,6 +53,9 @@ const EditChannel = () => { | |||||||
|         case 16: |         case 16: | ||||||
|           localModels = ['chatglm_pro', 'chatglm_std', 'chatglm_lite']; |           localModels = ['chatglm_pro', 'chatglm_std', 'chatglm_lite']; | ||||||
|           break; |           break; | ||||||
|  |         case 18: | ||||||
|  |           localModels = ['SparkDesk']; | ||||||
|  |           break; | ||||||
|       } |       } | ||||||
|       setInputs((inputs) => ({ ...inputs, models: localModels })); |       setInputs((inputs) => ({ ...inputs, models: localModels })); | ||||||
|     } |     } | ||||||
| @@ -347,7 +350,7 @@ const EditChannel = () => { | |||||||
|                 label='密钥' |                 label='密钥' | ||||||
|                 name='key' |                 name='key' | ||||||
|                 required |                 required | ||||||
|                 placeholder={inputs.type === 15 ? '请输入 access token,当前版本暂不支持自动刷新,请每 30 天更新一次' : '请输入渠道对应的鉴权密钥'} |                 placeholder={inputs.type === 15 ? '请输入 access token,当前版本暂不支持自动刷新,请每 30 天更新一次' : (inputs.type === 18 ? '按照如下格式输入:APPID|APISecret|APIKey' : '请输入渠道对应的鉴权密钥')} | ||||||
|                 onChange={handleInputChange} |                 onChange={handleInputChange} | ||||||
|                 value={inputs.key} |                 value={inputs.key} | ||||||
|                 autoComplete='new-password' |                 autoComplete='new-password' | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user