Merge branch 'main' into telegram-login

This commit is contained in:
Ehco 2024-03-03 19:42:06 +08:00 committed by GitHub
commit 02d5a5f16d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 233 additions and 116 deletions

View File

@ -9,14 +9,19 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
) )
// Pay Settings
var PayAddress = ""
var CustomCallbackAddress = ""
var EpayId = ""
var EpayKey = ""
var Price = 7.3
var MinTopUp = 1
var StartTime = time.Now().Unix() // unit: second var StartTime = time.Now().Unix() // unit: second
var Version = "v0.0.0" // this hard coding will be replaced automatically when building, no need to manually change var Version = "v0.0.0" // this hard coding will be replaced automatically when building, no need to manually change
var SystemName = "New API" var SystemName = "New API"
var ServerAddress = "http://localhost:3000" var ServerAddress = "http://localhost:3000"
var PayAddress = ""
var EpayId = ""
var EpayKey = ""
var Price = 7.3
var Footer = "" var Footer = ""
var Logo = "" var Logo = ""
var TopUpLink = "" var TopUpLink = ""
@ -29,6 +34,7 @@ var DrawingEnabled = true
var DataExportEnabled = true var DataExportEnabled = true
var DataExportInterval = 5 // unit: minute var DataExportInterval = 5 // unit: minute
var DataExportDefaultTime = "hour" // unit: minute var DataExportDefaultTime = "hour" // unit: minute
var DefaultCollapseSidebar = false // default value of collapse sidebar
// Any options with "Secret", "Token" in its key won't be return by GetOptions // Any options with "Secret", "Token" in its key won't be return by GetOptions

View File

