This commit is contained in:
CaIon 2023-08-14 22:16:32 +08:00
parent c134604cee
commit 8f2119e410
33 changed files with 3224 additions and 1138 deletions

254
Midjourney.md Normal file
View File

@ -0,0 +1,254 @@
# Midjourney Proxy API文档
**简介**:Midjourney Proxy API文档
**HOST**:https://api.nekoedu.com
**Version**:v2.3.5
[TOC]
# 任务提交
## 绘图变化
**接口地址**:`/mj/submit/change`
**请求方式**:`POST`
**请求数据类型**:`application/json`
**响应数据类型**:`*/*`
**接口描述**:
**请求示例**:
```javascript
{
"action": "UPSCALE",
"index": 1,
"notifyHook": "",
"state": "",
"taskId": "1320098173412546"
}
```
**请求参数**:
| 参数名称 | 参数说明 | 请求类型 | 是否必须 | 数据类型 | schema |
| -------- | -------- | ----- | -------- | -------- | ------ |
|changeDTO|changeDTO|body|true|变化任务提交参数|变化任务提交参数|
|  action|UPSCALE(放大); VARIATION(变换); REROLL(重新生成),可用值:UPSCALE,VARIATION,REROLL||true|string||
|  index|序号(1~4), action为UPSCALE,VARIATION时必传||false|integer(int32)||
|  notifyHook|回调地址, 为空时使用全局notifyHook||false|string||
|  state|自定义参数||false|string||
|  taskId|任务ID||true|string||
**响应状态**:
| 状态码 | 说明 | schema |
| -------- | -------- | ----- |
|200|OK|提交结果|
|201|Created||
|401|Unauthorized||
|403|Forbidden||
|404|Not Found||
**响应参数**:
| 参数名称 | 参数说明 | 类型 | schema |
| -------- | -------- | ----- |----- |
|code|状态码: 1(提交成功), 21(已存在), 22(排队中), other(错误)|integer(int32)|integer(int32)|
|description|描述|string||
|properties|扩展字段|object||
|result|任务ID|string||
**响应示例**:
```javascript
{
"code": 1,
"description": "提交成功",
"properties": {},
"result": 1320098173412546
}
```
## 提交Imagine任务
**接口地址**:`/mj/submit/imagine`
**请求方式**:`POST`
**请求数据类型**:`application/json`
**响应数据类型**:`*/*`
**接口描述**:
**请求示例**:
```javascript
{
"base64": "",
"notifyHook": "",
"prompt": "Cat",
"state": ""
}
```
**请求参数**:
| 参数名称 | 参数说明 | 请求类型 | 是否必须 | 数据类型 | schema |
| -------- | -------- | ----- | -------- | -------- | ------ |
|imagineDTO|imagineDTO|body|true|Imagine提交参数|Imagine提交参数|
|  base64|垫图base64||false|string||
|  notifyHook|回调地址, 为空时使用全局notifyHook||false|string||
|  prompt|提示词||true|string||
|  state|自定义参数||false|string||
**响应状态**:
| 状态码 | 说明 | schema |
| -------- | -------- | ----- |
|200|OK|提交结果|
|201|Created||
|401|Unauthorized||
|403|Forbidden||
|404|Not Found||
**响应参数**:
| 参数名称 | 参数说明 | 类型 | schema |
| -------- | -------- | ----- |----- |
|code|状态码: 1(提交成功), 21(已存在), 22(排队中), other(错误)|integer(int32)|integer(int32)|
|description|描述|string||
|properties|扩展字段|object||
|result|任务ID|string||
**响应示例**:
```javascript
{
"code": 1,
"description": "提交成功",
"properties": {},
"result": 1320098173412546
}
```
# 任务查询
## 指定ID获取任务
**接口地址**:`/mj/task/{id}/fetch`
**请求方式**:`GET`
**请求数据类型**:`application/x-www-form-urlencoded`
**响应数据类型**:`*/*`
**接口描述**:
**请求参数**:
| 参数名称 | 参数说明 | 请求类型 | 是否必须 | 数据类型 | schema |
| -------- | -------- | ----- | -------- | -------- | ------ |
|id|任务ID|path|false|string||
**响应状态**:
| 状态码 | 说明 | schema |
| -------- | -------- | ----- |
|200|OK|任务|
|401|Unauthorized||
|403|Forbidden||
|404|Not Found||
**响应参数**:
| 参数名称 | 参数说明 | 类型 | schema |
| -------- | -------- | ----- |----- |
|action|可用值:IMAGINE,UPSCALE,VARIATION,REROLL,DESCRIBE,BLEND|string||
|description|任务描述|string||
|failReason|失败原因|string||
|finishTime|结束时间|integer(int64)|integer(int64)|
|id|任务ID|string||
|imageUrl|图片url|string||
|progress|任务进度|string||
|prompt|提示词|string||
|promptEn|提示词-英文|string||
|startTime|开始执行时间|integer(int64)|integer(int64)|
|state|自定义参数|string||
|status|任务状态,可用值:NOT_START,SUBMITTED,IN_PROGRESS,FAILURE,SUCCESS|string||
|submitTime|提交时间|integer(int64)|integer(int64)|
**响应示例**:
```javascript
{
"action": "",
"description": "",
"failReason": "",
"finishTime": 0,
"id": "",
"imageUrl": "",
"progress": "",
"prompt": "",
"promptEn": "",
"startTime": 0,
"state": "",
"status": "",
"submitTime": 0
}
```

View File

