mirror of
				https://github.com/songquanpeng/one-api.git
				synced 2025-11-04 15:53:42 +08:00 
			
		
		
		
	Compare commits
	
		
			56 Commits
		
	
	
		
			v0.6.10-al
			...
			v0.6.11-al
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					c351e196e6 | ||
| 
						 | 
					a316ed7abc | ||
| 
						 | 
					0895d8660e | ||
| 
						 | 
					be1ed114f4 | ||
| 
						 | 
					eb6da573a3 | ||
| 
						 | 
					0a6273fc08 | ||
| 
						 | 
					5997fce454 | ||
| 
						 | 
					0df6d7a131 | ||
| 
						 | 
					93fdb60de5 | ||
| 
						 | 
					4db834da95 | ||
| 
						 | 
					6818ed5ca8 | ||
| 
						 | 
					7be3b5547d | ||
| 
						 | 
					2d7ea61d67 | ||
| 
						 | 
					83b34be067 | ||
| 
						 | 
					d5d879afdc | ||
| 
						 | 
					0f205a3aa3 | ||
| 
						 | 
					76c3f87351 | ||
| 
						 | 
					6d9a92f8f7 | ||
| 
						 | 
					835f0e0d67 | ||
| 
						 | 
					a6981f0d51 | ||
| 
						 | 
					678d613179 | ||
| 
						 | 
					be089a072b | ||
| 
						 | 
					45d10aa3df | ||
| 
						 | 
					9cdd48ac22 | ||
| 
						 | 
					310e7120e5 | ||
| 
						 | 
					3d29713268 | ||
| 
						 | 
					f2c7c424e9 | ||
| 
						 | 
					38a42bb265 | ||
| 
						 | 
					fa2e8f44b1 | ||
| 
						 | 
					9f74101543 | ||
| 
						 | 
					28a271a896 | ||
| 
						 | 
					e8ea87fff3 | ||
| 
						 | 
					abe2d2dba8 | ||
| 
						 | 
					4bcaa064d6 | ||
| 
						 | 
					52d81e0e24 | ||
| 
						 | 
					dc8c3bc69e | ||
| 
						 | 
					b4e69df802 | ||
| 
						 | 
					d9f74bdff3 | ||
| 
						 | 
					fa2a772731 | ||
| 
						 | 
					4f68f3e1b3 | ||
| 
						 | 
					0bab887b2d | ||
| 
						 | 
					0230d36643 | ||
| 
						 | 
					bad57d049a | ||
| 
						 | 
					dc470ce82e | ||
| 
						 | 
					ea0721d525 | ||
| 
						 | 
					d0402f9086 | ||
| 
						 | 
					1fead8e7f7 | ||
| 
						 | 
					09911a301d | ||
| 
						 | 
					f95e6b78b8 | ||
| 
						 | 
					605bb06667 | ||
| 
						 | 
					d88e07fd9a | ||
| 
						 | 
					3915ce9814 | ||
| 
						 | 
					999defc88b | ||
| 
						 | 
					b51c47bc77 | ||
| 
						 | 
					4f25cde132 | ||
| 
						 | 
					d89e9d7e44 | 
							
								
								
									
										2
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							@@ -12,8 +12,6 @@ name: CI
 | 
			
		||||
# would trigger our jobs twice on pull requests (once from "push" event and once
 | 
			
		||||
# from "pull_request->synchronize")
 | 
			
		||||
on:
 | 
			
		||||
  pull_request:
 | 
			
		||||
    types: [opened, reopened, synchronize]
 | 
			
		||||
  push:
 | 
			
		||||
    branches:
 | 
			
		||||
      - 'main'
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										19
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										19
									
								
								Dockerfile
									
									
									
									
									
								
							@@ -4,21 +4,20 @@ WORKDIR /web
 | 
			
		||||
COPY ./VERSION .
 | 
			
		||||
COPY ./web .
 | 
			
		||||
 | 
			
		||||
WORKDIR /web/default
 | 
			
		||||
RUN npm install
 | 
			
		||||
RUN DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat VERSION) npm run build
 | 
			
		||||
RUN npm install --prefix /web/default & \
 | 
			
		||||
    npm install --prefix /web/berry & \
 | 
			
		||||
    npm install --prefix /web/air & \
 | 
			
		||||
    wait
 | 
			
		||||
 | 
			
		||||
WORKDIR /web/berry
 | 
			
		||||
RUN npm install
 | 
			
		||||
RUN DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat VERSION) npm run build
 | 
			
		||||
 | 
			
		||||
WORKDIR /web/air
 | 
			
		||||
RUN npm install
 | 
			
		||||
RUN DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat VERSION) npm run build
 | 
			
		||||
RUN DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat /web/default/VERSION) npm run build --prefix /web/default & \
 | 
			
		||||
    DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat /web/berry/VERSION) npm run build --prefix /web/berry & \
 | 
			
		||||
    DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat /web/air/VERSION) npm run build --prefix /web/air & \
 | 
			
		||||
    wait
 | 
			
		||||
 | 
			
		||||
FROM golang:alpine AS builder2
 | 
			
		||||
 | 
			
		||||
RUN apk add --no-cache g++
 | 
			
		||||
RUN apk add --no-cache gcc musl-dev libc-dev sqlite-dev build-base
 | 
			
		||||
 | 
			
		||||
ENV GO111MODULE=on \
 | 
			
		||||
    CGO_ENABLED=1 \
 | 
			
		||||
 
 | 
			
		||||
@@ -410,6 +410,7 @@ graph LR
 | 
			
		||||
27. `INITIAL_ROOT_TOKEN`:如果设置了该值,则在系统首次启动时会自动创建一个值为该环境变量值的 root 用户令牌。
 | 
			
		||||
28. `INITIAL_ROOT_ACCESS_TOKEN`:如果设置了该值,则在系统首次启动时会自动创建一个值为该环境变量的 root 用户创建系统管理令牌。
 | 
			
		||||
29. `ENFORCE_INCLUDE_USAGE`:是否强制在 stream 模型下返回 usage,默认不开启,可选值为 `true` 和 `false`。
 | 
			
		||||
30. `TEST_PROMPT`:测试模型时的用户 prompt,默认为 `Print your model name exactly and do not output without any other text.`。
 | 
			
		||||
 | 
			
		||||
### 命令行参数
 | 
			
		||||
1. `--port <port_number>`: 指定服务器监听的端口号,默认为 `3000`。
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,14 @@
 | 
			
		||||
package config
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"github.com/songquanpeng/one-api/common/env"
 | 
			
		||||
	"os"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"sync"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/songquanpeng/one-api/common/env"
 | 
			
		||||
 | 
			
		||||
	"github.com/google/uuid"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -162,3 +163,4 @@ var UserContentRequestProxy = env.String("USER_CONTENT_REQUEST_PROXY", "")
 | 
			
		||||
var UserContentRequestTimeout = env.Int("USER_CONTENT_REQUEST_TIMEOUT", 30)
 | 
			
		||||
 | 
			
		||||
var EnforceIncludeUsage = env.Bool("ENFORCE_INCLUDE_USAGE", false)
 | 
			
		||||
var TestPrompt = env.String("TEST_PROMPT", "Print your model name exactly and do not output without any other text.")
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,8 @@
 | 
			
		||||
package helper
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"github.com/songquanpeng/one-api/common/random"
 | 
			
		||||
	"html/template"
 | 
			
		||||
	"log"
 | 
			
		||||
	"net"
 | 
			
		||||
@@ -11,6 +10,10 @@ import (
 | 
			
		||||
	"runtime"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
 | 
			
		||||
	"github.com/songquanpeng/one-api/common/random"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func OpenBrowser(url string) {
 | 
			
		||||
@@ -106,6 +109,18 @@ func GenRequestID() string {
 | 
			
		||||
	return GetTimeString() + random.GetRandomNumberString(8)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func SetRequestID(ctx context.Context, id string) context.Context {
 | 
			
		||||
	return context.WithValue(ctx, RequestIdKey, id)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetRequestID(ctx context.Context) string {
 | 
			
		||||
	rawRequestId := ctx.Value(RequestIdKey)
 | 
			
		||||
	if rawRequestId == nil {
 | 
			
		||||
		return ""
 | 
			
		||||
	}
 | 
			
		||||
	return rawRequestId.(string)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetResponseID(c *gin.Context) string {
 | 
			
		||||
	logID := c.GetString(RequestIdKey)
 | 
			
		||||
	return fmt.Sprintf("chatcmpl-%s", logID)
 | 
			
		||||
 
 | 
			
		||||
@@ -13,3 +13,8 @@ func GetTimeString() string {
 | 
			
		||||
	now := time.Now()
 | 
			
		||||
	return fmt.Sprintf("%s%d", now.Format("20060102150405"), now.UnixNano()%1e9)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CalcElapsedTime return the elapsed time in milliseconds (ms)
 | 
			
		||||
func CalcElapsedTime(start time.Time) int64 {
 | 
			
		||||
	return time.Now().Sub(start).Milliseconds()
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -7,19 +7,25 @@ import (
 | 
			
		||||
	"log"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"runtime"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"sync"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
 | 
			
		||||
	"github.com/songquanpeng/one-api/common/config"
 | 
			
		||||
	"github.com/songquanpeng/one-api/common/helper"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type loggerLevel string
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	loggerDEBUG = "DEBUG"
 | 
			
		||||
	loggerINFO  = "INFO"
 | 
			
		||||
	loggerWarn  = "WARN"
 | 
			
		||||
	loggerError = "ERR"
 | 
			
		||||
	loggerDEBUG loggerLevel = "DEBUG"
 | 
			
		||||
	loggerINFO  loggerLevel = "INFO"
 | 
			
		||||
	loggerWarn  loggerLevel = "WARN"
 | 
			
		||||
	loggerError loggerLevel = "ERROR"
 | 
			
		||||
	loggerFatal loggerLevel = "FATAL"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var setupLogOnce sync.Once
 | 
			
		||||
@@ -44,27 +50,26 @@ func SetupLogger() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func SysLog(s string) {
 | 
			
		||||
	t := time.Now()
 | 
			
		||||
	_, _ = fmt.Fprintf(gin.DefaultWriter, "[SYS] %v | %s \n", t.Format("2006/01/02 - 15:04:05"), s)
 | 
			
		||||
	logHelper(nil, loggerINFO, s)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func SysLogf(format string, a ...any) {
 | 
			
		||||
	SysLog(fmt.Sprintf(format, a...))
 | 
			
		||||
	logHelper(nil, loggerINFO, fmt.Sprintf(format, a...))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func SysError(s string) {
 | 
			
		||||
	t := time.Now()
 | 
			
		||||
	_, _ = fmt.Fprintf(gin.DefaultErrorWriter, "[SYS] %v | %s \n", t.Format("2006/01/02 - 15:04:05"), s)
 | 
			
		||||
	logHelper(nil, loggerError, s)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func SysErrorf(format string, a ...any) {
 | 
			
		||||
	SysError(fmt.Sprintf(format, a...))
 | 
			
		||||
	logHelper(nil, loggerError, fmt.Sprintf(format, a...))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Debug(ctx context.Context, msg string) {
 | 
			
		||||
	if config.DebugEnabled {
 | 
			
		||||
		logHelper(ctx, loggerDEBUG, msg)
 | 
			
		||||
	if !config.DebugEnabled {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	logHelper(ctx, loggerDEBUG, msg)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Info(ctx context.Context, msg string) {
 | 
			
		||||
@@ -80,37 +85,65 @@ func Error(ctx context.Context, msg string) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Debugf(ctx context.Context, format string, a ...any) {
 | 
			
		||||
	Debug(ctx, fmt.Sprintf(format, a...))
 | 
			
		||||
	logHelper(ctx, loggerDEBUG, fmt.Sprintf(format, a...))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Infof(ctx context.Context, format string, a ...any) {
 | 
			
		||||
	Info(ctx, fmt.Sprintf(format, a...))
 | 
			
		||||
	logHelper(ctx, loggerINFO, fmt.Sprintf(format, a...))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Warnf(ctx context.Context, format string, a ...any) {
 | 
			
		||||
	Warn(ctx, fmt.Sprintf(format, a...))
 | 
			
		||||
	logHelper(ctx, loggerWarn, fmt.Sprintf(format, a...))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Errorf(ctx context.Context, format string, a ...any) {
 | 
			
		||||
	Error(ctx, fmt.Sprintf(format, a...))
 | 
			
		||||
	logHelper(ctx, loggerError, fmt.Sprintf(format, a...))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func logHelper(ctx context.Context, level string, msg string) {
 | 
			
		||||
func FatalLog(s string) {
 | 
			
		||||
	logHelper(nil, loggerFatal, s)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func FatalLogf(format string, a ...any) {
 | 
			
		||||
	logHelper(nil, loggerFatal, fmt.Sprintf(format, a...))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func logHelper(ctx context.Context, level loggerLevel, msg string) {
 | 
			
		||||
	writer := gin.DefaultErrorWriter
 | 
			
		||||
	if level == loggerINFO {
 | 
			
		||||
		writer = gin.DefaultWriter
 | 
			
		||||
	}
 | 
			
		||||
	id := ctx.Value(helper.RequestIdKey)
 | 
			
		||||
	if id == nil {
 | 
			
		||||
		id = helper.GenRequestID()
 | 
			
		||||
	var requestId string
 | 
			
		||||
	if ctx != nil {
 | 
			
		||||
		rawRequestId := helper.GetRequestID(ctx)
 | 
			
		||||
		if rawRequestId != "" {
 | 
			
		||||
			requestId = fmt.Sprintf(" | %s", rawRequestId)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	lineInfo, funcName := getLineInfo()
 | 
			
		||||
	now := time.Now()
 | 
			
		||||
	_, _ = fmt.Fprintf(writer, "[%s] %v | %s | %s \n", level, now.Format("2006/01/02 - 15:04:05"), id, msg)
 | 
			
		||||
	_, _ = fmt.Fprintf(writer, "[%s] %v%s%s %s%s \n", level, now.Format("2006/01/02 - 15:04:05"), requestId, lineInfo, funcName, msg)
 | 
			
		||||
	SetupLogger()
 | 
			
		||||
	if level == loggerFatal {
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func FatalLog(v ...any) {
 | 
			
		||||
	t := time.Now()
 | 
			
		||||
	_, _ = fmt.Fprintf(gin.DefaultErrorWriter, "[FATAL] %v | %v \n", t.Format("2006/01/02 - 15:04:05"), v)
 | 
			
		||||
	os.Exit(1)
 | 
			
		||||
func getLineInfo() (string, string) {
 | 
			
		||||
	funcName := "[unknown] "
 | 
			
		||||
	pc, file, line, ok := runtime.Caller(3)
 | 
			
		||||
	if ok {
 | 
			
		||||
		if fn := runtime.FuncForPC(pc); fn != nil {
 | 
			
		||||
			parts := strings.Split(fn.Name(), ".")
 | 
			
		||||
			funcName = "[" + parts[len(parts)-1] + "] "
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		file = "unknown"
 | 
			
		||||
		line = 0
 | 
			
		||||
	}
 | 
			
		||||
	parts := strings.Split(file, "one-api/")
 | 
			
		||||
	if len(parts) > 1 {
 | 
			
		||||
		file = parts[1]
 | 
			
		||||
	}
 | 
			
		||||
	return fmt.Sprintf(" | %s:%d", file, line), funcName
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,16 +5,18 @@ import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-contrib/sessions"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
 | 
			
		||||
	"github.com/songquanpeng/one-api/common/config"
 | 
			
		||||
	"github.com/songquanpeng/one-api/common/logger"
 | 
			
		||||
	"github.com/songquanpeng/one-api/common/random"
 | 
			
		||||
	"github.com/songquanpeng/one-api/controller"
 | 
			
		||||
	"github.com/songquanpeng/one-api/model"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type GitHubOAuthResponse struct {
 | 
			
		||||
@@ -81,6 +83,7 @@ func getGitHubUserInfoByCode(code string) (*GitHubUser, error) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GitHubOAuth(c *gin.Context) {
 | 
			
		||||
	ctx := c.Request.Context()
 | 
			
		||||
	session := sessions.Default(c)
 | 
			
		||||
	state := c.Query("state")
 | 
			
		||||
	if state == "" || session.Get("oauth_state") == nil || state != session.Get("oauth_state").(string) {
 | 
			
		||||
@@ -136,7 +139,7 @@ func GitHubOAuth(c *gin.Context) {
 | 
			
		||||
			user.Role = model.RoleCommonUser
 | 
			
		||||
			user.Status = model.UserStatusEnabled
 | 
			
		||||
 | 
			
		||||
			if err := user.Insert(0); err != nil {
 | 
			
		||||
			if err := user.Insert(ctx, 0); err != nil {
 | 
			
		||||
				c.JSON(http.StatusOK, gin.H{
 | 
			
		||||
					"success": false,
 | 
			
		||||
					"message": err.Error(),
 | 
			
		||||
 
 | 
			
		||||
@@ -5,15 +5,17 @@ import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-contrib/sessions"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
 | 
			
		||||
	"github.com/songquanpeng/one-api/common/config"
 | 
			
		||||
	"github.com/songquanpeng/one-api/common/logger"
 | 
			
		||||
	"github.com/songquanpeng/one-api/controller"
 | 
			
		||||
	"github.com/songquanpeng/one-api/model"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type LarkOAuthResponse struct {
 | 
			
		||||
@@ -79,6 +81,7 @@ func getLarkUserInfoByCode(code string) (*LarkUser, error) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func LarkOAuth(c *gin.Context) {
 | 
			
		||||
	ctx := c.Request.Context()
 | 
			
		||||
	session := sessions.Default(c)
 | 
			
		||||
	state := c.Query("state")
 | 
			
		||||
	if state == "" || session.Get("oauth_state") == nil || state != session.Get("oauth_state").(string) {
 | 
			
		||||
@@ -125,7 +128,7 @@ func LarkOAuth(c *gin.Context) {
 | 
			
		||||
			user.Role = model.RoleCommonUser
 | 
			
		||||
			user.Status = model.UserStatusEnabled
 | 
			
		||||
 | 
			
		||||
			if err := user.Insert(0); err != nil {
 | 
			
		||||
			if err := user.Insert(ctx, 0); err != nil {
 | 
			
		||||
				c.JSON(http.StatusOK, gin.H{
 | 
			
		||||
					"success": false,
 | 
			
		||||
					"message": err.Error(),
 | 
			
		||||
 
 | 
			
		||||
@@ -5,15 +5,17 @@ import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-contrib/sessions"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
 | 
			
		||||
	"github.com/songquanpeng/one-api/common/config"
 | 
			
		||||
	"github.com/songquanpeng/one-api/common/logger"
 | 
			
		||||
	"github.com/songquanpeng/one-api/controller"
 | 
			
		||||
	"github.com/songquanpeng/one-api/model"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type OidcResponse struct {
 | 
			
		||||
@@ -87,6 +89,7 @@ func getOidcUserInfoByCode(code string) (*OidcUser, error) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func OidcAuth(c *gin.Context) {
 | 
			
		||||
	ctx := c.Request.Context()
 | 
			
		||||
	session := sessions.Default(c)
 | 
			
		||||
	state := c.Query("state")
 | 
			
		||||
	if state == "" || session.Get("oauth_state") == nil || state != session.Get("oauth_state").(string) {
 | 
			
		||||
@@ -142,7 +145,7 @@ func OidcAuth(c *gin.Context) {
 | 
			
		||||
			} else {
 | 
			
		||||
				user.DisplayName = "OIDC User"
 | 
			
		||||
			}
 | 
			
		||||
			err := user.Insert(0)
 | 
			
		||||
			err := user.Insert(ctx, 0)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				c.JSON(http.StatusOK, gin.H{
 | 
			
		||||
					"success": false,
 | 
			
		||||
 
 | 
			
		||||
@@ -4,14 +4,16 @@ import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
 | 
			
		||||
	"github.com/songquanpeng/one-api/common/config"
 | 
			
		||||
	"github.com/songquanpeng/one-api/common/ctxkey"
 | 
			
		||||
	"github.com/songquanpeng/one-api/controller"
 | 
			
		||||
	"github.com/songquanpeng/one-api/model"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type wechatLoginResponse struct {
 | 
			
		||||
@@ -52,6 +54,7 @@ func getWeChatIdByCode(code string) (string, error) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func WeChatAuth(c *gin.Context) {
 | 
			
		||||
	ctx := c.Request.Context()
 | 
			
		||||
	if !config.WeChatAuthEnabled {
 | 
			
		||||
		c.JSON(http.StatusOK, gin.H{
 | 
			
		||||
			"message": "管理员未开启通过微信登录以及注册",
 | 
			
		||||
@@ -87,7 +90,7 @@ func WeChatAuth(c *gin.Context) {
 | 
			
		||||
			user.Role = model.RoleCommonUser
 | 
			
		||||
			user.Status = model.UserStatusEnabled
 | 
			
		||||
 | 
			
		||||
			if err := user.Insert(0); err != nil {
 | 
			
		||||
			if err := user.Insert(ctx, 0); err != nil {
 | 
			
		||||
				c.JSON(http.StatusOK, gin.H{
 | 
			
		||||
					"success": false,
 | 
			
		||||
					"message": err.Error(),
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@ package controller
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"context"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
@@ -15,14 +16,17 @@ import (
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
 | 
			
		||||
	"github.com/songquanpeng/one-api/common/config"
 | 
			
		||||
	"github.com/songquanpeng/one-api/common/ctxkey"
 | 
			
		||||
	"github.com/songquanpeng/one-api/common/helper"
 | 
			
		||||
	"github.com/songquanpeng/one-api/common/logger"
 | 
			
		||||
	"github.com/songquanpeng/one-api/common/message"
 | 
			
		||||
	"github.com/songquanpeng/one-api/middleware"
 | 
			
		||||
	"github.com/songquanpeng/one-api/model"
 | 
			
		||||
	"github.com/songquanpeng/one-api/monitor"
 | 
			
		||||
	relay "github.com/songquanpeng/one-api/relay"
 | 
			
		||||
	"github.com/songquanpeng/one-api/relay"
 | 
			
		||||
	"github.com/songquanpeng/one-api/relay/adaptor/openai"
 | 
			
		||||
	"github.com/songquanpeng/one-api/relay/channeltype"
 | 
			
		||||
	"github.com/songquanpeng/one-api/relay/controller"
 | 
			
		||||
	"github.com/songquanpeng/one-api/relay/meta"
 | 
			
		||||
@@ -35,18 +39,34 @@ func buildTestRequest(model string) *relaymodel.GeneralOpenAIRequest {
 | 
			
		||||
		model = "gpt-3.5-turbo"
 | 
			
		||||
	}
 | 
			
		||||
	testRequest := &relaymodel.GeneralOpenAIRequest{
 | 
			
		||||
		MaxTokens: 2,
 | 
			
		||||
		Model:     model,
 | 
			
		||||
		Model: model,
 | 
			
		||||
	}
 | 
			
		||||
	testMessage := relaymodel.Message{
 | 
			
		||||
		Role:    "user",
 | 
			
		||||
		Content: "hi",
 | 
			
		||||
		Content: config.TestPrompt,
 | 
			
		||||
	}
 | 
			
		||||
	testRequest.Messages = append(testRequest.Messages, testMessage)
 | 
			
		||||
	return testRequest
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func testChannel(channel *model.Channel, request *relaymodel.GeneralOpenAIRequest) (err error, openaiErr *relaymodel.Error) {
 | 
			
		||||
func parseTestResponse(resp string) (*openai.TextResponse, string, error) {
 | 
			
		||||
	var response openai.TextResponse
 | 
			
		||||
	err := json.Unmarshal([]byte(resp), &response)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, "", err
 | 
			
		||||
	}
 | 
			
		||||
	if len(response.Choices) == 0 {
 | 
			
		||||
		return nil, "", errors.New("response has no choices")
 | 
			
		||||
	}
 | 
			
		||||
	stringContent, ok := response.Choices[0].Content.(string)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return nil, "", errors.New("response content is not string")
 | 
			
		||||
	}
 | 
			
		||||
	return &response, stringContent, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func testChannel(ctx context.Context, channel *model.Channel, request *relaymodel.GeneralOpenAIRequest) (responseMessage string, err error, openaiErr *relaymodel.Error) {
 | 
			
		||||
	startTime := time.Now()
 | 
			
		||||
	w := httptest.NewRecorder()
 | 
			
		||||
	c, _ := gin.CreateTestContext(w)
 | 
			
		||||
	c.Request = &http.Request{
 | 
			
		||||
@@ -66,7 +86,7 @@ func testChannel(channel *model.Channel, request *relaymodel.GeneralOpenAIReques
 | 
			
		||||
	apiType := channeltype.ToAPIType(channel.Type)
 | 
			
		||||
	adaptor := relay.GetAdaptor(apiType)
 | 
			
		||||
	if adaptor == nil {
 | 
			
		||||
		return fmt.Errorf("invalid api type: %d, adaptor is nil", apiType), nil
 | 
			
		||||
		return "", fmt.Errorf("invalid api type: %d, adaptor is nil", apiType), nil
 | 
			
		||||
	}
 | 
			
		||||
	adaptor.Init(meta)
 | 
			
		||||
	modelName := request.Model
 | 
			
		||||
@@ -84,41 +104,69 @@ func testChannel(channel *model.Channel, request *relaymodel.GeneralOpenAIReques
 | 
			
		||||
	request.Model = modelName
 | 
			
		||||
	convertedRequest, err := adaptor.ConvertRequest(c, relaymode.ChatCompletions, request)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err, nil
 | 
			
		||||
		return "", err, nil
 | 
			
		||||
	}
 | 
			
		||||
	jsonData, err := json.Marshal(convertedRequest)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err, nil
 | 
			
		||||
		return "", err, nil
 | 
			
		||||
	}
 | 
			
		||||
	defer func() {
 | 
			
		||||
		logContent := fmt.Sprintf("渠道 %s 测试成功,响应:%s", channel.Name, responseMessage)
 | 
			
		||||
		if err != nil || openaiErr != nil {
 | 
			
		||||
			errorMessage := ""
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				errorMessage = err.Error()
 | 
			
		||||
			} else {
 | 
			
		||||
				errorMessage = openaiErr.Message
 | 
			
		||||
			}
 | 
			
		||||
			logContent = fmt.Sprintf("渠道 %s 测试失败,错误:%s", channel.Name, errorMessage)
 | 
			
		||||
		}
 | 
			
		||||
		go model.RecordTestLog(ctx, &model.Log{
 | 
			
		||||
			ChannelId:   channel.Id,
 | 
			
		||||
			ModelName:   modelName,
 | 
			
		||||
			Content:     logContent,
 | 
			
		||||
			ElapsedTime: helper.CalcElapsedTime(startTime),
 | 
			
		||||
		})
 | 
			
		||||
	}()
 | 
			
		||||
	logger.SysLog(string(jsonData))
 | 
			
		||||
	requestBody := bytes.NewBuffer(jsonData)
 | 
			
		||||
	c.Request.Body = io.NopCloser(requestBody)
 | 
			
		||||
	resp, err := adaptor.DoRequest(c, meta, requestBody)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err, nil
 | 
			
		||||
		return "", err, nil
 | 
			
		||||
	}
 | 
			
		||||
	if resp != nil && resp.StatusCode != http.StatusOK {
 | 
			
		||||
		err := controller.RelayErrorHandler(resp)
 | 
			
		||||
		return fmt.Errorf("status code %d: %s", resp.StatusCode, err.Error.Message), &err.Error
 | 
			
		||||
		errorMessage := err.Error.Message
 | 
			
		||||
		if errorMessage != "" {
 | 
			
		||||
			errorMessage = ", error message: " + errorMessage
 | 
			
		||||
		}
 | 
			
		||||
		return "", fmt.Errorf("http status code: %d%s", resp.StatusCode, errorMessage), &err.Error
 | 
			
		||||
	}
 | 
			
		||||
	usage, respErr := adaptor.DoResponse(c, resp, meta)
 | 
			
		||||
	if respErr != nil {
 | 
			
		||||
		return fmt.Errorf("%s", respErr.Error.Message), &respErr.Error
 | 
			
		||||
		return "", fmt.Errorf("%s", respErr.Error.Message), &respErr.Error
 | 
			
		||||
	}
 | 
			
		||||
	if usage == nil {
 | 
			
		||||
		return errors.New("usage is nil"), nil
 | 
			
		||||
		return "", errors.New("usage is nil"), nil
 | 
			
		||||
	}
 | 
			
		||||
	rawResponse := w.Body.String()
 | 
			
		||||
	_, responseMessage, err = parseTestResponse(rawResponse)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err, nil
 | 
			
		||||
	}
 | 
			
		||||
	result := w.Result()
 | 
			
		||||
	// print result.Body
 | 
			
		||||
	respBody, err := io.ReadAll(result.Body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err, nil
 | 
			
		||||
		return "", err, nil
 | 
			
		||||
	}
 | 
			
		||||
	logger.SysLog(fmt.Sprintf("testing channel #%d, response: \n%s", channel.Id, string(respBody)))
 | 
			
		||||
	return nil, nil
 | 
			
		||||
	return responseMessage, nil, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestChannel(c *gin.Context) {
 | 
			
		||||
	ctx := c.Request.Context()
 | 
			
		||||
	id, err := strconv.Atoi(c.Param("id"))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		c.JSON(http.StatusOK, gin.H{
 | 
			
		||||
@@ -135,10 +183,10 @@ func TestChannel(c *gin.Context) {
 | 
			
		||||
		})
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	model := c.Query("model")
 | 
			
		||||
	testRequest := buildTestRequest(model)
 | 
			
		||||
	modelName := c.Query("model")
 | 
			
		||||
	testRequest := buildTestRequest(modelName)
 | 
			
		||||
	tik := time.Now()
 | 
			
		||||
	err, _ = testChannel(channel, testRequest)
 | 
			
		||||
	responseMessage, err, _ := testChannel(ctx, channel, testRequest)
 | 
			
		||||
	tok := time.Now()
 | 
			
		||||
	milliseconds := tok.Sub(tik).Milliseconds()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
@@ -148,18 +196,18 @@ func TestChannel(c *gin.Context) {
 | 
			
		||||
	consumedTime := float64(milliseconds) / 1000.0
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		c.JSON(http.StatusOK, gin.H{
 | 
			
		||||
			"success": false,
 | 
			
		||||
			"message": err.Error(),
 | 
			
		||||
			"time":    consumedTime,
 | 
			
		||||
			"model":   model,
 | 
			
		||||
			"success":   false,
 | 
			
		||||
			"message":   err.Error(),
 | 
			
		||||
			"time":      consumedTime,
 | 
			
		||||
			"modelName": modelName,
 | 
			
		||||
		})
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	c.JSON(http.StatusOK, gin.H{
 | 
			
		||||
		"success": true,
 | 
			
		||||
		"message": "",
 | 
			
		||||
		"time":    consumedTime,
 | 
			
		||||
		"model":   model,
 | 
			
		||||
		"success":   true,
 | 
			
		||||
		"message":   responseMessage,
 | 
			
		||||
		"time":      consumedTime,
 | 
			
		||||
		"modelName": modelName,
 | 
			
		||||
	})
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
@@ -167,7 +215,7 @@ func TestChannel(c *gin.Context) {
 | 
			
		||||
var testAllChannelsLock sync.Mutex
 | 
			
		||||
var testAllChannelsRunning bool = false
 | 
			
		||||
 | 
			
		||||
func testChannels(notify bool, scope string) error {
 | 
			
		||||
func testChannels(ctx context.Context, notify bool, scope string) error {
 | 
			
		||||
	if config.RootUserEmail == "" {
 | 
			
		||||
		config.RootUserEmail = model.GetRootUserEmail()
 | 
			
		||||
	}
 | 
			
		||||
@@ -191,7 +239,7 @@ func testChannels(notify bool, scope string) error {
 | 
			
		||||
			isChannelEnabled := channel.Status == model.ChannelStatusEnabled
 | 
			
		||||
			tik := time.Now()
 | 
			
		||||
			testRequest := buildTestRequest("")
 | 
			
		||||
			err, openaiErr := testChannel(channel, testRequest)
 | 
			
		||||
			_, err, openaiErr := testChannel(ctx, channel, testRequest)
 | 
			
		||||
			tok := time.Now()
 | 
			
		||||
			milliseconds := tok.Sub(tik).Milliseconds()
 | 
			
		||||
			if isChannelEnabled && milliseconds > disableThreshold {
 | 
			
		||||
@@ -225,11 +273,12 @@ func testChannels(notify bool, scope string) error {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestChannels(c *gin.Context) {
 | 
			
		||||
	ctx := c.Request.Context()
 | 
			
		||||
	scope := c.Query("scope")
 | 
			
		||||
	if scope == "" {
 | 
			
		||||
		scope = "all"
 | 
			
		||||
	}
 | 
			
		||||
	err := testChannels(true, scope)
 | 
			
		||||
	err := testChannels(ctx, true, scope)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		c.JSON(http.StatusOK, gin.H{
 | 
			
		||||
			"success": false,
 | 
			
		||||
@@ -245,10 +294,11 @@ func TestChannels(c *gin.Context) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func AutomaticallyTestChannels(frequency int) {
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	for {
 | 
			
		||||
		time.Sleep(time.Duration(frequency) * time.Minute)
 | 
			
		||||
		logger.SysLog("testing all channels")
 | 
			
		||||
		_ = testChannels(false, "all")
 | 
			
		||||
		_ = testChannels(ctx, false, "all")
 | 
			
		||||
		logger.SysLog("channel test finished")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -109,6 +109,7 @@ func Logout(c *gin.Context) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Register(c *gin.Context) {
 | 
			
		||||
	ctx := c.Request.Context()
 | 
			
		||||
	if !config.RegisterEnabled {
 | 
			
		||||
		c.JSON(http.StatusOK, gin.H{
 | 
			
		||||
			"message": "管理员关闭了新用户注册",
 | 
			
		||||
@@ -166,7 +167,7 @@ func Register(c *gin.Context) {
 | 
			
		||||
	if config.EmailVerificationEnabled {
 | 
			
		||||
		cleanUser.Email = user.Email
 | 
			
		||||
	}
 | 
			
		||||
	if err := cleanUser.Insert(inviterId); err != nil {
 | 
			
		||||
	if err := cleanUser.Insert(ctx, inviterId); err != nil {
 | 
			
		||||
		c.JSON(http.StatusOK, gin.H{
 | 
			
		||||
			"success": false,
 | 
			
		||||
			"message": err.Error(),
 | 
			
		||||
@@ -362,6 +363,7 @@ func GetSelf(c *gin.Context) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func UpdateUser(c *gin.Context) {
 | 
			
		||||
	ctx := c.Request.Context()
 | 
			
		||||
	var updatedUser model.User
 | 
			
		||||
	err := json.NewDecoder(c.Request.Body).Decode(&updatedUser)
 | 
			
		||||
	if err != nil || updatedUser.Id == 0 {
 | 
			
		||||
@@ -416,7 +418,7 @@ func UpdateUser(c *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	if originUser.Quota != updatedUser.Quota {
 | 
			
		||||
		model.RecordLog(originUser.Id, model.LogTypeManage, fmt.Sprintf("管理员将用户额度从 %s修改为 %s", common.LogQuota(originUser.Quota), common.LogQuota(updatedUser.Quota)))
 | 
			
		||||
		model.RecordLog(ctx, originUser.Id, model.LogTypeManage, fmt.Sprintf("管理员将用户额度从 %s修改为 %s", common.LogQuota(originUser.Quota), common.LogQuota(updatedUser.Quota)))
 | 
			
		||||
	}
 | 
			
		||||
	c.JSON(http.StatusOK, gin.H{
 | 
			
		||||
		"success": true,
 | 
			
		||||
@@ -535,6 +537,7 @@ func DeleteSelf(c *gin.Context) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func CreateUser(c *gin.Context) {
 | 
			
		||||
	ctx := c.Request.Context()
 | 
			
		||||
	var user model.User
 | 
			
		||||
	err := json.NewDecoder(c.Request.Body).Decode(&user)
 | 
			
		||||
	if err != nil || user.Username == "" || user.Password == "" {
 | 
			
		||||
@@ -568,7 +571,7 @@ func CreateUser(c *gin.Context) {
 | 
			
		||||
		Password:    user.Password,
 | 
			
		||||
		DisplayName: user.DisplayName,
 | 
			
		||||
	}
 | 
			
		||||
	if err := cleanUser.Insert(0); err != nil {
 | 
			
		||||
	if err := cleanUser.Insert(ctx, 0); err != nil {
 | 
			
		||||
		c.JSON(http.StatusOK, gin.H{
 | 
			
		||||
			"success": false,
 | 
			
		||||
			"message": err.Error(),
 | 
			
		||||
@@ -747,6 +750,7 @@ type topUpRequest struct {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TopUp(c *gin.Context) {
 | 
			
		||||
	ctx := c.Request.Context()
 | 
			
		||||
	req := topUpRequest{}
 | 
			
		||||
	err := c.ShouldBindJSON(&req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
@@ -757,7 +761,7 @@ func TopUp(c *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	id := c.GetInt("id")
 | 
			
		||||
	quota, err := model.Redeem(req.Key, id)
 | 
			
		||||
	quota, err := model.Redeem(ctx, req.Key, id)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		c.JSON(http.StatusOK, gin.H{
 | 
			
		||||
			"success": false,
 | 
			
		||||
@@ -780,6 +784,7 @@ type adminTopUpRequest struct {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func AdminTopUp(c *gin.Context) {
 | 
			
		||||
	ctx := c.Request.Context()
 | 
			
		||||
	req := adminTopUpRequest{}
 | 
			
		||||
	err := c.ShouldBindJSON(&req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
@@ -800,7 +805,7 @@ func AdminTopUp(c *gin.Context) {
 | 
			
		||||
	if req.Remark == "" {
 | 
			
		||||
		req.Remark = fmt.Sprintf("通过 API 充值 %s", common.LogQuota(int64(req.Quota)))
 | 
			
		||||
	}
 | 
			
		||||
	model.RecordTopupLog(req.UserId, req.Remark, req.Quota)
 | 
			
		||||
	model.RecordTopupLog(ctx, req.UserId, req.Remark, req.Quota)
 | 
			
		||||
	c.JSON(http.StatusOK, gin.H{
 | 
			
		||||
		"success": true,
 | 
			
		||||
		"message": "",
 | 
			
		||||
 
 | 
			
		||||
@@ -2,13 +2,15 @@ package middleware
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strconv"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
 | 
			
		||||
	"github.com/songquanpeng/one-api/common/ctxkey"
 | 
			
		||||
	"github.com/songquanpeng/one-api/common/logger"
 | 
			
		||||
	"github.com/songquanpeng/one-api/model"
 | 
			
		||||
	"github.com/songquanpeng/one-api/relay/channeltype"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strconv"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type ModelRequest struct {
 | 
			
		||||
@@ -17,6 +19,7 @@ type ModelRequest struct {
 | 
			
		||||
 | 
			
		||||
func Distribute() func(c *gin.Context) {
 | 
			
		||||
	return func(c *gin.Context) {
 | 
			
		||||
		ctx := c.Request.Context()
 | 
			
		||||
		userId := c.GetInt(ctxkey.Id)
 | 
			
		||||
		userGroup, _ := model.CacheGetUserGroup(userId)
 | 
			
		||||
		c.Set(ctxkey.Group, userGroup)
 | 
			
		||||
@@ -52,6 +55,7 @@ func Distribute() func(c *gin.Context) {
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		logger.Debugf(ctx, "user id %d, user group: %s, request model: %s, using channel #%d", userId, userGroup, requestModel, channel.Id)
 | 
			
		||||
		SetupContextForSelectedChannel(c, channel, requestModel)
 | 
			
		||||
		c.Next()
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
package middleware
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
 | 
			
		||||
	"github.com/songquanpeng/one-api/common/helper"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -10,7 +10,7 @@ func RequestId() func(c *gin.Context) {
 | 
			
		||||
	return func(c *gin.Context) {
 | 
			
		||||
		id := helper.GenRequestID()
 | 
			
		||||
		c.Set(helper.RequestIdKey, id)
 | 
			
		||||
		ctx := context.WithValue(c.Request.Context(), helper.RequestIdKey, id)
 | 
			
		||||
		ctx := helper.SetRequestID(c.Request.Context(), id)
 | 
			
		||||
		c.Request = c.Request.WithContext(ctx)
 | 
			
		||||
		c.Header(helper.RequestIdKey, id)
 | 
			
		||||
		c.Next()
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										87
									
								
								model/log.go
									
									
									
									
									
								
							
							
						
						
									
										87
									
								
								model/log.go
									
									
									
									
									
								
							@@ -4,26 +4,31 @@ import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
 | 
			
		||||
	"github.com/songquanpeng/one-api/common"
 | 
			
		||||
	"github.com/songquanpeng/one-api/common/config"
 | 
			
		||||
	"github.com/songquanpeng/one-api/common/helper"
 | 
			
		||||
	"github.com/songquanpeng/one-api/common/logger"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Log struct {
 | 
			
		||||
	Id               int    `json:"id"`
 | 
			
		||||
	UserId           int    `json:"user_id" gorm:"index"`
 | 
			
		||||
	CreatedAt        int64  `json:"created_at" gorm:"bigint;index:idx_created_at_type"`
 | 
			
		||||
	Type             int    `json:"type" gorm:"index:idx_created_at_type"`
 | 
			
		||||
	Content          string `json:"content"`
 | 
			
		||||
	Username         string `json:"username" gorm:"index:index_username_model_name,priority:2;default:''"`
 | 
			
		||||
	TokenName        string `json:"token_name" gorm:"index;default:''"`
 | 
			
		||||
	ModelName        string `json:"model_name" gorm:"index;index:index_username_model_name,priority:1;default:''"`
 | 
			
		||||
	Quota            int    `json:"quota" gorm:"default:0"`
 | 
			
		||||
	PromptTokens     int    `json:"prompt_tokens" gorm:"default:0"`
 | 
			
		||||
	CompletionTokens int    `json:"completion_tokens" gorm:"default:0"`
 | 
			
		||||
	ChannelId        int    `json:"channel" gorm:"index"`
 | 
			
		||||
	Id                int    `json:"id"`
 | 
			
		||||
	UserId            int    `json:"user_id" gorm:"index"`
 | 
			
		||||
	CreatedAt         int64  `json:"created_at" gorm:"bigint;index:idx_created_at_type"`
 | 
			
		||||
	Type              int    `json:"type" gorm:"index:idx_created_at_type"`
 | 
			
		||||
	Content           string `json:"content"`
 | 
			
		||||
	Username          string `json:"username" gorm:"index:index_username_model_name,priority:2;default:''"`
 | 
			
		||||
	TokenName         string `json:"token_name" gorm:"index;default:''"`
 | 
			
		||||
	ModelName         string `json:"model_name" gorm:"index;index:index_username_model_name,priority:1;default:''"`
 | 
			
		||||
	Quota             int    `json:"quota" gorm:"default:0"`
 | 
			
		||||
	PromptTokens      int    `json:"prompt_tokens" gorm:"default:0"`
 | 
			
		||||
	CompletionTokens  int    `json:"completion_tokens" gorm:"default:0"`
 | 
			
		||||
	ChannelId         int    `json:"channel" gorm:"index"`
 | 
			
		||||
	RequestId         string `json:"request_id" gorm:"default:''"`
 | 
			
		||||
	ElapsedTime       int64  `json:"elapsed_time" gorm:"default:0"` // unit is ms
 | 
			
		||||
	IsStream          bool   `json:"is_stream" gorm:"default:false"`
 | 
			
		||||
	SystemPromptReset bool   `json:"system_prompt_reset" gorm:"default:false"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
@@ -32,9 +37,21 @@ const (
 | 
			
		||||
	LogTypeConsume
 | 
			
		||||
	LogTypeManage
 | 
			
		||||
	LogTypeSystem
 | 
			
		||||
	LogTypeTest
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func RecordLog(userId int, logType int, content string) {
 | 
			
		||||
func recordLogHelper(ctx context.Context, log *Log) {
 | 
			
		||||
	requestId := helper.GetRequestID(ctx)
 | 
			
		||||
	log.RequestId = requestId
 | 
			
		||||
	err := LOG_DB.Create(log).Error
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Error(ctx, "failed to record log: "+err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	logger.Infof(ctx, "record log: %+v", log)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func RecordLog(ctx context.Context, userId int, logType int, content string) {
 | 
			
		||||
	if logType == LogTypeConsume && !config.LogConsumeEnabled {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
@@ -45,13 +62,10 @@ func RecordLog(userId int, logType int, content string) {
 | 
			
		||||
		Type:      logType,
 | 
			
		||||
		Content:   content,
 | 
			
		||||
	}
 | 
			
		||||
	err := LOG_DB.Create(log).Error
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.SysError("failed to record log: " + err.Error())
 | 
			
		||||
	}
 | 
			
		||||
	recordLogHelper(ctx, log)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func RecordTopupLog(userId int, content string, quota int) {
 | 
			
		||||
func RecordTopupLog(ctx context.Context, userId int, content string, quota int) {
 | 
			
		||||
	log := &Log{
 | 
			
		||||
		UserId:    userId,
 | 
			
		||||
		Username:  GetUsernameById(userId),
 | 
			
		||||
@@ -60,34 +74,23 @@ func RecordTopupLog(userId int, content string, quota int) {
 | 
			
		||||
		Content:   content,
 | 
			
		||||
		Quota:     quota,
 | 
			
		||||
	}
 | 
			
		||||
	err := LOG_DB.Create(log).Error
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.SysError("failed to record log: " + err.Error())
 | 
			
		||||
	}
 | 
			
		||||
	recordLogHelper(ctx, log)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func RecordConsumeLog(ctx context.Context, userId int, channelId int, promptTokens int, completionTokens int, modelName string, tokenName string, quota int64, content string) {
 | 
			
		||||
	logger.Info(ctx, fmt.Sprintf("record consume log: userId=%d, channelId=%d, promptTokens=%d, completionTokens=%d, modelName=%s, tokenName=%s, quota=%d, content=%s", userId, channelId, promptTokens, completionTokens, modelName, tokenName, quota, content))
 | 
			
		||||
func RecordConsumeLog(ctx context.Context, log *Log) {
 | 
			
		||||
	if !config.LogConsumeEnabled {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	log := &Log{
 | 
			
		||||
		UserId:           userId,
 | 
			
		||||
		Username:         GetUsernameById(userId),
 | 
			
		||||
		CreatedAt:        helper.GetTimestamp(),
 | 
			
		||||
		Type:             LogTypeConsume,
 | 
			
		||||
		Content:          content,
 | 
			
		||||
		PromptTokens:     promptTokens,
 | 
			
		||||
		CompletionTokens: completionTokens,
 | 
			
		||||
		TokenName:        tokenName,
 | 
			
		||||
		ModelName:        modelName,
 | 
			
		||||
		Quota:            int(quota),
 | 
			
		||||
		ChannelId:        channelId,
 | 
			
		||||
	}
 | 
			
		||||
	err := LOG_DB.Create(log).Error
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Error(ctx, "failed to record log: "+err.Error())
 | 
			
		||||
	}
 | 
			
		||||
	log.Username = GetUsernameById(log.UserId)
 | 
			
		||||
	log.CreatedAt = helper.GetTimestamp()
 | 
			
		||||
	log.Type = LogTypeConsume
 | 
			
		||||
	recordLogHelper(ctx, log)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func RecordTestLog(ctx context.Context, log *Log) {
 | 
			
		||||
	log.CreatedAt = helper.GetTimestamp()
 | 
			
		||||
	log.Type = LogTypeTest
 | 
			
		||||
	recordLogHelper(ctx, log)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetAllLogs(logType int, startTimestamp int64, endTimestamp int64, modelName string, username string, tokenName string, startIdx int, num int, channel int) (logs []*Log, err error) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,14 @@
 | 
			
		||||
package model
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
 | 
			
		||||
	"github.com/songquanpeng/one-api/common"
 | 
			
		||||
	"github.com/songquanpeng/one-api/common/helper"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
@@ -48,7 +51,7 @@ func GetRedemptionById(id int) (*Redemption, error) {
 | 
			
		||||
	return &redemption, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Redeem(key string, userId int) (quota int64, err error) {
 | 
			
		||||
func Redeem(ctx context.Context, key string, userId int) (quota int64, err error) {
 | 
			
		||||
	if key == "" {
 | 
			
		||||
		return 0, errors.New("未提供兑换码")
 | 
			
		||||
	}
 | 
			
		||||
@@ -82,7 +85,7 @@ func Redeem(key string, userId int) (quota int64, err error) {
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, errors.New("兑换失败," + err.Error())
 | 
			
		||||
	}
 | 
			
		||||
	RecordLog(userId, LogTypeTopup, fmt.Sprintf("通过兑换码充值 %s", common.LogQuota(redemption.Quota)))
 | 
			
		||||
	RecordLog(ctx, userId, LogTypeTopup, fmt.Sprintf("通过兑换码充值 %s", common.LogQuota(redemption.Quota)))
 | 
			
		||||
	return redemption.Quota, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,16 +1,19 @@
 | 
			
		||||
package model
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
 | 
			
		||||
	"github.com/songquanpeng/one-api/common"
 | 
			
		||||
	"github.com/songquanpeng/one-api/common/blacklist"
 | 
			
		||||
	"github.com/songquanpeng/one-api/common/config"
 | 
			
		||||
	"github.com/songquanpeng/one-api/common/helper"
 | 
			
		||||
	"github.com/songquanpeng/one-api/common/logger"
 | 
			
		||||
	"github.com/songquanpeng/one-api/common/random"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
	"strings"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
@@ -92,7 +95,7 @@ func GetUserById(id int, selectAll bool) (*User, error) {
 | 
			
		||||
	if selectAll {
 | 
			
		||||
		err = DB.First(&user, "id = ?", id).Error
 | 
			
		||||
	} else {
 | 
			
		||||
		err = DB.Omit("password").First(&user, "id = ?", id).Error
 | 
			
		||||
		err = DB.Omit("password", "access_token").First(&user, "id = ?", id).Error
 | 
			
		||||
	}
 | 
			
		||||
	return &user, err
 | 
			
		||||
}
 | 
			
		||||
@@ -114,7 +117,7 @@ func DeleteUserById(id int) (err error) {
 | 
			
		||||
	return user.Delete()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (user *User) Insert(inviterId int) error {
 | 
			
		||||
func (user *User) Insert(ctx context.Context, inviterId int) error {
 | 
			
		||||
	var err error
 | 
			
		||||
	if user.Password != "" {
 | 
			
		||||
		user.Password, err = common.Password2Hash(user.Password)
 | 
			
		||||
@@ -130,16 +133,16 @@ func (user *User) Insert(inviterId int) error {
 | 
			
		||||
		return result.Error
 | 
			
		||||
	}
 | 
			
		||||
	if config.QuotaForNewUser > 0 {
 | 
			
		||||
		RecordLog(user.Id, LogTypeSystem, fmt.Sprintf("新用户注册赠送 %s", common.LogQuota(config.QuotaForNewUser)))
 | 
			
		||||
		RecordLog(ctx, user.Id, LogTypeSystem, fmt.Sprintf("新用户注册赠送 %s", common.LogQuota(config.QuotaForNewUser)))
 | 
			
		||||
	}
 | 
			
		||||
	if inviterId != 0 {
 | 
			
		||||
		if config.QuotaForInvitee > 0 {
 | 
			
		||||
			_ = IncreaseUserQuota(user.Id, config.QuotaForInvitee)
 | 
			
		||||
			RecordLog(user.Id, LogTypeSystem, fmt.Sprintf("使用邀请码赠送 %s", common.LogQuota(config.QuotaForInvitee)))
 | 
			
		||||
			RecordLog(ctx, user.Id, LogTypeSystem, fmt.Sprintf("使用邀请码赠送 %s", common.LogQuota(config.QuotaForInvitee)))
 | 
			
		||||
		}
 | 
			
		||||
		if config.QuotaForInviter > 0 {
 | 
			
		||||
			_ = IncreaseUserQuota(inviterId, config.QuotaForInviter)
 | 
			
		||||
			RecordLog(inviterId, LogTypeSystem, fmt.Sprintf("邀请用户赠送 %s", common.LogQuota(config.QuotaForInviter)))
 | 
			
		||||
			RecordLog(ctx, inviterId, LogTypeSystem, fmt.Sprintf("邀请用户赠送 %s", common.LogQuota(config.QuotaForInviter)))
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	// create default token
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,6 @@ import (
 | 
			
		||||
	"net/http"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"github.com/songquanpeng/one-api/common/config"
 | 
			
		||||
	"github.com/songquanpeng/one-api/common/helper"
 | 
			
		||||
	channelhelper "github.com/songquanpeng/one-api/relay/adaptor"
 | 
			
		||||
	"github.com/songquanpeng/one-api/relay/adaptor/openai"
 | 
			
		||||
@@ -24,8 +23,11 @@ func (a *Adaptor) Init(meta *meta.Meta) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) {
 | 
			
		||||
	defaultVersion := config.GeminiVersion
 | 
			
		||||
	if meta.ActualModelName == "gemini-2.0-flash-exp" {
 | 
			
		||||
	var defaultVersion string
 | 
			
		||||
	switch meta.ActualModelName {
 | 
			
		||||
	case "gemini-2.0-flash-exp",
 | 
			
		||||
		"gemini-2.0-flash-thinking-exp",
 | 
			
		||||
		"gemini-2.0-flash-thinking-exp-01-21":
 | 
			
		||||
		defaultVersion = "v1beta"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -7,5 +7,5 @@ var ModelList = []string{
 | 
			
		||||
	"gemini-1.5-flash", "gemini-1.5-pro",
 | 
			
		||||
	"text-embedding-004", "aqa",
 | 
			
		||||
	"gemini-2.0-flash-exp",
 | 
			
		||||
	"gemini-2.0-flash-thinking-exp",
 | 
			
		||||
	"gemini-2.0-flash-thinking-exp", "gemini-2.0-flash-thinking-exp-01-21",
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,16 +2,19 @@ package tencent
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
 | 
			
		||||
	"github.com/songquanpeng/one-api/common/helper"
 | 
			
		||||
	"github.com/songquanpeng/one-api/relay/adaptor"
 | 
			
		||||
	"github.com/songquanpeng/one-api/relay/adaptor/openai"
 | 
			
		||||
	"github.com/songquanpeng/one-api/relay/meta"
 | 
			
		||||
	"github.com/songquanpeng/one-api/relay/model"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"github.com/songquanpeng/one-api/relay/relaymode"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// https://cloud.tencent.com/document/api/1729/101837
 | 
			
		||||
@@ -52,10 +55,18 @@ func (a *Adaptor) ConvertRequest(c *gin.Context, relayMode int, request *model.G
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	tencentRequest := ConvertRequest(*request)
 | 
			
		||||
	var convertedRequest any
 | 
			
		||||
	switch relayMode {
 | 
			
		||||
	case relaymode.Embeddings:
 | 
			
		||||
		a.Action = "GetEmbedding"
 | 
			
		||||
		convertedRequest = ConvertEmbeddingRequest(*request)
 | 
			
		||||
	default:
 | 
			
		||||
		a.Action = "ChatCompletions"
 | 
			
		||||
		convertedRequest = ConvertRequest(*request)
 | 
			
		||||
	}
 | 
			
		||||
	// we have to calculate the sign here
 | 
			
		||||
	a.Sign = GetSign(*tencentRequest, a, secretId, secretKey)
 | 
			
		||||
	return tencentRequest, nil
 | 
			
		||||
	a.Sign = GetSign(convertedRequest, a, secretId, secretKey)
 | 
			
		||||
	return convertedRequest, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *Adaptor) ConvertImageRequest(request *model.ImageRequest) (any, error) {
 | 
			
		||||
@@ -75,7 +86,12 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, meta *meta.Met
 | 
			
		||||
		err, responseText = StreamHandler(c, resp)
 | 
			
		||||
		usage = openai.ResponseText2Usage(responseText, meta.ActualModelName, meta.PromptTokens)
 | 
			
		||||
	} else {
 | 
			
		||||
		err, usage = Handler(c, resp)
 | 
			
		||||
		switch meta.Mode {
 | 
			
		||||
		case relaymode.Embeddings:
 | 
			
		||||
			err, usage = EmbeddingHandler(c, resp)
 | 
			
		||||
		default:
 | 
			
		||||
			err, usage = Handler(c, resp)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,4 +6,5 @@ var ModelList = []string{
 | 
			
		||||
	"hunyuan-standard-256K",
 | 
			
		||||
	"hunyuan-pro",
 | 
			
		||||
	"hunyuan-vision",
 | 
			
		||||
	"hunyuan-embedding",
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,6 @@ import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/songquanpeng/one-api/common/render"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strconv"
 | 
			
		||||
@@ -16,11 +15,14 @@ import (
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
 | 
			
		||||
	"github.com/songquanpeng/one-api/common"
 | 
			
		||||
	"github.com/songquanpeng/one-api/common/conv"
 | 
			
		||||
	"github.com/songquanpeng/one-api/common/ctxkey"
 | 
			
		||||
	"github.com/songquanpeng/one-api/common/helper"
 | 
			
		||||
	"github.com/songquanpeng/one-api/common/logger"
 | 
			
		||||
	"github.com/songquanpeng/one-api/common/random"
 | 
			
		||||
	"github.com/songquanpeng/one-api/common/render"
 | 
			
		||||
	"github.com/songquanpeng/one-api/relay/adaptor/openai"
 | 
			
		||||
	"github.com/songquanpeng/one-api/relay/constant"
 | 
			
		||||
	"github.com/songquanpeng/one-api/relay/model"
 | 
			
		||||
@@ -44,8 +46,68 @@ func ConvertRequest(request model.GeneralOpenAIRequest) *ChatRequest {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ConvertEmbeddingRequest(request model.GeneralOpenAIRequest) *EmbeddingRequest {
 | 
			
		||||
	return &EmbeddingRequest{
 | 
			
		||||
		InputList: request.ParseInput(),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func EmbeddingHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusCode, *model.Usage) {
 | 
			
		||||
	var tencentResponseP EmbeddingResponseP
 | 
			
		||||
	err := json.NewDecoder(resp.Body).Decode(&tencentResponseP)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return openai.ErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = resp.Body.Close()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return openai.ErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tencentResponse := tencentResponseP.Response
 | 
			
		||||
	if tencentResponse.Error.Code != "" {
 | 
			
		||||
		return &model.ErrorWithStatusCode{
 | 
			
		||||
			Error: model.Error{
 | 
			
		||||
				Message: tencentResponse.Error.Message,
 | 
			
		||||
				Code:    tencentResponse.Error.Code,
 | 
			
		||||
			},
 | 
			
		||||
			StatusCode: resp.StatusCode,
 | 
			
		||||
		}, nil
 | 
			
		||||
	}
 | 
			
		||||
	requestModel := c.GetString(ctxkey.RequestModel)
 | 
			
		||||
	fullTextResponse := embeddingResponseTencent2OpenAI(&tencentResponse)
 | 
			
		||||
	fullTextResponse.Model = requestModel
 | 
			
		||||
	jsonResponse, err := json.Marshal(fullTextResponse)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return openai.ErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil
 | 
			
		||||
	}
 | 
			
		||||
	c.Writer.Header().Set("Content-Type", "application/json")
 | 
			
		||||
	c.Writer.WriteHeader(resp.StatusCode)
 | 
			
		||||
	_, err = c.Writer.Write(jsonResponse)
 | 
			
		||||
	return nil, &fullTextResponse.Usage
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func embeddingResponseTencent2OpenAI(response *EmbeddingResponse) *openai.EmbeddingResponse {
 | 
			
		||||
	openAIEmbeddingResponse := openai.EmbeddingResponse{
 | 
			
		||||
		Object: "list",
 | 
			
		||||
		Data:   make([]openai.EmbeddingResponseItem, 0, len(response.Data)),
 | 
			
		||||
		Model:  "hunyuan-embedding",
 | 
			
		||||
		Usage:  model.Usage{TotalTokens: response.EmbeddingUsage.TotalTokens},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, item := range response.Data {
 | 
			
		||||
		openAIEmbeddingResponse.Data = append(openAIEmbeddingResponse.Data, openai.EmbeddingResponseItem{
 | 
			
		||||
			Object:    item.Object,
 | 
			
		||||
			Index:     item.Index,
 | 
			
		||||
			Embedding: item.Embedding,
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
	return &openAIEmbeddingResponse
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func responseTencent2OpenAI(response *ChatResponse) *openai.TextResponse {
 | 
			
		||||
	fullTextResponse := openai.TextResponse{
 | 
			
		||||
		Id:      response.ReqID,
 | 
			
		||||
		Object:  "chat.completion",
 | 
			
		||||
		Created: helper.GetTimestamp(),
 | 
			
		||||
		Usage: model.Usage{
 | 
			
		||||
@@ -148,7 +210,7 @@ func Handler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusCode, *
 | 
			
		||||
		return openai.ErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
 | 
			
		||||
	}
 | 
			
		||||
	TencentResponse = responseP.Response
 | 
			
		||||
	if TencentResponse.Error.Code != 0 {
 | 
			
		||||
	if TencentResponse.Error.Code != "" {
 | 
			
		||||
		return &model.ErrorWithStatusCode{
 | 
			
		||||
			Error: model.Error{
 | 
			
		||||
				Message: TencentResponse.Error.Message,
 | 
			
		||||
@@ -195,7 +257,7 @@ func hmacSha256(s, key string) string {
 | 
			
		||||
	return string(hashed.Sum(nil))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetSign(req ChatRequest, adaptor *Adaptor, secId, secKey string) string {
 | 
			
		||||
func GetSign(req any, adaptor *Adaptor, secId, secKey string) string {
 | 
			
		||||
	// build canonical request string
 | 
			
		||||
	host := "hunyuan.tencentcloudapi.com"
 | 
			
		||||
	httpRequestMethod := "POST"
 | 
			
		||||
 
 | 
			
		||||
@@ -35,16 +35,16 @@ type ChatRequest struct {
 | 
			
		||||
	// 1. 影响输出文本的多样性,取值越大,生成文本的多样性越强。
 | 
			
		||||
	// 2. 取值区间为 [0.0, 1.0],未传值时使用各模型推荐值。
 | 
			
		||||
	// 3. 非必要不建议使用,不合理的取值会影响效果。
 | 
			
		||||
	TopP *float64 `json:"TopP"`
 | 
			
		||||
	TopP *float64 `json:"TopP,omitempty"`
 | 
			
		||||
	// 说明:
 | 
			
		||||
	// 1. 较高的数值会使输出更加随机,而较低的数值会使其更加集中和确定。
 | 
			
		||||
	// 2. 取值区间为 [0.0, 2.0],未传值时使用各模型推荐值。
 | 
			
		||||
	// 3. 非必要不建议使用,不合理的取值会影响效果。
 | 
			
		||||
	Temperature *float64 `json:"Temperature"`
 | 
			
		||||
	Temperature *float64 `json:"Temperature,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Error struct {
 | 
			
		||||
	Code    int    `json:"Code"`
 | 
			
		||||
	Code    string `json:"Code"`
 | 
			
		||||
	Message string `json:"Message"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -61,15 +61,41 @@ type ResponseChoices struct {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ChatResponse struct {
 | 
			
		||||
	Choices []ResponseChoices `json:"Choices,omitempty"` // 结果
 | 
			
		||||
	Created int64             `json:"Created,omitempty"` // unix 时间戳的字符串
 | 
			
		||||
	Id      string            `json:"Id,omitempty"`      // 会话 id
 | 
			
		||||
	Usage   Usage             `json:"Usage,omitempty"`   // token 数量
 | 
			
		||||
	Error   Error             `json:"Error,omitempty"`   // 错误信息 注意:此字段可能返回 null,表示取不到有效值
 | 
			
		||||
	Note    string            `json:"Note,omitempty"`    // 注释
 | 
			
		||||
	ReqID   string            `json:"Req_id,omitempty"`  // 唯一请求 Id,每次请求都会返回。用于反馈接口入参
 | 
			
		||||
	Choices []ResponseChoices `json:"Choices,omitempty"`   // 结果
 | 
			
		||||
	Created int64             `json:"Created,omitempty"`   // unix 时间戳的字符串
 | 
			
		||||
	Id      string            `json:"Id,omitempty"`        // 会话 id
 | 
			
		||||
	Usage   Usage             `json:"Usage,omitempty"`     // token 数量
 | 
			
		||||
	Error   Error             `json:"Error,omitempty"`     // 错误信息 注意:此字段可能返回 null,表示取不到有效值
 | 
			
		||||
	Note    string            `json:"Note,omitempty"`      // 注释
 | 
			
		||||
	ReqID   string            `json:"RequestId,omitempty"` // 唯一请求 Id,每次请求都会返回。用于反馈接口入参
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ChatResponseP struct {
 | 
			
		||||
	Response ChatResponse `json:"Response,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type EmbeddingRequest struct {
 | 
			
		||||
	InputList []string `json:"InputList"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type EmbeddingData struct {
 | 
			
		||||
	Embedding []float64 `json:"Embedding"`
 | 
			
		||||
	Index     int       `json:"Index"`
 | 
			
		||||
	Object    string    `json:"Object"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type EmbeddingUsage struct {
 | 
			
		||||
	PromptTokens int `json:"PromptTokens"`
 | 
			
		||||
	TotalTokens  int `json:"TotalTokens"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type EmbeddingResponse struct {
 | 
			
		||||
	Data           []EmbeddingData `json:"Data"`
 | 
			
		||||
	EmbeddingUsage EmbeddingUsage  `json:"Usage,omitempty"`
 | 
			
		||||
	RequestId      string          `json:"RequestId,omitempty"`
 | 
			
		||||
	Error          Error           `json:"Error,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type EmbeddingResponseP struct {
 | 
			
		||||
	Response EmbeddingResponse `json:"Response,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -18,7 +18,8 @@ var ModelList = []string{
 | 
			
		||||
	"gemini-pro", "gemini-pro-vision",
 | 
			
		||||
	"gemini-1.5-pro-001", "gemini-1.5-flash-001",
 | 
			
		||||
	"gemini-1.5-pro-002", "gemini-1.5-flash-002",
 | 
			
		||||
	"gemini-2.0-flash-exp", "gemini-2.0-flash-thinking-exp",
 | 
			
		||||
	"gemini-2.0-flash-exp",
 | 
			
		||||
	"gemini-2.0-flash-thinking-exp", "gemini-2.0-flash-thinking-exp-01-21",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Adaptor struct {
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@ package billing
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	"github.com/songquanpeng/one-api/common/logger"
 | 
			
		||||
	"github.com/songquanpeng/one-api/model"
 | 
			
		||||
)
 | 
			
		||||
@@ -31,8 +32,17 @@ func PostConsumeQuota(ctx context.Context, tokenId int, quotaDelta int64, totalQ
 | 
			
		||||
	}
 | 
			
		||||
	// totalQuota is total quota consumed
 | 
			
		||||
	if totalQuota != 0 {
 | 
			
		||||
		logContent := fmt.Sprintf("模型倍率 %.2f,分组倍率 %.2f", modelRatio, groupRatio)
 | 
			
		||||
		model.RecordConsumeLog(ctx, userId, channelId, int(totalQuota), 0, modelName, tokenName, totalQuota, logContent)
 | 
			
		||||
		logContent := fmt.Sprintf("倍率:%.2f × %.2f", modelRatio, groupRatio)
 | 
			
		||||
		model.RecordConsumeLog(ctx, &model.Log{
 | 
			
		||||
			UserId:           userId,
 | 
			
		||||
			ChannelId:        channelId,
 | 
			
		||||
			PromptTokens:     int(totalQuota),
 | 
			
		||||
			CompletionTokens: 0,
 | 
			
		||||
			ModelName:        modelName,
 | 
			
		||||
			TokenName:        tokenName,
 | 
			
		||||
			Quota:            int(totalQuota),
 | 
			
		||||
			Content:          logContent,
 | 
			
		||||
		})
 | 
			
		||||
		model.UpdateUserUsedQuotaAndRequestCount(userId, totalQuota)
 | 
			
		||||
		model.UpdateChannelUsedQuota(channelId, totalQuota)
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -9,9 +9,10 @@ import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	USD2RMB = 7
 | 
			
		||||
	USD     = 500 // $0.002 = 1 -> $1 = 500
 | 
			
		||||
	RMB     = USD / USD2RMB
 | 
			
		||||
	USD2RMB   = 7
 | 
			
		||||
	USD       = 500 // $0.002 = 1 -> $1 = 500
 | 
			
		||||
	MILLI_USD = 1.0 / 1000 * USD
 | 
			
		||||
	RMB       = USD / USD2RMB
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// ModelRatio
 | 
			
		||||
@@ -109,15 +110,16 @@ var ModelRatio = map[string]float64{
 | 
			
		||||
	"bge-large-en":       0.002 * RMB,
 | 
			
		||||
	"tao-8k":             0.002 * RMB,
 | 
			
		||||
	// https://ai.google.dev/pricing
 | 
			
		||||
	"gemini-pro":                    1, // $0.00025 / 1k characters -> $0.001 / 1k tokens
 | 
			
		||||
	"gemini-1.0-pro":                1,
 | 
			
		||||
	"gemini-1.5-pro":                1,
 | 
			
		||||
	"gemini-1.5-pro-001":            1,
 | 
			
		||||
	"gemini-1.5-flash":              1,
 | 
			
		||||
	"gemini-1.5-flash-001":          1,
 | 
			
		||||
	"gemini-2.0-flash-exp":          1,
 | 
			
		||||
	"gemini-2.0-flash-thinking-exp": 1,
 | 
			
		||||
	"aqa":                           1,
 | 
			
		||||
	"gemini-pro":                          1, // $0.00025 / 1k characters -> $0.001 / 1k tokens
 | 
			
		||||
	"gemini-1.0-pro":                      1,
 | 
			
		||||
	"gemini-1.5-pro":                      1,
 | 
			
		||||
	"gemini-1.5-pro-001":                  1,
 | 
			
		||||
	"gemini-1.5-flash":                    1,
 | 
			
		||||
	"gemini-1.5-flash-001":                1,
 | 
			
		||||
	"gemini-2.0-flash-exp":                1,
 | 
			
		||||
	"gemini-2.0-flash-thinking-exp":       1,
 | 
			
		||||
	"gemini-2.0-flash-thinking-exp-01-21": 1,
 | 
			
		||||
	"aqa":                                 1,
 | 
			
		||||
	// https://open.bigmodel.cn/pricing
 | 
			
		||||
	"glm-4":         0.1 * RMB,
 | 
			
		||||
	"glm-4v":        0.1 * RMB,
 | 
			
		||||
@@ -279,8 +281,8 @@ var ModelRatio = map[string]float64{
 | 
			
		||||
	"command-r":             0.5 / 1000 * USD,
 | 
			
		||||
	"command-r-plus":        3.0 / 1000 * USD,
 | 
			
		||||
	// https://platform.deepseek.com/api-docs/pricing/
 | 
			
		||||
	"deepseek-chat":  1.0 / 1000 * RMB,
 | 
			
		||||
	"deepseek-coder": 1.0 / 1000 * RMB,
 | 
			
		||||
	"deepseek-chat":     0.14 * MILLI_USD,
 | 
			
		||||
	"deepseek-reasoner": 0.55 * MILLI_USD,
 | 
			
		||||
	// https://www.deepl.com/pro?cta=header-prices
 | 
			
		||||
	"deepl-zh": 25.0 / 1000 * USD,
 | 
			
		||||
	"deepl-en": 25.0 / 1000 * USD,
 | 
			
		||||
@@ -337,6 +339,11 @@ var CompletionRatio = map[string]float64{
 | 
			
		||||
	// aws llama3
 | 
			
		||||
	"llama3-8b-8192(33)":  0.0006 / 0.0003,
 | 
			
		||||
	"llama3-70b-8192(33)": 0.0035 / 0.00265,
 | 
			
		||||
	// whisper
 | 
			
		||||
	"whisper-1": 0, // only count input tokens
 | 
			
		||||
	// deepseek
 | 
			
		||||
	"deepseek-chat":     0.28 / 0.14,
 | 
			
		||||
	"deepseek-reasoner": 2.19 / 0.55,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
 
 | 
			
		||||
@@ -4,12 +4,15 @@ import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/songquanpeng/one-api/relay/constant/role"
 | 
			
		||||
	"math"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/songquanpeng/one-api/common/helper"
 | 
			
		||||
	"github.com/songquanpeng/one-api/relay/constant/role"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
 | 
			
		||||
	"github.com/songquanpeng/one-api/common"
 | 
			
		||||
	"github.com/songquanpeng/one-api/common/config"
 | 
			
		||||
	"github.com/songquanpeng/one-api/common/logger"
 | 
			
		||||
@@ -119,12 +122,20 @@ func postConsumeQuota(ctx context.Context, usage *relaymodel.Usage, meta *meta.M
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Error(ctx, "error update user quota cache: "+err.Error())
 | 
			
		||||
	}
 | 
			
		||||
	var extraLog string
 | 
			
		||||
	if systemPromptReset {
 | 
			
		||||
		extraLog = " (注意系统提示词已被重置)"
 | 
			
		||||
	}
 | 
			
		||||
	logContent := fmt.Sprintf("模型倍率 %.2f,分组倍率 %.2f,补全倍率 %.2f%s", modelRatio, groupRatio, completionRatio, extraLog)
 | 
			
		||||
	model.RecordConsumeLog(ctx, meta.UserId, meta.ChannelId, promptTokens, completionTokens, textRequest.Model, meta.TokenName, quota, logContent)
 | 
			
		||||
	logContent := fmt.Sprintf("倍率:%.2f × %.2f × %.2f", modelRatio, groupRatio, completionRatio)
 | 
			
		||||
	model.RecordConsumeLog(ctx, &model.Log{
 | 
			
		||||
		UserId:            meta.UserId,
 | 
			
		||||
		ChannelId:         meta.ChannelId,
 | 
			
		||||
		PromptTokens:      promptTokens,
 | 
			
		||||
		CompletionTokens:  completionTokens,
 | 
			
		||||
		ModelName:         textRequest.Model,
 | 
			
		||||
		TokenName:         meta.TokenName,
 | 
			
		||||
		Quota:             int(quota),
 | 
			
		||||
		Content:           logContent,
 | 
			
		||||
		IsStream:          meta.IsStream,
 | 
			
		||||
		ElapsedTime:       helper.CalcElapsedTime(meta.StartTime),
 | 
			
		||||
		SystemPromptReset: systemPromptReset,
 | 
			
		||||
	})
 | 
			
		||||
	model.UpdateUserUsedQuotaAndRequestCount(meta.UserId, quota)
 | 
			
		||||
	model.UpdateChannelUsedQuota(meta.ChannelId, quota)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,7 @@ import (
 | 
			
		||||
	"net/http"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
 | 
			
		||||
	"github.com/songquanpeng/one-api/common"
 | 
			
		||||
	"github.com/songquanpeng/one-api/common/ctxkey"
 | 
			
		||||
	"github.com/songquanpeng/one-api/common/logger"
 | 
			
		||||
@@ -209,8 +210,17 @@ func RelayImageHelper(c *gin.Context, relayMode int) *relaymodel.ErrorWithStatus
 | 
			
		||||
		}
 | 
			
		||||
		if quota != 0 {
 | 
			
		||||
			tokenName := c.GetString(ctxkey.TokenName)
 | 
			
		||||
			logContent := fmt.Sprintf("模型倍率 %.2f,分组倍率 %.2f", modelRatio, groupRatio)
 | 
			
		||||
			model.RecordConsumeLog(ctx, meta.UserId, meta.ChannelId, 0, 0, imageRequest.Model, tokenName, quota, logContent)
 | 
			
		||||
			logContent := fmt.Sprintf("倍率:%.2f × %.2f", modelRatio, groupRatio)
 | 
			
		||||
			model.RecordConsumeLog(ctx, &model.Log{
 | 
			
		||||
				UserId:           meta.UserId,
 | 
			
		||||
				ChannelId:        meta.ChannelId,
 | 
			
		||||
				PromptTokens:     0,
 | 
			
		||||
				CompletionTokens: 0,
 | 
			
		||||
				ModelName:        imageRequest.Model,
 | 
			
		||||
				TokenName:        tokenName,
 | 
			
		||||
				Quota:            int(quota),
 | 
			
		||||
				Content:          logContent,
 | 
			
		||||
			})
 | 
			
		||||
			model.UpdateUserUsedQuotaAndRequestCount(meta.UserId, quota)
 | 
			
		||||
			channelId := c.GetInt(ctxkey.ChannelId)
 | 
			
		||||
			model.UpdateChannelUsedQuota(channelId, quota)
 | 
			
		||||
 
 | 
			
		||||
@@ -4,11 +4,12 @@ import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/songquanpeng/one-api/common/config"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
 | 
			
		||||
	"github.com/songquanpeng/one-api/common/config"
 | 
			
		||||
	"github.com/songquanpeng/one-api/common/logger"
 | 
			
		||||
	"github.com/songquanpeng/one-api/relay"
 | 
			
		||||
	"github.com/songquanpeng/one-api/relay/adaptor"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,15 @@
 | 
			
		||||
package meta
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
 | 
			
		||||
	"github.com/songquanpeng/one-api/common/ctxkey"
 | 
			
		||||
	"github.com/songquanpeng/one-api/model"
 | 
			
		||||
	"github.com/songquanpeng/one-api/relay/channeltype"
 | 
			
		||||
	"github.com/songquanpeng/one-api/relay/relaymode"
 | 
			
		||||
	"strings"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Meta struct {
 | 
			
		||||
@@ -31,6 +34,7 @@ type Meta struct {
 | 
			
		||||
	RequestURLPath  string
 | 
			
		||||
	PromptTokens    int // only for DoResponse
 | 
			
		||||
	SystemPrompt    string
 | 
			
		||||
	StartTime       time.Time
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetByContext(c *gin.Context) *Meta {
 | 
			
		||||
@@ -48,6 +52,7 @@ func GetByContext(c *gin.Context) *Meta {
 | 
			
		||||
		APIKey:          strings.TrimPrefix(c.Request.Header.Get("Authorization"), "Bearer "),
 | 
			
		||||
		RequestURLPath:  c.Request.URL.String(),
 | 
			
		||||
		SystemPrompt:    c.GetString(ctxkey.SystemPrompt),
 | 
			
		||||
		StartTime:       time.Now(),
 | 
			
		||||
	}
 | 
			
		||||
	cfg, ok := c.Get(ctxkey.Config)
 | 
			
		||||
	if ok {
 | 
			
		||||
 
 | 
			
		||||
@@ -28,6 +28,8 @@ function renderType(type) {
 | 
			
		||||
      return <Tag color="orange" size="large"> 管理 </Tag>;
 | 
			
		||||
    case 4:
 | 
			
		||||
      return <Tag color="purple" size="large"> 系统 </Tag>;
 | 
			
		||||
    case 5:
 | 
			
		||||
      return <Tag color="violet" size="large"> 测试 </Tag>;
 | 
			
		||||
    default:
 | 
			
		||||
      return <Tag color="black" size="large"> 未知 </Tag>;
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,247 +1,260 @@
 | 
			
		||||
import { enqueueSnackbar } from 'notistack';
 | 
			
		||||
import { snackbarConstants } from 'constants/SnackbarConstants';
 | 
			
		||||
import { API } from './api';
 | 
			
		||||
import {enqueueSnackbar} from 'notistack';
 | 
			
		||||
import {snackbarConstants} from 'constants/SnackbarConstants';
 | 
			
		||||
import {API} from './api';
 | 
			
		||||
 | 
			
		||||
export function getSystemName() {
 | 
			
		||||
  let system_name = localStorage.getItem('system_name');
 | 
			
		||||
  if (!system_name) return 'One API';
 | 
			
		||||
  return system_name;
 | 
			
		||||
    let system_name = localStorage.getItem('system_name');
 | 
			
		||||
    if (!system_name) return 'One API';
 | 
			
		||||
    return system_name;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function isMobile() {
 | 
			
		||||
  return window.innerWidth <= 600;
 | 
			
		||||
    return window.innerWidth <= 600;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line
 | 
			
		||||
export function SnackbarHTMLContent({ htmlContent }) {
 | 
			
		||||
  return <div dangerouslySetInnerHTML={{ __html: htmlContent }} />;
 | 
			
		||||
export function SnackbarHTMLContent({htmlContent}) {
 | 
			
		||||
    return <div dangerouslySetInnerHTML={{__html: htmlContent}}/>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getSnackbarOptions(variant) {
 | 
			
		||||
  let options = snackbarConstants.Common[variant];
 | 
			
		||||
  if (isMobile()) {
 | 
			
		||||
    // 合并 options 和 snackbarConstants.Mobile
 | 
			
		||||
    options = { ...options, ...snackbarConstants.Mobile };
 | 
			
		||||
  }
 | 
			
		||||
  return options;
 | 
			
		||||
    let options = snackbarConstants.Common[variant];
 | 
			
		||||
    if (isMobile()) {
 | 
			
		||||
        // 合并 options 和 snackbarConstants.Mobile
 | 
			
		||||
        options = {...options, ...snackbarConstants.Mobile};
 | 
			
		||||
    }
 | 
			
		||||
    return options;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function showError(error) {
 | 
			
		||||
  if (error.message) {
 | 
			
		||||
    if (error.name === 'AxiosError') {
 | 
			
		||||
      switch (error.response.status) {
 | 
			
		||||
        case 429:
 | 
			
		||||
          enqueueSnackbar('错误:请求次数过多,请稍后再试!', getSnackbarOptions('ERROR'));
 | 
			
		||||
          break;
 | 
			
		||||
        case 500:
 | 
			
		||||
          enqueueSnackbar('错误:服务器内部错误,请联系管理员!', getSnackbarOptions('ERROR'));
 | 
			
		||||
          break;
 | 
			
		||||
        case 405:
 | 
			
		||||
          enqueueSnackbar('本站仅作演示之用,无服务端!', getSnackbarOptions('INFO'));
 | 
			
		||||
          break;
 | 
			
		||||
        default:
 | 
			
		||||
          enqueueSnackbar('错误:' + error.message, getSnackbarOptions('ERROR'));
 | 
			
		||||
      }
 | 
			
		||||
      return;
 | 
			
		||||
    if (error.message) {
 | 
			
		||||
        if (error.name === 'AxiosError') {
 | 
			
		||||
            switch (error.response.status) {
 | 
			
		||||
                case 429:
 | 
			
		||||
                    enqueueSnackbar('错误:请求次数过多,请稍后再试!', getSnackbarOptions('ERROR'));
 | 
			
		||||
                    break;
 | 
			
		||||
                case 500:
 | 
			
		||||
                    enqueueSnackbar('错误:服务器内部错误,请联系管理员!', getSnackbarOptions('ERROR'));
 | 
			
		||||
                    break;
 | 
			
		||||
                case 405:
 | 
			
		||||
                    enqueueSnackbar('本站仅作演示之用,无服务端!', getSnackbarOptions('INFO'));
 | 
			
		||||
                    break;
 | 
			
		||||
                default:
 | 
			
		||||
                    enqueueSnackbar('错误:' + error.message, getSnackbarOptions('ERROR'));
 | 
			
		||||
            }
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
    } else {
 | 
			
		||||
        enqueueSnackbar('错误:' + error, getSnackbarOptions('ERROR'));
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    enqueueSnackbar('错误:' + error, getSnackbarOptions('ERROR'));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function showNotice(message, isHTML = false) {
 | 
			
		||||
  if (isHTML) {
 | 
			
		||||
    enqueueSnackbar(<SnackbarHTMLContent htmlContent={message} />, getSnackbarOptions('NOTICE'));
 | 
			
		||||
  } else {
 | 
			
		||||
    enqueueSnackbar(message, getSnackbarOptions('NOTICE'));
 | 
			
		||||
  }
 | 
			
		||||
    if (isHTML) {
 | 
			
		||||
        enqueueSnackbar(<SnackbarHTMLContent htmlContent={message}/>, getSnackbarOptions('NOTICE'));
 | 
			
		||||
    } else {
 | 
			
		||||
        enqueueSnackbar(message, getSnackbarOptions('NOTICE'));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function showWarning(message) {
 | 
			
		||||
  enqueueSnackbar(message, getSnackbarOptions('WARNING'));
 | 
			
		||||
    enqueueSnackbar(message, getSnackbarOptions('WARNING'));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function showSuccess(message) {
 | 
			
		||||
  enqueueSnackbar(message, getSnackbarOptions('SUCCESS'));
 | 
			
		||||
    enqueueSnackbar(message, getSnackbarOptions('SUCCESS'));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function showInfo(message) {
 | 
			
		||||
  enqueueSnackbar(message, getSnackbarOptions('INFO'));
 | 
			
		||||
    enqueueSnackbar(message, getSnackbarOptions('INFO'));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function getOAuthState() {
 | 
			
		||||
  const res = await API.get('/api/oauth/state');
 | 
			
		||||
  const { success, message, data } = res.data;
 | 
			
		||||
  if (success) {
 | 
			
		||||
    return data;
 | 
			
		||||
  } else {
 | 
			
		||||
    showError(message);
 | 
			
		||||
    return '';
 | 
			
		||||
  }
 | 
			
		||||
    const res = await API.get('/api/oauth/state');
 | 
			
		||||
    const {success, message, data} = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
        return data;
 | 
			
		||||
    } else {
 | 
			
		||||
        showError(message);
 | 
			
		||||
        return '';
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function onGitHubOAuthClicked(github_client_id, openInNewTab = false) {
 | 
			
		||||
  const state = await getOAuthState();
 | 
			
		||||
  if (!state) return;
 | 
			
		||||
  let url = `https://github.com/login/oauth/authorize?client_id=${github_client_id}&state=${state}&scope=user:email`;
 | 
			
		||||
  if (openInNewTab) {
 | 
			
		||||
    window.open(url);
 | 
			
		||||
  } else {
 | 
			
		||||
    window.location.href = url;
 | 
			
		||||
  }
 | 
			
		||||
    const state = await getOAuthState();
 | 
			
		||||
    if (!state) return;
 | 
			
		||||
    let url = `https://github.com/login/oauth/authorize?client_id=${github_client_id}&state=${state}&scope=user:email`;
 | 
			
		||||
    if (openInNewTab) {
 | 
			
		||||
        window.open(url);
 | 
			
		||||
    } else {
 | 
			
		||||
        window.location.href = url;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function onLarkOAuthClicked(lark_client_id) {
 | 
			
		||||
  const state = await getOAuthState();
 | 
			
		||||
  if (!state) return;
 | 
			
		||||
  let redirect_uri = `${window.location.origin}/oauth/lark`;
 | 
			
		||||
  window.open(`https://accounts.feishu.cn/open-apis/authen/v1/authorize?redirect_uri=${redirect_uri}&client_id=${lark_client_id}&state=${state}`);
 | 
			
		||||
    const state = await getOAuthState();
 | 
			
		||||
    if (!state) return;
 | 
			
		||||
    let redirect_uri = `${window.location.origin}/oauth/lark`;
 | 
			
		||||
    window.open(`https://accounts.feishu.cn/open-apis/authen/v1/authorize?redirect_uri=${redirect_uri}&client_id=${lark_client_id}&state=${state}`);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function onOidcClicked(auth_url, client_id, openInNewTab = false) {
 | 
			
		||||
  const state = await getOAuthState();
 | 
			
		||||
  if (!state) return;
 | 
			
		||||
  const redirect_uri = `${window.location.origin}/oauth/oidc`;
 | 
			
		||||
  const response_type = "code";
 | 
			
		||||
  const scope = "openid profile email";
 | 
			
		||||
  const url = `${auth_url}?client_id=${client_id}&redirect_uri=${redirect_uri}&response_type=${response_type}&scope=${scope}&state=${state}`;
 | 
			
		||||
  if (openInNewTab) {
 | 
			
		||||
    window.open(url);
 | 
			
		||||
  } else
 | 
			
		||||
  {
 | 
			
		||||
    window.location.href = url;
 | 
			
		||||
  }
 | 
			
		||||
    const state = await getOAuthState();
 | 
			
		||||
    if (!state) return;
 | 
			
		||||
    const redirect_uri = `${window.location.origin}/oauth/oidc`;
 | 
			
		||||
    const response_type = "code";
 | 
			
		||||
    const scope = "openid profile email";
 | 
			
		||||
    const url = `${auth_url}?client_id=${client_id}&redirect_uri=${redirect_uri}&response_type=${response_type}&scope=${scope}&state=${state}`;
 | 
			
		||||
    if (openInNewTab) {
 | 
			
		||||
        window.open(url);
 | 
			
		||||
    } else {
 | 
			
		||||
        window.location.href = url;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function isAdmin() {
 | 
			
		||||
  let user = localStorage.getItem('user');
 | 
			
		||||
  if (!user) return false;
 | 
			
		||||
  user = JSON.parse(user);
 | 
			
		||||
  return user.role >= 10;
 | 
			
		||||
    let user = localStorage.getItem('user');
 | 
			
		||||
    if (!user) return false;
 | 
			
		||||
    user = JSON.parse(user);
 | 
			
		||||
    return user.role >= 10;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function timestamp2string(timestamp) {
 | 
			
		||||
  let date = new Date(timestamp * 1000);
 | 
			
		||||
  let year = date.getFullYear().toString();
 | 
			
		||||
  let month = (date.getMonth() + 1).toString();
 | 
			
		||||
  let day = date.getDate().toString();
 | 
			
		||||
  let hour = date.getHours().toString();
 | 
			
		||||
  let minute = date.getMinutes().toString();
 | 
			
		||||
  let second = date.getSeconds().toString();
 | 
			
		||||
  if (month.length === 1) {
 | 
			
		||||
    month = '0' + month;
 | 
			
		||||
  }
 | 
			
		||||
  if (day.length === 1) {
 | 
			
		||||
    day = '0' + day;
 | 
			
		||||
  }
 | 
			
		||||
  if (hour.length === 1) {
 | 
			
		||||
    hour = '0' + hour;
 | 
			
		||||
  }
 | 
			
		||||
  if (minute.length === 1) {
 | 
			
		||||
    minute = '0' + minute;
 | 
			
		||||
  }
 | 
			
		||||
  if (second.length === 1) {
 | 
			
		||||
    second = '0' + second;
 | 
			
		||||
  }
 | 
			
		||||
  return year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ':' + second;
 | 
			
		||||
    let date = new Date(timestamp * 1000);
 | 
			
		||||
    let year = date.getFullYear().toString();
 | 
			
		||||
    let month = (date.getMonth() + 1).toString();
 | 
			
		||||
    let day = date.getDate().toString();
 | 
			
		||||
    let hour = date.getHours().toString();
 | 
			
		||||
    let minute = date.getMinutes().toString();
 | 
			
		||||
    let second = date.getSeconds().toString();
 | 
			
		||||
    if (month.length === 1) {
 | 
			
		||||
        month = '0' + month;
 | 
			
		||||
    }
 | 
			
		||||
    if (day.length === 1) {
 | 
			
		||||
        day = '0' + day;
 | 
			
		||||
    }
 | 
			
		||||
    if (hour.length === 1) {
 | 
			
		||||
        hour = '0' + hour;
 | 
			
		||||
    }
 | 
			
		||||
    if (minute.length === 1) {
 | 
			
		||||
        minute = '0' + minute;
 | 
			
		||||
    }
 | 
			
		||||
    if (second.length === 1) {
 | 
			
		||||
        second = '0' + second;
 | 
			
		||||
    }
 | 
			
		||||
    return year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ':' + second;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function calculateQuota(quota, digits = 2) {
 | 
			
		||||
  let quotaPerUnit = localStorage.getItem('quota_per_unit');
 | 
			
		||||
  quotaPerUnit = parseFloat(quotaPerUnit);
 | 
			
		||||
    let quotaPerUnit = localStorage.getItem('quota_per_unit');
 | 
			
		||||
    quotaPerUnit = parseFloat(quotaPerUnit);
 | 
			
		||||
 | 
			
		||||
  return (quota / quotaPerUnit).toFixed(digits);
 | 
			
		||||
    return (quota / quotaPerUnit).toFixed(digits);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function renderQuota(quota, digits = 2) {
 | 
			
		||||
  let displayInCurrency = localStorage.getItem('display_in_currency');
 | 
			
		||||
  displayInCurrency = displayInCurrency === 'true';
 | 
			
		||||
  if (displayInCurrency) {
 | 
			
		||||
    return '$' + calculateQuota(quota, digits);
 | 
			
		||||
  }
 | 
			
		||||
  return renderNumber(quota);
 | 
			
		||||
    let displayInCurrency = localStorage.getItem('display_in_currency');
 | 
			
		||||
    displayInCurrency = displayInCurrency === 'true';
 | 
			
		||||
    if (displayInCurrency) {
 | 
			
		||||
        return '$' + calculateQuota(quota, digits);
 | 
			
		||||
    }
 | 
			
		||||
    return renderNumber(quota);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const verifyJSON = (str) => {
 | 
			
		||||
  try {
 | 
			
		||||
    JSON.parse(str);
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
  return true;
 | 
			
		||||
    try {
 | 
			
		||||
        JSON.parse(str);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
    return true;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function renderNumber(num) {
 | 
			
		||||
  if (num >= 1000000000) {
 | 
			
		||||
    return (num / 1000000000).toFixed(1) + 'B';
 | 
			
		||||
  } else if (num >= 1000000) {
 | 
			
		||||
    return (num / 1000000).toFixed(1) + 'M';
 | 
			
		||||
  } else if (num >= 10000) {
 | 
			
		||||
    return (num / 1000).toFixed(1) + 'k';
 | 
			
		||||
  } else {
 | 
			
		||||
    return num;
 | 
			
		||||
  }
 | 
			
		||||
    if (num >= 1000000000) {
 | 
			
		||||
        return (num / 1000000000).toFixed(1) + 'B';
 | 
			
		||||
    } else if (num >= 1000000) {
 | 
			
		||||
        return (num / 1000000).toFixed(1) + 'M';
 | 
			
		||||
    } else if (num >= 10000) {
 | 
			
		||||
        return (num / 1000).toFixed(1) + 'k';
 | 
			
		||||
    } else {
 | 
			
		||||
        return num;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function renderQuotaWithPrompt(quota, digits) {
 | 
			
		||||
  let displayInCurrency = localStorage.getItem('display_in_currency');
 | 
			
		||||
  displayInCurrency = displayInCurrency === 'true';
 | 
			
		||||
  if (displayInCurrency) {
 | 
			
		||||
    return `(等价金额:${renderQuota(quota, digits)})`;
 | 
			
		||||
  }
 | 
			
		||||
  return '';
 | 
			
		||||
    let displayInCurrency = localStorage.getItem('display_in_currency');
 | 
			
		||||
    displayInCurrency = displayInCurrency === 'true';
 | 
			
		||||
    if (displayInCurrency) {
 | 
			
		||||
        return `(等价金额:${renderQuota(quota, digits)})`;
 | 
			
		||||
    }
 | 
			
		||||
    return '';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function downloadTextAsFile(text, filename) {
 | 
			
		||||
  let blob = new Blob([text], { type: 'text/plain;charset=utf-8' });
 | 
			
		||||
  let url = URL.createObjectURL(blob);
 | 
			
		||||
  let a = document.createElement('a');
 | 
			
		||||
  a.href = url;
 | 
			
		||||
  a.download = filename;
 | 
			
		||||
  a.click();
 | 
			
		||||
    let blob = new Blob([text], {type: 'text/plain;charset=utf-8'});
 | 
			
		||||
    let url = URL.createObjectURL(blob);
 | 
			
		||||
    let a = document.createElement('a');
 | 
			
		||||
    a.href = url;
 | 
			
		||||
    a.download = filename;
 | 
			
		||||
    a.click();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function removeTrailingSlash(url) {
 | 
			
		||||
  if (url.endsWith('/')) {
 | 
			
		||||
    return url.slice(0, -1);
 | 
			
		||||
  } else {
 | 
			
		||||
    return url;
 | 
			
		||||
  }
 | 
			
		||||
    if (url.endsWith('/')) {
 | 
			
		||||
        return url.slice(0, -1);
 | 
			
		||||
    } else {
 | 
			
		||||
        return url;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let channelModels = undefined;
 | 
			
		||||
 | 
			
		||||
export async function loadChannelModels() {
 | 
			
		||||
  const res = await API.get('/api/models');
 | 
			
		||||
  const { success, data } = res.data;
 | 
			
		||||
  if (!success) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  channelModels = data;
 | 
			
		||||
  localStorage.setItem('channel_models', JSON.stringify(data));
 | 
			
		||||
    const res = await API.get('/api/models');
 | 
			
		||||
    const {success, data} = res.data;
 | 
			
		||||
    if (!success) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    channelModels = data;
 | 
			
		||||
    localStorage.setItem('channel_models', JSON.stringify(data));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getChannelModels(type) {
 | 
			
		||||
  if (channelModels !== undefined && type in channelModels) {
 | 
			
		||||
    return channelModels[type];
 | 
			
		||||
  }
 | 
			
		||||
  let models = localStorage.getItem('channel_models');
 | 
			
		||||
  if (!models) {
 | 
			
		||||
    if (channelModels !== undefined && type in channelModels) {
 | 
			
		||||
        return channelModels[type];
 | 
			
		||||
    }
 | 
			
		||||
    let models = localStorage.getItem('channel_models');
 | 
			
		||||
    if (!models) {
 | 
			
		||||
        return [];
 | 
			
		||||
    }
 | 
			
		||||
    channelModels = JSON.parse(models);
 | 
			
		||||
    if (type in channelModels) {
 | 
			
		||||
        return channelModels[type];
 | 
			
		||||
    }
 | 
			
		||||
    return [];
 | 
			
		||||
  }
 | 
			
		||||
  channelModels = JSON.parse(models);
 | 
			
		||||
  if (type in channelModels) {
 | 
			
		||||
    return channelModels[type];
 | 
			
		||||
  }
 | 
			
		||||
  return [];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function copy(text, name = '') {
 | 
			
		||||
  try {
 | 
			
		||||
    navigator.clipboard.writeText(text);
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    text = `复制${name}失败,请手动复制:<br /><br />${text}`;
 | 
			
		||||
    enqueueSnackbar(<SnackbarHTMLContent htmlContent={text} />, getSnackbarOptions('COPY'));
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  showSuccess(`复制${name}成功!`);
 | 
			
		||||
    if (navigator.clipboard && navigator.clipboard.writeText) {
 | 
			
		||||
        navigator.clipboard.writeText(text).then(() => {
 | 
			
		||||
            showNotice(`复制${name}成功!`, true);
 | 
			
		||||
        }, () => {
 | 
			
		||||
            text = `复制${name}失败,请手动复制:<br /><br />${text}`;
 | 
			
		||||
            enqueueSnackbar(<SnackbarHTMLContent htmlContent={text}/>, getSnackbarOptions('COPY'));
 | 
			
		||||
        });
 | 
			
		||||
    } else {
 | 
			
		||||
        const textArea = document.createElement("textarea");
 | 
			
		||||
        textArea.value = text;
 | 
			
		||||
        document.body.appendChild(textArea);
 | 
			
		||||
        textArea.select();
 | 
			
		||||
        try {
 | 
			
		||||
            document.execCommand('copy');
 | 
			
		||||
            showNotice(`复制${name}成功!`, true);
 | 
			
		||||
        } catch (err) {
 | 
			
		||||
            text = `复制${name}失败,请手动复制:<br /><br />${text}`;
 | 
			
		||||
            enqueueSnackbar(<SnackbarHTMLContent htmlContent={text}/>, getSnackbarOptions('COPY'));
 | 
			
		||||
        }
 | 
			
		||||
        document.body.removeChild(textArea);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,8 @@ const LOG_TYPE = {
 | 
			
		||||
  1: { value: '1', text: '充值', color: 'primary' },
 | 
			
		||||
  2: { value: '2', text: '消费', color: 'orange' },
 | 
			
		||||
  3: { value: '3', text: '管理', color: 'default' },
 | 
			
		||||
  4: { value: '4', text: '系统', color: 'secondary' }
 | 
			
		||||
  4: { value: '4', text: '系统', color: 'secondary' },
 | 
			
		||||
  5: { value: '5', text: '测试', color: 'secondary' },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default LOG_TYPE;
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,7 @@
 | 
			
		||||
    "react-scripts": "5.0.1",
 | 
			
		||||
    "react-toastify": "^9.0.8",
 | 
			
		||||
    "react-turnstile": "^1.0.5",
 | 
			
		||||
    "recharts": "^2.15.1",
 | 
			
		||||
    "semantic-ui-css": "^2.5.0",
 | 
			
		||||
    "semantic-ui-react": "^2.1.3"
 | 
			
		||||
  },
 | 
			
		||||
 
 | 
			
		||||
@@ -25,6 +25,7 @@ import TopUp from './pages/TopUp';
 | 
			
		||||
import Log from './pages/Log';
 | 
			
		||||
import Chat from './pages/Chat';
 | 
			
		||||
import LarkOAuth from './components/LarkOAuth';
 | 
			
		||||
import Dashboard from './pages/Dashboard';
 | 
			
		||||
 | 
			
		||||
const Home = lazy(() => import('./pages/Home'));
 | 
			
		||||
const About = lazy(() => import('./pages/About'));
 | 
			
		||||
@@ -261,11 +262,11 @@ function App() {
 | 
			
		||||
      <Route
 | 
			
		||||
        path='/topup'
 | 
			
		||||
        element={
 | 
			
		||||
        <PrivateRoute>
 | 
			
		||||
          <Suspense fallback={<Loading></Loading>}>
 | 
			
		||||
            <TopUp />
 | 
			
		||||
          </Suspense>
 | 
			
		||||
        </PrivateRoute>
 | 
			
		||||
          <PrivateRoute>
 | 
			
		||||
            <Suspense fallback={<Loading></Loading>}>
 | 
			
		||||
              <TopUp />
 | 
			
		||||
            </Suspense>
 | 
			
		||||
          </PrivateRoute>
 | 
			
		||||
        }
 | 
			
		||||
      />
 | 
			
		||||
      <Route
 | 
			
		||||
@@ -292,9 +293,15 @@ function App() {
 | 
			
		||||
          </Suspense>
 | 
			
		||||
        }
 | 
			
		||||
      />
 | 
			
		||||
      <Route path='*' element={
 | 
			
		||||
          <NotFound />
 | 
			
		||||
      } />
 | 
			
		||||
      <Route
 | 
			
		||||
        path='/dashboard'
 | 
			
		||||
        element={
 | 
			
		||||
          <PrivateRoute>
 | 
			
		||||
            <Dashboard />
 | 
			
		||||
          </PrivateRoute>
 | 
			
		||||
        }
 | 
			
		||||
      />
 | 
			
		||||
      <Route path='*' element={<NotFound />} />
 | 
			
		||||
    </Routes>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,15 @@
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import { Button, Dropdown, Form, Input, Label, Message, Pagination, Popup, Table } from 'semantic-ui-react';
 | 
			
		||||
import {
 | 
			
		||||
  Button,
 | 
			
		||||
  Dropdown,
 | 
			
		||||
  Form,
 | 
			
		||||
  Input,
 | 
			
		||||
  Label,
 | 
			
		||||
  Message,
 | 
			
		||||
  Pagination,
 | 
			
		||||
  Popup,
 | 
			
		||||
  Table,
 | 
			
		||||
} from 'semantic-ui-react';
 | 
			
		||||
import { Link } from 'react-router-dom';
 | 
			
		||||
import {
 | 
			
		||||
  API,
 | 
			
		||||
@@ -9,31 +19,31 @@ import {
 | 
			
		||||
  showError,
 | 
			
		||||
  showInfo,
 | 
			
		||||
  showSuccess,
 | 
			
		||||
  timestamp2string
 | 
			
		||||
  timestamp2string,
 | 
			
		||||
} from '../helpers';
 | 
			
		||||
 | 
			
		||||
import { CHANNEL_OPTIONS, ITEMS_PER_PAGE } from '../constants';
 | 
			
		||||
import { renderGroup, renderNumber } from '../helpers/render';
 | 
			
		||||
 | 
			
		||||
function renderTimestamp(timestamp) {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      {timestamp2string(timestamp)}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
  return <>{timestamp2string(timestamp)}</>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let type2label = undefined;
 | 
			
		||||
 | 
			
		||||
function renderType(type) {
 | 
			
		||||
  if (!type2label) {
 | 
			
		||||
    type2label = new Map;
 | 
			
		||||
    type2label = new Map();
 | 
			
		||||
    for (let i = 0; i < CHANNEL_OPTIONS.length; i++) {
 | 
			
		||||
      type2label[CHANNEL_OPTIONS[i].value] = CHANNEL_OPTIONS[i];
 | 
			
		||||
    }
 | 
			
		||||
    type2label[0] = { value: 0, text: '未知类型', color: 'grey' };
 | 
			
		||||
  }
 | 
			
		||||
  return <Label basic color={type2label[type]?.color}>{type2label[type] ? type2label[type].text : type}</Label>;
 | 
			
		||||
  return (
 | 
			
		||||
    <Label basic color={type2label[type]?.color}>
 | 
			
		||||
      {type2label[type] ? type2label[type].text : type}
 | 
			
		||||
    </Label>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function renderBalance(type, balance) {
 | 
			
		||||
@@ -62,10 +72,10 @@ function renderBalance(type, balance) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function isShowDetail() {
 | 
			
		||||
  return localStorage.getItem("show_detail") === "true";
 | 
			
		||||
  return localStorage.getItem('show_detail') === 'true';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const promptID = "detail"
 | 
			
		||||
const promptID = 'detail';
 | 
			
		||||
 | 
			
		||||
const ChannelsTable = () => {
 | 
			
		||||
  const [channels, setChannels] = useState([]);
 | 
			
		||||
@@ -81,33 +91,37 @@ const ChannelsTable = () => {
 | 
			
		||||
    const res = await API.get(`/api/channel/?p=${startIdx}`);
 | 
			
		||||
    const { success, message, data } = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
        let localChannels = data.map((channel) => {
 | 
			
		||||
            if (channel.models === '') {
 | 
			
		||||
                channel.models = [];
 | 
			
		||||
                channel.test_model = "";
 | 
			
		||||
            } else {
 | 
			
		||||
                channel.models = channel.models.split(',');
 | 
			
		||||
                if (channel.models.length > 0) {
 | 
			
		||||
                    channel.test_model = channel.models[0];
 | 
			
		||||
                }
 | 
			
		||||
                channel.model_options = channel.models.map((model) => {
 | 
			
		||||
                    return {
 | 
			
		||||
                        key: model,
 | 
			
		||||
                        text: model,
 | 
			
		||||
                        value: model,
 | 
			
		||||
                    }
 | 
			
		||||
                })
 | 
			
		||||
                console.log('channel', channel)
 | 
			
		||||
            }
 | 
			
		||||
            return channel;
 | 
			
		||||
        });
 | 
			
		||||
        if (startIdx === 0) {
 | 
			
		||||
            setChannels(localChannels);
 | 
			
		||||
      let localChannels = data.map((channel) => {
 | 
			
		||||
        if (channel.models === '') {
 | 
			
		||||
          channel.models = [];
 | 
			
		||||
          channel.test_model = '';
 | 
			
		||||
        } else {
 | 
			
		||||
            let newChannels = [...channels];
 | 
			
		||||
            newChannels.splice(startIdx * ITEMS_PER_PAGE, data.length, ...localChannels);
 | 
			
		||||
            setChannels(newChannels);
 | 
			
		||||
          channel.models = channel.models.split(',');
 | 
			
		||||
          if (channel.models.length > 0) {
 | 
			
		||||
            channel.test_model = channel.models[0];
 | 
			
		||||
          }
 | 
			
		||||
          channel.model_options = channel.models.map((model) => {
 | 
			
		||||
            return {
 | 
			
		||||
              key: model,
 | 
			
		||||
              text: model,
 | 
			
		||||
              value: model,
 | 
			
		||||
            };
 | 
			
		||||
          });
 | 
			
		||||
          console.log('channel', channel);
 | 
			
		||||
        }
 | 
			
		||||
        return channel;
 | 
			
		||||
      });
 | 
			
		||||
      if (startIdx === 0) {
 | 
			
		||||
        setChannels(localChannels);
 | 
			
		||||
      } else {
 | 
			
		||||
        let newChannels = [...channels];
 | 
			
		||||
        newChannels.splice(
 | 
			
		||||
          startIdx * ITEMS_PER_PAGE,
 | 
			
		||||
          data.length,
 | 
			
		||||
          ...localChannels
 | 
			
		||||
        );
 | 
			
		||||
        setChannels(newChannels);
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      showError(message);
 | 
			
		||||
    }
 | 
			
		||||
@@ -131,8 +145,8 @@ const ChannelsTable = () => {
 | 
			
		||||
 | 
			
		||||
  const toggleShowDetail = () => {
 | 
			
		||||
    setShowDetail(!showDetail);
 | 
			
		||||
    localStorage.setItem("show_detail", (!showDetail).toString());
 | 
			
		||||
  }
 | 
			
		||||
    localStorage.setItem('show_detail', (!showDetail).toString());
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    loadChannels(0)
 | 
			
		||||
@@ -196,13 +210,19 @@ const ChannelsTable = () => {
 | 
			
		||||
  const renderStatus = (status) => {
 | 
			
		||||
    switch (status) {
 | 
			
		||||
      case 1:
 | 
			
		||||
        return <Label basic color='green'>已启用</Label>;
 | 
			
		||||
        return (
 | 
			
		||||
          <Label basic color='green'>
 | 
			
		||||
            已启用
 | 
			
		||||
          </Label>
 | 
			
		||||
        );
 | 
			
		||||
      case 2:
 | 
			
		||||
        return (
 | 
			
		||||
          <Popup
 | 
			
		||||
            trigger={<Label basic color='red'>
 | 
			
		||||
              已禁用
 | 
			
		||||
            </Label>}
 | 
			
		||||
            trigger={
 | 
			
		||||
              <Label basic color='red'>
 | 
			
		||||
                已禁用
 | 
			
		||||
              </Label>
 | 
			
		||||
            }
 | 
			
		||||
            content='本渠道被手动禁用'
 | 
			
		||||
            basic
 | 
			
		||||
          />
 | 
			
		||||
@@ -210,9 +230,11 @@ const ChannelsTable = () => {
 | 
			
		||||
      case 3:
 | 
			
		||||
        return (
 | 
			
		||||
          <Popup
 | 
			
		||||
            trigger={<Label basic color='yellow'>
 | 
			
		||||
              已禁用
 | 
			
		||||
            </Label>}
 | 
			
		||||
            trigger={
 | 
			
		||||
              <Label basic color='yellow'>
 | 
			
		||||
                已禁用
 | 
			
		||||
              </Label>
 | 
			
		||||
            }
 | 
			
		||||
            content='本渠道被程序自动禁用'
 | 
			
		||||
            basic
 | 
			
		||||
          />
 | 
			
		||||
@@ -230,15 +252,35 @@ const ChannelsTable = () => {
 | 
			
		||||
    let time = responseTime / 1000;
 | 
			
		||||
    time = time.toFixed(2) + ' 秒';
 | 
			
		||||
    if (responseTime === 0) {
 | 
			
		||||
      return <Label basic color='grey'>未测试</Label>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Label basic color='grey'>
 | 
			
		||||
          未测试
 | 
			
		||||
        </Label>
 | 
			
		||||
      );
 | 
			
		||||
    } else if (responseTime <= 1000) {
 | 
			
		||||
      return <Label basic color='green'>{time}</Label>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Label basic color='green'>
 | 
			
		||||
          {time}
 | 
			
		||||
        </Label>
 | 
			
		||||
      );
 | 
			
		||||
    } else if (responseTime <= 3000) {
 | 
			
		||||
      return <Label basic color='olive'>{time}</Label>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Label basic color='olive'>
 | 
			
		||||
          {time}
 | 
			
		||||
        </Label>
 | 
			
		||||
      );
 | 
			
		||||
    } else if (responseTime <= 5000) {
 | 
			
		||||
      return <Label basic color='yellow'>{time}</Label>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Label basic color='yellow'>
 | 
			
		||||
          {time}
 | 
			
		||||
        </Label>
 | 
			
		||||
      );
 | 
			
		||||
    } else {
 | 
			
		||||
      return <Label basic color='red'>{time}</Label>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Label basic color='red'>
 | 
			
		||||
          {time}
 | 
			
		||||
        </Label>
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
@@ -277,7 +319,11 @@ const ChannelsTable = () => {
 | 
			
		||||
      newChannels[realIdx].response_time = time * 1000;
 | 
			
		||||
      newChannels[realIdx].test_time = Date.now() / 1000;
 | 
			
		||||
      setChannels(newChannels);
 | 
			
		||||
      showInfo(`渠道 ${name} 测试成功,模型 ${model},耗时 ${time.toFixed(2)} 秒。`);
 | 
			
		||||
      showInfo(
 | 
			
		||||
        `渠道 ${name} 测试成功,模型 ${model},耗时 ${time.toFixed(
 | 
			
		||||
          2
 | 
			
		||||
        )} 秒,模型输出:${message}`
 | 
			
		||||
      );
 | 
			
		||||
    } else {
 | 
			
		||||
      showError(message);
 | 
			
		||||
    }
 | 
			
		||||
@@ -360,7 +406,6 @@ const ChannelsTable = () => {
 | 
			
		||||
    setLoading(false);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <Form onSubmit={searchChannels}>
 | 
			
		||||
@@ -374,21 +419,23 @@ const ChannelsTable = () => {
 | 
			
		||||
          onChange={handleKeywordChange}
 | 
			
		||||
        />
 | 
			
		||||
      </Form>
 | 
			
		||||
      {
 | 
			
		||||
        showPrompt && (
 | 
			
		||||
          <Message onDismiss={() => {
 | 
			
		||||
      {showPrompt && (
 | 
			
		||||
        <Message
 | 
			
		||||
          onDismiss={() => {
 | 
			
		||||
            setShowPrompt(false);
 | 
			
		||||
            setPromptShown(promptID);
 | 
			
		||||
          }}>
 | 
			
		||||
            OpenAI 渠道已经不再支持通过 key 获取余额,因此余额显示为 0。对于支持的渠道类型,请点击余额进行刷新。
 | 
			
		||||
            <br/>
 | 
			
		||||
            渠道测试仅支持 chat 模型,优先使用 gpt-3.5-turbo,如果该模型不可用则使用你所配置的模型列表中的第一个模型。
 | 
			
		||||
            <br/>
 | 
			
		||||
            点击下方详情按钮可以显示余额以及设置额外的测试模型。
 | 
			
		||||
          </Message>
 | 
			
		||||
        )
 | 
			
		||||
      }
 | 
			
		||||
      <Table basic compact size='small'>
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          OpenAI 渠道已经不再支持通过 key 获取余额,因此余额显示为
 | 
			
		||||
          0。对于支持的渠道类型,请点击余额进行刷新。
 | 
			
		||||
          <br />
 | 
			
		||||
          渠道测试仅支持 chat 模型,优先使用
 | 
			
		||||
          gpt-3.5-turbo,如果该模型不可用则使用你所配置的模型列表中的第一个模型。
 | 
			
		||||
          <br />
 | 
			
		||||
          点击下方详情按钮可以显示余额以及设置额外的测试模型。
 | 
			
		||||
        </Message>
 | 
			
		||||
      )}
 | 
			
		||||
      <Table basic={'very'} compact size='small'>
 | 
			
		||||
        <Table.Header>
 | 
			
		||||
          <Table.Row>
 | 
			
		||||
            <Table.HeaderCell
 | 
			
		||||
@@ -478,7 +525,11 @@ const ChannelsTable = () => {
 | 
			
		||||
                  <Table.Cell>{renderStatus(channel.status)}</Table.Cell>
 | 
			
		||||
                  <Table.Cell>
 | 
			
		||||
                    <Popup
 | 
			
		||||
                      content={channel.test_time ? renderTimestamp(channel.test_time) : '未测试'}
 | 
			
		||||
                      content={
 | 
			
		||||
                        channel.test_time
 | 
			
		||||
                          ? renderTimestamp(channel.test_time)
 | 
			
		||||
                          : '未测试'
 | 
			
		||||
                      }
 | 
			
		||||
                      key={channel.id}
 | 
			
		||||
                      trigger={renderResponseTime(channel.response_time)}
 | 
			
		||||
                      basic
 | 
			
		||||
@@ -486,27 +537,38 @@ const ChannelsTable = () => {
 | 
			
		||||
                  </Table.Cell>
 | 
			
		||||
                  <Table.Cell hidden={!showDetail}>
 | 
			
		||||
                    <Popup
 | 
			
		||||
                      trigger={<span onClick={() => {
 | 
			
		||||
                        updateChannelBalance(channel.id, channel.name, idx);
 | 
			
		||||
                      }} style={{ cursor: 'pointer' }}>
 | 
			
		||||
                      {renderBalance(channel.type, channel.balance)}
 | 
			
		||||
                    </span>}
 | 
			
		||||
                      trigger={
 | 
			
		||||
                        <span
 | 
			
		||||
                          onClick={() => {
 | 
			
		||||
                            updateChannelBalance(channel.id, channel.name, idx);
 | 
			
		||||
                          }}
 | 
			
		||||
                          style={{ cursor: 'pointer' }}
 | 
			
		||||
                        >
 | 
			
		||||
                          {renderBalance(channel.type, channel.balance)}
 | 
			
		||||
                        </span>
 | 
			
		||||
                      }
 | 
			
		||||
                      content='点击更新'
 | 
			
		||||
                      basic
 | 
			
		||||
                    />
 | 
			
		||||
                  </Table.Cell>
 | 
			
		||||
                  <Table.Cell>
 | 
			
		||||
                    <Popup
 | 
			
		||||
                      trigger={<Input type='number' defaultValue={channel.priority} onBlur={(event) => {
 | 
			
		||||
                        manageChannel(
 | 
			
		||||
                          channel.id,
 | 
			
		||||
                          'priority',
 | 
			
		||||
                          idx,
 | 
			
		||||
                          event.target.value
 | 
			
		||||
                        );
 | 
			
		||||
                      }}>
 | 
			
		||||
                        <input style={{ maxWidth: '60px' }} />
 | 
			
		||||
                      </Input>}
 | 
			
		||||
                      trigger={
 | 
			
		||||
                        <Input
 | 
			
		||||
                          type='number'
 | 
			
		||||
                          defaultValue={channel.priority}
 | 
			
		||||
                          onBlur={(event) => {
 | 
			
		||||
                            manageChannel(
 | 
			
		||||
                              channel.id,
 | 
			
		||||
                              'priority',
 | 
			
		||||
                              idx,
 | 
			
		||||
                              event.target.value
 | 
			
		||||
                            );
 | 
			
		||||
                          }}
 | 
			
		||||
                        >
 | 
			
		||||
                          <input style={{ maxWidth: '60px' }} />
 | 
			
		||||
                        </Input>
 | 
			
		||||
                      }
 | 
			
		||||
                      content='渠道选择优先级,越高越优先'
 | 
			
		||||
                      basic
 | 
			
		||||
                    />
 | 
			
		||||
@@ -528,7 +590,12 @@ const ChannelsTable = () => {
 | 
			
		||||
                        size={'small'}
 | 
			
		||||
                        positive
 | 
			
		||||
                        onClick={() => {
 | 
			
		||||
                          testChannel(channel.id, channel.name, idx, channel.test_model);
 | 
			
		||||
                          testChannel(
 | 
			
		||||
                            channel.id,
 | 
			
		||||
                            channel.name,
 | 
			
		||||
                            idx,
 | 
			
		||||
                            channel.test_model
 | 
			
		||||
                          );
 | 
			
		||||
                        }}
 | 
			
		||||
                      >
 | 
			
		||||
                        测试
 | 
			
		||||
@@ -590,14 +657,31 @@ const ChannelsTable = () => {
 | 
			
		||||
 | 
			
		||||
        <Table.Footer>
 | 
			
		||||
          <Table.Row>
 | 
			
		||||
            <Table.HeaderCell colSpan={showDetail ? "10" : "8"}>
 | 
			
		||||
              <Button size='small' as={Link} to='/channel/add' loading={loading}>
 | 
			
		||||
            <Table.HeaderCell colSpan={showDetail ? '10' : '8'}>
 | 
			
		||||
              <Button
 | 
			
		||||
                size='small'
 | 
			
		||||
                as={Link}
 | 
			
		||||
                to='/channel/add'
 | 
			
		||||
                loading={loading}
 | 
			
		||||
              >
 | 
			
		||||
                添加新的渠道
 | 
			
		||||
              </Button>
 | 
			
		||||
              <Button size='small' loading={loading} onClick={()=>{testChannels("all")}}>
 | 
			
		||||
              <Button
 | 
			
		||||
                size='small'
 | 
			
		||||
                loading={loading}
 | 
			
		||||
                onClick={() => {
 | 
			
		||||
                  testChannels('all');
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                测试所有渠道
 | 
			
		||||
              </Button>
 | 
			
		||||
              <Button size='small' loading={loading} onClick={()=>{testChannels("disabled")}}>
 | 
			
		||||
              <Button
 | 
			
		||||
                size='small'
 | 
			
		||||
                loading={loading}
 | 
			
		||||
                onClick={() => {
 | 
			
		||||
                  testChannels('disabled');
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                测试禁用渠道
 | 
			
		||||
              </Button>
 | 
			
		||||
              {/*<Button size='small' onClick={updateAllChannelsBalance}*/}
 | 
			
		||||
@@ -612,7 +696,12 @@ const ChannelsTable = () => {
 | 
			
		||||
                flowing
 | 
			
		||||
                hoverable
 | 
			
		||||
              >
 | 
			
		||||
                <Button size='small' loading={loading} negative onClick={deleteAllDisabledChannels}>
 | 
			
		||||
                <Button
 | 
			
		||||
                  size='small'
 | 
			
		||||
                  loading={loading}
 | 
			
		||||
                  negative
 | 
			
		||||
                  onClick={deleteAllDisabledChannels}
 | 
			
		||||
                >
 | 
			
		||||
                  确认删除
 | 
			
		||||
                </Button>
 | 
			
		||||
              </Popup>
 | 
			
		||||
@@ -627,8 +716,12 @@ const ChannelsTable = () => {
 | 
			
		||||
                  (channels.length % ITEMS_PER_PAGE === 0 ? 1 : 0)
 | 
			
		||||
                }
 | 
			
		||||
              />
 | 
			
		||||
              <Button size='small' onClick={refresh} loading={loading}>刷新</Button>
 | 
			
		||||
              <Button size='small' onClick={toggleShowDetail}>{showDetail ? "隐藏详情" : "详情"}</Button>
 | 
			
		||||
              <Button size='small' onClick={refresh} loading={loading}>
 | 
			
		||||
                刷新
 | 
			
		||||
              </Button>
 | 
			
		||||
              <Button size='small' onClick={toggleShowDetail}>
 | 
			
		||||
                {showDetail ? '隐藏详情' : '详情'}
 | 
			
		||||
              </Button>
 | 
			
		||||
            </Table.HeaderCell>
 | 
			
		||||
          </Table.Row>
 | 
			
		||||
        </Table.Footer>
 | 
			
		||||
 
 | 
			
		||||
@@ -29,7 +29,7 @@ const Footer = () => {
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Segment vertical>
 | 
			
		||||
      <Container textAlign='center'>
 | 
			
		||||
      <Container textAlign='center' style={{ color: '#666666' }}>
 | 
			
		||||
        {footer ? (
 | 
			
		||||
          <div
 | 
			
		||||
            className='custom-footer'
 | 
			
		||||
@@ -37,10 +37,7 @@ const Footer = () => {
 | 
			
		||||
          ></div>
 | 
			
		||||
        ) : (
 | 
			
		||||
          <div className='custom-footer'>
 | 
			
		||||
            <a
 | 
			
		||||
              href='https://github.com/songquanpeng/one-api'
 | 
			
		||||
              target='_blank'
 | 
			
		||||
            >
 | 
			
		||||
            <a href='https://github.com/songquanpeng/one-api' target='_blank'>
 | 
			
		||||
              {systemName} {process.env.REACT_APP_VERSION}{' '}
 | 
			
		||||
            </a>
 | 
			
		||||
            由{' '}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,8 +2,22 @@ import React, { useContext, useState } from 'react';
 | 
			
		||||
import { Link, useNavigate } from 'react-router-dom';
 | 
			
		||||
import { UserContext } from '../context/User';
 | 
			
		||||
 | 
			
		||||
import { Button, Container, Dropdown, Icon, Menu, Segment } from 'semantic-ui-react';
 | 
			
		||||
import { API, getLogo, getSystemName, isAdmin, isMobile, showSuccess } from '../helpers';
 | 
			
		||||
import {
 | 
			
		||||
  Button,
 | 
			
		||||
  Container,
 | 
			
		||||
  Dropdown,
 | 
			
		||||
  Icon,
 | 
			
		||||
  Menu,
 | 
			
		||||
  Segment,
 | 
			
		||||
} from 'semantic-ui-react';
 | 
			
		||||
import {
 | 
			
		||||
  API,
 | 
			
		||||
  getLogo,
 | 
			
		||||
  getSystemName,
 | 
			
		||||
  isAdmin,
 | 
			
		||||
  isMobile,
 | 
			
		||||
  showSuccess,
 | 
			
		||||
} from '../helpers';
 | 
			
		||||
import '../index.css';
 | 
			
		||||
 | 
			
		||||
// Header Buttons
 | 
			
		||||
@@ -11,58 +25,63 @@ let headerButtons = [
 | 
			
		||||
  {
 | 
			
		||||
    name: '首页',
 | 
			
		||||
    to: '/',
 | 
			
		||||
    icon: 'home'
 | 
			
		||||
    icon: 'home',
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    name: '渠道',
 | 
			
		||||
    to: '/channel',
 | 
			
		||||
    icon: 'sitemap',
 | 
			
		||||
    admin: true
 | 
			
		||||
    admin: true,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    name: '令牌',
 | 
			
		||||
    to: '/token',
 | 
			
		||||
    icon: 'key'
 | 
			
		||||
    icon: 'key',
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    name: '兑换',
 | 
			
		||||
    to: '/redemption',
 | 
			
		||||
    icon: 'dollar sign',
 | 
			
		||||
    admin: true
 | 
			
		||||
    admin: true,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    name: '充值',
 | 
			
		||||
    to: '/topup',
 | 
			
		||||
    icon: 'cart'
 | 
			
		||||
    icon: 'cart',
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    name: '用户',
 | 
			
		||||
    to: '/user',
 | 
			
		||||
    icon: 'user',
 | 
			
		||||
    admin: true
 | 
			
		||||
    admin: true,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    name: '总览',
 | 
			
		||||
    to: '/dashboard',
 | 
			
		||||
    icon: 'chart bar',
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    name: '日志',
 | 
			
		||||
    to: '/log',
 | 
			
		||||
    icon: 'book'
 | 
			
		||||
    icon: 'book',
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    name: '设置',
 | 
			
		||||
    to: '/setting',
 | 
			
		||||
    icon: 'setting'
 | 
			
		||||
    icon: 'setting',
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    name: '关于',
 | 
			
		||||
    to: '/about',
 | 
			
		||||
    icon: 'info circle'
 | 
			
		||||
  }
 | 
			
		||||
    icon: 'info circle',
 | 
			
		||||
  },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
if (localStorage.getItem('chat_link')) {
 | 
			
		||||
  headerButtons.splice(1, 0, {
 | 
			
		||||
    name: '聊天',
 | 
			
		||||
    to: '/chat',
 | 
			
		||||
    icon: 'comments'
 | 
			
		||||
    icon: 'comments',
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -97,14 +116,24 @@ const Header = () => {
 | 
			
		||||
              navigate(button.to);
 | 
			
		||||
              setShowSidebar(false);
 | 
			
		||||
            }}
 | 
			
		||||
            style={{ fontSize: '15px' }}
 | 
			
		||||
          >
 | 
			
		||||
            {button.name}
 | 
			
		||||
          </Menu.Item>
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      return (
 | 
			
		||||
        <Menu.Item key={button.name} as={Link} to={button.to}>
 | 
			
		||||
          <Icon name={button.icon} />
 | 
			
		||||
        <Menu.Item
 | 
			
		||||
          key={button.name}
 | 
			
		||||
          as={Link}
 | 
			
		||||
          to={button.to}
 | 
			
		||||
          style={{
 | 
			
		||||
            fontSize: '15px',
 | 
			
		||||
            fontWeight: '400',
 | 
			
		||||
            color: '#666',
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <Icon name={button.icon} style={{ marginRight: '4px' }} />
 | 
			
		||||
          {button.name}
 | 
			
		||||
        </Menu.Item>
 | 
			
		||||
      );
 | 
			
		||||
@@ -120,21 +149,17 @@ const Header = () => {
 | 
			
		||||
          style={
 | 
			
		||||
            showSidebar
 | 
			
		||||
              ? {
 | 
			
		||||
                borderBottom: 'none',
 | 
			
		||||
                marginBottom: '0',
 | 
			
		||||
                borderTop: 'none',
 | 
			
		||||
                height: '51px'
 | 
			
		||||
              }
 | 
			
		||||
                  borderBottom: 'none',
 | 
			
		||||
                  marginBottom: '0',
 | 
			
		||||
                  borderTop: 'none',
 | 
			
		||||
                  height: '51px',
 | 
			
		||||
                }
 | 
			
		||||
              : { borderTop: 'none', height: '52px' }
 | 
			
		||||
          }
 | 
			
		||||
        >
 | 
			
		||||
          <Container>
 | 
			
		||||
            <Menu.Item as={Link} to='/'>
 | 
			
		||||
              <img
 | 
			
		||||
                src={logo}
 | 
			
		||||
                alt='logo'
 | 
			
		||||
                style={{ marginRight: '0.75em' }}
 | 
			
		||||
              />
 | 
			
		||||
              <img src={logo} alt='logo' style={{ marginRight: '0.75em' }} />
 | 
			
		||||
              <div style={{ fontSize: '20px' }}>
 | 
			
		||||
                <b>{systemName}</b>
 | 
			
		||||
              </div>
 | 
			
		||||
@@ -152,7 +177,9 @@ const Header = () => {
 | 
			
		||||
              {renderButtons(true)}
 | 
			
		||||
              <Menu.Item>
 | 
			
		||||
                {userState.user ? (
 | 
			
		||||
                  <Button onClick={logout}>注销</Button>
 | 
			
		||||
                  <Button onClick={logout} style={{ color: '#666666' }}>
 | 
			
		||||
                    注销
 | 
			
		||||
                  </Button>
 | 
			
		||||
                ) : (
 | 
			
		||||
                  <>
 | 
			
		||||
                    <Button
 | 
			
		||||
@@ -185,12 +212,25 @@ const Header = () => {
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <Menu borderless style={{ borderTop: 'none' }}>
 | 
			
		||||
      <Menu
 | 
			
		||||
        borderless
 | 
			
		||||
        style={{
 | 
			
		||||
          borderTop: 'none',
 | 
			
		||||
          boxShadow: 'rgba(0, 0, 0, 0.04) 0px 2px 12px 0px',
 | 
			
		||||
          border: 'none',
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <Container>
 | 
			
		||||
          <Menu.Item as={Link} to='/' className={'hide-on-mobile'}>
 | 
			
		||||
            <img src={logo} alt='logo' style={{ marginRight: '0.75em' }} />
 | 
			
		||||
            <div style={{ fontSize: '20px' }}>
 | 
			
		||||
              <b>{systemName}</b>
 | 
			
		||||
            <div
 | 
			
		||||
              style={{
 | 
			
		||||
                fontSize: '18px',
 | 
			
		||||
                fontWeight: '500',
 | 
			
		||||
                color: '#333',
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              {systemName}
 | 
			
		||||
            </div>
 | 
			
		||||
          </Menu.Item>
 | 
			
		||||
          {renderButtons(false)}
 | 
			
		||||
@@ -200,9 +240,23 @@ const Header = () => {
 | 
			
		||||
                text={userState.user.username}
 | 
			
		||||
                pointing
 | 
			
		||||
                className='link item'
 | 
			
		||||
                style={{
 | 
			
		||||
                  fontSize: '15px',
 | 
			
		||||
                  fontWeight: '400',
 | 
			
		||||
                  color: '#666',
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                <Dropdown.Menu>
 | 
			
		||||
                  <Dropdown.Item onClick={logout}>注销</Dropdown.Item>
 | 
			
		||||
                  <Dropdown.Item
 | 
			
		||||
                    onClick={logout}
 | 
			
		||||
                    style={{
 | 
			
		||||
                      fontSize: '15px',
 | 
			
		||||
                      fontWeight: '400',
 | 
			
		||||
                      color: '#666',
 | 
			
		||||
                    }}
 | 
			
		||||
                  >
 | 
			
		||||
                    注销
 | 
			
		||||
                  </Dropdown.Item>
 | 
			
		||||
                </Dropdown.Menu>
 | 
			
		||||
              </Dropdown>
 | 
			
		||||
            ) : (
 | 
			
		||||
@@ -211,6 +265,11 @@ const Header = () => {
 | 
			
		||||
                as={Link}
 | 
			
		||||
                to='/login'
 | 
			
		||||
                className='btn btn-link'
 | 
			
		||||
                style={{
 | 
			
		||||
                  fontSize: '15px',
 | 
			
		||||
                  fontWeight: '400',
 | 
			
		||||
                  color: '#666',
 | 
			
		||||
                }}
 | 
			
		||||
              />
 | 
			
		||||
            )}
 | 
			
		||||
          </Menu.Menu>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,16 @@
 | 
			
		||||
import React, { useContext, useEffect, useState } from 'react';
 | 
			
		||||
import { Button, Divider, Form, Grid, Header, Image, Message, Modal, Segment } from 'semantic-ui-react';
 | 
			
		||||
import {
 | 
			
		||||
  Button,
 | 
			
		||||
  Divider,
 | 
			
		||||
  Form,
 | 
			
		||||
  Grid,
 | 
			
		||||
  Header,
 | 
			
		||||
  Image,
 | 
			
		||||
  Message,
 | 
			
		||||
  Modal,
 | 
			
		||||
  Segment,
 | 
			
		||||
  Card,
 | 
			
		||||
} from 'semantic-ui-react';
 | 
			
		||||
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
 | 
			
		||||
import { UserContext } from '../context/User';
 | 
			
		||||
import { API, getLogo, showError, showSuccess, showWarning } from '../helpers';
 | 
			
		||||
@@ -10,7 +21,7 @@ const LoginForm = () => {
 | 
			
		||||
  const [inputs, setInputs] = useState({
 | 
			
		||||
    username: '',
 | 
			
		||||
    password: '',
 | 
			
		||||
    wechat_verification_code: ''
 | 
			
		||||
    wechat_verification_code: '',
 | 
			
		||||
  });
 | 
			
		||||
  const [searchParams, setSearchParams] = useSearchParams();
 | 
			
		||||
  const [submitted, setSubmitted] = useState(false);
 | 
			
		||||
@@ -63,7 +74,7 @@ const LoginForm = () => {
 | 
			
		||||
    if (username && password) {
 | 
			
		||||
      const res = await API.post(`/api/user/login`, {
 | 
			
		||||
        username,
 | 
			
		||||
        password
 | 
			
		||||
        password,
 | 
			
		||||
      });
 | 
			
		||||
      const { success, message, data } = res.data;
 | 
			
		||||
      if (success) {
 | 
			
		||||
@@ -86,129 +97,149 @@ const LoginForm = () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <Grid textAlign='center' style={{ marginTop: '48px' }}>
 | 
			
		||||
      <Grid.Column style={{ maxWidth: 450 }}>
 | 
			
		||||
        <Header as='h2' color='' textAlign='center'>
 | 
			
		||||
          <Image src={logo} /> 用户登录
 | 
			
		||||
        </Header>
 | 
			
		||||
        <Form size='large'>
 | 
			
		||||
          <Segment>
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              fluid
 | 
			
		||||
              icon='user'
 | 
			
		||||
              iconPosition='left'
 | 
			
		||||
              placeholder='用户名 / 邮箱地址'
 | 
			
		||||
              name='username'
 | 
			
		||||
              value={username}
 | 
			
		||||
              onChange={handleChange}
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              fluid
 | 
			
		||||
              icon='lock'
 | 
			
		||||
              iconPosition='left'
 | 
			
		||||
              placeholder='密码'
 | 
			
		||||
              name='password'
 | 
			
		||||
              type='password'
 | 
			
		||||
              value={password}
 | 
			
		||||
              onChange={handleChange}
 | 
			
		||||
            />
 | 
			
		||||
            <Button color='green' fluid size='large' onClick={handleSubmit}>
 | 
			
		||||
              登录
 | 
			
		||||
            </Button>
 | 
			
		||||
          </Segment>
 | 
			
		||||
        </Form>
 | 
			
		||||
        <Message>
 | 
			
		||||
          忘记密码?
 | 
			
		||||
          <Link to='/reset' className='btn btn-link'>
 | 
			
		||||
            点击重置
 | 
			
		||||
          </Link>
 | 
			
		||||
          ; 没有账户?
 | 
			
		||||
          <Link to='/register' className='btn btn-link'>
 | 
			
		||||
            点击注册
 | 
			
		||||
          </Link>
 | 
			
		||||
        </Message>
 | 
			
		||||
        {status.github_oauth || status.wechat_login || status.lark_client_id ? (
 | 
			
		||||
          <>
 | 
			
		||||
            <Divider horizontal>Or</Divider>
 | 
			
		||||
            <div style={{ display: "flex", justifyContent: "center" }}>
 | 
			
		||||
              {status.github_oauth ? (
 | 
			
		||||
                <Button
 | 
			
		||||
                  circular
 | 
			
		||||
                  color='black'
 | 
			
		||||
                  icon='github'
 | 
			
		||||
                  onClick={() => onGitHubOAuthClicked(status.github_client_id)}
 | 
			
		||||
                />
 | 
			
		||||
              ) : (
 | 
			
		||||
                <></>
 | 
			
		||||
              )}
 | 
			
		||||
              {status.wechat_login ? (
 | 
			
		||||
                <Button
 | 
			
		||||
                  circular
 | 
			
		||||
                  color='green'
 | 
			
		||||
                  icon='wechat'
 | 
			
		||||
                  onClick={onWeChatLoginClicked}
 | 
			
		||||
                />
 | 
			
		||||
              ) : (
 | 
			
		||||
                <></>
 | 
			
		||||
              )}
 | 
			
		||||
              {status.lark_client_id ? (
 | 
			
		||||
                <div style={{
 | 
			
		||||
                  background: "radial-gradient(circle, #FFFFFF, #FFFFFF, #00D6B9, #2F73FF, #0a3A9C)",
 | 
			
		||||
                  width: "36px",
 | 
			
		||||
                  height: "36px",
 | 
			
		||||
                  borderRadius: "10em",
 | 
			
		||||
                  display: "flex",
 | 
			
		||||
                  cursor: "pointer"
 | 
			
		||||
                }}
 | 
			
		||||
                  onClick={() => onLarkOAuthClicked(status.lark_client_id)}
 | 
			
		||||
                >
 | 
			
		||||
                  <Image
 | 
			
		||||
                    src={larkIcon}
 | 
			
		||||
                    avatar
 | 
			
		||||
                    style={{ width: "16px", height: "16px", cursor: "pointer", margin: "auto" }}
 | 
			
		||||
                    onClick={() => onLarkOAuthClicked(status.lark_client_id)}
 | 
			
		||||
                  />
 | 
			
		||||
                </div>
 | 
			
		||||
              ) : (
 | 
			
		||||
                <></>
 | 
			
		||||
              )}
 | 
			
		||||
            </div>
 | 
			
		||||
          </>
 | 
			
		||||
        ) : (
 | 
			
		||||
          <></>
 | 
			
		||||
        )}
 | 
			
		||||
        <Modal
 | 
			
		||||
          onClose={() => setShowWeChatLoginModal(false)}
 | 
			
		||||
          onOpen={() => setShowWeChatLoginModal(true)}
 | 
			
		||||
          open={showWeChatLoginModal}
 | 
			
		||||
          size={'mini'}
 | 
			
		||||
        <Card
 | 
			
		||||
          fluid
 | 
			
		||||
          className='chart-card'
 | 
			
		||||
          style={{ boxShadow: '0 1px 3px rgba(0,0,0,0.12)' }}
 | 
			
		||||
        >
 | 
			
		||||
          <Modal.Content>
 | 
			
		||||
            <Modal.Description>
 | 
			
		||||
              <Image src={status.wechat_qrcode} fluid />
 | 
			
		||||
              <div style={{ textAlign: 'center' }}>
 | 
			
		||||
                <p>
 | 
			
		||||
                  微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)
 | 
			
		||||
                </p>
 | 
			
		||||
          <Card.Content>
 | 
			
		||||
            <Card.Header>
 | 
			
		||||
              <Header
 | 
			
		||||
                as='h2'
 | 
			
		||||
                textAlign='center'
 | 
			
		||||
                style={{ marginBottom: '1.5em' }}
 | 
			
		||||
              >
 | 
			
		||||
                <Image src={logo} style={{ marginBottom: '10px' }} />
 | 
			
		||||
                <Header.Content>用户登录</Header.Content>
 | 
			
		||||
              </Header>
 | 
			
		||||
            </Card.Header>
 | 
			
		||||
            <Form size='large'>
 | 
			
		||||
              <Form.Input
 | 
			
		||||
                fluid
 | 
			
		||||
                icon='user'
 | 
			
		||||
                iconPosition='left'
 | 
			
		||||
                placeholder='用户名 / 邮箱地址'
 | 
			
		||||
                name='username'
 | 
			
		||||
                value={username}
 | 
			
		||||
                onChange={handleChange}
 | 
			
		||||
                style={{ marginBottom: '1em' }}
 | 
			
		||||
              />
 | 
			
		||||
              <Form.Input
 | 
			
		||||
                fluid
 | 
			
		||||
                icon='lock'
 | 
			
		||||
                iconPosition='left'
 | 
			
		||||
                placeholder='密码'
 | 
			
		||||
                name='password'
 | 
			
		||||
                type='password'
 | 
			
		||||
                value={password}
 | 
			
		||||
                onChange={handleChange}
 | 
			
		||||
                style={{ marginBottom: '1.5em' }}
 | 
			
		||||
              />
 | 
			
		||||
              <Button
 | 
			
		||||
                fluid
 | 
			
		||||
                size='large'
 | 
			
		||||
                style={{
 | 
			
		||||
                  background: '#2F73FF', // 使用更现代的蓝色
 | 
			
		||||
                  color: 'white',
 | 
			
		||||
                  marginBottom: '1.5em',
 | 
			
		||||
                }}
 | 
			
		||||
                onClick={handleSubmit}
 | 
			
		||||
              >
 | 
			
		||||
                登录
 | 
			
		||||
              </Button>
 | 
			
		||||
            </Form>
 | 
			
		||||
 | 
			
		||||
            <Divider />
 | 
			
		||||
            <Message style={{ background: 'transparent', boxShadow: 'none' }}>
 | 
			
		||||
              <div
 | 
			
		||||
                style={{
 | 
			
		||||
                  display: 'flex',
 | 
			
		||||
                  justifyContent: 'space-between',
 | 
			
		||||
                  fontSize: '0.9em',
 | 
			
		||||
                  color: '#666',
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                <div>
 | 
			
		||||
                  忘记密码?
 | 
			
		||||
                  <Link to='/reset' style={{ color: '#2185d0' }}>
 | 
			
		||||
                    点击重置
 | 
			
		||||
                  </Link>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div>
 | 
			
		||||
                  没有账户?
 | 
			
		||||
                  <Link to='/register' style={{ color: '#2185d0' }}>
 | 
			
		||||
                    点击注册
 | 
			
		||||
                  </Link>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
              <Form size='large'>
 | 
			
		||||
                <Form.Input
 | 
			
		||||
                  fluid
 | 
			
		||||
                  placeholder='验证码'
 | 
			
		||||
                  name='wechat_verification_code'
 | 
			
		||||
                  value={inputs.wechat_verification_code}
 | 
			
		||||
                  onChange={handleChange}
 | 
			
		||||
                />
 | 
			
		||||
                <Button
 | 
			
		||||
                  color=''
 | 
			
		||||
                  fluid
 | 
			
		||||
                  size='large'
 | 
			
		||||
                  onClick={onSubmitWeChatVerificationCode}
 | 
			
		||||
            </Message>
 | 
			
		||||
 | 
			
		||||
            {(status.github_oauth ||
 | 
			
		||||
              status.wechat_login ||
 | 
			
		||||
              status.lark_client_id) && (
 | 
			
		||||
              <>
 | 
			
		||||
                <Divider
 | 
			
		||||
                  horizontal
 | 
			
		||||
                  style={{ color: '#666', fontSize: '0.9em' }}
 | 
			
		||||
                >
 | 
			
		||||
                  登录
 | 
			
		||||
                </Button>
 | 
			
		||||
              </Form>
 | 
			
		||||
            </Modal.Description>
 | 
			
		||||
          </Modal.Content>
 | 
			
		||||
        </Modal>
 | 
			
		||||
                  使用其他方式登录
 | 
			
		||||
                </Divider>
 | 
			
		||||
                <div
 | 
			
		||||
                  style={{
 | 
			
		||||
                    display: 'flex',
 | 
			
		||||
                    justifyContent: 'center',
 | 
			
		||||
                    gap: '1em',
 | 
			
		||||
                    marginTop: '1em',
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  {status.github_oauth && (
 | 
			
		||||
                    <Button
 | 
			
		||||
                      circular
 | 
			
		||||
                      color='black'
 | 
			
		||||
                      icon='github'
 | 
			
		||||
                      onClick={() =>
 | 
			
		||||
                        onGitHubOAuthClicked(status.github_client_id)
 | 
			
		||||
                      }
 | 
			
		||||
                    />
 | 
			
		||||
                  )}
 | 
			
		||||
                  {status.wechat_login && (
 | 
			
		||||
                    <Button
 | 
			
		||||
                      circular
 | 
			
		||||
                      color='green'
 | 
			
		||||
                      icon='wechat'
 | 
			
		||||
                      onClick={onWeChatLoginClicked}
 | 
			
		||||
                    />
 | 
			
		||||
                  )}
 | 
			
		||||
                  {status.lark_client_id && (
 | 
			
		||||
                    <div
 | 
			
		||||
                      style={{
 | 
			
		||||
                        background:
 | 
			
		||||
                          'radial-gradient(circle, #FFFFFF, #FFFFFF, #FFFFFF, #FFFFFF, #FFFFFF)',
 | 
			
		||||
                        width: '36px',
 | 
			
		||||
                        height: '36px',
 | 
			
		||||
                        borderRadius: '10em',
 | 
			
		||||
                        display: 'flex',
 | 
			
		||||
                        cursor: 'pointer',
 | 
			
		||||
                      }}
 | 
			
		||||
                      onClick={() => onLarkOAuthClicked(status.lark_client_id)}
 | 
			
		||||
                    >
 | 
			
		||||
                      <Image
 | 
			
		||||
                        src={larkIcon}
 | 
			
		||||
                        avatar
 | 
			
		||||
                        style={{
 | 
			
		||||
                          width: '36px',
 | 
			
		||||
                          height: '36px',
 | 
			
		||||
                          cursor: 'pointer',
 | 
			
		||||
                          margin: 'auto',
 | 
			
		||||
                        }}
 | 
			
		||||
                      />
 | 
			
		||||
                    </div>
 | 
			
		||||
                  )}
 | 
			
		||||
                </div>
 | 
			
		||||
              </>
 | 
			
		||||
            )}
 | 
			
		||||
          </Card.Content>
 | 
			
		||||
        </Card>
 | 
			
		||||
      </Grid.Column>
 | 
			
		||||
    </Grid>
 | 
			
		||||
  );
 | 
			
		||||
 
 | 
			
		||||
@@ -1,21 +1,48 @@
 | 
			
		||||
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 {
 | 
			
		||||
  Button,
 | 
			
		||||
  Form,
 | 
			
		||||
  Header,
 | 
			
		||||
  Label,
 | 
			
		||||
  Pagination,
 | 
			
		||||
  Segment,
 | 
			
		||||
  Select,
 | 
			
		||||
  Table,
 | 
			
		||||
} from 'semantic-ui-react';
 | 
			
		||||
import {
 | 
			
		||||
  API,
 | 
			
		||||
  copy,
 | 
			
		||||
  isAdmin,
 | 
			
		||||
  showError,
 | 
			
		||||
  showSuccess,
 | 
			
		||||
  showWarning,
 | 
			
		||||
  timestamp2string,
 | 
			
		||||
} from '../helpers';
 | 
			
		||||
 | 
			
		||||
import { ITEMS_PER_PAGE } from '../constants';
 | 
			
		||||
import { renderQuota } from '../helpers/render';
 | 
			
		||||
import { renderColorLabel, renderQuota } from '../helpers/render';
 | 
			
		||||
import { Link } from 'react-router-dom';
 | 
			
		||||
 | 
			
		||||
function renderTimestamp(timestamp) {
 | 
			
		||||
function renderTimestamp(timestamp, request_id) {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
    <code
 | 
			
		||||
      onClick={async () => {
 | 
			
		||||
        if (await copy(request_id)) {
 | 
			
		||||
          showSuccess(`已复制请求 ID:${request_id}`);
 | 
			
		||||
        } else {
 | 
			
		||||
          showWarning(`请求 ID 复制失败:${request_id}`);
 | 
			
		||||
        }
 | 
			
		||||
      }}
 | 
			
		||||
      style={{ cursor: 'pointer' }}
 | 
			
		||||
    >
 | 
			
		||||
      {timestamp2string(timestamp)}
 | 
			
		||||
    </>
 | 
			
		||||
    </code>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const MODE_OPTIONS = [
 | 
			
		||||
  { key: 'all', text: '全部用户', value: 'all' },
 | 
			
		||||
  { key: 'self', text: '当前用户', value: 'self' }
 | 
			
		||||
  { key: 'self', text: '当前用户', value: 'self' },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const LOG_OPTIONS = [
 | 
			
		||||
@@ -23,24 +50,92 @@ const LOG_OPTIONS = [
 | 
			
		||||
  { key: '1', text: '充值', value: 1 },
 | 
			
		||||
  { key: '2', text: '消费', value: 2 },
 | 
			
		||||
  { key: '3', text: '管理', value: 3 },
 | 
			
		||||
  { key: '4', text: '系统', value: 4 }
 | 
			
		||||
  { key: '4', text: '系统', value: 4 },
 | 
			
		||||
  { key: '5', text: '测试', value: 5 },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
function renderType(type) {
 | 
			
		||||
  switch (type) {
 | 
			
		||||
    case 1:
 | 
			
		||||
      return <Label basic color='green'> 充值 </Label>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Label basic color='green'>
 | 
			
		||||
          充值
 | 
			
		||||
        </Label>
 | 
			
		||||
      );
 | 
			
		||||
    case 2:
 | 
			
		||||
      return <Label basic color='olive'> 消费 </Label>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Label basic color='olive'>
 | 
			
		||||
          消费
 | 
			
		||||
        </Label>
 | 
			
		||||
      );
 | 
			
		||||
    case 3:
 | 
			
		||||
      return <Label basic color='orange'> 管理 </Label>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Label basic color='orange'>
 | 
			
		||||
          管理
 | 
			
		||||
        </Label>
 | 
			
		||||
      );
 | 
			
		||||
    case 4:
 | 
			
		||||
      return <Label basic color='purple'> 系统 </Label>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Label basic color='purple'>
 | 
			
		||||
          系统
 | 
			
		||||
        </Label>
 | 
			
		||||
      );
 | 
			
		||||
    case 5:
 | 
			
		||||
      return (
 | 
			
		||||
        <Label basic color='violet'>
 | 
			
		||||
          测试
 | 
			
		||||
        </Label>
 | 
			
		||||
      );
 | 
			
		||||
    default:
 | 
			
		||||
      return <Label basic color='black'> 未知 </Label>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Label basic color='black'>
 | 
			
		||||
          未知
 | 
			
		||||
        </Label>
 | 
			
		||||
      );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getColorByElapsedTime(elapsedTime) {
 | 
			
		||||
  if (elapsedTime === undefined || 0) return 'black';
 | 
			
		||||
  if (elapsedTime < 1000) return 'green';
 | 
			
		||||
  if (elapsedTime < 3000) return 'olive';
 | 
			
		||||
  if (elapsedTime < 5000) return 'yellow';
 | 
			
		||||
  if (elapsedTime < 10000) return 'orange';
 | 
			
		||||
  return 'red';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function renderDetail(log) {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      {log.content}
 | 
			
		||||
      <br />
 | 
			
		||||
      {log.elapsed_time && (
 | 
			
		||||
        <Label
 | 
			
		||||
          basic
 | 
			
		||||
          size={'mini'}
 | 
			
		||||
          color={getColorByElapsedTime(log.elapsed_time)}
 | 
			
		||||
        >
 | 
			
		||||
          {log.elapsed_time} ms
 | 
			
		||||
        </Label>
 | 
			
		||||
      )}
 | 
			
		||||
      {log.is_stream && (
 | 
			
		||||
        <>
 | 
			
		||||
          <Label size={'mini'} color='pink'>
 | 
			
		||||
            Stream
 | 
			
		||||
          </Label>
 | 
			
		||||
        </>
 | 
			
		||||
      )}
 | 
			
		||||
      {log.system_prompt_reset && (
 | 
			
		||||
        <>
 | 
			
		||||
          <Label basic size={'mini'} color='red'>
 | 
			
		||||
            System Prompt Reset
 | 
			
		||||
          </Label>
 | 
			
		||||
        </>
 | 
			
		||||
      )}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const LogsTable = () => {
 | 
			
		||||
  const [logs, setLogs] = useState([]);
 | 
			
		||||
  const [showStat, setShowStat] = useState(false);
 | 
			
		||||
@@ -57,13 +152,20 @@ const LogsTable = () => {
 | 
			
		||||
    model_name: '',
 | 
			
		||||
    start_timestamp: timestamp2string(0),
 | 
			
		||||
    end_timestamp: timestamp2string(now.getTime() / 1000 + 3600),
 | 
			
		||||
    channel: ''
 | 
			
		||||
    channel: '',
 | 
			
		||||
  });
 | 
			
		||||
  const { username, token_name, model_name, start_timestamp, end_timestamp, channel } = inputs;
 | 
			
		||||
  const {
 | 
			
		||||
    username,
 | 
			
		||||
    token_name,
 | 
			
		||||
    model_name,
 | 
			
		||||
    start_timestamp,
 | 
			
		||||
    end_timestamp,
 | 
			
		||||
    channel,
 | 
			
		||||
  } = inputs;
 | 
			
		||||
 | 
			
		||||
  const [stat, setStat] = useState({
 | 
			
		||||
    quota: 0,
 | 
			
		||||
    token: 0
 | 
			
		||||
    token: 0,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const handleInputChange = (e, { name, value }) => {
 | 
			
		||||
@@ -73,7 +175,9 @@ const LogsTable = () => {
 | 
			
		||||
  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}`);
 | 
			
		||||
    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);
 | 
			
		||||
@@ -85,7 +189,9 @@ const LogsTable = () => {
 | 
			
		||||
  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}&channel=${channel}`);
 | 
			
		||||
    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}&channel=${channel}`
 | 
			
		||||
    );
 | 
			
		||||
    const { success, message, data } = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
      setStat(data);
 | 
			
		||||
@@ -105,6 +211,10 @@ const LogsTable = () => {
 | 
			
		||||
    setShowStat(!showStat);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const showUserTokenQuota = () => {
 | 
			
		||||
    return logType !== 5;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const loadLogs = async (startIdx) => {
 | 
			
		||||
    let url = '';
 | 
			
		||||
    let localStartTimestamp = Date.parse(start_timestamp) / 1000;
 | 
			
		||||
@@ -197,43 +307,88 @@ const LogsTable = () => {
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <Segment>
 | 
			
		||||
      <>
 | 
			
		||||
        <Header as='h3'>
 | 
			
		||||
          使用明细(总消耗额度:
 | 
			
		||||
          {showStat && renderQuota(stat.quota)}
 | 
			
		||||
          {!showStat && <span onClick={handleEyeClick} style={{ cursor: 'pointer', color: 'gray' }}>点击查看</span>}
 | 
			
		||||
          {!showStat && (
 | 
			
		||||
            <span
 | 
			
		||||
              onClick={handleEyeClick}
 | 
			
		||||
              style={{ cursor: 'pointer', color: 'gray' }}
 | 
			
		||||
            >
 | 
			
		||||
              点击查看
 | 
			
		||||
            </span>
 | 
			
		||||
          )}
 | 
			
		||||
          )
 | 
			
		||||
        </Header>
 | 
			
		||||
        <Form>
 | 
			
		||||
          <Form.Group>
 | 
			
		||||
            <Form.Input fluid label={'令牌名称'} width={3} value={token_name}
 | 
			
		||||
                        placeholder={'可选值'} name='token_name' onChange={handleInputChange} />
 | 
			
		||||
            <Form.Input fluid label='模型名称' width={3} value={model_name} placeholder='可选值'
 | 
			
		||||
                        name='model_name'
 | 
			
		||||
                        onChange={handleInputChange} />
 | 
			
		||||
            <Form.Input fluid label='起始时间' width={4} value={start_timestamp} type='datetime-local'
 | 
			
		||||
                        name='start_timestamp'
 | 
			
		||||
                        onChange={handleInputChange} />
 | 
			
		||||
            <Form.Input fluid label='结束时间' width={4} value={end_timestamp} type='datetime-local'
 | 
			
		||||
                        name='end_timestamp'
 | 
			
		||||
                        onChange={handleInputChange} />
 | 
			
		||||
            <Form.Button fluid label='操作' width={2} onClick={refresh}>查询</Form.Button>
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              fluid
 | 
			
		||||
              label={'令牌名称'}
 | 
			
		||||
              width={3}
 | 
			
		||||
              value={token_name}
 | 
			
		||||
              placeholder={'可选值'}
 | 
			
		||||
              name='token_name'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              fluid
 | 
			
		||||
              label='模型名称'
 | 
			
		||||
              width={3}
 | 
			
		||||
              value={model_name}
 | 
			
		||||
              placeholder='可选值'
 | 
			
		||||
              name='model_name'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              fluid
 | 
			
		||||
              label='起始时间'
 | 
			
		||||
              width={4}
 | 
			
		||||
              value={start_timestamp}
 | 
			
		||||
              type='datetime-local'
 | 
			
		||||
              name='start_timestamp'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              fluid
 | 
			
		||||
              label='结束时间'
 | 
			
		||||
              width={4}
 | 
			
		||||
              value={end_timestamp}
 | 
			
		||||
              type='datetime-local'
 | 
			
		||||
              name='end_timestamp'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Button fluid label='操作' width={2} onClick={refresh}>
 | 
			
		||||
              查询
 | 
			
		||||
            </Form.Button>
 | 
			
		||||
          </Form.Group>
 | 
			
		||||
          {
 | 
			
		||||
            isAdminUser && <>
 | 
			
		||||
          {isAdminUser && (
 | 
			
		||||
            <>
 | 
			
		||||
              <Form.Group>
 | 
			
		||||
                <Form.Input fluid label={'渠道 ID'} width={3} value={channel}
 | 
			
		||||
                            placeholder='可选值' name='channel'
 | 
			
		||||
                            onChange={handleInputChange} />
 | 
			
		||||
                <Form.Input fluid label={'用户名称'} width={3} value={username}
 | 
			
		||||
                            placeholder={'可选值'} name='username'
 | 
			
		||||
                            onChange={handleInputChange} />
 | 
			
		||||
 | 
			
		||||
                <Form.Input
 | 
			
		||||
                  fluid
 | 
			
		||||
                  label={'渠道 ID'}
 | 
			
		||||
                  width={3}
 | 
			
		||||
                  value={channel}
 | 
			
		||||
                  placeholder='可选值'
 | 
			
		||||
                  name='channel'
 | 
			
		||||
                  onChange={handleInputChange}
 | 
			
		||||
                />
 | 
			
		||||
                <Form.Input
 | 
			
		||||
                  fluid
 | 
			
		||||
                  label={'用户名称'}
 | 
			
		||||
                  width={3}
 | 
			
		||||
                  value={username}
 | 
			
		||||
                  placeholder={'可选值'}
 | 
			
		||||
                  name='username'
 | 
			
		||||
                  onChange={handleInputChange}
 | 
			
		||||
                />
 | 
			
		||||
              </Form.Group>
 | 
			
		||||
            </>
 | 
			
		||||
          }
 | 
			
		||||
          )}
 | 
			
		||||
        </Form>
 | 
			
		||||
        <Table basic compact size='small'>
 | 
			
		||||
        <Table basic={'very'} compact size='small'>
 | 
			
		||||
          <Table.Header>
 | 
			
		||||
            <Table.Row>
 | 
			
		||||
              <Table.HeaderCell
 | 
			
		||||
@@ -245,8 +400,8 @@ const LogsTable = () => {
 | 
			
		||||
              >
 | 
			
		||||
                时间
 | 
			
		||||
              </Table.HeaderCell>
 | 
			
		||||
              {
 | 
			
		||||
                isAdminUser && <Table.HeaderCell
 | 
			
		||||
              {isAdminUser && (
 | 
			
		||||
                <Table.HeaderCell
 | 
			
		||||
                  style={{ cursor: 'pointer' }}
 | 
			
		||||
                  onClick={() => {
 | 
			
		||||
                    sortLog('channel');
 | 
			
		||||
@@ -255,27 +410,7 @@ const LogsTable = () => {
 | 
			
		||||
                >
 | 
			
		||||
                  渠道
 | 
			
		||||
                </Table.HeaderCell>
 | 
			
		||||
              }
 | 
			
		||||
              {
 | 
			
		||||
                isAdminUser && <Table.HeaderCell
 | 
			
		||||
                  style={{ cursor: 'pointer' }}
 | 
			
		||||
                  onClick={() => {
 | 
			
		||||
                    sortLog('username');
 | 
			
		||||
                  }}
 | 
			
		||||
                  width={1}
 | 
			
		||||
                >
 | 
			
		||||
                  用户
 | 
			
		||||
                </Table.HeaderCell>
 | 
			
		||||
              }
 | 
			
		||||
              <Table.HeaderCell
 | 
			
		||||
                style={{ cursor: 'pointer' }}
 | 
			
		||||
                onClick={() => {
 | 
			
		||||
                  sortLog('token_name');
 | 
			
		||||
                }}
 | 
			
		||||
                width={1}
 | 
			
		||||
              >
 | 
			
		||||
                令牌
 | 
			
		||||
              </Table.HeaderCell>
 | 
			
		||||
              )}
 | 
			
		||||
              <Table.HeaderCell
 | 
			
		||||
                style={{ cursor: 'pointer' }}
 | 
			
		||||
                onClick={() => {
 | 
			
		||||
@@ -294,33 +429,57 @@ const LogsTable = () => {
 | 
			
		||||
              >
 | 
			
		||||
                模型
 | 
			
		||||
              </Table.HeaderCell>
 | 
			
		||||
              <Table.HeaderCell
 | 
			
		||||
                style={{ cursor: 'pointer' }}
 | 
			
		||||
                onClick={() => {
 | 
			
		||||
                  sortLog('prompt_tokens');
 | 
			
		||||
                }}
 | 
			
		||||
                width={1}
 | 
			
		||||
              >
 | 
			
		||||
                提示
 | 
			
		||||
              </Table.HeaderCell>
 | 
			
		||||
              <Table.HeaderCell
 | 
			
		||||
                style={{ cursor: 'pointer' }}
 | 
			
		||||
                onClick={() => {
 | 
			
		||||
                  sortLog('completion_tokens');
 | 
			
		||||
                }}
 | 
			
		||||
                width={1}
 | 
			
		||||
              >
 | 
			
		||||
                补全
 | 
			
		||||
              </Table.HeaderCell>
 | 
			
		||||
              <Table.HeaderCell
 | 
			
		||||
                style={{ cursor: 'pointer' }}
 | 
			
		||||
                onClick={() => {
 | 
			
		||||
                  sortLog('quota');
 | 
			
		||||
                }}
 | 
			
		||||
                width={1}
 | 
			
		||||
              >
 | 
			
		||||
                额度
 | 
			
		||||
              </Table.HeaderCell>
 | 
			
		||||
              {showUserTokenQuota() && (
 | 
			
		||||
                <>
 | 
			
		||||
                  {isAdminUser && (
 | 
			
		||||
                    <Table.HeaderCell
 | 
			
		||||
                      style={{ cursor: 'pointer' }}
 | 
			
		||||
                      onClick={() => {
 | 
			
		||||
                        sortLog('username');
 | 
			
		||||
                      }}
 | 
			
		||||
                      width={1}
 | 
			
		||||
                    >
 | 
			
		||||
                      用户
 | 
			
		||||
                    </Table.HeaderCell>
 | 
			
		||||
                  )}
 | 
			
		||||
                  <Table.HeaderCell
 | 
			
		||||
                    style={{ cursor: 'pointer' }}
 | 
			
		||||
                    onClick={() => {
 | 
			
		||||
                      sortLog('token_name');
 | 
			
		||||
                    }}
 | 
			
		||||
                    width={1}
 | 
			
		||||
                  >
 | 
			
		||||
                    令牌
 | 
			
		||||
                  </Table.HeaderCell>
 | 
			
		||||
                  <Table.HeaderCell
 | 
			
		||||
                    style={{ cursor: 'pointer' }}
 | 
			
		||||
                    onClick={() => {
 | 
			
		||||
                      sortLog('prompt_tokens');
 | 
			
		||||
                    }}
 | 
			
		||||
                    width={1}
 | 
			
		||||
                  >
 | 
			
		||||
                    提示
 | 
			
		||||
                  </Table.HeaderCell>
 | 
			
		||||
                  <Table.HeaderCell
 | 
			
		||||
                    style={{ cursor: 'pointer' }}
 | 
			
		||||
                    onClick={() => {
 | 
			
		||||
                      sortLog('completion_tokens');
 | 
			
		||||
                    }}
 | 
			
		||||
                    width={1}
 | 
			
		||||
                  >
 | 
			
		||||
                    补全
 | 
			
		||||
                  </Table.HeaderCell>
 | 
			
		||||
                  <Table.HeaderCell
 | 
			
		||||
                    style={{ cursor: 'pointer' }}
 | 
			
		||||
                    onClick={() => {
 | 
			
		||||
                      sortLog('quota');
 | 
			
		||||
                    }}
 | 
			
		||||
                    width={1}
 | 
			
		||||
                  >
 | 
			
		||||
                    额度
 | 
			
		||||
                  </Table.HeaderCell>
 | 
			
		||||
                </>
 | 
			
		||||
              )}
 | 
			
		||||
              <Table.HeaderCell
 | 
			
		||||
                style={{ cursor: 'pointer' }}
 | 
			
		||||
                onClick={() => {
 | 
			
		||||
@@ -343,24 +502,64 @@ const LogsTable = () => {
 | 
			
		||||
                if (log.deleted) return <></>;
 | 
			
		||||
                return (
 | 
			
		||||
                  <Table.Row key={log.id}>
 | 
			
		||||
                    <Table.Cell>{renderTimestamp(log.created_at)}</Table.Cell>
 | 
			
		||||
                    {
 | 
			
		||||
                      isAdminUser && (
 | 
			
		||||
                        <Table.Cell>{log.channel ? <Label basic>{log.channel}</Label> : ''}</Table.Cell>
 | 
			
		||||
                      )
 | 
			
		||||
                    }
 | 
			
		||||
                    {
 | 
			
		||||
                      isAdminUser && (
 | 
			
		||||
                        <Table.Cell>{log.username ? <Label>{log.username}</Label> : ''}</Table.Cell>
 | 
			
		||||
                      )
 | 
			
		||||
                    }
 | 
			
		||||
                    <Table.Cell>{log.token_name ? <Label basic>{log.token_name}</Label> : ''}</Table.Cell>
 | 
			
		||||
                    <Table.Cell>
 | 
			
		||||
                      {renderTimestamp(log.created_at, log.request_id)}
 | 
			
		||||
                    </Table.Cell>
 | 
			
		||||
                    {isAdminUser && (
 | 
			
		||||
                      <Table.Cell>
 | 
			
		||||
                        {log.channel ? (
 | 
			
		||||
                          <Label
 | 
			
		||||
                            basic
 | 
			
		||||
                            as={Link}
 | 
			
		||||
                            to={`/channel/edit/${log.channel}`}
 | 
			
		||||
                          >
 | 
			
		||||
                            {log.channel}
 | 
			
		||||
                          </Label>
 | 
			
		||||
                        ) : (
 | 
			
		||||
                          ''
 | 
			
		||||
                        )}
 | 
			
		||||
                      </Table.Cell>
 | 
			
		||||
                    )}
 | 
			
		||||
                    <Table.Cell>{renderType(log.type)}</Table.Cell>
 | 
			
		||||
                    <Table.Cell>{log.model_name ? <Label basic>{log.model_name}</Label> : ''}</Table.Cell>
 | 
			
		||||
                    <Table.Cell>{log.prompt_tokens ? log.prompt_tokens : ''}</Table.Cell>
 | 
			
		||||
                    <Table.Cell>{log.completion_tokens ? log.completion_tokens : ''}</Table.Cell>
 | 
			
		||||
                    <Table.Cell>{log.quota ? renderQuota(log.quota, 6) : ''}</Table.Cell>
 | 
			
		||||
                    <Table.Cell>{log.content}</Table.Cell>
 | 
			
		||||
                    <Table.Cell>
 | 
			
		||||
                      {log.model_name ? renderColorLabel(log.model_name) : ''}
 | 
			
		||||
                    </Table.Cell>
 | 
			
		||||
                    {showUserTokenQuota() && (
 | 
			
		||||
                      <>
 | 
			
		||||
                        {isAdminUser && (
 | 
			
		||||
                          <Table.Cell>
 | 
			
		||||
                            {log.username ? (
 | 
			
		||||
                              <Label
 | 
			
		||||
                                basic
 | 
			
		||||
                                as={Link}
 | 
			
		||||
                                to={`/user/edit/${log.user_id}`}
 | 
			
		||||
                              >
 | 
			
		||||
                                {log.username}
 | 
			
		||||
                              </Label>
 | 
			
		||||
                            ) : (
 | 
			
		||||
                              ''
 | 
			
		||||
                            )}
 | 
			
		||||
                          </Table.Cell>
 | 
			
		||||
                        )}
 | 
			
		||||
                        <Table.Cell>
 | 
			
		||||
                          {log.token_name
 | 
			
		||||
                            ? renderColorLabel(log.token_name)
 | 
			
		||||
                            : ''}
 | 
			
		||||
                        </Table.Cell>
 | 
			
		||||
 | 
			
		||||
                        <Table.Cell>
 | 
			
		||||
                          {log.prompt_tokens ? log.prompt_tokens : ''}
 | 
			
		||||
                        </Table.Cell>
 | 
			
		||||
                        <Table.Cell>
 | 
			
		||||
                          {log.completion_tokens ? log.completion_tokens : ''}
 | 
			
		||||
                        </Table.Cell>
 | 
			
		||||
                        <Table.Cell>
 | 
			
		||||
                          {log.quota ? renderQuota(log.quota, 6) : ''}
 | 
			
		||||
                        </Table.Cell>
 | 
			
		||||
                      </>
 | 
			
		||||
                    )}
 | 
			
		||||
 | 
			
		||||
                    <Table.Cell>{renderDetail(log)}</Table.Cell>
 | 
			
		||||
                  </Table.Row>
 | 
			
		||||
                );
 | 
			
		||||
              })}
 | 
			
		||||
@@ -379,7 +578,9 @@ const LogsTable = () => {
 | 
			
		||||
                    setLogType(value);
 | 
			
		||||
                  }}
 | 
			
		||||
                />
 | 
			
		||||
                <Button size='small' onClick={refresh} loading={loading}>刷新</Button>
 | 
			
		||||
                <Button size='small' onClick={refresh} loading={loading}>
 | 
			
		||||
                  刷新
 | 
			
		||||
                </Button>
 | 
			
		||||
                <Pagination
 | 
			
		||||
                  floated='right'
 | 
			
		||||
                  activePage={activePage}
 | 
			
		||||
@@ -395,7 +596,7 @@ const LogsTable = () => {
 | 
			
		||||
            </Table.Row>
 | 
			
		||||
          </Table.Footer>
 | 
			
		||||
        </Table>
 | 
			
		||||
      </Segment>
 | 
			
		||||
      </>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,21 @@
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import { Button, Form, Grid, Header, Image, Segment } from 'semantic-ui-react';
 | 
			
		||||
import { API, copy, showError, showInfo, showNotice, showSuccess } from '../helpers';
 | 
			
		||||
import {
 | 
			
		||||
  Button,
 | 
			
		||||
  Form,
 | 
			
		||||
  Grid,
 | 
			
		||||
  Header,
 | 
			
		||||
  Image,
 | 
			
		||||
  Card,
 | 
			
		||||
  Message,
 | 
			
		||||
} from 'semantic-ui-react';
 | 
			
		||||
import {
 | 
			
		||||
  API,
 | 
			
		||||
  copy,
 | 
			
		||||
  showError,
 | 
			
		||||
  showInfo,
 | 
			
		||||
  showNotice,
 | 
			
		||||
  showSuccess,
 | 
			
		||||
} from '../helpers';
 | 
			
		||||
import { useSearchParams } from 'react-router-dom';
 | 
			
		||||
 | 
			
		||||
const PasswordResetConfirm = () => {
 | 
			
		||||
@@ -63,48 +78,79 @@ const PasswordResetConfirm = () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <Grid textAlign='center' style={{ marginTop: '48px' }}>
 | 
			
		||||
      <Grid.Column style={{ maxWidth: 450 }}>
 | 
			
		||||
        <Header as='h2' color='' textAlign='center'>
 | 
			
		||||
          <Image src='/logo.png' /> 密码重置确认
 | 
			
		||||
        </Header>
 | 
			
		||||
        <Form size='large'>
 | 
			
		||||
          <Segment>
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              fluid
 | 
			
		||||
              icon='mail'
 | 
			
		||||
              iconPosition='left'
 | 
			
		||||
              placeholder='邮箱地址'
 | 
			
		||||
              name='email'
 | 
			
		||||
              value={email}
 | 
			
		||||
              readOnly
 | 
			
		||||
            />
 | 
			
		||||
            {newPassword && (
 | 
			
		||||
        <Card
 | 
			
		||||
          fluid
 | 
			
		||||
          className='chart-card'
 | 
			
		||||
          style={{ boxShadow: '0 1px 3px rgba(0,0,0,0.12)' }}
 | 
			
		||||
        >
 | 
			
		||||
          <Card.Content>
 | 
			
		||||
            <Card.Header>
 | 
			
		||||
              <Header
 | 
			
		||||
                as='h2'
 | 
			
		||||
                textAlign='center'
 | 
			
		||||
                style={{ marginBottom: '1.5em' }}
 | 
			
		||||
              >
 | 
			
		||||
                <Image src='/logo.png' style={{ marginBottom: '10px' }} />
 | 
			
		||||
                <Header.Content>密码重置确认</Header.Content>
 | 
			
		||||
              </Header>
 | 
			
		||||
            </Card.Header>
 | 
			
		||||
            <Form size='large'>
 | 
			
		||||
              <Form.Input
 | 
			
		||||
              fluid
 | 
			
		||||
              icon='lock'
 | 
			
		||||
              iconPosition='left'
 | 
			
		||||
              placeholder='新密码'
 | 
			
		||||
              name='newPassword'
 | 
			
		||||
              value={newPassword}
 | 
			
		||||
              readOnly
 | 
			
		||||
              onClick={(e) => {
 | 
			
		||||
                e.target.select();
 | 
			
		||||
                navigator.clipboard.writeText(newPassword);
 | 
			
		||||
                showNotice(`密码已复制到剪贴板:${newPassword}`);
 | 
			
		||||
              }}
 | 
			
		||||
            />            
 | 
			
		||||
                fluid
 | 
			
		||||
                icon='mail'
 | 
			
		||||
                iconPosition='left'
 | 
			
		||||
                placeholder='邮箱地址'
 | 
			
		||||
                name='email'
 | 
			
		||||
                value={email}
 | 
			
		||||
                readOnly
 | 
			
		||||
                style={{ marginBottom: '1em' }}
 | 
			
		||||
              />
 | 
			
		||||
              {newPassword && (
 | 
			
		||||
                <Form.Input
 | 
			
		||||
                  fluid
 | 
			
		||||
                  icon='lock'
 | 
			
		||||
                  iconPosition='left'
 | 
			
		||||
                  placeholder='新密码'
 | 
			
		||||
                  name='newPassword'
 | 
			
		||||
                  value={newPassword}
 | 
			
		||||
                  readOnly
 | 
			
		||||
                  style={{
 | 
			
		||||
                    marginBottom: '1em',
 | 
			
		||||
                    cursor: 'pointer',
 | 
			
		||||
                    backgroundColor: '#f8f9fa',
 | 
			
		||||
                  }}
 | 
			
		||||
                  onClick={(e) => {
 | 
			
		||||
                    e.target.select();
 | 
			
		||||
                    navigator.clipboard.writeText(newPassword);
 | 
			
		||||
                    showNotice(`密码已复制到剪贴板:${newPassword}`);
 | 
			
		||||
                  }}
 | 
			
		||||
                />
 | 
			
		||||
              )}
 | 
			
		||||
              <Button
 | 
			
		||||
                color='blue'
 | 
			
		||||
                fluid
 | 
			
		||||
                size='large'
 | 
			
		||||
                onClick={handleSubmit}
 | 
			
		||||
                loading={loading}
 | 
			
		||||
                disabled={disableButton}
 | 
			
		||||
                style={{
 | 
			
		||||
                  background: '#2F73FF', // 使用更现代的蓝色
 | 
			
		||||
                  color: 'white',
 | 
			
		||||
                  marginBottom: '1.5em',
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                {disableButton ? '密码重置完成' : '提交'}
 | 
			
		||||
              </Button>
 | 
			
		||||
            </Form>
 | 
			
		||||
            {newPassword && (
 | 
			
		||||
              <Message style={{ background: 'transparent', boxShadow: 'none' }}>
 | 
			
		||||
                <p style={{ fontSize: '0.9em', color: '#666' }}>
 | 
			
		||||
                  新密码已生成,请点击密码框或上方按钮复制。请及时登录并修改密码!
 | 
			
		||||
                </p>
 | 
			
		||||
              </Message>
 | 
			
		||||
            )}
 | 
			
		||||
            <Button
 | 
			
		||||
              color='green'
 | 
			
		||||
              fluid
 | 
			
		||||
              size='large'
 | 
			
		||||
              onClick={handleSubmit}
 | 
			
		||||
              loading={loading}
 | 
			
		||||
              disabled={disableButton}
 | 
			
		||||
            >
 | 
			
		||||
              {disableButton ? `密码重置完成` : '提交'}
 | 
			
		||||
            </Button>
 | 
			
		||||
          </Segment>
 | 
			
		||||
        </Form>
 | 
			
		||||
          </Card.Content>
 | 
			
		||||
        </Card>
 | 
			
		||||
      </Grid.Column>
 | 
			
		||||
    </Grid>
 | 
			
		||||
  );
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,19 @@
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import { Button, Form, Grid, Header, Image, Segment } from 'semantic-ui-react';
 | 
			
		||||
import {
 | 
			
		||||
  Button,
 | 
			
		||||
  Form,
 | 
			
		||||
  Grid,
 | 
			
		||||
  Header,
 | 
			
		||||
  Image,
 | 
			
		||||
  Card,
 | 
			
		||||
  Message,
 | 
			
		||||
} from 'semantic-ui-react';
 | 
			
		||||
import { API, showError, showInfo, showSuccess } from '../helpers';
 | 
			
		||||
import Turnstile from 'react-turnstile';
 | 
			
		||||
 | 
			
		||||
const PasswordResetForm = () => {
 | 
			
		||||
  const [inputs, setInputs] = useState({
 | 
			
		||||
    email: ''
 | 
			
		||||
    email: '',
 | 
			
		||||
  });
 | 
			
		||||
  const { email } = inputs;
 | 
			
		||||
 | 
			
		||||
@@ -42,7 +50,7 @@ const PasswordResetForm = () => {
 | 
			
		||||
 | 
			
		||||
  function handleChange(e) {
 | 
			
		||||
    const { name, value } = e.target;
 | 
			
		||||
    setInputs(inputs => ({ ...inputs, [name]: value }));
 | 
			
		||||
    setInputs((inputs) => ({ ...inputs, [name]: value }));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async function handleSubmit(e) {
 | 
			
		||||
@@ -69,42 +77,72 @@ const PasswordResetForm = () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <Grid textAlign='center' style={{ marginTop: '48px' }}>
 | 
			
		||||
      <Grid.Column style={{ maxWidth: 450 }}>
 | 
			
		||||
        <Header as='h2' color='' textAlign='center'>
 | 
			
		||||
          <Image src='/logo.png' /> 密码重置
 | 
			
		||||
        </Header>
 | 
			
		||||
        <Form size='large'>
 | 
			
		||||
          <Segment>
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              fluid
 | 
			
		||||
              icon='mail'
 | 
			
		||||
              iconPosition='left'
 | 
			
		||||
              placeholder='邮箱地址'
 | 
			
		||||
              name='email'
 | 
			
		||||
              value={email}
 | 
			
		||||
              onChange={handleChange}
 | 
			
		||||
            />
 | 
			
		||||
            {turnstileEnabled ? (
 | 
			
		||||
              <Turnstile
 | 
			
		||||
                sitekey={turnstileSiteKey}
 | 
			
		||||
                onVerify={(token) => {
 | 
			
		||||
                  setTurnstileToken(token);
 | 
			
		||||
                }}
 | 
			
		||||
        <Card
 | 
			
		||||
          fluid
 | 
			
		||||
          className='chart-card'
 | 
			
		||||
          style={{ boxShadow: '0 1px 3px rgba(0,0,0,0.12)' }}
 | 
			
		||||
        >
 | 
			
		||||
          <Card.Content>
 | 
			
		||||
            <Card.Header>
 | 
			
		||||
              <Header
 | 
			
		||||
                as='h2'
 | 
			
		||||
                textAlign='center'
 | 
			
		||||
                style={{ marginBottom: '1.5em' }}
 | 
			
		||||
              >
 | 
			
		||||
                <Image src='/logo.png' style={{ marginBottom: '10px' }} />
 | 
			
		||||
                <Header.Content>密码重置</Header.Content>
 | 
			
		||||
              </Header>
 | 
			
		||||
            </Card.Header>
 | 
			
		||||
            <Form size='large'>
 | 
			
		||||
              <Form.Input
 | 
			
		||||
                fluid
 | 
			
		||||
                icon='mail'
 | 
			
		||||
                iconPosition='left'
 | 
			
		||||
                placeholder='邮箱地址'
 | 
			
		||||
                name='email'
 | 
			
		||||
                value={email}
 | 
			
		||||
                onChange={handleChange}
 | 
			
		||||
                style={{ marginBottom: '1em' }}
 | 
			
		||||
              />
 | 
			
		||||
            ) : (
 | 
			
		||||
              <></>
 | 
			
		||||
            )}
 | 
			
		||||
            <Button
 | 
			
		||||
              color='green'
 | 
			
		||||
              fluid
 | 
			
		||||
              size='large'
 | 
			
		||||
              onClick={handleSubmit}
 | 
			
		||||
              loading={loading}
 | 
			
		||||
              disabled={disableButton}
 | 
			
		||||
            >
 | 
			
		||||
              {disableButton ? `重试 (${countdown})` : '提交'}
 | 
			
		||||
            </Button>
 | 
			
		||||
          </Segment>
 | 
			
		||||
        </Form>
 | 
			
		||||
              {turnstileEnabled && (
 | 
			
		||||
                <div
 | 
			
		||||
                  style={{
 | 
			
		||||
                    marginBottom: '1em',
 | 
			
		||||
                    display: 'flex',
 | 
			
		||||
                    justifyContent: 'center',
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  <Turnstile
 | 
			
		||||
                    sitekey={turnstileSiteKey}
 | 
			
		||||
                    onVerify={(token) => {
 | 
			
		||||
                      setTurnstileToken(token);
 | 
			
		||||
                    }}
 | 
			
		||||
                  />
 | 
			
		||||
                </div>
 | 
			
		||||
              )}
 | 
			
		||||
              <Button
 | 
			
		||||
                color='blue'
 | 
			
		||||
                fluid
 | 
			
		||||
                size='large'
 | 
			
		||||
                onClick={handleSubmit}
 | 
			
		||||
                loading={loading}
 | 
			
		||||
                disabled={disableButton}
 | 
			
		||||
                style={{
 | 
			
		||||
                  background: '#2F73FF', // 使用更现代的蓝色
 | 
			
		||||
                  color: 'white',
 | 
			
		||||
                  marginBottom: '1.5em',
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                {disableButton ? `重试 (${countdown})` : '提交'}
 | 
			
		||||
              </Button>
 | 
			
		||||
            </Form>
 | 
			
		||||
            <Message style={{ background: 'transparent', boxShadow: 'none' }}>
 | 
			
		||||
              <p style={{ fontSize: '0.9em', color: '#666' }}>
 | 
			
		||||
                系统将向您的邮箱发送一封包含重置链接的邮件,请注意查收。
 | 
			
		||||
              </p>
 | 
			
		||||
            </Message>
 | 
			
		||||
          </Card.Content>
 | 
			
		||||
        </Card>
 | 
			
		||||
      </Grid.Column>
 | 
			
		||||
    </Grid>
 | 
			
		||||
  );
 | 
			
		||||
 
 | 
			
		||||
@@ -1,29 +1,59 @@
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import { Button, Form, Label, Popup, Pagination, Table } from 'semantic-ui-react';
 | 
			
		||||
import {
 | 
			
		||||
  Button,
 | 
			
		||||
  Form,
 | 
			
		||||
  Label,
 | 
			
		||||
  Popup,
 | 
			
		||||
  Pagination,
 | 
			
		||||
  Table,
 | 
			
		||||
} from 'semantic-ui-react';
 | 
			
		||||
import { Link } from 'react-router-dom';
 | 
			
		||||
import { API, copy, showError, showInfo, showSuccess, showWarning, timestamp2string } from '../helpers';
 | 
			
		||||
import {
 | 
			
		||||
  API,
 | 
			
		||||
  copy,
 | 
			
		||||
  showError,
 | 
			
		||||
  showInfo,
 | 
			
		||||
  showSuccess,
 | 
			
		||||
  showWarning,
 | 
			
		||||
  timestamp2string,
 | 
			
		||||
} from '../helpers';
 | 
			
		||||
 | 
			
		||||
import { ITEMS_PER_PAGE } from '../constants';
 | 
			
		||||
import { renderQuota } from '../helpers/render';
 | 
			
		||||
 | 
			
		||||
function renderTimestamp(timestamp) {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      {timestamp2string(timestamp)}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
  return <>{timestamp2string(timestamp)}</>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function renderStatus(status) {
 | 
			
		||||
  switch (status) {
 | 
			
		||||
    case 1:
 | 
			
		||||
      return <Label basic color='green'>未使用</Label>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Label basic color='green'>
 | 
			
		||||
          未使用
 | 
			
		||||
        </Label>
 | 
			
		||||
      );
 | 
			
		||||
    case 2:
 | 
			
		||||
      return <Label basic color='red'> 已禁用 </Label>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Label basic color='red'>
 | 
			
		||||
          {' '}
 | 
			
		||||
          已禁用{' '}
 | 
			
		||||
        </Label>
 | 
			
		||||
      );
 | 
			
		||||
    case 3:
 | 
			
		||||
      return <Label basic color='grey'> 已使用 </Label>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Label basic color='grey'>
 | 
			
		||||
          {' '}
 | 
			
		||||
          已使用{' '}
 | 
			
		||||
        </Label>
 | 
			
		||||
      );
 | 
			
		||||
    default:
 | 
			
		||||
      return <Label basic color='black'> 未知状态 </Label>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Label basic color='black'>
 | 
			
		||||
          {' '}
 | 
			
		||||
          未知状态{' '}
 | 
			
		||||
        </Label>
 | 
			
		||||
      );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -110,7 +140,9 @@ const RedemptionsTable = () => {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    setSearching(true);
 | 
			
		||||
    const res = await API.get(`/api/redemption/search?keyword=${searchKeyword}`);
 | 
			
		||||
    const res = await API.get(
 | 
			
		||||
      `/api/redemption/search?keyword=${searchKeyword}`
 | 
			
		||||
    );
 | 
			
		||||
    const { success, message, data } = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
      setRedemptions(data);
 | 
			
		||||
@@ -159,7 +191,7 @@ const RedemptionsTable = () => {
 | 
			
		||||
        />
 | 
			
		||||
      </Form>
 | 
			
		||||
 | 
			
		||||
      <Table basic compact size='small'>
 | 
			
		||||
      <Table basic={'very'} compact size='small'>
 | 
			
		||||
        <Table.Header>
 | 
			
		||||
          <Table.Row>
 | 
			
		||||
            <Table.HeaderCell
 | 
			
		||||
@@ -225,11 +257,19 @@ const RedemptionsTable = () => {
 | 
			
		||||
              return (
 | 
			
		||||
                <Table.Row key={redemption.id}>
 | 
			
		||||
                  <Table.Cell>{redemption.id}</Table.Cell>
 | 
			
		||||
                  <Table.Cell>{redemption.name ? redemption.name : '无'}</Table.Cell>
 | 
			
		||||
                  <Table.Cell>
 | 
			
		||||
                    {redemption.name ? redemption.name : '无'}
 | 
			
		||||
                  </Table.Cell>
 | 
			
		||||
                  <Table.Cell>{renderStatus(redemption.status)}</Table.Cell>
 | 
			
		||||
                  <Table.Cell>{renderQuota(redemption.quota)}</Table.Cell>
 | 
			
		||||
                  <Table.Cell>{renderTimestamp(redemption.created_time)}</Table.Cell>
 | 
			
		||||
                  <Table.Cell>{redemption.redeemed_time ? renderTimestamp(redemption.redeemed_time) : "尚未兑换"} </Table.Cell>
 | 
			
		||||
                  <Table.Cell>
 | 
			
		||||
                    {renderTimestamp(redemption.created_time)}
 | 
			
		||||
                  </Table.Cell>
 | 
			
		||||
                  <Table.Cell>
 | 
			
		||||
                    {redemption.redeemed_time
 | 
			
		||||
                      ? renderTimestamp(redemption.redeemed_time)
 | 
			
		||||
                      : '尚未兑换'}{' '}
 | 
			
		||||
                  </Table.Cell>
 | 
			
		||||
                  <Table.Cell>
 | 
			
		||||
                    <div>
 | 
			
		||||
                      <Button
 | 
			
		||||
@@ -239,7 +279,9 @@ const RedemptionsTable = () => {
 | 
			
		||||
                          if (await copy(redemption.key)) {
 | 
			
		||||
                            showSuccess('已复制到剪贴板!');
 | 
			
		||||
                          } else {
 | 
			
		||||
                            showWarning('无法复制到剪贴板,请手动复制,已将兑换码填入搜索框。')
 | 
			
		||||
                            showWarning(
 | 
			
		||||
                              '无法复制到剪贴板,请手动复制,已将兑换码填入搜索框。'
 | 
			
		||||
                            );
 | 
			
		||||
                            setSearchKeyword(redemption.key);
 | 
			
		||||
                          }
 | 
			
		||||
                        }}
 | 
			
		||||
@@ -267,7 +309,7 @@ const RedemptionsTable = () => {
 | 
			
		||||
                      </Popup>
 | 
			
		||||
                      <Button
 | 
			
		||||
                        size={'small'}
 | 
			
		||||
                        disabled={redemption.status === 3}  // used
 | 
			
		||||
                        disabled={redemption.status === 3} // used
 | 
			
		||||
                        onClick={() => {
 | 
			
		||||
                          manageRedemption(
 | 
			
		||||
                            redemption.id,
 | 
			
		||||
@@ -295,7 +337,12 @@ const RedemptionsTable = () => {
 | 
			
		||||
        <Table.Footer>
 | 
			
		||||
          <Table.Row>
 | 
			
		||||
            <Table.HeaderCell colSpan='8'>
 | 
			
		||||
              <Button size='small' as={Link} to='/redemption/add' loading={loading}>
 | 
			
		||||
              <Button
 | 
			
		||||
                size='small'
 | 
			
		||||
                as={Link}
 | 
			
		||||
                to='/redemption/add'
 | 
			
		||||
                loading={loading}
 | 
			
		||||
              >
 | 
			
		||||
                添加新的兑换码
 | 
			
		||||
              </Button>
 | 
			
		||||
              <Pagination
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,15 @@
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import { Button, Form, Grid, Header, Image, Message, Segment } from 'semantic-ui-react';
 | 
			
		||||
import {
 | 
			
		||||
  Button,
 | 
			
		||||
  Form,
 | 
			
		||||
  Grid,
 | 
			
		||||
  Header,
 | 
			
		||||
  Image,
 | 
			
		||||
  Message,
 | 
			
		||||
  Segment,
 | 
			
		||||
  Card,
 | 
			
		||||
  Divider,
 | 
			
		||||
} from 'semantic-ui-react';
 | 
			
		||||
import { Link, useNavigate } from 'react-router-dom';
 | 
			
		||||
import { API, getLogo, showError, showInfo, showSuccess } from '../helpers';
 | 
			
		||||
import Turnstile from 'react-turnstile';
 | 
			
		||||
@@ -10,7 +20,7 @@ const RegisterForm = () => {
 | 
			
		||||
    password: '',
 | 
			
		||||
    password2: '',
 | 
			
		||||
    email: '',
 | 
			
		||||
    verification_code: ''
 | 
			
		||||
    verification_code: '',
 | 
			
		||||
  });
 | 
			
		||||
  const { username, password, password2 } = inputs;
 | 
			
		||||
  const [showEmailVerification, setShowEmailVerification] = useState(false);
 | 
			
		||||
@@ -100,92 +110,135 @@ const RegisterForm = () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <Grid textAlign='center' style={{ marginTop: '48px' }}>
 | 
			
		||||
      <Grid.Column style={{ maxWidth: 450 }}>
 | 
			
		||||
        <Header as='h2' color='' textAlign='center'>
 | 
			
		||||
          <Image src={logo} /> 新用户注册
 | 
			
		||||
        </Header>
 | 
			
		||||
        <Form size='large'>
 | 
			
		||||
          <Segment>
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              fluid
 | 
			
		||||
              icon='user'
 | 
			
		||||
              iconPosition='left'
 | 
			
		||||
              placeholder='输入用户名,最长 12 位'
 | 
			
		||||
              onChange={handleChange}
 | 
			
		||||
              name='username'
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              fluid
 | 
			
		||||
              icon='lock'
 | 
			
		||||
              iconPosition='left'
 | 
			
		||||
              placeholder='输入密码,最短 8 位,最长 20 位'
 | 
			
		||||
              onChange={handleChange}
 | 
			
		||||
              name='password'
 | 
			
		||||
              type='password'
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              fluid
 | 
			
		||||
              icon='lock'
 | 
			
		||||
              iconPosition='left'
 | 
			
		||||
              placeholder='输入密码,最短 8 位,最长 20 位'
 | 
			
		||||
              onChange={handleChange}
 | 
			
		||||
              name='password2'
 | 
			
		||||
              type='password'
 | 
			
		||||
            />
 | 
			
		||||
            {showEmailVerification ? (
 | 
			
		||||
              <>
 | 
			
		||||
                <Form.Input
 | 
			
		||||
                  fluid
 | 
			
		||||
                  icon='mail'
 | 
			
		||||
                  iconPosition='left'
 | 
			
		||||
                  placeholder='输入邮箱地址'
 | 
			
		||||
                  onChange={handleChange}
 | 
			
		||||
                  name='email'
 | 
			
		||||
                  type='email'
 | 
			
		||||
                  action={
 | 
			
		||||
                    <Button onClick={sendVerificationCode} disabled={loading}>
 | 
			
		||||
                      获取验证码
 | 
			
		||||
                    </Button>
 | 
			
		||||
                  }
 | 
			
		||||
                />
 | 
			
		||||
                <Form.Input
 | 
			
		||||
                  fluid
 | 
			
		||||
                  icon='lock'
 | 
			
		||||
                  iconPosition='left'
 | 
			
		||||
                  placeholder='输入验证码'
 | 
			
		||||
                  onChange={handleChange}
 | 
			
		||||
                  name='verification_code'
 | 
			
		||||
                />
 | 
			
		||||
              </>
 | 
			
		||||
            ) : (
 | 
			
		||||
              <></>
 | 
			
		||||
            )}
 | 
			
		||||
            {turnstileEnabled ? (
 | 
			
		||||
              <Turnstile
 | 
			
		||||
                sitekey={turnstileSiteKey}
 | 
			
		||||
                onVerify={(token) => {
 | 
			
		||||
                  setTurnstileToken(token);
 | 
			
		||||
                }}
 | 
			
		||||
        <Card
 | 
			
		||||
          fluid
 | 
			
		||||
          className='chart-card'
 | 
			
		||||
          style={{ boxShadow: '0 1px 3px rgba(0,0,0,0.12)' }}
 | 
			
		||||
        >
 | 
			
		||||
          <Card.Content>
 | 
			
		||||
            <Card.Header>
 | 
			
		||||
              <Header
 | 
			
		||||
                as='h2'
 | 
			
		||||
                textAlign='center'
 | 
			
		||||
                style={{ marginBottom: '1.5em' }}
 | 
			
		||||
              >
 | 
			
		||||
                <Image src={logo} style={{ marginBottom: '10px' }} />
 | 
			
		||||
                <Header.Content>新用户注册</Header.Content>
 | 
			
		||||
              </Header>
 | 
			
		||||
            </Card.Header>
 | 
			
		||||
            <Form size='large'>
 | 
			
		||||
              <Form.Input
 | 
			
		||||
                fluid
 | 
			
		||||
                icon='user'
 | 
			
		||||
                iconPosition='left'
 | 
			
		||||
                placeholder='输入用户名,最长 12 位'
 | 
			
		||||
                onChange={handleChange}
 | 
			
		||||
                name='username'
 | 
			
		||||
                style={{ marginBottom: '1em' }}
 | 
			
		||||
              />
 | 
			
		||||
            ) : (
 | 
			
		||||
              <></>
 | 
			
		||||
            )}
 | 
			
		||||
            <Button
 | 
			
		||||
              color='green'
 | 
			
		||||
              fluid
 | 
			
		||||
              size='large'
 | 
			
		||||
              onClick={handleSubmit}
 | 
			
		||||
              loading={loading}
 | 
			
		||||
            >
 | 
			
		||||
              注册
 | 
			
		||||
            </Button>
 | 
			
		||||
          </Segment>
 | 
			
		||||
        </Form>
 | 
			
		||||
        <Message>
 | 
			
		||||
          已有账户?
 | 
			
		||||
          <Link to='/login' className='btn btn-link'>
 | 
			
		||||
            点击登录
 | 
			
		||||
          </Link>
 | 
			
		||||
        </Message>
 | 
			
		||||
              <Form.Input
 | 
			
		||||
                fluid
 | 
			
		||||
                icon='lock'
 | 
			
		||||
                iconPosition='left'
 | 
			
		||||
                placeholder='输入密码,最短 8 位,最长 20 位'
 | 
			
		||||
                onChange={handleChange}
 | 
			
		||||
                name='password'
 | 
			
		||||
                type='password'
 | 
			
		||||
                style={{ marginBottom: '1em' }}
 | 
			
		||||
              />
 | 
			
		||||
              <Form.Input
 | 
			
		||||
                fluid
 | 
			
		||||
                icon='lock'
 | 
			
		||||
                iconPosition='left'
 | 
			
		||||
                placeholder='再次输入密码'
 | 
			
		||||
                onChange={handleChange}
 | 
			
		||||
                name='password2'
 | 
			
		||||
                type='password'
 | 
			
		||||
                style={{ marginBottom: '1em' }}
 | 
			
		||||
              />
 | 
			
		||||
 | 
			
		||||
              {showEmailVerification && (
 | 
			
		||||
                <>
 | 
			
		||||
                  <Form.Input
 | 
			
		||||
                    fluid
 | 
			
		||||
                    icon='mail'
 | 
			
		||||
                    iconPosition='left'
 | 
			
		||||
                    placeholder='输入邮箱地址'
 | 
			
		||||
                    onChange={handleChange}
 | 
			
		||||
                    name='email'
 | 
			
		||||
                    type='email'
 | 
			
		||||
                    action={
 | 
			
		||||
                      <Button
 | 
			
		||||
                        onClick={sendVerificationCode}
 | 
			
		||||
                        disabled={loading}
 | 
			
		||||
                        style={{ backgroundColor: '#2185d0', color: 'white' }}
 | 
			
		||||
                      >
 | 
			
		||||
                        获取验证码
 | 
			
		||||
                      </Button>
 | 
			
		||||
                    }
 | 
			
		||||
                    style={{ marginBottom: '1em' }}
 | 
			
		||||
                  />
 | 
			
		||||
                  <Form.Input
 | 
			
		||||
                    fluid
 | 
			
		||||
                    icon='lock'
 | 
			
		||||
                    iconPosition='left'
 | 
			
		||||
                    placeholder='输入验证码'
 | 
			
		||||
                    onChange={handleChange}
 | 
			
		||||
                    name='verification_code'
 | 
			
		||||
                    style={{ marginBottom: '1em' }}
 | 
			
		||||
                  />
 | 
			
		||||
                </>
 | 
			
		||||
              )}
 | 
			
		||||
 | 
			
		||||
              {turnstileEnabled && (
 | 
			
		||||
                <div
 | 
			
		||||
                  style={{
 | 
			
		||||
                    marginBottom: '1em',
 | 
			
		||||
                    display: 'flex',
 | 
			
		||||
                    justifyContent: 'center',
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  <Turnstile
 | 
			
		||||
                    sitekey={turnstileSiteKey}
 | 
			
		||||
                    onVerify={(token) => {
 | 
			
		||||
                      setTurnstileToken(token);
 | 
			
		||||
                    }}
 | 
			
		||||
                  />
 | 
			
		||||
                </div>
 | 
			
		||||
              )}
 | 
			
		||||
 | 
			
		||||
              <Button
 | 
			
		||||
                fluid
 | 
			
		||||
                size='large'
 | 
			
		||||
                onClick={handleSubmit}
 | 
			
		||||
                style={{
 | 
			
		||||
                  background: '#2F73FF', // 使用更现代的蓝色
 | 
			
		||||
                  color: 'white',
 | 
			
		||||
                  marginBottom: '1.5em',
 | 
			
		||||
                }}
 | 
			
		||||
                loading={loading}
 | 
			
		||||
              >
 | 
			
		||||
                注册
 | 
			
		||||
              </Button>
 | 
			
		||||
            </Form>
 | 
			
		||||
 | 
			
		||||
            <Divider />
 | 
			
		||||
            <Message style={{ background: 'transparent', boxShadow: 'none' }}>
 | 
			
		||||
              <div
 | 
			
		||||
                style={{
 | 
			
		||||
                  textAlign: 'center',
 | 
			
		||||
                  fontSize: '0.9em',
 | 
			
		||||
                  color: '#666',
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                已有账户?
 | 
			
		||||
                <Link to='/login' style={{ color: '#2185d0' }}>
 | 
			
		||||
                  点击登录
 | 
			
		||||
                </Link>
 | 
			
		||||
              </div>
 | 
			
		||||
            </Message>
 | 
			
		||||
          </Card.Content>
 | 
			
		||||
        </Card>
 | 
			
		||||
      </Grid.Column>
 | 
			
		||||
    </Grid>
 | 
			
		||||
  );
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,22 @@
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import { Button, Dropdown, Form, Label, Pagination, Popup, Table } from 'semantic-ui-react';
 | 
			
		||||
import {
 | 
			
		||||
  Button,
 | 
			
		||||
  Dropdown,
 | 
			
		||||
  Form,
 | 
			
		||||
  Label,
 | 
			
		||||
  Pagination,
 | 
			
		||||
  Popup,
 | 
			
		||||
  Table,
 | 
			
		||||
} from 'semantic-ui-react';
 | 
			
		||||
import { Link } from 'react-router-dom';
 | 
			
		||||
import { API, copy, showError, showSuccess, showWarning, timestamp2string } from '../helpers';
 | 
			
		||||
import {
 | 
			
		||||
  API,
 | 
			
		||||
  copy,
 | 
			
		||||
  showError,
 | 
			
		||||
  showSuccess,
 | 
			
		||||
  showWarning,
 | 
			
		||||
  timestamp2string,
 | 
			
		||||
} from '../helpers';
 | 
			
		||||
 | 
			
		||||
import { ITEMS_PER_PAGE } from '../constants';
 | 
			
		||||
import { renderQuota } from '../helpers/render';
 | 
			
		||||
@@ -21,25 +36,45 @@ const OPEN_LINK_OPTIONS = [
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
function renderTimestamp(timestamp) {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      {timestamp2string(timestamp)}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
  return <>{timestamp2string(timestamp)}</>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function renderStatus(status) {
 | 
			
		||||
  switch (status) {
 | 
			
		||||
    case 1:
 | 
			
		||||
      return <Label basic color='green'>已启用</Label>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Label basic color='green'>
 | 
			
		||||
          已启用
 | 
			
		||||
        </Label>
 | 
			
		||||
      );
 | 
			
		||||
    case 2:
 | 
			
		||||
      return <Label basic color='red'> 已禁用 </Label>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Label basic color='red'>
 | 
			
		||||
          {' '}
 | 
			
		||||
          已禁用{' '}
 | 
			
		||||
        </Label>
 | 
			
		||||
      );
 | 
			
		||||
    case 3:
 | 
			
		||||
      return <Label basic color='yellow'> 已过期 </Label>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Label basic color='yellow'>
 | 
			
		||||
          {' '}
 | 
			
		||||
          已过期{' '}
 | 
			
		||||
        </Label>
 | 
			
		||||
      );
 | 
			
		||||
    case 4:
 | 
			
		||||
      return <Label basic color='grey'> 已耗尽 </Label>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Label basic color='grey'>
 | 
			
		||||
          {' '}
 | 
			
		||||
          已耗尽{' '}
 | 
			
		||||
        </Label>
 | 
			
		||||
      );
 | 
			
		||||
    default:
 | 
			
		||||
      return <Label basic color='black'> 未知状态 </Label>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Label basic color='black'>
 | 
			
		||||
          {' '}
 | 
			
		||||
          未知状态{' '}
 | 
			
		||||
        </Label>
 | 
			
		||||
      );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -100,7 +135,8 @@ const TokensTable = () => {
 | 
			
		||||
    let nextUrl;
 | 
			
		||||
 | 
			
		||||
    if (nextLink) {
 | 
			
		||||
      nextUrl = nextLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
 | 
			
		||||
      nextUrl =
 | 
			
		||||
        nextLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
 | 
			
		||||
    } else {
 | 
			
		||||
      nextUrl = `https://app.nextchat.dev/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
 | 
			
		||||
    }
 | 
			
		||||
@@ -117,7 +153,9 @@ const TokensTable = () => {
 | 
			
		||||
        url = nextUrl;
 | 
			
		||||
        break;
 | 
			
		||||
      case 'lobechat':
 | 
			
		||||
        url = nextLink + `/?settings={"keyVaults":{"openai":{"apiKey":"sk-${key}","baseURL":"${serverAddress}/v1"}}}`;
 | 
			
		||||
        url =
 | 
			
		||||
          nextLink +
 | 
			
		||||
          `/?settings={"keyVaults":{"openai":{"apiKey":"sk-${key}","baseURL":"${serverAddress}/v1"}}}`;
 | 
			
		||||
        break;
 | 
			
		||||
      default:
 | 
			
		||||
        url = `sk-${key}`;
 | 
			
		||||
@@ -145,7 +183,8 @@ const TokensTable = () => {
 | 
			
		||||
    let defaultUrl;
 | 
			
		||||
 | 
			
		||||
    if (chatLink) {
 | 
			
		||||
      defaultUrl = chatLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
 | 
			
		||||
      defaultUrl =
 | 
			
		||||
        chatLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
 | 
			
		||||
    } else {
 | 
			
		||||
      defaultUrl = `https://app.nextchat.dev/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
 | 
			
		||||
    }
 | 
			
		||||
@@ -160,7 +199,9 @@ const TokensTable = () => {
 | 
			
		||||
        break;
 | 
			
		||||
 | 
			
		||||
      case 'lobechat':
 | 
			
		||||
        url = chatLink + `/?settings={"keyVaults":{"openai":{"apiKey":"sk-${key}","baseURL":"${serverAddress}/v1"}}}`;
 | 
			
		||||
        url =
 | 
			
		||||
          chatLink +
 | 
			
		||||
          `/?settings={"keyVaults":{"openai":{"apiKey":"sk-${key}","baseURL":"${serverAddress}/v1"}}}`;
 | 
			
		||||
        break;
 | 
			
		||||
 | 
			
		||||
      default:
 | 
			
		||||
@@ -168,7 +209,7 @@ const TokensTable = () => {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    window.open(url, '_blank');
 | 
			
		||||
  }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    loadTokens(0, orderBy)
 | 
			
		||||
@@ -274,7 +315,7 @@ const TokensTable = () => {
 | 
			
		||||
        />
 | 
			
		||||
      </Form>
 | 
			
		||||
 | 
			
		||||
      <Table basic compact size='small'>
 | 
			
		||||
      <Table basic={'very'} compact size='small'>
 | 
			
		||||
        <Table.Header>
 | 
			
		||||
          <Table.Row>
 | 
			
		||||
            <Table.HeaderCell
 | 
			
		||||
@@ -342,12 +383,20 @@ const TokensTable = () => {
 | 
			
		||||
                  <Table.Cell>{token.name ? token.name : '无'}</Table.Cell>
 | 
			
		||||
                  <Table.Cell>{renderStatus(token.status)}</Table.Cell>
 | 
			
		||||
                  <Table.Cell>{renderQuota(token.used_quota)}</Table.Cell>
 | 
			
		||||
                  <Table.Cell>{token.unlimited_quota ? '无限制' : renderQuota(token.remain_quota, 2)}</Table.Cell>
 | 
			
		||||
                  <Table.Cell>
 | 
			
		||||
                    {token.unlimited_quota
 | 
			
		||||
                      ? '无限制'
 | 
			
		||||
                      : renderQuota(token.remain_quota, 2)}
 | 
			
		||||
                  </Table.Cell>
 | 
			
		||||
                  <Table.Cell>{renderTimestamp(token.created_time)}</Table.Cell>
 | 
			
		||||
                  <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>
 | 
			
		||||
                    <div>
 | 
			
		||||
                    <Button.Group color='green' size={'small'}>
 | 
			
		||||
                      <Button.Group color='green' size={'small'}>
 | 
			
		||||
                        <Button
 | 
			
		||||
                          size={'small'}
 | 
			
		||||
                          positive
 | 
			
		||||
@@ -360,38 +409,37 @@ const TokensTable = () => {
 | 
			
		||||
                        <Dropdown
 | 
			
		||||
                          className='button icon'
 | 
			
		||||
                          floating
 | 
			
		||||
                          options={COPY_OPTIONS.map(option => ({
 | 
			
		||||
                          options={COPY_OPTIONS.map((option) => ({
 | 
			
		||||
                            ...option,
 | 
			
		||||
                            onClick: async () => {
 | 
			
		||||
                              await onCopy(option.value, token.key);
 | 
			
		||||
                            }
 | 
			
		||||
                            },
 | 
			
		||||
                          }))}
 | 
			
		||||
                          trigger={<></>}
 | 
			
		||||
                        />
 | 
			
		||||
                      </Button.Group>
 | 
			
		||||
                      {' '}
 | 
			
		||||
                      </Button.Group>{' '}
 | 
			
		||||
                      <Button.Group color='blue' size={'small'}>
 | 
			
		||||
                        <Button
 | 
			
		||||
                            size={'small'}
 | 
			
		||||
                            positive
 | 
			
		||||
                            onClick={() => {     
 | 
			
		||||
                              onOpenLink('', token.key);       
 | 
			
		||||
                            }}>
 | 
			
		||||
                            聊天
 | 
			
		||||
                          </Button>
 | 
			
		||||
                          <Dropdown   
 | 
			
		||||
                            className="button icon"       
 | 
			
		||||
                            floating
 | 
			
		||||
                            options={OPEN_LINK_OPTIONS.map(option => ({
 | 
			
		||||
                              ...option,
 | 
			
		||||
                              onClick: async () => {
 | 
			
		||||
                                await onOpenLink(option.value, token.key);
 | 
			
		||||
                              }
 | 
			
		||||
                            }))}       
 | 
			
		||||
                            trigger={<></>}   
 | 
			
		||||
                          />
 | 
			
		||||
                      </Button.Group>
 | 
			
		||||
                      {' '}
 | 
			
		||||
                          size={'small'}
 | 
			
		||||
                          positive
 | 
			
		||||
                          onClick={() => {
 | 
			
		||||
                            onOpenLink('', token.key);
 | 
			
		||||
                          }}
 | 
			
		||||
                        >
 | 
			
		||||
                          聊天
 | 
			
		||||
                        </Button>
 | 
			
		||||
                        <Dropdown
 | 
			
		||||
                          className='button icon'
 | 
			
		||||
                          floating
 | 
			
		||||
                          options={OPEN_LINK_OPTIONS.map((option) => ({
 | 
			
		||||
                            ...option,
 | 
			
		||||
                            onClick: async () => {
 | 
			
		||||
                              await onOpenLink(option.value, token.key);
 | 
			
		||||
                            },
 | 
			
		||||
                          }))}
 | 
			
		||||
                          trigger={<></>}
 | 
			
		||||
                        />
 | 
			
		||||
                      </Button.Group>{' '}
 | 
			
		||||
                      <Popup
 | 
			
		||||
                        trigger={
 | 
			
		||||
                          <Button size='small' negative>
 | 
			
		||||
@@ -443,14 +491,24 @@ const TokensTable = () => {
 | 
			
		||||
              <Button size='small' as={Link} to='/token/add' loading={loading}>
 | 
			
		||||
                添加新的令牌
 | 
			
		||||
              </Button>
 | 
			
		||||
              <Button size='small' onClick={refresh} loading={loading}>刷新</Button>
 | 
			
		||||
              <Button size='small' onClick={refresh} loading={loading}>
 | 
			
		||||
                刷新
 | 
			
		||||
              </Button>
 | 
			
		||||
              <Dropdown
 | 
			
		||||
                placeholder='排序方式'
 | 
			
		||||
                selection
 | 
			
		||||
                options={[
 | 
			
		||||
                  { key: '', text: '默认排序', value: '' },
 | 
			
		||||
                  { key: 'remain_quota', text: '按剩余额度排序', value: 'remain_quota' },
 | 
			
		||||
                  { key: 'used_quota', text: '按已用额度排序', value: 'used_quota' },
 | 
			
		||||
                  {
 | 
			
		||||
                    key: 'remain_quota',
 | 
			
		||||
                    text: '按剩余额度排序',
 | 
			
		||||
                    value: 'remain_quota',
 | 
			
		||||
                  },
 | 
			
		||||
                  {
 | 
			
		||||
                    key: 'used_quota',
 | 
			
		||||
                    text: '按已用额度排序',
 | 
			
		||||
                    value: 'used_quota',
 | 
			
		||||
                  },
 | 
			
		||||
                ]}
 | 
			
		||||
                value={orderBy}
 | 
			
		||||
                onChange={handleOrderByChange}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,23 @@
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import { Button, Form, Label, Pagination, Popup, Table, Dropdown } from 'semantic-ui-react';
 | 
			
		||||
import {
 | 
			
		||||
  Button,
 | 
			
		||||
  Form,
 | 
			
		||||
  Label,
 | 
			
		||||
  Pagination,
 | 
			
		||||
  Popup,
 | 
			
		||||
  Table,
 | 
			
		||||
  Dropdown,
 | 
			
		||||
} from 'semantic-ui-react';
 | 
			
		||||
import { Link } from 'react-router-dom';
 | 
			
		||||
import { API, showError, showSuccess } from '../helpers';
 | 
			
		||||
 | 
			
		||||
import { ITEMS_PER_PAGE } from '../constants';
 | 
			
		||||
import { renderGroup, renderNumber, renderQuota, renderText } from '../helpers/render';
 | 
			
		||||
import {
 | 
			
		||||
  renderGroup,
 | 
			
		||||
  renderNumber,
 | 
			
		||||
  renderQuota,
 | 
			
		||||
  renderText,
 | 
			
		||||
} from '../helpers/render';
 | 
			
		||||
 | 
			
		||||
function renderRole(role) {
 | 
			
		||||
  switch (role) {
 | 
			
		||||
@@ -66,7 +79,7 @@ const UsersTable = () => {
 | 
			
		||||
    (async () => {
 | 
			
		||||
      const res = await API.post('/api/user/manage', {
 | 
			
		||||
        username,
 | 
			
		||||
        action
 | 
			
		||||
        action,
 | 
			
		||||
      });
 | 
			
		||||
      const { success, message } = res.data;
 | 
			
		||||
      if (success) {
 | 
			
		||||
@@ -169,7 +182,7 @@ const UsersTable = () => {
 | 
			
		||||
        />
 | 
			
		||||
      </Form>
 | 
			
		||||
 | 
			
		||||
      <Table basic compact size='small'>
 | 
			
		||||
      <Table basic={'very'} compact size='small'>
 | 
			
		||||
        <Table.Header>
 | 
			
		||||
          <Table.Row>
 | 
			
		||||
            <Table.HeaderCell
 | 
			
		||||
@@ -239,7 +252,9 @@ const UsersTable = () => {
 | 
			
		||||
                    <Popup
 | 
			
		||||
                      content={user.email ? user.email : '未绑定邮箱地址'}
 | 
			
		||||
                      key={user.username}
 | 
			
		||||
                      header={user.display_name ? user.display_name : user.username}
 | 
			
		||||
                      header={
 | 
			
		||||
                        user.display_name ? user.display_name : user.username
 | 
			
		||||
                      }
 | 
			
		||||
                      trigger={<span>{renderText(user.username, 15)}</span>}
 | 
			
		||||
                      hoverable
 | 
			
		||||
                    />
 | 
			
		||||
@@ -249,9 +264,22 @@ const UsersTable = () => {
 | 
			
		||||
                  {/*  {user.email ? <Popup hoverable content={user.email} trigger={<span>{renderText(user.email, 24)}</span>} /> : '无'}*/}
 | 
			
		||||
                  {/*</Table.Cell>*/}
 | 
			
		||||
                  <Table.Cell>
 | 
			
		||||
                    <Popup content='剩余额度' trigger={<Label basic>{renderQuota(user.quota)}</Label>} />
 | 
			
		||||
                    <Popup content='已用额度' trigger={<Label basic>{renderQuota(user.used_quota)}</Label>} />
 | 
			
		||||
                    <Popup content='请求次数' trigger={<Label basic>{renderNumber(user.request_count)}</Label>} />
 | 
			
		||||
                    <Popup
 | 
			
		||||
                      content='剩余额度'
 | 
			
		||||
                      trigger={<Label basic>{renderQuota(user.quota)}</Label>}
 | 
			
		||||
                    />
 | 
			
		||||
                    <Popup
 | 
			
		||||
                      content='已用额度'
 | 
			
		||||
                      trigger={
 | 
			
		||||
                        <Label basic>{renderQuota(user.used_quota)}</Label>
 | 
			
		||||
                      }
 | 
			
		||||
                    />
 | 
			
		||||
                    <Popup
 | 
			
		||||
                      content='请求次数'
 | 
			
		||||
                      trigger={
 | 
			
		||||
                        <Label basic>{renderNumber(user.request_count)}</Label>
 | 
			
		||||
                      }
 | 
			
		||||
                    />
 | 
			
		||||
                  </Table.Cell>
 | 
			
		||||
                  <Table.Cell>{renderRole(user.role)}</Table.Cell>
 | 
			
		||||
                  <Table.Cell>{renderStatus(user.status)}</Table.Cell>
 | 
			
		||||
@@ -279,7 +307,11 @@ const UsersTable = () => {
 | 
			
		||||
                      </Button>
 | 
			
		||||
                      <Popup
 | 
			
		||||
                        trigger={
 | 
			
		||||
                          <Button size='small' negative disabled={user.role === 100}>
 | 
			
		||||
                          <Button
 | 
			
		||||
                            size='small'
 | 
			
		||||
                            negative
 | 
			
		||||
                            disabled={user.role === 100}
 | 
			
		||||
                          >
 | 
			
		||||
                            删除
 | 
			
		||||
                          </Button>
 | 
			
		||||
                        }
 | 
			
		||||
@@ -335,8 +367,16 @@ const UsersTable = () => {
 | 
			
		||||
                options={[
 | 
			
		||||
                  { key: '', text: '默认排序', value: '' },
 | 
			
		||||
                  { key: 'quota', text: '按剩余额度排序', value: 'quota' },
 | 
			
		||||
                  { key: 'used_quota', text: '按已用额度排序', value: 'used_quota' },
 | 
			
		||||
                  { key: 'request_count', text: '按请求次数排序', value: 'request_count' },
 | 
			
		||||
                  {
 | 
			
		||||
                    key: 'used_quota',
 | 
			
		||||
                    text: '按已用额度排序',
 | 
			
		||||
                    value: 'used_quota',
 | 
			
		||||
                  },
 | 
			
		||||
                  {
 | 
			
		||||
                    key: 'request_count',
 | 
			
		||||
                    text: '按请求次数排序',
 | 
			
		||||
                    value: 'request_count',
 | 
			
		||||
                  },
 | 
			
		||||
                ]}
 | 
			
		||||
                value={orderBy}
 | 
			
		||||
                onChange={handleOrderByChange}
 | 
			
		||||
 
 | 
			
		||||
@@ -13,16 +13,18 @@ export function renderGroup(group) {
 | 
			
		||||
  }
 | 
			
		||||
  let groups = group.split(',');
 | 
			
		||||
  groups.sort();
 | 
			
		||||
  return <>
 | 
			
		||||
    {groups.map((group) => {
 | 
			
		||||
      if (group === 'vip' || group === 'pro') {
 | 
			
		||||
        return <Label color='yellow'>{group}</Label>;
 | 
			
		||||
      } else if (group === 'svip' || group === 'premium') {
 | 
			
		||||
        return <Label color='red'>{group}</Label>;
 | 
			
		||||
      }
 | 
			
		||||
      return <Label>{group}</Label>;
 | 
			
		||||
    })}
 | 
			
		||||
  </>;
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      {groups.map((group) => {
 | 
			
		||||
        if (group === 'vip' || group === 'pro') {
 | 
			
		||||
          return <Label color='yellow'>{group}</Label>;
 | 
			
		||||
        } else if (group === 'svip' || group === 'premium') {
 | 
			
		||||
          return <Label color='red'>{group}</Label>;
 | 
			
		||||
        }
 | 
			
		||||
        return <Label>{group}</Label>;
 | 
			
		||||
      })}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function renderNumber(num) {
 | 
			
		||||
@@ -56,3 +58,32 @@ export function renderQuotaWithPrompt(quota, digits) {
 | 
			
		||||
  }
 | 
			
		||||
  return '';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const colors = [
 | 
			
		||||
  'red',
 | 
			
		||||
  'orange',
 | 
			
		||||
  'yellow',
 | 
			
		||||
  'olive',
 | 
			
		||||
  'green',
 | 
			
		||||
  'teal',
 | 
			
		||||
  'blue',
 | 
			
		||||
  'violet',
 | 
			
		||||
  'purple',
 | 
			
		||||
  'pink',
 | 
			
		||||
  'brown',
 | 
			
		||||
  'grey',
 | 
			
		||||
  'black',
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
export function renderColorLabel(text) {
 | 
			
		||||
  let hash = 0;
 | 
			
		||||
  for (let i = 0; i < text.length; i++) {
 | 
			
		||||
    hash = text.charCodeAt(i) + ((hash << 5) - hash);
 | 
			
		||||
  }
 | 
			
		||||
  let index = Math.abs(hash % colors.length);
 | 
			
		||||
  return (
 | 
			
		||||
    <Label basic color={colors[index]}>
 | 
			
		||||
      {text}
 | 
			
		||||
    </Label>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import { Header, Segment } from 'semantic-ui-react';
 | 
			
		||||
import { Card, Header, Segment } from 'semantic-ui-react';
 | 
			
		||||
import { API, showError } from '../../helpers';
 | 
			
		||||
import { marked } from 'marked';
 | 
			
		||||
 | 
			
		||||
@@ -28,31 +28,38 @@ const About = () => {
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    displayAbout().then();
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      {
 | 
			
		||||
        aboutLoaded && about === '' ? <>
 | 
			
		||||
          <Segment>
 | 
			
		||||
            <Header as='h3'>关于</Header>
 | 
			
		||||
            <p>可在设置页面设置关于内容,支持 HTML & Markdown</p>
 | 
			
		||||
            项目仓库地址:
 | 
			
		||||
            <a href='https://github.com/songquanpeng/one-api'>
 | 
			
		||||
              https://github.com/songquanpeng/one-api
 | 
			
		||||
            </a>
 | 
			
		||||
          </Segment>
 | 
			
		||||
        </> : <>
 | 
			
		||||
          {
 | 
			
		||||
            about.startsWith('https://') ? <iframe
 | 
			
		||||
      {aboutLoaded && about === '' ? (
 | 
			
		||||
        <div className='dashboard-container'>
 | 
			
		||||
          <Card fluid className='chart-card'>
 | 
			
		||||
            <Card.Content>
 | 
			
		||||
              <Card.Header className='header'>关于系统</Card.Header>
 | 
			
		||||
              <p>可在设置页面设置关于内容,支持 HTML & Markdown</p>
 | 
			
		||||
              项目仓库地址:
 | 
			
		||||
              <a href='https://github.com/songquanpeng/one-api'>
 | 
			
		||||
                https://github.com/songquanpeng/one-api
 | 
			
		||||
              </a>
 | 
			
		||||
            </Card.Content>
 | 
			
		||||
          </Card>
 | 
			
		||||
        </div>
 | 
			
		||||
      ) : (
 | 
			
		||||
        <>
 | 
			
		||||
          {about.startsWith('https://') ? (
 | 
			
		||||
            <iframe
 | 
			
		||||
              src={about}
 | 
			
		||||
              style={{ width: '100%', height: '100vh', border: 'none' }}
 | 
			
		||||
            /> : <div style={{ fontSize: 'larger' }} dangerouslySetInnerHTML={{ __html: about }}></div>
 | 
			
		||||
          }
 | 
			
		||||
            />
 | 
			
		||||
          ) : (
 | 
			
		||||
            <div
 | 
			
		||||
              style={{ fontSize: 'larger' }}
 | 
			
		||||
              dangerouslySetInnerHTML={{ __html: about }}
 | 
			
		||||
            ></div>
 | 
			
		||||
          )}
 | 
			
		||||
        </>
 | 
			
		||||
      }
 | 
			
		||||
      )}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export default About;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,29 @@
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import { Button, Form, Header, Input, Message, Segment } from 'semantic-ui-react';
 | 
			
		||||
import {
 | 
			
		||||
  Button,
 | 
			
		||||
  Form,
 | 
			
		||||
  Header,
 | 
			
		||||
  Input,
 | 
			
		||||
  Message,
 | 
			
		||||
  Segment,
 | 
			
		||||
  Card,
 | 
			
		||||
} from 'semantic-ui-react';
 | 
			
		||||
import { useNavigate, useParams } from 'react-router-dom';
 | 
			
		||||
import { API, copy, getChannelModels, showError, showInfo, showSuccess, verifyJSON } from '../../helpers';
 | 
			
		||||
import {
 | 
			
		||||
  API,
 | 
			
		||||
  copy,
 | 
			
		||||
  getChannelModels,
 | 
			
		||||
  showError,
 | 
			
		||||
  showInfo,
 | 
			
		||||
  showSuccess,
 | 
			
		||||
  verifyJSON,
 | 
			
		||||
} from '../../helpers';
 | 
			
		||||
import { CHANNEL_OPTIONS } from '../../constants';
 | 
			
		||||
 | 
			
		||||
const MODEL_MAPPING_EXAMPLE = {
 | 
			
		||||
  'gpt-3.5-turbo-0301': 'gpt-3.5-turbo',
 | 
			
		||||
  'gpt-4-0314': 'gpt-4',
 | 
			
		||||
  'gpt-4-32k-0314': 'gpt-4-32k'
 | 
			
		||||
  'gpt-4-32k-0314': 'gpt-4-32k',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function type2secretPrompt(type) {
 | 
			
		||||
@@ -45,7 +61,7 @@ const EditChannel = () => {
 | 
			
		||||
    model_mapping: '',
 | 
			
		||||
    system_prompt: '',
 | 
			
		||||
    models: [],
 | 
			
		||||
    groups: ['default']
 | 
			
		||||
    groups: ['default'],
 | 
			
		||||
  };
 | 
			
		||||
  const [batch, setBatch] = useState(false);
 | 
			
		||||
  const [inputs, setInputs] = useState(originInputs);
 | 
			
		||||
@@ -61,7 +77,7 @@ const EditChannel = () => {
 | 
			
		||||
    ak: '',
 | 
			
		||||
    user_id: '',
 | 
			
		||||
    vertex_ai_project_id: '',
 | 
			
		||||
    vertex_ai_adc: ''
 | 
			
		||||
    vertex_ai_adc: '',
 | 
			
		||||
  });
 | 
			
		||||
  const handleInputChange = (e, { name, value }) => {
 | 
			
		||||
    setInputs((inputs) => ({ ...inputs, [name]: value }));
 | 
			
		||||
@@ -93,7 +109,11 @@ const EditChannel = () => {
 | 
			
		||||
        data.groups = data.group.split(',');
 | 
			
		||||
      }
 | 
			
		||||
      if (data.model_mapping !== '') {
 | 
			
		||||
        data.model_mapping = JSON.stringify(JSON.parse(data.model_mapping), null, 2);
 | 
			
		||||
        data.model_mapping = JSON.stringify(
 | 
			
		||||
          JSON.parse(data.model_mapping),
 | 
			
		||||
          null,
 | 
			
		||||
          2
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      setInputs(data);
 | 
			
		||||
      if (data.config !== '') {
 | 
			
		||||
@@ -112,7 +132,7 @@ const EditChannel = () => {
 | 
			
		||||
      let localModelOptions = res.data.data.map((model) => ({
 | 
			
		||||
        key: model.id,
 | 
			
		||||
        text: model.id,
 | 
			
		||||
        value: model.id
 | 
			
		||||
        value: model.id,
 | 
			
		||||
      }));
 | 
			
		||||
      setOriginModelOptions(localModelOptions);
 | 
			
		||||
      setFullModels(res.data.data.map((model) => model.id));
 | 
			
		||||
@@ -124,11 +144,13 @@ const EditChannel = () => {
 | 
			
		||||
  const fetchGroups = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      let res = await API.get(`/api/group/`);
 | 
			
		||||
      setGroupOptions(res.data.data.map((group) => ({
 | 
			
		||||
        key: group,
 | 
			
		||||
        text: group,
 | 
			
		||||
        value: group
 | 
			
		||||
      })));
 | 
			
		||||
      setGroupOptions(
 | 
			
		||||
        res.data.data.map((group) => ({
 | 
			
		||||
          key: group,
 | 
			
		||||
          text: group,
 | 
			
		||||
          value: group,
 | 
			
		||||
        }))
 | 
			
		||||
      );
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      showError(error.message);
 | 
			
		||||
    }
 | 
			
		||||
@@ -141,7 +163,7 @@ const EditChannel = () => {
 | 
			
		||||
        localModelOptions.push({
 | 
			
		||||
          key: model,
 | 
			
		||||
          text: model,
 | 
			
		||||
          value: model
 | 
			
		||||
          value: model,
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
@@ -163,7 +185,11 @@ const EditChannel = () => {
 | 
			
		||||
    if (inputs.key === '') {
 | 
			
		||||
      if (config.ak !== '' && config.sk !== '' && config.region !== '') {
 | 
			
		||||
        inputs.key = `${config.ak}|${config.sk}|${config.region}`;
 | 
			
		||||
      } else if (config.region !== '' && config.vertex_ai_project_id !== '' && config.vertex_ai_adc !== '') {
 | 
			
		||||
      } else if (
 | 
			
		||||
        config.region !== '' &&
 | 
			
		||||
        config.vertex_ai_project_id !== '' &&
 | 
			
		||||
        config.vertex_ai_adc !== ''
 | 
			
		||||
      ) {
 | 
			
		||||
        inputs.key = `${config.region}|${config.vertex_ai_project_id}|${config.vertex_ai_adc}`;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
@@ -179,9 +205,12 @@ const EditChannel = () => {
 | 
			
		||||
      showInfo('模型映射必须是合法的 JSON 格式!');
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    let localInputs = {...inputs};
 | 
			
		||||
    let localInputs = { ...inputs };
 | 
			
		||||
    if (localInputs.base_url && localInputs.base_url.endsWith('/')) {
 | 
			
		||||
      localInputs.base_url = localInputs.base_url.slice(0, localInputs.base_url.length - 1);
 | 
			
		||||
      localInputs.base_url = localInputs.base_url.slice(
 | 
			
		||||
        0,
 | 
			
		||||
        localInputs.base_url.length - 1
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    if (localInputs.type === 3 && localInputs.other === '') {
 | 
			
		||||
      localInputs.other = '2024-03-01-preview';
 | 
			
		||||
@@ -191,7 +220,10 @@ const EditChannel = () => {
 | 
			
		||||
    localInputs.group = localInputs.groups.join(',');
 | 
			
		||||
    localInputs.config = JSON.stringify(config);
 | 
			
		||||
    if (isEdit) {
 | 
			
		||||
      res = await API.put(`/api/channel/`, { ...localInputs, id: parseInt(channelId) });
 | 
			
		||||
      res = await API.put(`/api/channel/`, {
 | 
			
		||||
        ...localInputs,
 | 
			
		||||
        id: parseInt(channelId),
 | 
			
		||||
      });
 | 
			
		||||
    } else {
 | 
			
		||||
      res = await API.post(`/api/channel/`, localInputs);
 | 
			
		||||
    }
 | 
			
		||||
@@ -217,9 +249,9 @@ const EditChannel = () => {
 | 
			
		||||
    localModelOptions.push({
 | 
			
		||||
      key: customModel,
 | 
			
		||||
      text: customModel,
 | 
			
		||||
      value: customModel
 | 
			
		||||
      value: customModel,
 | 
			
		||||
    });
 | 
			
		||||
    setModelOptions(modelOptions => {
 | 
			
		||||
    setModelOptions((modelOptions) => {
 | 
			
		||||
      return [...modelOptions, ...localModelOptions];
 | 
			
		||||
    });
 | 
			
		||||
    setCustomModel('');
 | 
			
		||||
@@ -227,34 +259,45 @@ const EditChannel = () => {
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <Segment loading={loading}>
 | 
			
		||||
        <Header as='h3'>{isEdit ? '更新渠道信息' : '创建新的渠道'}</Header>
 | 
			
		||||
        <Form autoComplete='new-password'>
 | 
			
		||||
          <Form.Field>
 | 
			
		||||
            <Form.Select
 | 
			
		||||
              label='类型'
 | 
			
		||||
              name='type'
 | 
			
		||||
              required
 | 
			
		||||
              search
 | 
			
		||||
              options={CHANNEL_OPTIONS}
 | 
			
		||||
              value={inputs.type}
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Field>
 | 
			
		||||
          {
 | 
			
		||||
            inputs.type === 3 && (
 | 
			
		||||
    <div className='dashboard-container'>
 | 
			
		||||
      <Card fluid className='chart-card'>
 | 
			
		||||
        <Card.Content>
 | 
			
		||||
          <Card.Header className='header'>
 | 
			
		||||
            {isEdit ? '更新渠道信息' : '创建新的渠道'}
 | 
			
		||||
          </Card.Header>
 | 
			
		||||
          <Form loading={loading} autoComplete='new-password'>
 | 
			
		||||
            <Form.Field>
 | 
			
		||||
              <Form.Select
 | 
			
		||||
                label='类型'
 | 
			
		||||
                name='type'
 | 
			
		||||
                required
 | 
			
		||||
                search
 | 
			
		||||
                options={CHANNEL_OPTIONS}
 | 
			
		||||
                value={inputs.type}
 | 
			
		||||
                onChange={handleInputChange}
 | 
			
		||||
              />
 | 
			
		||||
            </Form.Field>
 | 
			
		||||
            {inputs.type === 3 && (
 | 
			
		||||
              <>
 | 
			
		||||
                <Message>
 | 
			
		||||
                  注意,<strong>模型部署名称必须和模型名称保持一致</strong>,因为 One API 会把请求体中的 model
 | 
			
		||||
                  参数替换为你的部署名称(模型名称中的点会被剔除),<a target='_blank'
 | 
			
		||||
                                                                    href='https://github.com/songquanpeng/one-api/issues/133?notification_referrer_id=NT_kwDOAmJSYrM2NjIwMzI3NDgyOjM5OTk4MDUw#issuecomment-1571602271'>图片演示</a>。
 | 
			
		||||
                  注意,<strong>模型部署名称必须和模型名称保持一致</strong>
 | 
			
		||||
                  ,因为 One API 会把请求体中的 model
 | 
			
		||||
                  参数替换为你的部署名称(模型名称中的点会被剔除),
 | 
			
		||||
                  <a
 | 
			
		||||
                    target='_blank'
 | 
			
		||||
                    href='https://github.com/songquanpeng/one-api/issues/133?notification_referrer_id=NT_kwDOAmJSYrM2NjIwMzI3NDgyOjM5OTk4MDUw#issuecomment-1571602271'
 | 
			
		||||
                  >
 | 
			
		||||
                    图片演示
 | 
			
		||||
                  </a>
 | 
			
		||||
                  。
 | 
			
		||||
                </Message>
 | 
			
		||||
                <Form.Field>
 | 
			
		||||
                  <Form.Input
 | 
			
		||||
                    label='AZURE_OPENAI_ENDPOINT'
 | 
			
		||||
                    name='base_url'
 | 
			
		||||
                    placeholder={'请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com'}
 | 
			
		||||
                    placeholder={
 | 
			
		||||
                      '请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com'
 | 
			
		||||
                    }
 | 
			
		||||
                    onChange={handleInputChange}
 | 
			
		||||
                    value={inputs.base_url}
 | 
			
		||||
                    autoComplete='new-password'
 | 
			
		||||
@@ -264,73 +307,72 @@ const EditChannel = () => {
 | 
			
		||||
                  <Form.Input
 | 
			
		||||
                    label='默认 API 版本'
 | 
			
		||||
                    name='other'
 | 
			
		||||
                    placeholder={'请输入默认 API 版本,例如:2024-03-01-preview,该配置可以被实际的请求查询参数所覆盖'}
 | 
			
		||||
                    placeholder={
 | 
			
		||||
                      '请输入默认 API 版本,例如:2024-03-01-preview,该配置可以被实际的请求查询参数所覆盖'
 | 
			
		||||
                    }
 | 
			
		||||
                    onChange={handleInputChange}
 | 
			
		||||
                    value={inputs.other}
 | 
			
		||||
                    autoComplete='new-password'
 | 
			
		||||
                  />
 | 
			
		||||
                </Form.Field>
 | 
			
		||||
              </>
 | 
			
		||||
            )
 | 
			
		||||
          }
 | 
			
		||||
          {
 | 
			
		||||
            inputs.type === 8 && (
 | 
			
		||||
            )}
 | 
			
		||||
            {inputs.type === 8 && (
 | 
			
		||||
              <Form.Field>
 | 
			
		||||
                <Form.Input
 | 
			
		||||
                  label='Base URL'
 | 
			
		||||
                  name='base_url'
 | 
			
		||||
                  placeholder={'请输入自定义渠道的 Base URL,例如:https://openai.justsong.cn'}
 | 
			
		||||
                  placeholder={
 | 
			
		||||
                    '请输入自定义渠道的 Base URL,例如:https://openai.justsong.cn'
 | 
			
		||||
                  }
 | 
			
		||||
                  onChange={handleInputChange}
 | 
			
		||||
                  value={inputs.base_url}
 | 
			
		||||
                  autoComplete='new-password'
 | 
			
		||||
                />
 | 
			
		||||
              </Form.Field>
 | 
			
		||||
            )
 | 
			
		||||
          }
 | 
			
		||||
          <Form.Field>
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label='名称'
 | 
			
		||||
              required
 | 
			
		||||
              name='name'
 | 
			
		||||
              placeholder={'请为渠道命名'}
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              value={inputs.name}
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Field>
 | 
			
		||||
          <Form.Field>
 | 
			
		||||
            <Form.Dropdown
 | 
			
		||||
              label='分组'
 | 
			
		||||
              placeholder={'请选择可以使用该渠道的分组'}
 | 
			
		||||
              name='groups'
 | 
			
		||||
              required
 | 
			
		||||
              fluid
 | 
			
		||||
              multiple
 | 
			
		||||
              selection
 | 
			
		||||
              allowAdditions
 | 
			
		||||
              additionLabel={'请在系统设置页面编辑分组倍率以添加新的分组:'}
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              value={inputs.groups}
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              options={groupOptions}
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Field>
 | 
			
		||||
          {
 | 
			
		||||
            inputs.type === 18 && (
 | 
			
		||||
            )}
 | 
			
		||||
            <Form.Field>
 | 
			
		||||
              <Form.Input
 | 
			
		||||
                label='名称'
 | 
			
		||||
                name='name'
 | 
			
		||||
                placeholder={'请输入名称'}
 | 
			
		||||
                onChange={handleInputChange}
 | 
			
		||||
                value={inputs.name}
 | 
			
		||||
                required
 | 
			
		||||
              />
 | 
			
		||||
            </Form.Field>
 | 
			
		||||
            <Form.Field>
 | 
			
		||||
              <Form.Dropdown
 | 
			
		||||
                label='分组'
 | 
			
		||||
                placeholder={'请选择可以使用该渠道的分组'}
 | 
			
		||||
                name='groups'
 | 
			
		||||
                required
 | 
			
		||||
                fluid
 | 
			
		||||
                multiple
 | 
			
		||||
                selection
 | 
			
		||||
                allowAdditions
 | 
			
		||||
                additionLabel={'请在系统设置页面编辑分组倍率以添加新的分组:'}
 | 
			
		||||
                onChange={handleInputChange}
 | 
			
		||||
                value={inputs.groups}
 | 
			
		||||
                autoComplete='new-password'
 | 
			
		||||
                options={groupOptions}
 | 
			
		||||
              />
 | 
			
		||||
            </Form.Field>
 | 
			
		||||
            {inputs.type === 18 && (
 | 
			
		||||
              <Form.Field>
 | 
			
		||||
                <Form.Input
 | 
			
		||||
                  label='模型版本'
 | 
			
		||||
                  name='other'
 | 
			
		||||
                  placeholder={'请输入星火大模型版本,注意是接口地址中的版本号,例如:v2.1'}
 | 
			
		||||
                  placeholder={
 | 
			
		||||
                    '请输入星火大模型版本,注意是接口地址中的版本号,例如:v2.1'
 | 
			
		||||
                  }
 | 
			
		||||
                  onChange={handleInputChange}
 | 
			
		||||
                  value={inputs.other}
 | 
			
		||||
                  autoComplete='new-password'
 | 
			
		||||
                />
 | 
			
		||||
              </Form.Field>
 | 
			
		||||
            )
 | 
			
		||||
          }
 | 
			
		||||
          {
 | 
			
		||||
            inputs.type === 21 && (
 | 
			
		||||
            )}
 | 
			
		||||
            {inputs.type === 21 && (
 | 
			
		||||
              <Form.Field>
 | 
			
		||||
                <Form.Input
 | 
			
		||||
                  label='知识库 ID'
 | 
			
		||||
@@ -341,38 +383,40 @@ const EditChannel = () => {
 | 
			
		||||
                  autoComplete='new-password'
 | 
			
		||||
                />
 | 
			
		||||
              </Form.Field>
 | 
			
		||||
            )
 | 
			
		||||
          }
 | 
			
		||||
          {
 | 
			
		||||
            inputs.type === 17 && (
 | 
			
		||||
            )}
 | 
			
		||||
            {inputs.type === 17 && (
 | 
			
		||||
              <Form.Field>
 | 
			
		||||
                <Form.Input
 | 
			
		||||
                  label='插件参数'
 | 
			
		||||
                  name='other'
 | 
			
		||||
                  placeholder={'请输入插件参数,即 X-DashScope-Plugin 请求头的取值'}
 | 
			
		||||
                  placeholder={
 | 
			
		||||
                    '请输入插件参数,即 X-DashScope-Plugin 请求头的取值'
 | 
			
		||||
                  }
 | 
			
		||||
                  onChange={handleInputChange}
 | 
			
		||||
                  value={inputs.other}
 | 
			
		||||
                  autoComplete='new-password'
 | 
			
		||||
                />
 | 
			
		||||
              </Form.Field>
 | 
			
		||||
            )
 | 
			
		||||
          }
 | 
			
		||||
          {
 | 
			
		||||
            inputs.type === 34 && (
 | 
			
		||||
            )}
 | 
			
		||||
            {inputs.type === 34 && (
 | 
			
		||||
              <Message>
 | 
			
		||||
                对于 Coze 而言,模型名称即 Bot ID,你可以添加一个前缀 `bot-`,例如:`bot-123456`。
 | 
			
		||||
                对于 Coze 而言,模型名称即 Bot ID,你可以添加一个前缀
 | 
			
		||||
                `bot-`,例如:`bot-123456`。
 | 
			
		||||
              </Message>
 | 
			
		||||
            )
 | 
			
		||||
          }
 | 
			
		||||
          {
 | 
			
		||||
            inputs.type === 40 && (
 | 
			
		||||
            )}
 | 
			
		||||
            {inputs.type === 40 && (
 | 
			
		||||
              <Message>
 | 
			
		||||
                对于豆包而言,需要手动去 <a target="_blank" href="https://console.volcengine.com/ark/region:ark+cn-beijing/endpoint">模型推理页面</a> 创建推理接入点,以接入点名称作为模型名称,例如:`ep-20240608051426-tkxvl`。
 | 
			
		||||
                对于豆包而言,需要手动去{' '}
 | 
			
		||||
                <a
 | 
			
		||||
                  target='_blank'
 | 
			
		||||
                  href='https://console.volcengine.com/ark/region:ark+cn-beijing/endpoint'
 | 
			
		||||
                >
 | 
			
		||||
                  模型推理页面
 | 
			
		||||
                </a>{' '}
 | 
			
		||||
                创建推理接入点,以接入点名称作为模型名称,例如:`ep-20240608051426-tkxvl`。
 | 
			
		||||
              </Message>
 | 
			
		||||
            )
 | 
			
		||||
          }
 | 
			
		||||
          {
 | 
			
		||||
            inputs.type !== 43 && (
 | 
			
		||||
            )}
 | 
			
		||||
            {inputs.type !== 43 && (
 | 
			
		||||
              <Form.Field>
 | 
			
		||||
                <Form.Dropdown
 | 
			
		||||
                  label='模型'
 | 
			
		||||
@@ -392,23 +436,44 @@ const EditChannel = () => {
 | 
			
		||||
                  options={modelOptions}
 | 
			
		||||
                />
 | 
			
		||||
              </Form.Field>
 | 
			
		||||
            )
 | 
			
		||||
          }
 | 
			
		||||
          {
 | 
			
		||||
            inputs.type !== 43 && (
 | 
			
		||||
            )}
 | 
			
		||||
            {inputs.type !== 43 && (
 | 
			
		||||
              <div style={{ lineHeight: '40px', marginBottom: '12px' }}>
 | 
			
		||||
                <Button type={'button'} onClick={() => {
 | 
			
		||||
                  handleInputChange(null, { name: 'models', value: basicModels });
 | 
			
		||||
                }}>填入相关模型</Button>
 | 
			
		||||
                <Button type={'button'} onClick={() => {
 | 
			
		||||
                  handleInputChange(null, { name: 'models', value: fullModels });
 | 
			
		||||
                }}>填入所有模型</Button>
 | 
			
		||||
                <Button type={'button'} onClick={() => {
 | 
			
		||||
                  handleInputChange(null, { name: 'models', value: [] });
 | 
			
		||||
                }}>清除所有模型</Button>
 | 
			
		||||
                <Button
 | 
			
		||||
                  type={'button'}
 | 
			
		||||
                  onClick={() => {
 | 
			
		||||
                    handleInputChange(null, {
 | 
			
		||||
                      name: 'models',
 | 
			
		||||
                      value: basicModels,
 | 
			
		||||
                    });
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  填入相关模型
 | 
			
		||||
                </Button>
 | 
			
		||||
                <Button
 | 
			
		||||
                  type={'button'}
 | 
			
		||||
                  onClick={() => {
 | 
			
		||||
                    handleInputChange(null, {
 | 
			
		||||
                      name: 'models',
 | 
			
		||||
                      value: fullModels,
 | 
			
		||||
                    });
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  填入所有模型
 | 
			
		||||
                </Button>
 | 
			
		||||
                <Button
 | 
			
		||||
                  type={'button'}
 | 
			
		||||
                  onClick={() => {
 | 
			
		||||
                    handleInputChange(null, { name: 'models', value: [] });
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  清除所有模型
 | 
			
		||||
                </Button>
 | 
			
		||||
                <Input
 | 
			
		||||
                  action={
 | 
			
		||||
                    <Button type={'button'} onClick={addCustomModel}>填入</Button>
 | 
			
		||||
                    <Button type={'button'} onClick={addCustomModel}>
 | 
			
		||||
                      填入
 | 
			
		||||
                    </Button>
 | 
			
		||||
                  }
 | 
			
		||||
                  placeholder='输入自定义模型名称'
 | 
			
		||||
                  value={customModel}
 | 
			
		||||
@@ -423,37 +488,44 @@ const EditChannel = () => {
 | 
			
		||||
                  }}
 | 
			
		||||
                />
 | 
			
		||||
              </div>
 | 
			
		||||
            )
 | 
			
		||||
          }
 | 
			
		||||
          {
 | 
			
		||||
          inputs.type !== 43 && (<>
 | 
			
		||||
              <Form.Field>
 | 
			
		||||
                <Form.TextArea
 | 
			
		||||
                  label='模型重定向'
 | 
			
		||||
                  placeholder={`此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:\n${JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)}`}
 | 
			
		||||
                  name='model_mapping'
 | 
			
		||||
                  onChange={handleInputChange}
 | 
			
		||||
                  value={inputs.model_mapping}
 | 
			
		||||
                  style={{ minHeight: 150, fontFamily: 'JetBrains Mono, Consolas' }}
 | 
			
		||||
                  autoComplete='new-password'
 | 
			
		||||
                />
 | 
			
		||||
              </Form.Field>
 | 
			
		||||
            <Form.Field>
 | 
			
		||||
                <Form.TextArea
 | 
			
		||||
                  label='系统提示词'
 | 
			
		||||
                  placeholder={`此项可选,用于强制设置给定的系统提示词,请配合自定义模型 & 模型重定向使用,首先创建一个唯一的自定义模型名称并在上面填入,之后将该自定义模型重定向映射到该渠道一个原生支持的模型`}
 | 
			
		||||
                  name='system_prompt'
 | 
			
		||||
                  onChange={handleInputChange}
 | 
			
		||||
                  value={inputs.system_prompt}
 | 
			
		||||
                  style={{ minHeight: 150, fontFamily: 'JetBrains Mono, Consolas' }}
 | 
			
		||||
                  autoComplete='new-password'
 | 
			
		||||
                />
 | 
			
		||||
              </Form.Field>
 | 
			
		||||
            )}
 | 
			
		||||
            {inputs.type !== 43 && (
 | 
			
		||||
              <>
 | 
			
		||||
                <Form.Field>
 | 
			
		||||
                  <Form.TextArea
 | 
			
		||||
                    label='模型重定向'
 | 
			
		||||
                    placeholder={`此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:\n${JSON.stringify(
 | 
			
		||||
                      MODEL_MAPPING_EXAMPLE,
 | 
			
		||||
                      null,
 | 
			
		||||
                      2
 | 
			
		||||
                    )}`}
 | 
			
		||||
                    name='model_mapping'
 | 
			
		||||
                    onChange={handleInputChange}
 | 
			
		||||
                    value={inputs.model_mapping}
 | 
			
		||||
                    style={{
 | 
			
		||||
                      minHeight: 150,
 | 
			
		||||
                      fontFamily: 'JetBrains Mono, Consolas',
 | 
			
		||||
                    }}
 | 
			
		||||
                    autoComplete='new-password'
 | 
			
		||||
                  />
 | 
			
		||||
                </Form.Field>
 | 
			
		||||
                <Form.Field>
 | 
			
		||||
                  <Form.TextArea
 | 
			
		||||
                    label='系统提示词'
 | 
			
		||||
                    placeholder={`此项可选,用于强制设置给定的系统提示词,请配合自定义模型 & 模型重定向使用,首先创建一个唯一的自定义模型名称并在上面填入,之后将该自定义模型重定向映射到该渠道一个原生支持的模型`}
 | 
			
		||||
                    name='system_prompt'
 | 
			
		||||
                    onChange={handleInputChange}
 | 
			
		||||
                    value={inputs.system_prompt}
 | 
			
		||||
                    style={{
 | 
			
		||||
                      minHeight: 150,
 | 
			
		||||
                      fontFamily: 'JetBrains Mono, Consolas',
 | 
			
		||||
                    }}
 | 
			
		||||
                    autoComplete='new-password'
 | 
			
		||||
                  />
 | 
			
		||||
                </Form.Field>
 | 
			
		||||
              </>
 | 
			
		||||
            )
 | 
			
		||||
          }
 | 
			
		||||
          {
 | 
			
		||||
            inputs.type === 33 && (
 | 
			
		||||
            )}
 | 
			
		||||
            {inputs.type === 33 && (
 | 
			
		||||
              <Form.Field>
 | 
			
		||||
                <Form.Input
 | 
			
		||||
                  label='Region'
 | 
			
		||||
@@ -483,10 +555,8 @@ const EditChannel = () => {
 | 
			
		||||
                  autoComplete=''
 | 
			
		||||
                />
 | 
			
		||||
              </Form.Field>
 | 
			
		||||
            )
 | 
			
		||||
          }
 | 
			
		||||
          {
 | 
			
		||||
            inputs.type === 42 && (
 | 
			
		||||
            )}
 | 
			
		||||
            {inputs.type === 42 && (
 | 
			
		||||
              <Form.Field>
 | 
			
		||||
                <Form.Input
 | 
			
		||||
                  label='Region'
 | 
			
		||||
@@ -510,16 +580,16 @@ const EditChannel = () => {
 | 
			
		||||
                  label='Google Cloud Application Default Credentials JSON'
 | 
			
		||||
                  name='vertex_ai_adc'
 | 
			
		||||
                  required
 | 
			
		||||
                  placeholder={'Google Cloud Application Default Credentials JSON'}
 | 
			
		||||
                  placeholder={
 | 
			
		||||
                    'Google Cloud Application Default Credentials JSON'
 | 
			
		||||
                  }
 | 
			
		||||
                  onChange={handleConfigChange}
 | 
			
		||||
                  value={config.vertex_ai_adc}
 | 
			
		||||
                  autoComplete=''
 | 
			
		||||
                />
 | 
			
		||||
              </Form.Field>
 | 
			
		||||
            )
 | 
			
		||||
          }
 | 
			
		||||
          {
 | 
			
		||||
            inputs.type === 34 && (
 | 
			
		||||
            )}
 | 
			
		||||
            {inputs.type === 34 && (
 | 
			
		||||
              <Form.Input
 | 
			
		||||
                label='User ID'
 | 
			
		||||
                name='user_id'
 | 
			
		||||
@@ -528,90 +598,105 @@ const EditChannel = () => {
 | 
			
		||||
                onChange={handleConfigChange}
 | 
			
		||||
                value={config.user_id}
 | 
			
		||||
                autoComplete=''
 | 
			
		||||
              />)
 | 
			
		||||
          }
 | 
			
		||||
          {
 | 
			
		||||
            inputs.type !== 33 && inputs.type !== 42 && (batch ? <Form.Field>
 | 
			
		||||
              <Form.TextArea
 | 
			
		||||
                label='密钥'
 | 
			
		||||
                name='key'
 | 
			
		||||
                required
 | 
			
		||||
                placeholder={'请输入密钥,一行一个'}
 | 
			
		||||
                onChange={handleInputChange}
 | 
			
		||||
                value={inputs.key}
 | 
			
		||||
                style={{ minHeight: 150, fontFamily: 'JetBrains Mono, Consolas' }}
 | 
			
		||||
                autoComplete='new-password'
 | 
			
		||||
              />
 | 
			
		||||
            </Form.Field> : <Form.Field>
 | 
			
		||||
              <Form.Input
 | 
			
		||||
                label='密钥'
 | 
			
		||||
                name='key'
 | 
			
		||||
                required
 | 
			
		||||
                placeholder={type2secretPrompt(inputs.type)}
 | 
			
		||||
                onChange={handleInputChange}
 | 
			
		||||
                value={inputs.key}
 | 
			
		||||
                autoComplete='new-password'
 | 
			
		||||
              />
 | 
			
		||||
            </Form.Field>)
 | 
			
		||||
          }
 | 
			
		||||
          {
 | 
			
		||||
            inputs.type === 37 && (
 | 
			
		||||
            )}
 | 
			
		||||
            {inputs.type !== 33 &&
 | 
			
		||||
              inputs.type !== 42 &&
 | 
			
		||||
              (batch ? (
 | 
			
		||||
                <Form.Field>
 | 
			
		||||
                  <Form.TextArea
 | 
			
		||||
                    label='密钥'
 | 
			
		||||
                    name='key'
 | 
			
		||||
                    required
 | 
			
		||||
                    placeholder={'请输入密钥,一行一个'}
 | 
			
		||||
                    onChange={handleInputChange}
 | 
			
		||||
                    value={inputs.key}
 | 
			
		||||
                    style={{
 | 
			
		||||
                      minHeight: 150,
 | 
			
		||||
                      fontFamily: 'JetBrains Mono, Consolas',
 | 
			
		||||
                    }}
 | 
			
		||||
                    autoComplete='new-password'
 | 
			
		||||
                  />
 | 
			
		||||
                </Form.Field>
 | 
			
		||||
              ) : (
 | 
			
		||||
                <Form.Field>
 | 
			
		||||
                  <Form.Input
 | 
			
		||||
                    label='密钥'
 | 
			
		||||
                    name='key'
 | 
			
		||||
                    required
 | 
			
		||||
                    placeholder={type2secretPrompt(inputs.type)}
 | 
			
		||||
                    onChange={handleInputChange}
 | 
			
		||||
                    value={inputs.key}
 | 
			
		||||
                    autoComplete='new-password'
 | 
			
		||||
                  />
 | 
			
		||||
                </Form.Field>
 | 
			
		||||
              ))}
 | 
			
		||||
            {inputs.type === 37 && (
 | 
			
		||||
              <Form.Field>
 | 
			
		||||
                <Form.Input
 | 
			
		||||
                  label='Account ID'
 | 
			
		||||
                  name='user_id'
 | 
			
		||||
                  required
 | 
			
		||||
                  placeholder={'请输入 Account ID,例如:d8d7c61dbc334c32d3ced580e4bf42b4'}
 | 
			
		||||
                  placeholder={
 | 
			
		||||
                    '请输入 Account ID,例如:d8d7c61dbc334c32d3ced580e4bf42b4'
 | 
			
		||||
                  }
 | 
			
		||||
                  onChange={handleConfigChange}
 | 
			
		||||
                  value={config.user_id}
 | 
			
		||||
                  autoComplete=''
 | 
			
		||||
                />
 | 
			
		||||
              </Form.Field>
 | 
			
		||||
            )
 | 
			
		||||
          }
 | 
			
		||||
          {
 | 
			
		||||
            inputs.type !== 33 && !isEdit && (
 | 
			
		||||
            )}
 | 
			
		||||
            {inputs.type !== 33 && !isEdit && (
 | 
			
		||||
              <Form.Checkbox
 | 
			
		||||
                checked={batch}
 | 
			
		||||
                label='批量创建'
 | 
			
		||||
                name='batch'
 | 
			
		||||
                onChange={() => setBatch(!batch)}
 | 
			
		||||
              />
 | 
			
		||||
            )
 | 
			
		||||
          }
 | 
			
		||||
          {
 | 
			
		||||
            inputs.type !== 3 && inputs.type !== 33 && inputs.type !== 8 && inputs.type !== 22 && (
 | 
			
		||||
              <Form.Field>
 | 
			
		||||
                <Form.Input
 | 
			
		||||
                  label='代理'
 | 
			
		||||
                  name='base_url'
 | 
			
		||||
                  placeholder={'此项可选,用于通过代理站来进行 API 调用,请输入代理站地址,格式为:https://domain.com'}
 | 
			
		||||
                  onChange={handleInputChange}
 | 
			
		||||
                  value={inputs.base_url}
 | 
			
		||||
                  autoComplete='new-password'
 | 
			
		||||
                />
 | 
			
		||||
              </Form.Field>
 | 
			
		||||
            )
 | 
			
		||||
          }
 | 
			
		||||
          {
 | 
			
		||||
            inputs.type === 22 && (
 | 
			
		||||
            )}
 | 
			
		||||
            {inputs.type !== 3 &&
 | 
			
		||||
              inputs.type !== 33 &&
 | 
			
		||||
              inputs.type !== 8 &&
 | 
			
		||||
              inputs.type !== 22 && (
 | 
			
		||||
                <Form.Field>
 | 
			
		||||
                  <Form.Input
 | 
			
		||||
                    label='代理'
 | 
			
		||||
                    name='base_url'
 | 
			
		||||
                    placeholder={
 | 
			
		||||
                      '此项可选,用于通过代理站来进行 API 调用,请输入代理站地址,格式为:https://domain.com'
 | 
			
		||||
                    }
 | 
			
		||||
                    onChange={handleInputChange}
 | 
			
		||||
                    value={inputs.base_url}
 | 
			
		||||
                    autoComplete='new-password'
 | 
			
		||||
                  />
 | 
			
		||||
                </Form.Field>
 | 
			
		||||
              )}
 | 
			
		||||
            {inputs.type === 22 && (
 | 
			
		||||
              <Form.Field>
 | 
			
		||||
                <Form.Input
 | 
			
		||||
                  label='私有部署地址'
 | 
			
		||||
                  name='base_url'
 | 
			
		||||
                  placeholder={'请输入私有部署地址,格式为:https://fastgpt.run/api/openapi'}
 | 
			
		||||
                  placeholder={
 | 
			
		||||
                    '请输入私有部署地址,格式为:https://fastgpt.run/api/openapi'
 | 
			
		||||
                  }
 | 
			
		||||
                  onChange={handleInputChange}
 | 
			
		||||
                  value={inputs.base_url}
 | 
			
		||||
                  autoComplete='new-password'
 | 
			
		||||
                />
 | 
			
		||||
              </Form.Field>
 | 
			
		||||
            )
 | 
			
		||||
          }
 | 
			
		||||
          <Button onClick={handleCancel}>取消</Button>
 | 
			
		||||
          <Button type={isEdit ? 'button' : 'submit'} positive onClick={submit}>提交</Button>
 | 
			
		||||
        </Form>
 | 
			
		||||
      </Segment>
 | 
			
		||||
    </>
 | 
			
		||||
            )}
 | 
			
		||||
            <Button onClick={handleCancel}>取消</Button>
 | 
			
		||||
            <Button
 | 
			
		||||
              type={isEdit ? 'button' : 'submit'}
 | 
			
		||||
              positive
 | 
			
		||||
              onClick={submit}
 | 
			
		||||
            >
 | 
			
		||||
              提交
 | 
			
		||||
            </Button>
 | 
			
		||||
          </Form>
 | 
			
		||||
        </Card.Content>
 | 
			
		||||
      </Card>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,16 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { Header, Segment } from 'semantic-ui-react';
 | 
			
		||||
import { Card } from 'semantic-ui-react';
 | 
			
		||||
import ChannelsTable from '../../components/ChannelsTable';
 | 
			
		||||
 | 
			
		||||
const Channel = () => (
 | 
			
		||||
  <>
 | 
			
		||||
    <Segment>
 | 
			
		||||
      <Header as='h3'>管理渠道</Header>
 | 
			
		||||
      <ChannelsTable />
 | 
			
		||||
    </Segment>
 | 
			
		||||
  </>
 | 
			
		||||
  <div className='dashboard-container'>
 | 
			
		||||
    <Card fluid className='chart-card'>
 | 
			
		||||
      <Card.Content>
 | 
			
		||||
        <Card.Header className='header'>管理渠道</Card.Header>
 | 
			
		||||
        <ChannelsTable />
 | 
			
		||||
      </Card.Content>
 | 
			
		||||
    </Card>
 | 
			
		||||
  </div>
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export default Channel;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										109
									
								
								web/default/src/pages/Dashboard/Dashboard.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								web/default/src/pages/Dashboard/Dashboard.css
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,109 @@
 | 
			
		||||
.dashboard-container {
 | 
			
		||||
    padding: 20px 24px 40px;
 | 
			
		||||
    background-color: #ffffff;
 | 
			
		||||
    margin-top: -15px; /* 减小与导航栏的间距 */
 | 
			
		||||
    max-width: 1600px;        /* 设置最大宽度 */
 | 
			
		||||
    margin-left: auto;        /* 水平居中 */
 | 
			
		||||
    margin-right: auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.stat-card {
 | 
			
		||||
    background: linear-gradient(135deg, #2185d0 0%, #1678c2 100%) !important;
 | 
			
		||||
    color: white !important;
 | 
			
		||||
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1) !important;
 | 
			
		||||
    transition: transform 0.2s ease !important;
 | 
			
		||||
    margin-bottom: 1rem !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.stat-card:hover {
 | 
			
		||||
    transform: translateY(-5px);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.stat-card .statistic {
 | 
			
		||||
    color: white !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.charts-grid {
 | 
			
		||||
    margin-bottom: 1rem !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.charts-grid .column {
 | 
			
		||||
    padding: 0.5rem !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.chart-card {
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04) !important;
 | 
			
		||||
    border: none !important;
 | 
			
		||||
    border-radius: 16px !important;
 | 
			
		||||
    padding-top: 8px!important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.chart-container {
 | 
			
		||||
    margin-top: 2px;
 | 
			
		||||
    padding: 16px;
 | 
			
		||||
    background-color: white;
 | 
			
		||||
    border-radius: 12px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ui.card > .content > .header {
 | 
			
		||||
    color: #2B3674;
 | 
			
		||||
    font-size: 1.2em;
 | 
			
		||||
    margin-bottom: 15px;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: space-between;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    font-weight: 600;
 | 
			
		||||
    gap: 12px; /* 增加标题和数值之间的间距 */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.stat-value {
 | 
			
		||||
    color: #4318FF;
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
    font-size: 1.1em;
 | 
			
		||||
    background: rgba(67, 24, 255, 0.1);
 | 
			
		||||
    padding: 4px 12px;
 | 
			
		||||
    border-radius: 8px;
 | 
			
		||||
    white-space: nowrap; /* 防止数值换行 */
 | 
			
		||||
    margin-left: 16px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 优化图表响应式布局 */
 | 
			
		||||
@media (max-width: 768px) {
 | 
			
		||||
    .dashboard-container {
 | 
			
		||||
        padding: 10px 16px;   /* 移动端也相应减小内边距 */
 | 
			
		||||
        max-width: 100%;      /* 移动端占满全宽 */
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .chart-container {
 | 
			
		||||
        padding: 12px;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .charts-grid .column {
 | 
			
		||||
        padding: 0.25rem !important;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 设置页面的 Tab 样式 */
 | 
			
		||||
.settings-tab {
 | 
			
		||||
    margin-top: 1rem !important;
 | 
			
		||||
    border-bottom: none !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.settings-tab .item {
 | 
			
		||||
    color: #2B3674 !important;
 | 
			
		||||
    font-weight: 500 !important;
 | 
			
		||||
    padding: 0.8rem 1.2rem !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.settings-tab .active.item {
 | 
			
		||||
    color: #4318FF !important;
 | 
			
		||||
    font-weight: 600 !important;
 | 
			
		||||
    border-color: #4318FF !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ui.tab.segment {
 | 
			
		||||
    border: none !important;
 | 
			
		||||
    box-shadow: none !important;
 | 
			
		||||
    padding: 1rem 0 !important;
 | 
			
		||||
} 
 | 
			
		||||
							
								
								
									
										389
									
								
								web/default/src/pages/Dashboard/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										389
									
								
								web/default/src/pages/Dashboard/index.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,389 @@
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import { Card, Grid, Statistic } from 'semantic-ui-react';
 | 
			
		||||
import {
 | 
			
		||||
  LineChart,
 | 
			
		||||
  Line,
 | 
			
		||||
  XAxis,
 | 
			
		||||
  YAxis,
 | 
			
		||||
  CartesianGrid,
 | 
			
		||||
  Tooltip,
 | 
			
		||||
  ResponsiveContainer,
 | 
			
		||||
  BarChart,
 | 
			
		||||
  Bar,
 | 
			
		||||
  Legend,
 | 
			
		||||
} from 'recharts';
 | 
			
		||||
import axios from 'axios';
 | 
			
		||||
import './Dashboard.css';
 | 
			
		||||
 | 
			
		||||
// 在 Dashboard 组件内添加自定义配置
 | 
			
		||||
const chartConfig = {
 | 
			
		||||
  lineChart: {
 | 
			
		||||
    style: {
 | 
			
		||||
      background: '#fff',
 | 
			
		||||
      borderRadius: '8px',
 | 
			
		||||
    },
 | 
			
		||||
    line: {
 | 
			
		||||
      strokeWidth: 2,
 | 
			
		||||
      dot: false,
 | 
			
		||||
      activeDot: { r: 4 },
 | 
			
		||||
    },
 | 
			
		||||
    grid: {
 | 
			
		||||
      vertical: false,
 | 
			
		||||
      horizontal: true,
 | 
			
		||||
      opacity: 0.1,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  colors: {
 | 
			
		||||
    requests: '#4318FF',
 | 
			
		||||
    quota: '#00B5D8',
 | 
			
		||||
    tokens: '#6C63FF',
 | 
			
		||||
  },
 | 
			
		||||
  barColors: [
 | 
			
		||||
    '#4318FF', // 深紫色
 | 
			
		||||
    '#00B5D8', // 青色
 | 
			
		||||
    '#6C63FF', // 紫色
 | 
			
		||||
    '#05CD99', // 绿色
 | 
			
		||||
    '#FFB547', // 橙色
 | 
			
		||||
    '#FF5E7D', // 粉色
 | 
			
		||||
    '#41B883', // 翠绿
 | 
			
		||||
    '#7983FF', // 淡紫
 | 
			
		||||
    '#FF8F6B', // 珊瑚色
 | 
			
		||||
    '#49BEFF', // 天蓝
 | 
			
		||||
  ],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const Dashboard = () => {
 | 
			
		||||
  const [data, setData] = useState([]);
 | 
			
		||||
  const [summaryData, setSummaryData] = useState({
 | 
			
		||||
    todayRequests: 0,
 | 
			
		||||
    todayQuota: 0,
 | 
			
		||||
    todayTokens: 0,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    fetchDashboardData();
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const fetchDashboardData = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      const response = await axios.get('/api/user/dashboard');
 | 
			
		||||
      if (response.data.success) {
 | 
			
		||||
        const dashboardData = response.data.data || [];
 | 
			
		||||
        setData(dashboardData);
 | 
			
		||||
        calculateSummary(dashboardData);
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('Failed to fetch dashboard data:', error);
 | 
			
		||||
      setData([]);
 | 
			
		||||
      calculateSummary([]);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const calculateSummary = (dashboardData) => {
 | 
			
		||||
    if (!Array.isArray(dashboardData) || dashboardData.length === 0) {
 | 
			
		||||
      setSummaryData({
 | 
			
		||||
        todayRequests: 0,
 | 
			
		||||
        todayQuota: 0,
 | 
			
		||||
        todayTokens: 0
 | 
			
		||||
      });
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const today = new Date().toISOString().split('T')[0];
 | 
			
		||||
    const todayData = dashboardData.filter((item) => item.Day === today);
 | 
			
		||||
 | 
			
		||||
    const summary = {
 | 
			
		||||
      todayRequests: todayData.reduce(
 | 
			
		||||
        (sum, item) => sum + item.RequestCount,
 | 
			
		||||
        0
 | 
			
		||||
      ),
 | 
			
		||||
      todayQuota:
 | 
			
		||||
        todayData.reduce((sum, item) => sum + item.Quota, 0) / 1000000,
 | 
			
		||||
      todayTokens: todayData.reduce(
 | 
			
		||||
        (sum, item) => sum + item.PromptTokens + item.CompletionTokens,
 | 
			
		||||
        0
 | 
			
		||||
      ),
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    setSummaryData(summary);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // 处理数据以供折线图使用,补充缺失的日期
 | 
			
		||||
  const processTimeSeriesData = () => {
 | 
			
		||||
    const dailyData = {};
 | 
			
		||||
 | 
			
		||||
    // 获取日期范围
 | 
			
		||||
    const dates = data.map((item) => item.Day);
 | 
			
		||||
    const minDate = new Date(Math.min(...dates.map((d) => new Date(d))));
 | 
			
		||||
    const maxDate = new Date(Math.max(...dates.map((d) => new Date(d))));
 | 
			
		||||
 | 
			
		||||
    // 生成所有日期
 | 
			
		||||
    for (let d = new Date(minDate); d <= maxDate; d.setDate(d.getDate() + 1)) {
 | 
			
		||||
      const dateStr = d.toISOString().split('T')[0];
 | 
			
		||||
      dailyData[dateStr] = {
 | 
			
		||||
        date: dateStr,
 | 
			
		||||
        requests: 0,
 | 
			
		||||
        quota: 0,
 | 
			
		||||
        tokens: 0,
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 填充实际数据
 | 
			
		||||
    data.forEach((item) => {
 | 
			
		||||
      dailyData[item.Day].requests += item.RequestCount;
 | 
			
		||||
      dailyData[item.Day].quota += item.Quota / 1000000;
 | 
			
		||||
      dailyData[item.Day].tokens += item.PromptTokens + item.CompletionTokens;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return Object.values(dailyData).sort((a, b) =>
 | 
			
		||||
      a.date.localeCompare(b.date)
 | 
			
		||||
    );
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // 处理数据以供堆叠柱状图使用
 | 
			
		||||
  const processModelData = () => {
 | 
			
		||||
    const timeData = {};
 | 
			
		||||
 | 
			
		||||
    // 获取日期范围
 | 
			
		||||
    const dates = data.map((item) => item.Day);
 | 
			
		||||
    const minDate = new Date(Math.min(...dates.map((d) => new Date(d))));
 | 
			
		||||
    const maxDate = new Date(Math.max(...dates.map((d) => new Date(d))));
 | 
			
		||||
 | 
			
		||||
    // 生成所有日期
 | 
			
		||||
    for (let d = new Date(minDate); d <= maxDate; d.setDate(d.getDate() + 1)) {
 | 
			
		||||
      const dateStr = d.toISOString().split('T')[0];
 | 
			
		||||
      timeData[dateStr] = {
 | 
			
		||||
        date: dateStr,
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      // 初始化所有模型的数据为0
 | 
			
		||||
      const models = [...new Set(data.map((item) => item.ModelName))];
 | 
			
		||||
      models.forEach((model) => {
 | 
			
		||||
        timeData[dateStr][model] = 0;
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 填充实际数据
 | 
			
		||||
    data.forEach((item) => {
 | 
			
		||||
      timeData[item.Day][item.ModelName] =
 | 
			
		||||
        item.PromptTokens + item.CompletionTokens;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return Object.values(timeData).sort((a, b) => a.date.localeCompare(b.date));
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // 获取所有唯一的模型名称
 | 
			
		||||
  const getUniqueModels = () => {
 | 
			
		||||
    return [...new Set(data.map((item) => item.ModelName))];
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const timeSeriesData = processTimeSeriesData();
 | 
			
		||||
  const modelData = processModelData();
 | 
			
		||||
  const models = getUniqueModels();
 | 
			
		||||
 | 
			
		||||
  // 生成随机颜色
 | 
			
		||||
  const getRandomColor = (index) => {
 | 
			
		||||
    return chartConfig.barColors[index % chartConfig.barColors.length];
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className='dashboard-container'>
 | 
			
		||||
      {/* 三个并排的折线图 */}
 | 
			
		||||
      <Grid columns={3} stackable className='charts-grid'>
 | 
			
		||||
        <Grid.Column>
 | 
			
		||||
          <Card fluid className='chart-card'>
 | 
			
		||||
            <Card.Content>
 | 
			
		||||
              <Card.Header>
 | 
			
		||||
                模型请求趋势
 | 
			
		||||
                <span className='stat-value'>{summaryData.todayRequests}</span>
 | 
			
		||||
              </Card.Header>
 | 
			
		||||
              <div className='chart-container'>
 | 
			
		||||
                <ResponsiveContainer width='100%' height={120}>
 | 
			
		||||
                  <LineChart data={timeSeriesData}>
 | 
			
		||||
                    <CartesianGrid
 | 
			
		||||
                      strokeDasharray='3 3'
 | 
			
		||||
                      vertical={chartConfig.lineChart.grid.vertical}
 | 
			
		||||
                      horizontal={chartConfig.lineChart.grid.horizontal}
 | 
			
		||||
                      opacity={chartConfig.lineChart.grid.opacity}
 | 
			
		||||
                    />
 | 
			
		||||
                    <XAxis
 | 
			
		||||
                      dataKey='date'
 | 
			
		||||
                      axisLine={false}
 | 
			
		||||
                      tickLine={false}
 | 
			
		||||
                      tick={{ fontSize: 12, fill: '#A3AED0' }}
 | 
			
		||||
                    />
 | 
			
		||||
                    <YAxis hide={true} />
 | 
			
		||||
                    <Tooltip
 | 
			
		||||
                      contentStyle={{
 | 
			
		||||
                        background: '#fff',
 | 
			
		||||
                        border: 'none',
 | 
			
		||||
                        borderRadius: '4px',
 | 
			
		||||
                        boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
 | 
			
		||||
                      }}
 | 
			
		||||
                    />
 | 
			
		||||
                    <Line
 | 
			
		||||
                      type='monotone'
 | 
			
		||||
                      dataKey='requests'
 | 
			
		||||
                      stroke={chartConfig.colors.requests}
 | 
			
		||||
                      strokeWidth={chartConfig.lineChart.line.strokeWidth}
 | 
			
		||||
                      dot={chartConfig.lineChart.line.dot}
 | 
			
		||||
                      activeDot={chartConfig.lineChart.line.activeDot}
 | 
			
		||||
                    />
 | 
			
		||||
                  </LineChart>
 | 
			
		||||
                </ResponsiveContainer>
 | 
			
		||||
              </div>
 | 
			
		||||
            </Card.Content>
 | 
			
		||||
          </Card>
 | 
			
		||||
        </Grid.Column>
 | 
			
		||||
 | 
			
		||||
        <Grid.Column>
 | 
			
		||||
          <Card fluid className='chart-card'>
 | 
			
		||||
            <Card.Content>
 | 
			
		||||
              <Card.Header>
 | 
			
		||||
                额度消费趋势
 | 
			
		||||
                <span className='stat-value'>
 | 
			
		||||
                  ${summaryData.todayQuota.toFixed(3)}
 | 
			
		||||
                </span>
 | 
			
		||||
              </Card.Header>
 | 
			
		||||
              <div className='chart-container'>
 | 
			
		||||
                <ResponsiveContainer width='100%' height={120}>
 | 
			
		||||
                  <LineChart data={timeSeriesData}>
 | 
			
		||||
                    <CartesianGrid
 | 
			
		||||
                      strokeDasharray='3 3'
 | 
			
		||||
                      vertical={chartConfig.lineChart.grid.vertical}
 | 
			
		||||
                      horizontal={chartConfig.lineChart.grid.horizontal}
 | 
			
		||||
                      opacity={chartConfig.lineChart.grid.opacity}
 | 
			
		||||
                    />
 | 
			
		||||
                    <XAxis
 | 
			
		||||
                      dataKey='date'
 | 
			
		||||
                      axisLine={false}
 | 
			
		||||
                      tickLine={false}
 | 
			
		||||
                      tick={{ fontSize: 12, fill: '#A3AED0' }}
 | 
			
		||||
                    />
 | 
			
		||||
                    <YAxis hide={true} />
 | 
			
		||||
                    <Tooltip
 | 
			
		||||
                      contentStyle={{
 | 
			
		||||
                        background: '#fff',
 | 
			
		||||
                        border: 'none',
 | 
			
		||||
                        borderRadius: '4px',
 | 
			
		||||
                        boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
 | 
			
		||||
                      }}
 | 
			
		||||
                    />
 | 
			
		||||
                    <Line
 | 
			
		||||
                      type='monotone'
 | 
			
		||||
                      dataKey='quota'
 | 
			
		||||
                      stroke={chartConfig.colors.quota}
 | 
			
		||||
                      strokeWidth={chartConfig.lineChart.line.strokeWidth}
 | 
			
		||||
                      dot={chartConfig.lineChart.line.dot}
 | 
			
		||||
                      activeDot={chartConfig.lineChart.line.activeDot}
 | 
			
		||||
                    />
 | 
			
		||||
                  </LineChart>
 | 
			
		||||
                </ResponsiveContainer>
 | 
			
		||||
              </div>
 | 
			
		||||
            </Card.Content>
 | 
			
		||||
          </Card>
 | 
			
		||||
        </Grid.Column>
 | 
			
		||||
 | 
			
		||||
        <Grid.Column>
 | 
			
		||||
          <Card fluid className='chart-card'>
 | 
			
		||||
            <Card.Content>
 | 
			
		||||
              <Card.Header>
 | 
			
		||||
                Token 消费趋势
 | 
			
		||||
                <span className='stat-value'>{summaryData.todayTokens}</span>
 | 
			
		||||
              </Card.Header>
 | 
			
		||||
              <div className='chart-container'>
 | 
			
		||||
                <ResponsiveContainer width='100%' height={120}>
 | 
			
		||||
                  <LineChart data={timeSeriesData}>
 | 
			
		||||
                    <CartesianGrid
 | 
			
		||||
                      strokeDasharray='3 3'
 | 
			
		||||
                      vertical={chartConfig.lineChart.grid.vertical}
 | 
			
		||||
                      horizontal={chartConfig.lineChart.grid.horizontal}
 | 
			
		||||
                      opacity={chartConfig.lineChart.grid.opacity}
 | 
			
		||||
                    />
 | 
			
		||||
                    <XAxis
 | 
			
		||||
                      dataKey='date'
 | 
			
		||||
                      axisLine={false}
 | 
			
		||||
                      tickLine={false}
 | 
			
		||||
                      tick={{ fontSize: 12, fill: '#A3AED0' }}
 | 
			
		||||
                    />
 | 
			
		||||
                    <YAxis hide={true} />
 | 
			
		||||
                    <Tooltip
 | 
			
		||||
                      contentStyle={{
 | 
			
		||||
                        background: '#fff',
 | 
			
		||||
                        border: 'none',
 | 
			
		||||
                        borderRadius: '4px',
 | 
			
		||||
                        boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
 | 
			
		||||
                      }}
 | 
			
		||||
                    />
 | 
			
		||||
                    <Line
 | 
			
		||||
                      type='monotone'
 | 
			
		||||
                      dataKey='tokens'
 | 
			
		||||
                      stroke={chartConfig.colors.tokens}
 | 
			
		||||
                      strokeWidth={chartConfig.lineChart.line.strokeWidth}
 | 
			
		||||
                      dot={chartConfig.lineChart.line.dot}
 | 
			
		||||
                      activeDot={chartConfig.lineChart.line.activeDot}
 | 
			
		||||
                    />
 | 
			
		||||
                  </LineChart>
 | 
			
		||||
                </ResponsiveContainer>
 | 
			
		||||
              </div>
 | 
			
		||||
            </Card.Content>
 | 
			
		||||
          </Card>
 | 
			
		||||
        </Grid.Column>
 | 
			
		||||
      </Grid>
 | 
			
		||||
 | 
			
		||||
      {/* 模型使用统计 */}
 | 
			
		||||
      <Card fluid className='chart-card'>
 | 
			
		||||
        <Card.Content>
 | 
			
		||||
          <Card.Header>统计</Card.Header>
 | 
			
		||||
          <div className='chart-container'>
 | 
			
		||||
            <ResponsiveContainer width='100%' height={300}>
 | 
			
		||||
              <BarChart data={modelData}>
 | 
			
		||||
                <CartesianGrid
 | 
			
		||||
                  strokeDasharray='3 3'
 | 
			
		||||
                  vertical={false}
 | 
			
		||||
                  opacity={0.1}
 | 
			
		||||
                />
 | 
			
		||||
                <XAxis
 | 
			
		||||
                  dataKey='date'
 | 
			
		||||
                  axisLine={false}
 | 
			
		||||
                  tickLine={false}
 | 
			
		||||
                  tick={{ fontSize: 12, fill: '#A3AED0' }}
 | 
			
		||||
                />
 | 
			
		||||
                <YAxis
 | 
			
		||||
                  axisLine={false}
 | 
			
		||||
                  tickLine={false}
 | 
			
		||||
                  tick={{ fontSize: 12, fill: '#A3AED0' }}
 | 
			
		||||
                />
 | 
			
		||||
                <Tooltip
 | 
			
		||||
                  contentStyle={{
 | 
			
		||||
                    background: '#fff',
 | 
			
		||||
                    border: 'none',
 | 
			
		||||
                    borderRadius: '4px',
 | 
			
		||||
                    boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
 | 
			
		||||
                  }}
 | 
			
		||||
                />
 | 
			
		||||
                <Legend
 | 
			
		||||
                  wrapperStyle={{
 | 
			
		||||
                    paddingTop: '20px',
 | 
			
		||||
                  }}
 | 
			
		||||
                />
 | 
			
		||||
                {models.map((model, index) => (
 | 
			
		||||
                  <Bar
 | 
			
		||||
                    key={model}
 | 
			
		||||
                    dataKey={model}
 | 
			
		||||
                    stackId='a'
 | 
			
		||||
                    fill={getRandomColor(index)}
 | 
			
		||||
                    name={model}
 | 
			
		||||
                    radius={[4, 4, 0, 0]}
 | 
			
		||||
                  />
 | 
			
		||||
                ))}
 | 
			
		||||
              </BarChart>
 | 
			
		||||
            </ResponsiveContainer>
 | 
			
		||||
          </div>
 | 
			
		||||
        </Card.Content>
 | 
			
		||||
      </Card>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Dashboard;
 | 
			
		||||
@@ -3,22 +3,25 @@ import { Card, Grid, Header, Segment } from 'semantic-ui-react';
 | 
			
		||||
import { API, showError, showNotice, timestamp2string } from '../../helpers';
 | 
			
		||||
import { StatusContext } from '../../context/Status';
 | 
			
		||||
import { marked } from 'marked';
 | 
			
		||||
import { UserContext } from '../../context/User';
 | 
			
		||||
import { Link } from 'react-router-dom';
 | 
			
		||||
 | 
			
		||||
const Home = () => {
 | 
			
		||||
  const [statusState, statusDispatch] = useContext(StatusContext);
 | 
			
		||||
  const [homePageContentLoaded, setHomePageContentLoaded] = useState(false);
 | 
			
		||||
  const [homePageContent, setHomePageContent] = useState('');
 | 
			
		||||
  const [userState] = useContext(UserContext);
 | 
			
		||||
 | 
			
		||||
  const displayNotice = async () => {
 | 
			
		||||
    const res = await API.get('/api/notice');
 | 
			
		||||
    const { success, message, data } = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
      let oldNotice = localStorage.getItem('notice');
 | 
			
		||||
        if (data !== oldNotice && data !== '') {
 | 
			
		||||
            const htmlNotice = marked(data);
 | 
			
		||||
            showNotice(htmlNotice, true);
 | 
			
		||||
            localStorage.setItem('notice', data);
 | 
			
		||||
        }
 | 
			
		||||
      if (data !== oldNotice && data !== '') {
 | 
			
		||||
        const htmlNotice = marked(data);
 | 
			
		||||
        showNotice(htmlNotice, true);
 | 
			
		||||
        localStorage.setItem('notice', data);
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      showError(message);
 | 
			
		||||
    }
 | 
			
		||||
@@ -51,81 +54,239 @@ const Home = () => {
 | 
			
		||||
    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 ? statusState?.status?.version : "unknown"}</p>
 | 
			
		||||
                      <p>
 | 
			
		||||
                        源码:
 | 
			
		||||
                        <a
 | 
			
		||||
                          href='https://github.com/songquanpeng/one-api'
 | 
			
		||||
                          target='_blank'
 | 
			
		||||
      {homePageContentLoaded && homePageContent === '' ? (
 | 
			
		||||
        <div className='dashboard-container'>
 | 
			
		||||
          <Card fluid className='chart-card'>
 | 
			
		||||
            <Card.Content>
 | 
			
		||||
              <Card.Header className='header'>欢迎使用 One API</Card.Header>
 | 
			
		||||
              <Card.Description style={{ lineHeight: '1.6' }}>
 | 
			
		||||
                <p>
 | 
			
		||||
                  One API 是一个 LLM API
 | 
			
		||||
                  接口管理和分发系统,可以帮助您更好地管理和使用各大厂商的 LLM
 | 
			
		||||
                  API。
 | 
			
		||||
                </p>
 | 
			
		||||
                {!userState.user && (
 | 
			
		||||
                  <p>
 | 
			
		||||
                    如需使用,请先<Link to='/login'>登录</Link>或
 | 
			
		||||
                    <Link to='/register'>注册</Link>。
 | 
			
		||||
                  </p>
 | 
			
		||||
                )}
 | 
			
		||||
              </Card.Description>
 | 
			
		||||
            </Card.Content>
 | 
			
		||||
          </Card>
 | 
			
		||||
          <Card fluid className='chart-card'>
 | 
			
		||||
            <Card.Content>
 | 
			
		||||
              <Card.Header>
 | 
			
		||||
                <Header as='h3'>系统状况</Header>
 | 
			
		||||
              </Card.Header>
 | 
			
		||||
              <Grid columns={2} stackable>
 | 
			
		||||
                <Grid.Column>
 | 
			
		||||
                  <Card
 | 
			
		||||
                    fluid
 | 
			
		||||
                    className='chart-card'
 | 
			
		||||
                    style={{ boxShadow: '0 1px 3px rgba(0,0,0,0.12)' }}
 | 
			
		||||
                  >
 | 
			
		||||
                    <Card.Content>
 | 
			
		||||
                      <Card.Header>
 | 
			
		||||
                        <Header as='h3' style={{ color: '#444' }}>
 | 
			
		||||
                          系统信息
 | 
			
		||||
                        </Header>
 | 
			
		||||
                      </Card.Header>
 | 
			
		||||
                      <Card.Description
 | 
			
		||||
                        style={{ lineHeight: '2', marginTop: '1em' }}
 | 
			
		||||
                      >
 | 
			
		||||
                        <p
 | 
			
		||||
                          style={{
 | 
			
		||||
                            display: 'flex',
 | 
			
		||||
                            alignItems: 'center',
 | 
			
		||||
                            gap: '0.5em',
 | 
			
		||||
                          }}
 | 
			
		||||
                        >
 | 
			
		||||
                          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
 | 
			
		||||
                          <i className='info circle icon'></i>
 | 
			
		||||
                          <span style={{ fontWeight: 'bold' }}>名称:</span>
 | 
			
		||||
                          <span>{statusState?.status?.system_name}</span>
 | 
			
		||||
                        </p>
 | 
			
		||||
                        <p
 | 
			
		||||
                          style={{
 | 
			
		||||
                            display: 'flex',
 | 
			
		||||
                            alignItems: 'center',
 | 
			
		||||
                            gap: '0.5em',
 | 
			
		||||
                          }}
 | 
			
		||||
                        >
 | 
			
		||||
                          <i className='code branch icon'></i>
 | 
			
		||||
                          <span style={{ fontWeight: 'bold' }}>版本:</span>
 | 
			
		||||
                          <span>
 | 
			
		||||
                            {statusState?.status?.version || 'unknown'}
 | 
			
		||||
                          </span>
 | 
			
		||||
                        </p>
 | 
			
		||||
                        <p
 | 
			
		||||
                          style={{
 | 
			
		||||
                            display: 'flex',
 | 
			
		||||
                            alignItems: 'center',
 | 
			
		||||
                            gap: '0.5em',
 | 
			
		||||
                          }}
 | 
			
		||||
                        >
 | 
			
		||||
                          <i className='github icon'></i>
 | 
			
		||||
                          <span style={{ fontWeight: 'bold' }}>源码:</span>
 | 
			
		||||
                          <a
 | 
			
		||||
                            href='https://github.com/songquanpeng/one-api'
 | 
			
		||||
                            target='_blank'
 | 
			
		||||
                            style={{ color: '#2185d0' }}
 | 
			
		||||
                          >
 | 
			
		||||
                            GitHub 仓库
 | 
			
		||||
                          </a>
 | 
			
		||||
                        </p>
 | 
			
		||||
                        <p
 | 
			
		||||
                          style={{
 | 
			
		||||
                            display: 'flex',
 | 
			
		||||
                            alignItems: 'center',
 | 
			
		||||
                            gap: '0.5em',
 | 
			
		||||
                          }}
 | 
			
		||||
                        >
 | 
			
		||||
                          <i className='clock outline icon'></i>
 | 
			
		||||
                          <span style={{ fontWeight: 'bold' }}>启动时间:</span>
 | 
			
		||||
                          <span>{getStartTimeString()}</span>
 | 
			
		||||
                        </p>
 | 
			
		||||
                      </Card.Description>
 | 
			
		||||
                    </Card.Content>
 | 
			
		||||
                  </Card>
 | 
			
		||||
                </Grid.Column>
 | 
			
		||||
 | 
			
		||||
                <Grid.Column>
 | 
			
		||||
                  <Card
 | 
			
		||||
                    fluid
 | 
			
		||||
                    className='chart-card'
 | 
			
		||||
                    style={{ boxShadow: '0 1px 3px rgba(0,0,0,0.12)' }}
 | 
			
		||||
                  >
 | 
			
		||||
                    <Card.Content>
 | 
			
		||||
                      <Card.Header>
 | 
			
		||||
                        <Header as='h3' style={{ color: '#444' }}>
 | 
			
		||||
                          系统配置
 | 
			
		||||
                        </Header>
 | 
			
		||||
                      </Card.Header>
 | 
			
		||||
                      <Card.Description
 | 
			
		||||
                        style={{ lineHeight: '2', marginTop: '1em' }}
 | 
			
		||||
                      >
 | 
			
		||||
                        <p
 | 
			
		||||
                          style={{
 | 
			
		||||
                            display: 'flex',
 | 
			
		||||
                            alignItems: 'center',
 | 
			
		||||
                            gap: '0.5em',
 | 
			
		||||
                          }}
 | 
			
		||||
                        >
 | 
			
		||||
                          <i className='envelope icon'></i>
 | 
			
		||||
                          <span style={{ fontWeight: 'bold' }}>邮箱验证:</span>
 | 
			
		||||
                          <span
 | 
			
		||||
                            style={{
 | 
			
		||||
                              color: statusState?.status?.email_verification
 | 
			
		||||
                                ? '#21ba45'
 | 
			
		||||
                                : '#db2828',
 | 
			
		||||
                              fontWeight: '500',
 | 
			
		||||
                            }}
 | 
			
		||||
                          >
 | 
			
		||||
                            {statusState?.status?.email_verification
 | 
			
		||||
                              ? '已启用'
 | 
			
		||||
                              : '未启用'}
 | 
			
		||||
                          </span>
 | 
			
		||||
                        </p>
 | 
			
		||||
                        <p
 | 
			
		||||
                          style={{
 | 
			
		||||
                            display: 'flex',
 | 
			
		||||
                            alignItems: 'center',
 | 
			
		||||
                            gap: '0.5em',
 | 
			
		||||
                          }}
 | 
			
		||||
                        >
 | 
			
		||||
                          <i className='github icon'></i>
 | 
			
		||||
                          <span style={{ fontWeight: 'bold' }}>
 | 
			
		||||
                            GitHub 身份验证:
 | 
			
		||||
                          </span>
 | 
			
		||||
                          <span
 | 
			
		||||
                            style={{
 | 
			
		||||
                              color: statusState?.status?.github_oauth
 | 
			
		||||
                                ? '#21ba45'
 | 
			
		||||
                                : '#db2828',
 | 
			
		||||
                              fontWeight: '500',
 | 
			
		||||
                            }}
 | 
			
		||||
                          >
 | 
			
		||||
                            {statusState?.status?.github_oauth
 | 
			
		||||
                              ? '已启用'
 | 
			
		||||
                              : '未启用'}
 | 
			
		||||
                          </span>
 | 
			
		||||
                        </p>
 | 
			
		||||
                        <p
 | 
			
		||||
                          style={{
 | 
			
		||||
                            display: 'flex',
 | 
			
		||||
                            alignItems: 'center',
 | 
			
		||||
                            gap: '0.5em',
 | 
			
		||||
                          }}
 | 
			
		||||
                        >
 | 
			
		||||
                          <i className='wechat icon'></i>
 | 
			
		||||
                          <span style={{ fontWeight: 'bold' }}>
 | 
			
		||||
                            微信身份验证:
 | 
			
		||||
                          </span>
 | 
			
		||||
                          <span
 | 
			
		||||
                            style={{
 | 
			
		||||
                              color: statusState?.status?.wechat_login
 | 
			
		||||
                                ? '#21ba45'
 | 
			
		||||
                                : '#db2828',
 | 
			
		||||
                              fontWeight: '500',
 | 
			
		||||
                            }}
 | 
			
		||||
                          >
 | 
			
		||||
                            {statusState?.status?.wechat_login
 | 
			
		||||
                              ? '已启用'
 | 
			
		||||
                              : '未启用'}
 | 
			
		||||
                          </span>
 | 
			
		||||
                        </p>
 | 
			
		||||
                        <p
 | 
			
		||||
                          style={{
 | 
			
		||||
                            display: 'flex',
 | 
			
		||||
                            alignItems: 'center',
 | 
			
		||||
                            gap: '0.5em',
 | 
			
		||||
                          }}
 | 
			
		||||
                        >
 | 
			
		||||
                          <i className='shield alternate icon'></i>
 | 
			
		||||
                          <span style={{ fontWeight: 'bold' }}>
 | 
			
		||||
                            Turnstile 校验:
 | 
			
		||||
                          </span>
 | 
			
		||||
                          <span
 | 
			
		||||
                            style={{
 | 
			
		||||
                              color: statusState?.status?.turnstile_check
 | 
			
		||||
                                ? '#21ba45'
 | 
			
		||||
                                : '#db2828',
 | 
			
		||||
                              fontWeight: '500',
 | 
			
		||||
                            }}
 | 
			
		||||
                          >
 | 
			
		||||
                            {statusState?.status?.turnstile_check
 | 
			
		||||
                              ? '已启用'
 | 
			
		||||
                              : '未启用'}
 | 
			
		||||
                          </span>
 | 
			
		||||
                        </p>
 | 
			
		||||
                      </Card.Description>
 | 
			
		||||
                    </Card.Content>
 | 
			
		||||
                  </Card>
 | 
			
		||||
                </Grid.Column>
 | 
			
		||||
              </Grid>
 | 
			
		||||
            </Card.Content>
 | 
			
		||||
          </Card>{' '}
 | 
			
		||||
        </div>
 | 
			
		||||
      ) : (
 | 
			
		||||
        <>
 | 
			
		||||
          {homePageContent.startsWith('https://') ? (
 | 
			
		||||
            <iframe
 | 
			
		||||
              src={homePageContent}
 | 
			
		||||
              style={{ width: '100%', height: '100vh', border: 'none' }}
 | 
			
		||||
            /> : <div style={{ fontSize: 'larger' }} dangerouslySetInnerHTML={{ __html: homePageContent }}></div>
 | 
			
		||||
          }
 | 
			
		||||
            />
 | 
			
		||||
          ) : (
 | 
			
		||||
            <div
 | 
			
		||||
              style={{ fontSize: 'larger' }}
 | 
			
		||||
              dangerouslySetInnerHTML={{ __html: homePageContent }}
 | 
			
		||||
            ></div>
 | 
			
		||||
          )}
 | 
			
		||||
        </>
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      )}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,16 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { Header, Segment } from 'semantic-ui-react';
 | 
			
		||||
import { Card } from 'semantic-ui-react';
 | 
			
		||||
import LogsTable from '../../components/LogsTable';
 | 
			
		||||
 | 
			
		||||
const Token = () => (
 | 
			
		||||
  <>
 | 
			
		||||
    <LogsTable />
 | 
			
		||||
  </>
 | 
			
		||||
const Log = () => (
 | 
			
		||||
  <div className='dashboard-container'>
 | 
			
		||||
    <Card fluid className='chart-card'>
 | 
			
		||||
      <Card.Content>
 | 
			
		||||
        {/*<Card.Header className='header'>操作日志</Card.Header>*/}
 | 
			
		||||
        <LogsTable />
 | 
			
		||||
      </Card.Content>
 | 
			
		||||
    </Card>
 | 
			
		||||
  </div>
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export default Token;
 | 
			
		||||
export default Log;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import { Button, Form, Header, Segment } from 'semantic-ui-react';
 | 
			
		||||
import { Button, Form, Card } from 'semantic-ui-react';
 | 
			
		||||
import { useParams, useNavigate } from 'react-router-dom';
 | 
			
		||||
import { API, downloadTextAsFile, showError, showSuccess } from '../../helpers';
 | 
			
		||||
import { renderQuota, renderQuotaWithPrompt } from '../../helpers/render';
 | 
			
		||||
@@ -13,7 +13,7 @@ const EditRedemption = () => {
 | 
			
		||||
  const originInputs = {
 | 
			
		||||
    name: '',
 | 
			
		||||
    quota: 100000,
 | 
			
		||||
    count: 1
 | 
			
		||||
    count: 1,
 | 
			
		||||
  };
 | 
			
		||||
  const [inputs, setInputs] = useState(originInputs);
 | 
			
		||||
  const { name, quota, count } = inputs;
 | 
			
		||||
@@ -49,10 +49,13 @@ const EditRedemption = () => {
 | 
			
		||||
    localInputs.quota = parseInt(localInputs.quota);
 | 
			
		||||
    let res;
 | 
			
		||||
    if (isEdit) {
 | 
			
		||||
      res = await API.put(`/api/redemption/`, { ...localInputs, id: parseInt(redemptionId) });
 | 
			
		||||
      res = await API.put(`/api/redemption/`, {
 | 
			
		||||
        ...localInputs,
 | 
			
		||||
        id: parseInt(redemptionId),
 | 
			
		||||
      });
 | 
			
		||||
    } else {
 | 
			
		||||
      res = await API.post(`/api/redemption/`, {
 | 
			
		||||
        ...localInputs
 | 
			
		||||
        ...localInputs,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    const { success, message, data } = res.data;
 | 
			
		||||
@@ -67,61 +70,67 @@ const EditRedemption = () => {
 | 
			
		||||
      showError(message);
 | 
			
		||||
    }
 | 
			
		||||
    if (!isEdit && data) {
 | 
			
		||||
      let text = "";
 | 
			
		||||
      let text = '';
 | 
			
		||||
      for (let i = 0; i < data.length; i++) {
 | 
			
		||||
        text += data[i] + "\n";
 | 
			
		||||
        text += data[i] + '\n';
 | 
			
		||||
      }
 | 
			
		||||
      downloadTextAsFile(text, `${inputs.name}.txt`);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <Segment loading={loading}>
 | 
			
		||||
        <Header as='h3'>{isEdit ? '更新兑换码信息' : '创建新的兑换码'}</Header>
 | 
			
		||||
        <Form autoComplete='new-password'>
 | 
			
		||||
          <Form.Field>
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label='名称'
 | 
			
		||||
              name='name'
 | 
			
		||||
              placeholder={'请输入名称'}
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              value={name}
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              required={!isEdit}
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Field>
 | 
			
		||||
          <Form.Field>
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label={`额度${renderQuotaWithPrompt(quota)}`}
 | 
			
		||||
              name='quota'
 | 
			
		||||
              placeholder={'请输入单个兑换码中包含的额度'}
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              value={quota}
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              type='number'
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Field>
 | 
			
		||||
          {
 | 
			
		||||
            !isEdit && <>
 | 
			
		||||
              <Form.Field>
 | 
			
		||||
                <Form.Input
 | 
			
		||||
                  label='生成数量'
 | 
			
		||||
                  name='count'
 | 
			
		||||
                  placeholder={'请输入生成数量'}
 | 
			
		||||
                  onChange={handleInputChange}
 | 
			
		||||
                  value={count}
 | 
			
		||||
                  autoComplete='new-password'
 | 
			
		||||
                  type='number'
 | 
			
		||||
                />
 | 
			
		||||
              </Form.Field>
 | 
			
		||||
            </>
 | 
			
		||||
          }
 | 
			
		||||
          <Button positive onClick={submit}>提交</Button>
 | 
			
		||||
          <Button onClick={handleCancel}>取消</Button>
 | 
			
		||||
        </Form>
 | 
			
		||||
      </Segment>
 | 
			
		||||
    </>
 | 
			
		||||
    <div className='dashboard-container'>
 | 
			
		||||
      <Card fluid className='chart-card'>
 | 
			
		||||
        <Card.Content>
 | 
			
		||||
          <Card.Header className='header'>
 | 
			
		||||
            {isEdit ? '更新兑换码信息' : '创建新的兑换码'}
 | 
			
		||||
          </Card.Header>
 | 
			
		||||
          <Form loading={loading} autoComplete='new-password'>
 | 
			
		||||
            <Form.Field>
 | 
			
		||||
              <Form.Input
 | 
			
		||||
                label='名称'
 | 
			
		||||
                name='name'
 | 
			
		||||
                placeholder={'请输入名称'}
 | 
			
		||||
                onChange={handleInputChange}
 | 
			
		||||
                value={name}
 | 
			
		||||
                autoComplete='new-password'
 | 
			
		||||
                required={!isEdit}
 | 
			
		||||
              />
 | 
			
		||||
            </Form.Field>
 | 
			
		||||
            <Form.Field>
 | 
			
		||||
              <Form.Input
 | 
			
		||||
                label={`额度${renderQuotaWithPrompt(quota)}`}
 | 
			
		||||
                name='quota'
 | 
			
		||||
                placeholder={'请输入单个兑换码中包含的额度'}
 | 
			
		||||
                onChange={handleInputChange}
 | 
			
		||||
                value={quota}
 | 
			
		||||
                autoComplete='new-password'
 | 
			
		||||
                type='number'
 | 
			
		||||
              />
 | 
			
		||||
            </Form.Field>
 | 
			
		||||
            {!isEdit && (
 | 
			
		||||
              <>
 | 
			
		||||
                <Form.Field>
 | 
			
		||||
                  <Form.Input
 | 
			
		||||
                    label='生成数量'
 | 
			
		||||
                    name='count'
 | 
			
		||||
                    placeholder={'请输入生成数量'}
 | 
			
		||||
                    onChange={handleInputChange}
 | 
			
		||||
                    value={count}
 | 
			
		||||
                    autoComplete='new-password'
 | 
			
		||||
                    type='number'
 | 
			
		||||
                  />
 | 
			
		||||
                </Form.Field>
 | 
			
		||||
              </>
 | 
			
		||||
            )}
 | 
			
		||||
            <Button positive onClick={submit}>
 | 
			
		||||
              提交
 | 
			
		||||
            </Button>
 | 
			
		||||
            <Button onClick={handleCancel}>取消</Button>
 | 
			
		||||
          </Form>
 | 
			
		||||
        </Card.Content>
 | 
			
		||||
      </Card>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,16 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { Segment, Header } from 'semantic-ui-react';
 | 
			
		||||
import { Card } from 'semantic-ui-react';
 | 
			
		||||
import RedemptionsTable from '../../components/RedemptionsTable';
 | 
			
		||||
 | 
			
		||||
const Redemption = () => (
 | 
			
		||||
  <>
 | 
			
		||||
    <Segment>
 | 
			
		||||
      <Header as='h3'>管理兑换码</Header>
 | 
			
		||||
      <RedemptionsTable/>
 | 
			
		||||
    </Segment>
 | 
			
		||||
  </>
 | 
			
		||||
  <div className='dashboard-container'>
 | 
			
		||||
    <Card fluid className='chart-card'>
 | 
			
		||||
      <Card.Content>
 | 
			
		||||
        <Card.Header className='header'>兑换管理</Card.Header>
 | 
			
		||||
        <RedemptionsTable />
 | 
			
		||||
      </Card.Content>
 | 
			
		||||
    </Card>
 | 
			
		||||
  </div>
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export default Redemption;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { Segment, Tab } from 'semantic-ui-react';
 | 
			
		||||
import { Card, Tab } from 'semantic-ui-react';
 | 
			
		||||
import SystemSetting from '../../components/SystemSetting';
 | 
			
		||||
import { isRoot } from '../../helpers';
 | 
			
		||||
import OtherSetting from '../../components/OtherSetting';
 | 
			
		||||
@@ -14,8 +14,8 @@ const Setting = () => {
 | 
			
		||||
        <Tab.Pane attached={false}>
 | 
			
		||||
          <PersonalSetting />
 | 
			
		||||
        </Tab.Pane>
 | 
			
		||||
      )
 | 
			
		||||
    }
 | 
			
		||||
      ),
 | 
			
		||||
    },
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  if (isRoot()) {
 | 
			
		||||
@@ -25,7 +25,7 @@ const Setting = () => {
 | 
			
		||||
        <Tab.Pane attached={false}>
 | 
			
		||||
          <OperationSetting />
 | 
			
		||||
        </Tab.Pane>
 | 
			
		||||
      )
 | 
			
		||||
      ),
 | 
			
		||||
    });
 | 
			
		||||
    panes.push({
 | 
			
		||||
      menuItem: '系统设置',
 | 
			
		||||
@@ -33,7 +33,7 @@ const Setting = () => {
 | 
			
		||||
        <Tab.Pane attached={false}>
 | 
			
		||||
          <SystemSetting />
 | 
			
		||||
        </Tab.Pane>
 | 
			
		||||
      )
 | 
			
		||||
      ),
 | 
			
		||||
    });
 | 
			
		||||
    panes.push({
 | 
			
		||||
      menuItem: '其他设置',
 | 
			
		||||
@@ -41,14 +41,26 @@ const Setting = () => {
 | 
			
		||||
        <Tab.Pane attached={false}>
 | 
			
		||||
          <OtherSetting />
 | 
			
		||||
        </Tab.Pane>
 | 
			
		||||
      )
 | 
			
		||||
      ),
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Segment>
 | 
			
		||||
      <Tab menu={{ secondary: true, pointing: true }} panes={panes} />
 | 
			
		||||
    </Segment>
 | 
			
		||||
    <div className='dashboard-container'>
 | 
			
		||||
      <Card fluid className='chart-card'>
 | 
			
		||||
        <Card.Content>
 | 
			
		||||
          <Card.Header className='header'>系统设置</Card.Header>
 | 
			
		||||
          <Tab
 | 
			
		||||
            menu={{
 | 
			
		||||
              secondary: true,
 | 
			
		||||
              pointing: true,
 | 
			
		||||
              className: 'settings-tab', // 添加自定义类名以便样式化
 | 
			
		||||
            }}
 | 
			
		||||
            panes={panes}
 | 
			
		||||
          />
 | 
			
		||||
        </Card.Content>
 | 
			
		||||
      </Card>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,20 @@
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import { Button, Form, Header, Message, Segment } from 'semantic-ui-react';
 | 
			
		||||
import {
 | 
			
		||||
  Button,
 | 
			
		||||
  Form,
 | 
			
		||||
  Header,
 | 
			
		||||
  Message,
 | 
			
		||||
  Segment,
 | 
			
		||||
  Card,
 | 
			
		||||
} from 'semantic-ui-react';
 | 
			
		||||
import { useNavigate, useParams } from 'react-router-dom';
 | 
			
		||||
import { API, copy, showError, showSuccess, timestamp2string } from '../../helpers';
 | 
			
		||||
import {
 | 
			
		||||
  API,
 | 
			
		||||
  copy,
 | 
			
		||||
  showError,
 | 
			
		||||
  showSuccess,
 | 
			
		||||
  timestamp2string,
 | 
			
		||||
} from '../../helpers';
 | 
			
		||||
import { renderQuotaWithPrompt } from '../../helpers/render';
 | 
			
		||||
 | 
			
		||||
const EditToken = () => {
 | 
			
		||||
@@ -16,7 +29,7 @@ const EditToken = () => {
 | 
			
		||||
    expired_time: -1,
 | 
			
		||||
    unlimited_quota: false,
 | 
			
		||||
    models: [],
 | 
			
		||||
    subnet: "",
 | 
			
		||||
    subnet: '',
 | 
			
		||||
  };
 | 
			
		||||
  const [inputs, setInputs] = useState(originInputs);
 | 
			
		||||
  const { name, remain_quota, expired_time, unlimited_quota } = inputs;
 | 
			
		||||
@@ -79,7 +92,7 @@ const EditToken = () => {
 | 
			
		||||
        return {
 | 
			
		||||
          key: model,
 | 
			
		||||
          text: model,
 | 
			
		||||
          value: model
 | 
			
		||||
          value: model,
 | 
			
		||||
        };
 | 
			
		||||
      });
 | 
			
		||||
      setModelOptions(options);
 | 
			
		||||
@@ -103,7 +116,10 @@ const EditToken = () => {
 | 
			
		||||
    localInputs.models = localInputs.models.join(',');
 | 
			
		||||
    let res;
 | 
			
		||||
    if (isEdit) {
 | 
			
		||||
      res = await API.put(`/api/token/`, { ...localInputs, id: parseInt(tokenId) });
 | 
			
		||||
      res = await API.put(`/api/token/`, {
 | 
			
		||||
        ...localInputs,
 | 
			
		||||
        id: parseInt(tokenId),
 | 
			
		||||
      });
 | 
			
		||||
    } else {
 | 
			
		||||
      res = await API.post(`/api/token/`, localInputs);
 | 
			
		||||
    }
 | 
			
		||||
@@ -121,98 +137,142 @@ const EditToken = () => {
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <Segment loading={loading}>
 | 
			
		||||
        <Header as='h3'>{isEdit ? '更新令牌信息' : '创建新的令牌'}</Header>
 | 
			
		||||
        <Form autoComplete='new-password'>
 | 
			
		||||
          <Form.Field>
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label='名称'
 | 
			
		||||
              name='name'
 | 
			
		||||
              placeholder={'请输入名称'}
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              value={name}
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              required={!isEdit}
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Field>
 | 
			
		||||
          <Form.Field>
 | 
			
		||||
            <Form.Dropdown
 | 
			
		||||
              label='模型范围'
 | 
			
		||||
              placeholder={'请选择允许使用的模型,留空则不进行限制'}
 | 
			
		||||
              name='models'
 | 
			
		||||
              fluid
 | 
			
		||||
              multiple
 | 
			
		||||
              search
 | 
			
		||||
              onLabelClick={(e, { value }) => {
 | 
			
		||||
                copy(value).then();
 | 
			
		||||
    <div className='dashboard-container'>
 | 
			
		||||
      <Card fluid className='chart-card'>
 | 
			
		||||
        <Card.Content>
 | 
			
		||||
          <Card.Header className='header'>
 | 
			
		||||
            {isEdit ? '更新令牌信息' : '创建新的令牌'}
 | 
			
		||||
          </Card.Header>
 | 
			
		||||
          <Form loading={loading} autoComplete='new-password'>
 | 
			
		||||
            <Form.Field>
 | 
			
		||||
              <Form.Input
 | 
			
		||||
                label='名称'
 | 
			
		||||
                name='name'
 | 
			
		||||
                placeholder={'请输入名称'}
 | 
			
		||||
                onChange={handleInputChange}
 | 
			
		||||
                value={name}
 | 
			
		||||
                autoComplete='new-password'
 | 
			
		||||
                required={!isEdit}
 | 
			
		||||
              />
 | 
			
		||||
            </Form.Field>
 | 
			
		||||
            <Form.Field>
 | 
			
		||||
              <Form.Dropdown
 | 
			
		||||
                label='模型范围'
 | 
			
		||||
                placeholder={'请选择允许使用的模型,留空则不进行限制'}
 | 
			
		||||
                name='models'
 | 
			
		||||
                fluid
 | 
			
		||||
                multiple
 | 
			
		||||
                search
 | 
			
		||||
                onLabelClick={(e, { value }) => {
 | 
			
		||||
                  copy(value).then();
 | 
			
		||||
                }}
 | 
			
		||||
                selection
 | 
			
		||||
                onChange={handleInputChange}
 | 
			
		||||
                value={inputs.models}
 | 
			
		||||
                autoComplete='new-password'
 | 
			
		||||
                options={modelOptions}
 | 
			
		||||
              />
 | 
			
		||||
            </Form.Field>
 | 
			
		||||
            <Form.Field>
 | 
			
		||||
              <Form.Input
 | 
			
		||||
                label='IP 限制'
 | 
			
		||||
                name='subnet'
 | 
			
		||||
                placeholder={
 | 
			
		||||
                  '请输入允许访问的网段,例如:192.168.0.0/24,请使用英文逗号分隔多个网段'
 | 
			
		||||
                }
 | 
			
		||||
                onChange={handleInputChange}
 | 
			
		||||
                value={inputs.subnet}
 | 
			
		||||
                autoComplete='new-password'
 | 
			
		||||
              />
 | 
			
		||||
            </Form.Field>
 | 
			
		||||
            <Form.Field>
 | 
			
		||||
              <Form.Input
 | 
			
		||||
                label='过期时间'
 | 
			
		||||
                name='expired_time'
 | 
			
		||||
                placeholder={
 | 
			
		||||
                  '请输入过期时间,格式为 yyyy-MM-dd HH:mm:ss,-1 表示无限制'
 | 
			
		||||
                }
 | 
			
		||||
                onChange={handleInputChange}
 | 
			
		||||
                value={expired_time}
 | 
			
		||||
                autoComplete='new-password'
 | 
			
		||||
                type='datetime-local'
 | 
			
		||||
              />
 | 
			
		||||
            </Form.Field>
 | 
			
		||||
            <div style={{ lineHeight: '40px' }}>
 | 
			
		||||
              <Button
 | 
			
		||||
                type={'button'}
 | 
			
		||||
                onClick={() => {
 | 
			
		||||
                  setExpiredTime(0, 0, 0, 0);
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                永不过期
 | 
			
		||||
              </Button>
 | 
			
		||||
              <Button
 | 
			
		||||
                type={'button'}
 | 
			
		||||
                onClick={() => {
 | 
			
		||||
                  setExpiredTime(1, 0, 0, 0);
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                一个月后过期
 | 
			
		||||
              </Button>
 | 
			
		||||
              <Button
 | 
			
		||||
                type={'button'}
 | 
			
		||||
                onClick={() => {
 | 
			
		||||
                  setExpiredTime(0, 1, 0, 0);
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                一天后过期
 | 
			
		||||
              </Button>
 | 
			
		||||
              <Button
 | 
			
		||||
                type={'button'}
 | 
			
		||||
                onClick={() => {
 | 
			
		||||
                  setExpiredTime(0, 0, 1, 0);
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                一小时后过期
 | 
			
		||||
              </Button>
 | 
			
		||||
              <Button
 | 
			
		||||
                type={'button'}
 | 
			
		||||
                onClick={() => {
 | 
			
		||||
                  setExpiredTime(0, 0, 0, 1);
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                一分钟后过期
 | 
			
		||||
              </Button>
 | 
			
		||||
            </div>
 | 
			
		||||
            <Message>
 | 
			
		||||
              注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。
 | 
			
		||||
            </Message>
 | 
			
		||||
            <Form.Field>
 | 
			
		||||
              <Form.Input
 | 
			
		||||
                label={`额度${renderQuotaWithPrompt(remain_quota)}`}
 | 
			
		||||
                name='remain_quota'
 | 
			
		||||
                placeholder={'请输入额度'}
 | 
			
		||||
                onChange={handleInputChange}
 | 
			
		||||
                value={remain_quota}
 | 
			
		||||
                autoComplete='new-password'
 | 
			
		||||
                type='number'
 | 
			
		||||
                disabled={unlimited_quota}
 | 
			
		||||
              />
 | 
			
		||||
            </Form.Field>
 | 
			
		||||
            <Button
 | 
			
		||||
              type={'button'}
 | 
			
		||||
              onClick={() => {
 | 
			
		||||
                setUnlimitedQuota();
 | 
			
		||||
              }}
 | 
			
		||||
              selection
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              value={inputs.models}
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              options={modelOptions}
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Field>
 | 
			
		||||
          <Form.Field>
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label='IP 限制'
 | 
			
		||||
              name='subnet'
 | 
			
		||||
              placeholder={'请输入允许访问的网段,例如:192.168.0.0/24,请使用英文逗号分隔多个网段'}
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              value={inputs.subnet}
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Field>
 | 
			
		||||
          <Form.Field>
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label='过期时间'
 | 
			
		||||
              name='expired_time'
 | 
			
		||||
              placeholder={'请输入过期时间,格式为 yyyy-MM-dd HH:mm:ss,-1 表示无限制'}
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              value={expired_time}
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              type='datetime-local'
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Field>
 | 
			
		||||
          <div style={{ lineHeight: '40px' }}>
 | 
			
		||||
            <Button type={'button'} onClick={() => {
 | 
			
		||||
              setExpiredTime(0, 0, 0, 0);
 | 
			
		||||
            }}>永不过期</Button>
 | 
			
		||||
            <Button type={'button'} onClick={() => {
 | 
			
		||||
              setExpiredTime(1, 0, 0, 0);
 | 
			
		||||
            }}>一个月后过期</Button>
 | 
			
		||||
            <Button type={'button'} onClick={() => {
 | 
			
		||||
              setExpiredTime(0, 1, 0, 0);
 | 
			
		||||
            }}>一天后过期</Button>
 | 
			
		||||
            <Button type={'button'} onClick={() => {
 | 
			
		||||
              setExpiredTime(0, 0, 1, 0);
 | 
			
		||||
            }}>一小时后过期</Button>
 | 
			
		||||
            <Button type={'button'} onClick={() => {
 | 
			
		||||
              setExpiredTime(0, 0, 0, 1);
 | 
			
		||||
            }}>一分钟后过期</Button>
 | 
			
		||||
          </div>
 | 
			
		||||
          <Message>注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。</Message>
 | 
			
		||||
          <Form.Field>
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label={`额度${renderQuotaWithPrompt(remain_quota)}`}
 | 
			
		||||
              name='remain_quota'
 | 
			
		||||
              placeholder={'请输入额度'}
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              value={remain_quota}
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              type='number'
 | 
			
		||||
              disabled={unlimited_quota}
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Field>
 | 
			
		||||
          <Button type={'button'} onClick={() => {
 | 
			
		||||
            setUnlimitedQuota();
 | 
			
		||||
          }}>{unlimited_quota ? '取消无限额度' : '设为无限额度'}</Button>
 | 
			
		||||
          <Button floated='right' positive onClick={submit}>提交</Button>
 | 
			
		||||
          <Button floated='right' onClick={handleCancel}>取消</Button>
 | 
			
		||||
        </Form>
 | 
			
		||||
      </Segment>
 | 
			
		||||
    </>
 | 
			
		||||
            >
 | 
			
		||||
              {unlimited_quota ? '取消无限额度' : '设为无限额度'}
 | 
			
		||||
            </Button>
 | 
			
		||||
            <Button floated='right' positive onClick={submit}>
 | 
			
		||||
              提交
 | 
			
		||||
            </Button>
 | 
			
		||||
            <Button floated='right' onClick={handleCancel}>
 | 
			
		||||
              取消
 | 
			
		||||
            </Button>
 | 
			
		||||
          </Form>
 | 
			
		||||
        </Card.Content>
 | 
			
		||||
      </Card>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,16 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { Segment, Header } from 'semantic-ui-react';
 | 
			
		||||
import { Card } from 'semantic-ui-react';
 | 
			
		||||
import TokensTable from '../../components/TokensTable';
 | 
			
		||||
 | 
			
		||||
const Token = () => (
 | 
			
		||||
  <>
 | 
			
		||||
    <Segment>
 | 
			
		||||
      <Header as='h3'>我的令牌</Header>
 | 
			
		||||
      <TokensTable/>
 | 
			
		||||
    </Segment>
 | 
			
		||||
  </>
 | 
			
		||||
  <div className='dashboard-container'>
 | 
			
		||||
    <Card fluid className='chart-card'>
 | 
			
		||||
      <Card.Content>
 | 
			
		||||
        <Card.Header className='header'>令牌管理</Card.Header>
 | 
			
		||||
        <TokensTable />
 | 
			
		||||
      </Card.Content>
 | 
			
		||||
    </Card>
 | 
			
		||||
  </div>
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export default Token;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,13 @@
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import { Button, Form, Grid, Header, Segment, Statistic } from 'semantic-ui-react';
 | 
			
		||||
import {
 | 
			
		||||
  Button,
 | 
			
		||||
  Form,
 | 
			
		||||
  Grid,
 | 
			
		||||
  Header,
 | 
			
		||||
  Card,
 | 
			
		||||
  Statistic,
 | 
			
		||||
  Divider,
 | 
			
		||||
} from 'semantic-ui-react';
 | 
			
		||||
import { API, showError, showInfo, showSuccess } from '../../helpers';
 | 
			
		||||
import { renderQuota } from '../../helpers/render';
 | 
			
		||||
 | 
			
		||||
@@ -12,13 +20,13 @@ const TopUp = () => {
 | 
			
		||||
 | 
			
		||||
  const topUp = async () => {
 | 
			
		||||
    if (redemptionCode === '') {
 | 
			
		||||
      showInfo('请输入充值码!')
 | 
			
		||||
      showInfo('请输入兑换码!');
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    setIsSubmitting(true);
 | 
			
		||||
    try {
 | 
			
		||||
      const res = await API.post('/api/user/topup', {
 | 
			
		||||
        key: redemptionCode
 | 
			
		||||
        key: redemptionCode,
 | 
			
		||||
      });
 | 
			
		||||
      const { success, message, data } = res.data;
 | 
			
		||||
      if (success) {
 | 
			
		||||
@@ -45,23 +53,23 @@ const TopUp = () => {
 | 
			
		||||
    let url = new URL(topUpLink);
 | 
			
		||||
    let username = user.username;
 | 
			
		||||
    let user_id = user.id;
 | 
			
		||||
    // add  username and user_id to the topup link
 | 
			
		||||
    // add username and user_id to the topup link
 | 
			
		||||
    url.searchParams.append('username', username);
 | 
			
		||||
    url.searchParams.append('user_id', user_id);
 | 
			
		||||
    url.searchParams.append('transaction_id', crypto.randomUUID());
 | 
			
		||||
    window.open(url.toString(), '_blank');
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const getUserQuota = async ()=>{
 | 
			
		||||
    let res  = await API.get(`/api/user/self`);
 | 
			
		||||
    const {success, message, data} = res.data;
 | 
			
		||||
  const getUserQuota = async () => {
 | 
			
		||||
    let res = await API.get(`/api/user/self`);
 | 
			
		||||
    const { success, message, data } = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
      setUserQuota(data.quota);
 | 
			
		||||
      setUser(data);
 | 
			
		||||
    } else {
 | 
			
		||||
      showError(message);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    let status = localStorage.getItem('status');
 | 
			
		||||
@@ -75,37 +83,165 @@ const TopUp = () => {
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <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>
 | 
			
		||||
    <div className='dashboard-container'>
 | 
			
		||||
      <Card fluid className='chart-card'>
 | 
			
		||||
        <Card.Content>
 | 
			
		||||
          <Card.Header>
 | 
			
		||||
            <Header as='h2'>充值中心</Header>
 | 
			
		||||
          </Card.Header>
 | 
			
		||||
 | 
			
		||||
          <Grid columns={2} stackable>
 | 
			
		||||
            <Grid.Column>
 | 
			
		||||
              <Card
 | 
			
		||||
                fluid
 | 
			
		||||
                style={{
 | 
			
		||||
                  height: '100%',
 | 
			
		||||
                  boxShadow: '0 1px 3px rgba(0,0,0,0.1)',
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                <Card.Content
 | 
			
		||||
                  style={{
 | 
			
		||||
                    height: '100%',
 | 
			
		||||
                    display: 'flex',
 | 
			
		||||
                    flexDirection: 'column',
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  <Card.Header>
 | 
			
		||||
                    <Header as='h3' style={{ color: '#2185d0', margin: '1em' }}>
 | 
			
		||||
                      <i className='credit card icon'></i>
 | 
			
		||||
                      获取兑换码
 | 
			
		||||
                    </Header>
 | 
			
		||||
                  </Card.Header>
 | 
			
		||||
                  <Card.Description
 | 
			
		||||
                    style={{
 | 
			
		||||
                      flex: 1,
 | 
			
		||||
                      display: 'flex',
 | 
			
		||||
                      flexDirection: 'column',
 | 
			
		||||
                    }}
 | 
			
		||||
                  >
 | 
			
		||||
                    <div
 | 
			
		||||
                      style={{
 | 
			
		||||
                        flex: 1,
 | 
			
		||||
                        display: 'flex',
 | 
			
		||||
                        flexDirection: 'column',
 | 
			
		||||
                        justifyContent: 'space-between',
 | 
			
		||||
                      }}
 | 
			
		||||
                    >
 | 
			
		||||
                      <div style={{ textAlign: 'center', paddingTop: '1em' }}>
 | 
			
		||||
                        <Statistic>
 | 
			
		||||
                          <Statistic.Value style={{ color: '#2185d0' }}>
 | 
			
		||||
                            {renderQuota(userQuota)}
 | 
			
		||||
                          </Statistic.Value>
 | 
			
		||||
                          <Statistic.Label>当前可用额度</Statistic.Label>
 | 
			
		||||
                        </Statistic>
 | 
			
		||||
                      </div>
 | 
			
		||||
 | 
			
		||||
                      <div
 | 
			
		||||
                        style={{ textAlign: 'center', paddingBottom: '1em' }}
 | 
			
		||||
                      >
 | 
			
		||||
                        <Button
 | 
			
		||||
                          primary
 | 
			
		||||
                          size='large'
 | 
			
		||||
                          onClick={openTopUpLink}
 | 
			
		||||
                          style={{ width: '80%' }}
 | 
			
		||||
                        >
 | 
			
		||||
                          立即获取兑换码
 | 
			
		||||
                        </Button>
 | 
			
		||||
                      </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </Card.Description>
 | 
			
		||||
                </Card.Content>
 | 
			
		||||
              </Card>
 | 
			
		||||
            </Grid.Column>
 | 
			
		||||
 | 
			
		||||
            <Grid.Column>
 | 
			
		||||
              <Card
 | 
			
		||||
                fluid
 | 
			
		||||
                style={{
 | 
			
		||||
                  height: '100%',
 | 
			
		||||
                  boxShadow: '0 1px 3px rgba(0,0,0,0.1)',
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                <Card.Content
 | 
			
		||||
                  style={{
 | 
			
		||||
                    height: '100%',
 | 
			
		||||
                    display: 'flex',
 | 
			
		||||
                    flexDirection: 'column',
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  <Card.Header>
 | 
			
		||||
                    <Header as='h3' style={{ color: '#21ba45', margin: '1em' }}>
 | 
			
		||||
                      <i className='ticket alternate icon'></i>
 | 
			
		||||
                      兑换码充值
 | 
			
		||||
                    </Header>
 | 
			
		||||
                  </Card.Header>
 | 
			
		||||
                  <Card.Description
 | 
			
		||||
                    style={{
 | 
			
		||||
                      flex: 1,
 | 
			
		||||
                      display: 'flex',
 | 
			
		||||
                      flexDirection: 'column',
 | 
			
		||||
                    }}
 | 
			
		||||
                  >
 | 
			
		||||
                    <div
 | 
			
		||||
                      style={{
 | 
			
		||||
                        flex: 1,
 | 
			
		||||
                        display: 'flex',
 | 
			
		||||
                        flexDirection: 'column',
 | 
			
		||||
                        justifyContent: 'space-between',
 | 
			
		||||
                      }}
 | 
			
		||||
                    >
 | 
			
		||||
                      <Form.Input
 | 
			
		||||
                        fluid
 | 
			
		||||
                        icon='key'
 | 
			
		||||
                        iconPosition='left'
 | 
			
		||||
                        placeholder='请输入兑换码'
 | 
			
		||||
                        value={redemptionCode}
 | 
			
		||||
                        onChange={(e) => {
 | 
			
		||||
                          setRedemptionCode(e.target.value);
 | 
			
		||||
                        }}
 | 
			
		||||
                        onPaste={(e) => {
 | 
			
		||||
                          e.preventDefault();
 | 
			
		||||
                          const pastedText = e.clipboardData.getData('text');
 | 
			
		||||
                          setRedemptionCode(pastedText.trim());
 | 
			
		||||
                        }}
 | 
			
		||||
                        action={
 | 
			
		||||
                          <Button
 | 
			
		||||
                            icon='paste'
 | 
			
		||||
                            content='粘贴'
 | 
			
		||||
                            onClick={async () => {
 | 
			
		||||
                              try {
 | 
			
		||||
                                const text =
 | 
			
		||||
                                  await navigator.clipboard.readText();
 | 
			
		||||
                                setRedemptionCode(text.trim());
 | 
			
		||||
                              } catch (err) {
 | 
			
		||||
                                showError('无法访问剪贴板,请手动粘贴');
 | 
			
		||||
                              }
 | 
			
		||||
                            }}
 | 
			
		||||
                          />
 | 
			
		||||
                        }
 | 
			
		||||
                      />
 | 
			
		||||
 | 
			
		||||
                      <div style={{ paddingBottom: '1em' }}>
 | 
			
		||||
                        <Button
 | 
			
		||||
                          color='green'
 | 
			
		||||
                          fluid
 | 
			
		||||
                          size='large'
 | 
			
		||||
                          onClick={topUp}
 | 
			
		||||
                          loading={isSubmitting}
 | 
			
		||||
                          disabled={isSubmitting}
 | 
			
		||||
                        >
 | 
			
		||||
                          {isSubmitting ? '兑换中...' : '立即兑换'}
 | 
			
		||||
                        </Button>
 | 
			
		||||
                      </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </Card.Description>
 | 
			
		||||
                </Card.Content>
 | 
			
		||||
              </Card>
 | 
			
		||||
            </Grid.Column>
 | 
			
		||||
          </Grid>
 | 
			
		||||
        </Card.Content>
 | 
			
		||||
      </Card>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import { Button, Form, Header, Segment } from 'semantic-ui-react';
 | 
			
		||||
import { Button, Form, Card } from 'semantic-ui-react';
 | 
			
		||||
import { useParams, useNavigate } from 'react-router-dom';
 | 
			
		||||
import { API, showError, showSuccess } from '../../helpers';
 | 
			
		||||
import { renderQuota, renderQuotaWithPrompt } from '../../helpers/render';
 | 
			
		||||
@@ -16,30 +16,40 @@ const EditUser = () => {
 | 
			
		||||
    wechat_id: '',
 | 
			
		||||
    email: '',
 | 
			
		||||
    quota: 0,
 | 
			
		||||
    group: 'default'
 | 
			
		||||
    group: 'default',
 | 
			
		||||
  });
 | 
			
		||||
  const [groupOptions, setGroupOptions] = useState([]);
 | 
			
		||||
  const { username, display_name, password, github_id, wechat_id, email, quota, group } =
 | 
			
		||||
    inputs;
 | 
			
		||||
  const {
 | 
			
		||||
    username,
 | 
			
		||||
    display_name,
 | 
			
		||||
    password,
 | 
			
		||||
    github_id,
 | 
			
		||||
    wechat_id,
 | 
			
		||||
    email,
 | 
			
		||||
    quota,
 | 
			
		||||
    group,
 | 
			
		||||
  } = inputs;
 | 
			
		||||
  const handleInputChange = (e, { name, value }) => {
 | 
			
		||||
    setInputs((inputs) => ({ ...inputs, [name]: value }));
 | 
			
		||||
  };
 | 
			
		||||
  const fetchGroups = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      let res = await API.get(`/api/group/`);
 | 
			
		||||
      setGroupOptions(res.data.data.map((group) => ({
 | 
			
		||||
        key: group,
 | 
			
		||||
        text: group,
 | 
			
		||||
        value: group,
 | 
			
		||||
      })));
 | 
			
		||||
      setGroupOptions(
 | 
			
		||||
        res.data.data.map((group) => ({
 | 
			
		||||
          key: group,
 | 
			
		||||
          text: group,
 | 
			
		||||
          value: group,
 | 
			
		||||
        }))
 | 
			
		||||
      );
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      showError(error.message);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
  const navigate = useNavigate();
 | 
			
		||||
  const handleCancel = () => {
 | 
			
		||||
    navigate("/setting");
 | 
			
		||||
  }
 | 
			
		||||
    navigate('/setting');
 | 
			
		||||
  };
 | 
			
		||||
  const loadUser = async () => {
 | 
			
		||||
    let res = undefined;
 | 
			
		||||
    if (userId) {
 | 
			
		||||
@@ -83,107 +93,113 @@ const EditUser = () => {
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <Segment loading={loading}>
 | 
			
		||||
        <Header as='h3'>更新用户信息</Header>
 | 
			
		||||
        <Form autoComplete='new-password'>
 | 
			
		||||
          <Form.Field>
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label='用户名'
 | 
			
		||||
              name='username'
 | 
			
		||||
              placeholder={'请输入新的用户名'}
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              value={username}
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Field>
 | 
			
		||||
          <Form.Field>
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label='密码'
 | 
			
		||||
              name='password'
 | 
			
		||||
              type={'password'}
 | 
			
		||||
              placeholder={'请输入新的密码,最短 8 位'}
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              value={password}
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Field>
 | 
			
		||||
          <Form.Field>
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label='显示名称'
 | 
			
		||||
              name='display_name'
 | 
			
		||||
              placeholder={'请输入新的显示名称'}
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              value={display_name}
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Field>
 | 
			
		||||
          {
 | 
			
		||||
            userId && <>
 | 
			
		||||
              <Form.Field>
 | 
			
		||||
                <Form.Dropdown
 | 
			
		||||
                  label='分组'
 | 
			
		||||
                  placeholder={'请选择分组'}
 | 
			
		||||
                  name='group'
 | 
			
		||||
                  fluid
 | 
			
		||||
                  search
 | 
			
		||||
                  selection
 | 
			
		||||
                  allowAdditions
 | 
			
		||||
                  additionLabel={'请在系统设置页面编辑分组倍率以添加新的分组:'}
 | 
			
		||||
                  onChange={handleInputChange}
 | 
			
		||||
                  value={inputs.group}
 | 
			
		||||
                  autoComplete='new-password'
 | 
			
		||||
                  options={groupOptions}
 | 
			
		||||
                />
 | 
			
		||||
              </Form.Field>
 | 
			
		||||
              <Form.Field>
 | 
			
		||||
                <Form.Input
 | 
			
		||||
                  label={`剩余额度${renderQuotaWithPrompt(quota)}`}
 | 
			
		||||
                  name='quota'
 | 
			
		||||
                  placeholder={'请输入新的剩余额度'}
 | 
			
		||||
                  onChange={handleInputChange}
 | 
			
		||||
                  value={quota}
 | 
			
		||||
                  type={'number'}
 | 
			
		||||
                  autoComplete='new-password'
 | 
			
		||||
                />
 | 
			
		||||
              </Form.Field>
 | 
			
		||||
            </>
 | 
			
		||||
          }
 | 
			
		||||
          <Form.Field>
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label='已绑定的 GitHub 账户'
 | 
			
		||||
              name='github_id'
 | 
			
		||||
              value={github_id}
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              placeholder='此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
 | 
			
		||||
              readOnly
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Field>
 | 
			
		||||
          <Form.Field>
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label='已绑定的微信账户'
 | 
			
		||||
              name='wechat_id'
 | 
			
		||||
              value={wechat_id}
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              placeholder='此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
 | 
			
		||||
              readOnly
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Field>
 | 
			
		||||
          <Form.Field>
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label='已绑定的邮箱账户'
 | 
			
		||||
              name='email'
 | 
			
		||||
              value={email}
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              placeholder='此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
 | 
			
		||||
              readOnly
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Field>
 | 
			
		||||
          <Button onClick={handleCancel}>取消</Button>
 | 
			
		||||
          <Button positive onClick={submit}>提交</Button>
 | 
			
		||||
        </Form>
 | 
			
		||||
      </Segment>
 | 
			
		||||
    </>
 | 
			
		||||
    <div className='dashboard-container'>
 | 
			
		||||
      <Card fluid className='chart-card'>
 | 
			
		||||
        <Card.Content>
 | 
			
		||||
          <Card.Header className='header'>更新用户信息</Card.Header>
 | 
			
		||||
          <Form loading={loading} autoComplete='new-password'>
 | 
			
		||||
            <Form.Field>
 | 
			
		||||
              <Form.Input
 | 
			
		||||
                label='用户名'
 | 
			
		||||
                name='username'
 | 
			
		||||
                placeholder={'请输入新的用户名'}
 | 
			
		||||
                onChange={handleInputChange}
 | 
			
		||||
                value={username}
 | 
			
		||||
                autoComplete='new-password'
 | 
			
		||||
              />
 | 
			
		||||
            </Form.Field>
 | 
			
		||||
            <Form.Field>
 | 
			
		||||
              <Form.Input
 | 
			
		||||
                label='密码'
 | 
			
		||||
                name='password'
 | 
			
		||||
                type={'password'}
 | 
			
		||||
                placeholder={'请输入新的密码,最短 8 位'}
 | 
			
		||||
                onChange={handleInputChange}
 | 
			
		||||
                value={password}
 | 
			
		||||
                autoComplete='new-password'
 | 
			
		||||
              />
 | 
			
		||||
            </Form.Field>
 | 
			
		||||
            <Form.Field>
 | 
			
		||||
              <Form.Input
 | 
			
		||||
                label='显示名称'
 | 
			
		||||
                name='display_name'
 | 
			
		||||
                placeholder={'请输入新的显示名称'}
 | 
			
		||||
                onChange={handleInputChange}
 | 
			
		||||
                value={display_name}
 | 
			
		||||
                autoComplete='new-password'
 | 
			
		||||
              />
 | 
			
		||||
            </Form.Field>
 | 
			
		||||
            {userId && (
 | 
			
		||||
              <>
 | 
			
		||||
                <Form.Field>
 | 
			
		||||
                  <Form.Dropdown
 | 
			
		||||
                    label='分组'
 | 
			
		||||
                    placeholder={'请选择分组'}
 | 
			
		||||
                    name='group'
 | 
			
		||||
                    fluid
 | 
			
		||||
                    search
 | 
			
		||||
                    selection
 | 
			
		||||
                    allowAdditions
 | 
			
		||||
                    additionLabel={
 | 
			
		||||
                      '请在系统设置页面编辑分组倍率以添加新的分组:'
 | 
			
		||||
                    }
 | 
			
		||||
                    onChange={handleInputChange}
 | 
			
		||||
                    value={inputs.group}
 | 
			
		||||
                    autoComplete='new-password'
 | 
			
		||||
                    options={groupOptions}
 | 
			
		||||
                  />
 | 
			
		||||
                </Form.Field>
 | 
			
		||||
                <Form.Field>
 | 
			
		||||
                  <Form.Input
 | 
			
		||||
                    label={`剩余额度${renderQuotaWithPrompt(quota)}`}
 | 
			
		||||
                    name='quota'
 | 
			
		||||
                    placeholder={'请输入新的剩余额度'}
 | 
			
		||||
                    onChange={handleInputChange}
 | 
			
		||||
                    value={quota}
 | 
			
		||||
                    type={'number'}
 | 
			
		||||
                    autoComplete='new-password'
 | 
			
		||||
                  />
 | 
			
		||||
                </Form.Field>
 | 
			
		||||
              </>
 | 
			
		||||
            )}
 | 
			
		||||
            <Form.Field>
 | 
			
		||||
              <Form.Input
 | 
			
		||||
                label='已绑定的 GitHub 账户'
 | 
			
		||||
                name='github_id'
 | 
			
		||||
                value={github_id}
 | 
			
		||||
                autoComplete='new-password'
 | 
			
		||||
                placeholder='此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
 | 
			
		||||
                readOnly
 | 
			
		||||
              />
 | 
			
		||||
            </Form.Field>
 | 
			
		||||
            <Form.Field>
 | 
			
		||||
              <Form.Input
 | 
			
		||||
                label='已绑定的微信账户'
 | 
			
		||||
                name='wechat_id'
 | 
			
		||||
                value={wechat_id}
 | 
			
		||||
                autoComplete='new-password'
 | 
			
		||||
                placeholder='此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
 | 
			
		||||
                readOnly
 | 
			
		||||
              />
 | 
			
		||||
            </Form.Field>
 | 
			
		||||
            <Form.Field>
 | 
			
		||||
              <Form.Input
 | 
			
		||||
                label='已绑定的邮箱账户'
 | 
			
		||||
                name='email'
 | 
			
		||||
                value={email}
 | 
			
		||||
                autoComplete='new-password'
 | 
			
		||||
                placeholder='此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
 | 
			
		||||
                readOnly
 | 
			
		||||
              />
 | 
			
		||||
            </Form.Field>
 | 
			
		||||
            <Button onClick={handleCancel}>取消</Button>
 | 
			
		||||
            <Button positive onClick={submit}>
 | 
			
		||||
              提交
 | 
			
		||||
            </Button>
 | 
			
		||||
          </Form>
 | 
			
		||||
        </Card.Content>
 | 
			
		||||
      </Card>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,16 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { Segment, Header } from 'semantic-ui-react';
 | 
			
		||||
import { Card } from 'semantic-ui-react';
 | 
			
		||||
import UsersTable from '../../components/UsersTable';
 | 
			
		||||
 | 
			
		||||
const User = () => (
 | 
			
		||||
  <>
 | 
			
		||||
    <Segment>
 | 
			
		||||
      <Header as='h3'>管理用户</Header>
 | 
			
		||||
      <UsersTable/>
 | 
			
		||||
    </Segment>
 | 
			
		||||
  </>
 | 
			
		||||
  <div className='dashboard-container'>
 | 
			
		||||
    <Card fluid className='chart-card'>
 | 
			
		||||
      <Card.Content>
 | 
			
		||||
        <Card.Header className='header'>用户管理</Card.Header>
 | 
			
		||||
        <UsersTable />
 | 
			
		||||
      </Card.Content>
 | 
			
		||||
    </Card>
 | 
			
		||||
  </div>
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export default User;
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user