@ -80,7 +80,10 @@ var ModelRatio = map[string]float64{
"qwen-turbo": 0.8572, // ¥0.012 / 1k tokens "qwen-turbo": 0.8572, // ¥0.012 / 1k tokens
"qwen-plus": 10, // ¥0.14 / 1k tokens "qwen-plus": 10, // ¥0.14 / 1k tokens
"text-embedding-v1": 0.05, // ¥0.0007 / 1k tokens "text-embedding-v1": 0.05, // ¥0.0007 / 1k tokens
"SparkDesk": 1.2858, // ¥0.018 / 1k tokens "SparkDesk-v1.1": 1.2858, // ¥0.018 / 1k tokens
"SparkDesk-v2.1": 1.2858, // ¥0.018 / 1k tokens
"SparkDesk-v3.1": 1.2858, // ¥0.018 / 1k tokens
"SparkDesk-v3.5": 1.2858, // ¥0.018 / 1k tokens
"360GPT_S2_V9": 0.8572, // ¥0.012 / 1k tokens "360GPT_S2_V9": 0.8572, // ¥0.012 / 1k tokens
"embedding-bert-512-v1": 0.0715, // ¥0.001 / 1k tokens "embedding-bert-512-v1": 0.0715, // ¥0.001 / 1k tokens
"embedding_s1_v1": 0.0715, // ¥0.001 / 1k tokens "embedding_s1_v1": 0.0715, // ¥0.001 / 1k tokens

View File

@ -54,8 +54,9 @@ func FixChannelsAbilities(c *gin.Context) {
func SearchChannels(c *gin.Context) { func SearchChannels(c *gin.Context) {
keyword := c.Query("keyword") keyword := c.Query("keyword")
group := c.Query("group") group := c.Query("group")
modelKeyword := c.Query("model")
//idSort, _ := strconv.ParseBool(c.Query("id_sort")) //idSort, _ := strconv.ParseBool(c.Query("id_sort"))
channels, err := model.SearchChannels(keyword, group) channels, err := model.SearchChannels(keyword, group, modelKeyword)
if err != nil { if err != nil {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": false, "success": false,

View File

@ -29,6 +29,7 @@ func GetStatus(c *gin.Context) {
"wechat_login": common.WeChatAuthEnabled, "wechat_login": common.WeChatAuthEnabled,
"server_address": common.ServerAddress, "server_address": common.ServerAddress,
"price": common.Price, "price": common.Price,
"min_topup": common.MinTopUp,
"turnstile_check": common.TurnstileCheckEnabled, "turnstile_check": common.TurnstileCheckEnabled,
"turnstile_site_key": common.TurnstileSiteKey, "turnstile_site_key": common.TurnstileSiteKey,
"top_up_link": common.TopUpLink, "top_up_link": common.TopUpLink,
@ -40,6 +41,7 @@ func GetStatus(c *gin.Context) {
"enable_drawing": common.DrawingEnabled, "enable_drawing": common.DrawingEnabled,
"enable_data_export": common.DataExportEnabled, "enable_data_export": common.DataExportEnabled,
"data_export_default_time": common.DataExportDefaultTime, "data_export_default_time": common.DataExportDefaultTime,
"default_collapse_sidebar": common.DefaultCollapseSidebar,
"enable_online_topup": common.PayAddress != "" && common.EpayId != "" && common.EpayKey != "", "enable_online_topup": common.PayAddress != "" && common.EpayId != "" && common.EpayKey != "",
}, },
}) })

View File

@ -129,6 +129,13 @@ func ListModels(c *gin.Context) {
}) })
} }
func ChannelListModels(c *gin.Context) {
c.JSON(200, gin.H{
"object": "list",
"data": openAIModels,
})
}
func RetrieveModel(c *gin.Context) { func RetrieveModel(c *gin.Context) {
modelId := c.Param("model") modelId := c.Param("model")
if model, ok := openAIModelsMap[modelId]; ok { if model, ok := openAIModelsMap[modelId]; ok {

View File

@ -9,6 +9,7 @@ import (
"net/url" "net/url"
"one-api/common" "one-api/common"
"one-api/model" "one-api/model"
"one-api/service"
"strconv" "strconv"
"time" "time"
) )
@ -55,14 +56,14 @@ func RequestEpay(c *gin.Context) {
c.JSON(200, gin.H{"message": err.Error(), "data": 10}) c.JSON(200, gin.H{"message": err.Error(), "data": 10})
return return
} }
if req.Amount < 1 { if req.Amount < common.MinTopUp {
c.JSON(200, gin.H{"message": "充值金额不能小于1", "data": 10}) c.JSON(200, gin.H{"message": fmt.Sprintf("充值数量不能小于 %d", common.MinTopUp), "data": 10})
return return
} }
id := c.GetInt("id") id := c.GetInt("id")
user, _ := model.GetUserById(id, false) user, _ := model.GetUserById(id, false)
amount := GetAmount(float64(req.Amount), *user) payMoney := GetAmount(float64(req.Amount), *user)
var payType epay.PurchaseType var payType epay.PurchaseType
if req.PaymentMethod == "zfb" { if req.PaymentMethod == "zfb" {
@ -72,11 +73,10 @@ func RequestEpay(c *gin.Context) {
req.PaymentMethod = "wxpay" req.PaymentMethod = "wxpay"
payType = epay.WechatPay payType = epay.WechatPay
} }
callBackAddress := service.GetCallbackAddress()
returnUrl, _ := url.Parse(common.ServerAddress + "/log") returnUrl, _ := url.Parse(common.ServerAddress + "/log")
notifyUrl, _ := url.Parse(common.ServerAddress + "/api/user/epay/notify") notifyUrl, _ := url.Parse(callBackAddress + "/api/user/epay/notify")
tradeNo := strconv.FormatInt(time.Now().Unix(), 10) tradeNo := strconv.FormatInt(time.Now().Unix(), 10)
payMoney := amount
client := GetEpayClient() client := GetEpayClient()
if client == nil { if client == nil {
c.JSON(200, gin.H{"message": "error", "data": "当前管理员未配置支付信息"}) c.JSON(200, gin.H{"message": "error", "data": "当前管理员未配置支付信息"})
@ -169,8 +169,8 @@ func RequestAmount(c *gin.Context) {
c.JSON(200, gin.H{"message": "error", "data": "参数错误"}) c.JSON(200, gin.H{"message": "error", "data": "参数错误"})
return return
} }
if req.Amount < 1 { if req.Amount < common.MinTopUp {
c.JSON(200, gin.H{"message": "error", "data": "充值金额不能小于1"}) c.JSON(200, gin.H{"message": "error", "data": fmt.Sprintf("充值数量不能小于 %d", common.MinTopUp)})
return return
} }
id := c.GetInt("id") id := c.GetInt("id")

View File

@ -291,24 +291,27 @@ func CacheGetRandomSatisfiedChannel(group string, model string) (*Channel, error
} }
} }
} }
// 平滑系数
smoothingFactor := 10
// Calculate the total weight of all channels up to endIdx // Calculate the total weight of all channels up to endIdx
totalWeight := 0 totalWeight := 0
for _, channel := range channels[:endIdx] { for _, channel := range channels[:endIdx] {
totalWeight += channel.GetWeight() totalWeight += channel.GetWeight() + smoothingFactor
} }
if totalWeight == 0 { //if totalWeight == 0 {
// If all weights are 0, select a channel randomly // // If all weights are 0, select a channel randomly
return channels[rand.Intn(endIdx)], nil // return channels[rand.Intn(endIdx)], nil
} //}
// Generate a random value in the range [0, totalWeight) // Generate a random value in the range [0, totalWeight)
randomWeight := rand.Intn(totalWeight) randomWeight := rand.Intn(totalWeight)
// Find a channel based on its weight // Find a channel based on its weight
for _, channel := range channels[:endIdx] { for _, channel := range channels[:endIdx] {
randomWeight -= channel.GetWeight() randomWeight -= channel.GetWeight() + smoothingFactor
if randomWeight <= 0 { if randomWeight < 0 {
return channel, nil return channel, nil
} }
} }

View File