@ -79,6 +79,10 @@ var RequestInterval = time.Duration(requestInterval) * time.Second
var SyncFrequency = 10 * 60 // unit is second, will be overwritten by SYNC_FREQUENCY var SyncFrequency = 10 * 60 // unit is second, will be overwritten by SYNC_FREQUENCY
var NormalPrice = 1.5
var StablePrice = 6.0
var BasePrice = 1.5
const ( const (
RoleGuestUser = 0 RoleGuestUser = 0
RoleCommonUser = 1 RoleCommonUser = 1

View File

@ -94,6 +94,23 @@ func SearchUserLogs(c *gin.Context) {
}) })
} }
func GetLogByKey(c *gin.Context) {
key := c.Query("key")
logs, err := model.GetLogByKey(key)
if err != nil {
c.JSON(200, gin.H{
"success": false,
"message": err.Error(),
})
return
}
c.JSON(200, gin.H{
"success": true,
"message": "",
"data": logs,
})
}
func GetLogsStat(c *gin.Context) { func GetLogsStat(c *gin.Context) {
logType, _ := strconv.Atoi(c.Query("type")) logType, _ := strconv.Atoi(c.Query("type"))
startTimestamp, _ := strconv.ParseInt(c.Query("start_timestamp"), 10, 64) startTimestamp, _ := strconv.ParseInt(c.Query("start_timestamp"), 10, 64)

133
controller/midjourney.go Normal file
View File

@ -0,0 +1,133 @@
package controller
import (
"bytes"
"encoding/json"
"fmt"
"github.com/gin-gonic/gin"
"log"
"net/http"
"one-api/common"
"one-api/model"
"strconv"
"time"
)
func UpdateMidjourneyTask() {
imageModel := "midjourney"
for {
time.Sleep(time.Duration(15) * time.Second)
tasks := model.GetAllUnFinishTasks()
if len(tasks) != 0 {
//log.Printf("UpdateMidjourneyTask: %v", time.Now())
ids := make([]string, 0)
for _, task := range tasks {
ids = append(ids, task.MjId)
}
requestUrl := "http://107.173.171.147:8080/mj/task/list-by-condition"
requestBody := map[string]interface{}{
"ids": ids,
}
jsonStr, err := json.Marshal(requestBody)
if err != nil {
log.Printf("UpdateMidjourneyTask: %v", err)
}
req, err := http.NewRequest("POST", requestUrl, bytes.NewBuffer(jsonStr))
if err != nil {
log.Printf("UpdateMidjourneyTask: %v", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("mj-api-secret", "uhiftyuwadbkjshbiklahcuitguasguzhxliawodawdu")
resp, err := httpClient.Do(req)
if err != nil {
log.Printf("UpdateMidjourneyTask: %v", err)
}
defer resp.Body.Close()
var response []Midjourney
err = json.NewDecoder(resp.Body).Decode(&response)
if err != nil {
log.Printf("UpdateMidjourneyTask: %v", err)
}
for _, responseItem := range response {
var midjourneyTask *model.Midjourney
for _, mj := range tasks {
mj.MjId = responseItem.MjId
midjourneyTask = model.GetMjByuId(mj.Id)
}
if midjourneyTask != nil {
midjourneyTask.Code = 1
midjourneyTask.Progress = responseItem.Progress
midjourneyTask.PromptEn = responseItem.PromptEn
midjourneyTask.State = responseItem.State
midjourneyTask.SubmitTime = responseItem.SubmitTime
midjourneyTask.StartTime = responseItem.StartTime
midjourneyTask.FinishTime = responseItem.FinishTime
midjourneyTask.ImageUrl = responseItem.ImageUrl
midjourneyTask.Status = responseItem.Status
midjourneyTask.FailReason = responseItem.FailReason
if midjourneyTask.Progress != "100%" && responseItem.FailReason != "" {
log.Println(midjourneyTask.MjId + " 构建失败," + midjourneyTask.FailReason)
midjourneyTask.Progress = "100%"
err = model.CacheUpdateUserQuota(midjourneyTask.UserId)
if err != nil {
log.Println("error update user quota cache: " + err.Error())
} else {
modelRatio := common.GetModelRatio(imageModel)
groupRatio := common.GetGroupRatio("default")
ratio := modelRatio * groupRatio
quota := int(ratio * 1 * 1000)
if quota != 0 {
err := model.IncreaseUserQuota(midjourneyTask.UserId, quota)
if err != nil {
log.Println("fail to increase user quota")
}
logContent := fmt.Sprintf("%s 构图失败,补偿 %s", midjourneyTask.MjId, common.LogQuota(quota))
model.RecordLog(midjourneyTask.UserId, 1, logContent)
}
}
}
err = midjourneyTask.Update()
if err != nil {
log.Printf("UpdateMidjourneyTaskFail: %v", err)
}
log.Printf("UpdateMidjourneyTask: %v", midjourneyTask)
}
}
}
}
}
func GetAllMidjourney(c *gin.Context) {
p, _ := strconv.Atoi(c.Query("p"))
if p < 0 {
p = 0
}
logs := model.GetAllTasks(p*common.ItemsPerPage, common.ItemsPerPage)
if logs == nil {
logs = make([]*model.Midjourney, 0)
}
c.JSON(200, gin.H{
"success": true,
"message": "",
"data": logs,
})
}
func GetUserMidjourney(c *gin.Context) {
p, _ := strconv.Atoi(c.Query("p"))
if p < 0 {
p = 0
}
userId := c.GetInt("id")
log.Printf("userId = %d \n", userId)
logs := model.GetAllUserTask(userId, p*common.ItemsPerPage, common.ItemsPerPage)
if logs == nil {
logs = make([]*model.Midjourney, 0)
}
c.JSON(200, gin.H{
"success": true,
"message": "",
"data": logs,
})
}

View File

@ -31,6 +31,9 @@ func GetStatus(c *gin.Context) {
"chat_link": common.ChatLink, "chat_link": common.ChatLink,
"quota_per_unit": common.QuotaPerUnit, "quota_per_unit": common.QuotaPerUnit,
"display_in_currency": common.DisplayInCurrencyEnabled, "display_in_currency": common.DisplayInCurrencyEnabled,
"normal_price": common.NormalPrice,
"stable_price": common.StablePrice,
"base_price": common.BasePrice,
}, },
}) })
return return
@ -58,6 +61,17 @@ func GetAbout(c *gin.Context) {
return return
} }
func GetMidjourney(c *gin.Context) {
common.OptionMapRWMutex.RLock()
defer common.OptionMapRWMutex.RUnlock()
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
"data": common.OptionMap["Midjourney"],
})
return
}
func GetHomePageContent(c *gin.Context) { func GetHomePageContent(c *gin.Context) {
common.OptionMapRWMutex.RLock() common.OptionMapRWMutex.RLock()
defer common.OptionMapRWMutex.RUnlock() defer common.OptionMapRWMutex.RUnlock()

View File

@ -137,7 +137,7 @@ func relayImageHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode
if quota != 0 { if quota != 0 {
tokenName := c.GetString("token_name") tokenName := c.GetString("token_name")
logContent := fmt.Sprintf("模型倍率 %.2f,分组倍率 %.2f", modelRatio, groupRatio) logContent := fmt.Sprintf("模型倍率 %.2f,分组倍率 %.2f", modelRatio, groupRatio)
model.RecordConsumeLog(userId, 0, 0, imageModel, tokenName, quota, logContent) model.RecordConsumeLog(userId, 0, 0, imageModel, tokenName, quota, logContent, tokenId)
model.UpdateUserUsedQuotaAndRequestCount(userId, quota) model.UpdateUserUsedQuotaAndRequestCount(userId, quota)
channelId := c.GetInt("channel_id") channelId := c.GetInt("channel_id")
model.UpdateChannelUsedQuota(channelId, quota) model.UpdateChannelUsedQuota(channelId, quota)

388
controller/relay-mj.go Normal file
View File

@ -0,0 +1,388 @@
package controller
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"one-api/common"
"one-api/model"
"strings"
"github.com/gin-gonic/gin"
)
type Midjourney struct {
MjId string `json:"id"`
Action string `json:"action"`
Prompt string `json:"prompt"`
PromptEn string `json:"promptEn"`
Description string `json:"description"`
State string `json:"state"`
SubmitTime int64 `json:"submitTime"`
StartTime int64 `json:"startTime"`
FinishTime int64 `json:"finishTime"`
ImageUrl string `json:"imageUrl"`
Status string `json:"status"`
Progress string `json:"progress"`
FailReason string `json:"failReason"`
}
func relayMidjourneyNotify(c *gin.Context) *MidjourneyResponse {
var midjRequest Midjourney
err := common.UnmarshalBodyReusable(c, &midjRequest)
if err != nil {
return &MidjourneyResponse{
Code: 4,
Description: "bind_request_body_failed",
Properties: nil,
Result: "",
}
}
midjourneyTask := model.GetByMJId(midjRequest.MjId)
if midjourneyTask == nil {
return &MidjourneyResponse{
Code: 4,
Description: "midjourney_task_not_found",
Properties: nil,
Result: "",
}
}
midjourneyTask.Progress = midjRequest.Progress
midjourneyTask.PromptEn = midjRequest.PromptEn
midjourneyTask.State = midjRequest.State
midjourneyTask.SubmitTime = midjRequest.SubmitTime
midjourneyTask.StartTime = midjRequest.StartTime
midjourneyTask.FinishTime = midjRequest.FinishTime
midjourneyTask.ImageUrl = midjRequest.ImageUrl
midjourneyTask.Status = midjRequest.Status
midjourneyTask.FailReason = midjRequest.FailReason
err = midjourneyTask.Update()
if err != nil {
return &MidjourneyResponse{
Code: 4,
Description: "update_midjourney_task_failed",
}
}
return nil
}
func relayMidjourneyTask(c *gin.Context, relayMode int) *MidjourneyResponse {
taskId := c.Param("id")
originTask := model.GetByMJId(taskId)
if originTask == nil {
return &MidjourneyResponse{
Code: 4,
Description: "task_no_found",
}
}
var midjourneyTask Midjourney
midjourneyTask.MjId = originTask.MjId
midjourneyTask.Progress = originTask.Progress
midjourneyTask.PromptEn = originTask.PromptEn
midjourneyTask.State = originTask.State
midjourneyTask.SubmitTime = originTask.SubmitTime
midjourneyTask.StartTime = originTask.StartTime
midjourneyTask.FinishTime = originTask.FinishTime
midjourneyTask.ImageUrl = originTask.ImageUrl
midjourneyTask.Status = originTask.Status
midjourneyTask.FailReason = originTask.FailReason
midjourneyTask.Action = originTask.Action
midjourneyTask.Description = originTask.Description
midjourneyTask.Prompt = originTask.Prompt
jsonMap, err := json.Marshal(midjourneyTask)
if err != nil {
return &MidjourneyResponse{
Code: 4,
Description: "unmarshal_response_body_failed",
}
}
_, err = io.Copy(c.Writer, bytes.NewBuffer(jsonMap))
if err != nil {
return &MidjourneyResponse{
Code: 4,
Description: "copy_response_body_failed",
}
}
return nil
}
func relayMidjourneySubmit(c *gin.Context, relayMode int) *MidjourneyResponse {
imageModel := "midjourney"
tokenId := c.GetInt("token_id")
channelType := c.GetInt("channel")
userId := c.GetInt("id")
consumeQuota := c.GetBool("consume_quota")
group := c.GetString("group")
var midjRequest MidjourneyRequest
if consumeQuota {
err := common.UnmarshalBodyReusable(c, &midjRequest)
if err != nil {
return &MidjourneyResponse{
Code: 4,
Description: "bind_request_body_failed",
}
}
}
if relayMode == RelayModeMidjourneyImagine {
if midjRequest.Prompt == "" {
return &MidjourneyResponse{
Code: 4,
Description: "prompt_is_required",
}
}
midjRequest.Action = "IMAGINE"
} else if midjRequest.TaskId != "" {
originTask := model.GetByMJId(midjRequest.TaskId)
if originTask == nil {
return &MidjourneyResponse{
Code: 4,
Description: "task_no_found",
}
} else if originTask.Action == "UPSCALE" {
//return errorWrapper(errors.New("upscale task can not be change"), "request_params_error", http.StatusBadRequest).
return &MidjourneyResponse{
Code: 4,
Description: "upscale_task_can_not_be_change",
}
} else if originTask.Status != "SUCCESS" {
return &MidjourneyResponse{
Code: 4,
Description: "task_status_is_not_success",
}
}
midjRequest.Prompt = originTask.Prompt
} else if relayMode == RelayModeMidjourneyChange {
if midjRequest.TaskId == "" {
return &MidjourneyResponse{
Code: 4,
Description: "taskId_is_required",
}
} else if midjRequest.Action == "" {
return &MidjourneyResponse{
Code: 4,
Description: "action_is_required",
}
} else if midjRequest.Index == 0 {
return &MidjourneyResponse{
Code: 4,
Description: "index_can_only_be_1_2_3_4",
}
}
}
// map model name
modelMapping := c.GetString("model_mapping")
isModelMapped := false
if modelMapping != "" {
modelMap := make(map[string]string)
err := json.Unmarshal([]byte(modelMapping), &modelMap)
if err != nil {
//return errorWrapper(err, "unmarshal_model_mapping_failed", http.StatusInternalServerError)
return &MidjourneyResponse{
Code: 4,
Description: "unmarshal_model_mapping_failed",
}
}
if modelMap[imageModel] != "" {
imageModel = modelMap[imageModel]
isModelMapped = true
}
}
baseURL := common.ChannelBaseURLs[channelType]
requestURL := c.Request.URL.String()
if c.GetString("base_url") != "" {
baseURL = c.GetString("base_url")
}
//midjRequest.NotifyHook = "http://127.0.0.1:3000/mj/notify"
fullRequestURL := fmt.Sprintf("%s%s", baseURL, requestURL)
var requestBody io.Reader
if isModelMapped {
jsonStr, err := json.Marshal(midjRequest)
if err != nil {
return &MidjourneyResponse{
Code: 4,
Description: "marshal_text_request_failed",
}
}
requestBody = bytes.NewBuffer(jsonStr)
} else {
requestBody = c.Request.Body
}
modelRatio := common.GetModelRatio(imageModel)
groupRatio := common.GetGroupRatio(group)
ratio := modelRatio * groupRatio
userQuota, err := model.CacheGetUserQuota(userId)
sizeRatio := 1.0
quota := int(ratio * sizeRatio * 1000)
if consumeQuota && userQuota-quota < 0 {
return &MidjourneyResponse{
Code: 4,
Description: "quota_not_enough",
}
}
req, err := http.NewRequest(c.Request.Method, fullRequestURL, requestBody)
if err != nil {
return &MidjourneyResponse{
Code: 4,
Description: "create_request_failed",
}
}
//req.Header.Set("Authorization", c.Request.Header.Get("Authorization"))
req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type"))
req.Header.Set("Accept", c.Request.Header.Get("Accept"))
req.Header.Set("mj-api-secret", strings.Split(c.Request.Header.Get("Authorization"), " ")[1])
// print request header
log.Printf("request header: %s", req.Header)
log.Printf("request body: %s", midjRequest.Prompt)
resp, err := httpClient.Do(req)
if err != nil {
return &MidjourneyResponse{
Code: 4,
Description: "do_request_failed",
}
}
err = req.Body.Close()
if err != nil {
return &MidjourneyResponse{
Code: 4,
Description: "close_request_body_failed",
}
}
err = c.Request.Body.Close()
if err != nil {
return &MidjourneyResponse{
Code: 4,
Description: "close_request_body_failed",
}
}
var midjResponse MidjourneyResponse
defer func() {
if consumeQuota {
err := model.PostConsumeTokenQuota(tokenId, quota)
if err != nil {
common.SysError("error consuming token remain quota: " + err.Error())
}
err = model.CacheUpdateUserQuota(userId)
if err != nil {
common.SysError("error update user quota cache: " + err.Error())
}
if quota != 0 {
tokenName := c.GetString("token_name")
logContent := fmt.Sprintf("模型倍率 %.2f,分组倍率 %.2f", modelRatio, groupRatio)
model.RecordConsumeLog(userId, 0, 0, imageModel, tokenName, quota, logContent, tokenId)
model.UpdateUserUsedQuotaAndRequestCount(userId, quota)
channelId := c.GetInt("channel_id")
model.UpdateChannelUsedQuota(channelId, quota)
}
}
}()
//if consumeQuota {
//
//}
responseBody, err := io.ReadAll(resp.Body)
if err != nil {
return &MidjourneyResponse{
Code: 4,
Description: "read_response_body_failed",
}
}
err = resp.Body.Close()
if err != nil {
return &MidjourneyResponse{
Code: 4,
Description: "close_response_body_failed",
}
}
err = json.Unmarshal(responseBody, &midjResponse)
log.Printf("responseBody: %s", string(responseBody))
log.Printf("midjResponse: %v", midjResponse)
if resp.StatusCode != 200 {
return &MidjourneyResponse{
Code: 4,
Description: "fail_to_fetch_midjourney",
}
}
if err != nil {
return &MidjourneyResponse{
Code: 4,
Description: "unmarshal_response_body_failed",
}
}
if midjResponse.Code == 24 || midjResponse.Code == 21 || midjResponse.Code == 4 {
consumeQuota = false
}
midjourneyTask := &model.Midjourney{
UserId: userId,
Code: midjResponse.Code,
Action: midjRequest.Action,
MjId: midjResponse.Result,
Prompt: midjRequest.Prompt,
PromptEn: "",
Description: midjResponse.Description,
State: "",
SubmitTime: 0,
StartTime: 0,
FinishTime: 0,
ImageUrl: "",
Status: "",
Progress: "0%",
FailReason: "",
}
if midjResponse.Code == 4 {
midjourneyTask.FailReason = midjResponse.Description
}
err = midjourneyTask.Insert()
if err != nil {
return &MidjourneyResponse{
Code: 4,
Description: "insert_midjourney_task_failed",
}
}
resp.Body = io.NopCloser(bytes.NewBuffer(responseBody))
for k, v := range resp.Header {
c.Writer.Header().Set(k, v[0])
}
c.Writer.WriteHeader(resp.StatusCode)
_, err = io.Copy(c.Writer, resp.Body)
if err != nil {
return &MidjourneyResponse{
Code: 4,
Description: "copy_response_body_failed",
}
}
err = resp.Body.Close()
if err != nil {
return &MidjourneyResponse{
Code: 4,
Description: "close_response_body_failed",
}
}
return nil
}

View File

@ -95,6 +95,33 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
case common.ChannelTypeZhipu: case common.ChannelTypeZhipu:
apiType = APITypeZhipu apiType = APITypeZhipu
} }
isStable := c.GetBool("stable")
//if common.NormalPrice == -1 && strings.HasPrefix(textRequest.Model, "gpt-4") {
// nowUser, err := model.GetUserById(userId, false)
// if err != nil {
// return errorWrapper(err, "get_user_info_failed", http.StatusInternalServerError)
// }
// if nowUser.StableMode {
// group = "svip"
// isStable = true
// ////stableRatio = (common.StablePrice / common.BasePrice) * modelRatio
// //userMaxPrice, _ := strconv.ParseFloat(nowUser.MaxPrice, 64)
// //if userMaxPrice < common.StablePrice {
// // return errorWrapper(errors.New("当前低价通道不可用,稳定渠道价格为"+strconv.FormatFloat(common.StablePrice, 'f', -1, 64)+"R/刀"), "当前低价通道不可用", http.StatusInternalServerError)
// //}
// //
// ////ratio = stableRatio * groupRatio
// //channel, err := model.CacheGetRandomSatisfiedChannel("svip", textRequest.Model)
// //if err != nil {
// // message := fmt.Sprintf("当前分组 %s 下对于模型 %s 无可用渠道", "svip", textRequest.Model)
// // return errorWrapper(errors.New(message), "no_available_channel", http.StatusInternalServerError)
// //}
// //channelType = channel.Type
// } else {
// return errorWrapper(errors.New("当前低价通道不可用,请稍后再试,或者在后台开启稳定模式"), "当前低价通道不可用", http.StatusInternalServerError)
// }
//}
baseURL := common.ChannelBaseURLs[channelType] baseURL := common.ChannelBaseURLs[channelType]
requestURL := c.Request.URL.String() requestURL := c.Request.URL.String()
if c.GetString("base_url") != "" { if c.GetString("base_url") != "" {
@ -168,11 +195,17 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
if textRequest.MaxTokens != 0 { if textRequest.MaxTokens != 0 {
preConsumedTokens = promptTokens + textRequest.MaxTokens preConsumedTokens = promptTokens + textRequest.MaxTokens
} }
//stableRatio := common.GetStableRatio(textRequest.Model)
modelRatio := common.GetModelRatio(textRequest.Model) modelRatio := common.GetModelRatio(textRequest.Model)
stableRatio := modelRatio
groupRatio := common.GetGroupRatio(group) groupRatio := common.GetGroupRatio(group)
ratio := modelRatio * groupRatio ratio := modelRatio * groupRatio
preConsumedQuota := int(float64(preConsumedTokens) * ratio) preConsumedQuota := int(float64(preConsumedTokens) * ratio)
userQuota, err := model.CacheGetUserQuota(userId) userQuota, err := model.CacheGetUserQuota(userId)
if isStable {
stableRatio = (common.StablePrice / common.BasePrice) * modelRatio
ratio = stableRatio * groupRatio
}
if err != nil { if err != nil {
return errorWrapper(err, "get_user_quota_failed", http.StatusInternalServerError) return errorWrapper(err, "get_user_quota_failed", http.StatusInternalServerError)
} }
@ -301,6 +334,11 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
// we cannot just return, because we may have to return the pre-consumed quota // we cannot just return, because we may have to return the pre-consumed quota
quota = 0 quota = 0
} }
//if strings.HasPrefix(textRequest.Model, "gpt-4") {
// if quota < 5000 && quota != 0 {
// quota = 5000
// }
//}
quotaDelta := quota - preConsumedQuota quotaDelta := quota - preConsumedQuota
err := model.PostConsumeTokenQuota(tokenId, quotaDelta) err := model.PostConsumeTokenQuota(tokenId, quotaDelta)
if err != nil { if err != nil {
@ -312,8 +350,13 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
} }
if quota != 0 { if quota != 0 {
tokenName := c.GetString("token_name") tokenName := c.GetString("token_name")
logContent := fmt.Sprintf("模型倍率 %.2f,分组倍率 %.2f", modelRatio, groupRatio) var logContent string
model.RecordConsumeLog(userId, promptTokens, completionTokens, textRequest.Model, tokenName, quota, logContent) if isStable {
logContent = fmt.Sprintf("(稳定模式)模型倍率 %.2f,分组倍率 %.2f", stableRatio, groupRatio)
} else {
logContent = fmt.Sprintf("模型倍率 %.2f,分组倍率 %.2f", modelRatio, groupRatio)
}
model.RecordConsumeLog(userId, promptTokens, completionTokens, textRequest.Model, tokenName, quota, logContent, tokenId)
model.UpdateUserUsedQuotaAndRequestCount(userId, quota) model.UpdateUserUsedQuotaAndRequestCount(userId, quota)
channelId := c.GetInt("channel_id") channelId := c.GetInt("channel_id")
model.UpdateChannelUsedQuota(channelId, quota) model.UpdateChannelUsedQuota(channelId, quota)

View File

@ -2,6 +2,7 @@ package controller
import ( import (
"fmt" "fmt"
"log"
"net/http" "net/http"
"one-api/common" "one-api/common"
"strconv" "strconv"
@ -24,6 +25,10 @@ const (
RelayModeModerations RelayModeModerations
RelayModeImagesGenerations RelayModeImagesGenerations
RelayModeEdits RelayModeEdits
RelayModeMidjourneyImagine
RelayModeMidjourneyChange
RelayModeMidjourneyNotify
RelayModeMidjourneyTaskFetch
) )
// https://platform.openai.com/docs/api-reference/chat // https://platform.openai.com/docs/api-reference/chat
@ -128,6 +133,23 @@ type CompletionsStreamResponse struct {
} `json:"choices"` } `json:"choices"`
} }
type MidjourneyRequest struct {
Prompt string `json:"prompt"`
NotifyHook string `json:"notifyHook"`
Action string `json:"action"`
Index int `json:"index"`
State string `json:"state"`
TaskId string `json:"taskId"`
Base64Array []string `json:"base64Array"`
}
type MidjourneyResponse struct {
Code int `json:"code"`
Description string `json:"description"`
Properties interface{} `json:"properties"`
Result string `json:"result"`
}
func Relay(c *gin.Context) { func Relay(c *gin.Context) {
relayMode := RelayModeUnknown relayMode := RelayModeUnknown
if strings.HasPrefix(c.Request.URL.Path, "/v1/chat/completions") { if strings.HasPrefix(c.Request.URL.Path, "/v1/chat/completions") {
@ -179,6 +201,54 @@ func Relay(c *gin.Context) {
} }
} }
func RelayMidjourney(c *gin.Context) {
relayMode := RelayModeUnknown
if strings.HasPrefix(c.Request.URL.Path, "/mj/submit/imagine") {
relayMode = RelayModeMidjourneyImagine
} else if strings.HasPrefix(c.Request.URL.Path, "/mj/notify") {
relayMode = RelayModeMidjourneyNotify
} else if strings.HasPrefix(c.Request.URL.Path, "/mj/submit/change") {
relayMode = RelayModeMidjourneyChange
} else if strings.HasPrefix(c.Request.URL.Path, "/mj/task") {
relayMode = RelayModeMidjourneyTaskFetch
}
var err *MidjourneyResponse
switch relayMode {
case RelayModeMidjourneyNotify:
err = relayMidjourneyNotify(c)
case RelayModeMidjourneyTaskFetch:
err = relayMidjourneyTask(c, relayMode)
default:
err = relayMidjourneySubmit(c, relayMode)
}
//err = relayMidjourneySubmit(c, relayMode)
log.Println(err)
if err != nil {
retryTimesStr := c.Query("retry")
retryTimes, _ := strconv.Atoi(retryTimesStr)
if retryTimesStr == "" {
retryTimes = common.RetryTimes
}
if retryTimes > 0 {
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s?retry=%d", c.Request.URL.Path, retryTimes-1))
} else {
if err.Code == 30 {
err.Result = "当前分组负载已饱和,请稍后再试,或升级账户以提升服务质量。"
}
c.JSON(400, gin.H{
"error": err.Result,
})
}
channelId := c.GetInt("channel_id")
common.SysError(fmt.Sprintf("relay error (channel #%d): %s", channelId, err.Result))
//if shouldDisableChannel(&err.OpenAIError) {
// channelId := c.GetInt("channel_id")
// channelName := c.GetString("channel_name")
// disableChannel(channelId, channelName, err.Result)
//}
}
}
func RelayNotImplemented(c *gin.Context) { func RelayNotImplemented(c *gin.Context) {
err := OpenAIError{ err := OpenAIError{
Message: "API not implemented", Message: "API not implemented",

173
controller/topup.go Normal file
View File

@ -0,0 +1,173 @@
package controller
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/samber/lo"
epay "github.com/star-horizon/go-epay"
"log"
"net/url"
"one-api/common"
"one-api/model"
"strconv"
"time"
)
type EpayRequest struct {
Amount int `json:"amount"`
PaymentMethod string `json:"payment_method"`
TopUpCode string `json:"top_up_code"`
}
type AmountRequest struct {
Amount int `json:"amount"`
TopUpCode string `json:"top_up_code"`
}
var client, _ = epay.NewClientWithUrl(&epay.Config{
PartnerID: "1096",
Key: "n08V9LpE8JffA3NPP893689u8p39NV9J",
}, "https://api.lempay.org")
func GetAmount(id int, count float64, topUpCode string) float64 {
amount := count * 1.5
if topUpCode != "" {
if topUpCode == "nekoapi" {
if id == 89 {
amount = count * 1
} else if id == 98 || id == 105 || id == 107 {
amount = count * 1.2
} else if id == 1 {
amount = count * 1
}
}
}
return amount
}
func RequestEpay(c *gin.Context) {
var req EpayRequest
err := c.ShouldBindJSON(&req)
if err != nil {
c.JSON(200, gin.H{"message": err.Error(), "data": 10})
return
}
id := c.GetInt("id")
amount := GetAmount(id, float64(req.Amount), req.TopUpCode)
if id != 1 {
if req.Amount < 10 {
c.JSON(200, gin.H{"message": "最小充值10元", "data": amount, "count": 10})
return
}
}
if req.PaymentMethod == "zfb" {
if amount > 400 {
c.JSON(200, gin.H{"message": "支付宝最大充值400元", "data": amount, "count": 400})
return
}
req.PaymentMethod = "alipay"
}
if req.PaymentMethod == "wx" {
if amount > 600 {
c.JSON(200, gin.H{"message": "微信最大充值600元", "data": amount, "count": 600})
return
}
req.PaymentMethod = "wxpay"
}
returnUrl, _ := url.Parse("https://nekoapi.com/log")
notifyUrl, _ := url.Parse("https://nekoapi.com/api/user/epay/notify")
tradeNo := strconv.FormatInt(time.Now().Unix(), 10)
uri, params, err := client.Purchase(&epay.PurchaseArgs{
Type: epay.PurchaseType(req.PaymentMethod),
ServiceTradeNo: "A" + tradeNo,
Name: "B" + tradeNo,
Money: strconv.FormatFloat(amount*0.99, 'f', 2, 64),
Device: epay.PC,
NotifyUrl: notifyUrl,
ReturnUrl: returnUrl,
})
if err != nil {
c.JSON(200, gin.H{"message": "error", "data": "拉起支付失败"})
return
}
topUp := &model.TopUp{
UserId: id,
Amount: req.Amount,
Money: int(amount),
TradeNo: "A" + tradeNo,
CreateTime: time.Now().Unix(),
Status: "pending",
}
err = topUp.Insert()
if err != nil {
c.JSON(200, gin.H{"message": "error", "data": "创建订单失败"})
return
}
c.JSON(200, gin.H{"message": "success", "data": params, "url": uri})
}
func EpayNotify(c *gin.Context) {
params := lo.Reduce(lo.Keys(c.Request.URL.Query()), func(r map[string]string, t string, i int) map[string]string {
r[t] = c.Request.URL.Query().Get(t)
return r
}, map[string]string{})
verifyInfo, err := client.Verify(params)
if err == nil && verifyInfo.VerifyStatus {
_, err := c.Writer.Write([]byte("success"))
if err != nil {
log.Println("易支付回调写入失败")
}
} else {
_, err := c.Writer.Write([]byte("fail"))
if err != nil {
log.Println("易支付回调写入失败")
}
}
if verifyInfo.TradeStatus == epay.StatusTradeSuccess {
log.Println(verifyInfo)
topUp := model.GetTopUpByTradeNo(verifyInfo.ServiceTradeNo)
if topUp.Status == "pending" {
topUp.Status = "success"
err := topUp.Update()
if err != nil {
log.Printf("易支付回调更新订单失败: %v", topUp)
return
}
//user, _ := model.GetUserById(topUp.UserId, false)
//user.Quota += topUp.Amount * 500000
err = model.IncreaseUserQuota(topUp.UserId, topUp.Amount*500000)
if err != nil {
log.Printf("易支付回调更新用户失败: %v", topUp)
return
}
log.Printf("易支付回调更新用户成功 %v", topUp)
model.RecordLog(topUp.UserId, model.LogTypeTopup, fmt.Sprintf("使用在线充值成功,充值金额: %v", common.LogQuota(topUp.Amount*500000)))
}
} else {
log.Printf("易支付异常回调: %v", verifyInfo)
}
}
func RequestAmount(c *gin.Context) {
var req AmountRequest
err := c.ShouldBindJSON(&req)
if err != nil {
c.JSON(200, gin.H{"message": err.Error(), "data": 10})
return
}
id := c.GetInt("id")
if id != 1 {
if req.Amount < 10 {
c.JSON(200, gin.H{"message": "最小充值10刀", "data": GetAmount(id, 10, req.TopUpCode), "count": 10})
return
}
if req.Amount > 400 {
c.JSON(200, gin.H{"message": "最大充值400刀", "data": GetAmount(id, 400, req.TopUpCode), "count": 400})
return
}
}
c.JSON(200, gin.H{"message": "success", "data": GetAmount(id, float64(req.Amount), req.TopUpCode)})
}

View File

@ -3,6 +3,7 @@ package controller
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"log"
"net/http" "net/http"
"one-api/common" "one-api/common"
"one-api/model" "one-api/model"
@ -79,6 +80,8 @@ func setupLogin(user *model.User, c *gin.Context) {
DisplayName: user.DisplayName, DisplayName: user.DisplayName,
Role: user.Role, Role: user.Role,
Status: user.Status, Status: user.Status,
StableMode: user.StableMode,
MaxPrice: user.MaxPrice,
} }
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"message": "", "message": "",
@ -158,6 +161,8 @@ func Register(c *gin.Context) {
Password: user.Password, Password: user.Password,
DisplayName: user.Username, DisplayName: user.Username,
InviterId: inviterId, InviterId: inviterId,
StableMode: user.StableMode,
MaxPrice: user.MaxPrice,
} }
if common.EmailVerificationEnabled { if common.EmailVerificationEnabled {
cleanUser.Email = user.Email cleanUser.Email = user.Email
@ -420,6 +425,8 @@ func UpdateSelf(c *gin.Context) {
Username: user.Username, Username: user.Username,
Password: user.Password, Password: user.Password,
DisplayName: user.DisplayName, DisplayName: user.DisplayName,
StableMode: user.StableMode,
MaxPrice: user.MaxPrice,
} }
if user.Password == "$I_LOVE_U" { if user.Password == "$I_LOVE_U" {
user.Password = "" // rollback to what it should be user.Password = "" // rollback to what it should be
@ -741,3 +748,52 @@ func TopUp(c *gin.Context) {
}) })
return return
} }
type StableModeRequest struct {
StableMode bool `json:"stableMode"`
MaxPrice string `json:"maxPrice"`
}
func SetTableMode(c *gin.Context) {
req := &StableModeRequest{}
err := c.ShouldBindJSON(&req)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
log.Println(req)
id := c.GetInt("id")
user := model.User{
Id: id,
}
err = user.FillUserById()
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
user.StableMode = req.StableMode
if !req.StableMode {
req.MaxPrice = "0"
}
user.MaxPrice = req.MaxPrice
err = user.Update(false)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
"data": "设置成功",
})
return
}