@ -43,21 +43,39 @@ func GetAllChannels(startIdx int, num int, selectAll bool, idSort bool) ([]*Chan
return channels, err return channels, err
} }
func SearchChannels(keyword string, group string) (channels []*Channel, err error) { func SearchChannels(keyword string, group string, model string) ([]*Channel, error) {
var channels []*Channel
keyCol := "`key`" keyCol := "`key`"
groupCol := "`group`"
modelsCol := "`models`"
// 如果是 PostgreSQL使用双引号
if common.UsingPostgreSQL { if common.UsingPostgreSQL {
keyCol = `"key"` keyCol = `"key"`
groupCol = `"group"`
modelsCol = `"models"`
} }
// 构造基础查询
baseQuery := DB.Model(&Channel{}).Omit(keyCol)
// 构造WHERE子句
var whereClause string
var args []interface{}
if group != "" { if group != "" {
groupCol := "`group`" whereClause = "(id = ? OR name LIKE ? OR " + keyCol + " = ?) AND " + groupCol + " LIKE ? AND " + modelsCol + " LIKE ?"
if common.UsingPostgreSQL { args = append(args, common.String2Int(keyword), "%"+keyword+"%", keyword, "%"+group+"%", "%"+model+"%")
groupCol = `"group"`
}
err = DB.Omit("key").Where("(id = ? or name LIKE ? or "+keyCol+" = ?) and "+groupCol+" LIKE ?", common.String2Int(keyword), keyword+"%", keyword, "%"+group+"%").Find(&channels).Error
} else { } else {
err = DB.Omit("key").Where("id = ? or name LIKE ? or "+keyCol+" = ?", common.String2Int(keyword), keyword+"%", keyword).Find(&channels).Error whereClause = "(id = ? OR name LIKE ? OR " + keyCol + " = ?) AND " + modelsCol + " LIKE ?"
args = append(args, common.String2Int(keyword), "%"+keyword+"%", keyword, "%"+model+"%")
} }
return channels, err
// 执行查询
err := baseQuery.Where(whereClause, args...).Find(&channels).Error
if err != nil {
return nil, err
}
return channels, nil
} }
func GetChannelById(id int, selectAll bool) (*Channel, error) { func GetChannelById(id int, selectAll bool) (*Channel, error) {

View File

@ -57,9 +57,11 @@ func InitOptionMap() {
common.OptionMap["Logo"] = common.Logo common.OptionMap["Logo"] = common.Logo
common.OptionMap["ServerAddress"] = "" common.OptionMap["ServerAddress"] = ""
common.OptionMap["PayAddress"] = "" common.OptionMap["PayAddress"] = ""
common.OptionMap["CustomCallbackAddress"] = ""
common.OptionMap["EpayId"] = "" common.OptionMap["EpayId"] = ""
common.OptionMap["EpayKey"] = "" common.OptionMap["EpayKey"] = ""
common.OptionMap["Price"] = strconv.FormatFloat(common.Price, 'f', -1, 64) common.OptionMap["Price"] = strconv.FormatFloat(common.Price, 'f', -1, 64)
common.OptionMap["MinTopUp"] = strconv.Itoa(common.MinTopUp)
common.OptionMap["TopupGroupRatio"] = common.TopupGroupRatio2JSONString() common.OptionMap["TopupGroupRatio"] = common.TopupGroupRatio2JSONString()
common.OptionMap["GitHubClientId"] = "" common.OptionMap["GitHubClientId"] = ""
common.OptionMap["GitHubClientSecret"] = "" common.OptionMap["GitHubClientSecret"] = ""
@ -85,6 +87,7 @@ func InitOptionMap() {
common.OptionMap["RetryTimes"] = strconv.Itoa(common.RetryTimes) common.OptionMap["RetryTimes"] = strconv.Itoa(common.RetryTimes)
common.OptionMap["DataExportInterval"] = strconv.Itoa(common.DataExportInterval) common.OptionMap["DataExportInterval"] = strconv.Itoa(common.DataExportInterval)
common.OptionMap["DataExportDefaultTime"] = common.DataExportDefaultTime common.OptionMap["DataExportDefaultTime"] = common.DataExportDefaultTime
common.OptionMap["DefaultCollapseSidebar"] = strconv.FormatBool(common.DefaultCollapseSidebar)
common.OptionMapRWMutex.Unlock() common.OptionMapRWMutex.Unlock()
loadOptionsFromDatabase() loadOptionsFromDatabase()
@ -141,7 +144,7 @@ func updateOptionMap(key string, value string) (err error) {
common.ImageDownloadPermission = intValue common.ImageDownloadPermission = intValue
} }
} }
if strings.HasSuffix(key, "Enabled") { if strings.HasSuffix(key, "Enabled") || key == "DefaultCollapseSidebar" {
boolValue := value == "true" boolValue := value == "true"
switch key { switch key {
case "PasswordRegisterEnabled": case "PasswordRegisterEnabled":
@ -176,6 +179,8 @@ func updateOptionMap(key string, value string) (err error) {
common.DrawingEnabled = boolValue common.DrawingEnabled = boolValue
case "DataExportEnabled": case "DataExportEnabled":
common.DataExportEnabled = boolValue common.DataExportEnabled = boolValue
case "DefaultCollapseSidebar":
common.DefaultCollapseSidebar = boolValue
} }
} }
switch key { switch key {
@ -196,12 +201,16 @@ func updateOptionMap(key string, value string) (err error) {
common.ServerAddress = value common.ServerAddress = value
case "PayAddress": case "PayAddress":
common.PayAddress = value common.PayAddress = value
case "CustomCallbackAddress":
common.CustomCallbackAddress = value
case "EpayId": case "EpayId":
common.EpayId = value common.EpayId = value
case "EpayKey": case "EpayKey":
common.EpayKey = value common.EpayKey = value
case "Price": case "Price":
common.Price, _ = strconv.ParseFloat(value, 64) common.Price, _ = strconv.ParseFloat(value, 64)
case "MinTopUp":
common.MinTopUp, _ = strconv.Atoi(value)
case "TopupGroupRatio": case "TopupGroupRatio":
err = common.UpdateTopupGroupRatioByJSONString(value) err = common.UpdateTopupGroupRatioByJSONString(value)
case "GitHubClientId": case "GitHubClientId":

View File

@ -71,10 +71,10 @@ func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, request
func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage *dto.Usage, err *dto.OpenAIErrorWithStatusCode) { func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage *dto.Usage, err *dto.OpenAIErrorWithStatusCode) {
if info.IsStream { if info.IsStream {
var responseText string var responseText string
err, responseText = openaiStreamHandler(c, resp, info.RelayMode) err, responseText = OpenaiStreamHandler(c, resp, info.RelayMode)
usage = service.ResponseText2Usage(responseText, info.UpstreamModelName, info.PromptTokens) usage = service.ResponseText2Usage(responseText, info.UpstreamModelName, info.PromptTokens)
} else { } else {
err, usage = openaiHandler(c, resp, info.PromptTokens, info.UpstreamModelName) err, usage = OpenaiHandler(c, resp, info.PromptTokens, info.UpstreamModelName)
} }
return return
} }

View File

@ -16,7 +16,7 @@ import (
"time" "time"
) )
func openaiStreamHandler(c *gin.Context, resp *http.Response, relayMode int) (*dto.OpenAIErrorWithStatusCode, string) { func OpenaiStreamHandler(c *gin.Context, resp *http.Response, relayMode int) (*dto.OpenAIErrorWithStatusCode, string) {
var responseTextBuilder strings.Builder var responseTextBuilder strings.Builder
scanner := bufio.NewScanner(resp.Body) scanner := bufio.NewScanner(resp.Body)
scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) { scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
@ -111,7 +111,7 @@ func openaiStreamHandler(c *gin.Context, resp *http.Response, relayMode int) (*d
return nil, responseTextBuilder.String() return nil, responseTextBuilder.String()
} }
func openaiHandler(c *gin.Context, resp *http.Response, promptTokens int, model string) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) { func OpenaiHandler(c *gin.Context, resp *http.Response, promptTokens int, model string) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
var textResponse dto.TextResponse var textResponse dto.TextResponse
responseBody, err := io.ReadAll(resp.Body) responseBody, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {

View File

@ -1,4 +1,4 @@
package zhipu_v4 package zhipu_4v
import ( import (
"errors" "errors"
@ -8,7 +8,9 @@ import (
"net/http" "net/http"
"one-api/dto" "one-api/dto"
"one-api/relay/channel" "one-api/relay/channel"
"one-api/relay/channel/openai"
relaycommon "one-api/relay/common" relaycommon "one-api/relay/common"
"one-api/service"
) )
type Adaptor struct { type Adaptor struct {
@ -41,9 +43,11 @@ func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, request
func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage *dto.Usage, err *dto.OpenAIErrorWithStatusCode) { func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage *dto.Usage, err *dto.OpenAIErrorWithStatusCode) {
if info.IsStream { if info.IsStream {
err, usage = zhipuStreamHandler(c, resp) var responseText string
err, responseText = openai.OpenaiStreamHandler(c, resp, info.RelayMode)
usage = service.ResponseText2Usage(responseText, info.UpstreamModelName, info.PromptTokens)
} else { } else {
err, usage = zhipuHandler(c, resp) err, usage = openai.OpenaiHandler(c, resp, info.PromptTokens, info.UpstreamModelName)
} }
return return
} }

View File

@ -1,7 +1,7 @@
package zhipu_v4 package zhipu_4v
var ModelList = []string{ var ModelList = []string{
"glm-4", "glm-4v", "glm-3-turbo", "glm-4", "glm-4v", "glm-3-turbo",
} }
var ChannelName = "zhipu_v4" var ChannelName = "zhipu_4v"

View File

@ -1,4 +1,4 @@
package zhipu_v4 package zhipu_4v
import ( import (
"one-api/dto" "one-api/dto"

View File

@ -1,4 +1,4 @@
package zhipu_v4 package zhipu_4v
import ( import (
"bufio" "bufio"

View File

@ -59,6 +59,7 @@ func getAndValidateTextRequest(c *gin.Context, relayInfo *relaycommon.RelayInfo)
} }
} }
relayInfo.IsStream = textRequest.Stream relayInfo.IsStream = textRequest.Stream
relayInfo.UpstreamModelName = textRequest.Model
return textRequest, nil return textRequest, nil
} }

View File

@ -11,7 +11,7 @@ import (
"one-api/relay/channel/tencent" "one-api/relay/channel/tencent"
"one-api/relay/channel/xunfei" "one-api/relay/channel/xunfei"
"one-api/relay/channel/zhipu" "one-api/relay/channel/zhipu"
"one-api/relay/channel/zhipu_v4" "one-api/relay/channel/zhipu_4v"
"one-api/relay/constant" "one-api/relay/constant"
) )
@ -38,7 +38,7 @@ func GetAdaptor(apiType int) channel.Adaptor {
case constant.APITypeZhipu: case constant.APITypeZhipu:
return &zhipu.Adaptor{} return &zhipu.Adaptor{}
case constant.APITypeZhipu_v4: case constant.APITypeZhipu_v4:
return &zhipu_v4.Adaptor{} return &zhipu_4v.Adaptor{}
} }
return nil return nil
} }

View File

@ -75,7 +75,7 @@ func SetApiRouter(router *gin.Engine) {
{ {
channelRoute.GET("/", controller.GetAllChannels) channelRoute.GET("/", controller.GetAllChannels)
channelRoute.GET("/search", controller.SearchChannels) channelRoute.GET("/search", controller.SearchChannels)
channelRoute.GET("/models", controller.ListModels) channelRoute.GET("/models", controller.ChannelListModels)
channelRoute.GET("/:id", controller.GetChannel) channelRoute.GET("/:id", controller.GetChannel)
channelRoute.GET("/test", controller.TestAllChannels) channelRoute.GET("/test", controller.TestAllChannels)
channelRoute.GET("/test/:id", controller.TestChannel) channelRoute.GET("/test/:id", controller.TestChannel)

10
service/epay.go Normal file
View File

@ -0,0 +1,10 @@
package service
import "one-api/common"
func GetCallbackAddress() string {
if common.CustomCallbackAddress == "" {
return common.ServerAddress
}
return common.CustomCallbackAddress
}

View File

@ -29,7 +29,7 @@ const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About')); const About = lazy(() => import('./pages/About'));
function App() { function App() {
const [userState, userDispatch] = useContext(UserContext); const [userState, userDispatch] = useContext(UserContext);
const [statusState, statusDispatch] = useContext(StatusContext); // const [statusState, statusDispatch] = useContext(StatusContext);
const loadUser = () => { const loadUser = () => {
let user = localStorage.getItem('user'); let user = localStorage.getItem('user');
@ -38,47 +38,9 @@ function App() {
userDispatch({ type: 'login', payload: data }); userDispatch({ type: 'login', payload: data });
} }
}; };
const loadStatus = async () => {
const res = await API.get('/api/status');
const { success, data } = res.data;
if (success) {
localStorage.setItem('status', JSON.stringify(data));
statusDispatch({ type: 'set', payload: data });
localStorage.setItem('system_name', data.system_name);
localStorage.setItem('logo', data.logo);
localStorage.setItem('footer_html', data.footer_html);
localStorage.setItem('quota_per_unit', data.quota_per_unit);
localStorage.setItem('display_in_currency', data.display_in_currency);
localStorage.setItem('enable_drawing', data.enable_drawing);
localStorage.setItem('enable_data_export', data.enable_data_export);
localStorage.setItem('data_export_default_time', data.data_export_default_time);
if (data.chat_link) {
localStorage.setItem('chat_link', data.chat_link);
} else {
localStorage.removeItem('chat_link');
}
if (data.chat_link2) {
localStorage.setItem('chat_link2', data.chat_link2);
} else {
localStorage.removeItem('chat_link2');
}
// if (
// data.version !== process.env.REACT_APP_VERSION &&
// data.version !== 'v0.0.0' &&
// process.env.REACT_APP_VERSION !== ''
// ) {
// showNotice(
// `新版本可用:${data.version},请使用快捷键 Shift + F5 刷新页面`
// );
// }
} else {
showError('无法正常连接至服务器!');
}
};
useEffect(() => { useEffect(() => {
loadUser(); loadUser();
loadStatus().then();
let systemName = getSystemName(); let systemName = getSystemName();
if (systemName) { if (systemName) {
document.title = systemName; document.title = systemName;

View File

@ -257,6 +257,7 @@ const ChannelsTable = () => {
const [idSort, setIdSort] = useState(false); const [idSort, setIdSort] = useState(false);
const [searchKeyword, setSearchKeyword] = useState(''); const [searchKeyword, setSearchKeyword] = useState('');
const [searchGroup, setSearchGroup] = useState(''); const [searchGroup, setSearchGroup] = useState('');
const [searchModel, setSearchModel] = useState('');
const [searching, setSearching] = useState(false); const [searching, setSearching] = useState(false);
const [updatingBalance, setUpdatingBalance] = useState(false); const [updatingBalance, setUpdatingBalance] = useState(false);
const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE); const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
@ -440,15 +441,15 @@ const ChannelsTable = () => {
} }
}; };
const searchChannels = async (searchKeyword, searchGroup) => { const searchChannels = async (searchKeyword, searchGroup, searchModel) => {
if (searchKeyword === '' && searchGroup === '') { if (searchKeyword === '' && searchGroup === '' && searchModel === '') {
// if keyword is blank, load files instead. // if keyword is blank, load files instead.
await loadChannels(0, pageSize, idSort); await loadChannels(0, pageSize, idSort);
setActivePage(1); setActivePage(1);
return; return;
} }
setSearching(true); setSearching(true);
const res = await API.get(`/api/channel/search?keyword=${searchKeyword}&group=${searchGroup}`); const res = await API.get(`/api/channel/search?keyword=${searchKeyword}&group=${searchGroup}&model=${searchModel}`);
const {success, message, data} = res.data; const {success, message, data} = res.data;
if (success) { if (success) {
setChannels(data); setChannels(data);
@ -625,13 +626,12 @@ const ChannelsTable = () => {
return ( return (
<> <>
<EditChannel refresh={refresh} visible={showEdit} handleClose={closeEdit} editingChannel={editingChannel}/> <EditChannel refresh={refresh} visible={showEdit} handleClose={closeEdit} editingChannel={editingChannel}/>
<Form onSubmit={() => {searchChannels(searchKeyword, searchGroup)}} labelPosition='left'> <Form onSubmit={() => {searchChannels(searchKeyword, searchGroup, searchModel)}} labelPosition='left'>
<div style={{display: 'flex'}}> <div style={{display: 'flex'}}>
<Space> <Space>
<Form.Input <Form.Input
field='search' field='search_keyword'
label='关键词' label='搜索渠道关键词'
placeholder='ID名称和密钥 ...' placeholder='ID名称和密钥 ...'
value={searchKeyword} value={searchKeyword}
loading={searching} loading={searching}
@ -639,10 +639,22 @@ const ChannelsTable = () => {
setSearchKeyword(v.trim()) setSearchKeyword(v.trim())
}} }}
/> />
<Form.Input
field='search_model'
label='模型'
placeholder='模型关键字'
value={searchModel}
loading={searching}
onChange={(v)=>{
setSearchModel(v.trim())
}}
/>
<Form.Select field="group" label='分组' optionList={groupOptions} onChange={(v) => { <Form.Select field="group" label='分组' optionList={groupOptions} onChange={(v) => {
setSearchGroup(v) setSearchGroup(v)
searchChannels(searchKeyword, v) searchChannels(searchKeyword, v, searchModel)
}}/> }}/>
<Button label='查询' type="primary" htmlType="submit" className="btn-margin-right"
style={{marginRight: 8}}>查询</Button>
</Space> </Space>
</div> </div>
</Form> </Form>

View File

@ -27,6 +27,7 @@ const OperationSetting = () => {
DataExportEnabled: '', DataExportEnabled: '',
DataExportDefaultTime: 'hour', DataExportDefaultTime: 'hour',
DataExportInterval: 5, DataExportInterval: 5,
DefaultCollapseSidebar: '', // 默认折叠侧边栏
RetryTimes: 0 RetryTimes: 0
}); });
const [originInputs, setOriginInputs] = useState({}); const [originInputs, setOriginInputs] = useState({});
@ -65,6 +66,10 @@ const OperationSetting = () => {
if (key.endsWith('Enabled')) { if (key.endsWith('Enabled')) {
value = inputs[key] === 'true' ? 'false' : 'true'; value = inputs[key] === 'true' ? 'false' : 'true';
} }
if (key === 'DefaultCollapseSidebar') {
value = inputs[key] === 'true' ? 'false' : 'true';
}
console.log(key, value)
const res = await API.put('/api/option/', { const res = await API.put('/api/option/', {
key, key,
value value
@ -79,7 +84,7 @@ const OperationSetting = () => {
}; };
const handleInputChange = async (e, {name, value}) => { const handleInputChange = async (e, {name, value}) => {
if (name.endsWith('Enabled') || name === 'DataExportInterval' || name === 'DataExportDefaultTime') { if (name.endsWith('Enabled') || name === 'DataExportInterval' || name === 'DataExportDefaultTime' || name === 'DefaultCollapseSidebar') {
if (name === 'DataExportDefaultTime') { if (name === 'DataExportDefaultTime') {
localStorage.setItem('data_export_default_time', value); localStorage.setItem('data_export_default_time', value);
} }
@ -243,6 +248,12 @@ const OperationSetting = () => {
name='DrawingEnabled' name='DrawingEnabled'
onChange={handleInputChange} onChange={handleInputChange}
/> />
<Form.Checkbox
checked={inputs.DefaultCollapseSidebar === 'true'}
label='默认折叠侧边栏'
name='DefaultCollapseSidebar'
onChange={handleInputChange}
/>
</Form.Group> </Form.Group>
<Form.Button onClick={() => { <Form.Button onClick={() => {
submitConfig('general').then(); submitConfig('general').then();

View File

@ -1,8 +1,8 @@
import React, {useContext, useMemo, useState} from 'react'; import React, { useContext, useEffect, useLayoutEffect, useMemo, useState } from 'react';
import {Link, useNavigate} from 'react-router-dom'; import {Link, useNavigate} from 'react-router-dom';
import {UserContext} from '../context/User'; import {UserContext} from '../context/User';
import {API, getLogo, getSystemName, isAdmin, isMobile, showSuccess} from '../helpers'; import { API, getLogo, getSystemName, isAdmin, isMobile, showError, showSuccess } from '../helpers';
import '../index.css'; import '../index.css';
import { import {
@ -24,11 +24,14 @@ import {Nav, Avatar, Dropdown, Layout} from '@douyinfe/semi-ui';
const SiderBar = () => { const SiderBar = () => {
const [userState, userDispatch] = useContext(UserContext); const [userState, userDispatch] = useContext(UserContext);
const defaultIsCollapsed = isMobile() || localStorage.getItem('default_collapse_sidebar') === 'true';
let navigate = useNavigate(); let navigate = useNavigate();
const [selectedKeys, setSelectedKeys] = useState(['home']); const [selectedKeys, setSelectedKeys] = useState(['home']);
const [showSidebar, setShowSidebar] = useState(false);
const systemName = getSystemName(); const systemName = getSystemName();
const logo = getLogo(); const logo = getLogo();
const [isCollapsed, setIsCollapsed] = useState(defaultIsCollapsed);
const headerButtons = useMemo(() => [ const headerButtons = useMemo(() => [
{ {
text: '首页', text: '首页',
@ -110,15 +113,41 @@ const SiderBar = () => {
// } // }
], [localStorage.getItem('enable_data_export'), localStorage.getItem('enable_drawing'), localStorage.getItem('chat_link'), isAdmin()]); ], [localStorage.getItem('enable_data_export'), localStorage.getItem('enable_drawing'), localStorage.getItem('chat_link'), isAdmin()]);
const loadStatus = async () => {
const res = await API.get('/api/status');
const { success, data } = res.data;
if (success) {
localStorage.setItem('status', JSON.stringify(data));
// statusDispatch({ type: 'set', payload: data });
localStorage.setItem('system_name', data.system_name);
localStorage.setItem('logo', data.logo);
localStorage.setItem('footer_html', data.footer_html);
localStorage.setItem('quota_per_unit', data.quota_per_unit);
localStorage.setItem('display_in_currency', data.display_in_currency);
localStorage.setItem('enable_drawing', data.enable_drawing);
localStorage.setItem('enable_data_export', data.enable_data_export);
localStorage.setItem('data_export_default_time', data.data_export_default_time);
localStorage.setItem('default_collapse_sidebar', data.default_collapse_sidebar);
if (data.chat_link) {
localStorage.setItem('chat_link', data.chat_link);
} else {
localStorage.removeItem('chat_link');
}
if (data.chat_link2) {
localStorage.setItem('chat_link2', data.chat_link2);
} else {
localStorage.removeItem('chat_link2');
}
} else {
showError('无法正常连接至服务器!');
}
};
async function logout() { useEffect(() => {
setShowSidebar(false); loadStatus().then(() => {
await API.get('/api/user/logout'); setIsCollapsed(isMobile() || localStorage.getItem('default_collapse_sidebar') === 'true');
showSuccess('注销成功!'); });
userDispatch({type: 'logout'}); },[])
localStorage.removeItem('user');
navigate('/login');
}
return ( return (
<> <>
@ -127,7 +156,12 @@ const SiderBar = () => {
<Nav <Nav
// mode={'horizontal'} // mode={'horizontal'}
// bodyStyle={{ height: 100 }} // bodyStyle={{ height: 100 }}
defaultIsCollapsed={isMobile()} defaultIsCollapsed={isMobile() || localStorage.getItem('default_collapse_sidebar') === 'true'}
isCollapsed={isCollapsed}
onCollapseChange={collapsed => {
console.log(collapsed);
setIsCollapsed(collapsed);
}}
selectedKeys={selectedKeys} selectedKeys={selectedKeys}
renderWrapper={({itemElement, isSubNav, isInSubNav, props}) => { renderWrapper={({itemElement, isSubNav, isInSubNav, props}) => {
const routerMap = { const routerMap = {

View File

@ -20,8 +20,10 @@ const SystemSetting = () => {
EpayId: '', EpayId: '',
EpayKey: '', EpayKey: '',
Price: 7.3, Price: 7.3,
MinTopUp: 1,
TopupGroupRatio: '', TopupGroupRatio: '',
PayAddress: '', PayAddress: '',
CustomCallbackAddress: '',
Footer: '', Footer: '',
WeChatAuthEnabled: '', WeChatAuthEnabled: '',
WeChatServerAddress: '', WeChatServerAddress: '',
@ -292,8 +294,8 @@ const SystemSetting = () => {
<Form.Button onClick={submitServerAddress}> <Form.Button onClick={submitServerAddress}>
更新服务器地址 更新服务器地址
</Form.Button> </Form.Button>
<Divider /> <Divider/>
<Header as='h3'>支付设置当前仅支持易支付接口使用上方服务器地址作为回调地址</Header> <Header as='h3'>支付设置当前仅支持易支付接口默认使用上方服务器地址作为回调地址</Header>
<Form.Group widths='equal'> <Form.Group widths='equal'>
<Form.Input <Form.Input
label='支付地址,不填写则不启用在线支付' label='支付地址,不填写则不启用在线支付'
@ -316,14 +318,31 @@ const SystemSetting = () => {
name='EpayKey' name='EpayKey'
onChange={handleInputChange} onChange={handleInputChange}
/> />
<Form.Input
label='充值价格x元/美金)'
placeholder='例如7就是7元/美金'
value={inputs.Price}
name='Price'
min={0} </Form.Group>
onChange={handleInputChange} <Form.Group widths='equal'>
<Form.Input
label='回调地址,不填写则使用上方服务器地址作为回调地址'
placeholder='例如https://yourdomain.com'
value={inputs.CustomCallbackAddress}
name='CustomCallbackAddress'
onChange={handleInputChange}
/>
<Form.Input
label='充值价格x元/美金)'
placeholder='例如7就是7元/美金'
value={inputs.Price}
name='Price'
min={0}
onChange={handleInputChange}
/>
<Form.Input
label='最低充值数量'
placeholder='例如2就是最低充值2$'
value={inputs.MinTopUp}
name='MinTopUp'
min={1}
onChange={handleInputChange}
/> />
</Form.Group> </Form.Group>
<Form.Group widths='equal'> <Form.Group widths='equal'>

View File

@ -153,7 +153,7 @@ const TokensTable = () => {
[ [
{node: 'item', key: 'next', disabled: !localStorage.getItem('chat_link'), name: 'ChatGPT Next Web', onClick: () => {onOpenLink('next', record.key)}}, {node: 'item', key: 'next', disabled: !localStorage.getItem('chat_link'), name: 'ChatGPT Next Web', onClick: () => {onOpenLink('next', record.key)}},
{node: 'item', key: 'next-mj', disabled: !localStorage.getItem('chat_link2'), name: 'ChatGPT Web & Midjourney', onClick: () => {onOpenLink('next-mj', record.key)}}, {node: 'item', key: 'next-mj', disabled: !localStorage.getItem('chat_link2'), name: 'ChatGPT Web & Midjourney', onClick: () => {onOpenLink('next-mj', record.key)}},
{node: 'item', key: 'ama', name: 'AMA 问天BotGrem', onClick: () => {onOpenLink('ama', record.key)}}, {node: 'item', key: 'ama', name: 'AMA 问天BotGem', onClick: () => {onOpenLink('ama', record.key)}},
{node: 'item', key: 'opencat', name: 'OpenCat', onClick: () => {onOpenLink('opencat', record.key)}}, {node: 'item', key: 'opencat', name: 'OpenCat', onClick: () => {onOpenLink('opencat', record.key)}},
] ]
} }

View File

@ -132,6 +132,8 @@ export const modelColorMap = {
'gpt-4-0314': 'rgb(70,130,180)', // 钢蓝色 'gpt-4-0314': 'rgb(70,130,180)', // 钢蓝色
'gpt-4-0613': 'rgb(100,149,237)', // 矢车菊蓝 'gpt-4-0613': 'rgb(100,149,237)', // 矢车菊蓝
'gpt-4-1106-preview': 'rgb(30,144,255)', // 道奇蓝 'gpt-4-1106-preview': 'rgb(30,144,255)', // 道奇蓝
'gpt-4-0125-preview': 'rgb(2,177,236)', // 深天蓝
'gpt-4-turbo-preview': 'rgb(2,177,255)', // 深天蓝
'gpt-4-32k': 'rgb(104,111,238)', // 中紫色 'gpt-4-32k': 'rgb(104,111,238)', // 中紫色
'gpt-4-32k-0314': 'rgb(90,105,205)', // 暗灰蓝色 'gpt-4-32k-0314': 'rgb(90,105,205)', // 暗灰蓝色
'gpt-4-32k-0613': 'rgb(61,71,139)', // 暗蓝灰色 'gpt-4-32k-0613': 'rgb(61,71,139)', // 暗蓝灰色

View File

@ -10,6 +10,7 @@ const TopUp = () => {
const [topUpCount, setTopUpCount] = useState(10); const [topUpCount, setTopUpCount] = useState(10);
const [minTopupCount, setMinTopUpCount] = useState(1); const [minTopupCount, setMinTopUpCount] = useState(1);
const [amount, setAmount] = useState(0.0); const [amount, setAmount] = useState(0.0);
const [minTopUp, setMinTopUp] = useState(1);
const [topUpLink, setTopUpLink] = useState(''); const [topUpLink, setTopUpLink] = useState('');
const [enableOnlineTopUp, setEnableOnlineTopUp] = useState(false); const [enableOnlineTopUp, setEnableOnlineTopUp] = useState(false);
const [userQuota, setUserQuota] = useState(0); const [userQuota, setUserQuota] = useState(0);
@ -61,6 +62,10 @@ const TopUp = () => {
if (amount === 0) { if (amount === 0) {
await getAmount(); await getAmount();
} }
if (topUpCount < minTopUp) {
showInfo('充值数量不能小于' + minTopUp);
return;
}
setPayWay(payment) setPayWay(payment)
setOpen(true); setOpen(true);
} }
@ -69,6 +74,10 @@ const TopUp = () => {
if (amount === 0) { if (amount === 0) {
await getAmount(); await getAmount();
} }
if (topUpCount < minTopUp) {
showInfo('充值数量不能小于' + minTopUp);
return;
}
setOpen(false); setOpen(false);
try { try {
const res = await API.post('/api/user/pay', { const res = await API.post('/api/user/pay', {
@ -132,6 +141,9 @@ const TopUp = () => {
if (status.top_up_link) { if (status.top_up_link) {
setTopUpLink(status.top_up_link); setTopUpLink(status.top_up_link);
} }
if (status.min_topup) {
setMinTopUp(status.min_topup);
}
if (status.enable_online_topup) { if (status.enable_online_topup) {
setEnableOnlineTopUp(status.enable_online_topup); setEnableOnlineTopUp(status.enable_online_topup);
} }
@ -239,12 +251,13 @@ const TopUp = () => {
disabled={!enableOnlineTopUp} disabled={!enableOnlineTopUp}
field={'redemptionCount'} field={'redemptionCount'}
label={'实付金额:' + renderAmount()} label={'实付金额:' + renderAmount()}
placeholder='充值数量' placeholder={'充值数量,最低' + minTopUp + '$'}
name='redemptionCount' name='redemptionCount'
type={'number'} type={'number'}
value={topUpCount} value={topUpCount}
suffix={'$'} suffix={'$'}
min={1} min={minTopUp}
defaultValue={minTopUp}
max={100000} max={100000}
onChange={async (value) => { onChange={async (value) => {
if (value < 1) { if (value < 1) {