4
go.mod
View File

@ -14,6 +14,7 @@ require (
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/pkoukk/tiktoken-go v0.1.1 github.com/pkoukk/tiktoken-go v0.1.1
github.com/star-horizon/go-epay v0.0.0-20230204124159-fa2e2293fdc2
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
gorm.io/driver/sqlite v1.4.3 gorm.io/driver/sqlite v1.4.3
@ -42,12 +43,15 @@ require (
github.com/leodido/go-urn v1.2.4 // indirect github.com/leodido/go-urn v1.2.4 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/samber/lo v1.37.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect github.com/ugorji/go/codec v1.2.11 // indirect
golang.org/x/arch v0.3.0 // indirect golang.org/x/arch v0.3.0 // indirect
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect
golang.org/x/net v0.10.0 // indirect golang.org/x/net v0.10.0 // indirect
golang.org/x/sys v0.8.0 // indirect golang.org/x/sys v0.8.0 // indirect
golang.org/x/text v0.9.0 // indirect golang.org/x/text v0.9.0 // indirect

10
go.sum
View File

@ -97,6 +97,8 @@ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -117,6 +119,10 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/samber/lo v1.37.0 h1:XjVcB8g6tgUp8rsPsJ2CvhClfImrpL04YpQHXeHPhRw=
github.com/samber/lo v1.37.0/go.mod h1:9vaz2O4o8oOnK23pd2TrXufcbdbJIa3b6cstBWKpopA=
github.com/star-horizon/go-epay v0.0.0-20230204124159-fa2e2293fdc2 h1:avbt5a8F/zbYwFzTugrqWOBJe/K1cJj6+xpr+x1oVAI=
github.com/star-horizon/go-epay v0.0.0-20230204124159-fa2e2293fdc2/go.mod h1:SiffGCWGGMVwujne2dUQbJ5zUVD1V1Yj0hDuTfqFNEo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@ -144,6 +150,8 @@ golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM=
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
@ -163,8 +171,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=

View File

@ -74,6 +74,7 @@ func main() {
} }
go controller.AutomaticallyTestChannels(frequency) go controller.AutomaticallyTestChannels(frequency)
} }
go controller.UpdateMidjourneyTask()
// Initialize HTTP server // Initialize HTTP server
server := gin.Default() server := gin.Default()

View File

@ -2,6 +2,7 @@ package middleware
import ( import (
"fmt" "fmt"
"log"
"net/http" "net/http"
"one-api/common" "one-api/common"
"one-api/model" "one-api/model"
@ -21,6 +22,7 @@ func Distribute() func(c *gin.Context) {
userGroup, _ := model.CacheGetUserGroup(userId) userGroup, _ := model.CacheGetUserGroup(userId)
c.Set("group", userGroup) c.Set("group", userGroup)
var channel *model.Channel var channel *model.Channel
var err error
channelId, ok := c.Get("channelId") channelId, ok := c.Get("channelId")
if ok { if ok {
id, err := strconv.Atoi(channelId.(string)) id, err := strconv.Atoi(channelId.(string))
@ -56,19 +58,28 @@ func Distribute() func(c *gin.Context) {
return return
} }
} else { } else {
// Select a channel for the user // Select a channel for the user
var modelRequest ModelRequest var modelRequest ModelRequest
err := common.UnmarshalBodyReusable(c, &modelRequest) if strings.HasPrefix(c.Request.URL.Path, "/mj") {
if err != nil { if modelRequest.Model == "" {
c.JSON(http.StatusBadRequest, gin.H{ modelRequest.Model = "midjourney"
"error": gin.H{ }
"message": "无效的请求", } else {
"type": "one_api_error", err := common.UnmarshalBodyReusable(c, &modelRequest)
}, if err != nil {
}) log.Println(err)
c.Abort() c.JSON(http.StatusBadRequest, gin.H{
return "error": gin.H{
"message": "无效的请求",
"type": "one_api_error",
},
})
c.Abort()
return
}
} }
if strings.HasPrefix(c.Request.URL.Path, "/v1/moderations") { if strings.HasPrefix(c.Request.URL.Path, "/v1/moderations") {
if modelRequest.Model == "" { if modelRequest.Model == "" {
modelRequest.Model = "text-moderation-stable" modelRequest.Model = "text-moderation-stable"
@ -84,21 +95,51 @@ func Distribute() func(c *gin.Context) {
modelRequest.Model = "dall-e" modelRequest.Model = "dall-e"
} }
} }
isStable := false
channel, err = model.CacheGetRandomSatisfiedChannel(userGroup, modelRequest.Model) channel, err = model.CacheGetRandomSatisfiedChannel(userGroup, modelRequest.Model)
c.Set("stable", false)
if err != nil { if err != nil {
message := fmt.Sprintf("当前分组 %s 下对于模型 %s 无可用渠道", userGroup, modelRequest.Model) message := fmt.Sprintf("当前分组 %s 下对于模型 %s 无可用渠道", userGroup, modelRequest.Model)
if channel != nil { if strings.HasPrefix(modelRequest.Model, "gpt-4") {
common.SysError(fmt.Sprintf("渠道不存在:%d", channel.Id)) common.SysLog("GPT-4低价渠道宕机正在尝试转换")
message = "数据库一致性已被破坏,请联系管理员" nowUser, err := model.GetUserById(userId, false)
if err == nil {
if nowUser.StableMode {
userGroup = "svip"
//stableRatio = (common.StablePrice / common.BasePrice) * modelRatio
userMaxPrice, _ := strconv.ParseFloat(nowUser.MaxPrice, 64)
if userMaxPrice < common.StablePrice {
message = "当前低价通道不可用,稳定渠道价格为" + strconv.FormatFloat(common.StablePrice, 'f', -1, 64) + "R/刀"
} else {
//common.SysLog(fmt.Sprintf("用户 %s 使用稳定渠道", nowUser.Username))
channel, err = model.CacheGetRandomSatisfiedChannel(userGroup, modelRequest.Model)
if err != nil {
message = "稳定渠道已经宕机,请联系管理员"
}
isStable = true
common.SysLog(fmt.Sprintf("用户 %s 使用稳定渠道 %v", nowUser.Username, channel))
c.Set("stable", true)
}
} else {
message = "当前低价通道不可用,请稍后再试,或者在后台开启稳定渠道模式"
}
}
}
//if channel == nil {
// common.SysError(fmt.Sprintf("渠道不存在:%d", channel.Id))
// message = "数据库一致性已被破坏,请联系管理员"
//}
if !isStable {
c.JSON(http.StatusInternalServerError, gin.H{
"error": gin.H{
"message": message,
"type": "one_api_error",
},
})
c.Abort()
return
} }
c.JSON(http.StatusServiceUnavailable, gin.H{
"error": gin.H{
"message": message,
"type": "one_api_error",
},
})
c.Abort()
return
} }
} }
c.Set("channel", channel.Type) c.Set("channel", channel.Type)

View File

@ -3,6 +3,7 @@ package model
import ( import (
"gorm.io/gorm" "gorm.io/gorm"
"one-api/common" "one-api/common"
"strings"
) )
type Log struct { type Log struct {
@ -17,6 +18,7 @@ type Log struct {
Quota int `json:"quota" gorm:"default:0"` Quota int `json:"quota" gorm:"default:0"`
PromptTokens int `json:"prompt_tokens" gorm:"default:0"` PromptTokens int `json:"prompt_tokens" gorm:"default:0"`
CompletionTokens int `json:"completion_tokens" gorm:"default:0"` CompletionTokens int `json:"completion_tokens" gorm:"default:0"`
TokenId int `json:"token_id" gorm:"default:0;index"`
} }
const ( const (
@ -27,6 +29,11 @@ const (
LogTypeSystem LogTypeSystem
) )
func GetLogByKey(key string) (logs []*Log, err error) {
err = DB.Joins("left join tokens on tokens.id = logs.token_id").Where("tokens.key = ?", strings.Split(key, "-")[1]).Find(&logs).Error
return logs, err
}
func RecordLog(userId int, logType int, content string) { func RecordLog(userId int, logType int, content string) {
if logType == LogTypeConsume && !common.LogConsumeEnabled { if logType == LogTypeConsume && !common.LogConsumeEnabled {
return return
@ -44,7 +51,7 @@ func RecordLog(userId int, logType int, content string) {
} }
} }
func RecordConsumeLog(userId int, promptTokens int, completionTokens int, modelName string, tokenName string, quota int, content string) { func RecordConsumeLog(userId int, promptTokens int, completionTokens int, modelName string, tokenName string, quota int, content string, tokenId int) {
if !common.LogConsumeEnabled { if !common.LogConsumeEnabled {
return return
} }
@ -59,6 +66,7 @@ func RecordConsumeLog(userId int, promptTokens int, completionTokens int, modelN
TokenName: tokenName, TokenName: tokenName,
ModelName: modelName, ModelName: modelName,
Quota: quota, Quota: quota,
TokenId: tokenId,
} }
err := DB.Create(log).Error err := DB.Create(log).Error
if err != nil { if err != nil {

View File

@ -88,6 +88,14 @@ func InitDB() (err error) {
if err != nil { if err != nil {
return err return err
} }
err = db.AutoMigrate(&Midjourney{})
if err != nil {
return err
}
err = db.AutoMigrate(&TopUp{})
if err != nil {
return err
}
common.SysLog("database migrated") common.SysLog("database migrated")
err = createRootAccountIfNeed() err = createRootAccountIfNeed()
return err return err

87
model/midjourney.go Normal file
View File

@ -0,0 +1,87 @@
package model
type Midjourney struct {
Id int `json:"id"`
Code int `json:"code"`
UserId int `json:"user_id" gorm:"index"`
Action string `json:"action"`
MjId string `json:"mj_id" gorm:"index"`
Prompt string `json:"prompt"`
PromptEn string `json:"prompt_en"`
Description string `json:"description"`
State string `json:"state"`
SubmitTime int64 `json:"submit_time"`
StartTime int64 `json:"start_time"`
FinishTime int64 `json:"finish_time"`
ImageUrl string `json:"image_url"`
Status string `json:"status"`
Progress string `json:"progress"`
FailReason string `json:"fail_reason"`
}
func GetAllUserTask(userId int, startIdx int, num int) []*Midjourney {
var tasks []*Midjourney
var err error
err = DB.Where("user_id = ?", userId).Order("id desc").Limit(num).Offset(startIdx).Find(&tasks).Error
if err != nil {
return nil
}
return tasks
}
func GetAllTasks(startIdx int, num int) []*Midjourney {
var tasks []*Midjourney
var err error
err = DB.Order("id desc").Limit(num).Offset(startIdx).Find(&tasks).Error
if err != nil {
return nil
}
return tasks
}
func GetAllUnFinishTasks() []*Midjourney {
var tasks []*Midjourney
var err error
// get all tasks progress is not 100%
err = DB.Where("progress != ?", "100%").Find(&tasks).Error
if err != nil {
return nil
}
return tasks
}
func GetByMJId(mjId string) *Midjourney {
var mj *Midjourney
var err error
err = DB.Where("mj_id = ?", mjId).First(&mj).Error
if err != nil {
return nil
}
return mj
}
func GetMjByuId(id int) *Midjourney {
var mj *Midjourney
var err error
err = DB.Where("id = ?", id).First(&mj).Error
if err != nil {
return nil
}
return mj
}
func UpdateProgress(id int, progress string) error {
return DB.Model(&Midjourney{}).Where("id = ?", id).Update("progress", progress).Error
}
func (midjourney *Midjourney) Insert() error {
var err error
err = DB.Create(midjourney).Error
return err
}
func (midjourney *Midjourney) Update() error {
var err error
err = DB.Save(midjourney).Error
return err
}

View File

@ -69,6 +69,10 @@ func InitOptionMap() {
common.OptionMap["ChatLink"] = common.ChatLink common.OptionMap["ChatLink"] = common.ChatLink
common.OptionMap["QuotaPerUnit"] = strconv.FormatFloat(common.QuotaPerUnit, 'f', -1, 64) common.OptionMap["QuotaPerUnit"] = strconv.FormatFloat(common.QuotaPerUnit, 'f', -1, 64)
common.OptionMap["RetryTimes"] = strconv.Itoa(common.RetryTimes) common.OptionMap["RetryTimes"] = strconv.Itoa(common.RetryTimes)
common.OptionMap["NormalPrice"] = strconv.FormatFloat(common.NormalPrice, 'f', -1, 64)
common.OptionMap["StablePrice"] = strconv.FormatFloat(common.StablePrice, 'f', -1, 64)
common.OptionMap["BasePrice"] = strconv.FormatFloat(common.BasePrice, 'f', -1, 64)
common.OptionMapRWMutex.Unlock() common.OptionMapRWMutex.Unlock()
loadOptionsFromDatabase() loadOptionsFromDatabase()
} }
@ -207,6 +211,12 @@ func updateOptionMap(key string, value string) (err error) {
common.TopUpLink = value common.TopUpLink = value
case "ChatLink": case "ChatLink":
common.ChatLink = value common.ChatLink = value
case "NormalPrice":
common.NormalPrice, _ = strconv.ParseFloat(value, 64)
case "BasePrice":
common.BasePrice, _ = strconv.ParseFloat(value, 64)
case "StablePrice":
common.StablePrice, _ = strconv.ParseFloat(value, 64)
case "ChannelDisableThreshold": case "ChannelDisableThreshold":
common.ChannelDisableThreshold, _ = strconv.ParseFloat(value, 64) common.ChannelDisableThreshold, _ = strconv.ParseFloat(value, 64)
case "QuotaPerUnit": case "QuotaPerUnit":

43
model/topup.go Normal file
View File

@ -0,0 +1,43 @@
package model
type TopUp struct {
Id int `json:"id"`
UserId int `json:"user_id" gorm:"index"`
Amount int `json:"amount"`
Money int `json:"money"`
TradeNo string `json:"trade_no"`
CreateTime int64 `json:"create_time"`
Status string `json:"status"`
}
func (topUp *TopUp) Insert() error {
var err error
err = DB.Create(topUp).Error
return err
}
func (topUp *TopUp) Update() error {
var err error
err = DB.Save(topUp).Error
return err
}
func GetTopUpById(id int) *TopUp {
var topUp *TopUp
var err error
err = DB.Where("id = ?", id).First(&topUp).Error
if err != nil {
return nil
}
return topUp
}
func GetTopUpByTradeNo(tradeNo string) *TopUp {
var topUp *TopUp
var err error
err = DB.Where("trade_no = ?", tradeNo).First(&topUp).Error
if err != nil {
return nil
}
return topUp
}

View File

@ -28,6 +28,8 @@ type User struct {
Group string `json:"group" gorm:"type:varchar(32);default:'default'"` Group string `json:"group" gorm:"type:varchar(32);default:'default'"`
AffCode string `json:"aff_code" gorm:"type:varchar(32);column:aff_code;uniqueIndex"` AffCode string `json:"aff_code" gorm:"type:varchar(32);column:aff_code;uniqueIndex"`
InviterId int `json:"inviter_id" gorm:"type:int;column:inviter_id;index"` InviterId int `json:"inviter_id" gorm:"type:int;column:inviter_id;index"`
StableMode bool `json:"stable_mode" gorm:"type:tinyint;default:0;column:stable_mode"`
MaxPrice string `json:"max_price" gorm:"type:varchar(32);default:'7'"`
} }
func GetMaxUserId() int { func GetMaxUserId() int {
@ -116,7 +118,14 @@ func (user *User) Update(updatePassword bool) error {
return err return err
} }
} }
err = DB.Model(user).Updates(user).Error newUser := *user
err = DB.Model(user).UpdateColumns(map[string]interface{}{
"stable_mode": user.StableMode,
"max_price": user.MaxPrice,
}).Error
DB.First(&user, user.Id)
err = DB.Model(user).Updates(newUser).Error
return err return err
} }

View File

@ -16,6 +16,7 @@ func SetApiRouter(router *gin.Engine) {
apiRouter.GET("/status", controller.GetStatus) apiRouter.GET("/status", controller.GetStatus)
apiRouter.GET("/notice", controller.GetNotice) apiRouter.GET("/notice", controller.GetNotice)
apiRouter.GET("/about", controller.GetAbout) apiRouter.GET("/about", controller.GetAbout)
apiRouter.GET("/midjourney", controller.GetMidjourney)
apiRouter.GET("/home_page_content", controller.GetHomePageContent) apiRouter.GET("/home_page_content", controller.GetHomePageContent)
apiRouter.GET("/verification", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendEmailVerification) apiRouter.GET("/verification", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendEmailVerification)
apiRouter.GET("/reset_password", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendPasswordResetEmail) apiRouter.GET("/reset_password", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendPasswordResetEmail)
@ -29,7 +30,9 @@ func SetApiRouter(router *gin.Engine) {
{ {
userRoute.POST("/register", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.Register) userRoute.POST("/register", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.Register)
userRoute.POST("/login", middleware.CriticalRateLimit(), controller.Login) userRoute.POST("/login", middleware.CriticalRateLimit(), controller.Login)
//userRoute.POST("/tokenlog", middleware.CriticalRateLimit(), controller.TokenLog)
userRoute.GET("/logout", controller.Logout) userRoute.GET("/logout", controller.Logout)
userRoute.GET("/epay/notify", controller.EpayNotify)
selfRoute := userRoute.Group("/") selfRoute := userRoute.Group("/")
selfRoute.Use(middleware.UserAuth()) selfRoute.Use(middleware.UserAuth())
@ -40,6 +43,9 @@ func SetApiRouter(router *gin.Engine) {
selfRoute.GET("/token", controller.GenerateAccessToken) selfRoute.GET("/token", controller.GenerateAccessToken)
selfRoute.GET("/aff", controller.GetAffCode) selfRoute.GET("/aff", controller.GetAffCode)
selfRoute.POST("/topup", controller.TopUp) selfRoute.POST("/topup", controller.TopUp)
selfRoute.POST("/pay", controller.RequestEpay)
selfRoute.POST("/amount", controller.RequestAmount)
selfRoute.POST("/set_stable_mode", controller.SetTableMode)
} }
adminRoute := userRoute.Group("/") adminRoute := userRoute.Group("/")
@ -102,10 +108,14 @@ func SetApiRouter(router *gin.Engine) {
logRoute.GET("/search", middleware.AdminAuth(), controller.SearchAllLogs) logRoute.GET("/search", middleware.AdminAuth(), controller.SearchAllLogs)
logRoute.GET("/self", middleware.UserAuth(), controller.GetUserLogs) logRoute.GET("/self", middleware.UserAuth(), controller.GetUserLogs)
logRoute.GET("/self/search", middleware.UserAuth(), controller.SearchUserLogs) logRoute.GET("/self/search", middleware.UserAuth(), controller.SearchUserLogs)
logRoute.GET("/token", controller.GetLogByKey)
groupRoute := apiRouter.Group("/group") groupRoute := apiRouter.Group("/group")
groupRoute.Use(middleware.AdminAuth()) groupRoute.Use(middleware.AdminAuth())
{ {
groupRoute.GET("/", controller.GetGroups) groupRoute.GET("/", controller.GetGroups)
} }
mjRoute := apiRouter.Group("/mj")
mjRoute.GET("/self", middleware.UserAuth(), controller.GetUserMidjourney)
mjRoute.GET("/", middleware.AdminAuth(), controller.GetAllMidjourney)
} }
} }

View File

@ -41,4 +41,12 @@ func SetRelayRouter(router *gin.Engine) {
relayV1Router.DELETE("/models/:model", controller.RelayNotImplemented) relayV1Router.DELETE("/models/:model", controller.RelayNotImplemented)
relayV1Router.POST("/moderations", controller.Relay) relayV1Router.POST("/moderations", controller.Relay)
} }
relayMjRouter := router.Group("/mj")
relayMjRouter.Use(middleware.TokenAuth(), middleware.Distribute())
{
relayMjRouter.POST("/submit/imagine", controller.RelayMidjourney)
relayMjRouter.POST("/submit/change", controller.RelayMidjourney)
relayMjRouter.POST("/notify", controller.RelayMidjourney)
relayMjRouter.GET("/task/:id/fetch", controller.RelayMidjourney)
}
} }

View File

@ -1,20 +1,20 @@
import React, { lazy, Suspense, useContext, useEffect } from 'react'; import React, {lazy, Suspense, useContext, useEffect} from 'react';
import { Route, Routes } from 'react-router-dom'; import {Route, Routes} from 'react-router-dom';
import Loading from './components/Loading'; import Loading from './components/Loading';
import User from './pages/User'; import User from './pages/User';
import { PrivateRoute } from './components/PrivateRoute'; import {PrivateRoute} from './components/PrivateRoute';
import RegisterForm from './components/RegisterForm'; import RegisterForm from './components/RegisterForm';
import LoginForm from './components/LoginForm'; import LoginForm from './components/LoginForm';
import NotFound from './pages/NotFound'; import NotFound from './pages/NotFound';
import Setting from './pages/Setting'; import Setting from './pages/Setting';
import EditUser from './pages/User/EditUser'; import EditUser from './pages/User/EditUser';
import AddUser from './pages/User/AddUser'; import AddUser from './pages/User/AddUser';
import { API, getLogo, getSystemName, showError, showNotice } from './helpers'; import {API, getLogo, getSystemName, showError, showNotice} from './helpers';
import PasswordResetForm from './components/PasswordResetForm'; import PasswordResetForm from './components/PasswordResetForm';
import GitHubOAuth from './components/GitHubOAuth'; import GitHubOAuth from './components/GitHubOAuth';
import PasswordResetConfirm from './components/PasswordResetConfirm'; import PasswordResetConfirm from './components/PasswordResetConfirm';
import { UserContext } from './context/User'; import {UserContext} from './context/User';
import { StatusContext } from './context/Status'; import {StatusContext} from './context/Status';
import Channel from './pages/Channel'; import Channel from './pages/Channel';
import Token from './pages/Token'; import Token from './pages/Token';
import EditToken from './pages/Token/EditToken'; import EditToken from './pages/Token/EditToken';
@ -24,268 +24,295 @@ import EditRedemption from './pages/Redemption/EditRedemption';
import TopUp from './pages/TopUp'; import TopUp from './pages/TopUp';
import Log from './pages/Log'; import Log from './pages/Log';
import Chat from './pages/Chat'; import Chat from './pages/Chat';
import Midjourney from './pages/Midjourney';
const Home = lazy(() => import('./pages/Home')); 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');
if (user) { if (user) {
let data = JSON.parse(user); let data = JSON.parse(user);
userDispatch({ type: 'login', payload: data }); userDispatch({type: 'login', payload: data});
} }
}; };
const loadStatus = async () => { const loadStatus = async () => {
const res = await API.get('/api/status'); const res = await API.get('/api/status');
const { success, data } = res.data; const {success, data} = res.data;
if (success) { if (success) {
localStorage.setItem('status', JSON.stringify(data)); localStorage.setItem('status', JSON.stringify(data));
statusDispatch({ type: 'set', payload: data }); statusDispatch({type: 'set', payload: data});
localStorage.setItem('system_name', data.system_name); localStorage.setItem('system_name', data.system_name);
localStorage.setItem('logo', data.logo); localStorage.setItem('logo', data.logo);
localStorage.setItem('footer_html', data.footer_html); localStorage.setItem('footer_html', data.footer_html);
localStorage.setItem('quota_per_unit', data.quota_per_unit); localStorage.setItem('quota_per_unit', data.quota_per_unit);
localStorage.setItem('display_in_currency', data.display_in_currency); localStorage.setItem('display_in_currency', data.display_in_currency);
if (data.chat_link) { if (data.chat_link) {
localStorage.setItem('chat_link', data.chat_link); localStorage.setItem('chat_link', data.chat_link);
} else { } else {
localStorage.removeItem('chat_link'); localStorage.removeItem('chat_link');
} }
if ( if (
data.version !== process.env.REACT_APP_VERSION && data.version !== process.env.REACT_APP_VERSION &&
data.version !== 'v0.0.0' && data.version !== 'v0.0.0' &&
process.env.REACT_APP_VERSION !== '' process.env.REACT_APP_VERSION !== ''
) { ) {
showNotice( showNotice(
`新版本可用:${data.version},请使用快捷键 Shift + F5 刷新页面` `新版本可用:${data.version},请使用快捷键 Shift + F5 刷新页面`
); );
} }
} else { } else {
showError('无法正常连接至服务器!'); showError('无法正常连接至服务器!');
} }
}; };
useEffect(() => { // const getOptions = async () => {
loadUser(); // const res = await API.get('/api/option/');
loadStatus().then(); // const {success, message, data} = res.data;
let systemName = getSystemName(); // if (success) {
if (systemName) { // let newInputs = {};
document.title = systemName; // data.forEach((item) => {
} // if (item.key === 'ModelRatio' || item.key === 'GroupRatio') {
let logo = getLogo(); // item.value = JSON.stringify(JSON.parse(item.value), null, 2);
if (logo) { // }
let linkElement = document.querySelector("link[rel~='icon']"); // newInputs[item.key] = item.value;
if (linkElement) { // });
linkElement.href = logo; // setInputs(newInputs);
} // setOriginInputs(newInputs);
} // } else {
}, []); // showError(message);
// }
// };
return ( useEffect(() => {
<Routes> loadUser();
<Route loadStatus().then();
path='/' let systemName = getSystemName();
element={ if (systemName) {
<Suspense fallback={<Loading></Loading>}> document.title = systemName;
<Home />
</Suspense>
} }
/> let logo = getLogo();
<Route if (logo) {
path='/channel' let linkElement = document.querySelector("link[rel~='icon']");
element={ if (linkElement) {
<PrivateRoute> linkElement.href = logo;
<Channel /> }
</PrivateRoute>
} }
/> }, []);
<Route
path='/channel/edit/:id' return (
element={ <Routes>
<Suspense fallback={<Loading></Loading>}> <Route
<EditChannel /> path='/'
</Suspense> element={
} <Suspense fallback={<Loading></Loading>}>
/> <Home/>
<Route </Suspense>
path='/channel/add' }
element={ />
<Suspense fallback={<Loading></Loading>}> <Route
<EditChannel /> path='/channel'
</Suspense> element={
} <PrivateRoute>
/> <Channel/>
<Route </PrivateRoute>
path='/token' }
element={ />
<PrivateRoute> <Route
<Token /> path='/channel/edit/:id'
</PrivateRoute> element={
} <Suspense fallback={<Loading></Loading>}>
/> <EditChannel/>
<Route </Suspense>
path='/token/edit/:id' }
element={ />
<Suspense fallback={<Loading></Loading>}> <Route
<EditToken /> path='/channel/add'
</Suspense> element={
} <Suspense fallback={<Loading></Loading>}>
/> <EditChannel/>
<Route </Suspense>
path='/token/add' }
element={ />
<Suspense fallback={<Loading></Loading>}> <Route
<EditToken /> path='/token'
</Suspense> element={
} <PrivateRoute>
/> <Token/>
<Route </PrivateRoute>
path='/redemption' }
element={ />
<PrivateRoute> <Route
<Redemption /> path='/token/edit/:id'
</PrivateRoute> element={
} <Suspense fallback={<Loading></Loading>}>
/> <EditToken/>
<Route </Suspense>
path='/redemption/edit/:id' }
element={ />
<Suspense fallback={<Loading></Loading>}> <Route
<EditRedemption /> path='/token/add'
</Suspense> element={
} <Suspense fallback={<Loading></Loading>}>
/> <EditToken/>
<Route </Suspense>
path='/redemption/add' }
element={ />
<Suspense fallback={<Loading></Loading>}> <Route
<EditRedemption /> path='/redemption'
</Suspense> element={
} <PrivateRoute>
/> <Redemption/>
<Route </PrivateRoute>
path='/user' }
element={ />
<PrivateRoute> <Route
<User /> path='/redemption/edit/:id'
</PrivateRoute> element={
} <Suspense fallback={<Loading></Loading>}>
/> <EditRedemption/>
<Route </Suspense>
path='/user/edit/:id' }
element={ />
<Suspense fallback={<Loading></Loading>}> <Route
<EditUser /> path='/redemption/add'
</Suspense> element={
} <Suspense fallback={<Loading></Loading>}>
/> <EditRedemption/>
<Route </Suspense>
path='/user/edit' }
element={ />
<Suspense fallback={<Loading></Loading>}> <Route
<EditUser /> path='/user'
</Suspense> element={
} <PrivateRoute>
/> <User/>
<Route </PrivateRoute>
path='/user/add' }
element={ />
<Suspense fallback={<Loading></Loading>}> <Route
<AddUser /> path='/user/edit/:id'
</Suspense> element={
} <Suspense fallback={<Loading></Loading>}>
/> <EditUser/>
<Route </Suspense>
path='/user/reset' }
element={ />
<Suspense fallback={<Loading></Loading>}> <Route
<PasswordResetConfirm /> path='/user/edit'
</Suspense> element={
} <Suspense fallback={<Loading></Loading>}>
/> <EditUser/>
<Route </Suspense>
path='/login' }
element={ />
<Suspense fallback={<Loading></Loading>}> <Route
<LoginForm /> path='/user/add'
</Suspense> element={
} <Suspense fallback={<Loading></Loading>}>
/> <AddUser/>
<Route </Suspense>
path='/register' }
element={ />
<Suspense fallback={<Loading></Loading>}> <Route
<RegisterForm /> path='/user/reset'
</Suspense> element={
} <Suspense fallback={<Loading></Loading>}>
/> <PasswordResetConfirm/>
<Route </Suspense>
path='/reset' }
element={ />
<Suspense fallback={<Loading></Loading>}> <Route
<PasswordResetForm /> path='/login'
</Suspense> element={
} <Suspense fallback={<Loading></Loading>}>
/> <LoginForm/>
<Route </Suspense>
path='/oauth/github' }
element={ />
<Suspense fallback={<Loading></Loading>}> <Route
<GitHubOAuth /> path='/register'
</Suspense> element={
} <Suspense fallback={<Loading></Loading>}>
/> <RegisterForm/>
<Route </Suspense>
path='/setting' }
element={ />
<PrivateRoute> <Route
<Suspense fallback={<Loading></Loading>}> path='/reset'
<Setting /> element={
</Suspense> <Suspense fallback={<Loading></Loading>}>
</PrivateRoute> <PasswordResetForm/>
} </Suspense>
/> }
<Route />
path='/topup' <Route
element={ path='/oauth/github'
<PrivateRoute> element={
<Suspense fallback={<Loading></Loading>}> <Suspense fallback={<Loading></Loading>}>
<TopUp /> <GitHubOAuth/>
</Suspense> </Suspense>
</PrivateRoute> }
} />
/> <Route
<Route path='/setting'
path='/log' element={
element={ <PrivateRoute>
<PrivateRoute> <Suspense fallback={<Loading></Loading>}>
<Log /> <Setting/>
</PrivateRoute> </Suspense>
} </PrivateRoute>
/> }
<Route />
path='/about' <Route
element={ path='/topup'
<Suspense fallback={<Loading></Loading>}> element={
<About /> <PrivateRoute>
</Suspense> <Suspense fallback={<Loading></Loading>}>
} <TopUp/>
/> </Suspense>
<Route </PrivateRoute>
path='/chat' }
element={ />
<Suspense fallback={<Loading></Loading>}> <Route
<Chat /> path='/log'
</Suspense> element={
} <PrivateRoute>
/> <Log/>
<Route path='*' element={NotFound} /> </PrivateRoute>
</Routes> }
); />
<Route
path='/midjourney'
element={
<PrivateRoute>
<Midjourney/>
</PrivateRoute>
}
/>
<Route
path='/about'
element={
<Suspense fallback={<Loading></Loading>}>
<About/>
</Suspense>
}
/>
<Route
path='/chat'
element={
<Suspense fallback={<Loading></Loading>}>
<Chat/>
</Suspense>
}
/>
<Route path='*' element={NotFound}/>
</Routes>
);
} }
export default App; export default App;

View File

@ -4,7 +4,7 @@ import { Link } from 'react-router-dom';
import { API, showError, showInfo, showSuccess, timestamp2string } from '../helpers'; import { API, showError, showInfo, showSuccess, timestamp2string } from '../helpers';
import { CHANNEL_OPTIONS, ITEMS_PER_PAGE } from '../constants'; import { CHANNEL_OPTIONS, ITEMS_PER_PAGE } from '../constants';
import { renderGroup, renderNumber } from '../helpers/render'; import {renderGroup, renderNumber, renderQuota} from '../helpers/render';
function renderTimestamp(timestamp) { function renderTimestamp(timestamp) {
return ( return (
@ -299,6 +299,7 @@ const ChannelsTable = () => {
onClick={() => { onClick={() => {
sortChannel('group'); sortChannel('group');
}} }}
width={1}
> >
分组 分组
</Table.HeaderCell> </Table.HeaderCell>
@ -307,6 +308,7 @@ const ChannelsTable = () => {
onClick={() => { onClick={() => {
sortChannel('type'); sortChannel('type');
}} }}
width={2}
> >
类型 类型
</Table.HeaderCell> </Table.HeaderCell>
@ -315,6 +317,7 @@ const ChannelsTable = () => {
onClick={() => { onClick={() => {
sortChannel('status'); sortChannel('status');
}} }}
width={2}
> >
状态 状态
</Table.HeaderCell> </Table.HeaderCell>
@ -326,6 +329,15 @@ const ChannelsTable = () => {
> >
响应时间 响应时间
</Table.HeaderCell> </Table.HeaderCell>
<Table.HeaderCell
style={{ cursor: 'pointer' }}
onClick={() => {
sortChannel('used_quota');
}}
width={1}
>
已使用
</Table.HeaderCell>
<Table.HeaderCell <Table.HeaderCell
style={{ cursor: 'pointer' }} style={{ cursor: 'pointer' }}
onClick={() => { onClick={() => {
@ -361,6 +373,7 @@ const ChannelsTable = () => {
basic basic
/> />
</Table.Cell> </Table.Cell>
<Table.Cell>{renderQuota(channel.used_quota)}</Table.Cell>
<Table.Cell> <Table.Cell>
<Popup <Popup
content={channel.balance_updated_time ? renderTimestamp(channel.balance_updated_time) : '未更新'} content={channel.balance_updated_time ? renderTimestamp(channel.balance_updated_time) : '未更新'}

View File

@ -46,6 +46,11 @@ let headerButtons = [
to: '/log', to: '/log',
icon: 'book' icon: 'book'
}, },
{
name: 'Midjourney',
to: '/midjourney',
icon: 'images outline'
},
{ {
name: '设置', name: '设置',
to: '/setting', to: '/setting',

View File

@ -0,0 +1,385 @@
import React, { useEffect, useState } from 'react';
import { Button, Form, Header, Label, Pagination, Segment, Select, Table } from 'semantic-ui-react';
import { API, isAdmin, showError, timestamp2string } from '../helpers';
import { ITEMS_PER_PAGE } from '../constants';
import { renderQuota } from '../helpers/render';
import {Link} from "react-router-dom";
function renderTimestamp(timestamp) {
return (
<>
{timestamp2string(timestamp)}
</>
);
}
const MODE_OPTIONS = [
{ key: 'all', text: '全部用户', value: 'all' },
{ key: 'self', text: '当前用户', value: 'self' }
];
const LOG_OPTIONS = [
{ key: '0', text: '全部', value: 0 },
// { key: '1', text: '绘图', value: 1 },
// { key: '2', text: '放大', value: 2 },
// { key: '3', text: '变换', value: 3 },
// { key: '4', text: '图生文', value: 4 },
// { key: '5', text: '图片混合', value: 5 }
];
function renderType(type) {
switch (type) {
case 'IMAGINE':
return <Label basic color='blue'> 绘图 </Label>;
case 'UPSCALE':
return <Label basic color='orange'> 放大 </Label>;
case 'VARIATION':
return <Label basic color='purple'> 变换 </Label>;
case 'DESCRIBE':
return <Label basic color='yellow'> 图生文 </Label>;
case 'BLEAND':
return <Label basic color='olive'> 图混合 </Label>;
default:
return <Label basic color='black'> 未知 </Label>;
}
}
function renderCode(type) {
switch (type) {
case 1:
return <Label basic color='green'> 已提交 </Label>;
case 21:
return <Label basic color='olive'> 排队中 </Label>;
case 22:
return <Label basic color='orange'> 重复提交 </Label>;
default:
return <Label basic color='black'> 未知 </Label>;
}
}
function renderStatus(type) {
switch (type) {
case 'SUCCESS':
return <Label basic color='green'> 成功 </Label>;
case 'NOT_START':
return <Label basic color='black'> 未启动 </Label>;
case 'SUBMITTED':
return <Label basic color='yellow'> 队列中 </Label>;
case 'IN_PROGRESS':
return <Label basic color='blue'> 执行中 </Label>;
case 'FAILURE':
return <Label basic color='red'> 失败 </Label>;
default:
return <Label basic color='black'> 未知 </Label>;
}
}
const LogsTable = () => {
const [logs, setLogs] = useState([
]);
const [loading, setLoading] = useState(true);
const [activePage, setActivePage] = useState(1);
const [searchKeyword, setSearchKeyword] = useState('');
const [searching, setSearching] = useState(false);
const [logType, setLogType] = useState(0);
const isAdminUser = isAdmin();
let now = new Date();
const [inputs, setInputs] = useState({
username: '',
token_name: '',
model_name: '',
start_timestamp: timestamp2string(0),
end_timestamp: timestamp2string(now.getTime() / 1000 + 3600)
});
const { username, token_name, model_name, start_timestamp, end_timestamp } = inputs;
const [stat, setStat] = useState({
quota: 0,
token: 0
});
const handleInputChange = (e, { name, value }) => {
setInputs((inputs) => ({ ...inputs, [name]: value }));
};
const getLogSelfStat = async () => {
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
let localEndTimestamp = Date.parse(end_timestamp) / 1000;
let res = await API.get(`/api/log/self/stat?type=${logType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`);
const { success, message, data } = res.data;
if (success) {
setStat(data);
} else {
showError(message);
}
};
const getLogStat = async () => {
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
let localEndTimestamp = Date.parse(end_timestamp) / 1000;
let res = await API.get(`/api/log/stat?type=${logType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`);
const { success, message, data } = res.data;
if (success) {
setStat(data);
} else {
showError(message);
}
};
const loadLogs = async (startIdx) => {
let url = '';
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
let localEndTimestamp = Date.parse(end_timestamp) / 1000;
if (isAdminUser) {
url = `/api/mj/?p=${startIdx}&username=${username}&token_name=${token_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
} else {
url = `/api/mj/self/?p=${startIdx}&token_name=${token_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
}
const res = await API.get(url);
const { success, message, data } = res.data;
if (success) {
if (startIdx === 0) {
setLogs(data);
} else {
let newLogs = [...logs];
newLogs.splice(startIdx * ITEMS_PER_PAGE, data.length, ...data);
setLogs(newLogs);
}
} else {
showError(message);
}
setLoading(false);
};
const onPaginationChange = (e, { activePage }) => {
(async () => {
if (activePage === Math.ceil(logs.length / ITEMS_PER_PAGE) + 1) {
// In this case we have to load more data and then append them.
await loadLogs(activePage - 1);
}
setActivePage(activePage);
})();
};
const refresh = async () => {
setLoading(true);
setActivePage(1)
await loadLogs(0);
// if (isAdminUser) {
// getLogStat().then();
// } else {
// getLogSelfStat().then();
// }
};
useEffect(() => {
refresh().then();
}, [logType]);
const searchLogs = async () => {
if (searchKeyword === '') {
// if keyword is blank, load files instead.
await loadLogs(0);
setActivePage(1);
return;
}
setSearching(true);
const res = await API.get(`/api/log/self/search?keyword=${searchKeyword}`);
const { success, message, data } = res.data;
if (success) {
setLogs(data);
setActivePage(1);
} else {
showError(message);
}
setSearching(false);
};
const handleKeywordChange = async (e, { value }) => {
setSearchKeyword(value.trim());
};
const sortLog = (key) => {
if (logs.length === 0) return;
setLoading(true);
let sortedLogs = [...logs];
if (typeof sortedLogs[0][key] === 'string'){
sortedLogs.sort((a, b) => {
return ('' + a[key]).localeCompare(b[key]);
});
} else {
sortedLogs.sort((a, b) => {
if (a[key] === b[key]) return 0;
if (a[key] > b[key]) return -1;
if (a[key] < b[key]) return 1;
});
}
if (sortedLogs[0].id === logs[0].id) {
sortedLogs.reverse();
}
setLogs(sortedLogs);
setLoading(false);
};
return (
<>
<Segment>
<Table basic compact size='small'>
<Table.Header>
<Table.Row>
<Table.HeaderCell
style={{ cursor: 'pointer' }}
onClick={() => {
sortLog('submit_time');
}}
width={2}
>
提交时间
</Table.HeaderCell>
<Table.HeaderCell
style={{ cursor: 'pointer' }}
onClick={() => {
sortLog('action');
}}
width={1}
>
类型
</Table.HeaderCell>
<Table.HeaderCell
style={{ cursor: 'pointer' }}
onClick={() => {
sortLog('mj_id');
}}
width={2}
>
任务ID
</Table.HeaderCell>
<Table.HeaderCell
style={{ cursor: 'pointer' }}
onClick={() => {
sortLog('code');
}}
width={1}
>
提交结果
</Table.HeaderCell>
<Table.HeaderCell
style={{ cursor: 'pointer' }}
onClick={() => {
sortLog('status');
}}
width={1}
>
任务状态
</Table.HeaderCell>
<Table.HeaderCell
style={{ cursor: 'pointer' }}
onClick={() => {
sortLog('progress');
}}
width={1}
>
进度
</Table.HeaderCell>
<Table.HeaderCell
style={{ cursor: 'pointer' }}
onClick={() => {
sortLog('image_url');
}}
width={1}
>
结果图片
</Table.HeaderCell>
<Table.HeaderCell
style={{ cursor: 'pointer' }}
onClick={() => {
sortLog('prompt');
}}
width={3}
>
Prompt
</Table.HeaderCell>
<Table.HeaderCell
style={{ cursor: 'pointer' }}
onClick={() => {
sortLog('fail_reason');
}}
width={1}
>
失败原因
</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{logs
.slice(
(activePage - 1) * ITEMS_PER_PAGE,
activePage * ITEMS_PER_PAGE
)
.map((log, idx) => {
if (log.deleted) return <></>;
return (
<Table.Row key={log.created_at}>
<Table.Cell>{renderTimestamp(log.submit_time/1000)}</Table.Cell>
{/*{*/}
{/* isAdminUser && (*/}
{/* <Table.Cell>{log.username ? <Label>{log.username}</Label> : ''}</Table.Cell>*/}
{/* )*/}
{/*}*/}
<Table.Cell>{renderType(log.action)}</Table.Cell>
<Table.Cell>{log.mj_id}</Table.Cell>
<Table.Cell>{renderCode(log.code)}</Table.Cell>
<Table.Cell>{renderStatus(log.status)}</Table.Cell>
<Table.Cell>{log.progress ? <Label basic>{log.progress}</Label> : ''}</Table.Cell>
<Table.Cell>
{
log.image_url ? (
<Link to={log.image_url} target='_blank'>点击查看</Link>
) : '暂未生成图片'
}
</Table.Cell>
<Table.Cell>{log.prompt}</Table.Cell>
<Table.Cell>{log.fail_reason ? log.fail_reason : '无'}</Table.Cell>
</Table.Row>
);
})}
</Table.Body>
<Table.Footer>
<Table.Row>
<Table.HeaderCell colSpan={'9'}>
<Select
placeholder='选择明细分类'
options={LOG_OPTIONS}
style={{ marginRight: '8px' }}
name='logType'
value={logType}
onChange={(e, { name, value }) => {
setLogType(value);
}}
/>
<Button size='small' onClick={refresh} loading={loading}>刷新</Button>
<Pagination
floated='right'
activePage={activePage}
onPageChange={onPaginationChange}
size='small'
siblingRange={1}
totalPages={
Math.ceil(logs.length / ITEMS_PER_PAGE) +
(logs.length % ITEMS_PER_PAGE === 0 ? 1 : 0)
}
/>
</Table.HeaderCell>
</Table.Row>
</Table.Footer>
</Table>
</Segment>
</>
);
};
export default LogsTable;

View File

@ -1,331 +1,372 @@
import React, { useEffect, useState } from 'react'; import React, {useEffect, useState} from 'react';
import { Divider, Form, Grid, Header } from 'semantic-ui-react'; import {Divider, Form, Grid, Header} from 'semantic-ui-react';
import { API, showError, verifyJSON } from '../helpers'; import {API, showError, verifyJSON} from '../helpers';
const OperationSetting = () => { const OperationSetting = () => {
let [inputs, setInputs] = useState({ let [inputs, setInputs] = useState({
QuotaForNewUser: 0, QuotaForNewUser: 0,
QuotaForInviter: 0, QuotaForInviter: 0,
QuotaForInvitee: 0, QuotaForInvitee: 0,
QuotaRemindThreshold: 0, QuotaRemindThreshold: 0,
PreConsumedQuota: 0, PreConsumedQuota: 0,
ModelRatio: '', ModelRatio: '',
GroupRatio: '', GroupRatio: '',
TopUpLink: '', TopUpLink: '',
ChatLink: '', ChatLink: '',
QuotaPerUnit: 0, QuotaPerUnit: 0,
AutomaticDisableChannelEnabled: '', AutomaticDisableChannelEnabled: '',
ChannelDisableThreshold: 0, ChannelDisableThreshold: 0,
LogConsumeEnabled: '', LogConsumeEnabled: '',
DisplayInCurrencyEnabled: '', DisplayInCurrencyEnabled: '',
DisplayTokenStatEnabled: '', DisplayTokenStatEnabled: '',
ApproximateTokenEnabled: '', ApproximateTokenEnabled: '',
RetryTimes: 0, RetryTimes: 0,
}); StablePrice: 6,
const [originInputs, setOriginInputs] = useState({}); NormalPrice: 1.5,
let [loading, setLoading] = useState(false); BasePrice: 1.5,
const getOptions = async () => {
const res = await API.get('/api/option/');
const { success, message, data } = res.data;
if (success) {
let newInputs = {};
data.forEach((item) => {
if (item.key === 'ModelRatio' || item.key === 'GroupRatio') {
item.value = JSON.stringify(JSON.parse(item.value), null, 2);
}
newInputs[item.key] = item.value;
});
setInputs(newInputs);
setOriginInputs(newInputs);
} else {
showError(message);
}
};
useEffect(() => {
getOptions().then();
}, []);
const updateOption = async (key, value) => {
setLoading(true);
if (key.endsWith('Enabled')) {
value = inputs[key] === 'true' ? 'false' : 'true';
}
const res = await API.put('/api/option/', {
key,
value
}); });
const { success, message } = res.data; const [originInputs, setOriginInputs] = useState({});
if (success) { let [loading, setLoading] = useState(false);
setInputs((inputs) => ({ ...inputs, [key]: value }));
} else {
showError(message);
}
setLoading(false);
};
const handleInputChange = async (e, { name, value }) => { const getOptions = async () => {
if (name.endsWith('Enabled')) { const res = await API.get('/api/option/');
await updateOption(name, value); const {success, message, data} = res.data;
} else { if (success) {
setInputs((inputs) => ({ ...inputs, [name]: value })); let newInputs = {};
} data.forEach((item) => {
}; if (item.key === 'ModelRatio' || item.key === 'GroupRatio') {
item.value = JSON.stringify(JSON.parse(item.value), null, 2);
}
newInputs[item.key] = item.value;
});
setInputs(newInputs);
setOriginInputs(newInputs);
} else {
showError(message);
}
};
const submitConfig = async (group) => { useEffect(() => {
switch (group) { getOptions().then();
case 'monitor': }, []);
if (originInputs['ChannelDisableThreshold'] !== inputs.ChannelDisableThreshold) {
await updateOption('ChannelDisableThreshold', inputs.ChannelDisableThreshold);
}
if (originInputs['QuotaRemindThreshold'] !== inputs.QuotaRemindThreshold) {
await updateOption('QuotaRemindThreshold', inputs.QuotaRemindThreshold);
}
break;
case 'ratio':
if (originInputs['ModelRatio'] !== inputs.ModelRatio) {
if (!verifyJSON(inputs.ModelRatio)) {
showError('模型倍率不是合法的 JSON 字符串');
return;
}
await updateOption('ModelRatio', inputs.ModelRatio);
}
if (originInputs['GroupRatio'] !== inputs.GroupRatio) {
if (!verifyJSON(inputs.GroupRatio)) {
showError('分组倍率不是合法的 JSON 字符串');
return;
}
await updateOption('GroupRatio', inputs.GroupRatio);
}
break;
case 'quota':
if (originInputs['QuotaForNewUser'] !== inputs.QuotaForNewUser) {
await updateOption('QuotaForNewUser', inputs.QuotaForNewUser);
}
if (originInputs['QuotaForInvitee'] !== inputs.QuotaForInvitee) {
await updateOption('QuotaForInvitee', inputs.QuotaForInvitee);
}
if (originInputs['QuotaForInviter'] !== inputs.QuotaForInviter) {
await updateOption('QuotaForInviter', inputs.QuotaForInviter);
}
if (originInputs['PreConsumedQuota'] !== inputs.PreConsumedQuota) {
await updateOption('PreConsumedQuota', inputs.PreConsumedQuota);
}
break;
case 'general':
if (originInputs['TopUpLink'] !== inputs.TopUpLink) {
await updateOption('TopUpLink', inputs.TopUpLink);
}
if (originInputs['ChatLink'] !== inputs.ChatLink) {
await updateOption('ChatLink', inputs.ChatLink);
}
if (originInputs['QuotaPerUnit'] !== inputs.QuotaPerUnit) {
await updateOption('QuotaPerUnit', inputs.QuotaPerUnit);
}
if (originInputs['RetryTimes'] !== inputs.RetryTimes) {
await updateOption('RetryTimes', inputs.RetryTimes);
}
break;
}
};
return ( const updateOption = async (key, value) => {
<Grid columns={1}> setLoading(true);
<Grid.Column> if (key.endsWith('Enabled')) {
<Form loading={loading}> value = inputs[key] === 'true' ? 'false' : 'true';
<Header as='h3'> }
通用设置 const res = await API.put('/api/option/', {
</Header> key,
<Form.Group widths={4}> value
<Form.Input });
label='充值链接' const {success, message} = res.data;
name='TopUpLink' if (success) {
onChange={handleInputChange} setInputs((inputs) => ({...inputs, [key]: value}));
autoComplete='new-password' } else {
value={inputs.TopUpLink} showError(message);
type='link' }
placeholder='例如发卡网站的购买链接' setLoading(false);
/> };
<Form.Input
label='聊天页面链接' const handleInputChange = async (e, {name, value}) => {
name='ChatLink' if (name.endsWith('Enabled')) {
onChange={handleInputChange} await updateOption(name, value);
autoComplete='new-password' } else {
value={inputs.ChatLink} setInputs((inputs) => ({...inputs, [name]: value}));
type='link' }
placeholder='例如 ChatGPT Next Web 的部署地址' };
/>
<Form.Input const submitConfig = async (group) => {
label='单位美元额度' switch (group) {
name='QuotaPerUnit' case 'monitor':
onChange={handleInputChange} if (originInputs['ChannelDisableThreshold'] !== inputs.ChannelDisableThreshold) {
autoComplete='new-password' await updateOption('ChannelDisableThreshold', inputs.ChannelDisableThreshold);
value={inputs.QuotaPerUnit} }
type='number' if (originInputs['QuotaRemindThreshold'] !== inputs.QuotaRemindThreshold) {
step='0.01' await updateOption('QuotaRemindThreshold', inputs.QuotaRemindThreshold);
placeholder='一单位货币能兑换的额度' }
/> break;
<Form.Input case 'stable':
label='失败重试次数' await updateOption('StablePrice', inputs.StablePrice);
name='RetryTimes' await updateOption('NormalPrice', inputs.NormalPrice);
type={'number'} await updateOption('BasePrice', inputs.BasePrice);
step='1' localStorage.setItem('stable_price', inputs.StablePrice);
min='0' localStorage.setItem('normal_price', inputs.NormalPrice);
onChange={handleInputChange} localStorage.setItem('base_price', inputs.BasePrice);
autoComplete='new-password' break;
value={inputs.RetryTimes} case 'ratio':
placeholder='失败重试次数' if (originInputs['ModelRatio'] !== inputs.ModelRatio) {
/> if (!verifyJSON(inputs.ModelRatio)) {
</Form.Group> showError('模型倍率不是合法的 JSON 字符串');
<Form.Group inline> return;
<Form.Checkbox }
checked={inputs.LogConsumeEnabled === 'true'} await updateOption('ModelRatio', inputs.ModelRatio);
label='启用额度消费日志记录' }
name='LogConsumeEnabled' if (originInputs['GroupRatio'] !== inputs.GroupRatio) {
onChange={handleInputChange} if (!verifyJSON(inputs.GroupRatio)) {
/> showError('分组倍率不是合法的 JSON 字符串');
<Form.Checkbox return;
checked={inputs.DisplayInCurrencyEnabled === 'true'} }
label='以货币形式显示额度' await updateOption('GroupRatio', inputs.GroupRatio);
name='DisplayInCurrencyEnabled' }
onChange={handleInputChange} break;
/> case 'quota':
<Form.Checkbox if (originInputs['QuotaForNewUser'] !== inputs.QuotaForNewUser) {
checked={inputs.DisplayTokenStatEnabled === 'true'} await updateOption('QuotaForNewUser', inputs.QuotaForNewUser);
label='Billing 相关 API 显示令牌额度而非用户额度' }
name='DisplayTokenStatEnabled' if (originInputs['QuotaForInvitee'] !== inputs.QuotaForInvitee) {
onChange={handleInputChange} await updateOption('QuotaForInvitee', inputs.QuotaForInvitee);
/> }
<Form.Checkbox if (originInputs['QuotaForInviter'] !== inputs.QuotaForInviter) {
checked={inputs.ApproximateTokenEnabled === 'true'} await updateOption('QuotaForInviter', inputs.QuotaForInviter);
label='使用近似的方式估算 token 数以减少计算量' }
name='ApproximateTokenEnabled' if (originInputs['PreConsumedQuota'] !== inputs.PreConsumedQuota) {
onChange={handleInputChange} await updateOption('PreConsumedQuota', inputs.PreConsumedQuota);
/> }
</Form.Group> break;
<Form.Button onClick={() => { case 'general':
submitConfig('general').then(); if (originInputs['TopUpLink'] !== inputs.TopUpLink) {
}}>保存通用设置</Form.Button> await updateOption('TopUpLink', inputs.TopUpLink);
<Divider /> }
<Header as='h3'> if (originInputs['ChatLink'] !== inputs.ChatLink) {
监控设置 await updateOption('ChatLink', inputs.ChatLink);
</Header> }
<Form.Group widths={3}> if (originInputs['QuotaPerUnit'] !== inputs.QuotaPerUnit) {
<Form.Input await updateOption('QuotaPerUnit', inputs.QuotaPerUnit);
label='最长响应时间' }
name='ChannelDisableThreshold' if (originInputs['RetryTimes'] !== inputs.RetryTimes) {
onChange={handleInputChange} await updateOption('RetryTimes', inputs.RetryTimes);
autoComplete='new-password' }
value={inputs.ChannelDisableThreshold} break;
type='number' }
min='0' };
placeholder='单位秒,当运行通道全部测试时,超过此时间将自动禁用通道'
/> return (
<Form.Input <Grid columns={1}>
label='额度提醒阈值' <Grid.Column>
name='QuotaRemindThreshold' <Form loading={loading}>
onChange={handleInputChange} <Header as='h3'>
autoComplete='new-password' 通用设置
value={inputs.QuotaRemindThreshold} </Header>
type='number' <Form.Group widths={4}>
min='0' <Form.Input
placeholder='低于此额度时将发送邮件提醒用户' label='充值链接'
/> name='TopUpLink'
</Form.Group> onChange={handleInputChange}
<Form.Group inline> autoComplete='new-password'
<Form.Checkbox value={inputs.TopUpLink}
checked={inputs.AutomaticDisableChannelEnabled === 'true'} type='link'
label='失败时自动禁用通道' placeholder='例如发卡网站的购买链接'
name='AutomaticDisableChannelEnabled' />
onChange={handleInputChange} <Form.Input
/> label='聊天页面链接'
</Form.Group> name='ChatLink'
<Form.Button onClick={() => { onChange={handleInputChange}
submitConfig('monitor').then(); autoComplete='new-password'
}}>保存监控设置</Form.Button> value={inputs.ChatLink}
<Divider /> type='link'
<Header as='h3'> placeholder='例如 ChatGPT Next Web 的部署地址'
额度设置 />
</Header> <Form.Input
<Form.Group widths={4}> label='单位美元额度'
<Form.Input name='QuotaPerUnit'
label='新用户初始额度' onChange={handleInputChange}
name='QuotaForNewUser' autoComplete='new-password'
onChange={handleInputChange} value={inputs.QuotaPerUnit}
autoComplete='new-password' type='number'
value={inputs.QuotaForNewUser} step='0.01'
type='number' placeholder='一单位货币能兑换的额度'
min='0' />
placeholder='例如100' <Form.Input
/> label='失败重试次数'
<Form.Input name='RetryTimes'
label='请求预扣费额度' type={'number'}
name='PreConsumedQuota' step='1'
onChange={handleInputChange} min='0'
autoComplete='new-password' onChange={handleInputChange}
value={inputs.PreConsumedQuota} autoComplete='new-password'
type='number' value={inputs.RetryTimes}
min='0' placeholder='失败重试次数'
placeholder='请求结束后多退少补' />
/> </Form.Group>
<Form.Input <Form.Group inline>
label='邀请新用户奖励额度' <Form.Checkbox
name='QuotaForInviter' checked={inputs.LogConsumeEnabled === 'true'}
onChange={handleInputChange} label='启用额度消费日志记录'
autoComplete='new-password' name='LogConsumeEnabled'
value={inputs.QuotaForInviter} onChange={handleInputChange}
type='number' />
min='0' <Form.Checkbox
placeholder='例如2000' checked={inputs.DisplayInCurrencyEnabled === 'true'}
/> label='以货币形式显示额度'
<Form.Input name='DisplayInCurrencyEnabled'
label='新用户使用邀请码奖励额度' onChange={handleInputChange}
name='QuotaForInvitee' />
onChange={handleInputChange} <Form.Checkbox
autoComplete='new-password' checked={inputs.DisplayTokenStatEnabled === 'true'}
value={inputs.QuotaForInvitee} label='Billing 相关 API 显示令牌额度而非用户额度'
type='number' name='DisplayTokenStatEnabled'
min='0' onChange={handleInputChange}
placeholder='例如1000' />
/> <Form.Checkbox
</Form.Group> checked={inputs.ApproximateTokenEnabled === 'true'}
<Form.Button onClick={() => { label='使用近似的方式估算 token 数以减少计算量'
submitConfig('quota').then(); name='ApproximateTokenEnabled'
}}>保存额度设置</Form.Button> onChange={handleInputChange}
<Divider /> />
<Header as='h3'> </Form.Group>
倍率设置 <Form.Button onClick={() => {
</Header> submitConfig('general').then();
<Form.Group widths='equal'> }}>保存通用设置</Form.Button>
<Form.TextArea <Divider/>
label='模型倍率' <Header as='h3'>
name='ModelRatio' 监控设置
onChange={handleInputChange} </Header>
style={{ minHeight: 250, fontFamily: 'JetBrains Mono, Consolas' }} <Form.Group widths={3}>
autoComplete='new-password' <Form.Input
value={inputs.ModelRatio} label='最长响应时间'
placeholder='为一个 JSON 文本,键为模型名称,值为倍率' name='ChannelDisableThreshold'
/> onChange={handleInputChange}
</Form.Group> autoComplete='new-password'
<Form.Group widths='equal'> value={inputs.ChannelDisableThreshold}
<Form.TextArea type='number'
label='分组倍率' min='0'
name='GroupRatio' placeholder='单位秒,当运行通道全部测试时,超过此时间将自动禁用通道'
onChange={handleInputChange} />
style={{ minHeight: 250, fontFamily: 'JetBrains Mono, Consolas' }} <Form.Input
autoComplete='new-password' label='额度提醒阈值'
value={inputs.GroupRatio} name='QuotaRemindThreshold'
placeholder='为一个 JSON 文本,键为分组名称,值为倍率' onChange={handleInputChange}
/> autoComplete='new-password'
</Form.Group> value={inputs.QuotaRemindThreshold}
<Form.Button onClick={() => { type='number'
submitConfig('ratio').then(); min='0'
}}>保存倍率设置</Form.Button> placeholder='低于此额度时将发送邮件提醒用户'
</Form> />
</Grid.Column> </Form.Group>
</Grid> <Form.Group inline>
); <Form.Checkbox
checked={inputs.AutomaticDisableChannelEnabled === 'true'}
label='失败时自动禁用通道'
name='AutomaticDisableChannelEnabled'
onChange={handleInputChange}
/>
</Form.Group>
<Form.Button onClick={() => {
submitConfig('monitor').then();
}}>保存监控设置</Form.Button>
<Divider/>
<Header as='h3'>
通道设置
</Header>
<Form.Group widths={3}>
<Form.Input
label='普通渠道价格'
name='NormalPrice'
onChange={handleInputChange}
autoComplete='new-password'
value={inputs.NormalPrice}
type='number'
// min='1.5'
placeholder='n元/刀'
/>
<Form.Input
label='稳定渠道价格'
name='StablePrice'
onChange={handleInputChange}
autoComplete='new-password'
value={inputs.StablePrice}
type='number'
// min='1.5'
placeholder='n元/刀'
/>
</Form.Group>
<Form.Button onClick={() => {
submitConfig('stable').then();
}}>保存通道设置</Form.Button>
<Divider/>
<Header as='h3'>
额度设置
</Header>
<Form.Group widths={4}>
<Form.Input
label='新用户初始额度'
name='QuotaForNewUser'
onChange={handleInputChange}
autoComplete='new-password'
value={inputs.QuotaForNewUser}
type='number'
min='0'
placeholder='例如100'
/>
<Form.Input
label='请求预扣费额度'
name='PreConsumedQuota'
onChange={handleInputChange}
autoComplete='new-password'
value={inputs.PreConsumedQuota}
type='number'
min='0'
placeholder='请求结束后多退少补'
/>
<Form.Input
label='邀请新用户奖励额度'
name='QuotaForInviter'
onChange={handleInputChange}
autoComplete='new-password'
value={inputs.QuotaForInviter}
type='number'
min='0'
placeholder='例如2000'
/>
<Form.Input
label='新用户使用邀请码奖励额度'
name='QuotaForInvitee'
onChange={handleInputChange}
autoComplete='new-password'
value={inputs.QuotaForInvitee}
type='number'
min='0'
placeholder='例如1000'
/>
</Form.Group>
<Form.Button onClick={() => {
submitConfig('quota').then();
}}>保存额度设置</Form.Button>
<Divider/>
<Header as='h3'>
倍率设置
</Header>
<Form.Group widths='equal'>
<Form.TextArea
label='模型倍率'
name='ModelRatio'
onChange={handleInputChange}
style={{minHeight: 250, fontFamily: 'JetBrains Mono, Consolas'}}
autoComplete='new-password'
value={inputs.ModelRatio}
placeholder='为一个 JSON 文本,键为模型名称,值为倍率'
/>
</Form.Group>
<Form.Group widths='equal'>
<Form.TextArea
label='分组倍率'
name='GroupRatio'
onChange={handleInputChange}
style={{minHeight: 250, fontFamily: 'JetBrains Mono, Consolas'}}
autoComplete='new-password'
value={inputs.GroupRatio}
placeholder='为一个 JSON 文本,键为分组名称,值为倍率'
/>
</Form.Group>
<Form.Button onClick={() => {
submitConfig('ratio').then();
}}>保存倍率设置</Form.Button>
</Form>
</Grid.Column>
</Grid>
)
;
}; };
export default OperationSetting; export default OperationSetting;

View File

@ -1,323 +1,410 @@
import React, { useContext, useEffect, useState } from 'react'; import React, {useContext, useEffect, useState} from 'react';
import { Button, Divider, Form, Header, Image, Message, Modal } from 'semantic-ui-react'; import {Button, Checkbox, Divider, Form, Header, Image, Input, Message, Modal} from 'semantic-ui-react';
import { Link, useNavigate } from 'react-router-dom'; import {Link, useNavigate} from 'react-router-dom';
import { API, copy, showError, showInfo, showNotice, showSuccess } from '../helpers'; import {API, copy, showError, showInfo, showNotice, showSuccess} from '../helpers';
import Turnstile from 'react-turnstile'; import Turnstile from 'react-turnstile';
import { UserContext } from '../context/User'; import {UserContext} from '../context/User';
const PersonalSetting = () => { const PersonalSetting = () => {
const [userState, userDispatch] = useContext(UserContext); const [userState, userDispatch] = useContext(UserContext);
let navigate = useNavigate(); let navigate = useNavigate();
const [inputs, setInputs] = useState({ const [inputs, setInputs] = useState({
wechat_verification_code: '', wechat_verification_code: '',
email_verification_code: '', email_verification_code: '',
email: '', email: '',
self_account_deletion_confirmation: '' self_account_deletion_confirmation: ''
}); });
const [status, setStatus] = useState({}); const [stableMode, setStableMode] = useState({
const [showWeChatBindModal, setShowWeChatBindModal] = useState(false); stableMode: false,
const [showEmailBindModal, setShowEmailBindModal] = useState(false); maxPrice: 7,
const [showAccountDeleteModal, setShowAccountDeleteModal] = useState(false); });
const [turnstileEnabled, setTurnstileEnabled] = useState(false); const [status, setStatus] = useState({});
const [turnstileSiteKey, setTurnstileSiteKey] = useState(''); const [showWeChatBindModal, setShowWeChatBindModal] = useState(false);
const [turnstileToken, setTurnstileToken] = useState(''); const [showEmailBindModal, setShowEmailBindModal] = useState(false);
const [loading, setLoading] = useState(false); const [showAccountDeleteModal, setShowAccountDeleteModal] = useState(false);
const [disableButton, setDisableButton] = useState(false); const [turnstileEnabled, setTurnstileEnabled] = useState(false);
const [countdown, setCountdown] = useState(30); const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
const [turnstileToken, setTurnstileToken] = useState('');
const [loading, setLoading] = useState(false);
const [disableButton, setDisableButton] = useState(false);
const [countdown, setCountdown] = useState(30);
useEffect(() => { // setStableMode(userState.user.stableMode, userState.user.maxPrice);
let status = localStorage.getItem('status'); console.log(userState.user)
if (status) {
status = JSON.parse(status);
setStatus(status);
if (status.turnstile_check) {
setTurnstileEnabled(true);
setTurnstileSiteKey(status.turnstile_site_key);
}
}
}, []);
useEffect(() => {
let countdownInterval = null;
if (disableButton && countdown > 0) {
countdownInterval = setInterval(() => {
setCountdown(countdown - 1);
}, 1000);
} else if (countdown === 0) {
setDisableButton(false);
setCountdown(30);
}
return () => clearInterval(countdownInterval); // Clean up on unmount
}, [disableButton, countdown]);
const handleInputChange = (e, { name, value }) => { useEffect(() => {
setInputs((inputs) => ({ ...inputs, [name]: value })); let status = localStorage.getItem('status');
}; if (status) {
status = JSON.parse(status);
setStatus(status);
if (status.turnstile_check) {
setTurnstileEnabled(true);
setTurnstileSiteKey(status.turnstile_site_key);
}
}
// if (userState.user !== undefined) {
// setStableMode(userState.user.stable_mode, userState.user.max_price);
// }
}, []);
const generateAccessToken = async () => { useEffect(() => {
const res = await API.get('/api/user/token'); let countdownInterval = null;
const { success, message, data } = res.data; if (disableButton && countdown > 0) {
if (success) { countdownInterval = setInterval(() => {
await copy(data); setCountdown(countdown - 1);
showSuccess(`令牌已重置并已复制到剪贴板:${data}`); }, 1000);
} else { } else if (countdown === 0) {
showError(message); setDisableButton(false);
} setCountdown(30);
}; }
return () => clearInterval(countdownInterval); // Clean up on unmount
}, [disableButton, countdown]);
const getAffLink = async () => { useEffect(() => {
const res = await API.get('/api/user/aff'); if (userState.user !== undefined) {
const { success, message, data } = res.data; setStableMode({
if (success) { stableMode: userState.user.stable_mode,
let link = `${window.location.origin}/register?aff=${data}`; maxPrice: userState.user.max_price
await copy(link); })
showNotice(`邀请链接已复制到剪切板:${link}`); // if (stableMode.localMaxPrice !== userState.user.max_price) {
} else { // setStableMode({
showError(message); // localMaxPrice: userState.user.max_price
} // })
}; // }
}
}, [userState]);
const deleteAccount = async () => { const handleInputChange = (e, {name, value}) => {
if (inputs.self_account_deletion_confirmation !== userState.user.username) { setInputs((inputs) => ({...inputs, [name]: value}));
showError('请输入你的账户名以确认删除!'); };
return;
}
const res = await API.delete('/api/user/self'); const generateAccessToken = async () => {
const { success, message } = res.data; const res = await API.get('/api/user/token');
const {success, message, data} = res.data;
if (success) {
await copy(data);
showSuccess(`令牌已重置并已复制到剪贴板:${data}`);
} else {
showError(message);
}
};
if (success) { const getAffLink = async () => {
showSuccess('账户已删除!'); const res = await API.get('/api/user/aff');
await API.get('/api/user/logout'); const {success, message, data} = res.data;
userDispatch({ type: 'logout' }); if (success) {
localStorage.removeItem('user'); let link = `${window.location.origin}/register?aff=${data}`;
navigate('/login'); await copy(link);
} else { showNotice(`邀请链接已复制到剪切板:${link}`);
showError(message); } else {
} showError(message);
}; }
};
const bindWeChat = async () => { const deleteAccount = async () => {
if (inputs.wechat_verification_code === '') return; if (inputs.self_account_deletion_confirmation !== userState.user.username) {
const res = await API.get( showError('请输入你的账户名以确认删除!');
`/api/oauth/wechat/bind?code=${inputs.wechat_verification_code}` return;
); }
const { success, message } = res.data;
if (success) {
showSuccess('微信账户绑定成功!');
setShowWeChatBindModal(false);
} else {
showError(message);
}
};
const openGitHubOAuth = () => { const res = await API.delete('/api/user/self');
window.open( const {success, message} = res.data;
`https://github.com/login/oauth/authorize?client_id=${status.github_client_id}&scope=user:email`
);
};
const sendVerificationCode = async () => { if (success) {
setDisableButton(true); showSuccess('账户已删除!');
if (inputs.email === '') return; await API.get('/api/user/logout');
if (turnstileEnabled && turnstileToken === '') { userDispatch({type: 'logout'});
showInfo('请稍后几秒重试Turnstile 正在检查用户环境!'); localStorage.removeItem('user');
return; navigate('/login');
} } else {
setLoading(true); showError(message);
const res = await API.get( }
`/api/verification?email=${inputs.email}&turnstile=${turnstileToken}` };
);
const { success, message } = res.data;
if (success) {
showSuccess('验证码发送成功,请检查邮箱!');
} else {
showError(message);
}
setLoading(false);
};
const bindEmail = async () => { const bindWeChat = async () => {
if (inputs.email_verification_code === '') return; if (inputs.wechat_verification_code === '') return;
setLoading(true); const res = await API.get(
const res = await API.get( `/api/oauth/wechat/bind?code=${inputs.wechat_verification_code}`
`/api/oauth/email/bind?email=${inputs.email}&code=${inputs.email_verification_code}` );
); const {success, message} = res.data;
const { success, message } = res.data; if (success) {
if (success) { showSuccess('微信账户绑定成功!');
showSuccess('邮箱账户绑定成功!'); setShowWeChatBindModal(false);
setShowEmailBindModal(false); } else {
} else { showError(message);
showError(message); }
} };
setLoading(false);
};
return ( const openGitHubOAuth = () => {
<div style={{ lineHeight: '40px' }}> window.open(
<Header as='h3'>通用设置</Header> `https://github.com/login/oauth/authorize?client_id=${status.github_client_id}&scope=user:email`
<Message> );
注意此处生成的令牌用于系统管理而非用于请求 OpenAI 相关的服务请知悉 };
</Message>
<Button as={Link} to={`/user/edit/`}> const sendVerificationCode = async () => {
更新个人信息 setDisableButton(true);
</Button> if (inputs.email === '') return;
<Button onClick={generateAccessToken}>生成系统访问令牌</Button> if (turnstileEnabled && turnstileToken === '') {
<Button onClick={getAffLink}>复制邀请链接</Button> showInfo('请稍后几秒重试Turnstile 正在检查用户环境!');
<Button onClick={() => { return;
setShowAccountDeleteModal(true); }
}}>删除个人账户</Button> setLoading(true);
<Divider /> const res = await API.get(
<Header as='h3'>账号绑定</Header> `/api/verification?email=${inputs.email}&turnstile=${turnstileToken}`
{ );
status.wechat_login && ( const {success, message} = res.data;
<Button if (success) {
onClick={() => { showSuccess('验证码发送成功,请检查邮箱!');
setShowWeChatBindModal(true); } else {
}} showError(message);
> }
绑定微信账号 setLoading(false);
</Button> };
)
} const bindEmail = async () => {
<Modal if (inputs.email_verification_code === '') return;
onClose={() => setShowWeChatBindModal(false)} setLoading(true);
onOpen={() => setShowWeChatBindModal(true)} const res = await API.get(
open={showWeChatBindModal} `/api/oauth/email/bind?email=${inputs.email}&code=${inputs.email_verification_code}`
size={'mini'} );
> const {success, message} = res.data;
<Modal.Content> if (success) {
<Modal.Description> showSuccess('邮箱账户绑定成功!');
<Image src={status.wechat_qrcode} fluid /> setShowEmailBindModal(false);
<div style={{ textAlign: 'center' }}> } else {
<p> showError(message);
微信扫码关注公众号输入验证码获取验证码三分钟内有效 }
</p> setLoading(false);
</div> };
<Form size='large'>
<Form.Input // const setStableMod = ;
fluid
placeholder='验证码' return (
name='wechat_verification_code' <div style={{lineHeight: '40px'}}>
value={inputs.wechat_verification_code} <Header as='h3'>通用设置</Header>
onChange={handleInputChange} <Message>
/> 注意此处生成的令牌用于系统管理而非用于请求 OpenAI 相关的服务请知悉
<Button color='' fluid size='large' onClick={bindWeChat}> </Message>
绑定 <Button as={Link} to={`/user/edit/`}>
</Button> 更新个人信息
</Button>
<Button onClick={generateAccessToken}>生成系统访问令牌</Button>
<Button onClick={getAffLink}>复制邀请链接</Button>
<Button onClick={() => {
setShowAccountDeleteModal(true);
}}>删除个人账户</Button>
<Divider/>
<Header as='h3'>GPT-4消费设置</Header>
<Form>
<Form.Field>
<Checkbox label="启用稳定模式当低价渠道宕机时自动选择已开启的渠道以保证稳定性仅影响GPT-4"
checked={stableMode.stableMode}
onChange={
(e, data) => {
setStableMode({
...stableMode,
stableMode: data.checked
})
}
}
></Checkbox>
</Form.Field>
<Form.Field
control={Input}
label='最高接受价格n元/刀)'
placeholder='7'
type={'number'}
value={stableMode.maxPrice}
onChange={
(e, data) => {
setStableMode({
...stableMode,
maxPrice: data.value
})
}
}
>
{/*<label></label>*/}
{/*<input placeholder='7' value= {stableMode.maxPrice}/>*/}
</Form.Field>
<Button type='submit' onClick={
async (e, data) => {
if (stableMode.localMaxPrice === '') return;
// console.log(data)
// post to /api/user/set_stable_mode
const res = await API.post(`/api/user/set_stable_mode`, stableMode)
const {success, message} = res.data;
if (success) {
// userDispatch({type: 'stable_mode', payload: stableMode})
userState.user.stable_mode = stableMode.stableMode
userState.user.max_price = stableMode.maxPrice
localStorage.setItem('user', JSON.stringify(userState.user));
showSuccess('设置成功!');
} else {
showError(message);
}
}
}>保存消费设置</Button>
</Form> </Form>
</Modal.Description> {/*<Checkbox label="启用稳定模式(当低价渠道宕机时,自动选择已开启的渠道,以保证稳定性)" onChange={*/}
</Modal.Content> {/* (e, data) => {*/}
</Modal> {/* // if (inputs.email_verification_code === '') return;*/}
{ {/* console.log(data)*/}
status.github_oauth && ( {/* }*/}
<Button onClick={openGitHubOAuth}>绑定 GitHub 账号</Button> {/*}></Checkbox>*/}
) {/*<Input label="最高接受价格n元/刀)" type="integer"></Input>*/}
} <Divider/>
<Button <Header as='h3'>账号绑定</Header>
onClick={() => { {
setShowEmailBindModal(true); status.wechat_login && (
}} <Button
> onClick={() => {
绑定邮箱地址 setShowWeChatBindModal(true);
</Button> }}
<Modal >
onClose={() => setShowEmailBindModal(false)} 绑定微信账号
onOpen={() => setShowEmailBindModal(true)} </Button>
open={showEmailBindModal} )
size={'tiny'} }
style={{ maxWidth: '450px' }} <Modal
> onClose={() => setShowWeChatBindModal(false)}
<Modal.Header>绑定邮箱地址</Modal.Header> onOpen={() => setShowWeChatBindModal(true)}
<Modal.Content> open={showWeChatBindModal}
<Modal.Description> size={'mini'}
<Form size='large'> >
<Form.Input <Modal.Content>
fluid <Modal.Description>
placeholder='输入邮箱地址' <Image src={status.wechat_qrcode} fluid/>
onChange={handleInputChange} <div style={{textAlign: 'center'}}>
name='email' <p>
type='email' 微信扫码关注公众号输入验证码获取验证码三分钟内有效
action={ </p>
<Button onClick={sendVerificationCode} disabled={disableButton || loading}> </div>
{disableButton ? `重新发送(${countdown})` : '获取验证码'} <Form size='large'>
</Button> <Form.Input
} fluid
/> placeholder='验证码'
<Form.Input name='wechat_verification_code'
fluid value={inputs.wechat_verification_code}
placeholder='验证码' onChange={handleInputChange}
name='email_verification_code' />
value={inputs.email_verification_code} <Button color='' fluid size='large' onClick={bindWeChat}>
onChange={handleInputChange} 绑定
/> </Button>
{turnstileEnabled ? ( </Form>
<Turnstile </Modal.Description>
sitekey={turnstileSiteKey} </Modal.Content>
onVerify={(token) => { </Modal>
setTurnstileToken(token); {
}} status.github_oauth && (
/> <Button onClick={openGitHubOAuth}>绑定 GitHub 账号</Button>
) : ( )
<></> }
)} <Button
<Button onClick={() => {
color='' setShowEmailBindModal(true);
fluid }}
size='large' >
onClick={bindEmail} 绑定邮箱地址
loading={loading} </Button>
> <Modal
绑定 onClose={() => setShowEmailBindModal(false)}
</Button> onOpen={() => setShowEmailBindModal(true)}
</Form> open={showEmailBindModal}
</Modal.Description> size={'tiny'}
</Modal.Content> style={{maxWidth: '450px'}}
</Modal> >
<Modal <Modal.Header>绑定邮箱地址</Modal.Header>
onClose={() => setShowAccountDeleteModal(false)} <Modal.Content>
onOpen={() => setShowAccountDeleteModal(true)} <Modal.Description>
open={showAccountDeleteModal} <Form size='large'>
size={'tiny'} <Form.Input
style={{ maxWidth: '450px' }} fluid
> placeholder='输入邮箱地址'
<Modal.Header>确认删除自己的帐户</Modal.Header> onChange={handleInputChange}
<Modal.Content> name='email'
<Modal.Description> type='email'
<Form size='large'> action={
<Form.Input <Button onClick={sendVerificationCode} disabled={disableButton || loading}>
fluid {disableButton ? `重新发送(${countdown})` : '获取验证码'}
placeholder={`输入你的账户名 ${userState?.user?.username} 以确认删除`} </Button>
name='self_account_deletion_confirmation' }
value={inputs.self_account_deletion_confirmation} />
onChange={handleInputChange} <Form.Input
/> fluid
{turnstileEnabled ? ( placeholder='验证码'
<Turnstile name='email_verification_code'
sitekey={turnstileSiteKey} value={inputs.email_verification_code}
onVerify={(token) => { onChange={handleInputChange}
setTurnstileToken(token); />
}} {turnstileEnabled ? (
/> <Turnstile
) : ( sitekey={turnstileSiteKey}
<></> onVerify={(token) => {
)} setTurnstileToken(token);
<Button }}
color='red' />
fluid ) : (
size='large' <></>
onClick={deleteAccount} )}
loading={loading} <Button
> color=''
删除 fluid
</Button> size='large'
</Form> onClick={bindEmail}
</Modal.Description> loading={loading}
</Modal.Content> >
</Modal> 绑定
</div> </Button>
); </Form>
</Modal.Description>
</Modal.Content>
</Modal>
<Modal
onClose={() => setShowAccountDeleteModal(false)}
onOpen={() => setShowAccountDeleteModal(true)}
open={showAccountDeleteModal}
size={'tiny'}
style={{maxWidth: '450px'}}
>
<Modal.Header>确认删除自己的帐户</Modal.Header>
<Modal.Content>
<Modal.Description>
<Form size='large'>
<Form.Input
fluid
placeholder={`输入你的账户名 ${userState?.user?.username} 以确认删除`}
name='self_account_deletion_confirmation'
value={inputs.self_account_deletion_confirmation}
onChange={handleInputChange}
/>
{turnstileEnabled ? (
<Turnstile
sitekey={turnstileSiteKey}
onVerify={(token) => {
setTurnstileToken(token);
}}
/>
) : (
<></>
)}
<Button
color='red'
fluid
size='large'
onClick={deleteAccount}
loading={loading}
>
删除
</Button>
</Form>
</Modal.Description>
</Modal.Content>
</Modal>
</div>
);
}; };
export default PersonalSetting; export default PersonalSetting;

View File

@ -235,21 +235,27 @@ 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 <Popup
size={'small'} trigger={
positive <Button
onClick={async () => { size={'small'}
let key = "sk-" + token.key; positive
if (await copy(key)) { onClick={async () => {
showSuccess('已复制到剪贴板!'); let key = "sk-" + token.key;
} else { if (await copy(key)) {
showWarning('无法复制到剪贴板,请手动复制,已将令牌填入搜索框。'); showSuccess('已复制到剪贴板!');
setSearchKeyword(key); } else {
} showWarning('无法复制到剪贴板,请手动复制,已将令牌填入搜索框。');
}} setSearchKeyword(key);
> }
复制 }}
</Button> >
复制
</Button>
}
on={'hover'}
content={"sk-" + token.key}
/>
<Popup <Popup
trigger={ trigger={
<Button size='small' negative> <Button size='small' negative>

View File

@ -1,132 +1,121 @@
import React, { useContext, useEffect, useState } from 'react'; import React, {useContext, useEffect, useState} from 'react';
import { Card, Grid, Header, Segment } from 'semantic-ui-react'; import {Card, Grid, Header, Segment} from 'semantic-ui-react';
import { API, showError, showNotice, timestamp2string } from '../../helpers'; import {API, showError, showNotice, timestamp2string} from '../../helpers';
import { StatusContext } from '../../context/Status'; import {StatusContext} from '../../context/Status';
import { marked } from 'marked'; import {marked} from 'marked';
const Home = () => { const Home = () => {
const [statusState, statusDispatch] = useContext(StatusContext); const [statusState, statusDispatch] = useContext(StatusContext);
const [homePageContentLoaded, setHomePageContentLoaded] = useState(false); const [homePageContentLoaded, setHomePageContentLoaded] = useState(false);
const [homePageContent, setHomePageContent] = useState(''); const [homePageContent, setHomePageContent] = useState('');
const displayNotice = async () => { const displayNotice = async () => {
const res = await API.get('/api/notice'); const res = await API.get('/api/notice');
const { success, message, data } = res.data; const {success, message, data} = res.data;
if (success) { if (success) {
let oldNotice = localStorage.getItem('notice'); let oldNotice = localStorage.getItem('notice');
if (data !== oldNotice && data !== '') { if (data !== oldNotice && data !== '') {
showNotice(data); showNotice(data);
localStorage.setItem('notice', data); localStorage.setItem('notice', data);
} }
} else { } else {
showError(message); showError(message);
} }
}; };
const displayHomePageContent = async () => { const displayHomePageContent = async () => {
setHomePageContent(localStorage.getItem('home_page_content') || ''); setHomePageContent(localStorage.getItem('home_page_content') || '');
const res = await API.get('/api/home_page_content'); const res = await API.get('/api/home_page_content');
const { success, message, data } = res.data; const {success, message, data} = res.data;
if (success) { if (success) {
let content = data; let content = data;
if (!data.startsWith('https://')) { if (!data.startsWith('https://')) {
content = marked.parse(data); content = marked.parse(data);
} }
setHomePageContent(content); setHomePageContent(content);
localStorage.setItem('home_page_content', content); localStorage.setItem('home_page_content', content);
} else { } else {
showError(message); showError(message);
setHomePageContent('加载首页内容失败...'); setHomePageContent('加载首页内容失败...');
} }
setHomePageContentLoaded(true); setHomePageContentLoaded(true);
}; };
const getStartTimeString = () => { const getStartTimeString = () => {
const timestamp = statusState?.status?.start_time; const timestamp = statusState?.status?.start_time;
return timestamp2string(timestamp); return timestamp2string(timestamp);
}; };
useEffect(() => {
displayNotice().then();
displayHomePageContent().then();
}, []);
return (
<>
{
// homePageContentLoaded && homePageContent === '' ?
<>
<Segment>
<Header as='h3'>当前状态</Header>
<Grid columns={2} stackable>
<Grid.Column>
<Card fluid>
<Card.Content>
<Card.Header>GPT-3.5</Card.Header>
<Card.Meta>信息总览</Card.Meta>
<Card.Description>
<p>通道官方通道</p>
<p>状态存活</p>
<p>价格{statusState?.status?.base_price}R&nbsp;/&nbsp;</p>
</Card.Description>
</Card.Content>
</Card>
</Grid.Column>
<Grid.Column>
<Card fluid>
<Card.Content>
<Card.Header>GPT-4</Card.Header>
<Card.Meta>信息总览</Card.Meta>
<Card.Description>
<p>通道官方通道低价通道</p>
<p>
状态
{statusState?.status?.stable_price===-1?
<span style={{color:'red'}}>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>
:
<span style={{color:'green'}}>&emsp;&emsp;</span>
}
{statusState?.status?.normal_price===-1?
<span style={{color:'red'}}>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>
:
<span style={{color:'green'}}>&emsp;&emsp;</span>
}
</p>
<p>
价格{statusState?.status?.stable_price}R&nbsp;/&nbsp;刀|{statusState?.status?.normal_price}R&nbsp;/&nbsp;
</p>
</Card.Description>
</Card.Content>
</Card>
</Grid.Column>
</Grid>
</Segment>
{
homePageContent.startsWith('https://') ? <iframe
src={homePageContent}
style={{ width: '100%', height: '100vh', border: 'none' }}
/> : <div style={{ fontSize: 'larger' }} dangerouslySetInnerHTML={{ __html: homePageContent }}></div>
}
</>
// :
// <>
// </>
}
useEffect(() => {
displayNotice().then();
displayHomePageContent().then();
}, []);
return (
<>
{
homePageContentLoaded && homePageContent === '' ? <>
<Segment>
<Header as='h3'>系统状况</Header>
<Grid columns={2} stackable>
<Grid.Column>
<Card fluid>
<Card.Content>
<Card.Header>系统信息</Card.Header>
<Card.Meta>系统信息总览</Card.Meta>
<Card.Description>
<p>名称{statusState?.status?.system_name}</p>
<p>版本{statusState?.status?.version}</p>
<p>
源码
<a
href='https://github.com/songquanpeng/one-api'
target='_blank'
>
https://github.com/songquanpeng/one-api
</a>
</p>
<p>启动时间{getStartTimeString()}</p>
</Card.Description>
</Card.Content>
</Card>
</Grid.Column>
<Grid.Column>
<Card fluid>
<Card.Content>
<Card.Header>系统配置</Card.Header>
<Card.Meta>系统配置总览</Card.Meta>
<Card.Description>
<p>
邮箱验证
{statusState?.status?.email_verification === true
? '已启用'
: '未启用'}
</p>
<p>
GitHub 身份验证
{statusState?.status?.github_oauth === true
? '已启用'
: '未启用'}
</p>
<p>
微信身份验证
{statusState?.status?.wechat_login === true
? '已启用'
: '未启用'}
</p>
<p>
Turnstile 用户校验
{statusState?.status?.turnstile_check === true
? '已启用'
: '未启用'}
</p>
</Card.Description>
</Card.Content>
</Card>
</Grid.Column>
</Grid>
</Segment>
</> : <>
{
homePageContent.startsWith('https://') ? <iframe
src={homePageContent}
style={{ width: '100%', height: '100vh', border: 'none' }}
/> : <div style={{ fontSize: 'larger' }} dangerouslySetInnerHTML={{ __html: homePageContent }}></div>
}
</> </>
} );
</>
);
}; };
export default Home; export default Home;

View File

@ -0,0 +1,11 @@
import React from 'react';
import { Header, Segment } from 'semantic-ui-react';
import MjLogsTable from '../../components/MjLogsTable';
const Midjourney = () => (
<>
<MjLogsTable />
</>
);
export default Midjourney;

View File

@ -1,103 +1,236 @@
import React, { useEffect, useState } from 'react'; import React, {useEffect, useState} from 'react';
import { Button, Form, Grid, Header, Segment, Statistic } from 'semantic-ui-react'; import {Button, Form, Grid, Header, Segment, Statistic} from 'semantic-ui-react';
import { API, showError, showInfo, showSuccess } from '../../helpers'; import {API, showError, showInfo, showSuccess} from '../../helpers';
import { renderQuota } from '../../helpers/render'; import {renderNumber, renderQuota} from '../../helpers/render';
const TopUp = () => { const TopUp = () => {
const [redemptionCode, setRedemptionCode] = useState(''); const [redemptionCode, setRedemptionCode] = useState('');
const [topUpLink, setTopUpLink] = useState(''); const [topUpCode, setTopUpCode] = useState('');
const [userQuota, setUserQuota] = useState(0); const [topUpCount, setTopUpCount] = useState(10);
const [isSubmitting, setIsSubmitting] = useState(false); const [amount, setAmount] = useState(0);
const [topUpLink, setTopUpLink] = useState('');
const [userQuota, setUserQuota] = useState(0);
const [isSubmitting, setIsSubmitting] = useState(false);
const topUp = async () => { const topUp = async () => {
if (redemptionCode === '') { if (redemptionCode === '') {
showInfo('请输入充值码!') showInfo('请输入充值码!')
return; return;
} }
setIsSubmitting(true); setIsSubmitting(true);
try { try {
const res = await API.post('/api/user/topup', { const res = await API.post('/api/user/topup', {
key: redemptionCode key: redemptionCode
}); });
const { success, message, data } = res.data; const {success, message, data} = res.data;
if (success) { if (success) {
showSuccess('充值成功!'); showSuccess('充值成功!');
setUserQuota((quota) => { setUserQuota((quota) => {
return quota + data; return quota + data;
}); });
setRedemptionCode(''); setRedemptionCode('');
} else { } else {
showError(message); showError(message);
} }
} catch (err) { } catch (err) {
showError('请求失败'); showError('请求失败');
} finally { } finally {
setIsSubmitting(false); setIsSubmitting(false);
} }
}; };
const openTopUpLink = () => { const openTopUpLink = () => {
if (!topUpLink) { if (!topUpLink) {
showError('超级管理员未设置充值链接!'); showError('超级管理员未设置充值链接!');
return; return;
} }
window.open(topUpLink, '_blank'); window.open(topUpLink, '_blank');
}; };
const getUserQuota = async ()=>{ const onlineTopUp = async (payment) => {
let res = await API.get(`/api/user/self`); if (amount === 0) {
const {success, message, data} = res.data; await getAmount();
if (success) { }
setUserQuota(data.quota); try {
} else { const res = await API.post('/api/user/pay', {
showError(message); amount: parseInt(topUpCount),
top_up_code: topUpCode,
PaymentMethod: payment
});
if (res !== undefined) {
const {message, data} = res.data;
// showInfo(message);
if (message === 'success') {
let params = data
let url = res.data.url
let form = document.createElement('form')
form.action = url
form.method = 'POST'
form.target = '_blank'
for (let key in params) {
let input = document.createElement('input')
input.type = 'hidden'
input.name = key
input.value = params[key]
form.appendChild(input)
}
document.body.appendChild(form)
form.submit()
document.body.removeChild(form)
} else {
showError(message);
// setTopUpCount(parseInt(res.data.count));
setAmount(parseInt(data));
}
} else {
showError(res);
}
} catch (err) {
console.log(err);
} finally {
}
} }
}
useEffect(() => { const getUserQuota = async () => {
let status = localStorage.getItem('status'); let res = await API.get(`/api/user/self`);
if (status) { const {success, message, data} = res.data;
status = JSON.parse(status); if (success) {
if (status.top_up_link) { setUserQuota(data.quota);
setTopUpLink(status.top_up_link); } else {
} showError(message);
}
} }
getUserQuota().then();
}, []);
return ( useEffect(() => {
<Segment> let status = localStorage.getItem('status');
<Header as='h3'>充值额度</Header> if (status) {
<Grid columns={2} stackable> status = JSON.parse(status);
<Grid.Column> if (status.top_up_link) {
<Form> setTopUpLink(status.top_up_link);
<Form.Input }
placeholder='兑换码' }
name='redemptionCode' getUserQuota().then();
value={redemptionCode} }, []);
onChange={(e) => {
setRedemptionCode(e.target.value); const renderAmount = () => {
}} console.log(amount);
/> return amount + '元';
<Button color='green' onClick={openTopUpLink}> }
获取兑换码
</Button> const getAmount = async (value) => {
<Button color='yellow' onClick={topUp} disabled={isSubmitting}> if (value === undefined) {
{isSubmitting ? '兑换中...' : '兑换'} value = topUpCount;
</Button> }
</Form> try {
</Grid.Column> const res = await API.post('/api/user/amount', {
<Grid.Column> amount: parseFloat(value),
<Statistic.Group widths='one'> top_up_code: topUpCode
<Statistic> });
<Statistic.Value>{renderQuota(userQuota)}</Statistic.Value> if (res !== undefined) {
<Statistic.Label>剩余额度</Statistic.Label> const {message, data} = res.data;
</Statistic> // showInfo(message);
</Statistic.Group> if (message === 'success') {
</Grid.Column> setAmount(parseInt(data));
</Grid> } else {
</Segment> showError(message);
); // setTopUpCount(parseInt(res.data.count));
setAmount(parseInt(data));
}
} else {
showError(res);
}
} catch (err) {
console.log(err);
} finally {
}
}
return (
<div>
<Segment>
<Header as='h3'>充值额度</Header>
<Grid columns={2} stackable>
<Grid.Column>
<Form>
<Form.Input
placeholder='兑换码'
name='redemptionCode'
value={redemptionCode}
onChange={(e) => {
setRedemptionCode(e.target.value);
}}
/>
{/*<Button color='green' onClick={openTopUpLink}>*/}
{/* 获取兑换码*/}
{/*</Button>*/}
<Button color='yellow' onClick={topUp} disabled={isSubmitting}>
{isSubmitting ? '兑换中...' : '兑换'}
</Button>
</Form>
</Grid.Column>
<Grid.Column>
<Statistic.Group widths='one'>
<Statistic>
<Statistic.Value>{renderQuota(userQuota)}</Statistic.Value>
<Statistic.Label>剩余额度</Statistic.Label>
</Statistic>
</Statistic.Group>
</Grid.Column>
</Grid>
</Segment>
<Segment>
<Header as='h3'>在线充值最小10刀</Header>
<Grid columns={2} stackable>
<Grid.Column>
<Form>
<Form.Input
placeholder='充值金额最低10,最高400'
name='redemptionCount'
type={'number'}
value={topUpCount}
autoComplete={'off'}
onChange={async (e) => {
setTopUpCount(e.target.value);
await getAmount(e.target.value);
}}
/>
<Form.Input
placeholder='充值码,如果你没有充值码,可不填写'
name='redemptionCount'
value={topUpCode}
onChange={(e) => {
setTopUpCode(e.target.value);
}}
/>
<Button color='blue' onClick={
async () => {
onlineTopUp('zfb')
}
}>
支付宝最大400元
</Button>
<Button color='green' onClick={
async () => {
onlineTopUp('wx')
}
}>
微信最大600元
</Button>
</Form>
</Grid.Column>
<Grid.Column>
<Statistic.Group widths='one'>
<Statistic>
<Statistic.Value>{renderAmount()}</Statistic.Value>
<Statistic.Label>支付金额</Statistic.Label>
</Statistic>
</Statistic.Group>
</Grid.Column>
</Grid>
</Segment>
</div>
);
}; };
export default TopUp; export default TopUp;